From 0a748161ba1ea03497e5daa2b0df669a4e6ee61f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Feb 2026 13:17:06 +0000 Subject: [PATCH] archive: add 5 repo prompt(s) [skip ci] --- archive/Arisego/UnrealLive2D.txt | 133986 +++++++ archive/JanKXSKI/AssetTutorialPlugin.txt | 718 + archive/TimmHess/UnrealImageCapture.txt | 1482 + archive/nxrighthere/UnrealCLR.txt | 65804 ++++ archive/rdeioris/LuaMachine.txt | 15543 + .../sideeffects/HoudiniEngineForUnreal.txt | 302606 +++++++++++++++ archive/stungeye/UE5-With-Dear-ImGui.txt | 66046 ++++ 7 files changed, 586185 insertions(+) create mode 100644 archive/Arisego/UnrealLive2D.txt create mode 100644 archive/JanKXSKI/AssetTutorialPlugin.txt create mode 100644 archive/TimmHess/UnrealImageCapture.txt create mode 100644 archive/nxrighthere/UnrealCLR.txt create mode 100644 archive/rdeioris/LuaMachine.txt create mode 100644 archive/sideeffects/HoudiniEngineForUnreal.txt create mode 100644 archive/stungeye/UE5-With-Dear-ImGui.txt diff --git a/archive/Arisego/UnrealLive2D.txt b/archive/Arisego/UnrealLive2D.txt new file mode 100644 index 00000000..6d067629 --- /dev/null +++ b/archive/Arisego/UnrealLive2D.txt @@ -0,0 +1,133986 @@ +Project Path: arc_Arisego_UnrealLive2D_e1s9ydbs + +Source Tree: + +```txt +arc_Arisego_UnrealLive2D_e1s9ydbs +├── Content +│ ├── BpCubismPc.uasset +│ ├── CubismMap.umap +│ ├── CubismTest.uasset +│ ├── GameMode_Cubism.uasset +│ ├── HUD_CubismTest.uasset +│ ├── Mat_RT.uasset +│ ├── RT_00.uasset +│ ├── Samples +│ │ ├── Close.png +│ │ ├── Haru +│ │ │ ├── Haru.2048 +│ │ │ │ ├── texture_00.png +│ │ │ │ └── texture_01.png +│ │ │ ├── Haru.moc3 +│ │ │ ├── Haru.model3.json +│ │ │ ├── Haru.pose3.json +│ │ │ ├── Haru.userdata3.json +│ │ │ ├── expressions +│ │ │ │ ├── F01.exp3.json +│ │ │ │ ├── F02.exp3.json +│ │ │ │ ├── F03.exp3.json +│ │ │ │ ├── F04.exp3.json +│ │ │ │ ├── F05.exp3.json +│ │ │ │ ├── F06.exp3.json +│ │ │ │ ├── F07.exp3.json +│ │ │ │ └── F08.exp3.json +│ │ │ └── motions +│ │ │ ├── haru_g_idle.motion3.json +│ │ │ ├── haru_g_m01.motion3.json +│ │ │ ├── haru_g_m02.motion3.json +│ │ │ ├── haru_g_m03.motion3.json +│ │ │ ├── haru_g_m04.motion3.json +│ │ │ ├── haru_g_m05.motion3.json +│ │ │ ├── haru_g_m06.motion3.json +│ │ │ ├── haru_g_m07.motion3.json +│ │ │ ├── haru_g_m08.motion3.json +│ │ │ ├── haru_g_m09.motion3.json +│ │ │ ├── haru_g_m10.motion3.json +│ │ │ ├── haru_g_m11.motion3.json +│ │ │ ├── haru_g_m12.motion3.json +│ │ │ ├── haru_g_m13.motion3.json +│ │ │ ├── haru_g_m14.motion3.json +│ │ │ ├── haru_g_m15.motion3.json +│ │ │ ├── haru_g_m16.motion3.json +│ │ │ ├── haru_g_m17.motion3.json +│ │ │ ├── haru_g_m18.motion3.json +│ │ │ ├── haru_g_m19.motion3.json +│ │ │ ├── haru_g_m20.motion3.json +│ │ │ ├── haru_g_m21.motion3.json +│ │ │ ├── haru_g_m22.motion3.json +│ │ │ ├── haru_g_m23.motion3.json +│ │ │ ├── haru_g_m24.motion3.json +│ │ │ ├── haru_g_m25.motion3.json +│ │ │ └── haru_g_m26.motion3.json +│ │ ├── Hiyori +│ │ │ ├── Hiyori.2048 +│ │ │ │ ├── texture_00.png +│ │ │ │ └── texture_01.png +│ │ │ ├── Hiyori.moc3 +│ │ │ ├── Hiyori.model3.json +│ │ │ ├── Hiyori.physics3.json +│ │ │ ├── Hiyori.pose3.json +│ │ │ ├── Hiyori.userdata3.json +│ │ │ └── motions +│ │ │ ├── Hiyori_m01.motion3.json +│ │ │ ├── Hiyori_m02.motion3.json +│ │ │ ├── Hiyori_m03.motion3.json +│ │ │ ├── Hiyori_m04.motion3.json +│ │ │ ├── Hiyori_m05.motion3.json +│ │ │ ├── Hiyori_m06.motion3.json +│ │ │ ├── Hiyori_m07.motion3.json +│ │ │ ├── Hiyori_m08.motion3.json +│ │ │ ├── Hiyori_m09.motion3.json +│ │ │ └── Hiyori_m10.motion3.json +│ │ ├── Mark +│ │ │ ├── Mark.2048 +│ │ │ │ └── texture_00.png +│ │ │ ├── Mark.moc3 +│ │ │ ├── Mark.model3.json +│ │ │ ├── Mark.physics3.json +│ │ │ ├── Mark.userdata3.json +│ │ │ └── motions +│ │ │ ├── mark_m01.motion3.json +│ │ │ ├── mark_m02.motion3.json +│ │ │ ├── mark_m03.motion3.json +│ │ │ ├── mark_m04.motion3.json +│ │ │ ├── mark_m05.motion3.json +│ │ │ └── mark_m06.motion3.json +│ │ ├── Natori +│ │ │ ├── Natori.2048 +│ │ │ │ └── texture_00.png +│ │ │ ├── Natori.moc3 +│ │ │ ├── Natori.model3.json +│ │ │ ├── Natori.physics3.json +│ │ │ ├── Natori.pose3.json +│ │ │ ├── exp +│ │ │ │ ├── exp_00.exp3.json +│ │ │ │ ├── exp_01.exp3.json +│ │ │ │ ├── exp_02.exp3.json +│ │ │ │ ├── exp_03.exp3.json +│ │ │ │ ├── exp_04.exp3.json +│ │ │ │ ├── exp_05.exp3.json +│ │ │ │ ├── exp_06.exp3.json +│ │ │ │ ├── exp_07.exp3.json +│ │ │ │ ├── exp_08.exp3.json +│ │ │ │ ├── exp_09.exp3.json +│ │ │ │ └── exp_10.exp3.json +│ │ │ └── motions +│ │ │ ├── mtn_00.motion3.json +│ │ │ ├── mtn_01.motion3.json +│ │ │ ├── mtn_02.motion3.json +│ │ │ ├── mtn_03.motion3.json +│ │ │ ├── mtn_04.motion3.json +│ │ │ ├── mtn_05.motion3.json +│ │ │ ├── mtn_06.motion3.json +│ │ │ ├── mtn_07.motion3.json +│ │ │ └── mtn_08.motion3.json +│ │ ├── Rice +│ │ │ ├── Rice.2048 +│ │ │ │ ├── texture_00.png +│ │ │ │ └── texture_01.png +│ │ │ ├── Rice.cdi3.json +│ │ │ ├── Rice.moc3 +│ │ │ ├── Rice.model3.json +│ │ │ ├── Rice.physics3.json +│ │ │ └── motions +│ │ │ ├── mtn_00.motion3.json +│ │ │ ├── mtn_01.motion3.json +│ │ │ ├── mtn_02.motion3.json +│ │ │ └── mtn_03.motion3.json +│ │ ├── back_class_normal.png +│ │ └── icon_gear.png +│ └── UI_Cubism.uasset +├── Docs +│ └── Image +│ └── ScreenShot.GIF +├── LICENSE +├── ReadMe.md +├── Resources +│ └── Icon128.png +├── Shaders +│ └── Private +│ ├── CubismMask.usf +│ ├── CubismSep.usf +│ └── CubismSepMask.usf +├── Source +│ ├── ThirdParty +│ │ └── CubismLibrary +│ │ ├── CHANGELOG.md +│ │ ├── LICENSE.md +│ │ ├── Please_copy_cubism_core_for_native +│ │ ├── README.ja.md +│ │ ├── README.md +│ │ ├── RedistributableFiles.txt +│ │ └── include +│ │ └── Live2DCubismCore.h +│ └── UnrealLive2D +│ ├── Private +│ │ ├── CubisimInclude.h +│ │ ├── CubismBpLib.cpp +│ │ ├── CubismObject.cpp +│ │ ├── CubismUeTypes.cpp +│ │ ├── Draw +│ │ │ ├── CubismConfig.cpp +│ │ │ ├── CubismConfig.h +│ │ │ ├── CubismSepRender.cpp +│ │ │ ├── CubismSepRender.h +│ │ │ ├── ModelRenders.cpp +│ │ │ ├── ModelRenders.h +│ │ │ ├── ModelRenders_DrawableMask.cpp +│ │ │ ├── ModelRenders_DrawableNormal.cpp +│ │ │ └── ModelRenders_Mask.cpp +│ │ ├── FWPort +│ │ │ ├── CubismFrameWorkAllocator.cpp +│ │ │ ├── CubismFrameWorkAllocator.h +│ │ │ ├── ModelRender.cpp +│ │ │ ├── ModelRender.h +│ │ │ ├── RawModel.cpp +│ │ │ └── RawModel.h +│ │ ├── FrameWork +│ │ │ ├── CHANGELOG.md +│ │ │ ├── CMakeLists.txt +│ │ │ ├── LICENSE.md +│ │ │ ├── README.ja.md +│ │ │ ├── README.md +│ │ │ └── src +│ │ │ ├── CubismCdiJson.cpp +│ │ │ ├── CubismCdiJson.hpp +│ │ │ ├── CubismDefaultParameterId.cpp +│ │ │ ├── CubismDefaultParameterId.hpp +│ │ │ ├── CubismFramework.cpp +│ │ │ ├── CubismFramework.hpp +│ │ │ ├── CubismFrameworkConfig.hpp +│ │ │ ├── CubismJsonHolder.hpp +│ │ │ ├── CubismModelSettingJson.cpp +│ │ │ ├── CubismModelSettingJson.hpp +│ │ │ ├── Effect +│ │ │ │ ├── CubismBreath.cpp +│ │ │ │ ├── CubismBreath.hpp +│ │ │ │ ├── CubismEyeBlink.cpp +│ │ │ │ ├── CubismEyeBlink.hpp +│ │ │ │ ├── CubismPose.cpp +│ │ │ │ └── CubismPose.hpp +│ │ │ ├── ICubismAllocator.hpp +│ │ │ ├── ICubismModelSetting.hpp +│ │ │ ├── Id +│ │ │ │ ├── CubismId.cpp +│ │ │ │ ├── CubismId.hpp +│ │ │ │ ├── CubismIdManager.cpp +│ │ │ │ └── CubismIdManager.hpp +│ │ │ ├── Live2DCubismCore.hpp +│ │ │ ├── Math +│ │ │ │ ├── CubismMath.cpp +│ │ │ │ ├── CubismMath.hpp +│ │ │ │ ├── CubismMatrix44.cpp +│ │ │ │ ├── CubismMatrix44.hpp +│ │ │ │ ├── CubismModelMatrix.cpp +│ │ │ │ ├── CubismModelMatrix.hpp +│ │ │ │ ├── CubismTargetPoint.cpp +│ │ │ │ ├── CubismTargetPoint.hpp +│ │ │ │ ├── CubismVector2.cpp +│ │ │ │ ├── CubismVector2.hpp +│ │ │ │ ├── CubismViewMatrix.cpp +│ │ │ │ └── CubismViewMatrix.hpp +│ │ │ ├── Model +│ │ │ │ ├── CubismMoc.cpp +│ │ │ │ ├── CubismMoc.hpp +│ │ │ │ ├── CubismModel.cpp +│ │ │ │ ├── CubismModel.hpp +│ │ │ │ ├── CubismModelUserData.cpp +│ │ │ │ ├── CubismModelUserData.hpp +│ │ │ │ ├── CubismModelUserDataJson.cpp +│ │ │ │ ├── CubismModelUserDataJson.hpp +│ │ │ │ ├── CubismUserModel.cpp +│ │ │ │ └── CubismUserModel.hpp +│ │ │ ├── Motion +│ │ │ │ ├── ACubismMotion.cpp +│ │ │ │ ├── ACubismMotion.hpp +│ │ │ │ ├── CMakeLists.txt +│ │ │ │ ├── CubismExpressionMotion.cpp +│ │ │ │ ├── CubismExpressionMotion.hpp +│ │ │ │ ├── CubismExpressionMotionManager.cpp +│ │ │ │ ├── CubismExpressionMotionManager.hpp +│ │ │ │ ├── CubismMotion.cpp +│ │ │ │ ├── CubismMotion.hpp +│ │ │ │ ├── CubismMotionInternal.hpp +│ │ │ │ ├── CubismMotionJson.cpp +│ │ │ │ ├── CubismMotionJson.hpp +│ │ │ │ ├── CubismMotionManager.cpp +│ │ │ │ ├── CubismMotionManager.hpp +│ │ │ │ ├── CubismMotionQueueEntry.cpp +│ │ │ │ ├── CubismMotionQueueEntry.hpp +│ │ │ │ ├── CubismMotionQueueManager.cpp +│ │ │ │ └── CubismMotionQueueManager.hpp +│ │ │ ├── Physics +│ │ │ │ ├── CubismPhysics.cpp +│ │ │ │ ├── CubismPhysics.hpp +│ │ │ │ ├── CubismPhysicsInternal.hpp +│ │ │ │ ├── CubismPhysicsJson.cpp +│ │ │ │ └── CubismPhysicsJson.hpp +│ │ │ ├── Rendering +│ │ │ │ ├── CubismRenderer.cpp +│ │ │ │ └── CubismRenderer.hpp +│ │ │ ├── Type +│ │ │ │ ├── CubismBasicType.hpp +│ │ │ │ ├── csmMap.hpp +│ │ │ │ ├── csmRectF.cpp +│ │ │ │ ├── csmRectF.hpp +│ │ │ │ ├── csmString.cpp +│ │ │ │ ├── csmString.hpp +│ │ │ │ └── csmVector.hpp +│ │ │ └── Utils +│ │ │ ├── CubismDebug.cpp +│ │ │ ├── CubismDebug.hpp +│ │ │ ├── CubismJson.cpp +│ │ │ ├── CubismJson.hpp +│ │ │ ├── CubismString.cpp +│ │ │ └── CubismString.hpp +│ │ └── UnrealLive2D.cpp +│ ├── Public +│ │ ├── CubismBpLib.h +│ │ ├── CubismObject.h +│ │ ├── CubismUeTypes.h +│ │ └── UnrealLive2D.h +│ └── UnrealLive2D.Build.cs +└── UnrealLive2D.uplugin + +``` + +`Content/Samples/Haru/Haru.model3.json`: + +```json +{ + "Version": 3, + "FileReferences": { + "Moc": "Haru.moc3", + "Textures": [ + "Haru.2048/texture_00.png", + "Haru.2048/texture_01.png" + ], + "Pose": "Haru.pose3.json", + "Expressions": [ + {"Name":"f00","File":"expressions/F01.exp3.json"}, + {"Name":"f01","File":"expressions/F02.exp3.json"}, + {"Name":"f02","File":"expressions/F03.exp3.json"}, + {"Name":"f03","File":"expressions/F04.exp3.json"}, + {"Name":"f04","File":"expressions/F05.exp3.json"}, + {"Name":"f05","File":"expressions/F06.exp3.json"}, + {"Name":"f06","File":"expressions/F07.exp3.json"}, + {"Name":"f07","File":"expressions/F08.exp3.json"} + ], + "Motions": { + "Idle": [ + {"File":"motions/haru_g_idle.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5}, + {"File":"motions/haru_g_m15.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5} + ], + "TapBody": [ + {"File":"motions/haru_g_m06.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5}, + {"File":"motions/haru_g_m09.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5}, + {"File":"motions/haru_g_m20.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5}, + {"File":"motions/haru_g_m26.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5} + ] + }, + "UserData": "Haru.userdata3.json" + }, + "Groups": [ + { + "Target": "Parameter", + "Name": "LipSync", + "Ids": [ + "ParamMouthOpenY" + ] + }, + { + "Target": "Parameter", + "Name": "EyeBlink", + "Ids": [ + "ParamEyeLOpen", + "ParamEyeROpen" + ] + } + ], + "HitAreas": [ + {"Name":"Head", "Id":"HitArea"}, + {"Name":"Body", "Id":"HitArea2"} + ] +} +``` + +`Content/Samples/Haru/Haru.pose3.json`: + +```json +{ + "Type": "Live2D Pose", + "Groups": [ + [ + { + "Id": "Part01ArmLB001", + "Link": [] + }, + { + "Id": "Part01ArmRA001", + "Link": [] + } + ], + [ + { + "Id": "Part01ArmRB001", + "Link": [] + }, + { + "Id": "Part01ArmLA001", + "Link": [] + } + ] + ] +} +``` + +`Content/Samples/Haru/Haru.userdata3.json`: + +```json +{ + "Version": 3, + "Meta": { + "UserDataCount": 3, + "TotalUserDataSize": 9 + }, + "UserData": [ + { + "Target": "ArtMesh", + "Id": "D_PSD_27", + "Value": "tai" + }, + { + "Target": "ArtMesh", + "Id": "D_PSD_25", + "Value": "tai" + }, + { + "Target": "ArtMesh", + "Id": "D_PSD_24", + "Value": "tai" + } + ] +} +``` + +`Content/Samples/Haru/expressions/F01.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamMouthForm", + "Value": 0.27, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Haru/expressions/F02.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamBrowLY", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamMouthOpenY", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamEyeForm", + "Value": 0.54, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Haru/expressions/F03.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamBrowLY", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLX", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": -2, + "Blend": "Add" + }, + { + "Id": "ParamMouthOpenY", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamEyeForm", + "Value": -1, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Haru/expressions/F04.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 0.8, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeROpen", + "Value": 0.8, + "Blend": "Multiply" + }, + { + "Id": "ParamBrowLY", + "Value": -0.56, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": -0.56, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLAngle", + "Value": 0.35, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": 0.35, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": -0.74, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": -0.74, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": -1.76, + "Blend": "Add" + }, + { + "Id": "ParamEyeForm", + "Value": 1, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Haru/expressions/F05.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 0, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeLSmile", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamEyeROpen", + "Value": 0, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeRSmile", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLY", + "Value": 0.32, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": 0.32, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Haru/expressions/F06.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 2, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeROpen", + "Value": 2, + "Blend": "Multiply" + }, + { + "Id": "ParamBrowLY", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamEyeBallForm", + "Value": -0.65, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Haru/expressions/F07.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 0.89, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeROpen", + "Value": 0.89, + "Blend": "Multiply" + }, + { + "Id": "ParamBrowLY", + "Value": -0.56, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": -0.56, + "Blend": "Add" + }, + { + "Id": "ParamBrowLX", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLAngle", + "Value": 0.35, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": 0.35, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": -0.74, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": -0.74, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": -0.46, + "Blend": "Add" + }, + { + "Id": "ParamTere", + "Value": 1, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Haru/expressions/F08.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 0.8, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeROpen", + "Value": 0.8, + "Blend": "Multiply" + }, + { + "Id": "ParamBrowLForm", + "Value": -0.33, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": -0.33, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": -1.76, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_idle.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 10, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 298, + "TotalPointCount": 789, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 3.322, + 1, + 6.644, + 1, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.178, + 0, + 0.356, + -4, + 0.533, + -4, + 1, + 0.989, + -4, + 1.444, + -4, + 1.9, + -4, + 1, + 2.378, + -4, + 2.856, + 0, + 3.333, + 0, + 1, + 3.444, + 0, + 3.556, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.433, + 0, + 6.967, + 14, + 7.5, + 14, + 1, + 7.833, + 14, + 8.167, + 14, + 8.5, + 14, + 1, + 8.989, + 14, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.178, + 0, + 0.356, + -6, + 0.533, + -6, + 1, + 0.989, + -6, + 1.444, + -6, + 1.9, + -6, + 1, + 2.378, + -6, + 2.856, + 0, + 3.333, + 0, + 1, + 3.444, + 0, + 3.556, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.211, + 0, + 6.522, + 5, + 6.833, + 5, + 1, + 7.056, + 5, + 7.278, + -6, + 7.5, + -6, + 1, + 7.833, + -6, + 8.167, + -6, + 8.5, + -6, + 1, + 8.989, + -6, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.178, + 0, + 0.356, + -6, + 0.533, + -6, + 1, + 0.733, + -6, + 0.933, + 1.432, + 1.133, + 5, + 1, + 1.389, + 9.559, + 1.644, + 10, + 1.9, + 10, + 1, + 2.378, + 10, + 2.856, + -6, + 3.333, + -6, + 1, + 3.444, + -6, + 3.556, + -6, + 3.667, + -6, + 1, + 4.156, + -6, + 4.644, + 6, + 5.133, + 6, + 1, + 5.389, + 6, + 5.644, + 6, + 5.9, + 6, + 1, + 6.211, + 6, + 6.522, + 12, + 6.833, + 12, + 1, + 7.389, + 12, + 7.944, + -6, + 8.5, + -6, + 1, + 8.989, + -6, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 3.322, + 0, + 6.644, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.633, + 1, + 1.267, + 1, + 1.9, + 1, + 1, + 2, + 1, + 2.1, + 1, + 2.2, + 1, + 1, + 2.244, + 1, + 2.289, + 0, + 2.333, + 0, + 1, + 2.356, + 0, + 2.378, + 0, + 2.4, + 0, + 1, + 2.456, + 0, + 2.511, + 1, + 2.567, + 1, + 1, + 2.933, + 1, + 3.3, + 1, + 3.667, + 1, + 1, + 3.711, + 1, + 3.756, + 0, + 3.8, + 0, + 1, + 3.822, + 0, + 3.844, + 0, + 3.867, + 0, + 1, + 3.922, + 0, + 3.978, + 1, + 4.033, + 1, + 1, + 4.656, + 1, + 5.278, + 1, + 5.9, + 1, + 1, + 6.433, + 1, + 6.967, + 1, + 7.5, + 1, + 1, + 7.544, + 1, + 7.589, + 0, + 7.633, + 0, + 1, + 7.656, + 0, + 7.678, + 0, + 7.7, + 0, + 1, + 7.756, + 0, + 7.811, + 1, + 7.867, + 1, + 1, + 8.078, + 1, + 8.289, + 1, + 8.5, + 1, + 1, + 8.989, + 1, + 9.478, + 1, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.633, + 1, + 1.267, + 1, + 1.9, + 1, + 1, + 2, + 1, + 2.1, + 1, + 2.2, + 1, + 1, + 2.244, + 1, + 2.289, + 0, + 2.333, + 0, + 1, + 2.356, + 0, + 2.378, + 0, + 2.4, + 0, + 1, + 2.456, + 0, + 2.511, + 1, + 2.567, + 1, + 1, + 2.933, + 1, + 3.3, + 1, + 3.667, + 1, + 1, + 3.711, + 1, + 3.756, + 0, + 3.8, + 0, + 1, + 3.822, + 0, + 3.844, + 0, + 3.867, + 0, + 1, + 3.922, + 0, + 3.978, + 1, + 4.033, + 1, + 1, + 4.656, + 1, + 5.278, + 1, + 5.9, + 1, + 1, + 6.433, + 1, + 6.967, + 1, + 7.5, + 1, + 1, + 7.544, + 1, + 7.589, + 0, + 7.633, + 0, + 1, + 7.656, + 0, + 7.678, + 0, + 7.7, + 0, + 1, + 7.756, + 0, + 7.811, + 1, + 7.867, + 1, + 1, + 8.078, + 1, + 8.289, + 1, + 8.5, + 1, + 1, + 8.989, + 1, + 9.478, + 1, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 3.322, + 0, + 6.644, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.178, + 0, + 0.356, + 0.23, + 0.533, + 0.23, + 1, + 0.989, + 0.23, + 1.444, + 0.23, + 1.9, + 0.23, + 1, + 2.611, + 0.23, + 3.322, + 0, + 4.033, + 0, + 1, + 4.656, + 0, + 5.278, + 0, + 5.9, + 0, + 1, + 6.211, + 0, + 6.522, + -0.198, + 6.833, + -0.3, + 1, + 7.056, + -0.373, + 7.278, + -0.398, + 7.5, + -0.43, + 1, + 7.833, + -0.477, + 8.167, + -0.49, + 8.5, + -0.49, + 1, + 8.989, + -0.49, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.178, + 0, + 0.356, + 0.25, + 0.533, + 0.25, + 1, + 0.989, + 0.25, + 1.444, + 0.25, + 1.9, + 0.25, + 1, + 2.611, + 0.25, + 3.322, + 0, + 4.033, + 0, + 1, + 4.656, + 0, + 5.278, + 0, + 5.9, + 0, + 1, + 6.211, + 0, + 6.522, + -0.17, + 6.833, + -0.17, + 1, + 7.056, + -0.17, + 7.278, + 0.29, + 7.5, + 0.29, + 1, + 7.833, + 0.29, + 8.167, + 0.282, + 8.5, + 0.21, + 1, + 8.989, + 0.104, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 3.322, + 1, + 6.644, + 1, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 3.322, + 0, + 6.644, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 3.322, + 0, + 6.644, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.178, + 0, + 0.356, + -3, + 0.533, + -3, + 1, + 0.989, + -3, + 1.444, + -3, + 1.9, + -3, + 1, + 1.989, + -3, + 2.078, + -3, + 2.167, + -3, + 1, + 2.556, + -3, + 2.944, + 0, + 3.333, + 0, + 1, + 3.444, + 0, + 3.556, + 0, + 3.667, + 0, + 1, + 3.722, + 0, + 3.778, + 0, + 3.833, + 0, + 1, + 4.267, + 0, + 4.7, + 4, + 5.133, + 4, + 1, + 5.389, + 4, + 5.644, + 4, + 5.9, + 4, + 1, + 6, + 4, + 6.1, + 4, + 6.2, + 4, + 1, + 6.411, + 4, + 6.622, + 4.702, + 6.833, + 6, + 1, + 7.056, + 7.367, + 7.278, + 8, + 7.5, + 8, + 1, + 7.833, + 8, + 8.167, + 8, + 8.5, + 8, + 1, + 8.6, + 8, + 8.7, + 8, + 8.8, + 8, + 1, + 9.189, + 8, + 9.578, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 1.989, + 0, + 2.078, + 0, + 2.167, + 0, + 1, + 2.667, + 0, + 3.167, + 0, + 3.667, + 0, + 1, + 3.722, + 0, + 3.778, + 0, + 3.833, + 0, + 1, + 4.522, + 0, + 5.211, + 0, + 5.9, + 0, + 1, + 6, + 0, + 6.1, + 0, + 6.2, + 0, + 1, + 6.967, + 0, + 7.733, + 0, + 8.5, + 0, + 1, + 8.6, + 0, + 8.7, + 0, + 8.8, + 0, + 1, + 9.189, + 0, + 9.578, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 3.788, + 0.733, + 4.987, + 1, + 0.956, + 6.077, + 1.178, + 5.954, + 1.4, + 5.954, + 1, + 1.778, + 5.954, + 2.156, + 5.242, + 2.533, + 2.985, + 1, + 2.8, + 1.393, + 3.067, + 0, + 3.333, + 0, + 1, + 3.444, + 0, + 3.556, + 0, + 3.667, + 0, + 1, + 3.722, + 0, + 3.778, + 0, + 3.833, + 0, + 1, + 4.267, + 0, + 4.7, + -2, + 5.133, + -2, + 1, + 5.389, + -2, + 5.644, + -2, + 5.9, + -2, + 1, + 6, + -2, + 6.1, + -2, + 6.2, + -2, + 1, + 6.411, + -2, + 6.622, + -4, + 6.833, + -4, + 1, + 7.056, + -4, + 7.278, + -2, + 7.5, + -2, + 1, + 7.833, + -2, + 8.167, + -5, + 8.5, + -5, + 1, + 8.6, + -5, + 8.7, + -5, + 8.8, + -5, + 1, + 9.189, + -5, + 9.578, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 3.322, + 0, + 6.644, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 1, + 1, + 3.322, + 1, + 6.644, + 1, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 1, + 1, + 3.322, + 1, + 6.644, + 1, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 3.322, + 0, + 6.644, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 3.322, + 0, + 6.644, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 3.322, + 0, + 6.644, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + -0.1, + 1, + 3.322, + -0.1, + 6.644, + -0.1, + 9.967, + -0.1, + 0, + 10, + -0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 3.322, + 0, + 6.644, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0.1, + 1, + 3.322, + 0.1, + 6.644, + 0.1, + 9.967, + 0.1, + 0, + 10, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 3.322, + 0, + 6.644, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.633, + 0, + 1.267, + 0, + 1.9, + 0, + 1, + 2.489, + 0, + 3.078, + 0, + 3.667, + 0, + 1, + 4.411, + 0, + 5.156, + 0, + 5.9, + 0, + 1, + 6.767, + 0, + 7.633, + 0, + 8.5, + 0, + 1, + 8.989, + 0, + 9.478, + 0, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 9.967, + 0, + 0, + 10, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 9.967, + 1, + 0, + 10, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 9.967, + 0, + 0, + 10, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m01.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 2.9, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 145, + "TotalPointCount": 330, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 0.956, + 1, + 1.911, + 1, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.2, + 0, + 1.3, + -8, + 1.4, + -8, + 1, + 1.456, + -8, + 1.511, + -5.237, + 1.567, + 0, + 1, + 1.667, + 9.427, + 1.767, + 14, + 1.867, + 14, + 1, + 1.989, + 14, + 2.111, + -16, + 2.233, + -16, + 1, + 2.389, + -16, + 2.544, + 0, + 2.7, + 0, + 1, + 2.756, + 0, + 2.811, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.667, + 1, + 1.333, + 1, + 2, + 1, + 1, + 2.044, + 1, + 2.089, + 0, + 2.133, + 0, + 1, + 2.2, + 0, + 2.267, + 0, + 2.333, + 0, + 1, + 2.389, + 0, + 2.444, + 1, + 2.5, + 1, + 1, + 2.622, + 1, + 2.744, + 1, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.667, + 1, + 1.333, + 1, + 2, + 1, + 1, + 2.044, + 1, + 2.089, + 0, + 2.133, + 0, + 1, + 2.2, + 0, + 2.267, + 0, + 2.333, + 0, + 1, + 2.389, + 0, + 2.444, + 1, + 2.5, + 1, + 1, + 2.622, + 1, + 2.744, + 1, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.956, + 1, + 1.911, + 1, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.489, + 0, + 0.978, + 0, + 1.467, + 0, + 1, + 1.622, + 0, + 1.778, + 2, + 1.933, + 2, + 1, + 2.056, + 2, + 2.178, + 0, + 2.3, + 0, + 1, + 2.489, + 0, + 2.678, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 1, + 1, + 0.956, + 1, + 1.911, + 1, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 1, + 1, + 0.956, + 1, + 1.911, + 1, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 5, + 1, + 0.956, + 5, + 1.911, + 5, + 2.867, + 5, + 0, + 2.9, + 5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0.19, + 1, + 0.956, + 0.19, + 1.911, + 0.19, + 2.867, + 0.19, + 0, + 2.9, + 0.19 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + -0.17, + 1, + 0.956, + -0.17, + 1.911, + -0.17, + 2.867, + -0.17, + 0, + 2.9, + -0.17 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 2.867, + 0, + 0, + 2.9, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 0, + 2.9, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 2.867, + 0, + 0, + 2.9, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m02.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 2.033, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 148, + "TotalPointCount": 341, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 0.667, + 1, + 1.333, + 1, + 2, + 1, + 1, + 2.011, + 1, + 2.022, + 1, + 2.033, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.078, + 0, + 0.156, + 0, + 0.233, + 0, + 1, + 0.411, + 0, + 0.589, + 0, + 0.767, + 0, + 1, + 1.178, + 0, + 1.589, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.078, + 0, + 0.156, + 0, + 0.233, + 0, + 1, + 0.411, + 0, + 0.589, + 14, + 0.767, + 14, + 1, + 0.889, + 14, + 1.011, + -11, + 1.133, + -11, + 1, + 1.3, + -11, + 1.467, + 0, + 1.633, + 0, + 1, + 1.756, + 0, + 1.878, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.511, + 1, + 1.022, + 1, + 1.533, + 1, + 1, + 1.689, + 1, + 1.844, + 1, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.511, + 1, + 1.022, + 1, + 1.533, + 1, + 1, + 1.689, + 1, + 1.844, + 1, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.511, + 0, + 1.022, + 0, + 1.533, + 0, + 1, + 1.689, + 0, + 1.844, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.511, + 0, + 1.022, + 0, + 1.533, + 0, + 1, + 1.689, + 0, + 1.844, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.511, + 0, + 1.022, + 0, + 1.533, + 0, + 1, + 1.689, + 0, + 1.844, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.667, + 1, + 1.333, + 1, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.456, + 0, + 0.644, + 5, + 0.833, + 5, + 1, + 0.956, + 5, + 1.078, + 0, + 1.2, + 0, + 1, + 1.467, + 0, + 1.733, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 6, + 1, + 0.667, + 6, + 1.333, + 6, + 2, + 6, + 0, + 2.033, + 6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + -1, + 1, + 0.156, + -1, + 0.311, + -1, + 0.467, + -1, + 1, + 0.611, + -1, + 0.756, + -0.89, + 0.9, + -0.89, + 1, + 1.022, + -0.89, + 1.144, + -1, + 1.267, + -1, + 1, + 1.511, + -1, + 1.756, + -1, + 2, + -1, + 0, + 2.033, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + -1, + 1, + 0.156, + -1, + 0.311, + -1, + 0.467, + -1, + 1, + 0.611, + -1, + 0.756, + -0.89, + 0.9, + -0.89, + 1, + 1.022, + -0.89, + 1.144, + -1, + 1.267, + -1, + 1, + 1.511, + -1, + 1.756, + -1, + 2, + -1, + 0, + 2.033, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 2, + 0, + 2, + 2.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 2, + 0, + 2, + 2.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 2, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 2, + 0, + 2, + 2.033, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m03.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4.633, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 223, + "TotalPointCount": 564, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.533, + 1, + 3.067, + 1, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.344, + 0, + 0.689, + 0, + 1.033, + 0, + 1, + 1.089, + 0, + 1.144, + 0, + 1.2, + 0, + 1, + 1.622, + 0, + 2.044, + 0, + 2.467, + 0, + 1, + 2.578, + 0, + 2.689, + 0, + 2.8, + 0, + 1, + 2.911, + 0, + 3.022, + 0, + 3.133, + 0, + 1, + 3.233, + 0, + 3.333, + 0, + 3.433, + 0, + 1, + 3.544, + 0, + 3.656, + 0, + 3.767, + 0, + 1, + 3.856, + 0, + 3.944, + 0, + 4.033, + 0, + 1, + 4.222, + 0, + 4.411, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.344, + 0, + 0.689, + 0, + 1.033, + 0, + 1, + 1.089, + 0, + 1.144, + 0, + 1.2, + 0, + 1, + 1.622, + 0, + 2.044, + 0, + 2.467, + 0, + 1, + 2.578, + 0, + 2.689, + 13, + 2.8, + 13, + 1, + 2.911, + 13, + 3.022, + -13, + 3.133, + -13, + 1, + 3.233, + -13, + 3.333, + 0, + 3.433, + 0, + 1, + 3.544, + 0, + 3.656, + -13, + 3.767, + -13, + 1, + 3.856, + -13, + 3.944, + 0, + 4.033, + 0, + 1, + 4.222, + 0, + 4.411, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 0.8, + 1, + 0.133, + 0.8, + 0.267, + 0.8, + 0.4, + 0.8, + 1, + 0.578, + 0.8, + 0.756, + 0.9, + 0.933, + 0.9, + 1, + 1.067, + 0.9, + 1.2, + 0.8, + 1.333, + 0.8, + 1, + 1.422, + 0.8, + 1.511, + 0.8, + 1.6, + 0.8, + 1, + 1.678, + 0.8, + 1.756, + 1, + 1.833, + 1, + 1, + 1.889, + 1, + 1.944, + 0, + 2, + 0, + 1, + 2.056, + 0, + 2.111, + 0, + 2.167, + 0, + 1, + 2.256, + 0, + 2.344, + 1, + 2.433, + 1, + 1, + 2.589, + 1, + 2.744, + 1, + 2.9, + 1, + 1, + 2.956, + 1, + 3.011, + 0, + 3.067, + 0, + 1, + 3.4, + 0, + 3.733, + 0, + 4.067, + 0, + 1, + 4.2, + 0, + 4.333, + 0.8, + 4.467, + 0.8, + 1, + 4.511, + 0.8, + 4.556, + 0.8, + 4.6, + 0.8, + 0, + 4.633, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 0.8, + 1, + 0.133, + 0.8, + 0.267, + 0.8, + 0.4, + 0.8, + 1, + 0.567, + 0.8, + 0.733, + 0.9, + 0.9, + 0.9, + 1, + 1.044, + 0.9, + 1.189, + 0.8, + 1.333, + 0.8, + 1, + 1.422, + 0.8, + 1.511, + 0.8, + 1.6, + 0.8, + 1, + 1.678, + 0.8, + 1.756, + 1, + 1.833, + 1, + 1, + 1.889, + 1, + 1.944, + 0, + 2, + 0, + 1, + 2.056, + 0, + 2.111, + 0, + 2.167, + 0, + 1, + 2.256, + 0, + 2.344, + 1, + 2.433, + 1, + 1, + 2.589, + 1, + 2.744, + 1, + 2.9, + 1, + 1, + 2.956, + 1, + 3.011, + 0, + 3.067, + 0, + 1, + 3.4, + 0, + 3.733, + 0, + 4.067, + 0, + 1, + 4.2, + 0, + 4.333, + 0.8, + 4.467, + 0.8, + 1, + 4.511, + 0.8, + 4.556, + 0.8, + 4.6, + 0.8, + 0, + 4.633, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.467, + 0, + 0.933, + 0, + 1.4, + 0, + 1, + 1.522, + 0, + 1.644, + -0.1, + 1.767, + -0.1, + 1, + 1.889, + -0.1, + 2.011, + 0, + 2.133, + 0, + 1, + 2.956, + 0, + 3.778, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.467, + 0, + 0.933, + 0, + 1.4, + 0, + 1, + 1.522, + 0, + 1.644, + -0.38, + 1.767, + -0.38, + 1, + 1.889, + -0.38, + 2.011, + 0, + 2.133, + 0, + 1, + 2.956, + 0, + 3.778, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0, + 0.9, + 0, + 1, + 2.133, + 0, + 3.367, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0, + 0.9, + 0, + 1, + 2.133, + 0, + 3.367, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0, + 0.9, + 0, + 1, + 2.133, + 0, + 3.367, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0, + 0.9, + 0, + 1, + 2.133, + 0, + 3.367, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0, + 0.9, + 0, + 1, + 1.044, + 0, + 1.189, + 0.01, + 1.333, + 0.01, + 1, + 2.211, + 0.01, + 3.089, + 0.01, + 3.967, + 0.01, + 1, + 4.178, + 0.01, + 4.389, + 0.01, + 4.6, + 0.01, + 0, + 4.633, + 0.01 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0, + 0.9, + 0, + 1, + 1.044, + 0, + 1.189, + 0, + 1.333, + 0, + 1, + 2.211, + 0, + 3.089, + 0, + 3.967, + 0, + 1, + 4.178, + 0, + 4.389, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + -0.35, + 1, + 0.3, + -0.35, + 0.6, + -0.35, + 0.9, + -0.35, + 1, + 1.044, + -0.35, + 1.189, + -0.66, + 1.333, + -0.66, + 1, + 2.211, + -0.66, + 3.089, + -0.66, + 3.967, + -0.66, + 1, + 4.178, + -0.66, + 4.389, + -0.39, + 4.6, + -0.39, + 0, + 4.633, + -0.39 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + -0.35, + 1, + 0.3, + -0.35, + 0.6, + -0.35, + 0.9, + -0.35, + 1, + 1.044, + -0.35, + 1.189, + -0.65, + 1.333, + -0.65, + 1, + 2.211, + -0.65, + 3.089, + -0.65, + 3.967, + -0.65, + 1, + 4.178, + -0.65, + 4.389, + -0.39, + 4.6, + -0.39, + 0, + 4.633, + -0.39 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0, + 0.9, + 0, + 1, + 1.044, + 0, + 1.189, + -1, + 1.333, + -1, + 1, + 1.756, + -1, + 2.178, + -1, + 2.6, + -1, + 1, + 3.267, + -1, + 3.933, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.356, + 0, + 0.711, + 0, + 1.067, + 0, + 1, + 1.3, + 0, + 1.533, + 2, + 1.767, + 2, + 1, + 1.933, + 2, + 2.1, + 0, + 2.267, + 0, + 1, + 2.578, + 0, + 2.889, + 0, + 3.2, + 0, + 1, + 3.667, + 0, + 4.133, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.233, + 0, + 1.467, + 3, + 1.7, + 3, + 1, + 1.867, + 3, + 2.033, + 0, + 2.2, + 0, + 1, + 2.311, + 0, + 2.422, + 0, + 2.533, + 0, + 1, + 2.644, + 0, + 2.756, + 4, + 2.867, + 4, + 1, + 2.944, + 4, + 3.022, + -1, + 3.1, + -1, + 1, + 3.244, + -1, + 3.389, + 0, + 3.533, + 0, + 1, + 3.889, + 0, + 4.244, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + -5, + 0.9, + -5, + 1, + 1.133, + -5, + 1.367, + 0, + 1.6, + 0, + 1, + 2.389, + 0, + 3.178, + 0, + 3.967, + 0, + 1, + 4.178, + 0, + 4.389, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0.5, + 1, + 0.156, + 0.5, + 0.311, + 0.749, + 0.467, + 0.76, + 1, + 1.778, + 0.856, + 3.089, + 0.89, + 4.4, + 0.89, + 1, + 4.467, + 0.89, + 4.533, + 0.5, + 4.6, + 0.5, + 0, + 4.633, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0.5, + 1, + 0.156, + 0.5, + 0.311, + 0.743, + 0.467, + 0.76, + 1, + 1.778, + 0.9, + 3.089, + 0.95, + 4.4, + 0.95, + 1, + 4.467, + 0.95, + 4.533, + 0.5, + 4.6, + 0.5, + 0, + 4.633, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0.3, + 1, + 0.311, + 0.3, + 0.622, + 0.3, + 0.933, + 0.3, + 1, + 1.178, + 0.3, + 1.422, + 0.5, + 1.667, + 0.5, + 1, + 1.844, + 0.5, + 2.022, + 0.3, + 2.2, + 0.3, + 1, + 3, + 0.3, + 3.8, + 0.3, + 4.6, + 0.3, + 0, + 4.633, + 0.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0.3, + 1, + 0.289, + 0.3, + 0.578, + 0.3, + 0.867, + 0.3, + 1, + 1.111, + 0.3, + 1.356, + 0.5, + 1.6, + 0.5, + 1, + 1.778, + 0.5, + 1.956, + 0.3, + 2.133, + 0.3, + 1, + 2.956, + 0.3, + 3.778, + 0.3, + 4.6, + 0.3, + 0, + 4.633, + 0.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0.66, + 1, + 0.356, + 0.66, + 0.711, + 0.662, + 1.067, + 0.64, + 1, + 1.322, + 0.624, + 1.578, + 0.12, + 1.833, + 0.12, + 1, + 1.978, + 0.12, + 2.122, + 0.66, + 2.267, + 0.66, + 1, + 3.044, + 0.66, + 3.822, + 0.66, + 4.6, + 0.66, + 0, + 4.633, + 0.66 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + -1, + 1, + 0.322, + -1, + 0.644, + -1, + 0.967, + -1, + 1, + 1.233, + -1, + 1.5, + -0.68, + 1.767, + -0.68, + 1, + 1.933, + -0.68, + 2.1, + -1, + 2.267, + -1, + 1, + 3.044, + -1, + 3.822, + -1, + 4.6, + -1, + 0, + 4.633, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 1.533, + 0, + 3.067, + 0, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 0, + 2, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 0, + 2, + 4.6, + 0, + 0, + 4.633, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 4.6, + 1, + 0, + 4.633, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 4.6, + 0, + 0, + 4.633, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m04.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 5.3, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 330, + "TotalPointCount": 885, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.756, + 1, + 3.511, + 1, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.611, + 0, + 0.922, + 0, + 1.233, + 0, + 1, + 1.289, + 0, + 1.344, + 0, + 1.4, + 0, + 1, + 1.8, + 0, + 2.2, + 0, + 2.6, + 0, + 1, + 2.711, + 0, + 2.822, + 0, + 2.933, + 0, + 1, + 3.044, + 0, + 3.156, + 0, + 3.267, + 0, + 1, + 3.367, + 0, + 3.467, + 0, + 3.567, + 0, + 1, + 3.633, + 0, + 3.7, + 0, + 3.767, + 0, + 1, + 3.889, + 0, + 4.011, + 0, + 4.133, + 0, + 1, + 4.322, + 0, + 4.511, + 0, + 4.7, + 0, + 1, + 4.889, + 0, + 5.078, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.611, + 0, + 0.922, + 0, + 1.233, + 0, + 1, + 1.289, + 0, + 1.344, + 0, + 1.4, + 0, + 1, + 1.822, + 0, + 2.244, + 0, + 2.667, + 0, + 1, + 2.778, + 0, + 2.889, + 13, + 3, + 13, + 1, + 3.111, + 13, + 3.222, + -9.823, + 3.333, + -9.823, + 1, + 3.411, + -9.823, + 3.489, + 8.938, + 3.567, + 8.938, + 1, + 3.678, + 8.938, + 3.789, + -5.187, + 3.9, + -5.187, + 1, + 4.044, + -5.187, + 4.189, + 3, + 4.333, + 3, + 1, + 4.556, + 3, + 4.778, + 0, + 5, + 0, + 1, + 5.089, + 0, + 5.178, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.956, + 0, + 3.611, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.811, + 0, + 3.322, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.789, + 0, + 3.278, + 0, + 4.767, + 0, + 1, + 4.933, + 0, + 5.1, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.1, + 1, + 0.2, + 1, + 0.3, + 1, + 1, + 0.367, + 1, + 0.433, + 1, + 0.5, + 1, + 1, + 0.544, + 1, + 0.589, + 0, + 0.633, + 0, + 1, + 0.667, + 0, + 0.7, + 0, + 0.733, + 0, + 1, + 0.789, + 0, + 0.844, + 1.1, + 0.9, + 1.1, + 1, + 0.967, + 1.1, + 1.033, + 1, + 1.1, + 1, + 1, + 1.333, + 1, + 1.567, + 1, + 1.8, + 1, + 1, + 1.878, + 1, + 1.956, + 1.2, + 2.033, + 1.2, + 1, + 2.089, + 1.2, + 2.144, + 0, + 2.2, + 0, + 1, + 2.256, + 0, + 2.311, + 0, + 2.367, + 0, + 1, + 2.456, + 0, + 2.544, + 1, + 2.633, + 1, + 1, + 2.789, + 1, + 2.944, + 1, + 3.1, + 1, + 1, + 3.156, + 1, + 3.211, + 0, + 3.267, + 0, + 1, + 3.589, + 0, + 3.911, + 0, + 4.233, + 0, + 1, + 4.344, + 0, + 4.456, + 1, + 4.567, + 1, + 1, + 4.633, + 1, + 4.7, + 1, + 4.767, + 1, + 1, + 4.933, + 1, + 5.1, + 1, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.789, + 0, + 3.278, + 0, + 4.767, + 0, + 1, + 4.933, + 0, + 5.1, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.1, + 1, + 0.2, + 1, + 0.3, + 1, + 1, + 0.367, + 1, + 0.433, + 1, + 0.5, + 1, + 1, + 0.544, + 1, + 0.589, + 0, + 0.633, + 0, + 1, + 0.667, + 0, + 0.7, + 0, + 0.733, + 0, + 1, + 0.789, + 0, + 0.844, + 1.1, + 0.9, + 1.1, + 1, + 0.967, + 1.1, + 1.033, + 1, + 1.1, + 1, + 1, + 1.333, + 1, + 1.567, + 1, + 1.8, + 1, + 1, + 1.878, + 1, + 1.956, + 1.2, + 2.033, + 1.2, + 1, + 2.089, + 1.2, + 2.144, + 0, + 2.2, + 0, + 1, + 2.256, + 0, + 2.311, + 0, + 2.367, + 0, + 1, + 2.456, + 0, + 2.544, + 1, + 2.633, + 1, + 1, + 2.789, + 1, + 2.944, + 1, + 3.1, + 1, + 1, + 3.156, + 1, + 3.211, + 0, + 3.267, + 0, + 1, + 3.589, + 0, + 3.911, + 0, + 4.233, + 0, + 1, + 4.344, + 0, + 4.456, + 1, + 4.567, + 1, + 1, + 4.633, + 1, + 4.7, + 1, + 4.767, + 1, + 1, + 4.933, + 1, + 5.1, + 1, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.789, + 0, + 3.278, + 0, + 4.767, + 0, + 1, + 4.933, + 0, + 5.1, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.811, + 0, + 3.322, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.811, + 0, + 3.322, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.811, + 0, + 3.322, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.5, + 0, + 0.7, + 0, + 0.9, + 0, + 1, + 1.133, + 0, + 1.367, + 0, + 1.6, + 0, + 1, + 1.744, + 0, + 1.889, + -0.1, + 2.033, + -0.1, + 1, + 2.144, + -0.1, + 2.256, + 0, + 2.367, + 0, + 1, + 2.456, + 0, + 2.544, + 0, + 2.633, + 0, + 1, + 3.344, + 0, + 4.056, + 0, + 4.767, + 0, + 1, + 4.933, + 0, + 5.1, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.5, + 0, + 0.7, + 0, + 0.9, + 0, + 1, + 1.133, + 0, + 1.367, + 0, + 1.6, + 0, + 1, + 1.667, + 0, + 1.733, + -0.085, + 1.8, + -0.2, + 1, + 1.878, + -0.334, + 1.956, + -0.38, + 2.033, + -0.38, + 1, + 2.089, + -0.38, + 2.144, + -0.384, + 2.2, + -0.3, + 1, + 2.256, + -0.216, + 2.311, + 0, + 2.367, + 0, + 1, + 2.456, + 0, + 2.544, + 0, + 2.633, + 0, + 1, + 3.167, + 0, + 3.7, + -0.1, + 4.233, + -0.1, + 1, + 4.344, + -0.1, + 4.456, + 0, + 4.567, + 0, + 1, + 4.633, + 0, + 4.7, + 0, + 4.767, + 0, + 1, + 4.933, + 0, + 5.1, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.367, + 0, + 0.433, + 0, + 0.5, + 0, + 1, + 0.556, + 0, + 0.611, + -0.1, + 0.667, + -0.1, + 1, + 0.689, + -0.1, + 0.711, + -0.1, + 0.733, + -0.1, + 1, + 0.789, + -0.1, + 0.844, + 0.1, + 0.9, + 0.1, + 1, + 0.967, + 0.1, + 1.033, + 0, + 1.1, + 0, + 1, + 1.411, + 0, + 1.722, + 0.5, + 2.033, + 0.5, + 1, + 2.089, + 0.5, + 2.144, + -0.3, + 2.2, + -0.3, + 1, + 2.256, + -0.3, + 2.311, + -0.3, + 2.367, + -0.3, + 1, + 2.456, + -0.3, + 2.544, + -0.215, + 2.633, + -0.2, + 1, + 3.511, + -0.056, + 4.389, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.367, + 0, + 0.433, + 0, + 0.5, + 0, + 1, + 0.556, + 0, + 0.611, + -0.1, + 0.667, + -0.1, + 1, + 0.689, + -0.1, + 0.711, + -0.1, + 0.733, + -0.1, + 1, + 0.789, + -0.1, + 0.844, + 0.1, + 0.9, + 0.1, + 1, + 0.967, + 0.1, + 1.033, + 0, + 1.1, + 0, + 1, + 1.411, + 0, + 1.722, + 0.5, + 2.033, + 0.5, + 1, + 2.089, + 0.5, + 2.144, + -0.3, + 2.2, + -0.3, + 1, + 2.256, + -0.3, + 2.311, + -0.3, + 2.367, + -0.3, + 1, + 2.456, + -0.3, + 2.544, + -0.215, + 2.633, + -0.2, + 1, + 3.511, + -0.056, + 4.389, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.567, + 0, + 0.833, + 0, + 1.1, + 0, + 1, + 1.411, + 0, + 1.722, + 0.2, + 2.033, + 0.2, + 1, + 2.233, + 0.2, + 2.433, + -0.3, + 2.633, + -0.3, + 1, + 3.511, + -0.3, + 4.389, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.567, + 0, + 0.833, + 0, + 1.1, + 0, + 1, + 1.411, + 0, + 1.722, + 0.2, + 2.033, + 0.2, + 1, + 2.233, + 0.2, + 2.433, + -0.3, + 2.633, + -0.3, + 1, + 3.511, + -0.3, + 4.389, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.567, + 0, + 0.833, + -0.4, + 1.1, + -0.4, + 1, + 1.411, + -0.4, + 1.722, + 0.1, + 2.033, + 0.1, + 1, + 2.233, + 0.1, + 2.433, + -0.4, + 2.633, + -0.4, + 1, + 3.511, + -0.4, + 4.389, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.567, + 0, + 0.833, + -0.4, + 1.1, + -0.4, + 1, + 1.411, + -0.4, + 1.722, + 0.1, + 2.033, + 0.1, + 1, + 2.233, + 0.1, + 2.433, + -0.4, + 2.633, + -0.4, + 1, + 3.511, + -0.4, + 4.389, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.567, + 0, + 0.833, + -0.35, + 1.1, + -0.35, + 1, + 1.411, + -0.35, + 1.722, + 0, + 2.033, + 0, + 1, + 2.233, + 0, + 2.433, + -0.4, + 2.633, + -0.4, + 1, + 3.511, + -0.4, + 4.389, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.567, + 0, + 0.833, + -0.35, + 1.1, + -0.35, + 1, + 1.411, + -0.35, + 1.722, + 0, + 2.033, + 0, + 1, + 2.233, + 0, + 2.433, + -0.4, + 2.633, + -0.4, + 1, + 3.511, + -0.4, + 4.389, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 0, + 1, + 0.156, + 0, + 0.311, + 0, + 0.467, + 0, + 1, + 0.522, + 0, + 0.578, + -1, + 0.633, + -1, + 1, + 0.933, + -1, + 1.233, + -1, + 1.533, + -1, + 1, + 2.422, + -1, + 3.311, + -1, + 4.2, + -1, + 1, + 4.411, + -1, + 4.622, + -1, + 4.833, + -1, + 1, + 4.978, + -1, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.156, + 0, + 0.311, + 0, + 0.467, + 0, + 1, + 0.767, + 0, + 1.067, + 0, + 1.367, + 0, + 1, + 1.5, + 0, + 1.633, + 0.6, + 1.767, + 0.6, + 1, + 1.844, + 0.6, + 1.922, + 0.608, + 2, + 0.5, + 1, + 2.067, + 0.408, + 2.133, + 0, + 2.2, + 0, + 1, + 2.344, + 0, + 2.489, + 0, + 2.633, + 0, + 1, + 3.367, + 0, + 4.1, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.811, + 0, + 3.322, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.622, + 0, + 0.944, + 0, + 1.267, + 0, + 1, + 1.5, + 0, + 1.733, + 2, + 1.967, + 2, + 1, + 2.133, + 2, + 2.3, + 0, + 2.467, + 0, + 1, + 2.778, + 0, + 3.089, + 0, + 3.4, + 0, + 1, + 3.878, + 0, + 4.356, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.6, + 0, + 0.9, + 0, + 1.2, + 0, + 1, + 1.433, + 0, + 1.667, + 3, + 1.9, + 3, + 1, + 2.067, + 3, + 2.233, + 0, + 2.4, + 0, + 1, + 2.511, + 0, + 2.622, + 0, + 2.733, + 0, + 1, + 2.844, + 0, + 2.956, + 4, + 3.067, + 4, + 1, + 3.156, + 4, + 3.244, + -1, + 3.333, + -1, + 1, + 3.422, + -1, + 3.511, + 0.817, + 3.6, + 0.817, + 1, + 3.7, + 0.817, + 3.8, + 0, + 3.9, + 0, + 1, + 4.211, + 0, + 4.522, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.567, + 0, + 0.833, + -5, + 1.1, + -5, + 1, + 1.333, + -5, + 1.567, + 0, + 1.8, + 0, + 1, + 2.011, + 0, + 2.222, + 0, + 2.433, + 0, + 1, + 3.233, + 0, + 4.033, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.956, + 0, + 3.611, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.811, + 0, + 3.322, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.811, + 0, + 3.322, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0.5, + 1, + 0.1, + 0.5, + 0.2, + 0.5, + 0.3, + 0.5, + 1, + 0.422, + 0.5, + 0.544, + 0.752, + 0.667, + 0.76, + 1, + 2.056, + 0.854, + 3.444, + 0.89, + 4.833, + 0.89, + 1, + 4.978, + 0.89, + 5.122, + 0.5, + 5.267, + 0.5, + 0, + 5.3, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0.5, + 1, + 0.1, + 0.5, + 0.2, + 0.5, + 0.3, + 0.5, + 1, + 0.422, + 0.5, + 0.544, + 0.748, + 0.667, + 0.76, + 1, + 2.056, + 0.897, + 3.444, + 0.95, + 4.833, + 0.95, + 1, + 4.978, + 0.95, + 5.122, + 0.5, + 5.267, + 0.5, + 0, + 5.3, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0.2, + 1, + 0.1, + 0.2, + 0.2, + 0.2, + 0.3, + 0.2, + 1, + 0.578, + 0.2, + 0.856, + 0.21, + 1.133, + 0.3, + 1, + 1.378, + 0.38, + 1.622, + 0.5, + 1.867, + 0.5, + 1, + 2.122, + 0.5, + 2.378, + 0.2, + 2.633, + 0.2, + 1, + 3.511, + 0.2, + 4.389, + 0.2, + 5.267, + 0.2, + 0, + 5.3, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0.2, + 1, + 0.1, + 0.2, + 0.2, + 0.2, + 0.3, + 0.2, + 1, + 0.556, + 0.2, + 0.811, + 0.211, + 1.067, + 0.3, + 1, + 1.311, + 0.386, + 1.556, + 0.5, + 1.8, + 0.5, + 1, + 2.056, + 0.5, + 2.311, + 0.2, + 2.567, + 0.2, + 1, + 3.467, + 0.2, + 4.367, + 0.2, + 5.267, + 0.2, + 0, + 5.3, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.811, + 0, + 3.322, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0.66, + 1, + 0.1, + 0.66, + 0.2, + 0.66, + 0.3, + 0.66, + 1, + 0.622, + 0.66, + 0.944, + 0.663, + 1.267, + 0.64, + 1, + 1.522, + 0.621, + 1.778, + 0.12, + 2.033, + 0.12, + 1, + 2.178, + 0.12, + 2.322, + 0.66, + 2.467, + 0.66, + 1, + 3.256, + 0.66, + 4.044, + 0.66, + 4.833, + 0.66, + 1, + 4.978, + 0.66, + 5.122, + 0.66, + 5.267, + 0.66, + 0, + 5.3, + 0.66 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 1.611, + 0, + 3.222, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + -1, + 1, + 0.1, + -1, + 0.2, + -1, + 0.3, + -1, + 1, + 0.589, + -1, + 0.878, + -1, + 1.167, + -1, + 1, + 1.433, + -1, + 1.7, + -0.68, + 1.967, + -0.68, + 1, + 2.133, + -0.68, + 2.3, + -1, + 2.467, + -1, + 1, + 3.256, + -1, + 4.044, + -1, + 4.833, + -1, + 1, + 4.978, + -1, + 5.122, + -1, + 5.267, + -1, + 0, + 5.3, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.811, + 0, + 3.322, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.811, + 0, + 3.322, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 1.811, + 0, + 3.322, + 0, + 4.833, + 0, + 1, + 4.978, + 0, + 5.122, + 0, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 0, + 2, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 0, + 2, + 5.267, + 0, + 0, + 5.3, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 5.267, + 1, + 0, + 5.3, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 5.267, + 0, + 0, + 5.3, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m05.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 2.033, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 181, + "TotalPointCount": 438, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 0.667, + 1, + 1.333, + 1, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + 0, + 0.367, + 0, + 1, + 0.489, + 0, + 0.611, + 0, + 0.733, + 0, + 1, + 0.833, + 0, + 0.933, + 0, + 1.033, + 0, + 1, + 1.222, + 0, + 1.411, + 0, + 1.6, + 0, + 1, + 1.733, + 0, + 1.867, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + -5.165, + 0.3, + -9, + 1, + 0.333, + -10.278, + 0.367, + -10, + 0.4, + -10, + 1, + 0.489, + -10, + 0.578, + 13, + 0.667, + 13, + 1, + 0.767, + 13, + 0.867, + -9, + 0.967, + -9, + 1, + 1.156, + -9, + 1.344, + 0, + 1.533, + 0, + 1, + 1.689, + 0, + 1.844, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 2, + 1, + 0.233, + 2, + 0.467, + 2, + 0.7, + 2, + 1, + 0.756, + 2, + 0.811, + 0, + 0.867, + 0, + 1, + 0.967, + 0, + 1.067, + 0, + 1.167, + 0, + 1, + 1.244, + 0, + 1.322, + 2, + 1.4, + 2, + 1, + 1.6, + 2, + 1.8, + 2, + 2, + 2, + 0, + 2.033, + 2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 2, + 1, + 0.233, + 2, + 0.467, + 2, + 0.7, + 2, + 1, + 0.756, + 2, + 0.811, + 0, + 0.867, + 0, + 1, + 0.967, + 0, + 1.067, + 0, + 1.167, + 0, + 1, + 1.244, + 0, + 1.322, + 2, + 1.4, + 2, + 1, + 1.6, + 2, + 1.8, + 2, + 2, + 2, + 0, + 2.033, + 2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.422, + 0, + 0.511, + 0, + 0.6, + 0, + 1, + 0.633, + 0, + 0.667, + 0, + 0.7, + 0, + 1, + 1.133, + 0, + 1.567, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0.3, + 1, + 0.111, + 0.3, + 0.222, + 0.3, + 0.333, + 0.3, + 1, + 0.422, + 0.3, + 0.511, + 0.3, + 0.6, + 0.3, + 1, + 0.633, + 0.3, + 0.667, + 0.3, + 0.7, + 0.3, + 1, + 0.756, + 0.3, + 0.811, + -0.2, + 0.867, + -0.2, + 1, + 0.967, + -0.2, + 1.067, + -0.2, + 1.167, + -0.2, + 1, + 1.244, + -0.2, + 1.322, + 0.3, + 1.4, + 0.3, + 1, + 1.6, + 0.3, + 1.8, + 0.3, + 2, + 0.3, + 0, + 2.033, + 0.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 1, + 1, + 0.233, + 1, + 0.467, + 1, + 0.7, + 1, + 1, + 0.756, + 1, + 0.811, + 0.7, + 0.867, + 0.7, + 1, + 0.967, + 0.7, + 1.067, + 0.7, + 1.167, + 0.7, + 1, + 1.244, + 0.7, + 1.322, + 1, + 1.4, + 1, + 1, + 1.6, + 1, + 1.8, + 1, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 1, + 1, + 0.233, + 1, + 0.467, + 1, + 0.7, + 1, + 1, + 0.756, + 1, + 0.811, + 0.7, + 0.867, + 0.7, + 1, + 0.967, + 0.7, + 1.067, + 0.7, + 1.167, + 0.7, + 1, + 1.244, + 0.7, + 1.322, + 1, + 1.4, + 1, + 1, + 1.6, + 1, + 1.8, + 1, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + -0.84, + 1, + 0.667, + -0.84, + 1.333, + -0.84, + 2, + -0.84, + 0, + 2.033, + -0.84 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.133, + 0, + 0.267, + -2, + 0.4, + -2, + 1, + 0.533, + -2, + 0.667, + 2, + 0.8, + 2, + 1, + 0.911, + 2, + 1.022, + 0, + 1.133, + 0, + 1, + 1.422, + 0, + 1.711, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + -0.16, + 1, + 0.189, + -0.16, + 0.378, + -0.16, + 0.567, + -0.16, + 1, + 0.678, + -0.16, + 0.789, + -0.026, + 0.9, + -0.026, + 1, + 1, + -0.026, + 1.1, + -0.182, + 1.2, + -0.182, + 1, + 1.322, + -0.182, + 1.444, + -0.16, + 1.567, + -0.16, + 1, + 1.711, + -0.16, + 1.856, + -0.16, + 2, + -0.16, + 0, + 2.033, + -0.16 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + -0.15, + 1, + 0.189, + -0.15, + 0.378, + -0.15, + 0.567, + -0.15, + 1, + 0.678, + -0.15, + 0.789, + -0.025, + 0.9, + -0.025, + 1, + 1, + -0.025, + 1.1, + -0.181, + 1.2, + -0.181, + 1, + 1.322, + -0.181, + 1.444, + -0.15, + 1.567, + -0.15, + 1, + 1.711, + -0.15, + 1.856, + -0.15, + 2, + -0.15, + 0, + 2.033, + -0.15 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 0, + 0.567, + 0, + 1, + 0.644, + 0, + 0.722, + 0.614, + 0.8, + 0.614, + 1, + 0.889, + 0.614, + 0.978, + -0.1, + 1.067, + -0.1, + 1, + 1.178, + -0.1, + 1.289, + 0, + 1.4, + 0, + 1, + 1.6, + 0, + 1.8, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 0, + 0.567, + 0, + 1, + 0.644, + 0, + 0.722, + -0.6, + 0.8, + -0.6, + 1, + 0.889, + -0.6, + 0.978, + 0.1, + 1.067, + 0.1, + 1, + 1.178, + 0.1, + 1.289, + 0, + 1.4, + 0, + 1, + 1.6, + 0, + 1.8, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 0, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 2, + 0, + 0, + 2.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 2, + 1, + 0, + 2.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 2, + 0, + 0, + 2.033, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m06.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4.533, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 195, + "TotalPointCount": 480, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.5, + 1, + 3, + 1, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.467, + 0, + 0.933, + -14, + 1.4, + -14, + 1, + 1.567, + -14, + 1.733, + -14, + 1.9, + -14, + 1, + 2.367, + -14, + 2.833, + 0, + 3.3, + 0, + 1, + 3.7, + 0, + 4.1, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 0, + 0.733, + 0, + 1, + 0.9, + 0, + 1.067, + -13, + 1.233, + -13, + 1, + 1.422, + -13, + 1.611, + 5, + 1.8, + 5, + 1, + 2.267, + 5, + 2.733, + 0, + 3.2, + 0, + 1, + 3.633, + 0, + 4.067, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + -18, + 1.1, + -18, + 1, + 1.4, + -18, + 1.7, + -18, + 2, + -18, + 1, + 2.3, + -18, + 2.6, + 18, + 2.9, + 18, + 1, + 3.133, + 18, + 3.367, + -30, + 3.6, + -30, + 1, + 3.9, + -30, + 4.2, + -21, + 4.5, + -21, + 0, + 4.533, + -21 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 1.311, + 0, + 2.622, + 0, + 3.933, + 0, + 1, + 4.122, + 0, + 4.311, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.344, + 1, + 0.689, + 1, + 1.033, + 1, + 1, + 1.078, + 1, + 1.122, + 0, + 1.167, + 0, + 1, + 1.222, + 0, + 1.278, + 0, + 1.333, + 0, + 1, + 1.411, + 0, + 1.489, + 1, + 1.567, + 1, + 1, + 2.544, + 1, + 3.522, + 1, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.344, + 1, + 0.689, + 1, + 1.033, + 1, + 1, + 1.078, + 1, + 1.122, + 0, + 1.167, + 0, + 1, + 1.222, + 0, + 1.278, + 0, + 1.333, + 0, + 1, + 1.411, + 0, + 1.489, + 1, + 1.567, + 1, + 1, + 2.544, + 1, + 3.522, + 1, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0.29, + 0.9, + 0.29, + 1, + 0.989, + 0.29, + 1.078, + 0.29, + 1.167, + 0.29, + 1, + 1.2, + 0.29, + 1.233, + -0.39, + 1.267, + -0.39, + 1, + 1.378, + -0.39, + 1.489, + -0.39, + 1.6, + -0.39, + 1, + 2, + -0.39, + 2.4, + 0, + 2.8, + 0, + 1, + 3.367, + 0, + 3.933, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0.5, + 0.9, + 0.5, + 1, + 0.989, + 0.5, + 1.078, + 0.5, + 1.167, + 0.5, + 1, + 1.2, + 0.5, + 1.233, + 0.33, + 1.267, + 0.33, + 1, + 1.378, + 0.33, + 1.489, + 0.33, + 1.6, + 0.33, + 1, + 2, + 0.33, + 2.4, + 0, + 2.8, + 0, + 1, + 3.367, + 0, + 3.933, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 1.5, + 1, + 3, + 1, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.533, + 0, + 1.067, + 7, + 1.6, + 7, + 1, + 2.056, + 7, + 2.511, + -7, + 2.967, + -7, + 1, + 3.478, + -7, + 3.989, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.578, + 0, + 1.156, + -7, + 1.733, + -7, + 1, + 2.1, + -7, + 2.467, + 7, + 2.833, + 7, + 1, + 3.389, + 7, + 3.944, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0.5, + 1, + 0.156, + 0.5, + 0.311, + 0.746, + 0.467, + 0.76, + 1, + 1.556, + 0.858, + 2.644, + 0.89, + 3.733, + 0.89, + 1, + 3.8, + 0.89, + 3.867, + 0.637, + 3.933, + 0.5, + 1, + 4.122, + 0.111, + 4.311, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0.5, + 1, + 0.156, + 0.5, + 0.311, + 0.74, + 0.467, + 0.76, + 1, + 1.556, + 0.903, + 2.644, + 0.95, + 3.733, + 0.95, + 1, + 3.8, + 0.95, + 3.867, + 0.64, + 3.933, + 0.5, + 1, + 4.122, + 0.104, + 4.311, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 3.5, + 1, + 0.133, + 3.5, + 0.267, + 3.2, + 0.4, + 3.2, + 1, + 0.733, + 3.2, + 1.067, + 4.098, + 1.4, + 4.8, + 1, + 1.522, + 5.057, + 1.644, + 5, + 1.767, + 5, + 1, + 2.278, + 5, + 2.789, + 4.1, + 3.3, + 4.1, + 1, + 3.433, + 4.1, + 3.567, + 4.3, + 3.7, + 4.3, + 1, + 3.967, + 4.3, + 4.233, + 4.3, + 4.5, + 4.3, + 0, + 4.533, + 4.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 2.5, + 1, + 0.133, + 2.5, + 0.267, + 2.3, + 0.4, + 2.3, + 1, + 0.567, + 2.3, + 0.733, + 2.48, + 0.9, + 2.9, + 1, + 1.156, + 3.545, + 1.411, + 3.9, + 1.667, + 3.9, + 1, + 1.933, + 3.9, + 2.2, + 3.807, + 2.467, + 3.5, + 1, + 2.767, + 3.154, + 3.067, + 2.9, + 3.367, + 2.9, + 1, + 3.489, + 2.9, + 3.611, + 3, + 3.733, + 3, + 1, + 3.989, + 3, + 4.244, + 3, + 4.5, + 3, + 0, + 4.533, + 3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0, + 0.9, + 0, + 1, + 0.978, + 0, + 1.056, + 0.5, + 1.133, + 0.5, + 1, + 2.256, + 0.5, + 3.378, + 0.5, + 4.5, + 0.5, + 0, + 4.533, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0.8, + 0.5, + 0.8, + 1, + 0.956, + 0.8, + 1.411, + -1, + 1.867, + -1, + 1, + 2.111, + -1, + 2.356, + -1, + 2.6, + -1, + 1, + 2.8, + -1, + 3, + -0.52, + 3.2, + -0.52, + 1, + 3.411, + -0.52, + 3.622, + -1, + 3.833, + -1, + 1, + 4.056, + -1, + 4.278, + -1, + 4.5, + -1, + 0, + 4.533, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0.5, + 1, + 1.5, + 0.5, + 3, + 0.5, + 4.5, + 0.5, + 0, + 4.533, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0.41, + 0.267, + 0.41, + 1, + 0.344, + 0.41, + 0.422, + -0.174, + 0.5, + -0.33, + 1, + 0.656, + -0.643, + 0.811, + -0.68, + 0.967, + -0.68, + 1, + 1.222, + -0.68, + 1.478, + 0.36, + 1.733, + 0.36, + 1, + 1.933, + 0.36, + 2.133, + 0.36, + 2.333, + 0.36, + 1, + 2.422, + 0.36, + 2.511, + -0.26, + 2.6, + -0.26, + 1, + 2.767, + -0.26, + 2.933, + -0.26, + 3.1, + -0.26, + 1, + 3.233, + -0.26, + 3.367, + -0.46, + 3.5, + -0.46, + 1, + 3.633, + -0.46, + 3.767, + -0.31, + 3.9, + -0.31, + 1, + 4.1, + -0.31, + 4.3, + -0.31, + 4.5, + -0.31, + 0, + 4.533, + -0.31 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 1.5, + 0, + 3, + 0, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 0, + 2, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 0, + 2, + 4.5, + 0, + 0, + 4.533, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 4.5, + 1, + 0, + 4.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 4.5, + 0, + 0, + 4.533, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m07.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 3.933, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 189, + "TotalPointCount": 462, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.3, + 1, + 2.6, + 1, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.467, + 0, + 0.933, + 9.534, + 1.4, + 14, + 1, + 1.567, + 15.595, + 1.733, + 15, + 1.9, + 15, + 1, + 2.289, + 15, + 2.678, + 0, + 3.067, + 0, + 1, + 3.344, + 0, + 3.622, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 0.061, + 0.733, + -1, + 1, + 0.9, + -1.723, + 1.067, + -13, + 1.233, + -13, + 1, + 1.422, + -13, + 1.611, + 5, + 1.8, + 5, + 1, + 2.189, + 5, + 2.578, + 0, + 2.967, + 0, + 1, + 3.278, + 0, + 3.589, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + -18, + 1.1, + -18, + 1, + 1.4, + -18, + 1.7, + -18, + 2, + -18, + 1, + 2.3, + -18, + 2.6, + 18, + 2.9, + 18, + 1, + 3.233, + 18, + 3.567, + 18, + 3.9, + 18, + 0, + 3.933, + 18 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.344, + 1, + 0.689, + 1, + 1.033, + 1, + 1, + 1.078, + 1, + 1.122, + 0, + 1.167, + 0, + 1, + 1.222, + 0, + 1.278, + 0, + 1.333, + 0, + 1, + 1.411, + 0, + 1.489, + 1, + 1.567, + 1, + 1, + 2.344, + 1, + 3.122, + 1, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.344, + 1, + 0.689, + 1, + 1.033, + 1, + 1, + 1.078, + 1, + 1.122, + 0, + 1.167, + 0, + 1, + 1.222, + 0, + 1.278, + 0, + 1.333, + 0, + 1, + 1.411, + 0, + 1.489, + 1, + 1.567, + 1, + 1, + 2.344, + 1, + 3.122, + 1, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + -0.27, + 0.9, + -0.27, + 1, + 0.989, + -0.27, + 1.078, + -0.27, + 1.167, + -0.27, + 1, + 1.2, + -0.27, + 1.233, + 0.37, + 1.267, + 0.37, + 1, + 1.378, + 0.37, + 1.489, + 0.37, + 1.6, + 0.37, + 1, + 1.944, + 0.37, + 2.289, + 0, + 2.633, + 0, + 1, + 3.056, + 0, + 3.478, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0.58, + 0.9, + 0.58, + 1, + 0.989, + 0.58, + 1.078, + 0.58, + 1.167, + 0.58, + 1, + 1.2, + 0.58, + 1.233, + 0.29, + 1.267, + 0.29, + 1, + 1.378, + 0.29, + 1.489, + 0.29, + 1.6, + 0.29, + 1, + 1.944, + 0.29, + 2.289, + 0, + 2.633, + 0, + 1, + 3.056, + 0, + 3.478, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 1.3, + 1, + 2.6, + 1, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.533, + 0, + 1.067, + -7, + 1.6, + -7, + 1, + 2.056, + -7, + 2.511, + 4, + 2.967, + 4, + 1, + 3.278, + 4, + 3.589, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.578, + 0, + 1.156, + -10, + 1.733, + -10, + 1, + 2.1, + -10, + 2.467, + -7.702, + 2.833, + -4, + 1, + 3.122, + -1.083, + 3.411, + 0, + 3.7, + 0, + 1, + 3.767, + 0, + 3.833, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0.5, + 1, + 0.156, + 0.5, + 0.311, + 0.734, + 0.467, + 0.76, + 1, + 1.533, + 0.94, + 2.6, + 1, + 3.667, + 1, + 1, + 3.744, + 1, + 3.822, + 0.5, + 3.9, + 0.5, + 0, + 3.933, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0.5, + 1, + 0.156, + 0.5, + 0.311, + 0.734, + 0.467, + 0.76, + 1, + 1.533, + 0.94, + 2.6, + 1, + 3.667, + 1, + 1, + 3.744, + 1, + 3.822, + 0.5, + 3.9, + 0.5, + 0, + 3.933, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 2.5, + 1, + 0.133, + 2.5, + 0.267, + 2.3, + 0.4, + 2.3, + 1, + 0.567, + 2.3, + 0.733, + 2.42, + 0.9, + 2.9, + 1, + 1.067, + 3.38, + 1.233, + 3.8, + 1.4, + 3.8, + 1, + 1.556, + 3.8, + 1.711, + 3.857, + 1.867, + 3.7, + 1, + 2.356, + 3.207, + 2.844, + 2.4, + 3.333, + 2.4, + 1, + 3.522, + 2.4, + 3.711, + 2.5, + 3.9, + 2.5, + 0, + 3.933, + 2.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 3.5, + 1, + 0.133, + 3.5, + 0.267, + 3.1, + 0.4, + 3.1, + 1, + 0.733, + 3.1, + 1.067, + 5, + 1.4, + 5, + 1, + 1.522, + 5, + 1.644, + 5, + 1.767, + 5, + 1, + 2, + 5, + 2.233, + 4.5, + 2.467, + 4.5, + 1, + 2.944, + 4.5, + 3.422, + 4.5, + 3.9, + 4.5, + 0, + 3.933, + 4.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + -0.42, + 0.267, + -0.42, + 1, + 0.344, + -0.42, + 0.422, + 0.35, + 0.5, + 0.35, + 1, + 0.833, + 0.35, + 1.167, + 0, + 1.5, + 0, + 1, + 1.622, + 0, + 1.744, + 0.36, + 1.867, + 0.36, + 1, + 2.111, + 0.36, + 2.356, + -1, + 2.6, + -1, + 1, + 2.867, + -1, + 3.133, + -0.92, + 3.4, + -0.92, + 1, + 3.567, + -0.92, + 3.733, + -0.92, + 3.9, + -0.92, + 0, + 3.933, + -0.92 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0.5, + 1, + 0.278, + 0.5, + 0.556, + 0.5, + 0.833, + 0.5, + 1, + 0.9, + 0.5, + 0.967, + 0, + 1.033, + 0, + 1, + 1.989, + 0, + 2.944, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + -0.39, + 0.267, + -0.39, + 1, + 0.344, + -0.39, + 0.422, + -0.33, + 0.5, + -0.33, + 1, + 0.656, + -0.33, + 0.811, + -0.68, + 0.967, + -0.68, + 1, + 1.222, + -0.68, + 1.478, + 0.36, + 1.733, + 0.36, + 1, + 1.933, + 0.36, + 2.133, + 0.36, + 2.333, + 0.36, + 1, + 2.422, + 0.36, + 2.511, + -0.26, + 2.6, + -0.26, + 1, + 2.767, + -0.26, + 2.933, + -0.26, + 3.1, + -0.26, + 1, + 3.367, + -0.26, + 3.633, + -0.26, + 3.9, + -0.26, + 0, + 3.933, + -0.26 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 1.3, + 0, + 2.6, + 0, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 0, + 2, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 0, + 2, + 3.9, + 0, + 0, + 3.933, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 3.9, + 1, + 0, + 3.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 3.9, + 0, + 0, + 3.933, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m08.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4.6, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 201, + "TotalPointCount": 498, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.522, + 1, + 3.044, + 1, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.556, + 0, + 0.611, + 0, + 0.667, + 0, + 1, + 0.756, + 0, + 0.844, + 0, + 0.933, + 0, + 1, + 1.022, + 0, + 1.111, + 0, + 1.2, + 0, + 1, + 1.322, + 0, + 1.444, + 0, + 1.567, + 0, + 1, + 1.733, + 0, + 1.9, + 0, + 2.067, + 0, + 1, + 2.156, + 0, + 2.244, + 0, + 2.333, + 0, + 1, + 2.7, + 0, + 3.067, + 3, + 3.433, + 3, + 1, + 3.589, + 3, + 3.744, + 0, + 3.9, + 0, + 1, + 4.122, + 0, + 4.344, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.556, + 0, + 0.611, + 10, + 0.667, + 10, + 1, + 0.756, + 10, + 0.844, + -19.633, + 0.933, + -25, + 1, + 1.022, + -30.367, + 1.111, + -30, + 1.2, + -30, + 1, + 1.322, + -30, + 1.444, + -30, + 1.567, + -30, + 1, + 1.733, + -30, + 1.9, + 5, + 2.067, + 5, + 1, + 2.156, + 5, + 2.244, + 0.198, + 2.333, + 0, + 1, + 2.7, + -0.815, + 3.067, + -1, + 3.433, + -1, + 1, + 3.589, + -1, + 3.744, + 0, + 3.9, + 0, + 1, + 4.122, + 0, + 4.344, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.933, + 0, + 1.867, + 0, + 2.8, + 0, + 1, + 2.978, + 0, + 3.156, + 30, + 3.333, + 30, + 1, + 3.5, + 30, + 3.667, + -1, + 3.833, + -1, + 1, + 3.922, + -1, + 4.011, + 0, + 4.1, + 0, + 1, + 4.256, + 0, + 4.411, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.222, + 1, + 0.444, + 1, + 0.667, + 1, + 1, + 0.756, + 1, + 0.844, + 1, + 0.933, + 1, + 1, + 0.978, + 1, + 1.022, + 0, + 1.067, + 0, + 1, + 1.233, + 0, + 1.4, + 0, + 1.567, + 0, + 1, + 1.744, + 0, + 1.922, + 1, + 2.1, + 1, + 1, + 2.333, + 1, + 2.567, + 1, + 2.8, + 1, + 1, + 2.978, + 1, + 3.156, + 0.87, + 3.333, + 0.87, + 1, + 3.456, + 0.87, + 3.578, + 1, + 3.7, + 1, + 1, + 3.989, + 1, + 4.278, + 1, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.222, + 1, + 0.444, + 1, + 0.667, + 1, + 1, + 0.756, + 1, + 0.844, + 1, + 0.933, + 1, + 1, + 0.978, + 1, + 1.022, + 0, + 1.067, + 0, + 1, + 1.233, + 0, + 1.4, + 0, + 1.567, + 0, + 1, + 1.744, + 0, + 1.922, + 1, + 2.1, + 1, + 1, + 2.333, + 1, + 2.567, + 1, + 2.8, + 1, + 1, + 2.978, + 1, + 3.156, + 0.87, + 3.333, + 0.87, + 1, + 3.456, + 0.87, + 3.578, + 1, + 3.7, + 1, + 1, + 3.989, + 1, + 4.278, + 1, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 0, + 0.933, + 0, + 1, + 0.978, + 0, + 1.022, + -1, + 1.067, + -1, + 1, + 1.233, + -1, + 1.4, + -1, + 1.567, + -1, + 1, + 1.744, + -1, + 1.922, + 0, + 2.1, + 0, + 1, + 2.333, + 0, + 2.567, + 0, + 2.8, + 0, + 1, + 2.978, + 0, + 3.156, + -0.35, + 3.333, + -0.35, + 1, + 3.5, + -0.35, + 3.667, + 0, + 3.833, + 0, + 1, + 4.078, + 0, + 4.322, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 0, + 0.933, + 0, + 1, + 0.978, + 0, + 1.022, + -1, + 1.067, + -1, + 1, + 1.233, + -1, + 1.4, + -1, + 1.567, + -1, + 1, + 1.744, + -1, + 1.922, + 0, + 2.1, + 0, + 1, + 2.333, + 0, + 2.567, + 0, + 2.8, + 0, + 1, + 2.978, + 0, + 3.156, + -0.35, + 3.333, + -0.35, + 1, + 3.5, + -0.35, + 3.667, + 0, + 3.833, + 0, + 1, + 4.078, + 0, + 4.322, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 1.522, + 1, + 3.044, + 1, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.178, + 0, + 0.356, + 0, + 0.533, + 0, + 1, + 0.689, + 0, + 0.844, + -10, + 1, + -10, + 1, + 1.189, + -10, + 1.378, + -10, + 1.567, + -10, + 1, + 1.7, + -10, + 1.833, + 0, + 1.967, + 0, + 1, + 2.133, + 0, + 2.3, + 0, + 2.467, + 0, + 1, + 3.167, + 0, + 3.867, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.178, + 0, + 0.356, + 0, + 0.533, + 0, + 1, + 0.689, + 0, + 0.844, + 8.088, + 1, + 9, + 1, + 1.189, + 10.108, + 1.378, + 10, + 1.567, + 10, + 1, + 1.7, + 10, + 1.833, + 0, + 1.967, + 0, + 1, + 2.133, + 0, + 2.3, + 2, + 2.467, + 2, + 1, + 2.611, + 2, + 2.756, + 0, + 2.9, + 0, + 1, + 3.456, + 0, + 4.011, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0.91, + 1, + 0.678, + 0.91, + 1.356, + 0, + 2.033, + 0, + 1, + 2.422, + 0, + 2.811, + 1, + 3.2, + 1, + 1, + 3.656, + 1, + 4.111, + 1, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0.5, + 1, + 0.178, + 0.5, + 0.356, + 0.5, + 0.533, + 0.5, + 1, + 0.733, + 0.5, + 0.933, + 1, + 1.133, + 1, + 1, + 2.278, + 1, + 3.422, + 1, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0.5, + 1, + 0.178, + 0.5, + 0.356, + 0.5, + 0.533, + 0.5, + 1, + 0.733, + 0.5, + 0.933, + 1, + 1.133, + 1, + 1, + 2.278, + 1, + 3.422, + 1, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.078, + 0, + 1.156, + 0.19, + 1.233, + 0.19, + 1, + 2.344, + 0.19, + 3.456, + 0.19, + 4.567, + 0.19, + 0, + 4.6, + 0.19 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.078, + 0, + 1.156, + -0.17, + 1.233, + -0.17, + 1, + 2.344, + -0.17, + 3.456, + -0.17, + 4.567, + -0.17, + 0, + 4.6, + -0.17 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 1.522, + 0, + 3.044, + 0, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 4.567, + 0, + 0, + 4.6, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 4.567, + 1, + 0, + 4.6, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 4.567, + 0, + 0, + 4.6, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m09.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4.033, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 371, + "TotalPointCount": 968, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 0.956, + 1, + 1.911, + 1, + 2.867, + 1, + 1, + 3.244, + 1, + 3.622, + 1, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 1, + 1.133, + 1, + 1, + 1.311, + 1, + 1.489, + -0.44, + 1.667, + -0.984, + 1, + 1.833, + -1.291, + 2, + -1.455, + 2.167, + -1.455, + 1, + 2.344, + -1.455, + 2.522, + 0.37, + 2.7, + 0.37, + 1, + 2.944, + 0.37, + 3.189, + 0, + 3.433, + 0, + 1, + 3.622, + 0, + 3.811, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.267, + 0, + 0.533, + 0, + 0.8, + 0, + 1, + 0.956, + 0, + 1.111, + 17, + 1.267, + 17, + 1, + 1.444, + 17, + 1.622, + -16, + 1.8, + -16, + 1, + 1.978, + -16, + 2.156, + -16, + 2.333, + -16, + 1, + 2.411, + -16, + 2.489, + -16.369, + 2.567, + -15.184, + 1, + 2.7, + -13.151, + 2.833, + 1, + 2.967, + 1, + 1, + 3.233, + 1, + 3.5, + 0, + 3.767, + 0, + 1, + 3.844, + 0, + 3.922, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 3, + 1.133, + 3, + 1, + 1.311, + 3, + 1.489, + 0.06, + 1.667, + -1, + 1, + 1.844, + -2.06, + 2.022, + -2, + 2.2, + -2, + 1, + 2.378, + -2, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.222, + 1, + 0.444, + 1, + 0.667, + 1, + 1, + 0.822, + 1, + 0.978, + 1.1, + 1.133, + 1.1, + 1, + 1.311, + 1.1, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.3, + 0, + 2.4, + -0.009, + 2.5, + 0.2, + 1, + 2.578, + 0.362, + 2.656, + 1.1, + 2.733, + 1.1, + 1, + 2.933, + 1.1, + 3.133, + 1, + 3.333, + 1, + 1, + 3.556, + 1, + 3.778, + 1, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.222, + 1, + 0.444, + 1, + 0.667, + 1, + 1, + 0.822, + 1, + 0.978, + 1.1, + 1.133, + 1.1, + 1, + 1.311, + 1.1, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.3, + 0, + 2.4, + -0.009, + 2.5, + 0.2, + 1, + 2.578, + 0.362, + 2.656, + 1.1, + 2.733, + 1.1, + 1, + 2.933, + 1.1, + 3.133, + 1, + 3.333, + 1, + 1, + 3.556, + 1, + 3.778, + 1, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0.1, + 1.133, + 0.1, + 1, + 1.311, + 0.1, + 1.489, + -0.3, + 1.667, + -0.3, + 1, + 1.844, + -0.3, + 2.022, + -0.3, + 2.2, + -0.3, + 1, + 2.378, + -0.3, + 2.556, + 0.1, + 2.733, + 0.1, + 1, + 2.933, + 0.1, + 3.133, + 0, + 3.333, + 0, + 1, + 3.556, + 0, + 3.778, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0.1, + 1.133, + 0.1, + 1, + 1.311, + 0.1, + 1.489, + -0.3, + 1.667, + -0.3, + 1, + 1.844, + -0.3, + 2.022, + -0.3, + 2.2, + -0.3, + 1, + 2.378, + -0.3, + 2.556, + 0.1, + 2.733, + 0.1, + 1, + 2.933, + 0.1, + 3.133, + 0, + 3.333, + 0, + 1, + 3.556, + 0, + 3.778, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0.1, + 1.133, + 0.1, + 1, + 1.311, + 0.1, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0.1, + 1.133, + 0.1, + 1, + 1.311, + 0.1, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0.1, + 1.133, + 0.1, + 1, + 1.311, + 0.1, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0.1, + 1.133, + 0.1, + 1, + 1.311, + 0.1, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.222, + 1, + 0.444, + 1, + 0.667, + 1, + 1, + 0.822, + 1, + 0.978, + 1, + 1.133, + 1, + 1, + 1.311, + 1, + 1.489, + 1, + 1.667, + 1, + 1, + 1.844, + 1, + 2.022, + 1, + 2.2, + 1, + 1, + 2.378, + 1, + 2.556, + 1, + 2.733, + 1, + 1, + 3.156, + 1, + 3.578, + 1, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + -1, + 1.133, + -1, + 1, + 1.311, + -1, + 1.489, + -0.667, + 1.667, + 0, + 1, + 1.844, + 0.667, + 2.022, + 1, + 2.2, + 1, + 1, + 2.378, + 1, + 2.556, + -1, + 2.733, + -1, + 1, + 2.978, + -1, + 3.222, + 0, + 3.467, + 0, + 1, + 3.644, + 0, + 3.822, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 2, + 1.133, + 2, + 1, + 1.311, + 2, + 1.489, + -2.258, + 1.667, + -2.668, + 1, + 1.844, + -3.079, + 2.022, + -3, + 2.2, + -3, + 1, + 2.378, + -3, + 2.556, + 1, + 2.733, + 1, + 1, + 2.978, + 1, + 3.222, + 0, + 3.467, + 0, + 1, + 3.644, + 0, + 3.822, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 1, + 1.133, + 1, + 1, + 1.311, + 1, + 1.489, + -0.112, + 1.667, + -1, + 1, + 1.844, + -1.888, + 2.022, + -2, + 2.2, + -2, + 1, + 2.378, + -2, + 2.556, + 1, + 2.733, + 1, + 1, + 2.978, + 1, + 3.222, + 0, + 3.467, + 0, + 1, + 3.644, + 0, + 3.822, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + -1, + 1.133, + -1, + 1, + 1.311, + -1, + 1.489, + 1.94, + 1.667, + 3, + 1, + 1.844, + 4.06, + 2.022, + 4, + 2.2, + 4, + 1, + 2.378, + 4, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 1, + 1, + 0.222, + 1, + 0.444, + 1, + 0.667, + 1, + 1, + 0.822, + 1, + 0.978, + 1, + 1.133, + 1, + 1, + 1.311, + 1, + 1.489, + 1, + 1.667, + 1, + 1, + 1.844, + 1, + 2.022, + 1, + 2.2, + 1, + 1, + 2.378, + 1, + 2.556, + 1, + 2.733, + 1, + 1, + 3.156, + 1, + 3.578, + 1, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 1, + 1, + 0.222, + 1, + 0.444, + 1, + 0.667, + 1, + 1, + 0.822, + 1, + 0.978, + 1, + 1.133, + 1, + 1, + 1.311, + 1, + 1.489, + 1, + 1.667, + 1, + 1, + 1.844, + 1, + 2.022, + 1, + 2.2, + 1, + 1, + 2.378, + 1, + 2.556, + 1, + 2.733, + 1, + 1, + 3.156, + 1, + 3.578, + 1, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 5, + 1, + 0.222, + 5, + 0.444, + 5, + 0.667, + 5, + 1, + 0.822, + 5, + 0.978, + 5, + 1.133, + 5, + 1, + 1.311, + 5, + 1.489, + 5, + 1.667, + 5, + 1, + 1.844, + 5, + 2.022, + 5, + 2.2, + 5, + 1, + 2.378, + 5, + 2.556, + 5, + 2.733, + 5, + 1, + 3.156, + 5, + 3.578, + 5, + 4, + 5, + 0, + 4.033, + 5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + -0.1, + 1, + 0.222, + -0.1, + 0.444, + -0.1, + 0.667, + -0.1, + 1, + 0.822, + -0.1, + 0.978, + -0.1, + 1.133, + -0.1, + 1, + 1.311, + -0.1, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + -0.1, + 2.733, + -0.1, + 1, + 3.156, + -0.1, + 3.578, + -0.1, + 4, + -0.1, + 0, + 4.033, + -0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0.1, + 1, + 0.222, + 0.1, + 0.444, + 0.1, + 0.667, + 0.1, + 1, + 0.822, + 0.1, + 0.978, + 0.1, + 1.133, + 0.1, + 1, + 1.311, + 0.1, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0.1, + 2.733, + 0.1, + 1, + 3.156, + 0.1, + 3.578, + 0.1, + 4, + 0.1, + 0, + 4.033, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.844, + 0, + 2.022, + 0, + 2.2, + 0, + 1, + 2.378, + 0, + 2.556, + 0, + 2.733, + 0, + 1, + 3.156, + 0, + 3.578, + 0, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 2.867, + 0, + 2, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 2.867, + 0, + 2, + 4, + 0, + 0, + 4.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 2.867, + 1, + 2, + 4, + 1, + 0, + 4.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 2.867, + 0, + 2, + 4, + 0, + 0, + 4.033, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m10.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 5.533, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 516, + "TotalPointCount": 1403, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.133, + 1, + 2.267, + 1, + 3.4, + 1, + 1, + 4.1, + 1, + 4.8, + 1, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 0.15, + 0.7, + 2, + 1, + 0.844, + 4.186, + 0.989, + 7, + 1.133, + 7, + 1, + 1.211, + 7, + 1.289, + 7.283, + 1.367, + 6.75, + 1, + 1.622, + 4.997, + 1.878, + -2, + 2.133, + -2, + 1, + 2.611, + -2, + 3.089, + -2, + 3.567, + -2, + 1, + 3.667, + -2, + 3.767, + 2, + 3.867, + 2, + 1, + 4.178, + 2, + 4.489, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.133, + 0, + 0.267, + 0, + 0.4, + 0, + 1, + 0.467, + 0, + 0.533, + 27, + 0.6, + 27, + 1, + 0.656, + 27, + 0.711, + -11.41, + 0.767, + -15, + 1, + 0.911, + -24.333, + 1.056, + -26, + 1.2, + -26, + 1, + 1.344, + -26, + 1.489, + -25.361, + 1.633, + -25.361, + 1, + 1.756, + -25.361, + 1.878, + -30, + 2, + -30, + 1, + 2.111, + -30, + 2.222, + -3, + 2.333, + -3, + 1, + 2.744, + -3, + 3.156, + -3, + 3.567, + -3, + 1, + 3.689, + -3, + 3.811, + -22, + 3.933, + -22, + 1, + 4.089, + -22, + 4.244, + 12, + 4.4, + 12, + 1, + 4.556, + 12, + 4.711, + 0, + 4.867, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 1, + 0.7, + 1, + 1, + 0.844, + 1, + 0.989, + -5, + 1.133, + -5, + 1, + 1.211, + -5, + 1.289, + -5.066, + 1.367, + -4.944, + 1, + 1.622, + -4.546, + 1.878, + -3, + 2.133, + -3, + 1, + 2.611, + -3, + 3.089, + -3, + 3.567, + -3, + 1, + 3.667, + -3, + 3.767, + -9, + 3.867, + -9, + 1, + 4.022, + -9, + 4.178, + 7, + 4.333, + 7, + 1, + 4.489, + 7, + 4.644, + -2, + 4.8, + -2, + 1, + 4.989, + -2, + 5.178, + 0, + 5.367, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 0.5, + 0.7, + 0.5, + 1, + 0.844, + 0.5, + 0.989, + 0.5, + 1.133, + 0.5, + 1, + 1.211, + 0.5, + 1.289, + 0.5, + 1.367, + 0.5, + 1, + 1.622, + 0.5, + 1.878, + 0.5, + 2.133, + 0.5, + 1, + 2.611, + 0.5, + 3.089, + 0.5, + 3.567, + 0.5, + 1, + 3.978, + 0.5, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.111, + 1, + 0.222, + 1, + 0.333, + 1, + 1, + 0.4, + 1, + 0.467, + 2, + 0.533, + 2, + 1, + 0.589, + 2, + 0.644, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.3, + 0, + 1.467, + 0, + 1.633, + 0, + 1, + 1.756, + 0, + 1.878, + 0, + 2, + 0, + 1, + 2.044, + 0, + 2.089, + 1.5, + 2.133, + 1.5, + 1, + 2.2, + 1.5, + 2.267, + 1.5, + 2.333, + 1.5, + 1, + 2.389, + 1.5, + 2.444, + 0, + 2.5, + 0, + 1, + 2.533, + 0, + 2.567, + 0, + 2.6, + 0, + 1, + 2.644, + 0, + 2.689, + 0.788, + 2.733, + 1.4, + 1, + 2.744, + 1.553, + 2.756, + 1.5, + 2.767, + 1.5, + 1, + 2.822, + 1.5, + 2.878, + 0, + 2.933, + 0, + 1, + 2.944, + 0, + 2.956, + 0, + 2.967, + 0, + 1, + 3.011, + 0, + 3.056, + 1.4, + 3.1, + 1.4, + 1, + 3.256, + 1.4, + 3.411, + 1.4, + 3.567, + 1.4, + 1, + 3.667, + 1.4, + 3.767, + 0, + 3.867, + 0, + 1, + 4.022, + 0, + 4.178, + 0, + 4.333, + 0, + 1, + 4.489, + 0, + 4.644, + 1, + 4.8, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 1, + 0.533, + 1, + 1, + 0.589, + 1, + 0.644, + 1, + 0.7, + 1, + 1, + 0.844, + 1, + 0.989, + 1, + 1.133, + 1, + 1, + 1.3, + 1, + 1.467, + 1.009, + 1.633, + 0.913, + 1, + 1.756, + 0.843, + 1.878, + 0, + 2, + 0, + 1, + 2.044, + 0, + 2.089, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.111, + 1, + 0.222, + 1, + 0.333, + 1, + 1, + 0.4, + 1, + 0.467, + 2, + 0.533, + 2, + 1, + 0.589, + 2, + 0.644, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.3, + 0, + 1.467, + 0, + 1.633, + 0, + 1, + 1.756, + 0, + 1.878, + 0, + 2, + 0, + 1, + 2.044, + 0, + 2.089, + 1.5, + 2.133, + 1.5, + 1, + 2.2, + 1.5, + 2.267, + 1.5, + 2.333, + 1.5, + 1, + 2.389, + 1.5, + 2.444, + 0, + 2.5, + 0, + 1, + 2.533, + 0, + 2.567, + 0, + 2.6, + 0, + 1, + 2.644, + 0, + 2.689, + 0.788, + 2.733, + 1.4, + 1, + 2.744, + 1.553, + 2.756, + 1.5, + 2.767, + 1.5, + 1, + 2.822, + 1.5, + 2.878, + 0, + 2.933, + 0, + 1, + 2.944, + 0, + 2.956, + 0, + 2.967, + 0, + 1, + 3.011, + 0, + 3.056, + 1.4, + 3.1, + 1.4, + 1, + 3.256, + 1.4, + 3.411, + 1.4, + 3.567, + 1.4, + 1, + 3.667, + 1.4, + 3.767, + 0, + 3.867, + 0, + 1, + 4.022, + 0, + 4.178, + 0, + 4.333, + 0, + 1, + 4.489, + 0, + 4.644, + 1, + 4.8, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 1, + 0.533, + 1, + 1, + 0.589, + 1, + 0.644, + 1, + 0.7, + 1, + 1, + 0.844, + 1, + 0.989, + 1, + 1.133, + 1, + 1, + 1.3, + 1, + 1.467, + 1.009, + 1.633, + 0.913, + 1, + 1.756, + 0.843, + 1.878, + 0, + 2, + 0, + 1, + 2.044, + 0, + 2.089, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.211, + 0, + 1.289, + 0, + 1.367, + 0, + 1, + 1.622, + 0, + 1.878, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + -0.6, + 0.533, + -0.6, + 1, + 0.589, + -0.6, + 0.644, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.211, + 0, + 1.289, + 0, + 1.367, + 0, + 1, + 1.622, + 0, + 1.878, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.211, + 0, + 1.289, + 0, + 1.367, + 0, + 1, + 1.622, + 0, + 1.878, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 0.1, + 0.533, + 0.1, + 1, + 0.589, + 0.1, + 0.644, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.3, + 0, + 1.467, + 0, + 1.633, + 0, + 1, + 1.756, + 0, + 1.878, + 0, + 2, + 0, + 1, + 2.044, + 0, + 2.089, + 0, + 2.133, + 0, + 1, + 2.2, + 0, + 2.267, + 0, + 2.333, + 0, + 1, + 2.389, + 0, + 2.444, + -0.1, + 2.5, + -0.1, + 1, + 2.533, + -0.1, + 2.567, + -0.1, + 2.6, + -0.1, + 1, + 2.644, + -0.1, + 2.689, + 0, + 2.733, + 0, + 1, + 2.744, + 0, + 2.756, + 0, + 2.767, + 0, + 1, + 2.822, + 0, + 2.878, + -0.1, + 2.933, + -0.1, + 1, + 2.944, + -0.1, + 2.956, + -0.1, + 2.967, + -0.1, + 1, + 3.011, + -0.1, + 3.056, + 0, + 3.1, + 0, + 1, + 3.256, + 0, + 3.411, + 0, + 3.567, + 0, + 1, + 3.667, + 0, + 3.767, + -0.2, + 3.867, + -0.2, + 1, + 4.022, + -0.2, + 4.178, + -0.2, + 4.333, + -0.2, + 1, + 4.489, + -0.2, + 4.644, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 0.2, + 0.533, + 0.2, + 1, + 0.589, + 0.2, + 0.644, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.3, + 0, + 1.467, + 0, + 1.633, + 0, + 1, + 1.756, + 0, + 1.878, + 0, + 2, + 0, + 1, + 2.044, + 0, + 2.089, + 0, + 2.133, + 0, + 1, + 2.2, + 0, + 2.267, + 0, + 2.333, + 0, + 1, + 2.389, + 0, + 2.444, + -0.1, + 2.5, + -0.1, + 1, + 2.533, + -0.1, + 2.567, + -0.1, + 2.6, + -0.1, + 1, + 2.644, + -0.1, + 2.689, + 0, + 2.733, + 0, + 1, + 2.744, + 0, + 2.756, + 0, + 2.767, + 0, + 1, + 2.822, + 0, + 2.878, + -0.1, + 2.933, + -0.1, + 1, + 2.944, + -0.1, + 2.956, + -0.1, + 2.967, + -0.1, + 1, + 3.011, + -0.1, + 3.056, + 0, + 3.1, + 0, + 1, + 3.256, + 0, + 3.411, + 0, + 3.567, + 0, + 1, + 3.667, + 0, + 3.767, + -0.2, + 3.867, + -0.2, + 1, + 4.022, + -0.2, + 4.178, + -0.2, + 4.333, + -0.2, + 1, + 4.489, + -0.2, + 4.644, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 0.7, + 0.533, + 0.7, + 1, + 0.589, + 0.7, + 0.644, + -0.6, + 0.7, + -0.6, + 1, + 0.844, + -0.6, + 0.989, + -0.6, + 1.133, + -0.6, + 1, + 1.3, + -0.6, + 1.467, + -0.564, + 1.633, + -0.564, + 1, + 1.756, + -0.564, + 1.878, + -0.7, + 2, + -0.7, + 1, + 2.044, + -0.7, + 2.089, + 0.7, + 2.133, + 0.7, + 1, + 2.2, + 0.7, + 2.267, + 0.7, + 2.333, + 0.7, + 1, + 2.389, + 0.7, + 2.444, + 0.6, + 2.5, + 0.6, + 1, + 2.533, + 0.6, + 2.567, + 0.6, + 2.6, + 0.6, + 1, + 2.644, + 0.6, + 2.689, + 0.7, + 2.733, + 0.7, + 1, + 2.744, + 0.7, + 2.756, + 0.7, + 2.767, + 0.7, + 1, + 2.811, + 0.7, + 2.856, + 0.6, + 2.9, + 0.6, + 1, + 2.922, + 0.6, + 2.944, + 0.6, + 2.967, + 0.6, + 1, + 3.011, + 0.6, + 3.056, + 0.7, + 3.1, + 0.7, + 1, + 3.256, + 0.7, + 3.411, + 0.7, + 3.567, + 0.7, + 1, + 3.667, + 0.7, + 3.767, + -0.5, + 3.867, + -0.5, + 1, + 4.178, + -0.5, + 4.489, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 0.7, + 0.533, + 0.7, + 1, + 0.589, + 0.7, + 0.644, + -0.6, + 0.7, + -0.6, + 1, + 0.844, + -0.6, + 0.989, + -0.6, + 1.133, + -0.6, + 1, + 1.3, + -0.6, + 1.467, + -0.564, + 1.633, + -0.564, + 1, + 1.756, + -0.564, + 1.878, + -0.7, + 2, + -0.7, + 1, + 2.044, + -0.7, + 2.089, + 0.7, + 2.133, + 0.7, + 1, + 2.2, + 0.7, + 2.267, + 0.7, + 2.333, + 0.7, + 1, + 2.389, + 0.7, + 2.444, + 0.6, + 2.5, + 0.6, + 1, + 2.533, + 0.6, + 2.567, + 0.6, + 2.6, + 0.6, + 1, + 2.644, + 0.6, + 2.689, + 0.7, + 2.733, + 0.7, + 1, + 2.744, + 0.7, + 2.756, + 0.7, + 2.767, + 0.7, + 1, + 2.811, + 0.7, + 2.856, + 0.6, + 2.9, + 0.6, + 1, + 2.922, + 0.6, + 2.944, + 0.6, + 2.967, + 0.6, + 1, + 3.011, + 0.6, + 3.056, + 0.7, + 3.1, + 0.7, + 1, + 3.256, + 0.7, + 3.411, + 0.7, + 3.567, + 0.7, + 1, + 3.667, + 0.7, + 3.767, + -0.5, + 3.867, + -0.5, + 1, + 4.178, + -0.5, + 4.489, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 0.4, + 0.533, + 0.4, + 1, + 0.589, + 0.4, + 0.644, + -0.4, + 0.7, + -0.4, + 1, + 0.844, + -0.4, + 0.989, + -0.4, + 1.133, + -0.4, + 1, + 1.3, + -0.4, + 1.467, + -0.378, + 1.633, + -0.378, + 1, + 1.756, + -0.378, + 1.878, + -0.378, + 2, + -0.378, + 1, + 2.044, + -0.378, + 2.089, + 0.4, + 2.133, + 0.4, + 1, + 2.611, + 0.4, + 3.089, + 0.4, + 3.567, + 0.4, + 1, + 3.667, + 0.4, + 3.767, + -0.4, + 3.867, + -0.4, + 1, + 4.178, + -0.4, + 4.489, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 0.4, + 0.533, + 0.4, + 1, + 0.589, + 0.4, + 0.644, + -0.4, + 0.7, + -0.4, + 1, + 0.844, + -0.4, + 0.989, + -0.4, + 1.133, + -0.4, + 1, + 1.3, + -0.4, + 1.467, + -0.378, + 1.633, + -0.378, + 1, + 1.756, + -0.378, + 1.878, + -0.378, + 2, + -0.378, + 1, + 2.044, + -0.378, + 2.089, + 0.4, + 2.133, + 0.4, + 1, + 2.611, + 0.4, + 3.089, + 0.4, + 3.567, + 0.4, + 1, + 3.978, + 0.4, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 0.5, + 0.533, + 0.5, + 1, + 0.589, + 0.5, + 0.644, + -0.4, + 0.7, + -0.4, + 1, + 0.844, + -0.4, + 0.989, + -0.4, + 1.133, + -0.4, + 1, + 1.3, + -0.4, + 1.467, + -0.378, + 1.633, + -0.378, + 1, + 1.756, + -0.378, + 1.878, + -0.378, + 2, + -0.378, + 1, + 2.044, + -0.378, + 2.089, + 0.4, + 2.133, + 0.4, + 1, + 2.611, + 0.4, + 3.089, + 0.4, + 3.567, + 0.4, + 1, + 3.667, + 0.4, + 3.767, + 0, + 3.867, + 0, + 1, + 4.178, + 0, + 4.489, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 0.5, + 0.533, + 0.5, + 1, + 0.589, + 0.5, + 0.644, + -0.4, + 0.7, + -0.4, + 1, + 0.844, + -0.4, + 0.989, + -0.4, + 1.133, + -0.4, + 1, + 1.3, + -0.4, + 1.467, + -0.378, + 1.633, + -0.378, + 1, + 1.756, + -0.378, + 1.878, + -0.378, + 2, + -0.378, + 1, + 2.044, + -0.378, + 2.089, + 0.4, + 2.133, + 0.4, + 1, + 2.611, + 0.4, + 3.089, + 0.4, + 3.567, + 0.4, + 1, + 3.667, + 0.4, + 3.767, + 0, + 3.867, + 0, + 1, + 4.178, + 0, + 4.489, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 0.4, + 0.533, + 0.4, + 1, + 0.589, + 0.4, + 0.644, + -0.8, + 0.7, + -0.8, + 1, + 0.844, + -0.8, + 0.989, + -0.8, + 1.133, + -0.8, + 1, + 1.3, + -0.8, + 1.467, + -0.767, + 1.633, + -0.767, + 1, + 1.756, + -0.767, + 1.878, + -0.767, + 2, + -0.767, + 1, + 2.044, + -0.767, + 2.089, + 0.4, + 2.133, + 0.4, + 1, + 2.611, + 0.4, + 3.089, + 0.4, + 3.567, + 0.4, + 1, + 3.667, + 0.4, + 3.767, + -0.3, + 3.867, + -0.3, + 1, + 4.178, + -0.3, + 4.489, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 0.4, + 0.533, + 0.4, + 1, + 0.589, + 0.4, + 0.644, + -0.8, + 0.7, + -0.8, + 1, + 0.844, + -0.8, + 0.989, + -0.8, + 1.133, + -0.8, + 1, + 1.3, + -0.8, + 1.467, + -0.767, + 1.633, + -0.767, + 1, + 1.756, + -0.767, + 1.878, + -0.767, + 2, + -0.767, + 1, + 2.044, + -0.767, + 2.089, + 0.4, + 2.133, + 0.4, + 1, + 2.611, + 0.4, + 3.089, + 0.4, + 3.567, + 0.4, + 1, + 3.667, + 0.4, + 3.767, + -0.3, + 3.867, + -0.3, + 1, + 4.178, + -0.3, + 4.489, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.111, + 1, + 0.222, + 1, + 0.333, + 1, + 1, + 0.456, + 1, + 0.578, + -0.6, + 0.7, + -0.6, + 1, + 0.844, + -0.6, + 0.989, + -0.6, + 1.133, + -0.6, + 1, + 1.3, + -0.6, + 1.467, + -0.6, + 1.633, + -0.6, + 1, + 1.756, + -0.6, + 1.878, + -0.6, + 2, + -0.6, + 1, + 2.044, + -0.6, + 2.089, + -0.6, + 2.133, + -0.6, + 1, + 2.611, + -0.6, + 3.089, + -0.6, + 3.567, + -0.6, + 1, + 3.667, + -0.6, + 3.767, + -0.005, + 3.867, + 0.2, + 1, + 4.178, + 0.839, + 4.489, + 1, + 4.8, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 0.3, + 0.533, + 0.3, + 1, + 0.589, + 0.3, + 0.644, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.3, + 0, + 1.467, + 0.006, + 1.633, + 0.006, + 1, + 1.756, + 0.006, + 1.878, + 0.006, + 2, + 0.006, + 1, + 2.044, + 0.006, + 2.089, + 0.2, + 2.133, + 0.2, + 1, + 2.611, + 0.2, + 3.089, + 0.2, + 3.567, + 0.2, + 1, + 3.667, + 0.2, + 3.767, + 0, + 3.867, + 0, + 1, + 4.178, + 0, + 4.489, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.211, + 0, + 1.289, + 0, + 1.367, + 0, + 1, + 1.622, + 0, + 1.878, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 7, + 0.7, + 7, + 1, + 0.844, + 7, + 0.989, + 7, + 1.133, + 7, + 1, + 1.211, + 7, + 1.289, + 7.224, + 1.367, + 6.805, + 1, + 1.622, + 5.431, + 1.878, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.4, + 0, + 0.467, + 5, + 0.533, + 5, + 1, + 0.589, + 5, + 0.644, + -6, + 0.7, + -6, + 1, + 0.844, + -6, + 0.989, + -5.999, + 1.133, + -5.953, + 1, + 1.211, + -5.928, + 1.289, + -5.922, + 1.367, + -5.445, + 1, + 1.622, + -3.877, + 1.878, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.667, + 0, + 3.767, + -4, + 3.867, + -4, + 1, + 4.022, + -4, + 4.178, + 4, + 4.333, + 4, + 1, + 4.489, + 4, + 4.644, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + -4, + 0.7, + -4, + 1, + 0.844, + -4, + 0.989, + -4, + 1.133, + -4, + 1, + 1.211, + -4, + 1.289, + -4.13, + 1.367, + -3.889, + 1, + 1.622, + -3.096, + 1.878, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 8.063, + 0.7, + 9, + 1, + 0.844, + 10.107, + 0.989, + 10, + 1.133, + 10, + 1, + 1.367, + 10, + 1.6, + 10.026, + 1.833, + 9.364, + 1, + 1.978, + 8.954, + 2.122, + 0, + 2.267, + 0, + 1, + 2.7, + 0, + 3.133, + 0, + 3.567, + 0, + 1, + 3.667, + 0, + 3.767, + 1.903, + 3.867, + 3, + 1, + 4.022, + 4.706, + 4.178, + 5, + 4.333, + 5, + 1, + 4.489, + 5, + 4.644, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.211, + 0, + 1.289, + 0, + 1.367, + 0, + 1, + 1.622, + 0, + 1.878, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.211, + 0, + 1.289, + 0, + 1.367, + 0, + 1, + 1.622, + 0, + 1.878, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 1, + 1, + 0.111, + 1, + 0.222, + 1, + 0.333, + 1, + 1, + 0.4, + 1, + 0.467, + 0.281, + 0.533, + 0.2, + 1, + 0.589, + 0.133, + 0.644, + 0.124, + 0.7, + 0.1, + 1, + 0.867, + 0.027, + 1.033, + 0, + 1.2, + 0, + 1, + 1.322, + 0, + 1.444, + -0.006, + 1.567, + 0.008, + 1, + 1.8, + 0.036, + 2.033, + 0.3, + 2.267, + 0.3, + 1, + 2.4, + 0.3, + 2.533, + 0.264, + 2.667, + 0.264, + 1, + 2.967, + 0.264, + 3.267, + 0.27, + 3.567, + 0.3, + 1, + 3.667, + 0.31, + 3.767, + 0.4, + 3.867, + 0.4, + 1, + 4.022, + 0.4, + 4.178, + 0.4, + 4.333, + 0.4, + 1, + 4.489, + 0.4, + 4.644, + 1, + 4.8, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 1, + 1, + 0.111, + 1, + 0.222, + 1, + 0.333, + 1, + 1, + 0.4, + 1, + 0.467, + 0.281, + 0.533, + 0.2, + 1, + 0.589, + 0.133, + 0.644, + 0.124, + 0.7, + 0.1, + 1, + 0.867, + 0.027, + 1.033, + 0, + 1.2, + 0, + 1, + 1.322, + 0, + 1.444, + -0.006, + 1.567, + 0.008, + 1, + 1.8, + 0.036, + 2.033, + 0.3, + 2.267, + 0.3, + 1, + 2.4, + 0.3, + 2.533, + 0.263, + 2.667, + 0.263, + 1, + 2.967, + 0.263, + 3.267, + 0.268, + 3.567, + 0.3, + 1, + 3.667, + 0.311, + 3.767, + 0.4, + 3.867, + 0.4, + 1, + 4.022, + 0.4, + 4.178, + 0.4, + 4.333, + 0.4, + 1, + 4.489, + 0.4, + 4.644, + 1, + 4.8, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 5, + 1, + 0.111, + 5, + 0.222, + 5, + 0.333, + 5, + 1, + 0.456, + 5, + 0.578, + 0, + 0.7, + 0, + 1, + 0.867, + 0, + 1.033, + 0, + 1.2, + 0, + 1, + 1.322, + 0, + 1.444, + 0, + 1.567, + 0, + 1, + 1.8, + 0, + 2.033, + 0, + 2.267, + 0, + 1, + 2.7, + 0, + 3.133, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 5, + 4.8, + 5, + 0, + 5.533, + 5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 0, + 0.7, + 0, + 1, + 0.867, + 0, + 1.033, + 0, + 1.2, + 0, + 1, + 1.322, + 0, + 1.444, + 0, + 1.567, + 0, + 1, + 1.8, + 0, + 2.033, + 0, + 2.267, + 0, + 1, + 2.7, + 0, + 3.133, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 0, + 0.7, + 0, + 1, + 0.889, + 0, + 1.078, + 0, + 1.267, + 0, + 1, + 1.4, + 0, + 1.533, + 0, + 1.667, + 0, + 1, + 1.9, + 0, + 2.133, + 0, + 2.367, + 0, + 1, + 2.767, + 0, + 3.167, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + -0.1, + 1, + 0.111, + -0.1, + 0.222, + -0.1, + 0.333, + -0.1, + 1, + 0.456, + -0.1, + 0.578, + 0, + 0.7, + 0, + 1, + 0.889, + 0, + 1.078, + 0, + 1.267, + 0, + 1, + 1.4, + 0, + 1.533, + 0, + 1.667, + 0, + 1, + 1.9, + 0, + 2.133, + 0.4, + 2.367, + 0.4, + 1, + 2.767, + 0.4, + 3.167, + 0.4, + 3.567, + 0.4, + 1, + 3.667, + 0.4, + 3.767, + -0.8, + 3.867, + -0.8, + 1, + 4.022, + -0.8, + 4.178, + -0.629, + 4.333, + -0.4, + 1, + 4.489, + -0.171, + 4.644, + -0.1, + 4.8, + -0.1, + 0, + 5.533, + -0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 0, + 0.7, + 0, + 1, + 0.889, + 0, + 1.078, + 0, + 1.267, + 0, + 1, + 1.4, + 0, + 1.533, + 0, + 1.667, + 0, + 1, + 1.9, + 0, + 2.133, + 0, + 2.367, + 0, + 1, + 2.767, + 0, + 3.167, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0.1, + 1, + 0.111, + 0.1, + 0.222, + 0.1, + 0.333, + 0.1, + 1, + 0.456, + 0.1, + 0.578, + 0, + 0.7, + 0, + 1, + 0.889, + 0, + 1.078, + 0, + 1.267, + 0, + 1, + 1.4, + 0, + 1.533, + 0, + 1.667, + 0, + 1, + 1.9, + 0, + 2.133, + -0.4, + 2.367, + -0.4, + 1, + 2.767, + -0.4, + 3.167, + -0.4, + 3.567, + -0.4, + 1, + 3.667, + -0.4, + 3.767, + 1, + 3.867, + 1, + 1, + 4.022, + 1, + 4.178, + 0.967, + 4.333, + 0.7, + 1, + 4.489, + 0.433, + 4.644, + 0.1, + 4.8, + 0.1, + 0, + 5.533, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.211, + 0, + 1.289, + 0, + 1.367, + 0, + 1, + 1.622, + 0, + 1.878, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.211, + 0, + 1.289, + 0, + 1.367, + 0, + 1, + 1.622, + 0, + 1.878, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 0, + 0.7, + 0, + 1, + 0.844, + 0, + 0.989, + 0, + 1.133, + 0, + 1, + 1.211, + 0, + 1.289, + 0, + 1.367, + 0, + 1, + 1.622, + 0, + 1.878, + 0, + 2.133, + 0, + 1, + 2.611, + 0, + 3.089, + 0, + 3.567, + 0, + 1, + 3.978, + 0, + 4.389, + 0, + 4.8, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 3.4, + 0, + 2, + 5.5, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 3.4, + 0, + 2, + 5.5, + 0, + 0, + 5.533, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 2, + 5.5, + 1, + 0, + 5.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 3.4, + 0, + 2, + 5.5, + 0, + 0, + 5.533, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m11.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 3.433, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 183, + "TotalPointCount": 444, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.133, + 1, + 2.267, + 1, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.344, + 0, + 0.689, + 0, + 1.033, + 0, + 1, + 1.089, + 0, + 1.144, + 0, + 1.2, + 0, + 1, + 1.333, + 0, + 1.467, + 2, + 1.6, + 2, + 1, + 1.744, + 2, + 1.889, + -8, + 2.033, + -8, + 1, + 2.156, + -8, + 2.278, + 8, + 2.4, + 8, + 1, + 2.533, + 8, + 2.667, + 0, + 2.8, + 0, + 1, + 2.911, + 0, + 3.022, + 0, + 3.133, + 0, + 1, + 3.222, + 0, + 3.311, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.344, + 0, + 0.689, + 0, + 1.033, + 0, + 1, + 1.089, + 0, + 1.144, + 0, + 1.2, + 0, + 1, + 1.333, + 0, + 1.467, + 16, + 1.6, + 16, + 1, + 1.722, + 16, + 1.844, + -21, + 1.967, + -21, + 1, + 2.111, + -21, + 2.256, + -19.167, + 2.4, + -18, + 1, + 2.522, + -17.012, + 2.644, + -17.151, + 2.767, + -16, + 1, + 2.878, + -14.954, + 2.989, + 0, + 3.1, + 0, + 1, + 3.2, + 0, + 3.3, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 2, + 1, + 0.533, + 2, + 1.067, + 2, + 1.6, + 2, + 1, + 1.656, + 2, + 1.711, + 0, + 1.767, + 0, + 1, + 2.067, + 0, + 2.367, + 0, + 2.667, + 0, + 1, + 2.756, + 0, + 2.844, + 2, + 2.933, + 2, + 1, + 3.089, + 2, + 3.244, + 2, + 3.4, + 2, + 0, + 3.433, + 2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 2, + 1, + 0.533, + 2, + 1.067, + 2, + 1.6, + 2, + 1, + 1.656, + 2, + 1.711, + 0, + 1.767, + 0, + 1, + 2.067, + 0, + 2.367, + 0, + 2.667, + 0, + 1, + 2.756, + 0, + 2.844, + 2, + 2.933, + 2, + 1, + 3.089, + 2, + 3.244, + 2, + 3.4, + 2, + 0, + 3.433, + 2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.389, + 0, + 0.778, + 0, + 1.167, + 0, + 1, + 1.289, + 0, + 1.411, + -0.1, + 1.533, + -0.1, + 1, + 1.9, + -0.1, + 2.267, + 0, + 2.633, + 0, + 1, + 2.889, + 0, + 3.144, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.389, + 0, + 0.778, + 0, + 1.167, + 0, + 1, + 1.289, + 0, + 1.411, + -0.38, + 1.533, + -0.38, + 1, + 1.9, + -0.38, + 2.267, + 0, + 2.633, + 0, + 1, + 2.889, + 0, + 3.144, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0.61, + 1, + 1.133, + 0.61, + 2.267, + 0.61, + 3.4, + 0.61, + 0, + 3.433, + 0.61 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0.61, + 1, + 1.133, + 0.61, + 2.267, + 0.61, + 3.4, + 0.61, + 0, + 3.433, + 0.61 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + -0.64, + 1, + 1.133, + -0.64, + 2.267, + -0.64, + 3.4, + -0.64, + 0, + 3.433, + -0.64 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.344, + 0, + 0.689, + 0, + 1.033, + 0, + 1, + 1.244, + 0, + 1.456, + 2, + 1.667, + 2, + 1, + 1.822, + 2, + 1.978, + -3, + 2.133, + -3, + 1, + 2.256, + -3, + 2.378, + 2, + 2.5, + 2, + 1, + 2.622, + 2, + 2.744, + -2, + 2.867, + -2, + 1, + 2.978, + -2, + 3.089, + 0, + 3.2, + 0, + 1, + 3.267, + 0, + 3.333, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.211, + 0, + 1.422, + 3, + 1.633, + 3, + 1, + 1.778, + 3, + 1.922, + 0, + 2.067, + 0, + 1, + 2.511, + 0, + 2.956, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + -5, + 0.9, + -5, + 1, + 1.167, + -5, + 1.433, + 0, + 1.7, + 0, + 1, + 2.267, + 0, + 2.833, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0.5, + 1, + 0.156, + 0.5, + 0.311, + 0.743, + 0.467, + 0.76, + 1, + 1.378, + 0.86, + 2.289, + 0.89, + 3.2, + 0.89, + 1, + 3.267, + 0.89, + 3.333, + 0.5, + 3.4, + 0.5, + 0, + 3.433, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0.5, + 1, + 0.156, + 0.5, + 0.311, + 0.735, + 0.467, + 0.76, + 1, + 1.378, + 0.905, + 2.289, + 0.95, + 3.2, + 0.95, + 1, + 3.267, + 0.95, + 3.333, + 0.5, + 3.4, + 0.5, + 0, + 3.433, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0.3, + 1, + 0.311, + 0.3, + 0.622, + 0.3, + 0.933, + 0.3, + 1, + 1.156, + 0.3, + 1.378, + 0.5, + 1.6, + 0.5, + 1, + 1.756, + 0.5, + 1.911, + 0.3, + 2.067, + 0.3, + 1, + 2.511, + 0.3, + 2.956, + 0.3, + 3.4, + 0.3, + 0, + 3.433, + 0.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0.3, + 1, + 0.289, + 0.3, + 0.578, + 0.3, + 0.867, + 0.3, + 1, + 1.089, + 0.3, + 1.311, + 0.5, + 1.533, + 0.5, + 1, + 1.689, + 0.5, + 1.844, + 0.3, + 2, + 0.3, + 1, + 2.467, + 0.3, + 2.933, + 0.3, + 3.4, + 0.3, + 0, + 3.433, + 0.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0.66, + 1, + 0.344, + 0.66, + 0.689, + 0.662, + 1.033, + 0.64, + 1, + 1.267, + 0.625, + 1.5, + 0.12, + 1.733, + 0.12, + 1, + 1.867, + 0.12, + 2, + 0.66, + 2.133, + 0.66, + 1, + 2.556, + 0.66, + 2.978, + 0.66, + 3.4, + 0.66, + 0, + 3.433, + 0.66 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + -1, + 1, + 0.322, + -1, + 0.644, + -1, + 0.967, + -1, + 1, + 1.2, + -1, + 1.433, + -0.68, + 1.667, + -0.68, + 1, + 1.822, + -0.68, + 1.978, + -1, + 2.133, + -1, + 1, + 2.556, + -1, + 2.978, + -1, + 3.4, + -1, + 0, + 3.433, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 1.133, + 0, + 2.267, + 0, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 0, + 2, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 0, + 2, + 3.4, + 0, + 0, + 3.433, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 3.4, + 1, + 0, + 3.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 3.4, + 0, + 0, + 3.433, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m12.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4.933, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 330, + "TotalPointCount": 819, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.633, + 1, + 3.267, + 1, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + -18, + 0.633, + -18, + 1, + 0.744, + -18, + 0.856, + 19, + 0.967, + 19, + 1, + 1.078, + 19, + 1.189, + -11, + 1.3, + -11, + 1, + 1.411, + -11, + 1.522, + 5, + 1.633, + 5, + 1, + 1.722, + 5, + 1.811, + 0, + 1.9, + 0, + 1, + 2.089, + 0, + 2.278, + 0, + 2.467, + 0, + 1, + 2.956, + 0, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0, + 0.633, + 0, + 1, + 1.244, + 0, + 1.856, + 0, + 2.467, + 0, + 1, + 2.567, + 0, + 2.667, + 4, + 2.767, + 4, + 1, + 2.9, + 4, + 3.033, + -13, + 3.167, + -13, + 1, + 3.422, + -13, + 3.678, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + -11, + 1, + 0.1, + -11, + 0.2, + -11, + 0.3, + -11, + 1, + 0.411, + -11, + 0.522, + -11, + 0.633, + -11, + 1, + 1.244, + -11, + 1.856, + -11, + 2.467, + -11, + 1, + 2.956, + -11, + 3.444, + -6.965, + 3.933, + -2, + 1, + 4.122, + -0.082, + 4.311, + 0, + 4.5, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0, + 0.633, + 0, + 1, + 1.244, + 0, + 1.856, + 0, + 2.467, + 0, + 1, + 2.956, + 0, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 2, + 1, + 0.1, + 2, + 0.2, + 2, + 0.3, + 2, + 1, + 0.411, + 2, + 0.522, + 1, + 0.633, + 1, + 1, + 1.233, + 1, + 1.833, + 1, + 2.433, + 1, + 1, + 2.678, + 1, + 2.922, + 0, + 3.167, + 0, + 1, + 3.422, + 0, + 3.678, + 1, + 3.933, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0, + 0.633, + 0, + 1, + 1.244, + 0, + 1.856, + 0, + 2.467, + 0, + 1, + 2.956, + 0, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 2, + 1, + 0.1, + 2, + 0.2, + 2, + 0.3, + 2, + 1, + 0.411, + 2, + 0.522, + 1, + 0.633, + 1, + 1, + 1.244, + 1, + 1.856, + 1, + 2.467, + 1, + 1, + 2.7, + 1, + 2.933, + 0, + 3.167, + 0, + 1, + 3.422, + 0, + 3.678, + 1, + 3.933, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0, + 0.633, + 0, + 1, + 1.244, + 0, + 1.856, + 0, + 2.467, + 0, + 1, + 2.956, + 0, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0, + 0.633, + 0, + 1, + 1.244, + 0, + 1.856, + 0, + 2.467, + 0, + 1, + 2.956, + 0, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0, + 0.633, + 0, + 1, + 1.244, + 0, + 1.856, + 0, + 2.467, + 0, + 1, + 2.956, + 0, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0, + 0.633, + 0, + 1, + 1.244, + 0, + 1.856, + 0, + 2.467, + 0, + 1, + 2.956, + 0, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0.6, + 0.633, + 0.6, + 1, + 0.744, + 0.6, + 0.856, + -0.5, + 0.967, + -0.5, + 1, + 1.078, + -0.5, + 1.189, + 0.3, + 1.3, + 0.3, + 1, + 1.411, + 0.3, + 1.522, + -0.2, + 1.633, + -0.2, + 1, + 1.722, + -0.2, + 1.811, + 0.1, + 1.9, + 0.1, + 1, + 2.089, + 0.1, + 2.278, + 0.1, + 2.467, + 0.1, + 1, + 2.956, + 0.1, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0.293, + 0.633, + 0.4, + 1, + 0.744, + 0.507, + 0.856, + 0.5, + 0.967, + 0.5, + 1, + 1.078, + 0.5, + 1.189, + 0.5, + 1.3, + 0.5, + 1, + 1.411, + 0.5, + 1.522, + 0.473, + 1.633, + 0.4, + 1, + 1.722, + 0.341, + 1.811, + 0.3, + 1.9, + 0.3, + 1, + 2.089, + 0.3, + 2.278, + 0.3, + 2.467, + 0.3, + 1, + 2.956, + 0.3, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0.9, + 1, + 0.1, + 0.9, + 0.2, + 0.9, + 0.3, + 0.9, + 1, + 0.411, + 0.9, + 0.522, + -0.3, + 0.633, + -0.3, + 1, + 1.244, + -0.3, + 1.856, + -0.3, + 2.467, + -0.3, + 1, + 2.7, + -0.3, + 2.933, + 0.3, + 3.167, + 0.3, + 1, + 3.422, + 0.3, + 3.678, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0.9, + 1, + 0.1, + 0.9, + 0.2, + 0.9, + 0.3, + 0.9, + 1, + 0.411, + 0.9, + 0.522, + -0.3, + 0.633, + -0.3, + 1, + 1.244, + -0.3, + 1.856, + -0.3, + 2.467, + -0.3, + 1, + 2.7, + -0.3, + 2.933, + 0.3, + 3.167, + 0.3, + 1, + 3.422, + 0.3, + 3.678, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0.8, + 1, + 0.1, + 0.8, + 0.2, + 0.8, + 0.3, + 0.8, + 1, + 0.411, + 0.8, + 0.522, + -0.3, + 0.633, + -0.3, + 1, + 1.244, + -0.3, + 1.856, + -0.3, + 2.467, + -0.3, + 1, + 2.7, + -0.3, + 2.933, + 0.3, + 3.167, + 0.3, + 1, + 3.422, + 0.3, + 3.678, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0.8, + 1, + 0.1, + 0.8, + 0.2, + 0.8, + 0.3, + 0.8, + 1, + 0.411, + 0.8, + 0.522, + -0.3, + 0.633, + -0.3, + 1, + 1.244, + -0.3, + 1.856, + -0.3, + 2.467, + -0.3, + 1, + 2.7, + -0.3, + 2.933, + 0.3, + 3.167, + 0.3, + 1, + 3.422, + 0.3, + 3.678, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0.7, + 1, + 0.1, + 0.7, + 0.2, + 0.7, + 0.3, + 0.7, + 1, + 0.411, + 0.7, + 0.522, + 0.4, + 0.633, + 0.4, + 1, + 1.244, + 0.4, + 1.856, + 0.4, + 2.467, + 0.4, + 1, + 2.956, + 0.4, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0.7, + 1, + 0.1, + 0.7, + 0.2, + 0.7, + 0.3, + 0.7, + 1, + 0.411, + 0.7, + 0.522, + 0.4, + 0.633, + 0.4, + 1, + 1.244, + 0.4, + 1.856, + 0.4, + 2.467, + 0.4, + 1, + 2.956, + 0.4, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0.6, + 1, + 0.1, + 0.6, + 0.2, + 0.6, + 0.3, + 0.6, + 1, + 0.411, + 0.6, + 0.522, + -0.5, + 0.633, + -0.5, + 1, + 1.244, + -0.5, + 1.856, + -0.5, + 2.467, + -0.5, + 1, + 2.7, + -0.5, + 2.933, + 0.2, + 3.167, + 0.2, + 1, + 3.422, + 0.2, + 3.678, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0.6, + 1, + 0.1, + 0.6, + 0.2, + 0.6, + 0.3, + 0.6, + 1, + 0.411, + 0.6, + 0.522, + -0.5, + 0.633, + -0.5, + 1, + 1.244, + -0.5, + 1.856, + -0.5, + 2.467, + -0.5, + 1, + 2.7, + -0.5, + 2.933, + 0.2, + 3.167, + 0.2, + 1, + 3.422, + 0.2, + 3.678, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + -0.5, + 1, + 0.1, + -0.5, + 0.2, + -0.5, + 0.3, + -0.5, + 1, + 0.411, + -0.5, + 0.522, + -0.6, + 0.633, + -0.6, + 1, + 1.244, + -0.6, + 1.856, + -0.6, + 2.467, + -0.6, + 1, + 2.7, + -0.6, + 2.933, + 1, + 3.167, + 1, + 1, + 3.422, + 1, + 3.678, + 1, + 3.933, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0.5, + 0.633, + 0.5, + 1, + 1.244, + 0.5, + 1.856, + 0.5, + 2.467, + 0.5, + 1, + 2.7, + 0.5, + 2.933, + 0, + 3.167, + 0, + 1, + 3.422, + 0, + 3.678, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0, + 0.633, + 0, + 1, + 1.244, + 0, + 1.856, + 0, + 2.467, + 0, + 1, + 2.956, + 0, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + -1, + 1, + 0.1, + -1, + 0.2, + -1, + 0.3, + -1, + 1, + 0.411, + -1, + 0.522, + -1, + 0.633, + -1, + 1, + 1.244, + -1, + 1.856, + -1, + 2.467, + -1, + 1, + 2.956, + -1, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 5, + 1, + 0.1, + 5, + 0.2, + 5.234, + 0.3, + 3.279, + 1, + 0.411, + 1.107, + 0.522, + -10, + 0.633, + -10, + 1, + 1.244, + -10, + 1.856, + -10, + 2.467, + -10, + 1, + 2.7, + -10, + 2.933, + 8, + 3.167, + 8, + 1, + 3.422, + 8, + 3.678, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 3, + 1, + 0.1, + 3, + 0.2, + 3, + 0.3, + 3, + 1, + 0.411, + 3, + 0.522, + 3, + 0.633, + 3, + 1, + 1.244, + 3, + 1.856, + 3, + 2.467, + 3, + 1, + 2.956, + 3, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + -7, + 1, + 0.1, + -7, + 0.2, + -7.215, + 0.3, + -5.509, + 1, + 0.411, + -3.613, + 0.522, + 6, + 0.633, + 6, + 1, + 1.244, + 6, + 1.856, + 6, + 2.467, + 6, + 1, + 2.956, + 6, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0, + 0.633, + 0, + 1, + 1.244, + 0, + 1.856, + 0, + 2.467, + 0, + 1, + 2.956, + 0, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0, + 0.633, + 0, + 1, + 1.244, + 0, + 1.856, + 0, + 2.467, + 0, + 1, + 2.956, + 0, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0.7, + 1, + 0.1, + 0.7, + 0.2, + 1, + 0.3, + 1, + 1, + 0.411, + 1, + 0.522, + 1, + 0.633, + 1, + 1, + 1.244, + 1, + 1.856, + 0, + 2.467, + 0, + 1, + 2.611, + 0, + 2.756, + 0.331, + 2.9, + 0.5, + 1, + 3.244, + 0.902, + 3.589, + 1, + 3.933, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0.7, + 1, + 0.1, + 0.7, + 0.2, + 1, + 0.3, + 1, + 1, + 0.411, + 1, + 0.522, + 1, + 0.633, + 1, + 1, + 1.244, + 1, + 1.856, + 0, + 2.467, + 0, + 1, + 2.611, + 0, + 2.756, + 0.331, + 2.9, + 0.5, + 1, + 3.244, + 0.902, + 3.589, + 1, + 3.933, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 4.7, + 1, + 0.1, + 4.7, + 0.2, + 4.7, + 0.3, + 4.7, + 1, + 0.411, + 4.7, + 0.522, + 4.7, + 0.633, + 4.7, + 1, + 0.767, + 4.7, + 0.9, + 4.2, + 1.033, + 4.2, + 1, + 1.178, + 4.2, + 1.322, + 4.656, + 1.467, + 4.656, + 1, + 1.611, + 4.656, + 1.756, + 4.555, + 1.9, + 4.555, + 1, + 2.089, + 4.555, + 2.278, + 4.55, + 2.467, + 4.6, + 1, + 2.611, + 4.638, + 2.756, + 5, + 2.9, + 5, + 1, + 3.244, + 5, + 3.589, + 5, + 3.933, + 5, + 0, + 4.933, + 5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 4.7, + 1, + 0.1, + 4.7, + 0.2, + 4.7, + 0.3, + 4.7, + 1, + 0.411, + 4.7, + 0.522, + 4.7, + 0.633, + 4.7, + 1, + 0.767, + 4.7, + 0.9, + 4.2, + 1.033, + 4.2, + 1, + 1.178, + 4.2, + 1.322, + 4.65, + 1.467, + 4.65, + 1, + 1.611, + 4.65, + 1.756, + 4.529, + 1.9, + 4.529, + 1, + 2.089, + 4.529, + 2.278, + 4.525, + 2.467, + 4.6, + 1, + 2.611, + 4.658, + 2.756, + 5, + 2.9, + 5, + 1, + 3.244, + 5, + 3.589, + 5, + 3.933, + 5, + 0, + 4.933, + 5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 1, + 1, + 0.1, + 1, + 0.2, + 1, + 0.3, + 1, + 1, + 0.411, + 1, + 0.522, + 1, + 0.633, + 1, + 1, + 1.244, + 1, + 1.856, + 1, + 2.467, + 1, + 1, + 2.956, + 1, + 3.444, + 1, + 3.933, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0.1, + 1, + 0.1, + 0.1, + 0.2, + 0.1, + 0.3, + 0.1, + 1, + 0.411, + 0.1, + 0.522, + -0.9, + 0.633, + -0.9, + 1, + 0.844, + -0.9, + 1.056, + 0.2, + 1.267, + 0.2, + 1, + 1.422, + 0.2, + 1.578, + -0.6, + 1.733, + -0.6, + 1, + 1.978, + -0.6, + 2.222, + -0.6, + 2.467, + -0.6, + 1, + 2.956, + -0.6, + 3.444, + -0.1, + 3.933, + -0.1, + 0, + 4.933, + -0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 1, + 1, + 0.1, + 1, + 0.2, + 1, + 0.3, + 1, + 1, + 0.411, + 1, + 0.522, + 1, + 0.633, + 1, + 1, + 1.244, + 1, + 1.856, + 1, + 2.467, + 1, + 1, + 2.956, + 1, + 3.444, + 1, + 3.933, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + -0.1, + 1, + 0.1, + -0.1, + 0.2, + -0.1, + 0.3, + -0.1, + 1, + 0.411, + -0.1, + 0.522, + 1, + 0.633, + 1, + 1, + 0.844, + 1, + 1.056, + -0.2, + 1.267, + -0.2, + 1, + 1.422, + -0.2, + 1.578, + 0.6, + 1.733, + 0.6, + 1, + 1.978, + 0.6, + 2.222, + 0.6, + 2.467, + 0.6, + 1, + 2.956, + 0.6, + 3.444, + 0.1, + 3.933, + 0.1, + 0, + 4.933, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0, + 0.633, + 0, + 1, + 1.244, + 0, + 1.856, + 0, + 2.467, + 0, + 1, + 2.956, + 0, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0, + 0.633, + 0, + 1, + 1.244, + 0, + 1.856, + 0, + 2.467, + 0, + 1, + 2.956, + 0, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.411, + 0, + 0.522, + 0, + 0.633, + 0, + 1, + 1.244, + 0, + 1.856, + 0, + 2.467, + 0, + 1, + 2.956, + 0, + 3.444, + 0, + 3.933, + 0, + 0, + 4.933, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 1, + 2.722, + 1, + 2.811, + 0.002, + 2.9, + 0.002, + 2, + 4.9, + 0.002, + 0, + 4.933, + 0.002 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 1, + 1, + 0.878, + 1, + 1.756, + 1, + 2.633, + 0.999, + 1, + 2.722, + 0.999, + 2.811, + 0.001, + 2.9, + 0.001, + 2, + 4.9, + 0.001, + 0, + 4.933, + 0.001 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 0, + 1, + 0.878, + 0, + 1.756, + 0, + 2.633, + 0, + 1, + 2.722, + 0, + 2.811, + 1, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 0, + 1, + 0.878, + 0, + 1.756, + 0, + 2.633, + 0, + 1, + 2.722, + 0, + 2.811, + 1, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 2.633, + 1, + 2, + 2.9, + 1, + 2, + 4.9, + 1, + 0, + 4.933, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 2.633, + 0, + 2, + 2.9, + 0, + 2, + 4.9, + 0, + 0, + 4.933, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m13.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 2.533, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 137, + "TotalPointCount": 306, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 0.833, + 1, + 1.667, + 1, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.156, + 0, + 0.311, + 0, + 0.467, + 0, + 1, + 0.533, + 0, + 0.6, + 0, + 0.667, + 0, + 1, + 0.878, + 0, + 1.089, + 0, + 1.3, + 0, + 1, + 1.378, + 0, + 1.456, + 0, + 1.533, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.156, + 0, + 0.311, + 0, + 0.467, + 0, + 1, + 0.5, + 0, + 0.533, + -1, + 0.567, + -1, + 1, + 0.689, + -1, + 0.811, + 11, + 0.933, + 11, + 1, + 1.056, + 11, + 1.178, + -11, + 1.3, + -11, + 1, + 1.378, + -11, + 1.456, + 1, + 1.533, + 1, + 1, + 1.656, + 1, + 1.778, + 0, + 1.9, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 0, + 0.567, + 0, + 1, + 0.678, + 0, + 0.789, + -6, + 0.9, + -6, + 1, + 1.011, + -6, + 1.122, + 19, + 1.233, + 19, + 1, + 1.4, + 19, + 1.567, + 17, + 1.733, + 17, + 0, + 2.533, + 17 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 2, + 1, + 0.544, + 2, + 1.089, + 2, + 1.633, + 2, + 1, + 1.667, + 2, + 1.7, + 0, + 1.733, + 0, + 1, + 1.756, + 0, + 1.778, + 0, + 1.8, + 0, + 1, + 1.822, + 0, + 1.844, + 2, + 1.867, + 2, + 1, + 1.9, + 2, + 1.933, + 0, + 1.967, + 0, + 1, + 2.011, + 0, + 2.056, + 0, + 2.1, + 0, + 1, + 2.144, + 0, + 2.189, + 2, + 2.233, + 2, + 0, + 2.533, + 2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 2, + 1, + 0.544, + 2, + 1.089, + 2, + 1.633, + 2, + 1, + 1.667, + 2, + 1.7, + 0, + 1.733, + 0, + 1, + 1.756, + 0, + 1.778, + 0, + 1.8, + 0, + 1, + 1.822, + 0, + 1.844, + 2, + 1.867, + 2, + 1, + 1.9, + 2, + 1.933, + 0, + 1.967, + 0, + 1, + 2.011, + 0, + 2.056, + 0, + 2.1, + 0, + 1, + 2.144, + 0, + 2.189, + 2, + 2.233, + 2, + 0, + 2.533, + 2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.411, + 0, + 0.822, + 0.1, + 1.233, + 0.1, + 0, + 2.533, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.411, + 0, + 0.822, + 0.4, + 1.233, + 0.4, + 0, + 2.533, + 0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + -0.84, + 0, + 2.533, + -0.84 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 0, + 0.567, + 0, + 1, + 0.678, + 0, + 0.789, + 5, + 0.9, + 5, + 1, + 1.011, + 5, + 1.122, + 0, + 1.233, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 0, + 0.567, + 0, + 1, + 0.678, + 0, + 0.789, + -1, + 0.9, + -1, + 1, + 1.011, + -1, + 1.122, + 6, + 1.233, + 6, + 0, + 2.533, + 6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 0, + 0.567, + 0, + 1, + 0.678, + 0, + 0.789, + -2, + 0.9, + -2, + 1, + 1.011, + -2, + 1.122, + 9, + 1.233, + 9, + 0, + 2.533, + 9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + -0.16, + 1, + 0.211, + -0.16, + 0.422, + -0.16, + 0.633, + -0.16, + 1, + 0.789, + -0.16, + 0.944, + 0, + 1.1, + 0, + 1, + 1.189, + 0, + 1.278, + -0.16, + 1.367, + -0.16, + 0, + 2.533, + -0.16 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + -0.15, + 1, + 0.211, + -0.15, + 0.422, + -0.15, + 0.633, + -0.15, + 1, + 0.789, + -0.15, + 0.944, + 0, + 1.1, + 0, + 1, + 1.189, + 0, + 1.278, + -0.15, + 1.367, + -0.15, + 0, + 2.533, + -0.15 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0, + 1, + 0.267, + 0, + 0.533, + 0, + 0.8, + 0, + 1, + 0.911, + 0, + 1.022, + -0.53, + 1.133, + -0.53, + 1, + 1.233, + -0.53, + 1.333, + 0, + 1.433, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0, + 1, + 0.267, + 0, + 0.533, + 0, + 0.8, + 0, + 1, + 0.911, + 0, + 1.022, + 0.54, + 1.133, + 0.54, + 1, + 1.233, + 0.54, + 1.333, + 0, + 1.433, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 2.5, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 2.5, + 0, + 0, + 2.533, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 2.5, + 1, + 0, + 2.533, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 2.5, + 0, + 0, + 2.533, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m14.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 3.033, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 286, + "TotalPointCount": 753, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1, + 1, + 2, + 1, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.589, + 0, + 0.678, + 1, + 0.767, + 1, + 1, + 0.889, + 1, + 1.011, + 1.06, + 1.133, + 0.632, + 1, + 1.256, + 0.204, + 1.378, + -2, + 1.5, + -2, + 1, + 2, + -2, + 2.5, + -2, + 3, + -2, + 0, + 3.033, + -2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + 0, + 0.367, + 0, + 1, + 0.433, + 0, + 0.5, + -1, + 0.567, + -1, + 1, + 0.689, + -1, + 0.811, + 16, + 0.933, + 16, + 1, + 1.067, + 16, + 1.2, + 16.106, + 1.333, + 13.915, + 1, + 1.444, + 12.09, + 1.556, + -1, + 1.667, + -1, + 1, + 2.111, + -1, + 2.556, + -1, + 3, + -1, + 0, + 3.033, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + 0, + 0.633, + 0, + 1, + 0.767, + 0, + 0.9, + -9, + 1.033, + -9, + 1, + 1.1, + -9, + 1.167, + -9.696, + 1.233, + -5.567, + 1, + 1.356, + 2.004, + 1.478, + 15.49, + 1.6, + 19, + 1, + 1.778, + 19.291, + 1.978, + 18, + 2.133, + 18, + 1, + 2.422, + 18, + 2.711, + 18, + 3, + 18, + 0, + 3.033, + 18 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 1, + 0, + 2, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.167, + 1, + 0.333, + 1, + 0.5, + 1, + 1, + 0.589, + 1, + 0.678, + 0, + 0.767, + 0, + 1, + 0.8, + 0, + 0.833, + 0, + 0.867, + 0, + 1, + 0.956, + 0, + 1.044, + 1.332, + 1.133, + 1.616, + 1, + 1.256, + 2.007, + 1.378, + 2, + 1.5, + 2, + 1, + 1.667, + 2, + 1.833, + 2, + 2, + 2, + 1, + 2.044, + 2, + 2.089, + 0, + 2.133, + 0, + 1, + 2.167, + 0, + 2.2, + 0, + 2.233, + 0, + 1, + 2.311, + 0, + 2.389, + 2, + 2.467, + 2, + 1, + 2.644, + 2, + 2.822, + 2, + 3, + 2, + 0, + 3.033, + 2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.167, + 1, + 0.333, + 1, + 0.5, + 1, + 1, + 0.589, + 1, + 0.678, + 0, + 0.767, + 0, + 1, + 0.8, + 0, + 0.833, + 0, + 0.867, + 0, + 1, + 0.956, + 0, + 1.044, + 1.332, + 1.133, + 1.616, + 1, + 1.256, + 2.007, + 1.378, + 2, + 1.5, + 2, + 1, + 1.667, + 2, + 1.833, + 2, + 2, + 2, + 1, + 2.044, + 2, + 2.089, + 0, + 2.133, + 0, + 1, + 2.167, + 0, + 2.2, + 0, + 2.233, + 0, + 1, + 2.311, + 0, + 2.389, + 2, + 2.467, + 2, + 1, + 2.644, + 2, + 2.822, + 2, + 3, + 2, + 0, + 3.033, + 2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0.3, + 1.5, + 0.3, + 1, + 2, + 0.3, + 2.5, + 0.3, + 3, + 0.3, + 0, + 3.033, + 0.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0.039, + 1.133, + 0.123, + 1, + 1.256, + 0.172, + 1.378, + 0.2, + 1.5, + 0.2, + 1, + 1.667, + 0.2, + 1.833, + 0.2, + 2, + 0.2, + 1, + 2.044, + 0.2, + 2.089, + -0.2, + 2.133, + -0.2, + 1, + 2.167, + -0.2, + 2.2, + -0.2, + 2.233, + -0.2, + 1, + 2.311, + -0.2, + 2.389, + 0.2, + 2.467, + 0.2, + 1, + 2.644, + 0.2, + 2.822, + 0.2, + 3, + 0.2, + 0, + 3.033, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.589, + 0, + 0.678, + -0.1, + 0.767, + -0.1, + 1, + 0.8, + -0.1, + 0.833, + -0.1, + 0.867, + -0.1, + 1, + 0.956, + -0.1, + 1.044, + 0.291, + 1.133, + 0.493, + 1, + 1.256, + 0.77, + 1.378, + 0.8, + 1.5, + 0.8, + 1, + 2, + 0.8, + 2.5, + 0.8, + 3, + 0.8, + 0, + 3.033, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.589, + 0, + 0.678, + -0.1, + 0.767, + -0.1, + 1, + 0.8, + -0.1, + 0.833, + -0.1, + 0.867, + -0.1, + 1, + 0.956, + -0.1, + 1.044, + 0.291, + 1.133, + 0.493, + 1, + 1.256, + 0.77, + 1.378, + 0.8, + 1.5, + 0.8, + 1, + 2, + 0.8, + 2.5, + 0.8, + 3, + 0.8, + 0, + 3.033, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0.039, + 1.133, + 0.123, + 1, + 1.256, + 0.172, + 1.378, + 0.2, + 1.5, + 0.2, + 1, + 2, + 0.2, + 2.5, + 0.2, + 3, + 0.2, + 0, + 3.033, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0.039, + 1.133, + 0.123, + 1, + 1.256, + 0.172, + 1.378, + 0.2, + 1.5, + 0.2, + 1, + 2, + 0.2, + 2.5, + 0.2, + 3, + 0.2, + 0, + 3.033, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0.039, + 1.133, + 0.123, + 1, + 1.256, + 0.172, + 1.378, + 0.2, + 1.5, + 0.2, + 1, + 2, + 0.2, + 2.5, + 0.2, + 3, + 0.2, + 0, + 3.033, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0.039, + 1.133, + 0.123, + 1, + 1.256, + 0.172, + 1.378, + 0.2, + 1.5, + 0.2, + 1, + 2, + 0.2, + 2.5, + 0.2, + 3, + 0.2, + 0, + 3.033, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0.078, + 1.133, + 0.246, + 1, + 1.256, + 0.344, + 1.378, + 0.4, + 1.5, + 0.4, + 1, + 2, + 0.4, + 2.5, + 0.4, + 3, + 0.4, + 0, + 3.033, + 0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0.078, + 1.133, + 0.246, + 1, + 1.256, + 0.344, + 1.378, + 0.4, + 1.5, + 0.4, + 1, + 2, + 0.4, + 2.5, + 0.4, + 3, + 0.4, + 0, + 3.033, + 0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.167, + 1, + 0.333, + 1, + 0.5, + 1, + 1, + 0.589, + 1, + 0.678, + -0.5, + 0.767, + -0.5, + 1, + 0.889, + -0.5, + 1.011, + -0.232, + 1.133, + -0.232, + 1, + 1.256, + -0.232, + 1.378, + -1, + 1.5, + -1, + 1, + 2, + -1, + 2.5, + -1, + 3, + -1, + 0, + 3.033, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.589, + 0, + 0.678, + 3, + 0.767, + 3, + 1, + 0.889, + 3, + 1.011, + 1.294, + 1.133, + -0.616, + 1, + 1.256, + -2.526, + 1.378, + -3, + 1.5, + -3, + 1, + 2, + -3, + 2.5, + -3, + 3, + -3, + 0, + 3.033, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 7, + 1.133, + 7, + 1, + 1.256, + 7, + 1.378, + -9, + 1.5, + -9, + 1, + 2, + -9, + 2.5, + -9, + 3, + -9, + 0, + 3.033, + -9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.589, + 0, + 0.678, + -4.509, + 0.767, + -4.728, + 1, + 0.889, + -5.029, + 1.011, + -5, + 1.133, + -5, + 1, + 1.256, + -5, + 1.378, + 7, + 1.5, + 7, + 1, + 2, + 7, + 2.5, + 7, + 3, + 7, + 0, + 3.033, + 7 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.589, + 0, + 0.678, + -1.507, + 0.767, + -1.507, + 1, + 0.889, + -1.507, + 1.011, + -1.675, + 1.133, + -0.529, + 1, + 1.256, + 0.618, + 1.378, + 10, + 1.5, + 10, + 1, + 2, + 10, + 2.5, + 10, + 3, + 10, + 0, + 3.033, + 10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 1, + 1, + 0.167, + 1, + 0.333, + 1, + 0.5, + 1, + 1, + 0.711, + 1, + 0.922, + 0.3, + 1.133, + 0.3, + 1, + 1.256, + 0.3, + 1.378, + 0.327, + 1.5, + 0.4, + 1, + 1.6, + 0.459, + 1.7, + 0.5, + 1.8, + 0.5, + 1, + 2.2, + 0.5, + 2.6, + 0.5, + 3, + 0.5, + 0, + 3.033, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 1, + 1, + 0.167, + 1, + 0.333, + 1, + 0.5, + 1, + 1, + 0.711, + 1, + 0.922, + 0.3, + 1.133, + 0.3, + 1, + 1.256, + 0.3, + 1.378, + 0.327, + 1.5, + 0.4, + 1, + 1.6, + 0.459, + 1.7, + 0.5, + 1.8, + 0.5, + 1, + 2.2, + 0.5, + 2.6, + 0.5, + 3, + 0.5, + 0, + 3.033, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.744, + 0, + 0.989, + 0, + 1.233, + 0, + 1, + 1.467, + 0, + 1.7, + 0, + 1.933, + 0, + 1, + 2.289, + 0, + 2.644, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.589, + 0, + 0.678, + 0.9, + 0.767, + 0.9, + 1, + 0.922, + 0.9, + 1.078, + 0.681, + 1.233, + 0, + 1, + 1.344, + -0.486, + 1.456, + -0.9, + 1.567, + -0.9, + 1, + 1.689, + -0.9, + 1.811, + -0.6, + 1.933, + -0.6, + 1, + 2.289, + -0.6, + 2.644, + -0.6, + 3, + -0.6, + 0, + 3.033, + -0.6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.744, + 0, + 0.989, + 0, + 1.233, + 0, + 1, + 1.467, + 0, + 1.7, + 0, + 1.933, + 0, + 1, + 2.289, + 0, + 2.644, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.589, + 0, + 0.678, + -0.9, + 0.767, + -0.9, + 1, + 0.922, + -0.9, + 1.078, + -0.681, + 1.233, + 0, + 1, + 1.344, + 0.486, + 1.456, + 0.9, + 1.567, + 0.9, + 1, + 1.689, + 0.9, + 1.811, + 0.6, + 1.933, + 0.6, + 1, + 2.289, + 0.6, + 2.644, + 0.6, + 3, + 0.6, + 0, + 3.033, + 0.6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.711, + 0, + 0.922, + 0, + 1.133, + 0, + 1, + 1.256, + 0, + 1.378, + 0, + 1.5, + 0, + 1, + 2, + 0, + 2.5, + 0, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 3, + 0, + 0, + 3.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 3, + 1, + 0, + 3.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 3, + 0, + 0, + 3.033, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m15.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 5.333, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 169, + "TotalPointCount": 402, + "UserDataCount": 0, + "TotalUserDataSize": 0, + "FadeInTime": 0, + "FadeOutTime": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.767, + 1, + 3.533, + 1, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + -3, + 0.6, + -3, + 1, + 1.333, + -3, + 2.067, + -3, + 2.8, + -3, + 1, + 3.056, + -3, + 3.311, + 1, + 3.567, + 1, + 1, + 3.833, + 1, + 4.1, + 0, + 4.367, + 0, + 1, + 4.678, + 0, + 4.989, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + -10, + 0.6, + -10, + 1, + 1.333, + -10, + 2.067, + -10, + 2.8, + -10, + 1, + 3.056, + -10, + 3.311, + 11, + 3.567, + 11, + 1, + 3.833, + 11, + 4.1, + 0, + 4.367, + 0, + 1, + 4.678, + 0, + 4.989, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + -30, + 0.633, + -30, + 1, + 0.689, + -30, + 0.744, + -30, + 0.8, + -30, + 1, + 1.1, + -30, + 1.4, + 30, + 1.7, + 30, + 1, + 1.744, + 30, + 1.789, + 30, + 1.833, + 30, + 1, + 2.167, + 30, + 2.5, + -30, + 2.833, + -30, + 1, + 2.878, + -30, + 2.922, + -30, + 2.967, + -30, + 1, + 3.222, + -30, + 3.478, + 0, + 3.733, + 0, + 1, + 4.256, + 0, + 4.778, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 1.767, + 1, + 3.533, + 1, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 1.767, + 1, + 3.533, + 1, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0.03, + 0.6, + 0.03, + 1, + 0.989, + 0.03, + 1.378, + 0.03, + 1.767, + 0.03, + 1, + 2.144, + 0.03, + 2.522, + 0.03, + 2.9, + 0.03, + 1, + 3.1, + 0.03, + 3.3, + -0.1, + 3.5, + -0.1, + 1, + 3.767, + -0.1, + 4.033, + 0, + 4.3, + 0, + 1, + 4.633, + 0, + 4.967, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0.67, + 0.6, + 0.67, + 1, + 0.989, + 0.67, + 1.378, + 0.67, + 1.767, + 0.67, + 1, + 2.144, + 0.67, + 2.522, + 0.67, + 2.9, + 0.67, + 1, + 3.1, + 0.67, + 3.3, + -0.21, + 3.5, + -0.21, + 1, + 3.767, + -0.21, + 4.033, + 0, + 4.3, + 0, + 1, + 4.633, + 0, + 4.967, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 1.767, + 1, + 3.533, + 1, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.322, + 0, + 0.644, + 6, + 0.967, + 6, + 1, + 1.322, + 6, + 1.678, + -2, + 2.033, + -2, + 1, + 2.344, + -2, + 2.656, + 0, + 2.967, + 0, + 1, + 3.178, + 0, + 3.389, + -3, + 3.6, + -3, + 1, + 3.833, + -3, + 4.067, + 0, + 4.3, + 0, + 1, + 4.633, + 0, + 4.967, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.967, + 0, + 1.933, + 0, + 2.9, + 0, + 1, + 3.111, + 0, + 3.322, + 2, + 3.533, + 2, + 1, + 3.767, + 2, + 4, + 0, + 4.233, + 0, + 1, + 4.589, + 0, + 4.944, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0.5, + 1, + 0.156, + 0.5, + 0.311, + 0.75, + 0.467, + 0.76, + 1, + 1.978, + 0.855, + 3.489, + 0.89, + 5, + 0.89, + 1, + 5.1, + 0.89, + 5.2, + 0.5, + 5.3, + 0.5, + 0, + 5.333, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0.5, + 1, + 0.156, + 0.5, + 0.311, + 0.746, + 0.467, + 0.76, + 1, + 1.978, + 0.899, + 3.489, + 0.95, + 5, + 0.95, + 1, + 5.1, + 0.95, + 5.2, + 0.5, + 5.3, + 0.5, + 0, + 5.333, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0.24, + 1, + 0.356, + 0.24, + 0.711, + 0, + 1.067, + 0, + 1, + 1.389, + 0, + 1.711, + 0.24, + 2.033, + 0.24, + 1, + 3.122, + 0.24, + 4.211, + 0.24, + 5.3, + 0.24, + 0, + 5.333, + 0.24 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + -0.24, + 1, + 0.356, + -0.24, + 0.711, + 0, + 1.067, + 0, + 1, + 1.389, + 0, + 1.711, + -0.24, + 2.033, + -0.24, + 1, + 3.122, + -0.24, + 4.211, + -0.24, + 5.3, + -0.24, + 0, + 5.333, + -0.24 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 1.767, + 0, + 3.533, + 0, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 0, + 2, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 0, + 2, + 5.3, + 0, + 0, + 5.333, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 5.3, + 1, + 0, + 5.333, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 5.3, + 0, + 0, + 5.333, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m16.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 249, + "TotalPointCount": 642, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.322, + 1, + 2.644, + 1, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 2, + 1, + 0.333, + 2, + 0.667, + 2, + 1, + 2, + 1, + 1.133, + 2, + 1.267, + 2, + 1.4, + 2, + 1, + 1.511, + 2, + 1.622, + 2.132, + 1.733, + 1.634, + 1, + 1.878, + 0.986, + 2.022, + -4, + 2.167, + -4, + 1, + 2.367, + -4, + 2.567, + 0, + 2.767, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 3, + 1, + 0.267, + 3, + 0.533, + 3, + 0.8, + 3, + 1, + 0.878, + 3, + 0.956, + -4, + 1.033, + -4, + 1, + 1.189, + -4, + 1.344, + 2.269, + 1.5, + 11, + 1, + 1.611, + 17.237, + 1.722, + 19, + 1.833, + 19, + 1, + 1.978, + 19, + 2.122, + -28, + 2.267, + -28, + 1, + 2.467, + -28, + 2.667, + -23, + 2.867, + -23, + 0, + 4, + -23 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + -7, + 1, + 0.4, + -7, + 0.8, + -7, + 1.2, + -7, + 1, + 1.333, + -7, + 1.467, + 19, + 1.6, + 19, + 1, + 1.711, + 19, + 1.822, + 19.636, + 1.933, + 16.192, + 1, + 2.078, + 11.714, + 2.222, + -27, + 2.367, + -27, + 1, + 2.567, + -27, + 2.767, + -23, + 2.967, + -23, + 0, + 4, + -23 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 0.5, + 1.4, + 0.5, + 1, + 1.511, + 0.5, + 1.622, + 0.5, + 1.733, + 0.5, + 1, + 1.878, + 0.5, + 2.022, + 0.5, + 2.167, + 0.5, + 0, + 4, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.133, + 1, + 1.267, + 1.6, + 1.4, + 1.6, + 1, + 1.511, + 1.6, + 1.622, + 1.502, + 1.733, + 1.502, + 1, + 1.767, + 1.502, + 1.8, + 1.8, + 1.833, + 1.8, + 1, + 1.944, + 1.8, + 2.056, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 0, + 1.4, + 0, + 1, + 1.511, + 0, + 1.622, + 0, + 1.733, + 0, + 1, + 1.878, + 0, + 2.022, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.133, + 1, + 1.267, + 1.6, + 1.4, + 1.6, + 1, + 1.511, + 1.6, + 1.622, + 1.502, + 1.733, + 1.502, + 1, + 1.767, + 1.502, + 1.8, + 1.8, + 1.833, + 1.8, + 1, + 1.944, + 1.8, + 2.056, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 0, + 1.4, + 0, + 1, + 1.511, + 0, + 1.622, + 0, + 1.733, + 0, + 1, + 1.878, + 0, + 2.022, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.244, + 0, + 1.489, + 0.015, + 1.733, + 0.186, + 1, + 1.878, + 0.286, + 2.022, + 0.6, + 2.167, + 0.6, + 0, + 4, + 0.6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.244, + 0, + 1.489, + 0, + 1.733, + 0, + 1, + 1.878, + 0, + 2.022, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 0, + 1.4, + 0, + 1, + 1.511, + 0, + 1.622, + -0.023, + 1.733, + 0.061, + 1, + 1.878, + 0.171, + 2.022, + 1, + 2.167, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 0, + 1.4, + 0, + 1, + 1.511, + 0, + 1.622, + 0.031, + 1.733, + 0.031, + 1, + 1.878, + 0.031, + 2.022, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + -0.2, + 1.4, + -0.2, + 1, + 1.511, + -0.2, + 1.622, + -0.157, + 1.733, + -0.157, + 1, + 1.878, + -0.157, + 2.022, + -0.4, + 2.167, + -0.4, + 0, + 4, + -0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 0.7, + 1.4, + 0.7, + 1, + 1.511, + 0.7, + 1.622, + 0.735, + 1.733, + 0.608, + 1, + 1.878, + 0.444, + 2.022, + -0.8, + 2.167, + -0.8, + 0, + 4, + -0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 0.7, + 1.4, + 0.7, + 1, + 1.511, + 0.7, + 1.622, + 0.735, + 1.733, + 0.608, + 1, + 1.878, + 0.444, + 2.022, + -0.8, + 2.167, + -0.8, + 0, + 4, + -0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 0.1, + 1.4, + 0.1, + 1, + 1.511, + 0.1, + 1.622, + 0.112, + 1.733, + 0.069, + 1, + 1.878, + 0.015, + 2.022, + -0.4, + 2.167, + -0.4, + 0, + 4, + -0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 0.1, + 1.4, + 0.1, + 1, + 1.511, + 0.1, + 1.622, + 0.112, + 1.733, + 0.069, + 1, + 1.878, + 0.015, + 2.022, + -0.4, + 2.167, + -0.4, + 0, + 4, + -0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 0.7, + 1.4, + 0.7, + 1, + 1.511, + 0.7, + 1.622, + 0.4, + 1.733, + 0.4, + 1, + 1.878, + 0.4, + 2.022, + 0.8, + 2.167, + 0.8, + 0, + 4, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 0.7, + 1.4, + 0.7, + 1, + 1.511, + 0.7, + 1.622, + 0.4, + 1.733, + 0.4, + 1, + 1.878, + 0.4, + 2.022, + 0.8, + 2.167, + 0.8, + 0, + 4, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + -0.451, + 1.4, + -0.7, + 1, + 1.511, + -0.908, + 1.622, + -0.9, + 1.733, + -0.9, + 1, + 1.878, + -0.9, + 2.022, + -0.8, + 2.167, + -0.8, + 0, + 4, + -0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + -0.451, + 1.4, + -0.7, + 1, + 1.511, + -0.908, + 1.622, + -0.9, + 1.733, + -0.9, + 1, + 1.878, + -0.9, + 2.022, + -0.8, + 2.167, + -0.8, + 0, + 4, + -0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.133, + 1, + 1.267, + -0.37, + 1.4, + -0.4, + 1, + 1.511, + -0.425, + 1.622, + -0.417, + 1.733, + -0.435, + 1, + 1.878, + -0.459, + 2.022, + -0.5, + 2.167, + -0.5, + 0, + 4, + -0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 0.4, + 1.4, + 0.4, + 1, + 1.511, + 0.4, + 1.622, + 0.409, + 1.733, + 0.376, + 1, + 1.878, + 0.332, + 2.022, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.244, + 0, + 1.489, + 0, + 1.733, + 0, + 1, + 1.878, + 0, + 2.022, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.322, + 0, + 0.644, + 0, + 0.967, + 0, + 1, + 1.1, + 0, + 1.233, + 5, + 1.367, + 5, + 1, + 1.478, + 5, + 1.589, + 5.238, + 1.7, + 4.267, + 1, + 1.844, + 3.005, + 1.989, + -7, + 2.133, + -7, + 1, + 2.333, + -7, + 2.533, + -6, + 2.733, + -6, + 0, + 4, + -6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 3, + 1.4, + 3, + 1, + 1.511, + 3, + 1.622, + 3.254, + 1.733, + 2.206, + 1, + 1.878, + 0.845, + 2.022, + -10, + 2.167, + -10, + 1, + 2.367, + -10, + 2.567, + -9, + 2.767, + -9, + 0, + 4, + -9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.389, + 0, + 0.778, + 0, + 1.167, + 0, + 1, + 1.3, + 0, + 1.433, + 2, + 1.567, + 2, + 1, + 1.678, + 2, + 1.789, + 2.188, + 1.9, + 1.451, + 1, + 2.044, + 0.491, + 2.189, + -7, + 2.333, + -7, + 1, + 2.533, + -7, + 2.733, + -6, + 2.933, + -6, + 0, + 4, + -6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + -3, + 1.4, + -3, + 1, + 1.511, + -3, + 1.622, + -3.069, + 1.733, + -2.817, + 1, + 1.878, + -2.489, + 2.022, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.244, + 0, + 1.489, + 0, + 1.733, + 0, + 1, + 1.878, + 0, + 2.022, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.244, + 0, + 1.489, + 0, + 1.733, + 0, + 1, + 1.878, + 0, + 2.022, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.244, + 1, + 1.489, + 1, + 1.733, + 1, + 1, + 1.878, + 1, + 2.022, + 1, + 2.167, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.244, + 1, + 1.489, + 1, + 1.733, + 1, + 1, + 1.878, + 1, + 2.022, + 1, + 2.167, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 2.7, + 1, + 0.333, + 2.7, + 0.667, + 2.7, + 1, + 2.7, + 1, + 1.133, + 2.7, + 1.267, + 4.3, + 1.4, + 4.3, + 1, + 1.511, + 4.3, + 1.622, + 4.356, + 1.733, + 4.19, + 1, + 1.922, + 3.907, + 2.111, + 2.5, + 2.3, + 2.5, + 0, + 4, + 2.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 2.7, + 1, + 0.333, + 2.7, + 0.667, + 2.7, + 1, + 2.7, + 1, + 1.133, + 2.7, + 1.267, + 4.3, + 1.4, + 4.3, + 1, + 1.511, + 4.3, + 1.622, + 4.356, + 1.733, + 4.19, + 1, + 1.922, + 3.907, + 2.111, + 2.5, + 2.3, + 2.5, + 0, + 4, + 2.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0.5, + 1, + 0.333, + 0.5, + 0.667, + 0.5, + 1, + 0.5, + 1, + 1.311, + 0.5, + 1.622, + 0.5, + 1.933, + 0.5, + 1, + 2.178, + 0.5, + 2.422, + 0.5, + 2.667, + 0.5, + 0, + 4, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0.3, + 1, + 0.333, + 0.3, + 0.667, + 0.3, + 1, + 0.3, + 1, + 1.078, + 0.3, + 1.156, + 1, + 1.233, + 1, + 1, + 1.356, + 1, + 1.467, + 0.4, + 1.6, + 0.4, + 1, + 1.711, + 0.4, + 1.822, + 0.387, + 1.933, + 0.327, + 1, + 2.178, + 0.255, + 2.422, + 0.2, + 2.667, + 0.2, + 0, + 4, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.311, + 0, + 1.622, + 0, + 1.933, + 0, + 1, + 2.178, + 0, + 2.422, + 0, + 2.667, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + -0.17, + 1, + 0.333, + -0.17, + 0.667, + -0.17, + 1, + -0.17, + 1, + 1.078, + -0.17, + 1.156, + -1, + 1.233, + -1, + 1, + 1.356, + -1, + 1.467, + -0.4, + 1.6, + -0.4, + 1, + 1.711, + -0.4, + 1.822, + -0.379, + 1.933, + -0.333, + 1, + 2.178, + -0.262, + 2.422, + -0.2, + 2.667, + -0.2, + 0, + 4, + -0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.244, + 0, + 1.489, + 0, + 1.733, + 0, + 1, + 1.878, + 0, + 2.022, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.244, + 0, + 1.489, + 0, + 1.733, + 0, + 1, + 1.878, + 0, + 2.022, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.244, + 0, + 1.489, + 0, + 1.733, + 0, + 1, + 1.878, + 0, + 2.022, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 0, + 2, + 3.967, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 0, + 2, + 3.967, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 3.967, + 0, + 0, + 4, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m17.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4.5, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 209, + "TotalPointCount": 522, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.489, + 1, + 2.978, + 1, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.4, + 0, + 1.7, + -5, + 2, + -5, + 1, + 2.244, + -5, + 2.489, + 17, + 2.733, + 17, + 1, + 3.311, + 17, + 3.889, + 17, + 4.467, + 17, + 0, + 4.5, + 17 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.4, + 0, + 1.7, + 6.189, + 2, + 8, + 1, + 2.211, + 9.275, + 2.422, + 9, + 2.633, + 9, + 1, + 2.9, + 9, + 3.167, + -8, + 3.433, + -8, + 1, + 3.778, + -8, + 4.122, + -8, + 4.467, + -8, + 0, + 4.5, + -8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.922, + 0, + 2.744, + -10, + 3.567, + -10, + 1, + 3.867, + -10, + 4.167, + -10, + 4.467, + -10, + 0, + 4.5, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0.5, + 1, + 0.744, + 0.5, + 1.489, + 0.5, + 2.233, + 0.5, + 1, + 2.389, + 0.5, + 2.544, + 1, + 2.7, + 1, + 1, + 3.289, + 1, + 3.878, + 1, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 0.91, + 1, + 0.367, + 0.91, + 0.733, + 0.91, + 1.1, + 0.91, + 1, + 1.689, + 0.91, + 2.278, + 0.91, + 2.867, + 0.91, + 1, + 2.922, + 0.91, + 2.978, + 0, + 3.033, + 0, + 1, + 3.1, + 0, + 3.167, + 0, + 3.233, + 0, + 1, + 3.311, + 0, + 3.389, + 0.91, + 3.467, + 0.91, + 1, + 3.8, + 0.91, + 4.133, + 0.91, + 4.467, + 0.91, + 0, + 4.5, + 0.91 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 0.9, + 1, + 0.367, + 0.9, + 0.733, + 0.9, + 1.1, + 0.9, + 1, + 1.689, + 0.9, + 2.278, + 0.9, + 2.867, + 0.9, + 1, + 2.922, + 0.9, + 2.978, + 0, + 3.033, + 0, + 1, + 3.1, + 0, + 3.167, + 0, + 3.233, + 0, + 1, + 3.311, + 0, + 3.389, + 0.9, + 3.467, + 0.9, + 1, + 3.8, + 0.9, + 4.133, + 0.9, + 4.467, + 0.9, + 0, + 4.5, + 0.9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.378, + 0, + 1.656, + 0.06, + 1.933, + 0.06, + 1, + 2.411, + 0.06, + 2.889, + -0.58, + 3.367, + -0.58, + 1, + 3.733, + -0.58, + 4.1, + -0.58, + 4.467, + -0.58, + 0, + 4.5, + -0.58 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.378, + 0, + 1.656, + -0.08, + 1.933, + -0.08, + 1, + 2.411, + -0.08, + 2.889, + 0.83, + 3.367, + 0.83, + 1, + 3.733, + 0.83, + 4.1, + 0.83, + 4.467, + 0.83, + 0, + 4.5, + 0.83 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + -0.37, + 1, + 0.7, + -0.37, + 1.4, + -0.37, + 2.1, + -0.37, + 1, + 2.267, + -0.37, + 2.433, + -0.37, + 2.6, + -0.37, + 1, + 3.222, + -0.37, + 3.844, + -0.37, + 4.467, + -0.37, + 0, + 4.5, + -0.37 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + -0.37, + 1, + 0.7, + -0.37, + 1.4, + -0.37, + 2.1, + -0.37, + 1, + 2.267, + -0.37, + 2.433, + -0.37, + 2.6, + -0.37, + 1, + 3.222, + -0.37, + 3.844, + -0.37, + 4.467, + -0.37, + 0, + 4.5, + -0.37 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.7, + 0, + 1.4, + 0, + 2.1, + 0, + 1, + 2.267, + 0, + 2.433, + 0, + 2.6, + 0, + 1, + 3.222, + 0, + 3.844, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.7, + 0, + 1.4, + 0, + 2.1, + 0, + 1, + 2.267, + 0, + 2.433, + 0, + 2.6, + 0, + 1, + 3.222, + 0, + 3.844, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0.18, + 1, + 0.7, + 0.18, + 1.4, + 0.18, + 2.1, + 0.18, + 1, + 2.267, + 0.18, + 2.433, + 0.18, + 2.6, + 0.18, + 1, + 3.222, + 0.18, + 3.844, + 0.18, + 4.467, + 0.18, + 0, + 4.5, + 0.18 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0.28, + 1, + 0.7, + 0.28, + 1.4, + 0.28, + 2.1, + 0.28, + 1, + 2.267, + 0.28, + 2.433, + 0.28, + 2.6, + 0.28, + 1, + 3.222, + 0.28, + 3.844, + 0.28, + 4.467, + 0.28, + 0, + 4.5, + 0.28 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.7, + 0, + 1.4, + 0, + 2.1, + 0, + 1, + 2.267, + 0, + 2.433, + -0.64, + 2.6, + -0.64, + 1, + 3.222, + -0.64, + 3.844, + -0.64, + 4.467, + -0.64, + 0, + 4.5, + -0.64 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.7, + 0, + 1.4, + 0, + 2.1, + 0, + 1, + 2.267, + 0, + 2.433, + -0.51, + 2.6, + -0.51, + 1, + 3.222, + -0.51, + 3.844, + -0.51, + 4.467, + -0.51, + 0, + 4.5, + -0.51 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.367, + 1, + 0.733, + 1, + 1.1, + 1, + 1, + 2.222, + 1, + 3.344, + 1, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.278, + 0, + 1.456, + -6, + 1.633, + -6, + 1, + 2.2, + -6, + 2.767, + 10, + 3.333, + 10, + 1, + 3.711, + 10, + 4.089, + 10, + 4.467, + 10, + 0, + 4.5, + 10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.311, + 0, + 1.522, + 3, + 1.733, + 3, + 1, + 2.211, + 3, + 2.689, + -10, + 3.167, + -10, + 1, + 3.6, + -10, + 4.033, + -10, + 4.467, + -10, + 0, + 4.5, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.356, + 0, + 1.611, + -1.366, + 1.867, + -2, + 1, + 2.233, + -2.91, + 2.6, + -3, + 2.967, + -3, + 1, + 3.467, + -3, + 3.967, + -3, + 4.467, + -3, + 0, + 4.5, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.389, + 0, + 1.678, + -4, + 1.967, + -4, + 1, + 2.244, + -4, + 2.522, + 7, + 2.8, + 7, + 1, + 3.356, + 7, + 3.911, + 7, + 4.467, + 7, + 0, + 4.5, + 7 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + -1, + 1, + 0.367, + -1, + 0.733, + -1, + 1.1, + -1, + 1, + 1.411, + -1, + 1.722, + -0.9, + 2.033, + -0.9, + 1, + 2.522, + -0.9, + 3.011, + -1, + 3.5, + -1, + 1, + 3.822, + -1, + 4.144, + -1, + 4.467, + -1, + 0, + 4.5, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + -1, + 1, + 0.367, + -1, + 0.733, + -1, + 1.1, + -1, + 1, + 1.411, + -1, + 1.722, + -0.91, + 2.033, + -0.91, + 1, + 2.522, + -0.91, + 3.011, + -1, + 3.5, + -1, + 1, + 3.822, + -1, + 4.144, + -1, + 4.467, + -1, + 0, + 4.5, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 2.222, + 0, + 3.344, + 0, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 4.467, + 0, + 0, + 4.5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 4.467, + 1, + 0, + 4.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 4.467, + 0, + 0, + 4.5, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m18.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 3.2, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 167, + "TotalPointCount": 396, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.056, + 1, + 2.111, + 1, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + -30, + 0.567, + -30, + 1, + 0.733, + -30, + 0.9, + -30.83, + 1.067, + -25, + 1, + 1.267, + -18.004, + 1.467, + 30, + 1.667, + 30, + 1, + 1.944, + 30, + 2.222, + 0, + 2.5, + 0, + 1, + 2.722, + 0, + 2.944, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 1, + 1, + 1.056, + 1, + 2.111, + 1, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 0, + 1, + 0.4, + 0, + 0.8, + 0, + 1.2, + 0, + 1, + 1.333, + 0, + 1.467, + 1.17, + 1.6, + 1.17, + 1, + 1.711, + 1.17, + 1.822, + 1, + 1.933, + 1, + 1, + 2.122, + 1, + 2.311, + 1, + 2.5, + 1, + 1, + 2.722, + 1, + 2.944, + 1, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 1, + 1, + 1.056, + 1, + 2.111, + 1, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 0, + 1, + 0.4, + 0, + 0.8, + 0, + 1.2, + 0, + 1, + 1.333, + 0, + 1.467, + 1.17, + 1.6, + 1.17, + 1, + 1.711, + 1.17, + 1.822, + 1, + 1.933, + 1, + 1, + 2.122, + 1, + 2.311, + 1, + 2.5, + 1, + 1, + 2.722, + 1, + 2.944, + 1, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 1, + 1, + 1.056, + 1, + 2.111, + 1, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + -1, + 1, + 0.311, + -1, + 0.622, + -1, + 0.933, + -1, + 1, + 1.022, + -1, + 1.111, + -1, + 1.2, + -1, + 1, + 1.333, + -1, + 1.467, + 0.21, + 1.6, + 0.21, + 1, + 2.122, + 0.21, + 2.644, + 0.21, + 3.167, + 0.21, + 0, + 3.2, + 0.21 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + -1, + 1, + 0.311, + -1, + 0.622, + -1, + 0.933, + -1, + 1, + 1.022, + -1, + 1.111, + -1, + 1.2, + -1, + 1, + 1.333, + -1, + 1.467, + 0.2, + 1.6, + 0.2, + 1, + 2.122, + 0.2, + 2.644, + 0.2, + 3.167, + 0.2, + 0, + 3.2, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0.61, + 1, + 0.311, + 0.61, + 0.622, + 0.61, + 0.933, + 0.61, + 1, + 1.156, + 0.61, + 1.378, + 0.39, + 1.6, + 0.39, + 1, + 2.122, + 0.39, + 2.644, + 0.39, + 3.167, + 0.39, + 0, + 3.2, + 0.39 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0.61, + 1, + 0.311, + 0.61, + 0.622, + 0.61, + 0.933, + 0.61, + 1, + 1.156, + 0.61, + 1.378, + 0.39, + 1.6, + 0.39, + 1, + 2.122, + 0.39, + 2.644, + 0.39, + 3.167, + 0.39, + 0, + 3.2, + 0.39 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + -1, + 1, + 0.311, + -1, + 0.622, + -1, + 0.933, + -1, + 1, + 1.156, + -1, + 1.378, + 0, + 1.6, + 0, + 1, + 2.122, + 0, + 2.644, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + -1, + 1, + 0.311, + -1, + 0.622, + -1, + 0.933, + -1, + 1, + 1.156, + -1, + 1.378, + 0, + 1.6, + 0, + 1, + 2.122, + 0, + 2.644, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 1.056, + 1, + 2.111, + 1, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + -3.302, + 0.567, + -3.302, + 1, + 0.789, + -3.302, + 1.011, + -2.342, + 1.233, + -1, + 1, + 1.367, + -0.195, + 1.5, + 0, + 1.633, + 0, + 1, + 1.922, + 0, + 2.211, + 0, + 2.5, + 0, + 1, + 2.722, + 0, + 2.944, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + -4.918, + 0.567, + -4.918, + 1, + 0.789, + -4.918, + 1.011, + -4.041, + 1.233, + -2, + 1, + 1.367, + -0.776, + 1.5, + 0, + 1.633, + 0, + 1, + 1.922, + 0, + 2.211, + 0, + 2.5, + 0, + 1, + 2.722, + 0, + 2.944, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.489, + 0, + 0.978, + 1, + 1.467, + 1, + 1, + 2.033, + 1, + 2.6, + 0.006, + 3.167, + 0.006, + 0, + 3.2, + 0.006 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + -1, + 1, + 0.267, + -1, + 0.533, + -1, + 0.8, + -1, + 1, + 1.067, + -1, + 1.333, + 1, + 1.6, + 1, + 1, + 1.9, + 1, + 2.2, + 1, + 2.5, + 1, + 1, + 2.722, + 1, + 2.944, + 1, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + -1, + 1, + 0.267, + -1, + 0.533, + -1, + 0.8, + -1, + 1, + 1.067, + -1, + 1.333, + 1, + 1.6, + 1, + 1, + 1.9, + 1, + 2.2, + 1, + 2.5, + 1, + 1, + 2.722, + 1, + 2.944, + 1, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 1.056, + 0, + 2.111, + 0, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 3.167, + 0, + 0, + 3.2, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 3.167, + 1, + 0, + 3.2, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 3.167, + 0, + 0, + 3.2, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m19.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 8, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 133, + "TotalPointCount": 294, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 2.656, + 1, + 5.311, + 1, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + -8, + 1, + 2.656, + -8, + 5.311, + -8, + 7.967, + -8, + 0, + 8, + -8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + -30, + 1, + 2.656, + -30, + 5.311, + -30, + 7.967, + -30, + 0, + 8, + -30 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + -30, + 1, + 2.656, + -30, + 5.311, + -30, + 7.967, + -30, + 0, + 8, + -30 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 1, + 1, + 2.656, + 1, + 5.311, + 1, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + -1, + 1, + 2.656, + -1, + 5.311, + -1, + 7.967, + -1, + 0, + 8, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0.11, + 1, + 2.656, + 0.11, + 5.311, + 0.11, + 7.967, + 0.11, + 0, + 8, + 0.11 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0.46, + 1, + 2.656, + 0.46, + 5.311, + 0.46, + 7.967, + 0.46, + 0, + 8, + 0.46 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + -1, + 1, + 2.656, + -1, + 5.311, + -1, + 7.967, + -1, + 0, + 8, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + -1, + 1, + 2.656, + -1, + 5.311, + -1, + 7.967, + -1, + 0, + 8, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + -0.69, + 1, + 2.656, + -0.69, + 5.311, + -0.69, + 7.967, + -0.69, + 0, + 8, + -0.69 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + -0.67, + 1, + 2.656, + -0.67, + 5.311, + -0.67, + 7.967, + -0.67, + 0, + 8, + -0.67 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 2.656, + 1, + 5.311, + 1, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + -4.909, + 1, + 0.211, + -4.909, + 0.422, + -5.158, + 0.633, + -4.278, + 1, + 0.922, + -3.075, + 1.211, + 5.81, + 1.5, + 5.81, + 1, + 1.844, + 5.81, + 2.189, + -4.909, + 2.533, + -4.909, + 1, + 2.811, + -4.909, + 3.089, + 2.657, + 3.367, + 2.657, + 1, + 3.644, + 2.657, + 3.922, + -4.278, + 4.2, + -4.278, + 1, + 4.433, + -4.278, + 4.667, + 3.918, + 4.9, + 3.918, + 1, + 5.2, + 3.918, + 5.5, + -3.017, + 5.8, + -3.017, + 1, + 6.178, + -3.017, + 6.556, + 3.288, + 6.933, + 3.288, + 1, + 7.278, + 3.288, + 7.622, + -4.909, + 7.967, + -4.909, + 0, + 8, + -4.909 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + -10, + 1, + 2.656, + -10, + 5.311, + -10, + 7.967, + -10, + 0, + 8, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + -2, + 1, + 2.656, + -2, + 5.311, + -2, + 7.967, + -2, + 0, + 8, + -2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 10, + 1, + 2.656, + 10, + 5.311, + 10, + 7.967, + 10, + 0, + 8, + 10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.656, + 0, + 1.311, + 1, + 1.967, + 1, + 1, + 2.633, + 1, + 3.3, + 0, + 3.967, + 0, + 1, + 4.656, + 0, + 5.344, + 1, + 6.033, + 1, + 1, + 6.678, + 1, + 7.322, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 1, + 1, + 2.656, + 1, + 5.311, + 1, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 1, + 1, + 2.656, + 1, + 5.311, + 1, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 7.967, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 7.967, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 7.967, + 0, + 0, + 8, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m20.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 6.033, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 329, + "TotalPointCount": 882, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 2, + 1, + 4, + 1, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.733, + 0, + 1.133, + -21, + 1.533, + -21, + 1, + 1.611, + -21, + 1.689, + -21, + 1.767, + -21, + 1, + 1.944, + -21, + 2.122, + -20.074, + 2.3, + -17.479, + 1, + 2.5, + -12.897, + 2.7, + 19, + 2.9, + 19, + 1, + 3.078, + 19, + 3.256, + 19, + 3.433, + 19, + 1, + 3.689, + 19, + 3.944, + 19, + 4.2, + 19, + 1, + 4.8, + 19, + 5.4, + 19, + 6, + 19, + 0, + 6.033, + 19 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.544, + 0, + 0.756, + -13, + 0.967, + -13, + 1, + 1.122, + -13, + 1.278, + 7.633, + 1.433, + 8.986, + 1, + 1.578, + 10.242, + 1.722, + 9.689, + 1.867, + 11, + 1, + 2.044, + 12.613, + 2.222, + 28, + 2.4, + 28, + 1, + 2.6, + 28, + 2.8, + -9.978, + 3, + -11.975, + 1, + 3.144, + -13.418, + 3.289, + -13, + 3.433, + -13, + 1, + 3.689, + -13, + 3.944, + -13, + 4.2, + -13, + 1, + 4.311, + -13, + 4.422, + -6, + 4.533, + -6, + 1, + 4.644, + -6, + 4.756, + -13, + 4.867, + -13, + 1, + 5.244, + -13, + 5.622, + -13, + 6, + -13, + 0, + 6.033, + -13 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + -6, + 1, + 0.111, + -6, + 0.222, + -6, + 0.333, + -6, + 1, + 0.811, + -6, + 1.289, + -17, + 1.767, + -17, + 1, + 1.844, + -17, + 1.922, + -16.739, + 2, + -16, + 1, + 2.3, + -7.838, + 2.6, + 30, + 2.9, + 30, + 1, + 3.078, + 30, + 3.256, + 30, + 3.433, + 30, + 1, + 3.722, + 30, + 4.011, + 30, + 4.3, + 30, + 1, + 4.411, + 30, + 4.522, + 20, + 4.633, + 20, + 1, + 4.744, + 20, + 4.856, + 30, + 4.967, + 30, + 1, + 5.311, + 30, + 5.656, + 30, + 6, + 30, + 0, + 6.033, + 30 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.767, + 0, + 1.2, + 0, + 1.633, + 0, + 1, + 1.767, + 0, + 1.9, + 0, + 2.033, + 0, + 1, + 3.356, + 0, + 4.678, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 2.222, + 0, + 4.111, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.111, + 1, + 0.222, + 1, + 0.333, + 1, + 1, + 0.467, + 1, + 0.6, + 1, + 0.733, + 1, + 1, + 0.789, + 1, + 0.844, + 0, + 0.9, + 0, + 1, + 0.967, + 0, + 1.033, + 1, + 1.1, + 1, + 1, + 1.278, + 1, + 1.456, + 1, + 1.633, + 1, + 1, + 1.767, + 1, + 1.9, + 1, + 2.033, + 1, + 1, + 2.1, + 1, + 2.167, + 0, + 2.233, + 0, + 1, + 2.267, + 0, + 2.3, + 0, + 2.333, + 0, + 1, + 2.411, + 0, + 2.489, + 0.9, + 2.567, + 0.9, + 1, + 2.678, + 0.9, + 2.789, + 0.8, + 2.9, + 0.8, + 1, + 3.244, + 0.8, + 3.589, + 0.8, + 3.933, + 0.8, + 1, + 3.978, + 0.8, + 4.022, + 0, + 4.067, + 0, + 1, + 4.178, + 0, + 4.289, + 0, + 4.4, + 0, + 1, + 4.478, + 0, + 4.556, + 0.8, + 4.633, + 0.8, + 1, + 5.089, + 0.8, + 5.544, + 0.8, + 6, + 0.8, + 0, + 6.033, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.767, + 0, + 1.2, + 0, + 1.633, + 0, + 1, + 1.767, + 0, + 1.9, + 0, + 2.033, + 0, + 1, + 3.356, + 0, + 4.678, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.111, + 1, + 0.222, + 1, + 0.333, + 1, + 1, + 0.467, + 1, + 0.6, + 1, + 0.733, + 1, + 1, + 0.789, + 1, + 0.844, + 0, + 0.9, + 0, + 1, + 0.967, + 0, + 1.033, + 1, + 1.1, + 1, + 1, + 1.278, + 1, + 1.456, + 1, + 1.633, + 1, + 1, + 1.767, + 1, + 1.9, + 1, + 2.033, + 1, + 1, + 2.1, + 1, + 2.167, + 0, + 2.233, + 0, + 1, + 2.267, + 0, + 2.3, + 0, + 2.333, + 0, + 1, + 2.411, + 0, + 2.489, + 0.9, + 2.567, + 0.9, + 1, + 2.678, + 0.9, + 2.789, + 0.8, + 2.9, + 0.8, + 1, + 3.244, + 0.8, + 3.589, + 0.8, + 3.933, + 0.8, + 1, + 3.978, + 0.8, + 4.022, + 0, + 4.067, + 0, + 1, + 4.178, + 0, + 4.289, + 0, + 4.4, + 0, + 1, + 4.478, + 0, + 4.556, + 0.8, + 4.633, + 0.8, + 1, + 5.089, + 0.8, + 5.544, + 0.8, + 6, + 0.8, + 0, + 6.033, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.767, + 0, + 1.2, + 0, + 1.633, + 0, + 1, + 1.767, + 0, + 1.9, + 0, + 2.033, + 0, + 1, + 3.356, + 0, + 4.678, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.767, + 0, + 1.2, + 0, + 1.633, + 0, + 1, + 1.767, + 0, + 1.9, + 0, + 2.033, + 0, + 1, + 3.356, + 0, + 4.678, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.767, + 0, + 1.2, + 0, + 1.633, + 0, + 1, + 1.767, + 0, + 1.9, + 0, + 2.033, + 0, + 1, + 3.356, + 0, + 4.678, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.467, + 0, + 0.6, + 0, + 0.733, + 0, + 1, + 0.856, + 0, + 0.978, + -0.82, + 1.1, + -0.82, + 1, + 1.411, + -0.82, + 1.722, + -0.82, + 2.033, + -0.82, + 1, + 2.667, + -0.82, + 3.3, + -0.82, + 3.933, + -0.82, + 1, + 4.167, + -0.82, + 4.4, + 1, + 4.633, + 1, + 1, + 5.089, + 1, + 5.544, + 1, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.467, + 0, + 0.6, + 0, + 0.733, + 0, + 1, + 0.789, + 0, + 0.844, + -0.4, + 0.9, + -0.4, + 1, + 0.967, + -0.4, + 1.033, + 0.33, + 1.1, + 0.33, + 1, + 1.411, + 0.33, + 1.722, + 0.33, + 2.033, + 0.33, + 1, + 2.211, + 0.33, + 2.389, + -0.6, + 2.567, + -0.6, + 1, + 3.022, + -0.6, + 3.478, + 0.33, + 3.933, + 0.33, + 1, + 3.978, + 0.33, + 4.022, + -0.5, + 4.067, + -0.5, + 1, + 4.178, + -0.5, + 4.289, + -0.5, + 4.4, + -0.5, + 1, + 4.478, + -0.5, + 4.556, + -1, + 4.633, + -1, + 1, + 5.089, + -1, + 5.544, + -1, + 6, + -1, + 0, + 6.033, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.9, + 0, + 1.467, + 0, + 2.033, + 0, + 1, + 2.211, + 0, + 2.389, + 0, + 2.567, + 0, + 1, + 3.111, + 0, + 3.656, + -0.73, + 4.2, + -0.73, + 1, + 4.8, + -0.73, + 5.4, + -0.73, + 6, + -0.73, + 0, + 6.033, + -0.73 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.9, + 0, + 1.467, + 0, + 2.033, + 0, + 1, + 2.211, + 0, + 2.389, + 0, + 2.567, + 0, + 1, + 3.111, + 0, + 3.656, + -0.73, + 4.2, + -0.73, + 1, + 4.8, + -0.73, + 5.4, + -0.73, + 6, + -0.73, + 0, + 6.033, + -0.73 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.9, + 0, + 1.467, + 0, + 2.033, + 0, + 1, + 2.211, + 0, + 2.389, + 0, + 2.567, + 0, + 1, + 3.111, + 0, + 3.656, + -0.4, + 4.2, + -0.4, + 1, + 4.8, + -0.4, + 5.4, + -0.4, + 6, + -0.4, + 0, + 6.033, + -0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.9, + 0, + 1.467, + 0, + 2.033, + 0, + 1, + 2.211, + 0, + 2.389, + 0, + 2.567, + 0, + 1, + 3.111, + 0, + 3.656, + -0.34, + 4.2, + -0.34, + 1, + 4.8, + -0.34, + 5.4, + -0.34, + 6, + -0.34, + 0, + 6.033, + -0.34 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.9, + 0, + 1.467, + 0, + 2.033, + 0, + 1, + 2.211, + 0, + 2.389, + 0, + 2.567, + 0, + 1, + 3.111, + 0, + 3.656, + 0.47, + 4.2, + 0.47, + 1, + 4.8, + 0.47, + 5.4, + 0.47, + 6, + 0.47, + 0, + 6.033, + 0.47 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.9, + 0, + 1.467, + 0, + 2.033, + 0, + 1, + 2.211, + 0, + 2.389, + 0, + 2.567, + 0, + 1, + 3.111, + 0, + 3.656, + 0.46, + 4.2, + 0.46, + 1, + 4.8, + 0.46, + 5.4, + 0.46, + 6, + 0.46, + 0, + 6.033, + 0.46 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.9, + 0, + 1.467, + 0, + 2.033, + 0, + 1, + 2.211, + 0, + 2.389, + -1, + 2.567, + -1, + 1, + 3.111, + -1, + 3.656, + -1, + 4.2, + -1, + 1, + 4.8, + -1, + 5.4, + -1, + 6, + -1, + 0, + 6.033, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.9, + 0, + 1.467, + 0, + 2.033, + 0, + 1, + 2.211, + 0, + 2.389, + -1, + 2.567, + -1, + 1, + 3.111, + -1, + 3.656, + -1, + 4.2, + -1, + 1, + 4.8, + -1, + 5.4, + -1, + 6, + -1, + 0, + 6.033, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.9, + 0, + 1.467, + 0, + 2.033, + 0, + 1, + 2.211, + 0, + 2.389, + 0, + 2.567, + 0, + 1, + 3.111, + 0, + 3.656, + -1, + 4.2, + -1, + 1, + 4.8, + -1, + 5.4, + -1, + 6, + -1, + 0, + 6.033, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.767, + 0, + 1.2, + 0, + 1.633, + 0, + 1, + 3.089, + 0, + 4.544, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 2.222, + 0, + 4.111, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 1, + 0.7, + 1, + 1, + 1.056, + 1, + 1.411, + -10, + 1.767, + -10, + 1, + 1.911, + -10, + 2.056, + -10.237, + 2.2, + -9, + 1, + 2.389, + -7.382, + 2.578, + 6.715, + 2.767, + 6.783, + 1, + 3.011, + 6.871, + 3.256, + 6.898, + 3.5, + 6.933, + 1, + 3.711, + 6.963, + 3.922, + 6.965, + 4.133, + 7, + 1, + 4.256, + 7.02, + 4.378, + 10, + 4.5, + 10, + 1, + 4.622, + 10, + 4.744, + 7, + 4.867, + 7, + 1, + 5.244, + 7, + 5.622, + 7, + 6, + 7, + 0, + 6.033, + 7 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + -1, + 0.7, + -1, + 1, + 1.056, + -1, + 1.411, + -0.804, + 1.767, + 0, + 1, + 1.911, + 0.327, + 2.056, + 1, + 2.2, + 1, + 1, + 2.389, + 1, + 2.578, + -2.307, + 2.767, + -2.494, + 1, + 3.011, + -2.735, + 3.256, + -2.828, + 3.5, + -2.918, + 1, + 3.711, + -2.996, + 3.922, + -3, + 4.133, + -3, + 1, + 4.256, + -3, + 4.378, + -2, + 4.5, + -2, + 1, + 4.622, + -2, + 4.744, + -3, + 4.867, + -3, + 1, + 5.244, + -3, + 5.622, + -3, + 6, + -3, + 0, + 6.033, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + -1, + 0.7, + -1, + 1, + 1.056, + -1, + 1.411, + 5, + 1.767, + 5, + 1, + 1.911, + 5, + 2.056, + 0, + 2.2, + 0, + 1, + 2.389, + 0, + 2.578, + 3.604, + 2.767, + 3.716, + 1, + 3.011, + 3.86, + 3.256, + 3.923, + 3.5, + 3.965, + 1, + 3.711, + 4.002, + 3.922, + 4, + 4.133, + 4, + 1, + 4.256, + 4, + 4.378, + 3, + 4.5, + 3, + 1, + 4.622, + 3, + 4.744, + 4, + 4.867, + 4, + 1, + 5.244, + 4, + 5.622, + 4, + 6, + 4, + 0, + 6.033, + 4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.811, + 0, + 1.289, + 0, + 1.767, + 0, + 1, + 1.911, + 0, + 2.056, + -1, + 2.2, + -1, + 1, + 2.389, + -1, + 2.578, + 0, + 2.767, + 0, + 1, + 3.222, + 0, + 3.678, + 0, + 4.133, + 0, + 1, + 4.256, + 0, + 4.378, + -1, + 4.5, + -1, + 1, + 4.622, + -1, + 4.744, + 0, + 4.867, + 0, + 1, + 5.244, + 0, + 5.622, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.678, + 0, + 1.022, + 0, + 1.367, + 0, + 1, + 1.5, + 0, + 1.633, + 0, + 1.767, + 0, + 1, + 3.178, + 0, + 4.589, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.678, + 0, + 1.022, + 0, + 1.367, + 0, + 1, + 1.5, + 0, + 1.633, + 0, + 1.767, + 0, + 1, + 3.178, + 0, + 4.589, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0.5, + 1, + 0.111, + 0.5, + 0.222, + 0.5, + 0.333, + 0.5, + 1, + 0.678, + 0.5, + 1.022, + 0.5, + 1.367, + 0.5, + 1, + 1.5, + 0.5, + 1.633, + 0.5, + 1.767, + 0.5, + 1, + 3.178, + 0.5, + 4.589, + 0.5, + 6, + 0.5, + 0, + 6.033, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0.5, + 1, + 0.111, + 0.5, + 0.222, + 0.5, + 0.333, + 0.5, + 1, + 0.678, + 0.5, + 1.022, + 0.5, + 1.367, + 0.5, + 1, + 1.5, + 0.5, + 1.633, + 0.5, + 1.767, + 0.5, + 1, + 3.178, + 0.5, + 4.589, + 0.5, + 6, + 0.5, + 0, + 6.033, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + -0.2, + 1, + 0.111, + -0.2, + 0.222, + -0.2, + 0.333, + -0.2, + 1, + 0.489, + -0.2, + 0.644, + -0.2, + 0.8, + -0.2, + 1, + 0.989, + -0.2, + 1.178, + -1, + 1.367, + -1, + 1, + 1.5, + -1, + 1.633, + -1, + 1.767, + -1, + 1, + 3.178, + -1, + 4.589, + -1, + 6, + -1, + 0, + 6.033, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0.3, + 1, + 0.111, + 0.3, + 0.222, + 0.3, + 0.333, + 0.3, + 1, + 0.489, + 0.3, + 0.644, + 0.336, + 0.8, + 0.41, + 1, + 0.978, + 0.57, + 1.156, + 1.867, + 1.333, + 2.688, + 1, + 1.478, + 3.355, + 1.622, + 3.8, + 1.767, + 3.8, + 1, + 2.1, + 3.8, + 2.433, + 3.11, + 2.767, + 2.9, + 1, + 3.111, + 2.683, + 3.456, + 2.7, + 3.8, + 2.7, + 1, + 3.933, + 2.7, + 4.067, + 3.2, + 4.2, + 3.2, + 1, + 4.5, + 3.2, + 4.8, + 2.7, + 5.1, + 2.7, + 1, + 5.4, + 2.7, + 5.7, + 2.7, + 6, + 2.7, + 0, + 6.033, + 2.7 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 0, + 0.567, + 0, + 1, + 2.378, + 0, + 4.189, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 0, + 0.567, + 0, + 1, + 2.378, + 0, + 4.189, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 0, + 0.567, + 0, + 1, + 2.378, + 0, + 4.189, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + -0.3, + 1, + 0.189, + -0.3, + 0.378, + -0.3, + 0.567, + -0.3, + 1, + 0.756, + -0.3, + 0.944, + -0.621, + 1.133, + -0.621, + 1, + 1.289, + -0.621, + 1.444, + -0.632, + 1.6, + -0.573, + 1, + 1.767, + -0.509, + 1.933, + -0.064, + 2.1, + 0.312, + 1, + 2.233, + 0.613, + 2.367, + 1, + 2.5, + 1, + 1, + 2.722, + 1, + 2.944, + -0.55, + 3.167, + -0.55, + 1, + 3.456, + -0.55, + 3.744, + -0.55, + 4.033, + -0.55, + 1, + 4.156, + -0.55, + 4.278, + 0.35, + 4.4, + 0.35, + 1, + 4.711, + 0.35, + 5.022, + -0.55, + 5.333, + -0.55, + 1, + 5.556, + -0.55, + 5.778, + -0.55, + 6, + -0.55, + 0, + 6.033, + -0.55 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.678, + 0, + 1.022, + 0, + 1.367, + 0, + 1, + 1.5, + 0, + 1.633, + 0, + 1.767, + 0, + 1, + 3.178, + 0, + 4.589, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 2.222, + 0, + 4.111, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.678, + 0, + 1.022, + 0, + 1.367, + 0, + 1, + 1.5, + 0, + 1.633, + 0, + 1.767, + 0, + 1, + 3.178, + 0, + 4.589, + 0, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 0, + 2, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 0, + 2, + 6, + 0, + 0, + 6.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 6, + 1, + 0, + 6.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 6, + 0, + 0, + 6.033, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m21.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 5, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 354, + "TotalPointCount": 915, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 0.889, + 1, + 1.778, + 1, + 2.667, + 1, + 1, + 3.433, + 1, + 4.2, + 1, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.378, + 0, + 0.589, + 0, + 0.8, + 0, + 1, + 1, + 0, + 1.2, + 0, + 1.4, + 0, + 1, + 1.6, + 0, + 1.8, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 2.722, + 0, + 2.878, + 2, + 3.033, + 2, + 1, + 3.344, + 2, + 3.656, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.033, + 0, + 0.067, + 0, + 0.1, + 0, + 1, + 0.156, + 0, + 0.211, + 6, + 0.267, + 6, + 1, + 0.344, + 6, + 0.422, + -11.207, + 0.5, + -11.207, + 1, + 0.6, + -11.207, + 0.7, + 0, + 0.8, + 0, + 1, + 0.967, + 0, + 1.133, + -16.67, + 1.3, + -16.67, + 1, + 1.356, + -16.67, + 1.411, + -16.299, + 1.467, + -8.31, + 1, + 1.578, + 7.668, + 1.689, + 22.901, + 1.8, + 22.901, + 1, + 1.9, + 22.901, + 2, + -0.099, + 2.1, + -0.099, + 1, + 2.3, + -0.099, + 2.5, + 0, + 2.7, + 0, + 1, + 2.833, + 0, + 2.967, + -22, + 3.1, + -22, + 1, + 3.256, + -22, + 3.411, + 12, + 3.567, + 12, + 1, + 3.722, + 12, + 3.878, + -5, + 4.033, + -5, + 1, + 4.222, + -5, + 4.411, + 0, + 4.6, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.289, + 0, + 0.411, + 0, + 0.533, + 0, + 1, + 0.622, + 0, + 0.711, + 30, + 0.8, + 30, + 1, + 1, + 30, + 1.2, + -30, + 1.4, + -30, + 1, + 1.6, + -30, + 1.8, + 30, + 2, + 30, + 1, + 2.167, + 30, + 2.333, + 30.456, + 2.5, + 27, + 1, + 2.678, + 23.313, + 2.856, + -9, + 3.033, + -9, + 1, + 3.189, + -9, + 3.344, + 7, + 3.5, + 7, + 1, + 3.656, + 7, + 3.811, + -2, + 3.967, + -2, + 1, + 4.156, + -2, + 4.344, + 0, + 4.533, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.378, + 0, + 0.589, + 0.5, + 0.8, + 0.5, + 1, + 1.2, + 0.5, + 1.6, + 0.5, + 2, + 0.5, + 1, + 2.189, + 0.5, + 2.378, + 0.5, + 2.567, + 0.5, + 1, + 3.033, + 0.5, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.289, + 1, + 0.411, + 0, + 0.533, + 0, + 1, + 0.622, + 0, + 0.711, + 0, + 0.8, + 0, + 1, + 1, + 0, + 1.2, + 0, + 1.4, + 0, + 1, + 1.522, + 0, + 1.644, + 1, + 1.767, + 1, + 1, + 2.033, + 1, + 2.3, + 1, + 2.567, + 1, + 1, + 2.722, + 1, + 2.878, + 0, + 3.033, + 0, + 1, + 3.189, + 0, + 3.344, + 0, + 3.5, + 0, + 1, + 3.656, + 0, + 3.811, + 1, + 3.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.289, + 0, + 0.411, + 0, + 0.533, + 0, + 1, + 0.622, + 0, + 0.711, + 1, + 0.8, + 1, + 1, + 1, + 1, + 1.2, + 1, + 1.4, + 1, + 1, + 1.522, + 1, + 1.644, + 0, + 1.767, + 0, + 1, + 2.033, + 0, + 2.3, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.289, + 1, + 0.411, + 0, + 0.533, + 0, + 1, + 0.622, + 0, + 0.711, + 0, + 0.8, + 0, + 1, + 1, + 0, + 1.2, + 0, + 1.4, + 0, + 1, + 1.522, + 0, + 1.644, + 1, + 1.767, + 1, + 1, + 2.033, + 1, + 2.3, + 1, + 2.567, + 1, + 1, + 2.722, + 1, + 2.878, + 0, + 3.033, + 0, + 1, + 3.189, + 0, + 3.344, + 0, + 3.5, + 0, + 1, + 3.656, + 0, + 3.811, + 1, + 3.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.289, + 0, + 0.411, + 0, + 0.533, + 0, + 1, + 0.622, + 0, + 0.711, + 1, + 0.8, + 1, + 1, + 1, + 1, + 1.2, + 1, + 1.4, + 1, + 1, + 1.522, + 1, + 1.644, + 0, + 1.767, + 0, + 1, + 2.033, + 0, + 2.3, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 2.722, + 0, + 2.878, + -0.5, + 3.033, + -0.5, + 1, + 3.344, + -0.5, + 3.656, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 2.722, + 0, + 2.878, + -0.5, + 3.033, + -0.5, + 1, + 3.344, + -0.5, + 3.656, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 2.722, + 0, + 2.878, + -0.4, + 3.033, + -0.4, + 1, + 3.344, + -0.4, + 3.656, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 2.722, + 0, + 2.878, + 0, + 3.033, + 0, + 1, + 3.344, + 0, + 3.656, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 2.722, + 0, + 2.878, + 0, + 3.033, + 0, + 1, + 3.344, + 0, + 3.656, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 2.722, + 0, + 2.878, + -0.3, + 3.033, + -0.3, + 1, + 3.344, + -0.3, + 3.656, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 2.722, + 0, + 2.878, + -0.3, + 3.033, + -0.3, + 1, + 3.344, + -0.3, + 3.656, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.778, + 1, + 1.389, + 1, + 2, + 1, + 1, + 2.189, + 1, + 2.378, + 1, + 2.567, + 1, + 1, + 3.033, + 1, + 3.5, + 1, + 3.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.856, + 0, + 1.711, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.578, + 0, + 0.989, + 0, + 1.4, + 0, + 1, + 1.633, + 0, + 1.867, + -2.877, + 2.1, + -6, + 1, + 2.289, + -8.528, + 2.478, + -9, + 2.667, + -9, + 1, + 3.1, + -9, + 3.533, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.289, + 0, + 0.411, + -10, + 0.533, + -10, + 1, + 0.622, + -10, + 0.711, + 10, + 0.8, + 10, + 1, + 0.9, + 10, + 1, + -10, + 1.1, + -10, + 1, + 1.2, + -10, + 1.3, + 10, + 1.4, + 10, + 1, + 1.6, + 10, + 1.8, + -7, + 2, + -7, + 1, + 2.189, + -7, + 2.378, + -7, + 2.567, + -7, + 1, + 2.722, + -7, + 2.878, + -6.928, + 3.033, + -4, + 1, + 3.189, + -1.072, + 3.344, + 5, + 3.5, + 5, + 1, + 3.656, + 5, + 3.811, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.289, + 0, + 0.411, + 0, + 0.533, + 0, + 1, + 0.622, + 0, + 0.711, + 10, + 0.8, + 10, + 1, + 1, + 10, + 1.2, + -10, + 1.4, + -10, + 1, + 1.567, + -10, + 1.733, + 6, + 1.9, + 6, + 1, + 2.089, + 6, + 2.278, + 6, + 2.467, + 6, + 1, + 2.656, + 6, + 2.844, + 9, + 3.033, + 9, + 1, + 3.189, + 9, + 3.344, + -6, + 3.5, + -6, + 1, + 3.656, + -6, + 3.811, + 1, + 3.967, + 1, + 1, + 4.144, + 1, + 4.322, + 0, + 4.5, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 2.722, + 0, + 2.878, + 4, + 3.033, + 4, + 1, + 3.189, + 4, + 3.344, + -1, + 3.5, + -1, + 1, + 3.656, + -1, + 3.811, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.856, + 0, + 1.711, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0.5, + 1, + 0.056, + 0.5, + 0.111, + 0.5, + 0.167, + 0.5, + 1, + 0.289, + 0.5, + 0.411, + 0.6, + 0.533, + 0.6, + 1, + 0.622, + 0.6, + 0.711, + 0, + 0.8, + 0, + 1, + 0.9, + 0, + 1, + 0.57, + 1.1, + 0.57, + 1, + 1.2, + 0.57, + 1.3, + 0, + 1.4, + 0, + 1, + 1.6, + 0, + 1.8, + 0.5, + 2, + 0.5, + 1, + 2.189, + 0.5, + 2.378, + 0.5, + 2.567, + 0.5, + 1, + 2.722, + 0.5, + 2.878, + 0.489, + 3.033, + 0.4, + 1, + 3.189, + 0.311, + 3.344, + 0.2, + 3.5, + 0.2, + 1, + 3.656, + 0.2, + 3.811, + 1, + 3.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0.5, + 1, + 0.056, + 0.5, + 0.111, + 0.5, + 0.167, + 0.5, + 1, + 0.289, + 0.5, + 0.411, + 0.6, + 0.533, + 0.6, + 1, + 0.622, + 0.6, + 0.711, + 0, + 0.8, + 0, + 1, + 0.9, + 0, + 1, + 0.56, + 1.1, + 0.56, + 1, + 1.2, + 0.56, + 1.3, + 0, + 1.4, + 0, + 1, + 1.6, + 0, + 1.8, + 0.5, + 2, + 0.5, + 1, + 2.189, + 0.5, + 2.378, + 0.5, + 2.567, + 0.5, + 1, + 2.722, + 0.5, + 2.878, + 0.489, + 3.033, + 0.4, + 1, + 3.189, + 0.311, + 3.344, + 0.2, + 3.5, + 0.2, + 1, + 3.656, + 0.2, + 3.811, + 1, + 3.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 5, + 3.967, + 5, + 0, + 5, + 5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.378, + 0, + 0.589, + 0, + 0.8, + 0, + 1, + 1.389, + 0, + 1.978, + 0, + 2.567, + 0, + 1, + 3.111, + 0, + 3.656, + 0, + 4.2, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.289, + 0, + 0.411, + 0.887, + 0.533, + 0.963, + 1, + 0.622, + 1.018, + 0.711, + 1, + 0.8, + 1, + 1, + 0.844, + 1, + 0.889, + -0.593, + 0.933, + -0.684, + 1, + 0.967, + -0.752, + 1, + -0.73, + 1.033, + -0.73, + 1, + 1.111, + -0.73, + 1.189, + 0.874, + 1.267, + 0.969, + 1, + 1.311, + 1.023, + 1.356, + 1, + 1.4, + 1, + 1, + 1.511, + 1, + 1.622, + -0.566, + 1.733, + -0.566, + 1, + 1.822, + -0.566, + 1.911, + -0.1, + 2, + -0.1, + 1, + 2.189, + -0.1, + 2.378, + -0.1, + 2.567, + -0.1, + 1, + 2.722, + -0.1, + 2.878, + -0.8, + 3.033, + -0.8, + 1, + 3.189, + -0.8, + 3.344, + -0.4, + 3.5, + -0.4, + 1, + 3.578, + -0.4, + 3.656, + -1, + 3.733, + -1, + 1, + 3.889, + -1, + 4.044, + -0.1, + 4.2, + -0.1, + 0, + 5, + -0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.378, + 0, + 0.589, + 0, + 0.8, + 0, + 1, + 1.389, + 0, + 1.978, + 0, + 2.567, + 0, + 1, + 3.111, + 0, + 3.656, + 0, + 4.2, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.289, + 0, + 0.411, + -1, + 0.533, + -1, + 1, + 0.622, + -1, + 0.711, + -0.592, + 0.8, + 0, + 1, + 0.844, + 0.296, + 0.889, + 0.664, + 0.933, + 0.725, + 1, + 0.978, + 0.786, + 1.022, + 0.778, + 1.067, + 0.778, + 1, + 1.133, + 0.778, + 1.2, + -0.873, + 1.267, + -0.962, + 1, + 1.311, + -1.022, + 1.356, + -1, + 1.4, + -1, + 1, + 1.5, + -1, + 1.6, + 0.665, + 1.7, + 0.665, + 1, + 1.8, + 0.665, + 1.9, + 0.1, + 2, + 0.1, + 1, + 2.189, + 0.1, + 2.378, + 0.1, + 2.567, + 0.1, + 1, + 2.722, + 0.1, + 2.878, + 1, + 3.033, + 1, + 1, + 3.189, + 1, + 3.344, + 0.7, + 3.5, + 0.7, + 1, + 3.578, + 0.7, + 3.656, + 1, + 3.733, + 1, + 1, + 3.889, + 1, + 4.044, + 0.1, + 4.2, + 0.1, + 0, + 5, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.778, + 0, + 1.389, + 0, + 2, + 0, + 1, + 2.189, + 0, + 2.378, + 0, + 2.567, + 0, + 1, + 3.033, + 0, + 3.5, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 2.667, + 0, + 2, + 4.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 2.667, + 0, + 2, + 4.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 2.667, + 1, + 2, + 4.967, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 2.667, + 0, + 2, + 4.967, + 0, + 0, + 5, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m22.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 5.033, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 410, + "TotalPointCount": 1085, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.411, + 1, + 2.822, + 1, + 4.233, + 1, + 1, + 4.489, + 1, + 4.744, + 1, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.122, + 0, + 1.244, + -2, + 1.367, + -2, + 1, + 1.433, + -2, + 1.5, + -2.016, + 1.567, + -1.097, + 1, + 1.644, + -0.026, + 1.722, + 2, + 1.8, + 2, + 1, + 2.033, + 2, + 2.267, + 0, + 2.5, + 0, + 1, + 2.667, + 0, + 2.833, + 0, + 3, + 0, + 1, + 3.211, + 0, + 3.422, + -17, + 3.633, + -17, + 1, + 3.833, + -17, + 4.033, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.278, + 0, + 0.556, + 0, + 0.833, + 0, + 1, + 0.878, + 0, + 0.922, + -2, + 0.967, + -2, + 1, + 1.044, + -2, + 1.122, + 12, + 1.2, + 12, + 1, + 1.267, + 12, + 1.333, + 12.003, + 1.4, + 5.456, + 1, + 1.478, + -2.182, + 1.556, + -17, + 1.633, + -17, + 1, + 1.922, + -17, + 2.211, + -15, + 2.5, + -15, + 1, + 2.689, + -15, + 2.878, + -15, + 3.067, + -15, + 1, + 3.278, + -15, + 3.489, + -8.561, + 3.7, + 5, + 1, + 3.8, + 11.424, + 3.9, + 15, + 4, + 15, + 1, + 4.122, + 15, + 4.244, + 0, + 4.367, + 0, + 1, + 4.578, + 0, + 4.789, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.222, + 0, + 1.344, + -17, + 1.467, + -17, + 1, + 1.533, + -17, + 1.6, + -16.684, + 1.667, + -8.077, + 1, + 1.744, + 1.963, + 1.822, + 18, + 1.9, + 18, + 1, + 2.1, + 18, + 2.3, + 15, + 2.5, + 15, + 1, + 2.722, + 15, + 2.944, + 15, + 3.167, + 15, + 1, + 3.378, + 15, + 3.589, + -21, + 3.8, + -21, + 1, + 4.122, + -21, + 4.444, + 0, + 4.767, + 0, + 1, + 4.844, + 0, + 4.922, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0.5, + 1.8, + 0.5, + 1, + 2.2, + 0.5, + 2.6, + 0.5, + 3, + 0.5, + 1, + 3.411, + 0.5, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.122, + 1, + 1.244, + 1.9, + 1.367, + 1.9, + 1, + 1.433, + 1.9, + 1.5, + 1.913, + 1.567, + 1.471, + 1, + 1.644, + 0.956, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.067, + 0, + 3.133, + 0, + 3.2, + 0, + 1, + 3.267, + 0, + 3.333, + -0.041, + 3.4, + 0.2, + 1, + 3.478, + 0.481, + 3.556, + 1.6, + 3.633, + 1.6, + 1, + 3.833, + 1.6, + 4.033, + 1, + 4.233, + 1, + 1, + 4.489, + 1, + 4.744, + 1, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0.213, + 1.567, + 0.684, + 1, + 1.644, + 0.877, + 1.722, + 1, + 1.8, + 1, + 1, + 2.2, + 1, + 2.6, + 1, + 3, + 1, + 1, + 3.067, + 1, + 3.133, + 1, + 3.2, + 1, + 1, + 3.267, + 1, + 3.333, + 0, + 3.4, + 0, + 1, + 3.478, + 0, + 3.556, + 0, + 3.633, + 0, + 1, + 3.833, + 0, + 4.033, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.122, + 1, + 1.244, + 1.9, + 1.367, + 1.9, + 1, + 1.433, + 1.9, + 1.5, + 1.913, + 1.567, + 1.471, + 1, + 1.644, + 0.956, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.067, + 0, + 3.133, + 0, + 3.2, + 0, + 1, + 3.267, + 0, + 3.333, + -0.041, + 3.4, + 0.2, + 1, + 3.478, + 0.481, + 3.556, + 1.6, + 3.633, + 1.6, + 1, + 3.833, + 1.6, + 4.033, + 1, + 4.233, + 1, + 1, + 4.489, + 1, + 4.744, + 1, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0.213, + 1.567, + 0.684, + 1, + 1.644, + 0.877, + 1.722, + 1, + 1.8, + 1, + 1, + 2.2, + 1, + 2.6, + 1, + 3, + 1, + 1, + 3.067, + 1, + 3.133, + 1, + 3.2, + 1, + 1, + 3.267, + 1, + 3.333, + 0, + 3.4, + 0, + 1, + 3.478, + 0, + 3.556, + 0, + 3.633, + 0, + 1, + 3.833, + 0, + 4.033, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0.106, + 1.567, + 0.342, + 1, + 1.644, + 0.439, + 1.722, + 0.5, + 1.8, + 0.5, + 1, + 2.2, + 0.5, + 2.6, + 0.5, + 3, + 0.5, + 1, + 3.411, + 0.5, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.122, + 0, + 1.244, + 0.3, + 1.367, + 0.3, + 1, + 1.433, + 0.3, + 1.5, + 0.302, + 1.567, + 0.232, + 1, + 1.644, + 0.151, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.067, + 0, + 3.133, + -0.2, + 3.2, + -0.2, + 1, + 3.344, + -0.2, + 3.489, + 0.5, + 3.633, + 0.5, + 1, + 3.833, + 0.5, + 4.033, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.122, + 0, + 1.244, + 0.062, + 1.367, + 0.2, + 1, + 1.433, + 0.275, + 1.5, + 0.32, + 1.567, + 0.439, + 1, + 1.644, + 0.578, + 1.722, + 0.8, + 1.8, + 0.8, + 1, + 2.2, + 0.8, + 2.6, + 0.8, + 3, + 0.8, + 1, + 3.067, + 0.8, + 3.133, + -0.6, + 3.2, + -0.6, + 1, + 3.344, + -0.6, + 3.489, + 0, + 3.633, + 0, + 1, + 3.833, + 0, + 4.033, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0.205, + 1.567, + 0.205, + 1, + 1.644, + 0.205, + 1.722, + -0.2, + 1.8, + -0.2, + 1, + 2.2, + -0.2, + 2.6, + -0.2, + 3, + -0.2, + 1, + 3.411, + -0.2, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0.205, + 1.567, + 0.205, + 1, + 1.644, + 0.205, + 1.722, + -0.2, + 1.8, + -0.2, + 1, + 2.2, + -0.2, + 2.6, + -0.2, + 3, + -0.2, + 1, + 3.411, + -0.2, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0.043, + 1.567, + 0.137, + 1, + 1.644, + 0.175, + 1.722, + 0.2, + 1.8, + 0.2, + 1, + 2.2, + 0.2, + 2.6, + 0.2, + 3, + 0.2, + 1, + 3.411, + 0.2, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0.043, + 1.567, + 0.137, + 1, + 1.644, + 0.175, + 1.722, + 0.2, + 1.8, + 0.2, + 1, + 2.2, + 0.2, + 2.6, + 0.2, + 3, + 0.2, + 1, + 3.411, + 0.2, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0.043, + 1.567, + 0.137, + 1, + 1.644, + 0.175, + 1.722, + 0.2, + 1.8, + 0.2, + 1, + 2.2, + 0.2, + 2.6, + 0.2, + 3, + 0.2, + 1, + 3.411, + 0.2, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0.043, + 1.567, + 0.137, + 1, + 1.644, + 0.175, + 1.722, + 0.2, + 1.8, + 0.2, + 1, + 2.2, + 0.2, + 2.6, + 0.2, + 3, + 0.2, + 1, + 3.411, + 0.2, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0.043, + 1.567, + 0.137, + 1, + 1.644, + 0.175, + 1.722, + 0.2, + 1.8, + 0.2, + 1, + 2.2, + 0.2, + 2.6, + 0.2, + 3, + 0.2, + 1, + 3.411, + 0.2, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0.043, + 1.567, + 0.137, + 1, + 1.644, + 0.175, + 1.722, + 0.2, + 1.8, + 0.2, + 1, + 2.2, + 0.2, + 2.6, + 0.2, + 3, + 0.2, + 1, + 3.411, + 0.2, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.189, + 1, + 1.378, + 1, + 1.567, + 1, + 1, + 1.644, + 1, + 1.722, + 1, + 1.8, + 1, + 1, + 2.2, + 1, + 2.6, + 1, + 3, + 1, + 1, + 3.411, + 1, + 3.822, + 1, + 4.233, + 1, + 1, + 4.489, + 1, + 4.744, + 1, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.122, + 0, + 1.244, + 4, + 1.367, + 4, + 1, + 1.433, + 4, + 1.5, + 4.01, + 1.567, + 2.646, + 1, + 1.644, + 1.055, + 1.722, + -2, + 1.8, + -2, + 1, + 2.033, + -2, + 2.267, + -1, + 2.5, + -1, + 1, + 2.667, + -1, + 2.833, + -1, + 3, + -1, + 1, + 3.211, + -1, + 3.422, + -7, + 3.633, + -7, + 1, + 3.833, + -7, + 4.033, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.122, + 0, + 1.244, + 10, + 1.367, + 10, + 1, + 1.433, + 10, + 1.5, + 9.987, + 1.567, + 7.518, + 1, + 1.644, + 4.637, + 1.722, + -1, + 1.8, + -1, + 1, + 2.033, + -1, + 2.267, + 0, + 2.5, + 0, + 1, + 2.667, + 0, + 2.833, + 0, + 3, + 0, + 1, + 3.211, + 0, + 3.422, + 8, + 3.633, + 8, + 1, + 3.833, + 8, + 4.033, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.122, + 0, + 1.244, + -8, + 1.367, + -8, + 1, + 1.433, + -8, + 1.5, + -8, + 1.567, + -6.195, + 1, + 1.644, + -4.088, + 1.722, + 0, + 1.8, + 0, + 1, + 2.033, + 0, + 2.267, + -1, + 2.5, + -1, + 1, + 2.667, + -1, + 2.833, + -1, + 3, + -1, + 1, + 3.211, + -1, + 3.422, + -5, + 3.633, + -5, + 1, + 3.833, + -5, + 4.033, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.122, + 0, + 1.244, + -5, + 1.367, + -5, + 1, + 1.433, + -5, + 1.5, + -4.991, + 1.567, + -2.743, + 1, + 1.644, + -0.121, + 1.722, + 5, + 1.8, + 5, + 1, + 2.033, + 5, + 2.267, + 4, + 2.5, + 4, + 1, + 2.667, + 4, + 2.833, + 4, + 3, + 4, + 1, + 3.211, + 4, + 3.422, + -5, + 3.633, + -5, + 1, + 3.833, + -5, + 4.033, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.122, + 1, + 1.244, + 0.725, + 1.367, + 0.3, + 1, + 1.433, + 0.068, + 1.5, + -0.024, + 1.567, + -0.297, + 1, + 1.644, + -0.616, + 1.722, + -1, + 1.8, + -1, + 1, + 2.033, + -1, + 2.267, + -0.9, + 2.5, + -0.9, + 1, + 2.667, + -0.9, + 2.833, + -0.9, + 3, + -0.9, + 1, + 3.211, + -0.9, + 3.422, + -0.173, + 3.633, + 0.4, + 1, + 3.833, + 0.943, + 4.033, + 1, + 4.233, + 1, + 1, + 4.489, + 1, + 4.744, + 1, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.122, + 1, + 1.244, + 0.725, + 1.367, + 0.3, + 1, + 1.433, + 0.068, + 1.5, + -0.024, + 1.567, + -0.297, + 1, + 1.644, + -0.616, + 1.722, + -1, + 1.8, + -1, + 1, + 2.033, + -1, + 2.267, + -0.9, + 2.5, + -0.9, + 1, + 2.667, + -0.9, + 2.833, + -0.9, + 3, + -0.9, + 1, + 3.211, + -0.9, + 3.422, + -0.173, + 3.633, + 0.4, + 1, + 3.833, + 0.943, + 4.033, + 1, + 4.233, + 1, + 1, + 4.489, + 1, + 4.744, + 1, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 5, + 1, + 0.333, + 5, + 0.667, + 5, + 1, + 5, + 1, + 1.189, + 5, + 1.378, + 5, + 1.567, + 5, + 1, + 1.644, + 5, + 1.722, + 5, + 1.8, + 5, + 1, + 2.2, + 5, + 2.6, + 5, + 3, + 5, + 1, + 3.411, + 5, + 3.822, + 5, + 4.233, + 5, + 1, + 4.489, + 5, + 4.744, + 5, + 5, + 5, + 0, + 5.033, + 5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + -0.1, + 1, + 0.333, + -0.1, + 0.667, + -0.1, + 1, + -0.1, + 1, + 1.122, + -0.1, + 1.244, + -0.7, + 1.367, + -0.7, + 1, + 1.433, + -0.7, + 1.5, + -0.705, + 1.567, + -0.542, + 1, + 1.644, + -0.352, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + -0.1, + 4.233, + -0.1, + 1, + 4.489, + -0.1, + 4.744, + -0.1, + 5, + -0.1, + 0, + 5.033, + -0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0.1, + 1, + 0.333, + 0.1, + 0.667, + 0.1, + 1, + 0.1, + 1, + 1.122, + 0.1, + 1.244, + 0.7, + 1.367, + 0.7, + 1, + 1.433, + 0.7, + 1.5, + 0.705, + 1.567, + 0.542, + 1, + 1.644, + 0.352, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0.1, + 4.233, + 0.1, + 1, + 4.489, + 0.1, + 4.744, + 0.1, + 5, + 0.1, + 0, + 5.033, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.189, + 0, + 1.378, + 0, + 1.567, + 0, + 1, + 1.644, + 0, + 1.722, + 0, + 1.8, + 0, + 1, + 2.2, + 0, + 2.6, + 0, + 3, + 0, + 1, + 3.411, + 0, + 3.822, + 0, + 4.233, + 0, + 1, + 4.489, + 0, + 4.744, + 0, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 4.233, + 0, + 2, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 4.233, + 0, + 2, + 5, + 0, + 0, + 5.033, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 4.233, + 1, + 2, + 5, + 1, + 0, + 5.033, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 4.233, + 0, + 2, + 5, + 0, + 0, + 5.033, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m23.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 221, + "TotalPointCount": 558, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.322, + 1, + 2.644, + 1, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 9, + 1, + 0.333, + 9, + 0.667, + 9, + 1, + 9, + 1, + 1.156, + 9, + 1.311, + -3, + 1.467, + -3, + 1, + 1.722, + -3, + 1.978, + -2.724, + 2.233, + -2, + 1, + 2.444, + -1.402, + 2.656, + -1, + 2.867, + -1, + 0, + 4, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + -6, + 1, + 0.289, + -6, + 0.578, + -6, + 0.867, + -6, + 1, + 0.944, + -6, + 1.022, + -8, + 1.1, + -8, + 1, + 1.256, + -8, + 1.411, + -1.51, + 1.567, + 5, + 1, + 1.656, + 8.72, + 1.744, + 9, + 1.833, + 9, + 1, + 2, + 9, + 2.167, + -7, + 2.333, + -7, + 1, + 2.544, + -7, + 2.756, + -6, + 2.967, + -6, + 0, + 4, + -6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + -16, + 1, + 0.333, + -16, + 0.667, + -16, + 1, + -16, + 1, + 1.156, + -16, + 1.311, + -26, + 1.467, + -26, + 1, + 1.722, + -26, + 1.978, + 27, + 2.233, + 27, + 1, + 2.444, + 27, + 2.656, + 24, + 2.867, + 24, + 0, + 4, + 24 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.156, + 1, + 1.311, + 1.898, + 1.467, + 1.898, + 1, + 1.667, + 1.898, + 1.867, + 1, + 2.067, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.156, + 1, + 1.311, + 1.898, + 1.467, + 1.898, + 1, + 1.667, + 1.898, + 1.867, + 1, + 2.067, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0.1, + 2.067, + 0.1, + 0, + 4, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + -0.3, + 1, + 0.333, + -0.3, + 0.667, + -0.3, + 1, + -0.3, + 1, + 1.156, + -0.3, + 1.311, + 0.2, + 1.467, + 0.2, + 1, + 1.667, + 0.2, + 1.867, + 0.1, + 2.067, + 0.1, + 0, + 4, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0.5, + 1, + 0.333, + 0.5, + 0.667, + 0.5, + 1, + 0.5, + 1, + 1.156, + 0.5, + 1.311, + 0.2, + 1.467, + 0.2, + 1, + 1.667, + 0.2, + 1.867, + 0.3, + 2.067, + 0.3, + 0, + 4, + 0.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0.399, + 1.467, + 0.399, + 1, + 1.667, + 0.399, + 1.867, + 0.2, + 2.067, + 0.2, + 0, + 4, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0.399, + 1.467, + 0.399, + 1, + 1.667, + 0.399, + 1.867, + 0.2, + 2.067, + 0.2, + 0, + 4, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0.699, + 1.467, + 0.699, + 1, + 1.667, + 0.699, + 1.867, + 0.5, + 2.067, + 0.5, + 0, + 4, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0.699, + 1.467, + 0.699, + 1, + 1.667, + 0.699, + 1.867, + 0.5, + 2.067, + 0.5, + 0, + 4, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0.698, + 1.467, + 0.698, + 1, + 1.667, + 0.698, + 1.867, + 0.4, + 2.067, + 0.4, + 0, + 4, + 0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0.698, + 1.467, + 0.698, + 1, + 1.667, + 0.698, + 1.867, + 0.4, + 2.067, + 0.4, + 0, + 4, + 0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0.599, + 1.467, + 0.599, + 1, + 1.667, + 0.599, + 1.867, + 0.4, + 2.067, + 0.4, + 0, + 4, + 0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0.599, + 1.467, + 0.599, + 1, + 1.667, + 0.599, + 1.867, + 0.4, + 2.067, + 0.4, + 0, + 4, + 0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.156, + 1, + 1.311, + 0.603, + 1.467, + 0.603, + 1, + 1.667, + 0.603, + 1.867, + 1, + 2.067, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0.1, + 1.467, + 0.1, + 1, + 1.667, + 0.1, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 9, + 1, + 0.333, + 9, + 0.667, + 9, + 1, + 9, + 1, + 1.156, + 9, + 1.311, + 7.216, + 1.467, + 2.03, + 1, + 1.667, + -4.639, + 1.867, + -9, + 2.067, + -9, + 0, + 4, + -9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.344, + 0, + 0.689, + 0, + 1.033, + 0, + 1, + 1.189, + 0, + 1.344, + 6, + 1.5, + 6, + 1, + 1.7, + 6, + 1.9, + -1, + 2.1, + -1, + 1, + 2.456, + -1, + 2.811, + 0, + 3.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.289, + 0, + 0.578, + 0, + 0.867, + 0, + 1, + 1.022, + 0, + 1.178, + -4.928, + 1.333, + -4.928, + 1, + 1.533, + -4.928, + 1.733, + 5, + 1.933, + 5, + 1, + 2.289, + 5, + 2.644, + 4, + 3, + 4, + 0, + 4, + 4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.256, + 0, + 1.411, + -3.95, + 1.567, + -3.95, + 1, + 1.767, + -3.95, + 1.967, + 3, + 2.167, + 3, + 1, + 2.522, + 3, + 2.878, + 2, + 3.233, + 2, + 0, + 4, + 2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.156, + 1, + 1.311, + 0.5, + 1.467, + 0.5, + 1, + 1.667, + 0.5, + 1.867, + 1, + 2.067, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.156, + 1, + 1.311, + 0.5, + 1.467, + 0.5, + 1, + 1.667, + 0.5, + 1.867, + 1, + 2.067, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 5, + 1, + 0.333, + 5, + 0.667, + 5, + 1, + 5, + 1, + 1.156, + 5, + 1.311, + 5, + 1.467, + 5, + 1, + 1.667, + 5, + 1.867, + 5, + 2.067, + 5, + 0, + 4, + 5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 5, + 1, + 0.333, + 5, + 0.667, + 5, + 1, + 5, + 1, + 1.156, + 5, + 1.311, + 5, + 1.467, + 5, + 1, + 1.667, + 5, + 1.867, + 5, + 2.067, + 5, + 0, + 4, + 5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + -0.2, + 1, + 0.333, + -0.2, + 0.667, + -0.2, + 1, + -0.2, + 1, + 1.078, + -0.2, + 1.156, + 0.7, + 1.233, + 0.7, + 1, + 1.311, + 0.7, + 1.389, + 0.508, + 1.467, + 0.069, + 1, + 1.578, + -0.557, + 1.689, + -0.9, + 1.8, + -0.9, + 1, + 1.889, + -0.9, + 1.978, + -0.1, + 2.067, + -0.1, + 0, + 4, + -0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + 0.3, + 1, + 0.333, + 0.3, + 0.667, + 0.3, + 1, + 0.3, + 1, + 1.078, + 0.3, + 1.156, + -1, + 1.233, + -1, + 1, + 1.311, + -1, + 1.389, + -0.785, + 1.467, + -0.239, + 1, + 1.578, + 0.542, + 1.689, + 1, + 1.8, + 1, + 1, + 1.889, + 1, + 1.978, + 0.1, + 2.067, + 0.1, + 0, + 4, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.156, + 0, + 1.311, + 0, + 1.467, + 0, + 1, + 1.667, + 0, + 1.867, + 0, + 2.067, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 3.967, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 3.967, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 3.967, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 3.967, + 0, + 0, + 4, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m24.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 3.4, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 240, + "TotalPointCount": 615, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.122, + 1, + 2.244, + 1, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 3, + 1.3, + 3, + 1, + 1.422, + 3, + 1.544, + 3.072, + 1.667, + 1.363, + 1, + 1.756, + 0.121, + 1.844, + -30, + 1.933, + -30, + 1, + 2.078, + -30, + 2.222, + -28, + 2.367, + -28, + 0, + 3.4, + -28 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 0, + 0.933, + 0, + 1, + 0.978, + 0, + 1.022, + -1, + 1.067, + -1, + 1, + 1.178, + -1, + 1.289, + 23, + 1.4, + 23, + 1, + 1.522, + 23, + 1.644, + 23.023, + 1.767, + 19.188, + 1, + 1.856, + 16.4, + 1.944, + -30, + 2.033, + -30, + 1, + 2.178, + -30, + 2.322, + -28, + 2.467, + -28, + 0, + 3.4, + -28 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + -1.536, + 1.667, + -7.828, + 1, + 1.756, + -10.345, + 1.844, + -17, + 1.933, + -17, + 1, + 2.078, + -17, + 2.222, + -16, + 2.367, + -16, + 0, + 3.4, + -16 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0.051, + 1.667, + 0.255, + 1, + 1.756, + 0.337, + 1.844, + 0.5, + 1.933, + 0.5, + 0, + 3.4, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.1, + 1, + 1.2, + 1.9, + 1.3, + 1.9, + 1, + 1.422, + 1.9, + 1.544, + 1.908, + 1.667, + 1.828, + 1, + 1.756, + 1.77, + 1.844, + 0.8, + 1.933, + 0.8, + 0, + 3.4, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.1, + 1, + 1.2, + 1.9, + 1.3, + 1.9, + 1, + 1.422, + 1.9, + 1.544, + 1.908, + 1.667, + 1.828, + 1, + 1.756, + 1.77, + 1.844, + 0.8, + 1.933, + 0.8, + 0, + 3.4, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + -0.1, + 1.3, + -0.1, + 1, + 1.422, + -0.1, + 1.544, + -0.051, + 1.667, + -0.051, + 1, + 1.756, + -0.051, + 1.844, + -1, + 1.933, + -1, + 0, + 3.4, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.422, + 0, + 1.544, + 0.009, + 1.667, + 0.074, + 1, + 1.756, + 0.121, + 1.844, + 0.2, + 1.933, + 0.2, + 0, + 3.4, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.3, + 1.3, + 0.3, + 1, + 1.422, + 0.3, + 1.544, + 0.308, + 1.667, + 0.228, + 1, + 1.756, + 0.17, + 1.844, + -0.7, + 1.933, + -0.7, + 0, + 3.4, + -0.7 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.3, + 1.3, + 0.3, + 1, + 1.422, + 0.3, + 1.544, + 0.308, + 1.667, + 0.228, + 1, + 1.756, + 0.17, + 1.844, + -0.7, + 1.933, + -0.7, + 0, + 3.4, + -0.7 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.3, + 1.3, + 0.3, + 1, + 1.422, + 0.3, + 1.544, + 0.309, + 1.667, + 0.21, + 1, + 1.756, + 0.138, + 1.844, + -0.8, + 1.933, + -0.8, + 0, + 3.4, + -0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.3, + 1.3, + 0.3, + 1, + 1.422, + 0.3, + 1.544, + 0.309, + 1.667, + 0.21, + 1, + 1.756, + 0.138, + 1.844, + -0.8, + 1.933, + -0.8, + 0, + 3.4, + -0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.3, + 1.3, + 0.3, + 1, + 1.422, + 0.3, + 1.544, + 0.309, + 1.667, + 0.2, + 1, + 1.756, + 0.121, + 1.844, + -0.8, + 1.933, + -0.8, + 0, + 3.4, + -0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.3, + 1.3, + 0.3, + 1, + 1.422, + 0.3, + 1.544, + 0.309, + 1.667, + 0.2, + 1, + 1.756, + 0.121, + 1.844, + -0.8, + 1.933, + -0.8, + 0, + 3.4, + -0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.2, + 1.3, + 0.2, + 1, + 1.422, + 0.2, + 1.544, + 0.14, + 1.667, + -0.2, + 1, + 1.756, + -0.448, + 1.844, + -0.8, + 1.933, + -0.8, + 0, + 3.4, + -0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.2, + 1.3, + 0.2, + 1, + 1.422, + 0.2, + 1.544, + 0.14, + 1.667, + -0.2, + 1, + 1.756, + -0.448, + 1.844, + -0.8, + 1.933, + -0.8, + 0, + 3.4, + -0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.1, + 1, + 1.2, + 0.268, + 1.3, + 0.1, + 1, + 1.422, + -0.105, + 1.544, + -0.093, + 1.667, + -0.288, + 1, + 1.756, + -0.43, + 1.844, + -1, + 1.933, + -1, + 0, + 3.4, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.1, + 1.3, + 0.1, + 1, + 1.422, + 0.1, + 1.544, + 0.101, + 1.667, + 0.092, + 1, + 1.756, + 0.085, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0, + 0.9, + 0, + 1, + 1, + 0, + 1.1, + 6, + 1.2, + 6, + 1, + 1.322, + 6, + 1.444, + 6.063, + 1.567, + 5.062, + 1, + 1.656, + 4.334, + 1.744, + -9, + 1.833, + -9, + 1, + 1.978, + -9, + 2.122, + -8, + 2.267, + -8, + 0, + 3.4, + -8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 6, + 1.3, + 6, + 1, + 1.422, + 6, + 1.544, + 6.057, + 1.667, + 5.249, + 1, + 1.756, + 4.662, + 1.844, + -6, + 1.933, + -6, + 1, + 2.078, + -6, + 2.222, + -5, + 2.367, + -5, + 0, + 3.4, + -5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.2, + 0, + 1.3, + 6, + 1.4, + 6, + 1, + 1.522, + 6, + 1.644, + 6.052, + 1.767, + 5.374, + 1, + 1.856, + 4.882, + 1.944, + -4, + 2.033, + -4, + 1, + 2.178, + -4, + 2.322, + -3, + 2.467, + -3, + 0, + 3.4, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + -0.511, + 1.667, + -2.763, + 1, + 1.756, + -3.664, + 1.844, + -6, + 1.933, + -6, + 1, + 2.078, + -6, + 2.222, + -5, + 2.367, + -5, + 0, + 3.4, + -5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.1, + 1, + 1.2, + 0.472, + 1.3, + 0.3, + 1, + 1.422, + 0.089, + 1.544, + 0.096, + 1.667, + -0.117, + 1, + 1.756, + -0.272, + 1.844, + -1, + 1.933, + -1, + 1, + 2.078, + -1, + 2.222, + -0.9, + 2.367, + -0.9, + 0, + 3.4, + -0.9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.1, + 1, + 1.2, + 0.472, + 1.3, + 0.3, + 1, + 1.422, + 0.089, + 1.544, + 0.096, + 1.667, + -0.117, + 1, + 1.756, + -0.272, + 1.844, + -1, + 1.933, + -1, + 1, + 2.078, + -1, + 2.222, + -0.9, + 2.367, + -0.9, + 0, + 3.4, + -0.9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 5, + 1, + 0.333, + 5, + 0.667, + 5, + 1, + 5, + 1, + 1.222, + 5, + 1.444, + 5, + 1.667, + 5, + 1, + 1.756, + 5, + 1.844, + 5, + 1.933, + 5, + 0, + 3.4, + 5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0.19, + 1, + 0.333, + 0.19, + 0.667, + 0.19, + 1, + 0.19, + 1, + 1.222, + 0.19, + 1.444, + 0.183, + 1.667, + 0.149, + 1, + 1.756, + 0.135, + 1.844, + 0.1, + 1.933, + 0.1, + 0, + 3.4, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + -0.17, + 1, + 0.333, + -0.17, + 0.667, + -0.17, + 1, + -0.17, + 1, + 1.222, + -0.17, + 1.444, + -0.123, + 1.667, + 0.092, + 1, + 1.756, + 0.179, + 1.844, + 0.4, + 1.933, + 0.4, + 0, + 3.4, + 0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 0, + 1.933, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 3.367, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 3.367, + 0, + 0, + 3.4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 3.367, + 1, + 0, + 3.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 3.367, + 0, + 0, + 3.4, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m25.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 10.467, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 233, + "TotalPointCount": 594, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.333, + 1, + 2.667, + 1, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.233, + 0, + 0.467, + -1, + 0.7, + -1, + 1, + 0.789, + -1, + 0.878, + 7, + 0.967, + 7, + 1, + 1.2, + 7, + 1.433, + 7, + 1.667, + 7, + 1, + 1.767, + 7, + 1.867, + -4, + 1.967, + -4, + 1, + 2.111, + -4, + 2.256, + 2, + 2.4, + 2, + 1, + 2.622, + 2, + 2.844, + 0, + 3.067, + 0, + 1, + 3.378, + 0, + 3.689, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + -21, + 1, + 0.122, + -21, + 0.244, + -21, + 0.367, + -21, + 1, + 0.567, + -21, + 0.767, + -12.835, + 0.967, + 0, + 1, + 1.2, + 14.974, + 1.433, + 21, + 1.667, + 21, + 1, + 2, + 21, + 2.333, + 0, + 2.667, + 0, + 1, + 3.111, + 0, + 3.556, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.233, + 1, + 0.467, + 1, + 0.7, + 1, + 1, + 0.744, + 1, + 0.789, + 0, + 0.833, + 0, + 1, + 0.878, + 0, + 0.922, + 2, + 0.967, + 2, + 1, + 0.989, + 2, + 1.011, + 1.9, + 1.033, + 1.9, + 1, + 1.267, + 1.9, + 1.5, + 1.9, + 1.733, + 1.9, + 1, + 1.789, + 1.9, + 1.844, + 0, + 1.9, + 0, + 1, + 1.978, + 0, + 2.056, + 0, + 2.133, + 0, + 1, + 2.2, + 0, + 2.267, + 1.1, + 2.333, + 1.1, + 1, + 2.356, + 1.1, + 2.378, + 1, + 2.4, + 1, + 1, + 2.933, + 1, + 3.467, + 1, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.233, + 1, + 0.467, + 1, + 0.7, + 1, + 1, + 0.744, + 1, + 0.789, + 0, + 0.833, + 0, + 1, + 0.878, + 0, + 0.922, + 2, + 0.967, + 2, + 1, + 0.989, + 2, + 1.011, + 1.9, + 1.033, + 1.9, + 1, + 1.267, + 1.9, + 1.5, + 1.9, + 1.733, + 1.9, + 1, + 1.789, + 1.9, + 1.844, + 0, + 1.9, + 0, + 1, + 1.978, + 0, + 2.056, + 0, + 2.133, + 0, + 1, + 2.2, + 0, + 2.267, + 1.1, + 2.333, + 1.1, + 1, + 2.356, + 1.1, + 2.378, + 1, + 2.4, + 1, + 1, + 2.933, + 1, + 3.467, + 1, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.233, + 0, + 0.467, + 0, + 0.7, + 0, + 1, + 0.789, + 0, + 0.878, + -0.49, + 0.967, + -0.49, + 1, + 1.6, + -0.49, + 2.233, + -0.49, + 2.867, + -0.49, + 1, + 3.244, + -0.49, + 3.622, + -0.49, + 4, + -0.49, + 0, + 10.467, + -0.49 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.233, + 0, + 0.467, + 0, + 0.7, + 0, + 1, + 0.811, + 0, + 0.922, + 0, + 1.033, + 0, + 1, + 1.267, + 0, + 1.5, + 0, + 1.733, + 0, + 1, + 1.956, + 0, + 2.178, + 0, + 2.4, + 0, + 1, + 2.933, + 0, + 3.467, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.233, + 0, + 0.467, + 0, + 0.7, + 0, + 1, + 0.744, + 0, + 0.789, + -0.1, + 0.833, + -0.1, + 1, + 0.878, + -0.1, + 0.922, + 0.1, + 0.967, + 0.1, + 1, + 0.989, + 0.1, + 1.011, + 0, + 1.033, + 0, + 1, + 1.267, + 0, + 1.5, + 0, + 1.733, + 0, + 1, + 1.789, + 0, + 1.844, + -0.1, + 1.9, + -0.1, + 1, + 1.978, + -0.1, + 2.056, + -0.1, + 2.133, + -0.1, + 1, + 2.2, + -0.1, + 2.267, + 0.1, + 2.333, + 0.1, + 1, + 2.356, + 0.1, + 2.378, + 0, + 2.4, + 0, + 1, + 2.933, + 0, + 3.467, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 0, + 0.733, + 0, + 1, + 0.767, + 0, + 0.8, + -0.2, + 0.833, + -0.2, + 1, + 0.878, + -0.2, + 0.922, + 0.555, + 0.967, + 0.56, + 1, + 1.222, + 0.591, + 1.478, + 0.6, + 1.733, + 0.6, + 1, + 1.789, + 0.6, + 1.844, + -0.1, + 1.9, + -0.1, + 1, + 1.978, + -0.1, + 2.056, + -0.1, + 2.133, + -0.1, + 1, + 2.222, + -0.1, + 2.311, + 0.3, + 2.4, + 0.3, + 1, + 2.933, + 0.3, + 3.467, + 0.3, + 4, + 0.3, + 0, + 10.467, + 0.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 0, + 0.733, + 0, + 1, + 0.767, + 0, + 0.8, + -0.2, + 0.833, + -0.2, + 1, + 0.878, + -0.2, + 0.922, + 0.61, + 0.967, + 0.61, + 1, + 1.222, + 0.61, + 1.478, + 0.608, + 1.733, + 0.6, + 1, + 1.789, + 0.598, + 1.844, + -0.1, + 1.9, + -0.1, + 1, + 1.978, + -0.1, + 2.056, + -0.1, + 2.133, + -0.1, + 1, + 2.222, + -0.1, + 2.311, + 0.3, + 2.4, + 0.3, + 1, + 2.933, + 0.3, + 3.467, + 0.3, + 4, + 0.3, + 0, + 10.467, + 0.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 0, + 0.733, + 0, + 1, + 0.811, + 0, + 0.889, + 0.3, + 0.967, + 0.3, + 1, + 1.222, + 0.3, + 1.478, + 0.3, + 1.733, + 0.3, + 1, + 1.956, + 0.3, + 2.178, + 0.2, + 2.4, + 0.2, + 1, + 2.933, + 0.2, + 3.467, + 0.2, + 4, + 0.2, + 0, + 10.467, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 0, + 0.733, + 0, + 1, + 0.811, + 0, + 0.889, + 0.3, + 0.967, + 0.3, + 1, + 1.222, + 0.3, + 1.478, + 0.3, + 1.733, + 0.3, + 1, + 1.956, + 0.3, + 2.178, + 0.2, + 2.4, + 0.2, + 1, + 2.933, + 0.2, + 3.467, + 0.2, + 4, + 0.2, + 0, + 10.467, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 0, + 0.733, + 0, + 1, + 0.811, + 0, + 0.889, + 0.4, + 0.967, + 0.4, + 1, + 1.222, + 0.4, + 1.478, + 0.4, + 1.733, + 0.4, + 1, + 1.956, + 0.4, + 2.178, + 0.5, + 2.4, + 0.5, + 1, + 2.933, + 0.5, + 3.467, + 0.5, + 4, + 0.5, + 0, + 10.467, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 0, + 0.733, + 0, + 1, + 0.811, + 0, + 0.889, + 0.4, + 0.967, + 0.4, + 1, + 1.222, + 0.4, + 1.478, + 0.4, + 1.733, + 0.4, + 1, + 1.956, + 0.4, + 2.178, + 0.5, + 2.4, + 0.5, + 1, + 2.933, + 0.5, + 3.467, + 0.5, + 4, + 0.5, + 0, + 10.467, + 0.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 0, + 0.733, + 0, + 1, + 0.811, + 0, + 0.889, + 0.4, + 0.967, + 0.4, + 1, + 1.222, + 0.4, + 1.478, + 0.4, + 1.733, + 0.4, + 1, + 1.956, + 0.4, + 2.178, + 0.3, + 2.4, + 0.3, + 1, + 2.933, + 0.3, + 3.467, + 0.3, + 4, + 0.3, + 0, + 10.467, + 0.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 0, + 0.733, + 0, + 1, + 0.811, + 0, + 0.889, + 0.4, + 0.967, + 0.4, + 1, + 1.222, + 0.4, + 1.478, + 0.4, + 1.733, + 0.4, + 1, + 1.956, + 0.4, + 2.178, + 0.3, + 2.4, + 0.3, + 1, + 2.933, + 0.3, + 3.467, + 0.3, + 4, + 0.3, + 0, + 10.467, + 0.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.244, + 1, + 0.489, + 1, + 0.733, + 1, + 1, + 0.811, + 1, + 0.889, + -1, + 0.967, + -1, + 1, + 1.222, + -1, + 1.478, + -1, + 1.733, + -1, + 1, + 1.833, + -1, + 1.933, + -0.594, + 2.033, + 0, + 1, + 2.156, + 0.726, + 2.278, + 1, + 2.4, + 1, + 1, + 2.933, + 1, + 3.467, + 1, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 0, + 0.933, + 0, + 1, + 1.222, + 0, + 1.511, + -6, + 1.8, + -6, + 1, + 2.122, + -6, + 2.444, + 0, + 2.767, + 0, + 1, + 3.178, + 0, + 3.589, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.233, + 0, + 0.467, + -1, + 0.7, + -1, + 1, + 0.789, + -1, + 0.878, + 1, + 0.967, + 1, + 1, + 1.056, + 1, + 1.144, + 0, + 1.233, + 0, + 1, + 2.156, + 0, + 3.078, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.322, + 0, + 0.644, + 0, + 0.967, + 0, + 1, + 1.244, + 0, + 1.522, + 4, + 1.8, + 4, + 1, + 2.167, + 4, + 2.533, + 0, + 2.9, + 0, + 1, + 3.267, + 0, + 3.633, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 6, + 1, + 1.189, + 6, + 2.378, + 6, + 3.567, + 6, + 1, + 3.711, + 6, + 3.856, + 6, + 4, + 6, + 0, + 10.467, + 6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + -1, + 1, + 0.344, + -1, + 0.689, + -1, + 1.033, + -1, + 1, + 1.189, + -1, + 1.344, + -0.9, + 1.5, + -0.9, + 1, + 1.6, + -0.9, + 1.7, + -1, + 1.8, + -1, + 1, + 2.389, + -1, + 2.978, + -1, + 3.567, + -1, + 1, + 3.711, + -1, + 3.856, + -1, + 4, + -1, + 0, + 10.467, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + -1, + 1, + 0.344, + -1, + 0.689, + -1, + 1.033, + -1, + 1, + 1.189, + -1, + 1.344, + -0.9, + 1.5, + -0.9, + 1, + 1.6, + -0.9, + 1.7, + -1, + 1.8, + -1, + 1, + 2.389, + -1, + 2.978, + -1, + 3.567, + -1, + 1, + 3.711, + -1, + 3.856, + -1, + 4, + -1, + 0, + 10.467, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 1, + 1, + 1.333, + 1, + 2.667, + 1, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + -1, + 1, + 1.333, + -1, + 2.667, + -1, + 4, + -1, + 0, + 10.467, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 4, + 0, + 0, + 10.467, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 4, + 1, + 0, + 10.467, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 4, + 0, + 0, + 10.467, + 0 + ] + } + ] +} +``` + +`Content/Samples/Haru/motions/haru_g_m26.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4.967, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 65, + "TotalSegmentCount": 328, + "TotalPointCount": 879, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 1.644, + 1, + 3.289, + 1, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "Model", + "Id": "EyeBlink", + "Segments": [ + 0, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "Model", + "Id": "LipSync", + "Segments": [ + 0, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + -5, + 1.3, + -5, + 1, + 1.489, + -5, + 1.678, + -4, + 1.867, + -4, + 1, + 2.244, + -4, + 2.622, + -4, + 3, + -4, + 1, + 3.144, + -4, + 3.289, + 2, + 3.433, + 2, + 1, + 3.744, + 2, + 4.056, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.367, + 0, + 0.733, + 0, + 1.1, + 0, + 1, + 1.144, + 0, + 1.189, + -13, + 1.233, + -13, + 1, + 1.289, + -13, + 1.344, + 11, + 1.4, + 11, + 1, + 1.589, + 11, + 1.778, + 6, + 1.967, + 6, + 1, + 2.333, + 6, + 2.7, + 6, + 3.067, + 6, + 1, + 3.211, + 6, + 3.356, + -22, + 3.5, + -22, + 1, + 3.656, + -22, + 3.811, + 12, + 3.967, + 12, + 1, + 4.122, + 12, + 4.278, + 0, + 4.433, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + -8, + 1.3, + -8, + 1, + 1.489, + -8, + 1.678, + -6, + 1.867, + -6, + 1, + 2.244, + -6, + 2.622, + -6, + 3, + -6, + 1, + 3.144, + -6, + 3.289, + -9, + 3.433, + -9, + 1, + 3.589, + -9, + 3.744, + 7, + 3.9, + 7, + 1, + 4.056, + 7, + 4.211, + -2, + 4.367, + -2, + 1, + 4.556, + -2, + 4.744, + 0, + 4.933, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTere", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.5, + 1.3, + 0.5, + 1, + 1.867, + 0.5, + 2.433, + 0.5, + 3, + 0.5, + 1, + 3.456, + 0.5, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFaceForm", + "Segments": [ + 0, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.044, + 1, + 1.089, + 0, + 1.133, + 0, + 1, + 1.189, + 0, + 1.244, + 1.7, + 1.3, + 1.7, + 1, + 1.522, + 1.7, + 1.744, + 1.7, + 1.967, + 1.7, + 1, + 2, + 1.7, + 2.033, + 0, + 2.067, + 0, + 1, + 2.078, + 0, + 2.089, + 0, + 2.1, + 0, + 1, + 2.133, + 0, + 2.167, + 1.7, + 2.2, + 1.7, + 1, + 2.211, + 1.7, + 2.222, + 1.7, + 2.233, + 1.7, + 1, + 2.267, + 1.7, + 2.3, + 0, + 2.333, + 0, + 1, + 2.344, + 0, + 2.356, + 0, + 2.367, + 0, + 1, + 2.433, + 0, + 2.5, + 1.7, + 2.567, + 1.7, + 1, + 2.711, + 1.7, + 2.856, + 1.7, + 3, + 1.7, + 1, + 3.144, + 1.7, + 3.289, + 0, + 3.433, + 0, + 1, + 3.589, + 0, + 3.744, + 0, + 3.9, + 0, + 1, + 4.056, + 0, + 4.211, + 1, + 4.367, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.044, + 1, + 1.089, + 0, + 1.133, + 0, + 1, + 1.189, + 0, + 1.244, + 1.7, + 1.3, + 1.7, + 1, + 1.522, + 1.7, + 1.744, + 1.7, + 1.967, + 1.7, + 1, + 2, + 1.7, + 2.033, + 0, + 2.067, + 0, + 1, + 2.078, + 0, + 2.089, + 0, + 2.1, + 0, + 1, + 2.133, + 0, + 2.167, + 1.7, + 2.2, + 1.7, + 1, + 2.211, + 1.7, + 2.222, + 1.7, + 2.233, + 1.7, + 1, + 2.267, + 1.7, + 2.3, + 0, + 2.333, + 0, + 1, + 2.344, + 0, + 2.356, + 0, + 2.367, + 0, + 1, + 2.433, + 0, + 2.5, + 1.7, + 2.567, + 1.7, + 1, + 2.711, + 1.7, + 2.856, + 1.7, + 3, + 1.7, + 1, + 3.144, + 1.7, + 3.289, + 0, + 3.433, + 0, + 1, + 3.589, + 0, + 3.744, + 0, + 3.9, + 0, + 1, + 4.056, + 0, + 4.211, + 1, + 4.367, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + -0.6, + 1.3, + -0.6, + 1, + 1.867, + -0.6, + 2.433, + -0.6, + 3, + -0.6, + 1, + 3.456, + -0.6, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTear", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.2, + 1.3, + 0.2, + 1, + 1.522, + 0.2, + 1.744, + 0.2, + 1.967, + 0.2, + 1, + 2, + 0.2, + 2.033, + 0.2, + 2.067, + 0.2, + 1, + 2.078, + 0.2, + 2.089, + 0.2, + 2.1, + 0.2, + 1, + 2.133, + 0.2, + 2.167, + 0.2, + 2.2, + 0.2, + 1, + 2.211, + 0.2, + 2.222, + 0.2, + 2.233, + 0.2, + 1, + 2.267, + 0.2, + 2.3, + 0.2, + 2.333, + 0.2, + 1, + 2.344, + 0.2, + 2.356, + 0.2, + 2.367, + 0.2, + 1, + 2.433, + 0.2, + 2.5, + 0.2, + 2.567, + 0.2, + 1, + 2.711, + 0.2, + 2.856, + 0.2, + 3, + 0.2, + 1, + 3.456, + 0.2, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.1, + 1.3, + 0.1, + 1, + 1.522, + 0.1, + 1.744, + 0.1, + 1.967, + 0.1, + 1, + 2, + 0.1, + 2.033, + -0.2, + 2.067, + -0.2, + 1, + 2.078, + -0.2, + 2.089, + -0.2, + 2.1, + -0.2, + 1, + 2.133, + -0.2, + 2.167, + 0.1, + 2.2, + 0.1, + 1, + 2.211, + 0.1, + 2.222, + 0.1, + 2.233, + 0.1, + 1, + 2.267, + 0.1, + 2.3, + -0.2, + 2.333, + -0.2, + 1, + 2.344, + -0.2, + 2.356, + -0.2, + 2.367, + -0.2, + 1, + 2.433, + -0.2, + 2.5, + 0.1, + 2.567, + 0.1, + 1, + 2.711, + 0.1, + 2.856, + 0.1, + 3, + 0.1, + 1, + 3.456, + 0.1, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.8, + 1.3, + 0.8, + 1, + 1.867, + 0.8, + 2.433, + 0.8, + 3, + 0.8, + 1, + 3.144, + 0.8, + 3.289, + -0.5, + 3.433, + -0.5, + 1, + 3.744, + -0.5, + 4.056, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.8, + 1.3, + 0.8, + 1, + 1.867, + 0.8, + 2.433, + 0.8, + 3, + 0.8, + 1, + 3.144, + 0.8, + 3.289, + -0.5, + 3.433, + -0.5, + 1, + 3.744, + -0.5, + 4.056, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.5, + 1.3, + 0.5, + 1, + 1.867, + 0.5, + 2.433, + 0.5, + 3, + 0.5, + 1, + 3.144, + 0.5, + 3.289, + -0.4, + 3.433, + -0.4, + 1, + 3.744, + -0.4, + 4.056, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.5, + 1.3, + 0.5, + 1, + 1.867, + 0.5, + 2.433, + 0.5, + 3, + 0.5, + 1, + 3.456, + 0.5, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.4, + 1.3, + 0.4, + 1, + 1.867, + 0.4, + 2.433, + 0.4, + 3, + 0.4, + 1, + 3.144, + 0.4, + 3.289, + 0, + 3.433, + 0, + 1, + 3.744, + 0, + 4.056, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0.4, + 1.3, + 0.4, + 1, + 1.867, + 0.4, + 2.433, + 0.4, + 3, + 0.4, + 1, + 3.144, + 0.4, + 3.289, + 0, + 3.433, + 0, + 1, + 3.744, + 0, + 4.056, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + -0.6, + 1.3, + -0.6, + 1, + 1.867, + -0.6, + 2.433, + -0.6, + 3, + -0.6, + 1, + 3.144, + -0.6, + 3.289, + -0.412, + 3.433, + -0.3, + 1, + 3.744, + -0.059, + 4.056, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + -0.6, + 1.3, + -0.6, + 1, + 1.867, + -0.6, + 2.433, + -0.6, + 3, + -0.6, + 1, + 3.144, + -0.6, + 3.289, + -0.412, + 3.433, + -0.3, + 1, + 3.744, + -0.059, + 4.056, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.1, + 1, + 1.2, + -0.6, + 1.3, + -0.6, + 1, + 1.867, + -0.6, + 2.433, + -0.6, + 3, + -0.6, + 1, + 3.456, + -0.6, + 3.911, + 1, + 4.367, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamScarf", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + -3, + 1.3, + -3, + 1, + 1.489, + -3, + 1.678, + -2, + 1.867, + -2, + 1, + 2.244, + -2, + 2.622, + -2, + 3, + -2, + 1, + 3.456, + -2, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.044, + 0, + 1.089, + -2, + 1.133, + -2, + 1, + 1.189, + -2, + 1.244, + 5, + 1.3, + 5, + 1, + 1.489, + 5, + 1.678, + 4, + 1.867, + 4, + 1, + 2.244, + 4, + 2.622, + 4, + 3, + 4, + 1, + 3.144, + 4, + 3.289, + -7, + 3.433, + -7, + 1, + 3.589, + -7, + 3.744, + 9, + 3.9, + 9, + 1, + 4.056, + 9, + 4.211, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + -3, + 1.3, + -3, + 1, + 1.489, + -3, + 1.678, + -2, + 1.867, + -2, + 1, + 2.244, + -2, + 2.622, + -2, + 3, + -2, + 1, + 3.456, + -2, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyUpper", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + -7, + 1.3, + -7, + 1, + 1.489, + -7, + 1.678, + -6, + 1.867, + -6, + 1, + 2.244, + -6, + 2.622, + -6, + 3, + -6, + 1, + 3.144, + -6, + 3.289, + 1.057, + 3.433, + 3, + 1, + 3.589, + 5.092, + 3.744, + 5, + 3.9, + 5, + 1, + 4.056, + 5, + 4.211, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBustY", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.1, + 1, + 1.2, + 0.3, + 1.3, + 0.3, + 1, + 1.489, + 0.3, + 1.678, + 0.4, + 1.867, + 0.4, + 1, + 2.244, + 0.4, + 2.622, + 0.4, + 3, + 0.4, + 1, + 3.144, + 0.4, + 3.289, + 0.4, + 3.433, + 0.4, + 1, + 3.589, + 0.4, + 3.744, + 0.4, + 3.9, + 0.4, + 1, + 4.056, + 0.4, + 4.211, + 1, + 4.367, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.1, + 1, + 1.2, + 0.3, + 1.3, + 0.3, + 1, + 1.489, + 0.3, + 1.678, + 0.4, + 1.867, + 0.4, + 1, + 2.244, + 0.4, + 2.622, + 0.4, + 3, + 0.4, + 1, + 3.144, + 0.4, + 3.289, + 0.4, + 3.433, + 0.4, + 1, + 3.589, + 0.4, + 3.744, + 0.4, + 3.9, + 0.4, + 1, + 4.056, + 0.4, + 4.211, + 1, + 4.367, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 5, + 1, + 0.333, + 5, + 0.667, + 5, + 1, + 5, + 1, + 1.1, + 5, + 1.2, + 5, + 1.3, + 5, + 1, + 1.867, + 5, + 2.433, + 5, + 3, + 5, + 1, + 3.456, + 5, + 3.911, + 5, + 4.367, + 5, + 0, + 4.967, + 5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandChangeR", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleR", + "Segments": [ + 0, + 0.19, + 1, + 0.333, + 0.19, + 0.667, + 0.19, + 1, + 0.19, + 1, + 1.044, + 0.19, + 1.089, + 1, + 1.133, + 1, + 1, + 1.189, + 1, + 1.244, + 0.19, + 1.3, + 0.19, + 1, + 1.489, + 0.19, + 1.678, + 0.4, + 1.867, + 0.4, + 1, + 2.244, + 0.4, + 2.622, + 0.4, + 3, + 0.4, + 1, + 3.144, + 0.4, + 3.289, + -0.8, + 3.433, + -0.8, + 1, + 3.589, + -0.8, + 3.744, + -0.718, + 3.9, + -0.4, + 1, + 4.056, + -0.082, + 4.211, + 0.19, + 4.367, + 0.19, + 0, + 4.967, + 0.19 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandDhangeL", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandAngleL", + "Segments": [ + 0, + -0.17, + 1, + 0.333, + -0.17, + 0.667, + -0.17, + 1, + -0.17, + 1, + 1.044, + -0.17, + 1.089, + -0.6, + 1.133, + -0.6, + 1, + 1.189, + -0.6, + 1.244, + -0.17, + 1.3, + -0.17, + 1, + 1.489, + -0.17, + 1.678, + -0.5, + 1.867, + -0.5, + 1, + 2.244, + -0.5, + 2.622, + -0.5, + 3, + -0.5, + 1, + 3.144, + -0.5, + 3.289, + 1, + 3.433, + 1, + 1, + 3.589, + 1, + 3.744, + 0.997, + 3.9, + 0.7, + 1, + 4.056, + 0.403, + 4.211, + -0.17, + 4.367, + -0.17, + 0, + 4.967, + -0.17 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.1, + 0, + 1.2, + 0, + 1.3, + 0, + 1, + 1.867, + 0, + 2.433, + 0, + 3, + 0, + 1, + 3.456, + 0, + 3.911, + 0, + 4.367, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Core", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Hoho001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Brow001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Tear", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01EyeBall001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Eye001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Nose001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Mouth001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Face001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Ear001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Neck001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairFront001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairSide001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01HairBack001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRB001", + "Segments": [ + 0, + 0, + 2, + 4.933, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLB001", + "Segments": [ + 0, + 0, + 2, + 4.933, + 0, + 0, + 4.967, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmRA001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01ArmLA001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Body001", + "Segments": [ + 0, + 1, + 2, + 4.933, + 1, + 0, + 4.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part01Sketch", + "Segments": [ + 0, + 0, + 2, + 4.933, + 0, + 0, + 4.967, + 0 + ] + } + ] +} +``` + +`Content/Samples/Hiyori/Hiyori.model3.json`: + +```json +{ + "Version": 3, + "FileReferences": { + "Moc": "Hiyori.moc3", + "Textures": [ + "Hiyori.2048/texture_00.png", + "Hiyori.2048/texture_01.png" + ], + "Physics": "Hiyori.physics3.json", + "Pose": "Hiyori.pose3.json", + "UserData": "Hiyori.userdata3.json", + "Motions": { + "Idle": [ + { + "File": "motions/Hiyori_m01.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/Hiyori_m02.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/Hiyori_m03.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/Hiyori_m05.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/Hiyori_m06.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/Hiyori_m07.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/Hiyori_m08.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/Hiyori_m09.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/Hiyori_m10.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + } + ], + "TapBody": [ + { + "File": "motions/Hiyori_m04.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + } + ] + } + }, + "Groups": [ + { + "Target": "Parameter", + "Name": "LipSync", + "Ids": [ + "ParamMouthOpenY" + ] + }, + { + "Target": "Parameter", + "Name": "EyeBlink", + "Ids": [ + "ParamEyeLOpen", + "ParamEyeROpen" + ] + } + ], + "HitAreas": [ + { + "Id": "HitArea", + "Name": "Body" + } + ] +} +``` + +`Content/Samples/Hiyori/Hiyori.physics3.json`: + +```json +{ + "Version": 3, + "Meta": { + "PhysicsSettingCount": 11, + "TotalInputCount": 34, + "TotalOutputCount": 35, + "VertexCount": 58, + "EffectiveForces": { + "Gravity": { + "X": 0, + "Y": -1 + }, + "Wind": { + "X": 0, + "Y": 0 + } + }, + "PhysicsDictionary": [ + { + "Id": "PhysicsSetting2", + "Name": "前髪" + }, + { + "Id": "PhysicsSetting4", + "Name": "後ろ髪" + }, + { + "Id": "PhysicsSetting5", + "Name": "リボン 髪" + }, + { + "Id": "PhysicsSetting6", + "Name": "スカート 横揺れ" + }, + { + "Id": "PhysicsSetting8", + "Name": "スカート 縦揺れ" + }, + { + "Id": "PhysicsSetting9", + "Name": "リボン 体" + }, + { + "Id": "PhysicsSetting10", + "Name": "胸揺れ" + }, + { + "Id": "PhysicsSetting9", + "Name": "サイドアップ 右" + }, + { + "Id": "PhysicsSetting10", + "Name": "サイドアップ 左" + }, + { + "Id": "PhysicsSetting11", + "Name": "横髪 右" + }, + { + "Id": "PhysicsSetting12", + "Name": "横髪 左" + } + ] + }, + "PhysicsSettings": [ + { + "Id": "PhysicsSetting2", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHairFront" + }, + "VertexIndex": 1, + "Scale": 1.522, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 3 + }, + "Mobility": 0.95, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 3 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting4", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHairBack" + }, + "VertexIndex": 1, + "Scale": 2.061, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 15 + }, + "Mobility": 0.95, + "Delay": 0.8, + "Acceleration": 1.5, + "Radius": 15 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -30, + "Default": 0, + "Maximum": 30 + } + } + }, + { + "Id": "PhysicsSetting5", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamSideupRibbon" + }, + "VertexIndex": 1, + "Scale": 1.775, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 10 + }, + "Mobility": 0.9, + "Delay": 0.6, + "Acceleration": 1.5, + "Radius": 10 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -7.8, + "Default": 0, + "Maximum": 8 + } + } + }, + { + "Id": "PhysicsSetting6", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 100, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamSkirt" + }, + "VertexIndex": 1, + "Scale": 1.434, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 10 + }, + "Mobility": 0.9, + "Delay": 0.6, + "Acceleration": 1.5, + "Radius": 10 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting8", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleY" + }, + "Weight": 100, + "Type": "X", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamSkirt2" + }, + "VertexIndex": 1, + "Scale": 1.306, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 10 + }, + "Mobility": 0.8, + "Delay": 0.9, + "Acceleration": 2, + "Radius": 10 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting9", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 100, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamRibbon" + }, + "VertexIndex": 1, + "Scale": 1.402, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 10 + }, + "Mobility": 0.9, + "Delay": 0.6, + "Acceleration": 1.5, + "Radius": 10 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10.6 + } + } + }, + { + "Id": "PhysicsSetting10", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleY" + }, + "Weight": 100, + "Type": "X", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamBustY" + }, + "VertexIndex": 1, + "Scale": 1, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 3 + }, + "Mobility": 0.8, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 3 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting9", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_1_ArtMesh61" + }, + "VertexIndex": 1, + "Scale": 10, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_2_ArtMesh61" + }, + "VertexIndex": 2, + "Scale": 20, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_3_ArtMesh61" + }, + "VertexIndex": 3, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_4_ArtMesh61" + }, + "VertexIndex": 4, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_5_ArtMesh61" + }, + "VertexIndex": 5, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_6_ArtMesh61" + }, + "VertexIndex": 6, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_7_ArtMesh61" + }, + "VertexIndex": 7, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 10 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 20 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 30 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 40 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 50 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 60 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 70 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 80 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 90 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 100 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting10", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_1_ArtMesh62" + }, + "VertexIndex": 1, + "Scale": 10, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_2_ArtMesh62" + }, + "VertexIndex": 2, + "Scale": 20, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_3_ArtMesh62" + }, + "VertexIndex": 3, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_4_ArtMesh62" + }, + "VertexIndex": 4, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_5_ArtMesh62" + }, + "VertexIndex": 5, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_6_ArtMesh62" + }, + "VertexIndex": 6, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_7_ArtMesh62" + }, + "VertexIndex": 7, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 10 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 20 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 30 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 40 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 50 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 60 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 70 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 80 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 90 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + }, + { + "Position": { + "X": 0, + "Y": 100 + }, + "Mobility": 0.85, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 10 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting11", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_1_ArtMesh54" + }, + "VertexIndex": 1, + "Scale": 5, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_2_ArtMesh54" + }, + "VertexIndex": 2, + "Scale": 5, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_3_ArtMesh54" + }, + "VertexIndex": 3, + "Scale": 10, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_4_ArtMesh54" + }, + "VertexIndex": 4, + "Scale": 20, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_5_ArtMesh54" + }, + "VertexIndex": 5, + "Scale": 20, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_6_ArtMesh54" + }, + "VertexIndex": 6, + "Scale": 25, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_7_ArtMesh54" + }, + "VertexIndex": 7, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 0.9, + "Delay": 1, + "Acceleration": 1, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 15 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 30 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 45 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 60 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 75 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 90 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 105 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 120 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 135 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 150 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting12", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_1_ArtMesh55" + }, + "VertexIndex": 1, + "Scale": 5, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_2_ArtMesh55" + }, + "VertexIndex": 2, + "Scale": 5, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_3_ArtMesh55" + }, + "VertexIndex": 3, + "Scale": 10, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_4_ArtMesh55" + }, + "VertexIndex": 4, + "Scale": 20, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_5_ArtMesh55" + }, + "VertexIndex": 5, + "Scale": 20, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_6_ArtMesh55" + }, + "VertexIndex": 6, + "Scale": 25, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_7_ArtMesh55" + }, + "VertexIndex": 7, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 0.9, + "Delay": 0.7, + "Acceleration": 1, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 15 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 30 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 45 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 60 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 75 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 90 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 105 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 120 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 135 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + }, + { + "Position": { + "X": 0, + "Y": 150 + }, + "Mobility": 0.9, + "Delay": 0.85, + "Acceleration": 1.2, + "Radius": 15 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + } + ] +} +``` + +`Content/Samples/Hiyori/Hiyori.pose3.json`: + +```json +{ + "Type": "Live2D Pose", + "FadeInTime": 0.5, + "Groups": [ + [ + { + "Id": "PartArmA", + "Link": [] + }, + { + "Id": "PartArmB", + "Link": [] + } + ] + ] +} +``` + +`Content/Samples/Hiyori/Hiyori.userdata3.json`: + +```json +{ + "Version": 3, + "Meta": { + "UserDataCount": 7, + "TotalUserDataSize": 35 + }, + "UserData": [ + { + "Target": "ArtMesh", + "Id": "ArtMesh93", + "Value": "ribon" + }, + { + "Target": "ArtMesh", + "Id": "ArtMesh94", + "Value": "ribon" + }, + { + "Target": "ArtMesh", + "Id": "ArtMesh95", + "Value": "ribon" + }, + { + "Target": "ArtMesh", + "Id": "ArtMesh57", + "Value": "ribon" + }, + { + "Target": "ArtMesh", + "Id": "ArtMesh58", + "Value": "ribon" + }, + { + "Target": "ArtMesh", + "Id": "ArtMesh59", + "Value": "ribon" + }, + { + "Target": "ArtMesh", + "Id": "ArtMesh60", + "Value": "ribon" + } + ] +} +``` + +`Content/Samples/Hiyori/motions/Hiyori_m01.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4.733, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": false, + "CurveCount": 31, + "TotalSegmentCount": 135, + "TotalPointCount": 374, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + -8, + 1, + 0.067, + -8, + 0.133, + -8, + 0.2, + -8, + 1, + 0.278, + -8, + 0.356, + -8, + 0.433, + -8, + 1, + 0.556, + -8, + 0.678, + -8, + 0.8, + -8, + 1, + 0.956, + -8, + 1.111, + -8, + 1.267, + -8, + 1, + 1.522, + -8, + 1.778, + 0, + 2.033, + 0, + 1, + 2.122, + 0, + 2.211, + 0, + 2.3, + 0, + 1, + 2.556, + 0, + 2.811, + 9, + 3.067, + 9, + 1, + 3.356, + 9, + 3.644, + 0, + 3.933, + 0, + 1, + 4.067, + 0, + 4.2, + 1, + 4.333, + 1, + 0, + 4.733, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + -5, + 1, + 0.067, + -5, + 0.133, + -5, + 0.2, + -5, + 1, + 0.322, + -5, + 0.444, + 16, + 0.567, + 16, + 1, + 0.711, + 16, + 0.856, + -12, + 1, + -12, + 1, + 1.089, + -12, + 1.178, + 11, + 1.267, + 11, + 1, + 1.356, + 11, + 1.444, + 9.376, + 1.533, + 3, + 1, + 1.611, + -2.579, + 1.689, + -8, + 1.767, + -8, + 1, + 1.856, + -8, + 1.944, + 0, + 2.033, + 0, + 1, + 2.122, + 0, + 2.211, + 0, + 2.3, + 0, + 1, + 2.578, + 0, + 2.856, + -30, + 3.133, + -30, + 1, + 3.278, + -30, + 3.422, + 15, + 3.567, + 15, + 1, + 3.689, + 15, + 3.811, + -9, + 3.933, + -9, + 0, + 4.733, + -9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0, + 0.433, + 0, + 1, + 0.711, + 0, + 0.989, + 0, + 1.267, + 0, + 1, + 1.522, + 0, + 1.778, + 14, + 2.033, + 14, + 1, + 2.178, + 14, + 2.322, + 14, + 2.467, + 14, + 1, + 2.8, + 14, + 3.133, + 16, + 3.467, + 16, + 1, + 3.633, + 16, + 3.8, + -13, + 3.967, + -13, + 1, + 4.111, + -13, + 4.256, + -11.207, + 4.4, + -11.207, + 0, + 4.733, + -11.207 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.556, + 1, + 1.111, + 1, + 1.667, + 1, + 1, + 1.711, + 1, + 1.756, + 0, + 1.8, + 0, + 1, + 1.822, + 0, + 1.844, + 0, + 1.867, + 0, + 1, + 1.911, + 0, + 1.956, + 1, + 2, + 1, + 1, + 2.489, + 1, + 2.978, + 1, + 3.467, + 1, + 1, + 3.511, + 1, + 3.556, + 0, + 3.6, + 0, + 1, + 3.622, + 0, + 3.644, + 0, + 3.667, + 0, + 1, + 3.733, + 0, + 3.8, + 1, + 3.867, + 1, + 0, + 4.733, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.556, + 1, + 1.111, + 1, + 1.667, + 1, + 1, + 1.711, + 1, + 1.756, + 0, + 1.8, + 0, + 1, + 1.822, + 0, + 1.844, + 0, + 1.867, + 0, + 1, + 1.911, + 0, + 1.956, + 1, + 2, + 1, + 1, + 2.489, + 1, + 2.978, + 1, + 3.467, + 1, + 1, + 3.511, + 1, + 3.556, + 0, + 3.6, + 0, + 1, + 3.622, + 0, + 3.644, + 0, + 3.667, + 0, + 1, + 3.733, + 0, + 3.8, + 1, + 3.867, + 1, + 0, + 4.733, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.156, + 0, + 0.311, + -0.02, + 0.467, + -0.02, + 1, + 0.578, + -0.02, + 0.689, + 0, + 0.8, + 0, + 1, + 1.056, + 0, + 1.311, + 0, + 1.567, + 0, + 1, + 1.767, + 0, + 1.967, + -0.131, + 2.167, + -0.15, + 1, + 2.644, + -0.195, + 3.122, + -0.2, + 3.6, + -0.2, + 1, + 3.622, + -0.2, + 3.644, + 0.803, + 3.667, + 0.803, + 0, + 4.733, + 0.803 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.156, + 0, + 0.311, + 0.01, + 0.467, + 0.08, + 1, + 0.578, + 0.13, + 0.689, + 0.21, + 0.8, + 0.21, + 1, + 1.056, + 0.21, + 1.311, + 0.21, + 1.567, + 0.21, + 1, + 1.767, + 0.21, + 1.967, + 0.08, + 2.167, + 0.08, + 1, + 2.644, + 0.08, + 3.122, + 0.086, + 3.6, + 0.1, + 1, + 3.622, + 0.101, + 3.644, + 0.794, + 3.667, + 0.794, + 0, + 4.733, + 0.794 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 0, + 4.733, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 1, + 1, + 0.856, + 1, + 1.711, + 1, + 2.567, + 1, + 1, + 2.789, + 1, + 3.011, + 1.145, + 3.233, + 0, + 1, + 3.467, + -1.202, + 3.7, + -6, + 3.933, + -6, + 0, + 4.733, + -6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 7, + 0.433, + 7, + 1, + 0.611, + 7, + 0.789, + -4, + 0.967, + -4, + 1, + 1.078, + -4, + 1.189, + 6, + 1.3, + 6, + 1, + 1.433, + 6, + 1.567, + -3, + 1.7, + -3, + 1, + 1.789, + -3, + 1.878, + -1.459, + 1.967, + 0, + 1, + 2.067, + 1.642, + 2.167, + 3.47, + 2.267, + 4, + 1, + 2.367, + 4.53, + 2.467, + 4.312, + 2.567, + 5, + 1, + 2.889, + 7.215, + 3.211, + 10, + 3.533, + 10, + 1, + 3.667, + 10, + 3.8, + 0, + 3.933, + 0, + 1, + 4.067, + 0, + 4.2, + 0, + 4.333, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.822, + 0, + 1.644, + 0, + 2.467, + 0, + 1, + 2.722, + 0, + 2.978, + 5, + 3.233, + 5, + 1, + 3.489, + 5, + 3.744, + -5, + 4, + -5, + 1, + 4.156, + -5, + 4.311, + -3.976, + 4.467, + -3.976, + 0, + 4.733, + -3.976 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulder", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0.2, + 0.333, + 0.2, + 1, + 0.422, + 0.2, + 0.511, + -0.1, + 0.6, + -0.1, + 1, + 1.578, + -0.1, + 2.556, + -0.1, + 3.533, + -0.1, + 1, + 3.611, + -0.1, + 3.689, + 0.2, + 3.767, + 0.2, + 1, + 3.989, + 0.2, + 4.211, + -0.9, + 4.433, + -0.9, + 0, + 4.733, + -0.9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeg", + "Segments": [ + 0, + 1, + 0, + 4.733, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + -10, + 1, + 0.178, + -10, + 0.356, + -7, + 0.533, + -7, + 1, + 0.7, + -7, + 0.867, + -10, + 1.033, + -10, + 1, + 1.2, + -10, + 1.367, + -8, + 1.533, + -8, + 1, + 1.611, + -8, + 1.689, + -8.746, + 1.767, + -9, + 1, + 2.011, + -9.797, + 2.256, + -10, + 2.5, + -10, + 1, + 2.556, + -10, + 2.611, + -10, + 2.667, + -10, + 1, + 2.789, + -10, + 2.911, + -10, + 3.033, + -10, + 0, + 4.733, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + -10, + 1, + 0.178, + -10, + 0.356, + -7, + 0.533, + -7, + 1, + 0.7, + -7, + 0.867, + -10, + 1.033, + -10, + 1, + 1.2, + -10, + 1.367, + -6, + 1.533, + -6, + 1, + 1.611, + -6, + 1.689, + -6.903, + 1.767, + -7.5, + 1, + 2.011, + -9.377, + 2.256, + -10, + 2.5, + -10, + 1, + 2.567, + -10, + 2.633, + -8.958, + 2.7, + -8.958, + 1, + 2.811, + -8.958, + 2.922, + -10, + 3.033, + -10, + 0, + 4.733, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairAhoge", + "Segments": [ + 0, + 0, + 1, + 0.144, + 0, + 0.289, + 10, + 0.433, + 10, + 1, + 0.578, + 10, + 0.722, + -10, + 0.867, + -10, + 1, + 0.989, + -10, + 1.111, + 4, + 1.233, + 4, + 1, + 1.322, + 4, + 1.411, + 2.767, + 1.5, + 0, + 1, + 1.611, + -3.459, + 1.722, + -5.351, + 1.833, + -5.351, + 1, + 1.922, + -5.351, + 2.011, + 1.661, + 2.1, + 1.661, + 1, + 2.233, + 1.661, + 2.367, + 0, + 2.5, + 0, + 1, + 2.867, + 0, + 3.233, + 10, + 3.6, + 10, + 1, + 3.722, + 10, + 3.844, + -10, + 3.967, + -10, + 1, + 4.078, + -10, + 4.189, + 6, + 4.3, + 6, + 1, + 4.356, + 6, + 4.411, + 0, + 4.467, + 0, + 0, + 4.733, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmA", + "Segments": [ + 0, + 0.999, + 0, + 4.733, + 0.999 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmB", + "Segments": [ + 0, + 0.001, + 0, + 4.733, + 0.001 + ] + } + ] +} +``` + +`Content/Samples/Hiyori/motions/Hiyori_m02.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 5.967, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": false, + "CurveCount": 33, + "TotalSegmentCount": 190, + "TotalPointCount": 537, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 18, + 1, + 0.211, + 18, + 0.422, + 0, + 0.633, + 0, + 1, + 0.889, + 0, + 1.144, + 22, + 1.4, + 22, + 1, + 1.578, + 22, + 1.756, + 22, + 1.933, + 22, + 1, + 2.1, + 22, + 2.267, + 14.992, + 2.433, + 0, + 1, + 2.6, + -14.992, + 2.767, + -23, + 2.933, + -23, + 1, + 3.244, + -23, + 3.556, + -23, + 3.867, + -23, + 1, + 4.078, + -23, + 4.289, + 0, + 4.5, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + -24, + 1, + 0.211, + -24, + 0.422, + 0, + 0.633, + 0, + 1, + 0.889, + 0, + 1.144, + -18, + 1.4, + -18, + 1, + 1.522, + -18, + 1.644, + -11, + 1.767, + -11, + 1, + 1.822, + -11, + 1.878, + -11, + 1.933, + -11, + 1, + 2.1, + -11, + 2.267, + -3, + 2.433, + -3, + 1, + 2.6, + -3, + 2.767, + -30, + 2.933, + -30, + 1, + 3.133, + -30, + 3.333, + -23, + 3.533, + -23, + 1, + 3.644, + -23, + 3.756, + -23, + 3.867, + -23, + 1, + 3.911, + -23, + 3.956, + -23, + 4, + -23, + 1, + 4.167, + -23, + 4.333, + 0, + 4.5, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.756, + 0, + 0.844, + 1.707, + 0.933, + 8, + 1, + 1.122, + 21.373, + 1.311, + 30, + 1.5, + 30, + 1, + 1.656, + 30, + 1.811, + 30, + 1.967, + 30, + 1, + 2.289, + 30, + 2.611, + -30, + 2.933, + -30, + 1, + 3.244, + -30, + 3.556, + -30, + 3.867, + -30, + 1, + 4.078, + -30, + 4.289, + -21.337, + 4.5, + 1, + 1, + 4.578, + 9.23, + 4.656, + 17, + 4.733, + 17, + 0, + 5.967, + 17 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 1, + 0, + 5.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.044, + 1, + 1.089, + 0, + 1.133, + 0, + 1, + 1.156, + 0, + 1.178, + 0, + 1.2, + 0, + 1, + 1.256, + 0, + 1.311, + 1, + 1.367, + 1, + 1, + 1.833, + 1, + 2.3, + 1, + 2.767, + 1, + 1, + 2.867, + 1, + 2.967, + 1.2, + 3.067, + 1.2, + 1, + 3.233, + 1.2, + 3.4, + 1, + 3.567, + 1, + 1, + 3.689, + 1, + 3.811, + 1, + 3.933, + 1, + 1, + 3.978, + 1, + 4.022, + 0, + 4.067, + 0, + 1, + 4.089, + 0, + 4.111, + 0, + 4.133, + 0, + 1, + 4.189, + 0, + 4.244, + 1, + 4.3, + 1, + 1, + 4.4, + 1, + 4.5, + 1, + 4.6, + 1, + 1, + 4.633, + 1, + 4.667, + 0, + 4.7, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0.63, + 1, + 1.433, + 0.63, + 2.867, + 0.6, + 4.3, + 0.6, + 1, + 4.4, + 0.6, + 4.5, + 0.771, + 4.6, + 0.771, + 0, + 5.967, + 0.771 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.333, + 1, + 0.667, + 1, + 1, + 1, + 1, + 1.044, + 1, + 1.089, + 0, + 1.133, + 0, + 1, + 1.156, + 0, + 1.178, + 0, + 1.2, + 0, + 1, + 1.256, + 0, + 1.311, + 1, + 1.367, + 1, + 1, + 1.833, + 1, + 2.3, + 1, + 2.767, + 1, + 1, + 2.867, + 1, + 2.967, + 1.2, + 3.067, + 1.2, + 1, + 3.233, + 1.2, + 3.4, + 1, + 3.567, + 1, + 1, + 3.689, + 1, + 3.811, + 1, + 3.933, + 1, + 1, + 3.978, + 1, + 4.022, + 0, + 4.067, + 0, + 1, + 4.089, + 0, + 4.111, + 0, + 4.133, + 0, + 1, + 4.189, + 0, + 4.244, + 1, + 4.3, + 1, + 1, + 4.4, + 1, + 4.5, + 1, + 4.6, + 1, + 1, + 4.633, + 1, + 4.667, + 0, + 4.7, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0.63, + 1, + 1.433, + 0.63, + 2.867, + 0.6, + 4.3, + 0.6, + 1, + 4.4, + 0.6, + 4.5, + 0.76, + 4.6, + 0.76, + 0, + 5.967, + 0.76 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + -0.002, + 1, + 0.211, + -0.002, + 0.422, + 0, + 0.633, + 0, + 1, + 0.889, + 0, + 1.144, + -0.31, + 1.4, + -0.31, + 1, + 1.578, + -0.31, + 1.756, + -0.31, + 1.933, + -0.31, + 1, + 2.233, + -0.31, + 2.533, + 0.67, + 2.833, + 0.67, + 1, + 3.144, + 0.67, + 3.456, + 0.67, + 3.767, + 0.67, + 1, + 4.011, + 0.67, + 4.256, + 0, + 4.5, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + 0, + 0.633, + 0, + 1, + 0.889, + 0, + 1.144, + 0.38, + 1.4, + 0.38, + 1, + 1.578, + 0.38, + 1.756, + 0.38, + 1.933, + 0.38, + 1, + 2.233, + 0.38, + 2.533, + 0.46, + 2.833, + 0.46, + 1, + 3.144, + 0.46, + 3.456, + 0.46, + 3.767, + 0.46, + 1, + 4.011, + 0.46, + 4.256, + 0, + 4.5, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 1, + 2.933, + 0, + 3, + 0.125, + 3.067, + 0.125, + 1, + 3.133, + 0.125, + 3.2, + 0, + 3.267, + 0, + 1, + 3.711, + 0, + 4.156, + 0, + 4.6, + 0, + 1, + 4.667, + 0, + 4.733, + 0.396, + 4.8, + 0.396, + 0, + 5.967, + 0.396 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.956, + 0, + 1.911, + 0, + 2.867, + 0, + 1, + 2.933, + 0, + 3, + 0.125, + 3.067, + 0.125, + 1, + 3.133, + 0.125, + 3.2, + 0, + 3.267, + 0, + 1, + 3.711, + 0, + 4.156, + 0, + 4.6, + 0, + 1, + 4.667, + 0, + 4.733, + 0.417, + 4.8, + 0.417, + 0, + 5.967, + 0.417 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0.03, + 0, + 5.967, + 0.03 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + -1, + 1, + 1.433, + -1, + 2.867, + -1, + 4.3, + -1, + 1, + 4.367, + -1, + 4.433, + 1, + 4.5, + 1, + 0, + 5.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 1, + 1, + 1.433, + 1, + 2.867, + 1, + 4.3, + 1, + 1, + 4.367, + 1, + 4.433, + 0, + 4.5, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 0, + 0.733, + 0, + 1, + 0.778, + 0, + 0.822, + 0.667, + 0.867, + 2, + 1, + 1.044, + 7.333, + 1.222, + 10, + 1.4, + 10, + 1, + 1.533, + 10, + 1.667, + 10, + 1.8, + 10, + 1, + 2.178, + 10, + 2.556, + 6, + 2.933, + 6, + 1, + 3.244, + 6, + 3.556, + 6, + 3.867, + 6, + 1, + 4.189, + 6, + 4.511, + 0, + 4.833, + 0, + 1, + 4.956, + 0, + 5.078, + 1.192, + 5.2, + 1.192, + 1, + 5.322, + 1.192, + 5.444, + 0.013, + 5.567, + 0.013, + 0, + 5.967, + 0.013 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + 0, + 0.633, + 0, + 1, + 0.667, + 0, + 0.7, + 0, + 0.733, + 0, + 1, + 1.133, + 0, + 1.533, + 0, + 1.933, + 0, + 1, + 2.078, + 0, + 2.222, + 4, + 2.367, + 4, + 1, + 2.556, + 4, + 2.744, + -2.18, + 2.933, + -2.18, + 1, + 3.022, + -2.18, + 3.111, + 0, + 3.2, + 0, + 1, + 3.456, + 0, + 3.711, + -1, + 3.967, + -1, + 1, + 4.067, + -1, + 4.167, + -1.018, + 4.267, + 0, + 1, + 4.333, + 0.679, + 4.4, + 10, + 4.467, + 10, + 1, + 4.567, + 10, + 4.667, + -5, + 4.767, + -5, + 1, + 4.889, + -5, + 5.011, + 0, + 5.133, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.233, + 0, + 0.467, + 0, + 0.7, + 0, + 1, + 0.733, + 0, + 0.767, + 0, + 0.8, + 0, + 1, + 1.278, + 0, + 1.756, + -4, + 2.233, + -4, + 1, + 2.544, + -4, + 2.856, + 8, + 3.167, + 8, + 1, + 3.4, + 8, + 3.633, + 8.008, + 3.867, + 7.985, + 1, + 4.178, + 7.954, + 4.489, + -3, + 4.8, + -3, + 0, + 5.967, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + 1, + 0.367, + 1, + 1, + 0.511, + 1, + 0.656, + 0, + 0.8, + 0, + 1, + 1.022, + 0, + 1.244, + 1, + 1.467, + 1, + 1, + 1.689, + 1, + 1.911, + 0, + 2.133, + 0, + 1, + 2.344, + 0, + 2.556, + 1, + 2.767, + 1, + 1, + 2.967, + 1, + 3.167, + 0, + 3.367, + 0, + 1, + 3.578, + 0, + 3.789, + 1, + 4, + 1, + 1, + 4.233, + 1, + 4.467, + 0, + 4.7, + 0, + 1, + 4.9, + 0, + 5.1, + 0, + 5.3, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulder", + "Segments": [ + 0, + 0, + 1, + 0.456, + 0, + 0.911, + 0.4, + 1.367, + 0.4, + 1, + 1.889, + 0.4, + 2.411, + -1, + 2.933, + -1, + 1, + 3.244, + -1, + 3.556, + -1, + 3.867, + -1, + 0, + 5.967, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeg", + "Segments": [ + 0, + 1, + 0, + 5.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + -8.7, + 1, + 0.289, + -8.7, + 0.578, + -8.7, + 0.867, + -8.7, + 1, + 1, + -8.7, + 1.133, + -10, + 1.267, + -10, + 1, + 1.578, + -10, + 1.889, + -10, + 2.2, + -10, + 1, + 2.444, + -10, + 2.689, + -6.9, + 2.933, + -6.9, + 1, + 3.244, + -6.9, + 3.556, + -6.9, + 3.867, + -6.9, + 1, + 3.989, + -6.9, + 4.111, + -8.3, + 4.233, + -8.3, + 1, + 4.344, + -8.3, + 4.456, + -3, + 4.567, + -3, + 1, + 4.689, + -3, + 4.811, + -4, + 4.933, + -4, + 1, + 5.1, + -4, + 5.267, + -3, + 5.433, + -3, + 0, + 5.967, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + -8.7, + 1, + 0.289, + -8.7, + 0.578, + -8.7, + 0.867, + -8.7, + 1, + 1, + -8.7, + 1.133, + -10, + 1.267, + -10, + 1, + 1.578, + -10, + 1.889, + -10, + 2.2, + -10, + 1, + 2.444, + -10, + 2.689, + -10, + 2.933, + -10, + 1, + 3.244, + -10, + 3.556, + -10, + 3.867, + -10, + 1, + 3.989, + -10, + 4.111, + -9.998, + 4.233, + -7.8, + 1, + 4.344, + -5.802, + 4.456, + 0.03, + 4.567, + 0.03, + 1, + 4.689, + 0.03, + 4.811, + -1, + 4.933, + -1, + 1, + 5.122, + -1, + 5.311, + -0.002, + 5.5, + -0.002, + 0, + 5.967, + -0.002 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandL", + "Segments": [ + 0, + 0, + 1, + 1.067, + 0, + 2.133, + 0, + 3.2, + 0, + 1, + 3.633, + 0, + 4.067, + 1, + 4.5, + 1, + 1, + 4.667, + 1, + 4.833, + -1, + 5, + -1, + 1, + 5.122, + -1, + 5.244, + 0.208, + 5.367, + 0.208, + 0, + 5.967, + 0.208 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandR", + "Segments": [ + 0, + 0, + 1, + 1.067, + 0, + 2.133, + 0, + 3.2, + 0, + 1, + 3.633, + 0, + 4.067, + 1, + 4.5, + 1, + 1, + 4.667, + 1, + 4.833, + -1, + 5, + -1, + 1, + 5.122, + -1, + 5.244, + 0.208, + 5.367, + 0.208, + 0, + 5.967, + 0.208 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairAhoge", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.133, + 0, + 1.267, + 10, + 1.4, + 10, + 1, + 1.489, + 10, + 1.578, + 2.177, + 1.667, + 2.177, + 1, + 1.789, + 2.177, + 1.911, + 10, + 2.033, + 10, + 1, + 2.289, + 10, + 2.544, + -10, + 2.8, + -10, + 1, + 2.933, + -10, + 3.067, + 7.54, + 3.2, + 7.54, + 1, + 3.344, + 7.54, + 3.489, + 0, + 3.633, + 0, + 1, + 3.911, + 0, + 4.189, + 0, + 4.467, + 0, + 1, + 4.6, + 0, + 4.733, + 10, + 4.867, + 10, + 1, + 4.989, + 10, + 5.111, + -10, + 5.233, + -10, + 1, + 5.322, + -10, + 5.411, + 3.456, + 5.5, + 3.456, + 1, + 5.556, + 3.456, + 5.611, + 0, + 5.667, + 0, + 0, + 5.967, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmA", + "Segments": [ + 0, + 0.997, + 0, + 5.967, + 0.997 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmB", + "Segments": [ + 0, + 0, + 0, + 5.967, + 0 + ] + } + ] +} +``` + +`Content/Samples/Hiyori/motions/Hiyori_m03.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4.233, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": false, + "CurveCount": 33, + "TotalSegmentCount": 169, + "TotalPointCount": 474, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.389, + 0, + 0.611, + 0, + 0.833, + 0, + 1, + 0.933, + 0, + 1.033, + 0, + 1.133, + 0, + 1, + 1.311, + 0, + 1.489, + 0, + 1.667, + 0, + 1, + 1.778, + 0, + 1.889, + -0.268, + 2, + -1, + 1, + 2.089, + -1.585, + 2.178, + -2, + 2.267, + -2, + 0, + 4.233, + -2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.3, + 0, + 0.433, + -18, + 0.567, + -18, + 1, + 0.656, + -18, + 0.744, + 8, + 0.833, + 8, + 1, + 0.933, + 8, + 1.033, + -8, + 1.133, + -8, + 1, + 1.311, + -8, + 1.489, + 10, + 1.667, + 10, + 1, + 1.778, + 10, + 1.889, + -21, + 2, + -21, + 1, + 2.089, + -21, + 2.178, + -3, + 2.267, + -3, + 0, + 4.233, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.289, + 0, + 0.411, + -1, + 0.533, + -1, + 1, + 0.722, + -1, + 0.911, + 17, + 1.1, + 17, + 1, + 1.411, + 17, + 1.722, + -22, + 2.033, + -22, + 1, + 2.244, + -22, + 2.456, + 11, + 2.667, + 11, + 0, + 4.233, + 11 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.2, + 1, + 0.233, + 1, + 0.267, + 1, + 1, + 0.322, + 1, + 0.378, + 0, + 0.433, + 0, + 1, + 0.478, + 0, + 0.522, + 0, + 0.567, + 0, + 1, + 0.644, + 0, + 0.722, + 1, + 0.8, + 1, + 1, + 1.056, + 1, + 1.311, + 0.988, + 1.567, + 0.988, + 1, + 1.789, + 0.988, + 2.011, + 1, + 2.233, + 1, + 1, + 2.267, + 1, + 2.3, + 1, + 2.333, + 1, + 1, + 2.367, + 1, + 2.4, + 0, + 2.433, + 0, + 1, + 2.478, + 0, + 2.522, + 0, + 2.567, + 0, + 1, + 2.622, + 0, + 2.678, + 1, + 2.733, + 1, + 0, + 4.233, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.2, + 1, + 0.233, + 1, + 0.267, + 1, + 1, + 0.322, + 1, + 0.378, + 0, + 0.433, + 0, + 1, + 0.478, + 0, + 0.522, + 0, + 0.567, + 0, + 1, + 0.644, + 0, + 0.722, + 1, + 0.8, + 1, + 1, + 1.056, + 1, + 1.311, + 1, + 1.567, + 1, + 1, + 1.789, + 1, + 2.011, + 1, + 2.233, + 1, + 1, + 2.267, + 1, + 2.3, + 1, + 2.333, + 1, + 1, + 2.367, + 1, + 2.4, + 0, + 2.433, + 0, + 1, + 2.478, + 0, + 2.522, + 0, + 2.567, + 0, + 1, + 2.622, + 0, + 2.678, + 1, + 2.733, + 1, + 0, + 4.233, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.256, + 0, + 0.344, + 0, + 0.433, + 0, + 1, + 0.478, + 0, + 0.522, + -1, + 0.567, + -1, + 1, + 0.8, + -1, + 1.033, + -1, + 1.267, + -1, + 1, + 1.422, + -1, + 1.578, + 0.583, + 1.733, + 0.583, + 1, + 1.967, + 0.583, + 2.2, + 0.583, + 2.433, + 0.583, + 1, + 2.478, + 0.583, + 2.522, + 0, + 2.567, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.256, + 0, + 0.344, + 0, + 0.433, + 0, + 1, + 0.478, + 0, + 0.522, + 1, + 0.567, + 1, + 1, + 0.8, + 1, + 1.033, + 1, + 1.267, + 1, + 1, + 1.422, + 1, + 1.578, + 0.75, + 1.733, + 0.75, + 1, + 1.967, + 0.75, + 2.2, + 0.75, + 2.433, + 0.75, + 1, + 2.478, + 0.75, + 2.522, + 0, + 2.567, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 0, + 4.233, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.244, + 0, + 0.322, + -4, + 0.4, + -4, + 1, + 0.478, + -4, + 0.556, + 10, + 0.633, + 10, + 1, + 0.8, + 10, + 0.967, + -0.386, + 1.133, + -0.386, + 1, + 1.244, + -0.386, + 1.356, + 10, + 1.467, + 10, + 1, + 1.6, + 10, + 1.733, + 0, + 1.867, + 0, + 1, + 2.044, + 0, + 2.222, + 10, + 2.4, + 10, + 1, + 2.444, + 10, + 2.489, + 10.395, + 2.533, + 9.649, + 1, + 2.7, + 6.854, + 2.867, + -3.526, + 3.033, + -3.526, + 1, + 3.267, + -3.526, + 3.5, + 6.807, + 3.733, + 6.807, + 1, + 3.811, + 6.807, + 3.889, + 6, + 3.967, + 6, + 0, + 4.233, + 6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.256, + 0, + 0.344, + -1, + 0.433, + -1, + 1, + 0.656, + -1, + 0.878, + 6, + 1.1, + 6, + 1, + 1.333, + 6, + 1.567, + -3, + 1.8, + -3, + 1, + 2.256, + -3, + 2.711, + 8, + 3.167, + 8, + 0, + 4.233, + 8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 1, + 0.567, + 1, + 1, + 0.733, + 1, + 0.9, + 0, + 1.067, + 0, + 1, + 1.3, + 0, + 1.533, + 1, + 1.767, + 1, + 1, + 2.033, + 1, + 2.3, + 0, + 2.567, + 0, + 1, + 2.778, + 0, + 2.989, + 1, + 3.2, + 1, + 1, + 3.4, + 1, + 3.6, + 0, + 3.8, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulder", + "Segments": [ + 0, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeg", + "Segments": [ + 0, + 1, + 1, + 0.8, + 1, + 1.6, + 1, + 2.4, + 1, + 1, + 2.522, + 1, + 2.644, + 0, + 2.767, + 0, + 1, + 3.011, + 0, + 3.256, + 0.062, + 3.5, + 0.062, + 0, + 4.233, + 0.062 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + -10, + 1, + 0.256, + -10, + 0.511, + 0, + 0.767, + 0, + 1, + 0.889, + 0, + 1.011, + -6.5, + 1.133, + -6.5, + 1, + 1.289, + -6.5, + 1.444, + 0, + 1.6, + 0, + 1, + 1.756, + 0, + 1.911, + -8, + 2.067, + -8, + 1, + 2.344, + -8, + 2.622, + -1.4, + 2.9, + -1.4, + 1, + 2.978, + -1.4, + 3.056, + -1.775, + 3.133, + -2.3, + 1, + 3.211, + -2.825, + 3.289, + -3, + 3.367, + -3, + 1, + 3.433, + -3, + 3.5, + -2.5, + 3.567, + -2.5, + 1, + 3.733, + -2.5, + 3.9, + -3, + 4.067, + -3, + 0, + 4.233, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + -10, + 1, + 0.256, + -10, + 0.511, + 0, + 0.767, + 0, + 1, + 0.867, + 0, + 0.967, + -5.2, + 1.067, + -5.2, + 1, + 1.244, + -5.2, + 1.422, + 0, + 1.6, + 0, + 1, + 1.778, + 0, + 1.956, + -6.3, + 2.133, + -6.3, + 1, + 2.422, + -6.3, + 2.711, + 0.029, + 3, + 0.029, + 1, + 3.144, + 0.029, + 3.289, + 0, + 3.433, + 0, + 1, + 3.5, + 0, + 3.567, + 0, + 3.633, + 0, + 1, + 3.722, + 0, + 3.811, + -0.029, + 3.9, + -0.029, + 0, + 4.233, + -0.029 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandL", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.256, + 0, + 0.344, + 1, + 0.433, + 1, + 1, + 0.589, + 1, + 0.744, + -1, + 0.9, + -1, + 1, + 1.111, + -1, + 1.322, + 1, + 1.533, + 1, + 1, + 1.733, + 1, + 1.933, + -1, + 2.133, + -1, + 1, + 2.411, + -1, + 2.689, + 1, + 2.967, + 1, + 1, + 3.111, + 1, + 3.256, + -1, + 3.4, + -1, + 1, + 3.5, + -1, + 3.6, + 0.8, + 3.7, + 0.8, + 0, + 4.233, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandR", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.256, + 0, + 0.344, + 1, + 0.433, + 1, + 1, + 0.589, + 1, + 0.744, + -1, + 0.9, + -1, + 1, + 1.111, + -1, + 1.322, + 1, + 1.533, + 1, + 1, + 1.733, + 1, + 1.933, + -1, + 2.133, + -1, + 1, + 2.411, + -1, + 2.689, + 1, + 2.967, + 1, + 1, + 3.111, + 1, + 3.256, + -0.6, + 3.4, + -0.6, + 1, + 3.5, + -0.6, + 3.6, + 0.8, + 3.7, + 0.8, + 0, + 4.233, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairAhoge", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + -3.127, + 0.367, + -4, + 1, + 0.511, + -5.032, + 0.656, + -5, + 0.8, + -5, + 1, + 0.9, + -5, + 1, + 10, + 1.1, + 10, + 1, + 1.267, + 10, + 1.433, + 10.072, + 1.6, + 8, + 1, + 1.722, + 6.481, + 1.844, + -10, + 1.967, + -10, + 1, + 2.122, + -10, + 2.278, + -10.146, + 2.433, + -9.434, + 1, + 2.589, + -8.722, + 2.744, + 10, + 2.9, + 10, + 1, + 3.011, + 10, + 3.122, + -5.646, + 3.233, + -5.646, + 1, + 3.367, + -5.646, + 3.5, + 3.825, + 3.633, + 3.825, + 1, + 3.767, + 3.825, + 3.9, + 0, + 4.033, + 0, + 0, + 4.233, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmA", + "Segments": [ + 0, + 0.999, + 0, + 4.233, + 0.999 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmB", + "Segments": [ + 0, + 0, + 0, + 4.233, + 0 + ] + } + ] +} +``` + +`Content/Samples/Hiyori/motions/Hiyori_m04.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4.467, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": false, + "CurveCount": 31, + "TotalSegmentCount": 106, + "TotalPointCount": 287, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 1, + 1, + 0.211, + 1, + 0.422, + 0, + 0.633, + 0, + 1, + 0.911, + 0, + 1.189, + 5, + 1.467, + 5, + 1, + 1.689, + 5, + 1.911, + -16, + 2.133, + -16, + 1, + 2.356, + -16, + 2.578, + 13.871, + 2.8, + 13.871, + 1, + 2.956, + 13.871, + 3.111, + 0, + 3.267, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + 0, + 0.633, + 0, + 1, + 0.911, + 0, + 1.189, + -25, + 1.467, + -25, + 1, + 1.689, + -25, + 1.911, + -15.225, + 2.133, + -11, + 1, + 2.356, + -6.775, + 2.578, + -5.127, + 2.8, + -2.5, + 1, + 2.956, + -0.661, + 3.111, + 0, + 3.267, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.756, + 0, + 0.844, + -4, + 0.933, + -4, + 1, + 1.122, + -4, + 1.311, + 18, + 1.5, + 18, + 1, + 1.722, + 18, + 1.944, + -14, + 2.167, + -14, + 1, + 2.567, + -14, + 2.967, + -14, + 3.367, + -14, + 1, + 3.511, + -14, + 3.656, + -12, + 3.8, + -12, + 0, + 4.467, + -12 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 1, + 0, + 4.467, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1.2, + 1, + 0.389, + 1.2, + 0.778, + 1.148, + 1.167, + 1, + 1, + 1.211, + 0.983, + 1.256, + 0, + 1.3, + 0, + 1, + 1.322, + 0, + 1.344, + 0, + 1.367, + 0, + 1, + 1.422, + 0, + 1.478, + 1, + 1.533, + 1, + 1, + 1.944, + 1, + 2.356, + 1, + 2.767, + 1, + 1, + 2.811, + 1, + 2.856, + 0, + 2.9, + 0, + 1, + 2.922, + 0, + 2.944, + 0, + 2.967, + 0, + 1, + 3.022, + 0, + 3.078, + 1, + 3.133, + 1, + 0, + 4.467, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1.2, + 1, + 0.389, + 1.2, + 0.778, + 1.148, + 1.167, + 1, + 1, + 1.211, + 0.983, + 1.256, + 0, + 1.3, + 0, + 1, + 1.322, + 0, + 1.344, + 0, + 1.367, + 0, + 1, + 1.422, + 0, + 1.478, + 1, + 1.533, + 1, + 1, + 1.944, + 1, + 2.356, + 1, + 2.767, + 1, + 1, + 2.811, + 1, + 2.856, + 0, + 2.9, + 0, + 1, + 2.922, + 0, + 2.944, + 0, + 2.967, + 0, + 1, + 3.022, + 0, + 3.078, + 1, + 3.133, + 1, + 0, + 4.467, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + 0, + 0.633, + 0, + 1, + 0.911, + 0, + 1.189, + -0.44, + 1.467, + -0.44, + 1, + 1.689, + -0.44, + 1.911, + 0.79, + 2.133, + 0.79, + 1, + 2.511, + 0.79, + 2.889, + 0, + 3.267, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + 0, + 0.633, + 0, + 1, + 0.911, + 0, + 1.189, + -1, + 1.467, + -1, + 1, + 1.689, + -1, + 1.911, + -1, + 2.133, + -1, + 1, + 2.511, + -1, + 2.889, + 0, + 3.267, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 1, + 1, + 0.544, + 1, + 1.089, + 1, + 1.633, + 1, + 1, + 1.856, + 1, + 2.078, + 0, + 2.3, + 0, + 1, + 2.5, + 0, + 2.7, + 1, + 2.9, + 1, + 0, + 4.467, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 1, + 1, + 0.544, + 1, + 1.089, + 1, + 1.633, + 1, + 1, + 1.856, + 1, + 2.078, + 0, + 2.3, + 0, + 1, + 2.5, + 0, + 2.7, + 1, + 2.9, + 1, + 0, + 4.467, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + -1, + 0, + 4.467, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + -1, + 0, + 4.467, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + -2, + 0, + 4.467, + -2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 0, + 0.733, + 0, + 1, + 0.933, + 0, + 1.133, + -7, + 1.333, + -7, + 1, + 1.644, + -7, + 1.956, + 0, + 2.267, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.244, + 0, + 0.489, + 0, + 0.733, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 2, + 1, + 0.233, + 2, + 0.467, + 0, + 0.7, + 0, + 1, + 0.733, + 0, + 0.767, + 0, + 0.8, + 0, + 1, + 1, + 0, + 1.2, + -4, + 1.4, + -4, + 1, + 1.711, + -4, + 2.022, + 5, + 2.333, + 5, + 1, + 2.567, + 5, + 2.8, + 3.64, + 3.033, + 0, + 1, + 3.133, + -1.56, + 3.233, + -3, + 3.333, + -3, + 1, + 3.467, + -3, + 3.6, + -2, + 3.733, + -2, + 0, + 4.467, + -2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 1, + 0.567, + 1, + 1, + 0.711, + 1, + 0.856, + 0, + 1, + 0, + 1, + 1.222, + 0, + 1.444, + 1, + 1.667, + 1, + 1, + 1.889, + 1, + 2.111, + 0, + 2.333, + 0, + 1, + 2.544, + 0, + 2.756, + 1, + 2.967, + 1, + 1, + 3.167, + 1, + 3.367, + 0, + 3.567, + 0, + 0, + 4.467, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulder", + "Segments": [ + 0, + 0.1, + 1, + 0.467, + 0.1, + 0.933, + 1, + 1.4, + 1, + 1, + 1.844, + 1, + 2.289, + 1, + 2.733, + 1, + 1, + 2.967, + 1, + 3.2, + -1, + 3.433, + -1, + 0, + 4.467, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeg", + "Segments": [ + 0, + 1, + 0, + 4.467, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + -10, + 0, + 4.467, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + -10, + 0, + 4.467, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairAhoge", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0, + 0.9, + -0.012, + 1, + 1.067, + -0.019, + 1.233, + -6.827, + 1.4, + -6.827, + 1, + 1.511, + -6.827, + 1.622, + 7.958, + 1.733, + 7.958, + 1, + 1.944, + 7.958, + 2.156, + -7.565, + 2.367, + -7.565, + 1, + 2.5, + -7.565, + 2.633, + 9.434, + 2.767, + 9.434, + 1, + 2.978, + 9.434, + 3.189, + -8.871, + 3.4, + -8.871, + 1, + 3.5, + -8.871, + 3.6, + 7.588, + 3.7, + 7.588, + 1, + 3.789, + 7.588, + 3.878, + -3.904, + 3.967, + -3.904, + 1, + 4.011, + -3.904, + 4.056, + -0.032, + 4.1, + -0.032, + 0, + 4.467, + -0.032 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmA", + "Segments": [ + 0, + 0.997, + 0, + 4.467, + 0.997 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmB", + "Segments": [ + 0, + 0.001, + 0, + 4.467, + 0.001 + ] + } + ] +} +``` + +`Content/Samples/Hiyori/motions/Hiyori_m05.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 8.6, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": false, + "CurveCount": 32, + "TotalSegmentCount": 218, + "TotalPointCount": 622, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 1, + 0.567, + 1, + 1, + 1.122, + 1, + 1.678, + 1, + 2.233, + 1, + 1, + 2.533, + 1, + 2.833, + -30, + 3.133, + -30, + 1, + 4.067, + -30, + 5, + -30, + 5.933, + -30, + 1, + 6.322, + -30, + 6.711, + 0, + 7.1, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 18, + 0.567, + 18, + 1, + 0.856, + 18, + 1.144, + -9.95, + 1.433, + -9.95, + 1, + 1.522, + -9.95, + 1.611, + -1.589, + 1.7, + -1.589, + 1, + 1.911, + -1.589, + 2.122, + -8.047, + 2.333, + -8.047, + 1, + 2.6, + -8.047, + 2.867, + 30, + 3.133, + 30, + 1, + 3.478, + 30, + 3.822, + 29.529, + 4.167, + 27.847, + 1, + 4.311, + 27.142, + 4.456, + 25.42, + 4.6, + 24.243, + 1, + 4.922, + 21.616, + 5.244, + 20.338, + 5.567, + 20.338, + 1, + 5.789, + 20.338, + 6.011, + 27.397, + 6.233, + 27.397, + 1, + 6.522, + 27.397, + 6.811, + 0, + 7.1, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 0, + 0.567, + 0, + 1, + 0.8, + 0, + 1.033, + 25, + 1.267, + 25, + 1, + 1.4, + 25, + 1.533, + 25, + 1.667, + 25, + 1, + 1.878, + 25, + 2.089, + 25, + 2.3, + 25, + 1, + 2.578, + 25, + 2.856, + -30, + 3.133, + -30, + 1, + 4, + -30, + 4.867, + -30, + 5.733, + -30, + 1, + 5.8, + -30, + 5.867, + -30, + 5.933, + -30, + 1, + 6.322, + -30, + 6.711, + 0, + 7.1, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 1, + 2.844, + 0, + 5.089, + 0, + 7.333, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.278, + 1, + 0.556, + 1, + 0.833, + 1, + 1, + 0.867, + 1, + 0.9, + 0, + 0.933, + 0, + 1, + 1.222, + 0, + 1.511, + 0, + 1.8, + 0, + 1, + 1.889, + 0, + 1.978, + 1, + 2.067, + 1, + 1, + 2.322, + 1, + 2.578, + 1, + 2.833, + 1, + 1, + 2.889, + 1, + 2.944, + 0, + 3, + 0, + 1, + 3.022, + 0, + 3.044, + 0, + 3.067, + 0, + 1, + 3.122, + 0, + 3.178, + 1, + 3.233, + 1, + 1, + 3.5, + 1, + 3.767, + 1, + 4.033, + 1, + 1, + 4.089, + 1, + 4.144, + 0, + 4.2, + 0, + 1, + 4.222, + 0, + 4.244, + 0, + 4.267, + 0, + 1, + 4.322, + 0, + 4.378, + 1, + 4.433, + 1, + 1, + 5.133, + 1, + 5.833, + 1, + 6.533, + 1, + 1, + 6.589, + 1, + 6.644, + 0, + 6.7, + 0, + 1, + 6.722, + 0, + 6.744, + 0, + 6.767, + 0, + 1, + 6.822, + 0, + 6.878, + 1, + 6.933, + 1, + 1, + 7.067, + 1, + 7.2, + 1, + 7.333, + 1, + 0, + 8.6, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.278, + 0, + 0.556, + 0.812, + 0.833, + 0.812, + 0, + 8.6, + 0.812 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.278, + 1, + 0.556, + 1, + 0.833, + 1, + 1, + 0.867, + 1, + 0.9, + 0, + 0.933, + 0, + 1, + 1.222, + 0, + 1.511, + 0, + 1.8, + 0, + 1, + 1.889, + 0, + 1.978, + 1, + 2.067, + 1, + 1, + 2.322, + 1, + 2.578, + 1, + 2.833, + 1, + 1, + 2.889, + 1, + 2.944, + 0, + 3, + 0, + 1, + 3.022, + 0, + 3.044, + 0, + 3.067, + 0, + 1, + 3.122, + 0, + 3.178, + 1, + 3.233, + 1, + 1, + 3.5, + 1, + 3.767, + 1, + 4.033, + 1, + 1, + 4.089, + 1, + 4.144, + 0, + 4.2, + 0, + 1, + 4.222, + 0, + 4.244, + 0, + 4.267, + 0, + 1, + 4.322, + 0, + 4.378, + 1, + 4.433, + 1, + 1, + 5.133, + 1, + 5.833, + 1, + 6.533, + 1, + 1, + 6.589, + 1, + 6.644, + 0, + 6.7, + 0, + 1, + 6.722, + 0, + 6.744, + 0, + 6.767, + 0, + 1, + 6.822, + 0, + 6.878, + 1, + 6.933, + 1, + 1, + 7.067, + 1, + 7.2, + 1, + 7.333, + 1, + 0, + 8.6, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.278, + 0, + 0.556, + 0.812, + 0.833, + 0.812, + 1, + 1.967, + 0.812, + 3.1, + 0.8, + 4.233, + 0.8, + 0, + 8.6, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + -0.065, + 0.6, + -0.14, + 1, + 0.811, + -0.219, + 1.022, + -0.24, + 1.233, + -0.24, + 1, + 1.367, + -0.24, + 1.5, + -0.24, + 1.633, + -0.24, + 1, + 1.844, + -0.24, + 2.056, + -0.24, + 2.267, + -0.24, + 1, + 2.556, + -0.24, + 2.844, + 0.661, + 3.133, + 0.68, + 1, + 3.5, + 0.704, + 3.867, + 0.7, + 4.233, + 0.7, + 1, + 4.5, + 0.7, + 4.767, + -0.75, + 5.033, + -0.75, + 1, + 5.589, + -0.75, + 6.144, + 0.68, + 6.7, + 0.68, + 1, + 6.789, + 0.68, + 6.878, + 0, + 6.967, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + -0.38, + 0.6, + -0.38, + 1, + 0.811, + -0.38, + 1.022, + 0.21, + 1.233, + 0.21, + 1, + 1.367, + 0.21, + 1.5, + 0.21, + 1.633, + 0.21, + 1, + 1.844, + 0.21, + 2.056, + 0.21, + 2.267, + 0.21, + 1, + 2.556, + 0.21, + 2.844, + -0.33, + 3.133, + -0.33, + 1, + 3.767, + -0.33, + 4.4, + 0.625, + 5.033, + 0.625, + 1, + 5.589, + 0.625, + 6.144, + -0.33, + 6.7, + -0.33, + 1, + 6.789, + -0.33, + 6.878, + 0, + 6.967, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 1, + 0.656, + 0, + 0.711, + 0, + 0.767, + 0, + 1, + 0.822, + 0, + 0.878, + 0.667, + 0.933, + 0.667, + 1, + 1.211, + 0.667, + 1.489, + 0.667, + 1.767, + 0.667, + 1, + 2.011, + 0.667, + 2.256, + 0, + 2.5, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 1, + 0.656, + 0, + 0.711, + 0, + 0.767, + 0, + 1, + 0.822, + 0, + 0.878, + 0.646, + 0.933, + 0.646, + 1, + 1.211, + 0.646, + 1.489, + 0.646, + 1.767, + 0.646, + 1, + 2.011, + 0.646, + 2.256, + 0, + 2.5, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.2, + 1, + 0.4, + 1, + 0.6, + 1, + 1, + 0.889, + 1, + 1.178, + 1, + 1.467, + 1, + 1, + 1.611, + 1, + 1.756, + 1, + 1.9, + 1, + 1, + 2.189, + 1, + 2.478, + 0, + 2.767, + 0, + 1, + 3.989, + 0, + 5.211, + 0, + 6.433, + 0, + 1, + 6.533, + 0, + 6.633, + 1, + 6.733, + 1, + 0, + 8.6, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 1, + 0.667, + 0, + 0.733, + 0, + 0.8, + 0, + 1, + 0.844, + 0, + 0.889, + 1, + 0.933, + 1, + 1, + 1.111, + 1, + 1.289, + 1, + 1.467, + 1, + 1, + 1.611, + 1, + 1.756, + 0, + 1.9, + 0, + 1, + 2.056, + 0, + 2.211, + 0, + 2.367, + 0, + 1, + 2.5, + 0, + 2.633, + 1, + 2.767, + 1, + 1, + 3.989, + 1, + 5.211, + 1, + 6.433, + 1, + 1, + 6.533, + 1, + 6.633, + 0, + 6.733, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.133, + 0, + 0.267, + 0, + 0.4, + 0, + 1, + 0.5, + 0, + 0.6, + -1.353, + 0.7, + -1.353, + 1, + 0.933, + -1.353, + 1.167, + 10, + 1.4, + 10, + 1, + 1.544, + 10, + 1.689, + 9.799, + 1.833, + 9.799, + 1, + 2.089, + 9.799, + 2.344, + 9.875, + 2.6, + 9.875, + 1, + 2.944, + 9.875, + 3.289, + -9.599, + 3.633, + -9.599, + 1, + 3.844, + -9.599, + 4.056, + -2.657, + 4.267, + -2.657, + 1, + 4.467, + -2.657, + 4.667, + -9.098, + 4.867, + -9.098, + 1, + 5.144, + -9.098, + 5.422, + -4.812, + 5.7, + -4.812, + 1, + 5.967, + -4.812, + 6.233, + -10, + 6.5, + -10, + 1, + 6.778, + -10, + 7.056, + -1.43, + 7.333, + 0, + 1, + 7.533, + 1.03, + 7.733, + 0.752, + 7.933, + 0.752, + 1, + 8.011, + 0.752, + 8.089, + -0.326, + 8.167, + -0.326, + 0, + 8.6, + -0.326 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 5.421, + 0.6, + 5.421, + 1, + 0.867, + 5.421, + 1.133, + -3.737, + 1.4, + -3.737, + 1, + 1.511, + -3.737, + 1.622, + 0.246, + 1.733, + 0.246, + 1, + 1.933, + 0.246, + 2.133, + 0, + 2.333, + 0, + 1, + 2.389, + 0, + 2.444, + 2.993, + 2.5, + 4, + 1, + 2.711, + 7.827, + 2.922, + 9, + 3.133, + 9, + 1, + 4.256, + 9, + 5.378, + 9, + 6.5, + 9, + 1, + 6.778, + 9, + 7.056, + -5, + 7.333, + -5, + 1, + 7.5, + -5, + 7.667, + 2, + 7.833, + 2, + 1, + 7.9, + 2, + 7.967, + 0, + 8.033, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 2, + 0.6, + 2, + 1, + 0.867, + 2, + 1.133, + -10, + 1.4, + -10, + 1, + 1.533, + -10, + 1.667, + -10, + 1.8, + -10, + 1, + 2, + -10, + 2.2, + -10.197, + 2.4, + -6.566, + 1, + 2.6, + -2.936, + 2.8, + 10, + 3, + 10, + 1, + 3.433, + 10, + 3.867, + -4.436, + 4.3, + -4.436, + 1, + 4.578, + -4.436, + 4.856, + 10, + 5.133, + 10, + 1, + 5.433, + 10, + 5.733, + 0.695, + 6.033, + -4.261, + 1, + 6.211, + -7.197, + 6.389, + -6.792, + 6.567, + -6.792, + 1, + 6.644, + -6.792, + 6.722, + -6.224, + 6.8, + -4.11, + 1, + 6.989, + 1.023, + 7.178, + 4.236, + 7.367, + 4.236, + 1, + 7.689, + 4.236, + 8.011, + 0.025, + 8.333, + 0.025, + 0, + 8.6, + 0.025 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + 1, + 0.367, + 1, + 1, + 0.511, + 1, + 0.656, + 0, + 0.8, + 0, + 1, + 1.022, + 0, + 1.244, + 1, + 1.467, + 1, + 1, + 1.689, + 1, + 1.911, + 0, + 2.133, + 0, + 1, + 2.344, + 0, + 2.556, + 1, + 2.767, + 1, + 1, + 2.967, + 1, + 3.167, + 0, + 3.367, + 0, + 1, + 3.578, + 0, + 3.789, + 1, + 4, + 1, + 1, + 4.233, + 1, + 4.467, + 0, + 4.7, + 0, + 1, + 4.9, + 0, + 5.1, + 0, + 5.3, + 0, + 1, + 5.522, + 0, + 5.744, + 1, + 5.967, + 1, + 1, + 6.244, + 1, + 6.522, + 0, + 6.8, + 0, + 1, + 7.022, + 0, + 7.244, + 1, + 7.467, + 1, + 1, + 7.689, + 1, + 7.911, + 0, + 8.133, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulder", + "Segments": [ + 0, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeg", + "Segments": [ + 0, + 1, + 0, + 8.6, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + -10, + 1, + 0.2, + -10, + 0.4, + -10, + 0.6, + -10, + 1, + 0.833, + -10, + 1.067, + -7.083, + 1.3, + -7.083, + 1, + 1.433, + -7.083, + 1.567, + -7.083, + 1.7, + -7.083, + 1, + 1.911, + -7.083, + 2.122, + -10, + 2.333, + -10, + 1, + 3.467, + -10, + 4.6, + -10, + 5.733, + -10, + 1, + 5.8, + -10, + 5.867, + -10, + 5.933, + -10, + 1, + 6.4, + -10, + 6.867, + -10, + 7.333, + -10, + 0, + 8.6, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + -10, + 1, + 0.2, + -10, + 0.4, + -10, + 0.6, + -10, + 1, + 0.833, + -10, + 1.067, + 2.917, + 1.3, + 2.917, + 1, + 1.478, + 2.917, + 1.656, + 3.029, + 1.833, + 2.6, + 1, + 2.033, + 2.117, + 2.233, + -10, + 2.433, + -10, + 1, + 3.533, + -10, + 4.633, + -10, + 5.733, + -10, + 1, + 5.8, + -10, + 5.867, + -10, + 5.933, + -10, + 1, + 6.133, + -10, + 6.333, + -10, + 6.533, + -10, + 1, + 6.8, + -10, + 7.067, + -10, + 7.333, + -10, + 0, + 8.6, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0.02, + 0, + 8.6, + 0.02 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairAhoge", + "Segments": [ + 0, + 0, + 1, + 0.178, + 0, + 0.356, + -10, + 0.533, + -10, + 1, + 0.833, + -10, + 1.133, + 10, + 1.433, + 10, + 1, + 1.567, + 10, + 1.7, + 0, + 1.833, + 0, + 1, + 2.311, + 0, + 2.789, + 10, + 3.267, + 10, + 1, + 3.344, + 10, + 3.422, + 10, + 3.5, + 10, + 1, + 3.633, + 10, + 3.767, + -10, + 3.9, + -10, + 1, + 4.267, + -10, + 4.633, + 7.69, + 5, + 9.705, + 1, + 5.122, + 10.376, + 5.244, + 10, + 5.367, + 10, + 1, + 5.756, + 10, + 6.144, + -9.975, + 6.533, + -9.975, + 1, + 6.622, + -9.975, + 6.711, + -10.207, + 6.8, + -9.779, + 1, + 7.033, + -8.654, + 7.267, + 10, + 7.5, + 10, + 1, + 7.7, + 10, + 7.9, + -6.753, + 8.1, + -6.753, + 1, + 8.2, + -6.753, + 8.3, + 0, + 8.4, + 0, + 0, + 8.6, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmA", + "Segments": [ + 0, + 0.999, + 0, + 8.6, + 0.999 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmB", + "Segments": [ + 0, + 0, + 0, + 8.6, + 0 + ] + } + ] +} +``` + +`Content/Samples/Hiyori/motions/Hiyori_m06.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 5.4, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": false, + "CurveCount": 35, + "TotalSegmentCount": 262, + "TotalPointCount": 751, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 1, + 0.333, + 1, + 1, + 0.7, + 1, + 1.067, + 0, + 1.433, + 0, + 1, + 1.711, + 0, + 1.989, + 10, + 2.267, + 10, + 1, + 2.522, + 10, + 2.778, + -7, + 3.033, + -7, + 1, + 3.233, + -7, + 3.433, + 13, + 3.633, + 13, + 1, + 3.811, + 13, + 3.989, + 7.017, + 4.167, + 2, + 1, + 4.244, + -0.195, + 4.322, + 0, + 4.4, + 0, + 0, + 5.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 18, + 0.333, + 18, + 1, + 0.522, + 18, + 0.711, + -9.95, + 0.9, + -9.95, + 1, + 0.956, + -9.95, + 1.011, + -1.664, + 1.067, + -1.589, + 1, + 1.911, + -0.462, + 2.756, + 0, + 3.6, + 0, + 1, + 3.789, + 0, + 3.978, + -18, + 4.167, + -18, + 1, + 4.244, + -18, + 4.322, + -10, + 4.4, + -10, + 0, + 5.4, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.489, + 0, + 0.644, + 25, + 0.8, + 25, + 1, + 0.878, + 25, + 0.956, + 25, + 1.033, + 25, + 1, + 1.233, + 25, + 1.433, + 25, + 1.633, + 25, + 1, + 1.867, + 25, + 2.1, + -5, + 2.333, + -5, + 1, + 2.533, + -5, + 2.733, + 23, + 2.933, + 23, + 1, + 3.189, + 23, + 3.444, + -3, + 3.7, + -3, + 0, + 5.4, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + 0, + 0.367, + 0, + 0, + 5.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.178, + 1, + 0.356, + 1, + 0.533, + 1, + 1, + 0.567, + 1, + 0.6, + 0, + 0.633, + 0, + 1, + 0.644, + 0, + 0.656, + 0, + 0.667, + 0, + 1, + 0.7, + 0, + 0.733, + 1, + 0.767, + 1, + 1, + 1.056, + 1, + 1.344, + 1, + 1.633, + 1, + 1, + 1.667, + 1, + 1.7, + 0, + 1.733, + 0, + 1, + 1.744, + 0, + 1.756, + 0, + 1.767, + 0, + 1, + 1.8, + 0, + 1.833, + 1, + 1.867, + 1, + 1, + 2.433, + 1, + 3, + 1, + 3.567, + 1, + 1, + 3.6, + 1, + 3.633, + 0, + 3.667, + 0, + 1, + 3.678, + 0, + 3.689, + 0, + 3.7, + 0, + 1, + 3.733, + 0, + 3.767, + 1, + 3.8, + 1, + 1, + 3.944, + 1, + 4.089, + 1, + 4.233, + 1, + 1, + 4.256, + 1, + 4.278, + 0, + 4.3, + 0, + 0, + 5.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 1.378, + 0, + 2.756, + 0, + 4.133, + 0, + 1, + 4.167, + 0, + 4.2, + 0.833, + 4.233, + 0.833, + 0, + 5.4, + 0.833 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.178, + 1, + 0.356, + 1, + 0.533, + 1, + 1, + 0.567, + 1, + 0.6, + 0, + 0.633, + 0, + 1, + 0.644, + 0, + 0.656, + 0, + 0.667, + 0, + 1, + 0.7, + 0, + 0.733, + 1, + 0.767, + 1, + 1, + 1.056, + 1, + 1.344, + 1, + 1.633, + 1, + 1, + 1.667, + 1, + 1.7, + 0, + 1.733, + 0, + 1, + 1.744, + 0, + 1.756, + 0, + 1.767, + 0, + 1, + 1.8, + 0, + 1.833, + 1, + 1.867, + 1, + 1, + 2.433, + 1, + 3, + 1, + 3.567, + 1, + 1, + 3.6, + 1, + 3.633, + 0, + 3.667, + 0, + 1, + 3.678, + 0, + 3.689, + 0, + 3.7, + 0, + 1, + 3.733, + 0, + 3.767, + 0.998, + 3.8, + 1, + 1, + 3.944, + 1.01, + 4.089, + 1.013, + 4.233, + 1.013, + 1, + 4.256, + 1.013, + 4.278, + 0, + 4.3, + 0, + 0, + 5.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 1.378, + 0, + 2.756, + 0, + 4.133, + 0, + 1, + 4.167, + 0, + 4.2, + 0.823, + 4.233, + 0.823, + 0, + 5.4, + 0.823 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + -0.067, + 0.367, + -0.14, + 1, + 0.5, + -0.22, + 0.633, + -0.24, + 0.767, + -0.24, + 1, + 0.856, + -0.24, + 0.944, + -0.24, + 1.033, + -0.24, + 1, + 1.311, + -0.24, + 1.589, + -0.24, + 1.867, + -0.25, + 1, + 2.022, + -0.256, + 2.178, + -0.51, + 2.333, + -0.51, + 1, + 2.578, + -0.51, + 2.822, + 0.22, + 3.067, + 0.22, + 1, + 3.267, + 0.22, + 3.467, + -0.57, + 3.667, + -0.57, + 1, + 3.911, + -0.57, + 4.156, + -0.28, + 4.4, + -0.28, + 0, + 5.4, + -0.28 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + -0.38, + 0.367, + -0.38, + 1, + 0.5, + -0.38, + 0.633, + 0.21, + 0.767, + 0.21, + 1, + 0.856, + 0.21, + 0.944, + 0.21, + 1.033, + 0.21, + 1, + 1.311, + 0.21, + 1.589, + 0.29, + 1.867, + 0.29, + 1, + 2.022, + 0.29, + 2.178, + 0.21, + 2.333, + 0.21, + 1, + 2.578, + 0.21, + 2.822, + 0.29, + 3.067, + 0.29, + 1, + 3.267, + 0.29, + 3.467, + 0.08, + 3.667, + 0.08, + 1, + 3.911, + 0.08, + 4.156, + 0.13, + 4.4, + 0.13, + 0, + 5.4, + 0.13 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.078, + 0, + 0.156, + -0.008, + 0.233, + 0.021, + 1, + 0.322, + 0.053, + 0.411, + 0.625, + 0.5, + 0.625, + 1, + 0.567, + 0.625, + 0.633, + 0, + 0.7, + 0, + 1, + 1.144, + 0, + 1.589, + 0, + 2.033, + 0, + 1, + 2.089, + 0, + 2.144, + 0.229, + 2.2, + 0.229, + 1, + 2.278, + 0.229, + 2.356, + 0, + 2.433, + 0, + 1, + 2.544, + 0, + 2.656, + 0, + 2.767, + 0, + 1, + 2.822, + 0, + 2.878, + 0.229, + 2.933, + 0.229, + 1, + 3.011, + 0.229, + 3.089, + 0, + 3.167, + 0, + 1, + 3.456, + 0, + 3.744, + 0, + 4.033, + 0, + 1, + 4.133, + 0, + 4.233, + 0.479, + 4.333, + 0.479, + 0, + 5.4, + 0.479 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.078, + 0, + 0.156, + 0, + 0.233, + 0, + 1, + 0.322, + 0, + 0.411, + 0.625, + 0.5, + 0.625, + 1, + 0.567, + 0.625, + 0.633, + 0, + 0.7, + 0, + 1, + 1.144, + 0, + 1.589, + 0.005, + 2.033, + 0.021, + 1, + 2.089, + 0.023, + 2.144, + 0.188, + 2.2, + 0.188, + 1, + 2.278, + 0.188, + 2.356, + 0, + 2.433, + 0, + 1, + 2.544, + 0, + 2.656, + 0.001, + 2.767, + 0.021, + 1, + 2.822, + 0.031, + 2.878, + 0.229, + 2.933, + 0.229, + 1, + 3.011, + 0.229, + 3.089, + 0, + 3.167, + 0, + 1, + 3.456, + 0, + 3.744, + 0, + 4.033, + 0, + 1, + 4.133, + 0, + 4.233, + 0.479, + 4.333, + 0.479, + 0, + 5.4, + 0.479 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 1.678, + 0, + 2.856, + 0, + 4.033, + 0, + 1, + 4.133, + 0, + 4.233, + -0.479, + 4.333, + -0.479, + 0, + 5.4, + -0.479 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + 0, + 0.367, + 0, + 1, + 1.589, + 0, + 2.811, + 0, + 4.033, + 0, + 1, + 4.133, + 0, + 4.233, + 0.688, + 4.333, + 0.688, + 0, + 5.4, + 0.688 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 1.678, + 0, + 2.856, + 0, + 4.033, + 0, + 1, + 4.133, + 0, + 4.233, + 0, + 4.333, + 0, + 0, + 5.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + 0, + 0.367, + 0, + 1, + 1.589, + 0, + 2.811, + 0, + 4.033, + 0, + 1, + 4.133, + 0, + 4.233, + 0, + 4.333, + 0, + 0, + 5.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + 0, + 0.367, + 0, + 1, + 1.589, + 0, + 2.811, + 0, + 4.033, + 0, + 1, + 4.133, + 0, + 4.233, + 0, + 4.333, + 0, + 0, + 5.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + 0, + 0.367, + 0, + 1, + 1.589, + 0, + 2.811, + 0, + 4.033, + 0, + 1, + 4.133, + 0, + 4.233, + 0, + 4.333, + 0, + 0, + 5.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 1.367, + 1, + 2.733, + 1, + 4.1, + 1, + 0, + 5.4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 1.367, + 0, + 2.733, + 0, + 4.1, + 0, + 1, + 4.144, + 0, + 4.189, + 1, + 4.233, + 1, + 0, + 5.4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + -10, + 1, + 0.078, + -10, + 0.156, + 0, + 0.233, + 0, + 1, + 0.311, + 0, + 0.389, + -1.353, + 0.467, + -1.353, + 1, + 0.6, + -1.353, + 0.733, + 10, + 0.867, + 10, + 1, + 0.956, + 10, + 1.044, + 9.799, + 1.133, + 9.799, + 1, + 1.233, + 9.799, + 1.333, + 10, + 1.433, + 10, + 1, + 1.667, + 10, + 1.9, + -2.975, + 2.133, + -2.975, + 1, + 2.211, + -2.975, + 2.289, + -2.975, + 2.367, + -2.975, + 1, + 2.522, + -2.975, + 2.678, + 9.025, + 2.833, + 9.025, + 1, + 2.978, + 9.025, + 3.122, + 9.025, + 3.267, + 9.025, + 1, + 3.489, + 9.025, + 3.711, + 2.025, + 3.933, + 2.025, + 0, + 5.4, + 2.025 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 10, + 1, + 0.122, + 10, + 0.244, + 9.141, + 0.367, + 5.421, + 1, + 0.533, + 0.348, + 0.7, + -3.737, + 0.867, + -3.737, + 1, + 0.978, + -3.737, + 1.089, + 2.181, + 1.2, + 2.181, + 1, + 1.3, + 2.181, + 1.4, + 1.948, + 1.5, + 0.298, + 1, + 1.6, + -1.351, + 1.7, + -3.396, + 1.8, + -3.396, + 1, + 1.944, + -3.396, + 2.089, + 4.673, + 2.233, + 4.673, + 1, + 2.378, + 4.673, + 2.522, + -3.858, + 2.667, + -3.858, + 1, + 2.767, + -3.858, + 2.867, + 4.794, + 2.967, + 4.794, + 1, + 3.089, + 4.794, + 3.211, + -3.573, + 3.333, + -3.573, + 1, + 3.533, + -3.573, + 3.733, + 5.27, + 3.933, + 5.27, + 1, + 4.044, + 5.27, + 4.156, + -5.393, + 4.267, + -5.393, + 1, + 4.356, + -5.393, + 4.444, + 0.095, + 4.533, + 0.095, + 0, + 5.4, + 0.095 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + 2, + 0.367, + 2, + 1, + 0.533, + 2, + 0.7, + -10, + 0.867, + -10, + 1, + 1.078, + -10, + 1.289, + -10.013, + 1.5, + -9.975, + 1, + 1.767, + -9.927, + 2.033, + 2.026, + 2.3, + 2.026, + 1, + 2.522, + 2.026, + 2.744, + -9.975, + 2.967, + -9.975, + 1, + 3.322, + -9.975, + 3.678, + -6.592, + 4.033, + 1.8, + 1, + 4.178, + 5.209, + 4.322, + 8, + 4.467, + 8, + 0, + 5.4, + 8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + 1, + 0.367, + 1, + 1, + 0.511, + 1, + 0.656, + 0, + 0.8, + 0, + 1, + 1.022, + 0, + 1.244, + 1, + 1.467, + 1, + 1, + 1.689, + 1, + 1.911, + 0, + 2.133, + 0, + 1, + 2.344, + 0, + 2.556, + 1, + 2.767, + 1, + 1, + 2.967, + 1, + 3.167, + 0, + 3.367, + 0, + 1, + 3.578, + 0, + 3.789, + 1, + 4, + 1, + 1, + 4.233, + 1, + 4.467, + 0, + 4.7, + 0, + 0, + 5.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulder", + "Segments": [ + 0, + 0, + 1, + 0.144, + 0, + 0.289, + 1, + 0.433, + 1, + 1, + 0.6, + 1, + 0.767, + 0.049, + 0.933, + -0.1, + 1, + 1.167, + -0.309, + 1.4, + -0.3, + 1.633, + -0.3, + 1, + 1.844, + -0.3, + 2.056, + 0.9, + 2.267, + 0.9, + 1, + 2.411, + 0.9, + 2.556, + -0.22, + 2.7, + -0.22, + 1, + 2.844, + -0.22, + 2.989, + 0.6, + 3.133, + 0.6, + 1, + 3.289, + 0.6, + 3.444, + -0.4, + 3.6, + -0.4, + 1, + 3.7, + -0.4, + 3.8, + 0.5, + 3.9, + 0.5, + 1, + 4.044, + 0.5, + 4.189, + 0.238, + 4.333, + 0, + 1, + 4.444, + -0.183, + 4.556, + -0.2, + 4.667, + -0.2, + 0, + 5.4, + -0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeg", + "Segments": [ + 0, + 1, + 1, + 1.344, + 1, + 2.689, + 1, + 4.033, + 1, + 1, + 4.144, + 1, + 4.256, + 0, + 4.367, + 0, + 1, + 4.578, + 0, + 4.789, + 0.052, + 5, + 0.052, + 0, + 5.4, + 0.052 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 1.3, + 0.3, + 1.3, + 1, + 0.544, + 1.3, + 0.789, + 0, + 1.033, + 0, + 1, + 1.222, + 0, + 1.411, + 0, + 1.6, + 0, + 1, + 1.811, + 0, + 2.022, + -10, + 2.233, + -10, + 1, + 2.811, + -10, + 3.389, + -10, + 3.967, + -10, + 1, + 4.133, + -10, + 4.3, + -6.458, + 4.467, + -6.458, + 0, + 5.4, + -6.458 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 1.9, + 0.3, + 1.9, + 1, + 0.544, + 1.9, + 0.789, + 0, + 1.033, + 0, + 1, + 1.222, + 0, + 1.411, + 0, + 1.6, + 0, + 1, + 1.811, + 0, + 2.022, + -10, + 2.233, + -10, + 1, + 2.7, + -10, + 3.167, + -10, + 3.633, + -10, + 1, + 3.744, + -10, + 3.856, + -10, + 3.967, + -10, + 1, + 4.133, + -10, + 4.3, + -5.833, + 4.467, + -5.833, + 0, + 5.4, + -5.833 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLB", + "Segments": [ + 0, + 0, + 1, + 0.156, + 0, + 0.311, + 0, + 0.467, + 0, + 1, + 0.656, + 0, + 0.844, + 6.25, + 1.033, + 6.25, + 1, + 1.222, + 6.25, + 1.411, + 6.365, + 1.6, + 5.833, + 1, + 1.811, + 5.239, + 2.022, + -4.07, + 2.233, + -4.583, + 1, + 2.367, + -4.907, + 2.5, + -4.792, + 2.633, + -4.792, + 1, + 2.778, + -4.792, + 2.922, + -3.75, + 3.067, + -3.75, + 1, + 3.256, + -3.75, + 3.444, + -6.25, + 3.633, + -6.25, + 1, + 3.756, + -6.25, + 3.878, + -6.419, + 4, + -5.833, + 1, + 4.156, + -5.088, + 4.311, + 9.4, + 4.467, + 9.4, + 1, + 4.533, + 9.4, + 4.6, + 7.1, + 4.667, + 7.1, + 0, + 5.4, + 7.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandRB", + "Segments": [ + 0, + 0, + 1, + 0.156, + 0, + 0.311, + 0, + 0.467, + 0, + 1, + 0.656, + 0, + 0.844, + 8.064, + 1.033, + 8.333, + 1, + 1.222, + 8.603, + 1.411, + 8.542, + 1.6, + 8.542, + 1, + 1.811, + 8.542, + 2.022, + -5.8, + 2.233, + -7.292, + 1, + 2.367, + -8.234, + 2.5, + -7.917, + 2.633, + -7.917, + 1, + 2.778, + -7.917, + 2.922, + -7.917, + 3.067, + -7.917, + 1, + 3.256, + -7.917, + 3.444, + -8.084, + 3.633, + -8.333, + 1, + 3.756, + -8.494, + 3.878, + -8.542, + 4, + -8.542, + 1, + 4.156, + -8.542, + 4.311, + 10, + 4.467, + 10, + 1, + 4.533, + 10, + 4.6, + 6.8, + 4.667, + 6.8, + 0, + 5.4, + 6.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandL", + "Segments": [ + 0, + 0, + 1, + 0.144, + 0, + 0.289, + 0.958, + 0.433, + 0.958, + 1, + 0.611, + 0.958, + 0.789, + -1, + 0.967, + -1, + 1, + 1.222, + -1, + 1.478, + 0.4, + 1.733, + 0.4, + 1, + 1.922, + 0.4, + 2.111, + -1, + 2.3, + -1, + 1, + 2.544, + -1, + 2.789, + 1, + 3.033, + 1, + 1, + 3.344, + 1, + 3.656, + -1, + 3.967, + -1, + 1, + 4.222, + -1, + 4.478, + 0.4, + 4.733, + 0.4, + 0, + 5.4, + 0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandR", + "Segments": [ + 0, + 0, + 1, + 0.144, + 0, + 0.289, + 1, + 0.433, + 1, + 1, + 0.611, + 1, + 0.789, + -1, + 0.967, + -1, + 1, + 1.222, + -1, + 1.478, + 0.4, + 1.733, + 0.4, + 1, + 1.922, + 0.4, + 2.111, + -1, + 2.3, + -1, + 1, + 2.544, + -1, + 2.789, + 1, + 3.033, + 1, + 1, + 3.344, + 1, + 3.656, + -1, + 3.967, + -1, + 1, + 4.222, + -1, + 4.478, + 0.3, + 4.733, + 0.3, + 0, + 5.4, + 0.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairAhoge", + "Segments": [ + 0, + 0, + 1, + 0.022, + 0, + 0.044, + 0, + 0.067, + 0, + 1, + 0.144, + 0, + 0.222, + -10, + 0.3, + -10, + 1, + 0.456, + -10, + 0.611, + 8.967, + 0.767, + 8.967, + 1, + 0.889, + 8.967, + 1.011, + -5.867, + 1.133, + -5.867, + 1, + 1.244, + -5.867, + 1.356, + -3.178, + 1.467, + -0.111, + 1, + 1.522, + 1.423, + 1.578, + 1.562, + 1.633, + 1.562, + 1, + 1.733, + 1.562, + 1.833, + -4.563, + 1.933, + -4.563, + 1, + 2.078, + -4.563, + 2.222, + 10, + 2.367, + 10, + 1, + 2.411, + 10, + 2.456, + 10.159, + 2.5, + 9.828, + 1, + 2.6, + 9.082, + 2.7, + -8.352, + 2.8, + -8.352, + 1, + 2.867, + -8.352, + 2.933, + -8.632, + 3, + -7.761, + 1, + 3.111, + -6.31, + 3.222, + 6.63, + 3.333, + 6.63, + 1, + 3.389, + 6.63, + 3.444, + -4.883, + 3.5, + -4.883, + 1, + 3.544, + -4.883, + 3.589, + -5.066, + 3.633, + -4.785, + 1, + 3.811, + -3.661, + 3.989, + 9.803, + 4.167, + 9.803, + 1, + 4.256, + 9.803, + 4.344, + -9.656, + 4.433, + -9.656, + 1, + 4.567, + -9.656, + 4.7, + 5.375, + 4.833, + 5.375, + 1, + 4.967, + 5.375, + 5.1, + 0, + 5.233, + 0, + 0, + 5.4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmA", + "Segments": [ + 0, + 0.001, + 0, + 5.4, + 0.001 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmB", + "Segments": [ + 0, + 0.999, + 0, + 5.4, + 0.999 + ] + } + ] +} +``` + +`Content/Samples/Hiyori/motions/Hiyori_m07.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 1.933, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": false, + "CurveCount": 32, + "TotalSegmentCount": 121, + "TotalPointCount": 331, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 0, + 1.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 0, + 1.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.444, + 0, + 0.556, + 8, + 0.667, + 8, + 0, + 1.933, + 8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 0, + 1.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.111, + 1, + 0.222, + 1, + 0.333, + 1, + 1, + 0.378, + 1, + 0.422, + 0, + 0.467, + 0, + 1, + 0.522, + 0, + 0.578, + 1.2, + 0.633, + 1.2, + 1, + 0.744, + 1.2, + 0.856, + 1.2, + 0.967, + 1.2, + 1, + 0.989, + 1.2, + 1.011, + 0, + 1.033, + 0, + 1, + 1.067, + 0, + 1.1, + 1.2, + 1.133, + 1.2, + 1, + 1.167, + 1.2, + 1.2, + 1.2, + 1.233, + 1.2, + 1, + 1.267, + 1.2, + 1.3, + 0, + 1.333, + 0, + 1, + 1.356, + 0, + 1.378, + 1.2, + 1.4, + 1.2, + 0, + 1.933, + 1.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 0, + 1.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.111, + 1, + 0.222, + 1, + 0.333, + 1, + 1, + 0.378, + 1, + 0.422, + 0, + 0.467, + 0, + 1, + 0.522, + 0, + 0.578, + 1.2, + 0.633, + 1.2, + 1, + 0.744, + 1.2, + 0.856, + 1.2, + 0.967, + 1.2, + 1, + 0.989, + 1.2, + 1.011, + 0, + 1.033, + 0, + 1, + 1.067, + 0, + 1.1, + 1.2, + 1.133, + 1.2, + 1, + 1.167, + 1.2, + 1.2, + 1.2, + 1.233, + 1.2, + 1, + 1.267, + 1.2, + 1.3, + 0, + 1.333, + 0, + 1, + 1.356, + 0, + 1.378, + 1.2, + 1.4, + 1.2, + 0, + 1.933, + 1.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.356, + 0, + 0.378, + 0, + 0.4, + 0, + 1, + 0.489, + 0, + 0.578, + 0, + 0.667, + 0, + 0, + 1.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.356, + 0, + 0.378, + 0, + 0.4, + 0, + 1, + 0.489, + 0, + 0.578, + 0, + 0.667, + 0, + 0, + 1.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.356, + 0, + 0.378, + 0, + 0.4, + 0, + 1, + 0.489, + 0, + 0.578, + 0, + 0.667, + 0, + 0, + 1.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.356, + 0, + 0.378, + 0, + 0.4, + 0, + 1, + 0.489, + 0, + 0.578, + 0.26, + 0.667, + 0.26, + 0, + 1.933, + 0.26 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.356, + 0, + 0.378, + 0, + 0.4, + 0, + 1, + 0.489, + 0, + 0.578, + 0.36, + 0.667, + 0.36, + 0, + 1.933, + 0.36 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.356, + 0, + 0.378, + 0, + 0.4, + 0, + 1, + 0.489, + 0, + 0.578, + 0, + 0.667, + 0, + 0, + 1.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.356, + 0, + 0.378, + 0, + 0.4, + 0, + 1, + 0.489, + 0, + 0.578, + 0.27, + 0.667, + 0.27, + 0, + 1.933, + 0.27 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.356, + 0, + 0.378, + 0, + 0.4, + 0, + 1, + 0.489, + 0, + 0.578, + 0.26, + 0.667, + 0.26, + 0, + 1.933, + 0.26 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.356, + 0, + 0.378, + 0, + 0.4, + 0, + 1, + 0.489, + 0, + 0.578, + -0.03, + 0.667, + -0.03, + 0, + 1.933, + -0.03 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.356, + 0, + 0.378, + 0, + 0.4, + 0, + 1, + 0.489, + 0, + 0.578, + 0.33, + 0.667, + 0.33, + 0, + 1.933, + 0.33 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.356, + 0, + 0.378, + 0, + 0.4, + 0, + 1, + 0.489, + 0, + 0.578, + 0.21, + 0.667, + 0.21, + 0, + 1.933, + 0.21 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.111, + 1, + 0.222, + 1, + 0.333, + 1, + 1, + 0.356, + 1, + 0.378, + 1, + 0.4, + 1, + 1, + 0.489, + 1, + 0.578, + -2, + 0.667, + -2, + 0, + 1.933, + -2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.356, + 0, + 0.378, + 0, + 0.4, + 0, + 1, + 0.489, + 0, + 0.578, + 0.75, + 0.667, + 0.75, + 0, + 1.933, + 0.75 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.444, + 0, + 0.556, + -6, + 0.667, + -6, + 0, + 1.933, + -6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.422, + 0, + 0.511, + 10, + 0.6, + 10, + 1, + 0.667, + 10, + 0.733, + -6, + 0.8, + -6, + 1, + 0.833, + -6, + 0.867, + 5, + 0.9, + 5, + 1, + 1.011, + 5, + 1.122, + 0, + 1.233, + 0, + 0, + 1.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.444, + 0, + 0.556, + -3, + 0.667, + -3, + 0, + 1.933, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 0, + 1.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulder", + "Segments": [ + 0, + -0.062, + 1, + 0.111, + -0.062, + 0.222, + -0.103, + 0.333, + 0, + 1, + 0.589, + 0.238, + 0.844, + 1, + 1.1, + 1, + 0, + 1.933, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.478, + 0, + 0.622, + -10, + 0.767, + -10, + 1, + 0.811, + -10, + 0.856, + -8.2, + 0.9, + -8.2, + 0, + 1.933, + -8.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.478, + 0, + 0.622, + -10, + 0.767, + -10, + 1, + 0.811, + -10, + 0.856, + -7.2, + 0.9, + -7.2, + 0, + 1.933, + -7.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 0, + 1.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 0, + 1.933, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairAhoge", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 1.9, + 0.333, + 5.2, + 1, + 0.444, + 8.5, + 0.556, + 9.926, + 0.667, + 9.926, + 1, + 0.744, + 9.926, + 0.822, + -10, + 0.9, + -10, + 1, + 0.956, + -10, + 1.011, + 6, + 1.067, + 6, + 1, + 1.144, + 6, + 1.222, + -4, + 1.3, + -4, + 1, + 1.367, + -4, + 1.433, + 0, + 1.5, + 0, + 0, + 1.933, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmA", + "Segments": [ + 0, + 0.999, + 0, + 1.933, + 0.999 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmB", + "Segments": [ + 0, + 0, + 0, + 1.933, + 0 + ] + } + ] +} +``` + +`Content/Samples/Hiyori/motions/Hiyori_m08.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 2.133, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": false, + "CurveCount": 35, + "TotalSegmentCount": 133, + "TotalPointCount": 364, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.244, + 0, + 0.289, + 0, + 0.333, + 0, + 1, + 0.378, + 0, + 0.422, + 0, + 0.467, + 0, + 1, + 0.556, + 0, + 0.644, + 0, + 0.733, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.244, + 0, + 0.289, + 0, + 0.333, + 0, + 1, + 0.378, + 0, + 0.422, + -15, + 0.467, + -15, + 1, + 0.556, + -15, + 0.644, + 11, + 0.733, + 11, + 0, + 2.133, + 11 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.244, + 0, + 0.289, + 0, + 0.333, + 0, + 1, + 0.444, + 0, + 0.556, + -15, + 0.667, + -15, + 1, + 0.722, + -15, + 0.778, + -9, + 0.833, + -9, + 0, + 2.133, + -9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0, + 0.433, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.067, + 1, + 0.133, + 1, + 0.2, + 1, + 1, + 0.244, + 1, + 0.289, + 1, + 0.333, + 1, + 1, + 0.456, + 1, + 0.578, + 0, + 0.7, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.244, + 0, + 0.289, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 1, + 0.7, + 1, + 0, + 2.133, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.067, + 1, + 0.133, + 1, + 0.2, + 1, + 1, + 0.244, + 1, + 0.289, + 1, + 0.333, + 1, + 1, + 0.456, + 1, + 0.578, + 0, + 0.7, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.244, + 0, + 0.289, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + 1, + 0.7, + 1, + 0, + 2.133, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.244, + 0, + 0.289, + 0, + 0.333, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.244, + 0, + 0.289, + 0, + 0.333, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.244, + 0, + 0.289, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + -0.14, + 0.7, + -0.14, + 0, + 2.133, + -0.14 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.244, + 0, + 0.289, + 0, + 0.333, + 0, + 1, + 0.456, + 0, + 0.578, + -0.14, + 0.7, + -0.14, + 0, + 2.133, + -0.14 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0, + 0.433, + 0, + 1, + 0.522, + 0, + 0.611, + -0.21, + 0.7, + -0.21, + 0, + 2.133, + -0.21 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0, + 0.433, + 0, + 1, + 0.522, + 0, + 0.611, + 0.2, + 0.7, + 0.2, + 0, + 2.133, + 0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0, + 0.433, + 0, + 1, + 0.522, + 0, + 0.611, + 0.25, + 0.7, + 0.25, + 0, + 2.133, + 0.25 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0, + 0.433, + 0, + 1, + 0.522, + 0, + 0.611, + 0.23, + 0.7, + 0.23, + 0, + 2.133, + 0.23 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0, + 0.433, + 0, + 1, + 0.522, + 0, + 0.611, + 0, + 0.7, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0, + 0.433, + 0, + 1, + 0.522, + 0, + 0.611, + 0, + 0.7, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.067, + 1, + 0.133, + 1, + 0.2, + 1, + 1, + 0.278, + 1, + 0.356, + 1, + 0.433, + 1, + 1, + 0.522, + 1, + 0.611, + 1, + 0.7, + 1, + 0, + 2.133, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0, + 0.433, + 0, + 1, + 0.522, + 0, + 0.611, + 1, + 0.7, + 1, + 0, + 2.133, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 8, + 0.433, + 8, + 1, + 0.5, + 8, + 0.567, + -7, + 0.633, + -7, + 1, + 0.689, + -7, + 0.744, + 3, + 0.8, + 3, + 1, + 0.844, + 3, + 0.889, + 0, + 0.933, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0, + 0.433, + 0, + 1, + 0.511, + 0, + 0.589, + 4, + 0.667, + 4, + 1, + 0.722, + 4, + 0.778, + 2, + 0.833, + 2, + 0, + 2.133, + 2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0, + 0.433, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulder", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + -1, + 0.433, + -1, + 1, + 0.5, + -1, + 0.567, + 1, + 0.633, + 1, + 1, + 0.689, + 1, + 0.744, + -0.4, + 0.8, + -0.4, + 1, + 0.956, + -0.4, + 1.111, + 0, + 1.267, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeg", + "Segments": [ + 0, + 1, + 0, + 2.133, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.244, + 0, + 0.289, + -5.208, + 0.333, + -5.208, + 1, + 0.4, + -5.208, + 0.467, + 9.583, + 0.533, + 9.583, + 0, + 2.133, + 9.583 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.244, + 0, + 0.289, + -4.583, + 0.333, + -4.583, + 1, + 0.4, + -4.583, + 0.467, + 4.375, + 0.533, + 4.375, + 0, + 2.133, + 4.375 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLB", + "Segments": [ + 0, + 10, + 1, + 0.122, + 10, + 0.244, + 8.89, + 0.367, + 4.167, + 1, + 0.411, + 2.449, + 0.456, + -4.583, + 0.5, + -4.583, + 1, + 0.544, + -4.583, + 0.589, + 4.792, + 0.633, + 4.792, + 0, + 2.133, + 4.792 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandRB", + "Segments": [ + 0, + 10, + 1, + 0.122, + 10, + 0.244, + 8.89, + 0.367, + 4.167, + 1, + 0.411, + 2.449, + 0.456, + -4.583, + 0.5, + -4.583, + 1, + 0.544, + -4.583, + 0.589, + 7.292, + 0.633, + 7.292, + 0, + 2.133, + 7.292 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairAhoge", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.244, + 0, + 0.289, + -0.142, + 0.333, + 0.234, + 1, + 0.411, + 0.89, + 0.489, + 10, + 0.567, + 10, + 1, + 0.611, + 10, + 0.656, + -10, + 0.7, + -10, + 1, + 0.767, + -10, + 0.833, + 9.951, + 0.9, + 9.951, + 1, + 0.978, + 9.951, + 1.056, + -9.508, + 1.133, + -9.508, + 1, + 1.211, + -9.508, + 1.289, + 5.892, + 1.367, + 5.892, + 1, + 1.433, + 5.892, + 1.5, + -2, + 1.567, + -2, + 1, + 1.611, + -2, + 1.656, + 1, + 1.7, + 1, + 1, + 1.756, + 1, + 1.811, + 0, + 1.867, + 0, + 0, + 2.133, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmA", + "Segments": [ + 0, + 0.001, + 0, + 2.133, + 0.001 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmB", + "Segments": [ + 0, + 0.995, + 0, + 2.133, + 0.995 + ] + } + ] +} +``` + +`Content/Samples/Hiyori/motions/Hiyori_m09.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 1.633, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": false, + "CurveCount": 33, + "TotalSegmentCount": 150, + "TotalPointCount": 417, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.333, + 0, + 0.467, + -4, + 0.6, + -4, + 0, + 1.633, + -4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.289, + 0, + 0.378, + -20, + 0.467, + -20, + 1, + 0.522, + -20, + 0.578, + 2, + 0.633, + 2, + 1, + 0.667, + 2, + 0.7, + 0, + 0.733, + 0, + 1, + 0.8, + 0, + 0.867, + 0, + 0.933, + 0, + 1, + 1, + 0, + 1.067, + -21, + 1.133, + -21, + 1, + 1.167, + -21, + 1.2, + -17, + 1.233, + -17, + 0, + 1.633, + -17 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 0, + 1.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.356, + 0, + 0.511, + 0, + 0.667, + 0, + 1, + 0.778, + 0, + 0.889, + 1, + 1, + 1, + 0, + 1.633, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.067, + 1, + 0.133, + 1, + 0.2, + 1, + 1, + 0.244, + 1, + 0.289, + 1, + 0.333, + 1, + 1, + 0.378, + 1, + 0.422, + 0, + 0.467, + 0, + 1, + 0.489, + 0, + 0.511, + 0, + 0.533, + 0, + 1, + 0.578, + 0, + 0.622, + 1, + 0.667, + 1, + 1, + 0.8, + 1, + 0.933, + 1, + 1.067, + 1, + 1, + 1.122, + 1, + 1.178, + 0.825, + 1.233, + 0.825, + 0, + 1.633, + 0.825 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.544, + 0, + 0.889, + 0, + 1.233, + 0, + 0, + 1.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.067, + 1, + 0.133, + 1, + 0.2, + 1, + 1, + 0.244, + 1, + 0.289, + 1, + 0.333, + 1, + 1, + 0.378, + 1, + 0.422, + 0, + 0.467, + 0, + 1, + 0.489, + 0, + 0.511, + 0, + 0.533, + 0, + 1, + 0.578, + 0, + 0.622, + 0.996, + 0.667, + 1, + 1, + 0.8, + 1.011, + 0.933, + 1.013, + 1.067, + 1.013, + 1, + 1.122, + 1.013, + 1.178, + 0.8, + 1.233, + 0.8, + 0, + 1.633, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.322, + 0, + 0.444, + 0, + 0.567, + 0, + 1, + 0.789, + 0, + 1.011, + 0, + 1.233, + 0, + 0, + 1.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.322, + 0, + 0.444, + 0, + 0.567, + 0, + 1, + 0.789, + 0, + 1.011, + 0, + 1.233, + 0, + 0, + 1.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.322, + 0, + 0.444, + 0, + 0.567, + 0, + 1, + 0.789, + 0, + 1.011, + 0, + 1.233, + 0, + 0, + 1.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.322, + 0, + 0.444, + 1, + 0.567, + 1, + 1, + 0.733, + 1, + 0.9, + 1, + 1.067, + 1, + 1, + 1.122, + 1, + 1.178, + -0.417, + 1.233, + -0.417, + 0, + 1.633, + -0.417 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.322, + 0, + 0.444, + 1, + 0.567, + 1, + 1, + 0.733, + 1, + 0.9, + 1, + 1.067, + 1, + 1, + 1.122, + 1, + 1.178, + -1, + 1.233, + -1, + 0, + 1.633, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.322, + 0, + 0.444, + 0, + 0.567, + 0, + 1, + 0.789, + 0, + 1.011, + 0, + 1.233, + 0, + 0, + 1.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.322, + 0, + 0.444, + 0, + 0.567, + 0, + 1, + 0.789, + 0, + 1.011, + 0, + 1.233, + 0, + 0, + 1.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.322, + 0, + 0.444, + -1, + 0.567, + -1, + 1, + 0.789, + -1, + 1.011, + -1, + 1.233, + -1, + 0, + 1.633, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.322, + 0, + 0.444, + -1, + 0.567, + -1, + 1, + 0.789, + -1, + 1.011, + -1, + 1.233, + -1, + 0, + 1.633, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.322, + 0, + 0.444, + -1, + 0.567, + -1, + 1, + 0.789, + -1, + 1.011, + -1, + 1.233, + -1, + 0, + 1.633, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.322, + 0, + 0.444, + -1, + 0.567, + -1, + 1, + 0.789, + -1, + 1.011, + -1, + 1.233, + -1, + 0, + 1.633, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.067, + 1, + 0.133, + 1, + 0.2, + 1, + 1, + 0.322, + 1, + 0.444, + -2, + 0.567, + -2, + 1, + 0.789, + -2, + 1.011, + -2, + 1.233, + -2, + 0, + 1.633, + -2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.322, + 0, + 0.444, + 1, + 0.567, + 1, + 1, + 0.656, + 1, + 0.744, + 1, + 0.833, + 1, + 1, + 0.889, + 1, + 0.944, + 1, + 1, + 1, + 1, + 1.044, + 1, + 1.089, + 0, + 1.133, + 0, + 1, + 1.244, + 0, + 1.356, + 0, + 1.467, + 0, + 0, + 1.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.156, + 0, + 0.311, + 0, + 0.467, + 0, + 1, + 0.556, + 0, + 0.644, + -10, + 0.733, + -10, + 1, + 0.833, + -10, + 0.933, + -9, + 1.033, + -9, + 0, + 1.633, + -9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + -4, + 0.433, + -4, + 1, + 0.522, + -4, + 0.611, + 5, + 0.7, + 5, + 1, + 0.789, + 5, + 0.878, + 5, + 0.967, + 5, + 1, + 1.044, + 5, + 1.122, + -7, + 1.2, + -7, + 1, + 1.267, + -7, + 1.333, + 0, + 1.4, + 0, + 0, + 1.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 0, + 1.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 0, + 1.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulder", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.367, + 0, + 0.533, + 0.479, + 0.7, + 0.479, + 1, + 0.889, + 0.479, + 1.078, + -1, + 1.267, + -1, + 1, + 1.322, + -1, + 1.378, + -1, + 1.433, + -1, + 0, + 1.633, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeg", + "Segments": [ + 0, + 1, + 0, + 1.633, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.233, + 0, + 0.267, + 0, + 0.3, + 0, + 1, + 0.367, + 0, + 0.433, + -4.5, + 0.5, + -4.5, + 1, + 0.578, + -4.5, + 0.656, + 0.8, + 0.733, + 0.8, + 1, + 0.811, + 0.8, + 0.889, + -0.156, + 0.967, + -0.6, + 1, + 1.011, + -0.854, + 1.056, + -0.72, + 1.1, + -1, + 1, + 1.167, + -1.42, + 1.233, + -3.7, + 1.3, + -3.7, + 1, + 1.356, + -3.7, + 1.411, + -2.4, + 1.467, + -2.4, + 0, + 1.633, + -2.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.233, + 0, + 0.267, + 0, + 0.3, + 0, + 1, + 0.367, + 0, + 0.433, + -4.5, + 0.5, + -4.5, + 1, + 0.578, + -4.5, + 0.656, + 2.2, + 0.733, + 2.2, + 1, + 0.811, + 2.2, + 0.889, + -0.749, + 0.967, + -0.9, + 1, + 1.011, + -0.987, + 1.056, + -0.921, + 1.1, + -1, + 1, + 1.167, + -1.119, + 1.233, + -3.6, + 1.3, + -3.6, + 1, + 1.356, + -3.6, + 1.411, + -1.8, + 1.467, + -1.8, + 0, + 1.633, + -1.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 0, + 1.633, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.378, + 0, + 0.556, + 0.84, + 0.733, + 0.84, + 1, + 0.744, + 0.84, + 0.756, + 0.65, + 0.767, + 0.65, + 1, + 0.8, + 0.65, + 0.833, + 0.86, + 0.867, + 0.86, + 0, + 1.633, + 0.86 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairAhoge", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.289, + 0, + 0.378, + 6, + 0.467, + 6, + 1, + 0.5, + 6, + 0.533, + 4, + 0.567, + 0, + 1, + 0.622, + -6.667, + 0.678, + -10, + 0.733, + -10, + 1, + 0.767, + -10, + 0.8, + -6, + 0.833, + -6, + 1, + 0.933, + -6, + 1.033, + -10, + 1.133, + -10, + 1, + 1.178, + -10, + 1.222, + -2, + 1.267, + -2, + 1, + 1.311, + -2, + 1.356, + -9, + 1.4, + -9, + 0, + 1.633, + -9 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmA", + "Segments": [ + 0, + 0.998, + 0, + 1.633, + 0.998 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmB", + "Segments": [ + 0, + 0, + 0, + 1.633, + 0 + ] + } + ] +} +``` + +`Content/Samples/Hiyori/motions/Hiyori_m10.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4.2, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": false, + "CurveCount": 33, + "TotalSegmentCount": 118, + "TotalPointCount": 321, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.4, + 0, + 0.6, + 0, + 0.8, + 0, + 1, + 1.067, + 0, + 1.333, + 1.041, + 1.6, + 1.041, + 1, + 1.844, + 1.041, + 2.089, + -8, + 2.333, + -8, + 1, + 2.656, + -8, + 2.978, + 6, + 3.3, + 6, + 0, + 4.2, + 6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.344, + 0, + 0.489, + -30, + 0.633, + -30, + 0, + 4.2, + -30 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 0, + 4.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 0, + 4.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.067, + 1, + 0.133, + 1, + 0.2, + 1, + 1, + 0.311, + 1, + 0.422, + 0.988, + 0.533, + 0.8, + 1, + 0.589, + 0.706, + 0.644, + 0, + 0.7, + 0, + 1, + 0.722, + 0, + 0.744, + 0, + 0.767, + 0, + 1, + 0.822, + 0, + 0.878, + 0.8, + 0.933, + 0.8, + 1, + 1.422, + 0.8, + 1.911, + 0.8, + 2.4, + 0.8, + 1, + 2.456, + 0.8, + 2.511, + 0, + 2.567, + 0, + 1, + 2.589, + 0, + 2.611, + 0, + 2.633, + 0, + 1, + 2.689, + 0, + 2.744, + 0.8, + 2.8, + 0.8, + 0, + 4.2, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 0, + 4.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.067, + 1, + 0.133, + 1, + 0.2, + 1, + 1, + 0.311, + 1, + 0.422, + 0.988, + 0.533, + 0.8, + 1, + 0.589, + 0.706, + 0.644, + 0, + 0.7, + 0, + 1, + 0.722, + 0, + 0.744, + 0, + 0.767, + 0, + 1, + 0.822, + 0, + 0.878, + 0.8, + 0.933, + 0.8, + 1, + 1.422, + 0.8, + 1.911, + 0.8, + 2.4, + 0.8, + 1, + 2.456, + 0.8, + 2.511, + 0, + 2.567, + 0, + 1, + 2.589, + 0, + 2.611, + 0, + 2.633, + 0, + 1, + 2.689, + 0, + 2.744, + 0.8, + 2.8, + 0.8, + 0, + 4.2, + 0.8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0, + 0.433, + 0, + 0, + 4.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0, + 0.433, + 0, + 1, + 0.667, + 0, + 0.9, + 0.004, + 1.133, + -0.01, + 1, + 1.4, + -0.025, + 1.667, + -0.43, + 1.933, + -0.43, + 1, + 2.211, + -0.43, + 2.489, + 0.283, + 2.767, + 0.283, + 0, + 4.2, + 0.283 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + -1, + 0.433, + -1, + 0, + 4.2, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0.19, + 0.433, + 0.19, + 0, + 4.2, + 0.19 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0.11, + 0.433, + 0.11, + 0, + 4.2, + 0.11 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + -0.48, + 0.433, + -0.48, + 0, + 4.2, + -0.48 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0.29, + 0.433, + 0.29, + 0, + 4.2, + 0.29 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 1, + 0.433, + 1, + 0, + 4.2, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 0.85, + 0.433, + 0.85, + 0, + 4.2, + 0.85 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + -0.75, + 0.433, + -0.75, + 0, + 4.2, + -0.75 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + -0.87, + 0.433, + -0.87, + 0, + 4.2, + -0.87 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 1, + 1, + 0.067, + 1, + 0.133, + 1, + 0.2, + 1, + 1, + 0.278, + 1, + 0.356, + -1, + 0.433, + -1, + 0, + 4.2, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.278, + 0, + 0.356, + 1, + 0.433, + 1, + 0, + 4.2, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.444, + 0, + 0.689, + 0, + 0.933, + 0, + 1, + 1.211, + 0, + 1.489, + 0, + 1.767, + 0, + 1, + 2.056, + 0, + 2.344, + -6, + 2.633, + -6, + 1, + 3.033, + -6, + 3.433, + 10, + 3.833, + 10, + 0, + 4.2, + 10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 0, + 4.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.8, + 0, + 1.4, + -2, + 2, + -2, + 1, + 2.456, + -2, + 2.911, + 8.125, + 3.367, + 8.125, + 0, + 4.2, + 8.125 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 0, + 4.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulder", + "Segments": [ + 0, + 0, + 0, + 4.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeg", + "Segments": [ + 0, + 1, + 1, + 0.667, + 1, + 1.333, + 1, + 2, + 1, + 1, + 2.267, + 1, + 2.533, + 0.948, + 2.8, + 0.948, + 0, + 4.2, + 0.948 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLA", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.233, + 0, + 0.267, + 0, + 0.3, + 0, + 1, + 0.478, + 0, + 0.656, + -10, + 0.833, + -10, + 1, + 0.922, + -10, + 1.011, + -8.846, + 1.1, + -8.846, + 1, + 1.467, + -8.846, + 1.833, + -8.835, + 2.2, + -9.1, + 1, + 2.622, + -9.405, + 3.044, + -10, + 3.467, + -10, + 0, + 4.2, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRA", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.233, + 0, + 0.267, + 0, + 0.3, + 0, + 1, + 0.478, + 0, + 0.656, + -10, + 0.833, + -10, + 1, + 0.922, + -10, + 1.011, + -8.972, + 1.1, + -8.846, + 1, + 1.467, + -8.328, + 1.833, + -8.2, + 2.2, + -8.2, + 1, + 2.622, + -8.2, + 3.044, + -10, + 3.467, + -10, + 0, + 4.2, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmLB", + "Segments": [ + 0, + 0, + 0, + 4.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmRB", + "Segments": [ + 0, + 0, + 0, + 4.2, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairAhoge", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.233, + 0, + 0.267, + -5, + 0.3, + -5, + 1, + 0.378, + -5, + 0.456, + 10, + 0.533, + 10, + 1, + 0.633, + 10, + 0.733, + 4, + 0.833, + 4, + 0, + 4.2, + 4 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmA", + "Segments": [ + 0, + 0.997, + 0, + 4.2, + 0.997 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmB", + "Segments": [ + 0, + 0, + 0, + 4.2, + 0 + ] + } + ] +} +``` + +`Content/Samples/Mark/Mark.model3.json`: + +```json +{ + "Version": 3, + "FileReferences": { + "Moc": "Mark.moc3", + "Textures": [ + "Mark.2048/texture_00.png" + ], + "Physics": "Mark.physics3.json", + "UserData": "Mark.userdata3.json", + "Motions": { + "Idle": [ + { + "File": "motions/mark_m01.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/mark_m02.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/mark_m03.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/mark_m04.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/mark_m05.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/mark_m06.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + } + ] + } + }, + "Groups": [ + { + "Target": "Parameter", + "Name": "LipSync", + "Ids": [] + }, + { + "Target": "Parameter", + "Name": "EyeBlink", + "Ids": [ + "ParamEyeLOpen", + "ParamEyeROpen" + ] + } + ], + "HitAreas": [] +} +``` + +`Content/Samples/Mark/Mark.physics3.json`: + +```json +{ + "Version": 3, + "Meta": { + "PhysicsSettingCount": 1, + "TotalInputCount": 4, + "TotalOutputCount": 3, + "VertexCount": 2, + "EffectiveForces": { + "Gravity": { + "X": 0, + "Y": -1 + }, + "Wind": { + "X": 0, + "Y": 0 + } + }, + "PhysicsDictionary": [ + { + "Id": "PhysicsSetting1", + "Name": "Hair" + } + ] + }, + "PhysicsSettings": [ + { + "Id": "PhysicsSetting1", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHairFront" + }, + "VertexIndex": 1, + "Scale": 1.397, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHairSide" + }, + "VertexIndex": 1, + "Scale": 1.397, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHairBack" + }, + "VertexIndex": 1, + "Scale": 1.397, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 15 + }, + "Mobility": 0.95, + "Delay": 0.8, + "Acceleration": 1.5, + "Radius": 15 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + } + ] +} +``` + +`Content/Samples/Mark/Mark.userdata3.json`: + +```json +{ + "Version": 3, + "Meta": { + "UserDataCount": 5, + "TotalUserDataSize": 20 + }, + "UserData": [ + { + "Target": "ArtMesh", + "Id": "Hair2", + "Value": "hair" + }, + { + "Target": "ArtMesh", + "Id": "Hair4", + "Value": "hair" + }, + { + "Target": "ArtMesh", + "Id": "Hair5", + "Value": "hair" + }, + { + "Target": "ArtMesh", + "Id": "Hair3", + "Value": "hair" + }, + { + "Target": "ArtMesh", + "Id": "Hair1", + "Value": "hair" + } + ] +} +``` + +`Content/Samples/Mark/motions/mark_m01.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 10.4, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 38, + "TotalSegmentCount": 414, + "TotalPointCount": 1246, + "UserDataCount": 2, + "TotalUserDataSize": 14 + }, + "UserData": [ + { + "Time": 1.133, + "Value": "timing1" + }, + { + "Time": 3.667, + "Value": "timing2" + } + ], + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + -19, + 1, + 0.211, + -19, + 0.422, + 19, + 0.633, + 19, + 1, + 0.856, + 19, + 1.078, + -19, + 1.3, + -19, + 1, + 1.511, + -19, + 1.722, + 19, + 1.933, + 19, + 1, + 2.156, + 19, + 2.378, + -19, + 2.6, + -19, + 1, + 2.822, + -19, + 3.044, + 19, + 3.267, + 19, + 1, + 3.478, + 19, + 3.689, + -19, + 3.9, + -19, + 1, + 4.111, + -19, + 4.322, + 8.753, + 4.533, + 19, + 1, + 4.756, + 29.786, + 4.978, + 30, + 5.2, + 30, + 1, + 5.422, + 30, + 5.644, + 30, + 5.867, + 30, + 1, + 6.078, + 30, + 6.289, + 30, + 6.5, + 30, + 1, + 6.711, + 30, + 6.922, + 29.887, + 7.133, + 19, + 1, + 7.356, + 7.541, + 7.578, + -19, + 7.8, + -19, + 1, + 8.022, + -19, + 8.244, + 19, + 8.467, + 19, + 1, + 8.689, + 19, + 8.911, + -19, + 9.133, + -19, + 1, + 9.333, + -19, + 9.533, + 19, + 9.733, + 19, + 1, + 9.956, + 19, + 10.178, + -19, + 10.4, + -19 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + -18.45, + 1, + 0.089, + -18.45, + 0.178, + -6, + 0.267, + -6, + 1, + 0.389, + -6, + 0.511, + -18.5, + 0.633, + -18.5, + 1, + 0.733, + -18.5, + 0.833, + -6, + 0.933, + -6, + 1, + 1.056, + -6, + 1.178, + -18.45, + 1.3, + -18.45, + 1, + 1.4, + -18.45, + 1.5, + -6, + 1.6, + -6, + 1, + 1.711, + -6, + 1.822, + -18.5, + 1.933, + -18.5, + 1, + 2.044, + -18.5, + 2.156, + -6, + 2.267, + -6, + 1, + 2.378, + -6, + 2.489, + -18.45, + 2.6, + -18.45, + 1, + 2.7, + -18.45, + 2.8, + -6, + 2.9, + -6, + 1, + 3.022, + -6, + 3.144, + -18.5, + 3.267, + -18.5, + 1, + 3.356, + -18.5, + 3.444, + -6, + 3.533, + -6, + 1, + 3.656, + -6, + 3.778, + -18.45, + 3.9, + -18.45, + 1, + 4, + -18.45, + 4.1, + -6, + 4.2, + -6, + 1, + 4.311, + -6, + 4.422, + -18.5, + 4.533, + -18.5, + 1, + 4.644, + -18.5, + 4.756, + -6, + 4.867, + -6, + 1, + 4.978, + -6, + 5.089, + -18.45, + 5.2, + -18.45, + 1, + 5.3, + -18.45, + 5.4, + -6, + 5.5, + -6, + 1, + 5.622, + -6, + 5.744, + -18.5, + 5.867, + -18.5, + 1, + 5.956, + -18.5, + 6.044, + -6, + 6.133, + -6, + 1, + 6.256, + -6, + 6.378, + -18.45, + 6.5, + -18.45, + 1, + 6.6, + -18.45, + 6.7, + -6, + 6.8, + -6, + 1, + 6.911, + -6, + 7.022, + -18.5, + 7.133, + -18.5, + 1, + 7.244, + -18.5, + 7.356, + -6, + 7.467, + -6, + 1, + 7.578, + -6, + 7.689, + -18.45, + 7.8, + -18.45, + 1, + 7.9, + -18.45, + 8, + -6, + 8.1, + -6, + 1, + 8.222, + -6, + 8.344, + -18.5, + 8.467, + -18.5, + 1, + 8.567, + -18.5, + 8.667, + -6, + 8.767, + -6, + 1, + 8.889, + -6, + 9.011, + -18.45, + 9.133, + -18.45, + 1, + 9.222, + -18.45, + 9.311, + -6, + 9.4, + -6, + 1, + 9.511, + -6, + 9.622, + -18.5, + 9.733, + -18.5, + 1, + 9.844, + -18.5, + 9.956, + -6, + 10.067, + -6, + 1, + 10.178, + -6, + 10.289, + -18.45, + 10.4, + -18.45 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + -10.291, + 1, + 0.033, + -10.291, + 0.067, + -13, + 0.1, + -13, + 1, + 0.322, + -13, + 0.544, + 13, + 0.767, + 13, + 1, + 0.989, + 13, + 1.211, + -13, + 1.433, + -13, + 1, + 1.644, + -13, + 1.856, + 13, + 2.067, + 13, + 1, + 2.278, + 13, + 2.489, + -13, + 2.7, + -13, + 1, + 2.922, + -13, + 3.144, + 13, + 3.367, + 13, + 1, + 3.589, + 13, + 3.811, + -13, + 4.033, + -13, + 1, + 4.244, + -13, + 4.456, + 13, + 4.667, + 13, + 1, + 4.878, + 13, + 5.089, + -13, + 5.3, + -13, + 1, + 5.522, + -13, + 5.744, + 13, + 5.967, + 13, + 1, + 6.189, + 13, + 6.411, + -13, + 6.633, + -13, + 1, + 6.856, + -13, + 7.078, + 13, + 7.3, + 13, + 1, + 7.5, + 13, + 7.7, + -13, + 7.9, + -13, + 1, + 8.122, + -13, + 8.344, + 13, + 8.567, + 13, + 1, + 8.789, + 13, + 9.011, + -13, + 9.233, + -13, + 1, + 9.456, + -13, + 9.678, + 13, + 9.9, + 13, + 1, + 10.067, + 13, + 10.233, + -10.291, + 10.4, + -10.291 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 0.997, + 1, + 1.511, + 0.997, + 3.022, + 1, + 4.533, + 1, + 1, + 4.567, + 1, + 4.6, + 0, + 4.633, + 0, + 1, + 4.667, + 0, + 4.7, + 0, + 4.733, + 0, + 1, + 4.789, + 0, + 4.844, + 1, + 4.9, + 1, + 1, + 5.5, + 1, + 6.1, + 1, + 6.7, + 1, + 1, + 6.733, + 1, + 6.767, + 0, + 6.8, + 0, + 1, + 6.833, + 0, + 6.867, + 0, + 6.9, + 0, + 1, + 6.956, + 0, + 7.011, + 1, + 7.067, + 1, + 1, + 8.178, + 1, + 9.289, + 1, + 10.4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 0.997, + 1, + 1.511, + 0.997, + 3.022, + 1, + 4.533, + 1, + 1, + 4.567, + 1, + 4.6, + 0, + 4.633, + 0, + 1, + 4.667, + 0, + 4.7, + 0, + 4.733, + 0, + 1, + 4.789, + 0, + 4.844, + 1, + 4.9, + 1, + 1, + 5.5, + 1, + 6.1, + 1, + 6.7, + 1, + 1, + 6.733, + 1, + 6.767, + 0, + 6.8, + 0, + 1, + 6.833, + 0, + 6.867, + 0, + 6.9, + 0, + 1, + 6.956, + 0, + 7.011, + 1, + 7.067, + 1, + 1, + 8.178, + 1, + 9.289, + 1, + 10.4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0.1, + 1, + 0.211, + 0.1, + 0.422, + -0.1, + 0.633, + -0.1, + 1, + 0.856, + -0.1, + 1.078, + 0.1, + 1.3, + 0.1, + 1, + 1.511, + 0.1, + 1.722, + -0.1, + 1.933, + -0.1, + 1, + 2.156, + -0.1, + 2.378, + 0.1, + 2.6, + 0.1, + 1, + 2.822, + 0.1, + 3.044, + -0.1, + 3.267, + -0.1, + 1, + 3.478, + -0.1, + 3.689, + 0.1, + 3.9, + 0.1, + 1, + 4.111, + 0.1, + 4.322, + -0.1, + 4.533, + -0.1, + 1, + 4.567, + -0.1, + 4.6, + 0, + 4.633, + 0, + 1, + 4.667, + 0, + 4.7, + 0, + 4.733, + 0, + 1, + 4.789, + 0, + 4.844, + 1, + 4.9, + 1, + 1, + 5.222, + 1, + 5.544, + 1, + 5.867, + 1, + 1, + 6.133, + 1, + 6.4, + 1, + 6.667, + 1, + 1, + 6.7, + 1, + 6.733, + 0.794, + 6.767, + 0.5, + 1, + 6.789, + 0.304, + 6.811, + 0.028, + 6.833, + 0, + 1, + 6.9, + -0.085, + 6.967, + -0.1, + 7.033, + -0.1, + 1, + 7.067, + -0.1, + 7.1, + -0.1, + 7.133, + -0.1, + 1, + 7.356, + -0.1, + 7.578, + 0.1, + 7.8, + 0.1, + 1, + 8.022, + 0.1, + 8.244, + -0.1, + 8.467, + -0.1, + 1, + 8.689, + -0.1, + 8.911, + 0.1, + 9.133, + 0.1, + 1, + 9.333, + 0.1, + 9.533, + -0.1, + 9.733, + -0.1, + 1, + 9.956, + -0.1, + 10.178, + 0.1, + 10.4, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0.1, + 1, + 0.211, + 0.1, + 0.422, + 0.1, + 0.633, + 0.1, + 1, + 0.856, + 0.1, + 1.078, + 0.1, + 1.3, + 0.1, + 1, + 1.511, + 0.1, + 1.722, + 0.1, + 1.933, + 0.1, + 1, + 2.156, + 0.1, + 2.378, + 0.1, + 2.6, + 0.1, + 1, + 2.822, + 0.1, + 3.044, + 0.1, + 3.267, + 0.1, + 1, + 3.478, + 0.1, + 3.689, + 0.1, + 3.9, + 0.1, + 1, + 4.111, + 0.1, + 4.322, + 0.1, + 4.533, + 0.1, + 1, + 4.567, + 0.1, + 4.6, + -1, + 4.633, + -1, + 1, + 4.667, + -1, + 4.7, + -1, + 4.733, + -1, + 1, + 4.789, + -1, + 4.844, + 1, + 4.9, + 1, + 1, + 5.222, + 1, + 5.544, + 1, + 5.867, + 1, + 1, + 6.133, + 1, + 6.4, + 1, + 6.667, + 1, + 1, + 6.7, + 1, + 6.733, + 0.36, + 6.767, + -0.4, + 1, + 6.789, + -0.906, + 6.811, + -1, + 6.833, + -1, + 1, + 6.9, + -1, + 6.967, + 0.1, + 7.033, + 0.1, + 1, + 7.067, + 0.1, + 7.1, + 0.1, + 7.133, + 0.1, + 1, + 7.356, + 0.1, + 7.578, + 0.1, + 7.8, + 0.1, + 1, + 8.022, + 0.1, + 8.244, + 0.1, + 8.467, + 0.1, + 1, + 8.689, + 0.1, + 8.911, + 0.1, + 9.133, + 0.1, + 1, + 9.333, + 0.1, + 9.533, + 0.1, + 9.733, + 0.1, + 1, + 9.956, + 0.1, + 10.178, + 0.1, + 10.4, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 1, + 1, + 0.211, + 1, + 0.422, + 1, + 0.633, + 1, + 1, + 0.856, + 1, + 1.078, + 1, + 1.3, + 1, + 1, + 1.511, + 1, + 1.722, + 1, + 1.933, + 1, + 1, + 2.156, + 1, + 2.378, + 1, + 2.6, + 1, + 1, + 2.822, + 1, + 3.044, + 1, + 3.267, + 1, + 1, + 3.478, + 1, + 3.689, + 1, + 3.9, + 1, + 1, + 4.111, + 1, + 4.322, + 1, + 4.533, + 1, + 1, + 4.756, + 1, + 4.978, + 1, + 5.2, + 1, + 1, + 5.422, + 1, + 5.644, + 1, + 5.867, + 1, + 1, + 6.078, + 1, + 6.289, + 1, + 6.5, + 1, + 1, + 6.711, + 1, + 6.922, + 1, + 7.133, + 1, + 1, + 7.356, + 1, + 7.578, + 1, + 7.8, + 1, + 1, + 8.022, + 1, + 8.244, + 1, + 8.467, + 1, + 1, + 8.689, + 1, + 8.911, + 1, + 9.133, + 1, + 1, + 9.333, + 1, + 9.533, + 1, + 9.733, + 1, + 1, + 9.956, + 1, + 10.178, + 1, + 10.4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 1, + 1, + 0.211, + 1, + 0.422, + 1, + 0.633, + 1, + 1, + 0.856, + 1, + 1.078, + 1, + 1.3, + 1, + 1, + 1.511, + 1, + 1.722, + 1, + 1.933, + 1, + 1, + 2.156, + 1, + 2.378, + 1, + 2.6, + 1, + 1, + 2.822, + 1, + 3.044, + 1, + 3.267, + 1, + 1, + 3.478, + 1, + 3.689, + 1, + 3.9, + 1, + 1, + 4.111, + 1, + 4.322, + 1, + 4.533, + 1, + 1, + 4.756, + 1, + 4.978, + 1, + 5.2, + 1, + 1, + 5.422, + 1, + 5.644, + 1, + 5.867, + 1, + 1, + 6.078, + 1, + 6.289, + 1, + 6.5, + 1, + 1, + 6.711, + 1, + 6.922, + 1, + 7.133, + 1, + 1, + 7.356, + 1, + 7.578, + 1, + 7.8, + 1, + 1, + 8.022, + 1, + 8.244, + 1, + 8.467, + 1, + 1, + 8.689, + 1, + 8.911, + 1, + 9.133, + 1, + 1, + 9.333, + 1, + 9.533, + 1, + 9.733, + 1, + 1, + 9.956, + 1, + 10.178, + 1, + 10.4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + 0, + 0.633, + 0, + 1, + 0.856, + 0, + 1.078, + 0, + 1.3, + 0, + 1, + 1.511, + 0, + 1.722, + 0, + 1.933, + 0, + 1, + 2.156, + 0, + 2.378, + 0, + 2.6, + 0, + 1, + 2.822, + 0, + 3.044, + 0, + 3.267, + 0, + 1, + 3.478, + 0, + 3.689, + 0, + 3.9, + 0, + 1, + 4.111, + 0, + 4.322, + 0, + 4.533, + 0, + 1, + 4.756, + 0, + 4.978, + 0, + 5.2, + 0, + 1, + 5.422, + 0, + 5.644, + 0, + 5.867, + 0, + 1, + 6.078, + 0, + 6.289, + 0, + 6.5, + 0, + 1, + 6.711, + 0, + 6.922, + 0, + 7.133, + 0, + 1, + 7.356, + 0, + 7.578, + 0, + 7.8, + 0, + 1, + 8.022, + 0, + 8.244, + 0, + 8.467, + 0, + 1, + 8.689, + 0, + 8.911, + 0, + 9.133, + 0, + 1, + 9.333, + 0, + 9.533, + 0, + 9.733, + 0, + 1, + 9.956, + 0, + 10.178, + 0, + 10.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL", + "Segments": [ + 0, + -10, + 1, + 0.089, + -10, + 0.178, + -8, + 0.267, + -8, + 1, + 0.389, + -8, + 0.511, + -10, + 0.633, + -10, + 1, + 0.733, + -10, + 0.833, + -8, + 0.933, + -8, + 1, + 1.056, + -8, + 1.178, + -10, + 1.3, + -10, + 1, + 1.4, + -10, + 1.5, + -8, + 1.6, + -8, + 1, + 1.711, + -8, + 1.822, + -10, + 1.933, + -10, + 1, + 2.044, + -10, + 2.156, + -8, + 2.267, + -8, + 1, + 2.378, + -8, + 2.489, + -10, + 2.6, + -10, + 1, + 2.7, + -10, + 2.8, + -8, + 2.9, + -8, + 1, + 3.022, + -8, + 3.144, + -10, + 3.267, + -10, + 1, + 3.356, + -10, + 3.444, + -8, + 3.533, + -8, + 1, + 3.656, + -8, + 3.778, + -10, + 3.9, + -10, + 1, + 4, + -10, + 4.1, + -8, + 4.2, + -8, + 1, + 4.311, + -8, + 4.422, + -10, + 4.533, + -10, + 1, + 4.644, + -10, + 4.756, + -8, + 4.867, + -8, + 1, + 4.978, + -8, + 5.089, + -10, + 5.2, + -10, + 1, + 5.3, + -10, + 5.4, + -8, + 5.5, + -8, + 1, + 5.622, + -8, + 5.744, + -10, + 5.867, + -10, + 1, + 5.956, + -10, + 6.044, + -8, + 6.133, + -8, + 1, + 6.256, + -8, + 6.378, + -10, + 6.5, + -10, + 1, + 6.6, + -10, + 6.7, + -8, + 6.8, + -8, + 1, + 6.911, + -8, + 7.022, + -10, + 7.133, + -10, + 1, + 7.244, + -10, + 7.356, + -8, + 7.467, + -8, + 1, + 7.578, + -8, + 7.689, + -10, + 7.8, + -10, + 1, + 7.9, + -10, + 8, + -8, + 8.1, + -8, + 1, + 8.222, + -8, + 8.344, + -10, + 8.467, + -10, + 1, + 8.567, + -10, + 8.667, + -8, + 8.767, + -8, + 1, + 8.889, + -8, + 9.011, + -10, + 9.133, + -10, + 1, + 9.222, + -10, + 9.311, + -8, + 9.4, + -8, + 1, + 9.511, + -8, + 9.622, + -10, + 9.733, + -10, + 1, + 9.844, + -10, + 9.956, + -8, + 10.067, + -8, + 1, + 10.178, + -8, + 10.289, + -10, + 10.4, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR", + "Segments": [ + 0, + -10, + 1, + 0.089, + -10, + 0.178, + -8, + 0.267, + -8, + 1, + 0.389, + -8, + 0.511, + -10, + 0.633, + -10, + 1, + 0.733, + -10, + 0.833, + -8, + 0.933, + -8, + 1, + 1.056, + -8, + 1.178, + -10, + 1.3, + -10, + 1, + 1.4, + -10, + 1.5, + -8, + 1.6, + -8, + 1, + 1.711, + -8, + 1.822, + -10, + 1.933, + -10, + 1, + 2.044, + -10, + 2.156, + -8, + 2.267, + -8, + 1, + 2.378, + -8, + 2.489, + -10, + 2.6, + -10, + 1, + 2.7, + -10, + 2.8, + -8, + 2.9, + -8, + 1, + 3.022, + -8, + 3.144, + -10, + 3.267, + -10, + 1, + 3.356, + -10, + 3.444, + -8, + 3.533, + -8, + 1, + 3.656, + -8, + 3.778, + -10, + 3.9, + -10, + 1, + 4, + -10, + 4.1, + -8, + 4.2, + -8, + 1, + 4.311, + -8, + 4.422, + -10, + 4.533, + -10, + 1, + 4.644, + -10, + 4.756, + -8, + 4.867, + -8, + 1, + 4.978, + -8, + 5.089, + -10, + 5.2, + -10, + 1, + 5.3, + -10, + 5.4, + -8, + 5.5, + -8, + 1, + 5.622, + -8, + 5.744, + -10, + 5.867, + -10, + 1, + 5.956, + -10, + 6.044, + -8, + 6.133, + -8, + 1, + 6.256, + -8, + 6.378, + -10, + 6.5, + -10, + 1, + 6.6, + -10, + 6.7, + -8, + 6.8, + -8, + 1, + 6.911, + -8, + 7.022, + -10, + 7.133, + -10, + 1, + 7.244, + -10, + 7.356, + -8, + 7.467, + -8, + 1, + 7.578, + -8, + 7.689, + -10, + 7.8, + -10, + 1, + 7.9, + -10, + 8, + -8, + 8.1, + -8, + 1, + 8.222, + -8, + 8.344, + -10, + 8.467, + -10, + 1, + 8.567, + -10, + 8.667, + -8, + 8.767, + -8, + 1, + 8.889, + -8, + 9.011, + -10, + 9.133, + -10, + 1, + 9.222, + -10, + 9.311, + -8, + 9.4, + -8, + 1, + 9.511, + -8, + 9.622, + -10, + 9.733, + -10, + 1, + 9.844, + -10, + 9.956, + -8, + 10.067, + -8, + 1, + 10.178, + -8, + 10.289, + -10, + 10.4, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftLeg", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + 0, + 0.633, + 0, + 1, + 0.856, + 0, + 1.078, + 0, + 1.3, + 0, + 1, + 1.511, + 0, + 1.722, + 0, + 1.933, + 0, + 1, + 2.156, + 0, + 2.378, + 0, + 2.6, + 0, + 1, + 2.822, + 0, + 3.044, + 0, + 3.267, + 0, + 1, + 3.478, + 0, + 3.689, + 0, + 3.9, + 0, + 1, + 4.111, + 0, + 4.322, + 0, + 4.533, + 0, + 1, + 4.756, + 0, + 4.978, + 0, + 5.2, + 0, + 1, + 5.422, + 0, + 5.644, + 0, + 5.867, + 0, + 1, + 6.078, + 0, + 6.289, + 0, + 6.5, + 0, + 1, + 6.711, + 0, + 6.922, + 0, + 7.133, + 0, + 1, + 7.356, + 0, + 7.578, + 0, + 7.8, + 0, + 1, + 8.022, + 0, + 8.244, + 0, + 8.467, + 0, + 1, + 8.689, + 0, + 8.911, + 0, + 9.133, + 0, + 1, + 9.333, + 0, + 9.533, + 0, + 9.733, + 0, + 1, + 9.956, + 0, + 10.178, + 0, + 10.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightLeg", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + 0, + 0.633, + 0, + 1, + 0.856, + 0, + 1.078, + 0, + 1.3, + 0, + 1, + 1.511, + 0, + 1.722, + 0, + 1.933, + 0, + 1, + 2.156, + 0, + 2.378, + 0, + 2.6, + 0, + 1, + 2.822, + 0, + 3.044, + 0, + 3.267, + 0, + 1, + 3.478, + 0, + 3.689, + 0, + 3.9, + 0, + 1, + 4.111, + 0, + 4.322, + 0, + 4.533, + 0, + 1, + 4.756, + 0, + 4.978, + 0, + 5.2, + 0, + 1, + 5.422, + 0, + 5.644, + 0, + 5.867, + 0, + 1, + 6.078, + 0, + 6.289, + 0, + 6.5, + 0, + 1, + 6.711, + 0, + 6.922, + 0, + 7.133, + 0, + 1, + 7.356, + 0, + 7.578, + 0, + 7.8, + 0, + 1, + 8.022, + 0, + 8.244, + 0, + 8.467, + 0, + 1, + 8.689, + 0, + 8.911, + 0, + 9.133, + 0, + 1, + 9.333, + 0, + 9.533, + 0, + 9.733, + 0, + 1, + 9.956, + 0, + 10.178, + 0, + 10.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + -1, + 1, + 0.211, + -1, + 0.422, + 1, + 0.633, + 1, + 1, + 0.856, + 1, + 1.078, + -1, + 1.3, + -1, + 1, + 1.511, + -1, + 1.722, + 1, + 1.933, + 1, + 1, + 2.156, + 1, + 2.378, + -1, + 2.6, + -1, + 1, + 2.822, + -1, + 3.044, + 1, + 3.267, + 1, + 1, + 3.478, + 1, + 3.689, + -1, + 3.9, + -1, + 1, + 4.111, + -1, + 4.322, + 1, + 4.533, + 1, + 1, + 4.756, + 1, + 4.978, + -1, + 5.2, + -1, + 1, + 5.422, + -1, + 5.644, + 1, + 5.867, + 1, + 1, + 6.078, + 1, + 6.289, + -1, + 6.5, + -1, + 1, + 6.711, + -1, + 6.922, + 1, + 7.133, + 1, + 1, + 7.356, + 1, + 7.578, + -1, + 7.8, + -1, + 1, + 8.022, + -1, + 8.244, + 1, + 8.467, + 1, + 1, + 8.689, + 1, + 8.911, + -1, + 9.133, + -1, + 1, + 9.333, + -1, + 9.533, + 1, + 9.733, + 1, + 1, + 9.956, + 1, + 10.178, + -1, + 10.4, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 5.497, + 0.267, + 5.497, + 1, + 0.389, + 5.497, + 0.511, + 0, + 0.633, + 0, + 1, + 0.733, + 0, + 0.833, + 5.497, + 0.933, + 5.497, + 1, + 1.056, + 5.497, + 1.178, + 0, + 1.3, + 0, + 1, + 1.4, + 0, + 1.5, + 5.497, + 1.6, + 5.497, + 1, + 1.711, + 5.497, + 1.822, + 0, + 1.933, + 0, + 1, + 2.044, + 0, + 2.156, + 5.497, + 2.267, + 5.497, + 1, + 2.378, + 5.497, + 2.489, + 0, + 2.6, + 0, + 1, + 2.7, + 0, + 2.8, + 5.497, + 2.9, + 5.497, + 1, + 3.022, + 5.497, + 3.144, + 0, + 3.267, + 0, + 1, + 3.356, + 0, + 3.444, + 5.497, + 3.533, + 5.497, + 1, + 3.656, + 5.497, + 3.778, + 0, + 3.9, + 0, + 1, + 4, + 0, + 4.1, + 5.497, + 4.2, + 5.497, + 1, + 4.311, + 5.497, + 4.422, + 0, + 4.533, + 0, + 1, + 4.644, + 0, + 4.756, + 5.497, + 4.867, + 5.497, + 1, + 4.978, + 5.497, + 5.089, + 0, + 5.2, + 0, + 1, + 5.3, + 0, + 5.4, + 5.497, + 5.5, + 5.497, + 1, + 5.622, + 5.497, + 5.744, + 0, + 5.867, + 0, + 1, + 5.956, + 0, + 6.044, + 5.497, + 6.133, + 5.497, + 1, + 6.256, + 5.497, + 6.378, + 0, + 6.5, + 0, + 1, + 6.6, + 0, + 6.7, + 5.497, + 6.8, + 5.497, + 1, + 6.911, + 5.497, + 7.022, + 0, + 7.133, + 0, + 1, + 7.244, + 0, + 7.356, + 5.497, + 7.467, + 5.497, + 1, + 7.578, + 5.497, + 7.689, + 0, + 7.8, + 0, + 1, + 7.9, + 0, + 8, + 5.497, + 8.1, + 5.497, + 1, + 8.222, + 5.497, + 8.344, + 0, + 8.467, + 0, + 1, + 8.567, + 0, + 8.667, + 5.497, + 8.767, + 5.497, + 1, + 8.889, + 5.497, + 9.011, + 0, + 9.133, + 0, + 1, + 9.222, + 0, + 9.311, + 5.497, + 9.4, + 5.497, + 1, + 9.511, + 5.497, + 9.622, + 0, + 9.733, + 0, + 1, + 9.844, + 0, + 9.956, + 5.497, + 10.067, + 5.497, + 1, + 10.178, + 5.497, + 10.289, + 0, + 10.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + -6, + 1, + 0.211, + -6, + 0.422, + 6, + 0.633, + 6, + 1, + 0.856, + 6, + 1.078, + -6, + 1.3, + -6, + 1, + 1.511, + -6, + 1.722, + 6, + 1.933, + 6, + 1, + 2.156, + 6, + 2.378, + -6, + 2.6, + -6, + 1, + 2.822, + -6, + 3.044, + 6, + 3.267, + 6, + 1, + 3.478, + 6, + 3.689, + -6, + 3.9, + -6, + 1, + 4.111, + -6, + 4.322, + 6, + 4.533, + 6, + 1, + 4.756, + 6, + 4.978, + -6, + 5.2, + -6, + 1, + 5.422, + -6, + 5.644, + 6, + 5.867, + 6, + 1, + 6.078, + 6, + 6.289, + -6, + 6.5, + -6, + 1, + 6.711, + -6, + 6.922, + 6, + 7.133, + 6, + 1, + 7.356, + 6, + 7.578, + -6, + 7.8, + -6, + 1, + 8.022, + -6, + 8.244, + 6, + 8.467, + 6, + 1, + 8.689, + 6, + 8.911, + -6, + 9.133, + -6, + 1, + 9.333, + -6, + 9.533, + 6, + 9.733, + 6, + 1, + 9.956, + 6, + 10.178, + -6, + 10.4, + -6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + 0, + 0.633, + 0, + 1, + 0.856, + 0, + 1.078, + 0, + 1.3, + 0, + 1, + 1.511, + 0, + 1.722, + 0, + 1.933, + 0, + 1, + 2.156, + 0, + 2.378, + 0, + 2.6, + 0, + 1, + 2.822, + 0, + 3.044, + 0, + 3.267, + 0, + 1, + 3.478, + 0, + 3.689, + 0, + 3.9, + 0, + 1, + 4.111, + 0, + 4.322, + 0, + 4.533, + 0, + 1, + 4.756, + 0, + 4.978, + 0, + 5.2, + 0, + 1, + 5.422, + 0, + 5.644, + 0, + 5.867, + 0, + 1, + 6.078, + 0, + 6.289, + 0, + 6.5, + 0, + 1, + 6.711, + 0, + 6.922, + 0, + 7.133, + 0, + 1, + 7.356, + 0, + 7.578, + 0, + 7.8, + 0, + 1, + 8.022, + 0, + 8.244, + 0, + 8.467, + 0, + 1, + 8.689, + 0, + 8.911, + 0, + 9.133, + 0, + 1, + 9.333, + 0, + 9.533, + 0, + 9.733, + 0, + 1, + 9.956, + 0, + 10.178, + 0, + 10.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + 0, + 0.633, + 0, + 1, + 0.856, + 0, + 1.078, + 0, + 1.3, + 0, + 1, + 1.511, + 0, + 1.722, + 0, + 1.933, + 0, + 1, + 2.156, + 0, + 2.378, + 0, + 2.6, + 0, + 1, + 2.822, + 0, + 3.044, + 0, + 3.267, + 0, + 1, + 3.478, + 0, + 3.689, + 0, + 3.9, + 0, + 1, + 4.111, + 0, + 4.322, + 0, + 4.533, + 0, + 1, + 4.756, + 0, + 4.978, + 0, + 5.2, + 0, + 1, + 5.422, + 0, + 5.644, + 0, + 5.867, + 0, + 1, + 6.078, + 0, + 6.289, + 0, + 6.5, + 0, + 1, + 6.711, + 0, + 6.922, + 0, + 7.133, + 0, + 1, + 7.356, + 0, + 7.578, + 0, + 7.8, + 0, + 1, + 8.022, + 0, + 8.244, + 0, + 8.467, + 0, + 1, + 8.689, + 0, + 8.911, + 0, + 9.133, + 0, + 1, + 9.333, + 0, + 9.533, + 0, + 9.733, + 0, + 1, + 9.956, + 0, + 10.178, + 0, + 10.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + 0, + 0.633, + 0, + 1, + 0.856, + 0, + 1.078, + 0, + 1.3, + 0, + 1, + 1.511, + 0, + 1.722, + 0, + 1.933, + 0, + 1, + 2.156, + 0, + 2.378, + 0, + 2.6, + 0, + 1, + 2.822, + 0, + 3.044, + 0, + 3.267, + 0, + 1, + 3.478, + 0, + 3.689, + 0, + 3.9, + 0, + 1, + 4.111, + 0, + 4.322, + 0, + 4.533, + 0, + 1, + 4.756, + 0, + 4.978, + 0, + 5.2, + 0, + 1, + 5.422, + 0, + 5.644, + 0, + 5.867, + 0, + 1, + 6.078, + 0, + 6.289, + 0, + 6.5, + 0, + 1, + 6.711, + 0, + 6.922, + 0, + 7.133, + 0, + 1, + 7.356, + 0, + 7.578, + 0, + 7.8, + 0, + 1, + 8.022, + 0, + 8.244, + 0, + 8.467, + 0, + 1, + 8.689, + 0, + 8.911, + 0, + 9.133, + 0, + 1, + 9.333, + 0, + 9.533, + 0, + 9.733, + 0, + 1, + 9.956, + 0, + 10.178, + 0, + 10.4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + 0, + 0.633, + 0, + 1, + 0.856, + 0, + 1.078, + 0, + 1.3, + 0, + 1, + 1.511, + 0, + 1.722, + 0, + 1.933, + 0, + 1, + 2.156, + 0, + 2.378, + 0, + 2.6, + 0, + 1, + 2.822, + 0, + 3.044, + 0, + 3.267, + 0, + 1, + 3.478, + 0, + 3.689, + 0, + 3.9, + 0, + 1, + 4.111, + 0, + 4.322, + 0, + 4.533, + 0, + 1, + 4.756, + 0, + 4.978, + 0, + 5.2, + 0, + 1, + 5.422, + 0, + 5.644, + 0, + 5.867, + 0, + 1, + 6.078, + 0, + 6.289, + 0, + 6.5, + 0, + 1, + 6.711, + 0, + 6.922, + 0, + 7.133, + 0, + 1, + 7.356, + 0, + 7.578, + 0, + 7.8, + 0, + 1, + 8.022, + 0, + 8.244, + 0, + 8.467, + 0, + 1, + 8.689, + 0, + 8.911, + 0, + 9.133, + 0, + 1, + 9.333, + 0, + 9.533, + 0, + 9.733, + 0, + 1, + 9.956, + 0, + 10.178, + 0, + 10.4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHead", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairFront", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeL", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeBallL", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeWhiteL", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeR", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeBallR", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeWhiteR", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairSide", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartMouth", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartOral", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartFace", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartBody", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmL", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmR", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairBack", + "Segments": [ + 0, + 1, + 0, + 10.4, + 1 + ] + } + ] +} +``` + +`Content/Samples/Mark/motions/mark_m02.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4.8, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 38, + "TotalSegmentCount": 261, + "TotalPointCount": 787, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + -3, + 0.3, + -3, + 1, + 0.4, + -3, + 0.5, + -2, + 0.6, + 0, + 1, + 0.7, + 2, + 0.8, + 3, + 0.9, + 3, + 1, + 1, + 3, + 1.1, + 2, + 1.2, + 0, + 1, + 1.3, + -2, + 1.4, + -3, + 1.5, + -3, + 1, + 1.6, + -3, + 1.7, + -2, + 1.8, + 0, + 1, + 1.9, + 2, + 2, + 3, + 2.1, + 3, + 1, + 2.2, + 3, + 2.3, + 2, + 2.4, + 0, + 1, + 2.5, + -2, + 2.6, + -3, + 2.7, + -3, + 1, + 2.8, + -3, + 2.9, + -2, + 3, + 0, + 1, + 3.1, + 2, + 3.2, + 3, + 3.3, + 3, + 1, + 3.4, + 3, + 3.5, + 2, + 3.6, + 0, + 1, + 3.7, + -2, + 3.8, + -3, + 3.9, + -3, + 1, + 4, + -3, + 4.1, + -2, + 4.2, + 0, + 1, + 4.3, + 2, + 4.4, + 3, + 4.5, + 3, + 1, + 4.6, + 3, + 4.7, + 0, + 4.8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 7.804, + 1, + 0.067, + 7.804, + 0.133, + 30, + 0.2, + 30, + 1, + 0.3, + 30, + 0.4, + 0, + 0.5, + 0, + 1, + 0.6, + 0, + 0.7, + 30, + 0.8, + 30, + 1, + 0.9, + 30, + 1, + 0, + 1.1, + 0, + 1, + 1.2, + 0, + 1.3, + 30, + 1.4, + 30, + 1, + 1.5, + 30, + 1.6, + 0, + 1.7, + 0, + 1, + 1.8, + 0, + 1.9, + 30, + 2, + 30, + 1, + 2.1, + 30, + 2.2, + 0, + 2.3, + 0, + 1, + 2.4, + 0, + 2.5, + 30, + 2.6, + 30, + 1, + 2.7, + 30, + 2.8, + 0, + 2.9, + 0, + 1, + 3, + 0, + 3.1, + 30, + 3.2, + 30, + 1, + 3.3, + 30, + 3.4, + 0, + 3.5, + 0, + 1, + 3.6, + 0, + 3.7, + 30, + 3.8, + 30, + 1, + 3.9, + 30, + 4, + 0, + 4.1, + 0, + 1, + 4.2, + 0, + 4.3, + 30, + 4.4, + 30, + 1, + 4.5, + 30, + 4.6, + 0, + 4.7, + 0, + 1, + 4.733, + 0, + 4.767, + 7.804, + 4.8, + 7.804 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + -27.935, + 1, + 0.022, + -27.935, + 0.044, + -30, + 0.067, + -30, + 1, + 0.267, + -30, + 0.467, + 30, + 0.667, + 30, + 1, + 0.867, + 30, + 1.067, + -30, + 1.267, + -30, + 1, + 1.467, + -30, + 1.667, + 30, + 1.867, + 30, + 1, + 2.067, + 30, + 2.267, + -30, + 2.467, + -30, + 1, + 2.667, + -30, + 2.867, + 30, + 3.067, + 30, + 1, + 3.267, + 30, + 3.467, + -30, + 3.667, + -30, + 1, + 3.867, + -30, + 4.067, + 30, + 4.267, + 30, + 1, + 4.444, + 30, + 4.622, + -27.935, + 4.8, + -27.935 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 1.6, + 1, + 3.2, + 1, + 4.8, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 1.6, + 1, + 3.2, + 1, + 4.8, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 1, + 0.8, + 0, + 1, + 0, + 1.2, + 0, + 1, + 1.4, + 0, + 1.6, + 0, + 1.8, + 0, + 1, + 2, + 0, + 2.2, + 0, + 2.4, + 0, + 1, + 2.6, + 0, + 2.8, + 0, + 3, + 0, + 1, + 3.2, + 0, + 3.4, + 0, + 3.6, + 0, + 1, + 3.8, + 0, + 4, + 0, + 4.2, + 0, + 1, + 4.4, + 0, + 4.6, + 0, + 4.8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0.1, + 1, + 0.2, + 0.1, + 0.4, + 0.1, + 0.6, + 0.1, + 1, + 0.8, + 0.1, + 1, + 0.1, + 1.2, + 0.1, + 1, + 1.4, + 0.1, + 1.6, + 0.1, + 1.8, + 0.1, + 1, + 2, + 0.1, + 2.2, + 0.1, + 2.4, + 0.1, + 1, + 2.6, + 0.1, + 2.8, + 0.1, + 3, + 0.1, + 1, + 3.2, + 0.1, + 3.4, + 0.1, + 3.6, + 0.1, + 1, + 3.8, + 0.1, + 4, + 0.1, + 4.2, + 0.1, + 1, + 4.4, + 0.1, + 4.6, + 0.1, + 4.8, + 0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 1, + 1, + 0.2, + 1, + 0.4, + 1, + 0.6, + 1, + 1, + 0.8, + 1, + 1, + 1, + 1.2, + 1, + 1, + 1.4, + 1, + 1.6, + 1, + 1.8, + 1, + 1, + 2, + 1, + 2.2, + 1, + 2.4, + 1, + 1, + 2.6, + 1, + 2.8, + 1, + 3, + 1, + 1, + 3.2, + 1, + 3.4, + 1, + 3.6, + 1, + 1, + 3.8, + 1, + 4, + 1, + 4.2, + 1, + 1, + 4.4, + 1, + 4.6, + 1, + 4.8, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 1, + 1, + 0.2, + 1, + 0.4, + 1, + 0.6, + 1, + 1, + 0.8, + 1, + 1, + 1, + 1.2, + 1, + 1, + 1.4, + 1, + 1.6, + 1, + 1.8, + 1, + 1, + 2, + 1, + 2.2, + 1, + 2.4, + 1, + 1, + 2.6, + 1, + 2.8, + 1, + 3, + 1, + 1, + 3.2, + 1, + 3.4, + 1, + 3.6, + 1, + 1, + 3.8, + 1, + 4, + 1, + 4.2, + 1, + 1, + 4.4, + 1, + 4.6, + 1, + 4.8, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 1, + 0.8, + 0, + 1, + 0, + 1.2, + 0, + 1, + 1.4, + 0, + 1.6, + 0, + 1.8, + 0, + 1, + 2, + 0, + 2.2, + 0, + 2.4, + 0, + 1, + 2.6, + 0, + 2.8, + 0, + 3, + 0, + 1, + 3.2, + 0, + 3.4, + 0, + 3.6, + 0, + 1, + 3.8, + 0, + 4, + 0, + 4.2, + 0, + 1, + 4.4, + 0, + 4.6, + 0, + 4.8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL", + "Segments": [ + 0, + -3, + 1, + 0.1, + -5.043, + 0.2, + -7, + 0.3, + -7, + 1, + 0.4, + -7, + 0.5, + -4.182, + 0.6, + -1, + 1, + 0.7, + 2.182, + 0.8, + 3, + 0.9, + 3, + 1, + 1, + 3, + 1.1, + -3, + 1.2, + -3, + 1, + 1.3, + -3, + 1.4, + -7, + 1.5, + -7, + 1, + 1.6, + -7, + 1.7, + -4.182, + 1.8, + -1, + 1, + 1.9, + 2.182, + 2, + 3, + 2.1, + 3, + 1, + 2.2, + 3, + 2.3, + -3, + 2.4, + -3, + 1, + 2.5, + -3, + 2.6, + -7, + 2.7, + -7, + 1, + 2.8, + -7, + 2.9, + -4.182, + 3, + -1, + 1, + 3.1, + 2.182, + 3.2, + 3, + 3.3, + 3, + 1, + 3.4, + 3, + 3.5, + -3, + 3.6, + -3, + 1, + 3.7, + -3, + 3.8, + -7, + 3.9, + -7, + 1, + 4, + -7, + 4.1, + -4.182, + 4.2, + -1, + 1, + 4.3, + 2.182, + 4.4, + 3, + 4.5, + 3, + 1, + 4.6, + 3, + 4.7, + -3, + 4.8, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR", + "Segments": [ + 0, + -1, + 1, + 0.1, + 1.154, + 0.2, + 3, + 0.3, + 3, + 1, + 0.4, + 3, + 0.5, + 0.182, + 0.6, + -3, + 1, + 0.7, + -6.182, + 0.8, + -7, + 0.9, + -7, + 1, + 1, + -7, + 1.1, + -1, + 1.2, + -1, + 1, + 1.3, + -1, + 1.4, + 3, + 1.5, + 3, + 1, + 1.6, + 3, + 1.7, + 0.182, + 1.8, + -3, + 1, + 1.9, + -6.182, + 2, + -7, + 2.1, + -7, + 1, + 2.2, + -7, + 2.3, + -1, + 2.4, + -1, + 1, + 2.5, + -1, + 2.6, + 3, + 2.7, + 3, + 1, + 2.8, + 3, + 2.9, + 0.182, + 3, + -3, + 1, + 3.1, + -6.182, + 3.2, + -7, + 3.3, + -7, + 1, + 3.4, + -7, + 3.5, + -1, + 3.6, + -1, + 1, + 3.7, + -1, + 3.8, + 3, + 3.9, + 3, + 1, + 4, + 3, + 4.1, + 0.182, + 4.2, + -3, + 1, + 4.3, + -6.182, + 4.4, + -7, + 4.5, + -7, + 1, + 4.6, + -7, + 4.7, + -1, + 4.8, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftLeg", + "Segments": [ + 0, + 7, + 1, + 0.089, + 9.389, + 0.178, + 10, + 0.267, + 10, + 1, + 0.378, + 10, + 0.489, + -3, + 0.6, + -3, + 1, + 0.756, + -3, + 0.911, + -3, + 1.067, + -3, + 1, + 1.111, + -3, + 1.156, + 7, + 1.2, + 7, + 1, + 1.289, + 7, + 1.378, + 10, + 1.467, + 10, + 1, + 1.578, + 10, + 1.689, + -3, + 1.8, + -3, + 1, + 1.956, + -3, + 2.111, + -3, + 2.267, + -3, + 1, + 2.311, + -3, + 2.356, + 7, + 2.4, + 7, + 1, + 2.489, + 7, + 2.578, + 10, + 2.667, + 10, + 1, + 2.778, + 10, + 2.889, + -3, + 3, + -3, + 1, + 3.156, + -3, + 3.311, + -3, + 3.467, + -3, + 1, + 3.511, + -3, + 3.556, + 7, + 3.6, + 7, + 1, + 3.689, + 7, + 3.778, + 10, + 3.867, + 10, + 1, + 3.978, + 10, + 4.089, + -3, + 4.2, + -3, + 1, + 4.356, + -3, + 4.511, + -3, + 4.667, + -3, + 1, + 4.711, + -3, + 4.756, + 7, + 4.8, + 7 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightLeg", + "Segments": [ + 0, + -3, + 1, + 0.156, + -3, + 0.311, + -3, + 0.467, + -3, + 1, + 0.511, + -3, + 0.556, + 5.676, + 0.6, + 7, + 1, + 0.689, + 9.649, + 0.778, + 10, + 0.867, + 10, + 1, + 0.978, + 10, + 1.089, + -3, + 1.2, + -3, + 1, + 1.356, + -3, + 1.511, + -3, + 1.667, + -3, + 1, + 1.711, + -3, + 1.756, + 5.676, + 1.8, + 7, + 1, + 1.889, + 9.649, + 1.978, + 10, + 2.067, + 10, + 1, + 2.178, + 10, + 2.289, + -3, + 2.4, + -3, + 1, + 2.556, + -3, + 2.711, + -3, + 2.867, + -3, + 1, + 2.911, + -3, + 2.956, + 5.676, + 3, + 7, + 1, + 3.089, + 9.649, + 3.178, + 10, + 3.267, + 10, + 1, + 3.378, + 10, + 3.489, + -3, + 3.6, + -3, + 1, + 3.756, + -3, + 3.911, + -3, + 4.067, + -3, + 1, + 4.111, + -3, + 4.156, + 5.676, + 4.2, + 7, + 1, + 4.289, + 9.649, + 4.378, + 10, + 4.467, + 10, + 1, + 4.578, + 10, + 4.689, + -3, + 4.8, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 8, + 1, + 0.167, + -0.086, + 0.333, + -10, + 0.5, + -10, + 1, + 0.533, + -10, + 0.567, + -10.096, + 0.6, + -8, + 1, + 0.767, + 2.481, + 0.933, + 10, + 1.1, + 10, + 1, + 1.133, + 10, + 1.167, + 8, + 1.2, + 8, + 1, + 1.367, + 8, + 1.533, + -10, + 1.7, + -10, + 1, + 1.733, + -10, + 1.767, + -10.096, + 1.8, + -8, + 1, + 1.967, + 2.481, + 2.133, + 10, + 2.3, + 10, + 1, + 2.333, + 10, + 2.367, + 8, + 2.4, + 8, + 1, + 2.567, + 8, + 2.733, + -10, + 2.9, + -10, + 1, + 2.933, + -10, + 2.967, + -10.096, + 3, + -8, + 1, + 3.167, + 2.481, + 3.333, + 10, + 3.5, + 10, + 1, + 3.533, + 10, + 3.567, + 8, + 3.6, + 8, + 1, + 3.767, + 8, + 3.933, + -10, + 4.1, + -10, + 1, + 4.133, + -10, + 4.167, + -10.096, + 4.2, + -8, + 1, + 4.367, + 2.481, + 4.533, + 10, + 4.7, + 10, + 1, + 4.733, + 10, + 4.767, + 8, + 4.8, + 8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + -2, + 1, + 0.044, + -4.9, + 0.089, + -7.05, + 0.133, + -9.664, + 1, + 0.144, + -10.435, + 0.156, + -10.664, + 0.167, + -10.664, + 1, + 0.211, + -10.664, + 0.256, + 5.048, + 0.3, + 5.048, + 1, + 0.4, + 5.048, + 0.5, + 2.972, + 0.6, + -2, + 1, + 0.644, + -4.364, + 0.689, + -7.117, + 0.733, + -9.664, + 1, + 0.744, + -10.427, + 0.756, + -10.664, + 0.767, + -10.664, + 1, + 0.811, + -10.664, + 0.856, + 5.048, + 0.9, + 5.048, + 1, + 1, + 5.048, + 1.1, + -2, + 1.2, + -2, + 1, + 1.244, + -2, + 1.289, + -9.664, + 1.333, + -9.664, + 1, + 1.344, + -9.664, + 1.356, + -10.664, + 1.367, + -10.664, + 1, + 1.411, + -10.664, + 1.456, + 5.048, + 1.5, + 5.048, + 1, + 1.6, + 5.048, + 1.7, + -2, + 1.8, + -2, + 1, + 1.844, + -2, + 1.889, + -9.664, + 1.933, + -9.664, + 1, + 1.944, + -9.664, + 1.956, + -10.664, + 1.967, + -10.664, + 1, + 2.011, + -10.664, + 2.056, + 5.048, + 2.1, + 5.048, + 1, + 2.2, + 5.048, + 2.3, + -2, + 2.4, + -2, + 1, + 2.444, + -2, + 2.489, + -9.664, + 2.533, + -9.664, + 1, + 2.544, + -9.664, + 2.556, + -10.664, + 2.567, + -10.664, + 1, + 2.611, + -10.664, + 2.656, + 5.048, + 2.7, + 5.048, + 1, + 2.8, + 5.048, + 2.9, + -2, + 3, + -2, + 1, + 3.044, + -2, + 3.089, + -9.664, + 3.133, + -9.664, + 1, + 3.144, + -9.664, + 3.156, + -10.664, + 3.167, + -10.664, + 1, + 3.211, + -10.664, + 3.256, + 5.048, + 3.3, + 5.048, + 1, + 3.4, + 5.048, + 3.5, + -2, + 3.6, + -2, + 1, + 3.644, + -2, + 3.689, + -9.664, + 3.733, + -9.664, + 1, + 3.744, + -9.664, + 3.756, + -10.664, + 3.767, + -10.664, + 1, + 3.811, + -10.664, + 3.856, + 5.048, + 3.9, + 5.048, + 1, + 4, + 5.048, + 4.1, + -2, + 4.2, + -2, + 1, + 4.244, + -2, + 4.289, + -9.664, + 4.333, + -9.664, + 1, + 4.344, + -9.664, + 4.356, + -10.664, + 4.367, + -10.664, + 1, + 4.411, + -10.664, + 4.456, + 5.048, + 4.5, + 5.048, + 1, + 4.6, + 5.048, + 4.7, + -2, + 4.8, + -2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.1, + -15.897, + 0.2, + -30, + 0.3, + -30, + 1, + 0.4, + -30, + 0.5, + -20, + 0.6, + 0, + 1, + 0.7, + 20, + 0.8, + 30, + 0.9, + 30, + 1, + 1, + 30, + 1.1, + 0, + 1.2, + 0, + 1, + 1.3, + 0, + 1.4, + -30, + 1.5, + -30, + 1, + 1.6, + -30, + 1.7, + -20, + 1.8, + 0, + 1, + 1.9, + 20, + 2, + 30, + 2.1, + 30, + 1, + 2.2, + 30, + 2.3, + 0, + 2.4, + 0, + 1, + 2.5, + 0, + 2.6, + -30, + 2.7, + -30, + 1, + 2.8, + -30, + 2.9, + -20, + 3, + 0, + 1, + 3.1, + 20, + 3.2, + 30, + 3.3, + 30, + 1, + 3.4, + 30, + 3.5, + 0, + 3.6, + 0, + 1, + 3.7, + 0, + 3.8, + -30, + 3.9, + -30, + 1, + 4, + -30, + 4.1, + -20, + 4.2, + 0, + 1, + 4.3, + 20, + 4.4, + 30, + 4.5, + 30, + 1, + 4.6, + 30, + 4.7, + 0, + 4.8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 1, + 0.8, + 0, + 1, + 0, + 1.2, + 0, + 1, + 1.4, + 0, + 1.6, + 0, + 1.8, + 0, + 1, + 2, + 0, + 2.2, + 0, + 2.4, + 0, + 1, + 2.6, + 0, + 2.8, + 0, + 3, + 0, + 1, + 3.2, + 0, + 3.4, + 0, + 3.6, + 0, + 1, + 3.8, + 0, + 4, + 0, + 4.2, + 0, + 1, + 4.4, + 0, + 4.6, + 0, + 4.8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 1, + 0.8, + 0, + 1, + 0, + 1.2, + 0, + 1, + 1.4, + 0, + 1.6, + 0, + 1.8, + 0, + 1, + 2, + 0, + 2.2, + 0, + 2.4, + 0, + 1, + 2.6, + 0, + 2.8, + 0, + 3, + 0, + 1, + 3.2, + 0, + 3.4, + 0, + 3.6, + 0, + 1, + 3.8, + 0, + 4, + 0, + 4.2, + 0, + 1, + 4.4, + 0, + 4.6, + 0, + 4.8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 1, + 0.8, + 0, + 1, + 0, + 1.2, + 0, + 1, + 1.4, + 0, + 1.6, + 0, + 1.8, + 0, + 1, + 2, + 0, + 2.2, + 0, + 2.4, + 0, + 1, + 2.6, + 0, + 2.8, + 0, + 3, + 0, + 1, + 3.2, + 0, + 3.4, + 0, + 3.6, + 0, + 1, + 3.8, + 0, + 4, + 0, + 4.2, + 0, + 1, + 4.4, + 0, + 4.6, + 0, + 4.8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 1, + 0.8, + 0, + 1, + 0, + 1.2, + 0, + 1, + 1.4, + 0, + 1.6, + 0, + 1.8, + 0, + 1, + 2, + 0, + 2.2, + 0, + 2.4, + 0, + 1, + 2.6, + 0, + 2.8, + 0, + 3, + 0, + 1, + 3.2, + 0, + 3.4, + 0, + 3.6, + 0, + 1, + 3.8, + 0, + 4, + 0, + 4.2, + 0, + 1, + 4.4, + 0, + 4.6, + 0, + 4.8, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHead", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairFront", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeL", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeBallL", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeWhiteL", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeR", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeBallR", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeWhiteR", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairSide", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartMouth", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartOral", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartFace", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartBody", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmL", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmR", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairBack", + "Segments": [ + 0, + 1, + 0, + 4.8, + 1 + ] + } + ] +} +``` + +`Content/Samples/Mark/motions/mark_m03.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 1.967, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 38, + "TotalSegmentCount": 110, + "TotalPointCount": 294, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.444, + 0, + 0.556, + 1, + 0.667, + 1, + 1, + 0.756, + 1, + 0.844, + 0, + 0.933, + 0, + 1, + 1.044, + 0, + 1.156, + 0, + 1.267, + 0, + 1, + 1.378, + 0, + 1.489, + 0, + 1.6, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 0, + 0.133, + 0, + 1, + 0.244, + 0, + 0.356, + -29, + 0.467, + -29, + 1, + 0.578, + -29, + 0.689, + 30, + 0.8, + 30, + 1, + 0.889, + 30, + 0.978, + -30, + 1.067, + -30, + 1, + 1.178, + -30, + 1.289, + -30.267, + 1.4, + -28.344, + 1, + 1.511, + -26.422, + 1.622, + 9, + 1.733, + 9, + 0, + 1.967, + 9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + -2, + 0.367, + -2, + 1, + 0.478, + -2, + 0.589, + 3, + 0.7, + 3, + 1, + 0.789, + 3, + 0.878, + -8, + 0.967, + -8, + 1, + 1.078, + -8, + 1.189, + -8.102, + 1.3, + -7.558, + 1, + 1.411, + -7.015, + 1.522, + 0, + 1.633, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.433, + 1, + 0.867, + 0.01, + 1.3, + 0.01, + 1, + 1.422, + 0.01, + 1.544, + 1, + 1.667, + 1, + 1, + 1.767, + 1, + 1.867, + 1, + 1.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.433, + 1, + 0.867, + 0.01, + 1.3, + 0.01, + 1, + 1.422, + 0.01, + 1.544, + 1, + 1.667, + 1, + 1, + 1.767, + 1, + 1.867, + 1, + 1.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.756, + 0, + 0.844, + 0, + 0.933, + 0, + 1, + 1.078, + 0, + 1.222, + 0, + 1.367, + 0, + 1, + 1.467, + 0, + 1.567, + 0, + 1.667, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.756, + 0, + 0.844, + -0.5, + 0.933, + -0.5, + 1, + 1.078, + -0.5, + 1.222, + -0.5, + 1.367, + -0.5, + 1, + 1.467, + -0.5, + 1.567, + 0, + 1.667, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 1, + 1, + 0.311, + 1, + 0.622, + 0.8, + 0.933, + 0.8, + 1, + 1.078, + 0.8, + 1.222, + 0.8, + 1.367, + 0.8, + 1, + 1.467, + 0.8, + 1.567, + 1, + 1.667, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 1, + 1, + 0.311, + 1, + 0.622, + 0.8, + 0.933, + 0.8, + 1, + 1.078, + 0.8, + 1.222, + 0.8, + 1.367, + 0.8, + 1, + 1.467, + 0.8, + 1.567, + 1, + 1.667, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.444, + 0, + 0.556, + 0.1, + 0.667, + 0.1, + 1, + 0.756, + 0.1, + 0.844, + 0, + 0.933, + 0, + 1, + 1.044, + 0, + 1.156, + 0, + 1.267, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + -3, + 0.333, + -3, + 1, + 0.444, + -3, + 0.556, + -3, + 0.667, + -3, + 1, + 0.756, + -3, + 0.844, + 2.157, + 0.933, + 3, + 1, + 1.044, + 4.054, + 1.156, + 4, + 1.267, + 4, + 1, + 1.378, + 4, + 1.489, + 0, + 1.6, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + -3, + 0.333, + -3, + 1, + 0.444, + -3, + 0.556, + -3, + 0.667, + -3, + 1, + 0.756, + -3, + 0.844, + 2.157, + 0.933, + 3, + 1, + 1.044, + 4.054, + 1.156, + 4, + 1.267, + 4, + 1, + 1.378, + 4, + 1.489, + 0, + 1.6, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftLeg", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 0, + 0.933, + 0, + 1, + 1.044, + 0, + 1.156, + 0, + 1.267, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightLeg", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 0, + 0.933, + 0, + 1, + 1.044, + 0, + 1.156, + 0, + 1.267, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 0, + 0.933, + 0, + 1, + 1.044, + 0, + 1.156, + 0, + 1.267, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + -13, + 0.333, + -13, + 1, + 0.444, + -13, + 0.556, + 28, + 0.667, + 28, + 1, + 0.756, + 28, + 0.844, + -25, + 0.933, + -25, + 1, + 1.044, + -25, + 1.156, + -25.229, + 1.267, + -23.62, + 1, + 1.378, + -22.012, + 1.489, + 0, + 1.6, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 11, + 0.333, + 11, + 1, + 0.533, + 11, + 0.733, + -7.17, + 0.933, + -12, + 1, + 1.044, + -14.684, + 1.156, + -14, + 1.267, + -14, + 1, + 1.378, + -14, + 1.489, + 0, + 1.6, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 0, + 0.933, + 0, + 1, + 1.044, + 0, + 1.156, + 0, + 1.267, + 0, + 1, + 1.378, + 0, + 1.489, + 0, + 1.6, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 0, + 0.933, + 0, + 1, + 1.044, + 0, + 1.156, + 0, + 1.267, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 0, + 0.933, + 0, + 1, + 1.044, + 0, + 1.156, + 0, + 1.267, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 0, + 0.933, + 0, + 1, + 1.044, + 0, + 1.156, + 0, + 1.267, + 0, + 0, + 1.967, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHead", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairFront", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeL", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeBallL", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeWhiteL", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeR", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeBallR", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeWhiteR", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairSide", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartMouth", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartOral", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartFace", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartBody", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmL", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmR", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairBack", + "Segments": [ + 0, + 1, + 0, + 1.967, + 1 + ] + } + ] +} +``` + +`Content/Samples/Mark/motions/mark_m04.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4.833, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 38, + "TotalSegmentCount": 227, + "TotalPointCount": 613, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 0.422, + 1, + 0.844, + 1, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.333, + 0, + 0.4, + 0, + 0.467, + 0, + 1, + 0.544, + 0, + 0.622, + 0, + 0.7, + 0, + 1, + 0.778, + 0, + 0.856, + -30, + 0.933, + -30, + 1, + 1.022, + -30, + 1.111, + 30, + 1.2, + 30, + 1, + 1.322, + 30, + 1.444, + -30, + 1.567, + -30, + 1, + 1.733, + -30, + 1.9, + -21.371, + 2.067, + -14.414, + 1, + 2.211, + -8.384, + 2.356, + -1.605, + 2.5, + -0.937, + 1, + 2.711, + 0.038, + 2.922, + 0, + 3.133, + 0, + 1, + 3.4, + 0, + 3.667, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + -9, + 0.267, + -9, + 1, + 0.333, + -9, + 0.4, + 12.934, + 0.467, + 17.383, + 1, + 0.544, + 22.842, + 0.622, + 25.06, + 0.7, + 26.008, + 1, + 1.156, + 28.495, + 1.611, + 27, + 2.067, + 27, + 1, + 2.133, + 27, + 2.2, + 30, + 2.267, + 30, + 1, + 2.344, + 30, + 2.422, + -27.904, + 2.5, + -28.404, + 1, + 2.833, + -29.378, + 3.167, + -29.602, + 3.5, + -29.602, + 1, + 3.644, + -29.602, + 3.789, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.333, + 0, + 0.4, + 0, + 0.467, + 0, + 1, + 0.544, + 0, + 0.622, + 0, + 0.7, + 0, + 1, + 0.778, + 0, + 0.856, + -4, + 0.933, + -4, + 1, + 1.022, + -4, + 1.111, + 7.748, + 1.2, + 7.748, + 1, + 1.322, + 7.748, + 1.444, + -5.167, + 1.567, + -6.83, + 1, + 1.733, + -7.753, + 1.9, + -8.524, + 2.067, + -8.524, + 1, + 2.089, + -9.402, + 2.111, + -11.282, + 2.133, + -12, + 1, + 2.256, + -19.047, + 2.378, + 26.698, + 2.5, + 26.698, + 1, + 2.833, + 27.861, + 3.167, + 28, + 3.5, + 28, + 1, + 3.722, + 28, + 3.944, + -4, + 4.167, + -4, + 1, + 4.311, + -4, + 4.456, + 0, + 4.6, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.311, + 1, + 0.622, + 1, + 0.933, + 1, + 1, + 1.822, + 1, + 2.711, + 0, + 3.6, + 0, + 1, + 3.711, + 0, + 3.822, + 0.997, + 3.933, + 0.997, + 1, + 4.233, + 0.997, + 4.533, + 0.997, + 4.833, + 0.997 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.311, + 1, + 0.622, + 1, + 0.933, + 1, + 1, + 1.822, + 1, + 2.711, + 0, + 3.6, + 0, + 1, + 3.711, + 0, + 3.822, + 0.997, + 3.933, + 0.997, + 1, + 4.233, + 0.997, + 4.533, + 0.997, + 4.833, + 0.997 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.333, + 0, + 0.4, + 0, + 0.467, + 0, + 1, + 0.622, + 0, + 0.778, + 0, + 0.933, + 0, + 1, + 0.978, + 0, + 1.022, + 0, + 1.067, + 0, + 1, + 1.4, + 0, + 1.733, + 0, + 2.067, + 0, + 1, + 2.211, + 0, + 2.356, + 0, + 2.5, + 0, + 1, + 2.867, + 0, + 3.233, + 0, + 3.6, + 0, + 1, + 3.644, + 0, + 3.689, + 0, + 3.733, + 0, + 1, + 3.767, + 0, + 3.8, + 0, + 3.833, + 0, + 1, + 3.867, + 0, + 3.9, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.333, + 0, + 0.4, + 0, + 0.467, + 0, + 1, + 0.622, + 0, + 0.778, + 0, + 0.933, + 0, + 1, + 0.978, + 0, + 1.022, + -1, + 1.067, + -1, + 1, + 1.4, + -1, + 1.733, + -1, + 2.067, + -1, + 1, + 2.211, + -1, + 2.356, + -1.001, + 2.5, + -0.996, + 1, + 2.867, + -0.982, + 3.233, + -0.965, + 3.6, + -0.965, + 1, + 3.644, + -0.965, + 3.689, + -1, + 3.733, + -1, + 1, + 3.767, + -1, + 3.8, + -0.72, + 3.833, + -0.4, + 1, + 3.867, + -0.08, + 3.9, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 1, + 1, + 0.089, + 1, + 0.178, + 1, + 0.267, + 1, + 1, + 0.333, + 1, + 0.4, + 1.002, + 0.467, + 1.002, + 1, + 0.544, + 1.002, + 0.622, + 1.04, + 0.7, + 0.7, + 1, + 0.778, + 0.36, + 0.856, + -1, + 0.933, + -1, + 1, + 1.311, + -1, + 1.689, + -1, + 2.067, + -1, + 1, + 2.211, + -1, + 2.356, + -0.988, + 2.5, + -0.986, + 1, + 2.867, + -0.98, + 3.233, + -0.979, + 3.6, + -0.973, + 1, + 3.711, + -0.972, + 3.822, + 1, + 3.933, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 1, + 1, + 0.089, + 1, + 0.178, + 1, + 0.267, + 1, + 1, + 0.333, + 1, + 0.4, + 1.002, + 0.467, + 1.002, + 1, + 0.544, + 1.002, + 0.622, + 1.04, + 0.7, + 0.7, + 1, + 0.778, + 0.36, + 0.856, + -1, + 0.933, + -1, + 1, + 1.311, + -1, + 1.689, + -1, + 2.067, + -1, + 1, + 2.211, + -1, + 2.356, + -0.988, + 2.5, + -0.986, + 1, + 2.867, + -0.98, + 3.233, + -0.979, + 3.6, + -0.973, + 1, + 3.711, + -0.972, + 3.822, + 1, + 3.933, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0.1, + 0.267, + 0.1, + 1, + 0.333, + 0.1, + 0.4, + 0.101, + 0.467, + 0.097, + 1, + 0.544, + 0.093, + 0.622, + 0, + 0.7, + 0, + 1, + 1.156, + 0, + 1.611, + 0, + 2.067, + 0, + 1, + 2.211, + 0, + 2.356, + 0, + 2.5, + 0, + 1, + 2.711, + 0, + 2.922, + 0, + 3.133, + 0, + 1, + 3.4, + 0, + 3.667, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 2.806, + 0.267, + 4, + 1, + 0.333, + 4.895, + 0.4, + 5.285, + 0.467, + 5.597, + 1, + 0.544, + 5.96, + 0.622, + 6, + 0.7, + 6, + 1, + 0.778, + 6, + 0.856, + 4.44, + 0.933, + 3, + 1, + 1.022, + 1.492, + 1.111, + 0.206, + 1.2, + -1, + 1, + 1.322, + -3.459, + 1.444, + -6, + 1.567, + -6, + 1, + 1.733, + -6, + 1.9, + -6, + 2.067, + -6, + 1, + 2.156, + -6, + 2.244, + 3, + 2.333, + 3, + 1, + 2.478, + 3, + 2.622, + -6, + 2.767, + -6, + 1, + 2.889, + -6, + 3.011, + -6.079, + 3.133, + -5.92, + 1, + 3.4, + -5.575, + 3.667, + 0, + 3.933, + 0, + 1, + 4.1, + 0, + 4.267, + -2, + 4.433, + -2, + 0, + 4.833, + -2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 2.823, + 0.267, + 4, + 1, + 0.333, + 4.883, + 0.4, + 5.24, + 0.467, + 5.567, + 1, + 0.544, + 5.948, + 0.622, + 6, + 0.7, + 6, + 1, + 0.778, + 6, + 0.856, + 5.163, + 0.933, + 3, + 1, + 1.022, + 0.528, + 1.111, + -1, + 1.2, + -1, + 1, + 1.322, + -1, + 1.444, + 0, + 1.567, + 0, + 1, + 1.733, + 0, + 1.9, + -1, + 2.067, + -1, + 1, + 2.156, + -1, + 2.244, + 3, + 2.333, + 3, + 1, + 2.478, + 3, + 2.622, + -10, + 2.767, + -10, + 1, + 2.889, + -10, + 3.011, + -10, + 3.133, + -10, + 1, + 3.4, + -10, + 3.667, + 0, + 3.933, + 0, + 1, + 4.1, + 0, + 4.267, + -1, + 4.433, + -1, + 0, + 4.833, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftLeg", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.333, + 0, + 0.4, + 0, + 0.467, + 0, + 1, + 0.544, + 0, + 0.622, + 0, + 0.7, + 0, + 1, + 1.156, + 0, + 1.611, + 0, + 2.067, + 0, + 1, + 2.211, + 0, + 2.356, + 0, + 2.5, + 0, + 1, + 2.711, + 0, + 2.922, + 0, + 3.133, + 0, + 1, + 3.4, + 0, + 3.667, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightLeg", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.333, + 0, + 0.4, + 0, + 0.467, + 0, + 1, + 0.544, + 0, + 0.622, + 0, + 0.7, + 0, + 1, + 1.156, + 0, + 1.611, + 0, + 2.067, + 0, + 1, + 2.211, + 0, + 2.356, + 0, + 2.5, + 0, + 1, + 2.711, + 0, + 2.922, + 0, + 3.133, + 0, + 1, + 3.4, + 0, + 3.667, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.333, + 0, + 0.4, + 0, + 0.467, + 0, + 1, + 0.544, + 0, + 0.622, + 0, + 0.7, + 0, + 1, + 0.778, + 0, + 0.856, + -3, + 0.933, + -3, + 1, + 1.022, + -3, + 1.111, + 3, + 1.2, + 3, + 1, + 1.322, + 3, + 1.444, + -3, + 1.567, + -3, + 1, + 1.733, + -3, + 1.9, + 0, + 2.067, + 0, + 1, + 2.211, + 0, + 2.356, + 0, + 2.5, + 0, + 1, + 2.711, + 0, + 2.922, + 0, + 3.133, + 0, + 1, + 3.4, + 0, + 3.667, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 6.266, + 0.267, + 6.266, + 1, + 0.333, + 6.266, + 0.4, + 0, + 0.467, + 0, + 1, + 0.544, + 0, + 0.622, + 0, + 0.7, + 0, + 1, + 0.856, + 0, + 1.011, + -0.384, + 1.167, + 1.279, + 1, + 1.333, + 2.814, + 1.5, + 12.92, + 1.667, + 17.01, + 1, + 1.8, + 18.288, + 1.933, + 18, + 2.067, + 18, + 1, + 2.211, + 2.046, + 2.356, + -22, + 2.5, + -22, + 1, + 2.711, + -22, + 2.922, + -22.255, + 3.133, + -21.372, + 1, + 3.4, + -20.257, + 3.667, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 0, + 0.7, + 0, + 1, + 0.778, + 0, + 0.856, + -8, + 0.933, + -8, + 1, + 1.022, + -8, + 1.111, + 8, + 1.2, + 8, + 1, + 1.322, + 8, + 1.444, + -17.73, + 1.567, + -20.105, + 1, + 1.733, + -23.344, + 1.9, + -23.3, + 2.067, + -23.3, + 1, + 2.211, + -23.3, + 2.356, + 21.227, + 2.5, + 23.072, + 1, + 2.711, + 25.768, + 2.922, + 25.699, + 3.133, + 25.699, + 1, + 3.4, + 25.699, + 3.667, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.333, + 0, + 0.4, + 0, + 0.467, + 0, + 1, + 0.544, + 0, + 0.622, + 0, + 0.7, + 0, + 1, + 1.156, + 0, + 1.611, + 0, + 2.067, + 0, + 1, + 2.211, + 0, + 2.356, + 0, + 2.5, + 0, + 1, + 2.711, + 0, + 2.922, + 0, + 3.133, + 0, + 1, + 3.4, + 0, + 3.667, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.333, + 0, + 0.4, + 0, + 0.467, + 0, + 1, + 0.544, + 0, + 0.622, + 0, + 0.7, + 0, + 1, + 1.156, + 0, + 1.611, + 0, + 2.067, + 0, + 1, + 2.211, + 0, + 2.356, + 0, + 2.5, + 0, + 1, + 2.711, + 0, + 2.922, + 0, + 3.133, + 0, + 1, + 3.4, + 0, + 3.667, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.333, + 0, + 0.4, + 0, + 0.467, + 0, + 1, + 0.544, + 0, + 0.622, + 0, + 0.7, + 0, + 1, + 1.156, + 0, + 1.611, + 0, + 2.067, + 0, + 1, + 2.211, + 0, + 2.356, + 0, + 2.5, + 0, + 1, + 2.711, + 0, + 2.922, + 0, + 3.133, + 0, + 1, + 3.4, + 0, + 3.667, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.333, + 0, + 0.4, + 0, + 0.467, + 0, + 1, + 0.544, + 0, + 0.622, + 0, + 0.7, + 0, + 1, + 1.156, + 0, + 1.611, + 0, + 2.067, + 0, + 1, + 2.211, + 0, + 2.356, + 0, + 2.5, + 0, + 1, + 2.711, + 0, + 2.922, + 0, + 3.133, + 0, + 1, + 3.4, + 0, + 3.667, + 0, + 3.933, + 0, + 0, + 4.833, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHead", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairFront", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeL", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeBallL", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeWhiteL", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeR", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeBallR", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeWhiteR", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairSide", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartMouth", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartOral", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartFace", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartBody", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmL", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmR", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairBack", + "Segments": [ + 0, + 1, + 2, + 1.267, + 1, + 0, + 4.833, + 1 + ] + } + ] +} +``` + +`Content/Samples/Mark/motions/mark_m05.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 39, + "TotalSegmentCount": 180, + "TotalPointCount": 511, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 1, + 0.111, + 1, + 0.222, + 1, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.556, + 0, + 0.778, + 3, + 1, + 3, + 1, + 1.211, + 3, + 1.422, + 3, + 1.633, + 3, + 1, + 1.733, + 3, + 1.833, + -2, + 1.933, + -2, + 1, + 2.222, + -2, + 2.511, + -1.804, + 2.8, + -1, + 1, + 2.911, + -0.691, + 3.022, + 0, + 3.133, + 0, + 1, + 3.422, + 0, + 3.711, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.122, + 0, + 0.244, + 0, + 0.367, + 0, + 1, + 0.456, + 0, + 0.544, + 24, + 0.633, + 24, + 1, + 0.767, + 24, + 0.9, + -17, + 1.033, + -17, + 1, + 1.267, + -17, + 1.5, + -17, + 1.733, + -17, + 1, + 1.778, + -17, + 1.822, + -29.898, + 1.867, + -29.898, + 1, + 1.922, + -29.898, + 1.978, + 13.726, + 2.033, + 21, + 1, + 2.089, + 28.274, + 2.144, + 27.545, + 2.2, + 27.545, + 1, + 2.433, + 27.545, + 2.667, + -8, + 2.9, + -8, + 1, + 3.011, + -8, + 3.122, + 0, + 3.233, + 0, + 1, + 3.489, + 0, + 3.744, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.144, + 0, + 0.289, + 0, + 0.433, + 0, + 1, + 0.656, + 0, + 0.878, + -17, + 1.1, + -17, + 1, + 1.344, + -17, + 1.589, + -17, + 1.833, + -17, + 1, + 1.922, + -17, + 2.011, + 19, + 2.1, + 19, + 1, + 2.4, + 19, + 2.7, + -4, + 3, + -4, + 1, + 3.111, + -4, + 3.222, + 0, + 3.333, + 0, + 1, + 3.556, + 0, + 3.778, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.2, + 1, + 0.4, + 1, + 0.6, + 1, + 1, + 0.733, + 1, + 0.867, + 0.7, + 1, + 0.7, + 1, + 1.222, + 0.7, + 1.444, + 0.7, + 1.667, + 0.7, + 1, + 1.711, + 0.7, + 1.756, + 0, + 1.8, + 0, + 1, + 1.811, + 0, + 1.822, + 0, + 1.833, + 0, + 1, + 1.911, + 0, + 1.989, + 1, + 2.067, + 1, + 1, + 2.711, + 1, + 3.356, + 1, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.2, + 1, + 0.4, + 1, + 0.6, + 1, + 1, + 0.733, + 1, + 0.867, + 0.7, + 1, + 0.7, + 1, + 1.222, + 0.7, + 1.444, + 0.7, + 1.667, + 0.7, + 1, + 1.711, + 0.7, + 1.756, + 0, + 1.8, + 0, + 1, + 1.811, + 0, + 1.822, + 0, + 1.833, + 0, + 1, + 1.911, + 0, + 1.989, + 1, + 2.067, + 1, + 1, + 2.711, + 1, + 3.356, + 1, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.556, + 0, + 0.778, + 0.1, + 1, + 0.1, + 1, + 1.222, + 0.1, + 1.444, + 0.1, + 1.667, + 0.1, + 1, + 1.689, + 0.1, + 1.711, + 0.2, + 1.733, + 0.2, + 1, + 1.8, + 0.2, + 1.867, + 0.177, + 1.933, + 0.1, + 1, + 1.978, + 0.049, + 2.022, + 0, + 2.067, + 0, + 1, + 2.333, + 0, + 2.6, + 0, + 2.867, + 0, + 1, + 3.244, + 0, + 3.622, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.556, + 0, + 0.778, + -0.6, + 1, + -0.6, + 1, + 1.222, + -0.6, + 1.444, + -0.6, + 1.667, + -0.6, + 1, + 1.689, + -0.6, + 1.711, + -1, + 1.733, + -1, + 1, + 1.8, + -1, + 1.867, + -1.006, + 1.933, + -0.9, + 1, + 1.978, + -0.829, + 2.022, + 0.1, + 2.067, + 0.1, + 1, + 2.333, + 0.1, + 2.6, + 0, + 2.867, + 0, + 1, + 3.244, + 0, + 3.622, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 1, + 1, + 0.111, + 1, + 0.222, + 1, + 0.333, + 1, + 1, + 0.556, + 1, + 0.778, + 0.9, + 1, + 0.9, + 1, + 1.233, + 0.9, + 1.467, + 0.9, + 1.7, + 0.9, + 1, + 1.744, + 0.9, + 1.789, + 0.7, + 1.833, + 0.7, + 1, + 1.889, + 0.7, + 1.944, + 1, + 2, + 1, + 1, + 2.667, + 1, + 3.333, + 1, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 1, + 1, + 0.111, + 1, + 0.222, + 1, + 0.333, + 1, + 1, + 0.556, + 1, + 0.778, + 0.9, + 1, + 0.9, + 1, + 1.233, + 0.9, + 1.467, + 0.9, + 1.7, + 0.9, + 1, + 1.744, + 0.9, + 1.789, + 0.7, + 1.833, + 0.7, + 1, + 1.889, + 0.7, + 1.944, + 1, + 2, + 1, + 1, + 2.667, + 1, + 3.333, + 1, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.556, + 0, + 0.778, + 0, + 1, + 0, + 1, + 1.233, + 0, + 1.467, + 0, + 1.7, + 0, + 1, + 1.744, + 0, + 1.789, + 0, + 1.833, + 0, + 1, + 1.889, + 0, + 1.944, + 1, + 2, + 1, + 1, + 2.067, + 1, + 2.133, + 1, + 2.2, + 1, + 1, + 2.422, + 1, + 2.644, + 0, + 2.867, + 0, + 1, + 3.244, + 0, + 3.622, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.556, + 0, + 0.778, + 3, + 1, + 3, + 1, + 1.233, + 3, + 1.467, + 3, + 1.7, + 3, + 1, + 1.744, + 3, + 1.789, + 1, + 1.833, + 1, + 1, + 1.889, + 1, + 1.944, + 11.968, + 2, + 15, + 1, + 2.211, + 26.52, + 2.422, + 30, + 2.633, + 30, + 1, + 2.789, + 30, + 2.944, + -1, + 3.1, + -1, + 1, + 3.322, + -1, + 3.544, + 0, + 3.767, + 0, + 1, + 3.844, + 0, + 3.922, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.556, + 0, + 0.778, + 3, + 1, + 3, + 1, + 1.233, + 3, + 1.467, + 3, + 1.7, + 3, + 1, + 1.744, + 3, + 1.789, + 1, + 1.833, + 1, + 1, + 1.889, + 1, + 1.944, + 11.968, + 2, + 15, + 1, + 2.211, + 26.52, + 2.422, + 30, + 2.633, + 30, + 1, + 2.789, + 30, + 2.944, + -1, + 3.1, + -1, + 1, + 3.322, + -1, + 3.544, + 0, + 3.767, + 0, + 1, + 3.844, + 0, + 3.922, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftLeg", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.556, + 0, + 0.778, + 0, + 1, + 0, + 1, + 1.233, + 0, + 1.467, + 0, + 1.7, + 0, + 1, + 1.8, + 0, + 1.9, + 0, + 2, + 0, + 1, + 2.667, + 0, + 3.333, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightLeg", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.556, + 0, + 0.778, + 0, + 1, + 0, + 1, + 1.233, + 0, + 1.467, + 0, + 1.7, + 0, + 1, + 1.8, + 0, + 1.9, + 7.138, + 2, + 8, + 1, + 2.211, + 9.82, + 2.422, + 10, + 2.633, + 10, + 1, + 2.711, + 10, + 2.789, + 0, + 2.867, + 0, + 1, + 3.244, + 0, + 3.622, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.556, + 0, + 0.778, + 0, + 1, + 0, + 1, + 1.233, + 0, + 1.467, + 0, + 1.7, + 0, + 1, + 1.8, + 0, + 1.9, + -4, + 2, + -4, + 1, + 2.289, + -4, + 2.578, + 0, + 2.867, + 0, + 1, + 3.244, + 0, + 3.622, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 6, + 1, + 0.111, + 6, + 0.222, + 6, + 0.333, + 6, + 1, + 0.556, + 6, + 0.778, + -30, + 1, + -30, + 1, + 1.233, + -30, + 1.467, + -30, + 1.7, + -30, + 1, + 1.8, + -30, + 1.9, + 22, + 2, + 22, + 1, + 2.289, + 22, + 2.578, + -7, + 2.867, + -7, + 1, + 2.978, + -7, + 3.089, + -2.025, + 3.2, + 0, + 1, + 3.467, + 4.86, + 3.733, + 6, + 4, + 6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.422, + 0, + 0.511, + 13, + 0.6, + 13, + 1, + 0.733, + 13, + 0.867, + -14, + 1, + -14, + 1, + 1.233, + -14, + 1.467, + -14, + 1.7, + -14, + 1, + 1.8, + -14, + 1.9, + 13.084, + 2, + 18, + 1, + 2.211, + 28.379, + 2.422, + 30, + 2.633, + 30, + 1, + 2.711, + 30, + 2.789, + -13, + 2.867, + -13, + 1, + 3.244, + -13, + 3.622, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.556, + 0, + 0.778, + 0, + 1, + 0, + 1, + 1.233, + 0, + 1.467, + 0, + 1.7, + 0, + 1, + 1.8, + 0, + 1.9, + 0, + 2, + 0, + 1, + 2.667, + 0, + 3.333, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.556, + 0, + 0.778, + 0, + 1, + 0, + 1, + 1.233, + 0, + 1.467, + 0, + 1.7, + 0, + 1, + 1.8, + 0, + 1.9, + 0, + 2, + 0, + 1, + 2.667, + 0, + 3.333, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.556, + 0, + 0.778, + 0, + 1, + 0, + 1, + 1.233, + 0, + 1.467, + 0, + 1.7, + 0, + 1, + 1.8, + 0, + 1.9, + 0, + 2, + 0, + 1, + 2.667, + 0, + 3.333, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.556, + 0, + 0.778, + 0, + 1, + 0, + 1, + 1.233, + 0, + 1.467, + 0, + 1.7, + 0, + 1, + 1.8, + 0, + 1.9, + 0, + 2, + 0, + 1, + 2.667, + 0, + 3.333, + 0, + 4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHead", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairFront", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeL", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeBallL", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeWhiteL", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeR", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeBallR", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeWhiteR", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairSide", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartMouth", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartOral", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartFace", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartBody", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmL", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmR", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairBack", + "Segments": [ + 0, + 1, + 2, + 0.333, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "Part", + "Segments": [ + 0, + 1, + 0, + 4, + 1 + ] + } + ] +} +``` + +`Content/Samples/Mark/motions/mark_m06.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 1.433, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 38, + "TotalSegmentCount": 100, + "TotalPointCount": 266, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Model", + "Id": "Opacity", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + -2, + 0.3, + -2, + 1, + 0.478, + -2, + 0.656, + 4, + 0.833, + 4, + 1, + 0.911, + 4, + 0.989, + -2, + 1.067, + -2, + 1, + 1.156, + -2, + 1.244, + 0, + 1.333, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + -30, + 0.3, + -30, + 1, + 0.478, + -30, + 0.656, + 21, + 0.833, + 21, + 1, + 0.911, + 21, + 0.989, + -12, + 1.067, + -12, + 1, + 1.156, + -12, + 1.244, + 0, + 1.333, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + -2, + 0.3, + -2, + 1, + 0.478, + -2, + 0.656, + 4, + 0.833, + 4, + 1, + 0.911, + 4, + 0.989, + -2, + 1.067, + -2, + 1, + 1.156, + -2, + 1.244, + 0, + 1.333, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.478, + 1, + 0.956, + 1, + 1.433, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.478, + 1, + 0.956, + 1, + 1.433, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.478, + 0, + 0.656, + 0, + 0.833, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.478, + 0, + 0.656, + 0, + 0.833, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 1, + 1, + 0.1, + 1, + 0.2, + 1, + 0.3, + 1, + 1, + 0.478, + 1, + 0.656, + 1, + 0.833, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 1, + 1, + 0.1, + 1, + 0.2, + 1, + 0.3, + 1, + 1, + 0.478, + 1, + 0.656, + 1, + 0.833, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.344, + 0, + 0.389, + 0.858, + 0.433, + 0.9, + 1, + 0.533, + 0.994, + 0.633, + 1, + 0.733, + 1, + 1, + 0.811, + 1, + 0.889, + 0, + 0.967, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 6, + 0.3, + 6, + 1, + 0.344, + 6, + 0.389, + -4, + 0.433, + -4, + 1, + 0.478, + -4, + 0.522, + 8.164, + 0.567, + 12, + 1, + 0.656, + 19.673, + 0.744, + 21, + 0.833, + 21, + 1, + 0.911, + 21, + 0.989, + -5, + 1.067, + -5, + 1, + 1.156, + -5, + 1.244, + 0, + 1.333, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 6, + 0.3, + 6, + 1, + 0.344, + 6, + 0.389, + -4, + 0.433, + -4, + 1, + 0.478, + -4, + 0.522, + 8.164, + 0.567, + 12, + 1, + 0.656, + 19.673, + 0.744, + 21, + 0.833, + 21, + 1, + 0.911, + 21, + 0.989, + -5, + 1.067, + -5, + 1, + 1.156, + -5, + 1.244, + 0, + 1.333, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftLeg", + "Segments": [ + 0, + 0, + 1, + 0.144, + 0, + 0.289, + 0, + 0.433, + 0, + 1, + 0.467, + 0, + 0.5, + 10, + 0.533, + 10, + 1, + 0.556, + 10, + 0.578, + 10, + 0.6, + 10, + 1, + 0.678, + 10, + 0.756, + 0, + 0.833, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightLeg", + "Segments": [ + 0, + 0, + 1, + 0.144, + 0, + 0.289, + 0, + 0.433, + 0, + 1, + 0.467, + 0, + 0.5, + 10, + 0.533, + 10, + 1, + 0.556, + 10, + 0.578, + 10, + 0.6, + 10, + 1, + 0.678, + 10, + 0.756, + 0, + 0.833, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.478, + 0, + 0.656, + 0, + 0.833, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + -30, + 0.3, + -30, + 1, + 0.356, + -30, + 0.411, + 30, + 0.467, + 30, + 1, + 0.589, + 30, + 0.711, + -24, + 0.833, + -24, + 1, + 0.911, + -24, + 0.989, + 0, + 1.067, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + -19, + 0.3, + -19, + 1, + 0.478, + -19, + 0.656, + 12, + 0.833, + 12, + 1, + 0.911, + 12, + 0.989, + -2, + 1.067, + -2, + 1, + 1.156, + -2, + 1.244, + 0, + 1.333, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.478, + 0, + 0.656, + 0, + 0.833, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.478, + 0, + 0.656, + 0, + 0.833, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.478, + 0, + 0.656, + 0, + 0.833, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 0, + 0.3, + 0, + 1, + 0.478, + 0, + 0.656, + 0, + 0.833, + 0, + 0, + 1.433, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHead", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairFront", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeL", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeBallL", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeWhiteL", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeR", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeBallR", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartEyeWhiteR", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairSide", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartMouth", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartOral", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartFace", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartBody", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmL", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmR", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartHairBack", + "Segments": [ + 0, + 1, + 0, + 1.433, + 1 + ] + } + ] +} +``` + +`Content/Samples/Natori/Natori.model3.json`: + +```json +{ + "Version": 3, + "FileReferences": { + "Moc": "Natori.moc3", + "Textures": [ + "Natori.2048/texture_00.png" + ], + "Physics": "Natori.physics3.json", + "Pose": "Natori.pose3.json", + "Expressions": [ + { + "Name": "exp_00.exp3.json", + "File": "exp/exp_00.exp3.json" + }, + { + "Name": "exp_01.exp3.json", + "File": "exp/exp_01.exp3.json" + }, + { + "Name": "exp_02.exp3.json", + "File": "exp/exp_02.exp3.json" + }, + { + "Name": "exp_03.exp3.json", + "File": "exp/exp_03.exp3.json" + }, + { + "Name": "exp_04.exp3.json", + "File": "exp/exp_04.exp3.json" + }, + { + "Name": "exp_05.exp3.json", + "File": "exp/exp_05.exp3.json" + }, + { + "Name": "exp_06.exp3.json", + "File": "exp/exp_06.exp3.json" + }, + { + "Name": "exp_07.exp3.json", + "File": "exp/exp_07.exp3.json" + }, + { + "Name": "exp_08.exp3.json", + "File": "exp/exp_08.exp3.json" + }, + { + "Name": "exp_09.exp3.json", + "File": "exp/exp_09.exp3.json" + }, + { + "Name": "exp_10.exp3.json", + "File": "exp/exp_10.exp3.json" + } + ], + "Motions": { + "TapBody": [ + { + "File": "motions/mtn_01.motion3.json" + }, + { + "File": "motions/mtn_02.motion3.json" + }, + { + "File": "motions/mtn_03.motion3.json" + }, + { + "File": "motions/mtn_04.motion3.json" + }, + { + "File": "motions/mtn_05.motion3.json" + }, + { + "File": "motions/mtn_06.motion3.json" + }, + { + "File": "motions/mtn_07.motion3.json" + }, + { + "File": "motions/mtn_08.motion3.json" + } + ], + "Idle": [ + { + "File": "motions/mtn_00.motion3.json" + } + ] + } + }, + "Groups": [ + { + "Target": "Parameter", + "Name": "EyeBlink", + "Ids": [ + "ParamEyeLOpen", + "ParamEyeROpen" + ] + }, + { + "Target": "Parameter", + "Name": "LipSync", + "Ids": [ + "ParamMouthOpenY" + ] + } + ], + "HitAreas": [ + { + "Id": "HitAreaHead", + "Name": "Head" + }, + { + "Id": "HitAreaBody", + "Name": "Body" + } + ] +} +``` + +`Content/Samples/Natori/Natori.physics3.json`: + +```json +{ + "Version": 3, + "Meta": { + "PhysicsSettingCount": 11, + "TotalInputCount": 34, + "TotalOutputCount": 12, + "VertexCount": 23, + "EffectiveForces": { + "Gravity": { + "X": 0, + "Y": -1 + }, + "Wind": { + "X": 0, + "Y": 0 + } + }, + "PhysicsDictionary": [ + { + "Id": "PhysicsSetting1", + "Name": "前髪 揺れ" + }, + { + "Id": "PhysicsSetting2", + "Name": "横髪 揺れ" + }, + { + "Id": "PhysicsSetting3", + "Name": "後ろ髪 揺れ" + }, + { + "Id": "PhysicsSetting4", + "Name": "前髪ふわ" + }, + { + "Id": "PhysicsSetting5", + "Name": "横髪ふわ" + }, + { + "Id": "PhysicsSetting6", + "Name": "後ろ髪ふわ" + }, + { + "Id": "PhysicsSetting7", + "Name": "燕尾揺れ" + }, + { + "Id": "PhysicsSetting8", + "Name": "懐中時計腰 揺れ" + }, + { + "Id": "PhysicsSetting9", + "Name": "腰のチェーン揺れ" + }, + { + "Id": "PhysicsSetting10", + "Name": "懐中時計腰 横回転" + }, + { + "Id": "PhysicsSetting11", + "Name": "懐中時計B チェーン揺れ" + } + ] + }, + "PhysicsSettings": [ + { + "Id": "PhysicsSetting1", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHairFront" + }, + "VertexIndex": 1, + "Scale": 1.824, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 12.7 + }, + "Mobility": 0.95, + "Delay": 0.9, + "Acceleration": 1, + "Radius": 12.7 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting2", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHairSide" + }, + "VertexIndex": 1, + "Scale": 2, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 18.2 + }, + "Mobility": 0.95, + "Delay": 0.9, + "Acceleration": 1, + "Radius": 18.2 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting3", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHairBack" + }, + "VertexIndex": 1, + "Scale": 2, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 14.3 + }, + "Mobility": 1, + "Delay": 0.9, + "Acceleration": 1.42, + "Radius": 14.3 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting4", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleY" + }, + "Weight": 35, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleY" + }, + "Weight": 30, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyPosition" + }, + "Weight": 35, + "Type": "X", + "Reflect": true + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHairFrontFuwa" + }, + "VertexIndex": 1, + "Scale": 3, + "Weight": 100, + "Type": "Angle", + "Reflect": true + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 11.9 + }, + "Mobility": 0.79, + "Delay": 0.9, + "Acceleration": 1, + "Radius": 11.9 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting5", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleY" + }, + "Weight": 35, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleY" + }, + "Weight": 30, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyPosition" + }, + "Weight": 35, + "Type": "X", + "Reflect": true + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHairSideFuwa" + }, + "VertexIndex": 1, + "Scale": 3.5, + "Weight": 100, + "Type": "Angle", + "Reflect": true + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 14.3 + }, + "Mobility": 0.79, + "Delay": 0.9, + "Acceleration": 1.1, + "Radius": 14.3 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting6", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleY" + }, + "Weight": 35, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleY" + }, + "Weight": 30, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyPosition" + }, + "Weight": 35, + "Type": "X", + "Reflect": true + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHairBackFuwa" + }, + "VertexIndex": 1, + "Scale": 5, + "Weight": 100, + "Type": "Angle", + "Reflect": true + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 11.3 + }, + "Mobility": 0.79, + "Delay": 0.9, + "Acceleration": 1.16, + "Radius": 11.3 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting7", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 100, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 70, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamWaistAngleZ" + }, + "Weight": 30, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamJacket" + }, + "VertexIndex": 1, + "Scale": 1, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 31.9 + }, + "Mobility": 0.95, + "Delay": 0.8, + "Acceleration": 0.8, + "Radius": 31.9 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting8", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 100, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 70, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamWaistAngleZ" + }, + "Weight": 30, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamWatchSwingA1" + }, + "VertexIndex": 1, + "Scale": 1, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "ParamWatchSwingA2" + }, + "VertexIndex": 2, + "Scale": 1, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 15.4 + }, + "Mobility": 0.95, + "Delay": 1, + "Acceleration": 0.8, + "Radius": 15.4 + }, + { + "Position": { + "X": 0, + "Y": 31.9 + }, + "Mobility": 0.9, + "Delay": 1, + "Acceleration": 0.6, + "Radius": 16.5 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting9", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 100, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 70, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamWaistAngleZ" + }, + "Weight": 30, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamChainWaist" + }, + "VertexIndex": 1, + "Scale": 1, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 11.6 + }, + "Mobility": 0.95, + "Delay": 1, + "Acceleration": 1, + "Radius": 11.6 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting10", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 100, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 70, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamWaistAngleZ" + }, + "Weight": 30, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamWatchAX" + }, + "VertexIndex": 1, + "Scale": 2, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 72.1 + }, + "Mobility": 0.95, + "Delay": 1, + "Acceleration": 0.2, + "Radius": 72.1 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting11", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamArmBR03" + }, + "Weight": 100, + "Type": "X", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamWatchBChain" + }, + "VertexIndex": 1, + "Scale": 2, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 13.1 + }, + "Mobility": 0.95, + "Delay": 1, + "Acceleration": 0.66, + "Radius": 13.1 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + } + ] +} +``` + +`Content/Samples/Natori/Natori.pose3.json`: + +```json +{ + "Type": "Live2D Pose", + "FadeInTime": 0.2, + "Groups": [ + [ + { + "Id": "PartArmAL", + "Link": [] + }, + { + "Id": "PartArmCL", + "Link": [] + }, + { + "Id": "PartArmDL", + "Link": [] + } + ], + [ + { + "Id": "PartArmAR", + "Link": [] + }, + { + "Id": "PartArmBR", + "Link": [] + }, + { + "Id": "PartArmER", + "Link": [] + } + ], + [ + { + "Id": "PartWatchA", + "Link": [] + }, + { + "Id": "PartWatchB", + "Link": [] + } + ] + ] +} +``` + +`Content/Samples/Natori/exp/exp_00.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeROpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthOpenY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamTeethOn", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamCheek", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGlassUD", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassWhite", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlight", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlightMove", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeBallForm", + "Value": 0, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Natori/exp/exp_01.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 0, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeLSmile", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamEyeLForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeROpen", + "Value": 0, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeRSmile", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamEyeRForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm2", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthOpenY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamTeethOn", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamCheek", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGlassUD", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassWhite", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlight", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlightMove", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeBallForm", + "Value": 0, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Natori/exp/exp_02.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLForm", + "Value": 3, + "Blend": "Add" + }, + { + "Id": "ParamEyeROpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRForm", + "Value": 3, + "Blend": "Add" + }, + { + "Id": "ParamBrowLY", + "Value": -0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": -0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm2", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm2", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamMouthOpenY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm2", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamTeethOn", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamCheek", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGlassUD", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassWhite", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlight", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlightMove", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeBallForm", + "Value": 0, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Natori/exp/exp_03.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLForm", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamEyeROpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRForm", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLY", + "Value": 0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": 0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLAngle", + "Value": 0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": 0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm2", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm2", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamMouthOpenY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamTeethOn", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamCheek", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGlassUD", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassWhite", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlight", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlightMove", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeBallForm", + "Value": 0, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Natori/exp/exp_04.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLForm", + "Value": -2, + "Blend": "Add" + }, + { + "Id": "ParamEyeROpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRForm", + "Value": -2, + "Blend": "Add" + }, + { + "Id": "ParamBrowLY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLX", + "Value": 0.3, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": 0.3, + "Blend": "Add" + }, + { + "Id": "ParamBrowLAngle", + "Value": -0.4, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": -0.4, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": -2, + "Blend": "Add" + }, + { + "Id": "ParamMouthOpenY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamTeethOn", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamCheek", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGlassUD", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassWhite", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlight", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlightMove", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeBallForm", + "Value": 0, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Natori/exp/exp_05.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeROpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": -2, + "Blend": "Add" + }, + { + "Id": "ParamMouthOpenY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm2", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamTeethOn", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamCheek", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGlassUD", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassWhite", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlight", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlightMove", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeBallForm", + "Value": 0, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Natori/exp/exp_06.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLForm", + "Value": -2, + "Blend": "Add" + }, + { + "Id": "ParamEyeROpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRForm", + "Value": -2, + "Blend": "Add" + }, + { + "Id": "ParamBrowLY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamMouthOpenY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamTeethOn", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamCheek", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGlassUD", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassWhite", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlight", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlightMove", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeBallForm", + "Value": 0, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Natori/exp/exp_07.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeROpen", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": -1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": -2, + "Blend": "Add" + }, + { + "Id": "ParamMouthOpenY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm2", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamTeethOn", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamCheek", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamGlassUD", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassWhite", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlight", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlightMove", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeBallForm", + "Value": 0, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Natori/exp/exp_08.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 1.3, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeLSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeROpen", + "Value": 1.3, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeRSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLY", + "Value": 0.2, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": 0.2, + "Blend": "Add" + }, + { + "Id": "ParamBrowLX", + "Value": -0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": -0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLAngle", + "Value": 0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": 0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": -3, + "Blend": "Add" + }, + { + "Id": "ParamMouthOpenY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm2", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamTeethOn", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamCheek", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGlassUD", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassWhite", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlight", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlightMove", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeBallForm", + "Value": -1, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Natori/exp/exp_09.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 1.2, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeLSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeROpen", + "Value": 1.2, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeRSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLY", + "Value": 0.2, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": 0.2, + "Blend": "Add" + }, + { + "Id": "ParamBrowLX", + "Value": -0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": -0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLAngle", + "Value": 0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": 0.1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": -3, + "Blend": "Add" + }, + { + "Id": "ParamMouthOpenY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamTeethOn", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamCheek", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGlassUD", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassWhite", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlight", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlightMove", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeBallForm", + "Value": 0, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Natori/exp/exp_10.exp3.json`: + +```json +{ + "Type": "Live2D Expression", + "Parameters": [ + { + "Id": "ParamEyeLOpen", + "Value": 0, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeLSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeLForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeROpen", + "Value": 0, + "Blend": "Multiply" + }, + { + "Id": "ParamEyeRSmile", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeRForm", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRX", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRAngle", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamBrowLForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamBrowRForm2", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm", + "Value": -3, + "Blend": "Add" + }, + { + "Id": "ParamMouthOpenY", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamMouthForm2", + "Value": 1, + "Blend": "Add" + }, + { + "Id": "ParamTeethOn", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamCheek", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGlassUD", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassWhite", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlight", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamGrassHighlightMove", + "Value": 0, + "Blend": "Add" + }, + { + "Id": "ParamEyeBallForm", + "Value": 0, + "Blend": "Add" + } + ] +} +``` + +`Content/Samples/Natori/motions/mtn_00.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 3.967, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 1, + "TotalSegmentCount": 4, + "TotalPointCount": 13, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.756, + 0, + 1.244, + 1, + 1.733, + 1, + 1, + 1.844, + 1, + 1.956, + 1, + 2.067, + 1, + 1, + 2.7, + 1, + 3.333, + 0.034, + 3.967, + 0.001 + ] + } + ] +} +``` + +`Content/Samples/Natori/motions/mtn_01.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 7.97, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 96, + "TotalSegmentCount": 105, + "TotalPointCount": 389, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.611, + 0, + 1.222, + 5, + 1.833, + 5, + 1, + 2.4, + 5, + 2.967, + 0, + 3.533, + 0, + 1, + 4.278, + 0, + 5.022, + 4, + 5.767, + 4, + 1, + 6.5, + 4, + 7.233, + 0.119, + 7.967, + 0.003 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 2.656, + 1, + 5.311, + 1, + 7.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLForm", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 2.656, + 1, + 5.311, + 1, + 7.967, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRForm", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm2", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm2", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm2", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTeethOn", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGlassUD", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassWhite", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlight", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlightMove", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 1, + 1, + 0.589, + 1, + 1.178, + -1, + 1.767, + -1, + 1, + 2.333, + -1, + 2.9, + 1, + 3.467, + 1, + 1, + 4.2, + 1, + 4.933, + -1, + 5.667, + -1, + 1, + 6.433, + -1, + 7.2, + 0.943, + 7.967, + 0.999 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWaistAngleZ", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyPosition", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.6, + 0, + 1.2, + 1, + 1.8, + 1, + 1, + 2.367, + 1, + 2.933, + 0, + 3.5, + 0, + 1, + 4.244, + 0, + 4.989, + 1, + 5.733, + 1, + 1, + 6.478, + 1, + 7.222, + 0.029, + 7.967, + 0.001 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftShoulderUp", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightShoulderUp", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllX", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllY", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllRotate", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFrontFuwa", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSideFuwa", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBackFuwa", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamJacket", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamChainWaist", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA1", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA2", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchAX", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBSwitch", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBOpen", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBX", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL01", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL02", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL03", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR01", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR02", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR03", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR01", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR02", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR03", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand01Roll", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll1", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll2", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll3", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR01", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR02", + "Segments": [ + 0, + -30, + 1, + 2.656, + -30, + 5.311, + -0.25, + 7.967, + -0.002 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR03", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCLHandRoll1", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL01", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL02", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL03", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDLHand03Roll", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER01", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER02", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER03", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER04", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll1", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll2", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06", + "Segments": [ + 0, + 0, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll1", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll2", + "Segments": [ + 0, + 0, + 1, + 2.656, + 0, + 5.311, + 0, + 7.967, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchA", + "Segments": [ + 0, + 1, + 0, + 7.97, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchB", + "Segments": [ + 0, + 0, + 0, + 7.97, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAL", + "Segments": [ + 0, + 1, + 0, + 7.97, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAR", + "Segments": [ + 0, + 1, + 0, + 7.97, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmBR", + "Segments": [ + 0, + 0, + 0, + 7.97, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmCL", + "Segments": [ + 0, + 0, + 0, + 7.97, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmDL", + "Segments": [ + 0, + 0, + 0, + 7.97, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmER", + "Segments": [ + 0, + 0, + 0, + 7.97, + 0 + ] + } + ] +} +``` + +`Content/Samples/Natori/motions/mtn_02.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 5, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 97, + "TotalSegmentCount": 228, + "TotalPointCount": 587, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.511, + 0, + 0.856, + 0, + 1.2, + 0, + 1, + 1.856, + 0, + 2.511, + -0.883, + 3.167, + -4, + 1, + 3.578, + -5.955, + 3.989, + -8.02, + 4.4, + -8.02, + 0, + 5, + -8.02 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.189, + 0, + 0.211, + 0, + 0.233, + 0, + 1, + 0.378, + 0, + 0.522, + 2.709, + 0.667, + 4.062, + 1, + 0.856, + 5.832, + 1.044, + 6, + 1.233, + 6, + 1, + 1.878, + 6, + 2.522, + -12, + 3.167, + -12, + 1, + 3.589, + -12, + 4.011, + -9, + 4.433, + -9, + 0, + 5, + -9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.222, + 0, + 0.278, + 0, + 0.333, + 0, + 1, + 0.611, + 0, + 0.889, + 8, + 1.167, + 8, + 1, + 1.178, + 8, + 1.189, + 8, + 1.2, + 8, + 1, + 1.856, + 8, + 2.511, + -14, + 3.167, + -14, + 1, + 3.467, + -14, + 3.767, + -13, + 4.067, + -13, + 0, + 5, + -13 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.722, + 1, + 1.278, + 1, + 1.833, + 1, + 1, + 1.933, + 1, + 2.033, + 0, + 2.133, + 0, + 1, + 2.478, + 0, + 2.822, + 0, + 3.167, + 0, + 1, + 3.322, + 0, + 3.478, + 0.7, + 3.633, + 0.7, + 0, + 5, + 0.7 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLForm", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.722, + 1, + 1.278, + 1, + 1.833, + 1, + 1, + 1.933, + 1, + 2.033, + 0, + 2.133, + 0, + 1, + 2.478, + 0, + 2.822, + 0, + 3.167, + 0, + 1, + 3.322, + 0, + 3.478, + 0.7, + 3.633, + 0.7, + 0, + 5, + 0.7 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRForm", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.233, + 0, + 0.3, + 0, + 0.367, + 0, + 1, + 0.656, + 0, + 0.944, + 0, + 1.233, + 0, + 1, + 1.878, + 0, + 2.522, + -0.3, + 3.167, + -0.3, + 0, + 5, + -0.3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.233, + 0, + 0.3, + 0, + 0.367, + 0, + 1, + 0.656, + 0, + 0.944, + -0.2, + 1.233, + -0.2, + 1, + 1.878, + -0.2, + 2.522, + -0.2, + 3.167, + -0.2, + 0, + 5, + -0.2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm2", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm2", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm2", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTeethOn", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGlassUD", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassWhite", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlight", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlightMove", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + -1, + 1, + 0.056, + -1, + 0.111, + -1, + 0.167, + -1, + 1, + 0.489, + -1, + 0.811, + 0, + 1.133, + 0, + 1, + 1.778, + 0, + 2.422, + -3, + 3.067, + -3, + 0, + 5, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.256, + 0, + 0.344, + 1.011, + 0.433, + 1.616, + 1, + 0.6, + 2.751, + 0.767, + 3, + 0.933, + 3, + 1, + 1, + 3, + 1.067, + 3, + 1.133, + 3, + 1, + 1.778, + 3, + 2.422, + -2, + 3.067, + -2, + 1, + 3.511, + -2, + 3.956, + -1.5, + 4.4, + -1.5, + 0, + 5, + -1.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWaistAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyPosition", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.467, + 0, + 0.767, + 1, + 1.067, + 1, + 1, + 1.089, + 1, + 1.111, + 1, + 1.133, + 1, + 1, + 1.911, + 1, + 2.689, + 0, + 3.467, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftShoulderUp", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.489, + 0, + 0.811, + 0, + 1.133, + 0, + 1, + 1.778, + 0, + 2.422, + -9, + 3.067, + -9, + 0, + 5, + -9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightShoulderUp", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.489, + 0, + 0.811, + 0, + 1.133, + 0, + 1, + 1.778, + 0, + 2.422, + 8, + 3.067, + 8, + 0, + 5, + 8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllRotate", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFrontFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSideFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBackFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamJacket", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamChainWaist", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchAX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBSwitch", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBOpen", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.544, + 0, + 0.922, + 1, + 1.3, + 1, + 1, + 1.911, + 1, + 2.522, + 0, + 3.133, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.556, + 0, + 0.944, + -1, + 1.333, + -1, + 1, + 1.944, + -1, + 2.556, + 0, + 3.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.556, + 0, + 0.944, + 0, + 1.333, + 0, + 1, + 1.944, + 0, + 2.556, + -3, + 3.167, + -3, + 0, + 5, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR02", + "Segments": [ + 0, + -40, + 1, + 0.056, + -40, + 0.111, + -40, + 0.167, + -40, + 0, + 5, + -40 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR03", + "Segments": [ + 0, + -9, + 1, + 0.056, + -9, + 0.111, + -9, + 0.167, + -9, + 0, + 5, + -9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand01Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll3", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCLHandRoll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDLHand03Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.2, + 0, + 0.233, + 0, + 0.267, + 0, + 1, + 0.6, + 0, + 0.933, + 6, + 1.267, + 6, + 1, + 1.9, + 6, + 2.533, + -2, + 3.167, + -2, + 0, + 5, + -2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.544, + 0, + 0.922, + 3, + 1.3, + 3, + 1, + 1.933, + 3, + 2.567, + -2, + 3.2, + -2, + 1, + 3.589, + -2, + 3.978, + -1, + 4.367, + -1, + 0, + 5, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.211, + 0, + 0.256, + 0.2, + 0.3, + 0.2, + 1, + 0.633, + 0.2, + 0.967, + 0, + 1.3, + 0, + 1, + 1.933, + 0, + 2.567, + 2, + 3.2, + 2, + 1, + 3.589, + 2, + 3.978, + 1, + 4.367, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER04", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.533, + 0, + 0.9, + 0, + 1.267, + 0, + 1, + 1.9, + 0, + 2.533, + 2, + 3.167, + 2, + 0, + 5, + 2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll1", + "Segments": [ + 0, + 9, + 1, + 0.056, + 9, + 0.111, + 9, + 0.167, + 9, + 1, + 0.556, + 9, + 0.944, + 11, + 1.333, + 11, + 1, + 2.067, + 11, + 2.8, + -23, + 3.533, + -23, + 0, + 5, + -23 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll2", + "Segments": [ + 0, + 9, + 1, + 0.056, + 9, + 0.111, + 9, + 0.167, + 9, + 1, + 0.556, + 9, + 0.944, + 14, + 1.333, + 14, + 1, + 2.067, + 14, + 2.8, + -14, + 3.533, + -14, + 0, + 5, + -14 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchA", + "Segments": [ + 0, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchB", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAL", + "Segments": [ + 0, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAR", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmBR", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmCL", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmDL", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmER", + "Segments": [ + 0, + 1, + 0, + 5, + 1 + ] + } + ] +} +``` + +`Content/Samples/Natori/motions/mtn_03.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 5.5, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 96, + "TotalSegmentCount": 262, + "TotalPointCount": 690, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.822, + 0, + 1.478, + 0, + 2.133, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.222, + 0, + 0.278, + 0, + 0.333, + 0, + 1, + 0.544, + 0, + 0.756, + 10.2, + 0.967, + 10.2, + 1, + 1.356, + 10.2, + 1.744, + -30, + 2.133, + -30, + 1, + 2.267, + -30, + 2.4, + -27.952, + 2.533, + -27.952, + 1, + 2.867, + -27.952, + 3.2, + -30, + 3.533, + -30, + 1, + 3.822, + -30, + 4.111, + 2.617, + 4.4, + 2.617, + 1, + 4.533, + 2.617, + 4.667, + 0, + 4.8, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.567, + 1, + 0.967, + 1, + 1.367, + 1, + 1, + 1.478, + 1, + 1.589, + 0, + 1.7, + 0, + 1, + 2.4, + 0, + 3.1, + 0, + 3.8, + 0, + 1, + 3.944, + 0, + 4.089, + 1, + 4.233, + 1, + 0, + 5.5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.567, + 1, + 0.967, + 1, + 1.367, + 1, + 1, + 1.478, + 1, + 1.589, + 0, + 1.7, + 0, + 1, + 2.4, + 0, + 3.1, + 0, + 3.8, + 0, + 1, + 3.944, + 0, + 4.089, + 1, + 4.233, + 1, + 0, + 5.5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTeethOn", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGlassUD", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassWhite", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlight", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlightMove", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.411, + 0, + 0.656, + 1.24, + 0.9, + 1.24, + 1, + 1.3, + 1.24, + 1.7, + -5, + 2.1, + -5.92, + 1, + 2.533, + -6.917, + 2.967, + -6.78, + 3.4, + -6.78, + 1, + 3.856, + -6.78, + 4.311, + 0, + 4.767, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.411, + 0, + 0.656, + 1, + 0.9, + 1, + 1, + 1.311, + 1, + 1.722, + -1.464, + 2.133, + -2, + 1, + 2.567, + -2.564, + 3, + -2.5, + 3.433, + -2.5, + 1, + 3.878, + -2.5, + 4.322, + 0, + 4.767, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWaistAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.422, + 0, + 0.678, + 2, + 0.933, + 2, + 1, + 1.333, + 2, + 1.733, + -1.026, + 2.133, + -2, + 1, + 2.567, + -3.055, + 3, + -3, + 3.433, + -3, + 1, + 3.878, + -3, + 4.322, + 0, + 4.767, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyPosition", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.411, + 0, + 0.656, + -1, + 0.9, + -1, + 1, + 1.289, + -1, + 1.678, + 9.133, + 2.067, + 9.133, + 1, + 2.178, + 9.133, + 2.289, + 9, + 2.4, + 9, + 1, + 2.656, + 9, + 2.911, + 9, + 3.167, + 9, + 1, + 3.478, + 9, + 3.789, + -0.5, + 4.1, + -0.5, + 1, + 4.422, + -0.5, + 4.744, + 0, + 5.067, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.411, + 0, + 0.656, + 1, + 0.9, + 1, + 1, + 1.733, + 1, + 2.567, + 0, + 3.4, + 0, + 1, + 3.633, + 0, + 3.867, + 0.6, + 4.1, + 0.6, + 1, + 4.333, + 0.6, + 4.567, + 0, + 4.8, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftShoulderUp", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.422, + 0, + 0.678, + 0, + 0.933, + 0, + 1, + 1.322, + 0, + 1.711, + -9, + 2.1, + -9, + 1, + 2.989, + -9, + 3.878, + 0, + 4.767, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightShoulderUp", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.422, + 0, + 0.678, + 0, + 0.933, + 0, + 1, + 1.322, + 0, + 1.711, + 8, + 2.1, + 8, + 1, + 2.989, + 8, + 3.878, + 0, + 4.767, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllRotate", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFrontFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSideFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBackFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamJacket", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamChainWaist", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchAX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBSwitch", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBOpen", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL02", + "Segments": [ + 0, + -28, + 1, + 0.056, + -28, + 0.111, + -28, + 0.167, + -28, + 0, + 5.5, + -28 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL03", + "Segments": [ + 0, + 19, + 1, + 0.056, + 19, + 0.111, + 19, + 0.167, + 19, + 0, + 5.5, + 19 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.422, + 0, + 0.678, + 0.8, + 0.933, + 0.8, + 1, + 1.322, + 0.8, + 1.711, + -3, + 2.1, + -3, + 1, + 2.556, + -3, + 3.011, + -3, + 3.467, + -3, + 1, + 3.789, + -3, + 4.111, + 0.8, + 4.433, + 0.8, + 1, + 4.589, + 0.8, + 4.744, + 0, + 4.9, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.433, + 0, + 0.7, + 0.8, + 0.967, + 0.8, + 1, + 1.356, + 0.8, + 1.744, + -6, + 2.133, + -6, + 1, + 2.589, + -6, + 3.044, + -6, + 3.5, + -6, + 1, + 3.822, + -6, + 4.144, + 1, + 4.467, + 1, + 1, + 4.622, + 1, + 4.778, + 0, + 4.933, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.433, + 0, + 0.7, + 0, + 0.967, + 0, + 1, + 1.356, + 0, + 1.744, + -10, + 2.133, + -10, + 1, + 2.589, + -10, + 3.044, + -10, + 3.5, + -10, + 1, + 3.822, + -10, + 4.144, + 1, + 4.467, + 1, + 1, + 4.622, + 1, + 4.778, + 0, + 4.933, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 0, + 0.933, + 0, + 1, + 0.944, + 0, + 0.956, + -1, + 0.967, + -1, + 1, + 2.111, + -1, + 3.256, + -1, + 4.4, + -1, + 1, + 4.411, + -1, + 4.422, + 0, + 4.433, + 0, + 1, + 4.733, + 0, + 5.033, + 0, + 5.333, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand01Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll3", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR01", + "Segments": [ + 0, + 3, + 1, + 0.056, + 3, + 0.111, + 3, + 0.167, + 3, + 1, + 0.422, + 3, + 0.678, + 5, + 0.933, + 5, + 1, + 1.311, + 5, + 1.689, + -6, + 2.067, + -6, + 1, + 2.522, + -6, + 2.978, + -6, + 3.433, + -6, + 1, + 3.922, + -6, + 4.411, + 3, + 4.9, + 3, + 0, + 5.5, + 3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR02", + "Segments": [ + 0, + -29, + 1, + 0.056, + -29, + 0.111, + -29, + 0.167, + -29, + 1, + 0.444, + -29, + 0.722, + -30, + 1, + -30, + 1, + 1.367, + -30, + 1.733, + -21, + 2.1, + -21, + 1, + 2.556, + -21, + 3.011, + -21, + 3.467, + -21, + 1, + 3.956, + -21, + 4.444, + -29, + 4.933, + -29, + 0, + 5.5, + -29 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR03", + "Segments": [ + 0, + 11, + 1, + 0.056, + 11, + 0.111, + 11, + 0.167, + 11, + 1, + 0.444, + 11, + 0.722, + 9, + 1, + 9, + 1, + 1.367, + 9, + 1.733, + 28, + 2.1, + 28, + 1, + 2.556, + 28, + 3.011, + 28, + 3.467, + 28, + 1, + 3.956, + 28, + 4.444, + 11, + 4.933, + 11, + 0, + 5.5, + 11 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCLHandRoll1", + "Segments": [ + 0, + -18, + 1, + 0.056, + -18, + 0.111, + -18, + 0.167, + -18, + 1, + 0.456, + -18, + 0.744, + -18.077, + 1.033, + -21, + 1, + 1.411, + -24.822, + 1.789, + -30, + 2.167, + -30, + 1, + 2.622, + -30, + 3.078, + -30, + 3.533, + -30, + 1, + 4, + -30, + 4.467, + -18, + 4.933, + -18, + 0, + 5.5, + -18 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDLHand03Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER04", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 0, + 5.5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchA", + "Segments": [ + 0, + 1, + 0, + 5.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchB", + "Segments": [ + 0, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAL", + "Segments": [ + 0, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAR", + "Segments": [ + 0, + 1, + 0, + 5.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmBR", + "Segments": [ + 0, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmCL", + "Segments": [ + 0, + 1, + 0, + 5.5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmDL", + "Segments": [ + 0, + 0, + 0, + 5.5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmER", + "Segments": [ + 0, + 0, + 0, + 5.5, + 0 + ] + } + ] +} +``` + +`Content/Samples/Natori/motions/mtn_04.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 5, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 101, + "TotalSegmentCount": 318, + "TotalPointCount": 853, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.322, + 0, + 0.478, + 0, + 0.633, + 0, + 1, + 1.089, + 0, + 1.544, + -10.65, + 2, + -14, + 1, + 2.211, + -15.553, + 2.422, + -15, + 2.633, + -15, + 1, + 2.844, + -15, + 3.056, + 0, + 3.267, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.278, + 0, + 0.389, + 1, + 0.5, + 1, + 1, + 0.744, + 1, + 0.989, + -16.215, + 1.233, + -16.215, + 1, + 1.433, + -16.215, + 1.633, + -11.8, + 1.833, + -11.8, + 1, + 2.133, + -11.8, + 2.433, + -14.898, + 2.733, + -14.898, + 1, + 3.056, + -14.898, + 3.378, + 0, + 3.7, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 0, + 0.567, + 0, + 1, + 0.867, + 0, + 1.167, + 10, + 1.467, + 10, + 1, + 1.8, + 10, + 2.133, + 10, + 2.467, + 10, + 1, + 2.733, + 10, + 3, + 0, + 3.267, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.378, + 1, + 0.589, + 1, + 0.8, + 1, + 1, + 0.833, + 1, + 0.867, + 0, + 0.9, + 0, + 1, + 0.922, + 0, + 0.944, + 0, + 0.967, + 0, + 1, + 1.044, + 0, + 1.122, + 1, + 1.2, + 1, + 1, + 1.711, + 1, + 2.222, + 1, + 2.733, + 1, + 1, + 2.767, + 1, + 2.8, + 0, + 2.833, + 0, + 1, + 2.856, + 0, + 2.878, + 0, + 2.9, + 0, + 1, + 2.978, + 0, + 3.056, + 1, + 3.133, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.378, + 1, + 0.589, + 1, + 0.8, + 1, + 1, + 0.833, + 1, + 0.867, + 0, + 0.9, + 0, + 1, + 0.922, + 0, + 0.944, + 0, + 0.967, + 0, + 1, + 1.044, + 0, + 1.122, + 1, + 1.2, + 1, + 1, + 1.711, + 1, + 2.222, + 1, + 2.733, + 1, + 1, + 2.767, + 1, + 2.8, + 0, + 2.833, + 0, + 1, + 2.856, + 0, + 2.878, + 0, + 2.9, + 0, + 1, + 2.978, + 0, + 3.056, + 1, + 3.133, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.322, + 0, + 0.478, + 0, + 0.633, + 0, + 1, + 0.833, + 0, + 1.033, + -0.5, + 1.233, + -0.5, + 1, + 1.7, + -0.5, + 2.167, + -0.5, + 2.633, + -0.5, + 1, + 2.833, + -0.5, + 3.033, + 0, + 3.233, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.322, + 0, + 0.478, + 0, + 0.633, + 0, + 1, + 0.833, + 0, + 1.033, + -0.2, + 1.233, + -0.2, + 1, + 1.7, + -0.2, + 2.167, + -0.2, + 2.633, + -0.2, + 1, + 2.833, + -0.2, + 3.033, + 0, + 3.233, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + -0.1, + 1, + 0.056, + -0.1, + 0.111, + -0.1, + 0.167, + -0.1, + 0, + 5, + -0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + -0.1, + 1, + 0.056, + -0.1, + 0.111, + -0.1, + 0.167, + -0.1, + 0, + 5, + -0.1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm2", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm2", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTeethOn", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGlassUD", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassWhite", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlight", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlightMove", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.333, + 0, + 0.5, + 0.5, + 0.667, + 0.5, + 1, + 0.922, + 0.5, + 1.178, + -3, + 1.433, + -3, + 1, + 1.789, + -3, + 2.144, + -3, + 2.5, + -3, + 1, + 2.722, + -3, + 2.944, + 0, + 3.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.333, + 0, + 0.5, + -0.538, + 0.667, + -1, + 1, + 0.878, + -1.585, + 1.089, + -1.707, + 1.3, + -1.707, + 1, + 1.711, + -1.707, + 2.122, + 0.274, + 2.533, + 0.274, + 1, + 2.767, + 0.274, + 3, + 0, + 3.233, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWaistAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyPosition", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.356, + 0, + 0.544, + 0, + 0.733, + 0, + 1, + 1.489, + 0, + 2.244, + 0, + 3, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.333, + 0, + 0.5, + 0, + 0.667, + 0, + 1, + 0.944, + 0, + 1.222, + 0.42, + 1.5, + 0.5, + 1, + 1.867, + 0.605, + 2.233, + 0.6, + 2.6, + 0.6, + 1, + 3.111, + 0.6, + 3.622, + 0, + 4.133, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftShoulderUp", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.333, + 0, + 0.5, + 0, + 0.667, + 0, + 1, + 0.922, + 0, + 1.178, + -8.227, + 1.433, + -9, + 1, + 1.789, + -10.076, + 2.144, + -10, + 2.5, + -10, + 1, + 2.744, + -10, + 2.989, + 0, + 3.233, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightShoulderUp", + "Segments": [ + 0, + 8, + 1, + 0.056, + 8, + 0.111, + 8, + 0.167, + 8, + 1, + 0.267, + 8, + 0.367, + 8, + 0.467, + 8, + 1, + 0.533, + 8, + 0.6, + 4, + 0.667, + 4, + 1, + 0.767, + 4, + 0.867, + 9, + 0.967, + 9, + 1, + 1.5, + 9, + 2.033, + 9, + 2.567, + 9, + 1, + 2.722, + 9, + 2.878, + 0, + 3.033, + 0, + 1, + 3.267, + 0, + 3.5, + 8, + 3.733, + 8, + 0, + 5, + 8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllRotate", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFrontFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSideFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBackFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamJacket", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamChainWaist", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchAX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBSwitch", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.3, + 0, + 0.433, + 0, + 0.567, + 0, + 1, + 0.611, + 0, + 0.656, + -1, + 0.7, + -1, + 1, + 0.733, + -1, + 0.767, + 0, + 0.8, + 0, + 1, + 1.667, + 0, + 2.533, + 0, + 3.4, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBOpen", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 1.011, + 0, + 1.856, + 0, + 2.7, + 0, + 1, + 2.756, + 0, + 2.811, + 16, + 2.867, + 16, + 1, + 2.889, + 16, + 2.911, + 16, + 2.933, + 16, + 1, + 3.089, + 16, + 3.244, + 0, + 3.4, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBOpen2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.344, + 0, + 0.522, + 0, + 0.7, + 0, + 1, + 0.744, + 0, + 0.789, + 30, + 0.833, + 30, + 1, + 1.456, + 30, + 2.078, + 30, + 2.7, + 30, + 1, + 2.756, + 30, + 2.811, + 16, + 2.867, + 16, + 1, + 2.889, + 16, + 2.911, + 16, + 2.933, + 16, + 1, + 3.089, + 16, + 3.244, + 0, + 3.4, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.3, + 0, + 0.433, + 0, + 0.567, + 0, + 1, + 0.7, + 0, + 0.833, + -6.9, + 0.967, + -6.9, + 1, + 1.544, + -6.9, + 2.122, + -6.9, + 2.7, + -6.9, + 1, + 2.756, + -6.9, + 2.811, + 0, + 2.867, + 0, + 1, + 2.889, + 0, + 2.911, + 0, + 2.933, + 0, + 1, + 3.089, + 0, + 3.244, + 0, + 3.4, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBRoll", + "Segments": [ + 0, + -12, + 1, + 0.056, + -12, + 0.111, + -12, + 0.167, + -12, + 1, + 0.3, + -12, + 0.433, + -12, + 0.567, + -12, + 1, + 0.611, + -12, + 0.656, + -9, + 0.7, + -9, + 1, + 1.367, + -9, + 2.033, + -9, + 2.7, + -9, + 1, + 2.756, + -9, + 2.811, + -12, + 2.867, + -12, + 1, + 2.889, + -12, + 2.911, + -12, + 2.933, + -12, + 1, + 3.089, + -12, + 3.244, + -12, + 3.4, + -12, + 0, + 5, + -12 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBLR", + "Segments": [ + 0, + 2, + 1, + 0.056, + 2, + 0.111, + 2, + 0.167, + 2, + 1, + 0.3, + 2, + 0.433, + 2, + 0.567, + 2, + 1, + 0.7, + 2, + 0.833, + -3.4, + 0.967, + -3.4, + 1, + 1.544, + -3.4, + 2.122, + -3.331, + 2.7, + -3.144, + 1, + 2.756, + -3.126, + 2.811, + 0, + 2.867, + 0, + 1, + 2.889, + 0, + 2.911, + 0, + 2.933, + 0, + 1, + 3.089, + 0, + 3.244, + 2, + 3.4, + 2, + 0, + 5, + 2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBUD", + "Segments": [ + 0, + -2, + 1, + 0.056, + -2, + 0.111, + -2, + 0.167, + -2, + 1, + 1.011, + -2, + 1.856, + 1.06, + 2.7, + 1.06, + 1, + 2.756, + 1.06, + 2.811, + 0.241, + 2.867, + 0, + 1, + 2.889, + -0.096, + 2.911, + 0.01, + 2.933, + -0.11, + 1, + 3.089, + -0.955, + 3.244, + -2, + 3.4, + -2, + 0, + 5, + -2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR01", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR02", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR03", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR01", + "Segments": [ + 0, + -7, + 1, + 0.056, + -7, + 0.111, + -7, + 0.167, + -7, + 1, + 0.378, + -7, + 0.589, + -7, + 0.8, + -7, + 1, + 0.989, + -7, + 1.178, + -5, + 1.367, + -5, + 1, + 1.744, + -5, + 2.122, + -5, + 2.5, + -5, + 1, + 2.589, + -5, + 2.678, + -6, + 2.767, + -6, + 1, + 2.789, + -6, + 2.811, + -6, + 2.833, + -6, + 1, + 3.022, + -6, + 3.211, + -7, + 3.4, + -7, + 0, + 5, + -7 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR02", + "Segments": [ + 0, + 18, + 1, + 0.056, + 18, + 0.111, + 18, + 0.167, + 18, + 1, + 0.444, + 18, + 0.722, + 18, + 1, + 18, + 1, + 1.356, + 18, + 1.711, + 16.881, + 2.067, + 15, + 1, + 2.211, + 14.236, + 2.356, + 14, + 2.5, + 14, + 1, + 2.589, + 14, + 2.678, + 16.023, + 2.767, + 18, + 1, + 2.811, + 18.988, + 2.856, + 19, + 2.9, + 19, + 1, + 3.067, + 19, + 3.233, + 18, + 3.4, + 18, + 0, + 5, + 18 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.3, + 0, + 0.433, + -6, + 0.567, + -6, + 1, + 0.611, + -6, + 0.656, + 9, + 0.7, + 9, + 1, + 0.8, + 9, + 0.9, + 0.266, + 1, + 0, + 1, + 1.322, + -0.858, + 1.644, + -1, + 1.967, + -1, + 1, + 2.211, + -1, + 2.456, + -1, + 2.7, + -1, + 1, + 2.744, + -1, + 2.789, + 14, + 2.833, + 14, + 1, + 2.867, + 14, + 2.9, + 14, + 2.933, + 14, + 1, + 3.078, + 14, + 3.222, + 0, + 3.367, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand01", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand01Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05", + "FadeInTime": 0.2, + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 1.011, + 0, + 1.856, + 0, + 2.7, + 0, + 1, + 2.756, + 0, + 2.811, + 30, + 2.867, + 30, + 1, + 2.889, + 30, + 2.911, + 30, + 2.933, + 30, + 1, + 3.089, + 30, + 3.244, + 0, + 3.4, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.3, + 0, + 0.433, + 0, + 0.567, + 0, + 1, + 0.611, + 0, + 0.656, + 7.5, + 0.7, + 7.5, + 1, + 0.811, + 7.5, + 0.922, + 0, + 1.033, + 0, + 1, + 1.589, + 0, + 2.144, + 0, + 2.7, + 0, + 1, + 2.756, + 0, + 2.811, + 21, + 2.867, + 21, + 1, + 2.889, + 21, + 2.911, + 21, + 2.933, + 21, + 1, + 3.089, + 21, + 3.244, + 0, + 3.4, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll3", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 1.011, + 0, + 1.856, + 0, + 2.7, + 0, + 1, + 2.756, + 0, + 2.811, + 30, + 2.867, + 30, + 1, + 2.889, + 30, + 2.911, + 30, + 2.933, + 30, + 1, + 3.089, + 30, + 3.244, + 0, + 3.4, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCLHandRoll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDLHand03Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER03", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER04", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchA", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchB", + "Segments": [ + 0, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAL", + "Segments": [ + 0, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAR", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmBR", + "Segments": [ + 0, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmCL", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmDL", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmER", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + } + ] +} +``` + +`Content/Samples/Natori/motions/mtn_05.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 3.33, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 101, + "TotalSegmentCount": 278, + "TotalPointCount": 733, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.189, + 0, + 0.211, + 0, + 0.233, + 0, + 1, + 0.3, + 0, + 0.367, + 0, + 0.433, + 0, + 1, + 0.667, + 0, + 0.9, + 4.019, + 1.133, + 4.019, + 1, + 1.444, + 4.019, + 1.756, + 0, + 2.067, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + -9.011, + 1, + 0.056, + -9.011, + 0.111, + -9.011, + 0.167, + -9.011, + 1, + 0.222, + -9.011, + 0.278, + -9.011, + 0.333, + -9.011, + 1, + 0.544, + -9.011, + 0.756, + -30, + 0.967, + -30, + 1, + 1.089, + -30, + 1.211, + -30, + 1.333, + -30, + 1, + 1.567, + -30, + 1.8, + 0, + 2.033, + 0, + 1, + 2.067, + 0, + 2.1, + 0, + 2.133, + 0, + 1, + 2.356, + 0, + 2.578, + -5, + 2.8, + -5, + 0, + 3.333, + -5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.233, + 0, + 0.3, + 0, + 0.367, + 0, + 1, + 0.622, + 0, + 0.878, + -4, + 1.133, + -4, + 1, + 1.533, + -4, + 1.933, + 1, + 2.333, + 1, + 0, + 3.333, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.311, + 1, + 0.456, + 1, + 0.6, + 1, + 1, + 0.689, + 1, + 0.778, + 0, + 0.867, + 0, + 1, + 1.133, + 0, + 1.4, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 1, + 1.933, + 1, + 0, + 3.333, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.311, + 1, + 0.456, + 1, + 0.6, + 1, + 1, + 0.689, + 1, + 0.778, + 0, + 0.867, + 0, + 1, + 1.133, + 0, + 1.4, + 0, + 1.667, + 0, + 1, + 1.756, + 0, + 1.844, + 1, + 1.933, + 1, + 0, + 3.333, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.311, + 0, + 0.456, + 0, + 0.6, + 0, + 1, + 0.689, + 0, + 0.778, + 0.2, + 0.867, + 0.2, + 1, + 1.133, + 0.2, + 1.4, + 0.2, + 1.667, + 0.2, + 1, + 1.756, + 0.2, + 1.844, + 0, + 1.933, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.311, + 0, + 0.456, + 0, + 0.6, + 0, + 1, + 0.689, + 0, + 0.778, + -0.8, + 0.867, + -0.8, + 1, + 1.133, + -0.8, + 1.4, + -0.8, + 1.667, + -0.8, + 1, + 1.756, + -0.8, + 1.844, + 0, + 1.933, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTeethOn", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGlassUD", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.222, + 0, + 0.278, + 0, + 0.333, + 0, + 1, + 0.544, + 0, + 0.756, + -10, + 0.967, + -10, + 1, + 1.044, + -10, + 1.122, + 10, + 1.2, + 10, + 1, + 1.222, + 10, + 1.244, + 10, + 1.267, + 10, + 1, + 1.333, + 10, + 1.4, + 0, + 1.467, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassWhite", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.433, + 0, + 0.7, + 0, + 0.967, + 0, + 1, + 1.067, + 0, + 1.167, + 0.4, + 1.267, + 0.4, + 1, + 1.367, + 0.4, + 1.467, + 0, + 1.567, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlight", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.422, + 0, + 0.678, + 0, + 0.933, + 0, + 1, + 1.011, + 0, + 1.089, + 1, + 1.167, + 1, + 1, + 1.2, + 1, + 1.233, + 1, + 1.267, + 1, + 1, + 1.4, + 1, + 1.533, + 0, + 1.667, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlightMove", + "Segments": [ + 0, + -30, + 1, + 0.056, + -30, + 0.111, + -30, + 0.167, + -30, + 1, + 0.444, + -30, + 0.667, + -30, + 1, + -30, + 1, + 1.189, + -30, + 1.378, + 30, + 1.567, + 30, + 0, + 3.333, + 30 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.189, + 0, + 0.211, + 0, + 0.233, + 0, + 1, + 0.289, + 0, + 0.344, + 0, + 0.4, + 0, + 1, + 0.611, + 0, + 0.822, + 2, + 1.033, + 2, + 1, + 1.078, + 2, + 1.122, + 2, + 1.167, + 2, + 1, + 1.422, + 2, + 1.678, + 0, + 1.933, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.189, + 0, + 0.211, + 0, + 0.233, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWaistAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyPosition", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.256, + 0, + 0.344, + 0, + 0.433, + 0, + 1, + 0.633, + 0, + 0.833, + 2, + 1.033, + 2, + 1, + 1.244, + 2, + 1.456, + 0, + 1.667, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.233, + 0, + 0.3, + 0, + 0.367, + 0, + 1, + 0.722, + 0, + 1.078, + 1, + 1.433, + 1, + 1, + 1.667, + 1, + 1.9, + 1, + 2.133, + 1, + 0, + 3.333, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftShoulderUp", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightShoulderUp", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.189, + 0, + 0.211, + 0, + 0.233, + 0, + 1, + 0.478, + 0, + 0.722, + 2.225, + 0.967, + 9, + 1, + 1.056, + 11.464, + 1.144, + 15, + 1.233, + 15, + 1, + 1.589, + 15, + 1.944, + -6, + 2.3, + -6, + 1, + 2.489, + -6, + 2.678, + -3, + 2.867, + -3, + 0, + 3.333, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllRotate", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFrontFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSideFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBackFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamJacket", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamChainWaist", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchAX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBSwitch", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBOpen", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBOpen2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBRoll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBLR", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBUD", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand01Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll3", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCLHandRoll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDLHand03Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER01", + "Segments": [ + 0, + 16, + 1, + 0.056, + 16, + 0.111, + 16, + 0.167, + 16, + 1, + 0.311, + 16, + 0.456, + 16, + 0.6, + 16, + 1, + 1.033, + 16, + 1.467, + 16, + 1.9, + 16, + 1, + 2.044, + 16, + 2.189, + 18, + 2.333, + 18, + 0, + 3.333, + 18 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER02", + "Segments": [ + 0, + 11, + 1, + 0.056, + 11, + 0.111, + 11, + 0.167, + 11, + 1, + 0.311, + 11, + 0.456, + 14.565, + 0.6, + 18, + 1, + 0.711, + 20.643, + 0.822, + 21, + 0.933, + 21, + 1, + 1.267, + 21, + 1.6, + 13.953, + 1.933, + 0, + 1, + 2.078, + -6.046, + 2.222, + -9, + 2.367, + -9, + 1, + 2.544, + -9, + 2.722, + -8, + 2.9, + -8, + 0, + 3.333, + -8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER03", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.189, + 1, + 0.211, + 1, + 0.233, + 1, + 1, + 0.356, + 1, + 0.478, + 1, + 0.6, + 1, + 1, + 0.711, + 1, + 0.822, + -5.698, + 0.933, + -7, + 1, + 1.033, + -8.171, + 1.133, + -8.083, + 1.233, + -9, + 1, + 1.389, + -10.427, + 1.544, + -11.838, + 1.7, + -11.838, + 1, + 1.933, + -11.838, + 2.167, + 19, + 2.4, + 19, + 1, + 2.578, + 19, + 2.756, + 15.092, + 2.933, + 15.092, + 0, + 3.333, + 15.092 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER04", + "Segments": [ + 0, + 30, + 1, + 0.056, + 30, + 0.111, + 30, + 0.167, + 30, + 1, + 0.422, + 30, + 0.678, + 30, + 0.933, + 30, + 1, + 1.089, + 30, + 1.244, + 30, + 1.4, + 30, + 1, + 1.744, + 30, + 2.089, + 9, + 2.433, + 9, + 0, + 3.333, + 9 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06", + "FadeInTime": 0.2, + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.311, + 1, + 0.456, + 1, + 0.6, + 1, + 0, + 3.333, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.311, + 0, + 0.456, + 0, + 0.6, + 0, + 1, + 0.711, + 0, + 0.822, + 12.92, + 0.933, + 14, + 1, + 1.278, + 17.347, + 1.622, + 18, + 1.967, + 18, + 1, + 2.144, + 18, + 2.322, + 3, + 2.5, + 3, + 0, + 3.333, + 3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.411, + 0, + 0.656, + -30, + 0.9, + -30, + 1, + 1.167, + -30, + 1.433, + -30, + 1.7, + -30, + 1, + 1.956, + -30, + 2.211, + -24, + 2.467, + -24, + 0, + 3.333, + -24 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchA", + "Segments": [ + 0, + 1, + 0, + 3.33, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchB", + "Segments": [ + 0, + 0, + 0, + 3.33, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAL", + "Segments": [ + 0, + 1, + 0, + 3.33, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAR", + "Segments": [ + 0, + 0, + 0, + 3.33, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmBR", + "Segments": [ + 0, + 0, + 0, + 3.33, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmCL", + "Segments": [ + 0, + 0, + 0, + 3.33, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmDL", + "Segments": [ + 0, + 0, + 0, + 3.33, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmER", + "Segments": [ + 0, + 1, + 0, + 3.33, + 1 + ] + } + ] +} +``` + +`Content/Samples/Natori/motions/mtn_06.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 5, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 99, + "TotalSegmentCount": 283, + "TotalPointCount": 752, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.4, + 0, + 0.633, + 0, + 0.867, + 0, + 1, + 1.167, + 0, + 1.467, + 13.505, + 1.767, + 16, + 1, + 2.011, + 18.033, + 2.256, + 17.623, + 2.5, + 17.623, + 1, + 2.856, + 17.623, + 3.211, + -4.813, + 3.567, + -6, + 1, + 3.8, + -6.779, + 4.033, + -6.496, + 4.267, + -6.496, + 0, + 5, + -6.496 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0.017, + 1, + 0.056, + 0.017, + 0.111, + 0.017, + 0.167, + 0.017, + 1, + 0.456, + 0.017, + 0.744, + 0.229, + 1.033, + 1, + 1, + 1.211, + 1.475, + 1.389, + 2, + 1.567, + 2, + 1, + 1.744, + 2, + 1.922, + 0, + 2.1, + 0, + 1, + 2.4, + 0, + 2.7, + 3, + 3, + 3, + 1, + 3.211, + 3, + 3.422, + -5.586, + 3.633, + -5.586, + 1, + 3.644, + -5.586, + 3.656, + -5.586, + 3.667, + -5.586, + 1, + 3.822, + -5.586, + 3.978, + 1, + 4.133, + 1, + 1, + 4.389, + 1, + 4.644, + 0, + 4.9, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.433, + 0, + 0.7, + -3, + 0.967, + -3, + 1, + 1.256, + -3, + 1.544, + 4.968, + 1.833, + 6, + 1, + 2.156, + 7.151, + 2.478, + 7, + 2.8, + 7, + 1, + 3, + 7, + 3.2, + -2, + 3.4, + -2, + 1, + 3.578, + -2, + 3.756, + 0, + 3.933, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.589, + 1, + 1.011, + 1, + 1.433, + 1, + 1, + 1.467, + 1, + 1.5, + 0, + 1.533, + 0, + 1, + 1.556, + 0, + 1.578, + 0, + 1.6, + 0, + 1, + 1.678, + 0, + 1.756, + 1, + 1.833, + 1, + 1, + 2.344, + 1, + 2.856, + 1, + 3.367, + 1, + 1, + 3.4, + 1, + 3.433, + 0, + 3.467, + 0, + 1, + 3.489, + 0, + 3.511, + 0, + 3.533, + 0, + 1, + 3.611, + 0, + 3.689, + 1, + 3.767, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.589, + 1, + 1.011, + 1, + 1.433, + 1, + 1, + 1.467, + 1, + 1.5, + 0, + 1.533, + 0, + 1, + 1.556, + 0, + 1.578, + 0, + 1.6, + 0, + 1, + 1.678, + 0, + 1.756, + 1, + 1.833, + 1, + 1, + 2.344, + 1, + 2.856, + 1, + 3.367, + 1, + 1, + 3.4, + 1, + 3.433, + 0, + 3.467, + 0, + 1, + 3.489, + 0, + 3.511, + 0, + 3.533, + 0, + 1, + 3.611, + 0, + 3.689, + 1, + 3.767, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.578, + 0, + 0.989, + 0, + 1.4, + 0, + 1, + 1.544, + 0, + 1.689, + 0.542, + 1.833, + 0.542, + 1, + 2.056, + 0.542, + 2.278, + 0.542, + 2.5, + 0.542, + 1, + 2.789, + 0.542, + 3.078, + 0, + 3.367, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.578, + 0, + 0.989, + 0, + 1.4, + 0, + 1, + 1.544, + 0, + 1.689, + -0.386, + 1.833, + -0.386, + 1, + 2.056, + -0.386, + 2.278, + -0.386, + 2.5, + -0.386, + 1, + 2.789, + -0.386, + 3.078, + 0, + 3.367, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTeethOn", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGlassUD", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassWhite", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlight", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlightMove", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.356, + 0, + 0.544, + -1, + 0.733, + -1, + 1, + 1.044, + -1, + 1.356, + 4.155, + 1.667, + 5, + 1, + 1.911, + 5.664, + 2.156, + 5.5, + 2.4, + 5.5, + 1, + 2.656, + 5.5, + 2.911, + -1.168, + 3.167, + -2, + 1, + 3.5, + -3.085, + 3.833, + -3, + 4.167, + -3, + 0, + 5, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.356, + 0, + 0.544, + 1.915, + 0.733, + 2, + 1, + 0.878, + 2.065, + 1.022, + 2.028, + 1.167, + 2.088, + 1, + 1.344, + 2.162, + 1.522, + 2.904, + 1.7, + 2.904, + 1, + 1.867, + 2.904, + 2.033, + 2, + 2.2, + 2, + 1, + 2.356, + 2, + 2.511, + 2.5, + 2.667, + 2.5, + 1, + 2.978, + 2.5, + 3.289, + -1.006, + 3.6, + -1.006, + 1, + 3.8, + -1.006, + 4, + 0.139, + 4.2, + 0.139, + 1, + 4.378, + 0.139, + 4.556, + 0, + 4.733, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.433, + 0, + 0.7, + 0, + 0.967, + 0, + 1, + 1.222, + 0, + 1.478, + 1, + 1.733, + 1, + 1, + 2.456, + 1, + 3.178, + 1, + 3.9, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWaistAngleZ", + "Segments": [ + 0, + 0.124, + 1, + 0.056, + 0.124, + 0.111, + 0.124, + 0.167, + 0.124, + 1, + 0.478, + 0.124, + 0.789, + 2, + 1.1, + 2, + 1, + 1.3, + 2, + 1.5, + -1, + 1.7, + -1, + 1, + 2.2, + -1, + 2.7, + -1, + 3.2, + -1, + 1, + 3.456, + -1, + 3.711, + 0, + 3.967, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyPosition", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 1.656, + 0, + 3.144, + 0, + 4.633, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.411, + 0, + 0.656, + 0, + 0.9, + 0, + 1, + 1.411, + 0, + 1.922, + 0.501, + 2.433, + 0.501, + 1, + 2.511, + 0.501, + 2.589, + 0.501, + 2.667, + 0.501, + 1, + 3.178, + 0.501, + 3.689, + 0, + 4.2, + 0, + 1, + 4.322, + 0, + 4.444, + 0, + 4.567, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftShoulderUp", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.356, + 0, + 0.544, + -6, + 0.733, + -6, + 1, + 0.933, + -6, + 1.133, + 6, + 1.333, + 6, + 1, + 1.522, + 6, + 1.711, + 2, + 1.9, + 2, + 0, + 5, + 2 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightShoulderUp", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.289, + 0, + 0.411, + 0, + 0.533, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllRotate", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFrontFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSideFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBackFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamJacket", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamChainWaist", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchAX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBSwitch", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBOpen", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBOpen2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBRoll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBLR", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBUD", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL01", + "Segments": [ + 0, + 0.232, + 1, + 0.056, + 0.232, + 0.111, + 0.232, + 0.167, + 0.232, + 0, + 5, + 0.232 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL02", + "Segments": [ + 0, + -0.373, + 1, + 0.056, + -0.373, + 0.111, + -0.373, + 0.167, + -0.373, + 0, + 5, + -0.373 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL03", + "Segments": [ + 0, + -0.305, + 1, + 0.056, + -0.305, + 0.111, + -0.305, + 0.167, + -0.305, + 0, + 5, + -0.305 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.333, + 0, + 0.5, + 0, + 0.667, + 0, + 1, + 1, + 0, + 1.333, + 1, + 1.667, + 1, + 1, + 1.889, + 1, + 2.111, + 1, + 2.333, + 1, + 1, + 2.689, + 1, + 3.044, + 0, + 3.4, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.333, + 0, + 0.5, + 0, + 0.667, + 0, + 1, + 1.011, + 0, + 1.356, + -3, + 1.7, + -3, + 1, + 1.922, + -3, + 2.144, + -3, + 2.367, + -3, + 1, + 2.722, + -3, + 3.078, + 0, + 3.433, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.333, + 0, + 0.5, + 0, + 0.667, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand01Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll3", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCLHandRoll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL01", + "Segments": [ + 0, + -0.938, + 1, + 0.056, + -0.938, + 0.111, + -0.938, + 0.167, + -0.938, + 1, + 0.322, + -0.938, + 0.478, + -0.938, + 0.633, + -0.938, + 1, + 0.956, + -0.938, + 1.278, + -4, + 1.6, + -4, + 1, + 1.944, + -4, + 2.289, + 4, + 2.633, + 4, + 1, + 3.033, + 4, + 3.433, + 0, + 3.833, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL02", + "Segments": [ + 0, + -1.719, + 1, + 0.056, + -1.719, + 0.111, + -1.719, + 0.167, + -1.719, + 1, + 0.322, + -1.719, + 0.478, + -1.719, + 0.633, + -1.719, + 1, + 0.967, + -1.719, + 1.3, + 4.142, + 1.633, + 7, + 1, + 1.978, + 9.953, + 2.322, + 10, + 2.667, + 10, + 1, + 3.111, + 10, + 3.556, + 6, + 4, + 6, + 0, + 5, + 6 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL03", + "Segments": [ + 0, + -4.688, + 1, + 0.056, + -4.688, + 0.111, + -4.688, + 0.167, + -4.688, + 1, + 0.322, + -4.688, + 0.478, + -4.688, + 0.633, + -4.688, + 1, + 0.978, + -4.688, + 1.322, + 14.713, + 1.667, + 18, + 1, + 2.022, + 21.393, + 2.378, + 21, + 2.733, + 21, + 1, + 2.967, + 21, + 3.2, + 13, + 3.433, + 13, + 0, + 5, + 13 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDLHand03Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 1.044, + 0, + 1.922, + -30, + 2.8, + -30, + 1, + 3.2, + -30, + 3.6, + -5.084, + 4, + -2, + 1, + 4.333, + 0.57, + 4.667, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER04", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchA", + "Segments": [ + 0, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchB", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAL", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAR", + "Segments": [ + 0, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmBR", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmCL", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmDL", + "Segments": [ + 0, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmER", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + } + ] +} +``` + +`Content/Samples/Natori/motions/mtn_07.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 3.33, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 99, + "TotalSegmentCount": 246, + "TotalPointCount": 639, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.256, + 0, + 0.344, + 0, + 0.433, + 0, + 1, + 0.622, + 0, + 0.811, + 10, + 1, + 10, + 0, + 3.333, + 10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.178, + 0, + 0.189, + 0, + 0.2, + 0, + 1, + 0.267, + 0, + 0.333, + 1.753, + 0.4, + 3.095, + 1, + 0.444, + 3.989, + 0.489, + 3.997, + 0.533, + 3.997, + 1, + 0.744, + 3.997, + 0.956, + -27.096, + 1.167, + -27.096, + 1, + 1.256, + -27.096, + 1.344, + -25.324, + 1.433, + -25.065, + 1, + 1.811, + -23.961, + 2.189, + -23.671, + 2.567, + -23.671, + 0, + 3.333, + -23.671 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.211, + 0, + 0.256, + 0, + 0.3, + 0, + 1, + 0.378, + 0, + 0.456, + 0.319, + 0.533, + -1.334, + 1, + 0.7, + -4.877, + 0.867, + -11, + 1.033, + -11, + 1, + 1.289, + -11, + 1.544, + -10, + 1.8, + -10, + 0, + 3.333, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.344, + 1, + 0.522, + 1, + 0.7, + 1, + 1, + 0.744, + 1, + 0.789, + 0, + 0.833, + 0, + 1, + 0.989, + 0, + 1.144, + 0, + 1.3, + 0, + 1, + 1.378, + 0, + 1.456, + 1, + 1.533, + 1, + 0, + 3.333, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.344, + 1, + 0.522, + 1, + 0.7, + 1, + 1, + 0.744, + 1, + 0.789, + 0, + 0.833, + 0, + 1, + 0.989, + 0, + 1.144, + 0, + 1.3, + 0, + 1, + 1.378, + 0, + 1.456, + 1, + 1.533, + 1, + 0, + 3.333, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.389, + 0, + 0.611, + 0.1, + 0.833, + 0.1, + 1, + 1.067, + 0.1, + 1.3, + -0.4, + 1.533, + -0.4, + 0, + 3.333, + -0.4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.389, + 0, + 0.611, + -0.2, + 0.833, + -0.2, + 1, + 1.067, + -0.2, + 1.3, + 0.7, + 1.533, + 0.7, + 0, + 3.333, + 0.7 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTeethOn", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGlassUD", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassWhite", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlight", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlightMove", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.244, + 0, + 0.322, + -0.5, + 0.4, + -0.5, + 1, + 0.622, + -0.5, + 0.844, + 6, + 1.067, + 6, + 1, + 1.3, + 6, + 1.533, + 5, + 1.767, + 5, + 0, + 3.333, + 5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.244, + 0, + 0.322, + 0.625, + 0.4, + 0.625, + 1, + 0.633, + 0.625, + 0.867, + -4.741, + 1.1, + -4.741, + 1, + 1.333, + -4.741, + 1.567, + -4, + 1.8, + -4, + 0, + 3.333, + -4 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.256, + 0, + 0.344, + 0, + 0.433, + 0, + 1, + 0.611, + 0, + 0.789, + -3, + 0.967, + -3, + 0, + 3.333, + -3 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWaistAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.256, + 0, + 0.344, + 0, + 0.433, + 0, + 1, + 0.611, + 0, + 0.789, + -5, + 0.967, + -5, + 0, + 3.333, + -5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyPosition", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.244, + 0, + 0.322, + -3, + 0.4, + -3, + 1, + 0.544, + -3, + 0.689, + 0.972, + 0.833, + 3.4, + 1, + 1.144, + 8.63, + 1.456, + 10, + 1.767, + 10, + 0, + 3.333, + 10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.178, + 0, + 0.189, + 0, + 0.2, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftShoulderUp", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.244, + 0, + 0.322, + 1.58, + 0.4, + 4, + 1, + 0.589, + 9.878, + 0.778, + 16.122, + 0.967, + 21, + 1, + 1.222, + 27.599, + 1.478, + 30, + 1.733, + 30, + 0, + 3.333, + 30 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightShoulderUp", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.244, + 0, + 0.322, + 2, + 0.4, + 2, + 1, + 0.589, + 2, + 0.778, + -19, + 0.967, + -19, + 1, + 1.222, + -19, + 1.478, + -13, + 1.733, + -13, + 0, + 3.333, + -13 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllRotate", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFrontFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSideFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBackFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamJacket", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamChainWaist", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchAX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBSwitch", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBOpen", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBOpen2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBRoll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBLR", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBUD", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL01", + "Segments": [ + 0, + 0.7, + 1, + 0.056, + 0.7, + 0.111, + 0.7, + 0.167, + 0.7, + 1, + 0.422, + 0.7, + 0.678, + 0.191, + 0.933, + -0.5, + 1, + 1.211, + -1.251, + 1.489, + -1.5, + 1.767, + -1.5, + 0, + 3.333, + -1.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL02", + "Segments": [ + 0, + -8, + 1, + 0.056, + -8, + 0.111, + -8, + 0.167, + -8, + 1, + 0.433, + -8, + 0.7, + -8, + 0.967, + -8, + 1, + 1.233, + -8, + 1.5, + -8, + 1.767, + -8, + 0, + 3.333, + -8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL03", + "Segments": [ + 0, + -3, + 1, + 0.056, + -3, + 0.111, + -3, + 0.167, + -3, + 1, + 0.433, + -3, + 0.7, + -3, + 0.967, + -3, + 1, + 1.233, + -3, + 1.5, + 0, + 1.767, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL04", + "FadeInTime": 0.2, + "Segments": [ + 0, + -1, + 1, + 0.056, + -1, + 0.111, + -1, + 0.167, + -1, + 0, + 3.333, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR01", + "Segments": [ + 0, + 0.7, + 1, + 0.056, + 0.7, + 0.111, + 0.7, + 0.167, + 0.7, + 1, + 0.422, + 0.7, + 0.678, + 0.191, + 0.933, + -0.5, + 1, + 1.211, + -1.251, + 1.489, + -1.5, + 1.767, + -1.5, + 0, + 3.333, + -1.5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR02", + "Segments": [ + 0, + -8, + 1, + 0.056, + -8, + 0.111, + -8, + 0.167, + -8, + 1, + 0.433, + -8, + 0.7, + -8, + 0.967, + -8, + 1, + 1.233, + -8, + 1.5, + -7, + 1.767, + -7, + 0, + 3.333, + -7 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR03", + "Segments": [ + 0, + -3, + 1, + 0.056, + -3, + 0.111, + -3, + 0.167, + -3, + 1, + 0.433, + -3, + 0.7, + -3, + 0.967, + -3, + 1, + 1.233, + -3, + 1.5, + -10, + 1.767, + -10, + 0, + 3.333, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR04", + "FadeInTime": 0.2, + "Segments": [ + 0, + -1, + 1, + 0.056, + -1, + 0.111, + -1, + 0.167, + -1, + 0, + 3.333, + -1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand01Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll3", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCLHandRoll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDLHand03Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER04", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 3.333, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchA", + "Segments": [ + 0, + 1, + 0, + 3.33, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchB", + "Segments": [ + 0, + 0, + 0, + 3.33, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAL", + "Segments": [ + 0, + 1, + 0, + 3.33, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAR", + "Segments": [ + 0, + 1, + 0, + 3.33, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmBR", + "Segments": [ + 0, + 0, + 0, + 3.33, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmCL", + "Segments": [ + 0, + 0, + 0, + 3.33, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmDL", + "Segments": [ + 0, + 0, + 0, + 3.33, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmER", + "Segments": [ + 0, + 0, + 0, + 3.33, + 0 + ] + } + ] +} +``` + +`Content/Samples/Natori/motions/mtn_08.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 99, + "TotalSegmentCount": 221, + "TotalPointCount": 564, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.333, + 0, + 0.5, + 0, + 0.667, + 0, + 1, + 0.767, + 0, + 0.867, + -13, + 0.967, + -13, + 1, + 1.067, + -13, + 1.167, + 11, + 1.267, + 11, + 1, + 1.4, + 11, + 1.533, + -8, + 1.667, + -8, + 1, + 1.833, + -8, + 2, + 0, + 2.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.2, + 0, + 0.233, + 0, + 0.267, + 0, + 1, + 0.333, + 0, + 0.4, + 9, + 0.467, + 9, + 1, + 0.7, + 9, + 0.933, + -6, + 1.167, + -6, + 1, + 1.6, + -6, + 2.033, + -1, + 2.467, + -1, + 1, + 2.622, + -1, + 2.778, + -4, + 2.933, + -4, + 1, + 3.078, + -4, + 3.222, + 0, + 3.367, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.3, + 0, + 0.433, + 4, + 0.567, + 4, + 1, + 0.978, + 4, + 1.389, + 0, + 1.8, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.289, + 1, + 0.411, + 1, + 0.533, + 1, + 1, + 0.622, + 1, + 0.711, + 0, + 0.8, + 0, + 1, + 1.522, + 0, + 2.244, + 0, + 2.967, + 0, + 1, + 3.078, + 0, + 3.189, + 1, + 3.3, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 0.056, + 1, + 0.111, + 1, + 0.167, + 1, + 1, + 0.289, + 1, + 0.411, + 1, + 0.533, + 1, + 1, + 0.622, + 1, + 0.711, + 0, + 0.8, + 0, + 1, + 1.522, + 0, + 2.244, + 0, + 2.967, + 0, + 1, + 3.078, + 0, + 3.189, + 1, + 3.3, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRSmile", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeRForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.289, + 0, + 0.411, + -0.4, + 0.533, + -0.4, + 1, + 1.344, + -0.4, + 2.156, + -0.4, + 2.967, + -0.4, + 1, + 3.078, + -0.4, + 3.189, + 0, + 3.3, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRAngle", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowLForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBrowRForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthOpenY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMouthForm2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamTeethOn", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCheek", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGlassUD", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassWhite", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlight", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamGrassHighlightMove", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.256, + 0, + 0.344, + 2.02, + 0.433, + 2.02, + 1, + 0.856, + 2.02, + 1.278, + 0, + 1.7, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWaistAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyPosition", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBreath", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.256, + 0, + 0.344, + 0.5, + 0.433, + 0.5, + 1, + 0.633, + 0.5, + 0.833, + 0.279, + 1.033, + 0.2, + 1, + 1.467, + 0.029, + 1.9, + 0, + 2.333, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLeftShoulderUp", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.278, + 0, + 0.389, + 10, + 0.5, + 10, + 1, + 0.633, + 10, + 0.767, + -5, + 0.9, + -5, + 0, + 4, + -5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamRightShoulderUp", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 1, + 0.278, + 0, + 0.389, + 10, + 0.5, + 10, + 1, + 0.633, + 10, + 0.767, + -5, + 0.9, + -5, + 0, + 4, + -5 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllY", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllRotate", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFront", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSide", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBack", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairFrontFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairSideFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHairBackFuwa", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamJacket", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamChainWaist", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchSwingA2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchAX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBSwitch", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBOpen", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBOpen2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBRoll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBLR", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamWatchBUD", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAL04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmAR04", + "FadeInTime": 0.2, + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand01Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmBRHand05Roll3", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCR03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmCLHandRoll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDL03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmDLHand03Roll", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER01", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER02", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER03", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmER04", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand04Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll1", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmERHand06Roll2", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 0, + 0.167, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchA", + "Segments": [ + 0, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartWatchB", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAL", + "Segments": [ + 0, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmAR", + "Segments": [ + 0, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmBR", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmCL", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmDL", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "PartOpacity", + "Id": "PartArmER", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + } + ] +} +``` + +`Content/Samples/Rice/Rice.cdi3.json`: + +```json +{ + "Version": 3, + "Parameters": [ + { + "Id": "ParamAngleX", + "GroupId": "ParamGroupFace", + "Name": "角度 X" + }, + { + "Id": "ParamAngleZ", + "GroupId": "ParamGroupFace", + "Name": "角度 Z" + }, + { + "Id": "ParamEyeLOpen", + "GroupId": "ParamGroupEyes", + "Name": "左目 開閉" + }, + { + "Id": "ParamEyeROpen", + "GroupId": "ParamGroupEyes", + "Name": "右目 開閉" + }, + { + "Id": "ParamEyeBallX", + "GroupId": "ParamGroupEyes", + "Name": "目玉 X" + }, + { + "Id": "ParamEyeBallY", + "GroupId": "ParamGroupEyes", + "Name": "目玉 Y" + }, + { + "Id": "ParamBodyAngleX", + "GroupId": "ParamGroupBody", + "Name": "体の回転 X" + }, + { + "Id": "ParamBodyAngleY", + "GroupId": "ParamGroupBody", + "Name": "体の回転 Y" + }, + { + "Id": "ParamBodyAngleZ", + "GroupId": "ParamGroupBody", + "Name": "体の回転 Z" + }, + { + "Id": "ParamShoulderR", + "GroupId": "ParamGroupBody", + "Name": "右肩" + }, + { + "Id": "ParamShoulderL", + "GroupId": "ParamGroupBody", + "Name": "左肩" + }, + { + "Id": "ParamLegKnee", + "GroupId": "ParamGroupBody", + "Name": "屈伸" + }, + { + "Id": "ParamLegR", + "GroupId": "ParamGroupBody", + "Name": "右足の位置" + }, + { + "Id": "ParamLegRUpDw", + "GroupId": "ParamGroupBody", + "Name": "右足の上下" + }, + { + "Id": "ParamAllZ", + "GroupId": "ParamGroupBody", + "Name": "全体のZ" + }, + { + "Id": "ParamArmR01", + "GroupId": "ParamGroupArms", + "Name": "右上腕" + }, + { + "Id": "ParamArmR01Y", + "GroupId": "ParamGroupArms", + "Name": "右上腕 Y" + }, + { + "Id": "ParamArmR02", + "GroupId": "ParamGroupArms", + "Name": "右前腕" + }, + { + "Id": "ParamArmR02Y", + "GroupId": "ParamGroupArms", + "Name": "右前腕 Y" + }, + { + "Id": "ParamArmR03", + "GroupId": "ParamGroupArms", + "Name": "右手" + }, + { + "Id": "ParamArmL01", + "GroupId": "ParamGroupArms", + "Name": "左上腕" + }, + { + "Id": "ParamArmL02", + "GroupId": "ParamGroupArms", + "Name": "左前腕" + }, + { + "Id": "ParamArmL03", + "GroupId": "ParamGroupArms", + "Name": "左手" + }, + { + "Id": "ParamArmChange", + "GroupId": "ParamGroupArms", + "Name": "腕 切り替え" + }, + { + "Id": "ParamBookPage", + "GroupId": "ParamGroupArms", + "Name": "本 ページ" + }, + { + "Id": "ParamFlameOn", + "GroupId": "ParamGroupFlame", + "Name": "炎 表示" + }, + { + "Id": "ParamFlame", + "GroupId": "ParamGroupFlame", + "Name": "炎" + }, + { + "Id": "ParamFlameShaking", + "GroupId": "ParamGroupFlame", + "Name": "炎の揺れ" + }, + { + "Id": "ParamFlameX", + "GroupId": "ParamGroupFlame", + "Name": "炎の位置 X" + }, + { + "Id": "ParamFlameY", + "GroupId": "ParamGroupFlame", + "Name": "炎の位置 Y" + }, + { + "Id": "ParamCharge01On", + "GroupId": "ParamGroupEffectsA", + "Name": "溜めの表示" + }, + { + "Id": "ParamCharge01", + "GroupId": "ParamGroupEffectsA", + "Name": "溜め" + }, + { + "Id": "ParamHandLightAOn", + "GroupId": "ParamGroupEffectsA", + "Name": "手の光Aの表示" + }, + { + "Id": "ParamHandLightASize", + "GroupId": "ParamGroupEffectsA", + "Name": "手の光Aの拡縮" + }, + { + "Id": "ParamMagicPowersA", + "GroupId": "ParamGroupEffectsA", + "Name": "波動A" + }, + { + "Id": "ParamMagicAOn", + "GroupId": "ParamGroupEffectsA", + "Name": "魔法陣Aの表示" + }, + { + "Id": "ParamMagicARotation", + "GroupId": "ParamGroupEffectsA", + "Name": "魔法陣Aの回転" + }, + { + "Id": "ParamMagicALight", + "GroupId": "ParamGroupEffectsA", + "Name": "魔法陣A 光" + }, + { + "Id": "ParamEffectAX", + "GroupId": "ParamGroupEffectsA", + "Name": "エフェクトAの位置 X" + }, + { + "Id": "ParamEffectAY", + "GroupId": "ParamGroupEffectsA", + "Name": "エフェクトAの位置 Y" + }, + { + "Id": "ParamHandLightBOn", + "GroupId": "ParamGroupEffectsB", + "Name": "手の光Bの表示" + }, + { + "Id": "ParamHandLightBSize", + "GroupId": "ParamGroupEffectsB", + "Name": "手の光Bの拡張" + }, + { + "Id": "ParamMagicBOn", + "GroupId": "ParamGroupEffectsB", + "Name": "魔法陣Bの表示" + }, + { + "Id": "ParamMagicBRotation", + "GroupId": "ParamGroupEffectsB", + "Name": "魔法陣Bの回転" + }, + { + "Id": "ParamMagicBMove", + "GroupId": "ParamGroupEffectsB", + "Name": "魔法陣Bの移動" + }, + { + "Id": "ParamMagicBX", + "GroupId": "ParamGroupEffectsB", + "Name": "魔法陣Bの角度 X" + }, + { + "Id": "ParamMagicPowersBOn", + "GroupId": "ParamGroupEffectsB", + "Name": "波動Bの表示" + }, + { + "Id": "ParamMagicPowersBSize", + "GroupId": "ParamGroupEffectsB", + "Name": "波動Bの拡張" + }, + { + "Id": "ParamMagicPowersBThicknesses", + "GroupId": "ParamGroupEffectsB", + "Name": "波動Bの太さ" + }, + { + "Id": "ParamEffectBX", + "GroupId": "ParamGroupEffectsB", + "Name": "エフェクトBの位置 X" + }, + { + "Id": "ParamEffectBY", + "GroupId": "ParamGroupEffectsB", + "Name": "エフェクトBの位置 Y" + }, + { + "Id": "ParamHeadRibbon", + "GroupId": "ParamGroupSwing", + "Name": "頭リボンの揺れ" + }, + { + "Id": "ParamBustRibbon01", + "GroupId": "ParamGroupSwing", + "Name": "胸リボンの揺れ01" + }, + { + "Id": "ParamBustRibbon02", + "GroupId": "ParamGroupSwing", + "Name": "胸リボンの揺れ02" + }, + { + "Id": "ParamWaistRibbon01", + "GroupId": "ParamGroupSwing", + "Name": "腰リボンの揺れ01" + }, + { + "Id": "ParamWaistRibbon02", + "GroupId": "ParamGroupSwing", + "Name": "腰リボンの揺れ02" + }, + { + "Id": "ParamSkirt", + "GroupId": "ParamGroupSwing", + "Name": "スカートのなびき" + }, + { + "Id": "ParamSkirtX", + "GroupId": "ParamGroupSwing", + "Name": "スカートの揺れ X" + }, + { + "Id": "ParamSkirtY", + "GroupId": "ParamGroupSwing", + "Name": "スカートの揺れ Y" + }, + { + "Id": "ParamHairFront01", + "GroupId": "ParamGroupHairFront", + "Name": "髪揺れ 前01" + }, + { + "Id": "ParamHairFront02", + "GroupId": "ParamGroupHairFront", + "Name": "髪揺れ 前02" + }, + { + "Id": "Param_Angle_Rotation_1_ArtMesh67", + "GroupId": "ParamGroupHairSideR", + "Name": "[0]右横髪" + }, + { + "Id": "Param_Angle_Rotation_2_ArtMesh67", + "GroupId": "ParamGroupHairSideR", + "Name": "[1]右横髪" + }, + { + "Id": "Param_Angle_Rotation_3_ArtMesh67", + "GroupId": "ParamGroupHairSideR", + "Name": "[2]右横髪" + }, + { + "Id": "Param_Angle_Rotation_4_ArtMesh67", + "GroupId": "ParamGroupHairSideR", + "Name": "[3]右横髪" + }, + { + "Id": "Param_Angle_Rotation_5_ArtMesh67", + "GroupId": "ParamGroupHairSideR", + "Name": "[4]右横髪" + }, + { + "Id": "Param_Angle_Rotation_1_ArtMesh20", + "GroupId": "ParamGroupHairSideL", + "Name": "[0]左横髪" + }, + { + "Id": "Param_Angle_Rotation_2_ArtMesh20", + "GroupId": "ParamGroupHairSideL", + "Name": "[1]左横髪" + }, + { + "Id": "Param_Angle_Rotation_3_ArtMesh20", + "GroupId": "ParamGroupHairSideL", + "Name": "[2]左横髪" + }, + { + "Id": "Param_Angle_Rotation_4_ArtMesh20", + "GroupId": "ParamGroupHairSideL", + "Name": "[3]左横髪" + }, + { + "Id": "Param_Angle_Rotation_5_ArtMesh20", + "GroupId": "ParamGroupHairSideL", + "Name": "[4]左横髪" + }, + { + "Id": "Param_Angle_Rotation_1_ArtMesh82", + "GroupId": "ParamGroupHairBackA", + "Name": "[0]後髪A" + }, + { + "Id": "Param_Angle_Rotation_2_ArtMesh82", + "GroupId": "ParamGroupHairBackA", + "Name": "[1]後髪A" + }, + { + "Id": "Param_Angle_Rotation_3_ArtMesh82", + "GroupId": "ParamGroupHairBackA", + "Name": "[2]後髪A" + }, + { + "Id": "Param_Angle_Rotation_4_ArtMesh82", + "GroupId": "ParamGroupHairBackA", + "Name": "[3]後髪A" + }, + { + "Id": "Param_Angle_Rotation_5_ArtMesh82", + "GroupId": "ParamGroupHairBackA", + "Name": "[4]後髪A" + }, + { + "Id": "Param_Angle_Rotation_6_ArtMesh82", + "GroupId": "ParamGroupHairBackA", + "Name": "[5]後髪A" + }, + { + "Id": "Param_Angle_Rotation_7_ArtMesh82", + "GroupId": "ParamGroupHairBackA", + "Name": "[6]後髪A" + }, + { + "Id": "Param_Angle_Rotation_1_ArtMesh81", + "GroupId": "ParamGroupHairBackB", + "Name": "[0]後髪B" + }, + { + "Id": "Param_Angle_Rotation_2_ArtMesh81", + "GroupId": "ParamGroupHairBackB", + "Name": "[1]後髪B" + }, + { + "Id": "Param_Angle_Rotation_3_ArtMesh81", + "GroupId": "ParamGroupHairBackB", + "Name": "[2]後髪B" + }, + { + "Id": "Param_Angle_Rotation_4_ArtMesh81", + "GroupId": "ParamGroupHairBackB", + "Name": "[3]後髪B" + }, + { + "Id": "Param_Angle_Rotation_5_ArtMesh81", + "GroupId": "ParamGroupHairBackB", + "Name": "[4]後髪B" + }, + { + "Id": "Param_Angle_Rotation_6_ArtMesh81", + "GroupId": "ParamGroupHairBackB", + "Name": "[5]後髪B" + }, + { + "Id": "Param_Angle_Rotation_7_ArtMesh81", + "GroupId": "ParamGroupHairBackB", + "Name": "[6]後髪B" + }, + { + "Id": "Param_Angle_Rotation_8_ArtMesh81", + "GroupId": "ParamGroupHairBackB", + "Name": "[7]後髪B" + }, + { + "Id": "Param_Angle_Rotation_9_ArtMesh81", + "GroupId": "ParamGroupHairBackB", + "Name": "[8]後髪B" + }, + { + "Id": "Param_Angle_Rotation_1_ArtMesh80", + "GroupId": "ParamGroupHairBackC", + "Name": "[0]後髪C" + }, + { + "Id": "Param_Angle_Rotation_2_ArtMesh80", + "GroupId": "ParamGroupHairBackC", + "Name": "[1]後髪C" + }, + { + "Id": "Param_Angle_Rotation_3_ArtMesh80", + "GroupId": "ParamGroupHairBackC", + "Name": "[2]後髪C" + }, + { + "Id": "Param_Angle_Rotation_4_ArtMesh80", + "GroupId": "ParamGroupHairBackC", + "Name": "[3]後髪C" + }, + { + "Id": "Param_Angle_Rotation_5_ArtMesh80", + "GroupId": "ParamGroupHairBackC", + "Name": "[4]後髪C" + }, + { + "Id": "Param_Angle_Rotation_6_ArtMesh80", + "GroupId": "ParamGroupHairBackC", + "Name": "[5]後髪C" + }, + { + "Id": "Param_Angle_Rotation_7_ArtMesh80", + "GroupId": "ParamGroupHairBackC", + "Name": "[6]後髪C" + }, + { + "Id": "Param_Angle_Rotation_8_ArtMesh80", + "GroupId": "ParamGroupHairBackC", + "Name": "[7]後髪C" + }, + { + "Id": "Param_Angle_Rotation_9_ArtMesh80", + "GroupId": "ParamGroupHairBackC", + "Name": "[8]後髪C" + } + ], + "ParameterGroups": [ + { + "Id": "ParamGroupFace", + "GroupId": "", + "Name": "顔" + }, + { + "Id": "ParamGroupEyes", + "GroupId": "", + "Name": "目" + }, + { + "Id": "ParamGroupBody", + "GroupId": "", + "Name": "体" + }, + { + "Id": "ParamGroupArms", + "GroupId": "", + "Name": "腕" + }, + { + "Id": "ParamGroupFlame", + "GroupId": "", + "Name": "炎" + }, + { + "Id": "ParamGroupEffectsA", + "GroupId": "", + "Name": "エフェクトA" + }, + { + "Id": "ParamGroupEffectsB", + "GroupId": "", + "Name": "エフェクトB" + }, + { + "Id": "ParamGroupSwing", + "GroupId": "", + "Name": "揺れもの" + }, + { + "Id": "ParamGroupHairFront", + "GroupId": "", + "Name": "前髪" + }, + { + "Id": "ParamGroupHairSideR", + "GroupId": "", + "Name": "右横髪" + }, + { + "Id": "ParamGroupHairSideL", + "GroupId": "", + "Name": "左横髪" + }, + { + "Id": "ParamGroupHairBackA", + "GroupId": "", + "Name": "後髪A" + }, + { + "Id": "ParamGroupHairBackB", + "GroupId": "", + "Name": "後髪B" + }, + { + "Id": "ParamGroupHairBackC", + "GroupId": "", + "Name": "後髪C" + } + ], + "Parts": [ + { + "Id": "PartCore", + "Name": "コア" + }, + { + "Id": "PartFlame", + "Name": "炎" + }, + { + "Id": "PartMagicPowersB", + "Name": "波動B" + }, + { + "Id": "PartMagicB", + "Name": "魔法陣B" + }, + { + "Id": "PartMagicPowersA", + "Name": "波動A" + }, + { + "Id": "PartMagicA", + "Name": "魔法陣A" + }, + { + "Id": "PartHat", + "Name": "帽子" + }, + { + "Id": "PartHair", + "Name": "髪" + }, + { + "Id": "PartBrow", + "Name": "眉" + }, + { + "Id": "PartEyeL", + "Name": "左目" + }, + { + "Id": "PartEyeR", + "Name": "右目" + }, + { + "Id": "PartFace", + "Name": "顔" + }, + { + "Id": "PartArmR02", + "Name": "右腕 差分" + }, + { + "Id": "PartArmR01", + "Name": "右腕" + }, + { + "Id": "PartBody", + "Name": "体" + }, + { + "Id": "PartLegR", + "Name": "右足" + }, + { + "Id": "PartLegL", + "Name": "左足" + }, + { + "Id": "PartArmL", + "Name": "左腕" + }, + { + "Id": "PartHairBack", + "Name": "後ろ髪" + }, + { + "Id": "PartWaveSave", + "Name": "波動 溜め" + }, + { + "Id": "ArtMesh20_Skinning", + "Name": "左横髪(スキニング)" + }, + { + "Id": "Part20", + "Name": "左横髪(回転)" + }, + { + "Id": "ArtMesh67_Skinning", + "Name": "右横髪(スキニング)" + }, + { + "Id": "Part19", + "Name": "右横髪(回転)" + }, + { + "Id": "ArtMesh80_Skinning", + "Name": "後髪C(スキニング)" + }, + { + "Id": "Part17", + "Name": "後髪C(回転)" + }, + { + "Id": "ArtMesh81_Skinning", + "Name": "後髪B(スキニング)" + }, + { + "Id": "Part18", + "Name": "後髪B(回転)" + }, + { + "Id": "ArtMesh82_Skinning", + "Name": "後髪A(スキニング)" + }, + { + "Id": "Part16", + "Name": "後髪A(回転)" + } + ] +} +``` + +`Content/Samples/Rice/Rice.model3.json`: + +```json +{ + "Version": 3, + "FileReferences": { + "Moc": "Rice.moc3", + "Textures": [ + "Rice.2048/texture_00.png", + "Rice.2048/texture_01.png" + ], + "Physics": "Rice.physics3.json", + "DisplayInfo": "Rice.cdi3.json", + "Motions": { + "Idle": [ + { + "File": "motions/mtn_00.motion3.json" + } + ], + "TapBody": [ + { + "File": "motions/mtn_01.motion3.json" + }, + { + "File": "motions/mtn_02.motion3.json" + }, + { + "File": "motions/mtn_03.motion3.json" + } + ] + } + }, + "Groups": [ + { + "Target": "Parameter", + "Name": "EyeBlink", + "Ids": [ + "ParamEyeLOpen", + "ParamEyeROpen" + ] + }, + { + "Target": "Parameter", + "Name": "LipSync", + "Ids": [] + } + ], + "HitAreas": [ + { + "Id": "HitAreaBody", + "Name": "Body" + } + ] +} +``` + +`Content/Samples/Rice/Rice.physics3.json`: + +```json +{ + "Version": 3, + "Meta": { + "PhysicsSettingCount": 9, + "TotalInputCount": 32, + "TotalOutputCount": 42, + "VertexCount": 51, + "EffectiveForces": { + "Gravity": { + "X": 0, + "Y": -1 + }, + "Wind": { + "X": 0, + "Y": 0 + } + }, + "PhysicsDictionary": [ + { + "Id": "PhysicsSetting1", + "Name": "後髪A" + }, + { + "Id": "PhysicsSetting2", + "Name": "後髪B" + }, + { + "Id": "PhysicsSetting3", + "Name": "後髪C" + }, + { + "Id": "PhysicsSetting4", + "Name": "右横髪" + }, + { + "Id": "PhysicsSetting5", + "Name": "左横髪" + }, + { + "Id": "PhysicsSetting6", + "Name": "前髪" + }, + { + "Id": "PhysicsSetting7", + "Name": "頭リボン" + }, + { + "Id": "PhysicsSetting8", + "Name": "胸リボン" + }, + { + "Id": "PhysicsSetting9", + "Name": "腰リボン" + } + ] + }, + "PhysicsSettings": [ + { + "Id": "PhysicsSetting1", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_1_ArtMesh82" + }, + "VertexIndex": 1, + "Scale": 15, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_2_ArtMesh82" + }, + "VertexIndex": 2, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_3_ArtMesh82" + }, + "VertexIndex": 3, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_4_ArtMesh82" + }, + "VertexIndex": 4, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_5_ArtMesh82" + }, + "VertexIndex": 5, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_6_ArtMesh82" + }, + "VertexIndex": 6, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_7_ArtMesh82" + }, + "VertexIndex": 7, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 0.95, + "Acceleration": 1.5, + "Radius": 7 + }, + { + "Position": { + "X": 0, + "Y": 7 + }, + "Mobility": 0.85, + "Delay": 0.95, + "Acceleration": 1.5, + "Radius": 7 + }, + { + "Position": { + "X": 0, + "Y": 14 + }, + "Mobility": 0.85, + "Delay": 0.95, + "Acceleration": 1.5, + "Radius": 7 + }, + { + "Position": { + "X": 0, + "Y": 21 + }, + "Mobility": 0.85, + "Delay": 0.95, + "Acceleration": 1.5, + "Radius": 7 + }, + { + "Position": { + "X": 0, + "Y": 28 + }, + "Mobility": 0.85, + "Delay": 0.95, + "Acceleration": 1.5, + "Radius": 7 + }, + { + "Position": { + "X": 0, + "Y": 35 + }, + "Mobility": 0.85, + "Delay": 0.95, + "Acceleration": 1.5, + "Radius": 7 + }, + { + "Position": { + "X": 0, + "Y": 42 + }, + "Mobility": 0.85, + "Delay": 0.95, + "Acceleration": 1.5, + "Radius": 7 + }, + { + "Position": { + "X": 0, + "Y": 49 + }, + "Mobility": 0.85, + "Delay": 0.95, + "Acceleration": 1.5, + "Radius": 7 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting2", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_1_ArtMesh81" + }, + "VertexIndex": 1, + "Scale": 15, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_2_ArtMesh81" + }, + "VertexIndex": 2, + "Scale": 35, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_3_ArtMesh81" + }, + "VertexIndex": 3, + "Scale": 35, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_4_ArtMesh81" + }, + "VertexIndex": 4, + "Scale": 35, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_5_ArtMesh81" + }, + "VertexIndex": 5, + "Scale": 35, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_6_ArtMesh81" + }, + "VertexIndex": 6, + "Scale": 35, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_7_ArtMesh81" + }, + "VertexIndex": 7, + "Scale": 35, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_8_ArtMesh81" + }, + "VertexIndex": 8, + "Scale": 35, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_9_ArtMesh81" + }, + "VertexIndex": 9, + "Scale": 35, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 6 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 12 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 18 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 24 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 2, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 30 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 2, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 36 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 2, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 42 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 2.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 48 + }, + "Mobility": 0.95, + "Delay": 0.9, + "Acceleration": 2.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 54 + }, + "Mobility": 0.95, + "Delay": 0.9, + "Acceleration": 2.5, + "Radius": 6 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting3", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_1_ArtMesh80" + }, + "VertexIndex": 1, + "Scale": 15, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_2_ArtMesh80" + }, + "VertexIndex": 2, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_3_ArtMesh80" + }, + "VertexIndex": 3, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_4_ArtMesh80" + }, + "VertexIndex": 4, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_5_ArtMesh80" + }, + "VertexIndex": 5, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_6_ArtMesh80" + }, + "VertexIndex": 6, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_7_ArtMesh80" + }, + "VertexIndex": 7, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_8_ArtMesh80" + }, + "VertexIndex": 8, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_9_ArtMesh80" + }, + "VertexIndex": 9, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 6 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 12 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 18 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 24 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 2, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 30 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 2, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 36 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 2, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 42 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 2.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 48 + }, + "Mobility": 0.95, + "Delay": 0.9, + "Acceleration": 2.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 54 + }, + "Mobility": 0.95, + "Delay": 0.9, + "Acceleration": 2.5, + "Radius": 6 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting4", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_1_ArtMesh67" + }, + "VertexIndex": 1, + "Scale": 30, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_2_ArtMesh67" + }, + "VertexIndex": 2, + "Scale": 40, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_3_ArtMesh67" + }, + "VertexIndex": 3, + "Scale": 40, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_4_ArtMesh67" + }, + "VertexIndex": 4, + "Scale": 40, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_5_ArtMesh67" + }, + "VertexIndex": 5, + "Scale": 40, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 6 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 12 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 18 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 24 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 2, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 30 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 2, + "Radius": 6 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting5", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_1_ArtMesh20" + }, + "VertexIndex": 1, + "Scale": 10, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_2_ArtMesh20" + }, + "VertexIndex": 2, + "Scale": 35, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_3_ArtMesh20" + }, + "VertexIndex": 3, + "Scale": 40, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_4_ArtMesh20" + }, + "VertexIndex": 4, + "Scale": 40, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "Param_Angle_Rotation_5_ArtMesh20" + }, + "VertexIndex": 5, + "Scale": 40, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 5 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 10 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 15.5 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 21.5 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 2, + "Radius": 6 + }, + { + "Position": { + "X": 0, + "Y": 27.5 + }, + "Mobility": 0.85, + "Delay": 0.9, + "Acceleration": 2, + "Radius": 6 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting6", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHairFront01" + }, + "VertexIndex": 1, + "Scale": 3, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHairFront02" + }, + "VertexIndex": 2, + "Scale": 3, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 0.8, + "Delay": 0.9, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 7 + }, + "Mobility": 0.8, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 7 + }, + { + "Position": { + "X": 0, + "Y": 14 + }, + "Mobility": 0.8, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 7 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting7", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleX" + }, + "Weight": 60, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamAngleZ" + }, + "Weight": 60, + "Type": "Angle", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 40, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 40, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamHeadRibbon" + }, + "VertexIndex": 1, + "Scale": 2, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 4 + }, + "Mobility": 0.9, + "Delay": 0.9, + "Acceleration": 1.5, + "Radius": 4 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting8", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 100, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamBustRibbon01" + }, + "VertexIndex": 1, + "Scale": 1, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "ParamBustRibbon02" + }, + "VertexIndex": 2, + "Scale": 1, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 7 + }, + "Mobility": 0.9, + "Delay": 0.9, + "Acceleration": 1.2, + "Radius": 7 + }, + { + "Position": { + "X": 0, + "Y": 13 + }, + "Mobility": 0.9, + "Delay": 0.9, + "Acceleration": 1.2, + "Radius": 6 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + }, + { + "Id": "PhysicsSetting9", + "Input": [ + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleX" + }, + "Weight": 100, + "Type": "X", + "Reflect": false + }, + { + "Source": { + "Target": "Parameter", + "Id": "ParamBodyAngleZ" + }, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Output": [ + { + "Destination": { + "Target": "Parameter", + "Id": "ParamWaistRibbon01" + }, + "VertexIndex": 1, + "Scale": 1, + "Weight": 100, + "Type": "Angle", + "Reflect": false + }, + { + "Destination": { + "Target": "Parameter", + "Id": "ParamWaistRibbon02" + }, + "VertexIndex": 2, + "Scale": 1, + "Weight": 100, + "Type": "Angle", + "Reflect": false + } + ], + "Vertices": [ + { + "Position": { + "X": 0, + "Y": 0 + }, + "Mobility": 1, + "Delay": 1, + "Acceleration": 1, + "Radius": 0 + }, + { + "Position": { + "X": 0, + "Y": 12 + }, + "Mobility": 0.95, + "Delay": 0.8, + "Acceleration": 1.5, + "Radius": 12 + }, + { + "Position": { + "X": 0, + "Y": 22 + }, + "Mobility": 0.95, + "Delay": 0.8, + "Acceleration": 1.5, + "Radius": 10 + } + ], + "Normalization": { + "Position": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + }, + "Angle": { + "Minimum": -10, + "Default": 0, + "Maximum": 10 + } + } + } + ] +} +``` + +`Content/Samples/Rice/motions/mtn_00.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 4, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 54, + "TotalSegmentCount": 95, + "TotalPointCount": 283, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.344, + 0, + 0.689, + 3, + 1.033, + 3, + 1, + 1.4, + 3, + 1.767, + 0, + 2.133, + 0, + 1, + 2.444, + 0, + 2.756, + 2, + 3.067, + 2, + 1, + 3.378, + 2, + 3.689, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 0, + 4, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + -3, + 2, + -3, + 1, + 2.667, + -3, + 3.333, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.667, + 0, + 1.333, + 1, + 2, + 1, + 1, + 2.667, + 1, + 3.333, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulderR", + "Segments": [ + 0, + 0, + 1, + 0.344, + 0, + 0.689, + 2, + 1.033, + 2, + 1, + 1.367, + 2, + 1.7, + 0, + 2.033, + 0, + 1, + 2.378, + 0, + 2.722, + 1, + 3.067, + 1, + 1, + 3.378, + 1, + 3.689, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulderL", + "Segments": [ + 0, + 0, + 1, + 0.344, + 0, + 0.689, + 2, + 1.033, + 2, + 1, + 1.367, + 2, + 1.7, + 0, + 2.033, + 0, + 1, + 2.378, + 0, + 2.722, + 1, + 3.067, + 1, + 1, + 3.378, + 1, + 3.689, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLegKnee", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 1, + 0.933, + 1, + 1, + 1.267, + 1, + 1.6, + 0, + 1.933, + 0, + 1, + 2.289, + 0, + 2.644, + 1, + 3, + 1, + 1, + 3.333, + 1, + 3.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLegR", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLegRUpDw", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllZ", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR01", + "Segments": [ + 0, + 0, + 1, + 0.133, + 0, + 0.267, + -0.175, + 0.4, + -0.175, + 1, + 0.678, + -0.175, + 0.956, + 0.55, + 1.233, + 0.55, + 1, + 1.578, + 0.55, + 1.922, + -0.375, + 2.267, + -0.375, + 1, + 2.6, + -0.375, + 2.933, + 0.475, + 3.267, + 0.475, + 1, + 3.511, + 0.475, + 3.756, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR01Y", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR02", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR02Y", + "Segments": [ + 0, + -10, + 1, + 1.333, + -10, + 2.667, + -10, + 4, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR03", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + -1, + 0.5, + -1, + 1, + 0.778, + -1, + 1.056, + 3, + 1.333, + 3, + 1, + 1.667, + 3, + 2, + -2, + 2.333, + -2, + 1, + 2.678, + -2, + 3.022, + 1, + 3.367, + 1, + 1, + 3.578, + 1, + 3.789, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL01", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL02", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL03", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmChange", + "FadeInTime": 0.0, + "FadeOutTime": 0.0, + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBookPage", + "Segments": [ + 0, + 0, + 0, + 0.667, + 0, + 0, + 1.367, + 29, + 2, + 1.4, + 0, + 0, + 2.733, + 0, + 0, + 3.433, + 29, + 2, + 3.467, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameOn", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlame", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameShaking", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameX", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameY", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCharge01On", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCharge01", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightAOn", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightASize", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersA", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicAOn", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicARotation", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicALight", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectAX", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectAY", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightBOn", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightBSize", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBOn", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBRotation", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBMove", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBX", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersBOn", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersBSize", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersBThicknesses", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectBX", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectBY", + "Segments": [ + 0, + 0, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamSkirt", + "Segments": [ + 0, + 0, + 0, + 1, + 30, + 2, + 1.033, + 1, + 0, + 2, + 30, + 2, + 2.033, + 1, + 0, + 3, + 30, + 2, + 3.033, + 1, + 0, + 4, + 30 + ] + }, + { + "Target": "Parameter", + "Id": "ParamSkirtX", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + -0.5, + 0.5, + -0.5, + 1, + 0.778, + -0.5, + 1.056, + 0.2, + 1.333, + 0.2, + 1, + 1.633, + 0.2, + 1.933, + -0.4, + 2.233, + -0.4, + 1, + 2.567, + -0.4, + 2.9, + 0.4, + 3.233, + 0.4, + 1, + 3.489, + 0.4, + 3.744, + 0, + 4, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamSkirtY", + "Segments": [ + 0, + 0, + 1, + 0.289, + 0, + 0.578, + 0.4, + 0.867, + 0.4, + 1, + 1.189, + 0.4, + 1.511, + -0.7, + 1.833, + -0.7, + 1, + 2.189, + -0.7, + 2.544, + 0.6, + 2.9, + 0.6, + 1, + 3.267, + 0.6, + 3.633, + 0, + 4, + 0 + ] + } + ] +} +``` + +`Content/Samples/Rice/motions/mtn_01.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 6, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 54, + "TotalSegmentCount": 322, + "TotalPointCount": 880, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.144, + 0, + 0.289, + -10, + 0.433, + -10, + 1, + 0.578, + -10, + 0.722, + 11.077, + 0.867, + 15, + 1, + 1.311, + 27.071, + 1.756, + 30, + 2.2, + 30, + 1, + 2.267, + 30, + 2.333, + 30, + 2.4, + 30, + 1, + 2.456, + 30, + 2.511, + -30, + 2.567, + -30, + 1, + 2.7, + -30, + 2.833, + -27, + 2.967, + -27, + 1, + 3.311, + -27, + 3.656, + -27, + 4, + -27, + 1, + 4.3, + -27, + 4.6, + 0, + 4.9, + 0, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.144, + 0, + 0.289, + 27, + 0.433, + 27, + 1, + 0.567, + 27, + 0.7, + 8.467, + 0.833, + 6, + 1, + 1.289, + -2.429, + 1.744, + -7.126, + 2.2, + -15, + 1, + 2.278, + -16.344, + 2.356, + -30, + 2.433, + -30, + 1, + 2.478, + -30, + 2.522, + 11.148, + 2.567, + 21, + 1, + 2.611, + 30.852, + 2.656, + 30, + 2.7, + 30, + 1, + 2.789, + 30, + 2.878, + 0, + 2.967, + 0, + 1, + 3.1, + 0, + 3.233, + 6, + 3.367, + 6, + 1, + 3.578, + 6, + 3.789, + 6, + 4, + 6, + 1, + 4.156, + 6, + 4.311, + 6, + 4.467, + 6, + 1, + 4.644, + 6, + 4.822, + 0, + 5, + 0, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "FadeInTime": 0.5, + "FadeOutTime": 0.5, + "Segments": [ + 0, + 1, + 1, + 0.156, + 1, + 0.311, + 1, + 0.467, + 1, + 1, + 0.544, + 1, + 0.622, + 0, + 0.7, + 0, + 1, + 1.267, + 0, + 1.833, + 0, + 2.4, + 0, + 1, + 2.467, + 0, + 2.533, + 1, + 2.6, + 1, + 1, + 3.322, + 1, + 4.044, + 1, + 4.767, + 1, + 0, + 6, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "FadeInTime": 0.5, + "FadeOutTime": 0.5, + "Segments": [ + 0, + 1, + 1, + 0.156, + 1, + 0.311, + 1, + 0.467, + 1, + 1, + 0.544, + 1, + 0.622, + 0, + 0.7, + 0, + 1, + 1.267, + 0, + 1.833, + 0, + 2.4, + 0, + 1, + 2.467, + 0, + 2.533, + 1, + 2.6, + 1, + 1, + 3.322, + 1, + 4.044, + 1, + 4.767, + 1, + 0, + 6, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 1.589, + 0, + 3.178, + 0, + 4.767, + 0, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 1.589, + 0, + 3.178, + 0, + 4.767, + 0, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + -10, + 0.333, + -10, + 1, + 0.456, + -10, + 0.578, + 6.298, + 0.7, + 7, + 1, + 1.133, + 9.49, + 1.567, + 10, + 2, + 10, + 1, + 2.122, + 10, + 2.244, + 10, + 2.367, + 10, + 1, + 2.478, + 10, + 2.589, + -10, + 2.7, + -10, + 1, + 2.789, + -10, + 2.878, + -7, + 2.967, + -7, + 1, + 3.278, + -7, + 3.589, + -7, + 3.9, + -7, + 1, + 3.978, + -7, + 4.056, + -7, + 4.133, + -7, + 1, + 4.344, + -7, + 4.556, + 0, + 4.767, + 0, + 1, + 4.956, + 0, + 5.144, + 0, + 5.333, + 0, + 1, + 5.556, + 0, + 5.778, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 3, + 0.333, + 3, + 1, + 0.511, + 3, + 0.689, + 0, + 0.867, + 0, + 1, + 1.3, + 0, + 1.733, + 0, + 2.167, + 0, + 1, + 2.233, + 0, + 2.3, + -3, + 2.367, + -3, + 1, + 2.467, + -3, + 2.567, + 0, + 2.667, + 0, + 1, + 3.089, + 0, + 3.511, + 0, + 3.933, + 0, + 1, + 4.211, + 0, + 4.489, + 0, + 4.767, + 0, + 1, + 5.178, + 0, + 5.589, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 0, + 0.333, + 0, + 1, + 0.856, + 0, + 1.378, + 3, + 1.9, + 3, + 1, + 2, + 3, + 2.1, + 2.298, + 2.2, + 0, + 1, + 2.267, + -1.532, + 2.333, + -3, + 2.4, + -3, + 1, + 2.467, + -3, + 2.533, + 3, + 2.6, + 3, + 1, + 2.678, + 3, + 2.756, + 0, + 2.833, + 0, + 1, + 2.911, + 0, + 2.989, + 1, + 3.067, + 1, + 1, + 3.333, + 1, + 3.6, + 0, + 3.867, + 0, + 1, + 3.911, + 0, + 3.956, + 0, + 4, + 0, + 1, + 4.122, + 0, + 4.244, + 2, + 4.367, + 2, + 1, + 4.544, + 2, + 4.722, + 0, + 4.9, + 0, + 1, + 5.267, + 0, + 5.633, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulderR", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 5, + 0.333, + 5, + 1, + 0.489, + 5, + 0.644, + 4.6, + 0.8, + 4.6, + 1, + 1.267, + 4.6, + 1.733, + 4.876, + 2.2, + 5.4, + 1, + 2.267, + 5.475, + 2.333, + 5.461, + 2.4, + 5.6, + 1, + 2.478, + 5.762, + 2.556, + 10, + 2.633, + 10, + 1, + 2.744, + 10, + 2.856, + 7.2, + 2.967, + 7.2, + 1, + 3.311, + 7.2, + 3.656, + 7.2, + 4, + 7.2, + 1, + 4.256, + 7.2, + 4.511, + 0, + 4.767, + 0, + 1, + 5.178, + 0, + 5.589, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulderL", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + 5, + 0.333, + 5, + 1, + 0.422, + 5, + 0.511, + 0, + 0.6, + 0, + 1, + 1.133, + 0, + 1.667, + 0, + 2.2, + 0, + 1, + 2.456, + 0, + 2.711, + 0, + 2.967, + 0, + 1, + 3.311, + 0, + 3.656, + 0, + 4, + 0, + 1, + 4.256, + 0, + 4.511, + 0, + 4.767, + 0, + 1, + 5.178, + 0, + 5.589, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLegKnee", + "Segments": [ + 0, + 0, + 1, + 0.144, + 0, + 0.289, + 3, + 0.433, + 3, + 1, + 0.522, + 3, + 0.611, + 0.075, + 0.7, + 0, + 1, + 1.167, + -0.394, + 1.633, + -0.614, + 2.1, + -1, + 1, + 2.178, + -1.064, + 2.256, + -3, + 2.333, + -3, + 1, + 2.433, + -3, + 2.533, + 2, + 2.633, + 2, + 1, + 2.767, + 2, + 2.9, + 0, + 3.033, + 0, + 1, + 3.356, + 0, + 3.678, + 0, + 4, + 0, + 1, + 4.256, + 0, + 4.511, + 0, + 4.767, + 0, + 1, + 5.178, + 0, + 5.589, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLegR", + "Segments": [ + 0, + 0, + 1, + 0.722, + 0, + 1.444, + 0, + 2.167, + 0, + 1, + 2.344, + 0, + 2.522, + -10, + 2.7, + -10, + 1, + 2.889, + -10, + 3.078, + -8, + 3.267, + -8, + 1, + 3.511, + -8, + 3.756, + -8, + 4, + -8, + 1, + 4.256, + -8, + 4.511, + 0, + 4.767, + 0, + 1, + 5.178, + 0, + 5.589, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLegRUpDw", + "Segments": [ + 0, + 0, + 1, + 0.678, + 0, + 1.356, + 0, + 2.033, + 0, + 1, + 2.133, + 0, + 2.233, + 3, + 2.333, + 3, + 1, + 2.433, + 3, + 2.533, + 0, + 2.633, + 0, + 1, + 3.1, + 0, + 3.567, + 0, + 4.033, + 0, + 1, + 4.167, + 0, + 4.3, + 1, + 4.433, + 1, + 1, + 4.589, + 1, + 4.744, + 0, + 4.9, + 0, + 1, + 5.267, + 0, + 5.633, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllZ", + "Segments": [ + 0, + 0, + 1, + 1.333, + 0, + 2.667, + 0, + 4, + 0, + 1, + 4.244, + 0, + 4.489, + 0, + 4.733, + 0, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR01", + "Segments": [ + 0, + 0, + 1, + 0.133, + 0, + 0.267, + 5, + 0.4, + 5, + 1, + 0.5, + 5, + 0.6, + -1.703, + 0.7, + -2, + 1, + 1.111, + -3.221, + 1.522, + -3.831, + 1.933, + -5, + 1, + 2.011, + -5.221, + 2.089, + -9, + 2.167, + -9, + 1, + 2.233, + -9, + 2.3, + -8.692, + 2.367, + -6, + 1, + 2.4, + -4.654, + 2.433, + 28.535, + 2.467, + 29, + 1, + 2.533, + 29.93, + 2.6, + 30, + 2.667, + 30, + 1, + 2.756, + 30, + 2.844, + 29, + 2.933, + 29, + 1, + 3.022, + 29, + 3.111, + 30, + 3.2, + 30, + 1, + 3.489, + 30, + 3.778, + 30, + 4.067, + 30, + 1, + 4.356, + 30, + 4.644, + -0.525, + 4.933, + -0.525, + 1, + 5.144, + -0.525, + 5.356, + 0, + 5.567, + 0, + 1, + 5.711, + 0, + 5.856, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR01Y", + "Segments": [ + 0, + 0, + 1, + 1.389, + 0, + 2.778, + 0, + 4.167, + 0, + 1, + 4.422, + 0, + 4.678, + 0, + 4.933, + 0, + 1, + 5.289, + 0, + 5.644, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR02", + "Segments": [ + 0, + 0, + 1, + 0.133, + 0, + 0.267, + 1, + 0.4, + 1, + 1, + 0.522, + 1, + 0.644, + -33.746, + 0.767, + -34, + 1, + 1.178, + -34.856, + 1.589, + -35.216, + 2, + -36, + 1, + 2.078, + -36.148, + 2.156, + -40, + 2.233, + -40, + 1, + 2.3, + -40, + 2.367, + -35.967, + 2.433, + -24, + 1, + 2.467, + -18.017, + 2.5, + -2.828, + 2.533, + 0, + 1, + 2.578, + 3.77, + 2.622, + 4, + 2.667, + 4, + 1, + 2.756, + 4, + 2.844, + 0, + 2.933, + 0, + 1, + 3.344, + 0, + 3.756, + 0, + 4.167, + 0, + 1, + 4.422, + 0, + 4.678, + 0, + 4.933, + 0, + 1, + 5.289, + 0, + 5.644, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR02Y", + "Segments": [ + 0, + -10, + 1, + 0.133, + -10, + 0.267, + -10, + 0.4, + -10, + 1, + 0.522, + -10, + 0.644, + -20, + 0.767, + -20, + 1, + 1.178, + -20, + 1.589, + -20, + 2, + -20, + 1, + 2.078, + -20, + 2.156, + -20, + 2.233, + -20, + 1, + 2.289, + -20, + 2.344, + -18.458, + 2.4, + -10, + 1, + 2.422, + -6.617, + 2.444, + 10, + 2.467, + 10, + 1, + 2.5, + 10, + 2.533, + 0, + 2.567, + 0, + 1, + 3.044, + 0, + 3.522, + 0, + 4, + 0, + 1, + 4.256, + 0, + 4.511, + -10, + 4.767, + -10, + 1, + 5.178, + -10, + 5.589, + -10, + 6, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR03", + "Segments": [ + 0, + 0, + 1, + 0.156, + 0, + 0.311, + 18, + 0.467, + 18, + 1, + 0.6, + 18, + 0.733, + -15.591, + 0.867, + -16, + 1, + 1.289, + -17.295, + 1.711, + -17.781, + 2.133, + -19, + 1, + 2.233, + -19.289, + 2.333, + -30, + 2.433, + -30, + 1, + 2.533, + -30, + 2.633, + 11, + 2.733, + 11, + 1, + 2.844, + 11, + 2.956, + 0, + 3.067, + 0, + 1, + 3.378, + 0, + 3.689, + 0, + 4, + 0, + 1, + 4.256, + 0, + 4.511, + 0, + 4.767, + 0, + 1, + 5.178, + 0, + 5.589, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL01", + "Segments": [ + 0, + 0, + 1, + 0.267, + 0, + 0.533, + 0, + 0.8, + 0, + 1, + 1.289, + 0, + 1.778, + 0, + 2.267, + 0, + 1, + 2.411, + 0, + 2.556, + 7, + 2.7, + 7, + 1, + 2.811, + 7, + 2.922, + 3, + 3.033, + 3, + 1, + 3.356, + 3, + 3.678, + 3, + 4, + 3, + 1, + 4.256, + 3, + 4.511, + 0, + 4.767, + 0, + 1, + 5.178, + 0, + 5.589, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL02", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + 0, + 0.5, + 0, + 1, + 0.633, + 0, + 0.767, + 0, + 0.9, + 0, + 1, + 1.244, + 0, + 1.589, + 5, + 1.933, + 5, + 1, + 2.089, + 5, + 2.244, + 5, + 2.4, + 5, + 1, + 2.5, + 5, + 2.6, + -5, + 2.7, + -5, + 1, + 2.844, + -5, + 2.989, + -3, + 3.133, + -3, + 1, + 3.422, + -3, + 3.711, + -3, + 4, + -3, + 1, + 4.256, + -3, + 4.511, + 0, + 4.767, + 0, + 1, + 5.178, + 0, + 5.589, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL03", + "Segments": [ + 0, + 0, + 1, + 0.333, + 0, + 0.667, + 0, + 1, + 0, + 1, + 1.367, + 0, + 1.733, + -0.804, + 2.1, + -4, + 1, + 2.233, + -5.162, + 2.367, + -8, + 2.5, + -8, + 1, + 2.611, + -8, + 2.722, + 0, + 2.833, + 0, + 1, + 3, + 0, + 3.167, + -5, + 3.333, + -5, + 1, + 3.556, + -5, + 3.778, + -5, + 4, + -5, + 1, + 4.256, + -5, + 4.511, + 0, + 4.767, + 0, + 1, + 5.178, + 0, + 5.589, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmChange", + "FadeInTime": 0.0, + "FadeOutTime": 0.0, + "Segments": [ + 0, + 0, + 1, + 0.8, + 0, + 1.6, + 0, + 2.4, + 0, + 2, + 2.433, + 1, + 1, + 3.122, + 1, + 3.811, + 1, + 4.5, + 1, + 2, + 4.533, + 0, + 1, + 5.022, + 0, + 5.511, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBookPage", + "Segments": [ + 0, + 0, + 0, + 2.5, + 0, + 0, + 2.667, + 30, + 2, + 2.7, + 0, + 0, + 2.833, + 30, + 2, + 2.867, + 0, + 0, + 3.033, + 30, + 2, + 3.067, + 0, + 0, + 3.233, + 30, + 2, + 3.267, + 0, + 0, + 3.5, + 30, + 2, + 3.533, + 0, + 0, + 3.8, + 30, + 2, + 3.833, + 0, + 0, + 4.233, + 30, + 2, + 4.267, + 0, + 0, + 4.767, + 0, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameOn", + "Segments": [ + 0, + 0, + 1, + 2, + 0, + 4, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlame", + "Segments": [ + 0, + 0, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameShaking", + "Segments": [ + 0, + 0, + 1, + 2, + 0, + 4, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameX", + "Segments": [ + 0, + 0, + 1, + 2, + 0, + 4, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameY", + "Segments": [ + 0, + 0, + 1, + 2, + 0, + 4, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCharge01On", + "Segments": [ + 0, + 0, + 1, + 0.289, + 0, + 0.578, + 0, + 0.867, + 0, + 1, + 1.022, + 0, + 1.178, + 1, + 1.333, + 1, + 1, + 1.5, + 1, + 1.667, + 1, + 1.833, + 1, + 1, + 1.933, + 1, + 2.033, + 0, + 2.133, + 0, + 1, + 2.256, + 0, + 2.378, + 0, + 2.5, + 0, + 1, + 3.667, + 0, + 4.833, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCharge01", + "Segments": [ + 0, + 0, + 0, + 0.567, + 0, + 0, + 0.6, + 2.4, + 0, + 1.4, + 60, + 2, + 1.433, + 2.4, + 0, + 2.233, + 60, + 2, + 2.267, + 2.4, + 0, + 3.067, + 60, + 2, + 3.1, + 0, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightAOn", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 0, + 0.933, + 0, + 1, + 1.022, + 0, + 1.111, + 1, + 1.2, + 1, + 1, + 1.633, + 1, + 2.067, + 1, + 2.5, + 1, + 1, + 2.511, + 1, + 2.522, + 0, + 2.533, + 0, + 1, + 3.689, + 0, + 4.844, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightASize", + "Segments": [ + 0, + 0, + 1, + 0.3, + 0, + 0.6, + 0, + 0.9, + 0, + 1, + 1, + 0, + 1.1, + 1, + 1.2, + 1, + 1, + 1.244, + 1, + 1.289, + 0.9, + 1.333, + 0.9, + 1, + 1.378, + 0.9, + 1.422, + 1, + 1.467, + 1, + 1, + 1.511, + 1, + 1.556, + 0.9, + 1.6, + 0.9, + 1, + 1.644, + 0.9, + 1.689, + 1, + 1.733, + 1, + 1, + 1.778, + 1, + 1.822, + 0.9, + 1.867, + 0.9, + 1, + 1.911, + 0.9, + 1.956, + 1, + 2, + 1, + 1, + 2.044, + 1, + 2.089, + 0.9, + 2.133, + 0.9, + 1, + 2.178, + 0.9, + 2.222, + 1, + 2.267, + 1, + 1, + 2.311, + 1, + 2.356, + 0, + 2.4, + 0, + 1, + 2.444, + 0, + 2.489, + 0.4, + 2.533, + 0.4, + 1, + 2.544, + 0.4, + 2.556, + 0, + 2.567, + 0, + 1, + 3.711, + 0, + 4.856, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersA", + "Segments": [ + 0, + 0, + 1, + 0.833, + 0, + 1.667, + 0, + 2.5, + 0, + 1, + 2.533, + 0, + 2.567, + 4.032, + 2.6, + 10, + 1, + 2.678, + 23.925, + 2.756, + 30, + 2.833, + 30, + 2, + 2.867, + 0, + 1, + 3.911, + 0, + 4.956, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicAOn", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.8, + 0, + 0.933, + 15, + 1.067, + 15, + 1, + 1.456, + 15, + 1.844, + 15, + 2.233, + 15, + 1, + 2.3, + 15, + 2.367, + 13, + 2.433, + 13, + 1, + 2.5, + 13, + 2.567, + 30, + 2.633, + 30, + 2, + 2.667, + 0, + 1, + 3.778, + 0, + 4.889, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicARotation", + "Segments": [ + 0, + 0, + 0, + 0.5, + 0, + 0, + 1.433, + 60, + 2, + 1.467, + 2.143, + 0, + 2.4, + 60, + 2, + 2.433, + 2, + 0, + 3.067, + 60, + 2, + 3.1, + 3, + 0, + 3.733, + 60, + 2, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicALight", + "Segments": [ + 0, + 0, + 1, + 0.289, + 0, + 0.578, + 0, + 0.867, + 0, + 1, + 0.989, + 0, + 1.111, + 0.5, + 1.233, + 0.5, + 1, + 1.367, + 0.5, + 1.5, + 0.4, + 1.633, + 0.4, + 1, + 1.8, + 0.4, + 1.967, + 0.5, + 2.133, + 0.5, + 1, + 2.244, + 0.5, + 2.356, + 0, + 2.467, + 0, + 1, + 2.522, + 0, + 2.578, + 1, + 2.633, + 1, + 2, + 2.667, + 0, + 1, + 3.778, + 0, + 4.889, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectAX", + "Segments": [ + 0, + 0, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectAY", + "Segments": [ + 0, + 0, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightBOn", + "Segments": [ + 0, + 0, + 1, + 2, + 0, + 4, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightBSize", + "Segments": [ + 0, + 0, + 1, + 2, + 0, + 4, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBOn", + "Segments": [ + 0, + 0, + 1, + 2, + 0, + 4, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBRotation", + "Segments": [ + 0, + 0, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBMove", + "Segments": [ + 0, + 0, + 1, + 2, + 0, + 4, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBX", + "Segments": [ + 0, + 0, + 1, + 2, + 0, + 4, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersBOn", + "Segments": [ + 0, + 0, + 1, + 2, + 0, + 4, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersBSize", + "Segments": [ + 0, + 0, + 1, + 2, + 0, + 4, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersBThicknesses", + "Segments": [ + 0, + 0, + 1, + 2, + 0, + 4, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectBX", + "Segments": [ + 0, + 0, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectBY", + "Segments": [ + 0, + 0, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamSkirt", + "Segments": [ + 0, + 0, + 0, + 1, + 30, + 2, + 1.033, + 1, + 0, + 1.7, + 30, + 2, + 1.733, + 1, + 0, + 2.267, + 30, + 2, + 2.3, + 1.875, + 0, + 2.8, + 30, + 2, + 2.833, + 1.875, + 0, + 3.333, + 30, + 2, + 3.367, + 1.875, + 0, + 3.867, + 30, + 2, + 3.9, + 1.875, + 0, + 4.467, + 30, + 2, + 4.5, + 1.875, + 0, + 5.233, + 30, + 2, + 5.267, + 1.875, + 0, + 6, + 30 + ] + }, + { + "Target": "Parameter", + "Id": "ParamSkirtX", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + -0.7, + 0.567, + -0.7, + 1, + 0.856, + -0.7, + 1.144, + 0, + 1.433, + 0, + 1, + 1.7, + 0, + 1.967, + -0.6, + 2.233, + -0.6, + 1, + 2.311, + -0.6, + 2.389, + -0.637, + 2.467, + -0.5, + 1, + 2.567, + -0.324, + 2.667, + 1, + 2.767, + 1, + 1, + 2.844, + 1, + 2.922, + 0.4, + 3, + 0.4, + 1, + 3.111, + 0.4, + 3.222, + 1, + 3.333, + 1, + 1, + 3.444, + 1, + 3.556, + 0.8, + 3.667, + 0.8, + 1, + 3.767, + 0.8, + 3.867, + 1, + 3.967, + 1, + 1, + 4.156, + 1, + 4.344, + 0.2, + 4.533, + 0.2, + 1, + 4.778, + 0.2, + 5.022, + 0.4, + 5.267, + 0.4, + 1, + 5.511, + 0.4, + 5.756, + 0, + 6, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamSkirtY", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + -0.5, + 0.933, + -0.5, + 1, + 1.178, + -0.5, + 1.422, + 0.3, + 1.667, + 0.3, + 1, + 1.9, + 0.3, + 2.133, + -0.6, + 2.367, + -0.6, + 1, + 2.478, + -0.6, + 2.589, + 1, + 2.7, + 1, + 1, + 2.789, + 1, + 2.878, + -0.3, + 2.967, + -0.3, + 1, + 3.067, + -0.3, + 3.167, + 1, + 3.267, + 1, + 1, + 3.367, + 1, + 3.467, + -0.4, + 3.567, + -0.4, + 1, + 3.733, + -0.4, + 3.9, + 0.6, + 4.067, + 0.6, + 1, + 4.311, + 0.6, + 4.556, + -0.5, + 4.8, + -0.5, + 1, + 5.011, + -0.5, + 5.222, + 0.1, + 5.433, + 0.1, + 1, + 5.622, + 0.1, + 5.811, + 0, + 6, + 0 + ] + } + ] +} +``` + +`Content/Samples/Rice/motions/mtn_02.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 5, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 54, + "TotalSegmentCount": 164, + "TotalPointCount": 440, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.278, + 0, + 0.556, + 30, + 0.833, + 30, + 1, + 1.189, + 30, + 1.544, + 26, + 1.9, + 26, + 1, + 2.344, + 26, + 2.789, + 26, + 3.233, + 26, + 1, + 3.622, + 26, + 4.011, + 0, + 4.4, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.311, + 0, + 0.622, + 18, + 0.933, + 18, + 1, + 1.3, + 18, + 1.667, + 9, + 2.033, + 9, + 1, + 2.533, + 9, + 3.033, + 9, + 3.533, + 9, + 1, + 3.822, + 9, + 4.111, + -2, + 4.4, + -2, + 1, + 4.556, + -2, + 4.711, + 0, + 4.867, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 0, + 5, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.256, + 0, + 0.511, + 1, + 0.767, + 1, + 1, + 1.611, + 1, + 2.456, + 1, + 3.3, + 1, + 1, + 3.667, + 1, + 4.033, + 0, + 4.4, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.433, + 0, + 0.6, + -1, + 0.767, + -1, + 1, + 1.611, + -1, + 2.456, + -1, + 3.3, + -1, + 1, + 3.667, + -1, + 4.033, + 0, + 4.4, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.267, + 0, + 0.533, + -10, + 0.8, + -10, + 1, + 1.122, + -10, + 1.444, + -9, + 1.767, + -9, + 1, + 2.144, + -9, + 2.522, + -9, + 2.9, + -9, + 1, + 3.133, + -9, + 3.367, + -9.201, + 3.6, + -8, + 1, + 3.856, + -6.685, + 4.111, + 0, + 4.367, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.267, + 0, + 0.533, + 2, + 0.8, + 2, + 1, + 1.144, + 2, + 1.489, + 0, + 1.833, + 0, + 1, + 2.111, + 0, + 2.389, + 1, + 2.667, + 1, + 1, + 2.833, + 1, + 3, + 1, + 3.167, + 1, + 1, + 3.344, + 1, + 3.522, + 2, + 3.7, + 2, + 1, + 3.922, + 2, + 4.144, + 0, + 4.367, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.289, + 0, + 0.578, + 1, + 0.867, + 1, + 1, + 1.222, + 1, + 1.578, + -0.16, + 1.933, + -0.16, + 1, + 2.2, + -0.16, + 2.467, + 0, + 2.733, + 0, + 1, + 2.889, + 0, + 3.044, + 0, + 3.2, + 0, + 1, + 3.4, + 0, + 3.6, + 1, + 3.8, + 1, + 1, + 3.989, + 1, + 4.178, + 0, + 4.367, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulderR", + "Segments": [ + 0, + 0, + 1, + 0.078, + 0, + 0.156, + 0, + 0.233, + 0, + 1, + 0.478, + 0, + 0.722, + 5.2, + 0.967, + 5.2, + 1, + 1.211, + 5.2, + 1.456, + 0, + 1.7, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulderL", + "Segments": [ + 0, + 0, + 1, + 0.078, + 0, + 0.156, + 0, + 0.233, + 0, + 1, + 0.478, + 0, + 0.722, + 5.2, + 0.967, + 5.2, + 1, + 1.211, + 5.2, + 1.456, + 0, + 1.7, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLegKnee", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLegR", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLegRUpDw", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllZ", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR01", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + -9, + 0.567, + -9, + 1, + 0.867, + -9, + 1.167, + -7, + 1.467, + -7, + 1, + 2.033, + -7, + 2.6, + -7, + 3.167, + -7, + 1, + 3.511, + -7, + 3.856, + 0, + 4.2, + 0, + 1, + 4.4, + 0, + 4.6, + 0, + 4.8, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR01Y", + "Segments": [ + 0, + 0, + 1, + 1.067, + 0, + 2.133, + 0, + 3.2, + 0, + 1, + 3.656, + 0, + 4.111, + 0, + 4.567, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR02", + "Segments": [ + 0, + 0, + 1, + 0.067, + 0, + 0.133, + 0, + 0.2, + 0, + 1, + 0.367, + 0, + 0.533, + -27, + 0.7, + -27, + 1, + 1.011, + -27, + 1.322, + -22, + 1.633, + -22, + 1, + 1.9, + -22, + 2.167, + -23, + 2.433, + -23, + 1, + 2.589, + -23, + 2.744, + -23, + 2.9, + -23, + 1, + 3.033, + -23, + 3.167, + -24.01, + 3.3, + -24.01, + 1, + 3.489, + -24.01, + 3.678, + 1, + 3.867, + 1, + 1, + 4.1, + 1, + 4.333, + 0, + 4.567, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR02Y", + "Segments": [ + 0, + -10, + 1, + 0.033, + -10, + 0.067, + -10, + 0.1, + -10, + 1, + 0.233, + -10, + 0.367, + -18, + 0.5, + -18, + 1, + 1.478, + -18, + 2.456, + -18, + 3.433, + -18, + 1, + 3.611, + -18, + 3.789, + -10, + 3.967, + -10, + 0, + 5, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR03", + "Segments": [ + 0, + 0, + 1, + 0.267, + 0, + 0.533, + -10, + 0.8, + -10, + 1, + 1.156, + -10, + 1.511, + 8, + 1.867, + 8, + 1, + 2.311, + 8, + 2.756, + 8, + 3.2, + 8, + 1, + 3.656, + 8, + 4.111, + 0, + 4.567, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL01", + "Segments": [ + 0, + 0, + 1, + 0.211, + 0, + 0.422, + -6, + 0.633, + -6, + 1, + 1.533, + -6, + 2.433, + -6, + 3.333, + -6, + 1, + 3.678, + -6, + 4.022, + 0, + 4.367, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL02", + "Segments": [ + 0, + 0, + 1, + 0.233, + 0, + 0.467, + -2, + 0.7, + -2, + 1, + 1.622, + -2, + 2.544, + -2, + 3.467, + -2, + 1, + 3.8, + -2, + 4.133, + 0, + 4.467, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL03", + "Segments": [ + 0, + 0, + 1, + 0.233, + 0, + 0.467, + -11, + 0.7, + -11, + 1, + 1.589, + -11, + 2.478, + -11, + 3.367, + -11, + 1, + 3.7, + -11, + 4.033, + 0, + 4.367, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmChange", + "FadeInTime": 0.0, + "FadeOutTime": 0.0, + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBookPage", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameOn", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + 0, + 0.6, + 0, + 1, + 0.756, + 0, + 0.911, + 1, + 1.067, + 1, + 1, + 2, + 1, + 2.933, + 1, + 3.867, + 1, + 1, + 4.011, + 1, + 4.156, + 0, + 4.3, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlame", + "Segments": [ + 0, + 0, + 0, + 0.833, + 30, + 2, + 0.867, + 1.2, + 0, + 1.667, + 30, + 2, + 1.7, + 1.2, + 0, + 2.5, + 30, + 2, + 2.533, + 1.2, + 0, + 3.333, + 30, + 2, + 3.367, + 1.2, + 0, + 4.167, + 30, + 2, + 4.2, + 1.2, + 0, + 5, + 30 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameShaking", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameX", + "Segments": [ + 0, + 0, + 1, + 0.2, + 0, + 0.4, + -24, + 0.6, + -24, + 1, + 1.789, + -24, + 2.978, + 0, + 4.167, + 0, + 1, + 4.444, + 0, + 4.722, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameY", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + -10, + 0.267, + -10, + 1, + 0.489, + -10, + 0.711, + 4, + 0.933, + 4, + 1, + 1.111, + 4, + 1.289, + -10, + 1.467, + -10, + 1, + 1.689, + -10, + 1.911, + 4, + 2.133, + 4, + 1, + 2.3, + 4, + 2.467, + -10, + 2.633, + -10, + 1, + 2.867, + -10, + 3.1, + 4, + 3.333, + 4, + 1, + 3.611, + 4, + 3.889, + 0, + 4.167, + 0, + 1, + 4.444, + 0, + 4.722, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCharge01On", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCharge01", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightAOn", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightASize", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersA", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicAOn", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicARotation", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicALight", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectAX", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectAY", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightBOn", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightBSize", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBOn", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBRotation", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBMove", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBX", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersBOn", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersBSize", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersBThicknesses", + "Segments": [ + 0, + 0, + 1, + 1.667, + 0, + 3.333, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectBX", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectBY", + "Segments": [ + 0, + 0, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamSkirt", + "Segments": [ + 0, + 0, + 0, + 1, + 30, + 2, + 1.033, + 1, + 0, + 2, + 30, + 2, + 2.033, + 1, + 0, + 3, + 30, + 2, + 3.033, + 1, + 0, + 4, + 30, + 2, + 4.033, + 1, + 0, + 5, + 30 + ] + }, + { + "Target": "Parameter", + "Id": "ParamSkirtX", + "Segments": [ + 0, + 0, + 1, + 0.167, + 0, + 0.333, + -0.5, + 0.5, + -0.5, + 1, + 0.778, + -0.5, + 1.056, + 0.2, + 1.333, + 0.2, + 1, + 1.633, + 0.2, + 1.933, + -0.4, + 2.233, + -0.4, + 1, + 2.567, + -0.4, + 2.9, + 0.4, + 3.233, + 0.4, + 1, + 3.822, + 0.4, + 4.411, + 0, + 5, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamSkirtY", + "Segments": [ + 0, + 0, + 1, + 0.289, + 0, + 0.578, + 0.4, + 0.867, + 0.4, + 1, + 1.189, + 0.4, + 1.511, + -0.7, + 1.833, + -0.7, + 1, + 2.222, + -0.7, + 2.611, + -0.214, + 3, + 0, + 1, + 3.378, + 0.208, + 3.756, + 0.2, + 4.133, + 0.2, + 1, + 4.422, + 0.2, + 4.711, + 0, + 5, + 0 + ] + } + ] +} +``` + +`Content/Samples/Rice/motions/mtn_03.motion3.json`: + +```json +{ + "Version": 3, + "Meta": { + "Duration": 8, + "Fps": 30.0, + "Loop": true, + "AreBeziersRestricted": true, + "CurveCount": 54, + "TotalSegmentCount": 303, + "TotalPointCount": 793, + "UserDataCount": 0, + "TotalUserDataSize": 0 + }, + "Curves": [ + { + "Target": "Parameter", + "Id": "ParamAngleX", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + 30, + 0.3, + 30, + 1, + 0.433, + 30, + 0.567, + -30, + 0.7, + -30, + 1, + 0.978, + -30, + 1.256, + -30, + 1.533, + -30, + 1, + 2.022, + -30, + 2.511, + 30, + 3, + 30, + 1, + 3.8, + 30, + 4.6, + 30, + 5.4, + 30, + 1, + 5.667, + 30, + 5.933, + 0, + 6.2, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.189, + 0, + 0.378, + 3.748, + 0.567, + 10, + 1, + 0.678, + 13.678, + 0.789, + 15, + 0.9, + 15, + 1, + 1.033, + 15, + 1.167, + 10, + 1.3, + 10, + 1, + 1.411, + 10, + 1.522, + 10, + 1.633, + 10, + 1, + 2.1, + 10, + 2.567, + 0, + 3.033, + 0, + 1, + 3.167, + 0, + 3.3, + 0, + 3.433, + 0, + 1, + 3.489, + 0, + 3.544, + -13, + 3.6, + -13, + 1, + 3.744, + -13, + 3.889, + 30, + 4.033, + 30, + 1, + 4.189, + 30, + 4.344, + 12, + 4.5, + 12, + 1, + 4.8, + 12, + 5.1, + 12, + 5.4, + 12, + 1, + 5.522, + 12, + 5.644, + 12, + 5.767, + 12, + 1, + 5.911, + 12, + 6.056, + 0, + 6.2, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeLOpen", + "Segments": [ + 0, + 1, + 1, + 2.033, + 1, + 4.067, + 1, + 6.1, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeROpen", + "Segments": [ + 0, + 1, + 1, + 2.033, + 1, + 4.067, + 1, + 6.1, + 1, + 0, + 8, + 1 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallX", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + -0.6, + 0.3, + -0.6, + 1, + 0.433, + -0.6, + 0.567, + 0, + 0.7, + 0, + 1, + 1.644, + 0, + 2.589, + 0, + 3.533, + 0, + 1, + 3.611, + 0, + 3.689, + -0.2, + 3.767, + -0.2, + 1, + 4.011, + -0.2, + 4.256, + 0, + 4.5, + 0, + 1, + 5.033, + 0, + 5.567, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEyeBallY", + "Segments": [ + 0, + 0, + 1, + 1.178, + 0, + 2.356, + 0, + 3.533, + 0, + 1, + 3.611, + 0, + 3.689, + -0.8, + 3.767, + -0.8, + 1, + 3.856, + -0.8, + 3.944, + -0.8, + 4.033, + -0.8, + 1, + 4.189, + -0.8, + 4.344, + 0, + 4.5, + 0, + 1, + 5.033, + 0, + 5.567, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleX", + "Segments": [ + 0, + 0, + 1, + 0.056, + 0, + 0.111, + 6.925, + 0.167, + 9, + 1, + 0.2, + 10.245, + 0.233, + 10, + 0.267, + 10, + 1, + 0.411, + 10, + 0.556, + -10, + 0.7, + -10, + 1, + 0.989, + -10, + 1.278, + -10, + 1.567, + -10, + 1, + 2.044, + -10, + 2.522, + 0, + 3, + 0, + 1, + 3.189, + 0, + 3.378, + 0, + 3.567, + 0, + 1, + 3.611, + 0, + 3.656, + 10, + 3.7, + 10, + 1, + 3.922, + 10, + 4.144, + 8, + 4.367, + 8, + 1, + 4.689, + 8, + 5.011, + 8, + 5.333, + 8, + 1, + 5.589, + 8, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleY", + "Segments": [ + 0, + 0, + 1, + 0.522, + 0, + 1.044, + 0, + 1.567, + 0, + 1, + 2.822, + 0, + 4.078, + 0, + 5.333, + 0, + 1, + 5.589, + 0, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBodyAngleZ", + "Segments": [ + 0, + 0, + 1, + 0.1, + 0, + 0.2, + -3, + 0.3, + -3, + 1, + 0.444, + -3, + 0.589, + 1, + 0.733, + 1, + 1, + 0.933, + 1, + 1.133, + 0, + 1.333, + 0, + 1, + 1.422, + 0, + 1.511, + 0, + 1.6, + 0, + 1, + 2.067, + 0, + 2.533, + -3, + 3, + -3, + 1, + 3.122, + -3, + 3.244, + -3, + 3.367, + -3, + 1, + 3.433, + -3, + 3.5, + -5, + 3.567, + -5, + 1, + 3.6, + -5, + 3.633, + 4.75, + 3.667, + 6, + 1, + 3.756, + 9.333, + 3.844, + 10, + 3.933, + 10, + 1, + 4.089, + 10, + 4.244, + 1, + 4.4, + 1, + 1, + 4.522, + 1, + 4.644, + 2, + 4.767, + 2, + 1, + 4.956, + 2, + 5.144, + 2, + 5.333, + 2, + 1, + 5.589, + 2, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulderR", + "Segments": [ + 0, + 0, + 1, + 1.189, + 0, + 2.378, + 0, + 3.567, + 0, + 1, + 3.611, + 0, + 3.656, + 7.767, + 3.7, + 8.4, + 1, + 3.8, + 9.824, + 3.9, + 10, + 4, + 10, + 1, + 4.133, + 10, + 4.267, + 0, + 4.4, + 0, + 1, + 4.711, + 0, + 5.022, + 0, + 5.333, + 0, + 1, + 5.589, + 0, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamShoulderL", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 1.956, + 0, + 3.644, + 0, + 5.333, + 0, + 1, + 5.589, + 0, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLegKnee", + "Segments": [ + 0, + 0, + 1, + 0.522, + 0, + 1.044, + 0, + 1.567, + 0, + 1, + 2.167, + 0, + 2.767, + 0, + 3.367, + 0, + 1, + 3.433, + 0, + 3.5, + -2, + 3.567, + -2, + 1, + 3.622, + -2, + 3.678, + 5.887, + 3.733, + 7, + 1, + 3.789, + 8.113, + 3.844, + 8, + 3.9, + 8, + 1, + 4.044, + 8, + 4.189, + 0, + 4.333, + 0, + 1, + 4.667, + 0, + 5, + 0, + 5.333, + 0, + 1, + 5.589, + 0, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLegR", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + 0, + 0.267, + 0, + 1, + 0.422, + 0, + 0.578, + -5, + 0.733, + -5, + 1, + 1.678, + -5, + 2.622, + -5, + 3.567, + -5, + 1, + 3.689, + -5, + 3.811, + 0, + 3.933, + 0, + 1, + 4.1, + 0, + 4.267, + -4, + 4.433, + -4, + 1, + 4.733, + -4, + 5.033, + -4, + 5.333, + -4, + 1, + 5.589, + -4, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamLegRUpDw", + "Segments": [ + 0, + 0, + 1, + 1.189, + 0, + 2.378, + 0, + 3.567, + 0, + 1, + 3.678, + 0, + 3.789, + 10, + 3.9, + 10, + 1, + 4.078, + 10, + 4.256, + 0, + 4.433, + 0, + 1, + 4.733, + 0, + 5.033, + 0, + 5.333, + 0, + 1, + 5.589, + 0, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamAllZ", + "Segments": [ + 0, + 0, + 1, + 1.189, + 0, + 2.378, + 0, + 3.567, + 0, + 1, + 3.633, + 0, + 3.7, + 19.528, + 3.767, + 21, + 1, + 3.822, + 22.226, + 3.878, + 22, + 3.933, + 22, + 1, + 4.067, + 22, + 4.2, + -2, + 4.333, + -2, + 1, + 4.922, + -2, + 5.511, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR01", + "Segments": [ + 0, + 0, + 1, + 0.044, + 0, + 0.089, + -18, + 0.133, + -18, + 1, + 0.2, + -20.878, + 0.267, + -21, + 0.333, + -21, + 1, + 0.433, + -21, + 0.533, + 29, + 0.633, + 29, + 1, + 0.956, + 29, + 1.278, + 29, + 1.6, + 29, + 1, + 2.056, + 29, + 2.511, + 30, + 2.967, + 30, + 1, + 3.089, + 30, + 3.211, + 30, + 3.333, + 30, + 1, + 3.389, + 30, + 3.444, + 30, + 3.5, + 30, + 1, + 3.533, + 30, + 3.567, + 37.934, + 3.6, + 39, + 1, + 3.744, + 43.62, + 3.889, + 45, + 4.033, + 45, + 1, + 4.156, + 45, + 4.278, + 29, + 4.4, + 29, + 1, + 4.533, + 29, + 4.667, + 30, + 4.8, + 30, + 1, + 4.978, + 30, + 5.156, + 30, + 5.333, + 30, + 1, + 5.589, + 30, + 5.844, + -0.525, + 6.1, + -0.525, + 1, + 6.267, + -0.525, + 6.433, + 0, + 6.6, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR01Y", + "Segments": [ + 0, + 0, + 1, + 0.111, + 0, + 0.222, + -7.2, + 0.333, + -7.2, + 1, + 0.456, + -7.2, + 0.578, + 0, + 0.7, + 0, + 1, + 1, + 0, + 1.3, + 0, + 1.6, + 0, + 1, + 2.056, + 0, + 2.511, + -10, + 2.967, + -10, + 1, + 3.144, + -10, + 3.322, + -10, + 3.5, + -10, + 1, + 4.111, + -10, + 4.722, + -10, + 5.333, + -10, + 1, + 5.589, + -10, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR02", + "Segments": [ + 0, + 0, + 1, + 0.089, + 0, + 0.178, + -41, + 0.267, + -41, + 1, + 0.367, + -41, + 0.467, + -10.206, + 0.567, + -3, + 1, + 0.622, + 1.003, + 0.678, + 0, + 0.733, + 0, + 1, + 0.822, + 0, + 0.911, + 0, + 1, + 0, + 1, + 1.2, + 0, + 1.4, + 0, + 1.6, + 0, + 1, + 2.067, + 0, + 2.533, + 3, + 3, + 3, + 1, + 3.178, + 3, + 3.356, + 3, + 3.533, + 3, + 1, + 3.567, + 3, + 3.6, + 3, + 3.633, + 3, + 1, + 3.711, + 3, + 3.789, + 6, + 3.867, + 6, + 1, + 4.067, + 6, + 4.267, + 0, + 4.467, + 0, + 1, + 4.756, + 0, + 5.044, + 0, + 5.333, + 0, + 1, + 5.589, + 0, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR02Y", + "Segments": [ + 0, + -10, + 1, + 0.033, + -10, + 0.067, + -20, + 0.1, + -20, + 1, + 0.189, + -20, + 0.278, + -20, + 0.367, + -20, + 1, + 0.411, + -20, + 0.456, + 10, + 0.5, + 10, + 1, + 0.544, + 10, + 0.589, + -9, + 0.633, + -9, + 1, + 0.956, + -9, + 1.278, + -9, + 1.6, + -9, + 1, + 2.033, + -9, + 2.467, + 8, + 2.9, + 8, + 1, + 3.711, + 8, + 4.522, + 8, + 5.333, + 8, + 1, + 5.589, + 8, + 5.844, + -10, + 6.1, + -10, + 0, + 8, + -10 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmR03", + "Segments": [ + 0, + 0, + 1, + 0.133, + 0, + 0.267, + -21, + 0.4, + -21, + 1, + 0.522, + -21, + 0.644, + 4, + 0.767, + 4, + 1, + 0.856, + 4, + 0.944, + 0, + 1.033, + 0, + 1, + 1.833, + 0, + 2.633, + 0, + 3.433, + 0, + 1, + 3.478, + 0, + 3.522, + -21, + 3.567, + -21, + 1, + 3.611, + -21, + 3.656, + 16, + 3.7, + 16, + 1, + 3.856, + 16, + 4.011, + 16, + 4.167, + 16, + 1, + 4.311, + 16, + 4.456, + -2, + 4.6, + -2, + 1, + 4.711, + -2, + 4.822, + 0, + 4.933, + 0, + 1, + 5.067, + 0, + 5.2, + 0, + 5.333, + 0, + 1, + 5.589, + 0, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL01", + "Segments": [ + 0, + 0, + 1, + 1.778, + 0, + 3.556, + 0, + 5.333, + 0, + 1, + 5.589, + 0, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL02", + "Segments": [ + 0, + 0, + 1, + 1.189, + 0, + 2.378, + -8, + 3.567, + -8, + 1, + 3.689, + -8, + 3.811, + 11, + 3.933, + 11, + 1, + 4.056, + 11, + 4.178, + -3, + 4.3, + -3, + 1, + 4.444, + -3, + 4.589, + 0, + 4.733, + 0, + 1, + 4.933, + 0, + 5.133, + 0, + 5.333, + 0, + 1, + 5.589, + 0, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmL03", + "Segments": [ + 0, + 0, + 1, + 1.778, + 0, + 3.556, + 0, + 5.333, + 0, + 1, + 5.589, + 0, + 5.844, + 0, + 6.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamArmChange", + "FadeInTime": 0.0, + "FadeOutTime": 0.0, + "Segments": [ + 0, + 0, + 1, + 0.178, + 0, + 0.356, + 0, + 0.533, + 0, + 2, + 0.567, + 1, + 1, + 2.289, + 1, + 4.011, + 1, + 5.733, + 1, + 2, + 5.767, + 0, + 2, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamBookPage", + "Segments": [ + 0, + 0, + 0, + 3.633, + 0, + 0, + 3.833, + 30, + 2, + 3.867, + 0, + 0, + 4.067, + 30, + 2, + 4.1, + 0, + 0, + 4.333, + 30, + 2, + 4.367, + 0, + 0, + 4.733, + 30, + 2, + 4.767, + 0, + 0, + 5.367, + 30, + 2, + 5.4, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameOn", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlame", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameShaking", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameX", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamFlameY", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCharge01On", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamCharge01", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightAOn", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightASize", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersA", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicAOn", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicARotation", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicALight", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectAX", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectAY", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightBOn", + "Segments": [ + 0, + 0, + 1, + 0.233, + 0, + 0.467, + 0, + 0.7, + 0, + 1, + 0.767, + 0, + 0.833, + 1, + 0.9, + 1, + 2, + 3.533, + 1, + 2, + 3.567, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamHandLightBSize", + "Segments": [ + 0, + 0, + 1, + 0.222, + 0, + 0.444, + 0, + 0.667, + 0, + 1, + 0.822, + 0, + 0.978, + 0.6, + 1.133, + 0.6, + 1, + 1.222, + 0.6, + 1.311, + 0.4, + 1.4, + 0.4, + 1, + 1.467, + 0.4, + 1.533, + 0.6, + 1.6, + 0.6, + 1, + 1.667, + 0.6, + 1.733, + 0.4, + 1.8, + 0.4, + 1, + 1.867, + 0.4, + 1.933, + 0.6, + 2, + 0.6, + 1, + 2.067, + 0.6, + 2.133, + 0.4, + 2.2, + 0.4, + 1, + 2.267, + 0.4, + 2.333, + 0.6, + 2.4, + 0.6, + 1, + 2.467, + 0.6, + 2.533, + 0.4, + 2.6, + 0.4, + 1, + 2.667, + 0.4, + 2.733, + 0.6, + 2.8, + 0.6, + 1, + 2.911, + 0.6, + 3.022, + 0.4, + 3.133, + 0.4, + 1, + 3.222, + 0.4, + 3.311, + 1, + 3.4, + 1, + 1, + 3.467, + 1, + 3.533, + 0, + 3.6, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBOn", + "Segments": [ + 0, + 0, + 0, + 0.633, + 0, + 0, + 1.367, + 6, + 1, + 2.1, + 6, + 2.833, + 6, + 3.567, + 6, + 1, + 3.667, + 6, + 3.767, + 0, + 3.867, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBRotation", + "Segments": [ + 0, + 0, + 0, + 8, + 360 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBMove", + "Segments": [ + 0, + 0, + 1, + 0.256, + 0, + 0.511, + 0, + 0.767, + 0, + 1, + 1.022, + 0, + 1.278, + 1, + 1.533, + 1, + 1, + 2.211, + 1, + 2.889, + 1, + 3.567, + 1, + 1, + 3.711, + 1, + 3.856, + 0, + 4, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicBX", + "Segments": [ + 0, + -10, + 1, + 0.533, + -10, + 1.067, + -10, + 1.6, + -10, + 1, + 2.056, + -10, + 2.511, + 8, + 2.967, + 8, + 0, + 8, + 8 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersBOn", + "Segments": [ + 0, + 0, + 1, + 1.178, + 0, + 2.356, + 0, + 3.533, + 0, + 1, + 3.544, + 0, + 3.556, + 1, + 3.567, + 1, + 1, + 3.744, + 1, + 3.922, + 1, + 4.1, + 1, + 1, + 4.133, + 1, + 4.167, + 0, + 4.2, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersBSize", + "Segments": [ + 0, + 0, + 1, + 1.178, + 0, + 2.356, + 0, + 3.533, + 0, + 1, + 3.556, + 0, + 3.578, + 0.553, + 3.6, + 0.6, + 1, + 3.744, + 0.903, + 3.889, + 1, + 4.033, + 1, + 1, + 4.089, + 1, + 4.144, + 0, + 4.2, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamMagicPowersBThicknesses", + "Segments": [ + 0, + 0, + 1, + 1.167, + 0, + 2.333, + 0, + 3.5, + 0, + 1, + 3.522, + 0, + 3.544, + 10, + 3.567, + 10, + 1, + 3.589, + 10, + 3.611, + 2.2, + 3.633, + 2.2, + 1, + 3.656, + 2.2, + 3.678, + 7.987, + 3.7, + 8.5, + 1, + 3.756, + 9.783, + 3.811, + 10, + 3.867, + 10, + 1, + 3.944, + 10, + 4.022, + 0, + 4.1, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectBX", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamEffectBY", + "Segments": [ + 0, + 0, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamSkirt", + "Segments": [ + 0, + 0, + 0, + 1, + 30, + 2, + 1.033, + 1, + 0, + 2.033, + 30, + 2, + 2.067, + 1, + 0, + 2.967, + 30, + 2, + 3, + 1, + 0, + 3.833, + 30, + 2, + 3.867, + 1, + 0, + 4.533, + 30, + 2, + 4.567, + 1.875, + 0, + 5.067, + 30, + 2, + 5.1, + 1.875, + 0, + 5.767, + 30, + 2, + 5.8, + 1.875, + 0, + 6.833, + 30, + 2, + 6.867, + 1, + 0, + 8, + 30 + ] + }, + { + "Target": "Parameter", + "Id": "ParamSkirtX", + "Segments": [ + 0, + 0, + 1, + 0.156, + 0, + 0.311, + 1, + 0.467, + 1, + 1, + 0.667, + 1, + 0.867, + -1, + 1.067, + -1, + 1, + 1.311, + -1, + 1.556, + 0.6, + 1.8, + 0.6, + 1, + 2.056, + 0.6, + 2.311, + -0.5, + 2.567, + -0.5, + 1, + 2.756, + -0.5, + 2.944, + 0.4, + 3.133, + 0.4, + 1, + 3.267, + 0.4, + 3.4, + -0.4, + 3.533, + -0.4, + 1, + 3.678, + -0.4, + 3.822, + 1, + 3.967, + 1, + 1, + 4.089, + 1, + 4.211, + 0, + 4.333, + 0, + 1, + 4.511, + 0, + 4.689, + 1, + 4.867, + 1, + 1, + 5, + 1, + 5.133, + 0.1, + 5.267, + 0.1, + 1, + 5.433, + 0.1, + 5.6, + 1, + 5.767, + 1, + 1, + 5.989, + 1, + 6.211, + -0.6, + 6.433, + -0.6, + 1, + 6.678, + -0.6, + 6.922, + 0.6, + 7.167, + 0.6, + 1, + 7.444, + 0.6, + 7.722, + 0, + 8, + 0 + ] + }, + { + "Target": "Parameter", + "Id": "ParamSkirtY", + "Segments": [ + 0, + 0, + 1, + 0.289, + 0, + 0.578, + 0.4, + 0.867, + 0.4, + 1, + 1.189, + 0.4, + 1.511, + -0.7, + 1.833, + -0.7, + 1, + 2.144, + -0.7, + 2.456, + 0.4, + 2.767, + 0.4, + 1, + 2.978, + 0.4, + 3.189, + -0.5, + 3.4, + -0.5, + 1, + 3.522, + -0.5, + 3.644, + 1, + 3.767, + 1, + 1, + 3.867, + 1, + 3.967, + -1, + 4.067, + -1, + 1, + 4.2, + -1, + 4.333, + 0.7, + 4.467, + 0.7, + 1, + 4.611, + 0.7, + 4.756, + -0.6, + 4.9, + -0.6, + 1, + 5.122, + -0.6, + 5.344, + 0.2, + 5.567, + 0.2, + 1, + 5.756, + 0.2, + 5.944, + -0.6, + 6.133, + -0.6, + 1, + 6.411, + -0.6, + 6.689, + 0.1, + 6.967, + 0.1, + 1, + 7.311, + 0.1, + 7.656, + 0, + 8, + 0 + ] + } + ] +} +``` + +`LICENSE`: + +``` +MIT License + +Copyright (c) 2020 naru + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +`ReadMe.md`: + +```md +This project is archived. As now we can use Live2D in Unreal with [Official plugin](https://github.com/Live2D/CubismUnrealEngineComponents). + +--- + +# UnRealLive2D + +Unreal Engine plugin for Live2D model. + +![Screen Shot](Docs/Image/ScreenShot.GIF?raw=true) + +The model logic part is based on [CubismNativeFramework](https://github.com/Live2D/CubismNativeFramework/). + +The render part take advantage of the [global shader plugin system](https://docs.unrealengine.com/en-US/Programming/Rendering/ShaderInPlugin/QuickStart/index.html). + +## Usage + +1. Put this plugin in your project's Plugin folder +2. Download [Cubism Native SDK](https://www.live2d.com/en/download/cubism-sdk/download-native/) and copy `lib` and `include` to [CubismLibrary](https://github.com/Arisego/Live2DTest/tree/master/Plugins/UnrealLive2D/Source/ThirdParty/CubismLibrary) folder + +> A sample of the plugin usage can be found [here](https://github.com/Arisego/Live2DTest). + +You must donwload the corresponding version of [Cubism Native SDK](https://www.live2d.com/en/download/cubism-sdk/download-native/), or someting may not work as expected. + +After plugin is compiled and installed, you can find *CubismMap* in plugin content folder. That's the sample map showing how model is rendered to RenderTarget and then displayed in UMG and World. + +If you want to package game with the models in plugin folder, remeber to add `../Plugins/UnrealLive2D/Content/Samples` to your package copy path. + +## Notice + +This plugin was tested on __Win64__ envrioment only, it was supposed to work on all the platform that Cubism Native SDK supported, but some additional work might be needed. You can start with configuring `UnrealLive2D.Build.cs`. + +Render result is only tested with *Hiyori* and *Mark*. + +Some of the logic is not linked to blueprint and the control of the model logic is a bit dirty, this plugin is intend to be a good start for project that want to use Live2D model in Unreal Engine. + +There will be **NO** Edtior part for this plugin. + +``` + +`Shaders/Private/CubismMask.usf`: + +```usf +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +/*============================================================================= + LensDistortionUVGeneration.usf: Generate lens distortion and undistortion + UV displacement map into a render target. + + The pixel shader directly compute the distort viewport UV to undistort + viewport UV displacement using Sv_Position and the reference equations and + store them into the red and green channels. + + However to avoid resolving with a ferrari method, or doing a newton method + on the GPU to compute the undistort viewport UV to distort viewport UV + displacement, this couple of shaders works as follow: The vertex shader + undistort the grid's vertices, and pass down to the pixel shader the viewport + UV of where they should have been on screen without undistortion. The pixel + shader can then generate the undistort viewport UV to distort viewport UV + displacement by just subtracting the pixel's viewport UV. +=============================================================================*/ + +#include "/Engine/Public/Platform.ush" + +float4x4 ProjectMatrix; +float4x4 ClipMatrix; +float4 BaseColor; +float4 ChannelFlag; + +float TestFloat; + + +void MainVS( + in uint GlobalVertexId : SV_VertexID, + float2 InPosition : ATTRIBUTE0, + float2 InUV : ATTRIBUTE1, + out float2 OutUV : TEXCOORD0, + out float4 OutPosition : SV_POSITION, + out float4 OutClipPosition : TEXCOORD1 + ) +{ + OutPosition = mul(float4(InPosition, 0.0f, 1.0f), ProjectMatrix); + OutClipPosition = mul(float4(InPosition, 0.0f, 1.0f), ProjectMatrix); + OutUV.x = InUV.x; + OutUV.y = 1.0 - +InUV.y; +} + +Texture2D MainTexture; +SamplerState MainTextureSampler; + +void MainPS( + in noperspective float2 InUv : TEXCOORD0, + in float4 SvPosition : SV_POSITION, + in float4 ClipPosition : TEXCOORD1, + out float4 OutColor : SV_Target0 + ) +{ + float isInside = + step(BaseColor.x, ClipPosition.x / ClipPosition.w) + * step(BaseColor.y, ClipPosition.y / ClipPosition.w) + * step(ClipPosition.x / ClipPosition.w, BaseColor.z) + * step(ClipPosition.y / ClipPosition.w, BaseColor.w); + + //OutColor = ChannelFlag * MainTexture.Sample(MainTextureSampler, InUv).a * isInside; + OutColor = ChannelFlag * MainTexture.Sample(MainTextureSampler, InUv).a * isInside; + //OutColor = float4(TestFloat,0,0,1) * (1 - MainTexture.Sample(MainTextureSampler, InUv).a); + //OutColor = float4(TestFloat,0,0,1); +} +``` + +`Shaders/Private/CubismSep.usf`: + +```usf +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +/*============================================================================= + LensDistortionUVGeneration.usf: Generate lens distortion and undistortion + UV displacement map into a render target. + + The pixel shader directly compute the distort viewport UV to undistort + viewport UV displacement using Sv_Position and the reference equations and + store them into the red and green channels. + + However to avoid resolving with a ferrari method, or doing a newton method + on the GPU to compute the undistort viewport UV to distort viewport UV + displacement, this couple of shaders works as follow: The vertex shader + undistort the grid's vertices, and pass down to the pixel shader the viewport + UV of where they should have been on screen without undistortion. The pixel + shader can then generate the undistort viewport UV to distort viewport UV + displacement by just subtracting the pixel's viewport UV. +=============================================================================*/ + +#include "/Engine/Public/Platform.ush" + +float4 BaseColor; + +void MainVS( + in uint GlobalVertexId : SV_VertexID, + float2 InPosition : ATTRIBUTE0, + float2 InUV : ATTRIBUTE1, + out float2 OutUV : TEXCOORD0, + out float4 OutPosition : SV_POSITION + ) +{ + OutPosition = float4(InPosition,0,1) ; + + OutUV.x = InUV.x; + OutUV.y = 1.0 - +InUV.y; +} + +Texture2D InTexture; +SamplerState InTextureSampler; + + +void MainPS( + in noperspective float2 InUv : TEXCOORD0, + in float4 SvPosition : SV_POSITION, + out float4 OutColor : SV_Target0 + ) +{ + float4 color = InTexture.Sample(InTextureSampler, InUv) * BaseColor;; + color.xyz *= color.w; + OutColor = color; +} +``` + +`Shaders/Private/CubismSepMask.usf`: + +```usf +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +/*============================================================================= + LensDistortionUVGeneration.usf: Generate lens distortion and undistortion + UV displacement map into a render target. + + The pixel shader directly compute the distort viewport UV to undistort + viewport UV displacement using Sv_Position and the reference equations and + store them into the red and green channels. + + However to avoid resolving with a ferrari method, or doing a newton method + on the GPU to compute the undistort viewport UV to distort viewport UV + displacement, this couple of shaders works as follow: The vertex shader + undistort the grid's vertices, and pass down to the pixel shader the viewport + UV of where they should have been on screen without undistortion. The pixel + shader can then generate the undistort viewport UV to distort viewport UV + displacement by just subtracting the pixel's viewport UV. +=============================================================================*/ + +#include "/Engine/Public/Platform.ush" + +float4x4 ProjectMatrix; +float4x4 ClipMatrix; +float4 BaseColor; +float4 ChannelFlag; + +float TestFloat; + +void MainVS( + in uint GlobalVertexId : SV_VertexID, + float2 InPosition : ATTRIBUTE0, + float2 InUV : ATTRIBUTE1, + out float2 OutUV : TEXCOORD0, + out float4 OutPosition : SV_POSITION, + out float4 ClipPosition : TEXCOORD1 + ) +{ + OutPosition = float4(InPosition,0,1); + ClipPosition = mul(float4(InPosition,0,1), ClipMatrix); + + OutUV.x = InUV.x; + OutUV.y = 1.0 - +InUV.y; +} + +Texture2D MainTexture; +SamplerState MainSampler; +Texture2D MaskTexture; +SamplerState MaskSampler; + + +void MainPS( + in noperspective float2 InUv : TEXCOORD0, + in float4 SvPosition : SV_POSITION, + in float4 ClipPosition : TEXCOORD1, + out float4 OutColor : SV_Target0 + ) +{ + float4 color = MainTexture.Sample(MainSampler, InUv) * BaseColor; + //float4 color = MainTexture.Sample(MainSampler, InUv); + color.xyz *= color.w; + + float2 MaskUv = float2(ClipPosition.x, 1+ClipPosition.y); + MaskUv /= ClipPosition.w; + + float4 clipMask = (1.0 - MaskTexture.Sample(MaskSampler, MaskUv)) * ChannelFlag; + float maskVal = clipMask.r + clipMask.g + clipMask.b + clipMask.a; + color = color * maskVal; + //float4 color = float4(maskVal, 0, 1-maskVal, TestFloat); + + OutColor = color; +} + +void MainPsInvert( + in noperspective float2 InUv : TEXCOORD0, + in float4 SvPosition : SV_POSITION, + in float4 ClipPosition : TEXCOORD1, + out float4 OutColor : SV_Target0 + ) +{ + float4 color = MainTexture.Sample(MainSampler, InUv) * BaseColor; + //float4 color = MainTexture.Sample(MainSampler, InUv); + color.xyz *= color.w; + + float2 MaskUv = float2(ClipPosition.x, 1+ClipPosition.y); + MaskUv /= ClipPosition.w; + + float4 clipMask = (1.0 - MaskTexture.Sample(MaskSampler, MaskUv)) * ChannelFlag; + float maskVal = clipMask.r + clipMask.g + clipMask.b + clipMask.a; + color = color * (1.0-maskVal); + //float4 color = float4(maskVal, 0, 1-maskVal, TestFloat); + + OutColor = color; +} + +``` + +`Source/ThirdParty/CubismLibrary/CHANGELOG.md`: + +```md +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + + +## 2024-03-26 + +### Remove + +* [Unity] Remove built with Emscripten 1.38.48. + * Unity 2021.2 or later uses only Core under `Assets/Live2D/Cubism/Plugins/Experimental/Emscripten/latest`. + + +## 2023-09-28 + +### Remove + +* Remove bitcode from IOS build. + + +## 2023-08-17 + +### Added + +* Enhance Blend Shape features. + * Please see [here](https://docs.live2d.com/en/cubism-editor-manual/blend-shape/). + +### Changed + +* Upgrade Core version to 05.00.0000. + + +## 2023-05-09 + +### Changed + +* Change the GCC version of the library for Linux from 6.5.0 to 8.3.0. + + +## 2023-03-16 + +### Fixed + +* Fix a case in which the index of the mask's drawable object was negative value for `csmGetDrawableMasks()`. +* Fix a problem in which `csmHasMocConsistency()` was returned as 0 even though the MOC3 file was in the correct format. + * This problem was occurring in some models using the blendshape weight limit settings. +* Fix a problem that could cause a crash if a MOC3 file that is not in the correct format is loaded with `csmHasMocConsistency()`. + +### Changed + +* Upgrade Core version to 04.02.0004. + + +## 2023-03-10 + +### Added + +* Add the function `csmHasMocConsistency`. + * This function verifies that the `MOC3` file is valid. + +### Changed + +* Upgrade Core version to 04.02.0003. + + +## 2023-02-21 + +### Added + +* [Web] Added classes related to `Memory`. + * Add the funciton `initializeAmountOfMemory()` to adjust the amount of memory at initialization. + + +## 2022-10-28 + +### Fixed + +* [Java] Remove unnecessary methods. + + +## 2022-10-06 + +### Added + +* [Java] Add AAR file for Android. + + +## 2022-09-08 + +### Added + +* Add the multilingual supported documents. +* Support Visual Studio 2022. + + +## 2022-08-04 + +### Fixed + +* [Web] Fix `csmGetMocVersion` function argument. + + +## 2022-07-07 + +### Added + +* Add functions + * `csmGetParameterTypes` + * `csmGetDrawableParentPartIndices` + +* Add type `csmMocVersion` and enum. This type is the return value of `csmGetMocVersion`, `csmGetLatestMocVersion`. + +### Changed + +* Upgrade Core version to 04.02.0002. + + +## 2022-06-02 + +### Changed + +* Upgrade Core version to 04.02.0001. + +### Fixed + +* Fixed a bug that caused Multiply Color / Screen Color of different objects to be applied. + + +## 2022-05-19 + +### Added + +* Support new Multiply Color / Screen Color features. +* Support new Blend Shape features. + +### Changed + +* Upgrade Core version to 04.02.0000. This upgrade is following Cubism Editor 4.2 features. + + +## 2022-02-10 + +### Added + +* [Unity] Add bitcode library(.bc) for Emscripten latest version build. + +### Changed + +* [Unity] Change the bitcode file directory location. + * emsdk latest version build bitcode file in `latest` directory. + * emsdk 1.38.48 build bitcode file in `1_38_48` directory. + + +## 2021-12-09 + +### Added + +* Add static library(.a) for Mac Catalyst. + + +## 2021-10-07 + +### Added + +* Add `x86_64` library for Android. +* Add `arm64` library for macOS. + + +## 2021-03-09 + +### Added + +* Add funtcions for Viewer. + * `csmGetParameterKeyCounts` + * `csmGetParameterKeyValues` + + +### Changed + +* Update Core version to `04.01.0000`. + + +## 2020-01-30 + +### Added + +* Add static library(.lib) for statically linking DLL. +* Add symbol file for Windows dynamic library (dll). + + +## 2019-11-19 + +### Fixed + +* Fix linking static libraries for Windows (.lib). + + +## 2019-11-14 + +### Added + +* Support Visual Studio 2019. +* Support macOS dynamic library (dylib). + +### Changed + +* Update Windows dynamic library: Use Visual Studio 2019 for building. + +### Security + +* Bundle certificate and notary ticket to macOS shared library. + + +## 2019-09-04 + +### Added + +* Support new Inverted Masking features. +* Support ARM64 architecture for Universal Windows Platform. + +### Changed + +* Upgrade Core version to 04.00.0000 (67108864). This upgrade is following Cubism Editor 4.0 features. +* Add calling convention for *Windows/x86 DLL* only. + +### Removed + +* Remove bitcode binary due to suspension of *Cubism Bindings.* + + +## 2019-04-09 + +### Added + +* Support Universal Windows Platform for Windows Store Application. + + +## 2019-01-31 + +### Added + +* Add API to get the parent part of the specified part. +* Add API to get moc3 version. + + +## 2018-12-20 + +### Added + +* [Native] Add new function: `csmGetPartParentPartIndices`. +* [Native, 3.3 Support] Support new Warp Deformer features. + +### Changed + +* Upgrade Core version to 03.03.0000 (50528256). This upgrade is following Cubism Editor 3.3 features. + + +## 2018-08-22 + +### Added + +* [Native] Add support for Neon. + + +## 2018-05-14 + +### Added + +* [Native] Add Windows **Visual C++ 2013** library. +* [Windows] Add runtime library choice `MT`, `MD`, `MTd`, `MDd`. +* [iOS] Add support for iPhone Simulator SDK. + +### Fixed + +* Fix an error occurred when linking libraries for Android `arm64-v8a`. + + +## 2017-11-17 + +### Fixed + +* Fix processing of vertex index. + + +## 2017-10-05 + +### Added + +* Provide bitcode for iOS. + + +## 2017-08-09 + +### Added + +* [Native] Add Android *arm64-v8a* ABI library. + +### Fixed + +* Fix drawing order in certain scenarios. + + +## 2017-07-12 + +### Added + +* Add experimental support for Emscripten. +* Add `CHANGELOG.md`. + +### Fixed + +* Fix access violation in certain scenarios. +* Fix update result in certain scenarios. + + +## 2017-05-02 + +### Added + +* [Native] Add experimental support for Raspberry PI. +* Add `README.md`. + +``` + +`Source/ThirdParty/CubismLibrary/LICENSE.md`: + +```md +## Live2D Proprietary Software License + +Live2D Cubism Core is available under Live2D Proprietary Software License. + +* [Live2D Proprietary Software License Agreement](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_en.html) +* [Live2D Proprietary Software 使用許諾契約書](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_jp.html) +* [Live2D Proprietary Software 使用授权协议](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_cn.html) + +``` + +`Source/ThirdParty/CubismLibrary/README.ja.md`: + +```md +[English](README.md) / [日本語](README.ja.md) + +--- + +# Live2D Cubism Core + +このフォルダーには、ネイティブアプリケーションを開発するためのヘッダーおよびプラットフォーム固有のライブラリファイルが含まれています。 + +Cubism Coreは、Cubismモデルをアプリケーションにロードするための小さなライブラリです。 + + +## はじめに + +モデルを画面に表示して、アニメーションを再生するための情報については、[CubismNativeSamples](https://github.com/Live2D/CubismNativeSamples)をご覧ください。 + + +## ファイル構造 + +``` +Core +├─ dll # Shared (Dynamic) Library files +├─ include # Header file +└─ lib # Static Library files +``` + + +## ライブラリリスト + +| プラットフォーム | アーキテクチャ | dll | lib | パス | 注記 | +| --- | --- | --- | --- | --- | --- | +| Android | ARM64 | ✓ | ✓ | android/arm64-v8a | | +| Android | ARMv7 | ✓ | ✓ | android/armeabi-v7a | このライブラリは現在非推奨で、近日中に削除される予定です。 | +| Android | x86 | ✓ | ✓ | android/x86 | | +| Android | x86_64 | ✓ | ✓ | android/x86_64 | | +| iOS | ARM64 | | ✓ | ios/xxx-iphoneos | iOSデバイス | +| iOS | x86_64 | | ✓ | ios/xxx-iphonesimulator | iOS Simulator | +| Linux | x86_64 | ✓ | ✓ | linux/x86_64 | | +| macOS | ARM64 | ✓ | ✓ | macos/arm64 | | +| macOS | x86_64 | ✓ | ✓ | macos/x86_64 | | +| Mac Catalyst | ARM64 | | ✓ | experimental/catalyst | Universal Binary | +| Mac Catalyst | x86_64 | | ✓ | experimental/catalyst | Universal Binary | +| Raspberry Pi | ARM | ✓ | ✓ | experimental/rpi | | +| UWP | ARM | ✓ | | experimental/uwp/arm | | +| UWP | ARM64 | ✓ | | experimental/uwp/arm64 | | +| UWP | x64 | ✓ | | experimental/uwp/x64 | | +| UWP | x86 | ✓ | | experimental/uwp/x86 | | +| Windows | x86 | ✓ | ✓ | windows/x86 | | +| Windows | x86_64 | ✓ | ✓ | windows/x86_64 | | + +### 実験的ライブラリ + +`Raspberry Pi`、`UWP`、`catalyst`は実験的なライブラリです。 + +### Windows用スタティックライブラリ + +ライブラリは、VC++バージョン名ディレクトリの下にあります。 + +以下は、VC++バージョンのVisual Studioバージョンです。 + +| VC++バージョン | Visual Studioバージョン | +| ---: | --- | +| 120 | Visual Studio 2013 | +| 140 | Visual Studio 2015 | +| 141 | Visual Studio 2017 | +| 142 | Visual Studio 2019 | +| 143 | Visual Studio 2022 | + +また、サフィックスライブラリ(`_MD`、`_MDd`、`_MT`、`_MTd`)は、ライブラリのコンパイルに使用される[ランタイムライブラリの使用](https://docs.microsoft.com/en-us/cpp/build/reference/md-mt-ld-use-run-time-library) オプションを参照します。 + +### 呼び出し規約 + +*Windows/x86*のダイナミックリンクライブラリを使用する場合は、呼び出し規約として明示的に`__stdcall`を使用してください。 + +``` + +`Source/ThirdParty/CubismLibrary/README.md`: + +```md +[English](README.md) / [日本語](README.ja.md) + +--- + +# Live2D Cubism Core + +This folder contains header and platform-specific library files for developing native applications. + +What's the Core anyway? It's a small library that allows you to load Cubism models into your application. + + +## Getting Started + +You probably want to play back animations and want to display your models on screen, too. + +[Go here](https://github.com/Live2D/CubismNativeSamples) for further information. + + +## File Structure + +``` +Core +├─ dll # Shared (Dynamic) Library files +├─ include # Header file +└─ lib # Static Library files +``` + + +## Library List + +| Platform | Architecture | dll | lib | Path | Note | +| --- | --- | --- | --- | --- | --- | +| Android | ARM64 | ✓ | ✓ | android/arm64-v8a | | +| Android | ARMv7 | ✓ | ✓ | android/armeabi-v7a | This library is currently deprecated and will be removed in the near future. | +| Android | x86 | ✓ | ✓ | android/x86 | | +| Android | x86_64 | ✓ | ✓ | android/x86_64 | | +| iOS | ARM64 | | ✓ | ios/xxx-iphoneos | iOS Devices | +| iOS | x86_64 | | ✓ | ios/xxx-iphonesimulator | iOS Simulator | +| Linux | x86_64 | ✓ | ✓ | linux/x86_64 | | +| macOS | ARM64 | ✓ | ✓ | macos/arm64 | | +| macOS | x86_64 | ✓ | ✓ | macos/x86_64 | | +| Mac Catalyst | ARM64 | | ✓ | experimental/catalyst | Universal Binary | +| Mac Catalyst | x86_64 | | ✓ | experimental/catalyst | Universal Binary | +| Raspberry Pi | ARM | ✓ | ✓ | experimental/rpi | | +| UWP | ARM | ✓ |   | experimental/uwp/arm |   | +| UWP | ARM64 | ✓ | | experimental/uwp/arm64 | | +| UWP | x64 | ✓ | | experimental/uwp/x64 | | +| UWP | x86 | ✓ | | experimental/uwp/x86 | | +| Windows | x86 | ✓ | ✓ | windows/x86 | | +| Windows | x86_64 | ✓ | ✓ | windows/x86_64 | | + +### Experimental Library + +`Raspberry Pi`, `UWP` and `catalyst` are experimental libraries. + +### Windows Static Library + +Libraries are located under the VC++ version name directory. + +Below is the Visual Studio version for the VC++ version. + +| VC++ version | Visual Studio version | +| ---: | --- | +| 120 | Visual Studio 2013 | +| 140 | Visual Studio 2015 | +| 141 | Visual Studio 2017 | +| 142 | Visual Studio 2019 | +| 143 | Visual Studio 2022 | + +Also, the suffix library (`_MD`, `_MDd`, `_MT`, `_MTd`) refers to the [Use Run-Time Library] option used to compile the library. + +### Calling convention + +When using the dynamic library of *Windows/x86*, explicitly use `__stdcall` as the calling convention. + +``` + +`Source/ThirdParty/CubismLibrary/RedistributableFiles.txt`: + +```txt +The following is a list of files available for redistribution +under the terms of the Live2D Proprietary Software License Agreement: + +- dll/android/arm64-v8a/libLive2DCubismCore.so +- dll/android/armeabi-v7a/libLive2DCubismCore.so +- dll/android/x86/libLive2DCubismCore.so +- dll/android/x86_64/libLive2DCubismCore.so +- dll/experimental/rpi/libLive2DCubismCore.so +- dll/experimental/uwp/arm/Live2DCubismCore.dll +- dll/experimental/uwp/arm64/Live2DCubismCore.dll +- dll/experimental/uwp/x64/Live2DCubismCore.dll +- dll/experimental/uwp/x86/Live2DCubismCore.dll +- dll/linux/x86_64/libLive2DCubismCore.so +- dll/macos/Live2DCubismCore.bundle +- dll/macos/libLive2DCubismCore.dylib +- dll/windows/x86/Live2DCubismCore.dll +- dll/windows/x86/Live2DCubismCore.lib +- dll/windows/x86_64/Live2DCubismCore.dll +- dll/windows/x86_64/Live2DCubismCore.lib +- lib/android/arm64-v8a/libLive2DCubismCore.a +- lib/android/armeabi-v7a/libLive2DCubismCore.a +- lib/android/x86/libLive2DCubismCore.a +- lib/android/x86_64/libLive2DCubismCore.so +- lib/experimental/catalyst/libLive2DCubismCore.a +- lib/experimental/rpi/libLive2DCubismCore.a +- lib/ios/Debug-iphoneos/libLive2DCubismCore.a +- lib/ios/Debug-iphonesimulator/libLive2DCubismCore.a +- lib/ios/Release-iphoneos/libLive2DCubismCore.a +- lib/ios/Release-iphonesimulator/libLive2DCubismCore.a +- lib/linux/x86_64/libLive2DCubismCore.a +- lib/macos/arm64/libLive2DCubismCore.a +- lib/macos/x86_64/libLive2DCubismCore.a +- lib/windows/x86/120/Live2DCubismCore_MD.lib +- lib/windows/x86/120/Live2DCubismCore_MDd.lib +- lib/windows/x86/120/Live2DCubismCore_MT.lib +- lib/windows/x86/120/Live2DCubismCore_MTd.lib +- lib/windows/x86/140/Live2DCubismCore_MD.lib +- lib/windows/x86/140/Live2DCubismCore_MDd.lib +- lib/windows/x86/140/Live2DCubismCore_MT.lib +- lib/windows/x86/140/Live2DCubismCore_MTd.lib +- lib/windows/x86/141/Live2DCubismCore_MD.lib +- lib/windows/x86/141/Live2DCubismCore_MDd.lib +- lib/windows/x86/141/Live2DCubismCore_MT.lib +- lib/windows/x86/141/Live2DCubismCore_MTd.lib +- lib/windows/x86/142/Live2DCubismCore_MD.lib +- lib/windows/x86/142/Live2DCubismCore_MDd.lib +- lib/windows/x86/142/Live2DCubismCore_MT.lib +- lib/windows/x86/142/Live2DCubismCore_MTd.lib +- lib/windows/x86/143/Live2DCubismCore_MD.lib +- lib/windows/x86/143/Live2DCubismCore_MDd.lib +- lib/windows/x86/143/Live2DCubismCore_MT.lib +- lib/windows/x86/143/Live2DCubismCore_MTd.lib +- lib/windows/x86_64/120/Live2DCubismCore_MD.lib +- lib/windows/x86_64/120/Live2DCubismCore_MDd.lib +- lib/windows/x86_64/120/Live2DCubismCore_MT.lib +- lib/windows/x86_64/120/Live2DCubismCore_MTd.lib +- lib/windows/x86_64/140/Live2DCubismCore_MD.lib +- lib/windows/x86_64/140/Live2DCubismCore_MDd.lib +- lib/windows/x86_64/140/Live2DCubismCore_MT.lib +- lib/windows/x86_64/140/Live2DCubismCore_MTd.lib +- lib/windows/x86_64/141/Live2DCubismCore_MD.lib +- lib/windows/x86_64/141/Live2DCubismCore_MDd.lib +- lib/windows/x86_64/141/Live2DCubismCore_MT.lib +- lib/windows/x86_64/141/Live2DCubismCore_MTd.lib +- lib/windows/x86_64/142/Live2DCubismCore_MD.lib +- lib/windows/x86_64/142/Live2DCubismCore_MDd.lib +- lib/windows/x86_64/142/Live2DCubismCore_MT.lib +- lib/windows/x86_64/142/Live2DCubismCore_MTd.lib +- lib/windows/x86_64/143/Live2DCubismCore_MD.lib +- lib/windows/x86_64/143/Live2DCubismCore_MDd.lib +- lib/windows/x86_64/143/Live2DCubismCore_MT.lib +- lib/windows/x86_64/143/Live2DCubismCore_MTd.lib +``` + +`Source/ThirdParty/CubismLibrary/include/Live2DCubismCore.h`: + +```h +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Proprietary Software license + * that can be found at https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_en.html. + */ + +#ifndef LIVE2D_CUBISM_CORE_H +#define LIVE2D_CUBISM_CORE_H + +#if defined(__cplusplus) +extern "C" +{ +#endif + +/* ------- * + * DEFINES * + * ------- */ + +/** Core API attribute. */ +#if !defined(csmApi) +#define csmApi +#endif + + /* ----- * + * TYPES * + * ----- */ + + /** Cubism moc. */ + typedef struct csmMoc csmMoc; + + /** Cubism model. */ + typedef struct csmModel csmModel; + + /** Cubism version identifier. */ + typedef unsigned int csmVersion; + + /** Alignment constraints. */ + enum + { + /** Necessary alignment for mocs (in bytes). */ + csmAlignofMoc = 64, + + /** Necessary alignment for models (in bytes). */ + csmAlignofModel = 16 + }; + + /** Bit masks for non-dynamic drawable flags. */ + enum + { + /** Additive blend mode mask. */ + csmBlendAdditive = 1 << 0, + + /** Multiplicative blend mode mask. */ + csmBlendMultiplicative = 1 << 1, + + /** Double-sidedness mask. */ + csmIsDoubleSided = 1 << 2, + + /** Clipping mask inversion mode mask. */ + csmIsInvertedMask = 1 << 3 + }; + + /** Bit masks for dynamic drawable flags. */ + enum + { + /** Flag set when visible. */ + csmIsVisible = 1 << 0, + /** Flag set when visibility did change. */ + csmVisibilityDidChange = 1 << 1, + /** Flag set when opacity did change. */ + csmOpacityDidChange = 1 << 2, + /** Flag set when draw order did change. */ + csmDrawOrderDidChange = 1 << 3, + /** Flag set when render order did change. */ + csmRenderOrderDidChange = 1 << 4, + /** Flag set when vertex positions did change. */ + csmVertexPositionsDidChange = 1 << 5, + /** Flag set when blend color did change. */ + csmBlendColorDidChange = 1 << 6 + }; + + /** Bitfield. */ + typedef unsigned char csmFlags; + + /** moc3 file format version. */ + enum + { + /** unknown */ + csmMocVersion_Unknown = 0, + /** moc3 file version 3.0.00 - 3.2.07 */ + csmMocVersion_30 = 1, + /** moc3 file version 3.3.00 - 3.3.03 */ + csmMocVersion_33 = 2, + /** moc3 file version 4.0.00 - 4.1.05 */ + csmMocVersion_40 = 3, + /** moc3 file version 4.2.00 - 4.2.04 */ + csmMocVersion_42 = 4, + /** moc3 file version 5.0.00 - */ + csmMocVersion_50 = 5 + }; + + /** moc3 version identifier. */ + typedef unsigned int csmMocVersion; + + /** Parameter types. */ + enum + { + /** Normal parameter. */ + csmParameterType_Normal = 0, + + /** Parameter for blend shape. */ + csmParameterType_BlendShape = 1 + }; + + /** Parameter type. */ + typedef int csmParameterType; + + /** 2 component vector. */ + typedef struct + { + /** First component. */ + float X; + + /** Second component. */ + float Y; + } csmVector2; + + /** 4 component vector. */ + typedef struct + { + /** 1st component. */ + float X; + + /** 2nd component. */ + float Y; + + /** 3rd component. */ + float Z; + + /** 4th component. */ + float W; + } csmVector4; + + /** Log handler. + * + * @param message Null-terminated string message to log. + */ + typedef void (*csmLogFunction)(const char* message); + +#if CSM_CORE_WIN32_DLL +#define csmCallingConvention __stdcall +#else +#define csmCallingConvention +#endif + + /* ------- * + * VERSION * + * ------- */ + + /** + * Queries Core version. + * + * @return Core version. + */ + csmApi csmVersion csmCallingConvention csmGetVersion(); + + /** + * Gets Moc file supported latest version. + * + * @return csmMocVersion (Moc file latest format version). + */ + csmApi csmMocVersion csmCallingConvention csmGetLatestMocVersion(); + + /** + * Gets Moc file format version. + * + * @param address Address of moc. + * @param size Size of moc (in bytes). + * + * @return csmMocVersion + */ + csmApi csmMocVersion csmCallingConvention csmGetMocVersion(const void* address, const unsigned int size); + + /* ----------- * + * CONSISTENCY * + * ----------- */ + + /** + * Checks consistency of a moc. + * + * @param address Address of unrevived moc. The address must be aligned to 'csmAlignofMoc'. + * @param size Size of moc (in bytes). + * + * @return '1' if Moc is valid; '0' otherwise. + */ + csmApi int csmCallingConvention csmHasMocConsistency(void* address, const unsigned int size); + + /* ------- * + * LOGGING * + * ------- */ + + /** + * Queries log handler. + * + * @return Log handler. + */ + csmApi csmLogFunction csmCallingConvention csmGetLogFunction(); + + /** + * Sets log handler. + * + * @param handler Handler to use. + */ + csmApi void csmCallingConvention csmSetLogFunction(csmLogFunction handler); + + /* --- * + * MOC * + * --- */ + + /** + * Tries to revive a moc from bytes in place. + * + * @param address Address of unrevived moc. The address must be aligned to 'csmAlignofMoc'. + * @param size Size of moc (in bytes). + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi csmMoc* csmCallingConvention csmReviveMocInPlace(void* address, const unsigned int size); + + /* ----- * + * MODEL * + * ----- */ + + /** + * Queries size of a model in bytes. + * + * @param moc Moc to query. + * + * @return Valid size on success; '0' otherwise. + */ + csmApi unsigned int csmCallingConvention csmGetSizeofModel(const csmMoc* moc); + + /** + * Tries to instantiate a model in place. + * + * @param moc Source moc. + * @param address Address to place instance at. Address must be aligned to 'csmAlignofModel'. + * @param size Size of memory block for instance (in bytes). + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi csmModel* csmCallingConvention csmInitializeModelInPlace(const csmMoc* moc, + void* address, + const unsigned int size); + + /** + * Updates a model. + * + * @param model Model to update. + */ + csmApi void csmCallingConvention csmUpdateModel(csmModel* model); + + /* ------ * + * CANVAS * + * ------ */ + + /** + * Reads info on a model canvas. + * + * @param model Model query. + * + * @param outSizeInPixels Canvas dimensions. + * @param outOriginInPixels Origin of model on canvas. + * @param outPixelsPerUnit Aspect used for scaling pixels to units. + */ + csmApi void csmCallingConvention csmReadCanvasInfo(const csmModel* model, + csmVector2* outSizeInPixels, + csmVector2* outOriginInPixels, + float* outPixelsPerUnit); + + /* ---------- * + * PARAMETERS * + * ---------- */ + + /** + * Gets number of parameters. + * + * @param[in] model Model to query. + * + * @return Valid count on success; '-1' otherwise. + */ + csmApi int csmCallingConvention csmGetParameterCount(const csmModel* model); + + /** + * Gets parameter IDs. + * All IDs are null-terminated ANSI strings. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const char** csmCallingConvention csmGetParameterIds(const csmModel* model); + + + /** + * Gets parameter types. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const csmParameterType* csmCallingConvention csmGetParameterTypes(const csmModel* model); + + /** + * Gets minimum parameter values. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const float* csmCallingConvention csmGetParameterMinimumValues(const csmModel* model); + + /** + * Gets maximum parameter values. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const float* csmCallingConvention csmGetParameterMaximumValues(const csmModel* model); + + /** + * Gets default parameter values. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const float* csmCallingConvention csmGetParameterDefaultValues(const csmModel* model); + + /** + * Gets read/write parameter values buffer. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi float* csmCallingConvention csmGetParameterValues(csmModel* model); + + /** + * Gets number of key values of each parameter. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const int* csmCallingConvention csmGetParameterKeyCounts(const csmModel* model); + + /** + * Gets key values of each parameter. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const float** csmCallingConvention csmGetParameterKeyValues(const csmModel* model); + + + /* ----- * + * PARTS * + * ----- */ + + /** + * Gets number of parts. + * + * @param model Model to query. + * + * @return Valid count on success; '-1' otherwise. + */ + csmApi int csmCallingConvention csmGetPartCount(const csmModel* model); + + /** + * Gets parts IDs. + * All IDs are null-terminated ANSI strings. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const char** csmCallingConvention csmGetPartIds(const csmModel* model); + + /** + * Gets read/write part opacities buffer. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi float* csmCallingConvention csmGetPartOpacities(csmModel* model); + + /** + * Gets part's parent part indices. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const int* csmCallingConvention csmGetPartParentPartIndices(const csmModel* model); + + /* --------- * + * DRAWABLES * + * --------- */ + + /** + * Gets number of drawables. + * + * @param model Model to query. + * + * @return Valid count on success; '-1' otherwise. + */ + csmApi int csmCallingConvention csmGetDrawableCount(const csmModel* model); + + /** + * Gets drawable IDs. + * All IDs are null-terminated ANSI strings. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const char** csmCallingConvention csmGetDrawableIds(const csmModel* model); + + /** + * Gets constant drawable flags. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const csmFlags* csmCallingConvention csmGetDrawableConstantFlags(const csmModel* model); + + /** + * Gets dynamic drawable flags. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const csmFlags* csmCallingConvention csmGetDrawableDynamicFlags(const csmModel* model); + + /** + * Gets drawable texture indices. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const int* csmCallingConvention csmGetDrawableTextureIndices(const csmModel* model); + + /** + * Gets drawable draw orders. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const int* csmCallingConvention csmGetDrawableDrawOrders(const csmModel* model); + + /** + * Gets drawable render orders. + * The higher the order, the more up front a drawable is. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0'otherwise. + */ + csmApi const int* csmCallingConvention csmGetDrawableRenderOrders(const csmModel* model); + + /** + * Gets drawable opacities. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const float* csmCallingConvention csmGetDrawableOpacities(const csmModel* model); + + /** + * Gets numbers of masks of each drawable. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const int* csmCallingConvention csmGetDrawableMaskCounts(const csmModel* model); + + /** + * Gets mask indices of each drawable. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const int** csmCallingConvention csmGetDrawableMasks(const csmModel* model); + + /** + * Gets number of vertices of each drawable. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const int* csmCallingConvention csmGetDrawableVertexCounts(const csmModel* model); + + /** + * Gets vertex position data of each drawable. + * + * @param model Model to query. + * + * @return Valid pointer on success; a null pointer otherwise. + */ + csmApi const csmVector2** csmCallingConvention csmGetDrawableVertexPositions(const csmModel* model); + + /** + * Gets texture coordinate data of each drawables. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const csmVector2** csmCallingConvention csmGetDrawableVertexUvs(const csmModel* model); + + /** + * Gets number of triangle indices for each drawable. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const int* csmCallingConvention csmGetDrawableIndexCounts(const csmModel* model); + + /** + * Gets triangle index data for each drawable. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const unsigned short** csmCallingConvention csmGetDrawableIndices(const csmModel* model); + + /** + * Gets multiply color data for each drawable. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const csmVector4* csmCallingConvention csmGetDrawableMultiplyColors(const csmModel* model); + + /** + * Gets screen color data for each drawable. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const csmVector4* csmCallingConvention csmGetDrawableScreenColors(const csmModel* model); + + /** + * Gets drawable's parent part indices. + * + * @param model Model to query. + * + * @return Valid pointer on success; '0' otherwise. + */ + csmApi const int* csmCallingConvention csmGetDrawableParentPartIndices(const csmModel* model); + + /** + * Resets all dynamic drawable flags. + * + * @param model Model containing flags. + */ + csmApi void csmCallingConvention csmResetDrawableDynamicFlags(csmModel* model); + +#if defined(__cplusplus) +} +#endif + +#endif + +``` + +`Source/UnrealLive2D/Private/CubisimInclude.h`: + +```h + +#if PLATFORM_WINDOWS +#include "Windows\AllowWindowsPlatformTypes.h" +#endif + +#include "Live2DCubismCore.hpp" +#include "CubismFramework.hpp" + +#if PLATFORM_WINDOWS +#include "Windows\HideWindowsPlatformTypes.h" +#endif + +``` + +`Source/UnrealLive2D/Private/CubismBpLib.cpp`: + +```cpp +// Fill out your copyright notice in the Description page of Project Settings. + +#include "CubismBpLib.h" + +#include "Misc/Paths.h" +#include "Misc/FileHelper.h" +#include "CubismObject.h" +#include "CubisimInclude.h" + +#include "FWPort/CubismFrameWorkAllocator.h" +#include "Interfaces/IPluginManager.h" +#include "Templates/SharedPointer.h" + +#include "UnrealLive2D.h" + +using namespace Live2D::Cubism::Core; +using namespace Live2D::Cubism::Framework; + +CubismFrameWorkAllocator ls_Allocator; + +/** Log out func register to cubism */ +static void Cubism_Log(const char* message) +{ + printf("%s\n", message); + UE_LOG(LogCubism, Log, TEXT("[UCubismBpLib] %s"), ANSI_TO_TCHAR(message)); +} + +void AnimCallback(float time, const char* value) +{ + UE_LOG(LogCubism, Log, TEXT("[UCubismBpLib] AnimCallback: %f -> %s"), time, ANSI_TO_TCHAR(value)); +} + +///////////////////////////////////////////////////////////////////////////////////// +static bool lb_IsInited = false; + +void UCubismBpLib::InitCubism() +{ + if (lb_IsInited) + { + UE_LOG(LogTemp, Log, TEXT("UCubismBpLib::InitCubism: Already inited.")); + return; + } + + csmVersion version = csmGetVersion(); + UE_LOG(LogCubism, Log, TEXT("csm Version is: %u"), version); + + Csm::CubismFramework::Option _cubismOption; ///< Cubism SDK Option + //setup cubism + _cubismOption.LogFunction = Cubism_Log; + _cubismOption.LoggingLevel = Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Verbose; + Csm::CubismFramework::StartUp(&ls_Allocator, &_cubismOption); + + //Initialize cubism + CubismFramework::Initialize(); + + lb_IsInited = true; +} + +FString UCubismBpLib::Get_CubismPath() +{ + TSharedPtr tsp_Plugin = IPluginManager::Get().FindPlugin(TEXT("UnrealLive2D")); + if (tsp_Plugin.IsValid()) + { + return tsp_Plugin->GetBaseDir(); + } + else + { + return FPaths::ProjectPluginsDir() / TEXT("UnrealLive2D"); + } +} + +``` + +`Source/UnrealLive2D/Private/CubismObject.cpp`: + +```cpp +// Fill out your copyright notice in the Description page of Project Settings. + +#include "CubismObject.h" +#include "Engine/Texture2D.h" +#include "Engine/Canvas.h" +#include "Engine/CanvasRenderTarget2D.h" +#include "FWPort/RawModel.h" +#include "Draw/CubismSepRender.h" + +#include "UnrealLive2D.h" +#include "CubismBpLib.h" + + +UCubismObject::UCubismObject() +{ + msp_RawModel = nullptr; + msp_Render = MakeShared(); + +} + +UCubismObject::~UCubismObject() +{ +} + +void UCubismObject::Tick(float DeltaTime) +{ + if (_bPauseUpdate) + { + return; + } + + _TimeSeconds += DeltaTime; + if (_TimeSeconds < _TimeNextUpdate) + { + return; + } + + const float tf_UpdateDelta = ((TickRate > 0) ? (1.0f / TickRate) : 0.05f); + + if (msp_RawModel.IsValid()&& IsValid(TargetRender)) + { + /** + * The update control is a bit dirty + * If you want to change this logic, take care of GameThread/RenderThread yourself + * The render result will be flipping if GameThread updated vertex buffer while render is taking place. + */ + if (!msp_Render->RenderStates.bIsRendering) + { + check(!msp_Render->RenderStates.bIsRendering); + msp_Render->RenderStates.bIsRendering = true; + msp_Render->RenderStates.bRenderOver = false; + + msp_RawModel->OnUpdate(_TimeSeconds - _TimeLastUpdate); + _TimeLastUpdate = _TimeSeconds; + + msp_Render->DrawSeparateToRenderTarget(GetWorld(), TargetRender); + UE_LOG(LogCubism, Verbose, TEXT("Update: %f at %f || %f %f"), DeltaTime, GetWorld()->GetTimeSeconds(), _TimeSeconds, _TimeNextUpdate); + } + else + { + if (msp_Render->RenderStates.bRenderOver) + { + msp_Render->RenderStates.bIsRendering = false; + msp_Render->RenderStates.bRenderOver = false; + return; + } + } + } + + _TimeNextUpdate = _TimeSeconds + tf_UpdateDelta; +} + + +bool UCubismObject::IsTickableInEditor() const +{ + return true; +} + +TStatId UCubismObject::GetStatId() const +{ + RETURN_QUICK_DECLARE_CYCLE_STAT(UCubismViewer, STATGROUP_Tickables); +} + +bool UCubismObject::IsTickable() const +{ + if (HasAnyFlags(RF_ClassDefaultObject)) return false; + + return true; +} + +void UCubismObject::BeginDestroy() +{ + Super::BeginDestroy(); + + msp_RawModel.Reset(); + msp_Render.Reset(); +} + +void UCubismObject::LoadFromPath(const FString& InPath) +{ + /** Try init cubism, init with module startup will break cook */ + UCubismBpLib::InitCubism(); + + /** Load model */ + msp_RawModel.Reset(); + + msp_RawModel = MakeShared(); + const bool tb_ReadSuc = msp_RawModel->LoadAsset(InPath); + if (!tb_ReadSuc) + { + msp_RawModel.Reset(); + } + + msp_Render->InitRender(msp_RawModel, ModelConfig); + msp_RawModel->OnMotionPlayEnd.BindUObject(this, &UCubismObject::OnMotionPlayeEnd); +} + +void UCubismObject::SetUpdatePaused(bool InIsPaused) +{ + _bPauseUpdate = InIsPaused; +} + +////////////////////////////////////////////////////////////////////////// + +void UCubismObject::OnMotionPlayeEnd_Implementation() +{ + UE_LOG(LogCubism, Log, TEXT("UCubismObject::OnMotionPlayeEnd")); +} + +bool UCubismObject::OnTap(const FVector2D& InPos) +{ + if (!msp_RawModel.IsValid()) + { + UE_LOG(LogCubism, Error, TEXT("UCubismObject::OnTap: No valid model")); + return false; + } + + bool tb_Handled = msp_RawModel->OnTap(InPos.X, InPos.Y); + UE_LOG(LogCubism, Log, TEXT("UCubismObject::OnTap: %s %d"), + *InPos.ToString(), + tb_Handled ? 1 : 0 + ); + + return tb_Handled; +} + +void UCubismObject::PlayMotion(const FString& InName, const int32 InNo, const EMotionPriority InPriority /*= EMotionPriority::EMP_Normal*/) +{ + if (!msp_RawModel.IsValid()) + { + UE_LOG(LogCubism, Error, TEXT("UCubismObject::PlayMotion: No valid model")); + return; + } + + UE_LOG(LogCubism, Log, TEXT("UCubismObject::PlayMotion: %s_%d | %d"), *InName, InNo, (int32)InPriority); + msp_RawModel->PlayMotion(InName, InNo, (int32)InPriority); +} + +``` + +`Source/UnrealLive2D/Private/CubismUeTypes.cpp`: + +```cpp +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "CubismUeTypes.h" + + +``` + +`Source/UnrealLive2D/Private/Draw/CubismConfig.cpp`: + +```cpp +#include "CubismConfig.h" + +FModelConfig::FModelConfig() +{ +} + +``` + +`Source/UnrealLive2D/Private/Draw/CubismConfig.h`: + +```h +#pragma once + +#include "CubismConfig.generated.h" + +/** + * Model config goes here + */ +USTRUCT(BlueprintType) +struct FModelConfig +{ + GENERATED_USTRUCT_BODY() + +public: + FModelConfig(); + +public: + + /** + * If Opens + * We will try to render all mask in a single texture if possible + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere) + bool bTryLowPreciseMask = true; + +}; +``` + +`Source/UnrealLive2D/Private/Draw/CubismSepRender.cpp`: + +```cpp +#include "CubismSepRender.h" +#include "RHICommandList.h" +#include "TextureResource.h" +#include "ProfilingDebugging/RealtimeGPUProfiler.h" +#include "RenderingThread.h" +#include "Engine/TextureRenderTarget2D.h" +#include "Engine/World.h" +#include "GlobalShader.h" +#include "Shader.h" +#include "RHIStaticStates.h" +#include "PipelineStateCache.h" +#include "RHIDefinitions.h" +#include "FWPort/RawModel.h" +#include "Templates/SharedPointer.h" +#include "RHI.h" +#include "ImageUtils.h" +#include "FWPort/ModelRender.h" +#include "Engine/Texture2D.h" + +#include "UnrealLive2D.h" +#include "ModelRenders.h" +#include "SceneInterface.h" + +static bool GDrawTextureForTest = false; +FAutoConsoleVariableRef CVarDrawTextureForTest( + TEXT("cubism.db.texture"), + GDrawTextureForTest, + TEXT("Draw texture instead of model for debug"), + ECVF_Default); + +static bool GCubismNoMask = false; +FAutoConsoleVariableRef CVarCubismNoMask( + TEXT("cubism.db.nomask"), + GCubismNoMask, + TEXT("Draw model with no mask"), + ECVF_Default); + +#define _DRAW_TEXTURE_ GDrawTextureForTest +#define _DRAW_MODEL_ (!_DRAW_TEXTURE_) + +////////////////////////////////////////////////////////////////////////// +using Csm::csmFloat32; +using Csm::csmInt32; +using Csm::csmUint16; + +struct FCompiledSeparateDraw +{ + Csm::CubismModel *InModel = nullptr; + FCubismRenderState *InStates = nullptr; +}; + +////////////////////////////////////////////////////////////////////////// + +static void DrawSeparateToRenderTarget_RenderThread( + FRHICommandListImmediate &RHICmdList, + const FCompiledSeparateDraw &CompiledSeparateDraw, + const FName &TextureRenderTargetName, + FTextureRenderTargetResource *OutTextureRenderTargetResource, + ERHIFeatureLevel::Type FeatureLevel) +{ + +#if WANTS_DRAW_MESH_EVENTS + SCOPED_DRAW_EVENTF(RHICmdList, SceneCapture, TEXT("DrawSeparateToRenderTarget %s"), TextureRenderTargetName); +#else + SCOPED_DRAW_EVENT(RHICmdList, DrawSeparateToRenderTarget_RenderThread); +#endif + + ////////////////////////////////////////////////////////////////////////// + check(CompiledSeparateDraw.InModel); + Csm::CubismModel *tp_Model = CompiledSeparateDraw.InModel; + FCubismRenderState *tp_States = CompiledSeparateDraw.InStates; + + check(tp_States); + check(IsInRenderingThread()); + + /** Mask绘制-基础版 */ + const bool tb_IsUsingMask = tp_Model->IsUsingMasking(); + if (tb_IsUsingMask) + { + CubismClippingManager_UE *_clippingManager = tp_States->_ClippingManager.Get(); + _clippingManager->SetupClippingContext(*tp_Model, tp_States); + + /** Draw mask to single texture if in low precise mode */ + if (!tp_States->Get_UseHighPreciseMask()) + { + FModelRenders::RenderMask_Full(tp_States, RHICmdList, _clippingManager, FeatureLevel, tp_Model); + } + } + + /** 渲染模型主体 */ + if (_DRAW_MODEL_) + { + const csmInt32 drawableCount = tp_Model->GetDrawableCount(); + const csmInt32 *renderOrder = tp_Model->GetDrawableRenderOrders(); + + // インデックスを描画順でソート + for (csmInt32 i = 0; i < drawableCount; ++i) + { + const csmInt32 order = renderOrder[i]; + tp_States->_sortedDrawableIndexList[order] = i; + } + + ////////////////////////////////////////////////////////////////////////// + { + FRHITexture* RenderTargetTexture = OutTextureRenderTargetResource->GetRenderTargetTexture(); + RHICmdList.Transition(MakeArrayView(FModelRenders::ConvertTransitionResource(FExclusiveDepthStencil::DepthWrite_StencilWrite, RenderTargetTexture))); + + FRHIRenderPassInfo RPInfo(RenderTargetTexture, ERenderTargetActions::Clear_Store); + RHICmdList.BeginRenderPass(RPInfo, TEXT("DrawClear")); + RHICmdList.EndRenderPass(); + } + + ////////////////////////////////////////////////////////////////////////// + + // 描画 + for (csmInt32 i = 0; i < drawableCount; ++i) + { + const csmInt32 drawableIndex = tp_States->_sortedDrawableIndexList[i]; + + // Drawableが表示状態でなければ処理をパスする + if (!tp_Model->GetDrawableDynamicFlagIsVisible(drawableIndex)) + { + continue; + } + + const csmInt32 indexCount = tp_Model->GetDrawableVertexIndexCount(drawableIndex); + if (0 == indexCount) + { + continue; + } + + if (!tp_States->IndexBuffers.Contains(drawableIndex)) + { + continue; + } + + if (!tp_States->VertexBuffers.Contains(drawableIndex)) + { + UE_LOG(LogCubism, Error, TEXT("DrawSeparateToRenderTarget_RenderThread: Vertext buffer not inited.")); + continue; + } + + ////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////// + /** Mask relate */ + CubismClippingContext *clipContext = (tp_States->_ClippingManager.IsValid()) + ? (*tp_States->_ClippingManager->GetClippingContextListForDraw())[drawableIndex] + : NULL; + const bool tb_IsMaskDraw = (nullptr != clipContext); + if (tb_IsMaskDraw) + { +#if !UE_BUILD_SHIPPING + if (GCubismNoMask) + { + continue; + } +#endif + + /** High precise mask draws to mask buffer per drawable */ + if (tp_States->Get_UseHighPreciseMask()) + { + FModelRenders::RenderMask_Single(tp_States, RHICmdList, FeatureLevel, tp_Model, clipContext); + } + + FModelRenders::DrawSepMask(OutTextureRenderTargetResource, RHICmdList, FeatureLevel, tp_Model, drawableIndex, tp_States, clipContext); + } + else + { + FModelRenders::DrawSepNormal(OutTextureRenderTargetResource, RHICmdList, FeatureLevel, tp_Model, drawableIndex, tp_States); + } + } + } + + if (_DRAW_TEXTURE_) + { + FModelRenders::DrawTestTexture(OutTextureRenderTargetResource, RHICmdList, FeatureLevel, tp_States); + } + + tp_States->bRenderOver = true; +} + +void FCubismSepRender::DrawSeparateToRenderTarget( + class UWorld *World, + class UTextureRenderTarget2D *OutputRenderTarget) const +{ + check(IsInGameThread()); + + if (!OutputRenderTarget) + { + UE_LOG(LogCubism, Error, TEXT("FCubismSepRender::DrawSeparateToRenderTarget: OutputRenderTarget not valid")); + return; + } + + if (!IsValid(World)) + { + UE_LOG(LogCubism, Error, TEXT("FCubismSepRender::DrawSeparateToRenderTarget: World not valid")); + return; + } + + if (!_Model.IsValid()) + { + UE_LOG(LogCubism, Error, TEXT("FCubismSepRender::DrawSeparateToRenderTarget: _Model not valid")); + return; + } + + Csm::CubismModel *tp_Model = _Model->GetModel(); + if (!tp_Model) + { + UE_LOG(LogCubism, Error, TEXT("FCubismSepRender::DrawSeparateToRenderTarget: GetModel not valid")); + return; + } + + const FName TextureRenderTargetName = OutputRenderTarget->GetFName(); + FTextureRenderTargetResource *TextureRenderTargetResource = OutputRenderTarget->GameThread_GetRenderTargetResource(); + + FCompiledSeparateDraw ts_CompiledSeparateDraw; + ts_CompiledSeparateDraw.InModel = tp_Model; + ts_CompiledSeparateDraw.InStates = &RenderStates; + + ERHIFeatureLevel::Type FeatureLevel = World->Scene->GetFeatureLevel(); + + ENQUEUE_RENDER_COMMAND(CaptureCommand) + ( + [ts_CompiledSeparateDraw, TextureRenderTargetResource, TextureRenderTargetName, FeatureLevel](FRHICommandListImmediate &RHICmdList) + { + DrawSeparateToRenderTarget_RenderThread( + RHICmdList, + ts_CompiledSeparateDraw, + TextureRenderTargetName, + TextureRenderTargetResource, + FeatureLevel); + }); +} + +void FCubismSepRender::InitRender(TSharedPtr InModel, const FModelConfig &InModelConfig) +{ + if (!InModel.IsValid()) + { + /** You should never reach here, something is wrong. */ + checkNoEntry(); + return; + } + + _Model = InModel; + + Csm::CubismModel *tp_Model = _Model->GetModel(); + if (!tp_Model) + { + /** Model passing to render must be ready */ + checkNoEntry(); + return; + } + + LoadTextures(); + + csmInt32 td_DrawableCount = tp_Model->GetDrawableCount(); + RenderStates._sortedDrawableIndexList.Resize(td_DrawableCount, 0); + + ENQUEUE_RENDER_COMMAND(CubismInit) + ( + [=, this](FRHICommandListImmediate &RHICmdList) + { + for (csmInt32 td_DrawIter = 0; td_DrawIter < td_DrawableCount; td_DrawIter++) + { + /** Vertex buffer */ + const csmInt32 vcount = tp_Model->GetDrawableVertexCount(td_DrawIter); + if (0 != vcount) + { + FRHIResourceCreateInfo CreateInfo_Vert(TEXT("CubismInit")); + FBufferRHIRef ScratchVertexBufferRHI = RHICmdList.CreateVertexBuffer(vcount * sizeof(FCubismVertex), BUF_Dynamic, ERHIAccess::WritableMask, CreateInfo_Vert); + + RenderStates.VertexBuffers.Add(td_DrawIter, ScratchVertexBufferRHI); + RenderStates.VertexCount.Add(td_DrawIter, vcount); + UE_LOG(LogCubism, Log, TEXT("FCubismSepRender::InitRender: Vertext buffer info %d >> (%u, %u)"), td_DrawIter, ScratchVertexBufferRHI->GetSize(), ScratchVertexBufferRHI->GetUsage()); + } + + /** Index buffer are static */ + const csmInt32 indexCount = tp_Model->GetDrawableVertexIndexCount(td_DrawIter); + if (indexCount != 0) + { + const csmUint16 *indexArray = const_cast(tp_Model->GetDrawableVertexIndices(td_DrawIter)); + + FRHIResourceCreateInfo CreateInfo_Indice(TEXT("InitRender")); + FBufferRHIRef IndexBufferRHI = RHICmdList.CreateIndexBuffer(sizeof(uint16), sizeof(uint16) * indexCount, BUF_Static, CreateInfo_Indice); + void *VoidPtr = RHICmdList.LockBuffer(IndexBufferRHI, 0, sizeof(uint16) * indexCount, RLM_WriteOnly); + FMemory::Memcpy(VoidPtr, indexArray, indexCount * sizeof(uint16)); + RHICmdList.UnlockBuffer(IndexBufferRHI); + + RenderStates.IndexBuffers.Add(td_DrawIter, IndexBufferRHI); + } + + UE_LOG(LogCubism, Log, TEXT("FCubismSepRender::InitRender: [%d/%d] V:%d I:%d"), td_DrawIter, td_DrawableCount, vcount, indexCount); + } + + + + ////////////////////////////////////////////////////////////////////////// + RenderStates._ClippingManager.Reset(); + RenderStates._ClippingManager = MakeShared(); + + RenderStates._ClippingManager->Initialize( + *tp_Model, + tp_Model->GetDrawableCount(), + tp_Model->GetDrawableMasks(), + tp_Model->GetDrawableMaskCounts()); + + const csmInt32 bufferHeight = RenderStates._ClippingManager->GetClippingMaskBufferSize(); + + ETextureCreateFlags Flags = ETextureCreateFlags(TexCreate_None | TexCreate_RenderTargetable | TexCreate_ShaderResource); + const FRHITextureCreateDesc Desc = + FRHITextureCreateDesc::Create2D(TEXT("InitRenderSec")) + .SetExtent((int32)bufferHeight, (int32)bufferHeight) + .SetFormat(EPixelFormat::PF_B8G8R8A8) + .SetNumMips(1) + .SetNumSamples(1) + .SetFlags(Flags) + .SetInitialState(ERHIAccess::Unknown) + .SetGPUMask(FRHIGPUMask::All()) + .SetClearValue(FClearValueBinding(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f))); + + RenderStates.MaskBuffer = RHICreateTexture(Desc); + // TransitionResource(FExclusiveDepthStencil DepthStencilMode, FRHITexture * DepthTexture) + + ////////////////////////////////////////////////////////////////////////// + RenderStates.ClearStates(); + RenderStates.RenderModelConfig = InModelConfig; + }); + + + + + + UE_LOG(LogCubism, Log, TEXT("FCubismSepRender::InitRender: Completed")); +} + +void FCubismSepRender::LoadTextures() +{ + check(_Model.IsValid()); + + Csm::ICubismModelSetting *_modelSetting = _Model->Get_ModelSetting(); + csmInt32 td_TextureNum = _modelSetting->GetTextureCount(); + + /** Clear old textures */ + UnLoadTextures(); + RenderStates.Textures.SetNumZeroed(td_TextureNum); + + /** Load new texture */ + for (csmInt32 modelTextureNumber = 0; modelTextureNumber < td_TextureNum; modelTextureNumber++) + { + if (strcmp(_modelSetting->GetTextureFileName(modelTextureNumber), "") == 0) + { + continue; + } + + Csm::csmString texturePath = _modelSetting->GetTextureFileName(modelTextureNumber); + FString tstr_TempReadPath = _Model->Get_HomeDir() / UTF8_TO_TCHAR(texturePath.GetRawString()); + + UTexture2D *tp_LoadedImage = FImageUtils::ImportFileAsTexture2D(tstr_TempReadPath); + if (IsValid(tp_LoadedImage)) + { + RenderStates.Textures[modelTextureNumber] = tp_LoadedImage; + } + + /** Some kind of cheat, must call RemoveFromRoot as UnLoadTextures does if you directly remove some texture */ + tp_LoadedImage->AddToRoot(); + UE_LOG(LogCubism, Log, TEXT("FCubismSepRender::LoadTextures: From %s | %d / %d >> %p"), *tstr_TempReadPath, modelTextureNumber, td_TextureNum, tp_LoadedImage); + } + + UE_LOG(LogCubism, Log, TEXT("FCubismSepRender::LoadTextures: Complete with %d texture"), _modelSetting->GetTextureCount()); +} + +void FCubismSepRender::UnLoadTextures() +{ + for (UTexture2D *tp_Texture : RenderStates.Textures) + { + if (IsValid(tp_Texture)) + { + tp_Texture->RemoveFromRoot(); + } + } + + RenderStates.Textures.Empty(); +} + +////////////////////////////////////////////////////////////////////////// +void FCubismRenderState::ClearStates() +{ + bNoLowPreciseMask = false; +} + +bool FCubismRenderState::Get_UseHighPreciseMask() const +{ + /** Config decide not use low precise mask */ + if (!RenderModelConfig.bTryLowPreciseMask) + { + return true; + } + + /** + * Low precise is not possible + * @Note See SetupClippingContext for detail + */ + if (bNoLowPreciseMask) + { + return true; + } + + return false; +} + +``` + +`Source/UnrealLive2D/Private/Draw/CubismSepRender.h`: + +```h +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Templates/SharedPointer.h" +#include "CubismFramework.hpp" +#include "Type/csmVector.hpp" +#include "CubismConfig.h" +#include "RHIResources.h" + + +class UTexture2D; + +struct FCubismRenderState +{ + Csm::csmVector _sortedDrawableIndexList; ///< 描画オブジェクトのインデックスを描画順に並べたリスト + + TMap IndexBuffers; + TMap VertexBuffers; + TMap VertexCount; + + TArray Textures; + + TSharedPtr _ClippingManager; + + FTextureRHIRef MaskBuffer; + + FModelConfig RenderModelConfig; + + bool bIsRendering = false; + bool bRenderOver = false; + +public: + void ClearStates(); + + void NoLowPreciseMask(bool val) { bNoLowPreciseMask = val; } + bool Get_UseHighPreciseMask() const; + +private: + bool bNoLowPreciseMask = false; +}; + +/** + * + */ +struct FCubismSepRender +{ +public: + + void DrawSeparateToRenderTarget( + class UWorld* World, + class UTextureRenderTarget2D* OutputRenderTarget) const; + + void InitRender(TSharedPtr InModel, const FModelConfig& InModelConfig); + +private: + void LoadTextures(); + + void UnLoadTextures(); + + +private: + TSharedPtr _Model; + +public: + /** TODO: We are cheating */ + mutable FCubismRenderState RenderStates; + +}; +``` + +`Source/UnrealLive2D/Private/Draw/ModelRenders.cpp`: + +```cpp +#include "ModelRenders.h" +#include "Rendering/CubismRenderer.hpp" +#include "CubismSepRender.h" +#include "RHIDefinitions.h" +#include "RHIStaticStates.h" + +#include "UnrealLive2D.h" +#include "Engine/Texture2D.h" +#include "PipelineStateCache.h" + +using namespace Csm; + +void FModelRenders::SetUpBlendMode( + Csm::CubismModel* tp_Model, + const Csm::csmInt32 drawableIndex, + FGraphicsPipelineStateInitializer& GraphicsPSOInit +) +{ + Rendering::CubismRenderer::CubismBlendMode ts_BlendMode = tp_Model->GetDrawableBlendMode(drawableIndex); + switch (ts_BlendMode) + { + case Rendering::CubismRenderer::CubismBlendMode_Normal: + { + GraphicsPSOInit.BlendState = TStaticBlendState::GetRHI(); + break; + } + case Rendering::CubismRenderer::CubismBlendMode_Additive: + { + GraphicsPSOInit.BlendState = TStaticBlendState::GetRHI(); + break; + } + case Rendering::CubismRenderer::CubismBlendMode_Multiplicative: + { + GraphicsPSOInit.BlendState = TStaticBlendState::GetRHI(); + break; + } + } +} + +void FModelRenders::FillVertexBuffer( + Csm::CubismModel* tp_Model, + const Csm::csmInt32 drawableIndex, + FBufferRHIRef ScratchVertexBufferRHI, + FCubismRenderState* tp_States, + FRHICommandListImmediate& RHICmdList +) +{ + const csmInt32 td_NumVertext = tp_Model->GetDrawableVertexCount(drawableIndex); + UE_LOG(LogCubism, Verbose, TEXT("FillVertexBuffer: Vertext buffer info %d|%d >> (%u, %u)"), drawableIndex, td_NumVertext, ScratchVertexBufferRHI->GetSize(), ScratchVertexBufferRHI->GetUsage()); + check(td_NumVertext == tp_States->VertexCount[drawableIndex]); + { + + const csmFloat32* uvarray = reinterpret_cast(const_cast(tp_Model->GetDrawableVertexUvs(drawableIndex))); + const csmFloat32* varray = const_cast(tp_Model->GetDrawableVertices(drawableIndex)); + + check(varray); + check(uvarray); + + void* DrawableData = RHICmdList.LockBuffer(ScratchVertexBufferRHI, 0, td_NumVertext * sizeof(FCubismVertex), RLM_WriteOnly); + FCubismVertex* RESTRICT DestSamples = (FCubismVertex*)DrawableData; + + for (int32 td_VertexIndex = 0; td_VertexIndex < td_NumVertext; ++td_VertexIndex) + { + DestSamples[td_VertexIndex].Position.X = varray[td_VertexIndex * 2]; + DestSamples[td_VertexIndex].Position.Y = varray[td_VertexIndex * 2 + 1]; + DestSamples[td_VertexIndex].UV.X = uvarray[td_VertexIndex * 2]; + DestSamples[td_VertexIndex].UV.Y = uvarray[td_VertexIndex * 2 + 1]; + } + + RHICmdList.UnlockBuffer(ScratchVertexBufferRHI); + } +} + +class UTexture2D* FModelRenders::GetTexture( + Csm::CubismModel* tp_Model, + const Csm::csmInt32 drawableIndex, + FCubismRenderState* tp_States +) +{ + const csmInt32 td_TextrueNo = tp_Model->GetDrawableTextureIndices(drawableIndex); + UTexture2D* tp_Texture = nullptr; + if (tp_States->Textures.IsValidIndex(td_TextrueNo)) + { + tp_Texture = tp_States->Textures[td_TextrueNo]; + } + + if (!IsValid(tp_Texture)) + { + UE_LOG(LogCubism, Warning, TEXT("DrawSeparateToRenderTarget_RenderThread: Texture %d invalid"), td_TextrueNo); + return nullptr; + } + + return tp_Texture; +} + +FMatrix44f FModelRenders::ConvertCubismMatrix(Csm::CubismMatrix44& InCubismMartix) +{ + FMatrix44f ts_Mat; + + ts_Mat.M[0][0] = InCubismMartix.GetArray()[0]; + ts_Mat.M[0][1] = InCubismMartix.GetArray()[1]; + ts_Mat.M[0][2] = InCubismMartix.GetArray()[2]; + ts_Mat.M[0][3] = InCubismMartix.GetArray()[3]; + + ts_Mat.M[1][0] = InCubismMartix.GetArray()[4]; + ts_Mat.M[1][1] = InCubismMartix.GetArray()[5]; + ts_Mat.M[1][2] = InCubismMartix.GetArray()[6]; + ts_Mat.M[1][3] = InCubismMartix.GetArray()[7]; + + ts_Mat.M[2][0] = InCubismMartix.GetArray()[8]; + ts_Mat.M[2][1] = InCubismMartix.GetArray()[9]; + ts_Mat.M[2][2] = InCubismMartix.GetArray()[10]; + ts_Mat.M[2][3] = InCubismMartix.GetArray()[11]; + + ts_Mat.M[3][0] = InCubismMartix.GetArray()[12]; + ts_Mat.M[3][1] = InCubismMartix.GetArray()[13]; + ts_Mat.M[3][2] = InCubismMartix.GetArray()[14]; + ts_Mat.M[3][3] = InCubismMartix.GetArray()[15]; + + return ts_Mat; +} + +void FCubismVertexBuffer::InitRHI(FRHICommandListBase& RHICmdList) +{ + // create a static vertex buffer + FRHIResourceCreateInfo CreateInfo(TEXT("CubismVertexBuffer")); + VertexBufferRHI = RHICmdList.CreateVertexBuffer(sizeof(FCubismVertex) * 4, BUF_Static, CreateInfo); + void* VoidPtr = RHICmdList.LockBuffer(VertexBufferRHI, 0, sizeof(FCubismVertex) * 4, RLM_WriteOnly); + static const FCubismVertex Vertices[4] = + { + FCubismVertex(-0.9,-0.9, 0, 0), + FCubismVertex(-0.9,+0.9, 0, 1), + FCubismVertex(+0.9,-0.9, 1, 0), + FCubismVertex(+0.9,+0.9, 1, 1), + }; + FMemory::Memcpy(VoidPtr, Vertices, sizeof(FCubismVertex) * 4); + RHICmdList.UnlockBuffer(VertexBufferRHI); +} + +TGlobalResource GCubismVertexScreenBuffer; + +void FCubismVertexDeclaration::InitRHI(FRHICommandListBase& RHICmdList) +{ + FVertexDeclarationElementList Elements; + uint32 Stride = sizeof(FCubismVertex); + Elements.Add(FVertexElement(0, STRUCT_OFFSET(FCubismVertex, Position), VET_Float2, 0, Stride)); + Elements.Add(FVertexElement(0, STRUCT_OFFSET(FCubismVertex, UV), VET_Float2, 1, Stride)); + VertexDeclarationRHI = PipelineStateCache::GetOrCreateVertexDeclaration(Elements); +} + +void FCubismVertexDeclaration::ReleaseRHI() +{ + VertexDeclarationRHI.SafeRelease(); +} + +TGlobalResource GCubismVertexDeclaration; + +////////////////////////////////////////////////////////////////////////// +TArray> FModelRenders::ConvertTransitionResource(FExclusiveDepthStencil DepthStencilMode, FRHITexture* DepthTexture) +{ + check(DepthStencilMode.IsUsingDepth() || DepthStencilMode.IsUsingStencil()); + + TArray> Infos; + + DepthStencilMode.EnumerateSubresources([&](ERHIAccess NewAccess, uint32 PlaneSlice) + { + FRHITransitionInfo Info; + Info.Type = FRHITransitionInfo::EType::Texture; + Info.Texture = DepthTexture; + Info.AccessAfter = NewAccess; + Info.PlaneSlice = PlaneSlice; + Infos.Emplace(Info); + }); + + return Infos; +} +``` + +`Source/UnrealLive2D/Private/Draw/ModelRenders.h`: + +```h +#include "CubismFramework.hpp" +#include "Model/CubismModel.hpp" +#include "Type/CubismBasicType.hpp" +#include "RHIResources.h" +#include "RenderResource.h" +#include "RHICommandList.h" +#include "RHIDefinitions.h" +#include "UObject/NoExportTypes.h" + +////////////////////////////////////////////////////////////////////////// +// See CommonRenderResources.cpp + +/** The vertex data used to filter a texture. */ +struct FCubismVertex +{ + FVector2f Position; + FVector2f UV; + + FCubismVertex(float x, float y, float z, float w) + : Position(x, y), UV(z, w) + { + } +}; + +/** The filter vertex declaration resource type. */ +class FCubismVertexDeclaration : public FRenderResource +{ +public: + FVertexDeclarationRHIRef VertexDeclarationRHI; + + /** Destructor. */ + virtual ~FCubismVertexDeclaration() {} + + virtual void InitRHI(FRHICommandListBase &RHICmdList) override; + + virtual void ReleaseRHI(); +}; + +extern TGlobalResource GCubismVertexDeclaration; +class FRHITexture; + +/** Test Texture Draw */ +class FCubismVertexBuffer : public FVertexBuffer +{ +public: + /** + * Initialize the RHI for this rendering resource + */ + virtual void InitRHI(FRHICommandListBase &RHICmdList) override; +}; + +extern TGlobalResource GCubismVertexScreenBuffer; + +////////////////////////////////////////////////////////////////////////// +struct FCubismRenderState; + +struct FModelRenders +{ + static void SetUpBlendMode( + Csm::CubismModel *tp_Model, + const Csm::csmInt32 drawableIndex, + FGraphicsPipelineStateInitializer &GraphicsPSOInit); + + static void FillVertexBuffer( + Csm::CubismModel *tp_Model, + const Csm::csmInt32 drawableIndex, + FBufferRHIRef ScratchVertexBufferRHI, + FCubismRenderState *tp_States, + FRHICommandListImmediate &RHICmdList); + + static class UTexture2D *GetTexture( + Csm::CubismModel *tp_Model, + const Csm::csmInt32 drawableIndex, + FCubismRenderState *tp_States); + + static FMatrix44f ConvertCubismMatrix(Csm::CubismMatrix44 &InCubismMartix); + + /** Normal Drawable draw */ + static void DrawSepNormal( + class FTextureRenderTargetResource *OutTextureRenderTargetResource, + FRHICommandListImmediate &RHICmdList, + ERHIFeatureLevel::Type FeatureLevel, + Csm::CubismModel *tp_Model, + const Csm::csmInt32 drawableIndex, + struct FCubismRenderState *tp_States); + + ////////////////////////////////////////////////////////////////////////// + /** Masked Drawable Draw */ + static void DrawSepMask( + FTextureRenderTargetResource *OutTextureRenderTargetResource, + FRHICommandListImmediate &RHICmdList, ERHIFeatureLevel::Type FeatureLevel, + Csm::CubismModel *tp_Model, + const Csm::csmInt32 drawableIndex, + FCubismRenderState *tp_States, + class CubismClippingContext *clipContext); + + static void _DrawSepMask_Normal( + FTextureRenderTargetResource *OutTextureRenderTargetResource, + FRHICommandListImmediate &RHICmdList, ERHIFeatureLevel::Type FeatureLevel, + Csm::CubismModel *tp_Model, + const Csm::csmInt32 drawableIndex, + FCubismRenderState *tp_States, + class CubismClippingContext *clipContext); + + static void _DrawSepMask_Invert( + FTextureRenderTargetResource *OutTextureRenderTargetResource, + FRHICommandListImmediate &RHICmdList, ERHIFeatureLevel::Type FeatureLevel, + Csm::CubismModel *tp_Model, + const Csm::csmInt32 drawableIndex, + FCubismRenderState *tp_States, + class CubismClippingContext *clipContext); + + ////////////////////////////////////////////////////////////////////////// + /** Render Mask to buffer, low precise version */ + static void RenderMask_Full( + FCubismRenderState *tp_States, + FRHICommandListImmediate &RHICmdList, + class CubismClippingManager_UE *_clippingManager, + ERHIFeatureLevel::Type FeatureLevel, + Csm::CubismModel *tp_Model); + + /** Render Mask to buffer, high precise version */ + static void RenderMask_Single( + FCubismRenderState *tp_States, + FRHICommandListImmediate &RHICmdList, + ERHIFeatureLevel::Type FeatureLevel, + Csm::CubismModel *tp_Model, + class CubismClippingContext *clipContext); + + /** Draw Texture, for debug */ + static void DrawTestTexture( + FTextureRenderTargetResource *OutTextureRenderTargetResource, + FRHICommandListImmediate &RHICmdList, + ERHIFeatureLevel::Type FeatureLevel, + struct FCubismRenderState *tp_States); + + /** + * Copied from RHICommandList.h, as TransitionResource has been deprecated + * + * @see: RHICommandList.h, UE_DEPRECATED(5.1, "TransitionResource has been deprecated. Use Transition instead.") + */ + static TArray> ConvertTransitionResource(FExclusiveDepthStencil DepthStencilMode, FRHITexture *DepthTexture); +}; + +``` + +`Source/UnrealLive2D/Private/Draw/ModelRenders_DrawableMask.cpp`: + +```cpp +#include "ModelRenders.h" +#include "TextureResource.h" +#include "FWPort/ModelRender.h" +#include "GlobalShader.h" +#include "RHIStaticStates.h" +#include "CubismSepRender.h" +#include "Engine/Texture2D.h" +#include "PipelineStateCache.h" + +#include "UnrealLive2D.h" +#include "ShaderParameterUtils.h" +#include "DataDrivenShaderPlatformInfo.h" + +////////////////////////////////////////////////////////////////////////// + +class FCubismSepMask : public FGlobalShader +{ + DECLARE_INLINE_TYPE_LAYOUT(FCubismSepMask, NonVirtual); + +public: + static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) + { + return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5); + } + + static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) + { + FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); + } + + FCubismSepMask() {} + + FCubismSepMask(const ShaderMetaType::CompiledShaderInitializerType& Initializer) + : FGlobalShader(Initializer) + { + TestFloat.Bind(Initializer.ParameterMap, TEXT("TestFloat")); + ProjectMatrix.Bind(Initializer.ParameterMap, TEXT("ProjectMatrix")); + ClipMatrix.Bind(Initializer.ParameterMap, TEXT("ClipMatrix")); + BaseColor.Bind(Initializer.ParameterMap, TEXT("BaseColor")); + ChannelFlag.Bind(Initializer.ParameterMap, TEXT("ChannelFlag")); + + MainTexture.Bind(Initializer.ParameterMap, TEXT("MainTexture")); + MaskTexture.Bind(Initializer.ParameterMap, TEXT("MaskTexture")); + MainTextureSampler.Bind(Initializer.ParameterMap, TEXT("MainSampler")); + MaskSampler.Bind(Initializer.ParameterMap, TEXT("MaskSampler")); + } + + template + void SetParameters( + FRHICommandListImmediate& RHICmdList, + const TShaderRHIParamRef ShaderRHI, + const FMatrix44f& InProjectMatrix, + const FMatrix44f& InClipMatrix, + const FVector4f& InBaseColor, + const FVector4f& InChannelFlag, + FTextureRHIRef MainTextureRef, + FTextureRHIRef MaskTextureRef + ) + { + FRHIBatchedShaderParameters& BatchedParameters = RHICmdList.GetScratchShaderParameters(); + SetShaderValue(BatchedParameters, TestFloat, 1.0f); + SetShaderValue(BatchedParameters, ProjectMatrix, InProjectMatrix); + SetShaderValue(BatchedParameters, ClipMatrix, InClipMatrix); + SetShaderValue(BatchedParameters, BaseColor, InBaseColor); + SetShaderValue(BatchedParameters, ChannelFlag, InChannelFlag); + + SetTextureParameter(BatchedParameters, MainTexture, MainTextureRef); + SetTextureParameter(BatchedParameters, MaskTexture, MaskTextureRef); + + SetSamplerParameter(BatchedParameters, MainTextureSampler, TStaticSamplerState::GetRHI()); + SetSamplerParameter(BatchedParameters, MaskSampler, TStaticSamplerState::GetRHI()); + + RHICmdList.SetBatchedShaderParameters(ShaderRHI, BatchedParameters); + } + +private: + LAYOUT_FIELD(FShaderParameter, TestFloat); + LAYOUT_FIELD(FShaderParameter, ProjectMatrix); + LAYOUT_FIELD(FShaderParameter, ClipMatrix); + LAYOUT_FIELD(FShaderParameter, BaseColor); + LAYOUT_FIELD(FShaderParameter, ChannelFlag); + + LAYOUT_FIELD(FShaderResourceParameter, MainTexture); + LAYOUT_FIELD(FShaderResourceParameter, MaskTexture); + LAYOUT_FIELD(FShaderResourceParameter, MainTextureSampler); + LAYOUT_FIELD(FShaderResourceParameter, MaskSampler); +}; + +class FCubismSepMaskVS : public FCubismSepMask +{ + DECLARE_SHADER_TYPE(FCubismSepMaskVS, Global); + +public: + + /** Default constructor. */ + FCubismSepMaskVS() {} + + /** Initialization constructor. */ + FCubismSepMaskVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) + : FCubismSepMask(Initializer) + { + } + +private: +}; + +class FCubismSepMaskPS : public FCubismSepMask +{ + DECLARE_SHADER_TYPE(FCubismSepMaskPS, Global); + +public: + + /** Default constructor. */ + FCubismSepMaskPS() {} + + /** Initialization constructor. */ + FCubismSepMaskPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) + : FCubismSepMask(Initializer) + { + + } +}; + +IMPLEMENT_SHADER_TYPE(, FCubismSepMaskVS, TEXT("/Plugin/UnrealLive2D/Private/CubismSepMask.usf"), TEXT("MainVS"), SF_Vertex) +IMPLEMENT_SHADER_TYPE(, FCubismSepMaskPS, TEXT("/Plugin/UnrealLive2D/Private/CubismSepMask.usf"), TEXT("MainPS"), SF_Pixel) + +class FCubismSepMaskPS_Invert : public FCubismSepMask +{ + DECLARE_SHADER_TYPE(FCubismSepMaskPS_Invert, Global); + +public: + + /** Default constructor. */ + FCubismSepMaskPS_Invert() {} + + /** Initialization constructor. */ + FCubismSepMaskPS_Invert(const ShaderMetaType::CompiledShaderInitializerType& Initializer) + : FCubismSepMask(Initializer) + { + + } +}; + +IMPLEMENT_SHADER_TYPE(, FCubismSepMaskPS_Invert, TEXT("/Plugin/UnrealLive2D/Private/CubismSepMask.usf"), TEXT("MainPsInvert"), SF_Pixel) + +////////////////////////////////////////////////////////////////////////// +void FModelRenders::DrawSepMask( + FTextureRenderTargetResource* OutTextureRenderTargetResource, + FRHICommandListImmediate& RHICmdList, + ERHIFeatureLevel::Type FeatureLevel, + Csm::CubismModel* tp_Model, + const csmInt32 drawableIndex, + FCubismRenderState* tp_States, + CubismClippingContext* clipContext +) +{ + if (!tp_Model) + { + UE_LOG(LogCubism, Error, TEXT("FModelRenders::DrawSepMask: Model not valid")); + return; + } + + if (tp_Model->GetDrawableInvertedMask(drawableIndex)) + { + _DrawSepMask_Invert(OutTextureRenderTargetResource, RHICmdList, FeatureLevel, tp_Model, drawableIndex, tp_States, clipContext); + } + else + { + _DrawSepMask_Normal(OutTextureRenderTargetResource, RHICmdList, FeatureLevel, tp_Model, drawableIndex, tp_States, clipContext); + } +} + +void FModelRenders::_DrawSepMask_Normal( + FTextureRenderTargetResource* OutTextureRenderTargetResource, + FRHICommandListImmediate& RHICmdList, + ERHIFeatureLevel::Type FeatureLevel, + Csm::CubismModel* tp_Model, + const csmInt32 drawableIndex, + FCubismRenderState* tp_States, + CubismClippingContext* clipContext +) +{ + + csmFloat32 tf_Opacity = tp_Model->GetDrawableOpacity(drawableIndex); + if (tf_Opacity <= 0.0) + { + return; + } + + UTexture2D* tp_Texture = GetTexture(tp_Model, drawableIndex, tp_States); + if (!tp_Texture) + { + return; + } + + const csmInt32 indexCount = tp_Model->GetDrawableVertexIndexCount(drawableIndex); + check(indexCount > 0 && "Bad Index Count"); + + ////////////////////////////////////////////////////////////////////////// + FRHITexture* RenderTargetTexture = OutTextureRenderTargetResource->GetRenderTargetTexture(); + RHICmdList.Transition(MakeArrayView(FModelRenders::ConvertTransitionResource(FExclusiveDepthStencil::DepthWrite_StencilWrite, RenderTargetTexture))); + + FRHIRenderPassInfo RPInfo(RenderTargetTexture, ERenderTargetActions::Load_Store); + RHICmdList.BeginRenderPass(RPInfo, TEXT("DrawSeparate")); + { + FIntPoint DisplacementMapResolution(OutTextureRenderTargetResource->GetSizeX(), OutTextureRenderTargetResource->GetSizeY()); + + // Update viewport. + RHICmdList.SetViewport( + 0, 0, 0.f, + DisplacementMapResolution.X, DisplacementMapResolution.Y, 1.f); + + // Get shaders. + FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(FeatureLevel); + TShaderMapRef< FCubismSepMaskVS > VertexShader_Mask(GlobalShaderMap); + TShaderMapRef< FCubismSepMaskPS > PixelShader_Mask(GlobalShaderMap); + + // Set the graphic pipeline state. + FGraphicsPipelineStateInitializer GraphicsPSOInit; + RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); + GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); + GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); + + GraphicsPSOInit.RasterizerState = TStaticRasterizerState::GetRHI(); + GraphicsPSOInit.PrimitiveType = PT_TriangleList; + GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GCubismVertexDeclaration.VertexDeclarationRHI; + + + // Update viewport. + RHICmdList.SetViewport( + 0, 0, 0.f, + OutTextureRenderTargetResource->GetSizeX(), OutTextureRenderTargetResource->GetSizeY(), 1.f); + + ////////////////////////////////////////////////////////////////////////// + const csmInt32 td_NumVertext = tp_Model->GetDrawableVertexCount(drawableIndex); + csmBool tb_InvertMask = tp_Model->GetDrawableInvertedMask(drawableIndex); + SetUpBlendMode(tp_Model, drawableIndex, GraphicsPSOInit); + + ////////////////////////////////////////////////////////////////////////// + /** Drawable draw */ + FBufferRHIRef IndexBufferRHI = tp_States->IndexBuffers.FindRef(drawableIndex); + FBufferRHIRef ScratchVertexBufferRHI = tp_States->VertexBuffers.FindRef(drawableIndex); + + check(tp_Texture->GetResource()); + FTextureRHIRef tsr_TextureRHI = tp_Texture->GetResource()->TextureRHI; + + FillVertexBuffer(tp_Model, drawableIndex, ScratchVertexBufferRHI, tp_States, RHICmdList); + + ////////////////////////////////////////////////////////////////////////// + // TODO: ModelColor + FVector4f ts_BaseColor = FVector4f(1.0f, 1.0f, 1.0f, 1.0f); + ts_BaseColor.W = tf_Opacity; + + /** TODO: If find some model use this */ + check(!tb_InvertMask); + + GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader_Mask.GetVertexShader(); + GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader_Mask.GetPixelShader(); + + static FMatrix ts_FakeGlobal = FMatrix::Identity; + FMatrix44f ts_MartixForDraw; + FVector4f ts_ChanelFlag; + + { + // チャンネル + const csmInt32 channelNo = clipContext->_layoutChannelNo; + // チャンネルをRGBAに変換 + Csm::Rendering::CubismRenderer::CubismTextureColor* colorChannel = tp_States->_ClippingManager->GetChannelFlagAsColor(channelNo); + + csmRectF* rect = clipContext->_layoutBounds; + + ts_MartixForDraw = ConvertCubismMatrix(clipContext->_matrixForDraw); + ts_ChanelFlag = FVector4f(colorChannel->R, colorChannel->G, colorChannel->B, colorChannel->A); + } + + SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit,0); + + UE_LOG(LogCubism, Verbose, TEXT("_matrixForDraw: %s"), *ts_MartixForDraw.ToString()); + UE_LOG(LogCubism, Verbose, TEXT("_matrixForMask: %s"), *ConvertCubismMatrix(clipContext->_matrixForMask).ToString()); + + FTextureRHIRef tsr_MaskBuffer = tp_States->MaskBuffer; + FTextureRHIRef tsr_MaskTexture = tsr_MaskBuffer->GetTexture2D(); + + VertexShader_Mask->SetParameters(RHICmdList, VertexShader_Mask.GetVertexShader(), ts_MartixForDraw, ts_MartixForDraw, ts_BaseColor, ts_ChanelFlag, tsr_TextureRHI, tsr_MaskTexture); + PixelShader_Mask->SetParameters(RHICmdList, PixelShader_Mask.GetPixelShader(), ts_MartixForDraw, ts_MartixForDraw, ts_BaseColor, ts_ChanelFlag, tsr_TextureRHI, tsr_MaskTexture); + + ////////////////////////////////////////////////////////////////////////// + + + RHICmdList.SetStreamSource(0, ScratchVertexBufferRHI, 0); + //RHICmdList.DrawIndexedPrimitive(IndexBufferRHI, 0, 0, 4, 0, 2, 1); + + RHICmdList.DrawIndexedPrimitive( + IndexBufferRHI, + /*BaseVertexIndex=*/ 0, + /*MinIndex=*/ 0, + /*NumVertices=*/ td_NumVertext, + /*StartIndex=*/ 0, + /*NumPrimitives=*/ indexCount / 3, + /*NumInstances=*/ 1 + ); + } + RHICmdList.EndRenderPass(); + } + + void FModelRenders::_DrawSepMask_Invert( + FTextureRenderTargetResource* OutTextureRenderTargetResource, + FRHICommandListImmediate& RHICmdList, + ERHIFeatureLevel::Type FeatureLevel, + Csm::CubismModel* tp_Model, + const csmInt32 drawableIndex, + FCubismRenderState* tp_States, + CubismClippingContext* clipContext + ) + { + + csmFloat32 tf_Opacity = tp_Model->GetDrawableOpacity(drawableIndex); + if (tf_Opacity <= 0.0) + { + return; + } + + UTexture2D* tp_Texture = GetTexture(tp_Model, drawableIndex, tp_States); + if (!tp_Texture) + { + return; + } + + const csmInt32 indexCount = tp_Model->GetDrawableVertexIndexCount(drawableIndex); + check(indexCount > 0 && "Bad Index Count"); + + ////////////////////////////////////////////////////////////////////////// + FRHITexture* RenderTargetTexture = OutTextureRenderTargetResource->GetRenderTargetTexture(); + RHICmdList.Transition(MakeArrayView(FModelRenders::ConvertTransitionResource(FExclusiveDepthStencil::DepthWrite_StencilWrite, RenderTargetTexture))); + + FRHIRenderPassInfo RPInfo(RenderTargetTexture, ERenderTargetActions::Load_Store); + RHICmdList.BeginRenderPass(RPInfo, TEXT("DrawSeparate")); + { + FIntPoint DisplacementMapResolution(OutTextureRenderTargetResource->GetSizeX(), OutTextureRenderTargetResource->GetSizeY()); + + // Update viewport. + RHICmdList.SetViewport( + 0, 0, 0.f, + DisplacementMapResolution.X, DisplacementMapResolution.Y, 1.f); + + // Get shaders. + FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(FeatureLevel); + TShaderMapRef< FCubismSepMaskVS > VertexShader_Mask(GlobalShaderMap); + TShaderMapRef< FCubismSepMaskPS_Invert> PixelShader_Mask_Invert(GlobalShaderMap); + + // Set the graphic pipeline state. + FGraphicsPipelineStateInitializer GraphicsPSOInit; + RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); + GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); + GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); + GraphicsPSOInit.RasterizerState = TStaticRasterizerState::GetRHI(); + GraphicsPSOInit.PrimitiveType = PT_TriangleList; + GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GCubismVertexDeclaration.VertexDeclarationRHI; + + + // Update viewport. + RHICmdList.SetViewport( + 0, 0, 0.f, + OutTextureRenderTargetResource->GetSizeX(), OutTextureRenderTargetResource->GetSizeY(), 1.f); + + ////////////////////////////////////////////////////////////////////////// + const csmInt32 td_NumVertext = tp_Model->GetDrawableVertexCount(drawableIndex); + SetUpBlendMode(tp_Model, drawableIndex, GraphicsPSOInit); + + ////////////////////////////////////////////////////////////////////////// + /** Drawable draw */ + FBufferRHIRef IndexBufferRHI = tp_States->IndexBuffers.FindRef(drawableIndex); + FBufferRHIRef ScratchVertexBufferRHI = tp_States->VertexBuffers.FindRef(drawableIndex); + + check(tp_Texture->GetResource()); + FTextureRHIRef tsr_TextureRHI = tp_Texture->GetResource()->TextureRHI; + + FillVertexBuffer(tp_Model, drawableIndex, ScratchVertexBufferRHI, tp_States, RHICmdList); + + ////////////////////////////////////////////////////////////////////////// + // TODO: ModelColor + FVector4f ts_BaseColor = FVector4f(1.0f, 1.0f, 1.0f, 1.0f); + ts_BaseColor.W = tf_Opacity; + + GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader_Mask.GetVertexShader(); + GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader_Mask_Invert.GetPixelShader(); + + + static FMatrix ts_FakeGlobal = FMatrix::Identity; + FMatrix44f ts_MartixForDraw; + FVector4f ts_ChanelFlag; + + { + // チャンネル + const csmInt32 channelNo = clipContext->_layoutChannelNo; + // チャンネルをRGBAに変換 + Csm::Rendering::CubismRenderer::CubismTextureColor* colorChannel = tp_States->_ClippingManager->GetChannelFlagAsColor(channelNo); + + csmRectF* rect = clipContext->_layoutBounds; + + ts_MartixForDraw = ConvertCubismMatrix(clipContext->_matrixForDraw); + ts_ChanelFlag = FVector4f(colorChannel->R, colorChannel->G, colorChannel->B, colorChannel->A); + } + + SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); + + UE_LOG(LogCubism, Verbose, TEXT("_matrixForDraw: %s"), *ts_MartixForDraw.ToString()); + UE_LOG(LogCubism, Verbose, TEXT("_matrixForMask: %s"), *ConvertCubismMatrix(clipContext->_matrixForMask).ToString()); + + FTextureRHIRef tsr_MaskBuffer = tp_States->MaskBuffer; + FTextureRHIRef tsr_MaskTexture = tsr_MaskBuffer->GetTexture2D(); + + VertexShader_Mask->SetParameters(RHICmdList, VertexShader_Mask.GetVertexShader(), ts_MartixForDraw, ts_MartixForDraw, ts_BaseColor, ts_ChanelFlag, tsr_TextureRHI, tsr_MaskTexture); + PixelShader_Mask_Invert->SetParameters(RHICmdList, PixelShader_Mask_Invert.GetPixelShader(), ts_MartixForDraw, ts_MartixForDraw, ts_BaseColor, ts_ChanelFlag, tsr_TextureRHI, tsr_MaskTexture); + + + + ////////////////////////////////////////////////////////////////////////// + + + RHICmdList.SetStreamSource(0, ScratchVertexBufferRHI, 0); + //RHICmdList.DrawIndexedPrimitive(IndexBufferRHI, 0, 0, 4, 0, 2, 1); + + RHICmdList.DrawIndexedPrimitive( + IndexBufferRHI, + /*BaseVertexIndex=*/ 0, + /*MinIndex=*/ 0, + /*NumVertices=*/ td_NumVertext, + /*StartIndex=*/ 0, + /*NumPrimitives=*/ indexCount / 3, + /*NumInstances=*/ 1 + ); + } + RHICmdList.EndRenderPass(); + } + +``` + +`Source/UnrealLive2D/Private/Draw/ModelRenders_DrawableNormal.cpp`: + +```cpp +#include "ModelRenders.h" +#include "CubismSepRender.h" +#include "RHICommandList.h" +#include "RHIDefinitions.h" +#include "RHIResources.h" +#include "TextureResource.h" +#include "Shader.h" +#include "GlobalShader.h" +#include "RHIStaticStates.h" +#include "Engine/Texture2D.h" +#include "PipelineStateCache.h" +#include "RenderUtils.h" +#include "ShaderParameterUtils.h" +#include "DataDrivenShaderPlatformInfo.h" + + +////////////////////////////////////////////////////////////////////////// +class FCubismSepShader : public FGlobalShader +{ + DECLARE_INLINE_TYPE_LAYOUT(FCubismSepShader, NonVirtual); + +public: + static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) + { + return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5); + } + + static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) + { + FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); + } + + FCubismSepShader() {} + + FCubismSepShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer) + : FGlobalShader(Initializer) + { + TestFloat.Bind(Initializer.ParameterMap, TEXT("TestFloat")); + BaseColor.Bind(Initializer.ParameterMap, TEXT("BaseColor")); + + InTexture.Bind(Initializer.ParameterMap, TEXT("InTexture")); + InTextureSampler.Bind(Initializer.ParameterMap, TEXT("InTextureSampler")); + } + + template + void SetParameters( + FRHICommandListImmediate& RHICmdList, + const TShaderRHIParamRef ShaderRHI, + const FVector4f& InBaseColor, + FTextureRHIRef ShaderResourceTexture + ) + { + FRHIBatchedShaderParameters& BatchedParameters = RHICmdList.GetScratchShaderParameters(); + SetShaderValue(BatchedParameters, TestFloat, 1.0f); + SetShaderValue(BatchedParameters, BaseColor, InBaseColor); + + SetTextureParameter(BatchedParameters, InTexture, ShaderResourceTexture); + SetSamplerParameter(BatchedParameters, InTextureSampler, TStaticSamplerState::GetRHI()); + + RHICmdList.SetBatchedShaderParameters(ShaderRHI, BatchedParameters); + } + +private: + LAYOUT_FIELD(FShaderParameter, TestFloat); + LAYOUT_FIELD(FShaderParameter, BaseColor); + + LAYOUT_FIELD(FShaderResourceParameter, InTexture); + LAYOUT_FIELD(FShaderResourceParameter, InTextureSampler); +}; + + +class FCubismSepVS : public FCubismSepShader +{ + DECLARE_SHADER_TYPE(FCubismSepVS, Global); + +public: + + /** Default constructor. */ + FCubismSepVS() {} + + /** Initialization constructor. */ + FCubismSepVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) + : FCubismSepShader(Initializer) + { + } +}; + + +class FCubismSepPS : public FCubismSepShader +{ + DECLARE_SHADER_TYPE(FCubismSepPS, Global); + +public: + + /** Default constructor. */ + FCubismSepPS() {} + + /** Initialization constructor. */ + FCubismSepPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) + : FCubismSepShader(Initializer) + { + + + } +private: + +}; + +IMPLEMENT_SHADER_TYPE(, FCubismSepVS, TEXT("/Plugin/UnrealLive2D/Private/CubismSep.usf"), TEXT("MainVS"), SF_Vertex) +IMPLEMENT_SHADER_TYPE(, FCubismSepPS, TEXT("/Plugin/UnrealLive2D/Private/CubismSep.usf"), TEXT("MainPS"), SF_Pixel) + +////////////////////////////////////////////////////////////////////////// +using Csm::csmInt32; +using Csm::csmFloat32; +using Csm::csmUint16; +using Csm::csmBool; + +void FModelRenders::DrawSepNormal( + FTextureRenderTargetResource* OutTextureRenderTargetResource, + FRHICommandListImmediate& RHICmdList, + ERHIFeatureLevel::Type FeatureLevel, + Csm::CubismModel* tp_Model, + const Csm::csmInt32 drawableIndex, + struct FCubismRenderState* tp_States +) +{ + csmFloat32 tf_Opacity = tp_Model->GetDrawableOpacity(drawableIndex); + if (tf_Opacity <= 0.0) + { + return; + } + + UTexture2D* tp_Texture = GetTexture(tp_Model, drawableIndex, tp_States); + if (!tp_Texture) + { + return; + } + + const csmInt32 indexCount = tp_Model->GetDrawableVertexIndexCount(drawableIndex); + check(indexCount > 0&& "Bad Index Count"); + + FRHITexture* RenderTargetTexture = OutTextureRenderTargetResource->GetRenderTargetTexture(); + RHICmdList.Transition(MakeArrayView(FModelRenders::ConvertTransitionResource(FExclusiveDepthStencil::DepthWrite_StencilWrite, RenderTargetTexture))); + + FRHIRenderPassInfo RPInfo(RenderTargetTexture, ERenderTargetActions::Load_Store); + RHICmdList.BeginRenderPass(RPInfo, TEXT("DrawSeparate")); + { + FIntPoint DisplacementMapResolution(OutTextureRenderTargetResource->GetSizeX(), OutTextureRenderTargetResource->GetSizeY()); + + // Update viewport. + RHICmdList.SetViewport( + 0, 0, 0.f, + DisplacementMapResolution.X, DisplacementMapResolution.Y, 1.f); + + // Get shaders. + FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(FeatureLevel); + TShaderMapRef< FCubismSepVS > VertexShader(GlobalShaderMap); + TShaderMapRef< FCubismSepPS > PixelShader(GlobalShaderMap); + + // Set the graphic pipeline state. + FGraphicsPipelineStateInitializer GraphicsPSOInit; + RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); + GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); + GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); + GraphicsPSOInit.RasterizerState = TStaticRasterizerState::GetRHI(); + GraphicsPSOInit.PrimitiveType = PT_TriangleList; + GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GCubismVertexDeclaration.VertexDeclarationRHI; + + // Update viewport. + RHICmdList.SetViewport( + 0, 0, 0.f, + OutTextureRenderTargetResource->GetSizeX(), OutTextureRenderTargetResource->GetSizeY(), 1.f); + + ////////////////////////////////////////////////////////////////////////// + const csmInt32 td_NumVertext = tp_Model->GetDrawableVertexCount(drawableIndex); + csmBool tb_InvertMask = tp_Model->GetDrawableInvertedMask(drawableIndex); + SetUpBlendMode(tp_Model, drawableIndex, GraphicsPSOInit); + + ////////////////////////////////////////////////////////////////////////// + /** Drawable draw */ + FBufferRHIRef IndexBufferRHI = tp_States->IndexBuffers.FindRef(drawableIndex); + FBufferRHIRef ScratchVertexBufferRHI = tp_States->VertexBuffers.FindRef(drawableIndex); + + check(tp_Texture->GetResource()); + FTextureRHIRef tsr_TextureRHI = tp_Texture->GetResource()->TextureRHI; + + FillVertexBuffer(tp_Model, drawableIndex, ScratchVertexBufferRHI, tp_States, RHICmdList); + + ////////////////////////////////////////////////////////////////////////// + // TODO: ModelColor + FVector4f ts_BaseColor = FVector4f(1.0f, 1.0f, 1.0f, 1.0f); + ts_BaseColor.W = tf_Opacity; + + GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); + GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); + SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); + + VertexShader->SetParameters(RHICmdList, VertexShader.GetVertexShader(), ts_BaseColor, tsr_TextureRHI); + PixelShader->SetParameters(RHICmdList, PixelShader.GetPixelShader(), ts_BaseColor, tsr_TextureRHI); + + ////////////////////////////////////////////////////////////////////////// + + RHICmdList.SetStreamSource(0, ScratchVertexBufferRHI, 0); + + RHICmdList.DrawIndexedPrimitive( + IndexBufferRHI, + /*BaseVertexIndex=*/ 0, + /*MinIndex=*/ 0, + /*NumVertices=*/ td_NumVertext, + /*StartIndex=*/ 0, + /*NumPrimitives=*/ indexCount / 3, + /*NumInstances=*/ 1 + ); + } + RHICmdList.EndRenderPass(); +} + + +void FModelRenders::DrawTestTexture(FTextureRenderTargetResource* OutTextureRenderTargetResource, FRHICommandListImmediate& RHICmdList, ERHIFeatureLevel::Type FeatureLevel, struct FCubismRenderState* tp_States) +{ + FRHITexture* RenderTargetTexture = OutTextureRenderTargetResource->GetRenderTargetTexture(); + RHICmdList.Transition(MakeArrayView(FModelRenders::ConvertTransitionResource(FExclusiveDepthStencil::DepthWrite_StencilWrite, RenderTargetTexture))); + + FRHIRenderPassInfo RPInfo(RenderTargetTexture, ERenderTargetActions::Clear_Store); + RHICmdList.BeginRenderPass(RPInfo, TEXT("DrawSeparateTest")); + { + + FIntPoint DisplacementMapResolution(OutTextureRenderTargetResource->GetSizeX(), OutTextureRenderTargetResource->GetSizeY()); + + // Update viewport. + RHICmdList.SetViewport( + 0, 0, 0.f, + DisplacementMapResolution.X, DisplacementMapResolution.Y, 1.f); + + // Get shaders. + FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(FeatureLevel); + TShaderMapRef< FCubismSepVS > VertexShader(GlobalShaderMap); + TShaderMapRef< FCubismSepPS > PixelShader(GlobalShaderMap); + + // Set the graphic pipeline state. + FGraphicsPipelineStateInitializer GraphicsPSOInit; + RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); + GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); + GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); + GraphicsPSOInit.RasterizerState = TStaticRasterizerState::GetRHI(); + GraphicsPSOInit.PrimitiveType = PT_TriangleList; + GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GCubismVertexDeclaration.VertexDeclarationRHI; + + + // Update viewport. + RHICmdList.SetViewport( + 0, 0, 0.f, + OutTextureRenderTargetResource->GetSizeX(), OutTextureRenderTargetResource->GetSizeY(), 1.f); + + // Shader + GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); + GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); + + SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); + + FVector4f ts_FakeBase = FVector4f(1.0f, 1.0f, 1.0f, 1.0f); + VertexShader->SetParameters(RHICmdList, VertexShader.GetVertexShader(), ts_FakeBase, tp_States->MaskBuffer->GetTexture2D()); + PixelShader->SetParameters(RHICmdList, PixelShader.GetPixelShader(), ts_FakeBase, tp_States->MaskBuffer->GetTexture2D()); + + // Texture + //UTexture2D* tp_Texture = tp_States->Textures[0]; + //FTextureRHIRef tsr_TextureRHI = tp_Texture->Resource->TextureRHI; + //PixelShader->SetInTexture(RHICmdList, tsr_TextureRHI); + + + RHICmdList.SetStreamSource(0, GCubismVertexScreenBuffer.VertexBufferRHI, 0); + RHICmdList.DrawIndexedPrimitive(GTwoTrianglesIndexBuffer.IndexBufferRHI, 0, 0, 4, 0, 2, 1); + } + RHICmdList.EndRenderPass(); +} + + +``` + +`Source/UnrealLive2D/Private/Draw/ModelRenders_Mask.cpp`: + +```cpp +#include "ModelRenders.h" +#include "FWPort/ModelRender.h" +#include "Model/CubismModel.hpp" +#include "CubismSepRender.h" +#include "GlobalShader.h" +#include "RHIStaticStates.h" +#include "RHIResources.h" + +#include "UnrealLive2D.h" +#include "PipelineStateCache.h" +#include "Engine/Texture2D.h" +#include "ShaderParameterUtils.h" +#include "DataDrivenShaderPlatformInfo.h" + + +////////////////////////////////////////////////////////////////////////// +class FCubismMaskShader : public FGlobalShader +{ + DECLARE_INLINE_TYPE_LAYOUT(FCubismMaskShader, NonVirtual);; + +public: + static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) + { + return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5); + } + + static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) + { + FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); + } + + FCubismMaskShader() {} + + FCubismMaskShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer) + : FGlobalShader(Initializer) + { + TestFloat.Bind(Initializer.ParameterMap, TEXT("TestFloat")); + ProjectMatrix.Bind(Initializer.ParameterMap, TEXT("ProjectMatrix")); + BaseColor.Bind(Initializer.ParameterMap, TEXT("BaseColor")); + ChannelFlag.Bind(Initializer.ParameterMap, TEXT("ChannelFlag")); + + MainTexture.Bind(Initializer.ParameterMap, TEXT("MainTexture")); + MainTextureSampler.Bind(Initializer.ParameterMap, TEXT("MainTextureSampler")); + } + + template + void SetParameters( + FRHICommandListImmediate& RHICmdList, + const TShaderRHIParamRef ShaderRHI, + const FMatrix44f& InProjectMatrix, + const FVector4f& InBaseColor, + const FVector4f& InChannelFlag, + FTextureRHIRef ShaderResourceTexture + ) + { + FRHIBatchedShaderParameters& BatchedParameters = RHICmdList.GetScratchShaderParameters(); + + SetShaderValue(BatchedParameters, TestFloat, 1.0f); + SetShaderValue(BatchedParameters, ProjectMatrix, InProjectMatrix); + SetShaderValue(BatchedParameters, BaseColor, InBaseColor); + SetShaderValue(BatchedParameters, ChannelFlag, InChannelFlag); + + SetTextureParameter(BatchedParameters, MainTexture, ShaderResourceTexture); + SetSamplerParameter(BatchedParameters, MainTextureSampler, TStaticSamplerState::GetRHI()); + + RHICmdList.SetBatchedShaderParameters(ShaderRHI, BatchedParameters); + } + +private: + LAYOUT_FIELD(FShaderParameter, TestFloat); + LAYOUT_FIELD(FShaderParameter, ProjectMatrix); + LAYOUT_FIELD(FShaderParameter, BaseColor); + LAYOUT_FIELD(FShaderParameter, ChannelFlag); + + LAYOUT_FIELD(FShaderResourceParameter, MainTexture); + LAYOUT_FIELD(FShaderResourceParameter, MainTextureSampler); +}; + +class FCubismMaskVS : public FCubismMaskShader +{ + DECLARE_SHADER_TYPE(FCubismMaskVS, Global); + +public: + + /** Default constructor. */ + FCubismMaskVS() {} + + /** Initialization constructor. */ + FCubismMaskVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) + : FCubismMaskShader(Initializer) + { + } + +private: +}; + +class FCubismMaskPS : public FCubismMaskShader +{ + DECLARE_SHADER_TYPE(FCubismMaskPS, Global); + +public: + + /** Default constructor. */ + FCubismMaskPS() {} + + /** Initialization constructor. */ + FCubismMaskPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) + : FCubismMaskShader(Initializer) + { + + } +}; + +IMPLEMENT_SHADER_TYPE(, FCubismMaskVS, TEXT("/Plugin/UnrealLive2D/Private/CubismMask.usf"), TEXT("MainVS"), SF_Vertex) +IMPLEMENT_SHADER_TYPE(, FCubismMaskPS, TEXT("/Plugin/UnrealLive2D/Private/CubismMask.usf"), TEXT("MainPS"), SF_Pixel) + +////////////////////////////////////////////////////////////////////////// + + +void FillMaskParameter(CubismClippingContext* clipContext, CubismClippingManager_UE* _clippingManager, FMatrix44f& ts_MartixForMask, FVector4f& ts_BaseColor, FVector4f& ts_ChanelFlag) +{ + // チャンネル + const csmInt32 channelNo = clipContext->_layoutChannelNo; + // チャンネルをRGBAに変換 + Csm::Rendering::CubismRenderer::CubismTextureColor* colorChannel = _clippingManager->GetChannelFlagAsColor(channelNo); + + csmRectF* rect = clipContext->_layoutBounds; + + ts_MartixForMask = FModelRenders::ConvertCubismMatrix(clipContext->_matrixForMask); + ts_BaseColor = FVector4f(rect->X * 2.0f - 1.0f, rect->Y * 2.0f - 1.0f, rect->GetRight() * 2.0f - 1.0f, rect->GetBottom() * 2.0f - 1.0f); + ts_ChanelFlag = FVector4f(colorChannel->R, colorChannel->G, colorChannel->B, colorChannel->A); +} + +void FModelRenders::RenderMask_Full( + FCubismRenderState* tp_States, + FRHICommandListImmediate& RHICmdList, + CubismClippingManager_UE* _clippingManager, + ERHIFeatureLevel::Type FeatureLevel, + Csm::CubismModel* tp_Model +) +{ + ////////////////////////////////////////////////////////////////////////// + FRHITexture* RenderTargetTexture = tp_States->MaskBuffer; + + RHICmdList.Transition(MakeArrayView(FModelRenders::ConvertTransitionResource(FExclusiveDepthStencil::DepthWrite_StencilWrite, RenderTargetTexture))); + + FRHIRenderPassInfo RPInfo(RenderTargetTexture, ERenderTargetActions::Clear_Store); + RHICmdList.BeginRenderPass(RPInfo, TEXT("DrawMask01")); + { + + // Update viewport. + float tf_MaskSize = _clippingManager->_clippingMaskBufferSize; + RHICmdList.SetViewport( + 0, 0, 0.f, + tf_MaskSize, tf_MaskSize, 1.f); + + // Get shaders. + FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(FeatureLevel); + TShaderMapRef< FCubismMaskVS > VertexShader(GlobalShaderMap); + TShaderMapRef< FCubismMaskPS > PixelShader(GlobalShaderMap); + + //PixelShader->SetMaskParameter(RHICmdList, ts_Temp, FVector4(1, 1, 1, 1), FVector4(1, 1, 1, 1), nullptr); + + // Set the graphic pipeline state. + FGraphicsPipelineStateInitializer GraphicsPSOInit; + RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); + GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); + GraphicsPSOInit.BlendState = TStaticBlendState::GetRHI(); + GraphicsPSOInit.RasterizerState = TStaticRasterizerState::GetRHI(); + GraphicsPSOInit.PrimitiveType = PT_TriangleList; + GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GCubismVertexDeclaration.VertexDeclarationRHI; + GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); + GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); + + ////////////////////////////////////////////////////////////////////////// + for (csmUint32 clipIndex = 0; clipIndex < _clippingManager->_clippingContextListForMask.GetSize(); clipIndex++) + { + CubismClippingContext* clipContext = _clippingManager->_clippingContextListForMask[clipIndex]; + + const csmInt32 clipDrawCount = clipContext->_clippingIdCount; + for (csmInt32 i = 0; i < clipDrawCount; i++) + { + const csmInt32 clipDrawIndex = clipContext->_clippingIdList[i]; + + // 頂点情報が更新されておらず、信頼性がない場合は描画をパスする + if (!tp_Model->GetDrawableDynamicFlagVertexPositionsDidChange(clipDrawIndex)) + { + continue; + } + + // TODO: push [IsCulling] to [RasterizerState] if needed + const bool tb_IsCulling = (tp_Model->GetDrawableCulling(clipDrawIndex) != 0); + + const csmInt32 td_TextrueNo = tp_Model->GetDrawableTextureIndices(clipDrawIndex); + UTexture2D* tp_Texture = nullptr; + if (tp_States->Textures.IsValidIndex(td_TextrueNo)) + { + tp_Texture = tp_States->Textures[td_TextrueNo]; + } + + if (!IsValid(tp_Texture)) + { + UE_LOG(LogCubism, Warning, TEXT("DrawSeparateToRenderTarget_RenderThread:[Mask] Texture %d invalid"), td_TextrueNo); + continue; + } + + const csmInt32 indexCount = tp_Model->GetDrawableVertexIndexCount(clipDrawIndex); + if (0 == indexCount) + { + continue; + } + + if (!tp_States->IndexBuffers.Contains(clipDrawIndex)) + { + continue; + } + + if (!tp_States->VertexBuffers.Contains(clipDrawIndex)) + { + UE_LOG(LogCubism, Error, TEXT("DrawSeparateToRenderTarget_RenderThread:[Mask] Vertext buffer not inited.")); + continue; + } + + ////////////////////////////////////////////////////////////////////////// + csmFloat32 tf_Opacity = tp_Model->GetDrawableOpacity(clipDrawIndex); + Rendering::CubismRenderer::CubismBlendMode ts_BlendMode = tp_Model->GetDrawableBlendMode(clipDrawIndex); + csmBool tb_InvertMask = tp_Model->GetDrawableInvertedMask(clipDrawIndex); + + + + ////////////////////////////////////////////////////////////////////////// + /** Drawable draw */ + const csmInt32 td_NumVertext = tp_Model->GetDrawableVertexCount(clipDrawIndex); + + FBufferRHIRef IndexBufferRHI = tp_States->IndexBuffers.FindRef(clipDrawIndex); + FBufferRHIRef ScratchVertexBufferRHI = tp_States->VertexBuffers.FindRef(clipDrawIndex); + + check(tp_Texture->GetResource()); + FTextureRHIRef tsr_TextureRHI = tp_Texture->GetResource()->TextureRHI; + + FillVertexBuffer(tp_Model, clipDrawIndex, ScratchVertexBufferRHI, tp_States, RHICmdList); + + //////////////////////////////////////////////////////////////////////////// + SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); + + FMatrix44f ts_MartixForMask; + FVector4f ts_BaseColor; + FVector4f ts_ChanelFlag; + + FillMaskParameter(clipContext, _clippingManager, ts_MartixForMask, ts_BaseColor, ts_ChanelFlag); + + VertexShader->SetParameters(RHICmdList, VertexShader.GetVertexShader(), ts_MartixForMask, ts_BaseColor, ts_ChanelFlag, tsr_TextureRHI); + PixelShader->SetParameters(RHICmdList, PixelShader.GetPixelShader(), ts_MartixForMask, ts_BaseColor, ts_ChanelFlag, tsr_TextureRHI); + + //////////////////////////////////////////////////////////////////////////// + + RHICmdList.SetStreamSource(0, ScratchVertexBufferRHI, 0); + + RHICmdList.DrawIndexedPrimitive( + IndexBufferRHI, + /*BaseVertexIndex=*/ 0, + /*MinIndex=*/ 0, + /*NumVertices=*/ td_NumVertext, + /*StartIndex=*/ 0, + /*NumPrimitives=*/ indexCount / 3, + /*NumInstances=*/ 1 + ); + + + + } + } + + //FMatrix ts_Temp = FMatrix::Identity; + //VertexShader->SetParameters(RHICmdList, VertexShader->GetVertexShader(), ts_Temp); + //PixelShader->SetParameters(RHICmdList, PixelShader->GetPixelShader(), ts_Temp); + //SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit); + + //RHICmdList.SetStreamSource(0, GCubismVertexScreenBuffer.VertexBufferRHI, 0); + //RHICmdList.DrawIndexedPrimitive(GTwoTrianglesIndexBuffer.IndexBufferRHI, 0, 0, 4, 0, 2, 1); + + } + RHICmdList.EndRenderPass(); +} + +void FModelRenders::RenderMask_Single( + FCubismRenderState* tp_States, + FRHICommandListImmediate& RHICmdList, + ERHIFeatureLevel::Type FeatureLevel, + Csm::CubismModel* tp_Model, + class CubismClippingContext* clipContext +) +{ + CubismClippingManager_UE* _clippingManager = tp_States->_ClippingManager.Get(); + check(_clippingManager); + + ////////////////////////////////////////////////////////////////////////// + FRHITexture* RenderTargetTexture = tp_States->MaskBuffer; + + RHICmdList.Transition(MakeArrayView(FModelRenders::ConvertTransitionResource(FExclusiveDepthStencil::DepthWrite_StencilWrite, RenderTargetTexture))); + + FRHIRenderPassInfo RPInfo(RenderTargetTexture, ERenderTargetActions::Clear_Store); + RHICmdList.BeginRenderPass(RPInfo, TEXT("DrawMask02")); + { + + // Update viewport. + float tf_MaskSize = _clippingManager->_clippingMaskBufferSize; + RHICmdList.SetViewport( + 0, 0, 0.f, + tf_MaskSize, tf_MaskSize, 1.f); + + // Get shaders. + FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(FeatureLevel); + TShaderMapRef< FCubismMaskVS > VertexShader(GlobalShaderMap); + TShaderMapRef< FCubismMaskPS > PixelShader(GlobalShaderMap); + + //PixelShader->SetMaskParameter(RHICmdList, ts_Temp, FVector4(1, 1, 1, 1), FVector4(1, 1, 1, 1), nullptr); + + // Set the graphic pipeline state. + FGraphicsPipelineStateInitializer GraphicsPSOInit; + RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); + GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); + GraphicsPSOInit.BlendState = TStaticBlendState::GetRHI(); + GraphicsPSOInit.RasterizerState = TStaticRasterizerState::GetRHI(); + GraphicsPSOInit.PrimitiveType = PT_TriangleList; + GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GCubismVertexDeclaration.VertexDeclarationRHI; + GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); + GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); + + ////////////////////////////////////////////////////////////////////////// + + const csmInt32 clipDrawCount = clipContext->_clippingIdCount; + for (csmInt32 i = 0; i < clipDrawCount; i++) + { + const csmInt32 clipDrawIndex = clipContext->_clippingIdList[i]; + + // 頂点情報が更新されておらず、信頼性がない場合は描画をパスする + if (!tp_Model->GetDrawableDynamicFlagVertexPositionsDidChange(clipDrawIndex)) + { + continue; + } + + // TODO: push [IsCulling] to [RasterizerState] if needed + const bool tb_IsCulling = (tp_Model->GetDrawableCulling(clipDrawIndex) != 0); + + const csmInt32 td_TextrueNo = tp_Model->GetDrawableTextureIndices(clipDrawIndex); + UTexture2D* tp_Texture = nullptr; + if (tp_States->Textures.IsValidIndex(td_TextrueNo)) + { + tp_Texture = tp_States->Textures[td_TextrueNo]; + } + + if (!IsValid(tp_Texture)) + { + UE_LOG(LogCubism, Warning, TEXT("DrawSeparateToRenderTarget_RenderThread:[Mask] Texture %d invalid"), td_TextrueNo); + continue; + } + + const csmInt32 indexCount = tp_Model->GetDrawableVertexIndexCount(clipDrawIndex); + if (0 == indexCount) + { + continue; + } + + if (!tp_States->IndexBuffers.Contains(clipDrawIndex)) + { + continue; + } + + if (!tp_States->VertexBuffers.Contains(clipDrawIndex)) + { + UE_LOG(LogCubism, Error, TEXT("DrawSeparateToRenderTarget_RenderThread:[Mask] Vertext buffer not inited.")); + continue; + } + + ////////////////////////////////////////////////////////////////////////// + /** Drawable draw */ + const csmInt32 td_NumVertext = tp_Model->GetDrawableVertexCount(clipDrawIndex); + + FBufferRHIRef IndexBufferRHI = tp_States->IndexBuffers.FindRef(clipDrawIndex); + FBufferRHIRef ScratchVertexBufferRHI = tp_States->VertexBuffers.FindRef(clipDrawIndex); + + check(tp_Texture->GetResource()); + FTextureRHIRef tsr_TextureRHI = tp_Texture->GetResource()->TextureRHI; + + FillVertexBuffer(tp_Model, clipDrawIndex, ScratchVertexBufferRHI, tp_States, RHICmdList); + + //////////////////////////////////////////////////////////////////////////// + + FMatrix44f ts_MartixForMask; + FVector4f ts_BaseColor; + FVector4f ts_ChanelFlag; + + FillMaskParameter(clipContext, _clippingManager, ts_MartixForMask, ts_BaseColor, ts_ChanelFlag); + + VertexShader->SetParameters(RHICmdList, VertexShader.GetVertexShader(), ts_MartixForMask, ts_BaseColor, ts_ChanelFlag, tsr_TextureRHI); + PixelShader->SetParameters(RHICmdList, PixelShader.GetPixelShader(), ts_MartixForMask, ts_BaseColor, ts_ChanelFlag, tsr_TextureRHI); + SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit,0); + + //////////////////////////////////////////////////////////////////////////// + + RHICmdList.SetStreamSource(0, ScratchVertexBufferRHI, 0); + + RHICmdList.DrawIndexedPrimitive( + IndexBufferRHI, + /*BaseVertexIndex=*/ 0, + /*MinIndex=*/ 0, + /*NumVertices=*/ td_NumVertext, + /*StartIndex=*/ 0, + /*NumPrimitives=*/ indexCount / 3, + /*NumInstances=*/ 1 + ); + + + + } + + } + RHICmdList.EndRenderPass(); +} + +``` + +`Source/UnrealLive2D/Private/FWPort/CubismFrameWorkAllocator.cpp`: + +```cpp +#include "CubismFrameWorkAllocator.h" + +using namespace Csm; + +void* CubismFrameWorkAllocator::Allocate(const csmSizeType size) +{ + return malloc(size); +} + +void CubismFrameWorkAllocator::Deallocate(void* memory) +{ + free(memory); +} + +void* CubismFrameWorkAllocator::AllocateAligned(const csmSizeType size, const csmUint32 alignment) +{ + size_t offset, shift, alignedAddress; + void* allocation; + void** preamble; + + offset = alignment - 1 + sizeof(void*); + + allocation = Allocate(size + static_cast(offset)); + + alignedAddress = reinterpret_cast(allocation) + sizeof(void*); + + shift = alignedAddress % alignment; + + if (shift) + { + alignedAddress += (alignment - shift); + } + + preamble = reinterpret_cast(alignedAddress); + preamble[-1] = allocation; + + return reinterpret_cast(alignedAddress); +} + +void CubismFrameWorkAllocator::DeallocateAligned(void* alignedMemory) +{ + void** preamble; + + preamble = static_cast(alignedMemory); + + Deallocate(preamble[-1]); +} + +``` + +`Source/UnrealLive2D/Private/FWPort/CubismFrameWorkAllocator.h`: + +```h +#pragma once + +#include +#include + +class CubismFrameWorkAllocator : public Csm::ICubismAllocator +{ + void* Allocate(const Csm::csmSizeType size); + + void Deallocate(void* memory); + + void* AllocateAligned(const Csm::csmSizeType size, const Csm::csmUint32 alignment); + + void DeallocateAligned(void* alignedMemory); +}; +``` + +`Source/UnrealLive2D/Private/FWPort/ModelRender.cpp`: + +```cpp +#include "ModelRender.h" +#include "Model/CubismModel.hpp" +#include "Draw/CubismSepRender.h" + +/********************************************************************************************************************* +* CubismClippingManager_UE +********************************************************************************************************************/ +///< ファイルスコープの変数宣言 +namespace { + const csmInt32 ColorChannelCount = 4; ///< 実験時に1チャンネルの場合は1、RGBだけの場合は3、アルファも含める場合は4 +} + +CubismClippingManager_UE::CubismClippingManager_UE() + : _currentFrameNo(0) + , _clippingMaskBufferSize(256) +{ + CubismRenderer::CubismTextureColor* tmp = NULL; + tmp = CSM_NEW CubismRenderer::CubismTextureColor(); + tmp->R = 1.0f; + tmp->G = 0.0f; + tmp->B = 0.0f; + tmp->A = 0.0f; + _channelColors.PushBack(tmp); + tmp = CSM_NEW CubismRenderer::CubismTextureColor(); + tmp->R = 0.0f; + tmp->G = 1.0f; + tmp->B = 0.0f; + tmp->A = 0.0f; + _channelColors.PushBack(tmp); + tmp = CSM_NEW CubismRenderer::CubismTextureColor(); + tmp->R = 0.0f; + tmp->G = 0.0f; + tmp->B = 1.0f; + tmp->A = 0.0f; + _channelColors.PushBack(tmp); + tmp = CSM_NEW CubismRenderer::CubismTextureColor(); + tmp->R = 0.0f; + tmp->G = 0.0f; + tmp->B = 0.0f; + tmp->A = 1.0f; + _channelColors.PushBack(tmp); + +} + +CubismClippingManager_UE::~CubismClippingManager_UE() +{ + for (csmUint32 i = 0; i < _clippingContextListForMask.GetSize(); i++) + { + if (_clippingContextListForMask[i]) CSM_DELETE_SELF(CubismClippingContext, _clippingContextListForMask[i]); + _clippingContextListForMask[i] = NULL; + } + + // _clippingContextListForDrawは_clippingContextListForMaskにあるインスタンスを指している。上記の処理により要素ごとのDELETEは不要。 + for (csmUint32 i = 0; i < _clippingContextListForDraw.GetSize(); i++) + { + _clippingContextListForDraw[i] = NULL; + } + + for (csmUint32 i = 0; i < _channelColors.GetSize(); i++) + { + if (_channelColors[i]) CSM_DELETE(_channelColors[i]); + _channelColors[i] = NULL; + } +} + +void CubismClippingManager_UE::Initialize(CubismModel& model, csmInt32 drawableCount, const csmInt32** drawableMasks, const csmInt32* drawableMaskCounts) +{ + //クリッピングマスクを使う描画オブジェクトを全て登録する + //クリッピングマスクは、通常数個程度に限定して使うものとする + for (csmInt32 i = 0; i < drawableCount; i++) + { + if (drawableMaskCounts[i] <= 0) + { + //クリッピングマスクが使用されていないアートメッシュ(多くの場合使用しない) + _clippingContextListForDraw.PushBack(NULL); + continue; + } + + // 既にあるClipContextと同じかチェックする + CubismClippingContext* cc = FindSameClip(drawableMasks[i], drawableMaskCounts[i]); + if (cc == NULL) + { + // 同一のマスクが存在していない場合は生成する + cc = CSM_NEW CubismClippingContext(this, drawableMasks[i], drawableMaskCounts[i]); + _clippingContextListForMask.PushBack(cc); + } + + cc->AddClippedDrawable(i); + + _clippingContextListForDraw.PushBack(cc); + } +} + +CubismClippingContext* CubismClippingManager_UE::FindSameClip(const csmInt32* drawableMasks, csmInt32 drawableMaskCounts) const +{ + // 作成済みClippingContextと一致するか確認 + for (csmUint32 i = 0; i < _clippingContextListForMask.GetSize(); i++) + { + CubismClippingContext* cc = _clippingContextListForMask[i]; + const csmInt32 count = cc->_clippingIdCount; + if (count != drawableMaskCounts) continue; //個数が違う場合は別物 + csmInt32 samecount = 0; + + // 同じIDを持つか確認。配列の数が同じなので、一致した個数が同じなら同じ物を持つとする。 + for (csmInt32 j = 0; j < count; j++) + { + const csmInt32 clipId = cc->_clippingIdList[j]; + for (csmInt32 k = 0; k < count; k++) + { + if (drawableMasks[k] == clipId) + { + samecount++; + break; + } + } + } + if (samecount == count) + { + return cc; + } + } + return NULL; //見つからなかった +} + +void CubismClippingManager_UE::SetupClippingContext(CubismModel& model, struct FCubismRenderState* tp_RenderState) +{ + _currentFrameNo++; + + // 全てのクリッピングを用意する + // 同じクリップ(複数の場合はまとめて1つのクリップ)を使う場合は1度だけ設定する + csmInt32 usingClipCount = 0; + for (csmUint32 clipIndex = 0; clipIndex < _clippingContextListForMask.GetSize(); clipIndex++) + { + // 1つのクリッピングマスクに関して + CubismClippingContext* cc = _clippingContextListForMask[clipIndex]; + + // このクリップを利用する描画オブジェクト群全体を囲む矩形を計算 + CalcClippedDrawTotalBounds(model, cc); + + if (cc->_isUsing) + { + usingClipCount++; //使用中としてカウント + } + } + + // マスク作成処理 + if (usingClipCount > 0) + { + //if (!renderer->IsUsingHighPrecisionMask()) + //{ + // // ビューポートは退避済み + // // 生成したFrameBufferと同じサイズでビューポートを設定 + // CubismRenderer_D3D11::GetRenderStateManager()->SetViewport(renderContext, + // 0, + // 0, + // static_cast(_clippingMaskBufferSize), + // static_cast(_clippingMaskBufferSize), + // 0.0f, 1.0f); + + // useTarget.BeginDraw(renderContext); + // // 1が無効(描かれない)領域、0が有効(描かれる)領域。(シェーダで Cd*Csで0に近い値をかけてマスクを作る。1をかけると何も起こらない) + // useTarget.Clear(renderContext, 1.0f, 1.0f, 1.0f, 1.0f); + //} + + // 各マスクのレイアウトを決定していく + const bool tb_SetupGood = SetupLayoutBounds(tp_RenderState->Get_UseHighPreciseMask() ? 0 : usingClipCount); + if (!tb_SetupGood) + { + tp_RenderState->NoLowPreciseMask(true); + } + + // 実際にマスクを生成する + // 全てのマスクをどの様にレイアウトして描くかを決定し、ClipContext , ClippedDrawContext に記憶する + for (csmUint32 clipIndex = 0; clipIndex < _clippingContextListForMask.GetSize(); clipIndex++) + { + // --- 実際に1つのマスクを描く --- + CubismClippingContext* clipContext = _clippingContextListForMask[clipIndex]; + csmRectF* allClippedDrawRect = clipContext->_allClippedDrawRect; //このマスクを使う、全ての描画オブジェクトの論理座標上の囲み矩形 + csmRectF* layoutBoundsOnTex01 = clipContext->_layoutBounds; //この中にマスクを収める + + // モデル座標上の矩形を、適宜マージンを付けて使う + const csmFloat32 MARGIN = 0.05f; + _tmpBoundsOnModel.SetRect(allClippedDrawRect); + _tmpBoundsOnModel.Expand(allClippedDrawRect->Width * MARGIN, allClippedDrawRect->Height * MARGIN); + //########## 本来は割り当てられた領域の全体を使わず必要最低限のサイズがよい + + // シェーダ用の計算式を求める。回転を考慮しない場合は以下のとおり + // movePeriod' = movePeriod * scaleX + offX [[ movePeriod' = (movePeriod - tmpBoundsOnModel.movePeriod)*scale + layoutBoundsOnTex01.movePeriod ]] + const csmFloat32 scaleX = layoutBoundsOnTex01->Width / _tmpBoundsOnModel.Width; + const csmFloat32 scaleY = layoutBoundsOnTex01->Height / _tmpBoundsOnModel.Height; + + // マスク生成時に使う行列を求める + { + // シェーダに渡す行列を求める <<<<<<<<<<<<<<<<<<<<<<<< 要最適化(逆順に計算すればシンプルにできる) + _tmpMatrix.LoadIdentity(); + { + // Layout0..1 を -1..1に変換 + _tmpMatrix.TranslateRelative(-1.0f, -1.0f); + _tmpMatrix.ScaleRelative(2.0f, 2.0f); + } + { + // view to Layout0..1 + _tmpMatrix.TranslateRelative(layoutBoundsOnTex01->X, layoutBoundsOnTex01->Y); //new = [translate] + _tmpMatrix.ScaleRelative(scaleX, scaleY); //new = [translate][scale] + _tmpMatrix.TranslateRelative(-_tmpBoundsOnModel.X, -_tmpBoundsOnModel.Y); + //new = [translate][scale][translate] + } + // tmpMatrixForMask が計算結果 + _tmpMatrixForMask.SetMatrix(_tmpMatrix.GetArray()); + } + + //--------- draw時の mask 参照用行列を計算 + { + // シェーダに渡す行列を求める <<<<<<<<<<<<<<<<<<<<<<<< 要最適化(逆順に計算すればシンプルにできる) + _tmpMatrix.LoadIdentity(); + { + _tmpMatrix.TranslateRelative(layoutBoundsOnTex01->X, layoutBoundsOnTex01->Y); //new = [translate] + // 上下反転 + _tmpMatrix.ScaleRelative(scaleX, scaleY * -1.0f); //new = [translate][scale] + _tmpMatrix.TranslateRelative(-_tmpBoundsOnModel.X, -_tmpBoundsOnModel.Y); + //new = [translate][scale][translate] + } + + _tmpMatrixForDraw.SetMatrix(_tmpMatrix.GetArray()); + } + + clipContext->_matrixForMask.SetMatrix(_tmpMatrixForMask.GetArray()); + + clipContext->_matrixForDraw.SetMatrix(_tmpMatrixForDraw.GetArray()); + + //if (!renderer->IsUsingHighPrecisionMask()) + //{ + // const csmInt32 clipDrawCount = clipContext->_clippingIdCount; + // for (csmInt32 i = 0; i < clipDrawCount; i++) + // { + // const csmInt32 clipDrawIndex = clipContext->_clippingIdList[i]; + + // // 頂点情報が更新されておらず、信頼性がない場合は描画をパスする + // if (!model.GetDrawableDynamicFlagVertexPositionsDidChange(clipDrawIndex)) + // { + // continue; + // } + + // //renderer->IsCulling(model.GetDrawableCulling(clipDrawIndex) != 0); + + // // 今回専用の変換を適用して描く + // // チャンネルも切り替える必要がある(A,R,G,B) + // //renderer->SetClippingContextBufferForMask(clipContext); + // //renderer->DrawMeshDX11(clipDrawIndex, + // // model.GetDrawableTextureIndices(clipDrawIndex), + // // model.GetDrawableVertexIndexCount(clipDrawIndex), + // // model.GetDrawableVertexCount(clipDrawIndex), + // // const_cast(model.GetDrawableVertexIndices(clipDrawIndex)), + // // const_cast(model.GetDrawableVertices(clipDrawIndex)), + // // reinterpret_cast(const_cast(model.GetDrawableVertexUvs(clipDrawIndex))), + // // model.GetDrawableOpacity(clipDrawIndex), + // // CubismRenderer::CubismBlendMode::CubismBlendMode_Normal, //クリッピングは通常描画を強制 + // // false // マスク生成時はクリッピングの反転使用は全く関係がない + // //); + // } + //} + //else + //{ + // // NOP このモードの際はチャンネルを分けず、マトリクスの計算だけをしておいて描画自体は本体描画直前で行う + //} + } + + //if (!renderer->IsUsingHighPrecisionMask()) + //{ + // useTarget.EndDraw(renderContext); + + // renderer->SetClippingContextBufferForMask(NULL); + //} + } +} + +void CubismClippingManager_UE::CalcClippedDrawTotalBounds(CubismModel& model, CubismClippingContext* clippingContext) +{ + // 被クリッピングマスク(マスクされる描画オブジェクト)の全体の矩形 + csmFloat32 clippedDrawTotalMinX = FLT_MAX, clippedDrawTotalMinY = FLT_MAX; + csmFloat32 clippedDrawTotalMaxX = FLT_MIN, clippedDrawTotalMaxY = FLT_MIN; + + // このマスクが実際に必要か判定する + // このクリッピングを利用する「描画オブジェクト」がひとつでも使用可能であればマスクを生成する必要がある + + const csmInt32 clippedDrawCount = clippingContext->_clippedDrawableIndexList->GetSize(); + for (csmInt32 clippedDrawableIndex = 0; clippedDrawableIndex < clippedDrawCount; clippedDrawableIndex++) + { + // マスクを使用する描画オブジェクトの描画される矩形を求める + const csmInt32 drawableIndex = (*clippingContext->_clippedDrawableIndexList)[clippedDrawableIndex]; + + const csmInt32 drawableVertexCount = model.GetDrawableVertexCount(drawableIndex); + const csmFloat32* drawableVertexes = const_cast(model.GetDrawableVertices(drawableIndex)); + + csmFloat32 minX = FLT_MAX, minY = FLT_MAX; + csmFloat32 maxX = FLT_MIN, maxY = FLT_MIN; + + csmInt32 loop = drawableVertexCount * Constant::VertexStep; + for (csmInt32 pi = Constant::VertexOffset; pi < loop; pi += Constant::VertexStep) + { + csmFloat32 x = drawableVertexes[pi]; + csmFloat32 y = drawableVertexes[pi + 1]; + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + } + + // + if (minX == FLT_MAX) continue; //有効な点がひとつも取れなかったのでスキップする + + // 全体の矩形に反映 + if (minX < clippedDrawTotalMinX) clippedDrawTotalMinX = minX; + if (minY < clippedDrawTotalMinY) clippedDrawTotalMinY = minY; + if (maxX > clippedDrawTotalMaxX) clippedDrawTotalMaxX = maxX; + if (maxY > clippedDrawTotalMaxY) clippedDrawTotalMaxY = maxY; + } + if (clippedDrawTotalMinX == FLT_MAX) + { + clippingContext->_allClippedDrawRect->X = 0.0f; + clippingContext->_allClippedDrawRect->Y = 0.0f; + clippingContext->_allClippedDrawRect->Width = 0.0f; + clippingContext->_allClippedDrawRect->Height = 0.0f; + clippingContext->_isUsing = false; + } + else + { + clippingContext->_isUsing = true; + csmFloat32 w = clippedDrawTotalMaxX - clippedDrawTotalMinX; + csmFloat32 h = clippedDrawTotalMaxY - clippedDrawTotalMinY; + clippingContext->_allClippedDrawRect->X = clippedDrawTotalMinX; + clippingContext->_allClippedDrawRect->Y = clippedDrawTotalMinY; + clippingContext->_allClippedDrawRect->Width = w; + clippingContext->_allClippedDrawRect->Height = h; + } +} + +bool CubismClippingManager_UE::SetupLayoutBounds(csmInt32 usingClipCount) const +{ + if (usingClipCount <= 0) + {// この場合は一つのマスクターゲットを毎回クリアして使用する + for (csmUint32 index = 0; index < _clippingContextListForMask.GetSize(); index++) + { + CubismClippingContext* cc = _clippingContextListForMask[index]; + cc->_layoutChannelNo = 0; // どうせ毎回消すので固定で良い + cc->_layoutBounds->X = 0.0f; + cc->_layoutBounds->Y = 0.0f; + cc->_layoutBounds->Width = 1.0f; + cc->_layoutBounds->Height = 1.0f; + } + return true; + } + + // ひとつのRenderTextureを極力いっぱいに使ってマスクをレイアウトする + // マスクグループの数が4以下ならRGBA各チャンネルに1つずつマスクを配置し、5以上6以下ならRGBAを2,2,1,1と配置する + + // RGBAを順番に使っていく。 + const csmInt32 div = usingClipCount / ColorChannelCount; //1チャンネルに配置する基本のマスク個数 + const csmInt32 mod = usingClipCount % ColorChannelCount; //余り、この番号のチャンネルまでに1つずつ配分する + + // RGBAそれぞれのチャンネルを用意していく(0:R , 1:G , 2:B, 3:A, ) + csmInt32 curClipIndex = 0; //順番に設定していくk + + for (csmInt32 channelNo = 0; channelNo < ColorChannelCount; channelNo++) + { + // このチャンネルにレイアウトする数 + const csmInt32 layoutCount = div + (channelNo < mod ? 1 : 0); + + // 分割方法を決定する + if (layoutCount == 0) + { + // 何もしない + } + else if (layoutCount == 1) + { + //全てをそのまま使う + CubismClippingContext* cc = _clippingContextListForMask[curClipIndex++]; + cc->_layoutChannelNo = channelNo; + cc->_layoutBounds->X = 0.0f; + cc->_layoutBounds->Y = 0.0f; + cc->_layoutBounds->Width = 1.0f; + cc->_layoutBounds->Height = 1.0f; + } + else if (layoutCount == 2) + { + for (csmInt32 i = 0; i < layoutCount; i++) + { + const csmInt32 xpos = i % 2; + + CubismClippingContext* cc = _clippingContextListForMask[curClipIndex++]; + cc->_layoutChannelNo = channelNo; + + cc->_layoutBounds->X = xpos * 0.5f; + cc->_layoutBounds->Y = 0.0f; + cc->_layoutBounds->Width = 0.5f; + cc->_layoutBounds->Height = 1.0f; + //UVを2つに分解して使う + } + } + else if (layoutCount <= 4) + { + //4分割して使う + for (csmInt32 i = 0; i < layoutCount; i++) + { + const csmInt32 xpos = i % 2; + const csmInt32 ypos = i / 2; + + CubismClippingContext* cc = _clippingContextListForMask[curClipIndex++]; + cc->_layoutChannelNo = channelNo; + + cc->_layoutBounds->X = xpos * 0.5f; + cc->_layoutBounds->Y = ypos * 0.5f; + cc->_layoutBounds->Width = 0.5f; + cc->_layoutBounds->Height = 0.5f; + } + } + else if (layoutCount <= 9) + { + //9分割して使う + for (csmInt32 i = 0; i < layoutCount; i++) + { + const csmInt32 xpos = i % 3; + const csmInt32 ypos = i / 3; + + CubismClippingContext* cc = _clippingContextListForMask[curClipIndex++]; + cc->_layoutChannelNo = channelNo; + + cc->_layoutBounds->X = xpos / 3.0f; + cc->_layoutBounds->Y = ypos / 3.0f; + cc->_layoutBounds->Width = 1.0f / 3.0f; + cc->_layoutBounds->Height = 1.0f / 3.0f; + } + } + else + { + CubismLogError("not supported mask count : %d", layoutCount); + + // 開発モードの場合は停止させる + CSM_ASSERT(0); + + // 引き続き実行する場合、 SetupShaderProgramでオーバーアクセスが発生するので仕方なく適当に入れておく + // もちろん描画結果はろくなことにならない + for (csmInt32 i = 0; i < layoutCount; i++) + { + CubismClippingContext* cc = _clippingContextListForMask[curClipIndex++]; + cc->_layoutChannelNo = 0; + cc->_layoutBounds->X = 0.0f; + cc->_layoutBounds->Y = 0.0f; + cc->_layoutBounds->Width = 1.0f; + cc->_layoutBounds->Height = 1.0f; + } + + return false; + } + } + + return true; +} + +CubismRenderer::CubismTextureColor* CubismClippingManager_UE::GetChannelFlagAsColor(csmInt32 channelNo) +{ + return _channelColors[channelNo]; +} + +//CubismOffscreenFrame_D3D11* CubismClippingManager_UE::GetColorBuffer() const +//{ +// return _colorBuffer; +//} + +csmVector* CubismClippingManager_UE::GetClippingContextListForDraw() +{ + return &_clippingContextListForDraw; +} + +void CubismClippingManager_UE::SetClippingMaskBufferSize(csmInt32 size) +{ + _clippingMaskBufferSize = size; +} + +csmInt32 CubismClippingManager_UE::GetClippingMaskBufferSize() const +{ + return _clippingMaskBufferSize; +} + +/********************************************************************************************************************* +* CubismClippingContext +********************************************************************************************************************/ +CubismClippingContext::CubismClippingContext(CubismClippingManager_UE* manager, const csmInt32* clippingDrawableIndices, csmInt32 clipCount) +{ + _isUsing = false; + + _owner = manager; + + // クリップしている(=マスク用の)Drawableのインデックスリスト + _clippingIdList = clippingDrawableIndices; + + // マスクの数 + _clippingIdCount = clipCount; + + _layoutChannelNo = 0; + + _allClippedDrawRect = CSM_NEW csmRectF(); + _layoutBounds = CSM_NEW csmRectF(); + + _clippedDrawableIndexList = CSM_NEW csmVector(); +} + +CubismClippingContext::~CubismClippingContext() +{ + if (_layoutBounds != NULL) + { + CSM_DELETE(_layoutBounds); + _layoutBounds = NULL; + } + + if (_allClippedDrawRect != NULL) + { + CSM_DELETE(_allClippedDrawRect); + _allClippedDrawRect = NULL; + } + + if (_clippedDrawableIndexList != NULL) + { + CSM_DELETE(_clippedDrawableIndexList); + _clippedDrawableIndexList = NULL; + } +} + +void CubismClippingContext::AddClippedDrawable(csmInt32 drawableIndex) +{ + _clippedDrawableIndexList->PushBack(drawableIndex); +} + +CubismClippingManager_UE* CubismClippingContext::GetClippingManager() +{ + return _owner; +} + + +``` + +`Source/UnrealLive2D/Private/FWPort/ModelRender.h`: + +```h +#include "Live2DCubismCore.hpp" +#include "Rendering/CubismRenderer.hpp" +#include "Type/csmRectF.hpp" +#include "Type/csmVector.hpp" + + + +/** + * This head file is not supposed to be include by other head. + * So we can use Csm directly + */ +using namespace Csm; +using Rendering::CubismRenderer; + + +class CubismClippingContext; + + +/** + * @brief クリッピングマスクの処理を実行するクラス + * + */ +class CubismClippingManager_UE +{ + + friend struct FCubismSepRender; + +public: + /** + * @brief コンストラクタ + */ + CubismClippingManager_UE(); + + /** + * @brief デストラクタ + */ + virtual ~CubismClippingManager_UE(); + +public: + + /** + * @brief カラーチャンネル(RGBA)のフラグを取得する + * + * @param[in] channelNo -> カラーチャンネル(RGBA)の番号(0:R , 1:G , 2:B, 3:A) + */ + CubismRenderer::CubismTextureColor* GetChannelFlagAsColor(csmInt32 channelNo); + + /** + * @brief マスクされる描画オブジェクト群全体を囲む矩形(モデル座標系)を計算する + * + * @param[in] model -> モデルのインスタンス + * @param[in] clippingContext -> クリッピングマスクのコンテキスト + */ + void CalcClippedDrawTotalBounds(CubismModel& model, CubismClippingContext* clippingContext); + + + + /** + * @brief マネージャの初期化処理
+ * クリッピングマスクを使う描画オブジェクトの登録を行う + * + * @param[in] model -> モデルのインスタンス + * @param[in] drawableCount -> 描画オブジェクトの数 + * @param[in] drawableMasks -> 描画オブジェクトをマスクする描画オブジェクトのインデックスのリスト + * @param[in] drawableMaskCounts -> 描画オブジェクトをマスクする描画オブジェクトの数 + */ + void Initialize(CubismModel& model, csmInt32 drawableCount, const csmInt32** drawableMasks, const csmInt32* drawableMaskCounts); + + /** + * @brief クリッピングコンテキストを作成する。モデル描画時に実行する。 + * + * @param[in] model -> モデルのインスタンス + * @param[in] renderer -> レンダラのインスタンス + */ + void SetupClippingContext(CubismModel& model, struct FCubismRenderState* renderer); + + /** + * @brief 既にマスクを作っているかを確認。
+ * 作っているようであれば該当するクリッピングマスクのインスタンスを返す。
+ * 作っていなければNULLを返す + * + * @param[in] drawableMasks -> 描画オブジェクトをマスクする描画オブジェクトのリスト + * @param[in] drawableMaskCounts -> 描画オブジェクトをマスクする描画オブジェクトの数 + * @return 該当するクリッピングマスクが存在すればインスタンスを返し、なければNULLを返す。 + */ + CubismClippingContext* FindSameClip(const csmInt32* drawableMasks, csmInt32 drawableMaskCounts) const; + + /** + * @brief クリッピングコンテキストを配置するレイアウト。
+ * ひとつのレンダーテクスチャを極力いっぱいに使ってマスクをレイアウトする。
+ * マスクグループの数が4以下ならRGBA各チャンネルに1つずつマスクを配置し、5以上6以下ならRGBAを2,2,1,1と配置する。 + * + * @param[in] usingClipCount -> 配置するクリッピングコンテキストの数 + */ + bool SetupLayoutBounds(csmInt32 usingClipCount) const; + + /** + * @breif カラーバッファのアドレスを取得する + * + * @return カラーバッファのアドレス + */ + //CubismOffscreenFrame_UE* GetColorBuffer() const; + + /** + * @brief 画面描画に使用するクリッピングマスクのリストを取得する + * + * @return 画面描画に使用するクリッピングマスクのリスト + */ + csmVector* GetClippingContextListForDraw(); + + /** + *@brief クリッピングマスクバッファのサイズを設定する + * + *@param size -> クリッピングマスクバッファのサイズ + * + */ + void SetClippingMaskBufferSize(csmInt32 size); + + /** + *@brief クリッピングマスクバッファのサイズを取得する + * + *@return クリッピングマスクバッファのサイズ + * + */ + csmInt32 GetClippingMaskBufferSize() const; + + //CubismOffscreenFrame_UE* _colorBuffer; ///< マスク用カラーバッファーのアドレス + csmInt32 _currentFrameNo; ///< マスクテクスチャに与えるフレーム番号 + + csmVector _channelColors; + + /** + * 这下面的两个数组是指向同样的数据 + * 只不过Mask是单独渲染的 + * Draw是跟着Order一起渲染的 + */ + csmVector _clippingContextListForMask; ///< マスク用クリッピングコンテキストのリスト + csmVector _clippingContextListForDraw; ///< 描画用クリッピングコンテキストのリスト + csmInt32 _clippingMaskBufferSize; ///< クリッピングマスクのバッファサイズ(初期値:256) + + CubismMatrix44 _tmpMatrix; ///< マスク計算用の行列 + CubismMatrix44 _tmpMatrixForMask; ///< マスク計算用の行列 + CubismMatrix44 _tmpMatrixForDraw; ///< マスク計算用の行列 + csmRectF _tmpBoundsOnModel; ///< マスク配置計算用の矩形 + +}; + +/** + * @brief クリッピングマスクのコンテキスト + */ +class CubismClippingContext +{ + friend class CubismClippingManager_UE; + +public: + /** + * @brief 引数付きコンストラクタ + * + */ + CubismClippingContext(CubismClippingManager_UE* manager, const csmInt32* clippingDrawableIndices, csmInt32 clipCount); + + /** + * @brief デストラクタ + */ + virtual ~CubismClippingContext(); + + /** + * @brief このマスクにクリップされる描画オブジェクトを追加する + * + * @param[in] drawableIndex -> クリッピング対象に追加する描画オブジェクトのインデックス + */ + void AddClippedDrawable(csmInt32 drawableIndex); + + /** + * @brief このマスクを管理するマネージャのインスタンスを取得する。 + * + * @return クリッピングマネージャのインスタンス + */ + CubismClippingManager_UE* GetClippingManager(); + + csmBool _isUsing; ///< 現在の描画状態でマスクの準備が必要ならtrue + const csmInt32* _clippingIdList; ///< クリッピングマスクのIDリスト + csmInt32 _clippingIdCount; ///< クリッピングマスクの数 + csmInt32 _layoutChannelNo; ///< RGBAのいずれのチャンネルにこのクリップを配置するか(0:R , 1:G , 2:B , 3:A) + csmRectF* _layoutBounds; ///< マスク用チャンネルのどの領域にマスクを入れるか(View座標-1..1, UVは0..1に直す) + csmRectF* _allClippedDrawRect; ///< このクリッピングで、クリッピングされる全ての描画オブジェクトの囲み矩形(毎回更新) + CubismMatrix44 _matrixForMask; ///< マスクの位置計算結果を保持する行列 + CubismMatrix44 _matrixForDraw; ///< 描画オブジェクトの位置計算結果を保持する行列 + csmVector* _clippedDrawableIndexList; ///< このマスクにクリップされる描画オブジェクトのリスト + + CubismClippingManager_UE* _owner; ///< このマスクを管理しているマネージャのインスタンス +}; +``` + +`Source/UnrealLive2D/Private/FWPort/RawModel.cpp`: + +```cpp +#include "RawModel.h" +#include "Misc/Paths.h" +#include "Misc/FileHelper.h" +#include "CubismModelSettingJson.hpp" +#include "CubismFramework.hpp" +#include "CubismDefaultParameterId.hpp" +#include "Id/CubismIdManager.hpp" +#include "Utils/CubismString.hpp" +#include "Motion/CubismMotion.hpp" + +#include "UnrealLive2D.h" + +using namespace Live2D::Cubism::Framework; + +// 外部定義ファイル(json)と合わせる +const csmChar* MotionGroupIdle = "Idle"; // アイドリング +const csmChar* MotionGroupTapBody = "TapBody"; // 体をタップしたとき + +// モーションの優先度定数 +const csmInt32 PriorityNone = 0; +const csmInt32 PriorityIdle = 1; +const csmInt32 PriorityNormal = 2; +const csmInt32 PriorityForce = 3; + +// 外部定義ファイル(json)と合わせる +const csmChar* HitAreaNameHead = "Head"; +const csmChar* HitAreaNameBody = "Body"; + +FRawModel::FRawModel() +{ + _idParamAngleX = CubismFramework::GetIdManager()->GetId(DefaultParameterId::ParamAngleX); + _idParamAngleY = CubismFramework::GetIdManager()->GetId(DefaultParameterId::ParamAngleY); + _idParamAngleZ = CubismFramework::GetIdManager()->GetId(DefaultParameterId::ParamAngleZ); + _idParamBodyAngleX = CubismFramework::GetIdManager()->GetId(DefaultParameterId::ParamBodyAngleX); + _idParamEyeBallX = CubismFramework::GetIdManager()->GetId(DefaultParameterId::ParamEyeBallX); + _idParamEyeBallY = CubismFramework::GetIdManager()->GetId(DefaultParameterId::ParamEyeBallY); +} + +FRawModel::~FRawModel() +{ + UE_LOG(LogCubism, Log, TEXT("FRawModel::~FRawModel")); + + ReleaseMotions(); + ReleaseExpressions(); + + // This seams not done in official sample + //for (csmInt32 i = 0; i < _modelSetting->GetMotionGroupCount(); i++) + //{ + // const csmChar* group = _modelSetting->GetMotionGroupName(i); + // ReleaseMotionGroup(group); + //} + + delete _modelSetting; +} + +bool FRawModel::LoadAsset(const FString& InPath) +{ + FString tstr_FileName; + FString tstr_FileExtentsion; + FPaths::Split(InPath, _HomeDir, tstr_FileName, tstr_FileExtentsion); + + UE_LOG(LogCubism, Log, TEXT("FRawModel::LoadAsset: Path info [%s]/[%s].[%s]"), + *_HomeDir, + *tstr_FileName, + *tstr_FileExtentsion + ); + + UE_LOG(LogCubism,Log, TEXT("%s"), *FPaths::ProjectPluginsDir()); + + + TArray tarr_FileData; + if (!FFileHelper::LoadFileToArray(tarr_FileData, *InPath)) + { + UE_LOG(LogCubism, Error, TEXT("FRawModel::LoadAsset: Read <%s> failed!"), *InPath); + return false; + } + + _modelSetting = new CubismModelSettingJson(tarr_FileData.GetData(), tarr_FileData.Num()); + //delete Reader; + + + csmString _modelHomeDir = TCHAR_TO_UTF8(*_HomeDir); + + + //Cubism Model + if (strcmp(_modelSetting->GetModelFileName(), "") != 0) + { + csmString path = _modelSetting->GetModelFileName(); + + FString tstr_ModelPath = _HomeDir / UTF8_TO_TCHAR(path.GetRawString()); + + TArray tarr_ModelFile; + const bool tb_ReadSuc = FFileHelper::LoadFileToArray(tarr_ModelFile, *tstr_ModelPath); + if (tb_ReadSuc) + { + LoadModel(tarr_ModelFile.GetData(), tarr_ModelFile.Num()); + } + + UE_LOG(LogCubism, Log, TEXT("FRawModel::LoadAsset: Read model <%s> res=%d"), + *tstr_ModelPath, + tb_ReadSuc ? 1 : 0 + ); + + UE_LOG(LogCubism, Log, TEXT("FRawModel::LoadAsset: %f, %f"), _model->GetCanvasWidth(), _model->GetCanvasHeight()); + + } + + //Expression + if (_modelSetting->GetExpressionCount() > 0) + { + const csmInt32 count = _modelSetting->GetExpressionCount(); + for (csmInt32 i = 0; i < count; i++) + { + csmString name = _modelSetting->GetExpressionName(i); + csmString path = _modelSetting->GetExpressionFileName(i); + + FString tstr_ExpressionPath = _HomeDir / UTF8_TO_TCHAR(path.GetRawString()); + + TArray tarr_ModelFile; + const bool tb_ReadSuc = FFileHelper::LoadFileToArray(tarr_ModelFile, *tstr_ExpressionPath); + if (tb_ReadSuc) + { + ACubismMotion* motion = LoadExpression(tarr_ModelFile.GetData(), tarr_ModelFile.Num(), name.GetRawString()); + + if (_expressions[name] != NULL) + { + ACubismMotion::Delete(_expressions[name]); + _expressions[name] = NULL; + } + _expressions[name] = motion; + + + UE_LOG(LogCubism, Log, TEXT("FRawModel::LoadAsset: Read expression <%s><%s> res=%d"), + *tstr_ExpressionPath, + UTF8_TO_TCHAR(name.GetRawString()), + tb_ReadSuc ? 1 : 0 + ); + } + } + + UE_LOG(LogCubism, Log, TEXT("FRawModel::LoadAsset: Expression readed %d"),count); + } + + //Physics + if (strcmp(_modelSetting->GetPhysicsFileName(), "") != 0) + { + csmString path = _modelSetting->GetPhysicsFileName(); + + FString tstr_PhysicsPath = _HomeDir / UTF8_TO_TCHAR(path.GetRawString()); + + TArray tarr_ModelFile; + const bool tb_ReadSuc = FFileHelper::LoadFileToArray(tarr_ModelFile, *tstr_PhysicsPath); + if (tb_ReadSuc) + { + LoadPhysics(tarr_ModelFile.GetData(), tarr_ModelFile.Num()); + } + + UE_LOG(LogCubism, Log, TEXT("FRawModel::LoadAsset: Read Physics <%s> res=%d"), + *tstr_PhysicsPath, + tb_ReadSuc ? 1 : 0 + ); + } + + //Pose + if (strcmp(_modelSetting->GetPoseFileName(), "") != 0) + { + csmString path = _modelSetting->GetPoseFileName(); + FString tstr_TempReadPath = _HomeDir / UTF8_TO_TCHAR(path.GetRawString()); + + TArray tarr_ModelFile; + const bool tb_ReadSuc = FFileHelper::LoadFileToArray(tarr_ModelFile, *tstr_TempReadPath); + + if (tb_ReadSuc) + { + LoadPose(tarr_ModelFile.GetData(), tarr_ModelFile.Num()); + } + + UE_LOG(LogCubism, Log, TEXT("FRawModel::LoadAsset: Read Pose <%s> res=%d"), + *tstr_TempReadPath, + tb_ReadSuc ? 1 : 0 + ); + } + + //EyeBlink + if (_modelSetting->GetEyeBlinkParameterCount() > 0) + { + _eyeBlink = CubismEyeBlink::Create(_modelSetting); + } + + //Breath + { + _breath = CubismBreath::Create(); + + csmVector breathParameters; + + breathParameters.PushBack(CubismBreath::BreathParameterData(_idParamAngleX, 0.0f, 15.0f, 6.5345f, 0.5f)); + breathParameters.PushBack(CubismBreath::BreathParameterData(_idParamAngleY, 0.0f, 8.0f, 3.5345f, 0.5f)); + breathParameters.PushBack(CubismBreath::BreathParameterData(_idParamAngleZ, 0.0f, 10.0f, 5.5345f, 0.5f)); + breathParameters.PushBack(CubismBreath::BreathParameterData(_idParamBodyAngleX, 0.0f, 4.0f, 15.5345f, 0.5f)); + breathParameters.PushBack(CubismBreath::BreathParameterData(CubismFramework::GetIdManager()->GetId(DefaultParameterId::ParamBreath), 0.5f, 0.5f, 3.2345f, 0.5f)); + + _breath->SetParameters(breathParameters); + } + + //UserData + if (strcmp(_modelSetting->GetUserDataFile(), "") != 0) + { + csmString path = _modelSetting->GetUserDataFile(); + FString tstr_TempReadPath = _HomeDir / UTF8_TO_TCHAR(path.GetRawString()); + + TArray tarr_ModelFile; + const bool tb_ReadSuc = FFileHelper::LoadFileToArray(tarr_ModelFile, *tstr_TempReadPath); + + if (tb_ReadSuc) + { + LoadUserData(tarr_ModelFile.GetData(), tarr_ModelFile.Num()); + } + + UE_LOG(LogCubism, Log, TEXT("FRawModel::LoadAsset: Read UserData <%s> res=%d"), + *tstr_TempReadPath, + tb_ReadSuc ? 1 : 0 + ); + } + + // EyeBlinkIds + { + csmInt32 eyeBlinkIdCount = _modelSetting->GetEyeBlinkParameterCount(); + for (csmInt32 i = 0; i < eyeBlinkIdCount; ++i) + { + _eyeBlinkIds.PushBack(_modelSetting->GetEyeBlinkParameterId(i)); + } + } + + // LipSyncIds + { + csmInt32 lipSyncIdCount = _modelSetting->GetLipSyncParameterCount(); + for (csmInt32 i = 0; i < lipSyncIdCount; ++i) + { + _lipSyncIds.PushBack(_modelSetting->GetLipSyncParameterId(i)); + } + } + + //Layout + csmMap layout; + _modelSetting->GetLayoutMap(layout); + _modelMatrix->SetupFromLayout(layout); + + _model->SaveParameters(); + + for (csmInt32 i = 0; i < _modelSetting->GetMotionGroupCount(); i++) + { + const csmChar* group = _modelSetting->GetMotionGroupName(i); + PreloadMotionGroup(group); + } + + _motionManager->StopAllMotions(); + + ////////////////////////////////////////////////////////////////////////// + + UE_LOG(LogCubism, Log, TEXT("FRawModel::LoadAsset: Done")); + return true; +} + +void FRawModel::OnUpdate(float InDeltaTime) +{ + _userTimeSeconds += InDeltaTime; + + _dragManager->Update(InDeltaTime); + _dragX = _dragManager->GetX(); + _dragY = _dragManager->GetY(); + + // モーションによるパラメータ更新の有無 + csmBool motionUpdated = false; + + //----------------------------------------------------------------- + _model->LoadParameters(); // 前回セーブされた状態をロード + if (_motionManager->IsFinished()) + { + OnMotionPlayEnd.ExecuteIfBound(); + + // モーションの再生がない場合、待機モーションの中からランダムで再生する + StartRandomMotion(MotionGroupIdle, PriorityIdle); + } + else + { + motionUpdated = _motionManager->UpdateMotion(_model, InDeltaTime); // モーションを更新 + } + _model->SaveParameters(); // 状態を保存 + //----------------------------------------------------------------- + + // まばたき + if (!motionUpdated) + { + if (_eyeBlink != NULL) + { + // メインモーションの更新がないとき + _eyeBlink->UpdateParameters(_model, InDeltaTime); // 目パチ + } + } + + if (_expressionManager != NULL) + { + _expressionManager->UpdateMotion(_model, InDeltaTime); // 表情でパラメータ更新(相対変化) + } + + //ドラッグによる変化 + //ドラッグによる顔の向きの調整 + _model->AddParameterValue(_idParamAngleX, _dragX * 30); // -30から30の値を加える + _model->AddParameterValue(_idParamAngleY, _dragY * 30); + _model->AddParameterValue(_idParamAngleZ, _dragX * _dragY * -30); + + //ドラッグによる体の向きの調整 + _model->AddParameterValue(_idParamBodyAngleX, _dragX * 10); // -10から10の値を加える + + //ドラッグによる目の向きの調整 + _model->AddParameterValue(_idParamEyeBallX, _dragX); // -1から1の値を加える + _model->AddParameterValue(_idParamEyeBallY, _dragY); + + // 呼吸など + if (_breath != NULL) + { + _breath->UpdateParameters(_model, InDeltaTime); + } + + // 物理演算の設定 + if (_physics != NULL) + { + _physics->Evaluate(_model, InDeltaTime); + } + + // リップシンクの設定 + if (_lipSync) + { + csmFloat32 value = 0; // リアルタイムでリップシンクを行う場合、システムから音量を取得して0〜1の範囲で値を入力します。 + + for (csmUint32 i = 0; i < _lipSyncIds.GetSize(); ++i) + { + _model->AddParameterValue(_lipSyncIds[i], value, 0.8f); + } + } + + // ポーズの設定 + if (_pose != NULL) + { + _pose->UpdateParameters(_model, InDeltaTime); + } + + _model->Update(); +} + +void FRawModel::PreloadMotionGroup(const Csm::csmChar* group) +{ + const csmInt32 count = _modelSetting->GetMotionCount(group); + + for (csmInt32 i = 0; i < count; i++) + { + //ex) idle_0 + csmString name = Utils::CubismString::GetFormatedString("%s_%d", group, i); + csmString path = _modelSetting->GetMotionFileName(group, i); + + + FString tstr_TempReadPath = _HomeDir / UTF8_TO_TCHAR(path.GetRawString()); + + TArray tarr_ModelFile; + const bool tb_ReadSuc = FFileHelper::LoadFileToArray(tarr_ModelFile, *tstr_TempReadPath); + if (tb_ReadSuc) + { + CubismMotion* tmpMotion = static_cast(LoadMotion(tarr_ModelFile.GetData(), tarr_ModelFile.Num(), name.GetRawString())); + + csmFloat32 fadeTime = _modelSetting->GetMotionFadeInTimeValue(group, i); + if (fadeTime >= 0.0f) + { + tmpMotion->SetFadeInTime(fadeTime); + } + + fadeTime = _modelSetting->GetMotionFadeOutTimeValue(group, i); + if (fadeTime >= 0.0f) + { + tmpMotion->SetFadeOutTime(fadeTime); + } + tmpMotion->SetEffectIds(_eyeBlinkIds, _lipSyncIds); + + if (_motions[name] != NULL) + { + ACubismMotion::Delete(_motions[name]); + } + _motions[name] = tmpMotion; + } + + UE_LOG(LogCubism, Log, TEXT("FRawModel::PreloadMotionGroup: Read <%s><%s> res=%d"), + *tstr_TempReadPath, + UTF8_TO_TCHAR(name.GetRawString()), + tb_ReadSuc ? 1 : 0 + ); + } + + UE_LOG(LogCubism, Log, TEXT("FRawModel::PreloadMotionGroup: Complete with %d"), count); +} + +CubismMotionQueueEntryHandle FRawModel::StartMotion(const csmChar* group, csmInt32 no, csmInt32 priority) +{ + if (priority == PriorityForce) + { + _motionManager->SetReservePriority(priority); + } + else if (!_motionManager->ReserveMotion(priority)) + { + UE_LOG(LogCubism, Log, TEXT("FRawModel::StartMotion: Can't start motion.")); + return InvalidMotionQueueEntryHandleValue; + } + + const csmString fileName = _modelSetting->GetMotionFileName(group, no); + + //ex) idle_0 + csmString name = Utils::CubismString::GetFormatedString("%s_%d", group, no); + CubismMotion* motion = static_cast(_motions[name.GetRawString()]); + csmBool autoDelete = false; + + if (motion == NULL) + { + csmString path = fileName; + + + FString tstr_TempReadPath = _HomeDir / UTF8_TO_TCHAR(path.GetRawString()); + + TArray tarr_ModelFile; + const bool tb_ReadSuc = FFileHelper::LoadFileToArray(tarr_ModelFile, *tstr_TempReadPath); + + if (tb_ReadSuc) + { + + motion = static_cast(LoadMotion(tarr_ModelFile.GetData(), tarr_ModelFile.Num(), NULL)); + csmFloat32 fadeTime = _modelSetting->GetMotionFadeInTimeValue(group, no); + if (fadeTime >= 0.0f) + { + motion->SetFadeInTime(fadeTime); + } + + fadeTime = _modelSetting->GetMotionFadeOutTimeValue(group, no); + if (fadeTime >= 0.0f) + { + motion->SetFadeOutTime(fadeTime); + } + motion->SetEffectIds(_eyeBlinkIds, _lipSyncIds); + autoDelete = true; // 終了時にメモリから削除 + } + + UE_LOG(LogCubism, Log, TEXT("FRawModel::StartMotion: Read <%s> res=%d"), + *tstr_TempReadPath, + tb_ReadSuc ? 1 : 0 + ); + + + } + + //voice is not supported + //csmString voice = _modelSetting->GetMotionSoundFileName(group, no); + //if (strcmp(voice.GetRawString(), "") != 0) + //{ + // //csmString path = voice; + // //path = _modelHomeDir + path; + //} + + UE_LOG(LogCubism, Log, TEXT("FRawModel::StartMotion: Start [%s_%d]"), UTF8_TO_TCHAR(group), no); + return _motionManager->StartMotionPriority(motion, autoDelete, priority); +} + +CubismMotionQueueEntryHandle FRawModel::StartRandomMotion(const csmChar* group, csmInt32 priority) +{ + if (_modelSetting->GetMotionCount(group) == 0) + { + return InvalidMotionQueueEntryHandleValue; + } + + csmInt32 no = rand() % _modelSetting->GetMotionCount(group); + + return StartMotion(group, no, priority); +} + +void FRawModel::SetExpression(const Csm::csmChar* expressionID) +{ + ACubismMotion* motion = _expressions[expressionID]; + check(_expressionManager); + + if (motion != NULL) + { + _expressionManager->StartMotionPriority(motion, false, PriorityForce); + } + else + { + UE_LOG(LogCubism, Warning, TEXT("FRawModel::SetExpression: No expression found [%s]"), UTF8_TO_TCHAR(expressionID)); + } +} + +void FRawModel::SetRandomExpression() +{ + if (_expressions.GetSize() == 0) + { + return; + } + + csmInt32 no = rand() % _expressions.GetSize(); + csmMap::const_iterator map_ite; + csmInt32 i = 0; + for (map_ite = _expressions.Begin(); map_ite != _expressions.End(); map_ite++) + { + if (i == no) + { + csmString name = (*map_ite).First; + SetExpression(name.GetRawString()); + return; + } + i++; + } +} + +void FRawModel::ReleaseMotions() +{ + for (csmMap::const_iterator iter = _motions.Begin(); iter != _motions.End(); ++iter) + { + ACubismMotion::Delete(iter->Second); + } + + _motions.Clear(); +} + +void FRawModel::ReleaseExpressions() +{ + for (csmMap::const_iterator iter = _expressions.Begin(); iter != _expressions.End(); ++iter) + { + ACubismMotion::Delete(iter->Second); + } + + _expressions.Clear(); +} + +bool FRawModel::HitTest(const Csm::csmChar* hitAreaName, Csm::csmFloat32 x, Csm::csmFloat32 y) +{ + // 透明時は当たり判定なし。 + if (_opacity < 1) + { + return false; + } + + + const csmInt32 count = _modelSetting->GetHitAreasCount(); + for (csmInt32 i = 0; i < count; i++) + { + if (strcmp(_modelSetting->GetHitAreaName(i), hitAreaName) == 0) + { + const CubismIdHandle drawID = _modelSetting->GetHitAreaId(i); + return IsHit(drawID, x, y); + } + } + return false; // 存在しない場合はfalse +} + +void FRawModel::PlayMotion(const FString& InName, const int32 InNo, const int32 InPriority) +{ + StartMotion(TCHAR_TO_UTF8(*InName), InNo, (int32)InPriority); +} + +bool FRawModel::OnTap(csmFloat32 x, csmFloat32 y) +{ + + if (HitTest(HitAreaNameHead, x, y)) + { + SetRandomExpression(); + return true; + } + else if (HitTest(HitAreaNameBody, x, y)) + { + StartRandomMotion(MotionGroupTapBody, PriorityNormal); + return true; + } + + return false; +} + +``` + +`Source/UnrealLive2D/Private/FWPort/RawModel.h`: + +```h +#pragma once + +#include "CoreMinimal.h" + +#include "CubismFramework.hpp" +#include "Model/CubismUserModel.hpp" +#include "Motion/CubismMotionQueueManager.hpp" +#include "CubismUeTypes.h" + + +class FRawModel : public Csm::CubismUserModel +{ +public: + FRawModel(); + ~FRawModel(); + +public: + bool LoadAsset(const FString& InPath); + + void OnUpdate(float InDeltaTime); + +public: + Csm::ICubismModelSetting* Get_ModelSetting() const { return _modelSetting; }; + const FString& Get_HomeDir() const { return _HomeDir; }; + +private: + Csm::CubismMotionQueueEntryHandle StartMotion(const Csm::csmChar* group, Csm::csmInt32 no, Csm::csmInt32 priority); + Csm::CubismMotionQueueEntryHandle StartRandomMotion(const Csm::csmChar* group, Csm::csmInt32 priority); + + void SetExpression(const Csm::csmChar* expressionID); + void SetRandomExpression(); + +private: + FString _HomeDir; + + Csm::ICubismModelSetting* _modelSetting; + Csm::csmMap _expressions; + + const Csm::CubismId* _idParamAngleX; ///< パラメータID: ParamAngleX + const Csm::CubismId* _idParamAngleY; ///< パラメータID: ParamAngleX + const Csm::CubismId* _idParamAngleZ; ///< パラメータID: ParamAngleX + const Csm::CubismId* _idParamBodyAngleX; ///< パラメータID: ParamBodyAngleX + + Csm::csmVector _eyeBlinkIds; ///< モデルに設定されたまばたき機能用パラメータID + + Csm::csmVector _lipSyncIds; ///< モデルに設定されたリップシンク機能用パラメータID + + Csm::csmMap _motions; ///< 読み込まれているモーションのリスト + + const Csm::CubismId* _idParamEyeBallX; ///< パラメータID: ParamEyeBallX + const Csm::CubismId* _idParamEyeBallY; ///< パラメータID: ParamEyeBallXY + +private: + /** + * @brief モーションデータをグループ名から一括でロードする。
+ * モーションデータの名前は内部でModelSettingから取得する。 + * + * @param[in] group モーションデータのグループ名 + */ + void PreloadMotionGroup(const Csm::csmChar* group); + + void ReleaseMotions(); + void ReleaseExpressions(); + float _userTimeSeconds; + + bool HitTest(const Csm::csmChar* hitAreaName, Csm::csmFloat32 x, Csm::csmFloat32 y); + + ////////////////////////////////////////////////////////////////////////// + /** Link to UE4 */ +public: + FSimpleDelegate OnMotionPlayEnd; + + void PlayMotion(const FString& InName, const int32 InNo, const int32 InPriority); + + bool OnTap(float x, float y); +}; +``` + +`Source/UnrealLive2D/Private/FrameWork/CHANGELOG.md`: + +```md +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [5-r.1] - 2024-03-26 + +### Added + +* Add function `ModF()` to compute floating-point remainder in `CubismMath` class. + +### Changed + +* Change the `Reset()` function of `CubismPose` class to public. +* Change some processes in the renderer to be handled as functions. +* Change to output log if the argument `MotionQueueEntry` is `NULL` in the `UpdateFadeWeight()` function of the `ACubismMotion` class. + +### Deprecated + +* Deprecate the `_fadeWeight` variable and the `GetFadeWeight()` function of the `CubismExpressionMotion` class. + * The `_fadeWeight` variable of the `CubismExpressionMotion` class can cause problems. + * Please use the `GetFadeWeight()` function of the `CubismExpressionMotionManager` class with one argument from now on. +* The `StartMotion()` function of the `CubismMotionQueueManager` class with the unnecessary third argument `userTimeSeconds` is deprecated. + * Please use the `StartMotion()` function with two arguments from now on. + +### Fixed + +* Fix a typo in `SetParameterValue()`. +* Fix an issue that the override keyword is not specified for some functions of classes inheriting from `CubismRenderer`. +* Fix operator overloading in the `CubismId` class from being private to public. +* Fix a bug that caused incorrect weight values when expression motions were shared by multiple models. + * Change the way fadeWeight is managed for expression motions. +* Fix shader build error when running nmake in Vulkan. + +### Removed + +* Remove `CubismSetupMaskedShaderUniforms`, `CubismNormalShaderUniforms`, `CubismMaskedShaderUniforms` and `CubismFragMaskedShaderUniforms` from Metal and merged them into `CubismMaskedShaderUniforms`. + + +## [5-r.1-beta.4] - 2024-01-25 + +### Fixed + +* Fix memory leak in DX11. +* Fix an issue where models with a specific number of masks could not be drawn correctly. +* Fix to check for null when reading json. +* Fix an issue that caused some graphics drivers to not render correctly in Vulkan. +* Fix errors related to semaphore waiting stages. +* Fix errors that occurs when building with x86 in vulkan. + + +## [5-r.1-beta.3] - 2023-10-12 + +### Added + +* Add `clamp()` to CubismMath. + + +## [5-r.1-beta.2] - 2023-09-28 + +### Added + +* Add a function to the `ACubismMotion` class that sets arbitrary data in the callback. by [@MizunagiKB](https://github.com/MizunagiKB) +* Add a comment for clarity for the function whose usage is not intuitive. + +### Changed + +* Change getter functions to get some datas without `GetIdManager` in `CubismModel`. + +### Fixed + +* Fix a typo in a variable related to the bezier handle restriction state. + + +## [5-r.1-beta.1] - 2023-08-17 + +### Added + +* Add the function to get the ID of a given parameter.(`CubismModel::GetParameterId`) +* Add the `CubismExpressionMotionManager` class. +* Add a Renderer for Vulkan API in Windows. + +### Changed + +* Unify Offscreen drawing-related terminology with `OffscreenSurface`. +* Change comment guidelines for headers in the `Framework` directory. + +### Fixed + +* Fix a bug that caused cocos2d-x culling to not be performed correctly. +* Fix the structure of the class in renderer. +* Separate the high precision mask process from the clipping mask setup process. +* Separate shader class from CubismRenderer files for Cocos2d-x, Metal, OpenGL. +* Fix Metal rendering results on macOS to be similar to OpenGL. + * If you want to apply the previous Metal rendering results, change `mipFilter` in `MTLSamplerDescriptor` from `MTLSamplerMipFilterLinear` to `MTLSamplerMipFilterNotMipmapped`. +* Fix a bug that the value applied by multiply was not appropriate during expression transitions. + +### Removed + +* Remove several arguments of `DrawMesh` function. + + +## [4-r.7] - 2023-05-25 + +### Added + +* Add some function for checking consistency of MOC3. + * Add the function of checking consistency on reviving a MOC3. (`CubismMoc::Create`) + * Add the function of checking consistency from unrevived MOC3. (`CubismMoc::HasMocConsistencyFromUnrevivedMoc`) +* Add some functions to change Multiply and Screen colors on a per part basis. + +### Changed + +* Change access specifier for `CubismExpressionMotion`. +* Change to get opacity according to the current time of the motion. + +## [4-r.6.2] - 2023-03-16 + +### Fixed + +* Fix a bug that caused double and triple buffering to be disabled on DirectX systems due to multiple render textures in 4-r.6. +* Fix the condition of splitting the mask buffer according to the number of masks used to be in accordance with the specification. +* Fix some problems related to Cubism Core. + * See `CHANGELOG.md` in Core. + + +## [4-r.6.1] - 2023-03-10 + +### Added + +* Add function to validate MOC3 files. + + +## [4-r.6] - 2023-02-21 + +### Added + +* Add API to allow users to configure culling. +* The number of render textures used can now be increased arbitrarily. + * The maximum number of masks when using multiple render textures has been increased to "number of render textures * 32". + +### Fixed + +* Fix a bug that models with culling were not rendered correctly in Metal. +* Fix a bug that caused some masks to be rendered incorrectly when using 17 or more masks in DirectX systems. + +### Removed + +* Remove unnecessary variable `modelToWorldF` in renderer. + + +## [4-r.5.1] - 2022-09-15 + +### Fixed + +* Fix a bug that caused a crash if an empty array existed in json. + + +## [4-r.5] - 2022-09-08 + +### Added + +* Add immediate stabilization of physics. +* Add the multilingual supported documents. + +### Fixed + +* Fix a memory leak that occurred when defining a Vector with size 0. + + +## [4-r.5-beta.5] - 2022-08-04 + +### Fixed + +* Fix a bug in which processing was interrupted when an invalid vertex was specified in the middle of a physics operation. +* Fix physics system input to be split by the physics setting time. +* Fix a tiny memory leak in Cubism physics. + + +## [4-r.5-beta.4.1] - 2022-07-08 + +### Fixed + +* Fix Core API called in GetDrawableParentPartIndex function. + + +## [4-r.5-beta.4] - 2022-07-07 + +### Added + +* Add a function to get the latest .moc3 Version and the .moc3 Version of the loaded model. +* Add a function to get the type of parameters of the model. +* Add a function to get the parent part of the model's Drawable. + +### Changed + +* Disable ARC in Metal renderer. + +### Fixed + +* Fix dangling pointer in `GetRenderPassDescriptor` function for Metal. + + +## [4-r.5-beta.3] - 2022-06-16 + +### Fixed + +* `GetDrawableTextureIndices` function in `CubismModel` has been renamed to `GetDrawableTextureIndex` because the name was not correct. + * `GetDrawableTextureIndices` function is marked as deprecated. +* Fix physics system behaviour when exists Physics Fps Setting in .physics3.json. +* Fix force close problem when invalid `physics3.json` is read. +* Fixed memory leak in Cocos2d-x. + + +## [4-r.5-beta.2] - 2022-06-02 + +### Fixed + +* Fixed a bug that caused Multiply Color / Screen Color of different objects to be applied. + * See `CHANGELOG.md` in Core. + * No modifications to Samples and Framework. + + +## [4-r.5-beta.1] - 2022-05-19 + +### Added + +* Add processing related to Multiply Color / Screen Color added in Cubism 4.2. +* Add a function to reset the physics states. + +### Fixed + +* Fix GetTextureDirectory() to return the directory name of the 0th texture path. + + +## [4-r.4] - 2021-12-09 + +### Added + +* Add the rendering options on Metal: + * `USE_RENDER_TARGET` + * `USE_MODEL_RENDER_TARGET` + +* Add anisotropic filtering to Metal. +* Add a macro to toggle the precision of floating point numbers in OpenGL fragment shaders. +* Add a function to check `.cdi3.json` exists from `.model3.json`. +* Add `CubismJsonHolder`, a common class used to instantiate and check the validity of `CubismJson`. + * Each Json parser will now warn if an class of `CubismJson` is invalid. + +### Changed + +* Change each Json parser inherits a common class `CubismJsonHolder`. + +### Fixed + +* Fix renderer for Cocos2d-x v4.0. + * `RenderTexture` was empty when using `USE_MODEL_RENDER_TARGET`. +* Fix motions with different fade times from switching properly. +* Fix a bug that motions currently played do not fade out when play a motion. + + +## [4-r.4-beta.1] - 2021-10-07 + +### Added + +* Add a function to parse the opacity from `.motion3.json`. +* Add a Renderer for Metal API in iOS. + * There are some restrictions, see [NOTICE.md](https://github.com/Live2D/CubismNativeSamples/blob/e4144053d1546473d2e76d30779e26d84b00d9f9/NOTICE.md). + +### Fixed + +* Fix return correct error values for out-of-index arguments in cubismjson by [@cocor-au-lait](https://github.com/cocor-au-lait). +* Fix a warning when `SegmentType` could not be obtained when loading motion. +* Fix renderer for Cocos2d-x v4.0. + * Rendering didn't work when using `USE_RENDER_TARGET` and high precision masking. + + +## [4-r.3] - 2021-06-10 + +## [4-r.3-beta.1] - 2021-05-13 + +### Added + +* Add a Renderer for Cocos2d-x v4.0. +* Implement a function to get the correct value when the time axis of the Bezier handle cannot be linear. +* Add an argument to the function `SetClippingMaskBufferSize` to set the height and width of the clipping mask buffer. + +### Changed + +* Improve the quality of Clipping Mask on high precision masking. + + +## [4-r.2] - 2021-02-17 + +### Added + +* Implement anisotropic filtering for DirectX-based Renderer. +* Implement get pixel size and `PixelsPerUnit` of the model + +### Changed + +* Check pointer before use to avoid crash by [@Xrysnow](https://github.com/Xrysnow) + +### Fixed + +* Fix Physics input reflect flag on evaluate. +* Fix renderer for OpenGL. + * Add delete mask buffer when renderer instance is destroyed. +* Fix delay in starting fade-out for expressions. +* Fix memory bug causing segmentation fault when reallocating memory by [@adrianiainlam](https://github.com/adrianiainlam) +* Fix reference size of model matrix. +* Fix memory leaking the color buffer on destroyed `CubismOffscreenFrame_OpenGLES2`. +* Fix argument name typo at `CubismEyeBlink::SetBlinkingInterval()`. + + +## [4-r.1] - 2020-01-30 + +### Added + +* Add the callback function called on finished motion playback. + +### Changed + +* Include header files in CMake. +* `` is not included on macOS if `CSM_TARGET_COCOS` is defined. + +### Fixed + +* Fix rendering not working properly on Android devices with Tegra. + +### Deprecated + +* Use `target_include_directories` instead of using `FRAMEWORK_XXX_INCLUDE_PATH` variable in application CMake. +* Use `target_compile_definitions` instead of using `FRAMEWORK_DEFINITIOINS` variable in application CMake. +* Specify `FRAMEWORK_SOURCE` variable also in OpenGL application CMake. + + +## [4-beta.2] - 2019-11-14 + +### Added + +* Add the includes to `Framework` for Linux build. + +### Changed + +* Refactoring `CMakeLists.txt` + +### Fixed + +* Fix renderer for DirectX 9 / 11. + * Add missing implementation: Check the dynamic flags. + + +## [4-beta.1] - 2019-09-04 + +### Added + +* Support new Inverted Masking features. +* Add `.editorconfig` and `.gitattributes` to manage file formats. +* Add `.gitignore`. +* Add `CHANGELOG.md`. + +### Changed + +* Convert all file formats according to `.editorconfig`. + +### Fixed + +* Fix typo of `CubismCdiJson`. +* Fix invalid expressions of `CubismCdiJson`. + + +[5-r.1]: https://github.com/Live2D/CubismNativeFramework/compare/5-r.1-beta.4...5-r.1 +[5-r.1-beta.4]: https://github.com/Live2D/CubismNativeFramework/compare/5-r.1-beta.3...5-r.1-beta.4 +[5-r.1-beta.3]: https://github.com/Live2D/CubismNativeFramework/compare/5-r.1-beta.2...5-r.1-beta.3 +[5-r.1-beta.2]: https://github.com/Live2D/CubismNativeFramework/compare/5-r.1-beta.1...5-r.1-beta.2 +[5-r.1-beta.1]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.7...5-r.1-beta.1 +[4-r.7]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.6.2...4-r.7 +[4-r.6.2]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.6.1...4-r.6.2 +[4-r.6.1]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.6...4-r.6.1 +[4-r.6]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.5.1...4-r.6 +[4-r.5.1]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.5...4-r.5.1 +[4-r.5]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.5-beta.5...4-r.5 +[4-r.5-beta.5]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.5-beta.4.1...4-r.5-beta.5 +[4-r.5-beta.4.1]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.5-beta.4...4-r.5-beta.4.1 +[4-r.5-beta.4]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.5-beta.3...4-r.5-beta.4 +[4-r.5-beta.3]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.5-beta.2...4-r.5-beta.3 +[4-r.5-beta.2]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.5-beta.1...4-r.5-beta.2 +[4-r.5-beta.1]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.4...4-r.5-beta.1 +[4-r.4]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.4-beta.1...4-r.4 +[4-r.4-beta.1]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.3...4-r.4-beta.1 +[4-r.3]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.3-beta.1...4-r.3 +[4-r.3-beta.1]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.2...4-r.3-beta.1 +[4-r.2]: https://github.com/Live2D/CubismNativeFramework/compare/4-r.1...4-r.2 +[4-r.1]: https://github.com/Live2D/CubismNativeFramework/compare/4-beta.2...4-r.1 +[4-beta.2]: https://github.com/Live2D/CubismNativeFramework/compare/4-beta.1...4-beta.2 +[4-beta.1]: https://github.com/Live2D/CubismNativeFramework/compare/0f5da4981cc636fe3892bb94d5c60137c9cf1eb1...4-beta. + +``` + +`Source/UnrealLive2D/Private/FrameWork/CMakeLists.txt`: + +```txt +cmake_minimum_required(VERSION 3.10) + +set(LIB_NAME Framework) + +# Force static library. +add_library(${LIB_NAME} STATIC) + +add_subdirectory(src) + +# Add include path. +target_include_directories(${LIB_NAME} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + PRIVATE + ${RENDER_INCLUDE_PATH} +) + +# Deprecated functions +# The following expressions are written for compatibility +# and will be removed in a future release. + +# Add core include. +target_include_directories(${LIB_NAME} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../Core/include +) +# Add definitions. +target_compile_definitions(${LIB_NAME} + PRIVATE + ${FRAMEWORK_DEFINITIOINS} +) + +``` + +`Source/UnrealLive2D/Private/FrameWork/LICENSE.md`: + +```md +## Definitions + +### Live2D Cubism Components + +Cubism Native Framework is included in Live2D Cubism Components. + +Cubism Native Framework は Live2D Cubism Components に含まれます。 + +Cubism Native Framework 包括在 Live2D Cubism Components 中。 + +## Cubism SDK Release License + +*All business* users must obtain a Cubism SDK Release License. "Business" means an entity with the annual gross revenue more than ten million (10,000,000) JPY for the most recent fiscal year. + +* [Cubism SDK Release License](https://www.live2d.com/en/download/cubism-sdk/release-license/) + +直近会計年度の売上高が 1000 万円以上の事業者様がご利用になる場合は、Cubism SDK リリースライセンス(出版許諾契約)に同意していただく必要がございます。 + +* [Cubism SDK リリースライセンス](https://www.live2d.com/ja/download/cubism-sdk/release-license/) + +如果您的企业在最近一个会计年度的销售额达到或超过1000万日元,您必须得到Cubism SDK的出版授权许可(出版许可协议)。 + +* [Cubism SDK发行许可证](https://www.live2d.com/zh-CHS/download/cubism-sdk/release-license/) + + +## Live2D Open Software License + +Live2D Cubism Components is available under Live2D Open Software License. + +* [Live2D Open Software License Agreement](https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html) +* [Live2D Open Software 使用許諾契約書](https://www.live2d.com/eula/live2d-open-software-license-agreement_jp.html) +* [Live2D Open Software 使用授权协议](https://www.live2d.com/eula/live2d-open-software-license-agreement_cn.html) + + +## Live2D Proprietary Software License + +Live2D Cubism Core is available under Live2D Proprietary Software License. + +* [Live2D Proprietary Software License Agreement](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_en.html) +* [Live2D Proprietary Software 使用許諾契約書](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_jp.html) +* [Live2D Proprietary Software 使用授权协议](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_cn.html) + + +--- + +Please contact us from [here](https://www.live2d.jp/contact/) for more license information. + +``` + +`Source/UnrealLive2D/Private/FrameWork/README.ja.md`: + +```md +[English](README.md) / [日本語](README.ja.md) + +--- + +# Cubism Native Framework + +Live2D Cubism Editor で出力したモデルをアプリケーションで利用するためのフレームワークです。 + +モデルを表示、操作するための各種機能を提供します。 +モデルをロードするには Cubism Core ライブラリと組み合わせて使用します。 + + +## ライセンス + +本フレームワークを使用する前に、[ライセンス](LICENSE.md)をご確認ください。 + + +## Cubism 5新機能や過去バージョンとの互換性について + +本 SDK はCubism 5に対応した製品です。 +Cubism 5 Editorに搭載された新機能のSDK対応については [こちら](https://docs.live2d.com/cubism-sdk-manual/cubism-5-new-functions/)をご確認ください。 +過去バージョンのCubism SDKとの互換性については [こちら](https://docs.live2d.com/cubism-sdk-manual/compatibility-with-cubism-5/)をご確認ください。 + + +## コンポーネント + +### Effect + +自動まばたきやリップシンクなど、モデルに対してモーション情報をエフェクト的に付加する機能を提供します。 + +### Id + +モデルに設定されたパラメータ名・パーツ名・Drawable 名を独自の型で管理する機能を提供します。 + +### Math + +行列計算やベクトル計算など、モデルの操作や描画に必要な算術演算の機能を提供します。 + +### Model + +モデルを取り扱うための各種機能(生成、更新、破棄)を提供します。 + +### Motion + +モデルにモーションデータを適用するための各種機能(モーション再生、パラメータブレンド)を提供します。 + +### Physics + +モデルに物理演算による変形操作を適用するための機能を提供します。 + +### Rendering + +各種プラットフォームでモデルを描画するためのグラフィックス命令を実装したレンダラを提供します。 + +### Type + +本フレームワーク内で使用する C++ 型定義を提供します。 + +### Utils + +JSON パーサーやログ出力などのユーティリティ機能を提供します。 + + +## Live2D Cubism Core for Native + +当リポジトリには Live2D Cubism Core for Native は同梱されていません。 + +ダウンロードするには[こちら](https://www.live2d.com/download/cubism-sdk/download-native/)のページを参照ください。 + + +## サンプル + +標準的なアプリケーションの実装例については、下記サンプルリポジトリを参照ください。 + +[CubismNativeSamples](https://github.com/Live2D/CubismNativeSamples) + + +## マニュアル + +[Cubism SDK Manual](https://docs.live2d.com/cubism-sdk-manual/top/) + + +## 変更履歴 + +当リポジトリの変更履歴については [CHANGELOG.md](CHANGELOG.md) を参照ください。 + + +## プロジェクトへの貢献 + +プロジェクトに貢献する方法はたくさんあります。バグのログの記録、このGitHubでのプルリクエストの送信、Live2Dコミュニティでの問題の報告と提案の作成です。 + +### フォークとプルリクエスト + +修正、改善、さらには新機能をもたらすかどうかにかかわらず、プルリクエストに感謝します。メインリポジトリを可能な限りクリーンに保つために、必要に応じて個人用フォークと機能ブランチを作成してください。 + +### バグ + +Live2Dコミュニティでは、問題のレポートと機能リクエストを定期的にチェックしています。バグレポートを提出する前に、Live2Dコミュニティで検索して、問題のレポートまたは機能リクエストがすでに投稿されているかどうかを確認してください。問題がすでに存在する場合は、関連するコメントを追記してください。 + +### 提案 + +SDKの将来についてのフィードバックにも関心があります。Live2Dコミュニティで提案や機能のリクエストを送信できます。このプロセスをより効果的にするために、それらをより明確に定義するのに役立つより多くの情報を含めるようお願いしています。 + + +## フォーラム + +ユーザー同士でCubism SDKの活用方法の提案や質問をしたい場合は、是非フォーラムをご活用ください。 + +- [Live2D 公式クリエイターズフォーラム](https://creatorsforum.live2d.com/) +- [Live2D Creator's Forum(English)](https://community.live2d.com/) + +``` + +`Source/UnrealLive2D/Private/FrameWork/README.md`: + +```md +[English](README.md) / [日本語](README.ja.md) + +--- + +# Cubism Native Framework + +This is a framework for using models output by Live2D Cubism Editor in applications. + +It provides various functions for displaying and manipulating the model. +It is used in conjunction with the Cubism Core library to load the model. + + +## License + +Please check the [license](LICENSE.md) before using this framework. + +## Translation + +Comments of the source codes are being translated into English. +Please [Go Here](./TRANSLATION.md) for the status. + + +## Compatibility with Cubism 5 new features and previous Cubism SDK versions + +This SDK is compatible with Cubism 5. +For SDK compatibility with new features in Cubism 5 Editor, please refer to [here](https://docs.live2d.com/en/cubism-sdk-manual/cubism-5-new-functions/). +For compatibility with previous versions of Cubism SDK, please refer to [here](https://docs.live2d.com/en/cubism-sdk-manual/compatibility-with-cubism-5/). + + +## Components + +### Effect + +Provides functions such as automatic blinking and lip sync to add motion information as an effect to the model. + +### Id + +Provides functions to manage the parameter name, part name, and Drawable name set in the model with unique types. + +### Math + +Provides arithmetic functions required for manipulating and drawing the model, such as matrix and vector calculations. + +### Model + +Provides various functions (generate, update, destroy) for handling the model. + +### Motion + +Provides various functions (motion playback, parameter blending) for applying motion data to the model. + +### Physics + +Provides functions for applying transformation manipulations due to physics to the model. + +### Rendering + +Provides a renderer that implements graphics instructions for drawing the model on various platforms. + +### Type + +Provides C++ type definitions used within this framework. + +### Utils + +Provides utility functions such as JSON parser and log output. + + +## Live2D Cubism Core for Native + +Live2D Cubism Core for Native is not included in this repository. + +To download, please refer to [this](https://www.live2d.com/download/cubism-sdk/download-native/) page. + + +## Samples + +Please refer to the following sample repository for implementation examples of standard applications. + +[CubismNativeSamples](https://github.com/Live2D/CubismNativeSamples) + + +## Manual + +[Cubism SDK Manual](https://docs.live2d.com/cubism-sdk-manual/top/) + + +## Changelog + +Please refer to [CHANGELOG.md](CHANGELOG.md) for the changelog of this repository. + + +## Contributing + +There are many ways to contribute to the project: logging bugs, submitting pull requests on this GitHub, and reporting issues and making suggestions in Live2D Community. + +### Forking And Pull Requests + +We very much appreciate your pull requests, whether they bring fixes, improvements, or even new features. To keep the main repository as clean as possible, create a personal fork and feature branches there as needed. + +### Bugs + +We are regularly checking issue-reports and feature requests at Live2D Community. Before filing a bug report, please do a search in Live2D Community to see if the issue-report or feature request has already been posted. If you find your issue already exists, make relevant comments and add your reaction. + +### Suggestions + +We're also interested in your feedback for the future of the SDK. You can submit a suggestion or feature request at Live2D Community. To make this process more effective, we're asking that you include more information to help define them more clearly. + + +## Forum + +If you want to suggest or ask questions about how to use the Cubism SDK between users, please use the forum. + +- [Live2D Creator's Forum](https://community.live2d.com/) +- [Live2D 公式クリエイターズフォーラム (Japanese)](https://creatorsforum.live2d.com/) + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/CubismCdiJson.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismCdiJson.hpp" + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + +namespace { +// JSON keys +const csmChar* Version = "Version"; +const csmChar* Parameters = "Parameters"; +const csmChar* ParameterGroups = "ParameterGroups"; +const csmChar* Parts = "Parts"; +const csmChar* Id = "Id"; +const csmChar* GroupId = "GroupId"; +const csmChar* Name = "Name"; +} + +CubismCdiJson::CubismCdiJson(const csmByte* buffer, csmSizeInt size) +{ + CreateCubismJson(buffer, size); +} + +CubismCdiJson::~CubismCdiJson() +{ + DeleteCubismJson(); +} + +// キーが存在するかどうかのチェック +csmBool CubismCdiJson::IsExistParameters() const +{ + Utils::Value& node = (_json->GetRoot()[Parameters]); + return !node.IsNull() && !node.IsError(); +} + +csmBool CubismCdiJson::IsExistParameterGroups() const +{ + Utils::Value& node = (_json->GetRoot()[ParameterGroups]); + return !node.IsNull() && !node.IsError(); +} + +csmBool CubismCdiJson::IsExistParts() const +{ + Utils::Value& node = (_json->GetRoot()[Parts]); + return !node.IsNull() && !node.IsError(); +} + +// パラメータについて +csmInt32 CubismCdiJson::GetParametersCount() +{ + if (!IsExistParameters()) return 0; + return _json->GetRoot()[Parameters].GetSize(); +} + +const csmChar* CubismCdiJson::GetParametersId(csmInt32 index) +{ + return _json->GetRoot()[Parameters][index][Id].GetRawString(); +} + +const csmChar* CubismCdiJson::GetParametersGroupId(csmInt32 index) +{ + return _json->GetRoot()[Parameters][index][GroupId].GetRawString(); +} + +const csmChar* CubismCdiJson::GetParametersName(csmInt32 index) +{ + return _json->GetRoot()[Parameters][index][Name].GetRawString(); +} + +// パラメータグループについて +csmInt32 CubismCdiJson::GetParameterGroupsCount() +{ + if (!IsExistParameterGroups()) return 0; + return _json->GetRoot()[ParameterGroups].GetSize(); +} + +const csmChar* CubismCdiJson::GetParameterGroupsId(csmInt32 index) +{ + return _json->GetRoot()[ParameterGroups][index][Id].GetRawString(); +} + +const csmChar* CubismCdiJson::GetParameterGroupsGroupId(csmInt32 index) +{ + return _json->GetRoot()[ParameterGroups][index][GroupId].GetRawString(); +} + +const csmChar* CubismCdiJson::GetParameterGroupsName(csmInt32 index) +{ + return _json->GetRoot()[ParameterGroups][index][Name].GetRawString(); +} + +// パーツについて +csmInt32 CubismCdiJson::GetPartsCount() +{ + if (!IsExistParts()) return 0; + return _json->GetRoot()[Parts].GetSize(); +} + +const csmChar* CubismCdiJson::GetPartsId(csmInt32 index) +{ + return _json->GetRoot()[Parts][index][Id].GetRawString(); +} + +const csmChar* CubismCdiJson::GetPartsName(csmInt32 index) +{ + return _json->GetRoot()[Parts][index][Name].GetRawString(); +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/CubismCdiJson.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismJsonHolder.hpp" +#include "Utils/CubismJson.hpp" + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * Handles the Display Information File. + */ +class CubismCdiJson : public CubismJsonHolder +{ +public: + /** + * Constructor
+ * Loads the Display Information File. + * + * @param buffer Buffer that Display Settings File is loaded from. + * @param size Number of bytes in buffer + */ + CubismCdiJson(const csmByte* buffer, csmSizeInt size); + + /** +  * Destructor +  */ + virtual ~CubismCdiJson(); + + /** + * Returns the number of parameters. + * + * @return Number of parameters + */ + csmInt32 GetParametersCount(); + + /** + * Returns the ID of the parameter. + * + * @param index Index to the desired parameter + * + * @return ID of the parameter + */ + const csmChar* GetParametersId(csmInt32 index); + + /** + * Returns the ID of the parameter group that the parameter belongs to. + * + * @param index Index to the desired parameter + * + * @return ID of the parameter group + */ + const csmChar* GetParametersGroupId(csmInt32 index); + + /** + * Returns the name of the parameter. + * + * @param index Index to the desired parameter + * + * @return name of parameter + */ + const csmChar* GetParametersName(csmInt32 index); + + /** + * Returns the number of parameter groups. + * + * @return Number of parameter groups + */ + csmInt32 GetParameterGroupsCount(); + + /** + * Returns the ID of the parameter group. + * + * @param index Index to the desired parameter group + * + * @return ID of parameter group + */ + const csmChar* GetParameterGroupsId(csmInt32 index); + + /** + * Returns the ID of the parameter group that the parameter group belongs to. + * + * @param index Index to the desired parameter group + * + * @return ID of the parameter group + */ + const csmChar* GetParameterGroupsGroupId(csmInt32 index); + + /** + * Returns the name of the parameter group. + * + * @param index Index to the desired parameter group + * + * @return Name of the parameter group + */ + const csmChar* GetParameterGroupsName(csmInt32 index); + + /** + * Returns the number of parts. + * + * @return Number of parts + */ + csmInt32 GetPartsCount(); + + /** + * Returns the ID of the part. + * + * @param index Index to the desired part + * + * @return ID of the part + */ + const csmChar* GetPartsId(csmInt32 index); + + /** + * Returns the name of the part. + * + * @param index Index to the desired part + * + * @return name of the part + */ + const csmChar* GetPartsName(csmInt32 index); + + +private: + /** + * Returns whether the parameters exist in the Display Information File. + * + * @return true if parameters exists; otherwise false. + */ + csmBool IsExistParameters() const; + + /** + * Returns whether parameter groups exist in the Display Information File. + * + * @return true if parameters on paramter groups exist; otherwise false. + */ + csmBool IsExistParameterGroups() const; + + /** + * Returns whether the parts exist in the Display Information File. + * + * @return true if parts exist; otherwise false. + */ + csmBool IsExistParts() const; +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/CubismDefaultParameterId.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismDefaultParameterId.hpp" + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { namespace DefaultParameterId { + +// パーツID +const csmChar* HitAreaPrefix = "HitArea"; +const csmChar* HitAreaHead = "Head"; +const csmChar* HitAreaBody = "Body"; +const csmChar* PartsIdCore = "Parts01Core"; +const csmChar* PartsArmPrefix = "Parts01Arm_"; +const csmChar* PartsArmLPrefix = "Parts01ArmL_"; +const csmChar* PartsArmRPrefix = "Parts01ArmR_"; + +// パラメータID +const csmChar* ParamAngleX = "ParamAngleX"; +const csmChar* ParamAngleY = "ParamAngleY"; +const csmChar* ParamAngleZ = "ParamAngleZ"; +const csmChar* ParamEyeLOpen = "ParamEyeLOpen"; +const csmChar* ParamEyeLSmile = "ParamEyeLSmile"; +const csmChar* ParamEyeROpen = "ParamEyeROpen"; +const csmChar* ParamEyeRSmile = "ParamEyeRSmile"; +const csmChar* ParamEyeBallX = "ParamEyeBallX"; +const csmChar* ParamEyeBallY = "ParamEyeBallY"; +const csmChar* ParamEyeBallForm = "ParamEyeBallForm"; +const csmChar* ParamBrowLY = "ParamBrowLY"; +const csmChar* ParamBrowRY = "ParamBrowRY"; +const csmChar* ParamBrowLX = "ParamBrowLX"; +const csmChar* ParamBrowRX = "ParamBrowRX"; +const csmChar* ParamBrowLAngle = "ParamBrowLAngle"; +const csmChar* ParamBrowRAngle = "ParamBrowRAngle"; +const csmChar* ParamBrowLForm = "ParamBrowLForm"; +const csmChar* ParamBrowRForm = "ParamBrowRForm"; +const csmChar* ParamMouthForm = "ParamMouthForm"; +const csmChar* ParamMouthOpenY = "ParamMouthOpenY"; +const csmChar* ParamCheek = "ParamCheek"; +const csmChar* ParamBodyAngleX = "ParamBodyAngleX"; +const csmChar* ParamBodyAngleY = "ParamBodyAngleY"; +const csmChar* ParamBodyAngleZ = "ParamBodyAngleZ"; +const csmChar* ParamBreath = "ParamBreath"; +const csmChar* ParamArmLA = "ParamArmLA"; +const csmChar* ParamArmRA = "ParamArmRA"; +const csmChar* ParamArmLB = "ParamArmLB"; +const csmChar* ParamArmRB = "ParamArmRB"; +const csmChar* ParamHandL = "ParamHandL"; +const csmChar* ParamHandR = "ParamHandR"; +const csmChar* ParamHairFront = "ParamHairFront"; +const csmChar* ParamHairSide = "ParamHairSide"; +const csmChar* ParamHairBack = "ParamHairBack"; +const csmChar* ParamHairFluffy = "ParamHairFluffy"; +const csmChar* ParamShoulderY = "ParamShoulderY"; +const csmChar* ParamBustX = "ParamBustX"; +const csmChar* ParamBustY = "ParamBustY"; +const csmChar* ParamBaseX = "ParamBaseX"; +const csmChar* ParamBaseY = "ParamBaseY"; +const csmChar* ParamNONE = "NONE:"; +}}}} + +//--------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/CubismDefaultParameterId.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismFramework.hpp" + +/** + * Constants that hold the default values of parameter IDs.
+ * See manual for details on each ID.
+ * https://docs.live2d.com/en/cubism-editor-manual/standard-parametor-list/ + */ + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { namespace DefaultParameterId { + +/* ID of Part */ +extern const csmChar* HitAreaPrefix; +extern const csmChar* HitAreaHead; +extern const csmChar* HitAreaBody; +extern const csmChar* PartsIdCore; +extern const csmChar* PartsArmPrefix; +extern const csmChar* PartsArmLPrefix; +extern const csmChar* PartsArmRPrefix; + +/* ID of Parameter */ +extern const csmChar* ParamAngleX; +extern const csmChar* ParamAngleY; +extern const csmChar* ParamAngleZ; +extern const csmChar* ParamEyeLOpen; +extern const csmChar* ParamEyeLSmile; +extern const csmChar* ParamEyeROpen; +extern const csmChar* ParamEyeRSmile; +extern const csmChar* ParamEyeBallX; +extern const csmChar* ParamEyeBallY; +extern const csmChar* ParamEyeBallForm; +extern const csmChar* ParamBrowLY; +extern const csmChar* ParamBrowRY; +extern const csmChar* ParamBrowLX; +extern const csmChar* ParamBrowRX; +extern const csmChar* ParamBrowLAngle; +extern const csmChar* ParamBrowRAngle; +extern const csmChar* ParamBrowLForm; +extern const csmChar* ParamBrowRForm; +extern const csmChar* ParamMouthForm; +extern const csmChar* ParamMouthOpenY; +extern const csmChar* ParamCheek; +extern const csmChar* ParamBodyAngleX; +extern const csmChar* ParamBodyAngleY; +extern const csmChar* ParamBodyAngleZ; +extern const csmChar* ParamBreath; +extern const csmChar* ParamArmLA; +extern const csmChar* ParamArmRA; +extern const csmChar* ParamArmLB; +extern const csmChar* ParamArmRB; +extern const csmChar* ParamHandL; +extern const csmChar* ParamHandR; +extern const csmChar* ParamHairFront; +extern const csmChar* ParamHairSide; +extern const csmChar* ParamHairBack; +extern const csmChar* ParamHairFluffy; +extern const csmChar* ParamShoulderY; +extern const csmChar* ParamBustX; +extern const csmChar* ParamBustY; +extern const csmChar* ParamBaseX; +extern const csmChar* ParamBaseY; +extern const csmChar* ParamNONE; +}}}} + +//--------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/CubismFramework.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismFramework.hpp" +#include "Utils/CubismDebug.hpp" +#include "Utils/CubismJson.hpp" +#include "Id/CubismIdManager.hpp" +#include "Rendering/CubismRenderer.hpp" + +#ifdef CSM_DEBUG_MEMORY_LEAKING + +#include + +#endif + + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + +// Framework内で使う定数の宣言 +namespace Constant { + const csmInt32 VertexOffset = 0; + const csmInt32 VertexStep = 2; +} + +// ファイルスコープの変数を初期化 +namespace { +csmBool s_isStarted = false; +csmBool s_isInitialized = false; +ICubismAllocator* s_allocator = NULL; +const CubismFramework::Option* s_option = NULL; +CubismIdManager* s_cubismIdManager = NULL; + +} + +inline ICubismAllocator* GetAllocator() +{ + return s_allocator; +} + +#ifdef CSM_DEBUG_MEMORY_LEAKING + +namespace { +std::vector* s_allocationList = NULL; +} + +#endif + +csmBool CubismFramework::StartUp(ICubismAllocator* allocator, const Option* option) +{ + if(s_isStarted) + { + CubismLogInfo("CubismFramework::StartUp() is already done."); + return s_isStarted; + } + + s_option = option; + if (s_option != NULL) + { + Core::csmSetLogFunction(s_option->LogFunction); + } + + if (allocator == NULL) + { + CubismLogWarning("CubismFramework::StartUp() failed, need allocator instance."); + s_isStarted = false; + } + else + { + s_allocator = allocator; + s_isStarted = true; + } + + // Live2D Cubism Coreバージョン情報を表示 + if(s_isStarted) + { + const Core::csmVersion version = Core::csmGetVersion(); + + const csmUint32 major = static_cast((version & 0xFF000000) >> 24); + const csmUint32 minor = static_cast((version & 0x00FF0000) >> 16); + const csmUint32 patch = static_cast((version & 0x0000FFFF)); + const csmUint32 versionNumber = version; + + CubismLogInfo("Live2D Cubism Core version: %02d.%02d.%04d (%d)", major, minor, patch, versionNumber); + } + + CubismLogInfo("CubismFramework::StartUp() is complete."); + + return s_isStarted; +} + +void CubismFramework::CleanUp() +{ + s_isStarted = false; + s_isInitialized = false; + s_allocator = NULL; + s_option = NULL; + s_cubismIdManager = NULL; +#ifdef CSM_DEBUG_MEMORY_LEAKING + s_allocationList = NULL; +#endif +} + +csmBool CubismFramework::IsStarted() +{ + return s_isStarted; +} + +void CubismFramework::Initialize() +{ + CSM_ASSERT(s_isStarted); + if(!s_isStarted) + { + CubismLogWarning("CubismFramework is not started."); + return; + } + + // --- s_isInitializedによる連続初期化ガード --- + // 連続してリソース確保が行われないようにする。 + // 再度Initialize()するには先にDispose()を実行する必要がある。 + if (s_isInitialized) + { + CubismLogWarning("CubismFramework::Initialize() skipped, already initialized."); + return; + } + +#ifdef CSM_DEBUG_MEMORY_LEAKING + s_allocationList = CSM_NEW std::vector(); +#endif + + //---- static 初期化 ---- + Utils::Value::StaticInitializeNotForClientCall(); + + s_cubismIdManager = CSM_NEW CubismIdManager(); + + s_isInitialized = true; + + CubismLogInfo("CubismFramework::Initialize() is complete."); +} + +void CubismFramework::Dispose() +{ + CSM_ASSERT(s_isStarted); + if (!s_isStarted) + { + CubismLogWarning("CubismFramework is not started."); + return; + } + + // --- s_isInitializedによる未初期化解放ガード --- + // Dispose()するには先にInitialize()を実行する必要がある。 + if (!s_isInitialized) // false...リソース未確保の場合 + { + CubismLogWarning("CubismFramework::Dispose() skipped, not initialized."); + return; + } + + //---- static 解放 ---- + Utils::Value::StaticReleaseNotForClientCall(); + + CSM_DELETE(s_cubismIdManager); + + //レンダラの静的リソース(シェーダプログラム他)を解放する + Rendering::CubismRenderer::StaticRelease(); + +#ifdef CSM_DEBUG_MEMORY_LEAKING + + for (csmUint32 i = 0; i < s_allocationList->size(); ++i) + { + CubismLogInfo("Memory leaking: 0x%p", s_allocationList->at(i)); + } + + if (s_allocationList->size() == 0) + { + CubismLogInfo("No memory leaking"); + } + + CSM_DELETE(s_allocationList); + +#endif + + s_isInitialized = false; + + CubismLogInfo("CubismFramework::Dispose() is complete."); +} + +csmBool CubismFramework::IsInitialized() +{ + return s_isInitialized; +} + +void CubismFramework::CoreLogFunction(const csmChar* message) +{ + // Return if logging not possible. + if (!Core::csmGetLogFunction()) + { + return; + } + + Core::csmGetLogFunction()(message); +} + +CubismFramework::Option::LogLevel CubismFramework::GetLoggingLevel() +{ + if (s_option != NULL) + { + return s_option->LoggingLevel; + } + return Option::LogLevel_Off; +} + +CubismIdManager* CubismFramework::GetIdManager() +{ + return s_cubismIdManager; +} + +#ifdef CSM_DEBUG_MEMORY_LEAKING + +void* CubismFramework::Allocate(csmSizeType size, const csmChar* fileName, csmInt32 lineNumber) +{ + void* address = GetAllocator()->Allocate(size); + + CubismLogVerbose("CubismFramework::Allocate(0x%p, %dbytes) %s(%d)", address, size, fileName, lineNumber); + + if (s_allocationList) + { + s_allocationList->push_back(address); + } + + return address; +} + +void* CubismFramework::AllocateAligned(csmSizeType size, csmUint32 alignment, const csmChar* fileName, csmInt32 lineNumber) +{ + void* address = GetAllocator()->AllocateAligned(size, alignment); + + CubismLogVerbose("CubismFramework::AllocateAligned(0x%p, a:%d, %dbytes) %s(%d)", address, alignment, size, fileName, lineNumber); + + if (s_allocationList) + { + s_allocationList->push_back(address); + } + + return address; +} + +void CubismFramework::Deallocate(void* address, const csmChar* fileName, csmInt32 lineNumber) +{ + if (!address) + { + return; + } + + CubismLogVerbose("CubismFramework::Deallocate(0x%p) %s(%d)", address, fileName, lineNumber); + + if (s_allocationList) + { + for (std::vector::iterator iter = s_allocationList->begin(); iter != s_allocationList->end(); ++iter) + { + if (*iter != address) + { + continue; + } + + s_allocationList->erase(iter); + break; + } + } + + GetAllocator()->Deallocate(address); +} + +void CubismFramework::DeallocateAligned(void* address, const csmChar* fileName, csmInt32 lineNumber) +{ + if (!address) + { + return; + } + + CubismLogVerbose("CubismFramework::DeallocateAligned(0x%p) %s(%d)", address, fileName, lineNumber); + + if (s_allocationList) + { + for (std::vector::iterator iter = s_allocationList->begin(); iter != s_allocationList->end(); ++iter) + { + if (*iter != address) + { + continue; + } + + s_allocationList->erase(iter); + break; + } + } + + GetAllocator()->DeallocateAligned(address); +} + +#else + +void* CubismFramework::Allocate(csmSizeType size) +{ + return GetAllocator()->Allocate(size); +} + +void* CubismFramework::AllocateAligned(csmSizeType size, csmUint32 alignment) +{ + return GetAllocator()->AllocateAligned(size, alignment); +} + +void CubismFramework::Deallocate(void* address) +{ + if (!address) + { + return; + } + + GetAllocator()->Deallocate(address); +} + +void CubismFramework::DeallocateAligned(void* address) +{ + if (!address) + { + return; + } + + GetAllocator()->DeallocateAligned(address); +} + +#endif + +}}} +//--------- LIVE2D NAMESPACE ------------ + +#ifdef CSM_DEBUG_MEMORY_LEAKING + +void* operator new(Live2D::Cubism::Framework::csmSizeType size, Live2D::Cubism::Framework::CubismAllocationTag tag, const Live2D::Cubism::Framework::csmChar* fileName, Live2D::Cubism::Framework::csmInt32 lineNumber) +{ + return Live2D::Cubism::Framework::CubismFramework::Allocate(size, fileName, lineNumber); +} + +void* operator new (Live2D::Cubism::Framework::csmSizeType size, Live2D::Cubism::Framework::csmUint32 alignment, Live2D::Cubism::Framework::CubismAllocationAlignedTag tag, const Live2D::Cubism::Framework::csmChar* fileName, Live2D::Cubism::Framework::csmInt32 lineNumber) +{ + return Live2D::Cubism::Framework::CubismFramework::AllocateAligned(size, alignment, fileName, lineNumber); +} + +void operator delete(void* address, Live2D::Cubism::Framework::CubismAllocationTag tag, const Live2D::Cubism::Framework::csmChar* fileName, Live2D::Cubism::Framework::csmInt32 lineNumber) +{ + Live2D::Cubism::Framework::CubismFramework::Deallocate(address, fileName, lineNumber); +} + +void operator delete(void* address, Live2D::Cubism::Framework::CubismAllocationAlignedTag tag, const Live2D::Cubism::Framework::csmChar* fileName, Live2D::Cubism::Framework::csmInt32 lineNumber) +{ + Live2D::Cubism::Framework::CubismFramework::DeallocateAligned(address, fileName, lineNumber); +} + +#else + +void* operator new(Live2D::Cubism::Framework::csmSizeType size, Live2D::Cubism::Framework::CubismAllocationTag tag) +{ + return Live2D::Cubism::Framework::CubismFramework::Allocate(size); +} + +void* operator new (Live2D::Cubism::Framework::csmSizeType size, Live2D::Cubism::Framework::csmUint32 alignment, Live2D::Cubism::Framework::CubismAllocationAlignedTag tag) +{ + return Live2D::Cubism::Framework::CubismFramework::AllocateAligned(size, alignment); +} + +void operator delete(void* address, Live2D::Cubism::Framework::CubismAllocationTag tag) +{ + Live2D::Cubism::Framework::CubismFramework::Deallocate(address); +} + +void operator delete(void* address, Live2D::Cubism::Framework::CubismAllocationAlignedTag tag) +{ + Live2D::Cubism::Framework::CubismFramework::DeallocateAligned(address); +} + +#endif + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/CubismFramework.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + + +//======================================================== +// Include the Live2D Cubism Core header. +//======================================================== +#include "Live2DCubismCore.hpp" + + +//======================================================== +// Include the Framework Configurations header. +//======================================================== +#include "CubismFrameworkConfig.hpp" + + +//======================================================== +// Include the Custom Allocator header. +//======================================================== +#include +#include "ICubismAllocator.hpp" + +#ifdef __linux__ +#include +#endif + + +//======================================================== +// Configurations of Memory Allocator. +//======================================================== +namespace Live2D { namespace Cubism { namespace Framework { + +class CubismAllocationTag +{ }; + +class CubismAllocationAlignedTag +{ }; + +static CubismAllocationTag GlobalTag; +static CubismAllocationAlignedTag GloabalAlignedTag; + +}}} + +// Macros for memory allocation. +#ifdef CSM_DEBUG_MEMORY_LEAKING + +// For debugging. +// Tracks / Detects the memory leaking. + +void* operator new (Live2D::Cubism::Framework::csmSizeType size, Live2D::Cubism::Framework::CubismAllocationTag tag, const Live2D::Cubism::Framework::csmChar* fileName, Live2D::Cubism::Framework::csmInt32 lineNumber); +void* operator new (Live2D::Cubism::Framework::csmSizeType size, Live2D::Cubism::Framework::csmUint32 alignment, Live2D::Cubism::Framework::CubismAllocationAlignedTag tag, const Live2D::Cubism::Framework::csmChar* fileName, Live2D::Cubism::Framework::csmInt32 lineNumber); +void operator delete(void* address, Live2D::Cubism::Framework::CubismAllocationTag tag, const Live2D::Cubism::Framework::csmChar* fileName, Live2D::Cubism::Framework::csmInt32 lineNumber); +void operator delete(void* address, Live2D::Cubism::Framework::CubismAllocationAlignedTag tag, const Live2D::Cubism::Framework::csmChar* fileName, Live2D::Cubism::Framework::csmInt32 lineNumber); + +template +void CsmDelete(T* address, const Live2D::Cubism::Framework::csmChar* fileName, Live2D::Cubism::Framework::csmInt32 lineNumber) +{ + if (!address) + { + return; + } + + address->~T(); + + operator delete(reinterpret_cast(address), Live2D::Cubism::Framework::GlobalTag, fileName, lineNumber); +} + +#define CSM_NEW new(Live2D::Cubism::Framework::GlobalTag, __FILE__, __LINE__) +#define CSM_DELETE_SELF(type, obj) do { if (!obj){ break; } obj->~type(); operator delete(obj, Live2D::Cubism::Framework::GlobalTag, __FILE__, __LINE__); } while(0) +#define CSM_DELETE(obj) CsmDelete(obj, __FILE__, __LINE__) +#define CSM_MALLOC(size) Live2D::Cubism::Framework::CubismFramework::Allocate(size, __FILE__, __LINE__) +#define CSM_MALLOC_ALLIGNED(size, align) Live2D::Cubism::Framework::CubismFramework::AllocateAligned(size, align, __FILE__, __LINE__) +#define CSM_FREE(ptr) Live2D::Cubism::Framework::CubismFramework::Deallocate(ptr, __FILE__, __LINE__) +#define CSM_FREE_ALLIGNED(ptr) Live2D::Cubism::Framework::CubismFramework::DeallocateAligned(ptr, __FILE__, __LINE__) + +#else + +// For releasing. +// Without Tracking / Detecting. + +void* operator new (Live2D::Cubism::Framework::csmSizeType size, Live2D::Cubism::Framework::CubismAllocationTag tag); +void* operator new (Live2D::Cubism::Framework::csmSizeType size, Live2D::Cubism::Framework::csmUint32 alignment, Live2D::Cubism::Framework::CubismAllocationAlignedTag tag); +void operator delete(void* address, Live2D::Cubism::Framework::CubismAllocationTag tag); +void operator delete(void* address, Live2D::Cubism::Framework::CubismAllocationAlignedTag tag); + +template +void CsmDelete(T* address) +{ + if (!address) + { + return; + } + + address->~T(); + + operator delete(reinterpret_cast(address), Live2D::Cubism::Framework::GlobalTag); +} + +#define CSM_NEW new(Live2D::Cubism::Framework::GlobalTag) +#define CSM_DELETE_SELF(type, obj) do { if (!obj){ break; } obj->~type(); operator delete(obj, Live2D::Cubism::Framework::GlobalTag); } while(0) +#define CSM_DELETE(obj) CsmDelete(obj) +#define CSM_MALLOC(size) Live2D::Cubism::Framework::CubismFramework::Allocate(size) +#define CSM_MALLOC_ALLIGNED(size, align) Live2D::Cubism::Framework::CubismFramework::AllocateAligned(size, align) +#define CSM_FREE(ptr) Live2D::Cubism::Framework::CubismFramework::Deallocate(ptr) +#define CSM_FREE_ALLIGNED(ptr) Live2D::Cubism::Framework::CubismFramework::DeallocateAligned(ptr) + +#endif + +#define CSM_PLACEMENT_NEW(addrs) new((addrs)) + + +//======================================================== +// Include the definition of types for the each architectures. +//======================================================== +#include "Type/CubismBasicType.hpp" + + +//======================================================== +// Forward Declaration of CubismIdManager. +//======================================================== +namespace Live2D { namespace Cubism { namespace Framework { + +class CubismIdManager; + +}}} + + +//======================================================== +// Compiler Setttings for buiding the Framework. +//======================================================== +#ifdef _MSC_VER +#pragma warning (disable : 4100) +#endif + +#ifndef NULL +#define NULL 0 +#endif + + +//======================================================== +// Macros for validation. +//======================================================== +/** + * Ensures that the expression is valid. + * + * @param expression Expression to be ensured + * @param message Message if the expression is invalid + * @param body Handler if the expression is invalid + */ +#define CubismEnsure(expression, message, body) \ +do \ +{ \ + if (!(expression)) \ + { \ + CubismFramework::CoreLogFunction("[Cubism Framework] " message); \ + body; \ + } \ +} \ +while (0); + + + +//======================================================== +// Ailias of the Framework. +//======================================================== +namespace Csm = Live2D::Cubism::Framework; + + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * Constants. + */ +namespace Constant { +/** Vertex offset of Drawable */ +extern const csmInt32 VertexOffset; + +/** Number of vertices in Drawable */ +extern const csmInt32 VertexStep; +} + +/** + * Entrypoint for the Live2D Cubism SDK + * + * @see #StartUp() + * @see #Initialize() + * @see #Dispose() + * @see #CleanUp() + * + * @note At the start of use, call CubismFramework::StartUp() before calling CubismFramework::Initialize().
+ * At the end, call CubismFramework::Dispose() before calling CubismFramework::CleanUp(). + */ +class CubismFramework +{ +public: + /** + * Framework configurations. + */ + class Option + { + public: + /** + * Enumerator for Logging level + */ + enum LogLevel + { + /** Indicates that the logging of all is enabled. */ + LogLevel_Verbose = 0, + + /** Indicates that up to the logging level of Debug. */ + LogLevel_Debug, + + /** Indicates that up to the logging level of Info. */ + LogLevel_Info, + + /** Indicates that up to the logging level of Warning. */ + LogLevel_Warning, + + /** Indicates that up to the logging level of Error. */ + LogLevel_Error, + + /** Indicates that the logging of all is disabled. */ + LogLevel_Off + }; + + /** Logging function */ + Core::csmLogFunction LogFunction; + + /** Logging level */ + LogLevel LoggingLevel; + }; + + /** + * + * Enables the Cubism Framework API. + * + * @param allocator Instance of memory allocator + * @param option Instance of Framework configuration option.
+ * If an empty instance is set, the Framework configuration option is not used. + * + * @return true if Cubism Framework is available; otherwise false. + * + * @note Make sure to execute this function before executing the API of the Framework,
+ * and be sure to pass the memory allocator to the argument.
+ * Once the preparation is complete, the process will be skipped if executed again afterwards. + */ + static csmBool StartUp(ICubismAllocator* allocator, const Option* option = NULL); + + /** + * Enables the Cubism Framework API to reuse. + * + * @note Clear each parameter of CubismFramework initialized by StartUp().
+ * Use this method when reusing Cubism Framework that has been Dispose(). + */ + static void CleanUp(); + + /** + * Returns whether the API of Cubism Framework is available. + * + * @return true if the API is available; otherwise false. + */ + static csmBool IsStarted(); + + /** + * Initializes the resources in the Cubism Framework and makes the Model ready for display. + */ + static void Initialize(); + + /** + * Releases all resources in the Cubism Framework. + * + * @note Resources allocated externally will not be released.
+ * Discard them externally as appropriate. + */ + static void Dispose(); + + /** + * Returns whether the Cubism Framework resources have been initialized. + * + * @return true if the resource has been initialized; otherwise false. + */ + static csmBool IsInitialized(); + + /** + * Executes the logging function of Cubism Core API. + * + * @param message Message for logging + * + * @note Logging using Option::logFunction. + */ + static void CoreLogFunction(const csmChar* message); + + /** + * Returns the logging level setting. + * + * @return Logging level setting + */ + static Option::LogLevel GetLoggingLevel(); + + /** + * Returns the instance of CubismIdManager. + * + * @note Please use `GetId()` through this function to get `CubismId`.
+ * ex) CubismFramework::GetIdManager()->GetId(id_str); + * + * @return Instance of CubismIdManager. + */ + static CubismIdManager* GetIdManager(); + +#ifdef CSM_DEBUG_MEMORY_LEAKING + + /** + * (For debugging) Allocates the memory. + * + * @param size Desired amount of memory in bytes + * @param fileName Name of source code that called + * @param lineNumber Number of line of source code that called + * + * @return Pointer to the allocated memory if succeeded; otherwise `0` + */ + static void* Allocate(csmSizeType size, const csmChar* fileName, csmInt32 lineNumber); + + /** + * (For debugging) Allocates the memory with specified alignment. + * + * @param size Desired amount of memory in bytes + * @param alignment Desired alignment of memory in bytes + * @param fileName Name of source code that called + * @param lineNumber Number of line of source code that called + * + * @return Pointer to the allocated memory if succeeded; otherwise `0` + */ + static void* AllocateAligned(csmSizeType size, csmUint32 alignment, const csmChar* fileName, csmInt32 lineNumber); + + /** + * (For debugging) Deallocates the aligned memory. + * + * @param address Pointer to allocated memory to be deallocated + * @param fileName Name of source code that called + * @param lineNumber Number of line of source code that called + */ + static void Deallocate(void* address, const csmChar* fileName, csmInt32 lineNumber); + + /** + * (For debugging) Deallocates the aligned memory. + * + * @param address Pointer to allocated memory to be deallocated + * @param fileName Name of source code that called + * @param lineNumber Number of line of source code that called + */ + static void DeallocateAligned(void* address, const csmChar* fileName, csmInt32 lineNumber); + +#else + + /** + * Allocates the memory with specified alignment. + * + * @param size Desired amount of memory in bytes + * @param alignment Desired alignment of memory in bytes + * + * @return PPointer to the allocated memory if succeeded; otherwise `0` + */ + static void* Allocate(csmSizeType size); + + /** + * Allocates the memory with specified alignment. + * + * @param size Desired amount of memory in bytes + * @param alignment Desired alignment of memory in bytes + * + * @return Pointer to the allocated memory if succeeded; otherwise `0` + */ + static void* AllocateAligned(csmSizeType size, csmUint32 alignment); + + /** + * Deallocates the memory. + * + * @param address Pointer to allocated memory to be deallocated + */ + static void Deallocate(void* address); + + /** + * Deallocates the aligned memory. + * + * @param address Pointer to allocated memory to be deallocated + */ + static void DeallocateAligned(void* address); + +#endif + +private: + /** + * Constructor + * + * @note Prevent instantiating and be used as a static class. + */ + CubismFramework(){} + +}; + +}}} +//--------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/CubismFrameworkConfig.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +/** + * A set of macros to configure the Cubism Framework on build. + */ + +/** + * A set of macros to use for debugging Cubism Framework. + */ +/** Runs Cubism Framework as debug mode. */ +// #define CSM_DEBUG + +/** + * Tracks memory leaking of Cubism Framework. + * + * @note Leakage is verified at the termination of Cubism Framework. + */ +// #define CSM_DEBUG_MEMORY_LEAKING + + +/** + * A set of macros to configure the logging level forcefully. + * + * @note The set logging level is applied by changing the definition of a group of macros that call logging in the CubismDebug class. + */ +/** Indicates that the logging of all is enabled. */ +#define CSM_LOG_LEVEL_VERBOSE 0 + +/** Indicates that up to the logging level of Debug. */ +#define CSM_LOG_LEVEL_DEBUG 1 + +/** Indicates that up to the logging level of Info. */ +#define CSM_LOG_LEVEL_INFO 2 + +/** Indicates that up to the logging level of Warning. */ +#define CSM_LOG_LEVEL_WARNING 3 + +/** Indicates that up to the logging level of Error. */ +#define CSM_LOG_LEVEL_ERROR 4 + +/** Indicates that the logging of all is disabled. */ +#define CSM_LOG_LEVEL_OFF 5 + +/** + * Logging level to set + * + * @note Set one of the logging levels from CSM_LOG_LEVEL_VERBOSE to CSM_LOG_LEVEL_OFF. + */ +#define CSM_LOG_LEVEL CSM_LOG_LEVEL_VERBOSE + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/CubismJsonHolder.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "Utils/CubismJson.hpp" + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + /** + * An interface that implements the CubismJson instantiation
+ * and validity checks at each JSON parser. + */ + class CubismJsonHolder + { + public: + /** + * Constructor + */ + CubismJsonHolder() + : _json(NULL) + { } + + /** + * Destructor + */ + virtual ~CubismJsonHolder() + { } + + /** + * Returns whether the CubismJson is valid or not. + * + * @return true if valid; otherwise false + */ + csmBool IsValid() + { + return _json!=nullptr; + } + + protected: + + /** + * Make the instance of CubismJson. + * + * @param buffer Buffer into which JSON is loaded + * @param size Number of bytes in buffer + */ + void CreateCubismJson(const csmByte* buffer, csmSizeInt size) + { + _json = Utils::CubismJson::Create(buffer, size); + + if (!IsValid()) + { + CubismLogError("[CubismJsonHolder] Invalid Json document."); + } + }; + + /** + * Destroy the instance of CubismJson. + */ + void DeleteCubismJson() + { + Utils::CubismJson::Delete(_json); + _json = NULL; + } + + /** Instance of CubismJson */ + Utils::CubismJson* _json; + }; +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/CubismModelSettingJson.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismModelSettingJson.hpp" +#include "CubismFramework.hpp" +#include "Type/csmMap.hpp" +#include "Id/CubismId.hpp" +#include "Id/CubismIdManager.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief Model3Jsonのキー文字列 + * + */ +namespace { + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-variable" +#endif + +// JSON keys +const csmChar* Version = "Version"; +const csmChar* FileReferences = "FileReferences"; +const csmChar* Groups = "Groups"; +const csmChar* Layout = "Layout"; +const csmChar* HitAreas = "HitAreas"; + +const csmChar* Moc = "Moc"; +const csmChar* Textures = "Textures"; +const csmChar* Physics = "Physics"; +const csmChar* DisplayInfo = "DisplayInfo"; +const csmChar* Pose = "Pose"; +const csmChar* Expressions = "Expressions"; +const csmChar* Motions = "Motions"; + +const csmChar* UserData = "UserData"; +const csmChar* Name = "Name"; +const csmChar* FilePath = "File"; +const csmChar* Id = "Id"; +const csmChar* Ids = "Ids"; +const csmChar* Target = "Target"; + +// Motions +const csmChar* Idle = "Idle"; +const csmChar* TapBody = "TapBody"; +const csmChar* PinchIn = "PinchIn"; +const csmChar* PinchOut = "PinchOut"; +const csmChar* Shake = "Shake"; +const csmChar* FlickHead = "FlickHead"; +const csmChar* Parameter = "Parameter"; + +const csmChar* SoundPath = "Sound"; +const csmChar* FadeInTime = "FadeInTime"; +const csmChar* FadeOutTime = "FadeOutTime"; + +// Layout +const csmChar* CenterX = "CenterX"; +const csmChar* CenterY = "CenterY"; +const csmChar* X = "X"; +const csmChar* Y = "Y"; +const csmChar* Width = "Width"; +const csmChar* Height = "Height"; + +const csmChar* LipSync = "LipSync"; +const csmChar* EyeBlink = "EyeBlink"; + +const csmChar* InitParameter = "init_param"; +const csmChar* InitPartsVisible = "init_parts_visible"; +const csmChar* Val = "val"; + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif +} + +// キーが存在するかどうかのチェック +csmBool CubismModelSettingJson::IsExistModelFile() const +{ + Utils::Value& node = (*_jsonValue[FrequentNode_Moc]); + return !node.IsNull() && !node.IsError(); +} +csmBool CubismModelSettingJson::IsExistTextureFiles() const +{ + Utils::Value& node = (*_jsonValue[FrequentNode_Textures]); + return !node.IsNull() && !node.IsError(); +} +csmBool CubismModelSettingJson::IsExistHitAreas() const +{ + Utils::Value& node = (*_jsonValue[FrequentNode_HitAreas]); + return !node.IsNull() && !node.IsError(); +} +csmBool CubismModelSettingJson::IsExistPhysicsFile() const +{ + Utils::Value& node = (*_jsonValue[FrequentNode_Physics]); + return !node.IsNull() && !node.IsError(); +} +csmBool CubismModelSettingJson::IsExistPoseFile() const +{ + Utils::Value& node = (*_jsonValue[FrequentNode_Pose]); + return !node.IsNull() && !node.IsError(); +} +csmBool CubismModelSettingJson::IsExistDisplayInfoFile() const +{ + Utils::Value& node = (*_jsonValue[FrequentNode_DisplayInfo]); + return !node.IsNull() && !node.IsError(); +} +csmBool CubismModelSettingJson::IsExistExpressionFile() const +{ + Utils::Value& node = (*_jsonValue[FrequentNode_Expressions]); + return !node.IsNull() && !node.IsError(); +} +csmBool CubismModelSettingJson::IsExistMotionGroups() const +{ + Utils::Value& node = (*_jsonValue[FrequentNode_Motions]); + return !node.IsNull() && !node.IsError(); +} +csmBool CubismModelSettingJson::IsExistMotionGroupName(const csmChar* groupName) const +{ + Utils::Value& node = (*_jsonValue[FrequentNode_Motions])[groupName]; + return !node.IsNull() && !node.IsError(); +} +csmBool CubismModelSettingJson::IsExistMotionSoundFile(const csmChar* groupName, csmInt32 index) const +{ + Utils::Value& node = (*_jsonValue[FrequentNode_Motions])[groupName][index][SoundPath]; + return !node.IsNull() && !node.IsError(); +} +csmBool CubismModelSettingJson::IsExistMotionFadeIn(const csmChar* groupName, csmInt32 index) const +{ + Utils::Value& node = (*_jsonValue[FrequentNode_Motions])[groupName][index][FadeInTime]; + return !node.IsNull() && !node.IsError(); +} +csmBool CubismModelSettingJson::IsExistMotionFadeOut(const csmChar* groupName, csmInt32 index) const +{ + Utils::Value& node = (*_jsonValue[FrequentNode_Motions])[groupName][index][FadeOutTime]; + return !node.IsNull() && !node.IsError(); +} +csmBool CubismModelSettingJson::IsExistUserDataFile() const { return !_json->GetRoot()[FileReferences][UserData].IsNull(); } + + +csmBool CubismModelSettingJson::IsExistEyeBlinkParameters() const +{ + if (_jsonValue[FrequentNode_Groups]->IsNull() || _jsonValue[FrequentNode_Groups]->IsError()) + { + return false; + } + + for (csmInt32 i = 0; i < _jsonValue[FrequentNode_Groups]->GetSize(); ++i) + { + if (strcmp((*_jsonValue[FrequentNode_Groups])[i][Name].GetRawString(), EyeBlink) == 0) + { + return true; + } + } + return false; +} + +csmBool CubismModelSettingJson::IsExistLipSyncParameters() const +{ + if (_jsonValue[FrequentNode_Groups]->IsNull() || _jsonValue[FrequentNode_Groups]->IsError()) + { + return false; + } + + for (csmInt32 i = 0; i < _jsonValue[FrequentNode_Groups]->GetSize(); ++i) + { + if (strcmp((*_jsonValue[FrequentNode_Groups])[i][Name].GetRawString(), LipSync) == 0) + { + return true; + } + } + return false; +} + +CubismModelSettingJson::CubismModelSettingJson(const csmByte* buffer, csmSizeInt size) +{ + CreateCubismJson(buffer, size); + + if (_json) + { + _jsonValue.Clear(); + + // 順番はenum FrequentNodeと一致させる + _jsonValue.PushBack(&(_json->GetRoot()[Groups])); + _jsonValue.PushBack(&(_json->GetRoot()[FileReferences][Moc])); + _jsonValue.PushBack(&(_json->GetRoot()[FileReferences][Motions])); + _jsonValue.PushBack(&(_json->GetRoot()[FileReferences][DisplayInfo])); + _jsonValue.PushBack(&(_json->GetRoot()[FileReferences][Expressions])); + _jsonValue.PushBack(&(_json->GetRoot()[FileReferences][Textures])); + _jsonValue.PushBack(&(_json->GetRoot()[FileReferences][Physics])); + _jsonValue.PushBack(&(_json->GetRoot()[FileReferences][Pose])); + _jsonValue.PushBack(&(_json->GetRoot()[HitAreas])); + } +} + +CubismModelSettingJson::~CubismModelSettingJson() +{ + DeleteCubismJson(); +} + +Utils::CubismJson* CubismModelSettingJson::GetJsonPointer() const +{ + return _json; +} + +const csmChar* CubismModelSettingJson::GetModelFileName() +{ + if (!IsExistModelFile())return ""; + return (*_jsonValue[FrequentNode_Moc]).GetRawString(); +} + +// テクスチャについて +csmInt32 CubismModelSettingJson::GetTextureCount() +{ + if (!IsExistTextureFiles())return 0; + return (*_jsonValue[FrequentNode_Textures]).GetSize(); +} + +const csmChar* CubismModelSettingJson::GetTextureDirectory() +{ + if (!IsExistTextureFiles()) + { + return ""; + } + + csmVector splitPathBuffer; + csmVector charBuffer; + const csmChar* rawString = (*_jsonValue[FrequentNode_Textures])[0].GetRawString(); + csmInt32 rawStringSize = (*_jsonValue[FrequentNode_Textures])[0].GetString().GetLength(); + for (csmInt32 i = 0; i < rawStringSize; i++) + { + // /を含んでいる場合splitする + if (rawString[i] == '/') + { + csmString str = csmString(charBuffer.GetPtr()); + splitPathBuffer.PushBack(str); + charBuffer.Clear(); + } + // 一文字ずつVector配列に格納する + else + { + charBuffer.PushBack(static_cast(rawString[i] & 0xFF)); + } + } + + csmInt32 arrayLength = splitPathBuffer.GetSize(); + csmString textureDirectoryStr = csmString(); + for(csmInt32 i = 0; i < arrayLength; i++) + { + textureDirectoryStr = textureDirectoryStr + splitPathBuffer[i]; + if(i < arrayLength - 1) + { + textureDirectoryStr = textureDirectoryStr + "/"; + } + } + + return textureDirectoryStr.GetRawString(); +} + +const csmChar* CubismModelSettingJson::GetTextureFileName(csmInt32 index) +{ + return (*_jsonValue[FrequentNode_Textures])[index].GetRawString(); +} + +// あたり判定について +csmInt32 CubismModelSettingJson::GetHitAreasCount() +{ + if (!IsExistHitAreas())return 0; + return (*_jsonValue[FrequentNode_HitAreas]).GetSize(); +} + +CubismIdHandle CubismModelSettingJson::GetHitAreaId(csmInt32 index) +{ + return CubismFramework::GetIdManager()->GetId((*_jsonValue[FrequentNode_HitAreas])[index][Id].GetRawString()); +} + +const csmChar* CubismModelSettingJson::GetHitAreaName(csmInt32 index) +{ + return (*_jsonValue[FrequentNode_HitAreas])[index][Name].GetRawString(); +} + +// 物理演算、表示名称、パーツ切り替え、表情ファイルについて +const csmChar* CubismModelSettingJson::GetPhysicsFileName() +{ + if (!IsExistPhysicsFile())return ""; + return (*_jsonValue[FrequentNode_Physics]).GetRawString(); +} + +const csmChar* CubismModelSettingJson::GetPoseFileName() +{ + if (!IsExistPoseFile())return ""; + return (*_jsonValue[FrequentNode_Pose]).GetRawString(); +} + +const csmChar* CubismModelSettingJson::GetDisplayInfoFileName() +{ + if (!IsExistDisplayInfoFile())return ""; + return (*_jsonValue[FrequentNode_DisplayInfo]).GetRawString(); +} + +csmInt32 CubismModelSettingJson::GetExpressionCount() +{ + if (!IsExistExpressionFile())return 0; + return (*_jsonValue[FrequentNode_Expressions]).GetSize(); +} + +const csmChar* CubismModelSettingJson::GetExpressionName(csmInt32 index) +{ + return (*_jsonValue[FrequentNode_Expressions])[index][Name].GetRawString(); +} + +const csmChar* CubismModelSettingJson::GetExpressionFileName(csmInt32 index) +{ + return (*_jsonValue[FrequentNode_Expressions])[index][FilePath].GetRawString(); +} + +// モーションについて +csmInt32 CubismModelSettingJson::GetMotionGroupCount() +{ + if (!IsExistMotionGroups()) + { + return 0; + } + return (*_jsonValue[FrequentNode_Motions]).GetKeys().GetSize(); +} + +const csmChar* CubismModelSettingJson::GetMotionGroupName(csmInt32 index) +{ + if (!IsExistMotionGroups()) + { + return NULL; + } + return (*_jsonValue[FrequentNode_Motions]).GetKeys()[index].GetRawString(); +} + +csmInt32 CubismModelSettingJson::GetMotionCount(const csmChar* groupName) +{ + if (!IsExistMotionGroupName(groupName))return 0; + return (*_jsonValue[FrequentNode_Motions])[groupName].GetSize(); +} + +const csmChar* CubismModelSettingJson::GetMotionFileName(const csmChar* groupName, csmInt32 index) +{ + if (!IsExistMotionGroupName(groupName))return ""; + return (*_jsonValue[FrequentNode_Motions])[groupName][index][FilePath].GetRawString(); +} + +const csmChar* CubismModelSettingJson::GetMotionSoundFileName(const csmChar* groupName, csmInt32 index) +{ + if (!IsExistMotionSoundFile(groupName, index))return ""; + return (*_jsonValue[FrequentNode_Motions])[groupName][index][SoundPath].GetRawString(); +} + +csmFloat32 CubismModelSettingJson::GetMotionFadeInTimeValue(const csmChar* groupName, csmInt32 index) +{ + if (!IsExistMotionFadeIn(groupName, index))return -1.0f; + return (*_jsonValue[FrequentNode_Motions])[groupName][index][FadeInTime].ToFloat(); +} + +csmFloat32 CubismModelSettingJson::GetMotionFadeOutTimeValue(const csmChar* groupName, csmInt32 index) +{ + if (!IsExistMotionFadeOut(groupName, index))return -1.0f; + return (*_jsonValue[FrequentNode_Motions])[groupName][index][FadeOutTime].ToFloat(); +} + + +const csmChar* CubismModelSettingJson::GetUserDataFile() +{ + if (!IsExistUserDataFile()) + { + return ""; + } + return _json->GetRoot()[FileReferences][UserData].GetRawString(); +} + +csmBool CubismModelSettingJson::GetLayoutMap(csmMap& outLayoutMap) +{ + csmMap* map = _json->GetRoot()[Layout].GetMap(); + if (map == NULL) + { + return false; + } + csmMap::const_iterator map_ite; + csmBool ret = false; + for (map_ite = map->Begin(); map_ite != map->End(); ++map_ite) + { + outLayoutMap[map_ite->First] = map_ite->Second->ToFloat(); + ret = true; + } + return ret; +} + +csmInt32 CubismModelSettingJson::GetEyeBlinkParameterCount() +{ + if (!IsExistEyeBlinkParameters()) + { + return 0; + } + + csmInt32 num = 0; + for (csmInt32 i = 0; i < _jsonValue[FrequentNode_Groups]->GetSize(); i++) + { + Utils::Value& refI = (*_jsonValue[FrequentNode_Groups])[i]; + if(refI.IsNull() || refI.IsError()) + { + continue; + } + + if (strcmp(refI[Name].GetRawString(), EyeBlink) == 0) + { + num = refI[Ids].GetVector()->GetSize(); + break; + } + } + + return num; +} + +CubismIdHandle CubismModelSettingJson::GetEyeBlinkParameterId(csmInt32 index) +{ + if (!IsExistEyeBlinkParameters()) + { + return NULL; + } + + for (csmInt32 i = 0; i < _jsonValue[FrequentNode_Groups]->GetSize(); i++) + { + Utils::Value& refI = (*_jsonValue[FrequentNode_Groups])[i]; + if (refI.IsNull() || refI.IsError()) + { + continue; + } + + if (strcmp(refI[Name].GetRawString(), EyeBlink) == 0) + { + return CubismFramework::GetIdManager()->GetId(refI[Ids][index].GetRawString()); + } + } + return NULL; +} + +csmInt32 CubismModelSettingJson::GetLipSyncParameterCount() +{ + if (!IsExistLipSyncParameters()) + { + return 0; + } + + csmInt32 num = 0; + for (csmInt32 i = 0; i < _jsonValue[FrequentNode_Groups]->GetSize(); i++) + { + Utils::Value& refI = (*_jsonValue[FrequentNode_Groups])[i]; + if (refI.IsNull() || refI.IsError()) + { + continue; + } + + if (strcmp(refI[Name].GetRawString(), LipSync) == 0) + { + num = refI[Ids].GetVector()->GetSize(); + break; + } + } + + return num; +} + +CubismIdHandle CubismModelSettingJson::GetLipSyncParameterId(csmInt32 index) +{ + if (!IsExistLipSyncParameters()) + { + return NULL; + } + + for (csmInt32 i = 0; i < _jsonValue[FrequentNode_Groups]->GetSize(); i++) + { + Utils::Value& refI = (*_jsonValue[FrequentNode_Groups])[i]; + if (refI.IsNull() || refI.IsError()) + { + continue; + } + + if (strcmp(refI[Name].GetRawString(), LipSync) == 0) + { + return CubismFramework::GetIdManager()->GetId(refI[Ids][index].GetRawString()); + } + } + return NULL; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/CubismModelSettingJson.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "ICubismModelSetting.hpp" +#include "CubismJsonHolder.hpp" +#include "Utils/CubismJson.hpp" +#include "Id/CubismId.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * Handles the Model Settings File + */ +class CubismModelSettingJson : public ICubismModelSetting, public CubismJsonHolder +{ +public: + + /** + * Constructor
+ * Loads the Model Settings File. + * + * @param buffer Buffer into which the Model Settings File is loaded + * @param size Number of bytes in buffer + */ + CubismModelSettingJson(const csmByte* buffer, csmSizeInt size); + + /** + * Destructor + */ + virtual ~CubismModelSettingJson(); + + /** + * Returns the instance of CubismJson that handles the JSON document. + * + * @return Instance of CubismJson + */ + Utils::CubismJson* GetJsonPointer() const; + + /** + * Returns the file name of MOC3 File. + * + * @return Name of MOC3 File + */ + const csmChar* GetModelFileName(); + + /** + * Returns the number of textures in the Model. + * + * @return Number of textures + */ + csmInt32 GetTextureCount(); + + /** + * Returns the name of directory that texture is placed. + * + * @return Name of directory + */ + const csmChar* GetTextureDirectory(); + + /** + * Returns the name of texture in the Model. + * + * @param index Index to the desired texture name + * + * @return Name of texture + */ + const csmChar* GetTextureFileName(csmInt32 index); + + /** + * Returns the number of Hit Area settings in the Model. + * + * @return Number of Hit Area settings + */ + csmInt32 GetHitAreasCount(); + + /** + * Returns whether ID of Drawable that is set for the Hit Area. + * + * @param groupName Name of the desired Motion Group + * + * @return true if Motion Group information exists; otherwise false + */ + CubismIdHandle GetHitAreaId(csmInt32 index); + + /** + * Returns the name of Hit Area. + * + * @param index Index to the desired Hit Area + * + * @return Name of Hit Area + */ + const csmChar* GetHitAreaName(csmInt32 index); + + /** + * Returns the name of Physics Settings File in the Model. + * + * @return Name of Physics Settings File + */ + const csmChar* GetPhysicsFileName(); + + /** + * Returns the name of Pose Settings File in the Model. + * + * @return Name of Pose Settings File + */ + const csmChar* GetPoseFileName(); + + /** + * Returns the name of Display Settings File in the Model. + * + * @return Name of Display Settings File + */ + const csmChar* GetDisplayInfoFileName(); + + /** + * Returns the number of expressions in the Model. + * + * @return Number of expressions + */ + csmInt32 GetExpressionCount(); + + /** + * Returns the name of expression. + * + * @param index Index to the desired expression + * + * @return Name of expression + */ + const csmChar* GetExpressionName(csmInt32 index); + + /** + * Returns the name of Expression Settings File from the expression. + * + * @param index Index to the desired expression + * + * @return Name of Expression Settings File + */ + const csmChar* GetExpressionFileName(csmInt32 index); + + /** + * Returns the number of Motion Groups in the Model. + * + * @return Number of Motion Groups + */ + csmInt32 GetMotionGroupCount(); + + /** + * Returns the name of Motion Group. + * + * @param index Index to the desired Motion Group + * + * @return Name of Motion Group + */ + const csmChar* GetMotionGroupName(csmInt32 index); + + /** + * Returns the number of Motions in the Motion Group. + * + * @param groupName Name to the desired Motion Group + * + * @return Number of Motions + */ + csmInt32 GetMotionCount(const csmChar* groupName); + + /** + * Returns the file name of Motion File in the Motion Group. + * + * @param groupName Name to the desired Motion Group + * @param index Index to the desired motion + * + * @return Name of Motion File + */ + const csmChar* GetMotionFileName(const csmChar* groupName, csmInt32 index); + + /** + * Returns the name of Audio File attached to the Motion in the Motion Group. + * + * @param groupName Name to the desired Motion Group + * @param index Index to the desired Motion + * + * @return Name of Audio File + */ + const csmChar* GetMotionSoundFileName(const csmChar* groupName, csmInt32 index); + + /** + * Returns the Fade-in time at the start of the Motion in the Motion Group + * + * @param groupName Name to the desired Motion Group + * @param index Index to the desired Motion + * + * @return Fade-in time [sec] + */ + csmFloat32 GetMotionFadeInTimeValue(const csmChar* groupName, csmInt32 index); + + /** + * Returns the Fade-out time at the end of the Motion in the Motion Group. + * + * @param groupName Name to the desired Motion Group + * @param index Index to the desired Motion + * + * @return Fade-out time [sec] + */ + csmFloat32 GetMotionFadeOutTimeValue(const csmChar* groupName, csmInt32 index); + + /** + * Returns the name of User Data File in the Model. + * + * @return Name of User Data File + */ + const csmChar* GetUserDataFile(); + + /** + * Returns the Layout Information set in the model. + * + * @param outLayoutMap Instance of a collection to store + * + * @return true if Layout Information exists; otherwise false + */ + csmBool GetLayoutMap(csmMap& outLayoutMap); + + /** + * Returns the number of parameters attached to the Eye Blinking Settings. + * + * @return Number of parameters + */ + csmInt32 GetEyeBlinkParameterCount(); + + /** + * Returns the ID of parameter attached to the Eye Blinking Settings. + * + * @param index Index to the desired parameter + * + * @return ID of parameter + */ + CubismIdHandle GetEyeBlinkParameterId(csmInt32 index); + + /** + * Returns the number of parameters attached to the Lip-sync Settings. + * + * @return Number of parameters + */ + csmInt32 GetLipSyncParameterCount(); + + /** + * Returns the ID of parameter attached to the Lip-sync Settings. + * + * @param index Index to the desired parameter + * + * @return ID of parameter + */ + CubismIdHandle GetLipSyncParameterId(csmInt32 index); + +private: + + enum FrequentNode + { + FrequentNode_Groups, + FrequentNode_Moc, + FrequentNode_Motions, + FrequentNode_DisplayInfo, + FrequentNode_Expressions, + FrequentNode_Textures, + FrequentNode_Physics, + FrequentNode_Pose, + FrequentNode_HitAreas, + }; + + /** + * Returns whether the MOC3 File information exists in the Model Settings File. + * + * @return true if MOC3 File information exists; otherwise false + */ + csmBool IsExistModelFile() const; + + /** + * Returns whether Texture File information exists in the Model Settings File. + * + * @return true if Texture File information exists; otherwise false + */ + csmBool IsExistTextureFiles() const; + + /** + * Returns whether Hit Area information exists in the Model Settings File. + * + * @return true if Hit Area information exists; otherwise false + */ + csmBool IsExistHitAreas() const; + + /** + * Returns whether Physics Settings File information exists in the Model Settings File. + * + * @return true if Physics Settings File information exists; otherwise false + */ + csmBool IsExistPhysicsFile() const; + + /** + * Returns whether Pose Settings File information exists in the Model Settings File. + * + * @return true if Pose Settings File information exists; otherwise false + */ + csmBool IsExistPoseFile() const; + + /** + * Returns whether Display Settings File information exists in the Model Settings File. + * + * @return true if Display Settings File information exists; otherwise false + */ + csmBool IsExistDisplayInfoFile() const; + + /** + * Returns whether Expression File information exists in the Model Settings File. + * + * @return true if Expression File information exists; otherwise false + */ + csmBool IsExistExpressionFile() const; + + /** + * Returns whether Motion Group information exists in the Model Settings File. + * + * @return true if Motion Group information exists; otherwise false + */ + csmBool IsExistMotionGroups() const; + + /** + * Returns the Motion Group information with the specified name in the Model Settings File. + * + * @param groupName Name of the desired Motion Group + * + * @return true if Motion Group information exists; otherwise false + */ + csmBool IsExistMotionGroupName(const csmChar* groupName) const; + + /** + * Returns whether the Audio File information in the Motion Group with the specified name in the Model Settings File. + * + * @param groupName Name of the desired Motion Group + * @param index Index to the desired Motion + * + * @return true if Audio File information exists; otherwise false + */ + csmBool IsExistMotionSoundFile(const csmChar* groupName, csmInt32 index) const; + + /** + * Returns whether the Fade-in time at the start of Motion in Motion Group with the specified name exists in Model Setting File. + * + * @param groupName Name of the desired Motion Group + * @param index Index to the desired Motion + * + * @return true if Fade-in time exists; otherwise false + */ + csmBool IsExistMotionFadeIn(const csmChar* groupName, csmInt32 index) const; + + /** + * Returns whether the Fade-out time at the end of the Motion in the Motion Group with the specified name in the Model Settings File. + * + * @param groupName Name of the desired Motion Group + * @param index Index to the desired Motion + * + * @return true if Fade-out time exists; otherwise false + */ + csmBool IsExistMotionFadeOut(const csmChar* groupName, csmInt32 index) const; + + /** + * Returns whether the User Data File information exists in the Model Settings File. + * + * @return true if User Data File information exists; otherwise false + */ + csmBool IsExistUserDataFile() const; + + /** + * Returns whether the parameter information attached to the Eye Blinking Settings exists in the Model Settings File. + * + * @return true if parameter information exists; otherwise false + */ + csmBool IsExistEyeBlinkParameters() const; + + /** + * Returns whether the parameter information attached to the Lip-sync Settings exists in the Model Settings File. + * + * @return true if parameter information exists; otherwise false + */ + csmBool IsExistLipSyncParameters() const; + + /** Cache of JSON nodes */ + csmVector _jsonValue; +}; +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Effect/CubismBreath.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismBreath.hpp" +#include + +namespace Live2D { namespace Cubism { namespace Framework { + +CubismBreath* CubismBreath::Create() +{ + return CSM_NEW CubismBreath(); +} + +void CubismBreath::Delete(CubismBreath* instance) +{ + CSM_DELETE_SELF(CubismBreath, instance); +} + +CubismBreath::CubismBreath() + : _currentTime(0.0f) +{ } + +CubismBreath::~CubismBreath() +{ } + +void CubismBreath::SetParameters(const csmVector& breathParameters) +{ + _breathParameters = breathParameters; +} + +const csmVector& CubismBreath::GetParameters() const +{ + return _breathParameters; +} + +void CubismBreath::UpdateParameters(CubismModel* model, csmFloat32 deltaTimeSeconds) +{ + _currentTime += deltaTimeSeconds; + + const csmFloat32 t = _currentTime * 2.0f * 3.14159f; + + for (csmUint32 i = 0; i < _breathParameters.GetSize(); ++i) + { + BreathParameterData* data = &_breathParameters[i]; + + model->AddParameterValue(data->ParameterId, data->Offset + (data->Peak * sinf(t / data->Cycle)), data->Weight); + } +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Effect/CubismBreath.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "Model/CubismModel.hpp" +#include "Id/CubismId.hpp" +#include "Type/csmVector.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief 呼吸機能 + * + * 呼吸機能を提供する。 + */ +class CubismBreath +{ +public: + /** + * @brief 呼吸のパラメータ情報 + * + * 呼吸のパラメータ情報。 + */ + struct BreathParameterData + { + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + BreathParameterData() + : ParameterId(NULL) + , Offset(0.0f) + , Peak(0.0f) + , Cycle(0.0f) + , Weight(0.0f) + { } + + /** + * @brief コンストラクタ + * + * コンストラクタ。 + * + * @param[in] parameterId 呼吸をひもづけるパラメータID + * @param[in] offset 呼吸を正弦波としたときの、波のオフセット + * @param[in] peak 呼吸を正弦波としたときの、波の高さ + * @param[in] cycle 呼吸を正弦波としたときの、波の周期 + * @param[in] weight パラメータへの重み + */ + BreathParameterData(CubismIdHandle parameterId, csmFloat32 offset, csmFloat32 peak, csmFloat32 cycle, csmFloat32 weight) + : ParameterId(parameterId) + , Offset(offset) + , Peak(peak) + , Cycle(cycle) + , Weight(weight) + { } + + CubismIdHandle ParameterId; ///< 呼吸をひもづけるパラメータID + csmFloat32 Offset; ///< 呼吸を正弦波としたときの、波のオフセット + csmFloat32 Peak; ///< 呼吸を正弦波としたときの、波の高さ + csmFloat32 Cycle; ///< 呼吸を正弦波としたときの、波の周期 + csmFloat32 Weight; ///< パラメータへの重み + }; + + /** + * @brief インスタンスの作成 + * + * インスタンスを作成する。 + */ + static CubismBreath* Create(); + + + /** + * @brief インスタンスの破棄 + * + * インスタンスを破棄する。 + * + * @param[in] instance 対象のCubismBreath + */ + static void Delete(CubismBreath* instance); + + /** + * @brief 呼吸のパラメータのひもづけ + * + * 呼吸のパラメータをひもづける。 + * + * @param[in] breathParameters 呼吸をひもづけたいパラメータのリスト + */ + void SetParameters(const csmVector& breathParameters); + + + /** + * @brief 呼吸にひもづいているパラメータの取得 + * + * 呼吸にひもづいているパラメータを取得する。 + * + * @return 呼吸にひもづいているパラメータのリスト + */ + const csmVector& GetParameters() const; + + + /** + * @brief モデルのパラメータの更新 + * + * モデルのパラメータを更新する。 + * + * @param[in] model 対象のモデル + * @param[in] deltaTimeSeconds デルタ時間[秒] + */ + void UpdateParameters(CubismModel* model, csmFloat32 deltaTimeSeconds); + +private: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismBreath(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismBreath(); + + csmVector _breathParameters; ///< 呼吸にひもづいているパラメータのリスト + csmFloat32 _currentTime; ///< 積算時間[秒] +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Effect/CubismEyeBlink.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismEyeBlink.hpp" +#include "Id/CubismId.hpp" +#include + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * IDで指定された目のパラメータが、0のときに閉じるなら true 、1の時に閉じるなら false 。 + */ +const csmBool CloseIfZero = true; + +CubismEyeBlink* CubismEyeBlink::Create(ICubismModelSetting* modelSetting) +{ + return CSM_NEW CubismEyeBlink(modelSetting); +} + +void CubismEyeBlink::Delete(CubismEyeBlink* eyeBlink) +{ + CSM_DELETE_SELF(CubismEyeBlink, eyeBlink); +} + +CubismEyeBlink::CubismEyeBlink(ICubismModelSetting* modelSetting) + : _blinkingState(EyeState_First) + , _nextBlinkingTime(0.0f) + , _stateStartTimeSeconds(0.0f) + , _blinkingIntervalSeconds(4.0f) + , _closingSeconds(0.1f) + , _closedSeconds(0.05f) + , _openingSeconds(0.15f) + , _userTimeSeconds(0.0f) +{ + if (modelSetting == NULL) + { + return; + } + + for (csmInt32 i = 0; i < modelSetting->GetEyeBlinkParameterCount(); ++i) + { + _parameterIds.PushBack(modelSetting->GetEyeBlinkParameterId(i)); + } +} + +CubismEyeBlink::~CubismEyeBlink() +{ } + +csmFloat32 CubismEyeBlink::DetermineNextBlinkingTiming() const +{ + const csmFloat32 r = static_cast(rand()) / RAND_MAX; + + return _userTimeSeconds + (r * (2.0f * _blinkingIntervalSeconds - 1.0f)); +} + +void CubismEyeBlink::SetBlinkingInterval(csmFloat32 blinkingInterval) +{ + _blinkingIntervalSeconds = blinkingInterval; +} + +void CubismEyeBlink::SetBlinkingSettings(csmFloat32 closing, csmFloat32 closed, csmFloat32 opening) +{ + _closingSeconds = closing; + _closedSeconds = closed; + _openingSeconds = opening; +} + +void CubismEyeBlink::SetParameterIds(const csmVector& parameterIds) +{ + _parameterIds = parameterIds; +} + +const csmVector& CubismEyeBlink::GetParameterIds() const +{ + return _parameterIds; +} + +void CubismEyeBlink::UpdateParameters(CubismModel* model, csmFloat32 deltaTimeSeconds) +{ + _userTimeSeconds += deltaTimeSeconds; + csmFloat32 parameterValue; + csmFloat32 t = 0.0f; + + switch (_blinkingState) + { + case EyeState_Closing: + t = ((_userTimeSeconds - _stateStartTimeSeconds) / _closingSeconds); + + if (t >= 1.0f) + { + t = 1.0f; + _blinkingState = EyeState_Closed; + _stateStartTimeSeconds = _userTimeSeconds; + } + + parameterValue = 1.0f - t; + + break; + case EyeState_Closed: + t = ((_userTimeSeconds - _stateStartTimeSeconds) / _closedSeconds); + + if (t >= 1.0f) + { + _blinkingState = EyeState_Opening; + _stateStartTimeSeconds = _userTimeSeconds; + } + + parameterValue = 0.0f; + + break; + case EyeState_Opening: + t = ((_userTimeSeconds - _stateStartTimeSeconds) /_openingSeconds); + + if (t >= 1.0f) + { + t = 1.0f; + _blinkingState = EyeState_Interval; + _nextBlinkingTime = DetermineNextBlinkingTiming(); + } + + parameterValue = t; + + break; + case EyeState_Interval: + if (_nextBlinkingTime < _userTimeSeconds) + { + _blinkingState = EyeState_Closing; + _stateStartTimeSeconds = _userTimeSeconds; + } + + parameterValue = 1.0f; + + break; + case EyeState_First: + default: + _blinkingState = EyeState_Interval; + _nextBlinkingTime = DetermineNextBlinkingTiming(); + + parameterValue = 1.0f; + + break; + } + + if (!CloseIfZero) + { + parameterValue = -parameterValue; + } + + for (csmUint32 i = 0; i < _parameterIds.GetSize(); ++i) + { + model->SetParameterValue(_parameterIds[i], parameterValue); + } +} + +}}} + + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Effect/CubismEyeBlink.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "Model/CubismModel.hpp" +#include "Type/csmVector.hpp" +#include "Id/CubismId.hpp" +#include "ICubismModelSetting.hpp" + + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief 自動まばたき機能 + * + * 自動まばたき機能を提供する。 + * + */ +class CubismEyeBlink +{ +public: + /** + * @brief まばたきの状態 + * + * まばたきの状態を表す列挙型。 + */ + enum EyeState + { + EyeState_First = 0, ///< 初期状態 + EyeState_Interval, ///< まばたきしていない状態 + EyeState_Closing, ///< まぶたが閉じていく途中の状態 + EyeState_Closed, ///< まぶたが閉じている状態 + EyeState_Opening ///< まぶたが開いていく途中の状態 + }; + + /** + * @brief インスタンスの作成 + * + * インスタンスを作成する。 + * + * @param[in] modelSetting モデルの設定情報 + * @return 作成されたインスタンス + * @note 引数がNULLの場合、パラメータIDが設定されていない空のインスタンスを作成する。 + */ + static CubismEyeBlink* Create(ICubismModelSetting* modelSetting = NULL); + + /** + * @brief インスタンスの破棄 + * + * インスタンスを破棄する。 + * + * @param[in] eyeBlink 対象のCubismEyeBlink + */ + static void Delete(CubismEyeBlink* eyeBlink); + + /** + * @brief まばたきの間隔の設定 + * + * まばたきの間隔を設定する。 + * + * @param[in] blinkingInterval まばたきの間隔の時間[秒] + */ + void SetBlinkingInterval(csmFloat32 blinkingInterval); + + /** + * @brief またばきのモーションの詳細設定 + * + * まばたきのモーションの詳細設定を行う。 + * + * @param[in] closing まぶたを閉じる動作の所要時間[秒] + * @param[in] closed まぶたを閉じている動作の所要時間[秒] + * @param[in] opening まぶたを開く動作の所要時間[秒] + */ + void SetBlinkingSettings(csmFloat32 closing, csmFloat32 closed, csmFloat32 opening); + + /** + * @brief まばたきさせるパラメータIDのリストの設定 + * + * まばたきさせるパラメータIDのリストを設定する。 + * + * @param[in] parameterIds パラメータIDのリスト + */ + void SetParameterIds(const csmVector& parameterIds); + + /** + * @brief まばたきさせるパラメータIDのリストの取得 + * + * まばたきさせるパラメータIDのリストを取得する。 + * + * @return パラメータIDのリスト + */ + const csmVector& GetParameterIds() const; + + /** + * @brief モデルのパラメータの更新 + * + * モデルのパラメータを更新する。 + * + * @param[in] model 対象のモデル + * @param[in] deltaTimeSeconds デルタ時間[秒] + */ + void UpdateParameters(CubismModel* model, csmFloat32 deltaTimeSeconds); + +private: + + /** + * @brief コンストラクタ + * + * コンストラクタ。 + * + * @param[in] modelSetting モデルの設定情報 + */ + CubismEyeBlink(ICubismModelSetting* modelSetting); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismEyeBlink(); + + /** + * @brief 次のまばたきのタイミングの決定 + * + * 次のまばたきのタイミングを決定する。 + * + * @return 次のまばたきを行う時刻[秒] + */ + csmFloat32 DetermineNextBlinkingTiming() const; + + csmInt32 _blinkingState; ///< 現在の状態 + csmVector _parameterIds; ///< 操作対象のパラメータのIDのリスト + csmFloat32 _nextBlinkingTime; ///< 次のまばたきの時刻[秒] + csmFloat32 _stateStartTimeSeconds; ///< 現在の状態が開始した時刻[秒] + csmFloat32 _blinkingIntervalSeconds; ///< まばたきの間隔[秒] + csmFloat32 _closingSeconds; ///< まぶたを閉じる動作の所要時間[秒] + csmFloat32 _closedSeconds; ///< まぶたを閉じている動作の所要時間[秒] + csmFloat32 _openingSeconds; ///< まぶたを開く動作の所要時間[秒] + csmFloat32 _userTimeSeconds; ///< デルタ時間の積算値[秒] + +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Effect/CubismPose.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismPose.hpp" +#include "Id/CubismIdManager.hpp" + +using namespace Live2D::Cubism::Framework; + +namespace Live2D { namespace Cubism { namespace Framework { + +namespace { +const csmFloat32 Epsilon= 0.001f; +const csmFloat32 DefaultFadeInSeconds = 0.5f; + +// Pose.jsonのタグ +const csmChar* FadeIn = "FadeInTime"; +const csmChar* Link = "Link"; +const csmChar* Groups = "Groups"; +const csmChar* Id = "Id"; +} + +CubismPose::PartData::PartData() +{ } + +CubismPose::PartData::~PartData() +{ } + +CubismPose::PartData::PartData(const PartData& v) + : ParameterIndex(0) + , PartIndex(0) +{ + PartId = v.PartId; + + for (csmVector::const_iterator ite = v.Link.Begin(); ite != v.Link.End(); ++ite) + { + Link.PushBack(*ite); + } +} + +CubismPose::PartData& CubismPose::PartData::operator=(const PartData& v) +{ + PartId = v.PartId; + + for (csmVector::const_iterator ite = v.Link.Begin(); ite != v.Link.End(); ++ite) + { + Link.PushBack(*ite); + } + + return (*this); +} + +void CubismPose::PartData::Initialize(CubismModel* model) +{ + ParameterIndex = model->GetParameterIndex(PartId); + PartIndex = model->GetPartIndex(PartId); + + model->SetParameterValue(ParameterIndex, 1); +} + +CubismPose::CubismPose() : _fadeTimeSeconds(DefaultFadeInSeconds) + , _lastModel(NULL) +{ } + +CubismPose::~CubismPose() +{ } + +CubismPose* CubismPose::Create(const csmByte* pose3json, csmSizeInt size) +{ + Utils::CubismJson* json = Utils::CubismJson::Create(pose3json, size); + if (!json) + { + return NULL; + } + CubismPose* ret = CSM_NEW CubismPose(); + Utils::Value& root = json->GetRoot(); + + // フェード時間の指定 + if (!root[FadeIn].IsNull()) + { + ret->_fadeTimeSeconds = root[FadeIn].ToFloat(DefaultFadeInSeconds); + + if (ret->_fadeTimeSeconds < 0.0f) + { + ret->_fadeTimeSeconds = DefaultFadeInSeconds; + } + } + + // パーツグループ + Utils::Value& poseListInfo = root[Groups]; + const csmInt32 poseCount = poseListInfo.GetSize(); + + for (csmInt32 poseIndex = 0; poseIndex < poseCount; ++poseIndex) + { + Utils::Value& idListInfo = poseListInfo[poseIndex]; + const csmInt32 idCount = idListInfo.GetSize(); + csmInt32 groupCount = 0; + + for (csmInt32 groupIndex = 0; groupIndex < idCount; ++groupIndex) + { + Utils::Value& partInfo = idListInfo[groupIndex]; + PartData partData; + const CubismIdHandle parameterId = CubismFramework::GetIdManager()->GetId(partInfo[Id].GetRawString()); + + partData.PartId = parameterId; + + // リンクするパーツの設定 + if (!partInfo[Link].IsNull()) + { + Utils::Value& linkListInfo = partInfo[Link]; + const csmInt32 linkCount = linkListInfo.GetSize(); + + for (csmInt32 linkIndex = 0; linkIndex < linkCount; ++linkIndex) + { + PartData linkPart; + const CubismIdHandle linkId = CubismFramework::GetIdManager()->GetId(linkListInfo[linkIndex].GetString()); + + linkPart.PartId = linkId; + + partData.Link.PushBack(linkPart); + } + } + + ret->_partGroups.PushBack(partData); + + ++groupCount; + } + + ret->_partGroupCounts.PushBack(groupCount); + + } + + Utils::CubismJson::Delete(json); + + return ret; +} + +void CubismPose::Delete(CubismPose* pose) +{ + CSM_DELETE_SELF(CubismPose, pose); +} + +void CubismPose::Reset(CubismModel* model) +{ + csmInt32 beginIndex = 0; + + for (csmUint32 i = 0; i < _partGroupCounts.GetSize(); ++i) + { + const csmInt32 groupCount = _partGroupCounts[i]; + + for (csmInt32 j = beginIndex; j < beginIndex + groupCount; ++j) + { + _partGroups[j].Initialize(model); + + const csmInt32 partsIndex = _partGroups[j].PartIndex; + const csmInt32 paramIndex = _partGroups[j].ParameterIndex; + + if (partsIndex < 0) + { + continue; + } + + model->SetPartOpacity( partsIndex, (j == beginIndex ? 1.0f : 0.0f)); + model->SetParameterValue( paramIndex, (j == beginIndex ? 1.0f : 0.0f)); + + for (csmUint32 k = 0; k < _partGroups[j].Link.GetSize(); ++k) + { + _partGroups[j].Link[k].Initialize(model); + } + } + + beginIndex += groupCount; + + } + +} + +void CubismPose::CopyPartOpacities(CubismModel* model) +{ + for (csmUint32 groupIndex = 0; groupIndex < _partGroups.GetSize(); ++groupIndex) + { + PartData& partData = _partGroups[groupIndex]; + + if (partData.Link.GetSize() == 0) + { + continue; // 連動するパラメータはない + } + + const csmInt32 partIndex = _partGroups[groupIndex].PartIndex; + const csmFloat32 opacity = model->GetPartOpacity(partIndex); + + for (csmUint32 linkIndex = 0; linkIndex < partData.Link.GetSize(); ++linkIndex) + { + PartData& linkPart = partData.Link[linkIndex]; + const csmInt32 linkPartIndex = linkPart.PartIndex; + + if (linkPartIndex < 0) + { + continue; + } + + model->SetPartOpacity(linkPartIndex, opacity); + } + } +} + +void CubismPose::DoFade(CubismModel* model, csmFloat32 deltaTimeSeconds, csmInt32 beginIndex, csmInt32 partGroupCount) +{ + csmInt32 visiblePartIndex = -1; + csmFloat32 newOpacity = 1.0f; + + const csmFloat32 Phi = 0.5f; + const csmFloat32 BackOpacityThreshold = 0.15f; + + // 現在、表示状態になっているパーツを取得 + for (csmInt32 i = beginIndex; i < beginIndex + partGroupCount; ++i) + { + const csmInt32 partIndex = _partGroups[i].PartIndex; + const csmInt32 paramIndex = _partGroups[i].ParameterIndex; + + if (model->GetParameterValue(paramIndex) > Epsilon) + { + if (visiblePartIndex >= 0) + { + break; + } + + visiblePartIndex = i; + newOpacity = model->GetPartOpacity(partIndex); + + // 新しい不透明度を計算 + newOpacity += (deltaTimeSeconds / _fadeTimeSeconds); + + if (newOpacity > 1.0f) + { + newOpacity = 1.0f; + } + } + } + + if (visiblePartIndex < 0) + { + visiblePartIndex = 0; + newOpacity = 1.0f; + } + + // 表示パーツ、非表示パーツの不透明度を設定する + for (csmInt32 i = beginIndex; i < beginIndex + partGroupCount; ++i) + { + const csmInt32 partsIndex = _partGroups[i].PartIndex; + + // 表示パーツの設定 + if (visiblePartIndex == i) + { + model->SetPartOpacity(partsIndex, newOpacity); // 先に設定 + } + // 非表示パーツの設定 + else + { + csmFloat32 opacity = model->GetPartOpacity(partsIndex); + csmFloat32 a1; // 計算によって求められる不透明度 + + if (newOpacity < Phi) + { + a1 = newOpacity * (Phi - 1) / Phi + 1.0f; // (0,1),(phi,phi)を通る直線式 + } + else + { + a1 = (1 - newOpacity) * Phi / (1.0f - Phi); // (1,0),(phi,phi)を通る直線式 + } + + // 背景の見える割合を制限する場合 + const csmFloat32 backOpacity = (1.0f - a1) * (1.0f - newOpacity); + + if (backOpacity > BackOpacityThreshold) + { + a1 = 1.0f - BackOpacityThreshold / (1.0f - newOpacity); + } + + if (opacity > a1) + { + opacity = a1; // 計算の不透明度よりも大きければ(濃ければ)不透明度を上げる + } + + model->SetPartOpacity(partsIndex, opacity); + } + } +} + +void CubismPose::UpdateParameters(CubismModel* model, csmFloat32 deltaTimeSeconds) +{ + // 前回のモデルと同じではないときは初期化が必要 + if (model != _lastModel) + { + // パラメータインデックスの初期化 + Reset(model); + } + + _lastModel = model; + + // 設定から時間を変更すると、経過時間がマイナスになることがあるので、経過時間0として対応。 + if (deltaTimeSeconds < 0.0f) + { + deltaTimeSeconds = 0.0f; + } + + csmInt32 beginIndex = 0; + + for (csmUint32 i = 0; i < _partGroupCounts.GetSize(); i++) + { + const csmInt32 partGroupCount = _partGroupCounts[i]; + + DoFade(model, deltaTimeSeconds, beginIndex, partGroupCount); + + beginIndex += partGroupCount; + } + + CopyPartOpacities(model); +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Effect/CubismPose.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "Model/CubismModel.hpp" +#include "Utils/CubismJson.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { +/** + * @brief パーツの不透明度の設定 + * + * パーツの不透明度の管理と設定を行う。 + */ +class CubismPose +{ +public: + /** + * @brief パーツにまつわるデータを管理 + * + * パーツにまつわる諸々のデータを管理する。 + */ + struct PartData + { + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + PartData(); + + /** + * @brief コピーコンストラクタ + * + * コピーコンストラクタ。 + */ + PartData(const PartData& v); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~PartData(); + + /** + * @brief 代入のオーバーロード + * + * 代入のオーバーロード。 + */ + PartData& operator=(const PartData& v); + + /** + * @brief 初期化 + * + * 初期化する。 + * + * @param[in] model 初期化に使用するモデル + */ + void Initialize(CubismModel* model); + + CubismIdHandle PartId; ///< パーツID + csmInt32 ParameterIndex; ///< パラメータのインデックス + csmInt32 PartIndex; ///< パーツのインデックス + csmVector Link; ///< 連動するパラメータ + }; + + /** + * @brief インスタンスの作成 + * + * インスタンスを作成する。 + * + * @param[in] pose3json pose3.jsonのデータ + * @param[in] size pose3.jsonのデータのサイズ[Byte] + * @return 作成されたインスタンス + */ + static CubismPose* Create(const csmByte* pose3json, csmSizeInt size); + + /** + * @brief インスタンスの破棄 + * + * インスタンスを破棄する。 + * + * @param[in] pose 対象のCubismPose + */ + static void Delete(CubismPose* pose); + + /** + * @brief モデルのパラメータの更新 + * + * モデルのパラメータを更新する。 + * + * @param[in] model 対象のモデル + * @param[in] deltaTimeSeconds デルタ時間[秒] + */ + void UpdateParameters(CubismModel* model, csmFloat32 deltaTimeSeconds); + + /** + * @brief 表示を初期化 + * + * 表示を初期化する。 + * + * @param[in] model 対象のモデル + * + * @note 不透明度の初期値が0でないパラメータは、不透明度を1に設定する。 + */ + void Reset(CubismModel* model); + +private: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismPose(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismPose(); + + /** + * @brief パーツの不透明度をコピー + * + * パーツの不透明度をコピーし、リンクしているパーツへ設定する。 + * + * @param[in] model 対象のモデル + */ + void CopyPartOpacities(CubismModel* model); + + /** + * @brief パーツのフェード操作を実行 + * + * パーツのフェード操作を行う。 + * + * @param[in] model 対象のモデル + * @param[in] deltaTimeSeconds デルタ時間[秒] + * @param[in] beginIndex フェード操作を行うパーツグループの先頭インデックス + * @param[in] partGroupCount フェード操作を行うパーツグループの個数 + */ + void DoFade(CubismModel* model, csmFloat32 deltaTimeSeconds, csmInt32 beginIndex, csmInt32 partGroupCount); + + csmVector _partGroups; ///< パーツグループ + csmVector _partGroupCounts; ///< それぞれのパーツグループの個数 + csmFloat32 _fadeTimeSeconds; ///< フェード時間[秒] + CubismModel* _lastModel; ///< 前回操作したモデル +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/ICubismAllocator.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "Type/CubismBasicType.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * An interface to implement memory allocation and deallocation processes
+ * on the platform side and call from the Framework. + */ +class ICubismAllocator +{ +public: + /** + * Destructor + */ + virtual ~ICubismAllocator() {} + + /** + * Allocates the memory. + * + * @param size Desired amount of memory in bytes + * + * @return Pointer to the allocated memory if succeeded; otherwise `0` + */ + virtual void* Allocate(const csmSizeType size) = 0; + + /** + * Deallocates the memory. + * + * @param memory Pointer to allocated memory to be deallocated + */ + virtual void Deallocate(void* memory) = 0; + + + /** + * Allocates the memory with specified alignment. + * + * @param size Desired amount of memory in bytes + * @param alignment Desired alignment of memory in bytes + * + * @return Pointer to the allocated memory if succeeded; otherwise `0` + */ + virtual void* AllocateAligned(const csmSizeType size, const csmUint32 alignment) = 0; + + /** + * Deallocates the aligned memory. + * + * @param alignedMemory Pointer to allocated memory to be deallocated + */ + virtual void DeallocateAligned(void* alignedMemory) = 0; + +}; +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/ICubismModelSetting.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismFramework.hpp" +#include "Type/csmMap.hpp" +#include "Id/CubismId.hpp" + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * An interface that covers the settings defined in the Model Settings File. + * + * @note Inherit this interface if you need to extend model settings or implement a class that uses model settings. + */ +class ICubismModelSetting +{ +public: + /** + * Destructor + */ + virtual ~ICubismModelSetting() {} + + /** + * Returns the file name of MOC3 File. + * + * @return Name of MOC3 File + */ + virtual const csmChar* GetModelFileName() = 0; + + /** + * Returns the number of textures in the Model. + * + * @return Number of textures + */ + virtual csmInt32 GetTextureCount() = 0; + + /** + * Returns the name of directory that texture is placed. + * + * @return Name of directory + */ + virtual const csmChar* GetTextureDirectory() = 0; + + /** + * Returns the name of texture in the Model. + * + * @param index Index to the desired texture name + * + * @return Name of texture + */ + virtual const csmChar* GetTextureFileName(csmInt32 index) = 0; + + /** + * Returns the number of Hit Area settings in the Model. + * + * @return Number of Hit Area settings + */ + virtual csmInt32 GetHitAreasCount() = 0; + + /** + * Returns whether ID of Drawable that is set for the Hit Area. + * + * @param groupName Name of the desired Motion Group + * + * @return true if Motion Group information exists; otherwise false + */ + virtual CubismIdHandle GetHitAreaId(csmInt32 index) = 0; + + /** + * Returns the name of Hit Area. + * + * @param index Index to the desired Hit Area + * + * @return Name of Hit Area + */ + virtual const csmChar* GetHitAreaName(csmInt32 index) = 0; + + /** + * Returns the name of Physics Settings File in the Model. + * + * @return Name of Physics Settings File + */ + virtual const csmChar* GetPhysicsFileName() = 0; + + /** + * Returns the name of Pose Settings File in the Model. + * + * @return Name of Pose Settings File + */ + virtual const csmChar* GetPoseFileName() = 0; + + /** + * Returns the name of Display Settings File in the Model. + * + * @return Name of Display Settings File + */ + virtual const csmChar* GetDisplayInfoFileName() = 0; + + /** + * Returns the number of expressions in the Model. + * + * @return Number of expressions + */ + virtual csmInt32 GetExpressionCount() = 0; + + /** + * Returns the name of expression. + * + * @param index Index to the desired expression + * + * @return Name of expression + */ + virtual const csmChar* GetExpressionName(csmInt32 index) = 0; + + /** + * Returns the name of Expression Settings File from the expression. + * + * @param index Index to the desired expression + * + * @return Name of Expression Settings File + */ + virtual const csmChar* GetExpressionFileName(csmInt32 index) = 0; + + /** + * Returns the number of Motion Groups in the Model. + * + * @return Number of Motion Groups + */ + virtual csmInt32 GetMotionGroupCount() = 0; + + /** + * Returns the name of Motion Group. + * + * @param index Index to the desired Motion Group + * + * @return Name of Motion Group + */ + virtual const csmChar* GetMotionGroupName(csmInt32 index) = 0; + + /** + * Returns the number of Motions in the Motion Group. + * + * @param groupName Name to the desired Motion Group + * + * @return Number of Motions + */ + virtual csmInt32 GetMotionCount(const csmChar* groupName) = 0; + + /** + * Returns the file name of Motion File in the Motion Group. + * + * @param groupName Name to the desired Motion Group + * @param index Index to the desired motion + * + * @return Name of Motion File + */ + virtual const csmChar* GetMotionFileName(const csmChar* groupName, csmInt32 index) = 0; + + /** + * Returns the name of Audio File attached to the Motion in the Motion Group. + * + * @param groupName Name to the desired Motion Group + * @param index Index to the desired Motion + * + * @return Name of Audio File + */ + virtual const csmChar* GetMotionSoundFileName(const csmChar* groupName, csmInt32 index) = 0; + + /** + * Returns the Fade-in time at the start of the Motion in the Motion Group + * + * @param groupName Name to the desired Motion Group + * @param index Index to the desired Motion + * + * @return Fade-in time [sec] + */ + virtual csmFloat32 GetMotionFadeInTimeValue(const csmChar* groupName, csmInt32 index) = 0; + + /** + * Returns the Fade-out time at the end of the Motion in the Motion Group. + * + * @param groupName Name to the desired Motion Group + * @param index Index to the desired Motion + * + * @return Fade-out time [sec] + */ + virtual csmFloat32 GetMotionFadeOutTimeValue(const csmChar* groupName, csmInt32 index) = 0; + + /** + * Returns the name of User Data File in the Model. + * + * @return Name of User Data File + */ + virtual const csmChar* GetUserDataFile() = 0; + + /** + * Returns the Layout Information set in the model. + * + * @param outLayoutMap Instance of a collection to store + * + * @return true if Layout Information exists; otherwise false + */ + virtual csmBool GetLayoutMap(csmMap& outLayoutMap) = 0; + + /** + * Returns the number of parameters attached to the Eye Blinking Settings. + * + * @return Number of parameters + */ + virtual csmInt32 GetEyeBlinkParameterCount() = 0; + + /** + * Returns the ID of parameter attached to the Eye Blinking Settings. + * + * @param index Index to the desired parameter + * + * @return ID of parameter + */ + virtual CubismIdHandle GetEyeBlinkParameterId(csmInt32 index) = 0; + + /** + * Returns the number of parameters attached to the Lip-sync Settings. + * + * @return Number of parameters + */ + virtual csmInt32 GetLipSyncParameterCount() = 0; + + /** + * Returns the ID of parameter attached to the Lip-sync Settings. + * + * @param index Index to the desired parameter + * + * @return ID of parameter + */ + virtual CubismIdHandle GetLipSyncParameterId(csmInt32 index) = 0; +}; +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Id/CubismId.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismId.hpp" +#include "Type/CubismBasicType.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +CubismId::CubismId() +{ } + +CubismId::CubismId(const CubismId& c) + : _id(c._id) +{ } + +CubismId::CubismId(const csmChar* id) +{ + _id = id; +} + +CubismId::~CubismId() +{ } + +CubismId& CubismId::operator=(const CubismId& c) +{ + if (this != &c) + { + _id = c._id; + } + + return *this; +} + +csmBool CubismId::operator==(const CubismId& c) const +{ + return (_id == c._id); +} + +csmBool CubismId::operator!=(const CubismId& c) const +{ + return !(_id == c._id); +} + +const csmString& CubismId::GetString() const +{ + return _id; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Id/CubismId.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "Type/csmString.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +class CubismIdManager; + +/** + * @brief パラメータ名・パーツ名・Drawable名を保持 + * + * パラメータ名・パーツ名・Drawable名を保持するクラス。 + */ +struct CubismId +{ + friend class CubismIdManager; + + /** + * @brief ID名を取得 + * + * ID名を取得する。 + */ + const csmString& GetString() const; + + CubismId& operator=(const CubismId& c); + + csmBool operator==(const CubismId& c) const; + csmBool operator!=(const CubismId& c) const; + +private: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismId(); + + /** + * @brief コンストラクタ + * + * コンストラクタ。 + * + * @param[in] id ID名 + */ + CubismId(const csmChar* id); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + ~CubismId(); + + CubismId(const CubismId& c); + + csmString _id; ///< ID名 +}; + +typedef const CubismId* CubismIdHandle; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Id/CubismIdManager.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismIdManager.hpp" +#include "CubismId.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +CubismIdManager::CubismIdManager() +{ } + +CubismIdManager::~CubismIdManager() +{ + for (csmUint32 i = 0; i < _ids.GetSize(); ++i) + { + CSM_DELETE_SELF(CubismId, _ids[i]); + } +} + +void CubismIdManager::RegisterIds(const csmChar** ids, csmInt32 count) +{ + for (csmInt32 i = 0; i < count; ++i) + { + RegisterId(ids[i]); + } +} + +void CubismIdManager::RegisterIds(const csmVector& ids) +{ + for (csmUint32 i = 0; i < ids.GetSize(); ++i) + { + RegisterId(ids[i]); + } +} + +const CubismId* CubismIdManager::GetId(const csmString& id) +{ + return RegisterId(id.GetRawString()); +} + +const CubismId* CubismIdManager::GetId(const csmChar* id) +{ + return RegisterId(id); +} + +csmBool CubismIdManager::IsExist(const csmString& id) const +{ + return IsExist(id.GetRawString()); +} +csmBool CubismIdManager::IsExist(const csmChar* id) const +{ + return (FindId(id) != NULL); +} + +const CubismId* CubismIdManager::RegisterId(const csmChar* id) +{ + CubismId* result = NULL; + + if ((result = FindId(id)) != NULL) + { + return result; + } + + result = CSM_NEW CubismId(id); + _ids.PushBack(result); + + return result; +} + +const CubismId* CubismIdManager::RegisterId(const csmString& id) +{ + return RegisterId(id.GetRawString()); +} + +CubismId* CubismIdManager::FindId(const csmChar* id) const +{ + for (csmUint32 i = 0; i < _ids.GetSize(); ++i) + { + if (_ids[i]->GetString() == id) + { + return _ids[i]; + } + } + + return NULL; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Id/CubismIdManager.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "Type/CubismBasicType.hpp" +#include "Type/csmString.hpp" +#include "Type/csmVector.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +struct CubismId; + +/** + * @brief ID名の管理 + * + * ID名を管理する。 + */ +class CubismIdManager +{ + friend struct CubismId; + +public: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismIdManager(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + ~CubismIdManager(); + + /** + * @brief ID名をリストから登録 + * + * ID名をリストから登録する。 + * + * @param[in] ids ID名リスト + * @param[in] count IDの個数 + */ + void RegisterIds(const csmChar** ids, csmInt32 count); + + /** + * @brief ID名をリストから登録 + * + * ID名をリストから登録する。 + * + * @param[in] ids ID名リスト + */ + void RegisterIds(const csmVector& ids); + + /** + * @brief ID名を登録 + * + * ID名を登録する。 + * + * @param[in] id ID名 + */ + const CubismId* RegisterId(const csmChar* id); + + /** + * @brief ID名を登録 + * + * ID名を登録する。 + * + * @param[in] id ID名 + */ + const CubismId* RegisterId(const csmString& id); + + /** + * @brief ID名からIDを取得する。 + * + * ID名からIDを取得する。 + * + * @param[in] id ID名 + * + * @note 未登録のID名の場合、登録も行う。 + */ + const CubismId* GetId(const csmString& id); + + /** + * @brief ID名からIDを取得する。 + * + * ID名からIDを取得する。 + * + * @param[in] id ID名 + * + * @note 未登録のID名の場合、登録も行う。 + */ + const CubismId* GetId(const csmChar* id); + + /** + * @brief ID名からIDの確認 + * + * ID名からIDが登録されているかどうか確認する。 + * + * @retval true 存在する + * @retval false 存在しない + */ + csmBool IsExist(const csmString& id) const; + + /** + * @brief ID名からIDの確認 + * + * ID名からIDが登録されているかどうか確認する。 + * + * @retval true 存在する + * @retval false 存在しない + */ + csmBool IsExist(const csmChar* id) const; + +private: + CubismIdManager(const CubismIdManager&); + CubismIdManager& operator=(const CubismIdManager&); + + /** + * @brief ID名からIDを検索 + * + * ID名からIDを検索する。 + * + * @param[in] id ID名 + * @return 登録されているID。なければNULL。 + */ + CubismId* FindId(const csmChar* id) const; + + csmVector _ids; ///< 登録されているIDのリスト +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Live2DCubismCore.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +/** + * Header to include the Live2D Cubism Core in the `Live2D::Cubism::Core` namespace + * + * @note Include this header instead of including the `Live2DCubismCore.h` directly. + */ +namespace Live2D { namespace Cubism { namespace Core { +#include "Live2DCubismCore.h" +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Math/CubismMath.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismMath.hpp" +#include "Utils/CubismDebug.hpp" + +namespace Live2D {namespace Cubism {namespace Framework { + +const csmFloat32 CubismMath::Pi = 3.1415926535897932384626433832795f; +const csmFloat32 CubismMath::Epsilon = 0.00001f; + +csmInt32 CubismMath::Clamp(csmInt32 val, csmInt32 min, csmInt32 max) +{ + if (val < min) + { + return min; + } + else if (max < val) + { + return max; + } + + return val; +} + +csmFloat32 CubismMath::ClampF(csmFloat32 val, csmFloat32 min, csmFloat32 max) +{ + if (val < min) + { + return min; + } + else if (max < val) + { + return max; + } + + return val; +} + +csmFloat32 CubismMath::DegreesToRadian(csmFloat32 degrees) +{ + return (degrees / 180.0f) * Pi; +} + +csmFloat32 CubismMath::RadianToDegrees(csmFloat32 radian) +{ + return (radian * 180.0f) / Pi; +} + +csmFloat32 CubismMath::DirectionToRadian(CubismVector2 from, CubismVector2 to) +{ + csmFloat32 q1; + csmFloat32 q2; + csmFloat32 ret; + + q1 = atan2f(to.Y, to.X); + q2 = atan2f(from.Y, from.X); + + ret = q1 - q2; + + while (ret < -Pi) + { + ret += Pi * 2.0f; + } + + while (ret > Pi) + { + ret -= Pi * 2.0f; + } + + return ret; +} + +csmFloat32 CubismMath::DirectionToDegrees(CubismVector2 from, CubismVector2 to) +{ + csmFloat32 radian; + csmFloat32 degree; + + radian = DirectionToRadian(from, to); + degree = RadianToDegrees(radian); + + if ((to.X - from.X) > 0.0f) + { + degree = -degree; + } + + return degree; +} + +CubismVector2 CubismMath::RadianToDirection(csmFloat32 totalAngle) +{ + CubismVector2 ret; + + ret.X = CubismMath::SinF(totalAngle); + ret.Y = CubismMath::CosF(totalAngle); + + return ret; +} + +csmFloat32 CubismMath::QuadraticEquation(csmFloat32 a, csmFloat32 b, csmFloat32 c) +{ + if (CubismMath::AbsF(a) < CubismMath::Epsilon) + { + if (CubismMath::AbsF(b) < CubismMath::Epsilon) + { + return -c; + } + return -c / b; + } + + return -(b + CubismMath::SqrtF(b * b - 4.0f * a * c)) / (2.0f * a); +} + +csmFloat32 CubismMath::CardanoAlgorithmForBezier(csmFloat32 a, csmFloat32 b, csmFloat32 c, csmFloat32 d) +{ + if ( CubismMath::AbsF( a ) < CubismMath::Epsilon ) + { + return CubismMath::RangeF( QuadraticEquation(b, c, d), 0.0f, 1.0f); + } + + csmFloat32 ba = b / a; + csmFloat32 ca = c / a; + csmFloat32 da = d / a; + + + csmFloat32 p = (3.0f * ca - ba*ba) / 3.0f; + csmFloat32 p3 = p / 3.0f; + csmFloat32 q = (2.0f * ba*ba*ba - 9.0f * ba*ca + 27.0f * da) / 27.0f; + csmFloat32 q2 = q / 2.0f; + csmFloat32 discriminant = q2*q2 + p3*p3*p3; + + const csmFloat32 center = 0.5f; + const csmFloat32 threshold = center + 0.01f; + + if (discriminant < 0.0f) { + csmFloat32 mp3 = -p / 3.0f; + csmFloat32 mp33 = mp3*mp3*mp3; + csmFloat32 r = CubismMath::SqrtF(mp33); + csmFloat32 t = -q / (2.0f * r); + csmFloat32 cosphi = RangeF(t, -1.0f, 1.0f); + csmFloat32 phi = acos(cosphi); + csmFloat32 crtr = cbrt(r); + csmFloat32 t1 = 2.0f * crtr; + + csmFloat32 root1 = t1 * CubismMath::CosF(phi / 3.0f) - ba / 3.0f; + if ( abs( root1 - center) < threshold) + { + return RangeF( root1, 0.0f, 1.0f); + } + + csmFloat32 root2 = t1 * CubismMath::CosF((phi + 2.0f * CubismMath::Pi) / 3.0f) - ba / 3.0f; + if (abs(root2 - center) < threshold) + { + return RangeF(root2, 0.0f, 1.0f); + } + + csmFloat32 root3 = t1 * CubismMath::CosF((phi + 4.0f * CubismMath::Pi) / 3.0f) - ba / 3.0f; + return RangeF(root3, 0.0f, 1.0f); + } + + if (discriminant == 0.0f) { + csmFloat32 u1; + if (q2 < 0.0f) + { + u1 = cbrt(-q2); + } + else + { + u1 = -cbrt(q2); + } + + csmFloat32 root1 = 2.0f * u1 - ba / 3.0f; + if (abs(root1 - center) < threshold) + { + return RangeF(root1, 0.0f, 1.0f); + } + + csmFloat32 root2 = -u1 - ba / 3.0f; + return RangeF(root2, 0.0f, 1.0f); + } + + csmFloat32 sd = CubismMath::SqrtF(discriminant); + csmFloat32 u1 = cbrt(sd - q2); + csmFloat32 v1 = cbrt(sd + q2); + csmFloat32 root1 = u1 - v1 - ba / 3.0f; + return RangeF(root1, 0.0f, 1.0f); +} + +csmFloat32 CubismMath::ModF(csmFloat32 dividend, csmFloat32 divisor) +{ + if ( + !isfinite(dividend) || + divisor == 0 || + isnan(dividend) || + isnan(divisor) + ) { + CubismLogWarning("dividend: %f, divisor: %f ModF() returns 'NaN'.", dividend, divisor); + return NAN; + } + + // 絶対値に変換する。 + const csmFloat32 absDividend = CubismMath::AbsF(dividend); + const csmFloat32 absDivisor = CubismMath::AbsF(divisor); + + // 絶対値で割り算する。 + csmFloat32 result = absDividend - floorf(absDividend / absDivisor) * absDivisor; + + // 符号を被除数のものに指定する。 + return copysign(result, dividend); +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Math/CubismMath.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include +#include "Type/CubismBasicType.hpp" +#include "Math/CubismVector2.hpp" + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + +/** + *@brief 数値計算などに使用するユーティリティクラス + * + */ +class CubismMath +{ +public: + static const csmFloat32 Pi; + static const csmFloat32 Epsilon; + + /** + *@brief 第一引数の値を最小値と最大値の範囲に収めた値を返す + * + *@param value -> 収められる値 + *@param min -> 範囲の最小値 + *@param max -> 範囲の最大値 + *@return 最小値と最大値の範囲に収めた値 + */ + static csmFloat32 RangeF(csmFloat32 value, csmFloat32 min, csmFloat32 max) + { + if (value < min) value = min; + else if (value > max) value = max; + return value; + }; + + /** + *@brief サイン関数の値を求める + * + *@param x -> 角度値(ラジアン) + *@return サイン関数sin(x)の値 + */ + static csmFloat32 SinF(csmFloat32 x) + { + return sinf(x); + }; + + /** + *@brief コサイン関数の値を求める + * + *@param x -> 角度値(ラジアン) + *@return コサイン関数cos(x)の値 + */ + static csmFloat32 CosF(csmFloat32 x) + { + return cosf(x); + }; + + /** + *@brief 絶対値の値を求める + * + *@param x -> 絶対値を求める値 + *@return 値の絶対値 + */ + static csmFloat32 AbsF(csmFloat32 x) + { + return std::fabs(x); + }; + + /** + *@brief 平方根(ルート)を求める + * + *@param x -> 平方根を求める値 + *@return 値の平方根 + */ + static csmFloat32 SqrtF(csmFloat32 x) + { + return sqrtf(x); + }; + + /** + *@brief イージング処理されたサインを求める
+ * フェードイン・アウト時のイージングに利用できる + * + *@param value -> イージングを行う値 + *@return イージング処理されたサイン値 + */ + static csmFloat32 GetEasingSine(csmFloat32 value) + { + if (value < 0.0f) return 0.0f; + else if (value > 1.0f) return 1.0f; + + return static_cast(0.5f - 0.5f * CosF(value * Pi)); + } + + /** + * @brief 大きい方の値を返す。 + * + * @param l -> 左辺の値 + * @param r -> 右辺の値 + * @return 大きい方の値 + */ + static csmFloat32 Max(csmFloat32 l, csmFloat32 r) + { + return (l > r) ? l : r; + } + + /** + * @brief 小さい方の値を返す。 + * + * @param l -> 左辺の値 + * @param r -> 右辺の値 + * @return 小さい方の値 + */ + static csmFloat32 Min(csmFloat32 l, csmFloat32 r) + { + return (l > r) ? r : l; + } + + /** + * @brief 値を範囲内に納めて返す + * + * @param val -> 範囲内か確認する値 + * @param min -> 最小値 + * @param max -> 最大値 + * @return 範囲内に収まった値 + */ + static csmInt32 Clamp(csmInt32 val, csmInt32 min, csmInt32 max); + + /** + * @brief 値を範囲内に納めて返す + * + * @param val -> 範囲内か確認する値 + * @param min -> 最小値 + * @param max -> 最大値 + * @return 範囲内に収まった値 + */ + static csmFloat32 ClampF(csmFloat32 val, csmFloat32 min, csmFloat32 max); + + /** + * @brief 角度値をラジアン値に変換します。 + * + * @param degrees -> 角度値 + * @return 角度値から変換したラジアン値 + */ + static csmFloat32 DegreesToRadian(csmFloat32 degrees); + + /** + * @brief ラジアン値を角度値に変換します。 + * + * @param radian -> ラジアン値 + * @return ラジアン値から変換した角度値 + */ + static csmFloat32 RadianToDegrees(csmFloat32 radian); + + /** + * @brief 2つのベクトルからラジアン値を求めます。 + * + * @param from -> 始点ベクトル + * @param to -> 終点ベクトル + * @return ラジアン値から求めた方向ベクトル + */ + static csmFloat32 DirectionToRadian(CubismVector2 from, CubismVector2 to); + + /** + * @brief 2つのベクトルから角度値を求めます。 + * + * @param from -> 始点ベクトル + * @param to -> 終点ベクトル + * @return 角度値から求めた方向ベクトル + */ + static csmFloat32 DirectionToDegrees(CubismVector2 from, CubismVector2 to); + + /** + * @brief ラジアン値を方向ベクトルに変換します。 + * + * @param totalAngle -> ラジアン値 + * @return ラジアン値から変換した方向ベクトル + */ + static CubismVector2 RadianToDirection(csmFloat32 totalAngle); + + /** + * @brief 三次方程式の三次項の係数が0になったときに補欠的に二次方程式の解をもとめる。 + * a * x^2 + b * x + c = 0 + * + * @param a -> 二次項の係数値 + * @param b -> 一次項の係数値 + * @param c -> 定数項の値 + * @return 二次方程式の解 + */ + static csmFloat32 QuadraticEquation(csmFloat32 a, csmFloat32 b, csmFloat32 c); + + /** + * @brief カルダノの公式によってベジェのt値に該当する3次方程式の解を求める。 + * 重解になったときには0.0~1.0の値になる解を返す。 + * + * a * x^3 + b * x^2 + c * x + d = 0 + * + * @param a -> 三次項の係数値 + * @param b -> 二次項の係数値 + * @param c -> 一次項の係数値 + * @param d -> 定数項の値 + * @return 0.0~1.0の間にある解 + */ + static csmFloat32 CardanoAlgorithmForBezier(csmFloat32 a, csmFloat32 b, csmFloat32 c, csmFloat32 d); + + /** + * @brief 浮動小数点の余りを求める。 + * + * @param dividend 被除数(割られる値) + * @param divisor 除数(割る値) + * @returns 余り + */ + static csmFloat32 ModF(csmFloat32 dividend, csmFloat32 divisor); + +private: + /** + *@brief privateコンストラクタ + * + */ + CubismMath(); +}; + +}}} + +//--------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Math/CubismMatrix44.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismMatrix44.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { +CubismMatrix44::CubismMatrix44() +{ + LoadIdentity(); +} + +CubismMatrix44::~CubismMatrix44() +{ } + +void CubismMatrix44::LoadIdentity() +{ + csmFloat32 c[16] = { + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + + SetMatrix(c); +} + +csmFloat32* CubismMatrix44::GetArray() +{ + return _tr; +} + +void CubismMatrix44::Multiply(csmFloat32* a, csmFloat32* b, csmFloat32* dst) +{ + csmFloat32 c[16] = { + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f + }; + csmInt32 n = 4; + + for (csmInt32 i = 0; i < n; ++i) + { + for (csmInt32 j = 0; j < n; ++j) + { + for (csmInt32 k = 0; k < n; ++k) + { + c[j + i * 4] += a[k + i * 4] * b[j + k * 4]; + } + } + } + + for (csmInt32 i = 0; i < 16; ++i) + { + dst[i] = c[i]; + } +} + +void CubismMatrix44::TranslateRelative(csmFloat32 x, csmFloat32 y) +{ + csmFloat32 tr1[16] = { + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + x, y, 0.0f, 1.0f + }; + + Multiply(tr1, _tr, _tr); +} + +void CubismMatrix44::Translate(csmFloat32 x, csmFloat32 y) +{ + _tr[12] = x; + _tr[13] = y; +} + +void CubismMatrix44::ScaleRelative(csmFloat32 x, csmFloat32 y) +{ + csmFloat32 tr1[16] = { + x, 0.0f, 0.0f, 0.0f, + 0.0f, y, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + + Multiply(tr1, _tr, _tr); +} + +void CubismMatrix44::Scale(csmFloat32 x, csmFloat32 y) +{ + _tr[0] = x; + _tr[5] = y; +} + +csmFloat32 CubismMatrix44::TransformX(csmFloat32 src) +{ + return _tr[0] * src + _tr[12]; +} + +csmFloat32 CubismMatrix44::InvertTransformX(csmFloat32 src) +{ + return (src - _tr[12]) / _tr[0]; +} + +csmFloat32 CubismMatrix44::TransformY(csmFloat32 src) +{ + return _tr[5] * src + _tr[13]; +} + +csmFloat32 CubismMatrix44::InvertTransformY(csmFloat32 src) +{ + return (src - _tr[13]) / _tr[5]; +} + +void CubismMatrix44::SetMatrix(csmFloat32* tr) +{ + for (csmInt32 i = 0; i < 16; ++i) + { + _tr[i] = tr[i]; + } +} + +csmFloat32 CubismMatrix44::GetScaleX() const +{ + return _tr[0]; +} + +csmFloat32 CubismMatrix44::GetScaleY() const +{ + return _tr[5]; +} + +csmFloat32 CubismMatrix44::GetTranslateX() const +{ + return _tr[12]; +} + +csmFloat32 CubismMatrix44::GetTranslateY() const +{ + return _tr[13]; +} + +void CubismMatrix44::TranslateX(csmFloat32 x) +{ + _tr[12] = x; +} + +void CubismMatrix44::TranslateY(csmFloat32 y) +{ + _tr[13] = y; +} + +void CubismMatrix44::MultiplyByMatrix(CubismMatrix44* m) +{ + Multiply(m->GetArray(), _tr, _tr); +} +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Math/CubismMatrix44.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "Type/CubismBasicType.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief 4x4の行列 + * + * 4x4行列の便利クラス。 + */ +class CubismMatrix44 +{ +public: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismMatrix44(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismMatrix44(); + + /** + * @brief 乗算 + * + * 受け取った2つの行列の乗算を行う。 + * + * @param[in] a 行列a + * @param[in] b 行列b + * @param[out] dst 格納先の行列 + */ + static void Multiply(csmFloat32* a, csmFloat32* b, csmFloat32* dst); + + /** + * @brief 単位行列に初期化 + * + * 単位行列に初期化する。 + */ + void LoadIdentity(); + + /** + * @brief 行列を浮動小数点数の配列で取得 + * + * 行列を浮動小数点数の配列で取得する。 + * + * @return 16個の浮動小数点数で表される4x4の行列 + */ + csmFloat32* GetArray(); + + /** + * @brief 行列を設定 + * + * 行列を設定する。 + * + * @param[in] tr 16個の浮動小数点数で表される4x4の行列 + */ + void SetMatrix(csmFloat32* tr); + + /** + * @brief X軸の拡大率を取得 + * + * X軸の拡大率を取得する。 + * + * @return X軸の拡大率 + */ + csmFloat32 GetScaleX() const; + + /** + * @brief Y軸の拡大率を取得 + * + * Y軸の拡大率を取得する。 + * + * @return Y軸の拡大率 + */ + csmFloat32 GetScaleY() const; + + /** + * @brief X軸の移動量を取得 + * + * X軸の移動量を取得する。 + * + * @return X軸の移動量 + * + */ + csmFloat32 GetTranslateX() const; + + /** + * @brief Y軸の移動量を取得 + * + * Y軸の移動量を取得する。 + * + * @return Y軸の移動量 + */ + csmFloat32 GetTranslateY() const; + + /** + * @brief X軸の値を現在の行列で計算 + * + * X軸の値を現在の行列で計算する。 + * + * @param[in] src X軸の値 + * @return 現在の行列で計算されたX軸の値 + */ + csmFloat32 TransformX(csmFloat32 src); + + /** + * @brief Y軸の値を現在の行列で計算 + * + * Y軸の値を現在の行列で計算する。 + * + * @param[in] src Y軸の値 + * @return 現在の行列で計算されたY軸の値 + */ + csmFloat32 TransformY(csmFloat32 src); + + /** + * @brief X軸の値を現在の行列で逆計算 + * + * X軸の値を現在の行列で逆計算する。 + * + * @param[in] src X軸の値 + * @return 現在の行列で逆計算されたX軸の値 + */ + csmFloat32 InvertTransformX(csmFloat32 src); + + /** + * @brief Y軸の値を現在の行列で逆計算 + * + * Y軸の値を現在の行列で逆計算する。 + * + * @parain[in] src Y軸の値 + * @return 現在の行列で逆計算されたY軸の値 + */ + csmFloat32 InvertTransformY(csmFloat32 src); + + /** + * @brief 現在の行列の位置を起点にして移動 + * + * 現在の行列の位置を起点にして相対的に移動する。 + * + * @param[in] x X軸の移動量 + * @param[in] y Y軸の移動量 + */ + void TranslateRelative(csmFloat32 x, csmFloat32 y); + + /** + * @brief 現在の行列の位置を移動 + * + * 現在の行列の位置を指定した位置へ移動する。 + * + * @param[in] x X軸の移動量 + * @param[in] y Y軸の移動量 + */ + void Translate(csmFloat32 x, csmFloat32 y); + + /** + * @brief 現在の行列のX軸の位置を移動 + * + * 現在の行列のX軸の位置を指定した位置へ移動する。 + * + * @param[in] x X軸の移動量 + */ + void TranslateX(csmFloat32 x); + + /** + * @brief 現在の行列のY軸の位置を移動 + * + * 現在の行列のY軸の位置を指定した位置へ移動する。 + * + * @param[in] y Y軸の移動量 + */ + void TranslateY(csmFloat32 y); + + /** + * @brief 現在の行列の拡大率を相対的に設定 + * + * 現在の行列の拡大率を相対的に設定する。 + * + * @param[in] x X軸の拡大率 + * @param[in] y Y軸の拡大率 + */ + void ScaleRelative(csmFloat32 x, csmFloat32 y); + + /** + * @brief 現在の行列の拡大率を設定 + * + * 現在の行列の拡大率を指定した倍率に設定する。 + * + * @param[in] x X軸の拡大率 + * @param[in] y Y軸の拡大率 + */ + void Scale(csmFloat32 x, csmFloat32 y); + + /** + * @brief 引数で与えられた行列にこの行列を乗算 + * + * 引数で与えられた行列にこの行列を乗算する。 + * (引数で与えられた行列)* (この行列) + * + * @note 関数名と実際の計算内容に乖離があるため、今後計算順が修正される可能性があります。 + * @param[in] m 行列 + */ + void MultiplyByMatrix(CubismMatrix44* m); + +protected: + csmFloat32 _tr[16]; ///< 4x4行列データ +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Math/CubismModelMatrix.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismModelMatrix.hpp" +#include "Type/csmString.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +CubismModelMatrix::CubismModelMatrix() + : _width(0.0f) + , _height(0.0f) +{ } + +CubismModelMatrix::CubismModelMatrix(csmFloat32 w, csmFloat32 h) +{ + _width = w; + _height = h; + + SetHeight(2.0f); +} + +CubismModelMatrix::~CubismModelMatrix() +{ } + +void CubismModelMatrix::SetCenterPosition(csmFloat32 x, csmFloat32 y) +{ + CenterX(x); + CenterY(y); +} + +void CubismModelMatrix::Bottom(csmFloat32 y) +{ + const csmFloat32 h = _height * GetScaleY(); + TranslateY(y - h); +} + +void CubismModelMatrix::CenterY(csmFloat32 y) +{ + const csmFloat32 h = _height * GetScaleY(); + TranslateY(y - (h / 2.0f)); +} + +void CubismModelMatrix::Right(csmFloat32 x) +{ + const csmFloat32 w = _width * GetScaleX(); + TranslateX(x - w); +} + +void CubismModelMatrix::CenterX(csmFloat32 x) +{ + const csmFloat32 w = _width * GetScaleX(); + TranslateX(x - (w / 2.0f)); +} + +void CubismModelMatrix::SetWidth(csmFloat32 w) +{ + const csmFloat32 scaleX = w / _width; + const csmFloat32 scaleY = scaleX; + Scale(scaleX, scaleY); +} + +void CubismModelMatrix::SetHeight(csmFloat32 h) +{ + const csmFloat32 scaleX = h / _height; + const csmFloat32 scaleY = scaleX; + Scale(scaleX, scaleY); +} + +void CubismModelMatrix::SetupFromLayout(csmMap& layout) +{ + const csmChar* KeyWidth = "width"; + const csmChar* KeyHeight = "height"; + const csmChar* KeyX = "x"; + const csmChar* KeyY = "y"; + const csmChar* KeyCenterX = "center_x"; + const csmChar* KeyCenterY = "center_y"; + const csmChar* KeyTop = "top"; + const csmChar* KeyBottom = "bottom"; + const csmChar* KeyLeft = "left"; + const csmChar* KeyRight = "right"; + + for (csmMap::const_iterator ite = layout.Begin(); ite != layout.End(); ++ite) + { + const csmString key = ite->First; + const csmFloat32 value = ite->Second; + + if (key == KeyWidth) + { + SetWidth(value); + } + else if (key == KeyHeight) + { + SetHeight(value); + } + } + + for (csmMap::const_iterator ite = layout.Begin(); ite != layout.End(); ++ite) + { + const csmString key = ite->First; + const csmFloat32 value = ite->Second; + + if (key == KeyX) + { + SetX(value); + } + else if (key == KeyY) + { + SetY(value); + } + else if (key == KeyCenterX) + { + CenterX(value); + } + else if (key == KeyCenterY) + { + CenterY(value); + } + else if (key == KeyTop) + { + Top(value); + } + else if (key == KeyBottom) + { + Bottom(value); + } + else if (key == KeyLeft) + { + Left(value); + } + else if (key == KeyRight) + { + Right(value); + } + } +} + +void CubismModelMatrix::SetPosition(csmFloat32 x, csmFloat32 y) +{ + Translate(x, y); +} + +void CubismModelMatrix::Top(csmFloat32 y) +{ + SetY(y); +} + +void CubismModelMatrix::Left(csmFloat32 x) +{ + SetX(x); +} + +void CubismModelMatrix::SetX(csmFloat32 x) +{ + TranslateX(x); +} + +void CubismModelMatrix::SetY(csmFloat32 y) +{ + TranslateY(y); +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Math/CubismModelMatrix.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismMatrix44.hpp" +#include "Type/csmMap.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { +/** + * @brief モデル座標設定用の4x4行列 + * + * モデル座標設定用の4x4行列クラス。 + */ +class CubismModelMatrix : public CubismMatrix44 +{ +public: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismModelMatrix(); + + /** + * @brief コンストラクタ + * + * コンストラクタ。 + * + * @param[in] w 横幅 + * @param[in] h 縦幅 + */ + CubismModelMatrix(csmFloat32 w, csmFloat32 h); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismModelMatrix(); + + /** + * @brief 横幅を設定 + * + * 横幅を設定する。 + * + * @param[in] w 横幅 + */ + void SetWidth(csmFloat32 w); + + /** + * @brief 縦幅を設定 + * + * 縦幅を設定する。 + * + * @param[in] h 縦幅 + */ + void SetHeight(csmFloat32 h); + + /** + * @brief 位置を設定 + * + * 位置を設定する。 + * + * @param[in] x X軸の位置 + * @param[in] y Y軸の位置 + */ + void SetPosition(csmFloat32 x, csmFloat32 y); + + /** + * @brief 中心位置を設定 + * + * 中心位置を設定する。 + * + * @param[in] x X軸の中心位置 + * @param[in] y Y軸の中心位置 + * + * @note widthかheightを設定したあとでないと、拡大率が正しく取得できないためずれる。 + */ + void SetCenterPosition(csmFloat32 x, csmFloat32 y); + + /** + * @brief 上辺の位置を設定 + * + * 上辺の位置を設定する。 + * + * @param[in] y 上辺のY軸位置 + */ + void Top(csmFloat32 y); + + /** + * @brief 下辺の位置を設定 + * + * 下辺の位置を設定する。 + * + * @param[in] y 下辺のY軸位置 + */ + void Bottom(csmFloat32 y); + + /** + * @brief 左辺の位置を設定 + * + * 左辺の位置を設定する。 + * + * @param[in] x 左辺のX軸位置 + */ + void Left(csmFloat32 x); + + /** + * @brief 右辺の位置を設定 + * + * 右辺の位置を設定する。 + * + * @param[in] x 右辺のX軸位置 + */ + void Right(csmFloat32 x); + + /** + * @brief X軸の中心位置を設定 + * + * X軸の中心位置を設定する。 + * + * @param[in] x X軸の中心位置 + */ + void CenterX(csmFloat32 x); + + /** + * @brief X軸の位置を設定 + * + * X軸の位置を設定する。 + * + * @param[in] x X軸の位置 + */ + void SetX(csmFloat32 x); + + /** + * @brief Y軸の中心位置を設定 + * + * Y軸の中心位置を設定する。 + * + * @param[in] y Y軸の中心位置 + */ + void CenterY(csmFloat32 y); + + /** + * @brief Y軸の位置を設定 + * + * Y軸の位置を設定する。 + * + * @param[in] y Y軸の位置 + */ + void SetY(csmFloat32 y); + + /** + * @brief レイアウト情報から位置を設定 + * + * レイアウト情報から位置を設定する。 + * + * @param[in] layout レイアウト情報 + */ + void SetupFromLayout(csmMap& layout); + +private: + csmFloat32 _width; ///< 横幅 + csmFloat32 _height; ///< 縦幅 +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Math/CubismTargetPoint.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismTargetPoint.hpp" +#include "Math/CubismMath.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +const csmInt32 FrameRate = 30; +const csmFloat32 Epsilon = 0.01f; + +CubismTargetPoint::CubismTargetPoint() + : _faceTargetX(0.0f) + , _faceTargetY(0.0f) + , _faceX(0.0f) + , _faceY(0.0f) + , _faceVX(0.0f) + , _faceVY(0.0f) + , _lastTimeSeconds(0.0f) + , _userTimeSeconds(0.0f) +{ } + +CubismTargetPoint::~CubismTargetPoint() +{ } + +void CubismTargetPoint::Update(csmFloat32 deltaTimeSeconds) +{ + // デルタ時間を加算する + _userTimeSeconds += deltaTimeSeconds; + + // 首を中央から左右に振るときの平均的な早さは 秒程度。加速・減速を考慮して、その2倍を最高速度とする + // 顔のふり具合を、中央(0.0)から、左右は(+-1.0)とする + const csmFloat32 FaceParamMaxV = 40.0 / 10.0f; // 7.5秒間に40分移動(5.3/sc) + const csmFloat32 MaxV = FaceParamMaxV * 1.0f / static_cast(FrameRate); // 1frameあたりに変化できる速度の上限 + + if (_lastTimeSeconds == 0.0f) + { + _lastTimeSeconds = _userTimeSeconds; + return; + } + + const csmFloat32 deltaTimeWeight = (_userTimeSeconds - _lastTimeSeconds) * static_cast(FrameRate); + _lastTimeSeconds = _userTimeSeconds; + + // 最高速度になるまでの時間を + const csmFloat32 TimeToMaxSpeed = 0.15f; + const csmFloat32 FrameToMaxSpeed = TimeToMaxSpeed * static_cast(FrameRate); // sec * frame/sec + const csmFloat32 MaxA = deltaTimeWeight * MaxV / FrameToMaxSpeed; // 1frameあたりの加速度 + + // 目指す向きは、(dx, dy)方向のベクトルとなる + const csmFloat32 dx = _faceTargetX - _faceX; + const csmFloat32 dy = _faceTargetY - _faceY; + + if (CubismMath::AbsF(dx) <= Epsilon && CubismMath::AbsF(dy) <= Epsilon) + { + return; // 変化なし + } + + // 速度の最大よりも大きい場合は、速度を落とす + const csmFloat32 d = CubismMath::SqrtF((dx * dx) + (dy * dy)); + + // 進行方向の最大速度ベクトル + const csmFloat32 vx = MaxV * dx / d; + const csmFloat32 vy = MaxV * dy / d; + + // 現在の速度から、新規速度への変化(加速度)を求める + csmFloat32 ax = vx - _faceVX; + csmFloat32 ay = vy - _faceVY; + + const csmFloat32 a = CubismMath::SqrtF((ax * ax) + (ay * ay)); + + // 加速のとき + if (a < -MaxA || a > MaxA) + { + ax *= MaxA / a; + ay *= MaxA / a; + } + + // 加速度を元の速度に足して、新速度とする + _faceVX += ax; + _faceVY += ay; + + // 目的の方向に近づいたとき、滑らかに減速するための処理 + // 設定された加速度で止まることのできる距離と速度の関係から + // 現在とりうる最高速度を計算し、それ以上のときは速度を落とす + // ※本来、人間は筋力で力(加速度)を調整できるため、より自由度が高いが、簡単な処理ですませている + { + // 加速度、速度、距離の関係式。 + // 2 6 2 3 + // sqrt(a t + 16 a h t - 8 a h) - a t + // v = -------------------------------------- + // 2 + // 4 t - 2 + // (t=1) + // 時刻tは、あらかじめ加速度、速度を1/60(フレームレート、単位なし)で + // 考えているので、t=1として消してよい(※未検証) + + const csmFloat32 maxV = 0.5f * (CubismMath::SqrtF((MaxA * MaxA) + 16.0f * MaxA * d - 8.0f * MaxA * d) - MaxA); + const csmFloat32 curV = CubismMath::SqrtF((_faceVX * _faceVX) + (_faceVY * _faceVY)); + + if (curV > maxV) + { + // 現在の速度 > 最高速度のとき、最高速度まで減速 + _faceVX *= maxV / curV; + _faceVY *= maxV / curV; + } + } + + _faceX += _faceVX; + _faceY += _faceVY; +} + +void CubismTargetPoint::Set(csmFloat32 x, csmFloat32 y) +{ + this->_faceTargetX = x; + this->_faceTargetY = y; +} + +csmFloat32 CubismTargetPoint::GetX() const +{ + return this->_faceX; +} + +csmFloat32 CubismTargetPoint::GetY() const +{ + return this->_faceY; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Math/CubismTargetPoint.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismFramework.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief 顔の向きの制御機能 + * + * 顔の向きの制御機能を提供するクラス。 + */ +class CubismTargetPoint +{ +public: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismTargetPoint(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismTargetPoint(); + + /** + * @brief 更新処理 + * + * 更新処理を行う。 + * + * @param[in] deltaTimeSeconds デルタ時間[秒] + */ + void Update(csmFloat32 deltaTimeSeconds); + + /** + * @brief X軸の顔の向きの値を取得 + * + * X軸の顔の向きの値を取得する。 + * + * @return X軸の顔の向きの値(-1.0 - 1.0) + */ + csmFloat32 GetX() const; + + /** + * @brief Y軸の顔の向きの値を取得 + * + * Y軸の顔の向きの値を取得する。 + * + * @return Y軸の顔の向きの値(-1.0 - 1.0) + */ + csmFloat32 GetY() const; + + /** + * @brief 顔の向きの目標値を設定 + * + * 顔の向きの目標値を設定する。 + * + * @param[in] x X軸の顔の向きの値(-1.0 - 1.0) + * @param[in] y Y軸の顔の向きの値(-1.0 - 1.0) + */ + void Set(csmFloat32 x, csmFloat32 y); + +private: + csmFloat32 _faceTargetX; ///< 顔の向きのX目標値(この値に近づいていく) + csmFloat32 _faceTargetY; ///< 顔の向きのY目標値(この値に近づいていく) + csmFloat32 _faceX; ///< 顔の向きX(-1.0 - 1.0) + csmFloat32 _faceY; ///< 顔の向きY(-1.0 - 1.0) + csmFloat32 _faceVX; ///< 顔の向きの変化速度X + csmFloat32 _faceVY; ///< 顔の向きの変化速度Y + csmFloat32 _lastTimeSeconds; ///< 最後の実行時間[秒] + csmFloat32 _userTimeSeconds; ///< デルタ時間の積算値[秒] + +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Math/CubismVector2.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismVector2.hpp" +#include "Math/CubismMath.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { +CubismVector2 operator+(const CubismVector2& a, const CubismVector2& b) +{ + return CubismVector2(a.X + b.X, a.Y + b.Y); +} + +CubismVector2 operator-(const CubismVector2& a, const CubismVector2& b) +{ + return CubismVector2(a.X - b.X, a.Y - b.Y); +} + +CubismVector2 operator*(const CubismVector2& vector, const csmFloat32 scalar) +{ + return CubismVector2(vector.X * scalar, vector.Y * scalar); +} + +CubismVector2 operator*(const csmFloat32 scalar, const CubismVector2& vector) +{ + return CubismVector2(vector.X * scalar, vector.Y * scalar); +} + +CubismVector2 operator/(const CubismVector2& vector, const csmFloat32 scalar) +{ + return CubismVector2(vector.X / scalar, vector.Y / scalar); +} + +const CubismVector2& CubismVector2::operator+=(const CubismVector2& rhs) +{ + X += rhs.X; + Y += rhs.Y; + + return *this; +} + +const CubismVector2& CubismVector2::operator-=(const CubismVector2& rhs) +{ + X -= rhs.X; + Y -= rhs.Y; + + return *this; +} + +const CubismVector2& CubismVector2::operator*=(const CubismVector2& rhs) +{ + X *= rhs.X; + Y *= rhs.Y; + + return *this; +} + +const CubismVector2& CubismVector2::operator/=(const CubismVector2& rhs) +{ + X /= rhs.X; + Y /= rhs.Y; + + return *this; +} + +const CubismVector2& CubismVector2::operator*=(const csmFloat32 scalar) +{ + X *= scalar; + Y *= scalar; + + return *this; +} + +const CubismVector2& CubismVector2::operator/=(const csmFloat32 scalar) +{ + X /= scalar; + Y /= scalar; + + return *this; +} + +csmBool CubismVector2::operator==(const CubismVector2& rhs) const +{ + return (X == rhs.X) && (Y == rhs.Y); +} + +csmBool CubismVector2::operator!=(const CubismVector2& rhs) const +{ + return !(*this == rhs); +} + +csmFloat32 CubismVector2::GetLength() const +{ + return CubismMath::SqrtF(X * X + Y * Y); +} + +csmFloat32 CubismVector2::GetDistanceWith(CubismVector2 a) const +{ + return CubismMath::SqrtF(((X - a.X) * (X - a.X)) + ((Y - a.Y) * (Y - a.Y))); +} + +csmFloat32 CubismVector2::Dot(const CubismVector2& a) const +{ + return (X * a.X) + (Y * a.Y); +} + +void CubismVector2::Normalize() +{ + const csmFloat32 length = powf((X * X) + (Y * Y), 0.5f); + + X = X / length; + Y = Y / length; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Math/CubismVector2.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismFramework.hpp" +#include "math.h" + +#ifndef NULL +# define NULL 0 +#endif + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief 2次元ベクトル型 + * + * 2次元ベクトル型の機能を提供する。 + */ +struct CubismVector2 +{ + csmFloat32 X; ///< X軸の値 + csmFloat32 Y; ///< Y軸の値 + + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismVector2(): X(0.0f), Y(0.0f) + {} + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + CubismVector2(csmFloat32 x, csmFloat32 y) : X(x), Y(y) + {} + + /** + * @brief ベクトルの加算 + * + * ベクトルを加算する。 + * a + b = result + * + * @param[in] a 値a + * @oaram[in] b 値b + * @return a + bの結果 + */ + friend CubismVector2 operator+(const CubismVector2& a, const CubismVector2& b); + + /** + * @brief ベクトルの減算 + * + * ベクトルを減算する。 + * a - b = result + * + * @param[in] a 値a + * @param[in] b 値b + * @return a - bの結果 + */ + friend CubismVector2 operator-(const CubismVector2& a, const CubismVector2& b); + + /** + * @brief ベクトルの乗算(ベクトル値とスカラー値) + * + * ベクトルを乗算する。(ベクトル値とスカラー値) + * vector * scalar = result + * + * @param[in] vector 値vector(ベクトル値) + * @param[in] scalar 値scalar(スカラー値) + * @return vector * scalarの結果(ベクトル値) + */ + friend CubismVector2 operator*(const CubismVector2& vector, const csmFloat32 scalar); + + /** + * @brief ベクトルの乗算(スカラー値とベクトル値) + * + * ベクトルを乗算する。(スカラー値とベクトル値) + * scalar * vector = result + * + * @param[in] scalar 値scalar(スカラー値) + * @param[in] vector 値vector(ベクトル値) + * @return scalar * vectorの結果(ベクトル値) + */ + friend CubismVector2 operator*(const csmFloat32 scalar, const CubismVector2& vector); + + /** + * @brief ベクトルの除算(ベクトル値とスカラー値) + * + * ベクトルを除算する。(ベクトル値とスカラー値) + * vector / scalar = result + * + * @param[in] vector 値vector(ベクトル値) + * @param[in] scalar 値scalar(スカラー値) + * @return vector / scalarの結果(ベクトル値) + */ + friend CubismVector2 operator/(const CubismVector2& vector, const csmFloat32 scalar); + + /** + * @brief 加算 + * + * 加算する。 + * + * @param[in] rhs 加算する値 + * @return 結果 + */ + const CubismVector2& operator+=(const CubismVector2& rhs); + + /** + * @brief 減算 + * + * 減算する。 + * + * @param[in] rhs 減算する値 + * @return 結果 + */ + const CubismVector2& operator-=(const CubismVector2& rhs); + + /** + * @brief 乗算(ベクトル値) + * + * 乗算する。(ベクトル値) + * + * @param[in] rhs 乗算する値(ベクトル値) + * @return 結果 + */ + const CubismVector2& operator*=(const CubismVector2& rhs); + + /** + * @brief 除算(ベクトル値) + * + * 除算する。(ベクトル値) + * + * @param[in] rhs 除算する値(ベクトル値) + * @return 結果 + */ + const CubismVector2& operator/=(const CubismVector2& rhs); + + /** + * @brief 乗算(スカラー値) + * + * 乗算する。(スカラー値) + * + * @param[in] scalar 乗算する値(スカラー値) + * @return 結果 + */ + const CubismVector2& operator*=(const csmFloat32 scalar); + + /** + * @brief 除算(スカラー値) + * + * 除算する。(スカラー値) + * + * @param[in] scalar 除算する値(スカラー値) + * @return 結果 + */ + const CubismVector2& operator/=(const csmFloat32 scalar); + + /** + * @brief 等しさの確認(等しいか?) + * + * 値が等しいか? + * + * @param[in] rhs 確認する値 + * @retval true 値は等しい + * @retval false 値は等しくない + */ + csmBool operator==(const CubismVector2& rhs) const; + + /** + * @brief 等しさの確認(等しくないか?) + * + * 値が等しくないか? + * + * @param[in] rhs 確認する値 + * @retval true 値は等しくない + * @retval false 値は等しい + */ + csmBool operator!=(const CubismVector2& rhs) const; + + /** + * @brief 正規化の適用 + * + * 正規化する。 + */ + void Normalize(); + + /** + * @brief ベクトルの長さの取得 + * + * ベクトルの長さを取得する。 + * + * @return ベクトルの長さ + */ + csmFloat32 GetLength() const; + + /** + * @brief ベクトルの距離の取得 + * + * ベクトルの距離を取得する。 + * + * @param[in] a 点 + * @return ベクトルの距離 + */ + csmFloat32 GetDistanceWith(CubismVector2 a) const; + + /** + * @brief ドット積の計算 + * + * ドット積を計算する。 + * + * @param[in] a 値 + * @return 結果 + */ + csmFloat32 Dot(const CubismVector2& a) const; +}; + +// Utility functions for csmVector2. +CubismVector2 operator+(const CubismVector2& a, const CubismVector2& b); +CubismVector2 operator-(const CubismVector2& a, const CubismVector2& b); +CubismVector2 operator*(const CubismVector2& vector, const csmFloat32 scalar); +CubismVector2 operator*(const csmFloat32 scalar, const CubismVector2& vector); +CubismVector2 operator/(const CubismVector2& vector, const csmFloat32 scalar); + +}}} + +//------------------------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Math/CubismViewMatrix.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismViewMatrix.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +CubismViewMatrix::CubismViewMatrix() + : _screenLeft(0.0f) + , _screenRight(0.0f) + , _screenTop(0.0f) + , _screenBottom(0.0f) + , _maxLeft(0.0f) + , _maxRight(0.0f) + , _maxTop(0.0f) + , _maxBottom(0.0f) + , _maxScale(0.0f) + , _minScale(0.0f) +{ } + +CubismViewMatrix::~CubismViewMatrix() +{ } + +csmFloat32 CubismViewMatrix::GetMaxScale() const +{ + return _maxScale; +} + +csmFloat32 CubismViewMatrix::GetMinScale() const +{ + return _minScale; +} + +csmFloat32 CubismViewMatrix::GetScreenLeft() const +{ + return _screenLeft; +} + +csmFloat32 CubismViewMatrix::GetScreenRight() const +{ + return _screenRight; +} + +csmFloat32 CubismViewMatrix::GetScreenBottom() const +{ + return _screenBottom; +} + +csmFloat32 CubismViewMatrix::GetScreenTop() const +{ + return _screenTop; +} + +csmFloat32 CubismViewMatrix::GetMaxLeft() const +{ + return _maxLeft; +} + +csmFloat32 CubismViewMatrix::GetMaxRight() const +{ + return _maxRight; +} + +csmFloat32 CubismViewMatrix::GetMaxBottom() const +{ + return _maxBottom; +} + +csmFloat32 CubismViewMatrix::GetMaxTop() const +{ + return _maxTop; +} + +csmBool CubismViewMatrix::IsMaxScale() const +{ + return GetScaleX() >= _maxScale; +} + +csmBool CubismViewMatrix::IsMinScale() const +{ + return GetScaleX() <= _minScale; +} + + +void CubismViewMatrix::AdjustTranslate(csmFloat32 x, csmFloat32 y) +{ + if (_tr[0] * _maxLeft + (_tr[12] + x) > _screenLeft) + { + x = _screenLeft - _tr[0] * _maxLeft - _tr[12]; + } + + if (_tr[0] * _maxRight + (_tr[12] + x) < _screenRight) + { + x = _screenRight - _tr[0] * _maxRight - _tr[12]; + } + + + if (_tr[5] * _maxTop + (_tr[13] + y) < _screenTop) + { + y = _screenTop - _tr[5] * _maxTop - _tr[13]; + } + + if (_tr[5] * _maxBottom + (_tr[13] + y) > _screenBottom) + { + y = _screenBottom - _tr[5] * _maxBottom - _tr[13]; + } + + csmFloat32 tr1[] = { + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + x, y, 0.0f, 1.0f + }; + Multiply(tr1, _tr, _tr); +} + +void CubismViewMatrix::AdjustScale(csmFloat32 cx, csmFloat32 cy, csmFloat32 scale) +{ + csmFloat32 MaxScale = GetMaxScale(); + csmFloat32 MinScale = GetMinScale(); + + csmFloat32 targetScale = scale * _tr[0]; // + + if (targetScale < MinScale) + { + if (_tr[0] > 0.0f) + { + scale = MinScale / _tr[0]; + } + } + else if (targetScale > MaxScale) + { + if (_tr[0] > 0.0f) + { + scale = MaxScale / _tr[0]; + } + } + + csmFloat32 tr1[16] = { + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + cx, cy, 0.0f, 1.0f + }; + csmFloat32 tr2[16] = { + scale, 0.0f, 0.0f, 0.0f, + 0.0f, scale, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + csmFloat32 tr3[16] = { + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + -cx, -cy, 0.0f, 1.0f + }; + + Multiply(tr3, _tr, _tr); + Multiply(tr2, _tr, _tr); + Multiply(tr1, _tr, _tr); +} + +void CubismViewMatrix::SetScreenRect(csmFloat32 left, csmFloat32 right, csmFloat32 bottom, csmFloat32 top) +{ + _screenLeft = left; + _screenRight = right; + _screenTop = top; + _screenBottom = bottom; +} + +void CubismViewMatrix::SetMaxScreenRect(csmFloat32 left, csmFloat32 right, csmFloat32 bottom, csmFloat32 top) +{ + _maxLeft = left; + _maxRight = right; + _maxTop = top; + _maxBottom = bottom; +} + +void CubismViewMatrix::SetMaxScale(csmFloat32 maxScale) +{ + _maxScale = maxScale; +} + +void CubismViewMatrix::SetMinScale(csmFloat32 minScale) +{ + _minScale = minScale; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Math/CubismViewMatrix.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismMatrix44.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { +/** + * @brief カメラの位置変更に使うと便利な4x4行列 + * + * カメラの位置変更に使うと便利な4x4行列のクラス。 + */ +class CubismViewMatrix : public CubismMatrix44 +{ +public: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismViewMatrix(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismViewMatrix(); + + /** + * @brief 移動を調整 + * + * 移動を調整する。 + * + * @param[in] x X軸の移動量 + * @param[in] y Y軸の移動量 + */ + void AdjustTranslate(csmFloat32 x, csmFloat32 y); + + /** + * @brief 拡大率を調整 + * + * 拡大率を調整する。 + * + * @param[in] cx 拡大を行うX軸の中心位置 + * @param[in] cy 拡大を行うY軸の中心位置 + * @param[in] scale 拡大率 + */ + void AdjustScale(csmFloat32 cx, csmFloat32 cy, csmFloat32 scale); + + /** + * @brief デバイスに対応する論理座標上の範囲の設定 + * + * デバイスに対応する論理座標上の範囲の設定を行う。 + * + * @param[in] left 左辺のX軸の位置 + * @param[in] right 右辺のX軸の位置 + * @param[in] bottom 下辺のY軸の位置 + * @param[in] top 上辺のY軸の位置 + */ + void SetScreenRect(csmFloat32 left, csmFloat32 right, csmFloat32 bottom, csmFloat32 top); + + /** + * @brief デバイスに対応する論理座標上の移動可能範囲の設定 + * + * デバイスに対応する論理座標上の移動可能範囲の設定を行う。 + * + * @param[in] left 左辺のX軸の位置 + * @param[in] right 右辺のX軸の位置 + * @param[in] bottom 下辺のY軸の位置 + * @param[in] top 上辺のY軸の位置 + */ + void SetMaxScreenRect(csmFloat32 left, csmFloat32 right, csmFloat32 bottom, csmFloat32 top); + + /** + * @brief 最大拡大率の設定 + * + * 最大拡大率を設定する。 + * + * @param[in] maxScale 最大拡大率 + */ + void SetMaxScale(csmFloat32 maxScale); + + /** + * @brief 最小拡大率の設定 + * + * 最小拡大率を設定する。 + * + * @param[in] minScale 最小拡大率 + */ + void SetMinScale(csmFloat32 minScale); + + /** + * @brief 最大拡大率の取得 + * + * 最大拡大率を取得する。 + * + * @return 最大拡大率 + */ + csmFloat32 GetMaxScale() const; + + /** + * @brief 最小拡大率の取得 + * + * 最小拡大率を取得する。 + * + * @return 最小拡大率 + */ + csmFloat32 GetMinScale() const; + + /** + * @brief 拡大率が最大か? + * + * 拡大率が最大になっているかどうかを確認する。 + * + * @retval true 拡大率は最大になっている + * @retval false 拡大率は最大になっていない + */ + csmBool IsMaxScale() const; + + /** + * @brief 拡大率が最小か? + * + * 拡大率が最小になっているかどうかを確認する。 + * + * @retval true 拡大率は最小になっている + * @retval false 拡大率は最小になっていない + */ + csmBool IsMinScale() const; + + /** + * @brief デバイスに対応する論理座標の左辺のX軸位置の取得 + * + * デバイスに対応する論理座標の左辺のX軸位置を取得する。 + * + * @return デバイスに対応する論理座標の左辺のX軸位置 + */ + csmFloat32 GetScreenLeft() const; + + /** + * @brief デバイスに対応する論理座標の右辺のX軸位置の取得 + * + * デバイスに対応する論理座標の右辺のX軸位置を取得する。 + * + * @return デバイスに対応する論理座標の右辺のX軸位置 + */ + csmFloat32 GetScreenRight() const; + + /** + * @brief デバイスに対応する論理座標の下辺のY軸位置の取得 + * + * デバイスに対応する論理座標の下辺のY軸位置を取得する。 + * + * @return デバイスに対応する論理座標の下辺のY軸位置 + */ + csmFloat32 GetScreenBottom() const; + + /** + * @brief デバイスに対応する論理座標の上辺のY軸位置の取得 + * + * デバイスに対応する論理座標の上辺のY軸位置を取得する。 + * + * @return デバイスに対応する論理座標の上辺のY軸位置 + */ + csmFloat32 GetScreenTop() const; + + /** + * @brief 左辺のX軸位置の最大値の取得 + * + * 左辺のX軸位置の最大値を取得する。 + * + * @return 左辺のX軸位置の最大値 + */ + csmFloat32 GetMaxLeft() const; + + /** + * @brief 右辺のX軸位置の最大値の取得 + * + * 右辺のX軸位置の最大値を取得する。 + * + * @return 右辺のX軸位置の最大値 + */ + csmFloat32 GetMaxRight() const; + + /** + * @brief 下辺のY軸位置の最大値の取得 + * + * 下辺のY軸位置の最大値を取得する。 + * + * @return 下辺のY軸位置の最大値 + */ + csmFloat32 GetMaxBottom() const; + + /** + * @brief 上辺のY軸位置の最大値の取得 + * + * 上辺のY軸位置の最大値を取得する。 + * + * @return 上辺のY軸位置の最大値 + */ + csmFloat32 GetMaxTop() const; + +private: + csmFloat32 _screenLeft; ///< デバイスに対応する論理座標上の範囲(左辺X軸位置) + csmFloat32 _screenRight; ///< デバイスに対応する論理座標上の範囲(右辺X軸位置) + csmFloat32 _screenTop; ///< デバイスに対応する論理座標上の範囲(下辺Y軸位置) + csmFloat32 _screenBottom; ///< デバイスに対応する論理座標上の範囲(上辺Y軸位置) + csmFloat32 _maxLeft; ///< 論理座標上の移動可能範囲(左辺X軸位置) + csmFloat32 _maxRight; ///< 論理座標上の移動可能範囲(右辺X軸位置) + csmFloat32 _maxTop; ///< 論理座標上の移動可能範囲(下辺Y軸位置) + csmFloat32 _maxBottom; ///< 論理座標上の移動可能範囲(上辺Y軸位置) + csmFloat32 _maxScale; ///< 拡大率の最大値 + csmFloat32 _minScale; ///< 拡大率の最小値 +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Model/CubismMoc.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismMoc.hpp" +#include "CubismModel.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +CubismMoc* CubismMoc::Create(const csmByte* mocBytes, csmSizeInt size, csmBool shouldCheckMocConsistency) +{ + CubismMoc* cubismMoc = NULL; + + void* alignedBuffer = CSM_MALLOC_ALLIGNED(size, Core::csmAlignofMoc); + memcpy(alignedBuffer, mocBytes, size); + + if (shouldCheckMocConsistency) + { + // .moc3の整合性を確認 + csmBool consistency = HasMocConsistency(alignedBuffer, size); + if (!consistency) + { + CSM_FREE_ALLIGNED(alignedBuffer); + + // 整合性が確認できなければ処理しない + CubismLogError("Inconsistent MOC3."); + return cubismMoc; + } + } + + Core::csmMoc* moc = Core::csmReviveMocInPlace(alignedBuffer, size); + const Core::csmMocVersion version = Core::csmGetMocVersion(alignedBuffer, size); + + if (moc) + { + cubismMoc = CSM_NEW CubismMoc(moc); + cubismMoc->_mocVersion = version; + } + + return cubismMoc; +} + +void CubismMoc::Delete(CubismMoc* moc) +{ + CSM_DELETE_SELF(CubismMoc, moc); +} + +CubismMoc::CubismMoc(Core::csmMoc* moc) + : _moc(moc) + , _modelCount(0) + , _mocVersion(0) +{ } + +CubismMoc::~CubismMoc() +{ + CSM_ASSERT(_modelCount == 0); + + CSM_FREE_ALLIGNED(_moc); +} + +CubismModel* CubismMoc::CreateModel() +{ + CubismModel* cubismModel = NULL; + const csmUint32 modelSize = Core::csmGetSizeofModel(_moc); + void* modelMemory = CSM_MALLOC_ALLIGNED(modelSize, Core::csmAlignofModel); + + Core::csmModel* model = Core::csmInitializeModelInPlace(_moc, modelMemory, modelSize); + + if (model) + { + cubismModel = CSM_NEW CubismModel(model); + cubismModel->Initialize(); + + ++_modelCount; + } + + return cubismModel; +} + +void CubismMoc::DeleteModel(CubismModel* model) +{ + CSM_DELETE_SELF(CubismModel, model); + --_modelCount; +} + +Core::csmMocVersion CubismMoc::GetLatestMocVersion() +{ + return Core::csmGetLatestMocVersion(); +} + +Core::csmMocVersion CubismMoc::GetMocVersion() +{ + return _mocVersion; +} + +csmBool CubismMoc::HasMocConsistency(void* address, const csmUint32 size) +{ + csmInt32 isConsistent = Core::csmHasMocConsistency(address, size); + return isConsistent != 0 ? true : false; +} + +csmBool CubismMoc::HasMocConsistencyFromUnrevivedMoc(const csmByte* mocBytes, csmSizeInt size) +{ + void* alignedBuffer = CSM_MALLOC_ALLIGNED(size, Core::csmAlignofMoc); + memcpy(alignedBuffer, mocBytes, size); + + csmBool consistency = CubismMoc::HasMocConsistency(alignedBuffer, size); + + CSM_FREE_ALLIGNED(alignedBuffer); + + return consistency; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Model/CubismMoc.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismFramework.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +class CubismModel; + +/** + * @brief Mocデータの管理 + * + * Mocデータの管理を行うクラス。 + */ +class CubismMoc +{ + friend class CubismModel; +public: + /** + * @brief バッファからMocデータの作成 + * + * バッファからMocファイルを読み取り、Mocデータを作成する。 + * + * @param[in] mocBytes Mocファイルのバッファ + * @param[in] size バッファのサイズ + * @param[in] shouldCheckMocConsistency MOCの整合性チェックフラグ(初期値 : false) + */ + static CubismMoc* Create(const csmByte* mocBytes, csmSizeInt size, csmBool shouldCheckMocConsistency = false); + + /** + * @brief Mocデータを削除 + * + * Mocデータを削除する。 + */ + static void Delete(CubismMoc* moc); + + /** + * @brief モデルを作成 + * + * モデルを作成する。 + * + * @return Mocデータから作成されたモデル + */ + CubismModel* CreateModel(); + + /** + * @brief モデルを削除 + * + * モデルを削除する。 + * + * @param[in] model 対象のモデル + */ + void DeleteModel(CubismModel* model); + + /** + * @brief 最新の.moc3 Versionを取得 + * + * 最新の.moc3 Versionを取得する。 + * + * @return 最新の.moc3 Version + */ + static Core::csmMocVersion GetLatestMocVersion(); + + /** + * @brief 読み込んだモデルの.moc3 Versionを取得 + * + * 読み込んだモデルの.moc3 Versionを取得する。 + * + * @return 読み込んだモデルの.moc3 Version + */ + Core::csmMocVersion GetMocVersion(); + + /** + * @brief Checks consistency of a moc. + * + * @param address Address of unrevived moc. The address must be aligned to 'csmAlignofMoc'. + * @param size Size of moc (in bytes). + * + * @return '1' if Moc is valid; '0' otherwise. + */ + static csmBool HasMocConsistency(void* address, const csmUint32 size); + + /** + * @brief Checks consistency of a moc. + * + * @param mocBytes Mocファイルのバッファ + * @param size バッファのサイズ + * + * @return 'true' if Moc is valid; 'false' otherwise. + */ + static csmBool HasMocConsistencyFromUnrevivedMoc(const csmByte* mocBytes, csmSizeInt size); + +private: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismMoc(Core::csmMoc* moc); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismMoc(); + + Core::csmMoc* _moc; ///< Mocデータ + csmInt32 _modelCount; ///< Mocデータから作られたモデルの個数 + csmUint32 _mocVersion; ///< 読み込んだモデルの.moc3 Version +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Model/CubismModel.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismModel.hpp" +#include "Rendering/CubismRenderer.hpp" +#include "Id/CubismId.hpp" +#include "Id/CubismIdManager.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +static csmInt32 IsBitSet(const csmUint8 byte, const csmUint8 mask) +{ + return ((byte & mask) == mask); +} + +CubismModel::CubismModel(Core::csmModel* model) + : _model(model) + , _parameterValues(NULL) + , _parameterMaximumValues(NULL) + , _parameterMinimumValues(NULL) + , _partOpacities(NULL) + , _modelOpacity(1.0f) + , _isOverwrittenModelMultiplyColors(false) + , _isOverwrittenModelScreenColors(false) + , _isOverwrittenCullings(false) +{ } + +CubismModel::~CubismModel() +{ + CSM_FREE_ALLIGNED(_model); +} + +csmFloat32 CubismModel::GetParameterValue(CubismIdHandle parameterId) +{ + // 高速化のためにParameterIndexを取得できる機構になっているが、外部からの設定の時は呼び出し頻度が低いため不要 + const csmInt32 parameterIndex = GetParameterIndex(parameterId); + return GetParameterValue(parameterIndex); +} + +void CubismModel::SetParameterValue(CubismIdHandle parameterId, csmFloat32 value, csmFloat32 weight) +{ + const csmInt32 index = GetParameterIndex(parameterId); + SetParameterValue(index, value, weight); +} + +void CubismModel::AddParameterValue(CubismIdHandle parameterId, csmFloat32 value, csmFloat32 weight) +{ + const csmInt32 index = GetParameterIndex(parameterId); + AddParameterValue(index, value, weight); +} + +void CubismModel::AddParameterValue(csmInt32 parameterIndex, csmFloat32 value, csmFloat32 weight) +{ + SetParameterValue(parameterIndex, (GetParameterValue(parameterIndex) + (value * weight))); +} + +void CubismModel::MultiplyParameterValue(CubismIdHandle parameterId, csmFloat32 value, csmFloat32 weight) +{ + const csmInt32 index = GetParameterIndex(parameterId); + MultiplyParameterValue(index, value, weight); +} + +void CubismModel::MultiplyParameterValue(csmInt32 parameterIndex, csmFloat32 value, csmFloat32 weight) +{ + SetParameterValue(parameterIndex, (GetParameterValue(parameterIndex) * (1.0f + (value - 1.0f) * weight))); +} + +void CubismModel::Update() const +{ + // Update model. + Core::csmUpdateModel(_model); + + // Reset dynamic drawable flags. + Core::csmResetDrawableDynamicFlags(_model); +} + +void CubismModel::SetPartOpacity(CubismIdHandle partId, csmFloat32 opacity) +{ + // 高速化のためにPartIndexを取得できる機構になっているが、外部からの設定の時は呼び出し頻度が低いため不要 + const csmInt32 index = GetPartIndex(partId); + + if (index < 0) + { + return; // パーツが無いのでスキップ + } + + SetPartOpacity(index, opacity); +} + +void CubismModel::SetPartOpacity(csmInt32 partIndex, csmFloat32 opacity) +{ + if (_notExistPartOpacities.IsExist(partIndex)) + { + _notExistPartOpacities[partIndex] = opacity; + return; + } + + //インデックスの範囲内検知 + CSM_ASSERT(0 <= partIndex && partIndex < GetPartCount()); + + _partOpacities[partIndex] = opacity; +} + +csmFloat32 CubismModel::GetPartOpacity(CubismIdHandle partId) +{ + // 高速化のためにPartIndexを取得できる機構になっているが、外部からの設定の時は呼び出し頻度が低いため不要 + const csmInt32 index = GetPartIndex(partId); + + if (index < 0) + { + return 0; //パーツが無いのでスキップ + } + + return GetPartOpacity(index); +} + +csmFloat32 CubismModel::GetPartOpacity(csmInt32 partIndex) +{ + if (_notExistPartOpacities.IsExist(partIndex)) + { + // モデルに存在しないパーツIDの場合、非存在パーツリストから不透明度を返す + return _notExistPartOpacities[partIndex]; + } + + //インデックスの範囲内検知 + CSM_ASSERT(0 <= partIndex && partIndex < GetPartCount()); + + return _partOpacities[partIndex]; +} + +csmInt32 CubismModel::GetParameterCount() const +{ + return Core::csmGetParameterCount(_model); +} + +Core::csmParameterType CubismModel::GetParameterType(csmUint32 parameterIndex) const +{ + return Core::csmGetParameterTypes(_model)[parameterIndex]; +} + +csmFloat32 CubismModel::GetParameterDefaultValue(csmUint32 parameterIndex) const +{ + return Core::csmGetParameterDefaultValues(_model)[parameterIndex]; +} + +csmFloat32 CubismModel::GetParameterMaximumValue(csmUint32 parameterIndex) const +{ + return Core::csmGetParameterMaximumValues(_model)[parameterIndex]; +} + +csmFloat32 CubismModel::GetParameterMinimumValue(csmUint32 parameterIndex) const +{ + return Core::csmGetParameterMinimumValues(_model)[parameterIndex]; +} + +csmInt32 CubismModel::GetParameterIndex(CubismIdHandle parameterId) +{ + csmInt32 parameterIndex; + const csmInt32 idCount = Core::csmGetParameterCount(_model); + + + for (parameterIndex = 0; parameterIndex < idCount; ++parameterIndex) + { + if (parameterId != _parameterIds[parameterIndex]) + { + continue; + } + + return parameterIndex; + } + + // モデルに存在していない場合、非存在パラメータIDリスト内を検索し、そのインデックスを返す + if (_notExistParameterId.IsExist(parameterId)) + { + return _notExistParameterId[parameterId]; + } + + // 非存在パラメータIDリストにない場合、新しく要素を追加する + parameterIndex = Core::csmGetParameterCount(_model) + _notExistParameterId.GetSize(); + + _notExistParameterId[parameterId] = parameterIndex; + _notExistParameterValues.AppendKey(parameterIndex); + + return parameterIndex; +} + +CubismIdHandle CubismModel::GetParameterId(csmUint32 parameterIndex) +{ + CSM_ASSERT(0 <= parameterIndex && parameterIndex < _parameterIds.GetSize()); + return _parameterIds[parameterIndex]; +} + +csmFloat32 CubismModel::GetParameterValue(csmInt32 parameterIndex) +{ + if (_notExistParameterValues.IsExist(parameterIndex)) + { + return _notExistParameterValues[parameterIndex]; + } + + //インデックスの範囲内検知 + CSM_ASSERT(0 <= parameterIndex && parameterIndex < GetParameterCount()); + + return _parameterValues[parameterIndex]; +} + +void CubismModel::SetParameterValue(csmInt32 parameterIndex, csmFloat32 value, csmFloat32 weight) +{ + if (_notExistParameterValues.IsExist(parameterIndex)) + { + _notExistParameterValues[parameterIndex] = (weight == 1) + ? value + : (_notExistParameterValues[parameterIndex] * (1 - weight)) + + (value * weight); + return; + } + + //インデックスの範囲内検知 + CSM_ASSERT(0 <= parameterIndex && parameterIndex < GetParameterCount()); + + if (Core::csmGetParameterMaximumValues(_model)[parameterIndex] < value) + { + value = Core::csmGetParameterMaximumValues(_model)[parameterIndex]; + } + if (Core::csmGetParameterMinimumValues(_model)[parameterIndex] > value) + { + value = Core::csmGetParameterMinimumValues(_model)[parameterIndex]; + } + + _parameterValues[parameterIndex] = (weight == 1) + ? value + : _parameterValues[parameterIndex] = (_parameterValues[parameterIndex] * (1 - weight)) + (value * weight); +} + +csmFloat32 CubismModel::GetCanvasWidthPixel() const +{ + if (_model == NULL) + { + return 0.0f; + } + + Core::csmVector2 tmpSizeInPixels; + Core::csmVector2 tmpOriginInPixels; + csmFloat32 tmpPixelsPerUnit; + + Core::csmReadCanvasInfo(_model, &tmpSizeInPixels, &tmpOriginInPixels, &tmpPixelsPerUnit); + + return tmpSizeInPixels.X; +} + +csmFloat32 CubismModel::GetCanvasHeightPixel() const +{ + if (_model == NULL) + { + return 0.0f; + } + + Core::csmVector2 tmpSizeInPixels; + Core::csmVector2 tmpOriginInPixels; + csmFloat32 tmpPixelsPerUnit; + + Core::csmReadCanvasInfo(_model, &tmpSizeInPixels, &tmpOriginInPixels, &tmpPixelsPerUnit); + + return tmpSizeInPixels.Y; +} + +csmFloat32 CubismModel::GetPixelsPerUnit() const +{ + if (_model == NULL) + { + return 0.0f; + } + + Core::csmVector2 tmpSizeInPixels; + Core::csmVector2 tmpOriginInPixels; + csmFloat32 tmpPixelsPerUnit; + + Core::csmReadCanvasInfo(_model, &tmpSizeInPixels, &tmpOriginInPixels, &tmpPixelsPerUnit); + + return tmpPixelsPerUnit; +} + +csmFloat32 CubismModel::GetCanvasWidth() const +{ + if (_model == NULL) + { + return 0.0f; + } + + Core::csmVector2 tmpSizeInPixels; + Core::csmVector2 tmpOriginInPixels; + csmFloat32 tmpPixelsPerUnit; + + Core::csmReadCanvasInfo(_model, &tmpSizeInPixels, &tmpOriginInPixels, &tmpPixelsPerUnit); + + return tmpSizeInPixels.X / tmpPixelsPerUnit; +} + +csmFloat32 CubismModel::GetCanvasHeight() const +{ + if (_model == NULL) + { + return 0.0f; + } + + Core::csmVector2 tmpSizeInPixels; + Core::csmVector2 tmpOriginInPixels; + csmFloat32 tmpPixelsPerUnit; + + Core::csmReadCanvasInfo(_model, &tmpSizeInPixels, &tmpOriginInPixels, &tmpPixelsPerUnit); + + return tmpSizeInPixels.Y / tmpPixelsPerUnit; +} + +csmInt32 CubismModel::GetDrawableIndex(CubismIdHandle drawableId) const +{ + const csmInt32 drawableCount = Core::csmGetDrawableCount(_model); + + for (csmInt32 drawableIndex = 0; drawableIndex < drawableCount; ++drawableIndex) + { + if (_drawableIds[drawableIndex] == drawableId) + { + return drawableIndex; + } + } + + return -1; +} + +const csmFloat32* CubismModel::GetDrawableVertices(csmInt32 drawableIndex) const +{ + return reinterpret_cast(GetDrawableVertexPositions(drawableIndex)); +} + +csmInt32 CubismModel::GetPartIndex(CubismIdHandle partId) +{ + csmInt32 partIndex; + const csmInt32 idCount = Core::csmGetPartCount(_model); + + for (partIndex = 0; partIndex < idCount; ++partIndex) + { + if (partId == _partIds[partIndex]) + { + return partIndex; + } + } + + const csmInt32 partCount = Core::csmGetPartCount(_model); + + // モデルに存在していない場合、非存在パーツIDリスト内にあるかを検索し、そのインデックスを返す + if (_notExistPartId.IsExist(partId)) + { + return _notExistPartId[partId]; + } + + // 非存在パーツIDリストにない場合、新しく要素を追加する + partIndex = partCount + _notExistPartId.GetSize(); + + _notExistPartId[partId] = partIndex; + _notExistPartOpacities.AppendKey(partIndex); + + return partIndex; +} + +void CubismModel::Initialize() +{ + CSM_ASSERT(_model); + + _parameterValues = Core::csmGetParameterValues(_model); + _partOpacities = Core::csmGetPartOpacities(_model); + _parameterMaximumValues = Core::csmGetParameterMaximumValues(_model); + _parameterMinimumValues = Core::csmGetParameterMinimumValues(_model); + + { + const csmChar** parameterIds = Core::csmGetParameterIds(_model); + const csmInt32 parameterCount = Core::csmGetParameterCount(_model); + + _parameterIds.PrepareCapacity(parameterCount); + for (csmInt32 i = 0; i < parameterCount; ++i) + { + _parameterIds.PushBack(CubismFramework::GetIdManager()->GetId(parameterIds[i])); + } + } + + const csmInt32 partCount = Core::csmGetPartCount(_model); + { + const csmChar** partIds = Core::csmGetPartIds(_model); + + _partIds.PrepareCapacity(partCount); + for (csmInt32 i = 0; i < partCount; ++i) + { + _partIds.PushBack(CubismFramework::GetIdManager()->GetId(partIds[i])); + } + + _userPartMultiplyColors.PrepareCapacity(partCount); + _userPartScreenColors.PrepareCapacity(partCount); + _partChildDrawables.Resize(partCount); + } + + { + const csmChar** drawableIds = Core::csmGetDrawableIds(_model); + const csmInt32 drawableCount = Core::csmGetDrawableCount(_model); + + _drawableIds.PrepareCapacity(drawableCount); + _userMultiplyColors.PrepareCapacity(drawableCount); + _userScreenColors.PrepareCapacity(drawableCount); + _userCullings.PrepareCapacity(drawableCount); + + // カリング設定 + DrawableCullingData userCulling; + userCulling.IsOverwritten = false; + userCulling.IsCulling = 0; + + // 乗算色 + Rendering::CubismRenderer::CubismTextureColor multiplyColor; + multiplyColor.R = 1.0f; + multiplyColor.G = 1.0f; + multiplyColor.B = 1.0f; + multiplyColor.A = 1.0f; + + // スクリーン色 + Rendering::CubismRenderer::CubismTextureColor screenColor; + screenColor.R = 0.0f; + screenColor.G = 0.0f; + screenColor.B = 0.0f; + screenColor.A = 1.0f; + + // Parts + { + // 乗算色 + PartColorData userMultiplyColor; + userMultiplyColor.IsOverwritten = false; + userMultiplyColor.Color = multiplyColor; + + // スクリーン色 + PartColorData userScreenColor; + userScreenColor.IsOverwritten = false; + userScreenColor.Color = screenColor; + + for (csmInt32 i = 0; i < partCount; ++i) + { + _userPartMultiplyColors.PushBack(userMultiplyColor); + _userPartScreenColors.PushBack(userScreenColor); + } + } + + // Drawables + { + // 乗算色 + DrawableColorData userMultiplyColor; + userMultiplyColor.IsOverwritten = false; + userMultiplyColor.Color = multiplyColor; + + // スクリーン色 + DrawableColorData userScreenColor; + userScreenColor.IsOverwritten = false; + userScreenColor.Color = screenColor; + + for (csmInt32 i = 0; i < drawableCount; ++i) + { + _drawableIds.PushBack(CubismFramework::GetIdManager()->GetId(drawableIds[i])); + _userMultiplyColors.PushBack(userMultiplyColor); + _userScreenColors.PushBack(userScreenColor); + _userCullings.PushBack(userCulling); + + csmInt32 parentIndex = Core::csmGetDrawableParentPartIndices(_model)[i]; + if (parentIndex >= 0) + { + _partChildDrawables[parentIndex].PushBack(i); + } + } + } + } +} + +CubismIdHandle CubismModel::GetDrawableId(csmInt32 drawableIndex) const +{ + CSM_ASSERT(0 <= drawableIndex && drawableIndex < _drawableIds.GetSize()); + return _drawableIds[drawableIndex]; +} + +CubismIdHandle CubismModel::GetPartId(csmUint32 partIndex) +{ + CSM_ASSERT(0 <= partIndex && partIndex < _partIds.GetSize()); + return _partIds[partIndex]; +} + +csmInt32 CubismModel::GetPartCount() const +{ + const csmInt32 partCount = Core::csmGetPartCount(_model); + return partCount; +} + +const csmInt32* CubismModel::GetDrawableRenderOrders() const +{ + const csmInt32* renderOrders = Core::csmGetDrawableRenderOrders(_model); + return renderOrders; +} + +csmInt32 CubismModel::GetDrawableCount() const +{ + const csmInt32 drawableCount = Core::csmGetDrawableCount(_model); + return drawableCount; +} + +csmInt32 CubismModel::GetDrawableTextureIndices(csmInt32 drawableIndex) const +{ + return GetDrawableTextureIndex(drawableIndex); +} + +csmInt32 CubismModel::GetDrawableTextureIndex(csmInt32 drawableIndex) const +{ + const csmInt32* textureIndices = Core::csmGetDrawableTextureIndices(_model); + return textureIndices[drawableIndex]; +} + +csmInt32 CubismModel::GetDrawableVertexIndexCount(csmInt32 drawableIndex) const +{ + const csmInt32* indexCounts = Core::csmGetDrawableIndexCounts(_model); + return indexCounts[drawableIndex]; +} + +csmInt32 CubismModel::GetDrawableVertexCount(csmInt32 drawableIndex) const +{ + const csmInt32* vertexCounts = Core::csmGetDrawableVertexCounts(_model); + return vertexCounts[drawableIndex]; +} + +const csmUint16* CubismModel::GetDrawableVertexIndices(csmInt32 drawableIndex) const +{ + const csmUint16** indicesArray = Core::csmGetDrawableIndices(_model); + return indicesArray[drawableIndex]; +} + +const Core::csmVector2* CubismModel::GetDrawableVertexPositions(csmInt32 drawableIndex) const +{ + const Core::csmVector2** verticesArray = Core::csmGetDrawableVertexPositions(_model); + return verticesArray[drawableIndex]; +} + +const Core::csmVector2* CubismModel::GetDrawableVertexUvs(csmInt32 drawableIndex) const +{ + const Core::csmVector2** uvsArray = Core::csmGetDrawableVertexUvs(_model); + return uvsArray[drawableIndex]; +} + +csmFloat32 CubismModel::GetDrawableOpacity(csmInt32 drawableIndex) const +{ + const csmFloat32* opacities = Core::csmGetDrawableOpacities(_model); + return opacities[drawableIndex]; +} + +Core::csmVector4 CubismModel::GetDrawableMultiplyColor(csmInt32 drawableIndex) const +{ + const Core::csmVector4* multiplyColors = Core::csmGetDrawableMultiplyColors(_model); + return multiplyColors[drawableIndex]; +} + +Core::csmVector4 CubismModel::GetDrawableScreenColor(csmInt32 drawableIndex) const +{ + const Core::csmVector4* screenColors = Core::csmGetDrawableScreenColors(_model); + return screenColors[drawableIndex]; +} + +csmInt32 CubismModel::GetDrawableParentPartIndex(csmUint32 drawableIndex) const +{ + return Core::csmGetDrawableParentPartIndices(_model)[drawableIndex]; +} + +csmBool CubismModel::GetDrawableDynamicFlagIsVisible(csmInt32 drawableIndex) const +{ + const Core::csmFlags* dynamicFlags = Core::csmGetDrawableDynamicFlags(_model); + return IsBitSet(dynamicFlags[drawableIndex], Core::csmIsVisible)!=0 ? true : false; +} + +csmBool CubismModel::GetDrawableDynamicFlagVisibilityDidChange(csmInt32 drawableIndex) const +{ + const Core::csmFlags* dynamicFlags = Core::csmGetDrawableDynamicFlags(_model); + return IsBitSet(dynamicFlags[drawableIndex], Core::csmVisibilityDidChange)!=0 ? true : false; +} + +csmBool CubismModel::GetDrawableDynamicFlagOpacityDidChange(csmInt32 drawableIndex) const +{ + const Core::csmFlags* dynamicFlags = Core::csmGetDrawableDynamicFlags(_model); + return IsBitSet(dynamicFlags[drawableIndex], Core::csmOpacityDidChange) != 0 ? true : false; +} + +csmBool CubismModel::GetDrawableDynamicFlagDrawOrderDidChange(csmInt32 drawableIndex) const +{ + const Core::csmFlags* dynamicFlags = Core::csmGetDrawableDynamicFlags(_model); + return IsBitSet(dynamicFlags[drawableIndex], Core::csmDrawOrderDidChange) != 0 ? true : false; +} + +csmBool CubismModel::GetDrawableDynamicFlagRenderOrderDidChange(csmInt32 drawableIndex) const +{ + const Core::csmFlags* dynamicFlags = Core::csmGetDrawableDynamicFlags(_model); + return IsBitSet(dynamicFlags[drawableIndex], Core::csmRenderOrderDidChange) != 0 ? true : false; +} + +csmBool CubismModel::GetDrawableDynamicFlagVertexPositionsDidChange(csmInt32 drawableIndex) const +{ + const Core::csmFlags* dynamicFlags = Core::csmGetDrawableDynamicFlags(_model); + return IsBitSet(dynamicFlags[drawableIndex], Core::csmVertexPositionsDidChange) != 0 ? true : false; +} + +csmBool CubismModel::GetDrawableDynamicFlagBlendColorDidChange(csmInt32 drawableIndex) const +{ + const Core::csmFlags* dynamicFlags = Core::csmGetDrawableDynamicFlags(_model); + return IsBitSet(dynamicFlags[drawableIndex], Core::csmBlendColorDidChange) != 0 ? true : false; +} + +Rendering::CubismRenderer::CubismBlendMode CubismModel::GetDrawableBlendMode(csmInt32 drawableIndex) const +{ + const csmUint8* constantFlags = Core::csmGetDrawableConstantFlags(_model); + return (IsBitSet(constantFlags[drawableIndex], Core::csmBlendAdditive)) + ? Rendering::CubismRenderer::CubismBlendMode_Additive + : (IsBitSet(constantFlags[drawableIndex], Core::csmBlendMultiplicative)) + ? Rendering::CubismRenderer::CubismBlendMode_Multiplicative + : Rendering::CubismRenderer::CubismBlendMode_Normal; +} + +csmBool CubismModel::GetDrawableInvertedMask(csmInt32 drawableIndex) const +{ + const csmUint8* constantFlags = Core::csmGetDrawableConstantFlags(_model); + return IsBitSet(constantFlags[drawableIndex], Core::csmIsInvertedMask) != 0 ? true : false; +} + +const csmInt32** CubismModel::GetDrawableMasks() const +{ + const csmInt32** masks = Core::csmGetDrawableMasks(_model); + return masks; +} + +const csmInt32* CubismModel::GetDrawableMaskCounts() const +{ + const csmInt32* maskCounts = Core::csmGetDrawableMaskCounts(_model); + return maskCounts; +} + +void CubismModel::LoadParameters() +{ + csmInt32 parameterCount = Core::csmGetParameterCount(_model); + const csmInt32 savedParameterCount = static_cast(_savedParameters.GetSize()); + + if (parameterCount > savedParameterCount) + { + parameterCount = savedParameterCount; + } + + for (csmInt32 i = 0; i < parameterCount; ++i) + { + _parameterValues[i] = _savedParameters[i]; + } +} + +void CubismModel::SaveParameters() +{ + const csmInt32 parameterCount = Core::csmGetParameterCount(_model); + const csmInt32 savedParameterCount = static_cast(_savedParameters.GetSize()); + + for (csmInt32 i = 0; i < parameterCount; ++i) + { + if (i < savedParameterCount) + { + _savedParameters[i] = _parameterValues[i]; + } + else + { + _savedParameters.PushBack(_parameterValues[i], false); + } + } +} + +Rendering::CubismRenderer::CubismTextureColor CubismModel::GetMultiplyColor(csmInt32 drawableIndex) const +{ + if (GetOverwriteFlagForModelMultiplyColors() || GetOverwriteFlagForDrawableMultiplyColors(drawableIndex)) + { + return _userMultiplyColors[drawableIndex].Color; + } + + const Core::csmVector4 color = GetDrawableMultiplyColor(drawableIndex); + + return Rendering::CubismRenderer::CubismTextureColor(color.X, color.Y, color.Z, color.W); +} + +Rendering::CubismRenderer::CubismTextureColor CubismModel::GetScreenColor(csmInt32 drawableIndex) const +{ + if (GetOverwriteFlagForModelScreenColors() || GetOverwriteFlagForDrawableScreenColors(drawableIndex)) + { + return _userScreenColors[drawableIndex].Color; + } + + const Core::csmVector4 color = GetDrawableScreenColor(drawableIndex); + return Rendering::CubismRenderer::CubismTextureColor(color.X, color.Y, color.Z, color.W); +} + +void CubismModel::SetMultiplyColor(csmInt32 drawableIndex, const Rendering::CubismRenderer::CubismTextureColor& color) +{ + SetMultiplyColor(drawableIndex, color.R, color.G, color.B, color.A); +} + +void CubismModel::SetMultiplyColor(csmInt32 drawableIndex, csmFloat32 r, csmFloat32 g, csmFloat32 b, csmFloat32 a) +{ + _userMultiplyColors[drawableIndex].Color.R = r; + _userMultiplyColors[drawableIndex].Color.G = g; + _userMultiplyColors[drawableIndex].Color.B = b; + _userMultiplyColors[drawableIndex].Color.A = a; +} + +void CubismModel::SetScreenColor(csmInt32 drawableIndex, const Rendering::CubismRenderer::CubismTextureColor& color) +{ + SetScreenColor(drawableIndex, color.R, color.G, color.B, color.A); +} + +void CubismModel::SetScreenColor(csmInt32 drawableIndex, csmFloat32 r, csmFloat32 g, csmFloat32 b, csmFloat32 a) +{ + _userScreenColors[drawableIndex].Color.R = r; + _userScreenColors[drawableIndex].Color.G = g; + _userScreenColors[drawableIndex].Color.B = b; + _userScreenColors[drawableIndex].Color.A = a; +} + +Rendering::CubismRenderer::CubismTextureColor CubismModel::GetPartMultiplyColor(csmInt32 partIndex) const +{ + return _userPartMultiplyColors[partIndex].Color; +} + +Rendering::CubismRenderer::CubismTextureColor CubismModel::GetPartScreenColor(csmInt32 partIndex) const +{ + return _userPartScreenColors[partIndex].Color; +} + +void CubismModel::SetPartMultiplyColor(csmInt32 partIndex, const Rendering::CubismRenderer::CubismTextureColor& color) +{ + SetPartMultiplyColor(partIndex, color.R, color.G, color.B, color.A); +} + +void CubismModel::SetPartColor( + csmUint32 partIndex, + csmFloat32 r, csmFloat32 g, csmFloat32 b, csmFloat32 a, + csmVector& partColors, + csmVector & drawableColors) +{ + partColors[partIndex].Color.R = r; + partColors[partIndex].Color.G = g; + partColors[partIndex].Color.B = b; + partColors[partIndex].Color.A = a; + + if (partColors[partIndex].IsOverwritten) + { + for (csmUint32 i = 0; i < _partChildDrawables[partIndex].GetSize(); i++) + { + csmUint32 drawableIndex = _partChildDrawables[partIndex][i]; + drawableColors[drawableIndex].Color.R = r; + drawableColors[drawableIndex].Color.G = g; + drawableColors[drawableIndex].Color.B = b; + drawableColors[drawableIndex].Color.A = a; + } + } +} + +void CubismModel::SetPartMultiplyColor(csmInt32 partIndex, csmFloat32 r, csmFloat32 g, csmFloat32 b, csmFloat32 a) +{ + SetPartColor(partIndex, r, g, b, a, _userPartMultiplyColors, _userMultiplyColors); +} + +void CubismModel::SetPartScreenColor(csmInt32 partIndex, const Rendering::CubismRenderer::CubismTextureColor& color) +{ + SetPartScreenColor(partIndex, color.R, color.G, color.B, color.A); +} + +void CubismModel::SetPartScreenColor(csmInt32 partIndex, csmFloat32 r, csmFloat32 g, csmFloat32 b, csmFloat32 a) +{ + SetPartColor(partIndex, r, g, b, a, _userPartScreenColors, _userScreenColors); +} + +csmBool CubismModel::GetOverwriteFlagForModelMultiplyColors() const +{ + return _isOverwrittenModelMultiplyColors; +} + +csmBool CubismModel::GetOverwriteFlagForModelScreenColors() const +{ + return _isOverwrittenModelScreenColors; +} + +void CubismModel::SetOverwriteFlagForModelMultiplyColors(csmBool value) +{ + _isOverwrittenModelMultiplyColors = value; +} + +void CubismModel::SetOverwriteFlagForModelScreenColors(csmBool value) +{ + _isOverwrittenModelScreenColors = value; +} + +csmBool CubismModel::GetOverwriteFlagForDrawableMultiplyColors(csmInt32 drawableIndex) const +{ + return _userMultiplyColors[drawableIndex].IsOverwritten; +} + +csmBool CubismModel::GetOverwriteFlagForDrawableScreenColors(csmInt32 drawableIndex) const +{ + return _userScreenColors[drawableIndex].IsOverwritten; +} + +void CubismModel::SetOverwriteFlagForDrawableMultiplyColors(csmUint32 drawableIndex, csmBool value) +{ + _userMultiplyColors[drawableIndex].IsOverwritten = value; +} + +void CubismModel::SetOverwriteFlagForDrawableScreenColors(csmUint32 drawableIndex, csmBool value) +{ + _userScreenColors[drawableIndex].IsOverwritten = value; +} + +csmBool CubismModel::GetOverwriteColorForPartMultiplyColors(csmInt32 partIndex) const +{ + return _userPartMultiplyColors[partIndex].IsOverwritten; +} + +csmBool CubismModel::GetOverwriteColorForPartScreenColors(csmInt32 partIndex) const +{ + return _userPartScreenColors[partIndex].IsOverwritten; +} + +void CubismModel::SetOverwriteColorForPartColors( + csmUint32 partIndex, + csmBool value, + csmVector& partColors, + csmVector & drawableColors) +{ + partColors[partIndex].IsOverwritten = value; + + for (csmUint32 i = 0; i < _partChildDrawables[partIndex].GetSize(); i++) + { + csmUint32 drawableIndex = _partChildDrawables[partIndex][i]; + drawableColors[drawableIndex].IsOverwritten = value; + if (value) + { + drawableColors[drawableIndex].Color.R = partColors[partIndex].Color.R; + drawableColors[drawableIndex].Color.G = partColors[partIndex].Color.G; + drawableColors[drawableIndex].Color.B = partColors[partIndex].Color.B; + drawableColors[drawableIndex].Color.A = partColors[partIndex].Color.A; + } + } +} + +void CubismModel::SetOverwriteColorForPartMultiplyColors(csmUint32 partIndex, csmBool value) +{ + _userPartMultiplyColors[partIndex].IsOverwritten = value; + SetOverwriteColorForPartColors(partIndex, value, _userPartMultiplyColors, _userMultiplyColors); +} + +void CubismModel::SetOverwriteColorForPartScreenColors(csmUint32 partIndex, csmBool value) +{ + _userPartScreenColors[partIndex].IsOverwritten = value; + SetOverwriteColorForPartColors(partIndex, value, _userPartScreenColors, _userScreenColors); +} + +csmInt32 CubismModel::GetDrawableCulling(csmInt32 drawableIndex) const +{ + if (GetOverwriteFlagForModelCullings() || GetOverwriteFlagForDrawableCullings(drawableIndex)) + { + return _userCullings[drawableIndex].IsCulling; + } + + const Core::csmFlags* constantFlags = Core::csmGetDrawableConstantFlags(_model); + return !IsBitSet(constantFlags[drawableIndex], Core::csmIsDoubleSided); +} + +void CubismModel::SetDrawableCulling(csmInt32 drawableIndex, csmInt32 isCulling) +{ + _userCullings[drawableIndex].IsCulling = isCulling; +} + +csmBool CubismModel::GetOverwriteFlagForModelCullings() const +{ + return _isOverwrittenCullings; +} + +void CubismModel::SetOverwriteFlagForModelCullings(csmBool value) +{ + _isOverwrittenCullings = value; +} + +csmBool CubismModel::GetOverwriteFlagForDrawableCullings(csmInt32 drawableIndex) const +{ + return _userCullings[drawableIndex].IsOverwritten; +} + +void CubismModel::SetOverwriteFlagForDrawableCullings(csmUint32 drawableIndex, csmBool value) +{ + _userCullings[drawableIndex].IsOverwritten = value; +} + +csmFloat32 CubismModel::GetModelOpacity() +{ + return _modelOpacity; +} + +void CubismModel::SetModelOpacity(csmFloat32 value) +{ + _modelOpacity = value; +} + +Core::csmModel* CubismModel::GetModel() const +{ + return _model; +} + +csmBool CubismModel::IsUsingMasking() const +{ + for (csmInt32 d = 0; d < Core::csmGetDrawableCount(_model); ++d) + { + if (Core::csmGetDrawableMaskCounts(_model)[d] <= 0) + { + continue; + } + return true; + } + + return false; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Model/CubismModel.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismFramework.hpp" +#include "Type/csmMap.hpp" +#include "Type/csmVector.hpp" +#include "Rendering/CubismRenderer.hpp" +#include "Id/CubismId.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +class CubismMoc; + +/** + * @brief モデル + * + * Mocデータから生成されるモデルのクラス。 + */ +class CubismModel +{ + friend class CubismMoc; +public: + + /** + * @brief テクスチャの色をRGBAで扱うための構造体 + */ + struct DrawableColorData + { + /** + * @brief コンストラクタ + */ + DrawableColorData() + : IsOverwritten(false) + , Color() {}; + + /** + * @brief コンストラクタ + */ + DrawableColorData(csmBool isOverwritten, Rendering::CubismRenderer::CubismTextureColor color) + : IsOverwritten(isOverwritten) + , Color(color) {}; + + /** + * @brief デストラクタ + */ + virtual ~DrawableColorData() {}; + + csmBool IsOverwritten; + Rendering::CubismRenderer::CubismTextureColor Color; + + }; // DrawableColorData + + /** + * @brief テクスチャのカリング設定を管理するための構造体 + */ + struct DrawableCullingData + { + /** + * @brief コンストラクタ + */ + DrawableCullingData() + : IsOverwritten(false) + , IsCulling(0) {}; + + /** + * @brief コンストラクタ + */ + DrawableCullingData(csmBool isOverwritten, csmInt32 isCulling) + : IsOverwritten(isOverwritten) + , IsCulling(isCulling) {}; + + /** + * @brief デストラクタ + */ + virtual ~DrawableCullingData() {}; + + csmBool IsOverwritten; + csmInt32 IsCulling; + + }; // DrawableCullingData + + /** + * @brief テクスチャの色をRGBAで扱うための構造体 + */ + struct PartColorData + { + /** + * @brief コンストラクタ + */ + PartColorData() + : IsOverwritten(false) + , Color() {}; + + /** + * @brief コンストラクタ + */ + PartColorData(csmBool isOverwritten, Rendering::CubismRenderer::CubismTextureColor color) + : IsOverwritten(isOverwritten) + , Color(color) {}; + + /** + * @brief デストラクタ + */ + virtual ~PartColorData() {}; + + csmBool IsOverwritten; + Rendering::CubismRenderer::CubismTextureColor Color; + + }; // PartColorData + + /** + * @brief モデルのパラメータの更新 + * + * モデルのパラメータを更新する。 + */ + void Update() const; + + /** + * @brief Pixel単位でキャンバスの幅の取得 + * + * キャンバスの幅を取得する。 + * + * @return キャンバスの幅(pixel) + */ + csmFloat32 GetCanvasWidthPixel() const; + + /** + * @brief Pixel単位でキャンバスの高さの取得 + * + * キャンバスの高さを取得する。 + * + * @return キャンバスの高さ(pixel) + */ + csmFloat32 GetCanvasHeightPixel() const; + + /** + * @brief PixelsPerUnitの取得 + * + * PixelsPerUnitを取得する。 + * + * @return PixelsPerUnit + */ + csmFloat32 GetPixelsPerUnit() const; + + /** + * @brief Unit単位でキャンバスの幅の取得 + * + * キャンバスの幅を取得する。 + * + * @return キャンバスの幅(Unit) + */ + csmFloat32 GetCanvasWidth() const; + + /** + * @brief Unit単位でキャンバスの高さの取得 + * + * キャンバスの高さを取得する。 + * + * @return キャンバスの高さ(Unit) + */ + csmFloat32 GetCanvasHeight() const; + + /** + * @brief パーツのインデックスの取得 + * + * パーツのインデックスを取得する。 + * + * @param[in] partId パーツのID + * @return パーツのインデックス + */ + csmInt32 GetPartIndex(CubismIdHandle partId); + + /** + * @brief パーツのIDの取得 + * + * パーツのIDを取得する。 + * + * @param[in] partIndex パーツのIndex + * @return パーツのID + */ + CubismIdHandle GetPartId(csmUint32 partIndex); + + /** + * @brief パーツの個数の取得 + * + * パーツの個数を取得する。 + * + * @return パーツの個数 + */ + csmInt32 GetPartCount() const; + + /** + * @brief パーツの不透明度の設定 + * + * パーツの不透明度を設定する。 + * + * @param[in] partId パーツのID + * @param[in] opacity 不透明度 + */ + void SetPartOpacity(CubismIdHandle partId, csmFloat32 opacity); + + /** + * @brief パーツの不透明度の設定 + * + * パーツの不透明度を設定する。 + * + * @param[in] partIndex パーツのインデックス + * @param[in] opacity パーツの不透明度 + */ + void SetPartOpacity(csmInt32 partIndex, csmFloat32 opacity); + + /** + * @brief パーツの不透明度の取得 + * + * パーツの不透明度を取得する。 + * + * @param[in] partId パーツのID + * @return パーツの不透明度 + */ + csmFloat32 GetPartOpacity(CubismIdHandle partId); + + /** + * @brief パーツの不透明度の取得 + * + * パーツの不透明度を取得する。 + * + * @param[in] partIndex パーツのインデックス + * @return パーツの不透明度 + */ + csmFloat32 GetPartOpacity(csmInt32 partIndex); + + /** + * @brief パラメータのインデックスの取得 + * + * パラメータのインデックスを取得する。 + * + * @param[in] parameterId パラメータID + * @return パラメータのインデックス + */ + csmInt32 GetParameterIndex(CubismIdHandle parameterId); + + /** + * @brief パラメータのIDの取得 + * + * パラメータのIDを取得する。 + * + * @param[in] parameterIndex パラメータのIndex + * @return パラメータのID + */ + CubismIdHandle GetParameterId(csmUint32 parameterIndex); + + /** + * @brief パラメータの個数の取得 + * + * パラメータの個数を取得する。 + * + * @return パラメータの個数 + */ + csmInt32 GetParameterCount() const; + + /** + * @brief パラメータの種類の取得 + * + * パラメータの種類を取得する。 + * + * @return Core::csmParameterType_Normal -> 通常のパラメータ + * Core::csmParameterType_BlendShape -> ブレンドシェイプパラメータ + */ + Core::csmParameterType GetParameterType(csmUint32 parameterIndex) const; + + /** + * @brief パラメータの最大値の取得 + * + * パラメータの最大値を取得する。 + * + * @param[in] parameterIndex パラメータのインデックス + * @return パラメータの最大値 + */ + csmFloat32 GetParameterMaximumValue(csmUint32 parameterIndex) const; + + /** + * @brief パラメータの最小値の取得 + * + * パラメータの最小値を取得する。 + * + * @param[in] parameterIndex パラメータのインデックス + * @return パラメータの最小値 + */ + csmFloat32 GetParameterMinimumValue(csmUint32 parameterIndex) const; + + /** + * @brief パラメータのデフォルト値の取得 + * + * パラメータのデフォルト値を取得する。 + * + * @param[in] parameterIndex パラメータのインデックス + * @return パラメータのデフォルト値 + */ + csmFloat32 GetParameterDefaultValue(csmUint32 parameterIndex) const; + + /** + * @brief パラメータの値の取得 + * + * パラメータの値を取得する。 + * + * @param[in] parameterId パラメータID + * @return パラメータの値 + */ + csmFloat32 GetParameterValue(CubismIdHandle parameterId); + + /** + * @brief パラメータの値の取得 + * + * パラメータの値を取得する。 + * + * @param[in] parameterIndex パラメータのインデックス + * @return パラメータの値 + */ + csmFloat32 GetParameterValue(csmInt32 parameterIndex); + + /** + * @brief パラメータの値の設定 + * + * パラメータの値を設定する。 + * + * @param[in] parameterId パラメータID + * @param[in] value パラメータの値 + * @param[in] weight 重み + */ + void SetParameterValue(CubismIdHandle parameterId, csmFloat32 value, csmFloat32 weight = 1.0f); + + /** + * @brief パラメータの値の設定 + * + * パラメータの値を設定する。 + * + * @param[in] parameterIndex パラメータのインデックス + * @param[in] value パラメータの値 + * @param[in] weight 重み + */ + void SetParameterValue(csmInt32 parameterIndex, csmFloat32 value, csmFloat32 weight = 1.0f); + + /** + * @brief パラメータの値の加算 + * + * パラメータの値を加算する。 + * + * @param[in] parameterId パラメータID + * @param[in] value 加算する値 + * @param[in] weight 重み + */ + void AddParameterValue(CubismIdHandle parameterId, csmFloat32 value, csmFloat32 weight = 1.0f); + + /** + * @brief パラメータの値の加算 + * + * パラメータの値を加算する。 + * + * @param[in] parameterIndex パラメータのインデックス + * @param[in] value 加算する値 + * @param[in] weight 重み + */ + void AddParameterValue(csmInt32 parameterIndex, csmFloat32 value, csmFloat32 weight = 1.0f); + + /** + * @brief パラメータの値の乗算 + * + * パラメータの値を乗算する。 + * + * @param[in] parameterId パラメータID + * @param[in] value 乗算する値 + * @param[in] weight 重み + */ + void MultiplyParameterValue(CubismIdHandle parameterId, csmFloat32 value, csmFloat32 weight = 1.0f); + + /** + * @brief パラメータの値の乗算 + * + * パラメータの値を乗算する。 + * + * @param[in] parameterIndex パラメータのインデックス + * @param[in] value 乗算する値 + * @param[in] weight 重み + */ + void MultiplyParameterValue(csmInt32 parameterIndex, csmFloat32 value, csmFloat32 weight = 1.0f); + + /** + * @brief Drawableのインデックスの取得 + * + * Drawableのインデックスを取得する。 + * + * @param[in] drawableId DrawableのID + * @return Drawableのインデックス + */ + csmInt32 GetDrawableIndex(CubismIdHandle drawableId) const; + + /** + * @brief Drawableの個数の取得 + * + * Drawableの個数を取得する。 + * + * @return Drawableの個数 + */ + csmInt32 GetDrawableCount() const; + + /** + * @brief DrawableのIDの取得 + * + * DrawableのIDを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return DrawableのID + */ + CubismIdHandle GetDrawableId(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの描画順リストの取得 + * + * Drawableの描画順リストを取得する。 + * + * @return Drawableの描画順リスト + */ + const csmInt32* GetDrawableRenderOrders() const; + + /** + * @deprecated + * 関数名が誤っていたため、代替となる getDrawableTextureIndex を追加し、この関数は非推奨となりました。 + * + * @brief Drawableのテクスチャインデックスリストの取得 + * + * Drawableのテクスチャインデックスリストを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableのテクスチャインデックスリスト + */ + csmInt32 GetDrawableTextureIndices(csmInt32 drawableIndex) const; + + /** + * @brief Drawableのテクスチャインデックスリストの取得 + * + * Drawableのテクスチャインデックスを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableのテクスチャインデックス + */ + csmInt32 GetDrawableTextureIndex(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの頂点インデックスの個数の取得 + * + * Drawableの頂点インデックスの個数を取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableの頂点インデックスの個数 + */ + csmInt32 GetDrawableVertexIndexCount(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの頂点の個数の取得 + * + * Drawableの頂点の個数を取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableの頂点の個数 + */ + csmInt32 GetDrawableVertexCount(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの頂点リストの取得 + * + * Drawableの頂点リストを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableの頂点リスト + */ + const csmFloat32* GetDrawableVertices(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの頂点インデックスリストの取得 + * + * Drawableの頂点インデックスリストを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableの頂点インデックスリスト + */ + const csmUint16* GetDrawableVertexIndices(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの頂点リストの取得 + * + * Drawableの頂点リストを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableの頂点リスト + */ + const Core::csmVector2* GetDrawableVertexPositions(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの頂点のUVリストの取得 + * + * Drawableの頂点のUVリストを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableの頂点のUVリスト + */ + const Core::csmVector2* GetDrawableVertexUvs(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの不透明度の取得 + * + * Drawableの不透明度を取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableの不透明度 + */ + csmFloat32 GetDrawableOpacity(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの乗算色の取得 + * + * Drawableの乗算色を取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableの乗算色 + */ + Core::csmVector4 GetDrawableMultiplyColor(csmInt32 drawableIndex) const; + + /** + * @brief Drawableのスクリーン色の取得 + * + * Drawableのスクリーン色を取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableのスクリーン色 + */ + Core::csmVector4 GetDrawableScreenColor(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの親パーツのインデックスの取得 + * + * Drawableの親パーツのインデックスを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return drawableの親パーツのインデックス + */ + csmInt32 GetDrawableParentPartIndex(csmUint32 drawableIndex) const; + + /** + * @brief Drawableのブレンドモードの取得 + * + * Drawableのブレンドモードを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableのブレンドモード + */ + Rendering::CubismRenderer::CubismBlendMode GetDrawableBlendMode(csmInt32 drawableIndex) const; + + /** + * @brief Drawableのマスクの反転使用の取得 + * + * Drawableのマスク使用時の反転設定を取得する。 + * マスクを使用しない場合は無視される + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableのマスクの反転設定 + */ + csmBool GetDrawableInvertedMask(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの表示情報の取得 + * + * Drawableの表示情報を取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return true Drawableが表示 + * @retval false Drawableが非表示 + */ + csmBool GetDrawableDynamicFlagIsVisible(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの表示状態の変化の取得 + * + * 直近のCubismModel::Update関数でDrawableの表示状態が変化したかを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @retval true Drawableの表示状態が直近のCubismModel::Update関数で変化した + * @retval false Drawableの表示状態が直近のCubismModel::Update関数で変化していない + */ + csmBool GetDrawableDynamicFlagVisibilityDidChange(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの不透明度の変化情報の取得 + * + * 直近のCubismModel::Update関数でDrawableの不透明度が変化したかを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @retval true Drawableの不透明度が直近のCubismModel::Update関数で変化した + * @retval false Drawableの不透明度が直近のCubismModel::Update関数で変化していない + */ + csmBool GetDrawableDynamicFlagOpacityDidChange(csmInt32 drawableIndex) const; + + /** + * @brief DrawableのDrawOrderの変化情報の取得 + * + * 直近のCubismModel::Update関数でDrawableのDrawOrderが変化したかを取得する。 + * DrawOrderはArtMesh上で指定する0から1000の情報 + * + * @param[in] drawableIndex Drawableのインデックス + * @retval true Drawableの不透明度が直近のCubismModel::Update関数で変化した + * @retval false Drawableの不透明度が直近のCubismModel::Update関数で変化していない + */ + csmBool GetDrawableDynamicFlagDrawOrderDidChange(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの描画順序の変化情報の取得 + * + * 直近のCubismModel::Update関数でDrawableの描画の順序が変化したかを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @retval true Drawableの描画の順序が直近のCubismModel::Update関数で変化した + * @retval false Drawableの描画の順序が直近のCubismModel::Update関数で変化していない + */ + csmBool GetDrawableDynamicFlagRenderOrderDidChange(csmInt32 drawableIndex) const; + + /** + * @brief DrawableのVertexPositionsの変化情報の取得 + * + * 直近のCubismModel::Update関数でDrawableの頂点情報が変化したかを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @retval true Drawableの頂点情報が直近のCubismModel::Update関数で変化した + * @retval false Drawableの頂点情報が直近のCubismModel::Update関数で変化していない + */ + csmBool GetDrawableDynamicFlagVertexPositionsDidChange(csmInt32 drawableIndex) const; + + /** + * @brief Drawableの乗算色・スクリーン色の変化情報の取得 + * + * 直近のCubismModel::Update関数でDrawableの乗算色・スクリーン色が変化したかを取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @retval true Drawableの乗算色・スクリーン色が直近のCubismModel::Update関数で変化した + * @retval false Drawableの乗算色・スクリーン色が直近のCubismModel::Update関数で変化していない + */ + csmBool GetDrawableDynamicFlagBlendColorDidChange(csmInt32 drawableIndex) const; + + /** + * @brief Drawableのクリッピングマスクリストの取得 + * + * Drawableのクリッピングマスクリストを取得する。 + * + * @return Drawableのクリッピングマスクリスト + */ + const csmInt32** GetDrawableMasks() const; + + /** + * @brief Drawableのクリッピングマスクの個数リストの取得 + * + * Drawableのクリッピングマスクの個数リストを取得する。 + * + * @return Drawableのクリッピングマスクの個数リスト + */ + const csmInt32* GetDrawableMaskCounts() const; + + /** + * @brief クリッピングマスクの使用状態 + * + * クリッピングマスクを使用しているかどうか? + * + * @retval true クリッピングマスクを使用している + * @retval false クリッピングマスクを使用していない + */ + csmBool IsUsingMasking() const; + + /** + * @brief 保存されたパラメータの読み込み + * + * 保存されたパラメータを読み込む + */ + void LoadParameters(); + + /** + * @brief パラメータの保存 + * + * パラメータを保存する。 + */ + void SaveParameters(); + + /** + * @brief drawableの乗算色を取得する + */ + Rendering::CubismRenderer::CubismTextureColor GetMultiplyColor(csmInt32 drawableIndex) const; + + /** + * @brief drawableのスクリーン色を取得する + */ + Rendering::CubismRenderer::CubismTextureColor GetScreenColor(csmInt32 drawableIndex) const; + + /** + * @brief drawableの乗算色を設定する + */ + void SetMultiplyColor(csmInt32 drawableIndex, const Rendering::CubismRenderer::CubismTextureColor& color); + + /** + * @brief drawableの乗算色を設定する + */ + void SetMultiplyColor(csmInt32 drawableIndex, csmFloat32 r, csmFloat32 g, csmFloat32 b, csmFloat32 a = 1.0f); + + /** + * @brief drawableのスクリーン色を設定する + */ + void SetScreenColor(csmInt32 drawableIndex, const Rendering::CubismRenderer::CubismTextureColor& color); + + /** + * @brief drawableのスクリーン色を設定する + */ + void SetScreenColor(csmInt32 drawableIndex, csmFloat32 r, csmFloat32 g, csmFloat32 b, csmFloat32 a = 1.0f); + + /** + * @brief partの乗算色を取得する + */ + Rendering::CubismRenderer::CubismTextureColor GetPartMultiplyColor(csmInt32 partIndex) const; + + /** + * @brief partの乗算色を取得する + */ + Rendering::CubismRenderer::CubismTextureColor GetPartScreenColor(csmInt32 partIndex) const; + + /** + * @brief partのスクリーン色を設定する + */ + void SetPartMultiplyColor(csmInt32 partIndex, const Rendering::CubismRenderer::CubismTextureColor& color); + + /** + * @brief partの乗算色を設定する + */ + void SetPartMultiplyColor(csmInt32 partIndex, csmFloat32 r, csmFloat32 g, csmFloat32 b, csmFloat32 a = 1.0f); + + /** + * @brief partのスクリーン色を設定する + */ + void SetPartScreenColor(csmInt32 partIndex, const Rendering::CubismRenderer::CubismTextureColor& color); + + /** + * @brief partのスクリーン色を設定する + */ + void SetPartScreenColor(csmInt32 partIndex, csmFloat32 r, csmFloat32 g, csmFloat32 b, csmFloat32 a = 1.0f); + + /** + * @brief SDKからモデル全体の乗算色を上書きするか。 + * + * @retval true -> SDK上の色情報を使用 + * @retval false -> モデルの色情報を使用 + */ + csmBool GetOverwriteFlagForModelMultiplyColors() const; + + /** + * @brief SDKからモデル全体のスクリーン色を上書きするか。 + * + * @retval true -> SDK上の色情報を使用 + * @retval false -> モデルの色情報を使用 + */ + csmBool GetOverwriteFlagForModelScreenColors() const; + + /** + * @brief SDKからモデル全体の乗算色を上書きするかをセットする + * SDK上の色情報を使うならtrue、モデルの色情報を使うならfalse + */ + void SetOverwriteFlagForModelMultiplyColors(csmBool value); + + /** + * @brief SDKからモデル全体のスクリーン色を上書きするかをセットする + * SDK上の色情報を使うならtrue、モデルの色情報を使うならfalse + */ + void SetOverwriteFlagForModelScreenColors(csmBool value); + + /** + * @brief SDKからdrawableの乗算色を上書きするか。 + * + * @retval true -> SDK上の色情報を使用 + * @retval false -> モデルの色情報を使用 + */ + csmBool GetOverwriteFlagForDrawableMultiplyColors(csmInt32 drawableIndex) const; + + /** + * @brief SDKからdrawableのスクリーン色を上書きするか。 + * + * @retval true -> SDK上の色情報を使用 + * @retval false -> モデルの色情報を使用 + */ + csmBool GetOverwriteFlagForDrawableScreenColors(csmInt32 drawableIndex) const; + + /** + * @brief SDKからdrawableの乗算色を上書きするかをセットする + * SDK上の色情報を使うならtrue、モデルの色情報を使うならfalse + */ + void SetOverwriteFlagForDrawableMultiplyColors(csmUint32 drawableIndex, csmBool value); + + /** + * @brief SDKからdrawableのスクリーン色を上書きするかをセットする + * SDK上の色情報を使うならtrue、モデルの色情報を使うならfalse + */ + void SetOverwriteFlagForDrawableScreenColors(csmUint32 drawableIndex, csmBool value); + + /** + * @brief SDKからpartの乗算色を上書きするか。 + * + * @retval true -> SDK上の色情報を使用 + * @retval false -> モデルの色情報を使用 + */ + csmBool GetOverwriteColorForPartMultiplyColors(csmInt32 partIndex) const; + + /** + * @brief SDKからpartのスクリーン色を上書きするか。 + * + * @retval true -> SDK上の色情報を使用 + * @retval false -> モデルの色情報を使用 + */ + csmBool GetOverwriteColorForPartScreenColors(csmInt32 partIndex) const; + + /** + * @brief SDKからpartの乗算色を上書きするかをセットする + * SDK上の色情報を使うならtrue、モデルの色情報を使うならfalse + */ + void SetOverwriteColorForPartMultiplyColors(csmUint32 partIndex, csmBool value); + + /** + * @brief SDKからpartのスクリーン色を上書きするかをセットする + * SDK上の色情報を使うならtrue、モデルの色情報を使うならfalse + */ + void SetOverwriteColorForPartScreenColors(csmUint32 partIndex, csmBool value); + + /** + * @brief Drawableのカリング情報の取得 + * + * Drawableのカリング情報を取得する。 + * + * @param[in] drawableIndex Drawableのインデックス + * @return Drawableのカリング情報 + */ + csmInt32 GetDrawableCulling(csmInt32 drawableIndex) const; + + /** + * @brief Drawableのカリング情報を設定する + */ + void SetDrawableCulling(csmInt32 drawableIndex, csmInt32 isCulling); + + /** + * @brief SDKからモデル全体のカリング設定を上書きするか。 + * + * @retval true -> SDK上のカリング設定を使用 + * @retval false -> モデルのカリング設定を使用 + */ + csmBool GetOverwriteFlagForModelCullings() const; + + /** + * @brief SDKからモデル全体のカリング設定を上書きするかをセットする + * SDK上のカリング設定を使うならtrue、モデルのカリング設定を使うならfalse + */ + void SetOverwriteFlagForModelCullings(csmBool value); + + /** + * @brief SDKからdrawableのカリング設定を上書きするか。 + * + * @retval true -> SDK上のカリング設定を使用 + * @retval false -> モデルのカリング設定を使用 + */ + csmBool GetOverwriteFlagForDrawableCullings(csmInt32 drawableIndex) const; + + /** + * @brief SDKからdrawableのカリング設定を上書きするかをセットする + * SDK上のカリング設定を使うならtrue、モデルのカリング設定を使うならfalse + */ + void SetOverwriteFlagForDrawableCullings(csmUint32 drawableIndex, csmBool value); + + /** + * @brief モデルの不透明度を取得する + * + * @return 不透明度の値 + */ + csmFloat32 GetModelOpacity(); + + /** + * @brief モデルの不透明度を設定する + * + * @param[in] value 不透明度の値 + */ + void SetModelOpacity(csmFloat32 value); + + Core::csmModel* GetModel() const; + +private: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + * + * @param[in] model csmModelへのポインタ + */ + CubismModel(Core::csmModel* model); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismModel(); + + //Prevention of copy Constructor + CubismModel(const CubismModel&); + CubismModel& operator=(const CubismModel&); + + /** + * @brief 初期化 + * + * 初期化する。 + */ + void Initialize(); + + /** + * @brief partのOverwriteColor Set関数 + */ + void SetPartColor( + csmUint32 partIndex, + csmFloat32 r, csmFloat32 g, csmFloat32 b, csmFloat32 a, + csmVector& partColors, + csmVector & drawableColors); + + /** + * @brief partのOverwriteFlag Set関数 + */ + void SetOverwriteColorForPartColors( + csmUint32 partIndex, + csmBool value, + csmVector& partColors, + csmVector & drawableColors); + + csmMap _notExistPartOpacities; ///< 存在していないパーツの不透明度のリスト + csmMap _notExistPartId; ///< 存在していないパーツIDのリスト + + csmMap _notExistParameterValues; ///< 存在していないパラメータの値のリスト + csmMap _notExistParameterId; ///< 存在していないパラメータIDのリスト + + csmVector _savedParameters; ///< 保存されたパラメータ + + Core::csmModel* _model; ///< モデル + + csmFloat32* _parameterValues; ///< パラメータの値のリスト + const csmFloat32* _parameterMaximumValues; ///< パラメータの最大値のリスト + const csmFloat32* _parameterMinimumValues; ///< パラメータの最小値のリスト + + csmFloat32* _partOpacities; ///< パーツの不透明度のリスト + + csmFloat32 _modelOpacity; ///< モデルの不透明度 + + csmVector _parameterIds; + csmVector _partIds; + csmVector _drawableIds; + csmVector _userScreenColors; ///< Drawable 乗算色の配列 + csmVector _userMultiplyColors; ///< Drawable スクリーン色の配列 + csmVector _userCullings; ///< カリング設定の配列 + csmVector _userPartScreenColors; ///< Part 乗算色の配列 + csmVector _userPartMultiplyColors; ///< Part スクリーン色の配列 + csmVector > _partChildDrawables; ///< Partの子DrawableIndexの配列 + csmBool _isOverwrittenModelMultiplyColors; ///< 乗算色を全て上書きするか? + csmBool _isOverwrittenModelScreenColors; ///< スクリーン色を全て上書きするか? + csmBool _isOverwrittenCullings; ///< モデルのカリング設定をすべて上書きするか? +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Model/CubismModelUserData.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismModelUserData.hpp" +#include "CubismModelUserDataJson.hpp" +#include "Utils/CubismString.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +namespace +{ +const Live2D::Cubism::Framework::csmChar* ArtMesh = "ArtMesh"; +} + +CubismModelUserData::~CubismModelUserData() +{ + for (csmUint32 i = 0;i < _userDataNodes.GetSize(); ++i) + { + CSM_DELETE(const_cast(_userDataNodes[i])); + } +} + +const csmVector& CubismModelUserData::GetArtMeshUserDatas() const +{ + return _artMeshUserDataNodes; +} + +CubismModelUserData* CubismModelUserData::Create(const csmByte* buffer, const csmSizeInt size) +{ + CubismModelUserData* ret = CSM_NEW CubismModelUserData(); + + ret->ParseUserData(buffer, size); + + return ret; +} + +void CubismModelUserData::Delete(CubismModelUserData* modelUserData) +{ + CSM_DELETE_SELF(CubismModelUserData, modelUserData); +} + +void CubismModelUserData::ParseUserData(const csmByte* buffer, const csmSizeInt size) +{ + CubismModelUserDataJson* json = CSM_NEW CubismModelUserDataJson(buffer, size); + + if (!json->IsValid()) + { + CSM_DELETE(json); + return; + } + + const ModelUserDataType typeOfArtMesh = CubismFramework::GetIdManager()->GetId(ArtMesh); + + const csmUint32 nodeCount = json->GetUserDataCount(); + + for (csmUint32 i = 0; i < nodeCount; i++) + { + CubismModelUserDataNode* addNode = CSM_NEW CubismModelUserDataNode(); + addNode->TargetId = json->GetUserDataId(i); + addNode->TargetType = CubismFramework::GetIdManager()->GetId(json->GetUserDataTargetType(i)); + addNode->Value = json->GetUserDataValue(i); + _userDataNodes.PushBack(addNode); + + if (addNode->TargetType == typeOfArtMesh) + { + _artMeshUserDataNodes.PushBack(addNode); + } + } + + CSM_DELETE(json); +} +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Model/CubismModelUserData.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismModel.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/// ユーザデータの種類を指定するタイプ宣言 +typedef CubismIdHandle ModelUserDataType; + +/** +* @brief ユーザデータの管理クラス +* +* ユーザデータをロード、管理、検索インターフェイス、解放までを行う。 +*/ +class CubismModelUserData +{ +public: + /** + * @brief ユーザデータ構造体 + * + * Jsonから読み込んだユーザデータを記録しておくための構造体 + */ + struct CubismModelUserDataNode + { + ModelUserDataType TargetType; ///< ユーザデータターゲットタイプ + CubismIdHandle TargetId; ///< ユーザデータターゲットのID + csmString Value; ///< ユーザデータ + }; + + /** + * @brief インスタンスの作成 + * + * インスタンスを作成する。 + * + * @param[in] buffer userdata3.jsonが読み込まれいるバッファ + * @param[in] size バッファのサイズ + * @return 作成されたインスタンス + */ + static CubismModelUserData* Create(const csmByte* buffer, csmSizeInt size); + + /** + * @brief インスタンスの破棄 + * + * インスタンスを破棄する。 + * + * @param[in] modelUserData 破棄するインスタンス + */ + static void Delete(CubismModelUserData* modelUserData); + + /** + * @brief デストラクタ + * + * ユーザーデータ構造体配列を解放する + */ + virtual ~CubismModelUserData(); + + /** + * @brief ArtMeshのユーザデータのリストの取得 + * + * ArtMeshのユーザデータのリストの取得する。 + * + * @return csmVectorのユーザデータリスト + */ + const csmVector& GetArtMeshUserDatas() const; + +private: + + /** + * @brief userdata3.jsonのパース + * + * userdata3.jsonをパースする。 + * + * @param[in] buffer userdata3.jsonが読み込まれいるバッファ + * @param[in] size バッファのサイズ + */ + void ParseUserData(const csmByte* buffer, csmSizeInt size); + + csmVector _userDataNodes; ///< ユーザデータ構造体配列 + csmVector _artMeshUserDataNodes; ///< 閲覧リスト保持 +}; +}}} //--------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Model/CubismModelUserDataJson.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismModelUserDataJson.hpp" +#include "CubismModelUserData.hpp" + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + +namespace { +const csmChar* Meta = "Meta"; +const csmChar* UserDataCount = "UserDataCount"; +const csmChar* TotalUserDataSize = "TotalUserDataSize"; +const csmChar* UserData = "UserData"; +const csmChar* Target = "Target"; +const csmChar* Id = "Id"; +const csmChar* Value = "Value"; +} +CubismModelUserDataJson::CubismModelUserDataJson(const csmByte* buffer, csmSizeInt size) +{ + CreateCubismJson(buffer, size); +} + +CubismModelUserDataJson::~CubismModelUserDataJson() +{ + DeleteCubismJson(); +} + +csmInt32 CubismModelUserDataJson::GetUserDataCount() const +{ + return _json->GetRoot()[Meta][UserDataCount].ToInt(); +} + +csmInt32 CubismModelUserDataJson::GetTotalUserDataSize() const +{ + return _json->GetRoot()[Meta][TotalUserDataSize].ToInt(); +} + +csmString CubismModelUserDataJson::GetUserDataTargetType(const csmInt32 i) const +{ + return _json->GetRoot()[UserData][i][Target].GetRawString(); +} + +CubismIdHandle CubismModelUserDataJson::GetUserDataId(const csmInt32 i) const +{ + return CubismFramework::GetIdManager()->GetId(_json->GetRoot()[UserData][i][Id].GetRawString()); +} + +const csmChar* CubismModelUserDataJson::GetUserDataValue(const csmInt32 i) const +{ + return _json->GetRoot()[UserData][i][Value].GetRawString(); +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Model/CubismModelUserDataJson.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismJsonHolder.hpp" +#include "Utils/CubismJson.hpp" +#include "Model/CubismModel.hpp" +#include "Id/CubismIdManager.hpp" + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + +class CubismModelUserDataJson : public CubismJsonHolder +{ +public: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + * + * @param[in] buffer userdata3.jsonが読み込まれているバッファ + * @param[in] size バッファのサイズ + */ + CubismModelUserDataJson(const csmByte* buffer, csmSizeInt size); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismModelUserDataJson(); + + /** + * @brief ユーザデータ個数の取得 + * + * userdata3.jsonに入っていたユーザデータの個数を取得する。 + * + * @return ユーザデータの個数 + */ + csmInt32 GetUserDataCount() const; + + /** + * @brief ユーザデータ総文字列数の取得 + * + * ユーザデータ総文字列数の取得する。 + * + * @return ユーザデータ総文字列数 + */ + csmInt32 GetTotalUserDataSize() const; + + /** + * @brief ユーザデータのタイプの取得 + * + * 指定番目のユーザデータのタイプの取得を取得する。 + * + * @param[in] i インデックス + * + * @return ユーザデータのタイプ + */ + csmString GetUserDataTargetType(csmInt32 i) const; + + /** + * @brief ユーザデータのターゲットIDの取得 + * + * 指定番目のユーザデータのターゲットIDの取得を取得する。 + * + * @param[in] i インデックス + * + * @return ユーザデータターゲットID + */ + CubismIdHandle GetUserDataId(csmInt32 i) const; + + /** + * @brief ユーザデータの文字列の取得 + * + * 指定番目のユーザデータの文字列の取得を取得する。 + * + * @param[in] i インデックス + * + * @return ユーザデータ + */ + const csmChar* GetUserDataValue(csmInt32 i) const; +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Model/CubismUserModel.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismUserModel.hpp" +#include "Motion/CubismMotion.hpp" +#include "Physics/CubismPhysics.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +CubismUserModel::CubismUserModel() + : _moc(NULL) + , _model(NULL) + , _motionManager(NULL) + , _expressionManager(NULL) + , _eyeBlink(NULL) + , _breath(NULL) + , _modelMatrix(NULL) + , _pose(NULL) + , _dragManager(NULL) + , _physics(NULL) + , _modelUserData(NULL) + , _initialized(false) + , _updating(false) + , _opacity(1.0f) + , _lipSync(true) + , _lastLipSyncValue(0.0f) + , _dragX(0.0f) + , _dragY(0.0f) + , _accelerationX(0.0f) + , _accelerationY(0.0f) + , _accelerationZ(0.0f) + , _mocConsistency(false) + , _debugMode(false) + , _renderer(NULL) +{ + // モーションマネージャーを作成 + // MotionQueueManagerクラスからの継承なので使い方は同じ + _motionManager = CSM_NEW CubismMotionManager(); + _motionManager->SetEventCallback(CubismDefaultMotionEventCallback, this); + + // 表情モーションマネージャを作成 + _expressionManager = CSM_NEW CubismExpressionMotionManager(); + + // ドラッグによるアニメーション + _dragManager = CSM_NEW CubismTargetPoint(); + +} + +CubismUserModel::~CubismUserModel() +{ + CSM_DELETE(_motionManager); + CSM_DELETE(_expressionManager); + if (_moc) + { + _moc->DeleteModel(_model); + } + CubismMoc::Delete(_moc); + CSM_DELETE(_modelMatrix); + CubismPose::Delete(_pose); + CubismEyeBlink::Delete(_eyeBlink); + CubismBreath::Delete(_breath); + CSM_DELETE(_dragManager); + CubismPhysics::Delete(_physics); + CubismModelUserData::Delete(_modelUserData); + + DeleteRenderer(); +} + +void CubismUserModel::SetAcceleration(csmFloat32 x, csmFloat32 y, csmFloat32 z) +{ + _accelerationX = x; + _accelerationY = y; + _accelerationZ = z; +} + +void CubismUserModel::LoadModel(const csmByte* buffer, csmSizeInt size, csmBool shouldCheckMocConsistency) +{ + _moc = CubismMoc::Create(buffer, size, shouldCheckMocConsistency); + + if (_moc == NULL) + { + CubismLogError("Failed to CubismMoc::Create()."); + return; + } + + _model = _moc->CreateModel(); + + if (_model == NULL) + { + CubismLogError("Failed to CreateModel()."); + return; + } + + _model->SaveParameters(); + _modelMatrix = CSM_NEW CubismModelMatrix(_model->GetCanvasWidth(), _model->GetCanvasHeight()); + +} + +ACubismMotion* CubismUserModel::LoadExpression(const csmByte* buffer, csmSizeInt size, const csmChar* name) +{ + if (!buffer) + { + CubismLogError("Failed to LoadExpression()."); + return NULL; + } + + return CubismExpressionMotion::Create(buffer, size); +} + +void CubismUserModel::LoadPose(const csmByte* buffer, csmSizeInt size) +{ + _pose = CubismPose::Create(buffer, size); + if (!_pose) + { + CubismLogError("Failed to LoadPose()."); + } +} + +void CubismUserModel::LoadPhysics(const csmByte* buffer, csmSizeInt size) +{ + _physics = CubismPhysics::Create(buffer, size); + if (!_physics) + { + CubismLogError("Failed to LoadPhysics()."); + } +} + +void CubismUserModel::LoadUserData(const csmByte* buffer, csmSizeInt size) +{ + if (!buffer) + { + CubismLogError("Failed to LoadUserData()."); + return; + } + + _modelUserData = CubismModelUserData::Create(buffer, size); +} +csmBool CubismUserModel::IsHit(CubismIdHandle drawableId, csmFloat32 pointX, csmFloat32 pointY) +{ + const csmInt32 drawIndex = _model->GetDrawableIndex(drawableId); + + if (drawIndex < 0) + { + return false; // 存在しない場合はfalse + } + + const csmInt32 count = _model->GetDrawableVertexCount(drawIndex); + const csmFloat32* vertices = _model->GetDrawableVertices(drawIndex); + + csmFloat32 left = vertices[0]; + csmFloat32 right = vertices[0]; + csmFloat32 top = vertices[1]; + csmFloat32 bottom = vertices[1]; + + for (csmInt32 j = 1; j < count; ++j) + { + csmFloat32 x = vertices[Constant::VertexOffset + j * Constant::VertexStep]; + csmFloat32 y = vertices[Constant::VertexOffset + j * Constant::VertexStep + 1]; + + if (x < left) + { + left = x; // Min x + } + + if (x > right) + { + right = x; // Max x + } + + if (y < top) + { + top = y; // Min y + } + + if (y > bottom) + { + bottom = y; // Max y + } + } + + const csmFloat32 tx = _modelMatrix->InvertTransformX(pointX); + const csmFloat32 ty = _modelMatrix->InvertTransformY(pointY); + + return ((left <= tx) && (tx <= right) && (top <= ty) && (ty <= bottom)); +} + +ACubismMotion* CubismUserModel::LoadMotion(const csmByte* buffer, csmSizeInt size, const csmChar* name, ACubismMotion::FinishedMotionCallback onFinishedMotionHandler) +{ + if (!buffer) + { + CubismLogError("Failed to LoadMotion()."); + return NULL; + } + return CubismMotion::Create(buffer, size, onFinishedMotionHandler); +} + +void CubismUserModel::SetDragging(csmFloat32 x, csmFloat32 y) +{ + _dragManager->Set(x, y); +} + +CubismModelMatrix* CubismUserModel::GetModelMatrix() const +{ + return _modelMatrix; +} + +csmBool CubismUserModel::IsInitialized() +{ + return _initialized; +} + +void CubismUserModel::IsInitialized(csmBool v) +{ + _initialized = v; +} + +csmBool CubismUserModel::IsUpdating() +{ + return _updating; +} + +void CubismUserModel::IsUpdating(csmBool v) +{ + _updating = v; +} + +void CubismUserModel::SetOpacity(csmFloat32 a) +{ + _opacity = a; +} + +csmFloat32 CubismUserModel::GetOpacity() +{ + return _opacity; +} + +CubismModel* CubismUserModel::GetModel() const +{ + return _model; +} + +void CubismUserModel::CreateRenderer(csmInt32 maskBufferCount) +{ + if (_renderer) + { + DeleteRenderer(); + } + _renderer = Rendering::CubismRenderer::Create(); + + _renderer->Initialize(_model, maskBufferCount); +} + +void CubismUserModel::DeleteRenderer() +{ + if (_renderer) + { + Rendering::CubismRenderer::Delete(_renderer); + + _renderer = NULL; + } +} + +void CubismUserModel::CubismDefaultMotionEventCallback(const CubismMotionQueueManager* caller, const csmString& eventValue, void* customData) +{ + CubismUserModel* model = reinterpret_cast(customData); + if (model != NULL) + { + model->MotionEventFired(eventValue); + } +} + +void CubismUserModel::MotionEventFired(const csmString& eventValue) +{ + CubismLogInfo("%s",eventValue.GetRawString()); +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Model/CubismUserModel.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "Effect/CubismPose.hpp" +#include "Effect/CubismEyeBlink.hpp" +#include "Effect/CubismBreath.hpp" +#include "Math/CubismModelMatrix.hpp" +#include "Math/CubismTargetPoint.hpp" +#include "Model/CubismMoc.hpp" +#include "Model/CubismModel.hpp" +#include "Motion/CubismMotionManager.hpp" +#include "Motion/CubismExpressionMotion.hpp" +#include "Physics/CubismPhysics.hpp" +#include "Rendering/CubismRenderer.hpp" +#include "Model/CubismModelUserData.hpp" +#include "Motion/CubismExpressionMotionManager.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief ユーザーが実際に使用するモデル + * + * ユーザーが実際に使用するモデルの基底クラス。これを継承してユーザーが実装する。 + */ +class CubismUserModel +{ +public: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismUserModel(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismUserModel(); + + /** + * @brief 初期化状態の取得 + * + * 初期化されている状態か? + * + * @retval true 初期化されている + * @retval false 初期化されていない + */ + virtual csmBool IsInitialized(); + + /** + * @brief 初期化状態の設定 + * + * 初期化状態を設定する。 + * + * @param[in] v 初期化状態 + */ + virtual void IsInitialized(csmBool v); + + /** + * @brief 更新状態の取得 + * + * 更新されている状態か? + * + * @retval true 更新されている + * @retval false 更新されていない + */ + virtual csmBool IsUpdating(); + + /** + * @brief 更新状態の設定 + * + * 更新状態を設定する。 + * + * @param[in] v 更新状態 + */ + virtual void IsUpdating(csmBool v); + + /** + * @brief マウスドラッグ情報の設定 + * + * マウスドラッグの情報を設定する。 + * + * @param[in] x ドラッグしているカーソルのX位置 + * @param[in] y ドラッグしているカーソルのY位置 + */ + virtual void SetDragging(csmFloat32 x, csmFloat32 y); + + /** + * @brief 加速度情報の設定 + * + * 加速度の情報を設定する。 + * + * @param[in] x X軸方向の加速度 + * @param[in] y Y軸方向の加速度 + * @param[in] z Z軸方向の加速度 + */ + virtual void SetAcceleration(csmFloat32 x, csmFloat32 y, csmFloat32 z); + + /** + * @brief モデル行列の取得 + * + * モデル行列を取得する。 + * + * @return モデル行列 + */ + CubismModelMatrix* GetModelMatrix() const; + + /** + * @brief 不透明度の設定 + * + * 不透明度を設定する。 + * + * @param[in] a 不透明度 + */ + virtual void SetOpacity(csmFloat32 a); + + /** + * @brief 不透明度の取得 + * + * 不透明度を取得する。 + * + * @return 不透明度 + */ + virtual csmFloat32 GetOpacity(); + + /** + * @brief モデルデータの読み込み + * + * モデルデータを読み込む。 + * + * @param[in] buffer moc3ファイルが読み込まれているバッファ + * @param[in] size バッファのサイズ + * @param[in] shouldCheckMocConsistency MOCの整合性チェックフラグ(初期値 : false) + */ + virtual void LoadModel(const csmByte* buffer, csmSizeInt size, csmBool shouldCheckMocConsistency = false); + + /** + * @brief モーションデータの読み込み + * + * モーションデータを読み込む。 + * + * @param[in] buffer motion3.jsonファイルが読み込まれているバッファ + * @param[in] size バッファのサイズ + * @param[in] name モーションの名前 + * @param[in] onFinishedMotionHandler モーション再生終了時に呼び出されるコールバック関数。NULLの場合、呼び出されない。 + * @return モーションクラス + */ + virtual ACubismMotion* LoadMotion(const csmByte* buffer, csmSizeInt size, const csmChar* name, ACubismMotion::FinishedMotionCallback onFinishedMotionHandler = NULL); + + /** + * @brief 表情データの読み込み + * + * 表情データを読み込む。 + * + * @param[in] buffer expファイルが読み込まれているバッファ + * @param[in] size バッファのサイズ + * @param[in] name 表情の名前 + */ + virtual ACubismMotion* LoadExpression(const csmByte* buffer, csmSizeInt size, const csmChar* name); + + /** + * @brief ポーズデータの読み込み + * + * ポーズデータを読み込む。 + * + * @param[in] buffer pose3.jsonが読み込まれているバッファ + * @param[in] size バッファのサイズ + */ + virtual void LoadPose(const csmByte* buffer, csmSizeInt size); + + /** + * @brief 物理演算データの読み込み + * + * 物理演算データを読み込む。 + * + * @param[in] buffer physics3.jsonが読み込まれているバッファ + * @param[in] size バッファのサイズ + */ + virtual void LoadPhysics(const csmByte* buffer, csmSizeInt size); + + /** + * @brief モデルに付属するユーザーデータを読み込む + * + * ユーザーデータを読み込む。 + * + * @param[in] buffer userdata3.jsonが読み込まれているバッファ + * @param[in] size バッファのサイズ + */ + virtual void LoadUserData(const csmByte* buffer, csmSizeInt size); + + /** + * @brief あたり判定の取得 + * + * 指定した位置にDrawableがヒットしているかどうかを取得する。 + * + * @param[in] drawableId 検証したいDrawableのID + * @param[in] pointX X位置 + * @param[in] pointY Y位置 + * @retval true ヒットしている + * @retval false ヒットしていない + */ + virtual csmBool IsHit(CubismIdHandle drawableId, csmFloat32 pointX, csmFloat32 pointY); + + /** + * @brief モデルの取得 + * + * モデルを取得する。 + * + * @return モデル + */ + CubismModel* GetModel() const; + + /** + * @brief レンダラの取得 + * + * レンダラを取得する。 + * + * @return レンダラ + */ + template T* GetRenderer() { return dynamic_cast(_renderer); } + + /** + * @brief レンダラの生成 + * + * レンダラを生成して初期化を実行する。 + * + */ + void CreateRenderer(csmInt32 maskBufferCount = 1); + + /** + * @brief レンダラの解放 + * + * レンダラを解放する。 + * + */ + void DeleteRenderer(); + + /** + * @brief イベント発火時の標準処理 + * + * Eventが再生処理時にあった場合の処理をする。 + * 継承で上書きすることを想定している。 + * 上書きしない場合はログ出力をする。 + * + * @param[in] eventValue 発火したイベントの文字列データ + */ + virtual void MotionEventFired(const csmString& eventValue); + + /** + * @brief イベント用のCallback + * + * CubismMotionQueueManagerにイベント用に登録するためのCallback。 + * CubismUserModelの継承先のEventFiredを呼ぶ。 + * + * @param[in] caller 発火したイベントを管理していたモーションマネージャー、比較用 + * @param[in] eventValue 発火したイベントの文字列データ + * @param[in] customData CubismUserModelを継承したインスタンスを想定 + */ + static void CubismDefaultMotionEventCallback(const CubismMotionQueueManager* caller, const csmString& eventValue, void* customData); +protected: + CubismMoc* _moc; ///< Mocデータ + CubismModel* _model; ///< Modelインスタンス + + CubismMotionManager* _motionManager; ///< モーション管理 + CubismExpressionMotionManager* _expressionManager; ///< 表情管理 + CubismEyeBlink* _eyeBlink; ///< 自動まばたき + CubismBreath* _breath; ///< 呼吸 + CubismModelMatrix* _modelMatrix; ///< モデル行列 + CubismPose* _pose; ///< ポーズ管理 + CubismTargetPoint* _dragManager; ///< マウスドラッグ + CubismPhysics* _physics; ///< 物理演算 + CubismModelUserData* _modelUserData; ///< ユーザデータ + + csmBool _initialized; ///< 初期化されたかどうか + csmBool _updating; ///< 更新されたかどうか + csmFloat32 _opacity; ///< 不透明度 + csmBool _lipSync; ///< リップシンクするかどうか + csmFloat32 _lastLipSyncValue; ///< 最後のリップシンクの制御値 + csmFloat32 _dragX; ///< マウスドラッグのX位置 + csmFloat32 _dragY; ///< マウスドラッグのY位置 + csmFloat32 _accelerationX; ///< X軸方向の加速度 + csmFloat32 _accelerationY; ///< Y軸方向の加速度 + csmFloat32 _accelerationZ; ///< Z軸方向の加速度 + csmBool _mocConsistency; ///< MOC3整合性検証するかどうか + csmBool _debugMode; ///< デバッグモードかどうか + +private: + Rendering::CubismRenderer* _renderer; ///< レンダラ +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/ACubismMotion.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "ACubismMotion.hpp" +#include "Model/CubismModel.hpp" +#include "CubismMotionQueueEntry.hpp" +#include "Math/CubismMath.hpp" + + +namespace Live2D { namespace Cubism { namespace Framework { + +void ACubismMotion::Delete(ACubismMotion* motion) +{ + CSM_DELETE_SELF(ACubismMotion, motion); +} + +ACubismMotion::ACubismMotion() + : _fadeInSeconds(-1.0f) + , _fadeOutSeconds(-1.0f) + , _weight(1.0f) + , _offsetSeconds(0.0f) //再生の開始時刻 + , _onFinishedMotion(NULL) + , _onFinishedMotionCustomData(NULL) +{ } + +ACubismMotion::~ACubismMotion() +{ + this->_weight = 0.0f; +} + +void ACubismMotion::UpdateParameters(CubismModel* model, CubismMotionQueueEntry* motionQueueEntry, csmFloat32 userTimeSeconds) +{ + if (!motionQueueEntry->IsAvailable() || motionQueueEntry->IsFinished()) + { + return; + } + + SetupMotionQueueEntry(motionQueueEntry, userTimeSeconds); + + csmFloat32 fadeWeight = UpdateFadeWeight(motionQueueEntry, userTimeSeconds); + + //---- 全てのパラメータIDをループする ---- + DoUpdateParameters(model, userTimeSeconds, fadeWeight, motionQueueEntry); + + //後処理 + //終了時刻を過ぎたら終了フラグを立てる(CubismMotionQueueManager) + if ((motionQueueEntry->GetEndTime() > 0.0f) && (motionQueueEntry->GetEndTime() < userTimeSeconds)) + { + motionQueueEntry->IsFinished(true); //終了 + } +} + +void ACubismMotion::SetupMotionQueueEntry(CubismMotionQueueEntry* motionQueueEntry, csmFloat32 userTimeSeconds) +{ + if (!motionQueueEntry->IsAvailable() || motionQueueEntry->IsFinished()) { + return; + } + + if (motionQueueEntry->IsStarted()) { + return; + } + + motionQueueEntry->IsStarted(true); + motionQueueEntry->SetStartTime(userTimeSeconds - _offsetSeconds); //モーションの開始時刻を記録 + motionQueueEntry->SetFadeInStartTime(userTimeSeconds); //フェードインの開始時刻 + + const csmFloat32 duration = GetDuration(); + + if (motionQueueEntry->GetEndTime() < 0) + { + //開始していないうちに終了設定している場合がある。 + motionQueueEntry->SetEndTime((duration <= 0) ? -1 : motionQueueEntry->GetStartTime() + duration); + //duration == -1 の場合はループする + } +} + +csmFloat32 ACubismMotion::UpdateFadeWeight(CubismMotionQueueEntry* motionQueueEntry, csmFloat32 userTimeSeconds) +{ + if (motionQueueEntry == NULL) + { + CubismLogError("motionQueueEntry is null."); + } + + csmFloat32 fadeWeight = _weight; //現在の値と掛け合わせる割合 + + //---- フェードイン・アウトの処理 ---- + //単純なサイン関数でイージングする + const csmFloat32 fadeIn = _fadeInSeconds == 0.0f + ? 1.0f + : CubismMath::GetEasingSine((userTimeSeconds - motionQueueEntry->GetFadeInStartTime()) / _fadeInSeconds); + + const csmFloat32 fadeOut = (_fadeOutSeconds == 0.0f || motionQueueEntry->GetEndTime() < 0.0f) + ? 1.0f + : CubismMath::GetEasingSine((motionQueueEntry->GetEndTime() - userTimeSeconds) / _fadeOutSeconds); + + fadeWeight = fadeWeight * fadeIn * fadeOut; + + motionQueueEntry->SetState(userTimeSeconds, fadeWeight); + + CSM_ASSERT(0.0f <= fadeWeight && fadeWeight <= 1.0f); + + return fadeWeight; +} + + +void ACubismMotion::SetFadeInTime(csmFloat32 fadeInSeconds) +{ + this->_fadeInSeconds = fadeInSeconds; +} + +void ACubismMotion::SetFadeOutTime(csmFloat32 fadeOutSeconds) +{ + this->_fadeOutSeconds = fadeOutSeconds; +} + +csmFloat32 ACubismMotion::GetFadeOutTime() const +{ + return this->_fadeOutSeconds; +} + +csmFloat32 ACubismMotion::GetFadeInTime() const +{ + return this->_fadeInSeconds; +} + +void ACubismMotion::SetWeight(csmFloat32 weight) +{ + this->_weight = weight; +} + +csmFloat32 ACubismMotion::GetWeight() const +{ + return this->_weight; +} + +csmFloat32 ACubismMotion::GetDuration() +{ + return -1.0f; +} + +csmFloat32 ACubismMotion::GetLoopDuration() +{ + return -1.0f; +} + +void ACubismMotion::SetOffsetTime(csmFloat32 offsetSeconds) +{ + this->_offsetSeconds = offsetSeconds; +} + +const csmVector& ACubismMotion::GetFiredEvent(csmFloat32 beforeCheckTimeSeconds, csmFloat32 motionTimeSeconds) +{ + return _firedEventValues; +} + +void ACubismMotion::SetFinishedMotionHandler(FinishedMotionCallback onFinishedMotionHandler) +{ + this->_onFinishedMotion = onFinishedMotionHandler; +} + +ACubismMotion::FinishedMotionCallback ACubismMotion::GetFinishedMotionHandler() +{ + return this->_onFinishedMotion; +} + +void ACubismMotion::SetFinishedMotionCustomData(void* onFinishedMotionCustomData) +{ + this->_onFinishedMotionCustomData = onFinishedMotionCustomData; +} + +void* ACubismMotion::GetFinishedMotionCustomData() +{ + return this->_onFinishedMotionCustomData; +} + +void ACubismMotion::SetFinishedMotionHandlerAndMotionCustomData(FinishedMotionCallback onFinishedMotionHandler, void* onFinishedMotionCustomData) +{ + this->_onFinishedMotion = onFinishedMotionHandler; + this->_onFinishedMotionCustomData = onFinishedMotionCustomData; +} + +csmBool ACubismMotion::IsExistModelOpacity() const +{ + return false; +} + +csmInt32 ACubismMotion::GetModelOpacityIndex() const +{ + return -1; +} + +CubismIdHandle ACubismMotion::GetModelOpacityId(csmInt32 index) +{ + return NULL; +} + +csmFloat32 ACubismMotion::GetModelOpacityValue() const +{ + return 1.0f; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/ACubismMotion.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismFramework.hpp" +#include "Id/CubismId.hpp" +#include "Type/csmVector.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +class CubismMotionQueueManager; +class CubismMotionQueueEntry; +class CubismModel; + +/** + * @brief モーションの抽象基底クラス + * + * モーションの抽象基底クラス。MotionQueueManagerによってモーションの再生を管理する。 + */ +class ACubismMotion +{ +public: + /// モーション再生終了コールバック関数定義 + typedef void (*FinishedMotionCallback)(ACubismMotion* self); + /** + * @brief インスタンスの破棄 + * + * インスタンスを破棄する。 + * + * @param[in] motion 破棄対象のACubismMotion + */ + static void Delete(ACubismMotion* motion); + + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + ACubismMotion(); + + /** + * @brief モデルのパラメータ更新 + * + * モデルのパラメータを更新する。 + * + * @param[in] model 対象のモデル + * @param[in] motionQueueEntry CubismMotionQueueManagerで管理されているモーション + * @param[in] userTimeSeconds デルタ時間の積算値[秒] + */ + void UpdateParameters(CubismModel* model, CubismMotionQueueEntry* motionQueueEntry, csmFloat32 userTimeSeconds); + + /** + * モーションの再生を開始するためのセットアップを行う。 + * + * @param motionQueueEntry CubismMotionQueueManagerによって管理されるモーション + * @param userTimeSeconds 総再生時間(秒) + */ + void SetupMotionQueueEntry(CubismMotionQueueEntry* motionQueueEntry, csmFloat32 userTimeSeconds); + + /** + * @brief フェードイン + * + * フェードインの時間を設定する。 + * + * @param[in] fadeInSeconds フェードインにかかる時間[秒] + */ + void SetFadeInTime(csmFloat32 fadeInSeconds); + + /** + * @brief フェードアウト + * + * フェードアウトの時間を設定する。 + * + * @param[in] fadeOutSeconds フェードアウトにかかる時間[秒] + */ + void SetFadeOutTime(csmFloat32 fadeOutSeconds); + + /** + * @brief フェードアウトにかかる時間の取得 + * + * フェードアウトにかかる時間を取得する。 + * + * @return フェードアウトにかかる時間[秒] + */ + csmFloat32 GetFadeOutTime() const; + + /** + * @brief フェードインにかかる時間の取得 + * + * フェードインにかかる時間を取得する。 + * + * @return フェードインにかかる時間[秒] + */ + csmFloat32 GetFadeInTime() const; + + /** + * @brief モーション適用の重みの設定 + * + * モーション適用の重みを設定する。 + * + * @param[in] weight 重み(0.0 - 1.0) + */ + void SetWeight(csmFloat32 weight); + + /** + * @brief モーション適用の重みの取得 + * + * モーション適用の重みを取得する。 + * + * @return 重み(0.0 - 1.0) + */ + csmFloat32 GetWeight() const; + + /** + * @brief モーションの長さの取得 + * + * モーションの長さを取得する。 + * + * @return モーションの長さ[秒] + * + * @note ループのときは「-1」。 + * ループではない場合は、オーバーライドする。 + * 正の値の時は取得される時間で終了する。 + * 「-1」のときは外部から停止命令が無い限り終わらない処理となる。 + */ + virtual csmFloat32 GetDuration(); + + /** + * @brief モーションのループ1回分の長さの取得 + * + * モーションのループ1回分の長さを取得する。 + * + * @return モーションのループ1回分の長さ[秒] + * + * @note ループしない場合は GetDuration()と同じ値を返す。 + * ループ一回分の長さが定義できない場合(プログラム的に動き続けるサブクラスなど)の場合は「-1」を返す + */ + virtual csmFloat32 GetLoopDuration(); + + + /** + * @brief モーション再生の開始時刻の設定 + * + * モーション再生の開始時刻を設定する。 + * + * @param[in] offsetSeconds モーション再生の開始時刻[秒] + */ + void SetOffsetTime(csmFloat32 offsetSeconds); + + /** + * @brief モデルのパラメータ更新 + * + * イベント発火のチェック。 + * 入力する時間は呼ばれるモーションタイミングを0とした秒数で行う。 + * + * @param[in] beforeCheckTimeSeconds 前回のイベントチェック時間[秒] + * @param[in] motionTimeSeconds 今回の再生時間[秒] + */ + virtual const csmVector& GetFiredEvent(csmFloat32 beforeCheckTimeSeconds, + csmFloat32 motionTimeSeconds); + + + /** + * @brief モーション再生終了コールバックの登録 + * + * モーション再生終了コールバックを登録する。 + * IsFinishedフラグを設定するタイミングで呼び出される。 + * 以下の状態の際には呼び出されない: + * 1. 再生中のモーションが「ループ」として設定されているとき + * 2. コールバックにNULLが登録されているとき + * + * @param[in] onFinishedMotionHandler モーション再生終了コールバック関数 + */ + void SetFinishedMotionHandler(FinishedMotionCallback onFinishedMotionHandler); + + /** + * @brief モーション再生終了コールバックの取得 + * + * モーション再生終了コールバックを取得する。 + * + * @return 登録されているモーション再生終了コールバック関数。NULLのとき、関数は何も登録されていない。 + */ + FinishedMotionCallback GetFinishedMotionHandler(); + + /** + * @brief ユーザー任意データの登録 + * + * ユーザー任意データを登録します。 + * + * @param[in] onFinishedMotionCustomData ユーザー任意データ + */ + void SetFinishedMotionCustomData(void* onFinishedMotionCustomData); + + /** + * @brief ユーザー任意データの取得 + * + * ユーザー任意データを取得します。 + * + * @return 登録されているユーザー任意データ。 + */ + void* GetFinishedMotionCustomData(); + + /** + * @brief モーション再生終了コールバックとユーザー任意データの登録 + * + * モーション再生終了コールバックを登録する。 + * IsFinishedフラグを設定するタイミングで呼び出される。 + * 以下の状態の際には呼び出されない: + * 1. 再生中のモーションが「ループ」として設定されているとき + * 2. コールバックにNULLが登録されているとき + * + * @param[in] onFinishedMotionHandler モーション再生終了コールバック関数 + * @param[in] onFinishedMotionCustomData ユーザー任意データ + */ + void SetFinishedMotionHandlerAndMotionCustomData(FinishedMotionCallback onFinishedMotionHandler, void* onFinishedMotionCustomData); + + /** + * @brief 透明度のカーブが存在するかどうかを確認する + * + * @retval true -> キーが存在する + * @retval false -> キーが存在しない + */ + virtual csmBool IsExistModelOpacity() const; + + /** + * @brief 透明度のカーブのインデックスを返す + * + * + * @return success:透明度のカーブのインデックス + */ + virtual csmInt32 GetModelOpacityIndex() const; + + /** + * @brief 透明度のIdを返す + * + * + * @return success:透明度のId + */ + virtual CubismIdHandle GetModelOpacityId(csmInt32 index); + + /** + * @brief モデルのウェイト更新 + * + * モーションのウェイトを更新する。 + * + * @param[in] motionQueueEntry CubismMotionQueueManagerで管理されているモーション + * @param[in] userTimeSeconds デルタ時間の積算値[秒] + */ + csmFloat32 UpdateFadeWeight(CubismMotionQueueEntry* motionQueueEntry, csmFloat32 userTimeSeconds); + +private: + // Prevention of copy Constructor + ACubismMotion(const ACubismMotion&); + ACubismMotion& operator=(const ACubismMotion&); + +protected: + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~ACubismMotion(); + + /** + * + * @brief 指定時間の透明度の値を返す + * + * @param[in] motionTimeSeconds 現在の再生時間[秒] + * + * @return success:モーションの当該時間におけるOpacityの値 + * + * @note 更新後の値を取るにはUpdateParameters() の後に呼び出す。 + */ + virtual csmFloat32 GetModelOpacityValue() const; + + /** + * @brief モデルのパラメータの更新の実行 + * + * モデルのパラメータ更新を実行する。 + * + * @param[in] model 対象のモデル + * @param[in] userTimeSeconds デルタ時間の積算値[秒] + * @param[in] weight モーションの重み + * @param[in] motionQueueEntry CubismMotionQueueManagerで管理されているモーション + */ + virtual void DoUpdateParameters(CubismModel* model, csmFloat32 userTimeSeconds, csmFloat32 weight, CubismMotionQueueEntry* motionQueueEntry) = 0; + + csmFloat32 _fadeInSeconds; ///< フェードインにかかる時間[秒] + csmFloat32 _fadeOutSeconds; ///< フェードアウトにかかる時間[秒] + csmFloat32 _weight; ///< モーションの重み + csmFloat32 _offsetSeconds; ///< モーション再生の開始時刻[秒] + + csmVector _firedEventValues; + + FinishedMotionCallback _onFinishedMotion; ///< モーション再生終了コールバック関数ポインタ + void* _onFinishedMotionCustomData; ///< モーション再生終了コールバックに戻されるデータ +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CMakeLists.txt`: + +```txt +target_sources(${LIB_NAME} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/ACubismMotion.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ACubismMotion.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismExpressionMotion.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismExpressionMotion.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismExpressionMotionManager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismExpressionMotionManager.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismMotion.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismMotion.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismMotionInternal.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismMotionJson.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismMotionJson.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismMotionManager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismMotionManager.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismMotionQueueEntry.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismMotionQueueEntry.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismMotionQueueManager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/CubismMotionQueueManager.hpp +) + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismExpressionMotion.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismExpressionMotion.hpp" +#include "CubismMotionQueueEntry.hpp" +#include "Id/CubismIdManager.hpp" +#include "Math/CubismMath.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +const csmFloat32 CubismExpressionMotion::DefaultAdditiveValue = 0.0f; ///< 加算適用の初期値 +const csmFloat32 CubismExpressionMotion::DefaultMultiplyValue = 1.0f; ///< 乗算適用の初期値 + +namespace { +// exp3.jsonのキーとデフォルト値 +const csmChar* ExpressionKeyFadeIn = "FadeInTime"; +const csmChar* ExpressionKeyFadeOut = "FadeOutTime"; +const csmChar* ExpressionKeyParameters = "Parameters"; +const csmChar* ExpressionKeyId = "Id"; +const csmChar* ExpressionKeyValue = "Value"; +const csmChar* ExpressionKeyBlend = "Blend"; +const csmChar* BlendValueAdd = "Add"; +const csmChar* BlendValueMultiply = "Multiply"; +const csmChar* BlendValueOverwrite = "Overwrite"; +const csmFloat32 DefaultFadeTime = 1.0f; +} + + +CubismExpressionMotion::CubismExpressionMotion() +{ } + +CubismExpressionMotion::~CubismExpressionMotion() +{ } + +CubismExpressionMotion* CubismExpressionMotion::Create(const csmByte* buffer, csmSizeInt size) +{ + CubismExpressionMotion* expression = CSM_NEW CubismExpressionMotion(); + expression->Parse(buffer, size); + return expression; +} + +void CubismExpressionMotion::DoUpdateParameters(CubismModel* model, csmFloat32 userTimeSeconds, csmFloat32 weight, CubismMotionQueueEntry* motionQueueEntry) +{ + for (csmUint32 i = 0; i < _parameters.GetSize(); ++i) + { + ExpressionParameter& parameter = _parameters[i]; + + switch (parameter.BlendType) + { + case Additive: { + model->AddParameterValue(parameter.ParameterId, parameter.Value, weight); // 相対変化 加算 + break; + } + case Multiply: { + model->MultiplyParameterValue(parameter.ParameterId, parameter.Value, weight); // 相対変化 乗算 + break; + } + case Overwrite: { + model->SetParameterValue(parameter.ParameterId, parameter.Value, weight); // 絶対変化 上書き + break; + } + default: + // 仕様にない値を設定したときは既に加算モードになっている + break; + } + } +} + +void CubismExpressionMotion::CalculateExpressionParameters(CubismModel* model, csmFloat32 userTimeSeconds, CubismMotionQueueEntry* motionQueueEntry, + csmVector* expressionParameterValues, csmInt32 expressionIndex, csmFloat32 fadeWeight) +{ + if (motionQueueEntry == NULL || expressionParameterValues == NULL) + { + return; + } + + if (!motionQueueEntry->IsAvailable()) + { + return; + } + + // CubismExpressionMotion._fadeWeight は廃止予定です。 + // 互換性のために処理は残りますが、実際には使用しておりません。 + _fadeWeight = UpdateFadeWeight(motionQueueEntry, userTimeSeconds); + + // モデルに適用する値を計算 + for (csmInt32 i = 0; i < (csmInt32)expressionParameterValues->GetSize(); ++i) + { + CubismExpressionMotionManager::ExpressionParameterValue expressionParameterValue = expressionParameterValues->At(i); + + if (expressionParameterValue.ParameterId == NULL) + { + continue; + } + + const csmFloat32 currentParameterValue = expressionParameterValue.OverwriteValue = + model->GetParameterValue(expressionParameterValue.ParameterId); + + csmVector expressionParameters = GetExpressionParameters(); + csmInt32 parameterIndex = -1; + for (csmInt32 j = 0; j < (csmInt32)expressionParameters.GetSize(); ++j) + { + if (expressionParameterValue.ParameterId != expressionParameters[j].ParameterId) + { + continue; + } + + parameterIndex = j; + + break; + } + + // 再生中のExpressionが参照していないパラメータは初期値を適用 + if (parameterIndex < 0) + { + if (expressionIndex == 0) + { + expressionParameterValues->At(i).AdditiveValue = DefaultAdditiveValue; + + expressionParameterValues->At(i).MultiplyValue = DefaultMultiplyValue; + + expressionParameterValues->At(i).OverwriteValue = currentParameterValue; + } + else + { + expressionParameterValues->At(i).AdditiveValue = + CalculateValue(expressionParameterValue.AdditiveValue, DefaultAdditiveValue, fadeWeight); + + expressionParameterValues->At(i).MultiplyValue = + CalculateValue(expressionParameterValue.MultiplyValue, DefaultMultiplyValue, fadeWeight); + + expressionParameterValues->At(i).OverwriteValue = + CalculateValue(expressionParameterValue.OverwriteValue, currentParameterValue, fadeWeight); + } + continue; + } + + // 値を計算 + csmFloat32 value = expressionParameters.At(parameterIndex).Value; + csmFloat32 newAdditiveValue, newMultiplyValue, newSetValue; + switch (expressionParameters.At(parameterIndex).BlendType) { + case Additive: + newAdditiveValue = value; + newMultiplyValue = DefaultMultiplyValue; + newSetValue = currentParameterValue; + break; + case Multiply: + newAdditiveValue = DefaultAdditiveValue; + newMultiplyValue = value; + newSetValue = currentParameterValue; + break; + case Overwrite: + newAdditiveValue = DefaultAdditiveValue; + newMultiplyValue = DefaultMultiplyValue; + newSetValue = value; + break; + default: + return; + } + + if (expressionIndex == 0) { + expressionParameterValues->At(i).AdditiveValue = newAdditiveValue; + expressionParameterValues->At(i).MultiplyValue = newMultiplyValue; + expressionParameterValues->At(i).OverwriteValue = newSetValue; + } + else { + expressionParameterValues->At(i).AdditiveValue = (expressionParameterValue.AdditiveValue * (1.0f - fadeWeight)) + newAdditiveValue * fadeWeight; + expressionParameterValues->At(i).MultiplyValue = (expressionParameterValue.MultiplyValue * (1.0f - fadeWeight)) + newMultiplyValue * fadeWeight; + expressionParameterValues->At(i).OverwriteValue = (expressionParameterValue.OverwriteValue * (1.0f - fadeWeight)) + newSetValue * fadeWeight; + } + } +} + +csmVector CubismExpressionMotion::GetExpressionParameters() +{ + return _parameters; +} + +csmFloat32 CubismExpressionMotion::GetFadeWeight() +{ +#if _DEBUG + CubismLogWarning("GetFadeWeight() is a deprecated function. Please use CubismExpressionMotionManager.GetFadeWeight(int index)."); +#endif // _DEBUG + + return _fadeWeight; +} + +void CubismExpressionMotion::Parse(const csmByte* buffer, csmSizeInt size) +{ + Utils::CubismJson* json = Utils::CubismJson::Create(buffer, size); + if (!json) + { + return; + } + + Utils::Value& root = json->GetRoot(); + + SetFadeInTime(root[ExpressionKeyFadeIn].ToFloat(DefaultFadeTime)); // フェードイン + SetFadeOutTime(root[ExpressionKeyFadeOut].ToFloat(DefaultFadeTime)); // フェードアウト + + // 各パラメータについて + const csmInt32 parameterCount = root[ExpressionKeyParameters].GetSize(); + _parameters.PrepareCapacity(parameterCount); + + for (csmInt32 i = 0; i < parameterCount; ++i) + { + Utils::Value& param = root[ExpressionKeyParameters][i]; + const CubismIdHandle parameterId = CubismFramework::GetIdManager()->GetId(param[ExpressionKeyId].GetRawString()); // パラメータID + const csmFloat32 value = static_cast(param[ExpressionKeyValue].ToFloat()); // 値 + + // 計算方法の設定 + ExpressionBlendType blendType; + + if (param[ExpressionKeyBlend].IsNull() || param[ExpressionKeyBlend].GetString() == BlendValueAdd) + { + blendType = Additive; + } + else if (param[ExpressionKeyBlend].GetString() == BlendValueMultiply) + { + blendType = Multiply; + } + else if (param[ExpressionKeyBlend].GetString() == BlendValueOverwrite) + { + blendType = Overwrite; + } + else + { + // その他 仕様にない値を設定したときは加算モードにすることで復旧 + blendType = Additive; + } + + // 設定オブジェクトを作成してリストに追加する + ExpressionParameter item; + + item.ParameterId = parameterId; + item.BlendType = blendType; + item.Value = value; + + _parameters.PushBack(item); + } + + Utils::CubismJson::Delete(json);// JSONデータは不要になったら削除する +} + +csmFloat32 CubismExpressionMotion::CalculateValue(csmFloat32 source, csmFloat32 destination, csmFloat32 fadeWeight) +{ + return (source * (1.0f - fadeWeight)) + (destination * fadeWeight); +} + + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismExpressionMotion.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "ACubismMotion.hpp" +#include "Utils/CubismJson.hpp" +#include "Model/CubismModel.hpp" +#include "CubismExpressionMotionManager.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief 表情のモーション + * + * 表情のモーションクラス。 + */ +class CubismExpressionMotion : public ACubismMotion +{ +public: + /** + * @brief 表情パラメータ値の計算方式 + * + */ + enum ExpressionBlendType + { + Additive = 0, ///< 加算 + Multiply = 1, ///< 乗算 + Overwrite = 2 ///< 上書き + }; + + /** + * @brief 表情のパラメータ情報 + * + * 表情のパラメータ情報の構造体。 + */ + struct ExpressionParameter + { + CubismIdHandle ParameterId; ///< パラメータID + ExpressionBlendType BlendType; ///< パラメータの演算種類 + csmFloat32 Value; ///< 値 + }; + + /** + * @brief インスタンスの作成 + * + * インスタンスを作成する。 + * + * @param[in] buf expファイルが読み込まれているバッファ + * @param[in] size バッファのサイズ + * @return 作成されたインスタンス + */ + static CubismExpressionMotion* Create(const csmByte* buf, csmSizeInt size); + + /** + * @brief モデルのパラメータの更新の実行 + * + * モデルのパラメータ更新を実行する。 + * + * @param[in] model 対象のモデル + * @param[in] userTimeSeconds デルタ時間の積算値[秒] + * @param[in] weight モーションの重み + * @param[in] motionQueueEntry CubismMotionQueueManagerで管理されているモーション + */ + virtual void DoUpdateParameters(CubismModel* model, csmFloat32 userTimeSeconds, csmFloat32 weight, CubismMotionQueueEntry* motionQueueEntry); + + /** + * @brief 表情によるモデルのパラメータの計算 + * + * モデルの表情に関するパラメータを計算する。 + * + * @param[in] model 対象のモデル + * @param[in] userTimeSeconds デルタ時間の積算値[秒] + * @param[in] motionQueueEntry CubismMotionQueueManagerで管理されているモーション + * @param[in] expressionParameterValues モデルに適用する各パラメータの値 + * @param[in] expressionIndex 表情のインデックス + */ + void CalculateExpressionParameters(CubismModel* model, csmFloat32 userTimeSeconds, CubismMotionQueueEntry* motionQueueEntry, + csmVector* expressionParameterValues, csmInt32 expressionIndex, csmFloat32 fadeWeight); + + /** + * @brief 表情が参照しているパラメータを取得 + * + * 表情が参照しているパラメータを取得する。 + */ + csmVector GetExpressionParameters(); + + /** + * @brief 表情のフェードの値を取得 + * + * 現在の表情のフェードのウェイト値を取得する。 + * + * @return 表情のフェードのウェイト値 + * + * @deprecated CubismExpressionMotion._fadeWeightが削除予定のため非推奨 + * CubismExpressionMotionManager.getFadeWeight(int index) を使用してください。 + * + * @see CubismExpressionMotionManager#GetFadeWeight(int index) + */ + csmFloat32 GetFadeWeight(); + + static const csmFloat32 DefaultAdditiveValue; ///< 加算適用の初期値 + static const csmFloat32 DefaultMultiplyValue; ///< 乗算適用の初期値 + +protected: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismExpressionMotion(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismExpressionMotion(); + + /** + * @brief exp3.jsonのパース + * + * exp3.jsonをパースする。 + * + * @param[in] exp3Json exp3.jsonが読み込まれているバッファ + * @param[in] size バッファのサイズ + */ + void Parse(const csmByte* exp3Json, csmSizeInt size); + + csmVector _parameters; ///< 表情が参照しているパラメータ一覧 + +private: + + /** + * @brief ブレンド計算 + * + * 入力された値でブレンド計算をする。 + * + * @param[in] source 現在の値 + * @param[in] destination 適用する値 + */ + csmFloat32 CalculateValue(csmFloat32 source, csmFloat32 destination, csmFloat32 fadeWeight); + + + /** + * 表情の現在のウェイト + * + * @deprecated 不具合を引き起こす要因となるため非推奨。 + */ + csmFloat32 _fadeWeight; +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismExpressionMotionManager.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismExpressionMotionManager.hpp" +#include "CubismExpressionMotion.hpp" +#include "CubismMotionQueueEntry.hpp" +#include "CubismFramework.hpp" +#include "Math/CubismMath.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +CubismExpressionMotionManager::CubismExpressionMotionManager() + : _expressionParameterValues(CSM_NEW csmVector()) + , _currentPriority(0) + , _reservePriority(0) +{ } + +CubismExpressionMotionManager::~CubismExpressionMotionManager() +{ + if(_expressionParameterValues) + { + CSM_DELETE(_expressionParameterValues); + + _expressionParameterValues = NULL; + } + + _fadeWeights.Clear(); +} + +csmInt32 CubismExpressionMotionManager::GetCurrentPriority() const +{ + return _currentPriority; +} + +csmInt32 CubismExpressionMotionManager::GetReservePriority() const +{ + return _reservePriority; +} + +void CubismExpressionMotionManager::SetReservePriority(csmInt32 priority) +{ + _reservePriority = priority; +} + +CubismMotionQueueEntryHandle CubismExpressionMotionManager::StartMotionPriority(ACubismMotion* motion, csmBool autoDelete, csmInt32 priority) +{ + if (priority == _reservePriority) + { + _reservePriority = 0; // 予約を解除 + } + _currentPriority = priority; // 再生中モーションの優先度を設定 + + _fadeWeights.PushBack(0.0f); + + return CubismMotionQueueManager::StartMotion(motion, autoDelete); +} + +csmBool CubismExpressionMotionManager::UpdateMotion(CubismModel* model, csmFloat32 deltaTimeSeconds) +{ + _userTimeSeconds += deltaTimeSeconds; + csmBool updated = false; + csmVector* motions = GetCubismMotionQueueEntries(); + + csmFloat32 expressionWeight = 0.0f; + csmInt32 expressionIndex = 0; + + // ------- 処理を行う -------- + // 既にモーションがあれば終了フラグを立てる + for (csmVector::iterator ite = motions->Begin(); ite != motions->End();) + { + CubismMotionQueueEntry* motionQueueEntry = *ite; + + if (motionQueueEntry == NULL) + { + ite = motions->Erase(ite); // 削除 + continue; + } + + CubismExpressionMotion* expressionMotion = (CubismExpressionMotion*)motionQueueEntry->GetCubismMotion(); + + if (expressionMotion == NULL) + { + CSM_DELETE(motionQueueEntry); + ite = motions->Erase(ite); // 削除 + continue; + } + + csmVector expressionParameters = expressionMotion->GetExpressionParameters(); + if (motionQueueEntry->IsAvailable()) + { + // 再生中のExpressionが参照しているパラメータをすべてリストアップ + for (csmInt32 i = 0; i < (csmInt32)expressionParameters.GetSize(); ++i) + { + if (expressionParameters[i].ParameterId == NULL) + { + continue; + } + + csmInt32 index = -1; + // リストにパラメータIDが存在するか検索 + for (csmInt32 j = 0; j < (csmInt32)_expressionParameterValues->GetSize(); ++j) + { + if (_expressionParameterValues->At(j).ParameterId != expressionParameters[i].ParameterId) + { + continue; + } + + index = j; + break; + } + + if (index >= 0) + { + continue; + } + + // パラメータがリストに存在しないなら新規追加 + ExpressionParameterValue item; + item.ParameterId = expressionParameters[i].ParameterId; + item.AdditiveValue = CubismExpressionMotion::DefaultAdditiveValue; + item.MultiplyValue = CubismExpressionMotion::DefaultMultiplyValue; + item.OverwriteValue = model->GetParameterValue(item.ParameterId); + _expressionParameterValues->PushBack(item); + } + } + + // ------ 値を計算する ------ + expressionMotion->SetupMotionQueueEntry(motionQueueEntry, _userTimeSeconds); + _fadeWeights[expressionIndex] = expressionMotion->UpdateFadeWeight(motionQueueEntry, _userTimeSeconds); + expressionMotion->CalculateExpressionParameters(model, _userTimeSeconds, motionQueueEntry, + _expressionParameterValues, expressionIndex, _fadeWeights[expressionIndex]); + + expressionWeight += expressionMotion->GetFadeInTime() == 0.0f + ? 1.0f + : CubismMath::GetEasingSine((_userTimeSeconds - motionQueueEntry->GetFadeInStartTime()) / expressionMotion->GetFadeInTime()); + + updated = true; + + if (motionQueueEntry->IsTriggeredFadeOut()) + { + // フェードアウト開始 + motionQueueEntry->StartFadeout(motionQueueEntry->GetFadeOutSeconds(), _userTimeSeconds); + } + + ++ite; + ++expressionIndex; + } + + // ----- 最新のExpressionのフェードが完了していればそれ以前を削除する ------ + if (motions->GetSize() > 1) + { + CubismExpressionMotion* expressionMotion = + (CubismExpressionMotion*)(motions->At(motions->GetSize() - 1))->GetCubismMotion(); + + csmFloat32 latestFadeWeight = _fadeWeights[_fadeWeights.GetSize() - 1]; + if (latestFadeWeight >= 1.0f) + { + // 配列の最後の要素は削除しない + for (csmInt32 i = motions->GetSize()-2; i >= 0; i--) + { + CubismMotionQueueEntry* motionQueueEntry = motions->At(i); + CSM_DELETE(motionQueueEntry); + motions->Remove(i); + _fadeWeights.Remove(i); + } + } + } + + if (expressionWeight > 1.0f) + { + expressionWeight = 1.0f; + } + + // モデルに各値を適用 + for (csmInt32 i = 0; i < (csmInt32)_expressionParameterValues->GetSize(); ++i) + { + model->SetParameterValue(_expressionParameterValues->At(i).ParameterId, + (_expressionParameterValues->At(i).OverwriteValue + _expressionParameterValues->At(i).AdditiveValue) * _expressionParameterValues->At(i).MultiplyValue, + expressionWeight); + + _expressionParameterValues->At(i).AdditiveValue = CubismExpressionMotion::DefaultAdditiveValue; + _expressionParameterValues->At(i).MultiplyValue = CubismExpressionMotion::DefaultMultiplyValue; + } + + return updated; +} + +csmFloat32 CubismExpressionMotionManager::GetFadeWeight(csmInt32 index) +{ + return _fadeWeights[index]; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismExpressionMotionManager.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "Model/CubismModel.hpp" +#include "ACubismMotion.hpp" +#include "CubismMotionQueueManager.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief 表情モーションの管理 + * + * 表情モーションの管理を行うクラス。 + */ +class CubismExpressionMotionManager : public CubismMotionQueueManager +{ +public: + /** + * @brief パラメータに適用する表情の値を持たせる構造体 + */ + struct ExpressionParameterValue + { + CubismIdHandle ParameterId; ///< パラメータID + csmFloat32 AdditiveValue; ///< 加算値 + csmFloat32 MultiplyValue; ///< 乗算値 + csmFloat32 OverwriteValue; ///< 上書き値 + }; + + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismExpressionMotionManager(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismExpressionMotionManager(); + + /** + * @brief 再生中のモーションの優先度の取得 + * + * 再生中の表情モーションの優先度の取得する。 + * + * @return 表情モーションの優先度 + */ + csmInt32 GetCurrentPriority() const; + + /** + * @brief 予約中のモーションの優先度の取得 + * + * 予約中の表情モーションの優先度を取得する。 + * + * @return 表情モーションの優先度 + */ + csmInt32 GetReservePriority() const; + + /** + * @brief 予約中のモーションの優先度の設定 + * + * 予約中の表情モーションの優先度を設定する。 + * + * @param[in] priority 優先度 + */ + void SetReservePriority(csmInt32 priority); + + /** + * @brief 優先度を設定して表情モーションの開始 + * + * 優先度を設定して表情モーションを開始する。 + * + * @param[in] motion モーション + * @param[in] autoDelete 再生が終了したモーションのインスタンスを削除するならtrue + * @param[in] priority 優先度 + * @return 開始したモーションの識別番号を返す。個別のモーションが終了したか否かを判定するIsFinished()の引数で使用する。開始できない時は「-1」 + */ + CubismMotionQueueEntryHandle StartMotionPriority(ACubismMotion* motion, csmBool autoDelete, csmInt32 priority); + + /** + * @brief モーションの更新 + * + * 表情モーションを更新して、モデルにパラメータ値を反映する。 + * + * @param[in] model 対象のモデル + * @param[in] deltaTimeSeconds デルタ時間[秒] + * @retval true 更新されている + * @retval false 更新されていない + */ + csmBool UpdateMotion(CubismModel* model, csmFloat32 deltaTimeSeconds); + + /** + * 現在の表情のフェードのウェイト値を取得する。 + * + * @param index 取得する表情モーションのインデックス + * + * @return 表情のフェードのウェイト値 + */ + csmFloat32 GetFadeWeight(csmInt32 index); + +private: + + // モデルに適用する各パラメータの値 + csmVector* _expressionParameterValues; + + // 再生中の表情のウェイト + csmVector _fadeWeights; + + csmInt32 _currentPriority; ///< 現在再生中のモーションの優先度 + csmInt32 _reservePriority; ///< 再生予定のモーションの優先度。再生中は0になる。モーションファイルを別スレッドで読み込むときの機能。 +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismMotion.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismMotion.hpp" +#include +#include "CubismFramework.hpp" +#include "CubismMotionInternal.hpp" +#include "CubismMotionJson.hpp" +#include "CubismMotionQueueManager.hpp" +#include "CubismMotionQueueEntry.hpp" +#include "Math/CubismMath.hpp" +#include "Type/csmVector.hpp" +#include "Id/CubismIdManager.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +namespace { + +const csmChar* EffectNameEyeBlink = "EyeBlink"; +const csmChar* EffectNameLipSync = "LipSync"; +const csmChar* TargetNameModel = "Model"; +const csmChar* TargetNameParameter = "Parameter"; +const csmChar* TargetNamePartOpacity = "PartOpacity"; + +// Id +const csmChar* IdNameOpacity = "Opacity"; + +/** +* Cubism SDK R2 以前のモーションを再現させるなら true 、アニメータのモーションを正しく再現するなら false 。 +*/ +const csmBool UseOldBeziersCurveMotion = false; + +CubismMotionPoint LerpPoints(const CubismMotionPoint a, const CubismMotionPoint b, const csmFloat32 t) +{ + CubismMotionPoint result; + + result.Time = a.Time + ((b.Time - a.Time) * t); + result.Value = a.Value + ((b.Value - a.Value) * t); + + return result; +} + +csmFloat32 LinearEvaluate(const CubismMotionPoint* points, const csmFloat32 time) +{ + csmFloat32 t = (time - points[0].Time) / (points[1].Time - points[0].Time); + + if (t < 0.0f) + { + t = 0.0f; + } + + return points[0].Value + ((points[1].Value - points[0].Value) * t); +} + +csmFloat32 BezierEvaluate(const CubismMotionPoint* points, const csmFloat32 time) +{ + csmFloat32 t = (time - points[0].Time) / (points[3].Time - points[0].Time); + + if (t < 0.0f) + { + t = 0.0f; + } + + const CubismMotionPoint p01 = LerpPoints(points[0], points[1], t); + const CubismMotionPoint p12 = LerpPoints(points[1], points[2], t); + const CubismMotionPoint p23 = LerpPoints(points[2], points[3], t); + + const CubismMotionPoint p012 = LerpPoints(p01, p12, t); + const CubismMotionPoint p123 = LerpPoints(p12, p23, t); + + return LerpPoints(p012, p123, t).Value; +} + +csmFloat32 BezierEvaluateBinarySearch(const CubismMotionPoint* points, const csmFloat32 time) +{ + const csmFloat32 x_error = 0.01f; + + const csmFloat32 x = time; + csmFloat32 x1 = points[0].Time; + csmFloat32 x2 = points[3].Time; + csmFloat32 cx1 = points[1].Time; + csmFloat32 cx2 = points[2].Time; + + csmFloat32 ta = 0.0f; + csmFloat32 tb = 1.0f; + csmFloat32 t = 0.0f; + int i = 0; + + for (csmBool var33 = true; i < 20; ++i) { + if (x < x1 + x_error) { + t = ta; + break; + } + + if (x2 - x_error < x) { + t = tb; + break; + } + + csmFloat32 centerx = (cx1 + cx2) * 0.5f; + cx1 = (x1 + cx1) * 0.5f; + cx2 = (x2 + cx2) * 0.5f; + csmFloat32 ctrlx12 = (cx1 + centerx) * 0.5f; + csmFloat32 ctrlx21 = (cx2 + centerx) * 0.5f; + centerx = (ctrlx12 + ctrlx21) * 0.5f; + if (x < centerx) { + tb = (ta + tb) * 0.5f; + if (centerx - x_error < x) { + t = tb; + break; + } + + x2 = centerx; + cx2 = ctrlx12; + } + else { + ta = (ta + tb) * 0.5f; + if (x < centerx + x_error) { + t = ta; + break; + } + + x1 = centerx; + cx1 = ctrlx21; + } + } + + if (i == 20) { + t = (ta + tb) * 0.5f; + } + + if (t < 0.0f) + { + t = 0.0f; + } + if (t > 1.0f) + { + t = 1.0f; + } + + const CubismMotionPoint p01 = LerpPoints(points[0], points[1], t); + const CubismMotionPoint p12 = LerpPoints(points[1], points[2], t); + const CubismMotionPoint p23 = LerpPoints(points[2], points[3], t); + + const CubismMotionPoint p012 = LerpPoints(p01, p12, t); + const CubismMotionPoint p123 = LerpPoints(p12, p23, t); + + return LerpPoints(p012, p123, t).Value; +} + +csmFloat32 BezierEvaluateCardanoInterpretation(const CubismMotionPoint* points, const csmFloat32 time) +{ + const csmFloat32 x = time; + csmFloat32 x1 = points[0].Time; + csmFloat32 x2 = points[3].Time; + csmFloat32 cx1 = points[1].Time; + csmFloat32 cx2 = points[2].Time; + + csmFloat32 a = x2 - 3.0f * cx2 + 3.0f * cx1 - x1; + csmFloat32 b = 3.0f * cx2 - 6.0f * cx1 + 3.0f * x1; + csmFloat32 c = 3.0f * cx1 - 3.0f * x1; + csmFloat32 d = x1 - x; + + csmFloat32 t = CubismMath::CardanoAlgorithmForBezier(a, b, c, d); + + const CubismMotionPoint p01 = LerpPoints(points[0], points[1], t); + const CubismMotionPoint p12 = LerpPoints(points[1], points[2], t); + const CubismMotionPoint p23 = LerpPoints(points[2], points[3], t); + + const CubismMotionPoint p012 = LerpPoints(p01, p12, t); + const CubismMotionPoint p123 = LerpPoints(p12, p23, t); + + return LerpPoints(p012, p123, t).Value; +} + +csmFloat32 SteppedEvaluate(const CubismMotionPoint* points, const csmFloat32 time) +{ + return points[0].Value; +} + +csmFloat32 InverseSteppedEvaluate(const CubismMotionPoint* points, const csmFloat32 time) +{ + return points[1].Value; +} + +csmFloat32 EvaluateCurve(const CubismMotionData* motionData, const csmInt32 index, csmFloat32 time) +{ + // Find segment to evaluate. + const CubismMotionCurve& curve = motionData->Curves[index]; + + csmInt32 target = -1; + const csmInt32 totalSegmentCount = curve.BaseSegmentIndex + curve.SegmentCount; + csmInt32 pointPosition = 0; + for (csmInt32 i = curve.BaseSegmentIndex; i < totalSegmentCount; ++i) + { + // Get first point of next segment. + pointPosition = motionData->Segments[i].BasePointIndex + + (motionData->Segments[i].SegmentType == CubismMotionSegmentType_Bezier + ? 3 + : 1); + + + // Break if time lies within current segment. + if (motionData->Points[pointPosition].Time > time) + { + target = i; + break; + } + } + + + if (target == -1) + { + return motionData->Points[pointPosition].Value; + } + + + const CubismMotionSegment& segment = motionData->Segments[target]; + + return segment.Evaluate(&motionData->Points[segment.BasePointIndex], time); +} + +} + +CubismMotion::CubismMotion() + : _sourceFrameRate(30.0f) + , _loopDurationSeconds(-1.0f) + , _isLoop(false) // trueから false へデフォルトを変更 + , _isLoopFadeIn(true) // ループ時にフェードインが有効かどうかのフラグ + , _lastWeight(0.0f) + , _motionData(NULL) + , _modelCurveIdEyeBlink(NULL) + , _modelCurveIdLipSync(NULL) + , _modelCurveIdOpacity(NULL) + , _modelOpacity(1.0f) +{ } + +CubismMotion::~CubismMotion() +{ + CSM_DELETE(_motionData); +} + +CubismMotion* CubismMotion::Create(const csmByte* buffer, csmSizeInt size, FinishedMotionCallback onFinishedMotionHandler) +{ + CubismMotion* ret = CSM_NEW CubismMotion(); + + ret->Parse(buffer, size); + ret->_sourceFrameRate = ret->_motionData->Fps; + ret->_loopDurationSeconds = ret->_motionData->Duration; + ret->_onFinishedMotion = onFinishedMotionHandler; + + // NOTE: Editorではループありのモーション書き出しは非対応 + // ret->_loop = (ret->_motionData->Loop > 0); + + return ret; +} + +csmFloat32 CubismMotion::GetDuration() +{ + return _isLoop ? -1.0f : _loopDurationSeconds; +} + +void CubismMotion::DoUpdateParameters(CubismModel* model, csmFloat32 userTimeSeconds, csmFloat32 fadeWeight, CubismMotionQueueEntry* motionQueueEntry) +{ + if (_modelCurveIdEyeBlink == NULL) + { + _modelCurveIdEyeBlink = CubismFramework::GetIdManager()->GetId(EffectNameEyeBlink); + } + + if (_modelCurveIdLipSync == NULL) + { + _modelCurveIdLipSync = CubismFramework::GetIdManager()->GetId(EffectNameLipSync); + } + + if (_modelCurveIdOpacity == NULL) + { + _modelCurveIdOpacity = CubismFramework::GetIdManager()->GetId(IdNameOpacity); + } + + csmFloat32 timeOffsetSeconds = userTimeSeconds - motionQueueEntry->GetStartTime(); + + if (timeOffsetSeconds < 0.0f) + { + timeOffsetSeconds = 0.0f; // エラー回避 + } + + csmFloat32 lipSyncValue = FLT_MAX; + csmFloat32 eyeBlinkValue = FLT_MAX; + + //まばたき、リップシンクのうちモーションの適用を検出するためのビット(maxFlagCount個まで + const csmInt32 MaxTargetSize = 64; + csmUint64 lipSyncFlags = 0ULL; + csmUint64 eyeBlinkFlags = 0ULL; + + //瞬き、リップシンクのターゲット数が上限を超えている場合 + if (_eyeBlinkParameterIds.GetSize() > MaxTargetSize) + { + CubismLogDebug("too many eye blink targets : %d", _eyeBlinkParameterIds.GetSize()); + } + if (_lipSyncParameterIds.GetSize() > MaxTargetSize) + { + CubismLogDebug("too many lip sync targets : %d", _lipSyncParameterIds.GetSize()); + } + + const csmFloat32 tmpFadeIn = (_fadeInSeconds <= 0.0f) + ? 1.0f + : CubismMath::GetEasingSine((userTimeSeconds - motionQueueEntry->GetFadeInStartTime()) / _fadeInSeconds); + + const csmFloat32 tmpFadeOut = (_fadeOutSeconds <= 0.0f || motionQueueEntry->GetEndTime() < 0.0f) + ? 1.0f + : CubismMath::GetEasingSine((motionQueueEntry->GetEndTime() - userTimeSeconds) / _fadeOutSeconds); + + csmFloat32 value; + csmInt32 c, parameterIndex; + + // 'Repeat' time as necessary. + csmFloat32 time = timeOffsetSeconds; + + if (_isLoop) + { + while (time > _motionData->Duration) + { + time -= _motionData->Duration; + } + } + + csmVector& curves = _motionData->Curves; + + // Evaluate model curves. + for (c = 0; c < _motionData->CurveCount && curves[c].Type == CubismMotionCurveTarget_Model; ++c) + { + // Evaluate curve and call handler. + value = EvaluateCurve(_motionData, c, time); + + if (curves[c].Id == _modelCurveIdEyeBlink) + { + eyeBlinkValue = value; + } + else if (curves[c].Id == _modelCurveIdLipSync) + { + lipSyncValue = value; + } + else if (curves[c].Id == _modelCurveIdOpacity) + { + _modelOpacity = value; + + // ------ 不透明度の値が存在すれば反映する ------ + model->SetModelOpacity(GetModelOpacityValue()); + } + } + + csmInt32 parameterMotionCurveCount = 0; + + for (; c < _motionData->CurveCount && curves[c].Type == CubismMotionCurveTarget_Parameter; ++c) + { + parameterMotionCurveCount++; + + // Find parameter index. + parameterIndex = model->GetParameterIndex(curves[c].Id); + + // Skip curve evaluation if no value in sink. + if (parameterIndex == -1) + { + continue; + } + + const csmFloat32 sourceValue = model->GetParameterValue(parameterIndex); + + // Evaluate curve and apply value. + value = EvaluateCurve(_motionData, c, time); + + if (eyeBlinkValue != FLT_MAX) + { + for (csmUint32 i = 0; i < _eyeBlinkParameterIds.GetSize() && i < MaxTargetSize; ++i) + { + if (_eyeBlinkParameterIds[i] == curves[c].Id) + { + value *= eyeBlinkValue; + eyeBlinkFlags |= 1ULL << i; + break; + } + } + } + + if (lipSyncValue != FLT_MAX) + { + for (csmUint32 i = 0; i < _lipSyncParameterIds.GetSize() && i < MaxTargetSize; ++i) + { + if (_lipSyncParameterIds[i] == curves[c].Id) + { + value += lipSyncValue; + lipSyncFlags |= 1ULL << i; + break; + } + } + } + + csmFloat32 v; + // パラメータごとのフェード + if (curves[c].FadeInTime < 0.0f && curves[c].FadeOutTime < 0.0f) + { + //モーションのフェードを適用 + v = sourceValue + (value - sourceValue) * fadeWeight; + } + else + { + // パラメータに対してフェードインかフェードアウトが設定してある場合はそちらを適用 + csmFloat32 fin; + csmFloat32 fout; + + if (curves[c].FadeInTime < 0.0f) + { + fin = tmpFadeIn; + } + else + { + fin = curves[c].FadeInTime == 0.0f + ? 1.0f + : CubismMath::GetEasingSine((userTimeSeconds - motionQueueEntry->GetFadeInStartTime()) / curves[c].FadeInTime); + } + + if (curves[c].FadeOutTime < 0.0f) + { + fout = tmpFadeOut; + } + else + { + fout = (curves[c].FadeOutTime == 0.0f || motionQueueEntry->GetEndTime() < 0.0f) + ? 1.0f + : CubismMath::GetEasingSine((motionQueueEntry->GetEndTime() - userTimeSeconds) / curves[c].FadeOutTime ); + } + + const csmFloat32 paramWeight = _weight * fin * fout; + + // パラメータごとのフェードを適用 + v = sourceValue + (value - sourceValue) * paramWeight; + } + + model->SetParameterValue(parameterIndex, v); + } + + { + if (eyeBlinkValue != FLT_MAX) + { + for (csmUint32 i = 0; i < _eyeBlinkParameterIds.GetSize() && i < MaxTargetSize; ++i) + { + const csmFloat32 sourceValue = model->GetParameterValue(_eyeBlinkParameterIds[i]); + //モーションでの上書きがあった時にはまばたきは適用しない + if ((eyeBlinkFlags >> i) & 0x01) + { + continue; + } + + const csmFloat32 v = sourceValue + (eyeBlinkValue - sourceValue) * fadeWeight; + + model->SetParameterValue(_eyeBlinkParameterIds[i], v); + } + } + + if (lipSyncValue != FLT_MAX) + { + for (csmUint32 i = 0; i < _lipSyncParameterIds.GetSize() && i < MaxTargetSize; ++i) + { + const csmFloat32 sourceValue = model->GetParameterValue(_lipSyncParameterIds[i]); + //モーションでの上書きがあった時にはリップシンクは適用しない + if ((lipSyncFlags >> i) & 0x01) + { + continue; + } + + const csmFloat32 v = sourceValue + (lipSyncValue - sourceValue) * fadeWeight; + + model->SetParameterValue(_lipSyncParameterIds[i], v); + } + } + } + + for (; c < _motionData->CurveCount && curves[c].Type == CubismMotionCurveTarget_PartOpacity; ++c) + { + // Find parameter index. + parameterIndex = model->GetParameterIndex(curves[c].Id); + + // Skip curve evaluation if no value in sink. + if (parameterIndex == -1) + { + continue; + } + + // Evaluate curve and apply value. + value = EvaluateCurve(_motionData, c, time); + + model->SetParameterValue(parameterIndex, value); + } + + if (timeOffsetSeconds >= _motionData->Duration) + { + if (_isLoop) + { + motionQueueEntry->SetStartTime(userTimeSeconds); //最初の状態へ + if (_isLoopFadeIn) + { + //ループ中でループ用フェードインが有効のときは、フェードイン設定し直し + motionQueueEntry->SetFadeInStartTime(userTimeSeconds); + } + } + else + { + if (this->_onFinishedMotion != NULL) + { + this->_onFinishedMotion(this); + } + + motionQueueEntry->IsFinished(true); + } + } + + _lastWeight = fadeWeight; +} + +void CubismMotion::Parse(const csmByte* motionJson, const csmSizeInt size) +{ + _motionData = CSM_NEW CubismMotionData; + + CubismMotionJson* json = CSM_NEW CubismMotionJson(motionJson, size); + + if (!json->IsValid()) + { + CSM_DELETE(json); + return; + } + + _motionData->Duration = json->GetMotionDuration(); + _motionData->Loop = json->IsMotionLoop(); + _motionData->CurveCount = json->GetMotionCurveCount(); + _motionData->Fps = json->GetMotionFps(); + _motionData->EventCount = json->GetEventCount(); + + csmBool areBeziersRestricted = json->GetEvaluationOptionFlag( EvaluationOptionFlag_AreBeziersRestricted ); + + if (json->IsExistMotionFadeInTime()) + { + _fadeInSeconds = (json->GetMotionFadeInTime() < 0.0f) + ? 1.0f + : json->GetMotionFadeInTime(); + } + else + { + _fadeInSeconds = 1.0f; + } + + if (json->IsExistMotionFadeOutTime()) + { + _fadeOutSeconds = (json->GetMotionFadeOutTime() < 0.0f) + ? 1.0f + : json->GetMotionFadeOutTime(); + } + else + { + _fadeOutSeconds = 1.0f; + } + + _motionData->Curves.UpdateSize(_motionData->CurveCount, CubismMotionCurve(), true); + _motionData->Segments.UpdateSize(json->GetMotionTotalSegmentCount(), CubismMotionSegment(), true); + _motionData->Points.UpdateSize(json->GetMotionTotalPointCount(), CubismMotionPoint(), true); + _motionData->Events.UpdateSize(_motionData->EventCount, CubismMotionEvent(), true); + + csmInt32 totalPointCount = 0; + csmInt32 totalSegmentCount = 0; + + // Curves + for (csmInt32 curveCount = 0; curveCount < _motionData->CurveCount; ++curveCount) + { + if (strcmp(json->GetMotionCurveTarget(curveCount), TargetNameModel) == 0) + { + _motionData->Curves[curveCount].Type = CubismMotionCurveTarget_Model; + } + else if (strcmp(json->GetMotionCurveTarget(curveCount), TargetNameParameter) == 0) + { + _motionData->Curves[curveCount].Type = CubismMotionCurveTarget_Parameter; + } + else if (strcmp(json->GetMotionCurveTarget(curveCount), TargetNamePartOpacity) == 0) + { + _motionData->Curves[curveCount].Type = CubismMotionCurveTarget_PartOpacity; + } + else + { + CubismLogWarning("Warning : Unable to get segment type from Curve! The number of \"CurveCount\" may be incorrect!"); + } + + _motionData->Curves[curveCount].Id = json->GetMotionCurveId(curveCount); + + _motionData->Curves[curveCount].BaseSegmentIndex = totalSegmentCount; + + _motionData->Curves[curveCount].FadeInTime = + (json->IsExistMotionCurveFadeInTime(curveCount)) + ? json->GetMotionCurveFadeInTime(curveCount) + : -1.0f ; + _motionData->Curves[curveCount].FadeOutTime = + (json->IsExistMotionCurveFadeOutTime(curveCount)) + ? json->GetMotionCurveFadeOutTime(curveCount) + : -1.0f; + + // Segments + for (csmInt32 segmentPosition = 0; segmentPosition < json->GetMotionCurveSegmentCount(curveCount);) + { + if (segmentPosition == 0) + { + _motionData->Segments[totalSegmentCount].BasePointIndex = totalPointCount; + + _motionData->Points[totalPointCount].Time = json->GetMotionCurveSegment(curveCount, segmentPosition); + _motionData->Points[totalPointCount].Value = json->GetMotionCurveSegment(curveCount, segmentPosition + 1); + + totalPointCount += 1; + segmentPosition += 2; + } + else + { + _motionData->Segments[totalSegmentCount].BasePointIndex = totalPointCount - 1; + } + + const csmInt32 segment = static_cast(json->GetMotionCurveSegment(curveCount, segmentPosition)); + + switch (segment) + { + case CubismMotionSegmentType_Linear: { + _motionData->Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType_Linear; + _motionData->Segments[totalSegmentCount].Evaluate = LinearEvaluate; + + _motionData->Points[totalPointCount].Time = json->GetMotionCurveSegment(curveCount, (segmentPosition + 1)); + _motionData->Points[totalPointCount].Value = json->GetMotionCurveSegment(curveCount, (segmentPosition + 2)); + + totalPointCount += 1; + segmentPosition += 3; + + break; + } + case CubismMotionSegmentType_Bezier: { + _motionData->Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType_Bezier; + if (areBeziersRestricted || UseOldBeziersCurveMotion) { + _motionData->Segments[totalSegmentCount].Evaluate = BezierEvaluate; + } + else + { + _motionData->Segments[totalSegmentCount].Evaluate = BezierEvaluateCardanoInterpretation; + } + + _motionData->Points[totalPointCount].Time = json->GetMotionCurveSegment(curveCount, (segmentPosition + 1)); + _motionData->Points[totalPointCount].Value = json->GetMotionCurveSegment(curveCount, (segmentPosition + 2)); + + _motionData->Points[totalPointCount + 1].Time = json->GetMotionCurveSegment(curveCount, (segmentPosition + 3)); + _motionData->Points[totalPointCount + 1].Value = json->GetMotionCurveSegment(curveCount, (segmentPosition + 4)); + + _motionData->Points[totalPointCount + 2].Time = json->GetMotionCurveSegment(curveCount, (segmentPosition + 5)); + _motionData->Points[totalPointCount + 2].Value = json->GetMotionCurveSegment(curveCount, (segmentPosition + 6)); + + totalPointCount += 3; + segmentPosition += 7; + + break; + } + case CubismMotionSegmentType_Stepped: { + _motionData->Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType_Stepped; + _motionData->Segments[totalSegmentCount].Evaluate = SteppedEvaluate; + + _motionData->Points[totalPointCount].Time = json->GetMotionCurveSegment(curveCount, (segmentPosition + 1)); + _motionData->Points[totalPointCount].Value = json->GetMotionCurveSegment(curveCount, (segmentPosition + 2)); + + totalPointCount += 1; + segmentPosition += 3; + + break; + } + case CubismMotionSegmentType_InverseStepped: { + _motionData->Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType_InverseStepped; + _motionData->Segments[totalSegmentCount].Evaluate = InverseSteppedEvaluate; + + _motionData->Points[totalPointCount].Time = json->GetMotionCurveSegment(curveCount, (segmentPosition + 1)); + _motionData->Points[totalPointCount].Value = json->GetMotionCurveSegment(curveCount, (segmentPosition + 2)); + + totalPointCount += 1; + segmentPosition += 3; + + break; + } + default: { + CSM_ASSERT(0); + break; + } + } + + ++_motionData->Curves[curveCount].SegmentCount; + ++totalSegmentCount; + } + } + + + for (csmInt32 userdatacount = 0; userdatacount < json->GetEventCount(); ++userdatacount) + { + _motionData->Events[userdatacount].FireTime = json->GetEventTime(userdatacount); + _motionData->Events[userdatacount].Value = json->GetEventValue(userdatacount); + } + + CSM_DELETE(json); +} + +void CubismMotion::SetParameterFadeInTime(CubismIdHandle parameterId, csmFloat32 value) +{ + csmVector& curves = _motionData->Curves; + + for (csmInt16 i = 0; i < _motionData->CurveCount; ++i) + { + if (parameterId == curves[i].Id) + { + curves[i].FadeInTime = value; + return; + } + } +} + +void CubismMotion::SetParameterFadeOutTime(CubismIdHandle parameterId, csmFloat32 value) +{ + csmVector& curves = _motionData->Curves; + + for (csmInt16 i = 0; i < _motionData->CurveCount; ++i) + { + if (parameterId == curves[i].Id) + { + curves[i].FadeOutTime = value; + return; + } + } +} + +csmFloat32 CubismMotion::GetParameterFadeInTime(CubismIdHandle parameterId) const +{ + csmVector& curves = _motionData->Curves; + + for (csmInt16 i = 0; i < _motionData->CurveCount; ++i) + { + if (parameterId == curves[i].Id) + { + return curves[i].FadeInTime; + } + } + + return -1; +} + +csmFloat32 CubismMotion::GetParameterFadeOutTime(CubismIdHandle parameterId) const +{ + csmVector& curves = _motionData->Curves; + + for (csmInt16 i = 0; i < _motionData->CurveCount; ++i) + { + if (parameterId == curves[i].Id) + { + return curves[i].FadeOutTime; + } + } + + return -1; +} + +void CubismMotion::IsLoop(csmBool loop) +{ + this->_isLoop = loop; +} + +csmBool CubismMotion::IsLoop() const +{ + return this->_isLoop; +} + +void CubismMotion::IsLoopFadeIn(csmBool loopFadeIn) +{ + this->_isLoopFadeIn = loopFadeIn; +} + +csmBool CubismMotion::IsLoopFadeIn() const +{ + return this->_isLoopFadeIn; +} + +csmFloat32 CubismMotion::GetLoopDuration() +{ + return _loopDurationSeconds; +} + +void CubismMotion::SetEffectIds(const csmVector& eyeBlinkParameterIds, const csmVector& lipSyncParameterIds) +{ + _eyeBlinkParameterIds = eyeBlinkParameterIds; + _lipSyncParameterIds = lipSyncParameterIds; +} + +const csmVector& CubismMotion::GetFiredEvent(csmFloat32 beforeCheckTimeSeconds, csmFloat32 motionTimeSeconds) +{ + _firedEventValues.UpdateSize(0); + /// イベントの発火チェック + for (csmInt32 u = 0; u < _motionData->EventCount; ++u) + { + if ((_motionData->Events[u].FireTime >beforeCheckTimeSeconds) && + (_motionData->Events[u].FireTime <= motionTimeSeconds)) + { + _firedEventValues.PushBack(&_motionData->Events[u].Value); + } + } + + return _firedEventValues; +} + +csmBool CubismMotion::IsExistModelOpacity() const +{ + for (csmInt32 i = 0; i < _motionData->CurveCount; i++) + { + CubismMotionCurve curve = _motionData->Curves[i]; + + if (curve.Type != CubismMotionCurveTarget_Model) + { + continue; + } + + if (strcmp(curve.Id->GetString().GetRawString(), IdNameOpacity) == 0) + { + return true; + } + } + + return false; +} + +csmInt32 CubismMotion::GetModelOpacityIndex() const +{ + if (IsExistModelOpacity()) + { + for (csmInt32 i = 0; i < _motionData->CurveCount; i++) + { + CubismMotionCurve curve = _motionData->Curves[i]; + + if (curve.Type != CubismMotionCurveTarget_Model) + { + continue; + } + + if (strcmp(curve.Id->GetString().GetRawString(), IdNameOpacity) == 0) + { + return i; + } + } + } + + return -1; +} + +CubismIdHandle CubismMotion::GetModelOpacityId(csmInt32 index) +{ + if (index != -1) + { + CubismMotionCurve curve = _motionData->Curves[index]; + + if (curve.Type == CubismMotionCurveTarget_Model) + { + if (strcmp(curve.Id->GetString().GetRawString(), IdNameOpacity) == 0) + { + return CubismFramework::GetIdManager()->GetId(curve.Id->GetString().GetRawString()); + } + } + } + + return NULL; +} + +csmFloat32 CubismMotion::GetModelOpacityValue() const +{ + return _modelOpacity; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismMotion.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "ACubismMotion.hpp" +#include "Type/CubismBasicType.hpp" +#include "Type/csmVector.hpp" +#include "Id/CubismId.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +class CubismMotionQueueEntry; +struct CubismMotionData; + +/** + * @brief モーションクラス + * + * モーションのクラス。 + */ +class CubismMotion : public ACubismMotion +{ +public: + /** + * @brief インスタンスの生成 + * + * インスタンスを作成する。 + * + * @param[in] buffer motion3.jsonが読み込まれているバッファ + * @param[in] size バッファのサイズ + * @param[in] onFinishedMotionHandler モーション再生終了時に呼び出されるコールバック関数。NULLの場合、呼び出されない。 + * @return 作成されたインスタンス + */ + static CubismMotion* Create(const csmByte* buffer, csmSizeInt size, FinishedMotionCallback onFinishedMotionHandler = NULL); + + /** + * @brief モデルのパラメータの更新の実行 + * + * モデルのパラメータ更新を実行する。 + * + * @param[in] model 対象のモデル + * @param[in] userTimeSeconds 現在の時刻[秒] + * @param[in] fadeWeight モーションの重み + * @param[in] motionQueueEntry CubismMotionQueueManagerで管理されているモーション + */ + virtual void DoUpdateParameters(CubismModel* model, csmFloat32 userTimeSeconds, csmFloat32 fadeWeight, CubismMotionQueueEntry* motionQueueEntry); + + /** + * @brirf ループ情報の設定 + * + * ループ情報を設定する。 + * + * @param[in] loop ループ情報 + */ + void IsLoop(csmBool loop); + + /** + * @brief ループ情報の取得 + * + * モーションがループするかどうか? + * + * @retval true ループする + * @retval false ループしない + */ + csmBool IsLoop() const; + + /** + * @brief ループ時のフェードイン情報の設定 + * + * ループ時のフェードイン情報を設定する。 + * + * @param[in] loopFadeIn ループ時のフェードイン情報 + */ + void IsLoopFadeIn(csmBool loopFadeIn); + + /** + * @brief ループ時のフェードイン情報の取得 + * + * ループ時にフェードインするかどうか? + * + * @retval true する + * @retval false しない + */ + csmBool IsLoopFadeIn() const; + + /** + * @brief モーションの長さの取得 + * + * モーションの長さを取得する。 + * + * @return モーションの長さ[秒] + */ + virtual csmFloat32 GetDuration(); + + /** + * @brief モーションのループ時の長さの取得 + * + * モーションのループ時の長さを取得する。 + * + * @return モーションのループ時の長さ[秒] + */ + virtual csmFloat32 GetLoopDuration(); + + /** + * @brief パラメータに対するフェードインの時間の設定 + * + * パラメータに対するフェードインの時間を設定する。 + * + * @param[in] parameterId パラメータID + * @param[in] value フェードインにかかる時間[秒] + */ + void SetParameterFadeInTime(CubismIdHandle parameterId, csmFloat32 value); + + /** + * @brief パラメータに対するフェードアウトの時間の設定 + * + * パラメータに対するフェードアウトの時間を設定する。 + * + * @param[in] parameterId パラメータID + * @param[in] value フェードアウトにかかる時間[秒] + */ + void SetParameterFadeOutTime(CubismIdHandle parameterId, csmFloat32 value); + + /** + * @brief パラメータに対するフェードインの時間の取得 + * + * パラメータに対するフェードインの時間を取得する。 + * + * @param[in] parameterId パラメータID + * @return フェードインにかかる時間[秒] + */ + csmFloat32 GetParameterFadeInTime(CubismIdHandle parameterId) const; + + /** + * @brief パラメータに対するフェードアウトの時間の取得 + * + * パラメータに対するフェードアウトの時間を取得する。 + * + * @param[in] parameterId パラメータID + * @return フェードアウトにかかる時間[秒] + */ + csmFloat32 GetParameterFadeOutTime(CubismIdHandle parameterId) const; + + /** + * @brief 自動エフェクトがかかっているパラメータIDリストの設定 + * + * 自動エフェクトがかかっているパラメータIDリストを設定する。 + * + * @param[in] eyeBlinkParameterIds 自動まばたきがかかっているパラメータIDのリスト + * @param[in] lipSyncParameterIds リップシンクがかかっているパラメータIDのリスト + */ + void SetEffectIds(const csmVector& eyeBlinkParameterIds, const csmVector& lipSyncParameterIds); + + /** + * @brief モデルのパラメータ更新 + * + * イベント発火のチェック。 + * 入力する時間は呼ばれるモーションタイミングを0とした秒数で行う。 + * + * @param[in] beforeCheckTimeSeconds 前回のイベントチェック時間[秒] + * @param[in] motionTimeSeconds 今回の再生時間[秒] + */ + virtual const csmVector& GetFiredEvent(csmFloat32 beforeCheckTimeSeconds, csmFloat32 motionTimeSeconds); + + /** + * @brief 透明度のカーブが存在するかどうかを確認する + * + * @retval true -> キーが存在する + * @retval false -> キーが存在しない + */ + csmBool IsExistModelOpacity() const; + + /** + * @brief 透明度のカーブのインデックスを返す + * + * + * @return success:透明度のカーブのインデックス + */ + csmInt32 GetModelOpacityIndex() const; + + /** + * @brief 透明度のIdを返す + * + * + * @return success:透明度のId + */ + CubismIdHandle GetModelOpacityId(csmInt32 index); + +protected: + /** + * + * @brief 透明度の値を返す + * + * @param[in] motionTimeSeconds 現在の再生時間[秒](未利用) + * + * @return success:モーションの現在時間のOpacityの値 + * + * @note 更新後の値を取るにはUpdateParameters() の後に呼び出す。 + */ + csmFloat32 GetModelOpacityValue() const; + +private: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismMotion(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismMotion(); + + // Prevention of copy Constructor + CubismMotion(const CubismMotion&); + CubismMotion& operator=(const CubismMotion&); + + /** + * @brief motion3.jsonのパース + * + * motion3.jsonをパースする。 + * + * @param[in] motionJson motion3.jsonが読み込まれているバッファ + * @param[in] size バッファのサイズ + */ + void Parse(const csmByte* motionJson, const csmSizeInt size); + + csmFloat32 _sourceFrameRate; ///< ロードしたファイルのFPS。記述が無ければデフォルト値15fpsとなる + csmFloat32 _loopDurationSeconds; ///< mtnファイルで定義される一連のモーションの長さ + csmBool _isLoop; ///< ループするか? + csmBool _isLoopFadeIn; ///< ループ時にフェードインが有効かどうかのフラグ。初期値では有効。 + csmFloat32 _lastWeight; ///< 最後に設定された重み + + CubismMotionData* _motionData; ///< 実際のモーションデータ本体 + + csmVector _eyeBlinkParameterIds; ///< 自動まばたきを適用するパラメータIDハンドルのリスト。 モデル(モデルセッティング)とパラメータを対応付ける。 + csmVector _lipSyncParameterIds; ///< リップシンクを適用するパラメータIDハンドルのリスト。 モデル(モデルセッティング)とパラメータを対応付ける。 + + CubismIdHandle _modelCurveIdEyeBlink; ///< モデルが持つ自動まばたき用パラメータIDのハンドル。 モデルとモーションを対応付ける。 + CubismIdHandle _modelCurveIdLipSync; ///< モデルが持つリップシンク用パラメータIDのハンドル。 モデルとモーションを対応付ける。 + CubismIdHandle _modelCurveIdOpacity; ///< モデルが持つ不透明度用パラメータIDのハンドル。 モデルとモーションを対応付ける。 + + csmFloat32 _modelOpacity; ///< モーションから取得した不透明度 +}; + +}}} + +//--------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismMotionInternal.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismFramework.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief モーションカーブの種類 + * + * モーションカーブの種類。 + */ +enum CubismMotionCurveTarget +{ + CubismMotionCurveTarget_Model, ///< モデルに対して + CubismMotionCurveTarget_Parameter, ///< パラメータに対して + CubismMotionCurveTarget_PartOpacity ///< パーツの不透明度に対して +}; + + +/** + * @brief モーションカーブのセグメントの種類 + * + * モーションカーブのセグメントの種類。 + */ +enum CubismMotionSegmentType +{ + CubismMotionSegmentType_Linear = 0, ///< リニア + CubismMotionSegmentType_Bezier = 1, ///< ベジェ曲線 + CubismMotionSegmentType_Stepped = 2, ///< ステップ + CubismMotionSegmentType_InverseStepped = 3 ///< インバースステップ +}; + +/** + * @brief モーションカーブの制御点 + * + * モーションカーブの制御点。 + */ +struct CubismMotionPoint +{ + CubismMotionPoint() + : Time(0.0f) + , Value(0.0f) + { } + + csmFloat32 Time; ///< 時間[秒] + csmFloat32 Value; ///< 値 +}; + +/** + * @brief モーションカーブのセグメントの評価関数 + * + * モーションカーブのセグメントの評価関数。 + * + * @param[in] points モーションカーブの制御点リスト + * @param[in] time 評価する時間[秒] + */ +typedef csmFloat32 (*csmMotionSegmentEvaluationFunction)(const CubismMotionPoint* points, const csmFloat32 time); + + +/** + * @brief モーションカーブのセグメント + * + * モーションカーブのセグメント。 + */ +struct CubismMotionSegment +{ + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismMotionSegment() + : Evaluate(NULL) + , BasePointIndex(0) + , SegmentType(0) + { } + + csmMotionSegmentEvaluationFunction Evaluate; ///< 使用する評価関数 + csmInt32 BasePointIndex; ///< 最初のセグメントへのインデックス + csmInt32 SegmentType; ///< セグメントの種類 +}; + +/** + * @brief モーションカーブ + * + * モーションカーブ。 + */ +struct CubismMotionCurve +{ + CubismMotionCurve() + : Type(CubismMotionCurveTarget_Model) + , SegmentCount(0) + , BaseSegmentIndex(0) + , FadeInTime(0.0f) + , FadeOutTime(0.0f) + { } + + CubismMotionCurveTarget Type; ///< カーブの種類 + CubismIdHandle Id; ///< カーブのID + csmInt32 SegmentCount; ///< セグメントの個数 + csmInt32 BaseSegmentIndex; ///< 最初のセグメントのインデックス + csmFloat32 FadeInTime; ///< フェードインにかかる時間[秒] + csmFloat32 FadeOutTime; ///< フェードアウトにかかる時間[秒] +}; + +/** +* @brief イベント +* +* イベント。 +*/ +struct CubismMotionEvent +{ + CubismMotionEvent() + : FireTime(0.0f) + { } + + csmFloat32 FireTime; + csmString Value; +}; + +/** + * @brief モーションデータ + * + * モーションデータ。 + */ +struct CubismMotionData +{ + CubismMotionData() + : Duration(0.0f) + , Loop(0) + , CurveCount(0) + , EventCount(0) + , Fps(0.0f) + { } + + csmFloat32 Duration; ///< モーションの長さ[秒] + csmInt16 Loop; ///< ループするかどうか + csmInt16 CurveCount; ///< カーブの個数 + csmInt32 EventCount; ///< UserDataの個数 + csmFloat32 Fps; ///< フレームレート + csmVector Curves; ///< カーブのリスト + csmVector Segments; ///< セグメントのリスト + csmVector Points; ///< ポイントのリスト + csmVector Events; ///< イベントのリスト +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismMotionJson.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismMotionJson.hpp" +#include "Id/CubismId.hpp" +#include "Id/CubismIdManager.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +namespace { +// JSON keys +const csmChar* Meta = "Meta"; +const csmChar* Duration = "Duration"; +const csmChar* Loop = "Loop"; +const csmChar* AreBeziersRestricted = "AreBeziersRestricted"; +const csmChar* CurveCount = "CurveCount"; +const csmChar* Fps = "Fps"; +const csmChar* TotalSegmentCount = "TotalSegmentCount"; +const csmChar* TotalPointCount = "TotalPointCount"; +const csmChar* Curves = "Curves"; +const csmChar* Target = "Target"; +const csmChar* Id = "Id"; +const csmChar* FadeInTime = "FadeInTime"; +const csmChar* FadeOutTime = "FadeOutTime"; +const csmChar* Segments = "Segments"; +const csmChar* UserData = "UserData"; +const csmChar* UserDataCount = "UserDataCount"; +const csmChar* TotalUserDataSize = "TotalUserDataSize"; +const csmChar* Time = "Time"; +const csmChar* Value = "Value"; +} + +CubismMotionJson::CubismMotionJson(const csmByte* buffer, csmSizeInt size) +{ + CreateCubismJson(buffer, size); +} + +CubismMotionJson::~CubismMotionJson() +{ + DeleteCubismJson(); +} + +csmFloat32 CubismMotionJson::GetMotionDuration() const +{ + return _json->GetRoot()[Meta][Duration].ToFloat(); +} + +csmBool CubismMotionJson::IsMotionLoop() const +{ + return _json->GetRoot()[Meta][Loop].ToBoolean(); +} + +csmBool CubismMotionJson::GetEvaluationOptionFlag(const csmInt32 flagType) const +{ + if (EvaluationOptionFlag_AreBeziersRestricted == flagType) + { + return _json->GetRoot()[Meta][AreBeziersRestricted].ToBoolean(); + } + + return false; +} + +csmInt32 CubismMotionJson::GetMotionCurveCount() const +{ + return _json->GetRoot()[Meta][CurveCount].ToInt(); +} + +csmFloat32 CubismMotionJson::GetMotionFps() const +{ + return _json->GetRoot()[Meta][Fps].ToFloat(); +} + +csmInt32 CubismMotionJson::GetMotionTotalSegmentCount() const +{ + return _json->GetRoot()[Meta][TotalSegmentCount].ToInt(); +} + +csmInt32 CubismMotionJson::GetMotionTotalPointCount() const +{ + return _json->GetRoot()[Meta][TotalPointCount].ToInt(); +} + +csmBool CubismMotionJson::IsExistMotionFadeInTime() const +{ + return !_json->GetRoot()[Meta][FadeInTime].IsNull(); +} + +csmBool CubismMotionJson::IsExistMotionFadeOutTime() const +{ + return !_json->GetRoot()[Meta][FadeOutTime].IsNull(); +} + +csmFloat32 CubismMotionJson::GetMotionFadeInTime() const +{ + return _json->GetRoot()[Meta][FadeInTime].ToFloat(); +} + +csmFloat32 CubismMotionJson::GetMotionFadeOutTime() const +{ + return _json->GetRoot()[Meta][FadeOutTime].ToFloat(); +} + +const csmChar* CubismMotionJson::GetMotionCurveTarget(csmInt32 curveIndex) const +{ + return _json->GetRoot()[Curves][curveIndex][Target].GetRawString(); +} + +CubismIdHandle CubismMotionJson::GetMotionCurveId(csmInt32 curveIndex) const +{ + return CubismFramework::GetIdManager()->GetId(_json->GetRoot()[Curves][curveIndex][Id].GetRawString()); +} + +csmBool CubismMotionJson::IsExistMotionCurveFadeInTime(csmInt32 curveIndex) const +{ + return !_json->GetRoot()[Curves][curveIndex][FadeInTime].IsNull(); +} + +csmBool CubismMotionJson::IsExistMotionCurveFadeOutTime(csmInt32 curveIndex) const +{ + return !_json->GetRoot()[Curves][curveIndex][FadeOutTime].IsNull(); +} + +csmFloat32 CubismMotionJson::GetMotionCurveFadeInTime(csmInt32 curveIndex) const +{ + return _json->GetRoot()[Curves][curveIndex][FadeInTime].ToFloat(); +} + +csmFloat32 CubismMotionJson::GetMotionCurveFadeOutTime(csmInt32 curveIndex) const +{ + return _json->GetRoot()[Curves][curveIndex][FadeOutTime].ToFloat(); +} + +csmInt32 CubismMotionJson::GetMotionCurveSegmentCount(csmInt32 curveIndex) const +{ + return static_cast(_json->GetRoot()[Curves][curveIndex][Segments].GetVector()->GetSize()); +} + +csmFloat32 CubismMotionJson::GetMotionCurveSegment(csmInt32 curveIndex, csmInt32 segmentIndex) const +{ + return _json->GetRoot()[Curves][curveIndex][Segments][segmentIndex].ToFloat(); +} + +csmInt32 CubismMotionJson::GetEventCount() const +{ + return _json->GetRoot()[Meta][UserDataCount].ToInt(); +} + +csmInt32 CubismMotionJson::GetTotalEventValueSize() const +{ + return _json->GetRoot()[Meta][TotalUserDataSize].ToInt(); +} + +csmFloat32 CubismMotionJson::GetEventTime(csmInt32 userDataIndex) const +{ + return _json->GetRoot()[UserData][userDataIndex][Time].ToFloat(); +} + +const csmChar* CubismMotionJson::GetEventValue(csmInt32 userDataIndex) const +{ + return _json->GetRoot()[UserData][userDataIndex][Value].GetRawString(); +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismMotionJson.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + + +#pragma once + +#include "CubismJsonHolder.hpp" +#include "Utils/CubismJson.hpp" +#include "Id/CubismId.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** +* @brief ベジェカーブの解釈方法のフラグタイプ +*/ +enum EvaluationOptionFlag +{ + EvaluationOptionFlag_AreBeziersRestricted = 0, ///< ベジェハンドルの規制状態 +}; + +/** + * @brief motion3.jsonのコンテナ。 + * + * motion3.jsonのコンテナ。 + */ +class CubismMotionJson : public CubismJsonHolder +{ +public: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + * + * @param[in] buffer motion3.jsonが読み込まれているバッファ + * @param[in] size バッファのサイズ + */ + CubismMotionJson(const csmByte* buffer, csmSizeInt size); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismMotionJson(); + + /** + * @brief モーションの長さの取得 + * + * モーションの長さを取得する。 + * + * @return モーションの長さ[秒] + */ + csmFloat32 GetMotionDuration() const; + + /** + * @brief モーションのループ情報の取得 + * + * モーションがループするかどうか? + * + * @retval true ループする + * @retval false ループしない + */ + csmBool IsMotionLoop() const; + + /** + * @brief モーションのベジェカーブの解釈方式のフラグ取得 + * + * モーション内のベジェカーブハンドルの解釈フラグの状態を取得する。 + * + * @param[in] flagType EvaluationOptionFlagで指定されるフラグタイプ + * + * @retval true フラグあり + * @retval false フラグなし + */ + csmBool GetEvaluationOptionFlag(csmInt32 flagType) const; + + /** + * @brief モーションカーブの個数の取得 + * + * モーションカーブの個数を取得する。 + * + * @return モーションカーブの個数 + */ + csmInt32 GetMotionCurveCount() const; + + /** + * @brief モーションのフレームレートの取得 + * + * モーションのフレームレートを取得する。 + * + * @return フレームレート[FPS] + */ + csmFloat32 GetMotionFps() const; + + /** + * @brief モーションのセグメントの総合計の取得 + * + * モーションのセグメントの総合計を取得する。 + * + * @return モーションのセグメントの総合計 + */ + csmInt32 GetMotionTotalSegmentCount() const; + + /** + * @brief モーションのカーブの制御点の総合計の取得 + * + * モーションのカーブの制御点の総合計を取得する。 + * + * @return モーションのカーブの制御点の総合計 + */ + csmInt32 GetMotionTotalPointCount() const; + + /** + * @brief モーションのフェードイン時間の存在 + * + * モーションにフェードイン時間が設定されているかどうか? + * + * @retval true 存在する + * @retval false 存在しない + */ + csmBool IsExistMotionFadeInTime() const; + + /** + * @brief モーションのフェードアウト時間の存在 + * + * モーションにフェードアウト時間が設定されているかどうか? + * + * @retval true 存在する + * @retval false 存在しない + */ + csmBool IsExistMotionFadeOutTime() const; + + /** + * @brief モーションのフェードイン時間の取得 + * + * モーションのフェードイン時間を取得する。 + * + * @return フェードイン時間[秒] + */ + csmFloat32 GetMotionFadeInTime() const; + + /** + * @brief モーションのフェードアウト時間の取得 + * + * モーションのフェードアウト時間を取得する。 + * + * @return フェードアウト時間[秒] + */ + csmFloat32 GetMotionFadeOutTime() const; + + /** + * @brief モーションのカーブの種類の取得 + * + * モーションのカーブの種類を取得する。 + * + * @param[in] curveIndex カーブのインデックス + * @return カーブの種類 + */ + const csmChar* GetMotionCurveTarget(csmInt32 curveIndex) const; + + /** + * @brief モーションのカーブのIDの取得 + * + * モーションのカーブのIDを取得する。 + * + * @param[in] curveIndex カーブのインデックス + * @return カーブのID + */ + CubismIdHandle GetMotionCurveId(csmInt32 curveIndex) const; + + /** + * @brief モーションのカーブのフェードイン時間の存在 + * + * モーションのカーブにフェードイン時間が設定されているかどうか? + * + * @param[in] curveIndex カーブのインデックス + * @retval true 存在する + * @retval false 存在しない + */ + csmBool IsExistMotionCurveFadeInTime(csmInt32 curveIndex) const; + + /** + * @brief モーションのカーブのフェードアウト時間の存在 + * + * モーションのカーブにフェードアウト時間が設定されているかどうか? + * + * @param[in] curveIndex カーブのインデックス + * @retval true 存在する + * @retval false 存在しない + */ + csmBool IsExistMotionCurveFadeOutTime(csmInt32 curveIndex) const; + + /** + * @brief モーションのカーブのフェードイン時間の取得 + * + * モーションのカーブのフェードイン時間を取得する。 + * + * @param[in] curveIndex カーブのインデックス + * @return フェードイン時間[秒] + */ + csmFloat32 GetMotionCurveFadeInTime(csmInt32 curveIndex) const; + + /** + * @brief モーションのカーブのフェードアウト時間の取得 + * + * モーションのカーブのフェードアウト時間を取得する。 + * + * @param[in] curveIndex カーブのインデックス + * @return フェードアウト時間[秒] + */ + csmFloat32 GetMotionCurveFadeOutTime(csmInt32 curveIndex) const; + + /** + * @brief モーションのカーブのセグメントの個数の取得 + * + * モーションのカーブのセグメントの個数を取得する。 + * + * @param[in] curveIndex カーブのインデックス + * @return モーションのカーブのセグメントの個数 + */ + csmInt32 GetMotionCurveSegmentCount(csmInt32 curveIndex) const; + + + /** + * @brief モーションのカーブのセグメントの値の取得 + * + * モーションのカーブのセグメントの値を取得する、 + * + * @param[in] curveIndex カーブのインデックス + * @param[in] segmentIndex セグメントのインデックス + * @return セグメントの値 + */ + csmFloat32 GetMotionCurveSegment(csmInt32 curveIndex, csmInt32 segmentIndex) const; + + /** + * @brief イベントの個数の取得 + * + * イベントの個数の取得する。 + * + * @return イベントの個数 + */ + csmInt32 GetEventCount() const; + + /** + * @brief イベントの総文字数の取得 + * + * イベントの総文字数の取得する。 + * + * @return イベントの総文字数 + */ + csmInt32 GetTotalEventValueSize() const; + + /** + * @brief イベントの時間の取得 + * + * イベントの時間の取得する。 + * + * @param[in] userDataIndex イベントのインデックス + * @return イベントの時間[秒] + */ + csmFloat32 GetEventTime(csmInt32 userDataIndex) const; + + /** + * @brief イベントの取得 + * + * イベントの取得する。 + * + * @param[in] userDataIndex イベントのインデックス + * @return イベントの文字列 + */ + const csmChar* GetEventValue(csmInt32 userDataIndex) const; +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismMotionManager.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismMotionManager.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +CubismMotionManager::CubismMotionManager() + : _currentPriority(0) + , _reservePriority(0) +{ } + +CubismMotionManager::~CubismMotionManager() +{ } + +csmInt32 CubismMotionManager::GetCurrentPriority() const +{ + return _currentPriority; +} + +csmInt32 CubismMotionManager::GetReservePriority() const +{ + return _reservePriority; +} + +void CubismMotionManager::SetReservePriority(csmInt32 val) +{ + _reservePriority = val; +} + +CubismMotionQueueEntryHandle CubismMotionManager::StartMotionPriority(ACubismMotion* motion, csmBool autoDelete, csmInt32 priority) +{ + if (priority == _reservePriority) + { + _reservePriority = 0; // 予約を解除 + } + + _currentPriority = priority; // 再生中モーションの優先度を設定 + + return CubismMotionQueueManager::StartMotion(motion, autoDelete); +} + +csmBool CubismMotionManager::UpdateMotion(CubismModel* model, csmFloat32 deltaTimeSeconds) +{ + _userTimeSeconds += deltaTimeSeconds; + + const csmBool updated = CubismMotionQueueManager::DoUpdateMotion(model, _userTimeSeconds); + + if (IsFinished()) + { + _currentPriority = 0; // 再生中モーションの優先度を解除 + } + + return updated; +} + +csmBool CubismMotionManager::ReserveMotion(csmInt32 priority) +{ + if ((priority <= _reservePriority) || (priority <= _currentPriority)) + { + return false; + } + + _reservePriority = priority; + + return true; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismMotionManager.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "Model/CubismModel.hpp" +#include "ACubismMotion.hpp" +#include "CubismMotionQueueManager.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief モーションの管理 + * + * モーションの管理を行うクラス。 + */ +class CubismMotionManager : public CubismMotionQueueManager +{ +public: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismMotionManager(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismMotionManager(); + + /** + * @brief 再生中のモーションの優先度の取得 + * + * 再生中のモーションの優先度の取得する。 + * + * @return モーションの優先度 + */ + csmInt32 GetCurrentPriority() const; + + /** + * @brief 予約中のモーションの優先度の取得 + * + * 予約中のモーションの優先度を取得する。 + * + * @return モーションの優先度 + */ + csmInt32 GetReservePriority() const; + + /** + * @brief 予約中のモーションの優先度の設定 + * + * 予約中のモーションの優先度を設定する。 + * + * @param[in] val 優先度 + */ + void SetReservePriority(csmInt32 val); + + /** + * @brief 優先度を設定してモーションの開始 + * + * 優先度を設定してモーションを開始する。 + * + * @param[in] motion モーション + * @param[in] autoDelete 再生が終了したモーションのインスタンスを削除するならtrue + * @param[in] priority 優先度 + * @return 開始したモーションの識別番号を返す。個別のモーションが終了したか否かを判定するIsFinished()の引数で使用する。開始できない時は「-1」 + */ + CubismMotionQueueEntryHandle StartMotionPriority(ACubismMotion* motion, csmBool autoDelete, csmInt32 priority); + + /** + * @brief モーションの更新 + * + * モーションを更新して、モデルにパラメータ値を反映する。 + * + * @param[in] model 対象のモデル + * @param[in] deltaTimeSeconds デルタ時間[秒] + * @param[in][out] opacity 透明度の値(Nullable) + * @retval true 更新されている + * @retval false 更新されていない + */ + csmBool UpdateMotion(CubismModel* model, csmFloat32 deltaTimeSeconds); + + /** + * @brief モーションの予約 + * + * モーションを予約する。 + * + * @param[in] priority 優先度 + * @retval true 予約できた + * @retval false 予約できなかった + */ + csmBool ReserveMotion(csmInt32 priority); + +private: + csmInt32 _currentPriority; ///< 現在再生中のモーションの優先度 + csmInt32 _reservePriority; ///< 再生予定のモーションの優先度。再生中は0になる。モーションファイルを別スレッドで読み込むときの機能。 +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismMotionQueueEntry.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismMotionQueueEntry.hpp" +#include "CubismFramework.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +CubismMotionQueueEntry::CubismMotionQueueEntry() + : _autoDelete(false) + , _motion(NULL) + , _available(true) + , _finished(false) + , _started(false) + , _startTimeSeconds(-1.0f) + , _fadeInStartTimeSeconds(0.0f) + , _endTimeSeconds(-1.0f) + , _stateTimeSeconds(0.0f) + , _stateWeight(0.0f) + , _lastEventCheckSeconds(0.0f) + , _fadeOutSeconds(0.0f) + , _IsTriggeredFadeOut(false) + , _motionQueueEntryHandle(NULL) +{ + this->_motionQueueEntryHandle = this; +} + +CubismMotionQueueEntry::~CubismMotionQueueEntry() +{ + if (_autoDelete && _motion) + { + ACubismMotion::Delete(_motion); // + } +} + +void CubismMotionQueueEntry::SetFadeout(csmFloat32 fadeOutSeconds) +{ + _fadeOutSeconds = fadeOutSeconds; + _IsTriggeredFadeOut = true; +} + +void CubismMotionQueueEntry::StartFadeout(csmFloat32 fadeOutSeconds, csmFloat32 userTimeSeconds) +{ + const csmFloat32 newEndTimeSeconds = userTimeSeconds + fadeOutSeconds; + _IsTriggeredFadeOut = true; + + if (_endTimeSeconds < 0.0f || newEndTimeSeconds < _endTimeSeconds) + { + _endTimeSeconds = newEndTimeSeconds; + } +} + +csmBool CubismMotionQueueEntry::IsFinished() const +{ + return _finished; +} + +csmBool CubismMotionQueueEntry::IsStarted() const +{ + return _started; +} + +csmFloat32 CubismMotionQueueEntry::GetStartTime() const +{ + return _startTimeSeconds; +} + +csmFloat32 CubismMotionQueueEntry::GetFadeInStartTime() const +{ + return _fadeInStartTimeSeconds; +} + +csmFloat32 CubismMotionQueueEntry::GetEndTime() const +{ + return _endTimeSeconds; +} + +void CubismMotionQueueEntry::SetStartTime(csmFloat32 startTime) +{ + this->_startTimeSeconds = startTime; +} + +void CubismMotionQueueEntry::SetFadeInStartTime(csmFloat32 startTime) +{ + this->_fadeInStartTimeSeconds = startTime; +} + +void CubismMotionQueueEntry::SetEndTime(csmFloat32 endTime) +{ + this->_endTimeSeconds = endTime; +} + +void CubismMotionQueueEntry::IsFinished(csmBool f) +{ + this->_finished = f; +} + +void CubismMotionQueueEntry::IsStarted(csmBool f) +{ + this->_started = f; +} + +csmBool CubismMotionQueueEntry::IsAvailable() const +{ + return _available; +} + +void CubismMotionQueueEntry::IsAvailable(csmBool v) +{ + this->_available = v; +} + +void CubismMotionQueueEntry::SetState(csmFloat32 timeSeconds, csmFloat32 weight) +{ + this->_stateTimeSeconds = timeSeconds; + this->_stateWeight = weight; +} + +csmFloat32 CubismMotionQueueEntry::GetStateTime() const +{ + return this->_stateTimeSeconds; +} + +csmFloat32 CubismMotionQueueEntry::GetStateWeight() const +{ + return this->_stateWeight; +} + + +csmFloat32 CubismMotionQueueEntry::GetLastCheckEventTime() const +{ + return this->_lastEventCheckSeconds; +} + +void CubismMotionQueueEntry::SetLastCheckEventTime(csmFloat32 checkTime) +{ + this->_lastEventCheckSeconds = checkTime; +} + +csmBool CubismMotionQueueEntry::IsTriggeredFadeOut() +{ + return this->_IsTriggeredFadeOut; +} + +csmFloat32 CubismMotionQueueEntry::GetFadeOutSeconds() +{ + return this->_fadeOutSeconds; +} + +ACubismMotion* CubismMotionQueueEntry::GetCubismMotion() +{ + return _motion; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismMotionQueueEntry.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "ACubismMotion.hpp" +#include "Type/csmVector.hpp" +#include "Model/CubismUserModel.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +class CubismMotion; + +/** + * @brief CubismMotionQueueManagerで再生している各モーションの管理 + * + * CubismMotionQueueManagerで再生している各モーションの管理クラス。 + */ +class CubismMotionQueueEntry +{ + friend class CubismMotionQueueManager; + friend class ACubismMotion; + friend class CubismMotion; + +public: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismMotionQueueEntry(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismMotionQueueEntry(); + + /** + * @brief フェードアウト開始の設定 + * + * フェードアウトの開始を設定する。 + * + * @param[in] fadeOutSeconds フェードアウトにかかる時間[秒] + */ + void SetFadeout(csmFloat32 fadeOutSeconds); + + /** + * @brief フェードアウトの開始 + * + * フェードアウトを開始する。 + * + * @param[in] fadeOutSeconds フェードアウトにかかる時間[秒] + * @param[in] userTimeSeconds デルタ時間の積算値[秒] + */ + void StartFadeout(csmFloat32 fadeOutSeconds, csmFloat32 userTimeSeconds); + + /** + * @brief モーションの終了の確認 + * + * モーションが終了したかどうか。 + * + * @retval true モーションが終了した + * @retval false 終了していない + */ + csmBool IsFinished() const; + + /** + * @brief モーションの開始の確認 + * + * モーションが開始したかどうか。 + * + * @retval true モーションが開始した + * @retval false 終了していない + */ + csmBool IsStarted() const; + + /** + * @brief モーションの開始時刻の取得 + * + * モーションの開始時刻を取得する。 + * + * @return モーションの開始時刻[秒] + */ + csmFloat32 GetStartTime() const; + + /** + * @brief フェードインの開始時刻の取得 + * + * フェードインの開始時刻を取得する。 + * + * @return フェードインの開始時刻[秒] + */ + csmFloat32 GetFadeInStartTime() const; + + /** + * @brief フェードインの終了時刻の取得 + * + * フェードインの終了時刻を取得する。 + * + * @return フェードインの終了時刻[秒] + */ + csmFloat32 GetEndTime() const; + + /** + * @brief モーションの開始時刻の設定 + * + * モーションの開始時刻を設定する。 + * + * @param[in] startTime モーションの開始時刻[秒] + */ + void SetStartTime(csmFloat32 startTime); + + /** + * @brief フェードインの開始時刻の設定 + * + * フェードインの開始時刻を設定する。 + * + * @param[in] startTime フェードインの開始時刻[秒] + */ + void SetFadeInStartTime(csmFloat32 startTime); + + /** + * @brief フェードインの終了時刻の設定 + * + * フェードインの終了時刻を設定する。 + * + * @param[in] endTime フェードインの終了時刻[秒] + */ + void SetEndTime(csmFloat32 endTime); + + /** + * @brief モーションの終了の設定 + * + * モーションの終了を設定する。 + * + * @param[in] f trueならモーションの終了 + */ + void IsFinished(csmBool f); + + /** + * @brief モーションの開始の設定 + * + * モーションの開始を設定する。 + * + * @param[in] f trueならモーションの開始 + */ + void IsStarted(csmBool f); + + /** + * @brief モーションの有効性の確認 + * + * モーションの有効・無効を取得する。 + * + * @retval true モーションは有効 + * @retval false モーションは無効 + */ + csmBool IsAvailable() const; + + /** + * @brief モーションの有効性の設定 + * + * モーションの有効・無効を設定する。 + * + * @param[in] v trueならモーションは有効 + */ + void IsAvailable(csmBool v); + + /** + * @brief モーションの状態の設定 + * + * モーションの状態を設定する。 + * + * @param[in] timeSeconds 現在時刻[秒] + * @param[in] weight モーションの重み + */ + void SetState(csmFloat32 timeSeconds, csmFloat32 weight); + + /** + * @brief モーションの現在時刻の取得 + * + * モーションの現在時刻を取得する。 + * + * @return モーションの現在時刻[秒] + */ + csmFloat32 GetStateTime() const; + + /** + * @brief モーションの重みの取得 + * + * モーションの重みを取得する。 + * + * @return モーションの重み + */ + csmFloat32 GetStateWeight() const; + + /** + * @brief 最後にイベントの発火をチェックした時間を取得 + * + * 最後にイベントの発火をチェックした時間を取得する。 + * + * @return 最後にイベントの発火をチェックした時間[秒] + */ + csmFloat32 GetLastCheckEventTime() const; + + /** + * @brief 最後にイベントをチェックした時間を設定 + * + * 最後にイベントをチェックした時間を設定する。 + * + * @param[in] checkTime 最後にイベントをチェックした時間[秒] + */ + void SetLastCheckEventTime(csmFloat32 checkTime); + + /** + * @brief フェードアウトが開始しているかを取得 + * + * モーションがフェードアウトが開始しているかを取得する。 + * + * @return フェードアウトが開始しているか + */ + csmBool IsTriggeredFadeOut(); + + /** + * @brief フェードアウト時間の取得 + * + * モーションのフェードアウト時間を取得する。 + * + * @return フェードアウト開始[秒] + */ + csmFloat32 GetFadeOutSeconds(); + + ACubismMotion* GetCubismMotion(); + +private: + csmBool _autoDelete; ///< 自動削除 + ACubismMotion* _motion; ///< モーション + + csmBool _available; ///< 有効化フラグ + csmBool _finished; ///< 終了フラグ + csmBool _started; ///< 開始フラグ(0.9.00以降) + csmFloat32 _startTimeSeconds; ///< モーション再生開始時刻[秒] + csmFloat32 _fadeInStartTimeSeconds; ///< フェードイン開始時刻(ループの時は初回のみ)[秒] + csmFloat32 _endTimeSeconds; ///< 終了予定時刻[秒] + csmFloat32 _stateTimeSeconds; ///< 時刻の状態[秒] + csmFloat32 _stateWeight; ///< 重みの状態 + csmFloat32 _lastEventCheckSeconds; ///< 最終のMotion側のチェックした時間 + csmFloat32 _fadeOutSeconds; + csmBool _IsTriggeredFadeOut; + + CubismMotionQueueEntryHandle _motionQueueEntryHandle; ///< インスタンスごとに一意の値を持つ識別番号 +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismMotionQueueManager.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismMotionQueueManager.hpp" +#include "CubismMotionQueueEntry.hpp" +#include "CubismFramework.hpp" +#include "CubismMotion.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +const CubismMotionQueueEntryHandle InvalidMotionQueueEntryHandleValue = reinterpret_cast(-1); + +CubismMotionQueueManager::CubismMotionQueueManager() + : _userTimeSeconds(0.0f) + , _eventCallback(NULL) + , _eventCustomData(NULL) +{} + +CubismMotionQueueManager::~CubismMotionQueueManager() +{ + for (csmUint32 i = 0; i < _motions.GetSize(); ++i) + { + if (_motions[i]) + { + CSM_DELETE(_motions[i]); + } + } +} + +CubismMotionQueueEntryHandle CubismMotionQueueManager::StartMotion(ACubismMotion* motion, csmBool autoDelete) +{ + if (motion == NULL) + { + return InvalidMotionQueueEntryHandleValue; + } + + CubismMotionQueueEntry* motionQueueEntry = NULL; + + // 既にモーションがあれば終了フラグを立てる + for (csmUint32 i = 0; i < _motions.GetSize(); ++i) + { + motionQueueEntry = _motions.At(i); + if (motionQueueEntry == NULL) + { + continue; + } + + motionQueueEntry->SetFadeout(motionQueueEntry->_motion->GetFadeOutTime()); + } + + motionQueueEntry = CSM_NEW CubismMotionQueueEntry(); // 終了時に破棄する + motionQueueEntry->_autoDelete = autoDelete; + motionQueueEntry->_motion = motion; + + _motions.PushBack(motionQueueEntry, false); + + return motionQueueEntry->_motionQueueEntryHandle; +} + +CubismMotionQueueEntryHandle CubismMotionQueueManager::StartMotion(ACubismMotion* motion, csmBool autoDelete, csmFloat32 userTimeSeconds) +{ +#if _DEBUG + CubismLogWarning("StartMotion(ACubismMotion* motion, csmBool autoDelete, csmFloat32 userTimeSeconds) is a deprecated function. Please use StartMotion(ACubismMotion* motion, csmBool autoDelete)."); +#endif + + if (motion == NULL) + { + return InvalidMotionQueueEntryHandleValue; + } + + CubismMotionQueueEntry* motionQueueEntry = NULL; + + // 既にモーションがあれば終了フラグを立てる + for (csmUint32 i = 0; i < _motions.GetSize(); ++i) + { + motionQueueEntry = _motions.At(i); + if (motionQueueEntry == NULL) + { + continue; + } + + motionQueueEntry->SetFadeout(motionQueueEntry->_motion->GetFadeOutTime()); + } + + motionQueueEntry = CSM_NEW CubismMotionQueueEntry(); // 終了時に破棄する + motionQueueEntry->_autoDelete = autoDelete; + motionQueueEntry->_motion = motion; + + _motions.PushBack(motionQueueEntry, false); + + return motionQueueEntry->_motionQueueEntryHandle; +} + +csmBool CubismMotionQueueManager::DoUpdateMotion(CubismModel* model, csmFloat32 userTimeSeconds) +{ + csmBool updated = false; + + // ------- 処理を行う -------- + // 既にモーションがあれば終了フラグを立てる + + for (csmVector::iterator ite = _motions.Begin(); ite != _motions.End();) + { + CubismMotionQueueEntry* motionQueueEntry = *ite; + + if (motionQueueEntry == NULL) + { + ite = _motions.Erase(ite); // 削除 + continue; + } + + ACubismMotion* motion = motionQueueEntry->_motion; + + if (motion == NULL) + { + CSM_DELETE(motionQueueEntry); + ite = _motions.Erase(ite); // 削除 + + continue; + } + + // ------ 値を反映する ------ + motion->UpdateParameters(model, motionQueueEntry, userTimeSeconds); + updated = true; + + // ------ ユーザトリガーイベントを検査する ---- + const csmVector& firedList = motion->GetFiredEvent( + motionQueueEntry->GetLastCheckEventTime() - motionQueueEntry->GetStartTime() + , userTimeSeconds - motionQueueEntry->GetStartTime() + ); + + for (csmUint32 i = 0; i < firedList.GetSize(); ++i) + { + _eventCallback(this, *(firedList[i]), _eventCustomData); + } + + motionQueueEntry->SetLastCheckEventTime(userTimeSeconds); + + // ----- 終了済みの処理があれば削除する ------ + if (motionQueueEntry->IsFinished()) + { + CSM_DELETE(motionQueueEntry); + ite = _motions.Erase(ite); // 削除 + } + else + { + if (motionQueueEntry->IsTriggeredFadeOut()) + { + motionQueueEntry->StartFadeout(motionQueueEntry->GetFadeOutSeconds(), userTimeSeconds); + } + + ++ite; + } + } + + return updated; +} + +csmVector* CubismMotionQueueManager::GetCubismMotionQueueEntries() +{ + return &_motions; +} + +CubismMotionQueueEntry* CubismMotionQueueManager::GetCubismMotionQueueEntry(CubismMotionQueueEntryHandle motionQueueEntryNumber) +{ + //------- 処理を行う -------- + //既にモーションがあれば終了フラグを立てる + + for (csmVector::iterator ite = _motions.Begin(); ite != _motions.End(); ++ite) + { + CubismMotionQueueEntry* motionQueueEntry = *ite; + + if (motionQueueEntry == NULL) + { + continue; + } + + if (motionQueueEntry->_motionQueueEntryHandle == motionQueueEntryNumber) + { + return motionQueueEntry; + } + } + + return NULL; +} + +csmBool CubismMotionQueueManager::IsFinished() +{ + // ------- 処理を行う -------- + // 既にモーションがあれば終了フラグを立てる + + for (csmVector::iterator ite = _motions.Begin(); ite != _motions.End();) + { + CubismMotionQueueEntry* motionQueueEntry = *ite; + + if (motionQueueEntry == NULL) + { + ite = _motions.Erase(ite); // 削除 + continue; + } + + ACubismMotion* motion = motionQueueEntry->_motion; + + if (motion == NULL) + { + CSM_DELETE(motionQueueEntry); + ite = _motions.Erase(ite); // 削除 + continue; + } + + // ----- 終了済みの処理があれば削除する ------ + if (!motionQueueEntry->IsFinished()) + { + return false; + } + else + { + ++ite; + } + } + + return true; +} + +csmBool CubismMotionQueueManager::IsFinished(CubismMotionQueueEntryHandle motionQueueEntryNumber) +{ + // 既にモーションがあれば終了フラグを立てる + + for (csmVector::iterator ite = _motions.Begin(); ite != _motions.End(); ite++) + { + CubismMotionQueueEntry* motionQueueEntry = *ite; + + if (motionQueueEntry == NULL) + { + continue; + } + + if (motionQueueEntry->_motionQueueEntryHandle == motionQueueEntryNumber && !motionQueueEntry->IsFinished()) + { + return false; + } + } + + return true; +} + +void CubismMotionQueueManager::StopAllMotions() +{ + // ------- 処理を行う -------- + // 既にモーションがあれば終了フラグを立てる + + for (csmVector::iterator ite = _motions.Begin(); ite != _motions.End();) + { + CubismMotionQueueEntry* motionQueueEntry = *ite; + + if (motionQueueEntry == NULL) + { + ite = _motions.Erase(ite); + + continue; + } + + // ----- 終了済みの処理があれば削除する ------ + CSM_DELETE(motionQueueEntry); + ite = _motions.Erase(ite); //削除 + } +} + +void CubismMotionQueueManager::SetEventCallback(CubismMotionEventFunction callback, void* customData) +{ + _eventCallback = callback; + _eventCustomData = customData; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Motion/CubismMotionQueueManager.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "ACubismMotion.hpp" +#include "Model/CubismModel.hpp" +#include "Type/csmVector.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +class CubismMotionQueueEntry; +class CubismMotionQueueManager; + +/** +* @brief イベントのコールバック関数定義 +* +* イベントのコールバックに登録できる関数の型情報 +* +* @param[in] caller 発火したイベントを再生させたCubismMotionQueueManager +* @param[in] eventValue 発火したイベントの文字列データ +* @param[in] customData コールバックに返される登録時に指定されたデータ +*/ +typedef void(*CubismMotionEventFunction)(const CubismMotionQueueManager* caller, const csmString& eventValue, void* customData); + +/** + * @brief モーションの識別番号 + * + * モーションの識別番号の定義 + */ +typedef void* CubismMotionQueueEntryHandle; + +extern const CubismMotionQueueEntryHandle InvalidMotionQueueEntryHandleValue; ///< 無効なモーションの識別番号の定義 + +/** + * @brief モーション再生の管理 + * + * モーション再生の管理用クラス。CubismMotionモーションなどACubismMotionのサブクラスを再生するために使用する。 + * + * @note 再生中に別のモーションが StartMotion()された場合は、新しいモーションに滑らかに変化し旧モーションは中断する。 + * 表情用モーション、体用モーションなどを分けてモーション化した場合など、 + * 複数のモーションを同時に再生させる場合は、複数のCubismMotionQueueManagerインスタンスを使用する。 + */ +class CubismMotionQueueManager +{ +public: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismMotionQueueManager(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismMotionQueueManager(); + + /** + * @brief 指定したモーションの開始 + * + * 指定したモーションを開始する。同じタイプのモーションが既にある場合は、既存のモーションに終了フラグを立て、フェードアウトを開始させる。 + * + * @param[in] motion 開始するモーション + * @param[in] autoDelete 再生が終了したモーションのインスタンスを削除するなら true + * @return 開始したモーションの識別番号を返す。個別のモーションが終了したか否かを判定するIsFinished()の引数で使用する。開始できない時は「-1」 + */ + CubismMotionQueueEntryHandle StartMotion(ACubismMotion* motion, csmBool autoDelete); + + /** + * @deprecated 第3引数userTimeSecondsを関数内で使用していないため非推奨。StartMotion(ACubismMotion* motion, csmBool autoDelete)を使用してください。 + * @brief 指定したモーションの開始 + * + * 指定したモーションを開始する。同じタイプのモーションが既にある場合は、既存のモーションに終了フラグを立て、フェードアウトを開始させる。 + * + * @param[in] motion 開始するモーション + * @param[in] autoDelete 再生が終了したモーションのインスタンスを削除するなら true + * @param[in] userTimeSeconds デルタ時間の積算値[秒] + * @return 開始したモーションの識別番号を返す。個別のモーションが終了したか否かを判定するIsFinished()の引数で使用する。開始できない時は「-1」 + */ + CubismMotionQueueEntryHandle StartMotion(ACubismMotion* motion, csmBool autoDelete, csmFloat32 userTimeSeconds); + + /** + * @brief すべてのモーションの終了の確認 + * + * すべてのモーションが終了しているかどうか。 + * + * @retval true すべて終了している + * @retval false 終了していない + */ + csmBool IsFinished(); + + /** + * @brief 指定したモーションの終了の確認 + * + * 指定したモーションが終了しているかどうか。 + * + * @param[in] motionQueueEntryNumber モーションの識別番号 + * @retval true 指定したモーションは終了している + * @retval false 終了していない + */ + csmBool IsFinished(CubismMotionQueueEntryHandle motionQueueEntryNumber); + + /** + * @brief すべてのモーションの停止 + * + * すべてのモーションを停止する。 + */ + void StopAllMotions(); + + /** + * @brief 指定したCubismMotionQueueEntryの取得 + * + * 指定したCubismMotionQueueEntryを取得する。 + * + * @param[in] motionQueueEntryNumber モーションの識別番号 + * @return 指定したCubismMotionQueueEntryへのポインタ + * @retval NULL 見つからなかった + */ + CubismMotionQueueEntry* GetCubismMotionQueueEntry(CubismMotionQueueEntryHandle motionQueueEntryNumber); + + /** + * @brief CubismMotionQueueEntryの配列の取得 + * + * CubismMotionQueueEntryの配列を取得する。 + * + * @return CubismMotionQueueEntryの配列へのポインタ + * @retval NULL 見つからなかった + */ + csmVector* GetCubismMotionQueueEntries(); + + /** + * @brief イベントを受け取るCallbackの登録 + * + * イベントを受け取るCallbackの登録をする。 + * + * @param[in] callback コールバック関数 + * @param[in] customData コールバックに返されるデータ + */ + void SetEventCallback(CubismMotionEventFunction callback, void* customData = NULL); + +protected: + /** + * @brief モーションの更新 + * + * モーションを更新して、モデルにパラメータ値を反映する。 + * + * @param[in] model 対象のモデル + * @param[in] userTimeSeconds デルタ時間の積算値[秒] + * @retval true モデルへパラメータ値の反映あり + * @retval false モデルへパラメータ値の反映なし(モーションの変化なし) + */ + virtual csmBool DoUpdateMotion(CubismModel* model, csmFloat32 userTimeSeconds); + + + csmFloat32 _userTimeSeconds; ///< デルタ時間の積算値[秒] + +private: + csmVector _motions; ///< モーション + + CubismMotionEventFunction _eventCallback; ///< コールバック関数ポインタ + void* _eventCustomData; ///< コールバックに戻されるデータ +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Physics/CubismPhysics.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismPhysics.hpp" +#include "CubismPhysicsInternal.hpp" +#include "CubismPhysicsJson.hpp" +#include "Model/CubismModel.hpp" +#include "Utils/CubismString.hpp" +#include "Math/CubismMath.hpp" +#include "Math/CubismVector2.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/// physics constants +namespace { + +/// physics types tags. +const csmChar* PhysicsTypeTagX = "X"; +const csmChar* PhysicsTypeTagY = "Y"; +const csmChar* PhysicsTypeTagAngle = "Angle"; + +/// Constant of air resistance. +const csmFloat32 AirResistance = 5.0f; + +/// Constant of maximum weight of input and output ratio. +const csmFloat32 MaximumWeight = 100.0f; + +/// Constant of threshold of movement. +const csmFloat32 MovementThreshold = 0.001f; + +/// Constant of maximum allowed delta time +const csmFloat32 MaxDeltaTime = 5.0f; + +csmFloat32 GetRangeValue(csmFloat32 min, csmFloat32 max) +{ + csmFloat32 maxValue = CubismMath::Max(min, max); + csmFloat32 minValue = CubismMath::Min(min, max); + + return CubismMath::AbsF(maxValue - minValue); +} + +/// Gets sign. +/// +/// @param value Evaluation target value. +/// +/// @return Sign of value. +csmInt32 Sign(csmFloat32 value) +{ + csmInt32 ret = 0; + + if (value > 0.0f) + { + ret = 1; + } + else if (value < 0.0f) + { + ret = -1; + } + + return ret; +} + +csmFloat32 GetDefaultValue(csmFloat32 min, csmFloat32 max) +{ + const csmFloat32 minValue = CubismMath::Min(min, max); + return minValue + (GetRangeValue(min, max) / 2.0f); +} + +csmFloat32 NormalizeParameterValue( + csmFloat32 value, + csmFloat32 parameterMinimum, + csmFloat32 parameterMaximum, + csmFloat32 parameterDefault, + csmFloat32 normalizedMinimum, + csmFloat32 normalizedMaximum, + csmFloat32 normalizedDefault, + csmInt32 isInverted) +{ + csmFloat32 result = 0.0f; + + const csmFloat32 maxValue = CubismMath::Max(parameterMaximum, parameterMinimum); + + if (maxValue < value) + { + value = maxValue; + } + + const csmFloat32 minValue = CubismMath::Min(parameterMaximum, parameterMinimum); + + if (minValue > value) + { + value = minValue; + } + + const csmFloat32 minNormValue = CubismMath::Min(normalizedMinimum, normalizedMaximum); + const csmFloat32 maxNormValue = CubismMath::Max(normalizedMinimum, normalizedMaximum); + const csmFloat32 middleNormValue = normalizedDefault; + + const csmFloat32 middleValue = GetDefaultValue(minValue, maxValue); + const csmFloat32 paramValue = value - middleValue; + + switch (Sign(paramValue)) + { + case 1: { + const csmFloat32 nLength = maxNormValue - middleNormValue; + const csmFloat32 pLength = maxValue - middleValue; + if (pLength != 0.0f) + { + result = paramValue * (nLength / pLength); + result += middleNormValue; + } + + break; + } + case -1: { + const csmFloat32 nLength = minNormValue - middleNormValue; + const csmFloat32 pLength = minValue - middleValue; + if (pLength != 0.0f) + { + result = paramValue * (nLength / pLength); + result += middleNormValue; + } + + break; + } + case 0: { + result = middleNormValue; + + break; + } + default: { + break; + } + } + + return (isInverted) + ? result + : (result * -1.0f); +} + +void GetInputTranslationXFromNormalizedParameterValue(CubismVector2* targetTranslation, csmFloat32* targetAngle, csmFloat32 value, + csmFloat32 parameterMinimumValue, csmFloat32 parameterMaximumValue, + csmFloat32 parameterDefaultValue, + CubismPhysicsNormalization* normalizationPosition, + CubismPhysicsNormalization* normalizationAngle, csmInt32 isInverted, + csmFloat32 weight) +{ + targetTranslation->X += NormalizeParameterValue( + value, + parameterMinimumValue, + parameterMaximumValue, + parameterDefaultValue, + normalizationPosition->Minimum, + normalizationPosition->Maximum, + normalizationPosition->Default, + isInverted + ) * weight; +} + +void GetInputTranslationYFromNormalizedParameterValue(CubismVector2* targetTranslation, csmFloat32* targetAngle, csmFloat32 value, + csmFloat32 parameterMinimumValue, csmFloat32 parameterMaximumValue, + csmFloat32 parameterDefaultValue, + CubismPhysicsNormalization* normalizationPosition, + CubismPhysicsNormalization* normalizationAngle, + csmInt32 isInverted, csmFloat32 weight) +{ + targetTranslation->Y += NormalizeParameterValue( + value, + parameterMinimumValue, + parameterMaximumValue, + parameterDefaultValue, + normalizationPosition->Minimum, + normalizationPosition->Maximum, + normalizationPosition->Default, + isInverted + ) * weight; +} + +void GetInputAngleFromNormalizedParameterValue(CubismVector2* targetTranslation, csmFloat32* targetAngle, csmFloat32 value, + csmFloat32 parameterMinimumValue, csmFloat32 parameterMaximumValue, + csmFloat32 parameterDefaultValue, + CubismPhysicsNormalization* normalizationPosition, + CubismPhysicsNormalization* normalizationAngle, + csmInt32 isInverted, csmFloat32 weight) +{ + *targetAngle += NormalizeParameterValue( + value, + parameterMinimumValue, + parameterMaximumValue, + parameterDefaultValue, + normalizationAngle->Minimum, + normalizationAngle->Maximum, + normalizationAngle->Default, + isInverted + ) * weight; +} + +csmFloat32 GetOutputTranslationX(CubismVector2 translation, CubismPhysicsParticle* particles, csmInt32 particleIndex, + csmInt32 isInverted, CubismVector2 parentGravity) +{ + csmFloat32 outputValue = translation.X; + + if (isInverted) + { + outputValue *= -1.0f; + } + + return outputValue; +} + +csmFloat32 GetOutputTranslationY(CubismVector2 translation, CubismPhysicsParticle* particles, csmInt32 particleIndex, + csmInt32 isInverted, CubismVector2 parentGravity) +{ + csmFloat32 outputValue = translation.Y; + + if (isInverted) + { + outputValue *= -1.0f; + } + + return outputValue; +} + +csmFloat32 GetOutputAngle(CubismVector2 translation, CubismPhysicsParticle* particles, csmInt32 particleIndex, csmInt32 isInverted, + CubismVector2 parentGravity) +{ + csmFloat32 outputValue; + + if (particleIndex >= 2) + { + parentGravity = particles[particleIndex - 1].Position - particles[particleIndex - 2].Position; + } + else + { + parentGravity *= -1.0f; + } + + outputValue = CubismMath::DirectionToRadian(parentGravity, translation); + + if (isInverted) + { + outputValue *= -1.0f; + } + + return outputValue; +} + +csmFloat32 GetOutputScaleTranslationX(CubismVector2 translationScale, csmFloat32 angleScale) +{ + return translationScale.X; +} + +csmFloat32 GetOutputScaleTranslationY(CubismVector2 translationScale, csmFloat32 angleScale) +{ + return translationScale.Y; +} + +csmFloat32 GetOutputScaleAngle(CubismVector2 translationScale, csmFloat32 angleScale) +{ + return angleScale; +} + +/// Updates particles. +/// +/// @param strand Target array of particle. +/// @param strandCount Count of particle. +/// @param totalTranslation Total translation value. +/// @param totalAngle Total angle. +/// @param windDirection Direction of wind. +/// @param thresholdValue Threshold of movement. +/// @param deltaTimeSeconds Delta time. +/// @param airResistance Air resistance. +void UpdateParticles(CubismPhysicsParticle* strand, csmInt32 strandCount, CubismVector2 totalTranslation, csmFloat32 totalAngle, + CubismVector2 windDirection, csmFloat32 thresholdValue, csmFloat32 deltaTimeSeconds, csmFloat32 airResistance) +{ + csmInt32 i; + csmFloat32 totalRadian; + csmFloat32 delay; + csmFloat32 radian; + CubismVector2 currentGravity; + CubismVector2 direction; + CubismVector2 velocity; + CubismVector2 force; + CubismVector2 newDirection; + + strand[0].Position = totalTranslation; + + totalRadian = CubismMath::DegreesToRadian(totalAngle); + currentGravity = CubismMath::RadianToDirection(totalRadian); + currentGravity.Normalize(); + + for (i = 1; i < strandCount; ++i) + { + strand[i].Force = (currentGravity * strand[i].Acceleration) + windDirection; + + strand[i].LastPosition = strand[i].Position; + + delay = strand[i].Delay * deltaTimeSeconds * 30.0f; + + direction.X = strand[i].Position.X - strand[i - 1].Position.X; + direction.Y = strand[i].Position.Y - strand[i - 1].Position.Y; + + radian = CubismMath::DirectionToRadian(strand[i].LastGravity, currentGravity) / airResistance; + + direction.X = ((CubismMath::CosF(radian) * direction.X) - (direction.Y * CubismMath::SinF(radian))); + direction.Y = ((CubismMath::SinF(radian) * direction.X) + (direction.Y * CubismMath::CosF(radian))); + + strand[i].Position = strand[i - 1].Position + direction; + + velocity.X = strand[i].Velocity.X * delay; + velocity.Y = strand[i].Velocity.Y * delay; + force = strand[i].Force * delay * delay; + + strand[i].Position = strand[i].Position + velocity + force; + + newDirection = strand[i].Position - strand[i - 1].Position; + + newDirection.Normalize(); + + strand[i].Position = strand[i - 1].Position + (newDirection * strand[i].Radius); + + if (CubismMath::AbsF(strand[i].Position.X) < thresholdValue) + { + strand[i].Position.X = 0.0f; + } + + if (delay != 0.0f) + { + strand[i].Velocity.X = strand[i].Position.X - strand[i].LastPosition.X; + strand[i].Velocity.Y = strand[i].Position.Y - strand[i].LastPosition.Y; + strand[i].Velocity /= delay; + strand[i].Velocity *= strand[i].Mobility; + } + + strand[i].Force = CubismVector2(0.0f, 0.0f); + strand[i].LastGravity = currentGravity; + } +} + +/** + * Updates particles for stabilization. + * + * @param strand Target array of particle. + * @param strandCount Count of particle. + * @param totalTranslation Total translation value. + * @param totalAngle Total angle. + * @param windDirection Direction of Wind. + * @param thresholdValue Threshold of movement. + */ +void UpdateParticlesForStabilization(CubismPhysicsParticle* strand, csmInt32 strandCount, CubismVector2 totalTranslation, csmFloat32 totalAngle, + CubismVector2 windDirection, csmFloat32 thresholdValue) +{ + csmInt32 i; + csmFloat32 totalRadian; + CubismVector2 currentGravity; + CubismVector2 force; + + strand[0].Position = totalTranslation; + + totalRadian = CubismMath::DegreesToRadian(totalAngle); + currentGravity = CubismMath::RadianToDirection(totalRadian); + currentGravity.Normalize(); + + for (i = 1; i < strandCount; ++i) + { + strand[i].Force = (currentGravity * strand[i].Acceleration) + windDirection; + + strand[i].LastPosition = strand[i].Position; + + strand[i].Velocity = CubismVector2(0.0, 0.0); + + force = strand[i].Force; + force.Normalize(); + + force *= strand[i].Radius; + strand[i].Position = strand[i - 1].Position + force; + + if (CubismMath::AbsF(strand[i].Position.X) < thresholdValue) + { + strand[i].Position.X = 0.0f; + } + + strand[i].Force = CubismVector2(0.0f, 0.0f); + strand[i].LastGravity = currentGravity; + } +} + +/// Updates output parameter value. +/// +/// @param parameterValue Target parameter value. +/// @param parameterValueMinimum Minimum of parameter value. +/// @param parameterValueMaximum Maximum of parameter value. +/// @param translation Translation value. +void UpdateOutputParameterValue(csmFloat32* parameterValue, csmFloat32 parameterValueMinimum, csmFloat32 parameterValueMaximum, + csmFloat32 translation, CubismPhysicsOutput* output) +{ + csmFloat32 outputScale; + csmFloat32 value; + csmFloat32 weight; + + outputScale = output->GetScale(output->TranslationScale, output->AngleScale); + + value = translation * outputScale; + + if (value < parameterValueMinimum) + { + if (value < output->ValueBelowMinimum) + { + output->ValueBelowMinimum = value; + } + + value = parameterValueMinimum; + } + else if (value > parameterValueMaximum) + { + if (value > output->ValueExceededMaximum) + { + output->ValueExceededMaximum = value; + } + + value = parameterValueMaximum; + } + + weight = (output->Weight / MaximumWeight); + + if (weight >= 1.0f) + { + *parameterValue = value; + } + else + { + value = (*parameterValue * (1.0f - weight)) + (value * weight); + *parameterValue = value; + } +} + +} + +CubismPhysics::CubismPhysics() + : _physicsRig(NULL) +{ + // set default options. + _options.Gravity.Y = -1.0f; + _options.Gravity.X = 0; + _options.Wind.X = 0; + _options.Wind.Y = 0; + _currentRemainTime = 0.0f; +} + +CubismPhysics::~CubismPhysics() +{ + CSM_DELETE(_physicsRig); + _parameterCaches.Clear(); + _parameterInputCaches.Clear(); +} + +/// Initializes physics. +/// +/// @param physics Target rig. +void CubismPhysics::Initialize() +{ + CubismPhysicsParticle* strand; + CubismPhysicsSubRig* currentSetting; + csmInt32 i, settingIndex; + CubismVector2 radius; + + for (settingIndex = 0; settingIndex < _physicsRig->SubRigCount; ++settingIndex) + { + currentSetting = &_physicsRig->Settings[settingIndex]; + strand = &_physicsRig->Particles[currentSetting->BaseParticleIndex]; + + // Initialize the top of particle. + strand[0].InitialPosition = CubismVector2(0.0f, 0.0f); + strand[0].LastPosition = strand[0].InitialPosition; + strand[0].LastGravity = CubismVector2(0.0f, -1.0f); + strand[0].LastGravity.Y *= -1.0f; + strand[0].Velocity = CubismVector2(0.0f, 0.0f); + strand[0].Force = CubismVector2(0.0f, 0.0f); + + // Initialize particles. + for (i = 1; i < currentSetting->ParticleCount; ++i) + { + radius = CubismVector2(0.0f, 0.0f); + radius.Y = strand[i].Radius; + strand[i].InitialPosition = strand[i - 1].InitialPosition + radius; + strand[i].Position = strand[i].InitialPosition; + strand[i].LastPosition = strand[i].InitialPosition; + strand[i].LastGravity = CubismVector2(0.0f, -1.0f); + strand[i].LastGravity.Y *= -1.0f; + strand[i].Velocity = CubismVector2(0.0f, 0.0f); + strand[i].Force = CubismVector2(0.0f, 0.0f); + } + } +} + +/// Reset the physics states. +void CubismPhysics::Reset() +{ + // set default options. + _options.Gravity.Y = -1.0f; + _options.Gravity.X = 0.0f; + _options.Wind.X = 0.0f; + _options.Wind.Y = 0.0f; + + _physicsRig->Gravity.X = 0.0f; + _physicsRig->Gravity.Y = 0.0f; + _physicsRig->Wind.X = 0.0f; + _physicsRig->Wind.Y = 0.0f; + + Initialize(); +} + +CubismPhysics* CubismPhysics::Create(const csmByte* buffer, csmSizeInt size) +{ + CubismPhysics* ret = CSM_NEW CubismPhysics(); + + ret->Parse(buffer, size); + + if (!ret->_isJsonValid) + { + Delete(ret); + return NULL; + } + + ret->_physicsRig->Gravity.Y = 0; + return ret; +} + +void CubismPhysics::Delete(CubismPhysics* physics) +{ + CSM_DELETE_SELF(CubismPhysics, physics); +} + +void CubismPhysics::Parse(const csmByte* physicsJson, csmSizeInt size) +{ + _physicsRig = CSM_NEW CubismPhysicsRig; + + CubismPhysicsJson* json = CSM_NEW CubismPhysicsJson(physicsJson, size); + + _isJsonValid = json->IsValid(); + + if (!_isJsonValid) + { + CSM_DELETE(json); + return; + } + + _physicsRig->Gravity = json->GetGravity(); + _physicsRig->Wind = json->GetWind(); + _physicsRig->SubRigCount = json->GetSubRigCount(); + + _physicsRig->Fps = json->GetFps(); + + _physicsRig->Settings.UpdateSize(_physicsRig->SubRigCount, CubismPhysicsSubRig(), true); + _physicsRig->Inputs.UpdateSize(json->GetTotalInputCount(), CubismPhysicsInput(), true); + _physicsRig->Outputs.UpdateSize(json->GetTotalOutputCount(), CubismPhysicsOutput(), true); + _physicsRig->Particles.UpdateSize(json->GetVertexCount(), CubismPhysicsParticle(), true); + + _currentRigOutputs.Clear(); + _previousRigOutputs.Clear(); + + csmInt32 inputIndex = 0, outputIndex = 0, particleIndex = 0; + for (csmUint32 i = 0; i < _physicsRig->Settings.GetSize(); ++i) + { + _physicsRig->Settings[i].NormalizationPosition.Minimum = json->GetNormalizationPositionMinimumValue(i); + _physicsRig->Settings[i].NormalizationPosition.Maximum = json->GetNormalizationPositionMaximumValue(i); + _physicsRig->Settings[i].NormalizationPosition.Default = json->GetNormalizationPositionDefaultValue(i); + + _physicsRig->Settings[i].NormalizationAngle.Minimum = json->GetNormalizationAngleMinimumValue(i); + _physicsRig->Settings[i].NormalizationAngle.Maximum = json->GetNormalizationAngleMaximumValue(i); + _physicsRig->Settings[i].NormalizationAngle.Default = json->GetNormalizationAngleDefaultValue(i); + + // Input + _physicsRig->Settings[i].InputCount = json->GetInputCount(i); + _physicsRig->Settings[i].BaseInputIndex = inputIndex; + for (csmInt32 j = 0; j < _physicsRig->Settings[i].InputCount; ++j) + { + _physicsRig->Inputs[inputIndex + j].SourceParameterIndex = -1; + _physicsRig->Inputs[inputIndex + j].Weight = json->GetInputWeight(i, j); + _physicsRig->Inputs[inputIndex + j].Reflect = json->GetInputReflect(i, j); + + if (strcmp(json->GetInputType(i, j), PhysicsTypeTagX) == 0) + { + _physicsRig->Inputs[inputIndex + j].Type = CubismPhysicsSource_X; + _physicsRig->Inputs[inputIndex + j].GetNormalizedParameterValue = GetInputTranslationXFromNormalizedParameterValue; + } + else if (strcmp(json->GetInputType(i, j), PhysicsTypeTagY) == 0) + { + _physicsRig->Inputs[inputIndex + j].Type = CubismPhysicsSource_Y; + _physicsRig->Inputs[inputIndex + j].GetNormalizedParameterValue = GetInputTranslationYFromNormalizedParameterValue; + } + else if (strcmp(json->GetInputType(i, j), PhysicsTypeTagAngle) == 0) + { + _physicsRig->Inputs[inputIndex + j].Type = CubismPhysicsSource_Angle; + _physicsRig->Inputs[inputIndex + j].GetNormalizedParameterValue = GetInputAngleFromNormalizedParameterValue; + } + + _physicsRig->Inputs[inputIndex + j].Source.TargetType = CubismPhysicsTargetType_Parameter; + _physicsRig->Inputs[inputIndex + j].Source.Id = json->GetInputSourceId(i, j); + } + inputIndex += _physicsRig->Settings[i].InputCount; + + // Output + _physicsRig->Settings[i].OutputCount = json->GetOutputCount(i); + _physicsRig->Settings[i].BaseOutputIndex = outputIndex; + + PhysicsOutput currentRigOutput; + currentRigOutput.outputs.Resize(_physicsRig->Settings[i].OutputCount); + _currentRigOutputs.PushBack(currentRigOutput); + + PhysicsOutput previousRigOutput; + previousRigOutput.outputs.Resize(_physicsRig->Settings[i].OutputCount); + _previousRigOutputs.PushBack(previousRigOutput); + + for (csmInt32 j = 0; j < _physicsRig->Settings[i].OutputCount; ++j) + { + _physicsRig->Outputs[outputIndex + j].DestinationParameterIndex = -1; + _physicsRig->Outputs[outputIndex + j].VertexIndex = json->GetOutputVertexIndex(i, j); + _physicsRig->Outputs[outputIndex + j].AngleScale = json->GetOutputAngleScale(i, j); + _physicsRig->Outputs[outputIndex + j].Weight = json->GetOutputWeight(i, j); + _physicsRig->Outputs[outputIndex + j].Destination.TargetType = CubismPhysicsTargetType_Parameter; + + _physicsRig->Outputs[outputIndex + j].Destination.Id = json->GetOutputsDestinationId(i, j); + if (strcmp(json->GetOutputType(i, j), PhysicsTypeTagX) == 0) + { + _physicsRig->Outputs[outputIndex + j].Type = CubismPhysicsSource_X; + _physicsRig->Outputs[outputIndex + j].GetValue = GetOutputTranslationX; + _physicsRig->Outputs[outputIndex + j].GetScale = GetOutputScaleTranslationX; + } + else if (strcmp(json->GetOutputType(i, j), PhysicsTypeTagY) == 0) + { + _physicsRig->Outputs[outputIndex + j].Type = CubismPhysicsSource_Y; + _physicsRig->Outputs[outputIndex + j].GetValue = GetOutputTranslationY; + _physicsRig->Outputs[outputIndex + j].GetScale = GetOutputScaleTranslationY; + } + else if (strcmp(json->GetOutputType(i, j), PhysicsTypeTagAngle) == 0) + { + _physicsRig->Outputs[outputIndex + j].Type = CubismPhysicsSource_Angle; + _physicsRig->Outputs[outputIndex + j].GetValue = GetOutputAngle; + _physicsRig->Outputs[outputIndex + j].GetScale = GetOutputScaleAngle; + } + + _physicsRig->Outputs[outputIndex + j].Reflect = json->GetOutputReflect(i, j); + } + outputIndex += _physicsRig->Settings[i].OutputCount; + + // Particle + _physicsRig->Settings[i].ParticleCount = json->GetParticleCount(i); + _physicsRig->Settings[i].BaseParticleIndex = particleIndex; + for (csmInt32 j = 0; j < _physicsRig->Settings[i].ParticleCount; ++j) + { + _physicsRig->Particles[particleIndex + j].Mobility = json->GetParticleMobility(i, j); + _physicsRig->Particles[particleIndex + j].Delay = json->GetParticleDelay(i, j); + _physicsRig->Particles[particleIndex + j].Acceleration = json->GetParticleAcceleration(i, j); + _physicsRig->Particles[particleIndex + j].Radius = json->GetParticleRadius(i, j); + _physicsRig->Particles[particleIndex + j].Position = json->GetParticlePosition(i, j); + } + + particleIndex += _physicsRig->Settings[i].ParticleCount; + } + + Initialize(); + + CSM_DELETE(json); +} + + +void CubismPhysics::Stabilization(CubismModel* model) +{ + csmFloat32 totalAngle; + csmFloat32 weight; + csmFloat32 radAngle; + csmFloat32 outputValue; + CubismVector2 totalTranslation; + csmInt32 i, settingIndex, particleIndex; + CubismPhysicsSubRig* currentSetting; + CubismPhysicsInput* currentInputs; + CubismPhysicsOutput* currentOutputs; + CubismPhysicsParticle* currentParticles; + + csmFloat32* parameterValues; + const csmFloat32* parameterMaximumValues; + const csmFloat32* parameterMinimumValues; + const csmFloat32* parameterDefaultValues; + + parameterValues = Core::csmGetParameterValues(model->GetModel()); + parameterMaximumValues = Core::csmGetParameterMaximumValues(model->GetModel()); + parameterMinimumValues = Core::csmGetParameterMinimumValues(model->GetModel()); + parameterDefaultValues = Core::csmGetParameterDefaultValues(model->GetModel()); + + if (_parameterCaches.GetSize() < (csmUint32)model->GetParameterCount()) + { + _parameterCaches.Resize(model->GetParameterCount()); + } + if (_parameterInputCaches.GetSize() < (csmUint32)model->GetParameterCount()) + { + _parameterInputCaches.Resize(model->GetParameterCount()); + } + + for (int j = 0; j < model->GetParameterCount(); ++j) + { + _parameterCaches[j] = parameterValues[j]; + _parameterInputCaches[j] = parameterValues[j]; + } + + for (settingIndex = 0; settingIndex < _physicsRig->SubRigCount; ++settingIndex) + { + totalAngle = 0.0f; + totalTranslation.X = 0.0f; + totalTranslation.Y = 0.0f; + currentSetting = &_physicsRig->Settings[settingIndex]; + currentInputs = &_physicsRig->Inputs[currentSetting->BaseInputIndex]; + currentOutputs = &_physicsRig->Outputs[currentSetting->BaseOutputIndex]; + currentParticles = &_physicsRig->Particles[currentSetting->BaseParticleIndex]; + + // Load input parameters + for (i = 0; i < currentSetting->InputCount; ++i) + { + weight = currentInputs[i].Weight / MaximumWeight; + + if (currentInputs[i].SourceParameterIndex == -1) + { + currentInputs[i].SourceParameterIndex = model->GetParameterIndex(currentInputs[i].Source.Id); + } + + currentInputs[i].GetNormalizedParameterValue( + &totalTranslation, + &totalAngle, + parameterValues[currentInputs[i].SourceParameterIndex], + parameterMinimumValues[currentInputs[i].SourceParameterIndex], + parameterMaximumValues[currentInputs[i].SourceParameterIndex], + parameterDefaultValues[currentInputs[i].SourceParameterIndex], + ¤tSetting->NormalizationPosition, + ¤tSetting->NormalizationAngle, + currentInputs[i].Reflect, + weight + ); + + _parameterCaches[currentInputs[i].SourceParameterIndex] = + parameterValues[currentInputs[i].SourceParameterIndex]; + } + + radAngle = CubismMath::DegreesToRadian(-totalAngle); + + totalTranslation.X = (totalTranslation.X * CubismMath::CosF(radAngle) - totalTranslation.Y * CubismMath::SinF(radAngle)); + totalTranslation.Y = (totalTranslation.X * CubismMath::SinF(radAngle) + totalTranslation.Y * CubismMath::CosF(radAngle)); + + // Calculate particles position. + UpdateParticlesForStabilization( + currentParticles, + currentSetting->ParticleCount, + totalTranslation, + totalAngle, + _options.Wind, + MovementThreshold * currentSetting->NormalizationPosition.Maximum + ); + + // Update output parameters. + for (i = 0; i < currentSetting->OutputCount; ++i) + { + particleIndex = currentOutputs[i].VertexIndex; + + if (currentOutputs[i].DestinationParameterIndex == -1) + { + currentOutputs[i].DestinationParameterIndex = model->GetParameterIndex( + currentOutputs[i].Destination.Id); + } + + if (particleIndex < 1 || particleIndex >= currentSetting->ParticleCount) + { + continue; + } + + CubismVector2 translation; + translation.X = currentParticles[particleIndex].Position.X - currentParticles[particleIndex - 1].Position.X; + translation.Y = currentParticles[particleIndex].Position.Y - currentParticles[particleIndex - 1].Position.Y; + + outputValue = currentOutputs[i].GetValue( + translation, + currentParticles, + particleIndex, + currentOutputs[i].Reflect, + _options.Gravity + ); + + _currentRigOutputs[settingIndex].outputs[i] = outputValue; + _previousRigOutputs[settingIndex].outputs[i] = outputValue; + + UpdateOutputParameterValue( + ¶meterValues[currentOutputs[i].DestinationParameterIndex], + parameterMinimumValues[currentOutputs[i].DestinationParameterIndex], + parameterMaximumValues[currentOutputs[i].DestinationParameterIndex], + outputValue, + ¤tOutputs[i]); + + _parameterCaches[currentOutputs[i].DestinationParameterIndex] = parameterValues[currentOutputs[i].DestinationParameterIndex]; + } + } +} + +/// Pendulum interpolation weights +/// +/// 振り子の計算結果は保存され、パラメータへの出力は保存された前回の結果で補間されます。 +/// The result of the pendulum calculation is saved and +/// the output to the parameters is interpolated with the saved previous result of the pendulum calculation. +/// +/// 図で示すと[1]と[2]で補間されます。 +/// The figure shows the interpolation between [1] and [2]. +/// +/// 補間の重みは最新の振り子計算タイミングと次回のタイミングの間で見た現在時間で決定する。 +/// The weight of the interpolation are determined by the current time seen between +/// the latest pendulum calculation timing and the next timing. +/// +/// 図で示すと[2]と[4]の間でみた(3)の位置の重みになる。 +/// Figure shows the weight of position (3) as seen between [2] and [4]. +/// +/// 解釈として振り子計算のタイミングと重み計算のタイミングがズレる。 +/// As an interpretation, the pendulum calculation and weights are misaligned. +/// +/// physics3.jsonにFPS情報が存在しない場合は常に前の振り子状態で設定される。 +/// If there is no FPS information in physics3.json, it is always set in the previous pendulum state. +/// +/// この仕様は補間範囲を逸脱したことが原因の震えたような見た目を回避を目的にしている。 +/// The purpose of this specification is to avoid the quivering appearance caused by deviations from the interpolation range. +/// +/// ------------ time --------------> +/// +///         |+++++|------| <- weight +/// ==[1]====#=====[2]---(3)----(4) +/// ^ output contents +/// +/// 1:_previousRigOutputs +/// 2:_currentRigOutputs +/// 3:_currentRemainTime (now rendering) +/// 4:next particles timing +/// +/// @param model +/// @param deltaTimeSeconds rendering delta time. +void CubismPhysics::Evaluate(CubismModel* model, csmFloat32 deltaTimeSeconds) +{ + csmFloat32 totalAngle; + csmFloat32 weight; + csmFloat32 radAngle; + csmFloat32 outputValue; + CubismVector2 totalTranslation; + csmInt32 i, settingIndex, particleIndex; + CubismPhysicsSubRig* currentSetting; + CubismPhysicsInput* currentInputs; + CubismPhysicsOutput* currentOutputs; + CubismPhysicsParticle* currentParticles; + + if (0.0f >= deltaTimeSeconds) + { + return; + } + + csmFloat32* parameterValues; + const csmFloat32* parameterMaximumValues; + const csmFloat32* parameterMinimumValues; + const csmFloat32* parameterDefaultValues; + + csmFloat32 physicsDeltaTime; + _currentRemainTime += deltaTimeSeconds; + if (_currentRemainTime > MaxDeltaTime) + { + _currentRemainTime = 0.0f; + } + + parameterValues = Core::csmGetParameterValues(model->GetModel()); + parameterMaximumValues = Core::csmGetParameterMaximumValues(model->GetModel()); + parameterMinimumValues = Core::csmGetParameterMinimumValues(model->GetModel()); + parameterDefaultValues = Core::csmGetParameterDefaultValues(model->GetModel()); + + if (_parameterCaches.GetSize() < (uint32)model->GetParameterCount()) + { + _parameterCaches.Resize(model->GetParameterCount()); + } + if (_parameterInputCaches.GetSize() < (uint32)model->GetParameterCount()) + { + _parameterInputCaches.Resize(model->GetParameterCount()); + for (int j = 0; j < model->GetParameterCount(); ++j) { + _parameterInputCaches[j] = parameterValues[j]; + } + } + + if (_physicsRig->Fps > 0.0f) + { + physicsDeltaTime = 1.0f / _physicsRig->Fps; + } + else + { + physicsDeltaTime = deltaTimeSeconds; + } + + while (_currentRemainTime >= physicsDeltaTime) + { + // copyRigOutputs _currentRigOutputs to _previousRigOutputs + for (settingIndex = 0; settingIndex < _physicsRig->SubRigCount; ++settingIndex) + { + currentSetting = &_physicsRig->Settings[settingIndex]; + currentOutputs = &_physicsRig->Outputs[currentSetting->BaseOutputIndex]; + for (i = 0; i < currentSetting->OutputCount; ++i) + { + _previousRigOutputs[settingIndex].outputs[i] = _currentRigOutputs[settingIndex].outputs[i]; + } + } + + // 入力キャッシュとパラメータで線形補間してUpdateParticlesするタイミングでの入力を計算する。 + // Calculate the input at the timing to UpdateParticles by linear interpolation with the _parameterInputCaches and parameterValues. + // _parameterCachesはグループ間での値の伝搬の役割があるので_parameterInputCachesとの分離が必要。 + // _parameterCaches needs to be separated from _parameterInputCaches because of its role in propagating values between groups. + float inputWeight = physicsDeltaTime / _currentRemainTime; + for (csmInt32 j = 0; j < model->GetParameterCount(); ++j) + { + _parameterCaches[j] = _parameterInputCaches[j] * (1.0f - inputWeight) + parameterValues[j] * inputWeight; + _parameterInputCaches[j] = _parameterCaches[j]; + } + + for (settingIndex = 0; settingIndex < _physicsRig->SubRigCount; ++settingIndex) + { + totalAngle = 0.0f; + totalTranslation.X = 0.0f; + totalTranslation.Y = 0.0f; + currentSetting = &_physicsRig->Settings[settingIndex]; + currentInputs = &_physicsRig->Inputs[currentSetting->BaseInputIndex]; + currentOutputs = &_physicsRig->Outputs[currentSetting->BaseOutputIndex]; + currentParticles = &_physicsRig->Particles[currentSetting->BaseParticleIndex]; + + // Load input parameters. + for (i = 0; i < currentSetting->InputCount; ++i) + { + weight = currentInputs[i].Weight / MaximumWeight; + + if (currentInputs[i].SourceParameterIndex == -1) + { + currentInputs[i].SourceParameterIndex = model->GetParameterIndex(currentInputs[i].Source.Id); + } + + currentInputs[i].GetNormalizedParameterValue( + &totalTranslation, + &totalAngle, + _parameterCaches[currentInputs[i].SourceParameterIndex], + parameterMinimumValues[currentInputs[i].SourceParameterIndex], + parameterMaximumValues[currentInputs[i].SourceParameterIndex], + parameterDefaultValues[currentInputs[i].SourceParameterIndex], + ¤tSetting->NormalizationPosition, + ¤tSetting->NormalizationAngle, + currentInputs[i].Reflect, + weight + ); + } + + radAngle = CubismMath::DegreesToRadian(-totalAngle); + + totalTranslation.X = (totalTranslation.X * CubismMath::CosF(radAngle) - totalTranslation.Y * CubismMath::SinF(radAngle)); + totalTranslation.Y = (totalTranslation.X * CubismMath::SinF(radAngle) + totalTranslation.Y * CubismMath::CosF(radAngle)); + + // Calculate particles position. + UpdateParticles( + currentParticles, + currentSetting->ParticleCount, + totalTranslation, + totalAngle, + _options.Wind, + MovementThreshold * currentSetting->NormalizationPosition.Maximum, + physicsDeltaTime, + AirResistance + ); + + // Update output parameters. + for (i = 0; i < currentSetting->OutputCount; ++i) + { + particleIndex = currentOutputs[i].VertexIndex; + + if (currentOutputs[i].DestinationParameterIndex == -1) + { + currentOutputs[i].DestinationParameterIndex = model->GetParameterIndex(currentOutputs[i].Destination.Id); + } + + if (particleIndex < 1 || particleIndex >= currentSetting->ParticleCount) + { + continue; + } + + CubismVector2 translation; + translation.X = currentParticles[particleIndex].Position.X - currentParticles[particleIndex - 1].Position.X; + translation.Y = currentParticles[particleIndex].Position.Y - currentParticles[particleIndex - 1].Position.Y; + + outputValue = currentOutputs[i].GetValue( + translation, + currentParticles, + particleIndex, + currentOutputs[i].Reflect, + _options.Gravity + ); + + _currentRigOutputs[settingIndex].outputs[i] = outputValue; + + UpdateOutputParameterValue( + &_parameterCaches[currentOutputs[i].DestinationParameterIndex], + parameterMinimumValues[currentOutputs[i].DestinationParameterIndex], + parameterMaximumValues[currentOutputs[i].DestinationParameterIndex], + outputValue, + ¤tOutputs[i]); + } + } + + _currentRemainTime -= physicsDeltaTime; + } + + const float alpha = _currentRemainTime / physicsDeltaTime; + Interpolate(model, alpha); +} + +void CubismPhysics::Interpolate(CubismModel* model, csmFloat32 weight) +{ + csmInt32 i, settingIndex; + CubismPhysicsOutput* currentOutputs; + CubismPhysicsSubRig* currentSetting; + csmFloat32* parameterValues; + const csmFloat32* parameterMaximumValues; + const csmFloat32* parameterMinimumValues; + + parameterValues = Core::csmGetParameterValues(model->GetModel()); + parameterMaximumValues = Core::csmGetParameterMaximumValues(model->GetModel()); + parameterMinimumValues = Core::csmGetParameterMinimumValues(model->GetModel()); + + for (settingIndex = 0; settingIndex < _physicsRig->SubRigCount; ++settingIndex) + { + currentSetting = &_physicsRig->Settings[settingIndex]; + currentOutputs = &_physicsRig->Outputs[currentSetting->BaseOutputIndex]; + + // Load input parameters. + for (i = 0; i < currentSetting->OutputCount; ++i) + { + if (currentOutputs[i].DestinationParameterIndex == -1) + { + continue; + } + + UpdateOutputParameterValue( + ¶meterValues[currentOutputs[i].DestinationParameterIndex], + parameterMinimumValues[currentOutputs[i].DestinationParameterIndex], + parameterMaximumValues[currentOutputs[i].DestinationParameterIndex], + _previousRigOutputs[settingIndex].outputs[i] * (1 - weight) + _currentRigOutputs[settingIndex].outputs[i] * weight, + ¤tOutputs[i] + ); + } + } +} + +void CubismPhysics::SetOptions(const Options& options) +{ + _options = options; +} + +const CubismPhysics::Options& CubismPhysics::GetOptions() const +{ + return _options; +} + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Physics/CubismPhysics.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "Math/CubismVector2.hpp" +#include "CubismPhysicsInternal.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +class CubismModel; +struct CubismPhysicsRig; + +/** + * @brief 物理演算クラス + * + * 物理演算のクラス。 + */ +class CubismPhysics +{ +public: + /** + * @brief オプション + * + * 物理演算のオプション。 + */ + struct Options + { + CubismVector2 Gravity; ///< 重力方向 + CubismVector2 Wind; ///< 風の方向 + }; + + /** + * @brief 物理演算出力結果 + * + * パラメータに適用する前の物理演算の出力結果 + */ + struct PhysicsOutput + { + csmVector outputs; + }; + + /** + * @brief インスタンスの作成 + * + * インスタンスを作成する。 + * + * @param[in] buffer physics3.jsonが読み込まれいるバッファ + * @param[in] size バッファのサイズ + * @return 作成されたインスタンス + */ + static CubismPhysics* Create(const csmByte* buffer, csmSizeInt size); + + /** + * @brief インスタンスの破棄 + * + * インスタンスを破棄する。 + * + * @param[in] physics 破棄するインスタンス + */ + static void Delete(CubismPhysics* physics); + + /** + * @brief パラメータのリセット + * + * パラメータをリセットする。 + */ + void Reset(); + + /** + * @brief 現在のパラメータ値で物理演算が安定化する状態を演算する。 + * @param[in] model 物理演算の結果を適用するモデル + */ + void Stabilization(CubismModel* model); + + /** + * @brief 物理演算の評価 + * + * 物理演算を評価する。 + * + * @param[in] model 物理演算の結果を適用するモデル + * @param[in] deltaTimeSeconds デルタ時間[秒] + */ + void Evaluate(CubismModel* model, csmFloat32 deltaTimeSeconds); + + /** + * @brief オプションの設定 + * + * オプションを設定する。 + * + * @param[in] options オプション + */ + void SetOptions(const Options& options); + + /** + * @brief オプションの取得 + * + * オプションを取得する。 + * + * @return オプション + */ + const Options& GetOptions() const; + +private: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + */ + CubismPhysics(); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismPhysics(); + + // Prevention of copy Constructor + CubismPhysics(const CubismPhysics&); + CubismPhysics& operator=(const CubismPhysics&); + + /** + * @brief physics3.jsonのパース + * + * physics3.jsonをパースする。 + * + * @param[in] physicsJson physics3.jsonが読み込まれているバッファ + * @param[in] size バッファのサイズ + */ + void Parse(const csmByte* physicsJson, csmSizeInt size); + + /** + * @brief 初期化 + * + * 初期化する。 + */ + void Initialize(); + + /** + * @brief 物理演算結果の適用 + * + * 振り子演算の最新の結果と一つ前の結果から指定した重みで適用する。 + * + * @param model 物理演算の結果を適用するモデル + * @param weight 最新結果の重み + */ + void Interpolate(CubismModel* model, csmFloat32 weight); + + CubismPhysicsRig* _physicsRig; ///< 物理演算のデータ + Options _options; ///< オプション + + csmVector _currentRigOutputs; ///< 最新の振り子計算の結果 + csmVector _previousRigOutputs; ///< 一つ前の振り子計算の結果 + + csmFloat32 _currentRemainTime; ///< 物理演算が処理していない時間 + + csmVector _parameterCaches; ///< Evaluateで利用するパラメータのキャッシュ + csmVector _parameterInputCaches; ///< UpdateParticlesが動くときの入力をキャッシュ + + csmBool _isJsonValid; ///< 正しくJsonデータが取得出来たか +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Physics/CubismPhysicsInternal.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + + +#pragma once + +#include "Model/CubismModel.hpp" +#include "Math/CubismVector2.hpp" +#include "Id/CubismId.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief 物理演算の適用先の種類 + * + * 物理演算の適用先の種類。 + */ +enum CubismPhysicsTargetType +{ + CubismPhysicsTargetType_Parameter, ///< パラメータに対して適用 +}; + +/** + * @brief 物理演算の入力の種類 + * + * 物理演算の入力の種類。 + */ +enum CubismPhysicsSource +{ + CubismPhysicsSource_X, ///< X軸の位置から + CubismPhysicsSource_Y, ///< Y軸の位置から + CubismPhysicsSource_Angle, ///< 角度から +}; + +/** + * @brief 物理演算で使用する外部の力 + * + * 物理演算で使用する外部の力。 + */ +struct PhysicsJsonEffectiveForces +{ + CubismVector2 Gravity; ///< 重力 + CubismVector2 Wind; ///< 風 +}; + +/** + * @brief 物理演算のパラメータ情報 + * + * 物理演算のパラメータ情報。 + */ +struct CubismPhysicsParameter +{ + CubismIdHandle Id; ///< パラメータID + CubismPhysicsTargetType TargetType; ///< 適用先の種類 +}; + +/** + * @brief 物理演算の正規化情報 + * + * 物理演算の正規化情報。 + */ +struct CubismPhysicsNormalization +{ + csmFloat32 Minimum; ///< 最大値 + csmFloat32 Maximum; ///< 最小値 + csmFloat32 Default; ///< デフォルト値 +}; + +/** + * @brief 物理演算の演算に使用する物理点の情報 + * + * 物理演算の演算に使用する物理点の情報。 + */ +struct CubismPhysicsParticle +{ + CubismVector2 InitialPosition; ///< 初期位置 + csmFloat32 Mobility; ///< 動きやすさ + csmFloat32 Delay; ///< 遅れ + csmFloat32 Acceleration; ///< 加速度 + csmFloat32 Radius; ///< 距離 + CubismVector2 Position; ///< 現在の位置 + CubismVector2 LastPosition; ///< 最後の位置 + CubismVector2 LastGravity; ///< 最後の重力 + CubismVector2 Force; ///< 現在かかっている力 + CubismVector2 Velocity; ///< 現在の速度 +}; + +/** + * @brief 物理演算の物理点の管理 + * + * 物理演算の物理点の管理。 + */ +struct CubismPhysicsSubRig +{ + csmInt32 InputCount; ///< 入力の個数 + csmInt32 OutputCount; ///< 出力の個数 + csmInt32 ParticleCount; ///< 物理点の個数 + csmInt32 BaseInputIndex; ///< 入力の最初のインデックス + csmInt32 BaseOutputIndex; ///< 出力の最初のインデックス + csmInt32 BaseParticleIndex; ///< 物理点の最初のインデックス + CubismPhysicsNormalization NormalizationPosition; ///< 正規化された位置 + CubismPhysicsNormalization NormalizationAngle; ///< 正規化された角度 +}; + +/** + * @brief 正規化されたパラメータの取得関数の宣言 + * + * 正規化されたパラメータの取得関数の宣言。 + * + * @param[out] targetTranslation 演算結果の移動値 + * @param[out] targetAngle 演算結果の角度 + * @param[in] value パラメータの値 + * @param[in] parameterMinimumValue パラメータの最小値 + * @param[in] parameterMaximumValue パラメータの最大値 + * @param[in] parameterDefaultValue パラメータのデフォルト値 + * @param[in] normalizationPosition 正規化された位置 + * @param[in] normalizationAngle 正規化された角度 + * @param[in] isInverted 値が反転されているか? + * @param[in] weight 重み + */ +typedef void (*NormalizedPhysicsParameterValueGetter)( + CubismVector2* targetTranslation, + csmFloat32* targetAngle, + csmFloat32 value, + csmFloat32 parameterMinimumValue, + csmFloat32 parameterMaximumValue, + csmFloat32 parameterDefaultValue, + CubismPhysicsNormalization* normalizationPosition, + CubismPhysicsNormalization* normalizationAngle, + csmInt32 isInverted, + csmFloat32 weight +); + +/** + * @brief 物理演算の値の取得関数の宣言 + * + * 物理演算の値の取得関数の宣言。 + * + * @param[in] translation 移動値 + * @param[in] particles 物理点のリスト + * @param[in] isInverted 値が反転されているか? + * @param[in] parentGravity 重力 + * @return 値 + */ +typedef csmFloat32 (*PhysicsValueGetter)( + CubismVector2 translation, + CubismPhysicsParticle* particles, + csmInt32 particleIndex, + csmInt32 isInverted, + CubismVector2 parentGravity +); + +/** + * @brief 物理演算のスケールの取得関数の宣言 + * + * 物理演算のスケールの取得関数の宣言。 + * + * @param[in] translationScale 移動値のスケール + * @param[in] angleScale 角度のスケール + * @return スケール値 + */ +typedef csmFloat32 (*PhysicsScaleGetter)(CubismVector2 translationScale, csmFloat32 angleScale); + +/** + * @brief 物理演算の入力情報 + * + * 物理演算の入力情報。 + */ +struct CubismPhysicsInput +{ + CubismPhysicsParameter Source; ///< 入力元のパラメータ + csmInt32 SourceParameterIndex; ///< 入力元のパラメータのインデックス + csmFloat32 Weight; ///< 重み + csmInt16 Type; ///< 入力の種類 + csmInt16 Reflect; ///< 値が反転されているかどうか + NormalizedPhysicsParameterValueGetter GetNormalizedParameterValue; ///< 正規化されたパラメータ値の取得関数 +}; + +/** + * @brief 物理演算の出力情報 + * + * 物理演算の出力情報。 + */ +struct CubismPhysicsOutput +{ + CubismPhysicsParameter Destination; ///< 出力先のパラメータ + csmInt32 DestinationParameterIndex; ///< 出力先のパラメータのインデックス + csmInt32 VertexIndex; ///< 振り子のインデックス + CubismVector2 TranslationScale; ///< 移動値のスケール + csmFloat32 AngleScale; ///< 角度のスケール + csmFloat32 Weight; /// 重み + CubismPhysicsSource Type; ///< 出力の種類 + csmInt16 Reflect; ///< 値が反転されているかどうか + csmFloat32 ValueBelowMinimum; ///< 最小値を下回った時の値 + csmFloat32 ValueExceededMaximum; ///< 最大値をこえた時の値 + PhysicsValueGetter GetValue; ///< 物理演算の値の取得関数 + PhysicsScaleGetter GetScale; ///< 物理演算のスケール値の取得関数 +}; + +/** + * @brief 物理演算のデータ + * + * 物理演算のデータ。 + */ +struct CubismPhysicsRig +{ + csmInt32 SubRigCount; ///< 物理演算の物理点の個数 + csmVector Settings; ///< 物理演算の物理点の管理のリスト + csmVector Inputs; ///< 物理演算の入力のリスト + csmVector Outputs; ///< 物理演算の出力のリスト + csmVector Particles; ///< 物理演算の物理点のリスト + CubismVector2 Gravity; ///< 重力 + CubismVector2 Wind; ///< 風 + csmFloat32 Fps; ///< 物理演算動作FPS +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Physics/CubismPhysicsJson.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismPhysicsJson.hpp" +#include "Id/CubismIdManager.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/// Physics Json Constants +namespace { +// JSON keys +const csmChar* Position = "Position"; +const csmChar* X = "X"; +const csmChar* Y = "Y"; +const csmChar* Angle = "Angle"; +const csmChar* Type = "Type"; +const csmChar* Id = "Id"; + +// Meta +const csmChar* Meta = "Meta"; +const csmChar* EffectiveForces = "EffectiveForces"; +const csmChar* TotalInputCount = "TotalInputCount"; +const csmChar* TotalOutputCount = "TotalOutputCount"; +const csmChar* PhysicsSettingCount = "PhysicsSettingCount"; +const csmChar* Gravity = "Gravity"; +const csmChar* Wind = "Wind"; +const csmChar* VertexCount = "VertexCount"; +const csmChar* Fps = "Fps"; + +// PhysicsSettings +const csmChar* PhysicsSettings = "PhysicsSettings"; +const csmChar* Normalization = "Normalization"; +const csmChar* Minimum = "Minimum"; +const csmChar* Maximum = "Maximum"; +const csmChar* Default = "Default"; +const csmChar* Reflect = "Reflect"; +const csmChar* Weight = "Weight"; + +// Input +const csmChar* Input = "Input"; +const csmChar* Source = "Source"; + +// Output +const csmChar* Output = "Output"; +const csmChar* Scale = "Scale"; +const csmChar* VertexIndex = "VertexIndex"; +const csmChar* Destination = "Destination"; + +// Particle +const csmChar* Vertices = "Vertices"; +const csmChar* Mobility = "Mobility"; +const csmChar* Delay = "Delay"; +const csmChar* Radius = "Radius"; +const csmChar* Acceleration = "Acceleration"; +} + +CubismPhysicsJson::CubismPhysicsJson(const csmByte* buffer, csmSizeInt size) +{ + CreateCubismJson(buffer, size); +} + +CubismPhysicsJson::~CubismPhysicsJson() +{ + DeleteCubismJson(); +} + +CubismVector2 CubismPhysicsJson::GetGravity() const +{ + CubismVector2 ret; + ret.X = _json->GetRoot()[Meta][EffectiveForces][Gravity][X].ToFloat(); + ret.Y = _json->GetRoot()[Meta][EffectiveForces][Gravity][Y].ToFloat(); + return ret; +} + +CubismVector2 CubismPhysicsJson::GetWind() const +{ + CubismVector2 ret; + ret.X = _json->GetRoot()[Meta][EffectiveForces][Wind][X].ToFloat(); + ret.Y = _json->GetRoot()[Meta][EffectiveForces][Wind][Y].ToFloat(); + return ret; +} + +csmFloat32 CubismPhysicsJson::GetFps() const +{ + // if FPS information does not exist in physics3.json, 0.0f is returned. + return _json->GetRoot()[Meta][Fps].ToFloat(0.0f); +} + +csmInt32 CubismPhysicsJson::GetSubRigCount() const +{ + return _json->GetRoot()[Meta][PhysicsSettingCount].ToInt(); +} + +csmInt32 CubismPhysicsJson::GetTotalInputCount() const +{ + return _json->GetRoot()[Meta][TotalInputCount].ToInt(); +} + +csmInt32 CubismPhysicsJson::GetTotalOutputCount() const +{ + return _json->GetRoot()[Meta][TotalOutputCount].ToInt(); +} + +csmInt32 CubismPhysicsJson::GetVertexCount() const +{ + return _json->GetRoot()[Meta][VertexCount].ToInt(); +} + +// Input +csmFloat32 CubismPhysicsJson::GetNormalizationPositionMinimumValue(csmInt32 physicsSettingIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Normalization][Position][Minimum].ToFloat(); +} + +csmFloat32 CubismPhysicsJson::GetNormalizationPositionMaximumValue(csmInt32 physicsSettingIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Normalization][Position][Maximum].ToFloat(); +} + +csmFloat32 CubismPhysicsJson::GetNormalizationPositionDefaultValue(csmInt32 physicsSettingIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Normalization][Position][Default].ToFloat(); +} + +csmFloat32 CubismPhysicsJson::GetNormalizationAngleMinimumValue(csmInt32 physicsSettingIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Normalization][Angle][Minimum].ToFloat(); +} + +csmFloat32 CubismPhysicsJson::GetNormalizationAngleMaximumValue(csmInt32 physicsSettingIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Normalization][Angle][Maximum].ToFloat(); +} + +csmFloat32 CubismPhysicsJson::GetNormalizationAngleDefaultValue(csmInt32 physicsSettingIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Normalization][Angle][Default].ToFloat(); +} + +csmInt32 CubismPhysicsJson::GetInputCount(csmInt32 physicsSettingIndex) const +{ + return static_cast(_json->GetRoot()[PhysicsSettings][physicsSettingIndex][Input].GetVector()->GetSize()); +} + +csmFloat32 CubismPhysicsJson::GetInputWeight(csmInt32 physicsSettingIndex, csmInt32 inputIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Input][inputIndex][Weight].ToFloat(); +} + +csmBool CubismPhysicsJson::GetInputReflect(csmInt32 physicsSettingIndex, csmInt32 inputIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Input][inputIndex][Reflect].ToBoolean(); +} + +const csmChar* CubismPhysicsJson::GetInputType(csmInt32 physicsSettingIndex, csmInt32 inputIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Input][inputIndex][Type].GetRawString(); +} + +CubismIdHandle CubismPhysicsJson::GetInputSourceId(csmInt32 physicsSettingIndex, csmInt32 inputIndex) const +{ + return CubismFramework::GetIdManager()->GetId(_json->GetRoot()[PhysicsSettings][physicsSettingIndex][Input][inputIndex][Source][Id].GetRawString()); +} + +// Output +csmInt32 CubismPhysicsJson::GetOutputCount(csmInt32 physicsSettingIndex) const +{ + return static_cast(_json->GetRoot()[PhysicsSettings][physicsSettingIndex][Output].GetVector()->GetSize()); +} + +csmInt32 CubismPhysicsJson::GetOutputVertexIndex(csmInt32 physicsSettingIndex, csmInt32 outputIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Output][outputIndex][VertexIndex].ToInt(); +} + +csmFloat32 CubismPhysicsJson::GetOutputAngleScale(csmInt32 physicsSettingIndex, csmInt32 outputIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Output][outputIndex][Scale].ToFloat(); +} + +csmFloat32 CubismPhysicsJson::GetOutputWeight(csmInt32 physicsSettingIndex, csmInt32 outputIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Output][outputIndex][Weight].ToFloat(); +} + +CubismIdHandle CubismPhysicsJson::GetOutputsDestinationId(csmInt32 physicsSettingIndex, csmInt32 outputIndex) const +{ + return CubismFramework::GetIdManager()->GetId(_json->GetRoot()[PhysicsSettings][physicsSettingIndex][Output][outputIndex][Destination][Id].GetRawString()); +} + +const csmChar* CubismPhysicsJson::GetOutputType(csmInt32 physicsSettingIndex, csmInt32 outputIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Output][outputIndex][Type].GetRawString(); +} + +csmBool CubismPhysicsJson::GetOutputReflect(csmInt32 physicsSettingIndex, csmInt32 outputIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Output][outputIndex][Reflect].ToBoolean(); +} + +// Particle +csmInt32 CubismPhysicsJson::GetParticleCount(csmInt32 physicsSettingIndex) const +{ + return static_cast(_json->GetRoot()[PhysicsSettings][physicsSettingIndex][Vertices].GetVector()->GetSize()); +} + +csmFloat32 CubismPhysicsJson::GetParticleMobility(csmInt32 physicsSettingIndex, csmInt32 vertexIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Vertices][vertexIndex][Mobility].ToFloat(); +} + +csmFloat32 CubismPhysicsJson::GetParticleDelay(csmInt32 physicsSettingIndex, csmInt32 vertexIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Vertices][vertexIndex][Delay].ToFloat(); +} + +csmFloat32 CubismPhysicsJson::GetParticleAcceleration(csmInt32 physicsSettingIndex, csmInt32 vertexIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Vertices][vertexIndex][Acceleration].ToFloat(); +} + +csmFloat32 CubismPhysicsJson::GetParticleRadius(csmInt32 physicsSettingIndex, csmInt32 vertexIndex) const +{ + return _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Vertices][vertexIndex][Radius].ToFloat(); +} + +CubismVector2 CubismPhysicsJson::GetParticlePosition(csmInt32 physicsSettingIndex, csmInt32 vertexIndex) const +{ + return CubismVector2(_json->GetRoot()[PhysicsSettings][physicsSettingIndex][Vertices][vertexIndex][Position][X].ToFloat(), + _json->GetRoot()[PhysicsSettings][physicsSettingIndex][Vertices][vertexIndex][Position][Y].ToFloat() ); +} +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Physics/CubismPhysicsJson.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismJsonHolder.hpp" +#include "Utils/CubismJson.hpp" +#include "Math/CubismVector2.hpp" +#include "Id/CubismId.hpp" + +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief physics3.jsonのコンテナ + * + * physics3.jsonのコンテナ。 + */ +class CubismPhysicsJson : public CubismJsonHolder +{ +public: + /** + * @brief コンストラクタ + * + * コンストラクタ。 + * + * @param[in] buffer physics3.jsonが読み込まれているバッファ + * @param[in] size バッファのサイズ + */ + CubismPhysicsJson(const csmByte* buffer, csmSizeInt size); + + /** + * @brief デストラクタ + * + * デストラクタ。 + */ + virtual ~CubismPhysicsJson(); + + /** + * @brief 重力の取得 + * + * 重力を取得する。 + * + * @return 重力 + */ + CubismVector2 GetGravity() const; + + /** + * @brief 風の取得 + * + * 風を取得する。 + * + * @return 風 + */ + CubismVector2 GetWind() const; + + /** + * @brief 物理演算設定FPSの取得 + * + * 物理演算の想定FPSを取得する。 + * physics3.jsonにFPS情報が存在しない場合、0.0fを返す。 + * + * @return 物理演算設定FPS + */ + csmFloat32 GetFps() const; + + /** + * @brief 物理点の管理の個数の取得 + * + * 物理点の管理の個数を取得する。 + * + * @return 物理点の管理の個数 + */ + csmInt32 GetSubRigCount() const; + + /** + * @brief 入力の総合計の取得 + * + * 入力の総合計を取得する。 + * + * @return 入力の総合計 + */ + csmInt32 GetTotalInputCount() const; + + /** + * @brief 出力の総合計の取得 + * + * 出力の総合計を取得する。 + * + * @return 出力の総合計 + */ + csmInt32 GetTotalOutputCount() const; + + /** + * @brief 物理点の個数の取得 + * + * 物理点の個数を取得する。 + * + * @return 物理点の個数 + */ + csmInt32 GetVertexCount() const; + + /** + * @brief 正規化された位置の最小値の取得 + * + * 正規化された位置の最小値を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @return 正規化された位置の最小値 + */ + csmFloat32 GetNormalizationPositionMinimumValue(csmInt32 physicsSettingIndex) const; + + /** + * @brief 正規化された位置の最大値の取得 + * + * 正規化された位置の最大値を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @return 正規化された位置の最大値 + */ + csmFloat32 GetNormalizationPositionMaximumValue(csmInt32 physicsSettingIndex) const; + + /** + * @brief 正規化された位置のデフォルト値の取得 + * + * 正規化された位置のデフォルト値を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @return 正規化された位置のデフォルト値 + */ + csmFloat32 GetNormalizationPositionDefaultValue(csmInt32 physicsSettingIndex) const; + + /** + * @brief 正規化された角度の最小値の取得 + * + * 正規化された角度の最小値を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @return 正規化された角度の最小値 + */ + csmFloat32 GetNormalizationAngleMinimumValue(csmInt32 physicsSettingIndex) const; + + /** + * @brief 正規化された角度の最大値の取得 + * + * 正規化された角度の最大値を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @return 正規化された角度の最大値 + */ + csmFloat32 GetNormalizationAngleMaximumValue(csmInt32 physicsSettingIndex) const; + + /** + * @brief 正規化された角度のデフォルト値の取得 + * + * 正規化された角度のデフォルト値を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @return 正規化された角度のデフォルト値 + */ + csmFloat32 GetNormalizationAngleDefaultValue(csmInt32 physicsSettingIndex) const; + + /** + * @brief 入力の個数の取得 + * + * 入力の個数を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @return 入力の個数 + */ + csmInt32 GetInputCount(csmInt32 physicsSettingIndex) const; + + /** + * @brief 入力の重みの取得 + * + * 入力の重みを取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] inputIndex 入力のインデックス + * @return 入力の重み + */ + csmFloat32 GetInputWeight(csmInt32 physicsSettingIndex, csmInt32 inputIndex) const; + + /** + * @brief 入力の反転の取得 + * + * 入力の反転を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] inputIndex 入力のインデックス + * @return 入力の反転 + */ + csmBool GetInputReflect(csmInt32 physicsSettingIndex, csmInt32 inputIndex) const; + + /** + * @brief 入力の種類の取得 + * + * 入力の種類を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] inputIndex 入力のインデックス + * @return 入力の種類 + */ + const csmChar* GetInputType(csmInt32 physicsSettingIndex, csmInt32 inputIndex) const; + + /** + * @brief 入力元のIDの取得 + * + * 入力元のIDを取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] inputIndex 入力のインデックス + * @return 入力元のID + */ + CubismIdHandle GetInputSourceId(csmInt32 physicsSettingIndex, csmInt32 inputIndex) const; + + /** + * @brief 出力の個数の取得 + * + * 出力の個数を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @return 出力の個数 + */ + csmInt32 GetOutputCount(csmInt32 physicsSettingIndex) const; + + /** + * @brief 出力の物理点のインデックスの取得 + * + * 出力の物理点のインデックスを取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] outputIndex 出力のインデックス + * @return 出力の物理点のインデックス + */ + csmInt32 GetOutputVertexIndex(csmInt32 physicsSettingIndex, csmInt32 outputIndex) const; + + /** + * @brief 出力の角度のスケールの取得 + * + * 出力の角度のスケールを取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] outputIndex 出力のインデックス + * @return 出力の角度のスケール + */ + csmFloat32 GetOutputAngleScale(csmInt32 physicsSettingIndex, csmInt32 outputIndex) const; + + /** + * @brief 出力の重みの取得 + * + * 出力の重みを取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] outputIndex 出力のインデックス + * @return 出力の重み + */ + csmFloat32 GetOutputWeight(csmInt32 physicsSettingIndex, csmInt32 outputIndex) const; + + /** + * @brief 出力先のIDの取得 + * + * 出力先のIDを取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] outputIndex 出力のインデックス + * @return 出力先のID + */ + CubismIdHandle GetOutputsDestinationId(csmInt32 physicsSettingIndex, csmInt32 outputIndex) const; + + /** + * @brief 出力の種類の取得 + * + * 出力の種類を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] outputIndex 出力のインデックス + * @return 出力の種類 + */ + const csmChar* GetOutputType(csmInt32 physicsSettingIndex, csmInt32 outputIndex) const; + + /** + * @brief 出力の反転の取得 + * + * 出力の反転を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] outputIndex 出力のインデックス + * @return 出力の反転 + */ + csmBool GetOutputReflect(csmInt32 physicsSettingIndex, csmInt32 outputIndex) const; + + /** + * @brief 物理点の個数の取得 + * + * 物理点の個数を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @return 物理点の個数 + */ + csmInt32 GetParticleCount(csmInt32 physicsSettingIndex) const; + + /** + * @brief 物理点の動きやすさの取得 + * + * 物理点の動きやすさを取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] vertexIndex 物理点のインデックス + * @return 物理点の動きやすさ + */ + csmFloat32 GetParticleMobility(csmInt32 physicsSettingIndex, csmInt32 vertexIndex) const; + + /** + * @brief 物理点の遅れの取得 + * + * 物理点の遅れを取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] vertexIndex 物理点のインデックス + * @return 物理点の遅れ + */ + csmFloat32 GetParticleDelay(csmInt32 physicsSettingIndex, csmInt32 vertexIndex) const; + + /** + * @brief 物理点の加速度の取得 + * + * 物理点の加速度を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] vertexIndex 物理点のインデックス + * @return 物理点の加速度 + */ + csmFloat32 GetParticleAcceleration(csmInt32 physicsSettingIndex, csmInt32 vertexIndex) const; + + /** + * @brief 物理点の距離の取得 + * + * 物理点の距離を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] vertexIndex 物理点のインデックス + * @return 物理点の距離 + */ + csmFloat32 GetParticleRadius(csmInt32 physicsSettingIndex, csmInt32 vertexIndex) const; + + /** + * @brief 物理点の位置の取得 + * + * 物理点の位置を取得する。 + * + * @param[in] physicsSettingIndex 物理演算の設定のインデックス + * @param[in] vertexIndex 物理点のインデックス + * @return 物理点の位置 + */ + CubismVector2 GetParticlePosition(csmInt32 physicsSettingIndex, csmInt32 vertexIndex) const; +}; + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Rendering/CubismRenderer.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismRenderer.hpp" +#include "CubismFramework.hpp" +#include "Model/CubismModel.hpp" + +//------------ LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { namespace Rendering { + + + CubismRenderer* CubismRenderer::Create() + { + return nullptr; + } + + void CubismRenderer::StaticRelease() + { + + } + +void CubismRenderer::Delete(CubismRenderer* renderer) +{ + CSM_DELETE_SELF(CubismRenderer, renderer); +} + +CubismRenderer::CubismRenderer() + : _isCulling(false) + , _isPremultipliedAlpha(false) + , _anisotropy(0.0f) + , _model(NULL) + , _useHighPrecisionMask(false) +{ + //単位行列に初期化 + _mvpMatrix4x4.LoadIdentity(); +} + +CubismRenderer::~CubismRenderer() +{} + +void CubismRenderer::Initialize(Framework::CubismModel* model) +{ + Initialize(model, 1); +} + +void CubismRenderer::Initialize(Framework::CubismModel* model, csmInt32 maskBufferCount) +{ + _model = model; +} + +void CubismRenderer::DrawModel() +{ + if (GetModel() == NULL) return; + + /** + * DoDrawModelの描画前と描画後に以下の関数を呼んでください。 + * ・SaveProfile(); + * ・RestoreProfile(); + * これはレンダラの描画設定を保存・復帰させることで、 + * モデル描画直前の状態に戻すための処理です。 + */ + + SaveProfile(); + + DoDrawModel(); + + RestoreProfile(); +} + +void CubismRenderer::SetMvpMatrix(CubismMatrix44* matrix4x4) +{ + _mvpMatrix4x4.SetMatrix(matrix4x4->GetArray()); +} + +CubismMatrix44 CubismRenderer::GetMvpMatrix() const +{ + return _mvpMatrix4x4; +} + +void CubismRenderer::SetModelColor(csmFloat32 red, csmFloat32 green, csmFloat32 blue, csmFloat32 alpha) +{ + if (red < 0.0f) red = 0.0f; + else if (red > 1.0f) red = 1.0f; + + if (green < 0.0f) green = 0.0f; + else if (green > 1.0f) green = 1.0f; + + if (blue < 0.0f) blue = 0.0f; + else if (blue > 1.0f) blue = 1.0f; + + if (alpha < 0.0f) alpha = 0.0f; + else if (alpha > 1.0f) alpha = 1.0f; + + _modelColor.R = red; + _modelColor.G = green; + _modelColor.B = blue; + _modelColor.A = alpha; +} + +CubismRenderer::CubismTextureColor CubismRenderer::GetModelColor() const +{ + return _modelColor; +} + +CubismRenderer::CubismTextureColor CubismRenderer::GetModelColorWithOpacity(const csmFloat32 opacity) const +{ + CubismTextureColor modelColorRGBA = GetModelColor(); + modelColorRGBA.A *= opacity; + if (IsPremultipliedAlpha()) + { + modelColorRGBA.R *= modelColorRGBA.A; + modelColorRGBA.G *= modelColorRGBA.A; + modelColorRGBA.B *= modelColorRGBA.A; + } + return modelColorRGBA; +} + +void CubismRenderer::IsPremultipliedAlpha(csmBool enable) +{ + _isPremultipliedAlpha = enable; +} + +csmBool CubismRenderer::IsPremultipliedAlpha() const +{ + return _isPremultipliedAlpha; +} + +void CubismRenderer::IsCulling(csmBool culling) +{ + _isCulling = culling; +} + +csmBool CubismRenderer::IsCulling() const +{ + return _isCulling; +} + +void CubismRenderer::SetAnisotropy(csmFloat32 n) +{ + _anisotropy = n; +} + +csmFloat32 CubismRenderer::GetAnisotropy() const +{ + return _anisotropy; +} + +CubismModel* CubismRenderer::GetModel() const +{ + return _model; +} + +void CubismRenderer::UseHighPrecisionMask(csmBool high) +{ + _useHighPrecisionMask = high; +} + +csmBool CubismRenderer::IsUsingHighPrecisionMask() +{ + return _useHighPrecisionMask; +} + +/********************************************************************************************************************* +* CubismClippingContext +********************************************************************************************************************/ +CubismClippingContext::CubismClippingContext(const csmInt32* clippingDrawableIndices, csmInt32 clipCount) +{ + // クリップしている(=マスク用の)Drawableのインデックスリスト + _clippingIdList = clippingDrawableIndices; + + // マスクの数 + _clippingIdCount = clipCount; + + _layoutChannelIndex = 0; + + _allClippedDrawRect = CSM_NEW csmRectF(); + _layoutBounds = CSM_NEW csmRectF(); + + _clippedDrawableIndexList = CSM_NEW csmVector(); +} + +CubismClippingContext::~CubismClippingContext() +{ + if (_layoutBounds != NULL) + { + CSM_DELETE(_layoutBounds); + _layoutBounds = NULL; + } + + if (_allClippedDrawRect != NULL) + { + CSM_DELETE(_allClippedDrawRect); + _allClippedDrawRect = NULL; + } + + if (_clippedDrawableIndexList != NULL) + { + CSM_DELETE(_clippedDrawableIndexList); + _clippedDrawableIndexList = NULL; + } +} + +void CubismClippingContext::AddClippedDrawable(csmInt32 drawableIndex) +{ + _clippedDrawableIndexList->PushBack(drawableIndex); +} + +}}}} + +//------------ LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Rendering/CubismRenderer.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismFramework.hpp" +#include "Math/CubismMatrix44.hpp" +#include "Type/csmVector.hpp" +#include "Type/csmRectF.hpp" + +namespace Live2D {namespace Cubism {namespace Framework { +class CubismModel; +}}} + +//------------ LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { namespace Rendering { + +/** + * @brief モデル描画を処理するレンダラ
+ * サブクラスに環境依存の描画命令を記述する + */ +class CubismRenderer +{ +public: + + /** + * @brief カラーブレンディングのモード + */ + enum CubismBlendMode + { + CubismBlendMode_Normal = 0, ///< 通常 + + CubismBlendMode_Additive = 1, ///< 加算 + + CubismBlendMode_Multiplicative = 2, ///< 乗算 + + CubismBlendMode_Mask = 3, ///< マスク + }; // CubismBlendMode + + /** + * @brief テクスチャの色をRGBAで扱うための構造体 + */ + struct CubismTextureColor + { + /** + * @brief コンストラクタ + */ + CubismTextureColor() + : R(1.0f) + , G(1.0f) + , B(1.0f) + , A(1.0f) {}; + + /** + * @brief コンストラクタ + */ + CubismTextureColor(csmFloat32 r, csmFloat32 g, csmFloat32 b, csmFloat32 a) + : R(r) + , G(g) + , B(b) + , A(a) {}; + + /** + * @brief デストラクタ + */ + virtual ~CubismTextureColor() {}; + + csmFloat32 R; ///< 赤チャンネル + csmFloat32 G; ///< 緑チャンネル + csmFloat32 B; ///< 青チャンネル + csmFloat32 A; ///< αチャンネル + + }; // CubismTextureColor + + /** + * @brief レンダラのインスタンスを生成して取得する + * + * @retrun レンダラのインスタンス + */ + static CubismRenderer* Create(); + + /** + * @brief レンダラのインスタンスを解放する + */ + static void Delete(CubismRenderer* renderer); + + /** + * @brief レンダラが保持する静的なリソースを解放する + */ + static void StaticRelease(); + + /** + * @brief レンダラの初期化処理を実行する
+ * 引数に渡したモデルからレンダラの初期化処理に必要な情報を取り出すことができる + * + * @param[in] model -> モデルのインスタンス + */ + virtual void Initialize(Framework::CubismModel* model); + + /** + * @brief レンダラの初期化処理を実行する
+ * 引数に渡したモデルからレンダラの初期化処理に必要な情報を取り出すことができる + * + * @param[in] model -> モデルのインスタンス + * @param[in] maskBufferCount -> バッファの生成数 + */ + virtual void Initialize(Framework::CubismModel* model, csmInt32 maskBufferCount); + + /** + * @brief モデルを描画する + * + */ + void DrawModel(); + + /** + * @brief Model-View-Projection 行列をセットする
+ * 配列は複製されるので元の配列は外で破棄して良い + * + * @param[in] matrix4x4 -> Model-View-Projection 行列 + */ + void SetMvpMatrix(CubismMatrix44* matrix4x4); + + /** + * @brief Model-View-Projection 行列を取得する + * + * @return Model-View-Projection 行列 + */ + CubismMatrix44 GetMvpMatrix() const; + + /** + * @brief モデルの色をセットする。
+ * 各色0.0f~1.0fの間で指定する(1.0fが標準の状態)。 + * + * @param[in] red -> 赤チャンネルの値 + * @param[in] green -> 緑チャンネルの値 + * @param[in] blue -> 青チャンネルの値 + * @param[in] alpha -> αチャンネルの値 + * + */ + void SetModelColor(csmFloat32 red, csmFloat32 green, csmFloat32 blue, csmFloat32 alpha); + + /** + * @brief モデルの色を取得する。
+ * 各色0.0f~1.0fの間で指定する(1.0fが標準の状態)。 + * + * @return RGBAのカラー情報 + */ + CubismTextureColor GetModelColor() const; + + /** + * @brief 透明度を考慮したモデルの色を計算する。 + * + * @param[in] opacity -> 透明度 + * + * @return RGBAのカラー情報 + */ + CubismTextureColor GetModelColorWithOpacity(const csmFloat32 opacity) const; + + /** + * @brief 乗算済みαの有効・無効をセットする。
+ * 有効にするならtrue, 無効にするならfalseをセットする。 + */ + void IsPremultipliedAlpha(csmBool enable); + + /** + * @brief 乗算済みαの有効・無効を取得する。 + * + * @retval true -> 乗算済みα有効 + * @retval false -> 乗算済みα無効 + */ + csmBool IsPremultipliedAlpha() const; + + /** + * @brief カリング(片面描画)の有効・無効をセットする。
+ * 有効にするならtrue, 無効にするならfalseをセットする。 + */ + void IsCulling(csmBool culling); + + /** + * @brief カリング(片面描画)の有効・無効を取得する。 + * + * @retval true -> カリング有効 + * @retval false -> カリング無効 + */ + csmBool IsCulling() const; + + /** + * @brief テクスチャの異方性フィルタリングのパラメータをセットする
+ * パラメータ値の影響度はレンダラの実装に依存する + * + * @param[in] anisotropy -> パラメータの値 + */ + void SetAnisotropy(csmFloat32 anisotropy); + + /** + * @brief テクスチャの異方性フィルタリングのパラメータをセットする + * + * @return 異方性フィルタリングのパラメータ + */ + csmFloat32 GetAnisotropy() const; + + /** + * @brief レンダリングするモデルを取得する。 + * + * @return レンダリングするモデルのポインタ + */ + CubismModel* GetModel() const; + + /** + * @brief マスク描画の方式を変更する。 + * falseの場合、マスクを1枚のテクスチャに分割してレンダリングする(デフォルトはこちら)。 + * 高速だが、マスク個数の上限が36に限定され、質も荒くなる。 + * trueの場合、パーツ描画の前にその都度必要なマスクを描き直す。 + * レンダリング品質は高いが描画処理負荷は増す。 + */ + void UseHighPrecisionMask(csmBool high); + + /** + * @brief マスク描画の方式を取得する。 + */ + csmBool IsUsingHighPrecisionMask(); + +protected: + /** + * @brief コンストラクタ + */ + CubismRenderer(); + + /** + * @brief デストラクタ + */ + virtual ~CubismRenderer(); + + /** + * @brief モデル描画の実装 + * + */ + virtual void DoDrawModel() = 0; + + /** + * @brief モデル描画直前のレンダラのステートを保持する + */ + virtual void SaveProfile() = 0; + + + /** + * @brief モデル描画直前のレンダラのステートを復帰させる + */ + virtual void RestoreProfile() = 0; + +private: + // コピーコンストラクタを隠す + CubismRenderer(const CubismRenderer&); + CubismRenderer& operator=(const CubismRenderer&); + + CubismMatrix44 _mvpMatrix4x4; ///< Model-View-Projection 行列 + CubismTextureColor _modelColor; ///< モデル自体のカラー(RGBA) + csmBool _isCulling; ///< カリングが有効ならtrue + csmBool _isPremultipliedAlpha; ///< 乗算済みαならtrue + csmFloat32 _anisotropy; ///< テクスチャの異方性フィルタリングのパラメータ + CubismModel* _model; ///< レンダリング対象のモデル + + csmBool _useHighPrecisionMask; ///< falseの場合、マスクを纏めて描画する trueの場合、マスクはパーツ描画ごとに書き直す +}; + + +/** + * @brief クリッピングについての設定を保持するクラス
+ * サブクラスに環境依存のフィールドを保持する + */ +class CubismClippingContext +{ +public: + /** + * @brief 引数付きコンストラクタ + * + */ + CubismClippingContext(const csmInt32* clippingDrawableIndices, csmInt32 clipCount); + + /** + * @brief デストラクタ + */ + ~CubismClippingContext(); + + /** + * @brief このマスクにクリップされる描画オブジェクトを追加する + * + * @param[in] drawableIndex -> クリッピング対象に追加する描画オブジェクトのインデックス + */ + void AddClippedDrawable(csmInt32 drawableIndex); + + csmBool _isUsing; ///< 現在の描画状態でマスクの準備が必要ならtrue + const csmInt32* _clippingIdList; ///< クリッピングマスクのIDリスト + csmInt32 _clippingIdCount; ///< クリッピングマスクの数 + csmInt32 _layoutChannelIndex; ///< RGBAのいずれのチャンネルにこのクリップを配置するか(0:R , 1:G , 2:B , 3:A) + csmRectF* _layoutBounds; ///< マスク用チャンネルのどの領域にマスクを入れるか(View座標-1..1, UVは0..1に直す) + csmRectF* _allClippedDrawRect; ///< このクリッピングで、クリッピングされる全ての描画オブジェクトの囲み矩形(毎回更新) + CubismMatrix44 _matrixForMask; ///< マスクの位置計算結果を保持する行列 + CubismMatrix44 _matrixForDraw; ///< 描画オブジェクトの位置計算結果を保持する行列 + csmVector* _clippedDrawableIndexList; ///< このマスクにクリップされる描画オブジェクトのリスト + csmInt32 _bufferIndex; ///< このマスクが割り当てられるレンダーテクスチャ(フレームバッファ)やカラーバッファのインデックス +}; + +}}}} + +//------------ LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Type/CubismBasicType.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +/** + * @brief CubismFrameworkで用いる基本型の定義 + * + */ + +#include + +namespace Live2D { namespace Cubism { namespace Framework { +typedef bool csmBool; ///< 真偽値型 +typedef char csmChar; ///< 文字型 +typedef unsigned char csmUchar; ///< 符号なし文字型 + +typedef unsigned char csmByte; ///< バイト型 +typedef signed char csmInt8; ///< 符号付き文字型 +typedef unsigned char csmUint8; ///< 8ビット符号なし整数型 + +typedef signed short csmInt16; ///< 16ビット符号付き整数型 +typedef unsigned short csmUint16; ///< 16ビット符号なし整数型 + +typedef signed int csmInt32; ///< 32ビット符号付き整数型 +typedef unsigned int csmUint32; ///< 32ビット符号なし整数型 + +typedef signed long long csmInt64; ///< 64ビット符号付き整数型 +typedef unsigned long long csmUint64; ///< 64ビット符号なし整数型 + +typedef float csmFloat32; ///< 32ビット浮動小数点数型 + +typedef unsigned int csmSizeInt; ///< size_t型 + +#ifdef _MSC_VER +typedef ::size_t csmSizeType; +#else +typedef std::size_t csmSizeType; +#endif + +}}} + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Type/csmMap.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismFramework.hpp" +#include "csmString.hpp" +#include "Utils/CubismDebug.hpp" + +#ifndef NULL +# define NULL 0 +#endif + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + +//========================テンプレートの宣言============================== + +/** + * @brief Key-Valueのペアを定義するクラス。
+ * csmMapクラスの内部データで使用する。 + */ +template +class csmPair +{ +public: + + /** + * @brief コンストラクタ + */ + csmPair() : First() + , Second() {} + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] key -> Keyとしてセットする値 + */ + csmPair(const _KeyT& key) : First(key) + , Second() {} + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] key -> Keyとしてセットする値 + * @param[in] value -> Valueとしてセットする値 + */ + csmPair(const _KeyT& key, const _ValT& value): First(key) + , Second(value) {} + + /** + * @brief デストラクタ + */ + virtual ~csmPair() {} + + _KeyT First; ///< Keyとして用いる変数 + _ValT Second; ///< Valueとして用いる変数 +}; + +/** + *@brief マップ型
+ * コンシューマゲーム機等でSTLの組み込みを避けるための実装。std::map の簡易版 + */ +template +class csmMap +{ +public: + + /** + * @brief コンストラクタ + */ + csmMap(); + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] size -> 初期化時点で確保するサイズ + */ + csmMap(csmInt32 size); + + /** + * @brief デストラクタ + * + */ + virtual ~csmMap(); + + /** + * @brief キーを追加する + * + * @param[in] key -> 新たに追加するキー + */ + void AppendKey(_KeyT& key) + { + // 新しくKey/Valueのペアを作る + PrepareCapacity(_size + 1, false); //1つ以上入る隙間を作る + // 新しいkey/valueのインデックスは _size + + void* addr = &_keyValues[_size]; + CSM_PLACEMENT_NEW(addr) csmPair<_KeyT, _ValT>(key); //placement new + + _size += 1; + } + + /** + * @brief 添字演算子[key]のオーバーロード + * + * @return 添字から特定されるValue値 + */ + _ValT& operator[](_KeyT key) + { + csmInt32 found = -1; + for (csmInt32 i = 0; i < _size; i++) + { + if (_keyValues[i].First == key) + { + found = i; + break; + } + } + if (found >= 0) + { + return _keyValues[found].Second; + } + else + { + AppendKey(key); // 新規キーを追加 + return _keyValues[_size - 1].Second; + } + } + + /** + * @brief 添字演算子[key]のオーバーロード(const) + * + * @return 添字から特定されるValue値 + */ + const _ValT& operator[](_KeyT key) const + { + csmInt32 found = -1; + for (csmInt32 i = 0; i < _size; i++) + { + if (_keyValues[i].First == key) + { + found = i; + break; + } + } + if (found >= 0) + { + return _keyValues[found].Second; + } + else + { + if (!_dummyValuePtr) _dummyValuePtr = CSM_NEW _ValT(); + return *_dummyValuePtr; + } + } + + /** + * @brief 引数で渡したKeyを持つ要素が存在するか + * + * @retval true -> 引数で渡したKeyを持つ要素が存在する + * @retval false -> 引数で渡したKeyを持つ要素が存在しない + */ + csmBool IsExist(_KeyT key) + { + for (csmInt32 i = 0; i < _size; i++) + { + if (_keyValues[i].First == key) + { + return true; + } + } + return false; + } + + /** + * @brief Key-Valueのポインタを全て解放する + */ + void Clear(); + + /** + * @brief コンテナのサイズを取得する + * + * @return コンテナのサイズ + */ + csmInt32 GetSize() const { return _size; } + + /** + * @brief コンテナのキャパシティを確保する + * + * @param[in] newSize -> 新たなキャパシティ。引数の値が現在のサイズ未満の場合は何もしない。 + * @param[in] fitToSize -> trueなら指定したサイズに合わせる。falseならサイズを2倍確保しておく。 + */ + void PrepareCapacity(csmInt32 newSize, csmBool fitToSize); + + /** + * @brief csmMapのイテレータ + */ + class iterator + { + // csmMapをフレンドクラスとする + friend class csmMap; + + public: + /** + * @brief コンストラクタ + * + */ + iterator() : _index(0) + , _map(NULL) {} + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] v -> csmMapのオブジェクト + * + */ + iterator(csmMap<_KeyT, _ValT>* v) : _index(0) + , _map(v) {} + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] v -> csmMapのオブジェクト + * @param[in] idx -> コンテナから参照するインデックス値 + */ + iterator(csmMap<_KeyT, _ValT>* v, int idx) : _index(idx) + , _map(v) {} + + /** + * @brief =演算子のオーバーロード + * + */ + iterator& operator=(const iterator& ite) + { + this->_index = ite._index; + this->_map = ite._map; + return *this; + } + + /** + * @brief 前置++演算子のオーバーロード + * + */ + iterator& operator++() + { + // 前置 + this->_index++; + return *this; + } + + /** + * @brief 前置--演算子のオーバーロード + * + */ + iterator& operator--() + { + // 前置 + this->_index--; + return *this; + } + + /** + * @brief 後置++演算子のオーバーロード(intは後置用のダミー引数) + * + */ + iterator operator++(csmInt32) + { + // intは後置のためのダミー引数 + iterator iteold(this->_map, this->_index++); // 古い値を保存 + return iteold; // 古い値を返す + } + + /** + * @brief 後置--演算子のオーバーロード(intは後置用のダミー引数) + * + */ + iterator operator--(csmInt32) + { + // intは後置のためのダミー引数 + iterator iteold(this->_map, this->_index--); // 古い値を保存 + return iteold; + } + + /** + * @brief *演算子のオーバーロード + * + */ + csmPair<_KeyT, _ValT>& operator*() const + { + return this->_map->_keyValues[this->_index]; + } + + /** + * @brief !=演算子のオーバーロード + * + */ + csmBool operator!=(const iterator& ite) const + { + return (this->_index != ite._index) || (this->_map != ite._map); + } + + private: + csmInt32 _index; ///< コンテナのインデックス値 + csmMap<_KeyT, _ValT>* _map; ///< コンテナのポインタ + }; + + /** + * @brief csmMapのイテレータ(const) + */ + class const_iterator + { + // csmMapをフレンドクラスとする + friend class csmMap; + + public: + /** + * @brief コンストラクタ + * + */ + const_iterator() : _index(0) + , _map(NULL) {} + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] v -> csmVectorのオブジェクト + * + */ + const_iterator(const csmMap<_KeyT, _ValT>* v) : _index(0) + , _map(v) {} + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] v -> csmVectorのオブジェクト + * @param[in] idx -> コンテナから参照するインデックス値 + */ + const_iterator(const csmMap<_KeyT, _ValT>* v, csmInt32 idx) : _index(idx) + , _map(v) {} + + /** + * @brief =演算子のオーバーロード + * + */ + const_iterator& operator=(const const_iterator& ite) + { + this->_index = ite._index; + this->_map = ite._map; + return *this; + } + + /** + * @brief 前置++演算子のオーバーロード + * + */ + const_iterator& operator++() + { + // 前置 + ++this->_index; + return *this; + } + + /** + * @brief 前置--演算子のオーバーロード + * + */ + const_iterator& operator--() + { + // 前置 + --this->_index; + return *this; + } + + /** + * @brief 後置++演算子のオーバーロード(intは後置用のダミー引数) + * + */ + const_iterator operator++(csmInt32) + { + // intは後置のためのダミー引数 + const_iterator iteold(this->_map, this->_index++); // 古い値を保存 + return iteold; // 古い値を返す + } + + /** + * @brief 後置--演算子のオーバーロード(intは後置用のダミー引数) + * + */ + const_iterator operator--(csmInt32) + { + // intは後置のためのダミー引数 + const_iterator iteold(this->_map, this->_index--); // 古い値を保存 + return iteold; + } + + /** + * @brief ->演算子のオーバーロード + * + */ + csmPair<_KeyT, _ValT>* operator->() const + { + return &this->_map->_keyValues[this->_index]; + } + + /** + * @brief *演算子のオーバーロード + * + */ + csmPair<_KeyT, _ValT>& operator*() const + { + return this->_map->_keyValues[this->_index]; + } + + /** + * @brief !=演算子のオーバーロード + * + */ + csmBool operator!=(const const_iterator& ite) const + { + return (this->_index != ite._index) || (this->_map != ite._map); + } + + private: + csmInt32 _index; ///< コンテナのインデックス値 + const csmMap<_KeyT, _ValT>* _map; ///< コンテナのポインタ(const) + }; + + /** + * @brief コンテナの先頭要素を返す + * + */ + const const_iterator Begin() const + { + const_iterator ite(this, 0); + return ite; + } + + /** + * @bief コンテナの終端要素を返す + * + */ + const const_iterator End() const + { + const_iterator ite(this, _size); // 終了 + return ite; + } + + /** + * @brief コンテナから要素を削除する + * + * @param[in] ite -> 削除する要素 + * + */ + const iterator Erase(const iterator& ite) + { + int index = ite._index; + if (index < 0 || _size <= index) return ite; // 削除範囲外 + + // 削除(メモリをシフトする)、最後の一つを削除する場合はmove不要 + if (index < _size - 1) + memmove(&(_keyValues[index]), &(_keyValues[index + 1]), sizeof(csmPair<_KeyT, _ValT>) * (_size - index - 1)); + --_size; + + iterator ite2(this, index); // 終了 + return ite2; + } + + /** + * @brief コンテナから要素を削除する + * + * @param[in] ite -> 削除する要素 + * + */ + const const_iterator Erase(const const_iterator& ite) + { + csmInt32 index = ite._index; + if (index < 0 || _size <= index) return ite; // 削除範囲外 + + // 削除(メモリをシフトする)、最後の一つを削除する場合はmove不要 + if (index < _size - 1) + memmove(&(_keyValues[index]), &(_keyValues[index + 1]), sizeof(csmPair<_KeyT, _ValT>) * (_size - index - 1)); + --_size; + + const_iterator ite2(this, index); // 終了 + return ite2; + } + + /** + * @brief コンテナの値を32ビット符号付き整数型でダンプする + * + */ + void DumpAsInt() + { + for (csmInt32 i = 0; i < _size; i++) CubismLogDebug("%d ,", _keyValues[i]); + CubismLogDebug("\n"); + } + +private: + static const csmInt32 DefaultSize = 10; ///< コンテナ初期化のデフォルトサイズ + + csmPair<_KeyT, _ValT>* _keyValues; ///< Key-Valueペアの配列 + _ValT* _dummyValuePtr; ///< 空の値を返すためのダミー(staticのtemplteを回避するためメンバとする) + csmInt32 _size; ///< コンテナの要素数(サイズ) + csmInt32 _capacity; ///< コンテナのキャパシティ +}; + + +//========================テンプレートの定義============================== + +template +csmMap<_KeyT, _ValT>::csmMap() + : _keyValues(NULL) + , _dummyValuePtr(NULL) + , _size(0) + , _capacity(0) +{ } + +template +csmMap<_KeyT, _ValT>::csmMap(csmInt32 size) + : _dummyValuePtr(NULL) +{ + if (size < 1) + { + _keyValues = NULL; + _capacity = 0; + _size = 0; + } + else + { + _keyValues = static_cast *>(CSM_MALLOC(size * sizeof(csmPair<_KeyT, _ValT>))); + + CSM_ASSERT(_keyValues != NULL); + + // ここだけ calloc により、確保したバイトを0で埋める + memset(_keyValues, 0, size * sizeof(csmPair<_KeyT, _ValT>)); + + _capacity = size; + _size = size; + } +} + +template +csmMap<_KeyT, _ValT>::~csmMap() +{ + Clear(); +} + +template +void csmMap<_KeyT, _ValT>::PrepareCapacity(csmInt32 newSize, csmBool fitToSize) +{ + if (newSize > _capacity) + { + if (_capacity == 0) + { + if (!fitToSize && newSize < DefaultSize) newSize = DefaultSize; + + _keyValues = static_cast *>(CSM_MALLOC(sizeof(csmPair<_KeyT, _ValT>) * newSize)); + + CSM_ASSERT(_keyValues != NULL); + + _capacity = newSize; + } + else + { + if (!fitToSize && newSize < _capacity * 2) newSize = _capacity * 2; // 指定サイズに合わせる必要がない場合は、2倍に広げる + + csmInt32 tmp_capacity = newSize; + csmPair<_KeyT, _ValT>* tmp = static_cast *>(CSM_MALLOC(sizeof(csmPair<_KeyT, _ValT>) * tmp_capacity)); + + CSM_ASSERT(tmp != NULL); + + // 通常のMALLOCになったためコピーする + memcpy(static_cast(tmp), static_cast(_keyValues), sizeof(csmPair<_KeyT, _ValT>) * _capacity); + CSM_FREE(_keyValues); + + _keyValues = tmp; // そのまま + _capacity = newSize; + } + } + // _size = newsize ; +} + +template +void csmMap<_KeyT, _ValT>::Clear() +{ + if (_dummyValuePtr) CSM_DELETE(_dummyValuePtr); + for (csmInt32 i = 0; i < _size; i++) + { + _keyValues[i].~csmPair<_KeyT, _ValT>(); + } + + CSM_FREE(_keyValues); + + _keyValues = NULL; + + _size = 0; + _capacity = 0; +} +}}} + +//------------------------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Type/csmRectF.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "csmRectF.hpp" + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + +csmRectF::csmRectF() +{ } + +csmRectF::csmRectF(csmFloat32 x, csmFloat32 y, csmFloat32 w, csmFloat32 h) + : X(x) + , Y(y) + , Width(w) + , Height(h) +{ } + +csmRectF::~csmRectF() +{ } + +void csmRectF::SetRect(csmRectF* r) +{ + X = r->X; + Y = r->Y; + Width = r->Width; + Height = r->Height; +} + +void csmRectF::Expand(csmFloat32 w, csmFloat32 h) +{ + X -= w; + Y -= h; + Width += w * 2.0f; + Height += h * 2.0f; +} + +}}} +//--------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Type/csmRectF.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismFramework.hpp" + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief 矩形形状(座標・長さはfloat値)を定義するクラス + * + */ +class csmRectF +{ +public: + + /** + * @brief コンストラクタ + * + */ + csmRectF(); + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] x -> 左端X座標 + * @param[in] y -> 上端Y座標 + * @param[in] w -> 幅 + * @param[in] h -> 高さ + */ + csmRectF(csmFloat32 x, csmFloat32 y, csmFloat32 w, csmFloat32 h); + + /** + * @brief デストラクタ + * + */ + virtual ~csmRectF(); + + /** + * @brief 矩形中央のX座標を取得する + */ + csmFloat32 GetCenterX() const { return X + 0.5f * Width; } + + /** + * @brief 矩形中央のY座標を取得する + */ + csmFloat32 GetCenterY() const { return Y + 0.5f * Height; } + + /** + * @brief 右端のX座標を取得する + */ + csmFloat32 GetRight() const { return X + Width; } + + /** + * @brief 下端のY座標を取得する + */ + csmFloat32 GetBottom() const { return Y + Height; } + + /** + * @brief 矩形に値をセットする + * + * @param[in] r -> 矩形のインスタンス + */ + void SetRect(csmRectF* r); + + /** + * @brief 矩形中央を軸にして縦横を拡縮する + * + * @param[in] w -> 幅方向に拡縮する量 + * @param[in] h -> 高さ方向に拡縮する量 + */ + void Expand(csmFloat32 w, csmFloat32 h); + + csmFloat32 X; ///< 左端X座標 + csmFloat32 Y; ///< 上端Y座標 + csmFloat32 Width; ///< 幅 + csmFloat32 Height; ///< 高さ +}; +}}} + +//------------------------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Type/csmString.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "csmString.hpp" +#include +#include "CubismFramework.hpp" +#include "Utils/CubismDebug.hpp" + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { +csmInt32 csmString::s_totalInstanceNo = 0; + +namespace { +const csmChar* s_emptyString = ""; + +csmChar* GetEmptyString() +{ + return const_cast(s_emptyString); +} +} + +csmString::csmString() + : _ptr(NULL) + , _length(0) + , _hashcode(-1) +{ + this->_small[0] = '\0'; + _hashcode = CalcHashcode(WritePointer(), this->_length); + _instanceNo = s_totalInstanceNo++; +} + +csmString::csmString(const csmChar* c) +{ + const csmInt32 count = static_cast(strlen(c)); + + if (count) + { + Copy(c, count); + this->_hashcode = CalcHashcode(WritePointer(), _length); + } + else + { + SetEmpty(); + } + + _instanceNo = s_totalInstanceNo++; +} + +csmString::csmString(const csmString& s) +{ + if (!s.IsEmpty()) + { + Copy(s.GetRawString(), s._length); + this->_hashcode = s._hashcode; + } + else + { + SetEmpty(); + } + + _instanceNo = s_totalInstanceNo++; +} + +csmString::csmString(const csmChar* s, csmInt32 length) +{ + if (length) + { + Copy(s, length); + this->_hashcode = CalcHashcode(WritePointer(), _length); + } + else + { + SetEmpty(); + } + + _instanceNo = s_totalInstanceNo++; +} + +csmString::csmString(const csmChar* c, csmInt32 length, csmBool useptr) +{ + Initialize(c, length, useptr); + _instanceNo = s_totalInstanceNo++; +} + +void csmString::Initialize(const csmChar* c, csmInt32 length, csmBool usePtr) +{ + if (!length) + { + SetEmpty(); + + return; + } + + if (!usePtr) + { + Copy(c, length); + } + else + { + this->_ptr = const_cast(c); + this->_length = length; + this->_ptr[length] = 0x0; + } + + this->_hashcode = CalcHashcode(this->GetRawString(), this->_length); +} + +csmString::~csmString() +{ + if (IsEmpty()) + { + return; + } + + if (this->_ptr) + { + CSM_FREE(this->_ptr); + } +} + +void csmString::Clear() +{ + if (IsEmpty()) + { + return; + } + + CSM_FREE(this->_ptr); + + SetEmpty(); +} + +csmString& csmString::operator=(const csmChar* c) +{ + Clear(); //現在のポインタを開放してから処理する + + Copy(c, static_cast(strlen(c))); + this->_hashcode = CalcHashcode(this->GetRawString(), this->_length); + return *this; +} + +csmString& csmString::operator=(const csmString& s) +{ + Clear(); //現在のポインタを開放してから処理する + + Copy(s.GetRawString(), s._length); + this->_hashcode = s._hashcode; + return *this; +} + +csmString csmString::operator+(const csmString& s) const +{ + csmSizeType len1 = static_cast(this->_length); + csmSizeType len2 = static_cast(s._length); + + if (!len1 && !len2) + { + csmString ret; + return ret; + } + + if (len1 + len2 < SmallLength - 1) + { + csmChar buffer[SmallLength]; + csmChar* newptr = buffer; + + // + memcpy(newptr, GetRawString(), sizeof(csmChar) * len1); //nullを含めない + memcpy(&newptr[len1], s.GetRawString(), sizeof(csmChar) * (s._length + 1)); //nullを含めて+1 + + csmString ret(newptr, static_cast(len1 + len2), false); + return ret; + } + else + { + //メモリ管理の対象外とする(寿命が把握出来ない) + csmChar* newptr = static_cast(CSM_MALLOC(sizeof(csmChar) * (len1 + len2 + 1))); + + if (newptr == NULL) + { + csmString ret; + return ret; + } + + // + memcpy(newptr, GetRawString(), sizeof(csmChar) * len1); //nullを含めない + memcpy(&newptr[len1], s.GetRawString(), sizeof(csmChar) * (s._length + 1)); //nullを含めて+1 + + csmString ret(newptr, static_cast(len1 + len2), true); + return ret; + } +} + +csmString csmString::operator+(const csmChar* c) const +{ + csmSizeType len1 = static_cast(this->_length); + csmSizeType len2 = strlen(c); + + if (len1 + len2 < SmallLength - 1) + { + csmChar buffer[SmallLength]; + csmChar* newptr = buffer; + + // + memcpy(newptr, this->GetRawString(), sizeof(csmChar) * len1); //nullを含めない + memcpy(&newptr[len1], c, sizeof(csmChar) * (len2 + 1)); //nullを含めて+1 + + csmString ret(newptr, static_cast(len1 + len2), false); + return ret; + } + else + { + //メモリ管理の対象外とする(寿命が把握出来ない) + csmChar* newptr = static_cast(CSM_MALLOC(sizeof(csmChar) * (len1 + len2 + 1))); + + if (newptr == NULL) + { + csmString ret; + return ret; + } + + memcpy(newptr, this->GetRawString(), sizeof(csmChar) * len1); //nullを含めない + memcpy(&newptr[len1], c, sizeof(csmChar) * (len2 + 1)); //nullを含めて+1 + + csmString ret(newptr, static_cast(len1 + len2), true); + return ret; + } +} + +csmString& csmString::operator+=(const csmString& s) +{ + csmChar* newptr = NULL; + csmInt32 len1 = this->_length; + + if (this->_length + s._length < SmallLength - 1) + { + csmChar buffer[SmallLength]; + newptr = buffer; + + memcpy(newptr, this->GetRawString(), len1); //nullを含めない + memcpy(&newptr[len1], s.GetRawString(), s._length + 1); //nullを含めて+1 + + Clear(); // 現在のポインタを開放してから処理する + Initialize(newptr, len1 + s._length, false); + } + else + { + newptr = static_cast(CSM_MALLOC(sizeof(csmChar) * (this->_length + s._length + 1))); + + memcpy(newptr, this->GetRawString(), len1); //nullを含めない + memcpy(&newptr[len1], s.GetRawString(), s._length + 1); //nullを含めて+1 + + Clear(); //現在のポインタを開放してから処理する + Initialize(newptr, len1 + s._length, true); + } + return *this; +} + +csmString& csmString::operator+=(const csmChar* c) +{ + csmSizeType len1 = static_cast(this->_length); + csmSizeType len2 = strlen(c); + + csmChar* newptr = NULL; + + if (len1 + len2 < SmallLength - 1) + { + csmChar buffer[SmallLength]; + newptr = buffer; + + memcpy(newptr, GetRawString(), len1); //nullを含めない + memcpy(&newptr[len1], c, len2 + 1); //nullを含めて+1 + + Clear(); //現在のポインタを開放してから処理する + Initialize(newptr, static_cast(len1 + len2), false); + } + else + { + newptr = static_cast(CSM_MALLOC(sizeof(csmChar) * (len1 + len2 + 1))); + + memcpy(newptr, this->_ptr, len1); //nullを含めない + memcpy(&newptr[len1], c, len2 + 1); //nullを含めて+1 + + Clear(); //現在のポインタを開放してから処理する + Initialize(newptr, static_cast(len1 + len2), true); + } + return *this; +} + +csmBool csmString::operator==(const csmString& s) const +{ + //サイズ違い + if (s._length != this->_length) return false; + + //hashcode比較 + if (this->_hashcode != s._hashcode) return false; + + const csmChar* c1 = this->GetRawString(); + const csmChar* c2 = s.GetRawString(); + + //文字違い(逆順なのはPARAMの比較の特性) + for (csmInt32 i = this->_length - 1; i >= 0; --i) + { + if (c1[i] != c2[i]) return false; + } + return true; +} + +csmBool csmString::operator==(const csmChar* rc) const +{ + //サイズ違い + if (static_cast(strlen(rc)) != this->_length) return false; + + const csmChar* lc = this->GetRawString(); + + //文字違い(逆順なのはPARAMの比較の特性) + for (csmInt32 i = this->_length - 1; i >= 0; --i) + { + if (lc[i] != rc[i]) return false; + } + return true; +} + +csmBool csmString::operator<(const csmString& s) const +{ + return strcmp(this->GetRawString(), s.GetRawString()) < 0; +} + +csmBool csmString::operator<(const csmChar* c) const +{ + return strcmp(this->GetRawString(), c) < 0; +} + +csmBool csmString::operator>(const csmString& s) const +{ + return strcmp(this->GetRawString(), s.GetRawString()) > 0; +} + +csmBool csmString::operator>(const csmChar* c) const +{ + return strcmp(this->GetRawString(), c) > 0; +} + +csmString& csmString::Append(const csmChar* c, csmInt32 len2) +{ + csmInt32 len1 = this->_length; + + csmChar* newptr = NULL; + + if (len1 + len2 < SmallLength - 1) + { + csmChar buffer[SmallLength]; + newptr = buffer; + + memcpy(newptr, this->GetRawString(), len1); //nullを含めない + memcpy(&newptr[len1], c, len2); //nullを含めて+1 + + Clear(); // 現在のポインタを開放してから処理する + Initialize(newptr, len1 + len2, false); + } + else + { + newptr = static_cast(CSM_MALLOC(sizeof(csmChar) * (len1 + len2 + 1))); + + memcpy(newptr, this->GetRawString(), len1); //nullを含めない + memcpy(&newptr[len1], c, len2); //nullを含めて+1 + + Clear(); //現在のポインタを開放してから処理する + Initialize(newptr, len1 + len2, true); + } + return *this; +} + +csmString& csmString::Append(csmInt32 len2, const csmChar c) +{ + csmInt32 len1 = this->_length; + + csmChar* newptr = NULL; + + if(len1 + len2< SmallLength -1) + { + csmChar buffer[SmallLength]; + newptr = buffer; + memcpy(newptr, this->GetRawString(), len1); //nullを含めない + for (csmInt32 i = len1 + len2 - 1; i >= len1; --i) newptr[i] = c; + + Clear(); // 現在のポインタを開放してから処理する + Initialize(newptr, len1 + len2, false); + } + else + { + // 必要な長さ∔NULL分1バイトを新規確保 + newptr = static_cast(CSM_MALLOC(sizeof(csmChar) * (len1 + len2 + 1))); + // 元々の分をコピー + memcpy(newptr, this->GetRawString(), len1); //nullを含めない + // 新しい箇所をコピー + for (csmInt32 i = len1 + len2 - 1; i >= len1; --i) newptr[i] = c; + + Clear(); //現在のポインタを開放してから処理する + Initialize(newptr, len1 + len2, true); + } + return *this; +} + +void csmString::Copy(const csmChar* c, csmInt32 length) +{ + if (!length) + { + return; + } + + this->_length = length; + + if (this->_length < SmallLength -1) + { + this->_ptr = NULL; + + memcpy(this->_small, c, length); + this->_small[length] = 0x0; + } + else + { + this->_ptr = static_cast(CSM_MALLOC(sizeof(csmChar) * (length + 1))); + + memcpy(this->_ptr, c, length); + this->_ptr[length] = 0x0; + } +} + +csmInt32 csmString::CalcHashcode(const csmChar* c, csmInt32 length) +{ + csmInt32 hash = 0; + for (csmInt32 i = length; i >= 0; --i) + { + hash = hash * 31 + c[i]; + } + if ((hash == -1) || (c == GetEmptyString())) + { + hash = -2; //-1だけ特別な意味をもたせる + } + return hash; +} + +const csmChar* csmString::GetRawString() const +{ + if (this->_length < SmallLength -1) + { + return &_small[0]; + } + else + { + return _ptr; + } +} + +csmInt32 csmString::GetHashcode() +{ + if (_hashcode == -1) _hashcode = CalcHashcode(WritePointer(), this->_length); + return _hashcode; +} + +csmBool csmString::IsEmpty() const +{ +#ifdef CSM_DEBUG + + csmBool isEmpty = (_ptr == NULL && _small[0]=='\0'); + + if (isEmpty) + { + CSM_ASSERT(_length == 0); + } + + return isEmpty; + +#else + + //return (_ptr == GetEmptyString()); + + return (_ptr == NULL && _small[0] == '\0'); + +#endif +} + +void csmString::SetEmpty() +{ + //_ptr = GetEmptyString(); + _ptr = NULL; + _small[0] = '\0'; + _length = 0; + _hashcode = -1; + _hashcode = CalcHashcode(WritePointer(), this->_length); +} + +csmChar* csmString::WritePointer() +{ + if(this->_length< SmallLength -1) + { + return _small; + } + else + { + return _ptr; + } +} + +}}} + +//------------------------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Type/csmString.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismFramework.hpp" +#include + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + +/** + * @brief 文字列クラス
+ * コンシューマゲーム機等でSTLの組み込みを避けるための実装。
+ * std::string の簡易版マルチバイト文字列未対応 + */ +class csmString +{ +public: + /** + * @brief コンストラクタ + * + */ + csmString(); + + /** + * @brief 引数付きコンストラクタ + * + * @param c -> コピーする文字列 + */ + csmString(const csmChar* c); + + /** + * @brief 引数付きコンストラクタ + * + * @param c -> 文字列 + * @param length -> 文字列からコピーする長さ + */ + csmString(const csmChar* c, csmInt32 length); + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] s -> 文字列 + */ + csmString(const csmString& s); + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] c -> 文字列 + * @param[in] length -> 文字列からコピーする長さ + * @param[in] usePtr -> trueなら引数のポインタを保持する(インスタンスと共に破棄される)
falseなら内部でコピーを実行する + */ + csmString(const csmChar* c, csmInt32 length, csmBool usePtr); + + /** + * @brief デストラクタ + * + */ + virtual ~csmString(); + + /** + * @brief =演算子のオーバーロード(csmString型) + */ + csmString& operator=(const csmString& s); + + /** + * @brief =演算子のオーバーロード(csmChar型) + */ + csmString& operator=(const csmChar* c); + + /** + * @brief ==演算子のオーバーロード(csmString型) + */ + csmBool operator==(const csmString& s) const; + + /** + * @brief ==演算子のオーバーロード(csmChar型) + */ + csmBool operator==(const csmChar* c) const; + + /** + * @brief <演算子のオーバーロード(csmString型) + */ + csmBool operator<(const csmString& s) const; + + /** + * @brief <演算子のオーバーロード(csmChar型) + */ + csmBool operator<(const csmChar* c) const; + + /** + * @brief >演算子のオーバーロード(csmString型) + */ + csmBool operator>(const csmString& s) const; + + /** + * @brief >演算子のオーバーロード(csmChar型) + */ + csmBool operator>(const csmChar* c) const; + + /** + * @brief +演算子のオーバーロード(csmString型) + */ + csmString operator+(const csmString& s) const; + + /** + * @brief +演算子のオーバーロード(csmChar型) + */ + csmString operator+(const csmChar* c) const; + + /** + * @brief +=演算子のオーバーロード(csmString型) + */ + csmString& operator+=(const csmString& s); + + /** + * @brief +=演算子のオーバーロード(csmChar型) + */ + csmString& operator+=(const csmChar* c); + + /** + * @brief 文字列を後方に追加する + * + * @param[in] c -> 文字列 + * @param[in] length -> 追加する文字列の長さ + * @return 更新された文字列 + */ + csmString& Append(const csmChar* c, csmInt32 length); + + /** + * @brief 文字サイズを拡張して文字を埋める + * + * @param[in] length -> 拡張する文字数 + * @param[in] v -> 埋める文字 + * @return 更新された文字列 + */ + csmString& Append(csmInt32 length, const csmChar v); + + /** + * @brief 文字列の長さを返す + * + * @return 文字列の長さ + */ + csmInt32 GetLength() const { return _length; } + + /** + * @brief C言語文字列としてのポインタを取得する + * + * @return C言語文字列のポインタ + */ + const csmChar* GetRawString() const; + + /** + * @brief ポインタを解放する + */ + void Clear(); + + /** + * @brief ハッシュコードを取得する + * + * @return ハッシュコード + */ + csmInt32 GetHashcode(); + + +protected: + + /** + * @brief 文字列をコピーする. 終端に\0を追加する + * + * @param[in] c -> 文字列 + * @param[in] length -> コピーする文字数 + */ + void Copy(const csmChar* c, csmInt32 length); + + /** + * @brief csmStringインスタンスの初期化関数。文字列のセットとハッシュコードの算出を行う。 + * + * @param[in] c -> 文字列のポインタ + * @param[in] length -> 文字列の長さ + * @param[in] usePtr -> trueなら第一引数のポインタを保持する(インスタンスと共に破棄される)。
falseなら内部でコピーを実行する + */ + void Initialize(const csmChar* c, csmInt32 length, csmBool usePtr); + + /** + * @brief 文字列からハッシュ値を生成して返す + * + * @param[in] c -> 文字列 + * @param[in] length -> ハッシュ値の長さ + * @return 文字列から生成したハッシュ値 + */ + csmInt32 CalcHashcode(const csmChar* c, csmInt32 length); + +private: + static const csmInt32 SmallLength = 64; ///< この長さ-1未満の文字列は内部バッファを使用 + static const csmInt32 DefaultSize = 10; ///< デフォルトの文字数 + static csmInt32 s_totalInstanceNo; ///< 通算のインスタンス番号 + csmChar* _ptr; ///< 文字型配列のポインタ + csmInt32 _length; ///< 半角文字数(メモリ確保は最後に0が入るため_length+1) + csmInt32 _hashcode; ///< インスタンスに当てられたハッシュ値 + csmInt32 _instanceNo; ///< インスタンスに割り当てられた番号 + + csmChar _small[SmallLength]; ///< 文字列の長さがSmallLength-1未満の場合はこちらを使用 + + /** + * @brief 文字列が空かどうか? + * + * 文字列が空かどうか確認する。 + * + * @retval true 空の文字列 + * @retval false 値が設定されている + */ + csmBool IsEmpty() const; + + /** + * @brief 文字列を空に設定 + * + * 文字列を空に設定する。 + * + * @note ポインタに対してfree()はしない。 + */ + void SetEmpty(); + + /** + * @brief C言語文字列としてのポインタを取得する + * + * @return C言語文字列のポインタ + */ + csmChar* WritePointer(); +}; +}}} + +//------------------------- LIVE2D NAMESPACE ----------- + + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Type/csmVector.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "csmString.hpp" +#include "CubismFramework.hpp" +#include "Utils/CubismDebug.hpp" + +#ifndef NULL +# define NULL 0 +#endif + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { + +//========================テンプレートの宣言============================== + +/** + * @brief ベクター型(可変配列型)
+ * コンシューマゲーム機等でSTLの組み込みを避けるための実装。std::vector の簡易版 + * + */ +template +class csmVector +{ +public: + /** + * @brief コンストラクタ + */ + csmVector(); + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] initialCapacity -> 初期化後のキャパシティ。データサイズは_capacity * sizeof(T) + * @param[in] zeroClear -> trueなら初期化時に確保した領域を0で埋める + * + */ + csmVector(csmInt32 initialCapacity, csmBool zeroClear = false); + + /** + * @brief デストラクタ + * + */ + virtual ~csmVector(); + + /** + * @brief コンテナの先頭アドレスを返す + * + */ + T* GetPtr() + { + return _ptr; + } + + /** + * @brief []演算子のオーバーロード + * + */ + T& operator[](csmInt32 index) + { + return _ptr[index]; + } + + /** + * @brief []演算子のオーバーロード(const) + * + */ + const T& operator[](csmInt32 index) const + { + return _ptr[index]; + } + + /** + * @brief インデックスで指定した要素を返す + * + */ + T& At(int index) + { + return _ptr[index]; + } + + /** + * @brief PushBack処理.コンテナに新たな要素を追加する。 + * + * @param[in] value -> PushBack処理で追加する値。 + * @param[in] callPlacementNew -> PushBack時に配置newを呼び出す場合はtrue(クラスインスタンス) + * PushBack時に値を単純に代入する場合はfalse(プリミティブ、ポインタ) + */ + void PushBack(const T& value, csmBool callPlacementNew = true); + + /** + * @brief コンテナの全要素を解放する + * + */ + void Clear(); + + /** + * @brief コンテナの要素数を返す + * + * @return コンテナの要素数 + */ + csmUint32 GetSize() const { return _size; } + + /** + * @brief vector#resize()に相当するサイズ変更
+ * プリミティブ、ポインタ型などの時には、UpdateSize(size,value, false)で呼び出すと多少パフォーマンスが改善する場合がある。 + * + */ + void Resize(csmInt32 size, T value = T()) + { + UpdateSize(size, value, true); + } + + /*** + * @brief vector#resize()に相当するサイズ変更 + * + * @param[in] size -> 新しいサイズ + * @param[in] value -> リサイズ時に埋める値 + * @param[in] callPlacementNew -> UpdateSize時に配置newを呼び出す場合はtrue(クラスインスタンス)
+ * UpdateSize時に値を単純に代入する場合はfalse(プリミティブ、ポインタ) + * + */ + void UpdateSize(csmInt32 size, T value = T(), csmBool callPlacementNew = true); + + /** + * @brief コンテナのキャパシティを確保する
+ * + * @param[in] newSize -> 新たなキャパシティ。引数の値が現在のサイズ未満の場合は何もしない。 + */ + void PrepareCapacity(csmInt32 newSize); + + /** + * @brief csmVectorのイテレータの前方宣言 + */ + class iterator; + + /** + * @brief コンテナにコンテナ要素を挿入する + * + * @param[in] position -> 挿入する位置 + * @param[in] begin -> 挿入するコンテナの開始位置 + * @param[in] end -> 挿入するコンテナの終端位置 + * @param[in] callPlacementNew -> Insert時に配置newを呼び出す場合はtrue(クラスインスタンス) + * Insert時に値を単純に代入する場合はfalse(プリミティブ、ポインタ) + */ + void Insert(iterator position, iterator begin, iterator end, bool callPlacementNew = true); + + /** + * @brief コンテナの全要素に対して代入処理を行う。 + * + * @param[in] newSize -> 代入処理後のサイズ + * @param[in] value -> 要素に代入する値 + * @param[in] callPlacementNew -> Assign時に配置newを呼び出す場合はtrue(クラスインスタンス)
+ * Assign時に値を単純に代入する場合はfalse(プリミティブ、ポインタ) + * + */ + void Assign(csmInt32 newSize, T value = T(), csmBool callPlacementNew = true); + + /** + * @brief コンテナからインデックスで指定した要素を削除する + * + * @param[in] index -> インデックス値 + * @retval true -> 削除実行 + * @retval false -> 削除範囲外 + */ + csmBool Remove(csmInt32 index) + { + if (index < 0 || _size <= index) return false; // 削除範囲外 + _ptr[index].~T(); + + // 削除(メモリをシフトする)、最後の一つを削除する場合はmove不要 + if (index < _size - 1) memmove(&(_ptr[index]), &(_ptr[index + 1]), sizeof(T) * (_size - index - 1)); + --_size; + return true; + } + + /** + * @brief csmVectorのイテレータ + * + */ + class iterator + { + // csmVectorをフレンドクラスとする + friend class csmVector; + + public: + /** + * @brief コンストラクタ + * + */ + iterator() : _index(0) + , _vector(NULL) {} + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] v -> csmVectorのオブジェクト + * + */ + iterator(csmVector* v) : _index(0) + , _vector(v) {} + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] v -> csmVectorのオブジェクト + * @param[in] idx -> コンテナから参照するインデックス値 + */ + iterator(csmVector* v, csmInt32 idx) : _index(idx) + , _vector(v) {} + + /** + * @brief =演算子のオーバーロード + * + */ + iterator& operator=(const iterator& ite) + { + this->_index = ite._index; + this->_vector = ite._vector; + return *this; + } + + /** + * @brief 前置++演算子のオーバーロード + * + */ + iterator& operator++() + { + ++this->_index; + return *this; + } + + /** + * @brief 前置--演算子のオーバーロード + * + */ + iterator& operator--() + { + --this->_index; + return *this; + } + + /** + * @brief 後置++演算子のオーバーロード(intは後置用のダミー引数) + * + */ + iterator operator++(csmInt32) + { + iterator iteold(this->_vector, this->_index++); // 古い値を保存 + return iteold; // 古い値を返す + } + + /** + * @brief 後置--演算子のオーバーロード(intは後置用のダミー引数) + * + */ + iterator operator--(csmInt32) + { + iterator iteold(this->_vector, this->_index--); // 古い値を保存 + return iteold; + } + + /** + * @brief *演算子のオーバーロード + * + */ + T& operator*() const + { + return this->_vector->_ptr[this->_index]; + } + + /** + * @brief !=演算子のオーバーロード + * + */ + csmBool operator!=(const iterator& ite) const + { + return (this->_index != ite._index) || (this->_vector != ite._vector); + } + + private: + csmInt32 _index; ///< コンテナのインデックス値 + csmVector* _vector; ///< コンテナの参照 + + }; + + /** + * @brief csmVectorのイテレータ(const) + * + */ + class const_iterator + { + // csmVectorをフレンドクラスとする + friend class csmVector; + + public: + /** + * @brief コンストラクタ + * + */ + const_iterator() : _index(0) + , _vector(NULL) {} + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] v -> csmVectorのオブジェクト + * + */ + const_iterator(const csmVector* v) : _index(0) + , _vector(v) {} + + /** + * @brief 引数付きコンストラクタ + * + * @param[in] v -> csmVectorのオブジェクト + * @param[in] idx -> コンテナから参照するインデックス値 + */ + const_iterator(const csmVector* v, csmInt32 idx) : _index(idx) + , _vector(v) {} + /** + * @brief =演算子のオーバーロード + * + */ + const_iterator& operator=(const const_iterator& ite) + { + this->_index = ite._index; + this->_vector = ite._vector; + return *this; + } + + /** + * @brief 前置++演算子のオーバーロード + * + */ + const_iterator& operator++() + { + ++this->_index; + return *this; + } + + /** + * @brief 前置--演算子のオーバーロード + * + */ + const_iterator& operator--() + { + --this->_index; + return *this; + } + + /** + * @brief 後置++演算子のオーバーロード(intは後置用のダミー引数) + * + */ + const_iterator operator++(csmInt32) + { + const_iterator iteold(this->_vector, this->_index++); // 古い値を保存 + return iteold; // 古い値を返す + } + + /** + * @brief 後置--演算子のオーバーロード(intは後置用のダミー引数) + * + */ + const_iterator operator--(csmInt32) + { + const_iterator iteold(this->_vector, this->_index--); // 古い値を保存 + return iteold; + } + + /** + * @brief *演算子のオーバーロード + * + */ + T& operator*() const + { + return this->_vector->_ptr[this->_index]; + } + + /** + * @brief !=演算子のオーバーロード + * + */ + csmBool operator!=(const const_iterator& ite) const + { + return (this->_index != ite._index) || (this->_vector != ite._vector); + } + + private: + csmInt32 _index; ///< コンテナのインデックス値 + const csmVector* _vector; ///< コンテナのポインタ + }; + + /** + * @brief コンテナの先頭要素を返す + * + */ + const iterator Begin() + { + iterator ite(this, 0); + return ite; + } + + /** + * @bief コンテナの終端要素を返す + * + */ + const iterator End() + { + iterator ite(this, _size); // 終了 + + return ite; + } + + /** + * @brief コンテナの先頭要素を返す + * + */ + const const_iterator Begin() const + { + const_iterator ite(this, 0); + return ite; + } + + /** + * @bief コンテナの終端要素を返す + * + */ + const const_iterator End() const + { + const_iterator ite(this, _size); // 終了 + return ite; + } + + /** + * @brief コンテナから要素を削除して他の要素をシフトする + * + * @param[in] ite -> 削除する要素 + * + */ + const iterator Erase(const iterator& ite) + { + csmInt32 index = ite._index; + if (index < 0 || _size <= index) return ite; // 削除範囲外 + + // 削除(メモリをシフトする)、最後の一つを削除する場合はmove不要 + if (index < _size - 1) memmove(&(_ptr[index]), &(_ptr[index + 1]), sizeof(T) * (_size - index - 1)); + --_size; + + iterator ite2(this, index); // 終了 + return ite2; + } + + /** + * @brief コンテナから要素を削除して他の要素をシフトする + * + * @param[in] ite -> 削除する要素 + * + */ + const const_iterator Erase(const const_iterator& ite) + { + csmInt32 index = ite._index; + if (index < 0 || _size <= index) return ite; // 削除範囲外 + + // 削除(メモリをシフトする)、最後の一つを削除する場合はmove不要 + if (index < _size - 1) memmove(&(_ptr[index]), &(_ptr[index + 1]), sizeof(T) * (_size - index - 1)); + + --_size; + + const_iterator ite2(this, index); // 終了 + return ite2; + } + + /** + * @brief コンテナの値を32ビット符号付き整数型でダンプする + * + */ + void DumpAsInt() + { + for (csmInt32 i = 0; i < _size; i++) CubismLogDebug("%d ,", _ptr[i]); + CubismLogDebug("\n"); + } + + /** + * @brief コピーコンストラクタ + * + * @param[in] c -> csmVectorのインスタンス + */ + csmVector(const csmVector& c) + { + Copy(c); + } + + /** + * @brief コピーコンストラクタ + * + * @param[in] c -> csmVectorのインスタンス + */ + csmVector& operator=(const csmVector& c) + { + if (this != &c) + { + Clear(); + Copy(c); + } + + return *this; + } + +private: + static const csmInt32 s_defaultSize = 10; ///< コンテナ初期化のデフォルトサイズ + + /** + * @brief csmVectorのコピー関数 + * + * @param[in] c -> csmVectorのインスタンス + */ + void Copy(const csmVector& c) + { + _size = c._size; + _capacity = c._capacity; + + if (c._capacity == 0) + { + _ptr = NULL; + return; + } + + _ptr = (T*)CSM_MALLOC(_capacity * sizeof(T)); + + for (csmInt32 i = 0; i < _size; ++i) + { + CSM_PLACEMENT_NEW(&_ptr[i]) T(c._ptr[i]); + } + } + + T* _ptr; ///< コンテナの先頭アドレス(ポインタ) + csmInt32 _size; ///< コンテナの要素数(サイズ) + csmInt32 _capacity; ///< コンテナのキャパシティ +}; + +//========================テンプレートの定義============================== + +template +csmVector::csmVector() + : _ptr(NULL) + , _size(0) + , _capacity(0) +{ } + +template +csmVector::csmVector(csmInt32 initialCapacity, csmBool zeroClear) +{ + if (initialCapacity < 1) + { + _ptr = NULL; + _capacity = 0; + _size = 0; + } + else + { + _ptr = static_cast(CSM_MALLOC(sizeof(T) * initialCapacity)); // ここだけ calloc により、確保したバイトを0で埋める + + CSM_ASSERT(_ptr != NULL); + + if (zeroClear) + { + memset(_ptr, 0, sizeof(T) * initialCapacity); + } + + _capacity = initialCapacity; + _size = 0; + } +} + +template +csmVector::~csmVector() +{ + Clear(); +} + +template +void csmVector::PushBack(const T& value, csmBool callPlacementNew) +{ + if (_size >= _capacity) + { + PrepareCapacity(_capacity == 0 ? s_defaultSize : _capacity * 2); + } + + // placement new 指定のアドレスに、実体を生成する + if (callPlacementNew) + { + CSM_PLACEMENT_NEW(&_ptr[_size++]) T(value); + } + else + { + _ptr[_size++] = value; + } +} + +template +void csmVector::PrepareCapacity(csmInt32 newSize) +{ + if (newSize > _capacity) + { + if (_capacity == 0) + { + _ptr = static_cast(CSM_MALLOC(sizeof(T) * newSize)); + + CSM_ASSERT(_ptr != NULL); + + _capacity = newSize; + } + else + { + csmInt32 tmp_capacity = newSize; + T* tmp = static_cast(CSM_MALLOC(sizeof(T) * tmp_capacity)); + csmInt32 tmp_size = _size; + + CSM_ASSERT(tmp != NULL); + + for (csmInt32 i = 0; i < _size; i++) + { + CSM_PLACEMENT_NEW(&tmp[i]) T(_ptr[i]); + } + Clear(); + + _ptr = tmp; + _capacity = newSize; + _size = tmp_size; + } + } +} + +template +void csmVector::Clear() +{ + if (_ptr != NULL) + { + for (csmInt32 i = 0; i < _size; i++) + { + _ptr[i].~T(); + } + + CSM_FREE(_ptr); + } + + _ptr = NULL; + _size = 0; + _capacity = 0; +} + +template +void csmVector::UpdateSize(csmInt32 newSize, T value, csmBool callPlacementNew) +{ + csmInt32 cur_size = this->_size; + if (cur_size < newSize) + { + PrepareCapacity(newSize); // capacity更新 + + if (callPlacementNew) + { + for (csmInt32 i = _size; i < newSize; i++) + { + // placement new 指定のアドレスに、実体を生成する + CSM_PLACEMENT_NEW(&_ptr[i]) T(value); + } + } + else + { + for (csmInt32 i = _size; i < newSize; i++) + { + _ptr[i] = value; + } + } + } + else + { + //new_size <= _size + //--- + for (csmInt32 i = newSize; i < _size; i++) + { + _ptr[i].~T(); // 不要なので破棄する + } + } + this->_size = newSize; +} + +template +void csmVector::Assign(csmInt32 newSize, T value, csmBool callPlacementNew) +{ + csmInt32 cur_size = this->_size; + + // 全てデストラクト + for (csmInt32 i = 0; i < _size; i++) + { + _ptr[i].~T(); + } + + // + if (cur_size < newSize) + { + PrepareCapacity(newSize); // capacity更新 + } + + if (callPlacementNew) + { + for (csmInt32 i = 0; i < newSize; i++) + { + CSM_PLACEMENT_NEW(&_ptr[i]) T(value); // placement new 指定のアドレスに、実体を生成する + } + } + else + { + for (csmInt32 i = 0; i < newSize; i++) + { + _ptr[i] = value; + } + } + + this->_size = newSize; +} + +template +void csmVector::Insert(iterator position, iterator begin, iterator end, csmBool callPlacementNew) +{ + csmInt32 dst_si = position._index; + csmInt32 src_si = begin._index; + csmInt32 src_ei = end._index; + + csmInt32 addcount = src_ei - src_si; + + PrepareCapacity(_size + addcount); + + // 挿入用に既存データをシフトして隙間を作る + if (_size - dst_si > 0) + { + memmove(&(_ptr[dst_si + addcount]), &(_ptr[dst_si]), sizeof(T) * (_size - dst_si)); + } + + // placement new 指定のアドレスに、実体を生成する + if (callPlacementNew) + { + for (csmInt32 i = src_si; i < src_ei; i++, dst_si++) + { + CSM_PLACEMENT_NEW(&_ptr[i]) T(begin._vector->_ptr[i]); + } + } + else + { + for (csmInt32 i = src_si; i < src_ei; i++, dst_si++) + { + _ptr[dst_si] = begin._vector->_ptr[i]; + } + } + + this->_size = _size + addcount; +} +}}} + +//--------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Utils/CubismDebug.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismDebug.hpp" +#include "CubismFramework.hpp" +#include +#include + +//------------ LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { namespace Utils { + +void CubismDebug::Print(CubismFramework::Option::LogLevel logLevel, const csmChar* format, ...) +{ + // オプションで設定されたログ出力レベルを下回る場合はログに出さない + if (logLevel < CubismFramework::GetLoggingLevel()) + return; + + const Core::csmLogFunction logPrint = CubismFramework::CoreLogFunction; + + if (!logPrint) + return; + + csmChar buffer[256]; + va_list va; + va_start(va, format); +#ifdef _WINDOWS + vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, format, va); +#else + vsnprintf(buffer, sizeof(buffer), format, va); +#endif + va_end(va); + + logPrint(buffer); +} + +void CubismDebug::DumpBytes(CubismFramework::Option::LogLevel logLevel, const csmUint8* data, csmInt32 length) +{ + for (csmInt32 i = 0; i < length; i++) + { + if (i % 16 == 0 && i > 0) Print(logLevel, "\n"); + else if (i % 8 == 0 && i > 0) Print(logLevel, " "); + Print(logLevel, "%02X ", (data[i] & 0xFF)); + } + + Print(logLevel, "\n"); +} + +}}}} + +//------------ LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Utils/CubismDebug.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "CubismFramework.hpp" + +#ifdef CSM_DEBUG +#include "assert.h" +#define CSM_ASSERT(expr) assert(expr) +#else +#define CSM_ASSERT(expr) +#endif + +#define CubismLogPrint(level, fmt, ...) Live2D::Cubism::Framework::Utils::CubismDebug::Print(level, "[CSM]" fmt, ## __VA_ARGS__) +#define CubismLogPrintln(level, fmt, ...) CubismLogPrint(level, fmt "\n", ## __VA_ARGS__) + +#if CSM_LOG_LEVEL <= CSM_LOG_LEVEL_VERBOSE +#define CubismLogVerbose(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Verbose, "[V]" fmt, ## __VA_ARGS__) +#define CubismLogDebug(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Debug, "[D]" fmt, ## __VA_ARGS__) +#define CubismLogInfo(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Info, "[I]" fmt, ## __VA_ARGS__) +#define CubismLogWarning(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Warning, "[W]" fmt, ## __VA_ARGS__) +#define CubismLogError(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Error, "[E]" fmt, ## __VA_ARGS__) +#elif CSM_LOG_LEVEL == CSM_LOG_LEVEL_DEBUG +#define CubismLogVerbose(fmt, ...) +#define CubismLogDebug(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Debug, "[D]" fmt, ## __VA_ARGS__) +#define CubismLogInfo(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Info, "[I]" fmt, ## __VA_ARGS__) +#define CubismLogWarning(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Warning, "[W]" fmt, ## __VA_ARGS__) +#define CubismLogError(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Error, "[E]" fmt, ## __VA_ARGS__) +#elif CSM_LOG_LEVEL == CSM_LOG_LEVEL_INFO +#define CubismLogVerbose(fmt, ...) +#define CubismLogDebug(fmt, ...) +#define CubismLogInfo(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Info, "[I]" fmt, ## __VA_ARGS__) +#define CubismLogWarning(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Warning, "[W]" fmt, ## __VA_ARGS__) +#define CubismLogError(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Error, "[E]" fmt, ## __VA_ARGS__) +#elif CSM_LOG_LEVEL == CSM_LOG_LEVEL_WARNING +#define CubismLogVerbose(fmt, ...) +#define CubismLogDebug(fmt, ...) +#define CubismLogInfo(fmt, ...) +#define CubismLogWarning(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Warning, "[W]" fmt, ## __VA_ARGS__) +#define CubismLogError(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Error, "[E]" fmt, ## __VA_ARGS__) +#elif CSM_LOG_LEVEL == CSM_LOG_LEVEL_ERROR +#define CubismLogVerbose(fmt, ...) +#define CubismLogDebug(fmt, ...) +#define CubismLogInfo(fmt, ...) +#define CubismLogWarning(fmt, ...) +#define CubismLogError(fmt, ...) CubismLogPrintln(Live2D::Cubism::Framework::CubismFramework::Option::LogLevel_Error, "[E]" fmt, ## __VA_ARGS__) +#else +#define CubismLogVerbose(fmt, ...) +#define CubismLogDebug(fmt, ...) +#define CubismLogInfo(fmt, ...) +#define CubismLogWarning(fmt, ...) +#define CubismLogError(fmt, ...) +#endif + +//------------ LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { +template +class csmVector; +}}} + +namespace Live2D { namespace Cubism { namespace Framework { namespace Utils { + +/** + * @brief デバッグ用ユーティリティクラス。
+ * ログの出力、バイトのダンプなど + */ +class CubismDebug +{ +public: + /** + *@brief ログを出力する。第一引数にログレベルを設定する。
+ * CubismFramework::Initialize()時にオプションで設定されたログ出力レベルを下回る場合はログに出さない。 + * + *@param logLevel -> ログレベルの設定 + *@param format -> 書式付き文字列 + *@param ... -> 可変長引数 + * + */ + static void Print(CubismFramework::Option::LogLevel logLevel, const csmChar* format, ...); + + /** + *@brief データから指定した長さだけダンプ出力する。
+ * CubismFramework::Initialize()時にオプションで設定されたログ出力レベルを下回る場合はログに出さない。 + * + *@param logLevel -> ログレベルの設定 + *@param data -> ダンプするデータ + *@param length -> ダンプする長さ + * + */ + static void DumpBytes(CubismFramework::Option::LogLevel logLevel, const csmUint8* data, csmInt32 length); + +private: + + /** + * privateコンストラクタ + */ + CubismDebug(); + +}; +}}}} + +//------------ LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Utils/CubismJson.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismJson.hpp" +#include +#include "Type/csmString.hpp" +#include "CubismDebug.hpp" + +using namespace std; // for strtof + +//------------ LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { namespace Utils { + +//StaticInitializeNotForClientCall()で初期化する +Boolean* Boolean::TrueValue = NULL; +Boolean* Boolean::FalseValue = NULL; +Value* Value::ErrorValue = NULL; +Value* Value::NullValue = NULL; +csmVector* Value::s_dummyKeys = NULL; + +void Value::StaticReleaseNotForClientCall() +{ + CSM_DELETE(Boolean::TrueValue); + CSM_DELETE(Boolean::FalseValue); + CSM_DELETE(Value::ErrorValue); + CSM_DELETE(Value::NullValue); + CSM_DELETE(Value::s_dummyKeys); + + Boolean::TrueValue = NULL; + Boolean::FalseValue = NULL; + Value::ErrorValue = NULL; + Value::NullValue = NULL; + Value::s_dummyKeys = NULL; +} + +void Value::StaticInitializeNotForClientCall() +{ + Boolean::TrueValue = CSM_NEW Boolean(true); + Boolean::FalseValue = CSM_NEW Boolean(false); + + Value::ErrorValue = CSM_NEW Error("ERROR", true); + Value::NullValue = CSM_NEW Utils::NullValue(); + + Value::s_dummyKeys = CSM_NEW csmVector(); +} + +CubismJson::CubismJson() + : _error(NULL) + , _lineCount(0) + , _root(NULL) +{ } + +CubismJson::CubismJson(const csmByte* buffer, csmInt32 length) + : _error(NULL) + , _lineCount(0) + , _root(NULL) +{ + ParseBytes(buffer, length); +} + +CubismJson::~CubismJson() +{ + if (_root && !_root->IsStatic()) + { + CSM_DELETE(_root); + } + + _root = NULL; +} + +void CubismJson::Delete(CubismJson* instance) +{ + CSM_DELETE_SELF(CubismJson, instance); +} + + +CubismJson* CubismJson::Create(const csmByte* buffer, csmSizeInt size) +{ + CubismJson* json = CSM_NEW CubismJson(); + const csmBool succeeded = json->ParseBytes(buffer, size); + + if (!succeeded) + { + CubismJson::Delete(json); + return NULL; + } + else + { + return json; + } +} + + +Value& CubismJson::GetRoot() const +{ + return *_root; +} + + +csmBool CubismJson::ParseBytes(const csmByte* buffer, csmInt32 size) +{ + csmInt32 endPos; + _root = ParseValue(reinterpret_cast(buffer), size, 0, &endPos); + + if (_error) + { +#if defined(CSM_TARGET_WIN_GL) || defined(_MSC_VER) + csmChar strbuf[256] = {'\0'}; + _snprintf_s(strbuf, 256, 256, "Json parse error : @line %d\n", (_lineCount + 1)); + _root = CSM_NEW String(strbuf); +#else + csmChar strbuf[256] = { '\0' }; + snprintf(strbuf, 256, "Json parse error : @line %d\n", (_lineCount + 1)); + _root = CSM_NEW String(strbuf); +#endif + CubismLogInfo("%s", _root->GetRawString()); + return false; + } + else if (_root == NULL) + { + _root = CSM_NEW Error(_error, false); //rootは開放されるのでエラーオブジェクトを別途作る + return false; + } + return true; +} + + +csmString CubismJson::ParseString(const csmChar* string, csmInt32 length, csmInt32 begin, csmInt32* outEndPos) +{ + if (_error) + { + return NULL; + } + + if (!string) + { + _error = "string is null"; + return NULL; + } + + csmInt32 i = begin; + csmChar c, c2; + csmString ret; + csmInt32 buf_start = begin; //sbufに登録されていない文字の開始位置 + + for (; i < length; i++) + { + c = static_cast(string[i] & 0xFF); + + switch (c) + { + case '\"': {//終端の”, エスケープ文字は別に処理されるのでここにはこない + *outEndPos = i + 1; // ”の次の文字 + ret.Append(static_cast(string + buf_start), (i - buf_start)); + return ret; + } + case '\\': {//エスケープの場合 + i++; //2文字をセットで扱う + + if (i - 1 > buf_start) + { + ret.Append(static_cast(string + buf_start), (i - buf_start - 1)); //前の文字までを登録する + } + buf_start = i + 1; //エスケープ(2文字)の次の文字から + + if (i < length) + { + c2 = static_cast(string[i] & 0xFF); + switch (c2) + { + case '\\': ret.Append(1, '\\'); + break; + case '\"': ret.Append(1, '\"'); + break; + case '/': ret.Append(1, '/'); + break; + + case 'b': ret.Append(1, '\b'); + break; + case 'f': ret.Append(1, '\f'); + break; + case 'n': ret.Append(1, '\n'); + break; + case 'r': ret.Append(1, '\r'); + break; + case 't': ret.Append(1, '\t'); + break; + case 'u': + _error = "parse string/unicode escape not supported"; + default: + break; + } + } + else + { + _error = "parse string/escape error"; + } + break; + } + default: { + break; + } + } + } + _error = "parse string/illegal end"; + return NULL; +} + + +Value* CubismJson::ParseObject(const csmChar* buffer, csmInt32 length, csmInt32 begin, csmInt32* outEndPos) +{ + if (_error) + { + return NULL; + } + + if (!buffer) + { + _error = "buffer is null"; + return NULL; + } + + Map* ret = CSM_NEW Map(); + + //key : value , + csmString key; + csmInt32 i = begin; + csmChar c; + csmInt32 local_ret_endpos2[1]; + csmBool ok = false; + + // , が続く限りループ + for (; i < length; i++) + { + for (; i < length; i++) + { + c = static_cast(buffer[i] & 0xFF); + + switch (c) + { + case '\"': + key = ParseString(buffer, length, i + 1, local_ret_endpos2); + if (_error) return NULL; + i = local_ret_endpos2[0]; + ok = true; + goto BREAK_LOOP1; //-- loopから出る + case '}': //閉じカッコ + *outEndPos = i + 1; + return ret; //空 + case ':': + _error = "illegal ':' position"; + break; + case '\n': _lineCount++; + default: break; //スキップする文字 + } + } + BREAK_LOOP1: + if (!ok) + { + _error = "key not found"; + return NULL; + } + + ok = false; + + // : をチェック + for (; i < length; i++) + { + c = static_cast(buffer[i] & 0xFF); + + switch (c) + { + case ':': ok = true; + i++; + goto BREAK_LOOP2; //-- loopから出る + + case '}': + _error = "illegal '}' position"; + break; + case '\n': _lineCount++; + //case ' ': case '\t': case '\r': + default: break; //スキップする文字 + } + } + BREAK_LOOP2: + + if (!ok) + { + _error = "':' not found"; + return NULL; + } + + // 値をチェック + Value* value = ParseValue(buffer, length, i, local_ret_endpos2); + if (_error) + { + return NULL; + } + i = local_ret_endpos2[0]; + // ret.put( key , value ) ; + ret->Put(key, value); + + for (; i < length; i++) + { + c = static_cast(buffer[i] & 0xFF); + + switch (c) + { + case ',': + goto BREAK_LOOP3; + case '}': + *outEndPos = i + 1; + return ret; // << [] 正常終了 >> + case '\n': _lineCount++; + //case ' ': case '\t': case '\r': + default: break; //スキップ + } + } + BREAK_LOOP3: + ; //dummy + } + + _error = "illegal end of parseObject"; + return NULL; +} + + +Value* CubismJson::ParseArray(const csmChar* buffer, csmInt32 length, csmInt32 begin, csmInt32* outEndPos) +{ + if (_error) + { + return NULL; + } + + if (!buffer) + { + _error = "buffer is null"; + return NULL; + } + + Array* ret = CSM_NEW Array(); + + //key : value , + csmInt32 i = begin; + csmChar c; + csmInt32 local_ret_endpos2[1]; + + // , が続く限りループ + for (; i < length; i++) + { + // : をチェック + Value* value = ParseValue(buffer, length, i, local_ret_endpos2); + if (_error) + { + return NULL; + } + i = local_ret_endpos2[0]; + if (value) + { + ret->Add(value); + } + + //FOR_LOOP3: + //bool breakflag = false; + for (; i < length; i++) + { + c = static_cast(buffer[i] & 0xFF); + + switch (c) + { + case ',': + // breakflag = true; + // break;//次のKEY,VALUEへ + goto BREAK_LOOP3; + case ']': + *outEndPos = i + 1; + return ret; //終了 + case '\n': ++_lineCount; + //case ' ': case '\t': case '\r': + default: break; //スキップ + } + } + BREAK_LOOP3: + ; //dummy + } + + CSM_DELETE(ret); + _error = "illegal end of parseObject"; + return NULL; +} + + +Value* CubismJson::ParseValue(const csmChar* buffer, csmInt32 length, csmInt32 begin, csmInt32* outEndPos) +{ + if (_error) + { + return NULL; + } + + if (!buffer) + { + _error = "buffer is null"; + return NULL; + } + + Value* o = NULL; + csmInt32 i = begin; + csmFloat32 f; + csmString s1; //デバッグ用に使っている + + for (; i < length; i++) + { + csmChar c = static_cast(buffer[i] & 0xFF); + + switch (c) + { + case '-': case '.': + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + { + char* ret_ptr; + f = strtof(const_cast(buffer + i), &ret_ptr); + *outEndPos = static_cast(ret_ptr - buffer); + return CSM_NEW Float(f); + } + case '\"': + return CSM_NEW String(ParseString(buffer, length, i + 1, outEndPos)); //\"の次の文字から + case '[': + o = ParseArray(buffer, length, i + 1, outEndPos); + return o; + case '{': + o = ParseObject(buffer, length, i + 1, outEndPos); + return o; + case 'n': //null以外にない + if (i + 3 < length) + { + o = CSM_NEW NullValue(); //開放できるようにする + *outEndPos = i + 4; + } + else _error = "parse null"; + return o; + case 't': //true以外にない + if (i + 3 < length) + { + o = Boolean::TrueValue; + *outEndPos = i + 4; + } + else _error = "parse true"; + return o; + case 'f': //false以外にない + if (i + 4 < length) + { + o = Boolean::FalseValue; + *outEndPos = i + 5; + } + else _error = "parse false"; + return o; + case ',': //Array separator + _error = "illegal ',' position"; + return NULL; + case ']': //不正な}だがスキップする。配列の最後に不要な , があると思われる + *outEndPos = i; //同じ文字を再処理 + return NULL; + case '\n': _lineCount++; + case ' ': case '\t': case '\r': + default: //スキップ + break; + } + } + + _error = "illegal end of value"; + return NULL; +} + + +Map::~Map() +{ + csmMap::const_iterator ite = _map.Begin(); + while (ite != _map.End()) + { + Value* v = (*ite).Second; + if (v && !v->IsStatic()) + { + CSM_DELETE(v); + } + ++ite; + } + + if (_keys) + { + CSM_DELETE(_keys); + } +} + + +Array::~Array() +{ + csmVector::iterator ite = _array.Begin(); + for (; ite != _array.End(); ++ite) + { + Value* v = (*ite); + if (v && !v->IsStatic()) + { + CSM_DELETE(v); + } + } +} +}}}} +//------------ LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Utils/CubismJson.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once +#include +#include "CubismFramework.hpp" +#include "Type/csmVector.hpp" +#include "Type/csmMap.hpp" +#include "Type/csmString.hpp" + +//------------ LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { namespace Utils { +class Value; +class Error; +class NullValue; + +#define CSM_JSON_ERROR_TYPE_MISMATCH "Error:type mismatch" +#define CSM_JSON_ERROR_INDEX_OUT_OF_BOUNDS "Error:index out of bounds" + + +/** + * @brief パースしたJSONエレメントの要素の基底クラス + * + */ +class Value +{ + friend class Array; + friend class Live2D::Cubism::Framework::CubismFramework; + +public: + static Value* ErrorValue; ///< 一時的な返り値として返すエラー。CubismFramework::Dispose()するまではCSM_DELETEしない。 + static Value* NullValue; ///< 一時的な返り値として返すNULL。CubismFramework::Dispose()するまではCSM_DELETEしない。 + + /** + * @brief コンストラクタ + * + */ + Value() {} + + /** + * @brief デストラクタ + * + */ + virtual ~Value() {} + + /** + * @brief 要素を文字列で返す(csmString型) + * + */ + virtual const csmString& GetString(const csmString& defaultValue = "", const csmString& indent = "") = 0; + + /** + * @brief 要素を文字列で返す(csmChar*) + * + */ + virtual const csmChar* GetRawString(const csmString& defaultValue = "", const csmString& indent = "") + { + return this->GetString(defaultValue, indent).GetRawString(); + } + + /** + * @brief 要素を数値型で返す(csmInt32) + * + */ + virtual csmInt32 ToInt(csmInt32 defaultValue = 0) { return defaultValue; } + + /** + * @brief 要素を数値型で返す(csmFloat32) + * + */ + virtual csmFloat32 ToFloat(csmFloat32 defaultValue = 0.0f) { return defaultValue; } + + /** + * @brief 要素を真偽値で返す(csmBool) + * + */ + virtual csmBool ToBoolean(csmBool defaultValue = false) { return defaultValue; } + + /** + * @brief 要素を真偽値で返す(csmBool) + * + */ + virtual csmInt32 GetSize() { return 0; } + + /** + * @brief 要素をコンテナで返す(csmVector) + * + */ + virtual csmVector* GetVector(csmVector* defaultValue = NULL) { + if (!defaultValue) + { + defaultValue = new csmVector; + } + return defaultValue; + } + + /** + * @brief 要素をマップで返す(csmMap) + * + */ + virtual csmMap* GetMap(csmMap* defaultValue = NULL) { return defaultValue; } + + /** + * @brief 添字演算子[csmInt32] + * + */ + virtual Value& operator[](csmInt32 index) + { + return *(ErrorValue->SetErrorNotForClientCall(CSM_JSON_ERROR_TYPE_MISMATCH)); + } + + /** + * @brief 添字演算子[csmString] + * + */ + virtual Value& operator[](const csmString& string) + { + return *(NullValue->SetErrorNotForClientCall(CSM_JSON_ERROR_TYPE_MISMATCH)); + } + + /** + * @brief 添字演算子[csmChar*] + * + */ + virtual Value& operator[](const csmChar* s) + { + return *(NullValue->SetErrorNotForClientCall(CSM_JSON_ERROR_TYPE_MISMATCH)); + } + + /** + * @brief マップのキー一覧をコンテナで返す + * + * @return マップのキー一覧 + */ + virtual csmVector& GetKeys() + { + return *s_dummyKeys; + } + + /** + *@brief Valueの種類がエラー値ならtrue。 + */ + virtual csmBool IsError() { return false; } + + /** + *@brief Valueの種類がNULL値ならtrue。 + */ + virtual csmBool IsNull() { return false; } + + /** + *@brief Valueの種類が真偽値ならtrue。 + */ + virtual csmBool IsBool() { return false; } + + /** + *@brief Valueの種類が数値型ならtrue。 + */ + virtual csmBool IsFloat() { return false; } + + /** + *@brief Valueの種類が文字列ならtrue。 + */ + virtual csmBool IsString() { return false; } + + /** + *@brief Valueの種類が配列ならtrue。 + */ + virtual csmBool IsArray() { return false; } + + /** + *@brief Valueの種類がマップ型ならtrue。 + */ + virtual csmBool IsMap() { return false; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(const csmString& value) { return false; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(const csmChar* value) { return false; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(csmInt32 value) { return false; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(csmFloat32 value) { return false; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(csmBool value) { return false; } + + /** + *@brief Valueの値が静的ならtrue. 静的なら解放しない + */ + virtual csmBool IsStatic() { return false; } + + /** + *@brief Valueにエラー値をセットする + */ + virtual Value* SetErrorNotForClientCall(const csmChar* errorStr) { + this->_stringBuffer = errorStr; + return NullValue; + } + +protected: + csmString _stringBuffer; ///< 文字列バッファ + +private: + static csmVector* s_dummyKeys; ///< ダミーキー + + /** + * @brief 初期化用メソッド + */ + static void StaticInitializeNotForClientCall(); + + /** + * @brief リリース用メソッド + */ + static void StaticReleaseNotForClientCall(); + + +}; + +/** + * @brief Ascii文字のみ対応した最小限の軽量JSONパーサ。
+ * 仕様はJSONのサブセットとなる。
+ * 設定ファイル(model3.json)などのロード用
+ *
+ * [未対応項目]
+ * ・日本語などの非ASCII文字
+ * ・e による指数表現 + */ +class CubismJson +{ +public: + /** + * @brief バイトデータから直接ロードしてパースする
+ * 引数 buffer は外部で管理(破棄)する必要がある + * + * @param buffer -> バイトデータのバッファ + * @param size -> バッファサイズ + * @return CubismJsonクラスのインスタンス。失敗したらNULL。 + */ + static CubismJson* Create(const csmByte* buffer, csmSizeInt size); + + /** + * @brief パースしたJSONオブジェクトの解放処理 + * + * @param[in] instance CubismJsonクラスのインスタンス + * + */ + static void Delete(CubismJson* instance); + + /** + * @brief パースしたJSONのルート要素のポインタを返す + * + */ + Value& GetRoot() const; + + /** + * @brief パース時のエラー値を返す + * + */ + const csmChar* GetParseError() const { return _error; } + + /** + * @brief ルート要素の次の要素がファイルの終端だったらtrueを返す + * + */ + csmBool CheckEndOfFile() const { return (*_root)[1].Equals("EOF"); } + +protected: + /** + * @brief JSONのパースを実行する + * + * @param[in] buffer -> パース対象のデータバイト + * @param[in] size -> データバイトのサイズ + * @retval true -> 成功 + * @retval false -> 失敗 + */ + csmBool ParseBytes(const csmByte* buffer, csmInt32 size); + + /** + * @brief 次の「"」までの文字列をパースする。文字列は外部で解放する必要がある。 + * + * @param[in] string -> パース対象の文字列 + * @param[in] length -> パースする長さ + * @param[in] begin -> パースを開始する位置 + * @param[out] outEndPos -> パース終了時の位置 + * @return パースした文字列要素 + */ + csmString ParseString(const csmChar* string, csmInt32 length, csmInt32 begin, csmInt32* outEndPos); + + + /** + * @brief JSONのオブジェクトエレメントをパースしてValueオブジェクトを返す + * + * @param[in] buffer -> JSONエレメントのバッファ + * @param[in] length -> パースする長さ + * @param[in] begin -> パースを開始する位置 + * @param[out] outEndPos -> パース終了時の位置 + * @return パースから取得したValueオブジェクト + */ + Value* ParseObject(const csmChar* buffer, csmInt32 length, csmInt32 begin, csmInt32* outEndPos); + + /** + * @brief JSONの配列エレメントをパースしてValueオブジェクトを返す + * + * @param[in] buffer -> JSONエレメントのバッファ + * @param[in] length -> パースする長さ + * @param[in] begin -> パースを開始する位置 + * @param[out] outEndPos -> パース終了時の位置 + * @return パースから取得したValueオブジェクト + */ + Value* ParseArray(const csmChar* buffer, csmInt32 length, csmInt32 begin, csmInt32* outEndPos); + + /** + * @brief JSONエレメントからValue(float,String,Value*,Array,null,true,false)をパースする
+ * エレメントの書式に応じて内部でParseString(), ParseObject(), ParseArray()を呼ぶ
+ * + * @param[in] buffer -> JSONエレメントのバッファ + * @param[in] length -> パースする長さ + * @param[in] begin -> パースを開始する位置 + * @param[out] outEndPos -> パース終了時の位置 + * @return パースから取得したValueオブジェクト + */ + Value* ParseValue(const csmChar* buffer, csmInt32 length, csmInt32 begin, csmInt32* outEndPos); + +private: + /** + * @brief コンストラクタ + * + */ + CubismJson(); + + /** + * @brief 引数付きコンストラクタ + * @param[in] buffer -> JSONエレメントのバッファ + * @param[in] length -> パースする長さ + */ + CubismJson(const csmByte* buffer, csmInt32 length); + + /** + * @brief デストラクタ + * + */ + virtual ~CubismJson(); + + const csmChar* _error; ///< パース時のエラー + csmInt32 _lineCount; ///< エラー報告に用いる行数カウント + Value* _root; ///< パースされたルート要素 +}; + + +/** + *@brief パースしたJSONの要素をDouble値として扱う + */ +class Float : public Value +{ +public: + /** + * @brief コンストラクタ + */ + Float(csmFloat32 v) : Value() + { + this->_value = v; + } + + /** + * @brief デストラクタ + */ + virtual ~Float() {} + + /** + *@brief Valueの種類が数値型ならtrue。 + */ + virtual csmBool IsFloat() { return true; } + + /** + * @brief 要素を文字列で返す(csmString型) + * + */ + virtual const csmString& GetString(const csmString& defaultValue = "", const csmString& indent = "") + { +#if defined(CSM_TARGET_WIN_GL) || defined(_MSC_VER) + csmChar strbuf[32] = {'\0'}; + _snprintf_s(strbuf, 32, 32, "%f", this->_value); + _stringBuffer = csmString(strbuf); + return _stringBuffer; +#else + // string stream 未対応 + csmChar strbuf[32] = { '\0' }; + snprintf(strbuf, 32, "%f", this->_value); + _stringBuffer = csmString(strbuf); + return _stringBuffer; +#endif + } + + /** + * @brief 要素を数値型で返す(csmInt32) + */ + virtual csmInt32 ToInt(csmInt32 defaultValue = 0) { return static_cast(this->_value); } + + /** + * @brief 要素を数値型で返す(csmFloat32) + */ + virtual csmFloat32 ToFloat(csmFloat32 defaultValue = 0) { return this->_value; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(csmFloat32 v) { return v == this->_value; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(const csmString& v) { return false; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(const csmChar* v) { return false; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(csmInt32 v) { return false; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(csmBool v) { return false; } + +private: + csmFloat32 _value; ///< JSON要素の値 +}; + + +/** + * @brief パースしたJSONの要素を真偽値として扱う + * + */ +class Boolean : public Value +{ + friend class Value; + +public: + static Boolean* TrueValue; ///< true + static Boolean* FalseValue; ///< false + + /** + * @brief デストラクタ + * + */ + virtual ~Boolean() {} + + /** + *@brief Valueの種類が真偽値ならtrue。 + */ + virtual csmBool IsBool() { return true; } + + /** + * @brief 要素を真偽値で返す(csmBool) + */ + virtual csmBool ToBoolean(csmBool defaultValue = false) { return _boolValue; } + + /** + * @brief 要素を文字列で返す(csmString型) + */ + virtual const csmString& GetString(const csmString& defaultValue = "", const csmString& indent = "") + { + _stringBuffer = csmString(_boolValue ? "true" : "false"); + return _stringBuffer; + } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(csmBool v) { return v == _boolValue; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(const csmString& v) { return false; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(const csmChar* v) { return false; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(csmInt32 v) { return false; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(csmFloat32 v) { return false; } + + /** + *@brief Valueの値が静的ならtrue. 静的なら解放しない + */ + virtual csmBool IsStatic() { return true; } + +private: + /** + * @brief 引数付きコンストラクタ + */ + Boolean(csmBool v) : Value() { this->_boolValue = v; } + csmBool _boolValue; ///< JSON要素の値 +}; + + +/** + * @brief パースしたJSONの要素を文字列として扱う + * + */ +class String : public Value +{ +public: + /** + * @brief 引数付きコンストラクタ + */ + String(const csmString& s) : Value() { this->_stringBuffer = s; } + + /** + * @brief 引数付きコンストラクタ + */ + String(const csmChar* s) : Value() { this->_stringBuffer = s; } + + /** + * @brief デストラクタ + */ + virtual ~String() {} + + /** + *@brief Valueの種類が文字列ならtrue。 + */ + virtual csmBool IsString() { return true; } + + /** + * @brief 要素を文字列で返す(csmString型) + */ + virtual const csmString& GetString(const csmString& defaultValue = "", const csmString& indent = "") + { + return _stringBuffer; + } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(const csmString& v) { return (_stringBuffer == v); } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(const csmChar* v) { return (_stringBuffer == v); } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(csmInt32 v) { return false; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(csmFloat32 v) { return false; } + + /** + *@brief 引数の値と等しければtrue。 + */ + virtual csmBool Equals(csmBool v) { return false; } +}; + + +/** + * @brief JSONパース時のエラー結果。文字列型のように振る舞う + * + */ +class Error : public String +{ + friend class Value; // + friend class Array; // + friend class CubismJson; // + +public: + /** + *@brief Valueの値が静的ならtrue. 静的なら解放しない + */ + virtual csmBool IsStatic() { return _isStatic; } + + /** + * @brief エラー情報をセットする + */ + virtual Value* SetErrorNotForClientCall(const csmChar* s) + { + this->_stringBuffer = s; + return this; + } + +protected: + /** + * @brief 引数付きコンストラクタ + */ + Error(const csmString& s, csmBool isStatic) : String(s) + , _isStatic(isStatic) {} + + /** + * @brief デストラクタ + */ + virtual ~Error() {} + + /** + *@brief Valueの種類がエラー値ならtrue。 + */ + virtual csmBool IsError() { return true; } + + csmBool _isStatic; ///< 静的なValueかどうか +}; + + +/** + * @brief パースしたJSONの要素をNull値として持つ + */ +class NullValue : public Value +{ + friend class Value; // + friend class CubismJson; // + +public: + /** + * @brief デストラクタ + */ + virtual ~NullValue() {} + + /** + *@brief Valueの種類がNULL値ならtrue。 + */ + virtual csmBool IsNull() { return true; } + + /** + * @brief 要素を文字列で返す(csmString型) + */ + virtual const csmString& GetString(const csmString& defaultValue = "", const csmString& indent = "") + { + return _stringBuffer; + } + + /** + *@brief Valueの値が静的ならtrue. 静的なら解放しない + */ + virtual csmBool IsStatic() { return true; } + +private: + /** + * @brief コンストラクタ + */ + NullValue() : Value() { _stringBuffer = "NullValue"; } +}; + + +/** + * @brief パースしたJSONの要素を配列として持つ + * + */ +class Array : public Value +{ +public: + /** + * @brief コンストラクタ + */ + Array() : Value() + , _array() {} + + /** + * @brief デストラクタ + */ + virtual ~Array(); + + /** + *@brief Valueの種類が配列ならtrue。 + */ + virtual csmBool IsArray() { return true; } + + /** + * @brief 添字演算子[csmInt32] + * + */ + virtual Value& operator[](csmInt32 index) + { + if (index < 0 || (static_cast(_array.GetSize()) <= index)) + return *(ErrorValue->SetErrorNotForClientCall(CSM_JSON_ERROR_INDEX_OUT_OF_BOUNDS)); + Value* v = _array[index]; + + if (v == NULL) return *Value::NullValue; + return *v; + } + + /** + * @brief 添字演算子[csmString] + * + */ + virtual Value& operator[](const csmString& string) + { + return *(ErrorValue->SetErrorNotForClientCall(CSM_JSON_ERROR_TYPE_MISMATCH)); + } + + /** + * @brief 添字演算子[csmChar*] + * + */ + virtual Value& operator[](const csmChar* s) + { + return *(ErrorValue->SetErrorNotForClientCall(CSM_JSON_ERROR_TYPE_MISMATCH)); + } + + /** + * @brief 要素を文字列で返す(csmString型) + * + */ + virtual const csmString& GetString(const csmString& defaultValue = "", const csmString& indent = "") + { + _stringBuffer = indent + "[\n"; + csmVector::iterator ite = _array.Begin(); + for (; ite != _array.End(); ite++) + { + Value* v = (*ite); + _stringBuffer += indent + " " + v->GetString(indent + " ") + "\n"; + } + _stringBuffer += indent + "]\n"; + + return _stringBuffer; + } + + /** + * @brief 配列要素を追加する + */ + void Add(Value* v) { _array.PushBack(v, false); } + + /** + * @brief 要素をコンテナで返す(csmVector) + * + */ + virtual csmVector* GetVector(csmVector* defaultValue = NULL) { return &_array; } + + /** + * @brief 要素の数を返す + * + */ + virtual csmInt32 GetSize() { return static_cast(_array.GetSize()); } + +private: + csmVector _array; ///< JSON要素の値 +}; + + +/** + * @brief パースしたJSONの要素をマップとして持つ + * + */ +class Map : public Value +{ +public: + /** + * @brief コンストラクタ + */ + Map() : Value() + , _keys(NULL) {} + + /** + * @brief デストラクタ + */ + virtual ~Map(); + + /** + * @brief Valueの値がMap型ならtrue + */ + virtual csmBool IsMap() { return true; } + + /** + * @brief 添字演算子[csmString] + */ + virtual Value& operator[](const csmString& s) + { + Value* ret = _map[s]; + if (ret == NULL) + { + return *Value::NullValue; + } + return *ret; + } + + /** + * @brief 添字演算子[csmChar*] + * + */ + virtual Value& operator[](const csmChar* s) + { + for (csmMap::const_iterator iter = _map.Begin(); iter != _map.End(); ++iter) + { + if( strcmp(iter->First.GetRawString(), s) == 0 ) + { + if(iter->Second==NULL) + { + return *Value::NullValue; + } + return *iter->Second; + } + } + + return *Value::NullValue; + } + + /** + * @brief 添字演算子[csmInt32] + */ + virtual Value& operator[](csmInt32 index) + { + return *(ErrorValue->SetErrorNotForClientCall(CSM_JSON_ERROR_TYPE_MISMATCH)); + } + + virtual const csmString& GetString(const csmString& defaultValue = "", const csmString& indent = "") + { + _stringBuffer = indent + "{\n"; + csmMap::const_iterator ite = _map.Begin(); + while (ite != _map.End()) + { + const csmString& key = (*ite).First; + Value* v = (*ite).Second; + + _stringBuffer += indent + " " + key + " : " + v->GetString(indent + " ") + "\n"; + ++ite; + } + _stringBuffer += indent + "}\n"; + return _stringBuffer; + } + + /** + * @brief 要素をMap型で返す + */ + virtual csmMap* GetMap(csmMap* defaultValue = NULL) + { + return &_map; + } + + /** + * @brief Mapに要素を追加する + */ + void Put(csmString& key, Value* v) + { + _map[key] = v; + } + + /** + * @brief Mapからキーのリストを取得する + */ + virtual csmVector& GetKeys() + { + if (!_keys) + { + _keys = CSM_NEW csmVector(); + csmMap::const_iterator ite = _map.Begin(); + while (ite != _map.End()) + { + const csmString& key = (*ite).First; + _keys->PushBack(key, true); + ++ite; + } + } + return *_keys; + } + + /** + * @brief Mapの要素数を取得する + */ + virtual csmInt32 GetSize() { return static_cast(_keys->GetSize()); } + +private: + csmMap _map; ///< JSON要素の値 + csmVector* _keys; ///< JSON要素の値 +}; +}}}} + +//------------ LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Utils/CubismString.cpp`: + +```cpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#include "CubismString.hpp" +#include "Type/csmVector.hpp" +#include +#include + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { namespace Utils { + +//標準出力の戻り値が複製されるのでオーバーヘッドは大きい。 +csmString CubismString::GetFormatedString(const csmChar* format, ...) +{ + csmInt32 bufferSize = 256; + csmChar* buffer = static_cast(CSM_MALLOC(sizeof(csmChar)* bufferSize)); + + va_list args; + va_start(args, format); + + for (;;) { +#ifdef _WINDOWS + if (vsnprintf_s(buffer, bufferSize, _TRUNCATE, format, args) < bufferSize) { +#else + if (vsnprintf(buffer, bufferSize, format, args) < bufferSize) { +#endif + break; + } else { + // メモリが足りない為、拡張して確保しなおす。 + CSM_FREE(buffer); + bufferSize *= 2; + buffer = static_cast(CSM_MALLOC(sizeof(csmChar)* bufferSize)); + } + } + va_end(args); + + csmString ret = buffer; + CSM_FREE(buffer); + + return ret; // CubismString型にされて返されるためアドレスを返すので良い。 +} + +csmBool CubismString::IsStartsWith(const csmChar* text, const csmChar* startWord) +{ + while (*startWord != '\0') + { + if (*text == '\0' || *(text++) != *(startWord++)) + { + return false; + } + } + return true; +} + +csmFloat32 CubismString::StringToFloat(const csmChar* string, csmInt32 length, csmInt32 position, csmInt32* outEndPos) +{ + csmInt32 i = position; + csmBool minus = false; //マイナスフラグ + csmBool period = false; + csmFloat32 v1 = 0; + + //負号の確認 + csmInt32 c = string[i]; + if (c == '-') + { + minus = true; + i++; + } + + //整数部の確認 + for (; i < length; i++) + { + c = string[i]; + if ('0' <= c && c <= '9') + { + v1 = v1 * 10 + (c - '0'); + } + else if (c == '.') + { + period = true; + i++; + break; + } + else + { + break; + } + } + + //小数部の確認 + if (period) + { + csmFloat32 mul = 0.1f; + for (; i < length; i++) + { + c = string[i] & 0xFF; + if ('0' <= c && c <= '9') + { + v1 += mul * (c - '0'); + } + else + { + break; + } + mul *= 0.1f; //一桁下げる + if (!c) break; + } + } + + if (i == position) + { + //一文字も読み込まなかった場合 + *outEndPos = -1; //エラー値が入るので呼び出し元で適切な処理を行う + return 0; + } + + if (minus) v1 = -v1; + + *outEndPos = i; + return v1; +} + +}}}} + +//------------------------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/FrameWork/src/Utils/CubismString.hpp`: + +```hpp +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +#pragma once + +#include "Type/csmString.hpp" + +//--------- LIVE2D NAMESPACE ------------ +namespace Live2D { namespace Cubism { namespace Framework { namespace Utils{ +class CubismString +{ +public: + /** + * @brief 標準出力の書式を適用した文字列を取得する。 + * + * @param[in] format -> 標準出力の書式指定文字列 + * @param[in] ... -> 書式指定文字列に渡す文字列 + * @return 書式を適用した文字列 + */ + static csmString GetFormatedString(const csmChar* format, ...); + + /** + * @brief textがstartWordで始まっているかどうかを返す + * @param[in] text -> 検査対象の文字列 + * @param[in] startWord -> 比較対象の文字列 + * @retval false -> textがstartWordで始まっている + * @retval true -> textがstartWordで始まっていない + */ + static csmBool IsStartsWith(const csmChar* text, const csmChar* startWord); + + + /*** + * @brief position位置の文字から数字を解析する。 + * + * @param[in] string -> 文字列 + * @param[in] length -> 文字列の長さ + * @param[in] position -> 解析したい文字の位置 + * @param[out] outEndPos -> 一文字も読み込まなかった場合はエラー値(-1)が入る + * @return 解析結果の数値 + */ + static csmFloat32 StringToFloat(const csmChar* string, csmInt32 length, csmInt32 position, csmInt32* outEndPos); + +private: + // コンストラクタ・デストラクタ呼び出し不可な静的クラスにする + CubismString(); +}; +}}}} + +//--------- LIVE2D NAMESPACE ------------ + +``` + +`Source/UnrealLive2D/Private/UnrealLive2D.cpp`: + +```cpp +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +#include "UnrealLive2D.h" +#include "Core.h" +#include "Modules/ModuleManager.h" +#include "Interfaces/IPluginManager.h" +#include "CubismBpLib.h" +#include "Interfaces/IPluginManager.h" +#include "ShaderCore.h" + +#define LOCTEXT_NAMESPACE "FUnrealLive2DModule" + +DEFINE_LOG_CATEGORY(LogCubism); + +void FUnrealLive2DModule::StartupModule() +{ + TSharedPtr tsp_Plugin = IPluginManager::Get().FindPlugin(TEXT("UnrealLive2D")); + if (tsp_Plugin.IsValid()) + { + FString PluginShaderDir = FPaths::Combine(tsp_Plugin->GetBaseDir(), TEXT("Shaders")); + AddShaderSourceDirectoryMapping(TEXT("/Plugin/UnrealLive2D"), PluginShaderDir); + } + else + { + UE_LOG(LogCubism, Log, TEXT("FUnrealLive2DModule::StartupModule: Faild to find plugin , please check plugin config.")); + checkNoEntry(); + } +} + +void FUnrealLive2DModule::ShutdownModule() +{ + +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FUnrealLive2DModule, UnrealLive2D) +``` + +`Source/UnrealLive2D/Public/CubismBpLib.h`: + +```h +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "CubismBpLib.generated.h" + +class UCubismAsset; +class UTexture2D; + +/** + * + */ +UCLASS() +class UNREALLIVE2D_API UCubismBpLib : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable) + static void InitCubism(); + + UFUNCTION(BlueprintCallable, BlueprintPure) + static FString Get_CubismPath(); + +}; + +``` + +`Source/UnrealLive2D/Public/CubismObject.h`: + +```h +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include +#include "Templates/SharedPointer.h" +#include "Components/ActorComponent.h" +#include "Draw/CubismConfig.h" +#include "CubismUeTypes.h" +#include "CubismObject.generated.h" + +class UCubismDrawAble; +class UCubismAsset; +struct FModelInfo; + +/** + * + */ +UCLASS(Blueprintable,BlueprintType) +class UNREALLIVE2D_API UCubismObject : public UObject, public FTickableGameObject +{ + GENERATED_BODY() + +public: + UCubismObject(); + ~UCubismObject(); + + ////////////////////////////////////////////////////////////////////////// + /** Tick object */ + virtual bool IsTickableInEditor() const override; + virtual void Tick(float DeltaTime) override; + virtual TStatId GetStatId() const override; + virtual bool IsTickable() const override; + ////////////////////////////////////////////////////////////////////////// + + + void BeginDestroy() override; + + ////////////////////////////////////////////////////////////////////////// + +public: + UFUNCTION(BlueprintCallable, Category = "CubismModel") + void LoadFromPath(const FString& InPath); + + UFUNCTION(BlueprintCallable) + void SetUpdatePaused(bool InIsPaused); + +public: + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Render") + class UTextureRenderTarget2D* TargetRender = nullptr; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Render") + FModelConfig ModelConfig; + +private: + TSharedPtr msp_RawModel; + TSharedPtr msp_Render; + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cubism|Update") + int32 TickRate = 30; + +private: + float _TimeSeconds = 0.0f; + float _TimeNextUpdate = 0.0f; + float _TimeLastUpdate = 0.0f; + + bool _bPauseUpdate = false; + + ////////////////////////////////////////////////////////////////////////// +public: + /** Event called while motion play ends */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable) + void OnMotionPlayeEnd(); + + UFUNCTION(BlueprintCallable) + bool OnTap(const FVector2D& InPos); + + /** + * Motion control + * + * @param InName Group name of motion + * @param InNo Group number of motion + * @param InPriority Priori of motion to play + */ + UFUNCTION(BlueprintCallable) + void PlayMotion(const FString& InName, const int32 InNo, const EMotionPriority InPriority = EMotionPriority::EMP_Normal); +}; + +``` + +`Source/UnrealLive2D/Public/CubismUeTypes.h`: + +```h +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "CubismUeTypes.generated.h" + +UENUM(BlueprintType) +enum class EMotionPriority : uint8 +{ + EMP_None = 0 UMETA(DisplayName = "None"), + EMP_Idle = 1 UMETA(DisplayName = "Idle"), + EMP_Normal = 2 UMETA(DisplayName = "Normal"), + EMP_Force = 3 UMETA(DisplayName = "Force") +}; +``` + +`Source/UnrealLive2D/Public/UnrealLive2D.h`: + +```h +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Modules/ModuleManager.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogCubism, Log, All); + + +class FUnrealLive2DModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +private: + /** Handle to the test dll we will load */ + void* ExampleLibraryHandle; +}; + +``` + +`Source/UnrealLive2D/UnrealLive2D.Build.cs`: + +```cs +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System; +using System.IO; +using System.Collections.Generic; + + +public class UnrealLive2D : ModuleRules +{ + public UnrealLive2D(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + "ThirdParty/CubismLibrary/include", + "UnrealLive2D/Private/FrameWork/src" + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "Engine", + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + // ... add private dependencies that you statically link with here ... + "CoreUObject", + "Projects", + "RenderCore", + "SlateCore", + "RHI", + "Projects", + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + + bUseUnity = false; + PrivateDefinitions.Add("CSM_CORE_WIN32_DLL=0"); + PrivateDefinitions.Add("_DEBUG=UE_BUILD_DEVELOPMENT"); + + var ThirdPartyPath = Path.GetFullPath(Path.Combine(ModuleDirectory, "../ThirdParty/")); + var CubismLibPath = ThirdPartyPath + "CubismLibrary/"; + + if (Target.Platform == UnrealTargetPlatform.Win64) + { + CubismLibPath += "lib/windows/x86_64/142"; + PublicAdditionalLibraries.Add(String.Format(CubismLibPath + "/Live2DCubismCore_MTd.lib")); + } + else if (Target.Platform == UnrealTargetPlatform.Linux) + { + CubismLibPath += "dll/linux/x86_64"; + PublicAdditionalLibraries.Add(String.Format(CubismLibPath + "/libLive2DCubismCore.so")); + RuntimeDependencies.Add("$(TargetOutputDir)/libLive2DCubismCore.so", + String.Format(CubismLibPath + "/libLive2DCubismCore.so")); + } + } +} + +``` + +`UnrealLive2D.uplugin`: + +```uplugin +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0.0", + "FriendlyName": "UnrealLive2D", + "Description": "", + "Category": "Other", + "CreatedBy": "Naru", + "CreatedByURL": "", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": true, + "IsBetaVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "UnrealLive2D", + "Type": "Runtime", + "LoadingPhase": "PostConfigInit" + } + ] +} +``` \ No newline at end of file diff --git a/archive/JanKXSKI/AssetTutorialPlugin.txt b/archive/JanKXSKI/AssetTutorialPlugin.txt new file mode 100644 index 00000000..8a104757 --- /dev/null +++ b/archive/JanKXSKI/AssetTutorialPlugin.txt @@ -0,0 +1,718 @@ +Project Path: arc_JanKXSKI_AssetTutorialPlugin_m0zswh3g + +Source Tree: + +```txt +arc_JanKXSKI_AssetTutorialPlugin_m0zswh3g +├── AssetTutorialPlugin.uplugin +├── LICENSE +├── README.md +├── Resources +│ └── Icon128.png +└── Source + ├── AssetTutorialPlugin + │ ├── AssetTutorialPlugin.Build.cs + │ ├── Private + │ │ ├── AssetTutorialPlugin.cpp + │ │ └── NormalDistribution.cpp + │ └── Public + │ ├── AssetTutorialPlugin.h + │ └── NormalDistribution.h + └── AssetTutorialPluginEditor + ├── AssetTutorialPluginEditor.Build.cs + ├── Private + │ ├── AssetTutorialPluginEditor.cpp + │ ├── NormalDistributionActions.cpp + │ ├── NormalDistributionEditorToolkit.cpp + │ ├── NormalDistributionFactory.cpp + │ └── SNormalDistributionWidget.cpp + └── Public + ├── AssetTutorialPluginEditor.h + ├── NormalDistributionActions.h + ├── NormalDistributionEditorToolkit.h + ├── NormalDistributionFactory.h + └── SNormalDistributionWidget.h + +``` + +`AssetTutorialPlugin.uplugin`: + +```uplugin +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "AssetTutorialPlugin", + "Description": "", + "Category": "Other", + "CreatedBy": "", + "CreatedByURL": "", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": true, + "IsBetaVersion": false, + "IsExperimentalVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "AssetTutorialPlugin", + "Type": "Runtime", + "LoadingPhase": "Default" + }, + { + "Name": "AssetTutorialPluginEditor", + "Type": "Editor", + "LoadingPhase": "Default" + } + ] +} +``` + +`LICENSE`: + +``` +MIT License + +Copyright (c) 2022 JanKXSKI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +`README.md`: + +```md +This is the source code for the "Creating A Custom Asset Type With Its Own Editor In C++" tutorial over at https://dev.epicgames.com/community/learning/tutorials/vyKB/unreal-engine-creating-a-custom-asset-type-with-its-own-editor-in-c. +``` + +`Source/AssetTutorialPlugin/AssetTutorialPlugin.Build.cs`: + +```cs +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class AssetTutorialPlugin : ModuleRules +{ + public AssetTutorialPlugin(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} + +``` + +`Source/AssetTutorialPlugin/Private/AssetTutorialPlugin.cpp`: + +```cpp +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "AssetTutorialPlugin.h" + +#define LOCTEXT_NAMESPACE "FAssetTutorialPluginModule" + +void FAssetTutorialPluginModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +} + +void FAssetTutorialPluginModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FAssetTutorialPluginModule, AssetTutorialPlugin) +``` + +`Source/AssetTutorialPlugin/Private/NormalDistribution.cpp`: + +```cpp +#include "NormalDistribution.h" + +UNormalDistribution::UNormalDistribution() + : Mean(0.5f) + , StandardDeviation(0.2f) +{} + +float UNormalDistribution::DrawSample() +{ + return std::normal_distribution<>(Mean, StandardDeviation)(RandomNumberGenerator); +} + +void UNormalDistribution::LogSample() +{ + UE_LOG(LogTemp, Log, TEXT("%f"), DrawSample()) +} + +``` + +`Source/AssetTutorialPlugin/Public/AssetTutorialPlugin.h`: + +```h +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class FAssetTutorialPluginModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; + +``` + +`Source/AssetTutorialPlugin/Public/NormalDistribution.h`: + +```h +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include +#include "NormalDistribution.generated.h" + +UCLASS(BlueprintType) +class ASSETTUTORIALPLUGIN_API UNormalDistribution : public UObject +{ + GENERATED_BODY() +public: + UNormalDistribution(); + + UPROPERTY(EditAnywhere) + float Mean; + + UPROPERTY(EditAnywhere) + float StandardDeviation; + + UFUNCTION(BlueprintCallable) + float DrawSample(); + + UFUNCTION(CallInEditor) + void LogSample(); +private: + std::mt19937 RandomNumberGenerator; +}; + +``` + +`Source/AssetTutorialPluginEditor/AssetTutorialPluginEditor.Build.cs`: + +```cs +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class AssetTutorialPluginEditor : ModuleRules +{ + public AssetTutorialPluginEditor(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "AssetTutorialPlugin", + "UnrealEd" + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} + +``` + +`Source/AssetTutorialPluginEditor/Private/AssetTutorialPluginEditor.cpp`: + +```cpp +#include "AssetTutorialPluginEditor.h" + +void FAssetTutorialPluginEditorModule::StartupModule() +{ + NormalDistributionAssetTypeActions = MakeShared(); + FAssetToolsModule::GetModule().Get().RegisterAssetTypeActions(NormalDistributionAssetTypeActions.ToSharedRef()); +} + +void FAssetTutorialPluginEditorModule::ShutdownModule() +{ + if (!FModuleManager::Get().IsModuleLoaded("AssetTools")) return; + FAssetToolsModule::GetModule().Get().UnregisterAssetTypeActions(NormalDistributionAssetTypeActions.ToSharedRef()); +} + +IMPLEMENT_MODULE(FAssetTutorialPluginEditorModule, AssetTutorialPluginEditor) + +``` + +`Source/AssetTutorialPluginEditor/Private/NormalDistributionActions.cpp`: + +```cpp +#include "NormalDistributionActions.h" +#include "NormalDistribution.h" +#include "NormalDistributionEditorToolkit.h" + +UClass* FNormalDistributionAssetTypeActions::GetSupportedClass() const +{ + return UNormalDistribution::StaticClass(); +} + +FText FNormalDistributionAssetTypeActions::GetName() const +{ + return INVTEXT("Normal Distribution"); +} + +FColor FNormalDistributionAssetTypeActions::GetTypeColor() const +{ + return FColor::Cyan; +} + +uint32 FNormalDistributionAssetTypeActions::GetCategories() +{ + return EAssetTypeCategories::Misc; +} + +void FNormalDistributionAssetTypeActions::OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor) +{ + MakeShared()->InitEditor(InObjects); +} + +``` + +`Source/AssetTutorialPluginEditor/Private/NormalDistributionEditorToolkit.cpp`: + +```cpp +#include "NormalDistributionEditorToolkit.h" +#include "Widgets/Docking/SDockTab.h" +#include "SNormalDistributionWidget.h" +#include "Modules/ModuleManager.h" + +void FNormalDistributionEditorToolkit::InitEditor(const TArray& InObjects) +{ + NormalDistribution = Cast(InObjects[0]); + + const TSharedRef Layout = FTabManager::NewLayout("NormalDistributionEditorLayout") + ->AddArea + ( + FTabManager::NewPrimaryArea()->SetOrientation(Orient_Vertical) + ->Split + ( + FTabManager::NewSplitter() + ->SetSizeCoefficient(0.6f) + ->SetOrientation(Orient_Horizontal) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.8f) + ->AddTab("NormalDistributionPDFTab", ETabState::OpenedTab) + ) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.2f) + ->AddTab("NormalDistributionDetailsTab", ETabState::OpenedTab) + ) + ) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.4f) + ->AddTab("OutputLog", ETabState::OpenedTab) + ) + ); + FAssetEditorToolkit::InitAssetEditor(EToolkitMode::Standalone, {}, "NormalDistributionEditor", Layout, true, true, InObjects); +} + +void FNormalDistributionEditorToolkit::RegisterTabSpawners(const TSharedRef& InTabManager) +{ + FAssetEditorToolkit::RegisterTabSpawners(InTabManager); + + WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(INVTEXT("Normal Distribution Editor")); + + InTabManager->RegisterTabSpawner("NormalDistributionPDFTab", FOnSpawnTab::CreateLambda([=](const FSpawnTabArgs&) + { + return SNew(SDockTab) + [ + SNew(SNormalDistributionWidget) + .Mean(this, &FNormalDistributionEditorToolkit::GetMean) + .StandardDeviation(this, &FNormalDistributionEditorToolkit::GetStandardDeviation) + .OnMeanChanged(this, &FNormalDistributionEditorToolkit::SetMean) + .OnStandardDeviationChanged(this, &FNormalDistributionEditorToolkit::SetStandardDeviation) + ]; + })) + .SetDisplayName(INVTEXT("PDF")) + .SetGroup(WorkspaceMenuCategory.ToSharedRef()); + + FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked("PropertyEditor"); + FDetailsViewArgs DetailsViewArgs; + DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; + TSharedRef DetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs); + DetailsView->SetObjects(TArray{ NormalDistribution }); + InTabManager->RegisterTabSpawner("NormalDistributionDetailsTab", FOnSpawnTab::CreateLambda([=](const FSpawnTabArgs&) + { + return SNew(SDockTab) + [ + DetailsView + ]; + })) + .SetDisplayName(INVTEXT("Details")) + .SetGroup(WorkspaceMenuCategory.ToSharedRef()); +} + +void FNormalDistributionEditorToolkit::UnregisterTabSpawners(const TSharedRef& InTabManager) +{ + FAssetEditorToolkit::UnregisterTabSpawners(InTabManager); + InTabManager->UnregisterTabSpawner("NormalDistributionPDFTab"); + InTabManager->UnregisterTabSpawner("NormalDistributionDetailsTab"); +} + +float FNormalDistributionEditorToolkit::GetMean() const +{ + return NormalDistribution->Mean; +} + +float FNormalDistributionEditorToolkit::GetStandardDeviation() const +{ + return NormalDistribution->StandardDeviation; +} + +void FNormalDistributionEditorToolkit::SetMean(float Mean) +{ + NormalDistribution->Modify(); + NormalDistribution->Mean = Mean; +} + +void FNormalDistributionEditorToolkit::SetStandardDeviation(float StandardDeviation) +{ + NormalDistribution->Modify(); + NormalDistribution->StandardDeviation = StandardDeviation; +} + +``` + +`Source/AssetTutorialPluginEditor/Private/NormalDistributionFactory.cpp`: + +```cpp +#include "NormalDistributionFactory.h" +#include "NormalDistribution.h" + +UNormalDistributionFactory::UNormalDistributionFactory() +{ + SupportedClass = UNormalDistribution::StaticClass(); + bCreateNew = true; +} + +UObject* UNormalDistributionFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + return NewObject(InParent, Class, Name, Flags, Context); +} + +``` + +`Source/AssetTutorialPluginEditor/Private/SNormalDistributionWidget.cpp`: + +```cpp +#include "SNormalDistributionWidget.h" +#include "Editor.h" + +void SNormalDistributionWidget::Construct(const FArguments& InArgs) +{ + Mean = InArgs._Mean; + StandardDeviation = InArgs._StandardDeviation; + OnMeanChanged = InArgs._OnMeanChanged; + OnStandardDeviationChanged = InArgs._OnStandardDeviationChanged; +} + +int32 SNormalDistributionWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const +{ + const int32 NumPoints = 512; + TArray Points; + Points.Reserve(NumPoints); + const FTransform2D PointsTransform = GetPointsTransform(AllottedGeometry); + for (int32 PointIndex = 0; PointIndex < NumPoints; ++PointIndex) + { + const float X = PointIndex / (NumPoints - 1.0); + const float D = (X - Mean.Get()) / StandardDeviation.Get(); + const float Y = FMath::Exp(-0.5f * D * D); + Points.Add(PointsTransform.TransformPoint(FVector2D(X, Y))); + } + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Points); + return LayerId; +} + +FVector2D SNormalDistributionWidget::ComputeDesiredSize(float) const +{ + return FVector2D(200.0, 200.0); +} + +FReply SNormalDistributionWidget::OnMouseButtonDown(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent) +{ + if (GEditor && GEditor->CanTransact() && ensure(!GIsTransacting)) + GEditor->BeginTransaction(TEXT(""), INVTEXT("Edit Normal Distribution"), nullptr); + return FReply::Handled().CaptureMouse(SharedThis(this)); +} + +FReply SNormalDistributionWidget::OnMouseButtonUp(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent) +{ + if (GEditor) GEditor->EndTransaction(); + return FReply::Handled().ReleaseMouseCapture(); +} + +FReply SNormalDistributionWidget::OnMouseMove(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent) +{ + if (!HasMouseCapture()) return FReply::Unhandled(); + const FTransform2D PointsTransform = GetPointsTransform(AllottedGeometry); + const FVector2D LocalPosition = AllottedGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()); + const FVector2D NormalizedPosition = PointsTransform.Inverse().TransformPoint(LocalPosition); + if (OnMeanChanged.IsBound()) + OnMeanChanged.Execute(NormalizedPosition.X); + if (OnStandardDeviationChanged.IsBound()) + OnStandardDeviationChanged.Execute(FMath::Max(0.025f, FMath::Lerp(0.025f, 0.25f, NormalizedPosition.Y))); + return FReply::Handled(); +} + +FTransform2D SNormalDistributionWidget::GetPointsTransform(const FGeometry& AllottedGeometry) const +{ + const double Margin = 0.05 * AllottedGeometry.GetLocalSize().GetMin(); + const FScale2D Scale((AllottedGeometry.GetLocalSize() - 2.0 * Margin) * FVector2D(1.0, -1.0)); + const FVector2D Translation(Margin, AllottedGeometry.GetLocalSize().Y - Margin); + return FTransform2D(Scale, Translation); +} + +``` + +`Source/AssetTutorialPluginEditor/Public/AssetTutorialPluginEditor.h`: + +```h +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" +#include "NormalDistributionActions.h" + +class FAssetTutorialPluginEditorModule : public IModuleInterface +{ +public: + void StartupModule() override; + void ShutdownModule() override; +private: + TSharedPtr NormalDistributionAssetTypeActions; +}; + +``` + +`Source/AssetTutorialPluginEditor/Public/NormalDistributionActions.h`: + +```h +#pragma once + +#include "CoreMinimal.h" +#include "AssetTypeActions_Base.h" + +class FNormalDistributionAssetTypeActions : public FAssetTypeActions_Base +{ +public: + UClass* GetSupportedClass() const override; + FText GetName() const override; + FColor GetTypeColor() const override; + uint32 GetCategories() override; + void OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor) override; +}; + +``` + +`Source/AssetTutorialPluginEditor/Public/NormalDistributionEditorToolkit.h`: + +```h +#pragma once + +#include "CoreMinimal.h" +#include "NormalDistribution.h" +#include "Toolkits/AssetEditorToolkit.h" + +class FNormalDistributionEditorToolkit : public FAssetEditorToolkit +{ +public: + void InitEditor(const TArray& InObjects); + + void RegisterTabSpawners(const TSharedRef& TabManager) override; + void UnregisterTabSpawners(const TSharedRef& TabManager) override; + + FName GetToolkitFName() const override { return "NormalDistributionEditor"; } + FText GetBaseToolkitName() const override { return INVTEXT("Normal Distribution Editor"); } + FString GetWorldCentricTabPrefix() const override { return "Normal Distribution "; } + FLinearColor GetWorldCentricTabColorScale() const override { return {}; } + + float GetMean() const; + float GetStandardDeviation() const; + void SetMean(float Mean); + void SetStandardDeviation(float StandardDeviation); +private: + UNormalDistribution* NormalDistribution; +}; + +``` + +`Source/AssetTutorialPluginEditor/Public/NormalDistributionFactory.h`: + +```h +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "NormalDistributionFactory.generated.h" + +UCLASS() +class UNormalDistributionFactory : public UFactory +{ + GENERATED_BODY() +public: + UNormalDistributionFactory(); + UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn); +}; + +``` + +`Source/AssetTutorialPluginEditor/Public/SNormalDistributionWidget.h`: + +```h +#pragma once + +#include "CoreMinimal.h" +#include "Widgets/SLeafWidget.h" + +DECLARE_DELEGATE_OneParam(FOnMeanChanged, float /*NewMean*/) +DECLARE_DELEGATE_OneParam(FOnStandardDeviationChanged, float /*NewStandardDeviation*/) + +class SNormalDistributionWidget : public SLeafWidget +{ +public: + SLATE_BEGIN_ARGS(SNormalDistributionWidget) + : _Mean(0.5f) + , _StandardDeviation(0.2f) + {} + SLATE_ATTRIBUTE(float, Mean) + SLATE_ATTRIBUTE(float, StandardDeviation) + SLATE_EVENT(FOnMeanChanged, OnMeanChanged) + SLATE_EVENT(FOnStandardDeviationChanged, OnStandardDeviationChanged) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + + int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; + FVector2D ComputeDesiredSize(float) const override; + + FReply OnMouseButtonDown(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent) override; + FReply OnMouseButtonUp(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent) override; + FReply OnMouseMove(const FGeometry& AllottedGeometry, const FPointerEvent& MouseEvent) override; +private: + TAttribute Mean; + TAttribute StandardDeviation; + + FOnMeanChanged OnMeanChanged; + FOnStandardDeviationChanged OnStandardDeviationChanged; + + FTransform2D GetPointsTransform(const FGeometry& AllottedGeometry) const; +}; + +``` \ No newline at end of file diff --git a/archive/TimmHess/UnrealImageCapture.txt b/archive/TimmHess/UnrealImageCapture.txt new file mode 100644 index 00000000..2cc9800b --- /dev/null +++ b/archive/TimmHess/UnrealImageCapture.txt @@ -0,0 +1,1482 @@ +Project Path: arc_TimmHess_UnrealImageCapture_ibyzjn8h + +Source Tree: + +```txt +arc_TimmHess_UnrealImageCapture_ibyzjn8h +├── CaptureToDisk +│ ├── CaptureToDisk.uproject +│ ├── Config +│ │ ├── DefaultEditor.ini +│ │ ├── DefaultEngine.ini +│ │ ├── DefaultGame.ini +│ │ └── DefaultInput.ini +│ ├── Content +│ │ ├── NewWorld.umap +│ │ └── PP_Materials +│ │ ├── PP_Depth.uasset +│ │ ├── PP_PixelAnnotation.uasset +│ │ └── PP_Segmentation.uasset +│ └── Source +│ ├── CaptureToDisk +│ │ ├── CaptureToDisk.Build.cs +│ │ ├── CaptureToDisk.cpp +│ │ ├── CaptureToDisk.h +│ │ ├── Private +│ │ │ └── FrameCaptureManager.cpp +│ │ └── Public +│ │ └── FrameCaptureManager.h +│ ├── CaptureToDisk.Target.cs +│ └── CaptureToDiskEditor.Target.cs +├── LICENSE +├── README.md +└── gfx + ├── Activate_Lumen.png + ├── Apply_custom_depth_stencil.png + ├── CaptureResult_color.jpeg + ├── CaptureResult_segmentation.png + ├── Debug_level_blueprint_capture.png + ├── Enable_custom_depth_in_project.png + ├── FrameCaptureManage_settings.png + ├── pp_depth.png + └── pp_segmentation.png + +``` + +`CaptureToDisk/CaptureToDisk.uproject`: + +```uproject +{ + "FileVersion": 3, + "EngineAssociation": "5.5", + "Category": "", + "Description": "", + "Modules": [ + { + "Name": "CaptureToDisk", + "Type": "Runtime", + "LoadingPhase": "Default", + "AdditionalDependencies": [ + "Engine" + ] + } + ], + "Plugins": [ + { + "Name": "ModelingToolsEditorMode", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + } + ] +} +``` + +`CaptureToDisk/Config/DefaultEngine.ini`: + +```ini + + +[/Script/EngineSettings.GameMapsSettings] +GameDefaultMap=None +EditorStartupMap=/Game/NewWorld.NewWorld + +[/Script/Engine.RendererSettings] +r.AllowStaticLighting=False + +r.GenerateMeshDistanceFields=True + +r.DynamicGlobalIlluminationMethod=1 + +r.ReflectionMethod=1 + +r.SkinCache.CompileShaders=True + +r.RayTracing=True + +r.Shadow.Virtual.Enable=1 + +r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=True + +r.DefaultFeature.LocalExposure.HighlightContrastScale=0.8 + +r.DefaultFeature.LocalExposure.ShadowContrastScale=0.8 +r.CustomDepth=3 + +[/Script/WindowsTargetPlatform.WindowsTargetSettings] +DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 +DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 +-D3D12TargetedShaderFormats=PCD3D_SM5 ++D3D12TargetedShaderFormats=PCD3D_SM6 +-D3D11TargetedShaderFormats=PCD3D_SM5 ++D3D11TargetedShaderFormats=PCD3D_SM5 +Compiler=Default +AudioSampleRate=48000 +AudioCallbackBufferFrameSize=1024 +AudioNumBuffersToEnqueue=1 +AudioMaxChannels=0 +AudioNumSourceWorkers=4 +SpatializationPlugin= +SourceDataOverridePlugin= +ReverbPlugin= +OcclusionPlugin= +CompressionOverrides=(bOverrideCompressionTimes=False,DurationThreshold=5.000000,MaxNumRandomBranches=0,SoundCueQualityIndex=0) +CacheSizeKB=65536 +MaxChunkSizeOverrideKB=0 +bResampleForDevice=False +MaxSampleRate=48000.000000 +HighSampleRate=32000.000000 +MedSampleRate=24000.000000 +LowSampleRate=12000.000000 +MinSampleRate=8000.000000 +CompressionQualityModifier=1.000000 +AutoStreamingThreshold=0.000000 +SoundCueCookQualityIndex=-1 + +[/Script/LinuxTargetPlatform.LinuxTargetSettings] +-TargetedRHIs=SF_VULKAN_SM5 ++TargetedRHIs=SF_VULKAN_SM6 + +[/Script/HardwareTargeting.HardwareTargetingSettings] +TargetedHardwareClass=Desktop +AppliedTargetedHardwareClass=Desktop +DefaultGraphicsPerformance=Maximum +AppliedDefaultGraphicsPerformance=Maximum + +[/Script/WorldPartitionEditor.WorldPartitionEditorSettings] +CommandletClass=Class'/Script/UnrealEd.WorldPartitionConvertCommandlet' + +[/Script/Engine.UserInterfaceSettings] +bAuthorizeAutomaticWidgetVariableCreation=False +FontDPIPreset=Standard +FontDPI=72 + +[/Script/Engine.Engine] ++ActiveGameNameRedirects=(OldGameName="TP_Blank",NewGameName="/Script/CaptureToDisk") ++ActiveGameNameRedirects=(OldGameName="/Script/TP_Blank",NewGameName="/Script/CaptureToDisk") + +[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] +bEnablePlugin=True +bAllowNetworkConnection=True +SecurityToken=C073144A4B99B66C517BCE84ED67D96E +bIncludeInShipping=False +bAllowExternalStartInShipping=False +bCompileAFSProject=False +bUseCompression=False +bLogFiles=False +bReportStats=False +ConnectionType=USBOnly +bUseManualIPAddress=False +ManualIPAddress= + + +``` + +`CaptureToDisk/Config/DefaultGame.ini`: + +```ini + +[/Script/EngineSettings.GeneralProjectSettings] +ProjectID=367F1D8A41791D7E0556B6BBFEE3C31B + +``` + +`CaptureToDisk/Config/DefaultInput.ini`: + +```ini +[/Script/Engine.InputSettings] +-AxisConfig=(AxisKeyName="Gamepad_LeftX",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="Gamepad_LeftY",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="Gamepad_RightX",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="Gamepad_RightY",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="MouseX",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) +-AxisConfig=(AxisKeyName="MouseY",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) +-AxisConfig=(AxisKeyName="Mouse2D",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) ++AxisConfig=(AxisKeyName="Gamepad_LeftX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseX",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseY",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Mouse2D",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseWheelAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftTriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightTriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_Special_Left_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_Special_Left_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Grip_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Grip_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +bAltEnterTogglesFullscreen=True +bF11TogglesFullscreen=True +bUseMouseForTouch=False +bEnableMouseSmoothing=True +bEnableFOVScaling=True +bCaptureMouseOnLaunch=True +bEnableLegacyInputScales=True +bEnableMotionControls=True +bFilterInputByPlatformUser=False +bShouldFlushPressedKeysOnViewportFocusLost=True +bAlwaysShowTouchInterface=False +bShowConsoleOnFourFingerTap=True +bEnableGestureRecognizer=False +bUseAutocorrect=False +DefaultViewportMouseCaptureMode=CapturePermanently_IncludingInitialMouseDown +DefaultViewportMouseLockMode=LockOnCapture +FOVScale=0.011110 +DoubleClickTime=0.200000 +DefaultPlayerInputClass=/Script/EnhancedInput.EnhancedPlayerInput +DefaultInputComponentClass=/Script/EnhancedInput.EnhancedInputComponent +DefaultTouchInterface=/Engine/MobileResources/HUD/DefaultVirtualJoysticks.DefaultVirtualJoysticks +-ConsoleKeys=Tilde ++ConsoleKeys=Tilde + + +``` + +`CaptureToDisk/Source/CaptureToDisk.Target.cs`: + +```cs +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class CaptureToDiskTarget : TargetRules +{ + public CaptureToDiskTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Game; + DefaultBuildSettings = BuildSettingsVersion.V5; + IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_5; + ExtraModuleNames.Add("CaptureToDisk"); + } +} + +``` + +`CaptureToDisk/Source/CaptureToDisk/CaptureToDisk.Build.cs`: + +```cs +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class CaptureToDisk : ModuleRules +{ + public CaptureToDisk(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput" , "ImageWrapper", "RenderCore", "Renderer", "RHI"}); + + PrivateDependencyModuleNames.AddRange(new string[] { }); + + // Uncomment if you are using Slate UI + // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); + + // Uncomment if you are using online features + // PrivateDependencyModuleNames.Add("OnlineSubsystem"); + + // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true + } +} + +``` + +`CaptureToDisk/Source/CaptureToDisk/CaptureToDisk.cpp`: + +```cpp +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CaptureToDisk.h" +#include "Modules/ModuleManager.h" + +IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, CaptureToDisk, "CaptureToDisk" ); + +``` + +`CaptureToDisk/Source/CaptureToDisk/CaptureToDisk.h`: + +```h +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" + + +``` + +`CaptureToDisk/Source/CaptureToDisk/Private/FrameCaptureManager.cpp`: + +```cpp +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "FrameCaptureManager.h" + +#include "Engine/SceneCapture2D.h" +#include "Components/SceneCaptureComponent2D.h" +#include "ShowFlags.h" + +#include "Engine/TextureRenderTarget2D.h" + +#include "Materials/Material.h" + +#include "RHICommandList.h" + +#include "IImageWrapper.h" +#include "IImageWrapperModule.h" + +#include "ImageUtils.h" + +#include "Modules/ModuleManager.h" + +#include "Misc/FileHelper.h" + +// Sets default values +AFrameCaptureManager::AFrameCaptureManager() +{ + // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. + PrimaryActorTick.bCanEverTick = true; + +} + +// Called when the game starts or when spawned +void AFrameCaptureManager::BeginPlay() +{ + Super::BeginPlay(); + + if(CaptureComponent){ // nullptr check + SetupCaptureComponent(); + } else{ + UE_LOG(LogTemp, Error, TEXT("No CaptureComponent set!")); + } +} + +// Called every frame +void AFrameCaptureManager::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + if(!RenderRequestQueue.IsEmpty()){ + // Peek the next RenderRequest from queue + TSharedPtr nextRenderRequest = *RenderRequestQueue.Peek(); + + if(nextRenderRequest){ //nullptr check + if(nextRenderRequest->RenderFence.IsFenceComplete() && nextRenderRequest->Readback.IsReady()){ + // Load the image wrapper module + //IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked(FName("ImageWrapper")); + + // Get Data from Readback + nextRenderRequest->RawSize = nextRenderRequest->ImageSize.X * nextRenderRequest->ImageSize.Y * sizeof(FColor); + if(ImageFormat == ECustomImageFormat::EXR){ // handle float case + nextRenderRequest->RawSize = nextRenderRequest->ImageSize.X * nextRenderRequest->ImageSize.Y * sizeof(FFloat16Color); + } + + int32 RowPitchInPixels; + nextRenderRequest->RawData = nextRenderRequest->Readback.Lock(RowPitchInPixels, nullptr); // Pass RowPitchInPixels and no buffer size + + // Generate image name + FString fileName = ""; + fileName = FPaths::ProjectSavedDir() + SubDirectoryName + "/img" + "_" + ToStringWithLeadingZeros(ImgCounter, NumDigits); + fileName += GetFileEnding(ImageFormat); + + // Pass the raw data, filename, width, height, and format to the async task + RunAsyncImageSaveTask(nextRenderRequest, fileName, FrameWidth, FrameHeight, GetRGBFormatFromImageFormat(ImageFormat), ConvertImageFormat(ImageFormat)); + + // Increase ImgCounter for file names + ImgCounter += 1; + + // Release RenderRequest form the queue + RenderRequestQueue.Pop(); // Delete the first element from RenderQueue + // Put it into the queue of outsourced threads + InThreadRenderRequestQueue.Enqueue(nextRenderRequest); //Push to InThreadRenderRequestQueue + } + } + } + if(!InThreadRenderRequestQueue.IsEmpty()){ + UE_LOG(LogTemp, Log, TEXT("InThreadRenderRequestQueue not empty.")); + + // Get next element of the InThreadRenderRequestQueue + TSharedPtr nextRenderRequest = *InThreadRenderRequestQueue.Peek(); + + if(nextRenderRequest){ //nullptr check + if(nextRenderRequest->bIsComplete){ //check if complete + InThreadRenderRequestQueue.Pop(); // Remove from queue + } + } + } +} + +void AFrameCaptureManager::SetupCaptureComponent(){ + if(!IsValid(CaptureComponent)){ + UE_LOG(LogTemp, Error, TEXT("SetupCaptureComponent: CaptureComponent is not valid!")); + return; + } + + // Create RenderTargets + UTextureRenderTarget2D* renderTarget2D = NewObject(); + renderTarget2D->InitAutoFormat(256, 256); // some random format, got crashing otherwise + + // Float Capture + if(ImageFormat == ECustomImageFormat::EXR){ // handle float case + renderTarget2D->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA32f; + renderTarget2D->InitCustomFormat(FrameWidth, FrameHeight, PF_FloatRGBA, true); // PF_B8G8R8A8 disables HDR which will boost storing to disk due to less image information + } + // Color Capture + else{ + renderTarget2D->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA8_SRGB; //ETextureRenderTargetFormat::RTF_RGBA8; //8-bit color format + renderTarget2D->InitCustomFormat(FrameWidth, FrameHeight, PF_R8G8B8A8, true); // PF_R8G8B8A8 //PF_B8G8R8A8 // PF... disables HDR, which is most important since HDR gives gigantic overhead, and is not needed! + renderTarget2D->bForceLinearGamma = true; // Important for viewport-like color reproduction. + } + renderTarget2D->bGPUSharedFlag = true; // demand buffer on GPU + + // Assign RenderTarget + CaptureComponent->GetCaptureComponent2D()->TextureTarget = renderTarget2D; + // Set Camera Properties + CaptureComponent->GetCaptureComponent2D()->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR; + CaptureComponent->GetCaptureComponent2D()->TextureTarget->TargetGamma = GEngine->GetDisplayGamma(); + CaptureComponent->GetCaptureComponent2D()->ShowFlags.SetTemporalAA(true); + // lookup more showflags in documentation + + // Assign PostProcess Material if assigned + if(PostProcessMaterial){ // check nullptr + CaptureComponent->GetCaptureComponent2D()->AddOrUpdateBlendable(PostProcessMaterial); + } else { + UE_LOG(LogTemp, Log, TEXT("No PostProcessMaterial is assigend")); + } + UE_LOG(LogTemp, Warning, TEXT("Initialized RenderTarget!")); +} + + + +void AFrameCaptureManager::CaptureNonBlocking(){ + if(!IsValid(CaptureComponent)){ + UE_LOG(LogTemp, Error, TEXT("CaptureColorNonBlocking: CaptureComponent was not valid!")); + return; + } + CaptureComponent->GetCaptureComponent2D()->TextureTarget->TargetGamma = GEngine->GetDisplayGamma(); // 1.2f; + + // Get RenderConterxt + FTextureRenderTargetResource* renderTargetResource = CaptureComponent->GetCaptureComponent2D()->TextureTarget->GameThread_GetRenderTargetResource(); + + TSharedPtr renderRequest = + MakeShared( + renderTargetResource->GetSizeXY(), + FRHIGPUTextureReadback(TEXT("CameraCaptureManagerReadback") + ) + ); + + ENQUEUE_RENDER_COMMAND(SceneDrawCompletion)( + [renderRequest, renderTargetResource](FRHICommandListImmediate& RHICmdList) { + FTextureRHIRef Target = renderTargetResource->GetRenderTargetTexture(); + renderRequest->Readback.EnqueueCopy(RHICmdList, Target); + }); + + // Notifiy new task in RenderQueue + RenderRequestQueue.Enqueue(renderRequest); + + // Set RenderCommandFence + renderRequest->RenderFence.BeginFence(); +} + + + +FString AFrameCaptureManager::ToStringWithLeadingZeros(int32 Integer, int32 MaxDigits){ + FString result = FString::FromInt(Integer); + int32 stringSize = result.Len(); + int32 stringDelta = MaxDigits - stringSize; + if(stringDelta < 0){ + UE_LOG(LogTemp, Error, TEXT("MaxDigits of ImageCounter Overflow!")); + return result; + } + //FIXME: Smarter function for this.. + FString leadingZeros = ""; + for(size_t i=0;i RenderRequest, + FString ImageName, + int32 Width, + int32 Height, + ERGBFormat ActualRGBFormat, + EImageFormat ActualImageFormat) + { + UE_LOG(LogTemp, Warning, TEXT("Running Async Task")); + (new FAutoDeleteAsyncTask(RenderRequest, ImageName, Width, Height, ActualRGBFormat, ActualImageFormat))->StartBackgroundTask(); +} + + + +/* +******************************************************************* +*/ + +AsyncSaveImageToDiskTask::AsyncSaveImageToDiskTask( + TSharedPtr InRenderRequest, + FString ImageName, + int32 Width, + int32 Height, + ERGBFormat RGBFormat, + EImageFormat ImageFormat + ): + RenderRequest(InRenderRequest), + FileName(ImageName), + Width(Width), + Height(Height), + RGBFormat(RGBFormat), + ImageFormat(ImageFormat) +{ +} + +AsyncSaveImageToDiskTask::~AsyncSaveImageToDiskTask(){ + UE_LOG(LogTemp, Warning, TEXT("AsyncTaskDone")); +} + + +void AsyncSaveImageToDiskTask::DoWork(){ + UE_LOG(LogTemp, Warning, TEXT("Starting Work")); + + // Load the image wrapper module (if not already loaded) + IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked(FName("ImageWrapper")); + + // Create an image wrapper + TSharedPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(ImageFormat); + + // Error handling + if (!ImageWrapper.IsValid()) { + UE_LOG(LogTemp, Error, TEXT("Failed to create IImageWrapper!")); + return; + } + + // Set the raw data + int32 PixelDepth = 8; + if(ImageFormat == EImageFormat::EXR){ // Adjust pixel depth for EXR (float) data // Has to be EImageFormat because already converted.. + PixelDepth = 16; + } + UE_LOG(LogTemp, Warning, TEXT("PixelDepth: %d"), PixelDepth); + ImageWrapper->SetRaw(RenderRequest->RawData, RenderRequest->RawSize, Width, Height, RGBFormat, PixelDepth); + + // Compress the image + /** from /Engine/Source/Runtime/ImageWrapper/Public/IImageWrapper.h + * Enumerates available image compression qualities. + * + * JPEG interprets Quality as 1-100 + * JPEG default quality is 85 , Uncompressed means 100 + * + * for PNG: + * Negative qualities in [-1,-9] set PNG zlib level + * PNG interprets "Uncompressed" as zlib level 0 (none) + * otherwise default zlib level 3 is used. + * + * EXR respects the "Uncompressed" flag to turn off compression; otherwise ZIP_COMPRESSION is used. + */ + const TArray64& CompressedData = ImageWrapper->GetCompressed(100); + + // Save the compressed image to disk + FFileHelper::SaveArrayToFile(CompressedData, *FileName); + + //unlock the readback after the processing is done + RenderRequest->Readback.Unlock(); + + // Indicate that the processing is complete (using a flag) + RenderRequest->bIsComplete = true; + + //UE_LOG(LogTemp, Log, TEXT("Stored Image: %s"), *FileName); +} +``` + +`CaptureToDisk/Source/CaptureToDisk/Public/FrameCaptureManager.h`: + +```h +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +class ASceneCapture2D; +class UMaterial; + +#include "CoreMinimal.h" +#include "IImageWrapper.h" // Include the header that defines ERGBFormat +#include "GameFramework/Actor.h" +#include "FrameCaptureManager.generated.h" + + +UENUM(BlueprintType) +enum class ECustomImageFormat : uint8 +{ + //Invalid UMETA(DisplayName = "Invalid"), + PNG UMETA(DisplayName = "PNG"), + JPEG UMETA(DisplayName = "JPEG"), + //GrayscaleJPEG UMETA(DisplayName = "GrayscaleJPEG"), + //BMP UMETA(DisplayName = "BMP"), + //ICO UMETA(DisplayName = "ICO"), + EXR UMETA(DisplayName = "EXR"), + //ICNS UMETA(DisplayName = "ICNS"), + //TGA UMETA(DisplayName = "TGA"), + //HDR UMETA(DisplayName = "HDR"), + //TIFF UMETA(DisplayName = "TIFF"), + //DDS UMETA(DisplayName = "DDS"), + //UEJPEG UMETA(DisplayName = "UEJPEG"), + //GrayscaleUEJPEG UMETA(DisplayName = "GrayscaleUEJPEG") +}; + +struct FRenderRequestStruct{ + FIntPoint ImageSize; + FRHIGPUTextureReadback Readback; + FRenderCommandFence RenderFence; + + void* RawData = nullptr; + int64 RawSize = 0; + + bool bIsComplete = false; + + FRenderRequestStruct( + const FIntPoint& ImageSize, + const FRHIGPUTextureReadback& Readback) : + ImageSize(ImageSize), + Readback(Readback) {} +}; + + +UCLASS() +class CAPTURETODISK_API AFrameCaptureManager : public AActor +{ + GENERATED_BODY() + +public: + // Sets default values for this actor's properties + AFrameCaptureManager(); + + // Captured Data Sub-Directory Name + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture File") + FString SubDirectoryName = "/Color/"; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture File") + int NumDigits = 6; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture Format") + ECustomImageFormat ImageFormat = ECustomImageFormat::PNG; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture Format") + int FrameWidth = 640; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture Format") + int FrameHeight = 480; + + // Color Capture Components + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture Source") + ASceneCapture2D* CaptureComponent; + + // PostProcessMaterial used for segmentation + UPROPERTY(EditAnywhere, Category="Capture Source") + UMaterial* PostProcessMaterial = nullptr; + +protected: + // Called when the game starts or when spawned + virtual void BeginPlay() override; + + // RenderRequest Queue + TQueue> RenderRequestQueue; + TQueue> InThreadRenderRequestQueue; // holding the RenderRequests currently storing to disk + + // Counter for file names + int ImgCounter = 0; + + // Prepares SceneCapture2D component for capturing + void SetupCaptureComponent(); + + // Creates an async task that will save the captured image to disk + void RunAsyncImageSaveTask( + TSharedPtr RenderRequest, + FString ImageName, + int32 Width, + int32 Height, + ERGBFormat ActualRGBFormat, + EImageFormat ActualImageFormat + ); + + FString ToStringWithLeadingZeros(int32 Integer, int32 MaxDigits); + + FString GetFileEnding(ECustomImageFormat Format) + { + switch (Format) + { + case ECustomImageFormat::PNG: return ".png"; + case ECustomImageFormat::JPEG: return ".jpeg"; + case ECustomImageFormat::EXR: return ".exr"; + default: return ""; + } + }; + ERGBFormat GetRGBFormatFromImageFormat(ECustomImageFormat MyFormat) + { + { + switch (MyFormat) + { + case ECustomImageFormat::PNG: return ERGBFormat::RGBA; + case ECustomImageFormat::JPEG: return ERGBFormat::RGBA; + case ECustomImageFormat::EXR: return ERGBFormat::RGBAF; + default: return ERGBFormat::RGBA; + } + } + }; + EImageFormat ConvertImageFormat(ECustomImageFormat MyFormat) + { + switch (MyFormat) + { + case ECustomImageFormat::PNG: return EImageFormat::PNG; + case ECustomImageFormat::JPEG: return EImageFormat::JPEG; + //case ECustomImageFormat::GrayscaleJPEG: return EImageFormat::GrayscaleJPEG; + //case ECustomImageFormat::BMP: return EImageFormat::BMP; + //case ECustomImageFormat::ICO: return EImageFormat::ICO; + case ECustomImageFormat::EXR: return EImageFormat::EXR; + //case ECustomImageFormat::ICNS: return EImageFormat::ICNS; + //case ECustomImageFormat::TGA: return EImageFormat::TGA; + //case ECustomImageFormat::HDR: return EImageFormat::HDR; + //case ECustomImageFormat::TIFF: return EImageFormat::TIFF; + //case ECustomImageFormat::DDS: return EImageFormat::DDS; + //case ECustomImageFormat::UEJPEG: return EImageFormat::UEJPEG; + //case ECustomImageFormat::GrayscaleUEJPEG: return EImageFormat::GrayscaleUEJPEG; + default: return EImageFormat::PNG; + } + }; + +public: + // Called every frame + virtual void Tick(float DeltaTime) override; + + UFUNCTION(BlueprintCallable, Category = "ImageCapture") + void CaptureNonBlocking(); +}; + + +class AsyncSaveImageToDiskTask : public FNonAbandonableTask +{ +public: + AsyncSaveImageToDiskTask( + TSharedPtr InRenderRequest, + FString ImageName, + int32 Width, + int32 Height, + ERGBFormat RGBFormat, + EImageFormat ImageFormat); + ~AsyncSaveImageToDiskTask(); + + void DoWork(); + + FORCEINLINE TStatId GetStatId() const + { + RETURN_QUICK_DECLARE_CYCLE_STAT(AsyncSaveImageToDiskTask, STATGROUP_ThreadPoolAsyncTasks); + } + +private: + TSharedPtr RenderRequest; // Hold the shared pointer + FString FileName; + int32 Width; + int32 Height; + ERGBFormat RGBFormat; + EImageFormat ImageFormat; +}; + +``` + +`CaptureToDisk/Source/CaptureToDiskEditor.Target.cs`: + +```cs +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class CaptureToDiskEditorTarget : TargetRules +{ + public CaptureToDiskEditorTarget( TargetInfo Target) : base(Target) + { + Type = TargetType.Editor; + DefaultBuildSettings = BuildSettingsVersion.V5; + IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_5; + ExtraModuleNames.Add("CaptureToDisk"); + } +} + +``` + +`LICENSE`: + +``` +MIT License + +Copyright (c) 2020 Timm Hess + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +`README.md`: + +```md +# Image Capturing With UnrealEngine 5.5 (for deep learning) + + +| Color | Segmentation | +|---------|---------| +| ![](https://github.com/TimmHess/UnrealImageCapture/blob/master/gfx/CaptureResult_color.jpeg) | ![](https://github.com/TimmHess/UnrealImageCapture/blob/master/gfx/CaptureResult_segmentation.png) | + +# Changelog +- Updated to UE5.5.4 +- Streamlined capture code to not block the *render thread*, and further reduce the load on the *main game thread*. +- Fixed alpha channel for pixel annotation in the post-process material. Images are no longer appearing as "empty" because of their alpha mask being 0. +- Removed plugins from repository - reduce the mess of files +- Slightly updated the tutorial text + +# Outline +- [Introduction](#a-small-introduction) +- [MAIN: How to Save Images to Disk (without blocking the main threads)](#how-to-save-images-to-disk-in-ue5-without-blocking-the-rendering-or-main-thread) +- [Capturing Object Pixel-Annotations (segmentation mask)](#capturing-annotations) +- [Capturing Scene Depth](#setup-a-depth-capture) +- [Enable Lumen on SceneCapture2D](#enable-lumen-on-scenecapture2d) +- [Known Issues](#known-issues) + +# TLDR +Use these links to the [FrameCaptureManger.h](https://github.com/TimmHess/UnrealImageCapture/blob/master/CaptureToDisk/Source/CaptureToDisk/Public/FrameCaptureManager.h) and [FrameCaptureManger.cpp](https://github.com/TimmHess/UnrealImageCapture/blob/master/CaptureToDisk/Source/CaptureToDisk/Private/FrameCaptureManager.cpp) file. They are the only source needed. +Plus, make sure to link the correct unreal-libs to your project - check the [prerequisite](#prerequisite) or [CaptureToDisk.Build.cs](https://github.com/TimmHess/UnrealImageCapture/blob/master/CaptureToDisk/Source/CaptureToDisk/CaptureToDisk.Build.cs). + +**Kudos to the UE4 and UE5 community!** + +**Special thanks to [Panakotta00](https://github.com/Panakotta00), for pointing to an even better GPU readback!** + +Using the source as is you should get an `AFrameCaptureManager` (`Actor`) in your scene with the following settings. +![](https://github.com/TimmHess/UnrealImageCapture/blob/master/gfx/FrameCaptureManage_settings.png) + +Maybe most important is the `ImageFormat` setting. You want to use `PNG` for lossless compression when storing [object annotation](#capturing-object-segmentation-masks). For color rendered images most likely `JPEG` gives you better file-sizes. `EXR` is the float format - the code automatically stores .exr images when used. + +# Known Issues +Capturing per-pixel annotations is done using the `CustomDepth` feature. The `CustomDepthStencil` is of type `uint8` which allows a range of 0-255, i.e. **we can handle at most 256** different annotations per image! + +# +# A Small Introduction +In this repository I condense my findings on how to implement a component to capture images to disk from an arbitrary UE5 (former UE4) scene **from scratch** lowering the bar for UE novices (and potentially bypassing the need for large frameworks that don't fit ones own particular needs). This will include: +1. Capturing rendered images to disk at high FPS, without blocking the UE rendering thread or the main game thread +2. Rendering pixel annotations (or other graphics buffers, such as depth) at the same time + +UnrealEngine (UE) is a powerful tool to create virtual worlds capable of AAA productions. Generating temporally consistent data with automatic pixel-wise annotations from complex scenes, such as traffic scenarios, is a capability worth leveraging. Especially for training and validation of machine learning- or deep learning applications it has been explored in a variety of projects. Already, there are plugins available that allow rendering images from UE to disk at runtime, such as prominently [Carla](https://carla-ue5.readthedocs.io/en/latest/), [UnrealCV](https://unrealcv.org/), or [AirSim](https://github.com/microsoft/AirSim). This repository aims to be a tutorial that demonstrates such an 'image capturing' mechanism in detail for you to understand its inner workings, and in turn enable you to reuse it in a custom fashions that suit the needs of your project. + +When I was setting up scenes for my research the plugins mentioned above were just not yet supporting the latest engine versions that I wanted/needed to use. Also, I was missing a place where the knowledge of how to render images to disk was explains for non-advanced graphics-programmers. Of course, there are lots of sources for code available online and also there are community blog-entries scattered across multiple platforms explaining parts of the problem and possible solutions, even though they typically are targeting very particular scenarios. + +**Disclaimer: I do not claim to own any of the code. Merely, I condensed the sources already available online for easier use and provide an overview to the general functionality of this particular approach!** + + + +# +# How to Save Images to Disk In UE5 (without blocking the rendering or main thread) +I will go through the main components of the code step-by-step so that hopefully it will be easier to implement each step as you are following along. However, I recommend looking at the source that is merely a single class ([here]()). + +*In the explanations I skip certain quality-of-life-like aspects for sakes of readability, for example exposing image resolution settings to the editor instead of hardcoding them. Make sure to check out the sources linked in [TLDR](#tldr)* + + +# +## Prerequisite +You will need a UE5 C++ project. + +Also, you will have to add a few packages to your `'YourProjectName'.Build.cs` file. These are part of UnrealEngine, however, sometimes they are not added automatically resulting in unpleasant (linker) errors. Find the `'YourProjectName'.Build.cs` file in the `Source/'YourProjectName/` directory, and add or extend it to include the modules: `"ImageWrapper", "RenderCore", "Renderer", "RHI"`, for example like this: + +``` cpp +PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput" , "ImageWrapper", "RenderCore", "Renderer", "RHI"}); +``` + +# +## Setup A FrameCapture Component +I am using ```SceneCaptureComponent2D``` as the basis for capturing images. Placing one of these into your scene will give you an ```ASceneCaptureComponent``` which is its `Actor` instance. It basically behaves like any other camera component, but its viewport is not restricted by your computer's monitor or main camera viewport. This provides us the possibility to render images of arbitrary resolution independent from the actual screen resolution. + +> Add a ```FrameCaptureManager``` class of type `Actor` to your project. + +All functionality to request the capturing of a frame, as well as receiving the rendered image back, and storing the frame to disk will be handled by the `FrameCaptureManager`. + +In the ```FrameCaptureManager.h``` we add the following:\ +**FrameCaptureManager.h** +``` cpp +#pragma once +class ASceneCapture2D; // forward declaration + +#include ... // the stuff that is already there +``` +and to our public variables: +``` cpp +// Color Capture Components +UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture") +ASceneCapture2D* CaptureComponent; +``` + +This enables you to assign a ```CaptureComponent2d``` to your ```FrameCaptureManager``` inside the UE5 Editor. + +> Compile and place a ```FrameCaptureManager``` in your scene. + +As it does not have any primitive to render you will only see it in the editor's outline. In the details panel of the placed ```FrameCaptureManager``` you can now see the ```CaptureComponent``` assigned to ```None```. From the drop down menu select the ```CaptureComponent2D``` you already placed in the scene. + +Back to code: We will now prepare our yet "naked" `CaptureComponent2D` class for capturing images. This includes creating and assigning a `RenderTarget` - which is basically a `Texture` to store our image data to - and setting the camera properties. + +*Note: You could also do this in the Editor but if you deal with, i.e. multiple capture components, you may find it handy not to worry about creating and assigning all the components by hand!* + +> Create a setup function to put all your setup code for the CaptureComponents in the CaptureManger: + +**FrameCaptureManager.h** +``` cpp +protected: + + void SetupCaptureComponent(); +``` +**FrameCaptureManager.cpp** +``` cpp +#include ... + +// A bunch of includes we need +#include "Engine/SceneCapture2D.h" +#include "Components/SceneCaptureComponent2D.h" +#include "ShowFlags.h" + +#include "Engine/TextureRenderTarget2D.h" +#include "Materials/Material.h" +#include "RHICommandList.h" +#include "IImageWrapper.h" +#include "IImageWrapperModule.h" +#include "ImageUtils.h" + +#include "Modules/ModuleManager.h" +#include "Misc/FileHelper.h" + + +void AFrameCaptureManager::SetupCaptureComponent(){ + if(!IsValid(CaptureComponent)){ + UE_LOG(LogTemp, Error, TEXT("SetupCaptureComponent: CaptureComponent is not valid!")); + return; + } + + // Create RenderTargets + UTextureRenderTarget2D* renderTarget2D = NewObject(); + renderTarget2D->InitAutoFormat(256, 256); // some random format, got crashing otherwise + + renderTarget2D->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA8_SRGB; //ETextureRenderTargetFormat::RTF_RGBA8; //8-bit color format + renderTarget2D->InitCustomFormat(FrameWidth, FrameHeight, PF_R8G8B8A8, true); // PF... disables HDR, which is most important since HDR gives gigantic overhead, and is not needed! + renderTarget2D->bForceLinearGamma = true; // Important for viewport-like color reproduction. + + renderTarget2D->bGPUSharedFlag = true; // demand buffer on GPU + + // Assign RenderTarget + CaptureComponent->GetCaptureComponent2D()->TextureTarget = renderTarget2D; + // Set Camera Properties + CaptureComponent->GetCaptureComponent2D()->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR; + CaptureComponent->GetCaptureComponent2D()->TextureTarget->TargetGamma = GEngine->GetDisplayGamma(); + CaptureComponent->GetCaptureComponent2D()->ShowFlags.SetTemporalAA(true); + // lookup additional showflags in documentation +} +``` +> Call the code during `BeginPlay` of the `FrameCaptureManager` + +**FrameCaptureManager.cpp** +``` cpp +// Called when the game starts or when spawned +void AFrameCaptureManager::BeginPlay() +{ + Super::BeginPlay(); + + // Setup CaptureComponent + if(CaptureComponent){ // nullptr check + SetupCaptureComponent(); + } else{ + UE_LOG(LogTemp, Error, TEXT("No CaptureComponent set!")); + } +} + +``` +Now that because we have a `RenderTarget` applied to our `CaptureComponent` we can read its data and store it to disk. + + +# +## Organize RenderRequests +We do this by basically re-implementing UE's code for taking screenshots. Importantly, with the addition of not flushing our rendering pipeline. This prevents rendering *hiccups* that drop the framerate to 3 - 5 FPS. + +This addition will come at the price of needing to handle 'waiting times' before an image is done and copied from GPU. This is important to prevent reading old or uninitialized buffers (remember that `RenderThread` and `GameThread` are asynchronous). We do this by keeping a queue of ```RenderRequest``` that we can probe for being completed. + +> We add the following ```struct``` to our `FrameCaptureManager.h` above the `UCLASS()` definition: + +**FrameCaptureManager.h** +``` cpp +#include ... + +[...] + +struct FRenderRequestStruct{ + FIntPoint ImageSize; + FRHIGPUTextureReadback Readback; + FRenderCommandFence RenderFence; + + void* RawData = nullptr; + int64 RawSize = 0; + + bool bIsComplete = false; + + FRenderRequestStruct( + const FIntPoint& ImageSize, + const FRHIGPUTextureReadback& Readback) : + ImageSize(ImageSize), + Readback(Readback) {} +}; + +[...] +UCLASS() +class ... +[...] +``` +The ```FRHIGPUTextureReadback``` will hold the rendered results, e.g. color or depth values. The ```RenderFence``` is a neat feature of UE, letting you put a 'fence' into the render pipeline that can be checked to notify when it has passed the full rendering-pipeline. This gives a way to determine whether our render request is done and the buffers are safe to read. + +> We need to add a ```TQueue``` as a data structure to keep track of our render requests: + +**CaptureManger.h** +``` cpp +protected: + // RenderRequest Queue + TQueue> RenderRequestQueue; + TQueue> InThreadRenderRequestQueue; +``` + +# +## Implement placing render requests: +This function will place a render request on the UE rendering pipeline asking the data captured from our `CaptureComponent` to be copied in our image buffer so that we can further process it. + +**CaptureManger.h** +``` cpp +public: + UFUNCTION(BlueprintCallable, Category = "ImageCapture") + void CaptureNonBlocking(); +``` + +**CaptureManger.cpp** +``` cpp +void AFrameCaptureManager::CaptureNonBlocking(){ + if(!IsValid(CaptureComponent)){ + UE_LOG(LogTemp, Error, TEXT("CaptureColorNonBlocking: CaptureComponent was not valid!")); + return; + } + CaptureComponent->GetCaptureComponent2D()->TextureTarget->TargetGamma = GEngine->GetDisplayGamma(); + + // Get RenderConterxt + FTextureRenderTargetResource* renderTargetResource = CaptureComponent->GetCaptureComponent2D()->TextureTarget->GameThread_GetRenderTargetResource(); + + TSharedPtr renderRequest = + MakeShared( + renderTargetResource->GetSizeXY(), + FRHIGPUTextureReadback(TEXT("CameraCaptureManagerReadback") + ) + ); + + ENQUEUE_RENDER_COMMAND(SceneDrawCompletion)( + [renderRequest, renderTargetResource](FRHICommandListImmediate& RHICmdList) { + FTextureRHIRef Target = renderTargetResource->GetRenderTargetTexture(); + renderRequest->Readback.EnqueueCopy(RHICmdList, Target); + }); + + // Notifiy new task in RenderQueue + RenderRequestQueue.Enqueue(renderRequest); + + // Set RenderCommandFence + renderRequest->RenderFence.BeginFence(); +} +``` +With this, the image data will be stored in our queue of requests. Now we can think of storing it to disk. + +*Note: UFUNCTION(BlueprintCallable, Category = "ImageCapture") exposes this function to blueprint, so that you can easily test it* + + +# +## Save Image Data to Disk +In each tick of the `FrameCaptureManager` we look up the first element of the `RenderQueue`. If it's `RenderFence` is completed and the data is ready to read, we proceed with saving the image to disk. + +We need a procedure to write the data to disk, preferably without blocking our `GameThread`. +We implement an [asynchronous](https://wiki.unrealengine.com/Using_AsyncTasks) procedure storing the data to disk. + +**FrameCaptureManager.h** +``` cpp +UCLASS() +class ... { +[...] +}; + +// Below the AFrameCaptureManager class definition + +class AsyncSaveImageToDiskTask : public FNonAbandonableTask +{ +public: + AsyncSaveImageToDiskTask( + TSharedPtr InRenderRequest, + FString ImageName, + int32 Width, + int32 Height, + ERGBFormat RGBFormat, + EImageFormat ImageFormat); + ~AsyncSaveImageToDiskTask(); + + void DoWork(); + + FORCEINLINE TStatId GetStatId() const + { + RETURN_QUICK_DECLARE_CYCLE_STAT(AsyncSaveImageToDiskTask, STATGROUP_ThreadPoolAsyncTasks); + } + +private: + TSharedPtr RenderRequest; // Hold the shared pointer + FString FileName; + int32 Width; + int32 Height; + ERGBFormat RGBFormat; + EImageFormat ImageFormat; +}; +``` + +**FrameCaptureManager.cpp** +``` cpp +#include ... + +AsyncSaveImageToDiskTask::AsyncSaveImageToDiskTask( + TSharedPtr InRenderRequest, + FString ImageName, + int32 Width, + int32 Height, + ERGBFormat RGBFormat, + EImageFormat ImageFormat + ): + RenderRequest(InRenderRequest), + FileName(ImageName), + Width(Width), + Height(Height), + RGBFormat(RGBFormat), + ImageFormat(ImageFormat) +{ +} + +AsyncSaveImageToDiskTask::~AsyncSaveImageToDiskTask(){ + UE_LOG(LogTemp, Warning, TEXT("AsyncTaskDone")); +} + + +void AsyncSaveImageToDiskTask::DoWork(){ + UE_LOG(LogTemp, Warning, TEXT("Starting Work")); + + // Load the image wrapper module (if not already loaded) + IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked(FName("ImageWrapper")); + + // Create an image wrapper + TSharedPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(ImageFormat); + + // Error handling + if (!ImageWrapper.IsValid()) { + UE_LOG(LogTemp, Error, TEXT("Failed to create IImageWrapper!")); + return; + } + + // Set the raw data + int32 PixelDepth = 8; + ImageWrapper->SetRaw(RenderRequest->RawData, RenderRequest->RawSize, Width, Height, RGBFormat, PixelDepth); + + // Compress the image + const TArray64& CompressedData = ImageWrapper->GetCompressed(100); + + // Save the compressed image to disk + FFileHelper::SaveArrayToFile(CompressedData, *FileName); + + //unlock the readback after the processing is done + RenderRequest->Readback.Unlock(); + + // Indicate that the processing is complete (using a flag) + RenderRequest->bIsComplete = true; +} +``` +We offload the entire image processing into `DoWork()`, from applying compression encoding to finally storing it to disk, to the asynchronous thread. + +*Note that this requires our ``RenderRequest`, more precisely its `RawData` image-buffer to stay available while the data is being stored to disk. Otherwise we encounter segmentation faults and the engine will crash!* + + +# +## Override the `Tick` function of the `FrameCaptureManager`: +Finally, we put everything together in the `Tick` function of our `FrameCaptureManager`. We wait for a `RenderRequest` to become available, we hand it over to an asynchronous thread for being stored to disk, and we monitor the progress of the asynchronous thread to finally release the `RenderRequest`'s buffers to garbage collection. + + +**FrameCaptureManager.h** +``` cpp +public: + + // Called every frame + virtual void Tick(float DeltaTime) override; +``` +**FrameCaptureManager.cpp** +``` cpp +// Called every frame +void AFrameCaptureManager::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + if(!RenderRequestQueue.IsEmpty()){ + // Peek the next RenderRequest from queue + TSharedPtr nextRenderRequest = *RenderRequestQueue.Peek(); + + if(nextRenderRequest){ //nullptr check + if(nextRenderRequest->RenderFence.IsFenceComplete() && nextRenderRequest->Readback.IsReady()){ + // Load the image wrapper module + IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked(FName("ImageWrapper")); + + // Get Data from Readback + nextRenderRequest->RawSize = nextRenderRequest->ImageSize.X * nextRenderRequest->ImageSize.Y * sizeof(FColor); + + int32 RowPitchInPixels; + nextRenderRequest->RawData = nextRenderRequest->Readback.Lock(RowPitchInPixels, nullptr); // Pass RowPitchInPixels and no buffer size + + // Generate image name + FString fileName = ""; + fileName = FPaths::ProjectSavedDir() + SubDirectoryName + "/img" + "_" + ToStringWithLeadingZeros(ImgCounter, NumDigits); + fileName += GetFileEnding(ImageFormat); + + // Pass the raw data, filename, width, height, and format to the async task + RunAsyncImageSaveTask(nextRenderRequest, fileName, 1920, 1080, ERGBFormat::RGBA, EImageFormat::PNG); + + // Security check + if(VerboseLogging && !fileName.IsEmpty()){ + UE_LOG(LogTemp, Warning, TEXT("%s"), *fileName); + } + + // Increase ImgCounter for file names + ImgCounter += 1; + + // Release RenderRequest form the queue + RenderRequestQueue.Pop(); // Delete the first element from RenderQueue + // Put it into the queue of outsourced threads + InThreadRenderRequestQueue.Enqueue(nextRenderRequest); //Push to InThreadRenderRequestQueue + } + } + } + if(!InThreadRenderRequestQueue.IsEmpty()){ + UE_LOG(LogTemp, Log, TEXT("InThreadRenderRequestQueue not empty.")); + + // Get next element of the InThreadRenderRequestQueue + TSharedPtr nextRenderRequest = *InThreadRenderRequestQueue.Peek(); + + if(nextRenderRequest){ //nullptr check + if(nextRenderRequest->bIsComplete){ //check if complete + InThreadRenderRequestQueue.Pop(); // Remove from queue + } + } + } +} +``` + +For testing purposes we can call the `CaptureColorBlocking()` from the `LevelBlueprint` by attaching it to a button pressed event. + +![](https://github.com/TimmHess/UnrealImageCapture/blob/master/gfx/Debug_level_blueprint_capture.png) + +The captured images will now be saved into your project's `Saved` directory. + + +# +# Capturing Annotations +To be able to render color and segmentation at the same time, we need one additional `SceneCapture2D` component in our scene for each *type* of capture you want. Attach it to the `SceneCapture2D` already placed in your scene (from earlier in this tutorial). Make sure both have exactly the same location, rotation, and perspective settings. Otherwise your annotation will not match. + +# Capturing Object Segmentation Masks +To get labels for our images we will add a second `CaptureComponent` equipped with a `PostProcessMaterial` that visualizes `CustomDepth`. The `CustomDepthStencil` is settable for each actor in the scene, effectively letting us label and visualize categories of, as well as individual, actors. + +## 1. Enable and Set-Up Custom Depth Stencils +Find the **ProjectSettings** in your editor and search for *stencil* which will bring up `Custom Depth-Stencil Pass`. Switch this option from `Enabled` to `Enabled with Stencil`. + +You can set the custom depth in editor or from code. For simplicity I chose the editor. Place an arbitrary object(MeshActor) into the scene, and search for `custom depth` in its details panel. Under `Rendering` enable `Render CustomDepth Pass`, and set `CustomDepth Stencil Value` to whatever you like. For illustration purposes set it to 200. + +*Note: Make sure you have custom depth enabled with stencils in your project settings.* +![](https://github.com/TimmHess/UnrealImageCapture/blob/master/gfx/Enable_custom_depth_in_project.png) + +![](https://github.com/TimmHess/UnrealImageCapture/blob/master/gfx/Apply_custom_depth_stencil.png) + + +## 2. Setting Up The PostProcess Material +Add a new `Material` to your project content. (I will call it `PP_Segmentation`) + +Click on the material's output node and switch `MaterialDomain` from `Surface` to `PostProcess`. + +In the same panel search for "alpha" and activate `Output Alpha`. Set this value to `1.0` in the Material node. + +Right-click to open the node search and type `SceneTexture`, select the node from `Texture`-Category. + +In the details of this node, select `CustomStencil` as `SceneTextureId`. + +Add a `Division` node and connect the `SceneTexture`'s `Color` output to the division node. Set the division to be by 255. + +*Note: This is needed because the image buffer seems to be float valued, leading to values > 1 having no meaning, as image information ranges from 0.0 to 1.0.* + +Apply and save the material. + +![](https://github.com/TimmHess/UnrealImageCapture/blob/master/gfx/pp_segmentation.png) + + +**FrameCaptureManager.h** +``` cpp +public: + // PostProcessMaterial used for segmentation + UPROPERTY(EditAnywhere, Category="Segmentation Setup") + UMaterial* PostProcessMaterial = nullptr; +``` +**FrameCaptureManager.cpp** +``` cpp +void AFrameCaptureManager::SetupCaptureComponent(){ + [...] // previous function code + + // Assign PostProcess Material if assigned + if(PostProcessMaterial){ // check nullptr + CaptureComponent->GetCaptureComponent2D()->AddOrUpdateBlendable(PostProcessMaterial); + } else { + UE_LOG(LogTemp, Log, TEXT("No PostProcessMaterial is assigend")); + } +} +``` + +You can now reference the ``PostProcessMaterial` in the details panel of the `FrameCaptureManager` in the editor just like before the `SceneCapture2D`. + + +# +# Setup a Depth Capture +Capturing `SceneDepth` information has one important caveat - it requires storing float images (.exr). +Every thing else follows like [Object Segmentation](#capturing-object-segmentation-masks). We create a `PostProcessMaterial` to access the respectie GPU buffer. + +![](https://github.com/TimmHess/UnrealImageCapture/blob/master/gfx/pp_depth.png) + +Luckily UnrealEngine is perfectly capable of storing float images and even provides the .exr file format in its `IImageWrapper`. However, there are three places in our code where we need to take care to handle the float format correctly. Missing any of those will result in segmentation faults of the engine. + +## 1. Accounting for the RawSize correctly +We adjust the code that allocates the RawSize of our captured images to accomodate the float values using 16 bits. + +*Note that I decide for the RawSize to used based on the ImageFormat. This I do not explain in the tutorial - please check the source.* + +**FrameCaptureManager.cpp** +``` cpp +void AFrameCaptureManager::SetupCaptureComponent(){ + [...] + + // Get Data from Readback + nextRenderRequest->RawSize = nextRenderRequest->ImageSize.X * nextRenderRequest->ImageSize.Y * sizeof(FColor); + if(ImageFormat == ECustomImageFormat::EXR){ // handle float case + nextRenderRequest->RawSize = nextRenderRequest->ImageSize.X * nextRenderRequest->ImageSize.Y * sizeof(FFloat16Color); //FLOAT + } + + [...] +} +``` + +## 2. Adjust the correct RenderTarget format +**FrameCaptureManager.cpp** +``` cpp +void AFrameCaptureManager::SetupCaptureComponent(){ + [...] + + // Create RenderTargets + UTextureRenderTarget2D* renderTarget2D = NewObject(); + renderTarget2D->InitAutoFormat(256, 256); // some random format, got crashing otherwise + + // Float Capture + if(ImageFormat == ECustomImageFormat::EXR){ // handle float case + renderTarget2D->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA32f; + renderTarget2D->InitCustomFormat(FrameWidth, FrameHeight, PF_FloatRGBA, true); // PF_B8G8R8A8 disables HDR which will boost storing to disk due to less image information + } + // Color Capture + else{ + renderTarget2D->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA8_SRGB; //ETextureRenderTargetFormat::RTF_RGBA8; //8-bit color format + renderTarget2D->InitCustomFormat(FrameWidth, FrameHeight, PF_R8G8B8A8, true); // PF_R8G8B8A8 //PF_B8G8R8A8 // PF... disables HDR, which is most important since HDR gives gigantic overhead, and is not needed! + renderTarget2D->bForceLinearGamma = true; // Important for viewport-like color reproduction. + } + renderTarget2D->bGPUSharedFlag = true; // demand buffer on GPU + + [...] +} +``` + +## 3. Adjust the PixelDepth when storing +**FrameCaptureManager.cpp** +``` cpp +void AsyncSaveImageToDiskTask::DoWork(){ + [...] + + // Set the raw data + int32 PixelDepth = 8; + if(ImageFormat == EImageFormat::EXR){ // Adjust pixel depth for EXR (float) data // Has to be EImageFormat because already converted.. + PixelDepth = 16; + } + UE_LOG(LogTemp, Warning, TEXT("PixelDepth: %d"), PixelDepth); + ImageWrapper->SetRaw(RenderRequest->RawData, RenderRequest->RawSize, Width, Height, RGBFormat, PixelDepth); + + [...] +} +``` + + +# +# Enable Lumen on SceneCapture2D +In my tests using UE5.5.3+ the `SceneCapture2D` was fully capable of rendering scenes with Lumen. However you need might need to actiate it in the `SceneCapture2D` itself as it was not listening to the `PostProcessVolume` in my scene. + +![](https://github.com/TimmHess/UnrealImageCapture/blob/master/gfx/Activate_Lumen.png) + + + +``` \ No newline at end of file diff --git a/archive/nxrighthere/UnrealCLR.txt b/archive/nxrighthere/UnrealCLR.txt new file mode 100644 index 00000000..3594812f --- /dev/null +++ b/archive/nxrighthere/UnrealCLR.txt @@ -0,0 +1,65804 @@ +Project Path: arc_nxrighthere_UnrealCLR_3yj5pwi2 + +Source Tree: + +```txt +arc_nxrighthere_UnrealCLR_3yj5pwi2 +├── API +│ ├── AIController-AIController(string_Blueprint).md +│ ├── AIController-AllowStrafe.md +│ ├── AIController-ClearFocus(AIFocusPriority).md +│ ├── AIController-GetFocalPoint().md +│ ├── AIController-GetFocalPoint(Vector3).md +│ ├── AIController-GetFocusActor().md +│ ├── AIController-SetFocalPoint(Vector3_AIFocusPriority).md +│ ├── AIController-SetFocus(Actor_AIFocusPriority).md +│ ├── AIController.md +│ ├── AIFocusPriority.md +│ ├── Actor-Actor(string_Blueprint).md +│ ├── Actor-AddTag(string).md +│ ├── Actor-BlockInput.md +│ ├── Actor-CreationTime.md +│ ├── Actor-Destroy().md +│ ├── Actor-Equals(Actor).md +│ ├── Actor-ForEachAttachedActor-T-(Action-T-).md +│ ├── Actor-ForEachChildActor-T-(Action-T-).md +│ ├── Actor-ForEachComponent-T-(Action-T-).md +│ ├── Actor-ForEachOverlappingActor-T-(Action-T-).md +│ ├── Actor-GetBool(string_bool).md +│ ├── Actor-GetBounds(bool_Vector3_Vector3).md +│ ├── Actor-GetByte(string_byte).md +│ ├── Actor-GetComponent-T-(string).md +│ ├── Actor-GetComponentByID-T-(uint).md +│ ├── Actor-GetComponentByTag-T-(string).md +│ ├── Actor-GetDistanceTo(Actor).md +│ ├── Actor-GetDouble(string_double).md +│ ├── Actor-GetEnum-T-(string_T).md +│ ├── Actor-GetEyesViewPoint(Vector3_Quaternion).md +│ ├── Actor-GetFloat(string_float).md +│ ├── Actor-GetHashCode().md +│ ├── Actor-GetHorizontalDistanceTo(Actor).md +│ ├── Actor-GetInt(string_int).md +│ ├── Actor-GetLong(string_long).md +│ ├── Actor-GetRootComponent-T-().md +│ ├── Actor-GetShort(string_short).md +│ ├── Actor-GetString(string_string).md +│ ├── Actor-GetText(string_string).md +│ ├── Actor-GetUInt(string_uint).md +│ ├── Actor-GetULong(string_ulong).md +│ ├── Actor-GetUShort(string_ushort).md +│ ├── Actor-HasTag(string).md +│ ├── Actor-Hide(bool).md +│ ├── Actor-ID.md +│ ├── Actor-InputComponent.md +│ ├── Actor-Invoke(string).md +│ ├── Actor-IsOverlappingActor(Actor).md +│ ├── Actor-IsRootComponentMovable.md +│ ├── Actor-IsSpawned.md +│ ├── Actor-Name.md +│ ├── Actor-RegisterEvent(ActorEventType).md +│ ├── Actor-RemoveTag(string).md +│ ├── Actor-Rename(string).md +│ ├── Actor-SetBool(string_bool).md +│ ├── Actor-SetByte(string_byte).md +│ ├── Actor-SetDouble(string_double).md +│ ├── Actor-SetEnableCollision(bool).md +│ ├── Actor-SetEnableInput(PlayerController_bool).md +│ ├── Actor-SetEnum-T-(string_T).md +│ ├── Actor-SetFloat(string_float).md +│ ├── Actor-SetInt(string_int).md +│ ├── Actor-SetLifeSpan(float).md +│ ├── Actor-SetLong(string_long).md +│ ├── Actor-SetRootComponent(SceneComponent).md +│ ├── Actor-SetShort(string_short).md +│ ├── Actor-SetString(string_string).md +│ ├── Actor-SetText(string_string).md +│ ├── Actor-SetUInt(string_uint).md +│ ├── Actor-SetULong(string_ulong).md +│ ├── Actor-SetUShort(string_ushort).md +│ ├── Actor-TeleportTo(Vector3_Quaternion_bool_bool).md +│ ├── Actor-UnregisterEvent(ActorEventType).md +│ ├── Actor.md +│ ├── ActorComponent-AddTag(string).md +│ ├── ActorComponent-Destroy(bool).md +│ ├── ActorComponent-Equals(ActorComponent).md +│ ├── ActorComponent-GetActor-T-().md +│ ├── ActorComponent-GetBool(string_bool).md +│ ├── ActorComponent-GetByte(string_byte).md +│ ├── ActorComponent-GetDouble(string_double).md +│ ├── ActorComponent-GetEnum-T-(string_T).md +│ ├── ActorComponent-GetFloat(string_float).md +│ ├── ActorComponent-GetHashCode().md +│ ├── ActorComponent-GetInt(string_int).md +│ ├── ActorComponent-GetLong(string_long).md +│ ├── ActorComponent-GetShort(string_short).md +│ ├── ActorComponent-GetString(string_string).md +│ ├── ActorComponent-GetText(string_string).md +│ ├── ActorComponent-GetUInt(string_uint).md +│ ├── ActorComponent-GetULong(string_ulong).md +│ ├── ActorComponent-GetUShort(string_ushort).md +│ ├── ActorComponent-HasTag(string).md +│ ├── ActorComponent-ID.md +│ ├── ActorComponent-Invoke(string).md +│ ├── ActorComponent-IsCreated.md +│ ├── ActorComponent-IsOwnerSelected.md +│ ├── ActorComponent-Name.md +│ ├── ActorComponent-RemoveTag(string).md +│ ├── ActorComponent-Rename(string).md +│ ├── ActorComponent-SetBool(string_bool).md +│ ├── ActorComponent-SetByte(string_byte).md +│ ├── ActorComponent-SetDouble(string_double).md +│ ├── ActorComponent-SetEnum-T-(string_T).md +│ ├── ActorComponent-SetFloat(string_float).md +│ ├── ActorComponent-SetInt(string_int).md +│ ├── ActorComponent-SetLong(string_long).md +│ ├── ActorComponent-SetShort(string_short).md +│ ├── ActorComponent-SetString(string_string).md +│ ├── ActorComponent-SetText(string_string).md +│ ├── ActorComponent-SetUInt(string_uint).md +│ ├── ActorComponent-SetULong(string_ulong).md +│ ├── ActorComponent-SetUShort(string_ushort).md +│ ├── ActorComponent.md +│ ├── ActorCursorDelegate(ActorReference).md +│ ├── ActorEventType.md +│ ├── ActorHitDelegate(ActorReference_ActorReference_Vector3_Hit).md +│ ├── ActorKeyDelegate(ActorReference_string).md +│ ├── ActorOverlapDelegate(ActorReference_ActorReference).md +│ ├── ActorReference-Equals(ActorReference).md +│ ├── ActorReference-Equals(object).md +│ ├── ActorReference-GetHashCode().md +│ ├── ActorReference-ID.md +│ ├── ActorReference-IsSpawned.md +│ ├── ActorReference-Name.md +│ ├── ActorReference-ToActor-T-().md +│ ├── ActorReference-op_Equality(ActorReference_ActorReference).md +│ ├── ActorReference-op_Inequality(ActorReference_ActorReference).md +│ ├── ActorReference.md +│ ├── AmbientSound-AmbientSound(string_Blueprint).md +│ ├── AmbientSound.md +│ ├── AnimationAsset-Equals(AnimationAsset).md +│ ├── AnimationAsset-GetHashCode().md +│ ├── AnimationAsset-IsCreated.md +│ ├── AnimationAsset.md +│ ├── AnimationCompositeBase.md +│ ├── AnimationInstance-Equals(AnimationInstance).md +│ ├── AnimationInstance-GetBlendTime(AnimationMontage).md +│ ├── AnimationInstance-GetBool(string_bool).md +│ ├── AnimationInstance-GetByte(string_byte).md +│ ├── AnimationInstance-GetCurrentActiveMontage().md +│ ├── AnimationInstance-GetCurrentSection(AnimationMontage).md +│ ├── AnimationInstance-GetDouble(string_double).md +│ ├── AnimationInstance-GetEnum-T-(string_T).md +│ ├── AnimationInstance-GetFloat(string_float).md +│ ├── AnimationInstance-GetHashCode().md +│ ├── AnimationInstance-GetInt(string_int).md +│ ├── AnimationInstance-GetLong(string_long).md +│ ├── AnimationInstance-GetPlayRate(AnimationMontage).md +│ ├── AnimationInstance-GetPosition(AnimationMontage).md +│ ├── AnimationInstance-GetShort(string_short).md +│ ├── AnimationInstance-GetString(string_string).md +│ ├── AnimationInstance-GetText(string_string).md +│ ├── AnimationInstance-GetUInt(string_uint).md +│ ├── AnimationInstance-GetULong(string_ulong).md +│ ├── AnimationInstance-GetUShort(string_ushort).md +│ ├── AnimationInstance-Invoke(string).md +│ ├── AnimationInstance-IsCreated.md +│ ├── AnimationInstance-IsPlaying(AnimationMontage).md +│ ├── AnimationInstance-JumpToSection(AnimationMontage_string).md +│ ├── AnimationInstance-JumpToSectionsEnd(AnimationMontage_string).md +│ ├── AnimationInstance-PauseMontage(AnimationMontage).md +│ ├── AnimationInstance-PlayMontage(AnimationMontage_float_float_bool).md +│ ├── AnimationInstance-ResumeMontage(AnimationMontage).md +│ ├── AnimationInstance-SetBool(string_bool).md +│ ├── AnimationInstance-SetByte(string_byte).md +│ ├── AnimationInstance-SetDouble(string_double).md +│ ├── AnimationInstance-SetEnum-T-(string_T).md +│ ├── AnimationInstance-SetFloat(string_float).md +│ ├── AnimationInstance-SetInt(string_int).md +│ ├── AnimationInstance-SetLong(string_long).md +│ ├── AnimationInstance-SetNextSection(AnimationMontage_string_string).md +│ ├── AnimationInstance-SetPlayRate(AnimationMontage_float).md +│ ├── AnimationInstance-SetPosition(AnimationMontage_float).md +│ ├── AnimationInstance-SetShort(string_short).md +│ ├── AnimationInstance-SetString(string_string).md +│ ├── AnimationInstance-SetText(string_string).md +│ ├── AnimationInstance-SetUInt(string_uint).md +│ ├── AnimationInstance-SetULong(string_ulong).md +│ ├── AnimationInstance-SetUShort(string_ushort).md +│ ├── AnimationInstance-StopMontage(AnimationMontage_float).md +│ ├── AnimationInstance.md +│ ├── AnimationMode.md +│ ├── AnimationMontage-Load(string).md +│ ├── AnimationMontage.md +│ ├── AnimationSequence-Load(string).md +│ ├── AnimationSequence.md +│ ├── AnimationSequenceBase.md +│ ├── Application-DefaultLanguage.md +│ ├── Application-IsCanEverRender.md +│ ├── Application-IsPackagedForDistribution.md +│ ├── Application-IsPackagedForShipping.md +│ ├── Application-ProjectDirectory.md +│ ├── Application-ProjectName.md +│ ├── Application-RequestExit(bool).md +│ ├── Application-VolumeMultiplier.md +│ ├── Application.md +│ ├── Assert-IsFalse(bool_string_int_string).md +│ ├── Assert-IsNotNull-T-(T_string_int_string).md +│ ├── Assert-IsNull-T-(T_string_int_string).md +│ ├── Assert-IsTrue(bool_string_int_string).md +│ ├── Assert.md +│ ├── Asset-Equals(Asset).md +│ ├── Asset-Equals(object).md +│ ├── Asset-GetHashCode().md +│ ├── Asset-IsCreated.md +│ ├── Asset-Name.md +│ ├── Asset-Path.md +│ ├── Asset-op_Equality(Asset_Asset).md +│ ├── Asset-op_Inequality(Asset_Asset).md +│ ├── Asset.md +│ ├── AssetRegistry-AssetRegistry().md +│ ├── AssetRegistry-Equals(AssetRegistry).md +│ ├── AssetRegistry-ForEachAsset(Action-Asset-_string_bool_bool).md +│ ├── AssetRegistry-GetHashCode().md +│ ├── AssetRegistry-HasAssets(string_bool).md +│ ├── AssetRegistry-IsCreated.md +│ ├── AssetRegistry.md +│ ├── AttachmentTransformRule.md +│ ├── AudioComponent-AudioComponent(Actor_string_bool_Blueprint).md +│ ├── AudioComponent-FadeIn(float_float_float_AudioFadeCurve).md +│ ├── AudioComponent-FadeOut(float_float_AudioFadeCurve).md +│ ├── AudioComponent-IsPlaying.md +│ ├── AudioComponent-Paused.md +│ ├── AudioComponent-Play().md +│ ├── AudioComponent-SetSound(SoundBase).md +│ ├── AudioComponent-Stop().md +│ ├── AudioComponent.md +│ ├── AudioFadeCurve.md +│ ├── AutoPossessAI.md +│ ├── AutoReceiveInput.md +│ ├── BlendType.md +│ ├── Blueprint-Equals(Blueprint).md +│ ├── Blueprint-GetHashCode().md +│ ├── Blueprint-IsCreated.md +│ ├── Blueprint-Load(string).md +│ ├── Blueprint.md +│ ├── Bounds-BoxExtent.md +│ ├── Bounds-Equals(Bounds).md +│ ├── Bounds-Equals(object).md +│ ├── Bounds-GetHashCode().md +│ ├── Bounds-Origin.md +│ ├── Bounds-SphereRadius.md +│ ├── Bounds-op_Equality(Bounds_Bounds).md +│ ├── Bounds-op_Inequality(Bounds_Bounds).md +│ ├── Bounds.md +│ ├── BoxComponent-BoxComponent(Actor_string_bool_Blueprint).md +│ ├── BoxComponent-GetScaledBoxExtent().md +│ ├── BoxComponent-GetScaledBoxExtent(Vector3).md +│ ├── BoxComponent-GetUnscaledBoxExtent().md +│ ├── BoxComponent-GetUnscaledBoxExtent(Vector3).md +│ ├── BoxComponent-InitBoxExtent(Vector3).md +│ ├── BoxComponent-SetBoxExtent(Vector3_bool).md +│ ├── BoxComponent.md +│ ├── Brush-Brush(string_Blueprint).md +│ ├── Brush.md +│ ├── Camera-Camera(string_Blueprint).md +│ ├── Camera.md +│ ├── CameraComponent-AspectRatio.md +│ ├── CameraComponent-CameraComponent(Actor_string_bool_Blueprint).md +│ ├── CameraComponent-ConstrainAspectRatio.md +│ ├── CameraComponent-FieldOfView.md +│ ├── CameraComponent-LockToHeadMountedDisplay.md +│ ├── CameraComponent-OrthoFarClipPlane.md +│ ├── CameraComponent-OrthoNearClipPlane.md +│ ├── CameraComponent-OrthoWidth.md +│ ├── CameraComponent-SetProjectionMode(CameraProjectionMode).md +│ ├── CameraComponent.md +│ ├── CameraProjectionMode.md +│ ├── CapsuleComponent-CapsuleComponent(Actor_string_bool_Blueprint).md +│ ├── CapsuleComponent-GetScaledCapsuleSize(float_float).md +│ ├── CapsuleComponent-GetUnscaledCapsuleSize(float_float).md +│ ├── CapsuleComponent-InitCapsuleSize(float_float).md +│ ├── CapsuleComponent-ScaledCapsuleRadius.md +│ ├── CapsuleComponent-SetCapsuleRadius(float_bool).md +│ ├── CapsuleComponent-SetCapsuleSize(float_float_bool).md +│ ├── CapsuleComponent-ShapeScale.md +│ ├── CapsuleComponent-UnscaledCapsuleRadius.md +│ ├── CapsuleComponent.md +│ ├── Character-CanCrouch.md +│ ├── Character-CanJump.md +│ ├── Character-Character(string_Blueprint).md +│ ├── Character-CheckJumpInput(float).md +│ ├── Character-ClearJumpInput(float).md +│ ├── Character-Crouch().md +│ ├── Character-IsCrouched.md +│ ├── Character-Jump().md +│ ├── Character-Launch(Vector3_bool_bool).md +│ ├── Character-SetOnLandedCallback(CharacterLandedDelegate).md +│ ├── Character-StopCrouching().md +│ ├── Character-StopJumping().md +│ ├── Character.md +│ ├── CharacterLandedDelegate(Hit).md +│ ├── ChildActorComponent-ChildActorComponent(Actor_string_bool_Blueprint).md +│ ├── ChildActorComponent-GetChildActor-T-().md +│ ├── ChildActorComponent-SetChildActor-T-().md +│ ├── ChildActorComponent.md +│ ├── CollisionChannel.md +│ ├── CollisionMode.md +│ ├── CollisionResponse.md +│ ├── CollisionShape-CreateBox(Vector3).md +│ ├── CollisionShape-CreateCapsule(float_float).md +│ ├── CollisionShape-CreateSphere(float).md +│ ├── CollisionShape-Equals(CollisionShape).md +│ ├── CollisionShape-Equals(object).md +│ ├── CollisionShape-GetHashCode().md +│ ├── CollisionShape-ShapeType.md +│ ├── CollisionShape-op_Equality(CollisionShape_CollisionShape).md +│ ├── CollisionShape-op_Inequality(CollisionShape_CollisionShape).md +│ ├── CollisionShape.md +│ ├── CollisionShapeType.md +│ ├── CommandLine-Append(string).md +│ ├── CommandLine-Get().md +│ ├── CommandLine-Set(string).md +│ ├── CommandLine.md +│ ├── ComponentCursorDelegate(ComponentReference).md +│ ├── ComponentEventType.md +│ ├── ComponentHitDelegate(ComponentReference_ComponentReference_Vector3_Hit).md +│ ├── ComponentKeyDelegate(ComponentReference_string).md +│ ├── ComponentMobility.md +│ ├── ComponentOverlapDelegate(ComponentReference_ComponentReference).md +│ ├── ComponentReference-Equals(ComponentReference).md +│ ├── ComponentReference-Equals(object).md +│ ├── ComponentReference-GetHashCode().md +│ ├── ComponentReference-ID.md +│ ├── ComponentReference-IsCreated.md +│ ├── ComponentReference-Name.md +│ ├── ComponentReference-ToComponent-T-().md +│ ├── ComponentReference-op_Equality(ComponentReference_ComponentReference).md +│ ├── ComponentReference-op_Inequality(ComponentReference_ComponentReference).md +│ ├── ComponentReference.md +│ ├── ConsoleCommandDelegate(float).md +│ ├── ConsoleManager-FindVariable(string).md +│ ├── ConsoleManager-IsRegisteredVariable(string).md +│ ├── ConsoleManager-RegisterCommand(string_string_ConsoleCommandDelegate_bool).md +│ ├── ConsoleManager-RegisterVariable(string_string_bool_bool).md +│ ├── ConsoleManager-RegisterVariable(string_string_float_bool).md +│ ├── ConsoleManager-RegisterVariable(string_string_int_bool).md +│ ├── ConsoleManager-RegisterVariable(string_string_string_bool).md +│ ├── ConsoleManager-UnregisterObject(string).md +│ ├── ConsoleManager.md +│ ├── ConsoleObject-Equals(ConsoleObject).md +│ ├── ConsoleObject-GetHashCode().md +│ ├── ConsoleObject-IsBool.md +│ ├── ConsoleObject-IsCreated.md +│ ├── ConsoleObject-IsFloat.md +│ ├── ConsoleObject-IsInt.md +│ ├── ConsoleObject-IsString.md +│ ├── ConsoleObject.md +│ ├── ConsoleVariable-ClearOnChangedCallback().md +│ ├── ConsoleVariable-GetBool().md +│ ├── ConsoleVariable-GetFloat().md +│ ├── ConsoleVariable-GetInt().md +│ ├── ConsoleVariable-GetString().md +│ ├── ConsoleVariable-SetBool(bool).md +│ ├── ConsoleVariable-SetFloat(float).md +│ ├── ConsoleVariable-SetInt(int).md +│ ├── ConsoleVariable-SetOnChangedCallback(ConsoleVariableDelegate).md +│ ├── ConsoleVariable-SetString(string).md +│ ├── ConsoleVariable.md +│ ├── ConsoleVariableDelegate().md +│ ├── Controller-GetCharacter().md +│ ├── Controller-GetControlRotation().md +│ ├── Controller-GetControlRotation(Quaternion).md +│ ├── Controller-GetDesiredRotation().md +│ ├── Controller-GetDesiredRotation(Quaternion).md +│ ├── Controller-GetPawn().md +│ ├── Controller-GetViewTarget().md +│ ├── Controller-IsLookInputIgnored.md +│ ├── Controller-IsMoveInputIgnored.md +│ ├── Controller-IsPlayerController.md +│ ├── Controller-LineOfSightTo(Actor_Vector3_bool).md +│ ├── Controller-Possess(Pawn).md +│ ├── Controller-ResetIgnoreLookInput().md +│ ├── Controller-ResetIgnoreMoveInput().md +│ ├── Controller-SetControlRotation(Quaternion).md +│ ├── Controller-SetIgnoreLookInput(bool).md +│ ├── Controller-SetIgnoreMoveInput(bool).md +│ ├── Controller-SetInitialLocationAndRotation(Vector3_Quaternion).md +│ ├── Controller-Unpossess().md +│ ├── Controller.md +│ ├── ControllerHand.md +│ ├── Debug-AddOnScreenMessage(int_float_Color_string).md +│ ├── Debug-ClearOnScreenMessages().md +│ ├── Debug-DrawBox(Vector3_Vector3_Quaternion_Color_bool_float_byte_float).md +│ ├── Debug-DrawCapsule(Vector3_float_float_Quaternion_Color_bool_float_byte_float).md +│ ├── Debug-DrawCone(Vector3_Vector3_float_float_float_int_Color_bool_float_byte_float).md +│ ├── Debug-DrawCylinder(Vector3_Vector3_float_int_Color_bool_float_byte_float).md +│ ├── Debug-DrawLine(Vector3_Vector3_Color_bool_float_byte_float).md +│ ├── Debug-DrawPoint(Vector3_float_Color_bool_float_byte).md +│ ├── Debug-DrawSphere(Vector3_float_int_Color_bool_float_byte_float).md +│ ├── Debug-Exception(Exception).md +│ ├── Debug-FlushPersistentLines().md +│ ├── Debug-Log(LogLevel_string).md +│ ├── Debug.md +│ ├── DetachmentTransformRule.md +│ ├── DirectionalLight-DirectionalLight(string_Blueprint).md +│ ├── DirectionalLight.md +│ ├── DirectionalLightComponent-DirectionalLightComponent(Actor_string_bool_Blueprint).md +│ ├── DirectionalLightComponent.md +│ ├── Engine-AddActionMapping(string_string_bool_bool_bool_bool).md +│ ├── Engine-AddAxisMapping(string_string_float).md +│ ├── Engine-DelayGarbageCollection().md +│ ├── Engine-ForceGarbageCollection(bool).md +│ ├── Engine-FrameNumber.md +│ ├── Engine-GetScreenResolution().md +│ ├── Engine-GetScreenResolution(Vector2).md +│ ├── Engine-GetViewportSize().md +│ ├── Engine-GetViewportSize(Vector2).md +│ ├── Engine-IsEditor.md +│ ├── Engine-IsExitRequested.md +│ ├── Engine-IsForegroundWindow.md +│ ├── Engine-IsSplitScreen.md +│ ├── Engine-MaxFPS.md +│ ├── Engine-NetMode.md +│ ├── Engine-SetTitle(string).md +│ ├── Engine-Version.md +│ ├── Engine-WindowMode.md +│ ├── Engine.md +│ ├── Font-Equals(Font).md +│ ├── Font-GetHashCode().md +│ ├── Font-GetStringSize(string_int_int).md +│ ├── Font-IsCreated.md +│ ├── Font-Load(string).md +│ ├── Font.md +│ ├── GameModeBase-SwapPlayerControllers(PlayerController_PlayerController).md +│ ├── GameModeBase-UseSeamlessTravel.md +│ ├── GameModeBase.md +│ ├── HeadMountedDisplay-DeviceName.md +│ ├── HeadMountedDisplay-Enabled.md +│ ├── HeadMountedDisplay-IsConnected.md +│ ├── HeadMountedDisplay-LowPersistenceMode.md +│ ├── HeadMountedDisplay.md +│ ├── HierarchicalInstancedStaticMeshComponent-DisableCollision.md +│ ├── HierarchicalInstancedStaticMeshComponent-HierarchicalInstancedStaticMeshComponent(Actor_string_bool_Blueprint).md +│ ├── HierarchicalInstancedStaticMeshComponent.md +│ ├── Hit-BlockingHit.md +│ ├── Hit-Distance.md +│ ├── Hit-Equals(Hit).md +│ ├── Hit-Equals(object).md +│ ├── Hit-GetActor().md +│ ├── Hit-GetHashCode().md +│ ├── Hit-ImpactLocation.md +│ ├── Hit-ImpactNormal.md +│ ├── Hit-Location.md +│ ├── Hit-Normal.md +│ ├── Hit-PenetrationDepth.md +│ ├── Hit-StartPenetrating.md +│ ├── Hit-Time.md +│ ├── Hit-ToString().md +│ ├── Hit-ToString(IFormatProvider).md +│ ├── Hit-TraceEnd.md +│ ├── Hit-TraceStart.md +│ ├── Hit-op_Equality(Hit_Hit).md +│ ├── Hit-op_Implicitstring(Hit).md +│ ├── Hit-op_Inequality(Hit_Hit).md +│ ├── Hit.md +│ ├── HorizontalTextAligment.md +│ ├── InputAxisDelegate(float).md +│ ├── InputComponent-ActionBindingsNumber.md +│ ├── InputComponent-BindAction(string_InputEvent_InputDelegate_bool).md +│ ├── InputComponent-BindAxis(string_InputAxisDelegate_bool).md +│ ├── InputComponent-BlockInput.md +│ ├── InputComponent-ClearActionBindings().md +│ ├── InputComponent-HasBindings.md +│ ├── InputComponent-Priority.md +│ ├── InputComponent-RemoveActionBinding(string_InputEvent).md +│ ├── InputComponent.md +│ ├── InputDelegate().md +│ ├── InputEvent.md +│ ├── InstancedStaticMeshComponent-AddInstance(Transform).md +│ ├── InstancedStaticMeshComponent-AddInstances(Transform--).md +│ ├── InstancedStaticMeshComponent-BatchUpdateInstanceTransforms(int_Transform--_bool_bool_bool).md +│ ├── InstancedStaticMeshComponent-ClearInstances().md +│ ├── InstancedStaticMeshComponent-GetInstanceTransform(int_Transform_bool).md +│ ├── InstancedStaticMeshComponent-InstanceCount.md +│ ├── InstancedStaticMeshComponent-InstancedStaticMeshComponent(Actor_string_bool_Blueprint).md +│ ├── InstancedStaticMeshComponent-RemoveInstance(int).md +│ ├── InstancedStaticMeshComponent-UpdateInstanceTransform(int_Transform_bool_bool_bool).md +│ ├── InstancedStaticMeshComponent.md +│ ├── Keys-A.md +│ ├── Keys-Add.md +│ ├── Keys-Ampersand.md +│ ├── Keys-Android-Back.md +│ ├── Keys-Android-Menu.md +│ ├── Keys-Android-VolumeDown.md +│ ├── Keys-Android-VolumeUp.md +│ ├── Keys-Android.md +│ ├── Keys-AnyKey.md +│ ├── Keys-Apostrophe.md +│ ├── Keys-Asterix.md +│ ├── Keys-B.md +│ ├── Keys-BackSpace.md +│ ├── Keys-Backslash.md +│ ├── Keys-C.md +│ ├── Keys-CapsLock.md +│ ├── Keys-Caret.md +│ ├── Keys-Colon.md +│ ├── Keys-Comma.md +│ ├── Keys-D.md +│ ├── Keys-Daydream-LeftSelectClick.md +│ ├── Keys-Daydream-LeftTrackpadClick.md +│ ├── Keys-Daydream-LeftTrackpadTouch.md +│ ├── Keys-Daydream-LeftTrackpadX.md +│ ├── Keys-Daydream-LeftTrackpadY.md +│ ├── Keys-Daydream-RightSelectClick.md +│ ├── Keys-Daydream-RightTrackpadClick.md +│ ├── Keys-Daydream-RightTrackpadTouch.md +│ ├── Keys-Daydream-RightTrackpadX.md +│ ├── Keys-Daydream-RightTrackpadY.md +│ ├── Keys-Daydream.md +│ ├── Keys-Decimal.md +│ ├── Keys-Delete.md +│ ├── Keys-Divide.md +│ ├── Keys-Dollar.md +│ ├── Keys-Down.md +│ ├── Keys-E.md +│ ├── Keys-Eight.md +│ ├── Keys-End.md +│ ├── Keys-Enter.md +│ ├── Keys-Equal.md +│ ├── Keys-Escape.md +│ ├── Keys-Exclamation.md +│ ├── Keys-F.md +│ ├── Keys-F1.md +│ ├── Keys-F10.md +│ ├── Keys-F11.md +│ ├── Keys-F12.md +│ ├── Keys-F2.md +│ ├── Keys-F3.md +│ ├── Keys-F4.md +│ ├── Keys-F5.md +│ ├── Keys-F6.md +│ ├── Keys-F7.md +│ ├── Keys-F8.md +│ ├── Keys-F9.md +│ ├── Keys-Five.md +│ ├── Keys-Four.md +│ ├── Keys-G.md +│ ├── Keys-Gamepad-DPadDown.md +│ ├── Keys-Gamepad-DPadLeft.md +│ ├── Keys-Gamepad-DPadRight.md +│ ├── Keys-Gamepad-DPadUp.md +│ ├── Keys-Gamepad-FaceButtonBottom.md +│ ├── Keys-Gamepad-FaceButtonLeft.md +│ ├── Keys-Gamepad-FaceButtonRight.md +│ ├── Keys-Gamepad-FaceButtonTop.md +│ ├── Keys-Gamepad-LeftShoulder.md +│ ├── Keys-Gamepad-LeftThumbstick.md +│ ├── Keys-Gamepad-LeftTrigger.md +│ ├── Keys-Gamepad-LeftTriggerAxis.md +│ ├── Keys-Gamepad-LeftX.md +│ ├── Keys-Gamepad-LeftY.md +│ ├── Keys-Gamepad-RightShoulder.md +│ ├── Keys-Gamepad-RightThumbstick.md +│ ├── Keys-Gamepad-RightTrigger.md +│ ├── Keys-Gamepad-RightTriggerAxis.md +│ ├── Keys-Gamepad-RightX.md +│ ├── Keys-Gamepad-RightY.md +│ ├── Keys-Gamepad-SpecialLeft.md +│ ├── Keys-Gamepad-SpecialLeftX.md +│ ├── Keys-Gamepad-SpecialLeftY.md +│ ├── Keys-Gamepad-SpecialRight.md +│ ├── Keys-Gamepad.md +│ ├── Keys-Gesture-Flick.md +│ ├── Keys-Gesture-Pinch.md +│ ├── Keys-Gesture-Rotate.md +│ ├── Keys-Gesture.md +│ ├── Keys-H.md +│ ├── Keys-Home.md +│ ├── Keys-Hyphen.md +│ ├── Keys-I.md +│ ├── Keys-Insert.md +│ ├── Keys-Invalid.md +│ ├── Keys-J.md +│ ├── Keys-K.md +│ ├── Keys-L.md +│ ├── Keys-Left.md +│ ├── Keys-LeftAlt.md +│ ├── Keys-LeftBracket.md +│ ├── Keys-LeftCommand.md +│ ├── Keys-LeftControl.md +│ ├── Keys-LeftMouseButton.md +│ ├── Keys-LeftParantheses.md +│ ├── Keys-LeftShift.md +│ ├── Keys-M.md +│ ├── Keys-MiddleMouseButton.md +│ ├── Keys-MixedReality-LeftGripClick.md +│ ├── Keys-MixedReality-LeftMenuClick.md +│ ├── Keys-MixedReality-LeftThumbstickClick.md +│ ├── Keys-MixedReality-LeftThumbstickDown.md +│ ├── Keys-MixedReality-LeftThumbstickLeft.md +│ ├── Keys-MixedReality-LeftThumbstickRight.md +│ ├── Keys-MixedReality-LeftThumbstickUp.md +│ ├── Keys-MixedReality-LeftThumbstickX.md +│ ├── Keys-MixedReality-LeftThumbstickY.md +│ ├── Keys-MixedReality-LeftTrackpadClick.md +│ ├── Keys-MixedReality-LeftTrackpadDown.md +│ ├── Keys-MixedReality-LeftTrackpadLeft.md +│ ├── Keys-MixedReality-LeftTrackpadRight.md +│ ├── Keys-MixedReality-LeftTrackpadTouch.md +│ ├── Keys-MixedReality-LeftTrackpadUp.md +│ ├── Keys-MixedReality-LeftTrackpadX.md +│ ├── Keys-MixedReality-LeftTrackpadY.md +│ ├── Keys-MixedReality-LeftTriggerAxis.md +│ ├── Keys-MixedReality-LeftTriggerClick.md +│ ├── Keys-MixedReality-RightGripClick.md +│ ├── Keys-MixedReality-RightMenuClick.md +│ ├── Keys-MixedReality-RightThumbstickClick.md +│ ├── Keys-MixedReality-RightThumbstickDown.md +│ ├── Keys-MixedReality-RightThumbstickLeft.md +│ ├── Keys-MixedReality-RightThumbstickRight.md +│ ├── Keys-MixedReality-RightThumbstickUp.md +│ ├── Keys-MixedReality-RightThumbstickX.md +│ ├── Keys-MixedReality-RightThumbstickY.md +│ ├── Keys-MixedReality-RightTrackpadClick.md +│ ├── Keys-MixedReality-RightTrackpadDown.md +│ ├── Keys-MixedReality-RightTrackpadLeft.md +│ ├── Keys-MixedReality-RightTrackpadRight.md +│ ├── Keys-MixedReality-RightTrackpadTouch.md +│ ├── Keys-MixedReality-RightTrackpadUp.md +│ ├── Keys-MixedReality-RightTrackpadX.md +│ ├── Keys-MixedReality-RightTrackpadY.md +│ ├── Keys-MixedReality-RightTriggerAxis.md +│ ├── Keys-MixedReality-RightTriggerClick.md +│ ├── Keys-MixedReality.md +│ ├── Keys-MouseScrollDown.md +│ ├── Keys-MouseScrollUp.md +│ ├── Keys-MouseWheelAxis.md +│ ├── Keys-MouseX.md +│ ├── Keys-MouseY.md +│ ├── Keys-Multiply.md +│ ├── Keys-N.md +│ ├── Keys-Nine.md +│ ├── Keys-NumLock.md +│ ├── Keys-NumPadEight.md +│ ├── Keys-NumPadFive.md +│ ├── Keys-NumPadFour.md +│ ├── Keys-NumPadNine.md +│ ├── Keys-NumPadOne.md +│ ├── Keys-NumPadSeven.md +│ ├── Keys-NumPadSix.md +│ ├── Keys-NumPadThree.md +│ ├── Keys-NumPadTwo.md +│ ├── Keys-NumPadZero.md +│ ├── Keys-O.md +│ ├── Keys-OculusGo-LeftBackClick.md +│ ├── Keys-OculusGo-LeftSystemClick.md +│ ├── Keys-OculusGo-LeftTrackpadClick.md +│ ├── Keys-OculusGo-LeftTrackpadTouch.md +│ ├── Keys-OculusGo-LeftTrackpadX.md +│ ├── Keys-OculusGo-LeftTrackpadY.md +│ ├── Keys-OculusGo-LeftTriggerClick.md +│ ├── Keys-OculusGo-RightBackClick.md +│ ├── Keys-OculusGo-RightSystemClick.md +│ ├── Keys-OculusGo-RightTrackpadClick.md +│ ├── Keys-OculusGo-RightTrackpadTouch.md +│ ├── Keys-OculusGo-RightTrackpadX.md +│ ├── Keys-OculusGo-RightTrackpadY.md +│ ├── Keys-OculusGo-RightTriggerClick.md +│ ├── Keys-OculusGo.md +│ ├── Keys-OculusTouch-LeftGripAxis.md +│ ├── Keys-OculusTouch-LeftGripClick.md +│ ├── Keys-OculusTouch-LeftMenuClick.md +│ ├── Keys-OculusTouch-LeftThumbstickClick.md +│ ├── Keys-OculusTouch-LeftThumbstickDown.md +│ ├── Keys-OculusTouch-LeftThumbstickLeft.md +│ ├── Keys-OculusTouch-LeftThumbstickRight.md +│ ├── Keys-OculusTouch-LeftThumbstickTouch.md +│ ├── Keys-OculusTouch-LeftThumbstickUp.md +│ ├── Keys-OculusTouch-LeftThumbstickX.md +│ ├── Keys-OculusTouch-LeftThumbstickY.md +│ ├── Keys-OculusTouch-LeftTriggerAxis.md +│ ├── Keys-OculusTouch-LeftTriggerClick.md +│ ├── Keys-OculusTouch-LeftTriggerTouch.md +│ ├── Keys-OculusTouch-LeftXClick.md +│ ├── Keys-OculusTouch-LeftXTouch.md +│ ├── Keys-OculusTouch-LeftYClick.md +│ ├── Keys-OculusTouch-LeftYTouch.md +│ ├── Keys-OculusTouch-RightAClick.md +│ ├── Keys-OculusTouch-RightATouch.md +│ ├── Keys-OculusTouch-RightBClick.md +│ ├── Keys-OculusTouch-RightBTouch.md +│ ├── Keys-OculusTouch-RightGripAxis.md +│ ├── Keys-OculusTouch-RightGripClick.md +│ ├── Keys-OculusTouch-RightSystemClick.md +│ ├── Keys-OculusTouch-RightThumbstickClick.md +│ ├── Keys-OculusTouch-RightThumbstickDown.md +│ ├── Keys-OculusTouch-RightThumbstickLeft.md +│ ├── Keys-OculusTouch-RightThumbstickRight.md +│ ├── Keys-OculusTouch-RightThumbstickTouch.md +│ ├── Keys-OculusTouch-RightThumbstickUp.md +│ ├── Keys-OculusTouch-RightThumbstickX.md +│ ├── Keys-OculusTouch-RightThumbstickY.md +│ ├── Keys-OculusTouch-RightTriggerAxis.md +│ ├── Keys-OculusTouch-RightTriggerClick.md +│ ├── Keys-OculusTouch-RightTriggerTouch.md +│ ├── Keys-OculusTouch.md +│ ├── Keys-One.md +│ ├── Keys-P.md +│ ├── Keys-PS4-Special.md +│ ├── Keys-PS4.md +│ ├── Keys-PageDown.md +│ ├── Keys-PageUp.md +│ ├── Keys-Pause.md +│ ├── Keys-Period.md +│ ├── Keys-Platform-Delete.md +│ ├── Keys-Platform.md +│ ├── Keys-Q.md +│ ├── Keys-Quote.md +│ ├── Keys-R.md +│ ├── Keys-Right.md +│ ├── Keys-RightAlt.md +│ ├── Keys-RightBracket.md +│ ├── Keys-RightCommand.md +│ ├── Keys-RightControl.md +│ ├── Keys-RightMouseButton.md +│ ├── Keys-RightParantheses.md +│ ├── Keys-RightShift.md +│ ├── Keys-S.md +│ ├── Keys-ScrollLock.md +│ ├── Keys-Section.md +│ ├── Keys-Semicolon.md +│ ├── Keys-Seven.md +│ ├── Keys-Six.md +│ ├── Keys-Slash.md +│ ├── Keys-SpaceBar.md +│ ├── Keys-Steam-BackLeft.md +│ ├── Keys-Steam-BackRight.md +│ ├── Keys-Steam-TouchOne.md +│ ├── Keys-Steam-TouchThree.md +│ ├── Keys-Steam-TouchTwo.md +│ ├── Keys-Steam-TouchZero.md +│ ├── Keys-Steam.md +│ ├── Keys-Subtract.md +│ ├── Keys-T.md +│ ├── Keys-Tab.md +│ ├── Keys-Three.md +│ ├── Keys-ThumbMouseButton.md +│ ├── Keys-ThumbMouseButton2.md +│ ├── Keys-Tilde.md +│ ├── Keys-Two.md +│ ├── Keys-U.md +│ ├── Keys-Underscore.md +│ ├── Keys-Up.md +│ ├── Keys-V.md +│ ├── Keys-ValveIndex-LeftAClick.md +│ ├── Keys-ValveIndex-LeftATouch.md +│ ├── Keys-ValveIndex-LeftBClick.md +│ ├── Keys-ValveIndex-LeftBTouch.md +│ ├── Keys-ValveIndex-LeftGripAxis.md +│ ├── Keys-ValveIndex-LeftGripClick.md +│ ├── Keys-ValveIndex-LeftGripForce.md +│ ├── Keys-ValveIndex-LeftSystemClick.md +│ ├── Keys-ValveIndex-LeftSystemTouch.md +│ ├── Keys-ValveIndex-LeftThumbstickClick.md +│ ├── Keys-ValveIndex-LeftThumbstickDown.md +│ ├── Keys-ValveIndex-LeftThumbstickLeft.md +│ ├── Keys-ValveIndex-LeftThumbstickRight.md +│ ├── Keys-ValveIndex-LeftThumbstickTouch.md +│ ├── Keys-ValveIndex-LeftThumbstickUp.md +│ ├── Keys-ValveIndex-LeftThumbstickX.md +│ ├── Keys-ValveIndex-LeftThumbstickY.md +│ ├── Keys-ValveIndex-LeftTrackpadClick.md +│ ├── Keys-ValveIndex-LeftTrackpadDown.md +│ ├── Keys-ValveIndex-LeftTrackpadForce.md +│ ├── Keys-ValveIndex-LeftTrackpadLeft.md +│ ├── Keys-ValveIndex-LeftTrackpadRight.md +│ ├── Keys-ValveIndex-LeftTrackpadTouch.md +│ ├── Keys-ValveIndex-LeftTrackpadUp.md +│ ├── Keys-ValveIndex-LeftTrackpadX.md +│ ├── Keys-ValveIndex-LeftTrackpadY.md +│ ├── Keys-ValveIndex-LeftTriggerAxis.md +│ ├── Keys-ValveIndex-LeftTriggerClick.md +│ ├── Keys-ValveIndex-LeftTriggerTouch.md +│ ├── Keys-ValveIndex-RightAClick.md +│ ├── Keys-ValveIndex-RightATouch.md +│ ├── Keys-ValveIndex-RightBClick.md +│ ├── Keys-ValveIndex-RightBTouch.md +│ ├── Keys-ValveIndex-RightGripAxis.md +│ ├── Keys-ValveIndex-RightGripClick.md +│ ├── Keys-ValveIndex-RightGripForce.md +│ ├── Keys-ValveIndex-RightSystemClick.md +│ ├── Keys-ValveIndex-RightSystemTouch.md +│ ├── Keys-ValveIndex-RightThumbstickClick.md +│ ├── Keys-ValveIndex-RightThumbstickDown.md +│ ├── Keys-ValveIndex-RightThumbstickLeft.md +│ ├── Keys-ValveIndex-RightThumbstickRight.md +│ ├── Keys-ValveIndex-RightThumbstickTouch.md +│ ├── Keys-ValveIndex-RightThumbstickUp.md +│ ├── Keys-ValveIndex-RightThumbstickX.md +│ ├── Keys-ValveIndex-RightThumbstickY.md +│ ├── Keys-ValveIndex-RightTrackpadClick.md +│ ├── Keys-ValveIndex-RightTrackpadDown.md +│ ├── Keys-ValveIndex-RightTrackpadForce.md +│ ├── Keys-ValveIndex-RightTrackpadLeft.md +│ ├── Keys-ValveIndex-RightTrackpadRight.md +│ ├── Keys-ValveIndex-RightTrackpadTouch.md +│ ├── Keys-ValveIndex-RightTrackpadUp.md +│ ├── Keys-ValveIndex-RightTrackpadX.md +│ ├── Keys-ValveIndex-RightTrackpadY.md +│ ├── Keys-ValveIndex-RightTriggerAxis.md +│ ├── Keys-ValveIndex-RightTriggerClick.md +│ ├── Keys-ValveIndex-RightTriggerTouch.md +│ ├── Keys-ValveIndex.md +│ ├── Keys-Virtual-Acceleration.md +│ ├── Keys-Virtual-Accept.md +│ ├── Keys-Virtual-Back.md +│ ├── Keys-Virtual-Gravity.md +│ ├── Keys-Virtual-LeftStickDown.md +│ ├── Keys-Virtual-LeftStickLeft.md +│ ├── Keys-Virtual-LeftStickRight.md +│ ├── Keys-Virtual-LeftStickUp.md +│ ├── Keys-Virtual-RightStickDown.md +│ ├── Keys-Virtual-RightStickLeft.md +│ ├── Keys-Virtual-RightStickRight.md +│ ├── Keys-Virtual-RightStickUp.md +│ ├── Keys-Virtual-RotationRate.md +│ ├── Keys-Virtual-Tilt.md +│ ├── Keys-Virtual.md +│ ├── Keys-Vive-LeftGripClick.md +│ ├── Keys-Vive-LeftMenuClick.md +│ ├── Keys-Vive-LeftSystemClick.md +│ ├── Keys-Vive-LeftTrackpadClick.md +│ ├── Keys-Vive-LeftTrackpadDown.md +│ ├── Keys-Vive-LeftTrackpadLeft.md +│ ├── Keys-Vive-LeftTrackpadRight.md +│ ├── Keys-Vive-LeftTrackpadTouch.md +│ ├── Keys-Vive-LeftTrackpadUp.md +│ ├── Keys-Vive-LeftTrackpadX.md +│ ├── Keys-Vive-LeftTrackpadY.md +│ ├── Keys-Vive-LeftTriggerAxis.md +│ ├── Keys-Vive-LeftTriggerClick.md +│ ├── Keys-Vive-RightGripClick.md +│ ├── Keys-Vive-RightMenuClick.md +│ ├── Keys-Vive-RightSystemClick.md +│ ├── Keys-Vive-RightTrackpadClick.md +│ ├── Keys-Vive-RightTrackpadDown.md +│ ├── Keys-Vive-RightTrackpadLeft.md +│ ├── Keys-Vive-RightTrackpadRight.md +│ ├── Keys-Vive-RightTrackpadTouch.md +│ ├── Keys-Vive-RightTrackpadUp.md +│ ├── Keys-Vive-RightTrackpadX.md +│ ├── Keys-Vive-RightTrackpadY.md +│ ├── Keys-Vive-RightTriggerAxis.md +│ ├── Keys-Vive-RightTriggerClick.md +│ ├── Keys-Vive.md +│ ├── Keys-W.md +│ ├── Keys-X.md +│ ├── Keys-Xbox-GlobalBack.md +│ ├── Keys-Xbox-GlobalMenu.md +│ ├── Keys-Xbox-GlobalPause.md +│ ├── Keys-Xbox-GlobalPlay.md +│ ├── Keys-Xbox-GlobalView.md +│ ├── Keys-Xbox.md +│ ├── Keys-Y.md +│ ├── Keys-Z.md +│ ├── Keys-Zero.md +│ ├── Keys.md +│ ├── LevelScript.md +│ ├── Light.md +│ ├── LightComponent-LightComponent(Actor_string_bool_Blueprint).md +│ ├── LightComponent-SetIntensity(float).md +│ ├── LightComponent-SetLightColor(LinearColor).md +│ ├── LightComponent.md +│ ├── LightComponentBase-CastShadows.md +│ ├── LightComponentBase-Intensity.md +│ ├── LightComponentBase.md +│ ├── LinearColor-A.md +│ ├── LinearColor-Add(LinearColor_LinearColor).md +│ ├── LinearColor-B.md +│ ├── LinearColor-Black.md +│ ├── LinearColor-Blue.md +│ ├── LinearColor-Divide(LinearColor_LinearColor).md +│ ├── LinearColor-Equals(LinearColor).md +│ ├── LinearColor-Equals(object).md +│ ├── LinearColor-FromColor(Color).md +│ ├── LinearColor-G.md +│ ├── LinearColor-GetHashCode().md +│ ├── LinearColor-Green.md +│ ├── LinearColor-Grey.md +│ ├── LinearColor-Lerp(LinearColor_LinearColor_float).md +│ ├── LinearColor-LinearColor(Color).md +│ ├── LinearColor-LinearColor(Vector3_float).md +│ ├── LinearColor-LinearColor(Vector4).md +│ ├── LinearColor-LinearColor(float--).md +│ ├── LinearColor-LinearColor(float_float_float_float).md +│ ├── LinearColor-Multiply(LinearColor_LinearColor).md +│ ├── LinearColor-R.md +│ ├── LinearColor-Red.md +│ ├── LinearColor-Subtract(LinearColor_LinearColor).md +│ ├── LinearColor-ToArray().md +│ ├── LinearColor-ToString().md +│ ├── LinearColor-ToString(IFormatProvider).md +│ ├── LinearColor-ToVector3().md +│ ├── LinearColor-ToVector4().md +│ ├── LinearColor-White.md +│ ├── LinearColor-Yellow.md +│ ├── LinearColor-op_Addition(LinearColor_LinearColor).md +│ ├── LinearColor-op_Division(float_LinearColor).md +│ ├── LinearColor-op_Equality(LinearColor_LinearColor).md +│ ├── LinearColor-op_ImplicitLinearColor(Color).md +│ ├── LinearColor-op_Implicitstring(LinearColor).md +│ ├── LinearColor-op_Inequality(LinearColor_LinearColor).md +│ ├── LinearColor-op_Multiply(float_LinearColor).md +│ ├── LinearColor-op_Subtraction(LinearColor_LinearColor).md +│ ├── LinearColor-this-int-.md +│ ├── LinearColor.md +│ ├── LogLevel.md +│ ├── Material-IsDefaultMaterial.md +│ ├── Material-Load(string).md +│ ├── Material.md +│ ├── MaterialInstance-GetParent().md +│ ├── MaterialInstance-IsChildOf(MaterialInterface).md +│ ├── MaterialInstance.md +│ ├── MaterialInstanceDynamic-ClearParameterValues().md +│ ├── MaterialInstanceDynamic-SetScalarParameterValue(string_float).md +│ ├── MaterialInstanceDynamic-SetTextureParameterValue(string_Texture).md +│ ├── MaterialInstanceDynamic-SetVectorParameterValue(string_LinearColor).md +│ ├── MaterialInstanceDynamic.md +│ ├── MaterialInterface-Equals(MaterialInterface).md +│ ├── MaterialInterface-GetHashCode().md +│ ├── MaterialInterface-IsCreated.md +│ ├── MaterialInterface-IsTwoSided.md +│ ├── MaterialInterface.md +│ ├── Maths-Angle(Quaternion_Quaternion).md +│ ├── Maths-Angle(Vector2_Vector2).md +│ ├── Maths-Angle(Vector3_Vector3).md +│ ├── Maths-AngleAxis(float_Vector3).md +│ ├── Maths-ClampMagnitude(Vector2_float).md +│ ├── Maths-ClampMagnitude(Vector3_float).md +│ ├── Maths-CreateFromYawPitchRoll(float_float_float).md +│ ├── Maths-Damp(Quaternion_Quaternion_float_float).md +│ ├── Maths-Damp(Vector2_Vector2_float_float).md +│ ├── Maths-Damp(Vector3_Vector3_float_float).md +│ ├── Maths-Damp(double_double_double_double).md +│ ├── Maths-Damp(float_float_float_float).md +│ ├── Maths-DegToRadF.md +│ ├── Maths-DeltaAngle(double_double).md +│ ├── Maths-DeltaAngle(float_float).md +│ ├── Maths-Dot(double_double).md +│ ├── Maths-Dot(float_float).md +│ ├── Maths-Euler(Vector3).md +│ ├── Maths-Euler(float_float_float).md +│ ├── Maths-Fraction(double).md +│ ├── Maths-Fraction(float).md +│ ├── Maths-FromToRotation(Vector3_Vector3).md +│ ├── Maths-InverseLerp(double_double_double).md +│ ├── Maths-InverseLerp(float_float_float).md +│ ├── Maths-Lerp(double_double_double).md +│ ├── Maths-Lerp(float_float_float).md +│ ├── Maths-LerpAngle(double_double_double).md +│ ├── Maths-LerpAngle(float_float_float).md +│ ├── Maths-Magnitude(Vector2).md +│ ├── Maths-Magnitude(Vector3).md +│ ├── Maths-Magnitude(Vector4).md +│ ├── Maths-MoveTowards(Vector2_Vector2_float).md +│ ├── Maths-MoveTowards(Vector3_Vector3_float).md +│ ├── Maths-MoveTowards(double_double_double).md +│ ├── Maths-MoveTowards(float_float_float).md +│ ├── Maths-MoveTowardsAngle(double_double_double).md +│ ├── Maths-MoveTowardsAngle(float_float_float).md +│ ├── Maths-NextPowerOfTwo(double).md +│ ├── Maths-NextPowerOfTwo(float).md +│ ├── Maths-Perpendicular(Vector2).md +│ ├── Maths-PreviousPowerOfTwo(double).md +│ ├── Maths-PreviousPowerOfTwo(float).md +│ ├── Maths-Project(Vector3_Vector3).md +│ ├── Maths-ProjectOnPlane(Vector3_Vector3).md +│ ├── Maths-RadToDegF.md +│ ├── Maths-Refract(Vector2_Vector2_float).md +│ ├── Maths-Refract(Vector3_Vector3_float).md +│ ├── Maths-Refract(Vector4_Vector4_float).md +│ ├── Maths-Repeat(double_double).md +│ ├── Maths-Repeat(float_float).md +│ ├── Maths-RotateTowards(Quaternion_Quaternion_float).md +│ ├── Maths-Saturate(double).md +│ ├── Maths-Saturate(float).md +│ ├── Maths-SignedAngle(Vector3_Vector3_Vector3).md +│ ├── Maths-SmoothStep(double).md +│ ├── Maths-SmoothStep(float).md +│ ├── Maths-SmootherStep(double).md +│ ├── Maths-SmootherStep(float).md +│ ├── Maths-SquareMagnitude(Vector2).md +│ ├── Maths-SquareMagnitude(Vector3).md +│ ├── Maths-SquareMagnitude(Vector4).md +│ ├── Maths.md +│ ├── MeshComponent-GetMaterialIndex(string).md +│ ├── MeshComponent-IsValidMaterialSlotName(string).md +│ ├── MeshComponent.md +│ ├── MotionControllerComponent-DisableLowLatencyUpdate.md +│ ├── MotionControllerComponent-DisplayDeviceModel.md +│ ├── MotionControllerComponent-IsTracked.md +│ ├── MotionControllerComponent-MotionControllerComponent(Actor_string_bool_Blueprint).md +│ ├── MotionControllerComponent-SetAssociatedPlayerIndex(int).md +│ ├── MotionControllerComponent-SetCustomDisplayMesh(StaticMesh).md +│ ├── MotionControllerComponent-SetDisplayModelSource(string).md +│ ├── MotionControllerComponent-SetTrackingMotionSource(string).md +│ ├── MotionControllerComponent-TrackingSource.md +│ ├── MotionControllerComponent.md +│ ├── MovementComponent-ConstrainDirectionToPlane(Vector3_Vector3).md +│ ├── MovementComponent-ConstrainLocationToPlane(Vector3_Vector3).md +│ ├── MovementComponent-ConstrainNormalToPlane(Vector3_Vector3).md +│ ├── MovementComponent-ConstrainToPlane.md +│ ├── MovementComponent-GetGravity().md +│ ├── MovementComponent-GetMaxSpeed().md +│ ├── MovementComponent-GetPlaneConstraintNormal().md +│ ├── MovementComponent-GetPlaneConstraintNormal(Vector3).md +│ ├── MovementComponent-GetPlaneConstraintOrigin().md +│ ├── MovementComponent-GetPlaneConstraintOrigin(Vector3).md +│ ├── MovementComponent-GetVelocity().md +│ ├── MovementComponent-GetVelocity(Vector3).md +│ ├── MovementComponent-IsExceedingMaxSpeed(float).md +│ ├── MovementComponent-IsInWater.md +│ ├── MovementComponent-PlaneConstraint.md +│ ├── MovementComponent-SetPlaneConstraintFromVectors(Vector3_Vector3).md +│ ├── MovementComponent-SetPlaneConstraintNormal(Vector3).md +│ ├── MovementComponent-SetPlaneConstraintOrigin(Vector3).md +│ ├── MovementComponent-SetVelocity(Vector3).md +│ ├── MovementComponent-SnapToPlaneAtStart.md +│ ├── MovementComponent-StopMovement().md +│ ├── MovementComponent-UpdateOnlyIfRendered.md +│ ├── MovementComponent.md +│ ├── NetMode.md +│ ├── ObjectReference-Equals(ObjectReference).md +│ ├── ObjectReference-Equals(object).md +│ ├── ObjectReference-GetHashCode().md +│ ├── ObjectReference-ID.md +│ ├── ObjectReference-IsCreated.md +│ ├── ObjectReference-Name.md +│ ├── ObjectReference-ToActor-T-().md +│ ├── ObjectReference-ToComponent-T-().md +│ ├── ObjectReference-op_Equality(ObjectReference_ObjectReference).md +│ ├── ObjectReference-op_Inequality(ObjectReference_ObjectReference).md +│ ├── ObjectReference.md +│ ├── Pawn-AddControllerPitchInput(float).md +│ ├── Pawn-AddControllerRollInput(float).md +│ ├── Pawn-AddControllerYawInput(float).md +│ ├── Pawn-AddMovementInput(Vector3_float_bool).md +│ ├── Pawn-AutoPossessAI.md +│ ├── Pawn-AutoPossessPlayer.md +│ ├── Pawn-GetAIController().md +│ ├── Pawn-GetGravityDirection().md +│ ├── Pawn-GetGravityDirection(Vector3).md +│ ├── Pawn-GetPlayerController().md +│ ├── Pawn-IsControlled.md +│ ├── Pawn-IsPlayerControlled.md +│ ├── Pawn-Pawn(string_Blueprint).md +│ ├── Pawn-UseControllerRotationPitch.md +│ ├── Pawn-UseControllerRotationRoll.md +│ ├── Pawn-UseControllerRotationYaw.md +│ ├── Pawn.md +│ ├── PixelFormat.md +│ ├── PlaneConstraintAxis.md +│ ├── Player-Equals(Player).md +│ ├── Player-GetHashCode().md +│ ├── Player-GetPlayerController().md +│ ├── Player-IsCreated.md +│ ├── Player.md +│ ├── PlayerController-AddPitchInput(float).md +│ ├── PlayerController-AddRollInput(float).md +│ ├── PlayerController-AddYawInput(float).md +│ ├── PlayerController-ConsoleCommand(string_bool).md +│ ├── PlayerController-EnableClickEvents.md +│ ├── PlayerController-EnableMouseOverEvents.md +│ ├── PlayerController-GetHitResultAtScreenPosition(Vector2_CollisionChannel_Hit_bool).md +│ ├── PlayerController-GetHitResultUnderCursor(CollisionChannel_Hit_bool).md +│ ├── PlayerController-GetMousePosition(float_float).md +│ ├── PlayerController-GetPlayer().md +│ ├── PlayerController-GetPlayerInput().md +│ ├── PlayerController-IsPaused.md +│ ├── PlayerController-PlayerController(string_Blueprint).md +│ ├── PlayerController-SetMousePosition(float_float).md +│ ├── PlayerController-SetPause(bool).md +│ ├── PlayerController-SetViewTarget(Actor).md +│ ├── PlayerController-SetViewTargetWithBlend(Actor_float_float_BlendType_bool).md +│ ├── PlayerController-ShowMouseCursor.md +│ ├── PlayerController.md +│ ├── PlayerInput-AddActionMapping(string_string_bool_bool_bool_bool).md +│ ├── PlayerInput-AddAxisMapping(string_string_float).md +│ ├── PlayerInput-Equals(PlayerInput).md +│ ├── PlayerInput-GetHashCode().md +│ ├── PlayerInput-GetMouseSensitivity().md +│ ├── PlayerInput-GetMouseSensitivity(Vector2).md +│ ├── PlayerInput-GetTimeKeyPressed(string).md +│ ├── PlayerInput-IsCreated.md +│ ├── PlayerInput-IsKeyPressed(string).md +│ ├── PlayerInput-RemoveActionMapping(string_string).md +│ ├── PlayerInput-RemoveAxisMapping(string_string).md +│ ├── PlayerInput-SetMouseSensitivity(Vector2).md +│ ├── PlayerInput.md +│ ├── PointLight-PointLight(string_Blueprint).md +│ ├── PointLight.md +│ ├── PostProcessComponent-BlendRadius.md +│ ├── PostProcessComponent-BlendWeight.md +│ ├── PostProcessComponent-Enabled.md +│ ├── PostProcessComponent-PostProcessComponent(Actor_string_bool_Blueprint).md +│ ├── PostProcessComponent-Priority.md +│ ├── PostProcessComponent-Unbound.md +│ ├── PostProcessComponent.md +│ ├── PostProcessVolume-BlendRadius.md +│ ├── PostProcessVolume-BlendWeight.md +│ ├── PostProcessVolume-Enabled.md +│ ├── PostProcessVolume-PostProcessVolume(string_Blueprint).md +│ ├── PostProcessVolume-Priority.md +│ ├── PostProcessVolume-Unbound.md +│ ├── PostProcessVolume.md +│ ├── PrimitiveComponent-AddAngularImpulseInDegrees(Vector3_string_bool).md +│ ├── PrimitiveComponent-AddAngularImpulseInRadians(Vector3_string_bool).md +│ ├── PrimitiveComponent-AddForce(Vector3_string_bool).md +│ ├── PrimitiveComponent-AddForceAtLocation(Vector3_Vector3_string_bool).md +│ ├── PrimitiveComponent-AddImpulse(Vector3_string_bool).md +│ ├── PrimitiveComponent-AddImpulseAtLocation(Vector3_Vector3_string).md +│ ├── PrimitiveComponent-AddRadialForce(Vector3_float_float_bool_bool).md +│ ├── PrimitiveComponent-AddRadialImpulse(Vector3_float_float_bool_bool).md +│ ├── PrimitiveComponent-AddTorqueInDegrees(Vector3_string_bool).md +│ ├── PrimitiveComponent-AddTorqueInRadians(Vector3_string_bool).md +│ ├── PrimitiveComponent-AngularDamping.md +│ ├── PrimitiveComponent-CastShadow.md +│ ├── PrimitiveComponent-ClearMoveIgnoreActors().md +│ ├── PrimitiveComponent-ClearMoveIgnoreComponents().md +│ ├── PrimitiveComponent-CreateAndSetMaterialInstanceDynamic(int).md +│ ├── PrimitiveComponent-ForEachOverlappingComponent-T-(Action-T-).md +│ ├── PrimitiveComponent-GetDistanceToCollision(Vector3_Vector3).md +│ ├── PrimitiveComponent-GetMaterial(int).md +│ ├── PrimitiveComponent-GetPhysicsAngularVelocityInDegrees(Vector3_string).md +│ ├── PrimitiveComponent-GetPhysicsAngularVelocityInDegrees(string).md +│ ├── PrimitiveComponent-GetPhysicsAngularVelocityInRadians(Vector3_string).md +│ ├── PrimitiveComponent-GetPhysicsAngularVelocityInRadians(string).md +│ ├── PrimitiveComponent-GetPhysicsLinearVelocity(Vector3_string).md +│ ├── PrimitiveComponent-GetPhysicsLinearVelocity(string).md +│ ├── PrimitiveComponent-GetPhysicsLinearVelocityAtPoint(Vector3_Vector3_string).md +│ ├── PrimitiveComponent-GetPhysicsLinearVelocityAtPoint(Vector3_string).md +│ ├── PrimitiveComponent-GetSquaredDistanceToCollision(Vector3_float_Vector3).md +│ ├── PrimitiveComponent-IgnoreRadialForce.md +│ ├── PrimitiveComponent-IgnoreRadialImpulse.md +│ ├── PrimitiveComponent-IsGravityEnabled.md +│ ├── PrimitiveComponent-IsOverlappingComponent(PrimitiveComponent).md +│ ├── PrimitiveComponent-LinearDamping.md +│ ├── PrimitiveComponent-Mass.md +│ ├── PrimitiveComponent-MaterialsNumber.md +│ ├── PrimitiveComponent-OnlyOwnerSee.md +│ ├── PrimitiveComponent-OwnerNoSee.md +│ ├── PrimitiveComponent-RegisterEvent(ComponentEventType).md +│ ├── PrimitiveComponent-SetCenterOfMass(Vector3_string).md +│ ├── PrimitiveComponent-SetCollisionChannel(CollisionChannel).md +│ ├── PrimitiveComponent-SetCollisionMode(CollisionMode).md +│ ├── PrimitiveComponent-SetCollisionProfileName(string_bool).md +│ ├── PrimitiveComponent-SetCollisionResponseToAllChannels(CollisionResponse).md +│ ├── PrimitiveComponent-SetCollisionResponseToChannel(CollisionChannel_CollisionResponse).md +│ ├── PrimitiveComponent-SetEnableGravity(bool).md +│ ├── PrimitiveComponent-SetGenerateHitEvents(bool).md +│ ├── PrimitiveComponent-SetGenerateOverlapEvents(bool).md +│ ├── PrimitiveComponent-SetIgnoreActorWhenMoving(Actor_bool).md +│ ├── PrimitiveComponent-SetIgnoreComponentWhenMoving(PrimitiveComponent_bool).md +│ ├── PrimitiveComponent-SetMass(float_string).md +│ ├── PrimitiveComponent-SetMaterial(int_MaterialInterface).md +│ ├── PrimitiveComponent-SetPhysicsAngularVelocityInDegrees(Vector3_bool_string).md +│ ├── PrimitiveComponent-SetPhysicsAngularVelocityInRadians(Vector3_bool_string).md +│ ├── PrimitiveComponent-SetPhysicsLinearVelocity(Vector3_bool_string).md +│ ├── PrimitiveComponent-SetPhysicsMaxAngularVelocityInDegrees(float_bool_string).md +│ ├── PrimitiveComponent-SetPhysicsMaxAngularVelocityInRadians(float_bool_string).md +│ ├── PrimitiveComponent-SetSimulatePhysics(bool).md +│ ├── PrimitiveComponent-UnregisterEvent(ComponentEventType).md +│ ├── PrimitiveComponent.md +│ ├── RadialForceComponent-AddCollisionChannelToAffect(CollisionChannel).md +│ ├── RadialForceComponent-FireImpulse().md +│ ├── RadialForceComponent-ForceStrength.md +│ ├── RadialForceComponent-IgnoreOwningActor.md +│ ├── RadialForceComponent-ImpulseStrength.md +│ ├── RadialForceComponent-ImpulseVelocityChange.md +│ ├── RadialForceComponent-LinearFalloff.md +│ ├── RadialForceComponent-RadialForceComponent(Actor_string_bool_Blueprint).md +│ ├── RadialForceComponent-Radius.md +│ ├── RadialForceComponent.md +│ ├── RectLight-RectLight(string_Blueprint).md +│ ├── RectLight.md +│ ├── RotatingMovementComponent-GetPivotTranslation().md +│ ├── RotatingMovementComponent-GetPivotTranslation(Vector3).md +│ ├── RotatingMovementComponent-GetRotationRate().md +│ ├── RotatingMovementComponent-GetRotationRate(Quaternion).md +│ ├── RotatingMovementComponent-RotatingMovementComponent(Actor_string).md +│ ├── RotatingMovementComponent-RotationInLocalSpace.md +│ ├── RotatingMovementComponent-SetPivotTranslation(Vector3).md +│ ├── RotatingMovementComponent-SetRotationRate(Quaternion).md +│ ├── RotatingMovementComponent.md +│ ├── SceneComponent-Activate().md +│ ├── SceneComponent-AddLocalOffset(Vector3).md +│ ├── SceneComponent-AddLocalRotation(Quaternion).md +│ ├── SceneComponent-AddLocalTransform(Transform).md +│ ├── SceneComponent-AddRelativeLocation(Vector3).md +│ ├── SceneComponent-AddRelativeRotation(Quaternion).md +│ ├── SceneComponent-AddWorldOffset(Vector3).md +│ ├── SceneComponent-AddWorldRotation(Quaternion).md +│ ├── SceneComponent-AddWorldTransform(Transform).md +│ ├── SceneComponent-AttachToComponent(SceneComponent_AttachmentTransformRule_string).md +│ ├── SceneComponent-CanAttachAsChild(SceneComponent_string).md +│ ├── SceneComponent-Deactivate().md +│ ├── SceneComponent-DetachFromComponent(DetachmentTransformRule).md +│ ├── SceneComponent-ForEachAttachedChild-T-(Action-T-).md +│ ├── SceneComponent-GetAttachedSocketName().md +│ ├── SceneComponent-GetBounds(Transform_Bounds).md +│ ├── SceneComponent-GetForwardVector().md +│ ├── SceneComponent-GetForwardVector(Vector3).md +│ ├── SceneComponent-GetLocation().md +│ ├── SceneComponent-GetLocation(Vector3).md +│ ├── SceneComponent-GetRightVector().md +│ ├── SceneComponent-GetRightVector(Vector3).md +│ ├── SceneComponent-GetRotation().md +│ ├── SceneComponent-GetRotation(Quaternion).md +│ ├── SceneComponent-GetScale().md +│ ├── SceneComponent-GetScale(Vector3).md +│ ├── SceneComponent-GetSocketLocation(string).md +│ ├── SceneComponent-GetSocketLocation(string_Vector3).md +│ ├── SceneComponent-GetSocketRotation(string).md +│ ├── SceneComponent-GetSocketRotation(string_Quaternion).md +│ ├── SceneComponent-GetTransform().md +│ ├── SceneComponent-GetTransform(Transform).md +│ ├── SceneComponent-GetUpVector().md +│ ├── SceneComponent-GetUpVector(Vector3).md +│ ├── SceneComponent-GetVelocity().md +│ ├── SceneComponent-GetVelocity(Vector3).md +│ ├── SceneComponent-HasAnySockets.md +│ ├── SceneComponent-IsAttachedToActor(Actor).md +│ ├── SceneComponent-IsAttachedToComponent(SceneComponent).md +│ ├── SceneComponent-IsSocketExists(string).md +│ ├── SceneComponent-IsVisible.md +│ ├── SceneComponent-SceneComponent(Actor_string_bool_Blueprint).md +│ ├── SceneComponent-SetMobility(ComponentMobility).md +│ ├── SceneComponent-SetRelativeLocation(Vector3).md +│ ├── SceneComponent-SetRelativeRotation(Quaternion).md +│ ├── SceneComponent-SetRelativeTransform(Transform).md +│ ├── SceneComponent-SetVisibility(bool_bool).md +│ ├── SceneComponent-SetWorldLocation(Vector3).md +│ ├── SceneComponent-SetWorldRotation(Quaternion).md +│ ├── SceneComponent-SetWorldScale(Vector3).md +│ ├── SceneComponent-SetWorldTransform(Transform).md +│ ├── SceneComponent-UpdateToWorld(TeleportType_UpdateTransformFlags).md +│ ├── SceneComponent.md +│ ├── ShapeComponent-DynamicObstacle.md +│ ├── ShapeComponent-ShapeColor.md +│ ├── ShapeComponent.md +│ ├── SkeletalMesh-Load(string).md +│ ├── SkeletalMesh.md +│ ├── SkeletalMeshComponent-GetAnimationInstance().md +│ ├── SkeletalMeshComponent-IsPlaying.md +│ ├── SkeletalMeshComponent-Play(bool).md +│ ├── SkeletalMeshComponent-PlayAnimation(AnimationAsset_bool).md +│ ├── SkeletalMeshComponent-SetAnimation(AnimationAsset).md +│ ├── SkeletalMeshComponent-SetAnimationBlueprint(Blueprint).md +│ ├── SkeletalMeshComponent-SetAnimationMode(AnimationMode).md +│ ├── SkeletalMeshComponent-SkeletalMeshComponent(Actor_string_bool_Blueprint).md +│ ├── SkeletalMeshComponent-Stop().md +│ ├── SkeletalMeshComponent.md +│ ├── SkinnedMeshComponent-GetBoneIndex(string).md +│ ├── SkinnedMeshComponent-GetBoneName(int).md +│ ├── SkinnedMeshComponent-GetBoneTransform(int).md +│ ├── SkinnedMeshComponent-GetBoneTransform(int_Transform).md +│ ├── SkinnedMeshComponent-GetBonesNumber().md +│ ├── SkinnedMeshComponent-SetSkeletalMesh(SkeletalMesh_bool).md +│ ├── SkinnedMeshComponent.md +│ ├── SoundBase-Duration.md +│ ├── SoundBase-Equals(SoundBase).md +│ ├── SoundBase-GetHashCode().md +│ ├── SoundBase-IsCreated.md +│ ├── SoundBase.md +│ ├── SoundWave-Load(string).md +│ ├── SoundWave-Loop.md +│ ├── SoundWave.md +│ ├── SphereComponent-InitSphereRadius(float).md +│ ├── SphereComponent-ScaledSphereRadius.md +│ ├── SphereComponent-SetSphereRadius(float_bool).md +│ ├── SphereComponent-ShapeScale.md +│ ├── SphereComponent-SphereComponent(Actor_string_bool_Blueprint).md +│ ├── SphereComponent-UnscaledSphereRadius.md +│ ├── SphereComponent.md +│ ├── SplineComponent-AddSplinePoint(Vector3_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-AddSplinePointAtIndex(Vector3_int_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-ClearSplinePoints(bool).md +│ ├── SplineComponent-Duration.md +│ ├── SplineComponent-FindDirectionClosestToWorldLocation(Vector3_SplineCoordinateSpace).md +│ ├── SplineComponent-FindLocationClosestToWorldLocation(Vector3_SplineCoordinateSpace).md +│ ├── SplineComponent-FindRightVectorClosestToWorldLocation(Vector3_SplineCoordinateSpace).md +│ ├── SplineComponent-FindRollClosestToWorldLocation(Vector3_SplineCoordinateSpace).md +│ ├── SplineComponent-FindScaleClosestToWorldLocation(Vector3).md +│ ├── SplineComponent-FindTangentClosestToWorldLocation(Vector3_SplineCoordinateSpace).md +│ ├── SplineComponent-FindTransformClosestToWorldLocation(Vector3_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-FindUpVectorClosestToWorldLocation(Vector3_SplineCoordinateSpace).md +│ ├── SplineComponent-GetArriveTangentAtSplinePoint(int_SplineCoordinateSpace).md +│ ├── SplineComponent-GetArriveTangentAtSplinePoint(int_SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetDefaultUpVector(SplineCoordinateSpace).md +│ ├── SplineComponent-GetDefaultUpVector(SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetDirectionAtDistanceAlongSpline(float_SplineCoordinateSpace).md +│ ├── SplineComponent-GetDirectionAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetDirectionAtSplinePoint(int_SplineCoordinateSpace).md +│ ├── SplineComponent-GetDirectionAtSplinePoint(int_SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetDirectionAtTime(float_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-GetDirectionAtTime(float_SplineCoordinateSpace_bool_Vector3).md +│ ├── SplineComponent-GetDistanceAlongSplineAtSplinePoint(int).md +│ ├── SplineComponent-GetLeaveTangentAtSplinePoint(int_SplineCoordinateSpace).md +│ ├── SplineComponent-GetLeaveTangentAtSplinePoint(int_SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetLocationAndTangentAtSplinePoint(int_SplineCoordinateSpace_Vector3_Vector3).md +│ ├── SplineComponent-GetLocationAtDistanceAlongSpline(float_SplineCoordinateSpace).md +│ ├── SplineComponent-GetLocationAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetLocationAtSplinePoint(int_SplineCoordinateSpace).md +│ ├── SplineComponent-GetLocationAtSplinePoint(int_SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetLocationAtTime(float_SplineCoordinateSpace).md +│ ├── SplineComponent-GetLocationAtTime(float_SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetRightVectorAtDistanceAlongSpline(float_SplineCoordinateSpace).md +│ ├── SplineComponent-GetRightVectorAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetRightVectorAtSplinePoint(int_SplineCoordinateSpace).md +│ ├── SplineComponent-GetRightVectorAtSplinePoint(int_SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetRightVectorAtTime(float_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-GetRightVectorAtTime(float_SplineCoordinateSpace_bool_Vector3).md +│ ├── SplineComponent-GetRollAtDistanceAlongSpline(float_SplineCoordinateSpace).md +│ ├── SplineComponent-GetRollAtSplinePoint(int_SplineCoordinateSpace).md +│ ├── SplineComponent-GetRollAtTime(float_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-GetRotationAtDistanceAlongSpline(float_SplineCoordinateSpace).md +│ ├── SplineComponent-GetRotationAtDistanceAlongSpline(float_SplineCoordinateSpace_Quaternion).md +│ ├── SplineComponent-GetRotationAtSplinePoint(int_SplineCoordinateSpace).md +│ ├── SplineComponent-GetRotationAtSplinePoint(int_SplineCoordinateSpace_Quaternion).md +│ ├── SplineComponent-GetRotationAtTime(float_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-GetRotationAtTime(float_SplineCoordinateSpace_bool_Quaternion).md +│ ├── SplineComponent-GetScaleAtDistanceAlongSpline(float).md +│ ├── SplineComponent-GetScaleAtDistanceAlongSpline(float_Vector3).md +│ ├── SplineComponent-GetScaleAtSplinePoint(int).md +│ ├── SplineComponent-GetScaleAtSplinePoint(int_Vector3).md +│ ├── SplineComponent-GetScaleAtTime(float_bool).md +│ ├── SplineComponent-GetScaleAtTime(float_bool_Vector3).md +│ ├── SplineComponent-GetSplineLength().md +│ ├── SplineComponent-GetSplinePointType(int).md +│ ├── SplineComponent-GetTangentAtDistanceAlongSpline(float_SplineCoordinateSpace).md +│ ├── SplineComponent-GetTangentAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetTangentAtSplinePoint(int_SplineCoordinateSpace).md +│ ├── SplineComponent-GetTangentAtSplinePoint(int_SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetTangentAtTime(float_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-GetTangentAtTime(float_SplineCoordinateSpace_bool_Vector3).md +│ ├── SplineComponent-GetTransformAtDistanceAlongSpline(float_SplineCoordinateSpace).md +│ ├── SplineComponent-GetTransformAtDistanceAlongSpline(float_SplineCoordinateSpace_Transform).md +│ ├── SplineComponent-GetTransformAtSplinePoint(int_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-GetTransformAtSplinePoint(int_SplineCoordinateSpace_bool_Transform).md +│ ├── SplineComponent-GetTransformAtTime(float_SplineCoordinateSpace_bool_bool).md +│ ├── SplineComponent-GetTransformAtTime(float_SplineCoordinateSpace_bool_bool_Transform).md +│ ├── SplineComponent-GetUpVectorAtDistanceAlongSpline(float_SplineCoordinateSpace).md +│ ├── SplineComponent-GetUpVectorAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetUpVectorAtSplinePoint(int_SplineCoordinateSpace).md +│ ├── SplineComponent-GetUpVectorAtSplinePoint(int_SplineCoordinateSpace_Vector3).md +│ ├── SplineComponent-GetUpVectorAtTime(float_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-GetUpVectorAtTime(float_SplineCoordinateSpace_bool_Vector3).md +│ ├── SplineComponent-IsClosedLoop.md +│ ├── SplineComponent-RemoveSplinePoint(int_bool).md +│ ├── SplineComponent-SetClosedLoop(bool_bool).md +│ ├── SplineComponent-SetDefaultUpVector(Vector3_SplineCoordinateSpace).md +│ ├── SplineComponent-SetLocationAtSplinePoint(int_Vector3_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-SetSplinePointType(int_SplinePointType_bool).md +│ ├── SplineComponent-SetTangentAtSplinePoint(int_Vector3_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-SetTangentsAtSplinePoint(int_Vector3_Vector3_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-SetUpVectorAtSplinePoint(int_Vector3_SplineCoordinateSpace_bool).md +│ ├── SplineComponent-SplineComponent(Actor_string_bool_Blueprint).md +│ ├── SplineComponent-SplinePointsNumber.md +│ ├── SplineComponent-SplineSegmentsNumber.md +│ ├── SplineComponent-UpdateSpline().md +│ ├── SplineComponent.md +│ ├── SplineCoordinateSpace.md +│ ├── SplinePointType.md +│ ├── SpotLight-SpotLight(string_Blueprint).md +│ ├── SpotLight.md +│ ├── SpringArmComponent-CameraLagMaxDistance.md +│ ├── SpringArmComponent-CameraLagMaxTimeStep.md +│ ├── SpringArmComponent-CameraLagSubstepping.md +│ ├── SpringArmComponent-CameraPositionLag.md +│ ├── SpringArmComponent-CameraPositionLagSpeed.md +│ ├── SpringArmComponent-CameraRotationLag.md +│ ├── SpringArmComponent-CameraRotationLagSpeed.md +│ ├── SpringArmComponent-CollisionTest.md +│ ├── SpringArmComponent-DrawDebugLagMarkers.md +│ ├── SpringArmComponent-GetDesiredRotation().md +│ ├── SpringArmComponent-GetDesiredRotation(Quaternion).md +│ ├── SpringArmComponent-GetSocketOffset().md +│ ├── SpringArmComponent-GetSocketOffset(Vector3).md +│ ├── SpringArmComponent-GetTargetOffset().md +│ ├── SpringArmComponent-GetTargetOffset(Vector3).md +│ ├── SpringArmComponent-GetTargetRotation().md +│ ├── SpringArmComponent-GetTargetRotation(Quaternion).md +│ ├── SpringArmComponent-GetUnfixedCameraPosition().md +│ ├── SpringArmComponent-GetUnfixedCameraPosition(Vector3).md +│ ├── SpringArmComponent-InheritPitch.md +│ ├── SpringArmComponent-InheritRoll.md +│ ├── SpringArmComponent-InheritYaw.md +│ ├── SpringArmComponent-IsCollisionFixApplied.md +│ ├── SpringArmComponent-ProbeChannel.md +│ ├── SpringArmComponent-ProbeSize.md +│ ├── SpringArmComponent-SetSocketOffset(Vector3).md +│ ├── SpringArmComponent-SetTargetOffset(Vector3).md +│ ├── SpringArmComponent-SpringArmComponent(Actor_string_bool_Blueprint).md +│ ├── SpringArmComponent-TargetArmLength.md +│ ├── SpringArmComponent-UsePawnControlRotation.md +│ ├── SpringArmComponent.md +│ ├── StaticMesh-Cone.md +│ ├── StaticMesh-Cube.md +│ ├── StaticMesh-Cylinder.md +│ ├── StaticMesh-Load(string).md +│ ├── StaticMesh-Plane.md +│ ├── StaticMesh-Sphere.md +│ ├── StaticMesh.md +│ ├── StaticMeshComponent-GetLocalBounds(Vector3_Vector3).md +│ ├── StaticMeshComponent-GetStaticMesh().md +│ ├── StaticMeshComponent-SetStaticMesh(StaticMesh).md +│ ├── StaticMeshComponent-StaticMeshComponent(Actor_string_bool_Blueprint).md +│ ├── StaticMeshComponent.md +│ ├── StreamableRenderAsset-Equals(StreamableRenderAsset).md +│ ├── StreamableRenderAsset-GetHashCode().md +│ ├── StreamableRenderAsset-IsCreated.md +│ ├── StreamableRenderAsset.md +│ ├── TeleportType.md +│ ├── TextRenderComponent-SetFont(Font).md +│ ├── TextRenderComponent-SetHorizontalAlignment(HorizontalTextAligment).md +│ ├── TextRenderComponent-SetHorizontalSpacingAdjustment(float).md +│ ├── TextRenderComponent-SetScale(Vector2).md +│ ├── TextRenderComponent-SetText(string).md +│ ├── TextRenderComponent-SetTextMaterial(MaterialInterface).md +│ ├── TextRenderComponent-SetTextRenderColor(Color).md +│ ├── TextRenderComponent-SetVerticalAlignment(VerticalTextAligment).md +│ ├── TextRenderComponent-SetVerticalSpacingAdjustment(float).md +│ ├── TextRenderComponent-SetWorldSize(float).md +│ ├── TextRenderComponent-TextRenderComponent(Actor_string_bool_Blueprint).md +│ ├── TextRenderComponent.md +│ ├── Texture.md +│ ├── Texture2D-GetPixelFormat().md +│ ├── Texture2D-GetSize().md +│ ├── Texture2D-GetSize(Vector2).md +│ ├── Texture2D-HasAlphaChannel.md +│ ├── Texture2D-Load(string).md +│ ├── Texture2D-Texture2D(byte--_int).md +│ ├── Texture2D-Texture2D(string).md +│ ├── Texture2D.md +│ ├── Transform-Equals(Transform).md +│ ├── Transform-Equals(object).md +│ ├── Transform-GetHashCode().md +│ ├── Transform-Location.md +│ ├── Transform-Rotation.md +│ ├── Transform-Scale.md +│ ├── Transform-ToString().md +│ ├── Transform-ToString(IFormatProvider).md +│ ├── Transform-Transform(Vector3_Quaternion_Vector3).md +│ ├── Transform-op_Equality(Transform_Transform).md +│ ├── Transform-op_Implicitstring(Transform).md +│ ├── Transform-op_Inequality(Transform_Transform).md +│ ├── Transform.md +│ ├── TriggerBase.md +│ ├── TriggerBox-TriggerBox(string_Blueprint).md +│ ├── TriggerBox.md +│ ├── TriggerCapsule-TriggerCapsule(string_Blueprint).md +│ ├── TriggerCapsule.md +│ ├── TriggerSphere-TriggerSphere(string_Blueprint).md +│ ├── TriggerSphere.md +│ ├── TriggerVolume-TriggerVolume(string_Blueprint).md +│ ├── TriggerVolume.md +│ ├── UnrealEngine-Framework.md +│ ├── UpdateTransformFlags.md +│ ├── VerticalTextAligment.md +│ ├── Volume-EncompassesPoint(Vector3_float_float).md +│ ├── Volume.md +│ ├── WindowMode.md +│ ├── World-ActorCount.md +│ ├── World-CurrentLevelName.md +│ ├── World-DeltaTime.md +│ ├── World-ForEachActor-T-(Action-T-).md +│ ├── World-GetActor-T-(string).md +│ ├── World-GetActorByID-T-(uint).md +│ ├── World-GetActorByTag-T-(string).md +│ ├── World-GetFirstPlayerController().md +│ ├── World-GetGameMode().md +│ ├── World-GetWorldOrigin().md +│ ├── World-GetWorldOrigin(Vector3).md +│ ├── World-LineTraceSingleByChannel(Vector3_Vector3_CollisionChannel_Hit_bool_Actor_PrimitiveComponent).md +│ ├── World-LineTraceSingleByChannel(Vector3_Vector3_CollisionChannel_Hit_string_bool_Actor_PrimitiveComponent).md +│ ├── World-LineTraceSingleByProfile(Vector3_Vector3_string_Hit_bool_Actor_PrimitiveComponent).md +│ ├── World-LineTraceSingleByProfile(Vector3_Vector3_string_Hit_string_bool_Actor_PrimitiveComponent).md +│ ├── World-LineTraceTestByChannel(Vector3_Vector3_CollisionChannel_bool_Actor_PrimitiveComponent).md +│ ├── World-LineTraceTestByProfile(Vector3_Vector3_string_bool_Actor_PrimitiveComponent).md +│ ├── World-OpenLevel(string).md +│ ├── World-OverlapAnyTestByChannel(Vector3_Quaternion_CollisionChannel_CollisionShape_Actor_PrimitiveComponent).md +│ ├── World-OverlapAnyTestByProfile(Vector3_Quaternion_string_CollisionShape_Actor_PrimitiveComponent).md +│ ├── World-OverlapBlockingTestByChannel(Vector3_Quaternion_CollisionChannel_CollisionShape_Actor_PrimitiveComponent).md +│ ├── World-OverlapBlockingTestByProfile(Vector3_Quaternion_string_CollisionShape_Actor_PrimitiveComponent).md +│ ├── World-RealTime.md +│ ├── World-SetGravity(float).md +│ ├── World-SetOnActorBeginCursorOverCallback(ActorCursorDelegate).md +│ ├── World-SetOnActorBeginOverlapCallback(ActorOverlapDelegate).md +│ ├── World-SetOnActorClickedCallback(ActorKeyDelegate).md +│ ├── World-SetOnActorEndCursorOverCallback(ActorCursorDelegate).md +│ ├── World-SetOnActorEndOverlapCallback(ActorOverlapDelegate).md +│ ├── World-SetOnActorHitCallback(ActorHitDelegate).md +│ ├── World-SetOnActorReleasedCallback(ActorKeyDelegate).md +│ ├── World-SetOnComponentBeginCursorOverCallback(ComponentCursorDelegate).md +│ ├── World-SetOnComponentBeginOverlapCallback(ComponentOverlapDelegate).md +│ ├── World-SetOnComponentClickedCallback(ComponentKeyDelegate).md +│ ├── World-SetOnComponentEndCursorOverCallback(ComponentCursorDelegate).md +│ ├── World-SetOnComponentEndOverlapCallback(ComponentOverlapDelegate).md +│ ├── World-SetOnComponentHitCallback(ComponentHitDelegate).md +│ ├── World-SetOnComponentReleasedCallback(ComponentKeyDelegate).md +│ ├── World-SetWorldOrigin(Vector3).md +│ ├── World-SimulatePhysics.md +│ ├── World-SweepSingleByChannel(Vector3_Vector3_Quaternion_CollisionChannel_CollisionShape_Hit_bool_Actor_PrimitiveComponent).md +│ ├── World-SweepSingleByChannel(Vector3_Vector3_Quaternion_CollisionChannel_CollisionShape_Hit_string_bool_Actor_PrimitiveComponent).md +│ ├── World-SweepSingleByProfile(Vector3_Vector3_Quaternion_string_CollisionShape_Hit_bool_Actor_PrimitiveComponent).md +│ ├── World-SweepSingleByProfile(Vector3_Vector3_Quaternion_string_CollisionShape_Hit_string_bool_Actor_PrimitiveComponent).md +│ ├── World-SweepTestByChannel(Vector3_Vector3_Quaternion_CollisionChannel_CollisionShape_bool_Actor_PrimitiveComponent).md +│ ├── World-SweepTestByProfile(Vector3_Vector3_Quaternion_string_CollisionShape_bool_Actor_PrimitiveComponent).md +│ ├── World-Time.md +│ └── World.md +├── CODE_OF_CONDUCT.md +├── Content +│ └── Tests +│ ├── AlarmSound.uasset +│ ├── BasicMaterial.uasset +│ ├── BasicTexture.uasset +│ ├── BlueprintActor.uasset +│ ├── BlueprintAnimation.uasset +│ ├── BlueprintCameraActor.uasset +│ ├── BlueprintSceneComponent.uasset +│ ├── Characters +│ │ ├── Animations +│ │ │ ├── IdleAnimationMontage.uasset +│ │ │ ├── IdleAnimationSequence.uasset +│ │ │ ├── RunAnimationMontage.uasset +│ │ │ └── RunAnimationSequence.uasset +│ │ ├── Prototype.uasset +│ │ ├── PrototypeMaterial.uasset +│ │ ├── PrototypeMaterialInstance.uasset +│ │ ├── PrototypePhysicsAsset.uasset +│ │ ├── PrototypeSkeleton.uasset +│ │ └── Textures +│ │ ├── PrototypeColorMask.uasset +│ │ ├── PrototypeDiffuse.uasset +│ │ ├── PrototypeMask.uasset +│ │ └── PrototypeNormal.uasset +│ ├── PlayFont.uasset +│ ├── PlayFontMaterial.uasset +│ ├── TestSystems.uasset +│ ├── Tests.umap +│ ├── TestsGameMode.uasset +│ ├── Tests_BuiltData.uasset +│ └── TextureMaterial.uasset +├── Install +│ ├── Install.cs +│ └── Install.csproj +├── LICENSE +├── MANUAL.md +├── README.md +└── Source + ├── Managed + │ ├── Framework + │ │ ├── Codegen.cs + │ │ ├── Framework.cs + │ │ └── UnrealEngine.Framework.csproj + │ ├── Runtime + │ │ ├── Runtime.cs + │ │ ├── UnrealEngine.Runtime.csproj + │ │ └── libs + │ │ └── plugins + │ │ ├── LICENSE.txt + │ │ ├── Microsoft.DotNet.PlatformAbstractions.dll + │ │ ├── Microsoft.Extensions.DependencyModel.dll + │ │ ├── UnrealEngine.Plugins.deps.json + │ │ ├── UnrealEngine.Plugins.dll + │ │ └── UnrealEngine.Plugins.pdb + │ └── Tests + │ ├── AssertionConsistency.cs + │ ├── AudioPlayback.cs + │ ├── BlueprintsExtensibility.cs + │ ├── DebugVisualization.cs + │ ├── DynamicEvents.cs + │ ├── DynamicsConsistency.cs + │ ├── ExceptionsConsistency.cs + │ ├── ExternalConsistency.cs + │ ├── InstancedStaticMeshes.cs + │ ├── ObjectOrientedDesign.cs + │ ├── PhysicsSimulation.cs + │ ├── RadialForce.cs + │ ├── RuntimeConsistency.cs + │ ├── SkeletalMeshes.cs + │ ├── SpatialQueries.cs + │ ├── StaticMeshes.cs + │ ├── Tests.cs + │ ├── TextRenderer.cs + │ ├── TextureAssets.cs + │ ├── UnrealEngine.Tests.csproj + │ └── VirtualReality.cs + └── Native + ├── Resources + │ └── Icon128.png + ├── Runtime + │ ├── Linux + │ │ ├── LICENSE.txt + │ │ ├── ThirdPartyNotices.txt + │ │ ├── host + │ │ │ └── fxr + │ │ │ └── 6.0.1 + │ │ │ └── libhostfxr.so + │ │ └── shared + │ │ └── Microsoft.NETCore.App + │ │ └── 6.0.1 + │ │ ├── Microsoft.CSharp.dll + │ │ ├── Microsoft.NETCore.App.deps.json + │ │ ├── Microsoft.NETCore.App.runtimeconfig.json + │ │ ├── Microsoft.VisualBasic.Core.dll + │ │ ├── Microsoft.VisualBasic.dll + │ │ ├── Microsoft.Win32.Primitives.dll + │ │ ├── Microsoft.Win32.Registry.dll + │ │ ├── System.AppContext.dll + │ │ ├── System.Buffers.dll + │ │ ├── System.Collections.Concurrent.dll + │ │ ├── System.Collections.Immutable.dll + │ │ ├── System.Collections.NonGeneric.dll + │ │ ├── System.Collections.Specialized.dll + │ │ ├── System.Collections.dll + │ │ ├── System.ComponentModel.Annotations.dll + │ │ ├── System.ComponentModel.DataAnnotations.dll + │ │ ├── System.ComponentModel.EventBasedAsync.dll + │ │ ├── System.ComponentModel.Primitives.dll + │ │ ├── System.ComponentModel.TypeConverter.dll + │ │ ├── System.ComponentModel.dll + │ │ ├── System.Configuration.dll + │ │ ├── System.Console.dll + │ │ ├── System.Core.dll + │ │ ├── System.Data.Common.dll + │ │ ├── System.Data.DataSetExtensions.dll + │ │ ├── System.Data.dll + │ │ ├── System.Diagnostics.Contracts.dll + │ │ ├── System.Diagnostics.Debug.dll + │ │ ├── System.Diagnostics.DiagnosticSource.dll + │ │ ├── System.Diagnostics.FileVersionInfo.dll + │ │ ├── System.Diagnostics.Process.dll + │ │ ├── System.Diagnostics.StackTrace.dll + │ │ ├── System.Diagnostics.TextWriterTraceListener.dll + │ │ ├── System.Diagnostics.Tools.dll + │ │ ├── System.Diagnostics.TraceSource.dll + │ │ ├── System.Diagnostics.Tracing.dll + │ │ ├── System.Drawing.Primitives.dll + │ │ ├── System.Drawing.dll + │ │ ├── System.Dynamic.Runtime.dll + │ │ ├── System.Formats.Asn1.dll + │ │ ├── System.Globalization.Calendars.dll + │ │ ├── System.Globalization.Extensions.dll + │ │ ├── System.Globalization.dll + │ │ ├── System.IO.Compression.Brotli.dll + │ │ ├── System.IO.Compression.FileSystem.dll + │ │ ├── System.IO.Compression.ZipFile.dll + │ │ ├── System.IO.Compression.dll + │ │ ├── System.IO.FileSystem.AccessControl.dll + │ │ ├── System.IO.FileSystem.DriveInfo.dll + │ │ ├── System.IO.FileSystem.Primitives.dll + │ │ ├── System.IO.FileSystem.Watcher.dll + │ │ ├── System.IO.FileSystem.dll + │ │ ├── System.IO.IsolatedStorage.dll + │ │ ├── System.IO.MemoryMappedFiles.dll + │ │ ├── System.IO.Pipes.AccessControl.dll + │ │ ├── System.IO.Pipes.dll + │ │ ├── System.IO.UnmanagedMemoryStream.dll + │ │ ├── System.IO.dll + │ │ ├── System.Linq.Expressions.dll + │ │ ├── System.Linq.Parallel.dll + │ │ ├── System.Linq.Queryable.dll + │ │ ├── System.Linq.dll + │ │ ├── System.Memory.dll + │ │ ├── System.Net.Http.Json.dll + │ │ ├── System.Net.Http.dll + │ │ ├── System.Net.HttpListener.dll + │ │ ├── System.Net.Mail.dll + │ │ ├── System.Net.NameResolution.dll + │ │ ├── System.Net.NetworkInformation.dll + │ │ ├── System.Net.Ping.dll + │ │ ├── System.Net.Primitives.dll + │ │ ├── System.Net.Quic.dll + │ │ ├── System.Net.Requests.dll + │ │ ├── System.Net.Security.dll + │ │ ├── System.Net.ServicePoint.dll + │ │ ├── System.Net.Sockets.dll + │ │ ├── System.Net.WebClient.dll + │ │ ├── System.Net.WebHeaderCollection.dll + │ │ ├── System.Net.WebProxy.dll + │ │ ├── System.Net.WebSockets.Client.dll + │ │ ├── System.Net.WebSockets.dll + │ │ ├── System.Net.dll + │ │ ├── System.Numerics.Vectors.dll + │ │ ├── System.Numerics.dll + │ │ ├── System.ObjectModel.dll + │ │ ├── System.Private.CoreLib.dll + │ │ ├── System.Private.DataContractSerialization.dll + │ │ ├── System.Private.Uri.dll + │ │ ├── System.Private.Xml.Linq.dll + │ │ ├── System.Private.Xml.dll + │ │ ├── System.Reflection.DispatchProxy.dll + │ │ ├── System.Reflection.Emit.ILGeneration.dll + │ │ ├── System.Reflection.Emit.Lightweight.dll + │ │ ├── System.Reflection.Emit.dll + │ │ ├── System.Reflection.Extensions.dll + │ │ ├── System.Reflection.Metadata.dll + │ │ ├── System.Reflection.Primitives.dll + │ │ ├── System.Reflection.TypeExtensions.dll + │ │ ├── System.Reflection.dll + │ │ ├── System.Resources.Reader.dll + │ │ ├── System.Resources.ResourceManager.dll + │ │ ├── System.Resources.Writer.dll + │ │ ├── System.Runtime.CompilerServices.Unsafe.dll + │ │ ├── System.Runtime.CompilerServices.VisualC.dll + │ │ ├── System.Runtime.Extensions.dll + │ │ ├── System.Runtime.Handles.dll + │ │ ├── System.Runtime.InteropServices.RuntimeInformation.dll + │ │ ├── System.Runtime.InteropServices.dll + │ │ ├── System.Runtime.Intrinsics.dll + │ │ ├── System.Runtime.Loader.dll + │ │ ├── System.Runtime.Numerics.dll + │ │ ├── System.Runtime.Serialization.Formatters.dll + │ │ ├── System.Runtime.Serialization.Json.dll + │ │ ├── System.Runtime.Serialization.Primitives.dll + │ │ ├── System.Runtime.Serialization.Xml.dll + │ │ ├── System.Runtime.Serialization.dll + │ │ ├── System.Runtime.dll + │ │ ├── System.Security.AccessControl.dll + │ │ ├── System.Security.Claims.dll + │ │ ├── System.Security.Cryptography.Algorithms.dll + │ │ ├── System.Security.Cryptography.Cng.dll + │ │ ├── System.Security.Cryptography.Csp.dll + │ │ ├── System.Security.Cryptography.Encoding.dll + │ │ ├── System.Security.Cryptography.OpenSsl.dll + │ │ ├── System.Security.Cryptography.Primitives.dll + │ │ ├── System.Security.Cryptography.X509Certificates.dll + │ │ ├── System.Security.Principal.Windows.dll + │ │ ├── System.Security.Principal.dll + │ │ ├── System.Security.SecureString.dll + │ │ ├── System.Security.dll + │ │ ├── System.ServiceModel.Web.dll + │ │ ├── System.ServiceProcess.dll + │ │ ├── System.Text.Encoding.CodePages.dll + │ │ ├── System.Text.Encoding.Extensions.dll + │ │ ├── System.Text.Encoding.dll + │ │ ├── System.Text.Encodings.Web.dll + │ │ ├── System.Text.Json.dll + │ │ ├── System.Text.RegularExpressions.dll + │ │ ├── System.Threading.Channels.dll + │ │ ├── System.Threading.Overlapped.dll + │ │ ├── System.Threading.Tasks.Dataflow.dll + │ │ ├── System.Threading.Tasks.Extensions.dll + │ │ ├── System.Threading.Tasks.Parallel.dll + │ │ ├── System.Threading.Tasks.dll + │ │ ├── System.Threading.Thread.dll + │ │ ├── System.Threading.ThreadPool.dll + │ │ ├── System.Threading.Timer.dll + │ │ ├── System.Threading.dll + │ │ ├── System.Transactions.Local.dll + │ │ ├── System.Transactions.dll + │ │ ├── System.ValueTuple.dll + │ │ ├── System.Web.HttpUtility.dll + │ │ ├── System.Web.dll + │ │ ├── System.Windows.dll + │ │ ├── System.Xml.Linq.dll + │ │ ├── System.Xml.ReaderWriter.dll + │ │ ├── System.Xml.Serialization.dll + │ │ ├── System.Xml.XDocument.dll + │ │ ├── System.Xml.XPath.XDocument.dll + │ │ ├── System.Xml.XPath.dll + │ │ ├── System.Xml.XmlDocument.dll + │ │ ├── System.Xml.XmlSerializer.dll + │ │ ├── System.Xml.dll + │ │ ├── System.dll + │ │ ├── WindowsBase.dll + │ │ ├── createdump + │ │ ├── libSystem.Globalization.Native.so + │ │ ├── libSystem.IO.Compression.Native.so + │ │ ├── libSystem.Native.so + │ │ ├── libSystem.Net.Security.Native.so + │ │ ├── libSystem.Security.Cryptography.Native.OpenSsl.so + │ │ ├── libclrjit.so + │ │ ├── libcoreclr.so + │ │ ├── libcoreclrtraceptprovider.so + │ │ ├── libdbgshim.so + │ │ ├── libhostpolicy.so + │ │ ├── libmscordaccore.so + │ │ ├── libmscordbi.so + │ │ ├── mscorlib.dll + │ │ └── netstandard.dll + │ ├── Mac + │ │ ├── LICENSE.txt + │ │ ├── ThirdPartyNotices.txt + │ │ ├── host + │ │ │ └── fxr + │ │ │ └── 6.0.1 + │ │ │ └── libhostfxr.dylib + │ │ └── shared + │ │ └── Microsoft.NETCore.App + │ │ └── 6.0.1 + │ │ ├── Microsoft.CSharp.dll + │ │ ├── Microsoft.NETCore.App.deps.json + │ │ ├── Microsoft.NETCore.App.runtimeconfig.json + │ │ ├── Microsoft.VisualBasic.Core.dll + │ │ ├── Microsoft.VisualBasic.dll + │ │ ├── Microsoft.Win32.Primitives.dll + │ │ ├── Microsoft.Win32.Registry.dll + │ │ ├── System.AppContext.dll + │ │ ├── System.Buffers.dll + │ │ ├── System.Collections.Concurrent.dll + │ │ ├── System.Collections.Immutable.dll + │ │ ├── System.Collections.NonGeneric.dll + │ │ ├── System.Collections.Specialized.dll + │ │ ├── System.Collections.dll + │ │ ├── System.ComponentModel.Annotations.dll + │ │ ├── System.ComponentModel.DataAnnotations.dll + │ │ ├── System.ComponentModel.EventBasedAsync.dll + │ │ ├── System.ComponentModel.Primitives.dll + │ │ ├── System.ComponentModel.TypeConverter.dll + │ │ ├── System.ComponentModel.dll + │ │ ├── System.Configuration.dll + │ │ ├── System.Console.dll + │ │ ├── System.Core.dll + │ │ ├── System.Data.Common.dll + │ │ ├── System.Data.DataSetExtensions.dll + │ │ ├── System.Data.dll + │ │ ├── System.Diagnostics.Contracts.dll + │ │ ├── System.Diagnostics.Debug.dll + │ │ ├── System.Diagnostics.DiagnosticSource.dll + │ │ ├── System.Diagnostics.FileVersionInfo.dll + │ │ ├── System.Diagnostics.Process.dll + │ │ ├── System.Diagnostics.StackTrace.dll + │ │ ├── System.Diagnostics.TextWriterTraceListener.dll + │ │ ├── System.Diagnostics.Tools.dll + │ │ ├── System.Diagnostics.TraceSource.dll + │ │ ├── System.Diagnostics.Tracing.dll + │ │ ├── System.Drawing.Primitives.dll + │ │ ├── System.Drawing.dll + │ │ ├── System.Dynamic.Runtime.dll + │ │ ├── System.Formats.Asn1.dll + │ │ ├── System.Globalization.Calendars.dll + │ │ ├── System.Globalization.Extensions.dll + │ │ ├── System.Globalization.dll + │ │ ├── System.IO.Compression.Brotli.dll + │ │ ├── System.IO.Compression.FileSystem.dll + │ │ ├── System.IO.Compression.ZipFile.dll + │ │ ├── System.IO.Compression.dll + │ │ ├── System.IO.FileSystem.AccessControl.dll + │ │ ├── System.IO.FileSystem.DriveInfo.dll + │ │ ├── System.IO.FileSystem.Primitives.dll + │ │ ├── System.IO.FileSystem.Watcher.dll + │ │ ├── System.IO.FileSystem.dll + │ │ ├── System.IO.IsolatedStorage.dll + │ │ ├── System.IO.MemoryMappedFiles.dll + │ │ ├── System.IO.Pipes.AccessControl.dll + │ │ ├── System.IO.Pipes.dll + │ │ ├── System.IO.UnmanagedMemoryStream.dll + │ │ ├── System.IO.dll + │ │ ├── System.Linq.Expressions.dll + │ │ ├── System.Linq.Parallel.dll + │ │ ├── System.Linq.Queryable.dll + │ │ ├── System.Linq.dll + │ │ ├── System.Memory.dll + │ │ ├── System.Net.Http.Json.dll + │ │ ├── System.Net.Http.dll + │ │ ├── System.Net.HttpListener.dll + │ │ ├── System.Net.Mail.dll + │ │ ├── System.Net.NameResolution.dll + │ │ ├── System.Net.NetworkInformation.dll + │ │ ├── System.Net.Ping.dll + │ │ ├── System.Net.Primitives.dll + │ │ ├── System.Net.Quic.dll + │ │ ├── System.Net.Requests.dll + │ │ ├── System.Net.Security.dll + │ │ ├── System.Net.ServicePoint.dll + │ │ ├── System.Net.Sockets.dll + │ │ ├── System.Net.WebClient.dll + │ │ ├── System.Net.WebHeaderCollection.dll + │ │ ├── System.Net.WebProxy.dll + │ │ ├── System.Net.WebSockets.Client.dll + │ │ ├── System.Net.WebSockets.dll + │ │ ├── System.Net.dll + │ │ ├── System.Numerics.Vectors.dll + │ │ ├── System.Numerics.dll + │ │ ├── System.ObjectModel.dll + │ │ ├── System.Private.CoreLib.dll + │ │ ├── System.Private.DataContractSerialization.dll + │ │ ├── System.Private.Uri.dll + │ │ ├── System.Private.Xml.Linq.dll + │ │ ├── System.Private.Xml.dll + │ │ ├── System.Reflection.DispatchProxy.dll + │ │ ├── System.Reflection.Emit.ILGeneration.dll + │ │ ├── System.Reflection.Emit.Lightweight.dll + │ │ ├── System.Reflection.Emit.dll + │ │ ├── System.Reflection.Extensions.dll + │ │ ├── System.Reflection.Metadata.dll + │ │ ├── System.Reflection.Primitives.dll + │ │ ├── System.Reflection.TypeExtensions.dll + │ │ ├── System.Reflection.dll + │ │ ├── System.Resources.Reader.dll + │ │ ├── System.Resources.ResourceManager.dll + │ │ ├── System.Resources.Writer.dll + │ │ ├── System.Runtime.CompilerServices.Unsafe.dll + │ │ ├── System.Runtime.CompilerServices.VisualC.dll + │ │ ├── System.Runtime.Extensions.dll + │ │ ├── System.Runtime.Handles.dll + │ │ ├── System.Runtime.InteropServices.RuntimeInformation.dll + │ │ ├── System.Runtime.InteropServices.dll + │ │ ├── System.Runtime.Intrinsics.dll + │ │ ├── System.Runtime.Loader.dll + │ │ ├── System.Runtime.Numerics.dll + │ │ ├── System.Runtime.Serialization.Formatters.dll + │ │ ├── System.Runtime.Serialization.Json.dll + │ │ ├── System.Runtime.Serialization.Primitives.dll + │ │ ├── System.Runtime.Serialization.Xml.dll + │ │ ├── System.Runtime.Serialization.dll + │ │ ├── System.Runtime.dll + │ │ ├── System.Security.AccessControl.dll + │ │ ├── System.Security.Claims.dll + │ │ ├── System.Security.Cryptography.Algorithms.dll + │ │ ├── System.Security.Cryptography.Cng.dll + │ │ ├── System.Security.Cryptography.Csp.dll + │ │ ├── System.Security.Cryptography.Encoding.dll + │ │ ├── System.Security.Cryptography.OpenSsl.dll + │ │ ├── System.Security.Cryptography.Primitives.dll + │ │ ├── System.Security.Cryptography.X509Certificates.dll + │ │ ├── System.Security.Principal.Windows.dll + │ │ ├── System.Security.Principal.dll + │ │ ├── System.Security.SecureString.dll + │ │ ├── System.Security.dll + │ │ ├── System.ServiceModel.Web.dll + │ │ ├── System.ServiceProcess.dll + │ │ ├── System.Text.Encoding.CodePages.dll + │ │ ├── System.Text.Encoding.Extensions.dll + │ │ ├── System.Text.Encoding.dll + │ │ ├── System.Text.Encodings.Web.dll + │ │ ├── System.Text.Json.dll + │ │ ├── System.Text.RegularExpressions.dll + │ │ ├── System.Threading.Channels.dll + │ │ ├── System.Threading.Overlapped.dll + │ │ ├── System.Threading.Tasks.Dataflow.dll + │ │ ├── System.Threading.Tasks.Extensions.dll + │ │ ├── System.Threading.Tasks.Parallel.dll + │ │ ├── System.Threading.Tasks.dll + │ │ ├── System.Threading.Thread.dll + │ │ ├── System.Threading.ThreadPool.dll + │ │ ├── System.Threading.Timer.dll + │ │ ├── System.Threading.dll + │ │ ├── System.Transactions.Local.dll + │ │ ├── System.Transactions.dll + │ │ ├── System.ValueTuple.dll + │ │ ├── System.Web.HttpUtility.dll + │ │ ├── System.Web.dll + │ │ ├── System.Windows.dll + │ │ ├── System.Xml.Linq.dll + │ │ ├── System.Xml.ReaderWriter.dll + │ │ ├── System.Xml.Serialization.dll + │ │ ├── System.Xml.XDocument.dll + │ │ ├── System.Xml.XPath.XDocument.dll + │ │ ├── System.Xml.XPath.dll + │ │ ├── System.Xml.XmlDocument.dll + │ │ ├── System.Xml.XmlSerializer.dll + │ │ ├── System.Xml.dll + │ │ ├── System.dll + │ │ ├── WindowsBase.dll + │ │ ├── createdump + │ │ ├── libSystem.Globalization.Native.dylib + │ │ ├── libSystem.IO.Compression.Native.dylib + │ │ ├── libSystem.Native.dylib + │ │ ├── libSystem.Net.Security.Native.dylib + │ │ ├── libSystem.Security.Cryptography.Native.Apple.dylib + │ │ ├── libSystem.Security.Cryptography.Native.OpenSsl.dylib + │ │ ├── libclrjit.dylib + │ │ ├── libcoreclr.dylib + │ │ ├── libdbgshim.dylib + │ │ ├── libhostpolicy.dylib + │ │ ├── libmscordaccore.dylib + │ │ ├── libmscordbi.dylib + │ │ ├── mscorlib.dll + │ │ └── netstandard.dll + │ └── Win64 + │ ├── LICENSE.txt + │ ├── ThirdPartyNotices.txt + │ ├── host + │ │ └── fxr + │ │ └── 6.0.1 + │ │ └── hostfxr.dll + │ └── shared + │ └── Microsoft.NETCore.App + │ └── 6.0.1 + │ ├── Microsoft.CSharp.dll + │ ├── Microsoft.DiaSymReader.Native.amd64.dll + │ ├── Microsoft.NETCore.App.deps.json + │ ├── Microsoft.NETCore.App.runtimeconfig.json + │ ├── Microsoft.VisualBasic.Core.dll + │ ├── Microsoft.VisualBasic.dll + │ ├── Microsoft.Win32.Primitives.dll + │ ├── Microsoft.Win32.Registry.dll + │ ├── System.AppContext.dll + │ ├── System.Buffers.dll + │ ├── System.Collections.Concurrent.dll + │ ├── System.Collections.Immutable.dll + │ ├── System.Collections.NonGeneric.dll + │ ├── System.Collections.Specialized.dll + │ ├── System.Collections.dll + │ ├── System.ComponentModel.Annotations.dll + │ ├── System.ComponentModel.DataAnnotations.dll + │ ├── System.ComponentModel.EventBasedAsync.dll + │ ├── System.ComponentModel.Primitives.dll + │ ├── System.ComponentModel.TypeConverter.dll + │ ├── System.ComponentModel.dll + │ ├── System.Configuration.dll + │ ├── System.Console.dll + │ ├── System.Core.dll + │ ├── System.Data.Common.dll + │ ├── System.Data.DataSetExtensions.dll + │ ├── System.Data.dll + │ ├── System.Diagnostics.Contracts.dll + │ ├── System.Diagnostics.Debug.dll + │ ├── System.Diagnostics.DiagnosticSource.dll + │ ├── System.Diagnostics.FileVersionInfo.dll + │ ├── System.Diagnostics.Process.dll + │ ├── System.Diagnostics.StackTrace.dll + │ ├── System.Diagnostics.TextWriterTraceListener.dll + │ ├── System.Diagnostics.Tools.dll + │ ├── System.Diagnostics.TraceSource.dll + │ ├── System.Diagnostics.Tracing.dll + │ ├── System.Drawing.Primitives.dll + │ ├── System.Drawing.dll + │ ├── System.Dynamic.Runtime.dll + │ ├── System.Formats.Asn1.dll + │ ├── System.Globalization.Calendars.dll + │ ├── System.Globalization.Extensions.dll + │ ├── System.Globalization.dll + │ ├── System.IO.Compression.Brotli.dll + │ ├── System.IO.Compression.FileSystem.dll + │ ├── System.IO.Compression.Native.dll + │ ├── System.IO.Compression.ZipFile.dll + │ ├── System.IO.Compression.dll + │ ├── System.IO.FileSystem.AccessControl.dll + │ ├── System.IO.FileSystem.DriveInfo.dll + │ ├── System.IO.FileSystem.Primitives.dll + │ ├── System.IO.FileSystem.Watcher.dll + │ ├── System.IO.FileSystem.dll + │ ├── System.IO.IsolatedStorage.dll + │ ├── System.IO.MemoryMappedFiles.dll + │ ├── System.IO.Pipes.AccessControl.dll + │ ├── System.IO.Pipes.dll + │ ├── System.IO.UnmanagedMemoryStream.dll + │ ├── System.IO.dll + │ ├── System.Linq.Expressions.dll + │ ├── System.Linq.Parallel.dll + │ ├── System.Linq.Queryable.dll + │ ├── System.Linq.dll + │ ├── System.Memory.dll + │ ├── System.Net.Http.Json.dll + │ ├── System.Net.Http.dll + │ ├── System.Net.HttpListener.dll + │ ├── System.Net.Mail.dll + │ ├── System.Net.NameResolution.dll + │ ├── System.Net.NetworkInformation.dll + │ ├── System.Net.Ping.dll + │ ├── System.Net.Primitives.dll + │ ├── System.Net.Quic.dll + │ ├── System.Net.Requests.dll + │ ├── System.Net.Security.dll + │ ├── System.Net.ServicePoint.dll + │ ├── System.Net.Sockets.dll + │ ├── System.Net.WebClient.dll + │ ├── System.Net.WebHeaderCollection.dll + │ ├── System.Net.WebProxy.dll + │ ├── System.Net.WebSockets.Client.dll + │ ├── System.Net.WebSockets.dll + │ ├── System.Net.dll + │ ├── System.Numerics.Vectors.dll + │ ├── System.Numerics.dll + │ ├── System.ObjectModel.dll + │ ├── System.Private.CoreLib.dll + │ ├── System.Private.DataContractSerialization.dll + │ ├── System.Private.Uri.dll + │ ├── System.Private.Xml.Linq.dll + │ ├── System.Private.Xml.dll + │ ├── System.Reflection.DispatchProxy.dll + │ ├── System.Reflection.Emit.ILGeneration.dll + │ ├── System.Reflection.Emit.Lightweight.dll + │ ├── System.Reflection.Emit.dll + │ ├── System.Reflection.Extensions.dll + │ ├── System.Reflection.Metadata.dll + │ ├── System.Reflection.Primitives.dll + │ ├── System.Reflection.TypeExtensions.dll + │ ├── System.Reflection.dll + │ ├── System.Resources.Reader.dll + │ ├── System.Resources.ResourceManager.dll + │ ├── System.Resources.Writer.dll + │ ├── System.Runtime.CompilerServices.Unsafe.dll + │ ├── System.Runtime.CompilerServices.VisualC.dll + │ ├── System.Runtime.Extensions.dll + │ ├── System.Runtime.Handles.dll + │ ├── System.Runtime.InteropServices.RuntimeInformation.dll + │ ├── System.Runtime.InteropServices.dll + │ ├── System.Runtime.Intrinsics.dll + │ ├── System.Runtime.Loader.dll + │ ├── System.Runtime.Numerics.dll + │ ├── System.Runtime.Serialization.Formatters.dll + │ ├── System.Runtime.Serialization.Json.dll + │ ├── System.Runtime.Serialization.Primitives.dll + │ ├── System.Runtime.Serialization.Xml.dll + │ ├── System.Runtime.Serialization.dll + │ ├── System.Runtime.dll + │ ├── System.Security.AccessControl.dll + │ ├── System.Security.Claims.dll + │ ├── System.Security.Cryptography.Algorithms.dll + │ ├── System.Security.Cryptography.Cng.dll + │ ├── System.Security.Cryptography.Csp.dll + │ ├── System.Security.Cryptography.Encoding.dll + │ ├── System.Security.Cryptography.OpenSsl.dll + │ ├── System.Security.Cryptography.Primitives.dll + │ ├── System.Security.Cryptography.X509Certificates.dll + │ ├── System.Security.Principal.Windows.dll + │ ├── System.Security.Principal.dll + │ ├── System.Security.SecureString.dll + │ ├── System.Security.dll + │ ├── System.ServiceModel.Web.dll + │ ├── System.ServiceProcess.dll + │ ├── System.Text.Encoding.CodePages.dll + │ ├── System.Text.Encoding.Extensions.dll + │ ├── System.Text.Encoding.dll + │ ├── System.Text.Encodings.Web.dll + │ ├── System.Text.Json.dll + │ ├── System.Text.RegularExpressions.dll + │ ├── System.Threading.Channels.dll + │ ├── System.Threading.Overlapped.dll + │ ├── System.Threading.Tasks.Dataflow.dll + │ ├── System.Threading.Tasks.Extensions.dll + │ ├── System.Threading.Tasks.Parallel.dll + │ ├── System.Threading.Tasks.dll + │ ├── System.Threading.Thread.dll + │ ├── System.Threading.ThreadPool.dll + │ ├── System.Threading.Timer.dll + │ ├── System.Threading.dll + │ ├── System.Transactions.Local.dll + │ ├── System.Transactions.dll + │ ├── System.ValueTuple.dll + │ ├── System.Web.HttpUtility.dll + │ ├── System.Web.dll + │ ├── System.Windows.dll + │ ├── System.Xml.Linq.dll + │ ├── System.Xml.ReaderWriter.dll + │ ├── System.Xml.Serialization.dll + │ ├── System.Xml.XDocument.dll + │ ├── System.Xml.XPath.XDocument.dll + │ ├── System.Xml.XPath.dll + │ ├── System.Xml.XmlDocument.dll + │ ├── System.Xml.XmlSerializer.dll + │ ├── System.Xml.dll + │ ├── System.dll + │ ├── WindowsBase.dll + │ ├── api-ms-win-core-console-l1-1-0.dll + │ ├── api-ms-win-core-console-l1-2-0.dll + │ ├── api-ms-win-core-datetime-l1-1-0.dll + │ ├── api-ms-win-core-debug-l1-1-0.dll + │ ├── api-ms-win-core-errorhandling-l1-1-0.dll + │ ├── api-ms-win-core-file-l1-1-0.dll + │ ├── api-ms-win-core-file-l1-2-0.dll + │ ├── api-ms-win-core-file-l2-1-0.dll + │ ├── api-ms-win-core-handle-l1-1-0.dll + │ ├── api-ms-win-core-heap-l1-1-0.dll + │ ├── api-ms-win-core-interlocked-l1-1-0.dll + │ ├── api-ms-win-core-libraryloader-l1-1-0.dll + │ ├── api-ms-win-core-localization-l1-2-0.dll + │ ├── api-ms-win-core-memory-l1-1-0.dll + │ ├── api-ms-win-core-namedpipe-l1-1-0.dll + │ ├── api-ms-win-core-processenvironment-l1-1-0.dll + │ ├── api-ms-win-core-processthreads-l1-1-0.dll + │ ├── api-ms-win-core-processthreads-l1-1-1.dll + │ ├── api-ms-win-core-profile-l1-1-0.dll + │ ├── api-ms-win-core-rtlsupport-l1-1-0.dll + │ ├── api-ms-win-core-string-l1-1-0.dll + │ ├── api-ms-win-core-synch-l1-1-0.dll + │ ├── api-ms-win-core-synch-l1-2-0.dll + │ ├── api-ms-win-core-sysinfo-l1-1-0.dll + │ ├── api-ms-win-core-timezone-l1-1-0.dll + │ ├── api-ms-win-core-util-l1-1-0.dll + │ ├── api-ms-win-crt-conio-l1-1-0.dll + │ ├── api-ms-win-crt-convert-l1-1-0.dll + │ ├── api-ms-win-crt-environment-l1-1-0.dll + │ ├── api-ms-win-crt-filesystem-l1-1-0.dll + │ ├── api-ms-win-crt-heap-l1-1-0.dll + │ ├── api-ms-win-crt-locale-l1-1-0.dll + │ ├── api-ms-win-crt-math-l1-1-0.dll + │ ├── api-ms-win-crt-multibyte-l1-1-0.dll + │ ├── api-ms-win-crt-private-l1-1-0.dll + │ ├── api-ms-win-crt-process-l1-1-0.dll + │ ├── api-ms-win-crt-runtime-l1-1-0.dll + │ ├── api-ms-win-crt-stdio-l1-1-0.dll + │ ├── api-ms-win-crt-string-l1-1-0.dll + │ ├── api-ms-win-crt-time-l1-1-0.dll + │ ├── api-ms-win-crt-utility-l1-1-0.dll + │ ├── clretwrc.dll + │ ├── clrjit.dll + │ ├── coreclr.dll + │ ├── createdump.exe + │ ├── dbgshim.dll + │ ├── hostpolicy.dll + │ ├── mscordaccore.dll + │ ├── mscordaccore_amd64_amd64_6.0.121.56705.dll + │ ├── mscordbi.dll + │ ├── mscorlib.dll + │ ├── mscorrc.dll + │ ├── msquic.dll + │ ├── netstandard.dll + │ └── ucrtbase.dll + ├── Source + │ ├── Dependencies + │ │ └── CoreCLR + │ │ └── includes + │ │ ├── coreclr_delegates.h + │ │ └── hostfxr.h + │ └── UnrealCLR + │ ├── Private + │ │ ├── UnrealCLR.cpp + │ │ ├── UnrealCLRFramework.cpp + │ │ ├── UnrealCLRLibrary.cpp + │ │ └── UnrealCLRManager.cpp + │ ├── Public + │ │ ├── UnrealCLR.h + │ │ ├── UnrealCLRFramework.h + │ │ ├── UnrealCLRLibrary.h + │ │ └── UnrealCLRManager.h + │ └── UnrealCLR.Build.cs + └── UnrealCLR.uplugin + +``` + +`API/AIController-AIController(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AIController](./AIController.md 'UnrealEngine.Framework.AIController') +## AIController(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public AIController(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/AIController-AllowStrafe.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AIController](./AIController.md 'UnrealEngine.Framework.AIController') +## AIController.AllowStrafe Property +Gets or sets whether strafing allowed during movement +```csharp +public bool AllowStrafe { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AIController-ClearFocus(AIFocusPriority).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AIController](./AIController.md 'UnrealEngine.Framework.AIController') +## AIController.ClearFocus(UnrealEngine.Framework.AIFocusPriority) Method +Clears focus for the given priority, will clear focal point as a result +```csharp +public void ClearFocus(UnrealEngine.Framework.AIFocusPriority priority=UnrealEngine.Framework.AIFocusPriority.High); +``` +#### Parameters + +`priority` [AIFocusPriority](./AIFocusPriority.md 'UnrealEngine.Framework.AIFocusPriority') + + +``` + +`API/AIController-GetFocalPoint().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AIController](./AIController.md 'UnrealEngine.Framework.AIController') +## AIController.GetFocalPoint() Method +Returns the final position that controller should be looking at +```csharp +public System.Numerics.Vector3 GetFocalPoint(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/AIController-GetFocalPoint(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AIController](./AIController.md 'UnrealEngine.Framework.AIController') +## AIController.GetFocalPoint(System.Numerics.Vector3) Method +Retrieves the final position that controller should be looking at +```csharp +public void GetFocalPoint(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/AIController-GetFocusActor().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AIController](./AIController.md 'UnrealEngine.Framework.AIController') +## AIController.GetFocusActor() Method +Returns the focused actor or `null` on failure +```csharp +public UnrealEngine.Framework.Actor GetFocusActor(); +``` +#### Returns +[Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +``` + +`API/AIController-SetFocalPoint(Vector3_AIFocusPriority).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AIController](./AIController.md 'UnrealEngine.Framework.AIController') +## AIController.SetFocalPoint(System.Numerics.Vector3, UnrealEngine.Framework.AIFocusPriority) Method +Sets focal point for the given priority as absolute position or offset from base +```csharp +public void SetFocalPoint(in System.Numerics.Vector3 newFocus, UnrealEngine.Framework.AIFocusPriority priority); +``` +#### Parameters + +`newFocus` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`priority` [AIFocusPriority](./AIFocusPriority.md 'UnrealEngine.Framework.AIFocusPriority') + + +``` + +`API/AIController-SetFocus(Actor_AIFocusPriority).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AIController](./AIController.md 'UnrealEngine.Framework.AIController') +## AIController.SetFocus(UnrealEngine.Framework.Actor, UnrealEngine.Framework.AIFocusPriority) Method +Sets focus actor for the given priority, will set focal point as a result +```csharp +public void SetFocus(UnrealEngine.Framework.Actor newFocus, UnrealEngine.Framework.AIFocusPriority priority=UnrealEngine.Framework.AIFocusPriority.High); +``` +#### Parameters + +`newFocus` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`priority` [AIFocusPriority](./AIFocusPriority.md 'UnrealEngine.Framework.AIFocusPriority') + + +``` + +`API/AIController.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AIController Class +The base class of controllers for an AI-controlled [Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +```csharp +public class AIController : UnrealEngine.Framework.Controller +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 [Controller](./Controller.md 'UnrealEngine.Framework.Controller') 🡒 AIController +### Constructors +- [AIController(string, UnrealEngine.Framework.Blueprint)](./AIController-AIController(string_Blueprint).md 'UnrealEngine.Framework.AIController.AIController(string, UnrealEngine.Framework.Blueprint)') +### Properties +- [AllowStrafe](./AIController-AllowStrafe.md 'UnrealEngine.Framework.AIController.AllowStrafe') +### Methods +- [ClearFocus(UnrealEngine.Framework.AIFocusPriority)](./AIController-ClearFocus(AIFocusPriority).md 'UnrealEngine.Framework.AIController.ClearFocus(UnrealEngine.Framework.AIFocusPriority)') +- [GetFocalPoint()](./AIController-GetFocalPoint().md 'UnrealEngine.Framework.AIController.GetFocalPoint()') +- [GetFocalPoint(System.Numerics.Vector3)](./AIController-GetFocalPoint(Vector3).md 'UnrealEngine.Framework.AIController.GetFocalPoint(System.Numerics.Vector3)') +- [GetFocusActor()](./AIController-GetFocusActor().md 'UnrealEngine.Framework.AIController.GetFocusActor()') +- [SetFocalPoint(System.Numerics.Vector3, UnrealEngine.Framework.AIFocusPriority)](./AIController-SetFocalPoint(Vector3_AIFocusPriority).md 'UnrealEngine.Framework.AIController.SetFocalPoint(System.Numerics.Vector3, UnrealEngine.Framework.AIFocusPriority)') +- [SetFocus(UnrealEngine.Framework.Actor, UnrealEngine.Framework.AIFocusPriority)](./AIController-SetFocus(Actor_AIFocusPriority).md 'UnrealEngine.Framework.AIController.SetFocus(UnrealEngine.Framework.Actor, UnrealEngine.Framework.AIFocusPriority)') + +``` + +`API/AIFocusPriority.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AIFocusPriority Enum +Defines focus priority for AI +```csharp +public enum AIFocusPriority +``` +### Fields + +`Normal` 0 + + + +`Low` 1 + + + +`High` 2 + + + +`VeryHigh` 3 + + + +``` + +`API/Actor-Actor(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public Actor(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/Actor-AddTag(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.AddTag(string) Method +Adds a tag to the actor that can be used for grouping and categorizing +```csharp +public void AddTag(string tag); +``` +#### Parameters + +`tag` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/Actor-BlockInput.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.BlockInput Property +Gets or sets whether the all input on the stack below this actor will not be considered +```csharp +public bool BlockInput { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Actor-CreationTime.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.CreationTime Property +Gets the time when the actor was created relative to [Time](./World-Time.md 'UnrealEngine.Framework.World.Time') +```csharp +public float CreationTime { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Actor-Destroy().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.Destroy() Method +Returns `true` if the actor is destroyed or already marked for destruction, `false` if indestructible +```csharp +public bool Destroy(); +``` +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Actor-Equals(Actor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.Equals(UnrealEngine.Framework.Actor) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.Actor other); +``` +#### Parameters + +`other` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Actor-ForEachAttachedActor-T-(Action-T-).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.ForEachAttachedActor<T>(System.Action<T>) Method +Performs the specified action on each attached actor if any +```csharp +public void ForEachAttachedActor(System.Action action) + where T : UnrealEngine.Framework.Actor; +``` +#### Type parameters + +`T` + +Constraints [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Parameters + +`action` [System.Action<](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1')[T](#UnrealEngine-Framework-Actor-ForEachAttachedActor-T-(System-Action-T-)-T 'UnrealEngine.Framework.Actor.ForEachAttachedActor<T>(System.Action<T>).T')[>](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1') + + +``` + +`API/Actor-ForEachChildActor-T-(Action-T-).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.ForEachChildActor<T>(System.Action<T>) Method +Performs the specified action on each child actor with [ChildActorComponent](./ChildActorComponent.md 'UnrealEngine.Framework.ChildActorComponent'), including children of child if any +```csharp +public void ForEachChildActor(System.Action action) + where T : UnrealEngine.Framework.Actor; +``` +#### Type parameters + +`T` + +Constraints [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Parameters + +`action` [System.Action<](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1')[T](#UnrealEngine-Framework-Actor-ForEachChildActor-T-(System-Action-T-)-T 'UnrealEngine.Framework.Actor.ForEachChildActor<T>(System.Action<T>).T')[>](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1') + + +``` + +`API/Actor-ForEachComponent-T-(Action-T-).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.ForEachComponent<T>(System.Action<T>) Method +Performs the specified action on each component if any +```csharp +public void ForEachComponent(System.Action action) + where T : UnrealEngine.Framework.ActorComponent; +``` +#### Type parameters + +`T` + +Constraints [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') + +#### Parameters + +`action` [System.Action<](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1')[T](#UnrealEngine-Framework-Actor-ForEachComponent-T-(System-Action-T-)-T 'UnrealEngine.Framework.Actor.ForEachComponent<T>(System.Action<T>).T')[>](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1') + + +``` + +`API/Actor-ForEachOverlappingActor-T-(Action-T-).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.ForEachOverlappingActor<T>(System.Action<T>) Method +Performs the specified action on each overlapping actor if any +```csharp +public void ForEachOverlappingActor(System.Action action) + where T : UnrealEngine.Framework.Actor; +``` +#### Type parameters + +`T` + +Constraints [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Parameters + +`action` [System.Action<](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1')[T](#UnrealEngine-Framework-Actor-ForEachOverlappingActor-T-(System-Action-T-)-T 'UnrealEngine.Framework.Actor.ForEachOverlappingActor<T>(System.Action<T>).T')[>](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1') + + +``` + +`API/Actor-GetBool(string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetBool(string, bool) Method +Retrieves the value of the bool property +```csharp +public bool GetBool(string name, ref bool value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Actor-GetBounds(bool_Vector3_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetBounds(bool, System.Numerics.Vector3, System.Numerics.Vector3) Method +Retrieves the bounding box of all components of the actor +```csharp +public void GetBounds(bool onlyCollidingComponents, ref System.Numerics.Vector3 origin, ref System.Numerics.Vector3 extent); +``` +#### Parameters + +`onlyCollidingComponents` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, will only return the bounding box for components with enabled collision + + +`origin` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +The center of the actor in world space + + +`extent` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Half the actor's size in 3D space + + +``` + +`API/Actor-GetByte(string_byte).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetByte(string, byte) Method +Retrieves the value of the byte property +```csharp +public bool GetByte(string name, ref byte value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-GetComponent-T-(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetComponent<T>(string) Method +Returns the component of the actor if matches the specified type, optionally with the specified name +```csharp +public T GetComponent(string name=null) + where T : UnrealEngine.Framework.ActorComponent; +``` +#### Type parameters + +`T` +The type of the component + +Constraints [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') + +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + +#### Returns +[T](#UnrealEngine-Framework-Actor-GetComponent-T-(string)-T 'UnrealEngine.Framework.Actor.GetComponent<T>(string).T') +A component or `null` on failure + +``` + +`API/Actor-GetComponentByID-T-(uint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetComponentByID<T>(uint) Method +Returns the component of the actor if matches the specified type and ID +```csharp +public T GetComponentByID(uint id) + where T : UnrealEngine.Framework.ActorComponent; +``` +#### Type parameters + +`T` +The type of the component + +Constraints [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') + +#### Parameters + +`id` [System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') +The ID of the component + +#### Returns +[T](#UnrealEngine-Framework-Actor-GetComponentByID-T-(uint)-T 'UnrealEngine.Framework.Actor.GetComponentByID<T>(uint).T') +A component or `null` on failure + +``` + +`API/Actor-GetComponentByTag-T-(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetComponentByTag<T>(string) Method +Returns the component of the actor if matches the specified type and tag +```csharp +public T GetComponentByTag(string tag) + where T : UnrealEngine.Framework.ActorComponent; +``` +#### Type parameters + +`T` +The type of the component + +Constraints [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') + +#### Parameters + +`tag` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The tag of the component + +#### Returns +[T](#UnrealEngine-Framework-Actor-GetComponentByTag-T-(string)-T 'UnrealEngine.Framework.Actor.GetComponentByTag<T>(string).T') +A component or `null` on failure + +``` + +`API/Actor-GetDistanceTo(Actor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetDistanceTo(UnrealEngine.Framework.Actor) Method +Returns the distance from this actor to another one +```csharp +public float GetDistanceTo(UnrealEngine.Framework.Actor actor); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Actor-GetDouble(string_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetDouble(string, double) Method +Retrieves the value of the double property +```csharp +public bool GetDouble(string name, ref double value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-GetEnum-T-(string_T).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetEnum<T>(string, T) Method +Retrieves the value of the enum property +```csharp +public bool GetEnum(string name, ref T value) + where T : System.Enum; +``` +#### Type parameters + +`T` + +Constraints [System.Enum](https://docs.microsoft.com/en-us/dotnet/api/System.Enum 'System.Enum') + +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [T](#UnrealEngine-Framework-Actor-GetEnum-T-(string_T)-T 'UnrealEngine.Framework.Actor.GetEnum<T>(string, T).T') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-GetEyesViewPoint(Vector3_Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetEyesViewPoint(System.Numerics.Vector3, System.Numerics.Quaternion) Method +Retrieves the point of view of the actor +```csharp +public void GetEyesViewPoint(ref System.Numerics.Vector3 location, ref System.Numerics.Quaternion rotation); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/Actor-GetFloat(string_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetFloat(string, float) Method +Retrieves the value of the float property +```csharp +public bool GetFloat(string name, ref float value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/Actor-GetHorizontalDistanceTo(Actor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetHorizontalDistanceTo(UnrealEngine.Framework.Actor) Method +Returns the distance from this actor to another one, ignoring Z axis +```csharp +public float GetHorizontalDistanceTo(UnrealEngine.Framework.Actor actor); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Actor-GetInt(string_int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetInt(string, int) Method +Retrieves the value of the integer property +```csharp +public bool GetInt(string name, ref int value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-GetLong(string_long).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetLong(string, long) Method +Retrieves the value of the long property +```csharp +public bool GetLong(string name, ref long value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int64](https://docs.microsoft.com/en-us/dotnet/api/System.Int64 'System.Int64') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-GetRootComponent-T-().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetRootComponent<T>() Method +Returns the root component of the actor if matches the specified type +```csharp +public T GetRootComponent() + where T : UnrealEngine.Framework.SceneComponent; +``` +#### Type parameters + +`T` + +Constraints [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') + +#### Returns +[T](#UnrealEngine-Framework-Actor-GetRootComponent-T-()-T 'UnrealEngine.Framework.Actor.GetRootComponent<T>().T') +A component or `null` on failure + +``` + +`API/Actor-GetShort(string_short).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetShort(string, short) Method +Retrieves the value of the short property +```csharp +public bool GetShort(string name, ref short value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int16](https://docs.microsoft.com/en-us/dotnet/api/System.Int16 'System.Int16') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-GetString(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetString(string, string) Method +Retrieves the value of the string property +```csharp +public bool GetString(string name, ref string value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-GetText(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetText(string, string) Method +Retrieves the value of the text property +```csharp +public bool GetText(string name, ref string value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-GetUInt(string_uint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetUInt(string, uint) Method +Retrieves the value of the unsigned integer property +```csharp +public bool GetUInt(string name, ref uint value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-GetULong(string_ulong).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetULong(string, ulong) Method +Retrieves the value of the unsigned long property +```csharp +public bool GetULong(string name, ref ulong value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt64](https://docs.microsoft.com/en-us/dotnet/api/System.UInt64 'System.UInt64') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-GetUShort(string_ushort).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.GetUShort(string, ushort) Method +Retrieves the value of the unsigned short property +```csharp +public bool GetUShort(string name, ref ushort value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt16](https://docs.microsoft.com/en-us/dotnet/api/System.UInt16 'System.UInt16') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-HasTag(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.HasTag(string) Method +Indicates whether the actor has a tag +```csharp +public bool HasTag(string tag); +``` +#### Parameters + +`tag` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Actor-Hide(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.Hide(bool) Method +Hides the actor +```csharp +public void Hide(bool value); +``` +#### Parameters + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/Actor-ID.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.ID Property +Returns the unique ID of the actor, reused by the engine, only unique while the actor is alive +```csharp +public uint ID { get; } +``` +#### Property Value +[System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') + +``` + +`API/Actor-InputComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.InputComponent Property +Gets or sets the component that handles input for the actor, if enabled +```csharp +public UnrealEngine.Framework.InputComponent InputComponent { get; set; } +``` +#### Property Value +[InputComponent](./InputComponent.md 'UnrealEngine.Framework.InputComponent') + +``` + +`API/Actor-Invoke(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.Invoke(string) Method +Invokes a command, function, or an event with optional arguments +```csharp +public bool Invoke(string command); +``` +#### Parameters + +`command` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Actor-IsOverlappingActor(Actor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.IsOverlappingActor(UnrealEngine.Framework.Actor) Method +Returns `true` if any component of the actor is overlapping any component of another one +```csharp +public bool IsOverlappingActor(UnrealEngine.Framework.Actor other); +``` +#### Parameters + +`other` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Actor-IsRootComponentMovable.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.IsRootComponentMovable Property +Returns `true` if the root component is [Movable](./ComponentMobility.md#ComponentMobility-Movable 'UnrealEngine.Framework.ComponentMobility.Movable') +```csharp +public bool IsRootComponentMovable { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Actor-IsSpawned.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.IsSpawned Property +Returns `true` if the actor is spawned +```csharp +public bool IsSpawned { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Actor-Name.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.Name Property +Returns the name of the actor +```csharp +public string Name { get; } +``` +#### Property Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Actor-RegisterEvent(ActorEventType).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.RegisterEvent(UnrealEngine.Framework.ActorEventType) Method +Registers an event notification for the actor +```csharp +public void RegisterEvent(UnrealEngine.Framework.ActorEventType type); +``` +#### Parameters + +`type` [ActorEventType](./ActorEventType.md 'UnrealEngine.Framework.ActorEventType') + + +``` + +`API/Actor-RemoveTag(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.RemoveTag(string) Method +Removes a tag from the actor +```csharp +public void RemoveTag(string tag); +``` +#### Parameters + +`tag` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/Actor-Rename(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.Rename(string) Method +Renames the actor +```csharp +public void Rename(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/Actor-SetBool(string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetBool(string, bool) Method +Sets the value of the bool property +```csharp +public bool SetBool(string name, bool value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Actor-SetByte(string_byte).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetByte(string, byte) Method +Sets the value of the byte property +```csharp +public bool SetByte(string name, byte value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-SetDouble(string_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetDouble(string, double) Method +Sets the value of the double property +```csharp +public bool SetDouble(string name, double value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-SetEnableCollision(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetEnableCollision(bool) Method +Sets the collision detection of the actor +```csharp +public void SetEnableCollision(bool value); +``` +#### Parameters + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/Actor-SetEnableInput(PlayerController_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetEnableInput(UnrealEngine.Framework.PlayerController, bool) Method +Sets [InputComponent](./Actor-InputComponent.md 'UnrealEngine.Framework.Actor.InputComponent') for non-pawn actors handled by a [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +```csharp +public void SetEnableInput(UnrealEngine.Framework.PlayerController playerController, bool value); +``` +#### Parameters + +`playerController` [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') + + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/Actor-SetEnum-T-(string_T).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetEnum<T>(string, T) Method +Sets the value of the enum property +```csharp +public bool SetEnum(string name, T value) + where T : System.Enum; +``` +#### Type parameters + +`T` + +Constraints [System.Enum](https://docs.microsoft.com/en-us/dotnet/api/System.Enum 'System.Enum') + +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [T](#UnrealEngine-Framework-Actor-SetEnum-T-(string_T)-T 'UnrealEngine.Framework.Actor.SetEnum<T>(string, T).T') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-SetFloat(string_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetFloat(string, float) Method +Sets the value of the float property +```csharp +public bool SetFloat(string name, float value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-SetInt(string_int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetInt(string, int) Method +Sets the value of the integer property +```csharp +public bool SetInt(string name, int value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-SetLifeSpan(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetLifeSpan(float) Method +Sets the lifespan of the actor, when it expires the actor will be destroyed, if requested lifespan set to zero, the timer is cleared and the actor will remain alive +```csharp +public void SetLifeSpan(float lifeSpan); +``` +#### Parameters + +`lifeSpan` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Lifespan time in seconds + + +``` + +`API/Actor-SetLong(string_long).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetLong(string, long) Method +Sets the value of the long property +```csharp +public bool SetLong(string name, long value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int64](https://docs.microsoft.com/en-us/dotnet/api/System.Int64 'System.Int64') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-SetRootComponent(SceneComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetRootComponent(UnrealEngine.Framework.SceneComponent) Method +Sets the root component, the actor should be the owner of the component +```csharp +public bool SetRootComponent(UnrealEngine.Framework.SceneComponent component); +``` +#### Parameters + +`component` [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if successful + +``` + +`API/Actor-SetShort(string_short).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetShort(string, short) Method +Sets the value of the short property +```csharp +public bool SetShort(string name, short value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int16](https://docs.microsoft.com/en-us/dotnet/api/System.Int16 'System.Int16') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-SetString(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetString(string, string) Method +Sets the value of the string property +```csharp +public bool SetString(string name, string value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-SetText(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetText(string, string) Method +Sets the value of the text property +```csharp +public bool SetText(string name, string value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-SetUInt(string_uint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetUInt(string, uint) Method +Sets the value of the unsigned integer property +```csharp +public bool SetUInt(string name, uint value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-SetULong(string_ulong).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetULong(string, ulong) Method +Sets the value of the unsigned long property +```csharp +public bool SetULong(string name, ulong value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt64](https://docs.microsoft.com/en-us/dotnet/api/System.UInt64 'System.UInt64') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-SetUShort(string_ushort).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.SetUShort(string, ushort) Method +Sets the value of the unsigned short property +```csharp +public bool SetUShort(string name, ushort value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt16](https://docs.microsoft.com/en-us/dotnet/api/System.UInt16 'System.UInt16') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/Actor-TeleportTo(Vector3_Quaternion_bool_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.TeleportTo(System.Numerics.Vector3, System.Numerics.Quaternion, bool, bool) Method +Teleports an actor to a new location +```csharp +public bool TeleportTo(in System.Numerics.Vector3 destinationLocation, in System.Numerics.Quaternion destinationRotation, bool isATest=false, bool noCheck=false); +``` +#### Parameters + +`destinationLocation` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +The target destination point + + +`destinationRotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') +The target rotation at the destination + + +`isATest` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, shouldn't cause any notifications (used by AI pathfinding, for example) + + +`noCheck` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, should skip checking for positioning in the world or relative to other actors trying to slightly move the actor out + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if the actor has been successfully moved + +``` + +`API/Actor-UnregisterEvent(ActorEventType).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Actor](./Actor.md 'UnrealEngine.Framework.Actor') +## Actor.UnregisterEvent(UnrealEngine.Framework.ActorEventType) Method +Unregisters an event notification for the actor +```csharp +public void UnregisterEvent(UnrealEngine.Framework.ActorEventType type); +``` +#### Parameters + +`type` [ActorEventType](./ActorEventType.md 'UnrealEngine.Framework.ActorEventType') + + +``` + +`API/Actor.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Actor Class +The base class of an object that can be placed or spawned in a level +```csharp +public class Actor : +System.IEquatable +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Actor + +Derived +↳ [AmbientSound](./AmbientSound.md 'UnrealEngine.Framework.AmbientSound') +↳ [Brush](./Brush.md 'UnrealEngine.Framework.Brush') +↳ [Camera](./Camera.md 'UnrealEngine.Framework.Camera') +↳ [Controller](./Controller.md 'UnrealEngine.Framework.Controller') +↳ [GameModeBase](./GameModeBase.md 'UnrealEngine.Framework.GameModeBase') +↳ [LevelScript](./LevelScript.md 'UnrealEngine.Framework.LevelScript') +↳ [Light](./Light.md 'UnrealEngine.Framework.Light') +↳ [Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +↳ [TriggerBase](./TriggerBase.md 'UnrealEngine.Framework.TriggerBase') + +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[Actor](./Actor.md 'UnrealEngine.Framework.Actor')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Constructors +- [Actor(string, UnrealEngine.Framework.Blueprint)](./Actor-Actor(string_Blueprint).md 'UnrealEngine.Framework.Actor.Actor(string, UnrealEngine.Framework.Blueprint)') +### Properties +- [BlockInput](./Actor-BlockInput.md 'UnrealEngine.Framework.Actor.BlockInput') +- [CreationTime](./Actor-CreationTime.md 'UnrealEngine.Framework.Actor.CreationTime') +- [ID](./Actor-ID.md 'UnrealEngine.Framework.Actor.ID') +- [InputComponent](./Actor-InputComponent.md 'UnrealEngine.Framework.Actor.InputComponent') +- [IsRootComponentMovable](./Actor-IsRootComponentMovable.md 'UnrealEngine.Framework.Actor.IsRootComponentMovable') +- [IsSpawned](./Actor-IsSpawned.md 'UnrealEngine.Framework.Actor.IsSpawned') +- [Name](./Actor-Name.md 'UnrealEngine.Framework.Actor.Name') +### Methods +- [AddTag(string)](./Actor-AddTag(string).md 'UnrealEngine.Framework.Actor.AddTag(string)') +- [Destroy()](./Actor-Destroy().md 'UnrealEngine.Framework.Actor.Destroy()') +- [Equals(UnrealEngine.Framework.Actor)](./Actor-Equals(Actor).md 'UnrealEngine.Framework.Actor.Equals(UnrealEngine.Framework.Actor)') +- [ForEachAttachedActor<T>(System.Action<T>)](./Actor-ForEachAttachedActor-T-(Action-T-).md 'UnrealEngine.Framework.Actor.ForEachAttachedActor<T>(System.Action<T>)') +- [ForEachChildActor<T>(System.Action<T>)](./Actor-ForEachChildActor-T-(Action-T-).md 'UnrealEngine.Framework.Actor.ForEachChildActor<T>(System.Action<T>)') +- [ForEachComponent<T>(System.Action<T>)](./Actor-ForEachComponent-T-(Action-T-).md 'UnrealEngine.Framework.Actor.ForEachComponent<T>(System.Action<T>)') +- [ForEachOverlappingActor<T>(System.Action<T>)](./Actor-ForEachOverlappingActor-T-(Action-T-).md 'UnrealEngine.Framework.Actor.ForEachOverlappingActor<T>(System.Action<T>)') +- [GetBool(string, bool)](./Actor-GetBool(string_bool).md 'UnrealEngine.Framework.Actor.GetBool(string, bool)') +- [GetBounds(bool, System.Numerics.Vector3, System.Numerics.Vector3)](./Actor-GetBounds(bool_Vector3_Vector3).md 'UnrealEngine.Framework.Actor.GetBounds(bool, System.Numerics.Vector3, System.Numerics.Vector3)') +- [GetByte(string, byte)](./Actor-GetByte(string_byte).md 'UnrealEngine.Framework.Actor.GetByte(string, byte)') +- [GetComponent<T>(string)](./Actor-GetComponent-T-(string).md 'UnrealEngine.Framework.Actor.GetComponent<T>(string)') +- [GetComponentByID<T>(uint)](./Actor-GetComponentByID-T-(uint).md 'UnrealEngine.Framework.Actor.GetComponentByID<T>(uint)') +- [GetComponentByTag<T>(string)](./Actor-GetComponentByTag-T-(string).md 'UnrealEngine.Framework.Actor.GetComponentByTag<T>(string)') +- [GetDistanceTo(UnrealEngine.Framework.Actor)](./Actor-GetDistanceTo(Actor).md 'UnrealEngine.Framework.Actor.GetDistanceTo(UnrealEngine.Framework.Actor)') +- [GetDouble(string, double)](./Actor-GetDouble(string_double).md 'UnrealEngine.Framework.Actor.GetDouble(string, double)') +- [GetEnum<T>(string, T)](./Actor-GetEnum-T-(string_T).md 'UnrealEngine.Framework.Actor.GetEnum<T>(string, T)') +- [GetEyesViewPoint(System.Numerics.Vector3, System.Numerics.Quaternion)](./Actor-GetEyesViewPoint(Vector3_Quaternion).md 'UnrealEngine.Framework.Actor.GetEyesViewPoint(System.Numerics.Vector3, System.Numerics.Quaternion)') +- [GetFloat(string, float)](./Actor-GetFloat(string_float).md 'UnrealEngine.Framework.Actor.GetFloat(string, float)') +- [GetHashCode()](./Actor-GetHashCode().md 'UnrealEngine.Framework.Actor.GetHashCode()') +- [GetHorizontalDistanceTo(UnrealEngine.Framework.Actor)](./Actor-GetHorizontalDistanceTo(Actor).md 'UnrealEngine.Framework.Actor.GetHorizontalDistanceTo(UnrealEngine.Framework.Actor)') +- [GetInt(string, int)](./Actor-GetInt(string_int).md 'UnrealEngine.Framework.Actor.GetInt(string, int)') +- [GetLong(string, long)](./Actor-GetLong(string_long).md 'UnrealEngine.Framework.Actor.GetLong(string, long)') +- [GetRootComponent<T>()](./Actor-GetRootComponent-T-().md 'UnrealEngine.Framework.Actor.GetRootComponent<T>()') +- [GetShort(string, short)](./Actor-GetShort(string_short).md 'UnrealEngine.Framework.Actor.GetShort(string, short)') +- [GetString(string, string)](./Actor-GetString(string_string).md 'UnrealEngine.Framework.Actor.GetString(string, string)') +- [GetText(string, string)](./Actor-GetText(string_string).md 'UnrealEngine.Framework.Actor.GetText(string, string)') +- [GetUInt(string, uint)](./Actor-GetUInt(string_uint).md 'UnrealEngine.Framework.Actor.GetUInt(string, uint)') +- [GetULong(string, ulong)](./Actor-GetULong(string_ulong).md 'UnrealEngine.Framework.Actor.GetULong(string, ulong)') +- [GetUShort(string, ushort)](./Actor-GetUShort(string_ushort).md 'UnrealEngine.Framework.Actor.GetUShort(string, ushort)') +- [HasTag(string)](./Actor-HasTag(string).md 'UnrealEngine.Framework.Actor.HasTag(string)') +- [Hide(bool)](./Actor-Hide(bool).md 'UnrealEngine.Framework.Actor.Hide(bool)') +- [Invoke(string)](./Actor-Invoke(string).md 'UnrealEngine.Framework.Actor.Invoke(string)') +- [IsOverlappingActor(UnrealEngine.Framework.Actor)](./Actor-IsOverlappingActor(Actor).md 'UnrealEngine.Framework.Actor.IsOverlappingActor(UnrealEngine.Framework.Actor)') +- [RegisterEvent(UnrealEngine.Framework.ActorEventType)](./Actor-RegisterEvent(ActorEventType).md 'UnrealEngine.Framework.Actor.RegisterEvent(UnrealEngine.Framework.ActorEventType)') +- [RemoveTag(string)](./Actor-RemoveTag(string).md 'UnrealEngine.Framework.Actor.RemoveTag(string)') +- [Rename(string)](./Actor-Rename(string).md 'UnrealEngine.Framework.Actor.Rename(string)') +- [SetBool(string, bool)](./Actor-SetBool(string_bool).md 'UnrealEngine.Framework.Actor.SetBool(string, bool)') +- [SetByte(string, byte)](./Actor-SetByte(string_byte).md 'UnrealEngine.Framework.Actor.SetByte(string, byte)') +- [SetDouble(string, double)](./Actor-SetDouble(string_double).md 'UnrealEngine.Framework.Actor.SetDouble(string, double)') +- [SetEnableCollision(bool)](./Actor-SetEnableCollision(bool).md 'UnrealEngine.Framework.Actor.SetEnableCollision(bool)') +- [SetEnableInput(UnrealEngine.Framework.PlayerController, bool)](./Actor-SetEnableInput(PlayerController_bool).md 'UnrealEngine.Framework.Actor.SetEnableInput(UnrealEngine.Framework.PlayerController, bool)') +- [SetEnum<T>(string, T)](./Actor-SetEnum-T-(string_T).md 'UnrealEngine.Framework.Actor.SetEnum<T>(string, T)') +- [SetFloat(string, float)](./Actor-SetFloat(string_float).md 'UnrealEngine.Framework.Actor.SetFloat(string, float)') +- [SetInt(string, int)](./Actor-SetInt(string_int).md 'UnrealEngine.Framework.Actor.SetInt(string, int)') +- [SetLifeSpan(float)](./Actor-SetLifeSpan(float).md 'UnrealEngine.Framework.Actor.SetLifeSpan(float)') +- [SetLong(string, long)](./Actor-SetLong(string_long).md 'UnrealEngine.Framework.Actor.SetLong(string, long)') +- [SetRootComponent(UnrealEngine.Framework.SceneComponent)](./Actor-SetRootComponent(SceneComponent).md 'UnrealEngine.Framework.Actor.SetRootComponent(UnrealEngine.Framework.SceneComponent)') +- [SetShort(string, short)](./Actor-SetShort(string_short).md 'UnrealEngine.Framework.Actor.SetShort(string, short)') +- [SetString(string, string)](./Actor-SetString(string_string).md 'UnrealEngine.Framework.Actor.SetString(string, string)') +- [SetText(string, string)](./Actor-SetText(string_string).md 'UnrealEngine.Framework.Actor.SetText(string, string)') +- [SetUInt(string, uint)](./Actor-SetUInt(string_uint).md 'UnrealEngine.Framework.Actor.SetUInt(string, uint)') +- [SetULong(string, ulong)](./Actor-SetULong(string_ulong).md 'UnrealEngine.Framework.Actor.SetULong(string, ulong)') +- [SetUShort(string, ushort)](./Actor-SetUShort(string_ushort).md 'UnrealEngine.Framework.Actor.SetUShort(string, ushort)') +- [TeleportTo(System.Numerics.Vector3, System.Numerics.Quaternion, bool, bool)](./Actor-TeleportTo(Vector3_Quaternion_bool_bool).md 'UnrealEngine.Framework.Actor.TeleportTo(System.Numerics.Vector3, System.Numerics.Quaternion, bool, bool)') +- [UnregisterEvent(UnrealEngine.Framework.ActorEventType)](./Actor-UnregisterEvent(ActorEventType).md 'UnrealEngine.Framework.Actor.UnregisterEvent(UnrealEngine.Framework.ActorEventType)') + +``` + +`API/ActorComponent-AddTag(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.AddTag(string) Method +Adds a tag to the component that can be used for grouping and categorizing +```csharp +public void AddTag(string tag); +``` +#### Parameters + +`tag` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/ActorComponent-Destroy(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.Destroy(bool) Method +Unregisters the component, removes it from its outer actor's components array and marks for pending kill +```csharp +public void Destroy(bool promoteChild=false); +``` +#### Parameters + +`promoteChild` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +Promotes the child component in the hierarchy during the destruction + + +``` + +`API/ActorComponent-Equals(ActorComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.Equals(UnrealEngine.Framework.ActorComponent) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.ActorComponent other); +``` +#### Parameters + +`other` [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ActorComponent-GetActor-T-().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetActor<T>() Method +Returns the component's owner actor of the specified class +```csharp +public T GetActor() + where T : UnrealEngine.Framework.Actor; +``` +#### Type parameters + +`T` + +Constraints [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Returns +[T](#UnrealEngine-Framework-ActorComponent-GetActor-T-()-T 'UnrealEngine.Framework.ActorComponent.GetActor<T>().T') +An actor or `null` on failure + +``` + +`API/ActorComponent-GetBool(string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetBool(string, bool) Method +Retrieves the value of the bool property +```csharp +public bool GetBool(string name, ref bool value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ActorComponent-GetByte(string_byte).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetByte(string, byte) Method +Retrieves the value of the byte property +```csharp +public bool GetByte(string name, ref byte value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-GetDouble(string_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetDouble(string, double) Method +Retrieves the value of the double property +```csharp +public bool GetDouble(string name, ref double value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-GetEnum-T-(string_T).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetEnum<T>(string, T) Method +Retrieves the value of the enum property +```csharp +public bool GetEnum(string name, ref T value) + where T : System.Enum; +``` +#### Type parameters + +`T` + +Constraints [System.Enum](https://docs.microsoft.com/en-us/dotnet/api/System.Enum 'System.Enum') + +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [T](#UnrealEngine-Framework-ActorComponent-GetEnum-T-(string_T)-T 'UnrealEngine.Framework.ActorComponent.GetEnum<T>(string, T).T') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-GetFloat(string_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetFloat(string, float) Method +Retrieves the value of the float property +```csharp +public bool GetFloat(string name, ref float value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/ActorComponent-GetInt(string_int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetInt(string, int) Method +Retrieves the value of the integer property +```csharp +public bool GetInt(string name, ref int value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-GetLong(string_long).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetLong(string, long) Method +Retrieves the value of the long property +```csharp +public bool GetLong(string name, ref long value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int64](https://docs.microsoft.com/en-us/dotnet/api/System.Int64 'System.Int64') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-GetShort(string_short).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetShort(string, short) Method +Retrieves the value of the short property +```csharp +public bool GetShort(string name, ref short value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int16](https://docs.microsoft.com/en-us/dotnet/api/System.Int16 'System.Int16') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-GetString(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetString(string, string) Method +Retrieves the value of the string property +```csharp +public bool GetString(string name, ref string value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-GetText(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetText(string, string) Method +Retrieves the value of the text property +```csharp +public bool GetText(string name, ref string value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-GetUInt(string_uint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetUInt(string, uint) Method +Retrieves the value of the unsigned integer property +```csharp +public bool GetUInt(string name, ref uint value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-GetULong(string_ulong).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetULong(string, ulong) Method +Retrieves the value of the unsigned long property +```csharp +public bool GetULong(string name, ref ulong value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt64](https://docs.microsoft.com/en-us/dotnet/api/System.UInt64 'System.UInt64') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-GetUShort(string_ushort).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.GetUShort(string, ushort) Method +Retrieves the value of the unsigned short property +```csharp +public bool GetUShort(string name, ref ushort value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt16](https://docs.microsoft.com/en-us/dotnet/api/System.UInt16 'System.UInt16') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-HasTag(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.HasTag(string) Method +Indicates whether the component has a tag +```csharp +public bool HasTag(string tag); +``` +#### Parameters + +`tag` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ActorComponent-ID.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.ID Property +Returns the unique ID of the component, reused by the engine, only unique while the component is alive +```csharp +public uint ID { get; } +``` +#### Property Value +[System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') + +``` + +`API/ActorComponent-Invoke(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.Invoke(string) Method +Invokes a command, function, or an event with optional arguments +```csharp +public bool Invoke(string command); +``` +#### Parameters + +`command` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ActorComponent-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ActorComponent-IsOwnerSelected.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.IsOwnerSelected Property +Returns `true` if the component's owner is selected in the editor +```csharp +public bool IsOwnerSelected { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +### Remarks +Editor functionality + +``` + +`API/ActorComponent-Name.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.Name Property +Returns the name of the component +```csharp +public string Name { get; } +``` +#### Property Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/ActorComponent-RemoveTag(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.RemoveTag(string) Method +Removes a tag from the component +```csharp +public void RemoveTag(string tag); +``` +#### Parameters + +`tag` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/ActorComponent-Rename(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.Rename(string) Method +Renames the component +```csharp +public void Rename(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/ActorComponent-SetBool(string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.SetBool(string, bool) Method +Sets the value of the bool property +```csharp +public bool SetBool(string name, bool value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ActorComponent-SetByte(string_byte).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.SetByte(string, byte) Method +Sets the value of the byte property +```csharp +public bool SetByte(string name, byte value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-SetDouble(string_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.SetDouble(string, double) Method +Sets the value of the double property +```csharp +public bool SetDouble(string name, double value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-SetEnum-T-(string_T).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.SetEnum<T>(string, T) Method +Sets the value of the enum property +```csharp +public bool SetEnum(string name, T value) + where T : System.Enum; +``` +#### Type parameters + +`T` + +Constraints [System.Enum](https://docs.microsoft.com/en-us/dotnet/api/System.Enum 'System.Enum') + +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [T](#UnrealEngine-Framework-ActorComponent-SetEnum-T-(string_T)-T 'UnrealEngine.Framework.ActorComponent.SetEnum<T>(string, T).T') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-SetFloat(string_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.SetFloat(string, float) Method +Sets the value of the float property +```csharp +public bool SetFloat(string name, float value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-SetInt(string_int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.SetInt(string, int) Method +Sets the value of the integer property +```csharp +public bool SetInt(string name, int value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-SetLong(string_long).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.SetLong(string, long) Method +Sets the value of the long property +```csharp +public bool SetLong(string name, long value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int64](https://docs.microsoft.com/en-us/dotnet/api/System.Int64 'System.Int64') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-SetShort(string_short).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.SetShort(string, short) Method +Sets the value of the short property +```csharp +public bool SetShort(string name, short value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int16](https://docs.microsoft.com/en-us/dotnet/api/System.Int16 'System.Int16') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-SetString(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.SetString(string, string) Method +Sets the value of the string property +```csharp +public bool SetString(string name, string value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-SetText(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.SetText(string, string) Method +Sets the value of the text property +```csharp +public bool SetText(string name, string value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-SetUInt(string_uint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.SetUInt(string, uint) Method +Sets the value of the unsigned integer property +```csharp +public bool SetUInt(string name, uint value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-SetULong(string_ulong).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.SetULong(string, ulong) Method +Sets the value of the unsigned long property +```csharp +public bool SetULong(string name, ulong value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt64](https://docs.microsoft.com/en-us/dotnet/api/System.UInt64 'System.UInt64') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent-SetUShort(string_ushort).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +## ActorComponent.SetUShort(string, ushort) Method +Sets the value of the unsigned short property +```csharp +public bool SetUShort(string name, ushort value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt16](https://docs.microsoft.com/en-us/dotnet/api/System.UInt16 'System.UInt16') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/ActorComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ActorComponent Class +The base class of components that define reusable behavior and can be added to different types of actors +```csharp +public abstract class ActorComponent : +System.IEquatable +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 ActorComponent + +Derived +↳ [InputComponent](./InputComponent.md 'UnrealEngine.Framework.InputComponent') +↳ [MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +↳ [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') + +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [ID](./ActorComponent-ID.md 'UnrealEngine.Framework.ActorComponent.ID') +- [IsCreated](./ActorComponent-IsCreated.md 'UnrealEngine.Framework.ActorComponent.IsCreated') +- [IsOwnerSelected](./ActorComponent-IsOwnerSelected.md 'UnrealEngine.Framework.ActorComponent.IsOwnerSelected') +- [Name](./ActorComponent-Name.md 'UnrealEngine.Framework.ActorComponent.Name') +### Methods +- [AddTag(string)](./ActorComponent-AddTag(string).md 'UnrealEngine.Framework.ActorComponent.AddTag(string)') +- [Destroy(bool)](./ActorComponent-Destroy(bool).md 'UnrealEngine.Framework.ActorComponent.Destroy(bool)') +- [Equals(UnrealEngine.Framework.ActorComponent)](./ActorComponent-Equals(ActorComponent).md 'UnrealEngine.Framework.ActorComponent.Equals(UnrealEngine.Framework.ActorComponent)') +- [GetActor<T>()](./ActorComponent-GetActor-T-().md 'UnrealEngine.Framework.ActorComponent.GetActor<T>()') +- [GetBool(string, bool)](./ActorComponent-GetBool(string_bool).md 'UnrealEngine.Framework.ActorComponent.GetBool(string, bool)') +- [GetByte(string, byte)](./ActorComponent-GetByte(string_byte).md 'UnrealEngine.Framework.ActorComponent.GetByte(string, byte)') +- [GetDouble(string, double)](./ActorComponent-GetDouble(string_double).md 'UnrealEngine.Framework.ActorComponent.GetDouble(string, double)') +- [GetEnum<T>(string, T)](./ActorComponent-GetEnum-T-(string_T).md 'UnrealEngine.Framework.ActorComponent.GetEnum<T>(string, T)') +- [GetFloat(string, float)](./ActorComponent-GetFloat(string_float).md 'UnrealEngine.Framework.ActorComponent.GetFloat(string, float)') +- [GetHashCode()](./ActorComponent-GetHashCode().md 'UnrealEngine.Framework.ActorComponent.GetHashCode()') +- [GetInt(string, int)](./ActorComponent-GetInt(string_int).md 'UnrealEngine.Framework.ActorComponent.GetInt(string, int)') +- [GetLong(string, long)](./ActorComponent-GetLong(string_long).md 'UnrealEngine.Framework.ActorComponent.GetLong(string, long)') +- [GetShort(string, short)](./ActorComponent-GetShort(string_short).md 'UnrealEngine.Framework.ActorComponent.GetShort(string, short)') +- [GetString(string, string)](./ActorComponent-GetString(string_string).md 'UnrealEngine.Framework.ActorComponent.GetString(string, string)') +- [GetText(string, string)](./ActorComponent-GetText(string_string).md 'UnrealEngine.Framework.ActorComponent.GetText(string, string)') +- [GetUInt(string, uint)](./ActorComponent-GetUInt(string_uint).md 'UnrealEngine.Framework.ActorComponent.GetUInt(string, uint)') +- [GetULong(string, ulong)](./ActorComponent-GetULong(string_ulong).md 'UnrealEngine.Framework.ActorComponent.GetULong(string, ulong)') +- [GetUShort(string, ushort)](./ActorComponent-GetUShort(string_ushort).md 'UnrealEngine.Framework.ActorComponent.GetUShort(string, ushort)') +- [HasTag(string)](./ActorComponent-HasTag(string).md 'UnrealEngine.Framework.ActorComponent.HasTag(string)') +- [Invoke(string)](./ActorComponent-Invoke(string).md 'UnrealEngine.Framework.ActorComponent.Invoke(string)') +- [RemoveTag(string)](./ActorComponent-RemoveTag(string).md 'UnrealEngine.Framework.ActorComponent.RemoveTag(string)') +- [Rename(string)](./ActorComponent-Rename(string).md 'UnrealEngine.Framework.ActorComponent.Rename(string)') +- [SetBool(string, bool)](./ActorComponent-SetBool(string_bool).md 'UnrealEngine.Framework.ActorComponent.SetBool(string, bool)') +- [SetByte(string, byte)](./ActorComponent-SetByte(string_byte).md 'UnrealEngine.Framework.ActorComponent.SetByte(string, byte)') +- [SetDouble(string, double)](./ActorComponent-SetDouble(string_double).md 'UnrealEngine.Framework.ActorComponent.SetDouble(string, double)') +- [SetEnum<T>(string, T)](./ActorComponent-SetEnum-T-(string_T).md 'UnrealEngine.Framework.ActorComponent.SetEnum<T>(string, T)') +- [SetFloat(string, float)](./ActorComponent-SetFloat(string_float).md 'UnrealEngine.Framework.ActorComponent.SetFloat(string, float)') +- [SetInt(string, int)](./ActorComponent-SetInt(string_int).md 'UnrealEngine.Framework.ActorComponent.SetInt(string, int)') +- [SetLong(string, long)](./ActorComponent-SetLong(string_long).md 'UnrealEngine.Framework.ActorComponent.SetLong(string, long)') +- [SetShort(string, short)](./ActorComponent-SetShort(string_short).md 'UnrealEngine.Framework.ActorComponent.SetShort(string, short)') +- [SetString(string, string)](./ActorComponent-SetString(string_string).md 'UnrealEngine.Framework.ActorComponent.SetString(string, string)') +- [SetText(string, string)](./ActorComponent-SetText(string_string).md 'UnrealEngine.Framework.ActorComponent.SetText(string, string)') +- [SetUInt(string, uint)](./ActorComponent-SetUInt(string_uint).md 'UnrealEngine.Framework.ActorComponent.SetUInt(string, uint)') +- [SetULong(string, ulong)](./ActorComponent-SetULong(string_ulong).md 'UnrealEngine.Framework.ActorComponent.SetULong(string, ulong)') +- [SetUShort(string, ushort)](./ActorComponent-SetUShort(string_ushort).md 'UnrealEngine.Framework.ActorComponent.SetUShort(string, ushort)') + +``` + +`API/ActorCursorDelegate(ActorReference).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ActorCursorDelegate(UnrealEngine.Framework.ActorReference) Delegate +Delegate for actor cursor events +```csharp +public delegate void ActorCursorDelegate(UnrealEngine.Framework.ActorReference actor); +``` +#### Parameters + +`actor` [ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') + + +``` + +`API/ActorEventType.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ActorEventType Enum +Defines actor events +```csharp +public enum ActorEventType +``` +### Fields + +`OnActorBeginOverlap` 0 +Called when actors start overlapping + + +`OnActorEndOverlap` 1 +Called when actors stop overlapping + + +`OnActorHit` 2 +Called when actors hit collisions + + +`OnActorBeginCursorOver` 3 +Called when the mouse cursor is moved over an actor if mouse over events are enabled in the player controller + + +`OnActorEndCursorOver` 4 +Called when the mouse cursor is moved off an actor if mouse over events are enabled in the player controller + + +`OnActorClicked` 5 +Called when the mouse button is clicked while the mouse is over an actor if click events are enabled in the player controller + + +`OnActorReleased` 6 +Called when the mouse button is released while the mouse is over an actor if click events are enabled in the player controller + + +``` + +`API/ActorHitDelegate(ActorReference_ActorReference_Vector3_Hit).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ActorHitDelegate(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference, System.Numerics.Vector3, UnrealEngine.Framework.Hit) Delegate +Delegate for actor hit events +```csharp +public delegate void ActorHitDelegate(UnrealEngine.Framework.ActorReference hitActor, UnrealEngine.Framework.ActorReference otherActor, in System.Numerics.Vector3 normalImpulse, in UnrealEngine.Framework.Hit hit); +``` +#### Parameters + +`hitActor` [ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') + + +`otherActor` [ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') + + +`normalImpulse` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`hit` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +``` + +`API/ActorKeyDelegate(ActorReference_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ActorKeyDelegate(UnrealEngine.Framework.ActorReference, string) Delegate +Delegate for actor key events +```csharp +public delegate void ActorKeyDelegate(UnrealEngine.Framework.ActorReference actor, string key); +``` +#### Parameters + +`actor` [ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') + + +`key` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/ActorOverlapDelegate(ActorReference_ActorReference).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ActorOverlapDelegate(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference) Delegate +Delegate for actor overlap events +```csharp +public delegate void ActorOverlapDelegate(UnrealEngine.Framework.ActorReference overlapActor, UnrealEngine.Framework.ActorReference otherActor); +``` +#### Parameters + +`overlapActor` [ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') + + +`otherActor` [ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') + + +``` + +`API/ActorReference-Equals(ActorReference).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') +## ActorReference.Equals(UnrealEngine.Framework.ActorReference) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.ActorReference other); +``` +#### Parameters + +`other` [ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ActorReference-Equals(object).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') +## ActorReference.Equals(object) Method +Indicates equality of objects +```csharp +public override bool Equals(object value); +``` +#### Parameters + +`value` [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ActorReference-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') +## ActorReference.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/ActorReference-ID.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') +## ActorReference.ID Property +Returns the unique ID of the object, reused by the engine, only unique while the object is alive +```csharp +public uint ID { get; } +``` +#### Property Value +[System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') + +``` + +`API/ActorReference-IsSpawned.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') +## ActorReference.IsSpawned Property +Returns `true` if the actor is spawned +```csharp +public bool IsSpawned { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ActorReference-Name.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') +## ActorReference.Name Property +Returns the name of the object +```csharp +public string Name { get; } +``` +#### Property Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/ActorReference-ToActor-T-().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') +## ActorReference.ToActor<T>() Method +Converts the actor reference to the actor of the specified class +```csharp +public T ToActor() + where T : UnrealEngine.Framework.Actor; +``` +#### Type parameters + +`T` + +Constraints [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Returns +[T](#UnrealEngine-Framework-ActorReference-ToActor-T-()-T 'UnrealEngine.Framework.ActorReference.ToActor<T>().T') +An actor or `null` on failure + +``` + +`API/ActorReference-op_Equality(ActorReference_ActorReference).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') +## ActorReference.operator ==(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference) Operator +Tests for equality between two objects +```csharp +public static bool operator ==(UnrealEngine.Framework.ActorReference left, UnrealEngine.Framework.ActorReference right); +``` +#### Parameters + +`left` [ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') + + +`right` [ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ActorReference-op_Inequality(ActorReference_ActorReference).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') +## ActorReference.operator !=(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference) Operator +Tests for inequality between two objects +```csharp +public static bool operator !=(UnrealEngine.Framework.ActorReference left, UnrealEngine.Framework.ActorReference right); +``` +#### Parameters + +`left` [ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') + + +`right` [ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ActorReference.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ActorReference Struct +A representation of the engine's actor reference +```csharp +public struct ActorReference : +System.IEquatable +``` +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [ID](./ActorReference-ID.md 'UnrealEngine.Framework.ActorReference.ID') +- [IsSpawned](./ActorReference-IsSpawned.md 'UnrealEngine.Framework.ActorReference.IsSpawned') +- [Name](./ActorReference-Name.md 'UnrealEngine.Framework.ActorReference.Name') +### Methods +- [Equals(object)](./ActorReference-Equals(object).md 'UnrealEngine.Framework.ActorReference.Equals(object)') +- [Equals(UnrealEngine.Framework.ActorReference)](./ActorReference-Equals(ActorReference).md 'UnrealEngine.Framework.ActorReference.Equals(UnrealEngine.Framework.ActorReference)') +- [GetHashCode()](./ActorReference-GetHashCode().md 'UnrealEngine.Framework.ActorReference.GetHashCode()') +- [ToActor<T>()](./ActorReference-ToActor-T-().md 'UnrealEngine.Framework.ActorReference.ToActor<T>()') +### Operators +- [operator ==(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference)](./ActorReference-op_Equality(ActorReference_ActorReference).md 'UnrealEngine.Framework.ActorReference.op_Equality(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference)') +- [operator !=(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference)](./ActorReference-op_Inequality(ActorReference_ActorReference).md 'UnrealEngine.Framework.ActorReference.op_Inequality(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference)') + +``` + +`API/AmbientSound-AmbientSound(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AmbientSound](./AmbientSound.md 'UnrealEngine.Framework.AmbientSound') +## AmbientSound(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public AmbientSound(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/AmbientSound.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AmbientSound Class +A sound actor that can be placed in a level +```csharp +public class AmbientSound : UnrealEngine.Framework.Actor +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 AmbientSound +### Constructors +- [AmbientSound(string, UnrealEngine.Framework.Blueprint)](./AmbientSound-AmbientSound(string_Blueprint).md 'UnrealEngine.Framework.AmbientSound.AmbientSound(string, UnrealEngine.Framework.Blueprint)') + +``` + +`API/AnimationAsset-Equals(AnimationAsset).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationAsset](./AnimationAsset.md 'UnrealEngine.Framework.AnimationAsset') +## AnimationAsset.Equals(UnrealEngine.Framework.AnimationAsset) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.AnimationAsset other); +``` +#### Parameters + +`other` [AnimationAsset](./AnimationAsset.md 'UnrealEngine.Framework.AnimationAsset') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AnimationAsset-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationAsset](./AnimationAsset.md 'UnrealEngine.Framework.AnimationAsset') +## AnimationAsset.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/AnimationAsset-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationAsset](./AnimationAsset.md 'UnrealEngine.Framework.AnimationAsset') +## AnimationAsset.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AnimationAsset.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AnimationAsset Class +The base class of animation assets that can be played and evaluated to produce a pose +```csharp +public abstract class AnimationAsset : +System.IEquatable +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 AnimationAsset + +Derived +↳ [AnimationSequenceBase](./AnimationSequenceBase.md 'UnrealEngine.Framework.AnimationSequenceBase') + +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[AnimationAsset](./AnimationAsset.md 'UnrealEngine.Framework.AnimationAsset')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [IsCreated](./AnimationAsset-IsCreated.md 'UnrealEngine.Framework.AnimationAsset.IsCreated') +### Methods +- [Equals(UnrealEngine.Framework.AnimationAsset)](./AnimationAsset-Equals(AnimationAsset).md 'UnrealEngine.Framework.AnimationAsset.Equals(UnrealEngine.Framework.AnimationAsset)') +- [GetHashCode()](./AnimationAsset-GetHashCode().md 'UnrealEngine.Framework.AnimationAsset.GetHashCode()') + +``` + +`API/AnimationCompositeBase.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AnimationCompositeBase Class +The base class of animation composites +```csharp +public abstract class AnimationCompositeBase : UnrealEngine.Framework.AnimationSequenceBase +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [AnimationAsset](./AnimationAsset.md 'UnrealEngine.Framework.AnimationAsset') 🡒 [AnimationSequenceBase](./AnimationSequenceBase.md 'UnrealEngine.Framework.AnimationSequenceBase') 🡒 AnimationCompositeBase + +Derived +↳ [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + +``` + +`API/AnimationInstance-Equals(AnimationInstance).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.Equals(UnrealEngine.Framework.AnimationInstance) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.AnimationInstance other); +``` +#### Parameters + +`other` [AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AnimationInstance-GetBlendTime(AnimationMontage).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetBlendTime(UnrealEngine.Framework.AnimationMontage) Method +Returns the current blend time of the animation montage +```csharp +public float GetBlendTime(UnrealEngine.Framework.AnimationMontage montage); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/AnimationInstance-GetBool(string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetBool(string, bool) Method +Retrieves the value of the bool property +```csharp +public bool GetBool(string name, ref bool value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AnimationInstance-GetByte(string_byte).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetByte(string, byte) Method +Retrieves the value of the byte property +```csharp +public bool GetByte(string name, ref byte value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-GetCurrentActiveMontage().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetCurrentActiveMontage() Method +Returns the current active animation montage or `null` on failure +```csharp +public UnrealEngine.Framework.AnimationMontage GetCurrentActiveMontage(); +``` +#### Returns +[AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + +``` + +`API/AnimationInstance-GetCurrentSection(AnimationMontage).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetCurrentSection(UnrealEngine.Framework.AnimationMontage) Method +Returns the name of the current animation montage section +```csharp +public string GetCurrentSection(UnrealEngine.Framework.AnimationMontage montage); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/AnimationInstance-GetDouble(string_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetDouble(string, double) Method +Retrieves the value of the double property +```csharp +public bool GetDouble(string name, ref double value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-GetEnum-T-(string_T).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetEnum<T>(string, T) Method +Retrieves the value of the enum property +```csharp +public bool GetEnum(string name, ref T value) + where T : System.Enum; +``` +#### Type parameters + +`T` + +Constraints [System.Enum](https://docs.microsoft.com/en-us/dotnet/api/System.Enum 'System.Enum') + +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [T](#UnrealEngine-Framework-AnimationInstance-GetEnum-T-(string_T)-T 'UnrealEngine.Framework.AnimationInstance.GetEnum<T>(string, T).T') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-GetFloat(string_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetFloat(string, float) Method +Retrieves the value of the float property +```csharp +public bool GetFloat(string name, ref float value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/AnimationInstance-GetInt(string_int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetInt(string, int) Method +Retrieves the value of the integer property +```csharp +public bool GetInt(string name, ref int value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-GetLong(string_long).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetLong(string, long) Method +Retrieves the value of the long property +```csharp +public bool GetLong(string name, ref long value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int64](https://docs.microsoft.com/en-us/dotnet/api/System.Int64 'System.Int64') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-GetPlayRate(AnimationMontage).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetPlayRate(UnrealEngine.Framework.AnimationMontage) Method +Returns the current play rate of the animation montage +```csharp +public float GetPlayRate(UnrealEngine.Framework.AnimationMontage montage); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/AnimationInstance-GetPosition(AnimationMontage).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetPosition(UnrealEngine.Framework.AnimationMontage) Method +Returns the current position of the animation montage +```csharp +public float GetPosition(UnrealEngine.Framework.AnimationMontage montage); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/AnimationInstance-GetShort(string_short).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetShort(string, short) Method +Retrieves the value of the short property +```csharp +public bool GetShort(string name, ref short value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int16](https://docs.microsoft.com/en-us/dotnet/api/System.Int16 'System.Int16') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-GetString(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetString(string, string) Method +Retrieves the value of the string property +```csharp +public bool GetString(string name, ref string value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-GetText(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetText(string, string) Method +Retrieves the value of the text property +```csharp +public bool GetText(string name, ref string value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-GetUInt(string_uint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetUInt(string, uint) Method +Retrieves the value of the unsigned integer property +```csharp +public bool GetUInt(string name, ref uint value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-GetULong(string_ulong).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetULong(string, ulong) Method +Retrieves the value of the unsigned long property +```csharp +public bool GetULong(string name, ref ulong value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt64](https://docs.microsoft.com/en-us/dotnet/api/System.UInt64 'System.UInt64') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-GetUShort(string_ushort).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.GetUShort(string, ushort) Method +Retrieves the value of the unsigned short property +```csharp +public bool GetUShort(string name, ref ushort value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt16](https://docs.microsoft.com/en-us/dotnet/api/System.UInt16 'System.UInt16') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-Invoke(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.Invoke(string) Method +Invokes a command, function, or an event with optional arguments +```csharp +public bool Invoke(string command); +``` +#### Parameters + +`command` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AnimationInstance-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AnimationInstance-IsPlaying(AnimationMontage).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.IsPlaying(UnrealEngine.Framework.AnimationMontage) Method +Returns `true` if the animation montage is active and playing +```csharp +public bool IsPlaying(UnrealEngine.Framework.AnimationMontage montage); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AnimationInstance-JumpToSection(AnimationMontage_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.JumpToSection(UnrealEngine.Framework.AnimationMontage, string) Method +Makes the animation montage jump to a named section +```csharp +public void JumpToSection(UnrealEngine.Framework.AnimationMontage montage, string sectionName); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + + +`sectionName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/AnimationInstance-JumpToSectionsEnd(AnimationMontage_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.JumpToSectionsEnd(UnrealEngine.Framework.AnimationMontage, string) Method +Makes the animation montage jump to the end of a named section +```csharp +public void JumpToSectionsEnd(UnrealEngine.Framework.AnimationMontage montage, string sectionName); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + + +`sectionName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/AnimationInstance-PauseMontage(AnimationMontage).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.PauseMontage(UnrealEngine.Framework.AnimationMontage) Method +Pauses the animation montage +```csharp +public void PauseMontage(UnrealEngine.Framework.AnimationMontage montage); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + + +``` + +`API/AnimationInstance-PlayMontage(AnimationMontage_float_float_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.PlayMontage(UnrealEngine.Framework.AnimationMontage, float, float, bool) Method +Plays the animation montage +```csharp +public float PlayMontage(UnrealEngine.Framework.AnimationMontage montage, float playRate=1f, float timeToStartMontageAt=0f, bool stopAllMontages=true); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + + +`playRate` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`timeToStartMontageAt` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`stopAllMontages` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +The length of the animation montage in seconds, or 0.0f if failed to play + +``` + +`API/AnimationInstance-ResumeMontage(AnimationMontage).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.ResumeMontage(UnrealEngine.Framework.AnimationMontage) Method +Resumes the paused animation montage +```csharp +public void ResumeMontage(UnrealEngine.Framework.AnimationMontage montage); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + + +``` + +`API/AnimationInstance-SetBool(string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetBool(string, bool) Method +Sets the value of the bool property +```csharp +public bool SetBool(string name, bool value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AnimationInstance-SetByte(string_byte).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetByte(string, byte) Method +Sets the value of the byte property +```csharp +public bool SetByte(string name, byte value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-SetDouble(string_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetDouble(string, double) Method +Sets the value of the double property +```csharp +public bool SetDouble(string name, double value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-SetEnum-T-(string_T).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetEnum<T>(string, T) Method +Sets the value of the enum property +```csharp +public bool SetEnum(string name, T value) + where T : System.Enum; +``` +#### Type parameters + +`T` + +Constraints [System.Enum](https://docs.microsoft.com/en-us/dotnet/api/System.Enum 'System.Enum') + +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [T](#UnrealEngine-Framework-AnimationInstance-SetEnum-T-(string_T)-T 'UnrealEngine.Framework.AnimationInstance.SetEnum<T>(string, T).T') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-SetFloat(string_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetFloat(string, float) Method +Sets the value of the float property +```csharp +public bool SetFloat(string name, float value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-SetInt(string_int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetInt(string, int) Method +Sets the value of the integer property +```csharp +public bool SetInt(string name, int value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-SetLong(string_long).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetLong(string, long) Method +Sets the value of the long property +```csharp +public bool SetLong(string name, long value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int64](https://docs.microsoft.com/en-us/dotnet/api/System.Int64 'System.Int64') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-SetNextSection(AnimationMontage_string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetNextSection(UnrealEngine.Framework.AnimationMontage, string, string) Method +Sets the next section after [sectionToChange](#UnrealEngine-Framework-AnimationInstance-SetNextSection(UnrealEngine-Framework-AnimationMontage_string_string)-sectionToChange 'UnrealEngine.Framework.AnimationInstance.SetNextSection(UnrealEngine.Framework.AnimationMontage, string, string).sectionToChange') +```csharp +public void SetNextSection(UnrealEngine.Framework.AnimationMontage montage, string sectionToChange, string nextSection); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + + +`sectionToChange` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`nextSection` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/AnimationInstance-SetPlayRate(AnimationMontage_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetPlayRate(UnrealEngine.Framework.AnimationMontage, float) Method +Sets the current play rate of the animation montage +```csharp +public void SetPlayRate(UnrealEngine.Framework.AnimationMontage montage, float value); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/AnimationInstance-SetPosition(AnimationMontage_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetPosition(UnrealEngine.Framework.AnimationMontage, float) Method +Sets the current position of the animation montage +```csharp +public void SetPosition(UnrealEngine.Framework.AnimationMontage montage, float position); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + + +`position` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/AnimationInstance-SetShort(string_short).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetShort(string, short) Method +Sets the value of the short property +```csharp +public bool SetShort(string name, short value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Int16](https://docs.microsoft.com/en-us/dotnet/api/System.Int16 'System.Int16') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-SetString(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetString(string, string) Method +Sets the value of the string property +```csharp +public bool SetString(string name, string value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-SetText(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetText(string, string) Method +Sets the value of the text property +```csharp +public bool SetText(string name, string value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-SetUInt(string_uint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetUInt(string, uint) Method +Sets the value of the unsigned integer property +```csharp +public bool SetUInt(string name, uint value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-SetULong(string_ulong).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetULong(string, ulong) Method +Sets the value of the unsigned long property +```csharp +public bool SetULong(string name, ulong value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt64](https://docs.microsoft.com/en-us/dotnet/api/System.UInt64 'System.UInt64') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-SetUShort(string_ushort).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.SetUShort(string, ushort) Method +Sets the value of the unsigned short property +```csharp +public bool SetUShort(string name, ushort value); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.UInt16](https://docs.microsoft.com/en-us/dotnet/api/System.UInt16 'System.UInt16') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/AnimationInstance-StopMontage(AnimationMontage_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +## AnimationInstance.StopMontage(UnrealEngine.Framework.AnimationMontage, float) Method +Stops the animation montage, if [montage](#UnrealEngine-Framework-AnimationInstance-StopMontage(UnrealEngine-Framework-AnimationMontage_float)-montage 'UnrealEngine.Framework.AnimationInstance.StopMontage(UnrealEngine.Framework.AnimationMontage, float).montage') is `null` stops all active montages +```csharp +public void StopMontage(UnrealEngine.Framework.AnimationMontage montage, float blendOutTime); +``` +#### Parameters + +`montage` [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') + + +`blendOutTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/AnimationInstance.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AnimationInstance Class +An animation instance representation +```csharp +public class AnimationInstance : +System.IEquatable +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 AnimationInstance + +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [IsCreated](./AnimationInstance-IsCreated.md 'UnrealEngine.Framework.AnimationInstance.IsCreated') +### Methods +- [Equals(UnrealEngine.Framework.AnimationInstance)](./AnimationInstance-Equals(AnimationInstance).md 'UnrealEngine.Framework.AnimationInstance.Equals(UnrealEngine.Framework.AnimationInstance)') +- [GetBlendTime(UnrealEngine.Framework.AnimationMontage)](./AnimationInstance-GetBlendTime(AnimationMontage).md 'UnrealEngine.Framework.AnimationInstance.GetBlendTime(UnrealEngine.Framework.AnimationMontage)') +- [GetBool(string, bool)](./AnimationInstance-GetBool(string_bool).md 'UnrealEngine.Framework.AnimationInstance.GetBool(string, bool)') +- [GetByte(string, byte)](./AnimationInstance-GetByte(string_byte).md 'UnrealEngine.Framework.AnimationInstance.GetByte(string, byte)') +- [GetCurrentActiveMontage()](./AnimationInstance-GetCurrentActiveMontage().md 'UnrealEngine.Framework.AnimationInstance.GetCurrentActiveMontage()') +- [GetCurrentSection(UnrealEngine.Framework.AnimationMontage)](./AnimationInstance-GetCurrentSection(AnimationMontage).md 'UnrealEngine.Framework.AnimationInstance.GetCurrentSection(UnrealEngine.Framework.AnimationMontage)') +- [GetDouble(string, double)](./AnimationInstance-GetDouble(string_double).md 'UnrealEngine.Framework.AnimationInstance.GetDouble(string, double)') +- [GetEnum<T>(string, T)](./AnimationInstance-GetEnum-T-(string_T).md 'UnrealEngine.Framework.AnimationInstance.GetEnum<T>(string, T)') +- [GetFloat(string, float)](./AnimationInstance-GetFloat(string_float).md 'UnrealEngine.Framework.AnimationInstance.GetFloat(string, float)') +- [GetHashCode()](./AnimationInstance-GetHashCode().md 'UnrealEngine.Framework.AnimationInstance.GetHashCode()') +- [GetInt(string, int)](./AnimationInstance-GetInt(string_int).md 'UnrealEngine.Framework.AnimationInstance.GetInt(string, int)') +- [GetLong(string, long)](./AnimationInstance-GetLong(string_long).md 'UnrealEngine.Framework.AnimationInstance.GetLong(string, long)') +- [GetPlayRate(UnrealEngine.Framework.AnimationMontage)](./AnimationInstance-GetPlayRate(AnimationMontage).md 'UnrealEngine.Framework.AnimationInstance.GetPlayRate(UnrealEngine.Framework.AnimationMontage)') +- [GetPosition(UnrealEngine.Framework.AnimationMontage)](./AnimationInstance-GetPosition(AnimationMontage).md 'UnrealEngine.Framework.AnimationInstance.GetPosition(UnrealEngine.Framework.AnimationMontage)') +- [GetShort(string, short)](./AnimationInstance-GetShort(string_short).md 'UnrealEngine.Framework.AnimationInstance.GetShort(string, short)') +- [GetString(string, string)](./AnimationInstance-GetString(string_string).md 'UnrealEngine.Framework.AnimationInstance.GetString(string, string)') +- [GetText(string, string)](./AnimationInstance-GetText(string_string).md 'UnrealEngine.Framework.AnimationInstance.GetText(string, string)') +- [GetUInt(string, uint)](./AnimationInstance-GetUInt(string_uint).md 'UnrealEngine.Framework.AnimationInstance.GetUInt(string, uint)') +- [GetULong(string, ulong)](./AnimationInstance-GetULong(string_ulong).md 'UnrealEngine.Framework.AnimationInstance.GetULong(string, ulong)') +- [GetUShort(string, ushort)](./AnimationInstance-GetUShort(string_ushort).md 'UnrealEngine.Framework.AnimationInstance.GetUShort(string, ushort)') +- [Invoke(string)](./AnimationInstance-Invoke(string).md 'UnrealEngine.Framework.AnimationInstance.Invoke(string)') +- [IsPlaying(UnrealEngine.Framework.AnimationMontage)](./AnimationInstance-IsPlaying(AnimationMontage).md 'UnrealEngine.Framework.AnimationInstance.IsPlaying(UnrealEngine.Framework.AnimationMontage)') +- [JumpToSection(UnrealEngine.Framework.AnimationMontage, string)](./AnimationInstance-JumpToSection(AnimationMontage_string).md 'UnrealEngine.Framework.AnimationInstance.JumpToSection(UnrealEngine.Framework.AnimationMontage, string)') +- [JumpToSectionsEnd(UnrealEngine.Framework.AnimationMontage, string)](./AnimationInstance-JumpToSectionsEnd(AnimationMontage_string).md 'UnrealEngine.Framework.AnimationInstance.JumpToSectionsEnd(UnrealEngine.Framework.AnimationMontage, string)') +- [PauseMontage(UnrealEngine.Framework.AnimationMontage)](./AnimationInstance-PauseMontage(AnimationMontage).md 'UnrealEngine.Framework.AnimationInstance.PauseMontage(UnrealEngine.Framework.AnimationMontage)') +- [PlayMontage(UnrealEngine.Framework.AnimationMontage, float, float, bool)](./AnimationInstance-PlayMontage(AnimationMontage_float_float_bool).md 'UnrealEngine.Framework.AnimationInstance.PlayMontage(UnrealEngine.Framework.AnimationMontage, float, float, bool)') +- [ResumeMontage(UnrealEngine.Framework.AnimationMontage)](./AnimationInstance-ResumeMontage(AnimationMontage).md 'UnrealEngine.Framework.AnimationInstance.ResumeMontage(UnrealEngine.Framework.AnimationMontage)') +- [SetBool(string, bool)](./AnimationInstance-SetBool(string_bool).md 'UnrealEngine.Framework.AnimationInstance.SetBool(string, bool)') +- [SetByte(string, byte)](./AnimationInstance-SetByte(string_byte).md 'UnrealEngine.Framework.AnimationInstance.SetByte(string, byte)') +- [SetDouble(string, double)](./AnimationInstance-SetDouble(string_double).md 'UnrealEngine.Framework.AnimationInstance.SetDouble(string, double)') +- [SetEnum<T>(string, T)](./AnimationInstance-SetEnum-T-(string_T).md 'UnrealEngine.Framework.AnimationInstance.SetEnum<T>(string, T)') +- [SetFloat(string, float)](./AnimationInstance-SetFloat(string_float).md 'UnrealEngine.Framework.AnimationInstance.SetFloat(string, float)') +- [SetInt(string, int)](./AnimationInstance-SetInt(string_int).md 'UnrealEngine.Framework.AnimationInstance.SetInt(string, int)') +- [SetLong(string, long)](./AnimationInstance-SetLong(string_long).md 'UnrealEngine.Framework.AnimationInstance.SetLong(string, long)') +- [SetNextSection(UnrealEngine.Framework.AnimationMontage, string, string)](./AnimationInstance-SetNextSection(AnimationMontage_string_string).md 'UnrealEngine.Framework.AnimationInstance.SetNextSection(UnrealEngine.Framework.AnimationMontage, string, string)') +- [SetPlayRate(UnrealEngine.Framework.AnimationMontage, float)](./AnimationInstance-SetPlayRate(AnimationMontage_float).md 'UnrealEngine.Framework.AnimationInstance.SetPlayRate(UnrealEngine.Framework.AnimationMontage, float)') +- [SetPosition(UnrealEngine.Framework.AnimationMontage, float)](./AnimationInstance-SetPosition(AnimationMontage_float).md 'UnrealEngine.Framework.AnimationInstance.SetPosition(UnrealEngine.Framework.AnimationMontage, float)') +- [SetShort(string, short)](./AnimationInstance-SetShort(string_short).md 'UnrealEngine.Framework.AnimationInstance.SetShort(string, short)') +- [SetString(string, string)](./AnimationInstance-SetString(string_string).md 'UnrealEngine.Framework.AnimationInstance.SetString(string, string)') +- [SetText(string, string)](./AnimationInstance-SetText(string_string).md 'UnrealEngine.Framework.AnimationInstance.SetText(string, string)') +- [SetUInt(string, uint)](./AnimationInstance-SetUInt(string_uint).md 'UnrealEngine.Framework.AnimationInstance.SetUInt(string, uint)') +- [SetULong(string, ulong)](./AnimationInstance-SetULong(string_ulong).md 'UnrealEngine.Framework.AnimationInstance.SetULong(string, ulong)') +- [SetUShort(string, ushort)](./AnimationInstance-SetUShort(string_ushort).md 'UnrealEngine.Framework.AnimationInstance.SetUShort(string, ushort)') +- [StopMontage(UnrealEngine.Framework.AnimationMontage, float)](./AnimationInstance-StopMontage(AnimationMontage_float).md 'UnrealEngine.Framework.AnimationInstance.StopMontage(UnrealEngine.Framework.AnimationMontage, float)') + +``` + +`API/AnimationMode.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AnimationMode Enum +Defines the animation mode +```csharp +public enum AnimationMode +``` +### Fields + +`Blueprint` 0 + + + +`Asset` 1 + + + +`Custom` 2 + + + +``` + +`API/AnimationMontage-Load(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') +## AnimationMontage.Load(string) Method +Finds and loads an animation montage by name +```csharp +public static UnrealEngine.Framework.AnimationMontage Load(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') +An animation montage or `null` on failure + +``` + +`API/AnimationMontage.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AnimationMontage Class +A single animation asset that can combine and selectively play animations +```csharp +public class AnimationMontage : UnrealEngine.Framework.AnimationCompositeBase +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [AnimationAsset](./AnimationAsset.md 'UnrealEngine.Framework.AnimationAsset') 🡒 [AnimationSequenceBase](./AnimationSequenceBase.md 'UnrealEngine.Framework.AnimationSequenceBase') 🡒 [AnimationCompositeBase](./AnimationCompositeBase.md 'UnrealEngine.Framework.AnimationCompositeBase') 🡒 AnimationMontage +### Methods +- [Load(string)](./AnimationMontage-Load(string).md 'UnrealEngine.Framework.AnimationMontage.Load(string)') + +``` + +`API/AnimationSequence-Load(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AnimationSequence](./AnimationSequence.md 'UnrealEngine.Framework.AnimationSequence') +## AnimationSequence.Load(string) Method +Finds and loads an animation sequence by name +```csharp +public static UnrealEngine.Framework.AnimationSequence Load(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[AnimationSequence](./AnimationSequence.md 'UnrealEngine.Framework.AnimationSequence') +An animation sequence or `null` on failure + +``` + +`API/AnimationSequence.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AnimationSequence Class +A single animation asset that can be played +```csharp +public class AnimationSequence : UnrealEngine.Framework.AnimationSequenceBase +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [AnimationAsset](./AnimationAsset.md 'UnrealEngine.Framework.AnimationAsset') 🡒 [AnimationSequenceBase](./AnimationSequenceBase.md 'UnrealEngine.Framework.AnimationSequenceBase') 🡒 AnimationSequence +### Methods +- [Load(string)](./AnimationSequence-Load(string).md 'UnrealEngine.Framework.AnimationSequence.Load(string)') + +``` + +`API/AnimationSequenceBase.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AnimationSequenceBase Class +The base class of animation sequences +```csharp +public abstract class AnimationSequenceBase : UnrealEngine.Framework.AnimationAsset +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [AnimationAsset](./AnimationAsset.md 'UnrealEngine.Framework.AnimationAsset') 🡒 AnimationSequenceBase + +Derived +↳ [AnimationCompositeBase](./AnimationCompositeBase.md 'UnrealEngine.Framework.AnimationCompositeBase') +↳ [AnimationSequence](./AnimationSequence.md 'UnrealEngine.Framework.AnimationSequence') + +``` + +`API/Application-DefaultLanguage.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Application](./Application.md 'UnrealEngine.Framework.Application') +## Application.DefaultLanguage Property +Returns the default language used by current platform +```csharp +public static string DefaultLanguage { get; } +``` +#### Property Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Application-IsCanEverRender.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Application](./Application.md 'UnrealEngine.Framework.Application') +## Application.IsCanEverRender Property +Returns `true` if the application can render anything +```csharp +public static bool IsCanEverRender { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Application-IsPackagedForDistribution.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Application](./Application.md 'UnrealEngine.Framework.Application') +## Application.IsPackagedForDistribution Property +Returns `true` if current build is meant for release to retail +```csharp +public static bool IsPackagedForDistribution { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Application-IsPackagedForShipping.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Application](./Application.md 'UnrealEngine.Framework.Application') +## Application.IsPackagedForShipping Property +Returns `true` if current build is packaged for shipping +```csharp +public static bool IsPackagedForShipping { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Application-ProjectDirectory.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Application](./Application.md 'UnrealEngine.Framework.Application') +## Application.ProjectDirectory Property +Returns the project directory +```csharp +public static string ProjectDirectory { get; } +``` +#### Property Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Application-ProjectName.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Application](./Application.md 'UnrealEngine.Framework.Application') +## Application.ProjectName Property +Gets or sets the name of the current project +```csharp +public static string ProjectName { get; set; } +``` +#### Property Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Application-RequestExit(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Application](./Application.md 'UnrealEngine.Framework.Application') +## Application.RequestExit(bool) Method +Requests application exit +```csharp +public static void RequestExit(bool force=false); +``` +#### Parameters + +`force` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/Application-VolumeMultiplier.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Application](./Application.md 'UnrealEngine.Framework.Application') +## Application.VolumeMultiplier Property +Gets or sets the current volume multiplier +```csharp +public static float VolumeMultiplier { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Application.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Application Class +Provides information about the application +```csharp +public static class Application +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Application +### Properties +- [DefaultLanguage](./Application-DefaultLanguage.md 'UnrealEngine.Framework.Application.DefaultLanguage') +- [IsCanEverRender](./Application-IsCanEverRender.md 'UnrealEngine.Framework.Application.IsCanEverRender') +- [IsPackagedForDistribution](./Application-IsPackagedForDistribution.md 'UnrealEngine.Framework.Application.IsPackagedForDistribution') +- [IsPackagedForShipping](./Application-IsPackagedForShipping.md 'UnrealEngine.Framework.Application.IsPackagedForShipping') +- [ProjectDirectory](./Application-ProjectDirectory.md 'UnrealEngine.Framework.Application.ProjectDirectory') +- [ProjectName](./Application-ProjectName.md 'UnrealEngine.Framework.Application.ProjectName') +- [VolumeMultiplier](./Application-VolumeMultiplier.md 'UnrealEngine.Framework.Application.VolumeMultiplier') +### Methods +- [RequestExit(bool)](./Application-RequestExit(bool).md 'UnrealEngine.Framework.Application.RequestExit(bool)') + +``` + +`API/Assert-IsFalse(bool_string_int_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Assert](./Assert.md 'UnrealEngine.Framework.Assert') +## Assert.IsFalse(bool, string, int, string) Method +Logs an assertion if condition is `true`, and prints it on the screen +```csharp +public static void IsFalse(bool condition, string message=null, int callerLineNumber=0, string callerFilePath=null); +``` +#### Parameters + +`condition` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`message` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`callerLineNumber` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`callerFilePath` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/Assert-IsNotNull-T-(T_string_int_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Assert](./Assert.md 'UnrealEngine.Framework.Assert') +## Assert.IsNotNull<T>(T, string, int, string) Method +Logs an assertion if value is `null`, and prints it on the screen +```csharp +public static void IsNotNull(T value, string message=null, int callerLineNumber=0, string callerFilePath=null) + where T : class; +``` +#### Type parameters + +`T` + +#### Parameters + +`value` [T](#UnrealEngine-Framework-Assert-IsNotNull-T-(T_string_int_string)-T 'UnrealEngine.Framework.Assert.IsNotNull<T>(T, string, int, string).T') + + +`message` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`callerLineNumber` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`callerFilePath` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/Assert-IsNull-T-(T_string_int_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Assert](./Assert.md 'UnrealEngine.Framework.Assert') +## Assert.IsNull<T>(T, string, int, string) Method +Logs an assertion if value is not `null`, and prints it on the screen +```csharp +public static void IsNull(T value, string message=null, int callerLineNumber=0, string callerFilePath=null) + where T : class; +``` +#### Type parameters + +`T` + +#### Parameters + +`value` [T](#UnrealEngine-Framework-Assert-IsNull-T-(T_string_int_string)-T 'UnrealEngine.Framework.Assert.IsNull<T>(T, string, int, string).T') + + +`message` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`callerLineNumber` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`callerFilePath` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/Assert-IsTrue(bool_string_int_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Assert](./Assert.md 'UnrealEngine.Framework.Assert') +## Assert.IsTrue(bool, string, int, string) Method +Logs an assertion if condition is `false`, and prints it on the screen +```csharp +public static void IsTrue(bool condition, string message=null, int callerLineNumber=0, string callerFilePath=null); +``` +#### Parameters + +`condition` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`message` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`callerLineNumber` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`callerFilePath` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/Assert.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Assert Class +Functionality to detect and diagnose unexpected or invalid runtime conditions during development, emitted if the assembly is built with the Debug configuration or if `ASSERTIONS` symbol is defined, signals a breakpoint to an attached debugger +```csharp +public static class Assert +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Assert +### Methods +- [IsFalse(bool, string, int, string)](./Assert-IsFalse(bool_string_int_string).md 'UnrealEngine.Framework.Assert.IsFalse(bool, string, int, string)') +- [IsNotNull<T>(T, string, int, string)](./Assert-IsNotNull-T-(T_string_int_string).md 'UnrealEngine.Framework.Assert.IsNotNull<T>(T, string, int, string)') +- [IsNull<T>(T, string, int, string)](./Assert-IsNull-T-(T_string_int_string).md 'UnrealEngine.Framework.Assert.IsNull<T>(T, string, int, string)') +- [IsTrue(bool, string, int, string)](./Assert-IsTrue(bool_string_int_string).md 'UnrealEngine.Framework.Assert.IsTrue(bool, string, int, string)') + +``` + +`API/Asset-Equals(Asset).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Asset](./Asset.md 'UnrealEngine.Framework.Asset') +## Asset.Equals(UnrealEngine.Framework.Asset) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.Asset other); +``` +#### Parameters + +`other` [Asset](./Asset.md 'UnrealEngine.Framework.Asset') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Asset-Equals(object).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Asset](./Asset.md 'UnrealEngine.Framework.Asset') +## Asset.Equals(object) Method +Indicates equality of objects +```csharp +public override bool Equals(object value); +``` +#### Parameters + +`value` [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Asset-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Asset](./Asset.md 'UnrealEngine.Framework.Asset') +## Asset.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/Asset-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Asset](./Asset.md 'UnrealEngine.Framework.Asset') +## Asset.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Asset-Name.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Asset](./Asset.md 'UnrealEngine.Framework.Asset') +## Asset.Name Property +Returns the name of the asset +```csharp +public string Name { get; } +``` +#### Property Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Asset-Path.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Asset](./Asset.md 'UnrealEngine.Framework.Asset') +## Asset.Path Property +Returns the path to the asset +```csharp +public string Path { get; } +``` +#### Property Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Asset-op_Equality(Asset_Asset).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Asset](./Asset.md 'UnrealEngine.Framework.Asset') +## Asset.operator ==(UnrealEngine.Framework.Asset, UnrealEngine.Framework.Asset) Operator +Tests for equality between two objects +```csharp +public static bool operator ==(UnrealEngine.Framework.Asset left, UnrealEngine.Framework.Asset right); +``` +#### Parameters + +`left` [Asset](./Asset.md 'UnrealEngine.Framework.Asset') + + +`right` [Asset](./Asset.md 'UnrealEngine.Framework.Asset') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Asset-op_Inequality(Asset_Asset).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Asset](./Asset.md 'UnrealEngine.Framework.Asset') +## Asset.operator !=(UnrealEngine.Framework.Asset, UnrealEngine.Framework.Asset) Operator +Tests for inequality between two objects +```csharp +public static bool operator !=(UnrealEngine.Framework.Asset left, UnrealEngine.Framework.Asset right); +``` +#### Parameters + +`left` [Asset](./Asset.md 'UnrealEngine.Framework.Asset') + + +`right` [Asset](./Asset.md 'UnrealEngine.Framework.Asset') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Asset.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Asset Struct +A representation of the asset +```csharp +public struct Asset : +System.IEquatable +``` +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[Asset](./Asset.md 'UnrealEngine.Framework.Asset')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [IsCreated](./Asset-IsCreated.md 'UnrealEngine.Framework.Asset.IsCreated') +- [Name](./Asset-Name.md 'UnrealEngine.Framework.Asset.Name') +- [Path](./Asset-Path.md 'UnrealEngine.Framework.Asset.Path') +### Methods +- [Equals(object)](./Asset-Equals(object).md 'UnrealEngine.Framework.Asset.Equals(object)') +- [Equals(UnrealEngine.Framework.Asset)](./Asset-Equals(Asset).md 'UnrealEngine.Framework.Asset.Equals(UnrealEngine.Framework.Asset)') +- [GetHashCode()](./Asset-GetHashCode().md 'UnrealEngine.Framework.Asset.GetHashCode()') +### Operators +- [operator ==(UnrealEngine.Framework.Asset, UnrealEngine.Framework.Asset)](./Asset-op_Equality(Asset_Asset).md 'UnrealEngine.Framework.Asset.op_Equality(UnrealEngine.Framework.Asset, UnrealEngine.Framework.Asset)') +- [operator !=(UnrealEngine.Framework.Asset, UnrealEngine.Framework.Asset)](./Asset-op_Inequality(Asset_Asset).md 'UnrealEngine.Framework.Asset.op_Inequality(UnrealEngine.Framework.Asset, UnrealEngine.Framework.Asset)') + +``` + +`API/AssetRegistry-AssetRegistry().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AssetRegistry](./AssetRegistry.md 'UnrealEngine.Framework.AssetRegistry') +## AssetRegistry() Constructor +Initializes a new instance of the asset registry +```csharp +public AssetRegistry(); +``` + +``` + +`API/AssetRegistry-Equals(AssetRegistry).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AssetRegistry](./AssetRegistry.md 'UnrealEngine.Framework.AssetRegistry') +## AssetRegistry.Equals(UnrealEngine.Framework.AssetRegistry) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.AssetRegistry other); +``` +#### Parameters + +`other` [AssetRegistry](./AssetRegistry.md 'UnrealEngine.Framework.AssetRegistry') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AssetRegistry-ForEachAsset(Action-Asset-_string_bool_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AssetRegistry](./AssetRegistry.md 'UnrealEngine.Framework.AssetRegistry') +## AssetRegistry.ForEachAsset(System.Action<UnrealEngine.Framework.Asset>, string, bool, bool) Method +Performs the specified action on each asset if any +```csharp +public void ForEachAsset(System.Action action, string path, bool recursive=false, bool includeOnlyOnDiskAssets=false); +``` +#### Parameters + +`action` [System.Action<](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1')[Asset](./Asset.md 'UnrealEngine.Framework.Asset')[>](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1') + + +`path` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`recursive` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`includeOnlyOnDiskAssets` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/AssetRegistry-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AssetRegistry](./AssetRegistry.md 'UnrealEngine.Framework.AssetRegistry') +## AssetRegistry.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/AssetRegistry-HasAssets(string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AssetRegistry](./AssetRegistry.md 'UnrealEngine.Framework.AssetRegistry') +## AssetRegistry.HasAssets(string, bool) Method +Checks whether the given path contain assets, optionally testing sub-paths +```csharp +public bool HasAssets(string path, bool recursive=false); +``` +#### Parameters + +`path` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`recursive` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AssetRegistry-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AssetRegistry](./AssetRegistry.md 'UnrealEngine.Framework.AssetRegistry') +## AssetRegistry.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AssetRegistry.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AssetRegistry Class +An asset registry +```csharp +public class AssetRegistry : +System.IEquatable +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 AssetRegistry + +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[AssetRegistry](./AssetRegistry.md 'UnrealEngine.Framework.AssetRegistry')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Constructors +- [AssetRegistry()](./AssetRegistry-AssetRegistry().md 'UnrealEngine.Framework.AssetRegistry.AssetRegistry()') +### Properties +- [IsCreated](./AssetRegistry-IsCreated.md 'UnrealEngine.Framework.AssetRegistry.IsCreated') +### Methods +- [Equals(UnrealEngine.Framework.AssetRegistry)](./AssetRegistry-Equals(AssetRegistry).md 'UnrealEngine.Framework.AssetRegistry.Equals(UnrealEngine.Framework.AssetRegistry)') +- [ForEachAsset(System.Action<UnrealEngine.Framework.Asset>, string, bool, bool)](./AssetRegistry-ForEachAsset(Action-Asset-_string_bool_bool).md 'UnrealEngine.Framework.AssetRegistry.ForEachAsset(System.Action<UnrealEngine.Framework.Asset>, string, bool, bool)') +- [GetHashCode()](./AssetRegistry-GetHashCode().md 'UnrealEngine.Framework.AssetRegistry.GetHashCode()') +- [HasAssets(string, bool)](./AssetRegistry-HasAssets(string_bool).md 'UnrealEngine.Framework.AssetRegistry.HasAssets(string, bool)') + +``` + +`API/AttachmentTransformRule.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AttachmentTransformRule Enum +Defines rules for attaching components +```csharp +public enum AttachmentTransformRule +``` +### Fields + +`KeepRelativeTransform` 0 +Keeps current relative transform as the relative transform to the new parent + + +`KeepWorldTransform` 1 +Automatically calculates the relative transform such that the attached component maintains the same world transform + + +`SnapToTargetIncludingScale` 2 +Snaps location and rotation to the attach point, calculates the relative scale so that the final world scale of the component remains the same + + +`SnapToTargetNotIncludingScale` 3 +Snaps the entire transform to target, including scale + + +``` + +`API/AudioComponent-AudioComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AudioComponent](./AudioComponent.md 'UnrealEngine.Framework.AudioComponent') +## AudioComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public AudioComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/AudioComponent-FadeIn(float_float_float_AudioFadeCurve).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AudioComponent](./AudioComponent.md 'UnrealEngine.Framework.AudioComponent') +## AudioComponent.FadeIn(float, float, float, UnrealEngine.Framework.AudioFadeCurve) Method +Smoothly starts the audio, can be used instead of [Play()](./AudioComponent-Play().md 'UnrealEngine.Framework.AudioComponent.Play()') +```csharp +public void FadeIn(float duration, float volumeLevel=1f, float startTime=0f, UnrealEngine.Framework.AudioFadeCurve fadeCurve=UnrealEngine.Framework.AudioFadeCurve.Linear); +``` +#### Parameters + +`duration` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Duration to reach [volumeLevel](#UnrealEngine-Framework-AudioComponent-FadeIn(float_float_float_UnrealEngine-Framework-AudioFadeCurve)-volumeLevel 'UnrealEngine.Framework.AudioComponent.FadeIn(float, float, float, UnrealEngine.Framework.AudioFadeCurve).volumeLevel') + + +`volumeLevel` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +The percentage of calculated volume to fade to + + +`startTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Fading start time + + +`fadeCurve` [AudioFadeCurve](./AudioFadeCurve.md 'UnrealEngine.Framework.AudioFadeCurve') +Curve to adjust audio volume + + +``` + +`API/AudioComponent-FadeOut(float_float_AudioFadeCurve).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AudioComponent](./AudioComponent.md 'UnrealEngine.Framework.AudioComponent') +## AudioComponent.FadeOut(float, float, UnrealEngine.Framework.AudioFadeCurve) Method +Smoothly stops the audio, can be used instead of [Stop()](./AudioComponent-Stop().md 'UnrealEngine.Framework.AudioComponent.Stop()') +```csharp +public void FadeOut(float duration, float volumeLevel=0f, UnrealEngine.Framework.AudioFadeCurve fadeCurve=UnrealEngine.Framework.AudioFadeCurve.Linear); +``` +#### Parameters + +`duration` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Duration to reach [volumeLevel](#UnrealEngine-Framework-AudioComponent-FadeOut(float_float_UnrealEngine-Framework-AudioFadeCurve)-volumeLevel 'UnrealEngine.Framework.AudioComponent.FadeOut(float, float, UnrealEngine.Framework.AudioFadeCurve).volumeLevel') + + +`volumeLevel` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +he percentage of calculated volume to fade to + + +`fadeCurve` [AudioFadeCurve](./AudioFadeCurve.md 'UnrealEngine.Framework.AudioFadeCurve') +Curve to adjust audio volume + + +``` + +`API/AudioComponent-IsPlaying.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AudioComponent](./AudioComponent.md 'UnrealEngine.Framework.AudioComponent') +## AudioComponent.IsPlaying Property +Returns `true` if the sound playing any audio +```csharp +public bool IsPlaying { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AudioComponent-Paused.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AudioComponent](./AudioComponent.md 'UnrealEngine.Framework.AudioComponent') +## AudioComponent.Paused Property +Gets or sets whether the audio is paused +```csharp +public bool Paused { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/AudioComponent-Play().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AudioComponent](./AudioComponent.md 'UnrealEngine.Framework.AudioComponent') +## AudioComponent.Play() Method +Plays the audio +```csharp +public void Play(); +``` + +``` + +`API/AudioComponent-SetSound(SoundBase).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AudioComponent](./AudioComponent.md 'UnrealEngine.Framework.AudioComponent') +## AudioComponent.SetSound(UnrealEngine.Framework.SoundBase) Method +Sets the sound object +```csharp +public void SetSound(UnrealEngine.Framework.SoundBase sound); +``` +#### Parameters + +`sound` [SoundBase](./SoundBase.md 'UnrealEngine.Framework.SoundBase') + + +``` + +`API/AudioComponent-Stop().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[AudioComponent](./AudioComponent.md 'UnrealEngine.Framework.AudioComponent') +## AudioComponent.Stop() Method +Stops the audio +```csharp +public void Stop(); +``` + +``` + +`API/AudioComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AudioComponent Class +A component that is used to play a sound +```csharp +public class AudioComponent : UnrealEngine.Framework.SceneComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 AudioComponent +### Constructors +- [AudioComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./AudioComponent-AudioComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.AudioComponent.AudioComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Properties +- [IsPlaying](./AudioComponent-IsPlaying.md 'UnrealEngine.Framework.AudioComponent.IsPlaying') +- [Paused](./AudioComponent-Paused.md 'UnrealEngine.Framework.AudioComponent.Paused') +### Methods +- [FadeIn(float, float, float, UnrealEngine.Framework.AudioFadeCurve)](./AudioComponent-FadeIn(float_float_float_AudioFadeCurve).md 'UnrealEngine.Framework.AudioComponent.FadeIn(float, float, float, UnrealEngine.Framework.AudioFadeCurve)') +- [FadeOut(float, float, UnrealEngine.Framework.AudioFadeCurve)](./AudioComponent-FadeOut(float_float_AudioFadeCurve).md 'UnrealEngine.Framework.AudioComponent.FadeOut(float, float, UnrealEngine.Framework.AudioFadeCurve)') +- [Play()](./AudioComponent-Play().md 'UnrealEngine.Framework.AudioComponent.Play()') +- [SetSound(UnrealEngine.Framework.SoundBase)](./AudioComponent-SetSound(SoundBase).md 'UnrealEngine.Framework.AudioComponent.SetSound(UnrealEngine.Framework.SoundBase)') +- [Stop()](./AudioComponent-Stop().md 'UnrealEngine.Framework.AudioComponent.Stop()') + +``` + +`API/AudioFadeCurve.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AudioFadeCurve Enum +Defines the type of fade to adjust audio volume +```csharp +public enum AudioFadeCurve : System.Byte +``` +### Fields + +`Linear` 0 + + + +`Logarithmic` 1 + + + +`SCurve` 2 + + + +`Sin` 3 + + + +``` + +`API/AutoPossessAI.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AutoPossessAI Enum +Defines the possession type for AI pawn that will be automatically possed by an AI controller +```csharp +public enum AutoPossessAI : System.Byte +``` +### Fields + +`Disabled` 0 +Disabled and not possesses AI + + +`PlacedInWorld` 1 +Only possess by an AI controller if a pawn is placed in the world + + +`Spawned` 2 +Only possess by an AI controller if a pawn is spawned after the world has loaded + + +`PlacedInWorldOrSpawned` 3 +Pawn is automatically possessed by an AI controller whenever it's created + + +``` + +`API/AutoReceiveInput.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## AutoReceiveInput Enum +Defines the player index that will be used to pass input +```csharp +public enum AutoReceiveInput +``` +### Fields + +`Disabled` 0 + + + +`Player0` 1 + + + +`Player1` 2 + + + +`Player2` 3 + + + +`Player3` 4 + + + +`Player4` 5 + + + +`Player5` 6 + + + +`Player6` 7 + + + +`Player7` 8 + + + +``` + +`API/BlendType.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## BlendType Enum +Defines how to blend when changing view targets +```csharp +public enum BlendType +``` +### Fields + +`Linear` 0 +A simple linear interpolation + + +`Cubic` 1 +A slight ease in and ease out, but amount of ease cannot be tweaked + + +`EaseIn` 2 +Immediately accelerates, but smoothly decelerates into the target, ease amount can be controlled + + +`EaseOut` 3 +Smoothly accelerates, but does not decelerate into the target, ease amount can be controlled + + +`EaseInOut` 4 +Smoothly accelerates and decelerates, ease amount can be controlled + + +`PreBlended` 5 +The game's camera system has already performed the blending, the engine shouldn't blend at all + + +``` + +`API/Blueprint-Equals(Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +## Blueprint.Equals(UnrealEngine.Framework.Blueprint) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.Blueprint other); +``` +#### Parameters + +`other` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Blueprint-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +## Blueprint.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/Blueprint-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +## Blueprint.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Blueprint-Load(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +## Blueprint.Load(string) Method +Finds and loads a blueprint by name +```csharp +public static UnrealEngine.Framework.Blueprint Load(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +A blueprint or `null` on failure + +``` + +`API/Blueprint.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Blueprint Class +An asset that provides an intuitive node-based interface +```csharp +public class Blueprint : +System.IEquatable +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Blueprint + +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [IsCreated](./Blueprint-IsCreated.md 'UnrealEngine.Framework.Blueprint.IsCreated') +### Methods +- [Equals(UnrealEngine.Framework.Blueprint)](./Blueprint-Equals(Blueprint).md 'UnrealEngine.Framework.Blueprint.Equals(UnrealEngine.Framework.Blueprint)') +- [GetHashCode()](./Blueprint-GetHashCode().md 'UnrealEngine.Framework.Blueprint.GetHashCode()') +- [Load(string)](./Blueprint-Load(string).md 'UnrealEngine.Framework.Blueprint.Load(string)') + +``` + +`API/Bounds-BoxExtent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') +## Bounds.BoxExtent Property +Returns the extent of the bounding box +```csharp +public System.Numerics.Vector3 BoxExtent { get; } +``` +#### Property Value +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Bounds-Equals(Bounds).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') +## Bounds.Equals(UnrealEngine.Framework.Bounds) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.Bounds other); +``` +#### Parameters + +`other` [Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Bounds-Equals(object).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') +## Bounds.Equals(object) Method +Indicates equality of objects +```csharp +public override bool Equals(object value); +``` +#### Parameters + +`value` [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Bounds-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') +## Bounds.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/Bounds-Origin.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') +## Bounds.Origin Property +Returns the origin of the bounding box and sphere +```csharp +public System.Numerics.Vector3 Origin { get; } +``` +#### Property Value +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Bounds-SphereRadius.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') +## Bounds.SphereRadius Property +Returns the radius of the bounding sphere +```csharp +public float SphereRadius { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Bounds-op_Equality(Bounds_Bounds).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') +## Bounds.operator ==(UnrealEngine.Framework.Bounds, UnrealEngine.Framework.Bounds) Operator +Tests for equality between two objects +```csharp +public static bool operator ==(UnrealEngine.Framework.Bounds left, UnrealEngine.Framework.Bounds right); +``` +#### Parameters + +`left` [Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') + + +`right` [Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Bounds-op_Inequality(Bounds_Bounds).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') +## Bounds.operator !=(UnrealEngine.Framework.Bounds, UnrealEngine.Framework.Bounds) Operator +Tests for inequality between two objects +```csharp +public static bool operator !=(UnrealEngine.Framework.Bounds left, UnrealEngine.Framework.Bounds right); +``` +#### Parameters + +`left` [Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') + + +`right` [Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Bounds.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Bounds Struct +A combined axis-aligned bounding box and bounding sphere with the same origin +```csharp +public struct Bounds : +System.IEquatable +``` +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [BoxExtent](./Bounds-BoxExtent.md 'UnrealEngine.Framework.Bounds.BoxExtent') +- [Origin](./Bounds-Origin.md 'UnrealEngine.Framework.Bounds.Origin') +- [SphereRadius](./Bounds-SphereRadius.md 'UnrealEngine.Framework.Bounds.SphereRadius') +### Methods +- [Equals(object)](./Bounds-Equals(object).md 'UnrealEngine.Framework.Bounds.Equals(object)') +- [Equals(UnrealEngine.Framework.Bounds)](./Bounds-Equals(Bounds).md 'UnrealEngine.Framework.Bounds.Equals(UnrealEngine.Framework.Bounds)') +- [GetHashCode()](./Bounds-GetHashCode().md 'UnrealEngine.Framework.Bounds.GetHashCode()') +### Operators +- [operator ==(UnrealEngine.Framework.Bounds, UnrealEngine.Framework.Bounds)](./Bounds-op_Equality(Bounds_Bounds).md 'UnrealEngine.Framework.Bounds.op_Equality(UnrealEngine.Framework.Bounds, UnrealEngine.Framework.Bounds)') +- [operator !=(UnrealEngine.Framework.Bounds, UnrealEngine.Framework.Bounds)](./Bounds-op_Inequality(Bounds_Bounds).md 'UnrealEngine.Framework.Bounds.op_Inequality(UnrealEngine.Framework.Bounds, UnrealEngine.Framework.Bounds)') + +``` + +`API/BoxComponent-BoxComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[BoxComponent](./BoxComponent.md 'UnrealEngine.Framework.BoxComponent') +## BoxComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public BoxComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/BoxComponent-GetScaledBoxExtent().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[BoxComponent](./BoxComponent.md 'UnrealEngine.Framework.BoxComponent') +## BoxComponent.GetScaledBoxExtent() Method +Returns the box extents scaled by the component scale +```csharp +public System.Numerics.Vector3 GetScaledBoxExtent(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/BoxComponent-GetScaledBoxExtent(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[BoxComponent](./BoxComponent.md 'UnrealEngine.Framework.BoxComponent') +## BoxComponent.GetScaledBoxExtent(System.Numerics.Vector3) Method +Retrieves the box extents scaled by the component scale +```csharp +public void GetScaledBoxExtent(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/BoxComponent-GetUnscaledBoxExtent().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[BoxComponent](./BoxComponent.md 'UnrealEngine.Framework.BoxComponent') +## BoxComponent.GetUnscaledBoxExtent() Method +Returns the box extent ignoring the component scale +```csharp +public System.Numerics.Vector3 GetUnscaledBoxExtent(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/BoxComponent-GetUnscaledBoxExtent(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[BoxComponent](./BoxComponent.md 'UnrealEngine.Framework.BoxComponent') +## BoxComponent.GetUnscaledBoxExtent(System.Numerics.Vector3) Method +Retrieves the box extent ignoring the component scale +```csharp +public void GetUnscaledBoxExtent(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/BoxComponent-InitBoxExtent(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[BoxComponent](./BoxComponent.md 'UnrealEngine.Framework.BoxComponent') +## BoxComponent.InitBoxExtent(System.Numerics.Vector3) Method +Sets the box extent size without triggering a render or physics update +```csharp +public void InitBoxExtent(in System.Numerics.Vector3 extent); +``` +#### Parameters + +`extent` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/BoxComponent-SetBoxExtent(Vector3_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[BoxComponent](./BoxComponent.md 'UnrealEngine.Framework.BoxComponent') +## BoxComponent.SetBoxExtent(System.Numerics.Vector3, bool) Method +Sets the box extent size, unscaled before the component scale is applied +```csharp +public void SetBoxExtent(in System.Numerics.Vector3 extent, bool updateOverlaps=true); +``` +#### Parameters + +`extent` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`updateOverlaps` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/BoxComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## BoxComponent Class +A box generally used for simple collision +```csharp +public class BoxComponent : UnrealEngine.Framework.ShapeComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') 🡒 [ShapeComponent](./ShapeComponent.md 'UnrealEngine.Framework.ShapeComponent') 🡒 BoxComponent +### Constructors +- [BoxComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./BoxComponent-BoxComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.BoxComponent.BoxComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Methods +- [GetScaledBoxExtent()](./BoxComponent-GetScaledBoxExtent().md 'UnrealEngine.Framework.BoxComponent.GetScaledBoxExtent()') +- [GetScaledBoxExtent(System.Numerics.Vector3)](./BoxComponent-GetScaledBoxExtent(Vector3).md 'UnrealEngine.Framework.BoxComponent.GetScaledBoxExtent(System.Numerics.Vector3)') +- [GetUnscaledBoxExtent()](./BoxComponent-GetUnscaledBoxExtent().md 'UnrealEngine.Framework.BoxComponent.GetUnscaledBoxExtent()') +- [GetUnscaledBoxExtent(System.Numerics.Vector3)](./BoxComponent-GetUnscaledBoxExtent(Vector3).md 'UnrealEngine.Framework.BoxComponent.GetUnscaledBoxExtent(System.Numerics.Vector3)') +- [InitBoxExtent(System.Numerics.Vector3)](./BoxComponent-InitBoxExtent(Vector3).md 'UnrealEngine.Framework.BoxComponent.InitBoxExtent(System.Numerics.Vector3)') +- [SetBoxExtent(System.Numerics.Vector3, bool)](./BoxComponent-SetBoxExtent(Vector3_bool).md 'UnrealEngine.Framework.BoxComponent.SetBoxExtent(System.Numerics.Vector3, bool)') + +``` + +`API/Brush-Brush(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Brush](./Brush.md 'UnrealEngine.Framework.Brush') +## Brush(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public Brush(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/Brush.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Brush Class +The base class of brushes for level construction +```csharp +public class Brush : UnrealEngine.Framework.Actor +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 Brush + +Derived +↳ [Volume](./Volume.md 'UnrealEngine.Framework.Volume') +### Constructors +- [Brush(string, UnrealEngine.Framework.Blueprint)](./Brush-Brush(string_Blueprint).md 'UnrealEngine.Framework.Brush.Brush(string, UnrealEngine.Framework.Blueprint)') + +``` + +`API/Camera-Camera(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Camera](./Camera.md 'UnrealEngine.Framework.Camera') +## Camera(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public Camera(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/Camera.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Camera Class +A camera viewpoint that can be placed in a level +```csharp +public class Camera : UnrealEngine.Framework.Actor +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 Camera +### Constructors +- [Camera(string, UnrealEngine.Framework.Blueprint)](./Camera-Camera(string_Blueprint).md 'UnrealEngine.Framework.Camera.Camera(string, UnrealEngine.Framework.Blueprint)') + +``` + +`API/CameraComponent-AspectRatio.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CameraComponent](./CameraComponent.md 'UnrealEngine.Framework.CameraComponent') +## CameraComponent.AspectRatio Property +Gets or sets the aspect ratio if [ConstrainAspectRatio](./CameraComponent-ConstrainAspectRatio.md 'UnrealEngine.Framework.CameraComponent.ConstrainAspectRatio') set to `true` +```csharp +public float AspectRatio { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/CameraComponent-CameraComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CameraComponent](./CameraComponent.md 'UnrealEngine.Framework.CameraComponent') +## CameraComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public CameraComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/CameraComponent-ConstrainAspectRatio.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CameraComponent](./CameraComponent.md 'UnrealEngine.Framework.CameraComponent') +## CameraComponent.ConstrainAspectRatio Property +Gets or sets whether black bars will be added if the destination view has a different aspect ratio than the camera requested +```csharp +public bool ConstrainAspectRatio { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/CameraComponent-FieldOfView.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CameraComponent](./CameraComponent.md 'UnrealEngine.Framework.CameraComponent') +## CameraComponent.FieldOfView Property +Gets or sets the horizontal field of view (in degrees) in perspective mode (ignored in orthographic mode) +```csharp +public float FieldOfView { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/CameraComponent-LockToHeadMountedDisplay.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CameraComponent](./CameraComponent.md 'UnrealEngine.Framework.CameraComponent') +## CameraComponent.LockToHeadMountedDisplay Property +Gets or sets whether the camera's orientation and position is locked to the head-mounted display +```csharp +public bool LockToHeadMountedDisplay { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/CameraComponent-OrthoFarClipPlane.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CameraComponent](./CameraComponent.md 'UnrealEngine.Framework.CameraComponent') +## CameraComponent.OrthoFarClipPlane Property +Gets or sets the far plane distance of the orthographic view (in world units) +```csharp +public float OrthoFarClipPlane { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/CameraComponent-OrthoNearClipPlane.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CameraComponent](./CameraComponent.md 'UnrealEngine.Framework.CameraComponent') +## CameraComponent.OrthoNearClipPlane Property +Gets or sets the near plane distance of the orthographic view (in world units) +```csharp +public float OrthoNearClipPlane { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/CameraComponent-OrthoWidth.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CameraComponent](./CameraComponent.md 'UnrealEngine.Framework.CameraComponent') +## CameraComponent.OrthoWidth Property +Gets or sets the desired width of the orthographic view (in world units) +```csharp +public float OrthoWidth { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/CameraComponent-SetProjectionMode(CameraProjectionMode).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CameraComponent](./CameraComponent.md 'UnrealEngine.Framework.CameraComponent') +## CameraComponent.SetProjectionMode(UnrealEngine.Framework.CameraProjectionMode) Method +Sets the projection mode +```csharp +public void SetProjectionMode(UnrealEngine.Framework.CameraProjectionMode type); +``` +#### Parameters + +`type` [CameraProjectionMode](./CameraProjectionMode.md 'UnrealEngine.Framework.CameraProjectionMode') + + +``` + +`API/CameraComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## CameraComponent Class +Represents a camera viewpoint and settings, such as projection type, field of view, and post-process overrides +```csharp +public class CameraComponent : UnrealEngine.Framework.SceneComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 CameraComponent +### Constructors +- [CameraComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./CameraComponent-CameraComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.CameraComponent.CameraComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Properties +- [AspectRatio](./CameraComponent-AspectRatio.md 'UnrealEngine.Framework.CameraComponent.AspectRatio') +- [ConstrainAspectRatio](./CameraComponent-ConstrainAspectRatio.md 'UnrealEngine.Framework.CameraComponent.ConstrainAspectRatio') +- [FieldOfView](./CameraComponent-FieldOfView.md 'UnrealEngine.Framework.CameraComponent.FieldOfView') +- [LockToHeadMountedDisplay](./CameraComponent-LockToHeadMountedDisplay.md 'UnrealEngine.Framework.CameraComponent.LockToHeadMountedDisplay') +- [OrthoFarClipPlane](./CameraComponent-OrthoFarClipPlane.md 'UnrealEngine.Framework.CameraComponent.OrthoFarClipPlane') +- [OrthoNearClipPlane](./CameraComponent-OrthoNearClipPlane.md 'UnrealEngine.Framework.CameraComponent.OrthoNearClipPlane') +- [OrthoWidth](./CameraComponent-OrthoWidth.md 'UnrealEngine.Framework.CameraComponent.OrthoWidth') +### Methods +- [SetProjectionMode(UnrealEngine.Framework.CameraProjectionMode)](./CameraComponent-SetProjectionMode(CameraProjectionMode).md 'UnrealEngine.Framework.CameraComponent.SetProjectionMode(UnrealEngine.Framework.CameraProjectionMode)') + +``` + +`API/CameraProjectionMode.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## CameraProjectionMode Enum +Defines the projection mode for a camera +```csharp +public enum CameraProjectionMode +``` +### Fields + +`Perspective` 0 + + + +`Orthographic` 1 + + + +``` + +`API/CapsuleComponent-CapsuleComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CapsuleComponent](./CapsuleComponent.md 'UnrealEngine.Framework.CapsuleComponent') +## CapsuleComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public CapsuleComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/CapsuleComponent-GetScaledCapsuleSize(float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CapsuleComponent](./CapsuleComponent.md 'UnrealEngine.Framework.CapsuleComponent') +## CapsuleComponent.GetScaledCapsuleSize(float, float) Method +Retrieves the capsule radius and half-height scaled by the component scale +```csharp +public void GetScaledCapsuleSize(ref float radius, ref float halfHeight); +``` +#### Parameters + +`radius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`halfHeight` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/CapsuleComponent-GetUnscaledCapsuleSize(float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CapsuleComponent](./CapsuleComponent.md 'UnrealEngine.Framework.CapsuleComponent') +## CapsuleComponent.GetUnscaledCapsuleSize(float, float) Method +Retrieves the capsule radius and half-height ignoring the component scale +```csharp +public void GetUnscaledCapsuleSize(ref float radius, ref float halfHeight); +``` +#### Parameters + +`radius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`halfHeight` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/CapsuleComponent-InitCapsuleSize(float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CapsuleComponent](./CapsuleComponent.md 'UnrealEngine.Framework.CapsuleComponent') +## CapsuleComponent.InitCapsuleSize(float, float) Method +Sets the capsule size without triggering a render or physics update +```csharp +public void InitCapsuleSize(float radius, float halfHeight); +``` +#### Parameters + +`radius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`halfHeight` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/CapsuleComponent-ScaledCapsuleRadius.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CapsuleComponent](./CapsuleComponent.md 'UnrealEngine.Framework.CapsuleComponent') +## CapsuleComponent.ScaledCapsuleRadius Property +Returns the capsule radius scaled by the component scale +```csharp +public float ScaledCapsuleRadius { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/CapsuleComponent-SetCapsuleRadius(float_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CapsuleComponent](./CapsuleComponent.md 'UnrealEngine.Framework.CapsuleComponent') +## CapsuleComponent.SetCapsuleRadius(float, bool) Method +Sets the capsule radius, unscaled before the component scale is applied +```csharp +public void SetCapsuleRadius(float radius, bool updateOverlaps=true); +``` +#### Parameters + +`radius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`updateOverlaps` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/CapsuleComponent-SetCapsuleSize(float_float_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CapsuleComponent](./CapsuleComponent.md 'UnrealEngine.Framework.CapsuleComponent') +## CapsuleComponent.SetCapsuleSize(float, float, bool) Method +Sets the capsule size, unscaled before the component scale is applied +```csharp +public void SetCapsuleSize(float radius, float halfHeight, bool updateOverlaps=true); +``` +#### Parameters + +`radius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`halfHeight` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`updateOverlaps` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/CapsuleComponent-ShapeScale.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CapsuleComponent](./CapsuleComponent.md 'UnrealEngine.Framework.CapsuleComponent') +## CapsuleComponent.ShapeScale Property +Returns the scale of the shape +```csharp +public float ShapeScale { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/CapsuleComponent-UnscaledCapsuleRadius.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CapsuleComponent](./CapsuleComponent.md 'UnrealEngine.Framework.CapsuleComponent') +## CapsuleComponent.UnscaledCapsuleRadius Property +Returns the capsule radius ignoring the component scale +```csharp +public float UnscaledCapsuleRadius { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/CapsuleComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## CapsuleComponent Class +A capsule generally used for simple collision +```csharp +public class CapsuleComponent : UnrealEngine.Framework.ShapeComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') 🡒 [ShapeComponent](./ShapeComponent.md 'UnrealEngine.Framework.ShapeComponent') 🡒 CapsuleComponent +### Constructors +- [CapsuleComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./CapsuleComponent-CapsuleComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.CapsuleComponent.CapsuleComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Properties +- [ScaledCapsuleRadius](./CapsuleComponent-ScaledCapsuleRadius.md 'UnrealEngine.Framework.CapsuleComponent.ScaledCapsuleRadius') +- [ShapeScale](./CapsuleComponent-ShapeScale.md 'UnrealEngine.Framework.CapsuleComponent.ShapeScale') +- [UnscaledCapsuleRadius](./CapsuleComponent-UnscaledCapsuleRadius.md 'UnrealEngine.Framework.CapsuleComponent.UnscaledCapsuleRadius') +### Methods +- [GetScaledCapsuleSize(float, float)](./CapsuleComponent-GetScaledCapsuleSize(float_float).md 'UnrealEngine.Framework.CapsuleComponent.GetScaledCapsuleSize(float, float)') +- [GetUnscaledCapsuleSize(float, float)](./CapsuleComponent-GetUnscaledCapsuleSize(float_float).md 'UnrealEngine.Framework.CapsuleComponent.GetUnscaledCapsuleSize(float, float)') +- [InitCapsuleSize(float, float)](./CapsuleComponent-InitCapsuleSize(float_float).md 'UnrealEngine.Framework.CapsuleComponent.InitCapsuleSize(float, float)') +- [SetCapsuleRadius(float, bool)](./CapsuleComponent-SetCapsuleRadius(float_bool).md 'UnrealEngine.Framework.CapsuleComponent.SetCapsuleRadius(float, bool)') +- [SetCapsuleSize(float, float, bool)](./CapsuleComponent-SetCapsuleSize(float_float_bool).md 'UnrealEngine.Framework.CapsuleComponent.SetCapsuleSize(float, float, bool)') + +``` + +`API/Character-CanCrouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Character](./Character.md 'UnrealEngine.Framework.Character') +## Character.CanCrouch Property +Returns `true` if the character can crouch +```csharp +public bool CanCrouch { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Character-CanJump.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Character](./Character.md 'UnrealEngine.Framework.Character') +## Character.CanJump Property +Returns `true` if the character can jump +```csharp +public bool CanJump { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Character-Character(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Character](./Character.md 'UnrealEngine.Framework.Character') +## Character(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public Character(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/Character-CheckJumpInput(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Character](./Character.md 'UnrealEngine.Framework.Character') +## Character.CheckJumpInput(float) Method +Triggers jump if a jump button is pressed +```csharp +public void CheckJumpInput(float deltaTime); +``` +#### Parameters + +`deltaTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/Character-ClearJumpInput(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Character](./Character.md 'UnrealEngine.Framework.Character') +## Character.ClearJumpInput(float) Method +Updates jump input state after checking input +```csharp +public void ClearJumpInput(float deltaTime); +``` +#### Parameters + +`deltaTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/Character-Crouch().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Character](./Character.md 'UnrealEngine.Framework.Character') +## Character.Crouch() Method +Starts the character crouching on the next update +```csharp +public void Crouch(); +``` + +``` + +`API/Character-IsCrouched.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Character](./Character.md 'UnrealEngine.Framework.Character') +## Character.IsCrouched Property +Returns `true` if the character is currently crouched +```csharp +public bool IsCrouched { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Character-Jump().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Character](./Character.md 'UnrealEngine.Framework.Character') +## Character.Jump() Method +Starts the character jumping on the next update +```csharp +public void Jump(); +``` + +``` + +`API/Character-Launch(Vector3_bool_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Character](./Character.md 'UnrealEngine.Framework.Character') +## Character.Launch(System.Numerics.Vector3, bool, bool) Method +Launches the character using the specified velocity +```csharp +public void Launch(in System.Numerics.Vector3 velocity, bool overrideXY=false, bool overrideZ=false); +``` +#### Parameters + +`velocity` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`overrideXY` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`overrideZ` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/Character-SetOnLandedCallback(CharacterLandedDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Character](./Character.md 'UnrealEngine.Framework.Character') +## Character.SetOnLandedCallback(UnrealEngine.Framework.CharacterLandedDelegate) Method +Sets the callback function that is called when the character landing after falling +```csharp +public void SetOnLandedCallback(UnrealEngine.Framework.CharacterLandedDelegate callback); +``` +#### Parameters + +`callback` [CharacterLandedDelegate(UnrealEngine.Framework.Hit)](./CharacterLandedDelegate(Hit).md 'UnrealEngine.Framework.CharacterLandedDelegate(UnrealEngine.Framework.Hit)') + + +``` + +`API/Character-StopCrouching().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Character](./Character.md 'UnrealEngine.Framework.Character') +## Character.StopCrouching() Method +Stops the character crouching on the next update +```csharp +public void StopCrouching(); +``` + +``` + +`API/Character-StopJumping().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Character](./Character.md 'UnrealEngine.Framework.Character') +## Character.StopJumping() Method +Stops the character from jumping on the next update +```csharp +public void StopJumping(); +``` + +``` + +`API/Character.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Character Class +Represents a character that have a mesh, collision, and built-in movement logic +```csharp +public class Character : UnrealEngine.Framework.Pawn +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 [Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') 🡒 Character +### Constructors +- [Character(string, UnrealEngine.Framework.Blueprint)](./Character-Character(string_Blueprint).md 'UnrealEngine.Framework.Character.Character(string, UnrealEngine.Framework.Blueprint)') +### Properties +- [CanCrouch](./Character-CanCrouch.md 'UnrealEngine.Framework.Character.CanCrouch') +- [CanJump](./Character-CanJump.md 'UnrealEngine.Framework.Character.CanJump') +- [IsCrouched](./Character-IsCrouched.md 'UnrealEngine.Framework.Character.IsCrouched') +### Methods +- [CheckJumpInput(float)](./Character-CheckJumpInput(float).md 'UnrealEngine.Framework.Character.CheckJumpInput(float)') +- [ClearJumpInput(float)](./Character-ClearJumpInput(float).md 'UnrealEngine.Framework.Character.ClearJumpInput(float)') +- [Crouch()](./Character-Crouch().md 'UnrealEngine.Framework.Character.Crouch()') +- [Jump()](./Character-Jump().md 'UnrealEngine.Framework.Character.Jump()') +- [Launch(System.Numerics.Vector3, bool, bool)](./Character-Launch(Vector3_bool_bool).md 'UnrealEngine.Framework.Character.Launch(System.Numerics.Vector3, bool, bool)') +- [SetOnLandedCallback(UnrealEngine.Framework.CharacterLandedDelegate)](./Character-SetOnLandedCallback(CharacterLandedDelegate).md 'UnrealEngine.Framework.Character.SetOnLandedCallback(UnrealEngine.Framework.CharacterLandedDelegate)') +- [StopCrouching()](./Character-StopCrouching().md 'UnrealEngine.Framework.Character.StopCrouching()') +- [StopJumping()](./Character-StopJumping().md 'UnrealEngine.Framework.Character.StopJumping()') + +``` + +`API/CharacterLandedDelegate(Hit).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## CharacterLandedDelegate(UnrealEngine.Framework.Hit) Delegate +Delegate for character landing events +```csharp +public delegate void CharacterLandedDelegate(in UnrealEngine.Framework.Hit hit); +``` +#### Parameters + +`hit` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +``` + +`API/ChildActorComponent-ChildActorComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ChildActorComponent](./ChildActorComponent.md 'UnrealEngine.Framework.ChildActorComponent') +## ChildActorComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public ChildActorComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/ChildActorComponent-GetChildActor-T-().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ChildActorComponent](./ChildActorComponent.md 'UnrealEngine.Framework.ChildActorComponent') +## ChildActorComponent.GetChildActor<T>() Method +Gets the child actor +```csharp +public T GetChildActor() + where T : UnrealEngine.Framework.Actor; +``` +#### Type parameters + +`T` + +Constraints [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Returns +[T](#UnrealEngine-Framework-ChildActorComponent-GetChildActor-T-()-T 'UnrealEngine.Framework.ChildActorComponent.GetChildActor<T>().T') +An actor or `null` on failure + +``` + +`API/ChildActorComponent-SetChildActor-T-().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ChildActorComponent](./ChildActorComponent.md 'UnrealEngine.Framework.ChildActorComponent') +## ChildActorComponent.SetChildActor<T>() Method +Sets the child actor +```csharp +public T SetChildActor() + where T : UnrealEngine.Framework.Actor; +``` +#### Type parameters + +`T` + +Constraints [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Returns +[T](#UnrealEngine-Framework-ChildActorComponent-SetChildActor-T-()-T 'UnrealEngine.Framework.ChildActorComponent.SetChildActor<T>().T') +An actor or `null` on failure + +``` + +`API/ChildActorComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ChildActorComponent Class +A component that automatically spawns and destroys a child actor +```csharp +public class ChildActorComponent : UnrealEngine.Framework.SceneComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 ChildActorComponent +### Constructors +- [ChildActorComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./ChildActorComponent-ChildActorComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.ChildActorComponent.ChildActorComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Methods +- [GetChildActor<T>()](./ChildActorComponent-GetChildActor-T-().md 'UnrealEngine.Framework.ChildActorComponent.GetChildActor<T>()') +- [SetChildActor<T>()](./ChildActorComponent-SetChildActor-T-().md 'UnrealEngine.Framework.ChildActorComponent.SetChildActor<T>()') + +``` + +`API/CollisionChannel.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## CollisionChannel Enum +Defines collision channels +```csharp +public enum CollisionChannel +``` +### Fields + +`WorldStatic` 0 + + + +`WorldDynamic` 1 + + + +`Pawn` 2 + + + +`Visibility` 3 + + + +`Camera` 4 + + + +`PhysicsBody` 5 + + + +`Vehicle` 6 + + + +`Destructible` 7 + + + +`EngineTraceChannel1` 8 + + + +`EngineTraceChannel2` 9 + + + +`EngineTraceChannel3` 10 + + + +`EngineTraceChannel4` 11 + + + +`EngineTraceChannel5` 12 + + + +`EngineTraceChannel6` 13 + + + +`GameTraceChannel1` 14 + + + +`GameTraceChannel2` 15 + + + +`GameTraceChannel3` 16 + + + +`GameTraceChannel4` 17 + + + +`GameTraceChannel5` 18 + + + +`GameTraceChannel6` 19 + + + +`GameTraceChannel7` 20 + + + +`GameTraceChannel8` 21 + + + +`GameTraceChannel9` 22 + + + +`GameTraceChannel10` 23 + + + +`GameTraceChannel11` 24 + + + +`GameTraceChannel12` 25 + + + +`GameTraceChannel13` 26 + + + +`GameTraceChannel14` 27 + + + +`GameTraceChannel15` 28 + + + +`GameTraceChannel16` 29 + + + +`GameTraceChannel17` 30 + + + +`GameTraceChannel18` 31 + + + +``` + +`API/CollisionMode.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## CollisionMode Enum +Defines the collision mode +```csharp +public enum CollisionMode +``` +### Fields + +`NoCollision` 0 +No collision + + +`QueryOnly` 1 +Used for spatial queries (raycasts, sweeps, and overlaps) + + +`PhysicsOnly` 2 +Used for physics simulation (rigid bodies, and constraints) + + +`QueryAndPhysics` 3 +Can be used for both spatial queries and physics simulation + + +``` + +`API/CollisionResponse.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## CollisionResponse Enum +Defines collision response types +```csharp +public enum CollisionResponse +``` +### Fields + +`Ignore` 0 + + + +`Overlap` 1 + + + +`Block` 2 + + + +``` + +`API/CollisionShape-CreateBox(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') +## CollisionShape.CreateBox(System.Numerics.Vector3) Method +Returns a box shape +```csharp +public static UnrealEngine.Framework.CollisionShape CreateBox(in System.Numerics.Vector3 halfExtent); +``` +#### Parameters + +`halfExtent` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +#### Returns +[CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + +``` + +`API/CollisionShape-CreateCapsule(float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') +## CollisionShape.CreateCapsule(float, float) Method +Returns a capsule shape +```csharp +public static UnrealEngine.Framework.CollisionShape CreateCapsule(float radius, float halfHeight); +``` +#### Parameters + +`radius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`halfHeight` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + +``` + +`API/CollisionShape-CreateSphere(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') +## CollisionShape.CreateSphere(float) Method +Returns a sphere shape +```csharp +public static UnrealEngine.Framework.CollisionShape CreateSphere(float radius); +``` +#### Parameters + +`radius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + +``` + +`API/CollisionShape-Equals(CollisionShape).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') +## CollisionShape.Equals(UnrealEngine.Framework.CollisionShape) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.CollisionShape other); +``` +#### Parameters + +`other` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/CollisionShape-Equals(object).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') +## CollisionShape.Equals(object) Method +Indicates equality of objects +```csharp +public override bool Equals(object value); +``` +#### Parameters + +`value` [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/CollisionShape-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') +## CollisionShape.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/CollisionShape-ShapeType.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') +## CollisionShape.ShapeType Property +Returns the shape type +```csharp +public UnrealEngine.Framework.CollisionShapeType ShapeType { get; } +``` +#### Property Value +[CollisionShapeType](./CollisionShapeType.md 'UnrealEngine.Framework.CollisionShapeType') + +``` + +`API/CollisionShape-op_Equality(CollisionShape_CollisionShape).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') +## CollisionShape.operator ==(UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.CollisionShape) Operator +Tests for equality between two objects +```csharp +public static bool operator ==(UnrealEngine.Framework.CollisionShape left, UnrealEngine.Framework.CollisionShape right); +``` +#### Parameters + +`left` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + + +`right` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/CollisionShape-op_Inequality(CollisionShape_CollisionShape).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') +## CollisionShape.operator !=(UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.CollisionShape) Operator +Tests for inequality between two objects +```csharp +public static bool operator !=(UnrealEngine.Framework.CollisionShape left, UnrealEngine.Framework.CollisionShape right); +``` +#### Parameters + +`left` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + + +`right` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/CollisionShape.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## CollisionShape Struct +A collision shape +```csharp +public struct CollisionShape : +System.IEquatable +``` +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [ShapeType](./CollisionShape-ShapeType.md 'UnrealEngine.Framework.CollisionShape.ShapeType') +### Methods +- [CreateBox(System.Numerics.Vector3)](./CollisionShape-CreateBox(Vector3).md 'UnrealEngine.Framework.CollisionShape.CreateBox(System.Numerics.Vector3)') +- [CreateCapsule(float, float)](./CollisionShape-CreateCapsule(float_float).md 'UnrealEngine.Framework.CollisionShape.CreateCapsule(float, float)') +- [CreateSphere(float)](./CollisionShape-CreateSphere(float).md 'UnrealEngine.Framework.CollisionShape.CreateSphere(float)') +- [Equals(object)](./CollisionShape-Equals(object).md 'UnrealEngine.Framework.CollisionShape.Equals(object)') +- [Equals(UnrealEngine.Framework.CollisionShape)](./CollisionShape-Equals(CollisionShape).md 'UnrealEngine.Framework.CollisionShape.Equals(UnrealEngine.Framework.CollisionShape)') +- [GetHashCode()](./CollisionShape-GetHashCode().md 'UnrealEngine.Framework.CollisionShape.GetHashCode()') +### Operators +- [operator ==(UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.CollisionShape)](./CollisionShape-op_Equality(CollisionShape_CollisionShape).md 'UnrealEngine.Framework.CollisionShape.op_Equality(UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.CollisionShape)') +- [operator !=(UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.CollisionShape)](./CollisionShape-op_Inequality(CollisionShape_CollisionShape).md 'UnrealEngine.Framework.CollisionShape.op_Inequality(UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.CollisionShape)') + +``` + +`API/CollisionShapeType.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## CollisionShapeType Enum +Defines the collision shape type +```csharp +public enum CollisionShapeType +``` +### Fields + +`Box` 1 + + + +`Sphere` 2 + + + +`Capsule` 3 + + + +``` + +`API/CommandLine-Append(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CommandLine](./CommandLine.md 'UnrealEngine.Framework.CommandLine') +## CommandLine.Append(string) Method +Appends the string to the arguments as it is without adding a space +```csharp +public static void Append(string arguments); +``` +#### Parameters + +`arguments` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/CommandLine-Get().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CommandLine](./CommandLine.md 'UnrealEngine.Framework.CommandLine') +## CommandLine.Get() Method +Returns the user arguments +```csharp +public static string Get(); +``` +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/CommandLine-Set(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[CommandLine](./CommandLine.md 'UnrealEngine.Framework.CommandLine') +## CommandLine.Set(string) Method +Overrides the arguments +```csharp +public static void Set(string arguments); +``` +#### Parameters + +`arguments` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/CommandLine.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## CommandLine Class +Functionality to work with the command-line of the engine executable +```csharp +public static class CommandLine +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 CommandLine +### Methods +- [Append(string)](./CommandLine-Append(string).md 'UnrealEngine.Framework.CommandLine.Append(string)') +- [Get()](./CommandLine-Get().md 'UnrealEngine.Framework.CommandLine.Get()') +- [Set(string)](./CommandLine-Set(string).md 'UnrealEngine.Framework.CommandLine.Set(string)') + +``` + +`API/ComponentCursorDelegate(ComponentReference).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ComponentCursorDelegate(UnrealEngine.Framework.ComponentReference) Delegate +Delegate for component cursor events +```csharp +public delegate void ComponentCursorDelegate(UnrealEngine.Framework.ComponentReference component); +``` +#### Parameters + +`component` [ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') + + +``` + +`API/ComponentEventType.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ComponentEventType Enum +Defines component events +```csharp +public enum ComponentEventType +``` +### Fields + +`OnComponentBeginOverlap` 0 +Called when components start overlapping + + +`OnComponentEndOverlap` 1 +Called when components stop overlapping + + +`OnComponentHit` 2 +Called when components hit collisions + + +`OnComponentBeginCursorOver` 3 +Called when the mouse cursor is moved over a component and mouse over events are enabled in the player controller + + +`OnComponentEndCursorOver` 4 +Called when the mouse cursor is moved off a component and mouse over events are enabled in the player controller + + +`OnComponentClicked` 5 +Called when the mouse button is clicked while the mouse is over a component if click events are enabled in the player controller + + +`OnComponentReleased` 6 +Called when the mouse button is released while the mouse is over a component if click events are enabled in the player controller + + +``` + +`API/ComponentHitDelegate(ComponentReference_ComponentReference_Vector3_Hit).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ComponentHitDelegate(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference, System.Numerics.Vector3, UnrealEngine.Framework.Hit) Delegate +Delegate for component hit events +```csharp +public delegate void ComponentHitDelegate(UnrealEngine.Framework.ComponentReference hitComponent, UnrealEngine.Framework.ComponentReference otherComponent, in System.Numerics.Vector3 normalImpulse, in UnrealEngine.Framework.Hit hit); +``` +#### Parameters + +`hitComponent` [ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') + + +`otherComponent` [ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') + + +`normalImpulse` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`hit` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +``` + +`API/ComponentKeyDelegate(ComponentReference_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ComponentKeyDelegate(UnrealEngine.Framework.ComponentReference, string) Delegate +Delegate for component key events +```csharp +public delegate void ComponentKeyDelegate(UnrealEngine.Framework.ComponentReference component, string key); +``` +#### Parameters + +`component` [ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') + + +`key` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/ComponentMobility.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ComponentMobility Enum +Defines how often a component is allowed to move +```csharp +public enum ComponentMobility +``` +### Fields + +`Movable` 0 +Movable objects can be moved and changed + + +`Static` 1 +Static objects cannot be moved or changed +- Allows baked lighting +- Fastest rendering + + +`Stationary` 2 +A stationary light will only have its shadowing and bounced lighting from static geometry baked by Lightmass, all other lighting will be dynamic + + +``` + +`API/ComponentOverlapDelegate(ComponentReference_ComponentReference).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ComponentOverlapDelegate(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference) Delegate +Delegate for component overlap events +```csharp +public delegate void ComponentOverlapDelegate(UnrealEngine.Framework.ComponentReference overlapComponent, UnrealEngine.Framework.ComponentReference otherComponent); +``` +#### Parameters + +`overlapComponent` [ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') + + +`otherComponent` [ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') + + +``` + +`API/ComponentReference-Equals(ComponentReference).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') +## ComponentReference.Equals(UnrealEngine.Framework.ComponentReference) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.ComponentReference other); +``` +#### Parameters + +`other` [ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ComponentReference-Equals(object).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') +## ComponentReference.Equals(object) Method +Indicates equality of objects +```csharp +public override bool Equals(object value); +``` +#### Parameters + +`value` [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ComponentReference-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') +## ComponentReference.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/ComponentReference-ID.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') +## ComponentReference.ID Property +Returns the unique ID of the object, reused by the engine, only unique while the object is alive +```csharp +public uint ID { get; } +``` +#### Property Value +[System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') + +``` + +`API/ComponentReference-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') +## ComponentReference.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ComponentReference-Name.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') +## ComponentReference.Name Property +Returns the name of the object +```csharp +public string Name { get; } +``` +#### Property Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/ComponentReference-ToComponent-T-().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') +## ComponentReference.ToComponent<T>() Method +Converts the component reference to the component of the specified class +```csharp +public T ToComponent() + where T : UnrealEngine.Framework.ActorComponent; +``` +#### Type parameters + +`T` + +Constraints [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') + +#### Returns +[T](#UnrealEngine-Framework-ComponentReference-ToComponent-T-()-T 'UnrealEngine.Framework.ComponentReference.ToComponent<T>().T') +A component or `null` on failure + +``` + +`API/ComponentReference-op_Equality(ComponentReference_ComponentReference).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') +## ComponentReference.operator ==(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference) Operator +Tests for equality between two objects +```csharp +public static bool operator ==(UnrealEngine.Framework.ComponentReference left, UnrealEngine.Framework.ComponentReference right); +``` +#### Parameters + +`left` [ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') + + +`right` [ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ComponentReference-op_Inequality(ComponentReference_ComponentReference).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') +## ComponentReference.operator !=(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference) Operator +Tests for inequality between two objects +```csharp +public static bool operator !=(UnrealEngine.Framework.ComponentReference left, UnrealEngine.Framework.ComponentReference right); +``` +#### Parameters + +`left` [ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') + + +`right` [ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ComponentReference.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ComponentReference Struct +A representation of the engine's component reference +```csharp +public struct ComponentReference : +System.IEquatable +``` +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [ID](./ComponentReference-ID.md 'UnrealEngine.Framework.ComponentReference.ID') +- [IsCreated](./ComponentReference-IsCreated.md 'UnrealEngine.Framework.ComponentReference.IsCreated') +- [Name](./ComponentReference-Name.md 'UnrealEngine.Framework.ComponentReference.Name') +### Methods +- [Equals(object)](./ComponentReference-Equals(object).md 'UnrealEngine.Framework.ComponentReference.Equals(object)') +- [Equals(UnrealEngine.Framework.ComponentReference)](./ComponentReference-Equals(ComponentReference).md 'UnrealEngine.Framework.ComponentReference.Equals(UnrealEngine.Framework.ComponentReference)') +- [GetHashCode()](./ComponentReference-GetHashCode().md 'UnrealEngine.Framework.ComponentReference.GetHashCode()') +- [ToComponent<T>()](./ComponentReference-ToComponent-T-().md 'UnrealEngine.Framework.ComponentReference.ToComponent<T>()') +### Operators +- [operator ==(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference)](./ComponentReference-op_Equality(ComponentReference_ComponentReference).md 'UnrealEngine.Framework.ComponentReference.op_Equality(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference)') +- [operator !=(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference)](./ComponentReference-op_Inequality(ComponentReference_ComponentReference).md 'UnrealEngine.Framework.ComponentReference.op_Inequality(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference)') + +``` + +`API/ConsoleCommandDelegate(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ConsoleCommandDelegate(float) Delegate +Delegate for console command events +```csharp +public delegate void ConsoleCommandDelegate(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/ConsoleManager-FindVariable(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleManager](./ConsoleManager.md 'UnrealEngine.Framework.ConsoleManager') +## ConsoleManager.FindVariable(string) Method +Finds a console variable +```csharp +public static UnrealEngine.Framework.ConsoleVariable FindVariable(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +A console variable or `null` on failure + +``` + +`API/ConsoleManager-IsRegisteredVariable(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleManager](./ConsoleManager.md 'UnrealEngine.Framework.ConsoleManager') +## ConsoleManager.IsRegisteredVariable(string) Method +Returns `true` if a console command or variable has been registered +```csharp +public static bool IsRegisteredVariable(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ConsoleManager-RegisterCommand(string_string_ConsoleCommandDelegate_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleManager](./ConsoleManager.md 'UnrealEngine.Framework.ConsoleManager') +## ConsoleManager.RegisterCommand(string, string, UnrealEngine.Framework.ConsoleCommandDelegate, bool) Method +Creates and registers the callback function for a console command that takes no arguments, remains alive during the lifetime of the engine until unregistered +```csharp +public static void RegisterCommand(string name, string help, UnrealEngine.Framework.ConsoleCommandDelegate callback, bool readOnly=false); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the command + + +`help` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +Help text for the command + + +`callback` [ConsoleCommandDelegate(float)](./ConsoleCommandDelegate(float).md 'UnrealEngine.Framework.ConsoleCommandDelegate(float)') +The function to call when the command is executed + + +`readOnly` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, cannot be changed by the user from console + + +``` + +`API/ConsoleManager-RegisterVariable(string_string_bool_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleManager](./ConsoleManager.md 'UnrealEngine.Framework.ConsoleManager') +## ConsoleManager.RegisterVariable(string, string, bool, bool) Method +Creates and registers a bool console variable, remains alive during the lifetime of the engine until unregistered +```csharp +public static UnrealEngine.Framework.ConsoleVariable RegisterVariable(string name, string help, bool defaultValue=false, bool readOnly=false); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the variable + + +`help` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +Help text for the variable + + +`defaultValue` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +A default value + + +`readOnly` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, cannot be changed by the user from console + +#### Returns +[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +A console variable or `null` on failure + +``` + +`API/ConsoleManager-RegisterVariable(string_string_float_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleManager](./ConsoleManager.md 'UnrealEngine.Framework.ConsoleManager') +## ConsoleManager.RegisterVariable(string, string, float, bool) Method +Creates and registers a float console variable, remains alive during the lifetime of the engine until unregistered +```csharp +public static UnrealEngine.Framework.ConsoleVariable RegisterVariable(string name, string help, float defaultValue=0f, bool readOnly=false); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the variable + + +`help` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +Help text for the variable + + +`defaultValue` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +A default value + + +`readOnly` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, cannot be changed by the user from console + +#### Returns +[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +A console variable or `null` on failure + +``` + +`API/ConsoleManager-RegisterVariable(string_string_int_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleManager](./ConsoleManager.md 'UnrealEngine.Framework.ConsoleManager') +## ConsoleManager.RegisterVariable(string, string, int, bool) Method +Creates and registers an integer console variable, remains alive during the lifetime of the engine until unregistered +```csharp +public static UnrealEngine.Framework.ConsoleVariable RegisterVariable(string name, string help, int defaultValue=0, bool readOnly=false); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the variable + + +`help` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +Help text for the variable + + +`defaultValue` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') +A default value + + +`readOnly` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, cannot be changed by the user from console + +#### Returns +[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +A console variable or `null` on failure + +``` + +`API/ConsoleManager-RegisterVariable(string_string_string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleManager](./ConsoleManager.md 'UnrealEngine.Framework.ConsoleManager') +## ConsoleManager.RegisterVariable(string, string, string, bool) Method +Creates and registers a string console variable, remains alive during the lifetime of the engine until unregistered +```csharp +public static UnrealEngine.Framework.ConsoleVariable RegisterVariable(string name, string help, string defaultValue=null, bool readOnly=false); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the variable + + +`help` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +Help text for the variable + + +`defaultValue` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +A default value + + +`readOnly` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, cannot be changed by the user from console + +#### Returns +[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +A console variable or `null` on failure + +``` + +`API/ConsoleManager-UnregisterObject(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleManager](./ConsoleManager.md 'UnrealEngine.Framework.ConsoleManager') +## ConsoleManager.UnregisterObject(string) Method +Deletes and unregisters a console command or variable +```csharp +public static void UnregisterObject(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/ConsoleManager.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ConsoleManager Class +Handles console commands and variables +```csharp +public static class ConsoleManager +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 ConsoleManager +### Methods +- [FindVariable(string)](./ConsoleManager-FindVariable(string).md 'UnrealEngine.Framework.ConsoleManager.FindVariable(string)') +- [IsRegisteredVariable(string)](./ConsoleManager-IsRegisteredVariable(string).md 'UnrealEngine.Framework.ConsoleManager.IsRegisteredVariable(string)') +- [RegisterCommand(string, string, UnrealEngine.Framework.ConsoleCommandDelegate, bool)](./ConsoleManager-RegisterCommand(string_string_ConsoleCommandDelegate_bool).md 'UnrealEngine.Framework.ConsoleManager.RegisterCommand(string, string, UnrealEngine.Framework.ConsoleCommandDelegate, bool)') +- [RegisterVariable(string, string, bool, bool)](./ConsoleManager-RegisterVariable(string_string_bool_bool).md 'UnrealEngine.Framework.ConsoleManager.RegisterVariable(string, string, bool, bool)') +- [RegisterVariable(string, string, int, bool)](./ConsoleManager-RegisterVariable(string_string_int_bool).md 'UnrealEngine.Framework.ConsoleManager.RegisterVariable(string, string, int, bool)') +- [RegisterVariable(string, string, float, bool)](./ConsoleManager-RegisterVariable(string_string_float_bool).md 'UnrealEngine.Framework.ConsoleManager.RegisterVariable(string, string, float, bool)') +- [RegisterVariable(string, string, string, bool)](./ConsoleManager-RegisterVariable(string_string_string_bool).md 'UnrealEngine.Framework.ConsoleManager.RegisterVariable(string, string, string, bool)') +- [UnregisterObject(string)](./ConsoleManager-UnregisterObject(string).md 'UnrealEngine.Framework.ConsoleManager.UnregisterObject(string)') + +``` + +`API/ConsoleObject-Equals(ConsoleObject).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleObject](./ConsoleObject.md 'UnrealEngine.Framework.ConsoleObject') +## ConsoleObject.Equals(UnrealEngine.Framework.ConsoleObject) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.ConsoleObject other); +``` +#### Parameters + +`other` [ConsoleObject](./ConsoleObject.md 'UnrealEngine.Framework.ConsoleObject') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ConsoleObject-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleObject](./ConsoleObject.md 'UnrealEngine.Framework.ConsoleObject') +## ConsoleObject.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/ConsoleObject-IsBool.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleObject](./ConsoleObject.md 'UnrealEngine.Framework.ConsoleObject') +## ConsoleObject.IsBool Property +Returns `true` if the object is a bool +```csharp +public bool IsBool { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ConsoleObject-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleObject](./ConsoleObject.md 'UnrealEngine.Framework.ConsoleObject') +## ConsoleObject.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ConsoleObject-IsFloat.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleObject](./ConsoleObject.md 'UnrealEngine.Framework.ConsoleObject') +## ConsoleObject.IsFloat Property +Returns `true` if the object is a float +```csharp +public bool IsFloat { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ConsoleObject-IsInt.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleObject](./ConsoleObject.md 'UnrealEngine.Framework.ConsoleObject') +## ConsoleObject.IsInt Property +Returns `true` if the object is an integer +```csharp +public bool IsInt { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ConsoleObject-IsString.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleObject](./ConsoleObject.md 'UnrealEngine.Framework.ConsoleObject') +## ConsoleObject.IsString Property +Returns `true` if the object is a string +```csharp +public bool IsString { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ConsoleObject.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ConsoleObject Class +Interface for console objects +```csharp +public class ConsoleObject : +System.IEquatable +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 ConsoleObject + +Derived +↳ [ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') + +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[ConsoleObject](./ConsoleObject.md 'UnrealEngine.Framework.ConsoleObject')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [IsBool](./ConsoleObject-IsBool.md 'UnrealEngine.Framework.ConsoleObject.IsBool') +- [IsCreated](./ConsoleObject-IsCreated.md 'UnrealEngine.Framework.ConsoleObject.IsCreated') +- [IsFloat](./ConsoleObject-IsFloat.md 'UnrealEngine.Framework.ConsoleObject.IsFloat') +- [IsInt](./ConsoleObject-IsInt.md 'UnrealEngine.Framework.ConsoleObject.IsInt') +- [IsString](./ConsoleObject-IsString.md 'UnrealEngine.Framework.ConsoleObject.IsString') +### Methods +- [Equals(UnrealEngine.Framework.ConsoleObject)](./ConsoleObject-Equals(ConsoleObject).md 'UnrealEngine.Framework.ConsoleObject.Equals(UnrealEngine.Framework.ConsoleObject)') +- [GetHashCode()](./ConsoleObject-GetHashCode().md 'UnrealEngine.Framework.ConsoleObject.GetHashCode()') + +``` + +`API/ConsoleVariable-ClearOnChangedCallback().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +## ConsoleVariable.ClearOnChangedCallback() Method +Clears callback function +```csharp +public void ClearOnChangedCallback(); +``` + +``` + +`API/ConsoleVariable-GetBool().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +## ConsoleVariable.GetBool() Method +Returns the value as a bool, also works on integers and floats +```csharp +public bool GetBool(); +``` +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ConsoleVariable-GetFloat().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +## ConsoleVariable.GetFloat() Method +Returns the value as a float, works on all types +```csharp +public float GetFloat(); +``` +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/ConsoleVariable-GetInt().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +## ConsoleVariable.GetInt() Method +Returns the value as an integer, shouldn't be used on strings +```csharp +public int GetInt(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/ConsoleVariable-GetString().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +## ConsoleVariable.GetString() Method +Returns the value as a string, works on all types +```csharp +public string GetString(); +``` +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/ConsoleVariable-SetBool(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +## ConsoleVariable.SetBool(bool) Method +Sets the value as a bool +```csharp +public void SetBool(bool value); +``` +#### Parameters + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/ConsoleVariable-SetFloat(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +## ConsoleVariable.SetFloat(float) Method +Sets the value as a float +```csharp +public void SetFloat(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/ConsoleVariable-SetInt(int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +## ConsoleVariable.SetInt(int) Method +Sets the value as an integer +```csharp +public void SetInt(int value); +``` +#### Parameters + +`value` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +``` + +`API/ConsoleVariable-SetOnChangedCallback(ConsoleVariableDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +## ConsoleVariable.SetOnChangedCallback(UnrealEngine.Framework.ConsoleVariableDelegate) Method +Sets the callback function that is called when the console variable value changes +```csharp +public void SetOnChangedCallback(UnrealEngine.Framework.ConsoleVariableDelegate callback); +``` +#### Parameters + +`callback` [ConsoleVariableDelegate()](./ConsoleVariableDelegate().md 'UnrealEngine.Framework.ConsoleVariableDelegate()') +The function to call when the value of variable is changed + + +``` + +`API/ConsoleVariable-SetString(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +## ConsoleVariable.SetString(string) Method +Sets the value as a string +```csharp +public void SetString(string value); +``` +#### Parameters + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/ConsoleVariable.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ConsoleVariable Class +Interface for console variables +```csharp +public class ConsoleVariable : UnrealEngine.Framework.ConsoleObject +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ConsoleObject](./ConsoleObject.md 'UnrealEngine.Framework.ConsoleObject') 🡒 ConsoleVariable +### Methods +- [ClearOnChangedCallback()](./ConsoleVariable-ClearOnChangedCallback().md 'UnrealEngine.Framework.ConsoleVariable.ClearOnChangedCallback()') +- [GetBool()](./ConsoleVariable-GetBool().md 'UnrealEngine.Framework.ConsoleVariable.GetBool()') +- [GetFloat()](./ConsoleVariable-GetFloat().md 'UnrealEngine.Framework.ConsoleVariable.GetFloat()') +- [GetInt()](./ConsoleVariable-GetInt().md 'UnrealEngine.Framework.ConsoleVariable.GetInt()') +- [GetString()](./ConsoleVariable-GetString().md 'UnrealEngine.Framework.ConsoleVariable.GetString()') +- [SetBool(bool)](./ConsoleVariable-SetBool(bool).md 'UnrealEngine.Framework.ConsoleVariable.SetBool(bool)') +- [SetFloat(float)](./ConsoleVariable-SetFloat(float).md 'UnrealEngine.Framework.ConsoleVariable.SetFloat(float)') +- [SetInt(int)](./ConsoleVariable-SetInt(int).md 'UnrealEngine.Framework.ConsoleVariable.SetInt(int)') +- [SetOnChangedCallback(UnrealEngine.Framework.ConsoleVariableDelegate)](./ConsoleVariable-SetOnChangedCallback(ConsoleVariableDelegate).md 'UnrealEngine.Framework.ConsoleVariable.SetOnChangedCallback(UnrealEngine.Framework.ConsoleVariableDelegate)') +- [SetString(string)](./ConsoleVariable-SetString(string).md 'UnrealEngine.Framework.ConsoleVariable.SetString(string)') + +``` + +`API/ConsoleVariableDelegate().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ConsoleVariableDelegate() Delegate +Delegate for console variable events +```csharp +public delegate void ConsoleVariableDelegate(); +``` + +``` + +`API/Controller-GetCharacter().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.GetCharacter() Method +Returns the character or `null` on failure +```csharp +public UnrealEngine.Framework.Character GetCharacter(); +``` +#### Returns +[Character](./Character.md 'UnrealEngine.Framework.Character') + +``` + +`API/Controller-GetControlRotation().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.GetControlRotation() Method +Returns the control rotation which is a full aim rotation +```csharp +public System.Numerics.Quaternion GetControlRotation(); +``` +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/Controller-GetControlRotation(Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.GetControlRotation(System.Numerics.Quaternion) Method +Retrieves the control rotation which is a full aim rotation +```csharp +public void GetControlRotation(ref System.Numerics.Quaternion value); +``` +#### Parameters + +`value` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/Controller-GetDesiredRotation().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.GetDesiredRotation() Method +Returns the target rotation of the pawn +```csharp +public System.Numerics.Quaternion GetDesiredRotation(); +``` +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/Controller-GetDesiredRotation(Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.GetDesiredRotation(System.Numerics.Quaternion) Method +Retrieves the target rotation of the pawn +```csharp +public void GetDesiredRotation(ref System.Numerics.Quaternion value); +``` +#### Parameters + +`value` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/Controller-GetPawn().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.GetPawn() Method +Returns the controller's pawn or `null` on failure +```csharp +public UnrealEngine.Framework.Pawn GetPawn(); +``` +#### Returns +[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') + +``` + +`API/Controller-GetViewTarget().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.GetViewTarget() Method +Returns the actor the controller is looking at or `null` on failure +```csharp +public UnrealEngine.Framework.Actor GetViewTarget(); +``` +#### Returns +[Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +``` + +`API/Controller-IsLookInputIgnored.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.IsLookInputIgnored Property +Returns `true` if look input is ignored +```csharp +public bool IsLookInputIgnored { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Controller-IsMoveInputIgnored.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.IsMoveInputIgnored Property +Returns `true` if movement input is ignored +```csharp +public bool IsMoveInputIgnored { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Controller-IsPlayerController.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.IsPlayerController Property +Returns `true` if the controller is a [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +```csharp +public bool IsPlayerController { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Controller-LineOfSightTo(Actor_Vector3_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.LineOfSightTo(UnrealEngine.Framework.Actor, System.Numerics.Vector3, bool) Method +Checks line to center and top of other actor +```csharp +public bool LineOfSightTo(UnrealEngine.Framework.Actor actor, in System.Numerics.Vector3 viewPoint, bool alternateChecks=false); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor whose visibility is being checked + + +`viewPoint` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Eye position visibility is being checked from, if [System.Numerics.Vector3.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3.Zero 'System.Numerics.Vector3.Zero') is passed in, uses current view target's eye position + + +`alternateChecks` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +Used only in [AIController](./AIController.md 'UnrealEngine.Framework.AIController') implementation + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if controller's [Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') can see other actor + +``` + +`API/Controller-Possess(Pawn).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.Possess(UnrealEngine.Framework.Pawn) Method +Handles attaching the controller to the specified pawn +```csharp +public void Possess(UnrealEngine.Framework.Pawn pawn); +``` +#### Parameters + +`pawn` [Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') + + +``` + +`API/Controller-ResetIgnoreLookInput().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.ResetIgnoreLookInput() Method +Stops ignoring look input by resetting the ignore look input state +```csharp +public void ResetIgnoreLookInput(); +``` + +``` + +`API/Controller-ResetIgnoreMoveInput().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.ResetIgnoreMoveInput() Method +Stops ignoring move input by resetting the ignore move input state +```csharp +public void ResetIgnoreMoveInput(); +``` + +``` + +`API/Controller-SetControlRotation(Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.SetControlRotation(System.Numerics.Quaternion) Method +Sets the control rotation which is a full aim rotation +```csharp +public void SetControlRotation(in System.Numerics.Quaternion value); +``` +#### Parameters + +`value` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/Controller-SetIgnoreLookInput(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.SetIgnoreLookInput(bool) Method +Locks or unlocks look input, consecutive calls stack up and require the same amount of calls to undo, or can all be undone using [ResetIgnoreLookInput()](./Controller-ResetIgnoreLookInput().md 'UnrealEngine.Framework.Controller.ResetIgnoreLookInput()') +```csharp +public void SetIgnoreLookInput(bool value); +``` +#### Parameters + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/Controller-SetIgnoreMoveInput(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.SetIgnoreMoveInput(bool) Method +Locks or unlocks movement input, consecutive calls stack up and require the same amount of calls to undo, or can all be undone using [ResetIgnoreMoveInput()](./Controller-ResetIgnoreMoveInput().md 'UnrealEngine.Framework.Controller.ResetIgnoreMoveInput()') +```csharp +public void SetIgnoreMoveInput(bool value); +``` +#### Parameters + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/Controller-SetInitialLocationAndRotation(Vector3_Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.SetInitialLocationAndRotation(System.Numerics.Vector3, System.Numerics.Quaternion) Method +Sets the initial location and rotation of the controller, as well as the control rotation +```csharp +public void SetInitialLocationAndRotation(in System.Numerics.Vector3 newLocation, in System.Numerics.Quaternion newRotation); +``` +#### Parameters + +`newLocation` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`newRotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/Controller-Unpossess().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Controller](./Controller.md 'UnrealEngine.Framework.Controller') +## Controller.Unpossess() Method +Relinquishes control of the pawn +```csharp +public void Unpossess(); +``` + +``` + +`API/Controller.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Controller Class +Non-physical actors that can possess a [Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') to control its actions +```csharp +public abstract class Controller : UnrealEngine.Framework.Actor +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 Controller + +Derived +↳ [AIController](./AIController.md 'UnrealEngine.Framework.AIController') +↳ [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +### Properties +- [IsLookInputIgnored](./Controller-IsLookInputIgnored.md 'UnrealEngine.Framework.Controller.IsLookInputIgnored') +- [IsMoveInputIgnored](./Controller-IsMoveInputIgnored.md 'UnrealEngine.Framework.Controller.IsMoveInputIgnored') +- [IsPlayerController](./Controller-IsPlayerController.md 'UnrealEngine.Framework.Controller.IsPlayerController') +### Methods +- [GetCharacter()](./Controller-GetCharacter().md 'UnrealEngine.Framework.Controller.GetCharacter()') +- [GetControlRotation()](./Controller-GetControlRotation().md 'UnrealEngine.Framework.Controller.GetControlRotation()') +- [GetControlRotation(System.Numerics.Quaternion)](./Controller-GetControlRotation(Quaternion).md 'UnrealEngine.Framework.Controller.GetControlRotation(System.Numerics.Quaternion)') +- [GetDesiredRotation()](./Controller-GetDesiredRotation().md 'UnrealEngine.Framework.Controller.GetDesiredRotation()') +- [GetDesiredRotation(System.Numerics.Quaternion)](./Controller-GetDesiredRotation(Quaternion).md 'UnrealEngine.Framework.Controller.GetDesiredRotation(System.Numerics.Quaternion)') +- [GetPawn()](./Controller-GetPawn().md 'UnrealEngine.Framework.Controller.GetPawn()') +- [GetViewTarget()](./Controller-GetViewTarget().md 'UnrealEngine.Framework.Controller.GetViewTarget()') +- [LineOfSightTo(UnrealEngine.Framework.Actor, System.Numerics.Vector3, bool)](./Controller-LineOfSightTo(Actor_Vector3_bool).md 'UnrealEngine.Framework.Controller.LineOfSightTo(UnrealEngine.Framework.Actor, System.Numerics.Vector3, bool)') +- [Possess(UnrealEngine.Framework.Pawn)](./Controller-Possess(Pawn).md 'UnrealEngine.Framework.Controller.Possess(UnrealEngine.Framework.Pawn)') +- [ResetIgnoreLookInput()](./Controller-ResetIgnoreLookInput().md 'UnrealEngine.Framework.Controller.ResetIgnoreLookInput()') +- [ResetIgnoreMoveInput()](./Controller-ResetIgnoreMoveInput().md 'UnrealEngine.Framework.Controller.ResetIgnoreMoveInput()') +- [SetControlRotation(System.Numerics.Quaternion)](./Controller-SetControlRotation(Quaternion).md 'UnrealEngine.Framework.Controller.SetControlRotation(System.Numerics.Quaternion)') +- [SetIgnoreLookInput(bool)](./Controller-SetIgnoreLookInput(bool).md 'UnrealEngine.Framework.Controller.SetIgnoreLookInput(bool)') +- [SetIgnoreMoveInput(bool)](./Controller-SetIgnoreMoveInput(bool).md 'UnrealEngine.Framework.Controller.SetIgnoreMoveInput(bool)') +- [SetInitialLocationAndRotation(System.Numerics.Vector3, System.Numerics.Quaternion)](./Controller-SetInitialLocationAndRotation(Vector3_Quaternion).md 'UnrealEngine.Framework.Controller.SetInitialLocationAndRotation(System.Numerics.Vector3, System.Numerics.Quaternion)') +- [Unpossess()](./Controller-Unpossess().md 'UnrealEngine.Framework.Controller.Unpossess()') + +``` + +`API/ControllerHand.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ControllerHand Enum +Defines the controller hands for tracking +```csharp +public enum ControllerHand : System.Byte +``` +### Fields + +`Left` 0 + + + +`Right` 1 + + + +`AnyHand` 2 + + + +`Pad` 3 + + + +`ExternalCamera` 4 + + + +`Gun` 5 + + + +`Special1` 6 + + + +`Special2` 7 + + + +`Special3` 8 + + + +`Special4` 9 + + + +`Special5` 10 + + + +`Special6` 11 + + + +`Special7` 12 + + + +`Special8` 13 + + + +`Special9` 14 + + + +`Special10` 15 + + + +`Special11` 16 + + + +``` + +`API/Debug-AddOnScreenMessage(int_float_Color_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Debug](./Debug.md 'UnrealEngine.Framework.Debug') +## Debug.AddOnScreenMessage(int, float, System.Drawing.Color, string) Method +Prints a debug message on the screen assigned to the key identifier, omitted in builds with the Shipping configuration +```csharp +public static void AddOnScreenMessage(int key, float timeToDisplay, System.Drawing.Color displayColor, string message); +``` +#### Parameters + +`key` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`timeToDisplay` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`displayColor` [System.Drawing.Color](https://docs.microsoft.com/en-us/dotnet/api/System.Drawing.Color 'System.Drawing.Color') + + +`message` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/Debug-ClearOnScreenMessages().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Debug](./Debug.md 'UnrealEngine.Framework.Debug') +## Debug.ClearOnScreenMessages() Method +Clears any existing debug messages, omitted in builds with the Shipping configuration +```csharp +public static void ClearOnScreenMessages(); +``` + +``` + +`API/Debug-DrawBox(Vector3_Vector3_Quaternion_Color_bool_float_byte_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Debug](./Debug.md 'UnrealEngine.Framework.Debug') +## Debug.DrawBox(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, System.Drawing.Color, bool, float, byte, float) Method +Draws a debug box, omitted in builds with the Shipping configuration +```csharp +public static void DrawBox(in System.Numerics.Vector3 center, in System.Numerics.Vector3 extent, in System.Numerics.Quaternion rotation, System.Drawing.Color color, bool persistentLines=false, float lifeTime=-1f, byte depthPriority=0, float thickness=0f); +``` +#### Parameters + +`center` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`extent` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`color` [System.Drawing.Color](https://docs.microsoft.com/en-us/dotnet/api/System.Drawing.Color 'System.Drawing.Color') + + +`persistentLines` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`lifeTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`depthPriority` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte') + + +`thickness` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/Debug-DrawCapsule(Vector3_float_float_Quaternion_Color_bool_float_byte_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Debug](./Debug.md 'UnrealEngine.Framework.Debug') +## Debug.DrawCapsule(System.Numerics.Vector3, float, float, System.Numerics.Quaternion, System.Drawing.Color, bool, float, byte, float) Method +Draws a debug capsule, omitted in builds with the Shipping configuration +```csharp +public static void DrawCapsule(in System.Numerics.Vector3 center, float halfHeight, float radius, in System.Numerics.Quaternion rotation, System.Drawing.Color color, bool persistentLines=false, float lifeTime=-1f, byte depthPriority=0, float thickness=0f); +``` +#### Parameters + +`center` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`halfHeight` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`radius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`color` [System.Drawing.Color](https://docs.microsoft.com/en-us/dotnet/api/System.Drawing.Color 'System.Drawing.Color') + + +`persistentLines` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`lifeTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`depthPriority` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte') + + +`thickness` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/Debug-DrawCone(Vector3_Vector3_float_float_float_int_Color_bool_float_byte_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Debug](./Debug.md 'UnrealEngine.Framework.Debug') +## Debug.DrawCone(System.Numerics.Vector3, System.Numerics.Vector3, float, float, float, int, System.Drawing.Color, bool, float, byte, float) Method +Draws a debug cone, omitted in builds with the Shipping configuration +```csharp +public static void DrawCone(in System.Numerics.Vector3 origin, in System.Numerics.Vector3 direction, float length, float angleWidth, float angleHeight, int sides, System.Drawing.Color color, bool persistentLines=false, float lifeTime=-1f, byte depthPriority=0, float thickness=0f); +``` +#### Parameters + +`origin` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`direction` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`length` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`angleWidth` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`angleHeight` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`sides` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`color` [System.Drawing.Color](https://docs.microsoft.com/en-us/dotnet/api/System.Drawing.Color 'System.Drawing.Color') + + +`persistentLines` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`lifeTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`depthPriority` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte') + + +`thickness` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/Debug-DrawCylinder(Vector3_Vector3_float_int_Color_bool_float_byte_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Debug](./Debug.md 'UnrealEngine.Framework.Debug') +## Debug.DrawCylinder(System.Numerics.Vector3, System.Numerics.Vector3, float, int, System.Drawing.Color, bool, float, byte, float) Method +Draws a debug cylinder, omitted in builds with the Shipping configuration +```csharp +public static void DrawCylinder(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, float radius, int segments, System.Drawing.Color color, bool persistentLines=false, float lifeTime=-1f, byte depthPriority=0, float thickness=0f); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`radius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`segments` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`color` [System.Drawing.Color](https://docs.microsoft.com/en-us/dotnet/api/System.Drawing.Color 'System.Drawing.Color') + + +`persistentLines` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`lifeTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`depthPriority` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte') + + +`thickness` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/Debug-DrawLine(Vector3_Vector3_Color_bool_float_byte_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Debug](./Debug.md 'UnrealEngine.Framework.Debug') +## Debug.DrawLine(System.Numerics.Vector3, System.Numerics.Vector3, System.Drawing.Color, bool, float, byte, float) Method +Draws a debug line, omitted in builds with the Shipping configuration +```csharp +public static void DrawLine(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, System.Drawing.Color color, bool persistentLines=false, float lifeTime=-1f, byte depthPriority=0, float thickness=0f); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`color` [System.Drawing.Color](https://docs.microsoft.com/en-us/dotnet/api/System.Drawing.Color 'System.Drawing.Color') + + +`persistentLines` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`lifeTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`depthPriority` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte') + + +`thickness` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/Debug-DrawPoint(Vector3_float_Color_bool_float_byte).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Debug](./Debug.md 'UnrealEngine.Framework.Debug') +## Debug.DrawPoint(System.Numerics.Vector3, float, System.Drawing.Color, bool, float, byte) Method +Draws a debug point, omitted in builds with the Shipping configuration +```csharp +public static void DrawPoint(in System.Numerics.Vector3 location, float size, System.Drawing.Color color, bool persistentLines=false, float lifeTime=-1f, byte depthPriority=0); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`size` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`color` [System.Drawing.Color](https://docs.microsoft.com/en-us/dotnet/api/System.Drawing.Color 'System.Drawing.Color') + + +`persistentLines` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`lifeTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`depthPriority` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte') + + +``` + +`API/Debug-DrawSphere(Vector3_float_int_Color_bool_float_byte_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Debug](./Debug.md 'UnrealEngine.Framework.Debug') +## Debug.DrawSphere(System.Numerics.Vector3, float, int, System.Drawing.Color, bool, float, byte, float) Method +Draws a debug sphere, omitted in builds with the Shipping configuration +```csharp +public static void DrawSphere(in System.Numerics.Vector3 center, float radius, int segments, System.Drawing.Color color, bool persistentLines=false, float lifeTime=-1f, byte depthPriority=0, float thickness=0f); +``` +#### Parameters + +`center` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`radius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`segments` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`color` [System.Drawing.Color](https://docs.microsoft.com/en-us/dotnet/api/System.Drawing.Color 'System.Drawing.Color') + + +`persistentLines` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`lifeTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`depthPriority` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte') + + +`thickness` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/Debug-Exception(Exception).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Debug](./Debug.md 'UnrealEngine.Framework.Debug') +## Debug.Exception(System.Exception) Method +Creates a log file with the name of assembly if required and writes an exception to it, prints it on the screen, printing on the screen is omitted in builds with the Shipping configuration, but log file will persist +```csharp +public static void Exception(System.Exception exception); +``` +#### Parameters + +`exception` [System.Exception](https://docs.microsoft.com/en-us/dotnet/api/System.Exception 'System.Exception') + + +``` + +`API/Debug-FlushPersistentLines().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Debug](./Debug.md 'UnrealEngine.Framework.Debug') +## Debug.FlushPersistentLines() Method +Flushes persistent debug lines, omitted in builds with the Shipping configuration +```csharp +public static void FlushPersistentLines(); +``` + +``` + +`API/Debug-Log(LogLevel_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Debug](./Debug.md 'UnrealEngine.Framework.Debug') +## Debug.Log(UnrealEngine.Framework.LogLevel, string) Method +Logs a message in accordance to the specified level, omitted in builds with the Shipping configuration +```csharp +public static void Log(UnrealEngine.Framework.LogLevel level, string message); +``` +#### Parameters + +`level` [LogLevel](./LogLevel.md 'UnrealEngine.Framework.LogLevel') + + +`message` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/Debug.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Debug Class +Functionality for debugging +```csharp +public static class Debug +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Debug +### Methods +- [AddOnScreenMessage(int, float, System.Drawing.Color, string)](./Debug-AddOnScreenMessage(int_float_Color_string).md 'UnrealEngine.Framework.Debug.AddOnScreenMessage(int, float, System.Drawing.Color, string)') +- [ClearOnScreenMessages()](./Debug-ClearOnScreenMessages().md 'UnrealEngine.Framework.Debug.ClearOnScreenMessages()') +- [DrawBox(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, System.Drawing.Color, bool, float, byte, float)](./Debug-DrawBox(Vector3_Vector3_Quaternion_Color_bool_float_byte_float).md 'UnrealEngine.Framework.Debug.DrawBox(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, System.Drawing.Color, bool, float, byte, float)') +- [DrawCapsule(System.Numerics.Vector3, float, float, System.Numerics.Quaternion, System.Drawing.Color, bool, float, byte, float)](./Debug-DrawCapsule(Vector3_float_float_Quaternion_Color_bool_float_byte_float).md 'UnrealEngine.Framework.Debug.DrawCapsule(System.Numerics.Vector3, float, float, System.Numerics.Quaternion, System.Drawing.Color, bool, float, byte, float)') +- [DrawCone(System.Numerics.Vector3, System.Numerics.Vector3, float, float, float, int, System.Drawing.Color, bool, float, byte, float)](./Debug-DrawCone(Vector3_Vector3_float_float_float_int_Color_bool_float_byte_float).md 'UnrealEngine.Framework.Debug.DrawCone(System.Numerics.Vector3, System.Numerics.Vector3, float, float, float, int, System.Drawing.Color, bool, float, byte, float)') +- [DrawCylinder(System.Numerics.Vector3, System.Numerics.Vector3, float, int, System.Drawing.Color, bool, float, byte, float)](./Debug-DrawCylinder(Vector3_Vector3_float_int_Color_bool_float_byte_float).md 'UnrealEngine.Framework.Debug.DrawCylinder(System.Numerics.Vector3, System.Numerics.Vector3, float, int, System.Drawing.Color, bool, float, byte, float)') +- [DrawLine(System.Numerics.Vector3, System.Numerics.Vector3, System.Drawing.Color, bool, float, byte, float)](./Debug-DrawLine(Vector3_Vector3_Color_bool_float_byte_float).md 'UnrealEngine.Framework.Debug.DrawLine(System.Numerics.Vector3, System.Numerics.Vector3, System.Drawing.Color, bool, float, byte, float)') +- [DrawPoint(System.Numerics.Vector3, float, System.Drawing.Color, bool, float, byte)](./Debug-DrawPoint(Vector3_float_Color_bool_float_byte).md 'UnrealEngine.Framework.Debug.DrawPoint(System.Numerics.Vector3, float, System.Drawing.Color, bool, float, byte)') +- [DrawSphere(System.Numerics.Vector3, float, int, System.Drawing.Color, bool, float, byte, float)](./Debug-DrawSphere(Vector3_float_int_Color_bool_float_byte_float).md 'UnrealEngine.Framework.Debug.DrawSphere(System.Numerics.Vector3, float, int, System.Drawing.Color, bool, float, byte, float)') +- [Exception(System.Exception)](./Debug-Exception(Exception).md 'UnrealEngine.Framework.Debug.Exception(System.Exception)') +- [FlushPersistentLines()](./Debug-FlushPersistentLines().md 'UnrealEngine.Framework.Debug.FlushPersistentLines()') +- [Log(UnrealEngine.Framework.LogLevel, string)](./Debug-Log(LogLevel_string).md 'UnrealEngine.Framework.Debug.Log(UnrealEngine.Framework.LogLevel, string)') + +``` + +`API/DetachmentTransformRule.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## DetachmentTransformRule Enum +Defines rules for detaching components +```csharp +public enum DetachmentTransformRule +``` +### Fields + +`KeepRelativeTransform` 0 +Keeps current relative transform as the relative transform to the previous parent + + +`KeepWorldTransform` 1 +Automatically calculates the relative transform such that the detached component maintains the same world transform + + +``` + +`API/DirectionalLight-DirectionalLight(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[DirectionalLight](./DirectionalLight.md 'UnrealEngine.Framework.DirectionalLight') +## DirectionalLight(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public DirectionalLight(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/DirectionalLight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## DirectionalLight Class +Simulates light that is being emitted from a source that is infinitely far away +```csharp +public class DirectionalLight : UnrealEngine.Framework.Light +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 [Light](./Light.md 'UnrealEngine.Framework.Light') 🡒 DirectionalLight +### Constructors +- [DirectionalLight(string, UnrealEngine.Framework.Blueprint)](./DirectionalLight-DirectionalLight(string_Blueprint).md 'UnrealEngine.Framework.DirectionalLight.DirectionalLight(string, UnrealEngine.Framework.Blueprint)') + +``` + +`API/DirectionalLightComponent-DirectionalLightComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[DirectionalLightComponent](./DirectionalLightComponent.md 'UnrealEngine.Framework.DirectionalLightComponent') +## DirectionalLightComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public DirectionalLightComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/DirectionalLightComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## DirectionalLightComponent Class +A component that represents directional light +```csharp +public class DirectionalLightComponent : UnrealEngine.Framework.LightComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [LightComponentBase](./LightComponentBase.md 'UnrealEngine.Framework.LightComponentBase') 🡒 [LightComponent](./LightComponent.md 'UnrealEngine.Framework.LightComponent') 🡒 DirectionalLightComponent +### Constructors +- [DirectionalLightComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./DirectionalLightComponent-DirectionalLightComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.DirectionalLightComponent.DirectionalLightComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') + +``` + +`API/Engine-AddActionMapping(string_string_bool_bool_bool_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.AddActionMapping(string, string, bool, bool, bool, bool) Method +Adds an engine defined action mapping, cannot be remapped +```csharp +public static void AddActionMapping(string actionName, string key, bool shift=false, bool ctrl=false, bool alt=false, bool cmd=false); +``` +#### Parameters + +`actionName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +Friendly name of action + + +`key` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +Key to bind in accordance with [Keys](./Keys.md 'UnrealEngine.Framework.Keys') + + +`shift` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if one of the Shift keys must be down when the KeyEvent is received to be acknowledged + + +`ctrl` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if one of the Ctrl keys must be down when the KeyEvent is received to be acknowledged + + +`alt` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if one of the Alt keys must be down when the KeyEvent is received to be acknowledged + + +`cmd` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if one of the Cmd keys must be down when the KeyEvent is received to be acknowledged + + +``` + +`API/Engine-AddAxisMapping(string_string_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.AddAxisMapping(string, string, float) Method +Adds an engine defined mapping between an axis and key, cannot be remapped +```csharp +public static void AddAxisMapping(string axisName, string key, float scale=1f); +``` +#### Parameters + +`axisName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +Friendly name of axis + + +`key` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +Key to bind in accordance with [Keys](./Keys.md 'UnrealEngine.Framework.Keys') + + +`scale` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Multiplier to use for the mapping when accumulating the axis value + + +``` + +`API/Engine-DelayGarbageCollection().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.DelayGarbageCollection() Method +Requests a one frame delay of garbage collection +```csharp +public static void DelayGarbageCollection(); +``` + +``` + +`API/Engine-ForceGarbageCollection(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.ForceGarbageCollection(bool) Method +Updates the timer between garbage collection such that at the next opportunity garbage collection will be run +```csharp +public static void ForceGarbageCollection(bool fullPurge=false); +``` +#### Parameters + +`fullPurge` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, all possible memory will be freed + + +``` + +`API/Engine-FrameNumber.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.FrameNumber Property +Incremented once per frame before the scene is being rendered +```csharp +public static uint FrameNumber { get; } +``` +#### Property Value +[System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') + +``` + +`API/Engine-GetScreenResolution().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.GetScreenResolution() Method +Returns the current resolution of the screen +```csharp +public static System.Numerics.Vector2 GetScreenResolution(); +``` +#### Returns +[System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + +``` + +`API/Engine-GetScreenResolution(Vector2).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.GetScreenResolution(System.Numerics.Vector2) Method +Retrieves the current resolution of the screen +```csharp +public static void GetScreenResolution(ref System.Numerics.Vector2 value); +``` +#### Parameters + +`value` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + + +``` + +`API/Engine-GetViewportSize().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.GetViewportSize() Method +Returns the current size of the viewport +```csharp +public static System.Numerics.Vector2 GetViewportSize(); +``` +#### Returns +[System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + +``` + +`API/Engine-GetViewportSize(Vector2).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.GetViewportSize(System.Numerics.Vector2) Method +Retrieves the current size of the viewport +```csharp +public static void GetViewportSize(ref System.Numerics.Vector2 value); +``` +#### Parameters + +`value` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + + +``` + +`API/Engine-IsEditor.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.IsEditor Property +Returns `true` if the script is executing within the editor +```csharp +public static bool IsEditor { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Engine-IsExitRequested.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.IsExitRequested Property +Returns `true` if the exit was requested +```csharp +public static bool IsExitRequested { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Engine-IsForegroundWindow.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.IsForegroundWindow Property +Returns `true` if the window has focus +```csharp +public static bool IsForegroundWindow { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Engine-IsSplitScreen.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.IsSplitScreen Property +Returns `true` if the game is running in split screen mode +```csharp +public static bool IsSplitScreen { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Engine-MaxFPS.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.MaxFPS Property +Gets or sets max frames per second, overrides console variable +```csharp +public static float MaxFPS { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Engine-NetMode.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.NetMode Property +Returns the current networking mode +```csharp +public static UnrealEngine.Framework.NetMode NetMode { get; } +``` +#### Property Value +[NetMode](./NetMode.md 'UnrealEngine.Framework.NetMode') +### Remarks +Networking functionality + +``` + +`API/Engine-SetTitle(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.SetTitle(string) Method +Sets the window title +```csharp +public static void SetTitle(string title); +``` +#### Parameters + +`title` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/Engine-Version.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.Version Property +Returns the current engine version +```csharp +public static string Version { get; } +``` +#### Property Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Engine-WindowMode.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Engine](./Engine.md 'UnrealEngine.Framework.Engine') +## Engine.WindowMode Property +Returns the current mode of the window +```csharp +public static UnrealEngine.Framework.WindowMode WindowMode { get; } +``` +#### Property Value +[WindowMode](./WindowMode.md 'UnrealEngine.Framework.WindowMode') + +``` + +`API/Engine.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Engine Class +Functionality for management of engine systems +```csharp +public static class Engine +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Engine +### Properties +- [FrameNumber](./Engine-FrameNumber.md 'UnrealEngine.Framework.Engine.FrameNumber') +- [IsEditor](./Engine-IsEditor.md 'UnrealEngine.Framework.Engine.IsEditor') +- [IsExitRequested](./Engine-IsExitRequested.md 'UnrealEngine.Framework.Engine.IsExitRequested') +- [IsForegroundWindow](./Engine-IsForegroundWindow.md 'UnrealEngine.Framework.Engine.IsForegroundWindow') +- [IsSplitScreen](./Engine-IsSplitScreen.md 'UnrealEngine.Framework.Engine.IsSplitScreen') +- [MaxFPS](./Engine-MaxFPS.md 'UnrealEngine.Framework.Engine.MaxFPS') +- [NetMode](./Engine-NetMode.md 'UnrealEngine.Framework.Engine.NetMode') +- [Version](./Engine-Version.md 'UnrealEngine.Framework.Engine.Version') +- [WindowMode](./Engine-WindowMode.md 'UnrealEngine.Framework.Engine.WindowMode') +### Methods +- [AddActionMapping(string, string, bool, bool, bool, bool)](./Engine-AddActionMapping(string_string_bool_bool_bool_bool).md 'UnrealEngine.Framework.Engine.AddActionMapping(string, string, bool, bool, bool, bool)') +- [AddAxisMapping(string, string, float)](./Engine-AddAxisMapping(string_string_float).md 'UnrealEngine.Framework.Engine.AddAxisMapping(string, string, float)') +- [DelayGarbageCollection()](./Engine-DelayGarbageCollection().md 'UnrealEngine.Framework.Engine.DelayGarbageCollection()') +- [ForceGarbageCollection(bool)](./Engine-ForceGarbageCollection(bool).md 'UnrealEngine.Framework.Engine.ForceGarbageCollection(bool)') +- [GetScreenResolution()](./Engine-GetScreenResolution().md 'UnrealEngine.Framework.Engine.GetScreenResolution()') +- [GetScreenResolution(System.Numerics.Vector2)](./Engine-GetScreenResolution(Vector2).md 'UnrealEngine.Framework.Engine.GetScreenResolution(System.Numerics.Vector2)') +- [GetViewportSize()](./Engine-GetViewportSize().md 'UnrealEngine.Framework.Engine.GetViewportSize()') +- [GetViewportSize(System.Numerics.Vector2)](./Engine-GetViewportSize(Vector2).md 'UnrealEngine.Framework.Engine.GetViewportSize(System.Numerics.Vector2)') +- [SetTitle(string)](./Engine-SetTitle(string).md 'UnrealEngine.Framework.Engine.SetTitle(string)') + +``` + +`API/Font-Equals(Font).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Font](./Font.md 'UnrealEngine.Framework.Font') +## Font.Equals(UnrealEngine.Framework.Font) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.Font other); +``` +#### Parameters + +`other` [Font](./Font.md 'UnrealEngine.Framework.Font') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Font-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Font](./Font.md 'UnrealEngine.Framework.Font') +## Font.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/Font-GetStringSize(string_int_int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Font](./Font.md 'UnrealEngine.Framework.Font') +## Font.GetStringSize(string, int, int) Method +Retrieves height and width for a string +```csharp +public void GetStringSize(string text, ref int height, ref int width); +``` +#### Parameters + +`text` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`height` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`width` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +``` + +`API/Font-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Font](./Font.md 'UnrealEngine.Framework.Font') +## Font.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Font-Load(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Font](./Font.md 'UnrealEngine.Framework.Font') +## Font.Load(string) Method +Finds and loads a font by name +```csharp +public static UnrealEngine.Framework.Font Load(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[Font](./Font.md 'UnrealEngine.Framework.Font') +A font or `null` on failure + +``` + +`API/Font.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Font Class +A font object that is used to draw text +```csharp +public class Font : +System.IEquatable +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Font + +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[Font](./Font.md 'UnrealEngine.Framework.Font')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [IsCreated](./Font-IsCreated.md 'UnrealEngine.Framework.Font.IsCreated') +### Methods +- [Equals(UnrealEngine.Framework.Font)](./Font-Equals(Font).md 'UnrealEngine.Framework.Font.Equals(UnrealEngine.Framework.Font)') +- [GetHashCode()](./Font-GetHashCode().md 'UnrealEngine.Framework.Font.GetHashCode()') +- [GetStringSize(string, int, int)](./Font-GetStringSize(string_int_int).md 'UnrealEngine.Framework.Font.GetStringSize(string, int, int)') +- [Load(string)](./Font-Load(string).md 'UnrealEngine.Framework.Font.Load(string)') + +``` + +`API/GameModeBase-SwapPlayerControllers(PlayerController_PlayerController).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[GameModeBase](./GameModeBase.md 'UnrealEngine.Framework.GameModeBase') +## GameModeBase.SwapPlayerControllers(UnrealEngine.Framework.PlayerController, UnrealEngine.Framework.PlayerController) Method +Swaps player controllers +```csharp +public void SwapPlayerControllers(UnrealEngine.Framework.PlayerController playerController, UnrealEngine.Framework.PlayerController newPlayerController); +``` +#### Parameters + +`playerController` [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') + + +`newPlayerController` [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') + + +``` + +`API/GameModeBase-UseSeamlessTravel.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[GameModeBase](./GameModeBase.md 'UnrealEngine.Framework.GameModeBase') +## GameModeBase.UseSeamlessTravel Property +Gets or sets whether the game perform seamless map travels which loads in the background and doesn't disconnect clients +```csharp +public bool UseSeamlessTravel { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/GameModeBase.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## GameModeBase Class +Defines the game being played, instantiated only on the server +```csharp +public class GameModeBase : UnrealEngine.Framework.Actor +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 GameModeBase +### Properties +- [UseSeamlessTravel](./GameModeBase-UseSeamlessTravel.md 'UnrealEngine.Framework.GameModeBase.UseSeamlessTravel') +### Methods +- [SwapPlayerControllers(UnrealEngine.Framework.PlayerController, UnrealEngine.Framework.PlayerController)](./GameModeBase-SwapPlayerControllers(PlayerController_PlayerController).md 'UnrealEngine.Framework.GameModeBase.SwapPlayerControllers(UnrealEngine.Framework.PlayerController, UnrealEngine.Framework.PlayerController)') + +``` + +`API/HeadMountedDisplay-DeviceName.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[HeadMountedDisplay](./HeadMountedDisplay.md 'UnrealEngine.Framework.HeadMountedDisplay') +## HeadMountedDisplay.DeviceName Property +Returns the name of the device +```csharp +public static string DeviceName { get; } +``` +#### Property Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/HeadMountedDisplay-Enabled.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[HeadMountedDisplay](./HeadMountedDisplay.md 'UnrealEngine.Framework.HeadMountedDisplay') +## HeadMountedDisplay.Enabled Property +Gets or sets whether the head-mounted display is enabled +```csharp +public static bool Enabled { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/HeadMountedDisplay-IsConnected.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[HeadMountedDisplay](./HeadMountedDisplay.md 'UnrealEngine.Framework.HeadMountedDisplay') +## HeadMountedDisplay.IsConnected Property +Returns `true` if the head-mounted display is connected and ready to use +```csharp +public static bool IsConnected { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/HeadMountedDisplay-LowPersistenceMode.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[HeadMountedDisplay](./HeadMountedDisplay.md 'UnrealEngine.Framework.HeadMountedDisplay') +## HeadMountedDisplay.LowPersistenceMode Property +Gets or sets whether the head-mounted display is in low or full persistence mode +```csharp +public static bool LowPersistenceMode { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/HeadMountedDisplay.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## HeadMountedDisplay Class +Functionality for access to the head-mounted display +```csharp +public static class HeadMountedDisplay +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 HeadMountedDisplay +### Properties +- [DeviceName](./HeadMountedDisplay-DeviceName.md 'UnrealEngine.Framework.HeadMountedDisplay.DeviceName') +- [Enabled](./HeadMountedDisplay-Enabled.md 'UnrealEngine.Framework.HeadMountedDisplay.Enabled') +- [IsConnected](./HeadMountedDisplay-IsConnected.md 'UnrealEngine.Framework.HeadMountedDisplay.IsConnected') +- [LowPersistenceMode](./HeadMountedDisplay-LowPersistenceMode.md 'UnrealEngine.Framework.HeadMountedDisplay.LowPersistenceMode') + +``` + +`API/HierarchicalInstancedStaticMeshComponent-DisableCollision.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[HierarchicalInstancedStaticMeshComponent](./HierarchicalInstancedStaticMeshComponent.md 'UnrealEngine.Framework.HierarchicalInstancedStaticMeshComponent') +## HierarchicalInstancedStaticMeshComponent.DisableCollision Property +Gets or sets whether the collision should be disabled +```csharp +public bool DisableCollision { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/HierarchicalInstancedStaticMeshComponent-HierarchicalInstancedStaticMeshComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[HierarchicalInstancedStaticMeshComponent](./HierarchicalInstancedStaticMeshComponent.md 'UnrealEngine.Framework.HierarchicalInstancedStaticMeshComponent') +## HierarchicalInstancedStaticMeshComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public HierarchicalInstancedStaticMeshComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/HierarchicalInstancedStaticMeshComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## HierarchicalInstancedStaticMeshComponent Class +A component that efficiently renders multiple instances of the same [StaticMeshComponent](./StaticMeshComponent.md 'UnrealEngine.Framework.StaticMeshComponent') with different level of detail +```csharp +public class HierarchicalInstancedStaticMeshComponent : UnrealEngine.Framework.InstancedStaticMeshComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') 🡒 [MeshComponent](./MeshComponent.md 'UnrealEngine.Framework.MeshComponent') 🡒 [StaticMeshComponent](./StaticMeshComponent.md 'UnrealEngine.Framework.StaticMeshComponent') 🡒 [InstancedStaticMeshComponent](./InstancedStaticMeshComponent.md 'UnrealEngine.Framework.InstancedStaticMeshComponent') 🡒 HierarchicalInstancedStaticMeshComponent +### Constructors +- [HierarchicalInstancedStaticMeshComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./HierarchicalInstancedStaticMeshComponent-HierarchicalInstancedStaticMeshComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.HierarchicalInstancedStaticMeshComponent.HierarchicalInstancedStaticMeshComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Properties +- [DisableCollision](./HierarchicalInstancedStaticMeshComponent-DisableCollision.md 'UnrealEngine.Framework.HierarchicalInstancedStaticMeshComponent.DisableCollision') + +``` + +`API/Hit-BlockingHit.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.BlockingHit Property +Returns `true` if the hit was a result of blocking collision +```csharp +public bool BlockingHit { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Hit-Distance.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.Distance Property +Returns the distance from [TraceStart](./Hit-TraceStart.md 'UnrealEngine.Framework.Hit.TraceStart') to [Location](./Hit-Location.md 'UnrealEngine.Framework.Hit.Location') in world space +```csharp +public float Distance { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Hit-Equals(Hit).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.Equals(UnrealEngine.Framework.Hit) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.Hit other); +``` +#### Parameters + +`other` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Hit-Equals(object).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.Equals(object) Method +Indicates equality of objects +```csharp +public override bool Equals(object value); +``` +#### Parameters + +`value` [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Hit-GetActor().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.GetActor() Method +Returns the owner actor of the component that was hit or `null` on failure +```csharp +public UnrealEngine.Framework.Actor GetActor(); +``` +#### Returns +[Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +``` + +`API/Hit-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/Hit-ImpactLocation.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.ImpactLocation Property +Returns the location in world space of the actual contact of the trace shape with the impacted object +```csharp +public System.Numerics.Vector3 ImpactLocation { get; } +``` +#### Property Value +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Hit-ImpactNormal.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.ImpactNormal Property +Returns the normal of the hit in world space for the object that was hit by the sweep +```csharp +public System.Numerics.Vector3 ImpactNormal { get; } +``` +#### Property Value +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Hit-Location.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.Location Property +Returns the location in world space where the moving shape would end up against the impacted object if there was a hit +```csharp +public System.Numerics.Vector3 Location { get; } +``` +#### Property Value +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Hit-Normal.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.Normal Property +Returns the normal of the hit in world space for the object that was swept +```csharp +public System.Numerics.Vector3 Normal { get; } +``` +#### Property Value +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Hit-PenetrationDepth.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.PenetrationDepth Property +Returns the distance along with [Normal](./Hit-Normal.md 'UnrealEngine.Framework.Hit.Normal') that will result in moving out of penetration if [StartPenetrating](./Hit-StartPenetrating.md 'UnrealEngine.Framework.Hit.StartPenetrating') is `true` and a penetration vector can be computed +```csharp +public float PenetrationDepth { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Hit-StartPenetrating.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.StartPenetrating Property +Returns `true` if the trace started penetration +```csharp +public bool StartPenetrating { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Hit-Time.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.Time Property +Returns the impact along trace direction between 0.0f and 1.0f if there was a hit, indicating time between [TraceStart](./Hit-TraceStart.md 'UnrealEngine.Framework.Hit.TraceStart') and [TraceEnd](./Hit-TraceEnd.md 'UnrealEngine.Framework.Hit.TraceEnd') +```csharp +public float Time { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Hit-ToString().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.ToString() Method +Returns a string that represents this instance +```csharp +public override string ToString(); +``` +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Hit-ToString(IFormatProvider).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.ToString(System.IFormatProvider) Method +Returns a string that represents this instance +```csharp +public string ToString(System.IFormatProvider formatProvider); +``` +#### Parameters + +`formatProvider` [System.IFormatProvider](https://docs.microsoft.com/en-us/dotnet/api/System.IFormatProvider 'System.IFormatProvider') + +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Hit-TraceEnd.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.TraceEnd Property +Returns the end location of the trace +```csharp +public System.Numerics.Vector3 TraceEnd { get; } +``` +#### Property Value +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Hit-TraceStart.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.TraceStart Property +Returns the start location of the trace +```csharp +public System.Numerics.Vector3 TraceStart { get; } +``` +#### Property Value +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Hit-op_Equality(Hit_Hit).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.operator ==(UnrealEngine.Framework.Hit, UnrealEngine.Framework.Hit) Operator +Tests for equality between two objects +```csharp +public static bool operator ==(UnrealEngine.Framework.Hit left, UnrealEngine.Framework.Hit right); +``` +#### Parameters + +`left` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +`right` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Hit-op_Implicitstring(Hit).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.implicit operator string(UnrealEngine.Framework.Hit) Operator +Implicitly casts this instance to a string +```csharp +public static string implicit operator string(UnrealEngine.Framework.Hit value); +``` +#### Parameters + +`value` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Hit-op_Inequality(Hit_Hit).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Hit](./Hit.md 'UnrealEngine.Framework.Hit') +## Hit.operator !=(UnrealEngine.Framework.Hit, UnrealEngine.Framework.Hit) Operator +Tests for inequality between two objects +```csharp +public static bool operator !=(UnrealEngine.Framework.Hit left, UnrealEngine.Framework.Hit right); +``` +#### Parameters + +`left` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +`right` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Hit.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Hit Struct +A trace hit +```csharp +public struct Hit : +System.IEquatable +``` +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[Hit](./Hit.md 'UnrealEngine.Framework.Hit')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [BlockingHit](./Hit-BlockingHit.md 'UnrealEngine.Framework.Hit.BlockingHit') +- [Distance](./Hit-Distance.md 'UnrealEngine.Framework.Hit.Distance') +- [ImpactLocation](./Hit-ImpactLocation.md 'UnrealEngine.Framework.Hit.ImpactLocation') +- [ImpactNormal](./Hit-ImpactNormal.md 'UnrealEngine.Framework.Hit.ImpactNormal') +- [Location](./Hit-Location.md 'UnrealEngine.Framework.Hit.Location') +- [Normal](./Hit-Normal.md 'UnrealEngine.Framework.Hit.Normal') +- [PenetrationDepth](./Hit-PenetrationDepth.md 'UnrealEngine.Framework.Hit.PenetrationDepth') +- [StartPenetrating](./Hit-StartPenetrating.md 'UnrealEngine.Framework.Hit.StartPenetrating') +- [Time](./Hit-Time.md 'UnrealEngine.Framework.Hit.Time') +- [TraceEnd](./Hit-TraceEnd.md 'UnrealEngine.Framework.Hit.TraceEnd') +- [TraceStart](./Hit-TraceStart.md 'UnrealEngine.Framework.Hit.TraceStart') +### Methods +- [Equals(object)](./Hit-Equals(object).md 'UnrealEngine.Framework.Hit.Equals(object)') +- [Equals(UnrealEngine.Framework.Hit)](./Hit-Equals(Hit).md 'UnrealEngine.Framework.Hit.Equals(UnrealEngine.Framework.Hit)') +- [GetActor()](./Hit-GetActor().md 'UnrealEngine.Framework.Hit.GetActor()') +- [GetHashCode()](./Hit-GetHashCode().md 'UnrealEngine.Framework.Hit.GetHashCode()') +- [ToString()](./Hit-ToString().md 'UnrealEngine.Framework.Hit.ToString()') +- [ToString(System.IFormatProvider)](./Hit-ToString(IFormatProvider).md 'UnrealEngine.Framework.Hit.ToString(System.IFormatProvider)') +### Operators +- [operator ==(UnrealEngine.Framework.Hit, UnrealEngine.Framework.Hit)](./Hit-op_Equality(Hit_Hit).md 'UnrealEngine.Framework.Hit.op_Equality(UnrealEngine.Framework.Hit, UnrealEngine.Framework.Hit)') +- [implicit operator string(UnrealEngine.Framework.Hit)](./Hit-op_Implicitstring(Hit).md 'UnrealEngine.Framework.Hit.op_Implicit string(UnrealEngine.Framework.Hit)') +- [operator !=(UnrealEngine.Framework.Hit, UnrealEngine.Framework.Hit)](./Hit-op_Inequality(Hit_Hit).md 'UnrealEngine.Framework.Hit.op_Inequality(UnrealEngine.Framework.Hit, UnrealEngine.Framework.Hit)') + +``` + +`API/HorizontalTextAligment.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## HorizontalTextAligment Enum +Defines the horizontal text aligment type +```csharp +public enum HorizontalTextAligment +``` +### Fields + +`Left` 0 + + + +`Center` 1 + + + +`Right` 2 + + + +``` + +`API/InputAxisDelegate(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## InputAxisDelegate(float) Delegate +Delegate for axis events +```csharp +public delegate void InputAxisDelegate(float axisValue); +``` +#### Parameters + +`axisValue` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/InputComponent-ActionBindingsNumber.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InputComponent](./InputComponent.md 'UnrealEngine.Framework.InputComponent') +## InputComponent.ActionBindingsNumber Property +Returns the number of action bindings +```csharp +public int ActionBindingsNumber { get; } +``` +#### Property Value +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/InputComponent-BindAction(string_InputEvent_InputDelegate_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InputComponent](./InputComponent.md 'UnrealEngine.Framework.InputComponent') +## InputComponent.BindAction(string, UnrealEngine.Framework.InputEvent, UnrealEngine.Framework.InputDelegate, bool) Method +Binds the callback function to an action defined in the project settings, or by using [AddActionMapping(string, string, bool, bool, bool, bool)](./Engine-AddActionMapping(string_string_bool_bool_bool_bool).md 'UnrealEngine.Framework.Engine.AddActionMapping(string, string, bool, bool, bool, bool)') and [AddActionMapping(string, string, bool, bool, bool, bool)](./PlayerInput-AddActionMapping(string_string_bool_bool_bool_bool).md 'UnrealEngine.Framework.PlayerInput.AddActionMapping(string, string, bool, bool, bool, bool)') +```csharp +public void BindAction(string actionName, UnrealEngine.Framework.InputEvent keyEvent, UnrealEngine.Framework.InputDelegate callback, bool executedWhenPaused=false); +``` +#### Parameters + +`actionName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the action + + +`keyEvent` [InputEvent](./InputEvent.md 'UnrealEngine.Framework.InputEvent') +The type of input behavior + + +`callback` [InputDelegate()](./InputDelegate().md 'UnrealEngine.Framework.InputDelegate()') +The function to call when the input is triggered + + +`executedWhenPaused` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, executes even if the game is paused + + +``` + +`API/InputComponent-BindAxis(string_InputAxisDelegate_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InputComponent](./InputComponent.md 'UnrealEngine.Framework.InputComponent') +## InputComponent.BindAxis(string, UnrealEngine.Framework.InputAxisDelegate, bool) Method +Binds the callback function to an axis defined in the project settings, or by using [AddAxisMapping(string, string, float)](./Engine-AddAxisMapping(string_string_float).md 'UnrealEngine.Framework.Engine.AddAxisMapping(string, string, float)') and [AddAxisMapping(string, string, float)](./PlayerInput-AddAxisMapping(string_string_float).md 'UnrealEngine.Framework.PlayerInput.AddAxisMapping(string, string, float)') +```csharp +public void BindAxis(string axisName, UnrealEngine.Framework.InputAxisDelegate callback, bool executedWhenPaused=false); +``` +#### Parameters + +`axisName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the axis + + +`callback` [InputAxisDelegate(float)](./InputAxisDelegate(float).md 'UnrealEngine.Framework.InputAxisDelegate(float)') +The function to call while tracking axis + + +`executedWhenPaused` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, executes even if the game is paused + + +``` + +`API/InputComponent-BlockInput.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InputComponent](./InputComponent.md 'UnrealEngine.Framework.InputComponent') +## InputComponent.BlockInput Property +Gets or sets whether any components lower on the input stack should be allowed to receive input +```csharp +public bool BlockInput { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/InputComponent-ClearActionBindings().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InputComponent](./InputComponent.md 'UnrealEngine.Framework.InputComponent') +## InputComponent.ClearActionBindings() Method +Removes all action bindings +```csharp +public void ClearActionBindings(); +``` + +``` + +`API/InputComponent-HasBindings.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InputComponent](./InputComponent.md 'UnrealEngine.Framework.InputComponent') +## InputComponent.HasBindings Property +Indicates whether the component has any input bindings +```csharp +public bool HasBindings { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/InputComponent-Priority.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InputComponent](./InputComponent.md 'UnrealEngine.Framework.InputComponent') +## InputComponent.Priority Property +Gets or sets the priority of the input component when pushed in to the stack +```csharp +public int Priority { get; set; } +``` +#### Property Value +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/InputComponent-RemoveActionBinding(string_InputEvent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InputComponent](./InputComponent.md 'UnrealEngine.Framework.InputComponent') +## InputComponent.RemoveActionBinding(string, UnrealEngine.Framework.InputEvent) Method +Removes the action binding +```csharp +public void RemoveActionBinding(string actionName, UnrealEngine.Framework.InputEvent keyEvent); +``` +#### Parameters + +`actionName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`keyEvent` [InputEvent](./InputEvent.md 'UnrealEngine.Framework.InputEvent') + + +``` + +`API/InputComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## InputComponent Class +An input component is a transient component that enables an actor to bind various forms of input events to delegate functions +```csharp +public class InputComponent : UnrealEngine.Framework.ActorComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 InputComponent +### Properties +- [ActionBindingsNumber](./InputComponent-ActionBindingsNumber.md 'UnrealEngine.Framework.InputComponent.ActionBindingsNumber') +- [BlockInput](./InputComponent-BlockInput.md 'UnrealEngine.Framework.InputComponent.BlockInput') +- [HasBindings](./InputComponent-HasBindings.md 'UnrealEngine.Framework.InputComponent.HasBindings') +- [Priority](./InputComponent-Priority.md 'UnrealEngine.Framework.InputComponent.Priority') +### Methods +- [BindAction(string, UnrealEngine.Framework.InputEvent, UnrealEngine.Framework.InputDelegate, bool)](./InputComponent-BindAction(string_InputEvent_InputDelegate_bool).md 'UnrealEngine.Framework.InputComponent.BindAction(string, UnrealEngine.Framework.InputEvent, UnrealEngine.Framework.InputDelegate, bool)') +- [BindAxis(string, UnrealEngine.Framework.InputAxisDelegate, bool)](./InputComponent-BindAxis(string_InputAxisDelegate_bool).md 'UnrealEngine.Framework.InputComponent.BindAxis(string, UnrealEngine.Framework.InputAxisDelegate, bool)') +- [ClearActionBindings()](./InputComponent-ClearActionBindings().md 'UnrealEngine.Framework.InputComponent.ClearActionBindings()') +- [RemoveActionBinding(string, UnrealEngine.Framework.InputEvent)](./InputComponent-RemoveActionBinding(string_InputEvent).md 'UnrealEngine.Framework.InputComponent.RemoveActionBinding(string, UnrealEngine.Framework.InputEvent)') + +``` + +`API/InputDelegate().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## InputDelegate() Delegate +Delegate for action events +```csharp +public delegate void InputDelegate(); +``` + +``` + +`API/InputEvent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## InputEvent Enum +Defines input behavior type +```csharp +public enum InputEvent +``` +### Fields + +`Pressed` 0 +Key pressed + + +`Released` 1 +Key released + + +`Repeat` 2 +Key repeating + + +`DoubleClick` 3 +Key double clicked + + +`Axis` 4 +Axis activated + + +``` + +`API/InstancedStaticMeshComponent-AddInstance(Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InstancedStaticMeshComponent](./InstancedStaticMeshComponent.md 'UnrealEngine.Framework.InstancedStaticMeshComponent') +## InstancedStaticMeshComponent.AddInstance(UnrealEngine.Framework.Transform) Method +Adds an instance to the component using the transform that will be applied at instantiation +```csharp +public void AddInstance(in UnrealEngine.Framework.Transform instanceTransform); +``` +#### Parameters + +`instanceTransform` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +``` + +`API/InstancedStaticMeshComponent-AddInstances(Transform--).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InstancedStaticMeshComponent](./InstancedStaticMeshComponent.md 'UnrealEngine.Framework.InstancedStaticMeshComponent') +## InstancedStaticMeshComponent.AddInstances(UnrealEngine.Framework.Transform[]) Method +Adds multiple instances to the component using the transforms that will be applied at instantiation +```csharp +public void AddInstances(UnrealEngine.Framework.Transform[] instanceTransforms); +``` +#### Parameters + +`instanceTransforms` [Transform](./Transform.md 'UnrealEngine.Framework.Transform')[[]](https://docs.microsoft.com/en-us/dotnet/api/System.Array 'System.Array') + + +``` + +`API/InstancedStaticMeshComponent-BatchUpdateInstanceTransforms(int_Transform--_bool_bool_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InstancedStaticMeshComponent](./InstancedStaticMeshComponent.md 'UnrealEngine.Framework.InstancedStaticMeshComponent') +## InstancedStaticMeshComponent.BatchUpdateInstanceTransforms(int, UnrealEngine.Framework.Transform[], bool, bool, bool) Method +Updates the transform for an array of instances +```csharp +public bool BatchUpdateInstanceTransforms(int startInstanceIndex, UnrealEngine.Framework.Transform[] instanceTransforms, bool worldSpace=false, bool markRenderStateDirty=false, bool teleport=false); +``` +#### Parameters + +`startInstanceIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') +The starting index of the instances to update + + +`instanceTransforms` [Transform](./Transform.md 'UnrealEngine.Framework.Transform')[[]](https://docs.microsoft.com/en-us/dotnet/api/System.Array 'System.Array') +The new transforms to apply + + +`worldSpace` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, the new transforms are interpreted as a world space transforms, otherwise it is interpreted as local space + + +`markRenderStateDirty` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If the render state is marked as dirty the change should be visible immediately + + +`teleport` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +Whether the instances physics should be moved normally, or teleported (moved instantly, ignoring velocity) + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if successful + +``` + +`API/InstancedStaticMeshComponent-ClearInstances().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InstancedStaticMeshComponent](./InstancedStaticMeshComponent.md 'UnrealEngine.Framework.InstancedStaticMeshComponent') +## InstancedStaticMeshComponent.ClearInstances() Method +Clears all instances being rendered by the component +```csharp +public void ClearInstances(); +``` + +``` + +`API/InstancedStaticMeshComponent-GetInstanceTransform(int_Transform_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InstancedStaticMeshComponent](./InstancedStaticMeshComponent.md 'UnrealEngine.Framework.InstancedStaticMeshComponent') +## InstancedStaticMeshComponent.GetInstanceTransform(int, UnrealEngine.Framework.Transform, bool) Method +Retrieves the transform of the specified instance +```csharp +public bool GetInstanceTransform(int instanceIndex, ref UnrealEngine.Framework.Transform value, bool worldSpace=false); +``` +#### Parameters + +`instanceIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`value` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +`worldSpace` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/InstancedStaticMeshComponent-InstanceCount.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InstancedStaticMeshComponent](./InstancedStaticMeshComponent.md 'UnrealEngine.Framework.InstancedStaticMeshComponent') +## InstancedStaticMeshComponent.InstanceCount Property +Returns the number of instances in the component +```csharp +public int InstanceCount { get; } +``` +#### Property Value +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/InstancedStaticMeshComponent-InstancedStaticMeshComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InstancedStaticMeshComponent](./InstancedStaticMeshComponent.md 'UnrealEngine.Framework.InstancedStaticMeshComponent') +## InstancedStaticMeshComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public InstancedStaticMeshComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/InstancedStaticMeshComponent-RemoveInstance(int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InstancedStaticMeshComponent](./InstancedStaticMeshComponent.md 'UnrealEngine.Framework.InstancedStaticMeshComponent') +## InstancedStaticMeshComponent.RemoveInstance(int) Method +Removes the specified instance +```csharp +public bool RemoveInstance(int instanceIndex); +``` +#### Parameters + +`instanceIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/InstancedStaticMeshComponent-UpdateInstanceTransform(int_Transform_bool_bool_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[InstancedStaticMeshComponent](./InstancedStaticMeshComponent.md 'UnrealEngine.Framework.InstancedStaticMeshComponent') +## InstancedStaticMeshComponent.UpdateInstanceTransform(int, UnrealEngine.Framework.Transform, bool, bool, bool) Method +Updates the transform for the specified instance +```csharp +public bool UpdateInstanceTransform(int instanceIndex, in UnrealEngine.Framework.Transform instanceTransform, bool worldSpace=false, bool markRenderStateDirty=false, bool teleport=false); +``` +#### Parameters + +`instanceIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') +The index of the instance to update + + +`instanceTransform` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') +The new transform to apply + + +`worldSpace` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, the new transform is interpreted as a world space transform, otherwise it is interpreted as local space + + +`markRenderStateDirty` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If the render state is marked as dirty the change should be visible immediately, consider setting it to `true` only during the update of the last instance in a batch + + +`teleport` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +Whether the instance's physics should be moved normally, or teleported (moved instantly, ignoring velocity) + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if successful + +``` + +`API/InstancedStaticMeshComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## InstancedStaticMeshComponent Class +A component that efficiently renders multiple instances of the same [StaticMeshComponent](./StaticMeshComponent.md 'UnrealEngine.Framework.StaticMeshComponent') +```csharp +public class InstancedStaticMeshComponent : UnrealEngine.Framework.StaticMeshComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') 🡒 [MeshComponent](./MeshComponent.md 'UnrealEngine.Framework.MeshComponent') 🡒 [StaticMeshComponent](./StaticMeshComponent.md 'UnrealEngine.Framework.StaticMeshComponent') 🡒 InstancedStaticMeshComponent + +Derived +↳ [HierarchicalInstancedStaticMeshComponent](./HierarchicalInstancedStaticMeshComponent.md 'UnrealEngine.Framework.HierarchicalInstancedStaticMeshComponent') +### Constructors +- [InstancedStaticMeshComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./InstancedStaticMeshComponent-InstancedStaticMeshComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.InstancedStaticMeshComponent.InstancedStaticMeshComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Properties +- [InstanceCount](./InstancedStaticMeshComponent-InstanceCount.md 'UnrealEngine.Framework.InstancedStaticMeshComponent.InstanceCount') +### Methods +- [AddInstance(UnrealEngine.Framework.Transform)](./InstancedStaticMeshComponent-AddInstance(Transform).md 'UnrealEngine.Framework.InstancedStaticMeshComponent.AddInstance(UnrealEngine.Framework.Transform)') +- [AddInstances(UnrealEngine.Framework.Transform[])](./InstancedStaticMeshComponent-AddInstances(Transform--).md 'UnrealEngine.Framework.InstancedStaticMeshComponent.AddInstances(UnrealEngine.Framework.Transform[])') +- [BatchUpdateInstanceTransforms(int, UnrealEngine.Framework.Transform[], bool, bool, bool)](./InstancedStaticMeshComponent-BatchUpdateInstanceTransforms(int_Transform--_bool_bool_bool).md 'UnrealEngine.Framework.InstancedStaticMeshComponent.BatchUpdateInstanceTransforms(int, UnrealEngine.Framework.Transform[], bool, bool, bool)') +- [ClearInstances()](./InstancedStaticMeshComponent-ClearInstances().md 'UnrealEngine.Framework.InstancedStaticMeshComponent.ClearInstances()') +- [GetInstanceTransform(int, UnrealEngine.Framework.Transform, bool)](./InstancedStaticMeshComponent-GetInstanceTransform(int_Transform_bool).md 'UnrealEngine.Framework.InstancedStaticMeshComponent.GetInstanceTransform(int, UnrealEngine.Framework.Transform, bool)') +- [RemoveInstance(int)](./InstancedStaticMeshComponent-RemoveInstance(int).md 'UnrealEngine.Framework.InstancedStaticMeshComponent.RemoveInstance(int)') +- [UpdateInstanceTransform(int, UnrealEngine.Framework.Transform, bool, bool, bool)](./InstancedStaticMeshComponent-UpdateInstanceTransform(int_Transform_bool_bool_bool).md 'UnrealEngine.Framework.InstancedStaticMeshComponent.UpdateInstanceTransform(int, UnrealEngine.Framework.Transform, bool, bool, bool)') + +``` + +`API/Keys-A.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.A Field + +```csharp +public const string A = A; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Add.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Add Field + +```csharp +public const string Add = Add; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Ampersand.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Ampersand Field + +```csharp +public const string Ampersand = Ampersand; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Android-Back.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Android](./Keys-Android.md 'UnrealEngine.Framework.Keys.Android') +## Keys.Android.Back Field + +```csharp +public const string Back = Android_Back; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Android-Menu.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Android](./Keys-Android.md 'UnrealEngine.Framework.Keys.Android') +## Keys.Android.Menu Field + +```csharp +public const string Menu = Android_Menu; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Android-VolumeDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Android](./Keys-Android.md 'UnrealEngine.Framework.Keys.Android') +## Keys.Android.VolumeDown Field + +```csharp +public const string VolumeDown = Android_Volume_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Android-VolumeUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Android](./Keys-Android.md 'UnrealEngine.Framework.Keys.Android') +## Keys.Android.VolumeUp Field + +```csharp +public const string VolumeUp = Android_Volume_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Android.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Android Class +Android +```csharp +public static class Keys.Android +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.Android +### Fields +- [Back](./Keys-Android-Back.md 'UnrealEngine.Framework.Keys.Android.Back') +- [Menu](./Keys-Android-Menu.md 'UnrealEngine.Framework.Keys.Android.Menu') +- [VolumeDown](./Keys-Android-VolumeDown.md 'UnrealEngine.Framework.Keys.Android.VolumeDown') +- [VolumeUp](./Keys-Android-VolumeUp.md 'UnrealEngine.Framework.Keys.Android.VolumeUp') + +``` + +`API/Keys-AnyKey.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.AnyKey Field + +```csharp +public const string AnyKey = AnyKey; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Apostrophe.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Apostrophe Field + +```csharp +public const string Apostrophe = Apostrophe; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Asterix.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Asterix Field + +```csharp +public const string Asterix = Asterix; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-B.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.B Field + +```csharp +public const string B = B; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-BackSpace.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.BackSpace Field + +```csharp +public const string BackSpace = BackSpace; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Backslash.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Backslash Field + +```csharp +public const string Backslash = Backslash; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-C.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.C Field + +```csharp +public const string C = C; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-CapsLock.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.CapsLock Field + +```csharp +public const string CapsLock = CapsLock; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Caret.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Caret Field + +```csharp +public const string Caret = Caret; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Colon.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Colon Field + +```csharp +public const string Colon = Colon; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Comma.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Comma Field + +```csharp +public const string Comma = Comma; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-D.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.D Field + +```csharp +public const string D = D; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Daydream-LeftSelectClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Daydream](./Keys-Daydream.md 'UnrealEngine.Framework.Keys.Daydream') +## Keys.Daydream.LeftSelectClick Field + +```csharp +public const string LeftSelectClick = Daydream_Left_Select_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Daydream-LeftTrackpadClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Daydream](./Keys-Daydream.md 'UnrealEngine.Framework.Keys.Daydream') +## Keys.Daydream.LeftTrackpadClick Field + +```csharp +public const string LeftTrackpadClick = Daydream_Left_Trackpad_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Daydream-LeftTrackpadTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Daydream](./Keys-Daydream.md 'UnrealEngine.Framework.Keys.Daydream') +## Keys.Daydream.LeftTrackpadTouch Field + +```csharp +public const string LeftTrackpadTouch = Daydream_Left_Trackpad_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Daydream-LeftTrackpadX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Daydream](./Keys-Daydream.md 'UnrealEngine.Framework.Keys.Daydream') +## Keys.Daydream.LeftTrackpadX Field + +```csharp +public const string LeftTrackpadX = Daydream_Left_Trackpad_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Daydream-LeftTrackpadY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Daydream](./Keys-Daydream.md 'UnrealEngine.Framework.Keys.Daydream') +## Keys.Daydream.LeftTrackpadY Field + +```csharp +public const string LeftTrackpadY = Daydream_Left_Trackpad_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Daydream-RightSelectClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Daydream](./Keys-Daydream.md 'UnrealEngine.Framework.Keys.Daydream') +## Keys.Daydream.RightSelectClick Field + +```csharp +public const string RightSelectClick = Daydream_Right_Select_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Daydream-RightTrackpadClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Daydream](./Keys-Daydream.md 'UnrealEngine.Framework.Keys.Daydream') +## Keys.Daydream.RightTrackpadClick Field + +```csharp +public const string RightTrackpadClick = Daydream_Right_Trackpad_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Daydream-RightTrackpadTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Daydream](./Keys-Daydream.md 'UnrealEngine.Framework.Keys.Daydream') +## Keys.Daydream.RightTrackpadTouch Field + +```csharp +public const string RightTrackpadTouch = Daydream_Right_Trackpad_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Daydream-RightTrackpadX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Daydream](./Keys-Daydream.md 'UnrealEngine.Framework.Keys.Daydream') +## Keys.Daydream.RightTrackpadX Field + +```csharp +public const string RightTrackpadX = Daydream_Right_Trackpad_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Daydream-RightTrackpadY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Daydream](./Keys-Daydream.md 'UnrealEngine.Framework.Keys.Daydream') +## Keys.Daydream.RightTrackpadY Field + +```csharp +public const string RightTrackpadY = Daydream_Right_Trackpad_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Daydream.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Daydream Class +Google Daydream +```csharp +public static class Keys.Daydream +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.Daydream +### Fields +- [LeftSelectClick](./Keys-Daydream-LeftSelectClick.md 'UnrealEngine.Framework.Keys.Daydream.LeftSelectClick') +- [LeftTrackpadClick](./Keys-Daydream-LeftTrackpadClick.md 'UnrealEngine.Framework.Keys.Daydream.LeftTrackpadClick') +- [LeftTrackpadTouch](./Keys-Daydream-LeftTrackpadTouch.md 'UnrealEngine.Framework.Keys.Daydream.LeftTrackpadTouch') +- [LeftTrackpadX](./Keys-Daydream-LeftTrackpadX.md 'UnrealEngine.Framework.Keys.Daydream.LeftTrackpadX') +- [LeftTrackpadY](./Keys-Daydream-LeftTrackpadY.md 'UnrealEngine.Framework.Keys.Daydream.LeftTrackpadY') +- [RightSelectClick](./Keys-Daydream-RightSelectClick.md 'UnrealEngine.Framework.Keys.Daydream.RightSelectClick') +- [RightTrackpadClick](./Keys-Daydream-RightTrackpadClick.md 'UnrealEngine.Framework.Keys.Daydream.RightTrackpadClick') +- [RightTrackpadTouch](./Keys-Daydream-RightTrackpadTouch.md 'UnrealEngine.Framework.Keys.Daydream.RightTrackpadTouch') +- [RightTrackpadX](./Keys-Daydream-RightTrackpadX.md 'UnrealEngine.Framework.Keys.Daydream.RightTrackpadX') +- [RightTrackpadY](./Keys-Daydream-RightTrackpadY.md 'UnrealEngine.Framework.Keys.Daydream.RightTrackpadY') + +``` + +`API/Keys-Decimal.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Decimal Field + +```csharp +public const string Decimal = Decimal; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Delete.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Delete Field + +```csharp +public const string Delete = Delete; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Divide.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Divide Field + +```csharp +public const string Divide = Divide; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Dollar.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Dollar Field + +```csharp +public const string Dollar = Dollar; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Down.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Down Field + +```csharp +public const string Down = Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-E.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.E Field + +```csharp +public const string E = E; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Eight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Eight Field + +```csharp +public const string Eight = Eight; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-End.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.End Field + +```csharp +public const string End = End; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Enter.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Enter Field + +```csharp +public const string Enter = Enter; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Equal.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Equal Field + +```csharp +public const string Equal = Equals; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Escape.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Escape Field + +```csharp +public const string Escape = Escape; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Exclamation.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Exclamation Field + +```csharp +public const string Exclamation = Exclamation; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-F.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.F Field + +```csharp +public const string F = F; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-F1.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.F1 Field + +```csharp +public const string F1 = F1; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-F10.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.F10 Field + +```csharp +public const string F10 = F10; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-F11.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.F11 Field + +```csharp +public const string F11 = F11; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-F12.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.F12 Field + +```csharp +public const string F12 = F12; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-F2.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.F2 Field + +```csharp +public const string F2 = F2; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-F3.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.F3 Field + +```csharp +public const string F3 = F3; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-F4.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.F4 Field + +```csharp +public const string F4 = F4; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-F5.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.F5 Field + +```csharp +public const string F5 = F5; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-F6.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.F6 Field + +```csharp +public const string F6 = F6; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-F7.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.F7 Field + +```csharp +public const string F7 = F7; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-F8.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.F8 Field + +```csharp +public const string F8 = F8; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-F9.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.F9 Field + +```csharp +public const string F9 = F9; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Five.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Five Field + +```csharp +public const string Five = Five; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Four.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Four Field + +```csharp +public const string Four = Four; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-G.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.G Field + +```csharp +public const string G = G; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-DPadDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.DPadDown Field + +```csharp +public const string DPadDown = Gamepad_DPad_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-DPadLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.DPadLeft Field + +```csharp +public const string DPadLeft = Gamepad_DPad_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-DPadRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.DPadRight Field + +```csharp +public const string DPadRight = Gamepad_DPad_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-DPadUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.DPadUp Field + +```csharp +public const string DPadUp = Gamepad_DPad_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-FaceButtonBottom.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.FaceButtonBottom Field + +```csharp +public const string FaceButtonBottom = Gamepad_FaceButton_Bottom; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-FaceButtonLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.FaceButtonLeft Field + +```csharp +public const string FaceButtonLeft = Gamepad_FaceButton_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-FaceButtonRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.FaceButtonRight Field + +```csharp +public const string FaceButtonRight = Gamepad_FaceButton_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-FaceButtonTop.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.FaceButtonTop Field + +```csharp +public const string FaceButtonTop = Gamepad_FaceButton_Top; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-LeftShoulder.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.LeftShoulder Field + +```csharp +public const string LeftShoulder = Gamepad_LeftShoulder; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-LeftThumbstick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.LeftThumbstick Field + +```csharp +public const string LeftThumbstick = Gamepad_LeftThumbstick; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-LeftTrigger.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.LeftTrigger Field + +```csharp +public const string LeftTrigger = Gamepad_LeftTrigger; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-LeftTriggerAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.LeftTriggerAxis Field + +```csharp +public const string LeftTriggerAxis = Gamepad_LeftTriggerAxis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-LeftX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.LeftX Field + +```csharp +public const string LeftX = Gamepad_LeftX; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-LeftY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.LeftY Field + +```csharp +public const string LeftY = Gamepad_LeftY; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-RightShoulder.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.RightShoulder Field + +```csharp +public const string RightShoulder = Gamepad_RightShoulder; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-RightThumbstick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.RightThumbstick Field + +```csharp +public const string RightThumbstick = Gamepad_RightThumbstick; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-RightTrigger.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.RightTrigger Field + +```csharp +public const string RightTrigger = Gamepad_RightTrigger; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-RightTriggerAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.RightTriggerAxis Field + +```csharp +public const string RightTriggerAxis = Gamepad_RightTriggerAxis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-RightX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.RightX Field + +```csharp +public const string RightX = Gamepad_RightX; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-RightY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.RightY Field + +```csharp +public const string RightY = Gamepad_RightY; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-SpecialLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.SpecialLeft Field + +```csharp +public const string SpecialLeft = Gamepad_Special_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-SpecialLeftX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.SpecialLeftX Field + +```csharp +public const string SpecialLeftX = Gamepad_Special_Left_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-SpecialLeftY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.SpecialLeftY Field + +```csharp +public const string SpecialLeftY = Gamepad_Special_Left_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad-SpecialRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +## Keys.Gamepad.SpecialRight Field + +```csharp +public const string SpecialRight = Gamepad_Special_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gamepad.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Gamepad Class +Gamepad +```csharp +public static class Keys.Gamepad +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.Gamepad +### Fields +- [DPadDown](./Keys-Gamepad-DPadDown.md 'UnrealEngine.Framework.Keys.Gamepad.DPadDown') +- [DPadLeft](./Keys-Gamepad-DPadLeft.md 'UnrealEngine.Framework.Keys.Gamepad.DPadLeft') +- [DPadRight](./Keys-Gamepad-DPadRight.md 'UnrealEngine.Framework.Keys.Gamepad.DPadRight') +- [DPadUp](./Keys-Gamepad-DPadUp.md 'UnrealEngine.Framework.Keys.Gamepad.DPadUp') +- [FaceButtonBottom](./Keys-Gamepad-FaceButtonBottom.md 'UnrealEngine.Framework.Keys.Gamepad.FaceButtonBottom') +- [FaceButtonLeft](./Keys-Gamepad-FaceButtonLeft.md 'UnrealEngine.Framework.Keys.Gamepad.FaceButtonLeft') +- [FaceButtonRight](./Keys-Gamepad-FaceButtonRight.md 'UnrealEngine.Framework.Keys.Gamepad.FaceButtonRight') +- [FaceButtonTop](./Keys-Gamepad-FaceButtonTop.md 'UnrealEngine.Framework.Keys.Gamepad.FaceButtonTop') +- [LeftShoulder](./Keys-Gamepad-LeftShoulder.md 'UnrealEngine.Framework.Keys.Gamepad.LeftShoulder') +- [LeftThumbstick](./Keys-Gamepad-LeftThumbstick.md 'UnrealEngine.Framework.Keys.Gamepad.LeftThumbstick') +- [LeftTrigger](./Keys-Gamepad-LeftTrigger.md 'UnrealEngine.Framework.Keys.Gamepad.LeftTrigger') +- [LeftTriggerAxis](./Keys-Gamepad-LeftTriggerAxis.md 'UnrealEngine.Framework.Keys.Gamepad.LeftTriggerAxis') +- [LeftX](./Keys-Gamepad-LeftX.md 'UnrealEngine.Framework.Keys.Gamepad.LeftX') +- [LeftY](./Keys-Gamepad-LeftY.md 'UnrealEngine.Framework.Keys.Gamepad.LeftY') +- [RightShoulder](./Keys-Gamepad-RightShoulder.md 'UnrealEngine.Framework.Keys.Gamepad.RightShoulder') +- [RightThumbstick](./Keys-Gamepad-RightThumbstick.md 'UnrealEngine.Framework.Keys.Gamepad.RightThumbstick') +- [RightTrigger](./Keys-Gamepad-RightTrigger.md 'UnrealEngine.Framework.Keys.Gamepad.RightTrigger') +- [RightTriggerAxis](./Keys-Gamepad-RightTriggerAxis.md 'UnrealEngine.Framework.Keys.Gamepad.RightTriggerAxis') +- [RightX](./Keys-Gamepad-RightX.md 'UnrealEngine.Framework.Keys.Gamepad.RightX') +- [RightY](./Keys-Gamepad-RightY.md 'UnrealEngine.Framework.Keys.Gamepad.RightY') +- [SpecialLeft](./Keys-Gamepad-SpecialLeft.md 'UnrealEngine.Framework.Keys.Gamepad.SpecialLeft') +- [SpecialLeftX](./Keys-Gamepad-SpecialLeftX.md 'UnrealEngine.Framework.Keys.Gamepad.SpecialLeftX') +- [SpecialLeftY](./Keys-Gamepad-SpecialLeftY.md 'UnrealEngine.Framework.Keys.Gamepad.SpecialLeftY') +- [SpecialRight](./Keys-Gamepad-SpecialRight.md 'UnrealEngine.Framework.Keys.Gamepad.SpecialRight') + +``` + +`API/Keys-Gesture-Flick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gesture](./Keys-Gesture.md 'UnrealEngine.Framework.Keys.Gesture') +## Keys.Gesture.Flick Field + +```csharp +public const string Flick = Gesture_Flick; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gesture-Pinch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gesture](./Keys-Gesture.md 'UnrealEngine.Framework.Keys.Gesture') +## Keys.Gesture.Pinch Field + +```csharp +public const string Pinch = Gesture_Pinch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gesture-Rotate.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Gesture](./Keys-Gesture.md 'UnrealEngine.Framework.Keys.Gesture') +## Keys.Gesture.Rotate Field + +```csharp +public const string Rotate = Gesture_Rotate; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Gesture.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Gesture Class +Gestures +```csharp +public static class Keys.Gesture +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.Gesture +### Fields +- [Flick](./Keys-Gesture-Flick.md 'UnrealEngine.Framework.Keys.Gesture.Flick') +- [Pinch](./Keys-Gesture-Pinch.md 'UnrealEngine.Framework.Keys.Gesture.Pinch') +- [Rotate](./Keys-Gesture-Rotate.md 'UnrealEngine.Framework.Keys.Gesture.Rotate') + +``` + +`API/Keys-H.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.H Field + +```csharp +public const string H = H; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Home.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Home Field + +```csharp +public const string Home = Home; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Hyphen.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Hyphen Field + +```csharp +public const string Hyphen = Hyphen; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-I.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.I Field + +```csharp +public const string I = I; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Insert.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Insert Field + +```csharp +public const string Insert = Insert; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Invalid.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Invalid Field + +```csharp +public const string Invalid = Invalid; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-J.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.J Field + +```csharp +public const string J = J; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-K.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.K Field + +```csharp +public const string K = K; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-L.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.L Field + +```csharp +public const string L = L; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Left.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Left Field + +```csharp +public const string Left = Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-LeftAlt.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.LeftAlt Field + +```csharp +public const string LeftAlt = LeftAlt; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-LeftBracket.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.LeftBracket Field + +```csharp +public const string LeftBracket = LeftBracket; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-LeftCommand.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.LeftCommand Field + +```csharp +public const string LeftCommand = LeftCommand; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-LeftControl.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.LeftControl Field + +```csharp +public const string LeftControl = LeftControl; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-LeftMouseButton.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.LeftMouseButton Field + +```csharp +public const string LeftMouseButton = LeftMouseButton; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-LeftParantheses.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.LeftParantheses Field + +```csharp +public const string LeftParantheses = LeftParantheses; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-LeftShift.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.LeftShift Field + +```csharp +public const string LeftShift = LeftShift; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-M.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.M Field + +```csharp +public const string M = M; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MiddleMouseButton.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.MiddleMouseButton Field + +```csharp +public const string MiddleMouseButton = MiddleMouseButton; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftGripClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftGripClick Field + +```csharp +public const string LeftGripClick = MixedReality_Left_Grip_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftMenuClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftMenuClick Field + +```csharp +public const string LeftMenuClick = MixedReality_Left_Menu_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftThumbstickClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftThumbstickClick Field + +```csharp +public const string LeftThumbstickClick = MixedReality_Left_Thumbstick_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftThumbstickDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftThumbstickDown Field + +```csharp +public const string LeftThumbstickDown = MixedReality_Left_Thumbstick_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftThumbstickLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftThumbstickLeft Field + +```csharp +public const string LeftThumbstickLeft = MixedReality_Left_Thumbstick_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftThumbstickRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftThumbstickRight Field + +```csharp +public const string LeftThumbstickRight = MixedReality_Left_Thumbstick_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftThumbstickUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftThumbstickUp Field + +```csharp +public const string LeftThumbstickUp = MixedReality_Left_Thumbstick_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftThumbstickX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftThumbstickX Field + +```csharp +public const string LeftThumbstickX = MixedReality_Left_Thumbstick_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftThumbstickY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftThumbstickY Field + +```csharp +public const string LeftThumbstickY = MixedReality_Left_Thumbstick_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftTrackpadClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftTrackpadClick Field + +```csharp +public const string LeftTrackpadClick = MixedReality_Left_Trackpad_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftTrackpadDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftTrackpadDown Field + +```csharp +public const string LeftTrackpadDown = MixedReality_Left_Trackpad_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftTrackpadLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftTrackpadLeft Field + +```csharp +public const string LeftTrackpadLeft = MixedReality_Left_Trackpad_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftTrackpadRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftTrackpadRight Field + +```csharp +public const string LeftTrackpadRight = MixedReality_Left_Trackpad_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftTrackpadTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftTrackpadTouch Field + +```csharp +public const string LeftTrackpadTouch = MixedReality_Left_Trackpad_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftTrackpadUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftTrackpadUp Field + +```csharp +public const string LeftTrackpadUp = MixedReality_Left_Trackpad_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftTrackpadX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftTrackpadX Field + +```csharp +public const string LeftTrackpadX = MixedReality_Left_Trackpad_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftTrackpadY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftTrackpadY Field + +```csharp +public const string LeftTrackpadY = MixedReality_Left_Trackpad_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftTriggerAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftTriggerAxis Field + +```csharp +public const string LeftTriggerAxis = MixedReality_Left_Trigger_Axis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-LeftTriggerClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.LeftTriggerClick Field + +```csharp +public const string LeftTriggerClick = MixedReality_Left_Trigger_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightGripClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightGripClick Field + +```csharp +public const string RightGripClick = MixedReality_Right_Grip_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightMenuClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightMenuClick Field + +```csharp +public const string RightMenuClick = MixedReality_Right_Menu_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightThumbstickClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightThumbstickClick Field + +```csharp +public const string RightThumbstickClick = MixedReality_Right_Thumbstick_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightThumbstickDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightThumbstickDown Field + +```csharp +public const string RightThumbstickDown = MixedReality_Right_Thumbstick_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightThumbstickLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightThumbstickLeft Field + +```csharp +public const string RightThumbstickLeft = MixedReality_Right_Thumbstick_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightThumbstickRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightThumbstickRight Field + +```csharp +public const string RightThumbstickRight = MixedReality_Right_Thumbstick_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightThumbstickUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightThumbstickUp Field + +```csharp +public const string RightThumbstickUp = MixedReality_Right_Thumbstick_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightThumbstickX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightThumbstickX Field + +```csharp +public const string RightThumbstickX = MixedReality_Right_Thumbstick_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightThumbstickY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightThumbstickY Field + +```csharp +public const string RightThumbstickY = MixedReality_Right_Thumbstick_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightTrackpadClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightTrackpadClick Field + +```csharp +public const string RightTrackpadClick = MixedReality_Right_Trackpad_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightTrackpadDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightTrackpadDown Field + +```csharp +public const string RightTrackpadDown = MixedReality_Right_Trackpad_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightTrackpadLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightTrackpadLeft Field + +```csharp +public const string RightTrackpadLeft = MixedReality_Right_Trackpad_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightTrackpadRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightTrackpadRight Field + +```csharp +public const string RightTrackpadRight = MixedReality_Right_Trackpad_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightTrackpadTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightTrackpadTouch Field + +```csharp +public const string RightTrackpadTouch = MixedReality_Right_Trackpad_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightTrackpadUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightTrackpadUp Field + +```csharp +public const string RightTrackpadUp = MixedReality_Right_Trackpad_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightTrackpadX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightTrackpadX Field + +```csharp +public const string RightTrackpadX = MixedReality_Right_Trackpad_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightTrackpadY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightTrackpadY Field + +```csharp +public const string RightTrackpadY = MixedReality_Right_Trackpad_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightTriggerAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightTriggerAxis Field + +```csharp +public const string RightTriggerAxis = MixedReality_Right_Trigger_Axis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality-RightTriggerClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +## Keys.MixedReality.RightTriggerClick Field + +```csharp +public const string RightTriggerClick = MixedReality_Right_Trigger_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MixedReality.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.MixedReality Class +Microsoft Mixed Reality +```csharp +public static class Keys.MixedReality +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.MixedReality +### Fields +- [LeftGripClick](./Keys-MixedReality-LeftGripClick.md 'UnrealEngine.Framework.Keys.MixedReality.LeftGripClick') +- [LeftMenuClick](./Keys-MixedReality-LeftMenuClick.md 'UnrealEngine.Framework.Keys.MixedReality.LeftMenuClick') +- [LeftThumbstickClick](./Keys-MixedReality-LeftThumbstickClick.md 'UnrealEngine.Framework.Keys.MixedReality.LeftThumbstickClick') +- [LeftThumbstickDown](./Keys-MixedReality-LeftThumbstickDown.md 'UnrealEngine.Framework.Keys.MixedReality.LeftThumbstickDown') +- [LeftThumbstickLeft](./Keys-MixedReality-LeftThumbstickLeft.md 'UnrealEngine.Framework.Keys.MixedReality.LeftThumbstickLeft') +- [LeftThumbstickRight](./Keys-MixedReality-LeftThumbstickRight.md 'UnrealEngine.Framework.Keys.MixedReality.LeftThumbstickRight') +- [LeftThumbstickUp](./Keys-MixedReality-LeftThumbstickUp.md 'UnrealEngine.Framework.Keys.MixedReality.LeftThumbstickUp') +- [LeftThumbstickX](./Keys-MixedReality-LeftThumbstickX.md 'UnrealEngine.Framework.Keys.MixedReality.LeftThumbstickX') +- [LeftThumbstickY](./Keys-MixedReality-LeftThumbstickY.md 'UnrealEngine.Framework.Keys.MixedReality.LeftThumbstickY') +- [LeftTrackpadClick](./Keys-MixedReality-LeftTrackpadClick.md 'UnrealEngine.Framework.Keys.MixedReality.LeftTrackpadClick') +- [LeftTrackpadDown](./Keys-MixedReality-LeftTrackpadDown.md 'UnrealEngine.Framework.Keys.MixedReality.LeftTrackpadDown') +- [LeftTrackpadLeft](./Keys-MixedReality-LeftTrackpadLeft.md 'UnrealEngine.Framework.Keys.MixedReality.LeftTrackpadLeft') +- [LeftTrackpadRight](./Keys-MixedReality-LeftTrackpadRight.md 'UnrealEngine.Framework.Keys.MixedReality.LeftTrackpadRight') +- [LeftTrackpadTouch](./Keys-MixedReality-LeftTrackpadTouch.md 'UnrealEngine.Framework.Keys.MixedReality.LeftTrackpadTouch') +- [LeftTrackpadUp](./Keys-MixedReality-LeftTrackpadUp.md 'UnrealEngine.Framework.Keys.MixedReality.LeftTrackpadUp') +- [LeftTrackpadX](./Keys-MixedReality-LeftTrackpadX.md 'UnrealEngine.Framework.Keys.MixedReality.LeftTrackpadX') +- [LeftTrackpadY](./Keys-MixedReality-LeftTrackpadY.md 'UnrealEngine.Framework.Keys.MixedReality.LeftTrackpadY') +- [LeftTriggerAxis](./Keys-MixedReality-LeftTriggerAxis.md 'UnrealEngine.Framework.Keys.MixedReality.LeftTriggerAxis') +- [LeftTriggerClick](./Keys-MixedReality-LeftTriggerClick.md 'UnrealEngine.Framework.Keys.MixedReality.LeftTriggerClick') +- [RightGripClick](./Keys-MixedReality-RightGripClick.md 'UnrealEngine.Framework.Keys.MixedReality.RightGripClick') +- [RightMenuClick](./Keys-MixedReality-RightMenuClick.md 'UnrealEngine.Framework.Keys.MixedReality.RightMenuClick') +- [RightThumbstickClick](./Keys-MixedReality-RightThumbstickClick.md 'UnrealEngine.Framework.Keys.MixedReality.RightThumbstickClick') +- [RightThumbstickDown](./Keys-MixedReality-RightThumbstickDown.md 'UnrealEngine.Framework.Keys.MixedReality.RightThumbstickDown') +- [RightThumbstickLeft](./Keys-MixedReality-RightThumbstickLeft.md 'UnrealEngine.Framework.Keys.MixedReality.RightThumbstickLeft') +- [RightThumbstickRight](./Keys-MixedReality-RightThumbstickRight.md 'UnrealEngine.Framework.Keys.MixedReality.RightThumbstickRight') +- [RightThumbstickUp](./Keys-MixedReality-RightThumbstickUp.md 'UnrealEngine.Framework.Keys.MixedReality.RightThumbstickUp') +- [RightThumbstickX](./Keys-MixedReality-RightThumbstickX.md 'UnrealEngine.Framework.Keys.MixedReality.RightThumbstickX') +- [RightThumbstickY](./Keys-MixedReality-RightThumbstickY.md 'UnrealEngine.Framework.Keys.MixedReality.RightThumbstickY') +- [RightTrackpadClick](./Keys-MixedReality-RightTrackpadClick.md 'UnrealEngine.Framework.Keys.MixedReality.RightTrackpadClick') +- [RightTrackpadDown](./Keys-MixedReality-RightTrackpadDown.md 'UnrealEngine.Framework.Keys.MixedReality.RightTrackpadDown') +- [RightTrackpadLeft](./Keys-MixedReality-RightTrackpadLeft.md 'UnrealEngine.Framework.Keys.MixedReality.RightTrackpadLeft') +- [RightTrackpadRight](./Keys-MixedReality-RightTrackpadRight.md 'UnrealEngine.Framework.Keys.MixedReality.RightTrackpadRight') +- [RightTrackpadTouch](./Keys-MixedReality-RightTrackpadTouch.md 'UnrealEngine.Framework.Keys.MixedReality.RightTrackpadTouch') +- [RightTrackpadUp](./Keys-MixedReality-RightTrackpadUp.md 'UnrealEngine.Framework.Keys.MixedReality.RightTrackpadUp') +- [RightTrackpadX](./Keys-MixedReality-RightTrackpadX.md 'UnrealEngine.Framework.Keys.MixedReality.RightTrackpadX') +- [RightTrackpadY](./Keys-MixedReality-RightTrackpadY.md 'UnrealEngine.Framework.Keys.MixedReality.RightTrackpadY') +- [RightTriggerAxis](./Keys-MixedReality-RightTriggerAxis.md 'UnrealEngine.Framework.Keys.MixedReality.RightTriggerAxis') +- [RightTriggerClick](./Keys-MixedReality-RightTriggerClick.md 'UnrealEngine.Framework.Keys.MixedReality.RightTriggerClick') + +``` + +`API/Keys-MouseScrollDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.MouseScrollDown Field + +```csharp +public const string MouseScrollDown = MouseScrollDown; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MouseScrollUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.MouseScrollUp Field + +```csharp +public const string MouseScrollUp = MouseScrollUp; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MouseWheelAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.MouseWheelAxis Field + +```csharp +public const string MouseWheelAxis = MouseWheelAxis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MouseX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.MouseX Field + +```csharp +public const string MouseX = MouseX; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-MouseY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.MouseY Field + +```csharp +public const string MouseY = MouseY; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Multiply.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Multiply Field + +```csharp +public const string Multiply = Multiply; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-N.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.N Field + +```csharp +public const string N = N; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Nine.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Nine Field + +```csharp +public const string Nine = Nine; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-NumLock.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.NumLock Field + +```csharp +public const string NumLock = NumLock; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-NumPadEight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.NumPadEight Field + +```csharp +public const string NumPadEight = NumPadEight; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-NumPadFive.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.NumPadFive Field + +```csharp +public const string NumPadFive = NumPadFive; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-NumPadFour.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.NumPadFour Field + +```csharp +public const string NumPadFour = NumPadFour; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-NumPadNine.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.NumPadNine Field + +```csharp +public const string NumPadNine = NumPadNine; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-NumPadOne.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.NumPadOne Field + +```csharp +public const string NumPadOne = NumPadOne; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-NumPadSeven.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.NumPadSeven Field + +```csharp +public const string NumPadSeven = NumPadSeven; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-NumPadSix.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.NumPadSix Field + +```csharp +public const string NumPadSix = NumPadSix; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-NumPadThree.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.NumPadThree Field + +```csharp +public const string NumPadThree = NumPadThree; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-NumPadTwo.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.NumPadTwo Field + +```csharp +public const string NumPadTwo = NumPadTwo; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-NumPadZero.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.NumPadZero Field + +```csharp +public const string NumPadZero = NumPadZero; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-O.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.O Field + +```csharp +public const string O = O; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-LeftBackClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.LeftBackClick Field + +```csharp +public const string LeftBackClick = OculusGo_Left_Back_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-LeftSystemClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.LeftSystemClick Field + +```csharp +public const string LeftSystemClick = OculusGo_Left_System_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-LeftTrackpadClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.LeftTrackpadClick Field + +```csharp +public const string LeftTrackpadClick = OculusGo_Left_Trackpad_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-LeftTrackpadTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.LeftTrackpadTouch Field + +```csharp +public const string LeftTrackpadTouch = OculusGo_Left_Trackpad_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-LeftTrackpadX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.LeftTrackpadX Field + +```csharp +public const string LeftTrackpadX = OculusGo_Left_Trackpad_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-LeftTrackpadY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.LeftTrackpadY Field + +```csharp +public const string LeftTrackpadY = OculusGo_Left_Trackpad_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-LeftTriggerClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.LeftTriggerClick Field + +```csharp +public const string LeftTriggerClick = OculusGo_Left_Trigger_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-RightBackClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.RightBackClick Field + +```csharp +public const string RightBackClick = OculusGo_Right_Back_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-RightSystemClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.RightSystemClick Field + +```csharp +public const string RightSystemClick = OculusGo_Right_System_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-RightTrackpadClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.RightTrackpadClick Field + +```csharp +public const string RightTrackpadClick = OculusGo_Right_Trackpad_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-RightTrackpadTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.RightTrackpadTouch Field + +```csharp +public const string RightTrackpadTouch = OculusGo_Right_Trackpad_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-RightTrackpadX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.RightTrackpadX Field + +```csharp +public const string RightTrackpadX = OculusGo_Right_Trackpad_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-RightTrackpadY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.RightTrackpadY Field + +```csharp +public const string RightTrackpadY = OculusGo_Right_Trackpad_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo-RightTriggerClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +## Keys.OculusGo.RightTriggerClick Field + +```csharp +public const string RightTriggerClick = OculusGo_Right_Trigger_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusGo.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.OculusGo Class +Oculus Go +```csharp +public static class Keys.OculusGo +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.OculusGo +### Fields +- [LeftBackClick](./Keys-OculusGo-LeftBackClick.md 'UnrealEngine.Framework.Keys.OculusGo.LeftBackClick') +- [LeftSystemClick](./Keys-OculusGo-LeftSystemClick.md 'UnrealEngine.Framework.Keys.OculusGo.LeftSystemClick') +- [LeftTrackpadClick](./Keys-OculusGo-LeftTrackpadClick.md 'UnrealEngine.Framework.Keys.OculusGo.LeftTrackpadClick') +- [LeftTrackpadTouch](./Keys-OculusGo-LeftTrackpadTouch.md 'UnrealEngine.Framework.Keys.OculusGo.LeftTrackpadTouch') +- [LeftTrackpadX](./Keys-OculusGo-LeftTrackpadX.md 'UnrealEngine.Framework.Keys.OculusGo.LeftTrackpadX') +- [LeftTrackpadY](./Keys-OculusGo-LeftTrackpadY.md 'UnrealEngine.Framework.Keys.OculusGo.LeftTrackpadY') +- [LeftTriggerClick](./Keys-OculusGo-LeftTriggerClick.md 'UnrealEngine.Framework.Keys.OculusGo.LeftTriggerClick') +- [RightBackClick](./Keys-OculusGo-RightBackClick.md 'UnrealEngine.Framework.Keys.OculusGo.RightBackClick') +- [RightSystemClick](./Keys-OculusGo-RightSystemClick.md 'UnrealEngine.Framework.Keys.OculusGo.RightSystemClick') +- [RightTrackpadClick](./Keys-OculusGo-RightTrackpadClick.md 'UnrealEngine.Framework.Keys.OculusGo.RightTrackpadClick') +- [RightTrackpadTouch](./Keys-OculusGo-RightTrackpadTouch.md 'UnrealEngine.Framework.Keys.OculusGo.RightTrackpadTouch') +- [RightTrackpadX](./Keys-OculusGo-RightTrackpadX.md 'UnrealEngine.Framework.Keys.OculusGo.RightTrackpadX') +- [RightTrackpadY](./Keys-OculusGo-RightTrackpadY.md 'UnrealEngine.Framework.Keys.OculusGo.RightTrackpadY') +- [RightTriggerClick](./Keys-OculusGo-RightTriggerClick.md 'UnrealEngine.Framework.Keys.OculusGo.RightTriggerClick') + +``` + +`API/Keys-OculusTouch-LeftGripAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftGripAxis Field + +```csharp +public const string LeftGripAxis = OculusTouch_Left_Grip_Axis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftGripClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftGripClick Field + +```csharp +public const string LeftGripClick = OculusTouch_Left_Grip_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftMenuClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftMenuClick Field + +```csharp +public const string LeftMenuClick = OculusTouch_Left_Menu_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftThumbstickClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftThumbstickClick Field + +```csharp +public const string LeftThumbstickClick = OculusTouch_Left_Thumbstick_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftThumbstickDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftThumbstickDown Field + +```csharp +public const string LeftThumbstickDown = OculusTouch_Left_Thumbstick_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftThumbstickLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftThumbstickLeft Field + +```csharp +public const string LeftThumbstickLeft = OculusTouch_Left_Thumbstick_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftThumbstickRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftThumbstickRight Field + +```csharp +public const string LeftThumbstickRight = OculusTouch_Left_Thumbstick_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftThumbstickTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftThumbstickTouch Field + +```csharp +public const string LeftThumbstickTouch = OculusTouch_Left_Thumbstick_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftThumbstickUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftThumbstickUp Field + +```csharp +public const string LeftThumbstickUp = OculusTouch_Left_Thumbstick_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftThumbstickX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftThumbstickX Field + +```csharp +public const string LeftThumbstickX = OculusTouch_Left_Thumbstick_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftThumbstickY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftThumbstickY Field + +```csharp +public const string LeftThumbstickY = OculusTouch_Left_Thumbstick_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftTriggerAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftTriggerAxis Field + +```csharp +public const string LeftTriggerAxis = OculusTouch_Left_Trigger_Axis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftTriggerClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftTriggerClick Field + +```csharp +public const string LeftTriggerClick = OculusTouch_Left_Trigger_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftTriggerTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftTriggerTouch Field + +```csharp +public const string LeftTriggerTouch = OculusTouch_Left_Trigger_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftXClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftXClick Field + +```csharp +public const string LeftXClick = OculusTouch_Left_X_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftXTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftXTouch Field + +```csharp +public const string LeftXTouch = OculusTouch_Left_X_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftYClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftYClick Field + +```csharp +public const string LeftYClick = OculusTouch_Left_Y_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-LeftYTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.LeftYTouch Field + +```csharp +public const string LeftYTouch = OculusTouch_Left_Y_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightAClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightAClick Field + +```csharp +public const string RightAClick = OculusTouch_Right_A_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightATouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightATouch Field + +```csharp +public const string RightATouch = OculusTouch_Right_A_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightBClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightBClick Field + +```csharp +public const string RightBClick = OculusTouch_Right_B_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightBTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightBTouch Field + +```csharp +public const string RightBTouch = OculusTouch_Right_B_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightGripAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightGripAxis Field + +```csharp +public const string RightGripAxis = OculusTouch_Right_Grip_Axis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightGripClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightGripClick Field + +```csharp +public const string RightGripClick = OculusTouch_Right_Grip_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightSystemClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightSystemClick Field + +```csharp +public const string RightSystemClick = OculusTouch_Right_System_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightThumbstickClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightThumbstickClick Field + +```csharp +public const string RightThumbstickClick = OculusTouch_Right_Thumbstick_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightThumbstickDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightThumbstickDown Field + +```csharp +public const string RightThumbstickDown = OculusTouch_Right_Thumbstick_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightThumbstickLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightThumbstickLeft Field + +```csharp +public const string RightThumbstickLeft = OculusTouch_Right_Thumbstick_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightThumbstickRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightThumbstickRight Field + +```csharp +public const string RightThumbstickRight = OculusTouch_Right_Thumbstick_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightThumbstickTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightThumbstickTouch Field + +```csharp +public const string RightThumbstickTouch = OculusTouch_Right_Thumbstick_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightThumbstickUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightThumbstickUp Field + +```csharp +public const string RightThumbstickUp = OculusTouch_Right_Thumbstick_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightThumbstickX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightThumbstickX Field + +```csharp +public const string RightThumbstickX = OculusTouch_Right_Thumbstick_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightThumbstickY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightThumbstickY Field + +```csharp +public const string RightThumbstickY = OculusTouch_Right_Thumbstick_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightTriggerAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightTriggerAxis Field + +```csharp +public const string RightTriggerAxis = OculusTouch_Right_Trigger_Axis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightTriggerClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightTriggerClick Field + +```csharp +public const string RightTriggerClick = OculusTouch_Right_Trigger_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch-RightTriggerTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +## Keys.OculusTouch.RightTriggerTouch Field + +```csharp +public const string RightTriggerTouch = OculusTouch_Right_Trigger_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-OculusTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.OculusTouch Class +Oculus Touch +```csharp +public static class Keys.OculusTouch +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.OculusTouch +### Fields +- [LeftGripAxis](./Keys-OculusTouch-LeftGripAxis.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftGripAxis') +- [LeftGripClick](./Keys-OculusTouch-LeftGripClick.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftGripClick') +- [LeftMenuClick](./Keys-OculusTouch-LeftMenuClick.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftMenuClick') +- [LeftThumbstickClick](./Keys-OculusTouch-LeftThumbstickClick.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftThumbstickClick') +- [LeftThumbstickDown](./Keys-OculusTouch-LeftThumbstickDown.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftThumbstickDown') +- [LeftThumbstickLeft](./Keys-OculusTouch-LeftThumbstickLeft.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftThumbstickLeft') +- [LeftThumbstickRight](./Keys-OculusTouch-LeftThumbstickRight.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftThumbstickRight') +- [LeftThumbstickTouch](./Keys-OculusTouch-LeftThumbstickTouch.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftThumbstickTouch') +- [LeftThumbstickUp](./Keys-OculusTouch-LeftThumbstickUp.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftThumbstickUp') +- [LeftThumbstickX](./Keys-OculusTouch-LeftThumbstickX.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftThumbstickX') +- [LeftThumbstickY](./Keys-OculusTouch-LeftThumbstickY.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftThumbstickY') +- [LeftTriggerAxis](./Keys-OculusTouch-LeftTriggerAxis.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftTriggerAxis') +- [LeftTriggerClick](./Keys-OculusTouch-LeftTriggerClick.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftTriggerClick') +- [LeftTriggerTouch](./Keys-OculusTouch-LeftTriggerTouch.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftTriggerTouch') +- [LeftXClick](./Keys-OculusTouch-LeftXClick.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftXClick') +- [LeftXTouch](./Keys-OculusTouch-LeftXTouch.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftXTouch') +- [LeftYClick](./Keys-OculusTouch-LeftYClick.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftYClick') +- [LeftYTouch](./Keys-OculusTouch-LeftYTouch.md 'UnrealEngine.Framework.Keys.OculusTouch.LeftYTouch') +- [RightAClick](./Keys-OculusTouch-RightAClick.md 'UnrealEngine.Framework.Keys.OculusTouch.RightAClick') +- [RightATouch](./Keys-OculusTouch-RightATouch.md 'UnrealEngine.Framework.Keys.OculusTouch.RightATouch') +- [RightBClick](./Keys-OculusTouch-RightBClick.md 'UnrealEngine.Framework.Keys.OculusTouch.RightBClick') +- [RightBTouch](./Keys-OculusTouch-RightBTouch.md 'UnrealEngine.Framework.Keys.OculusTouch.RightBTouch') +- [RightGripAxis](./Keys-OculusTouch-RightGripAxis.md 'UnrealEngine.Framework.Keys.OculusTouch.RightGripAxis') +- [RightGripClick](./Keys-OculusTouch-RightGripClick.md 'UnrealEngine.Framework.Keys.OculusTouch.RightGripClick') +- [RightSystemClick](./Keys-OculusTouch-RightSystemClick.md 'UnrealEngine.Framework.Keys.OculusTouch.RightSystemClick') +- [RightThumbstickClick](./Keys-OculusTouch-RightThumbstickClick.md 'UnrealEngine.Framework.Keys.OculusTouch.RightThumbstickClick') +- [RightThumbstickDown](./Keys-OculusTouch-RightThumbstickDown.md 'UnrealEngine.Framework.Keys.OculusTouch.RightThumbstickDown') +- [RightThumbstickLeft](./Keys-OculusTouch-RightThumbstickLeft.md 'UnrealEngine.Framework.Keys.OculusTouch.RightThumbstickLeft') +- [RightThumbstickRight](./Keys-OculusTouch-RightThumbstickRight.md 'UnrealEngine.Framework.Keys.OculusTouch.RightThumbstickRight') +- [RightThumbstickTouch](./Keys-OculusTouch-RightThumbstickTouch.md 'UnrealEngine.Framework.Keys.OculusTouch.RightThumbstickTouch') +- [RightThumbstickUp](./Keys-OculusTouch-RightThumbstickUp.md 'UnrealEngine.Framework.Keys.OculusTouch.RightThumbstickUp') +- [RightThumbstickX](./Keys-OculusTouch-RightThumbstickX.md 'UnrealEngine.Framework.Keys.OculusTouch.RightThumbstickX') +- [RightThumbstickY](./Keys-OculusTouch-RightThumbstickY.md 'UnrealEngine.Framework.Keys.OculusTouch.RightThumbstickY') +- [RightTriggerAxis](./Keys-OculusTouch-RightTriggerAxis.md 'UnrealEngine.Framework.Keys.OculusTouch.RightTriggerAxis') +- [RightTriggerClick](./Keys-OculusTouch-RightTriggerClick.md 'UnrealEngine.Framework.Keys.OculusTouch.RightTriggerClick') +- [RightTriggerTouch](./Keys-OculusTouch-RightTriggerTouch.md 'UnrealEngine.Framework.Keys.OculusTouch.RightTriggerTouch') + +``` + +`API/Keys-One.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.One Field + +```csharp +public const string One = One; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-P.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.P Field + +```csharp +public const string P = P; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-PS4-Special.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.PS4](./Keys-PS4.md 'UnrealEngine.Framework.Keys.PS4') +## Keys.PS4.Special Field + +```csharp +public const string Special = PS4_Special; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-PS4.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.PS4 Class +PS4 +```csharp +public static class Keys.PS4 +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.PS4 +### Fields +- [Special](./Keys-PS4-Special.md 'UnrealEngine.Framework.Keys.PS4.Special') + +``` + +`API/Keys-PageDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.PageDown Field + +```csharp +public const string PageDown = PageDown; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-PageUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.PageUp Field + +```csharp +public const string PageUp = PageUp; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Pause.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Pause Field + +```csharp +public const string Pause = Pause; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Period.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Period Field + +```csharp +public const string Period = Period; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Platform-Delete.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Platform](./Keys-Platform.md 'UnrealEngine.Framework.Keys.Platform') +## Keys.Platform.Delete Field + +```csharp +public const string Delete = Platform_Delete; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Platform.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Platform Class +Platform-specific +```csharp +public static class Keys.Platform +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.Platform +### Fields +- [Delete](./Keys-Platform-Delete.md 'UnrealEngine.Framework.Keys.Platform.Delete') + +``` + +`API/Keys-Q.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Q Field + +```csharp +public const string Q = Q; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Quote.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Quote Field + +```csharp +public const string Quote = Quote; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-R.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.R Field + +```csharp +public const string R = R; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Right.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Right Field + +```csharp +public const string Right = Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-RightAlt.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.RightAlt Field + +```csharp +public const string RightAlt = RightAlt; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-RightBracket.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.RightBracket Field + +```csharp +public const string RightBracket = RightBracket; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-RightCommand.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.RightCommand Field + +```csharp +public const string RightCommand = RightCommand; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-RightControl.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.RightControl Field + +```csharp +public const string RightControl = RightControl; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-RightMouseButton.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.RightMouseButton Field + +```csharp +public const string RightMouseButton = RightMouseButton; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-RightParantheses.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.RightParantheses Field + +```csharp +public const string RightParantheses = RightParantheses; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-RightShift.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.RightShift Field + +```csharp +public const string RightShift = RightShift; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-S.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.S Field + +```csharp +public const string S = S; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ScrollLock.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.ScrollLock Field + +```csharp +public const string ScrollLock = ScrollLock; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Section.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Section Field + +```csharp +public const string Section = Section; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Semicolon.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Semicolon Field + +```csharp +public const string Semicolon = Semicolon; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Seven.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Seven Field + +```csharp +public const string Seven = Seven; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Six.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Six Field + +```csharp +public const string Six = Six; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Slash.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Slash Field + +```csharp +public const string Slash = Slash; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-SpaceBar.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.SpaceBar Field + +```csharp +public const string SpaceBar = SpaceBar; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Steam-BackLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Steam](./Keys-Steam.md 'UnrealEngine.Framework.Keys.Steam') +## Keys.Steam.BackLeft Field + +```csharp +public const string BackLeft = Steam_Back_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Steam-BackRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Steam](./Keys-Steam.md 'UnrealEngine.Framework.Keys.Steam') +## Keys.Steam.BackRight Field + +```csharp +public const string BackRight = Steam_Back_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Steam-TouchOne.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Steam](./Keys-Steam.md 'UnrealEngine.Framework.Keys.Steam') +## Keys.Steam.TouchOne Field + +```csharp +public const string TouchOne = Steam_Touch_1; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Steam-TouchThree.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Steam](./Keys-Steam.md 'UnrealEngine.Framework.Keys.Steam') +## Keys.Steam.TouchThree Field + +```csharp +public const string TouchThree = Steam_Touch_3; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Steam-TouchTwo.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Steam](./Keys-Steam.md 'UnrealEngine.Framework.Keys.Steam') +## Keys.Steam.TouchTwo Field + +```csharp +public const string TouchTwo = Steam_Touch_2; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Steam-TouchZero.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Steam](./Keys-Steam.md 'UnrealEngine.Framework.Keys.Steam') +## Keys.Steam.TouchZero Field + +```csharp +public const string TouchZero = Steam_Touch_0; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Steam.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Steam Class +Steam Controller +```csharp +public static class Keys.Steam +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.Steam +### Fields +- [BackLeft](./Keys-Steam-BackLeft.md 'UnrealEngine.Framework.Keys.Steam.BackLeft') +- [BackRight](./Keys-Steam-BackRight.md 'UnrealEngine.Framework.Keys.Steam.BackRight') +- [TouchOne](./Keys-Steam-TouchOne.md 'UnrealEngine.Framework.Keys.Steam.TouchOne') +- [TouchThree](./Keys-Steam-TouchThree.md 'UnrealEngine.Framework.Keys.Steam.TouchThree') +- [TouchTwo](./Keys-Steam-TouchTwo.md 'UnrealEngine.Framework.Keys.Steam.TouchTwo') +- [TouchZero](./Keys-Steam-TouchZero.md 'UnrealEngine.Framework.Keys.Steam.TouchZero') + +``` + +`API/Keys-Subtract.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Subtract Field + +```csharp +public const string Subtract = Subtract; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-T.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.T Field + +```csharp +public const string T = T; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Tab.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Tab Field + +```csharp +public const string Tab = Tab; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Three.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Three Field + +```csharp +public const string Three = Three; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ThumbMouseButton.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.ThumbMouseButton Field + +```csharp +public const string ThumbMouseButton = ThumbMouseButton; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ThumbMouseButton2.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.ThumbMouseButton2 Field + +```csharp +public const string ThumbMouseButton2 = ThumbMouseButton2; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Tilde.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Tilde Field + +```csharp +public const string Tilde = Tilde; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Two.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Two Field + +```csharp +public const string Two = Two; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-U.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.U Field + +```csharp +public const string U = U; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Underscore.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Underscore Field + +```csharp +public const string Underscore = Underscore; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Up.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Up Field + +```csharp +public const string Up = Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-V.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.V Field + +```csharp +public const string V = V; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftAClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftAClick Field + +```csharp +public const string LeftAClick = ValveIndex_Left_A_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftATouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftATouch Field + +```csharp +public const string LeftATouch = ValveIndex_Left_A_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftBClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftBClick Field + +```csharp +public const string LeftBClick = ValveIndex_Left_B_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftBTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftBTouch Field + +```csharp +public const string LeftBTouch = ValveIndex_Left_B_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftGripAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftGripAxis Field + +```csharp +public const string LeftGripAxis = ValveIndex_Left_Grip_Axis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftGripClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftGripClick Field + +```csharp +public const string LeftGripClick = ValveIndex_Left_Grip_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftGripForce.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftGripForce Field + +```csharp +public const string LeftGripForce = ValveIndex_Left_Grip_Force; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftSystemClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftSystemClick Field + +```csharp +public const string LeftSystemClick = ValveIndex_Left_System_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftSystemTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftSystemTouch Field + +```csharp +public const string LeftSystemTouch = ValveIndex_Left_System_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftThumbstickClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftThumbstickClick Field + +```csharp +public const string LeftThumbstickClick = ValveIndex_Left_Thumbstick_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftThumbstickDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftThumbstickDown Field + +```csharp +public const string LeftThumbstickDown = ValveIndex_Left_Thumbstick_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftThumbstickLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftThumbstickLeft Field + +```csharp +public const string LeftThumbstickLeft = ValveIndex_Left_Thumbstick_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftThumbstickRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftThumbstickRight Field + +```csharp +public const string LeftThumbstickRight = ValveIndex_Left_Thumbstick_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftThumbstickTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftThumbstickTouch Field + +```csharp +public const string LeftThumbstickTouch = ValveIndex_Left_Thumbstick_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftThumbstickUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftThumbstickUp Field + +```csharp +public const string LeftThumbstickUp = ValveIndex_Left_Thumbstick_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftThumbstickX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftThumbstickX Field + +```csharp +public const string LeftThumbstickX = ValveIndex_Left_Thumbstick_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftThumbstickY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftThumbstickY Field + +```csharp +public const string LeftThumbstickY = ValveIndex_Left_Thumbstick_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftTrackpadClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftTrackpadClick Field + +```csharp +public const string LeftTrackpadClick = ValveIndex_Left_Trackpad_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftTrackpadDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftTrackpadDown Field + +```csharp +public const string LeftTrackpadDown = ValveIndex_Left_Trackpad_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftTrackpadForce.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftTrackpadForce Field + +```csharp +public const string LeftTrackpadForce = ValveIndex_Left_Trackpad_Force; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftTrackpadLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftTrackpadLeft Field + +```csharp +public const string LeftTrackpadLeft = ValveIndex_Left_Trackpad_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftTrackpadRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftTrackpadRight Field + +```csharp +public const string LeftTrackpadRight = ValveIndex_Left_Trackpad_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftTrackpadTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftTrackpadTouch Field + +```csharp +public const string LeftTrackpadTouch = ValveIndex_Left_Trackpad_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftTrackpadUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftTrackpadUp Field + +```csharp +public const string LeftTrackpadUp = ValveIndex_Left_Trackpad_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftTrackpadX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftTrackpadX Field + +```csharp +public const string LeftTrackpadX = ValveIndex_Left_Trackpad_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftTrackpadY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftTrackpadY Field + +```csharp +public const string LeftTrackpadY = ValveIndex_Left_Trackpad_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftTriggerAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftTriggerAxis Field + +```csharp +public const string LeftTriggerAxis = ValveIndex_Left_Trigger_Axis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftTriggerClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftTriggerClick Field + +```csharp +public const string LeftTriggerClick = ValveIndex_Left_Trigger_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-LeftTriggerTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.LeftTriggerTouch Field + +```csharp +public const string LeftTriggerTouch = ValveIndex_Left_Trigger_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightAClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightAClick Field + +```csharp +public const string RightAClick = ValveIndex_Right_A_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightATouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightATouch Field + +```csharp +public const string RightATouch = ValveIndex_Right_A_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightBClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightBClick Field + +```csharp +public const string RightBClick = ValveIndex_Right_B_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightBTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightBTouch Field + +```csharp +public const string RightBTouch = ValveIndex_Right_B_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightGripAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightGripAxis Field + +```csharp +public const string RightGripAxis = ValveIndex_Right_Grip_Axis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightGripClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightGripClick Field + +```csharp +public const string RightGripClick = ValveIndex_Right_Grip_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightGripForce.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightGripForce Field + +```csharp +public const string RightGripForce = ValveIndex_Right_Grip_Force; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightSystemClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightSystemClick Field + +```csharp +public const string RightSystemClick = ValveIndex_Right_System_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightSystemTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightSystemTouch Field + +```csharp +public const string RightSystemTouch = ValveIndex_Right_System_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightThumbstickClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightThumbstickClick Field + +```csharp +public const string RightThumbstickClick = ValveIndex_Right_Thumbstick_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightThumbstickDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightThumbstickDown Field + +```csharp +public const string RightThumbstickDown = ValveIndex_Right_Thumbstick_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightThumbstickLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightThumbstickLeft Field + +```csharp +public const string RightThumbstickLeft = ValveIndex_Right_Thumbstick_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightThumbstickRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightThumbstickRight Field + +```csharp +public const string RightThumbstickRight = ValveIndex_Right_Thumbstick_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightThumbstickTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightThumbstickTouch Field + +```csharp +public const string RightThumbstickTouch = ValveIndex_Right_Thumbstick_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightThumbstickUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightThumbstickUp Field + +```csharp +public const string RightThumbstickUp = ValveIndex_Right_Thumbstick_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightThumbstickX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightThumbstickX Field + +```csharp +public const string RightThumbstickX = ValveIndex_Right_Thumbstick_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightThumbstickY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightThumbstickY Field + +```csharp +public const string RightThumbstickY = ValveIndex_Right_Thumbstick_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightTrackpadClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightTrackpadClick Field + +```csharp +public const string RightTrackpadClick = ValveIndex_Right_Trackpad_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightTrackpadDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightTrackpadDown Field + +```csharp +public const string RightTrackpadDown = ValveIndex_Right_Trackpad_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightTrackpadForce.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightTrackpadForce Field + +```csharp +public const string RightTrackpadForce = ValveIndex_Right_Trackpad_Force; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightTrackpadLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightTrackpadLeft Field + +```csharp +public const string RightTrackpadLeft = ValveIndex_Right_Trackpad_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightTrackpadRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightTrackpadRight Field + +```csharp +public const string RightTrackpadRight = ValveIndex_Right_Trackpad_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightTrackpadTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightTrackpadTouch Field + +```csharp +public const string RightTrackpadTouch = ValveIndex_Right_Trackpad_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightTrackpadUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightTrackpadUp Field + +```csharp +public const string RightTrackpadUp = ValveIndex_Right_Trackpad_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightTrackpadX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightTrackpadX Field + +```csharp +public const string RightTrackpadX = ValveIndex_Right_Trackpad_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightTrackpadY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightTrackpadY Field + +```csharp +public const string RightTrackpadY = ValveIndex_Right_Trackpad_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightTriggerAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightTriggerAxis Field + +```csharp +public const string RightTriggerAxis = ValveIndex_Right_Trigger_Axis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightTriggerClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightTriggerClick Field + +```csharp +public const string RightTriggerClick = ValveIndex_Right_Trigger_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex-RightTriggerTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +## Keys.ValveIndex.RightTriggerTouch Field + +```csharp +public const string RightTriggerTouch = ValveIndex_Right_Trigger_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-ValveIndex.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.ValveIndex Class +Valve Index +```csharp +public static class Keys.ValveIndex +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.ValveIndex +### Fields +- [LeftAClick](./Keys-ValveIndex-LeftAClick.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftAClick') +- [LeftATouch](./Keys-ValveIndex-LeftATouch.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftATouch') +- [LeftBClick](./Keys-ValveIndex-LeftBClick.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftBClick') +- [LeftBTouch](./Keys-ValveIndex-LeftBTouch.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftBTouch') +- [LeftGripAxis](./Keys-ValveIndex-LeftGripAxis.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftGripAxis') +- [LeftGripClick](./Keys-ValveIndex-LeftGripClick.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftGripClick') +- [LeftGripForce](./Keys-ValveIndex-LeftGripForce.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftGripForce') +- [LeftSystemClick](./Keys-ValveIndex-LeftSystemClick.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftSystemClick') +- [LeftSystemTouch](./Keys-ValveIndex-LeftSystemTouch.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftSystemTouch') +- [LeftThumbstickClick](./Keys-ValveIndex-LeftThumbstickClick.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftThumbstickClick') +- [LeftThumbstickDown](./Keys-ValveIndex-LeftThumbstickDown.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftThumbstickDown') +- [LeftThumbstickLeft](./Keys-ValveIndex-LeftThumbstickLeft.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftThumbstickLeft') +- [LeftThumbstickRight](./Keys-ValveIndex-LeftThumbstickRight.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftThumbstickRight') +- [LeftThumbstickTouch](./Keys-ValveIndex-LeftThumbstickTouch.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftThumbstickTouch') +- [LeftThumbstickUp](./Keys-ValveIndex-LeftThumbstickUp.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftThumbstickUp') +- [LeftThumbstickX](./Keys-ValveIndex-LeftThumbstickX.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftThumbstickX') +- [LeftThumbstickY](./Keys-ValveIndex-LeftThumbstickY.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftThumbstickY') +- [LeftTrackpadClick](./Keys-ValveIndex-LeftTrackpadClick.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftTrackpadClick') +- [LeftTrackpadDown](./Keys-ValveIndex-LeftTrackpadDown.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftTrackpadDown') +- [LeftTrackpadForce](./Keys-ValveIndex-LeftTrackpadForce.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftTrackpadForce') +- [LeftTrackpadLeft](./Keys-ValveIndex-LeftTrackpadLeft.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftTrackpadLeft') +- [LeftTrackpadRight](./Keys-ValveIndex-LeftTrackpadRight.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftTrackpadRight') +- [LeftTrackpadTouch](./Keys-ValveIndex-LeftTrackpadTouch.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftTrackpadTouch') +- [LeftTrackpadUp](./Keys-ValveIndex-LeftTrackpadUp.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftTrackpadUp') +- [LeftTrackpadX](./Keys-ValveIndex-LeftTrackpadX.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftTrackpadX') +- [LeftTrackpadY](./Keys-ValveIndex-LeftTrackpadY.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftTrackpadY') +- [LeftTriggerAxis](./Keys-ValveIndex-LeftTriggerAxis.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftTriggerAxis') +- [LeftTriggerClick](./Keys-ValveIndex-LeftTriggerClick.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftTriggerClick') +- [LeftTriggerTouch](./Keys-ValveIndex-LeftTriggerTouch.md 'UnrealEngine.Framework.Keys.ValveIndex.LeftTriggerTouch') +- [RightAClick](./Keys-ValveIndex-RightAClick.md 'UnrealEngine.Framework.Keys.ValveIndex.RightAClick') +- [RightATouch](./Keys-ValveIndex-RightATouch.md 'UnrealEngine.Framework.Keys.ValveIndex.RightATouch') +- [RightBClick](./Keys-ValveIndex-RightBClick.md 'UnrealEngine.Framework.Keys.ValveIndex.RightBClick') +- [RightBTouch](./Keys-ValveIndex-RightBTouch.md 'UnrealEngine.Framework.Keys.ValveIndex.RightBTouch') +- [RightGripAxis](./Keys-ValveIndex-RightGripAxis.md 'UnrealEngine.Framework.Keys.ValveIndex.RightGripAxis') +- [RightGripClick](./Keys-ValveIndex-RightGripClick.md 'UnrealEngine.Framework.Keys.ValveIndex.RightGripClick') +- [RightGripForce](./Keys-ValveIndex-RightGripForce.md 'UnrealEngine.Framework.Keys.ValveIndex.RightGripForce') +- [RightSystemClick](./Keys-ValveIndex-RightSystemClick.md 'UnrealEngine.Framework.Keys.ValveIndex.RightSystemClick') +- [RightSystemTouch](./Keys-ValveIndex-RightSystemTouch.md 'UnrealEngine.Framework.Keys.ValveIndex.RightSystemTouch') +- [RightThumbstickClick](./Keys-ValveIndex-RightThumbstickClick.md 'UnrealEngine.Framework.Keys.ValveIndex.RightThumbstickClick') +- [RightThumbstickDown](./Keys-ValveIndex-RightThumbstickDown.md 'UnrealEngine.Framework.Keys.ValveIndex.RightThumbstickDown') +- [RightThumbstickLeft](./Keys-ValveIndex-RightThumbstickLeft.md 'UnrealEngine.Framework.Keys.ValveIndex.RightThumbstickLeft') +- [RightThumbstickRight](./Keys-ValveIndex-RightThumbstickRight.md 'UnrealEngine.Framework.Keys.ValveIndex.RightThumbstickRight') +- [RightThumbstickTouch](./Keys-ValveIndex-RightThumbstickTouch.md 'UnrealEngine.Framework.Keys.ValveIndex.RightThumbstickTouch') +- [RightThumbstickUp](./Keys-ValveIndex-RightThumbstickUp.md 'UnrealEngine.Framework.Keys.ValveIndex.RightThumbstickUp') +- [RightThumbstickX](./Keys-ValveIndex-RightThumbstickX.md 'UnrealEngine.Framework.Keys.ValveIndex.RightThumbstickX') +- [RightThumbstickY](./Keys-ValveIndex-RightThumbstickY.md 'UnrealEngine.Framework.Keys.ValveIndex.RightThumbstickY') +- [RightTrackpadClick](./Keys-ValveIndex-RightTrackpadClick.md 'UnrealEngine.Framework.Keys.ValveIndex.RightTrackpadClick') +- [RightTrackpadDown](./Keys-ValveIndex-RightTrackpadDown.md 'UnrealEngine.Framework.Keys.ValveIndex.RightTrackpadDown') +- [RightTrackpadForce](./Keys-ValveIndex-RightTrackpadForce.md 'UnrealEngine.Framework.Keys.ValveIndex.RightTrackpadForce') +- [RightTrackpadLeft](./Keys-ValveIndex-RightTrackpadLeft.md 'UnrealEngine.Framework.Keys.ValveIndex.RightTrackpadLeft') +- [RightTrackpadRight](./Keys-ValveIndex-RightTrackpadRight.md 'UnrealEngine.Framework.Keys.ValveIndex.RightTrackpadRight') +- [RightTrackpadTouch](./Keys-ValveIndex-RightTrackpadTouch.md 'UnrealEngine.Framework.Keys.ValveIndex.RightTrackpadTouch') +- [RightTrackpadUp](./Keys-ValveIndex-RightTrackpadUp.md 'UnrealEngine.Framework.Keys.ValveIndex.RightTrackpadUp') +- [RightTrackpadX](./Keys-ValveIndex-RightTrackpadX.md 'UnrealEngine.Framework.Keys.ValveIndex.RightTrackpadX') +- [RightTrackpadY](./Keys-ValveIndex-RightTrackpadY.md 'UnrealEngine.Framework.Keys.ValveIndex.RightTrackpadY') +- [RightTriggerAxis](./Keys-ValveIndex-RightTriggerAxis.md 'UnrealEngine.Framework.Keys.ValveIndex.RightTriggerAxis') +- [RightTriggerClick](./Keys-ValveIndex-RightTriggerClick.md 'UnrealEngine.Framework.Keys.ValveIndex.RightTriggerClick') +- [RightTriggerTouch](./Keys-ValveIndex-RightTriggerTouch.md 'UnrealEngine.Framework.Keys.ValveIndex.RightTriggerTouch') + +``` + +`API/Keys-Virtual-Acceleration.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.Acceleration Field + +```csharp +public const string Acceleration = Acceleration; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual-Accept.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.Accept Field + +```csharp +public const string Accept = Virtual_Accept; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual-Back.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.Back Field + +```csharp +public const string Back = Virtual_Back; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual-Gravity.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.Gravity Field + +```csharp +public const string Gravity = Gravity; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual-LeftStickDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.LeftStickDown Field + +```csharp +public const string LeftStickDown = Gamepad_LeftStick_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual-LeftStickLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.LeftStickLeft Field + +```csharp +public const string LeftStickLeft = Gamepad_LeftStick_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual-LeftStickRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.LeftStickRight Field + +```csharp +public const string LeftStickRight = Gamepad_LeftStick_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual-LeftStickUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.LeftStickUp Field + +```csharp +public const string LeftStickUp = Gamepad_LeftStick_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual-RightStickDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.RightStickDown Field + +```csharp +public const string RightStickDown = Gamepad_RightStick_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual-RightStickLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.RightStickLeft Field + +```csharp +public const string RightStickLeft = Gamepad_RightStick_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual-RightStickRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.RightStickRight Field + +```csharp +public const string RightStickRight = Gamepad_RightStick_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual-RightStickUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.RightStickUp Field + +```csharp +public const string RightStickUp = Gamepad_RightStick_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual-RotationRate.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.RotationRate Field + +```csharp +public const string RotationRate = RotationRate; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual-Tilt.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +## Keys.Virtual.Tilt Field + +```csharp +public const string Tilt = Tilt; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Virtual.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Virtual Class +Virtual key codes used for input axis button press/release emulation +```csharp +public static class Keys.Virtual +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.Virtual +### Fields +- [Acceleration](./Keys-Virtual-Acceleration.md 'UnrealEngine.Framework.Keys.Virtual.Acceleration') +- [Accept](./Keys-Virtual-Accept.md 'UnrealEngine.Framework.Keys.Virtual.Accept') +- [Back](./Keys-Virtual-Back.md 'UnrealEngine.Framework.Keys.Virtual.Back') +- [Gravity](./Keys-Virtual-Gravity.md 'UnrealEngine.Framework.Keys.Virtual.Gravity') +- [LeftStickDown](./Keys-Virtual-LeftStickDown.md 'UnrealEngine.Framework.Keys.Virtual.LeftStickDown') +- [LeftStickLeft](./Keys-Virtual-LeftStickLeft.md 'UnrealEngine.Framework.Keys.Virtual.LeftStickLeft') +- [LeftStickRight](./Keys-Virtual-LeftStickRight.md 'UnrealEngine.Framework.Keys.Virtual.LeftStickRight') +- [LeftStickUp](./Keys-Virtual-LeftStickUp.md 'UnrealEngine.Framework.Keys.Virtual.LeftStickUp') +- [RightStickDown](./Keys-Virtual-RightStickDown.md 'UnrealEngine.Framework.Keys.Virtual.RightStickDown') +- [RightStickLeft](./Keys-Virtual-RightStickLeft.md 'UnrealEngine.Framework.Keys.Virtual.RightStickLeft') +- [RightStickRight](./Keys-Virtual-RightStickRight.md 'UnrealEngine.Framework.Keys.Virtual.RightStickRight') +- [RightStickUp](./Keys-Virtual-RightStickUp.md 'UnrealEngine.Framework.Keys.Virtual.RightStickUp') +- [RotationRate](./Keys-Virtual-RotationRate.md 'UnrealEngine.Framework.Keys.Virtual.RotationRate') +- [Tilt](./Keys-Virtual-Tilt.md 'UnrealEngine.Framework.Keys.Virtual.Tilt') + +``` + +`API/Keys-Vive-LeftGripClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.LeftGripClick Field + +```csharp +public const string LeftGripClick = Vive_Left_Grip_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-LeftMenuClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.LeftMenuClick Field + +```csharp +public const string LeftMenuClick = Vive_Left_Menu_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-LeftSystemClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.LeftSystemClick Field + +```csharp +public const string LeftSystemClick = Vive_Left_System_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-LeftTrackpadClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.LeftTrackpadClick Field + +```csharp +public const string LeftTrackpadClick = Vive_Left_Trackpad_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-LeftTrackpadDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.LeftTrackpadDown Field + +```csharp +public const string LeftTrackpadDown = Vive_Left_Trackpad_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-LeftTrackpadLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.LeftTrackpadLeft Field + +```csharp +public const string LeftTrackpadLeft = Vive_Left_Trackpad_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-LeftTrackpadRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.LeftTrackpadRight Field + +```csharp +public const string LeftTrackpadRight = Vive_Left_Trackpad_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-LeftTrackpadTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.LeftTrackpadTouch Field + +```csharp +public const string LeftTrackpadTouch = Vive_Left_Trackpad_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-LeftTrackpadUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.LeftTrackpadUp Field + +```csharp +public const string LeftTrackpadUp = Vive_Left_Trackpad_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-LeftTrackpadX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.LeftTrackpadX Field + +```csharp +public const string LeftTrackpadX = Vive_Left_Trackpad_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-LeftTrackpadY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.LeftTrackpadY Field + +```csharp +public const string LeftTrackpadY = Vive_Left_Trackpad_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-LeftTriggerAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.LeftTriggerAxis Field + +```csharp +public const string LeftTriggerAxis = Vive_Left_Trigger_Axis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-LeftTriggerClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.LeftTriggerClick Field + +```csharp +public const string LeftTriggerClick = Vive_Left_Trigger_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-RightGripClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.RightGripClick Field + +```csharp +public const string RightGripClick = Vive_Right_Grip_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-RightMenuClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.RightMenuClick Field + +```csharp +public const string RightMenuClick = Vive_Right_Menu_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-RightSystemClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.RightSystemClick Field + +```csharp +public const string RightSystemClick = Vive_Right_System_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-RightTrackpadClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.RightTrackpadClick Field + +```csharp +public const string RightTrackpadClick = Vive_Right_Trackpad_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-RightTrackpadDown.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.RightTrackpadDown Field + +```csharp +public const string RightTrackpadDown = Vive_Right_Trackpad_Down; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-RightTrackpadLeft.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.RightTrackpadLeft Field + +```csharp +public const string RightTrackpadLeft = Vive_Right_Trackpad_Left; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-RightTrackpadRight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.RightTrackpadRight Field + +```csharp +public const string RightTrackpadRight = Vive_Right_Trackpad_Right; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-RightTrackpadTouch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.RightTrackpadTouch Field + +```csharp +public const string RightTrackpadTouch = Vive_Right_Trackpad_Touch; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-RightTrackpadUp.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.RightTrackpadUp Field + +```csharp +public const string RightTrackpadUp = Vive_Right_Trackpad_Up; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-RightTrackpadX.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.RightTrackpadX Field + +```csharp +public const string RightTrackpadX = Vive_Right_Trackpad_X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-RightTrackpadY.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.RightTrackpadY Field + +```csharp +public const string RightTrackpadY = Vive_Right_Trackpad_Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-RightTriggerAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.RightTriggerAxis Field + +```csharp +public const string RightTriggerAxis = Vive_Right_Trigger_Axis; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive-RightTriggerClick.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +## Keys.Vive.RightTriggerClick Field + +```csharp +public const string RightTriggerClick = Vive_Right_Trigger_Click; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Vive.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Vive Class +HTC Vive +```csharp +public static class Keys.Vive +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.Vive +### Fields +- [LeftGripClick](./Keys-Vive-LeftGripClick.md 'UnrealEngine.Framework.Keys.Vive.LeftGripClick') +- [LeftMenuClick](./Keys-Vive-LeftMenuClick.md 'UnrealEngine.Framework.Keys.Vive.LeftMenuClick') +- [LeftSystemClick](./Keys-Vive-LeftSystemClick.md 'UnrealEngine.Framework.Keys.Vive.LeftSystemClick') +- [LeftTrackpadClick](./Keys-Vive-LeftTrackpadClick.md 'UnrealEngine.Framework.Keys.Vive.LeftTrackpadClick') +- [LeftTrackpadDown](./Keys-Vive-LeftTrackpadDown.md 'UnrealEngine.Framework.Keys.Vive.LeftTrackpadDown') +- [LeftTrackpadLeft](./Keys-Vive-LeftTrackpadLeft.md 'UnrealEngine.Framework.Keys.Vive.LeftTrackpadLeft') +- [LeftTrackpadRight](./Keys-Vive-LeftTrackpadRight.md 'UnrealEngine.Framework.Keys.Vive.LeftTrackpadRight') +- [LeftTrackpadTouch](./Keys-Vive-LeftTrackpadTouch.md 'UnrealEngine.Framework.Keys.Vive.LeftTrackpadTouch') +- [LeftTrackpadUp](./Keys-Vive-LeftTrackpadUp.md 'UnrealEngine.Framework.Keys.Vive.LeftTrackpadUp') +- [LeftTrackpadX](./Keys-Vive-LeftTrackpadX.md 'UnrealEngine.Framework.Keys.Vive.LeftTrackpadX') +- [LeftTrackpadY](./Keys-Vive-LeftTrackpadY.md 'UnrealEngine.Framework.Keys.Vive.LeftTrackpadY') +- [LeftTriggerAxis](./Keys-Vive-LeftTriggerAxis.md 'UnrealEngine.Framework.Keys.Vive.LeftTriggerAxis') +- [LeftTriggerClick](./Keys-Vive-LeftTriggerClick.md 'UnrealEngine.Framework.Keys.Vive.LeftTriggerClick') +- [RightGripClick](./Keys-Vive-RightGripClick.md 'UnrealEngine.Framework.Keys.Vive.RightGripClick') +- [RightMenuClick](./Keys-Vive-RightMenuClick.md 'UnrealEngine.Framework.Keys.Vive.RightMenuClick') +- [RightSystemClick](./Keys-Vive-RightSystemClick.md 'UnrealEngine.Framework.Keys.Vive.RightSystemClick') +- [RightTrackpadClick](./Keys-Vive-RightTrackpadClick.md 'UnrealEngine.Framework.Keys.Vive.RightTrackpadClick') +- [RightTrackpadDown](./Keys-Vive-RightTrackpadDown.md 'UnrealEngine.Framework.Keys.Vive.RightTrackpadDown') +- [RightTrackpadLeft](./Keys-Vive-RightTrackpadLeft.md 'UnrealEngine.Framework.Keys.Vive.RightTrackpadLeft') +- [RightTrackpadRight](./Keys-Vive-RightTrackpadRight.md 'UnrealEngine.Framework.Keys.Vive.RightTrackpadRight') +- [RightTrackpadTouch](./Keys-Vive-RightTrackpadTouch.md 'UnrealEngine.Framework.Keys.Vive.RightTrackpadTouch') +- [RightTrackpadUp](./Keys-Vive-RightTrackpadUp.md 'UnrealEngine.Framework.Keys.Vive.RightTrackpadUp') +- [RightTrackpadX](./Keys-Vive-RightTrackpadX.md 'UnrealEngine.Framework.Keys.Vive.RightTrackpadX') +- [RightTrackpadY](./Keys-Vive-RightTrackpadY.md 'UnrealEngine.Framework.Keys.Vive.RightTrackpadY') +- [RightTriggerAxis](./Keys-Vive-RightTriggerAxis.md 'UnrealEngine.Framework.Keys.Vive.RightTriggerAxis') +- [RightTriggerClick](./Keys-Vive-RightTriggerClick.md 'UnrealEngine.Framework.Keys.Vive.RightTriggerClick') + +``` + +`API/Keys-W.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.W Field + +```csharp +public const string W = W; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-X.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.X Field + +```csharp +public const string X = X; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Xbox-GlobalBack.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Xbox](./Keys-Xbox.md 'UnrealEngine.Framework.Keys.Xbox') +## Keys.Xbox.GlobalBack Field + +```csharp +public const string GlobalBack = Global_Back; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Xbox-GlobalMenu.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Xbox](./Keys-Xbox.md 'UnrealEngine.Framework.Keys.Xbox') +## Keys.Xbox.GlobalMenu Field + +```csharp +public const string GlobalMenu = Global_Menu; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Xbox-GlobalPause.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Xbox](./Keys-Xbox.md 'UnrealEngine.Framework.Keys.Xbox') +## Keys.Xbox.GlobalPause Field + +```csharp +public const string GlobalPause = Global_Pause; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Xbox-GlobalPlay.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Xbox](./Keys-Xbox.md 'UnrealEngine.Framework.Keys.Xbox') +## Keys.Xbox.GlobalPlay Field + +```csharp +public const string GlobalPlay = Global_Play; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Xbox-GlobalView.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys').[Keys.Xbox](./Keys-Xbox.md 'UnrealEngine.Framework.Keys.Xbox') +## Keys.Xbox.GlobalView Field + +```csharp +public const string GlobalView = Global_View; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Xbox.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Xbox Class +Xbox +```csharp +public static class Keys.Xbox +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys.Xbox +### Fields +- [GlobalBack](./Keys-Xbox-GlobalBack.md 'UnrealEngine.Framework.Keys.Xbox.GlobalBack') +- [GlobalMenu](./Keys-Xbox-GlobalMenu.md 'UnrealEngine.Framework.Keys.Xbox.GlobalMenu') +- [GlobalPause](./Keys-Xbox-GlobalPause.md 'UnrealEngine.Framework.Keys.Xbox.GlobalPause') +- [GlobalPlay](./Keys-Xbox-GlobalPlay.md 'UnrealEngine.Framework.Keys.Xbox.GlobalPlay') +- [GlobalView](./Keys-Xbox-GlobalView.md 'UnrealEngine.Framework.Keys.Xbox.GlobalView') + +``` + +`API/Keys-Y.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Y Field + +```csharp +public const string Y = Y; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Z.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Z Field + +```csharp +public const string Z = Z; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys-Zero.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Keys](./Keys.md 'UnrealEngine.Framework.Keys') +## Keys.Zero Field + +```csharp +public const string Zero = Zero; +``` +#### Field Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Keys.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Keys Class +Key codes +```csharp +public static class Keys +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Keys +### Fields +- [A](./Keys-A.md 'UnrealEngine.Framework.Keys.A') +- [Add](./Keys-Add.md 'UnrealEngine.Framework.Keys.Add') +- [Ampersand](./Keys-Ampersand.md 'UnrealEngine.Framework.Keys.Ampersand') +- [AnyKey](./Keys-AnyKey.md 'UnrealEngine.Framework.Keys.AnyKey') +- [Apostrophe](./Keys-Apostrophe.md 'UnrealEngine.Framework.Keys.Apostrophe') +- [Asterix](./Keys-Asterix.md 'UnrealEngine.Framework.Keys.Asterix') +- [B](./Keys-B.md 'UnrealEngine.Framework.Keys.B') +- [Backslash](./Keys-Backslash.md 'UnrealEngine.Framework.Keys.Backslash') +- [BackSpace](./Keys-BackSpace.md 'UnrealEngine.Framework.Keys.BackSpace') +- [C](./Keys-C.md 'UnrealEngine.Framework.Keys.C') +- [CapsLock](./Keys-CapsLock.md 'UnrealEngine.Framework.Keys.CapsLock') +- [Caret](./Keys-Caret.md 'UnrealEngine.Framework.Keys.Caret') +- [Colon](./Keys-Colon.md 'UnrealEngine.Framework.Keys.Colon') +- [Comma](./Keys-Comma.md 'UnrealEngine.Framework.Keys.Comma') +- [D](./Keys-D.md 'UnrealEngine.Framework.Keys.D') +- [Decimal](./Keys-Decimal.md 'UnrealEngine.Framework.Keys.Decimal') +- [Delete](./Keys-Delete.md 'UnrealEngine.Framework.Keys.Delete') +- [Divide](./Keys-Divide.md 'UnrealEngine.Framework.Keys.Divide') +- [Dollar](./Keys-Dollar.md 'UnrealEngine.Framework.Keys.Dollar') +- [Down](./Keys-Down.md 'UnrealEngine.Framework.Keys.Down') +- [E](./Keys-E.md 'UnrealEngine.Framework.Keys.E') +- [Eight](./Keys-Eight.md 'UnrealEngine.Framework.Keys.Eight') +- [End](./Keys-End.md 'UnrealEngine.Framework.Keys.End') +- [Enter](./Keys-Enter.md 'UnrealEngine.Framework.Keys.Enter') +- [Equal](./Keys-Equal.md 'UnrealEngine.Framework.Keys.Equal') +- [Escape](./Keys-Escape.md 'UnrealEngine.Framework.Keys.Escape') +- [Exclamation](./Keys-Exclamation.md 'UnrealEngine.Framework.Keys.Exclamation') +- [F](./Keys-F.md 'UnrealEngine.Framework.Keys.F') +- [F1](./Keys-F1.md 'UnrealEngine.Framework.Keys.F1') +- [F10](./Keys-F10.md 'UnrealEngine.Framework.Keys.F10') +- [F11](./Keys-F11.md 'UnrealEngine.Framework.Keys.F11') +- [F12](./Keys-F12.md 'UnrealEngine.Framework.Keys.F12') +- [F2](./Keys-F2.md 'UnrealEngine.Framework.Keys.F2') +- [F3](./Keys-F3.md 'UnrealEngine.Framework.Keys.F3') +- [F4](./Keys-F4.md 'UnrealEngine.Framework.Keys.F4') +- [F5](./Keys-F5.md 'UnrealEngine.Framework.Keys.F5') +- [F6](./Keys-F6.md 'UnrealEngine.Framework.Keys.F6') +- [F7](./Keys-F7.md 'UnrealEngine.Framework.Keys.F7') +- [F8](./Keys-F8.md 'UnrealEngine.Framework.Keys.F8') +- [F9](./Keys-F9.md 'UnrealEngine.Framework.Keys.F9') +- [Five](./Keys-Five.md 'UnrealEngine.Framework.Keys.Five') +- [Four](./Keys-Four.md 'UnrealEngine.Framework.Keys.Four') +- [G](./Keys-G.md 'UnrealEngine.Framework.Keys.G') +- [H](./Keys-H.md 'UnrealEngine.Framework.Keys.H') +- [Home](./Keys-Home.md 'UnrealEngine.Framework.Keys.Home') +- [Hyphen](./Keys-Hyphen.md 'UnrealEngine.Framework.Keys.Hyphen') +- [I](./Keys-I.md 'UnrealEngine.Framework.Keys.I') +- [Insert](./Keys-Insert.md 'UnrealEngine.Framework.Keys.Insert') +- [Invalid](./Keys-Invalid.md 'UnrealEngine.Framework.Keys.Invalid') +- [J](./Keys-J.md 'UnrealEngine.Framework.Keys.J') +- [K](./Keys-K.md 'UnrealEngine.Framework.Keys.K') +- [L](./Keys-L.md 'UnrealEngine.Framework.Keys.L') +- [Left](./Keys-Left.md 'UnrealEngine.Framework.Keys.Left') +- [LeftAlt](./Keys-LeftAlt.md 'UnrealEngine.Framework.Keys.LeftAlt') +- [LeftBracket](./Keys-LeftBracket.md 'UnrealEngine.Framework.Keys.LeftBracket') +- [LeftCommand](./Keys-LeftCommand.md 'UnrealEngine.Framework.Keys.LeftCommand') +- [LeftControl](./Keys-LeftControl.md 'UnrealEngine.Framework.Keys.LeftControl') +- [LeftMouseButton](./Keys-LeftMouseButton.md 'UnrealEngine.Framework.Keys.LeftMouseButton') +- [LeftParantheses](./Keys-LeftParantheses.md 'UnrealEngine.Framework.Keys.LeftParantheses') +- [LeftShift](./Keys-LeftShift.md 'UnrealEngine.Framework.Keys.LeftShift') +- [M](./Keys-M.md 'UnrealEngine.Framework.Keys.M') +- [MiddleMouseButton](./Keys-MiddleMouseButton.md 'UnrealEngine.Framework.Keys.MiddleMouseButton') +- [MouseScrollDown](./Keys-MouseScrollDown.md 'UnrealEngine.Framework.Keys.MouseScrollDown') +- [MouseScrollUp](./Keys-MouseScrollUp.md 'UnrealEngine.Framework.Keys.MouseScrollUp') +- [MouseWheelAxis](./Keys-MouseWheelAxis.md 'UnrealEngine.Framework.Keys.MouseWheelAxis') +- [MouseX](./Keys-MouseX.md 'UnrealEngine.Framework.Keys.MouseX') +- [MouseY](./Keys-MouseY.md 'UnrealEngine.Framework.Keys.MouseY') +- [Multiply](./Keys-Multiply.md 'UnrealEngine.Framework.Keys.Multiply') +- [N](./Keys-N.md 'UnrealEngine.Framework.Keys.N') +- [Nine](./Keys-Nine.md 'UnrealEngine.Framework.Keys.Nine') +- [NumLock](./Keys-NumLock.md 'UnrealEngine.Framework.Keys.NumLock') +- [NumPadEight](./Keys-NumPadEight.md 'UnrealEngine.Framework.Keys.NumPadEight') +- [NumPadFive](./Keys-NumPadFive.md 'UnrealEngine.Framework.Keys.NumPadFive') +- [NumPadFour](./Keys-NumPadFour.md 'UnrealEngine.Framework.Keys.NumPadFour') +- [NumPadNine](./Keys-NumPadNine.md 'UnrealEngine.Framework.Keys.NumPadNine') +- [NumPadOne](./Keys-NumPadOne.md 'UnrealEngine.Framework.Keys.NumPadOne') +- [NumPadSeven](./Keys-NumPadSeven.md 'UnrealEngine.Framework.Keys.NumPadSeven') +- [NumPadSix](./Keys-NumPadSix.md 'UnrealEngine.Framework.Keys.NumPadSix') +- [NumPadThree](./Keys-NumPadThree.md 'UnrealEngine.Framework.Keys.NumPadThree') +- [NumPadTwo](./Keys-NumPadTwo.md 'UnrealEngine.Framework.Keys.NumPadTwo') +- [NumPadZero](./Keys-NumPadZero.md 'UnrealEngine.Framework.Keys.NumPadZero') +- [O](./Keys-O.md 'UnrealEngine.Framework.Keys.O') +- [One](./Keys-One.md 'UnrealEngine.Framework.Keys.One') +- [P](./Keys-P.md 'UnrealEngine.Framework.Keys.P') +- [PageDown](./Keys-PageDown.md 'UnrealEngine.Framework.Keys.PageDown') +- [PageUp](./Keys-PageUp.md 'UnrealEngine.Framework.Keys.PageUp') +- [Pause](./Keys-Pause.md 'UnrealEngine.Framework.Keys.Pause') +- [Period](./Keys-Period.md 'UnrealEngine.Framework.Keys.Period') +- [Q](./Keys-Q.md 'UnrealEngine.Framework.Keys.Q') +- [Quote](./Keys-Quote.md 'UnrealEngine.Framework.Keys.Quote') +- [R](./Keys-R.md 'UnrealEngine.Framework.Keys.R') +- [Right](./Keys-Right.md 'UnrealEngine.Framework.Keys.Right') +- [RightAlt](./Keys-RightAlt.md 'UnrealEngine.Framework.Keys.RightAlt') +- [RightBracket](./Keys-RightBracket.md 'UnrealEngine.Framework.Keys.RightBracket') +- [RightCommand](./Keys-RightCommand.md 'UnrealEngine.Framework.Keys.RightCommand') +- [RightControl](./Keys-RightControl.md 'UnrealEngine.Framework.Keys.RightControl') +- [RightMouseButton](./Keys-RightMouseButton.md 'UnrealEngine.Framework.Keys.RightMouseButton') +- [RightParantheses](./Keys-RightParantheses.md 'UnrealEngine.Framework.Keys.RightParantheses') +- [RightShift](./Keys-RightShift.md 'UnrealEngine.Framework.Keys.RightShift') +- [S](./Keys-S.md 'UnrealEngine.Framework.Keys.S') +- [ScrollLock](./Keys-ScrollLock.md 'UnrealEngine.Framework.Keys.ScrollLock') +- [Section](./Keys-Section.md 'UnrealEngine.Framework.Keys.Section') +- [Semicolon](./Keys-Semicolon.md 'UnrealEngine.Framework.Keys.Semicolon') +- [Seven](./Keys-Seven.md 'UnrealEngine.Framework.Keys.Seven') +- [Six](./Keys-Six.md 'UnrealEngine.Framework.Keys.Six') +- [Slash](./Keys-Slash.md 'UnrealEngine.Framework.Keys.Slash') +- [SpaceBar](./Keys-SpaceBar.md 'UnrealEngine.Framework.Keys.SpaceBar') +- [Subtract](./Keys-Subtract.md 'UnrealEngine.Framework.Keys.Subtract') +- [T](./Keys-T.md 'UnrealEngine.Framework.Keys.T') +- [Tab](./Keys-Tab.md 'UnrealEngine.Framework.Keys.Tab') +- [Three](./Keys-Three.md 'UnrealEngine.Framework.Keys.Three') +- [ThumbMouseButton](./Keys-ThumbMouseButton.md 'UnrealEngine.Framework.Keys.ThumbMouseButton') +- [ThumbMouseButton2](./Keys-ThumbMouseButton2.md 'UnrealEngine.Framework.Keys.ThumbMouseButton2') +- [Tilde](./Keys-Tilde.md 'UnrealEngine.Framework.Keys.Tilde') +- [Two](./Keys-Two.md 'UnrealEngine.Framework.Keys.Two') +- [U](./Keys-U.md 'UnrealEngine.Framework.Keys.U') +- [Underscore](./Keys-Underscore.md 'UnrealEngine.Framework.Keys.Underscore') +- [Up](./Keys-Up.md 'UnrealEngine.Framework.Keys.Up') +- [V](./Keys-V.md 'UnrealEngine.Framework.Keys.V') +- [W](./Keys-W.md 'UnrealEngine.Framework.Keys.W') +- [X](./Keys-X.md 'UnrealEngine.Framework.Keys.X') +- [Y](./Keys-Y.md 'UnrealEngine.Framework.Keys.Y') +- [Z](./Keys-Z.md 'UnrealEngine.Framework.Keys.Z') +- [Zero](./Keys-Zero.md 'UnrealEngine.Framework.Keys.Zero') +### Classes +- [Keys.Android](./Keys-Android.md 'UnrealEngine.Framework.Keys.Android') +- [Keys.Daydream](./Keys-Daydream.md 'UnrealEngine.Framework.Keys.Daydream') +- [Keys.Gamepad](./Keys-Gamepad.md 'UnrealEngine.Framework.Keys.Gamepad') +- [Keys.Gesture](./Keys-Gesture.md 'UnrealEngine.Framework.Keys.Gesture') +- [Keys.MixedReality](./Keys-MixedReality.md 'UnrealEngine.Framework.Keys.MixedReality') +- [Keys.OculusGo](./Keys-OculusGo.md 'UnrealEngine.Framework.Keys.OculusGo') +- [Keys.OculusTouch](./Keys-OculusTouch.md 'UnrealEngine.Framework.Keys.OculusTouch') +- [Keys.Platform](./Keys-Platform.md 'UnrealEngine.Framework.Keys.Platform') +- [Keys.PS4](./Keys-PS4.md 'UnrealEngine.Framework.Keys.PS4') +- [Keys.Steam](./Keys-Steam.md 'UnrealEngine.Framework.Keys.Steam') +- [Keys.ValveIndex](./Keys-ValveIndex.md 'UnrealEngine.Framework.Keys.ValveIndex') +- [Keys.Virtual](./Keys-Virtual.md 'UnrealEngine.Framework.Keys.Virtual') +- [Keys.Vive](./Keys-Vive.md 'UnrealEngine.Framework.Keys.Vive') +- [Keys.Xbox](./Keys-Xbox.md 'UnrealEngine.Framework.Keys.Xbox') + +``` + +`API/LevelScript.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## LevelScript Class +A representation of the level-wide logic defined in the level blueprint +```csharp +public class LevelScript : UnrealEngine.Framework.Actor +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 LevelScript + +``` + +`API/Light.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Light Class +A light actor that can be placed in a level +```csharp +public abstract class Light : UnrealEngine.Framework.Actor +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 Light + +Derived +↳ [DirectionalLight](./DirectionalLight.md 'UnrealEngine.Framework.DirectionalLight') +↳ [PointLight](./PointLight.md 'UnrealEngine.Framework.PointLight') +↳ [RectLight](./RectLight.md 'UnrealEngine.Framework.RectLight') +↳ [SpotLight](./SpotLight.md 'UnrealEngine.Framework.SpotLight') + +``` + +`API/LightComponent-LightComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LightComponent](./LightComponent.md 'UnrealEngine.Framework.LightComponent') +## LightComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public LightComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/LightComponent-SetIntensity(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LightComponent](./LightComponent.md 'UnrealEngine.Framework.LightComponent') +## LightComponent.SetIntensity(float) Method +Sets the intensity of the light +```csharp +public void SetIntensity(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/LightComponent-SetLightColor(LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LightComponent](./LightComponent.md 'UnrealEngine.Framework.LightComponent') +## LightComponent.SetLightColor(UnrealEngine.Framework.LinearColor) Method +Sets the light color +```csharp +public void SetLightColor(in UnrealEngine.Framework.LinearColor value); +``` +#### Parameters + +`value` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + + +``` + +`API/LightComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## LightComponent Class +A component that represents light +```csharp +public class LightComponent : UnrealEngine.Framework.LightComponentBase +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [LightComponentBase](./LightComponentBase.md 'UnrealEngine.Framework.LightComponentBase') 🡒 LightComponent + +Derived +↳ [DirectionalLightComponent](./DirectionalLightComponent.md 'UnrealEngine.Framework.DirectionalLightComponent') +### Constructors +- [LightComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./LightComponent-LightComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.LightComponent.LightComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Methods +- [SetIntensity(float)](./LightComponent-SetIntensity(float).md 'UnrealEngine.Framework.LightComponent.SetIntensity(float)') +- [SetLightColor(UnrealEngine.Framework.LinearColor)](./LightComponent-SetLightColor(LinearColor).md 'UnrealEngine.Framework.LightComponent.SetLightColor(UnrealEngine.Framework.LinearColor)') + +``` + +`API/LightComponentBase-CastShadows.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LightComponentBase](./LightComponentBase.md 'UnrealEngine.Framework.LightComponentBase') +## LightComponentBase.CastShadows Property +Gets or sets whether the light should cast shadows +```csharp +public bool CastShadows { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/LightComponentBase-Intensity.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LightComponentBase](./LightComponentBase.md 'UnrealEngine.Framework.LightComponentBase') +## LightComponentBase.Intensity Property +Returns the total energy that the light emits +```csharp +public float Intensity { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/LightComponentBase.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## LightComponentBase Class +The base class of light components +```csharp +public abstract class LightComponentBase : UnrealEngine.Framework.SceneComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 LightComponentBase + +Derived +↳ [LightComponent](./LightComponent.md 'UnrealEngine.Framework.LightComponent') +### Properties +- [CastShadows](./LightComponentBase-CastShadows.md 'UnrealEngine.Framework.LightComponentBase.CastShadows') +- [Intensity](./LightComponentBase-Intensity.md 'UnrealEngine.Framework.LightComponentBase.Intensity') + +``` + +`API/LinearColor-A.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.A Property +Gets or sets the alpha component of the linear color +```csharp +public float A { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/LinearColor-Add(LinearColor_LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.Add(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor) Method +Adds two colors +```csharp +public static UnrealEngine.Framework.LinearColor Add(UnrealEngine.Framework.LinearColor left, UnrealEngine.Framework.LinearColor right); +``` +#### Parameters + +`left` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + + +`right` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +#### Returns +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-B.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.B Property +Gets or sets the blue component of the linear color +```csharp +public float B { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/LinearColor-Black.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.Black Property +The black color +```csharp +public static UnrealEngine.Framework.LinearColor Black { get; } +``` +#### Property Value +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-Blue.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.Blue Property +The blue color +```csharp +public static UnrealEngine.Framework.LinearColor Blue { get; } +``` +#### Property Value +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-Divide(LinearColor_LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.Divide(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor) Method +Divides two colors +```csharp +public static UnrealEngine.Framework.LinearColor Divide(UnrealEngine.Framework.LinearColor left, UnrealEngine.Framework.LinearColor right); +``` +#### Parameters + +`left` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + + +`right` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +#### Returns +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-Equals(LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.Equals(UnrealEngine.Framework.LinearColor) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.LinearColor other); +``` +#### Parameters + +`other` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/LinearColor-Equals(object).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.Equals(object) Method +Indicates equality of objects +```csharp +public override bool Equals(object value); +``` +#### Parameters + +`value` [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/LinearColor-FromColor(Color).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.FromColor(System.Drawing.Color) Method +Converts the color into a linear color +```csharp +public static UnrealEngine.Framework.LinearColor FromColor(System.Drawing.Color value); +``` +#### Parameters + +`value` [System.Drawing.Color](https://docs.microsoft.com/en-us/dotnet/api/System.Drawing.Color 'System.Drawing.Color') + +#### Returns +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-G.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.G Property +Gets or sets the green component of the linear color +```csharp +public float G { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/LinearColor-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/LinearColor-Green.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.Green Property +The green color +```csharp +public static UnrealEngine.Framework.LinearColor Green { get; } +``` +#### Property Value +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-Grey.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.Grey Property +The grey color +```csharp +public static UnrealEngine.Framework.LinearColor Grey { get; } +``` +#### Property Value +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-Lerp(LinearColor_LinearColor_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.Lerp(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor, float) Method +Performs a linear interpolation between two colors +```csharp +public static UnrealEngine.Framework.LinearColor Lerp(UnrealEngine.Framework.LinearColor start, UnrealEngine.Framework.LinearColor end, float amount); +``` +#### Parameters + +`start` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + + +`end` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + + +`amount` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-LinearColor(Color).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor(System.Drawing.Color) Constructor +Initializes a new instance the linear color +```csharp +public LinearColor(System.Drawing.Color value); +``` +#### Parameters + +`value` [System.Drawing.Color](https://docs.microsoft.com/en-us/dotnet/api/System.Drawing.Color 'System.Drawing.Color') + + +``` + +`API/LinearColor-LinearColor(Vector3_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor(System.Numerics.Vector3, float) Constructor +Initializes a new instance the linear color +```csharp +public LinearColor(System.Numerics.Vector3 value, float alpha=1f); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`alpha` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/LinearColor-LinearColor(Vector4).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor(System.Numerics.Vector4) Constructor +Initializes a new instance the linear color +```csharp +public LinearColor(System.Numerics.Vector4 value); +``` +#### Parameters + +`value` [System.Numerics.Vector4](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector4 'System.Numerics.Vector4') + + +``` + +`API/LinearColor-LinearColor(float--).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor(float[]) Constructor +Initializes a new instance the linear color +```csharp +public LinearColor(float[] values); +``` +#### Parameters + +`values` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single')[[]](https://docs.microsoft.com/en-us/dotnet/api/System.Array 'System.Array') + + +``` + +`API/LinearColor-LinearColor(float_float_float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor(float, float, float, float) Constructor +Initializes a new instance the linear color +```csharp +public LinearColor(float red, float green, float blue, float alpha=1f); +``` +#### Parameters + +`red` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`green` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`blue` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`alpha` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/LinearColor-Multiply(LinearColor_LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.Multiply(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor) Method +Multiplies two colors +```csharp +public static UnrealEngine.Framework.LinearColor Multiply(UnrealEngine.Framework.LinearColor left, UnrealEngine.Framework.LinearColor right); +``` +#### Parameters + +`left` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + + +`right` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +#### Returns +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-R.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.R Property +Gets or sets the red component of the linear color +```csharp +public float R { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/LinearColor-Red.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.Red Property +The red color +```csharp +public static UnrealEngine.Framework.LinearColor Red { get; } +``` +#### Property Value +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-Subtract(LinearColor_LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.Subtract(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor) Method +Subtracts two colors +```csharp +public static UnrealEngine.Framework.LinearColor Subtract(UnrealEngine.Framework.LinearColor left, UnrealEngine.Framework.LinearColor right); +``` +#### Parameters + +`left` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + + +`right` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +#### Returns +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-ToArray().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.ToArray() Method +Creates an array containing the elements of the linear color +```csharp +public float[] ToArray(); +``` +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single')[[]](https://docs.microsoft.com/en-us/dotnet/api/System.Array 'System.Array') + +``` + +`API/LinearColor-ToString().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.ToString() Method +Returns a string that represents this instance +```csharp +public override string ToString(); +``` +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/LinearColor-ToString(IFormatProvider).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.ToString(System.IFormatProvider) Method +Returns a string that represents this instance +```csharp +public string ToString(System.IFormatProvider formatProvider); +``` +#### Parameters + +`formatProvider` [System.IFormatProvider](https://docs.microsoft.com/en-us/dotnet/api/System.IFormatProvider 'System.IFormatProvider') + +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/LinearColor-ToVector3().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.ToVector3() Method +Converts the linear color into a three component vector +```csharp +public System.Numerics.Vector3 ToVector3(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/LinearColor-ToVector4().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.ToVector4() Method +Converts the linear color into a four component vector +```csharp +public System.Numerics.Vector4 ToVector4(); +``` +#### Returns +[System.Numerics.Vector4](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector4 'System.Numerics.Vector4') + +``` + +`API/LinearColor-White.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.White Property +The white color +```csharp +public static UnrealEngine.Framework.LinearColor White { get; } +``` +#### Property Value +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-Yellow.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.Yellow Property +The yellow color +```csharp +public static UnrealEngine.Framework.LinearColor Yellow { get; } +``` +#### Property Value +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-op_Addition(LinearColor_LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.operator +(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor) Operator +Adds two colors +```csharp +public static UnrealEngine.Framework.LinearColor operator +(UnrealEngine.Framework.LinearColor left, UnrealEngine.Framework.LinearColor right); +``` +#### Parameters + +`left` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + + +`right` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +#### Returns +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-op_Division(float_LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.operator /(float, UnrealEngine.Framework.LinearColor) Operator +Divides two colors +```csharp +public static UnrealEngine.Framework.LinearColor operator /(float scale, UnrealEngine.Framework.LinearColor value); +``` +#### Parameters + +`scale` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`value` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +#### Returns +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-op_Equality(LinearColor_LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.operator ==(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor) Operator +Tests for equality between two objects +```csharp +public static bool operator ==(UnrealEngine.Framework.LinearColor left, UnrealEngine.Framework.LinearColor right); +``` +#### Parameters + +`left` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + + +`right` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/LinearColor-op_ImplicitLinearColor(Color).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.implicit operator LinearColor(System.Drawing.Color) Operator +Implicitly casts color instance to a linear color +```csharp +public static UnrealEngine.Framework.LinearColor implicit operator LinearColor(System.Drawing.Color value); +``` +#### Parameters + +`value` [System.Drawing.Color](https://docs.microsoft.com/en-us/dotnet/api/System.Drawing.Color 'System.Drawing.Color') + +#### Returns +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-op_Implicitstring(LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.implicit operator string(UnrealEngine.Framework.LinearColor) Operator +Implicitly casts this instance to a string +```csharp +public static string implicit operator string(UnrealEngine.Framework.LinearColor value); +``` +#### Parameters + +`value` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/LinearColor-op_Inequality(LinearColor_LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.operator !=(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor) Operator +Tests for inequality between two objects +```csharp +public static bool operator !=(UnrealEngine.Framework.LinearColor left, UnrealEngine.Framework.LinearColor right); +``` +#### Parameters + +`left` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + + +`right` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/LinearColor-op_Multiply(float_LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.operator *(float, UnrealEngine.Framework.LinearColor) Operator +Multiplies two colors +```csharp +public static UnrealEngine.Framework.LinearColor operator *(float scale, UnrealEngine.Framework.LinearColor value); +``` +#### Parameters + +`scale` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`value` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +#### Returns +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-op_Subtraction(LinearColor_LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.operator -(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor) Operator +Subtracts two colors +```csharp +public static UnrealEngine.Framework.LinearColor operator -(UnrealEngine.Framework.LinearColor left, UnrealEngine.Framework.LinearColor right); +``` +#### Parameters + +`left` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + + +`right` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +#### Returns +[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + +``` + +`API/LinearColor-this-int-.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +## LinearColor.this[int] Property +Gets or sets the component at the specified index +```csharp +public float this[int index] { get; set; } +``` +#### Parameters + +`index` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/LinearColor.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## LinearColor Struct +A linear 32-bit floating-point RGBA color +```csharp +public struct LinearColor : +System.IEquatable +``` +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Constructors +- [LinearColor(System.Drawing.Color)](./LinearColor-LinearColor(Color).md 'UnrealEngine.Framework.LinearColor.LinearColor(System.Drawing.Color)') +- [LinearColor(System.Numerics.Vector3, float)](./LinearColor-LinearColor(Vector3_float).md 'UnrealEngine.Framework.LinearColor.LinearColor(System.Numerics.Vector3, float)') +- [LinearColor(System.Numerics.Vector4)](./LinearColor-LinearColor(Vector4).md 'UnrealEngine.Framework.LinearColor.LinearColor(System.Numerics.Vector4)') +- [LinearColor(float, float, float, float)](./LinearColor-LinearColor(float_float_float_float).md 'UnrealEngine.Framework.LinearColor.LinearColor(float, float, float, float)') +- [LinearColor(float[])](./LinearColor-LinearColor(float--).md 'UnrealEngine.Framework.LinearColor.LinearColor(float[])') +### Properties +- [A](./LinearColor-A.md 'UnrealEngine.Framework.LinearColor.A') +- [B](./LinearColor-B.md 'UnrealEngine.Framework.LinearColor.B') +- [Black](./LinearColor-Black.md 'UnrealEngine.Framework.LinearColor.Black') +- [Blue](./LinearColor-Blue.md 'UnrealEngine.Framework.LinearColor.Blue') +- [G](./LinearColor-G.md 'UnrealEngine.Framework.LinearColor.G') +- [Green](./LinearColor-Green.md 'UnrealEngine.Framework.LinearColor.Green') +- [Grey](./LinearColor-Grey.md 'UnrealEngine.Framework.LinearColor.Grey') +- [this[int]](./LinearColor-this-int-.md 'UnrealEngine.Framework.LinearColor.this[int]') +- [R](./LinearColor-R.md 'UnrealEngine.Framework.LinearColor.R') +- [Red](./LinearColor-Red.md 'UnrealEngine.Framework.LinearColor.Red') +- [White](./LinearColor-White.md 'UnrealEngine.Framework.LinearColor.White') +- [Yellow](./LinearColor-Yellow.md 'UnrealEngine.Framework.LinearColor.Yellow') +### Methods +- [Add(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)](./LinearColor-Add(LinearColor_LinearColor).md 'UnrealEngine.Framework.LinearColor.Add(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)') +- [Divide(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)](./LinearColor-Divide(LinearColor_LinearColor).md 'UnrealEngine.Framework.LinearColor.Divide(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)') +- [Equals(object)](./LinearColor-Equals(object).md 'UnrealEngine.Framework.LinearColor.Equals(object)') +- [Equals(UnrealEngine.Framework.LinearColor)](./LinearColor-Equals(LinearColor).md 'UnrealEngine.Framework.LinearColor.Equals(UnrealEngine.Framework.LinearColor)') +- [FromColor(System.Drawing.Color)](./LinearColor-FromColor(Color).md 'UnrealEngine.Framework.LinearColor.FromColor(System.Drawing.Color)') +- [GetHashCode()](./LinearColor-GetHashCode().md 'UnrealEngine.Framework.LinearColor.GetHashCode()') +- [Lerp(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor, float)](./LinearColor-Lerp(LinearColor_LinearColor_float).md 'UnrealEngine.Framework.LinearColor.Lerp(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor, float)') +- [Multiply(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)](./LinearColor-Multiply(LinearColor_LinearColor).md 'UnrealEngine.Framework.LinearColor.Multiply(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)') +- [Subtract(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)](./LinearColor-Subtract(LinearColor_LinearColor).md 'UnrealEngine.Framework.LinearColor.Subtract(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)') +- [ToArray()](./LinearColor-ToArray().md 'UnrealEngine.Framework.LinearColor.ToArray()') +- [ToString()](./LinearColor-ToString().md 'UnrealEngine.Framework.LinearColor.ToString()') +- [ToString(System.IFormatProvider)](./LinearColor-ToString(IFormatProvider).md 'UnrealEngine.Framework.LinearColor.ToString(System.IFormatProvider)') +- [ToVector3()](./LinearColor-ToVector3().md 'UnrealEngine.Framework.LinearColor.ToVector3()') +- [ToVector4()](./LinearColor-ToVector4().md 'UnrealEngine.Framework.LinearColor.ToVector4()') +### Operators +- [operator +(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)](./LinearColor-op_Addition(LinearColor_LinearColor).md 'UnrealEngine.Framework.LinearColor.op_Addition(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)') +- [operator /(float, UnrealEngine.Framework.LinearColor)](./LinearColor-op_Division(float_LinearColor).md 'UnrealEngine.Framework.LinearColor.op_Division(float, UnrealEngine.Framework.LinearColor)') +- [operator ==(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)](./LinearColor-op_Equality(LinearColor_LinearColor).md 'UnrealEngine.Framework.LinearColor.op_Equality(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)') +- [implicit operator LinearColor(System.Drawing.Color)](./LinearColor-op_ImplicitLinearColor(Color).md 'UnrealEngine.Framework.LinearColor.op_Implicit UnrealEngine.Framework.LinearColor(System.Drawing.Color)') +- [implicit operator string(UnrealEngine.Framework.LinearColor)](./LinearColor-op_Implicitstring(LinearColor).md 'UnrealEngine.Framework.LinearColor.op_Implicit string(UnrealEngine.Framework.LinearColor)') +- [operator !=(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)](./LinearColor-op_Inequality(LinearColor_LinearColor).md 'UnrealEngine.Framework.LinearColor.op_Inequality(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)') +- [operator *(float, UnrealEngine.Framework.LinearColor)](./LinearColor-op_Multiply(float_LinearColor).md 'UnrealEngine.Framework.LinearColor.op_Multiply(float, UnrealEngine.Framework.LinearColor)') +- [operator -(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)](./LinearColor-op_Subtraction(LinearColor_LinearColor).md 'UnrealEngine.Framework.LinearColor.op_Subtraction(UnrealEngine.Framework.LinearColor, UnrealEngine.Framework.LinearColor)') + +``` + +`API/LogLevel.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## LogLevel Enum +Defines the log level for an output log message +```csharp +public enum LogLevel +``` +### Fields + +`Display` 0 +Logs are printed to console and log files + + +`Warning` 1 +Logs are printed to console and log files with the yellow color + + +`Error` 2 +Logs are printed to console and log files with the red color + + +`Fatal` 3 +Logs are printed to console and log files then crashes the application even if logging is disabled + + +``` + +`API/Material-IsDefaultMaterial.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Material](./Material.md 'UnrealEngine.Framework.Material') +## Material.IsDefaultMaterial Property +Returns `true` if the material is one of the default materials +```csharp +public bool IsDefaultMaterial { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Material-Load(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Material](./Material.md 'UnrealEngine.Framework.Material') +## Material.Load(string) Method +Finds and loads a material by name +```csharp +public static UnrealEngine.Framework.Material Load(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[Material](./Material.md 'UnrealEngine.Framework.Material') +A material or `null` on failure + +``` + +`API/Material.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Material Class +An asset which can be applied to a mesh to control the visual look +```csharp +public class Material : UnrealEngine.Framework.MaterialInterface +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [MaterialInterface](./MaterialInterface.md 'UnrealEngine.Framework.MaterialInterface') 🡒 Material +### Properties +- [IsDefaultMaterial](./Material-IsDefaultMaterial.md 'UnrealEngine.Framework.Material.IsDefaultMaterial') +### Methods +- [Load(string)](./Material-Load(string).md 'UnrealEngine.Framework.Material.Load(string)') + +``` + +`API/MaterialInstance-GetParent().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MaterialInstance](./MaterialInstance.md 'UnrealEngine.Framework.MaterialInstance') +## MaterialInstance.GetParent() Method +Returns the parent material or `null` on failure +```csharp +public UnrealEngine.Framework.MaterialInstanceDynamic GetParent(); +``` +#### Returns +[MaterialInstanceDynamic](./MaterialInstanceDynamic.md 'UnrealEngine.Framework.MaterialInstanceDynamic') + +``` + +`API/MaterialInstance-IsChildOf(MaterialInterface).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MaterialInstance](./MaterialInstance.md 'UnrealEngine.Framework.MaterialInstance') +## MaterialInstance.IsChildOf(UnrealEngine.Framework.MaterialInterface) Method +Returns `true` if the material instance is a child of another Material +```csharp +public bool IsChildOf(UnrealEngine.Framework.MaterialInterface material); +``` +#### Parameters + +`material` [MaterialInterface](./MaterialInterface.md 'UnrealEngine.Framework.MaterialInterface') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/MaterialInstance.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## MaterialInstance Class +An abstract instance of the material +```csharp +public abstract class MaterialInstance : UnrealEngine.Framework.MaterialInterface +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [MaterialInterface](./MaterialInterface.md 'UnrealEngine.Framework.MaterialInterface') 🡒 MaterialInstance + +Derived +↳ [MaterialInstanceDynamic](./MaterialInstanceDynamic.md 'UnrealEngine.Framework.MaterialInstanceDynamic') +### Methods +- [GetParent()](./MaterialInstance-GetParent().md 'UnrealEngine.Framework.MaterialInstance.GetParent()') +- [IsChildOf(UnrealEngine.Framework.MaterialInterface)](./MaterialInstance-IsChildOf(MaterialInterface).md 'UnrealEngine.Framework.MaterialInstance.IsChildOf(UnrealEngine.Framework.MaterialInterface)') + +``` + +`API/MaterialInstanceDynamic-ClearParameterValues().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MaterialInstanceDynamic](./MaterialInstanceDynamic.md 'UnrealEngine.Framework.MaterialInstanceDynamic') +## MaterialInstanceDynamic.ClearParameterValues() Method +Removes all parameter values +```csharp +public void ClearParameterValues(); +``` + +``` + +`API/MaterialInstanceDynamic-SetScalarParameterValue(string_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MaterialInstanceDynamic](./MaterialInstanceDynamic.md 'UnrealEngine.Framework.MaterialInstanceDynamic') +## MaterialInstanceDynamic.SetScalarParameterValue(string, float) Method +Sets the scalar parameter value +```csharp +public void SetScalarParameterValue(string parameterName, float value); +``` +#### Parameters + +`parameterName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/MaterialInstanceDynamic-SetTextureParameterValue(string_Texture).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MaterialInstanceDynamic](./MaterialInstanceDynamic.md 'UnrealEngine.Framework.MaterialInstanceDynamic') +## MaterialInstanceDynamic.SetTextureParameterValue(string, UnrealEngine.Framework.Texture) Method +Sets the texture parameter value +```csharp +public void SetTextureParameterValue(string parameterName, UnrealEngine.Framework.Texture value); +``` +#### Parameters + +`parameterName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [Texture](./Texture.md 'UnrealEngine.Framework.Texture') + + +``` + +`API/MaterialInstanceDynamic-SetVectorParameterValue(string_LinearColor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MaterialInstanceDynamic](./MaterialInstanceDynamic.md 'UnrealEngine.Framework.MaterialInstanceDynamic') +## MaterialInstanceDynamic.SetVectorParameterValue(string, UnrealEngine.Framework.LinearColor) Method +Sets the vector parameter value +```csharp +public void SetVectorParameterValue(string parameterName, in UnrealEngine.Framework.LinearColor value); +``` +#### Parameters + +`parameterName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') + + +``` + +`API/MaterialInstanceDynamic.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## MaterialInstanceDynamic Class +A dynamic instance of the material +```csharp +public class MaterialInstanceDynamic : UnrealEngine.Framework.MaterialInstance +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [MaterialInterface](./MaterialInterface.md 'UnrealEngine.Framework.MaterialInterface') 🡒 [MaterialInstance](./MaterialInstance.md 'UnrealEngine.Framework.MaterialInstance') 🡒 MaterialInstanceDynamic +### Methods +- [ClearParameterValues()](./MaterialInstanceDynamic-ClearParameterValues().md 'UnrealEngine.Framework.MaterialInstanceDynamic.ClearParameterValues()') +- [SetScalarParameterValue(string, float)](./MaterialInstanceDynamic-SetScalarParameterValue(string_float).md 'UnrealEngine.Framework.MaterialInstanceDynamic.SetScalarParameterValue(string, float)') +- [SetTextureParameterValue(string, UnrealEngine.Framework.Texture)](./MaterialInstanceDynamic-SetTextureParameterValue(string_Texture).md 'UnrealEngine.Framework.MaterialInstanceDynamic.SetTextureParameterValue(string, UnrealEngine.Framework.Texture)') +- [SetVectorParameterValue(string, UnrealEngine.Framework.LinearColor)](./MaterialInstanceDynamic-SetVectorParameterValue(string_LinearColor).md 'UnrealEngine.Framework.MaterialInstanceDynamic.SetVectorParameterValue(string, UnrealEngine.Framework.LinearColor)') + +``` + +`API/MaterialInterface-Equals(MaterialInterface).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MaterialInterface](./MaterialInterface.md 'UnrealEngine.Framework.MaterialInterface') +## MaterialInterface.Equals(UnrealEngine.Framework.MaterialInterface) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.MaterialInterface other); +``` +#### Parameters + +`other` [MaterialInterface](./MaterialInterface.md 'UnrealEngine.Framework.MaterialInterface') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/MaterialInterface-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MaterialInterface](./MaterialInterface.md 'UnrealEngine.Framework.MaterialInterface') +## MaterialInterface.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/MaterialInterface-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MaterialInterface](./MaterialInterface.md 'UnrealEngine.Framework.MaterialInterface') +## MaterialInterface.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/MaterialInterface-IsTwoSided.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MaterialInterface](./MaterialInterface.md 'UnrealEngine.Framework.MaterialInterface') +## MaterialInterface.IsTwoSided Property +Returns `true` if the material is two sided +```csharp +public bool IsTwoSided { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/MaterialInterface.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## MaterialInterface Class +The base class of materials that can be applied to meshes +```csharp +public abstract class MaterialInterface : +System.IEquatable +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 MaterialInterface + +Derived +↳ [Material](./Material.md 'UnrealEngine.Framework.Material') +↳ [MaterialInstance](./MaterialInstance.md 'UnrealEngine.Framework.MaterialInstance') + +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[MaterialInterface](./MaterialInterface.md 'UnrealEngine.Framework.MaterialInterface')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [IsCreated](./MaterialInterface-IsCreated.md 'UnrealEngine.Framework.MaterialInterface.IsCreated') +- [IsTwoSided](./MaterialInterface-IsTwoSided.md 'UnrealEngine.Framework.MaterialInterface.IsTwoSided') +### Methods +- [Equals(UnrealEngine.Framework.MaterialInterface)](./MaterialInterface-Equals(MaterialInterface).md 'UnrealEngine.Framework.MaterialInterface.Equals(UnrealEngine.Framework.MaterialInterface)') +- [GetHashCode()](./MaterialInterface-GetHashCode().md 'UnrealEngine.Framework.MaterialInterface.GetHashCode()') + +``` + +`API/Maths-Angle(Quaternion_Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Angle(System.Numerics.Quaternion, System.Numerics.Quaternion) Method +Returns the unsigned angle in degrees +```csharp +public static float Angle(System.Numerics.Quaternion from, System.Numerics.Quaternion to); +``` +#### Parameters + +`from` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`to` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-Angle(Vector2_Vector2).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Angle(System.Numerics.Vector2, System.Numerics.Vector2) Method +Returns the unsigned angle in degrees +```csharp +public static float Angle(System.Numerics.Vector2 left, System.Numerics.Vector2 right); +``` +#### Parameters + +`left` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + + +`right` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-Angle(Vector3_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Angle(System.Numerics.Vector3, System.Numerics.Vector3) Method +Returns the unsigned angle in degrees +```csharp +public static float Angle(System.Numerics.Vector3 left, System.Numerics.Vector3 right); +``` +#### Parameters + +`left` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`right` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-AngleAxis(float_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.AngleAxis(float, System.Numerics.Vector3) Method +Returns a rotation which rotates angle degrees around axis +```csharp +public static System.Numerics.Quaternion AngleAxis(float angle, System.Numerics.Vector3 axis); +``` +#### Parameters + +`angle` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`axis` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/Maths-ClampMagnitude(Vector2_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.ClampMagnitude(System.Numerics.Vector2, float) Method +Returns a copy of vector with clamped magnitude +```csharp +public static System.Numerics.Vector2 ClampMagnitude(System.Numerics.Vector2 value, float maxLength); +``` +#### Parameters + +`value` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + + +`maxLength` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + +``` + +`API/Maths-ClampMagnitude(Vector3_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.ClampMagnitude(System.Numerics.Vector3, float) Method +Returns a copy of vector with clamped magnitude +```csharp +public static System.Numerics.Vector3 ClampMagnitude(System.Numerics.Vector3 value, float maxLength); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`maxLength` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Maths-CreateFromYawPitchRoll(float_float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.CreateFromYawPitchRoll(float, float, float) Method +Returns a rotation from the given yaw, pitch, and roll, in radians +```csharp +public static System.Numerics.Quaternion CreateFromYawPitchRoll(float yaw, float pitch, float roll); +``` +#### Parameters + +`yaw` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`pitch` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`roll` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/Maths-Damp(Quaternion_Quaternion_float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Damp(System.Numerics.Quaternion, System.Numerics.Quaternion, float, float) Method +Creates framerate-independent dampened motion between two values +```csharp +public static System.Numerics.Quaternion Damp(System.Numerics.Quaternion from, System.Numerics.Quaternion to, float lambda, float deltaTime); +``` +#### Parameters + +`from` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') +Value to damp from + + +`to` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') +Value to damp to + + +`lambda` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Smoothing factor + + +`deltaTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Time since last damp + +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/Maths-Damp(Vector2_Vector2_float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Damp(System.Numerics.Vector2, System.Numerics.Vector2, float, float) Method +Creates framerate-independent dampened motion between two values +```csharp +public static System.Numerics.Vector2 Damp(System.Numerics.Vector2 from, System.Numerics.Vector2 to, float lambda, float deltaTime); +``` +#### Parameters + +`from` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') +Value to damp from + + +`to` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') +Value to damp to + + +`lambda` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Smoothing factor + + +`deltaTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Time since last damp + +#### Returns +[System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + +``` + +`API/Maths-Damp(Vector3_Vector3_float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Damp(System.Numerics.Vector3, System.Numerics.Vector3, float, float) Method +Creates framerate-independent dampened motion between two values +```csharp +public static System.Numerics.Vector3 Damp(System.Numerics.Vector3 from, System.Numerics.Vector3 to, float lambda, float deltaTime); +``` +#### Parameters + +`from` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Value to damp from + + +`to` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Value to damp to + + +`lambda` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Smoothing factor + + +`deltaTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Time since last damp + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Maths-Damp(double_double_double_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Damp(double, double, double, double) Method +Creates framerate-independent dampened motion between two values +```csharp +public static double Damp(double from, double to, double lambda, double deltaTime); +``` +#### Parameters + +`from` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Value to damp from + + +`to` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Value to damp to + + +`lambda` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Smoothing factor + + +`deltaTime` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Time since last damp + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-Damp(float_float_float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Damp(float, float, float, float) Method +Creates framerate-independent dampened motion between two values +```csharp +public static float Damp(float from, float to, float lambda, float deltaTime); +``` +#### Parameters + +`from` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Value to damp from + + +`to` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Value to damp to + + +`lambda` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Smoothing factor + + +`deltaTime` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Time since last damp + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-DegToRadF.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.DegToRadF Field +Degrees-to-radians conversion constant +```csharp +public const float DegToRadF = 0,01745329; +``` +#### Field Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-DeltaAngle(double_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.DeltaAngle(double, double) Method +Calculates the shortest difference between the two given angles given in degrees +```csharp +public static double DeltaAngle(double current, double target); +``` +#### Parameters + +`current` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + + +`target` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-DeltaAngle(float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.DeltaAngle(float, float) Method +Calculates the shortest difference between the two given angles given in degrees +```csharp +public static float DeltaAngle(float current, float target); +``` +#### Parameters + +`current` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`target` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-Dot(double_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Dot(double, double) Method +Returns the dot product of two float values +```csharp +public static double Dot(double left, double right); +``` +#### Parameters + +`left` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + + +`right` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-Dot(float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Dot(float, float) Method +Returns the dot product of two float values +```csharp +public static float Dot(float left, float right); +``` +#### Parameters + +`left` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`right` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-Euler(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Euler(System.Numerics.Vector3) Method +Returns a rotation which rotates z degrees around the z axis, x degrees around the x axis, and y degrees around the y axis +```csharp +public static System.Numerics.Quaternion Euler(System.Numerics.Vector3 eulerAngles); +``` +#### Parameters + +`eulerAngles` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/Maths-Euler(float_float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Euler(float, float, float) Method +Returns a rotation which rotates z degrees around the z axis, x degrees around the x axis, and y degrees around the y axis +```csharp +public static System.Numerics.Quaternion Euler(float x, float y, float z); +``` +#### Parameters + +`x` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`y` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`z` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/Maths-Fraction(double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Fraction(double) Method +Returns the fractional part of a float value +```csharp +public static double Fraction(double value); +``` +#### Parameters + +`value` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-Fraction(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Fraction(float) Method +Returns the fractional part of a float value +```csharp +public static float Fraction(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-FromToRotation(Vector3_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.FromToRotation(System.Numerics.Vector3, System.Numerics.Vector3) Method +Returns a rotation which rotates from [fromDirection](#UnrealEngine-Framework-Maths-FromToRotation(System-Numerics-Vector3_System-Numerics-Vector3)-fromDirection 'UnrealEngine.Framework.Maths.FromToRotation(System.Numerics.Vector3, System.Numerics.Vector3).fromDirection') to [toDirection](#UnrealEngine-Framework-Maths-FromToRotation(System-Numerics-Vector3_System-Numerics-Vector3)-toDirection 'UnrealEngine.Framework.Maths.FromToRotation(System.Numerics.Vector3, System.Numerics.Vector3).toDirection') +```csharp +public static System.Numerics.Quaternion FromToRotation(System.Numerics.Vector3 fromDirection, System.Numerics.Vector3 toDirection); +``` +#### Parameters + +`fromDirection` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`toDirection` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/Maths-InverseLerp(double_double_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.InverseLerp(double, double, double) Method +Inverse-interpolates between two values linearly +```csharp +public static double InverseLerp(double from, double to, double amount); +``` +#### Parameters + +`from` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Value to interpolate from + + +`to` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Value to interpolate to + + +`amount` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Interpolation amount + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-InverseLerp(float_float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.InverseLerp(float, float, float) Method +Inverse-interpolates between two values linearly +```csharp +public static float InverseLerp(float from, float to, float amount); +``` +#### Parameters + +`from` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Value to interpolate from + + +`to` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Value to interpolate to + + +`amount` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Interpolation amount + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-Lerp(double_double_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Lerp(double, double, double) Method +Interpolates between two values linearly +```csharp +public static double Lerp(double from, double to, double amount); +``` +#### Parameters + +`from` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Value to interpolate from + + +`to` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Value to interpolate to + + +`amount` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Interpolation amount + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-Lerp(float_float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Lerp(float, float, float) Method +Interpolates between two values linearly +```csharp +public static float Lerp(float from, float to, float amount); +``` +#### Parameters + +`from` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Value to interpolate from + + +`to` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Value to interpolate to + + +`amount` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Interpolation amount + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-LerpAngle(double_double_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.LerpAngle(double, double, double) Method +Interpolates between two values linearly, but makes sure the values calculated correctly when they wrap around 360 degrees +```csharp +public static double LerpAngle(double from, double to, double amount); +``` +#### Parameters + +`from` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Value to interpolate from + + +`to` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Value to interpolate to + + +`amount` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Interpolation amount + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-LerpAngle(float_float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.LerpAngle(float, float, float) Method +Interpolates between two values linearly, but makes sure the values calculated correctly when they wrap around 360 degrees +```csharp +public static float LerpAngle(float from, float to, float amount); +``` +#### Parameters + +`from` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Value to interpolate from + + +`to` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Value to interpolate to + + +`amount` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Interpolation amount + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-Magnitude(Vector2).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Magnitude(System.Numerics.Vector2) Method +Returns the length of the vector +```csharp +public static float Magnitude(System.Numerics.Vector2 value); +``` +#### Parameters + +`value` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-Magnitude(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Magnitude(System.Numerics.Vector3) Method +Returns the length of the vector +```csharp +public static float Magnitude(System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-Magnitude(Vector4).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Magnitude(System.Numerics.Vector4) Method +Returns the length of the vector +```csharp +public static float Magnitude(System.Numerics.Vector4 value); +``` +#### Parameters + +`value` [System.Numerics.Vector4](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector4 'System.Numerics.Vector4') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-MoveTowards(Vector2_Vector2_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.MoveTowards(System.Numerics.Vector2, System.Numerics.Vector2, float) Method +Returns the vector moved towards a target +```csharp +public static System.Numerics.Vector2 MoveTowards(System.Numerics.Vector2 current, System.Numerics.Vector2 target, float maxDistanceDelta); +``` +#### Parameters + +`current` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + + +`target` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + + +`maxDistanceDelta` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + +``` + +`API/Maths-MoveTowards(Vector3_Vector3_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.MoveTowards(System.Numerics.Vector3, System.Numerics.Vector3, float) Method +Returns the vector moved towards a target +```csharp +public static System.Numerics.Vector3 MoveTowards(System.Numerics.Vector3 current, System.Numerics.Vector3 target, float maxDistanceDelta); +``` +#### Parameters + +`current` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`target` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`maxDistanceDelta` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Maths-MoveTowards(double_double_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.MoveTowards(double, double, double) Method +Returns the vector moved towards a target +```csharp +public static double MoveTowards(double current, double target, double maxDelta); +``` +#### Parameters + +`current` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + + +`target` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + + +`maxDelta` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-MoveTowards(float_float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.MoveTowards(float, float, float) Method +Returns the vector moved towards a target +```csharp +public static float MoveTowards(float current, float target, float maxDelta); +``` +#### Parameters + +`current` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`target` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`maxDelta` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-MoveTowardsAngle(double_double_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.MoveTowardsAngle(double, double, double) Method +Returns the vector moved towards a target, but makes sure the values calculated correctly when they wrap around 360 degrees +```csharp +public static double MoveTowardsAngle(double current, double target, double maxDelta); +``` +#### Parameters + +`current` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + + +`target` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + + +`maxDelta` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-MoveTowardsAngle(float_float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.MoveTowardsAngle(float, float, float) Method +Returns the vector moved towards a target, but makes sure the values calculated correctly when they wrap around 360 degrees +```csharp +public static float MoveTowardsAngle(float current, float target, float maxDelta); +``` +#### Parameters + +`current` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`target` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`maxDelta` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-NextPowerOfTwo(double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.NextPowerOfTwo(double) Method +Returns the next power of two +```csharp +public static double NextPowerOfTwo(double value); +``` +#### Parameters + +`value` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-NextPowerOfTwo(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.NextPowerOfTwo(float) Method +Returns the next power of two +```csharp +public static float NextPowerOfTwo(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-Perpendicular(Vector2).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Perpendicular(System.Numerics.Vector2) Method +Returns the vector perpendicular to the specified vector +```csharp +public static System.Numerics.Vector2 Perpendicular(System.Numerics.Vector2 value); +``` +#### Parameters + +`value` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + +#### Returns +[System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + +``` + +`API/Maths-PreviousPowerOfTwo(double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.PreviousPowerOfTwo(double) Method +Returns the previous power of two +```csharp +public static double PreviousPowerOfTwo(double value); +``` +#### Parameters + +`value` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-PreviousPowerOfTwo(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.PreviousPowerOfTwo(float) Method +Returns the previous power of two +```csharp +public static float PreviousPowerOfTwo(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-Project(Vector3_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Project(System.Numerics.Vector3, System.Numerics.Vector3) Method +Projects a vector onto another vector +```csharp +public static System.Numerics.Vector3 Project(System.Numerics.Vector3 value, System.Numerics.Vector3 normal); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`normal` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Maths-ProjectOnPlane(Vector3_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.ProjectOnPlane(System.Numerics.Vector3, System.Numerics.Vector3) Method +Projects a vector onto a plane defined by a normal orthogonal to the plane +```csharp +public static System.Numerics.Vector3 ProjectOnPlane(System.Numerics.Vector3 value, System.Numerics.Vector3 planeNormal); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`planeNormal` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Maths-RadToDegF.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.RadToDegF Field +Radians-to-degrees conversion constant +```csharp +public const float RadToDegF = 57,29578; +``` +#### Field Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-Refract(Vector2_Vector2_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Refract(System.Numerics.Vector2, System.Numerics.Vector2, float) Method +Returns the refraction vector +```csharp +public static System.Numerics.Vector2 Refract(System.Numerics.Vector2 value, System.Numerics.Vector2 normal, float eta); +``` +#### Parameters + +`value` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') +The incident vector + + +`normal` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') +The normal vector + + +`eta` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +The refraction index + +#### Returns +[System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + +``` + +`API/Maths-Refract(Vector3_Vector3_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Refract(System.Numerics.Vector3, System.Numerics.Vector3, float) Method +Returns the refraction vector +```csharp +public static System.Numerics.Vector3 Refract(System.Numerics.Vector3 value, System.Numerics.Vector3 normal, float eta); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +The incident vector + + +`normal` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +The normal vector + + +`eta` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +The refraction index + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Maths-Refract(Vector4_Vector4_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Refract(System.Numerics.Vector4, System.Numerics.Vector4, float) Method +Returns the refraction vector +```csharp +public static System.Numerics.Vector4 Refract(System.Numerics.Vector4 value, System.Numerics.Vector4 normal, float eta); +``` +#### Parameters + +`value` [System.Numerics.Vector4](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector4 'System.Numerics.Vector4') +The incident vector + + +`normal` [System.Numerics.Vector4](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector4 'System.Numerics.Vector4') +The normal vector + + +`eta` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +The refraction index + +#### Returns +[System.Numerics.Vector4](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector4 'System.Numerics.Vector4') + +``` + +`API/Maths-Repeat(double_double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Repeat(double, double) Method +Loops the value so that it is never larger than length and never smaller than 0.0d +```csharp +public static double Repeat(double value, double length); +``` +#### Parameters + +`value` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + + +`length` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-Repeat(float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Repeat(float, float) Method +Loops the value so that it is never larger than length and never smaller than 0.0f +```csharp +public static float Repeat(float value, float length); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`length` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-RotateTowards(Quaternion_Quaternion_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.RotateTowards(System.Numerics.Quaternion, System.Numerics.Quaternion, float) Method +Returns a rotation which rotated towards a target +```csharp +public static System.Numerics.Quaternion RotateTowards(System.Numerics.Quaternion from, System.Numerics.Quaternion to, float maxDegreesDelta); +``` +#### Parameters + +`from` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`to` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`maxDegreesDelta` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/Maths-Saturate(double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Saturate(double) Method +Clamps value between 0.0d and 1.0d +```csharp +public static double Saturate(double value); +``` +#### Parameters + +`value` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-Saturate(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.Saturate(float) Method +Clamps value between 0.0f and 1.0f +```csharp +public static float Saturate(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-SignedAngle(Vector3_Vector3_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.SignedAngle(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Vector3) Method +Returns the signed angle in degrees +```csharp +public static float SignedAngle(System.Numerics.Vector3 left, System.Numerics.Vector3 right, System.Numerics.Vector3 axis); +``` +#### Parameters + +`left` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`right` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`axis` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-SmoothStep(double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.SmoothStep(double) Method +Performs smooth (Cubic Hermite) interpolation between 0.0d and 1.0d +```csharp +public static double SmoothStep(double amount); +``` +#### Parameters + +`amount` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +Value between 0.0d and 1.0d indicating interpolation amount + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-SmoothStep(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.SmoothStep(float) Method +Performs smooth (Cubic Hermite) interpolation between 0.0f and 1.0f +```csharp +public static float SmoothStep(float amount); +``` +#### Parameters + +`amount` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Value between 0.0f and 1.0f indicating interpolation amount + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-SmootherStep(double).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.SmootherStep(double) Method +Performs a smoother interpolation between 0.0d and 1.0d with 1st and 2nd order derivatives of zero at endpoints +```csharp +public static double SmootherStep(double amount); +``` +#### Parameters + +`amount` [System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') +>Value between 0.0d and 1.0d indicating interpolation amount + +#### Returns +[System.Double](https://docs.microsoft.com/en-us/dotnet/api/System.Double 'System.Double') + +``` + +`API/Maths-SmootherStep(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.SmootherStep(float) Method +Performs a smoother interpolation between 0.0f and 1.0f with 1st and 2nd order derivatives of zero at endpoints +```csharp +public static float SmootherStep(float amount); +``` +#### Parameters + +`amount` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +>Value between 0.0f and 1.0f indicating interpolation amount + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-SquareMagnitude(Vector2).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.SquareMagnitude(System.Numerics.Vector2) Method +Returns the squared length of the vector +```csharp +public static float SquareMagnitude(System.Numerics.Vector2 value); +``` +#### Parameters + +`value` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-SquareMagnitude(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.SquareMagnitude(System.Numerics.Vector3) Method +Returns the squared length of the vector +```csharp +public static float SquareMagnitude(System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths-SquareMagnitude(Vector4).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Maths](./Maths.md 'UnrealEngine.Framework.Maths') +## Maths.SquareMagnitude(System.Numerics.Vector4) Method +Returns the squared length of the vector +```csharp +public static float SquareMagnitude(System.Numerics.Vector4 value); +``` +#### Parameters + +`value` [System.Numerics.Vector4](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector4 'System.Numerics.Vector4') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/Maths.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Maths Class +Provides additional static constants and methods for mathematical functions that are lack in [System.Math](https://docs.microsoft.com/en-us/dotnet/api/System.Math 'System.Math'), [System.MathF](https://docs.microsoft.com/en-us/dotnet/api/System.MathF 'System.MathF'), and [System.Numerics](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics 'System.Numerics') +```csharp +public static class Maths +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Maths +### Fields +- [DegToRadF](./Maths-DegToRadF.md 'UnrealEngine.Framework.Maths.DegToRadF') +- [RadToDegF](./Maths-RadToDegF.md 'UnrealEngine.Framework.Maths.RadToDegF') +### Methods +- [Angle(System.Numerics.Quaternion, System.Numerics.Quaternion)](./Maths-Angle(Quaternion_Quaternion).md 'UnrealEngine.Framework.Maths.Angle(System.Numerics.Quaternion, System.Numerics.Quaternion)') +- [Angle(System.Numerics.Vector2, System.Numerics.Vector2)](./Maths-Angle(Vector2_Vector2).md 'UnrealEngine.Framework.Maths.Angle(System.Numerics.Vector2, System.Numerics.Vector2)') +- [Angle(System.Numerics.Vector3, System.Numerics.Vector3)](./Maths-Angle(Vector3_Vector3).md 'UnrealEngine.Framework.Maths.Angle(System.Numerics.Vector3, System.Numerics.Vector3)') +- [AngleAxis(float, System.Numerics.Vector3)](./Maths-AngleAxis(float_Vector3).md 'UnrealEngine.Framework.Maths.AngleAxis(float, System.Numerics.Vector3)') +- [ClampMagnitude(System.Numerics.Vector2, float)](./Maths-ClampMagnitude(Vector2_float).md 'UnrealEngine.Framework.Maths.ClampMagnitude(System.Numerics.Vector2, float)') +- [ClampMagnitude(System.Numerics.Vector3, float)](./Maths-ClampMagnitude(Vector3_float).md 'UnrealEngine.Framework.Maths.ClampMagnitude(System.Numerics.Vector3, float)') +- [CreateFromYawPitchRoll(float, float, float)](./Maths-CreateFromYawPitchRoll(float_float_float).md 'UnrealEngine.Framework.Maths.CreateFromYawPitchRoll(float, float, float)') +- [Damp(double, double, double, double)](./Maths-Damp(double_double_double_double).md 'UnrealEngine.Framework.Maths.Damp(double, double, double, double)') +- [Damp(System.Numerics.Quaternion, System.Numerics.Quaternion, float, float)](./Maths-Damp(Quaternion_Quaternion_float_float).md 'UnrealEngine.Framework.Maths.Damp(System.Numerics.Quaternion, System.Numerics.Quaternion, float, float)') +- [Damp(System.Numerics.Vector2, System.Numerics.Vector2, float, float)](./Maths-Damp(Vector2_Vector2_float_float).md 'UnrealEngine.Framework.Maths.Damp(System.Numerics.Vector2, System.Numerics.Vector2, float, float)') +- [Damp(System.Numerics.Vector3, System.Numerics.Vector3, float, float)](./Maths-Damp(Vector3_Vector3_float_float).md 'UnrealEngine.Framework.Maths.Damp(System.Numerics.Vector3, System.Numerics.Vector3, float, float)') +- [Damp(float, float, float, float)](./Maths-Damp(float_float_float_float).md 'UnrealEngine.Framework.Maths.Damp(float, float, float, float)') +- [DeltaAngle(double, double)](./Maths-DeltaAngle(double_double).md 'UnrealEngine.Framework.Maths.DeltaAngle(double, double)') +- [DeltaAngle(float, float)](./Maths-DeltaAngle(float_float).md 'UnrealEngine.Framework.Maths.DeltaAngle(float, float)') +- [Dot(double, double)](./Maths-Dot(double_double).md 'UnrealEngine.Framework.Maths.Dot(double, double)') +- [Dot(float, float)](./Maths-Dot(float_float).md 'UnrealEngine.Framework.Maths.Dot(float, float)') +- [Euler(System.Numerics.Vector3)](./Maths-Euler(Vector3).md 'UnrealEngine.Framework.Maths.Euler(System.Numerics.Vector3)') +- [Euler(float, float, float)](./Maths-Euler(float_float_float).md 'UnrealEngine.Framework.Maths.Euler(float, float, float)') +- [Fraction(double)](./Maths-Fraction(double).md 'UnrealEngine.Framework.Maths.Fraction(double)') +- [Fraction(float)](./Maths-Fraction(float).md 'UnrealEngine.Framework.Maths.Fraction(float)') +- [FromToRotation(System.Numerics.Vector3, System.Numerics.Vector3)](./Maths-FromToRotation(Vector3_Vector3).md 'UnrealEngine.Framework.Maths.FromToRotation(System.Numerics.Vector3, System.Numerics.Vector3)') +- [InverseLerp(double, double, double)](./Maths-InverseLerp(double_double_double).md 'UnrealEngine.Framework.Maths.InverseLerp(double, double, double)') +- [InverseLerp(float, float, float)](./Maths-InverseLerp(float_float_float).md 'UnrealEngine.Framework.Maths.InverseLerp(float, float, float)') +- [Lerp(double, double, double)](./Maths-Lerp(double_double_double).md 'UnrealEngine.Framework.Maths.Lerp(double, double, double)') +- [Lerp(float, float, float)](./Maths-Lerp(float_float_float).md 'UnrealEngine.Framework.Maths.Lerp(float, float, float)') +- [LerpAngle(double, double, double)](./Maths-LerpAngle(double_double_double).md 'UnrealEngine.Framework.Maths.LerpAngle(double, double, double)') +- [LerpAngle(float, float, float)](./Maths-LerpAngle(float_float_float).md 'UnrealEngine.Framework.Maths.LerpAngle(float, float, float)') +- [Magnitude(System.Numerics.Vector2)](./Maths-Magnitude(Vector2).md 'UnrealEngine.Framework.Maths.Magnitude(System.Numerics.Vector2)') +- [Magnitude(System.Numerics.Vector3)](./Maths-Magnitude(Vector3).md 'UnrealEngine.Framework.Maths.Magnitude(System.Numerics.Vector3)') +- [Magnitude(System.Numerics.Vector4)](./Maths-Magnitude(Vector4).md 'UnrealEngine.Framework.Maths.Magnitude(System.Numerics.Vector4)') +- [MoveTowards(double, double, double)](./Maths-MoveTowards(double_double_double).md 'UnrealEngine.Framework.Maths.MoveTowards(double, double, double)') +- [MoveTowards(System.Numerics.Vector2, System.Numerics.Vector2, float)](./Maths-MoveTowards(Vector2_Vector2_float).md 'UnrealEngine.Framework.Maths.MoveTowards(System.Numerics.Vector2, System.Numerics.Vector2, float)') +- [MoveTowards(System.Numerics.Vector3, System.Numerics.Vector3, float)](./Maths-MoveTowards(Vector3_Vector3_float).md 'UnrealEngine.Framework.Maths.MoveTowards(System.Numerics.Vector3, System.Numerics.Vector3, float)') +- [MoveTowards(float, float, float)](./Maths-MoveTowards(float_float_float).md 'UnrealEngine.Framework.Maths.MoveTowards(float, float, float)') +- [MoveTowardsAngle(double, double, double)](./Maths-MoveTowardsAngle(double_double_double).md 'UnrealEngine.Framework.Maths.MoveTowardsAngle(double, double, double)') +- [MoveTowardsAngle(float, float, float)](./Maths-MoveTowardsAngle(float_float_float).md 'UnrealEngine.Framework.Maths.MoveTowardsAngle(float, float, float)') +- [NextPowerOfTwo(double)](./Maths-NextPowerOfTwo(double).md 'UnrealEngine.Framework.Maths.NextPowerOfTwo(double)') +- [NextPowerOfTwo(float)](./Maths-NextPowerOfTwo(float).md 'UnrealEngine.Framework.Maths.NextPowerOfTwo(float)') +- [Perpendicular(System.Numerics.Vector2)](./Maths-Perpendicular(Vector2).md 'UnrealEngine.Framework.Maths.Perpendicular(System.Numerics.Vector2)') +- [PreviousPowerOfTwo(double)](./Maths-PreviousPowerOfTwo(double).md 'UnrealEngine.Framework.Maths.PreviousPowerOfTwo(double)') +- [PreviousPowerOfTwo(float)](./Maths-PreviousPowerOfTwo(float).md 'UnrealEngine.Framework.Maths.PreviousPowerOfTwo(float)') +- [Project(System.Numerics.Vector3, System.Numerics.Vector3)](./Maths-Project(Vector3_Vector3).md 'UnrealEngine.Framework.Maths.Project(System.Numerics.Vector3, System.Numerics.Vector3)') +- [ProjectOnPlane(System.Numerics.Vector3, System.Numerics.Vector3)](./Maths-ProjectOnPlane(Vector3_Vector3).md 'UnrealEngine.Framework.Maths.ProjectOnPlane(System.Numerics.Vector3, System.Numerics.Vector3)') +- [Refract(System.Numerics.Vector2, System.Numerics.Vector2, float)](./Maths-Refract(Vector2_Vector2_float).md 'UnrealEngine.Framework.Maths.Refract(System.Numerics.Vector2, System.Numerics.Vector2, float)') +- [Refract(System.Numerics.Vector3, System.Numerics.Vector3, float)](./Maths-Refract(Vector3_Vector3_float).md 'UnrealEngine.Framework.Maths.Refract(System.Numerics.Vector3, System.Numerics.Vector3, float)') +- [Refract(System.Numerics.Vector4, System.Numerics.Vector4, float)](./Maths-Refract(Vector4_Vector4_float).md 'UnrealEngine.Framework.Maths.Refract(System.Numerics.Vector4, System.Numerics.Vector4, float)') +- [Repeat(double, double)](./Maths-Repeat(double_double).md 'UnrealEngine.Framework.Maths.Repeat(double, double)') +- [Repeat(float, float)](./Maths-Repeat(float_float).md 'UnrealEngine.Framework.Maths.Repeat(float, float)') +- [RotateTowards(System.Numerics.Quaternion, System.Numerics.Quaternion, float)](./Maths-RotateTowards(Quaternion_Quaternion_float).md 'UnrealEngine.Framework.Maths.RotateTowards(System.Numerics.Quaternion, System.Numerics.Quaternion, float)') +- [Saturate(double)](./Maths-Saturate(double).md 'UnrealEngine.Framework.Maths.Saturate(double)') +- [Saturate(float)](./Maths-Saturate(float).md 'UnrealEngine.Framework.Maths.Saturate(float)') +- [SignedAngle(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Vector3)](./Maths-SignedAngle(Vector3_Vector3_Vector3).md 'UnrealEngine.Framework.Maths.SignedAngle(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Vector3)') +- [SmootherStep(double)](./Maths-SmootherStep(double).md 'UnrealEngine.Framework.Maths.SmootherStep(double)') +- [SmootherStep(float)](./Maths-SmootherStep(float).md 'UnrealEngine.Framework.Maths.SmootherStep(float)') +- [SmoothStep(double)](./Maths-SmoothStep(double).md 'UnrealEngine.Framework.Maths.SmoothStep(double)') +- [SmoothStep(float)](./Maths-SmoothStep(float).md 'UnrealEngine.Framework.Maths.SmoothStep(float)') +- [SquareMagnitude(System.Numerics.Vector2)](./Maths-SquareMagnitude(Vector2).md 'UnrealEngine.Framework.Maths.SquareMagnitude(System.Numerics.Vector2)') +- [SquareMagnitude(System.Numerics.Vector3)](./Maths-SquareMagnitude(Vector3).md 'UnrealEngine.Framework.Maths.SquareMagnitude(System.Numerics.Vector3)') +- [SquareMagnitude(System.Numerics.Vector4)](./Maths-SquareMagnitude(Vector4).md 'UnrealEngine.Framework.Maths.SquareMagnitude(System.Numerics.Vector4)') + +``` + +`API/MeshComponent-GetMaterialIndex(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MeshComponent](./MeshComponent.md 'UnrealEngine.Framework.MeshComponent') +## MeshComponent.GetMaterialIndex(string) Method +Returns a material index the given a slot name +```csharp +public int GetMaterialIndex(string materialSlotName); +``` +#### Parameters + +`materialSlotName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/MeshComponent-IsValidMaterialSlotName(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MeshComponent](./MeshComponent.md 'UnrealEngine.Framework.MeshComponent') +## MeshComponent.IsValidMaterialSlotName(string) Method +Returns `true` if the given slot name is valid +```csharp +public bool IsValidMaterialSlotName(string materialSlotName); +``` +#### Parameters + +`materialSlotName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/MeshComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## MeshComponent Class +An abstract component that is an instance of a renderable collection of triangles +```csharp +public abstract class MeshComponent : UnrealEngine.Framework.PrimitiveComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') 🡒 MeshComponent + +Derived +↳ [SkinnedMeshComponent](./SkinnedMeshComponent.md 'UnrealEngine.Framework.SkinnedMeshComponent') +↳ [StaticMeshComponent](./StaticMeshComponent.md 'UnrealEngine.Framework.StaticMeshComponent') +### Methods +- [GetMaterialIndex(string)](./MeshComponent-GetMaterialIndex(string).md 'UnrealEngine.Framework.MeshComponent.GetMaterialIndex(string)') +- [IsValidMaterialSlotName(string)](./MeshComponent-IsValidMaterialSlotName(string).md 'UnrealEngine.Framework.MeshComponent.IsValidMaterialSlotName(string)') + +``` + +`API/MotionControllerComponent-DisableLowLatencyUpdate.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MotionControllerComponent](./MotionControllerComponent.md 'UnrealEngine.Framework.MotionControllerComponent') +## MotionControllerComponent.DisableLowLatencyUpdate Property +Gets or sets whether render transforms within the motion controller hierarchy will be updated a second time immediately before rendering +```csharp +public bool DisableLowLatencyUpdate { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/MotionControllerComponent-DisplayDeviceModel.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MotionControllerComponent](./MotionControllerComponent.md 'UnrealEngine.Framework.MotionControllerComponent') +## MotionControllerComponent.DisplayDeviceModel Property +Gets or sets whether to render a model associated with the set hand +```csharp +public bool DisplayDeviceModel { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/MotionControllerComponent-IsTracked.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MotionControllerComponent](./MotionControllerComponent.md 'UnrealEngine.Framework.MotionControllerComponent') +## MotionControllerComponent.IsTracked Property +Returns `true` if the component has a valid tracked device this frame +```csharp +public bool IsTracked { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/MotionControllerComponent-MotionControllerComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MotionControllerComponent](./MotionControllerComponent.md 'UnrealEngine.Framework.MotionControllerComponent') +## MotionControllerComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public MotionControllerComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/MotionControllerComponent-SetAssociatedPlayerIndex(int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MotionControllerComponent](./MotionControllerComponent.md 'UnrealEngine.Framework.MotionControllerComponent') +## MotionControllerComponent.SetAssociatedPlayerIndex(int) Method +Sets the player index which the motion controller should automatically follow +```csharp +public void SetAssociatedPlayerIndex(int playerIndex); +``` +#### Parameters + +`playerIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +``` + +`API/MotionControllerComponent-SetCustomDisplayMesh(StaticMesh).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MotionControllerComponent](./MotionControllerComponent.md 'UnrealEngine.Framework.MotionControllerComponent') +## MotionControllerComponent.SetCustomDisplayMesh(UnrealEngine.Framework.StaticMesh) Method +Sets the custom display mesh that attached to the motion controller +```csharp +public void SetCustomDisplayMesh(UnrealEngine.Framework.StaticMesh staticMesh); +``` +#### Parameters + +`staticMesh` [StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') + + +``` + +`API/MotionControllerComponent-SetDisplayModelSource(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MotionControllerComponent](./MotionControllerComponent.md 'UnrealEngine.Framework.MotionControllerComponent') +## MotionControllerComponent.SetDisplayModelSource(string) Method +Sets the display model source +```csharp +public void SetDisplayModelSource(string source); +``` +#### Parameters + +`source` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/MotionControllerComponent-SetTrackingMotionSource(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MotionControllerComponent](./MotionControllerComponent.md 'UnrealEngine.Framework.MotionControllerComponent') +## MotionControllerComponent.SetTrackingMotionSource(string) Method +Sets the tracking motion source +```csharp +public void SetTrackingMotionSource(string source); +``` +#### Parameters + +`source` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/MotionControllerComponent-TrackingSource.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MotionControllerComponent](./MotionControllerComponent.md 'UnrealEngine.Framework.MotionControllerComponent') +## MotionControllerComponent.TrackingSource Property +Gets or sets the current tracking source +```csharp +public UnrealEngine.Framework.ControllerHand TrackingSource { get; set; } +``` +#### Property Value +[ControllerHand](./ControllerHand.md 'UnrealEngine.Framework.ControllerHand') + +``` + +`API/MotionControllerComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## MotionControllerComponent Class +A component that represents a physical motion controller in 3D space +```csharp +public class MotionControllerComponent : UnrealEngine.Framework.PrimitiveComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') 🡒 MotionControllerComponent +### Constructors +- [MotionControllerComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./MotionControllerComponent-MotionControllerComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.MotionControllerComponent.MotionControllerComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Properties +- [DisableLowLatencyUpdate](./MotionControllerComponent-DisableLowLatencyUpdate.md 'UnrealEngine.Framework.MotionControllerComponent.DisableLowLatencyUpdate') +- [DisplayDeviceModel](./MotionControllerComponent-DisplayDeviceModel.md 'UnrealEngine.Framework.MotionControllerComponent.DisplayDeviceModel') +- [IsTracked](./MotionControllerComponent-IsTracked.md 'UnrealEngine.Framework.MotionControllerComponent.IsTracked') +- [TrackingSource](./MotionControllerComponent-TrackingSource.md 'UnrealEngine.Framework.MotionControllerComponent.TrackingSource') +### Methods +- [SetAssociatedPlayerIndex(int)](./MotionControllerComponent-SetAssociatedPlayerIndex(int).md 'UnrealEngine.Framework.MotionControllerComponent.SetAssociatedPlayerIndex(int)') +- [SetCustomDisplayMesh(UnrealEngine.Framework.StaticMesh)](./MotionControllerComponent-SetCustomDisplayMesh(StaticMesh).md 'UnrealEngine.Framework.MotionControllerComponent.SetCustomDisplayMesh(UnrealEngine.Framework.StaticMesh)') +- [SetDisplayModelSource(string)](./MotionControllerComponent-SetDisplayModelSource(string).md 'UnrealEngine.Framework.MotionControllerComponent.SetDisplayModelSource(string)') +- [SetTrackingMotionSource(string)](./MotionControllerComponent-SetTrackingMotionSource(string).md 'UnrealEngine.Framework.MotionControllerComponent.SetTrackingMotionSource(string)') + +``` + +`API/MovementComponent-ConstrainDirectionToPlane(Vector3_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.ConstrainDirectionToPlane(System.Numerics.Vector3, System.Numerics.Vector3) Method +Constrains a direction vector to the plane constraint, if enabled +```csharp +public void ConstrainDirectionToPlane(in System.Numerics.Vector3 direction, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`direction` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/MovementComponent-ConstrainLocationToPlane(Vector3_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.ConstrainLocationToPlane(System.Numerics.Vector3, System.Numerics.Vector3) Method +Constrains a location vector to the plane constraint, if enabled +```csharp +public void ConstrainLocationToPlane(in System.Numerics.Vector3 location, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/MovementComponent-ConstrainNormalToPlane(Vector3_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.ConstrainNormalToPlane(System.Numerics.Vector3, System.Numerics.Vector3) Method +Constrains a normal vector (of unit length) to the plane constraint, if enabled +```csharp +public void ConstrainNormalToPlane(in System.Numerics.Vector3 normal, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`normal` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/MovementComponent-ConstrainToPlane.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.ConstrainToPlane Property +Gets or sets whether the movement will be constrained to a plane +```csharp +public bool ConstrainToPlane { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/MovementComponent-GetGravity().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.GetGravity() Method +Returns gravity that affects the component +```csharp +public float GetGravity(); +``` +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/MovementComponent-GetMaxSpeed().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.GetMaxSpeed() Method +Returns maximum speed of the component in current movement mode +```csharp +public float GetMaxSpeed(); +``` +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/MovementComponent-GetPlaneConstraintNormal().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.GetPlaneConstraintNormal() Method +Returns the normal of the plane that constrains movement, enforced if the plane constraint is enabled +```csharp +public System.Numerics.Vector3 GetPlaneConstraintNormal(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/MovementComponent-GetPlaneConstraintNormal(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.GetPlaneConstraintNormal(System.Numerics.Vector3) Method +Retrieves the normal of the plane that constrains movement, enforced if the plane constraint is enabled +```csharp +public void GetPlaneConstraintNormal(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/MovementComponent-GetPlaneConstraintOrigin().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.GetPlaneConstraintOrigin() Method +Returns the plane constraint origin +```csharp +public System.Numerics.Vector3 GetPlaneConstraintOrigin(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/MovementComponent-GetPlaneConstraintOrigin(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.GetPlaneConstraintOrigin(System.Numerics.Vector3) Method +Retrieves the plane constraint origin +```csharp +public void GetPlaneConstraintOrigin(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/MovementComponent-GetVelocity().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.GetVelocity() Method +Returns the current velocity of updated component +```csharp +public System.Numerics.Vector3 GetVelocity(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/MovementComponent-GetVelocity(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.GetVelocity(System.Numerics.Vector3) Method +Retrieves the current velocity of updated component +```csharp +public void GetVelocity(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/MovementComponent-IsExceedingMaxSpeed(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.IsExceedingMaxSpeed(float) Method +Returns `true` if the current velocity is exceeding the given max speed within a small error tolerance +```csharp +public bool IsExceedingMaxSpeed(float maxSpeed); +``` +#### Parameters + +`maxSpeed` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/MovementComponent-IsInWater.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.IsInWater Property +Returns `true` if it's in physics volume with water flag +```csharp +public bool IsInWater { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/MovementComponent-PlaneConstraint.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.PlaneConstraint Property +Gets or sets the plane constraint axis +```csharp +public UnrealEngine.Framework.PlaneConstraintAxis PlaneConstraint { get; set; } +``` +#### Property Value +[PlaneConstraintAxis](./PlaneConstraintAxis.md 'UnrealEngine.Framework.PlaneConstraintAxis') + +``` + +`API/MovementComponent-SetPlaneConstraintFromVectors(Vector3_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.SetPlaneConstraintFromVectors(System.Numerics.Vector3, System.Numerics.Vector3) Method +Sets the plane that constrains movement computed from the forward and up vectors, enforced if the plane constraint is enabled +```csharp +public void SetPlaneConstraintFromVectors(in System.Numerics.Vector3 forward, in System.Numerics.Vector3 up); +``` +#### Parameters + +`forward` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`up` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/MovementComponent-SetPlaneConstraintNormal(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.SetPlaneConstraintNormal(System.Numerics.Vector3) Method +Sets the normal of the plane that constrains movement, enforced if the plane constraint is enabled +```csharp +public void SetPlaneConstraintNormal(in System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/MovementComponent-SetPlaneConstraintOrigin(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.SetPlaneConstraintOrigin(System.Numerics.Vector3) Method +Sets the origin of the plane that constrains movement, enforced if the plane constraint is enabled +```csharp +public void SetPlaneConstraintOrigin(in System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/MovementComponent-SetVelocity(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.SetVelocity(System.Numerics.Vector3) Method +Sets the current velocity of updated component +```csharp +public void SetVelocity(in System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/MovementComponent-SnapToPlaneAtStart.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.SnapToPlaneAtStart Property +Gets or sets whether the updated component will be snapped to the plane when first is attached, if plane constraints are enabled +```csharp +public bool SnapToPlaneAtStart { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/MovementComponent-StopMovement().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.StopMovement() Method +Stops movement immediately, zeroes velocity, usually zeros acceleration for components with acceleration +```csharp +public void StopMovement(); +``` + +``` + +`API/MovementComponent-UpdateOnlyIfRendered.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +## MovementComponent.UpdateOnlyIfRendered Property +Gets or sets whether should be skipped if the updated component was not recently rendered +```csharp +public bool UpdateOnlyIfRendered { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/MovementComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## MovementComponent Class +An abstract component that defines functionality for moving a [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +```csharp +public abstract class MovementComponent : UnrealEngine.Framework.ActorComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 MovementComponent + +Derived +↳ [RotatingMovementComponent](./RotatingMovementComponent.md 'UnrealEngine.Framework.RotatingMovementComponent') +### Properties +- [ConstrainToPlane](./MovementComponent-ConstrainToPlane.md 'UnrealEngine.Framework.MovementComponent.ConstrainToPlane') +- [IsInWater](./MovementComponent-IsInWater.md 'UnrealEngine.Framework.MovementComponent.IsInWater') +- [PlaneConstraint](./MovementComponent-PlaneConstraint.md 'UnrealEngine.Framework.MovementComponent.PlaneConstraint') +- [SnapToPlaneAtStart](./MovementComponent-SnapToPlaneAtStart.md 'UnrealEngine.Framework.MovementComponent.SnapToPlaneAtStart') +- [UpdateOnlyIfRendered](./MovementComponent-UpdateOnlyIfRendered.md 'UnrealEngine.Framework.MovementComponent.UpdateOnlyIfRendered') +### Methods +- [ConstrainDirectionToPlane(System.Numerics.Vector3, System.Numerics.Vector3)](./MovementComponent-ConstrainDirectionToPlane(Vector3_Vector3).md 'UnrealEngine.Framework.MovementComponent.ConstrainDirectionToPlane(System.Numerics.Vector3, System.Numerics.Vector3)') +- [ConstrainLocationToPlane(System.Numerics.Vector3, System.Numerics.Vector3)](./MovementComponent-ConstrainLocationToPlane(Vector3_Vector3).md 'UnrealEngine.Framework.MovementComponent.ConstrainLocationToPlane(System.Numerics.Vector3, System.Numerics.Vector3)') +- [ConstrainNormalToPlane(System.Numerics.Vector3, System.Numerics.Vector3)](./MovementComponent-ConstrainNormalToPlane(Vector3_Vector3).md 'UnrealEngine.Framework.MovementComponent.ConstrainNormalToPlane(System.Numerics.Vector3, System.Numerics.Vector3)') +- [GetGravity()](./MovementComponent-GetGravity().md 'UnrealEngine.Framework.MovementComponent.GetGravity()') +- [GetMaxSpeed()](./MovementComponent-GetMaxSpeed().md 'UnrealEngine.Framework.MovementComponent.GetMaxSpeed()') +- [GetPlaneConstraintNormal()](./MovementComponent-GetPlaneConstraintNormal().md 'UnrealEngine.Framework.MovementComponent.GetPlaneConstraintNormal()') +- [GetPlaneConstraintNormal(System.Numerics.Vector3)](./MovementComponent-GetPlaneConstraintNormal(Vector3).md 'UnrealEngine.Framework.MovementComponent.GetPlaneConstraintNormal(System.Numerics.Vector3)') +- [GetPlaneConstraintOrigin()](./MovementComponent-GetPlaneConstraintOrigin().md 'UnrealEngine.Framework.MovementComponent.GetPlaneConstraintOrigin()') +- [GetPlaneConstraintOrigin(System.Numerics.Vector3)](./MovementComponent-GetPlaneConstraintOrigin(Vector3).md 'UnrealEngine.Framework.MovementComponent.GetPlaneConstraintOrigin(System.Numerics.Vector3)') +- [GetVelocity()](./MovementComponent-GetVelocity().md 'UnrealEngine.Framework.MovementComponent.GetVelocity()') +- [GetVelocity(System.Numerics.Vector3)](./MovementComponent-GetVelocity(Vector3).md 'UnrealEngine.Framework.MovementComponent.GetVelocity(System.Numerics.Vector3)') +- [IsExceedingMaxSpeed(float)](./MovementComponent-IsExceedingMaxSpeed(float).md 'UnrealEngine.Framework.MovementComponent.IsExceedingMaxSpeed(float)') +- [SetPlaneConstraintFromVectors(System.Numerics.Vector3, System.Numerics.Vector3)](./MovementComponent-SetPlaneConstraintFromVectors(Vector3_Vector3).md 'UnrealEngine.Framework.MovementComponent.SetPlaneConstraintFromVectors(System.Numerics.Vector3, System.Numerics.Vector3)') +- [SetPlaneConstraintNormal(System.Numerics.Vector3)](./MovementComponent-SetPlaneConstraintNormal(Vector3).md 'UnrealEngine.Framework.MovementComponent.SetPlaneConstraintNormal(System.Numerics.Vector3)') +- [SetPlaneConstraintOrigin(System.Numerics.Vector3)](./MovementComponent-SetPlaneConstraintOrigin(Vector3).md 'UnrealEngine.Framework.MovementComponent.SetPlaneConstraintOrigin(System.Numerics.Vector3)') +- [SetVelocity(System.Numerics.Vector3)](./MovementComponent-SetVelocity(Vector3).md 'UnrealEngine.Framework.MovementComponent.SetVelocity(System.Numerics.Vector3)') +- [StopMovement()](./MovementComponent-StopMovement().md 'UnrealEngine.Framework.MovementComponent.StopMovement()') + +``` + +`API/NetMode.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## NetMode Enum +Defines the networking mode +```csharp +public enum NetMode : System.Byte +``` +### Fields + +`Standalone` 0 +A game without networking, with one or more local players + + +`DedicatedServer` 1 +A server with no local players + + +`ListenServer` 2 +A server that also has a local player who is hosting the game, available to other players on the network + + +`Client` 3 +A client connected to a server + + +``` + +`API/ObjectReference-Equals(ObjectReference).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') +## ObjectReference.Equals(UnrealEngine.Framework.ObjectReference) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.ObjectReference other); +``` +#### Parameters + +`other` [ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ObjectReference-Equals(object).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') +## ObjectReference.Equals(object) Method +Indicates equality of objects +```csharp +public override bool Equals(object value); +``` +#### Parameters + +`value` [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ObjectReference-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') +## ObjectReference.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/ObjectReference-ID.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') +## ObjectReference.ID Property +Returns the unique ID of the object, reused by the engine, only unique while the object is alive +```csharp +public uint ID { get; } +``` +#### Property Value +[System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') + +``` + +`API/ObjectReference-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') +## ObjectReference.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ObjectReference-Name.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') +## ObjectReference.Name Property +Returns the name of the object +```csharp +public string Name { get; } +``` +#### Property Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/ObjectReference-ToActor-T-().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') +## ObjectReference.ToActor<T>() Method +Converts the object reference to the actor of the specified class +```csharp +public T ToActor() + where T : UnrealEngine.Framework.Actor; +``` +#### Type parameters + +`T` + +Constraints [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Returns +[T](#UnrealEngine-Framework-ObjectReference-ToActor-T-()-T 'UnrealEngine.Framework.ObjectReference.ToActor<T>().T') +An actor or `null` on failure + +``` + +`API/ObjectReference-ToComponent-T-().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') +## ObjectReference.ToComponent<T>() Method +Converts the object reference to the component of the specified class +```csharp +public T ToComponent() + where T : UnrealEngine.Framework.ActorComponent; +``` +#### Type parameters + +`T` + +Constraints [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') + +#### Returns +[T](#UnrealEngine-Framework-ObjectReference-ToComponent-T-()-T 'UnrealEngine.Framework.ObjectReference.ToComponent<T>().T') +A component or `null` on failure + +``` + +`API/ObjectReference-op_Equality(ObjectReference_ObjectReference).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') +## ObjectReference.operator ==(UnrealEngine.Framework.ObjectReference, UnrealEngine.Framework.ObjectReference) Operator +Tests for equality between two objects +```csharp +public static bool operator ==(UnrealEngine.Framework.ObjectReference left, UnrealEngine.Framework.ObjectReference right); +``` +#### Parameters + +`left` [ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') + + +`right` [ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ObjectReference-op_Inequality(ObjectReference_ObjectReference).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') +## ObjectReference.operator !=(UnrealEngine.Framework.ObjectReference, UnrealEngine.Framework.ObjectReference) Operator +Tests for inequality between two objects +```csharp +public static bool operator !=(UnrealEngine.Framework.ObjectReference left, UnrealEngine.Framework.ObjectReference right); +``` +#### Parameters + +`left` [ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') + + +`right` [ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ObjectReference.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ObjectReference Struct +A representation of the engine's object reference +```csharp +public struct ObjectReference : +System.IEquatable +``` +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [ID](./ObjectReference-ID.md 'UnrealEngine.Framework.ObjectReference.ID') +- [IsCreated](./ObjectReference-IsCreated.md 'UnrealEngine.Framework.ObjectReference.IsCreated') +- [Name](./ObjectReference-Name.md 'UnrealEngine.Framework.ObjectReference.Name') +### Methods +- [Equals(object)](./ObjectReference-Equals(object).md 'UnrealEngine.Framework.ObjectReference.Equals(object)') +- [Equals(UnrealEngine.Framework.ObjectReference)](./ObjectReference-Equals(ObjectReference).md 'UnrealEngine.Framework.ObjectReference.Equals(UnrealEngine.Framework.ObjectReference)') +- [GetHashCode()](./ObjectReference-GetHashCode().md 'UnrealEngine.Framework.ObjectReference.GetHashCode()') +- [ToActor<T>()](./ObjectReference-ToActor-T-().md 'UnrealEngine.Framework.ObjectReference.ToActor<T>()') +- [ToComponent<T>()](./ObjectReference-ToComponent-T-().md 'UnrealEngine.Framework.ObjectReference.ToComponent<T>()') +### Operators +- [operator ==(UnrealEngine.Framework.ObjectReference, UnrealEngine.Framework.ObjectReference)](./ObjectReference-op_Equality(ObjectReference_ObjectReference).md 'UnrealEngine.Framework.ObjectReference.op_Equality(UnrealEngine.Framework.ObjectReference, UnrealEngine.Framework.ObjectReference)') +- [operator !=(UnrealEngine.Framework.ObjectReference, UnrealEngine.Framework.ObjectReference)](./ObjectReference-op_Inequality(ObjectReference_ObjectReference).md 'UnrealEngine.Framework.ObjectReference.op_Inequality(UnrealEngine.Framework.ObjectReference, UnrealEngine.Framework.ObjectReference)') + +``` + +`API/Pawn-AddControllerPitchInput(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.AddControllerPitchInput(float) Method +Adds pitch (look up) input to the controller, if it's a local [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +```csharp +public void AddControllerPitchInput(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/Pawn-AddControllerRollInput(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.AddControllerRollInput(float) Method +Adds roll input to the controller, if it's a local [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +```csharp +public void AddControllerRollInput(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/Pawn-AddControllerYawInput(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.AddControllerYawInput(float) Method +Adds yaw (turn) input to the controller, if it's a local [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +```csharp +public void AddControllerYawInput(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/Pawn-AddMovementInput(Vector3_float_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.AddMovementInput(System.Numerics.Vector3, float, bool) Method +Adds movement input along the given world direction vector (usually normalized) +```csharp +public void AddMovementInput(in System.Numerics.Vector3 worldDirection, float scaleValue=1f, bool force=false); +``` +#### Parameters + +`worldDirection` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Direction in world space to apply input + + +`scaleValue` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Scale to apply to input, 0.5f applies half the normal value, while -1.0 would reverse the direction + + +`force` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, always add the input, ignoring the result of [IsMoveInputIgnored](./Controller-IsMoveInputIgnored.md 'UnrealEngine.Framework.Controller.IsMoveInputIgnored') + + +``` + +`API/Pawn-AutoPossessAI.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.AutoPossessAI Property +Gets or sets the automatic possession type by an AI controller +```csharp +public UnrealEngine.Framework.AutoPossessAI AutoPossessAI { get; set; } +``` +#### Property Value +[AutoPossessAI](./AutoPossessAI.md 'UnrealEngine.Framework.AutoPossessAI') + +``` + +`API/Pawn-AutoPossessPlayer.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.AutoPossessPlayer Property +Gets or sets the player index for automatic possession by a player controller +```csharp +public UnrealEngine.Framework.AutoReceiveInput AutoPossessPlayer { get; set; } +``` +#### Property Value +[AutoReceiveInput](./AutoReceiveInput.md 'UnrealEngine.Framework.AutoReceiveInput') + +``` + +`API/Pawn-GetAIController().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.GetAIController() Method +Returns the AI controller or `null` on failure +```csharp +public UnrealEngine.Framework.AIController GetAIController(); +``` +#### Returns +[AIController](./AIController.md 'UnrealEngine.Framework.AIController') + +``` + +`API/Pawn-GetGravityDirection().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.GetGravityDirection() Method +Returns vector direction of gravity +```csharp +public System.Numerics.Vector3 GetGravityDirection(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Pawn-GetGravityDirection(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.GetGravityDirection(System.Numerics.Vector3) Method +Retrieves vector direction of gravity +```csharp +public void GetGravityDirection(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/Pawn-GetPlayerController().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.GetPlayerController() Method +Returns the player controller or `null` on failure +```csharp +public UnrealEngine.Framework.PlayerController GetPlayerController(); +``` +#### Returns +[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') + +``` + +`API/Pawn-IsControlled.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.IsControlled Property +Returns `true` if the pawn is possesed by a [Controller](./Controller.md 'UnrealEngine.Framework.Controller') +```csharp +public bool IsControlled { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Pawn-IsPlayerControlled.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.IsPlayerControlled Property +Returns `true` if the pawn is possesed by a [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +```csharp +public bool IsPlayerControlled { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Pawn-Pawn(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public Pawn(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/Pawn-UseControllerRotationPitch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.UseControllerRotationPitch Property +Gets or sets whether pitch will be updated to match the controller's control rotation pitch, if controlled by a [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +```csharp +public bool UseControllerRotationPitch { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Pawn-UseControllerRotationRoll.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.UseControllerRotationRoll Property +Gets or sets whether roll will be updated to match the controller's control rotation roll, if controlled by a [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +```csharp +public bool UseControllerRotationRoll { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Pawn-UseControllerRotationYaw.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +## Pawn.UseControllerRotationYaw Property +Gets or sets whether yaw will be updated to match the controller's control rotation yaw, if controlled by a [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +```csharp +public bool UseControllerRotationYaw { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Pawn.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Pawn Class +The base class of actors that can be possessed by players or AI +```csharp +public class Pawn : UnrealEngine.Framework.Actor +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 Pawn + +Derived +↳ [Character](./Character.md 'UnrealEngine.Framework.Character') +### Constructors +- [Pawn(string, UnrealEngine.Framework.Blueprint)](./Pawn-Pawn(string_Blueprint).md 'UnrealEngine.Framework.Pawn.Pawn(string, UnrealEngine.Framework.Blueprint)') +### Properties +- [AutoPossessAI](./Pawn-AutoPossessAI.md 'UnrealEngine.Framework.Pawn.AutoPossessAI') +- [AutoPossessPlayer](./Pawn-AutoPossessPlayer.md 'UnrealEngine.Framework.Pawn.AutoPossessPlayer') +- [IsControlled](./Pawn-IsControlled.md 'UnrealEngine.Framework.Pawn.IsControlled') +- [IsPlayerControlled](./Pawn-IsPlayerControlled.md 'UnrealEngine.Framework.Pawn.IsPlayerControlled') +- [UseControllerRotationPitch](./Pawn-UseControllerRotationPitch.md 'UnrealEngine.Framework.Pawn.UseControllerRotationPitch') +- [UseControllerRotationRoll](./Pawn-UseControllerRotationRoll.md 'UnrealEngine.Framework.Pawn.UseControllerRotationRoll') +- [UseControllerRotationYaw](./Pawn-UseControllerRotationYaw.md 'UnrealEngine.Framework.Pawn.UseControllerRotationYaw') +### Methods +- [AddControllerPitchInput(float)](./Pawn-AddControllerPitchInput(float).md 'UnrealEngine.Framework.Pawn.AddControllerPitchInput(float)') +- [AddControllerRollInput(float)](./Pawn-AddControllerRollInput(float).md 'UnrealEngine.Framework.Pawn.AddControllerRollInput(float)') +- [AddControllerYawInput(float)](./Pawn-AddControllerYawInput(float).md 'UnrealEngine.Framework.Pawn.AddControllerYawInput(float)') +- [AddMovementInput(System.Numerics.Vector3, float, bool)](./Pawn-AddMovementInput(Vector3_float_bool).md 'UnrealEngine.Framework.Pawn.AddMovementInput(System.Numerics.Vector3, float, bool)') +- [GetAIController()](./Pawn-GetAIController().md 'UnrealEngine.Framework.Pawn.GetAIController()') +- [GetGravityDirection()](./Pawn-GetGravityDirection().md 'UnrealEngine.Framework.Pawn.GetGravityDirection()') +- [GetGravityDirection(System.Numerics.Vector3)](./Pawn-GetGravityDirection(Vector3).md 'UnrealEngine.Framework.Pawn.GetGravityDirection(System.Numerics.Vector3)') +- [GetPlayerController()](./Pawn-GetPlayerController().md 'UnrealEngine.Framework.Pawn.GetPlayerController()') + +``` + +`API/PixelFormat.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## PixelFormat Enum +Defines the pixel format +```csharp +public enum PixelFormat +``` +### Fields + +`Unknown` 0 + + + +`A32B32G32R32F` 1 + + + +`B8G8R8A8` 2 + + + +`G8` 3 + + + +`G16` 4 + + + +`DXT1` 5 + + + +`DXT3` 6 + + + +`DXT5` 7 + + + +`UYVY` 8 + + + +`FloatRGB` 9 + + + +`FloatRGBA` 10 + + + +`DepthStencil` 11 + + + +`ShadowDepth` 12 + + + +`R32Float` 13 + + + +`G16R16` 14 + + + +`G16R16F` 15 + + + +`G16R16FFilter` 16 + + + +`G32R32F` 17 + + + +`A2B10G10R10` 18 + + + +`A16B16G16R16` 19 + + + +`D24` 20 + + + +`R16F` 21 + + + +`R16FFilter` 22 + + + +`BC5` 23 + + + +`V8U8` 24 + + + +`A1` 25 + + + +`FloatR11G11B10` 26 + + + +`A8` 27 + + + +`R32UInt` 28 + + + +`R32SInt` 29 + + + +`PVRTC2` 30 + + + +`PVRTC4` 31 + + + +`R16UInt` 32 + + + +`R16SInt` 33 + + + +`R16G16B16A16UInt` 34 + + + +`R16G16B16A16SInt` 35 + + + +`R5G6B5UNorm` 36 + + + +`R8G8B8A8` 37 + + + +`A8R8G8B8` 38 + + + +`BC4` 39 + + + +`R8G8` 40 + + + +`ATCRGB` 41 + + + +`ATCRGBAE` 42 + + + +`ATCRGBAI` 43 + + + +`X24G8` 44 + + + +`ETC1` 45 + + + +`ETC2RGB` 46 + + + +`ETC2RGBA` 47 + + + +`R32G32B32A32UInt` 48 + + + +`R16G16UInt` 49 + + + +`ASTC4x4` 50 + + + +`ASTC6x6` 51 + + + +`ASTC8x8` 52 + + + +`ASTC10x10` 53 + + + +`ASTC12x12` 54 + + + +`BC6H` 55 + + + +`BC7` 56 + + + +`R8UInt` 57 + + + +`L8` 58 + + + +`XGXR8` 59 + + + +`R8G8B8A8UInt` 60 + + + +`R8G8B8A8SNorm` 61 + + + +`R16G16B16A16UNorm` 62 + + + +`R16G16B16A16SNorm` 63 + + + +`PLATFORMHDR0` 64 + + + +`PLATFORMHDR1` 65 + + + +`PLATFORMHDR2` 66 + + + +`NV12` 67 + + + +`R32G32UInt` 68 + + + +`ETC2R11EAC` 69 + + + +`ETC2RG11EAC` 70 + + + +`R8` 71 + + + +``` + +`API/PlaneConstraintAxis.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## PlaneConstraintAxis Enum +Defines behavior when movement is restricted to a 2D plane defined by a specific axis/normal, so that movement along the locked axis will not be possible +```csharp +public enum PlaneConstraintAxis : System.Byte +``` +### Fields + +`Custom` 0 +Lock movement to a user-defined axis + + +`X` 1 +Lock movement in the X axis + + +`Y` 2 +Lock movement in the Y axis + + +`Z` 3 +Lock movement in the Z axis + + +`UseGlobalPhysicsSetting` 4 +Use the global physics project setting + + +``` + +`API/Player-Equals(Player).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Player](./Player.md 'UnrealEngine.Framework.Player') +## Player.Equals(UnrealEngine.Framework.Player) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.Player other); +``` +#### Parameters + +`other` [Player](./Player.md 'UnrealEngine.Framework.Player') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Player-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Player](./Player.md 'UnrealEngine.Framework.Player') +## Player.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/Player-GetPlayerController().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Player](./Player.md 'UnrealEngine.Framework.Player') +## Player.GetPlayerController() Method +Returns the player controller or `null` on failure +```csharp +public UnrealEngine.Framework.PlayerController GetPlayerController(); +``` +#### Returns +[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') + +``` + +`API/Player-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Player](./Player.md 'UnrealEngine.Framework.Player') +## Player.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Player.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Player Class +A player representation +```csharp +public class Player : +System.IEquatable +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 Player + +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[Player](./Player.md 'UnrealEngine.Framework.Player')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [IsCreated](./Player-IsCreated.md 'UnrealEngine.Framework.Player.IsCreated') +### Methods +- [Equals(UnrealEngine.Framework.Player)](./Player-Equals(Player).md 'UnrealEngine.Framework.Player.Equals(UnrealEngine.Framework.Player)') +- [GetHashCode()](./Player-GetHashCode().md 'UnrealEngine.Framework.Player.GetHashCode()') +- [GetPlayerController()](./Player-GetPlayerController().md 'UnrealEngine.Framework.Player.GetPlayerController()') + +``` + +`API/PlayerController-AddPitchInput(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.AddPitchInput(float) Method +Adds pitch (look up) input +```csharp +public void AddPitchInput(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/PlayerController-AddRollInput(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.AddRollInput(float) Method +Adds roll input +```csharp +public void AddRollInput(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/PlayerController-AddYawInput(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.AddYawInput(float) Method +Adds yaw (turn) input +```csharp +public void AddYawInput(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/PlayerController-ConsoleCommand(string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.ConsoleCommand(string, bool) Method +Executes the command on the [Player](./Player.md 'UnrealEngine.Framework.Player') object, `DumpConsoleCommands` command can be used to list all available variables and commands +```csharp +public void ConsoleCommand(string command, bool writeToLog=false); +``` +#### Parameters + +`command` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +A command to execute, string of commands optionally separated by a `|` symbol + + +`writeToLog` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + + +``` + +`API/PlayerController-EnableClickEvents.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.EnableClickEvents Property +Gets or sets whether click events should be generated +```csharp +public bool EnableClickEvents { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PlayerController-EnableMouseOverEvents.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.EnableMouseOverEvents Property +Gets or sets whether the mouse over events should be generated +```csharp +public bool EnableMouseOverEvents { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PlayerController-GetHitResultAtScreenPosition(Vector2_CollisionChannel_Hit_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.GetHitResultAtScreenPosition(System.Numerics.Vector2, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.Hit, bool) Method +Retrieves the first blocking hit from the position on the screen +```csharp +public bool GetHitResultAtScreenPosition(in System.Numerics.Vector2 screenPosition, UnrealEngine.Framework.CollisionChannel traceChannel, ref UnrealEngine.Framework.Hit hit, bool traceComplex=false); +``` +#### Parameters + +`screenPosition` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + + +`traceChannel` [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + + +`hit` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/PlayerController-GetHitResultUnderCursor(CollisionChannel_Hit_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.GetHitResultUnderCursor(UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.Hit, bool) Method +Retrieves the first blocking hit under the mouse cursor +```csharp +public bool GetHitResultUnderCursor(UnrealEngine.Framework.CollisionChannel traceChannel, ref UnrealEngine.Framework.Hit hit, bool traceComplex=false); +``` +#### Parameters + +`traceChannel` [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + + +`hit` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/PlayerController-GetMousePosition(float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.GetMousePosition(float, float) Method +Retrieves the X and Y screen coordinates of the mouse cursor +```csharp +public bool GetMousePosition(ref float x, ref float y); +``` +#### Parameters + +`x` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`y` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if successful + +``` + +`API/PlayerController-GetPlayer().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.GetPlayer() Method +Returns the player representation or `null` on failure +```csharp +public UnrealEngine.Framework.Player GetPlayer(); +``` +#### Returns +[Player](./Player.md 'UnrealEngine.Framework.Player') + +``` + +`API/PlayerController-GetPlayerInput().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.GetPlayerInput() Method +Returns the input manager or `null` on failure +```csharp +public UnrealEngine.Framework.PlayerInput GetPlayerInput(); +``` +#### Returns +[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') + +``` + +`API/PlayerController-IsPaused.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.IsPaused Property +Returns `true` if game is currently paused +```csharp +public bool IsPaused { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PlayerController-PlayerController(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public PlayerController(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/PlayerController-SetMousePosition(float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.SetMousePosition(float, float) Method +Positions the mouse cursor in screen space, in pixels +```csharp +public void SetMousePosition(float x, float y); +``` +#### Parameters + +`x` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`y` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/PlayerController-SetPause(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.SetPause(bool) Method +Pauses a local game +```csharp +public bool SetPause(bool value); +``` +#### Parameters + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/PlayerController-SetViewTarget(Actor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.SetViewTarget(UnrealEngine.Framework.Actor) Method +Sets the view target +```csharp +public void SetViewTarget(UnrealEngine.Framework.Actor newViewTarget); +``` +#### Parameters + +`newViewTarget` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +An actor to set as view target + + +``` + +`API/PlayerController-SetViewTargetWithBlend(Actor_float_float_BlendType_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.SetViewTargetWithBlend(UnrealEngine.Framework.Actor, float, float, UnrealEngine.Framework.BlendType, bool) Method +Sets the view target blending with variable control +```csharp +public void SetViewTargetWithBlend(UnrealEngine.Framework.Actor newViewTarget, float time=0f, float exponent=0f, UnrealEngine.Framework.BlendType type=UnrealEngine.Framework.BlendType.Linear, bool lockOutgoing=false); +``` +#### Parameters + +`newViewTarget` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +An actor to set as view target + + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Time taken to blend + + +`exponent` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Exponent, used by certain [BlendType](./BlendType.md 'UnrealEngine.Framework.BlendType') to control the shape of the curve + + +`type` [BlendType](./BlendType.md 'UnrealEngine.Framework.BlendType') +The blending type + + +`lockOutgoing` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, lock outgoing view target to last frame's camera position for the remainder of the blend + + +``` + +`API/PlayerController-ShowMouseCursor.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +## PlayerController.ShowMouseCursor Property +Gets or sets whether the mouse cursor should be displayed +```csharp +public bool ShowMouseCursor { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PlayerController.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## PlayerController Class +An actor that is used by human players to control a [Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +```csharp +public class PlayerController : UnrealEngine.Framework.Controller +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 [Controller](./Controller.md 'UnrealEngine.Framework.Controller') 🡒 PlayerController +### Constructors +- [PlayerController(string, UnrealEngine.Framework.Blueprint)](./PlayerController-PlayerController(string_Blueprint).md 'UnrealEngine.Framework.PlayerController.PlayerController(string, UnrealEngine.Framework.Blueprint)') +### Properties +- [EnableClickEvents](./PlayerController-EnableClickEvents.md 'UnrealEngine.Framework.PlayerController.EnableClickEvents') +- [EnableMouseOverEvents](./PlayerController-EnableMouseOverEvents.md 'UnrealEngine.Framework.PlayerController.EnableMouseOverEvents') +- [IsPaused](./PlayerController-IsPaused.md 'UnrealEngine.Framework.PlayerController.IsPaused') +- [ShowMouseCursor](./PlayerController-ShowMouseCursor.md 'UnrealEngine.Framework.PlayerController.ShowMouseCursor') +### Methods +- [AddPitchInput(float)](./PlayerController-AddPitchInput(float).md 'UnrealEngine.Framework.PlayerController.AddPitchInput(float)') +- [AddRollInput(float)](./PlayerController-AddRollInput(float).md 'UnrealEngine.Framework.PlayerController.AddRollInput(float)') +- [AddYawInput(float)](./PlayerController-AddYawInput(float).md 'UnrealEngine.Framework.PlayerController.AddYawInput(float)') +- [ConsoleCommand(string, bool)](./PlayerController-ConsoleCommand(string_bool).md 'UnrealEngine.Framework.PlayerController.ConsoleCommand(string, bool)') +- [GetHitResultAtScreenPosition(System.Numerics.Vector2, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.Hit, bool)](./PlayerController-GetHitResultAtScreenPosition(Vector2_CollisionChannel_Hit_bool).md 'UnrealEngine.Framework.PlayerController.GetHitResultAtScreenPosition(System.Numerics.Vector2, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.Hit, bool)') +- [GetHitResultUnderCursor(UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.Hit, bool)](./PlayerController-GetHitResultUnderCursor(CollisionChannel_Hit_bool).md 'UnrealEngine.Framework.PlayerController.GetHitResultUnderCursor(UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.Hit, bool)') +- [GetMousePosition(float, float)](./PlayerController-GetMousePosition(float_float).md 'UnrealEngine.Framework.PlayerController.GetMousePosition(float, float)') +- [GetPlayer()](./PlayerController-GetPlayer().md 'UnrealEngine.Framework.PlayerController.GetPlayer()') +- [GetPlayerInput()](./PlayerController-GetPlayerInput().md 'UnrealEngine.Framework.PlayerController.GetPlayerInput()') +- [SetMousePosition(float, float)](./PlayerController-SetMousePosition(float_float).md 'UnrealEngine.Framework.PlayerController.SetMousePosition(float, float)') +- [SetPause(bool)](./PlayerController-SetPause(bool).md 'UnrealEngine.Framework.PlayerController.SetPause(bool)') +- [SetViewTarget(UnrealEngine.Framework.Actor)](./PlayerController-SetViewTarget(Actor).md 'UnrealEngine.Framework.PlayerController.SetViewTarget(UnrealEngine.Framework.Actor)') +- [SetViewTargetWithBlend(UnrealEngine.Framework.Actor, float, float, UnrealEngine.Framework.BlendType, bool)](./PlayerController-SetViewTargetWithBlend(Actor_float_float_BlendType_bool).md 'UnrealEngine.Framework.PlayerController.SetViewTargetWithBlend(UnrealEngine.Framework.Actor, float, float, UnrealEngine.Framework.BlendType, bool)') + +``` + +`API/PlayerInput-AddActionMapping(string_string_bool_bool_bool_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') +## PlayerInput.AddActionMapping(string, string, bool, bool, bool, bool) Method +Adds a player-specific action mapping +```csharp +public void AddActionMapping(string actionName, string key, bool shift=false, bool ctrl=false, bool alt=false, bool cmd=false); +``` +#### Parameters + +`actionName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +Friendly name of action + + +`key` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +Key to bind in accordance with [Keys](./Keys.md 'UnrealEngine.Framework.Keys') + + +`shift` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if one of the Shift keys must be down when the KeyEvent is received to be acknowledged + + +`ctrl` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if one of the Ctrl keys must be down when the KeyEvent is received to be acknowledged + + +`alt` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if one of the Alt keys must be down when the KeyEvent is received to be acknowledged + + +`cmd` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if one of the Cmd keys must be down when the KeyEvent is received to be acknowledged + + +``` + +`API/PlayerInput-AddAxisMapping(string_string_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') +## PlayerInput.AddAxisMapping(string, string, float) Method +Adds a player-specific mapping between an axis and key +```csharp +public void AddAxisMapping(string axisName, string key, float scale=1f); +``` +#### Parameters + +`axisName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +Friendly name of axis + + +`key` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +Key to bind in accordance with [Keys](./Keys.md 'UnrealEngine.Framework.Keys') + + +`scale` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Multiplier to use for the mapping when accumulating the axis value + + +``` + +`API/PlayerInput-Equals(PlayerInput).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') +## PlayerInput.Equals(UnrealEngine.Framework.PlayerInput) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.PlayerInput other); +``` +#### Parameters + +`other` [PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PlayerInput-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') +## PlayerInput.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/PlayerInput-GetMouseSensitivity().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') +## PlayerInput.GetMouseSensitivity() Method +Returns mouse sensitivity +```csharp +public System.Numerics.Vector2 GetMouseSensitivity(); +``` +#### Returns +[System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + +``` + +`API/PlayerInput-GetMouseSensitivity(Vector2).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') +## PlayerInput.GetMouseSensitivity(System.Numerics.Vector2) Method +Retrieves mouse sensitivity +```csharp +public void GetMouseSensitivity(ref System.Numerics.Vector2 value); +``` +#### Parameters + +`value` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + + +``` + +`API/PlayerInput-GetTimeKeyPressed(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') +## PlayerInput.GetTimeKeyPressed(string) Method +Returns the time a key was pressed +```csharp +public float GetTimeKeyPressed(string key); +``` +#### Parameters + +`key` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/PlayerInput-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') +## PlayerInput.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PlayerInput-IsKeyPressed(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') +## PlayerInput.IsKeyPressed(string) Method +Returns `true` if a key is pressed +```csharp +public bool IsKeyPressed(string key); +``` +#### Parameters + +`key` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PlayerInput-RemoveActionMapping(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') +## PlayerInput.RemoveActionMapping(string, string) Method +Removes a player-specific action mapping +```csharp +public void RemoveActionMapping(string actionName, string key); +``` +#### Parameters + +`actionName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`key` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/PlayerInput-RemoveAxisMapping(string_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') +## PlayerInput.RemoveAxisMapping(string, string) Method +Removes a player-specific mapping between an axis and key +```csharp +public void RemoveAxisMapping(string axisName, string key); +``` +#### Parameters + +`axisName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`key` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/PlayerInput-SetMouseSensitivity(Vector2).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') +## PlayerInput.SetMouseSensitivity(System.Numerics.Vector2) Method +Sets mouse sensitivity +```csharp +public void SetMouseSensitivity(in System.Numerics.Vector2 value); +``` +#### Parameters + +`value` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + + +``` + +`API/PlayerInput.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## PlayerInput Class +An input manager of [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +```csharp +public class PlayerInput : +System.IEquatable +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 PlayerInput + +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [IsCreated](./PlayerInput-IsCreated.md 'UnrealEngine.Framework.PlayerInput.IsCreated') +### Methods +- [AddActionMapping(string, string, bool, bool, bool, bool)](./PlayerInput-AddActionMapping(string_string_bool_bool_bool_bool).md 'UnrealEngine.Framework.PlayerInput.AddActionMapping(string, string, bool, bool, bool, bool)') +- [AddAxisMapping(string, string, float)](./PlayerInput-AddAxisMapping(string_string_float).md 'UnrealEngine.Framework.PlayerInput.AddAxisMapping(string, string, float)') +- [Equals(UnrealEngine.Framework.PlayerInput)](./PlayerInput-Equals(PlayerInput).md 'UnrealEngine.Framework.PlayerInput.Equals(UnrealEngine.Framework.PlayerInput)') +- [GetHashCode()](./PlayerInput-GetHashCode().md 'UnrealEngine.Framework.PlayerInput.GetHashCode()') +- [GetMouseSensitivity()](./PlayerInput-GetMouseSensitivity().md 'UnrealEngine.Framework.PlayerInput.GetMouseSensitivity()') +- [GetMouseSensitivity(System.Numerics.Vector2)](./PlayerInput-GetMouseSensitivity(Vector2).md 'UnrealEngine.Framework.PlayerInput.GetMouseSensitivity(System.Numerics.Vector2)') +- [GetTimeKeyPressed(string)](./PlayerInput-GetTimeKeyPressed(string).md 'UnrealEngine.Framework.PlayerInput.GetTimeKeyPressed(string)') +- [IsKeyPressed(string)](./PlayerInput-IsKeyPressed(string).md 'UnrealEngine.Framework.PlayerInput.IsKeyPressed(string)') +- [RemoveActionMapping(string, string)](./PlayerInput-RemoveActionMapping(string_string).md 'UnrealEngine.Framework.PlayerInput.RemoveActionMapping(string, string)') +- [RemoveAxisMapping(string, string)](./PlayerInput-RemoveAxisMapping(string_string).md 'UnrealEngine.Framework.PlayerInput.RemoveAxisMapping(string, string)') +- [SetMouseSensitivity(System.Numerics.Vector2)](./PlayerInput-SetMouseSensitivity(Vector2).md 'UnrealEngine.Framework.PlayerInput.SetMouseSensitivity(System.Numerics.Vector2)') + +``` + +`API/PointLight-PointLight(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PointLight](./PointLight.md 'UnrealEngine.Framework.PointLight') +## PointLight(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public PointLight(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/PointLight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## PointLight Class +Emits light in all directions from the light bulb's tungsten filament +```csharp +public class PointLight : UnrealEngine.Framework.Light +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 [Light](./Light.md 'UnrealEngine.Framework.Light') 🡒 PointLight +### Constructors +- [PointLight(string, UnrealEngine.Framework.Blueprint)](./PointLight-PointLight(string_Blueprint).md 'UnrealEngine.Framework.PointLight.PointLight(string, UnrealEngine.Framework.Blueprint)') + +``` + +`API/PostProcessComponent-BlendRadius.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PostProcessComponent](./PostProcessComponent.md 'UnrealEngine.Framework.PostProcessComponent') +## PostProcessComponent.BlendRadius Property +Gets or sets the world space radius around the volume that is used for blending if not unbound +```csharp +public float BlendRadius { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/PostProcessComponent-BlendWeight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PostProcessComponent](./PostProcessComponent.md 'UnrealEngine.Framework.PostProcessComponent') +## PostProcessComponent.BlendWeight Property +Gets or sets the blend weight, 0.0f indicates no effect, 1.0f indicates full effect +```csharp +public float BlendWeight { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/PostProcessComponent-Enabled.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PostProcessComponent](./PostProcessComponent.md 'UnrealEngine.Framework.PostProcessComponent') +## PostProcessComponent.Enabled Property +Gets or sets whether the volume is enabled +```csharp +public bool Enabled { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PostProcessComponent-PostProcessComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PostProcessComponent](./PostProcessComponent.md 'UnrealEngine.Framework.PostProcessComponent') +## PostProcessComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public PostProcessComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/PostProcessComponent-Priority.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PostProcessComponent](./PostProcessComponent.md 'UnrealEngine.Framework.PostProcessComponent') +## PostProcessComponent.Priority Property +Gets or sets the priority of the volume +```csharp +public float Priority { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/PostProcessComponent-Unbound.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PostProcessComponent](./PostProcessComponent.md 'UnrealEngine.Framework.PostProcessComponent') +## PostProcessComponent.Unbound Property +Gets or sets whether the volume covers the whole world or only the area inside its bounds +```csharp +public bool Unbound { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PostProcessComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## PostProcessComponent Class +A component that is used for post-processing manipulations +```csharp +public class PostProcessComponent : UnrealEngine.Framework.SceneComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 PostProcessComponent +### Constructors +- [PostProcessComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./PostProcessComponent-PostProcessComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.PostProcessComponent.PostProcessComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Properties +- [BlendRadius](./PostProcessComponent-BlendRadius.md 'UnrealEngine.Framework.PostProcessComponent.BlendRadius') +- [BlendWeight](./PostProcessComponent-BlendWeight.md 'UnrealEngine.Framework.PostProcessComponent.BlendWeight') +- [Enabled](./PostProcessComponent-Enabled.md 'UnrealEngine.Framework.PostProcessComponent.Enabled') +- [Priority](./PostProcessComponent-Priority.md 'UnrealEngine.Framework.PostProcessComponent.Priority') +- [Unbound](./PostProcessComponent-Unbound.md 'UnrealEngine.Framework.PostProcessComponent.Unbound') + +``` + +`API/PostProcessVolume-BlendRadius.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PostProcessVolume](./PostProcessVolume.md 'UnrealEngine.Framework.PostProcessVolume') +## PostProcessVolume.BlendRadius Property +Gets or sets the world space radius around the volume that is used for blending if not unbound +```csharp +public float BlendRadius { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/PostProcessVolume-BlendWeight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PostProcessVolume](./PostProcessVolume.md 'UnrealEngine.Framework.PostProcessVolume') +## PostProcessVolume.BlendWeight Property +Gets or sets the blend weight, 0.0f indicates no effect, 1.0f indicates full effect +```csharp +public float BlendWeight { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/PostProcessVolume-Enabled.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PostProcessVolume](./PostProcessVolume.md 'UnrealEngine.Framework.PostProcessVolume') +## PostProcessVolume.Enabled Property +Gets or sets whether the volume is enabled +```csharp +public bool Enabled { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PostProcessVolume-PostProcessVolume(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PostProcessVolume](./PostProcessVolume.md 'UnrealEngine.Framework.PostProcessVolume') +## PostProcessVolume(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public PostProcessVolume(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/PostProcessVolume-Priority.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PostProcessVolume](./PostProcessVolume.md 'UnrealEngine.Framework.PostProcessVolume') +## PostProcessVolume.Priority Property +Gets or sets the priority of the volume +```csharp +public float Priority { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/PostProcessVolume-Unbound.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PostProcessVolume](./PostProcessVolume.md 'UnrealEngine.Framework.PostProcessVolume') +## PostProcessVolume.Unbound Property +Gets or sets whether the volume covers the whole world or only the area inside its bounds +```csharp +public bool Unbound { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PostProcessVolume.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## PostProcessVolume Class +An actor that is used for post-processing manipulations +```csharp +public class PostProcessVolume : UnrealEngine.Framework.Volume +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 [Brush](./Brush.md 'UnrealEngine.Framework.Brush') 🡒 [Volume](./Volume.md 'UnrealEngine.Framework.Volume') 🡒 PostProcessVolume +### Constructors +- [PostProcessVolume(string, UnrealEngine.Framework.Blueprint)](./PostProcessVolume-PostProcessVolume(string_Blueprint).md 'UnrealEngine.Framework.PostProcessVolume.PostProcessVolume(string, UnrealEngine.Framework.Blueprint)') +### Properties +- [BlendRadius](./PostProcessVolume-BlendRadius.md 'UnrealEngine.Framework.PostProcessVolume.BlendRadius') +- [BlendWeight](./PostProcessVolume-BlendWeight.md 'UnrealEngine.Framework.PostProcessVolume.BlendWeight') +- [Enabled](./PostProcessVolume-Enabled.md 'UnrealEngine.Framework.PostProcessVolume.Enabled') +- [Priority](./PostProcessVolume-Priority.md 'UnrealEngine.Framework.PostProcessVolume.Priority') +- [Unbound](./PostProcessVolume-Unbound.md 'UnrealEngine.Framework.PostProcessVolume.Unbound') + +``` + +`API/PrimitiveComponent-AddAngularImpulseInDegrees(Vector3_string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.AddAngularImpulseInDegrees(System.Numerics.Vector3, string, bool) Method +Adds an angular impulse in degrees to a rigid body +```csharp +public void AddAngularImpulseInDegrees(in System.Numerics.Vector3 impulse, string boneName=null, bool velocityChange=false); +``` +#### Parameters + +`impulse` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Magnitude and direction of the impulse to apply, the direction is the axis of rotation + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +If applied to [SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent'), the name of the body to apply an angular impulse to, or `null` to indicate the root body + + +`velocityChange` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, [impulse](#UnrealEngine-Framework-PrimitiveComponent-AddAngularImpulseInDegrees(System-Numerics-Vector3_string_bool)-impulse 'UnrealEngine.Framework.PrimitiveComponent.AddAngularImpulseInDegrees(System.Numerics.Vector3, string, bool).impulse') is taken as a change in velocity instead of a physical force (the mass will have no effect) + + +``` + +`API/PrimitiveComponent-AddAngularImpulseInRadians(Vector3_string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.AddAngularImpulseInRadians(System.Numerics.Vector3, string, bool) Method +Adds an angular impulse in radians to a rigid body +```csharp +public void AddAngularImpulseInRadians(in System.Numerics.Vector3 impulse, string boneName=null, bool velocityChange=false); +``` +#### Parameters + +`impulse` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Magnitude and direction of the impulse to apply, the direction is the axis of rotation + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +If applied to [SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent'), the name of the body to apply an angular impulse to, or `null` to indicate the root body + + +`velocityChange` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, [impulse](#UnrealEngine-Framework-PrimitiveComponent-AddAngularImpulseInRadians(System-Numerics-Vector3_string_bool)-impulse 'UnrealEngine.Framework.PrimitiveComponent.AddAngularImpulseInRadians(System.Numerics.Vector3, string, bool).impulse') is taken as a change in velocity instead of a physical force (the mass will have no effect) + + +``` + +`API/PrimitiveComponent-AddForce(Vector3_string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.AddForce(System.Numerics.Vector3, string, bool) Method +Adds a force to a rigid body +```csharp +public void AddForce(in System.Numerics.Vector3 force, string boneName=null, bool accelerationChange=false); +``` +#### Parameters + +`force` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Force vector to apply, magnitude indicates strength of force + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +If applied to [SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent'), the name of the body to apply an angular impulse to, or `null` to indicate the root body + + +`accelerationChange` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, [force](#UnrealEngine-Framework-PrimitiveComponent-AddForce(System-Numerics-Vector3_string_bool)-force 'UnrealEngine.Framework.PrimitiveComponent.AddForce(System.Numerics.Vector3, string, bool).force') is taken as a change in acceleration instead of a physical force (the mass will have no effect) + + +``` + +`API/PrimitiveComponent-AddForceAtLocation(Vector3_Vector3_string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.AddForceAtLocation(System.Numerics.Vector3, System.Numerics.Vector3, string, bool) Method +Adds a force to a rigid body at a specific location, optionally in local space +```csharp +public void AddForceAtLocation(in System.Numerics.Vector3 force, in System.Numerics.Vector3 location, string boneName=null, bool localSpace=false); +``` +#### Parameters + +`force` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Force vector to apply, magnitude indicates strength of force + + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +A point in world or local space to apply the force at + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +If applied to [SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent'), the name of the body to apply an angular impulse to, or `null` to indicate the root body + + +`localSpace` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, applies force in local space instead of world space + + +``` + +`API/PrimitiveComponent-AddImpulse(Vector3_string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.AddImpulse(System.Numerics.Vector3, string, bool) Method +Adds an impulse to a rigid body +```csharp +public void AddImpulse(in System.Numerics.Vector3 impulse, string boneName=null, bool velocityChange=false); +``` +#### Parameters + +`impulse` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Magnitude and direction of the impulse to apply + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +If applied to [SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent'), the name of the body to apply an angular impulse to, or `null` to indicate the root body + + +`velocityChange` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, [impulse](#UnrealEngine-Framework-PrimitiveComponent-AddImpulse(System-Numerics-Vector3_string_bool)-impulse 'UnrealEngine.Framework.PrimitiveComponent.AddImpulse(System.Numerics.Vector3, string, bool).impulse') is taken as a change in velocity instead of a physical force (the mass will have no effect) + + +``` + +`API/PrimitiveComponent-AddImpulseAtLocation(Vector3_Vector3_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.AddImpulseAtLocation(System.Numerics.Vector3, System.Numerics.Vector3, string) Method +Adds an impulse to a rigid body at a specific location +```csharp +public void AddImpulseAtLocation(in System.Numerics.Vector3 impulse, in System.Numerics.Vector3 location, string boneName=null); +``` +#### Parameters + +`impulse` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Magnitude and direction of the impulse to apply + + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +A point in world space to apply the impulse at + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +If applied to [SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent'), the name of the body to apply an angular impulse to, or `null` to indicate the root body + + +``` + +`API/PrimitiveComponent-AddRadialForce(Vector3_float_float_bool_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.AddRadialForce(System.Numerics.Vector3, float, float, bool, bool) Method +Adds a force to all rigid bodies in the component, originating from the supplied world-space location +```csharp +public void AddRadialForce(in System.Numerics.Vector3 origin, float radius, float strength, bool linearFalloff=false, bool accelerationChange=false); +``` +#### Parameters + +`origin` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Origin of the force in world space + + +`radius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Radius within which to apply the force + + +`strength` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Strength of the force to apply + + +`linearFalloff` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, the force will lose its strength linearly + + +`accelerationChange` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, [strength](#UnrealEngine-Framework-PrimitiveComponent-AddRadialForce(System-Numerics-Vector3_float_float_bool_bool)-strength 'UnrealEngine.Framework.PrimitiveComponent.AddRadialForce(System.Numerics.Vector3, float, float, bool, bool).strength') is taken as a change in acceleration instead of a physical force (the mass will have no effect) + + +``` + +`API/PrimitiveComponent-AddRadialImpulse(Vector3_float_float_bool_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.AddRadialImpulse(System.Numerics.Vector3, float, float, bool, bool) Method +Adds an impulse to all rigid bodies in the component, originating from the supplied world-space location +```csharp +public void AddRadialImpulse(in System.Numerics.Vector3 origin, float radius, float strength, bool linearFalloff=false, bool accelerationChange=false); +``` +#### Parameters + +`origin` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Origin of the impulse in world space + + +`radius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Radius within which to apply the impulse + + +`strength` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +Strength of the impulse to apply + + +`linearFalloff` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, the force will lose its strength linearly + + +`accelerationChange` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, [strength](#UnrealEngine-Framework-PrimitiveComponent-AddRadialImpulse(System-Numerics-Vector3_float_float_bool_bool)-strength 'UnrealEngine.Framework.PrimitiveComponent.AddRadialImpulse(System.Numerics.Vector3, float, float, bool, bool).strength') is taken as a change in acceleration instead of a physical force (the mass will have no effect) + + +``` + +`API/PrimitiveComponent-AddTorqueInDegrees(Vector3_string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.AddTorqueInDegrees(System.Numerics.Vector3, string, bool) Method +Adds a torque in degrees to a rigid body +```csharp +public void AddTorqueInDegrees(in System.Numerics.Vector3 torque, string boneName=null, bool accelerationChange=false); +``` +#### Parameters + +`torque` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Torque to apply, direction is axis of rotation and magnitude is strength of the torque + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +If applied to [SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent'), the name of the body to apply an angular impulse to, or `null` to indicate the root body + + +`accelerationChange` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, [torque](#UnrealEngine-Framework-PrimitiveComponent-AddTorqueInDegrees(System-Numerics-Vector3_string_bool)-torque 'UnrealEngine.Framework.PrimitiveComponent.AddTorqueInDegrees(System.Numerics.Vector3, string, bool).torque') is taken as a change in acceleration instead of a physical force (the mass will have no effect) + + +``` + +`API/PrimitiveComponent-AddTorqueInRadians(Vector3_string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.AddTorqueInRadians(System.Numerics.Vector3, string, bool) Method +Adds a torque in radians to a rigid body +```csharp +public void AddTorqueInRadians(in System.Numerics.Vector3 torque, string boneName=null, bool accelerationChange=false); +``` +#### Parameters + +`torque` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') +Torque to apply, direction is axis of rotation and magnitude is strength of the torque + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +If applied to [SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent'), the name of the body to apply an angular impulse to, or `null` to indicate the root body + + +`accelerationChange` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, [torque](#UnrealEngine-Framework-PrimitiveComponent-AddTorqueInRadians(System-Numerics-Vector3_string_bool)-torque 'UnrealEngine.Framework.PrimitiveComponent.AddTorqueInRadians(System.Numerics.Vector3, string, bool).torque') is taken as a change in acceleration instead of a physical force (the mass will have no effect) + + +``` + +`API/PrimitiveComponent-AngularDamping.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.AngularDamping Property +Gets or sets the angular damping of the component +```csharp +public float AngularDamping { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/PrimitiveComponent-CastShadow.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.CastShadow Property +Gets or sets whether the component should cast a shadow +```csharp +public bool CastShadow { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PrimitiveComponent-ClearMoveIgnoreActors().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.ClearMoveIgnoreActors() Method +Clears the list of actors that ignored during the movement +```csharp +public void ClearMoveIgnoreActors(); +``` + +``` + +`API/PrimitiveComponent-ClearMoveIgnoreComponents().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.ClearMoveIgnoreComponents() Method +Clears the list of components that ignored during the movement +```csharp +public void ClearMoveIgnoreComponents(); +``` + +``` + +`API/PrimitiveComponent-CreateAndSetMaterialInstanceDynamic(int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.CreateAndSetMaterialInstanceDynamic(int) Method +Creates a dynamic material instance for the specified element index, the parent of the instance is set to the material being replaced +```csharp +public UnrealEngine.Framework.MaterialInstanceDynamic CreateAndSetMaterialInstanceDynamic(int elementIndex); +``` +#### Parameters + +`elementIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') +The index of the skin to replace the material for, if invalid, the material is unchanged and `null` is returned + +#### Returns +[MaterialInstanceDynamic](./MaterialInstanceDynamic.md 'UnrealEngine.Framework.MaterialInstanceDynamic') +A material instance or `null` on failure + +``` + +`API/PrimitiveComponent-ForEachOverlappingComponent-T-(Action-T-).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.ForEachOverlappingComponent<T>(System.Action<T>) Method +Performs the specified action on each overlapping component if any +```csharp +public void ForEachOverlappingComponent(System.Action action) + where T : UnrealEngine.Framework.PrimitiveComponent; +``` +#### Type parameters + +`T` + +Constraints [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Parameters + +`action` [System.Action<](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1')[T](#UnrealEngine-Framework-PrimitiveComponent-ForEachOverlappingComponent-T-(System-Action-T-)-T 'UnrealEngine.Framework.PrimitiveComponent.ForEachOverlappingComponent<T>(System.Action<T>).T')[>](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1') + + +``` + +`API/PrimitiveComponent-GetDistanceToCollision(Vector3_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.GetDistanceToCollision(System.Numerics.Vector3, System.Numerics.Vector3) Method +Retrieves distance to closest collision +```csharp +public float GetDistanceToCollision(in System.Numerics.Vector3 point, ref System.Numerics.Vector3 closestPointOnCollision); +``` +#### Parameters + +`point` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + + +`closestPointOnCollision` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') +More than 0.0f if successful, equals to 0.0f if a point is inside the geometry, less than 0.0f if the primitive does not have collision or if the geometry not supported + +``` + +`API/PrimitiveComponent-GetMaterial(int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.GetMaterial(int) Method +Returns the material at the specified element index or `null` on failure +```csharp +public UnrealEngine.Framework.MaterialInstanceDynamic GetMaterial(int elementIndex); +``` +#### Parameters + +`elementIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Returns +[MaterialInstanceDynamic](./MaterialInstanceDynamic.md 'UnrealEngine.Framework.MaterialInstanceDynamic') + +``` + +`API/PrimitiveComponent-GetPhysicsAngularVelocityInDegrees(Vector3_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.GetPhysicsAngularVelocityInDegrees(System.Numerics.Vector3, string) Method +Retrieves the angular velocity in degrees of a single body +```csharp +public void GetPhysicsAngularVelocityInDegrees(ref System.Numerics.Vector3 value, string boneName=null); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/PrimitiveComponent-GetPhysicsAngularVelocityInDegrees(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.GetPhysicsAngularVelocityInDegrees(string) Method +Returns the angular velocity in degrees of a single body +```csharp +public System.Numerics.Vector3 GetPhysicsAngularVelocityInDegrees(string boneName=null); +``` +#### Parameters + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/PrimitiveComponent-GetPhysicsAngularVelocityInRadians(Vector3_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.GetPhysicsAngularVelocityInRadians(System.Numerics.Vector3, string) Method +Retrieves the angular velocity in radians of a single body +```csharp +public void GetPhysicsAngularVelocityInRadians(ref System.Numerics.Vector3 value, string boneName=null); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/PrimitiveComponent-GetPhysicsAngularVelocityInRadians(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.GetPhysicsAngularVelocityInRadians(string) Method +Returns the angular velocity in radians of a single body +```csharp +public System.Numerics.Vector3 GetPhysicsAngularVelocityInRadians(string boneName=null); +``` +#### Parameters + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/PrimitiveComponent-GetPhysicsLinearVelocity(Vector3_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.GetPhysicsLinearVelocity(System.Numerics.Vector3, string) Method +Retrieves the linear velocity of a single body +```csharp +public void GetPhysicsLinearVelocity(ref System.Numerics.Vector3 value, string boneName=null); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/PrimitiveComponent-GetPhysicsLinearVelocity(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.GetPhysicsLinearVelocity(string) Method +Returns the linear velocity of a single body +```csharp +public System.Numerics.Vector3 GetPhysicsLinearVelocity(string boneName=null); +``` +#### Parameters + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/PrimitiveComponent-GetPhysicsLinearVelocityAtPoint(Vector3_Vector3_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.GetPhysicsLinearVelocityAtPoint(System.Numerics.Vector3, System.Numerics.Vector3, string) Method +Retrieves the linear velocity of a point on a single body +```csharp +public void GetPhysicsLinearVelocityAtPoint(ref System.Numerics.Vector3 value, in System.Numerics.Vector3 point, string boneName=null); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`point` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/PrimitiveComponent-GetPhysicsLinearVelocityAtPoint(Vector3_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.GetPhysicsLinearVelocityAtPoint(System.Numerics.Vector3, string) Method +Returns the linear velocity of a point on a single body +```csharp +public System.Numerics.Vector3 GetPhysicsLinearVelocityAtPoint(in System.Numerics.Vector3 point, string boneName=null); +``` +#### Parameters + +`point` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/PrimitiveComponent-GetSquaredDistanceToCollision(Vector3_float_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.GetSquaredDistanceToCollision(System.Numerics.Vector3, float, System.Numerics.Vector3) Method +Retrieves squared distance to closest collision +```csharp +public bool GetSquaredDistanceToCollision(in System.Numerics.Vector3 point, ref float squaredDistance, ref System.Numerics.Vector3 closestPointOnCollision); +``` +#### Parameters + +`point` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + + +`squaredDistance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + + +`closestPointOnCollision` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if a distance to the body was found and [squaredDistance](#UnrealEngine-Framework-PrimitiveComponent-GetSquaredDistanceToCollision(System-Numerics-Vector3_float_System-Numerics-Vector3)-squaredDistance 'UnrealEngine.Framework.PrimitiveComponent.GetSquaredDistanceToCollision(System.Numerics.Vector3, float, System.Numerics.Vector3).squaredDistance') has been populated + +``` + +`API/PrimitiveComponent-IgnoreRadialForce.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.IgnoreRadialForce Property +Gets or sets whether the component should ignore radial forces +```csharp +public bool IgnoreRadialForce { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PrimitiveComponent-IgnoreRadialImpulse.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.IgnoreRadialImpulse Property +Gets or sets whether the component should ignore radial impulses +```csharp +public bool IgnoreRadialImpulse { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PrimitiveComponent-IsGravityEnabled.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.IsGravityEnabled Property +Returns `true` if the component is affected by gravity, always returns `false` if physics simulation is disabled for the component +```csharp +public bool IsGravityEnabled { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PrimitiveComponent-IsOverlappingComponent(PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.IsOverlappingComponent(UnrealEngine.Framework.PrimitiveComponent) Method +Returns `true` if the component is overlapping another component +```csharp +public bool IsOverlappingComponent(UnrealEngine.Framework.PrimitiveComponent other); +``` +#### Parameters + +`other` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PrimitiveComponent-LinearDamping.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.LinearDamping Property +Gets or sets the linear damping of the component +```csharp +public float LinearDamping { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/PrimitiveComponent-Mass.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.Mass Property +Returns approximate mass in kilograms +```csharp +public float Mass { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/PrimitiveComponent-MaterialsNumber.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.MaterialsNumber Property +Returns number of material elements in the primitive +```csharp +public int MaterialsNumber { get; } +``` +#### Property Value +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/PrimitiveComponent-OnlyOwnerSee.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.OnlyOwnerSee Property +Gets or sets the component visibility when the view actor is the component's owner +```csharp +public bool OnlyOwnerSee { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PrimitiveComponent-OwnerNoSee.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.OwnerNoSee Property +Gets or sets whether the component would not be visible when the view actor is the component's owner, directly or indirectly +```csharp +public bool OwnerNoSee { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/PrimitiveComponent-RegisterEvent(ComponentEventType).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.RegisterEvent(UnrealEngine.Framework.ComponentEventType) Method +Registers an event notification for the primitive component +```csharp +public void RegisterEvent(UnrealEngine.Framework.ComponentEventType type); +``` +#### Parameters + +`type` [ComponentEventType](./ComponentEventType.md 'UnrealEngine.Framework.ComponentEventType') + + +``` + +`API/PrimitiveComponent-SetCenterOfMass(Vector3_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetCenterOfMass(System.Numerics.Vector3, string) Method +Sets the center of mass of a single body +```csharp +public void SetCenterOfMass(in System.Numerics.Vector3 offset, string boneName=null); +``` +#### Parameters + +`offset` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/PrimitiveComponent-SetCollisionChannel(CollisionChannel).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetCollisionChannel(UnrealEngine.Framework.CollisionChannel) Method +Sets the collision channel of the component +```csharp +public void SetCollisionChannel(UnrealEngine.Framework.CollisionChannel channel); +``` +#### Parameters + +`channel` [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + + +``` + +`API/PrimitiveComponent-SetCollisionMode(CollisionMode).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetCollisionMode(UnrealEngine.Framework.CollisionMode) Method +Sets the collision mode of the component +```csharp +public void SetCollisionMode(UnrealEngine.Framework.CollisionMode mode); +``` +#### Parameters + +`mode` [CollisionMode](./CollisionMode.md 'UnrealEngine.Framework.CollisionMode') + + +``` + +`API/PrimitiveComponent-SetCollisionProfileName(string_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetCollisionProfileName(string, bool) Method +Sets the collision profile name of the component +```csharp +public void SetCollisionProfileName(string profileName, bool updateOverlaps=true); +``` +#### Parameters + +`profileName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`updateOverlaps` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/PrimitiveComponent-SetCollisionResponseToAllChannels(CollisionResponse).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetCollisionResponseToAllChannels(UnrealEngine.Framework.CollisionResponse) Method +Sets the collision response to all channels of the component +```csharp +public void SetCollisionResponseToAllChannels(UnrealEngine.Framework.CollisionResponse response); +``` +#### Parameters + +`response` [CollisionResponse](./CollisionResponse.md 'UnrealEngine.Framework.CollisionResponse') + + +``` + +`API/PrimitiveComponent-SetCollisionResponseToChannel(CollisionChannel_CollisionResponse).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetCollisionResponseToChannel(UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionResponse) Method +Sets the collision response to channel of the component +```csharp +public void SetCollisionResponseToChannel(UnrealEngine.Framework.CollisionChannel channel, UnrealEngine.Framework.CollisionResponse response); +``` +#### Parameters + +`channel` [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + + +`response` [CollisionResponse](./CollisionResponse.md 'UnrealEngine.Framework.CollisionResponse') + + +``` + +`API/PrimitiveComponent-SetEnableGravity(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetEnableGravity(bool) Method +Sets whether the component is affected by gravity, applies only to components with enabled physics simulation +```csharp +public void SetEnableGravity(bool value); +``` +#### Parameters + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/PrimitiveComponent-SetGenerateHitEvents(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetGenerateHitEvents(bool) Method +Sets whether the component should generate hit events when it's collides with other components +```csharp +public void SetGenerateHitEvents(bool value); +``` +#### Parameters + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/PrimitiveComponent-SetGenerateOverlapEvents(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetGenerateOverlapEvents(bool) Method +Sets whether the component should generate overlap events when it's overlaps other components +```csharp +public void SetGenerateOverlapEvents(bool value); +``` +#### Parameters + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/PrimitiveComponent-SetIgnoreActorWhenMoving(Actor_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetIgnoreActorWhenMoving(UnrealEngine.Framework.Actor, bool) Method +Sets whether to ignore collision of all components of a specified actor during the movement +```csharp +public void SetIgnoreActorWhenMoving(UnrealEngine.Framework.Actor actor, bool value); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/PrimitiveComponent-SetIgnoreComponentWhenMoving(PrimitiveComponent_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetIgnoreComponentWhenMoving(UnrealEngine.Framework.PrimitiveComponent, bool) Method +Sets whether to ignore collision of a specified component during the movement +```csharp +public void SetIgnoreComponentWhenMoving(UnrealEngine.Framework.PrimitiveComponent component, bool value); +``` +#### Parameters + +`component` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/PrimitiveComponent-SetMass(float_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetMass(float, string) Method +Sets the mass in kilograms of a rigid body +```csharp +public void SetMass(float mass, string boneName=null); +``` +#### Parameters + +`mass` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/PrimitiveComponent-SetMaterial(int_MaterialInterface).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetMaterial(int, UnrealEngine.Framework.MaterialInterface) Method +Sets the material applied to an element of the mesh +```csharp +public void SetMaterial(int elementIndex, UnrealEngine.Framework.MaterialInterface material); +``` +#### Parameters + +`elementIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') +The element to access the material of + + +`material` [MaterialInterface](./MaterialInterface.md 'UnrealEngine.Framework.MaterialInterface') +A material + + +``` + +`API/PrimitiveComponent-SetPhysicsAngularVelocityInDegrees(Vector3_bool_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetPhysicsAngularVelocityInDegrees(System.Numerics.Vector3, bool, string) Method +Sets the angular velocity in degrees of a single body +```csharp +public void SetPhysicsAngularVelocityInDegrees(in System.Numerics.Vector3 angularVelocity, bool addToCurrent=false, string boneName=null); +``` +#### Parameters + +`angularVelocity` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`addToCurrent` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/PrimitiveComponent-SetPhysicsAngularVelocityInRadians(Vector3_bool_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetPhysicsAngularVelocityInRadians(System.Numerics.Vector3, bool, string) Method +Sets the angular velocity in radians of a single body +```csharp +public void SetPhysicsAngularVelocityInRadians(in System.Numerics.Vector3 angularVelocity, bool addToCurrent=false, string boneName=null); +``` +#### Parameters + +`angularVelocity` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`addToCurrent` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/PrimitiveComponent-SetPhysicsLinearVelocity(Vector3_bool_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetPhysicsLinearVelocity(System.Numerics.Vector3, bool, string) Method +Sets the linear velocity of a single body +```csharp +public void SetPhysicsLinearVelocity(in System.Numerics.Vector3 velocity, bool addToCurrent=false, string boneName=null); +``` +#### Parameters + +`velocity` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`addToCurrent` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/PrimitiveComponent-SetPhysicsMaxAngularVelocityInDegrees(float_bool_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetPhysicsMaxAngularVelocityInDegrees(float, bool, string) Method +Sets the maximum angular velocity in degrees of a single body +```csharp +public void SetPhysicsMaxAngularVelocityInDegrees(float maxAngularVelocity, bool addToCurrent=false, string boneName=null); +``` +#### Parameters + +`maxAngularVelocity` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`addToCurrent` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/PrimitiveComponent-SetPhysicsMaxAngularVelocityInRadians(float_bool_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetPhysicsMaxAngularVelocityInRadians(float, bool, string) Method +Sets the maximum angular velocity in radians of a single body +```csharp +public void SetPhysicsMaxAngularVelocityInRadians(float maxAngularVelocity, bool addToCurrent=false, string boneName=null); +``` +#### Parameters + +`maxAngularVelocity` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`addToCurrent` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/PrimitiveComponent-SetSimulatePhysics(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.SetSimulatePhysics(bool) Method +Sets whether a single body should use physics simulation, or should be kinematic, if the component is currently attached to something, beginning simulation will detach it +```csharp +public void SetSimulatePhysics(bool value); +``` +#### Parameters + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/PrimitiveComponent-UnregisterEvent(ComponentEventType).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +## PrimitiveComponent.UnregisterEvent(UnrealEngine.Framework.ComponentEventType) Method +Unregisters an event notification for the primitive component +```csharp +public void UnregisterEvent(UnrealEngine.Framework.ComponentEventType type); +``` +#### Parameters + +`type` [ComponentEventType](./ComponentEventType.md 'UnrealEngine.Framework.ComponentEventType') + + +``` + +`API/PrimitiveComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## PrimitiveComponent Class +An abstract component that contains or generates some sort of geometry, generally to be rendered or used as collision data +```csharp +public abstract class PrimitiveComponent : UnrealEngine.Framework.SceneComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 PrimitiveComponent + +Derived +↳ [MeshComponent](./MeshComponent.md 'UnrealEngine.Framework.MeshComponent') +↳ [MotionControllerComponent](./MotionControllerComponent.md 'UnrealEngine.Framework.MotionControllerComponent') +↳ [ShapeComponent](./ShapeComponent.md 'UnrealEngine.Framework.ShapeComponent') +↳ [SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +↳ [TextRenderComponent](./TextRenderComponent.md 'UnrealEngine.Framework.TextRenderComponent') +### Properties +- [AngularDamping](./PrimitiveComponent-AngularDamping.md 'UnrealEngine.Framework.PrimitiveComponent.AngularDamping') +- [CastShadow](./PrimitiveComponent-CastShadow.md 'UnrealEngine.Framework.PrimitiveComponent.CastShadow') +- [IgnoreRadialForce](./PrimitiveComponent-IgnoreRadialForce.md 'UnrealEngine.Framework.PrimitiveComponent.IgnoreRadialForce') +- [IgnoreRadialImpulse](./PrimitiveComponent-IgnoreRadialImpulse.md 'UnrealEngine.Framework.PrimitiveComponent.IgnoreRadialImpulse') +- [IsGravityEnabled](./PrimitiveComponent-IsGravityEnabled.md 'UnrealEngine.Framework.PrimitiveComponent.IsGravityEnabled') +- [LinearDamping](./PrimitiveComponent-LinearDamping.md 'UnrealEngine.Framework.PrimitiveComponent.LinearDamping') +- [Mass](./PrimitiveComponent-Mass.md 'UnrealEngine.Framework.PrimitiveComponent.Mass') +- [MaterialsNumber](./PrimitiveComponent-MaterialsNumber.md 'UnrealEngine.Framework.PrimitiveComponent.MaterialsNumber') +- [OnlyOwnerSee](./PrimitiveComponent-OnlyOwnerSee.md 'UnrealEngine.Framework.PrimitiveComponent.OnlyOwnerSee') +- [OwnerNoSee](./PrimitiveComponent-OwnerNoSee.md 'UnrealEngine.Framework.PrimitiveComponent.OwnerNoSee') +### Methods +- [AddAngularImpulseInDegrees(System.Numerics.Vector3, string, bool)](./PrimitiveComponent-AddAngularImpulseInDegrees(Vector3_string_bool).md 'UnrealEngine.Framework.PrimitiveComponent.AddAngularImpulseInDegrees(System.Numerics.Vector3, string, bool)') +- [AddAngularImpulseInRadians(System.Numerics.Vector3, string, bool)](./PrimitiveComponent-AddAngularImpulseInRadians(Vector3_string_bool).md 'UnrealEngine.Framework.PrimitiveComponent.AddAngularImpulseInRadians(System.Numerics.Vector3, string, bool)') +- [AddForce(System.Numerics.Vector3, string, bool)](./PrimitiveComponent-AddForce(Vector3_string_bool).md 'UnrealEngine.Framework.PrimitiveComponent.AddForce(System.Numerics.Vector3, string, bool)') +- [AddForceAtLocation(System.Numerics.Vector3, System.Numerics.Vector3, string, bool)](./PrimitiveComponent-AddForceAtLocation(Vector3_Vector3_string_bool).md 'UnrealEngine.Framework.PrimitiveComponent.AddForceAtLocation(System.Numerics.Vector3, System.Numerics.Vector3, string, bool)') +- [AddImpulse(System.Numerics.Vector3, string, bool)](./PrimitiveComponent-AddImpulse(Vector3_string_bool).md 'UnrealEngine.Framework.PrimitiveComponent.AddImpulse(System.Numerics.Vector3, string, bool)') +- [AddImpulseAtLocation(System.Numerics.Vector3, System.Numerics.Vector3, string)](./PrimitiveComponent-AddImpulseAtLocation(Vector3_Vector3_string).md 'UnrealEngine.Framework.PrimitiveComponent.AddImpulseAtLocation(System.Numerics.Vector3, System.Numerics.Vector3, string)') +- [AddRadialForce(System.Numerics.Vector3, float, float, bool, bool)](./PrimitiveComponent-AddRadialForce(Vector3_float_float_bool_bool).md 'UnrealEngine.Framework.PrimitiveComponent.AddRadialForce(System.Numerics.Vector3, float, float, bool, bool)') +- [AddRadialImpulse(System.Numerics.Vector3, float, float, bool, bool)](./PrimitiveComponent-AddRadialImpulse(Vector3_float_float_bool_bool).md 'UnrealEngine.Framework.PrimitiveComponent.AddRadialImpulse(System.Numerics.Vector3, float, float, bool, bool)') +- [AddTorqueInDegrees(System.Numerics.Vector3, string, bool)](./PrimitiveComponent-AddTorqueInDegrees(Vector3_string_bool).md 'UnrealEngine.Framework.PrimitiveComponent.AddTorqueInDegrees(System.Numerics.Vector3, string, bool)') +- [AddTorqueInRadians(System.Numerics.Vector3, string, bool)](./PrimitiveComponent-AddTorqueInRadians(Vector3_string_bool).md 'UnrealEngine.Framework.PrimitiveComponent.AddTorqueInRadians(System.Numerics.Vector3, string, bool)') +- [ClearMoveIgnoreActors()](./PrimitiveComponent-ClearMoveIgnoreActors().md 'UnrealEngine.Framework.PrimitiveComponent.ClearMoveIgnoreActors()') +- [ClearMoveIgnoreComponents()](./PrimitiveComponent-ClearMoveIgnoreComponents().md 'UnrealEngine.Framework.PrimitiveComponent.ClearMoveIgnoreComponents()') +- [CreateAndSetMaterialInstanceDynamic(int)](./PrimitiveComponent-CreateAndSetMaterialInstanceDynamic(int).md 'UnrealEngine.Framework.PrimitiveComponent.CreateAndSetMaterialInstanceDynamic(int)') +- [ForEachOverlappingComponent<T>(System.Action<T>)](./PrimitiveComponent-ForEachOverlappingComponent-T-(Action-T-).md 'UnrealEngine.Framework.PrimitiveComponent.ForEachOverlappingComponent<T>(System.Action<T>)') +- [GetDistanceToCollision(System.Numerics.Vector3, System.Numerics.Vector3)](./PrimitiveComponent-GetDistanceToCollision(Vector3_Vector3).md 'UnrealEngine.Framework.PrimitiveComponent.GetDistanceToCollision(System.Numerics.Vector3, System.Numerics.Vector3)') +- [GetMaterial(int)](./PrimitiveComponent-GetMaterial(int).md 'UnrealEngine.Framework.PrimitiveComponent.GetMaterial(int)') +- [GetPhysicsAngularVelocityInDegrees(System.Numerics.Vector3, string)](./PrimitiveComponent-GetPhysicsAngularVelocityInDegrees(Vector3_string).md 'UnrealEngine.Framework.PrimitiveComponent.GetPhysicsAngularVelocityInDegrees(System.Numerics.Vector3, string)') +- [GetPhysicsAngularVelocityInDegrees(string)](./PrimitiveComponent-GetPhysicsAngularVelocityInDegrees(string).md 'UnrealEngine.Framework.PrimitiveComponent.GetPhysicsAngularVelocityInDegrees(string)') +- [GetPhysicsAngularVelocityInRadians(System.Numerics.Vector3, string)](./PrimitiveComponent-GetPhysicsAngularVelocityInRadians(Vector3_string).md 'UnrealEngine.Framework.PrimitiveComponent.GetPhysicsAngularVelocityInRadians(System.Numerics.Vector3, string)') +- [GetPhysicsAngularVelocityInRadians(string)](./PrimitiveComponent-GetPhysicsAngularVelocityInRadians(string).md 'UnrealEngine.Framework.PrimitiveComponent.GetPhysicsAngularVelocityInRadians(string)') +- [GetPhysicsLinearVelocity(System.Numerics.Vector3, string)](./PrimitiveComponent-GetPhysicsLinearVelocity(Vector3_string).md 'UnrealEngine.Framework.PrimitiveComponent.GetPhysicsLinearVelocity(System.Numerics.Vector3, string)') +- [GetPhysicsLinearVelocity(string)](./PrimitiveComponent-GetPhysicsLinearVelocity(string).md 'UnrealEngine.Framework.PrimitiveComponent.GetPhysicsLinearVelocity(string)') +- [GetPhysicsLinearVelocityAtPoint(System.Numerics.Vector3, System.Numerics.Vector3, string)](./PrimitiveComponent-GetPhysicsLinearVelocityAtPoint(Vector3_Vector3_string).md 'UnrealEngine.Framework.PrimitiveComponent.GetPhysicsLinearVelocityAtPoint(System.Numerics.Vector3, System.Numerics.Vector3, string)') +- [GetPhysicsLinearVelocityAtPoint(System.Numerics.Vector3, string)](./PrimitiveComponent-GetPhysicsLinearVelocityAtPoint(Vector3_string).md 'UnrealEngine.Framework.PrimitiveComponent.GetPhysicsLinearVelocityAtPoint(System.Numerics.Vector3, string)') +- [GetSquaredDistanceToCollision(System.Numerics.Vector3, float, System.Numerics.Vector3)](./PrimitiveComponent-GetSquaredDistanceToCollision(Vector3_float_Vector3).md 'UnrealEngine.Framework.PrimitiveComponent.GetSquaredDistanceToCollision(System.Numerics.Vector3, float, System.Numerics.Vector3)') +- [IsOverlappingComponent(UnrealEngine.Framework.PrimitiveComponent)](./PrimitiveComponent-IsOverlappingComponent(PrimitiveComponent).md 'UnrealEngine.Framework.PrimitiveComponent.IsOverlappingComponent(UnrealEngine.Framework.PrimitiveComponent)') +- [RegisterEvent(UnrealEngine.Framework.ComponentEventType)](./PrimitiveComponent-RegisterEvent(ComponentEventType).md 'UnrealEngine.Framework.PrimitiveComponent.RegisterEvent(UnrealEngine.Framework.ComponentEventType)') +- [SetCenterOfMass(System.Numerics.Vector3, string)](./PrimitiveComponent-SetCenterOfMass(Vector3_string).md 'UnrealEngine.Framework.PrimitiveComponent.SetCenterOfMass(System.Numerics.Vector3, string)') +- [SetCollisionChannel(UnrealEngine.Framework.CollisionChannel)](./PrimitiveComponent-SetCollisionChannel(CollisionChannel).md 'UnrealEngine.Framework.PrimitiveComponent.SetCollisionChannel(UnrealEngine.Framework.CollisionChannel)') +- [SetCollisionMode(UnrealEngine.Framework.CollisionMode)](./PrimitiveComponent-SetCollisionMode(CollisionMode).md 'UnrealEngine.Framework.PrimitiveComponent.SetCollisionMode(UnrealEngine.Framework.CollisionMode)') +- [SetCollisionProfileName(string, bool)](./PrimitiveComponent-SetCollisionProfileName(string_bool).md 'UnrealEngine.Framework.PrimitiveComponent.SetCollisionProfileName(string, bool)') +- [SetCollisionResponseToAllChannels(UnrealEngine.Framework.CollisionResponse)](./PrimitiveComponent-SetCollisionResponseToAllChannels(CollisionResponse).md 'UnrealEngine.Framework.PrimitiveComponent.SetCollisionResponseToAllChannels(UnrealEngine.Framework.CollisionResponse)') +- [SetCollisionResponseToChannel(UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionResponse)](./PrimitiveComponent-SetCollisionResponseToChannel(CollisionChannel_CollisionResponse).md 'UnrealEngine.Framework.PrimitiveComponent.SetCollisionResponseToChannel(UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionResponse)') +- [SetEnableGravity(bool)](./PrimitiveComponent-SetEnableGravity(bool).md 'UnrealEngine.Framework.PrimitiveComponent.SetEnableGravity(bool)') +- [SetGenerateHitEvents(bool)](./PrimitiveComponent-SetGenerateHitEvents(bool).md 'UnrealEngine.Framework.PrimitiveComponent.SetGenerateHitEvents(bool)') +- [SetGenerateOverlapEvents(bool)](./PrimitiveComponent-SetGenerateOverlapEvents(bool).md 'UnrealEngine.Framework.PrimitiveComponent.SetGenerateOverlapEvents(bool)') +- [SetIgnoreActorWhenMoving(UnrealEngine.Framework.Actor, bool)](./PrimitiveComponent-SetIgnoreActorWhenMoving(Actor_bool).md 'UnrealEngine.Framework.PrimitiveComponent.SetIgnoreActorWhenMoving(UnrealEngine.Framework.Actor, bool)') +- [SetIgnoreComponentWhenMoving(UnrealEngine.Framework.PrimitiveComponent, bool)](./PrimitiveComponent-SetIgnoreComponentWhenMoving(PrimitiveComponent_bool).md 'UnrealEngine.Framework.PrimitiveComponent.SetIgnoreComponentWhenMoving(UnrealEngine.Framework.PrimitiveComponent, bool)') +- [SetMass(float, string)](./PrimitiveComponent-SetMass(float_string).md 'UnrealEngine.Framework.PrimitiveComponent.SetMass(float, string)') +- [SetMaterial(int, UnrealEngine.Framework.MaterialInterface)](./PrimitiveComponent-SetMaterial(int_MaterialInterface).md 'UnrealEngine.Framework.PrimitiveComponent.SetMaterial(int, UnrealEngine.Framework.MaterialInterface)') +- [SetPhysicsAngularVelocityInDegrees(System.Numerics.Vector3, bool, string)](./PrimitiveComponent-SetPhysicsAngularVelocityInDegrees(Vector3_bool_string).md 'UnrealEngine.Framework.PrimitiveComponent.SetPhysicsAngularVelocityInDegrees(System.Numerics.Vector3, bool, string)') +- [SetPhysicsAngularVelocityInRadians(System.Numerics.Vector3, bool, string)](./PrimitiveComponent-SetPhysicsAngularVelocityInRadians(Vector3_bool_string).md 'UnrealEngine.Framework.PrimitiveComponent.SetPhysicsAngularVelocityInRadians(System.Numerics.Vector3, bool, string)') +- [SetPhysicsLinearVelocity(System.Numerics.Vector3, bool, string)](./PrimitiveComponent-SetPhysicsLinearVelocity(Vector3_bool_string).md 'UnrealEngine.Framework.PrimitiveComponent.SetPhysicsLinearVelocity(System.Numerics.Vector3, bool, string)') +- [SetPhysicsMaxAngularVelocityInDegrees(float, bool, string)](./PrimitiveComponent-SetPhysicsMaxAngularVelocityInDegrees(float_bool_string).md 'UnrealEngine.Framework.PrimitiveComponent.SetPhysicsMaxAngularVelocityInDegrees(float, bool, string)') +- [SetPhysicsMaxAngularVelocityInRadians(float, bool, string)](./PrimitiveComponent-SetPhysicsMaxAngularVelocityInRadians(float_bool_string).md 'UnrealEngine.Framework.PrimitiveComponent.SetPhysicsMaxAngularVelocityInRadians(float, bool, string)') +- [SetSimulatePhysics(bool)](./PrimitiveComponent-SetSimulatePhysics(bool).md 'UnrealEngine.Framework.PrimitiveComponent.SetSimulatePhysics(bool)') +- [UnregisterEvent(UnrealEngine.Framework.ComponentEventType)](./PrimitiveComponent-UnregisterEvent(ComponentEventType).md 'UnrealEngine.Framework.PrimitiveComponent.UnregisterEvent(UnrealEngine.Framework.ComponentEventType)') + +``` + +`API/RadialForceComponent-AddCollisionChannelToAffect(CollisionChannel).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RadialForceComponent](./RadialForceComponent.md 'UnrealEngine.Framework.RadialForceComponent') +## RadialForceComponent.AddCollisionChannelToAffect(UnrealEngine.Framework.CollisionChannel) Method +Adds a collision channel that will be affected by the radial force +```csharp +public void AddCollisionChannelToAffect(UnrealEngine.Framework.CollisionChannel channel); +``` +#### Parameters + +`channel` [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + + +``` + +`API/RadialForceComponent-FireImpulse().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RadialForceComponent](./RadialForceComponent.md 'UnrealEngine.Framework.RadialForceComponent') +## RadialForceComponent.FireImpulse() Method +Fires a single impulse +```csharp +public void FireImpulse(); +``` + +``` + +`API/RadialForceComponent-ForceStrength.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RadialForceComponent](./RadialForceComponent.md 'UnrealEngine.Framework.RadialForceComponent') +## RadialForceComponent.ForceStrength Property +Gets or sets the force strength +```csharp +public float ForceStrength { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/RadialForceComponent-IgnoreOwningActor.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RadialForceComponent](./RadialForceComponent.md 'UnrealEngine.Framework.RadialForceComponent') +## RadialForceComponent.IgnoreOwningActor Property +Gets or sets whether to apply the force or impulse to any physics objects that are part of the actor which owns the component +```csharp +public bool IgnoreOwningActor { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/RadialForceComponent-ImpulseStrength.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RadialForceComponent](./RadialForceComponent.md 'UnrealEngine.Framework.RadialForceComponent') +## RadialForceComponent.ImpulseStrength Property +Gets or sets the impulse strength +```csharp +public float ImpulseStrength { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/RadialForceComponent-ImpulseVelocityChange.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RadialForceComponent](./RadialForceComponent.md 'UnrealEngine.Framework.RadialForceComponent') +## RadialForceComponent.ImpulseVelocityChange Property +Gets or sets whether the impulse will ignore the mass of objects and will always result in a fixed velocity change +```csharp +public bool ImpulseVelocityChange { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/RadialForceComponent-LinearFalloff.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RadialForceComponent](./RadialForceComponent.md 'UnrealEngine.Framework.RadialForceComponent') +## RadialForceComponent.LinearFalloff Property +Gets or sets whether the force or impulse should lose its strength linearly +```csharp +public bool LinearFalloff { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/RadialForceComponent-RadialForceComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RadialForceComponent](./RadialForceComponent.md 'UnrealEngine.Framework.RadialForceComponent') +## RadialForceComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public RadialForceComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/RadialForceComponent-Radius.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RadialForceComponent](./RadialForceComponent.md 'UnrealEngine.Framework.RadialForceComponent') +## RadialForceComponent.Radius Property +Gets or sets the radius within the force or impulse will be applied +```csharp +public float Radius { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/RadialForceComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## RadialForceComponent Class +A component that emits a radial force or impulse that can affect physics objects and destructible objects +```csharp +public class RadialForceComponent : UnrealEngine.Framework.SceneComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 RadialForceComponent +### Constructors +- [RadialForceComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./RadialForceComponent-RadialForceComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.RadialForceComponent.RadialForceComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Properties +- [ForceStrength](./RadialForceComponent-ForceStrength.md 'UnrealEngine.Framework.RadialForceComponent.ForceStrength') +- [IgnoreOwningActor](./RadialForceComponent-IgnoreOwningActor.md 'UnrealEngine.Framework.RadialForceComponent.IgnoreOwningActor') +- [ImpulseStrength](./RadialForceComponent-ImpulseStrength.md 'UnrealEngine.Framework.RadialForceComponent.ImpulseStrength') +- [ImpulseVelocityChange](./RadialForceComponent-ImpulseVelocityChange.md 'UnrealEngine.Framework.RadialForceComponent.ImpulseVelocityChange') +- [LinearFalloff](./RadialForceComponent-LinearFalloff.md 'UnrealEngine.Framework.RadialForceComponent.LinearFalloff') +- [Radius](./RadialForceComponent-Radius.md 'UnrealEngine.Framework.RadialForceComponent.Radius') +### Methods +- [AddCollisionChannelToAffect(UnrealEngine.Framework.CollisionChannel)](./RadialForceComponent-AddCollisionChannelToAffect(CollisionChannel).md 'UnrealEngine.Framework.RadialForceComponent.AddCollisionChannelToAffect(UnrealEngine.Framework.CollisionChannel)') +- [FireImpulse()](./RadialForceComponent-FireImpulse().md 'UnrealEngine.Framework.RadialForceComponent.FireImpulse()') + +``` + +`API/RectLight-RectLight(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RectLight](./RectLight.md 'UnrealEngine.Framework.RectLight') +## RectLight(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public RectLight(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/RectLight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## RectLight Class +Emits light into the scene from a rectangular plane with a defined width and height +```csharp +public class RectLight : UnrealEngine.Framework.Light +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 [Light](./Light.md 'UnrealEngine.Framework.Light') 🡒 RectLight +### Constructors +- [RectLight(string, UnrealEngine.Framework.Blueprint)](./RectLight-RectLight(string_Blueprint).md 'UnrealEngine.Framework.RectLight.RectLight(string, UnrealEngine.Framework.Blueprint)') + +``` + +`API/RotatingMovementComponent-GetPivotTranslation().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RotatingMovementComponent](./RotatingMovementComponent.md 'UnrealEngine.Framework.RotatingMovementComponent') +## RotatingMovementComponent.GetPivotTranslation() Method +Returns translation of pivot point around which the component rotates, relative to the current rotation +```csharp +public System.Numerics.Vector3 GetPivotTranslation(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/RotatingMovementComponent-GetPivotTranslation(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RotatingMovementComponent](./RotatingMovementComponent.md 'UnrealEngine.Framework.RotatingMovementComponent') +## RotatingMovementComponent.GetPivotTranslation(System.Numerics.Vector3) Method +Retrieves translation of pivot point around which the component rotates, relative to the current rotation +```csharp +public void GetPivotTranslation(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/RotatingMovementComponent-GetRotationRate().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RotatingMovementComponent](./RotatingMovementComponent.md 'UnrealEngine.Framework.RotatingMovementComponent') +## RotatingMovementComponent.GetRotationRate() Method +Returns yaw, pitch, and roll rotation rate of the component +```csharp +public System.Numerics.Quaternion GetRotationRate(); +``` +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/RotatingMovementComponent-GetRotationRate(Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RotatingMovementComponent](./RotatingMovementComponent.md 'UnrealEngine.Framework.RotatingMovementComponent') +## RotatingMovementComponent.GetRotationRate(System.Numerics.Quaternion) Method +Retrieves yaw, pitch, and roll rotation rate of the component +```csharp +public void GetRotationRate(ref System.Numerics.Quaternion value); +``` +#### Parameters + +`value` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/RotatingMovementComponent-RotatingMovementComponent(Actor_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RotatingMovementComponent](./RotatingMovementComponent.md 'UnrealEngine.Framework.RotatingMovementComponent') +## RotatingMovementComponent(UnrealEngine.Framework.Actor, string) Constructor +Creates the component attached to an actor +```csharp +public RotatingMovementComponent(UnrealEngine.Framework.Actor actor, string name=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +``` + +`API/RotatingMovementComponent-RotationInLocalSpace.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RotatingMovementComponent](./RotatingMovementComponent.md 'UnrealEngine.Framework.RotatingMovementComponent') +## RotatingMovementComponent.RotationInLocalSpace Property +Gets or sets whether rotation is applied in local or world space +```csharp +public bool RotationInLocalSpace { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/RotatingMovementComponent-SetPivotTranslation(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RotatingMovementComponent](./RotatingMovementComponent.md 'UnrealEngine.Framework.RotatingMovementComponent') +## RotatingMovementComponent.SetPivotTranslation(System.Numerics.Vector3) Method +Sets translation of pivot point around which the component rotates, relative to the current rotation +```csharp +public void SetPivotTranslation(in System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/RotatingMovementComponent-SetRotationRate(Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[RotatingMovementComponent](./RotatingMovementComponent.md 'UnrealEngine.Framework.RotatingMovementComponent') +## RotatingMovementComponent.SetRotationRate(System.Numerics.Quaternion) Method +Sets yaw, pitch, and roll rotation rate of the component +```csharp +public void SetRotationRate(in System.Numerics.Quaternion value); +``` +#### Parameters + +`value` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/RotatingMovementComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## RotatingMovementComponent Class +A component that performs continuous rotation at a specific rotation rate +```csharp +public class RotatingMovementComponent : UnrealEngine.Framework.MovementComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') 🡒 RotatingMovementComponent +### Constructors +- [RotatingMovementComponent(UnrealEngine.Framework.Actor, string)](./RotatingMovementComponent-RotatingMovementComponent(Actor_string).md 'UnrealEngine.Framework.RotatingMovementComponent.RotatingMovementComponent(UnrealEngine.Framework.Actor, string)') +### Properties +- [RotationInLocalSpace](./RotatingMovementComponent-RotationInLocalSpace.md 'UnrealEngine.Framework.RotatingMovementComponent.RotationInLocalSpace') +### Methods +- [GetPivotTranslation()](./RotatingMovementComponent-GetPivotTranslation().md 'UnrealEngine.Framework.RotatingMovementComponent.GetPivotTranslation()') +- [GetPivotTranslation(System.Numerics.Vector3)](./RotatingMovementComponent-GetPivotTranslation(Vector3).md 'UnrealEngine.Framework.RotatingMovementComponent.GetPivotTranslation(System.Numerics.Vector3)') +- [GetRotationRate()](./RotatingMovementComponent-GetRotationRate().md 'UnrealEngine.Framework.RotatingMovementComponent.GetRotationRate()') +- [GetRotationRate(System.Numerics.Quaternion)](./RotatingMovementComponent-GetRotationRate(Quaternion).md 'UnrealEngine.Framework.RotatingMovementComponent.GetRotationRate(System.Numerics.Quaternion)') +- [SetPivotTranslation(System.Numerics.Vector3)](./RotatingMovementComponent-SetPivotTranslation(Vector3).md 'UnrealEngine.Framework.RotatingMovementComponent.SetPivotTranslation(System.Numerics.Vector3)') +- [SetRotationRate(System.Numerics.Quaternion)](./RotatingMovementComponent-SetRotationRate(Quaternion).md 'UnrealEngine.Framework.RotatingMovementComponent.SetRotationRate(System.Numerics.Quaternion)') + +``` + +`API/SceneComponent-Activate().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.Activate() Method +Activates the component +```csharp +public void Activate(); +``` + +``` + +`API/SceneComponent-AddLocalOffset(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.AddLocalOffset(System.Numerics.Vector3) Method +Adds a delta to the location of the component in its local reference frame +```csharp +public void AddLocalOffset(in System.Numerics.Vector3 deltaLocation); +``` +#### Parameters + +`deltaLocation` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SceneComponent-AddLocalRotation(Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.AddLocalRotation(System.Numerics.Quaternion) Method +Adds a delta to the rotation of the component in its local reference frame +```csharp +public void AddLocalRotation(in System.Numerics.Quaternion deltaRotation); +``` +#### Parameters + +`deltaRotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/SceneComponent-AddLocalTransform(Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.AddLocalTransform(UnrealEngine.Framework.Transform) Method +Adds a delta to the transform of the component in its local reference frame, scale is unchanged +```csharp +public void AddLocalTransform(in UnrealEngine.Framework.Transform deltaTransform); +``` +#### Parameters + +`deltaTransform` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +``` + +`API/SceneComponent-AddRelativeLocation(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.AddRelativeLocation(System.Numerics.Vector3) Method +Adds a delta to the translation of the component relative to its parent +```csharp +public void AddRelativeLocation(in System.Numerics.Vector3 deltaLocation); +``` +#### Parameters + +`deltaLocation` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SceneComponent-AddRelativeRotation(Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.AddRelativeRotation(System.Numerics.Quaternion) Method +Adds a delta to the rotation of the component relative to its parent +```csharp +public void AddRelativeRotation(in System.Numerics.Quaternion deltaRotation); +``` +#### Parameters + +`deltaRotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/SceneComponent-AddWorldOffset(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.AddWorldOffset(System.Numerics.Vector3) Method +Adds a delta to the location of the component in world space +```csharp +public void AddWorldOffset(in System.Numerics.Vector3 deltaLocation); +``` +#### Parameters + +`deltaLocation` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SceneComponent-AddWorldRotation(Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.AddWorldRotation(System.Numerics.Quaternion) Method +Adds a delta to the rotation of the component in world space +```csharp +public void AddWorldRotation(in System.Numerics.Quaternion deltaRotation); +``` +#### Parameters + +`deltaRotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/SceneComponent-AddWorldTransform(Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.AddWorldTransform(UnrealEngine.Framework.Transform) Method +Adds a delta to the transform of the component in world space, scale is unchanged +```csharp +public void AddWorldTransform(in UnrealEngine.Framework.Transform deltaTransform); +``` +#### Parameters + +`deltaTransform` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +``` + +`API/SceneComponent-AttachToComponent(SceneComponent_AttachmentTransformRule_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.AttachToComponent(UnrealEngine.Framework.SceneComponent, UnrealEngine.Framework.AttachmentTransformRule, string) Method +Attaches the component to another component, optionally at a named socket +```csharp +public bool AttachToComponent(UnrealEngine.Framework.SceneComponent parent, UnrealEngine.Framework.AttachmentTransformRule attachmentRule, string socketName=null); +``` +#### Parameters + +`parent` [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') + + +`attachmentRule` [AttachmentTransformRule](./AttachmentTransformRule.md 'UnrealEngine.Framework.AttachmentTransformRule') + + +`socketName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if successful + +``` + +`API/SceneComponent-CanAttachAsChild(SceneComponent_string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.CanAttachAsChild(UnrealEngine.Framework.SceneComponent, string) Method +Returns `true` if another scene component can be attached as a child +```csharp +public bool CanAttachAsChild(UnrealEngine.Framework.SceneComponent childComponent, string socketName=null); +``` +#### Parameters + +`childComponent` [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') + + +`socketName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SceneComponent-Deactivate().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.Deactivate() Method +Deactivates the component +```csharp +public void Deactivate(); +``` + +``` + +`API/SceneComponent-DetachFromComponent(DetachmentTransformRule).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.DetachFromComponent(UnrealEngine.Framework.DetachmentTransformRule) Method +Detaches the component from a parent +```csharp +public void DetachFromComponent(UnrealEngine.Framework.DetachmentTransformRule detachmentRule); +``` +#### Parameters + +`detachmentRule` [DetachmentTransformRule](./DetachmentTransformRule.md 'UnrealEngine.Framework.DetachmentTransformRule') + + +``` + +`API/SceneComponent-ForEachAttachedChild-T-(Action-T-).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.ForEachAttachedChild<T>(System.Action<T>) Method +Performs the specified action on each attached child component if any +```csharp +public void ForEachAttachedChild(System.Action action) + where T : UnrealEngine.Framework.SceneComponent; +``` +#### Type parameters + +`T` + +Constraints [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') + +#### Parameters + +`action` [System.Action<](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1')[T](#UnrealEngine-Framework-SceneComponent-ForEachAttachedChild-T-(System-Action-T-)-T 'UnrealEngine.Framework.SceneComponent.ForEachAttachedChild<T>(System.Action<T>).T')[>](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1') + + +``` + +`API/SceneComponent-GetAttachedSocketName().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetAttachedSocketName() Method +Returns the name of a socket the component is attached to +```csharp +public string GetAttachedSocketName(); +``` +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/SceneComponent-GetBounds(Transform_Bounds).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetBounds(UnrealEngine.Framework.Transform, UnrealEngine.Framework.Bounds) Method +Retrieves calculated bounds of the component +```csharp +public void GetBounds(in UnrealEngine.Framework.Transform localToWorld, ref UnrealEngine.Framework.Bounds value); +``` +#### Parameters + +`localToWorld` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +`value` [Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') + + +``` + +`API/SceneComponent-GetForwardVector().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetForwardVector() Method +Returns the forward X unit direction vector from the component in world space +```csharp +public System.Numerics.Vector3 GetForwardVector(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SceneComponent-GetForwardVector(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetForwardVector(System.Numerics.Vector3) Method +Retrieves the forward X unit direction vector from the component in world space +```csharp +public void GetForwardVector(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SceneComponent-GetLocation().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetLocation() Method +Returns location of the component in world space +```csharp +public System.Numerics.Vector3 GetLocation(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SceneComponent-GetLocation(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetLocation(System.Numerics.Vector3) Method +Retrieves location of the component in world space +```csharp +public void GetLocation(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SceneComponent-GetRightVector().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetRightVector() Method +Returns the right Y unit direction vector from the component in world space +```csharp +public System.Numerics.Vector3 GetRightVector(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SceneComponent-GetRightVector(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetRightVector(System.Numerics.Vector3) Method +Retrieves the right Y unit direction vector from the component in world space +```csharp +public void GetRightVector(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SceneComponent-GetRotation().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetRotation() Method +Returns rotation of the component in world space +```csharp +public System.Numerics.Quaternion GetRotation(); +``` +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/SceneComponent-GetRotation(Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetRotation(System.Numerics.Quaternion) Method +Returns rotation of the component in world space +```csharp +public void GetRotation(ref System.Numerics.Quaternion value); +``` +#### Parameters + +`value` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/SceneComponent-GetScale().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetScale() Method +Returns scale of the component in world space +```csharp +public System.Numerics.Vector3 GetScale(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SceneComponent-GetScale(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetScale(System.Numerics.Vector3) Method +Retrieves scale of the component in world space +```csharp +public void GetScale(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SceneComponent-GetSocketLocation(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetSocketLocation(string) Method +Returns location of a socket in world space +```csharp +public System.Numerics.Vector3 GetSocketLocation(string socketName); +``` +#### Parameters + +`socketName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SceneComponent-GetSocketLocation(string_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetSocketLocation(string, System.Numerics.Vector3) Method +Retrieves location of a socket in world space +```csharp +public void GetSocketLocation(string socketName, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`socketName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SceneComponent-GetSocketRotation(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetSocketRotation(string) Method +Returns rotation of a socket in world space +```csharp +public System.Numerics.Quaternion GetSocketRotation(string socketName); +``` +#### Parameters + +`socketName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/SceneComponent-GetSocketRotation(string_Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetSocketRotation(string, System.Numerics.Quaternion) Method +Retrieves rotation of a socket in world space +```csharp +public void GetSocketRotation(string socketName, ref System.Numerics.Quaternion value); +``` +#### Parameters + +`socketName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`value` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/SceneComponent-GetTransform().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetTransform() Method +Returns the transform which assigned to the component +```csharp +public UnrealEngine.Framework.Transform GetTransform(); +``` +#### Returns +[Transform](./Transform.md 'UnrealEngine.Framework.Transform') + +``` + +`API/SceneComponent-GetTransform(Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetTransform(UnrealEngine.Framework.Transform) Method +Returns the transform which assigned to the component +```csharp +public void GetTransform(ref UnrealEngine.Framework.Transform value); +``` +#### Parameters + +`value` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +``` + +`API/SceneComponent-GetUpVector().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetUpVector() Method +Returns the up Z unit direction vector from the component in world space +```csharp +public System.Numerics.Vector3 GetUpVector(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SceneComponent-GetUpVector(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetUpVector(System.Numerics.Vector3) Method +Retrieves the up Z unit direction vector from the component in world space +```csharp +public void GetUpVector(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SceneComponent-GetVelocity().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetVelocity() Method +Returns velocity of the component, or the velocity of the physics body if simulating physics +```csharp +public System.Numerics.Vector3 GetVelocity(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SceneComponent-GetVelocity(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.GetVelocity(System.Numerics.Vector3) Method +Retrieves velocity of the component, or the velocity of the physics body if simulating physics +```csharp +public void GetVelocity(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SceneComponent-HasAnySockets.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.HasAnySockets Property +Returns `true` if the component has any sockets +```csharp +public bool HasAnySockets { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SceneComponent-IsAttachedToActor(Actor).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.IsAttachedToActor(UnrealEngine.Framework.Actor) Method +Returns `true` if the component is attached to the actor +```csharp +public bool IsAttachedToActor(UnrealEngine.Framework.Actor actor); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SceneComponent-IsAttachedToComponent(SceneComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.IsAttachedToComponent(UnrealEngine.Framework.SceneComponent) Method +Returns `true` if the component is attached to the supplied component +```csharp +public bool IsAttachedToComponent(UnrealEngine.Framework.SceneComponent component); +``` +#### Parameters + +`component` [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SceneComponent-IsSocketExists(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.IsSocketExists(string) Method +Returns `true` if the a socket with the given name exists +```csharp +public bool IsSocketExists(string socketName); +``` +#### Parameters + +`socketName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SceneComponent-IsVisible.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.IsVisible Property +Returns `true` if the component is visible in the current context +```csharp +public bool IsVisible { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SceneComponent-SceneComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public SceneComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/SceneComponent-SetMobility(ComponentMobility).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.SetMobility(UnrealEngine.Framework.ComponentMobility) Method +Sets how often the component is allowed to move at runtime +```csharp +public void SetMobility(UnrealEngine.Framework.ComponentMobility mobility); +``` +#### Parameters + +`mobility` [ComponentMobility](./ComponentMobility.md 'UnrealEngine.Framework.ComponentMobility') + + +``` + +`API/SceneComponent-SetRelativeLocation(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.SetRelativeLocation(System.Numerics.Vector3) Method +Sets the location of the component relative to its parent +```csharp +public void SetRelativeLocation(in System.Numerics.Vector3 location); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SceneComponent-SetRelativeRotation(Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.SetRelativeRotation(System.Numerics.Quaternion) Method +Sets the rotation of the component relative to its parent +```csharp +public void SetRelativeRotation(in System.Numerics.Quaternion rotation); +``` +#### Parameters + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/SceneComponent-SetRelativeTransform(Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.SetRelativeTransform(UnrealEngine.Framework.Transform) Method +Sets the transform of the component relative to its parent +```csharp +public void SetRelativeTransform(in UnrealEngine.Framework.Transform transform); +``` +#### Parameters + +`transform` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +``` + +`API/SceneComponent-SetVisibility(bool_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.SetVisibility(bool, bool) Method +Sets the visibility of the component +```csharp +public void SetVisibility(bool newVisibility, bool propagateToChildren=false); +``` +#### Parameters + +`newVisibility` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`propagateToChildren` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SceneComponent-SetWorldLocation(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.SetWorldLocation(System.Numerics.Vector3) Method +Sets the location of the component in world space +```csharp +public void SetWorldLocation(in System.Numerics.Vector3 location); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SceneComponent-SetWorldRotation(Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.SetWorldRotation(System.Numerics.Quaternion) Method +Sets the rotation of the component in world space +```csharp +public void SetWorldRotation(in System.Numerics.Quaternion rotation); +``` +#### Parameters + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/SceneComponent-SetWorldScale(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.SetWorldScale(System.Numerics.Vector3) Method +Sets the scale of the component world space +```csharp +public void SetWorldScale(in System.Numerics.Vector3 scale); +``` +#### Parameters + +`scale` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SceneComponent-SetWorldTransform(Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.SetWorldTransform(UnrealEngine.Framework.Transform) Method +Sets the transform of the component in world space +```csharp +public void SetWorldTransform(in UnrealEngine.Framework.Transform transform); +``` +#### Parameters + +`transform` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +``` + +`API/SceneComponent-UpdateToWorld(TeleportType_UpdateTransformFlags).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +## SceneComponent.UpdateToWorld(UnrealEngine.Framework.TeleportType, UnrealEngine.Framework.UpdateTransformFlags) Method +Recalculates the value of the component to world transform +```csharp +public void UpdateToWorld(UnrealEngine.Framework.TeleportType type=UnrealEngine.Framework.TeleportType.None, UnrealEngine.Framework.UpdateTransformFlags flags=UnrealEngine.Framework.UpdateTransformFlags.None); +``` +#### Parameters + +`type` [TeleportType](./TeleportType.md 'UnrealEngine.Framework.TeleportType') + + +`flags` [UpdateTransformFlags](./UpdateTransformFlags.md 'UnrealEngine.Framework.UpdateTransformFlags') + + +``` + +`API/SceneComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## SceneComponent Class +The base class of components that can be transformed or attached, but has no rendering or collision capabilities +```csharp +public class SceneComponent : UnrealEngine.Framework.ActorComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 SceneComponent + +Derived +↳ [AudioComponent](./AudioComponent.md 'UnrealEngine.Framework.AudioComponent') +↳ [CameraComponent](./CameraComponent.md 'UnrealEngine.Framework.CameraComponent') +↳ [ChildActorComponent](./ChildActorComponent.md 'UnrealEngine.Framework.ChildActorComponent') +↳ [LightComponentBase](./LightComponentBase.md 'UnrealEngine.Framework.LightComponentBase') +↳ [PostProcessComponent](./PostProcessComponent.md 'UnrealEngine.Framework.PostProcessComponent') +↳ [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +↳ [RadialForceComponent](./RadialForceComponent.md 'UnrealEngine.Framework.RadialForceComponent') +↳ [SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +### Constructors +- [SceneComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./SceneComponent-SceneComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.SceneComponent.SceneComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Properties +- [HasAnySockets](./SceneComponent-HasAnySockets.md 'UnrealEngine.Framework.SceneComponent.HasAnySockets') +- [IsVisible](./SceneComponent-IsVisible.md 'UnrealEngine.Framework.SceneComponent.IsVisible') +### Methods +- [Activate()](./SceneComponent-Activate().md 'UnrealEngine.Framework.SceneComponent.Activate()') +- [AddLocalOffset(System.Numerics.Vector3)](./SceneComponent-AddLocalOffset(Vector3).md 'UnrealEngine.Framework.SceneComponent.AddLocalOffset(System.Numerics.Vector3)') +- [AddLocalRotation(System.Numerics.Quaternion)](./SceneComponent-AddLocalRotation(Quaternion).md 'UnrealEngine.Framework.SceneComponent.AddLocalRotation(System.Numerics.Quaternion)') +- [AddLocalTransform(UnrealEngine.Framework.Transform)](./SceneComponent-AddLocalTransform(Transform).md 'UnrealEngine.Framework.SceneComponent.AddLocalTransform(UnrealEngine.Framework.Transform)') +- [AddRelativeLocation(System.Numerics.Vector3)](./SceneComponent-AddRelativeLocation(Vector3).md 'UnrealEngine.Framework.SceneComponent.AddRelativeLocation(System.Numerics.Vector3)') +- [AddRelativeRotation(System.Numerics.Quaternion)](./SceneComponent-AddRelativeRotation(Quaternion).md 'UnrealEngine.Framework.SceneComponent.AddRelativeRotation(System.Numerics.Quaternion)') +- [AddWorldOffset(System.Numerics.Vector3)](./SceneComponent-AddWorldOffset(Vector3).md 'UnrealEngine.Framework.SceneComponent.AddWorldOffset(System.Numerics.Vector3)') +- [AddWorldRotation(System.Numerics.Quaternion)](./SceneComponent-AddWorldRotation(Quaternion).md 'UnrealEngine.Framework.SceneComponent.AddWorldRotation(System.Numerics.Quaternion)') +- [AddWorldTransform(UnrealEngine.Framework.Transform)](./SceneComponent-AddWorldTransform(Transform).md 'UnrealEngine.Framework.SceneComponent.AddWorldTransform(UnrealEngine.Framework.Transform)') +- [AttachToComponent(UnrealEngine.Framework.SceneComponent, UnrealEngine.Framework.AttachmentTransformRule, string)](./SceneComponent-AttachToComponent(SceneComponent_AttachmentTransformRule_string).md 'UnrealEngine.Framework.SceneComponent.AttachToComponent(UnrealEngine.Framework.SceneComponent, UnrealEngine.Framework.AttachmentTransformRule, string)') +- [CanAttachAsChild(UnrealEngine.Framework.SceneComponent, string)](./SceneComponent-CanAttachAsChild(SceneComponent_string).md 'UnrealEngine.Framework.SceneComponent.CanAttachAsChild(UnrealEngine.Framework.SceneComponent, string)') +- [Deactivate()](./SceneComponent-Deactivate().md 'UnrealEngine.Framework.SceneComponent.Deactivate()') +- [DetachFromComponent(UnrealEngine.Framework.DetachmentTransformRule)](./SceneComponent-DetachFromComponent(DetachmentTransformRule).md 'UnrealEngine.Framework.SceneComponent.DetachFromComponent(UnrealEngine.Framework.DetachmentTransformRule)') +- [ForEachAttachedChild<T>(System.Action<T>)](./SceneComponent-ForEachAttachedChild-T-(Action-T-).md 'UnrealEngine.Framework.SceneComponent.ForEachAttachedChild<T>(System.Action<T>)') +- [GetAttachedSocketName()](./SceneComponent-GetAttachedSocketName().md 'UnrealEngine.Framework.SceneComponent.GetAttachedSocketName()') +- [GetBounds(UnrealEngine.Framework.Transform, UnrealEngine.Framework.Bounds)](./SceneComponent-GetBounds(Transform_Bounds).md 'UnrealEngine.Framework.SceneComponent.GetBounds(UnrealEngine.Framework.Transform, UnrealEngine.Framework.Bounds)') +- [GetForwardVector()](./SceneComponent-GetForwardVector().md 'UnrealEngine.Framework.SceneComponent.GetForwardVector()') +- [GetForwardVector(System.Numerics.Vector3)](./SceneComponent-GetForwardVector(Vector3).md 'UnrealEngine.Framework.SceneComponent.GetForwardVector(System.Numerics.Vector3)') +- [GetLocation()](./SceneComponent-GetLocation().md 'UnrealEngine.Framework.SceneComponent.GetLocation()') +- [GetLocation(System.Numerics.Vector3)](./SceneComponent-GetLocation(Vector3).md 'UnrealEngine.Framework.SceneComponent.GetLocation(System.Numerics.Vector3)') +- [GetRightVector()](./SceneComponent-GetRightVector().md 'UnrealEngine.Framework.SceneComponent.GetRightVector()') +- [GetRightVector(System.Numerics.Vector3)](./SceneComponent-GetRightVector(Vector3).md 'UnrealEngine.Framework.SceneComponent.GetRightVector(System.Numerics.Vector3)') +- [GetRotation()](./SceneComponent-GetRotation().md 'UnrealEngine.Framework.SceneComponent.GetRotation()') +- [GetRotation(System.Numerics.Quaternion)](./SceneComponent-GetRotation(Quaternion).md 'UnrealEngine.Framework.SceneComponent.GetRotation(System.Numerics.Quaternion)') +- [GetScale()](./SceneComponent-GetScale().md 'UnrealEngine.Framework.SceneComponent.GetScale()') +- [GetScale(System.Numerics.Vector3)](./SceneComponent-GetScale(Vector3).md 'UnrealEngine.Framework.SceneComponent.GetScale(System.Numerics.Vector3)') +- [GetSocketLocation(string)](./SceneComponent-GetSocketLocation(string).md 'UnrealEngine.Framework.SceneComponent.GetSocketLocation(string)') +- [GetSocketLocation(string, System.Numerics.Vector3)](./SceneComponent-GetSocketLocation(string_Vector3).md 'UnrealEngine.Framework.SceneComponent.GetSocketLocation(string, System.Numerics.Vector3)') +- [GetSocketRotation(string)](./SceneComponent-GetSocketRotation(string).md 'UnrealEngine.Framework.SceneComponent.GetSocketRotation(string)') +- [GetSocketRotation(string, System.Numerics.Quaternion)](./SceneComponent-GetSocketRotation(string_Quaternion).md 'UnrealEngine.Framework.SceneComponent.GetSocketRotation(string, System.Numerics.Quaternion)') +- [GetTransform()](./SceneComponent-GetTransform().md 'UnrealEngine.Framework.SceneComponent.GetTransform()') +- [GetTransform(UnrealEngine.Framework.Transform)](./SceneComponent-GetTransform(Transform).md 'UnrealEngine.Framework.SceneComponent.GetTransform(UnrealEngine.Framework.Transform)') +- [GetUpVector()](./SceneComponent-GetUpVector().md 'UnrealEngine.Framework.SceneComponent.GetUpVector()') +- [GetUpVector(System.Numerics.Vector3)](./SceneComponent-GetUpVector(Vector3).md 'UnrealEngine.Framework.SceneComponent.GetUpVector(System.Numerics.Vector3)') +- [GetVelocity()](./SceneComponent-GetVelocity().md 'UnrealEngine.Framework.SceneComponent.GetVelocity()') +- [GetVelocity(System.Numerics.Vector3)](./SceneComponent-GetVelocity(Vector3).md 'UnrealEngine.Framework.SceneComponent.GetVelocity(System.Numerics.Vector3)') +- [IsAttachedToActor(UnrealEngine.Framework.Actor)](./SceneComponent-IsAttachedToActor(Actor).md 'UnrealEngine.Framework.SceneComponent.IsAttachedToActor(UnrealEngine.Framework.Actor)') +- [IsAttachedToComponent(UnrealEngine.Framework.SceneComponent)](./SceneComponent-IsAttachedToComponent(SceneComponent).md 'UnrealEngine.Framework.SceneComponent.IsAttachedToComponent(UnrealEngine.Framework.SceneComponent)') +- [IsSocketExists(string)](./SceneComponent-IsSocketExists(string).md 'UnrealEngine.Framework.SceneComponent.IsSocketExists(string)') +- [SetMobility(UnrealEngine.Framework.ComponentMobility)](./SceneComponent-SetMobility(ComponentMobility).md 'UnrealEngine.Framework.SceneComponent.SetMobility(UnrealEngine.Framework.ComponentMobility)') +- [SetRelativeLocation(System.Numerics.Vector3)](./SceneComponent-SetRelativeLocation(Vector3).md 'UnrealEngine.Framework.SceneComponent.SetRelativeLocation(System.Numerics.Vector3)') +- [SetRelativeRotation(System.Numerics.Quaternion)](./SceneComponent-SetRelativeRotation(Quaternion).md 'UnrealEngine.Framework.SceneComponent.SetRelativeRotation(System.Numerics.Quaternion)') +- [SetRelativeTransform(UnrealEngine.Framework.Transform)](./SceneComponent-SetRelativeTransform(Transform).md 'UnrealEngine.Framework.SceneComponent.SetRelativeTransform(UnrealEngine.Framework.Transform)') +- [SetVisibility(bool, bool)](./SceneComponent-SetVisibility(bool_bool).md 'UnrealEngine.Framework.SceneComponent.SetVisibility(bool, bool)') +- [SetWorldLocation(System.Numerics.Vector3)](./SceneComponent-SetWorldLocation(Vector3).md 'UnrealEngine.Framework.SceneComponent.SetWorldLocation(System.Numerics.Vector3)') +- [SetWorldRotation(System.Numerics.Quaternion)](./SceneComponent-SetWorldRotation(Quaternion).md 'UnrealEngine.Framework.SceneComponent.SetWorldRotation(System.Numerics.Quaternion)') +- [SetWorldScale(System.Numerics.Vector3)](./SceneComponent-SetWorldScale(Vector3).md 'UnrealEngine.Framework.SceneComponent.SetWorldScale(System.Numerics.Vector3)') +- [SetWorldTransform(UnrealEngine.Framework.Transform)](./SceneComponent-SetWorldTransform(Transform).md 'UnrealEngine.Framework.SceneComponent.SetWorldTransform(UnrealEngine.Framework.Transform)') +- [UpdateToWorld(UnrealEngine.Framework.TeleportType, UnrealEngine.Framework.UpdateTransformFlags)](./SceneComponent-UpdateToWorld(TeleportType_UpdateTransformFlags).md 'UnrealEngine.Framework.SceneComponent.UpdateToWorld(UnrealEngine.Framework.TeleportType, UnrealEngine.Framework.UpdateTransformFlags)') + +``` + +`API/ShapeComponent-DynamicObstacle.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ShapeComponent](./ShapeComponent.md 'UnrealEngine.Framework.ShapeComponent') +## ShapeComponent.DynamicObstacle Property +Gets or sets whether the shape will be used for navigation as a dynamic modifier instead of using regular collision data +```csharp +public bool DynamicObstacle { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/ShapeComponent-ShapeColor.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[ShapeComponent](./ShapeComponent.md 'UnrealEngine.Framework.ShapeComponent') +## ShapeComponent.ShapeColor Property +Gets or sets the color of the shape +```csharp +public System.Drawing.Color ShapeColor { get; set; } +``` +#### Property Value +[System.Drawing.Color](https://docs.microsoft.com/en-us/dotnet/api/System.Drawing.Color 'System.Drawing.Color') + +``` + +`API/ShapeComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## ShapeComponent Class +An abstract component that is represented by a simple geometrical shape +```csharp +public abstract class ShapeComponent : UnrealEngine.Framework.PrimitiveComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') 🡒 ShapeComponent + +Derived +↳ [BoxComponent](./BoxComponent.md 'UnrealEngine.Framework.BoxComponent') +↳ [CapsuleComponent](./CapsuleComponent.md 'UnrealEngine.Framework.CapsuleComponent') +↳ [SphereComponent](./SphereComponent.md 'UnrealEngine.Framework.SphereComponent') +### Properties +- [DynamicObstacle](./ShapeComponent-DynamicObstacle.md 'UnrealEngine.Framework.ShapeComponent.DynamicObstacle') +- [ShapeColor](./ShapeComponent-ShapeColor.md 'UnrealEngine.Framework.ShapeComponent.ShapeColor') + +``` + +`API/SkeletalMesh-Load(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkeletalMesh](./SkeletalMesh.md 'UnrealEngine.Framework.SkeletalMesh') +## SkeletalMesh.Load(string) Method +Finds and loads a skeletal mesh by name +```csharp +public static UnrealEngine.Framework.SkeletalMesh Load(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[SkeletalMesh](./SkeletalMesh.md 'UnrealEngine.Framework.SkeletalMesh') +A skeletal mesh or `null` on failure + +``` + +`API/SkeletalMesh.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## SkeletalMesh Class +A geometry bound to a hierarchical skeleton of bones which can be animated +```csharp +public class SkeletalMesh : UnrealEngine.Framework.StreamableRenderAsset +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [StreamableRenderAsset](./StreamableRenderAsset.md 'UnrealEngine.Framework.StreamableRenderAsset') 🡒 SkeletalMesh +### Methods +- [Load(string)](./SkeletalMesh-Load(string).md 'UnrealEngine.Framework.SkeletalMesh.Load(string)') + +``` + +`API/SkeletalMeshComponent-GetAnimationInstance().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent') +## SkeletalMeshComponent.GetAnimationInstance() Method +Returns the animation instance that is driving the class or `null` on failure +```csharp +public UnrealEngine.Framework.AnimationInstance GetAnimationInstance(); +``` +#### Returns +[AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') + +``` + +`API/SkeletalMeshComponent-IsPlaying.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent') +## SkeletalMeshComponent.IsPlaying Property +Returns `true` if playing animation +```csharp +public bool IsPlaying { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SkeletalMeshComponent-Play(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent') +## SkeletalMeshComponent.Play(bool) Method +Plays the animation +```csharp +public void Play(bool loop=false); +``` +#### Parameters + +`loop` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SkeletalMeshComponent-PlayAnimation(AnimationAsset_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent') +## SkeletalMeshComponent.PlayAnimation(UnrealEngine.Framework.AnimationAsset, bool) Method +Plays the animation asset +```csharp +public void PlayAnimation(UnrealEngine.Framework.AnimationAsset asset, bool loop=false); +``` +#### Parameters + +`asset` [AnimationAsset](./AnimationAsset.md 'UnrealEngine.Framework.AnimationAsset') + + +`loop` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SkeletalMeshComponent-SetAnimation(AnimationAsset).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent') +## SkeletalMeshComponent.SetAnimation(UnrealEngine.Framework.AnimationAsset) Method +Sets the animation to play +```csharp +public void SetAnimation(UnrealEngine.Framework.AnimationAsset asset); +``` +#### Parameters + +`asset` [AnimationAsset](./AnimationAsset.md 'UnrealEngine.Framework.AnimationAsset') + + +``` + +`API/SkeletalMeshComponent-SetAnimationBlueprint(Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent') +## SkeletalMeshComponent.SetAnimationBlueprint(UnrealEngine.Framework.Blueprint) Method +Sets the animation blueprint +```csharp +public void SetAnimationBlueprint(UnrealEngine.Framework.Blueprint blueprint); +``` +#### Parameters + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') + + +``` + +`API/SkeletalMeshComponent-SetAnimationMode(AnimationMode).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent') +## SkeletalMeshComponent.SetAnimationMode(UnrealEngine.Framework.AnimationMode) Method +Sets the animation mode +```csharp +public void SetAnimationMode(UnrealEngine.Framework.AnimationMode mode); +``` +#### Parameters + +`mode` [AnimationMode](./AnimationMode.md 'UnrealEngine.Framework.AnimationMode') + + +``` + +`API/SkeletalMeshComponent-SkeletalMeshComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent') +## SkeletalMeshComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public SkeletalMeshComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/SkeletalMeshComponent-Stop().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent') +## SkeletalMeshComponent.Stop() Method +Stops the animation +```csharp +public void Stop(); +``` + +``` + +`API/SkeletalMeshComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## SkeletalMeshComponent Class +A component that is used to create an instance of an animated [SkeletalMesh](./SkeletalMesh.md 'UnrealEngine.Framework.SkeletalMesh') asset +```csharp +public class SkeletalMeshComponent : UnrealEngine.Framework.SkinnedMeshComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') 🡒 [MeshComponent](./MeshComponent.md 'UnrealEngine.Framework.MeshComponent') 🡒 [SkinnedMeshComponent](./SkinnedMeshComponent.md 'UnrealEngine.Framework.SkinnedMeshComponent') 🡒 SkeletalMeshComponent +### Constructors +- [SkeletalMeshComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./SkeletalMeshComponent-SkeletalMeshComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.SkeletalMeshComponent.SkeletalMeshComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Properties +- [IsPlaying](./SkeletalMeshComponent-IsPlaying.md 'UnrealEngine.Framework.SkeletalMeshComponent.IsPlaying') +### Methods +- [GetAnimationInstance()](./SkeletalMeshComponent-GetAnimationInstance().md 'UnrealEngine.Framework.SkeletalMeshComponent.GetAnimationInstance()') +- [Play(bool)](./SkeletalMeshComponent-Play(bool).md 'UnrealEngine.Framework.SkeletalMeshComponent.Play(bool)') +- [PlayAnimation(UnrealEngine.Framework.AnimationAsset, bool)](./SkeletalMeshComponent-PlayAnimation(AnimationAsset_bool).md 'UnrealEngine.Framework.SkeletalMeshComponent.PlayAnimation(UnrealEngine.Framework.AnimationAsset, bool)') +- [SetAnimation(UnrealEngine.Framework.AnimationAsset)](./SkeletalMeshComponent-SetAnimation(AnimationAsset).md 'UnrealEngine.Framework.SkeletalMeshComponent.SetAnimation(UnrealEngine.Framework.AnimationAsset)') +- [SetAnimationBlueprint(UnrealEngine.Framework.Blueprint)](./SkeletalMeshComponent-SetAnimationBlueprint(Blueprint).md 'UnrealEngine.Framework.SkeletalMeshComponent.SetAnimationBlueprint(UnrealEngine.Framework.Blueprint)') +- [SetAnimationMode(UnrealEngine.Framework.AnimationMode)](./SkeletalMeshComponent-SetAnimationMode(AnimationMode).md 'UnrealEngine.Framework.SkeletalMeshComponent.SetAnimationMode(UnrealEngine.Framework.AnimationMode)') +- [Stop()](./SkeletalMeshComponent-Stop().md 'UnrealEngine.Framework.SkeletalMeshComponent.Stop()') + +``` + +`API/SkinnedMeshComponent-GetBoneIndex(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkinnedMeshComponent](./SkinnedMeshComponent.md 'UnrealEngine.Framework.SkinnedMeshComponent') +## SkinnedMeshComponent.GetBoneIndex(string) Method +Returns the index of a bone by name +```csharp +public int GetBoneIndex(string boneName); +``` +#### Parameters + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/SkinnedMeshComponent-GetBoneName(int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkinnedMeshComponent](./SkinnedMeshComponent.md 'UnrealEngine.Framework.SkinnedMeshComponent') +## SkinnedMeshComponent.GetBoneName(int) Method +Returns the name of a bone by index +```csharp +public string GetBoneName(int boneIndex); +``` +#### Parameters + +`boneIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/SkinnedMeshComponent-GetBoneTransform(int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkinnedMeshComponent](./SkinnedMeshComponent.md 'UnrealEngine.Framework.SkinnedMeshComponent') +## SkinnedMeshComponent.GetBoneTransform(int) Method +Returns the transform of a bone by index +```csharp +public UnrealEngine.Framework.Transform GetBoneTransform(int boneIndex); +``` +#### Parameters + +`boneIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Returns +[Transform](./Transform.md 'UnrealEngine.Framework.Transform') + +``` + +`API/SkinnedMeshComponent-GetBoneTransform(int_Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkinnedMeshComponent](./SkinnedMeshComponent.md 'UnrealEngine.Framework.SkinnedMeshComponent') +## SkinnedMeshComponent.GetBoneTransform(int, UnrealEngine.Framework.Transform) Method +Retrieves the transform of a bone by index +```csharp +public void GetBoneTransform(int boneIndex, ref UnrealEngine.Framework.Transform value); +``` +#### Parameters + +`boneIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`value` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +``` + +`API/SkinnedMeshComponent-GetBonesNumber().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkinnedMeshComponent](./SkinnedMeshComponent.md 'UnrealEngine.Framework.SkinnedMeshComponent') +## SkinnedMeshComponent.GetBonesNumber() Method +Returns the number of bones in the skeleton +```csharp +public int GetBonesNumber(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/SkinnedMeshComponent-SetSkeletalMesh(SkeletalMesh_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SkinnedMeshComponent](./SkinnedMeshComponent.md 'UnrealEngine.Framework.SkinnedMeshComponent') +## SkinnedMeshComponent.SetSkeletalMesh(UnrealEngine.Framework.SkeletalMesh, bool) Method +Sets the skeletal mesh +```csharp +public void SetSkeletalMesh(UnrealEngine.Framework.SkeletalMesh skeletalMesh, bool reinitializePose=true); +``` +#### Parameters + +`skeletalMesh` [SkeletalMesh](./SkeletalMesh.md 'UnrealEngine.Framework.SkeletalMesh') + + +`reinitializePose` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SkinnedMeshComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## SkinnedMeshComponent Class +A component that supports bone skinned mesh rendering +```csharp +public abstract class SkinnedMeshComponent : UnrealEngine.Framework.MeshComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') 🡒 [MeshComponent](./MeshComponent.md 'UnrealEngine.Framework.MeshComponent') 🡒 SkinnedMeshComponent + +Derived +↳ [SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent') +### Methods +- [GetBoneIndex(string)](./SkinnedMeshComponent-GetBoneIndex(string).md 'UnrealEngine.Framework.SkinnedMeshComponent.GetBoneIndex(string)') +- [GetBoneName(int)](./SkinnedMeshComponent-GetBoneName(int).md 'UnrealEngine.Framework.SkinnedMeshComponent.GetBoneName(int)') +- [GetBonesNumber()](./SkinnedMeshComponent-GetBonesNumber().md 'UnrealEngine.Framework.SkinnedMeshComponent.GetBonesNumber()') +- [GetBoneTransform(int)](./SkinnedMeshComponent-GetBoneTransform(int).md 'UnrealEngine.Framework.SkinnedMeshComponent.GetBoneTransform(int)') +- [GetBoneTransform(int, UnrealEngine.Framework.Transform)](./SkinnedMeshComponent-GetBoneTransform(int_Transform).md 'UnrealEngine.Framework.SkinnedMeshComponent.GetBoneTransform(int, UnrealEngine.Framework.Transform)') +- [SetSkeletalMesh(UnrealEngine.Framework.SkeletalMesh, bool)](./SkinnedMeshComponent-SetSkeletalMesh(SkeletalMesh_bool).md 'UnrealEngine.Framework.SkinnedMeshComponent.SetSkeletalMesh(UnrealEngine.Framework.SkeletalMesh, bool)') + +``` + +`API/SoundBase-Duration.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SoundBase](./SoundBase.md 'UnrealEngine.Framework.SoundBase') +## SoundBase.Duration Property +Returns the duration of a sound object in seconds +```csharp +public float Duration { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SoundBase-Equals(SoundBase).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SoundBase](./SoundBase.md 'UnrealEngine.Framework.SoundBase') +## SoundBase.Equals(UnrealEngine.Framework.SoundBase) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.SoundBase other); +``` +#### Parameters + +`other` [SoundBase](./SoundBase.md 'UnrealEngine.Framework.SoundBase') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SoundBase-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SoundBase](./SoundBase.md 'UnrealEngine.Framework.SoundBase') +## SoundBase.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/SoundBase-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SoundBase](./SoundBase.md 'UnrealEngine.Framework.SoundBase') +## SoundBase.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SoundBase.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## SoundBase Class +The base class of playable sound objects +```csharp +public abstract class SoundBase : +System.IEquatable +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 SoundBase + +Derived +↳ [SoundWave](./SoundWave.md 'UnrealEngine.Framework.SoundWave') + +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[SoundBase](./SoundBase.md 'UnrealEngine.Framework.SoundBase')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [Duration](./SoundBase-Duration.md 'UnrealEngine.Framework.SoundBase.Duration') +- [IsCreated](./SoundBase-IsCreated.md 'UnrealEngine.Framework.SoundBase.IsCreated') +### Methods +- [Equals(UnrealEngine.Framework.SoundBase)](./SoundBase-Equals(SoundBase).md 'UnrealEngine.Framework.SoundBase.Equals(UnrealEngine.Framework.SoundBase)') +- [GetHashCode()](./SoundBase-GetHashCode().md 'UnrealEngine.Framework.SoundBase.GetHashCode()') + +``` + +`API/SoundWave-Load(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SoundWave](./SoundWave.md 'UnrealEngine.Framework.SoundWave') +## SoundWave.Load(string) Method +Finds and loads a sound wave by name +```csharp +public static UnrealEngine.Framework.SoundWave Load(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[SoundWave](./SoundWave.md 'UnrealEngine.Framework.SoundWave') +A sound wave or `null` on failure + +``` + +`API/SoundWave-Loop.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SoundWave](./SoundWave.md 'UnrealEngine.Framework.SoundWave') +## SoundWave.Loop Property +Gets or sets whether the sound wave will be looped if played directly +```csharp +public bool Loop { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SoundWave.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## SoundWave Class +A playable sound object for raw wave files +```csharp +public class SoundWave : UnrealEngine.Framework.SoundBase +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [SoundBase](./SoundBase.md 'UnrealEngine.Framework.SoundBase') 🡒 SoundWave +### Properties +- [Loop](./SoundWave-Loop.md 'UnrealEngine.Framework.SoundWave.Loop') +### Methods +- [Load(string)](./SoundWave-Load(string).md 'UnrealEngine.Framework.SoundWave.Load(string)') + +``` + +`API/SphereComponent-InitSphereRadius(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SphereComponent](./SphereComponent.md 'UnrealEngine.Framework.SphereComponent') +## SphereComponent.InitSphereRadius(float) Method +Sets the sphere radius without triggering a render or physics update +```csharp +public void InitSphereRadius(float sphereRadius); +``` +#### Parameters + +`sphereRadius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/SphereComponent-ScaledSphereRadius.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SphereComponent](./SphereComponent.md 'UnrealEngine.Framework.SphereComponent') +## SphereComponent.ScaledSphereRadius Property +Returns the sphere radius scaled by the component scale +```csharp +public float ScaledSphereRadius { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SphereComponent-SetSphereRadius(float_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SphereComponent](./SphereComponent.md 'UnrealEngine.Framework.SphereComponent') +## SphereComponent.SetSphereRadius(float, bool) Method +Sets the sphere radius, unscaled before the component scale is applied +```csharp +public void SetSphereRadius(float sphereRadius, bool updateOverlaps=true); +``` +#### Parameters + +`sphereRadius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`updateOverlaps` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SphereComponent-ShapeScale.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SphereComponent](./SphereComponent.md 'UnrealEngine.Framework.SphereComponent') +## SphereComponent.ShapeScale Property +Returns the scale of the shape +```csharp +public float ShapeScale { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SphereComponent-SphereComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SphereComponent](./SphereComponent.md 'UnrealEngine.Framework.SphereComponent') +## SphereComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public SphereComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/SphereComponent-UnscaledSphereRadius.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SphereComponent](./SphereComponent.md 'UnrealEngine.Framework.SphereComponent') +## SphereComponent.UnscaledSphereRadius Property +Returns the sphere radius ignoring the component scale +```csharp +public float UnscaledSphereRadius { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SphereComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## SphereComponent Class +A sphere generally used for simple collision +```csharp +public class SphereComponent : UnrealEngine.Framework.ShapeComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') 🡒 [ShapeComponent](./ShapeComponent.md 'UnrealEngine.Framework.ShapeComponent') 🡒 SphereComponent +### Constructors +- [SphereComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./SphereComponent-SphereComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.SphereComponent.SphereComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Properties +- [ScaledSphereRadius](./SphereComponent-ScaledSphereRadius.md 'UnrealEngine.Framework.SphereComponent.ScaledSphereRadius') +- [ShapeScale](./SphereComponent-ShapeScale.md 'UnrealEngine.Framework.SphereComponent.ShapeScale') +- [UnscaledSphereRadius](./SphereComponent-UnscaledSphereRadius.md 'UnrealEngine.Framework.SphereComponent.UnscaledSphereRadius') +### Methods +- [InitSphereRadius(float)](./SphereComponent-InitSphereRadius(float).md 'UnrealEngine.Framework.SphereComponent.InitSphereRadius(float)') +- [SetSphereRadius(float, bool)](./SphereComponent-SetSphereRadius(float_bool).md 'UnrealEngine.Framework.SphereComponent.SetSphereRadius(float, bool)') + +``` + +`API/SplineComponent-AddSplinePoint(Vector3_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.AddSplinePoint(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Adds a point to the spline +```csharp +public void AddSplinePoint(in System.Numerics.Vector3 location, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool updateSpline=true); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`updateSpline` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SplineComponent-AddSplinePointAtIndex(Vector3_int_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.AddSplinePointAtIndex(System.Numerics.Vector3, int, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Adds a point to the spline at the specified index +```csharp +public void AddSplinePointAtIndex(in System.Numerics.Vector3 location, int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool updateSpline=true); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`updateSpline` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SplineComponent-ClearSplinePoints(bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.ClearSplinePoints(bool) Method +Clears all the points in the spline +```csharp +public void ClearSplinePoints(bool updateSpline=true); +``` +#### Parameters + +`updateSpline` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SplineComponent-Duration.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.Duration Property +Gets or sets the duration of the spline in seconds +```csharp +public float Duration { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SplineComponent-FindDirectionClosestToWorldLocation(Vector3_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.FindDirectionClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns a unit direction vector of the spline tangent closest to the location in world space +```csharp +public System.Numerics.Vector3 FindDirectionClosestToWorldLocation(in System.Numerics.Vector3 location, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-FindLocationClosestToWorldLocation(Vector3_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.FindLocationClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns a point of the spline closest to the location in world space +```csharp +public System.Numerics.Vector3 FindLocationClosestToWorldLocation(in System.Numerics.Vector3 location, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-FindRightVectorClosestToWorldLocation(Vector3_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.FindRightVectorClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns a unit direction vector corresponding to the spline's right vector closest to the location in world space +```csharp +public System.Numerics.Vector3 FindRightVectorClosestToWorldLocation(in System.Numerics.Vector3 location, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-FindRollClosestToWorldLocation(Vector3_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.FindRollClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the spline's roll in degrees closest to the location in world space +```csharp +public float FindRollClosestToWorldLocation(in System.Numerics.Vector3 location, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SplineComponent-FindScaleClosestToWorldLocation(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.FindScaleClosestToWorldLocation(System.Numerics.Vector3) Method +Returns the spline's scale closest to the location in world space +```csharp +public System.Numerics.Vector3 FindScaleClosestToWorldLocation(in System.Numerics.Vector3 location); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-FindTangentClosestToWorldLocation(Vector3_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.FindTangentClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns a tangent of the spline closest to the location in world space +```csharp +public System.Numerics.Vector3 FindTangentClosestToWorldLocation(in System.Numerics.Vector3 location, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-FindTransformClosestToWorldLocation(Vector3_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.FindTransformClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Returns a transform closest to the location in world space +```csharp +public UnrealEngine.Framework.Transform FindTransformClosestToWorldLocation(in System.Numerics.Vector3 location, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useScale=false); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useScale` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[Transform](./Transform.md 'UnrealEngine.Framework.Transform') + +``` + +`API/SplineComponent-FindUpVectorClosestToWorldLocation(Vector3_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.FindUpVectorClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns a unit direction vector corresponding to the spline's up vector closest to the location in world space +```csharp +public System.Numerics.Vector3 FindUpVectorClosestToWorldLocation(in System.Numerics.Vector3 location, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetArriveTangentAtSplinePoint(int_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetArriveTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the arrive tangent at the spline point +```csharp +public System.Numerics.Vector3 GetArriveTangentAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetArriveTangentAtSplinePoint(int_SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetArriveTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves the arrive tangent at the spline point +```csharp +public void GetArriveTangentAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetDefaultUpVector(SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetDefaultUpVector(UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the default up vector of the spline +```csharp +public System.Numerics.Vector3 GetDefaultUpVector(UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetDefaultUpVector(SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetDefaultUpVector(UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves the default up vector of the spline +```csharp +public void GetDefaultUpVector(UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetDirectionAtDistanceAlongSpline(float_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetDirectionAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns a unit direction vector of the spline tangent at the given distance along the length of the spline +```csharp +public System.Numerics.Vector3 GetDirectionAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetDirectionAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetDirectionAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves a unit direction vector of the spline tangent at the given distance along the length of the spline +```csharp +public void GetDirectionAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetDirectionAtSplinePoint(int_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetDirectionAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns a unit direction vector at the spline point +```csharp +public System.Numerics.Vector3 GetDirectionAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetDirectionAtSplinePoint(int_SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetDirectionAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves a unit direction vector at the spline point +```csharp +public void GetDirectionAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetDirectionAtTime(float_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetDirectionAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Returns a unit direction vector of the spline tangent at the given time from 0.0f to the spline duration +```csharp +public System.Numerics.Vector3 GetDirectionAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useConstantVelocity=false); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetDirectionAtTime(float_SplineCoordinateSpace_bool_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetDirectionAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Vector3) Method +Retrieves a unit direction vector of the spline tangent at the given time from 0.0f to the spline duration +```csharp +public void GetDirectionAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useConstantVelocity, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetDistanceAlongSplineAtSplinePoint(int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetDistanceAlongSplineAtSplinePoint(int) Method +Returns the distance along the spline at the spline point +```csharp +public float GetDistanceAlongSplineAtSplinePoint(int pointIndex); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SplineComponent-GetLeaveTangentAtSplinePoint(int_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetLeaveTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the leave tangent at the spline point +```csharp +public System.Numerics.Vector3 GetLeaveTangentAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetLeaveTangentAtSplinePoint(int_SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetLeaveTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves the leave tangent at the spline point +```csharp +public void GetLeaveTangentAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetLocationAndTangentAtSplinePoint(int_SplineCoordinateSpace_Vector3_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetLocationAndTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3, System.Numerics.Vector3) Method +Retrieves the location and tangent at the spline point +```csharp +public void GetLocationAndTangentAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 location, ref System.Numerics.Vector3 tangent); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`tangent` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetLocationAtDistanceAlongSpline(float_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetLocationAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the location at the given distance along the length of the spline +```csharp +public System.Numerics.Vector3 GetLocationAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetLocationAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetLocationAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves the location at the given distance along the length of the spline +```csharp +public void GetLocationAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetLocationAtSplinePoint(int_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetLocationAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the location at the spline point +```csharp +public System.Numerics.Vector3 GetLocationAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetLocationAtSplinePoint(int_SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetLocationAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves the location at the spline point +```csharp +public void GetLocationAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetLocationAtTime(float_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetLocationAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the location at the given time from 0.0f to the spline duration +```csharp +public System.Numerics.Vector3 GetLocationAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetLocationAtTime(float_SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetLocationAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves the location at the given time from 0.0f to the spline duration +```csharp +public void GetLocationAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetRightVectorAtDistanceAlongSpline(float_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRightVectorAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace) Method +Retrieves a unit direction vector corresponding to the spline's right vector at the given distance along the length of the spline +```csharp +public System.Numerics.Vector3 GetRightVectorAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetRightVectorAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRightVectorAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves a unit direction vector corresponding to the spline's right vector at the given distance along the length of the spline +```csharp +public void GetRightVectorAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetRightVectorAtSplinePoint(int_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRightVectorAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the spline's right vector at the spline point +```csharp +public System.Numerics.Vector3 GetRightVectorAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetRightVectorAtSplinePoint(int_SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRightVectorAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves the spline's right vector at the spline point +```csharp +public void GetRightVectorAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetRightVectorAtTime(float_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRightVectorAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Returns the spline's right vector at the given time from 0.0f to the spline duration +```csharp +public System.Numerics.Vector3 GetRightVectorAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useConstantVelocity=false); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetRightVectorAtTime(float_SplineCoordinateSpace_bool_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRightVectorAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Vector3) Method +Retrieves the spline's right vector at the given time from 0.0f to the spline duration +```csharp +public void GetRightVectorAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useConstantVelocity, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetRollAtDistanceAlongSpline(float_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRollAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the spline's roll in degrees at the given distance along the length of the spline +```csharp +public float GetRollAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SplineComponent-GetRollAtSplinePoint(int_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRollAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the spline's roll in degrees at the spline point +```csharp +public float GetRollAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SplineComponent-GetRollAtTime(float_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRollAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Returns the spline's roll in degrees at the given time from 0.0f to the spline duration +```csharp +public float GetRollAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useConstantVelocity=false); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SplineComponent-GetRotationAtDistanceAlongSpline(float_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRotationAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns a rotation corresponding to the spline's rotation at the given distance along the length of the spline +```csharp +public System.Numerics.Quaternion GetRotationAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/SplineComponent-GetRotationAtDistanceAlongSpline(float_SplineCoordinateSpace_Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRotationAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Quaternion) Method +Retrieves a rotation corresponding to the spline's rotation at the given distance along the length of the spline +```csharp +public void GetRotationAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Quaternion value); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/SplineComponent-GetRotationAtSplinePoint(int_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRotationAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns a spline's rotation at the spline point +```csharp +public System.Numerics.Quaternion GetRotationAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/SplineComponent-GetRotationAtSplinePoint(int_SplineCoordinateSpace_Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRotationAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Quaternion) Method +Retrieves a spline's rotation at the spline point +```csharp +public void GetRotationAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Quaternion value); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/SplineComponent-GetRotationAtTime(float_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRotationAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Returns a rotation corresponding to the spline's position and direction at the given time from 0.0f to the spline duration +```csharp +public System.Numerics.Quaternion GetRotationAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useConstantVelocity=false); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/SplineComponent-GetRotationAtTime(float_SplineCoordinateSpace_bool_Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetRotationAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Quaternion) Method +Retrieves a rotation corresponding to the spline's position and direction at the given time from 0.0f to the spline duration +```csharp +public void GetRotationAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useConstantVelocity, ref System.Numerics.Quaternion value); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`value` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/SplineComponent-GetScaleAtDistanceAlongSpline(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetScaleAtDistanceAlongSpline(float) Method +Returns the spline's scale at the given distance along the length of the spline +```csharp +public System.Numerics.Vector3 GetScaleAtDistanceAlongSpline(float distance); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetScaleAtDistanceAlongSpline(float_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetScaleAtDistanceAlongSpline(float, System.Numerics.Vector3) Method +Retrieves the spline's scale at the given distance along the length of the spline +```csharp +public void GetScaleAtDistanceAlongSpline(float distance, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetScaleAtSplinePoint(int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetScaleAtSplinePoint(int) Method +Returns the spline's scale at the spline point +```csharp +public System.Numerics.Vector3 GetScaleAtSplinePoint(int pointIndex); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetScaleAtSplinePoint(int_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetScaleAtSplinePoint(int, System.Numerics.Vector3) Method +Retrieves the spline's scale at the spline point +```csharp +public void GetScaleAtSplinePoint(int pointIndex, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetScaleAtTime(float_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetScaleAtTime(float, bool) Method +Returns the spline's scale at the given time from 0.0f to the spline duration +```csharp +public System.Numerics.Vector3 GetScaleAtTime(float time, bool useConstantVelocity=false); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetScaleAtTime(float_bool_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetScaleAtTime(float, bool, System.Numerics.Vector3) Method +Retrieves the spline's scale at the given time from 0.0f to the spline duration +```csharp +public void GetScaleAtTime(float time, bool useConstantVelocity, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetSplineLength().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetSplineLength() Method +Returns the total length along the spline +```csharp +public float GetSplineLength(); +``` +#### Returns +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SplineComponent-GetSplinePointType(int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetSplinePointType(int) Method +Returns the type of a spline point +```csharp +public UnrealEngine.Framework.SplinePointType GetSplinePointType(int pointIndex); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +#### Returns +[SplinePointType](./SplinePointType.md 'UnrealEngine.Framework.SplinePointType') + +``` + +`API/SplineComponent-GetTangentAtDistanceAlongSpline(float_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetTangentAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the tangent at the given distance along the length of the spline +```csharp +public System.Numerics.Vector3 GetTangentAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetTangentAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetTangentAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves the tangent at the given distance along the length of the spline +```csharp +public void GetTangentAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetTangentAtSplinePoint(int_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the tangent at the spline point +```csharp +public System.Numerics.Vector3 GetTangentAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetTangentAtSplinePoint(int_SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves the tangent at the spline point +```csharp +public void GetTangentAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetTangentAtTime(float_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetTangentAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Returns the tangent at the given time from 0.0f to the spline duration +```csharp +public System.Numerics.Vector3 GetTangentAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useConstantVelocity=false); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetTangentAtTime(float_SplineCoordinateSpace_bool_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetTangentAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Vector3) Method +Retrieves the tangent at the given time from 0.0f to the spline duration +```csharp +public void GetTangentAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useConstantVelocity, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetTransformAtDistanceAlongSpline(float_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetTransformAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the transform at the given distance along the length of the spline +```csharp +public UnrealEngine.Framework.Transform GetTransformAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[Transform](./Transform.md 'UnrealEngine.Framework.Transform') + +``` + +`API/SplineComponent-GetTransformAtDistanceAlongSpline(float_SplineCoordinateSpace_Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetTransformAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, UnrealEngine.Framework.Transform) Method +Retrieves the transform at the given distance along the length of the spline +```csharp +public void GetTransformAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref UnrealEngine.Framework.Transform value); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +``` + +`API/SplineComponent-GetTransformAtSplinePoint(int_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetTransformAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Returns the transform at the spline point +```csharp +public UnrealEngine.Framework.Transform GetTransformAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useScale=false); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useScale` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[Transform](./Transform.md 'UnrealEngine.Framework.Transform') + +``` + +`API/SplineComponent-GetTransformAtSplinePoint(int_SplineCoordinateSpace_bool_Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetTransformAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, bool, UnrealEngine.Framework.Transform) Method +Retrieves the transform at the spline point +```csharp +public void GetTransformAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useScale, ref UnrealEngine.Framework.Transform value); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useScale` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`value` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +``` + +`API/SplineComponent-GetTransformAtTime(float_SplineCoordinateSpace_bool_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetTransformAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, bool) Method +Returns the spline's transform at the given time from 0.0f to the spline duration +```csharp +public UnrealEngine.Framework.Transform GetTransformAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useConstantVelocity=false, bool useScale=false); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`useScale` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[Transform](./Transform.md 'UnrealEngine.Framework.Transform') + +``` + +`API/SplineComponent-GetTransformAtTime(float_SplineCoordinateSpace_bool_bool_Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetTransformAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, bool, UnrealEngine.Framework.Transform) Method +Retrieves the spline's transform at the given time from 0.0f to the spline duration +```csharp +public void GetTransformAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useConstantVelocity, bool useScale, ref UnrealEngine.Framework.Transform value); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`useScale` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`value` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +``` + +`API/SplineComponent-GetUpVectorAtDistanceAlongSpline(float_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetUpVectorAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns a unit direction vector corresponding to the spline's up vector at the given distance along the length of the spline +```csharp +public System.Numerics.Vector3 GetUpVectorAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetUpVectorAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetUpVectorAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves a unit direction vector corresponding to the spline's up vector at the given distance along the length of the spline +```csharp +public void GetUpVectorAtDistanceAlongSpline(float distance, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`distance` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetUpVectorAtSplinePoint(int_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetUpVectorAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace) Method +Returns the spline's up vector at the spline point +```csharp +public System.Numerics.Vector3 GetUpVectorAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetUpVectorAtSplinePoint(int_SplineCoordinateSpace_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetUpVectorAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3) Method +Retrieves the spline's up vector at the spline point +```csharp +public void GetUpVectorAtSplinePoint(int pointIndex, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-GetUpVectorAtTime(float_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetUpVectorAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Returns the spline's up vector at the given time from 0.0f to the spline duration +```csharp +public System.Numerics.Vector3 GetUpVectorAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useConstantVelocity=false); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SplineComponent-GetUpVectorAtTime(float_SplineCoordinateSpace_bool_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.GetUpVectorAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Vector3) Method +Retrieves the spline's up vector at the given time from 0.0f to the spline duration +```csharp +public void GetUpVectorAtTime(float time, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool useConstantVelocity, ref System.Numerics.Vector3 value); +``` +#### Parameters + +`time` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`useConstantVelocity` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SplineComponent-IsClosedLoop.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.IsClosedLoop Property +Returns `true` if the spline is a closed loop +```csharp +public bool IsClosedLoop { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SplineComponent-RemoveSplinePoint(int_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.RemoveSplinePoint(int, bool) Method +Removes a point at the specified index from the spline +```csharp +public void RemoveSplinePoint(int pointIndex, bool updateSpline=true); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`updateSpline` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SplineComponent-SetClosedLoop(bool_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.SetClosedLoop(bool, bool) Method +Sets whether the spline is a closed loop +```csharp +public void SetClosedLoop(bool value, bool updateSpline=true); +``` +#### Parameters + +`value` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`updateSpline` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SplineComponent-SetDefaultUpVector(Vector3_SplineCoordinateSpace).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.SetDefaultUpVector(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace) Method +Sets the default up vector of the spline +```csharp +public void SetDefaultUpVector(in System.Numerics.Vector3 value, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +``` + +`API/SplineComponent-SetLocationAtSplinePoint(int_Vector3_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.SetLocationAtSplinePoint(int, System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Sets an existing point to a new location +```csharp +public void SetLocationAtSplinePoint(int pointIndex, in System.Numerics.Vector3 value, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool updateSpline=true); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`updateSpline` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SplineComponent-SetSplinePointType(int_SplinePointType_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.SetSplinePointType(int, UnrealEngine.Framework.SplinePointType, bool) Method +Sets the type of a spline point +```csharp +public void SetSplinePointType(int pointIndex, UnrealEngine.Framework.SplinePointType type, bool updateSpline=true); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`type` [SplinePointType](./SplinePointType.md 'UnrealEngine.Framework.SplinePointType') + + +`updateSpline` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SplineComponent-SetTangentAtSplinePoint(int_Vector3_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.SetTangentAtSplinePoint(int, System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Sets the tangent at a given spline point +```csharp +public void SetTangentAtSplinePoint(int pointIndex, in System.Numerics.Vector3 tangent, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool updateSpline=true); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`tangent` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`updateSpline` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SplineComponent-SetTangentsAtSplinePoint(int_Vector3_Vector3_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.SetTangentsAtSplinePoint(int, System.Numerics.Vector3, System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Sets the tangents at a given spline point +```csharp +public void SetTangentsAtSplinePoint(int pointIndex, in System.Numerics.Vector3 arriveTangent, in System.Numerics.Vector3 leaveTangent, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool updateSpline=true); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`arriveTangent` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`leaveTangent` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`updateSpline` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SplineComponent-SetUpVectorAtSplinePoint(int_Vector3_SplineCoordinateSpace_bool).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.SetUpVectorAtSplinePoint(int, System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool) Method +Sets the up vector at a given spline point +```csharp +public void SetUpVectorAtSplinePoint(int pointIndex, in System.Numerics.Vector3 upVector, UnrealEngine.Framework.SplineCoordinateSpace coordinateSpace, bool updateSpline=true); +``` +#### Parameters + +`pointIndex` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +`upVector` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`coordinateSpace` [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') + + +`updateSpline` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +``` + +`API/SplineComponent-SplineComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public SplineComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/SplineComponent-SplinePointsNumber.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.SplinePointsNumber Property +Returns the number of spline points +```csharp +public int SplinePointsNumber { get; } +``` +#### Property Value +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/SplineComponent-SplineSegmentsNumber.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.SplineSegmentsNumber Property +Returns the number of spline segments +```csharp +public int SplineSegmentsNumber { get; } +``` +#### Property Value +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/SplineComponent-UpdateSpline().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +## SplineComponent.UpdateSpline() Method +Updates the spline +```csharp +public void UpdateSpline(); +``` + +``` + +`API/SplineComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## SplineComponent Class +Represents a spline shape +```csharp +public class SplineComponent : UnrealEngine.Framework.PrimitiveComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') 🡒 SplineComponent +### Constructors +- [SplineComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./SplineComponent-SplineComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.SplineComponent.SplineComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Properties +- [Duration](./SplineComponent-Duration.md 'UnrealEngine.Framework.SplineComponent.Duration') +- [IsClosedLoop](./SplineComponent-IsClosedLoop.md 'UnrealEngine.Framework.SplineComponent.IsClosedLoop') +- [SplinePointsNumber](./SplineComponent-SplinePointsNumber.md 'UnrealEngine.Framework.SplineComponent.SplinePointsNumber') +- [SplineSegmentsNumber](./SplineComponent-SplineSegmentsNumber.md 'UnrealEngine.Framework.SplineComponent.SplineSegmentsNumber') +### Methods +- [AddSplinePoint(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-AddSplinePoint(Vector3_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.AddSplinePoint(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [AddSplinePointAtIndex(System.Numerics.Vector3, int, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-AddSplinePointAtIndex(Vector3_int_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.AddSplinePointAtIndex(System.Numerics.Vector3, int, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [ClearSplinePoints(bool)](./SplineComponent-ClearSplinePoints(bool).md 'UnrealEngine.Framework.SplineComponent.ClearSplinePoints(bool)') +- [FindDirectionClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-FindDirectionClosestToWorldLocation(Vector3_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.FindDirectionClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)') +- [FindLocationClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-FindLocationClosestToWorldLocation(Vector3_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.FindLocationClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)') +- [FindRightVectorClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-FindRightVectorClosestToWorldLocation(Vector3_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.FindRightVectorClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)') +- [FindRollClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-FindRollClosestToWorldLocation(Vector3_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.FindRollClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)') +- [FindScaleClosestToWorldLocation(System.Numerics.Vector3)](./SplineComponent-FindScaleClosestToWorldLocation(Vector3).md 'UnrealEngine.Framework.SplineComponent.FindScaleClosestToWorldLocation(System.Numerics.Vector3)') +- [FindTangentClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-FindTangentClosestToWorldLocation(Vector3_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.FindTangentClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)') +- [FindTransformClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-FindTransformClosestToWorldLocation(Vector3_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.FindTransformClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [FindUpVectorClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-FindUpVectorClosestToWorldLocation(Vector3_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.FindUpVectorClosestToWorldLocation(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetArriveTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetArriveTangentAtSplinePoint(int_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetArriveTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetArriveTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetArriveTangentAtSplinePoint(int_SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetArriveTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetDefaultUpVector(UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetDefaultUpVector(SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetDefaultUpVector(UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetDefaultUpVector(UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetDefaultUpVector(SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetDefaultUpVector(UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetDirectionAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetDirectionAtDistanceAlongSpline(float_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetDirectionAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetDirectionAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetDirectionAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetDirectionAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetDirectionAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetDirectionAtSplinePoint(int_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetDirectionAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetDirectionAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetDirectionAtSplinePoint(int_SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetDirectionAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetDirectionAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-GetDirectionAtTime(float_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.GetDirectionAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [GetDirectionAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Vector3)](./SplineComponent-GetDirectionAtTime(float_SplineCoordinateSpace_bool_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetDirectionAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Vector3)') +- [GetDistanceAlongSplineAtSplinePoint(int)](./SplineComponent-GetDistanceAlongSplineAtSplinePoint(int).md 'UnrealEngine.Framework.SplineComponent.GetDistanceAlongSplineAtSplinePoint(int)') +- [GetLeaveTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetLeaveTangentAtSplinePoint(int_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetLeaveTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetLeaveTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetLeaveTangentAtSplinePoint(int_SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetLeaveTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetLocationAndTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3, System.Numerics.Vector3)](./SplineComponent-GetLocationAndTangentAtSplinePoint(int_SplineCoordinateSpace_Vector3_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetLocationAndTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3, System.Numerics.Vector3)') +- [GetLocationAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetLocationAtDistanceAlongSpline(float_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetLocationAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetLocationAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetLocationAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetLocationAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetLocationAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetLocationAtSplinePoint(int_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetLocationAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetLocationAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetLocationAtSplinePoint(int_SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetLocationAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetLocationAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetLocationAtTime(float_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetLocationAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetLocationAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetLocationAtTime(float_SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetLocationAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetRightVectorAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetRightVectorAtDistanceAlongSpline(float_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetRightVectorAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetRightVectorAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetRightVectorAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetRightVectorAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetRightVectorAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetRightVectorAtSplinePoint(int_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetRightVectorAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetRightVectorAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetRightVectorAtSplinePoint(int_SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetRightVectorAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetRightVectorAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-GetRightVectorAtTime(float_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.GetRightVectorAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [GetRightVectorAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Vector3)](./SplineComponent-GetRightVectorAtTime(float_SplineCoordinateSpace_bool_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetRightVectorAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Vector3)') +- [GetRollAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetRollAtDistanceAlongSpline(float_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetRollAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetRollAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetRollAtSplinePoint(int_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetRollAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetRollAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-GetRollAtTime(float_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.GetRollAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [GetRotationAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetRotationAtDistanceAlongSpline(float_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetRotationAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetRotationAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Quaternion)](./SplineComponent-GetRotationAtDistanceAlongSpline(float_SplineCoordinateSpace_Quaternion).md 'UnrealEngine.Framework.SplineComponent.GetRotationAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Quaternion)') +- [GetRotationAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetRotationAtSplinePoint(int_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetRotationAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetRotationAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Quaternion)](./SplineComponent-GetRotationAtSplinePoint(int_SplineCoordinateSpace_Quaternion).md 'UnrealEngine.Framework.SplineComponent.GetRotationAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Quaternion)') +- [GetRotationAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-GetRotationAtTime(float_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.GetRotationAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [GetRotationAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Quaternion)](./SplineComponent-GetRotationAtTime(float_SplineCoordinateSpace_bool_Quaternion).md 'UnrealEngine.Framework.SplineComponent.GetRotationAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Quaternion)') +- [GetScaleAtDistanceAlongSpline(float)](./SplineComponent-GetScaleAtDistanceAlongSpline(float).md 'UnrealEngine.Framework.SplineComponent.GetScaleAtDistanceAlongSpline(float)') +- [GetScaleAtDistanceAlongSpline(float, System.Numerics.Vector3)](./SplineComponent-GetScaleAtDistanceAlongSpline(float_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetScaleAtDistanceAlongSpline(float, System.Numerics.Vector3)') +- [GetScaleAtSplinePoint(int)](./SplineComponent-GetScaleAtSplinePoint(int).md 'UnrealEngine.Framework.SplineComponent.GetScaleAtSplinePoint(int)') +- [GetScaleAtSplinePoint(int, System.Numerics.Vector3)](./SplineComponent-GetScaleAtSplinePoint(int_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetScaleAtSplinePoint(int, System.Numerics.Vector3)') +- [GetScaleAtTime(float, bool)](./SplineComponent-GetScaleAtTime(float_bool).md 'UnrealEngine.Framework.SplineComponent.GetScaleAtTime(float, bool)') +- [GetScaleAtTime(float, bool, System.Numerics.Vector3)](./SplineComponent-GetScaleAtTime(float_bool_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetScaleAtTime(float, bool, System.Numerics.Vector3)') +- [GetSplineLength()](./SplineComponent-GetSplineLength().md 'UnrealEngine.Framework.SplineComponent.GetSplineLength()') +- [GetSplinePointType(int)](./SplineComponent-GetSplinePointType(int).md 'UnrealEngine.Framework.SplineComponent.GetSplinePointType(int)') +- [GetTangentAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetTangentAtDistanceAlongSpline(float_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetTangentAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetTangentAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetTangentAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetTangentAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetTangentAtSplinePoint(int_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetTangentAtSplinePoint(int_SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetTangentAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetTangentAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-GetTangentAtTime(float_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.GetTangentAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [GetTangentAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Vector3)](./SplineComponent-GetTangentAtTime(float_SplineCoordinateSpace_bool_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetTangentAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Vector3)') +- [GetTransformAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetTransformAtDistanceAlongSpline(float_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetTransformAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetTransformAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, UnrealEngine.Framework.Transform)](./SplineComponent-GetTransformAtDistanceAlongSpline(float_SplineCoordinateSpace_Transform).md 'UnrealEngine.Framework.SplineComponent.GetTransformAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, UnrealEngine.Framework.Transform)') +- [GetTransformAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-GetTransformAtSplinePoint(int_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.GetTransformAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [GetTransformAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, bool, UnrealEngine.Framework.Transform)](./SplineComponent-GetTransformAtSplinePoint(int_SplineCoordinateSpace_bool_Transform).md 'UnrealEngine.Framework.SplineComponent.GetTransformAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, bool, UnrealEngine.Framework.Transform)') +- [GetTransformAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, bool)](./SplineComponent-GetTransformAtTime(float_SplineCoordinateSpace_bool_bool).md 'UnrealEngine.Framework.SplineComponent.GetTransformAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, bool)') +- [GetTransformAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, bool, UnrealEngine.Framework.Transform)](./SplineComponent-GetTransformAtTime(float_SplineCoordinateSpace_bool_bool_Transform).md 'UnrealEngine.Framework.SplineComponent.GetTransformAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, bool, UnrealEngine.Framework.Transform)') +- [GetUpVectorAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetUpVectorAtDistanceAlongSpline(float_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetUpVectorAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetUpVectorAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetUpVectorAtDistanceAlongSpline(float_SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetUpVectorAtDistanceAlongSpline(float, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetUpVectorAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-GetUpVectorAtSplinePoint(int_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.GetUpVectorAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace)') +- [GetUpVectorAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)](./SplineComponent-GetUpVectorAtSplinePoint(int_SplineCoordinateSpace_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetUpVectorAtSplinePoint(int, UnrealEngine.Framework.SplineCoordinateSpace, System.Numerics.Vector3)') +- [GetUpVectorAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-GetUpVectorAtTime(float_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.GetUpVectorAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [GetUpVectorAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Vector3)](./SplineComponent-GetUpVectorAtTime(float_SplineCoordinateSpace_bool_Vector3).md 'UnrealEngine.Framework.SplineComponent.GetUpVectorAtTime(float, UnrealEngine.Framework.SplineCoordinateSpace, bool, System.Numerics.Vector3)') +- [RemoveSplinePoint(int, bool)](./SplineComponent-RemoveSplinePoint(int_bool).md 'UnrealEngine.Framework.SplineComponent.RemoveSplinePoint(int, bool)') +- [SetClosedLoop(bool, bool)](./SplineComponent-SetClosedLoop(bool_bool).md 'UnrealEngine.Framework.SplineComponent.SetClosedLoop(bool, bool)') +- [SetDefaultUpVector(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)](./SplineComponent-SetDefaultUpVector(Vector3_SplineCoordinateSpace).md 'UnrealEngine.Framework.SplineComponent.SetDefaultUpVector(System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace)') +- [SetLocationAtSplinePoint(int, System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-SetLocationAtSplinePoint(int_Vector3_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.SetLocationAtSplinePoint(int, System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [SetSplinePointType(int, UnrealEngine.Framework.SplinePointType, bool)](./SplineComponent-SetSplinePointType(int_SplinePointType_bool).md 'UnrealEngine.Framework.SplineComponent.SetSplinePointType(int, UnrealEngine.Framework.SplinePointType, bool)') +- [SetTangentAtSplinePoint(int, System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-SetTangentAtSplinePoint(int_Vector3_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.SetTangentAtSplinePoint(int, System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [SetTangentsAtSplinePoint(int, System.Numerics.Vector3, System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-SetTangentsAtSplinePoint(int_Vector3_Vector3_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.SetTangentsAtSplinePoint(int, System.Numerics.Vector3, System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [SetUpVectorAtSplinePoint(int, System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool)](./SplineComponent-SetUpVectorAtSplinePoint(int_Vector3_SplineCoordinateSpace_bool).md 'UnrealEngine.Framework.SplineComponent.SetUpVectorAtSplinePoint(int, System.Numerics.Vector3, UnrealEngine.Framework.SplineCoordinateSpace, bool)') +- [UpdateSpline()](./SplineComponent-UpdateSpline().md 'UnrealEngine.Framework.SplineComponent.UpdateSpline()') + +``` + +`API/SplineCoordinateSpace.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## SplineCoordinateSpace Enum +Defines coordinate space +```csharp +public enum SplineCoordinateSpace +``` +### Fields + +`Local` 0 + + + +`World` 1 + + + +``` + +`API/SplinePointType.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## SplinePointType Enum +Defines the spline point type +```csharp +public enum SplinePointType +``` +### Fields + +`Linear` 0 + + + +`Curve` 1 + + + +`Constant` 2 + + + +`CurveClamped` 3 + + + +`CurveCustomTangent` 4 + + + +``` + +`API/SpotLight-SpotLight(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpotLight](./SpotLight.md 'UnrealEngine.Framework.SpotLight') +## SpotLight(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public SpotLight(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/SpotLight.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## SpotLight Class +Emits light from a single point in a cone shape +```csharp +public class SpotLight : UnrealEngine.Framework.Light +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 [Light](./Light.md 'UnrealEngine.Framework.Light') 🡒 SpotLight +### Constructors +- [SpotLight(string, UnrealEngine.Framework.Blueprint)](./SpotLight-SpotLight(string_Blueprint).md 'UnrealEngine.Framework.SpotLight.SpotLight(string, UnrealEngine.Framework.Blueprint)') + +``` + +`API/SpringArmComponent-CameraLagMaxDistance.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.CameraLagMaxDistance Property +Gets or sets a max distance the camera target may lag behind the current location +```csharp +public float CameraLagMaxDistance { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SpringArmComponent-CameraLagMaxTimeStep.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.CameraLagMaxTimeStep Property +Gets or sets a max time step used when sub-stepping camera lag +```csharp +public float CameraLagMaxTimeStep { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SpringArmComponent-CameraLagSubstepping.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.CameraLagSubstepping Property +Gets or sets whether the sub-step camera damping so that it handles fluctuating frame rates well +```csharp +public bool CameraLagSubstepping { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SpringArmComponent-CameraPositionLag.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.CameraPositionLag Property +Gets or sets whether the camera lags behind target position to smooth its movement +```csharp +public bool CameraPositionLag { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SpringArmComponent-CameraPositionLagSpeed.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.CameraPositionLagSpeed Property +Gets or sets how quickly the camera reaches a target position +```csharp +public float CameraPositionLagSpeed { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SpringArmComponent-CameraRotationLag.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.CameraRotationLag Property +Gets or sets whether the camera lags behind target rotation to smooth its movement +```csharp +public bool CameraRotationLag { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SpringArmComponent-CameraRotationLagSpeed.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.CameraRotationLagSpeed Property +Gets or sets how quickly еру camera reaches a target rotation +```csharp +public float CameraRotationLagSpeed { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SpringArmComponent-CollisionTest.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.CollisionTest Property +Gets or sets whether the collision test is enabled using [ProbeChannel](./SpringArmComponent-ProbeChannel.md 'UnrealEngine.Framework.SpringArmComponent.ProbeChannel') and [ProbeSize](./SpringArmComponent-ProbeSize.md 'UnrealEngine.Framework.SpringArmComponent.ProbeSize') to prevent camera clipping into level +```csharp +public bool CollisionTest { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SpringArmComponent-DrawDebugLagMarkers.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.DrawDebugLagMarkers Property +Gets or sets whether draw markers at the camera target (in green) and the lagged position (in yellow) if the camera location lag is enabled +```csharp +public bool DrawDebugLagMarkers { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SpringArmComponent-GetDesiredRotation().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.GetDesiredRotation() Method +Returns the desired rotation for the spring arm, before the rotation constraints such as [InheritYaw](./SpringArmComponent-InheritYaw.md 'UnrealEngine.Framework.SpringArmComponent.InheritYaw'), [InheritPitch](./SpringArmComponent-InheritPitch.md 'UnrealEngine.Framework.SpringArmComponent.InheritPitch'), or [InheritRoll](./SpringArmComponent-InheritRoll.md 'UnrealEngine.Framework.SpringArmComponent.InheritRoll') are enforced +```csharp +public System.Numerics.Quaternion GetDesiredRotation(); +``` +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/SpringArmComponent-GetDesiredRotation(Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.GetDesiredRotation(System.Numerics.Quaternion) Method +Retrieves the desired rotation for the spring arm, before the rotation constraints such as [InheritYaw](./SpringArmComponent-InheritYaw.md 'UnrealEngine.Framework.SpringArmComponent.InheritYaw'), [InheritPitch](./SpringArmComponent-InheritPitch.md 'UnrealEngine.Framework.SpringArmComponent.InheritPitch'), or [InheritRoll](./SpringArmComponent-InheritRoll.md 'UnrealEngine.Framework.SpringArmComponent.InheritRoll') are enforced +```csharp +public void GetDesiredRotation(ref System.Numerics.Quaternion value); +``` +#### Parameters + +`value` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/SpringArmComponent-GetSocketOffset().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.GetSocketOffset() Method +Returns offset at the end of the spring arm, can be used instead of the relative offset of the attached component to ensure the line trace works as desired +```csharp +public System.Numerics.Vector3 GetSocketOffset(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SpringArmComponent-GetSocketOffset(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.GetSocketOffset(System.Numerics.Vector3) Method +Retrieves offset at the end of the spring arm, can be used instead of the relative offset of the attached component to ensure the line trace works as desired +```csharp +public void GetSocketOffset(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SpringArmComponent-GetTargetOffset().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.GetTargetOffset() Method +Returns offset at the start of the spring arm in world space +```csharp +public System.Numerics.Vector3 GetTargetOffset(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SpringArmComponent-GetTargetOffset(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.GetTargetOffset(System.Numerics.Vector3) Method +Retrieves offset at the start of the spring arm in world space +```csharp +public void GetTargetOffset(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SpringArmComponent-GetTargetRotation().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.GetTargetRotation() Method +Returns the target inherited rotation +```csharp +public System.Numerics.Quaternion GetTargetRotation(); +``` +#### Returns +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/SpringArmComponent-GetTargetRotation(Quaternion).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.GetTargetRotation(System.Numerics.Quaternion) Method +Retrieves the target inherited rotation +```csharp +public void GetTargetRotation(ref System.Numerics.Quaternion value); +``` +#### Parameters + +`value` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +``` + +`API/SpringArmComponent-GetUnfixedCameraPosition().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.GetUnfixedCameraPosition() Method +Returns the unfixed camera position +```csharp +public System.Numerics.Vector3 GetUnfixedCameraPosition(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/SpringArmComponent-GetUnfixedCameraPosition(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.GetUnfixedCameraPosition(System.Numerics.Vector3) Method +Retrieves the unfixed camera position +```csharp +public void GetUnfixedCameraPosition(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SpringArmComponent-InheritPitch.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.InheritPitch Property +Gets or sets whether the component should inherit pitch from the parent component, has no effect if using absolute rotation +```csharp +public bool InheritPitch { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SpringArmComponent-InheritRoll.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.InheritRoll Property +Gets or sets whether the component should inherit roll from the parent component, has no effect if using absolute rotation +```csharp +public bool InheritRoll { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SpringArmComponent-InheritYaw.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.InheritYaw Property +Gets or sets whether the component should inherit yaw from the parent component, has no effect if using absolute rotation +```csharp +public bool InheritYaw { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SpringArmComponent-IsCollisionFixApplied.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.IsCollisionFixApplied Property +Returns `true` if the collision test displacement being applied +```csharp +public bool IsCollisionFixApplied { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SpringArmComponent-ProbeChannel.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.ProbeChannel Property +Gets or sets the collision channel of the query probe ([Camera](./CollisionChannel.md#CollisionChannel-Camera 'UnrealEngine.Framework.CollisionChannel.Camera') by default) +```csharp +public UnrealEngine.Framework.CollisionChannel ProbeChannel { get; set; } +``` +#### Property Value +[CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + +``` + +`API/SpringArmComponent-ProbeSize.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.ProbeSize Property +Gets or sets how big should be the query probe sphere +```csharp +public float ProbeSize { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SpringArmComponent-SetSocketOffset(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.SetSocketOffset(System.Numerics.Vector3) Method +Sets offset at the end of the spring arm, can be used instead of the relative offset of the attached component to ensure the line trace works as desired +```csharp +public void SetSocketOffset(in System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SpringArmComponent-SetTargetOffset(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.SetTargetOffset(System.Numerics.Vector3) Method +Sets offset at the start of the spring arm in world space +```csharp +public void SetTargetOffset(in System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/SpringArmComponent-SpringArmComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public SpringArmComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/SpringArmComponent-TargetArmLength.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.TargetArmLength Property +Gets or sets the natural length of the spring arm when there are no collisions +```csharp +public float TargetArmLength { get; set; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/SpringArmComponent-UsePawnControlRotation.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +## SpringArmComponent.UsePawnControlRotation Property +Gets or sets if the component should use the view/control rotation of the pawn +```csharp +public bool UsePawnControlRotation { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/SpringArmComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## SpringArmComponent Class +A component that maintains its children at a fixed distance from the parent, but will retract the children if there is a collision, and spring back when there is no collision +```csharp +public class SpringArmComponent : UnrealEngine.Framework.SceneComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 SpringArmComponent +### Constructors +- [SpringArmComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./SpringArmComponent-SpringArmComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.SpringArmComponent.SpringArmComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Properties +- [CameraLagMaxDistance](./SpringArmComponent-CameraLagMaxDistance.md 'UnrealEngine.Framework.SpringArmComponent.CameraLagMaxDistance') +- [CameraLagMaxTimeStep](./SpringArmComponent-CameraLagMaxTimeStep.md 'UnrealEngine.Framework.SpringArmComponent.CameraLagMaxTimeStep') +- [CameraLagSubstepping](./SpringArmComponent-CameraLagSubstepping.md 'UnrealEngine.Framework.SpringArmComponent.CameraLagSubstepping') +- [CameraPositionLag](./SpringArmComponent-CameraPositionLag.md 'UnrealEngine.Framework.SpringArmComponent.CameraPositionLag') +- [CameraPositionLagSpeed](./SpringArmComponent-CameraPositionLagSpeed.md 'UnrealEngine.Framework.SpringArmComponent.CameraPositionLagSpeed') +- [CameraRotationLag](./SpringArmComponent-CameraRotationLag.md 'UnrealEngine.Framework.SpringArmComponent.CameraRotationLag') +- [CameraRotationLagSpeed](./SpringArmComponent-CameraRotationLagSpeed.md 'UnrealEngine.Framework.SpringArmComponent.CameraRotationLagSpeed') +- [CollisionTest](./SpringArmComponent-CollisionTest.md 'UnrealEngine.Framework.SpringArmComponent.CollisionTest') +- [DrawDebugLagMarkers](./SpringArmComponent-DrawDebugLagMarkers.md 'UnrealEngine.Framework.SpringArmComponent.DrawDebugLagMarkers') +- [InheritPitch](./SpringArmComponent-InheritPitch.md 'UnrealEngine.Framework.SpringArmComponent.InheritPitch') +- [InheritRoll](./SpringArmComponent-InheritRoll.md 'UnrealEngine.Framework.SpringArmComponent.InheritRoll') +- [InheritYaw](./SpringArmComponent-InheritYaw.md 'UnrealEngine.Framework.SpringArmComponent.InheritYaw') +- [IsCollisionFixApplied](./SpringArmComponent-IsCollisionFixApplied.md 'UnrealEngine.Framework.SpringArmComponent.IsCollisionFixApplied') +- [ProbeChannel](./SpringArmComponent-ProbeChannel.md 'UnrealEngine.Framework.SpringArmComponent.ProbeChannel') +- [ProbeSize](./SpringArmComponent-ProbeSize.md 'UnrealEngine.Framework.SpringArmComponent.ProbeSize') +- [TargetArmLength](./SpringArmComponent-TargetArmLength.md 'UnrealEngine.Framework.SpringArmComponent.TargetArmLength') +- [UsePawnControlRotation](./SpringArmComponent-UsePawnControlRotation.md 'UnrealEngine.Framework.SpringArmComponent.UsePawnControlRotation') +### Methods +- [GetDesiredRotation()](./SpringArmComponent-GetDesiredRotation().md 'UnrealEngine.Framework.SpringArmComponent.GetDesiredRotation()') +- [GetDesiredRotation(System.Numerics.Quaternion)](./SpringArmComponent-GetDesiredRotation(Quaternion).md 'UnrealEngine.Framework.SpringArmComponent.GetDesiredRotation(System.Numerics.Quaternion)') +- [GetSocketOffset()](./SpringArmComponent-GetSocketOffset().md 'UnrealEngine.Framework.SpringArmComponent.GetSocketOffset()') +- [GetSocketOffset(System.Numerics.Vector3)](./SpringArmComponent-GetSocketOffset(Vector3).md 'UnrealEngine.Framework.SpringArmComponent.GetSocketOffset(System.Numerics.Vector3)') +- [GetTargetOffset()](./SpringArmComponent-GetTargetOffset().md 'UnrealEngine.Framework.SpringArmComponent.GetTargetOffset()') +- [GetTargetOffset(System.Numerics.Vector3)](./SpringArmComponent-GetTargetOffset(Vector3).md 'UnrealEngine.Framework.SpringArmComponent.GetTargetOffset(System.Numerics.Vector3)') +- [GetTargetRotation()](./SpringArmComponent-GetTargetRotation().md 'UnrealEngine.Framework.SpringArmComponent.GetTargetRotation()') +- [GetTargetRotation(System.Numerics.Quaternion)](./SpringArmComponent-GetTargetRotation(Quaternion).md 'UnrealEngine.Framework.SpringArmComponent.GetTargetRotation(System.Numerics.Quaternion)') +- [GetUnfixedCameraPosition()](./SpringArmComponent-GetUnfixedCameraPosition().md 'UnrealEngine.Framework.SpringArmComponent.GetUnfixedCameraPosition()') +- [GetUnfixedCameraPosition(System.Numerics.Vector3)](./SpringArmComponent-GetUnfixedCameraPosition(Vector3).md 'UnrealEngine.Framework.SpringArmComponent.GetUnfixedCameraPosition(System.Numerics.Vector3)') +- [SetSocketOffset(System.Numerics.Vector3)](./SpringArmComponent-SetSocketOffset(Vector3).md 'UnrealEngine.Framework.SpringArmComponent.SetSocketOffset(System.Numerics.Vector3)') +- [SetTargetOffset(System.Numerics.Vector3)](./SpringArmComponent-SetTargetOffset(Vector3).md 'UnrealEngine.Framework.SpringArmComponent.SetTargetOffset(System.Numerics.Vector3)') + +``` + +`API/StaticMesh-Cone.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') +## StaticMesh.Cone Field +A basic cone +```csharp +public static readonly StaticMesh Cone; +``` +#### Field Value +[StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') + +``` + +`API/StaticMesh-Cube.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') +## StaticMesh.Cube Field +A basic cube +```csharp +public static readonly StaticMesh Cube; +``` +#### Field Value +[StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') + +``` + +`API/StaticMesh-Cylinder.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') +## StaticMesh.Cylinder Field +A basic cylinder +```csharp +public static readonly StaticMesh Cylinder; +``` +#### Field Value +[StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') + +``` + +`API/StaticMesh-Load(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') +## StaticMesh.Load(string) Method +Finds and loads a static mesh by name +```csharp +public static UnrealEngine.Framework.StaticMesh Load(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') +A static mesh or `null` on failure + +``` + +`API/StaticMesh-Plane.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') +## StaticMesh.Plane Field +A basic plane +```csharp +public static readonly StaticMesh Plane; +``` +#### Field Value +[StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') + +``` + +`API/StaticMesh-Sphere.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') +## StaticMesh.Sphere Field +A basic sphere +```csharp +public static readonly StaticMesh Sphere; +``` +#### Field Value +[StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') + +``` + +`API/StaticMesh.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## StaticMesh Class +A piece of geometry that consists of a static set of polygons +```csharp +public class StaticMesh : UnrealEngine.Framework.StreamableRenderAsset +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [StreamableRenderAsset](./StreamableRenderAsset.md 'UnrealEngine.Framework.StreamableRenderAsset') 🡒 StaticMesh +### Fields +- [Cone](./StaticMesh-Cone.md 'UnrealEngine.Framework.StaticMesh.Cone') +- [Cube](./StaticMesh-Cube.md 'UnrealEngine.Framework.StaticMesh.Cube') +- [Cylinder](./StaticMesh-Cylinder.md 'UnrealEngine.Framework.StaticMesh.Cylinder') +- [Plane](./StaticMesh-Plane.md 'UnrealEngine.Framework.StaticMesh.Plane') +- [Sphere](./StaticMesh-Sphere.md 'UnrealEngine.Framework.StaticMesh.Sphere') +### Methods +- [Load(string)](./StaticMesh-Load(string).md 'UnrealEngine.Framework.StaticMesh.Load(string)') + +``` + +`API/StaticMeshComponent-GetLocalBounds(Vector3_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[StaticMeshComponent](./StaticMeshComponent.md 'UnrealEngine.Framework.StaticMeshComponent') +## StaticMeshComponent.GetLocalBounds(System.Numerics.Vector3, System.Numerics.Vector3) Method +Retrieves local bounds of the mesh +```csharp +public void GetLocalBounds(ref System.Numerics.Vector3 min, ref System.Numerics.Vector3 max); +``` +#### Parameters + +`min` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`max` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/StaticMeshComponent-GetStaticMesh().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[StaticMeshComponent](./StaticMeshComponent.md 'UnrealEngine.Framework.StaticMeshComponent') +## StaticMeshComponent.GetStaticMesh() Method +Returns the static mesh used by this instance or `null` on failure +```csharp +public UnrealEngine.Framework.StaticMesh GetStaticMesh(); +``` +#### Returns +[StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') + +``` + +`API/StaticMeshComponent-SetStaticMesh(StaticMesh).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[StaticMeshComponent](./StaticMeshComponent.md 'UnrealEngine.Framework.StaticMeshComponent') +## StaticMeshComponent.SetStaticMesh(UnrealEngine.Framework.StaticMesh) Method +Returns `true` if the mesh was set +```csharp +public bool SetStaticMesh(UnrealEngine.Framework.StaticMesh staticMesh); +``` +#### Parameters + +`staticMesh` [StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/StaticMeshComponent-StaticMeshComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[StaticMeshComponent](./StaticMeshComponent.md 'UnrealEngine.Framework.StaticMeshComponent') +## StaticMeshComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public StaticMeshComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/StaticMeshComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## StaticMeshComponent Class +A component that is used to create an instance of a static mesh, a piece of geometry that consists of a static set of polygons +```csharp +public class StaticMeshComponent : UnrealEngine.Framework.MeshComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') 🡒 [MeshComponent](./MeshComponent.md 'UnrealEngine.Framework.MeshComponent') 🡒 StaticMeshComponent + +Derived +↳ [InstancedStaticMeshComponent](./InstancedStaticMeshComponent.md 'UnrealEngine.Framework.InstancedStaticMeshComponent') +### Constructors +- [StaticMeshComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./StaticMeshComponent-StaticMeshComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.StaticMeshComponent.StaticMeshComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Methods +- [GetLocalBounds(System.Numerics.Vector3, System.Numerics.Vector3)](./StaticMeshComponent-GetLocalBounds(Vector3_Vector3).md 'UnrealEngine.Framework.StaticMeshComponent.GetLocalBounds(System.Numerics.Vector3, System.Numerics.Vector3)') +- [GetStaticMesh()](./StaticMeshComponent-GetStaticMesh().md 'UnrealEngine.Framework.StaticMeshComponent.GetStaticMesh()') +- [SetStaticMesh(UnrealEngine.Framework.StaticMesh)](./StaticMeshComponent-SetStaticMesh(StaticMesh).md 'UnrealEngine.Framework.StaticMeshComponent.SetStaticMesh(UnrealEngine.Framework.StaticMesh)') + +``` + +`API/StreamableRenderAsset-Equals(StreamableRenderAsset).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[StreamableRenderAsset](./StreamableRenderAsset.md 'UnrealEngine.Framework.StreamableRenderAsset') +## StreamableRenderAsset.Equals(UnrealEngine.Framework.StreamableRenderAsset) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.StreamableRenderAsset other); +``` +#### Parameters + +`other` [StreamableRenderAsset](./StreamableRenderAsset.md 'UnrealEngine.Framework.StreamableRenderAsset') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/StreamableRenderAsset-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[StreamableRenderAsset](./StreamableRenderAsset.md 'UnrealEngine.Framework.StreamableRenderAsset') +## StreamableRenderAsset.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/StreamableRenderAsset-IsCreated.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[StreamableRenderAsset](./StreamableRenderAsset.md 'UnrealEngine.Framework.StreamableRenderAsset') +## StreamableRenderAsset.IsCreated Property +Returns `true` if the object is created +```csharp +public bool IsCreated { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/StreamableRenderAsset.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## StreamableRenderAsset Class +A render asset that can be streamed at runtime +```csharp +public abstract class StreamableRenderAsset : +System.IEquatable +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 StreamableRenderAsset + +Derived +↳ [SkeletalMesh](./SkeletalMesh.md 'UnrealEngine.Framework.SkeletalMesh') +↳ [StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') +↳ [Texture](./Texture.md 'UnrealEngine.Framework.Texture') + +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[StreamableRenderAsset](./StreamableRenderAsset.md 'UnrealEngine.Framework.StreamableRenderAsset')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Properties +- [IsCreated](./StreamableRenderAsset-IsCreated.md 'UnrealEngine.Framework.StreamableRenderAsset.IsCreated') +### Methods +- [Equals(UnrealEngine.Framework.StreamableRenderAsset)](./StreamableRenderAsset-Equals(StreamableRenderAsset).md 'UnrealEngine.Framework.StreamableRenderAsset.Equals(UnrealEngine.Framework.StreamableRenderAsset)') +- [GetHashCode()](./StreamableRenderAsset-GetHashCode().md 'UnrealEngine.Framework.StreamableRenderAsset.GetHashCode()') + +``` + +`API/TeleportType.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## TeleportType Enum +Defines teleportation types of physics body +```csharp +public enum TeleportType : System.Byte +``` +### Fields + +`None` 0 +Don't teleport physics body + + +`TeleportPhysics` 1 +Teleport physics body so that velocity remains the same and no collision occurs + + +`ResetPhysics` 2 +Teleport physics body and reset physics state completely + + +``` + +`API/TextRenderComponent-SetFont(Font).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TextRenderComponent](./TextRenderComponent.md 'UnrealEngine.Framework.TextRenderComponent') +## TextRenderComponent.SetFont(UnrealEngine.Framework.Font) Method +Sets the font of the component +```csharp +public void SetFont(UnrealEngine.Framework.Font value); +``` +#### Parameters + +`value` [Font](./Font.md 'UnrealEngine.Framework.Font') + + +``` + +`API/TextRenderComponent-SetHorizontalAlignment(HorizontalTextAligment).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TextRenderComponent](./TextRenderComponent.md 'UnrealEngine.Framework.TextRenderComponent') +## TextRenderComponent.SetHorizontalAlignment(UnrealEngine.Framework.HorizontalTextAligment) Method +Sets the herizontal alignment +```csharp +public void SetHorizontalAlignment(UnrealEngine.Framework.HorizontalTextAligment value); +``` +#### Parameters + +`value` [HorizontalTextAligment](./HorizontalTextAligment.md 'UnrealEngine.Framework.HorizontalTextAligment') + + +``` + +`API/TextRenderComponent-SetHorizontalSpacingAdjustment(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TextRenderComponent](./TextRenderComponent.md 'UnrealEngine.Framework.TextRenderComponent') +## TextRenderComponent.SetHorizontalSpacingAdjustment(float) Method +Sets the horizontal spacing adjustment +```csharp +public void SetHorizontalSpacingAdjustment(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/TextRenderComponent-SetScale(Vector2).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TextRenderComponent](./TextRenderComponent.md 'UnrealEngine.Framework.TextRenderComponent') +## TextRenderComponent.SetScale(System.Numerics.Vector2) Method +Sets the text scale +```csharp +public void SetScale(in System.Numerics.Vector2 value); +``` +#### Parameters + +`value` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + + +``` + +`API/TextRenderComponent-SetText(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TextRenderComponent](./TextRenderComponent.md 'UnrealEngine.Framework.TextRenderComponent') +## TextRenderComponent.SetText(string) Method +Sets the text of the component +```csharp +public void SetText(string value); +``` +#### Parameters + +`value` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/TextRenderComponent-SetTextMaterial(MaterialInterface).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TextRenderComponent](./TextRenderComponent.md 'UnrealEngine.Framework.TextRenderComponent') +## TextRenderComponent.SetTextMaterial(UnrealEngine.Framework.MaterialInterface) Method +Sets the text material of the component +```csharp +public void SetTextMaterial(UnrealEngine.Framework.MaterialInterface material); +``` +#### Parameters + +`material` [MaterialInterface](./MaterialInterface.md 'UnrealEngine.Framework.MaterialInterface') + + +``` + +`API/TextRenderComponent-SetTextRenderColor(Color).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TextRenderComponent](./TextRenderComponent.md 'UnrealEngine.Framework.TextRenderComponent') +## TextRenderComponent.SetTextRenderColor(System.Drawing.Color) Method +Sets the text render color of the component +```csharp +public void SetTextRenderColor(System.Drawing.Color value); +``` +#### Parameters + +`value` [System.Drawing.Color](https://docs.microsoft.com/en-us/dotnet/api/System.Drawing.Color 'System.Drawing.Color') + + +``` + +`API/TextRenderComponent-SetVerticalAlignment(VerticalTextAligment).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TextRenderComponent](./TextRenderComponent.md 'UnrealEngine.Framework.TextRenderComponent') +## TextRenderComponent.SetVerticalAlignment(UnrealEngine.Framework.VerticalTextAligment) Method +Sets the vertical alignment +```csharp +public void SetVerticalAlignment(UnrealEngine.Framework.VerticalTextAligment value); +``` +#### Parameters + +`value` [VerticalTextAligment](./VerticalTextAligment.md 'UnrealEngine.Framework.VerticalTextAligment') + + +``` + +`API/TextRenderComponent-SetVerticalSpacingAdjustment(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TextRenderComponent](./TextRenderComponent.md 'UnrealEngine.Framework.TextRenderComponent') +## TextRenderComponent.SetVerticalSpacingAdjustment(float) Method +Sets the vertical spacing adjustment +```csharp +public void SetVerticalSpacingAdjustment(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/TextRenderComponent-SetWorldSize(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TextRenderComponent](./TextRenderComponent.md 'UnrealEngine.Framework.TextRenderComponent') +## TextRenderComponent.SetWorldSize(float) Method +Sets the world size of the text +```csharp +public void SetWorldSize(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/TextRenderComponent-TextRenderComponent(Actor_string_bool_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TextRenderComponent](./TextRenderComponent.md 'UnrealEngine.Framework.TextRenderComponent') +## TextRenderComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint) Constructor +Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically +```csharp +public TextRenderComponent(UnrealEngine.Framework.Actor actor, string name=null, bool setAsRoot=false, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`actor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +The actor to attach the component to + + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the component + + +`setAsRoot` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +If `true`, sets the component as the root + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the component + + +``` + +`API/TextRenderComponent.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## TextRenderComponent Class +Renders text in the world +```csharp +public class TextRenderComponent : UnrealEngine.Framework.PrimitiveComponent +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') 🡒 [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') 🡒 [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') 🡒 TextRenderComponent +### Constructors +- [TextRenderComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)](./TextRenderComponent-TextRenderComponent(Actor_string_bool_Blueprint).md 'UnrealEngine.Framework.TextRenderComponent.TextRenderComponent(UnrealEngine.Framework.Actor, string, bool, UnrealEngine.Framework.Blueprint)') +### Methods +- [SetFont(UnrealEngine.Framework.Font)](./TextRenderComponent-SetFont(Font).md 'UnrealEngine.Framework.TextRenderComponent.SetFont(UnrealEngine.Framework.Font)') +- [SetHorizontalAlignment(UnrealEngine.Framework.HorizontalTextAligment)](./TextRenderComponent-SetHorizontalAlignment(HorizontalTextAligment).md 'UnrealEngine.Framework.TextRenderComponent.SetHorizontalAlignment(UnrealEngine.Framework.HorizontalTextAligment)') +- [SetHorizontalSpacingAdjustment(float)](./TextRenderComponent-SetHorizontalSpacingAdjustment(float).md 'UnrealEngine.Framework.TextRenderComponent.SetHorizontalSpacingAdjustment(float)') +- [SetScale(System.Numerics.Vector2)](./TextRenderComponent-SetScale(Vector2).md 'UnrealEngine.Framework.TextRenderComponent.SetScale(System.Numerics.Vector2)') +- [SetText(string)](./TextRenderComponent-SetText(string).md 'UnrealEngine.Framework.TextRenderComponent.SetText(string)') +- [SetTextMaterial(UnrealEngine.Framework.MaterialInterface)](./TextRenderComponent-SetTextMaterial(MaterialInterface).md 'UnrealEngine.Framework.TextRenderComponent.SetTextMaterial(UnrealEngine.Framework.MaterialInterface)') +- [SetTextRenderColor(System.Drawing.Color)](./TextRenderComponent-SetTextRenderColor(Color).md 'UnrealEngine.Framework.TextRenderComponent.SetTextRenderColor(System.Drawing.Color)') +- [SetVerticalAlignment(UnrealEngine.Framework.VerticalTextAligment)](./TextRenderComponent-SetVerticalAlignment(VerticalTextAligment).md 'UnrealEngine.Framework.TextRenderComponent.SetVerticalAlignment(UnrealEngine.Framework.VerticalTextAligment)') +- [SetVerticalSpacingAdjustment(float)](./TextRenderComponent-SetVerticalSpacingAdjustment(float).md 'UnrealEngine.Framework.TextRenderComponent.SetVerticalSpacingAdjustment(float)') +- [SetWorldSize(float)](./TextRenderComponent-SetWorldSize(float).md 'UnrealEngine.Framework.TextRenderComponent.SetWorldSize(float)') + +``` + +`API/Texture.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Texture Class +A representation of the surface of an object +```csharp +public abstract class Texture : UnrealEngine.Framework.StreamableRenderAsset +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [StreamableRenderAsset](./StreamableRenderAsset.md 'UnrealEngine.Framework.StreamableRenderAsset') 🡒 Texture + +Derived +↳ [Texture2D](./Texture2D.md 'UnrealEngine.Framework.Texture2D') + +``` + +`API/Texture2D-GetPixelFormat().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Texture2D](./Texture2D.md 'UnrealEngine.Framework.Texture2D') +## Texture2D.GetPixelFormat() Method +Returns the pixel format +```csharp +public UnrealEngine.Framework.PixelFormat GetPixelFormat(); +``` +#### Returns +[PixelFormat](./PixelFormat.md 'UnrealEngine.Framework.PixelFormat') + +``` + +`API/Texture2D-GetSize().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Texture2D](./Texture2D.md 'UnrealEngine.Framework.Texture2D') +## Texture2D.GetSize() Method +Returns size of the texture +```csharp +public System.Numerics.Vector2 GetSize(); +``` +#### Returns +[System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + +``` + +`API/Texture2D-GetSize(Vector2).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Texture2D](./Texture2D.md 'UnrealEngine.Framework.Texture2D') +## Texture2D.GetSize(System.Numerics.Vector2) Method +Retrieves size of the texture +```csharp +public void GetSize(ref System.Numerics.Vector2 value); +``` +#### Parameters + +`value` [System.Numerics.Vector2](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector2 'System.Numerics.Vector2') + + +``` + +`API/Texture2D-HasAlphaChannel.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Texture2D](./Texture2D.md 'UnrealEngine.Framework.Texture2D') +## Texture2D.HasAlphaChannel Property +Returns `true` if the runtime texture has an alpha channel that is not completely white +```csharp +public bool HasAlphaChannel { get; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Texture2D-Load(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Texture2D](./Texture2D.md 'UnrealEngine.Framework.Texture2D') +## Texture2D.Load(string) Method +Finds and loads a texture by name +```csharp +public static UnrealEngine.Framework.Texture2D Load(string name); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +#### Returns +[Texture2D](./Texture2D.md 'UnrealEngine.Framework.Texture2D') +A texture or `null` on failure + +``` + +`API/Texture2D-Texture2D(byte--_int).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Texture2D](./Texture2D.md 'UnrealEngine.Framework.Texture2D') +## Texture2D(byte[], int) Constructor +Creates a texture asset from a raw PNG, JPEG, BMP, or EXR image buffer +```csharp +public Texture2D(byte[] buffer, int length); +``` +#### Parameters + +`buffer` [System.Byte](https://docs.microsoft.com/en-us/dotnet/api/System.Byte 'System.Byte')[[]](https://docs.microsoft.com/en-us/dotnet/api/System.Array 'System.Array') + + +`length` [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + + +``` + +`API/Texture2D-Texture2D(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Texture2D](./Texture2D.md 'UnrealEngine.Framework.Texture2D') +## Texture2D(string) Constructor +Creates a texture asset from a raw PNG, JPEG, BMP, or EXR image file +```csharp +public Texture2D(string filePath); +``` +#### Parameters + +`filePath` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/Texture2D.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Texture2D Class +A texture asset +```csharp +public class Texture2D : UnrealEngine.Framework.Texture +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [StreamableRenderAsset](./StreamableRenderAsset.md 'UnrealEngine.Framework.StreamableRenderAsset') 🡒 [Texture](./Texture.md 'UnrealEngine.Framework.Texture') 🡒 Texture2D +### Constructors +- [Texture2D(byte[], int)](./Texture2D-Texture2D(byte--_int).md 'UnrealEngine.Framework.Texture2D.Texture2D(byte[], int)') +- [Texture2D(string)](./Texture2D-Texture2D(string).md 'UnrealEngine.Framework.Texture2D.Texture2D(string)') +### Properties +- [HasAlphaChannel](./Texture2D-HasAlphaChannel.md 'UnrealEngine.Framework.Texture2D.HasAlphaChannel') +### Methods +- [GetPixelFormat()](./Texture2D-GetPixelFormat().md 'UnrealEngine.Framework.Texture2D.GetPixelFormat()') +- [GetSize()](./Texture2D-GetSize().md 'UnrealEngine.Framework.Texture2D.GetSize()') +- [GetSize(System.Numerics.Vector2)](./Texture2D-GetSize(Vector2).md 'UnrealEngine.Framework.Texture2D.GetSize(System.Numerics.Vector2)') +- [Load(string)](./Texture2D-Load(string).md 'UnrealEngine.Framework.Texture2D.Load(string)') + +``` + +`API/Transform-Equals(Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Transform](./Transform.md 'UnrealEngine.Framework.Transform') +## Transform.Equals(UnrealEngine.Framework.Transform) Method +Indicates equality of objects +```csharp +public bool Equals(UnrealEngine.Framework.Transform other); +``` +#### Parameters + +`other` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Transform-Equals(object).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Transform](./Transform.md 'UnrealEngine.Framework.Transform') +## Transform.Equals(object) Method +Indicates equality of objects +```csharp +public override bool Equals(object value); +``` +#### Parameters + +`value` [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Transform-GetHashCode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Transform](./Transform.md 'UnrealEngine.Framework.Transform') +## Transform.GetHashCode() Method +Returns a hash code for the object +```csharp +public override int GetHashCode(); +``` +#### Returns +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/Transform-Location.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Transform](./Transform.md 'UnrealEngine.Framework.Transform') +## Transform.Location Property +Gets or sets the location component +```csharp +public System.Numerics.Vector3 Location { get; set; } +``` +#### Property Value +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Transform-Rotation.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Transform](./Transform.md 'UnrealEngine.Framework.Transform') +## Transform.Rotation Property +Gets or sets the rotation component +```csharp +public System.Numerics.Quaternion Rotation { get; set; } +``` +#### Property Value +[System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + +``` + +`API/Transform-Scale.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Transform](./Transform.md 'UnrealEngine.Framework.Transform') +## Transform.Scale Property +Gets or sets the scale component +```csharp +public System.Numerics.Vector3 Scale { get; set; } +``` +#### Property Value +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/Transform-ToString().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Transform](./Transform.md 'UnrealEngine.Framework.Transform') +## Transform.ToString() Method +Returns a string that represents this instance +```csharp +public override string ToString(); +``` +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Transform-ToString(IFormatProvider).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Transform](./Transform.md 'UnrealEngine.Framework.Transform') +## Transform.ToString(System.IFormatProvider) Method +Returns a string that represents this instance +```csharp +public string ToString(System.IFormatProvider formatProvider); +``` +#### Parameters + +`formatProvider` [System.IFormatProvider](https://docs.microsoft.com/en-us/dotnet/api/System.IFormatProvider 'System.IFormatProvider') + +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Transform-Transform(Vector3_Quaternion_Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Transform](./Transform.md 'UnrealEngine.Framework.Transform') +## Transform(System.Numerics.Vector3, System.Numerics.Quaternion, System.Numerics.Vector3) Constructor +Initializes a new instance the transform +```csharp +public Transform(System.Numerics.Vector3 location=default(System.Numerics.Vector3), System.Numerics.Quaternion rotation=default(System.Numerics.Quaternion), System.Numerics.Vector3 scale=default(System.Numerics.Vector3)); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`scale` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/Transform-op_Equality(Transform_Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Transform](./Transform.md 'UnrealEngine.Framework.Transform') +## Transform.operator ==(UnrealEngine.Framework.Transform, UnrealEngine.Framework.Transform) Operator +Tests for equality between two objects +```csharp +public static bool operator ==(UnrealEngine.Framework.Transform left, UnrealEngine.Framework.Transform right); +``` +#### Parameters + +`left` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +`right` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Transform-op_Implicitstring(Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Transform](./Transform.md 'UnrealEngine.Framework.Transform') +## Transform.implicit operator string(UnrealEngine.Framework.Transform) Operator +Implicitly casts this instance to a string +```csharp +public static string implicit operator string(UnrealEngine.Framework.Transform value); +``` +#### Parameters + +`value` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + +#### Returns +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/Transform-op_Inequality(Transform_Transform).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Transform](./Transform.md 'UnrealEngine.Framework.Transform') +## Transform.operator !=(UnrealEngine.Framework.Transform, UnrealEngine.Framework.Transform) Operator +Tests for inequality between two objects +```csharp +public static bool operator !=(UnrealEngine.Framework.Transform left, UnrealEngine.Framework.Transform right); +``` +#### Parameters + +`left` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + + +`right` [Transform](./Transform.md 'UnrealEngine.Framework.Transform') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Transform.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Transform Struct +Transform composed of location, rotation, and scale +```csharp +public struct Transform : +System.IEquatable +``` +Implements [System.IEquatable<](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1')[Transform](./Transform.md 'UnrealEngine.Framework.Transform')[>](https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1 'System.IEquatable`1') +### Constructors +- [Transform(System.Numerics.Vector3, System.Numerics.Quaternion, System.Numerics.Vector3)](./Transform-Transform(Vector3_Quaternion_Vector3).md 'UnrealEngine.Framework.Transform.Transform(System.Numerics.Vector3, System.Numerics.Quaternion, System.Numerics.Vector3)') +### Properties +- [Location](./Transform-Location.md 'UnrealEngine.Framework.Transform.Location') +- [Rotation](./Transform-Rotation.md 'UnrealEngine.Framework.Transform.Rotation') +- [Scale](./Transform-Scale.md 'UnrealEngine.Framework.Transform.Scale') +### Methods +- [Equals(object)](./Transform-Equals(object).md 'UnrealEngine.Framework.Transform.Equals(object)') +- [Equals(UnrealEngine.Framework.Transform)](./Transform-Equals(Transform).md 'UnrealEngine.Framework.Transform.Equals(UnrealEngine.Framework.Transform)') +- [GetHashCode()](./Transform-GetHashCode().md 'UnrealEngine.Framework.Transform.GetHashCode()') +- [ToString()](./Transform-ToString().md 'UnrealEngine.Framework.Transform.ToString()') +- [ToString(System.IFormatProvider)](./Transform-ToString(IFormatProvider).md 'UnrealEngine.Framework.Transform.ToString(System.IFormatProvider)') +### Operators +- [operator ==(UnrealEngine.Framework.Transform, UnrealEngine.Framework.Transform)](./Transform-op_Equality(Transform_Transform).md 'UnrealEngine.Framework.Transform.op_Equality(UnrealEngine.Framework.Transform, UnrealEngine.Framework.Transform)') +- [implicit operator string(UnrealEngine.Framework.Transform)](./Transform-op_Implicitstring(Transform).md 'UnrealEngine.Framework.Transform.op_Implicit string(UnrealEngine.Framework.Transform)') +- [operator !=(UnrealEngine.Framework.Transform, UnrealEngine.Framework.Transform)](./Transform-op_Inequality(Transform_Transform).md 'UnrealEngine.Framework.Transform.op_Inequality(UnrealEngine.Framework.Transform, UnrealEngine.Framework.Transform)') + +``` + +`API/TriggerBase.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## TriggerBase Class +The base class of actors that used to generate collision events +```csharp +public abstract class TriggerBase : UnrealEngine.Framework.Actor +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 TriggerBase + +Derived +↳ [TriggerBox](./TriggerBox.md 'UnrealEngine.Framework.TriggerBox') +↳ [TriggerCapsule](./TriggerCapsule.md 'UnrealEngine.Framework.TriggerCapsule') +↳ [TriggerSphere](./TriggerSphere.md 'UnrealEngine.Framework.TriggerSphere') + +``` + +`API/TriggerBox-TriggerBox(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TriggerBox](./TriggerBox.md 'UnrealEngine.Framework.TriggerBox') +## TriggerBox(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public TriggerBox(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/TriggerBox.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## TriggerBox Class +A box shaped trigger with [BoxComponent](./BoxComponent.md 'UnrealEngine.Framework.BoxComponent') used to generate overlap events +```csharp +public class TriggerBox : UnrealEngine.Framework.TriggerBase +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 [TriggerBase](./TriggerBase.md 'UnrealEngine.Framework.TriggerBase') 🡒 TriggerBox +### Constructors +- [TriggerBox(string, UnrealEngine.Framework.Blueprint)](./TriggerBox-TriggerBox(string_Blueprint).md 'UnrealEngine.Framework.TriggerBox.TriggerBox(string, UnrealEngine.Framework.Blueprint)') + +``` + +`API/TriggerCapsule-TriggerCapsule(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TriggerCapsule](./TriggerCapsule.md 'UnrealEngine.Framework.TriggerCapsule') +## TriggerCapsule(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public TriggerCapsule(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/TriggerCapsule.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## TriggerCapsule Class +A capsule shaped trigger with [CapsuleComponent](./CapsuleComponent.md 'UnrealEngine.Framework.CapsuleComponent') used to generate overlap events +```csharp +public class TriggerCapsule : UnrealEngine.Framework.TriggerBase +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 [TriggerBase](./TriggerBase.md 'UnrealEngine.Framework.TriggerBase') 🡒 TriggerCapsule +### Constructors +- [TriggerCapsule(string, UnrealEngine.Framework.Blueprint)](./TriggerCapsule-TriggerCapsule(string_Blueprint).md 'UnrealEngine.Framework.TriggerCapsule.TriggerCapsule(string, UnrealEngine.Framework.Blueprint)') + +``` + +`API/TriggerSphere-TriggerSphere(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TriggerSphere](./TriggerSphere.md 'UnrealEngine.Framework.TriggerSphere') +## TriggerSphere(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public TriggerSphere(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/TriggerSphere.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## TriggerSphere Class +A sphere shaped trigger with [SphereComponent](./SphereComponent.md 'UnrealEngine.Framework.SphereComponent') used to generate overlap events +```csharp +public class TriggerSphere : UnrealEngine.Framework.TriggerBase +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 [TriggerBase](./TriggerBase.md 'UnrealEngine.Framework.TriggerBase') 🡒 TriggerSphere +### Constructors +- [TriggerSphere(string, UnrealEngine.Framework.Blueprint)](./TriggerSphere-TriggerSphere(string_Blueprint).md 'UnrealEngine.Framework.TriggerSphere.TriggerSphere(string, UnrealEngine.Framework.Blueprint)') + +``` + +`API/TriggerVolume-TriggerVolume(string_Blueprint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[TriggerVolume](./TriggerVolume.md 'UnrealEngine.Framework.TriggerVolume') +## TriggerVolume(string, UnrealEngine.Framework.Blueprint) Constructor +Spawns the actor in the world +```csharp +public TriggerVolume(string name=null, UnrealEngine.Framework.Blueprint blueprint=null); +``` +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor + + +`blueprint` [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +The blueprint class to use as a base class, should be equal to the exact type of the actor + + +``` + +`API/TriggerVolume.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## TriggerVolume Class +An actor that is used to trigger events +```csharp +public class TriggerVolume : UnrealEngine.Framework.Volume +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 [Brush](./Brush.md 'UnrealEngine.Framework.Brush') 🡒 [Volume](./Volume.md 'UnrealEngine.Framework.Volume') 🡒 TriggerVolume +### Constructors +- [TriggerVolume(string, UnrealEngine.Framework.Blueprint)](./TriggerVolume-TriggerVolume(string_Blueprint).md 'UnrealEngine.Framework.TriggerVolume.TriggerVolume(string, UnrealEngine.Framework.Blueprint)') + +``` + +`API/UnrealEngine-Framework.md`: + +```md +## UnrealEngine.Framework Namespace +### Classes +- [Actor](./Actor.md 'UnrealEngine.Framework.Actor') +- [ActorComponent](./ActorComponent.md 'UnrealEngine.Framework.ActorComponent') +- [AIController](./AIController.md 'UnrealEngine.Framework.AIController') +- [AmbientSound](./AmbientSound.md 'UnrealEngine.Framework.AmbientSound') +- [AnimationAsset](./AnimationAsset.md 'UnrealEngine.Framework.AnimationAsset') +- [AnimationCompositeBase](./AnimationCompositeBase.md 'UnrealEngine.Framework.AnimationCompositeBase') +- [AnimationInstance](./AnimationInstance.md 'UnrealEngine.Framework.AnimationInstance') +- [AnimationMontage](./AnimationMontage.md 'UnrealEngine.Framework.AnimationMontage') +- [AnimationSequence](./AnimationSequence.md 'UnrealEngine.Framework.AnimationSequence') +- [AnimationSequenceBase](./AnimationSequenceBase.md 'UnrealEngine.Framework.AnimationSequenceBase') +- [Application](./Application.md 'UnrealEngine.Framework.Application') +- [Assert](./Assert.md 'UnrealEngine.Framework.Assert') +- [AssetRegistry](./AssetRegistry.md 'UnrealEngine.Framework.AssetRegistry') +- [AudioComponent](./AudioComponent.md 'UnrealEngine.Framework.AudioComponent') +- [Blueprint](./Blueprint.md 'UnrealEngine.Framework.Blueprint') +- [BoxComponent](./BoxComponent.md 'UnrealEngine.Framework.BoxComponent') +- [Brush](./Brush.md 'UnrealEngine.Framework.Brush') +- [Camera](./Camera.md 'UnrealEngine.Framework.Camera') +- [CameraComponent](./CameraComponent.md 'UnrealEngine.Framework.CameraComponent') +- [CapsuleComponent](./CapsuleComponent.md 'UnrealEngine.Framework.CapsuleComponent') +- [Character](./Character.md 'UnrealEngine.Framework.Character') +- [ChildActorComponent](./ChildActorComponent.md 'UnrealEngine.Framework.ChildActorComponent') +- [CommandLine](./CommandLine.md 'UnrealEngine.Framework.CommandLine') +- [ConsoleManager](./ConsoleManager.md 'UnrealEngine.Framework.ConsoleManager') +- [ConsoleObject](./ConsoleObject.md 'UnrealEngine.Framework.ConsoleObject') +- [ConsoleVariable](./ConsoleVariable.md 'UnrealEngine.Framework.ConsoleVariable') +- [Controller](./Controller.md 'UnrealEngine.Framework.Controller') +- [Debug](./Debug.md 'UnrealEngine.Framework.Debug') +- [DirectionalLight](./DirectionalLight.md 'UnrealEngine.Framework.DirectionalLight') +- [DirectionalLightComponent](./DirectionalLightComponent.md 'UnrealEngine.Framework.DirectionalLightComponent') +- [Engine](./Engine.md 'UnrealEngine.Framework.Engine') +- [Font](./Font.md 'UnrealEngine.Framework.Font') +- [GameModeBase](./GameModeBase.md 'UnrealEngine.Framework.GameModeBase') +- [HeadMountedDisplay](./HeadMountedDisplay.md 'UnrealEngine.Framework.HeadMountedDisplay') +- [HierarchicalInstancedStaticMeshComponent](./HierarchicalInstancedStaticMeshComponent.md 'UnrealEngine.Framework.HierarchicalInstancedStaticMeshComponent') +- [InputComponent](./InputComponent.md 'UnrealEngine.Framework.InputComponent') +- [InstancedStaticMeshComponent](./InstancedStaticMeshComponent.md 'UnrealEngine.Framework.InstancedStaticMeshComponent') +- [Keys](./Keys.md 'UnrealEngine.Framework.Keys') +- [LevelScript](./LevelScript.md 'UnrealEngine.Framework.LevelScript') +- [Light](./Light.md 'UnrealEngine.Framework.Light') +- [LightComponent](./LightComponent.md 'UnrealEngine.Framework.LightComponent') +- [LightComponentBase](./LightComponentBase.md 'UnrealEngine.Framework.LightComponentBase') +- [Material](./Material.md 'UnrealEngine.Framework.Material') +- [MaterialInstance](./MaterialInstance.md 'UnrealEngine.Framework.MaterialInstance') +- [MaterialInstanceDynamic](./MaterialInstanceDynamic.md 'UnrealEngine.Framework.MaterialInstanceDynamic') +- [MaterialInterface](./MaterialInterface.md 'UnrealEngine.Framework.MaterialInterface') +- [Maths](./Maths.md 'UnrealEngine.Framework.Maths') +- [MeshComponent](./MeshComponent.md 'UnrealEngine.Framework.MeshComponent') +- [MotionControllerComponent](./MotionControllerComponent.md 'UnrealEngine.Framework.MotionControllerComponent') +- [MovementComponent](./MovementComponent.md 'UnrealEngine.Framework.MovementComponent') +- [Pawn](./Pawn.md 'UnrealEngine.Framework.Pawn') +- [Player](./Player.md 'UnrealEngine.Framework.Player') +- [PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +- [PlayerInput](./PlayerInput.md 'UnrealEngine.Framework.PlayerInput') +- [PointLight](./PointLight.md 'UnrealEngine.Framework.PointLight') +- [PostProcessComponent](./PostProcessComponent.md 'UnrealEngine.Framework.PostProcessComponent') +- [PostProcessVolume](./PostProcessVolume.md 'UnrealEngine.Framework.PostProcessVolume') +- [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') +- [RadialForceComponent](./RadialForceComponent.md 'UnrealEngine.Framework.RadialForceComponent') +- [RectLight](./RectLight.md 'UnrealEngine.Framework.RectLight') +- [RotatingMovementComponent](./RotatingMovementComponent.md 'UnrealEngine.Framework.RotatingMovementComponent') +- [SceneComponent](./SceneComponent.md 'UnrealEngine.Framework.SceneComponent') +- [ShapeComponent](./ShapeComponent.md 'UnrealEngine.Framework.ShapeComponent') +- [SkeletalMesh](./SkeletalMesh.md 'UnrealEngine.Framework.SkeletalMesh') +- [SkeletalMeshComponent](./SkeletalMeshComponent.md 'UnrealEngine.Framework.SkeletalMeshComponent') +- [SkinnedMeshComponent](./SkinnedMeshComponent.md 'UnrealEngine.Framework.SkinnedMeshComponent') +- [SoundBase](./SoundBase.md 'UnrealEngine.Framework.SoundBase') +- [SoundWave](./SoundWave.md 'UnrealEngine.Framework.SoundWave') +- [SphereComponent](./SphereComponent.md 'UnrealEngine.Framework.SphereComponent') +- [SplineComponent](./SplineComponent.md 'UnrealEngine.Framework.SplineComponent') +- [SpotLight](./SpotLight.md 'UnrealEngine.Framework.SpotLight') +- [SpringArmComponent](./SpringArmComponent.md 'UnrealEngine.Framework.SpringArmComponent') +- [StaticMesh](./StaticMesh.md 'UnrealEngine.Framework.StaticMesh') +- [StaticMeshComponent](./StaticMeshComponent.md 'UnrealEngine.Framework.StaticMeshComponent') +- [StreamableRenderAsset](./StreamableRenderAsset.md 'UnrealEngine.Framework.StreamableRenderAsset') +- [TextRenderComponent](./TextRenderComponent.md 'UnrealEngine.Framework.TextRenderComponent') +- [Texture](./Texture.md 'UnrealEngine.Framework.Texture') +- [Texture2D](./Texture2D.md 'UnrealEngine.Framework.Texture2D') +- [TriggerBase](./TriggerBase.md 'UnrealEngine.Framework.TriggerBase') +- [TriggerBox](./TriggerBox.md 'UnrealEngine.Framework.TriggerBox') +- [TriggerCapsule](./TriggerCapsule.md 'UnrealEngine.Framework.TriggerCapsule') +- [TriggerSphere](./TriggerSphere.md 'UnrealEngine.Framework.TriggerSphere') +- [TriggerVolume](./TriggerVolume.md 'UnrealEngine.Framework.TriggerVolume') +- [Volume](./Volume.md 'UnrealEngine.Framework.Volume') +- [World](./World.md 'UnrealEngine.Framework.World') +### Structs +- [ActorReference](./ActorReference.md 'UnrealEngine.Framework.ActorReference') +- [Asset](./Asset.md 'UnrealEngine.Framework.Asset') +- [Bounds](./Bounds.md 'UnrealEngine.Framework.Bounds') +- [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') +- [ComponentReference](./ComponentReference.md 'UnrealEngine.Framework.ComponentReference') +- [Hit](./Hit.md 'UnrealEngine.Framework.Hit') +- [LinearColor](./LinearColor.md 'UnrealEngine.Framework.LinearColor') +- [ObjectReference](./ObjectReference.md 'UnrealEngine.Framework.ObjectReference') +- [Transform](./Transform.md 'UnrealEngine.Framework.Transform') +### Enums +- [ActorEventType](./ActorEventType.md 'UnrealEngine.Framework.ActorEventType') +- [AIFocusPriority](./AIFocusPriority.md 'UnrealEngine.Framework.AIFocusPriority') +- [AnimationMode](./AnimationMode.md 'UnrealEngine.Framework.AnimationMode') +- [AttachmentTransformRule](./AttachmentTransformRule.md 'UnrealEngine.Framework.AttachmentTransformRule') +- [AudioFadeCurve](./AudioFadeCurve.md 'UnrealEngine.Framework.AudioFadeCurve') +- [AutoPossessAI](./AutoPossessAI.md 'UnrealEngine.Framework.AutoPossessAI') +- [AutoReceiveInput](./AutoReceiveInput.md 'UnrealEngine.Framework.AutoReceiveInput') +- [BlendType](./BlendType.md 'UnrealEngine.Framework.BlendType') +- [CameraProjectionMode](./CameraProjectionMode.md 'UnrealEngine.Framework.CameraProjectionMode') +- [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') +- [CollisionMode](./CollisionMode.md 'UnrealEngine.Framework.CollisionMode') +- [CollisionResponse](./CollisionResponse.md 'UnrealEngine.Framework.CollisionResponse') +- [CollisionShapeType](./CollisionShapeType.md 'UnrealEngine.Framework.CollisionShapeType') +- [ComponentEventType](./ComponentEventType.md 'UnrealEngine.Framework.ComponentEventType') +- [ComponentMobility](./ComponentMobility.md 'UnrealEngine.Framework.ComponentMobility') +- [ControllerHand](./ControllerHand.md 'UnrealEngine.Framework.ControllerHand') +- [DetachmentTransformRule](./DetachmentTransformRule.md 'UnrealEngine.Framework.DetachmentTransformRule') +- [HorizontalTextAligment](./HorizontalTextAligment.md 'UnrealEngine.Framework.HorizontalTextAligment') +- [InputEvent](./InputEvent.md 'UnrealEngine.Framework.InputEvent') +- [LogLevel](./LogLevel.md 'UnrealEngine.Framework.LogLevel') +- [NetMode](./NetMode.md 'UnrealEngine.Framework.NetMode') +- [PixelFormat](./PixelFormat.md 'UnrealEngine.Framework.PixelFormat') +- [PlaneConstraintAxis](./PlaneConstraintAxis.md 'UnrealEngine.Framework.PlaneConstraintAxis') +- [SplineCoordinateSpace](./SplineCoordinateSpace.md 'UnrealEngine.Framework.SplineCoordinateSpace') +- [SplinePointType](./SplinePointType.md 'UnrealEngine.Framework.SplinePointType') +- [TeleportType](./TeleportType.md 'UnrealEngine.Framework.TeleportType') +- [UpdateTransformFlags](./UpdateTransformFlags.md 'UnrealEngine.Framework.UpdateTransformFlags') +- [VerticalTextAligment](./VerticalTextAligment.md 'UnrealEngine.Framework.VerticalTextAligment') +- [WindowMode](./WindowMode.md 'UnrealEngine.Framework.WindowMode') +### Delegates +- [ActorCursorDelegate(UnrealEngine.Framework.ActorReference)](./ActorCursorDelegate(ActorReference).md 'UnrealEngine.Framework.ActorCursorDelegate(UnrealEngine.Framework.ActorReference)') +- [ActorHitDelegate(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference, System.Numerics.Vector3, UnrealEngine.Framework.Hit)](./ActorHitDelegate(ActorReference_ActorReference_Vector3_Hit).md 'UnrealEngine.Framework.ActorHitDelegate(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference, System.Numerics.Vector3, UnrealEngine.Framework.Hit)') +- [ActorKeyDelegate(UnrealEngine.Framework.ActorReference, string)](./ActorKeyDelegate(ActorReference_string).md 'UnrealEngine.Framework.ActorKeyDelegate(UnrealEngine.Framework.ActorReference, string)') +- [ActorOverlapDelegate(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference)](./ActorOverlapDelegate(ActorReference_ActorReference).md 'UnrealEngine.Framework.ActorOverlapDelegate(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference)') +- [CharacterLandedDelegate(UnrealEngine.Framework.Hit)](./CharacterLandedDelegate(Hit).md 'UnrealEngine.Framework.CharacterLandedDelegate(UnrealEngine.Framework.Hit)') +- [ComponentCursorDelegate(UnrealEngine.Framework.ComponentReference)](./ComponentCursorDelegate(ComponentReference).md 'UnrealEngine.Framework.ComponentCursorDelegate(UnrealEngine.Framework.ComponentReference)') +- [ComponentHitDelegate(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference, System.Numerics.Vector3, UnrealEngine.Framework.Hit)](./ComponentHitDelegate(ComponentReference_ComponentReference_Vector3_Hit).md 'UnrealEngine.Framework.ComponentHitDelegate(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference, System.Numerics.Vector3, UnrealEngine.Framework.Hit)') +- [ComponentKeyDelegate(UnrealEngine.Framework.ComponentReference, string)](./ComponentKeyDelegate(ComponentReference_string).md 'UnrealEngine.Framework.ComponentKeyDelegate(UnrealEngine.Framework.ComponentReference, string)') +- [ComponentOverlapDelegate(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference)](./ComponentOverlapDelegate(ComponentReference_ComponentReference).md 'UnrealEngine.Framework.ComponentOverlapDelegate(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference)') +- [ConsoleCommandDelegate(float)](./ConsoleCommandDelegate(float).md 'UnrealEngine.Framework.ConsoleCommandDelegate(float)') +- [ConsoleVariableDelegate()](./ConsoleVariableDelegate().md 'UnrealEngine.Framework.ConsoleVariableDelegate()') +- [InputAxisDelegate(float)](./InputAxisDelegate(float).md 'UnrealEngine.Framework.InputAxisDelegate(float)') +- [InputDelegate()](./InputDelegate().md 'UnrealEngine.Framework.InputDelegate()') + +``` + +`API/UpdateTransformFlags.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## UpdateTransformFlags Enum +Defines how to update transform during movement +```csharp +public enum UpdateTransformFlags +``` +### Fields + +`None` 0 +Default update + + +`SkipPhysicsUpdate` 1 +Don't update the underlying physics + + +`PropagateFromParent` 2 +The update is coming as a result of the parent updating + + +`OnlyUpdateIfUsingSocket` 4 +Only update child transform if attached to parent via a socket + + +``` + +`API/VerticalTextAligment.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## VerticalTextAligment Enum +Defines the vertical text aligment type +```csharp +public enum VerticalTextAligment +``` +### Fields + +`Top` 0 + + + +`Center` 1 + + + +`Bottom` 2 + + + +``` + +`API/Volume-EncompassesPoint(Vector3_float_float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[Volume](./Volume.md 'UnrealEngine.Framework.Volume') +## Volume.EncompassesPoint(System.Numerics.Vector3, float, float) Method +Returns `true` if a point or sphere overlaps the volume +```csharp +public bool EncompassesPoint(in System.Numerics.Vector3 point, float sphereRadius, ref float distanceToPoint); +``` +#### Parameters + +`point` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`sphereRadius` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +`distanceToPoint` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/Volume.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## Volume Class +An editable 3D volume placed in a level, different types of volumes perform different functions +```csharp +public abstract class Volume : UnrealEngine.Framework.Brush +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 [Actor](./Actor.md 'UnrealEngine.Framework.Actor') 🡒 [Brush](./Brush.md 'UnrealEngine.Framework.Brush') 🡒 Volume + +Derived +↳ [PostProcessVolume](./PostProcessVolume.md 'UnrealEngine.Framework.PostProcessVolume') +↳ [TriggerVolume](./TriggerVolume.md 'UnrealEngine.Framework.TriggerVolume') +### Methods +- [EncompassesPoint(System.Numerics.Vector3, float, float)](./Volume-EncompassesPoint(Vector3_float_float).md 'UnrealEngine.Framework.Volume.EncompassesPoint(System.Numerics.Vector3, float, float)') + +``` + +`API/WindowMode.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## WindowMode Enum +Defines the window mode +```csharp +public enum WindowMode +``` +### Fields + +`Fullscreen` 0 + + + +`WindowedFullscreen` 1 + + + +`Windowed` 2 + + + +``` + +`API/World-ActorCount.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.ActorCount Property +Returns the actor count +```csharp +public static int ActorCount { get; } +``` +#### Property Value +[System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') + +``` + +`API/World-CurrentLevelName.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.CurrentLevelName Property +Returns the name of the current level +```csharp +public static string CurrentLevelName { get; } +``` +#### Property Value +[System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + +``` + +`API/World-DeltaTime.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.DeltaTime Property +Returns the frame delta time in seconds +```csharp +public static float DeltaTime { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/World-ForEachActor-T-(Action-T-).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.ForEachActor<T>(System.Action<T>) Method +Performs the specified action on each actor in the world +```csharp +public static void ForEachActor(System.Action action) + where T : UnrealEngine.Framework.Actor; +``` +#### Type parameters + +`T` + +Constraints [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Parameters + +`action` [System.Action<](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1')[T](#UnrealEngine-Framework-World-ForEachActor-T-(System-Action-T-)-T 'UnrealEngine.Framework.World.ForEachActor<T>(System.Action<T>).T')[>](https://docs.microsoft.com/en-us/dotnet/api/System.Action-1 'System.Action`1') + + +``` + +`API/World-GetActor-T-(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.GetActor<T>(string) Method +Returns the first actor in the world of the specified class, optionally with the specified name, this operation is slow and should be used with caution +```csharp +public static T GetActor(string name=null) + where T : UnrealEngine.Framework.Actor; +``` +#### Type parameters + +`T` +The type of the actor + +Constraints [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Parameters + +`name` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The name of the actor, may differ from the label in the editor + +#### Returns +[T](#UnrealEngine-Framework-World-GetActor-T-(string)-T 'UnrealEngine.Framework.World.GetActor<T>(string).T') +An actor or `null` on failure + +``` + +`API/World-GetActorByID-T-(uint).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.GetActorByID<T>(uint) Method +Returns the first actor in the world of the specified class and ID, this operation is slow and should be used with caution +```csharp +public static T GetActorByID(uint id) + where T : UnrealEngine.Framework.Actor; +``` +#### Type parameters + +`T` +The type of the actor + +Constraints [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Parameters + +`id` [System.UInt32](https://docs.microsoft.com/en-us/dotnet/api/System.UInt32 'System.UInt32') +The ID of the actor + +#### Returns +[T](#UnrealEngine-Framework-World-GetActorByID-T-(uint)-T 'UnrealEngine.Framework.World.GetActorByID<T>(uint).T') +An actor or `null` on failure + +``` + +`API/World-GetActorByTag-T-(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.GetActorByTag<T>(string) Method +Returns the first actor in the world of the specified class and tag, this operation is slow and should be used with caution +```csharp +public static T GetActorByTag(string tag) + where T : UnrealEngine.Framework.Actor; +``` +#### Type parameters + +`T` +The type of the actor + +Constraints [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + +#### Parameters + +`tag` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') +The tag of the actor + +#### Returns +[T](#UnrealEngine-Framework-World-GetActorByTag-T-(string)-T 'UnrealEngine.Framework.World.GetActorByTag<T>(string).T') +An actor or `null` on failure + +``` + +`API/World-GetFirstPlayerController().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.GetFirstPlayerController() Method +Returns the first player controller +```csharp +public static UnrealEngine.Framework.PlayerController GetFirstPlayerController(); +``` +#### Returns +[PlayerController](./PlayerController.md 'UnrealEngine.Framework.PlayerController') +A player controller or `null` if there is none + +``` + +`API/World-GetGameMode().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.GetGameMode() Method +Returns the current game mode instance which is always valid during gameplay on the server +```csharp +public static UnrealEngine.Framework.GameModeBase GetGameMode(); +``` +#### Returns +[GameModeBase](./GameModeBase.md 'UnrealEngine.Framework.GameModeBase') +A game mode or `null` on failure + +``` + +`API/World-GetWorldOrigin().md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.GetWorldOrigin() Method +Returns the current location of the world origin +```csharp +public static System.Numerics.Vector3 GetWorldOrigin(); +``` +#### Returns +[System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +``` + +`API/World-GetWorldOrigin(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.GetWorldOrigin(System.Numerics.Vector3) Method +Retrieves the current location of the world origin to a reference +```csharp +public static void GetWorldOrigin(ref System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +``` + +`API/World-LineTraceSingleByChannel(Vector3_Vector3_CollisionChannel_Hit_bool_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.LineTraceSingleByChannel(System.Numerics.Vector3, System.Numerics.Vector3, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.Hit, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Traces a ray against the world using a specific channel and retrieves the first blocking hit +```csharp +public static bool LineTraceSingleByChannel(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, UnrealEngine.Framework.CollisionChannel channel, ref UnrealEngine.Framework.Hit hit, bool traceComplex=false, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`channel` [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + + +`hit` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-LineTraceSingleByChannel(Vector3_Vector3_CollisionChannel_Hit_string_bool_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.LineTraceSingleByChannel(System.Numerics.Vector3, System.Numerics.Vector3, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.Hit, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Traces a ray against the world using a specific channel and retrieves the first blocking hit with a bone name +```csharp +public static bool LineTraceSingleByChannel(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, UnrealEngine.Framework.CollisionChannel channel, ref UnrealEngine.Framework.Hit hit, ref string boneName, bool traceComplex=false, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`channel` [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + + +`hit` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-LineTraceSingleByProfile(Vector3_Vector3_string_Hit_bool_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.LineTraceSingleByProfile(System.Numerics.Vector3, System.Numerics.Vector3, string, UnrealEngine.Framework.Hit, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Traces a ray against the world using a specific profile and retrieves the first blocking hit +```csharp +public static bool LineTraceSingleByProfile(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, string profileName, ref UnrealEngine.Framework.Hit hit, bool traceComplex=false, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`profileName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`hit` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-LineTraceSingleByProfile(Vector3_Vector3_string_Hit_string_bool_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.LineTraceSingleByProfile(System.Numerics.Vector3, System.Numerics.Vector3, string, UnrealEngine.Framework.Hit, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Traces a ray against the world using a specific profile and retrieves the first blocking hit with a bone name +```csharp +public static bool LineTraceSingleByProfile(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, string profileName, ref UnrealEngine.Framework.Hit hit, ref string boneName, bool traceComplex=false, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`profileName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`hit` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-LineTraceTestByChannel(Vector3_Vector3_CollisionChannel_bool_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.LineTraceTestByChannel(System.Numerics.Vector3, System.Numerics.Vector3, UnrealEngine.Framework.CollisionChannel, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Traces a ray against the world using a specific channel +```csharp +public static bool LineTraceTestByChannel(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, UnrealEngine.Framework.CollisionChannel channel, bool traceComplex=false, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`channel` [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-LineTraceTestByProfile(Vector3_Vector3_string_bool_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.LineTraceTestByProfile(System.Numerics.Vector3, System.Numerics.Vector3, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Traces a ray against the world using a specific profile +```csharp +public static bool LineTraceTestByProfile(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, string profileName, bool traceComplex=false, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`profileName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-OpenLevel(string).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.OpenLevel(string) Method +Travels to another level +```csharp +public static void OpenLevel(string levelName); +``` +#### Parameters + +`levelName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +``` + +`API/World-OverlapAnyTestByChannel(Vector3_Quaternion_CollisionChannel_CollisionShape_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.OverlapAnyTestByChannel(System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Tests the collision shape at the specified location using a specific channel to determine if any blocking or overlapping occurred +```csharp +public static bool OverlapAnyTestByChannel(in System.Numerics.Vector3 location, in System.Numerics.Quaternion rotation, UnrealEngine.Framework.CollisionChannel channel, in UnrealEngine.Framework.CollisionShape shape, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`channel` [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + + +`shape` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-OverlapAnyTestByProfile(Vector3_Quaternion_string_CollisionShape_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.OverlapAnyTestByProfile(System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Tests the collision shape at the specified location using a specific profile to determine if any blocking or overlapping occurred +```csharp +public static bool OverlapAnyTestByProfile(in System.Numerics.Vector3 location, in System.Numerics.Quaternion rotation, string profileName, in UnrealEngine.Framework.CollisionShape shape, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`profileName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`shape` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-OverlapBlockingTestByChannel(Vector3_Quaternion_CollisionChannel_CollisionShape_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.OverlapBlockingTestByChannel(System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Tests the collision shape at the specified location using a specific channel to determine if any blocking occurred +```csharp +public static bool OverlapBlockingTestByChannel(in System.Numerics.Vector3 location, in System.Numerics.Quaternion rotation, UnrealEngine.Framework.CollisionChannel channel, in UnrealEngine.Framework.CollisionShape shape, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`channel` [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + + +`shape` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-OverlapBlockingTestByProfile(Vector3_Quaternion_string_CollisionShape_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.OverlapBlockingTestByProfile(System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Tests the collision shape at the specified location using a specific profile to determine if any blocking occurred +```csharp +public static bool OverlapBlockingTestByProfile(in System.Numerics.Vector3 location, in System.Numerics.Quaternion rotation, string profileName, in UnrealEngine.Framework.CollisionShape shape, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`location` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`profileName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`shape` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-RealTime.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.RealTime Property +Returns time in seconds since the world was brought up for play, does not stop when the game pauses, not dilated or clamped +```csharp +public static float RealTime { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/World-SetGravity(float).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetGravity(float) Method +Sets the gravity applied to all objects in the world +```csharp +public static void SetGravity(float value); +``` +#### Parameters + +`value` [System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + + +``` + +`API/World-SetOnActorBeginCursorOverCallback(ActorCursorDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnActorBeginCursorOverCallback(UnrealEngine.Framework.ActorCursorDelegate) Method +Sets the callback function that is called when the mouse cursor is moved over an actor if mouse over events are enabled in the player controller +```csharp +public static void SetOnActorBeginCursorOverCallback(UnrealEngine.Framework.ActorCursorDelegate callback); +``` +#### Parameters + +`callback` [ActorCursorDelegate(UnrealEngine.Framework.ActorReference)](./ActorCursorDelegate(ActorReference).md 'UnrealEngine.Framework.ActorCursorDelegate(UnrealEngine.Framework.ActorReference)') + + +``` + +`API/World-SetOnActorBeginOverlapCallback(ActorOverlapDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnActorBeginOverlapCallback(UnrealEngine.Framework.ActorOverlapDelegate) Method +Sets the callback function that is called when actors start overlapping +```csharp +public static void SetOnActorBeginOverlapCallback(UnrealEngine.Framework.ActorOverlapDelegate callback); +``` +#### Parameters + +`callback` [ActorOverlapDelegate(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference)](./ActorOverlapDelegate(ActorReference_ActorReference).md 'UnrealEngine.Framework.ActorOverlapDelegate(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference)') + + +``` + +`API/World-SetOnActorClickedCallback(ActorKeyDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnActorClickedCallback(UnrealEngine.Framework.ActorKeyDelegate) Method +Sets the callback function that is called when the mouse button is clicked while the mouse is over an actor if click events are enabled in the player controller +```csharp +public static void SetOnActorClickedCallback(UnrealEngine.Framework.ActorKeyDelegate callback); +``` +#### Parameters + +`callback` [ActorKeyDelegate(UnrealEngine.Framework.ActorReference, string)](./ActorKeyDelegate(ActorReference_string).md 'UnrealEngine.Framework.ActorKeyDelegate(UnrealEngine.Framework.ActorReference, string)') + + +``` + +`API/World-SetOnActorEndCursorOverCallback(ActorCursorDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnActorEndCursorOverCallback(UnrealEngine.Framework.ActorCursorDelegate) Method +Sets the callback function that is called when the mouse cursor is moved off an actor if mouse over events are enabled in the player controller +```csharp +public static void SetOnActorEndCursorOverCallback(UnrealEngine.Framework.ActorCursorDelegate callback); +``` +#### Parameters + +`callback` [ActorCursorDelegate(UnrealEngine.Framework.ActorReference)](./ActorCursorDelegate(ActorReference).md 'UnrealEngine.Framework.ActorCursorDelegate(UnrealEngine.Framework.ActorReference)') + + +``` + +`API/World-SetOnActorEndOverlapCallback(ActorOverlapDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnActorEndOverlapCallback(UnrealEngine.Framework.ActorOverlapDelegate) Method +Sets the callback function that is called when actors end overlapping +```csharp +public static void SetOnActorEndOverlapCallback(UnrealEngine.Framework.ActorOverlapDelegate callback); +``` +#### Parameters + +`callback` [ActorOverlapDelegate(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference)](./ActorOverlapDelegate(ActorReference_ActorReference).md 'UnrealEngine.Framework.ActorOverlapDelegate(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference)') + + +``` + +`API/World-SetOnActorHitCallback(ActorHitDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnActorHitCallback(UnrealEngine.Framework.ActorHitDelegate) Method +Sets the callback function that is called when actors hit collisions +```csharp +public static void SetOnActorHitCallback(UnrealEngine.Framework.ActorHitDelegate callback); +``` +#### Parameters + +`callback` [ActorHitDelegate(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference, System.Numerics.Vector3, UnrealEngine.Framework.Hit)](./ActorHitDelegate(ActorReference_ActorReference_Vector3_Hit).md 'UnrealEngine.Framework.ActorHitDelegate(UnrealEngine.Framework.ActorReference, UnrealEngine.Framework.ActorReference, System.Numerics.Vector3, UnrealEngine.Framework.Hit)') + + +``` + +`API/World-SetOnActorReleasedCallback(ActorKeyDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnActorReleasedCallback(UnrealEngine.Framework.ActorKeyDelegate) Method +Sets the callback function that is called when the mouse button is released while the mouse is over an actor if click events are enabled in the player controller +```csharp +public static void SetOnActorReleasedCallback(UnrealEngine.Framework.ActorKeyDelegate callback); +``` +#### Parameters + +`callback` [ActorKeyDelegate(UnrealEngine.Framework.ActorReference, string)](./ActorKeyDelegate(ActorReference_string).md 'UnrealEngine.Framework.ActorKeyDelegate(UnrealEngine.Framework.ActorReference, string)') + + +``` + +`API/World-SetOnComponentBeginCursorOverCallback(ComponentCursorDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnComponentBeginCursorOverCallback(UnrealEngine.Framework.ComponentCursorDelegate) Method +Sets the callback function that is called when the mouse cursor is moved over a component and mouse over events are enabled in the player controller +```csharp +public static void SetOnComponentBeginCursorOverCallback(UnrealEngine.Framework.ComponentCursorDelegate callback); +``` +#### Parameters + +`callback` [ComponentCursorDelegate(UnrealEngine.Framework.ComponentReference)](./ComponentCursorDelegate(ComponentReference).md 'UnrealEngine.Framework.ComponentCursorDelegate(UnrealEngine.Framework.ComponentReference)') + + +``` + +`API/World-SetOnComponentBeginOverlapCallback(ComponentOverlapDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnComponentBeginOverlapCallback(UnrealEngine.Framework.ComponentOverlapDelegate) Method +Sets the callback function that is called when primitive components start overlapping +```csharp +public static void SetOnComponentBeginOverlapCallback(UnrealEngine.Framework.ComponentOverlapDelegate callback); +``` +#### Parameters + +`callback` [ComponentOverlapDelegate(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference)](./ComponentOverlapDelegate(ComponentReference_ComponentReference).md 'UnrealEngine.Framework.ComponentOverlapDelegate(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference)') + + +``` + +`API/World-SetOnComponentClickedCallback(ComponentKeyDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnComponentClickedCallback(UnrealEngine.Framework.ComponentKeyDelegate) Method +Sets the callback function that is called when the mouse button is clicked while the mouse is over a component if click events are enabled in the player controller +```csharp +public static void SetOnComponentClickedCallback(UnrealEngine.Framework.ComponentKeyDelegate callback); +``` +#### Parameters + +`callback` [ComponentKeyDelegate(UnrealEngine.Framework.ComponentReference, string)](./ComponentKeyDelegate(ComponentReference_string).md 'UnrealEngine.Framework.ComponentKeyDelegate(UnrealEngine.Framework.ComponentReference, string)') + + +``` + +`API/World-SetOnComponentEndCursorOverCallback(ComponentCursorDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnComponentEndCursorOverCallback(UnrealEngine.Framework.ComponentCursorDelegate) Method +Sets the callback function that is called when the mouse cursor is moved off a component and mouse over events are enabled in the player controller +```csharp +public static void SetOnComponentEndCursorOverCallback(UnrealEngine.Framework.ComponentCursorDelegate callback); +``` +#### Parameters + +`callback` [ComponentCursorDelegate(UnrealEngine.Framework.ComponentReference)](./ComponentCursorDelegate(ComponentReference).md 'UnrealEngine.Framework.ComponentCursorDelegate(UnrealEngine.Framework.ComponentReference)') + + +``` + +`API/World-SetOnComponentEndOverlapCallback(ComponentOverlapDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnComponentEndOverlapCallback(UnrealEngine.Framework.ComponentOverlapDelegate) Method +Sets the callback function that is called when primitive components end overlapping +```csharp +public static void SetOnComponentEndOverlapCallback(UnrealEngine.Framework.ComponentOverlapDelegate callback); +``` +#### Parameters + +`callback` [ComponentOverlapDelegate(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference)](./ComponentOverlapDelegate(ComponentReference_ComponentReference).md 'UnrealEngine.Framework.ComponentOverlapDelegate(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference)') + + +``` + +`API/World-SetOnComponentHitCallback(ComponentHitDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnComponentHitCallback(UnrealEngine.Framework.ComponentHitDelegate) Method +Sets the callback function that is called when components hit collisions +```csharp +public static void SetOnComponentHitCallback(UnrealEngine.Framework.ComponentHitDelegate callback); +``` +#### Parameters + +`callback` [ComponentHitDelegate(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference, System.Numerics.Vector3, UnrealEngine.Framework.Hit)](./ComponentHitDelegate(ComponentReference_ComponentReference_Vector3_Hit).md 'UnrealEngine.Framework.ComponentHitDelegate(UnrealEngine.Framework.ComponentReference, UnrealEngine.Framework.ComponentReference, System.Numerics.Vector3, UnrealEngine.Framework.Hit)') + + +``` + +`API/World-SetOnComponentReleasedCallback(ComponentKeyDelegate).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetOnComponentReleasedCallback(UnrealEngine.Framework.ComponentKeyDelegate) Method +Sets the callback function that is called when the mouse button is released while the mouse is over a component if click events are enabled in the player controller +```csharp +public static void SetOnComponentReleasedCallback(UnrealEngine.Framework.ComponentKeyDelegate callback); +``` +#### Parameters + +`callback` [ComponentKeyDelegate(UnrealEngine.Framework.ComponentReference, string)](./ComponentKeyDelegate(ComponentReference_string).md 'UnrealEngine.Framework.ComponentKeyDelegate(UnrealEngine.Framework.ComponentReference, string)') + + +``` + +`API/World-SetWorldOrigin(Vector3).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SetWorldOrigin(System.Numerics.Vector3) Method +Sets world origin to the specified location +```csharp +public static bool SetWorldOrigin(in System.Numerics.Vector3 value); +``` +#### Parameters + +`value` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` if the world origin was succesfuly shifted, or `false` if one of the levels are pending visibility update + +``` + +`API/World-SimulatePhysics.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SimulatePhysics Property +Gets or sets physics simulation for the world +```csharp +public static bool SimulatePhysics { get; set; } +``` +#### Property Value +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + +``` + +`API/World-SweepSingleByChannel(Vector3_Vector3_Quaternion_CollisionChannel_CollisionShape_Hit_bool_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SweepSingleByChannel(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Hit, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Sweeps a shape against the world using a specific profile and retrieves the first blocking hit +```csharp +public static bool SweepSingleByChannel(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, in System.Numerics.Quaternion rotation, UnrealEngine.Framework.CollisionChannel channel, in UnrealEngine.Framework.CollisionShape shape, ref UnrealEngine.Framework.Hit hit, bool traceComplex=false, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`channel` [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + + +`shape` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + + +`hit` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-SweepSingleByChannel(Vector3_Vector3_Quaternion_CollisionChannel_CollisionShape_Hit_string_bool_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SweepSingleByChannel(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Hit, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Sweeps a shape against the world using a specific profile and retrieves the first blocking hit with a bone name +```csharp +public static bool SweepSingleByChannel(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, in System.Numerics.Quaternion rotation, UnrealEngine.Framework.CollisionChannel channel, in UnrealEngine.Framework.CollisionShape shape, ref UnrealEngine.Framework.Hit hit, ref string boneName, bool traceComplex=false, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`channel` [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + + +`shape` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + + +`hit` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-SweepSingleByProfile(Vector3_Vector3_Quaternion_string_CollisionShape_Hit_bool_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SweepSingleByProfile(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Hit, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Sweeps a shape against the world using a specific profile and retrieves the first blocking hit +```csharp +public static bool SweepSingleByProfile(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, in System.Numerics.Quaternion rotation, string profileName, in UnrealEngine.Framework.CollisionShape shape, ref UnrealEngine.Framework.Hit hit, bool traceComplex=false, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`profileName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`shape` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + + +`hit` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-SweepSingleByProfile(Vector3_Vector3_Quaternion_string_CollisionShape_Hit_string_bool_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SweepSingleByProfile(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Hit, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Sweeps a shape against the world using a specific profile and retrieves the first blocking hit with a bone name +```csharp +public static bool SweepSingleByProfile(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, in System.Numerics.Quaternion rotation, string profileName, in UnrealEngine.Framework.CollisionShape shape, ref UnrealEngine.Framework.Hit hit, ref string boneName, bool traceComplex=false, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`profileName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`shape` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + + +`hit` [Hit](./Hit.md 'UnrealEngine.Framework.Hit') + + +`boneName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-SweepTestByChannel(Vector3_Vector3_Quaternion_CollisionChannel_CollisionShape_bool_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SweepTestByChannel(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Sweeps a shape against the world using a specific profile +```csharp +public static bool SweepTestByChannel(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, in System.Numerics.Quaternion rotation, UnrealEngine.Framework.CollisionChannel channel, in UnrealEngine.Framework.CollisionShape shape, bool traceComplex=false, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`channel` [CollisionChannel](./CollisionChannel.md 'UnrealEngine.Framework.CollisionChannel') + + +`shape` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-SweepTestByProfile(Vector3_Vector3_Quaternion_string_CollisionShape_bool_Actor_PrimitiveComponent).md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.SweepTestByProfile(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent) Method +Sweeps a shape against the world using a specific profile +```csharp +public static bool SweepTestByProfile(in System.Numerics.Vector3 start, in System.Numerics.Vector3 end, in System.Numerics.Quaternion rotation, string profileName, in UnrealEngine.Framework.CollisionShape shape, bool traceComplex=false, UnrealEngine.Framework.Actor ignoredActor=null, UnrealEngine.Framework.PrimitiveComponent ignoredComponent=null); +``` +#### Parameters + +`start` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`end` [System.Numerics.Vector3](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Vector3 'System.Numerics.Vector3') + + +`rotation` [System.Numerics.Quaternion](https://docs.microsoft.com/en-us/dotnet/api/System.Numerics.Quaternion 'System.Numerics.Quaternion') + + +`profileName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String') + + +`shape` [CollisionShape](./CollisionShape.md 'UnrealEngine.Framework.CollisionShape') + + +`traceComplex` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') + + +`ignoredActor` [Actor](./Actor.md 'UnrealEngine.Framework.Actor') + + +`ignoredComponent` [PrimitiveComponent](./PrimitiveComponent.md 'UnrealEngine.Framework.PrimitiveComponent') + +#### Returns +[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean') +`true` on success + +``` + +`API/World-Time.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework').[World](./World.md 'UnrealEngine.Framework.World') +## World.Time Property +Returns time in seconds since the world was brought up for play, it is stopped when the game pauses, it is dilated or clamped +```csharp +public static float Time { get; } +``` +#### Property Value +[System.Single](https://docs.microsoft.com/en-us/dotnet/api/System.Single 'System.Single') + +``` + +`API/World.md`: + +```md +### [UnrealEngine.Framework](./UnrealEngine-Framework.md 'UnrealEngine.Framework') +## World Class +The top-level representation of a map or a sandbox in which actors and components will exist and rendered +```csharp +public static class World +``` +Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 World +### Properties +- [ActorCount](./World-ActorCount.md 'UnrealEngine.Framework.World.ActorCount') +- [CurrentLevelName](./World-CurrentLevelName.md 'UnrealEngine.Framework.World.CurrentLevelName') +- [DeltaTime](./World-DeltaTime.md 'UnrealEngine.Framework.World.DeltaTime') +- [RealTime](./World-RealTime.md 'UnrealEngine.Framework.World.RealTime') +- [SimulatePhysics](./World-SimulatePhysics.md 'UnrealEngine.Framework.World.SimulatePhysics') +- [Time](./World-Time.md 'UnrealEngine.Framework.World.Time') +### Methods +- [ForEachActor<T>(System.Action<T>)](./World-ForEachActor-T-(Action-T-).md 'UnrealEngine.Framework.World.ForEachActor<T>(System.Action<T>)') +- [GetActor<T>(string)](./World-GetActor-T-(string).md 'UnrealEngine.Framework.World.GetActor<T>(string)') +- [GetActorByID<T>(uint)](./World-GetActorByID-T-(uint).md 'UnrealEngine.Framework.World.GetActorByID<T>(uint)') +- [GetActorByTag<T>(string)](./World-GetActorByTag-T-(string).md 'UnrealEngine.Framework.World.GetActorByTag<T>(string)') +- [GetFirstPlayerController()](./World-GetFirstPlayerController().md 'UnrealEngine.Framework.World.GetFirstPlayerController()') +- [GetGameMode()](./World-GetGameMode().md 'UnrealEngine.Framework.World.GetGameMode()') +- [GetWorldOrigin()](./World-GetWorldOrigin().md 'UnrealEngine.Framework.World.GetWorldOrigin()') +- [GetWorldOrigin(System.Numerics.Vector3)](./World-GetWorldOrigin(Vector3).md 'UnrealEngine.Framework.World.GetWorldOrigin(System.Numerics.Vector3)') +- [LineTraceSingleByChannel(System.Numerics.Vector3, System.Numerics.Vector3, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.Hit, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-LineTraceSingleByChannel(Vector3_Vector3_CollisionChannel_Hit_bool_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.LineTraceSingleByChannel(System.Numerics.Vector3, System.Numerics.Vector3, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.Hit, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [LineTraceSingleByChannel(System.Numerics.Vector3, System.Numerics.Vector3, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.Hit, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-LineTraceSingleByChannel(Vector3_Vector3_CollisionChannel_Hit_string_bool_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.LineTraceSingleByChannel(System.Numerics.Vector3, System.Numerics.Vector3, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.Hit, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [LineTraceSingleByProfile(System.Numerics.Vector3, System.Numerics.Vector3, string, UnrealEngine.Framework.Hit, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-LineTraceSingleByProfile(Vector3_Vector3_string_Hit_bool_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.LineTraceSingleByProfile(System.Numerics.Vector3, System.Numerics.Vector3, string, UnrealEngine.Framework.Hit, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [LineTraceSingleByProfile(System.Numerics.Vector3, System.Numerics.Vector3, string, UnrealEngine.Framework.Hit, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-LineTraceSingleByProfile(Vector3_Vector3_string_Hit_string_bool_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.LineTraceSingleByProfile(System.Numerics.Vector3, System.Numerics.Vector3, string, UnrealEngine.Framework.Hit, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [LineTraceTestByChannel(System.Numerics.Vector3, System.Numerics.Vector3, UnrealEngine.Framework.CollisionChannel, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-LineTraceTestByChannel(Vector3_Vector3_CollisionChannel_bool_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.LineTraceTestByChannel(System.Numerics.Vector3, System.Numerics.Vector3, UnrealEngine.Framework.CollisionChannel, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [LineTraceTestByProfile(System.Numerics.Vector3, System.Numerics.Vector3, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-LineTraceTestByProfile(Vector3_Vector3_string_bool_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.LineTraceTestByProfile(System.Numerics.Vector3, System.Numerics.Vector3, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [OpenLevel(string)](./World-OpenLevel(string).md 'UnrealEngine.Framework.World.OpenLevel(string)') +- [OverlapAnyTestByChannel(System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-OverlapAnyTestByChannel(Vector3_Quaternion_CollisionChannel_CollisionShape_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.OverlapAnyTestByChannel(System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [OverlapAnyTestByProfile(System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-OverlapAnyTestByProfile(Vector3_Quaternion_string_CollisionShape_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.OverlapAnyTestByProfile(System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [OverlapBlockingTestByChannel(System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-OverlapBlockingTestByChannel(Vector3_Quaternion_CollisionChannel_CollisionShape_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.OverlapBlockingTestByChannel(System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [OverlapBlockingTestByProfile(System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-OverlapBlockingTestByProfile(Vector3_Quaternion_string_CollisionShape_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.OverlapBlockingTestByProfile(System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [SetGravity(float)](./World-SetGravity(float).md 'UnrealEngine.Framework.World.SetGravity(float)') +- [SetOnActorBeginCursorOverCallback(UnrealEngine.Framework.ActorCursorDelegate)](./World-SetOnActorBeginCursorOverCallback(ActorCursorDelegate).md 'UnrealEngine.Framework.World.SetOnActorBeginCursorOverCallback(UnrealEngine.Framework.ActorCursorDelegate)') +- [SetOnActorBeginOverlapCallback(UnrealEngine.Framework.ActorOverlapDelegate)](./World-SetOnActorBeginOverlapCallback(ActorOverlapDelegate).md 'UnrealEngine.Framework.World.SetOnActorBeginOverlapCallback(UnrealEngine.Framework.ActorOverlapDelegate)') +- [SetOnActorClickedCallback(UnrealEngine.Framework.ActorKeyDelegate)](./World-SetOnActorClickedCallback(ActorKeyDelegate).md 'UnrealEngine.Framework.World.SetOnActorClickedCallback(UnrealEngine.Framework.ActorKeyDelegate)') +- [SetOnActorEndCursorOverCallback(UnrealEngine.Framework.ActorCursorDelegate)](./World-SetOnActorEndCursorOverCallback(ActorCursorDelegate).md 'UnrealEngine.Framework.World.SetOnActorEndCursorOverCallback(UnrealEngine.Framework.ActorCursorDelegate)') +- [SetOnActorEndOverlapCallback(UnrealEngine.Framework.ActorOverlapDelegate)](./World-SetOnActorEndOverlapCallback(ActorOverlapDelegate).md 'UnrealEngine.Framework.World.SetOnActorEndOverlapCallback(UnrealEngine.Framework.ActorOverlapDelegate)') +- [SetOnActorHitCallback(UnrealEngine.Framework.ActorHitDelegate)](./World-SetOnActorHitCallback(ActorHitDelegate).md 'UnrealEngine.Framework.World.SetOnActorHitCallback(UnrealEngine.Framework.ActorHitDelegate)') +- [SetOnActorReleasedCallback(UnrealEngine.Framework.ActorKeyDelegate)](./World-SetOnActorReleasedCallback(ActorKeyDelegate).md 'UnrealEngine.Framework.World.SetOnActorReleasedCallback(UnrealEngine.Framework.ActorKeyDelegate)') +- [SetOnComponentBeginCursorOverCallback(UnrealEngine.Framework.ComponentCursorDelegate)](./World-SetOnComponentBeginCursorOverCallback(ComponentCursorDelegate).md 'UnrealEngine.Framework.World.SetOnComponentBeginCursorOverCallback(UnrealEngine.Framework.ComponentCursorDelegate)') +- [SetOnComponentBeginOverlapCallback(UnrealEngine.Framework.ComponentOverlapDelegate)](./World-SetOnComponentBeginOverlapCallback(ComponentOverlapDelegate).md 'UnrealEngine.Framework.World.SetOnComponentBeginOverlapCallback(UnrealEngine.Framework.ComponentOverlapDelegate)') +- [SetOnComponentClickedCallback(UnrealEngine.Framework.ComponentKeyDelegate)](./World-SetOnComponentClickedCallback(ComponentKeyDelegate).md 'UnrealEngine.Framework.World.SetOnComponentClickedCallback(UnrealEngine.Framework.ComponentKeyDelegate)') +- [SetOnComponentEndCursorOverCallback(UnrealEngine.Framework.ComponentCursorDelegate)](./World-SetOnComponentEndCursorOverCallback(ComponentCursorDelegate).md 'UnrealEngine.Framework.World.SetOnComponentEndCursorOverCallback(UnrealEngine.Framework.ComponentCursorDelegate)') +- [SetOnComponentEndOverlapCallback(UnrealEngine.Framework.ComponentOverlapDelegate)](./World-SetOnComponentEndOverlapCallback(ComponentOverlapDelegate).md 'UnrealEngine.Framework.World.SetOnComponentEndOverlapCallback(UnrealEngine.Framework.ComponentOverlapDelegate)') +- [SetOnComponentHitCallback(UnrealEngine.Framework.ComponentHitDelegate)](./World-SetOnComponentHitCallback(ComponentHitDelegate).md 'UnrealEngine.Framework.World.SetOnComponentHitCallback(UnrealEngine.Framework.ComponentHitDelegate)') +- [SetOnComponentReleasedCallback(UnrealEngine.Framework.ComponentKeyDelegate)](./World-SetOnComponentReleasedCallback(ComponentKeyDelegate).md 'UnrealEngine.Framework.World.SetOnComponentReleasedCallback(UnrealEngine.Framework.ComponentKeyDelegate)') +- [SetWorldOrigin(System.Numerics.Vector3)](./World-SetWorldOrigin(Vector3).md 'UnrealEngine.Framework.World.SetWorldOrigin(System.Numerics.Vector3)') +- [SweepSingleByChannel(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Hit, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-SweepSingleByChannel(Vector3_Vector3_Quaternion_CollisionChannel_CollisionShape_Hit_bool_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.SweepSingleByChannel(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Hit, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [SweepSingleByChannel(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Hit, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-SweepSingleByChannel(Vector3_Vector3_Quaternion_CollisionChannel_CollisionShape_Hit_string_bool_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.SweepSingleByChannel(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Hit, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [SweepSingleByProfile(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Hit, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-SweepSingleByProfile(Vector3_Vector3_Quaternion_string_CollisionShape_Hit_bool_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.SweepSingleByProfile(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Hit, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [SweepSingleByProfile(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Hit, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-SweepSingleByProfile(Vector3_Vector3_Quaternion_string_CollisionShape_Hit_string_bool_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.SweepSingleByProfile(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, UnrealEngine.Framework.Hit, string, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [SweepTestByChannel(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-SweepTestByChannel(Vector3_Vector3_Quaternion_CollisionChannel_CollisionShape_bool_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.SweepTestByChannel(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, UnrealEngine.Framework.CollisionChannel, UnrealEngine.Framework.CollisionShape, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') +- [SweepTestByProfile(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)](./World-SweepTestByProfile(Vector3_Vector3_Quaternion_string_CollisionShape_bool_Actor_PrimitiveComponent).md 'UnrealEngine.Framework.World.SweepTestByProfile(System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Quaternion, string, UnrealEngine.Framework.CollisionShape, bool, UnrealEngine.Framework.Actor, UnrealEngine.Framework.PrimitiveComponent)') + +``` + +`CODE_OF_CONDUCT.md`: + +```md +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at nxrighthere@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq + +``` + +`Install/Install.cs`: + +```cs +using System; +using System.Diagnostics; +using System.IO; + +public static class Install { + private static void Main(string[] arguments) { + Console.Title = "UnrealCLR Installation Tool"; + + using StreamReader consoleReader = new(Console.OpenStandardInput(8192), Console.InputEncoding, false, bufferSize: 1024); + + Console.SetIn(consoleReader); + + string projectPath = null; + bool? compileTestsOption = null; + bool? overwriteFilesOption = null; + + for (int i = 0; i < arguments.Length; i++) { + if (arguments[i].Contains("--project-path", StringComparison.Ordinal)) + projectPath = arguments[i + 1]; + + if (arguments[i].Contains("--compile-tests", StringComparison.Ordinal)) + compileTestsOption = bool.Parse(arguments[i + 1]); + + if (arguments[i].Contains("--overwrite-files", StringComparison.Ordinal)) + overwriteFilesOption = true; + } + + Console.WriteLine("Welcome to the UnrealCLR installation tool!"); + + if (String.IsNullOrEmpty(projectPath)) { + Console.Write(Environment.NewLine + "Please, set a path to an Unreal Engine project: "); + + projectPath = @"" + Console.ReadLine(); + } + + projectPath = projectPath.Replace("\"", String.Empty, StringComparison.Ordinal).Replace("\'", String.Empty, StringComparison.Ordinal).TrimEnd(Path.DirectorySeparatorChar); + + string sourcePath = Path.Combine(Directory.GetCurrentDirectory(), ".."); + + if (Directory.GetFiles(projectPath, "*.uproject", SearchOption.TopDirectoryOnly).Length != 0) { + Console.WriteLine($"Project file found in \"{ projectPath }\" folder!"); + + bool compileTests = false; + + if (compileTestsOption != null) { + compileTests = compileTestsOption.GetValueOrDefault(); + } else { + Console.Write(Environment.NewLine + "Do you want to compile and install tests? [y/n] "); + + if (Console.ReadKey(false).Key == ConsoleKey.Y) + compileTests = true; + } + + bool overwriteFiles = false; + + if (overwriteFilesOption != null) { + overwriteFiles = overwriteFilesOption.GetValueOrDefault(); + } else { + Console.Write(Environment.NewLine + "Installation will delete all previous files of the plugin" + (compileTests ? " and content of tests" : String.Empty) + ". Do you want to continue? [y/n] "); + + if (Console.ReadKey(false).Key == ConsoleKey.Y) + overwriteFiles = true; + } + + if (overwriteFiles) { + string nativeSource = Path.Combine(sourcePath, "Source/Native"); + + Console.WriteLine(Environment.NewLine + "Removing the previous plugin installation..."); + + if (Directory.Exists(Path.Combine(projectPath, "Plugins/UnrealCLR"))) + Directory.Delete(Path.Combine(projectPath, "Plugins/UnrealCLR"), true); + + Console.WriteLine("Copying native source code and the runtime host of the plugin..."); + + foreach (string directoriesPath in Directory.GetDirectories(nativeSource, "*", SearchOption.AllDirectories)) { + Directory.CreateDirectory(directoriesPath.Replace(nativeSource, Path.Combine(projectPath, "Plugins/UnrealCLR"), StringComparison.Ordinal)); + } + + foreach (string filesPath in Directory.GetFiles(nativeSource, "*.*", SearchOption.AllDirectories)) { + File.Copy(filesPath, filesPath.Replace(nativeSource, Path.Combine(projectPath, "Plugins/UnrealCLR"), StringComparison.Ordinal), true); + } + + Console.WriteLine("Launching compilation of the managed runtime..."); + + var runtimeCompilation = Process.Start(new ProcessStartInfo { + FileName = "dotnet", + Arguments = $"publish \"{ sourcePath }/Source/Managed/Runtime\" --configuration Release --framework net6.0 --output \"{ projectPath }/Plugins/UnrealCLR/Managed\"", + CreateNoWindow = false, + UseShellExecute = false + }); + + runtimeCompilation.WaitForExit(); + + if (runtimeCompilation.ExitCode != 0) + Error("Compilation of the runtime was finished with an error (Exit code: " + runtimeCompilation.ExitCode + ")!"); + + Console.WriteLine("Launching compilation of the framework..."); + + var frameworkCompilation = Process.Start(new ProcessStartInfo { + FileName = "dotnet", + Arguments = $"publish \"{ sourcePath }/Source/Managed/Framework\" --configuration Release --framework net6.0 --output \"{ sourcePath }/Source/Managed/Framework/bin/Release\"", + CreateNoWindow = false, + UseShellExecute = false + }); + + frameworkCompilation.WaitForExit(); + + if (frameworkCompilation.ExitCode != 0) + Error("Compilation of the framework was finished with an error (Exit code: " + frameworkCompilation.ExitCode + ")!"); + + if (compileTests) { + string contentPath = Path.Combine(sourcePath, "Content"); + + Console.WriteLine("Removing the previous content of the tests..."); + + if (Directory.Exists(Path.Combine(projectPath, "Content/Tests"))) + Directory.Delete(Path.Combine(projectPath, "Content/Tests"), true); + + Console.WriteLine("Copying the content of the tests..."); + + foreach (string directoriesPath in Directory.GetDirectories(contentPath, "*", SearchOption.AllDirectories)) { + Directory.CreateDirectory(directoriesPath.Replace(contentPath, Path.Combine(projectPath, "Content"), StringComparison.Ordinal)); + } + + foreach (string filesPath in Directory.GetFiles(contentPath, "*.*", SearchOption.AllDirectories)) { + File.Copy(filesPath, filesPath.Replace(contentPath, Path.Combine(projectPath, "Content"), StringComparison.Ordinal), true); + } + + Console.WriteLine("Launching compilation of the tests..."); + + var testsCompilation = Process.Start(new ProcessStartInfo { + FileName = "dotnet", + Arguments = $"publish \"{ sourcePath }/Source/Managed/Tests\" --configuration Release --framework net6.0 --output \"{ projectPath }/Managed/Tests\"", + CreateNoWindow = false, + UseShellExecute = false + }); + + testsCompilation.WaitForExit(); + + if (testsCompilation.ExitCode != 0) + Error("Compilation of the tests was finished with an error (Exit code: " + testsCompilation.ExitCode + ")!"); + } + + Console.Write("Done!"); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write(" Please, don't forget to recompile custom code with an updated framework!"); + Console.ResetColor(); + Environment.Exit(0); + } else { + Console.WriteLine(Environment.NewLine + "Installation canceled"); + } + } else { + Error($"Project file not found in \"{ projectPath }\" folder!"); + } + } + + private static void Error(string message) { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ResetColor(); + Environment.Exit(-1); + } +} + +``` + +`Install/Install.csproj`: + +```csproj + + + + Exe + net6.0 + x64 + CA1028, CA1030, CA1031, CA1034, CA1067, CA1303, CA1305, CA1720 + + + + + + + + +``` + +`LICENSE`: + +``` +MIT License + +Copyright (c) 2021 Stanislav Denisov (nxrighthere@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +`MANUAL.md`: + +```md +- [Getting started](#getting-started) + * [Development](#development) + * [Project](#project) + + [Entry point](#entry-point) + + [Blueprint functions](#blueprint-functions) + * [Packaging](#packaging) +- [Engine](#engine) + * [World events](#world-events) + * [Code structure](#code-structure) + * [Blueprints](#blueprints) + * [Data passing](#data-passing) +- [Tools](#tools) + +Getting started +-------- +### Development +UnrealCLR doesn't depend on how the development environment is organized. Any IDE such as [Visual Studio](https://visualstudio.microsoft.com), [Visual Code](https://code.visualstudio.com), or [Rider](https://www.jetbrains.com/rider/), can be used to manage a project. The programmer has full freedom to set up the building pipeline in any desirable way just as for a regular .NET library. + +### Project +After [building and installing](https://github.com/nxrighthere/UnrealCLR#building) the plugin, use IDE or [CLI tool](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-new) to create a [.NET class library](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-new#classlib) project which targets `net6.0` in any preferable location. Don't store source code in `%Project%/Managed` folder of the engine's project, it's used exclusively for loading and packaging user assemblies by the plugin. + +Add a reference to `UnrealEngine.Framework.dll` assembly located in `Source/Managed/Framework/bin/Release` folder. + +Assuming you put your code in `%Project%/MyDotNetCode`, your project file should look similar to this: +```xml + + + net6.0 + false + ../Managed/Build + + + + + %UnrealCLR%/Source/Managed/Framework/bin/Release/UnrealEngine.Framework.dll + + + +``` + +Create a new or open a source code file in the .NET project and replace its content with the following code: + +#### Entry point + +
+C# + +```csharp +using System; +using System.Drawing; +using UnrealEngine.Framework; + +namespace Game { + public class Main { // Indicates the main entry point for automatic loading by the plugin + public static void OnWorldBegin() => Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "Hello, Unreal Engine!"); + + public static void OnWorldPostBegin() => Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "How's it going?"); + + public static void OnWorldEnd() => Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "See you soon, Unreal Engine!"); + + public static void OnWorldPrePhysicsTick(float deltaTime) => Debug.AddOnScreenMessage(1, 10.0f, Color.DeepPink, "On pre physics tick invoked!"); + + public static void OnWorldDuringPhysicsTick(float deltaTime) => Debug.AddOnScreenMessage(2, 10.0f, Color.DeepPink, "On during physics tick invoked!"); + + public static void OnWorldPostPhysicsTick(float deltaTime) => Debug.AddOnScreenMessage(3, 10.0f, Color.DeepPink, "On post physics tick invoked!"); + + public static void OnWorldPostUpdateTick(float deltaTime) => Debug.AddOnScreenMessage(4, 10.0f, Color.DeepPink, "On post update tick invoked!"); + } +} +``` +
+ +
+F# + +```fsharp +namespace Game + +open System +open System.Drawing +open UnrealEngine.Framework + +module Main = // Indicates the main entry point for automatic loading by the plugin + let OnWorldBegin() = Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "Hello, Unreal Engine!") + + let OnWorldPostBegin() = Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "How's it going?") + + let OnWorldEnd() = Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "See you soon, Unreal Engine!") + + let OnWorldPrePhysicsTick(deltaTime:float32) = Debug.AddOnScreenMessage(1, 10.0f, Color.DeepPink, "On pre physics tick invoked!") + + let OnWorldDuringPhysicsTick(deltaTime:float32) = Debug.AddOnScreenMessage(2, 10.0f, Color.DeepPink, "On during physics tick invoked!") + + let OnWorldPostPhysicsTick(deltaTime:float32) = Debug.AddOnScreenMessage(3, 10.0f, Color.DeepPink, "On post physics tick invoked!") + + let OnWorldPostUpdateTick(deltaTime:float32) = Debug.AddOnScreenMessage(4, 10.0f, Color.DeepPink, "On post update tick invoked!") +``` +
+ +All functions of the main entry point are optional, and it's not necessary to implement them for every [tick group](https://docs.unrealengine.com/en-US/Programming/UnrealArchitecture/Actors/Ticking/index.html). + +[Publish](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish) a .NET assembly to `%Project%/Managed` folder of the engine's project, and make sure that no other assemblies of other .NET projects are stored there. + +Assemblies that no longer referenced and unused in the project will persist in `%Project%/Managed` folder. Consider maintaining this folder through IDE or automation scripts. + +Enter the [play mode](https://docs.unrealengine.com/en-US/Basics/HowTo/PIE/index.html) to execute managed code. Stop the play mode to unload assemblies from memory for further recompilation. + +#### Blueprint functions + +
+C# + +```csharp +using System; +using System.Drawing; +using UnrealEngine.Framework; + +namespace Game { + public class System { // Custom class for loading functions from blueprints + public static void Function() => Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "Blueprint function invoked!"); + } +} +``` +
+ +
+F# + +```fsharp +namespace Game + +open System +open System.Drawing +open UnrealEngine.Framework + +module System = // Custom module for loading functions from blueprints + let Function() = Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "Blueprint function invoked!") +``` +
+ +To run a blueprint function, create a new or open an existing [level](https://docs.unrealengine.com/en-US/Engine/QuickStart/index.html#3.createanewlevel) of the engine. Open level blueprint by navigating to `Blueprints -> Open Level Blueprint` and create a basic execution flow by right-clicking on the graph and selecting nodes from the .NET category: + +

+ graph +

+ +Compile the blueprint and enter the play mode. + +### Packaging +The plugin is transparently integrated into the [packaging](https://docs.unrealengine.com/en-US/Engine/Basics/Projects/Packaging/index.html) pipeline of the engine and ready for standalone distribution. + +Engine +-------- +### World events +The framework provides world events that are executed by the engine in a predetermined order to drive logic and simulation. Event functions are automatically loaded by the plugin from `Main` class for further execution. + +`OnWorldBegin()` Called after the world is initialized before the level script. + +`OnWorldPostBegin()` Called after the level script when default actors are spawned. + +`OnWorldEnd()` Called before deinitialization of the world after the level script. + +`OnWorldPrePhysicsTick(float deltaTime)` Called at the beginning of the frame. + +`OnWorldDuringPhysicsTick(float deltaTime)` Called when physics simulation has begun. + +`OnWorldPostPhysicsTick(float deltaTime)` Called when physics simulation is complete. + +`OnWorldPostUpdateTick(float deltaTime)` Called after cameras are updated. + +### Code structure +The plugin allows organizing the code structure of the project in any preferable way. Any paradigms or patterns can be used to drive logic and simulation without any intermediate management between user code and the engine. + +**Object-Oriented Design** + +```csharp +using System; +using System.Drawing; +using UnrealEngine.Framework; + +namespace Game { + public class Main { + private static Entity[] entities = new Entity[32]; + + public static void OnWorldBegin() { + for (int i = 0; i < entities.Length; i++) { + entities[i] = new Entity(nameof(Entity) + i); + entities[i].OnBegin(); + } + } + + public static void OnWorldPrePhysicsTick(float deltaTime) { + for (int i = 0; i < entities.Length; i++) { + if (entities[i].CanTick) + entities[i].OnPrePhysicsTick(deltaTime); + } + } + } + + public class Entity : Actor { + public Entity(string name = null, bool canTick = true) : base(name) { + CanTick = canTick; + } + + public bool CanTick { get; set; } + + public void OnBegin() => Debug.AddOnScreenMessage(-1, 1.0f, Color.LightSeaGreen, Name + " begin!"); + + public void OnPrePhysicsTick(float deltaTime) => Debug.AddOnScreenMessage(-1, 1.0f, Color.LightSteelBlue, Name + " tick!"); + } +} +``` + +**Data-Oriented Design** + +```csharp +using System; +using System.Drawing; +using UnrealEngine.Framework; + +namespace Game { + public class Main { + private static Actor[] entities = new Actor[32]; + private static bool[] canTick = new bool[entities.Length]; + + public static void OnWorldBegin() { + for (int i = 0; i < entities.Length; i++) { + entities[i] = new Actor("Entity" + i); + canTick[i] = true; + + OnEntityBegin(i); + } + } + + public static void OnWorldPrePhysicsTick(float deltaTime) { + for (int i = 0; i < entities.Length; i++) { + if (canTick[i]) + OnEntityPrePhysicsTick(i, deltaTime); + } + } + + private static void OnEntityBegin(int id) => Debug.AddOnScreenMessage(-1, 1.0f, Color.LightSeaGreen, entities[id].Name + " begin!"); + + private static void OnEntityPrePhysicsTick(int id, float deltaTime) => Debug.AddOnScreenMessage(-1, 1.0f, Color.LightSteelBlue, entities[id].Name + " tick!"); + } +} +``` + +See [tests project](https://github.com/nxrighthere/UnrealCLR/tree/master/Source/Managed/Tests) for a basic implementation of various systems. + +### Blueprints +The plugin provides two blueprints to manage the execution. They can be used in any combinations with other nodes, data types, and C++ code to weave managed functionality with events. It's highly recommended to use creative approaches that extract information from blueprint classes instead of using plain strings for managed functions. + +**Find Managed Function** + +Attempts to find a managed function from loaded assemblies. This node performs a fast operation to retrieve function pointer from the cached dictionary once assemblies loaded after entering the play mode. Logs an error if the managed function was not found and sets `Result` parameter to `false`. `Optional` checkbox indicates if the managed function is optional and the error should not be logged. + +**Execute Managed Function** + +Attempts to execute a managed function. This node performs a fast execution of a function pointer. Optionally allows passing an [object reference](https://github.com/nxrighthere/UnrealCLR/blob/master/API/ObjectReference.md) of the engine to managed code with further conversion to an appropriate type. + +### Data passing +Several options are available to pass data between the managed runtime and the engine. + +**Commands, functions, and events** + +The engine's reflection system allows to dynamically invoke commands, functions, and events of engine classes from managed code to pass data on-demand through custom arguments. + +Blueprint event dispatcher: + +

+ event-dispatcher +

+ +```csharp +blueprint.Invoke($"TestEvent \"{ message }\" { value }"); +``` + +See [Actor.Invoke()](https://github.com/nxrighthere/UnrealCLR/blob/master/API/Actor-Invoke(string).md), [ActorComponent.Invoke()](https://github.com/nxrighthere/UnrealCLR/blob/master/API/ActorComponent-Invoke(string).md), and [AnimationInstance.Invoke()](https://github.com/nxrighthere/UnrealCLR/blob/master/API/AnimationInstance-Invoke(string).md) methods. + +**Blueprint variables** + +The engine provides a convenient way to [manage variables/properties](https://docs.unrealengine.com/en-US/Engine/Blueprints/UserGuide/Variables/index.html) per actor/component basis that are accessible from the world outliner. + +Instance-editable blueprint variables: + +

+ blueprint-variables +

+ +Exposed as properties and accessible from the world outliner: + +

+ editor-properties +

+ +See [Actor](https://github.com/nxrighthere/UnrealCLR/blob/master/API/Actor.md), [ActorComponent](https://github.com/nxrighthere/UnrealCLR/blob/master/API/ActorComponent.md), and [AnimationInstance](https://github.com/nxrighthere/UnrealCLR/blob/master/API/AnimationInstance.md) classes for appropriate methods to get and set data. + +**Console variables** + +Data that should be globally accessible can be stored in console variables and modified from the editor's console. + +

+ console-variable +

+ +See [ConsoleVariable](https://github.com/nxrighthere/UnrealCLR/blob/master/API/ConsoleVariable.md) class for appropriate methods to get and set data. + +Tools +-------- +The plugin is compatible with [.NET tools](https://github.com/natemcmaster/dotnet-tools) and makes the engine's application instance visible as a regular .NET application for IDEs and external programs. + +``` + +`README.md`: + +```md +

+ alt logo +

+ +UnrealCLR is a plugin which natively integrates .NET host into the Unreal Engine with the Common Language Runtime for direct execution of managed code to build a game/application logic using the full power of C# 10.0, F# 6.0, and .NET facilities with engine API. The project is aimed at stability, performance, and maintainability. + +[API reference](https://github.com/nxrighthere/UnrealCLR/blob/master/API/UnrealEngine-Framework.md) | [Manual](https://github.com/nxrighthere/UnrealCLR/blob/master/MANUAL.md) | [Code of Conduct](https://github.com/nxrighthere/UnrealCLR/blob/master/CODE_OF_CONDUCT.md) | [Community](https://github.com/nxrighthere/UnrealCLR/discussions) | [Roadmap](https://github.com/users/nxrighthere/projects/5?fullscreen=true) | [Contact](mailto:nxrighthere@gmail.com) + +Features: + +- Host loading, integration, and management during the lifetime of the engine +- Dynamic loading, unloading, isolation, and dependency resolving of user assemblies at runtime +- On-the-fly access and execution of managed functionality through blueprints +- Runtime exceptions handling and tracing +- Continuously evolving framework for access to the engine API from managed code written in idiomatic C# +- High-performance interoperability through optimized code and utilization of blittable data types +- Support of .NET facilities including hardware-accelerated math with transparent re-mapping to vector types of the engine +- Support of .NET tools for debugging and profiling such as [JetBrains](https://www.jetbrains.com/products.html#lang=csharp) product line, [dnSpy](https://github.com/0xd4d/dnSpy) debugger, and [others](https://www.nuget.org/packages?packagetype=dotnettool) +- Full independence from the compilation pipeline of assemblies with support of [NuGet](https://www.nuget.org) packages, analyzers, and generators +- Automatic project packaging for standalone distribution +- Carefully handcrafted source code for best maintainability and performance +- Extensive unit testing to ensure the robustness and consistency +- Distributed as a plugin and doesn't require rebuilding the engine +- Documented source code + +The plugin is available for Windows, Linux, and macOS (x64). + +

+ megagrant +

+ +Building +-------- +### Prerequisites +- Unreal Engine 4.25.4 or higher +- A native [compilation toolchain](https://docs.unrealengine.com/en-US/Programming/Development/VisualStudioSetup/index.html#runtheunrealenginepre-requisiteinstaller) with platform-specific dependencies +- [.NET 6 SDK 6.0.101](https://dotnet.microsoft.com/download/dotnet/6.0) + +### Auto + +#### Compilation +Create a new or use an existing Unreal Engine C++ or blueprints project. Clone the repository or download a desirable version from the [releases](https://github.com/nxrighthere/UnrealCLR/releases) section. Navigate to `Install` folder, and run `dotnet run` command. Follow the installation instructions. Open the project after the installation process is complete. + +#### Upgrading +Make sure that the Unreal Engine is not running. Re-run the installation process. Recompile custom code with an updated framework. + +#### Command-line options +`--project-path ` Sets a path to an Unreal Engine project + +`--compile-tests ` Indicates whether tests should be compiled + +`--overwrite-files` Indicates whether all previous files of the plugin and content of tests should be overwritten + +### Manual + +#### Compilation + +##### Plugin +Create a new or use an existing Unreal Engine C++ or blueprints project. Clone the repository or download a desirable version from the [releases](https://github.com/nxrighthere/UnrealCLR/releases) section. Copy the content of the `Source/Native` folder to `%Project%/Plugins/UnrealCLR` directory. Compile the managed runtime from `Source/Managed/Runtime` folder by running the following command: `dotnet publish --configuration Release --framework net6.0 --output "%Project%/Plugins/UnrealCLR/Managed"`. Restart Unreal Engine, open the project, and build the plugin. + +##### Tests +To quickly start testing, open a project with the plugin in Unreal Engine, copy all folders from the `Content` of the repository to `%Project%/Content` directory, and wait until they loaded in the Content Browser. Compile the managed assemblies from `Source/Managed/Tests` folder by running the following commands: +``` +dotnet publish "../Framework" --configuration Release --framework net6.0 +dotnet publish --configuration Release --framework net6.0 --output "%Project%/Managed/Tests" +``` + +#### Upgrading +Make sure that the Unreal Engine is not running. Delete the plugin folder from a project, and repeat all steps from the compilation section. Recompile custom code with an updated framework. + +Running +-------- +### Plugin +The plugin is automatically loaded at startup. To make sure that it's initialized open the console window from `Window -> Developer Tools -> Output Log`, find `UnrealCLR` logs using the search input. + +### Tests +Open the scene with tests in the editor and enter the play mode. To switch a test, navigate to `Blueprints -> Open Level Blueprint`, select the `Test Systems` enumeration on the left panel, and change default value on the right panel. + +Overview +-------- +### Design and architecture +UnrealCLR is designed to be flexible and extensible. The plugin is transparently managing core functionality of the runtime, binding and caching the engine API for managed environment. The programmer has full control over execution flow through code and blueprints that allow to dynamically weave native events of the engine and its objects with managed logic. There's no hidden states or obscured order of execution behind the lifecycle of scripts. + +### Assemblies management +At runtime, UnrealCLR loading managed assemblies into a cached isolated context. It allows dynamically replace assemblies after unloading them from memory, therefore the programmer can work with code without restarting the editor for continuous development. The compilation pipeline is entirely up to a developer, it can be organized in any desirable way without any limitations and with full support of [NuGet](https://www.nuget.org) packages. + +### Engine application program interface +The plugin and framework are evolving all the time to utilize as much power of the Unreal Engine as possible. It's crucial to have a feature-rich API. The system is created with high-performance in mind but without trading safety. The vast majority of code is written and verified by hand to prevent any unexpected behaviors at runtime and to ensure stability. + +### Powerful tooling +Use your favorite IDE and [.NET tools](https://github.com/natemcmaster/dotnet-tools) that can be attached to the process of the engine just as to a regular .NET application for profiling and debugging. It's very convenient and works out of the box without any external effort. Analyze performance, monitor CPU usage and memory consumption, debug execution, take full control over the code. Explore new possibilities and extend your toolset in no time. + +Essentials +-------- +### Exceptions +The runtime redirects all unhandled exceptions to log files, the console window, and on-screen messages of the engine, however, it's highly recommended to use [try-catch](https://docs.microsoft.com/en-us/dotnet/standard/exceptions/how-to-use-the-try-catch-block-to-catch-exceptions) blocks in C# to override redirections with custom handlers. It's necessary to have an attached debugger to properly trace exceptions in the editor or standalone. Use [option types](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/options) in F# to avoid exceptions altogether. + +### Memory management +Unreal Engine, as well as .NET runtime, utilizes a garbage collector for memory management. The framework is designed with consistency in mind to prevent crashes and validate memory transparently for a programmer, no matter how objects were created and freed: with C++, C#, F#, or blueprints. + +### Hot reload +The plugin is independent of the compilation routine of user assemblies. It's loading assemblies from `%Project%/Managed` folder and resolving dependencies at runtime after entering/leaving the play mode. The framework of the plugin with the engine API is automatically recognized and loaded as a dependency. + +### Ecosystem compatibility +The framework replicates the class hierarchy of the engine with full interoperability support. Any external C++ code, blueprints, and plugins are compatible and extensible with UnrealCLR by design through the engine API. The plugin integrated into the engine's building pipeline and ready for distribution of self-contained applications. + +Acknowledgments +-------- +Thanks to [@natemcmaster](https://github.com/natemcmaster) for a great .NET library that simplifies dynamic loading of assemblies. + +Special thanks to [@Doraku](https://github.com/Doraku) for an amazing documentation generator and rapid improvement of it. + +Supporters +-------- +These wonderful people make open-source better: +

+ supporters +

+ +``` + +`Source/Managed/Framework/Codegen.cs`: + +```cs +/* + * Unreal Engine .NET 6 integration + * Copyright (c) 2021 Stanislav Denisov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System; +using System.IO; +using System.Collections.Generic; +using System.Numerics; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace UnrealEngine.Framework { + // Automatically generated + + internal static class Shared { + internal const int checksum = 0x2F0; + internal static Dictionary userFunctions = new(); + private const string dynamicTypesAssemblyName = "UnrealEngine.DynamicTypes"; + private static readonly ModuleBuilder moduleBuilder = AssemblyBuilder.DefineDynamicAssembly(new(dynamicTypesAssemblyName), AssemblyBuilderAccess.RunAndCollect).DefineDynamicModule(dynamicTypesAssemblyName); + private static readonly Type[] delegateCtorSignature = { typeof(object), typeof(IntPtr) }; + private static Dictionary delegatesCache = new(); + private static Dictionary delegateTypesCache = new(); + private const MethodAttributes ctorAttributes = MethodAttributes.RTSpecialName | MethodAttributes.HideBySig | MethodAttributes.Public; + private const MethodImplAttributes implAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; + private const MethodAttributes invokeAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual; + private const TypeAttributes delegateTypeAttributes = TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.AnsiClass | TypeAttributes.AutoClass; + + internal static unsafe Dictionary Load(IntPtr* events, IntPtr functions, Assembly pluginAssembly) { + int position = 0; + IntPtr* buffer = (IntPtr*)functions; + + unchecked { + int head = 0; + IntPtr* assertFunctions = (IntPtr*)buffer[position++]; + + Assert.outputMessage = (delegate* unmanaged[Cdecl])assertFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* commandLineFunctions = (IntPtr*)buffer[position++]; + + CommandLine.get = (delegate* unmanaged[Cdecl])commandLineFunctions[head++]; + CommandLine.set = (delegate* unmanaged[Cdecl])commandLineFunctions[head++]; + CommandLine.append = (delegate* unmanaged[Cdecl])commandLineFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* debugFunctions = (IntPtr*)buffer[position++]; + + Debug.log = (delegate* unmanaged[Cdecl])debugFunctions[head++]; + Debug.exception = (delegate* unmanaged[Cdecl])debugFunctions[head++]; + Debug.addOnScreenMessage = (delegate* unmanaged[Cdecl])debugFunctions[head++]; + Debug.clearOnScreenMessages = (delegate* unmanaged[Cdecl])debugFunctions[head++]; + Debug.drawBox = (delegate* unmanaged[Cdecl])debugFunctions[head++]; + Debug.drawCapsule = (delegate* unmanaged[Cdecl])debugFunctions[head++]; + Debug.drawCone = (delegate* unmanaged[Cdecl])debugFunctions[head++]; + Debug.drawCylinder = (delegate* unmanaged[Cdecl])debugFunctions[head++]; + Debug.drawSphere = (delegate* unmanaged[Cdecl])debugFunctions[head++]; + Debug.drawLine = (delegate* unmanaged[Cdecl])debugFunctions[head++]; + Debug.drawPoint = (delegate* unmanaged[Cdecl])debugFunctions[head++]; + Debug.flushPersistentLines = (delegate* unmanaged[Cdecl])debugFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* objectFunctions = (IntPtr*)buffer[position++]; + + Object.isPendingKill = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.isValid = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.load = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.rename = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.invoke = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.toActor = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.toComponent = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getID = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getName = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getBool = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getByte = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getShort = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getInt = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getLong = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getUShort = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getUInt = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getULong = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getFloat = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getDouble = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getEnum = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getString = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.getText = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.setBool = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.setByte = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.setShort = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.setInt = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.setLong = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.setUShort = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.setUInt = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.setULong = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.setFloat = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.setDouble = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.setEnum = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.setString = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + Object.setText = (delegate* unmanaged[Cdecl])objectFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* applicationFunctions = (IntPtr*)buffer[position++]; + + Application.isCanEverRender = (delegate* unmanaged[Cdecl])applicationFunctions[head++]; + Application.isPackagedForDistribution = (delegate* unmanaged[Cdecl])applicationFunctions[head++]; + Application.isPackagedForShipping = (delegate* unmanaged[Cdecl])applicationFunctions[head++]; + Application.getProjectDirectory = (delegate* unmanaged[Cdecl])applicationFunctions[head++]; + Application.getDefaultLanguage = (delegate* unmanaged[Cdecl])applicationFunctions[head++]; + Application.getProjectName = (delegate* unmanaged[Cdecl])applicationFunctions[head++]; + Application.getVolumeMultiplier = (delegate* unmanaged[Cdecl])applicationFunctions[head++]; + Application.setProjectName = (delegate* unmanaged[Cdecl])applicationFunctions[head++]; + Application.setVolumeMultiplier = (delegate* unmanaged[Cdecl])applicationFunctions[head++]; + Application.requestExit = (delegate* unmanaged[Cdecl])applicationFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* consoleManagerFunctions = (IntPtr*)buffer[position++]; + + ConsoleManager.isRegisteredVariable = (delegate* unmanaged[Cdecl])consoleManagerFunctions[head++]; + ConsoleManager.findVariable = (delegate* unmanaged[Cdecl])consoleManagerFunctions[head++]; + ConsoleManager.registerVariableBool = (delegate* unmanaged[Cdecl])consoleManagerFunctions[head++]; + ConsoleManager.registerVariableInt = (delegate* unmanaged[Cdecl])consoleManagerFunctions[head++]; + ConsoleManager.registerVariableFloat = (delegate* unmanaged[Cdecl])consoleManagerFunctions[head++]; + ConsoleManager.registerVariableString = (delegate* unmanaged[Cdecl])consoleManagerFunctions[head++]; + ConsoleManager.registerCommand = (delegate* unmanaged[Cdecl])consoleManagerFunctions[head++]; + ConsoleManager.unregisterObject = (delegate* unmanaged[Cdecl])consoleManagerFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* engineFunctions = (IntPtr*)buffer[position++]; + + Engine.isSplitScreen = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.isEditor = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.isForegroundWindow = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.isExitRequested = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.getNetMode = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.getFrameNumber = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.getViewportSize = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.getScreenResolution = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.getWindowMode = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.getVersion = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.getMaxFPS = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.setMaxFPS = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.setTitle = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.addActionMapping = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.addAxisMapping = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.forceGarbageCollection = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + Engine.delayGarbageCollection = (delegate* unmanaged[Cdecl])engineFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* headMountedDisplayFunctions = (IntPtr*)buffer[position++]; + + HeadMountedDisplay.isConnected = (delegate* unmanaged[Cdecl])headMountedDisplayFunctions[head++]; + HeadMountedDisplay.getEnabled = (delegate* unmanaged[Cdecl])headMountedDisplayFunctions[head++]; + HeadMountedDisplay.getLowPersistenceMode = (delegate* unmanaged[Cdecl])headMountedDisplayFunctions[head++]; + HeadMountedDisplay.getDeviceName = (delegate* unmanaged[Cdecl])headMountedDisplayFunctions[head++]; + HeadMountedDisplay.setEnable = (delegate* unmanaged[Cdecl])headMountedDisplayFunctions[head++]; + HeadMountedDisplay.setLowPersistenceMode = (delegate* unmanaged[Cdecl])headMountedDisplayFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* worldFunctions = (IntPtr*)buffer[position++]; + + World.forEachActor = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.getActorCount = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.getDeltaSeconds = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.getRealTimeSeconds = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.getTimeSeconds = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.getCurrentLevelName = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.getSimulatePhysics = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.getWorldOrigin = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.getActor = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.getActorByTag = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.getActorByID = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.getFirstPlayerController = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.getGameMode = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnActorBeginOverlapCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnActorBeginCursorOverCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnActorEndCursorOverCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnActorClickedCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnActorReleasedCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnActorEndOverlapCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnActorHitCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnComponentBeginOverlapCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnComponentEndOverlapCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnComponentHitCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnComponentBeginCursorOverCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnComponentEndCursorOverCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnComponentClickedCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setOnComponentReleasedCallback = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setSimulatePhysics = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setGravity = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.setWorldOrigin = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.openLevel = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.lineTraceTestByChannel = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.lineTraceTestByProfile = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.lineTraceSingleByChannel = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.lineTraceSingleByProfile = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.sweepTestByChannel = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.sweepTestByProfile = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.sweepSingleByChannel = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.sweepSingleByProfile = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.overlapAnyTestByChannel = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.overlapAnyTestByProfile = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.overlapBlockingTestByChannel = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + World.overlapBlockingTestByProfile = (delegate* unmanaged[Cdecl])worldFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* assetFunctions = (IntPtr*)buffer[position++]; + + Asset.isValid = (delegate* unmanaged[Cdecl])assetFunctions[head++]; + Asset.getName = (delegate* unmanaged[Cdecl])assetFunctions[head++]; + Asset.getPath = (delegate* unmanaged[Cdecl])assetFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* assetRegistryFunctions = (IntPtr*)buffer[position++]; + + AssetRegistry.get = (delegate* unmanaged[Cdecl])assetRegistryFunctions[head++]; + AssetRegistry.hasAssets = (delegate* unmanaged[Cdecl])assetRegistryFunctions[head++]; + AssetRegistry.forEachAsset = (delegate* unmanaged[Cdecl])assetRegistryFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* blueprintFunctions = (IntPtr*)buffer[position++]; + + Blueprint.isValidActorClass = (delegate* unmanaged[Cdecl])blueprintFunctions[head++]; + Blueprint.isValidComponentClass = (delegate* unmanaged[Cdecl])blueprintFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* consoleObjectFunctions = (IntPtr*)buffer[position++]; + + ConsoleObject.isBool = (delegate* unmanaged[Cdecl])consoleObjectFunctions[head++]; + ConsoleObject.isInt = (delegate* unmanaged[Cdecl])consoleObjectFunctions[head++]; + ConsoleObject.isFloat = (delegate* unmanaged[Cdecl])consoleObjectFunctions[head++]; + ConsoleObject.isString = (delegate* unmanaged[Cdecl])consoleObjectFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* consoleVariableFunctions = (IntPtr*)buffer[position++]; + + ConsoleVariable.getBool = (delegate* unmanaged[Cdecl])consoleVariableFunctions[head++]; + ConsoleVariable.getInt = (delegate* unmanaged[Cdecl])consoleVariableFunctions[head++]; + ConsoleVariable.getFloat = (delegate* unmanaged[Cdecl])consoleVariableFunctions[head++]; + ConsoleVariable.getString = (delegate* unmanaged[Cdecl])consoleVariableFunctions[head++]; + ConsoleVariable.setBool = (delegate* unmanaged[Cdecl])consoleVariableFunctions[head++]; + ConsoleVariable.setInt = (delegate* unmanaged[Cdecl])consoleVariableFunctions[head++]; + ConsoleVariable.setFloat = (delegate* unmanaged[Cdecl])consoleVariableFunctions[head++]; + ConsoleVariable.setString = (delegate* unmanaged[Cdecl])consoleVariableFunctions[head++]; + ConsoleVariable.setOnChangedCallback = (delegate* unmanaged[Cdecl])consoleVariableFunctions[head++]; + ConsoleVariable.clearOnChangedCallback = (delegate* unmanaged[Cdecl])consoleVariableFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* actorFunctions = (IntPtr*)buffer[position++]; + + Actor.isPendingKill = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.isRootComponentMovable = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.isOverlappingActor = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.forEachComponent = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.forEachAttachedActor = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.forEachChildActor = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.forEachOverlappingActor = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.spawn = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.destroy = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.rename = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.hide = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.teleportTo = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.getComponent = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.getComponentByTag = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.getComponentByID = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.getRootComponent = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.getInputComponent = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.getCreationTime = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.getBlockInput = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.getDistanceTo = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.getHorizontalDistanceTo = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.getBounds = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.getEyesViewPoint = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.setRootComponent = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.setInputComponent = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.setBlockInput = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.setLifeSpan = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.setEnableInput = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.setEnableCollision = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.addTag = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.removeTag = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.hasTag = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.registerEvent = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + Actor.unregisterEvent = (delegate* unmanaged[Cdecl])actorFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* gameModeBaseFunctions = (IntPtr*)buffer[position++]; + + GameModeBase.getUseSeamlessTravel = (delegate* unmanaged[Cdecl])gameModeBaseFunctions[head++]; + GameModeBase.setUseSeamlessTravel = (delegate* unmanaged[Cdecl])gameModeBaseFunctions[head++]; + GameModeBase.swapPlayerControllers = (delegate* unmanaged[Cdecl])gameModeBaseFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* pawnFunctions = (IntPtr*)buffer[position++]; + + Pawn.isControlled = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.isPlayerControlled = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.getAutoPossessAI = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.getAutoPossessPlayer = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.getUseControllerRotationYaw = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.getUseControllerRotationPitch = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.getUseControllerRotationRoll = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.getGravityDirection = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.getAIController = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.getPlayerController = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.setAutoPossessAI = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.setAutoPossessPlayer = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.setUseControllerRotationYaw = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.setUseControllerRotationPitch = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.setUseControllerRotationRoll = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.addControllerYawInput = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.addControllerPitchInput = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.addControllerRollInput = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + Pawn.addMovementInput = (delegate* unmanaged[Cdecl])pawnFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* characterFunctions = (IntPtr*)buffer[position++]; + + Character.isCrouched = (delegate* unmanaged[Cdecl])characterFunctions[head++]; + Character.canCrouch = (delegate* unmanaged[Cdecl])characterFunctions[head++]; + Character.canJump = (delegate* unmanaged[Cdecl])characterFunctions[head++]; + Character.checkJumpInput = (delegate* unmanaged[Cdecl])characterFunctions[head++]; + Character.clearJumpInput = (delegate* unmanaged[Cdecl])characterFunctions[head++]; + Character.launch = (delegate* unmanaged[Cdecl])characterFunctions[head++]; + Character.crouch = (delegate* unmanaged[Cdecl])characterFunctions[head++]; + Character.stopCrouching = (delegate* unmanaged[Cdecl])characterFunctions[head++]; + Character.jump = (delegate* unmanaged[Cdecl])characterFunctions[head++]; + Character.stopJumping = (delegate* unmanaged[Cdecl])characterFunctions[head++]; + Character.setOnLandedCallback = (delegate* unmanaged[Cdecl])characterFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* controllerFunctions = (IntPtr*)buffer[position++]; + + Controller.isLookInputIgnored = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.isMoveInputIgnored = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.isPlayerController = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.getPawn = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.getCharacter = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.getViewTarget = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.getControlRotation = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.getDesiredRotation = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.lineOfSightTo = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.setControlRotation = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.setInitialLocationAndRotation = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.setIgnoreLookInput = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.setIgnoreMoveInput = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.resetIgnoreLookInput = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.resetIgnoreMoveInput = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.possess = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + Controller.unpossess = (delegate* unmanaged[Cdecl])controllerFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* aIControllerFunctions = (IntPtr*)buffer[position++]; + + AIController.clearFocus = (delegate* unmanaged[Cdecl])aIControllerFunctions[head++]; + AIController.getFocalPoint = (delegate* unmanaged[Cdecl])aIControllerFunctions[head++]; + AIController.setFocalPoint = (delegate* unmanaged[Cdecl])aIControllerFunctions[head++]; + AIController.getFocusActor = (delegate* unmanaged[Cdecl])aIControllerFunctions[head++]; + AIController.getAllowStrafe = (delegate* unmanaged[Cdecl])aIControllerFunctions[head++]; + AIController.setAllowStrafe = (delegate* unmanaged[Cdecl])aIControllerFunctions[head++]; + AIController.setFocus = (delegate* unmanaged[Cdecl])aIControllerFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* playerControllerFunctions = (IntPtr*)buffer[position++]; + + PlayerController.isPaused = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.getShowMouseCursor = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.getEnableClickEvents = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.getEnableMouseOverEvents = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.getMousePosition = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.getPlayer = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.getPlayerInput = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.getHitResultAtScreenPosition = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.getHitResultUnderCursor = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.setShowMouseCursor = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.setEnableClickEvents = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.setEnableMouseOverEvents = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.setMousePosition = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.consoleCommand = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.setPause = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.setViewTarget = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.setViewTargetWithBlend = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.addYawInput = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.addPitchInput = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + PlayerController.addRollInput = (delegate* unmanaged[Cdecl])playerControllerFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* volumeFunctions = (IntPtr*)buffer[position++]; + + Volume.encompassesPoint = (delegate* unmanaged[Cdecl])volumeFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* postProcessVolumeFunctions = (IntPtr*)buffer[position++]; + + PostProcessVolume.getEnabled = (delegate* unmanaged[Cdecl])postProcessVolumeFunctions[head++]; + PostProcessVolume.getBlendRadius = (delegate* unmanaged[Cdecl])postProcessVolumeFunctions[head++]; + PostProcessVolume.getBlendWeight = (delegate* unmanaged[Cdecl])postProcessVolumeFunctions[head++]; + PostProcessVolume.getUnbound = (delegate* unmanaged[Cdecl])postProcessVolumeFunctions[head++]; + PostProcessVolume.getPriority = (delegate* unmanaged[Cdecl])postProcessVolumeFunctions[head++]; + PostProcessVolume.setEnabled = (delegate* unmanaged[Cdecl])postProcessVolumeFunctions[head++]; + PostProcessVolume.setBlendRadius = (delegate* unmanaged[Cdecl])postProcessVolumeFunctions[head++]; + PostProcessVolume.setBlendWeight = (delegate* unmanaged[Cdecl])postProcessVolumeFunctions[head++]; + PostProcessVolume.setUnbound = (delegate* unmanaged[Cdecl])postProcessVolumeFunctions[head++]; + PostProcessVolume.setPriority = (delegate* unmanaged[Cdecl])postProcessVolumeFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* soundBaseFunctions = (IntPtr*)buffer[position++]; + + SoundBase.getDuration = (delegate* unmanaged[Cdecl])soundBaseFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* soundWaveFunctions = (IntPtr*)buffer[position++]; + + SoundWave.getLoop = (delegate* unmanaged[Cdecl])soundWaveFunctions[head++]; + SoundWave.setLoop = (delegate* unmanaged[Cdecl])soundWaveFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* animationInstanceFunctions = (IntPtr*)buffer[position++]; + + AnimationInstance.getCurrentActiveMontage = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.isPlaying = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.getPlayRate = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.getPosition = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.getBlendTime = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.getCurrentSection = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.setPlayRate = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.setPosition = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.setNextSection = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.playMontage = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.pauseMontage = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.resumeMontage = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.stopMontage = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.jumpToSection = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + AnimationInstance.jumpToSectionsEnd = (delegate* unmanaged[Cdecl])animationInstanceFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* playerFunctions = (IntPtr*)buffer[position++]; + + Player.getPlayerController = (delegate* unmanaged[Cdecl])playerFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* playerInputFunctions = (IntPtr*)buffer[position++]; + + PlayerInput.isKeyPressed = (delegate* unmanaged[Cdecl])playerInputFunctions[head++]; + PlayerInput.getTimeKeyPressed = (delegate* unmanaged[Cdecl])playerInputFunctions[head++]; + PlayerInput.getMouseSensitivity = (delegate* unmanaged[Cdecl])playerInputFunctions[head++]; + PlayerInput.setMouseSensitivity = (delegate* unmanaged[Cdecl])playerInputFunctions[head++]; + PlayerInput.addActionMapping = (delegate* unmanaged[Cdecl])playerInputFunctions[head++]; + PlayerInput.addAxisMapping = (delegate* unmanaged[Cdecl])playerInputFunctions[head++]; + PlayerInput.removeActionMapping = (delegate* unmanaged[Cdecl])playerInputFunctions[head++]; + PlayerInput.removeAxisMapping = (delegate* unmanaged[Cdecl])playerInputFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* fontFunctions = (IntPtr*)buffer[position++]; + + Font.getStringSize = (delegate* unmanaged[Cdecl])fontFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* texture2DFunctions = (IntPtr*)buffer[position++]; + + Texture2D.createFromFile = (delegate* unmanaged[Cdecl])texture2DFunctions[head++]; + Texture2D.createFromBuffer = (delegate* unmanaged[Cdecl])texture2DFunctions[head++]; + Texture2D.hasAlphaChannel = (delegate* unmanaged[Cdecl])texture2DFunctions[head++]; + Texture2D.getSize = (delegate* unmanaged[Cdecl])texture2DFunctions[head++]; + Texture2D.getPixelFormat = (delegate* unmanaged[Cdecl])texture2DFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* actorComponentFunctions = (IntPtr*)buffer[position++]; + + ActorComponent.isOwnerSelected = (delegate* unmanaged[Cdecl])actorComponentFunctions[head++]; + ActorComponent.getOwner = (delegate* unmanaged[Cdecl])actorComponentFunctions[head++]; + ActorComponent.destroy = (delegate* unmanaged[Cdecl])actorComponentFunctions[head++]; + ActorComponent.addTag = (delegate* unmanaged[Cdecl])actorComponentFunctions[head++]; + ActorComponent.removeTag = (delegate* unmanaged[Cdecl])actorComponentFunctions[head++]; + ActorComponent.hasTag = (delegate* unmanaged[Cdecl])actorComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* inputComponentFunctions = (IntPtr*)buffer[position++]; + + InputComponent.hasBindings = (delegate* unmanaged[Cdecl])inputComponentFunctions[head++]; + InputComponent.getActionBindingsNumber = (delegate* unmanaged[Cdecl])inputComponentFunctions[head++]; + InputComponent.clearActionBindings = (delegate* unmanaged[Cdecl])inputComponentFunctions[head++]; + InputComponent.bindAction = (delegate* unmanaged[Cdecl])inputComponentFunctions[head++]; + InputComponent.bindAxis = (delegate* unmanaged[Cdecl])inputComponentFunctions[head++]; + InputComponent.removeActionBinding = (delegate* unmanaged[Cdecl])inputComponentFunctions[head++]; + InputComponent.getBlockInput = (delegate* unmanaged[Cdecl])inputComponentFunctions[head++]; + InputComponent.setBlockInput = (delegate* unmanaged[Cdecl])inputComponentFunctions[head++]; + InputComponent.getPriority = (delegate* unmanaged[Cdecl])inputComponentFunctions[head++]; + InputComponent.setPriority = (delegate* unmanaged[Cdecl])inputComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* movementComponentFunctions = (IntPtr*)buffer[position++]; + + MovementComponent.getConstrainToPlane = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.getSnapToPlaneAtStart = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.getVelocity = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.getPlaneConstraint = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.getUpdateOnlyIfRendered = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.getPlaneConstraintNormal = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.getPlaneConstraintOrigin = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.getGravity = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.getMaxSpeed = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.setConstrainToPlane = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.setSnapToPlaneAtStart = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.setUpdateOnlyIfRendered = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.setVelocity = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.setPlaneConstraint = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.setPlaneConstraintNormal = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.setPlaneConstraintOrigin = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.setPlaneConstraintFromVectors = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.isExceedingMaxSpeed = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.isInWater = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.stopMovement = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.constrainDirectionToPlane = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.constrainLocationToPlane = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + MovementComponent.constrainNormalToPlane = (delegate* unmanaged[Cdecl])movementComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* rotatingMovementComponentFunctions = (IntPtr*)buffer[position++]; + + RotatingMovementComponent.create = (delegate* unmanaged[Cdecl])rotatingMovementComponentFunctions[head++]; + RotatingMovementComponent.getRotationInLocalSpace = (delegate* unmanaged[Cdecl])rotatingMovementComponentFunctions[head++]; + RotatingMovementComponent.getPivotTranslation = (delegate* unmanaged[Cdecl])rotatingMovementComponentFunctions[head++]; + RotatingMovementComponent.getRotationRate = (delegate* unmanaged[Cdecl])rotatingMovementComponentFunctions[head++]; + RotatingMovementComponent.setRotationInLocalSpace = (delegate* unmanaged[Cdecl])rotatingMovementComponentFunctions[head++]; + RotatingMovementComponent.setPivotTranslation = (delegate* unmanaged[Cdecl])rotatingMovementComponentFunctions[head++]; + RotatingMovementComponent.setRotationRate = (delegate* unmanaged[Cdecl])rotatingMovementComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* sceneComponentFunctions = (IntPtr*)buffer[position++]; + + SceneComponent.isAttachedToComponent = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.isAttachedToActor = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.isVisible = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.isSocketExists = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.hasAnySockets = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.canAttachAsChild = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.forEachAttachedChild = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.create = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.attachToComponent = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.detachFromComponent = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.activate = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.deactivate = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.updateToWorld = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.addLocalOffset = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.addLocalRotation = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.addRelativeLocation = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.addRelativeRotation = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.addLocalTransform = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.addWorldOffset = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.addWorldRotation = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.addWorldTransform = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.getAttachedSocketName = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.getBounds = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.getSocketLocation = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.getSocketRotation = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.getComponentVelocity = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.getComponentLocation = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.getComponentRotation = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.getComponentScale = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.getComponentTransform = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.getForwardVector = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.getRightVector = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.getUpVector = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.setMobility = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.setVisibility = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.setRelativeLocation = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.setRelativeRotation = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.setRelativeTransform = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.setWorldLocation = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.setWorldRotation = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.setWorldScale = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + SceneComponent.setWorldTransform = (delegate* unmanaged[Cdecl])sceneComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* audioComponentFunctions = (IntPtr*)buffer[position++]; + + AudioComponent.isPlaying = (delegate* unmanaged[Cdecl])audioComponentFunctions[head++]; + AudioComponent.getPaused = (delegate* unmanaged[Cdecl])audioComponentFunctions[head++]; + AudioComponent.setSound = (delegate* unmanaged[Cdecl])audioComponentFunctions[head++]; + AudioComponent.setPaused = (delegate* unmanaged[Cdecl])audioComponentFunctions[head++]; + AudioComponent.play = (delegate* unmanaged[Cdecl])audioComponentFunctions[head++]; + AudioComponent.stop = (delegate* unmanaged[Cdecl])audioComponentFunctions[head++]; + AudioComponent.fadeIn = (delegate* unmanaged[Cdecl])audioComponentFunctions[head++]; + AudioComponent.fadeOut = (delegate* unmanaged[Cdecl])audioComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* cameraComponentFunctions = (IntPtr*)buffer[position++]; + + CameraComponent.getConstrainAspectRatio = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.getAspectRatio = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.getFieldOfView = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.getOrthoFarClipPlane = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.getOrthoNearClipPlane = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.getOrthoWidth = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.getLockToHeadMountedDisplay = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.setProjectionMode = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.setConstrainAspectRatio = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.setAspectRatio = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.setFieldOfView = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.setOrthoFarClipPlane = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.setOrthoNearClipPlane = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.setOrthoWidth = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + CameraComponent.setLockToHeadMountedDisplay = (delegate* unmanaged[Cdecl])cameraComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* childActorComponentFunctions = (IntPtr*)buffer[position++]; + + ChildActorComponent.getChildActor = (delegate* unmanaged[Cdecl])childActorComponentFunctions[head++]; + ChildActorComponent.setChildActor = (delegate* unmanaged[Cdecl])childActorComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* springArmComponentFunctions = (IntPtr*)buffer[position++]; + + SpringArmComponent.isCollisionFixApplied = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getDrawDebugLagMarkers = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getCollisionTest = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getCameraPositionLag = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getCameraRotationLag = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getCameraLagSubstepping = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getInheritPitch = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getInheritRoll = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getInheritYaw = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getCameraLagMaxDistance = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getCameraLagMaxTimeStep = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getCameraPositionLagSpeed = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getCameraRotationLagSpeed = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getProbeChannel = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getProbeSize = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getSocketOffset = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getTargetArmLength = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getTargetOffset = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getUnfixedCameraPosition = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getDesiredRotation = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getTargetRotation = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.getUsePawnControlRotation = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setDrawDebugLagMarkers = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setCollisionTest = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setCameraPositionLag = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setCameraRotationLag = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setCameraLagSubstepping = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setInheritPitch = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setInheritRoll = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setInheritYaw = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setCameraLagMaxDistance = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setCameraLagMaxTimeStep = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setCameraPositionLagSpeed = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setCameraRotationLagSpeed = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setProbeChannel = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setProbeSize = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setSocketOffset = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setTargetArmLength = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setTargetOffset = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + SpringArmComponent.setUsePawnControlRotation = (delegate* unmanaged[Cdecl])springArmComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* postProcessComponentFunctions = (IntPtr*)buffer[position++]; + + PostProcessComponent.getEnabled = (delegate* unmanaged[Cdecl])postProcessComponentFunctions[head++]; + PostProcessComponent.getBlendRadius = (delegate* unmanaged[Cdecl])postProcessComponentFunctions[head++]; + PostProcessComponent.getBlendWeight = (delegate* unmanaged[Cdecl])postProcessComponentFunctions[head++]; + PostProcessComponent.getUnbound = (delegate* unmanaged[Cdecl])postProcessComponentFunctions[head++]; + PostProcessComponent.getPriority = (delegate* unmanaged[Cdecl])postProcessComponentFunctions[head++]; + PostProcessComponent.setEnabled = (delegate* unmanaged[Cdecl])postProcessComponentFunctions[head++]; + PostProcessComponent.setBlendRadius = (delegate* unmanaged[Cdecl])postProcessComponentFunctions[head++]; + PostProcessComponent.setBlendWeight = (delegate* unmanaged[Cdecl])postProcessComponentFunctions[head++]; + PostProcessComponent.setUnbound = (delegate* unmanaged[Cdecl])postProcessComponentFunctions[head++]; + PostProcessComponent.setPriority = (delegate* unmanaged[Cdecl])postProcessComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* primitiveComponentFunctions = (IntPtr*)buffer[position++]; + + PrimitiveComponent.isGravityEnabled = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.isOverlappingComponent = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.forEachOverlappingComponent = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.addAngularImpulseInDegrees = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.addAngularImpulseInRadians = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.addForce = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.addForceAtLocation = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.addImpulse = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.addImpulseAtLocation = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.addRadialForce = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.addRadialImpulse = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.addTorqueInDegrees = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.addTorqueInRadians = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getMass = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getPhysicsLinearVelocity = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getPhysicsLinearVelocityAtPoint = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getPhysicsAngularVelocityInDegrees = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getPhysicsAngularVelocityInRadians = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getCastShadow = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getOnlyOwnerSee = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getOwnerNoSee = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getIgnoreRadialForce = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getIgnoreRadialImpulse = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getMaterial = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getMaterialsNumber = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getDistanceToCollision = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getSquaredDistanceToCollision = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getAngularDamping = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.getLinearDamping = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setGenerateOverlapEvents = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setGenerateHitEvents = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setMass = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setCenterOfMass = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setPhysicsLinearVelocity = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setPhysicsAngularVelocityInDegrees = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setPhysicsAngularVelocityInRadians = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setPhysicsMaxAngularVelocityInDegrees = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setPhysicsMaxAngularVelocityInRadians = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setCastShadow = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setOnlyOwnerSee = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setOwnerNoSee = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setIgnoreRadialForce = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setIgnoreRadialImpulse = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setMaterial = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setSimulatePhysics = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setAngularDamping = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setLinearDamping = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setEnableGravity = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setCollisionMode = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setCollisionChannel = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setCollisionProfileName = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setCollisionResponseToChannel = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setCollisionResponseToAllChannels = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setIgnoreActorWhenMoving = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.setIgnoreComponentWhenMoving = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.clearMoveIgnoreActors = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.clearMoveIgnoreComponents = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.createAndSetMaterialInstanceDynamic = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.registerEvent = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + PrimitiveComponent.unregisterEvent = (delegate* unmanaged[Cdecl])primitiveComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* shapeComponentFunctions = (IntPtr*)buffer[position++]; + + ShapeComponent.getDynamicObstacle = (delegate* unmanaged[Cdecl])shapeComponentFunctions[head++]; + ShapeComponent.getShapeColor = (delegate* unmanaged[Cdecl])shapeComponentFunctions[head++]; + ShapeComponent.setDynamicObstacle = (delegate* unmanaged[Cdecl])shapeComponentFunctions[head++]; + ShapeComponent.setShapeColor = (delegate* unmanaged[Cdecl])shapeComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* boxComponentFunctions = (IntPtr*)buffer[position++]; + + BoxComponent.getScaledBoxExtent = (delegate* unmanaged[Cdecl])boxComponentFunctions[head++]; + BoxComponent.getUnscaledBoxExtent = (delegate* unmanaged[Cdecl])boxComponentFunctions[head++]; + BoxComponent.setBoxExtent = (delegate* unmanaged[Cdecl])boxComponentFunctions[head++]; + BoxComponent.initBoxExtent = (delegate* unmanaged[Cdecl])boxComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* sphereComponentFunctions = (IntPtr*)buffer[position++]; + + SphereComponent.getScaledSphereRadius = (delegate* unmanaged[Cdecl])sphereComponentFunctions[head++]; + SphereComponent.getUnscaledSphereRadius = (delegate* unmanaged[Cdecl])sphereComponentFunctions[head++]; + SphereComponent.getShapeScale = (delegate* unmanaged[Cdecl])sphereComponentFunctions[head++]; + SphereComponent.setSphereRadius = (delegate* unmanaged[Cdecl])sphereComponentFunctions[head++]; + SphereComponent.initSphereRadius = (delegate* unmanaged[Cdecl])sphereComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* capsuleComponentFunctions = (IntPtr*)buffer[position++]; + + CapsuleComponent.getScaledCapsuleRadius = (delegate* unmanaged[Cdecl])capsuleComponentFunctions[head++]; + CapsuleComponent.getUnscaledCapsuleRadius = (delegate* unmanaged[Cdecl])capsuleComponentFunctions[head++]; + CapsuleComponent.getShapeScale = (delegate* unmanaged[Cdecl])capsuleComponentFunctions[head++]; + CapsuleComponent.getScaledCapsuleSize = (delegate* unmanaged[Cdecl])capsuleComponentFunctions[head++]; + CapsuleComponent.getUnscaledCapsuleSize = (delegate* unmanaged[Cdecl])capsuleComponentFunctions[head++]; + CapsuleComponent.setCapsuleRadius = (delegate* unmanaged[Cdecl])capsuleComponentFunctions[head++]; + CapsuleComponent.setCapsuleSize = (delegate* unmanaged[Cdecl])capsuleComponentFunctions[head++]; + CapsuleComponent.initCapsuleSize = (delegate* unmanaged[Cdecl])capsuleComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* meshComponentFunctions = (IntPtr*)buffer[position++]; + + MeshComponent.isValidMaterialSlotName = (delegate* unmanaged[Cdecl])meshComponentFunctions[head++]; + MeshComponent.getMaterialIndex = (delegate* unmanaged[Cdecl])meshComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* textRenderComponentFunctions = (IntPtr*)buffer[position++]; + + TextRenderComponent.setFont = (delegate* unmanaged[Cdecl])textRenderComponentFunctions[head++]; + TextRenderComponent.setText = (delegate* unmanaged[Cdecl])textRenderComponentFunctions[head++]; + TextRenderComponent.setTextMaterial = (delegate* unmanaged[Cdecl])textRenderComponentFunctions[head++]; + TextRenderComponent.setTextRenderColor = (delegate* unmanaged[Cdecl])textRenderComponentFunctions[head++]; + TextRenderComponent.setHorizontalAlignment = (delegate* unmanaged[Cdecl])textRenderComponentFunctions[head++]; + TextRenderComponent.setHorizontalSpacingAdjustment = (delegate* unmanaged[Cdecl])textRenderComponentFunctions[head++]; + TextRenderComponent.setVerticalAlignment = (delegate* unmanaged[Cdecl])textRenderComponentFunctions[head++]; + TextRenderComponent.setVerticalSpacingAdjustment = (delegate* unmanaged[Cdecl])textRenderComponentFunctions[head++]; + TextRenderComponent.setScale = (delegate* unmanaged[Cdecl])textRenderComponentFunctions[head++]; + TextRenderComponent.setWorldSize = (delegate* unmanaged[Cdecl])textRenderComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* lightComponentBaseFunctions = (IntPtr*)buffer[position++]; + + LightComponentBase.getIntensity = (delegate* unmanaged[Cdecl])lightComponentBaseFunctions[head++]; + LightComponentBase.getCastShadows = (delegate* unmanaged[Cdecl])lightComponentBaseFunctions[head++]; + LightComponentBase.setCastShadows = (delegate* unmanaged[Cdecl])lightComponentBaseFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* lightComponentFunctions = (IntPtr*)buffer[position++]; + + LightComponent.setIntensity = (delegate* unmanaged[Cdecl])lightComponentFunctions[head++]; + LightComponent.setLightColor = (delegate* unmanaged[Cdecl])lightComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* motionControllerComponentFunctions = (IntPtr*)buffer[position++]; + + MotionControllerComponent.isTracked = (delegate* unmanaged[Cdecl])motionControllerComponentFunctions[head++]; + MotionControllerComponent.getDisplayDeviceModel = (delegate* unmanaged[Cdecl])motionControllerComponentFunctions[head++]; + MotionControllerComponent.getDisableLowLatencyUpdate = (delegate* unmanaged[Cdecl])motionControllerComponentFunctions[head++]; + MotionControllerComponent.getTrackingSource = (delegate* unmanaged[Cdecl])motionControllerComponentFunctions[head++]; + MotionControllerComponent.setDisplayDeviceModel = (delegate* unmanaged[Cdecl])motionControllerComponentFunctions[head++]; + MotionControllerComponent.setDisableLowLatencyUpdate = (delegate* unmanaged[Cdecl])motionControllerComponentFunctions[head++]; + MotionControllerComponent.setTrackingSource = (delegate* unmanaged[Cdecl])motionControllerComponentFunctions[head++]; + MotionControllerComponent.setTrackingMotionSource = (delegate* unmanaged[Cdecl])motionControllerComponentFunctions[head++]; + MotionControllerComponent.setAssociatedPlayerIndex = (delegate* unmanaged[Cdecl])motionControllerComponentFunctions[head++]; + MotionControllerComponent.setCustomDisplayMesh = (delegate* unmanaged[Cdecl])motionControllerComponentFunctions[head++]; + MotionControllerComponent.setDisplayModelSource = (delegate* unmanaged[Cdecl])motionControllerComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* staticMeshComponentFunctions = (IntPtr*)buffer[position++]; + + StaticMeshComponent.getLocalBounds = (delegate* unmanaged[Cdecl])staticMeshComponentFunctions[head++]; + StaticMeshComponent.getStaticMesh = (delegate* unmanaged[Cdecl])staticMeshComponentFunctions[head++]; + StaticMeshComponent.setStaticMesh = (delegate* unmanaged[Cdecl])staticMeshComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* instancedStaticMeshComponentFunctions = (IntPtr*)buffer[position++]; + + InstancedStaticMeshComponent.getInstanceCount = (delegate* unmanaged[Cdecl])instancedStaticMeshComponentFunctions[head++]; + InstancedStaticMeshComponent.getInstanceTransform = (delegate* unmanaged[Cdecl])instancedStaticMeshComponentFunctions[head++]; + InstancedStaticMeshComponent.addInstance = (delegate* unmanaged[Cdecl])instancedStaticMeshComponentFunctions[head++]; + InstancedStaticMeshComponent.addInstances = (delegate* unmanaged[Cdecl])instancedStaticMeshComponentFunctions[head++]; + InstancedStaticMeshComponent.updateInstanceTransform = (delegate* unmanaged[Cdecl])instancedStaticMeshComponentFunctions[head++]; + InstancedStaticMeshComponent.batchUpdateInstanceTransforms = (delegate* unmanaged[Cdecl])instancedStaticMeshComponentFunctions[head++]; + InstancedStaticMeshComponent.removeInstance = (delegate* unmanaged[Cdecl])instancedStaticMeshComponentFunctions[head++]; + InstancedStaticMeshComponent.clearInstances = (delegate* unmanaged[Cdecl])instancedStaticMeshComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* hierarchicalInstancedStaticMeshComponentFunctions = (IntPtr*)buffer[position++]; + + HierarchicalInstancedStaticMeshComponent.getDisableCollision = (delegate* unmanaged[Cdecl])hierarchicalInstancedStaticMeshComponentFunctions[head++]; + HierarchicalInstancedStaticMeshComponent.setDisableCollision = (delegate* unmanaged[Cdecl])hierarchicalInstancedStaticMeshComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* skinnedMeshComponentFunctions = (IntPtr*)buffer[position++]; + + SkinnedMeshComponent.getBonesNumber = (delegate* unmanaged[Cdecl])skinnedMeshComponentFunctions[head++]; + SkinnedMeshComponent.getBoneIndex = (delegate* unmanaged[Cdecl])skinnedMeshComponentFunctions[head++]; + SkinnedMeshComponent.getBoneName = (delegate* unmanaged[Cdecl])skinnedMeshComponentFunctions[head++]; + SkinnedMeshComponent.getBoneTransform = (delegate* unmanaged[Cdecl])skinnedMeshComponentFunctions[head++]; + SkinnedMeshComponent.setSkeletalMesh = (delegate* unmanaged[Cdecl])skinnedMeshComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* skeletalMeshComponentFunctions = (IntPtr*)buffer[position++]; + + SkeletalMeshComponent.isPlaying = (delegate* unmanaged[Cdecl])skeletalMeshComponentFunctions[head++]; + SkeletalMeshComponent.getAnimationInstance = (delegate* unmanaged[Cdecl])skeletalMeshComponentFunctions[head++]; + SkeletalMeshComponent.setAnimation = (delegate* unmanaged[Cdecl])skeletalMeshComponentFunctions[head++]; + SkeletalMeshComponent.setAnimationMode = (delegate* unmanaged[Cdecl])skeletalMeshComponentFunctions[head++]; + SkeletalMeshComponent.setAnimationBlueprint = (delegate* unmanaged[Cdecl])skeletalMeshComponentFunctions[head++]; + SkeletalMeshComponent.play = (delegate* unmanaged[Cdecl])skeletalMeshComponentFunctions[head++]; + SkeletalMeshComponent.playAnimation = (delegate* unmanaged[Cdecl])skeletalMeshComponentFunctions[head++]; + SkeletalMeshComponent.stop = (delegate* unmanaged[Cdecl])skeletalMeshComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* splineComponentFunctions = (IntPtr*)buffer[position++]; + + SplineComponent.isClosedLoop = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getDuration = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getSplinePointType = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getSplinePointsNumber = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getSplineSegmentsNumber = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getTangentAtDistanceAlongSpline = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getTangentAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getTangentAtTime = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getTransformAtDistanceAlongSpline = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getTransformAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getArriveTangentAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getDefaultUpVector = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getDirectionAtDistanceAlongSpline = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getDirectionAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getDirectionAtTime = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getDistanceAlongSplineAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getLeaveTangentAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getLocationAndTangentAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getLocationAtDistanceAlongSpline = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getLocationAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getLocationAtTime = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getRightVectorAtDistanceAlongSpline = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getRightVectorAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getRightVectorAtTime = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getRollAtDistanceAlongSpline = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getRollAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getRollAtTime = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getRotationAtDistanceAlongSpline = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getRotationAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getRotationAtTime = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getScaleAtDistanceAlongSpline = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getScaleAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getScaleAtTime = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getSplineLength = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getTransformAtTime = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getUpVectorAtDistanceAlongSpline = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getUpVectorAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.getUpVectorAtTime = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.setDuration = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.setSplinePointType = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.setClosedLoop = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.setDefaultUpVector = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.setLocationAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.setTangentAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.setTangentsAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.setUpVectorAtSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.addSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.addSplinePointAtIndex = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.clearSplinePoints = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.findDirectionClosestToWorldLocation = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.findLocationClosestToWorldLocation = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.findUpVectorClosestToWorldLocation = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.findRightVectorClosestToWorldLocation = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.findRollClosestToWorldLocation = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.findScaleClosestToWorldLocation = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.findTangentClosestToWorldLocation = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.findTransformClosestToWorldLocation = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.removeSplinePoint = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + SplineComponent.updateSpline = (delegate* unmanaged[Cdecl])splineComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* radialForceComponentFunctions = (IntPtr*)buffer[position++]; + + RadialForceComponent.getIgnoreOwningActor = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + RadialForceComponent.getImpulseVelocityChange = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + RadialForceComponent.getLinearFalloff = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + RadialForceComponent.getForceStrength = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + RadialForceComponent.getImpulseStrength = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + RadialForceComponent.getRadius = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + RadialForceComponent.setIgnoreOwningActor = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + RadialForceComponent.setImpulseVelocityChange = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + RadialForceComponent.setLinearFalloff = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + RadialForceComponent.setForceStrength = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + RadialForceComponent.setImpulseStrength = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + RadialForceComponent.setRadius = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + RadialForceComponent.addCollisionChannelToAffect = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + RadialForceComponent.fireImpulse = (delegate* unmanaged[Cdecl])radialForceComponentFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* materialInterfaceFunctions = (IntPtr*)buffer[position++]; + + MaterialInterface.isTwoSided = (delegate* unmanaged[Cdecl])materialInterfaceFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* materialFunctions = (IntPtr*)buffer[position++]; + + Material.isDefaultMaterial = (delegate* unmanaged[Cdecl])materialFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* materialInstanceFunctions = (IntPtr*)buffer[position++]; + + MaterialInstance.isChildOf = (delegate* unmanaged[Cdecl])materialInstanceFunctions[head++]; + MaterialInstance.getParent = (delegate* unmanaged[Cdecl])materialInstanceFunctions[head++]; + } + + unchecked { + int head = 0; + IntPtr* materialInstanceDynamicFunctions = (IntPtr*)buffer[position++]; + + MaterialInstanceDynamic.clearParameterValues = (delegate* unmanaged[Cdecl])materialInstanceDynamicFunctions[head++]; + MaterialInstanceDynamic.setTextureParameterValue = (delegate* unmanaged[Cdecl])materialInstanceDynamicFunctions[head++]; + MaterialInstanceDynamic.setVectorParameterValue = (delegate* unmanaged[Cdecl])materialInstanceDynamicFunctions[head++]; + MaterialInstanceDynamic.setScalarParameterValue = (delegate* unmanaged[Cdecl])materialInstanceDynamicFunctions[head++]; + } + + unchecked { + Type[] types = pluginAssembly.GetTypes(); + + foreach (Type type in types) { + MethodInfo[] methods = type.GetMethods(); + + if (type.Name == "Main" && type.IsPublic) { + foreach (MethodInfo method in methods) { + if (method.IsPublic && method.IsStatic && !method.IsGenericMethod) { + ParameterInfo[] parameterInfos = method.GetParameters(); + + if (parameterInfos.Length <= 1) { + if (method.Name == "OnWorldBegin") { + if (parameterInfos.Length == 0) + events[0] = GetFunctionPointer(method); + else + throw new ArgumentException(method.Name + " should not have arguments"); + + continue; + } + + if (method.Name == "OnWorldPostBegin") { + if (parameterInfos.Length == 0) + events[1] = GetFunctionPointer(method); + else + throw new ArgumentException(method.Name + " should not have arguments"); + + continue; + } + + if (method.Name == "OnWorldPrePhysicsTick") { + if (parameterInfos.Length == 1 && parameterInfos[0].ParameterType == typeof(float)) + events[2] = GetFunctionPointer(method); + else + throw new ArgumentException(method.Name + " should have a float argument"); + + continue; + } + + if (method.Name == "OnWorldDuringPhysicsTick") { + if (parameterInfos.Length == 1 && parameterInfos[0].ParameterType == typeof(float)) + events[3] = GetFunctionPointer(method); + else + throw new ArgumentException(method.Name + " should have a float argument"); + + continue; + } + + if (method.Name == "OnWorldPostPhysicsTick") { + if (parameterInfos.Length == 1 && parameterInfos[0].ParameterType == typeof(float)) + events[4] = GetFunctionPointer(method); + else + throw new ArgumentException(method.Name + " should have a float argument"); + + continue; + } + + if (method.Name == "OnWorldPostUpdateTick") { + if (parameterInfos.Length == 1 && parameterInfos[0].ParameterType == typeof(float)) + events[5] = GetFunctionPointer(method); + else + throw new ArgumentException(method.Name + " should have a float argument"); + + continue; + } + + if (method.Name == "OnWorldEnd") { + if (parameterInfos.Length == 0) + events[6] = GetFunctionPointer(method); + else + throw new ArgumentException(method.Name + " should not have arguments"); + + continue; + } + } + } + } + } + + foreach (MethodInfo method in methods) { + if (method.IsPublic && method.IsStatic && !method.IsGenericMethod) { + ParameterInfo[] parameterInfos = method.GetParameters(); + + if (parameterInfos.Length <= 1) { + if (parameterInfos.Length == 1 && parameterInfos[0].ParameterType != typeof(ObjectReference)) + continue; + + string name = type.FullName + "." + method.Name; + + userFunctions.Add(name.GetHashCode(StringComparison.Ordinal), GetFunctionPointer(method)); + } + } + } + } + } + + Directory.SetCurrentDirectory(Application.ProjectDirectory); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + return userFunctions; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string GetTypeName(Type type) => type.FullName.Replace(".", string.Empty, StringComparison.Ordinal); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string GetMethodName(Type[] parameters, Type returnType) { + string name = GetTypeName(returnType); + + foreach (Type type in parameters) { + name += '_' + GetTypeName(type); + } + + return name; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Type GetDelegateType(Type[] parameters, Type returnType) { + string methodName = GetMethodName(parameters, returnType); + + return delegateTypesCache.GetOrAdd(methodName, () => MakeDelegate(parameters, returnType, methodName)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Type MakeDelegate(Type[] types, Type returnType, string name) { + TypeBuilder builder = moduleBuilder.DefineType(name, delegateTypeAttributes, typeof(MulticastDelegate)); + + builder.DefineConstructor(ctorAttributes, CallingConventions.Standard, delegateCtorSignature).SetImplementationFlags(implAttributes); + builder.DefineMethod("Invoke", invokeAttributes, returnType, types).SetImplementationFlags(implAttributes); + + return builder.CreateTypeInfo(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static IntPtr GetFunctionPointer(MethodInfo method) { + string methodName = $"{ method.DeclaringType.FullName }.{ method.Name }"; + + Delegate dynamicDelegate = delegatesCache.GetOrAdd(methodName, () => { + ParameterInfo[] parameterInfos = method.GetParameters(); + Type[] parameterTypes = new Type[parameterInfos.Length]; + + for (int i = 0; i < parameterTypes.Length; i++) { + parameterTypes[i] = parameterInfos[i].ParameterType; + } + + return method.CreateDelegate(GetDelegateType(parameterTypes, method.ReturnType)); + }); + + return Collector.GetFunctionPointer(dynamicDelegate); + } + } + + [StructLayout(LayoutKind.Sequential)] + partial struct LinearColor { + private float r; + private float g; + private float b; + private float a; + } + + [StructLayout(LayoutKind.Sequential)] + partial struct Transform { + private Vector3 location; + private Quaternion rotation; + private Vector3 scale; + } + + [StructLayout(LayoutKind.Sequential)] + partial struct Hit { + private Vector3 location; + private Vector3 impactLocation; + private Vector3 normal; + private Vector3 impactNormal; + private Vector3 traceStart; + private Vector3 traceEnd; + private IntPtr actor; + private float time; + private float distance; + private float penetrationDepth; + private Bool blockingHit; + private Bool startPenetrating; + } + + [StructLayout(LayoutKind.Explicit, Size = 28)] + partial struct Bounds { + [FieldOffset(0)] + private Vector3 origin; + [FieldOffset(12)] + private Vector3 boxExtent; + [FieldOffset(24)] + private float sphereRadius; + } + + [StructLayout(LayoutKind.Explicit, Size = 16)] + partial struct CollisionShape { + [FieldOffset(0)] + private CollisionShapeType shapeType; + [FieldOffset(4)] + private Box box; + [FieldOffset(4)] + private Sphere sphere; + [FieldOffset(4)] + private Capsule capsule; + + private struct Box { + internal Vector3 halfExtent; + } + + private struct Sphere { + internal float radius; + } + + private struct Capsule { + internal float radius; + internal float halfHeight; + } + } + + internal struct Bool { + private byte value; + + public Bool(byte value) => this.value = value; + + public static implicit operator bool(Bool value) => value.value != 0; + + public static implicit operator Bool(bool value) => !value ? new(0) : new(1); + + public override int GetHashCode() => value.GetHashCode(); + } + + internal enum ObjectType : int { + Blueprint, + SoundWave, + AnimationSequence, + AnimationMontage, + StaticMesh, + SkeletalMesh, + Material, + Font, + Texture2D + } + + internal enum ActorType : int { + Base, + Camera, + TriggerBox, + TriggerSphere, + TriggerCapsule, + Pawn, + Character, + AIController, + PlayerController, + Brush, + AmbientSound, + DirectionalLight, + PointLight, + RectLight, + SpotLight, + TriggerVolume, + PostProcessVolume, + LevelScript, + GameModeBase + } + + internal enum ComponentType : int { + // Non-movable + Actor, + Input, + Movement, + RotatingMovement, + // Movable + Scene, + Audio, + Camera, + Light, + DirectionalLight, + MotionController, + StaticMesh, + InstancedStaticMesh, + HierarchicalInstancedStaticMesh, + ChildActor, + SpringArm, + PostProcess, + Box, + Sphere, + Capsule, + TextRender, + SkeletalMesh, + Spline, + RadialForce + } + + static unsafe partial class Assert { + internal static delegate* unmanaged[Cdecl] outputMessage; + } + + static unsafe partial class CommandLine { + internal static delegate* unmanaged[Cdecl] get; + internal static delegate* unmanaged[Cdecl] set; + internal static delegate* unmanaged[Cdecl] append; + } + + static unsafe partial class Debug { + internal static delegate* unmanaged[Cdecl] log; + internal static delegate* unmanaged[Cdecl] exception; + internal static delegate* unmanaged[Cdecl] addOnScreenMessage; + internal static delegate* unmanaged[Cdecl] clearOnScreenMessages; + internal static delegate* unmanaged[Cdecl] drawBox; + internal static delegate* unmanaged[Cdecl] drawCapsule; + internal static delegate* unmanaged[Cdecl] drawCone; + internal static delegate* unmanaged[Cdecl] drawCylinder; + internal static delegate* unmanaged[Cdecl] drawSphere; + internal static delegate* unmanaged[Cdecl] drawLine; + internal static delegate* unmanaged[Cdecl] drawPoint; + internal static delegate* unmanaged[Cdecl] flushPersistentLines; + } + + internal static unsafe class Object { + internal static delegate* unmanaged[Cdecl] isPendingKill; + internal static delegate* unmanaged[Cdecl] isValid; + internal static delegate* unmanaged[Cdecl] load; + internal static delegate* unmanaged[Cdecl] rename; + internal static delegate* unmanaged[Cdecl] invoke; + internal static delegate* unmanaged[Cdecl] toActor; + internal static delegate* unmanaged[Cdecl] toComponent; + internal static delegate* unmanaged[Cdecl] getID; + internal static delegate* unmanaged[Cdecl] getName; + internal static delegate* unmanaged[Cdecl] getBool; + internal static delegate* unmanaged[Cdecl] getByte; + internal static delegate* unmanaged[Cdecl] getShort; + internal static delegate* unmanaged[Cdecl] getInt; + internal static delegate* unmanaged[Cdecl] getLong; + internal static delegate* unmanaged[Cdecl] getUShort; + internal static delegate* unmanaged[Cdecl] getUInt; + internal static delegate* unmanaged[Cdecl] getULong; + internal static delegate* unmanaged[Cdecl] getFloat; + internal static delegate* unmanaged[Cdecl] getDouble; + internal static delegate* unmanaged[Cdecl] getEnum; + internal static delegate* unmanaged[Cdecl] getString; + internal static delegate* unmanaged[Cdecl] getText; + internal static delegate* unmanaged[Cdecl] setBool; + internal static delegate* unmanaged[Cdecl] setByte; + internal static delegate* unmanaged[Cdecl] setShort; + internal static delegate* unmanaged[Cdecl] setInt; + internal static delegate* unmanaged[Cdecl] setLong; + internal static delegate* unmanaged[Cdecl] setUShort; + internal static delegate* unmanaged[Cdecl] setUInt; + internal static delegate* unmanaged[Cdecl] setULong; + internal static delegate* unmanaged[Cdecl] setFloat; + internal static delegate* unmanaged[Cdecl] setDouble; + internal static delegate* unmanaged[Cdecl] setEnum; + internal static delegate* unmanaged[Cdecl] setString; + internal static delegate* unmanaged[Cdecl] setText; + } + + static unsafe partial class Application { + internal static delegate* unmanaged[Cdecl] isCanEverRender; + internal static delegate* unmanaged[Cdecl] isPackagedForDistribution; + internal static delegate* unmanaged[Cdecl] isPackagedForShipping; + internal static delegate* unmanaged[Cdecl] getProjectDirectory; + internal static delegate* unmanaged[Cdecl] getDefaultLanguage; + internal static delegate* unmanaged[Cdecl] getProjectName; + internal static delegate* unmanaged[Cdecl] getVolumeMultiplier; + internal static delegate* unmanaged[Cdecl] setProjectName; + internal static delegate* unmanaged[Cdecl] setVolumeMultiplier; + internal static delegate* unmanaged[Cdecl] requestExit; + } + + static unsafe partial class ConsoleManager { + internal static delegate* unmanaged[Cdecl] isRegisteredVariable; + internal static delegate* unmanaged[Cdecl] findVariable; + internal static delegate* unmanaged[Cdecl] registerVariableBool; + internal static delegate* unmanaged[Cdecl] registerVariableInt; + internal static delegate* unmanaged[Cdecl] registerVariableFloat; + internal static delegate* unmanaged[Cdecl] registerVariableString; + internal static delegate* unmanaged[Cdecl] registerCommand; + internal static delegate* unmanaged[Cdecl] unregisterObject; + } + + static unsafe partial class Engine { + internal static delegate* unmanaged[Cdecl] isSplitScreen; + internal static delegate* unmanaged[Cdecl] isEditor; + internal static delegate* unmanaged[Cdecl] isForegroundWindow; + internal static delegate* unmanaged[Cdecl] isExitRequested; + internal static delegate* unmanaged[Cdecl] getNetMode; + internal static delegate* unmanaged[Cdecl] getFrameNumber; + internal static delegate* unmanaged[Cdecl] getViewportSize; + internal static delegate* unmanaged[Cdecl] getScreenResolution; + internal static delegate* unmanaged[Cdecl] getWindowMode; + internal static delegate* unmanaged[Cdecl] getVersion; + internal static delegate* unmanaged[Cdecl] getMaxFPS; + internal static delegate* unmanaged[Cdecl] setMaxFPS; + internal static delegate* unmanaged[Cdecl] setTitle; + internal static delegate* unmanaged[Cdecl] addActionMapping; + internal static delegate* unmanaged[Cdecl] addAxisMapping; + internal static delegate* unmanaged[Cdecl] forceGarbageCollection; + internal static delegate* unmanaged[Cdecl] delayGarbageCollection; + } + + static unsafe partial class HeadMountedDisplay { + internal static delegate* unmanaged[Cdecl] isConnected; + internal static delegate* unmanaged[Cdecl] getEnabled; + internal static delegate* unmanaged[Cdecl] getLowPersistenceMode; + internal static delegate* unmanaged[Cdecl] getDeviceName; + internal static delegate* unmanaged[Cdecl] setEnable; + internal static delegate* unmanaged[Cdecl] setLowPersistenceMode; + } + + static unsafe partial class World { + internal static delegate* unmanaged[Cdecl] forEachActor; + internal static delegate* unmanaged[Cdecl] getActorCount; + internal static delegate* unmanaged[Cdecl] getDeltaSeconds; + internal static delegate* unmanaged[Cdecl] getRealTimeSeconds; + internal static delegate* unmanaged[Cdecl] getTimeSeconds; + internal static delegate* unmanaged[Cdecl] getCurrentLevelName; + internal static delegate* unmanaged[Cdecl] getSimulatePhysics; + internal static delegate* unmanaged[Cdecl] getWorldOrigin; + internal static delegate* unmanaged[Cdecl] getActor; + internal static delegate* unmanaged[Cdecl] getActorByTag; + internal static delegate* unmanaged[Cdecl] getActorByID; + internal static delegate* unmanaged[Cdecl] getFirstPlayerController; + internal static delegate* unmanaged[Cdecl] getGameMode; + internal static delegate* unmanaged[Cdecl] setOnActorBeginOverlapCallback; + internal static delegate* unmanaged[Cdecl] setOnActorEndOverlapCallback; + internal static delegate* unmanaged[Cdecl] setOnActorHitCallback; + internal static delegate* unmanaged[Cdecl] setOnActorBeginCursorOverCallback; + internal static delegate* unmanaged[Cdecl] setOnActorEndCursorOverCallback; + internal static delegate* unmanaged[Cdecl] setOnActorClickedCallback; + internal static delegate* unmanaged[Cdecl] setOnActorReleasedCallback; + internal static delegate* unmanaged[Cdecl] setOnComponentBeginOverlapCallback; + internal static delegate* unmanaged[Cdecl] setOnComponentEndOverlapCallback; + internal static delegate* unmanaged[Cdecl] setOnComponentHitCallback; + internal static delegate* unmanaged[Cdecl] setOnComponentBeginCursorOverCallback; + internal static delegate* unmanaged[Cdecl] setOnComponentEndCursorOverCallback; + internal static delegate* unmanaged[Cdecl] setOnComponentClickedCallback; + internal static delegate* unmanaged[Cdecl] setOnComponentReleasedCallback; + internal static delegate* unmanaged[Cdecl] setSimulatePhysics; + internal static delegate* unmanaged[Cdecl] setGravity; + internal static delegate* unmanaged[Cdecl] setWorldOrigin; + internal static delegate* unmanaged[Cdecl] openLevel; + internal static delegate* unmanaged[Cdecl] lineTraceTestByChannel; + internal static delegate* unmanaged[Cdecl] lineTraceTestByProfile; + internal static delegate* unmanaged[Cdecl] lineTraceSingleByChannel; + internal static delegate* unmanaged[Cdecl] lineTraceSingleByProfile; + internal static delegate* unmanaged[Cdecl] sweepTestByChannel; + internal static delegate* unmanaged[Cdecl] sweepTestByProfile; + internal static delegate* unmanaged[Cdecl] sweepSingleByChannel; + internal static delegate* unmanaged[Cdecl] sweepSingleByProfile; + internal static delegate* unmanaged[Cdecl] overlapAnyTestByChannel; + internal static delegate* unmanaged[Cdecl] overlapAnyTestByProfile; + internal static delegate* unmanaged[Cdecl] overlapBlockingTestByChannel; + internal static delegate* unmanaged[Cdecl] overlapBlockingTestByProfile; + } + + unsafe partial struct Asset { + internal static delegate* unmanaged[Cdecl] isValid; + internal static delegate* unmanaged[Cdecl] getName; + internal static delegate* unmanaged[Cdecl] getPath; + } + + unsafe partial class AssetRegistry { + internal static delegate* unmanaged[Cdecl] get; + internal static delegate* unmanaged[Cdecl] hasAssets; + internal static delegate* unmanaged[Cdecl] forEachAsset; + } + + unsafe partial class Blueprint { + internal static delegate* unmanaged[Cdecl] isValidActorClass; + internal static delegate* unmanaged[Cdecl] isValidComponentClass; + } + + unsafe partial class ConsoleObject { + internal static delegate* unmanaged[Cdecl] isBool; + internal static delegate* unmanaged[Cdecl] isInt; + internal static delegate* unmanaged[Cdecl] isFloat; + internal static delegate* unmanaged[Cdecl] isString; + } + + unsafe partial class ConsoleVariable { + internal static delegate* unmanaged[Cdecl] getBool; + internal static delegate* unmanaged[Cdecl] getInt; + internal static delegate* unmanaged[Cdecl] getFloat; + internal static delegate* unmanaged[Cdecl] getString; + internal static delegate* unmanaged[Cdecl] setBool; + internal static delegate* unmanaged[Cdecl] setInt; + internal static delegate* unmanaged[Cdecl] setFloat; + internal static delegate* unmanaged[Cdecl] setString; + internal static delegate* unmanaged[Cdecl] setOnChangedCallback; + internal static delegate* unmanaged[Cdecl] clearOnChangedCallback; + } + + unsafe partial class Actor { + internal static delegate* unmanaged[Cdecl] isPendingKill; + internal static delegate* unmanaged[Cdecl] isRootComponentMovable; + internal static delegate* unmanaged[Cdecl] isOverlappingActor; + internal static delegate* unmanaged[Cdecl] forEachComponent; + internal static delegate* unmanaged[Cdecl] forEachAttachedActor; + internal static delegate* unmanaged[Cdecl] forEachChildActor; + internal static delegate* unmanaged[Cdecl] forEachOverlappingActor; + internal static delegate* unmanaged[Cdecl] spawn; + internal static delegate* unmanaged[Cdecl] destroy; + internal static delegate* unmanaged[Cdecl] rename; + internal static delegate* unmanaged[Cdecl] hide; + internal static delegate* unmanaged[Cdecl] teleportTo; + internal static delegate* unmanaged[Cdecl] getComponent; + internal static delegate* unmanaged[Cdecl] getComponentByTag; + internal static delegate* unmanaged[Cdecl] getComponentByID; + internal static delegate* unmanaged[Cdecl] getRootComponent; + internal static delegate* unmanaged[Cdecl] getInputComponent; + internal static delegate* unmanaged[Cdecl] getCreationTime; + internal static delegate* unmanaged[Cdecl] getBlockInput; + internal static delegate* unmanaged[Cdecl] getDistanceTo; + internal static delegate* unmanaged[Cdecl] getHorizontalDistanceTo; + internal static delegate* unmanaged[Cdecl] getBounds; + internal static delegate* unmanaged[Cdecl] getEyesViewPoint; + internal static delegate* unmanaged[Cdecl] setRootComponent; + internal static delegate* unmanaged[Cdecl] setInputComponent; + internal static delegate* unmanaged[Cdecl] setBlockInput; + internal static delegate* unmanaged[Cdecl] setLifeSpan; + internal static delegate* unmanaged[Cdecl] setEnableInput; + internal static delegate* unmanaged[Cdecl] setEnableCollision; + internal static delegate* unmanaged[Cdecl] addTag; + internal static delegate* unmanaged[Cdecl] removeTag; + internal static delegate* unmanaged[Cdecl] hasTag; + internal static delegate* unmanaged[Cdecl] registerEvent; + internal static delegate* unmanaged[Cdecl] unregisterEvent; + } + + unsafe partial class GameModeBase { + internal static delegate* unmanaged[Cdecl] getUseSeamlessTravel; + internal static delegate* unmanaged[Cdecl] setUseSeamlessTravel; + internal static delegate* unmanaged[Cdecl] swapPlayerControllers; + } + + unsafe partial class TriggerBase { } + + unsafe partial class TriggerBox { } + + unsafe partial class TriggerCapsule { } + + unsafe partial class TriggerSphere { } + + unsafe partial class Pawn { + internal static delegate* unmanaged[Cdecl] isControlled; + internal static delegate* unmanaged[Cdecl] isPlayerControlled; + internal static delegate* unmanaged[Cdecl] getAutoPossessAI; + internal static delegate* unmanaged[Cdecl] getAutoPossessPlayer; + internal static delegate* unmanaged[Cdecl] getUseControllerRotationYaw; + internal static delegate* unmanaged[Cdecl] getUseControllerRotationPitch; + internal static delegate* unmanaged[Cdecl] getUseControllerRotationRoll; + internal static delegate* unmanaged[Cdecl] getGravityDirection; + internal static delegate* unmanaged[Cdecl] getAIController; + internal static delegate* unmanaged[Cdecl] getPlayerController; + internal static delegate* unmanaged[Cdecl] setAutoPossessAI; + internal static delegate* unmanaged[Cdecl] setAutoPossessPlayer; + internal static delegate* unmanaged[Cdecl] setUseControllerRotationYaw; + internal static delegate* unmanaged[Cdecl] setUseControllerRotationPitch; + internal static delegate* unmanaged[Cdecl] setUseControllerRotationRoll; + internal static delegate* unmanaged[Cdecl] addControllerYawInput; + internal static delegate* unmanaged[Cdecl] addControllerPitchInput; + internal static delegate* unmanaged[Cdecl] addControllerRollInput; + internal static delegate* unmanaged[Cdecl] addMovementInput; + } + + unsafe partial class Character { + internal static delegate* unmanaged[Cdecl] isCrouched; + internal static delegate* unmanaged[Cdecl] canCrouch; + internal static delegate* unmanaged[Cdecl] canJump; + internal static delegate* unmanaged[Cdecl] checkJumpInput; + internal static delegate* unmanaged[Cdecl] clearJumpInput; + internal static delegate* unmanaged[Cdecl] launch; + internal static delegate* unmanaged[Cdecl] crouch; + internal static delegate* unmanaged[Cdecl] stopCrouching; + internal static delegate* unmanaged[Cdecl] jump; + internal static delegate* unmanaged[Cdecl] stopJumping; + internal static delegate* unmanaged[Cdecl] setOnLandedCallback; + } + + unsafe partial class Controller { + internal static delegate* unmanaged[Cdecl] isLookInputIgnored; + internal static delegate* unmanaged[Cdecl] isMoveInputIgnored; + internal static delegate* unmanaged[Cdecl] isPlayerController; + internal static delegate* unmanaged[Cdecl] getPawn; + internal static delegate* unmanaged[Cdecl] getCharacter; + internal static delegate* unmanaged[Cdecl] getViewTarget; + internal static delegate* unmanaged[Cdecl] getControlRotation; + internal static delegate* unmanaged[Cdecl] getDesiredRotation; + internal static delegate* unmanaged[Cdecl] lineOfSightTo; + internal static delegate* unmanaged[Cdecl] setControlRotation; + internal static delegate* unmanaged[Cdecl] setInitialLocationAndRotation; + internal static delegate* unmanaged[Cdecl] setIgnoreLookInput; + internal static delegate* unmanaged[Cdecl] setIgnoreMoveInput; + internal static delegate* unmanaged[Cdecl] resetIgnoreLookInput; + internal static delegate* unmanaged[Cdecl] resetIgnoreMoveInput; + internal static delegate* unmanaged[Cdecl] possess; + internal static delegate* unmanaged[Cdecl] unpossess; + } + + unsafe partial class AIController { + internal static delegate* unmanaged[Cdecl] clearFocus; + internal static delegate* unmanaged[Cdecl] getFocalPoint; + internal static delegate* unmanaged[Cdecl] setFocalPoint; + internal static delegate* unmanaged[Cdecl] getFocusActor; + internal static delegate* unmanaged[Cdecl] getAllowStrafe; + internal static delegate* unmanaged[Cdecl] setAllowStrafe; + internal static delegate* unmanaged[Cdecl] setFocus; + } + + unsafe partial class PlayerController { + internal static delegate* unmanaged[Cdecl] isPaused; + internal static delegate* unmanaged[Cdecl] getShowMouseCursor; + internal static delegate* unmanaged[Cdecl] getEnableClickEvents; + internal static delegate* unmanaged[Cdecl] getEnableMouseOverEvents; + internal static delegate* unmanaged[Cdecl] getMousePosition; + internal static delegate* unmanaged[Cdecl] getPlayer; + internal static delegate* unmanaged[Cdecl] getPlayerInput; + internal static delegate* unmanaged[Cdecl] getHitResultAtScreenPosition; + internal static delegate* unmanaged[Cdecl] getHitResultUnderCursor; + internal static delegate* unmanaged[Cdecl] setShowMouseCursor; + internal static delegate* unmanaged[Cdecl] setEnableClickEvents; + internal static delegate* unmanaged[Cdecl] setEnableMouseOverEvents; + internal static delegate* unmanaged[Cdecl] setMousePosition; + internal static delegate* unmanaged[Cdecl] consoleCommand; + internal static delegate* unmanaged[Cdecl] setPause; + internal static delegate* unmanaged[Cdecl] setViewTarget; + internal static delegate* unmanaged[Cdecl] setViewTargetWithBlend; + internal static delegate* unmanaged[Cdecl] addYawInput; + internal static delegate* unmanaged[Cdecl] addPitchInput; + internal static delegate* unmanaged[Cdecl] addRollInput; + } + + unsafe partial class Volume { + internal static delegate* unmanaged[Cdecl] encompassesPoint; + } + + unsafe partial class TriggerVolume { } + + unsafe partial class PostProcessVolume { + internal static delegate* unmanaged[Cdecl] getEnabled; + internal static delegate* unmanaged[Cdecl] getBlendRadius; + internal static delegate* unmanaged[Cdecl] getBlendWeight; + internal static delegate* unmanaged[Cdecl] getUnbound; + internal static delegate* unmanaged[Cdecl] getPriority; + internal static delegate* unmanaged[Cdecl] setEnabled; + internal static delegate* unmanaged[Cdecl] setBlendRadius; + internal static delegate* unmanaged[Cdecl] setBlendWeight; + internal static delegate* unmanaged[Cdecl] setUnbound; + internal static delegate* unmanaged[Cdecl] setPriority; + } + + unsafe partial class LevelScript { } + + unsafe partial class AmbientSound { } + + unsafe partial class Light { } + + unsafe partial class DirectionalLight { } + + unsafe partial class PointLight { } + + unsafe partial class RectLight { } + + unsafe partial class SpotLight { } + + unsafe partial class SoundBase { + internal static delegate* unmanaged[Cdecl] getDuration; + } + + unsafe partial class SoundWave { + internal static delegate* unmanaged[Cdecl] getLoop; + internal static delegate* unmanaged[Cdecl] setLoop; + } + + unsafe partial class AnimationAsset { } + + unsafe partial class AnimationSequenceBase { } + + unsafe partial class AnimationSequence { } + + unsafe partial class AnimationCompositeBase { } + + unsafe partial class AnimationMontage { } + + unsafe partial class AnimationInstance { + internal static delegate* unmanaged[Cdecl] getCurrentActiveMontage; + internal static delegate* unmanaged[Cdecl] isPlaying; + internal static delegate* unmanaged[Cdecl] getPlayRate; + internal static delegate* unmanaged[Cdecl] getPosition; + internal static delegate* unmanaged[Cdecl] getBlendTime; + internal static delegate* unmanaged[Cdecl] getCurrentSection; + internal static delegate* unmanaged[Cdecl] setPlayRate; + internal static delegate* unmanaged[Cdecl] setPosition; + internal static delegate* unmanaged[Cdecl] setNextSection; + internal static delegate* unmanaged[Cdecl] playMontage; + internal static delegate* unmanaged[Cdecl] pauseMontage; + internal static delegate* unmanaged[Cdecl] resumeMontage; + internal static delegate* unmanaged[Cdecl] stopMontage; + internal static delegate* unmanaged[Cdecl] jumpToSection; + internal static delegate* unmanaged[Cdecl] jumpToSectionsEnd; + } + + unsafe partial class Player { + internal static delegate* unmanaged[Cdecl] getPlayerController; + } + + unsafe partial class PlayerInput { + internal static delegate* unmanaged[Cdecl] isKeyPressed; + internal static delegate* unmanaged[Cdecl] getTimeKeyPressed; + internal static delegate* unmanaged[Cdecl] getMouseSensitivity; + internal static delegate* unmanaged[Cdecl] setMouseSensitivity; + internal static delegate* unmanaged[Cdecl] addActionMapping; + internal static delegate* unmanaged[Cdecl] addAxisMapping; + internal static delegate* unmanaged[Cdecl] removeActionMapping; + internal static delegate* unmanaged[Cdecl] removeAxisMapping; + } + + unsafe partial class Font { + internal static delegate* unmanaged[Cdecl] getStringSize; + } + + unsafe partial class StreamableRenderAsset { } + + unsafe partial class StaticMesh { } + + unsafe partial class SkeletalMesh { } + + unsafe partial class Texture { } + + unsafe partial class Texture2D { + internal static delegate* unmanaged[Cdecl] createFromFile; + internal static delegate* unmanaged[Cdecl] createFromBuffer; + internal static delegate* unmanaged[Cdecl] hasAlphaChannel; + internal static delegate* unmanaged[Cdecl] getSize; + internal static delegate* unmanaged[Cdecl] getPixelFormat; + } + + unsafe partial class ActorComponent { + internal static delegate* unmanaged[Cdecl] isOwnerSelected; + internal static delegate* unmanaged[Cdecl] getOwner; + internal static delegate* unmanaged[Cdecl] destroy; + internal static delegate* unmanaged[Cdecl] addTag; + internal static delegate* unmanaged[Cdecl] removeTag; + internal static delegate* unmanaged[Cdecl] hasTag; + } + + unsafe partial class InputComponent { + internal static delegate* unmanaged[Cdecl] hasBindings; + internal static delegate* unmanaged[Cdecl] getActionBindingsNumber; + internal static delegate* unmanaged[Cdecl] clearActionBindings; + internal static delegate* unmanaged[Cdecl] bindAction; + internal static delegate* unmanaged[Cdecl] bindAxis; + internal static delegate* unmanaged[Cdecl] removeActionBinding; + internal static delegate* unmanaged[Cdecl] getBlockInput; + internal static delegate* unmanaged[Cdecl] setBlockInput; + internal static delegate* unmanaged[Cdecl] getPriority; + internal static delegate* unmanaged[Cdecl] setPriority; + } + + unsafe partial class MovementComponent { + internal static delegate* unmanaged[Cdecl] getConstrainToPlane; + internal static delegate* unmanaged[Cdecl] getSnapToPlaneAtStart; + internal static delegate* unmanaged[Cdecl] getUpdateOnlyIfRendered; + internal static delegate* unmanaged[Cdecl] getVelocity; + internal static delegate* unmanaged[Cdecl] getPlaneConstraint; + internal static delegate* unmanaged[Cdecl] getPlaneConstraintNormal; + internal static delegate* unmanaged[Cdecl] getPlaneConstraintOrigin; + internal static delegate* unmanaged[Cdecl] getGravity; + internal static delegate* unmanaged[Cdecl] getMaxSpeed; + internal static delegate* unmanaged[Cdecl] setConstrainToPlane; + internal static delegate* unmanaged[Cdecl] setSnapToPlaneAtStart; + internal static delegate* unmanaged[Cdecl] setUpdateOnlyIfRendered; + internal static delegate* unmanaged[Cdecl] setVelocity; + internal static delegate* unmanaged[Cdecl] setPlaneConstraint; + internal static delegate* unmanaged[Cdecl] setPlaneConstraintNormal; + internal static delegate* unmanaged[Cdecl] setPlaneConstraintOrigin; + internal static delegate* unmanaged[Cdecl] setPlaneConstraintFromVectors; + internal static delegate* unmanaged[Cdecl] isExceedingMaxSpeed; + internal static delegate* unmanaged[Cdecl] isInWater; + internal static delegate* unmanaged[Cdecl] stopMovement; + internal static delegate* unmanaged[Cdecl] constrainDirectionToPlane; + internal static delegate* unmanaged[Cdecl] constrainLocationToPlane; + internal static delegate* unmanaged[Cdecl] constrainNormalToPlane; + } + + unsafe partial class RotatingMovementComponent { + internal static delegate* unmanaged[Cdecl] create; + internal static delegate* unmanaged[Cdecl] getRotationInLocalSpace; + internal static delegate* unmanaged[Cdecl] getPivotTranslation; + internal static delegate* unmanaged[Cdecl] getRotationRate; + internal static delegate* unmanaged[Cdecl] setRotationInLocalSpace; + internal static delegate* unmanaged[Cdecl] setPivotTranslation; + internal static delegate* unmanaged[Cdecl] setRotationRate; + } + + unsafe partial class SceneComponent { + internal static delegate* unmanaged[Cdecl] isAttachedToComponent; + internal static delegate* unmanaged[Cdecl] isAttachedToActor; + internal static delegate* unmanaged[Cdecl] isVisible; + internal static delegate* unmanaged[Cdecl] isSocketExists; + internal static delegate* unmanaged[Cdecl] hasAnySockets; + internal static delegate* unmanaged[Cdecl] canAttachAsChild; + internal static delegate* unmanaged[Cdecl] forEachAttachedChild; + internal static delegate* unmanaged[Cdecl] create; + internal static delegate* unmanaged[Cdecl] attachToComponent; + internal static delegate* unmanaged[Cdecl] detachFromComponent; + internal static delegate* unmanaged[Cdecl] activate; + internal static delegate* unmanaged[Cdecl] deactivate; + internal static delegate* unmanaged[Cdecl] updateToWorld; + internal static delegate* unmanaged[Cdecl] addLocalOffset; + internal static delegate* unmanaged[Cdecl] addLocalRotation; + internal static delegate* unmanaged[Cdecl] addRelativeLocation; + internal static delegate* unmanaged[Cdecl] addRelativeRotation; + internal static delegate* unmanaged[Cdecl] addLocalTransform; + internal static delegate* unmanaged[Cdecl] addWorldOffset; + internal static delegate* unmanaged[Cdecl] addWorldRotation; + internal static delegate* unmanaged[Cdecl] addWorldTransform; + internal static delegate* unmanaged[Cdecl] getAttachedSocketName; + internal static delegate* unmanaged[Cdecl] getBounds; + internal static delegate* unmanaged[Cdecl] getSocketLocation; + internal static delegate* unmanaged[Cdecl] getSocketRotation; + internal static delegate* unmanaged[Cdecl] getComponentVelocity; + internal static delegate* unmanaged[Cdecl] getComponentLocation; + internal static delegate* unmanaged[Cdecl] getComponentRotation; + internal static delegate* unmanaged[Cdecl] getComponentScale; + internal static delegate* unmanaged[Cdecl] getComponentTransform; + internal static delegate* unmanaged[Cdecl] getForwardVector; + internal static delegate* unmanaged[Cdecl] getRightVector; + internal static delegate* unmanaged[Cdecl] getUpVector; + internal static delegate* unmanaged[Cdecl] setMobility; + internal static delegate* unmanaged[Cdecl] setVisibility; + internal static delegate* unmanaged[Cdecl] setRelativeLocation; + internal static delegate* unmanaged[Cdecl] setRelativeRotation; + internal static delegate* unmanaged[Cdecl] setRelativeTransform; + internal static delegate* unmanaged[Cdecl] setWorldLocation; + internal static delegate* unmanaged[Cdecl] setWorldRotation; + internal static delegate* unmanaged[Cdecl] setWorldScale; + internal static delegate* unmanaged[Cdecl] setWorldTransform; + } + + unsafe partial class AudioComponent { + internal static delegate* unmanaged[Cdecl] isPlaying; + internal static delegate* unmanaged[Cdecl] getPaused; + internal static delegate* unmanaged[Cdecl] setSound; + internal static delegate* unmanaged[Cdecl] setPaused; + internal static delegate* unmanaged[Cdecl] play; + internal static delegate* unmanaged[Cdecl] stop; + internal static delegate* unmanaged[Cdecl] fadeIn; + internal static delegate* unmanaged[Cdecl] fadeOut; + } + + unsafe partial class CameraComponent { + internal static delegate* unmanaged[Cdecl] getConstrainAspectRatio; + internal static delegate* unmanaged[Cdecl] getAspectRatio; + internal static delegate* unmanaged[Cdecl] getFieldOfView; + internal static delegate* unmanaged[Cdecl] getOrthoFarClipPlane; + internal static delegate* unmanaged[Cdecl] getOrthoNearClipPlane; + internal static delegate* unmanaged[Cdecl] getOrthoWidth; + internal static delegate* unmanaged[Cdecl] getLockToHeadMountedDisplay; + internal static delegate* unmanaged[Cdecl] setProjectionMode; + internal static delegate* unmanaged[Cdecl] setConstrainAspectRatio; + internal static delegate* unmanaged[Cdecl] setAspectRatio; + internal static delegate* unmanaged[Cdecl] setFieldOfView; + internal static delegate* unmanaged[Cdecl] setOrthoFarClipPlane; + internal static delegate* unmanaged[Cdecl] setOrthoNearClipPlane; + internal static delegate* unmanaged[Cdecl] setOrthoWidth; + internal static delegate* unmanaged[Cdecl] setLockToHeadMountedDisplay; + } + + unsafe partial class ChildActorComponent { + internal static delegate* unmanaged[Cdecl] getChildActor; + internal static delegate* unmanaged[Cdecl] setChildActor; + } + + unsafe partial class SpringArmComponent { + internal static delegate* unmanaged[Cdecl] isCollisionFixApplied; + internal static delegate* unmanaged[Cdecl] getDrawDebugLagMarkers; + internal static delegate* unmanaged[Cdecl] getCollisionTest; + internal static delegate* unmanaged[Cdecl] getCameraPositionLag; + internal static delegate* unmanaged[Cdecl] getCameraRotationLag; + internal static delegate* unmanaged[Cdecl] getCameraLagSubstepping; + internal static delegate* unmanaged[Cdecl] getInheritPitch; + internal static delegate* unmanaged[Cdecl] getInheritRoll; + internal static delegate* unmanaged[Cdecl] getInheritYaw; + internal static delegate* unmanaged[Cdecl] getCameraLagMaxDistance; + internal static delegate* unmanaged[Cdecl] getCameraLagMaxTimeStep; + internal static delegate* unmanaged[Cdecl] getCameraPositionLagSpeed; + internal static delegate* unmanaged[Cdecl] getCameraRotationLagSpeed; + internal static delegate* unmanaged[Cdecl] getProbeChannel; + internal static delegate* unmanaged[Cdecl] getProbeSize; + internal static delegate* unmanaged[Cdecl] getSocketOffset; + internal static delegate* unmanaged[Cdecl] getTargetArmLength; + internal static delegate* unmanaged[Cdecl] getTargetOffset; + internal static delegate* unmanaged[Cdecl] getUnfixedCameraPosition; + internal static delegate* unmanaged[Cdecl] getDesiredRotation; + internal static delegate* unmanaged[Cdecl] getTargetRotation; + internal static delegate* unmanaged[Cdecl] getUsePawnControlRotation; + internal static delegate* unmanaged[Cdecl] setDrawDebugLagMarkers; + internal static delegate* unmanaged[Cdecl] setCollisionTest; + internal static delegate* unmanaged[Cdecl] setCameraPositionLag; + internal static delegate* unmanaged[Cdecl] setCameraRotationLag; + internal static delegate* unmanaged[Cdecl] setCameraLagSubstepping; + internal static delegate* unmanaged[Cdecl] setInheritPitch; + internal static delegate* unmanaged[Cdecl] setInheritRoll; + internal static delegate* unmanaged[Cdecl] setInheritYaw; + internal static delegate* unmanaged[Cdecl] setCameraLagMaxDistance; + internal static delegate* unmanaged[Cdecl] setCameraLagMaxTimeStep; + internal static delegate* unmanaged[Cdecl] setCameraPositionLagSpeed; + internal static delegate* unmanaged[Cdecl] setCameraRotationLagSpeed; + internal static delegate* unmanaged[Cdecl] setProbeChannel; + internal static delegate* unmanaged[Cdecl] setProbeSize; + internal static delegate* unmanaged[Cdecl] setSocketOffset; + internal static delegate* unmanaged[Cdecl] setTargetArmLength; + internal static delegate* unmanaged[Cdecl] setTargetOffset; + internal static delegate* unmanaged[Cdecl] setUsePawnControlRotation; + } + + unsafe partial class PostProcessComponent { + internal static delegate* unmanaged[Cdecl] getEnabled; + internal static delegate* unmanaged[Cdecl] getBlendRadius; + internal static delegate* unmanaged[Cdecl] getBlendWeight; + internal static delegate* unmanaged[Cdecl] getUnbound; + internal static delegate* unmanaged[Cdecl] getPriority; + internal static delegate* unmanaged[Cdecl] setEnabled; + internal static delegate* unmanaged[Cdecl] setBlendRadius; + internal static delegate* unmanaged[Cdecl] setBlendWeight; + internal static delegate* unmanaged[Cdecl] setUnbound; + internal static delegate* unmanaged[Cdecl] setPriority; + } + + unsafe partial class PrimitiveComponent { + internal static delegate* unmanaged[Cdecl] isGravityEnabled; + internal static delegate* unmanaged[Cdecl] isOverlappingComponent; + internal static delegate* unmanaged[Cdecl] forEachOverlappingComponent; + internal static delegate* unmanaged[Cdecl] addAngularImpulseInDegrees; + internal static delegate* unmanaged[Cdecl] addAngularImpulseInRadians; + internal static delegate* unmanaged[Cdecl] addForce; + internal static delegate* unmanaged[Cdecl] addForceAtLocation; + internal static delegate* unmanaged[Cdecl] addImpulse; + internal static delegate* unmanaged[Cdecl] addImpulseAtLocation; + internal static delegate* unmanaged[Cdecl] addRadialForce; + internal static delegate* unmanaged[Cdecl] addRadialImpulse; + internal static delegate* unmanaged[Cdecl] addTorqueInDegrees; + internal static delegate* unmanaged[Cdecl] addTorqueInRadians; + internal static delegate* unmanaged[Cdecl] getMass; + internal static delegate* unmanaged[Cdecl] getPhysicsLinearVelocity; + internal static delegate* unmanaged[Cdecl] getPhysicsLinearVelocityAtPoint; + internal static delegate* unmanaged[Cdecl] getPhysicsAngularVelocityInDegrees; + internal static delegate* unmanaged[Cdecl] getPhysicsAngularVelocityInRadians; + internal static delegate* unmanaged[Cdecl] getCastShadow; + internal static delegate* unmanaged[Cdecl] getOnlyOwnerSee; + internal static delegate* unmanaged[Cdecl] getOwnerNoSee; + internal static delegate* unmanaged[Cdecl] getIgnoreRadialForce; + internal static delegate* unmanaged[Cdecl] getIgnoreRadialImpulse; + internal static delegate* unmanaged[Cdecl] getMaterial; + internal static delegate* unmanaged[Cdecl] getMaterialsNumber; + internal static delegate* unmanaged[Cdecl] getDistanceToCollision; + internal static delegate* unmanaged[Cdecl] getSquaredDistanceToCollision; + internal static delegate* unmanaged[Cdecl] getAngularDamping; + internal static delegate* unmanaged[Cdecl] getLinearDamping; + internal static delegate* unmanaged[Cdecl] setGenerateOverlapEvents; + internal static delegate* unmanaged[Cdecl] setGenerateHitEvents; + internal static delegate* unmanaged[Cdecl] setMass; + internal static delegate* unmanaged[Cdecl] setCenterOfMass; + internal static delegate* unmanaged[Cdecl] setPhysicsLinearVelocity; + internal static delegate* unmanaged[Cdecl] setPhysicsAngularVelocityInDegrees; + internal static delegate* unmanaged[Cdecl] setPhysicsAngularVelocityInRadians; + internal static delegate* unmanaged[Cdecl] setPhysicsMaxAngularVelocityInDegrees; + internal static delegate* unmanaged[Cdecl] setPhysicsMaxAngularVelocityInRadians; + internal static delegate* unmanaged[Cdecl] setCastShadow; + internal static delegate* unmanaged[Cdecl] setOnlyOwnerSee; + internal static delegate* unmanaged[Cdecl] setOwnerNoSee; + internal static delegate* unmanaged[Cdecl] setIgnoreRadialForce; + internal static delegate* unmanaged[Cdecl] setIgnoreRadialImpulse; + internal static delegate* unmanaged[Cdecl] setMaterial; + internal static delegate* unmanaged[Cdecl] setSimulatePhysics; + internal static delegate* unmanaged[Cdecl] setAngularDamping; + internal static delegate* unmanaged[Cdecl] setLinearDamping; + internal static delegate* unmanaged[Cdecl] setEnableGravity; + internal static delegate* unmanaged[Cdecl] setCollisionMode; + internal static delegate* unmanaged[Cdecl] setCollisionChannel; + internal static delegate* unmanaged[Cdecl] setCollisionProfileName; + internal static delegate* unmanaged[Cdecl] setCollisionResponseToChannel; + internal static delegate* unmanaged[Cdecl] setCollisionResponseToAllChannels; + internal static delegate* unmanaged[Cdecl] setIgnoreActorWhenMoving; + internal static delegate* unmanaged[Cdecl] setIgnoreComponentWhenMoving; + internal static delegate* unmanaged[Cdecl] clearMoveIgnoreActors; + internal static delegate* unmanaged[Cdecl] clearMoveIgnoreComponents; + internal static delegate* unmanaged[Cdecl] createAndSetMaterialInstanceDynamic; + internal static delegate* unmanaged[Cdecl] registerEvent; + internal static delegate* unmanaged[Cdecl] unregisterEvent; + } + + unsafe partial class ShapeComponent { + internal static delegate* unmanaged[Cdecl] getDynamicObstacle; + internal static delegate* unmanaged[Cdecl] getShapeColor; + internal static delegate* unmanaged[Cdecl] setDynamicObstacle; + internal static delegate* unmanaged[Cdecl] setShapeColor; + } + + unsafe partial class BoxComponent { + internal static delegate* unmanaged[Cdecl] getScaledBoxExtent; + internal static delegate* unmanaged[Cdecl] getUnscaledBoxExtent; + internal static delegate* unmanaged[Cdecl] setBoxExtent; + internal static delegate* unmanaged[Cdecl] initBoxExtent; + } + + unsafe partial class SphereComponent { + internal static delegate* unmanaged[Cdecl] getScaledSphereRadius; + internal static delegate* unmanaged[Cdecl] getUnscaledSphereRadius; + internal static delegate* unmanaged[Cdecl] getShapeScale; + internal static delegate* unmanaged[Cdecl] setSphereRadius; + internal static delegate* unmanaged[Cdecl] initSphereRadius; + } + + unsafe partial class CapsuleComponent { + internal static delegate* unmanaged[Cdecl] getScaledCapsuleRadius; + internal static delegate* unmanaged[Cdecl] getUnscaledCapsuleRadius; + internal static delegate* unmanaged[Cdecl] getShapeScale; + internal static delegate* unmanaged[Cdecl] getScaledCapsuleSize; + internal static delegate* unmanaged[Cdecl] getUnscaledCapsuleSize; + internal static delegate* unmanaged[Cdecl] setCapsuleRadius; + internal static delegate* unmanaged[Cdecl] setCapsuleSize; + internal static delegate* unmanaged[Cdecl] initCapsuleSize; + } + + unsafe partial class MeshComponent { + internal static delegate* unmanaged[Cdecl] isValidMaterialSlotName; + internal static delegate* unmanaged[Cdecl] getMaterialIndex; + } + + unsafe partial class TextRenderComponent { + internal static delegate* unmanaged[Cdecl] setFont; + internal static delegate* unmanaged[Cdecl] setText; + internal static delegate* unmanaged[Cdecl] setTextMaterial; + internal static delegate* unmanaged[Cdecl] setTextRenderColor; + internal static delegate* unmanaged[Cdecl] setHorizontalAlignment; + internal static delegate* unmanaged[Cdecl] setHorizontalSpacingAdjustment; + internal static delegate* unmanaged[Cdecl] setVerticalAlignment; + internal static delegate* unmanaged[Cdecl] setVerticalSpacingAdjustment; + internal static delegate* unmanaged[Cdecl] setScale; + internal static delegate* unmanaged[Cdecl] setWorldSize; + } + + unsafe partial class LightComponentBase { + internal static delegate* unmanaged[Cdecl] getIntensity; + internal static delegate* unmanaged[Cdecl] getCastShadows; + internal static delegate* unmanaged[Cdecl] setCastShadows; + } + + unsafe partial class LightComponent { + internal static delegate* unmanaged[Cdecl] setIntensity; + internal static delegate* unmanaged[Cdecl] setLightColor; + } + + unsafe partial class DirectionalLightComponent { } + + unsafe partial class MotionControllerComponent { + internal static delegate* unmanaged[Cdecl] isTracked; + internal static delegate* unmanaged[Cdecl] getDisplayDeviceModel; + internal static delegate* unmanaged[Cdecl] getDisableLowLatencyUpdate; + internal static delegate* unmanaged[Cdecl] getTrackingSource; + internal static delegate* unmanaged[Cdecl] setDisplayDeviceModel; + internal static delegate* unmanaged[Cdecl] setDisableLowLatencyUpdate; + internal static delegate* unmanaged[Cdecl] setTrackingSource; + internal static delegate* unmanaged[Cdecl] setTrackingMotionSource; + internal static delegate* unmanaged[Cdecl] setAssociatedPlayerIndex; + internal static delegate* unmanaged[Cdecl] setCustomDisplayMesh; + internal static delegate* unmanaged[Cdecl] setDisplayModelSource; + } + + unsafe partial class StaticMeshComponent { + internal static delegate* unmanaged[Cdecl] getLocalBounds; + internal static delegate* unmanaged[Cdecl] getStaticMesh; + internal static delegate* unmanaged[Cdecl] setStaticMesh; + } + + unsafe partial class InstancedStaticMeshComponent { + internal static delegate* unmanaged[Cdecl] getInstanceCount; + internal static delegate* unmanaged[Cdecl] getInstanceTransform; + internal static delegate* unmanaged[Cdecl] addInstance; + internal static delegate* unmanaged[Cdecl] addInstances; + internal static delegate* unmanaged[Cdecl] updateInstanceTransform; + internal static delegate* unmanaged[Cdecl] batchUpdateInstanceTransforms; + internal static delegate* unmanaged[Cdecl] removeInstance; + internal static delegate* unmanaged[Cdecl] clearInstances; + } + + unsafe partial class HierarchicalInstancedStaticMeshComponent { + internal static delegate* unmanaged[Cdecl] getDisableCollision; + internal static delegate* unmanaged[Cdecl] setDisableCollision; + } + + unsafe partial class SkinnedMeshComponent { + internal static delegate* unmanaged[Cdecl] getBonesNumber; + internal static delegate* unmanaged[Cdecl] getBoneIndex; + internal static delegate* unmanaged[Cdecl] getBoneName; + internal static delegate* unmanaged[Cdecl] getBoneTransform; + internal static delegate* unmanaged[Cdecl] setSkeletalMesh; + } + + unsafe partial class SkeletalMeshComponent { + internal static delegate* unmanaged[Cdecl] isPlaying; + internal static delegate* unmanaged[Cdecl] getAnimationInstance; + internal static delegate* unmanaged[Cdecl] setAnimation; + internal static delegate* unmanaged[Cdecl] setAnimationMode; + internal static delegate* unmanaged[Cdecl] setAnimationBlueprint; + internal static delegate* unmanaged[Cdecl] play; + internal static delegate* unmanaged[Cdecl] playAnimation; + internal static delegate* unmanaged[Cdecl] stop; + } + + unsafe partial class SplineComponent { + internal static delegate* unmanaged[Cdecl] isClosedLoop; + internal static delegate* unmanaged[Cdecl] getDuration; + internal static delegate* unmanaged[Cdecl] getSplinePointType; + internal static delegate* unmanaged[Cdecl] getSplinePointsNumber; + internal static delegate* unmanaged[Cdecl] getSplineSegmentsNumber; + internal static delegate* unmanaged[Cdecl] getTangentAtDistanceAlongSpline; + internal static delegate* unmanaged[Cdecl] getTangentAtSplinePoint; + internal static delegate* unmanaged[Cdecl] getTangentAtTime; + internal static delegate* unmanaged[Cdecl] getTransformAtDistanceAlongSpline; + internal static delegate* unmanaged[Cdecl] getTransformAtSplinePoint; + internal static delegate* unmanaged[Cdecl] getArriveTangentAtSplinePoint; + internal static delegate* unmanaged[Cdecl] getDefaultUpVector; + internal static delegate* unmanaged[Cdecl] getDirectionAtDistanceAlongSpline; + internal static delegate* unmanaged[Cdecl] getDirectionAtSplinePoint; + internal static delegate* unmanaged[Cdecl] getDirectionAtTime; + internal static delegate* unmanaged[Cdecl] getDistanceAlongSplineAtSplinePoint; + internal static delegate* unmanaged[Cdecl] getLeaveTangentAtSplinePoint; + internal static delegate* unmanaged[Cdecl] getLocationAndTangentAtSplinePoint; + internal static delegate* unmanaged[Cdecl] getLocationAtDistanceAlongSpline; + internal static delegate* unmanaged[Cdecl] getLocationAtSplinePoint; + internal static delegate* unmanaged[Cdecl] getLocationAtTime; + internal static delegate* unmanaged[Cdecl] getRightVectorAtDistanceAlongSpline; + internal static delegate* unmanaged[Cdecl] getRightVectorAtSplinePoint; + internal static delegate* unmanaged[Cdecl] getRightVectorAtTime; + internal static delegate* unmanaged[Cdecl] getRollAtDistanceAlongSpline; + internal static delegate* unmanaged[Cdecl] getRollAtSplinePoint; + internal static delegate* unmanaged[Cdecl] getRollAtTime; + internal static delegate* unmanaged[Cdecl] getRotationAtDistanceAlongSpline; + internal static delegate* unmanaged[Cdecl] getRotationAtSplinePoint; + internal static delegate* unmanaged[Cdecl] getRotationAtTime; + internal static delegate* unmanaged[Cdecl] getScaleAtDistanceAlongSpline; + internal static delegate* unmanaged[Cdecl] getScaleAtSplinePoint; + internal static delegate* unmanaged[Cdecl] getScaleAtTime; + internal static delegate* unmanaged[Cdecl] getSplineLength; + internal static delegate* unmanaged[Cdecl] getTransformAtTime; + internal static delegate* unmanaged[Cdecl] getUpVectorAtDistanceAlongSpline; + internal static delegate* unmanaged[Cdecl] getUpVectorAtSplinePoint; + internal static delegate* unmanaged[Cdecl] getUpVectorAtTime; + internal static delegate* unmanaged[Cdecl] setDuration; + internal static delegate* unmanaged[Cdecl] setSplinePointType; + internal static delegate* unmanaged[Cdecl] setClosedLoop; + internal static delegate* unmanaged[Cdecl] setDefaultUpVector; + internal static delegate* unmanaged[Cdecl] setLocationAtSplinePoint; + internal static delegate* unmanaged[Cdecl] setTangentAtSplinePoint; + internal static delegate* unmanaged[Cdecl] setTangentsAtSplinePoint; + internal static delegate* unmanaged[Cdecl] setUpVectorAtSplinePoint; + internal static delegate* unmanaged[Cdecl] addSplinePoint; + internal static delegate* unmanaged[Cdecl] addSplinePointAtIndex; + internal static delegate* unmanaged[Cdecl] clearSplinePoints; + internal static delegate* unmanaged[Cdecl] findDirectionClosestToWorldLocation; + internal static delegate* unmanaged[Cdecl] findLocationClosestToWorldLocation; + internal static delegate* unmanaged[Cdecl] findUpVectorClosestToWorldLocation; + internal static delegate* unmanaged[Cdecl] findRightVectorClosestToWorldLocation; + internal static delegate* unmanaged[Cdecl] findRollClosestToWorldLocation; + internal static delegate* unmanaged[Cdecl] findScaleClosestToWorldLocation; + internal static delegate* unmanaged[Cdecl] findTangentClosestToWorldLocation; + internal static delegate* unmanaged[Cdecl] findTransformClosestToWorldLocation; + internal static delegate* unmanaged[Cdecl] removeSplinePoint; + internal static delegate* unmanaged[Cdecl] updateSpline; + } + + unsafe partial class RadialForceComponent { + internal static delegate* unmanaged[Cdecl] getIgnoreOwningActor; + internal static delegate* unmanaged[Cdecl] getImpulseVelocityChange; + internal static delegate* unmanaged[Cdecl] getLinearFalloff; + internal static delegate* unmanaged[Cdecl] getForceStrength; + internal static delegate* unmanaged[Cdecl] getImpulseStrength; + internal static delegate* unmanaged[Cdecl] getRadius; + internal static delegate* unmanaged[Cdecl] setIgnoreOwningActor; + internal static delegate* unmanaged[Cdecl] setImpulseVelocityChange; + internal static delegate* unmanaged[Cdecl] setLinearFalloff; + internal static delegate* unmanaged[Cdecl] setForceStrength; + internal static delegate* unmanaged[Cdecl] setImpulseStrength; + internal static delegate* unmanaged[Cdecl] setRadius; + internal static delegate* unmanaged[Cdecl] addCollisionChannelToAffect; + internal static delegate* unmanaged[Cdecl] fireImpulse; + } + + unsafe partial class MaterialInterface { + internal static delegate* unmanaged[Cdecl] isTwoSided; + } + + unsafe partial class Material { + internal static delegate* unmanaged[Cdecl] isDefaultMaterial; + } + + unsafe partial class MaterialInstance { + internal static delegate* unmanaged[Cdecl] isChildOf; + internal static delegate* unmanaged[Cdecl] getParent; + } + + unsafe partial class MaterialInstanceDynamic { + internal static delegate* unmanaged[Cdecl] clearParameterValues; + internal static delegate* unmanaged[Cdecl] setTextureParameterValue; + internal static delegate* unmanaged[Cdecl] setVectorParameterValue; + internal static delegate* unmanaged[Cdecl] setScalarParameterValue; + } +} + +``` + +`Source/Managed/Framework/Framework.cs`: + +```cs +/* + * Unreal Engine .NET 6 integration + * Copyright (c) 2021 Stanislav Denisov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Text; + +namespace UnrealEngine.Framework { + // Internal + + internal static class ArrayPool { + [ThreadStatic] + private static byte[] stringBuffer; + + public static byte[] GetStringBuffer() { + if (stringBuffer == null) + stringBuffer = GC.AllocateUninitializedArray(8192, pinned: true); + + Array.Clear(stringBuffer, 0, stringBuffer.Length); + + return stringBuffer; + } + } + + internal static class Collector { + [ThreadStatic] + private static List references; + + public static IntPtr GetFunctionPointer(Delegate reference) { + if (references == null) + references = new(); + + references.Add(reference); + + return Marshal.GetFunctionPointerForDelegate(reference); + } + } + + internal static class Extensions { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static T GetOrAdd(this IDictionary dictionary, S key, Func valueCreator) => dictionary.TryGetValue(key, out var value) ? value : dictionary[key] = valueCreator(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static byte[] StringToBytes(this string value) { + if (value != null) + return Encoding.UTF8.GetBytes(value); + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string BytesToString(this byte[] buffer) { + int end; + + for (end = 0; end < buffer.Length && buffer[end] != 0; end++); + + unsafe { + fixed (byte* pinnedBuffer = buffer) { + return new((sbyte*)pinnedBuffer, 0, end); + } + } + } + } + + internal static class Tables { + // Table for fast conversion from the color to a linear color + internal static readonly float[] Color = new float[256] { + 0.0f, + 0.000303526983548838f, 0.000607053967097675f, 0.000910580950646512f, 0.00121410793419535f, 0.00151763491774419f, + 0.00182116190129302f, 0.00212468888484186f, 0.0024282158683907f, 0.00273174285193954f, 0.00303526983548838f, + 0.00334653564113713f, 0.00367650719436314f, 0.00402471688178252f, 0.00439144189356217f, 0.00477695332960869f, + 0.005181516543916f, 0.00560539145834456f, 0.00604883284946662f, 0.00651209061157708f, 0.00699540999852809f, + 0.00749903184667767f, 0.00802319278093555f, 0.0085681254056307f, 0.00913405848170623f, 0.00972121709156193f, + 0.0103298227927056f, 0.0109600937612386f, 0.0116122449260844f, 0.012286488094766f, 0.0129830320714536f, + 0.0137020827679224f, 0.0144438433080002f, 0.0152085141260192f, 0.0159962930597398f, 0.0168073754381669f, + 0.0176419541646397f, 0.0185002197955389f, 0.0193823606149269f, 0.0202885627054049f, 0.0212190100154473f, + 0.0221738844234532f, 0.02315336579873f, 0.0241576320596103f, 0.0251868592288862f, 0.0262412214867272f, + 0.0273208912212394f, 0.0284260390768075f, 0.0295568340003534f, 0.0307134432856324f, 0.0318960326156814f, + 0.0331047661035236f, 0.0343398063312275f, 0.0356013143874111f, 0.0368894499032755f, 0.0382043710872463f, + 0.0395462347582974f, 0.0409151963780232f, 0.0423114100815264f, 0.0437350287071788f, 0.0451862038253117f, + 0.0466650857658898f, 0.0481718236452158f, 0.049706565391714f, 0.0512694577708345f, 0.0528606464091205f, + 0.0544802758174765f, 0.0561284894136735f, 0.0578054295441256f, 0.0595112375049707f, 0.0612460535624849f, + 0.0630100169728596f, 0.0648032660013696f, 0.0666259379409563f, 0.0684781691302512f, 0.070360094971063f, + 0.0722718499453493f, 0.0742135676316953f, 0.0761853807213167f, 0.0781874210336082f, 0.0802198195312533f, + 0.0822827063349132f, 0.0843762107375113f, 0.0865004612181274f, 0.0886555854555171f, 0.0908417103412699f, + 0.0930589619926197f, 0.0953074657649191f, 0.0975873462637915f, 0.0998987273569704f, 0.102241732185838f, + 0.104616483176675f, 0.107023102051626f, 0.109461709839399f, 0.1119324268857f, 0.114435372863418f, + 0.116970666782559f, 0.119538426999953f, 0.122138771228724f, 0.124771816547542f, 0.127437679409664f, + 0.130136475651761f, 0.132868320502552f, 0.135633328591233f, 0.138431613955729f, 0.141263290050755f, + 0.144128469755705f, 0.147027265382362f, 0.149959788682454f, 0.152926150855031f, 0.155926462553701f, + 0.158960833893705f, 0.162029374458845f, 0.16513219330827f, 0.168269398983119f, 0.171441099513036f, + 0.174647402422543f, 0.17788841473729f, 0.181164242990184f, 0.184474993227387f, 0.187820771014205f, + 0.191201681440861f, 0.194617829128147f, 0.198069318232982f, 0.201556252453853f, 0.205078735036156f, + 0.208636868777438f, 0.212230756032542f, 0.215860498718652f, 0.219526198320249f, 0.223227955893977f, + 0.226965872073417f, 0.23074004707378f, 0.23455058069651f, 0.238397572333811f, 0.242281120973093f, + 0.246201325201334f, 0.250158283209375f, 0.254152092796134f, 0.258182851372752f, 0.262250655966664f, + 0.266355603225604f, 0.270497789421545f, 0.274677310454565f, 0.278894261856656f, 0.283148738795466f, + 0.287440836077983f, 0.291770648154158f, 0.296138269120463f, 0.300543792723403f, 0.304987312362961f, + 0.309468921095997f, 0.313988711639584f, 0.3185467763743f, 0.323143207347467f, 0.32777809627633f, + 0.332451534551205f, 0.337163613238559f, 0.341914423084057f, 0.346704054515559f, 0.351532597646068f, + 0.356400142276637f, 0.361306777899234f, 0.36625259369956f, 0.371237678559833f, 0.376262121061519f, + 0.381326009488037f, 0.386429431827418f, 0.39157247577492f, 0.396755228735618f, 0.401977777826949f, + 0.407240209881218f, 0.41254261144808f, 0.417885068796976f, 0.423267667919539f, 0.428690494531971f, + 0.434153634077377f, 0.439657171728079f, 0.445201192387887f, 0.450785780694349f, 0.456411021020965f, + 0.462076997479369f, 0.467783793921492f, 0.473531493941681f, 0.479320180878805f, 0.485149937818323f, + 0.491020847594331f, 0.496932992791578f, 0.502886455747457f, 0.50888131855397f, 0.514917663059676f, + 0.520995570871595f, 0.527115123357109f, 0.533276401645826f, 0.539479486631421f, 0.545724458973463f, + 0.552011399099209f, 0.558340387205378f, 0.56471150325991f, 0.571124827003694f, 0.577580437952282f, + 0.584078415397575f, 0.590618838409497f, 0.597201785837643f, 0.603827336312907f, 0.610495568249093f, + 0.617206559844509f, 0.623960389083534f, 0.630757133738175f, 0.637596871369601f, 0.644479679329661f, + 0.651405634762384f, 0.658374814605461f, 0.665387295591707f, 0.672443154250516f, 0.679542466909286f, + 0.686685309694841f, 0.693871758534824f, 0.701101889159085f, 0.708375777101046f, 0.71569349769906f, + 0.723055126097739f, 0.730460737249286f, 0.737910405914797f, 0.745404206665559f, 0.752942213884326f, + 0.760524501766589f, 0.768151144321824f, 0.775822215374732f, 0.783537788566466f, 0.791297937355839f, + 0.799102735020525f, 0.806952254658248f, 0.81484656918795f, 0.822785751350956f, 0.830769873712124f, + 0.838799008660978f, 0.846873228412837f, 0.854992605009927f, 0.863157210322481f, 0.871367116049835f, + 0.879622393721502f, 0.887923114698241f, 0.896269350173118f, 0.904661171172551f, 0.913098648557343f, + 0.921581853023715f, 0.930110855104312f, 0.938685725169219f, 0.947306533426946f, 0.955973349925421f, + 0.964686244552961f, 0.973445287039244f, 0.982250546956257f, 0.991102093719252f, 1.0f + }; + } + + // Public + + /// + /// Defines the log level for an output log message + /// + public enum LogLevel : int { + /// + /// Logs are printed to console and log files + /// + Display, + /// + /// Logs are printed to console and log files with the yellow color + /// + Warning, + /// + /// Logs are printed to console and log files with the red color + /// + Error, + /// + /// Logs are printed to console and log files then crashes the application even if logging is disabled + /// + Fatal + } + + /// + /// Defines rules for attaching components + /// + public enum AttachmentTransformRule : int { + /// + /// Keeps current relative transform as the relative transform to the new parent + /// + KeepRelativeTransform, + /// + /// Automatically calculates the relative transform such that the attached component maintains the same world transform + /// + KeepWorldTransform, + /// + /// Snaps location and rotation to the attach point, calculates the relative scale so that the final world scale of the component remains the same + /// + SnapToTargetIncludingScale, + /// + /// Snaps the entire transform to target, including scale + /// + SnapToTargetNotIncludingScale + } + + /// + /// Defines rules for detaching components + /// + public enum DetachmentTransformRule : int { + /// + /// Keeps current relative transform as the relative transform to the previous parent + /// + KeepRelativeTransform, + /// + /// Automatically calculates the relative transform such that the detached component maintains the same world transform + /// + KeepWorldTransform + } + + /// + /// Defines teleportation types of physics body + /// + public enum TeleportType : byte { + /// + /// Don't teleport physics body + /// + None, + /// + /// Teleport physics body so that velocity remains the same and no collision occurs + /// + TeleportPhysics, + /// + /// Teleport physics body and reset physics state completely + /// + ResetPhysics + } + + /// + /// Defines how to update transform during movement + /// + [Flags] + public enum UpdateTransformFlags : int { + /// + /// Default update + /// + None = 0, + /// + /// Don't update the underlying physics + /// + SkipPhysicsUpdate = 1 << 0, + /// + /// The update is coming as a result of the parent updating + /// + PropagateFromParent = 1 << 1, + /// + /// Only update child transform if attached to parent via a socket + /// + OnlyUpdateIfUsingSocket = 1 << 2 + } + + /// + /// Defines focus priority for AI + /// + public enum AIFocusPriority : int { + /// + Normal = 0, + /// + Low = 1, + /// + High = 2, + /// + VeryHigh = 3 + } + + /// + /// Defines actor events + /// + public enum ActorEventType : int { + /// + /// Called when actors start overlapping + /// + OnActorBeginOverlap, + /// + /// Called when actors stop overlapping + /// + OnActorEndOverlap, + /// + /// Called when actors hit collisions + /// + OnActorHit, + /// + /// Called when the mouse cursor is moved over an actor if mouse over events are enabled in the player controller + /// + OnActorBeginCursorOver, + /// + /// Called when the mouse cursor is moved off an actor if mouse over events are enabled in the player controller + /// + OnActorEndCursorOver, + /// + /// Called when the mouse button is clicked while the mouse is over an actor if click events are enabled in the player controller + /// + OnActorClicked, + /// + /// Called when the mouse button is released while the mouse is over an actor if click events are enabled in the player controller + /// + OnActorReleased + } + + /// + /// Defines component events + /// + public enum ComponentEventType : int { + /// + /// Called when components start overlapping + /// + OnComponentBeginOverlap, + /// + /// Called when components stop overlapping + /// + OnComponentEndOverlap, + /// + /// Called when components hit collisions + /// + OnComponentHit, + /// + /// Called when the mouse cursor is moved over a component and mouse over events are enabled in the player controller + /// + OnComponentBeginCursorOver, + /// + /// Called when the mouse cursor is moved off a component and mouse over events are enabled in the player controller + /// + OnComponentEndCursorOver, + /// + /// Called when the mouse button is clicked while the mouse is over a component if click events are enabled in the player controller + /// + OnComponentClicked, + /// + /// Called when the mouse button is released while the mouse is over a component if click events are enabled in the player controller + /// + OnComponentReleased + } + + /// + /// Defines the animation mode + /// + public enum AnimationMode : int { + /// + Blueprint, + /// + Asset, + /// + Custom + } + + /// + /// Defines the player index that will be used to pass input + /// + public enum AutoReceiveInput : int { + /// + Disabled, + /// + Player0, + /// + Player1, + /// + Player2, + /// + Player3, + /// + Player4, + /// + Player5, + /// + Player6, + /// + Player7 + } + + /// + /// Defines the projection mode for a camera + /// + public enum CameraProjectionMode : int { + /// + Perspective, + /// + Orthographic + } + + /// + /// Defines the collision mode + /// + public enum CollisionMode : int { + /// + /// No collision + /// + NoCollision, + /// + /// Used for spatial queries (raycasts, sweeps, and overlaps) + /// + QueryOnly, + /// + /// Used for physics simulation (rigid bodies, and constraints) + /// + PhysicsOnly, + /// + /// Can be used for both spatial queries and physics simulation + /// + QueryAndPhysics + } + + /// + /// Defines the collision shape type + /// + public enum CollisionShapeType : int { + /// + Box = 1, + /// + Sphere = 2, + /// + Capsule = 3 + } + + /// + /// Defines how often a component is allowed to move + /// + public enum ComponentMobility : int { + /// + /// Movable objects can be moved and changed + /// + Movable, + /// + /// Static objects cannot be moved or changed + /// - Allows baked lighting + /// - Fastest rendering + /// + Static, + /// + /// A stationary light will only have its shadowing and bounced lighting from static geometry baked by Lightmass, all other lighting will be dynamic + /// + Stationary + } + + /// + /// Defines coordinate space + /// + public enum SplineCoordinateSpace : int { + /// + Local, + /// + World + } + + /// + /// Defines the spline point type + /// + public enum SplinePointType : int { + /// + Linear, + /// + Curve, + /// + Constant, + /// + CurveClamped, + /// + CurveCustomTangent + } + + /// + /// Defines the window mode + /// + public enum WindowMode : int { + /// + Fullscreen, + /// + WindowedFullscreen, + /// + Windowed + } + + /// + /// Defines the type of fade to adjust audio volume + /// + public enum AudioFadeCurve : byte { + /// + Linear, + /// + Logarithmic, + /// + SCurve, + /// + Sin + } + + /// + /// Defines the possession type for AI pawn that will be automatically possed by an AI controller + /// + public enum AutoPossessAI : byte { + /// + /// Disabled and not possesses AI + /// + Disabled, + /// + /// Only possess by an AI controller if a pawn is placed in the world + /// + PlacedInWorld, + /// + /// Only possess by an AI controller if a pawn is spawned after the world has loaded + /// + Spawned, + /// + /// Pawn is automatically possessed by an AI controller whenever it's created + /// + PlacedInWorldOrSpawned + } + + /// + /// Defines how to blend when changing view targets + /// + public enum BlendType : int { + /// + /// A simple linear interpolation + /// + Linear, + /// + /// A slight ease in and ease out, but amount of ease cannot be tweaked + /// + Cubic, + /// + /// Immediately accelerates, but smoothly decelerates into the target, ease amount can be controlled + /// + EaseIn, + /// + /// Smoothly accelerates, but does not decelerate into the target, ease amount can be controlled + /// + EaseOut, + /// + /// Smoothly accelerates and decelerates, ease amount can be controlled + /// + EaseInOut, + /// + /// The game's camera system has already performed the blending, the engine shouldn't blend at all + /// + PreBlended + } + + /// + /// Defines collision channels + /// + public enum CollisionChannel : int { + /// + WorldStatic, + /// + WorldDynamic, + /// + Pawn, + /// + Visibility, + /// + Camera, + /// + PhysicsBody, + /// + Vehicle, + /// + Destructible, + /// + EngineTraceChannel1, + /// + EngineTraceChannel2, + /// + EngineTraceChannel3, + /// + EngineTraceChannel4, + /// + EngineTraceChannel5, + /// + EngineTraceChannel6, + /// + GameTraceChannel1, + /// + GameTraceChannel2, + /// + GameTraceChannel3, + /// + GameTraceChannel4, + /// + GameTraceChannel5, + /// + GameTraceChannel6, + /// + GameTraceChannel7, + /// + GameTraceChannel8, + /// + GameTraceChannel9, + /// + GameTraceChannel10, + /// + GameTraceChannel11, + /// + GameTraceChannel12, + /// + GameTraceChannel13, + /// + GameTraceChannel14, + /// + GameTraceChannel15, + /// + GameTraceChannel16, + /// + GameTraceChannel17, + /// + GameTraceChannel18 + } + + /// + /// Defines collision response types + /// + public enum CollisionResponse : int { + /// + Ignore, + /// + Overlap, + /// + Block + } + + /// + /// Defines the controller hands for tracking + /// + public enum ControllerHand : byte { + /// + Left, + /// + Right, + /// + AnyHand, + /// + Pad, + /// + ExternalCamera, + /// + Gun, + /// + Special1, + /// + Special2, + /// + Special3, + /// + Special4, + /// + Special5, + /// + Special6, + /// + Special7, + /// + Special8, + /// + Special9, + /// + Special10, + /// + Special11 + } + + /// + /// Defines the horizontal text aligment type + /// + public enum HorizontalTextAligment : int { + /// + Left, + /// + Center, + /// + Right + } + + /// + /// Defines the vertical text aligment type + /// + public enum VerticalTextAligment : int { + /// + Top, + /// + Center, + /// + Bottom + } + + /// + /// Defines behavior when movement is restricted to a 2D plane defined by a specific axis/normal, so that movement along the locked axis will not be possible + /// + public enum PlaneConstraintAxis : byte { + /// + /// Lock movement to a user-defined axis + /// + Custom, + /// + /// Lock movement in the X axis + /// + X, + /// + /// Lock movement in the Y axis + /// + Y, + /// + /// Lock movement in the Z axis + /// + Z, + /// + /// Use the global physics project setting + /// + UseGlobalPhysicsSetting + } + + /// + /// Defines input behavior type + /// + public enum InputEvent : int { + /// + /// Key pressed + /// + Pressed = 0, + /// + /// Key released + /// + Released = 1, + /// + /// Key repeating + /// + Repeat = 2, + /// + /// Key double clicked + /// + DoubleClick = 3, + /// + /// Axis activated + /// + Axis = 4 + } + + /// + /// Defines the networking mode + /// + public enum NetMode : byte { + /// + /// A game without networking, with one or more local players + /// + Standalone, + /// + /// A server with no local players + /// + DedicatedServer, + /// + /// A server that also has a local player who is hosting the game, available to other players on the network + /// + ListenServer, + /// + /// A client connected to a server + /// + Client + } + + /// + /// Defines the pixel format + /// + public enum PixelFormat : int { + /// + Unknown = 0, + /// + A32B32G32R32F = 1, + /// + B8G8R8A8 = 2, + /// + G8 = 3, + /// + G16 = 4, + /// + DXT1 = 5, + /// + DXT3 = 6, + /// + DXT5 = 7, + /// + UYVY = 8, + /// + FloatRGB = 9, + /// + FloatRGBA = 10, + /// + DepthStencil = 11, + /// + ShadowDepth = 12, + /// + R32Float = 13, + /// + G16R16 = 14, + /// + G16R16F = 15, + /// + G16R16FFilter = 16, + /// + G32R32F = 17, + /// + A2B10G10R10 = 18, + /// + A16B16G16R16 = 19, + /// + D24 = 20, + /// + R16F = 21, + /// + R16FFilter = 22, + /// + BC5 = 23, + /// + V8U8 = 24, + /// + A1 = 25, + /// + FloatR11G11B10 = 26, + /// + A8 = 27, + /// + R32UInt = 28, + /// + R32SInt = 29, + /// + PVRTC2 = 30, + /// + PVRTC4 = 31, + /// + R16UInt = 32, + /// + R16SInt = 33, + /// + R16G16B16A16UInt = 34, + /// + R16G16B16A16SInt = 35, + /// + R5G6B5UNorm = 36, + /// + R8G8B8A8 = 37, + /// + A8R8G8B8 = 38, + /// + BC4 = 39, + /// + R8G8 = 40, + /// + ATCRGB = 41, + /// + ATCRGBAE = 42, + /// + ATCRGBAI = 43, + /// + X24G8 = 44, + /// + ETC1 = 45, + /// + ETC2RGB = 46, + /// + ETC2RGBA = 47, + /// + R32G32B32A32UInt = 48, + /// + R16G16UInt = 49, + /// + ASTC4x4 = 50, + /// + ASTC6x6 = 51, + /// + ASTC8x8 = 52, + /// + ASTC10x10 = 53, + /// + ASTC12x12 = 54, + /// + BC6H = 55, + /// + BC7 = 56, + /// + R8UInt = 57, + /// + L8 = 58, + /// + XGXR8 = 59, + /// + R8G8B8A8UInt = 60, + /// + R8G8B8A8SNorm = 61, + /// + R16G16B16A16UNorm = 62, + /// + R16G16B16A16SNorm = 63, + /// + PLATFORMHDR0 = 64, + /// + PLATFORMHDR1 = 65, + /// + PLATFORMHDR2 = 66, + /// + NV12 = 67, + /// + R32G32UInt = 68, + /// + ETC2R11EAC = 69, + /// + ETC2RG11EAC = 70, + /// + R8 = 71 + } + + /// + /// A representation of the engine's object reference + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct ObjectReference : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + /// + /// Tests for equality between two objects + /// + public static bool operator ==(ObjectReference left, ObjectReference right) => left.Equals(right); + + /// + /// Tests for inequality between two objects + /// + public static bool operator !=(ObjectReference left, ObjectReference right) => !left.Equals(right); + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero && Object.isValid(pointer); + + /// + /// Returns the unique ID of the object, reused by the engine, only unique while the object is alive + /// + public uint ID => Object.getID(Pointer); + + /// + /// Returns the name of the object + /// + public string Name { + get { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + Object.getName(Pointer, stringBuffer); + + return stringBuffer.BytesToString(); + } + } + + /// + /// Indicates equality of objects + /// + public bool Equals(ObjectReference other) => IsCreated && pointer == other.pointer; + + /// + /// Indicates equality of objects + /// + public override bool Equals(object value) { + if (value == null) + return false; + + if (!ReferenceEquals(value.GetType(), typeof(ObjectReference))) + return false; + + return Equals((ObjectReference)value); + } + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Converts the object reference to the actor of the specified class + /// + /// An actor or null on failure + public T ToActor() where T : Actor { + T actor = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = Object.toActor(Pointer, actor.Type); + + if (pointer != IntPtr.Zero) { + actor.Pointer = pointer; + + return actor; + } + + return null; + } + + /// + /// Converts the object reference to the component of the specified class + /// + /// A component or null on failure + public T ToComponent() where T : ActorComponent { + T component = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = Object.toComponent(Pointer, component.Type); + + if (pointer != IntPtr.Zero) { + component.Pointer = pointer; + + return component; + } + + return null; + } + } + + /// + /// A representation of the engine's actor reference + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct ActorReference : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsSpawned) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + /// + /// Tests for equality between two objects + /// + public static bool operator ==(ActorReference left, ActorReference right) => left.Equals(right); + + /// + /// Tests for inequality between two objects + /// + public static bool operator !=(ActorReference left, ActorReference right) => !left.Equals(right); + + /// + /// Returns true if the actor is spawned + /// + public bool IsSpawned => pointer != IntPtr.Zero && !Object.isPendingKill(pointer); + + /// + /// Returns the unique ID of the object, reused by the engine, only unique while the object is alive + /// + public uint ID => Object.getID(Pointer); + + /// + /// Returns the name of the object + /// + public string Name { + get { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + Object.getName(Pointer, stringBuffer); + + return stringBuffer.BytesToString(); + } + } + + /// + /// Indicates equality of objects + /// + public bool Equals(ActorReference other) => IsSpawned && pointer == other.pointer; + + /// + /// Indicates equality of objects + /// + public override bool Equals(object value) { + if (value == null) + return false; + + if (!ReferenceEquals(value.GetType(), typeof(ActorReference))) + return false; + + return Equals((ActorReference)value); + } + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Converts the actor reference to the actor of the specified class + /// + /// An actor or null on failure + public T ToActor() where T : Actor { + T actor = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = Object.toActor(Pointer, actor.Type); + + if (pointer != IntPtr.Zero) { + actor.Pointer = pointer; + + return actor; + } + + return null; + } + } + + /// + /// A representation of the engine's component reference + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct ComponentReference : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + /// + /// Tests for equality between two objects + /// + public static bool operator ==(ComponentReference left, ComponentReference right) => left.Equals(right); + + /// + /// Tests for inequality between two objects + /// + public static bool operator !=(ComponentReference left, ComponentReference right) => !left.Equals(right); + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero && Object.isValid(pointer); + + /// + /// Returns the unique ID of the object, reused by the engine, only unique while the object is alive + /// + public uint ID => Object.getID(Pointer); + + /// + /// Returns the name of the object + /// + public string Name { + get { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + Object.getName(Pointer, stringBuffer); + + return stringBuffer.BytesToString(); + } + } + + /// + /// Indicates equality of objects + /// + public bool Equals(ComponentReference other) => IsCreated && pointer == other.pointer; + + /// + /// Indicates equality of objects + /// + public override bool Equals(object value) { + if (value == null) + return false; + + if (!ReferenceEquals(value.GetType(), typeof(ComponentReference))) + return false; + + return Equals((ComponentReference)value); + } + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Converts the component reference to the component of the specified class + /// + /// A component or null on failure + public T ToComponent() where T : ActorComponent { + T component = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = Object.toComponent(Pointer, component.Type); + + if (pointer != IntPtr.Zero) { + component.Pointer = pointer; + + return component; + } + + return null; + } + } + + /// + /// A linear 32-bit floating-point RGBA color + /// + public partial struct LinearColor : IEquatable { + /// + /// Initializes a new instance the linear color + /// + public LinearColor(float red, float green, float blue, float alpha = 1.0f) { + r = red; + g = green; + b = blue; + a = alpha; + } + + /// + /// Initializes a new instance the linear color + /// + public LinearColor(Vector3 value, float alpha = 1.0f) { + r = value.X; + g = value.Y; + b = value.Z; + a = alpha; + } + + /// + /// Initializes a new instance the linear color + /// + public LinearColor(Vector4 value) { + r = value.X; + g = value.Y; + b = value.Z; + a = value.W; + } + + /// + /// Initializes a new instance the linear color + /// + public LinearColor(Color value) { + r = Tables.Color[value.R]; + g = Tables.Color[value.G]; + b = Tables.Color[value.B]; + a = Tables.Color[value.A]; + } + + /// + /// Initializes a new instance the linear color + /// + public LinearColor(float[] values) { + if (values == null) + throw new ArgumentNullException(nameof(values)); + + if (values.Length != 3 && values.Length != 4) + throw new ArgumentOutOfRangeException(nameof(values)); + + r = values[0]; + g = values[1]; + b = values[2]; + a = values.Length >= 4 ? values[3] : 1.0f; + } + + /// + /// Gets or sets the component at the specified index + /// + public float this[int index] { + get { + switch (index) { + case 0: return r; + case 1: return g; + case 2: return b; + case 3: return a; + } + + throw new ArgumentOutOfRangeException(nameof(index)); + } + + set { + switch (index) { + case 0: r = value; break; + case 1: g = value; break; + case 2: b = value; break; + case 3: a = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } + + /// + /// Gets or sets the red component of the linear color + /// + public float R { + get => r; + set => r = value; + } + + /// + /// Gets or sets the green component of the linear color + /// + public float G { + get => g; + set => g = value; + } + + /// + /// Gets or sets the blue component of the linear color + /// + public float B { + get => b; + set => b = value; + } + + /// + /// Gets or sets the alpha component of the linear color + /// + public float A { + get => a; + set => a = value; + } + + /// + /// The black color + /// + public static LinearColor Black => new(0.0f, 0.0f, 0.0f, 1.0f); + + /// + /// The blue color + /// + public static LinearColor Blue => new(0.0f, 0.0f, 1.0f, 1.0f); + + /// + /// The green color + /// + public static LinearColor Green => new(0.0f, 1.0f, 0.0f, 1.0f); + + /// + /// The grey color + /// + public static LinearColor Grey => new(0.5f, 0.5f, 0.5f, 1.0f); + + /// + /// The red color + /// + public static LinearColor Red => new(1.0f, 0.0f, 0.0f, 1.0f); + + /// + /// The white color + /// + public static LinearColor White => new(1.0f, 1.0f, 1.0f, 1.0f); + + /// + /// The yellow color + /// + public static LinearColor Yellow => new(1.0f, 1.0f, 0.0f, 1.0f); + + /// + /// Tests for equality between two objects + /// + public static bool operator ==(LinearColor left, LinearColor right) => left.Equals(right); + + /// + /// Tests for inequality between two objects + /// + public static bool operator !=(LinearColor left, LinearColor right) => !left.Equals(right); + + /// + /// Adds two colors + /// + public static LinearColor operator +(LinearColor left, LinearColor right) => new(left.r + right.r, left.g + right.g, left.b + right.b, left.a + right.a); + + /// + /// Subtracts two colors + /// + public static LinearColor operator -(LinearColor left, LinearColor right) => new(left.b - right.b, left.g - right.g, left.b - right.b, left.a - right.a); + + /// + /// Multiplies two colors + /// + public static LinearColor operator *(float scale, LinearColor value) => new(value.r * scale, value.g * scale, value.b * scale, value.a * scale); + + /// + /// Divides two colors + /// + public static LinearColor operator /(float scale, LinearColor value) => new(value.r / scale, value.g / scale, value.b / scale, value.a / scale); + + /// + /// Implicitly casts color instance to a linear color + /// + public static implicit operator LinearColor(Color value) => new(value); + + /// + /// Implicitly casts this instance to a string + /// + public static implicit operator string(LinearColor value) => value.ToString(); + + /// + /// Adds two colors + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static LinearColor Add(LinearColor left, LinearColor right) => new(left.r + right.r, left.g + right.g, left.b + right.b, left.a + right.a); + + /// + /// Subtracts two colors + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static LinearColor Subtract(LinearColor left, LinearColor right) => new(left.r - right.r, left.g - right.g, left.b - right.b, left.a - right.a); + + /// + /// Multiplies two colors + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static LinearColor Multiply(LinearColor left, LinearColor right) => new(left.r * right.r, left.g * right.g, left.b * right.b, left.a * right.a); + + /// + /// Divides two colors + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static LinearColor Divide(LinearColor left, LinearColor right) => new(left.r / right.r, left.g / right.g, left.b / right.b, left.a / right.a); + + /// + /// Performs a linear interpolation between two colors + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static LinearColor Lerp(LinearColor start, LinearColor end, float amount) => new(Maths.Lerp(start.r, end.r, amount), Maths.Lerp(start.g, end.g, amount), Maths.Lerp(start.b, end.b, amount), Maths.Lerp(start.a, end.a, amount)); + + /// + /// Converts the color into a linear color + /// + public static LinearColor FromColor(Color value) => new(value); + + /// + /// Converts the linear color into a three component vector + /// + public Vector3 ToVector3() => new(r, g, b); + + /// + /// Converts the linear color into a four component vector + /// + public Vector4 ToVector4() => new(r, g, b, a); + + /// + /// Creates an array containing the elements of the linear color + /// + public float[] ToArray() => new[] { r, g, b, a }; + + /// + /// Returns a string that represents this instance + /// + public override string ToString() => ToString(CultureInfo.CurrentCulture); + + /// + /// Returns a string that represents this instance + /// + public string ToString(IFormatProvider formatProvider) => string.Format(formatProvider, "R:{0} G:{1} B:{2} A:{3}", R, G, B, A); + + /// + /// Indicates equality of objects + /// + public bool Equals(LinearColor other) => r == other.r && g == other.g && b == other.b && a == other.a; + + /// + /// Indicates equality of objects + /// + public override bool Equals(object value) { + if (value == null) + return false; + + if (!ReferenceEquals(value.GetType(), typeof(LinearColor))) + return false; + + return Equals((LinearColor)value); + } + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => HashCode.Combine(r, g, b, a); + } + + /// + /// Transform composed of location, rotation, and scale + /// + public partial struct Transform : IEquatable { + /// + /// Initializes a new instance the transform + /// + public Transform(Vector3 location = default, Quaternion rotation = default, Vector3 scale = default) { + this.location = location; + this.rotation = rotation; + this.scale = scale; + } + + /// + /// Gets or sets the location component + /// + public Vector3 Location { + get => location; + set => location = value; + } + + /// + /// Gets or sets the rotation component + /// + public Quaternion Rotation { + get => rotation; + set => rotation = value; + } + + /// + /// Gets or sets the scale component + /// + public Vector3 Scale { + get => scale; + set => scale = value; + } + + /// + /// Tests for equality between two objects + /// + public static bool operator ==(Transform left, Transform right) => left.Equals(right); + + /// + /// Tests for inequality between two objects + /// + public static bool operator !=(Transform left, Transform right) => !left.Equals(right); + + /// + /// Implicitly casts this instance to a string + /// + public static implicit operator string(Transform value) => value.ToString(); + + /// + /// Returns a string that represents this instance + /// + public override string ToString() => ToString(CultureInfo.CurrentCulture); + + /// + /// Returns a string that represents this instance + /// + public string ToString(IFormatProvider formatProvider) => string.Format(formatProvider, "Location:{0} Rotation:{1} Scale:{2}", Location, Rotation, Scale); + + /// + /// Indicates equality of objects + /// + public bool Equals(Transform other) => location == other.location && rotation == other.rotation && scale == other.scale; + + /// + /// Indicates equality of objects + /// + public override bool Equals(object value) { + if (value == null) + return false; + + if (!ReferenceEquals(value.GetType(), typeof(Transform))) + return false; + + return Equals((Transform)value); + } + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => HashCode.Combine(location, rotation, scale); + } + + /// + /// A trace hit + /// + public partial struct Hit : IEquatable { + /// + /// Returns the location in world space where the moving shape would end up against the impacted object if there was a hit + /// + public Vector3 Location => location; + + /// + /// Returns the location in world space of the actual contact of the trace shape with the impacted object + /// + public Vector3 ImpactLocation => impactLocation; + + /// + /// Returns the normal of the hit in world space for the object that was swept + /// + public Vector3 Normal => normal; + + /// + /// Returns the normal of the hit in world space for the object that was hit by the sweep + /// + public Vector3 ImpactNormal => impactNormal; + + /// + /// Returns the start location of the trace + /// + public Vector3 TraceStart => traceStart; + + /// + /// Returns the end location of the trace + /// + public Vector3 TraceEnd => traceEnd; + + /// + /// Returns the impact along trace direction between 0.0f and 1.0f if there was a hit, indicating time between and + /// + public float Time => time; + + /// + /// Returns the distance from to in world space + /// + public float Distance => distance; + + /// + /// Returns the distance along with that will result in moving out of penetration if is true and a penetration vector can be computed + /// + public float PenetrationDepth => penetrationDepth; + + /// + /// Returns true if the hit was a result of blocking collision + /// + public bool BlockingHit => blockingHit; + + /// + /// Returns true if the trace started penetration + /// + public bool StartPenetrating => startPenetrating; + + /// + /// Returns the owner actor of the component that was hit or null on failure + /// + public Actor GetActor() { + if (actor != IntPtr.Zero) + return new(actor); + + return null; + } + + /// + /// Implicitly casts this instance to a string + /// + public static implicit operator string(Hit value) => value.ToString(); + + /// + /// Returns a string that represents this instance + /// + public override string ToString() => ToString(CultureInfo.CurrentCulture); + + /// + /// Returns a string that represents this instance + /// + public string ToString(IFormatProvider formatProvider) => string.Format(formatProvider, "Location:{0} ImpactLocation:{1} Normal:{2} ImpactNormal:{3}, TraceStart:{4}, TraceEnd:{5}, Time:{6}, Distance:{7}, PenetrationDepth:{8}, BlockingHit:{9}, StartPenetrating:{10}, Actor:{11}", Location, ImpactLocation, Normal, ImpactNormal, TraceStart, TraceEnd, Time, Distance, PenetrationDepth, BlockingHit, StartPenetrating, actor != IntPtr.Zero); + + /// + /// Tests for equality between two objects + /// + public static bool operator ==(Hit left, Hit right) => left.Equals(right); + + /// + /// Tests for inequality between two objects + /// + public static bool operator !=(Hit left, Hit right) => !left.Equals(right); + + /// + /// Indicates equality of objects + /// + public bool Equals(Hit other) => location == other.location && impactLocation == other.impactLocation && normal == other.normal && impactNormal == other.impactNormal && traceStart == other.traceStart && traceEnd == other.traceEnd && actor == other.actor && time == other.time && distance == other.distance && penetrationDepth == other.penetrationDepth && blockingHit == other.blockingHit && startPenetrating == other.startPenetrating; + + /// + /// Indicates equality of objects + /// + public override bool Equals(object value) { + if (value == null) + return false; + + if (!ReferenceEquals(value.GetType(), typeof(Hit))) + return false; + + return Equals((Hit)value); + } + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => HashCode.Combine(location, impactLocation, normal, impactNormal, traceStart, traceEnd, actor) ^ HashCode.Combine(time, distance, penetrationDepth, blockingHit, startPenetrating); + } + + /// + /// A combined axis-aligned bounding box and bounding sphere with the same origin + /// + public partial struct Bounds : IEquatable { + /// + /// Returns the origin of the bounding box and sphere + /// + public Vector3 Origin => origin; + + /// + /// Returns the extent of the bounding box + /// + public Vector3 BoxExtent => boxExtent; + + /// + /// Returns the radius of the bounding sphere + /// + public float SphereRadius => sphereRadius; + + /// + /// Tests for equality between two objects + /// + public static bool operator ==(Bounds left, Bounds right) => left.Equals(right); + + /// + /// Tests for inequality between two objects + /// + public static bool operator !=(Bounds left, Bounds right) => !left.Equals(right); + + /// + /// Indicates equality of objects + /// + public bool Equals(Bounds other) => origin == other.origin && boxExtent == other.boxExtent && sphereRadius == other.sphereRadius; + + /// + /// Indicates equality of objects + /// + public override bool Equals(object value) { + if (value == null) + return false; + + if (!ReferenceEquals(value.GetType(), typeof(Bounds))) + return false; + + return Equals((Bounds)value); + } + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => HashCode.Combine(origin, boxExtent, sphereRadius); + } + + /// + /// A collision shape + /// + public partial struct CollisionShape : IEquatable { + /// + /// Returns the shape type + /// + public CollisionShapeType ShapeType => shapeType; + + /// + /// Returns a box shape + /// + public static CollisionShape CreateBox(in Vector3 halfExtent) { + CollisionShape collisionShape = default; + + collisionShape.shapeType = CollisionShapeType.Box; + collisionShape.box.halfExtent = halfExtent; + + return collisionShape; + } + + /// + /// Returns a sphere shape + /// + public static CollisionShape CreateSphere(float radius) { + CollisionShape collisionShape = default; + + collisionShape.shapeType = CollisionShapeType.Sphere; + collisionShape.sphere.radius = radius; + + return collisionShape; + } + + /// + /// Returns a capsule shape + /// + public static CollisionShape CreateCapsule(float radius, float halfHeight) { + CollisionShape collisionShape = default; + + collisionShape.shapeType = CollisionShapeType.Capsule; + collisionShape.capsule.radius = radius; + collisionShape.capsule.halfHeight = halfHeight; + + return collisionShape; + } + + /// + /// Tests for equality between two objects + /// + public static bool operator ==(CollisionShape left, CollisionShape right) => left.Equals(right); + + /// + /// Tests for inequality between two objects + /// + public static bool operator !=(CollisionShape left, CollisionShape right) => !left.Equals(right); + + /// + /// Indicates equality of objects + /// + public bool Equals(CollisionShape other) => shapeType == other.shapeType && box.halfExtent == other.box.halfExtent; + + /// + /// Indicates equality of objects + /// + public override bool Equals(object value) { + if (value == null) + return false; + + if (!ReferenceEquals(value.GetType(), typeof(CollisionShape))) + return false; + + return Equals((CollisionShape)value); + } + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => HashCode.Combine(shapeType, box.halfExtent); + } + + /// + /// Delegate for action events + /// + public delegate void InputDelegate(); + + /// + /// Delegate for axis events + /// + public delegate void InputAxisDelegate(float axisValue); + + /// + /// Delegate for console variable events + /// + public delegate void ConsoleVariableDelegate(); + + /// + /// Delegate for console command events + /// + public delegate void ConsoleCommandDelegate(float value); + + /// + /// Delegate for actor overlap events + /// + public delegate void ActorOverlapDelegate(ActorReference overlapActor, ActorReference otherActor); + + /// + /// Delegate for actor hit events + /// + public delegate void ActorHitDelegate(ActorReference hitActor, ActorReference otherActor, in Vector3 normalImpulse, in Hit hit); + + /// + /// Delegate for actor cursor events + /// + public delegate void ActorCursorDelegate(ActorReference actor); + + /// + /// Delegate for actor key events + /// + public delegate void ActorKeyDelegate(ActorReference actor, string key); + + /// + /// Delegate for component overlap events + /// + public delegate void ComponentOverlapDelegate(ComponentReference overlapComponent, ComponentReference otherComponent); + + /// + /// Delegate for component hit events + /// + public delegate void ComponentHitDelegate(ComponentReference hitComponent, ComponentReference otherComponent, in Vector3 normalImpulse, in Hit hit); + + /// + /// Delegate for component cursor events + /// + public delegate void ComponentCursorDelegate(ComponentReference component); + + /// + /// Delegate for component key events + /// + public delegate void ComponentKeyDelegate(ComponentReference component, string key); + + /// + /// Delegate for character landing events + /// + public delegate void CharacterLandedDelegate(in Hit hit); + + /// + /// Provides additional static constants and methods for mathematical functions that are lack in , , and + /// + public static class Maths { + /// + /// Degrees-to-radians conversion constant + /// + public const float DegToRadF = MathF.PI * 2.0f / 360.0f; + + /// + /// Radians-to-degrees conversion constant + /// + public const float RadToDegF = 1.0f / DegToRadF; + + // Double-precision + + /// + /// Returns the dot product of two float values + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Dot(double left, double right) => left * right; + + /// + /// Clamps value between 0.0d and 1.0d + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Saturate(double value) => value < 0.0d ? 0.0d : value > 1.0d ? 1.0d : value; + + /// + /// Returns the fractional part of a float value + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Fraction(double value) => value - Math.Floor(value); + + /// + /// Calculates the shortest difference between the two given angles given in degrees + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double DeltaAngle(double current, double target) { + double delta = Repeat((target - current), 360.0d); + + if (delta > 180.0d) + delta -= 360.0d; + + return delta; + } + + /// + /// Returns the next power of two + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double NextPowerOfTwo(double value) => Math.Pow(2.0d, Math.Ceiling(Math.Log(value, 2.0d))); + + /// + /// Returns the previous power of two + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double PreviousPowerOfTwo(double value) => Math.Pow(2.0d, Math.Floor(Math.Log(value, 2.0d))); + + /// + /// Performs smooth (Cubic Hermite) interpolation between 0.0d and 1.0d + /// + /// Value between 0.0d and 1.0d indicating interpolation amount + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double SmoothStep(double amount) => (amount <= 0.0d) ? 0.0d : (amount >= 1.0d) ? 1.0d : amount * amount * (3.0d - (2.0d * amount)); + + /// + /// Performs a smoother interpolation between 0.0d and 1.0d with 1st and 2nd order derivatives of zero at endpoints + /// + /// >Value between 0.0d and 1.0d indicating interpolation amount + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double SmootherStep(double amount) => (amount <= 0.0d) ? 0.0d : (amount >= 1.0d) ? 1.0d : amount * amount * amount * (amount * ((amount * 6.0d) - 15.0d) + 10.0d); + + /// + /// Loops the value so that it is never larger than length and never smaller than 0.0d + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Repeat(double value, double length) => Math.Clamp(value - Math.Floor(value / length) * length, 0.0d, length); + + /// + /// Interpolates between two values linearly + /// + /// Value to interpolate from + /// Value to interpolate to + /// Interpolation amount + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Lerp(double from, double to, double amount) => from + amount * (to - from); + + /// + /// Interpolates between two values linearly, but makes sure the values calculated correctly when they wrap around 360 degrees + /// + /// Value to interpolate from + /// Value to interpolate to + /// Interpolation amount + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double LerpAngle(double from, double to, double amount) { + double delta = Repeat((to - from), 360.0d); + + if (delta > 180.0d) + delta -= 360.0d; + + return from + delta * Saturate(amount); + } + + /// + /// Inverse-interpolates between two values linearly + /// + /// Value to interpolate from + /// Value to interpolate to + /// Interpolation amount + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double InverseLerp(double from, double to, double amount) => from != to ? Saturate((amount - from) / (to - from)) : 0.0d; + + /// + /// Creates framerate-independent dampened motion between two values + /// + /// Value to damp from + /// Value to damp to + /// Smoothing factor + /// Time since last damp + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Damp(double from, double to, double lambda, double deltaTime) => Lerp(from, to, 1.0d - Math.Exp(-lambda * deltaTime)); + + /// + /// Returns the vector moved towards a target + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MoveTowards(double current, double target, double maxDelta) => Math.Abs(target - current) <= maxDelta ? target : current + Math.Sign(target - current) * maxDelta; + + /// + /// Returns the vector moved towards a target, but makes sure the values calculated correctly when they wrap around 360 degrees + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MoveTowardsAngle(double current, double target, double maxDelta) { + double deltaAngle = DeltaAngle(current, target); + + if (-maxDelta < deltaAngle && deltaAngle < maxDelta) + return target; + + target = current + deltaAngle; + + return MoveTowards(current, target, maxDelta); + } + + // Single-precision + + /// + /// Returns the dot product of two float values + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Dot(float left, float right) => left * right; + + /// + /// Clamps value between 0.0f and 1.0f + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Saturate(float value) => value < 0.0f ? 0.0f : value > 1.0f ? 1.0f : value; + + /// + /// Returns the fractional part of a float value + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Fraction(float value) => value - MathF.Floor(value); + + /// + /// Calculates the shortest difference between the two given angles given in degrees + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DeltaAngle(float current, float target) { + float delta = Repeat((target - current), 360.0f); + + if (delta > 180.0f) + delta -= 360.0f; + + return delta; + } + + /// + /// Returns the next power of two + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float NextPowerOfTwo(float value) => MathF.Pow(2.0f, MathF.Ceiling(MathF.Log(value, 2.0f))); + + /// + /// Returns the previous power of two + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float PreviousPowerOfTwo(float value) => MathF.Pow(2.0f, MathF.Floor(MathF.Log(value, 2.0f))); + + /// + /// Performs smooth (Cubic Hermite) interpolation between 0.0f and 1.0f + /// + /// Value between 0.0f and 1.0f indicating interpolation amount + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float SmoothStep(float amount) => (amount <= 0.0f) ? 0.0f : (amount >= 1.0f) ? 1.0f : amount * amount * (3.0f - (2.0f * amount)); + + /// + /// Performs a smoother interpolation between 0.0f and 1.0f with 1st and 2nd order derivatives of zero at endpoints + /// + /// >Value between 0.0f and 1.0f indicating interpolation amount + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float SmootherStep(float amount) => (amount <= 0.0f) ? 0.0f : (amount >= 1.0f) ? 1.0f : amount * amount * amount * (amount * ((amount * 6.0f) - 15.0f) + 10.0f); + + /// + /// Loops the value so that it is never larger than length and never smaller than 0.0f + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Repeat(float value, float length) => Math.Clamp(value - MathF.Floor(value / length) * length, 0.0f, length); + + /// + /// Interpolates between two values linearly + /// + /// Value to interpolate from + /// Value to interpolate to + /// Interpolation amount + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Lerp(float from, float to, float amount) => from + amount * (to - from); + + /// + /// Interpolates between two values linearly, but makes sure the values calculated correctly when they wrap around 360 degrees + /// + /// Value to interpolate from + /// Value to interpolate to + /// Interpolation amount + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float LerpAngle(float from, float to, float amount) { + float delta = Repeat((to - from), 360.0f); + + if (delta > 180.0f) + delta -= 360.0f; + + return from + delta * Saturate(amount); + } + + /// + /// Inverse-interpolates between two values linearly + /// + /// Value to interpolate from + /// Value to interpolate to + /// Interpolation amount + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float InverseLerp(float from, float to, float amount) => from != to ? Saturate((amount - from) / (to - from)) : 0.0f; + + /// + /// Creates framerate-independent dampened motion between two values + /// + /// Value to damp from + /// Value to damp to + /// Smoothing factor + /// Time since last damp + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Damp(float from, float to, float lambda, float deltaTime) => Lerp(from, to, 1.0f - MathF.Exp(-lambda * deltaTime)); + + /// + /// Returns the vector moved towards a target + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MoveTowards(float current, float target, float maxDelta) => MathF.Abs(target - current) <= maxDelta ? target : current + MathF.Sign(target - current) * maxDelta; + + /// + /// Returns the vector moved towards a target, but makes sure the values calculated correctly when they wrap around 360 degrees + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MoveTowardsAngle(float current, float target, float maxDelta) { + float deltaAngle = DeltaAngle(current, target); + + if (-maxDelta < deltaAngle && deltaAngle < maxDelta) + return target; + + target = current + deltaAngle; + + return MoveTowards(current, target, maxDelta); + } + + // Vector2 + + /// + /// Returns the length of the vector + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Magnitude(Vector2 value) => MathF.Sqrt(SquareMagnitude(value)); + + /// + /// Returns the squared length of the vector + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float SquareMagnitude(Vector2 value) => Vector2.Dot(value, value); + + /// + /// Returns the refraction vector + /// + /// The incident vector + /// The normal vector + /// The refraction index + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Refract(Vector2 value, Vector2 normal, float eta) { + float ni = Vector2.Dot(normal, value); + float k = 1.0f - eta * eta * (1.0f - ni * ni); + + return k >= 0.0f ? eta * value - (eta * ni + MathF.Sqrt(k)) * normal : Vector2.Zero; + } + + /// + /// Returns the vector perpendicular to the specified vector + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Perpendicular(Vector2 value) => new(-value.Y, value.X); + + /// + /// Returns the unsigned angle in degrees + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Angle(Vector2 left, Vector2 right) { + Vector2 lr = left * Magnitude(right); + Vector2 rl = right * Magnitude(left); + + return 2.0f * MathF.Atan2(Magnitude(lr - rl), Magnitude(lr + rl)) * RadToDegF; + } + + /// + /// Returns a copy of vector with clamped magnitude + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 ClampMagnitude(Vector2 value, float maxLength) { + float squareMagnitude = SquareMagnitude(value); + + if (squareMagnitude > maxLength * maxLength) { + float magnitude = MathF.Sqrt(squareMagnitude); + + return new((value.X / magnitude) * maxLength, (value.Y / magnitude) * maxLength); + } + + return value; + } + + /// + /// Creates framerate-independent dampened motion between two values + /// + /// Value to damp from + /// Value to damp to + /// Smoothing factor + /// Time since last damp + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Damp(Vector2 from, Vector2 to, float lambda, float deltaTime) => Vector2.Lerp(from, to, 1.0f - MathF.Exp(-lambda * deltaTime)); + + /// + /// Returns the vector moved towards a target + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 MoveTowards(Vector2 current, Vector2 target, float maxDistanceDelta) { + Vector2 destination = Vector2.Subtract(target, current); + + float squareMagnitude = SquareMagnitude(destination); + + if (squareMagnitude == 0.0f || (maxDistanceDelta >= 0.0f && squareMagnitude <= maxDistanceDelta * maxDistanceDelta)) + return target; + + float distance = MathF.Sqrt(squareMagnitude); + + return new(current.X + destination.X / distance * maxDistanceDelta, current.Y + destination.Y / distance * maxDistanceDelta); + } + + // Vector3 + + /// + /// Returns the length of the vector + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Magnitude(Vector3 value) => MathF.Sqrt(SquareMagnitude(value)); + + /// + /// Returns the squared length of the vector + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float SquareMagnitude(Vector3 value) => Vector3.Dot(value, value); + + /// + /// Returns the refraction vector + /// + /// The incident vector + /// The normal vector + /// The refraction index + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Refract(Vector3 value, Vector3 normal, float eta) { + float ni = Vector3.Dot(normal, value); + float k = 1.0f - eta * eta * (1.0f - ni * ni); + + return k >= 0.0f ? eta * value - (eta * ni + MathF.Sqrt(k)) * normal : Vector3.Zero; + } + + /// + /// Returns the unsigned angle in degrees + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Angle(Vector3 left, Vector3 right) { + Vector3 lr = left * Magnitude(right); + Vector3 rl = right * Magnitude(left); + + return 2.0f * MathF.Atan2(Magnitude(lr - rl), Magnitude(lr + rl)) * RadToDegF; + } + + /// + /// Returns the signed angle in degrees + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float SignedAngle(Vector3 left, Vector3 right, Vector3 axis) { + Vector3 cross = Vector3.Cross(left, right); + + return Angle(left, right) * MathF.Sign(Vector3.Dot(axis, cross)); + } + + /// + /// Returns a copy of vector with clamped magnitude + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ClampMagnitude(Vector3 value, float maxLength) { + float squareMagnitude = SquareMagnitude(value); + + if (squareMagnitude > maxLength * maxLength) { + float magnitude = MathF.Sqrt(squareMagnitude); + + return new((value.X / magnitude) * maxLength, (value.Y / magnitude) * maxLength, (value.Z / magnitude) * maxLength); + } + + return value; + } + + /// + /// Creates framerate-independent dampened motion between two values + /// + /// Value to damp from + /// Value to damp to + /// Smoothing factor + /// Time since last damp + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Damp(Vector3 from, Vector3 to, float lambda, float deltaTime) => Vector3.Lerp(from, to, 1.0f - MathF.Exp(-lambda * deltaTime)); + + /// + /// Returns the vector moved towards a target + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 MoveTowards(Vector3 current, Vector3 target, float maxDistanceDelta) { + Vector3 destination = Vector3.Subtract(target, current); + + float squareMagnitude = SquareMagnitude(destination); + + if (squareMagnitude == 0.0f || (maxDistanceDelta >= 0.0f && squareMagnitude <= maxDistanceDelta * maxDistanceDelta)) + return target; + + float distance = MathF.Sqrt(squareMagnitude); + + return new(current.X + destination.X / distance * maxDistanceDelta, current.Y + destination.Y / distance * maxDistanceDelta, current.Z + destination.Z / distance * maxDistanceDelta); + } + + /// + /// Projects a vector onto another vector + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Project(Vector3 value, Vector3 normal) { + float squareMagnitude = SquareMagnitude(normal); + + if (squareMagnitude < Single.Epsilon) + return Vector3.Zero; + + float dot = Vector3.Dot(value, normal); + + return new(normal.X * dot / squareMagnitude, normal.Y * dot / squareMagnitude, normal.Z * dot / squareMagnitude); + } + + /// + /// Projects a vector onto a plane defined by a normal orthogonal to the plane + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ProjectOnPlane(Vector3 value, Vector3 planeNormal) { + float squareMagnitude = SquareMagnitude(planeNormal); + + if (squareMagnitude < Single.Epsilon) + return value; + + float dot = Vector3.Dot(value, planeNormal); + + return new(value.X - planeNormal.X * dot / squareMagnitude, value.Y - planeNormal.Y * dot / squareMagnitude, value.Z - planeNormal.Z * dot / squareMagnitude); + } + + // Vector4 + + /// + /// Returns the length of the vector + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Magnitude(Vector4 value) => MathF.Sqrt(SquareMagnitude(value)); + + /// + /// Returns the squared length of the vector + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float SquareMagnitude(Vector4 value) => Vector4.Dot(value, value); + + /// + /// Returns the refraction vector + /// + /// The incident vector + /// The normal vector + /// The refraction index + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Refract(Vector4 value, Vector4 normal, float eta) { + float ni = Vector4.Dot(normal, value); + float k = 1.0f - eta * eta * (1.0f - ni * ni); + + return k >= 0.0f ? eta * value - (eta * ni + MathF.Sqrt(k)) * normal : Vector4.Zero; + } + + // Quaternion + + /// + /// Returns a rotation which rotates z degrees around the z axis, x degrees around the x axis, and y degrees around the y axis + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion Euler(float x, float y, float z) { + x *= DegToRadF; + y *= DegToRadF; + z *= DegToRadF; + + return CreateFromYawPitchRoll(z, -y, -x); + } + + /// + /// Returns a rotation which rotates z degrees around the z axis, x degrees around the x axis, and y degrees around the y axis + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion Euler(Vector3 eulerAngles) => Euler(eulerAngles.X, eulerAngles.Y, eulerAngles.Z); + + /// + /// Returns the unsigned angle in degrees + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Angle(Quaternion from, Quaternion to) { + float dot = Quaternion.Dot(from, to); + + return dot > 1.0f - 0.000001f ? 0.0f : MathF.Acos(MathF.Min(MathF.Abs(dot), 1.0f)) * 2.0f * RadToDegF; + } + + /// + /// Returns a rotation which rotates angle degrees around axis + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion AngleAxis(float angle, Vector3 axis) { + axis = axis * DegToRadF; + axis = Vector3.Normalize(axis); + + float halfAngle = angle * DegToRadF * 0.5f; + float sin = MathF.Sin(halfAngle); + + return new(axis.X * sin, axis.Y * sin, axis.Z * sin, MathF.Cos(halfAngle)); + } + + /// + /// Returns a rotation which rotates from to + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion FromToRotation(Vector3 fromDirection, Vector3 toDirection) { + float dot = Vector3.Dot(fromDirection, toDirection); + float normal = MathF.Sqrt(SquareMagnitude(fromDirection) * SquareMagnitude(toDirection)); + float real = normal + dot; + Vector3 final = default; + + if (real < Single.Epsilon * normal) { + real = 0.0f; + final = MathF.Abs(fromDirection.X) > MathF.Abs(fromDirection.Z) ? new(-fromDirection.Y, fromDirection.X, 0.0f) : new(0.0f, -fromDirection.Z, fromDirection.Y); + } else { + final = Vector3.Cross(fromDirection, toDirection); + } + + return Quaternion.Normalize(new(final, real)); + } + + /// + /// Creates framerate-independent dampened motion between two values + /// + /// Value to damp from + /// Value to damp to + /// Smoothing factor + /// Time since last damp + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion Damp(Quaternion from, Quaternion to, float lambda, float deltaTime) => Quaternion.Slerp(from, to, 1.0f - MathF.Exp(-lambda * deltaTime)); + + /// + /// Returns a rotation which rotated towards a target + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion RotateTowards(Quaternion from, Quaternion to, float maxDegreesDelta) { + float angle = Angle(from, to); + + if (angle == 0.0f) + return to; + + return Quaternion.Slerp(from, to, MathF.Min(1.0f, maxDegreesDelta / angle)); + } + + /// + /// Returns a rotation from the given yaw, pitch, and roll, in radians + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion CreateFromYawPitchRoll(float yaw, float pitch, float roll) { + float sr, cr, sp, cp, sy, cy; + float halfRoll = roll * 0.5f; + + sr = MathF.Sin(halfRoll); + cr = MathF.Cos(halfRoll); + + float halfPitch = pitch * 0.5f; + + sp = MathF.Sin(halfPitch); + cp = MathF.Cos(halfPitch); + + float halfYaw = yaw * 0.5f; + + sy = MathF.Sin(halfYaw); + cy = MathF.Cos(halfYaw); + + Quaternion result = default; + + result.X = cy * cp * sr - sy * sp * cr; + result.Y = cy * sp * cr + sy * cp * sr; + result.Z = sy * cp * cr - cy * sp * sr; + result.W = cy * cp * cr + sy * sp * sr; + + return result; + } + } + + /// + /// Key codes + /// + public static class Keys { + /// + public const string AnyKey = "AnyKey"; + + /// + public const string MouseX = "MouseX"; + /// + public const string MouseY = "MouseY"; + /// + public const string MouseScrollUp = "MouseScrollUp"; + /// + public const string MouseScrollDown = "MouseScrollDown"; + /// + public const string MouseWheelAxis = "MouseWheelAxis"; + + /// + public const string LeftMouseButton = "LeftMouseButton"; + /// + public const string RightMouseButton = "RightMouseButton"; + /// + public const string MiddleMouseButton = "MiddleMouseButton"; + /// + public const string ThumbMouseButton = "ThumbMouseButton"; + /// + public const string ThumbMouseButton2 = "ThumbMouseButton2"; + + /// + public const string BackSpace = "BackSpace"; + /// + public const string Tab = "Tab"; + /// + public const string Enter = "Enter"; + /// + public const string Pause = "Pause"; + + /// + public const string CapsLock = "CapsLock"; + /// + public const string Escape = "Escape"; + /// + public const string SpaceBar = "SpaceBar"; + /// + public const string PageUp = "PageUp"; + /// + public const string PageDown = "PageDown"; + /// + public const string End = "End"; + /// + public const string Home = "Home"; + + /// + public const string Left = "Left"; + /// + public const string Up = "Up"; + /// + public const string Right = "Right"; + /// + public const string Down = "Down"; + + /// + public const string Insert = "Insert"; + /// + public const string Delete = "Delete"; + + /// + public const string Zero = "Zero"; + /// + public const string One = "One"; + /// + public const string Two = "Two"; + /// + public const string Three = "Three"; + /// + public const string Four = "Four"; + /// + public const string Five = "Five"; + /// + public const string Six = "Six"; + /// + public const string Seven = "Seven"; + /// + public const string Eight = "Eight"; + /// + public const string Nine = "Nine"; + + /// + public const string A = "A"; + /// + public const string B = "B"; + /// + public const string C = "C"; + /// + public const string D = "D"; + /// + public const string E = "E"; + /// + public const string F = "F"; + /// + public const string G = "G"; + /// + public const string H = "H"; + /// + public const string I = "I"; + /// + public const string J = "J"; + /// + public const string K = "K"; + /// + public const string L = "L"; + /// + public const string M = "M"; + /// + public const string N = "N"; + /// + public const string O = "O"; + /// + public const string P = "P"; + /// + public const string Q = "Q"; + /// + public const string R = "R"; + /// + public const string S = "S"; + /// + public const string T = "T"; + /// + public const string U = "U"; + /// + public const string V = "V"; + /// + public const string W = "W"; + /// + public const string X = "X"; + /// + public const string Y = "Y"; + /// + public const string Z = "Z"; + + /// + public const string NumPadZero = "NumPadZero"; + /// + public const string NumPadOne = "NumPadOne"; + /// + public const string NumPadTwo = "NumPadTwo"; + /// + public const string NumPadThree = "NumPadThree"; + /// + public const string NumPadFour = "NumPadFour"; + /// + public const string NumPadFive = "NumPadFive"; + /// + public const string NumPadSix = "NumPadSix"; + /// + public const string NumPadSeven = "NumPadSeven"; + /// + public const string NumPadEight = "NumPadEight"; + /// + public const string NumPadNine = "NumPadNine"; + + /// + public const string Multiply = "Multiply"; + /// + public const string Add = "Add"; + /// + public const string Subtract = "Subtract"; + /// + public const string Decimal = "Decimal"; + /// + public const string Divide = "Divide"; + + /// + public const string F1 = "F1"; + /// + public const string F2 = "F2"; + /// + public const string F3 = "F3"; + /// + public const string F4 = "F4"; + /// + public const string F5 = "F5"; + /// + public const string F6 = "F6"; + /// + public const string F7 = "F7"; + /// + public const string F8 = "F8"; + /// + public const string F9 = "F9"; + /// + public const string F10 = "F10"; + /// + public const string F11 = "F11"; + /// + public const string F12 = "F12"; + + /// + public const string NumLock = "NumLock"; + /// + public const string ScrollLock = "ScrollLock"; + + /// + public const string LeftShift = "LeftShift"; + /// + public const string RightShift = "RightShift"; + /// + public const string LeftControl = "LeftControl"; + /// + public const string RightControl = "RightControl"; + /// + public const string LeftAlt = "LeftAlt"; + /// + public const string RightAlt = "RightAlt"; + /// + public const string LeftCommand = "LeftCommand"; + /// + public const string RightCommand = "RightCommand"; + + /// + public const string Semicolon = "Semicolon"; + /// + public const string Equal = "Equals"; + /// + public const string Comma = "Comma"; + /// + public const string Underscore = "Underscore"; + /// + public const string Hyphen = "Hyphen"; + /// + public const string Period = "Period"; + /// + public const string Slash = "Slash"; + /// + public const string Tilde = "Tilde"; + /// + public const string LeftBracket = "LeftBracket"; + /// + public const string Backslash = "Backslash"; + /// + public const string RightBracket = "RightBracket"; + /// + public const string Apostrophe = "Apostrophe"; + + /// + public const string Ampersand = "Ampersand"; + /// + public const string Asterix = "Asterix"; + /// + public const string Caret = "Caret"; + /// + public const string Colon = "Colon"; + /// + public const string Dollar = "Dollar"; + /// + public const string Exclamation = "Exclamation"; + /// + public const string LeftParantheses = "LeftParantheses"; + /// + public const string RightParantheses = "RightParantheses"; + /// + public const string Quote = "Quote"; + + /// + public const string Section = "Section"; + + /// + public const string Invalid = "Invalid"; + + /// + /// Platform-specific + /// + public static class Platform { + /// + public const string Delete = "Platform_Delete"; + } + + /// + /// Gamepad + /// + public static class Gamepad { + /// + public const string LeftX = "Gamepad_LeftX"; + /// + public const string LeftY = "Gamepad_LeftY"; + /// + public const string RightX = "Gamepad_RightX"; + /// + public const string RightY = "Gamepad_RightY"; + /// + public const string LeftTriggerAxis = "Gamepad_LeftTriggerAxis"; + /// + public const string RightTriggerAxis = "Gamepad_RightTriggerAxis"; + /// + public const string LeftThumbstick = "Gamepad_LeftThumbstick"; + /// + public const string RightThumbstick = "Gamepad_RightThumbstick"; + /// + public const string LeftShoulder = "Gamepad_LeftShoulder"; + /// + public const string RightShoulder = "Gamepad_RightShoulder"; + /// + public const string LeftTrigger = "Gamepad_LeftTrigger"; + /// + public const string RightTrigger = "Gamepad_RightTrigger"; + /// + public const string DPadUp = "Gamepad_DPad_Up"; + /// + public const string DPadDown = "Gamepad_DPad_Down"; + /// + public const string DPadRight = "Gamepad_DPad_Right"; + /// + public const string DPadLeft = "Gamepad_DPad_Left"; + /// + public const string SpecialLeft = "Gamepad_Special_Left"; + /// + public const string SpecialLeftX = "Gamepad_Special_Left_X"; + /// + public const string SpecialLeftY = "Gamepad_Special_Left_Y"; + /// + public const string SpecialRight = "Gamepad_Special_Right"; + /// + public const string FaceButtonBottom = "Gamepad_FaceButton_Bottom"; + /// + public const string FaceButtonRight = "Gamepad_FaceButton_Right"; + /// + public const string FaceButtonLeft = "Gamepad_FaceButton_Left"; + /// + public const string FaceButtonTop = "Gamepad_FaceButton_Top"; + } + + /// + /// Virtual key codes used for input axis button press/release emulation + /// + public static class Virtual { + /// + public const string LeftStickUp = "Gamepad_LeftStick_Up"; + /// + public const string LeftStickDown = "Gamepad_LeftStick_Down"; + /// + public const string LeftStickRight = "Gamepad_LeftStick_Right"; + /// + public const string LeftStickLeft = "Gamepad_LeftStick_Left"; + /// + public const string RightStickUp = "Gamepad_RightStick_Up"; + /// + public const string RightStickDown = "Gamepad_RightStick_Down"; + /// + public const string RightStickRight = "Gamepad_RightStick_Right"; + /// + public const string RightStickLeft = "Gamepad_RightStick_Left"; + + /// + public const string Tilt = "Tilt"; + /// + public const string RotationRate = "RotationRate"; + /// + public const string Gravity = "Gravity"; + /// + public const string Acceleration = "Acceleration"; + + /// + public const string Accept = "Virtual_Accept"; + /// + public const string Back = "Virtual_Back"; + } + + /// + /// Gestures + /// + public static class Gesture { + /// + public const string Pinch = "Gesture_Pinch"; + /// + public const string Flick = "Gesture_Flick"; + /// + public const string Rotate = "Gesture_Rotate"; + } + + /// + /// PS4 + /// + public static class PS4 { + /// + public const string Special = "PS4_Special"; + } + + /// + /// Steam Controller + /// + public static class Steam { + /// + public const string TouchZero = "Steam_Touch_0"; + /// + public const string TouchOne = "Steam_Touch_1"; + /// + public const string TouchTwo = "Steam_Touch_2"; + /// + public const string TouchThree = "Steam_Touch_3"; + /// + public const string BackLeft = "Steam_Back_Left"; + /// + public const string BackRight = "Steam_Back_Right"; + } + + /// + /// Xbox + /// + public static class Xbox { + /// + public const string GlobalMenu = "Global_Menu"; + /// + public const string GlobalView = "Global_View"; + /// + public const string GlobalPause = "Global_Pause"; + /// + public const string GlobalPlay = "Global_Play"; + /// + public const string GlobalBack = "Global_Back"; + } + + /// + /// Android + /// + public static class Android { + /// + public const string Back = "Android_Back"; + /// + public const string VolumeUp = "Android_Volume_Up"; + /// + public const string VolumeDown = "Android_Volume_Down"; + /// + public const string Menu = "Android_Menu"; + } + + /// + /// Google Daydream + /// + public static class Daydream { + /// + public const string LeftSelectClick = "Daydream_Left_Select_Click"; + /// + public const string LeftTrackpadX = "Daydream_Left_Trackpad_X"; + /// + public const string LeftTrackpadY = "Daydream_Left_Trackpad_Y"; + /// + public const string LeftTrackpadClick = "Daydream_Left_Trackpad_Click"; + /// + public const string LeftTrackpadTouch = "Daydream_Left_Trackpad_Touch"; + /// + public const string RightSelectClick = "Daydream_Right_Select_Click"; + /// + public const string RightTrackpadX = "Daydream_Right_Trackpad_X"; + /// + public const string RightTrackpadY = "Daydream_Right_Trackpad_Y"; + /// + public const string RightTrackpadClick = "Daydream_Right_Trackpad_Click"; + /// + public const string RightTrackpadTouch = "Daydream_Right_Trackpad_Touch"; + } + + /// + /// HTC Vive + /// + public static class Vive { + /// + public const string LeftSystemClick = "Vive_Left_System_Click"; + /// + public const string LeftGripClick = "Vive_Left_Grip_Click"; + /// + public const string LeftMenuClick = "Vive_Left_Menu_Click"; + /// + public const string LeftTriggerClick = "Vive_Left_Trigger_Click"; + /// + public const string LeftTriggerAxis = "Vive_Left_Trigger_Axis"; + /// + public const string LeftTrackpadX = "Vive_Left_Trackpad_X"; + /// + public const string LeftTrackpadY = "Vive_Left_Trackpad_Y"; + /// + public const string LeftTrackpadClick = "Vive_Left_Trackpad_Click"; + /// + public const string LeftTrackpadTouch = "Vive_Left_Trackpad_Touch"; + /// + public const string LeftTrackpadUp = "Vive_Left_Trackpad_Up"; + /// + public const string LeftTrackpadDown = "Vive_Left_Trackpad_Down"; + /// + public const string LeftTrackpadLeft = "Vive_Left_Trackpad_Left"; + /// + public const string LeftTrackpadRight = "Vive_Left_Trackpad_Right"; + /// + public const string RightSystemClick = "Vive_Right_System_Click"; + /// + public const string RightGripClick = "Vive_Right_Grip_Click"; + /// + public const string RightMenuClick = "Vive_Right_Menu_Click"; + /// + public const string RightTriggerClick = "Vive_Right_Trigger_Click"; + /// + public const string RightTriggerAxis = "Vive_Right_Trigger_Axis"; + /// + public const string RightTrackpadX = "Vive_Right_Trackpad_X"; + /// + public const string RightTrackpadY = "Vive_Right_Trackpad_Y"; + /// + public const string RightTrackpadClick = "Vive_Right_Trackpad_Click"; + /// + public const string RightTrackpadTouch = "Vive_Right_Trackpad_Touch"; + /// + public const string RightTrackpadUp = "Vive_Right_Trackpad_Up"; + /// + public const string RightTrackpadDown = "Vive_Right_Trackpad_Down"; + /// + public const string RightTrackpadLeft = "Vive_Right_Trackpad_Left"; + /// + public const string RightTrackpadRight = "Vive_Right_Trackpad_Right"; + } + + /// + /// Microsoft Mixed Reality + /// + public static class MixedReality { + /// + public const string LeftMenuClick = "MixedReality_Left_Menu_Click"; + /// + public const string LeftGripClick = "MixedReality_Left_Grip_Click"; + /// + public const string LeftTriggerClick = "MixedReality_Left_Trigger_Click"; + /// + public const string LeftTriggerAxis = "MixedReality_Left_Trigger_Axis"; + /// + public const string LeftThumbstickX = "MixedReality_Left_Thumbstick_X"; + /// + public const string LeftThumbstickY = "MixedReality_Left_Thumbstick_Y"; + /// + public const string LeftThumbstickClick = "MixedReality_Left_Thumbstick_Click"; + /// + public const string LeftThumbstickUp = "MixedReality_Left_Thumbstick_Up"; + /// + public const string LeftThumbstickDown = "MixedReality_Left_Thumbstick_Down"; + /// + public const string LeftThumbstickLeft = "MixedReality_Left_Thumbstick_Left"; + /// + public const string LeftThumbstickRight = "MixedReality_Left_Thumbstick_Right"; + /// + public const string LeftTrackpadX = "MixedReality_Left_Trackpad_X"; + /// + public const string LeftTrackpadY = "MixedReality_Left_Trackpad_Y"; + /// + public const string LeftTrackpadClick = "MixedReality_Left_Trackpad_Click"; + /// + public const string LeftTrackpadTouch = "MixedReality_Left_Trackpad_Touch"; + /// + public const string LeftTrackpadUp = "MixedReality_Left_Trackpad_Up"; + /// + public const string LeftTrackpadDown = "MixedReality_Left_Trackpad_Down"; + /// + public const string LeftTrackpadLeft = "MixedReality_Left_Trackpad_Left"; + /// + public const string LeftTrackpadRight = "MixedReality_Left_Trackpad_Right"; + /// + public const string RightMenuClick = "MixedReality_Right_Menu_Click"; + /// + public const string RightGripClick = "MixedReality_Right_Grip_Click"; + /// + public const string RightTriggerClick = "MixedReality_Right_Trigger_Click"; + /// + public const string RightTriggerAxis = "MixedReality_Right_Trigger_Axis"; + /// + public const string RightThumbstickX = "MixedReality_Right_Thumbstick_X"; + /// + public const string RightThumbstickY = "MixedReality_Right_Thumbstick_Y"; + /// + public const string RightThumbstickClick = "MixedReality_Right_Thumbstick_Click"; + /// + public const string RightThumbstickUp = "MixedReality_Right_Thumbstick_Up"; + /// + public const string RightThumbstickDown = "MixedReality_Right_Thumbstick_Down"; + /// + public const string RightThumbstickLeft = "MixedReality_Right_Thumbstick_Left"; + /// + public const string RightThumbstickRight = "MixedReality_Right_Thumbstick_Right"; + /// + public const string RightTrackpadX = "MixedReality_Right_Trackpad_X"; + /// + public const string RightTrackpadY = "MixedReality_Right_Trackpad_Y"; + /// + public const string RightTrackpadClick = "MixedReality_Right_Trackpad_Click"; + /// + public const string RightTrackpadTouch = "MixedReality_Right_Trackpad_Touch"; + /// + public const string RightTrackpadUp = "MixedReality_Right_Trackpad_Up"; + /// + public const string RightTrackpadDown = "MixedReality_Right_Trackpad_Down"; + /// + public const string RightTrackpadLeft = "MixedReality_Right_Trackpad_Left"; + /// + public const string RightTrackpadRight = "MixedReality_Right_Trackpad_Right"; + } + + /// + /// Oculus Go + /// + public static class OculusGo { + /// + public const string LeftSystemClick = "OculusGo_Left_System_Click"; + /// + public const string LeftBackClick = "OculusGo_Left_Back_Click"; + /// + public const string LeftTriggerClick = "OculusGo_Left_Trigger_Click"; + /// + public const string LeftTrackpadX = "OculusGo_Left_Trackpad_X"; + /// + public const string LeftTrackpadY = "OculusGo_Left_Trackpad_Y"; + /// + public const string LeftTrackpadClick = "OculusGo_Left_Trackpad_Click"; + /// + public const string LeftTrackpadTouch = "OculusGo_Left_Trackpad_Touch"; + /// + public const string RightSystemClick = "OculusGo_Right_System_Click"; + /// + public const string RightBackClick = "OculusGo_Right_Back_Click"; + /// + public const string RightTriggerClick = "OculusGo_Right_Trigger_Click"; + /// + public const string RightTrackpadX = "OculusGo_Right_Trackpad_X"; + /// + public const string RightTrackpadY = "OculusGo_Right_Trackpad_Y"; + /// + public const string RightTrackpadClick = "OculusGo_Right_Trackpad_Click"; + /// + public const string RightTrackpadTouch = "OculusGo_Right_Trackpad_Touch"; + } + + /// + /// Oculus Touch + /// + public static class OculusTouch { + /// + public const string LeftXClick = "OculusTouch_Left_X_Click"; + /// + public const string LeftYClick = "OculusTouch_Left_Y_Click"; + /// + public const string LeftXTouch = "OculusTouch_Left_X_Touch"; + /// + public const string LeftYTouch = "OculusTouch_Left_Y_Touch"; + /// + public const string LeftMenuClick = "OculusTouch_Left_Menu_Click"; + /// + public const string LeftGripClick = "OculusTouch_Left_Grip_Click"; + /// + public const string LeftGripAxis = "OculusTouch_Left_Grip_Axis"; + /// + public const string LeftTriggerClick = "OculusTouch_Left_Trigger_Click"; + /// + public const string LeftTriggerAxis = "OculusTouch_Left_Trigger_Axis"; + /// + public const string LeftTriggerTouch = "OculusTouch_Left_Trigger_Touch"; + /// + public const string LeftThumbstickX = "OculusTouch_Left_Thumbstick_X"; + /// + public const string LeftThumbstickY = "OculusTouch_Left_Thumbstick_Y"; + /// + public const string LeftThumbstickClick = "OculusTouch_Left_Thumbstick_Click"; + /// + public const string LeftThumbstickTouch = "OculusTouch_Left_Thumbstick_Touch"; + /// + public const string LeftThumbstickUp = "OculusTouch_Left_Thumbstick_Up"; + /// + public const string LeftThumbstickDown = "OculusTouch_Left_Thumbstick_Down"; + /// + public const string LeftThumbstickLeft = "OculusTouch_Left_Thumbstick_Left"; + /// + public const string LeftThumbstickRight = "OculusTouch_Left_Thumbstick_Right"; + /// + public const string RightAClick = "OculusTouch_Right_A_Click"; + /// + public const string RightBClick = "OculusTouch_Right_B_Click"; + /// + public const string RightATouch = "OculusTouch_Right_A_Touch"; + /// + public const string RightBTouch = "OculusTouch_Right_B_Touch"; + /// + public const string RightSystemClick = "OculusTouch_Right_System_Click"; + /// + public const string RightGripClick = "OculusTouch_Right_Grip_Click"; + /// + public const string RightGripAxis = "OculusTouch_Right_Grip_Axis"; + /// + public const string RightTriggerClick = "OculusTouch_Right_Trigger_Click"; + /// + public const string RightTriggerAxis = "OculusTouch_Right_Trigger_Axis"; + /// + public const string RightTriggerTouch = "OculusTouch_Right_Trigger_Touch"; + /// + public const string RightThumbstickX = "OculusTouch_Right_Thumbstick_X"; + /// + public const string RightThumbstickY = "OculusTouch_Right_Thumbstick_Y"; + /// + public const string RightThumbstickClick = "OculusTouch_Right_Thumbstick_Click"; + /// + public const string RightThumbstickTouch = "OculusTouch_Right_Thumbstick_Touch"; + /// + public const string RightThumbstickUp = "OculusTouch_Right_Thumbstick_Up"; + /// + public const string RightThumbstickDown = "OculusTouch_Right_Thumbstick_Down"; + /// + public const string RightThumbstickLeft = "OculusTouch_Right_Thumbstick_Left"; + /// + public const string RightThumbstickRight = "OculusTouch_Right_Thumbstick_Right"; + } + + /// + /// Valve Index + /// + public static class ValveIndex { + /// + public const string LeftAClick = "ValveIndex_Left_A_Click"; + /// + public const string LeftBClick = "ValveIndex_Left_B_Click"; + /// + public const string LeftATouch = "ValveIndex_Left_A_Touch"; + /// + public const string LeftBTouch = "ValveIndex_Left_B_Touch"; + /// + public const string LeftSystemClick = "ValveIndex_Left_System_Click"; + /// + public const string LeftSystemTouch = "ValveIndex_Left_System_Touch"; + /// + public const string LeftGripClick = "ValveIndex_Left_Grip_Click"; + /// + public const string LeftGripAxis = "ValveIndex_Left_Grip_Axis"; + /// + public const string LeftGripForce = "ValveIndex_Left_Grip_Force"; + /// + public const string LeftTriggerClick = "ValveIndex_Left_Trigger_Click"; + /// + public const string LeftTriggerAxis = "ValveIndex_Left_Trigger_Axis"; + /// + public const string LeftTriggerTouch = "ValveIndex_Left_Trigger_Touch"; + /// + public const string LeftThumbstickX = "ValveIndex_Left_Thumbstick_X"; + /// + public const string LeftThumbstickY = "ValveIndex_Left_Thumbstick_Y"; + /// + public const string LeftThumbstickClick = "ValveIndex_Left_Thumbstick_Click"; + /// + public const string LeftThumbstickTouch = "ValveIndex_Left_Thumbstick_Touch"; + /// + public const string LeftThumbstickUp = "ValveIndex_Left_Thumbstick_Up"; + /// + public const string LeftThumbstickDown = "ValveIndex_Left_Thumbstick_Down"; + /// + public const string LeftThumbstickLeft = "ValveIndex_Left_Thumbstick_Left"; + /// + public const string LeftThumbstickRight = "ValveIndex_Left_Thumbstick_Right"; + /// + public const string LeftTrackpadX = "ValveIndex_Left_Trackpad_X"; + /// + public const string LeftTrackpadY = "ValveIndex_Left_Trackpad_Y"; + /// + public const string LeftTrackpadClick = "ValveIndex_Left_Trackpad_Click"; + /// + public const string LeftTrackpadForce = "ValveIndex_Left_Trackpad_Force"; + /// + public const string LeftTrackpadTouch = "ValveIndex_Left_Trackpad_Touch"; + /// + public const string LeftTrackpadUp = "ValveIndex_Left_Trackpad_Up"; + /// + public const string LeftTrackpadDown = "ValveIndex_Left_Trackpad_Down"; + /// + public const string LeftTrackpadLeft = "ValveIndex_Left_Trackpad_Left"; + /// + public const string LeftTrackpadRight = "ValveIndex_Left_Trackpad_Right"; + /// + public const string RightAClick = "ValveIndex_Right_A_Click"; + /// + public const string RightBClick = "ValveIndex_Right_B_Click"; + /// + public const string RightATouch = "ValveIndex_Right_A_Touch"; + /// + public const string RightBTouch = "ValveIndex_Right_B_Touch"; + /// + public const string RightSystemClick = "ValveIndex_Right_System_Click"; + /// + public const string RightSystemTouch = "ValveIndex_Right_System_Touch"; + /// + public const string RightGripClick = "ValveIndex_Right_Grip_Click"; + /// + public const string RightGripAxis = "ValveIndex_Right_Grip_Axis"; + /// + public const string RightGripForce = "ValveIndex_Right_Grip_Force"; + /// + public const string RightTriggerClick = "ValveIndex_Right_Trigger_Click"; + /// + public const string RightTriggerAxis = "ValveIndex_Right_Trigger_Axis"; + /// + public const string RightTriggerTouch = "ValveIndex_Right_Trigger_Touch"; + /// + public const string RightThumbstickX = "ValveIndex_Right_Thumbstick_X"; + /// + public const string RightThumbstickY = "ValveIndex_Right_Thumbstick_Y"; + /// + public const string RightThumbstickClick = "ValveIndex_Right_Thumbstick_Click"; + /// + public const string RightThumbstickTouch = "ValveIndex_Right_Thumbstick_Touch"; + /// + public const string RightThumbstickUp = "ValveIndex_Right_Thumbstick_Up"; + /// + public const string RightThumbstickDown = "ValveIndex_Right_Thumbstick_Down"; + /// + public const string RightThumbstickLeft = "ValveIndex_Right_Thumbstick_Left"; + /// + public const string RightThumbstickRight = "ValveIndex_Right_Thumbstick_Right"; + /// + public const string RightTrackpadX = "ValveIndex_Right_Trackpad_X"; + /// + public const string RightTrackpadY = "ValveIndex_Right_Trackpad_Y"; + /// + public const string RightTrackpadClick = "ValveIndex_Right_Trackpad_Click"; + /// + public const string RightTrackpadForce = "ValveIndex_Right_Trackpad_Force"; + /// + public const string RightTrackpadTouch = "ValveIndex_Right_Trackpad_Touch"; + /// + public const string RightTrackpadUp = "ValveIndex_Right_Trackpad_Up"; + /// + public const string RightTrackpadDown = "ValveIndex_Right_Trackpad_Down"; + /// + public const string RightTrackpadLeft = "ValveIndex_Right_Trackpad_Left"; + /// + public const string RightTrackpadRight = "ValveIndex_Right_Trackpad_Right"; + } + } + + /// + /// Functionality to detect and diagnose unexpected or invalid runtime conditions during development, emitted if the assembly is built with the Debug configuration or if ASSERTIONS symbol is defined, signals a breakpoint to an attached debugger + /// + public static unsafe partial class Assert { + [ThreadStatic] + private static StringBuilder stringBuffer = new(8192); + + private static void Message(string message, int callerLineNumber, string callerFilePath) { + stringBuffer.Clear() + .AppendFormat("Assertion is failed at line {0} in file \"{1}\"", callerLineNumber, callerFilePath); + + if (message != null) + stringBuffer.AppendFormat(" with message: {0}", message); + + outputMessage(stringBuffer.ToString().StringToBytes()); + + Debugger.Break(); + } + + /// + /// Logs an assertion if condition is true, and prints it on the screen + /// + [Conditional("DEBUG"), Conditional("ASSERTIONS")] + public static void IsFalse(bool condition, string message = null, [CallerLineNumber] int callerLineNumber = 0, [CallerFilePath] string callerFilePath = null) { + if (condition) + Message(message, callerLineNumber, callerFilePath); + } + + /// + /// Logs an assertion if condition is false, and prints it on the screen + /// + [Conditional("DEBUG"), Conditional("ASSERTIONS")] + public static void IsTrue(bool condition, string message = null, [CallerLineNumber] int callerLineNumber = 0, [CallerFilePath] string callerFilePath = null) { + if (!condition) + Message(message, callerLineNumber, callerFilePath); + } + + /// + /// Logs an assertion if value is not `null`, and prints it on the screen + /// + [Conditional("DEBUG"), Conditional("ASSERTIONS")] + public static void IsNull(T value, string message = null, [CallerLineNumber] int callerLineNumber = 0, [CallerFilePath] string callerFilePath = null) where T : class { + if (value != null) + Message(message, callerLineNumber, callerFilePath); + } + + /// + /// Logs an assertion if value is `null`, and prints it on the screen + /// + [Conditional("DEBUG"), Conditional("ASSERTIONS")] + public static void IsNotNull(T value, string message = null, [CallerLineNumber] int callerLineNumber = 0, [CallerFilePath] string callerFilePath = null) where T : class { + if (value == null) + Message(message, callerLineNumber, callerFilePath); + } + } + + /// + /// Functionality to work with the command-line of the engine executable + /// + public static unsafe partial class CommandLine { + /// + /// Returns the user arguments + /// + public static string Get() { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + get(stringBuffer); + + return stringBuffer.BytesToString(); + } + + /// + /// Overrides the arguments + /// + public static void Set(string arguments) { + if (arguments == null) + throw new ArgumentNullException(nameof(arguments)); + + set(arguments.StringToBytes()); + } + + /// + /// Appends the string to the arguments as it is without adding a space + /// + public static void Append(string arguments) { + if (arguments == null) + throw new ArgumentNullException(nameof(arguments)); + + append(arguments.StringToBytes()); + } + } + + /// + /// Functionality for debugging + /// + public static unsafe partial class Debug { + [ThreadStatic] + private static StringBuilder stringBuffer = new(8192); + + /// + /// Logs a message in accordance to the specified level, omitted in builds with the Shipping configuration + /// + public static void Log(LogLevel level, string message) { + if (message == null) + throw new ArgumentNullException(nameof(message)); + + log(level, message.StringToBytes()); + } + + /// + /// Creates a log file with the name of assembly if required and writes an exception to it, prints it on the screen, printing on the screen is omitted in builds with the Shipping configuration, but log file will persist + /// + public static void Exception(Exception exception) { + if (exception == null) + throw new ArgumentNullException(nameof(exception)); + + stringBuffer.Clear() + .AppendFormat("Time: {0}", DateTime.Now.ToString("dd/MM/yyyy hh:mm:ss tt")) + .AppendLine().AppendFormat("Message: {0}", exception.Message) + .AppendLine().AppendFormat("StackTrace: {0}", exception.StackTrace) + .AppendLine().AppendFormat("Source: {0}", exception.Source) + .AppendLine(); + + Debug.exception(stringBuffer.ToString().StringToBytes()); + + using (StreamWriter streamWriter = File.AppendText(Application.ProjectDirectory + "Saved/Logs/Exceptions-" + Assembly.GetCallingAssembly().GetName().Name + ".log")) { + streamWriter.WriteLine(stringBuffer); + streamWriter.Close(); + } + } + + /// + /// Prints a debug message on the screen assigned to the key identifier, omitted in builds with the Shipping configuration + /// + public static void AddOnScreenMessage(int key, float timeToDisplay, Color displayColor, string message) { + if (message == null) + throw new ArgumentNullException(nameof(message)); + + addOnScreenMessage(key, timeToDisplay, displayColor.ToArgb(), message.StringToBytes()); + } + + /// + /// Clears any existing debug messages, omitted in builds with the Shipping configuration + /// + public static void ClearOnScreenMessages() => clearOnScreenMessages(); + + /// + /// Draws a debug box, omitted in builds with the Shipping configuration + /// + public static void DrawBox(in Vector3 center, in Vector3 extent, in Quaternion rotation, Color color, bool persistentLines = false, float lifeTime = -1.0f, byte depthPriority = 0, float thickness = 0.0f) => drawBox(center, extent, rotation, color.ToArgb(), persistentLines, lifeTime, depthPriority, thickness); + + /// + /// Draws a debug capsule, omitted in builds with the Shipping configuration + /// + public static void DrawCapsule(in Vector3 center, float halfHeight, float radius, in Quaternion rotation, Color color, bool persistentLines = false, float lifeTime = -1.0f, byte depthPriority = 0, float thickness = 0.0f) => drawCapsule(center, halfHeight, radius, rotation, color.ToArgb(), persistentLines, lifeTime, depthPriority, thickness); + + /// + /// Draws a debug cone, omitted in builds with the Shipping configuration + /// + public static void DrawCone(in Vector3 origin, in Vector3 direction, float length, float angleWidth, float angleHeight, int sides, Color color, bool persistentLines = false, float lifeTime = -1.0f, byte depthPriority = 0, float thickness = 0.0f) => drawCone(origin, direction, length, angleWidth, angleHeight, sides, color.ToArgb(), persistentLines, lifeTime, depthPriority, thickness); + + /// + /// Draws a debug cylinder, omitted in builds with the Shipping configuration + /// + public static void DrawCylinder(in Vector3 start, in Vector3 end, float radius, int segments, Color color, bool persistentLines = false, float lifeTime = -1.0f, byte depthPriority = 0, float thickness = 0.0f) => drawCylinder(start, end, radius, segments, color.ToArgb(), persistentLines, lifeTime, depthPriority, thickness); + + /// + /// Draws a debug sphere, omitted in builds with the Shipping configuration + /// + public static void DrawSphere(in Vector3 center, float radius, int segments, Color color, bool persistentLines = false, float lifeTime = -1.0f, byte depthPriority = 0, float thickness = 0.0f) => drawSphere(center, radius, segments, color.ToArgb(), persistentLines, lifeTime, depthPriority, thickness); + + /// + /// Draws a debug line, omitted in builds with the Shipping configuration + /// + public static void DrawLine(in Vector3 start, in Vector3 end, Color color, bool persistentLines = false, float lifeTime = -1.0f, byte depthPriority = 0, float thickness = 0.0f) => drawLine(start, end, color.ToArgb(), persistentLines, lifeTime, depthPriority, thickness); + + /// + /// Draws a debug point, omitted in builds with the Shipping configuration + /// + public static void DrawPoint(in Vector3 location, float size, Color color, bool persistentLines = false, float lifeTime = -1.0f, byte depthPriority = 0) => drawPoint(location, size, color.ToArgb(), persistentLines, lifeTime, depthPriority); + + /// + /// Flushes persistent debug lines, omitted in builds with the Shipping configuration + /// + public static void FlushPersistentLines() => flushPersistentLines(); + } + + /// + /// Provides information about the application + /// + public static unsafe partial class Application { + /// + /// Returns true if the application can render anything + /// + public static bool IsCanEverRender => isCanEverRender(); + + /// + /// Returns true if current build is meant for release to retail + /// + public static bool IsPackagedForDistribution => isPackagedForDistribution(); + + /// + /// Returns true if current build is packaged for shipping + /// + public static bool IsPackagedForShipping => isPackagedForShipping(); + + /// + /// Returns the project directory + /// + public static string ProjectDirectory { + get { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + getProjectDirectory(stringBuffer); + + return stringBuffer.BytesToString(); + } + } + + /// + /// Returns the default language used by current platform + /// + public static string DefaultLanguage { + get { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + getDefaultLanguage(stringBuffer); + + return stringBuffer.BytesToString(); + } + } + + /// + /// Gets or sets the name of the current project + /// + public static string ProjectName { + get { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + getProjectName(stringBuffer); + + return stringBuffer.BytesToString(); + } + + set { + setProjectName(value.StringToBytes()); + } + } + + /// + /// Gets or sets the current volume multiplier + /// + public static float VolumeMultiplier { + get => getVolumeMultiplier(); + set => setVolumeMultiplier(value); + } + + /// + /// Requests application exit + /// + public static void RequestExit(bool force = false) => requestExit(force); + } + + /// + /// Handles console commands and variables + /// + public static unsafe partial class ConsoleManager { + /// + /// Returns true if a console command or variable has been registered + /// + public static bool IsRegisteredVariable(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return isRegisteredVariable(name.StringToBytes()); + } + + /// + /// Finds a console variable + /// + /// A console variable or null on failure + public static ConsoleVariable FindVariable(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (findVariable(name.StringToBytes()) != IntPtr.Zero) + return new(name); + + return null; + } + + /// + /// Creates and registers a bool console variable, remains alive during the lifetime of the engine until unregistered + /// + /// The name of the variable + /// Help text for the variable + /// A default value + /// If true, cannot be changed by the user from console + /// A console variable or null on failure + public static ConsoleVariable RegisterVariable(string name, string help, bool defaultValue = false, bool readOnly = false) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (help == null) + throw new ArgumentNullException(nameof(help)); + + if (registerVariableBool(name.StringToBytes(), help.StringToBytes(), defaultValue, readOnly) != IntPtr.Zero) + return new(name); + + return null; + } + + /// + /// Creates and registers an integer console variable, remains alive during the lifetime of the engine until unregistered + /// + /// The name of the variable + /// Help text for the variable + /// A default value + /// If true, cannot be changed by the user from console + /// A console variable or null on failure + public static ConsoleVariable RegisterVariable(string name, string help, int defaultValue = 0, bool readOnly = false) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (help == null) + throw new ArgumentNullException(nameof(help)); + + if (registerVariableInt(name.StringToBytes(), help.StringToBytes(), defaultValue, readOnly) != IntPtr.Zero) + return new(name); + + return null; + } + + /// + /// Creates and registers a float console variable, remains alive during the lifetime of the engine until unregistered + /// + /// The name of the variable + /// Help text for the variable + /// A default value + /// If true, cannot be changed by the user from console + /// A console variable or null on failure + public static ConsoleVariable RegisterVariable(string name, string help, float defaultValue = 0.0f, bool readOnly = false) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (help == null) + throw new ArgumentNullException(nameof(help)); + + if (registerVariableFloat(name.StringToBytes(), help.StringToBytes(), defaultValue, readOnly) != IntPtr.Zero) + return new(name); + + return null; + } + + /// + /// Creates and registers a string console variable, remains alive during the lifetime of the engine until unregistered + /// + /// The name of the variable + /// Help text for the variable + /// A default value + /// If true, cannot be changed by the user from console + /// A console variable or null on failure + public static ConsoleVariable RegisterVariable(string name, string help, string defaultValue = null, bool readOnly = false) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (help == null) + throw new ArgumentNullException(nameof(help)); + + if (registerVariableString(name.StringToBytes(), help.StringToBytes(), defaultValue.StringToBytes(), readOnly) != IntPtr.Zero) + return new(name); + + return null; + } + + /// + /// Creates and registers the callback function for a console command that takes no arguments, remains alive during the lifetime of the engine until unregistered + /// + /// The name of the command + /// Help text for the command + /// The function to call when the command is executed + /// If true, cannot be changed by the user from console + public static void RegisterCommand(string name, string help, ConsoleCommandDelegate callback, bool readOnly = false) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (help == null) + throw new ArgumentNullException(nameof(help)); + + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + registerCommand(name.StringToBytes(), help.StringToBytes(), Collector.GetFunctionPointer(callback), readOnly); + } + + /// + /// Deletes and unregisters a console command or variable + /// + public static void UnregisterObject(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + unregisterObject(name.StringToBytes()); + } + } + + /// + /// Functionality for management of engine systems + /// + public static unsafe partial class Engine { + /// + /// Returns true if the game is running in split screen mode + /// + public static bool IsSplitScreen => isSplitScreen(); + + /// + /// Returns true if the script is executing within the editor + /// + public static bool IsEditor => isEditor(); + + /// + /// Returns true if the window has focus + /// + public static bool IsForegroundWindow => isForegroundWindow(); + + /// + /// Returns true if the exit was requested + /// + public static bool IsExitRequested => isExitRequested(); + + /// + /// Returns the current networking mode + /// + /// Networking functionality + public static NetMode NetMode => getNetMode(); + + /// + /// Incremented once per frame before the scene is being rendered + /// + public static uint FrameNumber => getFrameNumber(); + + /// + /// Retrieves the current size of the viewport + /// + public static void GetViewportSize(ref Vector2 value) => getViewportSize(ref value); + + /// + /// Returns the current size of the viewport + /// + public static Vector2 GetViewportSize() { + Vector2 value = default; + + getViewportSize(ref value); + + return value; + } + + /// + /// Retrieves the current resolution of the screen + /// + public static void GetScreenResolution(ref Vector2 value) => getScreenResolution(ref value); + + /// + /// Returns the current resolution of the screen + /// + public static Vector2 GetScreenResolution() { + Vector2 value = default; + + getScreenResolution(ref value); + + return value; + } + + /// + /// Returns the current mode of the window + /// + public static WindowMode WindowMode => getWindowMode(); + + /// + /// Updates the timer between garbage collection such that at the next opportunity garbage collection will be run + /// + /// If true, all possible memory will be freed + public static void ForceGarbageCollection(bool fullPurge = false) => forceGarbageCollection(fullPurge); + + /// + /// Requests a one frame delay of garbage collection + /// + public static void DelayGarbageCollection() => delayGarbageCollection(); + + /// + /// Returns the current engine version + /// + public static string Version { + get { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + getVersion(stringBuffer); + + return stringBuffer.BytesToString(); + } + } + + /// + /// Gets or sets max frames per second, overrides console variable + /// + public static float MaxFPS { + get => getMaxFPS(); + set => setMaxFPS(value); + } + + /// + /// Adds an engine defined action mapping, cannot be remapped + /// + /// Friendly name of action + /// Key to bind in accordance with + /// true if one of the Shift keys must be down when the KeyEvent is received to be acknowledged + /// true if one of the Ctrl keys must be down when the KeyEvent is received to be acknowledged + /// true if one of the Alt keys must be down when the KeyEvent is received to be acknowledged + /// true if one of the Cmd keys must be down when the KeyEvent is received to be acknowledged + public static void AddActionMapping(string actionName, string key, bool shift = false, bool ctrl = false, bool alt = false, bool cmd = false) { + if (actionName == null) + throw new ArgumentNullException(nameof(actionName)); + + if (key == null) + throw new ArgumentNullException(nameof(key)); + + addActionMapping(actionName.StringToBytes(), key.StringToBytes(), shift, ctrl, alt, cmd); + } + + /// + /// Adds an engine defined mapping between an axis and key, cannot be remapped + /// + /// Friendly name of axis + /// Key to bind in accordance with + /// Multiplier to use for the mapping when accumulating the axis value + public static void AddAxisMapping(string axisName, string key, float scale = 1.0f) { + if (axisName == null) + throw new ArgumentNullException(nameof(axisName)); + + if (key == null) + throw new ArgumentNullException(nameof(key)); + + addAxisMapping(axisName.StringToBytes(), key.StringToBytes(), scale); + } + + /// + /// Sets the window title + /// + public static void SetTitle(string title) => setTitle(title.StringToBytes()); + } + + /// + /// Functionality for access to the head-mounted display + /// + public static unsafe partial class HeadMountedDisplay { + /// + /// Returns true if the head-mounted display is connected and ready to use + /// + public static bool IsConnected => isConnected(); + + /// + /// Gets or sets whether the head-mounted display is enabled + /// + public static bool Enabled { + get => getEnabled(); + set => setEnable(value); + } + + /// + /// Gets or sets whether the head-mounted display is in low or full persistence mode + /// + public static bool LowPersistenceMode { + get => getLowPersistenceMode(); + set => setLowPersistenceMode(value); + } + + /// + /// Returns the name of the device + /// + public static string DeviceName { + get { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + getDeviceName(stringBuffer); + + return stringBuffer.BytesToString(); + } + } + } + + /// + /// The top-level representation of a map or a sandbox in which actors and components will exist and rendered + /// + public static unsafe partial class World { + /// + /// Returns the actor count + /// + public static int ActorCount => getActorCount(); + + /// + /// Returns the frame delta time in seconds + /// + public static float DeltaTime => getDeltaSeconds(); + + /// + /// Returns time in seconds since the world was brought up for play, does not stop when the game pauses, not dilated or clamped + /// + public static float RealTime => getRealTimeSeconds(); + + /// + /// Returns time in seconds since the world was brought up for play, it is stopped when the game pauses, it is dilated or clamped + /// + public static float Time => getTimeSeconds(); + + /// + /// Returns the name of the current level + /// + public static string CurrentLevelName { + get { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + getCurrentLevelName(stringBuffer); + + return stringBuffer.BytesToString(); + } + } + + /// + /// Gets or sets physics simulation for the world + /// + public static bool SimulatePhysics { + get => getSimulatePhysics(); + set => setSimulatePhysics(value); + } + + /// + /// Performs the specified action on each actor in the world + /// + public static unsafe void ForEachActor(Action action) where T : Actor { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + ObjectReference* array = null; + int elements = 0; + + forEachActor(ref array, ref elements); + + for (int i = 0; i < elements; i++) { + T actor = array[i].ToActor(); + + if (actor != null) + action(actor); + } + } + + /// + /// Returns the first actor in the world of the specified class, optionally with the specified name, this operation is slow and should be used with caution + /// + /// The name of the actor, may differ from the label in the editor + /// The type of the actor + /// An actor or null on failure + public static T GetActor(string name = null) where T : Actor { + T actor = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = getActor(name.StringToBytes(), actor.Type); + + if (pointer != IntPtr.Zero) { + actor.Pointer = pointer; + + return actor; + } + + return null; + } + + /// + /// Returns the first actor in the world of the specified class and tag, this operation is slow and should be used with caution + /// + /// The tag of the actor + /// The type of the actor + /// An actor or null on failure + public static T GetActorByTag(string tag) where T : Actor { + T actor = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = getActorByTag(tag.StringToBytes(), actor.Type); + + if (pointer != IntPtr.Zero) { + actor.Pointer = pointer; + + return actor; + } + + return null; + } + + /// + /// Returns the first actor in the world of the specified class and ID, this operation is slow and should be used with caution + /// + /// The ID of the actor + /// The type of the actor + /// An actor or null on failure + public static T GetActorByID(uint id) where T : Actor { + T actor = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = getActorByID(id, actor.Type); + + if (pointer != IntPtr.Zero) { + actor.Pointer = pointer; + + return actor; + } + + return null; + } + + /// + /// Returns the first player controller + /// + /// A player controller or null if there is none + public static PlayerController GetFirstPlayerController() { + IntPtr pointer = getFirstPlayerController(); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Returns the current game mode instance which is always valid during gameplay on the server + /// + /// A game mode or null on failure + public static GameModeBase GetGameMode() { + IntPtr pointer = getGameMode(); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Retrieves the current location of the world origin to a reference + /// + public static void GetWorldOrigin(ref Vector3 value) => getWorldOrigin(ref value); + + /// + /// Returns the current location of the world origin + /// + public static Vector3 GetWorldOrigin() { + Vector3 value = default; + + getWorldOrigin(ref value); + + return value; + } + + /// + /// Sets the callback function that is called when actors start overlapping + /// + public static void SetOnActorBeginOverlapCallback(ActorOverlapDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnActorBeginOverlapCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the callback function that is called when actors end overlapping + /// + public static void SetOnActorEndOverlapCallback(ActorOverlapDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnActorEndOverlapCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the callback function that is called when actors hit collisions + /// + public static void SetOnActorHitCallback(ActorHitDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnActorHitCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the callback function that is called when the mouse cursor is moved over an actor if mouse over events are enabled in the player controller + /// + public static void SetOnActorBeginCursorOverCallback(ActorCursorDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnActorBeginCursorOverCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the callback function that is called when the mouse cursor is moved off an actor if mouse over events are enabled in the player controller + /// + public static void SetOnActorEndCursorOverCallback(ActorCursorDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnActorEndCursorOverCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the callback function that is called when the mouse button is clicked while the mouse is over an actor if click events are enabled in the player controller + /// + public static void SetOnActorClickedCallback(ActorKeyDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnActorClickedCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the callback function that is called when the mouse button is released while the mouse is over an actor if click events are enabled in the player controller + /// + public static void SetOnActorReleasedCallback(ActorKeyDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnActorReleasedCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the callback function that is called when primitive components start overlapping + /// + public static void SetOnComponentBeginOverlapCallback(ComponentOverlapDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnComponentBeginOverlapCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the callback function that is called when primitive components end overlapping + /// + public static void SetOnComponentEndOverlapCallback(ComponentOverlapDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnComponentEndOverlapCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the callback function that is called when components hit collisions + /// + public static void SetOnComponentHitCallback(ComponentHitDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnComponentHitCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the callback function that is called when the mouse cursor is moved over a component and mouse over events are enabled in the player controller + /// + public static void SetOnComponentBeginCursorOverCallback(ComponentCursorDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnComponentBeginCursorOverCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the callback function that is called when the mouse cursor is moved off a component and mouse over events are enabled in the player controller + /// + public static void SetOnComponentEndCursorOverCallback(ComponentCursorDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnComponentEndCursorOverCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the callback function that is called when the mouse button is clicked while the mouse is over a component if click events are enabled in the player controller + /// + public static void SetOnComponentClickedCallback(ComponentKeyDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnComponentClickedCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the callback function that is called when the mouse button is released while the mouse is over a component if click events are enabled in the player controller + /// + public static void SetOnComponentReleasedCallback(ComponentKeyDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnComponentReleasedCallback(Collector.GetFunctionPointer(callback)); + } + + /// + /// Sets the gravity applied to all objects in the world + /// + public static void SetGravity(float value) => setGravity(value); + + /// + /// Sets world origin to the specified location + /// + /// true if the world origin was succesfuly shifted, or false if one of the levels are pending visibility update + public static bool SetWorldOrigin(in Vector3 value) => setWorldOrigin(value); + + /// + /// Travels to another level + /// + public static void OpenLevel(string levelName) => openLevel(levelName.StringToBytes()); + + /// + /// Traces a ray against the world using a specific channel + /// + /// true on success + public static bool LineTraceTestByChannel(in Vector3 start, in Vector3 end, CollisionChannel channel, bool traceComplex = false, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) => lineTraceTestByChannel(start, end, channel, traceComplex, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + /// + /// Traces a ray against the world using a specific profile + /// + /// true on success + public static bool LineTraceTestByProfile(in Vector3 start, in Vector3 end, string profileName, bool traceComplex = false, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) => lineTraceTestByProfile(start, end, profileName.StringToBytes(), traceComplex, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + /// + /// Traces a ray against the world using a specific channel and retrieves the first blocking hit + /// + /// true on success + public static bool LineTraceSingleByChannel(in Vector3 start, in Vector3 end, CollisionChannel channel, ref Hit hit, bool traceComplex = false, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) => lineTraceSingleByChannel(start, end, channel, ref hit, null, traceComplex, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + /// + /// Traces a ray against the world using a specific channel and retrieves the first blocking hit with a bone name + /// + /// true on success + public static bool LineTraceSingleByChannel(in Vector3 start, in Vector3 end, CollisionChannel channel, ref Hit hit, ref string boneName, bool traceComplex = false, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + bool result = lineTraceSingleByChannel(start, end, channel, ref hit, stringBuffer, traceComplex, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + boneName = stringBuffer.BytesToString(); + + return result; + } + + /// + /// Traces a ray against the world using a specific profile and retrieves the first blocking hit + /// + /// true on success + public static bool LineTraceSingleByProfile(in Vector3 start, in Vector3 end, string profileName, ref Hit hit, bool traceComplex = false, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) => lineTraceSingleByProfile(start, end, profileName.StringToBytes(), ref hit, null, traceComplex, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + /// + /// Traces a ray against the world using a specific profile and retrieves the first blocking hit with a bone name + /// + /// true on success + public static bool LineTraceSingleByProfile(in Vector3 start, in Vector3 end, string profileName, ref Hit hit, ref string boneName, bool traceComplex = false, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + bool result = lineTraceSingleByProfile(start, end, profileName.StringToBytes(), ref hit, stringBuffer, traceComplex, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + boneName = stringBuffer.BytesToString(); + + return result; + } + + /// + /// Sweeps a shape against the world using a specific profile + /// + /// true on success + public static bool SweepTestByChannel(in Vector3 start, in Vector3 end, in Quaternion rotation, CollisionChannel channel, in CollisionShape shape, bool traceComplex = false, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) => sweepTestByChannel(start, end, rotation, channel, shape, traceComplex, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + /// + /// Sweeps a shape against the world using a specific profile + /// + /// true on success + public static bool SweepTestByProfile(in Vector3 start, in Vector3 end, in Quaternion rotation, string profileName, in CollisionShape shape, bool traceComplex = false, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) => sweepTestByProfile(start, end, rotation, profileName.StringToBytes(), shape, traceComplex, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + /// + /// Sweeps a shape against the world using a specific profile and retrieves the first blocking hit + /// + /// true on success + public static bool SweepSingleByChannel(in Vector3 start, in Vector3 end, in Quaternion rotation, CollisionChannel channel, in CollisionShape shape, ref Hit hit, bool traceComplex = false, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) => sweepSingleByChannel(start, end, rotation, channel, shape, ref hit, null, traceComplex, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + /// + /// Sweeps a shape against the world using a specific profile and retrieves the first blocking hit with a bone name + /// + /// true on success + public static bool SweepSingleByChannel(in Vector3 start, in Vector3 end, in Quaternion rotation, CollisionChannel channel, in CollisionShape shape, ref Hit hit, ref string boneName, bool traceComplex = false, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + bool result = sweepSingleByChannel(start, end, rotation, channel, shape, ref hit, stringBuffer, traceComplex, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + boneName = stringBuffer.BytesToString(); + + return result; + } + + /// + /// Sweeps a shape against the world using a specific profile and retrieves the first blocking hit + /// + /// true on success + public static bool SweepSingleByProfile(in Vector3 start, in Vector3 end, in Quaternion rotation, string profileName, in CollisionShape shape, ref Hit hit, bool traceComplex = false, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) => sweepSingleByProfile(start, end, rotation, profileName.StringToBytes(), shape, ref hit, null, traceComplex, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + /// + /// Sweeps a shape against the world using a specific profile and retrieves the first blocking hit with a bone name + /// + /// true on success + public static bool SweepSingleByProfile(in Vector3 start, in Vector3 end, in Quaternion rotation, string profileName, in CollisionShape shape, ref Hit hit, ref string boneName, bool traceComplex = false, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + bool result = sweepSingleByProfile(start, end, rotation, profileName.StringToBytes(), shape, ref hit, stringBuffer, traceComplex, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + boneName = stringBuffer.BytesToString(); + + return result; + } + + /// + /// Tests the collision shape at the specified location using a specific channel to determine if any blocking or overlapping occurred + /// + /// true on success + public static bool OverlapAnyTestByChannel(in Vector3 location, in Quaternion rotation, CollisionChannel channel, in CollisionShape shape, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) => overlapAnyTestByChannel(location, rotation, channel, shape, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + /// + /// Tests the collision shape at the specified location using a specific profile to determine if any blocking or overlapping occurred + /// + /// true on success + public static bool OverlapAnyTestByProfile(in Vector3 location, in Quaternion rotation, string profileName, in CollisionShape shape, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) => overlapAnyTestByProfile(location, rotation, profileName.StringToBytes(), shape, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + /// + /// Tests the collision shape at the specified location using a specific channel to determine if any blocking occurred + /// + /// true on success + public static bool OverlapBlockingTestByChannel(in Vector3 location, in Quaternion rotation, CollisionChannel channel, in CollisionShape shape, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) => overlapBlockingTestByChannel(location, rotation, channel, shape, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + + /// + /// Tests the collision shape at the specified location using a specific profile to determine if any blocking occurred + /// + /// true on success + public static bool OverlapBlockingTestByProfile(in Vector3 location, in Quaternion rotation, string profileName, in CollisionShape shape, Actor ignoredActor = null, PrimitiveComponent ignoredComponent = null) => overlapBlockingTestByProfile(location, rotation, profileName.StringToBytes(), shape, ignoredActor != null ? ignoredActor.Pointer : IntPtr.Zero, ignoredComponent != null ? ignoredComponent.Pointer : IntPtr.Zero); + } + + /// + /// A representation of the asset + /// + public unsafe partial struct Asset : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + /// + /// Tests for equality between two objects + /// + public static bool operator ==(Asset left, Asset right) => left.Equals(right); + + /// + /// Tests for inequality between two objects + /// + public static bool operator !=(Asset left, Asset right) => !left.Equals(right); + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero && isValid(pointer); + + /// + /// Indicates equality of objects + /// + public bool Equals(Asset other) => IsCreated && pointer == other.pointer; + + /// + /// Indicates equality of objects + /// + public override bool Equals(object value) { + if (value == null) + return false; + + if (!ReferenceEquals(value.GetType(), typeof(Asset))) + return false; + + return Equals((Asset)value); + } + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Returns the name of the asset + /// + public string Name { + get { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + getName(Pointer, stringBuffer); + + return stringBuffer.BytesToString(); + } + } + + /// + /// Returns the path to the asset + /// + public string Path { + get { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + getPath(Pointer, stringBuffer); + + return stringBuffer.BytesToString(); + } + } + } + + /// + /// An asset registry + /// + public unsafe partial class AssetRegistry : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + /// + /// Initializes a new instance of the asset registry + /// + public AssetRegistry() => Pointer = get(); + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero; + + /// + /// Indicates equality of objects + /// + public bool Equals(AssetRegistry other) => IsCreated && pointer == other?.pointer; + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Checks whether the given path contain assets, optionally testing sub-paths + /// + public bool HasAssets(string path, bool recursive = false) { + if (path == null) + throw new ArgumentNullException(nameof(path)); + + return hasAssets(Pointer, path.StringToBytes(), recursive); + } + + /// + /// Performs the specified action on each asset if any + /// + public unsafe void ForEachAsset(Action action, string path, bool recursive = false, bool includeOnlyOnDiskAssets = false) { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + Asset* array = null; + int elements = 0; + + forEachAsset(Pointer, path.StringToBytes(), recursive, includeOnlyOnDiskAssets, ref array, ref elements); + + for (int i = 0; i < elements; i++) { + action(array[i]); + } + } + } + + /// + /// Interface for console objects + /// + public unsafe partial class ConsoleObject : IEquatable { + private string name; + + internal string Name { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return name; + } + + set { + name = value; + } + } + + internal IntPtr Pointer { + get { + IntPtr pointer = ConsoleManager.findVariable(Name.StringToBytes()); + + if (pointer == IntPtr.Zero) + throw new InvalidOperationException(); + + return pointer; + } + } + + private protected ConsoleObject() { } + + internal ConsoleObject(string name) => Name = name; + + /// + /// Returns true if the object is created + /// + public bool IsCreated => name != null && ConsoleManager.isRegisteredVariable(name.StringToBytes()); + + /// + /// Indicates equality of objects + /// + public bool Equals(ConsoleObject other) => IsCreated && name == other?.name; + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => name.GetHashCode(StringComparison.Ordinal); + + /// + /// Returns true if the object is a bool + /// + public bool IsBool => isBool(Pointer); + + /// + /// Returns true if the object is an integer + /// + public bool IsInt => isInt(Pointer); + + /// + /// Returns true if the object is a float + /// + public bool IsFloat => isFloat(Pointer); + + /// + /// Returns true if the object is a string + /// + public bool IsString => isString(Pointer); + } + + /// + /// Interface for console variables + /// + public unsafe partial class ConsoleVariable : ConsoleObject { + private protected ConsoleVariable() { } + + internal ConsoleVariable(string name) => Name = name; + + /// + /// Returns the value as a bool, also works on integers and floats + /// + public bool GetBool() => getBool(Pointer); + + /// + /// Returns the value as an integer, shouldn't be used on strings + /// + public int GetInt() => getInt(Pointer); + + /// + /// Returns the value as a float, works on all types + /// + public float GetFloat() => getFloat(Pointer); + + /// + /// Returns the value as a string, works on all types + /// + public string GetString() { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + getString(Pointer, stringBuffer); + + return stringBuffer.BytesToString(); + } + + /// + /// Sets the value as a bool + /// + public void SetBool(bool value) => setBool(Pointer, value); + + /// + /// Sets the value as an integer + /// + public void SetInt(int value) => setInt(Pointer, value); + + /// + /// Sets the value as a float + /// + public void SetFloat(float value) => setFloat(Pointer, value); + + /// + /// Sets the value as a string + /// + public void SetString(string value) => setString(Pointer, value.StringToBytes()); + + /// + /// Sets the callback function that is called when the console variable value changes + /// + /// The function to call when the value of variable is changed + public void SetOnChangedCallback(ConsoleVariableDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnChangedCallback(Pointer, Collector.GetFunctionPointer(callback)); + } + + /// + /// Clears callback function + /// + public void ClearOnChangedCallback() => clearOnChangedCallback(Pointer); + } + + /// + /// The base class of an object that can be placed or spawned in a level + /// + public unsafe partial class Actor : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsSpawned) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + internal virtual ActorType Type => ActorType.Base; + + private protected Actor() { } + + internal Actor(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public Actor(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Returns true if the actor is spawned + /// + public bool IsSpawned => pointer != IntPtr.Zero && !isPendingKill(pointer); + + /// + /// Returns true if the root component is + /// + public bool IsRootComponentMovable => isRootComponentMovable(Pointer); + + /// + /// Returns true if any component of the actor is overlapping any component of another one + /// + public bool IsOverlappingActor(Actor other) { + if (other == null) + throw new ArgumentNullException(nameof(other)); + + return isOverlappingActor(Pointer, other.Pointer); + } + + /// + /// Performs the specified action on each component if any + /// + public unsafe void ForEachComponent(Action action) where T : ActorComponent { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + ObjectReference* array = null; + int elements = 0; + + forEachComponent(Pointer, ref array, ref elements); + + for (int i = 0; i < elements; i++) { + T component = array[i].ToComponent(); + + if (component != null) + action(component); + } + } + + /// + /// Performs the specified action on each attached actor if any + /// + public unsafe void ForEachAttachedActor(Action action) where T : Actor { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + ObjectReference* array = null; + int elements = 0; + + forEachAttachedActor(Pointer, ref array, ref elements); + + for (int i = 0; i < elements; i++) { + T actor = array[i].ToActor(); + + if (actor != null) + action(actor); + } + } + + /// + /// Performs the specified action on each child actor with , including children of child if any + /// + public unsafe void ForEachChildActor(Action action) where T : Actor { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + ObjectReference* array = null; + int elements = 0; + + forEachChildActor(Pointer, ref array, ref elements); + + for (int i = 0; i < elements; i++) { + T actor = array[i].ToActor(); + + if (actor != null) + action(actor); + } + } + + /// + /// Performs the specified action on each overlapping actor if any + /// + public unsafe void ForEachOverlappingActor(Action action) where T : Actor { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + ObjectReference* array = null; + int elements = 0; + + forEachOverlappingActor(Pointer, ref array, ref elements); + + for (int i = 0; i < elements; i++) { + T actor = array[i].ToActor(); + + if (actor != null) + action(actor); + } + } + + /// + /// Returns the unique ID of the actor, reused by the engine, only unique while the actor is alive + /// + public uint ID => Object.getID(Pointer); + + /// + /// Returns the name of the actor + /// + public string Name { + get { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + Object.getName(Pointer, stringBuffer); + + return stringBuffer.BytesToString(); + } + } + + /// + /// Retrieves the value of the bool property + /// + public bool GetBool(string name, ref bool value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getBool(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the byte property + /// + /// true on success + public bool GetByte(string name, ref byte value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getByte(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the short property + /// + /// true on success + public bool GetShort(string name, ref short value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getShort(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the integer property + /// + /// true on success + public bool GetInt(string name, ref int value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getInt(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the long property + /// + /// true on success + public bool GetLong(string name, ref long value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getLong(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the unsigned short property + /// + /// true on success + public bool GetUShort(string name, ref ushort value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getUShort(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the unsigned integer property + /// + /// true on success + public bool GetUInt(string name, ref uint value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getUInt(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the unsigned long property + /// + /// true on success + public bool GetULong(string name, ref ulong value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getULong(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the float property + /// + /// true on success + public bool GetFloat(string name, ref float value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getFloat(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the double property + /// + /// true on success + public bool GetDouble(string name, ref double value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getDouble(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the enum property + /// + /// true on success + public bool GetEnum(string name, ref T value) where T : Enum { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + int data = 0; + + if (Object.getEnum(Pointer, name.StringToBytes(), ref data)) { + value = (T)Enum.ToObject(typeof(T), data); + + return true; + } + + return false; + } + + /// + /// Retrieves the value of the string property + /// + /// true on success + public bool GetString(string name, ref string value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + if (Object.getString(Pointer, name.StringToBytes(), stringBuffer)) { + value = stringBuffer.BytesToString(); + + return true; + } + + return false; + } + + /// + /// Retrieves the value of the text property + /// + /// true on success + public bool GetText(string name, ref string value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + if (Object.getText(Pointer, name.StringToBytes(), stringBuffer)) { + value = stringBuffer.BytesToString(); + + return true; + } + + return false; + } + + /// + /// Sets the value of the bool property + /// + public bool SetBool(string name, bool value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setBool(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the byte property + /// + /// true on success + public bool SetByte(string name, byte value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setByte(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the short property + /// + /// true on success + public bool SetShort(string name, short value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setShort(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the integer property + /// + /// true on success + public bool SetInt(string name, int value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setInt(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the long property + /// + /// true on success + public bool SetLong(string name, long value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setLong(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the unsigned short property + /// + /// true on success + public bool SetUShort(string name, ushort value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setUShort(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the unsigned integer property + /// + /// true on success + public bool SetUInt(string name, uint value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setUInt(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the unsigned long property + /// + /// true on success + public bool SetULong(string name, ulong value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setULong(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the float property + /// + /// true on success + public bool SetFloat(string name, float value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setFloat(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the double property + /// + /// true on success + public bool SetDouble(string name, double value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setDouble(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the enum property + /// + /// true on success + public bool SetEnum(string name, T value) where T : Enum { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setEnum(Pointer, name.StringToBytes(), Convert.ToInt32(value)); + } + + /// + /// Sets the value of the string property + /// + /// true on success + public bool SetString(string name, string value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (value == null) + throw new ArgumentNullException(nameof(value)); + + return Object.setString(Pointer, name.StringToBytes(), value.StringToBytes()); + } + + /// + /// Sets the value of the text property + /// + /// true on success + public bool SetText(string name, string value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (value == null) + throw new ArgumentNullException(nameof(value)); + + return Object.setText(Pointer, name.StringToBytes(), value.StringToBytes()); + } + + /// + /// Gets or sets the component that handles input for the actor, if enabled + /// + public InputComponent InputComponent { + get { + IntPtr pointer = getInputComponent(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + set { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + setInputComponent(Pointer, value.Pointer); + } + } + + /// + /// Gets the time when the actor was created relative to + /// + public float CreationTime => getCreationTime(Pointer); + + /// + /// Gets or sets whether the all input on the stack below this actor will not be considered + /// + public bool BlockInput { + get => getBlockInput(Pointer); + set => setBlockInput(Pointer, value); + } + + /// + /// Indicates equality of objects + /// + public bool Equals(Actor other) => IsSpawned && pointer == other?.pointer; + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Returns true if the actor is destroyed or already marked for destruction, false if indestructible + /// + public bool Destroy() => destroy(Pointer); + + /// + /// Renames the actor + /// + public void Rename(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + rename(Pointer, name.StringToBytes()); + } + + /// + /// Invokes a command, function, or an event with optional arguments + /// + public bool Invoke(string command) => Object.invoke(Pointer, command.StringToBytes()); + + /// + /// Hides the actor + /// + public void Hide(bool value) => hide(Pointer, value); + + /// + /// Teleports an actor to a new location + /// + /// The target destination point + /// The target rotation at the destination + /// If true, shouldn't cause any notifications (used by AI pathfinding, for example) + /// If true, should skip checking for positioning in the world or relative to other actors trying to slightly move the actor out + /// true if the actor has been successfully moved + public bool TeleportTo(in Vector3 destinationLocation, in Quaternion destinationRotation, bool isATest = false, bool noCheck = false) => teleportTo(Pointer, destinationLocation, destinationRotation, isATest, noCheck); + + /// + /// Returns the distance from this actor to another one + /// + public float GetDistanceTo(Actor actor) { + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + return getDistanceTo(Pointer, actor.Pointer); + } + + /// + /// Returns the distance from this actor to another one, ignoring Z axis + /// + public float GetHorizontalDistanceTo(Actor actor) { + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + return getHorizontalDistanceTo(Pointer, actor.Pointer); + } + + /// + /// Retrieves the bounding box of all components of the actor + /// + /// If true, will only return the bounding box for components with enabled collision + /// The center of the actor in world space + /// Half the actor's size in 3D space + public void GetBounds(bool onlyCollidingComponents, ref Vector3 origin, ref Vector3 extent) => getBounds(Pointer, onlyCollidingComponents, ref origin, ref extent); + + /// + /// Retrieves the point of view of the actor + /// + public void GetEyesViewPoint(ref Vector3 location, ref Quaternion rotation) => getEyesViewPoint(Pointer, ref location, ref rotation); + + /// + /// Returns the component of the actor if matches the specified type, optionally with the specified name + /// + /// The name of the component + /// The type of the component + /// A component or null on failure + public T GetComponent(string name = null) where T : ActorComponent { + T component = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = getComponent(Pointer, name.StringToBytes(), component.Type); + + if (pointer != IntPtr.Zero) { + component.Pointer = pointer; + + return component; + } + + return null; + } + + /// + /// Returns the component of the actor if matches the specified type and tag + /// + /// The tag of the component + /// The type of the component + /// A component or null on failure + public T GetComponentByTag(string tag) where T : ActorComponent { + T component = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = getComponentByTag(Pointer, tag.StringToBytes(), component.Type); + + if (pointer != IntPtr.Zero) { + component.Pointer = pointer; + + return component; + } + + return null; + } + + /// + /// Returns the component of the actor if matches the specified type and ID + /// + /// The ID of the component + /// The type of the component + /// A component or null on failure + public T GetComponentByID(uint id) where T : ActorComponent { + T component = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = getComponentByID(Pointer, id, component.Type); + + if (pointer != IntPtr.Zero) { + component.Pointer = pointer; + + return component; + } + + return null; + } + + /// + /// Returns the root component of the actor if matches the specified type + /// + /// A component or null on failure + public T GetRootComponent() where T : SceneComponent { + T component = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = getRootComponent(Pointer, component.Type); + + if (pointer != IntPtr.Zero) { + component.Pointer = pointer; + + return component; + } + + return null; + } + + /// + /// Sets the root component, the actor should be the owner of the component + /// + /// true if successful + public bool SetRootComponent(SceneComponent component) { + if (component == null) + throw new ArgumentNullException(nameof(component)); + + return setRootComponent(Pointer, component.Pointer); + } + + /// + /// Sets the lifespan of the actor, when it expires the actor will be destroyed, if requested lifespan set to zero, the timer is cleared and the actor will remain alive + /// + /// Lifespan time in seconds + public void SetLifeSpan(float lifeSpan) => setLifeSpan(Pointer, lifeSpan); + + /// + /// Sets for non-pawn actors handled by a + /// + public void SetEnableInput(PlayerController playerController, bool value) { + if (playerController == null) + throw new ArgumentNullException(nameof(playerController)); + + setEnableInput(Pointer, playerController.Pointer, value); + } + + /// + /// Sets the collision detection of the actor + /// + public void SetEnableCollision(bool value) => setEnableCollision(Pointer, value); + + /// + /// Adds a tag to the actor that can be used for grouping and categorizing + /// + public void AddTag(string tag) => addTag(Pointer, tag.StringToBytes()); + + /// + /// Removes a tag from the actor + /// + public void RemoveTag(string tag) => removeTag(Pointer, tag.StringToBytes()); + + /// + /// Indicates whether the actor has a tag + /// + public bool HasTag(string tag) => hasTag(Pointer, tag.StringToBytes()); + + /// + /// Registers an event notification for the actor + /// + public void RegisterEvent(ActorEventType type) => registerEvent(Pointer, type); + + /// + /// Unregisters an event notification for the actor + /// + public void UnregisterEvent(ActorEventType type) => unregisterEvent(Pointer, type); + } + + /// + /// A camera viewpoint that can be placed in a level + /// + public unsafe partial class Camera : Actor { + internal override ActorType Type => ActorType.Camera; + + private protected Camera() { } + + internal Camera(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public Camera(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + } + + /// + /// The base class of actors that used to generate collision events + /// + public abstract unsafe partial class TriggerBase : Actor { + private protected TriggerBase() { } + } + + /// + /// A box shaped trigger with used to generate overlap events + /// + public unsafe partial class TriggerBox : TriggerBase { + internal override ActorType Type => ActorType.TriggerBox; + + private protected TriggerBox() { } + + internal TriggerBox(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public TriggerBox(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + } + + /// + /// A sphere shaped trigger with used to generate overlap events + /// + public unsafe partial class TriggerSphere : TriggerBase { + internal override ActorType Type => ActorType.TriggerSphere; + + private protected TriggerSphere() { } + + internal TriggerSphere(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public TriggerSphere(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + } + + /// + /// A capsule shaped trigger with used to generate overlap events + /// + public unsafe partial class TriggerCapsule : TriggerBase { + internal override ActorType Type => ActorType.TriggerCapsule; + + private protected TriggerCapsule() { } + + internal TriggerCapsule(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public TriggerCapsule(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + } + + /// + /// Defines the game being played, instantiated only on the server + /// + public unsafe partial class GameModeBase : Actor { + internal override ActorType Type => ActorType.GameModeBase; + + private protected GameModeBase() { } + + internal GameModeBase(IntPtr pointer) => Pointer = pointer; + + /// + /// Gets or sets whether the game perform seamless map travels which loads in the background and doesn't disconnect clients + /// + public bool UseSeamlessTravel { + get => getUseSeamlessTravel(Pointer); + set => setUseSeamlessTravel(Pointer, value); + } + + /// + /// Swaps player controllers + /// + public void SwapPlayerControllers(PlayerController playerController, PlayerController newPlayerController) { + if (playerController == null) + throw new ArgumentNullException(nameof(playerController)); + + if (newPlayerController == null) + throw new ArgumentNullException(nameof(newPlayerController)); + + swapPlayerControllers(Pointer, playerController.Pointer, newPlayerController.Pointer); + } + } + + /// + /// The base class of actors that can be possessed by players or AI + /// + public unsafe partial class Pawn : Actor { + internal override ActorType Type => ActorType.Pawn; + + private protected Pawn() { } + + internal Pawn(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public Pawn(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Returns true if the pawn is possesed by a + /// + public bool IsControlled => isControlled(Pointer); + + /// + /// Returns true if the pawn is possesed by a + /// + public bool IsPlayerControlled => isPlayerControlled(Pointer); + + /// + /// Gets or sets the automatic possession type by an AI controller + /// + public AutoPossessAI AutoPossessAI { + get => getAutoPossessAI(Pointer); + set => setAutoPossessAI(Pointer, value); + } + + /// + /// Gets or sets the player index for automatic possession by a player controller + /// + public AutoReceiveInput AutoPossessPlayer { + get => getAutoPossessPlayer(Pointer); + set => setAutoPossessPlayer(Pointer, value); + } + + /// + /// Gets or sets whether yaw will be updated to match the controller's control rotation yaw, if controlled by a + /// + public bool UseControllerRotationYaw { + get => getUseControllerRotationYaw(Pointer); + set => setUseControllerRotationYaw(Pointer, value); + } + + /// + /// Gets or sets whether pitch will be updated to match the controller's control rotation pitch, if controlled by a + /// + public bool UseControllerRotationPitch { + get => getUseControllerRotationPitch(Pointer); + set => setUseControllerRotationPitch(Pointer, value); + } + + /// + /// Gets or sets whether roll will be updated to match the controller's control rotation roll, if controlled by a + /// + public bool UseControllerRotationRoll { + get => getUseControllerRotationRoll(Pointer); + set => setUseControllerRotationRoll(Pointer, value); + } + + /// + /// Retrieves vector direction of gravity + /// + public void GetGravityDirection(ref Vector3 value) => getGravityDirection(Pointer, ref value); + + /// + /// Returns vector direction of gravity + /// + public Vector3 GetGravityDirection() { + Vector3 value = default; + + getGravityDirection(Pointer, ref value); + + return value; + } + + /// + /// Returns the AI controller or null on failure + /// + public AIController GetAIController() { + IntPtr pointer = getAIController(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Returns the player controller or null on failure + /// + public PlayerController GetPlayerController() { + IntPtr pointer = getPlayerController(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Adds yaw (turn) input to the controller, if it's a local + /// + public void AddControllerYawInput(float value) => addControllerYawInput(Pointer, value); + + /// + /// Adds pitch (look up) input to the controller, if it's a local + /// + public void AddControllerPitchInput(float value) => addControllerPitchInput(Pointer, value); + + /// + /// Adds roll input to the controller, if it's a local + /// + public void AddControllerRollInput(float value) => addControllerRollInput(Pointer, value); + + /// + /// Adds movement input along the given world direction vector (usually normalized) + /// + /// Direction in world space to apply input + /// Scale to apply to input, 0.5f applies half the normal value, while -1.0 would reverse the direction + /// If true, always add the input, ignoring the result of + public void AddMovementInput(in Vector3 worldDirection, float scaleValue = 1.0f, bool force = false) => addMovementInput(Pointer, worldDirection, scaleValue, force); + } + + /// + /// Represents a character that have a mesh, collision, and built-in movement logic + /// + public unsafe partial class Character : Pawn { + internal override ActorType Type => ActorType.Character; + + private protected Character() { } + + internal Character(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public Character(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Returns true if the character is currently crouched + /// + public bool IsCrouched => isCrouched(Pointer); + + /// + /// Returns true if the character can crouch + /// + public bool CanCrouch => canCrouch(Pointer); + + /// + /// Returns true if the character can jump + /// + public bool CanJump => canJump(Pointer); + + /// + /// Triggers jump if a jump button is pressed + /// + public void CheckJumpInput(float deltaTime) => checkJumpInput(Pointer, deltaTime); + + /// + /// Updates jump input state after checking input + /// + public void ClearJumpInput(float deltaTime) => clearJumpInput(Pointer, deltaTime); + + /// + /// Launches the character using the specified velocity + /// + public void Launch(in Vector3 velocity, bool overrideXY = false, bool overrideZ = false) => launch(Pointer, velocity, overrideXY, overrideZ); + + /// + /// Starts the character crouching on the next update + /// + public void Crouch() => crouch(Pointer); + + /// + /// Stops the character crouching on the next update + /// + public void StopCrouching() => stopCrouching(Pointer); + + /// + /// Starts the character jumping on the next update + /// + public void Jump() => jump(Pointer); + + /// + /// Stops the character from jumping on the next update + /// + public void StopJumping() => stopJumping(Pointer); + + /// + /// Sets the callback function that is called when the character landing after falling + /// + public void SetOnLandedCallback(CharacterLandedDelegate callback) { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + setOnLandedCallback(Pointer, Collector.GetFunctionPointer(callback)); + } + } + + /// + /// Non-physical actors that can possess a to control its actions + /// + public abstract unsafe partial class Controller : Actor { + private protected Controller() { } + + /// + /// Returns true if look input is ignored + /// + public bool IsLookInputIgnored => isLookInputIgnored(Pointer); + + /// + /// Returns true if movement input is ignored + /// + public bool IsMoveInputIgnored => isMoveInputIgnored(Pointer); + + /// + /// Returns true if the controller is a + /// + public bool IsPlayerController => isPlayerController(Pointer); + + /// + /// Returns the controller's pawn or null on failure + /// + public Pawn GetPawn() { + IntPtr pointer = getPawn(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Returns the character or null on failure + /// + public Character GetCharacter() { + IntPtr pointer = getCharacter(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Returns the actor the controller is looking at or null on failure + /// + public Actor GetViewTarget() { + IntPtr pointer = getViewTarget(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Retrieves the control rotation which is a full aim rotation + /// + public void GetControlRotation(ref Quaternion value) => getControlRotation(Pointer, ref value); + + /// + /// Returns the control rotation which is a full aim rotation + /// + public Quaternion GetControlRotation() { + Quaternion value = default; + + getControlRotation(Pointer, ref value); + + return value; + } + + /// + /// Retrieves the target rotation of the pawn + /// + public void GetDesiredRotation(ref Quaternion value) => getDesiredRotation(Pointer, ref value); + + /// + /// Returns the target rotation of the pawn + /// + public Quaternion GetDesiredRotation() { + Quaternion value = default; + + getDesiredRotation(Pointer, ref value); + + return value; + } + + /// + /// Checks line to center and top of other actor + /// + /// The actor whose visibility is being checked + /// Eye position visibility is being checked from, if is passed in, uses current view target's eye position + /// Used only in implementation + /// true if controller's can see other actor + public bool LineOfSightTo(Actor actor, in Vector3 viewPoint, bool alternateChecks = false) { + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + return lineOfSightTo(Pointer, actor.Pointer, viewPoint, alternateChecks); + } + + /// + /// Sets the control rotation which is a full aim rotation + /// + public void SetControlRotation(in Quaternion value) => setControlRotation(Pointer, value); + + /// + /// Sets the initial location and rotation of the controller, as well as the control rotation + /// + public void SetInitialLocationAndRotation(in Vector3 newLocation, in Quaternion newRotation) => setInitialLocationAndRotation(Pointer, newLocation, newRotation); + + /// + /// Locks or unlocks look input, consecutive calls stack up and require the same amount of calls to undo, or can all be undone using + /// + public void SetIgnoreLookInput(bool value) => setIgnoreLookInput(Pointer, value); + + /// + /// Locks or unlocks movement input, consecutive calls stack up and require the same amount of calls to undo, or can all be undone using + /// + public void SetIgnoreMoveInput(bool value) => setIgnoreMoveInput(Pointer, value); + + /// + /// Stops ignoring look input by resetting the ignore look input state + /// + public void ResetIgnoreLookInput() => resetIgnoreLookInput(Pointer); + + /// + /// Stops ignoring move input by resetting the ignore move input state + /// + public void ResetIgnoreMoveInput() => resetIgnoreMoveInput(Pointer); + + /// + /// Handles attaching the controller to the specified pawn + /// + public void Possess(Pawn pawn) { + if (pawn == null) + throw new ArgumentNullException(nameof(pawn)); + + possess(Pointer, pawn.Pointer); + } + + /// + /// Relinquishes control of the pawn + /// + public void Unpossess() => unpossess(Pointer); + } + + /// + /// The base class of controllers for an AI-controlled + /// + public unsafe partial class AIController : Controller { + internal override ActorType Type => ActorType.AIController; + + private protected AIController() { } + + internal AIController(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public AIController(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Gets or sets whether strafing allowed during movement + /// + public bool AllowStrafe { + get => getAllowStrafe(Pointer); + set => setAllowStrafe(Pointer, value); + } + + /// + /// Clears focus for the given priority, will clear focal point as a result + /// + public void ClearFocus(AIFocusPriority priority = AIFocusPriority.High) => clearFocus(Pointer, priority); + + /// + /// Retrieves the final position that controller should be looking at + /// + public void GetFocalPoint(ref Vector3 value) => getFocalPoint(Pointer, ref value); + + /// + /// Returns the final position that controller should be looking at + /// + public Vector3 GetFocalPoint() { + Vector3 value = default; + + getFocalPoint(Pointer, ref value); + + return value; + } + + /// + /// Sets focal point for the given priority as absolute position or offset from base + /// + public void SetFocalPoint(in Vector3 newFocus, AIFocusPriority priority) => setFocalPoint(Pointer, newFocus, priority); + + /// + /// Returns the focused actor or null on failure + /// + public Actor GetFocusActor() { + IntPtr pointer = getFocusActor(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Sets focus actor for the given priority, will set focal point as a result + /// + public void SetFocus(Actor newFocus, AIFocusPriority priority = AIFocusPriority.High) { + if (newFocus == null) + throw new ArgumentNullException(nameof(newFocus)); + + setFocus(Pointer, newFocus.Pointer, priority); + } + } + + /// + /// An actor that is used by human players to control a + /// + public unsafe partial class PlayerController : Controller { + internal override ActorType Type => ActorType.PlayerController; + + private protected PlayerController() { } + + internal PlayerController(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public PlayerController(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Returns true if game is currently paused + /// + public bool IsPaused => isPaused(Pointer); + + /// + /// Gets or sets whether the mouse cursor should be displayed + /// + public bool ShowMouseCursor { + get => getShowMouseCursor(Pointer); + set => setShowMouseCursor(Pointer, value); + } + + /// + /// Gets or sets whether click events should be generated + /// + public bool EnableClickEvents { + get => getEnableClickEvents(Pointer); + set => setEnableClickEvents(Pointer, value); + } + + /// + /// Gets or sets whether the mouse over events should be generated + /// + public bool EnableMouseOverEvents { + get => getEnableMouseOverEvents(Pointer); + set => setEnableMouseOverEvents(Pointer, value); + } + + /// + /// Retrieves the X and Y screen coordinates of the mouse cursor + /// + /// true if successful + public bool GetMousePosition(ref float x, ref float y) => getMousePosition(Pointer, ref x, ref y); + + /// + /// Returns the player representation or null on failure + /// + public Player GetPlayer() { + IntPtr pointer = getPlayer(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Returns the input manager or null on failure + /// + public PlayerInput GetPlayerInput() { + IntPtr pointer = getPlayerInput(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Retrieves the first blocking hit from the position on the screen + /// + /// true on success + public bool GetHitResultAtScreenPosition(in Vector2 screenPosition, CollisionChannel traceChannel, ref Hit hit, bool traceComplex = false) => getHitResultAtScreenPosition(Pointer, screenPosition, traceChannel, ref hit, traceComplex); + + /// + /// Retrieves the first blocking hit under the mouse cursor + /// + /// true on success + public bool GetHitResultUnderCursor(CollisionChannel traceChannel, ref Hit hit, bool traceComplex = false) => getHitResultUnderCursor(Pointer, traceChannel, ref hit, traceComplex); + + /// + /// Positions the mouse cursor in screen space, in pixels + /// + public void SetMousePosition(float x, float y) => setMousePosition(Pointer, x, y); + + /// + /// Executes the command on the object, DumpConsoleCommands command can be used to list all available variables and commands + /// + /// A command to execute, string of commands optionally separated by a | symbol + /// + public void ConsoleCommand(string command, bool writeToLog = false) => consoleCommand(Pointer, command.StringToBytes(), writeToLog); + + /// + /// Pauses a local game + /// + /// true on success + public bool SetPause(bool value) => setPause(Pointer, value); + + /// + /// Sets the view target + /// + /// An actor to set as view target + public void SetViewTarget(Actor newViewTarget) { + if (newViewTarget == null) + throw new ArgumentNullException(nameof(newViewTarget)); + + setViewTarget(Pointer, newViewTarget.Pointer); + } + + /// + /// Sets the view target blending with variable control + /// + /// An actor to set as view target + /// Time taken to blend + /// Exponent, used by certain to control the shape of the curve + /// The blending type + /// If true, lock outgoing view target to last frame's camera position for the remainder of the blend + public void SetViewTargetWithBlend(Actor newViewTarget, float time = 0.0f, float exponent = 0.0f, BlendType type = BlendType.Linear, bool lockOutgoing = false) { + if (newViewTarget == null) + throw new ArgumentNullException(nameof(newViewTarget)); + + setViewTargetWithBlend(Pointer, newViewTarget.Pointer, time, exponent, type, lockOutgoing); + } + + /// + /// Adds yaw (turn) input + /// + public void AddYawInput(float value) => addYawInput(Pointer, value); + + /// + /// Adds pitch (look up) input + /// + public void AddPitchInput(float value) => addPitchInput(Pointer, value); + + /// + /// Adds roll input + /// + public void AddRollInput(float value) => addRollInput(Pointer, value); + } + + /// + /// The base class of brushes for level construction + /// + public unsafe partial class Brush : Actor { + internal override ActorType Type => ActorType.Brush; + + private protected Brush() { } + + internal Brush(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public Brush(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + } + + /// + /// An editable 3D volume placed in a level, different types of volumes perform different functions + /// + public abstract unsafe partial class Volume : Brush { + private protected Volume() { } + + /// + /// Returns true if a point or sphere overlaps the volume + /// + public bool EncompassesPoint(in Vector3 point, float sphereRadius, ref float distanceToPoint) => encompassesPoint(Pointer, point, sphereRadius, ref distanceToPoint); + } + + /// + /// An actor that is used to trigger events + /// + public unsafe partial class TriggerVolume : Volume { + internal override ActorType Type => ActorType.TriggerVolume; + + private protected TriggerVolume() { } + + internal TriggerVolume(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public TriggerVolume(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + } + + /// + /// An actor that is used for post-processing manipulations + /// + public unsafe partial class PostProcessVolume : Volume { + internal override ActorType Type => ActorType.PostProcessVolume; + + private protected PostProcessVolume() { } + + internal PostProcessVolume(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public PostProcessVolume(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Gets or sets whether the volume is enabled + /// + public bool Enabled { + get => getEnabled(Pointer); + set => setEnabled(Pointer, value); + } + + /// + /// Gets or sets the world space radius around the volume that is used for blending if not unbound + /// + public float BlendRadius { + get => getBlendRadius(Pointer); + set => setBlendRadius(Pointer, value); + } + + /// + /// Gets or sets the blend weight, 0.0f indicates no effect, 1.0f indicates full effect + /// + public float BlendWeight { + get => getBlendWeight(Pointer); + set => setBlendWeight(Pointer, value); + } + + /// + /// Gets or sets whether the volume covers the whole world or only the area inside its bounds + /// + public bool Unbound { + get => getUnbound(Pointer); + set => setUnbound(Pointer, value); + } + + /// + /// Gets or sets the priority of the volume + /// + public float Priority { + get => getPriority(Pointer); + set => setPriority(Pointer, value); + } + } + + /// + /// A representation of the level-wide logic defined in the level blueprint + /// + public unsafe partial class LevelScript : Actor { + internal override ActorType Type => ActorType.LevelScript; + + private protected LevelScript() { } + + internal LevelScript(IntPtr pointer) => Pointer = pointer; + } + + /// + /// A sound actor that can be placed in a level + /// + public unsafe partial class AmbientSound : Actor { + internal override ActorType Type => ActorType.AmbientSound; + + private protected AmbientSound() { } + + internal AmbientSound(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public AmbientSound(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + } + + /// + /// A light actor that can be placed in a level + /// + public abstract unsafe partial class Light : Actor { + private protected Light() { } + } + + /// + /// Simulates light that is being emitted from a source that is infinitely far away + /// + public unsafe partial class DirectionalLight : Light { + internal override ActorType Type => ActorType.DirectionalLight; + + private protected DirectionalLight() { } + + internal DirectionalLight(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public DirectionalLight(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + } + + /// + /// Emits light in all directions from the light bulb's tungsten filament + /// + public unsafe partial class PointLight : Light { + internal override ActorType Type => ActorType.PointLight; + + private protected PointLight() { } + + internal PointLight(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public PointLight(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + } + + /// + /// Emits light into the scene from a rectangular plane with a defined width and height + /// + public unsafe partial class RectLight : Light { + internal override ActorType Type => ActorType.RectLight; + + private protected RectLight() { } + + internal RectLight(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public RectLight(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + } + + /// + /// Emits light from a single point in a cone shape + /// + public unsafe partial class SpotLight : Light { + internal override ActorType Type => ActorType.SpotLight; + + private protected SpotLight() { } + + internal SpotLight(IntPtr pointer) => Pointer = pointer; + + /// + /// Spawns the actor in the world + /// + /// The name of the actor + /// The blueprint class to use as a base class, should be equal to the exact type of the actor + public SpotLight(string name = null, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = spawn(name.StringToBytes(), Type, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + } + + /// + /// The base class of playable sound objects + /// + public abstract unsafe partial class SoundBase : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + private protected SoundBase() { } + + internal SoundBase(IntPtr pointer) => Pointer = pointer; + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero && Object.isValid(pointer); + + /// + /// Indicates equality of objects + /// + public bool Equals(SoundBase other) => IsCreated && pointer == other?.pointer; + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Returns the duration of a sound object in seconds + /// + public float Duration => getDuration(Pointer); + } + + /// + /// A playable sound object for raw wave files + /// + public unsafe partial class SoundWave : SoundBase { + private protected SoundWave() { } + + internal SoundWave(IntPtr pointer) => Pointer = pointer; + + /// + /// Finds and loads a sound wave by name + /// + /// A sound wave or null on failure + public static SoundWave Load(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + IntPtr pointer = Object.load(ObjectType.SoundWave, name.StringToBytes()); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Gets or sets whether the sound wave will be looped if played directly + /// + public bool Loop { + get => getLoop(Pointer); + set => setLoop(Pointer, value); + } + } + + /// + /// The base class of animation assets that can be played and evaluated to produce a pose + /// + public abstract unsafe partial class AnimationAsset : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + private protected AnimationAsset() { } + + internal AnimationAsset(IntPtr pointer) => Pointer = pointer; + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero && Object.isValid(pointer); + + /// + /// Indicates equality of objects + /// + public bool Equals(AnimationAsset other) => IsCreated && pointer == other?.pointer; + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + } + + /// + /// The base class of animation sequences + /// + public abstract unsafe partial class AnimationSequenceBase : AnimationAsset { + private protected AnimationSequenceBase() { } + } + + /// + /// A single animation asset that can be played + /// + public unsafe partial class AnimationSequence : AnimationSequenceBase { + private protected AnimationSequence() { } + + internal AnimationSequence(IntPtr pointer) => Pointer = pointer; + + /// + /// Finds and loads an animation sequence by name + /// + /// An animation sequence or null on failure + public static AnimationSequence Load(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + IntPtr pointer = Object.load(ObjectType.AnimationSequence, name.StringToBytes()); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + } + + /// + /// The base class of animation composites + /// + public abstract unsafe partial class AnimationCompositeBase : AnimationSequenceBase { + private protected AnimationCompositeBase() { } + } + + /// + /// A single animation asset that can combine and selectively play animations + /// + public unsafe partial class AnimationMontage : AnimationCompositeBase { + private protected AnimationMontage() { } + + internal AnimationMontage(IntPtr pointer) => Pointer = pointer; + + /// + /// Finds and loads an animation montage by name + /// + /// An animation montage or null on failure + public static AnimationMontage Load(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + IntPtr pointer = Object.load(ObjectType.AnimationMontage, name.StringToBytes()); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + } + + /// + /// An animation instance representation + /// + public unsafe partial class AnimationInstance : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + private protected AnimationInstance() { } + + internal AnimationInstance(IntPtr pointer) => Pointer = pointer; + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero && Object.isValid(pointer); + + /// + /// Retrieves the value of the bool property + /// + public bool GetBool(string name, ref bool value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getBool(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the byte property + /// + /// true on success + public bool GetByte(string name, ref byte value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getByte(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the short property + /// + /// true on success + public bool GetShort(string name, ref short value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getShort(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the integer property + /// + /// true on success + public bool GetInt(string name, ref int value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getInt(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the long property + /// + /// true on success + public bool GetLong(string name, ref long value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getLong(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the unsigned short property + /// + /// true on success + public bool GetUShort(string name, ref ushort value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getUShort(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the unsigned integer property + /// + /// true on success + public bool GetUInt(string name, ref uint value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getUInt(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the unsigned long property + /// + /// true on success + public bool GetULong(string name, ref ulong value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getULong(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the float property + /// + /// true on success + public bool GetFloat(string name, ref float value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getFloat(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the double property + /// + /// true on success + public bool GetDouble(string name, ref double value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getDouble(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the enum property + /// + /// true on success + public bool GetEnum(string name, ref T value) where T : Enum { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + int data = 0; + + if (Object.getEnum(Pointer, name.StringToBytes(), ref data)) { + value = (T)Enum.ToObject(typeof(T), data); + + return true; + } + + return false; + } + + /// + /// Retrieves the value of the string property + /// + /// true on success + public bool GetString(string name, ref string value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + if (Object.getString(Pointer, name.StringToBytes(), stringBuffer)) { + value = stringBuffer.BytesToString(); + + return true; + } + + return false; + } + + /// + /// Retrieves the value of the text property + /// + /// true on success + public bool GetText(string name, ref string value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + if (Object.getText(Pointer, name.StringToBytes(), stringBuffer)) { + value = stringBuffer.BytesToString(); + + return true; + } + + return false; + } + + /// + /// Sets the value of the bool property + /// + public bool SetBool(string name, bool value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setBool(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the byte property + /// + /// true on success + public bool SetByte(string name, byte value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setByte(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the short property + /// + /// true on success + public bool SetShort(string name, short value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setShort(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the integer property + /// + /// true on success + public bool SetInt(string name, int value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setInt(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the long property + /// + /// true on success + public bool SetLong(string name, long value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setLong(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the unsigned short property + /// + /// true on success + public bool SetUShort(string name, ushort value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setUShort(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the unsigned integer property + /// + /// true on success + public bool SetUInt(string name, uint value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setUInt(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the unsigned long property + /// + /// true on success + public bool SetULong(string name, ulong value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setULong(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the float property + /// + /// true on success + public bool SetFloat(string name, float value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setFloat(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the double property + /// + /// true on success + public bool SetDouble(string name, double value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setDouble(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the enum property + /// + /// true on success + public bool SetEnum(string name, T value) where T : Enum { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setEnum(Pointer, name.StringToBytes(), Convert.ToInt32(value)); + } + + /// + /// Sets the value of the string property + /// + /// true on success + public bool SetString(string name, string value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (value == null) + throw new ArgumentNullException(nameof(value)); + + return Object.setString(Pointer, name.StringToBytes(), value.StringToBytes()); + } + + /// + /// Sets the value of the text property + /// + /// true on success + public bool SetText(string name, string value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (value == null) + throw new ArgumentNullException(nameof(value)); + + return Object.setText(Pointer, name.StringToBytes(), value.StringToBytes()); + } + + /// + /// Indicates equality of objects + /// + public bool Equals(AnimationInstance other) => IsCreated && pointer == other?.pointer; + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Invokes a command, function, or an event with optional arguments + /// + public bool Invoke(string command) => Object.invoke(Pointer, command.StringToBytes()); + + /// + /// Returns the current active animation montage or null on failure + /// + public AnimationMontage GetCurrentActiveMontage() { + IntPtr pointer = getCurrentActiveMontage(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Returns true if the animation montage is active and playing + /// + public bool IsPlaying(AnimationMontage montage) { + if (montage == null) + throw new ArgumentNullException(nameof(montage)); + + return isPlaying(Pointer, montage.Pointer); + } + + /// + /// Returns the current play rate of the animation montage + /// + public float GetPlayRate(AnimationMontage montage) { + if (montage == null) + throw new ArgumentNullException(nameof(montage)); + + return getPlayRate(Pointer, montage.Pointer); + } + + /// + /// Returns the current position of the animation montage + /// + public float GetPosition(AnimationMontage montage) { + if (montage == null) + throw new ArgumentNullException(nameof(montage)); + + return getPosition(Pointer, montage.Pointer); + } + + /// + /// Returns the current blend time of the animation montage + /// + public float GetBlendTime(AnimationMontage montage) { + if (montage == null) + throw new ArgumentNullException(nameof(montage)); + + return getBlendTime(Pointer, montage.Pointer); + } + + /// + /// Returns the name of the current animation montage section + /// + public string GetCurrentSection(AnimationMontage montage) { + if (montage == null) + throw new ArgumentNullException(nameof(montage)); + + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + getCurrentSection(Pointer, montage.Pointer, stringBuffer); + + return stringBuffer.BytesToString(); + } + + /// + /// Sets the current play rate of the animation montage + /// + public void SetPlayRate(AnimationMontage montage, float value) { + if (montage == null) + throw new ArgumentNullException(nameof(montage)); + + setPlayRate(Pointer, montage.Pointer, value); + } + + /// + /// Sets the current position of the animation montage + /// + public void SetPosition(AnimationMontage montage, float position) { + if (montage == null) + throw new ArgumentNullException(nameof(montage)); + + setPosition(Pointer, montage.Pointer, position); + } + + /// + /// Sets the next section after + /// + public void SetNextSection(AnimationMontage montage, string sectionToChange, string nextSection) { + if (montage == null) + throw new ArgumentNullException(nameof(montage)); + + setNextSection(Pointer, montage.Pointer, sectionToChange.StringToBytes(), nextSection.StringToBytes()); + } + + /// + /// Plays the animation montage + /// + /// The length of the animation montage in seconds, or 0.0f if failed to play + public float PlayMontage(AnimationMontage montage, float playRate = 1.0f, float timeToStartMontageAt = 0.0f, bool stopAllMontages = true) { + if (montage == null) + throw new ArgumentNullException(nameof(montage)); + + return playMontage(Pointer, montage.Pointer, playRate, timeToStartMontageAt, stopAllMontages); + } + + /// + /// Pauses the animation montage + /// + public void PauseMontage(AnimationMontage montage) { + if (montage == null) + throw new ArgumentNullException(nameof(montage)); + + pauseMontage(Pointer, montage.Pointer); + } + + /// + /// Resumes the paused animation montage + /// + public void ResumeMontage(AnimationMontage montage) { + if (montage == null) + throw new ArgumentNullException(nameof(montage)); + + resumeMontage(Pointer, montage.Pointer); + } + + /// + /// Stops the animation montage, if is null stops all active montages + /// + public void StopMontage(AnimationMontage montage, float blendOutTime) => stopMontage(Pointer, montage != null ? montage.Pointer : IntPtr.Zero, blendOutTime); + + /// + /// Makes the animation montage jump to a named section + /// + public void JumpToSection(AnimationMontage montage, string sectionName) { + if (montage == null) + throw new ArgumentNullException(nameof(montage)); + + if (sectionName == null) + throw new ArgumentNullException(nameof(sectionName)); + + jumpToSection(Pointer, montage.Pointer, sectionName.StringToBytes()); + } + + /// + /// Makes the animation montage jump to the end of a named section + /// + public void JumpToSectionsEnd(AnimationMontage montage, string sectionName) { + if (montage == null) + throw new ArgumentNullException(nameof(montage)); + + if (sectionName == null) + throw new ArgumentNullException(nameof(sectionName)); + + jumpToSectionsEnd(Pointer, montage.Pointer, sectionName.StringToBytes()); + } + } + + /// + /// A player representation + /// + public unsafe partial class Player : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + private protected Player() { } + + internal Player(IntPtr pointer) => Pointer = pointer; + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero && Object.isValid(pointer); + + /// + /// Indicates equality of objects + /// + public bool Equals(Player other) => IsCreated && pointer == other?.pointer; + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Returns the player controller or null on failure + /// + public PlayerController GetPlayerController() { + IntPtr pointer = getPlayerController(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + } + + /// + /// An input manager of + /// + public unsafe partial class PlayerInput : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + private protected PlayerInput() { } + + internal PlayerInput(IntPtr pointer) => Pointer = pointer; + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero && Object.isValid(pointer); + + /// + /// Indicates equality of objects + /// + public bool Equals(PlayerInput other) => IsCreated && pointer == other?.pointer; + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Returns true if a key is pressed + /// + public bool IsKeyPressed(string key) { + if (key == null) + throw new ArgumentNullException(nameof(key)); + + return isKeyPressed(Pointer, key.StringToBytes()); + } + + /// + /// Returns the time a key was pressed + /// + public float GetTimeKeyPressed(string key) { + if (key == null) + throw new ArgumentNullException(nameof(key)); + + return getTimeKeyPressed(Pointer, key.StringToBytes()); + } + + /// + /// Retrieves mouse sensitivity + /// + public void GetMouseSensitivity(ref Vector2 value) => getMouseSensitivity(Pointer, ref value); + + /// + /// Returns mouse sensitivity + /// + public Vector2 GetMouseSensitivity() { + Vector2 value = default; + + getMouseSensitivity(Pointer, ref value); + + return value; + } + + /// + /// Sets mouse sensitivity + /// + public void SetMouseSensitivity(in Vector2 value) => setMouseSensitivity(Pointer, value); + + /// + /// Adds a player-specific action mapping + /// + /// Friendly name of action + /// Key to bind in accordance with + /// true if one of the Shift keys must be down when the KeyEvent is received to be acknowledged + /// true if one of the Ctrl keys must be down when the KeyEvent is received to be acknowledged + /// true if one of the Alt keys must be down when the KeyEvent is received to be acknowledged + /// true if one of the Cmd keys must be down when the KeyEvent is received to be acknowledged + public void AddActionMapping(string actionName, string key, bool shift = false, bool ctrl = false, bool alt = false, bool cmd = false) { + if (actionName == null) + throw new ArgumentNullException(nameof(actionName)); + + if (key == null) + throw new ArgumentNullException(nameof(key)); + + addActionMapping(Pointer, actionName.StringToBytes(), key.StringToBytes(), shift, ctrl, alt, cmd); + } + + /// + /// Adds a player-specific mapping between an axis and key + /// + /// Friendly name of axis + /// Key to bind in accordance with + /// Multiplier to use for the mapping when accumulating the axis value + public void AddAxisMapping(string axisName, string key, float scale = 1.0f) { + if (axisName == null) + throw new ArgumentNullException(nameof(axisName)); + + if (key == null) + throw new ArgumentNullException(nameof(key)); + + addAxisMapping(Pointer, axisName.StringToBytes(), key.StringToBytes(), scale); + } + + /// + /// Removes a player-specific action mapping + /// + public void RemoveActionMapping(string actionName, string key) => removeActionMapping(Pointer, actionName.StringToBytes(), key.StringToBytes()); + + /// + /// Removes a player-specific mapping between an axis and key + /// + public void RemoveAxisMapping(string axisName, string key) => removeAxisMapping(Pointer, axisName.StringToBytes(), key.StringToBytes()); + } + + /// + /// An asset that provides an intuitive node-based interface + /// + public unsafe partial class Blueprint : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + private protected Blueprint() { } + + internal Blueprint(IntPtr pointer) => Pointer = pointer; + + internal bool IsValidClass(ActorType type) => isValidActorClass(Pointer, type); + + internal bool IsValidClass(ComponentType type) => isValidComponentClass(Pointer, type); + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero && Object.isValid(pointer); + + /// + /// Indicates equality of objects + /// + public bool Equals(Blueprint other) => IsCreated && pointer == other?.pointer; + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Finds and loads a blueprint by name + /// + /// A blueprint or null on failure + public static Blueprint Load(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + IntPtr pointer = Object.load(ObjectType.Blueprint, name.StringToBytes()); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + } + + /// + /// A font object that is used to draw text + /// + public unsafe partial class Font : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + private protected Font() { } + + internal Font(IntPtr pointer) => Pointer = pointer; + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero && Object.isValid(pointer); + + /// + /// Indicates equality of objects + /// + public bool Equals(Font other) => IsCreated && pointer == other?.pointer; + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Finds and loads a font by name + /// + /// A font or null on failure + public static Font Load(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + IntPtr pointer = Object.load(ObjectType.Font, name.StringToBytes()); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Retrieves height and width for a string + /// + public void GetStringSize(string text, ref int height, ref int width) => getStringSize(Pointer, text.StringToBytes(), ref height, ref width); + } + + /// + /// A render asset that can be streamed at runtime + /// + public abstract unsafe partial class StreamableRenderAsset : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + private protected StreamableRenderAsset() { } + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero && Object.isValid(pointer); + + /// + /// Indicates equality of objects + /// + public bool Equals(StreamableRenderAsset other) => IsCreated && pointer == other?.pointer; + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + } + + /// + /// A piece of geometry that consists of a static set of polygons + /// + public unsafe partial class StaticMesh : StreamableRenderAsset { + private protected StaticMesh() { } + + internal StaticMesh(IntPtr pointer) => Pointer = pointer; + + /// + /// A basic cone + /// + public static readonly StaticMesh Cone = Load("/Engine/BasicShapes/Cone"); + + /// + /// A basic cylinder + /// + public static readonly StaticMesh Cylinder = Load("/Engine/BasicShapes/Cylinder"); + + /// + /// A basic cube + /// + public static readonly StaticMesh Cube = Load("/Engine/BasicShapes/Cube"); + + /// + /// A basic plane + /// + public static readonly StaticMesh Plane = Load("/Engine/BasicShapes/Plane"); + + /// + /// A basic sphere + /// + public static readonly StaticMesh Sphere = Load("/Engine/BasicShapes/Sphere"); + + /// + /// Finds and loads a static mesh by name + /// + /// A static mesh or null on failure + public static StaticMesh Load(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + IntPtr pointer = Object.load(ObjectType.StaticMesh, name.StringToBytes()); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + } + + /// + /// A geometry bound to a hierarchical skeleton of bones which can be animated + /// + public unsafe partial class SkeletalMesh : StreamableRenderAsset { + private protected SkeletalMesh() { } + + internal SkeletalMesh(IntPtr pointer) => Pointer = pointer; + + /// + /// Finds and loads a skeletal mesh by name + /// + /// A skeletal mesh or null on failure + public static SkeletalMesh Load(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + IntPtr pointer = Object.load(ObjectType.SkeletalMesh, name.StringToBytes()); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + } + + /// + /// A representation of the surface of an object + /// + public abstract unsafe partial class Texture : StreamableRenderAsset { + private protected Texture() { } + } + + /// + /// A texture asset + /// + public unsafe partial class Texture2D : Texture { + private protected Texture2D() { } + + internal Texture2D(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates a texture asset from a raw PNG, JPEG, BMP, or EXR image file + /// + public Texture2D(string filePath) { + if (filePath == null) + throw new ArgumentNullException(nameof(filePath)); + + Pointer = createFromFile(filePath.StringToBytes()); + } + + /// + /// Creates a texture asset from a raw PNG, JPEG, BMP, or EXR image buffer + /// + public Texture2D(byte[] buffer, int length) { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + Pointer = createFromBuffer(buffer, length); + } + + /// + /// Finds and loads a texture by name + /// + /// A texture or null on failure + public static Texture2D Load(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + IntPtr pointer = Object.load(ObjectType.Texture2D, name.StringToBytes()); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Returns true if the runtime texture has an alpha channel that is not completely white + /// + public bool HasAlphaChannel => hasAlphaChannel(Pointer); + + /// + /// Retrieves size of the texture + /// + public void GetSize(ref Vector2 value) => getSize(Pointer, ref value); + + /// + /// Returns size of the texture + /// + public Vector2 GetSize() { + Vector2 value = default; + + getSize(Pointer, ref value); + + return value; + } + + /// + /// Returns the pixel format + /// + public PixelFormat GetPixelFormat() => getPixelFormat(Pointer); + } + + /// + /// The base class of components that define reusable behavior and can be added to different types of actors + /// + public abstract unsafe partial class ActorComponent : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + internal virtual ComponentType Type => ComponentType.Actor; + + private protected ActorComponent() { } + + internal ActorComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero && !Object.isPendingKill(pointer); + + /// + /// Returns the unique ID of the component, reused by the engine, only unique while the component is alive + /// + public uint ID => Object.getID(Pointer); + + /// + /// Returns the name of the component + /// + public string Name { + get { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + Object.getName(Pointer, stringBuffer); + + return stringBuffer.BytesToString(); + } + } + + /// + /// Retrieves the value of the bool property + /// + public bool GetBool(string name, ref bool value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getBool(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the byte property + /// + /// true on success + public bool GetByte(string name, ref byte value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getByte(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the short property + /// + /// true on success + public bool GetShort(string name, ref short value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getShort(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the integer property + /// + /// true on success + public bool GetInt(string name, ref int value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getInt(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the long property + /// + /// true on success + public bool GetLong(string name, ref long value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getLong(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the unsigned short property + /// + /// true on success + public bool GetUShort(string name, ref ushort value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getUShort(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the unsigned integer property + /// + /// true on success + public bool GetUInt(string name, ref uint value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getUInt(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the unsigned long property + /// + /// true on success + public bool GetULong(string name, ref ulong value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getULong(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the float property + /// + /// true on success + public bool GetFloat(string name, ref float value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getFloat(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the double property + /// + /// true on success + public bool GetDouble(string name, ref double value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.getDouble(Pointer, name.StringToBytes(), ref value); + } + + /// + /// Retrieves the value of the enum property + /// + /// true on success + public bool GetEnum(string name, ref T value) where T : Enum { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + int data = 0; + + if (Object.getEnum(Pointer, name.StringToBytes(), ref data)) { + value = (T)Enum.ToObject(typeof(T), data); + + return true; + } + + return false; + } + + /// + /// Retrieves the value of the string property + /// + /// true on success + public bool GetString(string name, ref string value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + if (Object.getString(Pointer, name.StringToBytes(), stringBuffer)) { + value = stringBuffer.BytesToString(); + + return true; + } + + return false; + } + + /// + /// Retrieves the value of the text property + /// + /// true on success + public bool GetText(string name, ref string value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + if (Object.getText(Pointer, name.StringToBytes(), stringBuffer)) { + value = stringBuffer.BytesToString(); + + return true; + } + + return false; + } + + /// + /// Sets the value of the bool property + /// + public bool SetBool(string name, bool value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setBool(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the byte property + /// + /// true on success + public bool SetByte(string name, byte value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setByte(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the short property + /// + /// true on success + public bool SetShort(string name, short value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setShort(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the integer property + /// + /// true on success + public bool SetInt(string name, int value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setInt(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the long property + /// + /// true on success + public bool SetLong(string name, long value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setLong(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the unsigned short property + /// + /// true on success + public bool SetUShort(string name, ushort value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setUShort(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the unsigned integer property + /// + /// true on success + public bool SetUInt(string name, uint value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setUInt(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the unsigned long property + /// + /// true on success + public bool SetULong(string name, ulong value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setULong(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the float property + /// + /// true on success + public bool SetFloat(string name, float value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setFloat(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the double property + /// + /// true on success + public bool SetDouble(string name, double value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setDouble(Pointer, name.StringToBytes(), value); + } + + /// + /// Sets the value of the enum property + /// + /// true on success + public bool SetEnum(string name, T value) where T : Enum { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return Object.setEnum(Pointer, name.StringToBytes(), Convert.ToInt32(value)); + } + + /// + /// Sets the value of the string property + /// + /// true on success + public bool SetString(string name, string value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (value == null) + throw new ArgumentNullException(nameof(value)); + + return Object.setString(Pointer, name.StringToBytes(), value.StringToBytes()); + } + + /// + /// Sets the value of the text property + /// + /// true on success + public bool SetText(string name, string value) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (value == null) + throw new ArgumentNullException(nameof(value)); + + return Object.setText(Pointer, name.StringToBytes(), value.StringToBytes()); + } + + /// + /// Indicates equality of objects + /// + public bool Equals(ActorComponent other) => IsCreated && pointer == other?.pointer; + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Renames the component + /// + public void Rename(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + Object.rename(Pointer, name.StringToBytes()); + } + + /// + /// Invokes a command, function, or an event with optional arguments + /// + public bool Invoke(string command) => Object.invoke(Pointer, command.StringToBytes()); + + /// + /// Unregisters the component, removes it from its outer actor's components array and marks for pending kill + /// + /// Promotes the child component in the hierarchy during the destruction + public void Destroy(bool promoteChild = false) => destroy(Pointer, promoteChild); + + /// + /// Returns true if the component's owner is selected in the editor + /// + /// Editor functionality + public bool IsOwnerSelected => isOwnerSelected(Pointer); + + /// + /// Returns the component's owner actor of the specified class + /// + /// An actor or null on failure + public T GetActor() where T : Actor { + T actor = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = getOwner(Pointer, actor.Type); + + if (pointer != IntPtr.Zero) { + actor.Pointer = pointer; + + return actor; + } + + return null; + } + + /// + /// Adds a tag to the component that can be used for grouping and categorizing + /// + public void AddTag(string tag) => addTag(Pointer, tag.StringToBytes()); + + /// + /// Removes a tag from the component + /// + public void RemoveTag(string tag) => removeTag(Pointer, tag.StringToBytes()); + + /// + /// Indicates whether the component has a tag + /// + public bool HasTag(string tag) => hasTag(Pointer, tag.StringToBytes()); + } + + /// + /// An input component is a transient component that enables an actor to bind various forms of input events to delegate functions + /// + public unsafe partial class InputComponent : ActorComponent { + internal override ComponentType Type => ComponentType.Input; + + private protected InputComponent() { } + + internal InputComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Indicates whether the component has any input bindings + /// + public bool HasBindings => hasBindings(Pointer); + + /// + /// Returns the number of action bindings + /// + public int ActionBindingsNumber => getActionBindingsNumber(Pointer); + + /// + /// Gets or sets whether any components lower on the input stack should be allowed to receive input + /// + public bool BlockInput { + get => getBlockInput(Pointer); + set => setBlockInput(Pointer, value); + } + + /// + /// Gets or sets the priority of the input component when pushed in to the stack + /// + public int Priority { + get => getPriority(Pointer); + set => setPriority(Pointer, value); + } + + /// + /// Removes all action bindings + /// + public void ClearActionBindings() => clearActionBindings(Pointer); + + /// + /// Binds the callback function to an action defined in the project settings, or by using and + /// + /// The name of the action + /// The type of input behavior + /// The function to call when the input is triggered + /// If true, executes even if the game is paused + public void BindAction(string actionName, InputEvent keyEvent, InputDelegate callback, bool executedWhenPaused = false) { + if (actionName == null) + throw new ArgumentNullException(nameof(actionName)); + + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + bindAction(Pointer, actionName.StringToBytes(), keyEvent, executedWhenPaused, Collector.GetFunctionPointer(callback)); + } + + /// + /// Binds the callback function to an axis defined in the project settings, or by using and + /// + /// The name of the axis + /// The function to call while tracking axis + /// If true, executes even if the game is paused + public void BindAxis(string axisName, InputAxisDelegate callback, bool executedWhenPaused = false) { + if (axisName == null) + throw new ArgumentNullException(nameof(axisName)); + + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + bindAxis(Pointer, axisName.StringToBytes(), executedWhenPaused, Collector.GetFunctionPointer(callback)); + } + + /// + /// Removes the action binding + /// + public void RemoveActionBinding(string actionName, InputEvent keyEvent) => removeActionBinding(Pointer, actionName.StringToBytes(), keyEvent); + } + + /// + /// An abstract component that defines functionality for moving a + /// + public abstract unsafe partial class MovementComponent : ActorComponent { + internal override ComponentType Type => ComponentType.Movement; + + private protected MovementComponent() { } + + internal MovementComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Gets or sets whether the movement will be constrained to a plane + /// + public bool ConstrainToPlane { + get => getConstrainToPlane(Pointer); + set => setConstrainToPlane(Pointer, value); + } + + /// + /// Gets or sets whether the updated component will be snapped to the plane when first is attached, if plane constraints are enabled + /// + public bool SnapToPlaneAtStart { + get => getSnapToPlaneAtStart(Pointer); + set => setSnapToPlaneAtStart(Pointer, value); + } + + /// + /// Gets or sets whether should be skipped if the updated component was not recently rendered + /// + public bool UpdateOnlyIfRendered { + get => getUpdateOnlyIfRendered(Pointer); + set => setUpdateOnlyIfRendered(Pointer, value); + } + + /// + /// Gets or sets the plane constraint axis + /// + public PlaneConstraintAxis PlaneConstraint { + get => getPlaneConstraint(Pointer); + set => setPlaneConstraint(Pointer, value); + } + + /// + /// Returns true if it's in physics volume with water flag + /// + public bool IsInWater => isInWater(Pointer); + + /// + /// Retrieves the current velocity of updated component + /// + public void GetVelocity(ref Vector3 value) => getVelocity(Pointer, ref value); + + /// + /// Returns the current velocity of updated component + /// + public Vector3 GetVelocity() { + Vector3 value = default; + + getVelocity(Pointer, ref value); + + return value; + } + + /// + /// Retrieves the normal of the plane that constrains movement, enforced if the plane constraint is enabled + /// + public void GetPlaneConstraintNormal(ref Vector3 value) => getPlaneConstraintNormal(Pointer, ref value); + + /// + /// Returns the normal of the plane that constrains movement, enforced if the plane constraint is enabled + /// + public Vector3 GetPlaneConstraintNormal() { + Vector3 value = default; + + getPlaneConstraintNormal(Pointer, ref value); + + return value; + } + + /// + /// Retrieves the plane constraint origin + /// + public void GetPlaneConstraintOrigin(ref Vector3 value) => getPlaneConstraintOrigin(Pointer, ref value); + + /// + /// Returns the plane constraint origin + /// + public Vector3 GetPlaneConstraintOrigin() { + Vector3 value = default; + + getPlaneConstraintOrigin(Pointer, ref value); + + return value; + } + + /// + /// Returns gravity that affects the component + /// + public float GetGravity() => getGravity(Pointer); + + /// + /// Returns maximum speed of the component in current movement mode + /// + public float GetMaxSpeed() => getMaxSpeed(Pointer); + + /// + /// Sets the current velocity of updated component + /// + public void SetVelocity(in Vector3 value) => setVelocity(Pointer, value); + + /// + /// Sets the normal of the plane that constrains movement, enforced if the plane constraint is enabled + /// + public void SetPlaneConstraintNormal(in Vector3 value) => setPlaneConstraintNormal(Pointer, value); + + /// + /// Sets the origin of the plane that constrains movement, enforced if the plane constraint is enabled + /// + public void SetPlaneConstraintOrigin(in Vector3 value) => setPlaneConstraintOrigin(Pointer, value); + + /// + /// Sets the plane that constrains movement computed from the forward and up vectors, enforced if the plane constraint is enabled + /// + public void SetPlaneConstraintFromVectors(in Vector3 forward, in Vector3 up) => setPlaneConstraintFromVectors(Pointer, forward, up); + + /// + /// Returns true if the current velocity is exceeding the given max speed within a small error tolerance + /// + public bool IsExceedingMaxSpeed(float maxSpeed) => isExceedingMaxSpeed(Pointer, maxSpeed); + + /// + /// Stops movement immediately, zeroes velocity, usually zeros acceleration for components with acceleration + /// + public void StopMovement() => stopMovement(Pointer); + + /// + /// Constrains a direction vector to the plane constraint, if enabled + /// + public void ConstrainDirectionToPlane(in Vector3 direction, ref Vector3 value) => constrainDirectionToPlane(Pointer, direction, ref value); + + /// + /// Constrains a location vector to the plane constraint, if enabled + /// + public void ConstrainLocationToPlane(in Vector3 location, ref Vector3 value) => constrainLocationToPlane(Pointer, location, ref value); + + /// + /// Constrains a normal vector (of unit length) to the plane constraint, if enabled + /// + public void ConstrainNormalToPlane(in Vector3 normal, ref Vector3 value) => constrainNormalToPlane(Pointer, normal, ref value); + } + + /// + /// A component that performs continuous rotation at a specific rotation rate + /// + public unsafe partial class RotatingMovementComponent : MovementComponent { + internal override ComponentType Type => ComponentType.RotatingMovement; + + private protected RotatingMovementComponent() { } + + internal RotatingMovementComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor + /// + /// The actor to attach the component to + /// The name of the component + public RotatingMovementComponent(Actor actor, string name = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + Pointer = create(actor.Pointer, name.StringToBytes()); + } + + /// + /// Gets or sets whether rotation is applied in local or world space + /// + public bool RotationInLocalSpace { + get => getRotationInLocalSpace(Pointer); + set => setRotationInLocalSpace(Pointer, value); + } + + /// + /// Retrieves translation of pivot point around which the component rotates, relative to the current rotation + /// + public void GetPivotTranslation(ref Vector3 value) => getPivotTranslation(Pointer, ref value); + + /// + /// Returns translation of pivot point around which the component rotates, relative to the current rotation + /// + public Vector3 GetPivotTranslation() { + Vector3 value = default; + + getPivotTranslation(Pointer, ref value); + + return value; + } + + /// + /// Retrieves yaw, pitch, and roll rotation rate of the component + /// + public void GetRotationRate(ref Quaternion value) => getRotationRate(Pointer, ref value); + + /// + /// Returns yaw, pitch, and roll rotation rate of the component + /// + public Quaternion GetRotationRate() { + Quaternion value = default; + + getRotationRate(Pointer, ref value); + + return value; + } + + /// + /// Sets translation of pivot point around which the component rotates, relative to the current rotation + /// + public void SetPivotTranslation(in Vector3 value) => setPivotTranslation(Pointer, value); + + /// + /// Sets yaw, pitch, and roll rotation rate of the component + /// + public void SetRotationRate(in Quaternion value) => setRotationRate(Pointer, value); + } + + /// + /// The base class of components that can be transformed or attached, but has no rendering or collision capabilities + /// + public unsafe partial class SceneComponent : ActorComponent { + internal override ComponentType Type => ComponentType.Scene; + + private protected SceneComponent() { } + + internal SceneComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public SceneComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Returns true if the component is attached to the supplied component + /// + public bool IsAttachedToComponent(SceneComponent component) { + if (component == null) + throw new ArgumentNullException(nameof(component)); + + return isAttachedToComponent(Pointer, component.Pointer); + } + + /// + /// Returns true if the component is attached to the actor + /// + public bool IsAttachedToActor(Actor actor) { + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + return isAttachedToActor(Pointer, actor.Pointer); + } + + /// + /// Returns true if the component is visible in the current context + /// + public bool IsVisible => isVisible(Pointer); + + /// + /// Returns true if the a socket with the given name exists + /// + public bool IsSocketExists(string socketName) { + if (socketName == null) + throw new ArgumentNullException(nameof(socketName)); + + return isSocketExists(Pointer, socketName.StringToBytes()); + } + + /// + /// Returns true if the component has any sockets + /// + public bool HasAnySockets => hasAnySockets(Pointer); + + /// + /// Returns true if another scene component can be attached as a child + /// + public bool CanAttachAsChild(SceneComponent childComponent, string socketName = null) { + if (childComponent == null) + throw new ArgumentNullException(nameof(childComponent)); + + return canAttachAsChild(Pointer, childComponent.Pointer, socketName.StringToBytes()); + } + + /// + /// Performs the specified action on each attached child component if any + /// + public unsafe void ForEachAttachedChild(Action action) where T : SceneComponent { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + ObjectReference* array = null; + int elements = 0; + + forEachAttachedChild(Pointer, ref array, ref elements); + + for (int i = 0; i < elements; i++) { + T component = array[i].ToComponent(); + + if (component != null) + action(component); + } + } + + /// + /// Attaches the component to another component, optionally at a named socket + /// + /// true if successful + public bool AttachToComponent(SceneComponent parent, AttachmentTransformRule attachmentRule, string socketName = null) { + if (parent == null) + throw new ArgumentNullException(nameof(parent)); + + return attachToComponent(Pointer, parent.Pointer, attachmentRule, socketName.StringToBytes()); + } + + /// + /// Detaches the component from a parent + /// + public void DetachFromComponent(DetachmentTransformRule detachmentRule) => detachFromComponent(Pointer, detachmentRule); + + /// + /// Activates the component + /// + public void Activate() => activate(Pointer); + + /// + /// Deactivates the component + /// + public void Deactivate() => deactivate(Pointer); + + /// + /// Recalculates the value of the component to world transform + /// + public void UpdateToWorld(TeleportType type = TeleportType.None, UpdateTransformFlags flags = UpdateTransformFlags.None) => updateToWorld(Pointer, type, flags); + + /// + /// Adds a delta to the location of the component in its local reference frame + /// + public void AddLocalOffset(in Vector3 deltaLocation) => addLocalOffset(Pointer, deltaLocation); + + /// + /// Adds a delta to the rotation of the component in its local reference frame + /// + public void AddLocalRotation(in Quaternion deltaRotation) => addLocalRotation(Pointer, deltaRotation); + + /// + /// Adds a delta to the translation of the component relative to its parent + /// + public void AddRelativeLocation(in Vector3 deltaLocation) => addRelativeLocation(Pointer, deltaLocation); + + /// + /// Adds a delta to the rotation of the component relative to its parent + /// + public void AddRelativeRotation(in Quaternion deltaRotation) => addRelativeRotation(Pointer, deltaRotation); + + /// + /// Adds a delta to the transform of the component in its local reference frame, scale is unchanged + /// + public void AddLocalTransform(in Transform deltaTransform) => addLocalTransform(Pointer, deltaTransform); + + /// + /// Adds a delta to the location of the component in world space + /// + public void AddWorldOffset(in Vector3 deltaLocation) => addWorldOffset(Pointer, deltaLocation); + + /// + /// Adds a delta to the rotation of the component in world space + /// + public void AddWorldRotation(in Quaternion deltaRotation) => addWorldRotation(Pointer, deltaRotation); + + /// + /// Adds a delta to the transform of the component in world space, scale is unchanged + /// + public void AddWorldTransform(in Transform deltaTransform) => addWorldTransform(Pointer, deltaTransform); + + /// + /// Returns the name of a socket the component is attached to + /// + public string GetAttachedSocketName() { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + getAttachedSocketName(Pointer, stringBuffer); + + return stringBuffer.BytesToString(); + } + + /// + /// Retrieves calculated bounds of the component + /// + public void GetBounds(in Transform localToWorld, ref Bounds value) => getBounds(Pointer, localToWorld, ref value); + + /// + /// Retrieves location of a socket in world space + /// + public void GetSocketLocation(string socketName, ref Vector3 value) => getSocketLocation(Pointer, socketName.StringToBytes(), ref value); + + /// + /// Returns location of a socket in world space + /// + public Vector3 GetSocketLocation(string socketName) { + Vector3 value = default; + + getSocketLocation(Pointer, socketName.StringToBytes(), ref value); + + return value; + } + + /// + /// Retrieves rotation of a socket in world space + /// + public void GetSocketRotation(string socketName, ref Quaternion value) => getSocketRotation(Pointer, socketName.StringToBytes(), ref value); + + /// + /// Returns rotation of a socket in world space + /// + public Quaternion GetSocketRotation(string socketName) { + Quaternion value = default; + + getSocketRotation(Pointer, socketName.StringToBytes(), ref value); + + return value; + } + + /// + /// Retrieves velocity of the component, or the velocity of the physics body if simulating physics + /// + public void GetVelocity(ref Vector3 value) => getComponentVelocity(Pointer, ref value); + + /// + /// Returns velocity of the component, or the velocity of the physics body if simulating physics + /// + public Vector3 GetVelocity() { + Vector3 value = default; + + getComponentVelocity(Pointer, ref value); + + return value; + } + + /// + /// Retrieves location of the component in world space + /// + public void GetLocation(ref Vector3 value) => getComponentLocation(Pointer, ref value); + + /// + /// Returns location of the component in world space + /// + public Vector3 GetLocation() { + Vector3 value = default; + + getComponentLocation(Pointer, ref value); + + return value; + } + + /// + /// Returns rotation of the component in world space + /// + public void GetRotation(ref Quaternion value) => getComponentRotation(Pointer, ref value); + + /// + /// Returns rotation of the component in world space + /// + public Quaternion GetRotation() { + Quaternion value = default; + + getComponentRotation(Pointer, ref value); + + return value; + } + + /// + /// Retrieves scale of the component in world space + /// + public void GetScale(ref Vector3 value) => getComponentScale(Pointer, ref value); + + /// + /// Returns scale of the component in world space + /// + public Vector3 GetScale() { + Vector3 value = default; + + getComponentScale(Pointer, ref value); + + return value; + } + + /// + /// Returns the transform which assigned to the component + /// + public void GetTransform(ref Transform value) => SceneComponent.getComponentTransform(Pointer, ref value); + + /// + /// Returns the transform which assigned to the component + /// + public Transform GetTransform() { + Transform value = default; + + SceneComponent.getComponentTransform(Pointer, ref value); + + return value; + } + + /// + /// Retrieves the forward X unit direction vector from the component in world space + /// + public void GetForwardVector(ref Vector3 value) => getForwardVector(Pointer, ref value); + + /// + /// Returns the forward X unit direction vector from the component in world space + /// + public Vector3 GetForwardVector() { + Vector3 value = default; + + getForwardVector(Pointer, ref value); + + return value; + } + + /// + /// Retrieves the right Y unit direction vector from the component in world space + /// + public void GetRightVector(ref Vector3 value) => getRightVector(Pointer, ref value); + + /// + /// Returns the right Y unit direction vector from the component in world space + /// + public Vector3 GetRightVector() { + Vector3 value = default; + + getRightVector(Pointer, ref value); + + return value; + } + + /// + /// Retrieves the up Z unit direction vector from the component in world space + /// + public void GetUpVector(ref Vector3 value) => getUpVector(Pointer, ref value); + + /// + /// Returns the up Z unit direction vector from the component in world space + /// + public Vector3 GetUpVector() { + Vector3 value = default; + + getUpVector(Pointer, ref value); + + return value; + } + + /// + /// Sets how often the component is allowed to move at runtime + /// + public void SetMobility(ComponentMobility mobility) => setMobility(Pointer, mobility); + + /// + /// Sets the visibility of the component + /// + public void SetVisibility(bool newVisibility, bool propagateToChildren = false) => setVisibility(Pointer, newVisibility, propagateToChildren); + + /// + /// Sets the location of the component relative to its parent + /// + public void SetRelativeLocation(in Vector3 location) => setRelativeLocation(Pointer, location); + + /// + /// Sets the rotation of the component relative to its parent + /// + public void SetRelativeRotation(in Quaternion rotation) => setRelativeRotation(Pointer, rotation); + + /// + /// Sets the transform of the component relative to its parent + /// + public void SetRelativeTransform(in Transform transform) => setRelativeTransform(Pointer, transform); + + /// + /// Sets the location of the component in world space + /// + public void SetWorldLocation(in Vector3 location) => setWorldLocation(Pointer, location); + + /// + /// Sets the rotation of the component in world space + /// + public void SetWorldRotation(in Quaternion rotation) => setWorldRotation(Pointer, rotation); + + /// + /// Sets the scale of the component world space + /// + public void SetWorldScale(in Vector3 scale) => setWorldScale(Pointer, scale); + + /// + /// Sets the transform of the component in world space + /// + public void SetWorldTransform(in Transform transform) => setWorldTransform(Pointer, transform); + } + + /// + /// A component that is used to play a sound + /// + public unsafe partial class AudioComponent : SceneComponent { + internal override ComponentType Type => ComponentType.Audio; + + private protected AudioComponent() { } + + internal AudioComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public AudioComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Returns true if the sound playing any audio + /// + public bool IsPlaying => isPlaying(Pointer); + + /// + /// Gets or sets whether the audio is paused + /// + public bool Paused { + get => getPaused(Pointer); + set => setPaused(Pointer, value); + } + + /// + /// Sets the sound object + /// + public void SetSound(SoundBase sound) { + if (sound == null) + throw new ArgumentNullException(nameof(sound)); + + setSound(Pointer, sound.Pointer); + } + + /// + /// Plays the audio + /// + public void Play() => play(Pointer); + + /// + /// Stops the audio + /// + public void Stop() => stop(Pointer); + + /// + /// Smoothly starts the audio, can be used instead of + /// + /// Duration to reach + /// The percentage of calculated volume to fade to + /// Fading start time + /// Curve to adjust audio volume + public void FadeIn(float duration, float volumeLevel = 1.0f, float startTime = 0.0f, AudioFadeCurve fadeCurve = AudioFadeCurve.Linear) => fadeIn(Pointer, duration, volumeLevel, startTime, fadeCurve); + + /// + /// Smoothly stops the audio, can be used instead of + /// + /// Duration to reach + /// he percentage of calculated volume to fade to + /// Curve to adjust audio volume + public void FadeOut(float duration, float volumeLevel = 0.0f, AudioFadeCurve fadeCurve = AudioFadeCurve.Linear) => fadeOut(Pointer, duration, volumeLevel, fadeCurve); + } + + /// + /// Represents a camera viewpoint and settings, such as projection type, field of view, and post-process overrides + /// + public unsafe partial class CameraComponent : SceneComponent { + internal override ComponentType Type => ComponentType.Camera; + + private protected CameraComponent() { } + + internal CameraComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public CameraComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Gets or sets whether black bars will be added if the destination view has a different aspect ratio than the camera requested + /// + public bool ConstrainAspectRatio { + get => getConstrainAspectRatio(Pointer); + set => setConstrainAspectRatio(Pointer, value); + } + + /// + /// Gets or sets the aspect ratio if set to true + /// + public float AspectRatio { + get => getAspectRatio(Pointer); + set => setAspectRatio(Pointer, value); + } + + /// + /// Gets or sets the horizontal field of view (in degrees) in perspective mode (ignored in orthographic mode) + /// + public float FieldOfView { + get => getFieldOfView(Pointer); + set => setFieldOfView(Pointer, value); + } + + /// + /// Gets or sets the far plane distance of the orthographic view (in world units) + /// + public float OrthoFarClipPlane { + get => getOrthoFarClipPlane(Pointer); + set => setOrthoFarClipPlane(Pointer, value); + } + + /// + /// Gets or sets the near plane distance of the orthographic view (in world units) + /// + public float OrthoNearClipPlane { + get => getOrthoNearClipPlane(Pointer); + set => setOrthoNearClipPlane(Pointer, value); + } + + /// + /// Gets or sets the desired width of the orthographic view (in world units) + /// + public float OrthoWidth { + get => getOrthoWidth(Pointer); + set => setOrthoWidth(Pointer, value); + } + + /// + /// Gets or sets whether the camera's orientation and position is locked to the head-mounted display + /// + public bool LockToHeadMountedDisplay { + get => getLockToHeadMountedDisplay(Pointer); + set => setLockToHeadMountedDisplay(Pointer, value); + } + + /// + /// Sets the projection mode + /// + public void SetProjectionMode(CameraProjectionMode type) => setProjectionMode(Pointer, type); + } + + /// + /// A component that automatically spawns and destroys a child actor + /// + public unsafe partial class ChildActorComponent : SceneComponent { + internal override ComponentType Type => ComponentType.ChildActor; + + private protected ChildActorComponent() { } + + internal ChildActorComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public ChildActorComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Gets the child actor + /// + /// An actor or null on failure + public T GetChildActor() where T : Actor { + T actor = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = getChildActor(Pointer, actor.Type); + + if (pointer != IntPtr.Zero) { + actor.Pointer = pointer; + + return actor; + } + + return null; + } + + /// + /// Sets the child actor + /// + /// An actor or null on failure + public T SetChildActor() where T : Actor { + T actor = FormatterServices.GetUninitializedObject(typeof(T)) as T; + IntPtr pointer = setChildActor(Pointer, actor.Type); + + if (pointer != IntPtr.Zero) { + actor.Pointer = pointer; + + return actor; + } + + return null; + } + } + + /// + /// A component that maintains its children at a fixed distance from the parent, but will retract the children if there is a collision, and spring back when there is no collision + /// + public unsafe partial class SpringArmComponent : SceneComponent { + internal override ComponentType Type => ComponentType.SpringArm; + + private protected SpringArmComponent() { } + + internal SpringArmComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public SpringArmComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Returns true if the collision test displacement being applied + /// + public bool IsCollisionFixApplied => isCollisionFixApplied(Pointer); + + /// + /// Gets or sets whether draw markers at the camera target (in green) and the lagged position (in yellow) if the camera location lag is enabled + /// + public bool DrawDebugLagMarkers { + get => getDrawDebugLagMarkers(Pointer); + set => setDrawDebugLagMarkers(Pointer, value); + } + + /// + /// Gets or sets whether the collision test is enabled using and to prevent camera clipping into level + /// + public bool CollisionTest { + get => getCollisionTest(Pointer); + set => setCollisionTest(Pointer, value); + } + + /// + /// Gets or sets whether the camera lags behind target position to smooth its movement + /// + public bool CameraPositionLag { + get => getCameraPositionLag(Pointer); + set => setCameraPositionLag(Pointer, value); + } + + /// + /// Gets or sets whether the camera lags behind target rotation to smooth its movement + /// + public bool CameraRotationLag { + get => getCameraRotationLag(Pointer); + set => setCameraRotationLag(Pointer, value); + } + + /// + /// Gets or sets whether the sub-step camera damping so that it handles fluctuating frame rates well + /// + public bool CameraLagSubstepping { + get => getCameraLagSubstepping(Pointer); + set => setCameraLagSubstepping(Pointer, value); + } + + /// + /// Gets or sets whether the component should inherit yaw from the parent component, has no effect if using absolute rotation + /// + public bool InheritYaw { + get => getInheritYaw(Pointer); + set => setInheritYaw(Pointer, value); + } + + /// + /// Gets or sets whether the component should inherit pitch from the parent component, has no effect if using absolute rotation + /// + public bool InheritPitch { + get => getInheritPitch(Pointer); + set => setInheritPitch(Pointer, value); + } + + /// + /// Gets or sets whether the component should inherit roll from the parent component, has no effect if using absolute rotation + /// + public bool InheritRoll { + get => getInheritRoll(Pointer); + set => setInheritRoll(Pointer, value); + } + + /// + /// Gets or sets a max distance the camera target may lag behind the current location + /// + public float CameraLagMaxDistance { + get => getCameraLagMaxDistance(Pointer); + set => setCameraLagMaxDistance(Pointer, value); + } + + /// + /// Gets or sets a max time step used when sub-stepping camera lag + /// + public float CameraLagMaxTimeStep { + get => getCameraLagMaxTimeStep(Pointer); + set => setCameraLagMaxTimeStep(Pointer, value); + } + + /// + /// Gets or sets how quickly the camera reaches a target position + /// + public float CameraPositionLagSpeed { + get => getCameraPositionLagSpeed(Pointer); + set => setCameraPositionLagSpeed(Pointer, value); + } + + /// + /// Gets or sets how quickly еру camera reaches a target rotation + /// + public float CameraRotationLagSpeed { + get => getCameraRotationLagSpeed(Pointer); + set => setCameraRotationLagSpeed(Pointer, value); + } + + /// + /// Gets or sets the collision channel of the query probe ( by default) + /// + public CollisionChannel ProbeChannel { + get => getProbeChannel(Pointer); + set => setProbeChannel(Pointer, value); + } + + /// + /// Gets or sets how big should be the query probe sphere + /// + public float ProbeSize { + get => getProbeSize(Pointer); + set => setProbeSize(Pointer, value); + } + + /// + /// Gets or sets the natural length of the spring arm when there are no collisions + /// + public float TargetArmLength { + get => getTargetArmLength(Pointer); + set => setTargetArmLength(Pointer, value); + } + + /// + /// Gets or sets if the component should use the view/control rotation of the pawn + /// + public bool UsePawnControlRotation { + get => getUsePawnControlRotation(Pointer); + set => setUsePawnControlRotation(Pointer, value); + } + + /// + /// Retrieves offset at the end of the spring arm, can be used instead of the relative offset of the attached component to ensure the line trace works as desired + /// + public void GetSocketOffset(ref Vector3 value) => getSocketOffset(Pointer, ref value); + + /// + /// Returns offset at the end of the spring arm, can be used instead of the relative offset of the attached component to ensure the line trace works as desired + /// + public Vector3 GetSocketOffset() { + Vector3 value = default; + + getSocketOffset(Pointer, ref value); + + return value; + } + + /// + /// Retrieves offset at the start of the spring arm in world space + /// + public void GetTargetOffset(ref Vector3 value) => getTargetOffset(Pointer, ref value); + + /// + /// Returns offset at the start of the spring arm in world space + /// + public Vector3 GetTargetOffset() { + Vector3 value = default; + + getTargetOffset(Pointer, ref value); + + return value; + } + + /// + /// Retrieves the unfixed camera position + /// + public void GetUnfixedCameraPosition(ref Vector3 value) => getUnfixedCameraPosition(Pointer, ref value); + + /// + /// Returns the unfixed camera position + /// + public Vector3 GetUnfixedCameraPosition() { + Vector3 value = default; + + getUnfixedCameraPosition(Pointer, ref value); + + return value; + } + + /// + /// Retrieves the desired rotation for the spring arm, before the rotation constraints such as , , or are enforced + /// + public void GetDesiredRotation(ref Quaternion value) => getDesiredRotation(Pointer, ref value); + + /// + /// Returns the desired rotation for the spring arm, before the rotation constraints such as , , or are enforced + /// + public Quaternion GetDesiredRotation() { + Quaternion value = default; + + getDesiredRotation(Pointer, ref value); + + return value; + } + + /// + /// Retrieves the target inherited rotation + /// + public void GetTargetRotation(ref Quaternion value) => getTargetRotation(Pointer, ref value); + + /// + /// Returns the target inherited rotation + /// + public Quaternion GetTargetRotation() { + Quaternion value = default; + + getTargetRotation(Pointer, ref value); + + return value; + } + + /// + /// Sets offset at the end of the spring arm, can be used instead of the relative offset of the attached component to ensure the line trace works as desired + /// + public void SetSocketOffset(in Vector3 value) => setSocketOffset(Pointer, value); + + /// + /// Sets offset at the start of the spring arm in world space + /// + public void SetTargetOffset(in Vector3 value) => setTargetOffset(Pointer, value); + } + + /// + /// A component that is used for post-processing manipulations + /// + public unsafe partial class PostProcessComponent : SceneComponent { + internal override ComponentType Type => ComponentType.PostProcess; + + private protected PostProcessComponent() { } + + internal PostProcessComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public PostProcessComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Gets or sets whether the volume is enabled + /// + public bool Enabled { + get => getEnabled(Pointer); + set => setEnabled(Pointer, value); + } + + /// + /// Gets or sets the world space radius around the volume that is used for blending if not unbound + /// + public float BlendRadius { + get => getBlendRadius(Pointer); + set => setBlendRadius(Pointer, value); + } + + /// + /// Gets or sets the blend weight, 0.0f indicates no effect, 1.0f indicates full effect + /// + public float BlendWeight { + get => getBlendWeight(Pointer); + set => setBlendWeight(Pointer, value); + } + + /// + /// Gets or sets whether the volume covers the whole world or only the area inside its bounds + /// + public bool Unbound { + get => getUnbound(Pointer); + set => setUnbound(Pointer, value); + } + + /// + /// Gets or sets the priority of the volume + /// + public float Priority { + get => getPriority(Pointer); + set => setPriority(Pointer, value); + } + } + + /// + /// An abstract component that contains or generates some sort of geometry, generally to be rendered or used as collision data + /// + public abstract unsafe partial class PrimitiveComponent : SceneComponent { + private protected PrimitiveComponent() { } + + /// + /// Returns true if the component is affected by gravity, always returns false if physics simulation is disabled for the component + /// + public bool IsGravityEnabled => isGravityEnabled(Pointer); + + /// + /// Returns true if the component is overlapping another component + /// + public bool IsOverlappingComponent(PrimitiveComponent other) { + if (other == null) + throw new ArgumentNullException(nameof(other)); + + return isOverlappingComponent(Pointer, other.Pointer); + } + + /// + /// Performs the specified action on each overlapping component if any + /// + public unsafe void ForEachOverlappingComponent(Action action) where T : PrimitiveComponent { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + ObjectReference* array = null; + int elements = 0; + + forEachOverlappingComponent(Pointer, ref array, ref elements); + + for (int i = 0; i < elements; i++) { + T component = array[i].ToComponent(); + + if (component != null) + action(component); + } + } + + /// + /// Returns approximate mass in kilograms + /// + public float Mass => getMass(Pointer); + + /// + /// Gets or sets whether the component should cast a shadow + /// + public bool CastShadow { + get => getCastShadow(Pointer); + set => setCastShadow(Pointer, value); + } + + /// + /// Gets or sets the component visibility when the view actor is the component's owner + /// + public bool OnlyOwnerSee { + get => getOnlyOwnerSee(Pointer); + set => setOnlyOwnerSee(Pointer, value); + } + + /// + /// Gets or sets whether the component would not be visible when the view actor is the component's owner, directly or indirectly + /// + public bool OwnerNoSee { + get => getOwnerNoSee(Pointer); + set => setOwnerNoSee(Pointer, value); + } + + /// + /// Gets or sets whether the component should ignore radial forces + /// + public bool IgnoreRadialForce { + get => getIgnoreRadialForce(Pointer); + set => setIgnoreRadialForce(Pointer, value); + } + + /// + /// Gets or sets whether the component should ignore radial impulses + /// + public bool IgnoreRadialImpulse { + get => getIgnoreRadialImpulse(Pointer); + set => setIgnoreRadialImpulse(Pointer, value); + } + + /// + /// Gets or sets the angular damping of the component + /// + public float AngularDamping { + get => getAngularDamping(Pointer); + set => setAngularDamping(Pointer, value); + } + + /// + /// Gets or sets the linear damping of the component + /// + public float LinearDamping { + get => getLinearDamping(Pointer); + set => setLinearDamping(Pointer, value); + } + + /// + /// Returns number of material elements in the primitive + /// + public int MaterialsNumber => getMaterialsNumber(Pointer); + + /// + /// Adds an angular impulse in degrees to a rigid body + /// + /// Magnitude and direction of the impulse to apply, the direction is the axis of rotation + /// If applied to , the name of the body to apply an angular impulse to, or null to indicate the root body + /// If true, is taken as a change in velocity instead of a physical force (the mass will have no effect) + public void AddAngularImpulseInDegrees(in Vector3 impulse, string boneName = null, bool velocityChange = false) => addAngularImpulseInDegrees(Pointer, impulse, boneName.StringToBytes(), velocityChange); + + /// + /// Adds an angular impulse in radians to a rigid body + /// + /// Magnitude and direction of the impulse to apply, the direction is the axis of rotation + /// If applied to , the name of the body to apply an angular impulse to, or null to indicate the root body + /// If true, is taken as a change in velocity instead of a physical force (the mass will have no effect) + public void AddAngularImpulseInRadians(in Vector3 impulse, string boneName = null, bool velocityChange = false) => addAngularImpulseInRadians(Pointer, impulse, boneName.StringToBytes(), velocityChange); + + /// + /// Adds a force to a rigid body + /// + /// Force vector to apply, magnitude indicates strength of force + /// If applied to , the name of the body to apply an angular impulse to, or null to indicate the root body + /// If true, is taken as a change in acceleration instead of a physical force (the mass will have no effect) + public void AddForce(in Vector3 force, string boneName = null, bool accelerationChange = false) => addForce(Pointer, force, boneName.StringToBytes(), accelerationChange); + + /// + /// Adds a force to a rigid body at a specific location, optionally in local space + /// + /// Force vector to apply, magnitude indicates strength of force + /// A point in world or local space to apply the force at + /// If applied to , the name of the body to apply an angular impulse to, or null to indicate the root body + /// If true, applies force in local space instead of world space + public void AddForceAtLocation(in Vector3 force, in Vector3 location, string boneName = null, bool localSpace = false) => addForceAtLocation(Pointer, force, location, boneName.StringToBytes(), localSpace); + + /// + /// Adds an impulse to a rigid body + /// + /// Magnitude and direction of the impulse to apply + /// If applied to , the name of the body to apply an angular impulse to, or null to indicate the root body + /// If true, is taken as a change in velocity instead of a physical force (the mass will have no effect) + public void AddImpulse(in Vector3 impulse, string boneName = null, bool velocityChange = false) => addImpulse(Pointer, impulse, boneName.StringToBytes(), velocityChange); + + /// + /// Adds an impulse to a rigid body at a specific location + /// + /// Magnitude and direction of the impulse to apply + /// A point in world space to apply the impulse at + /// If applied to , the name of the body to apply an angular impulse to, or null to indicate the root body + public void AddImpulseAtLocation(in Vector3 impulse, in Vector3 location, string boneName = null) => addImpulseAtLocation(Pointer, impulse, location, boneName.StringToBytes()); + + /// + /// Adds a force to all rigid bodies in the component, originating from the supplied world-space location + /// + /// Origin of the force in world space + /// Radius within which to apply the force + /// Strength of the force to apply + /// If true, the force will lose its strength linearly + /// If true, is taken as a change in acceleration instead of a physical force (the mass will have no effect) + public void AddRadialForce(in Vector3 origin, float radius, float strength, bool linearFalloff = false, bool accelerationChange = false) => addRadialForce(Pointer, origin, radius, strength, linearFalloff, accelerationChange); + + /// + /// Adds an impulse to all rigid bodies in the component, originating from the supplied world-space location + /// + /// Origin of the impulse in world space + /// Radius within which to apply the impulse + /// Strength of the impulse to apply + /// If true, the force will lose its strength linearly + /// If true, is taken as a change in acceleration instead of a physical force (the mass will have no effect) + public void AddRadialImpulse(in Vector3 origin, float radius, float strength, bool linearFalloff = false, bool accelerationChange = false) => addRadialImpulse(Pointer, origin, radius, strength, linearFalloff, accelerationChange); + + /// + /// Adds a torque in degrees to a rigid body + /// + /// Torque to apply, direction is axis of rotation and magnitude is strength of the torque + /// If applied to , the name of the body to apply an angular impulse to, or null to indicate the root body + /// If true, is taken as a change in acceleration instead of a physical force (the mass will have no effect) + public void AddTorqueInDegrees(in Vector3 torque, string boneName = null, bool accelerationChange = false) => addTorqueInDegrees(Pointer, torque, boneName.StringToBytes(), accelerationChange); + + /// + /// Adds a torque in radians to a rigid body + /// + /// Torque to apply, direction is axis of rotation and magnitude is strength of the torque + /// If applied to , the name of the body to apply an angular impulse to, or null to indicate the root body + /// If true, is taken as a change in acceleration instead of a physical force (the mass will have no effect) + public void AddTorqueInRadians(in Vector3 torque, string boneName = null, bool accelerationChange = false) => addTorqueInRadians(Pointer, torque, boneName.StringToBytes(), accelerationChange); + + /// + /// Retrieves the linear velocity of a single body + /// + public void GetPhysicsLinearVelocity(ref Vector3 value, string boneName = null) => getPhysicsLinearVelocity(Pointer, ref value, boneName.StringToBytes()); + + /// + /// Returns the linear velocity of a single body + /// + public Vector3 GetPhysicsLinearVelocity(string boneName = null) { + Vector3 value = default; + + getPhysicsLinearVelocity(Pointer, ref value, boneName.StringToBytes()); + + return value; + } + + /// + /// Retrieves the linear velocity of a point on a single body + /// + public void GetPhysicsLinearVelocityAtPoint(ref Vector3 value, in Vector3 point, string boneName = null) => getPhysicsLinearVelocityAtPoint(Pointer, ref value, point, boneName.StringToBytes()); + + /// + /// Returns the linear velocity of a point on a single body + /// + public Vector3 GetPhysicsLinearVelocityAtPoint(in Vector3 point, string boneName = null) { + Vector3 value = default; + + getPhysicsLinearVelocityAtPoint(Pointer, ref value, point, boneName.StringToBytes()); + + return value; + } + + /// + /// Retrieves the angular velocity in degrees of a single body + /// + public void GetPhysicsAngularVelocityInDegrees(ref Vector3 value, string boneName = null) => getPhysicsAngularVelocityInDegrees(Pointer, ref value, boneName.StringToBytes()); + + /// + /// Returns the angular velocity in degrees of a single body + /// + public Vector3 GetPhysicsAngularVelocityInDegrees(string boneName = null) { + Vector3 value = default; + + getPhysicsAngularVelocityInDegrees(Pointer, ref value, boneName.StringToBytes()); + + return value; + } + + /// + /// Retrieves the angular velocity in radians of a single body + /// + public void GetPhysicsAngularVelocityInRadians(ref Vector3 value, string boneName = null) => getPhysicsAngularVelocityInRadians(Pointer, ref value, boneName.StringToBytes()); + + /// + /// Returns the angular velocity in radians of a single body + /// + public Vector3 GetPhysicsAngularVelocityInRadians(string boneName = null) { + Vector3 value = default; + + getPhysicsAngularVelocityInRadians(Pointer, ref value, boneName.StringToBytes()); + + return value; + } + + /// + /// Returns the material at the specified element index or null on failure + /// + public MaterialInstanceDynamic GetMaterial(int elementIndex) { + IntPtr pointer = getMaterial(Pointer, elementIndex); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Retrieves distance to closest collision + /// + /// + /// + /// More than 0.0f if successful, equals to 0.0f if a point is inside the geometry, less than 0.0f if the primitive does not have collision or if the geometry not supported + public float GetDistanceToCollision(in Vector3 point, ref Vector3 closestPointOnCollision) => getDistanceToCollision(Pointer, point, ref closestPointOnCollision); + + /// + /// Retrieves squared distance to closest collision + /// + /// + /// + /// + /// true if a distance to the body was found and has been populated + public bool GetSquaredDistanceToCollision(in Vector3 point, ref float squaredDistance, ref Vector3 closestPointOnCollision) => getSquaredDistanceToCollision(Pointer, point, ref squaredDistance, ref closestPointOnCollision); + + /// + /// Sets the material applied to an element of the mesh + /// + /// The element to access the material of + /// A material + public void SetMaterial(int elementIndex, MaterialInterface material) { + if (material == null) + throw new ArgumentNullException(nameof(material)); + + setMaterial(Pointer, elementIndex, material.Pointer); + } + + /// + /// Sets whether the component should generate overlap events when it's overlaps other components + /// + public void SetGenerateOverlapEvents(bool value) => setGenerateOverlapEvents(Pointer, value); + + /// + /// Sets whether the component should generate hit events when it's collides with other components + /// + public void SetGenerateHitEvents(bool value) => setGenerateHitEvents(Pointer, value); + + /// + /// Sets the mass in kilograms of a rigid body + /// + public void SetMass(float mass, string boneName = null) => setMass(Pointer, mass, boneName.StringToBytes()); + + /// + /// Sets the center of mass of a single body + /// + public void SetCenterOfMass(in Vector3 offset, string boneName = null) => setCenterOfMass(Pointer, offset, boneName.StringToBytes()); + + /// + /// Sets the linear velocity of a single body + /// + public void SetPhysicsLinearVelocity(in Vector3 velocity, bool addToCurrent = false, string boneName = null) => setPhysicsLinearVelocity(Pointer, velocity, addToCurrent, boneName.StringToBytes()); + + /// + /// Sets the angular velocity in degrees of a single body + /// + public void SetPhysicsAngularVelocityInDegrees(in Vector3 angularVelocity, bool addToCurrent = false, string boneName = null) => setPhysicsAngularVelocityInDegrees(Pointer, angularVelocity, addToCurrent, boneName.StringToBytes()); + + /// + /// Sets the angular velocity in radians of a single body + /// + public void SetPhysicsAngularVelocityInRadians(in Vector3 angularVelocity, bool addToCurrent = false, string boneName = null) => setPhysicsAngularVelocityInRadians(Pointer, angularVelocity, addToCurrent, boneName.StringToBytes()); + + /// + /// Sets the maximum angular velocity in degrees of a single body + /// + public void SetPhysicsMaxAngularVelocityInDegrees(float maxAngularVelocity, bool addToCurrent = false, string boneName = null) => setPhysicsMaxAngularVelocityInDegrees(Pointer, maxAngularVelocity, addToCurrent, boneName.StringToBytes()); + + /// + /// Sets the maximum angular velocity in radians of a single body + /// + public void SetPhysicsMaxAngularVelocityInRadians(float maxAngularVelocity, bool addToCurrent = false, string boneName = null) => setPhysicsMaxAngularVelocityInRadians(Pointer, maxAngularVelocity, addToCurrent, boneName.StringToBytes()); + + /// + /// Sets whether a single body should use physics simulation, or should be kinematic, if the component is currently attached to something, beginning simulation will detach it + /// + public void SetSimulatePhysics(bool value) => setSimulatePhysics(Pointer, value); + + /// + /// Sets whether the component is affected by gravity, applies only to components with enabled physics simulation + /// + public void SetEnableGravity(bool value) => setEnableGravity(Pointer, value); + + /// + /// Sets the collision mode of the component + /// + public void SetCollisionMode(CollisionMode mode) => setCollisionMode(Pointer, mode); + + /// + /// Sets the collision channel of the component + /// + public void SetCollisionChannel(CollisionChannel channel) => setCollisionChannel(Pointer, channel); + + /// + /// Sets the collision profile name of the component + /// + public void SetCollisionProfileName(string profileName, bool updateOverlaps = true) => setCollisionProfileName(Pointer, profileName.StringToBytes(), updateOverlaps); + + /// + /// Sets the collision response to channel of the component + /// + public void SetCollisionResponseToChannel(CollisionChannel channel, CollisionResponse response) => setCollisionResponseToChannel(Pointer, channel, response); + + /// + /// Sets the collision response to all channels of the component + /// + public void SetCollisionResponseToAllChannels(CollisionResponse response) => setCollisionResponseToAllChannels(Pointer, response); + + /// + /// Sets whether to ignore collision of all components of a specified actor during the movement + /// + public void SetIgnoreActorWhenMoving(Actor actor, bool value) { + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + setIgnoreActorWhenMoving(Pointer, actor.Pointer, value); + } + + /// + /// Sets whether to ignore collision of a specified component during the movement + /// + public void SetIgnoreComponentWhenMoving(PrimitiveComponent component, bool value) { + if (component == null) + throw new ArgumentNullException(nameof(component)); + + setIgnoreComponentWhenMoving(Pointer, component.Pointer, value); + } + + /// + /// Clears the list of actors that ignored during the movement + /// + public void ClearMoveIgnoreActors() => clearMoveIgnoreActors(Pointer); + + /// + /// Clears the list of components that ignored during the movement + /// + public void ClearMoveIgnoreComponents() => clearMoveIgnoreComponents(Pointer); + + /// + /// Creates a dynamic material instance for the specified element index, the parent of the instance is set to the material being replaced + /// + /// The index of the skin to replace the material for, if invalid, the material is unchanged and null is returned + /// A material instance or null on failure + public MaterialInstanceDynamic CreateAndSetMaterialInstanceDynamic(int elementIndex) { + IntPtr pointer = createAndSetMaterialInstanceDynamic(Pointer, elementIndex); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Registers an event notification for the primitive component + /// + public void RegisterEvent(ComponentEventType type) => registerEvent(Pointer, type); + + /// + /// Unregisters an event notification for the primitive component + /// + public void UnregisterEvent(ComponentEventType type) => unregisterEvent(Pointer, type); + } + + /// + /// An abstract component that is represented by a simple geometrical shape + /// + public abstract unsafe partial class ShapeComponent : PrimitiveComponent { + private protected ShapeComponent() { } + + /// + /// Gets or sets whether the shape will be used for navigation as a dynamic modifier instead of using regular collision data + /// + public bool DynamicObstacle { + get => getDynamicObstacle(Pointer); + set => setDynamicObstacle(Pointer, value); + } + + /// + /// Gets or sets the color of the shape + /// + public Color ShapeColor { + get => Color.FromArgb(getShapeColor(Pointer)); + set => setShapeColor(Pointer, value.ToArgb()); + } + } + + /// + /// A box generally used for simple collision + /// + public unsafe partial class BoxComponent : ShapeComponent { + internal override ComponentType Type => ComponentType.Box; + + private protected BoxComponent() { } + + internal BoxComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public BoxComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Retrieves the box extents scaled by the component scale + /// + public void GetScaledBoxExtent(ref Vector3 value) => getScaledBoxExtent(Pointer, ref value); + + /// + /// Returns the box extents scaled by the component scale + /// + public Vector3 GetScaledBoxExtent() { + Vector3 value = default; + + getScaledBoxExtent(Pointer, ref value); + + return value; + } + + /// + /// Retrieves the box extent ignoring the component scale + /// + public void GetUnscaledBoxExtent(ref Vector3 value) => getUnscaledBoxExtent(Pointer, ref value); + + /// + /// Returns the box extent ignoring the component scale + /// + public Vector3 GetUnscaledBoxExtent() { + Vector3 value = default; + + getUnscaledBoxExtent(Pointer, ref value); + + return value; + } + + /// + /// Sets the box extent size, unscaled before the component scale is applied + /// + public void SetBoxExtent(in Vector3 extent, bool updateOverlaps = true) => setBoxExtent(Pointer, extent, updateOverlaps); + + /// + /// Sets the box extent size without triggering a render or physics update + /// + public void InitBoxExtent(in Vector3 extent) => initBoxExtent(Pointer, extent); + } + + /// + /// A sphere generally used for simple collision + /// + public unsafe partial class SphereComponent : ShapeComponent { + internal override ComponentType Type => ComponentType.Sphere; + + private protected SphereComponent() { } + + internal SphereComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public SphereComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Returns the sphere radius scaled by the component scale + /// + public float ScaledSphereRadius => getScaledSphereRadius(Pointer); + + /// + /// Returns the sphere radius ignoring the component scale + /// + public float UnscaledSphereRadius => getUnscaledSphereRadius(Pointer); + + /// + /// Returns the scale of the shape + /// + public float ShapeScale => getShapeScale(Pointer); + + /// + /// Sets the sphere radius, unscaled before the component scale is applied + /// + public void SetSphereRadius(float sphereRadius, bool updateOverlaps = true) => setSphereRadius(Pointer, sphereRadius, updateOverlaps); + + /// + /// Sets the sphere radius without triggering a render or physics update + /// + public void InitSphereRadius(float sphereRadius) => initSphereRadius(Pointer, sphereRadius); + } + + /// + /// A capsule generally used for simple collision + /// + public unsafe partial class CapsuleComponent : ShapeComponent { + internal override ComponentType Type => ComponentType.Capsule; + + private protected CapsuleComponent() { } + + internal CapsuleComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public CapsuleComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Returns the capsule radius scaled by the component scale + /// + public float ScaledCapsuleRadius => getScaledCapsuleRadius(Pointer); + + /// + /// Returns the capsule radius ignoring the component scale + /// + public float UnscaledCapsuleRadius => getUnscaledCapsuleRadius(Pointer); + + /// + /// Returns the scale of the shape + /// + public float ShapeScale => getShapeScale(Pointer); + + /// + /// Retrieves the capsule radius and half-height scaled by the component scale + /// + public void GetScaledCapsuleSize(ref float radius, ref float halfHeight) => getScaledCapsuleSize(Pointer, ref radius, ref halfHeight); + + /// + /// Retrieves the capsule radius and half-height ignoring the component scale + /// + public void GetUnscaledCapsuleSize(ref float radius, ref float halfHeight) => getUnscaledCapsuleSize(Pointer, ref radius, ref halfHeight); + + /// + /// Sets the capsule radius, unscaled before the component scale is applied + /// + public void SetCapsuleRadius(float radius, bool updateOverlaps = true) => setCapsuleRadius(Pointer, radius, updateOverlaps); + + /// + /// Sets the capsule size, unscaled before the component scale is applied + /// + public void SetCapsuleSize(float radius, float halfHeight, bool updateOverlaps = true) => setCapsuleSize(Pointer, radius, halfHeight, updateOverlaps); + + /// + /// Sets the capsule size without triggering a render or physics update + /// + public void InitCapsuleSize(float radius, float halfHeight) => initCapsuleSize(Pointer, radius, halfHeight); + } + + /// + /// An abstract component that is an instance of a renderable collection of triangles + /// + public abstract unsafe partial class MeshComponent : PrimitiveComponent { + private protected MeshComponent() { } + + /// + /// Returns true if the given slot name is valid + /// + public bool IsValidMaterialSlotName(string materialSlotName) { + if (materialSlotName == null) + throw new ArgumentNullException(nameof(materialSlotName)); + + return isValidMaterialSlotName(Pointer, materialSlotName.StringToBytes()); + } + + /// + /// Returns a material index the given a slot name + /// + public int GetMaterialIndex(string materialSlotName) { + if (materialSlotName == null) + throw new ArgumentNullException(nameof(materialSlotName)); + + return getMaterialIndex(Pointer, materialSlotName.StringToBytes()); + } + } + + /// + /// Renders text in the world + /// + public unsafe partial class TextRenderComponent : PrimitiveComponent { + internal override ComponentType Type => ComponentType.TextRender; + + private protected TextRenderComponent() { } + + internal TextRenderComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public TextRenderComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Sets the font of the component + /// + public void SetFont(Font value) { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + setFont(Pointer, value.Pointer); + } + + /// + /// Sets the text of the component + /// + public void SetText(string value) => setText(Pointer, value.StringToBytes()); + + /// + /// Sets the text material of the component + /// + public void SetTextMaterial(MaterialInterface material) { + if (material == null) + throw new ArgumentNullException(nameof(material)); + + setTextMaterial(Pointer, material.Pointer); + } + + /// + /// Sets the text render color of the component + /// + public void SetTextRenderColor(Color value) => setTextRenderColor(Pointer, value.ToArgb()); + + /// + /// Sets the herizontal alignment + /// + public void SetHorizontalAlignment(HorizontalTextAligment value) => setHorizontalAlignment(Pointer, value); + + /// + /// Sets the horizontal spacing adjustment + /// + public void SetHorizontalSpacingAdjustment(float value) => setHorizontalSpacingAdjustment(Pointer, value); + + /// + /// Sets the vertical alignment + /// + public void SetVerticalAlignment(VerticalTextAligment value) => setVerticalAlignment(Pointer, value); + + /// + /// Sets the vertical spacing adjustment + /// + public void SetVerticalSpacingAdjustment(float value) => setVerticalSpacingAdjustment(Pointer, value); + + /// + /// Sets the text scale + /// + public void SetScale(in Vector2 value) => setScale(Pointer, value); + + /// + /// Sets the world size of the text + /// + public void SetWorldSize(float value) => setWorldSize(Pointer, value); + } + + /// + /// The base class of light components + /// + public abstract unsafe partial class LightComponentBase : SceneComponent { + private protected LightComponentBase() { } + + /// + /// Returns the total energy that the light emits + /// + public float Intensity => getIntensity(Pointer); + + /// + /// Gets or sets whether the light should cast shadows + /// + public bool CastShadows { + get => getCastShadows(Pointer); + set => setCastShadows(Pointer, value); + } + } + + /// + /// A component that represents light + /// + public unsafe partial class LightComponent : LightComponentBase { + internal override ComponentType Type => ComponentType.Light; + + private protected LightComponent() { } + + internal LightComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public LightComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Sets the intensity of the light + /// + public void SetIntensity(float value) => setIntensity(Pointer, value); + + /// + /// Sets the light color + /// + public void SetLightColor(in LinearColor value) => setLightColor(Pointer, value); + } + + /// + /// A component that represents directional light + /// + public unsafe partial class DirectionalLightComponent : LightComponent { + internal override ComponentType Type => ComponentType.DirectionalLight; + + private protected DirectionalLightComponent() { } + + internal DirectionalLightComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public DirectionalLightComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + } + + /// + /// A component that represents a physical motion controller in 3D space + /// + public unsafe partial class MotionControllerComponent : PrimitiveComponent { + internal override ComponentType Type => ComponentType.MotionController; + + private protected MotionControllerComponent() { } + + internal MotionControllerComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public MotionControllerComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Returns true if the component has a valid tracked device this frame + /// + public bool IsTracked => isTracked(Pointer); + + /// + /// Gets or sets whether to render a model associated with the set hand + /// + public bool DisplayDeviceModel { + get => getDisplayDeviceModel(Pointer); + set => setDisplayDeviceModel(Pointer, value); + } + + /// + /// Gets or sets whether render transforms within the motion controller hierarchy will be updated a second time immediately before rendering + /// + public bool DisableLowLatencyUpdate { + get => getDisableLowLatencyUpdate(Pointer); + set => setDisableLowLatencyUpdate(Pointer, value); + } + + /// + /// Gets or sets the current tracking source + /// + public ControllerHand TrackingSource { + get => getTrackingSource(Pointer); + set => setTrackingSource(Pointer, value); + } + + /// + /// Sets the tracking motion source + /// + public void SetTrackingMotionSource(string source) => setTrackingMotionSource(Pointer, source.StringToBytes()); + + /// + /// Sets the player index which the motion controller should automatically follow + /// + public void SetAssociatedPlayerIndex(int playerIndex) => setAssociatedPlayerIndex(Pointer, playerIndex); + + /// + /// Sets the custom display mesh that attached to the motion controller + /// + public void SetCustomDisplayMesh(StaticMesh staticMesh) { + if (staticMesh == null) + throw new ArgumentNullException(nameof(staticMesh)); + + setCustomDisplayMesh(Pointer, staticMesh.Pointer); + } + + /// + /// Sets the display model source + /// + public void SetDisplayModelSource(string source) => setDisplayModelSource(Pointer, source.StringToBytes()); + } + + /// + /// A component that is used to create an instance of a static mesh, a piece of geometry that consists of a static set of polygons + /// + public unsafe partial class StaticMeshComponent : MeshComponent { + internal override ComponentType Type => ComponentType.StaticMesh; + + private protected StaticMeshComponent() { } + + internal StaticMeshComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public StaticMeshComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Retrieves local bounds of the mesh + /// + public void GetLocalBounds(ref Vector3 min, ref Vector3 max) => getLocalBounds(Pointer, ref min, ref max); + + /// + /// Returns the static mesh used by this instance or null on failure + /// + public StaticMesh GetStaticMesh() { + IntPtr pointer = getStaticMesh(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Returns true if the mesh was set + /// + public bool SetStaticMesh(StaticMesh staticMesh) { + if (staticMesh == null) + throw new ArgumentNullException(nameof(staticMesh)); + + return setStaticMesh(Pointer, staticMesh.Pointer); + } + } + + /// + /// A component that efficiently renders multiple instances of the same + /// + public unsafe partial class InstancedStaticMeshComponent : StaticMeshComponent { + internal override ComponentType Type => ComponentType.InstancedStaticMesh; + + private protected InstancedStaticMeshComponent() { } + + internal InstancedStaticMeshComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public InstancedStaticMeshComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Returns the number of instances in the component + /// + public int InstanceCount => getInstanceCount(Pointer); + + /// + /// Retrieves the transform of the specified instance + /// + public bool GetInstanceTransform(int instanceIndex, ref Transform value, bool worldSpace = false) => getInstanceTransform(Pointer, instanceIndex, ref value, worldSpace); + + /// + /// Adds an instance to the component using the transform that will be applied at instantiation + /// + public void AddInstance(in Transform instanceTransform) => addInstance(Pointer, instanceTransform); + + /// + /// Adds multiple instances to the component using the transforms that will be applied at instantiation + /// + public void AddInstances(Transform[] instanceTransforms) { + if (instanceTransforms == null) + throw new ArgumentNullException(nameof(instanceTransforms)); + + addInstances(Pointer, instanceTransforms.Length, instanceTransforms); + } + + /// + /// Updates the transform for the specified instance + /// + /// The index of the instance to update + /// The new transform to apply + /// If true, the new transform is interpreted as a world space transform, otherwise it is interpreted as local space + /// If the render state is marked as dirty the change should be visible immediately, consider setting it to true only during the update of the last instance in a batch + /// Whether the instance's physics should be moved normally, or teleported (moved instantly, ignoring velocity) + /// true if successful + public bool UpdateInstanceTransform(int instanceIndex, in Transform instanceTransform, bool worldSpace = false, bool markRenderStateDirty = false, bool teleport = false) => updateInstanceTransform(Pointer, instanceIndex, instanceTransform, worldSpace, markRenderStateDirty, teleport); + + /// + /// Updates the transform for an array of instances + /// + /// The starting index of the instances to update + /// The new transforms to apply + /// If true, the new transforms are interpreted as a world space transforms, otherwise it is interpreted as local space + /// If the render state is marked as dirty the change should be visible immediately + /// Whether the instances physics should be moved normally, or teleported (moved instantly, ignoring velocity) + /// true if successful + public bool BatchUpdateInstanceTransforms(int startInstanceIndex, Transform[] instanceTransforms, bool worldSpace = false, bool markRenderStateDirty = false, bool teleport = false) { + if (instanceTransforms == null) + throw new ArgumentNullException(nameof(instanceTransforms)); + + return batchUpdateInstanceTransforms(Pointer, startInstanceIndex, instanceTransforms.Length, instanceTransforms, worldSpace, markRenderStateDirty, teleport); + } + + /// + /// Removes the specified instance + /// + public bool RemoveInstance(int instanceIndex) => removeInstance(Pointer, instanceIndex); + + /// + /// Clears all instances being rendered by the component + /// + public void ClearInstances() => clearInstances(Pointer); + } + + /// + /// A component that efficiently renders multiple instances of the same with different level of detail + /// + public unsafe partial class HierarchicalInstancedStaticMeshComponent : InstancedStaticMeshComponent { + internal override ComponentType Type => ComponentType.HierarchicalInstancedStaticMesh; + + private protected HierarchicalInstancedStaticMeshComponent() { } + + internal HierarchicalInstancedStaticMeshComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public HierarchicalInstancedStaticMeshComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Gets or sets whether the collision should be disabled + /// + public bool DisableCollision { + get => getDisableCollision(Pointer); + set => setDisableCollision(Pointer, value); + } + } + + /// + /// A component that supports bone skinned mesh rendering + /// + public abstract unsafe partial class SkinnedMeshComponent : MeshComponent { + private protected SkinnedMeshComponent() { } + + /// + /// Returns the number of bones in the skeleton + /// + public int GetBonesNumber() => getBonesNumber(Pointer); + + /// + /// Returns the index of a bone by name + /// + public int GetBoneIndex(string boneName) => getBoneIndex(Pointer, boneName.StringToBytes()); + + /// + /// Returns the name of a bone by index + /// + public string GetBoneName(int boneIndex) { + byte[] stringBuffer = ArrayPool.GetStringBuffer(); + + getBoneName(Pointer, boneIndex, stringBuffer); + + return stringBuffer.BytesToString(); + } + + /// + /// Retrieves the transform of a bone by index + /// + public void GetBoneTransform(int boneIndex, ref Transform value) => getBoneTransform(Pointer, boneIndex, ref value); + + /// + /// Returns the transform of a bone by index + /// + public Transform GetBoneTransform(int boneIndex) { + Transform value = default; + + getBoneTransform(Pointer, boneIndex, ref value); + + return value; + } + + /// + /// Sets the skeletal mesh + /// + public void SetSkeletalMesh(SkeletalMesh skeletalMesh, bool reinitializePose = true) { + if (skeletalMesh == null) + throw new ArgumentNullException(nameof(skeletalMesh)); + + setSkeletalMesh(Pointer, skeletalMesh.Pointer, reinitializePose); + } + } + + /// + /// A component that is used to create an instance of an animated asset + /// + public unsafe partial class SkeletalMeshComponent : SkinnedMeshComponent { + internal override ComponentType Type => ComponentType.SkeletalMesh; + + private protected SkeletalMeshComponent() { } + + internal SkeletalMeshComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public SkeletalMeshComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Returns true if playing animation + /// + public bool IsPlaying => isPlaying(Pointer); + + /// + /// Returns the animation instance that is driving the class or null on failure + /// + public AnimationInstance GetAnimationInstance() { + IntPtr pointer = getAnimationInstance(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Sets the animation to play + /// + public void SetAnimation(AnimationAsset asset) { + if (asset == null) + throw new ArgumentNullException(nameof(asset)); + + setAnimation(Pointer, asset.Pointer); + } + + /// + /// Sets the animation mode + /// + public void SetAnimationMode(AnimationMode mode) => setAnimationMode(Pointer, mode); + + /// + /// Sets the animation blueprint + /// + public void SetAnimationBlueprint(Blueprint blueprint) { + if (blueprint == null) + throw new ArgumentNullException(nameof(blueprint)); + + setAnimationBlueprint(Pointer, blueprint.Pointer); + } + + /// + /// Plays the animation + /// + public void Play(bool loop = false) => play(Pointer, loop); + + /// + /// Plays the animation asset + /// + public void PlayAnimation(AnimationAsset asset, bool loop = false) { + if (asset == null) + throw new ArgumentNullException(nameof(asset)); + + playAnimation(Pointer, asset.Pointer, loop); + } + + /// + /// Stops the animation + /// + public void Stop() => stop(Pointer); + } + + /// + /// Represents a spline shape + /// + public unsafe partial class SplineComponent : PrimitiveComponent { + internal override ComponentType Type => ComponentType.Spline; + + private protected SplineComponent() { } + + internal SplineComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public SplineComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Returns true if the spline is a closed loop + /// + public bool IsClosedLoop => isClosedLoop(Pointer); + + /// + /// Gets or sets the duration of the spline in seconds + /// + public float Duration { + get => getDuration(Pointer); + set => setDuration(Pointer, value); + } + + /// + /// Returns the number of spline points + /// + public int SplinePointsNumber => getSplinePointsNumber(Pointer); + + /// + /// Returns the number of spline segments + /// + public int SplineSegmentsNumber => getSplineSegmentsNumber(Pointer); + + /// + /// Returns the type of a spline point + /// + public SplinePointType GetSplinePointType(int pointIndex) => getSplinePointType(Pointer, pointIndex); + + /// + /// Retrieves the tangent at the given distance along the length of the spline + /// + public void GetTangentAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getTangentAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + /// + /// Returns the tangent at the given distance along the length of the spline + /// + public Vector3 GetTangentAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getTangentAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves the tangent at the spline point + /// + public void GetTangentAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getTangentAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + /// + /// Returns the tangent at the spline point + /// + public Vector3 GetTangentAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getTangentAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves the tangent at the given time from 0.0f to the spline duration + /// + public void GetTangentAtTime(float time, SplineCoordinateSpace coordinateSpace, bool useConstantVelocity, ref Vector3 value) => getTangentAtTime(Pointer, time, coordinateSpace, useConstantVelocity, ref value); + + /// + /// Returns the tangent at the given time from 0.0f to the spline duration + /// + public Vector3 GetTangentAtTime(float time, SplineCoordinateSpace coordinateSpace, bool useConstantVelocity = false) { + Vector3 value = default; + + getTangentAtTime(Pointer, time, coordinateSpace, useConstantVelocity, ref value); + + return value; + } + + /// + /// Retrieves the transform at the given distance along the length of the spline + /// + public void GetTransformAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace, ref Transform value) => getTransformAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + /// + /// Returns the transform at the given distance along the length of the spline + /// + public Transform GetTransformAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace) { + Transform value = default; + + getTransformAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves the transform at the spline point + /// + public void GetTransformAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace, bool useScale, ref Transform value) => getTransformAtSplinePoint(Pointer, pointIndex, coordinateSpace, useScale, ref value); + + /// + /// Returns the transform at the spline point + /// + public Transform GetTransformAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace, bool useScale = false) { + Transform value = default; + + getTransformAtSplinePoint(Pointer, pointIndex, coordinateSpace, useScale, ref value); + + return value; + } + + /// + /// Retrieves the arrive tangent at the spline point + /// + public void GetArriveTangentAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getArriveTangentAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + /// + /// Returns the arrive tangent at the spline point + /// + public Vector3 GetArriveTangentAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getArriveTangentAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves the default up vector of the spline + /// + public void GetDefaultUpVector(SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getDefaultUpVector(Pointer, coordinateSpace, ref value); + + /// + /// Returns the default up vector of the spline + /// + public Vector3 GetDefaultUpVector(SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getDefaultUpVector(Pointer, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves a unit direction vector of the spline tangent at the given distance along the length of the spline + /// + public void GetDirectionAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getDirectionAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + /// + /// Returns a unit direction vector of the spline tangent at the given distance along the length of the spline + /// + public Vector3 GetDirectionAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getDirectionAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves a unit direction vector at the spline point + /// + public void GetDirectionAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getDirectionAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + /// + /// Returns a unit direction vector at the spline point + /// + public Vector3 GetDirectionAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getDirectionAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves a unit direction vector of the spline tangent at the given time from 0.0f to the spline duration + /// + public void GetDirectionAtTime(float time, SplineCoordinateSpace coordinateSpace, bool useConstantVelocity, ref Vector3 value) => getDirectionAtTime(Pointer, time, coordinateSpace, useConstantVelocity, ref value); + + /// + /// Returns a unit direction vector of the spline tangent at the given time from 0.0f to the spline duration + /// + public Vector3 GetDirectionAtTime(float time, SplineCoordinateSpace coordinateSpace, bool useConstantVelocity = false) { + Vector3 value = default; + + getDirectionAtTime(Pointer, time, coordinateSpace, useConstantVelocity, ref value); + + return value; + } + + /// + /// Returns the distance along the spline at the spline point + /// + public float GetDistanceAlongSplineAtSplinePoint(int pointIndex) => getDistanceAlongSplineAtSplinePoint(Pointer, pointIndex); + + /// + /// Retrieves the leave tangent at the spline point + /// + public void GetLeaveTangentAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getLeaveTangentAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + /// + /// Returns the leave tangent at the spline point + /// + public Vector3 GetLeaveTangentAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getLeaveTangentAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves the location and tangent at the spline point + /// + public void GetLocationAndTangentAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace, ref Vector3 location, ref Vector3 tangent) => getLocationAndTangentAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref location, ref tangent); + + /// + /// Retrieves the location at the given distance along the length of the spline + /// + public void GetLocationAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getLocationAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + /// + /// Returns the location at the given distance along the length of the spline + /// + public Vector3 GetLocationAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getLocationAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves the location at the spline point + /// + public void GetLocationAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getLocationAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + /// + /// Returns the location at the spline point + /// + public Vector3 GetLocationAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getLocationAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves the location at the given time from 0.0f to the spline duration + /// + public void GetLocationAtTime(float time, SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getLocationAtTime(Pointer, time, coordinateSpace, ref value); + + /// + /// Returns the location at the given time from 0.0f to the spline duration + /// + public Vector3 GetLocationAtTime(float time, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getLocationAtTime(Pointer, time, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves a unit direction vector corresponding to the spline's right vector at the given distance along the length of the spline + /// + public void GetRightVectorAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getRightVectorAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + /// + /// Retrieves a unit direction vector corresponding to the spline's right vector at the given distance along the length of the spline + /// + public Vector3 GetRightVectorAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getRightVectorAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves the spline's right vector at the spline point + /// + public void GetRightVectorAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getRightVectorAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + /// + /// Returns the spline's right vector at the spline point + /// + public Vector3 GetRightVectorAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getRightVectorAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves the spline's right vector at the given time from 0.0f to the spline duration + /// + public void GetRightVectorAtTime(float time, SplineCoordinateSpace coordinateSpace, bool useConstantVelocity, ref Vector3 value) => getRightVectorAtTime(Pointer, time, coordinateSpace, useConstantVelocity, ref value); + + /// + /// Returns the spline's right vector at the given time from 0.0f to the spline duration + /// + public Vector3 GetRightVectorAtTime(float time, SplineCoordinateSpace coordinateSpace, bool useConstantVelocity = false) { + Vector3 value = default; + + getRightVectorAtTime(Pointer, time, coordinateSpace, useConstantVelocity, ref value); + + return value; + } + + /// + /// Returns the spline's roll in degrees at the given distance along the length of the spline + /// + public float GetRollAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace) => getRollAtDistanceAlongSpline(Pointer, distance, coordinateSpace); + + /// + /// Returns the spline's roll in degrees at the spline point + /// + public float GetRollAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace) => getRollAtSplinePoint(Pointer, pointIndex, coordinateSpace); + + /// + /// Returns the spline's roll in degrees at the given time from 0.0f to the spline duration + /// + public float GetRollAtTime(float time, SplineCoordinateSpace coordinateSpace, bool useConstantVelocity = false) => getRollAtTime(Pointer, time, coordinateSpace, useConstantVelocity); + + /// + /// Retrieves a rotation corresponding to the spline's rotation at the given distance along the length of the spline + /// + public void GetRotationAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace, ref Quaternion value) => getRotationAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + /// + /// Returns a rotation corresponding to the spline's rotation at the given distance along the length of the spline + /// + public Quaternion GetRotationAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace) { + Quaternion value = default; + + getRotationAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves a spline's rotation at the spline point + /// + public void GetRotationAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace, ref Quaternion value) => getRotationAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + /// + /// Returns a spline's rotation at the spline point + /// + public Quaternion GetRotationAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace) { + Quaternion value = default; + + getRotationAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves a rotation corresponding to the spline's position and direction at the given time from 0.0f to the spline duration + /// + public void GetRotationAtTime(float time, SplineCoordinateSpace coordinateSpace, bool useConstantVelocity, ref Quaternion value) => getRotationAtTime(Pointer, time, coordinateSpace, useConstantVelocity, ref value); + + /// + /// Returns a rotation corresponding to the spline's position and direction at the given time from 0.0f to the spline duration + /// + public Quaternion GetRotationAtTime(float time, SplineCoordinateSpace coordinateSpace, bool useConstantVelocity = false) { + Quaternion value = default; + + getRotationAtTime(Pointer, time, coordinateSpace, useConstantVelocity, ref value); + + return value; + } + + /// + /// Retrieves the spline's scale at the given distance along the length of the spline + /// + public void GetScaleAtDistanceAlongSpline(float distance, ref Vector3 value) => getScaleAtDistanceAlongSpline(Pointer, distance, ref value); + + /// + /// Returns the spline's scale at the given distance along the length of the spline + /// + public Vector3 GetScaleAtDistanceAlongSpline(float distance) { + Vector3 value = default; + + getScaleAtDistanceAlongSpline(Pointer, distance, ref value); + + return value; + } + + /// + /// Retrieves the spline's scale at the spline point + /// + public void GetScaleAtSplinePoint(int pointIndex, ref Vector3 value) => getScaleAtSplinePoint(Pointer, pointIndex, ref value); + + /// + /// Returns the spline's scale at the spline point + /// + public Vector3 GetScaleAtSplinePoint(int pointIndex) { + Vector3 value = default; + + getScaleAtSplinePoint(Pointer, pointIndex, ref value); + + return value; + } + + /// + /// Retrieves the spline's scale at the given time from 0.0f to the spline duration + /// + public void GetScaleAtTime(float time, bool useConstantVelocity, ref Vector3 value) => getScaleAtTime(Pointer, time, useConstantVelocity, ref value); + + /// + /// Returns the spline's scale at the given time from 0.0f to the spline duration + /// + public Vector3 GetScaleAtTime(float time, bool useConstantVelocity = false) { + Vector3 value = default; + + getScaleAtTime(Pointer, time, useConstantVelocity, ref value); + + return value; + } + + /// + /// Returns the total length along the spline + /// + public float GetSplineLength() => getSplineLength(Pointer); + + /// + /// Retrieves the spline's transform at the given time from 0.0f to the spline duration + /// + public void GetTransformAtTime(float time, SplineCoordinateSpace coordinateSpace, bool useConstantVelocity, bool useScale, ref Transform value) => getTransformAtTime(Pointer, time, coordinateSpace, useConstantVelocity, useScale, ref value); + + /// + /// Returns the spline's transform at the given time from 0.0f to the spline duration + /// + public Transform GetTransformAtTime(float time, SplineCoordinateSpace coordinateSpace, bool useConstantVelocity = false, bool useScale = false) { + Transform value = default; + + getTransformAtTime(Pointer, time, coordinateSpace, useConstantVelocity, useScale, ref value); + + return value; + } + + /// + /// Retrieves a unit direction vector corresponding to the spline's up vector at the given distance along the length of the spline + /// + public void GetUpVectorAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getUpVectorAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + /// + /// Returns a unit direction vector corresponding to the spline's up vector at the given distance along the length of the spline + /// + public Vector3 GetUpVectorAtDistanceAlongSpline(float distance, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getUpVectorAtDistanceAlongSpline(Pointer, distance, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves the spline's up vector at the spline point + /// + public void GetUpVectorAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace, ref Vector3 value) => getUpVectorAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + /// + /// Returns the spline's up vector at the spline point + /// + public Vector3 GetUpVectorAtSplinePoint(int pointIndex, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + getUpVectorAtSplinePoint(Pointer, pointIndex, coordinateSpace, ref value); + + return value; + } + + /// + /// Retrieves the spline's up vector at the given time from 0.0f to the spline duration + /// + public void GetUpVectorAtTime(float time, SplineCoordinateSpace coordinateSpace, bool useConstantVelocity, ref Vector3 value) => getUpVectorAtTime(Pointer, time, coordinateSpace, useConstantVelocity, ref value); + + /// + /// Returns the spline's up vector at the given time from 0.0f to the spline duration + /// + public Vector3 GetUpVectorAtTime(float time, SplineCoordinateSpace coordinateSpace, bool useConstantVelocity = false) { + Vector3 value = default; + + getUpVectorAtTime(Pointer, time, coordinateSpace, useConstantVelocity, ref value); + + return value; + } + + /// + /// Sets the type of a spline point + /// + public void SetSplinePointType(int pointIndex, SplinePointType type, bool updateSpline = true) => setSplinePointType(Pointer, pointIndex, type, updateSpline); + + /// + /// Sets whether the spline is a closed loop + /// + public void SetClosedLoop(bool value, bool updateSpline = true) => setClosedLoop(Pointer, value, updateSpline); + + /// + /// Sets the default up vector of the spline + /// + public void SetDefaultUpVector(in Vector3 value, SplineCoordinateSpace coordinateSpace) => setDefaultUpVector(Pointer, value, coordinateSpace); + + /// + /// Sets an existing point to a new location + /// + public void SetLocationAtSplinePoint(int pointIndex, in Vector3 value, SplineCoordinateSpace coordinateSpace, bool updateSpline = true) => setLocationAtSplinePoint(Pointer, pointIndex, value, coordinateSpace, updateSpline); + + /// + /// Sets the tangent at a given spline point + /// + public void SetTangentAtSplinePoint(int pointIndex, in Vector3 tangent, SplineCoordinateSpace coordinateSpace, bool updateSpline = true) => setTangentAtSplinePoint(Pointer, pointIndex, tangent, coordinateSpace, updateSpline); + + /// + /// Sets the tangents at a given spline point + /// + public void SetTangentsAtSplinePoint(int pointIndex, in Vector3 arriveTangent, in Vector3 leaveTangent, SplineCoordinateSpace coordinateSpace, bool updateSpline = true) => setTangentsAtSplinePoint(Pointer, pointIndex, arriveTangent, leaveTangent, coordinateSpace, updateSpline); + + /// + /// Sets the up vector at a given spline point + /// + public void SetUpVectorAtSplinePoint(int pointIndex, in Vector3 upVector, SplineCoordinateSpace coordinateSpace, bool updateSpline = true) => setUpVectorAtSplinePoint(Pointer, pointIndex, upVector, coordinateSpace, updateSpline); + + /// + /// Adds a point to the spline + /// + public void AddSplinePoint(in Vector3 location, SplineCoordinateSpace coordinateSpace, bool updateSpline = true) => addSplinePoint(Pointer, location, coordinateSpace, updateSpline); + + /// + /// Adds a point to the spline at the specified index + /// + public void AddSplinePointAtIndex(in Vector3 location, int pointIndex, SplineCoordinateSpace coordinateSpace, bool updateSpline = true) => addSplinePointAtIndex(Pointer, location, pointIndex, coordinateSpace, updateSpline); + + /// + /// Clears all the points in the spline + /// + public void ClearSplinePoints(bool updateSpline = true) => clearSplinePoints(Pointer, updateSpline); + + /// + /// Returns a unit direction vector of the spline tangent closest to the location in world space + /// + public Vector3 FindDirectionClosestToWorldLocation(in Vector3 location, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + findDirectionClosestToWorldLocation(Pointer, location, coordinateSpace, ref value); + + return value; + } + + /// + /// Returns a point of the spline closest to the location in world space + /// + public Vector3 FindLocationClosestToWorldLocation(in Vector3 location, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + findLocationClosestToWorldLocation(Pointer, location, coordinateSpace, ref value); + + return value; + } + + /// + /// Returns a unit direction vector corresponding to the spline's up vector closest to the location in world space + /// + public Vector3 FindUpVectorClosestToWorldLocation(in Vector3 location, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + findUpVectorClosestToWorldLocation(Pointer, location, coordinateSpace, ref value); + + return value; + } + + /// + /// Returns a unit direction vector corresponding to the spline's right vector closest to the location in world space + /// + public Vector3 FindRightVectorClosestToWorldLocation(in Vector3 location, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + findRightVectorClosestToWorldLocation(Pointer, location, coordinateSpace, ref value); + + return value; + } + + /// + /// Returns the spline's roll in degrees closest to the location in world space + /// + public float FindRollClosestToWorldLocation(in Vector3 location, SplineCoordinateSpace coordinateSpace) => findRollClosestToWorldLocation(Pointer, location, coordinateSpace); + + /// + /// Returns the spline's scale closest to the location in world space + /// + public Vector3 FindScaleClosestToWorldLocation(in Vector3 location) { + Vector3 value = default; + + findScaleClosestToWorldLocation(Pointer, location, ref value); + + return value; + } + + /// + /// Returns a tangent of the spline closest to the location in world space + /// + public Vector3 FindTangentClosestToWorldLocation(in Vector3 location, SplineCoordinateSpace coordinateSpace) { + Vector3 value = default; + + findTangentClosestToWorldLocation(Pointer, location, coordinateSpace, ref value); + + return value; + } + + /// + /// Returns a transform closest to the location in world space + /// + public Transform FindTransformClosestToWorldLocation(in Vector3 location, SplineCoordinateSpace coordinateSpace, bool useScale = false) { + Transform value = default; + + findTransformClosestToWorldLocation(Pointer, location, coordinateSpace, useScale, ref value); + + return value; + } + + /// + /// Removes a point at the specified index from the spline + /// + public void RemoveSplinePoint(int pointIndex, bool updateSpline = true) => removeSplinePoint(Pointer, pointIndex, updateSpline); + + /// + /// Updates the spline + /// + public void UpdateSpline() => updateSpline(Pointer); + } + + /// + /// A component that emits a radial force or impulse that can affect physics objects and destructible objects + /// + public unsafe partial class RadialForceComponent : SceneComponent { + internal override ComponentType Type => ComponentType.RadialForce; + + private protected RadialForceComponent() { } + + internal RadialForceComponent(IntPtr pointer) => Pointer = pointer; + + /// + /// Creates the component attached to an actor and optionally sets it as the root, first component will be set as the root automatically + /// + /// The actor to attach the component to + /// The name of the component + /// If true, sets the component as the root + /// The blueprint class to use as a base class, should be equal to the exact type of the component + public RadialForceComponent(Actor actor, string name = null, bool setAsRoot = false, Blueprint blueprint = null) { + if (name?.Length == 0) + name = null; + + if (actor == null) + throw new ArgumentNullException(nameof(actor)); + + if (blueprint != null && !blueprint.IsValidClass(Type)) + throw new InvalidOperationException(); + + Pointer = create(actor.Pointer, Type, name.StringToBytes(), setAsRoot, blueprint != null ? blueprint.Pointer : IntPtr.Zero); + } + + /// + /// Gets or sets whether to apply the force or impulse to any physics objects that are part of the actor which owns the component + /// + public bool IgnoreOwningActor { + get => getIgnoreOwningActor(Pointer); + set => setIgnoreOwningActor(Pointer, value); + } + + /// + /// Gets or sets whether the impulse will ignore the mass of objects and will always result in a fixed velocity change + /// + public bool ImpulseVelocityChange { + get => getImpulseVelocityChange(Pointer); + set => setImpulseVelocityChange(Pointer, value); + } + + /// + /// Gets or sets whether the force or impulse should lose its strength linearly + /// + public bool LinearFalloff { + get => getLinearFalloff(Pointer); + set => setLinearFalloff(Pointer, value); + } + + /// + /// Gets or sets the force strength + /// + public float ForceStrength { + get => getForceStrength(Pointer); + set => setForceStrength(Pointer, value); + } + + /// + /// Gets or sets the impulse strength + /// + public float ImpulseStrength { + get => getImpulseStrength(Pointer); + set => setImpulseStrength(Pointer, value); + } + + /// + /// Gets or sets the radius within the force or impulse will be applied + /// + public float Radius { + get => getRadius(Pointer); + set => setRadius(Pointer, value); + } + + /// + /// Adds a collision channel that will be affected by the radial force + /// + public void AddCollisionChannelToAffect(CollisionChannel channel) => addCollisionChannelToAffect(Pointer, channel); + + /// + /// Fires a single impulse + /// + public void FireImpulse() => fireImpulse(Pointer); + } + + /// + /// The base class of materials that can be applied to meshes + /// + public abstract unsafe partial class MaterialInterface : IEquatable { + private IntPtr pointer; + + internal IntPtr Pointer { + get { + if (!IsCreated) + throw new InvalidOperationException(); + + return pointer; + } + + set { + if (value == IntPtr.Zero) + throw new InvalidOperationException(); + + pointer = value; + } + } + + private protected MaterialInterface() { } + + /// + /// Returns true if the object is created + /// + public bool IsCreated => pointer != IntPtr.Zero && Object.isValid(pointer); + + /// + /// Indicates equality of objects + /// + public bool Equals(MaterialInterface other) => IsCreated && pointer == other?.pointer; + + /// + /// Returns a hash code for the object + /// + public override int GetHashCode() => pointer.GetHashCode(); + + /// + /// Returns true if the material is two sided + /// + public bool IsTwoSided => isTwoSided(Pointer); + } + + /// + /// An asset which can be applied to a mesh to control the visual look + /// + public unsafe partial class Material : MaterialInterface { + private protected Material() { } + + internal Material(IntPtr pointer) => Pointer = pointer; + + /// + /// Finds and loads a material by name + /// + /// A material or null on failure + public static Material Load(string name) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + IntPtr pointer = Object.load(ObjectType.Material, name.StringToBytes()); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + + /// + /// Returns true if the material is one of the default materials + /// + public bool IsDefaultMaterial => isDefaultMaterial(Pointer); + } + + /// + /// An abstract instance of the material + /// + public abstract unsafe partial class MaterialInstance : MaterialInterface { + private protected MaterialInstance() { } + + /// + /// Returns true if the material instance is a child of another Material + /// + public bool IsChildOf(MaterialInterface material) { + if (material == null) + throw new ArgumentNullException(nameof(material)); + + return isChildOf(Pointer, material.Pointer); + } + + /// + /// Returns the parent material or null on failure + /// + public MaterialInstanceDynamic GetParent() { + IntPtr pointer = getParent(Pointer); + + if (pointer != IntPtr.Zero) + return new(pointer); + + return null; + } + } + + /// + /// A dynamic instance of the material + /// + public unsafe partial class MaterialInstanceDynamic : MaterialInstance { + private protected MaterialInstanceDynamic() { } + + internal MaterialInstanceDynamic(IntPtr pointer) => Pointer = pointer; + + /// + /// Removes all parameter values + /// + public void ClearParameterValues() => clearParameterValues(Pointer); + + /// + /// Sets the texture parameter value + /// + public void SetTextureParameterValue(string parameterName, Texture value) { + if (parameterName == null) + throw new ArgumentNullException(nameof(parameterName)); + + if (value == null) + throw new ArgumentNullException(nameof(value)); + + setTextureParameterValue(Pointer, parameterName.StringToBytes(), value.Pointer); + } + + /// + /// Sets the vector parameter value + /// + public void SetVectorParameterValue(string parameterName, in LinearColor value) { + if (parameterName == null) + throw new ArgumentNullException(nameof(parameterName)); + + setVectorParameterValue(Pointer, parameterName.StringToBytes(), value); + } + + /// + /// Sets the scalar parameter value + /// + public void SetScalarParameterValue(string parameterName, float value) { + if (parameterName == null) + throw new ArgumentNullException(nameof(parameterName)); + + setScalarParameterValue(Pointer, parameterName.StringToBytes(), value); + } + } +} + +``` + +`Source/Managed/Framework/UnrealEngine.Framework.csproj`: + +```csproj + + + + Library + net6.0 + x64 + True + True + False + False + AllEnabledByDefault + CA1008, CA1014, CA1024, CA1028, CA1030, CA1031, CA1034, CA1067, CA1303, CA1305, CA1508, CA1708, CA1711, CA1720, CA1725, CA2201 + + + + False + True + + + + True + False + + + + ../../../API + DeclaringType + Name + + + + Copyright (c) 2021 Stanislav Denisov (nxrighthere@gmail.com) + MIT + + + + + + + + + + + + +``` + +`Source/Managed/Runtime/Runtime.cs`: + +```cs +/* + * Unreal Engine .NET 6 integration + * Copyright (c) 2021 Stanislav Denisov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using UnrealEngine.Plugins; + +namespace UnrealEngine.Runtime { + internal enum LogLevel : int { + Display, + Warning, + Error, + Fatal + } + + internal enum CallbackType : int { + ActorOverlapDelegate, + ActorHitDelegate, + ActorCursorDelegate, + ActorKeyDelegate, + ComponentOverlapDelegate, + ComponentHitDelegate, + ComponentCursorDelegate, + ComponentKeyDelegate, + CharacterLandedDelegate + } + + internal enum ArgumentType : int { + None, + Single, + Integer, + Pointer, + Callback + } + + internal enum CommandType : int { + Initialize = 1, + LoadAssemblies = 2, + UnloadAssemblies = 3, + Find = 4, + Execute = 5 + } + + [StructLayout(LayoutKind.Explicit, Size = 16)] + internal unsafe struct Callback { + [FieldOffset(0)] + internal IntPtr* parameters; + [FieldOffset(8)] + internal CallbackType type; + } + + [StructLayout(LayoutKind.Explicit, Size = 24)] + internal unsafe struct Argument { + [FieldOffset(0)] + internal float single; + [FieldOffset(0)] + internal uint integer; + [FieldOffset(0)] + internal IntPtr pointer; + [FieldOffset(0)] + internal Callback callback; + [FieldOffset(16)] + internal ArgumentType type; + } + + [StructLayout(LayoutKind.Explicit, Size = 40)] + internal unsafe struct Command { + // Initialize + [FieldOffset(0)] + internal IntPtr* buffer; + [FieldOffset(8)] + internal int checksum; + // Find + [FieldOffset(0)] + internal IntPtr method; + [FieldOffset(8)] + internal int optional; + // Execute + [FieldOffset(0)] + internal IntPtr function; + [FieldOffset(8)] + internal Argument value; + [FieldOffset(32)] + internal CommandType type; + } + + internal sealed class Plugin { + internal PluginLoader loader; + internal Assembly assembly; + internal Dictionary userFunctions; + } + + internal sealed class AssembliesContextManager { + internal AssemblyLoadContext assembliesContext; + + [MethodImpl(MethodImplOptions.NoInlining)] + internal WeakReference CreateAssembliesContext() { + assembliesContext = new("UnrealEngine", true); + + return new(assembliesContext, trackResurrection: true); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal void UnloadAssembliesContext() => assembliesContext?.Unload(); + } + + internal static unsafe class Core { + private static AssembliesContextManager assembliesContextManager; + private static WeakReference assembliesContextWeakReference; + private static Plugin plugin; + private static IntPtr sharedEvents; + private static IntPtr sharedFunctions; + private static int sharedChecksum; + + private static delegate* unmanaged[Cdecl] Exception; + private static delegate* unmanaged[Cdecl] Log; + + [UnmanagedCallersOnly] + internal static unsafe IntPtr ManagedCommand(Command command) { + if (command.type == CommandType.Execute) { + try { + switch (command.value.type) { + case ArgumentType.None: { + ((delegate* unmanaged[Cdecl])command.function)(); + break; + } + + case ArgumentType.Single: { + ((delegate* unmanaged[Cdecl])command.function)(command.value.single); + break; + } + + case ArgumentType.Integer: { + ((delegate* unmanaged[Cdecl])command.function)(command.value.integer); + break; + } + + case ArgumentType.Pointer: { + ((delegate* unmanaged[Cdecl])command.function)(command.value.pointer); + break; + } + + case ArgumentType.Callback: { + if (command.value.callback.type == CallbackType.ActorOverlapDelegate || command.value.callback.type == CallbackType.ComponentOverlapDelegate || command.value.callback.type == CallbackType.ActorKeyDelegate || command.value.callback.type == CallbackType.ComponentKeyDelegate) + ((delegate* unmanaged[Cdecl])command.function)(command.value.callback.parameters[0], command.value.callback.parameters[1]); + else if (command.value.callback.type == CallbackType.ActorHitDelegate || command.value.callback.type == CallbackType.ComponentHitDelegate) + ((delegate* unmanaged[Cdecl])command.function)(command.value.callback.parameters[0], command.value.callback.parameters[1], command.value.callback.parameters[2], command.value.callback.parameters[3]); + else if (command.value.callback.type == CallbackType.ActorCursorDelegate || command.value.callback.type == CallbackType.ComponentCursorDelegate || command.value.callback.type == CallbackType.CharacterLandedDelegate) + ((delegate* unmanaged[Cdecl])command.function)(command.value.callback.parameters[0]); + else + throw new Exception("Unknown callback type"); + break; + } + + default: + throw new Exception("Unknown function type"); + } + } + + catch (Exception exception) { + try { + Exception(exception.ToString()); + } + + catch (FileNotFoundException fileNotFoundException) { + Exception("One of the project dependencies is missed! Please, publish the project instead of building it\r\n" + fileNotFoundException.ToString()); + } + } + + return default; + } + + if (command.type == CommandType.Find) { + IntPtr function = IntPtr.Zero; + + try { + string method = Marshal.PtrToStringAnsi(command.method); + + if (!plugin.userFunctions.TryGetValue(method.GetHashCode(StringComparison.Ordinal), out function) && command.optional != 1) + Log(LogLevel.Error, "Managed function was not found \"" + method + "\""); + } + + catch (Exception exception) { + Exception(exception.ToString()); + } + + return function; + } + + if (command.type == CommandType.Initialize) { + try { + assembliesContextManager = new(); + assembliesContextWeakReference = assembliesContextManager.CreateAssembliesContext(); + + int position = 0; + IntPtr* buffer = command.buffer; + + unchecked { + int head = 0; + IntPtr* runtimeFunctions = (IntPtr*)buffer[position++]; + + Exception = (delegate* unmanaged[Cdecl])runtimeFunctions[head++]; + Log = (delegate* unmanaged[Cdecl])runtimeFunctions[head++]; + } + + sharedEvents = buffer[position++]; + sharedFunctions = buffer[position++]; + sharedChecksum = command.checksum; + } + + catch (Exception exception) { + Exception("Runtime initialization failed\r\n" + exception.ToString()); + } + + return new(0xF); + } + + if (command.type == CommandType.LoadAssemblies) { + try { + const string frameworkAssemblyName = "UnrealEngine.Framework"; + string assemblyPath = Assembly.GetExecutingAssembly().Location; + string managedFolder = assemblyPath.Substring(0, assemblyPath.IndexOf("Plugins", StringComparison.Ordinal)) + "Managed"; + string[] folders = Directory.GetDirectories(managedFolder); + + Array.Resize(ref folders, folders.Length + 1); + + folders[folders.Length - 1] = managedFolder; + + foreach (string folder in folders) { + IEnumerable assemblies = Directory.EnumerateFiles(folder, "*.dll", SearchOption.AllDirectories); + + foreach (string assembly in assemblies) { + AssemblyName name = null; + bool loadingFailed = false; + + try { + name = AssemblyName.GetAssemblyName(assembly); + } + + catch (BadImageFormatException) { + continue; + } + + if (name?.Name != frameworkAssemblyName) { + plugin = new(); + plugin.loader = PluginLoader.CreateFromAssemblyFile(assembly, config => { config.DefaultContext = assembliesContextManager.assembliesContext; config.IsUnloadable = true; config.LoadInMemory = true; }); + plugin.assembly = plugin.loader.LoadAssemblyFromPath(assembly); + + AssemblyName[] referencedAssemblies = plugin.assembly.GetReferencedAssemblies(); + + foreach (AssemblyName referencedAssembly in referencedAssemblies) { + if (referencedAssembly.Name == frameworkAssemblyName) { + Assembly framework = plugin.loader.LoadAssembly(referencedAssembly); + + using (assembliesContextManager.assembliesContext.EnterContextualReflection()) { + Type sharedClass = framework.GetType(frameworkAssemblyName + ".Shared"); + + if ((int)sharedClass.GetField("checksum", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null) == sharedChecksum) { + plugin.userFunctions = (Dictionary)sharedClass.GetMethod("Load", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, new object[] { sharedEvents, sharedFunctions, plugin.assembly }); + + Log(LogLevel.Display, "Framework loaded succesfuly for " + assembly); + + return default; + } else { + Log(LogLevel.Fatal, "Framework loading failed, version is incompatible with the runtime, please, recompile the project with an updated version referenced in " + assembly); + + loadingFailed = true; + } + } + } + } + + UnloadAssemblies(); + + if (loadingFailed) + return default; + } + } + } + } + + catch (Exception exception) { + Exception("Loading of assemblies failed\r\n" + exception.ToString()); + UnloadAssemblies(); + } + + return default; + } + + if (command.type == CommandType.UnloadAssemblies) + UnloadAssemblies(); + + return default; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void UnloadAssemblies() { + try { + plugin?.loader.Dispose(); + plugin = null; + + assembliesContextManager.UnloadAssembliesContext(); + assembliesContextManager = null; + + if (assembliesContextWeakReference.IsAlive) { + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); + GC.WaitForPendingFinalizers(); + } + + assembliesContextManager = new(); + assembliesContextWeakReference = assembliesContextManager.CreateAssembliesContext(); + } + + catch (Exception exception) { + Exception("Unloading of assemblies failed\r\n" + exception.ToString()); + } + } + } +} + +``` + +`Source/Managed/Runtime/UnrealEngine.Runtime.csproj`: + +```csproj + + + + net6.0 + x64 + True + True + False + False + False + False + AllEnabledByDefault + CA1008, CA1014, CA1024, CA1028, CA1030, CA1031, CA1034, CA1067, CA1303, CA1305, CA1508, CA1708, CA1711, CA1720, CA1725, CA1845, CA2201 + + + + False + True + + + + True + False + + + + Copyright (c) 2021 Stanislav Denisov (nxrighthere@gmail.com) + MIT + + + + + libs/plugins/UnrealEngine.Plugins.dll + + + + + + + + + +``` + +`Source/Managed/Runtime/libs/plugins/LICENSE.txt`: + +```txt +License notice for UnrealEngine.Plugins.dll +-------------------------------------- + +Copyright (c) 2017 Nate McMaster + +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +``` + +`Source/Managed/Runtime/libs/plugins/UnrealEngine.Plugins.deps.json`: + +```json +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": { + "UnrealEngine.Plugins/1.0.0": { + "dependencies": { + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Microsoft.Extensions.DependencyModel": "5.0.0" + }, + "runtime": { + "UnrealEngine.Plugins.dll": {} + } + }, + "Microsoft.DotNet.PlatformAbstractions/3.1.6": { + "runtime": { + "lib/netstandard2.0/Microsoft.DotNet.PlatformAbstractions.dll": { + "assemblyVersion": "3.1.6.0", + "fileVersion": "3.100.620.31604" + } + } + }, + "Microsoft.Extensions.DependencyModel/5.0.0": { + "runtime": { + "lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll": { + "assemblyVersion": "5.0.0.0", + "fileVersion": "5.0.20.51904" + } + } + } + } + }, + "libraries": { + "UnrealEngine.Plugins/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Microsoft.DotNet.PlatformAbstractions/3.1.6": { + "type": "package", + "serviceable": true, + "sha512": "sha512-jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==", + "path": "microsoft.dotnet.platformabstractions/3.1.6", + "hashPath": "microsoft.dotnet.platformabstractions.3.1.6.nupkg.sha512" + }, + "Microsoft.Extensions.DependencyModel/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-umBECCoMC+sOUgm083yFr8SxTobUOcPFH4AXigdO2xJiszCHAnmeDl4qPphJt+oaJ/XIfV1wOjIts2nRnki61Q==", + "path": "microsoft.extensions.dependencymodel/5.0.0", + "hashPath": "microsoft.extensions.dependencymodel.5.0.0.nupkg.sha512" + } + } +} +``` + +`Source/Managed/Tests/AssertionConsistency.cs`: + +```cs +namespace UnrealEngine.Tests { + public class AssertionConsistency : ISystem { + private Actor actor; + + public void OnBeginPlay() { + Assert.IsFalse(true, "Is true"); + Assert.IsTrue(false, "Is false"); + Assert.IsNotNull(actor, "Is null"); + + actor = new(); + + Assert.IsNull(actor, "Is not null"); + } + } +} +``` + +`Source/Managed/Tests/AudioPlayback.cs`: + +```cs +namespace UnrealEngine.Tests { + public class AudioPlayback : ISystem { + public void OnBeginPlay() { + Actor alarmSound = new("AlarmSound"); + AudioComponent alarmAudioComponent = new(alarmSound); + SoundWave alarmSoundWave = SoundWave.Load("/Game/Tests/AlarmSound"); + + Debug.AddOnScreenMessage(-1, 5.0f, Color.PowderBlue, "Sound wave duration: " + alarmSoundWave.Duration + " seconds"); + + alarmSoundWave.Loop = true; + alarmAudioComponent.SetSound(alarmSoundWave); + alarmAudioComponent.Play(); + + Assert.IsTrue(alarmAudioComponent.IsPlaying); + } + + public void OnEndPlay() => Debug.ClearOnScreenMessages(); + } +} + +``` + +`Source/Managed/Tests/BlueprintsExtensibility.cs`: + +```cs +namespace UnrealEngine.Tests { + public class BlueprintsExtensibility : ISystem { + private Blueprint blueprintActor; + private Blueprint blueprintSceneComponent; + private Actor actor; + private SceneComponent sceneComponent; + private LevelScript levelScript; + private SkeletalMeshComponent skeletalMeshComponent; + private AnimationInstance animationInstance; + private const string boolProperty = "Test Bool"; + private const string byteProperty = "Test Byte"; + private const string intProperty = "Test Int"; + private const string floatProperty = "Test Float"; + private const string stringProperty = "Test String"; + private const string textProperty = "Test Text"; + + public BlueprintsExtensibility() { + blueprintActor = Blueprint.Load("/Game/Tests/BlueprintActor"); + blueprintSceneComponent = Blueprint.Load("/Game/Tests/BlueprintSceneComponent"); + actor = new(blueprint: blueprintActor); + sceneComponent = new(actor, blueprint: blueprintSceneComponent); + levelScript = World.GetActor(); + skeletalMeshComponent = actor.GetComponent(); + animationInstance = skeletalMeshComponent.GetAnimationInstance(); + } + + public void OnBeginPlay() { + const string eventMessage = "Blueprint event dispatched"; + const float eventValue = 100.0f; + + Assert.IsTrue(levelScript.Invoke($"TestEvent \"{ eventMessage }: \" { eventValue }")); + Assert.IsTrue(actor.IsSpawned); + Assert.IsTrue(sceneComponent.IsCreated); + + TestActorBoolProperty(); + TestActorByteProperty(); + TestActorIntProperty(); + TestActorFloatProperty(); + TestActorStringProperty(); + TestActorTextProperty(); + TestSceneComponentBoolProperty(); + TestSceneComponentByteProperty(); + TestSceneComponentIntProperty(); + TestSceneComponentFloatProperty(); + TestSceneComponentStringProperty(); + TestSceneComponentTextProperty(); + TestLevelScriptBoolProperty(); + TestLevelScriptByteProperty(); + TestLevelScriptIntProperty(); + TestLevelScriptFloatProperty(); + TestLevelScriptStringProperty(); + TestLevelScriptTextProperty(); + TestAnimationBoolProperty(); + TestAnimationByteProperty(); + TestAnimationIntProperty(); + TestAnimationFloatProperty(); + TestAnimationStringProperty(); + TestAnimationTextProperty(); + } + + public void OnEndPlay() => Debug.ClearOnScreenMessages(); + + private void TestActorBoolProperty() { + bool value = false; + + Assert.IsTrue(actor.SetBool(boolProperty, true)); + + if (actor.GetBool(boolProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " actor property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " actor property value retrievement failed!"); + } + + private void TestActorByteProperty() { + byte value = 0; + + Assert.IsTrue(actor.SetByte(byteProperty, 128)); + + if (actor.GetByte(byteProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " actor property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " actor property value retrievement failed!"); + } + + private void TestActorIntProperty() { + int value = 0; + + Assert.IsTrue(actor.SetInt(intProperty, 500)); + + if (actor.GetInt(intProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " actor property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " actor property value retrievement failed!"); + } + + private void TestActorFloatProperty() { + float value = 0; + + Assert.IsTrue(actor.SetFloat(floatProperty, 250.5f)); + + if (actor.GetFloat(floatProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " actor property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " actor property value retrievement failed!"); + } + + private void TestActorStringProperty() { + string value = String.Empty; + + Assert.IsTrue(actor.SetString(stringProperty, "Test string from managed code")); + + if (actor.GetString(stringProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " actor property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " actor property value retrievement failed!"); + } + + private void TestActorTextProperty() { + string value = String.Empty; + + Assert.IsTrue(actor.SetText(textProperty, "Test text from managed code")); + + if (actor.GetText(textProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " actor property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " actor property value retrievement failed!"); + } + + private void TestSceneComponentBoolProperty() { + bool value = false; + + Assert.IsTrue(sceneComponent.SetBool(boolProperty, true)); + + if (sceneComponent.GetBool(boolProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " scene component property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " scene component property value retrievement failed!"); + } + + private void TestSceneComponentByteProperty() { + byte value = 0; + + Assert.IsTrue(sceneComponent.SetByte(byteProperty, 128)); + + if (sceneComponent.GetByte(byteProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " scene component property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " scene component property value retrievement failed!"); + } + + private void TestSceneComponentIntProperty() { + int value = 0; + + Assert.IsTrue(sceneComponent.SetInt(intProperty, 500)); + + if (sceneComponent.GetInt(intProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " scene component property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " scene component property value retrievement failed!"); + } + + private void TestSceneComponentFloatProperty() { + float value = 0; + + Assert.IsTrue(sceneComponent.SetFloat(floatProperty, 250.5f)); + + if (sceneComponent.GetFloat(floatProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " scene component property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " scene component property value retrievement failed!"); + } + + private void TestSceneComponentStringProperty() { + string value = String.Empty; + + Assert.IsTrue(sceneComponent.SetString(stringProperty, "Test string from managed code")); + + if (sceneComponent.GetString(stringProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " scene component property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " scene component property value retrievement failed!"); + } + + private void TestSceneComponentTextProperty() { + string value = String.Empty; + + Assert.IsTrue(sceneComponent.SetText(textProperty, "Test text from managed code")); + + if (sceneComponent.GetText(textProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " scene component property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " scene component property value retrievement failed!"); + } + + private void TestLevelScriptBoolProperty() { + bool value = false; + + Assert.IsTrue(levelScript.SetBool(boolProperty, true)); + + if (levelScript.GetBool(boolProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " level script property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " level script property value retrievement failed!"); + } + + private void TestLevelScriptByteProperty() { + byte value = 0; + + Assert.IsTrue(levelScript.SetByte(byteProperty, 128)); + + if (levelScript.GetByte(byteProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " level script property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " level script property value retrievement failed!"); + } + + private void TestLevelScriptIntProperty() { + int value = 0; + + Assert.IsTrue(levelScript.SetInt(intProperty, 500)); + + if (levelScript.GetInt(intProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " level script property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " level script property value retrievement failed!"); + } + + private void TestLevelScriptFloatProperty() { + float value = 0; + + Assert.IsTrue(levelScript.SetFloat(floatProperty, 250.5f)); + + if (levelScript.GetFloat(floatProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " level script property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " level script property value retrievement failed!"); + } + + private void TestLevelScriptStringProperty() { + string value = String.Empty; + + Assert.IsTrue(levelScript.SetString(stringProperty, "Test string message from managed code")); + + if (levelScript.GetString(stringProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " level script property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " level script property value retrievement failed!"); + } + + private void TestLevelScriptTextProperty() { + string value = String.Empty; + + Assert.IsTrue(levelScript.SetText(textProperty, "Test text message from managed code")); + + if (levelScript.GetText(textProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " level script property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " level script property value retrievement failed!"); + } + + private void TestAnimationBoolProperty() { + bool value = false; + + Assert.IsTrue(animationInstance.SetBool(boolProperty, true)); + + if (animationInstance.GetBool(boolProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " animation property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " animation property value retrievement failed!"); + } + + private void TestAnimationByteProperty() { + byte value = 0; + + Assert.IsTrue(animationInstance.SetByte(byteProperty, 128)); + + if (animationInstance.GetByte(byteProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " animation property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " animation property value retrievement failed!"); + } + + private void TestAnimationIntProperty() { + int value = 0; + + Assert.IsTrue(animationInstance.SetInt(intProperty, 500)); + + if (animationInstance.GetInt(intProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " animation property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " animation property value retrievement failed!"); + } + + private void TestAnimationFloatProperty() { + float value = 0; + + Assert.IsTrue(animationInstance.SetFloat(floatProperty, 250.5f)); + + if (animationInstance.GetFloat(floatProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " animation property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " animation property value retrievement failed!"); + } + + private void TestAnimationStringProperty() { + string value = String.Empty; + + Assert.IsTrue(animationInstance.SetString(stringProperty, "Test string message from managed code")); + + if (animationInstance.GetString(stringProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " animation property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " animation property value retrievement failed!"); + } + + private void TestAnimationTextProperty() { + string value = String.Empty; + + Assert.IsTrue(animationInstance.SetText(textProperty, "Test text message from managed code")); + + if (animationInstance.GetText(textProperty, ref value)) + Debug.AddOnScreenMessage(-1, 30.0f, Color.LimeGreen, value.GetType() + " animation property value retrieved: " + value); + else + Debug.AddOnScreenMessage(-1, 30.0f, Color.Red, value.GetType() + " animation property value retrievement failed!"); + } + + public static void TestBlueprintActorFunction(ObjectReference self) { + Actor blueprintActor = self.ToActor(); + + Debug.AddOnScreenMessage(-1, 30.0f, Color.Orange, "Cheers from managed function of the " + blueprintActor.Name); + } + + public static void TestBlueprintComponentFunction(ObjectReference self) { + SceneComponent blueprintSceneComponent = self.ToComponent(); + + Debug.AddOnScreenMessage(-1, 30.0f, Color.Orange, "Cheers from managed function of the " + blueprintSceneComponent.Name); + } + } +} +``` + +`Source/Managed/Tests/DebugVisualization.cs`: + +```cs +namespace UnrealEngine.Tests { + public class DebugVisualization : ISystem { + public void OnBeginPlay() { + World.GetFirstPlayerController().SetViewTarget(World.GetActor("MainCamera")); + + const float linesThickness = 3.0f; + + Debug.DrawBox(new(0.0f, -300.0f, 0.0f), new(50.0f, 50.0f, 50.0f), Quaternion.Identity, Color.Red, true, thickness: linesThickness); + Debug.DrawCapsule(new(0.0f, -150.0f, 0.0f), 50.0f, 25.0f, Quaternion.Identity, Color.Yellow, true, thickness: linesThickness); + Debug.DrawCone(new(0.0f, 0.0f, 50.0f), -Vector3.UnitZ, 115.0f, 0.5f, 0.5f, 8, Color.Green, true, thickness: linesThickness); + Debug.DrawCylinder(new(0.0f, 150.0f, 50.0f), new(0.0f, 150.0f, -50.0f), 50.0f, 32, Color.Blue, true, thickness: linesThickness); + Debug.DrawSphere(new(0.0f, 300.0f, 0.0f), 50.0f, 16, Color.DeepPink, true, thickness: linesThickness); + + Debug.DrawLine(new(0.0f, -350.0f, -100.0f), new(0.0f, 350.0f, -100.0f), Color.DarkViolet, true, thickness: linesThickness); + + Debug.DrawPoint(new(0.0f, -350.0f, -150.0f), 8.0f, Color.MediumVioletRed, true); + Debug.DrawPoint(new(0.0f, 350.0f, -150.0f), 8.0f, Color.MediumVioletRed, true); + } + + public void OnEndPlay() => Debug.ClearOnScreenMessages(); + } +} +``` + +`Source/Managed/Tests/DynamicEvents.cs`: + +```cs +namespace UnrealEngine.Tests { + public class DynamicEvents : ISystem { + private PlayerController playerController; + private TriggerBox triggerBox; + private BoxComponent triggerCollisionComponent; + private Actor leftActor; + private Actor rightActor; + private StaticMeshComponent leftStaticMeshComponent; + private StaticMeshComponent rightStaticMeshComponent; + private Material material; + private bool stopTranslation; + private const float startY = 450.0f; + + public DynamicEvents() { + playerController = World.GetFirstPlayerController(); + triggerBox = new(); + triggerCollisionComponent = triggerBox.GetComponent(); + leftActor = new("LeftActor"); + rightActor = new("RightActor"); + leftStaticMeshComponent = new(leftActor, "LeftActorComponent", true); + rightStaticMeshComponent = new(rightActor, "RightActorComponent", true); + material = Material.Load("/Game/Tests/BasicMaterial"); + stopTranslation = false; + } + + public void OnBeginPlay() { + playerController.ShowMouseCursor = true; + playerController.EnableClickEvents = true; + playerController.EnableMouseOverEvents = true; + playerController.SetViewTarget(World.GetActor("MainCamera")); + + World.SetOnActorBeginOverlapCallback(OnActorBeginOverlap); + World.SetOnActorEndOverlapCallback(OnActorEndOverlap); + World.SetOnActorHitCallback(OnActorHit); + World.SetOnActorBeginCursorOverCallback(OnActorBeginCursorOver); + World.SetOnActorEndCursorOverCallback(OnActorEndCursorOver); + World.SetOnActorClickedCallback(OnActorClicked); + World.SetOnActorReleasedCallback(OnActorReleased); + World.SetOnComponentBeginOverlapCallback(OnComponentBeginOverlap); + World.SetOnComponentEndOverlapCallback(OnComponentEndOverlap); + World.SetOnComponentHitCallback(OnComponentHit); + World.SetOnComponentBeginCursorOverCallback(OnComponentBeginCursorOver); + World.SetOnComponentEndCursorOverCallback(OnComponentEndCursorOver); + World.SetOnComponentClickedCallback(OnComponentClicked); + World.SetOnComponentReleasedCallback(OnComponentReleased); + + const float linesThickness = 3.0f; + + Vector3 collisionShape = new(200.0f, 200.0f, 200.0f); + + triggerCollisionComponent.SetBoxExtent(collisionShape); + + Debug.DrawBox(triggerCollisionComponent.GetLocation(), collisionShape, Quaternion.Identity, Color.Aqua, true, thickness: linesThickness); + + leftActor.RegisterEvent(ActorEventType.OnActorBeginOverlap); + leftActor.RegisterEvent(ActorEventType.OnActorEndOverlap); + leftActor.RegisterEvent(ActorEventType.OnActorHit); + leftActor.RegisterEvent(ActorEventType.OnActorBeginCursorOver); + leftActor.RegisterEvent(ActorEventType.OnActorEndCursorOver); + leftActor.RegisterEvent(ActorEventType.OnActorClicked); + leftActor.RegisterEvent(ActorEventType.OnActorReleased); + + leftStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentBeginOverlap); + leftStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentEndOverlap); + leftStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentHit); + leftStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentBeginCursorOver); + leftStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentEndCursorOver); + leftStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentClicked); + leftStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentReleased); + leftStaticMeshComponent.SetGenerateOverlapEvents(true); + leftStaticMeshComponent.SetGenerateHitEvents(true); + + leftStaticMeshComponent.SetStaticMesh(StaticMesh.Cube); + leftStaticMeshComponent.SetMaterial(0, material); + leftStaticMeshComponent.CreateAndSetMaterialInstanceDynamic(0).SetVectorParameterValue("Color", LinearColor.Green); + leftStaticMeshComponent.SetWorldLocation(new(0.0f, -startY, 0.0f)); + leftStaticMeshComponent.UpdateToWorld(TeleportType.ResetPhysics); + leftStaticMeshComponent.SetEnableGravity(false); + leftStaticMeshComponent.SetSimulatePhysics(true); + + Assert.IsNotNull(leftStaticMeshComponent.GetMaterial(0)); + + rightActor.RegisterEvent(ActorEventType.OnActorBeginOverlap); + rightActor.RegisterEvent(ActorEventType.OnActorEndOverlap); + rightActor.RegisterEvent(ActorEventType.OnActorHit); + rightActor.RegisterEvent(ActorEventType.OnActorBeginCursorOver); + rightActor.RegisterEvent(ActorEventType.OnActorEndCursorOver); + rightActor.RegisterEvent(ActorEventType.OnActorClicked); + rightActor.RegisterEvent(ActorEventType.OnActorReleased); + + rightStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentBeginOverlap); + rightStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentEndOverlap); + rightStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentHit); + rightStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentBeginCursorOver); + rightStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentEndCursorOver); + rightStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentClicked); + rightStaticMeshComponent.RegisterEvent(ComponentEventType.OnComponentReleased); + rightStaticMeshComponent.SetGenerateOverlapEvents(true); + rightStaticMeshComponent.SetGenerateHitEvents(true); + + rightStaticMeshComponent.SetStaticMesh(StaticMesh.Cube); + rightStaticMeshComponent.SetMaterial(0, material); + rightStaticMeshComponent.CreateAndSetMaterialInstanceDynamic(0).SetVectorParameterValue("Color", LinearColor.Yellow); + rightStaticMeshComponent.SetWorldLocation(new(0.0f, startY, 0.0f)); + rightStaticMeshComponent.UpdateToWorld(TeleportType.ResetPhysics); + rightStaticMeshComponent.SetEnableGravity(false); + rightStaticMeshComponent.SetSimulatePhysics(true); + + Assert.IsNotNull(rightStaticMeshComponent.GetMaterial(0)); + } + + public void OnTick(float deltaTime) { + Translate(leftStaticMeshComponent, 750.0f); + Translate(rightStaticMeshComponent, -750.0f); + + Hit hit = default; + + if (playerController.GetHitResultUnderCursor(CollisionChannel.WorldDynamic, ref hit)) + Debug.AddOnScreenMessage(13, 3.0f, Color.CornflowerBlue, "Cursor hit " + hit.GetActor().Name); + + triggerBox.ForEachOverlappingActor(OnTriggerOverlapActor); + triggerCollisionComponent.ForEachOverlappingComponent(OnTriggerOverlapComponent); + } + + public void OnEndPlay() => Debug.ClearOnScreenMessages(); + + private Action OnTriggerOverlapActor = (actor) => { + Debug.AddOnScreenMessage((int)(actor.ID % Int32.MaxValue), 3.0f, Color.Aqua, "Trigger box overlapped " + actor.Name); + }; + + private Action OnTriggerOverlapComponent = (component) => { + Debug.AddOnScreenMessage((int)(component.ID % Int32.MaxValue), 3.0f, Color.Aquamarine, "Trigger collision component overlapped " + component.Name); + }; + + private void Translate(StaticMeshComponent staticMeshComponent, float direction) { + if (!stopTranslation) { + Vector3 currentLocation = default; + + staticMeshComponent.GetLocation(ref currentLocation); + + currentLocation.Y += direction * World.DeltaTime; + + staticMeshComponent.SetWorldLocation(currentLocation); + } + } + + private void OnActorBeginOverlap(ActorReference overlapActor, ActorReference otherActor) { + Debug.AddOnScreenMessage(-1, 3.0f, overlapActor.ID == leftActor.ID ? Color.Lime : Color.Yellow, overlapActor.Name + " start overlapping " + otherActor.Name); + + Assert.IsNotNull(overlapActor.ToActor()); + Assert.IsNotNull(otherActor.ToActor()); + } + + private void OnActorEndOverlap(ActorReference overlapActor, ActorReference otherActor) { + Debug.AddOnScreenMessage(-1, 3.0f, overlapActor.ID == leftActor.ID ? Color.Lime : Color.Yellow, overlapActor.Name + " stop overlapping " + otherActor.Name); + + Assert.IsNotNull(overlapActor.ToActor()); + Assert.IsNotNull(otherActor.ToActor()); + } + + private void OnActorHit(ActorReference hitActor, ActorReference otherActor, in Vector3 normalImpulse, in Hit hit) { + Debug.AddOnScreenMessage(1, 3.0f, hitActor.ID == leftActor.ID ? Color.Lime : Color.Yellow, hitActor.Name + " hit " + otherActor.Name); + + if (!stopTranslation) { + leftStaticMeshComponent.AddImpulse(new(0.0f, -250.0f, 0.0f), velocityChange: true); + leftStaticMeshComponent.AddTorqueInRadians(new(20.0f, 25.0f, 30.0f), accelerationChange: true); + rightStaticMeshComponent.AddImpulse(new(0.0f, 250.0f, 0.0f), velocityChange: true); + rightStaticMeshComponent.AddTorqueInRadians(new(-20.0f, -25.0f, -30.0f), accelerationChange: true); + stopTranslation = true; + } + + Assert.IsNotNull(hitActor.ToActor()); + Assert.IsNotNull(otherActor.ToActor()); + Assert.IsNotNull(hit.GetActor()); + } + + private void OnActorBeginCursorOver(ActorReference actor) { + Debug.AddOnScreenMessage(2, 3.0f, Color.Plum, "Cursor moved over " + actor.Name); + + Assert.IsNotNull(actor.ToActor()); + } + + private void OnActorEndCursorOver(ActorReference actor) { + Debug.AddOnScreenMessage(3, 3.0f, Color.Plum, "Cursor moved off " + actor.Name); + + Assert.IsNotNull(actor.ToActor()); + } + + private void OnActorClicked(ActorReference actor, string key) { + Debug.AddOnScreenMessage(4, 3.0f, Color.Thistle, key + " clicked on " + actor.Name); + + if (key == Keys.LeftMouseButton) + Debug.AddOnScreenMessage(5, 3.0f, Color.MistyRose, key + " validated on actor!"); + + Assert.IsNotNull(actor.ToActor()); + } + + private void OnActorReleased(ActorReference actor, string key) { + Debug.AddOnScreenMessage(6, 3.0f, Color.Thistle, key + " released on " + actor.Name); + + if (key == Keys.LeftMouseButton) + Debug.AddOnScreenMessage(5, 3.0f, Color.MistyRose, key + " validated on actor!"); + + Assert.IsNotNull(actor.ToActor()); + } + + private void OnComponentBeginOverlap(ComponentReference overlapComponent, ComponentReference otherComponent) { + Debug.AddOnScreenMessage(-1, 3.0f, overlapComponent.ID == leftStaticMeshComponent.ID ? Color.Lime : Color.Yellow, overlapComponent.Name + " start overlapping " + otherComponent.Name); + + Assert.IsNotNull(overlapComponent.ToComponent()); + Assert.IsNotNull(otherComponent.ToComponent()); + } + + private void OnComponentEndOverlap(ComponentReference overlapComponent, ComponentReference otherComponent) { + Debug.AddOnScreenMessage(-1, 3.0f, overlapComponent.ID == leftStaticMeshComponent.ID ? Color.Lime : Color.Yellow, overlapComponent.Name + " end overlapping " + otherComponent.Name); + + Assert.IsNotNull(overlapComponent.ToComponent()); + Assert.IsNotNull(otherComponent.ToComponent()); + } + + private void OnComponentHit(ComponentReference hitComponent, ComponentReference otherComponent, in Vector3 normalImpulse, in Hit hit) { + Debug.AddOnScreenMessage(7, 3.0f, hitComponent.ID == leftStaticMeshComponent.ID ? Color.Lime : Color.Yellow, hitComponent.Name + " hit " + otherComponent.Name); + + Assert.IsNotNull(hitComponent.ToComponent()); + Assert.IsNotNull(otherComponent.ToComponent()); + Assert.IsNotNull(hit.GetActor()); + } + + private void OnComponentBeginCursorOver(ComponentReference component) { + Debug.AddOnScreenMessage(8, 3.0f, Color.Plum, "Cursor moved over " + component.Name); + + Assert.IsNotNull(component.ToComponent()); + } + + private void OnComponentEndCursorOver(ComponentReference component) { + Debug.AddOnScreenMessage(9, 3.0f, Color.Plum, "Cursor moved off " + component.Name); + + Assert.IsNotNull(component.ToComponent()); + } + + private void OnComponentClicked(ComponentReference component, string key) { + Debug.AddOnScreenMessage(10, 3.0f, Color.Thistle, key + " clicked on " + component.Name); + + if (key == Keys.LeftMouseButton) + Debug.AddOnScreenMessage(11, 3.0f, Color.MistyRose, key + " validated on component!"); + + Assert.IsNotNull(component.ToComponent()); + } + + private void OnComponentReleased(ComponentReference component, string key) { + Debug.AddOnScreenMessage(12, 3.0f, Color.Thistle, key + " released on " + component.Name); + + if (key == Keys.LeftMouseButton) + Debug.AddOnScreenMessage(11, 3.0f, Color.MistyRose, key + " validated on component!"); + + Assert.IsNotNull(component.ToComponent()); + } + } +} +``` + +`Source/Managed/Tests/DynamicsConsistency.cs`: + +```cs +namespace UnrealEngine.Tests { + public class DynamicsConsistency : ISystem { + private PlayerController playerController; + private PlayerInput playerInput; + private ConsoleVariable variable; + private uint commandsCount; + private const int variableValue = 64; + private const string consoleVariable = "TestVariable"; + private const string consoleCommand = "TestCommand"; + private const string pauseResumeAction = "Pause/Resume"; + private const string playerCommandAction = "PlayerCommandAction"; + private const string pauseResumeKey = Keys.Q; + private const string playerCommandKey = Keys.X; + private const string messageKey = Keys.E; + private const string mouseXAction = "Mouse X Message"; + private const string mouseYAction = "Mouse Y Message"; + private const string mouseXKey = Keys.MouseX; + private const string mouseYKey = Keys.MouseY; + + public DynamicsConsistency() { + playerController = World.GetFirstPlayerController(); + playerInput = playerController.GetPlayerInput(); + variable = ConsoleManager.RegisterVariable(consoleVariable, "A test variable", variableValue); + commandsCount = 0; + } + + public void OnBeginPlay() { + Assert.IsFalse(variable.IsBool); + Assert.IsFalse(variable.IsFloat); + Assert.IsFalse(variable.IsString); + Assert.IsTrue(variable.IsInt); + Assert.IsTrue(variable.GetInt() == variableValue); + + variable.SetOnChangedCallback(VariableEvent); + + ConsoleManager.RegisterCommand(consoleCommand, "Executes a test command", ConsoleCommand); + + Assert.IsTrue(ConsoleManager.IsRegisteredVariable(consoleCommand)); + + Engine.AddActionMapping(pauseResumeAction, pauseResumeKey); + Engine.AddAxisMapping(mouseXAction, mouseXKey); + Engine.AddAxisMapping(mouseYAction, mouseYKey); + + playerInput.AddActionMapping(playerCommandAction, playerCommandKey); + + InputComponent inputComponent = playerController.InputComponent; + + Assert.IsFalse(inputComponent.HasBindings); + + inputComponent.BindAction(pauseResumeAction, InputEvent.Pressed, PauseResume, true); + inputComponent.BindAction(playerCommandAction, InputEvent.Pressed, PlayerCommand, true); + inputComponent.BindAxis(mouseXAction, MouseXMessage); + inputComponent.BindAxis(mouseYAction, MouseYMessage); + + Assert.IsTrue(inputComponent.HasBindings); + Assert.IsTrue(inputComponent.ActionBindingsNumber == 2); + + const string removableAction = "TestRemovable"; + const string removableKey = Keys.R; + + playerInput.AddActionMapping(removableAction, removableKey, ctrl: true, alt: true); + playerInput.RemoveActionMapping(removableAction, removableKey); + } + + public void OnTick(float deltaTime) { + TimeTest(); + MousePositionTest(); + WindowTest(); + ActionBindingsTest(); + KeyPressTest(); + ConsoleTest(); + } + + public void OnEndPlay() { + ConsoleManager.UnregisterObject(consoleVariable); + ConsoleManager.UnregisterObject(consoleCommand); + Debug.ClearOnScreenMessages(); + } + + private void VariableEvent() => Debug.AddOnScreenMessage(-1, 5.0f, Color.LightBlue, "Variable has changed: " + variable.GetInt()); + + private void ConsoleCommand(float value) => Debug.AddOnScreenMessage(-1, 5.0f, Color.LightGreen, "Test console command executed: " + value); + + private void PauseResume() => playerController.SetPause(!playerController.IsPaused); + + private void PlayerCommand() => playerController.ConsoleCommand(consoleCommand + " " + ++commandsCount); + + private void MouseXMessage(float axisValue) => Debug.AddOnScreenMessage(1, 3.0f, Color.PaleGoldenrod, "Mouse X axis value: " + axisValue); + + private void MouseYMessage(float axisValue) => Debug.AddOnScreenMessage(2, 3.0f, Color.PaleGoldenrod, "Mouse Y axis value: " + axisValue); + + private void TimeTest() { + Debug.AddOnScreenMessage(3, 3.0f, Color.LightCyan, "Time: " + World.Time); + Debug.AddOnScreenMessage(4, 3.0f, Color.LightCyan, "Delta time: " + World.DeltaTime); + Debug.AddOnScreenMessage(5, 3.0f, Color.LightCyan, "Real time: " + World.RealTime); + } + + private void MousePositionTest() { + float mousePositionX = 0.0f; + float mousePositionY = 0.0f; + + playerController.GetMousePosition(ref mousePositionX, ref mousePositionY); + + Debug.AddOnScreenMessage(7, 3.0f, Color.MediumAquamarine, "Mouse position X: " + mousePositionX); + Debug.AddOnScreenMessage(8, 3.0f, Color.MediumAquamarine, "Mouse position Y: " + mousePositionY); + } + + private void WindowTest() { + Vector2 viewportSize = default; + + Engine.GetViewportSize(ref viewportSize); + + Debug.AddOnScreenMessage(9, 3.0f, Color.LightSkyBlue, "Viewport size X: " + viewportSize.X); + Debug.AddOnScreenMessage(10, 3.0f, Color.LightSkyBlue, "Viewport size Y: " + viewportSize.Y); + Debug.AddOnScreenMessage(11, 3.0f, Color.LightSteelBlue, "Window has focus: " + Engine.IsForegroundWindow); + Debug.AddOnScreenMessage(12, 3.0f, Color.LightSteelBlue, "Window mode: " + Engine.WindowMode); + } + + private void ActionBindingsTest() { + Debug.AddOnScreenMessage(13, 3.0f, Color.Orange, "Action bindings: " + playerController.InputComponent.ActionBindingsNumber); + Debug.AddOnScreenMessage(14, 3.0f, Color.Orange, "Press [" + pauseResumeKey + "] key to pause/resume"); + Debug.AddOnScreenMessage(15, 3.0f, Color.Orange, "Press [" + playerCommandKey + "] key to execute a player command"); + } + + private void KeyPressTest() { + Debug.AddOnScreenMessage(16, 3.0f, Color.Khaki, "Press [" + messageKey + "] key for a message"); + + if (playerInput.IsKeyPressed(Keys.E)) + Debug.AddOnScreenMessage(-1, 0.1f, Color.LightSalmon, "[" + messageKey + "] key pressed!"); + } + + private void ConsoleTest() { + Debug.AddOnScreenMessage(17, 3.0f, Color.YellowGreen, "Enter " + consoleVariable + " (value) to the console to change a variable"); + Debug.AddOnScreenMessage(18, 3.0f, Color.YellowGreen, "Enter " + consoleCommand + " (value) to the console to execute a command"); + } + } +} +``` + +`Source/Managed/Tests/ExceptionsConsistency.cs`: + +```cs +namespace UnrealEngine.Tests { + public class ExceptionsConsistency : ISystem { + private const string consoleVariable = "TestVariable"; + private const string consoleCommand = "TestCommand"; + + public void OnBeginPlay() { + ConsoleVariable variable = ConsoleManager.RegisterVariable(consoleVariable, "A test variable", 0); + + ConsoleManager.RegisterCommand(consoleCommand, "A test command", ConsoleCommand); + + variable.SetOnChangedCallback(VariableEvent); + + PlayerController playerController = World.GetFirstPlayerController(); + + playerController.ConsoleCommand(consoleVariable + " 1"); + playerController.ConsoleCommand(consoleCommand + " 1.5"); + + throw new Exception("Test exception (OnBeginPlay)"); + } + + public void OnEndPlay() { + ConsoleManager.UnregisterObject(consoleVariable); + ConsoleManager.UnregisterObject(consoleCommand); + Debug.ClearOnScreenMessages(); + } + + private void VariableEvent() => throw new Exception("Test exception (VariableEvent)"); + + private void ConsoleCommand(float value) => throw new Exception("Test exception (ConsoleCommand)"); + } +} +``` + +`Source/Managed/Tests/ExternalConsistency.cs`: + +```cs +namespace UnrealEngine.Tests { + public class ExternalConsistency : ISystem { + public void OnBeginPlay() { + AssetRegistryTest(); + CommandLineArgumentsTest(); + ReferencesEqualityTest(); + NamingTest(); + HashCodesTest(); + ActorsHierarchyTest(); + ChildActorsTest(); + ComponentsAttachmentTest(); + ComponentsMatchingTest(); + NonSceneComponentTest(); + ObjectIDsTest(); + MaxFramesPerSecondTest(); + TagsTest(); + + Debug.AddOnScreenMessage(-1, 10.0f, Color.MediumTurquoise, "Verify " + MethodBase.GetCurrentMethod().DeclaringType + " results in output log!"); + } + + private void AssetRegistryTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + AssetRegistry assetRegistry = new(); + + if (!assetRegistry.HasAssets("/Game/Tests", true)) { + Debug.Log(LogLevel.Error, "Asset registry assets path test failed!"); + + return; + } + + bool assetFound = false; + + Action OnAsset = (asset) => { + if (asset.Path == "/Game/Tests/Tests") + assetFound = true; + }; + + assetRegistry.ForEachAsset(OnAsset, "/Game/Tests", true); + + if (!assetFound) { + Debug.Log(LogLevel.Error, "Asset registry path to asset test failed!"); + + return; + } + + Debug.Log(LogLevel.Display, "Test passed successfully"); + } + + private void CommandLineArgumentsTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + string append = " -test 1"; + + CommandLine.Append(append); + + string arguments = CommandLine.Get(); + + if (arguments.IndexOf(append) == -1) { + Debug.Log(LogLevel.Error, "Command-line append operation failed!"); + + return; + } + + append = "-test 2"; + + CommandLine.Set(append); + + arguments = CommandLine.Get(); + + if (arguments.IndexOf(append) == -1) { + Debug.Log(LogLevel.Error, "Command-line set operation failed!"); + + return; + } + + ushort exceptions = 0; + + try { + CommandLine.Append(null); + } + + catch { + exceptions++; + } + + try { + CommandLine.Set(null); + } + + catch { + exceptions++; + } + + if (exceptions != 2) { + Debug.Log(LogLevel.Error, "Command-line null checks failed!"); + + return; + } + + Debug.Log(LogLevel.Display, "Test passed successfully"); + } + + private void ReferencesEqualityTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + TriggerBox actorLeft = new(); + TriggerSphere actorRight = new(); + SceneComponent sceneComponentLeft = new(actorLeft); + SceneComponent sceneComponentRight = new(actorRight); + + if (sceneComponentLeft.Equals(sceneComponentRight) || !sceneComponentLeft.Equals(sceneComponentLeft)) { + Debug.Log(LogLevel.Error, "Scene components equality check failed!"); + + return; + } + + if (actorLeft.Equals(sceneComponentRight.GetActor()) || !actorLeft.Equals(sceneComponentLeft.GetActor())) { + Debug.Log(LogLevel.Error, "Scene components owners equality check failed!"); + + return; + } + + sceneComponentLeft.Destroy(); + + if (sceneComponentRight.Equals(sceneComponentLeft)) { + Debug.Log(LogLevel.Error, "Scene components equality check after destruction failed!"); + + return; + } + + sceneComponentRight.Destroy(); + + if (sceneComponentRight.Equals(sceneComponentRight)) { + Debug.Log(LogLevel.Error, "Scene components equality check with null failed!"); + + return; + } + + if (actorLeft.Equals(actorRight) || !actorLeft.Equals(actorLeft)) { + Debug.Log(LogLevel.Error, "Actors equality check failed!"); + + return; + } + + actorLeft.Destroy(); + + if (actorRight.Equals(actorLeft)) { + Debug.Log(LogLevel.Error, "Actors equality check after destruction failed!"); + + return; + } + + actorRight.Destroy(); + + if (actorRight.Equals(actorRight)) { + Debug.Log(LogLevel.Error, "Actors equality check with null failed!"); + + return; + } + + Debug.Log(LogLevel.Display, "Test passed successfully"); + } + + private void NamingTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + const string actorName = "TestActorName"; + const string actorShortName = "TestActor"; + const string componentName = "TestComponentName"; + const string componentShortName = "TestComponent"; + const string renamedSuffix = "Renamed"; + + Actor actor = new(actorName); + SceneComponent sceneComponent = new(actor, componentName); + + if (actor.Name != actorName || sceneComponent.Name != componentName) { + Debug.Log(LogLevel.Error, "Names check failed!"); + + return; + } + + Actor namedActor = World.GetActor(actorName); + + if (!actor.Equals(namedActor)) { + Debug.Log(LogLevel.Error, "Actor from world with a name check after removing failed!"); + + return; + } + + actor.Rename(actorName + renamedSuffix); + sceneComponent.Rename(componentName + renamedSuffix); + + if (actor.Name != actorName + renamedSuffix || sceneComponent.Name != componentName + renamedSuffix) { + Debug.Log(LogLevel.Error, "Renaming to more characters check failed!"); + + return; + } + + actor.Rename(actorShortName); + sceneComponent.Rename(componentShortName); + + if (actor.Name != actorShortName || sceneComponent.Name != componentShortName) { + Debug.Log(LogLevel.Error, "Renaming to less characters check failed!"); + + return; + } + + Debug.Log(LogLevel.Display, "Test passed successfully"); + } + + private void HashCodesTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + Actor actorLeft = new(); + Actor actorRight = new(); + SceneComponent sceneComponentLeft = new(actorLeft); + SceneComponent sceneComponentRight = new(actorRight); + + if (sceneComponentLeft.GetHashCode() == sceneComponentRight.GetHashCode() || sceneComponentLeft.GetHashCode() != sceneComponentLeft.GetHashCode()) { + Debug.Log(LogLevel.Error, "Scene components hash codes check failed!"); + + return; + } + + sceneComponentLeft.Destroy(); + + if (sceneComponentRight.GetHashCode() == sceneComponentLeft.GetHashCode()) { + Debug.Log(LogLevel.Error, "Scene components hash codes check after destruction failed!"); + + return; + } + + if (actorLeft.GetHashCode() == actorRight.GetHashCode() || actorLeft.GetHashCode() != actorLeft.GetHashCode()) { + Debug.Log(LogLevel.Error, "Actors hash codes check failed!"); + + return; + } + + actorLeft.Destroy(); + + if (actorRight.GetHashCode() == actorLeft.GetHashCode()) { + Debug.Log(LogLevel.Error, "Actors hash codes check after destruction failed!"); + + return; + } + + Debug.Log(LogLevel.Display, "Test passed successfully"); + } + + private void ActorsHierarchyTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + const string actorName = "TestPlayerController"; + + PlayerController playerController = new(actorName); + Actor actor = World.GetActor(actorName); + + if (playerController == null || actor == null) { + Debug.Log(LogLevel.Error, "Actor valid references check failed!"); + + return; + } + + AmbientSound ambientSound = World.GetActor(actorName); + Brush brush = World.GetActor(actorName); + + if (ambientSound != null || brush != null) { + Debug.Log(LogLevel.Error, "Actor invalid references check failed!"); + + return; + } + + Debug.Log(LogLevel.Display, "Test passed successfully"); + } + + private void ChildActorsTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + Actor actor = new(); + ChildActorComponent childActorComponent = new(actor, setAsRoot: true); + TriggerBox childActor = childActorComponent.SetChildActor(); + + if (childActor == null) { + Debug.Log(LogLevel.Error, "Child actor creation check failed!"); + + return; + } + + childActor = childActorComponent.GetChildActor(); + + if (childActor == null) { + Debug.Log(LogLevel.Error, "Child actor obtainment check failed!"); + + return; + } + + BoxComponent boxComponent = childActor.GetComponent(); + Vector3 initialExtent = new(100.0f, 100.0f, 100.0f); + + boxComponent.InitBoxExtent(initialExtent); + + if (initialExtent != boxComponent.GetUnscaledBoxExtent()) { + Debug.Log(LogLevel.Error, "Child actor box extent check failed!"); + + return; + } + + int attachedActorCount = 0; + + Action OnAttachedActor = (actor) => attachedActorCount++; + + actor.ForEachAttachedActor(OnAttachedActor); + + if (attachedActorCount != 1) { + Debug.Log(LogLevel.Error, "Batched attached actor check failed!"); + + return; + } + + int childActorCount = 0; + + Action OnChildActor = (actor) => childActorCount++; + + actor.ForEachChildActor(OnChildActor); + + if (childActorCount != 1) { + Debug.Log(LogLevel.Error, "Batched child actor check failed!"); + + return; + } + + Debug.Log(LogLevel.Display, "Test passed successfully"); + } + + private void ComponentsAttachmentTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + Actor actor = new(); + StaticMeshComponent staticMeshComponent = new(actor, setAsRoot: true); + InstancedStaticMeshComponent instancedStaticMeshComponent = new(actor); + SceneComponent sceneComponent = new(actor); + BoxComponent boxComponent = new(actor); + SphereComponent sphereComponent = new(actor); + + sceneComponent.AttachToComponent(instancedStaticMeshComponent, AttachmentTransformRule.KeepRelativeTransform); + + if (!sceneComponent.IsAttachedToComponent(instancedStaticMeshComponent)) { + Debug.Log(LogLevel.Error, "Component attachment check failed!"); + + return; + } + + if (!sceneComponent.IsAttachedToActor(actor)) { + Debug.Log(LogLevel.Error, "Component attachment to actor check failed!"); + + return; + } + + int componentCount = 0; + + Action OnComponent = (component) => componentCount++; + + actor.ForEachComponent(OnComponent); + + if (componentCount != 5) { + Debug.Log(LogLevel.Error, "Batched component check failed!"); + + return; + } + + int attachedChildCount = 0; + + Action OnAttachedChild = (component) => attachedChildCount++; + + staticMeshComponent.ForEachAttachedChild(OnAttachedChild); + + if (attachedChildCount != 3) { + Debug.Log(LogLevel.Error, "Batched component child attachment check failed!"); + + return; + } + + Debug.Log(LogLevel.Display, "Test passed successfully"); + } + + private void ComponentsMatchingTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + Actor actor = new(); + StaticMeshComponent staticMeshComponent = new(actor, setAsRoot: true); + SceneComponent sceneComponent = actor.GetRootComponent(); + InstancedStaticMeshComponent instancedStaticMeshComponent = actor.GetRootComponent(); + + if (sceneComponent == null) { + Debug.Log(LogLevel.Error, "Component obtainment with inherited type matching to the root component failed!"); + + return; + } + + if (instancedStaticMeshComponent != null) { + Debug.Log(LogLevel.Error, "Component obtainment with type matching to the root component failed!"); + + return; + } + + if (actor.GetComponent() == null) { + Debug.Log(LogLevel.Error, "Component type matching failed!"); + + return; + } + + if (actor.GetRootComponent() == null) { + Debug.Log(LogLevel.Error, "Component type matching to the root component failed!"); + + return; + } + + Debug.Log(LogLevel.Display, "Test passed successfully"); + } + + private void NonSceneComponentTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + Actor actor = new(); + RotatingMovementComponent rotatingMovementComponent = new(actor, "TestName"); + + if (rotatingMovementComponent == null) { + Debug.Log(LogLevel.Error, "Component was not instantiated!"); + + return; + } + + Debug.Log(LogLevel.Display, "Test passed successfully"); + } + + private void ObjectIDsTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + Actor actor = new(); + SceneComponent sceneComponent = new(actor, setAsRoot: true); + + Actor actorByID = World.GetActorByID(actor.ID); + + if (actorByID == null) { + Debug.Log(LogLevel.Error, "Actor with a valid ID was not found!"); + + return; + } + + SceneComponent sceneComponentByID = actorByID.GetComponent(); + + if (sceneComponentByID == null) { + Debug.Log(LogLevel.Error, "Component with a valid ID was not found!"); + + return; + } + + Debug.Log(LogLevel.Display, "Test passed successfully"); + } + + private void MaxFramesPerSecondTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + float currentMaxFPS = Engine.MaxFPS; + float newMaxFPS = 60.0f; + + Engine.MaxFPS = newMaxFPS; + + float maxFPS = Engine.MaxFPS; + + if (!maxFPS.Equals(newMaxFPS)) { + Debug.Log(LogLevel.Error, "Max FPS check failed!"); + + return; + } + + Engine.MaxFPS = currentMaxFPS; + + Debug.Log(LogLevel.Display, "Test passed successfully"); + } + + private void TagsTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + Actor actor = new(); + SceneComponent sceneComponent = new(actor); + + const string tag = "TestTag"; + + actor.AddTag(tag); + + if (!actor.HasTag(tag)) { + Debug.Log(LogLevel.Error, "Actor tag check failed!"); + + return; + } + + Actor taggedActor = World.GetActorByTag(tag); + + if (!actor.Equals(taggedActor)) { + Debug.Log(LogLevel.Error, "Actor from world with a tag check failed!"); + + return; + } + + actor.RemoveTag(tag); + + if (actor.HasTag(tag)) { + Debug.Log(LogLevel.Error, "Actor tag check after removing failed!"); + + return; + } + + sceneComponent.AddTag(tag); + + if (!sceneComponent.HasTag(tag)) { + Debug.Log(LogLevel.Error, "Component tag check failed!"); + + return; + } + + SceneComponent taggedSceneComponent = taggedActor.GetComponentByTag(tag); + + if (!sceneComponent.Equals(taggedSceneComponent)) { + Debug.Log(LogLevel.Error, "Component from actor with a tag check failed!"); + + return; + } + + sceneComponent.RemoveTag(tag); + + if (sceneComponent.HasTag(tag)) { + Debug.Log(LogLevel.Error, "Component tag check after removing failed!"); + + return; + } + + Debug.Log(LogLevel.Display, "Test passed successfully"); + } + } +} +``` + +`Source/Managed/Tests/InstancedStaticMeshes.cs`: + +```cs +namespace UnrealEngine.Tests { + public class InstancedStaticMeshes : ISystem { + private Actor actor; + private SceneComponent sceneComponent; + private Transform[] transforms; + private InstancedStaticMeshComponent instancedStaticMeshComponent; + private Material material; + private float rotationSpeed; + private const int maxCubes = 200; + + public InstancedStaticMeshes() { + actor = new("InstancedCubes"); + sceneComponent = new(actor); + transforms = new Transform[maxCubes]; + instancedStaticMeshComponent = new(actor, setAsRoot: true); + material = Material.Load("/Game/Tests/BasicMaterial"); + rotationSpeed = 2.5f; + } + + public void OnBeginPlay() { + World.GetFirstPlayerController().SetViewTarget(World.GetActor("MainCamera")); + + instancedStaticMeshComponent.SetStaticMesh(StaticMesh.Cube); + instancedStaticMeshComponent.SetMaterial(0, material); + instancedStaticMeshComponent.CreateAndSetMaterialInstanceDynamic(0).SetVectorParameterValue("Color", LinearColor.White); + + for (int i = 0; i < maxCubes; i++) { + sceneComponent.SetRelativeLocation(new(150.0f * i, 50.0f * i, 100.0f * i)); + sceneComponent.SetRelativeRotation(Maths.CreateFromYawPitchRoll(5.0f * i, 0.0f, 0.0f)); + sceneComponent.AddLocalOffset(new(15.0f * i, 20.0f * i, 25.0f * i)); + sceneComponent.GetTransform(ref transforms[i]); + } + + instancedStaticMeshComponent.AddInstances(transforms); + + Debug.AddOnScreenMessage(-1, 3.0f, Color.LightGreen, "Instances are created! Number of instances: " + instancedStaticMeshComponent.InstanceCount); + } + + public void OnTick(float deltaTime) { + Debug.AddOnScreenMessage(1, 1.0f, Color.SkyBlue, "Frame number: " + Engine.FrameNumber); + + Quaternion deltaRotation = Maths.CreateFromYawPitchRoll(rotationSpeed * deltaTime, rotationSpeed * deltaTime, rotationSpeed * deltaTime); + + for (int i = 0; i < maxCubes; i++) { + sceneComponent.SetWorldTransform(transforms[i]); + sceneComponent.AddLocalRotation(deltaRotation); + sceneComponent.GetTransform(ref transforms[i]); + } + + instancedStaticMeshComponent.BatchUpdateInstanceTransforms(0, transforms, markRenderStateDirty: true); + } + + public void OnEndPlay() => Debug.ClearOnScreenMessages(); + } +} +``` + +`Source/Managed/Tests/ObjectOrientedDesign.cs`: + +```cs +namespace UnrealEngine.Tests { + public class ObjectOrientedDesign : ISystem { + private Entity[] entities; + private Material material; + private Random random; + private const int maxEntities = 10; + + public ObjectOrientedDesign() { + entities = new Entity[maxEntities]; + material = Material.Load("/Game/Tests/BasicMaterial"); + random = new(); + } + + public void OnBeginPlay() { + World.GetFirstPlayerController().SetViewTarget(World.GetActor("MainCamera")); + + for (int i = 0; i < maxEntities; i++) { + string entityName = "Entity" + (i > 0 ? i.ToString() : String.Empty); + + entities[i] = new(entityName); + entities[i].CreateMesh(material, 1.0f, "StateComponent", true); + entities[i].StateComponent.SetRelativeRotation(Maths.CreateFromYawPitchRoll(5.0f * i, 0.0f, 0.0f)); + entities[i].StateComponent.CreateAndSetMaterialInstanceDynamic(0).SetVectorParameterValue("Color", new((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble())); + entities[i].StateComponent.SetRelativeLocation(new(0.0f, 0.0f, 120.0f * i)); + entities[i].StateComponent.AddLocalOffset(new(0.0f, 0.0f, -420.0f)); + + Entity entity = World.GetActor(entityName); + StateComponent component = entity.GetComponent(); + + Assert.IsTrue(entity.Equals(entities[i])); + Assert.IsTrue(component.Equals(entities[i].StateComponent)); + } + + Debug.AddOnScreenMessage(-1, 3.0f, Color.LightGreen, "Actors are spawned! Number of actors in the world: " + World.ActorCount); + } + + public void OnTick(float deltaTime) { + for (int i = 0; i < maxEntities; i++) { + entities[i].StateComponent.AddLocalRotation(new(Vector3.UnitZ * entities[i].StateComponent.RotationSpeed * deltaTime, -1.0f)); + } + } + + public void OnEndPlay() => Debug.ClearOnScreenMessages(); + + private class Entity : Actor { + private StateComponent component; + + public StateComponent StateComponent => component; + + public Entity(string name = null) : base(name) { } + + public void CreateMesh(Material material, float rotationSpeed, string name = null, bool setAsRoot = false) { + component = new(this, rotationSpeed, name, setAsRoot); + component.SetStaticMesh(StaticMesh.Cube); + component.SetMaterial(0, material); + } + } + + private class StateComponent : StaticMeshComponent { + public float RotationSpeed { get; set; } + + public StateComponent(Entity entity, float rotationSpeed, string name = null, bool setAsRoot = false) : base(entity, name, setAsRoot) => RotationSpeed = rotationSpeed; + } + } +} +``` + +`Source/Managed/Tests/PhysicsSimulation.cs`: + +```cs +namespace UnrealEngine.Tests { + public class PhysicsSimulation : ISystem { + private Actor[] actors; + private StaticMeshComponent[] staticMeshComponents; + private Material material; + private float rotationSpeed; + private Random random; + private const int maxActors = 200; + + public PhysicsSimulation() { + actors = new Actor[maxActors]; + staticMeshComponents = new StaticMeshComponent[maxActors]; + material = Material.Load("/Game/Tests/BasicMaterial"); + rotationSpeed = 2.5f; + random = new(); + } + + public void OnBeginPlay() { + Debug.AddOnScreenMessage(-1, 3.0f, Color.LightGreen, MethodBase.GetCurrentMethod().DeclaringType + " system started!"); + + World.GetFirstPlayerController().SetViewTarget(World.GetActor("MainCamera")); + + const int halfActors = maxActors / 2; + + for (int i = 0; i < maxActors; i++) { + actors[i] = new(); + staticMeshComponents[i] = new(actors[i], setAsRoot: true); + staticMeshComponents[i].SetStaticMesh(StaticMesh.Cube); + staticMeshComponents[i].SetMaterial(0, material); + staticMeshComponents[i].CreateAndSetMaterialInstanceDynamic(0).SetVectorParameterValue("Color", LinearColor.Yellow); + + if (i < halfActors) + staticMeshComponents[i].SetRelativeLocation(new(0.0f, -400.0f, 250.0f * i)); + else + staticMeshComponents[i].SetRelativeLocation(new(0.0f, 400.0f, 250.0f * (i - halfActors))); + + staticMeshComponents[i].UpdateToWorld(TeleportType.ResetPhysics); + staticMeshComponents[i].SetSimulatePhysics(true); + } + + Debug.AddOnScreenMessage(-1, 3.0f, Color.LightGreen, "Actors are spawned! Number of actors in the world: " + World.ActorCount); + } + + public void OnTick(float deltaTime) { + Debug.AddOnScreenMessage(1, 1.0f, Color.SkyBlue, "Frame number: " + Engine.FrameNumber); + + Quaternion deltaRotation = Maths.CreateFromYawPitchRoll(rotationSpeed * deltaTime, 0.0f, 0.0f); + + for (int i = 0; i < maxActors; i++) { + staticMeshComponents[i].AddLocalRotation(deltaRotation); + } + } + + public void OnEndPlay() => Debug.ClearOnScreenMessages(); + } +} +``` + +`Source/Managed/Tests/RadialForce.cs`: + +```cs +namespace UnrealEngine.Tests { + public class RadialForce : ISystem { + public void OnBeginPlay() { + World.GetFirstPlayerController().SetViewTarget(World.GetActor("MainCamera")); + + const int maxActors = 100; + + Actor[] actors = new Actor[maxActors]; + StaticMeshComponent[] staticMeshComponents = new StaticMeshComponent[maxActors]; + + for (int i = 0; i < maxActors; i++) { + actors[i] = new(); + staticMeshComponents[i] = new(actors[i], setAsRoot: true); + staticMeshComponents[i].SetStaticMesh(StaticMesh.Cube); + staticMeshComponents[i].SetMaterial(0, Material.Load("/Game/Tests/BasicMaterial")); + staticMeshComponents[i].CreateAndSetMaterialInstanceDynamic(0).SetVectorParameterValue("Color", LinearColor.Red); + staticMeshComponents[i].SetRelativeLocation(new Vector3(10000.0f, 0.0f, 0.0f)); + staticMeshComponents[i].UpdateToWorld(TeleportType.ResetPhysics); + staticMeshComponents[i].SetSimulatePhysics(true); + staticMeshComponents[i].SetCollisionChannel(CollisionChannel.PhysicsBody); + } + + RadialForceComponent radialForceComponent = new(new(), setAsRoot: true); + + radialForceComponent.IgnoreOwningActor = true; + radialForceComponent.ImpulseVelocityChange = true; + radialForceComponent.LinearFalloff = true; + radialForceComponent.ForceStrength = 10000000.0f; + radialForceComponent.Radius = 1000; + radialForceComponent.SetRelativeLocation(new(10000.0f, 0.0f, -100.0f)); + + Debug.AddOnScreenMessage(-1, 3.0f, Color.LightGreen, "Actors are spawned! Number of actors in the world: " + World.ActorCount); + } + + public void OnEndPlay() => Debug.ClearOnScreenMessages(); + } +} +``` + +`Source/Managed/Tests/RuntimeConsistency.cs`: + +```cs +namespace UnrealEngine.Tests { + public class RuntimeConsistency : ISystem { + public void OnBeginPlay() { + ActorMemoryManagementTest(); + ComponentMemoryManagementTest(); + ActorBlueprintClassesMatchingTest(); + ComponentBlueprintClassesMatchingTest(); + DuplicateActorMemoryManagementTest(); + DuplicateComponentMemoryManagementTest(); + ConsoleVariablesMemoryManagementTest(); + + Debug.AddOnScreenMessage(-1, 10.0f, Color.MediumTurquoise, "Verify " + MethodBase.GetCurrentMethod().DeclaringType + " results in output log!"); + } + + private void ActorMemoryManagementTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + try { + Actor actor = new(); + SceneComponent sceneComponent = new(actor, setAsRoot: true); + + actor.Destroy(); + + Debug.Log(LogLevel.Display, "Triggering invalid action after the destruction"); + + sceneComponent.Destroy(); + } + + catch (Exception exception) { + Debug.Log(LogLevel.Display, "The exception has successfully reached: " + exception.Message); + + return; + } + + Debug.Log(LogLevel.Error, MethodBase.GetCurrentMethod().Name + " test failed!"); + } + + private void ComponentMemoryManagementTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + try { + Actor actor = new(); + SceneComponent sceneComponent = new(actor, setAsRoot: true); + + sceneComponent.Destroy(); + + Debug.Log(LogLevel.Display, "Triggering invalid action after the destruction"); + + sceneComponent.AddLocalOffset(default(Vector3)); + } + + catch (Exception exception) { + Debug.Log(LogLevel.Display, "The exception has successfully reached: " + exception.Message); + + return; + } + + Debug.Log(LogLevel.Error, MethodBase.GetCurrentMethod().Name + " test failed!"); + } + + private void ActorBlueprintClassesMatchingTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + try { + Blueprint blueprintActor = Blueprint.Load("/Game/Tests/BlueprintCameraActor"); + + Debug.Log(LogLevel.Display, "Triggering invalid action after the blueprint loading"); + + Actor actor = new(blueprint: blueprintActor); + } + + catch (Exception exception) { + Debug.Log(LogLevel.Display, "The exception has successfully reached: " + exception.Message); + + return; + } + + Debug.Log(LogLevel.Error, MethodBase.GetCurrentMethod().Name + " test failed!"); + } + + private void ComponentBlueprintClassesMatchingTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + try { + Blueprint blueprintComponent = Blueprint.Load("/Game/Tests/BlueprintActor"); + Actor actor = new(); + + Debug.Log(LogLevel.Display, "Triggering invalid action after the blueprint loading"); + + SceneComponent sceneComponent = new(actor, blueprint: blueprintComponent); + } + + catch (Exception exception) { + Debug.Log(LogLevel.Display, "The exception has successfully reached: " + exception.Message); + + return; + } + + Debug.Log(LogLevel.Error, MethodBase.GetCurrentMethod().Name + " test failed!"); + } + + private void DuplicateActorMemoryManagementTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + const string actorName = "Player"; + + try { + Pawn pawn = new(); + Pawn namedPawn = new(actorName); + Pawn duplicateActor = World.GetActor(actorName); + + if (!namedPawn.Equals(duplicateActor)) { + Debug.Log(LogLevel.Error, "Actor reference equality check failed!"); + + return; + } + + namedPawn.Destroy(); + + Debug.Log(LogLevel.Display, "Triggering invalid actions after the destruction"); + + duplicateActor.SetLifeSpan(5.0f); + duplicateActor.Destroy(); + } + + catch (Exception exception) { + Debug.Log(LogLevel.Display, "The exception has successfully reached: " + exception.Message); + + return; + } + + Debug.Log(LogLevel.Error, MethodBase.GetCurrentMethod().Name + " test failed!"); + } + + private void DuplicateComponentMemoryManagementTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + try { + Actor actor = new(); + SceneComponent sceneComponent = new(actor, setAsRoot: true); + SceneComponent duplicateReference = actor.GetRootComponent(); + + if (!sceneComponent.Equals(duplicateReference)) { + Debug.Log(LogLevel.Error, "Scene component reference equality check failed!"); + + return; + } + + sceneComponent.Destroy(); + + Debug.Log(LogLevel.Display, "Triggering invalid actions after the destruction"); + + duplicateReference.AddLocalOffset(default(Vector3)); + duplicateReference.Destroy(); + } + + catch (Exception exception) { + Debug.Log(LogLevel.Display, "The exception has successfully reached: " + exception.Message); + + return; + } + + Debug.Log(LogLevel.Error, MethodBase.GetCurrentMethod().Name + " test failed!"); + } + + private void ConsoleVariablesMemoryManagementTest() { + Debug.Log(LogLevel.Display, "Starting " + MethodBase.GetCurrentMethod().Name + "..."); + + const string variableName = "TestVariable"; + + try { + ConsoleVariable variable = ConsoleManager.RegisterVariable(variableName, "A test variable", 64); + + if (!ConsoleManager.IsRegisteredVariable(variableName)) { + Debug.Log(LogLevel.Error, "Variable registration check failed!"); + + return; + } + + ConsoleManager.UnregisterObject(variableName); + + Debug.Log(LogLevel.Display, "Triggering invalid actions after the destruction"); + + variable.GetInt(); + } + + catch (Exception exception) { + Debug.Log(LogLevel.Display, "The exception has successfully reached: " + exception.Message); + + return; + } + + Debug.Log(LogLevel.Error, MethodBase.GetCurrentMethod().Name + " test failed!"); + } + } +} +``` + +`Source/Managed/Tests/SkeletalMeshes.cs`: + +```cs +namespace UnrealEngine.Tests { + public class SkeletalMeshes : ISystem { + public void OnBeginPlay() { + World.GetFirstPlayerController().SetViewTarget(World.GetActor("MainCamera")); + + SkeletalMesh prototypeMesh = SkeletalMesh.Load("/Game/Tests/Characters/Prototype"); + + Actor leftPrototype = new("leftPrototype"); + SkeletalMeshComponent leftSkeletalMeshComponent = new(leftPrototype); + + leftSkeletalMeshComponent.SetSkeletalMesh(prototypeMesh); + leftSkeletalMeshComponent.SetWorldLocation(new(-700.0f, -70.0f, -100.0f)); + leftSkeletalMeshComponent.SetWorldRotation(Maths.Euler(0.0f, 0.0f, 90.0f)); + leftSkeletalMeshComponent.SetAnimationMode(AnimationMode.Asset); + leftSkeletalMeshComponent.PlayAnimation(AnimationSequence.Load("/Game/Tests/Characters/Animations/IdleAnimationSequence"), true); + + Assert.IsTrue(leftSkeletalMeshComponent.IsPlaying); + Assert.IsTrue(leftSkeletalMeshComponent.GetBoneName(0) == "root"); + + Bounds leftSkeletalMeshComponentBounds = default; + + leftSkeletalMeshComponent.GetBounds(leftSkeletalMeshComponent.GetTransform(), ref leftSkeletalMeshComponentBounds); + + Assert.IsFalse(leftSkeletalMeshComponentBounds == default(Bounds)); + + Actor rightPrototype = new("rightPrototype"); + SkeletalMeshComponent rightSkeletalMeshComponent = new(rightPrototype); + + rightSkeletalMeshComponent.SetSkeletalMesh(prototypeMesh); + rightSkeletalMeshComponent.SetWorldLocation(new(-700.0f, 70.0f, -100.0f)); + rightSkeletalMeshComponent.SetWorldRotation(Maths.Euler(0.0f, 0.0f, 90.0f)); + rightSkeletalMeshComponent.SetAnimationMode(AnimationMode.Asset); + + MaterialInstanceDynamic prototypeMaterial = rightSkeletalMeshComponent.CreateAndSetMaterialInstanceDynamic(0); + + prototypeMaterial.SetVectorParameterValue("AccentColor", new(0.0f, 0.5f, 1.0f)); + + Assert.IsNotNull(prototypeMaterial.GetParent()); + + AnimationMontage rightPrototypeAnimationMontage = AnimationMontage.Load("/Game/Tests/Characters/Animations/RunAnimationMontage"); + + rightSkeletalMeshComponent.PlayAnimation(rightPrototypeAnimationMontage, true); + + AnimationInstance rightPrototypeAnimationInstance = rightSkeletalMeshComponent.GetAnimationInstance(); + + Assert.IsTrue(rightPrototypeAnimationInstance.IsPlaying(rightPrototypeAnimationMontage)); + Assert.IsTrue(rightSkeletalMeshComponent.GetBoneName(0) == "root"); + + Bounds rightSkeletalMeshComponentBounds = default; + + rightSkeletalMeshComponent.GetBounds(rightSkeletalMeshComponent.GetTransform(), ref rightSkeletalMeshComponentBounds); + + Assert.IsFalse(rightSkeletalMeshComponentBounds == default(Bounds)); + } + + public void OnEndPlay() => Debug.ClearOnScreenMessages(); + } +} +``` + +`Source/Managed/Tests/SpatialQueries.cs`: + +```cs +namespace UnrealEngine.Tests { + public class SpatialQueries : ISystem { + public void OnBeginPlay() { + World.GetFirstPlayerController().SetViewTarget(World.GetActor("MainCamera")); + + const float linesThickness = 3.0f; + const string collisionProfile = "TestCollisionProfile"; + + Actor box = new("Box"); + StaticMeshComponent staticMeshComponent = new(box, setAsRoot: true); + Vector3 boxLocation = new(0.0f, 500.0f, 0.0f); + Vector3 boxScale = new(1.0f, 1.0f, 1.0f); + + staticMeshComponent.SetWorldLocation(boxLocation); + staticMeshComponent.SetWorldScale(boxScale); + staticMeshComponent.SetStaticMesh(StaticMesh.Cube); + staticMeshComponent.SetMaterial(0, Material.Load("/Game/Tests/BasicMaterial")); + staticMeshComponent.CreateAndSetMaterialInstanceDynamic(0).SetVectorParameterValue("Color", new(0.18f, 0.0f, 0.9f)); + staticMeshComponent.SetCollisionChannel(CollisionChannel.WorldStatic); + staticMeshComponent.SetCollisionProfileName(collisionProfile); + + Debug.DrawBox(boxLocation, boxScale * 50.0f, Quaternion.Identity, Color.SlateBlue, true, thickness: linesThickness); + + Hit hit = default; + Vector3 lineTraceStart = new(0.0f, 0.0f, 0.0f); + + bool hitTraceByChannel = World.LineTraceSingleByChannel(lineTraceStart, boxLocation, CollisionChannel.WorldStatic, ref hit); + + Assert.IsTrue(hitTraceByChannel); + Assert.IsTrue(hit.BlockingHit); + Assert.IsTrue(box.Equals(hit.GetActor())); + + if (hitTraceByChannel) { + Debug.AddOnScreenMessage(-1, 15.0f, Color.DeepPink, "Box trace hit by channel!"); + Debug.DrawPoint(hit.TraceStart, 8.0f, Color.DeepPink, true, depthPriority: 3); + Debug.DrawPoint(hit.TraceEnd, 8.0f, Color.DeepPink, true, depthPriority: 3); + Debug.DrawLine(hit.TraceStart, hit.TraceEnd, Color.DeepPink, true, depthPriority: 3, thickness: linesThickness); + } + + bool hitTraceByProfile = World.LineTraceSingleByProfile(lineTraceStart, boxLocation, collisionProfile, ref hit); + + Assert.IsTrue(hitTraceByProfile); + Assert.IsTrue(hit.BlockingHit); + + if (hitTraceByProfile) + Debug.AddOnScreenMessage(-1, 15.0f, Color.DeepPink, "Box trace hit by profile!"); + + const float sphereRadius = 50.0f; + + CollisionShape sphereShape = CollisionShape.CreateSphere(sphereRadius); + + bool hitSweepByChannel = World.SweepSingleByChannel(lineTraceStart, boxLocation, Quaternion.Identity, CollisionChannel.WorldStatic, sphereShape, ref hit); + + Assert.IsTrue(hitSweepByChannel); + Assert.IsTrue(hit.BlockingHit); + + if (hitSweepByChannel) { + Debug.AddOnScreenMessage(-1, 15.0f, Color.Yellow, "Sphere sweep hit by channel!"); + Debug.DrawSphere(hit.Location, sphereRadius, 16, Color.Yellow, true, depthPriority: 2, thickness: linesThickness); + } + + bool hitSweepByProfile = World.SweepSingleByProfile(lineTraceStart, boxLocation, Quaternion.Identity, collisionProfile, sphereShape, ref hit); + + Assert.IsTrue(hitSweepByProfile); + Assert.IsTrue(hit.BlockingHit); + + if (hitSweepByProfile) + Debug.AddOnScreenMessage(-1, 15.0f, Color.Yellow, "Sphere sweep hit by profile!"); + + Vector3 overlapLocation = boxLocation - new Vector3(0.0f, 100.0f, 0.0f); + Vector3 boxExtent = boxScale * 60.0f; + CollisionShape boxShape = CollisionShape.CreateBox(boxExtent); + + bool overlapByChannel = World.OverlapAnyTestByChannel(overlapLocation, Quaternion.Identity, CollisionChannel.WorldStatic, boxShape); + + Assert.IsTrue(hitSweepByProfile); + + if (overlapByChannel) { + Debug.AddOnScreenMessage(-1, 15.0f, Color.MediumTurquoise, "Box overlap by channel!"); + Debug.DrawBox(overlapLocation, boxExtent, Quaternion.Identity, Color.MediumTurquoise, true, thickness: linesThickness); + } + + bool overlapByProfile = World.OverlapAnyTestByProfile(overlapLocation, Quaternion.Identity, collisionProfile, boxShape); + + Assert.IsTrue(hitSweepByProfile); + + if (overlapByProfile) + Debug.AddOnScreenMessage(-1, 15.0f, Color.MediumTurquoise, "Box overlap by profile!"); + } + + public void OnEndPlay() => Debug.ClearOnScreenMessages(); + } +} +``` + +`Source/Managed/Tests/StaticMeshes.cs`: + +```cs +namespace UnrealEngine.Tests { + public class StaticMeshes : ISystem { + private Actor[] actors; + private StaticMeshComponent[] staticMeshComponents; + private Material material; + private float rotationSpeed; + private Random random; + private const int maxActors = 200; + + public StaticMeshes() { + actors = new Actor[maxActors]; + staticMeshComponents = new StaticMeshComponent[maxActors]; + material = Material.Load("/Game/Tests/BasicMaterial"); + rotationSpeed = 2.5f; + random = new(); + } + + public void OnBeginPlay() { + World.GetFirstPlayerController().SetViewTarget(World.GetActor("MainCamera")); + + for (int i = 0; i < maxActors; i++) { + actors[i] = new(); + staticMeshComponents[i] = new(actors[i], setAsRoot: true); + staticMeshComponents[i].AddLocalOffset(new(15.0f * i, 20.0f * i, 25.0f * i)); + staticMeshComponents[i].SetStaticMesh(StaticMesh.Cube); + staticMeshComponents[i].SetMaterial(0, material); + staticMeshComponents[i].CreateAndSetMaterialInstanceDynamic(0).SetVectorParameterValue("Color", new((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble())); + staticMeshComponents[i].SetRelativeLocation(new(150.0f * i, 50.0f * i, 100.0f * i)); + staticMeshComponents[i].SetRelativeRotation(Maths.CreateFromYawPitchRoll(5.0f * i, 0.0f, 0.0f)); + } + + Debug.AddOnScreenMessage(-1, 3.0f, Color.LightGreen, "Actors are spawned! Number of actors in the world: " + World.ActorCount); + } + + public void OnTick(float deltaTime) { + Debug.AddOnScreenMessage(1, 1.0f, Color.SkyBlue, "Frame number: " + Engine.FrameNumber); + + Quaternion deltaRotation = Maths.CreateFromYawPitchRoll(rotationSpeed * deltaTime, rotationSpeed * deltaTime, rotationSpeed * deltaTime); + + for (int i = 0; i < maxActors; i++) { + staticMeshComponents[i].AddLocalRotation(deltaRotation); + } + } + + public void OnEndPlay() => Debug.ClearOnScreenMessages(); + } +} +``` + +`Source/Managed/Tests/Tests.cs`: + +```cs +global using System; +global using System.Drawing; +global using System.Numerics; +global using System.Reflection; +global using UnrealEngine.Framework; + +namespace UnrealEngine.Tests { + public class Main { + private static ISystem runningSystem; + + public static void OnWorldPostBegin() { + Debug.Log(LogLevel.Display, "Hello, Unreal Engine!"); + + if (World.CurrentLevelName == "Tests") { + TestSystems testSystem = default; + + if (World.GetActor().GetEnum("Test Systems", ref testSystem)) + Debug.AddOnScreenMessage(-1, 3.0f, Color.LightGreen, testSystem + " system started!"); + + runningSystem = testSystem switch { + TestSystems.AssertionConsistency => new AssertionConsistency(), + TestSystems.AudioPlayback => new AudioPlayback(), + TestSystems.BlueprintsExtensibility => new BlueprintsExtensibility(), + TestSystems.DebugVisualization => new DebugVisualization(), + TestSystems.DynamicEvents => new DynamicEvents(), + TestSystems.DynamicsConsistency => new DynamicsConsistency(), + TestSystems.ExceptionsConsistency => new ExceptionsConsistency(), + TestSystems.ExternalConsistency => new ExternalConsistency(), + TestSystems.InstancedStaticMeshes => new InstancedStaticMeshes(), + TestSystems.ObjectOrientedDesign => new ObjectOrientedDesign(), + TestSystems.PhysicsSimulation => new PhysicsSimulation(), + TestSystems.RadialForce => new RadialForce(), + TestSystems.RuntimeConsistency => new RuntimeConsistency(), + TestSystems.SkeletalMeshes => new SkeletalMeshes(), + TestSystems.SpatialQueries => new SpatialQueries(), + TestSystems.StaticMeshes => new StaticMeshes(), + TestSystems.TextRenderer => new TextRenderer(), + TestSystems.TextureAssets => new TextureAssets(), + TestSystems.VirtualReality => new VirtualReality(), + _ => throw new Exception("Unknown system") + }; + } + + runningSystem?.OnBeginPlay(); + } + + public static void OnWorldPrePhysicsTick(float deltaTime) => runningSystem?.OnTick(deltaTime); + + public static void OnWorldEnd() { + runningSystem?.OnEndPlay(); + + Debug.Log(LogLevel.Display, "See you soon, Unreal Engine!"); + } + + private enum TestSystems { + AssertionConsistency, + AudioPlayback, + BlueprintsExtensibility, + DebugVisualization, + DynamicEvents, + DynamicsConsistency, + ExceptionsConsistency, + ExternalConsistency, + InstancedStaticMeshes, + ObjectOrientedDesign, + PhysicsSimulation, + RadialForce, + RuntimeConsistency, + SkeletalMeshes, + SpatialQueries, + StaticMeshes, + TextRenderer, + TextureAssets, + VirtualReality + } + } + + public interface ISystem { + void OnBeginPlay() { } + void OnTick(float deltaTime) { } + void OnEndPlay() { } + } +} +``` + +`Source/Managed/Tests/TextRenderer.cs`: + +```cs +namespace UnrealEngine.Tests { + public class TextRenderer : ISystem { + private Actor actor; + private TextRenderComponent textRenderComponent; + private Font font; + private Material material; + + public TextRenderer() { + actor = new("TextActor"); + textRenderComponent = new(actor); + font = Font.Load("/Game/Tests/PlayFont"); + material = Material.Load("/Game/Tests/PlayFontMaterial"); + } + + public void OnBeginPlay() { + textRenderComponent.SetFont(font); + textRenderComponent.SetTextMaterial(material); + textRenderComponent.SetText("WELCOME TO THE WORLD"); + textRenderComponent.SetHorizontalSpacingAdjustment(2.0f); + textRenderComponent.SetHorizontalAlignment(HorizontalTextAligment.Center); + textRenderComponent.SetVerticalAlignment(VerticalTextAligment.Center); + textRenderComponent.SetWorldLocation(new Vector3(200.0f, 0.0f, 0.0f)); + textRenderComponent.SetWorldRotation(Maths.Euler(0.0f, 0.0f, 180.0f)); + } + } +} +``` + +`Source/Managed/Tests/TextureAssets.cs`: + +```cs +namespace UnrealEngine.Tests { + public class TextureAssets : ISystem { + public void OnBeginPlay() { + World.GetFirstPlayerController().SetViewTarget(World.GetActor("MainCamera")); + + Actor actor = new(); + StaticMeshComponent staticMeshComponent = new(actor, setAsRoot: true); + Texture2D texture = Texture2D.Load("/Game/Tests/BasicTexture"); + + staticMeshComponent.SetStaticMesh(StaticMesh.Plane); + staticMeshComponent.SetMaterial(0, Material.Load("/Game/Tests/TextureMaterial")); + staticMeshComponent.CreateAndSetMaterialInstanceDynamic(0).SetTextureParameterValue("Texture", texture); + staticMeshComponent.SetWorldLocation(new(-800.0f, 0.0f, 0.0f)); + staticMeshComponent.SetWorldRotation(Maths.Euler(90.0f, 0.0f, 90.0f)); + + Debug.AddOnScreenMessage(-1, 5.0f, Color.PowderBlue, "Texture size: " + texture.GetSize()); + Debug.AddOnScreenMessage(-1, 5.0f, Color.PowderBlue, "Pixel format: " + texture.GetPixelFormat()); + } + + public void OnEndPlay() => Debug.ClearOnScreenMessages(); + } +} +``` + +`Source/Managed/Tests/UnrealEngine.Tests.csproj`: + +```csproj + + + + Library + net6.0 + x64 + False + False + $(DefineConstants);ASSERTIONS + + + + False + True + + + + True + False + + + + + ../Framework/bin/Release/UnrealEngine.Framework.dll + + + + + +``` + +`Source/Managed/Tests/VirtualReality.cs`: + +```cs +namespace UnrealEngine.Tests { + public class VirtualReality : ISystem { + private Pawn pawnVR; + private SceneComponent body; + private MotionControllerComponent leftHand; + private MotionControllerComponent rightHand; + + public VirtualReality() { + pawnVR = new("PawnVR"); + body = new(pawnVR, "Root"); + leftHand = new(pawnVR, "LeftHand"); + rightHand = new(pawnVR, "RightHand"); + } + + public void OnBeginPlay() { + Camera mainCamera = World.GetActor("MainCamera"); + + mainCamera.GetComponent().LockToHeadMountedDisplay = true; + + PlayerController playerController = World.GetFirstPlayerController(); + + playerController.SetViewTarget(mainCamera); + playerController.Possess(pawnVR); + + Pawn playerPawn = playerController.GetPawn(); + + Assert.IsTrue(playerPawn.IsControlled); + Assert.IsTrue(playerPawn.IsPlayerControlled); + + leftHand.DisplayDeviceModel = true; + leftHand.SetTrackingMotionSource("Left"); + + rightHand.DisplayDeviceModel = true; + rightHand.SetTrackingMotionSource("Right"); + } + + public void OnTick(float deltaTime) { + Debug.AddOnScreenMessage(1, 3.0f, Color.Yellow, "Head-mounted display is connected: " + HeadMountedDisplay.IsConnected); + Debug.AddOnScreenMessage(2, 3.0f, Color.Yellow, "Device enabled: " + HeadMountedDisplay.Enabled); + Debug.AddOnScreenMessage(3, 3.0f, Color.Yellow, "Device name: " + HeadMountedDisplay.DeviceName); + Debug.AddOnScreenMessage(4, 3.0f, Color.Orange, "Left motion controller is tracked: " + leftHand.IsTracked); + Debug.AddOnScreenMessage(5, 3.0f, Color.Orange, "Right motion controller is tracked: " + rightHand.IsTracked); + } + + public void OnEndPlay() => Debug.ClearOnScreenMessages(); + } +} +``` + +`Source/Native/Runtime/Linux/LICENSE.txt`: + +```txt +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +`Source/Native/Runtime/Linux/ThirdPartyNotices.txt`: + +```txt +.NET Runtime uses third-party libraries or other resources that may be +distributed under licenses different than the .NET Runtime software. + +In the event that we accidentally failed to list a required notice, please +bring it to our attention. Post an issue or email us: + + dotnet@microsoft.com + +The attached notices are provided for information only. + +License notice for ASP.NET +------------------------------- + +Copyright (c) .NET Foundation. All rights reserved. +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/dotnet/aspnetcore/blob/main/LICENSE.txt + +License notice for Slicing-by-8 +------------------------------- + +http://sourceforge.net/projects/slicing-by-8/ + +Copyright (c) 2004-2006 Intel Corporation - All Rights Reserved + + +This software program is licensed subject to the BSD License, available at +http://www.opensource.org/licenses/bsd-license.html. + +License notice for Unicode data +------------------------------- + +https://www.unicode.org/license.html + +Copyright © 1991-2020 Unicode, Inc. All rights reserved. +Distributed under the Terms of Use in https://www.unicode.org/copyright.html. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation +(the "Data Files") or Unicode software and any associated documentation +(the "Software") to deal in the Data Files or Software +without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, and/or sell copies of +the Data Files or Software, and to permit persons to whom the Data Files +or Software are furnished to do so, provided that either +(a) this copyright and permission notice appear with all copies +of the Data Files or Software, or +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS +NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, +use or other dealings in these Data Files or Software without prior +written authorization of the copyright holder. + +License notice for Zlib +----------------------- + +https://github.com/madler/zlib +http://zlib.net/zlib_license.html + +/* zlib.h -- interface of the 'zlib' general purpose compression library + version 1.2.11, January 15th, 2017 + + Copyright (C) 1995-2017 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +*/ + +License notice for Mono +------------------------------- + +http://www.mono-project.com/docs/about-mono/ + +Copyright (c) .NET Foundation Contributors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the Software), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for International Organization for Standardization +----------------------------------------------------------------- + +Portions (C) International Organization for Standardization 1986: + Permission to copy in any form is granted for use with + conforming SGML systems and applications as defined in + ISO 8879, provided this notice is included in all copies. + +License notice for Intel +------------------------ + +"Copyright (c) 2004-2006 Intel Corporation - All Rights Reserved + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for Xamarin and Novell +------------------------------------- + +Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Copyright (c) 2011 Novell, Inc (http://www.novell.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Third party notice for W3C +-------------------------- + +"W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE +Status: This license takes effect 13 May, 2015. +This work is being provided by the copyright holders under the following license. +License +By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions. +Permission to copy, modify, and distribute this work, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the work or portions thereof, including modifications: +The full text of this NOTICE in a location viewable to users of the redistributed or derivative work. +Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none exist, the W3C Software and Document Short Notice should be included. +Notice of any changes or modifications, through a copyright statement on the new code or document such as "This software or document includes material copied from or derived from [title and URI of the W3C document]. Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." +Disclaimers +THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. +COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. +The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the work without specific, written prior permission. Title to copyright in this work will at all times remain with copyright holders." + +License notice for Bit Twiddling Hacks +-------------------------------------- + +Bit Twiddling Hacks + +By Sean Eron Anderson +seander@cs.stanford.edu + +Individually, the code snippets here are in the public domain (unless otherwise +noted) — feel free to use them however you please. The aggregate collection and +descriptions are © 1997-2005 Sean Eron Anderson. The code and descriptions are +distributed in the hope that they will be useful, but WITHOUT ANY WARRANTY and +without even the implied warranty of merchantability or fitness for a particular +purpose. + +License notice for Brotli +-------------------------------------- + +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +compress_fragment.c: +Copyright (c) 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +""AS IS"" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +decode_fuzzer.c: +Copyright (c) 2015 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +""AS IS"" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + +License notice for Json.NET +------------------------------- + +https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md + +The MIT License (MIT) + +Copyright (c) 2007 James Newton-King + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for vectorized base64 encoding / decoding +-------------------------------------------------------- + +Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2013-2017, Alfred Klomp +Copyright (c) 2015-2017, Wojciech Mula +Copyright (c) 2016-2017, Matthieu Darbois +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for RFC 3492 +--------------------------- + +The punycode implementation is based on the sample code in RFC 3492 + +Copyright (C) The Internet Society (2003). All Rights Reserved. + +This document and translations of it may be copied and furnished to +others, and derivative works that comment on or otherwise explain it +or assist in its implementation may be prepared, copied, published +and distributed, in whole or in part, without restriction of any +kind, provided that the above copyright notice and this paragraph are +included on all such copies and derivative works. However, this +document itself may not be modified in any way, such as by removing +the copyright notice or references to the Internet Society or other +Internet organizations, except as needed for the purpose of +developing Internet standards in which case the procedures for +copyrights defined in the Internet Standards process must be +followed, or as required to translate it into languages other than +English. + +The limited permissions granted above are perpetual and will not be +revoked by the Internet Society or its successors or assigns. + +This document and the information contained herein is provided on an +"AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING +TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING +BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION +HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF +MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +License notice for Algorithm from Internet Draft document "UUIDs and GUIDs" +--------------------------------------------------------------------------- + +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & +Digital Equipment Corporation, Maynard, Mass. +To anyone who acknowledges that this file is provided "AS IS" +without any express or implied warranty: permission to use, copy, +modify, and distribute this file for any purpose is hereby +granted without fee, provided that the above copyright notices and +this notice appears in all source code copies, and that none of +the names of Open Software Foundation, Inc., Hewlett-Packard +Company, or Digital Equipment Corporation be used in advertising +or publicity pertaining to distribution of the software without +specific, written prior permission. Neither Open Software +Foundation, Inc., Hewlett-Packard Company, Microsoft, nor Digital Equipment +Corporation makes any representations about the suitability of +this software for any purpose. + +Copyright(C) The Internet Society 1997. All Rights Reserved. + +This document and translations of it may be copied and furnished to others, +and derivative works that comment on or otherwise explain it or assist in +its implementation may be prepared, copied, published and distributed, in +whole or in part, without restriction of any kind, provided that the above +copyright notice and this paragraph are included on all such copies and +derivative works.However, this document itself may not be modified in any +way, such as by removing the copyright notice or references to the Internet +Society or other Internet organizations, except as needed for the purpose of +developing Internet standards in which case the procedures for copyrights +defined in the Internet Standards process must be followed, or as required +to translate it into languages other than English. + +The limited permissions granted above are perpetual and will not be revoked +by the Internet Society or its successors or assigns. + +This document and the information contained herein is provided on an "AS IS" +basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING TASK FORCE +DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY +RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A +PARTICULAR PURPOSE. + +License notice for Algorithm from RFC 4122 - +A Universally Unique IDentifier (UUID) URN Namespace +---------------------------------------------------- + +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & +Digital Equipment Corporation, Maynard, Mass. +Copyright (c) 1998 Microsoft. +To anyone who acknowledges that this file is provided "AS IS" +without any express or implied warranty: permission to use, copy, +modify, and distribute this file for any purpose is hereby +granted without fee, provided that the above copyright notices and +this notice appears in all source code copies, and that none of +the names of Open Software Foundation, Inc., Hewlett-Packard +Company, Microsoft, or Digital Equipment Corporation be used in +advertising or publicity pertaining to distribution of the software +without specific, written prior permission. Neither Open Software +Foundation, Inc., Hewlett-Packard Company, Microsoft, nor Digital +Equipment Corporation makes any representations about the +suitability of this software for any purpose." + +License notice for The LLVM Compiler Infrastructure +--------------------------------------------------- + +Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + * Neither the names of the LLVM Team, University of Illinois at + Urbana-Champaign, nor the names of its contributors may be used to + endorse or promote products derived from this Software without specific + prior written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. + +License notice for Bob Jenkins +------------------------------ + +By Bob Jenkins, 1996. bob_jenkins@burtleburtle.net. You may use this +code any way you wish, private, educational, or commercial. It's free. + +License notice for Greg Parker +------------------------------ + +Greg Parker gparker@cs.stanford.edu December 2000 +This code is in the public domain and may be copied or modified without +permission. + +License notice for libunwind based code +---------------------------------------- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for Printing Floating-Point Numbers (Dragon4) +------------------------------------------------------------ + +/****************************************************************************** + Copyright (c) 2014 Ryan Juckett + http://www.ryanjuckett.com/ + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +******************************************************************************/ + +License notice for Printing Floating-point Numbers (Grisu3) +----------------------------------------------------------- + +Copyright 2012 the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for xxHash +------------------------- + +xxHash Library +Copyright (c) 2012-2014, Yann Collet +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for Berkeley SoftFloat Release 3e +------------------------------------------------ + +https://github.com/ucb-bar/berkeley-softfloat-3 +https://github.com/ucb-bar/berkeley-softfloat-3/blob/master/COPYING.txt + +License for Berkeley SoftFloat Release 3e + +John R. Hauser +2018 January 20 + +The following applies to the whole of SoftFloat Release 3e as well as to +each source file individually. + +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the +University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions, and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS", AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for xoshiro RNGs +-------------------------------- + +Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org) + +To the extent possible under law, the author has dedicated all copyright +and related and neighboring rights to this software to the public domain +worldwide. This software is distributed without any warranty. + +See . + +License for fastmod (https://github.com/lemire/fastmod) +-------------------------------------- + + Copyright 2018 Daniel Lemire + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +License notice for The C++ REST SDK +----------------------------------- + +C++ REST SDK + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for MessagePack-CSharp +------------------------------------- + +MessagePack for C# + +MIT License + +Copyright (c) 2017 Yoshifumi Kawai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for lz4net +------------------------------------- + +lz4net + +Copyright (c) 2013-2017, Milosz Krajewski + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for Nerdbank.Streams +----------------------------------- + +The MIT License (MIT) + +Copyright (c) Andrew Arnott + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for RapidJSON +---------------------------- + +Tencent is pleased to support the open source community by making RapidJSON available. + +Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved. + +Licensed under the MIT License (the "License"); you may not use this file except +in compliance with the License. You may obtain a copy of the License at + +http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +License notice for DirectX Math Library +--------------------------------------- + +https://github.com/microsoft/DirectXMath/blob/master/LICENSE + + The MIT License (MIT) + +Copyright (c) 2011-2020 Microsoft Corp + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for ldap4net +--------------------------- + +The MIT License (MIT) + +Copyright (c) 2018 Alexander Chermyanin + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for vectorized sorting code +------------------------------------------ + +MIT License + +Copyright (c) 2020 Dan Shechter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for musl +----------------------- + +musl as a whole is licensed under the following standard MIT license: + +Copyright © 2005-2020 Rich Felker, et al. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for "Faster Unsigned Division by Constants" +---------------------------------------------------------- + +Reference implementations of computing and using the "magic number" approach to dividing +by constants, including codegen instructions. The unsigned division incorporates the +"round down" optimization per ridiculous_fish. + +This is free and unencumbered software. Any copyright is dedicated to the Public Domain. + +License notice for mimalloc +--------------------------- + +MIT License + +Copyright (c) 2019 Microsoft Corporation, Daan Leijen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for Apple header files +------------------------------------- + +Copyright (c) 1980, 1986, 1993 + The Regents of the University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by the University of + California, Berkeley and its contributors. +4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +License notice for Angular v8.0 +-------------------------------- + +The MIT License (MIT) +===================== + +Copyright (c) 2010-2019 Google LLC. http://angular.io/license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +License notice for BedrockFramework +=================================== + +MIT License + +Copyright (c) 2019 David Fowler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for Swashbuckle +=================================== + +The MIT License (MIT) + +Copyright (c) 2016 Richard Morris + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for cli-spinners +============================================= + +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for dotnet-deb-tool +------------------------------------ + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for IIS-Common +------------------------------------ + +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE + +License notice for IIS-Setup +------------------------------------ + +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE + +License notice for LZMA SDK +--------------------------- + +http://7-zip.org/sdk.html + +LZMA SDK is placed in the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute the +original LZMA SDK code, either in source code form or as a compiled binary, +for any purpose, commercial or non-commercial, and by any means. + +License notice for MonoDevelop +------------------------------ + +Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Copyright (c) 2011 Novell, Inc (http://www.novell.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +License notice for Nuget.Client +------------------------------- + +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +License notice for Ookie.Dialogs +-------------------------------- + +http://www.ookii.org/software/dialogs/ + +Copyright © Sven Groot (Ookii.org) 2009 +All rights reserved. + + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1) Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2) Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3) Neither the name of the ORGANIZATION nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +License notice for viz.js +------------------------------------ + +Copyright (c) 2014-2018 Michael Daines + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for West Wind Live Reload ASP.NET Core Middleware +============================================= + + + +--- + +lz4net + +Copyright (c) 2013-2017, Milosz Krajewski + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +MIT License +----------- + +Copyright (c) 2019-2020 West Wind Technologies + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License for fastmod (https://github.com/lemire/fastmod) and ibm-fpgen (https://github.com/nigeltao/parse-number-fxx-test-data) +-------------------------------------- + + Copyright 2018 Daniel Lemire + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +License notice for JavaScript queues +------------------------------------- + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. + +Statement of Purpose +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: +the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; +moral rights retained by the original author(s) and/or performer(s); +publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; +rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; +rights protecting the extraction, dissemination, use and reuse of data in a Work; +database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and +other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. +2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. +3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. +4. Limitations and Disclaimers. +a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. +b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. +c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. +d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. + +License notice for MSBuild Locator +------------------------------------- + +https://github.com/Microsoft/MSBuildLocator + +Copyright (c) 2018 .NET Foundation and Contributors + +This software is licensed subject to the MIT license, available at +https://opensource.org/licenses/MIT + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for Newtonsoft.Json +=================================== + +The MIT License (MIT) + +Copyright (c) 2007 James Newton-King + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for NuGet.Client +------------------------------- + +In reference to: https://github.com/dotnet/templating/blob/main/build/nuget.exe + +https://github.com/NuGet/NuGet.Client/blob/dev/LICENSE.txt + +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +License notice for Roslyn Clr Heap Allocation Analyzer +------------------------------------- + +https://github.com/Microsoft/RoslynClrHeapAllocationAnalyzer + +Copyright (c) 2018 Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +License notice for StyleCop Analyzers +------------------------------------- + +The MIT License (MIT) + +Copyright (c) Tunnel Vision Laboratories, LLC + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for corefx +------------------------------------- + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +`Source/Native/Runtime/Linux/shared/Microsoft.NETCore.App/6.0.1/Microsoft.NETCore.App.deps.json`: + +```json +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0/linux-x64", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": {}, + ".NETCoreApp,Version=v6.0/linux-x64": { + "Microsoft.NETCore.App.Runtime.linux-x64/6.0.1": { + "runtime": { + "System.Private.CoreLib.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.VisualBasic.dll": { + "assemblyVersion": "10.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "mscorlib.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "netstandard.dll": { + "assemblyVersion": "2.1.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.AppContext.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Buffers.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.DataAnnotations.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Configuration.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Core.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Data.DataSetExtensions.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Data.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Contracts.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Debug.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Tools.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Tracing.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Drawing.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Dynamic.Runtime.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Globalization.Calendars.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Globalization.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Globalization.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Compression.FileSystem.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.UnmanagedMemoryStream.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Numerics.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Numerics.Vectors.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Emit.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Emit.ILGeneration.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Emit.Lightweight.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Resources.Reader.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Resources.ResourceManager.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Handles.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Intrinsics.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Loader.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.Json.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.Xml.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Principal.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.SecureString.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ServiceModel.Web.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ServiceProcess.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Encoding.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Encoding.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Overlapped.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Tasks.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Tasks.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Thread.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.ThreadPool.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Timer.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Transactions.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ValueTuple.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Web.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Windows.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.Linq.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.ReaderWriter.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.Serialization.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XDocument.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XmlDocument.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XmlSerializer.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XPath.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "WindowsBase.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.CSharp.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.VisualBasic.Core.dll": { + "assemblyVersion": "11.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.Win32.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.Win32.Registry.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.Concurrent.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.Immutable.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.NonGeneric.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.Specialized.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.Annotations.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.EventBasedAsync.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.TypeConverter.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Console.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Data.Common.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.DiagnosticSource.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.FileVersionInfo.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Process.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.StackTrace.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.TextWriterTraceListener.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.TraceSource.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Drawing.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Formats.Asn1.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Compression.Brotli.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Compression.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Compression.ZipFile.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.AccessControl.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.DriveInfo.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.Watcher.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.IsolatedStorage.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.MemoryMappedFiles.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Pipes.AccessControl.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Pipes.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Linq.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Linq.Expressions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Linq.Parallel.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Linq.Queryable.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Memory.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Http.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Http.Json.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.HttpListener.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Mail.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.NameResolution.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.NetworkInformation.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Ping.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Quic.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Requests.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Security.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.ServicePoint.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Sockets.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebClient.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebHeaderCollection.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebProxy.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebSockets.Client.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebSockets.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ObjectModel.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Private.DataContractSerialization.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Private.Uri.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Private.Xml.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Private.Xml.Linq.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.DispatchProxy.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Metadata.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.TypeExtensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Resources.Writer.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.CompilerServices.Unsafe.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.CompilerServices.VisualC.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.InteropServices.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.InteropServices.RuntimeInformation.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Numerics.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.Formatters.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.AccessControl.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Claims.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Algorithms.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Cng.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Csp.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Encoding.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.OpenSsl.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.X509Certificates.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Principal.Windows.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Encoding.CodePages.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Encodings.Web.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Json.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.RegularExpressions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Channels.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Tasks.Dataflow.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Tasks.Parallel.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Transactions.Local.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Web.HttpUtility.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XPath.XDocument.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + } + }, + "native": { + "createdump": { + "fileVersion": "0.0.0.0" + }, + "libclrjit.so": { + "fileVersion": "0.0.0.0" + }, + "libcoreclr.so": { + "fileVersion": "0.0.0.0" + }, + "libcoreclrtraceptprovider.so": { + "fileVersion": "0.0.0.0" + }, + "libdbgshim.so": { + "fileVersion": "0.0.0.0" + }, + "libmscordaccore.so": { + "fileVersion": "0.0.0.0" + }, + "libmscordbi.so": { + "fileVersion": "0.0.0.0" + }, + "libSystem.Globalization.Native.so": { + "fileVersion": "0.0.0.0" + }, + "libSystem.IO.Compression.Native.so": { + "fileVersion": "0.0.0.0" + }, + "libSystem.Native.so": { + "fileVersion": "0.0.0.0" + }, + "libSystem.Net.Security.Native.so": { + "fileVersion": "0.0.0.0" + }, + "libSystem.Security.Cryptography.Native.OpenSsl.so": { + "fileVersion": "0.0.0.0" + }, + "libhostpolicy.so": { + "fileVersion": "0.0.0.0" + } + } + } + } + }, + "libraries": { + "Microsoft.NETCore.App.Runtime.linux-x64/6.0.1": { + "type": "package", + "serviceable": true, + "sha512": "", + "path": "microsoft.netcore.app.runtime.linux-x64/6.0.1" + } + }, + "runtimes": { + "alpine-x64": [ + "alpine", + "linux-musl-x64", + "linux-musl", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "alpine.3.10-x64": [ + "alpine.3.10", + "alpine.3.9-x64", + "alpine.3.9", + "alpine.3.8-x64", + "alpine.3.8", + "alpine.3.7-x64", + "alpine.3.7", + "alpine.3.6-x64", + "alpine.3.6", + "alpine-x64", + "alpine", + "linux-musl-x64", + "linux-musl", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "alpine.3.11-x64": [ + "alpine.3.11", + "alpine.3.10-x64", + "alpine.3.10", + "alpine.3.9-x64", + "alpine.3.9", + "alpine.3.8-x64", + "alpine.3.8", + "alpine.3.7-x64", + "alpine.3.7", + "alpine.3.6-x64", + "alpine.3.6", + "alpine-x64", + "alpine", + "linux-musl-x64", + "linux-musl", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "alpine.3.12-x64": [ + "alpine.3.12", + "alpine.3.11-x64", + "alpine.3.11", + "alpine.3.10-x64", + "alpine.3.10", + "alpine.3.9-x64", + "alpine.3.9", + "alpine.3.8-x64", + "alpine.3.8", + "alpine.3.7-x64", + "alpine.3.7", + "alpine.3.6-x64", + "alpine.3.6", + "alpine-x64", + "alpine", + "linux-musl-x64", + "linux-musl", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "alpine.3.13-x64": [ + "alpine.3.13", + "alpine.3.12-x64", + "alpine.3.12", + "alpine.3.11-x64", + "alpine.3.11", + "alpine.3.10-x64", + "alpine.3.10", + "alpine.3.9-x64", + "alpine.3.9", + "alpine.3.8-x64", + "alpine.3.8", + "alpine.3.7-x64", + "alpine.3.7", + "alpine.3.6-x64", + "alpine.3.6", + "alpine-x64", + "alpine", + "linux-musl-x64", + "linux-musl", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "alpine.3.14-x64": [ + "alpine.3.14", + "alpine.3.13-x64", + "alpine.3.13", + "alpine.3.12-x64", + "alpine.3.12", + "alpine.3.11-x64", + "alpine.3.11", + "alpine.3.10-x64", + "alpine.3.10", + "alpine.3.9-x64", + "alpine.3.9", + "alpine.3.8-x64", + "alpine.3.8", + "alpine.3.7-x64", + "alpine.3.7", + "alpine.3.6-x64", + "alpine.3.6", + "alpine-x64", + "alpine", + "linux-musl-x64", + "linux-musl", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "alpine.3.6-x64": [ + "alpine.3.6", + "alpine-x64", + "alpine", + "linux-musl-x64", + "linux-musl", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "alpine.3.7-x64": [ + "alpine.3.7", + "alpine.3.6-x64", + "alpine.3.6", + "alpine-x64", + "alpine", + "linux-musl-x64", + "linux-musl", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "alpine.3.8-x64": [ + "alpine.3.8", + "alpine.3.7-x64", + "alpine.3.7", + "alpine.3.6-x64", + "alpine.3.6", + "alpine-x64", + "alpine", + "linux-musl-x64", + "linux-musl", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "alpine.3.9-x64": [ + "alpine.3.9", + "alpine.3.8-x64", + "alpine.3.8", + "alpine.3.7-x64", + "alpine.3.7", + "alpine.3.6-x64", + "alpine.3.6", + "alpine-x64", + "alpine", + "linux-musl-x64", + "linux-musl", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "android-x64": [ + "android", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "android.21-x64": [ + "android.21", + "android-x64", + "android", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "android.22-x64": [ + "android.22", + "android.21-x64", + "android.21", + "android-x64", + "android", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "android.23-x64": [ + "android.23", + "android.22-x64", + "android.22", + "android.21-x64", + "android.21", + "android-x64", + "android", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "android.24-x64": [ + "android.24", + "android.23-x64", + "android.23", + "android.22-x64", + "android.22", + "android.21-x64", + "android.21", + "android-x64", + "android", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "android.25-x64": [ + "android.25", + "android.24-x64", + "android.24", + "android.23-x64", + "android.23", + "android.22-x64", + "android.22", + "android.21-x64", + "android.21", + "android-x64", + "android", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "android.26-x64": [ + "android.26", + "android.25-x64", + "android.25", + "android.24-x64", + "android.24", + "android.23-x64", + "android.23", + "android.22-x64", + "android.22", + "android.21-x64", + "android.21", + "android-x64", + "android", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "android.27-x64": [ + "android.27", + "android.26-x64", + "android.26", + "android.25-x64", + "android.25", + "android.24-x64", + "android.24", + "android.23-x64", + "android.23", + "android.22-x64", + "android.22", + "android.21-x64", + "android.21", + "android-x64", + "android", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "android.28-x64": [ + "android.28", + "android.27-x64", + "android.27", + "android.26-x64", + "android.26", + "android.25-x64", + "android.25", + "android.24-x64", + "android.24", + "android.23-x64", + "android.23", + "android.22-x64", + "android.22", + "android.21-x64", + "android.21", + "android-x64", + "android", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "android.29-x64": [ + "android.29", + "android.28-x64", + "android.28", + "android.27-x64", + "android.27", + "android.26-x64", + "android.26", + "android.25-x64", + "android.25", + "android.24-x64", + "android.24", + "android.23-x64", + "android.23", + "android.22-x64", + "android.22", + "android.21-x64", + "android.21", + "android-x64", + "android", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "android.30-x64": [ + "android.30", + "android.29-x64", + "android.29", + "android.28-x64", + "android.28", + "android.27-x64", + "android.27", + "android.26-x64", + "android.26", + "android.25-x64", + "android.25", + "android.24-x64", + "android.24", + "android.23-x64", + "android.23", + "android.22-x64", + "android.22", + "android.21-x64", + "android.21", + "android-x64", + "android", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "android.31-x64": [ + "android.31", + "android.30-x64", + "android.30", + "android.29-x64", + "android.29", + "android.28-x64", + "android.28", + "android.27-x64", + "android.27", + "android.26-x64", + "android.26", + "android.25-x64", + "android.25", + "android.24-x64", + "android.24", + "android.23-x64", + "android.23", + "android.22-x64", + "android.22", + "android.21-x64", + "android.21", + "android-x64", + "android", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "arch-x64": [ + "arch", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "centos-x64": [ + "centos", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "centos.7-x64": [ + "centos.7", + "centos-x64", + "rhel.7-x64", + "centos", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "centos.8-x64": [ + "centos.8", + "centos-x64", + "rhel.8-x64", + "centos", + "rhel.8", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "centos.9-x64": [ + "centos.9", + "centos-x64", + "rhel.9-x64", + "centos", + "rhel.9", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "debian-x64": [ + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "debian.10-x64": [ + "debian.10", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "debian.11-x64": [ + "debian.11", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "debian.8-x64": [ + "debian.8", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "debian.9-x64": [ + "debian.9", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "exherbo-x64": [ + "exherbo", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora-x64": [ + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.23-x64": [ + "fedora.23", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.24-x64": [ + "fedora.24", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.25-x64": [ + "fedora.25", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.26-x64": [ + "fedora.26", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.27-x64": [ + "fedora.27", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.28-x64": [ + "fedora.28", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.29-x64": [ + "fedora.29", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.30-x64": [ + "fedora.30", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.31-x64": [ + "fedora.31", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.32-x64": [ + "fedora.32", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.33-x64": [ + "fedora.33", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.34-x64": [ + "fedora.34", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.35-x64": [ + "fedora.35", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "fedora.36-x64": [ + "fedora.36", + "fedora-x64", + "fedora", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "gentoo-x64": [ + "gentoo", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "linux-musl-x64": [ + "linux-musl", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "linux-x64": [ + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "linuxmint.17-x64": [ + "linuxmint.17", + "ubuntu.14.04-x64", + "ubuntu.14.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "linuxmint.17.1-x64": [ + "linuxmint.17.1", + "linuxmint.17-x64", + "linuxmint.17", + "ubuntu.14.04-x64", + "ubuntu.14.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "linuxmint.17.2-x64": [ + "linuxmint.17.2", + "linuxmint.17.1-x64", + "linuxmint.17.1", + "linuxmint.17-x64", + "linuxmint.17", + "ubuntu.14.04-x64", + "ubuntu.14.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "linuxmint.17.3-x64": [ + "linuxmint.17.3", + "linuxmint.17.2-x64", + "linuxmint.17.2", + "linuxmint.17.1-x64", + "linuxmint.17.1", + "linuxmint.17-x64", + "linuxmint.17", + "ubuntu.14.04-x64", + "ubuntu.14.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "linuxmint.18-x64": [ + "linuxmint.18", + "ubuntu.16.04-x64", + "ubuntu.16.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "linuxmint.18.1-x64": [ + "linuxmint.18.1", + "linuxmint.18-x64", + "linuxmint.18", + "ubuntu.16.04-x64", + "ubuntu.16.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "linuxmint.18.2-x64": [ + "linuxmint.18.2", + "linuxmint.18.1-x64", + "linuxmint.18.1", + "linuxmint.18-x64", + "linuxmint.18", + "ubuntu.16.04-x64", + "ubuntu.16.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "linuxmint.18.3-x64": [ + "linuxmint.18.3", + "linuxmint.18.2-x64", + "linuxmint.18.2", + "linuxmint.18.1-x64", + "linuxmint.18.1", + "linuxmint.18-x64", + "linuxmint.18", + "ubuntu.16.04-x64", + "ubuntu.16.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "linuxmint.19-x64": [ + "linuxmint.19", + "ubuntu.18.04-x64", + "ubuntu.18.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "linuxmint.19.1-x64": [ + "linuxmint.19.1", + "linuxmint.19-x64", + "linuxmint.19", + "ubuntu.18.04-x64", + "ubuntu.18.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "linuxmint.19.2-x64": [ + "linuxmint.19.2", + "linuxmint.19.1-x64", + "linuxmint.19.1", + "linuxmint.19-x64", + "linuxmint.19", + "ubuntu.18.04-x64", + "ubuntu.18.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ol-x64": [ + "ol", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ol.7-x64": [ + "ol.7", + "ol-x64", + "rhel.7-x64", + "ol", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ol.7.0-x64": [ + "ol.7.0", + "ol.7-x64", + "rhel.7.0-x64", + "ol.7", + "rhel.7.0", + "ol-x64", + "rhel.7-x64", + "ol", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ol.7.1-x64": [ + "ol.7.1", + "ol.7.0-x64", + "rhel.7.1-x64", + "ol.7.0", + "rhel.7.1", + "ol.7-x64", + "rhel.7.0-x64", + "ol.7", + "rhel.7.0", + "ol-x64", + "rhel.7-x64", + "ol", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ol.7.2-x64": [ + "ol.7.2", + "ol.7.1-x64", + "rhel.7.2-x64", + "ol.7.1", + "rhel.7.2", + "ol.7.0-x64", + "rhel.7.1-x64", + "ol.7.0", + "rhel.7.1", + "ol.7-x64", + "rhel.7.0-x64", + "ol.7", + "rhel.7.0", + "ol-x64", + "rhel.7-x64", + "ol", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ol.7.3-x64": [ + "ol.7.3", + "ol.7.2-x64", + "rhel.7.3-x64", + "ol.7.2", + "rhel.7.3", + "ol.7.1-x64", + "rhel.7.2-x64", + "ol.7.1", + "rhel.7.2", + "ol.7.0-x64", + "rhel.7.1-x64", + "ol.7.0", + "rhel.7.1", + "ol.7-x64", + "rhel.7.0-x64", + "ol.7", + "rhel.7.0", + "ol-x64", + "rhel.7-x64", + "ol", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ol.7.4-x64": [ + "ol.7.4", + "ol.7.3-x64", + "rhel.7.4-x64", + "ol.7.3", + "rhel.7.4", + "ol.7.2-x64", + "rhel.7.3-x64", + "ol.7.2", + "rhel.7.3", + "ol.7.1-x64", + "rhel.7.2-x64", + "ol.7.1", + "rhel.7.2", + "ol.7.0-x64", + "rhel.7.1-x64", + "ol.7.0", + "rhel.7.1", + "ol.7-x64", + "rhel.7.0-x64", + "ol.7", + "rhel.7.0", + "ol-x64", + "rhel.7-x64", + "ol", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ol.7.5-x64": [ + "ol.7.5", + "ol.7.4-x64", + "rhel.7.5-x64", + "ol.7.4", + "rhel.7.5", + "ol.7.3-x64", + "rhel.7.4-x64", + "ol.7.3", + "rhel.7.4", + "ol.7.2-x64", + "rhel.7.3-x64", + "ol.7.2", + "rhel.7.3", + "ol.7.1-x64", + "rhel.7.2-x64", + "ol.7.1", + "rhel.7.2", + "ol.7.0-x64", + "rhel.7.1-x64", + "ol.7.0", + "rhel.7.1", + "ol.7-x64", + "rhel.7.0-x64", + "ol.7", + "rhel.7.0", + "ol-x64", + "rhel.7-x64", + "ol", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ol.7.6-x64": [ + "ol.7.6", + "ol.7.5-x64", + "rhel.7.6-x64", + "ol.7.5", + "rhel.7.6", + "ol.7.4-x64", + "rhel.7.5-x64", + "ol.7.4", + "rhel.7.5", + "ol.7.3-x64", + "rhel.7.4-x64", + "ol.7.3", + "rhel.7.4", + "ol.7.2-x64", + "rhel.7.3-x64", + "ol.7.2", + "rhel.7.3", + "ol.7.1-x64", + "rhel.7.2-x64", + "ol.7.1", + "rhel.7.2", + "ol.7.0-x64", + "rhel.7.1-x64", + "ol.7.0", + "rhel.7.1", + "ol.7-x64", + "rhel.7.0-x64", + "ol.7", + "rhel.7.0", + "ol-x64", + "rhel.7-x64", + "ol", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ol.8-x64": [ + "ol.8", + "ol-x64", + "rhel.8-x64", + "ol", + "rhel.8", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ol.8.0-x64": [ + "ol.8.0", + "ol.8-x64", + "rhel.8.0-x64", + "ol.8", + "rhel.8.0", + "ol-x64", + "rhel.8-x64", + "ol", + "rhel.8", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "opensuse-x64": [ + "opensuse", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "opensuse.13.2-x64": [ + "opensuse.13.2", + "opensuse-x64", + "opensuse", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "opensuse.15.0-x64": [ + "opensuse.15.0", + "opensuse-x64", + "opensuse", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "opensuse.15.1-x64": [ + "opensuse.15.1", + "opensuse-x64", + "opensuse", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "opensuse.42.1-x64": [ + "opensuse.42.1", + "opensuse-x64", + "opensuse", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "opensuse.42.2-x64": [ + "opensuse.42.2", + "opensuse-x64", + "opensuse", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "opensuse.42.3-x64": [ + "opensuse.42.3", + "opensuse-x64", + "opensuse", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel-x64": [ + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel.6-x64": [ + "rhel.6", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel.7-x64": [ + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel.7.0-x64": [ + "rhel.7.0", + "rhel.7-x64", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel.7.1-x64": [ + "rhel.7.1", + "rhel.7.0-x64", + "rhel.7.0", + "rhel.7-x64", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel.7.2-x64": [ + "rhel.7.2", + "rhel.7.1-x64", + "rhel.7.1", + "rhel.7.0-x64", + "rhel.7.0", + "rhel.7-x64", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel.7.3-x64": [ + "rhel.7.3", + "rhel.7.2-x64", + "rhel.7.2", + "rhel.7.1-x64", + "rhel.7.1", + "rhel.7.0-x64", + "rhel.7.0", + "rhel.7-x64", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel.7.4-x64": [ + "rhel.7.4", + "rhel.7.3-x64", + "rhel.7.3", + "rhel.7.2-x64", + "rhel.7.2", + "rhel.7.1-x64", + "rhel.7.1", + "rhel.7.0-x64", + "rhel.7.0", + "rhel.7-x64", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel.7.5-x64": [ + "rhel.7.5", + "rhel.7.4-x64", + "rhel.7.4", + "rhel.7.3-x64", + "rhel.7.3", + "rhel.7.2-x64", + "rhel.7.2", + "rhel.7.1-x64", + "rhel.7.1", + "rhel.7.0-x64", + "rhel.7.0", + "rhel.7-x64", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel.7.6-x64": [ + "rhel.7.6", + "rhel.7.5-x64", + "rhel.7.5", + "rhel.7.4-x64", + "rhel.7.4", + "rhel.7.3-x64", + "rhel.7.3", + "rhel.7.2-x64", + "rhel.7.2", + "rhel.7.1-x64", + "rhel.7.1", + "rhel.7.0-x64", + "rhel.7.0", + "rhel.7-x64", + "rhel.7", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel.8-x64": [ + "rhel.8", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel.8.0-x64": [ + "rhel.8.0", + "rhel.8-x64", + "rhel.8", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel.8.1-x64": [ + "rhel.8.1", + "rhel.8.0-x64", + "rhel.8.0", + "rhel.8-x64", + "rhel.8", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "rhel.9-x64": [ + "rhel.9", + "rhel-x64", + "rhel", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "sles-x64": [ + "sles", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "sles.12-x64": [ + "sles.12", + "sles-x64", + "sles", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "sles.12.1-x64": [ + "sles.12.1", + "sles.12-x64", + "sles.12", + "sles-x64", + "sles", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "sles.12.2-x64": [ + "sles.12.2", + "sles.12.1-x64", + "sles.12.1", + "sles.12-x64", + "sles.12", + "sles-x64", + "sles", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "sles.12.3-x64": [ + "sles.12.3", + "sles.12.2-x64", + "sles.12.2", + "sles.12.1-x64", + "sles.12.1", + "sles.12-x64", + "sles.12", + "sles-x64", + "sles", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "sles.12.4-x64": [ + "sles.12.4", + "sles.12.3-x64", + "sles.12.3", + "sles.12.2-x64", + "sles.12.2", + "sles.12.1-x64", + "sles.12.1", + "sles.12-x64", + "sles.12", + "sles-x64", + "sles", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "sles.15-x64": [ + "sles.15", + "sles.12.4-x64", + "sles.12.4", + "sles.12.3-x64", + "sles.12.3", + "sles.12.2-x64", + "sles.12.2", + "sles.12.1-x64", + "sles.12.1", + "sles.12-x64", + "sles.12", + "sles-x64", + "sles", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "sles.15.1-x64": [ + "sles.15.1", + "sles.15-x64", + "sles.15", + "sles.12.4-x64", + "sles.12.4", + "sles.12.3-x64", + "sles.12.3", + "sles.12.2-x64", + "sles.12.2", + "sles.12.1-x64", + "sles.12.1", + "sles.12-x64", + "sles.12", + "sles-x64", + "sles", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu-x64": [ + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.14.04-x64": [ + "ubuntu.14.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.14.10-x64": [ + "ubuntu.14.10", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.15.04-x64": [ + "ubuntu.15.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.15.10-x64": [ + "ubuntu.15.10", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.16.04-x64": [ + "ubuntu.16.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.16.10-x64": [ + "ubuntu.16.10", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.17.04-x64": [ + "ubuntu.17.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.17.10-x64": [ + "ubuntu.17.10", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.18.04-x64": [ + "ubuntu.18.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.18.10-x64": [ + "ubuntu.18.10", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.19.04-x64": [ + "ubuntu.19.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.19.10-x64": [ + "ubuntu.19.10", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.20.04-x64": [ + "ubuntu.20.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.20.10-x64": [ + "ubuntu.20.10", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.21.04-x64": [ + "ubuntu.21.04", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ], + "ubuntu.21.10-x64": [ + "ubuntu.21.10", + "ubuntu-x64", + "ubuntu", + "debian-x64", + "debian", + "linux-x64", + "linux", + "unix-x64", + "unix", + "any", + "base" + ] + } +} +``` + +`Source/Native/Runtime/Linux/shared/Microsoft.NETCore.App/6.0.1/Microsoft.NETCore.App.runtimeconfig.json`: + +```json +{ + "runtimeOptions": { + "tfm": "net6.0", + "configProperties": { + "System.Reflection.Metadata.MetadataUpdater.IsSupported": false + } + } +} +``` + +`Source/Native/Runtime/Mac/LICENSE.txt`: + +```txt +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +`Source/Native/Runtime/Mac/ThirdPartyNotices.txt`: + +```txt +.NET Runtime uses third-party libraries or other resources that may be +distributed under licenses different than the .NET Runtime software. + +In the event that we accidentally failed to list a required notice, please +bring it to our attention. Post an issue or email us: + + dotnet@microsoft.com + +The attached notices are provided for information only. + +License notice for ASP.NET +------------------------------- + +Copyright (c) .NET Foundation. All rights reserved. +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/dotnet/aspnetcore/blob/main/LICENSE.txt + +License notice for Slicing-by-8 +------------------------------- + +http://sourceforge.net/projects/slicing-by-8/ + +Copyright (c) 2004-2006 Intel Corporation - All Rights Reserved + + +This software program is licensed subject to the BSD License, available at +http://www.opensource.org/licenses/bsd-license.html. + +License notice for Unicode data +------------------------------- + +https://www.unicode.org/license.html + +Copyright © 1991-2020 Unicode, Inc. All rights reserved. +Distributed under the Terms of Use in https://www.unicode.org/copyright.html. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation +(the "Data Files") or Unicode software and any associated documentation +(the "Software") to deal in the Data Files or Software +without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, and/or sell copies of +the Data Files or Software, and to permit persons to whom the Data Files +or Software are furnished to do so, provided that either +(a) this copyright and permission notice appear with all copies +of the Data Files or Software, or +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS +NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, +use or other dealings in these Data Files or Software without prior +written authorization of the copyright holder. + +License notice for Zlib +----------------------- + +https://github.com/madler/zlib +http://zlib.net/zlib_license.html + +/* zlib.h -- interface of the 'zlib' general purpose compression library + version 1.2.11, January 15th, 2017 + + Copyright (C) 1995-2017 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +*/ + +License notice for Mono +------------------------------- + +http://www.mono-project.com/docs/about-mono/ + +Copyright (c) .NET Foundation Contributors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the Software), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for International Organization for Standardization +----------------------------------------------------------------- + +Portions (C) International Organization for Standardization 1986: + Permission to copy in any form is granted for use with + conforming SGML systems and applications as defined in + ISO 8879, provided this notice is included in all copies. + +License notice for Intel +------------------------ + +"Copyright (c) 2004-2006 Intel Corporation - All Rights Reserved + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for Xamarin and Novell +------------------------------------- + +Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Copyright (c) 2011 Novell, Inc (http://www.novell.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Third party notice for W3C +-------------------------- + +"W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE +Status: This license takes effect 13 May, 2015. +This work is being provided by the copyright holders under the following license. +License +By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions. +Permission to copy, modify, and distribute this work, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the work or portions thereof, including modifications: +The full text of this NOTICE in a location viewable to users of the redistributed or derivative work. +Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none exist, the W3C Software and Document Short Notice should be included. +Notice of any changes or modifications, through a copyright statement on the new code or document such as "This software or document includes material copied from or derived from [title and URI of the W3C document]. Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." +Disclaimers +THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. +COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. +The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the work without specific, written prior permission. Title to copyright in this work will at all times remain with copyright holders." + +License notice for Bit Twiddling Hacks +-------------------------------------- + +Bit Twiddling Hacks + +By Sean Eron Anderson +seander@cs.stanford.edu + +Individually, the code snippets here are in the public domain (unless otherwise +noted) — feel free to use them however you please. The aggregate collection and +descriptions are © 1997-2005 Sean Eron Anderson. The code and descriptions are +distributed in the hope that they will be useful, but WITHOUT ANY WARRANTY and +without even the implied warranty of merchantability or fitness for a particular +purpose. + +License notice for Brotli +-------------------------------------- + +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +compress_fragment.c: +Copyright (c) 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +""AS IS"" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +decode_fuzzer.c: +Copyright (c) 2015 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +""AS IS"" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + +License notice for Json.NET +------------------------------- + +https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md + +The MIT License (MIT) + +Copyright (c) 2007 James Newton-King + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for vectorized base64 encoding / decoding +-------------------------------------------------------- + +Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2013-2017, Alfred Klomp +Copyright (c) 2015-2017, Wojciech Mula +Copyright (c) 2016-2017, Matthieu Darbois +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for RFC 3492 +--------------------------- + +The punycode implementation is based on the sample code in RFC 3492 + +Copyright (C) The Internet Society (2003). All Rights Reserved. + +This document and translations of it may be copied and furnished to +others, and derivative works that comment on or otherwise explain it +or assist in its implementation may be prepared, copied, published +and distributed, in whole or in part, without restriction of any +kind, provided that the above copyright notice and this paragraph are +included on all such copies and derivative works. However, this +document itself may not be modified in any way, such as by removing +the copyright notice or references to the Internet Society or other +Internet organizations, except as needed for the purpose of +developing Internet standards in which case the procedures for +copyrights defined in the Internet Standards process must be +followed, or as required to translate it into languages other than +English. + +The limited permissions granted above are perpetual and will not be +revoked by the Internet Society or its successors or assigns. + +This document and the information contained herein is provided on an +"AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING +TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING +BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION +HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF +MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +License notice for Algorithm from Internet Draft document "UUIDs and GUIDs" +--------------------------------------------------------------------------- + +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & +Digital Equipment Corporation, Maynard, Mass. +To anyone who acknowledges that this file is provided "AS IS" +without any express or implied warranty: permission to use, copy, +modify, and distribute this file for any purpose is hereby +granted without fee, provided that the above copyright notices and +this notice appears in all source code copies, and that none of +the names of Open Software Foundation, Inc., Hewlett-Packard +Company, or Digital Equipment Corporation be used in advertising +or publicity pertaining to distribution of the software without +specific, written prior permission. Neither Open Software +Foundation, Inc., Hewlett-Packard Company, Microsoft, nor Digital Equipment +Corporation makes any representations about the suitability of +this software for any purpose. + +Copyright(C) The Internet Society 1997. All Rights Reserved. + +This document and translations of it may be copied and furnished to others, +and derivative works that comment on or otherwise explain it or assist in +its implementation may be prepared, copied, published and distributed, in +whole or in part, without restriction of any kind, provided that the above +copyright notice and this paragraph are included on all such copies and +derivative works.However, this document itself may not be modified in any +way, such as by removing the copyright notice or references to the Internet +Society or other Internet organizations, except as needed for the purpose of +developing Internet standards in which case the procedures for copyrights +defined in the Internet Standards process must be followed, or as required +to translate it into languages other than English. + +The limited permissions granted above are perpetual and will not be revoked +by the Internet Society or its successors or assigns. + +This document and the information contained herein is provided on an "AS IS" +basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING TASK FORCE +DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY +RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A +PARTICULAR PURPOSE. + +License notice for Algorithm from RFC 4122 - +A Universally Unique IDentifier (UUID) URN Namespace +---------------------------------------------------- + +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & +Digital Equipment Corporation, Maynard, Mass. +Copyright (c) 1998 Microsoft. +To anyone who acknowledges that this file is provided "AS IS" +without any express or implied warranty: permission to use, copy, +modify, and distribute this file for any purpose is hereby +granted without fee, provided that the above copyright notices and +this notice appears in all source code copies, and that none of +the names of Open Software Foundation, Inc., Hewlett-Packard +Company, Microsoft, or Digital Equipment Corporation be used in +advertising or publicity pertaining to distribution of the software +without specific, written prior permission. Neither Open Software +Foundation, Inc., Hewlett-Packard Company, Microsoft, nor Digital +Equipment Corporation makes any representations about the +suitability of this software for any purpose." + +License notice for The LLVM Compiler Infrastructure +--------------------------------------------------- + +Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + * Neither the names of the LLVM Team, University of Illinois at + Urbana-Champaign, nor the names of its contributors may be used to + endorse or promote products derived from this Software without specific + prior written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. + +License notice for Bob Jenkins +------------------------------ + +By Bob Jenkins, 1996. bob_jenkins@burtleburtle.net. You may use this +code any way you wish, private, educational, or commercial. It's free. + +License notice for Greg Parker +------------------------------ + +Greg Parker gparker@cs.stanford.edu December 2000 +This code is in the public domain and may be copied or modified without +permission. + +License notice for libunwind based code +---------------------------------------- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for Printing Floating-Point Numbers (Dragon4) +------------------------------------------------------------ + +/****************************************************************************** + Copyright (c) 2014 Ryan Juckett + http://www.ryanjuckett.com/ + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +******************************************************************************/ + +License notice for Printing Floating-point Numbers (Grisu3) +----------------------------------------------------------- + +Copyright 2012 the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for xxHash +------------------------- + +xxHash Library +Copyright (c) 2012-2014, Yann Collet +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for Berkeley SoftFloat Release 3e +------------------------------------------------ + +https://github.com/ucb-bar/berkeley-softfloat-3 +https://github.com/ucb-bar/berkeley-softfloat-3/blob/master/COPYING.txt + +License for Berkeley SoftFloat Release 3e + +John R. Hauser +2018 January 20 + +The following applies to the whole of SoftFloat Release 3e as well as to +each source file individually. + +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the +University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions, and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS", AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for xoshiro RNGs +-------------------------------- + +Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org) + +To the extent possible under law, the author has dedicated all copyright +and related and neighboring rights to this software to the public domain +worldwide. This software is distributed without any warranty. + +See . + +License for fastmod (https://github.com/lemire/fastmod) +-------------------------------------- + + Copyright 2018 Daniel Lemire + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +License notice for The C++ REST SDK +----------------------------------- + +C++ REST SDK + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for MessagePack-CSharp +------------------------------------- + +MessagePack for C# + +MIT License + +Copyright (c) 2017 Yoshifumi Kawai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for lz4net +------------------------------------- + +lz4net + +Copyright (c) 2013-2017, Milosz Krajewski + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for Nerdbank.Streams +----------------------------------- + +The MIT License (MIT) + +Copyright (c) Andrew Arnott + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for RapidJSON +---------------------------- + +Tencent is pleased to support the open source community by making RapidJSON available. + +Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved. + +Licensed under the MIT License (the "License"); you may not use this file except +in compliance with the License. You may obtain a copy of the License at + +http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +License notice for DirectX Math Library +--------------------------------------- + +https://github.com/microsoft/DirectXMath/blob/master/LICENSE + + The MIT License (MIT) + +Copyright (c) 2011-2020 Microsoft Corp + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for ldap4net +--------------------------- + +The MIT License (MIT) + +Copyright (c) 2018 Alexander Chermyanin + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for vectorized sorting code +------------------------------------------ + +MIT License + +Copyright (c) 2020 Dan Shechter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for musl +----------------------- + +musl as a whole is licensed under the following standard MIT license: + +Copyright © 2005-2020 Rich Felker, et al. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for "Faster Unsigned Division by Constants" +---------------------------------------------------------- + +Reference implementations of computing and using the "magic number" approach to dividing +by constants, including codegen instructions. The unsigned division incorporates the +"round down" optimization per ridiculous_fish. + +This is free and unencumbered software. Any copyright is dedicated to the Public Domain. + +License notice for mimalloc +--------------------------- + +MIT License + +Copyright (c) 2019 Microsoft Corporation, Daan Leijen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for Apple header files +------------------------------------- + +Copyright (c) 1980, 1986, 1993 + The Regents of the University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by the University of + California, Berkeley and its contributors. +4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +License notice for Angular v8.0 +-------------------------------- + +The MIT License (MIT) +===================== + +Copyright (c) 2010-2019 Google LLC. http://angular.io/license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +License notice for BedrockFramework +=================================== + +MIT License + +Copyright (c) 2019 David Fowler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for Swashbuckle +=================================== + +The MIT License (MIT) + +Copyright (c) 2016 Richard Morris + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for cli-spinners +============================================= + +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for dotnet-deb-tool +------------------------------------ + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for IIS-Common +------------------------------------ + +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE + +License notice for IIS-Setup +------------------------------------ + +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE + +License notice for LZMA SDK +--------------------------- + +http://7-zip.org/sdk.html + +LZMA SDK is placed in the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute the +original LZMA SDK code, either in source code form or as a compiled binary, +for any purpose, commercial or non-commercial, and by any means. + +License notice for MonoDevelop +------------------------------ + +Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Copyright (c) 2011 Novell, Inc (http://www.novell.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +License notice for Nuget.Client +------------------------------- + +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +License notice for Ookie.Dialogs +-------------------------------- + +http://www.ookii.org/software/dialogs/ + +Copyright © Sven Groot (Ookii.org) 2009 +All rights reserved. + + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1) Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2) Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3) Neither the name of the ORGANIZATION nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +License notice for viz.js +------------------------------------ + +Copyright (c) 2014-2018 Michael Daines + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for West Wind Live Reload ASP.NET Core Middleware +============================================= + + + +--- + +lz4net + +Copyright (c) 2013-2017, Milosz Krajewski + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +MIT License +----------- + +Copyright (c) 2019-2020 West Wind Technologies + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License for fastmod (https://github.com/lemire/fastmod) and ibm-fpgen (https://github.com/nigeltao/parse-number-fxx-test-data) +-------------------------------------- + + Copyright 2018 Daniel Lemire + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +License notice for JavaScript queues +------------------------------------- + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. + +Statement of Purpose +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: +the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; +moral rights retained by the original author(s) and/or performer(s); +publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; +rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; +rights protecting the extraction, dissemination, use and reuse of data in a Work; +database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and +other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. +2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. +3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. +4. Limitations and Disclaimers. +a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. +b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. +c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. +d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. + +License notice for MSBuild Locator +------------------------------------- + +https://github.com/Microsoft/MSBuildLocator + +Copyright (c) 2018 .NET Foundation and Contributors + +This software is licensed subject to the MIT license, available at +https://opensource.org/licenses/MIT + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for Newtonsoft.Json +=================================== + +The MIT License (MIT) + +Copyright (c) 2007 James Newton-King + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for NuGet.Client +------------------------------- + +In reference to: https://github.com/dotnet/templating/blob/main/build/nuget.exe + +https://github.com/NuGet/NuGet.Client/blob/dev/LICENSE.txt + +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +License notice for Roslyn Clr Heap Allocation Analyzer +------------------------------------- + +https://github.com/Microsoft/RoslynClrHeapAllocationAnalyzer + +Copyright (c) 2018 Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +License notice for StyleCop Analyzers +------------------------------------- + +The MIT License (MIT) + +Copyright (c) Tunnel Vision Laboratories, LLC + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for corefx +------------------------------------- + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +`Source/Native/Runtime/Mac/shared/Microsoft.NETCore.App/6.0.1/Microsoft.NETCore.App.deps.json`: + +```json +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0/osx-x64", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": {}, + ".NETCoreApp,Version=v6.0/osx-x64": { + "Microsoft.NETCore.App.Runtime.osx-x64/6.0.1": { + "runtime": { + "System.Private.CoreLib.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.VisualBasic.dll": { + "assemblyVersion": "10.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "mscorlib.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "netstandard.dll": { + "assemblyVersion": "2.1.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.AppContext.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Buffers.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.DataAnnotations.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Configuration.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Core.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Data.DataSetExtensions.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Data.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Contracts.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Debug.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Tools.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Tracing.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Drawing.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Dynamic.Runtime.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Globalization.Calendars.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Globalization.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Globalization.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Compression.FileSystem.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.UnmanagedMemoryStream.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Numerics.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Numerics.Vectors.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Emit.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Emit.ILGeneration.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Emit.Lightweight.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Resources.Reader.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Resources.ResourceManager.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Handles.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Intrinsics.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Loader.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.Json.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.Xml.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Principal.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.SecureString.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ServiceModel.Web.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ServiceProcess.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Encoding.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Encoding.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Overlapped.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Tasks.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Tasks.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Thread.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.ThreadPool.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Timer.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Transactions.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ValueTuple.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Web.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Windows.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.Linq.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.ReaderWriter.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.Serialization.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XDocument.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XmlDocument.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XmlSerializer.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XPath.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "WindowsBase.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.CSharp.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.VisualBasic.Core.dll": { + "assemblyVersion": "11.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.Win32.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.Win32.Registry.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.Concurrent.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.Immutable.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.NonGeneric.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.Specialized.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.Annotations.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.EventBasedAsync.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.TypeConverter.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Console.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Data.Common.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.DiagnosticSource.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.FileVersionInfo.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Process.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.StackTrace.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.TextWriterTraceListener.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.TraceSource.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Drawing.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Formats.Asn1.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Compression.Brotli.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Compression.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Compression.ZipFile.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.AccessControl.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.DriveInfo.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.Watcher.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.IsolatedStorage.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.MemoryMappedFiles.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Pipes.AccessControl.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Pipes.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Linq.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Linq.Expressions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Linq.Parallel.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Linq.Queryable.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Memory.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Http.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Http.Json.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.HttpListener.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Mail.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.NameResolution.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.NetworkInformation.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Ping.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Quic.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Requests.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Security.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.ServicePoint.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Sockets.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebClient.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebHeaderCollection.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebProxy.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebSockets.Client.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebSockets.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ObjectModel.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Private.DataContractSerialization.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Private.Uri.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Private.Xml.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Private.Xml.Linq.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.DispatchProxy.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Metadata.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.TypeExtensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Resources.Writer.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.CompilerServices.Unsafe.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.CompilerServices.VisualC.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.InteropServices.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.InteropServices.RuntimeInformation.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Numerics.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.Formatters.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.AccessControl.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Claims.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Algorithms.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Cng.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Csp.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Encoding.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.OpenSsl.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.X509Certificates.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Principal.Windows.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Encoding.CodePages.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Encodings.Web.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Json.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.RegularExpressions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Channels.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Tasks.Dataflow.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Tasks.Parallel.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Transactions.Local.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Web.HttpUtility.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XPath.XDocument.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + } + }, + "native": { + "createdump": { + "fileVersion": "0.0.0.0" + }, + "libclrjit.dylib": { + "fileVersion": "0.0.0.0" + }, + "libcoreclr.dylib": { + "fileVersion": "0.0.0.0" + }, + "libdbgshim.dylib": { + "fileVersion": "0.0.0.0" + }, + "libmscordaccore.dylib": { + "fileVersion": "0.0.0.0" + }, + "libmscordbi.dylib": { + "fileVersion": "0.0.0.0" + }, + "libSystem.Globalization.Native.dylib": { + "fileVersion": "0.0.0.0" + }, + "libSystem.IO.Compression.Native.dylib": { + "fileVersion": "0.0.0.0" + }, + "libSystem.Native.dylib": { + "fileVersion": "0.0.0.0" + }, + "libSystem.Net.Security.Native.dylib": { + "fileVersion": "0.0.0.0" + }, + "libSystem.Security.Cryptography.Native.Apple.dylib": { + "fileVersion": "0.0.0.0" + }, + "libSystem.Security.Cryptography.Native.OpenSsl.dylib": { + "fileVersion": "0.0.0.0" + }, + "libhostpolicy.dylib": { + "fileVersion": "0.0.0.0" + } + } + } + } + }, + "libraries": { + "Microsoft.NETCore.App.Runtime.osx-x64/6.0.1": { + "type": "package", + "serviceable": true, + "sha512": "", + "path": "microsoft.netcore.app.runtime.osx-x64/6.0.1" + } + }, + "runtimes": { + "osx-x64": [ + "osx", + "unix-x64", + "unix", + "any", + "base" + ], + "osx.10.10-x64": [ + "osx.10.10", + "osx-x64", + "osx", + "unix-x64", + "unix", + "any", + "base" + ], + "osx.10.11-x64": [ + "osx.10.11", + "osx.10.10-x64", + "osx.10.10", + "osx-x64", + "osx", + "unix-x64", + "unix", + "any", + "base" + ], + "osx.10.12-x64": [ + "osx.10.12", + "osx.10.11-x64", + "osx.10.11", + "osx.10.10-x64", + "osx.10.10", + "osx-x64", + "osx", + "unix-x64", + "unix", + "any", + "base" + ], + "osx.10.13-x64": [ + "osx.10.13", + "osx.10.12-x64", + "osx.10.12", + "osx.10.11-x64", + "osx.10.11", + "osx.10.10-x64", + "osx.10.10", + "osx-x64", + "osx", + "unix-x64", + "unix", + "any", + "base" + ], + "osx.10.14-x64": [ + "osx.10.14", + "osx.10.13-x64", + "osx.10.13", + "osx.10.12-x64", + "osx.10.12", + "osx.10.11-x64", + "osx.10.11", + "osx.10.10-x64", + "osx.10.10", + "osx-x64", + "osx", + "unix-x64", + "unix", + "any", + "base" + ], + "osx.10.15-x64": [ + "osx.10.15", + "osx.10.14-x64", + "osx.10.14", + "osx.10.13-x64", + "osx.10.13", + "osx.10.12-x64", + "osx.10.12", + "osx.10.11-x64", + "osx.10.11", + "osx.10.10-x64", + "osx.10.10", + "osx-x64", + "osx", + "unix-x64", + "unix", + "any", + "base" + ], + "osx.10.16-x64": [ + "osx.10.16", + "osx.10.15-x64", + "osx.10.15", + "osx.10.14-x64", + "osx.10.14", + "osx.10.13-x64", + "osx.10.13", + "osx.10.12-x64", + "osx.10.12", + "osx.10.11-x64", + "osx.10.11", + "osx.10.10-x64", + "osx.10.10", + "osx-x64", + "osx", + "unix-x64", + "unix", + "any", + "base" + ], + "osx.11.0-x64": [ + "osx.11.0", + "osx.10.16-x64", + "osx.10.16", + "osx.10.15-x64", + "osx.10.15", + "osx.10.14-x64", + "osx.10.14", + "osx.10.13-x64", + "osx.10.13", + "osx.10.12-x64", + "osx.10.12", + "osx.10.11-x64", + "osx.10.11", + "osx.10.10-x64", + "osx.10.10", + "osx-x64", + "osx", + "unix-x64", + "unix", + "any", + "base" + ], + "osx.12-x64": [ + "osx.12", + "osx.11.0-x64", + "osx.11.0", + "osx.10.16-x64", + "osx.10.16", + "osx.10.15-x64", + "osx.10.15", + "osx.10.14-x64", + "osx.10.14", + "osx.10.13-x64", + "osx.10.13", + "osx.10.12-x64", + "osx.10.12", + "osx.10.11-x64", + "osx.10.11", + "osx.10.10-x64", + "osx.10.10", + "osx-x64", + "osx", + "unix-x64", + "unix", + "any", + "base" + ] + } +} +``` + +`Source/Native/Runtime/Mac/shared/Microsoft.NETCore.App/6.0.1/Microsoft.NETCore.App.runtimeconfig.json`: + +```json +{ + "runtimeOptions": { + "tfm": "net6.0", + "configProperties": { + "System.Reflection.Metadata.MetadataUpdater.IsSupported": false + } + } +} +``` + +`Source/Native/Runtime/Win64/LICENSE.txt`: + +```txt +MICROSOFT SOFTWARE LICENSE TERMS +MICROSOFT .NET LIBRARY +These license terms are an agreement between Microsoft Corporation (or based on where you live, one of its affiliates) and you. Please read them. They apply to the software named above, which includes the media on which you received it, if any. The terms also apply to any Microsoft + * updates, + * supplements, + * Internet-based services, and + * support services +for this software, unless other terms accompany those items. If so, those terms apply. +BY USING THE SOFTWARE, YOU ACCEPT THESE TERMS. IF YOU DO NOT ACCEPT THEM, DO NOT USE THE SOFTWARE. +IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE PERPETUAL RIGHTS BELOW. +1. INSTALLATION AND USE RIGHTS. + a. Installation and Use. You may install and use any number of copies of the software to design, develop and test your programs. + b. Third Party Programs. The software may include third party programs that Microsoft, not the third party, licenses to you under this agreement. Notices, if any, for the third party program are included for your information only. +2. DATA. The software may collect information about you and your use of the software, and send that to Microsoft. Microsoft may use this information to improve our products and services. You can learn more about data collection and use in the help documentation and the privacy statement at https://go.microsoft.com/fwlink/?LinkId=528096. Your use of the software operates as your consent to these practices. +3. ADDITIONAL LICENSING REQUIREMENTS AND/OR USE RIGHTS. + a. DISTRIBUTABLE CODE. The software is comprised of Distributable Code. "Distributable Code" is code that you are permitted to distribute in programs you develop if you comply with the terms below. + i. Right to Use and Distribute. + * You may copy and distribute the object code form of the software. + * Third Party Distribution. You may permit distributors of your programs to copy and distribute the Distributable Code as part of those programs. + ii. Distribution Requirements. For any Distributable Code you distribute, you must + * add significant primary functionality to it in your programs; + * require distributors and external end users to agree to terms that protect it at least as much as this agreement; + * display your valid copyright notice on your programs; and + * indemnify, defend, and hold harmless Microsoft from any claims, including attorneys' fees, related to the distribution or use of your programs. + iii. Distribution Restrictions. You may not + * alter any copyright, trademark or patent notice in the Distributable Code; + * use Microsoft's trademarks in your programs' names or in a way that suggests your programs come from or are endorsed by Microsoft; + * include Distributable Code in malicious, deceptive or unlawful programs; or + * modify or distribute the source code of any Distributable Code so that any part of it becomes subject to an Excluded License. An Excluded License is one that requires, as a condition of use, modification or distribution, that + * the code be disclosed or distributed in source code form; or + * others have the right to modify it. +4. SCOPE OF LICENSE. The software is licensed, not sold. This agreement only gives you some rights to use the software. Microsoft reserves all other rights. Unless applicable law gives you more rights despite this limitation, you may use the software only as expressly permitted in this agreement. In doing so, you must comply with any technical limitations in the software that only allow you to use it in certain ways. You may not + * work around any technical limitations in the software; + * reverse engineer, decompile or disassemble the software, except and only to the extent that applicable law expressly permits, despite this limitation; + * publish the software for others to copy; + * rent, lease or lend the software; + * transfer the software or this agreement to any third party; or + * use the software for commercial software hosting services. +5. BACKUP COPY. You may make one backup copy of the software. You may use it only to reinstall the software. +6. DOCUMENTATION. Any person that has valid access to your computer or internal network may copy and use the documentation for your internal, reference purposes. +7. EXPORT RESTRICTIONS. The software is subject to United States export laws and regulations. You must comply with all domestic and international export laws and regulations that apply to the software. These laws include restrictions on destinations, end users and end use. For additional information, see www.microsoft.com/exporting. +8. SUPPORT SERVICES. Because this software is "as is," we may not provide support services for it. +9. ENTIRE AGREEMENT. This agreement, and the terms for supplements, updates, Internet-based services and support services that you use, are the entire agreement for the software and support services. +10. APPLICABLE LAW. + a. United States. If you acquired the software in the United States, Washington state law governs the interpretation of this agreement and applies to claims for breach of it, regardless of conflict of laws principles. The laws of the state where you live govern all other claims, including claims under state consumer protection laws, unfair competition laws, and in tort. + b. Outside the United States. If you acquired the software in any other country, the laws of that country apply. +11. LEGAL EFFECT. This agreement describes certain legal rights. You may have other rights under the laws of your country. You may also have rights with respect to the party from whom you acquired the software. This agreement does not change your rights under the laws of your country if the laws of your country do not permit it to do so. +12. DISCLAIMER OF WARRANTY. THE SOFTWARE IS LICENSED "AS-IS." YOU BEAR THE RISK OF USING IT. MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTEES OR CONDITIONS. YOU MAY HAVE ADDITIONAL CONSUMER RIGHTS OR STATUTORY GUARANTEES UNDER YOUR LOCAL LAWS WHICH THIS AGREEMENT CANNOT CHANGE. TO THE EXTENT PERMITTED UNDER YOUR LOCAL LAWS, MICROSOFT EXCLUDES THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. +FOR AUSTRALIA - YOU HAVE STATUTORY GUARANTEES UNDER THE AUSTRALIAN CONSUMER LAW AND NOTHING IN THESE TERMS IS INTENDED TO AFFECT THOSE RIGHTS. +13. LIMITATION ON AND EXCLUSION OF REMEDIES AND DAMAGES. YOU CAN RECOVER FROM MICROSOFT AND ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S. $5.00. YOU CANNOT RECOVER ANY OTHER DAMAGES, INCLUDING CONSEQUENTIAL, LOST PROFITS, SPECIAL, INDIRECT OR INCIDENTAL DAMAGES. +This limitation applies to + * anything related to the software, services, content (including code) on third party Internet sites, or third party programs; and + * claims for breach of contract, breach of warranty, guarantee or condition, strict liability, negligence, or other tort to the extent permitted by applicable law. +It also applies even if Microsoft knew or should have known about the possibility of the damages. The above limitation or exclusion may not apply to you because your country may not allow the exclusion or limitation of incidental, consequential or other damages. +Please note: As this software is distributed in Quebec, Canada, some of the clauses in this agreement are provided below in French. +Remarque : Ce logiciel étant distribué au Québec, Canada, certaines des clauses dans ce contrat sont fournies ci-dessous en français. +EXONÉRATION DE GARANTIE. Le logiciel visé par une licence est offert « tel quel ». Toute utilisation de ce logiciel est à votre seule risque et péril. Microsoft n'accorde aucune autre garantie expresse. Vous pouvez bénéficier de droits additionnels en vertu du droit local sur la protection des consommateurs, que ce contrat ne peut modifier. La ou elles sont permises par le droit locale, les garanties implicites de qualité marchande, d'adéquation à un usage particulier et d'absence de contrefaçon sont exclues. +LIMITATION DES DOMMAGES-INTÉRÊTS ET EXCLUSION DE RESPONSABILITÉ POUR LES DOMMAGES. Vous pouvez obtenir de Microsoft et de ses fournisseurs une indemnisation en cas de dommages directs uniquement à hauteur de 5,00 $ US. Vous ne pouvez prétendre à aucune indemnisation pour les autres dommages, y compris les dommages spéciaux, indirects ou accessoires et pertes de bénéfices. +Cette limitation concerne : + * tout ce qui est relié au logiciel, aux services ou au contenu (y compris le code) figurant sur des sites Internet tiers ou dans des programmes tiers ; et + * les réclamations au titre de violation de contrat ou de garantie, ou au titre de responsabilité stricte, de négligence ou d'une autre faute dans la limite autorisée par la loi en vigueur. +Elle s'applique également, même si Microsoft connaissait ou devrait connaître l'éventualité d'un tel dommage. Si votre pays n'autorise pas l'exclusion ou la limitation de responsabilité pour les dommages indirects, accessoires ou de quelque nature que ce soit, il se peut que la limitation ou l'exclusion ci-dessus ne s'appliquera pas à votre égard. +EFFET JURIDIQUE. Le présent contrat décrit certains droits juridiques. Vous pourriez avoir d'autres droits prévus par les lois de votre pays. Le présent contrat ne modifie pas les droits que vous confèrent les lois de votre pays si celles-ci ne le permettent pas. + + +``` + +`Source/Native/Runtime/Win64/ThirdPartyNotices.txt`: + +```txt +.NET Runtime uses third-party libraries or other resources that may be +distributed under licenses different than the .NET Runtime software. + +In the event that we accidentally failed to list a required notice, please +bring it to our attention. Post an issue or email us: + + dotnet@microsoft.com + +The attached notices are provided for information only. + +License notice for ASP.NET +------------------------------- + +Copyright (c) .NET Foundation. All rights reserved. +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/dotnet/aspnetcore/blob/main/LICENSE.txt + +License notice for Slicing-by-8 +------------------------------- + +http://sourceforge.net/projects/slicing-by-8/ + +Copyright (c) 2004-2006 Intel Corporation - All Rights Reserved + + +This software program is licensed subject to the BSD License, available at +http://www.opensource.org/licenses/bsd-license.html. + +License notice for Unicode data +------------------------------- + +https://www.unicode.org/license.html + +Copyright © 1991-2020 Unicode, Inc. All rights reserved. +Distributed under the Terms of Use in https://www.unicode.org/copyright.html. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation +(the "Data Files") or Unicode software and any associated documentation +(the "Software") to deal in the Data Files or Software +without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, and/or sell copies of +the Data Files or Software, and to permit persons to whom the Data Files +or Software are furnished to do so, provided that either +(a) this copyright and permission notice appear with all copies +of the Data Files or Software, or +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS +NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, +use or other dealings in these Data Files or Software without prior +written authorization of the copyright holder. + +License notice for Zlib +----------------------- + +https://github.com/madler/zlib +http://zlib.net/zlib_license.html + +/* zlib.h -- interface of the 'zlib' general purpose compression library + version 1.2.11, January 15th, 2017 + + Copyright (C) 1995-2017 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +*/ + +License notice for Mono +------------------------------- + +http://www.mono-project.com/docs/about-mono/ + +Copyright (c) .NET Foundation Contributors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the Software), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for International Organization for Standardization +----------------------------------------------------------------- + +Portions (C) International Organization for Standardization 1986: + Permission to copy in any form is granted for use with + conforming SGML systems and applications as defined in + ISO 8879, provided this notice is included in all copies. + +License notice for Intel +------------------------ + +"Copyright (c) 2004-2006 Intel Corporation - All Rights Reserved + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for Xamarin and Novell +------------------------------------- + +Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Copyright (c) 2011 Novell, Inc (http://www.novell.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Third party notice for W3C +-------------------------- + +"W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE +Status: This license takes effect 13 May, 2015. +This work is being provided by the copyright holders under the following license. +License +By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions. +Permission to copy, modify, and distribute this work, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the work or portions thereof, including modifications: +The full text of this NOTICE in a location viewable to users of the redistributed or derivative work. +Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none exist, the W3C Software and Document Short Notice should be included. +Notice of any changes or modifications, through a copyright statement on the new code or document such as "This software or document includes material copied from or derived from [title and URI of the W3C document]. Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." +Disclaimers +THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. +COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. +The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the work without specific, written prior permission. Title to copyright in this work will at all times remain with copyright holders." + +License notice for Bit Twiddling Hacks +-------------------------------------- + +Bit Twiddling Hacks + +By Sean Eron Anderson +seander@cs.stanford.edu + +Individually, the code snippets here are in the public domain (unless otherwise +noted) — feel free to use them however you please. The aggregate collection and +descriptions are © 1997-2005 Sean Eron Anderson. The code and descriptions are +distributed in the hope that they will be useful, but WITHOUT ANY WARRANTY and +without even the implied warranty of merchantability or fitness for a particular +purpose. + +License notice for Brotli +-------------------------------------- + +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +compress_fragment.c: +Copyright (c) 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +""AS IS"" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +decode_fuzzer.c: +Copyright (c) 2015 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +""AS IS"" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + +License notice for Json.NET +------------------------------- + +https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md + +The MIT License (MIT) + +Copyright (c) 2007 James Newton-King + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for vectorized base64 encoding / decoding +-------------------------------------------------------- + +Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2013-2017, Alfred Klomp +Copyright (c) 2015-2017, Wojciech Mula +Copyright (c) 2016-2017, Matthieu Darbois +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for RFC 3492 +--------------------------- + +The punycode implementation is based on the sample code in RFC 3492 + +Copyright (C) The Internet Society (2003). All Rights Reserved. + +This document and translations of it may be copied and furnished to +others, and derivative works that comment on or otherwise explain it +or assist in its implementation may be prepared, copied, published +and distributed, in whole or in part, without restriction of any +kind, provided that the above copyright notice and this paragraph are +included on all such copies and derivative works. However, this +document itself may not be modified in any way, such as by removing +the copyright notice or references to the Internet Society or other +Internet organizations, except as needed for the purpose of +developing Internet standards in which case the procedures for +copyrights defined in the Internet Standards process must be +followed, or as required to translate it into languages other than +English. + +The limited permissions granted above are perpetual and will not be +revoked by the Internet Society or its successors or assigns. + +This document and the information contained herein is provided on an +"AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING +TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING +BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION +HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF +MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +License notice for Algorithm from Internet Draft document "UUIDs and GUIDs" +--------------------------------------------------------------------------- + +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & +Digital Equipment Corporation, Maynard, Mass. +To anyone who acknowledges that this file is provided "AS IS" +without any express or implied warranty: permission to use, copy, +modify, and distribute this file for any purpose is hereby +granted without fee, provided that the above copyright notices and +this notice appears in all source code copies, and that none of +the names of Open Software Foundation, Inc., Hewlett-Packard +Company, or Digital Equipment Corporation be used in advertising +or publicity pertaining to distribution of the software without +specific, written prior permission. Neither Open Software +Foundation, Inc., Hewlett-Packard Company, Microsoft, nor Digital Equipment +Corporation makes any representations about the suitability of +this software for any purpose. + +Copyright(C) The Internet Society 1997. All Rights Reserved. + +This document and translations of it may be copied and furnished to others, +and derivative works that comment on or otherwise explain it or assist in +its implementation may be prepared, copied, published and distributed, in +whole or in part, without restriction of any kind, provided that the above +copyright notice and this paragraph are included on all such copies and +derivative works.However, this document itself may not be modified in any +way, such as by removing the copyright notice or references to the Internet +Society or other Internet organizations, except as needed for the purpose of +developing Internet standards in which case the procedures for copyrights +defined in the Internet Standards process must be followed, or as required +to translate it into languages other than English. + +The limited permissions granted above are perpetual and will not be revoked +by the Internet Society or its successors or assigns. + +This document and the information contained herein is provided on an "AS IS" +basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING TASK FORCE +DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY +RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A +PARTICULAR PURPOSE. + +License notice for Algorithm from RFC 4122 - +A Universally Unique IDentifier (UUID) URN Namespace +---------------------------------------------------- + +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & +Digital Equipment Corporation, Maynard, Mass. +Copyright (c) 1998 Microsoft. +To anyone who acknowledges that this file is provided "AS IS" +without any express or implied warranty: permission to use, copy, +modify, and distribute this file for any purpose is hereby +granted without fee, provided that the above copyright notices and +this notice appears in all source code copies, and that none of +the names of Open Software Foundation, Inc., Hewlett-Packard +Company, Microsoft, or Digital Equipment Corporation be used in +advertising or publicity pertaining to distribution of the software +without specific, written prior permission. Neither Open Software +Foundation, Inc., Hewlett-Packard Company, Microsoft, nor Digital +Equipment Corporation makes any representations about the +suitability of this software for any purpose." + +License notice for The LLVM Compiler Infrastructure +--------------------------------------------------- + +Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + * Neither the names of the LLVM Team, University of Illinois at + Urbana-Champaign, nor the names of its contributors may be used to + endorse or promote products derived from this Software without specific + prior written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. + +License notice for Bob Jenkins +------------------------------ + +By Bob Jenkins, 1996. bob_jenkins@burtleburtle.net. You may use this +code any way you wish, private, educational, or commercial. It's free. + +License notice for Greg Parker +------------------------------ + +Greg Parker gparker@cs.stanford.edu December 2000 +This code is in the public domain and may be copied or modified without +permission. + +License notice for libunwind based code +---------------------------------------- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for Printing Floating-Point Numbers (Dragon4) +------------------------------------------------------------ + +/****************************************************************************** + Copyright (c) 2014 Ryan Juckett + http://www.ryanjuckett.com/ + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +******************************************************************************/ + +License notice for Printing Floating-point Numbers (Grisu3) +----------------------------------------------------------- + +Copyright 2012 the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for xxHash +------------------------- + +xxHash Library +Copyright (c) 2012-2014, Yann Collet +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for Berkeley SoftFloat Release 3e +------------------------------------------------ + +https://github.com/ucb-bar/berkeley-softfloat-3 +https://github.com/ucb-bar/berkeley-softfloat-3/blob/master/COPYING.txt + +License for Berkeley SoftFloat Release 3e + +John R. Hauser +2018 January 20 + +The following applies to the whole of SoftFloat Release 3e as well as to +each source file individually. + +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the +University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions, and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS", AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for xoshiro RNGs +-------------------------------- + +Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org) + +To the extent possible under law, the author has dedicated all copyright +and related and neighboring rights to this software to the public domain +worldwide. This software is distributed without any warranty. + +See . + +License for fastmod (https://github.com/lemire/fastmod) +-------------------------------------- + + Copyright 2018 Daniel Lemire + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +License notice for The C++ REST SDK +----------------------------------- + +C++ REST SDK + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for MessagePack-CSharp +------------------------------------- + +MessagePack for C# + +MIT License + +Copyright (c) 2017 Yoshifumi Kawai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for lz4net +------------------------------------- + +lz4net + +Copyright (c) 2013-2017, Milosz Krajewski + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for Nerdbank.Streams +----------------------------------- + +The MIT License (MIT) + +Copyright (c) Andrew Arnott + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for RapidJSON +---------------------------- + +Tencent is pleased to support the open source community by making RapidJSON available. + +Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved. + +Licensed under the MIT License (the "License"); you may not use this file except +in compliance with the License. You may obtain a copy of the License at + +http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +License notice for DirectX Math Library +--------------------------------------- + +https://github.com/microsoft/DirectXMath/blob/master/LICENSE + + The MIT License (MIT) + +Copyright (c) 2011-2020 Microsoft Corp + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for ldap4net +--------------------------- + +The MIT License (MIT) + +Copyright (c) 2018 Alexander Chermyanin + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for vectorized sorting code +------------------------------------------ + +MIT License + +Copyright (c) 2020 Dan Shechter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for musl +----------------------- + +musl as a whole is licensed under the following standard MIT license: + +Copyright © 2005-2020 Rich Felker, et al. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for "Faster Unsigned Division by Constants" +---------------------------------------------------------- + +Reference implementations of computing and using the "magic number" approach to dividing +by constants, including codegen instructions. The unsigned division incorporates the +"round down" optimization per ridiculous_fish. + +This is free and unencumbered software. Any copyright is dedicated to the Public Domain. + +License notice for mimalloc +--------------------------- + +MIT License + +Copyright (c) 2019 Microsoft Corporation, Daan Leijen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for Apple header files +------------------------------------- + +Copyright (c) 1980, 1986, 1993 + The Regents of the University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by the University of + California, Berkeley and its contributors. +4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +License notice for Angular v8.0 +-------------------------------- + +The MIT License (MIT) +===================== + +Copyright (c) 2010-2019 Google LLC. http://angular.io/license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +License notice for BedrockFramework +=================================== + +MIT License + +Copyright (c) 2019 David Fowler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for Swashbuckle +=================================== + +The MIT License (MIT) + +Copyright (c) 2016 Richard Morris + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for cli-spinners +============================================= + +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for dotnet-deb-tool +------------------------------------ + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for IIS-Common +------------------------------------ + +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE + +License notice for IIS-Setup +------------------------------------ + +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE + +License notice for LZMA SDK +--------------------------- + +http://7-zip.org/sdk.html + +LZMA SDK is placed in the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute the +original LZMA SDK code, either in source code form or as a compiled binary, +for any purpose, commercial or non-commercial, and by any means. + +License notice for MonoDevelop +------------------------------ + +Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Copyright (c) 2011 Novell, Inc (http://www.novell.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +License notice for Nuget.Client +------------------------------- + +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +License notice for Ookie.Dialogs +-------------------------------- + +http://www.ookii.org/software/dialogs/ + +Copyright © Sven Groot (Ookii.org) 2009 +All rights reserved. + + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1) Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2) Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3) Neither the name of the ORGANIZATION nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +License notice for viz.js +------------------------------------ + +Copyright (c) 2014-2018 Michael Daines + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for West Wind Live Reload ASP.NET Core Middleware +============================================= + + + +--- + +lz4net + +Copyright (c) 2013-2017, Milosz Krajewski + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +MIT License +----------- + +Copyright (c) 2019-2020 West Wind Technologies + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License for fastmod (https://github.com/lemire/fastmod) and ibm-fpgen (https://github.com/nigeltao/parse-number-fxx-test-data) +-------------------------------------- + + Copyright 2018 Daniel Lemire + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +License notice for JavaScript queues +------------------------------------- + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. + +Statement of Purpose +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: +the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; +moral rights retained by the original author(s) and/or performer(s); +publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; +rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; +rights protecting the extraction, dissemination, use and reuse of data in a Work; +database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and +other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. +2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. +3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. +4. Limitations and Disclaimers. +a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. +b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. +c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. +d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. + +License notice for MSBuild Locator +------------------------------------- + +https://github.com/Microsoft/MSBuildLocator + +Copyright (c) 2018 .NET Foundation and Contributors + +This software is licensed subject to the MIT license, available at +https://opensource.org/licenses/MIT + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for Newtonsoft.Json +=================================== + +The MIT License (MIT) + +Copyright (c) 2007 James Newton-King + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for NuGet.Client +------------------------------- + +In reference to: https://github.com/dotnet/templating/blob/main/build/nuget.exe + +https://github.com/NuGet/NuGet.Client/blob/dev/LICENSE.txt + +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +License notice for Roslyn Clr Heap Allocation Analyzer +------------------------------------- + +https://github.com/Microsoft/RoslynClrHeapAllocationAnalyzer + +Copyright (c) 2018 Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +License notice for StyleCop Analyzers +------------------------------------- + +The MIT License (MIT) + +Copyright (c) Tunnel Vision Laboratories, LLC + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for corefx +------------------------------------- + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +`Source/Native/Runtime/Win64/shared/Microsoft.NETCore.App/6.0.1/Microsoft.NETCore.App.deps.json`: + +```json +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0/win-x64", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": {}, + ".NETCoreApp,Version=v6.0/win-x64": { + "Microsoft.NETCore.App.Runtime.win-x64/6.0.1": { + "runtime": { + "System.Private.CoreLib.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.VisualBasic.dll": { + "assemblyVersion": "10.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "mscorlib.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "netstandard.dll": { + "assemblyVersion": "2.1.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.AppContext.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Buffers.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.DataAnnotations.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Configuration.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Core.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Data.DataSetExtensions.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Data.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Contracts.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Debug.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Tools.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Tracing.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Drawing.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Dynamic.Runtime.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Globalization.Calendars.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Globalization.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Globalization.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Compression.FileSystem.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Pipes.AccessControl.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.UnmanagedMemoryStream.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Numerics.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Numerics.Vectors.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Emit.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Emit.ILGeneration.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Emit.Lightweight.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Resources.Reader.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Resources.ResourceManager.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Handles.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Intrinsics.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Loader.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.Json.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.Xml.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Principal.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.SecureString.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ServiceModel.Web.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ServiceProcess.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Encoding.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Encoding.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Overlapped.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Tasks.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Tasks.Extensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Thread.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.ThreadPool.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Timer.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Transactions.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ValueTuple.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Web.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Windows.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.Linq.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.ReaderWriter.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.Serialization.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XDocument.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XmlDocument.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XmlSerializer.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XPath.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "WindowsBase.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.CSharp.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.VisualBasic.Core.dll": { + "assemblyVersion": "11.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.Win32.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "Microsoft.Win32.Registry.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.Concurrent.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.Immutable.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.NonGeneric.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Collections.Specialized.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.Annotations.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.EventBasedAsync.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ComponentModel.TypeConverter.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Console.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Data.Common.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.DiagnosticSource.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.FileVersionInfo.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.Process.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.StackTrace.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.TextWriterTraceListener.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Diagnostics.TraceSource.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Drawing.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Formats.Asn1.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Compression.Brotli.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Compression.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Compression.ZipFile.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.AccessControl.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.DriveInfo.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.FileSystem.Watcher.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.IsolatedStorage.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.MemoryMappedFiles.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.IO.Pipes.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Linq.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Linq.Expressions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Linq.Parallel.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Linq.Queryable.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Memory.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Http.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Http.Json.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.HttpListener.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Mail.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.NameResolution.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.NetworkInformation.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Ping.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Quic.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Requests.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Security.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.ServicePoint.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.Sockets.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebClient.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebHeaderCollection.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebProxy.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebSockets.Client.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Net.WebSockets.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.ObjectModel.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Private.DataContractSerialization.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Private.Uri.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Private.Xml.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Private.Xml.Linq.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.DispatchProxy.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.Metadata.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Reflection.TypeExtensions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Resources.Writer.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.CompilerServices.Unsafe.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.CompilerServices.VisualC.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.InteropServices.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.InteropServices.RuntimeInformation.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Numerics.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.Formatters.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Runtime.Serialization.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.AccessControl.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Claims.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Algorithms.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Cng.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Csp.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Encoding.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.OpenSsl.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.Primitives.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Cryptography.X509Certificates.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Security.Principal.Windows.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Encoding.CodePages.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Encodings.Web.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.Json.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Text.RegularExpressions.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Channels.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Tasks.Dataflow.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Threading.Tasks.Parallel.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Transactions.Local.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Web.HttpUtility.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + }, + "System.Xml.XPath.XDocument.dll": { + "assemblyVersion": "6.0.0.0", + "fileVersion": "6.0.121.56705" + } + }, + "native": { + "clretwrc.dll": { + "fileVersion": "6.0.121.56705" + }, + "clrjit.dll": { + "fileVersion": "6.0.121.56705" + }, + "coreclr.dll": { + "fileVersion": "6.0.121.56705" + }, + "createdump.exe": { + "fileVersion": "6.0.121.56705" + }, + "dbgshim.dll": { + "fileVersion": "6.0.121.56705" + }, + "mscordaccore.dll": { + "fileVersion": "6.0.121.56705" + }, + "mscordaccore_amd64_amd64_6.0.121.56705.dll": { + "fileVersion": "6.0.121.56705" + }, + "mscordbi.dll": { + "fileVersion": "6.0.121.56705" + }, + "mscorrc.dll": { + "fileVersion": "6.0.121.56705" + }, + "api-ms-win-core-console-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-console-l1-2-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-datetime-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-debug-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-errorhandling-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-file-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-file-l1-2-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-file-l2-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-handle-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-heap-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-interlocked-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-libraryloader-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-localization-l1-2-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-memory-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-namedpipe-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-processenvironment-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-processthreads-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-processthreads-l1-1-1.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-profile-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-rtlsupport-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-string-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-synch-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-synch-l1-2-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-sysinfo-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-timezone-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-core-util-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-conio-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-convert-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-environment-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-filesystem-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-heap-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-locale-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-math-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-multibyte-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-private-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-process-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-runtime-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-stdio-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-string-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-time-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "api-ms-win-crt-utility-l1-1-0.dll": { + "fileVersion": "10.0.19041.685" + }, + "ucrtbase.dll": { + "fileVersion": "10.0.19041.685" + }, + "msquic.dll": { + "fileVersion": "1.7.0.0" + }, + "System.IO.Compression.Native.dll": { + "fileVersion": "42.42.42.42424" + }, + "hostpolicy.dll": { + "fileVersion": "6.0.121.56705" + }, + "Microsoft.DiaSymReader.Native.amd64.dll": { + "fileVersion": "14.28.29715.1" + } + } + } + } + }, + "libraries": { + "Microsoft.NETCore.App.Runtime.win-x64/6.0.1": { + "type": "package", + "serviceable": true, + "sha512": "", + "path": "microsoft.netcore.app.runtime.win-x64/6.0.1" + } + }, + "runtimes": { + "win-x64": [ + "win", + "any", + "base" + ], + "win-x64-aot": [ + "win-aot", + "win-x64", + "win", + "aot", + "any", + "base" + ], + "win10-x64": [ + "win10", + "win81-x64", + "win81", + "win8-x64", + "win8", + "win7-x64", + "win7", + "win-x64", + "win", + "any", + "base" + ], + "win10-x64-aot": [ + "win10-aot", + "win10-x64", + "win10", + "win81-x64-aot", + "win81-aot", + "win81-x64", + "win81", + "win8-x64-aot", + "win8-aot", + "win8-x64", + "win8", + "win7-x64-aot", + "win7-aot", + "win7-x64", + "win7", + "win-x64-aot", + "win-aot", + "win-x64", + "win", + "aot", + "any", + "base" + ], + "win7-x64": [ + "win7", + "win-x64", + "win", + "any", + "base" + ], + "win7-x64-aot": [ + "win7-aot", + "win7-x64", + "win7", + "win-x64-aot", + "win-aot", + "win-x64", + "win", + "aot", + "any", + "base" + ], + "win8-x64": [ + "win8", + "win7-x64", + "win7", + "win-x64", + "win", + "any", + "base" + ], + "win8-x64-aot": [ + "win8-aot", + "win8-x64", + "win8", + "win7-x64-aot", + "win7-aot", + "win7-x64", + "win7", + "win-x64-aot", + "win-aot", + "win-x64", + "win", + "aot", + "any", + "base" + ], + "win81-x64": [ + "win81", + "win8-x64", + "win8", + "win7-x64", + "win7", + "win-x64", + "win", + "any", + "base" + ], + "win81-x64-aot": [ + "win81-aot", + "win81-x64", + "win81", + "win8-x64-aot", + "win8-aot", + "win8-x64", + "win8", + "win7-x64-aot", + "win7-aot", + "win7-x64", + "win7", + "win-x64-aot", + "win-aot", + "win-x64", + "win", + "aot", + "any", + "base" + ] + } +} +``` + +`Source/Native/Runtime/Win64/shared/Microsoft.NETCore.App/6.0.1/Microsoft.NETCore.App.runtimeconfig.json`: + +```json +{ + "runtimeOptions": { + "tfm": "net6.0", + "configProperties": { + "System.Reflection.Metadata.MetadataUpdater.IsSupported": false + } + } +} +``` + +`Source/Native/Source/Dependencies/CoreCLR/includes/coreclr_delegates.h`: + +```h +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef __CORECLR_DELEGATES_H__ +#define __CORECLR_DELEGATES_H__ + +#include + +#if defined(_WIN32) + #define CORECLR_DELEGATE_CALLTYPE __stdcall + #ifdef _WCHAR_T_DEFINED + typedef wchar_t char_t; + #else + typedef unsigned short char_t; + #endif +#else + #define CORECLR_DELEGATE_CALLTYPE + typedef char char_t; +#endif + +#define UNMANAGEDCALLERSONLY_METHOD ((const char_t*)-1) + +// Signature of delegate returned by coreclr_delegate_type::load_assembly_and_get_function_pointer +typedef int (CORECLR_DELEGATE_CALLTYPE *load_assembly_and_get_function_pointer_fn)( + const char_t *assembly_path /* Fully qualified path to assembly */, + const char_t *type_name /* Assembly qualified type name */, + const char_t *method_name /* Public static method name compatible with delegateType */, + const char_t *delegate_type_name /* Assembly qualified delegate type name or null + or UNMANAGEDCALLERSONLY_METHOD if the method is marked with + the UnmanagedCallersOnlyAttribute. */, + void *reserved /* Extensibility parameter (currently unused and must be 0) */, + /*out*/ void **delegate /* Pointer where to store the function pointer result */); + +// Signature of delegate returned by load_assembly_and_get_function_pointer_fn when delegate_type_name == null (default) +typedef int (CORECLR_DELEGATE_CALLTYPE *component_entry_point_fn)(void *arg, int32_t arg_size_in_bytes); + +typedef int (CORECLR_DELEGATE_CALLTYPE *get_function_pointer_fn)( + const char_t *type_name /* Assembly qualified type name */, + const char_t *method_name /* Public static method name compatible with delegateType */, + const char_t *delegate_type_name /* Assembly qualified delegate type name or null, + or UNMANAGEDCALLERSONLY_METHOD if the method is marked with + the UnmanagedCallersOnlyAttribute. */, + void *load_context /* Extensibility parameter (currently unused and must be 0) */, + void *reserved /* Extensibility parameter (currently unused and must be 0) */, + /*out*/ void **delegate /* Pointer where to store the function pointer result */); + +#endif // __CORECLR_DELEGATES_H__ + +``` + +`Source/Native/Source/Dependencies/CoreCLR/includes/hostfxr.h`: + +```h +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef __HOSTFXR_H__ +#define __HOSTFXR_H__ + +#include +#include + +#if defined(_WIN32) + #define HOSTFXR_CALLTYPE __cdecl + #ifdef _WCHAR_T_DEFINED + typedef wchar_t char_t; + #else + typedef unsigned short char_t; + #endif +#else + #define HOSTFXR_CALLTYPE + typedef char char_t; +#endif + +enum hostfxr_delegate_type +{ + hdt_com_activation, + hdt_load_in_memory_assembly, + hdt_winrt_activation, + hdt_com_register, + hdt_com_unregister, + hdt_load_assembly_and_get_function_pointer, + hdt_get_function_pointer, +}; + +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_main_fn)(const int argc, const char_t **argv); +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_main_startupinfo_fn)( + const int argc, + const char_t **argv, + const char_t *host_path, + const char_t *dotnet_root, + const char_t *app_path); +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_bundle_startupinfo_fn)( + const int argc, + const char_t** argv, + const char_t* host_path, + const char_t* dotnet_root, + const char_t* app_path, + int64_t bundle_header_offset); + +typedef void(HOSTFXR_CALLTYPE *hostfxr_error_writer_fn)(const char_t *message); + +// +// Sets a callback which is to be used to write errors to. +// +// Parameters: +// error_writer +// A callback function which will be invoked every time an error is to be reported. +// Or nullptr to unregister previously registered callback and return to the default behavior. +// Return value: +// The previously registered callback (which is now unregistered), or nullptr if no previous callback +// was registered +// +// The error writer is registered per-thread, so the registration is thread-local. On each thread +// only one callback can be registered. Subsequent registrations overwrite the previous ones. +// +// By default no callback is registered in which case the errors are written to stderr. +// +// Each call to the error writer is sort of like writing a single line (the EOL character is omitted). +// Multiple calls to the error writer may occure for one failure. +// +// If the hostfxr invokes functions in hostpolicy as part of its operation, the error writer +// will be propagated to hostpolicy for the duration of the call. This means that errors from +// both hostfxr and hostpolicy will be reporter through the same error writer. +// +typedef hostfxr_error_writer_fn(HOSTFXR_CALLTYPE *hostfxr_set_error_writer_fn)(hostfxr_error_writer_fn error_writer); + +typedef void* hostfxr_handle; +struct hostfxr_initialize_parameters +{ + size_t size; + const char_t *host_path; + const char_t *dotnet_root; +}; + +// +// Initializes the hosting components for a dotnet command line running an application +// +// Parameters: +// argc +// Number of argv arguments +// argv +// Command-line arguments for running an application (as if through the dotnet executable). +// Only command-line arguments which are accepted by runtime installation are supported, SDK/CLI commands are not supported. +// For example 'app.dll app_argument_1 app_argument_2`. +// parameters +// Optional. Additional parameters for initialization +// host_context_handle +// On success, this will be populated with an opaque value representing the initialized host context +// +// Return value: +// Success - Hosting components were successfully initialized +// HostInvalidState - Hosting components are already initialized +// +// This function parses the specified command-line arguments to determine the application to run. It will +// then find the corresponding .runtimeconfig.json and .deps.json with which to resolve frameworks and +// dependencies and prepare everything needed to load the runtime. +// +// This function only supports arguments for running an application. It does not support SDK commands. +// +// This function does not load the runtime. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_initialize_for_dotnet_command_line_fn)( + int argc, + const char_t **argv, + const struct hostfxr_initialize_parameters *parameters, + /*out*/ hostfxr_handle *host_context_handle); + +// +// Initializes the hosting components using a .runtimeconfig.json file +// +// Parameters: +// runtime_config_path +// Path to the .runtimeconfig.json file +// parameters +// Optional. Additional parameters for initialization +// host_context_handle +// On success, this will be populated with an opaque value representing the initialized host context +// +// Return value: +// Success - Hosting components were successfully initialized +// Success_HostAlreadyInitialized - Config is compatible with already initialized hosting components +// Success_DifferentRuntimeProperties - Config has runtime properties that differ from already initialized hosting components +// CoreHostIncompatibleConfig - Config is incompatible with already initialized hosting components +// +// This function will process the .runtimeconfig.json to resolve frameworks and prepare everything needed +// to load the runtime. It will only process the .deps.json from frameworks (not any app/component that +// may be next to the .runtimeconfig.json). +// +// This function does not load the runtime. +// +// If called when the runtime has already been loaded, this function will check if the specified runtime +// config is compatible with the existing runtime. +// +// Both Success_HostAlreadyInitialized and Success_DifferentRuntimeProperties codes are considered successful +// initializations. In the case of Success_DifferentRuntimeProperties, it is left to the consumer to verify that +// the difference in properties is acceptable. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_initialize_for_runtime_config_fn)( + const char_t *runtime_config_path, + const struct hostfxr_initialize_parameters *parameters, + /*out*/ hostfxr_handle *host_context_handle); + +// +// Gets the runtime property value for an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// name +// Runtime property name +// value +// Out parameter. Pointer to a buffer with the property value. +// +// Return value: +// The error code result. +// +// The buffer pointed to by value is owned by the host context. The lifetime of the buffer is only +// guaranteed until any of the below occur: +// - a 'run' method is called for the host context +// - properties are changed via hostfxr_set_runtime_property_value +// - the host context is closed via 'hostfxr_close' +// +// If host_context_handle is nullptr and an active host context exists, this function will get the +// property value for the active host context. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_property_value_fn)( + const hostfxr_handle host_context_handle, + const char_t *name, + /*out*/ const char_t **value); + +// +// Sets the value of a runtime property for an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// name +// Runtime property name +// value +// Value to set +// +// Return value: +// The error code result. +// +// Setting properties is only supported for the first host context, before the runtime has been loaded. +// +// If the property already exists in the host context, it will be overwritten. If value is nullptr, the +// property will be removed. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_set_runtime_property_value_fn)( + const hostfxr_handle host_context_handle, + const char_t *name, + const char_t *value); + +// +// Gets all the runtime properties for an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// count +// [in] Size of the keys and values buffers +// [out] Number of properties returned (size of keys/values buffers used). If the input value is too +// small or keys/values is nullptr, this is populated with the number of available properties +// keys +// Array of pointers to buffers with runtime property keys +// values +// Array of pointers to buffers with runtime property values +// +// Return value: +// The error code result. +// +// The buffers pointed to by keys and values are owned by the host context. The lifetime of the buffers is only +// guaranteed until any of the below occur: +// - a 'run' method is called for the host context +// - properties are changed via hostfxr_set_runtime_property_value +// - the host context is closed via 'hostfxr_close' +// +// If host_context_handle is nullptr and an active host context exists, this function will get the +// properties for the active host context. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_properties_fn)( + const hostfxr_handle host_context_handle, + /*inout*/ size_t * count, + /*out*/ const char_t **keys, + /*out*/ const char_t **values); + +// +// Load CoreCLR and run the application for an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// +// Return value: +// If the app was successfully run, the exit code of the application. Otherwise, the error code result. +// +// The host_context_handle must have been initialized using hostfxr_initialize_for_dotnet_command_line. +// +// This function will not return until the managed application exits. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_run_app_fn)(const hostfxr_handle host_context_handle); + +// +// Gets a typed delegate from the currently loaded CoreCLR or from a newly created one. +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// type +// Type of runtime delegate requested +// delegate +// An out parameter that will be assigned the delegate. +// +// Return value: +// The error code result. +// +// If the host_context_handle was initialized using hostfxr_initialize_for_runtime_config, +// then all delegate types are supported. +// If the host_context_handle was initialized using hostfxr_initialize_for_dotnet_command_line, +// then only the following delegate types are currently supported: +// hdt_load_assembly_and_get_function_pointer +// hdt_get_function_pointer +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_delegate_fn)( + const hostfxr_handle host_context_handle, + enum hostfxr_delegate_type type, + /*out*/ void **delegate); + +// +// Closes an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// +// Return value: +// The error code result. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_close_fn)(const hostfxr_handle host_context_handle); + +struct hostfxr_dotnet_environment_sdk_info +{ + size_t size; + const char_t* version; + const char_t* path; +}; + +typedef void(HOSTFXR_CALLTYPE* hostfxr_get_dotnet_environment_info_result_fn)( + const struct hostfxr_dotnet_environment_info* info, + void* result_context); + +struct hostfxr_dotnet_environment_framework_info +{ + size_t size; + const char_t* name; + const char_t* version; + const char_t* path; +}; + +struct hostfxr_dotnet_environment_info +{ + size_t size; + + const char_t* hostfxr_version; + const char_t* hostfxr_commit_hash; + + size_t sdk_count; + const hostfxr_dotnet_environment_sdk_info* sdks; + + size_t framework_count; + const hostfxr_dotnet_environment_framework_info* frameworks; +}; + +#endif //__HOSTFXR_H__ + +``` + +`Source/Native/Source/UnrealCLR/Private/UnrealCLR.cpp`: + +```cpp +/* + * Unreal Engine .NET 6 integration + * Copyright (c) 2021 Stanislav Denisov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "UnrealCLR.h" + +#define LOCTEXT_NAMESPACE "UnrealCLR" + +DEFINE_LOG_CATEGORY(LogUnrealCLR); + +void UnrealCLR::Module::StartupModule() { + #define HOSTFXR_VERSION "6.0.1" + #define HOSTFXR_WINDOWS "hostfxr.dll" + #define HOSTFXR_MAC "libhostfxr.dylib" + #define HOSTFXR_LINUX "libhostfxr.so" + + #ifdef UNREALCLR_WINDOWS + #define HOSTFXR_PATH "Plugins/UnrealCLR/Runtime/Win64/host/fxr/" HOSTFXR_VERSION "/" HOSTFXR_WINDOWS + #define UNREALCLR_PLATFORM_STRING(string) string + #elif defined(UNREALCLR_MAC) + #define HOSTFXR_PATH "Plugins/UnrealCLR/Runtime/Mac/host/fxr/" HOSTFXR_VERSION "/" HOSTFXR_MAC + #define UNREALCLR_PLATFORM_STRING(string) TCHAR_TO_ANSI(string) + #elif defined(UNREALCLR_UNIX) + #define HOSTFXR_PATH "Plugins/UnrealCLR/Runtime/Linux/host/fxr/" HOSTFXR_VERSION "/" HOSTFXR_LINUX + #define UNREALCLR_PLATFORM_STRING(string) TCHAR_TO_ANSI(string) + #else + #error "Unknown platform" + #endif + + UnrealCLR::Status = UnrealCLR::StatusType::Stopped; + UnrealCLR::ProjectPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir()); + UnrealCLR::UserAssembliesPath = UnrealCLR::ProjectPath + TEXT("Managed/"); + + OnWorldPostInitializationHandle = FWorldDelegates::OnPostWorldInitialization.AddRaw(this, &UnrealCLR::Module::OnWorldPostInitialization); + OnWorldCleanupHandle = FWorldDelegates::OnWorldCleanup.AddRaw(this, &UnrealCLR::Module::OnWorldCleanup); + + const FString hostfxrPath = UnrealCLR::ProjectPath + TEXT(HOSTFXR_PATH); + const FString assembliesPath = UnrealCLR::ProjectPath + TEXT("Plugins/UnrealCLR/Managed/"); + const FString runtimeConfigPath = assembliesPath + TEXT("UnrealEngine.Runtime.runtimeconfig.json"); + const FString runtimeAssemblyPath = assembliesPath + TEXT("UnrealEngine.Runtime.dll"); + const FString runtimeTypeName = TEXT("UnrealEngine.Runtime.Core, UnrealEngine.Runtime"); + const FString runtimeMethodName = TEXT("ManagedCommand"); + + UE_LOG(LogUnrealCLR, Display, TEXT("%s: Host path set to \"%s\""), ANSI_TO_TCHAR(__FUNCTION__), *hostfxrPath); + + HostfxrLibrary = FPlatformProcess::GetDllHandle(*hostfxrPath); + + if (HostfxrLibrary) { + UE_LOG(LogUnrealCLR, Display, TEXT("%s: Host library loaded successfuly!"), ANSI_TO_TCHAR(__FUNCTION__)); + + hostfxr_set_error_writer_fn HostfxrSetErrorWriter = (hostfxr_set_error_writer_fn)FPlatformProcess::GetDllExport(HostfxrLibrary, TEXT("hostfxr_set_error_writer")); + + if (!HostfxrSetErrorWriter) { + UE_LOG(LogUnrealCLR, Error, TEXT("%s: Unable to locate hostfxr_set_error_writer entry point!"), ANSI_TO_TCHAR(__FUNCTION__)); + + return; + } + + hostfxr_initialize_for_runtime_config_fn HostfxrInitializeForRuntimeConfig = (hostfxr_initialize_for_runtime_config_fn)FPlatformProcess::GetDllExport(HostfxrLibrary, TEXT("hostfxr_initialize_for_runtime_config")); + + if (!HostfxrInitializeForRuntimeConfig) { + UE_LOG(LogUnrealCLR, Error, TEXT("%s: Unable to locate hostfxr_initialize_for_runtime_config entry point!"), ANSI_TO_TCHAR(__FUNCTION__)); + + return; + } + + hostfxr_get_runtime_delegate_fn HostfxrGetRuntimeDelegate = (hostfxr_get_runtime_delegate_fn)FPlatformProcess::GetDllExport(HostfxrLibrary, TEXT("hostfxr_get_runtime_delegate")); + + if (!HostfxrGetRuntimeDelegate) { + UE_LOG(LogUnrealCLR, Error, TEXT("%s: Unable to locate hostfxr_get_runtime_delegate entry point!"), ANSI_TO_TCHAR(__FUNCTION__)); + + return; + } + + hostfxr_close_fn HostfxrClose = (hostfxr_close_fn)FPlatformProcess::GetDllExport(HostfxrLibrary, TEXT("hostfxr_close")); + + if (!HostfxrClose) { + UE_LOG(LogUnrealCLR, Error, TEXT("%s: Unable to locate hostfxr_close entry point!"), ANSI_TO_TCHAR(__FUNCTION__)); + + return; + } + + HostfxrSetErrorWriter(&HostError); + + hostfxr_handle HostfxrContext = nullptr; + + if (HostfxrInitializeForRuntimeConfig(UNREALCLR_PLATFORM_STRING(*runtimeConfigPath), nullptr, &HostfxrContext) != 0 || !HostfxrContext) { + UE_LOG(LogUnrealCLR, Error, TEXT("%s: Unable to initialize the host! Please, try to restart the engine."), ANSI_TO_TCHAR(__FUNCTION__)); + + HostfxrClose(HostfxrContext); + + return; + } + + void* hostfxrLoadAssemblyAndGetFunctionPointer = nullptr; + + if (HostfxrGetRuntimeDelegate(HostfxrContext, hdt_load_assembly_and_get_function_pointer, &hostfxrLoadAssemblyAndGetFunctionPointer) != 0 || !HostfxrGetRuntimeDelegate) { + UE_LOG(LogUnrealCLR, Error, TEXT("%s: Unable to get hdt_load_assembly_and_get_function_pointer runtime delegate!"), ANSI_TO_TCHAR(__FUNCTION__)); + + HostfxrClose(HostfxrContext); + + return; + } + + HostfxrClose(HostfxrContext); + + UE_LOG(LogUnrealCLR, Display, TEXT("%s: Host functions loaded successfuly!"), ANSI_TO_TCHAR(__FUNCTION__)); + + load_assembly_and_get_function_pointer_fn HostfxrLoadAssemblyAndGetFunctionPointer = (load_assembly_and_get_function_pointer_fn)hostfxrLoadAssemblyAndGetFunctionPointer; + + if (HostfxrLoadAssemblyAndGetFunctionPointer && HostfxrLoadAssemblyAndGetFunctionPointer(UNREALCLR_PLATFORM_STRING(*runtimeAssemblyPath), UNREALCLR_PLATFORM_STRING(*runtimeTypeName), UNREALCLR_PLATFORM_STRING(*runtimeMethodName), UNMANAGEDCALLERSONLY_METHOD, nullptr, (void**)&UnrealCLR::ManagedCommand) == 0) { + UE_LOG(LogUnrealCLR, Display, TEXT("%s: Host runtime assembly loaded successfuly!"), ANSI_TO_TCHAR(__FUNCTION__)); + } else { + UE_LOG(LogUnrealCLR, Error, TEXT("%s: Host runtime assembly loading failed!"), ANSI_TO_TCHAR(__FUNCTION__)); + + return; + } + + #if WITH_EDITOR + IPlatformFile& platformFile = FPlatformFileManager::Get().GetPlatformFile(); + + if (!platformFile.DirectoryExists(*UnrealCLR::UserAssembliesPath)) { + platformFile.CreateDirectory(*UnrealCLR::UserAssembliesPath); + + if (!platformFile.DirectoryExists(*UnrealCLR::UserAssembliesPath)) + UE_LOG(LogUnrealCLR, Warning, TEXT("%s: Unable to create a folder for managed assemblies at %s."), ANSI_TO_TCHAR(__FUNCTION__), *UnrealCLR::UserAssembliesPath); + } + #endif + + if (UnrealCLR::ManagedCommand) { + // Framework pointers + + int32 position = 0; + int32 checksum = 0; + + { + int32 head = 0; + Shared::Functions[position++] = Shared::AssertFunctions; + + Shared::AssertFunctions[head++] = (void*)&UnrealCLRFramework::Assert::OutputMessage; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::CommandLineFunctions; + + Shared::CommandLineFunctions[head++] = (void*)&UnrealCLRFramework::CommandLine::Get; + Shared::CommandLineFunctions[head++] = (void*)&UnrealCLRFramework::CommandLine::Set; + Shared::CommandLineFunctions[head++] = (void*)&UnrealCLRFramework::CommandLine::Append; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::DebugFunctions; + + Shared::DebugFunctions[head++] = (void*)&UnrealCLRFramework::Debug::Log; + Shared::DebugFunctions[head++] = (void*)&UnrealCLRFramework::Debug::Exception; + Shared::DebugFunctions[head++] = (void*)&UnrealCLRFramework::Debug::AddOnScreenMessage; + Shared::DebugFunctions[head++] = (void*)&UnrealCLRFramework::Debug::ClearOnScreenMessages; + Shared::DebugFunctions[head++] = (void*)&UnrealCLRFramework::Debug::DrawBox; + Shared::DebugFunctions[head++] = (void*)&UnrealCLRFramework::Debug::DrawCapsule; + Shared::DebugFunctions[head++] = (void*)&UnrealCLRFramework::Debug::DrawCone; + Shared::DebugFunctions[head++] = (void*)&UnrealCLRFramework::Debug::DrawCylinder; + Shared::DebugFunctions[head++] = (void*)&UnrealCLRFramework::Debug::DrawSphere; + Shared::DebugFunctions[head++] = (void*)&UnrealCLRFramework::Debug::DrawLine; + Shared::DebugFunctions[head++] = (void*)&UnrealCLRFramework::Debug::DrawPoint; + Shared::DebugFunctions[head++] = (void*)&UnrealCLRFramework::Debug::FlushPersistentLines; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::ObjectFunctions; + + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::IsPendingKill; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::IsValid; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::Load; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::Rename; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::Invoke; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::ToActor; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::ToComponent; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetID; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetName; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetBool; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetByte; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetShort; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetInt; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetLong; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetUShort; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetUInt; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetULong; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetFloat; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetDouble; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetEnum; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetString; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::GetText; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::SetBool; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::SetByte; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::SetShort; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::SetInt; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::SetLong; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::SetUShort; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::SetUInt; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::SetULong; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::SetFloat; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::SetDouble; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::SetEnum; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::SetString; + Shared::ObjectFunctions[head++] = (void*)&UnrealCLRFramework::Object::SetText; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::ApplicationFunctions; + + Shared::ApplicationFunctions[head++] = (void*)&UnrealCLRFramework::Application::IsCanEverRender; + Shared::ApplicationFunctions[head++] = (void*)&UnrealCLRFramework::Application::IsPackagedForDistribution; + Shared::ApplicationFunctions[head++] = (void*)&UnrealCLRFramework::Application::IsPackagedForShipping; + Shared::ApplicationFunctions[head++] = (void*)&UnrealCLRFramework::Application::GetProjectDirectory; + Shared::ApplicationFunctions[head++] = (void*)&UnrealCLRFramework::Application::GetDefaultLanguage; + Shared::ApplicationFunctions[head++] = (void*)&UnrealCLRFramework::Application::GetProjectName; + Shared::ApplicationFunctions[head++] = (void*)&UnrealCLRFramework::Application::GetVolumeMultiplier; + Shared::ApplicationFunctions[head++] = (void*)&UnrealCLRFramework::Application::SetProjectName; + Shared::ApplicationFunctions[head++] = (void*)&UnrealCLRFramework::Application::SetVolumeMultiplier; + Shared::ApplicationFunctions[head++] = (void*)&UnrealCLRFramework::Application::RequestExit; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::ConsoleManagerFunctions; + + Shared::ConsoleManagerFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleManager::IsRegisteredVariable; + Shared::ConsoleManagerFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleManager::FindVariable; + Shared::ConsoleManagerFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleManager::RegisterVariableBool; + Shared::ConsoleManagerFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleManager::RegisterVariableInt; + Shared::ConsoleManagerFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleManager::RegisterVariableFloat; + Shared::ConsoleManagerFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleManager::RegisterVariableString; + Shared::ConsoleManagerFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleManager::RegisterCommand; + Shared::ConsoleManagerFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleManager::UnregisterObject; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::EngineFunctions; + + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::IsSplitScreen; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::IsEditor; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::IsForegroundWindow; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::IsExitRequested; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::GetNetMode; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::GetFrameNumber; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::GetViewportSize; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::GetScreenResolution; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::GetWindowMode; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::GetVersion; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::GetMaxFPS; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::SetMaxFPS; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::SetTitle; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::AddActionMapping; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::AddAxisMapping; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::ForceGarbageCollection; + Shared::EngineFunctions[head++] = (void*)&UnrealCLRFramework::Engine::DelayGarbageCollection; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::HeadMountedDisplayFunctions; + + Shared::HeadMountedDisplayFunctions[head++] = (void*)&UnrealCLRFramework::HeadMountedDisplay::IsConnected; + Shared::HeadMountedDisplayFunctions[head++] = (void*)&UnrealCLRFramework::HeadMountedDisplay::GetEnabled; + Shared::HeadMountedDisplayFunctions[head++] = (void*)&UnrealCLRFramework::HeadMountedDisplay::GetLowPersistenceMode; + Shared::HeadMountedDisplayFunctions[head++] = (void*)&UnrealCLRFramework::HeadMountedDisplay::GetDeviceName; + Shared::HeadMountedDisplayFunctions[head++] = (void*)&UnrealCLRFramework::HeadMountedDisplay::SetEnable; + Shared::HeadMountedDisplayFunctions[head++] = (void*)&UnrealCLRFramework::HeadMountedDisplay::SetLowPersistenceMode; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::WorldFunctions; + + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::ForEachActor; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::GetActorCount; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::GetDeltaSeconds; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::GetRealTimeSeconds; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::GetTimeSeconds; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::GetCurrentLevelName; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::GetSimulatePhysics; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::GetWorldOrigin; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::GetActor; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::GetActorByTag; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::GetActorByID; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::GetFirstPlayerController; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::GetGameMode; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnActorBeginOverlapCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnActorBeginCursorOverCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnActorEndCursorOverCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnActorClickedCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnActorReleasedCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnActorEndOverlapCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnActorHitCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnComponentBeginOverlapCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnComponentEndOverlapCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnComponentHitCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnComponentBeginCursorOverCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnComponentEndCursorOverCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnComponentClickedCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetOnComponentReleasedCallback; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetSimulatePhysics; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetGravity; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SetWorldOrigin; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::OpenLevel; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::LineTraceTestByChannel; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::LineTraceTestByProfile; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::LineTraceSingleByChannel; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::LineTraceSingleByProfile; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SweepTestByChannel; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SweepTestByProfile; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SweepSingleByChannel; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::SweepSingleByProfile; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::OverlapAnyTestByChannel; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::OverlapAnyTestByProfile; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::OverlapBlockingTestByChannel; + Shared::WorldFunctions[head++] = (void*)&UnrealCLRFramework::World::OverlapBlockingTestByProfile; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::AssetFunctions; + + Shared::AssetFunctions[head++] = (void*)&UnrealCLRFramework::Asset::IsValid; + Shared::AssetFunctions[head++] = (void*)&UnrealCLRFramework::Asset::GetName; + Shared::AssetFunctions[head++] = (void*)&UnrealCLRFramework::Asset::GetPath; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::AssetRegistryFunctions; + + Shared::AssetRegistryFunctions[head++] = (void*)&UnrealCLRFramework::AssetRegistry::Get; + Shared::AssetRegistryFunctions[head++] = (void*)&UnrealCLRFramework::AssetRegistry::HasAssets; + Shared::AssetRegistryFunctions[head++] = (void*)&UnrealCLRFramework::AssetRegistry::ForEachAsset; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::BlueprintFunctions; + + Shared::BlueprintFunctions[head++] = (void*)&UnrealCLRFramework::Blueprint::IsValidActorClass; + Shared::BlueprintFunctions[head++] = (void*)&UnrealCLRFramework::Blueprint::IsValidComponentClass; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::ConsoleObjectFunctions; + + Shared::ConsoleObjectFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleObject::IsBool; + Shared::ConsoleObjectFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleObject::IsInt; + Shared::ConsoleObjectFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleObject::IsFloat; + Shared::ConsoleObjectFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleObject::IsString; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::ConsoleVariableFunctions; + + Shared::ConsoleVariableFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleVariable::GetBool; + Shared::ConsoleVariableFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleVariable::GetInt; + Shared::ConsoleVariableFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleVariable::GetFloat; + Shared::ConsoleVariableFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleVariable::GetString; + Shared::ConsoleVariableFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleVariable::SetBool; + Shared::ConsoleVariableFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleVariable::SetInt; + Shared::ConsoleVariableFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleVariable::SetFloat; + Shared::ConsoleVariableFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleVariable::SetString; + Shared::ConsoleVariableFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleVariable::SetOnChangedCallback; + Shared::ConsoleVariableFunctions[head++] = (void*)&UnrealCLRFramework::ConsoleVariable::ClearOnChangedCallback; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::ActorFunctions; + + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::IsPendingKill; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::IsRootComponentMovable; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::IsOverlappingActor; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::ForEachComponent; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::ForEachAttachedActor; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::ForEachChildActor; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::ForEachOverlappingActor; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::Spawn; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::Destroy; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::Rename; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::Hide; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::TeleportTo; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::GetComponent; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::GetComponentByTag; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::GetComponentByID; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::GetRootComponent; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::GetInputComponent; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::GetCreationTime; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::GetBlockInput; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::GetDistanceTo; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::GetHorizontalDistanceTo; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::GetBounds; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::GetEyesViewPoint; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::SetRootComponent; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::SetInputComponent; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::SetBlockInput; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::SetLifeSpan; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::SetEnableInput; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::SetEnableCollision; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::AddTag; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::RemoveTag; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::HasTag; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::RegisterEvent; + Shared::ActorFunctions[head++] = (void*)&UnrealCLRFramework::Actor::UnregisterEvent; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::GameModeBaseFunctions; + + Shared::GameModeBaseFunctions[head++] = (void*)&UnrealCLRFramework::GameModeBase::GetUseSeamlessTravel; + Shared::GameModeBaseFunctions[head++] = (void*)&UnrealCLRFramework::GameModeBase::SetUseSeamlessTravel; + Shared::GameModeBaseFunctions[head++] = (void*)&UnrealCLRFramework::GameModeBase::SwapPlayerControllers; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::PawnFunctions; + + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::IsControlled; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::IsPlayerControlled; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::GetAutoPossessAI; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::GetAutoPossessPlayer; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::GetUseControllerRotationYaw; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::GetUseControllerRotationPitch; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::GetUseControllerRotationRoll; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::GetGravityDirection; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::GetAIController; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::GetPlayerController; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::SetAutoPossessAI; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::SetAutoPossessPlayer; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::SetUseControllerRotationYaw; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::SetUseControllerRotationPitch; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::SetUseControllerRotationRoll; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::AddControllerYawInput; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::AddControllerPitchInput; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::AddControllerRollInput; + Shared::PawnFunctions[head++] = (void*)&UnrealCLRFramework::Pawn::AddMovementInput; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::CharacterFunctions; + + Shared::CharacterFunctions[head++] = (void*)&UnrealCLRFramework::Character::IsCrouched; + Shared::CharacterFunctions[head++] = (void*)&UnrealCLRFramework::Character::CanCrouch; + Shared::CharacterFunctions[head++] = (void*)&UnrealCLRFramework::Character::CanJump; + Shared::CharacterFunctions[head++] = (void*)&UnrealCLRFramework::Character::CheckJumpInput; + Shared::CharacterFunctions[head++] = (void*)&UnrealCLRFramework::Character::ClearJumpInput; + Shared::CharacterFunctions[head++] = (void*)&UnrealCLRFramework::Character::Launch; + Shared::CharacterFunctions[head++] = (void*)&UnrealCLRFramework::Character::Crouch; + Shared::CharacterFunctions[head++] = (void*)&UnrealCLRFramework::Character::StopCrouching; + Shared::CharacterFunctions[head++] = (void*)&UnrealCLRFramework::Character::Jump; + Shared::CharacterFunctions[head++] = (void*)&UnrealCLRFramework::Character::StopJumping; + Shared::CharacterFunctions[head++] = (void*)&UnrealCLRFramework::Character::SetOnLandedCallback; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::ControllerFunctions; + + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::IsLookInputIgnored; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::IsMoveInputIgnored; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::IsPlayerController; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::GetPawn; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::GetCharacter; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::GetViewTarget; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::GetControlRotation; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::GetDesiredRotation; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::LineOfSightTo; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::SetControlRotation; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::SetInitialLocationAndRotation; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::SetIgnoreLookInput; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::SetIgnoreMoveInput; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::ResetIgnoreLookInput; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::ResetIgnoreMoveInput; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::Possess; + Shared::ControllerFunctions[head++] = (void*)&UnrealCLRFramework::Controller::Unpossess; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::AIControllerFunctions; + + Shared::AIControllerFunctions[head++] = (void*)&UnrealCLRFramework::AIController::ClearFocus; + Shared::AIControllerFunctions[head++] = (void*)&UnrealCLRFramework::AIController::GetFocalPoint; + Shared::AIControllerFunctions[head++] = (void*)&UnrealCLRFramework::AIController::SetFocalPoint; + Shared::AIControllerFunctions[head++] = (void*)&UnrealCLRFramework::AIController::GetFocusActor; + Shared::AIControllerFunctions[head++] = (void*)&UnrealCLRFramework::AIController::GetAllowStrafe; + Shared::AIControllerFunctions[head++] = (void*)&UnrealCLRFramework::AIController::SetAllowStrafe; + Shared::AIControllerFunctions[head++] = (void*)&UnrealCLRFramework::AIController::SetFocus; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::PlayerControllerFunctions; + + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::IsPaused; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::GetShowMouseCursor; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::GetEnableClickEvents; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::GetEnableMouseOverEvents; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::GetMousePosition; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::GetPlayer; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::GetPlayerInput; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::GetHitResultAtScreenPosition; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::GetHitResultUnderCursor; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::SetShowMouseCursor; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::SetEnableClickEvents; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::SetEnableMouseOverEvents; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::SetMousePosition; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::ConsoleCommand; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::SetPause; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::SetViewTarget; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::SetViewTargetWithBlend; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::AddYawInput; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::AddPitchInput; + Shared::PlayerControllerFunctions[head++] = (void*)&UnrealCLRFramework::PlayerController::AddRollInput; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::VolumeFunctions; + + Shared::VolumeFunctions[head++] = (void*)&UnrealCLRFramework::Volume::EncompassesPoint; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::PostProcessVolumeFunctions; + + Shared::PostProcessVolumeFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessVolume::GetEnabled; + Shared::PostProcessVolumeFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessVolume::GetBlendRadius; + Shared::PostProcessVolumeFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessVolume::GetBlendWeight; + Shared::PostProcessVolumeFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessVolume::GetUnbound; + Shared::PostProcessVolumeFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessVolume::GetPriority; + Shared::PostProcessVolumeFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessVolume::SetEnabled; + Shared::PostProcessVolumeFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessVolume::SetBlendRadius; + Shared::PostProcessVolumeFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessVolume::SetBlendWeight; + Shared::PostProcessVolumeFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessVolume::SetUnbound; + Shared::PostProcessVolumeFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessVolume::SetPriority; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::SoundBaseFunctions; + + Shared::SoundBaseFunctions[head++] = (void*)&UnrealCLRFramework::SoundBase::GetDuration; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::SoundWaveFunctions; + + Shared::SoundWaveFunctions[head++] = (void*)&UnrealCLRFramework::SoundWave::GetLoop; + Shared::SoundWaveFunctions[head++] = (void*)&UnrealCLRFramework::SoundWave::SetLoop; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::AnimationInstanceFunctions; + + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::GetCurrentActiveMontage; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::IsPlaying; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::GetPlayRate; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::GetPosition; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::GetBlendTime; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::GetCurrentSection; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::SetPlayRate; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::SetPosition; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::SetNextSection; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::PlayMontage; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::PauseMontage; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::ResumeMontage; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::StopMontage; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::JumpToSection; + Shared::AnimationInstanceFunctions[head++] = (void*)&UnrealCLRFramework::AnimationInstance::JumpToSectionsEnd; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::PlayerFunctions; + + Shared::PlayerFunctions[head++] = (void*)&UnrealCLRFramework::Player::GetPlayerController; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::PlayerInputFunctions; + + Shared::PlayerInputFunctions[head++] = (void*)&UnrealCLRFramework::PlayerInput::IsKeyPressed; + Shared::PlayerInputFunctions[head++] = (void*)&UnrealCLRFramework::PlayerInput::GetTimeKeyPressed; + Shared::PlayerInputFunctions[head++] = (void*)&UnrealCLRFramework::PlayerInput::GetMouseSensitivity; + Shared::PlayerInputFunctions[head++] = (void*)&UnrealCLRFramework::PlayerInput::SetMouseSensitivity; + Shared::PlayerInputFunctions[head++] = (void*)&UnrealCLRFramework::PlayerInput::AddActionMapping; + Shared::PlayerInputFunctions[head++] = (void*)&UnrealCLRFramework::PlayerInput::AddAxisMapping; + Shared::PlayerInputFunctions[head++] = (void*)&UnrealCLRFramework::PlayerInput::RemoveActionMapping; + Shared::PlayerInputFunctions[head++] = (void*)&UnrealCLRFramework::PlayerInput::RemoveAxisMapping; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::FontFunctions; + + Shared::FontFunctions[head++] = (void*)&UnrealCLRFramework::Font::GetStringSize; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::Texture2DFunctions; + + Shared::Texture2DFunctions[head++] = (void*)&UnrealCLRFramework::Texture2D::CreateFromFile; + Shared::Texture2DFunctions[head++] = (void*)&UnrealCLRFramework::Texture2D::CreateFromBuffer; + Shared::Texture2DFunctions[head++] = (void*)&UnrealCLRFramework::Texture2D::HasAlphaChannel; + Shared::Texture2DFunctions[head++] = (void*)&UnrealCLRFramework::Texture2D::GetSize; + Shared::Texture2DFunctions[head++] = (void*)&UnrealCLRFramework::Texture2D::GetPixelFormat; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::ActorComponentFunctions; + + Shared::ActorComponentFunctions[head++] = (void*)&UnrealCLRFramework::ActorComponent::IsOwnerSelected; + Shared::ActorComponentFunctions[head++] = (void*)&UnrealCLRFramework::ActorComponent::GetOwner; + Shared::ActorComponentFunctions[head++] = (void*)&UnrealCLRFramework::ActorComponent::Destroy; + Shared::ActorComponentFunctions[head++] = (void*)&UnrealCLRFramework::ActorComponent::AddTag; + Shared::ActorComponentFunctions[head++] = (void*)&UnrealCLRFramework::ActorComponent::RemoveTag; + Shared::ActorComponentFunctions[head++] = (void*)&UnrealCLRFramework::ActorComponent::HasTag; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::InputComponentFunctions; + + Shared::InputComponentFunctions[head++] = (void*)&UnrealCLRFramework::InputComponent::HasBindings; + Shared::InputComponentFunctions[head++] = (void*)&UnrealCLRFramework::InputComponent::GetActionBindingsNumber; + Shared::InputComponentFunctions[head++] = (void*)&UnrealCLRFramework::InputComponent::ClearActionBindings; + Shared::InputComponentFunctions[head++] = (void*)&UnrealCLRFramework::InputComponent::BindAction; + Shared::InputComponentFunctions[head++] = (void*)&UnrealCLRFramework::InputComponent::BindAxis; + Shared::InputComponentFunctions[head++] = (void*)&UnrealCLRFramework::InputComponent::RemoveActionBinding; + Shared::InputComponentFunctions[head++] = (void*)&UnrealCLRFramework::InputComponent::GetBlockInput; + Shared::InputComponentFunctions[head++] = (void*)&UnrealCLRFramework::InputComponent::SetBlockInput; + Shared::InputComponentFunctions[head++] = (void*)&UnrealCLRFramework::InputComponent::GetPriority; + Shared::InputComponentFunctions[head++] = (void*)&UnrealCLRFramework::InputComponent::SetPriority; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::MovementComponentFunctions; + + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::GetConstrainToPlane; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::GetSnapToPlaneAtStart; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::GetVelocity; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::GetPlaneConstraint; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::GetUpdateOnlyIfRendered; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::GetPlaneConstraintNormal; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::GetPlaneConstraintOrigin; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::GetGravity; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::GetMaxSpeed; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::SetConstrainToPlane; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::SetSnapToPlaneAtStart; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::SetUpdateOnlyIfRendered; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::SetVelocity; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::SetPlaneConstraint; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::SetPlaneConstraintNormal; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::SetPlaneConstraintOrigin; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::SetPlaneConstraintFromVectors; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::IsExceedingMaxSpeed; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::IsInWater; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::StopMovement; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::ConstrainDirectionToPlane; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::ConstrainLocationToPlane; + Shared::MovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::MovementComponent::ConstrainNormalToPlane; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::RotatingMovementComponentFunctions; + + Shared::RotatingMovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::RotatingMovementComponent::Create; + Shared::RotatingMovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::RotatingMovementComponent::GetRotationInLocalSpace; + Shared::RotatingMovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::RotatingMovementComponent::GetPivotTranslation; + Shared::RotatingMovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::RotatingMovementComponent::GetRotationRate; + Shared::RotatingMovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::RotatingMovementComponent::SetRotationInLocalSpace; + Shared::RotatingMovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::RotatingMovementComponent::SetPivotTranslation; + Shared::RotatingMovementComponentFunctions[head++] = (void*)&UnrealCLRFramework::RotatingMovementComponent::SetRotationRate; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::SceneComponentFunctions; + + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::IsAttachedToComponent; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::IsAttachedToActor; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::IsVisible; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::IsSocketExists; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::HasAnySockets; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::CanAttachAsChild; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::ForEachAttachedChild; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::Create; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::AttachToComponent; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::DetachFromComponent; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::Activate; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::Deactivate; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::UpdateToWorld; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::AddLocalOffset; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::AddLocalRotation; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::AddRelativeLocation; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::AddRelativeRotation; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::AddLocalTransform; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::AddWorldOffset; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::AddWorldRotation; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::AddWorldTransform; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::GetAttachedSocketName; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::GetBounds; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::GetSocketLocation; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::GetSocketRotation; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::GetComponentVelocity; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::GetComponentLocation; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::GetComponentRotation; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::GetComponentScale; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::GetComponentTransform; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::GetForwardVector; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::GetRightVector; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::GetUpVector; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::SetMobility; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::SetVisibility; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::SetRelativeLocation; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::SetRelativeRotation; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::SetRelativeTransform; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::SetWorldLocation; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::SetWorldRotation; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::SetWorldScale; + Shared::SceneComponentFunctions[head++] = (void*)&UnrealCLRFramework::SceneComponent::SetWorldTransform; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::AudioComponentFunctions; + + Shared::AudioComponentFunctions[head++] = (void*)&UnrealCLRFramework::AudioComponent::IsPlaying; + Shared::AudioComponentFunctions[head++] = (void*)&UnrealCLRFramework::AudioComponent::GetPaused; + Shared::AudioComponentFunctions[head++] = (void*)&UnrealCLRFramework::AudioComponent::SetSound; + Shared::AudioComponentFunctions[head++] = (void*)&UnrealCLRFramework::AudioComponent::SetPaused; + Shared::AudioComponentFunctions[head++] = (void*)&UnrealCLRFramework::AudioComponent::Play; + Shared::AudioComponentFunctions[head++] = (void*)&UnrealCLRFramework::AudioComponent::Stop; + Shared::AudioComponentFunctions[head++] = (void*)&UnrealCLRFramework::AudioComponent::FadeIn; + Shared::AudioComponentFunctions[head++] = (void*)&UnrealCLRFramework::AudioComponent::FadeOut; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::CameraComponentFunctions; + + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::GetConstrainAspectRatio; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::GetAspectRatio; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::GetFieldOfView; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::GetOrthoFarClipPlane; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::GetOrthoNearClipPlane; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::GetOrthoWidth; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::GetLockToHeadMountedDisplay; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::SetProjectionMode; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::SetConstrainAspectRatio; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::SetAspectRatio; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::SetFieldOfView; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::SetOrthoFarClipPlane; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::SetOrthoNearClipPlane; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::SetOrthoWidth; + Shared::CameraComponentFunctions[head++] = (void*)&UnrealCLRFramework::CameraComponent::SetLockToHeadMountedDisplay; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::ChildActorComponentFunctions; + + Shared::ChildActorComponentFunctions[head++] = (void*)&UnrealCLRFramework::ChildActorComponent::GetChildActor; + Shared::ChildActorComponentFunctions[head++] = (void*)&UnrealCLRFramework::ChildActorComponent::SetChildActor; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::SpringArmComponentFunctions; + + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::IsCollisionFixApplied; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetDrawDebugLagMarkers; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetCollisionTest; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetCameraPositionLag; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetCameraRotationLag; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetCameraLagSubstepping; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetInheritPitch; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetInheritRoll; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetInheritYaw; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetCameraLagMaxDistance; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetCameraLagMaxTimeStep; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetCameraPositionLagSpeed; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetCameraRotationLagSpeed; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetProbeChannel; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetProbeSize; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetSocketOffset; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetTargetArmLength; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetTargetOffset; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetUnfixedCameraPosition; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetDesiredRotation; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetTargetRotation; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::GetUsePawnControlRotation; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetDrawDebugLagMarkers; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetCollisionTest; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetCameraPositionLag; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetCameraRotationLag; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetCameraLagSubstepping; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetInheritPitch; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetInheritRoll; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetInheritYaw; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetCameraLagMaxDistance; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetCameraLagMaxTimeStep; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetCameraPositionLagSpeed; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetCameraRotationLagSpeed; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetProbeChannel; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetProbeSize; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetSocketOffset; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetTargetArmLength; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetTargetOffset; + Shared::SpringArmComponentFunctions[head++] = (void*)&UnrealCLRFramework::SpringArmComponent::SetUsePawnControlRotation; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::PostProcessComponentFunctions; + + Shared::PostProcessComponentFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessComponent::GetEnabled; + Shared::PostProcessComponentFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessComponent::GetBlendRadius; + Shared::PostProcessComponentFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessComponent::GetBlendWeight; + Shared::PostProcessComponentFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessComponent::GetUnbound; + Shared::PostProcessComponentFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessComponent::GetPriority; + Shared::PostProcessComponentFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessComponent::SetEnabled; + Shared::PostProcessComponentFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessComponent::SetBlendRadius; + Shared::PostProcessComponentFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessComponent::SetBlendWeight; + Shared::PostProcessComponentFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessComponent::SetUnbound; + Shared::PostProcessComponentFunctions[head++] = (void*)&UnrealCLRFramework::PostProcessComponent::SetPriority; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::PrimitiveComponentFunctions; + + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::IsGravityEnabled; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::IsOverlappingComponent; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::ForEachOverlappingComponent; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::AddAngularImpulseInDegrees; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::AddAngularImpulseInRadians; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::AddForce; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::AddForceAtLocation; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::AddImpulse; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::AddImpulseAtLocation; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::AddRadialForce; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::AddRadialImpulse; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::AddTorqueInDegrees; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::AddTorqueInRadians; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetMass; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetPhysicsLinearVelocity; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetPhysicsLinearVelocityAtPoint; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetPhysicsAngularVelocityInDegrees; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetPhysicsAngularVelocityInRadians; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetCastShadow; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetOnlyOwnerSee; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetOwnerNoSee; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetIgnoreRadialForce; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetIgnoreRadialImpulse; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetMaterial; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetMaterialsNumber; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetDistanceToCollision; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetSquaredDistanceToCollision; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetAngularDamping; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::GetLinearDamping; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetGenerateOverlapEvents; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetGenerateHitEvents; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetMass; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetCenterOfMass; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetPhysicsLinearVelocity; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetPhysicsAngularVelocityInDegrees; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetPhysicsAngularVelocityInRadians; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetPhysicsMaxAngularVelocityInDegrees; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetPhysicsMaxAngularVelocityInRadians; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetCastShadow; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetOnlyOwnerSee; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetOwnerNoSee; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetIgnoreRadialForce; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetIgnoreRadialImpulse; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetMaterial; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetSimulatePhysics; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetAngularDamping; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetLinearDamping; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetEnableGravity; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetCollisionMode; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetCollisionChannel; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetCollisionProfileName; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetCollisionResponseToChannel; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetCollisionResponseToAllChannels; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetIgnoreActorWhenMoving; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::SetIgnoreComponentWhenMoving; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::ClearMoveIgnoreActors; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::ClearMoveIgnoreComponents; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::CreateAndSetMaterialInstanceDynamic; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::RegisterEvent; + Shared::PrimitiveComponentFunctions[head++] = (void*)&UnrealCLRFramework::PrimitiveComponent::UnregisterEvent; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::ShapeComponentFunctions; + + Shared::ShapeComponentFunctions[head++] = (void*)&UnrealCLRFramework::ShapeComponent::GetDynamicObstacle; + Shared::ShapeComponentFunctions[head++] = (void*)&UnrealCLRFramework::ShapeComponent::GetShapeColor; + Shared::ShapeComponentFunctions[head++] = (void*)&UnrealCLRFramework::ShapeComponent::SetDynamicObstacle; + Shared::ShapeComponentFunctions[head++] = (void*)&UnrealCLRFramework::ShapeComponent::SetShapeColor; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::BoxComponentFunctions; + + Shared::BoxComponentFunctions[head++] = (void*)&UnrealCLRFramework::BoxComponent::GetScaledBoxExtent; + Shared::BoxComponentFunctions[head++] = (void*)&UnrealCLRFramework::BoxComponent::GetUnscaledBoxExtent; + Shared::BoxComponentFunctions[head++] = (void*)&UnrealCLRFramework::BoxComponent::SetBoxExtent; + Shared::BoxComponentFunctions[head++] = (void*)&UnrealCLRFramework::BoxComponent::InitBoxExtent; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::SphereComponentFunctions; + + Shared::SphereComponentFunctions[head++] = (void*)&UnrealCLRFramework::SphereComponent::GetScaledSphereRadius; + Shared::SphereComponentFunctions[head++] = (void*)&UnrealCLRFramework::SphereComponent::GetUnscaledSphereRadius; + Shared::SphereComponentFunctions[head++] = (void*)&UnrealCLRFramework::SphereComponent::GetShapeScale; + Shared::SphereComponentFunctions[head++] = (void*)&UnrealCLRFramework::SphereComponent::SetSphereRadius; + Shared::SphereComponentFunctions[head++] = (void*)&UnrealCLRFramework::SphereComponent::InitSphereRadius; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::CapsuleComponentFunctions; + + Shared::CapsuleComponentFunctions[head++] = (void*)&UnrealCLRFramework::CapsuleComponent::GetScaledCapsuleRadius; + Shared::CapsuleComponentFunctions[head++] = (void*)&UnrealCLRFramework::CapsuleComponent::GetUnscaledCapsuleRadius; + Shared::CapsuleComponentFunctions[head++] = (void*)&UnrealCLRFramework::CapsuleComponent::GetShapeScale; + Shared::CapsuleComponentFunctions[head++] = (void*)&UnrealCLRFramework::CapsuleComponent::GetScaledCapsuleSize; + Shared::CapsuleComponentFunctions[head++] = (void*)&UnrealCLRFramework::CapsuleComponent::GetUnscaledCapsuleSize; + Shared::CapsuleComponentFunctions[head++] = (void*)&UnrealCLRFramework::CapsuleComponent::SetCapsuleRadius; + Shared::CapsuleComponentFunctions[head++] = (void*)&UnrealCLRFramework::CapsuleComponent::SetCapsuleSize; + Shared::CapsuleComponentFunctions[head++] = (void*)&UnrealCLRFramework::CapsuleComponent::InitCapsuleSize; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::MeshComponentFunctions; + + Shared::MeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::MeshComponent::IsValidMaterialSlotName; + Shared::MeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::MeshComponent::GetMaterialIndex; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::TextRenderComponentFunctions; + + Shared::TextRenderComponentFunctions[head++] = (void*)&UnrealCLRFramework::TextRenderComponent::SetFont; + Shared::TextRenderComponentFunctions[head++] = (void*)&UnrealCLRFramework::TextRenderComponent::SetText; + Shared::TextRenderComponentFunctions[head++] = (void*)&UnrealCLRFramework::TextRenderComponent::SetTextMaterial; + Shared::TextRenderComponentFunctions[head++] = (void*)&UnrealCLRFramework::TextRenderComponent::SetTextRenderColor; + Shared::TextRenderComponentFunctions[head++] = (void*)&UnrealCLRFramework::TextRenderComponent::SetHorizontalAlignment; + Shared::TextRenderComponentFunctions[head++] = (void*)&UnrealCLRFramework::TextRenderComponent::SetHorizontalSpacingAdjustment; + Shared::TextRenderComponentFunctions[head++] = (void*)&UnrealCLRFramework::TextRenderComponent::SetVerticalAlignment; + Shared::TextRenderComponentFunctions[head++] = (void*)&UnrealCLRFramework::TextRenderComponent::SetVerticalSpacingAdjustment; + Shared::TextRenderComponentFunctions[head++] = (void*)&UnrealCLRFramework::TextRenderComponent::SetScale; + Shared::TextRenderComponentFunctions[head++] = (void*)&UnrealCLRFramework::TextRenderComponent::SetWorldSize; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::LightComponentBaseFunctions; + + Shared::LightComponentBaseFunctions[head++] = (void*)&UnrealCLRFramework::LightComponentBase::GetIntensity; + Shared::LightComponentBaseFunctions[head++] = (void*)&UnrealCLRFramework::LightComponentBase::GetCastShadows; + Shared::LightComponentBaseFunctions[head++] = (void*)&UnrealCLRFramework::LightComponentBase::SetCastShadows; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::LightComponentFunctions; + + Shared::LightComponentFunctions[head++] = (void*)&UnrealCLRFramework::LightComponent::SetIntensity; + Shared::LightComponentFunctions[head++] = (void*)&UnrealCLRFramework::LightComponent::SetLightColor; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::MotionControllerComponentFunctions; + + Shared::MotionControllerComponentFunctions[head++] = (void*)&UnrealCLRFramework::MotionControllerComponent::IsTracked; + Shared::MotionControllerComponentFunctions[head++] = (void*)&UnrealCLRFramework::MotionControllerComponent::GetDisplayDeviceModel; + Shared::MotionControllerComponentFunctions[head++] = (void*)&UnrealCLRFramework::MotionControllerComponent::GetDisableLowLatencyUpdate; + Shared::MotionControllerComponentFunctions[head++] = (void*)&UnrealCLRFramework::MotionControllerComponent::GetTrackingSource; + Shared::MotionControllerComponentFunctions[head++] = (void*)&UnrealCLRFramework::MotionControllerComponent::SetDisplayDeviceModel; + Shared::MotionControllerComponentFunctions[head++] = (void*)&UnrealCLRFramework::MotionControllerComponent::SetDisableLowLatencyUpdate; + Shared::MotionControllerComponentFunctions[head++] = (void*)&UnrealCLRFramework::MotionControllerComponent::SetTrackingSource; + Shared::MotionControllerComponentFunctions[head++] = (void*)&UnrealCLRFramework::MotionControllerComponent::SetTrackingMotionSource; + Shared::MotionControllerComponentFunctions[head++] = (void*)&UnrealCLRFramework::MotionControllerComponent::SetAssociatedPlayerIndex; + Shared::MotionControllerComponentFunctions[head++] = (void*)&UnrealCLRFramework::MotionControllerComponent::SetCustomDisplayMesh; + Shared::MotionControllerComponentFunctions[head++] = (void*)&UnrealCLRFramework::MotionControllerComponent::SetDisplayModelSource; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::StaticMeshComponentFunctions; + + Shared::StaticMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::StaticMeshComponent::GetLocalBounds; + Shared::StaticMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::StaticMeshComponent::GetStaticMesh; + Shared::StaticMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::StaticMeshComponent::SetStaticMesh; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::InstancedStaticMeshComponentFunctions; + + Shared::InstancedStaticMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::InstancedStaticMeshComponent::GetInstanceCount; + Shared::InstancedStaticMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::InstancedStaticMeshComponent::GetInstanceTransform; + Shared::InstancedStaticMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::InstancedStaticMeshComponent::AddInstance; + Shared::InstancedStaticMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::InstancedStaticMeshComponent::AddInstances; + Shared::InstancedStaticMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::InstancedStaticMeshComponent::UpdateInstanceTransform; + Shared::InstancedStaticMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::InstancedStaticMeshComponent::BatchUpdateInstanceTransforms; + Shared::InstancedStaticMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::InstancedStaticMeshComponent::RemoveInstance; + Shared::InstancedStaticMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::InstancedStaticMeshComponent::ClearInstances; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::HierarchicalInstancedStaticMeshComponentFunctions; + + Shared::HierarchicalInstancedStaticMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::HierarchicalInstancedStaticMeshComponent::GetDisableCollision; + Shared::HierarchicalInstancedStaticMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::HierarchicalInstancedStaticMeshComponent::SetDisableCollision; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::SkinnedMeshComponentFunctions; + + Shared::SkinnedMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::SkinnedMeshComponent::GetBonesNumber; + Shared::SkinnedMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::SkinnedMeshComponent::GetBoneIndex; + Shared::SkinnedMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::SkinnedMeshComponent::GetBoneName; + Shared::SkinnedMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::SkinnedMeshComponent::GetBoneTransform; + Shared::SkinnedMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::SkinnedMeshComponent::SetSkeletalMesh; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::SkeletalMeshComponentFunctions; + + Shared::SkeletalMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::SkeletalMeshComponent::IsPlaying; + Shared::SkeletalMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::SkeletalMeshComponent::GetAnimationInstance; + Shared::SkeletalMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::SkeletalMeshComponent::SetAnimation; + Shared::SkeletalMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::SkeletalMeshComponent::SetAnimationMode; + Shared::SkeletalMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::SkeletalMeshComponent::SetAnimationBlueprint; + Shared::SkeletalMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::SkeletalMeshComponent::Play; + Shared::SkeletalMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::SkeletalMeshComponent::PlayAnimation; + Shared::SkeletalMeshComponentFunctions[head++] = (void*)&UnrealCLRFramework::SkeletalMeshComponent::Stop; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::SplineComponentFunctions; + + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::IsClosedLoop; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetDuration; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetSplinePointType; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetSplinePointsNumber; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetSplineSegmentsNumber; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetTangentAtDistanceAlongSpline; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetTangentAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetTangentAtTime; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetTransformAtDistanceAlongSpline; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetTransformAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetArriveTangentAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetDefaultUpVector; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetDirectionAtDistanceAlongSpline; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetDirectionAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetDirectionAtTime; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetDistanceAlongSplineAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetLeaveTangentAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetLocationAndTangentAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetLocationAtDistanceAlongSpline; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetLocationAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetLocationAtTime; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetRightVectorAtDistanceAlongSpline; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetRightVectorAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetRightVectorAtTime; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetRollAtDistanceAlongSpline; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetRollAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetRollAtTime; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetRotationAtDistanceAlongSpline; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetRotationAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetRotationAtTime; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetScaleAtDistanceAlongSpline; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetScaleAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetScaleAtTime; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetSplineLength; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetTransformAtTime; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetUpVectorAtDistanceAlongSpline; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetUpVectorAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::GetUpVectorAtTime; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::SetDuration; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::SetSplinePointType; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::SetClosedLoop; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::SetDefaultUpVector; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::SetLocationAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::SetTangentAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::SetTangentsAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::SetUpVectorAtSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::AddSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::AddSplinePointAtIndex; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::ClearSplinePoints; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::FindDirectionClosestToWorldLocation; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::FindLocationClosestToWorldLocation; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::FindUpVectorClosestToWorldLocation; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::FindRightVectorClosestToWorldLocation; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::FindRollClosestToWorldLocation; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::FindScaleClosestToWorldLocation; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::FindTangentClosestToWorldLocation; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::FindTransformClosestToWorldLocation; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::RemoveSplinePoint; + Shared::SplineComponentFunctions[head++] = (void*)&UnrealCLRFramework::SplineComponent::UpdateSpline; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::RadialForceComponentFunctions; + + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::GetIgnoreOwningActor; + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::GetImpulseVelocityChange; + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::GetLinearFalloff; + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::GetForceStrength; + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::GetImpulseStrength; + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::GetRadius; + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::SetIgnoreOwningActor; + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::SetImpulseVelocityChange; + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::SetLinearFalloff; + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::SetForceStrength; + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::SetImpulseStrength; + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::SetRadius; + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::AddCollisionChannelToAffect; + Shared::RadialForceComponentFunctions[head++] = (void*)&UnrealCLRFramework::RadialForceComponent::FireImpulse; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::MaterialInterfaceFunctions; + + Shared::MaterialInterfaceFunctions[head++] = (void*)&UnrealCLRFramework::MaterialInterface::IsTwoSided; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::MaterialFunctions; + + Shared::MaterialFunctions[head++] = (void*)&UnrealCLRFramework::Material::IsDefaultMaterial; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::MaterialInstanceFunctions; + + Shared::MaterialInstanceFunctions[head++] = (void*)&UnrealCLRFramework::MaterialInstance::IsChildOf; + Shared::MaterialInstanceFunctions[head++] = (void*)&UnrealCLRFramework::MaterialInstance::GetParent; + + checksum += head; + } + + { + int32 head = 0; + Shared::Functions[position++] = Shared::MaterialInstanceDynamicFunctions; + + Shared::MaterialInstanceDynamicFunctions[head++] = (void*)&UnrealCLRFramework::MaterialInstanceDynamic::ClearParameterValues; + Shared::MaterialInstanceDynamicFunctions[head++] = (void*)&UnrealCLRFramework::MaterialInstanceDynamic::SetTextureParameterValue; + Shared::MaterialInstanceDynamicFunctions[head++] = (void*)&UnrealCLRFramework::MaterialInstanceDynamic::SetVectorParameterValue; + Shared::MaterialInstanceDynamicFunctions[head++] = (void*)&UnrealCLRFramework::MaterialInstanceDynamic::SetScalarParameterValue; + + checksum += head; + } + + checksum += position; + + // Runtime pointers + + Shared::RuntimeFunctions[0] = (void*)&UnrealCLR::Module::Exception; + Shared::RuntimeFunctions[1] = (void*)&UnrealCLR::Module::Log; + + constexpr void* functions[3] = { + Shared::RuntimeFunctions, + Shared::Events, + Shared::Functions + }; + + if (reinterpret_cast(UnrealCLR::ManagedCommand(UnrealCLR::Command(functions, checksum))) == 0xF) { + UE_LOG(LogUnrealCLR, Display, TEXT("%s: Host runtime assembly initialized successfuly!"), ANSI_TO_TCHAR(__FUNCTION__)); + } else { + UE_LOG(LogUnrealCLR, Error, TEXT("%s: Host runtime assembly initialization failed!"), ANSI_TO_TCHAR(__FUNCTION__)); + + return; + } + + UnrealCLR::Status = UnrealCLR::StatusType::Idle; + + UE_LOG(LogUnrealCLR, Display, TEXT("%s: Host loaded successfuly!"), ANSI_TO_TCHAR(__FUNCTION__)); + } else { + UE_LOG(LogUnrealCLR, Error, TEXT("%s: Host runtime assembly unable to load the initialization function!"), ANSI_TO_TCHAR(__FUNCTION__)); + + return; + } + } else { + UE_LOG(LogUnrealCLR, Error, TEXT("%s: Host library loading failed!"), ANSI_TO_TCHAR(__FUNCTION__)); + } +} + +void UnrealCLR::Module::ShutdownModule() { + FWorldDelegates::OnPostWorldInitialization.Remove(OnWorldPostInitializationHandle); + FWorldDelegates::OnWorldCleanup.Remove(OnWorldCleanupHandle); + + FPlatformProcess::FreeDllHandle(HostfxrLibrary); +} + +void UnrealCLR::Module::OnWorldPostInitialization(UWorld* World, const UWorld::InitializationValues InitializationValues) { + if (World->IsGameWorld()) { + if (UnrealCLR::WorldTickState == TickState::Stopped) { + UnrealCLR::Engine::Manager = NewObject(); + UnrealCLR::Engine::Manager->AddToRoot(); + UnrealCLR::Engine::World = World; + + if (UnrealCLR::Status != UnrealCLR::StatusType::Stopped) { + UnrealCLR::ManagedCommand(UnrealCLR::Command(CommandType::LoadAssemblies)); + UnrealCLR::Status = UnrealCLR::StatusType::Running; + + for (TActorIterator currentActor(UnrealCLR::Engine::World); currentActor; ++currentActor) { + RegisterTickFunction(OnPrePhysicsTickFunction, TG_PrePhysics, *currentActor); + RegisterTickFunction(OnDuringPhysicsTickFunction, TG_DuringPhysics, *currentActor); + RegisterTickFunction(OnPostPhysicsTickFunction, TG_PostPhysics, *currentActor); + RegisterTickFunction(OnPostUpdateTickFunction, TG_PostUpdateWork, *currentActor); + + UnrealCLR::WorldTickState = UnrealCLR::TickState::Registered; + + if (UnrealCLR::Shared::Events[OnWorldBegin]) + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[OnWorldBegin])); + + break; + } + } else { + #if WITH_EDITOR + FNotificationInfo notificationInfo(FText::FromString(TEXT("UnrealCLR host is not initialized! Please, check logs and try to restart the engine."))); + + notificationInfo.ExpireDuration = 5.0f; + + FSlateNotificationManager::Get().AddNotification(notificationInfo); + #endif + } + } + } +} + +void UnrealCLR::Module::OnWorldCleanup(UWorld* World, bool SessionEnded, bool CleanupResources) { + if (World->IsGameWorld() && World == UnrealCLR::Engine::World && UnrealCLR::WorldTickState != UnrealCLR::TickState::Stopped) { + if (UnrealCLR::Status != UnrealCLR::StatusType::Stopped) { + if (UnrealCLR::Shared::Events[OnWorldEnd]) + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[OnWorldEnd])); + + OnPrePhysicsTickFunction.UnRegisterTickFunction(); + OnDuringPhysicsTickFunction.UnRegisterTickFunction(); + OnPostPhysicsTickFunction.UnRegisterTickFunction(); + OnPostUpdateTickFunction.UnRegisterTickFunction(); + + UnrealCLR::ManagedCommand(UnrealCLR::Command(CommandType::UnloadAssemblies)); + UnrealCLR::Status = UnrealCLR::StatusType::Idle; + } + + UnrealCLR::Engine::World = nullptr; + UnrealCLR::Engine::Manager->RemoveFromRoot(); + UnrealCLR::Engine::Manager = nullptr; + UnrealCLR::WorldTickState = UnrealCLR::TickState::Stopped; + + FMemory::Memset(UnrealCLR::Shared::Events, 0, sizeof(UnrealCLR::Shared::Events)); + } +} + +void UnrealCLR::Module::RegisterTickFunction(FTickFunction& TickFunction, ETickingGroup TickGroup, AWorldSettings* LevelActor) { + TickFunction.bCanEverTick = true; + TickFunction.bTickEvenWhenPaused = false; + TickFunction.bStartWithTickEnabled = true; + TickFunction.bHighPriority = true; + TickFunction.bAllowTickOnDedicatedServer = true; + TickFunction.bRunOnAnyThread = false; + TickFunction.TickGroup = TickGroup; + TickFunction.RegisterTickFunction(UnrealCLR::Engine::World->PersistentLevel); + LevelActor->PrimaryActorTick.AddPrerequisite(UnrealCLR::Engine::Manager, TickFunction); +} + +void UnrealCLR::Module::HostError(const char_t* Message) { + UE_LOG(LogUnrealCLR, Error, TEXT("%s: %s"), ANSI_TO_TCHAR(__FUNCTION__), *FString(Message)); +} + +void UnrealCLR::Module::Exception(const char* Message) { + const FString message(ANSI_TO_TCHAR(Message)); + + FString OutputLog(message); + + OutputLog.ReplaceCharInline(TEXT('\n'), TEXT(' ')); + OutputLog.ReplaceCharInline(TEXT('\r'), TEXT(' ')); + OutputLog.ReplaceInline(TEXT(" "), TEXT(" ")); + + UE_LOG(LogUnrealCLR, Error, TEXT("%s: %s"), ANSI_TO_TCHAR(__FUNCTION__), *OutputLog); + + GEngine->AddOnScreenDebugMessage((uint64)-1, 10.0f, FColor::Red, *message); +} + +void UnrealCLR::Module::Log(UnrealCLR::LogLevel Level, const char* Message) { + #define UNREALCLR_LOG(Verbosity) UE_LOG(LogUnrealCLR, Verbosity, TEXT("%s: %s"), ANSI_TO_TCHAR(__FUNCTION__), *message); + + FString message(ANSI_TO_TCHAR(Message)); + + if (Level == UnrealCLR::LogLevel::Display) { + UNREALCLR_LOG(Display); + } else if (Level == UnrealCLR::LogLevel::Warning) { + UNREALCLR_LOG(Warning); + + GEngine->AddOnScreenDebugMessage((uint64)-1, 60.0f, FColor::Yellow, *message); + } else if (Level == UnrealCLR::LogLevel::Error) { + UNREALCLR_LOG(Error); + + GEngine->AddOnScreenDebugMessage((uint64)-1, 60.0f, FColor::Red, *message); + } else if (Level == UnrealCLR::LogLevel::Fatal) { + UNREALCLR_LOG(Error); + + GEngine->AddOnScreenDebugMessage((uint64)-1, 60.0f, FColor::Red, *message); + + UnrealCLR::Status = UnrealCLR::StatusType::Idle; + } +} + +void UnrealCLR::PrePhysicsTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) { + if (UnrealCLR::WorldTickState != UnrealCLR::TickState::Started && UnrealCLR::Shared::Events[OnWorldPostBegin]) { + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[OnWorldPostBegin])); + UnrealCLR::WorldTickState = UnrealCLR::TickState::Started; + } + + if (UnrealCLR::Shared::Events[OnWorldPrePhysicsTick]) + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[OnWorldPrePhysicsTick], DeltaTime)); +} + +void UnrealCLR::DuringPhysicsTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) { + if (UnrealCLR::Shared::Events[OnWorldDuringPhysicsTick]) + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[OnWorldDuringPhysicsTick], DeltaTime)); +} + +void UnrealCLR::PostPhysicsTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) { + if (UnrealCLR::Shared::Events[OnWorldPostPhysicsTick]) + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[OnWorldPostPhysicsTick], DeltaTime)); +} + +void UnrealCLR::PostUpdateTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) { + if (UnrealCLR::Shared::Events[OnWorldPostUpdateTick]) + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[OnWorldPostUpdateTick], DeltaTime)); +} + +FString UnrealCLR::PrePhysicsTickFunction::DiagnosticMessage() { + return TEXT("PrePhysicsTickFunction"); +} + +FString UnrealCLR::DuringPhysicsTickFunction::DiagnosticMessage() { + return TEXT("DuringPhysicsTickFunction"); +} + +FString UnrealCLR::PostPhysicsTickFunction::DiagnosticMessage() { + return TEXT("PostPhysicsTickFunction"); +} + +FString UnrealCLR::PostUpdateTickFunction::DiagnosticMessage() { + return TEXT("PostUpdateTickFunction"); +} + +size_t UnrealCLR::Utility::Strcpy(char* Destination, const char* Source, size_t Length) { + char* destination = Destination; + const char* source = Source; + size_t length = Length; + + if (length != 0 && --length != 0) { + do { + if ((*destination++ = *source++) == 0) + break; + } + + while (--length != 0); + } + + if (length == 0) { + if (Length != 0) + *destination = '\0'; + + while (*source++); + } + + return (source - Source - 1); +} + +size_t UnrealCLR::Utility::Strlen(const char* Source) { + return strlen(Source) + 1; +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(UnrealCLR::Module, UnrealCLR) + +``` + +`Source/Native/Source/UnrealCLR/Private/UnrealCLRFramework.cpp`: + +```cpp +/* + * Unreal Engine .NET 6 integration + * Copyright (c) 2021 Stanislav Denisov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "UnrealCLRFramework.h" + +DEFINE_LOG_CATEGORY(LogUnrealManaged); + +namespace UnrealCLRFramework { + #define UNREALCLR_GET_ATTACHMENT_RULE(Rule, Result) {\ + switch (Rule) {\ + case AttachmentTransformRule::KeepRelativeTransform:\ + Result = FAttachmentTransformRules::KeepRelativeTransform;\ + break;\ + case AttachmentTransformRule::KeepWorldTransform:\ + Result = FAttachmentTransformRules::KeepWorldTransform;\ + break;\ + case AttachmentTransformRule::SnapToTargetIncludingScale:\ + Result = FAttachmentTransformRules::SnapToTargetIncludingScale;\ + break;\ + case AttachmentTransformRule::SnapToTargetNotIncludingScale:\ + Result = FAttachmentTransformRules::SnapToTargetNotIncludingScale;\ + break;\ + default:\ + break;\ + }\ + } + + #define UNREALCLR_GET_DETACHMENT_RULE(Rule, Result) {\ + switch (Rule) {\ + case DetachmentTransformRule::KeepRelativeTransform:\ + Result = FDetachmentTransformRules::KeepRelativeTransform;\ + break;\ + case DetachmentTransformRule::KeepWorldTransform:\ + Result = FDetachmentTransformRules::KeepWorldTransform;\ + break;\ + default:\ + break;\ + }\ + } + + #define UNREALCLR_GET_ACTOR_TYPE(Type, Head, Tail, Result) {\ + switch (Type) {\ + case ActorType::Base:\ + Result = Head AActor Tail;\ + break;\ + case ActorType::Camera:\ + Result = Head ACameraActor Tail;\ + break;\ + case ActorType::TriggerBox:\ + Result = Head ATriggerBox Tail;\ + break;\ + case ActorType::TriggerSphere:\ + Result = Head ATriggerSphere Tail;\ + break;\ + case ActorType::TriggerCapsule:\ + Result = Head ATriggerCapsule Tail;\ + break;\ + case ActorType::Pawn:\ + Result = Head APawn Tail;\ + break;\ + case ActorType::Character:\ + Result = Head ACharacter Tail;\ + break;\ + case ActorType::AIController:\ + Result = Head AAIController Tail;\ + break;\ + case ActorType::PlayerController:\ + Result = Head APlayerController Tail;\ + break;\ + case ActorType::Brush:\ + Result = Head ABrush Tail;\ + break;\ + case ActorType::AmbientSound:\ + Result = Head AAmbientSound Tail;\ + break;\ + case ActorType::DirectionalLight:\ + Result = Head ADirectionalLight Tail;\ + break;\ + case ActorType::PointLight:\ + Result = Head APointLight Tail;\ + break;\ + case ActorType::RectLight:\ + Result = Head ARectLight Tail;\ + break;\ + case ActorType::SpotLight:\ + Result = Head ASpotLight Tail;\ + break;\ + case ActorType::TriggerVolume:\ + Result = Head ATriggerVolume Tail;\ + break;\ + case ActorType::PostProcessVolume:\ + Result = Head APostProcessVolume Tail;\ + break;\ + case ActorType::LevelScript:\ + Result = Head ALevelScriptActor Tail;\ + break;\ + case ActorType::GameModeBase:\ + Result = Head AGameModeBase Tail;\ + break;\ + default:\ + break;\ + }\ + } + + #define UNREALCLR_GET_COMPONENT_TYPE(Type, Head, Tail, Result) {\ + switch (Type) {\ + case ComponentType::Actor:\ + Result = Head UActorComponent Tail;\ + break;\ + case ComponentType::Input:\ + Result = Head UInputComponent Tail;\ + break;\ + case ComponentType::Movement:\ + Result = Head UMovementComponent Tail;\ + break;\ + case ComponentType::RotatingMovement:\ + Result = Head URotatingMovementComponent Tail;\ + break;\ + case ComponentType::Scene:\ + Result = Head USceneComponent Tail;\ + break;\ + case ComponentType::Audio:\ + Result = Head UAudioComponent Tail;\ + break;\ + case ComponentType::Camera:\ + Result = Head UCameraComponent Tail;\ + break;\ + case ComponentType::Light:\ + Result = Head ULightComponent Tail;\ + break;\ + case ComponentType::DirectionalLight:\ + Result = Head UDirectionalLightComponent Tail;\ + break;\ + case ComponentType::MotionController:\ + Result = Head UMotionControllerComponent Tail;\ + break;\ + case ComponentType::StaticMesh:\ + Result = Head UStaticMeshComponent Tail;\ + break;\ + case ComponentType::InstancedStaticMesh:\ + Result = Head UInstancedStaticMeshComponent Tail;\ + break;\ + case ComponentType::HierarchicalInstancedStaticMesh:\ + Result = Head UHierarchicalInstancedStaticMeshComponent Tail;\ + break;\ + case ComponentType::ChildActor:\ + Result = Head UChildActorComponent Tail;\ + break;\ + case ComponentType::SpringArm:\ + Result = Head USpringArmComponent Tail;\ + break;\ + case ComponentType::PostProcess:\ + Result = Head UPostProcessComponent Tail;\ + break;\ + case ComponentType::Box:\ + Result = Head UBoxComponent Tail;\ + break;\ + case ComponentType::Sphere:\ + Result = Head USphereComponent Tail;\ + break;\ + case ComponentType::Capsule:\ + Result = Head UCapsuleComponent Tail;\ + break;\ + case ComponentType::SkeletalMesh:\ + Result = Head USkeletalMeshComponent Tail;\ + break;\ + case ComponentType::TextRender:\ + Result = Head UTextRenderComponent Tail;\ + break;\ + case ComponentType::Spline:\ + Result = Head USplineComponent Tail;\ + break;\ + case ComponentType::RadialForce:\ + Result = Head URadialForceComponent Tail;\ + break;\ + default:\ + break;\ + }\ + } + + #define UNREALCLR_GET_MOVABLE_COMPONENT_TYPE(Type, Head, Tail, Result) {\ + switch (Type) {\ + case ComponentType::Scene:\ + Result = Head USceneComponent Tail;\ + break;\ + case ComponentType::Audio:\ + Result = Head UAudioComponent Tail;\ + break;\ + case ComponentType::Camera:\ + Result = Head UCameraComponent Tail;\ + break;\ + case ComponentType::Light:\ + Result = Head ULightComponent Tail;\ + break;\ + case ComponentType::DirectionalLight:\ + Result = Head UDirectionalLightComponent Tail;\ + break;\ + case ComponentType::MotionController:\ + Result = Head UMotionControllerComponent Tail;\ + break;\ + case ComponentType::StaticMesh:\ + Result = Head UStaticMeshComponent Tail;\ + break;\ + case ComponentType::InstancedStaticMesh:\ + Result = Head UInstancedStaticMeshComponent Tail;\ + break;\ + case ComponentType::HierarchicalInstancedStaticMesh:\ + Result = Head UHierarchicalInstancedStaticMeshComponent Tail;\ + break;\ + case ComponentType::ChildActor:\ + Result = Head UChildActorComponent Tail;\ + break;\ + case ComponentType::SpringArm:\ + Result = Head USpringArmComponent Tail;\ + break;\ + case ComponentType::PostProcess:\ + Result = Head UPostProcessComponent Tail;\ + break;\ + case ComponentType::Box:\ + Result = Head UBoxComponent Tail;\ + break;\ + case ComponentType::Sphere:\ + Result = Head USphereComponent Tail;\ + break;\ + case ComponentType::Capsule:\ + Result = Head UCapsuleComponent Tail;\ + break;\ + case ComponentType::SkeletalMesh:\ + Result = Head USkeletalMeshComponent Tail;\ + break;\ + case ComponentType::TextRender:\ + Result = Head UTextRenderComponent Tail;\ + break;\ + case ComponentType::Spline:\ + Result = Head USplineComponent Tail;\ + break;\ + case ComponentType::RadialForce:\ + Result = Head URadialForceComponent Tail;\ + break;\ + default:\ + break;\ + }\ + } + + #define UNREALCLR_GET_PROPERTY_VALUE(Type, Object, Name, Value)\ + FName name(UTF8_TO_TCHAR(Name));\ + for (TFieldIterator currentProperty(Object->GetClass()); currentProperty; ++currentProperty) {\ + Type* property = *currentProperty;\ + if (property->GetFName() == name) {\ + *Value = property->GetPropertyValue_InContainer(Object);\ + return true;\ + }\ + }\ + return false; + + #define UNREALCLR_SET_PROPERTY_VALUE(Type, Object, Name, Value)\ + FName name(UTF8_TO_TCHAR(Name));\ + for (TFieldIterator currentProperty(Object->GetClass()); currentProperty; ++currentProperty) {\ + Type* property = *currentProperty;\ + if (property->GetFName() == name) {\ + property->SetPropertyValue_InContainer(Object, Value);\ + return true;\ + }\ + }\ + return false; + + #define UNREALCLR_GET_BONE_NAME(Hit, Name)\ + if (Name && Hit.BoneName.GetStringLength() > 0) {\ + const char* boneName = TCHAR_TO_UTF8(*Hit.BoneName.ToString());\ + UnrealCLR::Utility::Strcpy(Name, boneName, UnrealCLR::Utility::Strlen(boneName));\ + } + + #define UNREALCLR_SET_BONE_NAME(Name)\ + FName boneName;\ + if (!Name)\ + boneName = NAME_None;\ + else\ + boneName = FName(UTF8_TO_TCHAR(Name)); + + #define UNREALCLR_SET_COLLISION_QUERY_PARAMS(IgnoredActor, IgnoredComponent)\ + FCollisionQueryParams queryParams;\ + if (IgnoredActor)\ + queryParams.AddIgnoredActor(IgnoredActor);\ + if (IgnoredComponent)\ + queryParams.AddIgnoredComponent(IgnoredComponent); + + #define UNREALCLR_SET_COMPONENT_INSTANCE(Component, Name)\ + Actor->AddInstanceComponent(Component);\ + component->OnComponentCreated();\ + component->RegisterComponent();\ + if (Name)\ + component->Rename(*FString(UTF8_TO_TCHAR(Name))); + + #define UNREALCLR_SET_ACTOR_EVENT(Type, Condition, Method) {\ + switch (Type) {\ + case ActorEventType::OnActorBeginOverlap:\ + if (Condition Actor->OnActorBeginOverlap.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorBeginOverlap))\ + Actor->OnActorBeginOverlap. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorBeginOverlap);\ + break;\ + case ActorEventType::OnActorEndOverlap:\ + if (Condition Actor->OnActorEndOverlap.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorEndOverlap))\ + Actor->OnActorEndOverlap. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorEndOverlap);\ + break;\ + case ActorEventType::OnActorHit:\ + if (Condition Actor->OnActorHit.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorHit))\ + Actor->OnActorHit. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorHit);\ + break;\ + case ActorEventType::OnActorBeginCursorOver:\ + if (Condition Actor->OnBeginCursorOver.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorBeginCursorOver))\ + Actor->OnBeginCursorOver. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorBeginCursorOver);\ + break;\ + case ActorEventType::OnActorEndCursorOver:\ + if (Condition Actor->OnEndCursorOver.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorEndCursorOver))\ + Actor->OnEndCursorOver. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorEndCursorOver);\ + break;\ + case ActorEventType::OnActorClicked:\ + if (Condition Actor->OnClicked.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorClicked))\ + Actor->OnClicked. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorClicked);\ + break;\ + case ActorEventType::OnActorReleased:\ + if (Condition Actor->OnReleased.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorReleased))\ + Actor->OnReleased. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ActorReleased);\ + break;\ + default:\ + break;\ + }\ + } + + #define UNREALCLR_SET_COMPONENT_EVENT(Type, Condition, Method) {\ + switch (Type) {\ + case ComponentEventType::OnComponentBeginOverlap:\ + if (Condition PrimitiveComponent->OnComponentBeginOverlap.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentBeginOverlap))\ + PrimitiveComponent->OnComponentBeginOverlap. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentBeginOverlap);\ + break;\ + case ComponentEventType::OnComponentEndOverlap:\ + if (Condition PrimitiveComponent->OnComponentEndOverlap.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentEndOverlap))\ + PrimitiveComponent->OnComponentEndOverlap. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentEndOverlap);\ + break;\ + case ComponentEventType::OnComponentHit:\ + if (Condition PrimitiveComponent->OnComponentHit.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentHit))\ + PrimitiveComponent->OnComponentHit. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentHit);\ + break;\ + case ComponentEventType::OnComponentBeginCursorOver:\ + if (Condition PrimitiveComponent->OnBeginCursorOver.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentBeginCursorOver))\ + PrimitiveComponent->OnBeginCursorOver. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentBeginCursorOver);\ + break;\ + case ComponentEventType::OnComponentEndCursorOver:\ + if (Condition PrimitiveComponent->OnEndCursorOver.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentEndCursorOver))\ + PrimitiveComponent->OnEndCursorOver. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentEndCursorOver);\ + break;\ + case ComponentEventType::OnComponentClicked:\ + if (Condition PrimitiveComponent->OnClicked.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentClicked))\ + PrimitiveComponent->OnClicked. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentClicked);\ + break;\ + case ComponentEventType::OnComponentReleased:\ + if (Condition PrimitiveComponent->OnReleased.IsAlreadyBound(UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentReleased))\ + PrimitiveComponent->OnReleased. Method (UnrealCLR::Engine::Manager, &UUnrealCLRManager::ComponentReleased);\ + break;\ + default:\ + break;\ + }\ + } + + #define UNREALCLR_COLOR_TO_INTEGER(Color) (Color.A << 24) + (Color.R << 16) + (Color.G << 8) + Color.B + + #if ENGINE_MAJOR_VERSION == 4 + #if ENGINE_MINOR_VERSION <= 26 + #define UNREALCLR_BLEND_TYPE 5 + #elif ENGINE_MINOR_VERSION >= 27 + #define UNREALCLR_BLEND_TYPE 6 + #endif + + #if ENGINE_MINOR_VERSION <= 25 + #define UNREALCLR_PIXEL_FORMAT 71 + #elif ENGINE_MINOR_VERSION >= 26 + #define UNREALCLR_PIXEL_FORMAT 72 + #endif + #elif ENGINE_MAJOR_VERSION == 5 + #define UNREALCLR_PIXEL_FORMAT 72 + #define UNREALCLR_BLEND_TYPE 6 + #endif + + static_assert(AudioFadeCurve::Count == AudioFadeCurve(4), "Invalid elements count of the [AudioFadeCurve] enumeration"); + static_assert(BlendType::VTBlend_MAX == BlendType(UNREALCLR_BLEND_TYPE), "Invalid elements count of the [BlendType] enumeration"); + static_assert(CollisionChannel::ECC_MAX == CollisionChannel(33), "Invalid elements count of the [CollisionChannel] enumeration"); + static_assert(CollisionResponse::ECR_MAX == CollisionResponse(3), "Invalid elements count of the [CollisionResponse] enumeration"); + static_assert(ControllerHand::ControllerHand_Count == ControllerHand(17), "Invalid elements count of the [ControllerHand] enumeration"); + static_assert(InputEvent::IE_MAX == InputEvent(5), "Invalid elements count of the [InputEvent] enumeration"); + static_assert(NetMode::NM_MAX == NetMode(4), "Invalid elements count of the [NetMode] enumeration"); + static_assert(PixelFormat::PF_MAX == PixelFormat(UNREALCLR_PIXEL_FORMAT), "Invalid elements count of the [PixelFormat] enumeration"); + + static_assert(sizeof(Bounds) == 28, "Invalid size of the [Bounds] structure"); + static_assert(sizeof(CollisionShape) == 16, "Invalid size of the [CollisionShape] structure"); + + namespace Assert { + void OutputMessage(const char* Message) { + FString message(UTF8_TO_TCHAR(Message)); + + UE_LOG(LogUnrealManaged, Error, TEXT("%s: %s"), ANSI_TO_TCHAR(__FUNCTION__), *message); + + GEngine->AddOnScreenDebugMessage((uint64)-1, 60.0f, FColor::Red, *message); + } + } + + namespace CommandLine { + void Get(char* Arguments) { + const char* arguments = TCHAR_TO_UTF8(FCommandLine::Get()); + + UnrealCLR::Utility::Strcpy(Arguments, arguments, UnrealCLR::Utility::Strlen(arguments)); + } + + void Set(const char* Arguments) { + FCommandLine::Set(UTF8_TO_TCHAR(Arguments)); + } + + void Append(const char* Arguments) { + FCommandLine::Append(UTF8_TO_TCHAR(Arguments)); + } + } + + namespace Debug { + void Log(LogLevel Level, const char* Message) { + #define UNREALCLR_FRAMEWORK_LOG(Verbosity) UE_LOG(LogUnrealManaged, Verbosity, TEXT("%s: %s"), ANSI_TO_TCHAR(__FUNCTION__), *FString(UTF8_TO_TCHAR(Message))); + + if (Level == LogLevel::Display) { + UNREALCLR_FRAMEWORK_LOG(Display); + } else if (Level == LogLevel::Warning) { + UNREALCLR_FRAMEWORK_LOG(Warning); + } else if (Level == LogLevel::Error) { + UNREALCLR_FRAMEWORK_LOG(Error); + } else if (Level == LogLevel::Fatal) { + UNREALCLR_FRAMEWORK_LOG(Fatal); + } + } + + void Exception(const char* Message) { + GEngine->AddOnScreenDebugMessage((uint64)-1, 10.0f, FColor::Red, *FString(UTF8_TO_TCHAR(Message))); + } + + void AddOnScreenMessage(int32 Key, float TimeToDisplay, Color DisplayColor, const char* Message) { + GEngine->AddOnScreenDebugMessage((uint64)Key, TimeToDisplay, DisplayColor, *FString(UTF8_TO_TCHAR(Message))); + } + + void ClearOnScreenMessages() { + GEngine->ClearOnScreenDebugMessages(); + } + + void DrawBox(const Vector3* Center, const Vector3* Extent, const Quaternion* Rotation, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority, float Thickness) { + DrawDebugBox(UnrealCLR::Engine::World, *Center, *Extent, *Rotation, Color, PersistentLines, LifeTime, DepthPriority, Thickness); + } + + void DrawCapsule(const Vector3* Center, float HalfHeight, float Radius, const Quaternion* Rotation, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority, float Thickness) { + DrawDebugCapsule(UnrealCLR::Engine::World, *Center, HalfHeight, Radius, *Rotation, Color, PersistentLines, LifeTime, DepthPriority, Thickness); + } + + void DrawCone(const Vector3* Origin, const Vector3* Direction, float Length, float AngleWidth, float AngleHeight, int32 Sides, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority, float Thickness) { + DrawDebugCone(UnrealCLR::Engine::World, *Origin, *Direction, Length, AngleWidth, AngleHeight, Sides, Color, PersistentLines, LifeTime, DepthPriority, Thickness); + } + + void DrawCylinder(const Vector3* Start, const Vector3* End, float Radius, int32 Segments, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority, float Thickness) { + DrawDebugCylinder(UnrealCLR::Engine::World, *Start, *End, Radius, Segments, Color, PersistentLines, LifeTime, DepthPriority, Thickness); + } + + void DrawSphere(const Vector3* Center, float Radius, int32 Segments, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority, float Thickness) { + DrawDebugSphere(UnrealCLR::Engine::World, *Center, Radius, Segments, Color, PersistentLines, LifeTime, DepthPriority, Thickness); + } + + void DrawLine(const Vector3* Start, const Vector3* End, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority, float Thickness) { + DrawDebugLine(UnrealCLR::Engine::World, *Start, *End, Color, PersistentLines, LifeTime, DepthPriority, Thickness); + } + + void DrawPoint(const Vector3* Location, float Size, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority) { + DrawDebugPoint(UnrealCLR::Engine::World, *Location, Size, Color, PersistentLines, LifeTime, DepthPriority); + } + + void FlushPersistentLines() { + FlushPersistentDebugLines(UnrealCLR::Engine::World); + } + } + + namespace Object { + bool IsPendingKill(UObject* Object) { + return Object->IsPendingKill(); + } + + bool IsValid(UObject* Object) { + return Object->IsValidLowLevel(); + } + + UObject* Load(ObjectType Type, const char* Name) { + UObject* object = nullptr; + + switch (Type) { + case ObjectType::Blueprint: { + #if WITH_EDITOR + object = StaticLoadObject(UBlueprint::StaticClass(), nullptr, *FString(UTF8_TO_TCHAR(Name))); + #else + FString name(UTF8_TO_TCHAR(Name)); + int32 index = INDEX_NONE; + + if (name.FindLastChar(TCHAR('/'), index)) { + name.AppendChar(TCHAR('.')); + name.Append(name.Mid(index + 1, name.Len() - index - 2)); + } + + name.Append(TEXT("_C")); + + object = StaticLoadObject(UClass::StaticClass(), nullptr, *name); + #endif + break; + } + + case ObjectType::SoundWave: { + object = StaticLoadObject(USoundWave::StaticClass(), nullptr, *FString(UTF8_TO_TCHAR(Name))); + break; + } + + case ObjectType::AnimationSequence: { + object = StaticLoadObject(UAnimSequence::StaticClass(), nullptr, *FString(UTF8_TO_TCHAR(Name))); + break; + } + + case ObjectType::AnimationMontage: { + object = StaticLoadObject(UAnimMontage::StaticClass(), nullptr, *FString(UTF8_TO_TCHAR(Name))); + break; + } + + case ObjectType::StaticMesh: { + object = StaticLoadObject(UStaticMesh::StaticClass(), nullptr, *FString(UTF8_TO_TCHAR(Name))); + break; + } + + case ObjectType::SkeletalMesh: { + object = StaticLoadObject(USkeletalMesh::StaticClass(), nullptr, *FString(UTF8_TO_TCHAR(Name))); + break; + } + + case ObjectType::Material: { + object = StaticLoadObject(UMaterial::StaticClass(), nullptr, *FString(UTF8_TO_TCHAR(Name))); + break; + } + + case ObjectType::Font: { + object = StaticLoadObject(UFont::StaticClass(), nullptr, *FString(UTF8_TO_TCHAR(Name))); + break; + } + + case ObjectType::Texture2D: { + object = StaticLoadObject(UTexture2D::StaticClass(), nullptr, *FString(UTF8_TO_TCHAR(Name))); + break; + } + + default: + break; + } + + return object; + } + + void Rename(UObject* Object, const char* Name) { + FString name(UTF8_TO_TCHAR(Name)); + + Object->Rename(*name); + } + + bool Invoke(UObject* Object, const char* Command) { + static FOutputDeviceNull outputDevice; + + return Object->CallFunctionByNameWithArguments(UTF8_TO_TCHAR(Command), outputDevice, nullptr, true); + } + + AActor* ToActor(UObject* Object, ActorType Type) { + AActor* actor = nullptr; + + UNREALCLR_GET_ACTOR_TYPE(Type, Cast<, >(Object), actor); + + return actor; + } + + UActorComponent* ToComponent(UObject* Object, ComponentType Type) { + UActorComponent* component = nullptr; + + UNREALCLR_GET_COMPONENT_TYPE(Type, Cast<, >(Object), component); + + return component; + } + + uint32 GetID(UObject* Object) { + return Object->GetUniqueID(); + } + + void GetName(UObject* Object, char* Name) { + const char* name = TCHAR_TO_UTF8(*Object->GetName()); + + UnrealCLR::Utility::Strcpy(Name, name, UnrealCLR::Utility::Strlen(name)); + } + + bool GetBool(UObject* Object, const char* Name, bool* Value) { + UNREALCLR_GET_PROPERTY_VALUE(FBoolProperty, Object, Name, Value); + } + + bool GetByte(UObject* Object, const char* Name, uint8* Value) { + UNREALCLR_GET_PROPERTY_VALUE(FByteProperty, Object, Name, Value); + } + + bool GetShort(UObject* Object, const char* Name, int16* Value) { + UNREALCLR_GET_PROPERTY_VALUE(FInt16Property, Object, Name, Value); + } + + bool GetInt(UObject* Object, const char* Name, int32* Value) { + UNREALCLR_GET_PROPERTY_VALUE(FIntProperty, Object, Name, Value); + } + + bool GetLong(UObject* Object, const char* Name, int64* Value) { + UNREALCLR_GET_PROPERTY_VALUE(FInt64Property, Object, Name, Value); + } + + bool GetUShort(UObject* Object, const char* Name, uint16* Value) { + UNREALCLR_GET_PROPERTY_VALUE(FUInt16Property, Object, Name, Value); + } + + bool GetUInt(UObject* Object, const char* Name, uint32* Value) { + UNREALCLR_GET_PROPERTY_VALUE(FUInt32Property, Object, Name, Value); + } + + bool GetULong(UObject* Object, const char* Name, uint64* Value) { + UNREALCLR_GET_PROPERTY_VALUE(FUInt64Property, Object, Name, Value); + } + + bool GetFloat(UObject* Object, const char* Name, float* Value) { + UNREALCLR_GET_PROPERTY_VALUE(FFloatProperty, Object, Name, Value); + } + + bool GetDouble(UObject* Object, const char* Name, double* Value) { + UNREALCLR_GET_PROPERTY_VALUE(FDoubleProperty, Object, Name, Value); + } + + bool GetEnum(UObject* Object, const char* Name, int32* Value) { + FName name(UTF8_TO_TCHAR(Name)); + + for (TFieldIterator currentProperty(Object->GetClass()); currentProperty; ++currentProperty) { + FNumericProperty* property = *currentProperty; + + if (property->GetFName() == name) { + *Value = static_cast(property->GetSignedIntPropertyValue(property->ContainerPtrToValuePtr(Object))); + + return true; + } + } + + return false; + } + + bool GetString(UObject* Object, const char* Name, char* Value) { + FName name(UTF8_TO_TCHAR(Name)); + + for (TFieldIterator currentProperty(Object->GetClass()); currentProperty; ++currentProperty) { + FStrProperty* property = *currentProperty; + + if (property->GetFName() == name) { + const char* string = TCHAR_TO_UTF8(*property->GetPropertyValue_InContainer(Object)); + + UnrealCLR::Utility::Strcpy((char*)Value, string, UnrealCLR::Utility::Strlen(string)); + + return true; + } + } + + return false; + } + + bool GetText(UObject* Object, const char* Name, char* Value) { + FName name(UTF8_TO_TCHAR(Name)); + + for (TFieldIterator currentProperty(Object->GetClass()); currentProperty; ++currentProperty) { + FTextProperty* property = *currentProperty; + + if (property->GetFName() == name) { + const char* string = TCHAR_TO_UTF8(*property->GetPropertyValue_InContainer(Object).ToString()); + + UnrealCLR::Utility::Strcpy(Value, string, UnrealCLR::Utility::Strlen(string)); + + return true; + } + } + + return false; + } + + bool SetBool(UObject* Object, const char* Name, bool Value) { + UNREALCLR_SET_PROPERTY_VALUE(FBoolProperty, Object, Name, Value); + } + + bool SetByte(UObject* Object, const char* Name, uint8 Value) { + UNREALCLR_SET_PROPERTY_VALUE(FByteProperty, Object, Name, Value); + } + + bool SetShort(UObject* Object, const char* Name, int16 Value) { + UNREALCLR_SET_PROPERTY_VALUE(FInt16Property, Object, Name, Value); + } + + bool SetInt(UObject* Object, const char* Name, int32 Value) { + UNREALCLR_SET_PROPERTY_VALUE(FIntProperty, Object, Name, Value); + } + + bool SetLong(UObject* Object, const char* Name, int64 Value) { + UNREALCLR_SET_PROPERTY_VALUE(FInt64Property, Object, Name, Value); + } + + bool SetUShort(UObject* Object, const char* Name, uint16 Value) { + UNREALCLR_SET_PROPERTY_VALUE(FUInt16Property, Object, Name, Value); + } + + bool SetUInt(UObject* Object, const char* Name, uint32 Value) { + UNREALCLR_SET_PROPERTY_VALUE(FUInt32Property, Object, Name, Value); + } + + bool SetULong(UObject* Object, const char* Name, uint64 Value) { + UNREALCLR_SET_PROPERTY_VALUE(FUInt64Property, Object, Name, Value); + } + + bool SetFloat(UObject* Object, const char* Name, float Value) { + UNREALCLR_SET_PROPERTY_VALUE(FFloatProperty, Object, Name, Value); + } + + bool SetDouble(UObject* Object, const char* Name, double Value) { + UNREALCLR_SET_PROPERTY_VALUE(FDoubleProperty, Object, Name, Value); + } + + bool SetEnum(UObject* Object, const char* Name, int32 Value) { + FName name(UTF8_TO_TCHAR(Name)); + + for (TFieldIterator currentProperty(Object->GetClass()); currentProperty; ++currentProperty) { + FNumericProperty* property = *currentProperty; + + if (property->GetFName() == name) { + property->SetIntPropertyValue(property->ContainerPtrToValuePtr(Object), static_cast(Value)); + + return true; + } + } + + return false; + } + + bool SetString(UObject* Object, const char* Name, const char* Value) { + FName name(UTF8_TO_TCHAR(Name)); + + for (TFieldIterator currentProperty(Object->GetClass()); currentProperty; ++currentProperty) { + FStrProperty* property = *currentProperty; + + if (property->GetFName() == name) { + property->SetPropertyValue_InContainer(Object, FString(UTF8_TO_TCHAR(Value))); + + return true; + } + } + + return false; + } + + bool SetText(UObject* Object, const char* Name, const char* Value) { + FName name(UTF8_TO_TCHAR(Name)); + + for (TFieldIterator currentProperty(Object->GetClass()); currentProperty; ++currentProperty) { + FTextProperty* property = *currentProperty; + + if (property->GetFName() == name) { + property->SetPropertyValue_InContainer(Object, FText::FromString(FString(UTF8_TO_TCHAR(Value)))); + + return true; + } + } + + return false; + } + } + + namespace Asset { + bool IsValid(FAssetData* Asset) { + return Asset->IsValid(); + } + + void GetName(FAssetData* Asset, char* Name) { + const char* name = TCHAR_TO_UTF8(*Asset->AssetName.ToString()); + + UnrealCLR::Utility::Strcpy(Name, name, UnrealCLR::Utility::Strlen(name)); + } + + void GetPath(FAssetData* Asset, char* Path) { + FString objectPath = Asset->ObjectPath.ToString(); + + int32 index = INDEX_NONE; + + if (objectPath.FindLastChar(TCHAR('.'), index)) + objectPath = FString(index, *objectPath); + + const char* path = TCHAR_TO_UTF8(*objectPath); + + UnrealCLR::Utility::Strcpy(Path, path, UnrealCLR::Utility::Strlen(path)); + } + } + + namespace AssetRegistry { + IAssetRegistry* Get() { + static IAssetRegistry* assetRegistry; + + if (!assetRegistry) + assetRegistry = &FModuleManager::Get().LoadModuleChecked(TEXT("AssetRegistry")).Get(); + + return assetRegistry; + } + + bool HasAssets(IAssetRegistry* AssetRegistry, const char* Path, bool Recursive) { + return AssetRegistry->HasAssets(FName(UTF8_TO_TCHAR(Path)), Recursive); + } + + void ForEachAsset(IAssetRegistry* AssetRegistry, const char* Path, bool Recursive, bool IncludeOnlyOnDiskAssets, FAssetData** Array, int32* Elements) { + static TArray assets; + static TArray references; + + assets.Reset(); + references.Reset(); + + AssetRegistry->GetAssetsByPath(FName(UTF8_TO_TCHAR(Path)), assets, Recursive, IncludeOnlyOnDiskAssets); + + int32 elements = assets.Num(); + + if (elements > 0) { + for (int32 i = 0; i < elements; i++) { + references.Add(&assets[i]); + } + + *Array = reinterpret_cast(references.GetData()); + *Elements = references.Num(); + } + } + } + + namespace Blueprint { + bool IsValidActorClass(UBlueprint* Blueprint, ActorType Type) { + #if WITH_EDITOR + TSubclassOf type; + + UNREALCLR_GET_ACTOR_TYPE(Type, UNREALCLR_NONE, ::StaticClass(), type); + + return Blueprint->ParentClass == type; + #else + return true; + #endif + } + + bool IsValidComponentClass(UBlueprint* Blueprint, ComponentType Type) { + #if WITH_EDITOR + TSubclassOf type; + + UNREALCLR_GET_MOVABLE_COMPONENT_TYPE(Type, UNREALCLR_NONE, ::StaticClass(), type); + + return Blueprint->ParentClass == type; + #else + return true; + #endif + } + } + + namespace Application { + bool IsCanEverRender() { + return FApp::CanEverRender(); + } + + bool IsPackagedForDistribution() { + return FGenericPlatformMisc::IsPackagedForDistribution(); + } + + bool IsPackagedForShipping() { + #if UE_BUILD_SHIPPING + return true; + #else + return false; + #endif + } + + void GetProjectDirectory(char* Directory) { + const char* directory = TCHAR_TO_UTF8(*FPaths::ConvertRelativePathToFull(FPaths::ProjectDir())); + + UnrealCLR::Utility::Strcpy(Directory, directory, UnrealCLR::Utility::Strlen(directory)); + } + + void GetDefaultLanguage(char* Language) { + const char* language = TCHAR_TO_UTF8(*FGenericPlatformMisc::GetDefaultLanguage()); + + UnrealCLR::Utility::Strcpy(Language, language, UnrealCLR::Utility::Strlen(language)); + } + + void GetProjectName(char* ProjectName) { + const char* projectName = TCHAR_TO_UTF8(FApp::GetProjectName()); + + UnrealCLR::Utility::Strcpy(ProjectName, projectName, UnrealCLR::Utility::Strlen(projectName)); + } + + float GetVolumeMultiplier() { + return FApp::GetVolumeMultiplier(); + } + + void SetProjectName(const char* ProjectName) { + FApp::SetProjectName(UTF8_TO_TCHAR(ProjectName)); + } + + void SetVolumeMultiplier(float Value) { + FApp::SetVolumeMultiplier(Value); + } + + void RequestExit(bool Force) { + FGenericPlatformMisc::RequestExit(Force); + } + } + + namespace ConsoleManager { + bool IsRegisteredVariable(const char* Name) { + return IConsoleManager::Get().IsNameRegistered(UTF8_TO_TCHAR(Name)); + } + + IConsoleVariable* FindVariable(const char* Name) { + return IConsoleManager::Get().FindConsoleVariable(UTF8_TO_TCHAR(Name)); + } + + IConsoleVariable* RegisterVariableBool(const char* Name, const char* Help, bool DefaultValue, bool ReadOnly) { + return IConsoleManager::Get().RegisterConsoleVariable(UTF8_TO_TCHAR(Name), DefaultValue, UTF8_TO_TCHAR(Help), !ReadOnly ? ECVF_Default : ECVF_ReadOnly); + } + + IConsoleVariable* RegisterVariableInt(const char* Name, const char* Help, int32 DefaultValue, bool ReadOnly) { + return IConsoleManager::Get().RegisterConsoleVariable(UTF8_TO_TCHAR(Name), DefaultValue, UTF8_TO_TCHAR(Help), !ReadOnly ? ECVF_Default : ECVF_ReadOnly); + } + + IConsoleVariable* RegisterVariableFloat(const char* Name, const char* Help, float DefaultValue, bool ReadOnly) { + return IConsoleManager::Get().RegisterConsoleVariable(UTF8_TO_TCHAR(Name), DefaultValue, UTF8_TO_TCHAR(Help), !ReadOnly ? ECVF_Default : ECVF_ReadOnly); + } + + IConsoleVariable* RegisterVariableString(const char* Name, const char* Help, const char* DefaultValue, bool ReadOnly) { + return IConsoleManager::Get().RegisterConsoleVariable(UTF8_TO_TCHAR(Name), UTF8_TO_TCHAR(DefaultValue), UTF8_TO_TCHAR(Help), !ReadOnly ? ECVF_Default : ECVF_ReadOnly); + } + + void RegisterCommand(const char* Name, const char* Help, ConsoleCommandDelegate Callback, bool ReadOnly) { + auto callback = [Callback](const TArray& Arguments) { + if (UnrealCLR::Status == UnrealCLR::StatusType::Running) { + float value = 0.0f; + + if (Arguments.Num() > 0) + FDefaultValueHelper::ParseFloat(Arguments[0], value); + + UnrealCLR::ManagedCommand(UnrealCLR::Command((void*)Callback, value)); + } + }; + + IConsoleManager::Get().RegisterConsoleCommand(UTF8_TO_TCHAR(Name), UTF8_TO_TCHAR(Help), FConsoleCommandWithArgsDelegate::CreateLambda(callback), !ReadOnly ? ECVF_Default : ECVF_ReadOnly); + } + + void UnregisterObject(const char* Name) { + IConsoleManager::Get().UnregisterConsoleObject(UTF8_TO_TCHAR(Name), false); + } + } + + namespace Engine { + bool IsSplitScreen() { + return GEngine->IsSplitScreen(UnrealCLR::Engine::World); + } + + bool IsEditor() { + return UnrealCLR::Engine::World->IsPlayInEditor(); + } + + bool IsForegroundWindow() { + return UnrealCLR::Engine::World->GetGameViewport()->Viewport->IsForegroundWindow(); + } + + bool IsExitRequested() { + return IsEngineExitRequested(); + } + + NetMode GetNetMode() { + return UnrealCLR::Engine::World->GetNetMode(); + } + + uint32 GetFrameNumber() { + return GFrameNumber; + } + + void GetViewportSize(Vector2* Value) { + *Value = FVector2D(GEngine->GameViewport->Viewport->GetSizeXY()); + } + + void GetScreenResolution(Vector2* Value) { + *Value = FVector2D(static_cast(GSystemResolution.ResX), static_cast(GSystemResolution.ResY)); + } + + WindowMode GetWindowMode() { + return UnrealCLR::Engine::World->GetGameViewport()->Viewport->GetWindowMode(); + } + + void GetVersion(char* Version) { + const char* version = TCHAR_TO_UTF8(*FEngineVersion::Current().ToString()); + + UnrealCLR::Utility::Strcpy(Version, version, UnrealCLR::Utility::Strlen(version)); + } + + float GetMaxFPS() { + return GEngine->GetMaxFPS(); + } + + void SetMaxFPS(float MaxFPS) { + GEngine->SetMaxFPS(MaxFPS); + } + + void SetTitle(const char* Title) { + UGameEngine* gameEngine = Cast(GEngine); + + if (gameEngine) { + TSharedPtr gameViewportWindow = gameEngine->GameViewportWindow.Pin(); + + if (gameViewportWindow.IsValid()) + gameViewportWindow->SetTitle(FText::FromString(FString(UTF8_TO_TCHAR(Title)))); + } + } + + void AddActionMapping(const char* ActionName, const char* Key, bool Shift, bool Ctrl, bool Alt, bool Cmd) { + UPlayerInput::AddEngineDefinedActionMapping(FInputActionKeyMapping(FName(UTF8_TO_TCHAR(ActionName)), FKey(UTF8_TO_TCHAR(Key)), Shift, Ctrl, Alt, Cmd)); + } + + void AddAxisMapping(const char* AxisName, const char* Key, float Scale) { + UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping(FName(UTF8_TO_TCHAR(AxisName)), FKey(UTF8_TO_TCHAR(Key)), Scale)); + } + + void ForceGarbageCollection(bool FullPurge) { + GEngine->ForceGarbageCollection(FullPurge); + } + + void DelayGarbageCollection() { + GEngine->DelayGarbageCollection(); + } + } + + namespace HeadMountedDisplay { + bool IsConnected() { + return UHeadMountedDisplayFunctionLibrary::IsHeadMountedDisplayConnected(); + } + + bool GetEnabled() { + return UHeadMountedDisplayFunctionLibrary::IsHeadMountedDisplayEnabled(); + } + + bool GetLowPersistenceMode() { + return UHeadMountedDisplayFunctionLibrary::IsInLowPersistenceMode(); + } + + void GetDeviceName(char* Name) { + FName deviceName = UHeadMountedDisplayFunctionLibrary::GetHMDDeviceName(); + + const char* name = TCHAR_TO_UTF8(*deviceName.ToString()); + + UnrealCLR::Utility::Strcpy(Name, name, UnrealCLR::Utility::Strlen(name)); + } + + void SetEnable(bool Value) { + UHeadMountedDisplayFunctionLibrary::EnableHMD(Value); + } + + void SetLowPersistenceMode(bool Value) { + UHeadMountedDisplayFunctionLibrary::EnableLowPersistenceMode(Value); + } + } + + namespace World { + void ForEachActor(AActor** Array, int32* Elements) { + static TArray actors; + + actors.Reset(); + + for (TActorIterator currentActor(UnrealCLR::Engine::World); currentActor; ++currentActor) { + actors.Add(*currentActor); + } + + int32 elements = actors.Num(); + + if (elements > 0) { + *Array = reinterpret_cast(actors.GetData()); + *Elements = elements; + } + } + + int32 GetActorCount() { + return UnrealCLR::Engine::World->GetActorCount(); + } + + float GetDeltaSeconds() { + return UnrealCLR::Engine::World->GetDeltaSeconds(); + } + + float GetRealTimeSeconds() { + return UnrealCLR::Engine::World->GetRealTimeSeconds(); + } + + float GetTimeSeconds() { + return UnrealCLR::Engine::World->GetTimeSeconds(); + } + + void GetCurrentLevelName(char* LevelName) { + FString mapName = UnrealCLR::Engine::World->GetMapName(); + + mapName.RemoveFromStart(UnrealCLR::Engine::World->StreamingLevelsPrefix); + + const char* levelName = TCHAR_TO_UTF8(*mapName); + + UnrealCLR::Utility::Strcpy(LevelName, levelName, UnrealCLR::Utility::Strlen(levelName)); + } + + bool GetSimulatePhysics() { + return UnrealCLR::Engine::World->bShouldSimulatePhysics; + } + + void GetWorldOrigin(Vector3* Value) { + *Value = FVector(UnrealCLR::Engine::World->OriginLocation); + } + + AActor* GetActor(const char* Name, ActorType Type) { + FString name; + AActor* actor = nullptr; + TSubclassOf type; + + if (Name) + name = FString(UTF8_TO_TCHAR(Name)); + + UNREALCLR_GET_ACTOR_TYPE(Type, UNREALCLR_NONE, ::StaticClass(), type); + + for (TActorIterator currentActor(UnrealCLR::Engine::World, type); currentActor; ++currentActor) { + if (!Name || (Name && *currentActor->GetName() == name)) { + actor = *currentActor; + break; + } + } + + return actor; + } + + AActor* GetActorByTag(const char* Tag, ActorType Type) { + AActor* actor = nullptr; + TSubclassOf type; + FName tag(UTF8_TO_TCHAR(Tag)); + + UNREALCLR_GET_ACTOR_TYPE(Type, UNREALCLR_NONE, ::StaticClass(), type); + + for (TActorIterator currentActor(UnrealCLR::Engine::World, type); currentActor; ++currentActor) { + if (currentActor->ActorHasTag(tag)) { + actor = *currentActor; + break; + } + } + + return actor; + } + + AActor* GetActorByID(uint32 ID, ActorType Type) { + AActor* actor = nullptr; + TSubclassOf type; + + UNREALCLR_GET_ACTOR_TYPE(Type, UNREALCLR_NONE, ::StaticClass(), type); + + for (TActorIterator currentActor(UnrealCLR::Engine::World, type); currentActor; ++currentActor) { + if (currentActor->GetUniqueID() == ID) { + actor = *currentActor; + break; + } + } + + return actor; + } + + APlayerController* GetFirstPlayerController() { + return UnrealCLR::Engine::World->GetFirstPlayerController(); + } + + AGameModeBase* GetGameMode() { + return UnrealCLR::Engine::World->GetAuthGameMode(); + } + + void SetOnActorBeginOverlapCallback(ActorOverlapDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnActorBeginOverlap] = (void*)Callback; + } + + void SetOnActorEndOverlapCallback(ActorOverlapDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnActorEndOverlap] = (void*)Callback; + } + + void SetOnActorHitCallback(ActorHitDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnActorHit] = (void*)Callback; + } + + void SetOnActorBeginCursorOverCallback(ActorCursorDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnActorBeginCursorOver] = (void*)Callback; + } + + void SetOnActorEndCursorOverCallback(ActorCursorDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnActorEndCursorOver] = (void*)Callback; + } + + void SetOnActorClickedCallback(ActorKeyDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnActorClicked] = (void*)Callback; + } + + void SetOnActorReleasedCallback(ActorKeyDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnActorReleased] = (void*)Callback; + } + + void SetOnComponentBeginOverlapCallback(ComponentOverlapDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnComponentBeginOverlap] = (void*)Callback; + } + + void SetOnComponentEndOverlapCallback(ComponentOverlapDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnComponentEndOverlap] = (void*)Callback; + } + + void SetOnComponentHitCallback(ComponentHitDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnComponentHit] = (void*)Callback; + } + + void SetOnComponentBeginCursorOverCallback(ComponentCursorDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnComponentBeginCursorOver] = (void*)Callback; + } + + void SetOnComponentEndCursorOverCallback(ComponentCursorDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnComponentEndCursorOver] = (void*)Callback; + } + + void SetOnComponentClickedCallback(ComponentKeyDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnComponentClicked] = (void*)Callback; + } + + void SetOnComponentReleasedCallback(ComponentKeyDelegate Callback) { + UnrealCLR::Shared::Events[UnrealCLR::OnComponentReleased] = (void*)Callback; + } + + void SetSimulatePhysics(bool Value) { + UnrealCLR::Engine::World->bShouldSimulatePhysics = Value; + } + + void SetGravity(float Value) { + AWorldSettings* worldSettings = UnrealCLR::Engine::World->GetWorldSettings(); + + worldSettings->bGlobalGravitySet = true; + worldSettings->GlobalGravityZ = Value; + } + + bool SetWorldOrigin(const Vector3* Value) { + return UnrealCLR::Engine::World->SetNewWorldOrigin(FIntVector(*Value)); + } + + void OpenLevel(const char* LevelName) { + GEngine->SetClientTravel(UnrealCLR::Engine::World, UTF8_TO_TCHAR(LevelName), TRAVEL_Absolute); + } + + bool LineTraceTestByChannel(const Vector3* Start, const Vector3* End, CollisionChannel Channel, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent) { + UNREALCLR_SET_COLLISION_QUERY_PARAMS(IgnoredActor, IgnoredComponent); + + queryParams.bTraceComplex = TraceComplex; + + return UnrealCLR::Engine::World->LineTraceTestByChannel(*Start, *End, Channel, queryParams); + } + + bool LineTraceTestByProfile(const Vector3* Start, const Vector3* End, const char* ProfileName, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent) { + UNREALCLR_SET_COLLISION_QUERY_PARAMS(IgnoredActor, IgnoredComponent); + + queryParams.bTraceComplex = TraceComplex; + + return UnrealCLR::Engine::World->LineTraceTestByProfile(*Start, *End, FName(UTF8_TO_TCHAR(ProfileName)), queryParams); + } + + bool LineTraceSingleByChannel(const Vector3* Start, const Vector3* End, CollisionChannel Channel, Hit* Hit, char* BoneName, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent) { + FHitResult hit; + + UNREALCLR_SET_COLLISION_QUERY_PARAMS(IgnoredActor, IgnoredComponent); + + queryParams.bTraceComplex = TraceComplex; + + bool result = UnrealCLR::Engine::World->LineTraceSingleByChannel(hit, *Start, *End, Channel, queryParams); + + UNREALCLR_GET_BONE_NAME(hit, BoneName); + + *Hit = hit; + + return result; + } + + bool LineTraceSingleByProfile(const Vector3* Start, const Vector3* End, const char* ProfileName, Hit* Hit, char* BoneName, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent) { + FHitResult hit; + + UNREALCLR_SET_COLLISION_QUERY_PARAMS(IgnoredActor, IgnoredComponent); + + queryParams.bTraceComplex = TraceComplex; + + bool result = UnrealCLR::Engine::World->LineTraceSingleByProfile(hit, *Start, *End, FName(UTF8_TO_TCHAR(ProfileName)), queryParams); + + UNREALCLR_GET_BONE_NAME(hit, BoneName); + + *Hit = hit; + + return result; + } + + bool SweepTestByChannel(const Vector3* Start, const Vector3* End, const Quaternion* Rotation, CollisionChannel Channel, const CollisionShape* Shape, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent) { + UNREALCLR_SET_COLLISION_QUERY_PARAMS(IgnoredActor, IgnoredComponent); + + queryParams.bTraceComplex = TraceComplex; + + return UnrealCLR::Engine::World->SweepTestByChannel(*Start, *End, *Rotation, Channel, *Shape, queryParams); + } + + bool SweepTestByProfile(const Vector3* Start, const Vector3* End, const Quaternion* Rotation, const char* ProfileName, const CollisionShape* Shape, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent) { + UNREALCLR_SET_COLLISION_QUERY_PARAMS(IgnoredActor, IgnoredComponent); + + queryParams.bTraceComplex = TraceComplex; + + return UnrealCLR::Engine::World->SweepTestByProfile( *Start, *End, *Rotation, FName(UTF8_TO_TCHAR(ProfileName)), *Shape, queryParams); + } + + bool SweepSingleByChannel(const Vector3* Start, const Vector3* End, const Quaternion* Rotation, CollisionChannel Channel, const CollisionShape* Shape, Hit* Hit, char* BoneName, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent) { + FHitResult hit; + + UNREALCLR_SET_COLLISION_QUERY_PARAMS(IgnoredActor, IgnoredComponent); + + queryParams.bTraceComplex = TraceComplex; + + bool result = UnrealCLR::Engine::World->SweepSingleByChannel(hit, *Start, *End, *Rotation, Channel, *Shape, queryParams); + + UNREALCLR_GET_BONE_NAME(hit, BoneName); + + *Hit = hit; + + return result; + } + + bool SweepSingleByProfile(const Vector3* Start, const Vector3* End, const Quaternion* Rotation, const char* ProfileName, const CollisionShape* Shape, Hit* Hit, char* BoneName, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent) { + FHitResult hit; + + UNREALCLR_SET_COLLISION_QUERY_PARAMS(IgnoredActor, IgnoredComponent); + + queryParams.bTraceComplex = TraceComplex; + + bool result = UnrealCLR::Engine::World->SweepSingleByProfile(hit, *Start, *End, *Rotation, FName(UTF8_TO_TCHAR(ProfileName)), *Shape, queryParams); + + UNREALCLR_GET_BONE_NAME(hit, BoneName); + + *Hit = hit; + + return result; + } + + bool OverlapAnyTestByChannel(const Vector3* Location, const Quaternion* Rotation, CollisionChannel Channel, const CollisionShape* Shape, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent) { + UNREALCLR_SET_COLLISION_QUERY_PARAMS(IgnoredActor, IgnoredComponent); + + return UnrealCLR::Engine::World->OverlapAnyTestByChannel(*Location, *Rotation, Channel, *Shape, queryParams); + } + + bool OverlapAnyTestByProfile(const Vector3* Location, const Quaternion* Rotation, const char* ProfileName, const CollisionShape* Shape, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent) { + UNREALCLR_SET_COLLISION_QUERY_PARAMS(IgnoredActor, IgnoredComponent); + + return UnrealCLR::Engine::World->OverlapAnyTestByProfile(*Location, *Rotation, FName(UTF8_TO_TCHAR(ProfileName)), *Shape, queryParams); + } + + bool OverlapBlockingTestByChannel(const Vector3* Location, const Quaternion* Rotation, CollisionChannel Channel, const CollisionShape* Shape, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent) { + UNREALCLR_SET_COLLISION_QUERY_PARAMS(IgnoredActor, IgnoredComponent); + + return UnrealCLR::Engine::World->OverlapBlockingTestByChannel(*Location, *Rotation, Channel, *Shape, queryParams); + } + + bool OverlapBlockingTestByProfile(const Vector3* Location, const Quaternion* Rotation, const char* ProfileName, const CollisionShape* Shape, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent) { + UNREALCLR_SET_COLLISION_QUERY_PARAMS(IgnoredActor, IgnoredComponent); + + return UnrealCLR::Engine::World->OverlapBlockingTestByProfile(*Location, *Rotation, FName(UTF8_TO_TCHAR(ProfileName)), *Shape, queryParams); + } + } + + namespace ConsoleObject { + bool IsBool(IConsoleObject* ConsoleObject) { + return ConsoleObject->IsVariableBool(); + } + + bool IsInt(IConsoleObject* ConsoleObject) { + return ConsoleObject->IsVariableInt(); + } + + bool IsFloat(IConsoleObject* ConsoleObject) { + return ConsoleObject->IsVariableFloat(); + } + + bool IsString(IConsoleObject* ConsoleObject) { + return ConsoleObject->IsVariableString(); + } + } + + namespace ConsoleVariable { + bool GetBool(IConsoleVariable* ConsoleVariable) { + return ConsoleVariable->GetBool(); + } + + int32 GetInt(IConsoleVariable* ConsoleVariable) { + return ConsoleVariable->GetInt(); + } + + float GetFloat(IConsoleVariable* ConsoleVariable) { + return ConsoleVariable->GetFloat(); + } + + void GetString(IConsoleVariable* ConsoleVariable, char* Value) { + const char* value = TCHAR_TO_UTF8(*ConsoleVariable->GetString()); + + UnrealCLR::Utility::Strcpy(Value, value, UnrealCLR::Utility::Strlen(value)); + } + + void SetBool(IConsoleVariable* ConsoleVariable, bool Value) { + ConsoleVariable->Set(Value); + } + + void SetInt(IConsoleVariable* ConsoleVariable, int32 Value) { + ConsoleVariable->Set(Value); + } + + void SetFloat(IConsoleVariable* ConsoleVariable, float Value) { + ConsoleVariable->Set(Value); + } + + void SetString(IConsoleVariable* ConsoleVariable, const char* Value) { + ConsoleVariable->Set(UTF8_TO_TCHAR(Value)); + } + + void SetOnChangedCallback(IConsoleVariable* ConsoleVariable, ConsoleVariableDelegate Callback) { + auto callback = [Callback](IConsoleVariable* ConsoleVariable) { + UnrealCLR::ManagedCommand(UnrealCLR::Command((void*)Callback)); + }; + + ConsoleVariable->SetOnChangedCallback(FConsoleVariableDelegate::CreateLambda(callback)); + } + + void ClearOnChangedCallback(IConsoleVariable* ConsoleVariable) { + FConsoleVariableDelegate emptyDelegate; + + ConsoleVariable->SetOnChangedCallback(emptyDelegate); + } + } + + namespace Actor { + bool IsPendingKill(AActor* Actor) { + return Actor->IsPendingKillPending(); + } + + bool IsRootComponentMovable(AActor* Actor) { + return Actor->IsRootComponentMovable(); + } + + bool IsOverlappingActor(AActor* Actor, AActor* Other) { + return Actor->IsOverlappingActor(Other); + } + + void ForEachComponent(AActor* Actor, UActorComponent** Array, int32* Elements) { + static TArray components; + + components.Reset(); + + Actor->GetComponents(components); + + int32 elements = components.Num(); + + if (elements > 0) { + *Array = reinterpret_cast(components.GetData()); + *Elements = elements; + } + } + + void ForEachAttachedActor(AActor* Actor, AActor** Array, int32* Elements) { + static TArray actors; + + actors.Reset(); + + Actor->GetAttachedActors(actors); + + int32 elements = actors.Num(); + + if (elements > 0) { + *Array = reinterpret_cast(actors.GetData()); + *Elements = elements; + } + } + + void ForEachChildActor(AActor* Actor, AActor** Array, int32* Elements) { + static TArray actors; + + actors.Reset(); + + Actor->GetAllChildActors(actors); + + int32 elements = actors.Num(); + + if (elements > 0) { + *Array = reinterpret_cast(actors.GetData()); + *Elements = elements; + } + } + + void ForEachOverlappingActor(AActor* Actor, AActor** Array, int32* Elements) { + static TArray overlappingActors; + + overlappingActors.Reset(); + + Actor->GetOverlappingActors(overlappingActors); + + int32 elements = overlappingActors.Num(); + + if (elements > 0) { + *Array = reinterpret_cast(overlappingActors.GetData()); + *Elements = elements; + } + } + + AActor* Spawn(const char* Name, ActorType Type, UObject* Blueprint) { + AActor* actor = nullptr; + + if (!Blueprint) { + UNREALCLR_GET_ACTOR_TYPE(Type, UnrealCLR::Engine::World->SpawnActor UNREALCLR_BRACKET_LEFT, ::StaticClass() UNREALCLR_BRACKET_RIGHT, actor); + } else { + #if !WITH_EDITOR + UNREALCLR_GET_ACTOR_TYPE(Type, UnrealCLR::Engine::World->SpawnActor<, >(Cast(Blueprint)), actor); + #else + UNREALCLR_GET_ACTOR_TYPE(Type, UnrealCLR::Engine::World->SpawnActor<, >(Cast(Blueprint)->GeneratedClass), actor); + #endif + } + + if (actor && Name) { + FString name(UTF8_TO_TCHAR(Name)); + + actor->Rename(*name); + + #if WITH_EDITOR + actor->SetActorLabel(*name); + #endif + } + + return actor; + } + + bool Destroy(AActor* Actor) { + return UnrealCLR::Engine::World->DestroyActor(Actor); + } + + void Rename(AActor* Actor, const char* Name) { + FString name(UTF8_TO_TCHAR(Name)); + + Actor->Rename(*name); + + #if WITH_EDITOR + Actor->SetActorLabel(*name); + #endif + } + + void Hide(AActor* Actor, bool Value) { + Actor->SetActorHiddenInGame(Value); + } + + bool TeleportTo(AActor* Actor, const Vector3* DestinationLocation, const Quaternion* DestinationRotation, bool IsATest, bool NoCheck) { + return Actor->TeleportTo(*DestinationLocation, FRotator(*DestinationRotation), IsATest, NoCheck); + } + + UActorComponent* GetComponent(AActor* Actor, const char* Name, ComponentType Type) { + FString name; + UActorComponent* component = nullptr; + TSubclassOf type; + + if (Name) + name = FString(UTF8_TO_TCHAR(Name)); + + UNREALCLR_GET_COMPONENT_TYPE(Type, UNREALCLR_NONE, ::StaticClass(), type); + + for (UActorComponent* currentComponent : Actor->GetComponents()) { + if (currentComponent && currentComponent->IsA(type) && (!Name || (Name && *currentComponent->GetName() == name))) { + component = currentComponent; + break; + } + } + + return component; + } + + UActorComponent* GetComponentByTag(AActor* Actor, const char* Tag, ComponentType Type) { + UActorComponent* component = nullptr; + TSubclassOf type; + FName tag(UTF8_TO_TCHAR(Tag)); + + UNREALCLR_GET_COMPONENT_TYPE(Type, UNREALCLR_NONE, ::StaticClass(), type); + + for (UActorComponent* currentComponent : Actor->GetComponents()) { + if (currentComponent->ComponentHasTag(tag)) { + component = currentComponent; + break; + } + } + + return component; + } + + UActorComponent* GetComponentByID(AActor* Actor, uint32 ID, ComponentType Type) { + UActorComponent* component = nullptr; + TSubclassOf type; + + UNREALCLR_GET_COMPONENT_TYPE(Type, UNREALCLR_NONE, ::StaticClass(), type); + + for (UActorComponent* currentComponent : Actor->GetComponents()) { + if (currentComponent->GetUniqueID() == ID) { + component = currentComponent; + break; + } + } + + return component; + } + + USceneComponent* GetRootComponent(AActor* Actor, ComponentType Type) { + USceneComponent* component = nullptr; + USceneComponent* rootComponent = Actor->GetRootComponent(); + TSubclassOf type; + + UNREALCLR_GET_MOVABLE_COMPONENT_TYPE(Type, UNREALCLR_NONE, ::StaticClass(), type); + + if (rootComponent->IsA(type)) + component = rootComponent; + + return component; + } + + UInputComponent* GetInputComponent(AActor* Actor) { + return Actor->InputComponent; + } + + float GetCreationTime(AActor* Actor) { + return Actor->CreationTime; + } + + bool GetBlockInput(AActor* Actor) { + return Actor->bBlockInput; + } + + float GetDistanceTo(AActor* Actor, AActor* Other) { + return Actor->GetDistanceTo(Other); + } + + float GetHorizontalDistanceTo(AActor* Actor, AActor* Other) { + return Actor->GetDistanceTo(Other); + } + + void GetBounds(AActor* Actor, bool OnlyCollidingComponents, Vector3* Origin, Vector3* Extent) { + FVector origin, extent; + + Actor->GetActorBounds(OnlyCollidingComponents, origin, extent); + + *Origin = origin; + *Extent = extent; + } + + void GetEyesViewPoint(AActor* Actor, Vector3* Location, Quaternion* Rotation) { + FVector location; + FRotator rotation; + + Actor->GetActorEyesViewPoint(location, rotation); + + *Location = location; + *Rotation = rotation.Quaternion(); + } + + bool SetRootComponent(AActor* Actor, USceneComponent* RootComponent) { + return Actor->SetRootComponent(RootComponent); + } + + void SetInputComponent(AActor* Actor, UInputComponent* InputComponent) { + Actor->InputComponent = InputComponent; + } + + void SetBlockInput(AActor* Actor, bool Value) { + Actor->bBlockInput = Value; + } + + void SetLifeSpan(AActor* Actor, float LifeSpan) { + Actor->SetLifeSpan(LifeSpan); + } + + void SetEnableInput(AActor* Actor, APlayerController* PlayerController, bool Value) { + if (Value) + Actor->EnableInput(PlayerController); + else + Actor->DisableInput(PlayerController); + } + + void SetEnableCollision(AActor* Actor, bool Value) { + Actor->SetActorEnableCollision(Value); + } + + void AddTag(AActor* Actor, const char* Tag) { + Actor->Tags.AddUnique(FName(UTF8_TO_TCHAR(Tag))); + } + + void RemoveTag(AActor* Actor, const char* Tag) { + Actor->Tags.Remove(FName(UTF8_TO_TCHAR(Tag))); + } + + bool HasTag(AActor* Actor, const char* Tag) { + return Actor->ActorHasTag(FName(UTF8_TO_TCHAR(Tag))); + } + + void RegisterEvent(AActor* Actor, ActorEventType Type) { + UNREALCLR_SET_ACTOR_EVENT(Type, !, AddDynamic); + } + + void UnregisterEvent(AActor* Actor, ActorEventType Type) { + UNREALCLR_SET_ACTOR_EVENT(Type, UNREALCLR_NONE, RemoveDynamic); + } + } + + namespace GameModeBase { + bool GetUseSeamlessTravel(AGameModeBase* GameModeBase) { + return GameModeBase->bUseSeamlessTravel; + } + + void SetUseSeamlessTravel(AGameModeBase* GameModeBase, bool Value) { + GameModeBase->bUseSeamlessTravel = Value; + } + + void SwapPlayerControllers(AGameModeBase* GameModeBase, APlayerController* PlayerController, APlayerController* NewPlayerController) { + GameModeBase->SwapPlayerControllers(PlayerController, NewPlayerController); + } + } + + namespace Pawn { + bool IsControlled(APawn* Pawn) { + return Pawn->IsPawnControlled(); + } + + bool IsPlayerControlled(APawn* Pawn) { + return Pawn->IsPlayerControlled(); + } + + AutoPossessAI GetAutoPossessAI(APawn* Pawn) { + return Pawn->AutoPossessAI; + } + + AutoReceiveInput GetAutoPossessPlayer(APawn* Pawn) { + return Pawn->AutoPossessPlayer; + } + + bool GetUseControllerRotationYaw(APawn* Pawn) { + return Pawn->bUseControllerRotationYaw; + } + + bool GetUseControllerRotationPitch(APawn* Pawn) { + return Pawn->bUseControllerRotationPitch; + } + + bool GetUseControllerRotationRoll(APawn* Pawn) { + return Pawn->bUseControllerRotationRoll; + } + + void GetGravityDirection(APawn* Pawn, Vector3* Value) { + *Value = Pawn->GetGravityDirection(); + } + + AAIController* GetAIController(APawn* Pawn) { + return Cast(Pawn->GetController()); + } + + APlayerController* GetPlayerController(APawn* Pawn) { + return Cast(Pawn->GetController()); + } + + void SetAutoPossessAI(APawn* Pawn, AutoPossessAI Value) { + Pawn->AutoPossessAI = Value; + } + + void SetAutoPossessPlayer(APawn* Pawn, AutoReceiveInput Value) { + Pawn->AutoPossessPlayer = Value; + } + + void SetUseControllerRotationYaw(APawn* Pawn, bool Value) { + Pawn->bUseControllerRotationYaw = Value; + } + + void SetUseControllerRotationPitch(APawn* Pawn, bool Value) { + Pawn->bUseControllerRotationPitch = Value; + } + + void SetUseControllerRotationRoll(APawn* Pawn, bool Value) { + Pawn->bUseControllerRotationRoll = Value; + } + + void AddControllerYawInput(APawn* Pawn, float Value) { + Pawn->AddControllerYawInput(Value); + } + + void AddControllerPitchInput(APawn* Pawn, float Value) { + Pawn->AddControllerPitchInput(Value); + } + + void AddControllerRollInput(APawn* Pawn, float Value) { + Pawn->AddControllerRollInput(Value); + } + + void AddMovementInput(APawn* Pawn, const Vector3* WorldDirection, float ScaleValue, bool Force) { + Pawn->AddMovementInput(*WorldDirection, ScaleValue, Force); + } + } + + namespace Character { + bool IsCrouched(ACharacter* Character) { + return Character->bIsCrouched; + } + + bool CanCrouch(ACharacter* Character) { + return Character->CanCrouch(); + } + + bool CanJump(ACharacter* Character) { + return Character->CanJump(); + } + + void CheckJumpInput(ACharacter* Character, float DeltaTime) { + Character->CheckJumpInput(DeltaTime); + } + + void ClearJumpInput(ACharacter* Character, float DeltaTime) { + Character->ClearJumpInput(DeltaTime); + } + + void Launch(ACharacter* Character, const Vector3* Velocity, bool OverrideXY, bool OverrideZ) { + Character->LaunchCharacter(*Velocity, OverrideXY, OverrideZ); + } + + void Crouch(ACharacter* Character) { + Character->Crouch(); + } + + void StopCrouching(ACharacter* Character) { + Character->UnCrouch(); + } + + void Jump(ACharacter* Character) { + Character->Jump(); + } + + void StopJumping(ACharacter* Character) { + Character->StopJumping(); + } + + void SetOnLandedCallback(ACharacter* Character, CharacterLandedDelegate Callback) { + UUnrealCLRCharacter* character = NewObject(Character); + + character->LandedCallback = (void*)Callback; + + Character->LandedDelegate.AddDynamic(character, &UUnrealCLRCharacter::Landed); + } + } + + namespace Controller { + bool IsLookInputIgnored(AController* Controller) { + return Controller->IsLookInputIgnored(); + } + + bool IsMoveInputIgnored(AController* Controller) { + return Controller->IsMoveInputIgnored(); + } + + bool IsPlayerController(AController* Controller) { + return Controller->IsPlayerController(); + } + + APawn* GetPawn(AController* Controller) { + return Controller->GetPawn(); + } + + ACharacter* GetCharacter(AController* Controller) { + return Controller->GetCharacter(); + } + + AActor* GetViewTarget(AController* Controller) { + return Controller->GetViewTarget(); + } + + void GetControlRotation(AController* Controller, Quaternion* Value) { + *Value = Controller->GetControlRotation().Quaternion(); + } + + void GetDesiredRotation(AController* Controller, Quaternion* Value) { + *Value = Controller->GetDesiredRotation().Quaternion(); + } + + bool LineOfSightTo(AController* Controller, AActor* Actor, const Vector3* ViewPoint, bool AlternateChecks) { + return Controller->LineOfSightTo(Actor, *ViewPoint, AlternateChecks); + } + + void SetControlRotation(AController* Controller, const Quaternion* Value) { + Controller->SetControlRotation(FRotator(*Value)); + } + + void SetInitialLocationAndRotation(AController* Controller, const Vector3* NewLocation, const Quaternion* NewRotation) { + Controller->SetInitialLocationAndRotation(*NewLocation, FRotator(*NewRotation)); + } + + void SetIgnoreLookInput(AController* Controller, bool Value) { + Controller->SetIgnoreLookInput(Value); + } + + void SetIgnoreMoveInput(AController* Controller, bool Value) { + Controller->SetIgnoreMoveInput(Value); + } + + void ResetIgnoreLookInput(AController* Controller) { + Controller->ResetIgnoreLookInput(); + } + + void ResetIgnoreMoveInput(AController* Controller) { + Controller->ResetIgnoreMoveInput(); + } + + void Possess(AController* Controller, APawn* Pawn) { + Controller->Possess(Pawn); + } + + void Unpossess(AController* Controller) { + Controller->UnPossess(); + } + } + + namespace AIController { + void ClearFocus(AAIController* AIController, AIFocusPriority Priority) { + AIController->ClearFocus(static_cast(Priority)); + } + + void GetFocalPoint(AAIController* AIController, Vector3* Value) { + *Value = AIController->GetFocalPoint(); + } + + void SetFocalPoint(AAIController* AIController, const Vector3* NewFocus, AIFocusPriority Priority) { + AIController->SetFocalPoint(*NewFocus, static_cast(Priority)); + } + + AActor* GetFocusActor(AAIController* AIController) { + return AIController->GetFocusActor(); + } + + bool GetAllowStrafe(AAIController* AIController) { + return AIController->bAllowStrafe; + } + + void SetAllowStrafe(AAIController* AIController, bool Value) { + AIController->bAllowStrafe = Value; + } + + void SetFocus(AAIController* AIController, AActor* NewFocus, AIFocusPriority Priority) { + AIController->SetFocus(NewFocus, static_cast(Priority)); + } + } + + namespace PlayerController { + bool IsPaused(APlayerController* PlayerController) { + return PlayerController->IsPaused(); + } + + bool GetShowMouseCursor(APlayerController* PlayerController) { + return PlayerController->bShowMouseCursor; + } + + bool GetEnableClickEvents(APlayerController* PlayerController) { + return PlayerController->bEnableClickEvents; + } + + bool GetEnableMouseOverEvents(APlayerController* PlayerController) { + return PlayerController->bEnableMouseOverEvents; + } + + bool GetMousePosition(APlayerController* PlayerController, float* X, float* Y) { + return PlayerController->GetMousePosition(*X, *Y); + } + + UPlayer* GetPlayer(APlayerController* PlayerController) { + return PlayerController->Player; + } + + UPlayerInput* GetPlayerInput(APlayerController* PlayerController) { + return PlayerController->PlayerInput; + } + + bool GetHitResultAtScreenPosition(APlayerController* PlayerController, const Vector2* ScreenPosition, CollisionChannel TraceChannel, Hit* Hit, bool TraceComplex) { + FHitResult hit; + + bool result = PlayerController->GetHitResultAtScreenPosition(*ScreenPosition, TraceChannel, TraceComplex, hit); + + *Hit = hit; + + return result; + } + + bool GetHitResultUnderCursor(APlayerController* PlayerController, CollisionChannel TraceChannel, Hit* Hit, bool TraceComplex) { + FHitResult hit; + + bool result = PlayerController->GetHitResultUnderCursor(TraceChannel, TraceComplex, hit); + + *Hit = hit; + + return result; + } + + void SetShowMouseCursor(APlayerController* PlayerController, bool Value) { + PlayerController->bShowMouseCursor = Value; + } + + void SetEnableClickEvents(APlayerController* PlayerController, bool Value) { + PlayerController->bEnableClickEvents = Value; + } + + void SetEnableMouseOverEvents(APlayerController* PlayerController, bool Value) { + PlayerController->bEnableMouseOverEvents = Value; + } + + void SetMousePosition(APlayerController* PlayerController, float X, float Y) { + PlayerController->SetMouseLocation(static_cast(X), static_cast(Y)); + } + + void ConsoleCommand(APlayerController* PlayerController, const char* Command, bool WriteToLog) { + PlayerController->ConsoleCommand(FString(UTF8_TO_TCHAR(Command)), WriteToLog); + } + + bool SetPause(APlayerController* PlayerController, bool Value) { + return PlayerController->SetPause(Value); + } + + void SetViewTarget(APlayerController* PlayerController, AActor* NewViewTarget) { + PlayerController->SetViewTarget(NewViewTarget); + } + + void SetViewTargetWithBlend(APlayerController* PlayerController, AActor* NewViewTarget, float Time, float Exponent, BlendType Type, bool LockOutgoing) { + PlayerController->SetViewTargetWithBlend(NewViewTarget, Time, Type, Exponent, LockOutgoing); + } + + void AddYawInput(APlayerController* PlayerController, float Value) { + PlayerController->AddYawInput(Value); + } + + void AddPitchInput(APlayerController* PlayerController, float Value) { + PlayerController->AddPitchInput(Value); + } + + void AddRollInput(APlayerController* PlayerController, float Value) { + PlayerController->AddRollInput(Value); + } + } + + namespace Volume { + bool EncompassesPoint(AVolume* Volume, const Vector3* Point, float SphereRadius, float* DistanceToPoint) { + return Volume->EncompassesPoint(*Point, SphereRadius, DistanceToPoint); + } + } + + namespace PostProcessVolume { + bool GetEnabled(APostProcessVolume* PostProcessVolume) { + return PostProcessVolume->bEnabled; + } + + float GetBlendRadius(APostProcessVolume* PostProcessVolume) { + return PostProcessVolume->BlendRadius; + } + + float GetBlendWeight(APostProcessVolume* PostProcessVolume) { + return PostProcessVolume->BlendWeight; + } + + bool GetUnbound(APostProcessVolume* PostProcessVolume) { + return PostProcessVolume->bUnbound; + } + + float GetPriority(APostProcessVolume* PostProcessVolume) { + return PostProcessVolume->Priority; + } + + void SetEnabled(APostProcessVolume* PostProcessVolume, bool Value) { + PostProcessVolume->bEnabled = Value; + } + + void SetBlendRadius(APostProcessVolume* PostProcessVolume, float Value) { + PostProcessVolume->BlendRadius = Value; + } + + void SetBlendWeight(APostProcessVolume* PostProcessVolume, float Value) { + PostProcessVolume->BlendWeight = Value; + } + + void SetUnbound(APostProcessVolume* PostProcessVolume, bool Value) { + PostProcessVolume->bUnbound = Value; + } + + void SetPriority(APostProcessVolume* PostProcessVolume, float Priority) { + PostProcessVolume->Priority = Priority; + } + } + + namespace SoundBase { + float GetDuration(USoundBase* SoundBase) { + return SoundBase->Duration; + } + } + + namespace SoundWave { + bool GetLoop(USoundWave* SoundWave) { + return SoundWave->bLooping; + } + + void SetLoop(USoundWave* SoundWave, bool Value) { + SoundWave->bLooping = Value; + } + } + + namespace AnimationInstance { + UAnimMontage* GetCurrentActiveMontage(UAnimInstance* AnimationInstance) { + return AnimationInstance->GetCurrentActiveMontage(); + } + + bool IsPlaying(UAnimInstance* AnimationInstance, UAnimMontage* Montage) { + return AnimationInstance->Montage_IsPlaying(Montage); + } + + float GetPlayRate(UAnimInstance* AnimationInstance, UAnimMontage* Montage) { + return AnimationInstance->Montage_GetPlayRate(Montage); + } + + float GetPosition(UAnimInstance* AnimationInstance, UAnimMontage* Montage) { + return AnimationInstance->Montage_GetPosition(Montage); + } + + float GetBlendTime(UAnimInstance* AnimationInstance, UAnimMontage* Montage) { + return AnimationInstance->Montage_GetBlendTime(Montage); + } + + void GetCurrentSection(UAnimInstance* AnimationInstance, UAnimMontage* Montage, char* SectionName) { + const char* sectionName = TCHAR_TO_UTF8(*AnimationInstance->Montage_GetCurrentSection(Montage).ToString()); + + UnrealCLR::Utility::Strcpy(SectionName, sectionName, UnrealCLR::Utility::Strlen(sectionName)); + } + + void SetPlayRate(UAnimInstance* AnimationInstance, UAnimMontage* Montage, float Value) { + AnimationInstance->Montage_SetPlayRate(Montage, Value); + } + + void SetPosition(UAnimInstance* AnimationInstance, UAnimMontage* Montage, float Position) { + AnimationInstance->Montage_SetPosition(Montage, Position); + } + + void SetNextSection(UAnimInstance* AnimationInstance, UAnimMontage* Montage, const char* SectionToChange, const char* NextSection) { + AnimationInstance->Montage_SetNextSection(FName(UTF8_TO_TCHAR(SectionToChange)), FName(UTF8_TO_TCHAR(NextSection)), Montage); + } + + float PlayMontage(UAnimInstance* AnimationInstance, UAnimMontage* Montage, float PlayRate, float TimeToStartMontageAt, bool StopAllMontages) { + return AnimationInstance->Montage_Play(Montage, PlayRate, EMontagePlayReturnType::MontageLength, TimeToStartMontageAt, StopAllMontages); + } + + void PauseMontage(UAnimInstance* AnimationInstance, UAnimMontage* Montage) { + AnimationInstance->Montage_Pause(Montage); + } + + void ResumeMontage(UAnimInstance* AnimationInstance, UAnimMontage* Montage) { + AnimationInstance->Montage_Resume(Montage); + } + + void StopMontage(UAnimInstance* AnimationInstance, UAnimMontage* Montage, float BlendOutTime) { + AnimationInstance->Montage_Stop(BlendOutTime, Montage); + } + + void JumpToSection(UAnimInstance* AnimationInstance, UAnimMontage* Montage, const char* SectionName) { + AnimationInstance->Montage_JumpToSection(FName(UTF8_TO_TCHAR(SectionName)), Montage); + } + + void JumpToSectionsEnd(UAnimInstance* AnimationInstance, UAnimMontage* Montage, const char* SectionName) { + AnimationInstance->Montage_JumpToSectionsEnd(FName(UTF8_TO_TCHAR(SectionName)), Montage); + } + } + + namespace Player { + APlayerController* GetPlayerController(UPlayer* Player) { + return Player->GetPlayerController(UnrealCLR::Engine::World); + } + } + + namespace PlayerInput { + bool IsKeyPressed(UPlayerInput* PlayerInput, const char* Key) { + return PlayerInput->IsPressed(FKey(UTF8_TO_TCHAR(Key))); + } + + float GetTimeKeyPressed(UPlayerInput* PlayerInput, const char* Key) { + return PlayerInput->GetTimeDown(FKey(UTF8_TO_TCHAR(Key))); + } + + void GetMouseSensitivity(UPlayerInput* PlayerInput, Vector2* Value) { + Value->X = PlayerInput->GetMouseSensitivityX(); + Value->Y = PlayerInput->GetMouseSensitivityY(); + } + + void SetMouseSensitivity(UPlayerInput* PlayerInput, const Vector2* Value) { + PlayerInput->SetMouseSensitivity(Value->X, Value->Y); + } + + void AddActionMapping(UPlayerInput* PlayerInput, const char* ActionName, const char* Key, bool Shift, bool Ctrl, bool Alt, bool Cmd) { + PlayerInput->AddActionMapping(FInputActionKeyMapping(FName(UTF8_TO_TCHAR(ActionName)), FKey(UTF8_TO_TCHAR(Key)), Shift, Ctrl, Alt, Cmd)); + } + + void AddAxisMapping(UPlayerInput* PlayerInput, const char* AxisName, const char* Key, float Scale) { + PlayerInput->AddAxisMapping(FInputAxisKeyMapping(FName(UTF8_TO_TCHAR(AxisName)), FKey(UTF8_TO_TCHAR(Key)), Scale)); + } + + void RemoveActionMapping(UPlayerInput* PlayerInput, const char* ActionName, const char* Key) { + PlayerInput->RemoveActionMapping(FInputActionKeyMapping(FName(UTF8_TO_TCHAR(ActionName)), FKey(UTF8_TO_TCHAR(Key)))); + } + + void RemoveAxisMapping(UPlayerInput* PlayerInput, const char* AxisName, const char* Key) { + PlayerInput->RemoveAxisMapping(FInputAxisKeyMapping(FName(UTF8_TO_TCHAR(AxisName)), FKey(UTF8_TO_TCHAR(Key)))); + } + } + + namespace Font { + void GetStringSize(UFont* Font, const char* Text, int32* Height, int32* Width) { + int32 height, width; + + Font->GetStringHeightAndWidth(UTF8_TO_TCHAR(Text), height, width); + + *Height = height; + *Width = width; + } + } + + namespace Texture2D { + UTexture2D* CreateFromFile(const char* FilePath) { + return FImageUtils::ImportFileAsTexture2D(FString(UTF8_TO_TCHAR(FilePath))); + } + + UTexture2D* CreateFromBuffer(const uint8* Buffer, int32 Length) { + return FImageUtils::ImportBufferAsTexture2D(TArray(Buffer, Length)); + } + + bool HasAlphaChannel(UTexture2D* Texture2D) { + return Texture2D->HasAlphaChannel(); + } + + void GetSize(UTexture2D* Texture2D, Vector2* Value) { + Value->X = Texture2D->GetSizeX(); + Value->Y = Texture2D->GetSizeY(); + } + + PixelFormat GetPixelFormat(UTexture2D* Texture2D) { + return Texture2D->GetPixelFormat(); + } + } + + namespace ActorComponent { + bool IsOwnerSelected(UActorComponent* ActorComponent) { + return ActorComponent->IsOwnerSelected(); + } + + AActor* GetOwner(UActorComponent* ActorComponent, ActorType Type) { + AActor* actor = nullptr; + + UNREALCLR_GET_ACTOR_TYPE(Type, Cast<, >(ActorComponent->GetOwner()), actor); + + return actor; + } + + void Destroy(UActorComponent* ActorComponent, bool PromoteChild) { + ActorComponent->DestroyComponent(PromoteChild); + } + + void AddTag(UActorComponent* ActorComponent, const char* Tag) { + ActorComponent->ComponentTags.AddUnique(FName(UTF8_TO_TCHAR(Tag))); + } + + void RemoveTag(UActorComponent* ActorComponent, const char* Tag) { + ActorComponent->ComponentTags.Remove(FName(UTF8_TO_TCHAR(Tag))); + } + + bool HasTag(UActorComponent* ActorComponent, const char* Tag) { + return ActorComponent->ComponentHasTag(FName(UTF8_TO_TCHAR(Tag))); + } + } + + namespace InputComponent { + bool HasBindings(UInputComponent* InputComponent) { + return InputComponent->HasBindings(); + } + + int32 GetActionBindingsNumber(UInputComponent* InputComponent) { + return InputComponent->GetNumActionBindings(); + } + + void ClearActionBindings(UInputComponent* InputComponent) { + InputComponent->ClearActionBindings(); + } + + void BindAction(UInputComponent* InputComponent, const char* ActionName, InputEvent KeyEvent, bool ExecutedWhenPaused, InputDelegate Callback) { + FInputActionBinding actionBinding(FName(UTF8_TO_TCHAR(ActionName)), KeyEvent); + + actionBinding.bExecuteWhenPaused = ExecutedWhenPaused; + actionBinding.ActionDelegate.GetDelegateForManualSet().BindLambda([Callback]() { + UnrealCLR::ManagedCommand(UnrealCLR::Command((void*)Callback)); + }); + + InputComponent->AddActionBinding(actionBinding); + } + + void BindAxis(UInputComponent* InputComponent, const char* AxisName, bool ExecutedWhenPaused, InputAxisDelegate Callback) { + FInputAxisBinding axisBinding(FName(UTF8_TO_TCHAR(AxisName))); + + axisBinding.bExecuteWhenPaused = ExecutedWhenPaused; + axisBinding.AxisDelegate.GetDelegateForManualSet().BindLambda([Callback](float AxisValue) { + UnrealCLR::ManagedCommand(UnrealCLR::Command((void*)Callback, AxisValue)); + }); + + InputComponent->AxisBindings.Emplace(axisBinding); + } + + void RemoveActionBinding(UInputComponent* InputComponent, const char* ActionName, InputEvent KeyEvent) { + InputComponent->RemoveActionBinding(FName(UTF8_TO_TCHAR(ActionName)), KeyEvent); + } + + bool GetBlockInput(UInputComponent* InputComponent) { + return InputComponent->bBlockInput; + } + + void SetBlockInput(UInputComponent* InputComponent, bool Value) { + InputComponent->bBlockInput = Value; + } + + int32 GetPriority(UInputComponent* InputComponent) { + return InputComponent->Priority; + } + + void SetPriority(UInputComponent* InputComponent, int32 Value) { + InputComponent->Priority = Value; + } + } + + namespace MovementComponent { + bool GetConstrainToPlane(UMovementComponent* MovementComponent) { + return MovementComponent->bConstrainToPlane; + } + + bool GetSnapToPlaneAtStart(UMovementComponent* MovementComponent) { + return MovementComponent->bSnapToPlaneAtStart; + } + + bool GetUpdateOnlyIfRendered(UMovementComponent* MovementComponent) { + return MovementComponent->bUpdateOnlyIfRendered; + } + + void GetVelocity(UMovementComponent* MovementComponent, Vector3* Value) { + *Value = MovementComponent->Velocity; + } + + PlaneConstraintAxis GetPlaneConstraint(UMovementComponent* MovementComponent) { + return MovementComponent->GetPlaneConstraintAxisSetting(); + } + + void GetPlaneConstraintNormal(UMovementComponent* MovementComponent, Vector3* Value) { + *Value = MovementComponent->GetPlaneConstraintNormal(); + } + + void GetPlaneConstraintOrigin(UMovementComponent* MovementComponent, Vector3* Value) { + *Value = MovementComponent->GetPlaneConstraintOrigin(); + } + + float GetGravity(UMovementComponent* MovementComponent) { + return MovementComponent->GetGravityZ(); + } + + float GetMaxSpeed(UMovementComponent* MovementComponent) { + return MovementComponent->GetMaxSpeed(); + } + + void SetConstrainToPlane(UMovementComponent* MovementComponent, bool Value) { + MovementComponent->bConstrainToPlane = Value; + } + + void SetSnapToPlaneAtStart(UMovementComponent* MovementComponent, bool Value) { + MovementComponent->bSnapToPlaneAtStart = Value; + } + + void SetUpdateOnlyIfRendered(UMovementComponent* MovementComponent, bool Value) { + MovementComponent->bUpdateOnlyIfRendered = Value; + } + + void SetVelocity(UMovementComponent* MovementComponent, const Vector3* Value) { + MovementComponent->Velocity = *Value; + } + + void SetPlaneConstraint(UMovementComponent* MovementComponent, PlaneConstraintAxis Value) { + MovementComponent->SetPlaneConstraintAxisSetting(Value); + } + + void SetPlaneConstraintNormal(UMovementComponent* MovementComponent, const Vector3* Value) { + MovementComponent->SetPlaneConstraintNormal(*Value); + } + + void SetPlaneConstraintOrigin(UMovementComponent* MovementComponent, const Vector3* Value) { + MovementComponent->SetPlaneConstraintOrigin(*Value); + } + + void SetPlaneConstraintFromVectors(UMovementComponent* MovementComponent, const Vector3* Forward, const Vector3* Up) { + MovementComponent->SetPlaneConstraintFromVectors(*Forward, *Up); + } + + bool IsExceedingMaxSpeed(UMovementComponent* MovementComponent, float MaxSpeed) { + return MovementComponent->IsExceedingMaxSpeed(MaxSpeed); + } + + bool IsInWater(UMovementComponent* MovementComponent) { + return MovementComponent->IsInWater(); + } + + void StopMovement(UMovementComponent* MovementComponent) { + MovementComponent->StopMovementImmediately(); + } + + void ConstrainDirectionToPlane(UMovementComponent* MovementComponent, const Vector3* Direction, Vector3* Value) { + *Value = MovementComponent->ConstrainDirectionToPlane(*Direction); + } + + void ConstrainLocationToPlane(UMovementComponent* MovementComponent, const Vector3* Location, Vector3* Value) { + *Value = MovementComponent->ConstrainLocationToPlane(*Location); + } + + void ConstrainNormalToPlane(UMovementComponent* MovementComponent, const Vector3* Normal, Vector3* Value) { + *Value = MovementComponent->ConstrainNormalToPlane(*Normal); + } + } + + namespace RotatingMovementComponent { + URotatingMovementComponent* Create(AActor* Actor, const char* Name) { + URotatingMovementComponent* component = NewObject(Actor); + + UNREALCLR_SET_COMPONENT_INSTANCE(component, Name); + + return component; + } + + bool GetRotationInLocalSpace(URotatingMovementComponent* RotatingMovementComponent) { + return RotatingMovementComponent->bRotationInLocalSpace; + } + + void GetPivotTranslation(URotatingMovementComponent* RotatingMovementComponent, Vector3* Value) { + *Value = RotatingMovementComponent->PivotTranslation; + } + + void GetRotationRate(URotatingMovementComponent* RotatingMovementComponent, Quaternion* Value) { + *Value = RotatingMovementComponent->RotationRate.Quaternion(); + } + + void SetRotationInLocalSpace(URotatingMovementComponent* RotatingMovementComponent, bool Value) { + RotatingMovementComponent->bRotationInLocalSpace = Value; + } + + void SetPivotTranslation(URotatingMovementComponent* RotatingMovementComponent, const Vector3* Value) { + RotatingMovementComponent->PivotTranslation = *Value; + } + + void SetRotationRate(URotatingMovementComponent* RotatingMovementComponent, const Quaternion* Value) { + RotatingMovementComponent->RotationRate = FRotator(*Value); + } + } + + namespace SceneComponent { + bool IsAttachedToComponent(USceneComponent* SceneComponent, USceneComponent* Component) { + return SceneComponent->IsAttachedTo(Component); + } + + bool IsAttachedToActor(USceneComponent* SceneComponent, AActor* Actor) { + USceneComponent* Component = SceneComponent; + + while (Component) { + if (Component->GetOwner() == Actor) + return true; + + Component = Component->GetAttachParent(); + } + + return false; + } + + bool IsVisible(USceneComponent* SceneComponent) { + return SceneComponent->IsVisible(); + } + + bool IsSocketExists(USceneComponent* SceneComponent, const char* SocketName) { + return SceneComponent->DoesSocketExist(FName(UTF8_TO_TCHAR(SocketName))); + } + + bool HasAnySockets(USceneComponent* SceneComponent) { + return SceneComponent->HasAnySockets(); + } + + bool CanAttachAsChild(USceneComponent* SceneComponent, USceneComponent* ChildComponent, const char* SocketName) { + return SceneComponent->CanAttachAsChild(ChildComponent, FName(UTF8_TO_TCHAR(SocketName))); + } + + void ForEachAttachedChild(USceneComponent* SceneComponent, USceneComponent** Array, int32* Elements) { + static TArray attachedComponents; + + attachedComponents.Reset(); + + attachedComponents = SceneComponent->GetAttachChildren(); + + int32 elements = attachedComponents.Num(); + + if (elements > 0) { + *Array = reinterpret_cast(attachedComponents.GetData()); + *Elements = elements; + } + } + + USceneComponent* Create(AActor* Actor, ComponentType Type, const char* Name, bool SetAsRoot, UObject* Blueprint) { + USceneComponent* component = nullptr; + + if (!Blueprint) { + UNREALCLR_GET_MOVABLE_COMPONENT_TYPE(Type, NewObject<, >(Actor), component); + } else { + #if !WITH_EDITOR + UNREALCLR_GET_MOVABLE_COMPONENT_TYPE(Type, NewObject<, >(Actor, Cast(Blueprint)), component); + #else + UNREALCLR_GET_MOVABLE_COMPONENT_TYPE(Type, NewObject<, >(Actor, Cast(Blueprint)->GeneratedClass), component); + #endif + } + + if (component) { + USceneComponent* rootComponent = Actor->GetRootComponent(); + + if (!rootComponent || SetAsRoot) + Actor->SetRootComponent(component); + else + component->AttachToComponent(rootComponent, FAttachmentTransformRules::KeepRelativeTransform); + + UNREALCLR_SET_COMPONENT_INSTANCE(component, Name); + } + + return component; + } + + bool AttachToComponent(USceneComponent* SceneComponent, USceneComponent* Parent, AttachmentTransformRule AttachmentRule, const char* SocketName) { + FAttachmentTransformRules attachmentRules = FAttachmentTransformRules::KeepRelativeTransform; + FName socketName; + + UNREALCLR_GET_ATTACHMENT_RULE(AttachmentRule, attachmentRules); + + if (SocketName) + socketName = FName(UTF8_TO_TCHAR(SocketName)); + + return SceneComponent->AttachToComponent(Parent, attachmentRules, socketName); + } + + void DetachFromComponent(USceneComponent* SceneComponent, DetachmentTransformRule DetachmentRule) { + FDetachmentTransformRules detachmentRules = FDetachmentTransformRules::KeepRelativeTransform; + + UNREALCLR_GET_DETACHMENT_RULE(DetachmentRule, detachmentRules); + + SceneComponent->DetachFromComponent(detachmentRules); + } + + void Activate(USceneComponent* SceneComponent) { + SceneComponent->Activate(); + } + + void Deactivate(USceneComponent* SceneComponent) { + SceneComponent->Deactivate(); + } + + void UpdateToWorld(USceneComponent* SceneComponent, TeleportType Type, UpdateTransformFlags Flags) { + SceneComponent->UpdateComponentToWorld(static_cast(Flags), Type); + } + + void AddLocalOffset(USceneComponent* SceneComponent, const Vector3* DeltaLocation) { + SceneComponent->AddLocalOffset(*DeltaLocation); + } + + void AddLocalRotation(USceneComponent* SceneComponent, const Quaternion* DeltaRotation) { + SceneComponent->AddLocalRotation(*DeltaRotation); + } + + void AddRelativeLocation(USceneComponent* SceneComponent, const Vector3* DeltaLocation) { + SceneComponent->AddRelativeLocation(*DeltaLocation); + } + + void AddRelativeRotation(USceneComponent* SceneComponent, const Quaternion* DeltaRotation) { + SceneComponent->AddRelativeRotation(*DeltaRotation); + } + + void AddLocalTransform(USceneComponent* SceneComponent, const Transform* DeltaTransform) { + SceneComponent->AddLocalTransform(*DeltaTransform); + } + + void AddWorldOffset(USceneComponent* SceneComponent, const Vector3* DeltaLocation) { + SceneComponent->AddWorldOffset(*DeltaLocation); + } + + void AddWorldRotation(USceneComponent* SceneComponent, const Quaternion* DeltaRotation) { + SceneComponent->AddWorldRotation(*DeltaRotation); + } + + void AddWorldTransform(USceneComponent* SceneComponent, const Transform* DeltaTransform) { + SceneComponent->AddWorldTransform(*DeltaTransform); + } + + void GetAttachedSocketName(USceneComponent* SceneComponent, char* SocketName) { + const char* socketName = TCHAR_TO_UTF8(*SceneComponent->GetAttachSocketName().ToString()); + + UnrealCLR::Utility::Strcpy(SocketName, socketName, UnrealCLR::Utility::Strlen(socketName)); + } + + void GetBounds(USceneComponent* SceneComponent, const Transform* LocalToWorld, Bounds* Value) { + *Value = SceneComponent->CalcBounds(*LocalToWorld); + } + + void GetSocketLocation(USceneComponent* SceneComponent, const char* SocketName, Vector3* Value) { + *Value = SceneComponent->GetSocketLocation(FName(UTF8_TO_TCHAR(SocketName))); + } + + void GetSocketRotation(USceneComponent* SceneComponent, const char* SocketName, Quaternion* Value) { + *Value = SceneComponent->GetSocketQuaternion(FName(UTF8_TO_TCHAR(SocketName))); + } + + void GetComponentVelocity(USceneComponent* SceneComponent, Vector3* Value) { + *Value = SceneComponent->GetComponentVelocity(); + } + + void GetComponentLocation(USceneComponent* SceneComponent, Vector3* Value) { + *Value = SceneComponent->GetComponentLocation(); + } + + void GetComponentRotation(USceneComponent* SceneComponent, Quaternion* Value) { + *Value = SceneComponent->GetComponentQuat(); + } + + void GetComponentScale(USceneComponent* SceneComponent, Vector3* Value) { + *Value = SceneComponent->GetComponentScale(); + } + + void GetComponentTransform(USceneComponent* SceneComponent, Transform* Value) { + *Value = SceneComponent->GetComponentTransform(); + } + + void GetForwardVector(USceneComponent* SceneComponent, Vector3* Value) { + *Value = SceneComponent->GetForwardVector(); + } + + void GetRightVector(USceneComponent* SceneComponent, Vector3* Value) { + *Value = SceneComponent->GetRightVector(); + } + + void GetUpVector(USceneComponent* SceneComponent, Vector3* Value) { + *Value = SceneComponent->GetUpVector(); + } + + void SetMobility(USceneComponent* SceneComponent, ComponentMobility Mobility) { + SceneComponent->SetMobility(Mobility); + } + + void SetVisibility(USceneComponent* SceneComponent, bool NewVisibility, bool PropagateToChildren) { + SceneComponent->SetVisibility(NewVisibility, PropagateToChildren); + } + + void SetRelativeLocation(USceneComponent* SceneComponent, const Vector3* Location) { + SceneComponent->SetRelativeLocation(*Location); + } + + void SetRelativeRotation(USceneComponent* SceneComponent, const Quaternion* Rotation) { + SceneComponent->SetRelativeRotation(*Rotation); + } + + void SetRelativeTransform(USceneComponent* SceneComponent, const Transform* Transform) { + SceneComponent->SetRelativeTransform(*Transform); + } + + void SetWorldLocation(USceneComponent* SceneComponent, const Vector3* Location) { + SceneComponent->SetWorldLocation(*Location); + } + + void SetWorldRotation(USceneComponent* SceneComponent, const Quaternion* Rotation) { + SceneComponent->SetWorldRotation(*Rotation); + } + + void SetWorldScale(USceneComponent* SceneComponent, const Vector3* Scale) { + SceneComponent->SetWorldScale3D(*Scale); + } + + void SetWorldTransform(USceneComponent* SceneComponent, const Transform* Transform) { + SceneComponent->SetWorldTransform(*Transform); + } + } + + namespace AudioComponent { + bool IsPlaying(UAudioComponent* AudioComponent) { + return AudioComponent->IsPlaying(); + } + + bool GetPaused(UAudioComponent* AudioComponent) { + return AudioComponent->bIsPaused; + } + + void SetSound(UAudioComponent* AudioComponent, USoundBase* Sound) { + AudioComponent->SetSound(Sound); + } + + void SetPaused(UAudioComponent* AudioComponent, bool Value) { + AudioComponent->SetPaused(Value); + } + + void Play(UAudioComponent* AudioComponent) { + AudioComponent->Play(); + } + + void Stop(UAudioComponent* AudioComponent) { + AudioComponent->Stop(); + } + + void FadeIn(UAudioComponent* AudioComponent, float Duration, float VolumeLevel, float StartTime, AudioFadeCurve FadeCurve) { + AudioComponent->FadeIn(Duration, VolumeLevel, StartTime, FadeCurve); + } + + void FadeOut(UAudioComponent* AudioComponent, float Duration, float VolumeLevel, AudioFadeCurve FadeCurve) { + AudioComponent->FadeOut(Duration, VolumeLevel, FadeCurve); + } + } + + namespace CameraComponent { + bool GetConstrainAspectRatio(UCameraComponent* CameraComponent) { + return CameraComponent->bConstrainAspectRatio; + } + + float GetAspectRatio(UCameraComponent* CameraComponent) { + return CameraComponent->AspectRatio; + } + + float GetFieldOfView(UCameraComponent* CameraComponent) { + return CameraComponent->FieldOfView; + } + + float GetOrthoFarClipPlane(UCameraComponent* CameraComponent) { + return CameraComponent->OrthoFarClipPlane; + } + + float GetOrthoNearClipPlane(UCameraComponent* CameraComponent) { + return CameraComponent->OrthoNearClipPlane; + } + + float GetOrthoWidth(UCameraComponent* CameraComponent) { + return CameraComponent->OrthoWidth; + } + + bool GetLockToHeadMountedDisplay(UCameraComponent* CameraComponent) { + return CameraComponent->bLockToHmd; + } + + void SetProjectionMode(UCameraComponent* CameraComponent, CameraProjectionMode Mode) { + CameraComponent->SetProjectionMode(Mode); + } + + void SetConstrainAspectRatio(UCameraComponent* CameraComponent, bool Value) { + CameraComponent->bConstrainAspectRatio = Value; + } + + void SetAspectRatio(UCameraComponent* CameraComponent, float Value) { + CameraComponent->AspectRatio = Value; + } + + void SetFieldOfView(UCameraComponent* CameraComponent, float Value) { + CameraComponent->FieldOfView = Value; + } + + void SetOrthoFarClipPlane(UCameraComponent* CameraComponent, float Value) { + CameraComponent->OrthoFarClipPlane = Value; + } + + void SetOrthoNearClipPlane(UCameraComponent* CameraComponent, float Value) { + CameraComponent->OrthoNearClipPlane = Value; + } + + void SetOrthoWidth(UCameraComponent* CameraComponent, float Value) { + CameraComponent->OrthoWidth = Value; + } + + void SetLockToHeadMountedDisplay(UCameraComponent* CameraComponent, bool Value) { + CameraComponent->bLockToHmd = Value; + } + } + + namespace ChildActorComponent { + AActor* GetChildActor(UChildActorComponent* ChildActorComponent, ActorType Type) { + AActor* actor = nullptr; + + UNREALCLR_GET_ACTOR_TYPE(Type, Cast<, >(ChildActorComponent->GetChildActor()), actor); + + return actor; + } + + AActor* SetChildActor(UChildActorComponent* ChildActorComponent, ActorType Type) { + TSubclassOf type; + + UNREALCLR_GET_ACTOR_TYPE(Type, UNREALCLR_NONE, ::StaticClass(), type); + + ChildActorComponent->SetChildActorClass(type); + + return ChildActorComponent->GetChildActor(); + } + } + + namespace SpringArmComponent { + bool IsCollisionFixApplied(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->IsCollisionFixApplied(); + } + + bool GetDrawDebugLagMarkers(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->bDrawDebugLagMarkers; + } + + bool GetCollisionTest(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->bDoCollisionTest; + } + + bool GetCameraPositionLag(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->bEnableCameraLag; + } + + bool GetCameraRotationLag(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->bEnableCameraRotationLag; + } + + bool GetCameraLagSubstepping(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->bUseCameraLagSubstepping; + } + + bool GetInheritPitch(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->bInheritPitch; + } + + bool GetInheritRoll(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->bInheritRoll; + } + + bool GetInheritYaw(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->bInheritYaw; + } + + float GetCameraLagMaxDistance(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->CameraLagMaxDistance; + } + + float GetCameraLagMaxTimeStep(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->CameraLagMaxTimeStep; + } + + float GetCameraPositionLagSpeed(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->CameraLagSpeed; + } + + float GetCameraRotationLagSpeed(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->CameraRotationLagSpeed; + } + + CollisionChannel GetProbeChannel(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->ProbeChannel; + } + + float GetProbeSize(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->ProbeSize; + } + + void GetSocketOffset(USpringArmComponent* SpringArmComponent, Vector3* Value) { + *Value = SpringArmComponent->SocketOffset; + } + + float GetTargetArmLength(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->TargetArmLength; + } + + void GetTargetOffset(USpringArmComponent* SpringArmComponent, Vector3* Value) { + *Value = SpringArmComponent->TargetOffset; + } + + void GetUnfixedCameraPosition(USpringArmComponent* SpringArmComponent, Vector3* Value) { + *Value = SpringArmComponent->GetUnfixedCameraPosition(); + } + + void GetDesiredRotation(USpringArmComponent* SpringArmComponent, Quaternion* Value) { + *Value = SpringArmComponent->GetDesiredRotation().Quaternion(); + } + + void GetTargetRotation(USpringArmComponent* SpringArmComponent, Quaternion* Value) { + *Value = SpringArmComponent->GetTargetRotation().Quaternion(); + } + + bool GetUsePawnControlRotation(USpringArmComponent* SpringArmComponent) { + return SpringArmComponent->bUsePawnControlRotation; + } + + void SetDrawDebugLagMarkers(USpringArmComponent* SpringArmComponent, bool Value) { + SpringArmComponent->bDrawDebugLagMarkers = Value; + } + + void SetCollisionTest(USpringArmComponent* SpringArmComponent, bool Value) { + SpringArmComponent->bDoCollisionTest = Value; + } + + void SetCameraPositionLag(USpringArmComponent* SpringArmComponent, bool Value) { + SpringArmComponent->bEnableCameraLag = Value; + } + + void SetCameraRotationLag(USpringArmComponent* SpringArmComponent, bool Value) { + SpringArmComponent->bEnableCameraRotationLag = Value; + } + + void SetCameraLagSubstepping(USpringArmComponent* SpringArmComponent, bool Value) { + SpringArmComponent->bUseCameraLagSubstepping = Value; + } + + void SetInheritPitch(USpringArmComponent* SpringArmComponent, bool Value) { + SpringArmComponent->bInheritPitch = Value; + } + + void SetInheritRoll(USpringArmComponent* SpringArmComponent, bool Value) { + SpringArmComponent->bInheritRoll = Value; + } + + void SetInheritYaw(USpringArmComponent* SpringArmComponent, bool Value) { + SpringArmComponent->bInheritYaw = Value; + } + + void SetCameraLagMaxDistance(USpringArmComponent* SpringArmComponent, float Value) { + SpringArmComponent->CameraLagMaxDistance = Value; + } + + void SetCameraLagMaxTimeStep(USpringArmComponent* SpringArmComponent, float Value) { + SpringArmComponent->CameraLagMaxTimeStep = Value; + } + + void SetCameraPositionLagSpeed(USpringArmComponent* SpringArmComponent, float Value) { + SpringArmComponent->CameraLagSpeed = Value; + } + + void SetCameraRotationLagSpeed(USpringArmComponent* SpringArmComponent, float Value) { + SpringArmComponent->CameraRotationLagSpeed = Value; + } + + void SetProbeChannel(USpringArmComponent* SpringArmComponent, CollisionChannel Value) { + SpringArmComponent->ProbeChannel = Value; + } + + void SetProbeSize(USpringArmComponent* SpringArmComponent, float Value) { + SpringArmComponent->ProbeSize = Value; + } + + void SetSocketOffset(USpringArmComponent* SpringArmComponent, const Vector3* Value) { + SpringArmComponent->SocketOffset = *Value; + } + + void SetTargetArmLength(USpringArmComponent* SpringArmComponent, float Value) { + SpringArmComponent->TargetArmLength = Value; + } + + void SetTargetOffset(USpringArmComponent* SpringArmComponent, const Vector3* Value) { + SpringArmComponent->TargetOffset = *Value; + } + + void SetUsePawnControlRotation(USpringArmComponent* SpringArmComponent, bool value) { + SpringArmComponent->bUsePawnControlRotation = value; + } + } + + namespace PostProcessComponent { + bool GetEnabled(UPostProcessComponent* PostProcessComponent) { + return PostProcessComponent->bEnabled; + } + + float GetBlendRadius(UPostProcessComponent* PostProcessComponent) { + return PostProcessComponent->BlendRadius; + } + + float GetBlendWeight(UPostProcessComponent* PostProcessComponent) { + return PostProcessComponent->BlendWeight; + } + + bool GetUnbound(UPostProcessComponent* PostProcessComponent) { + return PostProcessComponent->bUnbound; + } + + float GetPriority(UPostProcessComponent* PostProcessComponent) { + return PostProcessComponent->Priority; + } + + void SetEnabled(UPostProcessComponent* PostProcessComponent, bool Value) { + PostProcessComponent->bEnabled = Value; + } + + void SetBlendRadius(UPostProcessComponent* PostProcessComponent, float Value) { + PostProcessComponent->BlendRadius = Value; + } + + void SetBlendWeight(UPostProcessComponent* PostProcessComponent, float Value) { + PostProcessComponent->BlendWeight = Value; + } + + void SetUnbound(UPostProcessComponent* PostProcessComponent, bool Value) { + PostProcessComponent->bUnbound = Value; + } + + void SetPriority(UPostProcessComponent* PostProcessComponent, float Priority) { + PostProcessComponent->Priority = Priority; + } + } + + namespace PrimitiveComponent { + bool IsGravityEnabled(UPrimitiveComponent* PrimitiveComponent) { + return PrimitiveComponent->IsGravityEnabled(); + } + + bool IsOverlappingComponent(UPrimitiveComponent* PrimitiveComponent, UPrimitiveComponent* Other) { + return PrimitiveComponent->IsOverlappingComponent(Other); + } + + void ForEachOverlappingComponent(UPrimitiveComponent* PrimitiveComponent, UPrimitiveComponent** Array, int32* Elements) { + static TArray overlappingComponents; + + overlappingComponents.Reset(); + + PrimitiveComponent->GetOverlappingComponents(overlappingComponents); + + int32 elements = overlappingComponents.Num(); + + if (elements > 0) { + *Array = reinterpret_cast(overlappingComponents.GetData()); + *Elements = elements; + } + } + + void AddAngularImpulseInDegrees(UPrimitiveComponent* PrimitiveComponent, const Vector3* Impulse, const char* BoneName, bool VelocityChange) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->AddAngularImpulseInDegrees(*Impulse, boneName, VelocityChange); + } + + void AddAngularImpulseInRadians(UPrimitiveComponent* PrimitiveComponent, const Vector3* Impulse, const char* BoneName, bool VelocityChange) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->AddAngularImpulseInRadians(*Impulse, boneName, VelocityChange); + } + + void AddForce(UPrimitiveComponent* PrimitiveComponent, const Vector3* Force, const char* BoneName, bool AccelerationChange) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->AddForce(*Force, boneName, AccelerationChange); + } + + void AddForceAtLocation(UPrimitiveComponent* PrimitiveComponent, const Vector3* Force, const Vector3* Location, const char* BoneName, bool LocalSpace) { + UNREALCLR_SET_BONE_NAME(BoneName); + + if (!LocalSpace) + PrimitiveComponent->AddForceAtLocation(*Force, *Location, boneName); + else + PrimitiveComponent->AddForceAtLocationLocal(*Force, *Location, boneName); + } + + void AddImpulse(UPrimitiveComponent* PrimitiveComponent, const Vector3* Impulse, const char* BoneName, bool VelocityChange) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->AddImpulse(*Impulse, boneName, VelocityChange); + } + + void AddImpulseAtLocation(UPrimitiveComponent* PrimitiveComponent, const Vector3* Impulse, const Vector3* Location, const char* BoneName) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->AddImpulseAtLocation(*Impulse, *Location, boneName); + } + + void AddRadialForce(UPrimitiveComponent* PrimitiveComponent, const Vector3* Origin, float Radius, float Strength, bool LinearFalloff, bool AccelerationChange) { + PrimitiveComponent->AddRadialForce(*Origin, Radius, Strength, LinearFalloff ? ERadialImpulseFalloff::RIF_Linear : ERadialImpulseFalloff::RIF_Constant, AccelerationChange); + } + + void AddRadialImpulse(UPrimitiveComponent* PrimitiveComponent, const Vector3* Origin, float Radius, float Strength, bool LinearFalloff, bool AccelerationChange) { + PrimitiveComponent->AddRadialImpulse(*Origin, Radius, Strength, LinearFalloff ? ERadialImpulseFalloff::RIF_Linear : ERadialImpulseFalloff::RIF_Constant, AccelerationChange); + } + + void AddTorqueInDegrees(UPrimitiveComponent* PrimitiveComponent, const Vector3* Torque, const char* BoneName, bool AccelerationChange) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->AddTorqueInDegrees(*Torque, boneName, AccelerationChange); + } + + void AddTorqueInRadians(UPrimitiveComponent* PrimitiveComponent, const Vector3* Torque, const char* BoneName, bool AccelerationChange) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->AddTorqueInRadians(*Torque, boneName, AccelerationChange); + } + + float GetMass(UPrimitiveComponent* PrimitiveComponent) { + return PrimitiveComponent->GetMass(); + } + + void GetPhysicsLinearVelocity(UPrimitiveComponent* PrimitiveComponent, Vector3* Value, const char* BoneName) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->GetPhysicsLinearVelocity(boneName); + } + + void GetPhysicsLinearVelocityAtPoint(UPrimitiveComponent* PrimitiveComponent, Vector3* Value, const Vector3* Point, const char* BoneName) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->GetPhysicsLinearVelocityAtPoint(*Point, boneName); + } + + void GetPhysicsAngularVelocityInDegrees(UPrimitiveComponent* PrimitiveComponent, Vector3* Value, const char* BoneName) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->GetPhysicsAngularVelocityInDegrees(boneName); + } + + void GetPhysicsAngularVelocityInRadians(UPrimitiveComponent* PrimitiveComponent, Vector3* Value, const char* BoneName) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->GetPhysicsAngularVelocityInRadians(boneName); + } + + bool GetCastShadow(UPrimitiveComponent* PrimitiveComponent) { + return PrimitiveComponent->CastShadow; + } + + bool GetOnlyOwnerSee(UPrimitiveComponent* PrimitiveComponent) { + return PrimitiveComponent->bOnlyOwnerSee; + } + + bool GetOwnerNoSee(UPrimitiveComponent* PrimitiveComponent) { + return PrimitiveComponent->bOwnerNoSee; + } + + bool GetIgnoreRadialForce(UPrimitiveComponent* PrimitiveComponent) { + return PrimitiveComponent->bIgnoreRadialForce; + } + + bool GetIgnoreRadialImpulse(UPrimitiveComponent* PrimitiveComponent) { + return PrimitiveComponent->bIgnoreRadialImpulse; + } + + UMaterialInstanceDynamic* GetMaterial(UPrimitiveComponent* PrimitiveComponent, int32 ElementIndex) { + return Cast(PrimitiveComponent->GetMaterial(ElementIndex)); + } + + int32 GetMaterialsNumber(UPrimitiveComponent* PrimitiveComponent) { + return PrimitiveComponent->GetNumMaterials(); + } + + float GetDistanceToCollision(UPrimitiveComponent* PrimitiveComponent, const Vector3* Point, Vector3* ClosestPointOnCollision) { + FVector closestPointOnCollision; + + float result = PrimitiveComponent->GetDistanceToCollision(*Point, closestPointOnCollision); + + *ClosestPointOnCollision = closestPointOnCollision; + + return result; + } + + bool GetSquaredDistanceToCollision(UPrimitiveComponent* PrimitiveComponent, const Vector3* Point, float* SquaredDistance, Vector3* ClosestPointOnCollision) { + FVector closestPointOnCollision; + float squaredDistance = 0.0f; + + bool result = PrimitiveComponent->GetSquaredDistanceToCollision(*Point, squaredDistance, closestPointOnCollision); + + *SquaredDistance = squaredDistance; + *ClosestPointOnCollision = closestPointOnCollision; + + return result; + } + + float GetAngularDamping(UPrimitiveComponent* PrimitiveComponent) { + return PrimitiveComponent->GetAngularDamping(); + } + + float GetLinearDamping(UPrimitiveComponent* PrimitiveComponent) { + return PrimitiveComponent->GetLinearDamping(); + } + + void SetGenerateOverlapEvents(UPrimitiveComponent* PrimitiveComponent, bool Value) { + PrimitiveComponent->SetGenerateOverlapEvents(Value); + } + + void SetGenerateHitEvents(UPrimitiveComponent* PrimitiveComponent, bool Value) { + PrimitiveComponent->SetNotifyRigidBodyCollision(Value); + } + + void SetMass(UPrimitiveComponent* PrimitiveComponent, float Mass, const char* BoneName) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->SetMassOverrideInKg(boneName, Mass); + } + + void SetCenterOfMass(UPrimitiveComponent* PrimitiveComponent, const Vector3* Offset, const char* BoneName) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->SetCenterOfMass(*Offset, boneName); + } + + void SetPhysicsLinearVelocity(UPrimitiveComponent* PrimitiveComponent, const Vector3* Velocity, bool AddToCurrent, const char* BoneName) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->SetPhysicsLinearVelocity(*Velocity, AddToCurrent, boneName); + } + + void SetPhysicsAngularVelocityInDegrees(UPrimitiveComponent* PrimitiveComponent, const Vector3* AngularVelocity, bool AddToCurrent, const char* BoneName) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->SetPhysicsAngularVelocityInDegrees(*AngularVelocity, AddToCurrent, boneName); + } + + void SetPhysicsAngularVelocityInRadians(UPrimitiveComponent* PrimitiveComponent, const Vector3* AngularVelocity, bool AddToCurrent, const char* BoneName) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->SetPhysicsAngularVelocityInRadians(*AngularVelocity, AddToCurrent, boneName); + } + + void SetPhysicsMaxAngularVelocityInDegrees(UPrimitiveComponent* PrimitiveComponent, float MaxAngularVelocity, bool AddToCurrent, const char* BoneName) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->SetPhysicsMaxAngularVelocityInDegrees(MaxAngularVelocity, AddToCurrent, boneName); + } + + void SetPhysicsMaxAngularVelocityInRadians(UPrimitiveComponent* PrimitiveComponent, float MaxAngularVelocity, bool AddToCurrent, const char* BoneName) { + UNREALCLR_SET_BONE_NAME(BoneName); + + PrimitiveComponent->SetPhysicsMaxAngularVelocityInRadians(MaxAngularVelocity, AddToCurrent, boneName); + } + + void SetCastShadow(UPrimitiveComponent* PrimitiveComponent, bool Value) { + PrimitiveComponent->CastShadow = Value; + } + + void SetOnlyOwnerSee(UPrimitiveComponent* PrimitiveComponent, bool Value) { + PrimitiveComponent->bOnlyOwnerSee = Value; + } + + void SetOwnerNoSee(UPrimitiveComponent* PrimitiveComponent, bool Value) { + PrimitiveComponent->bOwnerNoSee = Value; + } + + void SetIgnoreRadialForce(UPrimitiveComponent* PrimitiveComponent, bool Value) { + PrimitiveComponent->bIgnoreRadialForce = Value; + } + + void SetIgnoreRadialImpulse(UPrimitiveComponent* PrimitiveComponent, bool Value) { + PrimitiveComponent->bIgnoreRadialImpulse = Value; + } + + void SetMaterial(UPrimitiveComponent* PrimitiveComponent, int32 ElementIndex, UMaterialInterface* Material) { + PrimitiveComponent->SetMaterial(ElementIndex, Material); + } + + void SetSimulatePhysics(UPrimitiveComponent* PrimitiveComponent, bool Value) { + PrimitiveComponent->SetSimulatePhysics(Value); + } + + void SetAngularDamping(UPrimitiveComponent* PrimitiveComponent, float Value) { + PrimitiveComponent->SetAngularDamping(Value); + } + + void SetLinearDamping(UPrimitiveComponent* PrimitiveComponent, float Value) { + PrimitiveComponent->SetLinearDamping(Value); + } + + void SetEnableGravity(UPrimitiveComponent* PrimitiveComponent, bool Value) { + PrimitiveComponent->SetEnableGravity(Value); + } + + void SetCollisionMode(UPrimitiveComponent* PrimitiveComponent, CollisionMode Mode) { + PrimitiveComponent->SetCollisionEnabled(Mode); + } + + void SetCollisionChannel(UPrimitiveComponent* PrimitiveComponent, CollisionChannel Channel) { + PrimitiveComponent->SetCollisionObjectType(Channel); + } + + void SetCollisionProfileName(UPrimitiveComponent* PrimitiveComponent, const char* ProfileName, bool UpdateOverlaps) { + PrimitiveComponent->SetCollisionProfileName(FName(UTF8_TO_TCHAR(ProfileName)), UpdateOverlaps); + } + + void SetCollisionResponseToChannel(UPrimitiveComponent* PrimitiveComponent, CollisionChannel Channel, CollisionResponse Response) { + PrimitiveComponent->SetCollisionResponseToChannel(Channel, Response); + } + + void SetCollisionResponseToAllChannels(UPrimitiveComponent* PrimitiveComponent, CollisionResponse Response) { + PrimitiveComponent->SetCollisionResponseToAllChannels(Response); + } + + void SetIgnoreActorWhenMoving(UPrimitiveComponent* PrimitiveComponent, AActor* Actor, bool Value) { + PrimitiveComponent->IgnoreActorWhenMoving(Actor, Value); + } + + void SetIgnoreComponentWhenMoving(UPrimitiveComponent* PrimitiveComponent, UPrimitiveComponent* Component, bool Value) { + PrimitiveComponent->IgnoreComponentWhenMoving(Component, Value); + } + + void ClearMoveIgnoreActors(UPrimitiveComponent* PrimitiveComponent) { + PrimitiveComponent->ClearMoveIgnoreActors(); + } + + void ClearMoveIgnoreComponents(UPrimitiveComponent* PrimitiveComponent) { + PrimitiveComponent->ClearMoveIgnoreComponents(); + } + + UMaterialInstanceDynamic* CreateAndSetMaterialInstanceDynamic(UPrimitiveComponent* PrimitiveComponent, int32 ElementIndex) { + return PrimitiveComponent->CreateAndSetMaterialInstanceDynamic(ElementIndex); + } + + void RegisterEvent(UPrimitiveComponent* PrimitiveComponent, ComponentEventType Type) { + UNREALCLR_SET_COMPONENT_EVENT(Type, !, AddDynamic); + } + + void UnregisterEvent(UPrimitiveComponent* PrimitiveComponent, ComponentEventType Type) { + UNREALCLR_SET_COMPONENT_EVENT(Type, UNREALCLR_NONE, RemoveDynamic); + } + } + + namespace ShapeComponent { + bool GetDynamicObstacle(UShapeComponent* ShapeComponent) { + return ShapeComponent->bDynamicObstacle; + } + + int32 GetShapeColor(UShapeComponent* ShapeComponent) { + return UNREALCLR_COLOR_TO_INTEGER(ShapeComponent->ShapeColor); + } + + void SetDynamicObstacle(UShapeComponent* ShapeComponent, bool Value) { + ShapeComponent->bDynamicObstacle = Value; + } + + void SetShapeColor(UShapeComponent* ShapeComponent, Color Value) { + ShapeComponent->ShapeColor = Value; + } + } + + namespace BoxComponent { + void GetScaledBoxExtent(UBoxComponent* BoxComponent, Vector3* Value) { + *Value = BoxComponent->GetScaledBoxExtent(); + } + + void GetUnscaledBoxExtent(UBoxComponent* BoxComponent, Vector3* Value) { + *Value = BoxComponent->GetUnscaledBoxExtent(); + } + + void SetBoxExtent(UBoxComponent* BoxComponent, const Vector3* Extent, bool UpdateOverlaps) { + BoxComponent->SetBoxExtent(*Extent, UpdateOverlaps); + } + + void InitBoxExtent(UBoxComponent* BoxComponent, const Vector3* Extent) { + BoxComponent->InitBoxExtent(*Extent); + } + } + + namespace SphereComponent { + float GetScaledSphereRadius(USphereComponent* SphereComponent) { + return SphereComponent->GetScaledSphereRadius(); + } + + float GetUnscaledSphereRadius(USphereComponent* SphereComponent) { + return SphereComponent->GetUnscaledSphereRadius(); + } + + float GetShapeScale(USphereComponent* SphereComponent) { + return SphereComponent->GetShapeScale(); + } + + void SetSphereRadius(USphereComponent* SphereComponent, float SphereRadius, bool UpdateOverlaps) { + SphereComponent->SetSphereRadius(SphereRadius, UpdateOverlaps); + } + + void InitSphereRadius(USphereComponent* SphereComponent, float SphereRadius) { + SphereComponent->InitSphereRadius(SphereRadius); + } + } + + namespace CapsuleComponent { + float GetScaledCapsuleRadius(UCapsuleComponent* CapsuleComponent) { + return CapsuleComponent->GetScaledCapsuleRadius(); + } + + float GetUnscaledCapsuleRadius(UCapsuleComponent* CapsuleComponent) { + return CapsuleComponent->GetUnscaledCapsuleRadius(); + } + + float GetShapeScale(UCapsuleComponent* CapsuleComponent) { + return CapsuleComponent->GetShapeScale(); + } + + void GetScaledCapsuleSize(UCapsuleComponent* CapsuleComponent, float* Radius, float* HalfHeight) { + CapsuleComponent->GetScaledCapsuleSize(*Radius, *HalfHeight); + } + + void GetUnscaledCapsuleSize(UCapsuleComponent* CapsuleComponent, float* Radius, float* HalfHeight) { + CapsuleComponent->GetUnscaledCapsuleSize(*Radius, *HalfHeight); + } + + void SetCapsuleRadius(UCapsuleComponent* CapsuleComponent, float Radius, bool UpdateOverlaps) { + CapsuleComponent->SetCapsuleRadius(Radius, UpdateOverlaps); + } + + void SetCapsuleSize(UCapsuleComponent* CapsuleComponent, float Radius, float HalfHeight, bool UpdateOverlaps) { + CapsuleComponent->SetCapsuleSize(Radius, HalfHeight, UpdateOverlaps); + } + + void InitCapsuleSize(UCapsuleComponent* CapsuleComponent, float Radius, float HalfHeight) { + CapsuleComponent->InitCapsuleSize(Radius, HalfHeight); + } + } + + namespace MeshComponent { + bool IsValidMaterialSlotName(UMeshComponent* MeshComponent, const char* MaterialSlotName) { + return MeshComponent->IsMaterialSlotNameValid(FName(UTF8_TO_TCHAR(MaterialSlotName))); + } + + int32 GetMaterialIndex(UMeshComponent* MeshComponent, const char* MaterialSlotName) { + return MeshComponent->GetMaterialIndex(FName(UTF8_TO_TCHAR(MaterialSlotName))); + } + } + + namespace TextRenderComponent { + void SetFont(UTextRenderComponent* TextRenderComponent, UFont* Value) { + TextRenderComponent->SetFont(Value); + } + + void SetText(UTextRenderComponent* TextRenderComponent, const char* Value) { + TextRenderComponent->SetText(FText::FromString(FString(UTF8_TO_TCHAR(Value)))); + } + + void SetTextMaterial(UTextRenderComponent* TextRenderComponent, UMaterialInterface* Material) { + TextRenderComponent->SetTextMaterial(Material); + } + + void SetTextRenderColor(UTextRenderComponent* TextRenderComponent, Color Value) { + TextRenderComponent->SetTextRenderColor(Value); + } + + void SetHorizontalAlignment(UTextRenderComponent* TextRenderComponent, HorizontalTextAligment Value) { + TextRenderComponent->SetHorizontalAlignment(Value); + } + + void SetHorizontalSpacingAdjustment(UTextRenderComponent* TextRenderComponent, float Value) { + TextRenderComponent->SetHorizSpacingAdjust(Value); + } + + void SetVerticalAlignment(UTextRenderComponent* TextRenderComponent, VerticalTextAligment Value) { + TextRenderComponent->SetVerticalAlignment(Value); + } + + void SetVerticalSpacingAdjustment(UTextRenderComponent* TextRenderComponent, float Value) { + TextRenderComponent->SetVertSpacingAdjust(Value); + } + + void SetScale(UTextRenderComponent* TextRenderComponent, const Vector2* Value) { + TextRenderComponent->SetXScale(Value->X); + TextRenderComponent->SetYScale(Value->Y); + } + + void SetWorldSize(UTextRenderComponent* TextRenderComponent, float Value) { + TextRenderComponent->SetWorldSize(Value); + } + } + + namespace LightComponentBase { + float GetIntensity(ULightComponentBase* LightComponentBase) { + return LightComponentBase->Intensity; + } + + bool GetCastShadows(ULightComponentBase* LightComponentBase) { + return LightComponentBase->CastShadows; + } + + void SetCastShadows(ULightComponentBase* LightComponentBase, bool Value) { + LightComponentBase->SetCastShadows(Value); + } + } + + namespace LightComponent { + void SetIntensity(ULightComponent* LightComponent, float Value) { + LightComponent->SetIntensity(Value); + } + + void SetLightColor(ULightComponent* LightComponent, const LinearColor* Value) { + LightComponent->SetLightColor(*Value); + } + } + + namespace MotionControllerComponent { + bool IsTracked(UMotionControllerComponent* MotionControllerComponent) { + return MotionControllerComponent->IsTracked(); + } + + bool GetDisplayDeviceModel(UMotionControllerComponent* MotionControllerComponent) { + return MotionControllerComponent->bDisplayDeviceModel; + } + + bool GetDisableLowLatencyUpdate(UMotionControllerComponent* MotionControllerComponent) { + return MotionControllerComponent->bDisableLowLatencyUpdate; + } + + ControllerHand GetTrackingSource(UMotionControllerComponent* MotionControllerComponent) { + return MotionControllerComponent->GetTrackingSource(); + } + + void SetDisplayDeviceModel(UMotionControllerComponent* MotionControllerComponent, bool Value) { + MotionControllerComponent->bDisplayDeviceModel = true; + } + + void SetDisableLowLatencyUpdate(UMotionControllerComponent* MotionControllerComponent, bool Value) { + MotionControllerComponent->bDisableLowLatencyUpdate = Value; + } + + void SetTrackingSource(UMotionControllerComponent* MotionControllerComponent, ControllerHand Value) { + MotionControllerComponent->SetTrackingSource(Value); + } + + void SetTrackingMotionSource(UMotionControllerComponent* MotionControllerComponent, const char* Source) { + MotionControllerComponent->SetTrackingMotionSource(FName(UTF8_TO_TCHAR(Source))); + } + + void SetAssociatedPlayerIndex(UMotionControllerComponent* MotionControllerComponent, int32 PlayerIndex) { + MotionControllerComponent->SetAssociatedPlayerIndex(PlayerIndex); + } + + void SetCustomDisplayMesh(UMotionControllerComponent* MotionControllerComponent, UStaticMesh* StaticMesh) { + MotionControllerComponent->SetCustomDisplayMesh(StaticMesh); + } + + void SetDisplayModelSource(UMotionControllerComponent* MotionControllerComponent, const char* Source) { + MotionControllerComponent->SetDisplayModelSource(FName(UTF8_TO_TCHAR(Source))); + } + } + + namespace StaticMeshComponent { + void GetLocalBounds(UStaticMeshComponent* StaticMeshComponent, Vector3* Min, Vector3* Max) { + FVector min, max; + + StaticMeshComponent->GetLocalBounds(min, max); + + *Min = min; + *Max = max; + } + + UStaticMesh* GetStaticMesh(UStaticMeshComponent* StaticMeshComponent) { + return StaticMeshComponent->GetStaticMesh(); + } + + bool SetStaticMesh(UStaticMeshComponent* StaticMeshComponent, UStaticMesh* StaticMesh) { + return StaticMeshComponent->SetStaticMesh(StaticMesh); + } + } + + namespace InstancedStaticMeshComponent { + int32 GetInstanceCount(UInstancedStaticMeshComponent* InstancedStaticMeshComponent) { + return InstancedStaticMeshComponent->GetInstanceCount(); + } + + bool GetInstanceTransform(UInstancedStaticMeshComponent* InstancedStaticMeshComponent, int32 InstanceIndex, Transform* Value, bool WorldSpace) { + FTransform transform; + + bool result = InstancedStaticMeshComponent->GetInstanceTransform(InstanceIndex, transform, WorldSpace); + + if (result) + *Value = transform; + + return result; + } + + void AddInstance(UInstancedStaticMeshComponent* InstancedStaticMeshComponent, const Transform* InstanceTransform) { + InstancedStaticMeshComponent->AddInstance(*InstanceTransform); + } + + void AddInstances(UInstancedStaticMeshComponent* InstancedStaticMeshComponent, int32 EndInstanceIndex, const Transform InstanceTransforms[]) { + static TArray instanceTransforms; + + instanceTransforms.Reserve(EndInstanceIndex + 1); + instanceTransforms.Reset(); + + for (int32 i = 0; i < EndInstanceIndex; i++) { + instanceTransforms.Add(InstanceTransforms[i]); + } + + InstancedStaticMeshComponent->AddInstances(instanceTransforms, false); + } + + bool UpdateInstanceTransform(UInstancedStaticMeshComponent* InstancedStaticMeshComponent, int32 InstanceIndex, const Transform* InstanceTransform, bool WorldSpace, bool MarkRenderStateDirty, bool Teleport) { + return InstancedStaticMeshComponent->UpdateInstanceTransform(InstanceIndex, *InstanceTransform, WorldSpace, MarkRenderStateDirty, Teleport); + } + + bool BatchUpdateInstanceTransforms(UInstancedStaticMeshComponent* InstancedStaticMeshComponent, int32 StartInstanceIndex, int32 EndInstanceIndex, const Transform InstanceTransforms[], bool WorldSpace, bool MarkRenderStateDirty, bool Teleport) { + static TArray instanceTransforms; + + instanceTransforms.Reserve(EndInstanceIndex + 1); + instanceTransforms.Reset(); + + for (int32 i = 0; i < EndInstanceIndex; i++) { + instanceTransforms.Add(InstanceTransforms[i]); + } + + return InstancedStaticMeshComponent->BatchUpdateInstancesTransforms(StartInstanceIndex, instanceTransforms, WorldSpace, MarkRenderStateDirty, Teleport); + } + + bool RemoveInstance(UInstancedStaticMeshComponent* InstancedStaticMeshComponent, int32 InstanceIndex) { + return InstancedStaticMeshComponent->RemoveInstance(InstanceIndex); + } + + void ClearInstances(UInstancedStaticMeshComponent* InstancedStaticMeshComponent) { + InstancedStaticMeshComponent->ClearInstances(); + } + } + + namespace HierarchicalInstancedStaticMeshComponent { + bool GetDisableCollision(UHierarchicalInstancedStaticMeshComponent* HierarchicalInstancedStaticMeshComponent) { + return HierarchicalInstancedStaticMeshComponent->bDisableCollision; + } + + void SetDisableCollision(UHierarchicalInstancedStaticMeshComponent* HierarchicalInstancedStaticMeshComponent, bool Value) { + HierarchicalInstancedStaticMeshComponent->bDisableCollision = Value; + } + } + + namespace SkinnedMeshComponent { + int32 GetBonesNumber(USkinnedMeshComponent* SkinnedMeshComponent) { + return SkinnedMeshComponent->GetNumBones(); + } + + int32 GetBoneIndex(USkinnedMeshComponent* SkinnedMeshComponent, const char* BoneName) { + return SkinnedMeshComponent->GetBoneIndex(FName(UTF8_TO_TCHAR(BoneName))); + } + + void GetBoneName(USkinnedMeshComponent* SkinnedMeshComponent, int32 BoneIndex, char* BoneName) { + const char* boneName = TCHAR_TO_UTF8(*SkinnedMeshComponent->GetBoneName(BoneIndex).ToString()); + + UnrealCLR::Utility::Strcpy(BoneName, boneName, UnrealCLR::Utility::Strlen(boneName)); + } + + void GetBoneTransform(USkinnedMeshComponent* SkinnedMeshComponent, int32 BoneIndex, Transform* Value) { + *Value = SkinnedMeshComponent->GetBoneTransform(BoneIndex); + } + + void SetSkeletalMesh(USkinnedMeshComponent* SkinnedMeshComponent, USkeletalMesh* SkeletalMesh, bool ReinitializePose) { + SkinnedMeshComponent->SetSkeletalMesh(SkeletalMesh, ReinitializePose); + } + } + + namespace SkeletalMeshComponent { + bool IsPlaying(USkeletalMeshComponent* SkeletalMeshComponent) { + return SkeletalMeshComponent->IsPlaying(); + } + + UAnimInstance* GetAnimationInstance(USkeletalMeshComponent* SkeletalMeshComponent) { + return SkeletalMeshComponent->GetAnimInstance(); + } + + void SetAnimation(USkeletalMeshComponent* SkeletalMeshComponent, UAnimationAsset* Asset) { + SkeletalMeshComponent->SetAnimation(Asset); + } + + void SetAnimationMode(USkeletalMeshComponent* SkeletalMeshComponent, AnimationMode Mode) { + SkeletalMeshComponent->SetAnimationMode(Mode); + } + + void SetAnimationBlueprint(USkeletalMeshComponent* SkeletalMeshComponent, UObject* Blueprint) { + #if !WITH_EDITOR + SkeletalMeshComponent->SetAnimInstanceClass(Cast(Blueprint)); + #else + SkeletalMeshComponent->SetAnimInstanceClass(Cast(Blueprint)->GeneratedClass); + #endif + } + + void Play(USkeletalMeshComponent* SkeletalMeshComponent, bool Loop) { + SkeletalMeshComponent->Play(Loop); + } + + void PlayAnimation(USkeletalMeshComponent* SkeletalMeshComponent, UAnimationAsset* Asset, bool Loop) { + SkeletalMeshComponent->PlayAnimation(Asset, Loop); + } + + void Stop(USkeletalMeshComponent* SkeletalMeshComponent) { + SkeletalMeshComponent->Stop(); + } + } + + namespace SplineComponent { + bool IsClosedLoop(USplineComponent* SplineComponent) { + return SplineComponent->IsClosedLoop(); + } + + float GetDuration(USplineComponent* SplineComponent) { + return SplineComponent->Duration; + } + + SplinePointType GetSplinePointType(USplineComponent* SplineComponent, int32 PointIndex) { + return SplineComponent->GetSplinePointType(PointIndex); + } + + int32 GetSplinePointsNumber(USplineComponent* SplineComponent) { + return SplineComponent->GetNumberOfSplinePoints(); + } + + int32 GetSplineSegmentsNumber(USplineComponent* SplineComponent) { + return SplineComponent->GetNumberOfSplineSegments(); + } + + void GetTangentAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetTangentAtDistanceAlongSpline(Distance, CoordinateSpace); + } + + void GetTangentAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetTangentAtSplinePoint(PointIndex, CoordinateSpace); + } + + void GetTangentAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity, Vector3* Value) { + *Value = SplineComponent->GetTangentAtTime(Time, CoordinateSpace, UseConstantVelocity); + } + + void GetTransformAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Transform* Value) { + *Value = SplineComponent->GetTransformAtDistanceAlongSpline(Distance, CoordinateSpace); + } + + void GetTransformAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, bool UseScale, Transform* Value) { + *Value = SplineComponent->GetTransformAtSplinePoint(PointIndex, CoordinateSpace, UseScale); + } + + void GetArriveTangentAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetArriveTangentAtSplinePoint(PointIndex, CoordinateSpace); + } + + void GetDefaultUpVector(USplineComponent* SplineComponent, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetDefaultUpVector(CoordinateSpace); + } + + void GetDirectionAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetDirectionAtDistanceAlongSpline(Distance, CoordinateSpace); + } + + void GetDirectionAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetDirectionAtSplinePoint(PointIndex, CoordinateSpace); + } + + void GetDirectionAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity, Vector3* Value) { + *Value = SplineComponent->GetDirectionAtTime(Time, CoordinateSpace, UseConstantVelocity); + } + + float GetDistanceAlongSplineAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex) { + return SplineComponent->GetDistanceAlongSplineAtSplinePoint(PointIndex); + } + + void GetLeaveTangentAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetLeaveTangentAtSplinePoint(PointIndex, CoordinateSpace); + } + + void GetLocationAndTangentAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Location, Vector3* Tangent) { + FVector location, tangent; + + SplineComponent->GetLocationAndTangentAtSplinePoint(PointIndex, location, tangent, CoordinateSpace); + + *Location = location; + *Tangent = tangent; + } + + void GetLocationAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetLocationAtDistanceAlongSpline(Distance, CoordinateSpace); + } + + void GetLocationAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetLocationAtSplinePoint(PointIndex, CoordinateSpace); + } + + void GetLocationAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetLocationAtTime(Time, CoordinateSpace); + } + + void GetRightVectorAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetRightVectorAtDistanceAlongSpline(Distance, CoordinateSpace); + } + + void GetRightVectorAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetRightVectorAtSplinePoint(PointIndex, CoordinateSpace); + } + + void GetRightVectorAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity, Vector3* Value) { + *Value = SplineComponent->GetRightVectorAtTime(Time, CoordinateSpace, UseConstantVelocity); + } + + float GetRollAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace) { + return SplineComponent->GetRollAtDistanceAlongSpline(Distance, CoordinateSpace); + } + + float GetRollAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace) { + return SplineComponent->GetRollAtSplinePoint(PointIndex, CoordinateSpace); + } + + float GetRollAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity) { + return SplineComponent->GetRollAtTime(Time, CoordinateSpace, UseConstantVelocity); + } + + void GetRotationAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Quaternion* Value) { + *Value = SplineComponent->GetRotationAtDistanceAlongSpline(Distance, CoordinateSpace).Quaternion(); + } + + void GetRotationAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Quaternion* Value) { + *Value = SplineComponent->GetRotationAtSplinePoint(PointIndex, CoordinateSpace).Quaternion(); + } + + void GetRotationAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity, Quaternion* Value) { + *Value = SplineComponent->GetRotationAtTime(Time, CoordinateSpace, UseConstantVelocity).Quaternion(); + } + + void GetScaleAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, Vector3* Value) { + *Value = SplineComponent->GetScaleAtDistanceAlongSpline(Distance); + } + + void GetScaleAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, Vector3* Value) { + *Value = SplineComponent->GetScaleAtSplinePoint(PointIndex); + } + + void GetScaleAtTime(USplineComponent* SplineComponent, float Time, bool UseConstantVelocity, Vector3* Value) { + *Value = SplineComponent->GetScaleAtTime(Time, UseConstantVelocity); + } + + float GetSplineLength(USplineComponent* SplineComponent) { + return SplineComponent->GetSplineLength(); + } + + void GetTransformAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity, bool UseScale, Transform* Value) { + *Value = SplineComponent->GetTransformAtTime(Time, CoordinateSpace, UseConstantVelocity, UseScale); + } + + void GetUpVectorAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetUpVectorAtDistanceAlongSpline(Distance, CoordinateSpace); + } + + void GetUpVectorAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->GetUpVectorAtSplinePoint(PointIndex, CoordinateSpace); + } + + void GetUpVectorAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity, Vector3* Value) { + *Value = SplineComponent->GetUpVectorAtTime(Time, CoordinateSpace, UseConstantVelocity); + } + + void SetDuration(USplineComponent* SplineComponent, float Value) { + SplineComponent->Duration = Value; + } + + void SetSplinePointType(USplineComponent* SplineComponent, int32 PointIndex, SplinePointType Type, bool UpdateSpline) { + SplineComponent->SetSplinePointType(PointIndex, Type, UpdateSpline); + } + + void SetClosedLoop(USplineComponent* SplineComponent, bool Value, bool UpdateSpline) { + SplineComponent->SetClosedLoop(Value, UpdateSpline); + } + + void SetDefaultUpVector(USplineComponent* SplineComponent, const Vector3* Value, SplineCoordinateSpace CoordinateSpace) { + SplineComponent->SetDefaultUpVector(*Value, CoordinateSpace); + } + + void SetLocationAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, const Vector3* Value, SplineCoordinateSpace CoordinateSpace, bool UpdateSpline) { + SplineComponent->SetLocationAtSplinePoint(PointIndex, *Value, CoordinateSpace, UpdateSpline); + } + + void SetTangentAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, const Vector3* Tangent, SplineCoordinateSpace CoordinateSpace, bool UpdateSpline) { + SplineComponent->SetTangentAtSplinePoint(PointIndex, *Tangent, CoordinateSpace, UpdateSpline); + } + + void SetTangentsAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, const Vector3* ArriveTangent, const Vector3* LeaveTangent, SplineCoordinateSpace CoordinateSpace, bool UpdateSpline) { + SplineComponent->SetTangentsAtSplinePoint(PointIndex, *ArriveTangent, *LeaveTangent, CoordinateSpace, UpdateSpline); + } + + void SetUpVectorAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, const Vector3* UpVector, SplineCoordinateSpace CoordinateSpace, bool UpdateSpline) { + SplineComponent->SetUpVectorAtSplinePoint(PointIndex, *UpVector, CoordinateSpace, UpdateSpline); + } + + void AddSplinePoint(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, bool UpdateSpline) { + SplineComponent->AddSplinePoint(*Location, CoordinateSpace, UpdateSpline); + } + + void AddSplinePointAtIndex(USplineComponent* SplineComponent, const Vector3* Location, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, bool UpdateSpline) { + SplineComponent->AddSplinePointAtIndex(*Location, PointIndex, CoordinateSpace, UpdateSpline); + } + + void ClearSplinePoints(USplineComponent* SplineComponent, bool UpdateSpline) { + SplineComponent->ClearSplinePoints(UpdateSpline); + } + + void FindDirectionClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->FindDirectionClosestToWorldLocation(*Location, CoordinateSpace); + } + + void FindLocationClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->FindLocationClosestToWorldLocation(*Location, CoordinateSpace); + } + + void FindUpVectorClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->FindUpVectorClosestToWorldLocation(*Location, CoordinateSpace); + } + + void FindRightVectorClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->FindRightVectorClosestToWorldLocation(*Location, CoordinateSpace); + } + + float FindRollClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace) { + return SplineComponent->FindRollClosestToWorldLocation(*Location, CoordinateSpace); + } + + void FindScaleClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, Vector3* Value) { + *Value = SplineComponent->FindScaleClosestToWorldLocation(*Location); + } + + void FindTangentClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, Vector3* Value) { + *Value = SplineComponent->FindTangentClosestToWorldLocation(*Location, CoordinateSpace); + } + + void FindTransformClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, bool UseScale, Transform* Value) { + *Value = SplineComponent->FindTransformClosestToWorldLocation(*Location, CoordinateSpace, UseScale); + } + + void RemoveSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, bool UpdateSpline) { + SplineComponent->RemoveSplinePoint(PointIndex, UpdateSpline); + } + + void UpdateSpline(USplineComponent* SplineComponent) { + SplineComponent->UpdateSpline(); + } + } + + namespace RadialForceComponent { + bool GetIgnoreOwningActor(URadialForceComponent* RadialForceComponent) { + return RadialForceComponent->bIgnoreOwningActor; + } + + bool GetImpulseVelocityChange(URadialForceComponent* RadialForceComponent) { + return RadialForceComponent->bImpulseVelChange; + } + + bool GetLinearFalloff(URadialForceComponent* RadialForceComponent) { + return RadialForceComponent->Falloff == ERadialImpulseFalloff::RIF_Linear; + } + + float GetForceStrength(URadialForceComponent* RadialForceComponent) { + return RadialForceComponent->ForceStrength; + } + + float GetImpulseStrength(URadialForceComponent* RadialForceComponent) { + return RadialForceComponent->ImpulseStrength; + } + + float GetRadius(URadialForceComponent* RadialForceComponent) { + return RadialForceComponent->Radius; + } + + void SetIgnoreOwningActor(URadialForceComponent* RadialForceComponent, bool Value) { + RadialForceComponent->bIgnoreOwningActor = Value; + } + + void SetImpulseVelocityChange(URadialForceComponent* RadialForceComponent, bool Value) { + RadialForceComponent->bImpulseVelChange = Value; + } + + void SetLinearFalloff(URadialForceComponent* RadialForceComponent, bool Value) { + RadialForceComponent->Falloff = Value ? ERadialImpulseFalloff::RIF_Linear : ERadialImpulseFalloff::RIF_Constant; + } + + void SetForceStrength(URadialForceComponent* RadialForceComponent, float Value) { + RadialForceComponent->ForceStrength = Value; + } + + void SetImpulseStrength(URadialForceComponent* RadialForceComponent, float Value) { + RadialForceComponent->ImpulseStrength = Value; + } + + void SetRadius(URadialForceComponent* RadialForceComponent, float Value) { + RadialForceComponent->Radius = Value; + } + + void AddCollisionChannelToAffect(URadialForceComponent* RadialForceComponent, CollisionChannel Channel) { + RadialForceComponent->AddCollisionChannelToAffect(Channel); + } + + void FireImpulse(URadialForceComponent* RadialForceComponent) { + RadialForceComponent->FireImpulse(); + } + } + + namespace MaterialInterface { + bool IsTwoSided(UMaterialInterface* MaterialInterface) { + return MaterialInterface->IsTwoSided(); + } + } + + namespace Material { + bool IsDefaultMaterial(UMaterial* Material) { + return Material->IsDefaultMaterial(); + } + } + + namespace MaterialInstance { + bool IsChildOf(UMaterialInstance* MaterialInstance, UMaterialInterface* Material) { + return MaterialInstance->IsChildOf(Material); + } + + UMaterialInstanceDynamic* GetParent(UMaterialInstance* MaterialInstance) { + return UMaterialInstanceDynamic::Create(MaterialInstance->Parent, MaterialInstance); + } + } + + namespace MaterialInstanceDynamic { + void ClearParameterValues(UMaterialInstanceDynamic* MaterialInstanceDynamic) { + MaterialInstanceDynamic->ClearParameterValues(); + } + + void SetTextureParameterValue(UMaterialInstanceDynamic* MaterialInstanceDynamic, const char* ParameterName, UTexture* Value) { + MaterialInstanceDynamic->SetTextureParameterValue(FName(UTF8_TO_TCHAR(ParameterName)), Value); + } + + void SetVectorParameterValue(UMaterialInstanceDynamic* MaterialInstanceDynamic, const char* ParameterName, const LinearColor* Value) { + MaterialInstanceDynamic->SetVectorParameterValue(FName(UTF8_TO_TCHAR(ParameterName)), *Value); + } + + void SetScalarParameterValue(UMaterialInstanceDynamic* MaterialInstanceDynamic, const char* ParameterName, float Value) { + MaterialInstanceDynamic->SetScalarParameterValue(FName(UTF8_TO_TCHAR(ParameterName)), Value); + } + } +} + +``` + +`Source/Native/Source/UnrealCLR/Private/UnrealCLRLibrary.cpp`: + +```cpp +/* + * Unreal Engine .NET 6 integration + * Copyright (c) 2021 Stanislav Denisov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "UnrealCLRLibrary.h" + +FManagedFunction::FManagedFunction() : Pointer() { } + +UUnrealCLRLibrary::UUnrealCLRLibrary(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { } + +void UUnrealCLRLibrary::ExecuteManagedFunction(FManagedFunction ManagedFunction, UObject* Object = nullptr) { + if (UnrealCLR::Status == UnrealCLR::StatusType::Running && ManagedFunction.Pointer) + UnrealCLR::ManagedCommand(UnrealCLR::Command(ManagedFunction.Pointer, Object)); +} + +FManagedFunction UUnrealCLRLibrary::FindManagedFunction(FString Method, bool Optional, bool& Result) { + FManagedFunction managedFunction; + + if (UnrealCLR::Status == UnrealCLR::StatusType::Running && !Method.IsEmpty()) + managedFunction.Pointer = UnrealCLR::ManagedCommand(UnrealCLR::Command(TCHAR_TO_ANSI(*Method), Optional)); + + Result = managedFunction.Pointer != nullptr; + + return managedFunction; +} + +UUnrealCLRCharacter::UUnrealCLRCharacter(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { } + +void UUnrealCLRCharacter::Landed(const FHitResult& Hit) { + UnrealCLRFramework::Hit hit(Hit); + + void* parameters[1] = { + &hit + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(LandedCallback, UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::CharacterLandedDelegate))); +} +``` + +`Source/Native/Source/UnrealCLR/Private/UnrealCLRManager.cpp`: + +```cpp +/* + * Unreal Engine .NET 6 integration + * Copyright (c) 2021 Stanislav Denisov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "UnrealCLRManager.h" + +void UUnrealCLRManager::ActorBeginOverlap(AActor* OverlapActor, AActor* OtherActor) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnActorBeginOverlap]) { + void* parameters[2] = { + OverlapActor, + OtherActor + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnActorBeginOverlap], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ActorOverlapDelegate))); + } +} + +void UUnrealCLRManager::ActorEndOverlap(AActor* OverlapActor, AActor* OtherActor) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnActorEndOverlap]) { + void* parameters[2] = { + OverlapActor, + OtherActor + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnActorEndOverlap], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ActorOverlapDelegate))); + } +} + +void UUnrealCLRManager::ActorHit(AActor* HitActor, AActor* OtherActor, FVector NormalImpulse, const FHitResult& Hit) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnActorHit]) { + UnrealCLRFramework::Vector3 normalImpulse(NormalImpulse); + UnrealCLRFramework::Hit hit(Hit); + + void* parameters[4] = { + HitActor, + OtherActor, + &normalImpulse, + &hit + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnActorHit], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ActorHitDelegate))); + } +} + +void UUnrealCLRManager::ActorBeginCursorOver(AActor* Actor) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnActorBeginCursorOver]) { + void* parameters[1] = { + Actor + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnActorBeginCursorOver], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ActorCursorDelegate))); + } +} + +void UUnrealCLRManager::ActorEndCursorOver(AActor* Actor) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnActorEndCursorOver]) { + void* parameters[1] = { + Actor + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnActorEndCursorOver], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ActorCursorDelegate))); + } +} + +void UUnrealCLRManager::ActorClicked(AActor* Actor, FKey Key) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnActorClicked]) { + FString key = Key.ToString(); + + void* parameters[2] = { + Actor, + TCHAR_TO_ANSI(*key) + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnActorClicked], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ActorKeyDelegate))); + } +} + +void UUnrealCLRManager::ActorReleased(AActor* Actor, FKey Key) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnActorReleased]) { + FString key = Key.ToString(); + + void* parameters[2] = { + Actor, + TCHAR_TO_ANSI(*key) + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnActorReleased], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ActorKeyDelegate))); + } +} + +void UUnrealCLRManager::ComponentBeginOverlap(UPrimitiveComponent* OverlapComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, int32 OtherBodyIndex, bool FromSweep, const FHitResult& SweepResult) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnComponentBeginOverlap]) { + void* parameters[2] = { + OverlapComponent, + OtherComponent + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnComponentBeginOverlap], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ComponentOverlapDelegate))); + } +} + +void UUnrealCLRManager::ComponentEndOverlap(UPrimitiveComponent* OverlapComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, int32 OtherBodyIndex) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnComponentEndOverlap]) { + void* parameters[2] = { + OverlapComponent, + OtherComponent + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnComponentEndOverlap], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ComponentOverlapDelegate))); + } +} + +void UUnrealCLRManager::ComponentHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnComponentHit]) { + UnrealCLRFramework::Vector3 normalImpulse(NormalImpulse); + UnrealCLRFramework::Hit hit(Hit); + + void* parameters[4] = { + HitComponent, + OtherComponent, + &normalImpulse, + &hit + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnComponentHit], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ComponentHitDelegate))); + } +} + +void UUnrealCLRManager::ComponentBeginCursorOver(UPrimitiveComponent* Component) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnActorBeginCursorOver]) { + void* parameters[1] = { + Component + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnComponentBeginCursorOver], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ComponentCursorDelegate))); + } +} + +void UUnrealCLRManager::ComponentEndCursorOver(UPrimitiveComponent* Component) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnComponentEndCursorOver]) { + void* parameters[1] = { + Component + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnComponentEndCursorOver], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ComponentCursorDelegate))); + } +} + +void UUnrealCLRManager::ComponentClicked(UPrimitiveComponent* Component, FKey Key) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnComponentClicked]) { + FString key = Key.ToString(); + + void* parameters[2] = { + Component, + TCHAR_TO_ANSI(*key) + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnComponentClicked], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ComponentKeyDelegate))); + } +} + +void UUnrealCLRManager::ComponentReleased(UPrimitiveComponent* Component, FKey Key) { + if (UnrealCLR::Shared::Events[UnrealCLR::OnComponentReleased]) { + FString key = Key.ToString(); + + void* parameters[2] = { + Component, + TCHAR_TO_ANSI(*key) + }; + + UnrealCLR::ManagedCommand(UnrealCLR::Command(UnrealCLR::Shared::Events[UnrealCLR::OnComponentReleased], UnrealCLR::Callback(parameters, UnrealCLR::CallbackType::ComponentKeyDelegate))); + } +} +``` + +`Source/Native/Source/UnrealCLR/Public/UnrealCLR.h`: + +```h +/* + * Unreal Engine .NET 6 integration + * Copyright (c) 2021 Stanislav Denisov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +// @third party code - BEGIN CoreCLR +#include "../../Dependencies/CoreCLR/includes/coreclr_delegates.h" +#include "../../Dependencies/CoreCLR/includes/hostfxr.h" +// @third party code - END CoreCLR + +#include "AIController.h" +#include "Animation/AnimInstance.h" +#include "AssetRegistryModule.h" +#include "Camera/CameraActor.h" +#include "Camera/CameraComponent.h" +#include "Components/AudioComponent.h" +#include "Components/BoxComponent.h" +#include "Components/CapsuleComponent.h" +#include "Components/DirectionalLightComponent.h" +#include "Components/HierarchicalInstancedStaticMeshComponent.h" +#include "Components/InputComponent.h" +#include "Components/LightComponent.h" +#include "Components/LightComponentBase.h" +#include "Components/PostProcessComponent.h" +#include "Components/ShapeComponent.h" +#include "Components/SphereComponent.h" +#include "Components/SplineComponent.h" +#include "Components/TextRenderComponent.h" +#include "DrawDebugHelpers.h" +#include "Engine/DirectionalLight.h" +#include "Engine/Font.h" +#include "Engine/GameEngine.h" +#include "Engine/LevelScriptActor.h" +#include "Engine/Light.h" +#include "Engine/Player.h" +#include "Engine/PointLight.h" +#include "Engine/PostProcessVolume.h" +#include "Engine/RectLight.h" +#include "Engine/SpotLight.h" +#include "Engine/TriggerBox.h" +#include "Engine/TriggerCapsule.h" +#include "Engine/TriggerSphere.h" +#include "Engine/TriggerVolume.h" +#include "EngineUtils.h" +#include "GameFramework/Actor.h" +#include "GameFramework/Character.h" +#include "GameFramework/GameModeBase.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/PlayerInput.h" +#include "GameFramework/RotatingMovementComponent.h" +#include "GameFramework/SpringArmComponent.h" +#include "HeadMountedDisplayFunctionLibrary.h" +#include "IAssetRegistry.h" +#include "ImageUtils.h" +#include "Materials/MaterialInstanceDynamic.h" +#include "Misc/DefaultValueHelper.h" +#include "Misc/OutputDeviceNull.h" +#include "Modules/ModuleManager.h" +#include "MotionControllerComponent.h" +#include "PhysicsEngine/RadialForceComponent.h" +#include "Sound/AmbientSound.h" +#include "UnrealEngine.h" + +#include "UnrealCLRFramework.h" +#include "UnrealCLRLibrary.h" +#include "UnrealCLRManager.h" + +#if WITH_EDITOR + #include "Editor.h" + #include "Framework/Notifications/NotificationManager.h" + #include "Widgets/Notifications/SNotificationList.h" +#endif + +#ifdef _WIN32 + #define UNREALCLR_WINDOWS 1 +#elif defined(__unix__) + #define UNREALCLR_UNIX 2 +#elif defined(__APPLE__) + #define UNREALCLR_MAC 3 +#endif + +#define UNREALCLR_NONE +#define UNREALCLR_BRACKET_LEFT ( +#define UNREALCLR_BRACKET_RIGHT ) + +UNREALCLR_API DECLARE_LOG_CATEGORY_EXTERN(LogUnrealCLR, Log, All); + +namespace UnrealCLR { + enum struct StatusType : int32 { + Stopped, + Idle, + Running + }; + + enum struct TickState : int32 { + Stopped, + Registered, + Started + }; + + enum struct LogLevel : int32 { + Display, + Warning, + Error, + Fatal + }; + + enum struct CallbackType : int32 { + ActorOverlapDelegate, + ActorHitDelegate, + ActorCursorDelegate, + ActorKeyDelegate, + ComponentOverlapDelegate, + ComponentHitDelegate, + ComponentCursorDelegate, + ComponentKeyDelegate, + CharacterLandedDelegate + }; + + enum struct ArgumentType : int32 { + None, + Single, + Integer, + Pointer, + Callback + }; + + enum struct CommandType : int32 { + Initialize = 1, + LoadAssemblies = 2, + UnloadAssemblies = 3, + Find = 4, + Execute = 5 + }; + + enum { + OnWorldBegin, + OnWorldPostBegin, + OnWorldPrePhysicsTick, + OnWorldDuringPhysicsTick, + OnWorldPostPhysicsTick, + OnWorldPostUpdateTick, + OnWorldEnd, + OnActorBeginOverlap, + OnActorEndOverlap, + OnActorHit, + OnActorBeginCursorOver, + OnActorEndCursorOver, + OnActorClicked, + OnActorReleased, + OnComponentBeginOverlap, + OnComponentEndOverlap, + OnComponentHit, + OnComponentBeginCursorOver, + OnComponentEndCursorOver, + OnComponentClicked, + OnComponentReleased + }; + + struct Callback { + void** Parameters; + CallbackType Type; + + FORCEINLINE Callback(void** Parameters, CallbackType Type) { + this->Parameters = Parameters; + this->Type = Type; + } + }; + + struct Argument { + union { + float Single; + uint32_t Integer; + void* Pointer; + Callback Callback; + }; + ArgumentType Type; + + FORCEINLINE Argument(float Value) { + this->Single = Value; + this->Type = ArgumentType::Single; + } + + FORCEINLINE Argument(uint32_t Value) { + this->Integer = Value; + this->Type = ArgumentType::Integer; + } + + FORCEINLINE Argument(void* Value) { + this->Pointer = Value; + this->Type = !Value ? ArgumentType::None : ArgumentType::Pointer; + } + + FORCEINLINE Argument(UnrealCLR::Callback Value) { + this->Callback = Value; + this->Type = ArgumentType::Callback; + } + }; + + struct Command { + union { + struct { + void* Buffer; + int32 Checksum; + }; + struct { + char* Method; + int32 Optional; + }; + struct { + void* Function; + Argument Value; + }; + }; + CommandType Type; + + FORCEINLINE Command(void* const Functions[3], int32 Checksum) { + this->Buffer = (void*)Functions; + this->Checksum = Checksum; + this->Type = CommandType::Initialize; + } + + FORCEINLINE Command(CommandType Type) { + this->Type = Type; + } + + FORCEINLINE Command(const char* Method, bool Optional) { + this->Method = (char*)Method; + this->Optional = Optional; + this->Type = CommandType::Find; + } + + FORCEINLINE Command(void* Function) { + this->Function = Function; + this->Value = nullptr; + this->Type = CommandType::Execute; + } + + FORCEINLINE Command(void* Function, Argument Value) { + this->Function = Function; + this->Value = Value; + this->Type = CommandType::Execute; + } + }; + + static_assert(sizeof(Callback) == 16, "Invalid size of the [Callback] structure"); + static_assert(sizeof(Argument) == 24, "Invalid size of the [Argument] structure"); + static_assert(sizeof(Command) == 40, "Invalid size of the [Command] structure"); + + static void* (*ManagedCommand)(Command); + + static FString ProjectPath; + static FString UserAssembliesPath; + + static StatusType Status = StatusType::Stopped; + static TickState WorldTickState = TickState::Stopped; + + struct PrePhysicsTickFunction : public FTickFunction { + virtual void ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override; + virtual FString DiagnosticMessage() override; + }; + + struct DuringPhysicsTickFunction : public FTickFunction { + virtual void ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override; + virtual FString DiagnosticMessage() override; + }; + + struct PostPhysicsTickFunction : public FTickFunction { + virtual void ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override; + virtual FString DiagnosticMessage() override; + }; + + struct PostUpdateTickFunction : public FTickFunction { + virtual void ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override; + virtual FString DiagnosticMessage() override; + }; + + class Module : public IModuleInterface { + protected: + + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + private: + + void OnWorldPostInitialization(UWorld* World, const UWorld::InitializationValues InitializationValues); + void OnWorldCleanup(UWorld* World, bool SessionEnded, bool CleanupResources); + + static void RegisterTickFunction(FTickFunction& TickFunction, ETickingGroup TickGroup, AWorldSettings* LevelActor); + static void HostError(const char_t* Message); + static void Exception(const char* Message); + static void Log(UnrealCLR::LogLevel Level, const char* Message); + + FDelegateHandle OnWorldPostInitializationHandle; + FDelegateHandle OnWorldCleanupHandle; + + PrePhysicsTickFunction OnPrePhysicsTickFunction; + DuringPhysicsTickFunction OnDuringPhysicsTickFunction; + PostPhysicsTickFunction OnPostPhysicsTickFunction; + PostUpdateTickFunction OnPostUpdateTickFunction; + + void* HostfxrLibrary; + }; + + namespace Engine { + static UUnrealCLRManager* Manager; + static UWorld* World; + } + + namespace Shared { + static constexpr int32 storageSize = 128; + + // Non-instantiable + + static void* AssertFunctions[storageSize]; + static void* CommandLineFunctions[storageSize]; + static void* DebugFunctions[storageSize]; + static void* ObjectFunctions[storageSize]; + static void* ApplicationFunctions[storageSize]; + static void* ConsoleManagerFunctions[storageSize]; + static void* EngineFunctions[storageSize]; + static void* WorldFunctions[storageSize]; + + // Instantiable + + static void* AssetFunctions[storageSize]; + static void* AssetRegistryFunctions[storageSize]; + static void* BlueprintFunctions[storageSize]; + static void* ConsoleObjectFunctions[storageSize]; + static void* ConsoleVariableFunctions[storageSize]; + static void* ActorFunctions[storageSize]; + static void* GameModeBaseFunctions[storageSize]; + static void* PawnFunctions[storageSize]; + static void* CharacterFunctions[storageSize]; + static void* ControllerFunctions[storageSize]; + static void* AIControllerFunctions[storageSize]; + static void* PlayerControllerFunctions[storageSize]; + static void* VolumeFunctions[storageSize]; + static void* PostProcessVolumeFunctions[storageSize]; + static void* SoundBaseFunctions[storageSize]; + static void* SoundWaveFunctions[storageSize]; + static void* AnimationInstanceFunctions[storageSize]; + static void* PlayerFunctions[storageSize]; + static void* PlayerInputFunctions[storageSize]; + static void* FontFunctions[storageSize]; + static void* Texture2DFunctions[storageSize]; + static void* ActorComponentFunctions[storageSize]; + static void* InputComponentFunctions[storageSize]; + static void* MovementComponentFunctions[storageSize]; + static void* RotatingMovementComponentFunctions[storageSize]; + static void* SceneComponentFunctions[storageSize]; + static void* AudioComponentFunctions[storageSize]; + static void* CameraComponentFunctions[storageSize]; + static void* ChildActorComponentFunctions[storageSize]; + static void* SpringArmComponentFunctions[storageSize]; + static void* PostProcessComponentFunctions[storageSize]; + static void* PrimitiveComponentFunctions[storageSize]; + static void* ShapeComponentFunctions[storageSize]; + static void* BoxComponentFunctions[storageSize]; + static void* SphereComponentFunctions[storageSize]; + static void* CapsuleComponentFunctions[storageSize]; + static void* MeshComponentFunctions[storageSize]; + static void* TextRenderComponentFunctions[storageSize]; + static void* LightComponentBaseFunctions[storageSize]; + static void* LightComponentFunctions[storageSize]; + static void* MotionControllerComponentFunctions[storageSize]; + static void* StaticMeshComponentFunctions[storageSize]; + static void* InstancedStaticMeshComponentFunctions[storageSize]; + static void* HierarchicalInstancedStaticMeshComponentFunctions[storageSize]; + static void* SkinnedMeshComponentFunctions[storageSize]; + static void* SkeletalMeshComponentFunctions[storageSize]; + static void* SplineComponentFunctions[storageSize]; + static void* RadialForceComponentFunctions[storageSize]; + static void* MaterialInterfaceFunctions[storageSize]; + static void* MaterialFunctions[storageSize]; + static void* MaterialInstanceFunctions[storageSize]; + static void* MaterialInstanceDynamicFunctions[storageSize]; + static void* HeadMountedDisplayFunctions[storageSize]; + + static void* RuntimeFunctions[2]; + static void* Events[128]; + static void* Functions[128]; + } + + namespace Utility { + FORCEINLINE static size_t Strcpy(char* Destination, const char* Source, size_t Length); + FORCEINLINE static size_t Strlen(const char* Source); + } +} +``` + +`Source/Native/Source/UnrealCLR/Public/UnrealCLRFramework.h`: + +```h +/* + * Unreal Engine .NET 6 integration + * Copyright (c) 2021 Stanislav Denisov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +UNREALCLR_API DECLARE_LOG_CATEGORY_EXTERN(LogUnrealManaged, Log, All); + +namespace UnrealCLRFramework { + using AnimationMode = EAnimationMode::Type; + using AutoReceiveInput = EAutoReceiveInput::Type; + using CameraProjectionMode = ECameraProjectionMode::Type; + using CollisionMode = ECollisionEnabled::Type; + using CollisionShapeType = ECollisionShape::Type; + using ComponentMobility = EComponentMobility::Type; + using SplineCoordinateSpace = ESplineCoordinateSpace::Type; + using SplinePointType = ESplinePointType::Type; + using WindowMode = EWindowMode::Type; + + using AudioFadeCurve = EAudioFaderCurve; + using AutoPossessAI = EAutoPossessAI; + using BlendType = EViewTargetBlendFunction; + using CollisionChannel = ECollisionChannel; + using CollisionResponse = ECollisionResponse; + using ControllerHand = EControllerHand; + using HorizontalTextAligment = EHorizTextAligment; + using InputEvent = EInputEvent; + using NetMode = ENetMode; + using PixelFormat = EPixelFormat; + using TeleportType = ETeleportType; + using VerticalTextAligment = EVerticalTextAligment; + using PlaneConstraintAxis = EPlaneConstraintAxisSetting; + + using Bounds = FBoxSphereBounds; + using CollisionShape = FCollisionShape; + + enum struct LogLevel : int32 { + Display, + Warning, + Error, + Fatal + }; + + enum struct AttachmentTransformRule : int32 { + KeepRelativeTransform, + KeepWorldTransform, + SnapToTargetIncludingScale, + SnapToTargetNotIncludingScale + }; + + enum struct DetachmentTransformRule : int32 { + KeepRelativeTransform, + KeepWorldTransform + }; + + enum struct UpdateTransformFlags : int32 { + None = 0, + SkipPhysicsUpdate = 1 << 0, + PropagateFromParent = 1 << 1, + OnlyUpdateIfUsingSocket = 1 << 2 + }; + + enum struct AIFocusPriority : int32 { + Default = 0, + Move = 1, + Gameplay = 2 + }; + + enum struct ActorEventType : int32 { + OnActorBeginOverlap, + OnActorEndOverlap, + OnActorHit, + OnActorBeginCursorOver, + OnActorEndCursorOver, + OnActorClicked, + OnActorReleased + }; + + enum struct ComponentEventType : int32 { + OnComponentBeginOverlap, + OnComponentEndOverlap, + OnComponentHit, + OnComponentBeginCursorOver, + OnComponentEndCursorOver, + OnComponentClicked, + OnComponentReleased + }; + + struct Color { + uint8 B; + uint8 G; + uint8 R; + uint8 A; + + FORCEINLINE Color(FColor Value) { + this->R = Value.R; + this->G = Value.G; + this->B = Value.B; + this->A = Value.A; + } + + FORCEINLINE operator FColor() const { return FColor(R, G, B, A); } + }; + + struct Vector2 { + float X; + float Y; + + FORCEINLINE Vector2(FVector2D Value) { + this->X = Value.X; + this->Y = Value.Y; + } + + FORCEINLINE operator FVector2D() const { return FVector2D(X, Y); } + }; + + struct Vector3 { + float X; + float Y; + float Z; + + FORCEINLINE Vector3(FVector Value) { + this->X = Value.X; + this->Y = Value.Y; + this->Z = Value.Z; + } + + FORCEINLINE operator FVector() const { return FVector(X, Y, Z); } + }; + + struct Quaternion { + float X; + float Y; + float Z; + float W; + + FORCEINLINE Quaternion(FQuat Value) { + this->X = Value.X; + this->Y = Value.Y; + this->Z = Value.Z; + this->W = Value.W; + } + + FORCEINLINE operator FQuat() const { return FQuat(X, Y, Z, W); } + }; + + struct Transform { + Vector3 Location; + Quaternion Rotation; + Vector3 Scale; + + FORCEINLINE Transform(const FTransform& Value) : + Location(Value.GetTranslation()), + Rotation(Value.GetRotation()), + Scale(Value.GetScale3D()) { } + + FORCEINLINE operator FTransform() const { return FTransform(Rotation, Location, Scale); } + }; + + struct LinearColor { + float R; + float G; + float B; + float A; + + FORCEINLINE LinearColor(FLinearColor Value) { + this->R = Value.R; + this->G = Value.G; + this->B = Value.B; + this->A = Value.A; + } + + FORCEINLINE operator FLinearColor() const { return FLinearColor(R, G, B, A); } + }; + + struct Hit { + Vector3 Location; + Vector3 ImpactLocation; + Vector3 Normal; + Vector3 ImpactNormal; + Vector3 TraceStart; + Vector3 TraceEnd; + AActor* Actor; + float Time; + float Distance; + float PenetrationDepth; + bool BlockingHit; + bool StartPenetrating; + + FORCEINLINE Hit(const FHitResult& Value) : + Location(FVector(Value.Location.X, Value.Location.Y, Value.Location.Z)), + ImpactLocation(FVector(Value.ImpactPoint.X, Value.ImpactPoint.Y, Value.ImpactPoint.Z)), + Normal(FVector(Value.Normal.X, Value.Normal.Y, Value.Normal.Z)), + ImpactNormal(FVector(Value.ImpactNormal.X, Value.ImpactNormal.Y, Value.ImpactNormal.Z)), + TraceStart(FVector(Value.TraceStart.X, Value.TraceStart.Y, Value.TraceStart.Z)), + TraceEnd(FVector(Value.TraceEnd.X, Value.TraceEnd.Y, Value.TraceEnd.Z)), + Actor(Value.GetActor()), + Time(Value.Time), + Distance(Value.Distance), + PenetrationDepth(Value.PenetrationDepth), + BlockingHit(Value.bBlockingHit), + StartPenetrating(Value.bStartPenetrating) { } + }; + + typedef void (*InputDelegate)(); + + typedef void (*InputAxisDelegate)(float); + + typedef void (*ConsoleVariableDelegate)(); + + typedef void (*ConsoleCommandDelegate)(float); + + typedef void (*ActorOverlapDelegate)(AActor*, AActor*); + + typedef void (*ActorHitDelegate)(AActor* HitActor, AActor* OtherActor, const Vector3* NormalImpulse, const Hit* Hit); + + typedef void (*ActorCursorDelegate)(AActor* Actor); + + typedef void (*ActorKeyDelegate)(AActor* Actor, const char* Key); + + typedef void (*ComponentOverlapDelegate)(UPrimitiveComponent*, UPrimitiveComponent*); + + typedef void (*ComponentHitDelegate)(UPrimitiveComponent* HitComponent, UPrimitiveComponent* OtherComponent, const Vector3* NormalImpulse, const Hit* Hit); + + typedef void (*ComponentCursorDelegate)(UPrimitiveComponent* Component); + + typedef void (*ComponentKeyDelegate)(UPrimitiveComponent* Component, const char* Key); + + typedef void (*CharacterLandedDelegate)(const Hit* Hit); + + // Enumerable + + enum struct ObjectType : int32 { + Blueprint, + SoundWave, + AnimationSequence, + AnimationMontage, + StaticMesh, + SkeletalMesh, + Material, + Font, + Texture2D + }; + + enum struct ActorType : int32 { + Base, + Camera, + TriggerBox, + TriggerSphere, + TriggerCapsule, + Pawn, + Character, + AIController, + PlayerController, + Brush, + AmbientSound, + DirectionalLight, + PointLight, + RectLight, + SpotLight, + TriggerVolume, + PostProcessVolume, + LevelScript, + GameModeBase + }; + + enum struct ComponentType : int32 { + // Non-movable + Actor, + Input, + Movement, + RotatingMovement, + // Movable + Scene, + Audio, + Camera, + Light, + DirectionalLight, + MotionController, + StaticMesh, + InstancedStaticMesh, + HierarchicalInstancedStaticMesh, + ChildActor, + SpringArm, + PostProcess, + Box, + Sphere, + Capsule, + TextRender, + SkeletalMesh, + Spline, + RadialForce + }; + + // Non-instantiable + + namespace Assert { + static void OutputMessage(const char* Message); + } + + namespace CommandLine { + static void Get(char* Arguments); + static void Set(const char* Arguments); + static void Append(const char* Arguments); + } + + namespace Debug { + static void Log(LogLevel Level, const char* Message); + static void Exception(const char* Exception); + static void AddOnScreenMessage(int32 Key, float TimeToDisplay, Color DisplayColor, const char* Message); + static void ClearOnScreenMessages(); + static void DrawBox(const Vector3* Center, const Vector3* Extent, const Quaternion* Rotation, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority, float Thickness); + static void DrawCapsule(const Vector3* Center, float HalfHeight, float Radius, const Quaternion* Rotation, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority, float Thickness); + static void DrawCone(const Vector3* Origin, const Vector3* Direction, float Length, float AngleWidth, float AngleHeight, int32 Sides, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority, float Thickness); + static void DrawCylinder(const Vector3* Start, const Vector3* End, float Radius, int32 Segments, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority, float Thickness); + static void DrawSphere(const Vector3* Center, float Radius, int32 Segments, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority, float Thickness); + static void DrawLine(const Vector3* Start, const Vector3* End, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority, float Thickness); + static void DrawPoint(const Vector3* Location, float Size, Color Color, bool PersistentLines, float LifeTime, uint8 DepthPriority); + static void FlushPersistentLines(); + } + + namespace Object { + static bool IsPendingKill(UObject* Object); + static bool IsValid(UObject* Object); + static UObject* Load(ObjectType Type, const char* Name); + static void Rename(UObject* Object, const char* Name); + static bool Invoke(UObject* Object, const char* Command); + static AActor* ToActor(UObject* Object, ActorType Type); + static UActorComponent* ToComponent(UObject* Object, ComponentType Type); + static uint32 GetID(UObject* Object); + static void GetName(UObject* Object, char* Name); + static bool GetBool(UObject* Object, const char* Name, bool* value); + static bool GetByte(UObject* Object, const char* Name, uint8* Value); + static bool GetShort(UObject* Object, const char* Name, int16* Value); + static bool GetInt(UObject* Object, const char* Name, int32* Value); + static bool GetLong(UObject* Object, const char* Name, int64* Value); + static bool GetUShort(UObject* Object, const char* Name, uint16* Value); + static bool GetUInt(UObject* Object, const char* Name, uint32* Value); + static bool GetULong(UObject* Object, const char* Name, uint64* Value); + static bool GetFloat(UObject* Object, const char* Name, float* Value); + static bool GetDouble(UObject* Object, const char* Name, double* Value); + static bool GetEnum(UObject* Object, const char* Name, int32* Value); + static bool GetString(UObject* Object, const char* Name, char* Value); + static bool GetText(UObject* Object, const char* Name, char* Value); + static bool SetBool(UObject* Object, const char* Name, bool value); + static bool SetByte(UObject* Object, const char* Name, uint8 Value); + static bool SetShort(UObject* Object, const char* Name, int16 Value); + static bool SetInt(UObject* Object, const char* Name, int32 Value); + static bool SetLong(UObject* Object, const char* Name, int64 Value); + static bool SetUShort(UObject* Object, const char* Name, uint16 Value); + static bool SetUInt(UObject* Object, const char* Name, uint32 Value); + static bool SetULong(UObject* Object, const char* Name, uint64 Value); + static bool SetFloat(UObject* Object, const char* Name, float Value); + static bool SetDouble(UObject* Object, const char* Name, double Value); + static bool SetEnum(UObject* Object, const char* Name, int32 Value); + static bool SetString(UObject* Object, const char* Name, const char* Value); + static bool SetText(UObject* Object, const char* Name, const char* Value); + } + + namespace Application { + static bool IsCanEverRender(); + static bool IsPackagedForDistribution(); + static bool IsPackagedForShipping(); + static void GetProjectDirectory(char* Directory); + static void GetDefaultLanguage(char* Language); + static void GetProjectName(char* ProjectName); + static float GetVolumeMultiplier(); + static void SetProjectName(const char* ProjectName); + static void SetVolumeMultiplier(float Value); + static void RequestExit(bool Force); + } + + namespace ConsoleManager { + static bool IsRegisteredVariable(const char* Name); + static IConsoleVariable* FindVariable(const char* Name); + static IConsoleVariable* RegisterVariableBool(const char* Name, const char* Help, bool DefaultValue, bool ReadOnly); + static IConsoleVariable* RegisterVariableInt(const char* Name, const char* Help, int32 DefaultValue, bool ReadOnly); + static IConsoleVariable* RegisterVariableFloat(const char* Name, const char* Help, float DefaultValue, bool ReadOnly); + static IConsoleVariable* RegisterVariableString(const char* Name, const char* Help, const char* DefaultValue, bool ReadOnly); + static void RegisterCommand(const char* Name, const char* Help, ConsoleCommandDelegate Callback, bool ReadOnly); + static void UnregisterObject(const char* Name); + } + + namespace Engine { + static bool IsSplitScreen(); + static bool IsEditor(); + static bool IsForegroundWindow(); + static bool IsExitRequested(); + static NetMode GetNetMode(); + static uint32 GetFrameNumber(); + static void GetViewportSize(Vector2* Value); + static void GetScreenResolution(Vector2* Value); + static WindowMode GetWindowMode(); + static void GetVersion(char* Version); + static float GetMaxFPS(); + static void SetMaxFPS(float MaxFPS); + static void SetTitle(const char* Title); + static void AddActionMapping(const char* ActionName, const char* Key, bool Shift, bool Ctrl, bool Alt, bool Cmd); + static void AddAxisMapping(const char* AxisName, const char* Key, float Scale); + static void ForceGarbageCollection(bool FullPurge); + static void DelayGarbageCollection(); + } + + namespace HeadMountedDisplay { + static bool IsConnected(); + static bool GetEnabled(); + static bool GetLowPersistenceMode(); + static void GetDeviceName(char* Name); + static void SetEnable(bool Value); + static void SetLowPersistenceMode(bool Value); + } + + namespace World { + static void ForEachActor(AActor** Array, int32* Elements); + static int32 GetActorCount(); + static float GetDeltaSeconds(); + static float GetRealTimeSeconds(); + static float GetTimeSeconds(); + static void GetCurrentLevelName(char* LevelName); + static bool GetSimulatePhysics(); + static void GetWorldOrigin(Vector3* Value); + static AActor* GetActor(const char* Name, ActorType Type); + static AActor* GetActorByTag(const char* Tag, ActorType Type); + static AActor* GetActorByID(uint32 ID, ActorType Type); + static APlayerController* GetFirstPlayerController(); + static AGameModeBase* GetGameMode(); + static void SetOnActorBeginOverlapCallback(ActorOverlapDelegate Callback); + static void SetOnActorEndOverlapCallback(ActorOverlapDelegate Callback); + static void SetOnActorHitCallback(ActorHitDelegate Callback); + static void SetOnActorBeginCursorOverCallback(ActorCursorDelegate Callback); + static void SetOnActorEndCursorOverCallback(ActorCursorDelegate Callback); + static void SetOnActorClickedCallback(ActorKeyDelegate Callback); + static void SetOnActorReleasedCallback(ActorKeyDelegate Callback); + static void SetOnComponentBeginOverlapCallback(ComponentOverlapDelegate Callback); + static void SetOnComponentEndOverlapCallback(ComponentOverlapDelegate Callback); + static void SetOnComponentHitCallback(ComponentHitDelegate Callback); + static void SetOnComponentBeginCursorOverCallback(ComponentCursorDelegate Callback); + static void SetOnComponentEndCursorOverCallback(ComponentCursorDelegate Callback); + static void SetOnComponentClickedCallback(ComponentKeyDelegate Callback); + static void SetOnComponentReleasedCallback(ComponentKeyDelegate Callback); + static void SetSimulatePhysics(bool Value); + static void SetGravity(float Value); + static bool SetWorldOrigin(const Vector3* Value); + static void OpenLevel(const char* LevelName); + static bool LineTraceTestByChannel(const Vector3* Start, const Vector3* End, CollisionChannel Channel, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent); + static bool LineTraceTestByProfile(const Vector3* Start, const Vector3* End, const char* ProfileName, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent); + static bool LineTraceSingleByChannel(const Vector3* Start, const Vector3* End, CollisionChannel Channel, Hit* Hit, char* BoneName, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent); + static bool LineTraceSingleByProfile(const Vector3* Start, const Vector3* End, const char* ProfileName, Hit* Hit, char* BoneName, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent); + static bool SweepTestByChannel(const Vector3* Start, const Vector3* End, const Quaternion* Rotation, CollisionChannel Channel, const CollisionShape* Shape, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent); + static bool SweepTestByProfile(const Vector3* Start, const Vector3* End, const Quaternion* Rotation, const char* ProfileName, const CollisionShape* Shape, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent); + static bool SweepSingleByChannel(const Vector3* Start, const Vector3* End, const Quaternion* Rotation, CollisionChannel Channel, const CollisionShape* Shape, Hit* Hit, char* BoneName, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent); + static bool SweepSingleByProfile(const Vector3* Start, const Vector3* End, const Quaternion* Rotation, const char* ProfileName, const CollisionShape* Shape, Hit* Hit, char* BoneName, bool TraceComplex, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent); + static bool OverlapAnyTestByChannel(const Vector3* Location, const Quaternion* Rotation, CollisionChannel Channel, const CollisionShape* Shape, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent); + static bool OverlapAnyTestByProfile(const Vector3* Location, const Quaternion* Rotation, const char* ProfileName, const CollisionShape* Shape, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent); + static bool OverlapBlockingTestByChannel(const Vector3* Location, const Quaternion* Rotation, CollisionChannel Channel, const CollisionShape* Shape, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent); + static bool OverlapBlockingTestByProfile(const Vector3* Location, const Quaternion* Rotation, const char* ProfileName, const CollisionShape* Shape, AActor* IgnoredActor, UPrimitiveComponent* IgnoredComponent); + } + + // Instantiable + + namespace Asset { + static bool IsValid(FAssetData* Asset); + static void GetName(FAssetData* Asset, char* Name); + static void GetPath(FAssetData* Asset, char* Path); + } + + namespace AssetRegistry { + static IAssetRegistry* Get(); + static bool HasAssets(IAssetRegistry* AssetRegistry, const char* Path, bool Recursive); + static void ForEachAsset(IAssetRegistry* AssetRegistry, const char* Path, bool Recursive, bool IncludeOnlyOnDiskAssets, FAssetData** Array, int32* Elements); + } + + namespace Blueprint { + static bool IsValidActorClass(UBlueprint* Blueprint, ActorType Type); + static bool IsValidComponentClass(UBlueprint* Blueprint, ComponentType Type); + } + + namespace ConsoleObject { + static bool IsBool(IConsoleObject* ConsoleObject); + static bool IsInt(IConsoleObject* ConsoleObject); + static bool IsFloat(IConsoleObject* ConsoleObject); + static bool IsString(IConsoleObject* ConsoleObject); + } + + namespace ConsoleVariable { + static bool GetBool(IConsoleVariable* ConsoleVariable); + static int32 GetInt(IConsoleVariable* ConsoleVariable); + static float GetFloat(IConsoleVariable* ConsoleVariable); + static void GetString(IConsoleVariable* ConsoleVariable, char* Value); + static void SetBool(IConsoleVariable* ConsoleVariable, bool Value); + static void SetInt(IConsoleVariable* ConsoleVariable, int32 Value); + static void SetFloat(IConsoleVariable* ConsoleVariable, float Value); + static void SetString(IConsoleVariable* ConsoleVariable, const char* Value); + static void SetOnChangedCallback(IConsoleVariable* ConsoleVariable, ConsoleVariableDelegate Callback); + static void ClearOnChangedCallback(IConsoleVariable* ConsoleVariable); + } + + namespace Actor { + static bool IsPendingKill(AActor* Actor); + static bool IsRootComponentMovable(AActor* Actor); + static bool IsOverlappingActor(AActor* Actor, AActor* Other); + static void ForEachComponent(AActor* Actor, UActorComponent** Array, int32* Elements); + static void ForEachAttachedActor(AActor* Actor, AActor** Array, int32* Elements); + static void ForEachChildActor(AActor* Actor, AActor** Array, int32* Elements); + static void ForEachOverlappingActor(AActor* Actor, AActor** Array, int32* Elements); + static AActor* Spawn(const char* Name, ActorType Type, UObject* Blueprint); + static bool Destroy(AActor* Actor); + static void Rename(AActor* Actor, const char* Name); + static void Hide(AActor* Actor, bool Value); + static bool TeleportTo(AActor* Actor, const Vector3* DestinationLocation, const Quaternion* DestinationRotation, bool IsATest, bool NoCheck); + static UActorComponent* GetComponent(AActor* Actor, const char* Name, ComponentType Type); + static UActorComponent* GetComponentByTag(AActor* Actor, const char* Tag, ComponentType Type); + static UActorComponent* GetComponentByID(AActor* Actor, uint32 ID, ComponentType Type); + static USceneComponent* GetRootComponent(AActor* Actor, ComponentType Type); + static UInputComponent* GetInputComponent(AActor* Actor); + static float GetCreationTime(AActor* Actor); + static bool GetBlockInput(AActor* Actor); + static float GetDistanceTo(AActor* Actor, AActor* Other); + static float GetHorizontalDistanceTo(AActor* Actor, AActor* Other); + static void GetBounds(AActor* Actor, bool OnlyCollidingComponents, Vector3* Origin, Vector3* Extent); + static void GetEyesViewPoint(AActor* Actor, Vector3* Location, Quaternion* Rotation); + static bool SetRootComponent(AActor* Actor, USceneComponent* RootComponent); + static void SetInputComponent(AActor* Actor, UInputComponent* InputComponent); + static void SetBlockInput(AActor* Actor, bool Value); + static void SetLifeSpan(AActor* Actor, float LifeSpan); + static void SetEnableInput(AActor* Actor, APlayerController* PlayerController, bool Value); + static void SetEnableCollision(AActor* Actor, bool Value); + static void AddTag(AActor* Actor, const char* Tag); + static void RemoveTag(AActor* Actor, const char* Tag); + static bool HasTag(AActor* Actor, const char* Tag); + static void RegisterEvent(AActor* Actor, ActorEventType Type); + static void UnregisterEvent(AActor* Actor, ActorEventType Type); + } + + namespace GameModeBase { + static bool GetUseSeamlessTravel(AGameModeBase* GameModeBase); + static void SetUseSeamlessTravel(AGameModeBase* GameModeBase, bool Value); + static void SwapPlayerControllers(AGameModeBase* GameModeBase, APlayerController* PlayerController, APlayerController* NewPlayerController); + } + + namespace TriggerBase { } + + namespace TriggerBox { } + + namespace TriggerCapsule { } + + namespace TriggerSphere { } + + namespace Pawn { + static bool IsControlled(APawn* Pawn); + static bool IsPlayerControlled(APawn* Pawn); + static AutoPossessAI GetAutoPossessAI(APawn* Pawn); + static AutoReceiveInput GetAutoPossessPlayer(APawn* Pawn); + static bool GetUseControllerRotationYaw(APawn* Pawn); + static bool GetUseControllerRotationPitch(APawn* Pawn); + static bool GetUseControllerRotationRoll(APawn* Pawn); + static void GetGravityDirection(APawn* Pawn, Vector3* Value); + static AAIController* GetAIController(APawn* Pawn); + static APlayerController* GetPlayerController(APawn* Pawn); + static void SetAutoPossessAI(APawn* Pawn, AutoPossessAI Value); + static void SetAutoPossessPlayer(APawn* Pawn, AutoReceiveInput Value); + static void SetUseControllerRotationYaw(APawn* Pawn, bool Value); + static void SetUseControllerRotationPitch(APawn* Pawn, bool Value); + static void SetUseControllerRotationRoll(APawn* Pawn, bool Value); + static void AddControllerYawInput(APawn* Pawn, float Value); + static void AddControllerPitchInput(APawn* Pawn, float Value); + static void AddControllerRollInput(APawn* Pawn, float Value); + static void AddMovementInput(APawn* Pawn, const Vector3* WorldDirection, float ScaleValue, bool Force); + } + + namespace Character { + static bool IsCrouched(ACharacter* Character); + static bool CanCrouch(ACharacter* Character); + static bool CanJump(ACharacter* Character); + static void CheckJumpInput(ACharacter* Character, float DeltaTime); + static void ClearJumpInput(ACharacter* Character, float DeltaTime); + static void Launch(ACharacter* Character, const Vector3* Velocity, bool OverrideXY, bool OverrideZ); + static void Crouch(ACharacter* Character); + static void StopCrouching(ACharacter* Character); + static void Jump(ACharacter* Character); + static void StopJumping(ACharacter* Character); + static void SetOnLandedCallback(ACharacter* Character, CharacterLandedDelegate Callback); + } + + namespace Controller { + static bool IsLookInputIgnored(AController* Controller); + static bool IsMoveInputIgnored(AController* Controller); + static bool IsPlayerController(AController* Controller); + static APawn* GetPawn(AController* Controller); + static ACharacter* GetCharacter(AController* Controller); + static AActor* GetViewTarget(AController* Controller); + static void GetControlRotation(AController* Controller, Quaternion* Value); + static void GetDesiredRotation(AController* Controller, Quaternion* Value); + static bool LineOfSightTo(AController* Controller, AActor* Actor, const Vector3* ViewPoint, bool AlternateChecks); + static void SetControlRotation(AController* Controller, const Quaternion* Value); + static void SetInitialLocationAndRotation(AController* Controller, const Vector3* NewLocation, const Quaternion* NewRotation); + static void SetIgnoreLookInput(AController* Controller, bool Value); + static void SetIgnoreMoveInput(AController* Controller, bool Value); + static void ResetIgnoreLookInput(AController* Controller); + static void ResetIgnoreMoveInput(AController* Controller); + static void Possess(AController* Controller, APawn* Pawn); + static void Unpossess(AController* Controller); + } + + namespace AIController { + static void ClearFocus(AAIController* AIController, AIFocusPriority Priority); + static void GetFocalPoint(AAIController* AIController, Vector3* Value); + static void SetFocalPoint(AAIController* AIController, const Vector3* NewFocus, AIFocusPriority Priority); + static AActor* GetFocusActor(AAIController* AIController); + static bool GetAllowStrafe(AAIController* AIController); + static void SetAllowStrafe(AAIController* AIController, bool Value); + static void SetFocus(AAIController* AIController, AActor* NewFocus, AIFocusPriority Priority); + } + + namespace PlayerController { + static bool IsPaused(APlayerController* PlayerController); + static bool GetShowMouseCursor(APlayerController* PlayerController); + static bool GetEnableClickEvents(APlayerController* PlayerController); + static bool GetEnableMouseOverEvents(APlayerController* PlayerController); + static bool GetMousePosition(APlayerController* PlayerController, float* X, float* Y); + static UPlayer* GetPlayer(APlayerController* PlayerController); + static UPlayerInput* GetPlayerInput(APlayerController* PlayerController); + static bool GetHitResultAtScreenPosition(APlayerController* PlayerController, const Vector2* ScreenPosition, CollisionChannel TraceChannel, Hit* Hit, bool TraceComplex); + static bool GetHitResultUnderCursor(APlayerController* PlayerController, CollisionChannel TraceChannel, Hit* Hit, bool TraceComplex); + static void SetShowMouseCursor(APlayerController* PlayerController, bool Value); + static void SetEnableClickEvents(APlayerController* PlayerController, bool Value); + static void SetEnableMouseOverEvents(APlayerController* PlayerController, bool Value); + static void SetMousePosition(APlayerController* PlayerController, float X, float Y); + static void ConsoleCommand(APlayerController* PlayerController, const char* Command, bool WriteToLog); + static bool SetPause(APlayerController* PlayerController, bool Value); + static void SetViewTarget(APlayerController* PlayerController, AActor* NewViewTarget); + static void SetViewTargetWithBlend(APlayerController* PlayerController, AActor* NewViewTarget, float Time, float Exponent, BlendType Type, bool LockOutgoing); + static void AddYawInput(APlayerController* PlayerController, float Value); + static void AddPitchInput(APlayerController* PlayerController, float Value); + static void AddRollInput(APlayerController* PlayerController, float Value); + } + + namespace Volume { + static bool EncompassesPoint(AVolume* Volume, const Vector3* Point, float SphereRadius, float* OutDistanceToPoint); + } + + namespace TriggerVolume { } + + namespace PostProcessVolume { + static bool GetEnabled(APostProcessVolume* PostProcessVolume); + static float GetBlendRadius(APostProcessVolume* PostProcessVolume); + static float GetBlendWeight(APostProcessVolume* PostProcessVolume); + static bool GetUnbound(APostProcessVolume* PostProcessVolume); + static float GetPriority(APostProcessVolume* PostProcessVolume); + static void SetEnabled(APostProcessVolume* PostProcessVolume, bool Value); + static void SetBlendRadius(APostProcessVolume* PostProcessVolume, float Value); + static void SetBlendWeight(APostProcessVolume* PostProcessVolume, float Value); + static void SetUnbound(APostProcessVolume* PostProcessVolume, bool Value); + static void SetPriority(APostProcessVolume* PostProcessVolume, float Priority); + } + + namespace LevelScript { } + + namespace AmbientSound { } + + namespace Light { } + + namespace DirectionalLight { } + + namespace PointLight { } + + namespace RectLight { } + + namespace SpotLight { } + + namespace SoundBase { + static float GetDuration(USoundBase* SoundBase); + } + + namespace SoundWave { + static bool GetLoop(USoundWave* SoundWave); + static void SetLoop(USoundWave* SoundWave, bool Value); + } + + namespace AnimationAsset { + + } + + namespace AnimationSequenceBase { + + } + + namespace AnimationSequence { + + } + + namespace AnimationCompositeBase { + + } + + namespace AnimationMontage { + + } + + namespace AnimationInstance { + static UAnimMontage* GetCurrentActiveMontage(UAnimInstance* AnimationInstance); + static bool IsPlaying(UAnimInstance* AnimationInstance, UAnimMontage* Montage); + static float GetPlayRate(UAnimInstance* AnimationInstance, UAnimMontage* Montage); + static float GetPosition(UAnimInstance* AnimationInstance, UAnimMontage* Montage); + static float GetBlendTime(UAnimInstance* AnimationInstance, UAnimMontage* Montage); + static void GetCurrentSection(UAnimInstance* AnimationInstance, UAnimMontage* Montage, char* SectionName); + static void SetPlayRate(UAnimInstance* AnimationInstance, UAnimMontage* Montage, float Value); + static void SetPosition(UAnimInstance* AnimationInstance, UAnimMontage* Montage, float Position); + static void SetNextSection(UAnimInstance* AnimationInstance, UAnimMontage* Montage, const char* SectionToChange, const char* NextSection); + static float PlayMontage(UAnimInstance* AnimationInstance, UAnimMontage* Montage, float PlayRate, float TimeToStartMontageAt, bool StopAllMontages); + static void PauseMontage(UAnimInstance* AnimationInstance, UAnimMontage* Montage); + static void ResumeMontage(UAnimInstance* AnimationInstance, UAnimMontage* Montage); + static void StopMontage(UAnimInstance* AnimationInstance, UAnimMontage* Montage, float BlendOutTime); + static void JumpToSection(UAnimInstance* AnimationInstance, UAnimMontage* Montage, const char* SectionName); + static void JumpToSectionsEnd(UAnimInstance* AnimationInstance, UAnimMontage* Montage, const char* SectionName); + } + + namespace Player { + static APlayerController* GetPlayerController(UPlayer* Player); + } + + namespace PlayerInput { + static bool IsKeyPressed(UPlayerInput* PlayerInput, const char* Key); + static float GetTimeKeyPressed(UPlayerInput* PlayerInput, const char* Key); + static void GetMouseSensitivity(UPlayerInput* PlayerInput, Vector2* Value); + static void SetMouseSensitivity(UPlayerInput* PlayerInput, const Vector2* Value); + static void AddActionMapping(UPlayerInput* PlayerInput, const char* ActionName, const char* Key, bool Shift, bool Ctrl, bool Alt, bool Cmd); + static void AddAxisMapping(UPlayerInput* PlayerInput, const char* AxisName, const char* Key, float Scale); + static void RemoveActionMapping(UPlayerInput* PlayerInput, const char* ActionName, const char* Key); + static void RemoveAxisMapping(UPlayerInput* PlayerInput, const char* AxisName, const char* Key); + } + + namespace Font { + static void GetStringSize(UFont* Font, const char* Text, int32* Height, int32* Width); + } + + namespace StreamableRenderAsset { + + } + + namespace StaticMesh { + + } + + namespace SkeletalMesh { + + } + + namespace Texture { + + } + + namespace Texture2D { + static UTexture2D* CreateFromFile(const char* FilePath); + static UTexture2D* CreateFromBuffer(const uint8* Buffer, int32 Length); + static bool HasAlphaChannel(UTexture2D* Texture2D); + static void GetSize(UTexture2D* Texture2D, Vector2* Value); + static PixelFormat GetPixelFormat(UTexture2D* Texture2D); + } + + namespace ActorComponent { + static bool IsOwnerSelected(UActorComponent* ActorComponent); + static AActor* GetOwner(UActorComponent* ActorComponent, ActorType Type); + static void Destroy(UActorComponent* ActorComponent, bool PromoteChild); + static void AddTag(UActorComponent* ActorComponent, const char* Tag); + static void RemoveTag(UActorComponent* ActorComponent, const char* Tag); + static bool HasTag(UActorComponent* ActorComponent, const char* Tag); + } + + namespace InputComponent { + static bool HasBindings(UInputComponent* InputComponent); + static int32 GetActionBindingsNumber(UInputComponent* InputComponent); + static void ClearActionBindings(UInputComponent* InputComponent); + static void BindAction(UInputComponent* InputComponent, const char* ActionName, InputEvent KeyEvent, bool ExecutedWhenPaused, InputDelegate Callback); + static void BindAxis(UInputComponent* InputComponent, const char* AxisName, bool ExecutedWhenPaused, InputAxisDelegate Callback); + static void RemoveActionBinding(UInputComponent* InputComponent, const char* ActionName, InputEvent KeyEvent); + static bool GetBlockInput(UInputComponent* InputComponent); + static void SetBlockInput(UInputComponent* InputComponent, bool Value); + static int32 GetPriority(UInputComponent* InputComponent); + static void SetPriority(UInputComponent* InputComponent, int32 Value); + } + + namespace MovementComponent { + static bool GetConstrainToPlane(UMovementComponent* MovementComponent); + static bool GetSnapToPlaneAtStart(UMovementComponent* MovementComponent); + static bool GetUpdateOnlyIfRendered(UMovementComponent* MovementComponent); + static void GetVelocity(UMovementComponent* MovementComponent, Vector3* Value); + static PlaneConstraintAxis GetPlaneConstraint(UMovementComponent* MovementComponent); + static void GetPlaneConstraintNormal(UMovementComponent* MovementComponent, Vector3* Value); + static void GetPlaneConstraintOrigin(UMovementComponent* MovementComponent, Vector3* Value); + static float GetGravity(UMovementComponent* MovementComponent); + static float GetMaxSpeed(UMovementComponent* MovementComponent); + static void SetConstrainToPlane(UMovementComponent* MovementComponent, bool Value); + static void SetSnapToPlaneAtStart(UMovementComponent* MovementComponent, bool Value); + static void SetUpdateOnlyIfRendered(UMovementComponent* MovementComponent, bool Value); + static void SetVelocity(UMovementComponent* MovementComponent, const Vector3* Value); + static void SetPlaneConstraint(UMovementComponent* MovementComponent, PlaneConstraintAxis Value); + static void SetPlaneConstraintNormal(UMovementComponent* MovementComponent, const Vector3* Value); + static void SetPlaneConstraintOrigin(UMovementComponent* MovementComponent, const Vector3* Value); + static void SetPlaneConstraintFromVectors(UMovementComponent* MovementComponent, const Vector3* Forward, const Vector3* Up); + static bool IsExceedingMaxSpeed(UMovementComponent* MovementComponent, float MaxSpeed); + static bool IsInWater(UMovementComponent* MovementComponent); + static void StopMovement(UMovementComponent* MovementComponent); + static void ConstrainDirectionToPlane(UMovementComponent* MovementComponent, const Vector3* Direction, Vector3* Value); + static void ConstrainLocationToPlane(UMovementComponent* MovementComponent, const Vector3* Location, Vector3* Value); + static void ConstrainNormalToPlane(UMovementComponent* MovementComponent, const Vector3* Normal, Vector3* Value); + } + + namespace RotatingMovementComponent { + static URotatingMovementComponent* Create(AActor* Actor, const char* Name); + static bool GetRotationInLocalSpace(URotatingMovementComponent* RotatingMovementComponent); + static void GetPivotTranslation(URotatingMovementComponent* RotatingMovementComponent, Vector3* Value); + static void GetRotationRate(URotatingMovementComponent* RotatingMovementComponent, Quaternion* Value); + static void SetRotationInLocalSpace(URotatingMovementComponent* RotatingMovementComponent, bool Value); + static void SetPivotTranslation(URotatingMovementComponent* RotatingMovementComponent, const Vector3* Value); + static void SetRotationRate(URotatingMovementComponent* RotatingMovementComponent, const Quaternion* Value); + } + + namespace SceneComponent { + static bool IsAttachedToComponent(USceneComponent* SceneComponent, USceneComponent* Component); + static bool IsAttachedToActor(USceneComponent* SceneComponent, AActor* Actor); + static bool IsVisible(USceneComponent* SceneComponent); + static bool IsSocketExists(USceneComponent* SceneComponent, const char* SocketName); + static bool HasAnySockets(USceneComponent* SceneComponent); + static bool CanAttachAsChild(USceneComponent* SceneComponent, USceneComponent* ChildComponent, const char* SocketName); + static void ForEachAttachedChild(USceneComponent* SceneComponent, USceneComponent** Array, int32* Elements); + static USceneComponent* Create(AActor* Actor, ComponentType Type, const char* Name, bool SetAsRoot, UObject* Blueprint); + static bool AttachToComponent(USceneComponent* SceneComponent, USceneComponent* Parent, AttachmentTransformRule AttachmentRule, const char* SocketName); + static void DetachFromComponent(USceneComponent* SceneComponent, DetachmentTransformRule DetachmentRule); + static void Activate(USceneComponent* SceneComponent); + static void Deactivate(USceneComponent* SceneComponent); + static void UpdateToWorld(USceneComponent* SceneComponent, TeleportType Type, UpdateTransformFlags Flags); + static void AddLocalOffset(USceneComponent* SceneComponent, const Vector3* DeltaLocation); + static void AddLocalRotation(USceneComponent* SceneComponent, const Quaternion* DeltaRotation); + static void AddRelativeLocation(USceneComponent* SceneComponent, const Vector3* DeltaLocation); + static void AddRelativeRotation(USceneComponent* SceneComponent, const Quaternion* DeltaRotation); + static void AddLocalTransform(USceneComponent* SceneComponent, const Transform* DeltaTransform); + static void AddWorldOffset(USceneComponent* SceneComponent, const Vector3* DeltaLocation); + static void AddWorldRotation(USceneComponent* SceneComponent, const Quaternion* DeltaRotation); + static void AddWorldTransform(USceneComponent* SceneComponent, const Transform* DeltaTransform); + static void GetAttachedSocketName(USceneComponent* SceneComponent, char* SocketName); + static void GetBounds(USceneComponent* SceneComponent, const Transform* LocalToWorld, Bounds* Value); + static void GetSocketLocation(USceneComponent* SceneComponent, const char* SocketName, Vector3* Value); + static void GetSocketRotation(USceneComponent* SceneComponent, const char* SocketName, Quaternion* Value); + static void GetComponentVelocity(USceneComponent* SceneComponent, Vector3* Value); + static void GetComponentLocation(USceneComponent* SceneComponent, Vector3* Value); + static void GetComponentRotation(USceneComponent* SceneComponent, Quaternion* Value); + static void GetComponentScale(USceneComponent* SceneComponent, Vector3* Value); + static void GetComponentTransform(USceneComponent* SceneComponent, Transform* Value); + static void GetForwardVector(USceneComponent* SceneComponent, Vector3* Value); + static void GetRightVector(USceneComponent* SceneComponent, Vector3* Value); + static void GetUpVector(USceneComponent* SceneComponent, Vector3* Value); + static void SetMobility(USceneComponent* SceneComponent, ComponentMobility Mobility); + static void SetVisibility(USceneComponent* SceneComponent, bool NewVisibility, bool PropagateToChildren); + static void SetRelativeLocation(USceneComponent* SceneComponent, const Vector3* Location); + static void SetRelativeRotation(USceneComponent* SceneComponent, const Quaternion* Rotation); + static void SetRelativeTransform(USceneComponent* SceneComponent, const Transform* Transform); + static void SetWorldLocation(USceneComponent* SceneComponent, const Vector3* Location); + static void SetWorldRotation(USceneComponent* SceneComponent, const Quaternion* Rotation); + static void SetWorldScale(USceneComponent* SceneComponent, const Vector3* Scale); + static void SetWorldTransform(USceneComponent* SceneComponent, const Transform* Transform); + } + + namespace AudioComponent { + static bool IsPlaying(UAudioComponent* AudioComponent); + static bool GetPaused(UAudioComponent* AudioComponent); + static void SetSound(UAudioComponent* AudioComponent, USoundBase* Sound); + static void SetPaused(UAudioComponent* AudioComponent, bool Value); + static void Play(UAudioComponent* AudioComponent); + static void Stop(UAudioComponent* AudioComponent); + static void FadeIn(UAudioComponent* AudioComponent, float Duration, float VolumeLevel, float StartTime, AudioFadeCurve FadeCurve); + static void FadeOut(UAudioComponent* AudioComponent, float Duration, float VolumeLevel, AudioFadeCurve FadeCurve); + } + + namespace CameraComponent { + static bool GetConstrainAspectRatio(UCameraComponent* CameraComponent); + static float GetAspectRatio(UCameraComponent* CameraComponent); + static float GetFieldOfView(UCameraComponent* CameraComponent); + static float GetOrthoFarClipPlane(UCameraComponent* CameraComponent); + static float GetOrthoNearClipPlane(UCameraComponent* CameraComponent); + static float GetOrthoWidth(UCameraComponent* CameraComponent); + static bool GetLockToHeadMountedDisplay(UCameraComponent* CameraComponent); + static void SetProjectionMode(UCameraComponent* CameraComponent, CameraProjectionMode Mode); + static void SetConstrainAspectRatio(UCameraComponent* CameraComponent, bool Value); + static void SetAspectRatio(UCameraComponent* CameraComponent, float Value); + static void SetFieldOfView(UCameraComponent* CameraComponent, float Value); + static void SetOrthoFarClipPlane(UCameraComponent* CameraComponent, float Value); + static void SetOrthoNearClipPlane(UCameraComponent* CameraComponent, float Value); + static void SetOrthoWidth(UCameraComponent* CameraComponent, float Value); + static void SetLockToHeadMountedDisplay(UCameraComponent* CameraComponent, bool Value); + } + + namespace ChildActorComponent { + static AActor* GetChildActor(UChildActorComponent* ChildActorComponent, ActorType Type); + static AActor* SetChildActor(UChildActorComponent* ChildActorComponent, ActorType Type); + } + + namespace SpringArmComponent { + static bool IsCollisionFixApplied(USpringArmComponent* SpringArmComponent); + static bool GetDrawDebugLagMarkers(USpringArmComponent* SpringArmComponent); + static bool GetCollisionTest(USpringArmComponent* SpringArmComponent); + static bool GetCameraPositionLag(USpringArmComponent* SpringArmComponent); + static bool GetCameraRotationLag(USpringArmComponent* SpringArmComponent); + static bool GetCameraLagSubstepping(USpringArmComponent* SpringArmComponent); + static bool GetInheritPitch(USpringArmComponent* SpringArmComponent); + static bool GetInheritRoll(USpringArmComponent* SpringArmComponent); + static bool GetInheritYaw(USpringArmComponent* SpringArmComponent); + static float GetCameraLagMaxDistance(USpringArmComponent* SpringArmComponent); + static float GetCameraLagMaxTimeStep(USpringArmComponent* SpringArmComponent); + static float GetCameraPositionLagSpeed(USpringArmComponent* SpringArmComponent); + static float GetCameraRotationLagSpeed(USpringArmComponent* SpringArmComponent); + static CollisionChannel GetProbeChannel(USpringArmComponent* SpringArmComponent); + static float GetProbeSize(USpringArmComponent* SpringArmComponent); + static void GetSocketOffset(USpringArmComponent* SpringArmComponent, Vector3* Value); + static float GetTargetArmLength(USpringArmComponent* SpringArmComponent); + static void GetTargetOffset(USpringArmComponent* SpringArmComponent, Vector3* Value); + static void GetUnfixedCameraPosition(USpringArmComponent* SpringArmComponent, Vector3* Value); + static void GetDesiredRotation(USpringArmComponent* SpringArmComponent, Quaternion* Value); + static void GetTargetRotation(USpringArmComponent* SpringArmComponent, Quaternion* Value); + static bool GetUsePawnControlRotation(USpringArmComponent* SpringArmComponent); + static void SetDrawDebugLagMarkers(USpringArmComponent* SpringArmComponent, bool Value); + static void SetCollisionTest(USpringArmComponent* SpringArmComponent, bool Value); + static void SetCameraPositionLag(USpringArmComponent* SpringArmComponent, bool Value); + static void SetCameraRotationLag(USpringArmComponent* SpringArmComponent, bool Value); + static void SetCameraLagSubstepping(USpringArmComponent* SpringArmComponent, bool Value); + static void SetInheritPitch(USpringArmComponent* SpringArmComponent, bool Value); + static void SetInheritRoll(USpringArmComponent* SpringArmComponent, bool Value); + static void SetInheritYaw(USpringArmComponent* SpringArmComponent, bool Value); + static void SetCameraLagMaxDistance(USpringArmComponent* SpringArmComponent, float Value); + static void SetCameraLagMaxTimeStep(USpringArmComponent* SpringArmComponent, float Value); + static void SetCameraPositionLagSpeed(USpringArmComponent* SpringArmComponent, float Value); + static void SetCameraRotationLagSpeed(USpringArmComponent* SpringArmComponent, float Value); + static void SetProbeChannel(USpringArmComponent* SpringArmComponent, CollisionChannel Value); + static void SetProbeSize(USpringArmComponent* SpringArmComponent, float Value); + static void SetSocketOffset(USpringArmComponent* SpringArmComponent, const Vector3* Value); + static void SetTargetArmLength(USpringArmComponent* SpringArmComponent, float Value); + static void SetTargetOffset(USpringArmComponent* SpringArmComponent, const Vector3* Value); + static void SetUsePawnControlRotation(USpringArmComponent* SpringArmComponent, bool value); + } + + namespace PostProcessComponent { + static bool GetEnabled(UPostProcessComponent* PostProcessComponent); + static float GetBlendRadius(UPostProcessComponent* PostProcessComponent); + static float GetBlendWeight(UPostProcessComponent* PostProcessComponent); + static bool GetUnbound(UPostProcessComponent* PostProcessComponent); + static float GetPriority(UPostProcessComponent* PostProcessComponent); + static void SetEnabled(UPostProcessComponent* PostProcessComponent, bool Value); + static void SetBlendRadius(UPostProcessComponent* PostProcessComponent, float Value); + static void SetBlendWeight(UPostProcessComponent* PostProcessComponent, float Value); + static void SetUnbound(UPostProcessComponent* PostProcessComponent, bool Value); + static void SetPriority(UPostProcessComponent* PostProcessComponent, float Priority); + } + + namespace PrimitiveComponent { + static bool IsGravityEnabled(UPrimitiveComponent* PrimitiveComponent); + static bool IsOverlappingComponent(UPrimitiveComponent* PrimitiveComponent, UPrimitiveComponent* Other); + static void ForEachOverlappingComponent(UPrimitiveComponent* PrimitiveComponent, UPrimitiveComponent** Array, int32* Elements); + static void AddAngularImpulseInDegrees(UPrimitiveComponent* PrimitiveComponent, const Vector3* Impulse, const char* BoneName, bool VelocityChange); + static void AddAngularImpulseInRadians(UPrimitiveComponent* PrimitiveComponent, const Vector3* Impulse, const char* BoneName, bool VelocityChange); + static void AddForce(UPrimitiveComponent* PrimitiveComponent, const Vector3* Force, const char* BoneName, bool AccelerationChange); + static void AddForceAtLocation(UPrimitiveComponent* PrimitiveComponent, const Vector3* Force, const Vector3* Location, const char* BoneName, bool LocalSpace); + static void AddImpulse(UPrimitiveComponent* PrimitiveComponent, const Vector3* Impulse, const char* BoneName, bool VelocityChange); + static void AddImpulseAtLocation(UPrimitiveComponent* PrimitiveComponent, const Vector3* Impulse, const Vector3* Location, const char* BoneName); + static void AddRadialForce(UPrimitiveComponent* PrimitiveComponent, const Vector3* Origin, float Radius, float Strength, bool LinearFalloff, bool AccelerationChange); + static void AddRadialImpulse(UPrimitiveComponent* PrimitiveComponent, const Vector3* Origin, float Radius, float Strength, bool LinearFalloff, bool AccelerationChange); + static void AddTorqueInDegrees(UPrimitiveComponent* PrimitiveComponent, const Vector3* Torque, const char* BoneName, bool AccelerationChange); + static void AddTorqueInRadians(UPrimitiveComponent* PrimitiveComponent, const Vector3* Torque, const char* BoneName, bool AccelerationChange); + static float GetMass(UPrimitiveComponent* PrimitiveComponent); + static void GetPhysicsLinearVelocity(UPrimitiveComponent* PrimitiveComponent, Vector3* Value, const char* BoneName); + static void GetPhysicsLinearVelocityAtPoint(UPrimitiveComponent* PrimitiveComponent, Vector3* Value, const Vector3* Point, const char* BoneName); + static void GetPhysicsAngularVelocityInDegrees(UPrimitiveComponent* PrimitiveComponent, Vector3* Value, const char* BoneName); + static void GetPhysicsAngularVelocityInRadians(UPrimitiveComponent* PrimitiveComponent, Vector3* Value, const char* BoneName); + static bool GetCastShadow(UPrimitiveComponent* PrimitiveComponent); + static bool GetOnlyOwnerSee(UPrimitiveComponent* PrimitiveComponent); + static bool GetOwnerNoSee(UPrimitiveComponent* PrimitiveComponent); + static bool GetIgnoreRadialForce(UPrimitiveComponent* PrimitiveComponent); + static bool GetIgnoreRadialImpulse(UPrimitiveComponent* PrimitiveComponent); + static UMaterialInstanceDynamic* GetMaterial(UPrimitiveComponent* PrimitiveComponent, int32 ElementIndex); + static int32 GetMaterialsNumber(UPrimitiveComponent* PrimitiveComponent); + static float GetDistanceToCollision(UPrimitiveComponent* PrimitiveComponent, const Vector3* Point, Vector3* ClosestPointOnCollision); + static bool GetSquaredDistanceToCollision(UPrimitiveComponent* PrimitiveComponent, const Vector3* Point, float* SquaredDistance, Vector3* ClosestPointOnCollision); + static float GetAngularDamping(UPrimitiveComponent* PrimitiveComponent); + static float GetLinearDamping(UPrimitiveComponent* PrimitiveComponent); + static void SetGenerateOverlapEvents(UPrimitiveComponent* PrimitiveComponent, bool Value); + static void SetGenerateHitEvents(UPrimitiveComponent* PrimitiveComponent, bool Value); + static void SetMass(UPrimitiveComponent* PrimitiveComponent, float Mass, const char* BoneName); + static void SetCenterOfMass(UPrimitiveComponent* PrimitiveComponent, const Vector3* Offset, const char* BoneName); + static void SetPhysicsLinearVelocity(UPrimitiveComponent* PrimitiveComponent, const Vector3* Velocity, bool AddToCurrent, const char* BoneName); + static void SetPhysicsAngularVelocityInDegrees(UPrimitiveComponent* PrimitiveComponent, const Vector3* AngularVelocity, bool AddToCurrent, const char* BoneName); + static void SetPhysicsAngularVelocityInRadians(UPrimitiveComponent* PrimitiveComponent, const Vector3* AngularVelocity, bool AddToCurrent, const char* BoneName); + static void SetPhysicsMaxAngularVelocityInDegrees(UPrimitiveComponent* PrimitiveComponent, float MaxAngularVelocity, bool AddToCurrent, const char* BoneName); + static void SetPhysicsMaxAngularVelocityInRadians(UPrimitiveComponent* PrimitiveComponent, float MaxAngularVelocity, bool AddToCurrent, const char* BoneName); + static void SetCastShadow(UPrimitiveComponent* PrimitiveComponent, bool Value); + static void SetOnlyOwnerSee(UPrimitiveComponent* PrimitiveComponent, bool Value); + static void SetOwnerNoSee(UPrimitiveComponent* PrimitiveComponent, bool Value); + static void SetIgnoreRadialForce(UPrimitiveComponent* PrimitiveComponent, bool Value); + static void SetIgnoreRadialImpulse(UPrimitiveComponent* PrimitiveComponent, bool Value); + static void SetMaterial(UPrimitiveComponent* PrimitiveComponent, int32 ElementIndex, UMaterialInterface* Material); + static void SetSimulatePhysics(UPrimitiveComponent* PrimitiveComponent, bool Value); + static void SetAngularDamping(UPrimitiveComponent* PrimitiveComponent, float Value); + static void SetLinearDamping(UPrimitiveComponent* PrimitiveComponent, float Value); + static void SetEnableGravity(UPrimitiveComponent* PrimitiveComponent, bool Value); + static void SetCollisionMode(UPrimitiveComponent* PrimitiveComponent, CollisionMode Mode); + static void SetCollisionChannel(UPrimitiveComponent* PrimitiveComponent, CollisionChannel Channel); + static void SetCollisionProfileName(UPrimitiveComponent* PrimitiveComponent, const char* ProfileName, bool UpdateOverlaps); + static void SetCollisionResponseToChannel(UPrimitiveComponent* PrimitiveComponent, CollisionChannel Channel, CollisionResponse Response); + static void SetCollisionResponseToAllChannels(UPrimitiveComponent* PrimitiveComponent, CollisionResponse Response); + static void SetIgnoreActorWhenMoving(UPrimitiveComponent* PrimitiveComponent, AActor* Actor, bool Value); + static void SetIgnoreComponentWhenMoving(UPrimitiveComponent* PrimitiveComponent, UPrimitiveComponent* Component, bool Value); + static void ClearMoveIgnoreActors(UPrimitiveComponent* PrimitiveComponent); + static void ClearMoveIgnoreComponents(UPrimitiveComponent* PrimitiveComponent); + static UMaterialInstanceDynamic* CreateAndSetMaterialInstanceDynamic(UPrimitiveComponent* PrimitiveComponent, int32 ElementIndex); + static void RegisterEvent(UPrimitiveComponent* PrimitiveComponent, ComponentEventType Type); + static void UnregisterEvent(UPrimitiveComponent* PrimitiveComponent, ComponentEventType Type); + } + + namespace ShapeComponent { + static bool GetDynamicObstacle(UShapeComponent* ShapeComponent); + static int32 GetShapeColor(UShapeComponent* ShapeComponent); + static void SetDynamicObstacle(UShapeComponent* ShapeComponent, bool Value); + static void SetShapeColor(UShapeComponent* ShapeComponent, Color Value); + } + + namespace BoxComponent { + static void GetScaledBoxExtent(UBoxComponent* BoxComponent, Vector3* Value); + static void GetUnscaledBoxExtent(UBoxComponent* BoxComponent, Vector3* Value); + static void SetBoxExtent(UBoxComponent* BoxComponent, const Vector3* Extent, bool UpdateOverlaps); + static void InitBoxExtent(UBoxComponent* BoxComponent, const Vector3* Extent); + } + + namespace SphereComponent { + static float GetScaledSphereRadius(USphereComponent* SphereComponent); + static float GetUnscaledSphereRadius(USphereComponent* SphereComponent); + static float GetShapeScale(USphereComponent* SphereComponent); + static void SetSphereRadius(USphereComponent* SphereComponent, float SphereRadius, bool UpdateOverlaps); + static void InitSphereRadius(USphereComponent* SphereComponent, float SphereRadius); + } + + namespace CapsuleComponent { + static float GetScaledCapsuleRadius(UCapsuleComponent* CapsuleComponent); + static float GetUnscaledCapsuleRadius(UCapsuleComponent* CapsuleComponent); + static float GetShapeScale(UCapsuleComponent* CapsuleComponent); + static void GetScaledCapsuleSize(UCapsuleComponent* CapsuleComponent, float* Radius, float* HalfHeight); + static void GetUnscaledCapsuleSize(UCapsuleComponent* CapsuleComponent, float* Radius, float* HalfHeight); + static void SetCapsuleRadius(UCapsuleComponent* CapsuleComponent, float Radius, bool UpdateOverlaps); + static void SetCapsuleSize(UCapsuleComponent* CapsuleComponent, float Radius, float HalfHeight, bool UpdateOverlaps); + static void InitCapsuleSize(UCapsuleComponent* CapsuleComponent, float Radius, float HalfHeight); + } + + namespace MeshComponent { + static bool IsValidMaterialSlotName(UMeshComponent* MeshComponent, const char* MaterialSlotName); + static int32 GetMaterialIndex(UMeshComponent* MeshComponent, const char* MaterialSlotName); + } + + namespace TextRenderComponent { + static void SetFont(UTextRenderComponent* TextRenderComponent, UFont* Value); + static void SetText(UTextRenderComponent* TextRenderComponent, const char* Value); + static void SetTextMaterial(UTextRenderComponent* TextRenderComponent, UMaterialInterface* Material); + static void SetTextRenderColor(UTextRenderComponent* TextRenderComponent, Color Value); + static void SetHorizontalAlignment(UTextRenderComponent* TextRenderComponent, HorizontalTextAligment Value); + static void SetHorizontalSpacingAdjustment(UTextRenderComponent* TextRenderComponent, float Value); + static void SetVerticalAlignment(UTextRenderComponent* TextRenderComponent, VerticalTextAligment Value); + static void SetVerticalSpacingAdjustment(UTextRenderComponent* TextRenderComponent, float Value); + static void SetScale(UTextRenderComponent* TextRenderComponent, const Vector2* Value); + static void SetWorldSize(UTextRenderComponent* TextRenderComponent, float Value); + } + + namespace LightComponentBase { + static float GetIntensity(ULightComponentBase* LightComponentBase); + static bool GetCastShadows(ULightComponentBase* LightComponentBase); + static void SetCastShadows(ULightComponentBase* LightComponentBase, bool Value); + } + + namespace LightComponent { + static void SetIntensity(ULightComponent* LightComponent, float Value); + static void SetLightColor(ULightComponent* LightComponent, const LinearColor* Value); + } + + namespace DirectionalLightComponent { + + } + + namespace MotionControllerComponent { + static bool IsTracked(UMotionControllerComponent* MotionControllerComponent); + static bool GetDisplayDeviceModel(UMotionControllerComponent* MotionControllerComponent); + static bool GetDisableLowLatencyUpdate(UMotionControllerComponent* MotionControllerComponent); + static ControllerHand GetTrackingSource(UMotionControllerComponent* MotionControllerComponent); + static void SetDisplayDeviceModel(UMotionControllerComponent* MotionControllerComponent, bool Value); + static void SetDisableLowLatencyUpdate(UMotionControllerComponent* MotionControllerComponent, bool Value); + static void SetTrackingSource(UMotionControllerComponent* MotionControllerComponent, ControllerHand Value); + static void SetTrackingMotionSource(UMotionControllerComponent* MotionControllerComponent, const char* Source); + static void SetAssociatedPlayerIndex(UMotionControllerComponent* MotionControllerComponent, int32 PlayerIndex); + static void SetCustomDisplayMesh(UMotionControllerComponent* MotionControllerComponent, UStaticMesh* StaticMesh); + static void SetDisplayModelSource(UMotionControllerComponent* MotionControllerComponent, const char* Source); + } + + namespace StaticMeshComponent { + static void GetLocalBounds(UStaticMeshComponent* StaticMeshComponent, Vector3* Min, Vector3* Max); + static UStaticMesh* GetStaticMesh(UStaticMeshComponent* StaticMeshComponent); + static bool SetStaticMesh(UStaticMeshComponent* StaticMeshComponent, UStaticMesh* StaticMesh); + } + + namespace InstancedStaticMeshComponent { + static int32 GetInstanceCount(UInstancedStaticMeshComponent* InstancedStaticMeshComponent); + static bool GetInstanceTransform(UInstancedStaticMeshComponent* InstancedStaticMeshComponent, int32 InstanceIndex, Transform* Value, bool WorldSpace); + static void AddInstance(UInstancedStaticMeshComponent* InstancedStaticMeshComponent, const Transform* InstanceTransform); + static void AddInstances(UInstancedStaticMeshComponent* InstancedStaticMeshComponent, int32 EndInstanceIndex, const Transform InstanceTransforms[]); + static bool UpdateInstanceTransform(UInstancedStaticMeshComponent* InstancedStaticMeshComponent, int32 InstanceIndex, const Transform* InstanceTransform, bool WorldSpace, bool MarkRenderStateDirty, bool Teleport); + static bool BatchUpdateInstanceTransforms(UInstancedStaticMeshComponent* InstancedStaticMeshComponent, int32 StartInstanceIndex, int32 EndInstanceIndex, const Transform InstanceTransforms[], bool WorldSpace, bool MarkRenderStateDirty, bool Teleport); + static bool RemoveInstance(UInstancedStaticMeshComponent* InstancedStaticMeshComponent, int32 InstanceIndex); + static void ClearInstances(UInstancedStaticMeshComponent* InstancedStaticMeshComponent); + } + + namespace HierarchicalInstancedStaticMeshComponent { + static bool GetDisableCollision(UHierarchicalInstancedStaticMeshComponent* HierarchicalInstancedStaticMeshComponent); + static void SetDisableCollision(UHierarchicalInstancedStaticMeshComponent* HierarchicalInstancedStaticMeshComponent, bool Value); + } + + namespace SkinnedMeshComponent { + static int32 GetBonesNumber(USkinnedMeshComponent* SkinnedMeshComponent); + static int32 GetBoneIndex(USkinnedMeshComponent* SkinnedMeshComponent, const char* BoneName); + static void GetBoneName(USkinnedMeshComponent* SkinnedMeshComponent, int32 BoneIndex, char* BoneName); + static void GetBoneTransform(USkinnedMeshComponent* SkinnedMeshComponent, int32 BoneIndex, Transform* Value); + static void SetSkeletalMesh(USkinnedMeshComponent* SkinnedMeshComponent, USkeletalMesh* SkeletalMesh, bool ReinitializePose); + } + + namespace SkeletalMeshComponent { + static bool IsPlaying(USkeletalMeshComponent* SkeletalMeshComponent); + static UAnimInstance* GetAnimationInstance(USkeletalMeshComponent* SkeletalMeshComponent); + static void SetAnimation(USkeletalMeshComponent* SkeletalMeshComponent, UAnimationAsset* Asset); + static void SetAnimationMode(USkeletalMeshComponent* SkeletalMeshComponent, AnimationMode Mode); + static void SetAnimationBlueprint(USkeletalMeshComponent* SkeletalMeshComponent, UObject* Blueprint); + static void Play(USkeletalMeshComponent* SkeletalMeshComponent, bool Loop); + static void PlayAnimation(USkeletalMeshComponent* SkeletalMeshComponent, UAnimationAsset* Asset, bool Loop); + static void Stop(USkeletalMeshComponent* SkeletalMeshComponent); + } + + namespace SplineComponent { + static bool IsClosedLoop(USplineComponent* SplineComponent); + static float GetDuration(USplineComponent* SplineComponent); + static SplinePointType GetSplinePointType(USplineComponent* SplineComponent, int32 PointIndex); + static int32 GetSplinePointsNumber(USplineComponent* SplineComponent); + static int32 GetSplineSegmentsNumber(USplineComponent* SplineComponent); + static void GetTangentAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetTangentAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetTangentAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity, Vector3* Value); + static void GetTransformAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Transform* Value); + static void GetTransformAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, bool UseScale, Transform* Value); + static void GetArriveTangentAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetDefaultUpVector(USplineComponent* SplineComponent, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetDirectionAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetDirectionAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetDirectionAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity, Vector3* Value); + static float GetDistanceAlongSplineAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex); + static void GetLeaveTangentAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetLocationAndTangentAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Location, Vector3* Tangent); + static void GetLocationAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetLocationAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetLocationAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetRightVectorAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetRightVectorAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetRightVectorAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity, Vector3* Value); + static float GetRollAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace); + static float GetRollAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace); + static float GetRollAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity); + static void GetRotationAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Quaternion* Value); + static void GetRotationAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Quaternion* Value); + static void GetRotationAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity, Quaternion* Value); + static void GetScaleAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, Vector3* Value); + static void GetScaleAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, Vector3* Value); + static void GetScaleAtTime(USplineComponent* SplineComponent, float Time, bool UseConstantVelocity, Vector3* Value); + static float GetSplineLength(USplineComponent* SplineComponent); + static void GetTransformAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity, bool UseScale, Transform* Value); + static void GetUpVectorAtDistanceAlongSpline(USplineComponent* SplineComponent, float Distance, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetUpVectorAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void GetUpVectorAtTime(USplineComponent* SplineComponent, float Time, SplineCoordinateSpace CoordinateSpace, bool UseConstantVelocity, Vector3* Value); + static void SetDuration(USplineComponent* SplineComponent, float Value); + static void SetSplinePointType(USplineComponent* SplineComponent, int32 PointIndex, SplinePointType Type, bool UpdateSpline); + static void SetClosedLoop(USplineComponent* SplineComponent, bool Value, bool UpdateSpline); + static void SetDefaultUpVector(USplineComponent* SplineComponent, const Vector3* Value, SplineCoordinateSpace CoordinateSpace); + static void SetLocationAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, const Vector3* Value, SplineCoordinateSpace CoordinateSpace, bool UpdateSpline); + static void SetTangentAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, const Vector3* Tangent, SplineCoordinateSpace CoordinateSpace, bool UpdateSpline); + static void SetTangentsAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, const Vector3* ArriveTangent, const Vector3* LeaveTangent, SplineCoordinateSpace CoordinateSpace, bool UpdateSpline); + static void SetUpVectorAtSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, const Vector3* UpVector, SplineCoordinateSpace CoordinateSpace, bool UpdateSpline); + static void AddSplinePoint(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, bool UpdateSpline); + static void AddSplinePointAtIndex(USplineComponent* SplineComponent, const Vector3* Location, int32 PointIndex, SplineCoordinateSpace CoordinateSpace, bool UpdateSpline); + static void ClearSplinePoints(USplineComponent* SplineComponent, bool UpdateSpline); + static void FindDirectionClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void FindLocationClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void FindUpVectorClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void FindRightVectorClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static float FindRollClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace); + static void FindScaleClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, Vector3* Value); + static void FindTangentClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, Vector3* Value); + static void FindTransformClosestToWorldLocation(USplineComponent* SplineComponent, const Vector3* Location, SplineCoordinateSpace CoordinateSpace, bool UseScale, Transform* Value); + static void RemoveSplinePoint(USplineComponent* SplineComponent, int32 PointIndex, bool UpdateSpline); + static void UpdateSpline(USplineComponent* SplineComponent); + } + + namespace RadialForceComponent { + static bool GetIgnoreOwningActor(URadialForceComponent* RadialForceComponent); + static bool GetImpulseVelocityChange(URadialForceComponent* RadialForceComponent); + static bool GetLinearFalloff(URadialForceComponent* RadialForceComponent); + static float GetForceStrength(URadialForceComponent* RadialForceComponent); + static float GetImpulseStrength(URadialForceComponent* RadialForceComponent); + static float GetRadius(URadialForceComponent* RadialForceComponent); + static void SetIgnoreOwningActor(URadialForceComponent* RadialForceComponent, bool Value); + static void SetImpulseVelocityChange(URadialForceComponent* RadialForceComponent, bool Value); + static void SetLinearFalloff(URadialForceComponent* RadialForceComponent, bool Value); + static void SetForceStrength(URadialForceComponent* RadialForceComponent, float Value); + static void SetImpulseStrength(URadialForceComponent* RadialForceComponent, float Value); + static void SetRadius(URadialForceComponent* RadialForceComponent, float Value); + static void AddCollisionChannelToAffect(URadialForceComponent* RadialForceComponent, CollisionChannel Channel); + static void FireImpulse(URadialForceComponent* RadialForceComponent); + } + + namespace MaterialInterface { + static bool IsTwoSided(UMaterialInterface* MaterialInterface); + } + + namespace Material { + static bool IsDefaultMaterial(UMaterial* Material); + } + + namespace MaterialInstance { + static bool IsChildOf(UMaterialInstance* MaterialInstance, UMaterialInterface* Material); + static UMaterialInstanceDynamic* GetParent(UMaterialInstance* MaterialInstance); + } + + namespace MaterialInstanceDynamic { + static void ClearParameterValues(UMaterialInstanceDynamic* MaterialInstanceDynamic); + static void SetTextureParameterValue(UMaterialInstanceDynamic* MaterialInstanceDynamic, const char* ParameterName, UTexture* Value); + static void SetVectorParameterValue(UMaterialInstanceDynamic* MaterialInstanceDynamic, const char* ParameterName, const LinearColor* Value); + static void SetScalarParameterValue(UMaterialInstanceDynamic* MaterialInstanceDynamic, const char* ParameterName, float Value); + } +} +``` + +`Source/Native/Source/UnrealCLR/Public/UnrealCLRLibrary.h`: + +```h +/* + * Unreal Engine .NET 6 integration + * Copyright (c) 2021 Stanislav Denisov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" +#include "UnrealCLRLibrary.generated.h" + +USTRUCT(BlueprintType) +struct UNREALCLR_API FManagedFunction { + GENERATED_BODY() + + public: + + void* Pointer; + + FManagedFunction(); +}; + +UCLASS() +class UNREALCLR_API UUnrealCLRLibrary : public UBlueprintFunctionLibrary { + GENERATED_UCLASS_BODY() + + public: + + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = ".NET", meta = (ToolTip = "Executes the managed function with optional object reference argument")) + static void ExecuteManagedFunction(FManagedFunction ManagedFunction, UObject* Object); + + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = ".NET", meta = (ToolTip = "Finds the managed function from loaded assembly, optional parameter suppresses errors if the function was not found")) + static FManagedFunction FindManagedFunction(FString Method, bool Optional, bool& Result); +}; + +UCLASS() +class UNREALCLR_API UUnrealCLRCharacter : public UObject { + GENERATED_UCLASS_BODY() + + public: + + void* LandedCallback; + + UFUNCTION() + void Landed(const FHitResult& Hit); +}; +``` + +`Source/Native/Source/UnrealCLR/Public/UnrealCLRManager.h`: + +```h +/* + * Unreal Engine .NET 6 integration + * Copyright (c) 2021 Stanislav Denisov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "UnrealCLRManager.generated.h" + +UCLASS() +class UNREALCLR_API UUnrealCLRManager : public UObject { + GENERATED_BODY() + + public: + + UFUNCTION() + void ActorBeginOverlap(AActor* OverlapActor, AActor* OtherActor); + + UFUNCTION() + void ActorEndOverlap(AActor* OverlapActor, AActor* OtherActor); + + UFUNCTION() + void ActorHit(AActor* HitActor, AActor* OtherActor, FVector NormalImpulse, const FHitResult& Hit); + + UFUNCTION() + void ActorBeginCursorOver(AActor* Actor); + + UFUNCTION() + void ActorEndCursorOver(AActor* Actor); + + UFUNCTION() + void ActorClicked(AActor* Actor, FKey Button); + + UFUNCTION() + void ActorReleased(AActor* Actor, FKey Button); + + UFUNCTION() + void ComponentBeginOverlap(UPrimitiveComponent* OverlapComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, int32 OtherBodyIndex, bool FromSweep, const FHitResult& SweepResult); + + UFUNCTION() + void ComponentEndOverlap(UPrimitiveComponent* OverlapComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, int32 OtherBodyIndex); + + UFUNCTION() + void ComponentHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit); + + UFUNCTION() + void ComponentBeginCursorOver(UPrimitiveComponent* Component); + + UFUNCTION() + void ComponentEndCursorOver(UPrimitiveComponent* Component); + + UFUNCTION() + void ComponentClicked(UPrimitiveComponent* Component, FKey Key); + + UFUNCTION() + void ComponentReleased(UPrimitiveComponent* Component, FKey Key); +}; +``` + +`Source/Native/Source/UnrealCLR/UnrealCLR.Build.cs`: + +```cs +/* + * Unreal Engine .NET 6 integration + * Copyright (c) 2021 Stanislav Denisov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System; +using System.IO; +using UnrealBuildTool; + +public class UnrealCLR : ModuleRules { + public UnrealCLR(ReadOnlyTargetRules Target) : base(Target) { + #if UE_4_24_OR_LATER + bLegacyPublicIncludePaths = false; + DefaultBuildSettings = BuildSettingsVersion.V2; + #else + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + #endif + + PublicIncludePaths.AddRange(new string[] { }); + + PrivateIncludePaths.AddRange(new string[] { }); + + PublicDependencyModuleNames.AddRange(new string[] { + "Core" + }); + + PrivateDependencyModuleNames.AddRange(new string[] { + "AIModule", + "AssetRegistry", + "CoreUObject", + "Engine", + "HeadMountedDisplay", + "InputCore", + "Slate", + "SlateCore" + }); + + DynamicallyLoadedModuleNames.AddRange(new string[] { }); + + if (Target.bBuildEditor) { + PrivateDependencyModuleNames.AddRange(new string[] { + "UnrealEd" + }); + } else { + string runtimePath = null; + + if (Target.Platform == UnrealTargetPlatform.Win64) + runtimePath = Path.Combine(ModuleDirectory, "../../Runtime/Win64"); + else if (Target.Platform == UnrealTargetPlatform.Linux) + runtimePath = Path.Combine(ModuleDirectory, "../../Runtime/Linux"); + else if (Target.Platform == UnrealTargetPlatform.Mac) + runtimePath = Path.Combine(ModuleDirectory, "../../Runtime/Mac"); + else + throw new Exception("Unknown platform"); + + string[] files = Directory.GetFiles(runtimePath, "*.*", SearchOption.AllDirectories); + + foreach (string file in files) { + RuntimeDependencies.Add(file); + } + + files = Directory.GetFiles(Path.Combine(ModuleDirectory, "../../Managed"), "*.*", SearchOption.AllDirectories); + + foreach (string file in files) { + RuntimeDependencies.Add(file); + } + + string userAssemblies = Path.Combine(PluginDirectory , "../../Managed"); + + if (Directory.Exists(userAssemblies)) { + files = Directory.GetFiles(userAssemblies, "*.*", SearchOption.AllDirectories); + + foreach (string file in files) { + RuntimeDependencies.Add(file); + } + } + } + } +} +``` + +`Source/Native/UnrealCLR.uplugin`: + +```uplugin +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.32", + "FriendlyName": "UnrealCLR", + "Description": ".NET 6 integration", + "Category": "Scripting", + "CreatedBy": "nxrighthere", + "CreatedByURL": "https://github.com/nxrighthere", + "DocsURL": "https://github.com/nxrighthere/UnrealCLR", + "MarketplaceURL": "", + "SupportURL": "https://github.com/nxrighthere/UnrealCLR/discussions", + "EnabledByDefault": true, + "CanContainContent": false, + "IsBetaVersion": false, + "IsExperimentalVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "UnrealCLR", + "Type": "Runtime", + "LoadingPhase": "PreLoadingScreen", + "WhitelistPlatforms": [ + "Win64", + "Linux", + "Mac" + ] + } + ] +} +``` \ No newline at end of file diff --git a/archive/rdeioris/LuaMachine.txt b/archive/rdeioris/LuaMachine.txt new file mode 100644 index 00000000..c1103a41 --- /dev/null +++ b/archive/rdeioris/LuaMachine.txt @@ -0,0 +1,15543 @@ +Project Path: arc_rdeioris_LuaMachine_k8e_f0e8 + +Source Tree: + +```txt +arc_rdeioris_LuaMachine_k8e_f0e8 +├── BuildingNotes.md +├── Config +│ └── FilterPlugin.ini +├── Content +│ ├── Code +│ │ ├── ReflectionMetaTable.uasset +│ │ ├── TestActorCode.uasset +│ │ ├── TestBrokenCode.uasset +│ │ ├── TestCoroutines.uasset +│ │ ├── TestDummyCode.uasset +│ │ ├── TestMainCode.uasset +│ │ └── TestProxyCall.uasset +│ ├── LuaMachine_Tests.umap +│ ├── LuaReflectionTestState.uasset +│ ├── LuaTestActor.uasset +│ ├── LuaTestState.uasset +│ ├── Scripts +│ │ ├── submodules +│ │ │ └── test004.lua +│ │ ├── test001.lua +│ │ ├── test002.lua +│ │ ├── test003.lua +│ │ ├── test011.lua +│ │ └── trackedmodules +│ │ └── test005.lua +│ ├── TestAnotherTableInterface.uasset +│ ├── TestTableInterface.uasset +│ └── Tests +│ ├── TestActorField.uasset +│ ├── TestActorInception.uasset +│ ├── TestActorMethod.uasset +│ ├── TestActorMethodMulti.uasset +│ ├── TestActorMethodMultiNoArgs.uasset +│ ├── TestActorMethodValueMultiNoArgs.uasset +│ ├── TestActorReflectionBoolean.uasset +│ ├── TestActorReflectionBooleanFalse.uasset +│ ├── TestActorReflectionFunction.uasset +│ ├── TestActorReflectionFunctionK2.uasset +│ ├── TestActorReflectionFunctionStruct.uasset +│ ├── TestActorReflectionMulticastDelegate.uasset +│ ├── TestActorReflectionString.uasset +│ ├── TestActorReflectionStringSet.uasset +│ ├── TestActorSetField.uasset +│ ├── TestActorSetFieldOfTable.uasset +│ ├── TestActorToLuaComponent.uasset +│ ├── TestActorToLuaComponentByName.uasset +│ ├── TestActorToLuaComponentByName2.uasset +│ ├── TestActorToLuaComponentByStateAndName.uasset +│ ├── TestActorToLuaComponentFromState.uasset +│ ├── TestActorUFunction.uasset +│ ├── TestActorUFunctionAsValue.uasset +│ ├── TestActorUFunctionAsValueRaw.uasset +│ ├── TestArrayCreation.uasset +│ ├── TestCallTable.uasset +│ ├── TestCallTableWithSelf.uasset +│ ├── TestCallValueMulti.uasset +│ ├── TestCleanLazyTable.uasset +│ ├── TestCleanTable.uasset +│ ├── TestCoroutine.uasset +│ ├── TestCoroutineResume.uasset +│ ├── TestCoroutineResumeMulti.uasset +│ ├── TestCoroutineResumeMulti_BP.uasset +│ ├── TestCoroutineResume__newindex.uasset +│ ├── TestCoroutineResume__preload.uasset +│ ├── TestGC.uasset +│ ├── TestGetGlobalTable.uasset +│ ├── TestGetGlobalTableFallback.uasset +│ ├── TestGetGlobalTableNil.uasset +│ ├── TestGlobalCallValueMulti.uasset +│ ├── TestIterateArrayKeys.uasset +│ ├── TestIterateArrayValues.uasset +│ ├── TestIterateTableValues.uasset +│ ├── TestLuaValueCallError.uasset +│ ├── TestLuaValueCallErrorReferenced.uasset +│ ├── TestMultiArgs.uasset +│ ├── TestOwnage.uasset +│ ├── TestOwnageComplex.uasset +│ ├── TestRunAsset.uasset +│ ├── TestRunBrokenAsset.uasset +│ ├── TestRunFileError.uasset +│ ├── TestRunFiles.uasset +│ ├── TestRunNonExistingFile.uasset +│ ├── TestRunString.uasset +│ ├── TestRunStringWithVariable.uasset +│ ├── TestSetGlobalTable.uasset +│ ├── TestSetGlobalTableCreation.uasset +│ ├── TestSetGlobalTableNativeComplex.uasset +│ ├── TestSetGlobalTableSharedWithUnreal.uasset +│ ├── TestSetGlobalTableTable.uasset +│ ├── TestStack.uasset +│ ├── TestStateReload.uasset +│ ├── TestTableAsset.uasset +│ ├── TestTableFields.uasset +│ ├── TestTableImplements.uasset +│ ├── TestTableImplementsAll.uasset +│ ├── TestTableImplementsAllRedLight.uasset +│ ├── TestTableImplementsAny.uasset +│ ├── TestTablePointers.uasset +│ ├── TestTypes.uasset +│ ├── TestUserDataMetaTable.uasset +│ ├── TestUserDataMetaTableCreateObjectInState.uasset +│ └── TestUserDataMetaTableGet.uasset +├── Docs +│ ├── LuaBlueprintFunctionLibrary.md +│ ├── LuaComponent.md +│ ├── LuaCoroutines.md +│ ├── LuaMachine_Logo.png +│ ├── ReflectionAPI.md +│ ├── Screenshots +│ │ ├── Android24.PNG +│ │ ├── Coroutine.PNG +│ │ ├── Debugger.PNG +│ │ ├── LuaCodeBox.PNG +│ │ ├── LuaComponent.PNG +│ │ ├── LuaComponent2.PNG +│ │ ├── LuaComponent3.PNG +│ │ ├── LuaComponent4.PNG +│ │ ├── LuaComponent5.PNG +│ │ ├── LuaConsole.PNG +│ │ ├── Packaging.PNG +│ │ ├── Packaging2.PNG +│ │ ├── PlayerToLuaComponent.PNG +│ │ ├── Quickstart.PNG +│ │ ├── Quickstart2.PNG +│ │ ├── Quickstart3.PNG +│ │ ├── Quickstart4.PNG +│ │ ├── Quickstart5.PNG +│ │ ├── Quickstart6.PNG +│ │ ├── Quickstart7.PNG +│ │ ├── Quickstart8.PNG +│ │ └── SyntaxColor.PNG +│ └── TipsAndTricks.md +├── LICENSE +├── LuaMachine.uplugin +├── README.md +├── Resources +│ ├── Icon128.png +│ └── LuaTableAsset256.png +├── Source +│ ├── LuaMachine +│ │ ├── LuaMachine.Build.cs +│ │ ├── Private +│ │ │ ├── LuaBlueprintFunctionLibrary.cpp +│ │ │ ├── LuaBlueprintPackage.cpp +│ │ │ ├── LuaCode.cpp +│ │ │ ├── LuaCommandExecutor.cpp +│ │ │ ├── LuaComponent.cpp +│ │ │ ├── LuaDelegate.cpp +│ │ │ ├── LuaGlobalNameComponent.cpp +│ │ │ ├── LuaMachine.cpp +│ │ │ ├── LuaMultiLineEditableTextBox.cpp +│ │ │ ├── LuaState.cpp +│ │ │ ├── LuaSyntaxHighlighter.cpp +│ │ │ ├── LuaTableAsset.cpp +│ │ │ ├── LuaUserDataObject.cpp +│ │ │ └── LuaValue.cpp +│ │ └── Public +│ │ ├── LuaBlueprintFunctionLibrary.h +│ │ ├── LuaBlueprintPackage.h +│ │ ├── LuaCode.h +│ │ ├── LuaCommandExecutor.h +│ │ ├── LuaComponent.h +│ │ ├── LuaDelegate.h +│ │ ├── LuaGlobalNameComponent.h +│ │ ├── LuaMachine.h +│ │ ├── LuaMultiLineEditableTextBox.h +│ │ ├── LuaState.h +│ │ ├── LuaSyntaxHighlighter.h +│ │ ├── LuaTableAsset.h +│ │ ├── LuaUserDataObject.h +│ │ └── LuaValue.h +│ ├── LuaMachineEditor +│ │ ├── LuaMachineEditor.Build.cs +│ │ ├── Private +│ │ │ ├── LuaCodeCustomization.cpp +│ │ │ ├── LuaCodeFactory.cpp +│ │ │ ├── LuaMachineEditor.cpp +│ │ │ └── LuaValueCustomization.cpp +│ │ └── Public +│ │ ├── LuaCodeCustomization.h +│ │ ├── LuaCodeFactory.h +│ │ ├── LuaMachineEditor.h +│ │ └── LuaValueCustomization.h +│ └── ThirdParty +│ ├── ARM64 +│ ├── ARMv7 +│ ├── lua +│ │ ├── lauxlib.h +│ │ ├── lua.h +│ │ ├── lua.hpp +│ │ ├── luaconf.h +│ │ └── lualib.h +│ └── x64 +└── Tutorials + ├── JsonLuaBlueprintPackage.md + ├── ReflectionShell.md + ├── RickAndMortyAPI.md + ├── RickAndMortyAPI_Data + │ ├── RickAndMorty.PNG + │ ├── RickAndMorty001.PNG + │ ├── RickAndMorty002.PNG + │ ├── RickAndMorty003.PNG + │ ├── RickAndMorty004.PNG + │ ├── RickAndMorty005.PNG + │ ├── RickAndMorty006.PNG + │ ├── RickAndMorty007.PNG + │ ├── RickAndMorty008.PNG + │ ├── RickAndMorty009.PNG + │ ├── RickAndMorty010.PNG + │ ├── RickAndMorty011.PNG + │ ├── RickAndMorty012.PNG + │ ├── RickAndMorty013.PNG + │ ├── RickAndMorty014.PNG + │ ├── RickAndMorty015.PNG + │ ├── RickAndMorty016.PNG + │ ├── RickAndMorty017.PNG + │ ├── RickAndMorty018.PNG + │ ├── RickAndMorty019.PNG + │ ├── RickAndMorty020.PNG + │ ├── RickAndMorty021.PNG + │ ├── RickAndMorty022.PNG + │ ├── RickAndMorty023.PNG + │ ├── RickAndMorty024.PNG + │ └── RickAndMorty025.PNG + ├── SimpleDialogueSystem.md + ├── SimpleDialogueSystem_Data + │ ├── BeginPlay.PNG + │ ├── CloseDialogue.PNG + │ ├── CodeAsset.PNG + │ ├── ContentBrowser.PNG + │ ├── CurrentTarget.PNG + │ ├── DialogueLuaState.PNG + │ ├── DialogueQuestionDesigner.PNG + │ ├── DialogueQuestionGraph.PNG + │ ├── DialogueScreenDesigner.PNG + │ ├── DialogueScreenGraph.PNG + │ ├── EventLuaError.PNG + │ ├── FromFile.PNG + │ ├── GetPlayer.PNG + │ ├── Intro.PNG + │ ├── LevelBlueprint.PNG + │ ├── LuaStateTable1.PNG + │ ├── LuaStateTable2.PNG + │ ├── LuaStateTable3.PNG + │ ├── Modding.PNG + │ ├── MultipleCharacters.PNG + │ ├── OpenDialogue.PNG + │ ├── Overlap.PNG + │ ├── Owner.PNG + │ ├── Page2.PNG + │ ├── Page3.PNG + │ ├── PrintAndLuaError.PNG + │ ├── SetCamera.PNG + │ ├── SetFace.PNG + │ ├── SetFlash.PNG + │ ├── SetFlashExposed.PNG + │ ├── Shop.PNG + │ ├── Speak.PNG + │ ├── SpeakWithTwinblast.PNG + │ ├── TalkingCharacter.PNG + │ ├── TalkingScript.PNG + │ ├── ThreeScripts.PNG + │ └── Twinblast.PNG + └── WebSocketsWithLuaUserDataObjects.md + +``` + +`BuildingNotes.md`: + +```md +# Building Lua static library for Mac + +```sh +# edit src/Makefile and set MYCFLAGS to -mmacosx-version-min=10.12 -arch arm64 -arch x86_64 +make macosx +``` + +# Building Lua static library for Linux x86_64 + +```sh +# after having added -fPIC to MYCFLAGS in src/Makefile +make linux +``` + +# Building Lua static library for Win64 + +```sh +# open visual studio shell for x64 and move to the src/ directory (change cl.exe and lib.exe with their arm64 version for hololens) +cl /MD /O2 /c /DLUA_BUILD_AS_DLL /DLUA_COMPAT_5_2 *.c +del lua.obj +del luac.obj +lib /OUT:liblua53_win64.lib *.obj +``` + +# Building Lua static library for Android + +```sh +# assume ndk is installed in home with api 24 +~/ndk/bin/clang -O2 -Wall -Werror -Wextra -DLUA_USE_POSIX -DLUA_USEDLOPEN -DLUA_COMPAT_5_2 -std=gnu99 -c *.c +rm lua.o +rm luac.o + ~/ndk/bin/arm-linux-androideabi-ar.exe rcu liblua53_android.a *.o +``` + +# Building Lua static library for iOS + +Comment the os_execute function in loslib.c and its mapping in the syslib array + +```sh +clang -arch arm64 -mios-version-min=7.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/ -O2 -Wall -Werror -Wextra -DLUA_USE_POSIX -DLUA_USEDLOPEN -DLUA_COMPAT_5_2 -std=gnu99 -c *.c +rm lua.o luac.o + ar rcu liblua53_ios.a *.o +``` + + +# Building Lua static library for Linux AArch64 + +```sh +# assume cross compiler for aarch64 is available +aarch64-linux-gnu-gcc -O2 -Wall -Werror -Wextra -DLUA_USE_POSIX -DLUA_USEDLOPEN -DLUA_COMPAT_5_2 -std=gnu99 -c *.c +rm lua.o +rm luac.o +aarch64-linux-gnu-ar rcD liblua53_linux_aarch64.a *.o +``` + +``` + +`Config/FilterPlugin.ini`: + +```ini +[FilterPlugin] +/Docs/... +/Tutorials/... + +``` + +`Content/Scripts/submodules/test004.lua`: + +```lua +return 17.2230 +``` + +`Content/Scripts/test001.lua`: + +```lua +return 22 +``` + +`Content/Scripts/test002.lua`: + +```lua +return 30 +``` + +`Content/Scripts/test003.lua`: + +```lua +return broken() +``` + +`Content/Scripts/test011.lua`: + +```lua +return require('test005') +``` + +`Content/Scripts/trackedmodules/test005.lua`: + +```lua +return 'i am tracked' +``` + +`Docs/LuaBlueprintFunctionLibrary.md`: + +```md +# LuaBlueprintFunctionLibrary + +This is the BlueprintFunctionLibrary for accessing the LuaMachine api from both C++ and Blueprints. + +```cpp +#include "LuaMachine/Public/LuaBlueprintFunctionLibrary.h" +``` + +## FLuaValue LuaCreateString(FString String) + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +static FLuaValue LuaCreateString(FString String); +``` + +Create a new lua string from an Unreal FString + +## FLuaValue LuaCreateNumber(float Value) + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +static FLuaValue LuaCreateNumber(float Value); +``` + +Create a new Lua Number from an Unreal float + +## FLuaValue LuaCreateInteger(int32 Value) + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +static FLuaValue LuaCreateInteger(int32 Value); +``` + +Create a new Lua Integer from an Unreal int32 + +## FLuaValue LuaCreateBool(bool bInBool) + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +static FLuaValue LuaCreateBool(bool bInBool); +``` + +Create a new Lua Integer from an Unreal boolean + +## FLuaValue LuaCreateTable(UObject* WorldContextObject, TSubclassOf\ State) + +```cpp +UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject")) +static FLuaValue LuaCreateTable(UObject* WorldContextObject, TSubclassOf State); +``` + +Create a new Lua Table in the specific LuaState. The passed WorldContext will became the CurrentWorld for the LuaState (so you can call GetWorld() on the LuaState itself) + +## FLuaValue LuaCreateObject(UObject* InObject) + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +static FLuaValue LuaCreateObject(UObject* InObject); +``` + +Create a new Lua userdata pointing to an Unreal UObject + +## FLuaValue LuaGetGlobal(UObject* WorldContextObject, TSubclassOf\ State, FString Name) + +```cpp +UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject")) +static FLuaValue LuaGetGlobal(UObject* WorldContextObject, TSubclassOf State, FString Name); +``` + +Get 'Name' from the Global Table. Name can be in the dotted form to specify subpaths (example: package.path) + +## LuaSetGlobal(UObject* WorldContextObject, TSubclassOf\ State, FString Name, FLuaValue Value) + +```cpp +UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject")) +static void LuaSetGlobal(UObject* WorldContextObject, TSubclassOf State, FString Name, FLuaValue Value); +``` + +Assign 'Value' to 'Name' in the Global Table. Name can be in the dotted form to specify subpaths (example: package.path) + +## FLuaValue LuaTableGetField(FLuaValue Table, FString Key) + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +static FLuaValue LuaTableGetField(FLuaValue Table, FString Key); +``` + +Get a value from a lua table + +## FLuaValue LuaTableGetByIndex(FLuaValue Table, int32 Index) + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +static FLuaValue LuaTableGetByIndex(FLuaValue Table, int32 Index); +``` + +Get a value from a lua table/array using an index + +## TArray\ LuaTableGetKeys(FLuaValue Table) + + ```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +static TArray LuaTableGetKeys(FLuaValue Table); + ``` + + Get the list of keys in a table + +## TArray\ LuaTableGetValues(FLuaValue Table) + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +static TArray LuaTableGetValues(FLuaValue Table); +``` + +Get the list of values in a table + +## FLuaValue LuaTableSetField(FLuaValue Table, FString Key, FLuaValue Value) + +```cpp +UFUNCTION(BlueprintCallable) +static FLuaValue LuaTableSetField(FLuaValue Table, FString Key, FLuaValue Value); +``` + +Set the field of a table + +## FLuaValue LuaGlobalCall(UObject* WorldContextObject, TSubclassOf\ State, FString Name, TArray\ Args) + +```cpp +UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", AutoCreateRefTerm = "Args")) +static FLuaValue LuaGlobalCall(UObject* WorldContextObject, TSubclassOf State, FString Name, TArray Args); +``` + +Call a function by its name from the global table + +## FLuaValue LuaGlobalCallValue(UObject* WorldContextObject, TSubclassOf\ State, FLuaValue Value, TArray\ Args) + +```cpp +UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", AutoCreateRefTerm = "Args")) +static FLuaValue LuaGlobalCallValue(UObject* WorldContextObject, TSubclassOf State, FLuaValue Value, TArray Args); +``` + +Call a value in the specified State + +## FLuaValue LuaValueCall(FLuaValue Value, TArray\ Args) + +```cpp +UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "Args")) +static FLuaValue LuaValueCall(FLuaValue Value, TArray Args); +``` + +Call a value + +## TArray\ LuaGlobalCallMulti(UObject* WorldContextObject, TSubclassOf\ State, FString Name, TArray\ Args) + +```cpp +UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", AutoCreateRefTerm = "Args")) +static TArray LuaGlobalCallMulti(UObject* WorldContextObject, TSubclassOf State, FString Name, TArray Args); +``` + +Call a function by its name from the global table and returns multiple values + +## TArray\ LuaGlobalCallValueMulti(UObject* WorldContextObject, TSubclassOf\ State, FLuaValue Value, TArray\ Args) + +```cpp +UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", AutoCreateRefTerm = "Args")) +static TArray LuaGlobalCallValueMulti(UObject* WorldContextObject, TSubclassOf State, FLuaValue Value, TArray Args); +``` + +Call a value in the specified State and returns multiple values + +## TArray\ LuaValueCallMulti(FLuaValue Value, TArray\ Args) + +```cpp +UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "Args")) +static TArray LuaValueCallMulti(FLuaValue Value, TArray Args); +``` + +Call a value and returns multiple values + +## int32 LuaValueLength(FLuaValue Value) + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +static int32 LuaValueLength(FLuaValue Value); +``` + +Get the length of a value (equivalent to the lua '#') + +## int32 LuaGetTop(UObject* WorldContextObject, TSubclassOf\ State) + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure, meta = (WorldContext = "WorldContextObject")) +static int32 LuaGetTop(UObject* WorldContextObject, TSubclassOf State); +``` + +Get the stack top of the specified State. Useful for debugging/testing + +## FLuaValue LuaRunFile(UObject* WorldContextObject, TSubclassOf\ State, FString Filename, bool bIgnoreNonExistent) + +```cpp +UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject")) +static FLuaValue LuaRunFile(UObject* WorldContextObject, TSubclassOf State, FString Filename, bool bIgnoreNonExistent); +``` + +Run the specified file (relative to the Content/ directory) in the specified State. + +## FLuaValue LuaRunCodeAsset(UObject* WorldContextObject, TSubclassOf\ State, ULuaCode* CodeAsset) + +```cpp +UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject")) +static FLuaValue LuaRunCodeAsset(UObject* WorldContextObject, TSubclassOf State, ULuaCode* CodeAsset); +``` + +Run the specified Code Asset in the specified State. + +## int32 LuaGetUsedMemory(UObject* WorldContextObject, TSubclassOf\ State) + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure, meta = (WorldContext = "WorldContextObject")) +static int32 LuaGetUsedMemory(UObject* WorldContextObject, TSubclassOf State); +``` + +Get the used memory (in Kbytes) of the specified Lua State. + +## void LuaGCCollect(UObject* WorldContextObject, TSubclassOf\ State) + +```cpp +UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject")) +static void LuaGCCollect(UObject* WorldContextObject, TSubclassOf State); +``` + +Triggers Lua GC + +## void LuaGCStop(UObject* WorldContextObject, TSubclassOf\ State) + +```cpp +UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject")) +static void LuaGCStop(UObject* WorldContextObject, TSubclassOf State); +``` + +Disable automatic execution of the lua GC + +## void LuaGCRestart(UObject* WorldContextObject, TSubclassOf\ State) + +```cpp +UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject")) +static void LuaGCRestart(UObject* WorldContextObject, TSubclassOf State); +``` + +Re-enable automatic execution of the lua GC + +## bool LuaValueIsNil(FLuaValue Value) + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +static bool LuaValueIsNil(FLuaValue Value); +``` + +check if a LuaValue is nil + +## bool LuaValueIsTable(FLuaValue Value) + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +static bool LuaValueIsTable(FLuaValue Value); +``` + +check if a LuaValue is a table + +## Autocasting + +```cpp +UFUNCTION(BlueprintPure, meta=(BlueprintAutocast)) +static FString Conv_LuaValueToString(FLuaValue Value); + +UFUNCTION(BlueprintPure, meta = (BlueprintAutocast)) +static FText Conv_LuaValueToText(FLuaValue Value); + +UFUNCTION(BlueprintPure, meta = (BlueprintAutocast)) +static UObject* Conv_LuaValueToObject(FLuaValue Value); + +UFUNCTION(BlueprintPure, meta = (BlueprintAutocast)) +static UClass* Conv_LuaValueToClass(FLuaValue Value); + +UFUNCTION(BlueprintPure, meta = (BlueprintAutocast)) +static FLuaValue Conv_ObjectToLuaValue(UObject* Object); + +UFUNCTION(BlueprintPure, meta = (BlueprintAutocast)) +static FLuaValue Conv_FloatToLuaValue(float Value); + +UFUNCTION(BlueprintPure, meta = (BlueprintAutocast)) +static int32 Conv_LuaValueToInt(FLuaValue Value); + +UFUNCTION(BlueprintPure, meta = (BlueprintAutocast)) +static float Conv_LuaValueToFloat(FLuaValue Value); + +UFUNCTION(BlueprintPure, meta = (BlueprintAutocast)) +static FLuaValue Conv_IntToLuaValue(int32 Value); + +UFUNCTION(BlueprintPure, meta = (BlueprintAutocast)) +static FLuaValue Conv_StringToLuaValue(FString Value); + +UFUNCTION(BlueprintPure, meta = (BlueprintAutocast)) +static bool Conv_LuaValueToBool(FLuaValue Value); +``` + +``` + +`Docs/LuaComponent.md`: + +```md +# LuaComponent + +This is an ActorComponent allowing you to expose Actor's related data to the Lua Virtual Machine. + +The Component itself will be mapped to a lua userdata on a specific LuaState with its virtual table (and metatable) + +![LuaComponent](Screenshots/LuaComponent.PNG?raw=true "LuaComponent") + +Create a new lua script with the following content: + +```lua +actor = {} + +function actor:get_hp() + self.name = 'test' + print(self) + self.hp = self.hp + 1 + return self.hp +end +``` + +Note the colon-syntax for implementing OOP style programming. Basically the LuaComponent will automatically pass the userdata as the first argument of actor.get_hp: + +![LuaComponent2](Screenshots/LuaComponent2.PNG?raw=true "LuaComponent2") + +While the game runs, you can see the "Table" property being updated from lua: + +![LuaComponent3](Screenshots/LuaComponent3.PNG?raw=true "LuaComponent3") + +## Metatable + +You can specify a metatable for the LuaComponent. As an example you may want to implement a "__tostring" function so when doing + +```lua +print(self) +``` + +you can get a meaningful value: + +![LuaComponent4](Screenshots/LuaComponent4.PNG?raw=true "LuaComponent4") + +![LuaComponent5](Screenshots/LuaComponent5.PNG?raw=true "LuaComponent5") + +## LuaComponent Interactions + +When passing UObject's as LuaValue, they are incapsulated as userdata, so technically (by default) you cannot do anything relevant via Lua (except for passing them as arguments to functions). LuaComponents instead, get a proper metatable exposing the values set in the Table property. A Common pattern is to return the LuaComponent of an Actor to Lua, instead of the actor itself: + +![PlayerToLuaComponent](Screenshots/PlayerToLuaComponent.PNG?raw=true "PlayerToLuaComponent") + +Note that you can eventually assign an automatic metatable to UObjects too, using the LuaState UserData MetaTable features: +https://github.com/rdeioris/LuaMachine/blob/master/Tutorials/ReflectionShell.md + +## Exposed Functions + +### LuaCallFunction + +```cpp +UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "Args")) +FLuaValue LuaCallFunction(FString FunctionName, TArray Args, bool bGlobal); +``` + +Call a Lua function by its name. If bGlobal is false, the name will be searched in the component-associated virtual table. +The userdata is passed as the first argument. + +### LuaCallValue + +```cpp +UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "Args")) +FLuaValue LuaCallValue(FLuaValue Value, TArray Args); +``` + +Takes a callable LuaValue and execute it passing userdata as the first argument. + +### LuaGetField + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +FLuaValue LuaGetField(FString Name); +``` + +Get a field value from the userdata virtual table + +### LuaSetField + +```cpp +UFUNCTION(BlueprintCallable) +void LuaSetField(FString Name, FLuaValue Value); +``` + +Set a field in the userdata virtual table + +## Exposed Events + +### OnLuaError + +Triggered when the lua virtual machine generates an error when triggered from the LuaComponent. The error string is passed as the event argument + +``` + +`Docs/LuaCoroutines.md`: + +```md +# Using Lua Coroutines + +Coroutines in lua are "interruptable" functions: they can 'suspend' themselves and can be 'resumed' by another coroutine or from the main code: + +```lua +coro = coroutine.create(function() + -- suspend function (without returning values) + coroutine.yield() + -- suspend returning 5 values + coroutine.yield('one', 'two', 'three', 'four', 'five') + -- suspend returning 1 value + coroutine.yield(17) + -- suspend returning 1 value + coroutine.yield('ended') +end) + +-- run the coroutine until the first yield +alive = coroutine.resume(coro) + +-- ... until second yield (it returns five values) +alive, one, two, three, four, five = coroutine.resume(coro) + +-- ... until third yield +alive, seventeen = coroutine.resume(coro) + +-- ... the last yield +alive, ended = coroutine.resume(coro) + +-- complete the coroutine +alive = coroutine.resume(coro) + +-- this time 'alive' will be 'false' as the coroutine is 'dead' (it has finished its work) +alive = coroutine.resume(coro) +``` + +Note that albeit coroutine.yield() can be called without arguments, the coroutine.resume() function will always get the first return value as a boolean specifying if the coroutine is still alive or not. + +More infos can be found here: + +https://www.lua.org/manual/5.3/manual.html#2.6 + +Coroutines maps to native lua objects named 'threads'. Lua Threads are fully supported by the FLuaValue USTRUCT, so you can create a coroutine from lua and pass it to Unreal Engine functions: + + +![Coroutine](Screenshots/Coroutine.PNG?raw=true "Coroutine") + +The BeginPlay event will create a new coroutine (a thread LuaValue) and stores it in a blueprint variable. + +At each tick the coroutine.resume() lua function is called passing it the coroutine as the first argument. Note the usage of the LuaGlobalCallMulti node as it allows to receive multiple return values. Remember that the first return value of coroutine.resume() is always the 'status' of the coroutine (true is alive, false is dead) + +``` + +`Docs/ReflectionAPI.md`: + +```md +# The LuaMachine Reflection API + +While 'safety' of scripting api exposure is one of the main objective of LuaMachine, sometimes accessing the reflection api of Unreal Engine is pretty useful (and often a quick way for exposing scripting when you are not worried about giving too much power to users) + +Albeit the api is usable from blueprints, it is strongly suggested to go to C++ when defining more advanced states. + +## Converting between structs and Lua tables + +The reflection API is pretty big, so before going into a more-or-less-useful implementation of Lua Reflection, we will cover some low-level details. + +The first one is the automatic conversion between USTRUCTs and lua tables. The following 3 template functions allows easy conversion from any USTRUCT to an FLuaValue and the opposite. + +```cpp +template +FLuaValue StructToLuaValue(T& InStruct); + +template +T LuaValueToStruct(FLuaValue& LuaValue); + +template +T LuaValueToBaseStruct(FLuaValue& LuaValue) +``` + +Let's implement a SetActorLocation() wrapper for a LuaState: + +```cpp +#include "StructManagerLuaState.h" +#include "GameFramework/Actor.h" + +UStructManagerLuaState::UStructManagerLuaState() +{ + Table.Add("set_actor_location", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(UStructManagerLuaState, SetActorLocation))); +} + +FLuaValue UStructManagerLuaState::SetActorLocation(FLuaValue Actor, FLuaValue Location) +{ + AActor* ActorObject = Cast(Actor.Object); + if (ActorObject) + { + FHitResult Hit; + // FVector is a base type (like FRotator, FQuat, ...) , so we need to use LuaValueToBaseStruct instead of LuaValueToStruct + ActorObject->SetActorLocation(LuaValueToBaseStruct(Location), true, &Hit); + // convert the FHitResult struct to a lua table + return StructToLuaValue(Hit); + } + // nil + return FLuaValue(); +} +``` + +## Getting/Setting properties by name + +The following c++/blueprint functions allow to access the Unreal properties using the reflection system: + +```cpp +UFUNCTION(BlueprintCallable, Category = "Lua") +FLuaValue GetLuaValueFromProperty(UObject* InObject, const FString& PropertyName); + +UFUNCTION(BlueprintCallable, Category = "Lua") +bool SetPropertyFromLuaValue(UObject* InObject, const FString& PropertyName, FLuaValue Value); +``` + +Properties Values are always converted to FLuaValue. + +This is a simple (and pretty useless) example: + +```cpp +void UStructManagerLuaState::SetActorProperty(FLuaValue Actor, FLuaValue Property, FLuaValue Value) +{ + AActor* ActorObject = Cast(Actor.Object); + if (ActorObject) + { + SetPropertyFromLuaValue(ActorObject, Property.ToString(), Value); + } +} +``` + +Delegates are automatically managed as lua functions: + +lua example for setting a delegate (assuming the previous defined UStructManagerLuaState::SetActorProperty has been exposed as 'set_actor_property') : + +```lua +function setup(actor) + set_actor_property(actor, 'OnActorHit', function(me, other, normal, hitresult) + print('hit with', other) + end + ) +end +``` + +## Implementing a LuaState that automatically exposes everything to the Lua VM + +This is probably the reason you are reading this page ;) + +In the following lines i will show you a 'simple' LuaState that automatically uses the Unreal reflection to expose UObject, UFunction, Properties, Structs, Delegates directly to lua without additional steps (in addition to assigning a lua name/variable to them when required). + +Let's start by defining the header (LuaReflectionState.h): + +```cpp +#pragma once + +#include "CoreMinimal.h" +#include "LuaState.h" +#include "LuaReflectionState.generated.h" + +UCLASS() +class ULuaReflectionState : public ULuaState +{ + GENERATED_BODY() + +public: + ULuaReflectionState(); + +protected: + void LuaStateInit() override; + + // __index(object, key) -> returning 1 value + LUACFUNCTION(ULuaReflectionState, MetaMethodIndex, 1, 2); + + // __newindex(object, key, value) + LUACFUNCTION(ULuaReflectionState, MetaMethodNewIndex, 0, 3); + + // __eq(object1, object2) -> returning bool + LUACFUNCTION(ULuaReflectionState, MetaMethodEq, 1, 2); + + // __string(object) -> returning string + LUACFUNCTION(ULuaReflectionState, MetaMethodToString, 1, 1); +}; +``` + +The new (and most important) stuff here is the usage of the LUACFUNCTION definition. + +Here we are defining 4 metamethods to be called whenever an Unreal object is passed around in the lua state. UObjects are mapped to userdata, so this method defines what to do when using them. As an example, if the 'mannequin' lua variable is mapped to a Character, we want to make it jump by calling mannequin.Jump(). So our system needs to resolve Jump as a function and call it. + +We cannot use plain UFunctions for those metamethods (as they are userdata under the hood), so we need to code them using lower level LUA c api. The LUACFUNCTION wraps those low-level functions in Unreal C++ api-friendly ones. + +Now let's define the cpp part (LuaReflectionState.cpp): + +```cpp +#include "LuaReflectionState.h" +#include "LuaBlueprintFunctionLibrary.h" + +ULuaReflectionState::ULuaReflectionState() +{ + // allow to call native UFunctions with implicit FLuaValue conversions + bRawLuaFunctionCall = true; +} + +void ULuaReflectionState::LuaStateInit() +{ + UserDataMetaTable = CreateLuaTable(); + UserDataMetaTable.SetField("__index", ULuaReflectionState::MetaMethodIndex_C); + UserDataMetaTable.SetField("__newindex", ULuaReflectionState::MetaMethodNewIndex_C); + UserDataMetaTable.SetField("__eq", ULuaReflectionState::MetaMethodEq_C); + UserDataMetaTable.SetField("__tostring", ULuaReflectionState::MetaMethodToString_C); +} + +TArray ULuaReflectionState::MetaMethodIndex(TArray LuaArgs) +{ + TArray ReturnValues; + + UObject* Object = LuaArgs[0].Object; + FString Key = LuaArgs[1].ToString(); + + // skip nullptr and classes + if (!Object || Object->IsA()) + { + return ReturnValues; + } + + ELuaReflectionType ReflectionType = ELuaReflectionType::Unknown; + ULuaBlueprintFunctionLibrary::GetLuaReflectionType(Object, Key, ReflectionType); + + if (ReflectionType == ELuaReflectionType::Property) + { + ReturnValues.Add(GetLuaValueFromProperty(Object, Key)); + } + else if (ReflectionType == ELuaReflectionType::Function) + { + ReturnValues.Add(FLuaValue::FunctionOfObject(Object, FName(Key))); + } + + return ReturnValues; +} + +TArray ULuaReflectionState::MetaMethodNewIndex(TArray LuaArgs) +{ + TArray ReturnValues; + + UObject* Object = LuaArgs[0].Object; + FString Key = LuaArgs[1].ToString(); + FLuaValue Value = LuaArgs[2]; + + // skip nullptr and classes + if (!Object || Object->IsA()) + { + return ReturnValues; + } + + ELuaReflectionType ReflectionType = ELuaReflectionType::Unknown; + ULuaBlueprintFunctionLibrary::GetLuaReflectionType(Object, Key, ReflectionType); + + if (ReflectionType == ELuaReflectionType::Property) + { + SetPropertyFromLuaValue(Object, Key, Value); + } + + return ReturnValues; +} + +TArray ULuaReflectionState::MetaMethodEq(TArray LuaArgs) +{ + TArray ReturnValues; + + UObject* Object = LuaArgs[0].Object; + UObject* OtherObject = LuaArgs[1].Object; + + if (!Object || !OtherObject) + { + ReturnValues.Add(FLuaValue(false)); + } + else + { + ReturnValues.Add(FLuaValue(Object == OtherObject)); + } + + return ReturnValues; +} + +TArray ULuaReflectionState::MetaMethodToString(TArray LuaArgs) +{ + TArray ReturnValues; + + UObject* Object = LuaArgs[0].Object; + + // skip nullptr and classes + if (!Object || Object->IsA()) + { + return ReturnValues; + } + + ReturnValues.Add(Object->GetFullName()); + + return ReturnValues; +} +``` + +We are now ready for building a simple logic in lua (put this in a script or a LuaCodeAsset): + +```lua +mannequin.OnActorBeginOverlap = function(me, other) + print(tostring(me) .. ' colliding with ' .. tostring(other)) + other.K2_AddActorLocalRotation({Yaw=10, Pitch=0, Roll=0}) +end + +mannequin.Jump() +``` + +Here we have a script that assumes a character can overlap with a (movable) object that will be rotated of 10 degrees on overlapping. + +You can run this script in the LuaReflectionState using a blueprint. The only missing part is assigning your character to the lua 'mannequin' variable. For doing this automatically just add to the character a LuaGlobalNameComponent on which you can specify the state and the name. + +Notes: + +* You do not need to attach LuaComponents to actors (everything is automatic) +* You can attach multiple LuaGlobalNameComponents on the same actor (allowing it to be available on multiple states or with multiple names) +* The LuaGlobalNameComponent is super easy, just give it a look to adapt it to more complex scenario +* The LuaReflectionState class is not part of the official sources to encourage users/developers to implement their own solutions (like hiding dangerous methods or exposing subsets of them) + +``` + +`Docs/TipsAndTricks.md`: + +```md +# Tips & Tricks + +In this page users can publish their c++ snippets (please make pull requests with your credits) + +## Running lua files recursively from a directory and get their return value + +Author: Roberto De Ioris + +C++/UFUNCTION code: + +```cpp +#include "HAL/FileManager.h" + +ULuaStateOrLuaPackage::ULuaStateOrLuaPackage() +{ + Table.Add("scan_and_run", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(ULuaStateOrLuaPackage, ScanAndRunLuaScripts))); +} + +TArray ULuaStateOrLuaPackage::ScanAndRunLuaScripts(FLuaValue Directory) +{ + TArray Results; + TArray Filenames; + IFileManager::Get().FindFilesRecursive(Filenames, *Directory.ToString(), TEXT("*.lua"), true, false); + + for (FString Filename : Filenames) + { + Results.Add(ULuaBlueprintFunctionLibrary::LuaRunNonContentFile(GetWorld(), GetLuaState(), Filename, false)); + } + + return Results; +} +``` + +Lua script: + +```lua +items = {foobar.scan_and_run('D:/luascripts')} + +for _, ret in ipairs(items) do + print(ret) +end +``` + +## Wrapping Vectors in Lua tables with math support via metatables + +The Goal: + +```lua +a = vector:new(1,2,3) +b = vector:new(4,5,6) + +print(a) +print(b) + +print(a + b) +``` + +The Implementation: + +(ULuaVector is a subclass of ULuaBlueprintPackage and must be added to the LuaState Blueprint Package Table as 'vector') + +```cpp +#include "LuaVector.h" +#include "LuaBlueprintFunctionLibrary.h" + +ULuaVector::ULuaVector() +{ + Table.Add("new", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(ULuaVector, VectorNew))); + Table.Add("__add", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(ULuaVector, VectorAdd))); + Table.Add("__tostring", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(ULuaVector, VectorStr))); +} + +FLuaValue ULuaVector::VectorNew(FLuaValue SelfPackage, FLuaValue X, FLuaValue Y, FLuaValue Z) +{ + return BuildVector(FVector(X.ToFloat(), Y.ToFloat(), Z.ToFloat())); +} + +FLuaValue ULuaVector::VectorAdd(FLuaValue A, FLuaValue B) +{ + return BuildVector(ULuaBlueprintFunctionLibrary::LuaTableToVector(A) + ULuaBlueprintFunctionLibrary::LuaTableToVector(B)); +} + +FLuaValue ULuaVector::VectorStr(FLuaValue LuaVector) +{ + return ULuaBlueprintFunctionLibrary::LuaTableToVector(LuaVector).ToString(); +} + +FLuaValue ULuaVector::BuildVector(FVector Vector) +{ + FLuaValue NewTable = ULuaBlueprintFunctionLibrary::LuaCreateTable(GetWorld(), GetLuaState()); + NewTable.SetField("x", Vector.X); + NewTable.SetField("y", Vector.Y); + NewTable.SetField("z", Vector.Z); + + NewTable.SetMetaTable(GetSelfLuaTable()); + + return NewTable; +} +``` + +## Mapping BlueprintFunctionLibrary to UserData + +The Goal: + +```lua +kismet_math = bpfl.require('KismetMathLibrary') + +print(kismet_math.Abs(-17.3)) + +print(kismet_math.Add_DoubleDouble(17, 0.3)) + +print(kismet_math.Add_VectorFloat({1,2,3}, 0.3).X) +``` + +'bpfl' (shortcut for 'Blueprint Function Library') will be the package to get a userdata from a BlueprintFunctionLibrary ('KismetMathLibrary' on the example, but can be anything) + +```cpp +UCLASS() +class UBPFLLuaBlueprintPackage : public ULuaBlueprintPackage +{ + GENERATED_BODY() +public: + UBPFLLuaBlueprintPackage(); + + UFUNCTION() + FLuaValue RequireBlueprintFunctionLibrary(FLuaValue LibraryName); +}; +``` + +```cpp +UBPFLLuaBlueprintPackage::UBPFLLuaBlueprintPackage() +{ + Table.Add("require", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(UBPFLLuaBlueprintPackage, RequireBlueprintFunctionLibrary))); +} + +FLuaValue UBPFLLuaBlueprintPackage::RequireBlueprintFunctionLibrary(FLuaValue LibraryName) +{ + UClass* FoundClass = Cast(StaticFindObject(UClass::StaticClass(), ANY_PACKAGE, *LibraryName.ToString())); + if (FoundClass) + { + ULuaUserDataBPFL* UserDataBPFL = NewObject(GetLuaStateInstance()); + UserDataBPFL->InitializeWithClass(FoundClass); + return FLuaValue(UserDataBPFL); + } + return FLuaValue(); +} +``` + +The RequireBlueprintFunctionLibrary lua exposed UFunction will search for the specified library name using the reflection system, and will create a ULuaUserDataBPFL (check below) that will keep a reference to the blueprint library + +```cpp +UCLASS() +class ULuaUserDataBPFL : public ULuaUserDataObject +{ + GENERATED_BODY() + +public: + void InitializeWithClass(UClass* InClass); + + virtual FLuaValue ReceiveLuaMetaIndex_Implementation(FLuaValue Key) override; + +protected: + UPROPERTY() + UClass* BPFLClass; +}; +``` + +```cpp +void ULuaUserDataBPFL::InitializeWithClass(UClass* InClass) +{ + BPFLClass = InClass; +} + +FLuaValue ULuaUserDataBPFL::ReceiveLuaMetaIndex_Implementation(FLuaValue Key) +{ + UFunction* Function = BPFLClass->FindFunctionByName(*Key.ToString()); + if (Function) + { + return FLuaValue::FunctionOfObject(BPFLClass->GetDefaultObject(), Function->GetFName()); + } + + return FLuaValue(); +} +``` + +The key here is the ReceiveLuaMetaIndex_Implementation override, that will return the UFunction ptr of the supplied function name (if it exists). + +You can now map the UBPFLLuaBlueprintPackage to the bpfl package in your LuaState configuration and (this is required) you need to enable the bRawLuaFunctionCall too: this will allow the state to automatically convert UFunction arguments to lua values. + +``` + +`LICENSE`: + +``` +MIT License + +Copyright (c) 2018 Roberto De Ioris + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +`LuaMachine.uplugin`: + +```uplugin +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "20250604", + "FriendlyName": "LuaMachine", + "Description": "Expose a Lua api for your project", + "Category": "Scripting", + "CreatedBy": "Roberto De Ioris", + "CreatedByURL": "https://github.com/rdeioris", + "DocsURL": "https://github.com/rdeioris/LuaMachine", + "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/48a58a495c3f4d788fc9195bceff2ec8", + "SupportURL": "https://github.com/rdeioris/LuaMachine", + "CanContainContent": true, + "IsBetaVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "LuaMachine", + "Type": "Runtime", + "LoadingPhase": "PreDefault", + "WhitelistPlatforms": [ + "Mac", + "Win64", + "Linux", + "LinuxArm64", + "Android", + "IOS", + ] + }, + { + "Name": "LuaMachineEditor", + "Type": "Editor", + "LoadingPhase": "Default", + "WhitelistPlatforms": [ + "Linux", + "Mac", + "Win64" + ] + } + ] +} + +``` + +`README.md`: + +```md +# LuaMachine +![LuaMachine Logo](Docs/LuaMachine_Logo.png?raw=true "LuaMachine Logo") + +Unreal Engine Plugin for adding Lua scripting to your projects. + +If you want modders to customize your game/project, or you need to allow game designers to script parts of the logic, or more generally you want a framework for adding lua scripting capabilities to your projects, this plugin is for you. + +![LuaMachine](Docs/Screenshots/SyntaxColor.PNG?raw=true "LuaMachine") + +Note: The master branch is focused on Unreal Engine 5, if you need Unreal Engine 4 support use the ue4 branch. + +Experimental LuaU (https://luau.org/) support is available (for Windows) in the "luau" branch. + +## How it works + +Contrary to the other Unreal Engine Lua plugins, this one does not try to automatically expose the Unreal Engine api, but completely hides it giving to the user/scripter only the features the developer decided to include (via Blueprints or C++). +An advanced api for using/abusing Unreal Engine Reflection is included too. + +Currently Windows 64bit, Mac, Linux x86_64 (both Runtime and Editor), Linux AArch64, Android (32 and 64 bit) and iOS are supported. Minimal supported Engine version is 4.20. + +## Discord Channel + +Join the LuaMachine Discord channel: https://discord.gg/eTXDfEU + +## Marketplace + +Albeit fully open source and free, you can find the plugin in the Unreal Engine Marketplace for about 30$. If you want to sponsor the project consider buying it: + +https://www.unrealengine.com/marketplace/en-US/product/luamachine + +## Becoming a patron + +Consider sponsoring the project using patreon: https://www.patreon.com/rdeioris + +## Quickstart + +In this fast (and useless) example we will print a string generated by a Lua script. + +Install the LuaMachine plugin by downloading it from the official github releases page (free, but you need to compile it) or from the Marketplace (ready to use, about 30$): + +https://github.com/rdeioris/LuaMachine/releases/ + +https://www.unrealengine.com/marketplace/luamachine + +To compile from sources, decompress the zip file in the Plugins/ directory of your project (create it if it does not exist) and restart the Unreal Editor. (if you want to manually compile it, ensure C++ is enabled in your project and regenerate the solution/xcode file) + +Open your favourite editor for lua scripting and create a file into the project Content/ directory (call it hello.lua) with the following code: + +```lua +current_time = os.date() +return "Hello, i am a lua script, current time is: " .. current_time +``` + +Now you need to create a new LuaState, it is a special Blueprint representing a Lua Virtual Machine (you can have multiple Lua virtual machines in the same project). Just add a new blueprint inheriting from LuaState and name as you want (QuickLuaState is a good candidate). + +Finally open your Level Blueprint and add the following nodes: + +![Quickstart](Docs/Screenshots/Quickstart.PNG?raw=true "Quickstart") + +If you play the game, you will see the Lua string printed on top. + +The LuaRunFile node will execute a lua file (relative to the Content/ directory) in the specified LuaState (QuickLuaState in our case) and will return the value returned by the script itself as a LuaValue UStruct. The plugin offers a bunch of operations you can apply to LuaValue's, in this example we simply converted the value to an Unreal String. + +Time to something more complex, edit your hello.lua script: + +```lua + +function call_me_from_unreal(number) + return number * 2 +end + +current_time = os.date() +return "Hello, i am a lua script, current time is: " .. current_time +``` + +Now the script will add the call_me_from_unreal function to the global table: + +![Quickstart2](Docs/Screenshots/Quickstart2.PNG?raw=true "Quickstart2") + +The LuaGlobalCall node calls a function from the global table passing arguments as an array of LuaValue's. + +So, now we know how to call lua functions from Unreal, time to do the opposite. + +Open the QuickLuaState Blueprint you created before. This Blueprint exposes lot of different fields to configure your Lua Virtual Machine: + +![Quickstart3](Docs/Screenshots/Quickstart3.PNG?raw=true "Quickstart3") + +The Table 'TMap' allows you to add items to the Lua global table. We will first try by adding a string and a number (float): + +![Quickstart4](Docs/Screenshots/Quickstart4.PNG?raw=true "Quickstart4") + +Note that we set the "Lua Filename" field to hello.lua, in this way we can avoid to call the LuaRunFile node as hello.lua will be executed during the spawn of the QuickLuaState. + +Let's modify our Level Blueprint: + +![Quickstart5](Docs/Screenshots/Quickstart5.PNG?raw=true "Quickstart5") + +The hello.lua script is loaded automatically, while the LuaGetGlobal nodes, retrieve the specific fields from the Lua global table, and return them as LuaValue's + +Time to expose an Unreal UFunction to Lua: just add a Function to QuickLuaState: + +![Quickstart6](Docs/Screenshots/Quickstart6.PNG?raw=true "Quickstart6") + +and expose it in the Table TMap: + +![Quickstart7](Docs/Screenshots/Quickstart7.PNG?raw=true "Quickstart7") + +calling get_player_location() from lua, will trigger the QuickLuaState's GetPlayerPawnLocation(). Note that not all of the UFunction's can be exposed: if they take or return values they must be LuaValue's + +Let's update hello.lua + +```lua +function call_me_from_unreal(number) + location = get_player_location() + print(location.x, location.y, location.z) + return number * 2 + location.x + location.y + location.z +end + +current_time = os.date() +return "Hello, i am a lua script, current time is: " .. current_time +``` + +and the Level Blueprint: + +![Quickstart8](Docs/Screenshots/Quickstart8.PNG?raw=true "Quickstart8") + +Please check the print() function: it is automatically mapped to the Unreal Output Log by the plugin. + +Now you should have an idea of the plugin potential, continue reading for more infos. + +## LuaState + +A LuaState (ULuaState C++ class) represents a single Lua virtual machine (there can be multiple, each one isolated from the others). This is a "singleton" as each LuaState class can have a single instance for the whole process life. (Note that while in the Editor, LuaState's are constantly recreated whenever you enter PIE mode to simplify development) + +Having multiple LuaState's allows clean separation of domains: as an example you may want a state for game configuration, another one for game logic and one for the modders. More complex examples include having states dedicated to specific Actors, GameModes or Levels. + +LuaState's are loaded on-demand, so you can create dozens of them but they will not start Lua VMs until you need them. + +LuaState's can load and execute scripts automatically if you specify the fields "LuaCodeAsset" and/or "LuaFilename". You can load scripts (both in text and bytecode format) from your filesystem (via "LuaFilename"), or from a specific asset exposed by the plugin, named LuaCode (you can specify it with the "LuaCodeAsset" field). The advantage of using filesystem scripts is that you can change them even after the project has been packaged (unless you package them too), while LuaCode assets are directly built in your pak files (as bytecode by default). + +### LuaState Properties + +* LuaCodeAsset: specify a LuaCode asset to automatically load an execute on spawn +* LuaFileName: specify a script path (relative to Content/) to load and execute on spawn +* Table: TMap allows adding FLuaValue's to the Lua VM global table +* RequireTable: TMap allows to map LuaCode assets to specific name, so you can call require('name') from your code +* LuaOpenLibs: if true, automatically load the lua standard library on spawn +* AddProjectContentDirToPackage: if true, when doing require('name') will search for 'name.lua' in the Content/ directory +* AppendProjectContentDirToPackage: TArray allows specifying a list of Content/ subdirectories to search for packages (while doing require('name')) +* OverridePackagePath: (advanced users) allows to modify package.path +* OverridePackageCPath: (advanced users) allows to modify package.cpath +* LogError: enable/disable logging of Lua errors + +### LuaState Events + +The "LuaError" event is exposed by ULuaState. + +If defined, it will be triggered whenever the Lua VM generates an error. The Error message is passed as an argument. This is really useful for adding in-game consoles, or to catch specific errors. + +### LuaMachine Debugger + +A Simple Lua Debugger is included in the plugin (you can find it under the Window/Developer Tools menu) + +![Debugger](Docs/Screenshots/Debugger.PNG?raw=true "Quickstart8") + +You can control/monitor active LuaStates as well as triggering both Unreal and Lua GC's + +### LuaMachine Console + +As a great companion for the debugger, each LuaState automatically activates a lua console in your output log window: + +![LuaConsole](Docs/Screenshots/LuaConsole.PNG?raw=true "LuaConsole") + +LuaConsoles allow multiline input (shift-return for newlines, return for submitting the commands) and are automatically destroyed when the state is destroyed. + +Obviously they are only available in the editor. + +### LuaState in C++ + +You can define your LuaState's as C++ classes, this is handy for exposing functions that would be hard to define with blueprints: + +```cpp +#pragma once + +#include "CoreMinimal.h" +#include "LuaState.h" +#include "AdvancedLuaState.generated.h" + +/** + * + */ +UCLASS() +class LUATEST420_API UAdvancedLuaState : public ULuaState +{ + GENERATED_BODY() +public: + UAdvancedLuaState(); + + UFUNCTION() + FLuaValue GetPlayerPawnLocation(); + + UFUNCTION() + FLuaValue GetSimpleString(); + +}; +``` + +```cpp +#include "AdvancedLuaState.h" +#include "Kismet/GameplayStatics.h" +#include "Engine/World.h" +#include "GameFramework/Pawn.h" + +UAdvancedLuaState::UAdvancedLuaState() +{ + Table.Add("simple_string", FLuaValue("Test string")); + Table.Add("get_player_location", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(UAdvancedLuaState, GetPlayerPawnLocation))); + Table.Add("get_simple_string", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(UAdvancedLuaState, GetSimpleString))); +} + +FLuaValue UAdvancedLuaState::GetPlayerPawnLocation() +{ + FVector Location = UGameplayStatics::GetPlayerPawn(GetWorld(), 0)->GetActorLocation(); + FLuaValue Table = CreateLuaTable(); + Table.SetField("x", FLuaValue(Location.X)); + Table.SetField("y", FLuaValue(Location.Y)); + Table.SetField("z", FLuaValue(Location.Z)); + + return Table; +} + +FLuaValue UAdvancedLuaState::GetSimpleString() +{ + return FLuaValue("Hello World"); +} + +``` + +GET_FUNCTION_NAME_CHECKED is an Unreal Macro for sanitizing UFunction names. Albeit not necessary (the argument is the FName of the function) it is highly suggested to use it to avoid silent errors. + +You can call LuaState's offered api using the ULuaBlueprintFunctionLibrary class. + +Check its docs here: [LuaBlueprintFunctionLibrary](Docs/LuaBlueprintFunctionLibrary.md) + +## LuaValue + +LuaValue's are the way Unreal communicates with a specific Lua virtual machine. They contains values that both Lua and your project can use. + +This a list of the currently supported values: + +* Nil (nil/NULL value) +* Bool (boolean value) +* Integer (integer/int32 value) +* Number (float value) +* String (string/FString value) +* Function (reference to a lua function) +* Table (reference to a lua table) +* UObject (pointer to an Unreal UObject) +* UFunction (pointer to an Unreal UFunction) +* Thread (reference to a lua thread/coroutines) + +Note that tables are passed by reference, so technically you can update the same table from both lua and Unreal. + +Check [LuaBlueprintFunctionLibrary](Docs/LuaBlueprintFunctionLibrary.md) for infos on how to use the FLuaValue api. + +## Shortcut for specifying field names + +As a shortcut for reaching long paths in tables, you can use the dot as the separator for field names. + +As an example the LuaGetGlobal node, can access the package.path item by simply passing it "package.path" as the field name. + +## LuaComponent + +ULuaComponent allows to map a table to an ActorComponent + +Check its docs here: [LuaComponent](Docs/LuaComponent.md) + +## LuaBlueprintPackage + +If you have C++/Blueprint functions that you want to expose to multiple LuaStates, consider grouping them in a LuaBlueprintPackage. + +Check: https://github.com/rdeioris/LuaMachine/blob/master/Tutorials/JsonLuaBlueprintPackage.md + +## LuaUserDataObject + +You can create new Lua object types by subclassing ULuaUserDataObject. + +check this funny tutorial about exposing the Unreal WebSockets api to Lua: https://github.com/rdeioris/LuaMachine/blob/master/Tutorials/WebSocketsWithLuaUserDataObjects.md + +## LuaCodeBox/ULuaMultiLineEditableTextBox + +This is a UMG widget for showing/editing lua code with syntax colouring: + +![LuaCodeBox](Docs/Screenshots/LuaCodeBox.PNG?raw=true "LuaCodeBox") + +The syntax highlighter is available in the FLuaMachineSyntaxHighlighterTextLayoutMarshaller class (so you can reuse it +for building more advanced Slate/UMG widgets) + +## Lua Threads/Coroutines + +Lua Threads/Coroutines are fully supported + +Check dedicated docs here: [LuaCoroutines](Docs/LuaCoroutines.md) + +## Packaging + +You have various way to use Lua scripting facilities in your packaged project: + +### LuaCode for everything + +Transform your scripts (just drag them in the content browser) to LuaCode assets and eventually fix the RequireTable. By default they will be byte-compiled. It is highly suggested to "sign" the pak file to avoid pak file modifications. This method does not permit customization of scripts after the build. + +### Scripts file packaging + +You can include your scripts in the pak file automatically by specifying the directory containing them in the package settings: + +![Packaging](Docs/Screenshots/Packaging.PNG?raw=true "Packaging") + +Signing the pak file could be a good thing again, in addition to this byte-compiling the scripts could be accomplished with the 'luac' command (included in lua distributions) + +### Scripts file inclusion + +This is for allowing easy customization of your scripts after the packaging. Basically you include the scripts directories in your Content directory: + +![Packaging2](Docs/Screenshots/Packaging2.PNG?raw=true "Packaging2") + +## Android deployment + +The plugin supports Android systems starting from api 24. Before deploying ensure you have force the right minimal api version: + +![Android24](Docs/Screenshots/Android24.PNG?raw=true "Android24") + +Cooked LuaCode assets are converted to 32bit size_t at runtime. + +## iOS deployment + +Nothing special, just ensure to package script files (instead of simply including them) + +## Functional Tests + +The projects includes various functional tests (all written as blueprints, so they could be useful as examples too). + +Just enable the plugin content visualization from the content browser and from the Automator tool execute the Project related tests. + +## Tutorials + +https://github.com/rdeioris/LuaMachine/blob/master/Tutorials/SimpleDialogueSystem.md + +https://github.com/rdeioris/LuaMachine/blob/master/Tutorials/RickAndMortyAPI.md + +## C++ Tips & Tricks + +https://github.com/rdeioris/LuaMachine/blob/master/Docs/TipsAndTricks.md + +## Reflection API + +https://github.com/rdeioris/LuaMachine/blob/master/Docs/ReflectionAPI.md + +## Commercial Support + +Commercial support is offered by Blitter (based in Rome, Italy), just contact info at blitter.studio or send a private message to rdeioris on the LuaMachine Discord server. + +## Sponsors + +The LuaMachine logo has been designed by Nicolas Martel (aka. Nogitsu) + +``` + +`Source/LuaMachine/LuaMachine.Build.cs`: + +```cs +// Copyright 2018-2023 - Roberto De Ioris + +using UnrealBuildTool; + +public class LuaMachine : ModuleRules +{ + public LuaMachine(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + bUseUnity = false; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "HTTP", + "Json", + "PakFile" + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "UMG", + "InputCore", + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + + if (Target.bBuildEditor) + { + PrivateDependencyModuleNames.AddRange(new string[]{ + "UnrealEd", + "Projects" + }); + } + + string ThirdPartyDirectory = System.IO.Path.Combine(ModuleDirectory, "..", "ThirdParty"); + + if (Target.Platform == UnrealTargetPlatform.Win64) + { + PublicAdditionalLibraries.Add(System.IO.Path.Combine(ThirdPartyDirectory, "x64", "liblua53_win64.lib")); + } + + else if (Target.Platform == UnrealTargetPlatform.Mac) + { + PublicAdditionalLibraries.Add(System.IO.Path.Combine(ThirdPartyDirectory, "x64", "liblua53_mac.a")); + } + + else if (Target.Platform == UnrealTargetPlatform.Linux) + { + PublicAdditionalLibraries.Add(System.IO.Path.Combine(ThirdPartyDirectory, "x64", "liblua53_linux64.a")); + } + + else if (Target.Platform == UnrealTargetPlatform.LinuxArm64) + { + PublicAdditionalLibraries.Add(System.IO.Path.Combine(ThirdPartyDirectory, "ARM64", "liblua53_linux_aarch64.a")); + } + + else if (Target.Platform == UnrealTargetPlatform.Android) + { + PublicAdditionalLibraries.Add(System.IO.Path.Combine(ThirdPartyDirectory, "ARMv7", "liblua53_android.a")); + PublicAdditionalLibraries.Add(System.IO.Path.Combine(ThirdPartyDirectory, "ARM64", "liblua53_android64.a")); + } + + else if (Target.Platform == UnrealTargetPlatform.IOS) + { + PublicAdditionalLibraries.Add(System.IO.Path.Combine(ThirdPartyDirectory, "ARM64", "liblua53_ios.a")); + } + + } +} + +``` + +`Source/LuaMachine/Private/LuaBlueprintFunctionLibrary.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + +#include "LuaBlueprintFunctionLibrary.h" +#include "LuaComponent.h" +#include "LuaMachine.h" +#include "Runtime/Online/HTTP/Public/Interfaces/IHttpResponse.h" +#include "Runtime/Core/Public/Math/BigInt.h" +#include "Runtime/Core/Public/Misc/Base64.h" +#include "Runtime/Core/Public/Misc/SecureHash.h" +#include "Serialization/JsonSerializer.h" +#include "Engine/Texture2D.h" +#include "IImageWrapper.h" +#include "IImageWrapperModule.h" +#include "IPlatformFilePak.h" +#if ENGINE_MAJOR_VERSION >= 5 +#include "HAL/PlatformFileManager.h" +#else +#include "HAL/PlatformFilemanager.h" +#endif +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 0 +#include "AssetRegistry/IAssetRegistry.h" +#include "AssetRegistry/AssetRegistryModule.h" +#else +#include "IAssetRegistry.h" +#include "AssetRegistryModule.h" +#endif +#include "Misc/FileHelper.h" +#include "Serialization/ArrayReader.h" +#include "TextureResource.h" + +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 5 +#include "Engine/BlueprintGeneratedClass.h" +#endif + +FLuaValue ULuaBlueprintFunctionLibrary::LuaCreateNil() +{ + return FLuaValue(); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaCreateString(const FString& String) +{ + return FLuaValue(String); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaCreateNumber(const float Value) +{ + return FLuaValue(Value); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaCreateInteger(const int32 Value) +{ + return FLuaValue(Value); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaCreateInteger64(const int64 Value) +{ + return FLuaValue(Value); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaCreateBool(const bool bInBool) +{ + return FLuaValue(bInBool); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaCreateObject(UObject* InObject) +{ + return FLuaValue(InObject); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaCreateUFunction(UObject* InObject, const FString& FunctionName) +{ + if (InObject && InObject->FindFunction(FName(*FunctionName))) + { + FLuaValue Value = FLuaValue::Function(FName(*FunctionName)); + Value.Object = InObject; + return Value; + } + + return FLuaValue(); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaCreateTable(UObject* WorldContextObject, TSubclassOf State) +{ + FLuaValue LuaValue; + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return LuaValue; + + return L->CreateLuaTable(); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaCreateLazyTable(UObject* WorldContextObject, TSubclassOf State) +{ + FLuaValue LuaValue; + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return LuaValue; + + return L->CreateLuaLazyTable(); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaCreateThread(UObject* WorldContextObject, TSubclassOf State, FLuaValue Value) +{ + FLuaValue LuaValue; + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return LuaValue; + + return L->CreateLuaThread(Value); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaCreateObjectInState(UObject* WorldContextObject, TSubclassOf State, UObject* InObject) +{ + FLuaValue LuaValue; + if (!InObject) + return LuaValue; + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return LuaValue; + + LuaValue = FLuaValue(InObject); + LuaValue.LuaState = L; + return LuaValue; +} + +void ULuaBlueprintFunctionLibrary::LuaStateDestroy(UObject* WorldContextObject, TSubclassOf State) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return; + FLuaMachineModule::Get().UnregisterLuaState(L); +} + +void ULuaBlueprintFunctionLibrary::LuaStateReload(UObject* WorldContextObject, TSubclassOf State) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return; + FLuaMachineModule::Get().UnregisterLuaState(L); + FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); +} + +FString ULuaBlueprintFunctionLibrary::Conv_LuaValueToString(const FLuaValue& Value) +{ + return Value.ToString(); +} + +FVector ULuaBlueprintFunctionLibrary::Conv_LuaValueToFVector(const FLuaValue& Value) +{ + return LuaTableToVector(Value); +} + +FRotator ULuaBlueprintFunctionLibrary::Conv_LuaValueToFRotator(const FLuaValue& Value) +{ + return LuaTableToRotator(Value); +} + +FName ULuaBlueprintFunctionLibrary::Conv_LuaValueToName(const FLuaValue& Value) +{ + return FName(*Value.ToString()); +} + +FText ULuaBlueprintFunctionLibrary::Conv_LuaValueToText(const FLuaValue& Value) +{ + return FText::FromString(Value.ToString()); +} + +UObject* ULuaBlueprintFunctionLibrary::Conv_LuaValueToObject(const FLuaValue& Value) +{ + if (Value.Type == ELuaValueType::UObject) + { + return Value.Object; + } + return nullptr; +} + +UClass* ULuaBlueprintFunctionLibrary::Conv_LuaValueToClass(const FLuaValue& Value) +{ + if (Value.Type == ELuaValueType::UObject) + { + UClass* Class = Cast(Value.Object); + if (Class) + return Class; + UBlueprint* Blueprint = Cast(Value.Object); + if (Blueprint) + return Blueprint->GeneratedClass; + } + return nullptr; +} + +FLuaValue ULuaBlueprintFunctionLibrary::Conv_ObjectToLuaValue(UObject* Object) +{ + return FLuaValue(Object); +} + + +FLuaValue ULuaBlueprintFunctionLibrary::Conv_FloatToLuaValue(const float Value) +{ + return FLuaValue(Value); +} + +FLuaValue ULuaBlueprintFunctionLibrary::Conv_BoolToLuaValue(const bool Value) +{ + return FLuaValue(Value); +} + +int32 ULuaBlueprintFunctionLibrary::Conv_LuaValueToInt(const FLuaValue& Value) +{ + return Value.ToInteger(); +} + +int64 ULuaBlueprintFunctionLibrary::Conv_LuaValueToInt64(const FLuaValue& Value) +{ + return Value.ToInteger(); +} + +float ULuaBlueprintFunctionLibrary::Conv_LuaValueToFloat(const FLuaValue& Value) +{ + return Value.ToFloat(); +} + +bool ULuaBlueprintFunctionLibrary::Conv_LuaValueToBool(const FLuaValue& Value) +{ + return Value.ToBool(); +} + +FLuaValue ULuaBlueprintFunctionLibrary::Conv_IntToLuaValue(const int32 Value) +{ + return FLuaValue(Value); +} + +FLuaValue ULuaBlueprintFunctionLibrary::Conv_Int64ToLuaValue(const int64 Value) +{ + return FLuaValue(Value); +} + +FLuaValue ULuaBlueprintFunctionLibrary::Conv_StringToLuaValue(const FString& Value) +{ + return FLuaValue(Value); +} + +FLuaValue ULuaBlueprintFunctionLibrary::Conv_TextToLuaValue(const FText& Value) +{ + return FLuaValue(Value.ToString()); +} + +FLuaValue ULuaBlueprintFunctionLibrary::Conv_NameToLuaValue(const FName Value) +{ + return FLuaValue(Value.ToString()); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaGetGlobal(UObject* WorldContextObject, TSubclassOf State, const FString& Name) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return FLuaValue(); + + uint32 ItemsToPop = L->GetFieldFromTree(Name); + FLuaValue ReturnValue = L->ToLuaValue(-1); + L->Pop(ItemsToPop); + return ReturnValue; +} + +int64 ULuaBlueprintFunctionLibrary::LuaValueToPointer(UObject* WorldContextObject, TSubclassOf State, FLuaValue Value) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return 0; + + L->FromLuaValue(Value); + const void* Ptr = L->ToPointer(-1); + L->Pop(); + + return (int64)Ptr; +} + +FString ULuaBlueprintFunctionLibrary::LuaValueToHexPointer(UObject* WorldContextObject, TSubclassOf State, FLuaValue Value) +{ + int64 Ptr = LuaValueToPointer(WorldContextObject, State, Value); + if (FGenericPlatformProperties::IsLittleEndian()) + { + uint8 BEPtr[8] = + { + (uint8)((Ptr >> 56) & 0xff), + (uint8)((Ptr >> 48) & 0xff), + (uint8)((Ptr >> 40) & 0xff), + (uint8)((Ptr >> 32) & 0xff), + (uint8)((Ptr >> 24) & 0xff), + (uint8)((Ptr >> 16) & 0xff), + (uint8)((Ptr >> 8) & 0xff), + (uint8)((Ptr) & 0xff), + }; + return BytesToHex((const uint8*)BEPtr, sizeof(int64)); + } + return BytesToHex((const uint8*)&Ptr, sizeof(int64)); +} + +FString ULuaBlueprintFunctionLibrary::LuaValueToBase64(const FLuaValue& Value) +{ + return Value.ToBase64(); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaValueFromBase64(const FString& Base64) +{ + return FLuaValue::FromBase64(Base64); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaValueFromUTF16(const FString& String) +{ + TArray Bytes; + if (FGenericPlatformProperties::IsLittleEndian()) + { + for (int32 Index = 0; Index < String.Len(); Index++) + { + uint16 UTF16Char = (uint16)String[Index]; + Bytes.Add((uint8)(UTF16Char & 0xFF)); + Bytes.Add((uint8)((UTF16Char >> 8) & 0xFF)); + } + } + else + { + for (int32 Index = 0; Index < String.Len(); Index++) + { + uint16 UTF16Char = (uint16)String[Index]; + Bytes.Add((uint8)((UTF16Char >> 8) & 0xFF)); + Bytes.Add((uint8)(UTF16Char & 0xFF)); + } + } + return FLuaValue(Bytes); +} + +FString ULuaBlueprintFunctionLibrary::LuaValueToUTF16(const FLuaValue& Value) +{ + FString ReturnValue; + TArray Bytes = Value.ToBytes(); + if (Bytes.Num() % 2 != 0) + { + return ReturnValue; + } + + if (FGenericPlatformProperties::IsLittleEndian()) + { + for (int32 Index = 0; Index < Bytes.Num(); Index += 2) + { + uint16 UTF16Low = Bytes[Index]; + uint16 UTF16High = Bytes[Index + 1]; + ReturnValue.AppendChar((TCHAR)((UTF16High << 8) | UTF16Low)); + } + } + else + { + for (int32 Index = 0; Index < Bytes.Num(); Index += 2) + { + uint16 UTF16High = Bytes[Index]; + uint16 UTF16Low = Bytes[Index + 1]; + ReturnValue.AppendChar((TCHAR)((UTF16High << 8) | UTF16Low)); + } + } + return ReturnValue; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaValueFromUTF8(const FString& String) +{ + FTCHARToUTF8 UTF8String(*String); + return FLuaValue((const char*)UTF8String.Get(), UTF8String.Length()); +} + +FString ULuaBlueprintFunctionLibrary::LuaValueToUTF8(const FLuaValue& Value) +{ + FString ReturnValue; + TArray Bytes = Value.ToBytes(); + Bytes.Add(0); + return FString(UTF8_TO_TCHAR(Bytes.GetData())); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaValueFromUTF32(const FString& String) +{ +#if ENGINE_MINOR_VERSION >= 25 + FTCHARToUTF32 UTF32String(*String); + return FLuaValue((const char*)UTF32String.Get(), UTF32String.Length()); +#else + UE_LOG(LogLuaMachine, Error, TEXT("UTF32 not supported in this engine version")); + return FLuaValue(); +#endif +} + +FString ULuaBlueprintFunctionLibrary::LuaValueToUTF32(const FLuaValue& Value) +{ +#if ENGINE_MINOR_VERSION >= 25 + FString ReturnValue; + TArray Bytes = Value.ToBytes(); + Bytes.Add(0); + Bytes.Add(0); + Bytes.Add(0); + Bytes.Add(0); + return FString(FUTF32ToTCHAR((const UTF32CHAR*)Bytes.GetData(), Bytes.Num() / 4).Get()); +#else + UE_LOG(LogLuaMachine, Error, TEXT("UTF32 not supported in this engine version")); + return FString(""); +#endif +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaRunFile(UObject* WorldContextObject, TSubclassOf State, const FString& Filename, const bool bIgnoreNonExistent) +{ + FLuaValue ReturnValue; + + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return ReturnValue; + + if (!L->RunFile(Filename, bIgnoreNonExistent, 1)) + { + if (L->bLogError) + L->LogError(L->LastError); + L->ReceiveLuaError(L->LastError); + } + else + { + ReturnValue = L->ToLuaValue(-1); + } + + L->Pop(); + return ReturnValue; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaRunNonContentFile(UObject* WorldContextObject, TSubclassOf State, const FString& Filename, const bool bIgnoreNonExistent) +{ + FLuaValue ReturnValue; + + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return ReturnValue; + + if (!L->RunFile(Filename, bIgnoreNonExistent, 1, true)) + { + if (L->bLogError) + L->LogError(L->LastError); + L->ReceiveLuaError(L->LastError); + } + else + { + ReturnValue = L->ToLuaValue(-1); + } + + L->Pop(); + return ReturnValue; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaRunString(UObject* WorldContextObject, TSubclassOf State, const FString& CodeString, FString CodePath) +{ + FLuaValue ReturnValue; + + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + { + return ReturnValue; + } + + return L->RunString(CodeString, CodePath); +} + +ELuaThreadStatus ULuaBlueprintFunctionLibrary::LuaThreadGetStatus(FLuaValue Value) +{ + if (Value.Type != ELuaValueType::Thread || !Value.LuaState.IsValid()) + return ELuaThreadStatus::Invalid; + + return Value.LuaState->GetLuaThreadStatus(Value); +} + +int32 ULuaBlueprintFunctionLibrary::LuaThreadGetStackTop(FLuaValue Value) +{ + if (Value.Type != ELuaValueType::Thread || !Value.LuaState.IsValid()) + { + return MIN_int32; + } + + return Value.LuaState->GetLuaThreadStackTop(Value); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaRunCodeAsset(UObject* WorldContextObject, TSubclassOf State, ULuaCode* CodeAsset) +{ + FLuaValue ReturnValue; + + if (!CodeAsset) + return ReturnValue; + + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return ReturnValue; + + if (!L->RunCodeAsset(CodeAsset, 1)) + { + if (L->bLogError) + L->LogError(L->LastError); + L->ReceiveLuaError(L->LastError); + } + else { + ReturnValue = L->ToLuaValue(-1); + } + L->Pop(); + return ReturnValue; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaRunByteCode(UObject* WorldContextObject, TSubclassOf State, const TArray& ByteCode, const FString& CodePath) +{ + FLuaValue ReturnValue; + + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return ReturnValue; + + if (!L->RunCode(ByteCode, CodePath, 1)) + { + if (L->bLogError) + L->LogError(L->LastError); + L->ReceiveLuaError(L->LastError); + } + else + { + ReturnValue = L->ToLuaValue(-1); + } + L->Pop(); + return ReturnValue; +} + +UTexture2D* ULuaBlueprintFunctionLibrary::LuaValueToTransientTexture(int32 Width, int32 Height, const FLuaValue& Value, EPixelFormat PixelFormat, bool bDetectFormat) +{ + if (Value.Type != ELuaValueType::String) + { + return nullptr; + } + + IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked(TEXT("ImageWrapper")); + + TArray Bytes = Value.ToBytes(); + + if (bDetectFormat) + { + EImageFormat ImageFormat = ImageWrapperModule.DetectImageFormat(Bytes.GetData(), Bytes.Num()); + if (ImageFormat == EImageFormat::Invalid) + { + UE_LOG(LogLuaMachine, Error, TEXT("Unable to detect image format")); + return nullptr; + } + + TSharedPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(ImageFormat); + if (!ImageWrapper.IsValid()) + { + UE_LOG(LogLuaMachine, Error, TEXT("Unable to create ImageWrapper")); + return nullptr; + } + + if (!ImageWrapper->SetCompressed(Bytes.GetData(), Bytes.Num())) + { + UE_LOG(LogLuaMachine, Error, TEXT("Unable to parse image data")); + return nullptr; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + TArray UncompressedBytes; +#else + const TArray* UncompressedBytes = nullptr; +#endif + if (!ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedBytes)) + { + UE_LOG(LogLuaMachine, Error, TEXT("Unable to get raw image data")); + return nullptr; + } + PixelFormat = EPixelFormat::PF_B8G8R8A8; + Width = ImageWrapper->GetWidth(); + Height = ImageWrapper->GetHeight(); +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + Bytes = UncompressedBytes; +#else + Bytes = *UncompressedBytes; +#endif + } + + UTexture2D* Texture = UTexture2D::CreateTransient(Width, Height, PixelFormat); + if (!Texture) + { + return nullptr; + } + +#if ENGINE_MAJOR_VERSION > 4 + FTexture2DMipMap& Mip = Texture->GetPlatformData()->Mips[0]; +#else + FTexture2DMipMap& Mip = Texture->PlatformData->Mips[0]; +#endif + void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE); + FMemory::Memcpy(Data, Bytes.GetData(), Bytes.Num()); + Mip.BulkData.Unlock(); + Texture->UpdateResource(); + + return Texture; +} + +void ULuaBlueprintFunctionLibrary::LuaHttpRequest(UObject* WorldContextObject, TSubclassOf State, const FString& Method, const FString& URL, TMap Headers, FLuaValue Body, FLuaValue Context, const FLuaHttpResponseReceived& ResponseReceived, const FLuaHttpError& Error) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return; + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 26 + TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); +#else + TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); +#endif + HttpRequest->SetVerb(Method); + HttpRequest->SetURL(URL); + for (TPair Header : Headers) + { + HttpRequest->AppendToHeader(Header.Key, Header.Value); + } + HttpRequest->SetContent(Body.ToBytes()); + + TSharedRef ContextSmartRef = L->AddLuaSmartReference(Context); + + HttpRequest->OnProcessRequestComplete().BindStatic(&ULuaBlueprintFunctionLibrary::HttpGenericRequestDone, TWeakPtr(ContextSmartRef), ResponseReceived, Error); + HttpRequest->ProcessRequest(); +} + +void ULuaBlueprintFunctionLibrary::HttpGenericRequestDone(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, TWeakPtr Context, FLuaHttpResponseReceived ResponseReceived, FLuaHttpError Error) +{ + // if the context is invalid, the LuaState is already dead + if (!Context.IsValid()) + return; + + TSharedRef SmartContext = Context.Pin().ToSharedRef(); + + SmartContext->LuaState->RemoveLuaSmartReference(SmartContext); + + if (!bWasSuccessful) + { + Error.ExecuteIfBound(SmartContext->Value); + return; + } + + FLuaValue StatusCode = FLuaValue(Response->GetResponseCode()); + FLuaValue Headers = SmartContext->LuaState->CreateLuaTable(); + for (auto HeaderLine : Response->GetAllHeaders()) + { + int32 Index; + if (HeaderLine.Len() > 2 && HeaderLine.FindChar(':', Index)) + { + FString Key; + if (Index > 0) + Key = HeaderLine.Left(Index); + FString Value = HeaderLine.Right(HeaderLine.Len() - (Index + 2)); + Headers.SetField(Key, FLuaValue(Value)); + } + } + FLuaValue Content = FLuaValue(Response->GetContent()); + FLuaValue LuaHttpResponse = SmartContext->LuaState->CreateLuaTable(); + LuaHttpResponse.SetFieldByIndex(1, StatusCode); + LuaHttpResponse.SetFieldByIndex(2, Headers); + LuaHttpResponse.SetFieldByIndex(3, Content); + ResponseReceived.ExecuteIfBound(SmartContext->Value, LuaHttpResponse); +} + +void ULuaBlueprintFunctionLibrary::LuaRunURL(UObject* WorldContextObject, TSubclassOf State, const FString& URL, TMap Headers, const FString& SecurityHeader, const FString& SignaturePublicExponent, const FString& SignatureModulus, FLuaHttpSuccess Completed) +{ + + // Security CHECK + if (!SecurityHeader.IsEmpty() || !SignaturePublicExponent.IsEmpty() || !SignatureModulus.IsEmpty()) + { + if (SecurityHeader.IsEmpty() || SignaturePublicExponent.IsEmpty() || SignatureModulus.IsEmpty()) + { + UE_LOG(LogLuaMachine, Error, TEXT("For secure LuaRunURL() you need to specify the Security HTTP Header, the Signature Public Exponent and the Signature Modulus")); + return; + } + } +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 26 + TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); +#else + TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); +#endif + HttpRequest->SetURL(URL); + for (TPair Header : Headers) + { + HttpRequest->AppendToHeader(Header.Key, Header.Value); + } + HttpRequest->OnProcessRequestComplete().BindStatic(&ULuaBlueprintFunctionLibrary::HttpRequestDone, State, TWeakObjectPtr(WorldContextObject->GetWorld()), SecurityHeader, SignaturePublicExponent, SignatureModulus, Completed); + HttpRequest->ProcessRequest(); +} + +void ULuaBlueprintFunctionLibrary::HttpRequestDone(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, TSubclassOf LuaState, TWeakObjectPtr World, const FString SecurityHeader, const FString SignaturePublicExponent, const FString SignatureModulus, FLuaHttpSuccess Completed) +{ + FLuaValue ReturnValue; + int32 StatusCode = -1; + + if (!bWasSuccessful) + { + UE_LOG(LogLuaMachine, Error, TEXT("HTTP session failed for \"%s %s\""), *Request->GetVerb(), *Request->GetURL()); + } + + else if (!World.IsValid()) + { + UE_LOG(LogLuaMachine, Error, TEXT("Unable to access LuaState as the World object is no more available (\"%s %s\")"), *Request->GetVerb(), *Request->GetURL()); + } + else + { + // Check signature + if (!SecurityHeader.IsEmpty()) + { + // check code size + if (Response->GetContentLength() <= 0) + { + UE_LOG(LogLuaMachine, Error, TEXT("[Security] Invalid Content Size")); + Completed.ExecuteIfBound(ReturnValue, bWasSuccessful, StatusCode); + return; + } + FString EncryptesSignatureBase64 = Response->GetHeader(SecurityHeader); + if (EncryptesSignatureBase64.IsEmpty()) + { + UE_LOG(LogLuaMachine, Error, TEXT("[Security] Invalid Security HTTP Header")); + Completed.ExecuteIfBound(ReturnValue, bWasSuccessful, StatusCode); + return; + } + + const uint32 KeySize = 64; + + TArray Signature; + FBase64::Decode(EncryptesSignatureBase64, Signature); + + if (Signature.Num() != KeySize) + { + UE_LOG(LogLuaMachine, Error, TEXT("[Security] Invalid Signature")); + Completed.ExecuteIfBound(ReturnValue, bWasSuccessful, StatusCode); + return; + } + TEncryptionInt SignatureValue = TEncryptionInt((uint32*)&Signature[0]); + + TArray PublicExponent; + FBase64::Decode(SignaturePublicExponent, PublicExponent); + if (PublicExponent.Num() != KeySize) + { + UE_LOG(LogLuaMachine, Error, TEXT("[Security] Invalid Signature Public Exponent")); + Completed.ExecuteIfBound(ReturnValue, bWasSuccessful, StatusCode); + return; + } + + TArray Modulus; + FBase64::Decode(SignatureModulus, Modulus); + if (Modulus.Num() != KeySize) + { + UE_LOG(LogLuaMachine, Error, TEXT("[Security] Invalid Signature Modulus")); + Completed.ExecuteIfBound(ReturnValue, bWasSuccessful, StatusCode); + return; + } + + TEncryptionInt PublicKey = TEncryptionInt((uint32*)&PublicExponent[0]); + TEncryptionInt ModulusValue = TEncryptionInt((uint32*)&Modulus[0]); + + TEncryptionInt ShaHash; + FSHA1::HashBuffer(Response->GetContent().GetData(), Response->GetContentLength(), (uint8*)&ShaHash); + + TEncryptionInt DecryptedSignature = FEncryption::ModularPow(SignatureValue, PublicKey, ModulusValue); + if (DecryptedSignature != ShaHash) + { + UE_LOG(LogLuaMachine, Error, TEXT("[Security] Signature check failed")); + Completed.ExecuteIfBound(ReturnValue, bWasSuccessful, StatusCode); + return; + } + } + + ReturnValue = LuaRunString(World.Get(), LuaState, Response->GetContentAsString()); + StatusCode = Response->GetResponseCode(); + } + + Completed.ExecuteIfBound(ReturnValue, bWasSuccessful, StatusCode); +} + +void ULuaBlueprintFunctionLibrary::LuaTableFillObject(FLuaValue InTable, UObject* InObject) +{ + if (InTable.Type != ELuaValueType::Table || !InObject) + return; + + ULuaState* L = InTable.LuaState.Get(); + if (!L) + return; + + UStruct* Class = Cast(InObject); + if (!Class) + Class = InObject->GetClass(); + + L->FromLuaValue(InTable); + L->PushNil(); // first key + while (L->Next(-2)) + { + FLuaValue Key = L->ToLuaValue(-2); + FLuaValue Value = L->ToLuaValue(-1); + L->SetPropertyFromLuaValue(InObject, Key.ToString(), Value); + L->Pop(); // pop the value + } + + L->Pop(); // pop the table +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaTableGetField(FLuaValue Table, const FString& Key) +{ + ULuaState* L = Table.LuaState.Get(); + if (!L) + return FLuaValue(); + + return Table.GetField(Key); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaComponentGetField(FLuaValue LuaComponent, const FString& Key) +{ + FLuaValue ReturnValue; + if (LuaComponent.Type != ELuaValueType::UObject) + return ReturnValue; + + ULuaState* L = LuaComponent.LuaState.Get(); + if (!L) + return ReturnValue; + + ULuaComponent* Component = Cast(LuaComponent.Object); + + FLuaValue* LuaValue = Component->Table.Find(Key); + if (LuaValue) + { + return *LuaValue; + } + + return ReturnValue; +} + +bool ULuaBlueprintFunctionLibrary::LuaValueIsNil(const FLuaValue& Value) +{ + return Value.Type == ELuaValueType::Nil; +} + +bool ULuaBlueprintFunctionLibrary::LuaValueIsOwned(const FLuaValue& Value) +{ + return Value.LuaState != nullptr; +} + +TSubclassOf ULuaBlueprintFunctionLibrary::LuaValueGetOwner(const FLuaValue& Value) +{ + if (!Value.LuaState.IsValid()) + { + return nullptr; + } + return Value.LuaState->GetClass(); +} + +bool ULuaBlueprintFunctionLibrary::LuaValueIsNotNil(const FLuaValue& Value) +{ + return Value.Type != ELuaValueType::Nil; +} + +bool ULuaBlueprintFunctionLibrary::LuaValueIsTable(const FLuaValue& Value) +{ + return Value.Type == ELuaValueType::Table; +} + +bool ULuaBlueprintFunctionLibrary::LuaValueIsBoolean(const FLuaValue& Value) +{ + return Value.Type == ELuaValueType::Bool; +} + +bool ULuaBlueprintFunctionLibrary::LuaValueIsThread(const FLuaValue& Value) +{ + return Value.Type == ELuaValueType::Thread; +} + +bool ULuaBlueprintFunctionLibrary::LuaValueIsFunction(const FLuaValue& Value) +{ + return Value.Type == ELuaValueType::Function; +} + +bool ULuaBlueprintFunctionLibrary::LuaValueIsNumber(const FLuaValue& Value) +{ + return Value.Type == ELuaValueType::Number; +} + +bool ULuaBlueprintFunctionLibrary::LuaValueIsInteger(const FLuaValue& Value) +{ + return Value.Type == ELuaValueType::Integer; +} + +bool ULuaBlueprintFunctionLibrary::LuaValueIsString(const FLuaValue& Value) +{ + return Value.Type == ELuaValueType::String; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaTableGetByIndex(FLuaValue Table, int32 Index) +{ + if (Table.Type != ELuaValueType::Table) + return FLuaValue(); + + ULuaState* L = Table.LuaState.Get(); + if (!L) + return FLuaValue(); + + return Table.GetFieldByIndex(Index); +} + +FLuaValue ULuaBlueprintFunctionLibrary::AssignLuaValueToLuaState(UObject* WorldContextObject, FLuaValue Value, TSubclassOf State) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return Value; + Value.LuaState = L; + return Value; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaTableSetByIndex(FLuaValue Table, int32 Index, FLuaValue Value) +{ + if (Table.Type != ELuaValueType::Table) + return FLuaValue(); + + ULuaState* L = Table.LuaState.Get(); + if (!L) + return FLuaValue(); + + return Table.SetFieldByIndex(Index, Value); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaTableSetField(FLuaValue Table, const FString& Key, FLuaValue Value) +{ + FLuaValue ReturnValue; + if (Table.Type != ELuaValueType::Table) + return ReturnValue; + + ULuaState* L = Table.LuaState.Get(); + if (!L) + return ReturnValue; + + return Table.SetField(Key, Value); +} + +FLuaValue ULuaBlueprintFunctionLibrary::GetLuaComponentAsLuaValue(AActor* Actor) +{ + if (!Actor) + return FLuaValue(); + + return FLuaValue(Actor->GetComponentByClass(ULuaComponent::StaticClass())); +} + +FLuaValue ULuaBlueprintFunctionLibrary::GetLuaComponentByStateAsLuaValue(AActor* Actor, TSubclassOf State) +{ + if (!Actor) + return FLuaValue(); +#if ENGINE_MAJOR_VERSION < 5 && ENGINE_MINOR_VERSION < 24 + TArray Components = Actor->GetComponentsByClass(ULuaComponent::StaticClass()); +#else + TArray Components; + Actor->GetComponents(Components); +#endif + for (UActorComponent* Component : Components) + { + ULuaComponent* LuaComponent = Cast(Component); + if (LuaComponent) + { + if (LuaComponent->LuaState == State) + { + return FLuaValue(LuaComponent); + } + } + } + + return FLuaValue(); +} + +FLuaValue ULuaBlueprintFunctionLibrary::GetLuaComponentByNameAsLuaValue(AActor* Actor, const FString& Name) +{ + if (!Actor) + return FLuaValue(); + +#if ENGINE_MAJOR_VERSION < 5 && ENGINE_MINOR_VERSION < 24 + TArray Components = Actor->GetComponentsByClass(ULuaComponent::StaticClass()); +#else + TArray Components; + Actor->GetComponents(Components); +#endif + for (UActorComponent* Component : Components) + { + ULuaComponent* LuaComponent = Cast(Component); + if (LuaComponent) + { + if (LuaComponent->GetName() == Name) + { + return FLuaValue(LuaComponent); + } + } + } + + return FLuaValue(); +} + +FLuaValue ULuaBlueprintFunctionLibrary::GetLuaComponentByStateAndNameAsLuaValue(AActor* Actor, TSubclassOf State, const FString& Name) +{ + if (!Actor) + return FLuaValue(); + +#if ENGINE_MAJOR_VERSION < 5 && ENGINE_MINOR_VERSION < 24 + TArray Components = Actor->GetComponentsByClass(ULuaComponent::StaticClass()); +#else + TArray Components; + Actor->GetComponents(Components); +#endif + for (UActorComponent* Component : Components) + { + ULuaComponent* LuaComponent = Cast(Component); + if (LuaComponent) + { + if (LuaComponent->LuaState == State && LuaComponent->GetName() == Name) + { + return FLuaValue(LuaComponent); + } + } + } + + return FLuaValue(); +} + +int32 ULuaBlueprintFunctionLibrary::LuaGetTop(UObject* WorldContextObject, TSubclassOf State) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return MIN_int32; + return L->GetTop(); +} + +void ULuaBlueprintFunctionLibrary::LuaSetGlobal(UObject* WorldContextObject, TSubclassOf State, const FString& Name, FLuaValue Value) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return; + L->SetFieldFromTree(Name, Value, true); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaGlobalCall(UObject* WorldContextObject, TSubclassOf State, const FString& Name, TArray Args) +{ + FLuaValue ReturnValue; + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return ReturnValue; + + int32 ItemsToPop = L->GetFieldFromTree(Name); + + int NArgs = 0; + for (FLuaValue& Arg : Args) + { + L->FromLuaValue(Arg); + NArgs++; + } + + L->PCall(NArgs, ReturnValue); + + // we have the return value and the function has been removed, so we do not need to change ItemsToPop + L->Pop(ItemsToPop); + + return ReturnValue; +} + +TArray ULuaBlueprintFunctionLibrary::LuaGlobalCallMulti(UObject* WorldContextObject, TSubclassOf State, const FString& Name, TArray Args) +{ + TArray ReturnValue; + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return ReturnValue; + + int32 ItemsToPop = L->GetFieldFromTree(Name); + + int32 StackTop = L->GetTop(); + + int NArgs = 0; + for (FLuaValue& Arg : Args) + { + L->FromLuaValue(Arg); + NArgs++; + } + + FLuaValue LastReturnValue; + if (L->PCall(NArgs, LastReturnValue, LUA_MULTRET)) + { + + int32 NumOfReturnValues = (L->GetTop() - StackTop) + 1; + if (NumOfReturnValues > 0) + { + for (int32 i = -1; i >= -(NumOfReturnValues); i--) + { + ReturnValue.Insert(L->ToLuaValue(i), 0); + } + L->Pop(NumOfReturnValues - 1); + } + + } + + // we have the return value and the function has been removed, so we do not need to change ItemsToPop + L->Pop(ItemsToPop); + + return ReturnValue; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaGlobalCallValue(UObject* WorldContextObject, TSubclassOf State, FLuaValue Value, TArray Args) +{ + FLuaValue ReturnValue; + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return ReturnValue; + + L->FromLuaValue(Value); + + int NArgs = 0; + for (FLuaValue& Arg : Args) + { + L->FromLuaValue(Arg); + NArgs++; + } + + L->PCall(NArgs, ReturnValue); + + L->Pop(); + + return ReturnValue; +} + +TArray ULuaBlueprintFunctionLibrary::LuaGlobalCallValueMulti(UObject* WorldContextObject, TSubclassOf State, FLuaValue Value, TArray Args) +{ + TArray ReturnValue; + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return ReturnValue; + + L->FromLuaValue(Value); + + int32 StackTop = L->GetTop(); + + int NArgs = 0; + for (FLuaValue& Arg : Args) + { + L->FromLuaValue(Arg); + NArgs++; + } + + FLuaValue LastReturnValue; + if (L->PCall(NArgs, LastReturnValue, LUA_MULTRET)) + { + + int32 NumOfReturnValues = (L->GetTop() - StackTop) + 1; + if (NumOfReturnValues > 0) + { + for (int32 i = -1; i >= -(NumOfReturnValues); i--) + { + ReturnValue.Insert(L->ToLuaValue(i), 0); + } + L->Pop(NumOfReturnValues - 1); + } + + } + + L->Pop(); + + return ReturnValue; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaValueCall(FLuaValue Value, TArray Args) +{ + FLuaValue ReturnValue; + + ULuaState* L = Value.LuaState.Get(); + if (!L) + return ReturnValue; + + L->FromLuaValue(Value); + + int NArgs = 0; + for (FLuaValue& Arg : Args) + { + L->FromLuaValue(Arg); + NArgs++; + } + + L->PCall(NArgs, ReturnValue); + + L->Pop(); + + return ReturnValue; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaValueCallIfNotNil(FLuaValue Value, TArray Args) +{ + FLuaValue ReturnValue; + if (Value.Type != ELuaValueType::Nil) + ReturnValue = LuaValueCall(Value, Args); + + return ReturnValue; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaTableKeyCall(FLuaValue InTable, const FString& Key, TArray Args) +{ + FLuaValue ReturnValue; + if (InTable.Type != ELuaValueType::Table) + return ReturnValue; + + ULuaState* L = InTable.LuaState.Get(); + if (!L) + return ReturnValue; + + FLuaValue Value = InTable.GetField(Key); + if (Value.Type == ELuaValueType::Nil) + return ReturnValue; + + return LuaValueCall(Value, Args); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaTableKeyCallWithSelf(FLuaValue InTable, const FString& Key, TArray Args) +{ + FLuaValue ReturnValue; + if (InTable.Type != ELuaValueType::Table) + return ReturnValue; + + ULuaState* L = InTable.LuaState.Get(); + if (!L) + return ReturnValue; + + FLuaValue Value = InTable.GetField(Key); + if (Value.Type == ELuaValueType::Nil) + return ReturnValue; + + Args.Insert(InTable, 0); + + return LuaValueCall(Value, Args); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaTableIndexCall(FLuaValue InTable, int32 Index, TArray Args) +{ + FLuaValue ReturnValue; + if (InTable.Type != ELuaValueType::Table) + return ReturnValue; + + ULuaState* L = InTable.LuaState.Get(); + if (!L) + return ReturnValue; + + FLuaValue Value = InTable.GetFieldByIndex(Index); + if (Value.Type == ELuaValueType::Nil) + return ReturnValue; + + return LuaValueCall(Value, Args); +} + +TArray ULuaBlueprintFunctionLibrary::LuaTableUnpack(FLuaValue InTable) +{ + TArray ReturnValue; + if (InTable.Type != ELuaValueType::Table) + return ReturnValue; + + int32 Index = 1; + for (;;) + { + FLuaValue Item = InTable.GetFieldByIndex(Index++); + if (Item.Type == ELuaValueType::Nil) + break; + ReturnValue.Add(Item); + } + + return ReturnValue; +} + +TArray ULuaBlueprintFunctionLibrary::LuaTableMergeUnpack(FLuaValue InTable1, FLuaValue InTable2) +{ + TArray ReturnValue; + if (InTable1.Type != ELuaValueType::Table) + return ReturnValue; + + if (InTable2.Type != ELuaValueType::Table) + return ReturnValue; + + int32 Index = 1; + for (;;) + { + FLuaValue Item = InTable1.GetFieldByIndex(Index++); + if (Item.Type == ELuaValueType::Nil) + break; + ReturnValue.Add(Item); + } + + for (;;) + { + FLuaValue Item = InTable2.GetFieldByIndex(Index++); + if (Item.Type == ELuaValueType::Nil) + break; + ReturnValue.Add(Item); + } + + return ReturnValue; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaTablePack(UObject* WorldContextObject, TSubclassOf State, TArray Values) +{ + FLuaValue ReturnValue; + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return ReturnValue; + + ReturnValue = L->CreateLuaTable(); + + int32 Index = 1; + + for (FLuaValue& Value : Values) + { + ReturnValue.SetFieldByIndex(Index++, Value); + } + + return ReturnValue; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaTableMergePack(UObject* WorldContextObject, TSubclassOf State, TArray Values1, TArray Values2) +{ + FLuaValue ReturnValue; + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return ReturnValue; + + ReturnValue = L->CreateLuaTable(); + + int32 Index = 1; + + for (FLuaValue& Value : Values1) + { + ReturnValue.SetFieldByIndex(Index++, Value); + } + + for (FLuaValue& Value : Values2) + { + ReturnValue.SetFieldByIndex(Index++, Value); + } + + return ReturnValue; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaTableFromMap(UObject* WorldContextObject, TSubclassOf State, TMap Map) +{ + FLuaValue ReturnValue; + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return ReturnValue; + + ReturnValue = L->CreateLuaTable(); + + for (TPair& Pair : Map) + { + ReturnValue.SetField(Pair.Key, Pair.Value); + } + + return ReturnValue; +} + +TArray ULuaBlueprintFunctionLibrary::LuaTableRange(FLuaValue InTable, int32 First, int32 Last) +{ + TArray ReturnValue; + if (InTable.Type != ELuaValueType::Table) + return ReturnValue; + + for (int32 i = First; i <= Last; i++) + { + ReturnValue.Add(InTable.GetFieldByIndex(i)); + } + + return ReturnValue; +} + +TArray ULuaBlueprintFunctionLibrary::LuaValueArrayMerge(TArray Array1, TArray Array2) +{ + TArray NewArray = Array1; + NewArray.Append(Array2); + return NewArray; +} + +TArray ULuaBlueprintFunctionLibrary::LuaValueArrayAppend(TArray Array, FLuaValue Value) +{ + TArray NewArray = Array; + NewArray.Add(Value); + return NewArray; +} + +TArray ULuaBlueprintFunctionLibrary::LuaValueCallMulti(FLuaValue Value, TArray Args) +{ + TArray ReturnValue; + + ULuaState* L = Value.LuaState.Get(); + if (!L) + return ReturnValue; + + L->FromLuaValue(Value); + + int32 StackTop = L->GetTop(); + + int NArgs = 0; + for (FLuaValue& Arg : Args) + { + L->FromLuaValue(Arg); + NArgs++; + } + + FLuaValue LastReturnValue; + if (L->PCall(NArgs, LastReturnValue, LUA_MULTRET)) + { + + int32 NumOfReturnValues = (L->GetTop() - StackTop) + 1; + if (NumOfReturnValues > 0) + { + for (int32 i = -1; i >= -(NumOfReturnValues); i--) + { + ReturnValue.Insert(L->ToLuaValue(i), 0); + } + L->Pop(NumOfReturnValues - 1); + } + + } + + L->Pop(); + + return ReturnValue; +} + +void ULuaBlueprintFunctionLibrary::LuaValueYield(FLuaValue Value, TArray Args) +{ + if (Value.Type != ELuaValueType::Thread) + return; + + ULuaState* L = Value.LuaState.Get(); + if (!L) + return; + + L->FromLuaValue(Value); + + int32 StackTop = L->GetTop(); + + int NArgs = 0; + for (FLuaValue& Arg : Args) + { + L->FromLuaValue(Arg); + NArgs++; + } + + L->Yield(-1 - NArgs, NArgs); + + L->Pop(); +} + +TArray ULuaBlueprintFunctionLibrary::LuaValueResumeMulti(FLuaValue Value, TArray Args) +{ + TArray ReturnValue; + + if (Value.Type != ELuaValueType::Thread) + return ReturnValue; + + ULuaState* L = Value.LuaState.Get(); + if (!L) + return ReturnValue; + + L->FromLuaValue(Value); + + int32 StackTop = L->GetTop(); + + int NArgs = 0; + for (FLuaValue& Arg : Args) + { + L->FromLuaValue(Arg); + NArgs++; + } + + L->Resume(-1 - NArgs, NArgs); + + int32 NumOfReturnValues = (L->GetTop() - StackTop); + if (NumOfReturnValues > 0) + { + for (int32 i = -1; i >= -(NumOfReturnValues); i--) + { + ReturnValue.Insert(L->ToLuaValue(i), 0); + } + L->Pop(NumOfReturnValues); + } + + L->Pop(); + + return ReturnValue; +} + +FVector ULuaBlueprintFunctionLibrary::LuaTableToVector(FLuaValue Value) +{ + if (Value.Type != ELuaValueType::Table) + { + return FVector(NAN); + } + + auto GetVectorField = [](FLuaValue& Table, const char* Field_n, const char* Field_N, int32 Index) -> FLuaValue + { + FLuaValue N = Table.GetField(Field_n); + if (N.IsNil()) + { + N = Table.GetField(Field_N); + if (N.IsNil()) + { + N = Table.GetFieldByIndex(Index); + if (N.IsNil()) + { + N = FLuaValue(NAN); + } + } + } + return N; + }; + + FLuaValue X = GetVectorField(Value, "x", "X", 1); + FLuaValue Y = GetVectorField(Value, "y", "Y", 2); + FLuaValue Z = GetVectorField(Value, "z", "Z", 3); + + return FVector(X.ToFloat(), Y.ToFloat(), Z.ToFloat()); +} + +FRotator ULuaBlueprintFunctionLibrary::LuaTableToRotator(FLuaValue Value) +{ + if (Value.Type != ELuaValueType::Table) + { + return FRotator(NAN); + } + + auto GetRotatorField = [](FLuaValue& Table, const char* Field_n, const char* Field_N, int32 Index) -> FLuaValue + { + FLuaValue N = Table.GetField(Field_n); + if (N.IsNil()) + { + N = Table.GetField(Field_N); + if (N.IsNil()) + { + N = Table.GetFieldByIndex(Index); + if (N.IsNil()) + { + N = FLuaValue(NAN); + } + } + } + return N; + }; + + FLuaValue Roll = GetRotatorField(Value, "roll", "Roll", 1); + FLuaValue Pitch = GetRotatorField(Value, "pitch", "Pitch", 2); + FLuaValue Yaw = GetRotatorField(Value, "yaw", "Yaw", 3); + + return FRotator(Pitch.ToFloat(), Yaw.ToFloat(), Roll.ToFloat()); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaTableSetMetaTable(FLuaValue InTable, FLuaValue InMetaTable) +{ + FLuaValue ReturnValue; + if (InTable.Type != ELuaValueType::Table || InMetaTable.Type != ELuaValueType::Table) + return ReturnValue; + + ULuaState* L = InTable.LuaState.Get(); + if (!L) + return ReturnValue; + + return InTable.SetMetaTable(InMetaTable); +} + +int32 ULuaBlueprintFunctionLibrary::LuaValueLength(FLuaValue Value) +{ + + ULuaState* L = Value.LuaState.Get(); + if (!L) + return 0; + + L->FromLuaValue(Value); + L->Len(-1); + int32 Length = L->ToInteger(-1); + L->Pop(2); + + return Length; +} + +TArray ULuaBlueprintFunctionLibrary::LuaTableGetKeys(FLuaValue Table) +{ + TArray Keys; + + if (Table.Type != ELuaValueType::Table) + return Keys; + + ULuaState* L = Table.LuaState.Get(); + if (!L) + return Keys; + + L->FromLuaValue(Table); + L->PushNil(); // first key + while (L->Next(-2)) + { + Keys.Add(L->ToLuaValue(-2)); // add key + L->Pop(); // pop the value + } + + L->Pop(); // pop the table + + return Keys; +} + +TArray ULuaBlueprintFunctionLibrary::LuaTableGetValues(FLuaValue Table) +{ + TArray Keys; + + if (Table.Type != ELuaValueType::Table) + return Keys; + + ULuaState* L = Table.LuaState.Get(); + if (!L) + return Keys; + + L->FromLuaValue(Table); + L->PushNil(); // first key + while (L->Next(-2)) + { + Keys.Add(L->ToLuaValue(-1)); // add value + L->Pop(); // pop the value + } + + L->Pop(); // pop the table + + return Keys; +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaTableAssetToLuaTable(UObject* WorldContextObject, TSubclassOf State, ULuaTableAsset* TableAsset) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return FLuaValue(); + + return TableAsset->ToLuaTable(L); +} + +FLuaValue ULuaBlueprintFunctionLibrary::LuaNewLuaUserDataObject(UObject* WorldContextObject, TSubclassOf State, TSubclassOf UserDataObjectClass, bool bTrackObject) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return FLuaValue(); + + return L->NewLuaUserDataObject(UserDataObjectClass, bTrackObject); +} + +ULuaState* ULuaBlueprintFunctionLibrary::LuaGetState(UObject* WorldContextObject, TSubclassOf State) +{ + return FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); +} + +bool ULuaBlueprintFunctionLibrary::LuaTableImplements(FLuaValue Table, ULuaTableAsset* TableAsset) +{ + if (Table.Type != ELuaValueType::Table) + return false; + + ULuaState* L = Table.LuaState.Get(); + if (!L) + return false; + + for (TPair& Pair : TableAsset->Table) + { + FLuaValue Item = Table.GetField(Pair.Key); + if (Item.Type == ELuaValueType::Nil) + return false; + if (Item.Type != Pair.Value.Type) + return false; + } + + return true; +} + +bool ULuaBlueprintFunctionLibrary::LuaTableImplementsAll(FLuaValue Table, TArray TableAssets) +{ + for (ULuaTableAsset* TableAsset : TableAssets) + { + if (!LuaTableImplements(Table, TableAsset)) + return false; + } + return true; +} + +bool ULuaBlueprintFunctionLibrary::LuaTableImplementsAny(FLuaValue Table, TArray TableAssets) +{ + for (ULuaTableAsset* TableAsset : TableAssets) + { + if (LuaTableImplements(Table, TableAsset)) + return true; + } + return false; +} + +int32 ULuaBlueprintFunctionLibrary::LuaGetUsedMemory(UObject* WorldContextObject, TSubclassOf State) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return -1; + + return L->GC(LUA_GCCOUNT); +} + +void ULuaBlueprintFunctionLibrary::LuaGCCollect(UObject* WorldContextObject, TSubclassOf State) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return; + + L->GC(LUA_GCCOLLECT); +} + +void ULuaBlueprintFunctionLibrary::LuaGCStop(UObject* WorldContextObject, TSubclassOf State) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return; + + L->GC(LUA_GCSTOP); +} + +void ULuaBlueprintFunctionLibrary::LuaGCRestart(UObject* WorldContextObject, TSubclassOf State) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return; + + L->GC(LUA_GCRESTART); +} + +void ULuaBlueprintFunctionLibrary::LuaSetUserDataMetaTable(UObject* WorldContextObject, TSubclassOf State, FLuaValue MetaTable) +{ + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return; + L->SetUserDataMetaTable(MetaTable); +} + +bool ULuaBlueprintFunctionLibrary::LuaValueIsReferencedInLuaRegistry(FLuaValue Value) +{ + return Value.IsReferencedInLuaRegistry(); +} + +UClass* ULuaBlueprintFunctionLibrary::LuaValueToBlueprintGeneratedClass(const FLuaValue& Value) +{ + UObject* LoadedObject = nullptr; + if (Value.Type == ELuaValueType::String) + { + LoadedObject = StaticLoadObject(UBlueprint::StaticClass(), nullptr, *Value.ToString()); + } + else if (Value.Type == ELuaValueType::UObject) + { + LoadedObject = Value.Object; + } + + if (!LoadedObject) + return nullptr; + + UBlueprint* Blueprint = Cast(LoadedObject); + if (!Blueprint) + return nullptr; + return Cast(Blueprint->GeneratedClass); +} + +UClass* ULuaBlueprintFunctionLibrary::LuaValueLoadClass(const FLuaValue& Value, bool bDetectBlueprintGeneratedClass) +{ + UObject* LoadedObject = nullptr; + if (Value.Type == ELuaValueType::String) + { + LoadedObject = StaticLoadObject(UObject::StaticClass(), nullptr, *Value.ToString()); + } + else if (Value.Type == ELuaValueType::UObject) + { + LoadedObject = Value.Object; + } + + if (!LoadedObject) + return nullptr; + + if (bDetectBlueprintGeneratedClass) + { + UBlueprint* Blueprint = Cast(LoadedObject); + if (Blueprint) + { + return Cast(Blueprint->GeneratedClass); + } + UBlueprintGeneratedClass* BlueprintGeneratedClass = Cast(LoadedObject); + if (BlueprintGeneratedClass) + { + return BlueprintGeneratedClass; + } + } + + return Cast(LoadedObject); +} + +UObject* ULuaBlueprintFunctionLibrary::LuaValueLoadObject(const FLuaValue& Value) +{ + UObject* LoadedObject = nullptr; + if (Value.Type == ELuaValueType::String) + { + LoadedObject = StaticLoadObject(UObject::StaticClass(), nullptr, *Value.ToString()); + } + else if (Value.Type == ELuaValueType::UObject) + { + LoadedObject = Value.Object; + } + + return LoadedObject; +} + +bool ULuaBlueprintFunctionLibrary::LuaValueFromJson(UObject* WorldContextObject, TSubclassOf State, const FString& Json, FLuaValue& LuaValue) +{ + // default to nil + LuaValue = FLuaValue(); + + ULuaState* L = FLuaMachineModule::Get().GetLuaState(State, WorldContextObject->GetWorld()); + if (!L) + return false; + + TSharedPtr JsonValue; + TSharedRef< TJsonReader > JsonReader = TJsonReaderFactory::Create(Json); + if (!FJsonSerializer::Deserialize(JsonReader, JsonValue)) + { + return false; + } + + LuaValue = FLuaValue::FromJsonValue(L, *JsonValue); + return true; +} + +FString ULuaBlueprintFunctionLibrary::LuaValueToJson(FLuaValue Value) +{ + FString Json; + TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&Json); + FJsonSerializer::Serialize(Value.ToJsonValue(), "", JsonWriter); + return Json; +} + +bool ULuaBlueprintFunctionLibrary::LuaLoadPakFile(const FString& Filename, FString Mountpoint, TArray& Assets, FString ContentPath, FString AssetRegistryPath) +{ + if (!Mountpoint.StartsWith("/") || !Mountpoint.EndsWith("/")) + { + UE_LOG(LogLuaMachine, Error, TEXT("Invalid Mountpoint, must be in the format /Name/")); + return false; + } + + IPlatformFile& TopPlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + bool bCustomPakPlatformFile = false; + + FPakPlatformFile* PakPlatformFile = (FPakPlatformFile*)FPlatformFileManager::Get().FindPlatformFile(TEXT("PakFile")); + if (!PakPlatformFile) + { + PakPlatformFile = new FPakPlatformFile(); + if (!PakPlatformFile->Initialize(&TopPlatformFile, TEXT(""))) + { + UE_LOG(LogLuaMachine, Error, TEXT("Unable to setup PakPlatformFile")); + delete(PakPlatformFile); + return false; + } + FPlatformFileManager::Get().SetPlatformFile(*PakPlatformFile); + bCustomPakPlatformFile = true; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION > 26 + TRefCountPtr PakFile = new FPakFile(PakPlatformFile, *Filename, false); +#else + FPakFile PakFile(PakPlatformFile, *Filename, false); +#endif + if (!PakFile.IsValid()) + { + UE_LOG(LogLuaMachine, Error, TEXT("Unable to open PakFile")); + if (bCustomPakPlatformFile) + { + FPlatformFileManager::Get().SetPlatformFile(TopPlatformFile); + delete(PakPlatformFile); + } + return false; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION > 26 + FString PakFileMountPoint = PakFile->GetMountPoint(); +#else + FString PakFileMountPoint = PakFile.GetMountPoint(); +#endif + + FPaths::MakeStandardFilename(Mountpoint); + + FPaths::MakeStandardFilename(PakFileMountPoint); + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION > 26 + PakFile->SetMountPoint(*PakFileMountPoint); +#else + PakFile.SetMountPoint(*PakFileMountPoint); +#endif + + if (!PakPlatformFile->Mount(*Filename, 0, *PakFileMountPoint)) + { + UE_LOG(LogLuaMachine, Error, TEXT("Unable to mount PakFile")); + if (bCustomPakPlatformFile) + { + FPlatformFileManager::Get().SetPlatformFile(TopPlatformFile); + delete(PakPlatformFile); + } + return false; + } + + if (ContentPath.IsEmpty()) + { + ContentPath = "/Plugins" + Mountpoint + "Content/"; + } + + FString MountDestination = PakFileMountPoint + ContentPath; + FPaths::MakeStandardFilename(MountDestination); + + FPackageName::RegisterMountPoint(Mountpoint, MountDestination); + + IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")).Get(); + +#if WITH_EDITOR +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION > 23 + int32 bPreviousGAllowUnversionedContentInEditor = GAllowUnversionedContentInEditor; +#else + bool bPreviousGAllowUnversionedContentInEditor = GAllowUnversionedContentInEditor; +#endif + GAllowUnversionedContentInEditor = true; +#endif + + if (AssetRegistryPath.IsEmpty()) + { + AssetRegistryPath = "/Plugins" + Mountpoint + "AssetRegistry.bin"; + } + + FArrayReader SerializedAssetData; + if (!FFileHelper::LoadFileToArray(SerializedAssetData, *(PakFileMountPoint + AssetRegistryPath))) + { + UE_LOG(LogLuaMachine, Error, TEXT("Unable to parse AssetRegistry file")); + if (bCustomPakPlatformFile) + { + FPlatformFileManager::Get().SetPlatformFile(TopPlatformFile); + delete(PakPlatformFile); + } +#if WITH_EDITOR + GAllowUnversionedContentInEditor = bPreviousGAllowUnversionedContentInEditor; +#endif + return false; + } + + AssetRegistry.Serialize(SerializedAssetData); + + AssetRegistry.ScanPathsSynchronous({ Mountpoint }, true); + + TArray AssetData; + AssetRegistry.GetAllAssets(AssetData, false); + + for (auto Asset : AssetData) + { +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 0 + if (Asset.GetObjectPathString().StartsWith(Mountpoint)) +#else + if (Asset.ObjectPath.ToString().StartsWith(Mountpoint)) +#endif + { + Assets.Add(FLuaValue(Asset.GetAsset())); + } + } + + if (bCustomPakPlatformFile) + { + FPlatformFileManager::Get().SetPlatformFile(TopPlatformFile); + delete(PakPlatformFile); + } + +#if WITH_EDITOR + GAllowUnversionedContentInEditor = bPreviousGAllowUnversionedContentInEditor; +#endif + + return true; +} + +void ULuaBlueprintFunctionLibrary::SwitchOnLuaValueType(const FLuaValue& LuaValue, ELuaValueType& LuaValueTypes) +{ + LuaValueTypes = LuaValue.Type; +} + +void ULuaBlueprintFunctionLibrary::GetLuaReflectionType(UObject* InObject, const FString& Name, ELuaReflectionType& LuaReflectionTypes) +{ + LuaReflectionTypes = ELuaReflectionType::Unknown; + UClass* Class = InObject->GetClass(); + if (!Class) + { + return; + } + + if (Class->FindPropertyByName(FName(*Name)) != nullptr) + { + LuaReflectionTypes = ELuaReflectionType::Property; + return; + } + + if (Class->FindFunctionByName(FName(*Name))) + { + LuaReflectionTypes = ELuaReflectionType::Function; + return; + } +} + +void ULuaBlueprintFunctionLibrary::RegisterLuaConsoleCommand(const FString& CommandName, const FLuaValue& LuaConsoleCommand) +{ + FLuaMachineModule::Get().RegisterLuaConsoleCommand(CommandName, LuaConsoleCommand); +} + +void ULuaBlueprintFunctionLibrary::UnregisterLuaConsoleCommand(const FString& CommandName) +{ + FLuaMachineModule::Get().UnregisterLuaConsoleCommand(CommandName); +} + +ULuaState* ULuaBlueprintFunctionLibrary::CreateDynamicLuaState(UObject* WorldContextObject, TSubclassOf LuaStateClass) +{ + if (!LuaStateClass) + { + return nullptr; + } + + if (LuaStateClass == ULuaState::StaticClass()) + { + UE_LOG(LogLuaMachine, Error, TEXT("attempt to use LuaState Abstract class, please create a child of LuaState")); + return nullptr; + } + + + ULuaState* NewLuaState = NewObject((UObject*)GetTransientPackage(), LuaStateClass); + if (!NewLuaState) + { + return nullptr; + } + + return NewLuaState->GetLuaState(WorldContextObject->GetWorld()); +} + +``` + +`Source/LuaMachine/Private/LuaBlueprintPackage.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + + +#include "LuaBlueprintPackage.h" +#include "LuaState.h" + +TSubclassOf ULuaBlueprintPackage::GetLuaState() const +{ + ULuaState* LuaState = Cast(GetOuter()); + if (LuaState) + { + return LuaState->GetClass(); + } + return nullptr; +} + +ULuaState* ULuaBlueprintPackage::GetLuaStateInstance() const +{ + return Cast(GetOuter()); +} + +UWorld* ULuaBlueprintPackage::GetWorld() const +{ + ULuaState* LuaState = Cast(GetOuter()); + if (LuaState) + { + return LuaState->GetWorld(); + } + return nullptr; +} + +void ULuaBlueprintPackage::Init() +{ + +} +``` + +`Source/LuaMachine/Private/LuaCode.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris +// Reimport system by yama2akira (Akira Yamamoto) + +#include "LuaCode.h" +#include "LuaMachine.h" +#include "Serialization/CustomVersion.h" +#include "EditorFramework/AssetImportData.h" + +const FGuid FLuaCodeObjectVersion::GUID(0x01C2E96A1, 0xE24436EA, 0x6C69B025, 0x14E7FC3); +FCustomVersionRegistration GRegisterLuaCodeCustomVersion(FLuaCodeObjectVersion::GUID, FLuaCodeObjectVersion::LatestVersion, TEXT("LuaCodeVer")); + +ULuaCode::ULuaCode() +{ + bCookAsBytecode = true; + bCooked = false; +} + +void ULuaCode::Serialize(FArchive& Ar) +{ + bool bSkipOriginalCode = false; + if (Ar.IsCooking() && !Ar.IsLoading()) + { + if (bCookAsBytecode && !Code.IsEmpty()) + { + bCooked = true; + FString ErrorString; + ByteCode = ULuaState::ToByteCode(Code.ToString(), GetPathName(), ErrorString); + if (!ErrorString.IsEmpty()) + { + UE_LOG(LogLuaMachine, Error, TEXT("Unable to generate bytecode: %s"), *ErrorString); + } + bSkipOriginalCode = true; + } + } + else if (Ar.IsSaving()) + { + bCooked = false; + TArray EmptyData; + ByteCode = EmptyData; + } + + FText OriginalCode; + + if (bSkipOriginalCode) + { + OriginalCode = Code; + Code = FText::GetEmpty(); + } + + + Super::Serialize(Ar); + Ar.UsingCustomVersion(FLuaCodeObjectVersion::GUID); + + if (Ar.CustomVer(FLuaCodeObjectVersion::GUID) < FLuaCodeObjectVersion::FixDuplicationOfProperties) + { + Ar << bCooked; + Ar << Code; + Ar << bCookAsBytecode; + Ar << ByteCode; + } + + if (bSkipOriginalCode) + { + Code = OriginalCode; + } +} + +#if ENGINE_MAJOR_VERSION > 4 +void ULuaCode::PreSave(FObjectPreSaveContext ObjectSaveContext) +{ + Super::PreSave(ObjectSaveContext); +#else +void ULuaCode::PreSave(const ITargetPlatform * TargetPlatform) +{ + Super::PreSave(TargetPlatform); +#endif + + for (ULuaState* LuaState : FLuaMachineModule::Get().GetRegisteredLuaStates()) + { + if (LuaState->LuaCodeAsset == this) + { + FLuaMachineModule::Get().UnregisterLuaState(LuaState); + } + } +} + +#if WITH_EDITORONLY_DATA +void ULuaCode::PostInitProperties() +{ + if (!HasAnyFlags(RF_ClassDefaultObject)) + { + AssetImportData = NewObject(this, TEXT("AssetImportData")); + } + Super::PostInitProperties(); +} + +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 4 +void ULuaCode::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const +{ + if (AssetImportData) + { + Context.AddTag(FAssetRegistryTag(SourceFileTagName(), AssetImportData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden)); + } + Super::GetAssetRegistryTags(Context); +} +#else +void ULuaCode::GetAssetRegistryTags(TArray& OutTags) const +{ + if (AssetImportData) + { + OutTags.Add(FAssetRegistryTag(SourceFileTagName(), AssetImportData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden)); + } + Super::GetAssetRegistryTags(OutTags); +} +#endif + +#endif + +``` + +`Source/LuaMachine/Private/LuaCommandExecutor.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + + +#include "LuaCommandExecutor.h" +#include "LuaBlueprintFunctionLibrary.h" + +FLuaCommandExecutor::FLuaCommandExecutor() +{ + LuaState = nullptr; +} + +FLuaCommandExecutor::~FLuaCommandExecutor() +{ +} + +FName FLuaCommandExecutor::GetName() const +{ + return FName(*FString::Printf(TEXT("LuaMachine:%s"), *LuaState->GetName())); +} + +FText FLuaCommandExecutor::GetDisplayName() const +{ + return FText::FromName(GetName()); +} + +FText FLuaCommandExecutor::GetDescription() const +{ + return GetDisplayName(); +} + +FText FLuaCommandExecutor::GetHintText() const +{ + return FText::GetEmpty(); +} + +void FLuaCommandExecutor::GetAutoCompleteSuggestions(const TCHAR* Input, TArray& Out) +{ +} + +void FLuaCommandExecutor::GetExecHistory(TArray& Out) +{ + IConsoleManager::Get().GetConsoleHistory(*(GetName().ToString()), Out); +} + +bool FLuaCommandExecutor::Exec(const TCHAR* Input) +{ + IConsoleManager::Get().AddConsoleHistoryEntry(*(GetName().ToString()), Input); + + LuaState->RunString(Input, ""); + + return true; +} + +bool FLuaCommandExecutor::AllowHotKeyClose() const +{ + return false; +} + +bool FLuaCommandExecutor::AllowMultiLine() const +{ + return true; +} + +FInputChord FLuaCommandExecutor::GetHotKey() const +{ + return FInputChord(); +} + + + +``` + +`Source/LuaMachine/Private/LuaComponent.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + +#include "LuaComponent.h" +#include "LuaMachine.h" +#include "LuaBlueprintFunctionLibrary.h" +#include "GameFramework/Actor.h" + + +// Sets default values for this component's properties +ULuaComponent::ULuaComponent() +{ + // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features + // off to improve performance if you don't need them. + PrimaryComponentTick.bCanEverTick = false; + + // ... + + bLazy = false; + bLogError = false; + bImplicitSelf = false; +} + +void ULuaComponent::OnRegister() +{ + Super::OnRegister(); + + if (GetWorld()->IsGameWorld()) + { + for (const FString& GlobalName : GlobalNames) + { + ULuaBlueprintFunctionLibrary::LuaSetGlobal(GetWorld(), LuaState, GlobalName, FLuaValue(this)); + } + } +} + +// Called when the game starts +void ULuaComponent::BeginPlay() +{ + Super::BeginPlay(); + + if (!bLazy) + FLuaMachineModule::Get().GetLuaState(LuaState, GetWorld()); + + // ... + +} + +// Called every frame +void ULuaComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + // ... +} + +ULuaState* ULuaComponent::LuaComponentGetState() +{ + return FLuaMachineModule::Get().GetLuaState(LuaState, GetWorld()); +} + +FLuaValue ULuaComponent::LuaGetField(const FString& Name) +{ + FLuaValue ReturnValue; + ULuaState* L = LuaComponentGetState(); + if (!L) + return ReturnValue; + + // push component pointer as userdata + L->NewUObject(this, nullptr); + L->SetupAndAssignUserDataMetatable(this, Metatable, nullptr); + + int32 ItemsToPop = L->GetFieldFromTree(Name, false); + ReturnValue = L->ToLuaValue(-1); + + // we need to remove the return value and the object + L->Pop(ItemsToPop + 1); + + return ReturnValue; +} + +void ULuaComponent::LuaSetField(const FString& Name, FLuaValue Value) +{ + ULuaState* L = LuaComponentGetState(); + if (!L) + return; + + // push component pointer as userdata + L->NewUObject(this, nullptr); + L->SetupAndAssignUserDataMetatable(this, Metatable, nullptr); + + L->SetFieldFromTree(Name, Value, false); + + // remove UObject + L->Pop(); + +} + +FLuaValue ULuaComponent::LuaCallFunction(const FString& Name, TArray Args, bool bGlobal) +{ + FLuaValue ReturnValue; + + ULuaState* L = LuaComponentGetState(); + if (!L) + return ReturnValue; + + // push component pointer as userdata + L->NewUObject(this, nullptr); + L->SetupAndAssignUserDataMetatable(this, Metatable, nullptr); + + int32 ItemsToPop = L->GetFieldFromTree(Name, bGlobal); + + // first argument (self/actor) + L->PushValue(-(ItemsToPop + 1)); + int NArgs = 1; + for (FLuaValue& Arg : Args) + { + L->FromLuaValue(Arg); + NArgs++; + } + + if (!L->PCall(NArgs, ReturnValue)) + { + if (L->InceptionLevel == 0) + { + if (bLogError) + L->LogError(L->LastError); + OnLuaError.Broadcast(L->LastError); + } + } + + // the return value and the function has been removed, so we do not need to change ItemsToPop + L->Pop(ItemsToPop + 1); + + return ReturnValue; +} + +TArray ULuaComponent::LuaCallFunctionMulti(FString Name, TArray Args, bool bGlobal) +{ + TArray ReturnValue; + + ULuaState* L = LuaComponentGetState(); + if (!L) + return ReturnValue; + + // push component pointer as userdata + L->NewUObject(this, nullptr); + L->SetupAndAssignUserDataMetatable(this, Metatable, nullptr); + + int32 ItemsToPop = L->GetFieldFromTree(Name, bGlobal); + int32 StackTop = L->GetTop(); + + // first argument (self/actor) + L->PushValue(-(ItemsToPop + 1)); + int NArgs = 1; + for (FLuaValue& Arg : Args) + { + L->FromLuaValue(Arg); + NArgs++; + } + + FLuaValue LastReturnValue; + if (!L->PCall(NArgs, LastReturnValue, LUA_MULTRET)) + { + if (L->InceptionLevel == 0) + { + if (bLogError) + L->LogError(L->LastError); + OnLuaError.Broadcast(L->LastError); + } + } + else + { + int32 NumOfReturnValues = (L->GetTop() - StackTop) + 1; + if (NumOfReturnValues > 0) + { + for (int32 i = -1; i >= -(NumOfReturnValues); i--) + { + ReturnValue.Insert(L->ToLuaValue(i), 0); + } + L->Pop(NumOfReturnValues - 1); + } + } + + // the return value and the function has been removed, so we do not need to change ItemsToPop + L->Pop(ItemsToPop + 1); + + return ReturnValue; +} + +FLuaValue ULuaComponent::LuaCallValue(FLuaValue Value, TArray Args) +{ + FLuaValue ReturnValue; + + ULuaState* L = Value.LuaState.Get(); + if (!L) + return ReturnValue; + + // push function + L->FromLuaValue(Value); + // push component pointer as userdata + L->NewUObject(this, nullptr); + L->SetupAndAssignUserDataMetatable(this, Metatable, nullptr); + + int NArgs = 1; + for (FLuaValue& Arg : Args) + { + L->FromLuaValue(Arg); + NArgs++; + } + + if (!L->PCall(NArgs, ReturnValue)) + { + if (L->InceptionLevel == 0) + { + if (bLogError) + L->LogError(L->LastError); + OnLuaError.Broadcast(L->LastError); + } + } + + L->Pop(); + + return ReturnValue; +} + +FLuaValue ULuaComponent::LuaCallValueIfNotNil(FLuaValue Value, TArray Args) +{ + FLuaValue ReturnValue; + if (Value.Type != ELuaValueType::Nil) + ReturnValue = LuaCallValue(Value, Args); + + return ReturnValue; +} + +FLuaValue ULuaComponent::LuaCallTableKey(FLuaValue InTable, FString Key, TArray Args) +{ + FLuaValue ReturnValue; + + if (InTable.Type != ELuaValueType::Table) + return ReturnValue; + + ULuaState* L = InTable.LuaState.Get(); + if (!L) + return ReturnValue; + + FLuaValue Value = InTable.GetField(Key); + if (Value.Type == ELuaValueType::Nil) + return ReturnValue; + + return LuaCallValue(Value, Args); +} + +FLuaValue ULuaComponent::LuaCallTableIndex(FLuaValue InTable, int32 Index, TArray Args) +{ + FLuaValue ReturnValue; + + if (InTable.Type != ELuaValueType::Table) + return ReturnValue; + + ULuaState* L = InTable.LuaState.Get(); + if (!L) + return ReturnValue; + + FLuaValue Value = InTable.GetFieldByIndex(Index); + if (Value.Type == ELuaValueType::Nil) + return ReturnValue; + + return LuaCallValue(Value, Args); +} + +TArray ULuaComponent::LuaCallValueMulti(FLuaValue Value, TArray Args) +{ + TArray ReturnValue; + + ULuaState* L = Value.LuaState.Get(); + if (!L) + return ReturnValue; + + // push function + L->FromLuaValue(Value); + int32 StackTop = L->GetTop(); + + // push component pointer as userdata + L->NewUObject(this, nullptr); + L->SetupAndAssignUserDataMetatable(this, Metatable, nullptr); + + int NArgs = 1; + for (FLuaValue& Arg : Args) + { + L->FromLuaValue(Arg); + NArgs++; + } + + FLuaValue LastReturnValue; + if (!L->PCall(NArgs, LastReturnValue, LUA_MULTRET)) + { + if (L->InceptionLevel == 0) + { + if (bLogError) + L->LogError(L->LastError); + OnLuaError.Broadcast(L->LastError); + } + } + else + { + int32 NumOfReturnValues = (L->GetTop() - StackTop) + 1; + if (NumOfReturnValues > 0) + { + for (int32 i = -1; i >= -(NumOfReturnValues); i--) + { + ReturnValue.Insert(L->ToLuaValue(i), 0); + } + L->Pop(NumOfReturnValues - 1); + } + } + + L->Pop(); + + return ReturnValue; +} + +TArray ULuaComponent::LuaCallValueMultiIfNotNil(FLuaValue Value, TArray Args) +{ + TArray ReturnValue; + if (Value.Type != ELuaValueType::Nil) + ReturnValue = LuaCallValueMulti(Value, Args); + + return ReturnValue; +} + +TArray ULuaComponent::LuaCallTableKeyMulti(FLuaValue InTable, FString Key, TArray Args) +{ + TArray ReturnValue; + + if (InTable.Type != ELuaValueType::Table) + return ReturnValue; + + ULuaState* L = InTable.LuaState.Get(); + if (!L) + return ReturnValue; + + FLuaValue Value = InTable.GetField(Key); + if (Value.Type == ELuaValueType::Nil) + return ReturnValue; + + return LuaCallValueMulti(Value, Args); +} + +TArray ULuaComponent::LuaCallTableIndexMulti(FLuaValue InTable, int32 Index, TArray Args) +{ + TArray ReturnValue; + + if (InTable.Type != ELuaValueType::Table) + return ReturnValue; + + ULuaState* L = InTable.LuaState.Get(); + if (!L) + return ReturnValue; + + FLuaValue Value = InTable.GetFieldByIndex(Index); + if (Value.Type == ELuaValueType::Nil) + return ReturnValue; + + return LuaCallValueMulti(Value, Args); +} + +FLuaValue ULuaComponent::ReceiveLuaMetaIndex_Implementation(FLuaValue Key) +{ + return FLuaValue(); +} + +bool ULuaComponent::ReceiveLuaMetaNewIndex_Implementation(FLuaValue Key, FLuaValue Value) +{ + return false; +} +``` + +`Source/LuaMachine/Private/LuaDelegate.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + + +#include "LuaDelegate.h" +#include "LuaBlueprintFunctionLibrary.h" + +void ULuaDelegate::LuaDelegateFunction() +{ +} + +void ULuaDelegate::SetupLuaDelegate(UFunction* InSignature, ULuaState* InLuaState, FLuaValue InLuaValue) +{ + LuaDelegateSignature = InSignature; + LuaState = InLuaState; + LuaValue = InLuaValue; +} + +void ULuaDelegate::ProcessEvent(UFunction* Function, void* Parms) +{ + if (!LuaState.IsValid()) + { + return; + } + + TArray LuaArgs; +#if ENGINE_MAJOR_VERSION > 4 ||ENGINE_MINOR_VERSION >= 25 + for (TFieldIterator It(LuaDelegateSignature); (It && (It->PropertyFlags & (CPF_Parm | CPF_ReturnParm)) == CPF_Parm); ++It) + { + FProperty* Prop = *It; +#else + for (TFieldIterator It(LuaDelegateSignature); (It && (It->PropertyFlags & (CPF_Parm | CPF_ReturnParm)) == CPF_Parm); ++It) + { + UProperty* Prop = *It; +#endif + bool bPropSuccess = false; + LuaArgs.Add(LuaState->FromProperty(Parms, Prop, bPropSuccess, 0)); + } + + ULuaBlueprintFunctionLibrary::LuaGlobalCallValue(LuaState->GetWorld(), LuaState->GetClass(), LuaValue, LuaArgs); +} + +``` + +`Source/LuaMachine/Private/LuaGlobalNameComponent.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + +#include "LuaGlobalNameComponent.h" +#include "LuaBlueprintFunctionLibrary.h" + +// Sets default values for this component's properties +ULuaGlobalNameComponent::ULuaGlobalNameComponent() +{ + // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features + // off to improve performance if you don't need them. + PrimaryComponentTick.bCanEverTick = false; + + // ... +} + +void ULuaGlobalNameComponent::OnRegister() +{ + Super::OnRegister(); + + if (GetWorld()->IsGameWorld() && !LuaGlobalName.IsEmpty()) + { + ULuaBlueprintFunctionLibrary::LuaSetGlobal(GetWorld(), LuaState, LuaGlobalName, FLuaValue(GetOwner())); + } +} + +// Called when the game starts +void ULuaGlobalNameComponent::BeginPlay() +{ + Super::BeginPlay(); + + // ... +} + + +// Called every frame +void ULuaGlobalNameComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + // ... +} + + +``` + +`Source/LuaMachine/Private/LuaMachine.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + +#include "LuaMachine.h" +#include "LuaBlueprintFunctionLibrary.h" +#if WITH_EDITOR +#include "Editor/UnrealEd/Public/Editor.h" +#include "Editor/PropertyEditor/Public/PropertyEditorModule.h" +#include "Runtime/Projects/Public/Interfaces/IPluginManager.h" +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 +#include "Styling/SlateStyleRegistry.h" +#else +#include "SlateCore/Public/Styling/SlateStyleRegistry.h" +#endif +#endif + +#define LOCTEXT_NAMESPACE "FLuaMachineModule" + +void FLuaMachineModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +#if WITH_EDITOR + FEditorDelegates::BeginPIE.AddRaw(this, &FLuaMachineModule::CleanupLuaStates); + FEditorDelegates::EndPIE.AddRaw(this, &FLuaMachineModule::CleanupLuaStates); +#endif + + // streaming level hooks + FWorldDelegates::LevelAddedToWorld.AddRaw(this, &FLuaMachineModule::LuaLevelAddedToWorld); + FWorldDelegates::LevelRemovedFromWorld.AddRaw(this, &FLuaMachineModule::LuaLevelRemovedFromWorld); + +} + +void FLuaMachineModule::LuaLevelAddedToWorld(ULevel* Level, UWorld* World) +{ + for (ULuaState* LuaState : GetRegisteredLuaStates()) + { +#if ENGINE_MAJOR_VERSION > 4 + if (LuaState->IsValidLowLevel() && !IsValid(LuaState)) +#else + if (LuaState->IsValidLowLevel() && !LuaState->IsPendingKill()) +#endif + { + LuaState->ReceiveLuaLevelAddedToWorld(Level, World); + } + } +} + +void FLuaMachineModule::LuaLevelRemovedFromWorld(ULevel* Level, UWorld* World) +{ + for (ULuaState* LuaState : GetRegisteredLuaStates()) + { +#if ENGINE_MAJOR_VERSION > 4 + if (LuaState->IsValidLowLevel() && !IsValid(LuaState)) +#else + if (LuaState->IsValidLowLevel() && !LuaState->IsPendingKill()) +#endif + { + LuaState->ReceiveLuaLevelRemovedFromWorld(Level, World); + } + } +} + +void FLuaMachineModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +void FLuaMachineModule::AddReferencedObjects(FReferenceCollector& Collector) +{ + Collector.AddReferencedObjects(LuaStates); +} + +void FLuaMachineModule::CleanupLuaStates(bool bIsSimulating) +{ + TArray> LuaStatesKeys; + LuaStates.GetKeys(LuaStatesKeys); + +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 4 + TMap, TObjectPtr> PersistentLuaStates; +#else + TMap, ULuaState*> PersistentLuaStates; +#endif + + for (TSubclassOf LuaStateClass : LuaStatesKeys) + { + if (LuaStates[LuaStateClass]->bPersistent) + { + PersistentLuaStates.Add(LuaStateClass, LuaStates[LuaStateClass]); + } + else + { + if (FLuaCommandExecutor* LuaConsole = LuaStates[LuaStateClass]->GetLuaConsole()) + { + IModularFeatures::Get().UnregisterModularFeature(IConsoleCommandExecutor::ModularFeatureName(), LuaConsole); + } + } + } + + LuaStates = PersistentLuaStates; + OnRegisteredLuaStatesChanged.Broadcast(); +} + +ULuaState* FLuaMachineModule::GetLuaState(TSubclassOf LuaStateClass, UWorld* InWorld, bool bCheckOnly) +{ + if (!LuaStateClass) + { + return nullptr; + } + + if (LuaStateClass == ULuaState::StaticClass()) + { + UE_LOG(LogLuaMachine, Error, TEXT("attempt to use LuaState Abstract class, please create a child of LuaState")); + return nullptr; + } + + if (!LuaStates.Contains(LuaStateClass)) + { + if (bCheckOnly) + { + return nullptr; + } + ULuaState* NewLuaState = NewObject((UObject*)GetTransientPackage(), LuaStateClass); + LuaStates.Add(LuaStateClass, NewLuaState); + OnNewLuaState.Broadcast(NewLuaState); + OnRegisteredLuaStatesChanged.Broadcast(); + } + + return LuaStates[LuaStateClass]->GetLuaState(InWorld); +} + +TArray FLuaMachineModule::GetRegisteredLuaStates() +{ + TArray RegisteredStates; +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 4 + for (TPair< TSubclassOf, TObjectPtr>& Pair : LuaStates) +#else + for (TPair< TSubclassOf, ULuaState*>& Pair : LuaStates) +#endif + { + RegisteredStates.Add(Pair.Value); + } + + return RegisteredStates; +} + +void FLuaMachineModule::UnregisterLuaState(ULuaState* LuaState) +{ + TSubclassOf FoundLuaStateClass = nullptr; +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 4 + for (TPair< TSubclassOf, TObjectPtr>& Pair : LuaStates) +#else + for (TPair< TSubclassOf, ULuaState*>& Pair : LuaStates) +#endif + { + if (Pair.Value == LuaState) + { + FoundLuaStateClass = Pair.Key; + break; + } + } + + if (FoundLuaStateClass) + { + LuaStates.Remove(FoundLuaStateClass); + } + + // trick for waking up on low-level destructor + OnRegisteredLuaStatesChanged.Broadcast(); +} + +void FLuaMachineModule::RegisterLuaConsoleCommand(const FString& CommandName, const FLuaValue& LuaConsoleCommand) +{ + // first check in lua console commands + UnregisterLuaConsoleCommand(CommandName); + if (IConsoleManager::Get().FindConsoleObject(*CommandName)) + { + UE_LOG(LogLuaMachine, Error, TEXT("Lua Console Command Name \"%s\" is already in use."), *CommandName); + return; + } + + if (IConsoleManager::Get().RegisterConsoleCommand(*CommandName, *CommandName, FConsoleCommandWithArgsDelegate::CreateRaw(this, &FLuaMachineModule::RunLuaConsoleCommand, LuaConsoleCommand))) + { + LuaConsoleCommands.Add(CommandName); + } +} + +void FLuaMachineModule::UnregisterLuaConsoleCommand(const FString& CommandName) +{ + if (LuaConsoleCommands.Contains(CommandName)) + { + IConsoleManager::Get().UnregisterConsoleObject(*CommandName); + LuaConsoleCommands.Remove(CommandName); + } +} + +void FLuaMachineModule::RunLuaConsoleCommand(const TArray& Args, FLuaValue LuaConsoleCommand) +{ + TArray LuaArgs; + for (FString Arg : Args) + { + LuaArgs.Add(FLuaValue(Arg)); + } + ULuaBlueprintFunctionLibrary::LuaValueCallIfNotNil(LuaConsoleCommand, LuaArgs); +} + +FLuaMachineModule& FLuaMachineModule::Get() +{ + static FLuaMachineModule* Singleton = nullptr; + if (Singleton == nullptr) + { + Singleton = &FModuleManager::LoadModuleChecked("LuaMachine"); + } + return *Singleton; +} + +bool FLuaMachineModule::Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar) +{ + if (FParse::Command(&Cmd, TEXT("luaspawn"))) + { + if (!*Cmd) + { + UE_LOG(LogLuaMachine, Error, TEXT("please specify a valid LuaState path.")); + return false; + } + + UObject* RequestedObject = LoadObject(nullptr, Cmd); + if (RequestedObject) + { + bool bLoaded = false; + UClass* ObjectAsClass = Cast(RequestedObject); + if (ObjectAsClass && ObjectAsClass->IsChildOf()) + { + bLoaded = true; + } + + if (!bLoaded) + { + UBlueprint* ObjectAsBlueprint = Cast(RequestedObject); + if (ObjectAsBlueprint) + { + ObjectAsClass = ObjectAsBlueprint->GeneratedClass; + if (ObjectAsClass && ObjectAsClass->IsChildOf()) + { + bLoaded = true; + } + } + } + + if (bLoaded) + { + ULuaState* SpawnedState = FLuaMachineModule::Get().GetLuaState(ObjectAsClass, InWorld, false); + if (SpawnedState) + { + UE_LOG(LogLuaMachine, Log, TEXT("%s spawned."), *SpawnedState->GetName()); + } + return true; + } + + UE_LOG(LogLuaMachine, Error, TEXT("specified argument is not a valid LuaState path.")); + } + } + + return false; +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FLuaMachineModule, LuaMachine) +``` + +`Source/LuaMachine/Private/LuaMultiLineEditableTextBox.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + +#include "LuaMultiLineEditableTextBox.h" +#include "Runtime/Slate/Public/Widgets/Input/SMultiLineEditableTextBox.h" +#include "Runtime/Slate/Public/Widgets/Input/SEditableTextBox.h" +#include "LuaSyntaxHighlighter.h" + +#define LOCTEXT_NAMESPACE "UMG" + +FLuaCustomHighlighter::FLuaCustomHighlighter() +{ + Color = FLinearColor::White; +} + +ULuaMultiLineEditableTextBox::ULuaMultiLineEditableTextBox() +{ +#if ENGINE_MAJOR_VERSION >=5 && ENGINE_MINOR_VERSION >= 2 + EditableTextBoxStyle.SetTextStyle(CodeStyle); +#endif + + SEditableTextBox::FArguments Defaults; + WidgetStyle = *Defaults._Style; + WidgetStyle.BackgroundColor = FSlateColor(FLinearColor::Black); + WidgetStyle.ForegroundColor = FSlateColor(FLinearColor::White); + + TabSize = 2; + + bIsReadonly = false; + bHandleTab = true; + bHandleArrows = true; + + CodeStyle = FTextBlockStyle() +#if ENGINE_MAJOR_VERSION > 4 && ENGINE_MINOR_VERSION > 0 + .SetFont(WidgetStyle.TextStyle.Font) +#else + .SetFont(WidgetStyle.Font) +#endif + .SetColorAndOpacity(FLinearColor::White) + .SetShadowOffset(FVector2D::ZeroVector) + .SetSelectedBackgroundColor(FSlateColor(FLinearColor::Blue)) + .SetShadowColorAndOpacity(FLinearColor::Black); + + CommentColor = FLinearColor::Green; + StringColor = FLinearColor(FColor::Orange); + KeywordColor = FLinearColor(FColor::Cyan); + NilColor = FLinearColor::Red; + StdLibColor = FLinearColor::Yellow; + BasicColor = FLinearColor(FColor::Magenta); + +} + +FText ULuaMultiLineEditableTextBox::GetText() const +{ + if (EditableTextBoxPtr.IsValid()) + { + return EditableTextBoxPtr->GetText(); + } + return FText::GetEmpty(); +} + +FText ULuaMultiLineEditableTextBox::GetSelectedText() const +{ + if (EditableTextBoxPtr.IsValid()) + { + return EditableTextBoxPtr->GetSelectedText(); + } + return FText::GetEmpty(); +} + +void ULuaMultiLineEditableTextBox::SetText(FText InText) +{ + if (EditableTextBoxPtr.IsValid()) + { + EditableTextBoxPtr->SetText(InText); + } +} + +FReply ULuaMultiLineEditableTextBox::OnKeyChar(const FGeometry& InGeometry, const FCharacterEvent& InCharacterEvent) +{ + const TCHAR Character = InCharacterEvent.GetCharacter(); + if (Character == TEXT('\t')) + { + FString Spaces; + // using Max here for avoiding negative values + for (int32 i = 0; i < FMath::Max(TabSize, 0); i++) + Spaces = Spaces.AppendChar(TEXT(" ")[0]); + EditableTextBoxPtr->ClearSelection(); + EditableTextBoxPtr->InsertTextAtCursor(Spaces); + return FReply::Handled(); + } + return EditableTextBoxPtr->SMultiLineEditableTextBox::OnKeyChar(InGeometry, InCharacterEvent); +} + +FReply ULuaMultiLineEditableTextBox::OnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) +{ + FKey Key = InKeyEvent.GetKey(); + if (bHandleTab && Key == EKeys::Tab) + { + return FReply::Handled(); + } + + if (bHandleArrows) + { + if (Key == EKeys::Up) + { + MoveCursorUp(); + return FReply::Handled(); + } + + if (Key == EKeys::Down) + { + MoveCursorDown(); + return FReply::Handled(); + } + + if (Key == EKeys::Right) + { + MoveCursorRight(); + return FReply::Handled(); + } + + if (Key == EKeys::Left) + { + MoveCursorLeft(); + return FReply::Handled(); + } + } + + return EditableTextBoxPtr->SMultiLineEditableTextBox::OnKeyDown(InGeometry, InKeyEvent); +} + +void ULuaMultiLineEditableTextBox::OnCursorMoved(const FTextLocation& Location) +{ + CursorLocation = Location; +} + +int32 ULuaMultiLineEditableTextBox::GetCursorLine() const +{ + return CursorLocation.GetLineIndex(); +} + +int32 ULuaMultiLineEditableTextBox::GetCursorColumn() const +{ + return CursorLocation.GetOffset(); +} + +void ULuaMultiLineEditableTextBox::CursorGoTo(int32 Line, int32 Column) +{ + if (Line < 0) + Line = 0; + if (Column < 0) + Column = 0; + return EditableTextBoxPtr->GoTo(FTextLocation(Line, Column)); +} + +void ULuaMultiLineEditableTextBox::MoveCursorUp() +{ + return CursorGoTo(CursorLocation.GetLineIndex() - 1, 0); +} + +void ULuaMultiLineEditableTextBox::MoveCursorDown() +{ + return CursorGoTo(CursorLocation.GetLineIndex() + 1, 0); +} + +void ULuaMultiLineEditableTextBox::MoveCursorRight() +{ + return CursorGoTo(CursorLocation.GetLineIndex(), CursorLocation.GetOffset() + 1); +} + +void ULuaMultiLineEditableTextBox::MoveCursorLeft() +{ + return CursorGoTo(CursorLocation.GetLineIndex(), CursorLocation.GetOffset() - 1); +} + +void ULuaMultiLineEditableTextBox::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + + EditableTextBoxPtr->SetStyle(&WidgetStyle); + + Super::SynchronizeTextLayoutProperties(*EditableTextBoxPtr); +} + +#if WITH_EDITOR +const FText ULuaMultiLineEditableTextBox::GetPaletteCategory() +{ + return LOCTEXT("Lua", "Lua"); +} +#endif + +void ULuaMultiLineEditableTextBox::ReleaseSlateResources(bool bReleaseChildren) +{ + Super::ReleaseSlateResources(bReleaseChildren); + + EditableTextBoxPtr.Reset(); +} + +TSharedRef ULuaMultiLineEditableTextBox::RebuildWidget() +{ + FLuaSyntaxTextStyle Style; + Style.NormalTextStyle = CodeStyle; + Style.CommentTextStyle = FTextBlockStyle(CodeStyle).SetColorAndOpacity(CommentColor); + Style.KeywordTextStyle = FTextBlockStyle(CodeStyle).SetColorAndOpacity(KeywordColor); + Style.NilTextStyle = FTextBlockStyle(CodeStyle).SetColorAndOpacity(NilColor); + Style.BasicTextStyle = FTextBlockStyle(CodeStyle).SetColorAndOpacity(BasicColor); + Style.StdLibTextStyle = FTextBlockStyle(CodeStyle).SetColorAndOpacity(StdLibColor); + Style.StringTextStyle = FTextBlockStyle(CodeStyle).SetColorAndOpacity(StringColor); + for (FLuaCustomHighlighter& Highlighter : CustomTokensMapping) + { + for (FString& Token : Highlighter.Tokens) + { + Style.CustomTextColorMapping.Add(Token, Highlighter.Color); + } + } + + EditableTextBoxPtr = SNew(SMultiLineEditableTextBox) + .Marshaller(FLuaMachineSyntaxHighlighterTextLayoutMarshaller::Create(Style)) +#if ENGINE_MAJOR_VERSION >=5 && ENGINE_MINOR_VERSION >= 1 + .Style(&EditableTextBoxStyle) +#else + .TextStyle(&CodeStyle) +#endif + .OnKeyCharHandler_UObject(this, &ULuaMultiLineEditableTextBox::OnKeyChar) + .OnKeyDownHandler_UObject(this, &ULuaMultiLineEditableTextBox::OnKeyDown) + .IsReadOnly(bIsReadonly) + .AllowContextMenu(false) + .OnCursorMoved_UObject(this, &ULuaMultiLineEditableTextBox::OnCursorMoved) + .Style(&WidgetStyle); + + return EditableTextBoxPtr.ToSharedRef(); +} + +#undef LOCTEXT_NAMESPACE +``` + +`Source/LuaMachine/Private/LuaState.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + +#include "LuaState.h" +#include "LuaComponent.h" +#include "LuaUserDataObject.h" +#include "LuaMachine.h" +#include "LuaBlueprintPackage.h" +#include "LuaBlueprintFunctionLibrary.h" +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 0 +#include "AssetRegistry/AssetRegistryModule.h" +#else +#include "AssetRegistryModule.h" +#endif +#include "GameFramework/Actor.h" +#include "Runtime/Core/Public/Misc/FileHelper.h" +#include "Runtime/Core/Public/Misc/Paths.h" +#include "Runtime/Core/Public/Serialization/BufferArchive.h" +#include "Runtime/CoreUObject/Public/UObject/TextProperty.h" + +LUAMACHINE_API DEFINE_LOG_CATEGORY(LogLuaMachine); + +ULuaState::ULuaState() +{ + L = nullptr; + bLuaOpenLibs = true; + bDisabled = false; + bLogError = true; + bAddProjectContentDirToPackagePath = true; + bPersistent = false; + bEnableLineHook = false; + bEnableCallHook = false; + bEnableReturnHook = false; + bEnableCountHook = false; + bRawLuaFunctionCall = false; + + FCoreUObjectDelegates::GetPostGarbageCollect().AddUObject(this, &ULuaState::GCLuaDelegatesCheck); +} + +ULuaState* ULuaState::GetLuaState(UWorld* InWorld) +{ + CurrentWorld = InWorld; + + if (L != nullptr) + { + return this; + } + + if (bDisabled) + { + return nullptr; + } + + L = luaL_newstate(); + + if (bLuaOpenLibs) + { + luaL_openlibs(L); + } + + // load "package" for allowing minimal setup + luaL_requiref(L, "package", luaopen_package, 1); + lua_pop(L, 1); + + if (!bLuaOpenLibs) + { + if (LuaLibsLoader.bLoadBase) + { + luaL_requiref(L, "_G", luaopen_base, 1); + lua_pop(L, 1); + } + + if (LuaLibsLoader.bLoadCoroutine) + { + luaL_requiref(L, "coroutine", luaopen_coroutine, 1); + lua_pop(L, 1); + } + + if (LuaLibsLoader.bLoadTable) + { + luaL_requiref(L, "table", luaopen_table, 1); + lua_pop(L, 1); + } + + if (LuaLibsLoader.bLoadIO) + { + luaL_requiref(L, "io", luaopen_io, 1); + lua_pop(L, 1); + } + + if (LuaLibsLoader.bLoadOS) + { + luaL_requiref(L, "os", luaopen_os, 1); + lua_pop(L, 1); + } + + if (LuaLibsLoader.bLoadString) + { + luaL_requiref(L, "string", luaopen_string, 1); + lua_pop(L, 1); + } + + if (LuaLibsLoader.bLoadMath) + { + luaL_requiref(L, "math", luaopen_math, 1); + lua_pop(L, 1); + } + + if (LuaLibsLoader.bLoadUTF8) + { + luaL_requiref(L, "utf8", luaopen_utf8, 1); + lua_pop(L, 1); + } + + if (LuaLibsLoader.bLoadDebug) + { + luaL_requiref(L, "debug", luaopen_debug, 1); + lua_pop(L, 1); + } + } + + ULuaState** LuaExtraSpacePtr = (ULuaState**)lua_getextraspace(L); + *LuaExtraSpacePtr = this; + // get the global table + lua_pushglobaltable(L); + // override print + PushCFunction(ULuaState::TableFunction_print); + SetField(-2, "print"); + + GetField(-1, "package"); + if (!OverridePackagePath.IsEmpty()) + { + OverridePackagePath.ReplaceInline(*FString("$(CONTENT_DIR)"), *FPaths::ProjectContentDir()); + lua_pushstring(L, TCHAR_TO_ANSI(*OverridePackagePath)); + SetField(-2, "path"); + } + + if (bAddProjectContentDirToPackagePath) + { + GetField(-1, "path"); + const char* CurrentLuaPath = lua_tostring(L, -1); + FString NewPackagePath = FString(CurrentLuaPath) + ";" + FPaths::ProjectContentDir() + "/?.lua"; + Pop(); + lua_pushstring(L, TCHAR_TO_ANSI(*NewPackagePath)); + SetField(-2, "path"); + } + + for (FString SubDir : AppendProjectContentDirSubDir) + { + GetField(-1, "path"); + const char* CurrentLuaPath = lua_tostring(L, -1); + FString NewPackagePath = FString(CurrentLuaPath) + ";" + FPaths::ProjectContentDir() / SubDir + "/?.lua"; + Pop(); + lua_pushstring(L, TCHAR_TO_ANSI(*NewPackagePath)); + SetField(-2, "path"); + } + + if (!OverridePackageCPath.IsEmpty()) + { + OverridePackageCPath.ReplaceInline(*FString("$(CONTENT_DIR)"), *FPaths::ProjectContentDir()); + + static const FString libExtension = +#if PLATFORM_MAC || PLATFORM_IOS + FString("dylib"); +#elif PLATFORM_LINUX || PLATFORM_ANDROID + FString("so"); +#elif PLATFORM_WINDOWS + FString("dll"); +#else + FString(""); +#endif + + OverridePackageCPath.ReplaceInline(*FString("$(LIB_EXT)"), *libExtension); + + lua_pushstring(L, TCHAR_TO_ANSI(*OverridePackageCPath)); + SetField(-2, "cpath"); + } + // manage RequireTable + GetField(-1, "preload"); + for (TPair& Pair : RequireTable) + { + PushCFunction(ULuaState::TableFunction_package_preload); + SetField(-2, TCHAR_TO_ANSI(*Pair.Key)); + } + + // pop package.prelod + Pop(1); + + // manage searchers + GetField(-1, "searchers"); + PushCFunction(ULuaState::TableFunction_package_loader); + constexpr int PackageLoadersFirstAvailableIndex = 5; + lua_seti(L, -2, PackageLoadersFirstAvailableIndex); + + // pop package.searchers (and package) + Pop(2); + + + for (TPair& Pair : Table) + { + FromLuaValue(Pair.Value, this, L); + SetField(-2, TCHAR_TO_ANSI(*Pair.Key)); + } + + for (TPair>& Pair : LuaBlueprintPackagesTable) + { + if (Pair.Value) + { + NewTable(); + ULuaBlueprintPackage* LuaBlueprintPackage = NewObject(this, Pair.Value); + if (LuaBlueprintPackage) + { + for (auto LuaPair : LuaBlueprintPackage->Table) + { + FromLuaValue(LuaPair.Value, LuaBlueprintPackage); + SetField(-2, TCHAR_TO_ANSI(*LuaPair.Key)); + } + // this avoid the package to be GC'd + LuaBlueprintPackages.Add(Pair.Key, LuaBlueprintPackage); + LuaBlueprintPackage->SelfTable = ToLuaValue(-1); + LuaBlueprintPackage->Init(); + LuaBlueprintPackage->ReceiveInit(); + } + } + else + { + PushNil(); + } + SetField(-2, TCHAR_TO_ANSI(*Pair.Key)); + } + + // pop global table + Pop(); + + // This allows subclasses to do any last minute initialization on lua state before + // we load code + ReceiveLuaStatePreInitialized(); + + int DebugMask = 0; + // install hooks + if (bEnableLineHook) + { + DebugMask |= LUA_MASKLINE; + } + if (bEnableCallHook) + { + DebugMask |= LUA_MASKCALL; + } + if (bEnableReturnHook) + { + DebugMask |= LUA_MASKRET; + } + if (bEnableCountHook) + { + DebugMask |= LUA_MASKCOUNT; + } + + if (DebugMask != 0) + { + lua_sethook(L, Debug_Hook, DebugMask, HookInstructionCount); + } + + if (LuaCodeAsset) + { + if (!RunCodeAsset(LuaCodeAsset)) + { + if (bLogError) + LogError(LastError); + ReceiveLuaError(LastError); + bDisabled = true; + return nullptr; + } + } + + if (!LuaFilename.IsEmpty()) + { + if (!RunFile(LuaFilename, true)) + { + if (bLogError) + LogError(LastError); + ReceiveLuaError(LastError); + bDisabled = true; + return nullptr; + } + } + + if (UserDataMetaTableFromCodeAsset) + { + if (!RunCodeAsset(UserDataMetaTableFromCodeAsset, 1)) + { + if (bLogError) + LogError(LastError); + ReceiveLuaError(LastError); + bDisabled = true; + return nullptr; + } + UserDataMetaTable = ToLuaValue(-1); + Pop(); + } + + LuaStateInit(); + ReceiveLuaStateInitialized(); + +#if WITH_EDITOR + if (!(GetFlags() & RF_ClassDefaultObject)) + { + LuaConsole.LuaState = this; + IModularFeatures::Get().RegisterModularFeature(IConsoleCommandExecutor::ModularFeatureName(), &LuaConsole); + } +#endif + + + return this; +} + +FLuaValue ULuaState::GetLuaBlueprintPackageTable(const FString& PackageName) +{ + if (!LuaBlueprintPackages.Contains(PackageName)) + { + return FLuaValue(); + } + + return LuaBlueprintPackages[PackageName]->SelfTable; +} + +bool ULuaState::RunCodeAsset(ULuaCode* CodeAsset, int NRet) +{ + + if (CodeAsset->bCooked && CodeAsset->bCookAsBytecode) + { +#if PLATFORM_ANDROID + // fix size_t of the bytecode + if (CodeAsset->ByteCode.Num() >= 14) + CodeAsset->ByteCode[13] = sizeof(size_t); +#endif + return RunCode(CodeAsset->ByteCode, CodeAsset->GetPathName(), NRet); + } + + return RunCode(CodeAsset->Code.ToString(), CodeAsset->GetPathName(), NRet); + +} + +bool ULuaState::RunFile(const FString& Filename, bool bIgnoreNonExistent, int NRet, bool bNonContentDirectory) +{ + TArray Code; + FString AbsoluteFilename = FPaths::Combine(FPaths::ProjectContentDir(), Filename); + + if (bNonContentDirectory) + { + AbsoluteFilename = Filename; + } + + if (!FPaths::FileExists(AbsoluteFilename)) + { + if (bIgnoreNonExistent) + return true; + LastError = FString::Printf(TEXT("Unable to open file %s"), *Filename); + FLuaValue LuaLastError = FLuaValue(LastError); + FromLuaValue(LuaLastError); + return false; + } + + if (FFileHelper::LoadFileToArray(Code, *AbsoluteFilename)) + { + if (RunCode(Code, AbsoluteFilename, NRet)) + { + return true; + } + return false; + } + + LastError = FString::Printf(TEXT("Unable to open file %s"), *Filename); + FLuaValue LuaLastError = FLuaValue(LastError); + FromLuaValue(LuaLastError); + return false; +} + +bool ULuaState::RunCode(const FString& Code, const FString& CodePath, int NRet) +{ + TArray Bytes; + Bytes.Append((uint8*)TCHAR_TO_UTF8(*Code), FCStringAnsi::Strlen(TCHAR_TO_UTF8(*Code))); + return RunCode(Bytes, CodePath, NRet); +} + +bool ULuaState::RunCode(const TArray& Code, const FString& CodePath, int NRet) +{ + FString FullCodePath = FString("@") + CodePath; + + if (luaL_loadbuffer(L, (const char*)Code.GetData(), Code.Num(), TCHAR_TO_ANSI(*FullCodePath))) + { + LastError = FString::Printf(TEXT("Lua loading error: %s"), ANSI_TO_TCHAR(lua_tostring(L, -1))); + return false; + } + else + { + + if (lua_pcall(L, 0, NRet, 0)) + { + LastError = FString::Printf(TEXT("Lua execution error: %s"), ANSI_TO_TCHAR(lua_tostring(L, -1))); + return false; + } + } + + return true; +} + +int ULuaState::ToByteCode_Writer(lua_State* L, const void* Ptr, size_t Size, void* UserData) +{ + TArray* Output = (TArray*)UserData; + Output->Append((uint8*)Ptr, Size); + return 0; +} + +TArray ULuaState::ToByteCode(const FString& Code, const FString& CodePath, FString& ErrorString) +{ + const TCHAR* CodeRaw = *Code; + FString FullCodePath = FString("@") + CodePath; + TArray Output; + + lua_State* L = luaL_newstate(); + if (luaL_loadbuffer(L, TCHAR_TO_UTF8(CodeRaw), FCStringAnsi::Strlen(TCHAR_TO_UTF8(CodeRaw)), TCHAR_TO_ANSI(*FullCodePath))) + { + ErrorString = ANSI_TO_TCHAR(lua_tostring(L, -1)); + Output.Empty(); + lua_close(L); + return Output; + } + + if (lua_dump(L, ULuaState::ToByteCode_Writer, &Output, 1)) + { + ErrorString = ANSI_TO_TCHAR(lua_tostring(L, -1)); + Output.Empty(); + lua_close(L); + return Output; + } + + lua_close(L); + return Output; +} + +void ULuaState::FromLuaValue(FLuaValue& LuaValue, UObject* CallContext, lua_State* State) +{ + if (!State) + { + State = this->L; + } + + switch (LuaValue.Type) + { + case ELuaValueType::Bool: + lua_pushboolean(State, LuaValue.Bool ? 1 : 0); + break; + case ELuaValueType::Integer: + lua_pushinteger(State, LuaValue.Integer); + break; + case ELuaValueType::Number: + lua_pushnumber(State, LuaValue.Number); + break; + case ELuaValueType::String: + { + TArray Bytes = LuaValue.ToBytes(); + lua_pushlstring(State, (const char*)Bytes.GetData(), Bytes.Num()); + } + break; + case ELuaValueType::Table: + if (LuaValue.LuaRef == LUA_NOREF) + { + lua_newtable(State); + lua_pushvalue(State, -1); + // hold references in the main state + LuaValue.LuaRef = luaL_ref(this->L, LUA_REGISTRYINDEX); + LuaValue.LuaState = this; + break; + } + if (this != LuaValue.LuaState) + { + lua_pushnil(State); + break; + } + lua_rawgeti(this->L, LUA_REGISTRYINDEX, LuaValue.LuaRef); + if (this->L != State) + lua_xmove(this->L, State, 1); + break; + case ELuaValueType::Thread: + if (LuaValue.LuaRef == LUA_NOREF) + { + lua_newthread(State); + lua_pushvalue(State, -1); + LuaValue.LuaRef = luaL_ref(this->L, LUA_REGISTRYINDEX); + LuaValue.LuaState = this; + break; + } + if (this != LuaValue.LuaState) + { + lua_pushnil(State); + break; + } + lua_rawgeti(this->L, LUA_REGISTRYINDEX, LuaValue.LuaRef); + if (this->L != State) + lua_xmove(this->L, State, 1); + break; + case ELuaValueType::Function: + if (this != LuaValue.LuaState || LuaValue.LuaRef == LUA_NOREF) + { + lua_pushnil(State); + break; + } + lua_rawgeti(this->L, LUA_REGISTRYINDEX, LuaValue.LuaRef); + if (this->L != State) + lua_xmove(this->L, State, 1); + break; + case ELuaValueType::UObject: + { + if (!LuaValue.Object) + { + lua_pushnil(State); + break; + } + + NewUObject(LuaValue.Object, State); + if (ULuaComponent* LuaComponent = Cast(LuaValue.Object)) + { + if (!LuaComponent->LuaState) + { + UE_LOG(LogLuaMachine, Warning, TEXT("%s has no associated LuaState"), *LuaComponent->GetFullName()); + } + // ensure we are in the same LuaState + else if (LuaComponent->LuaState == GetClass()) + { + SetupAndAssignUserDataMetatable(LuaComponent, LuaComponent->Metatable, State); + } + } + else if (ULuaUserDataObject* LuaUserDataObject = Cast(LuaValue.Object)) + { + if (!LuaUserDataObject->GetLuaState()) + { + UE_LOG(LogLuaMachine, Warning, TEXT("%s has no associated LuaState"), *LuaUserDataObject->GetFullName()); + } + // ensure we are in the same LuaState + else if (LuaUserDataObject->GetLuaState() == GetClass()) + { + SetupAndAssignUserDataMetatable(LuaUserDataObject, LuaUserDataObject->Metatable, State); + } + } + else + { + if (UserDataMetaTable.Type == ELuaValueType::Table) + { + FromLuaValue(UserDataMetaTable, nullptr, State); + } + else + { + lua_newtable(State); + // allow comparison between userdata/UObject/UFunction + lua_pushcfunction(State, ULuaState::MetaTableFunctionUserData__eq); + lua_setfield(State, -2, "__eq"); + } + lua_setmetatable(State, -2); + } + } + break; + case ELuaValueType::UFunction: + // if no context is assigned to the function, own it ! + if (!LuaValue.LuaState.IsValid()) + { + LuaValue.LuaState = this; + } + if (this != LuaValue.LuaState) + { + lua_pushnil(State); + break; + } + // first time we should have a CallContext, then we cache it in the Object field + if (!CallContext) + { + CallContext = LuaValue.Object; + } + if (CallContext) + { + UObject* FunctionOwner = CallContext; + if (ULuaComponent* LuaComponent = Cast(CallContext)) + { + FunctionOwner = LuaComponent->GetOwner(); + } + + if (FunctionOwner) + { + UFunction* Function = FunctionOwner->FindFunction(LuaValue.FunctionName); + if (Function) + { + // cache it for context-less calls + LuaValue.Object = CallContext; + FLuaUserData* LuaCallContext = (FLuaUserData*)lua_newuserdata(State, sizeof(FLuaUserData)); + LuaCallContext->Type = ELuaValueType::UFunction; + LuaCallContext->Context = CallContext; + LuaCallContext->Function = Function; + lua_newtable(State); + lua_pushcfunction(State, bRawLuaFunctionCall ? ULuaState::MetaTableFunction__rawcall : ULuaState::MetaTableFunction__call); + lua_setfield(State, -2, "__call"); + lua_setmetatable(State, -2); + return; + } + } + } + // no function found + lua_pushnil(State); + break; + case ELuaValueType::MulticastDelegate: + // if no context is assigned to the function, own it ! + if (!LuaValue.LuaState.IsValid()) + { + LuaValue.LuaState = this; + } + + if (this != LuaValue.LuaState) + { + lua_pushnil(State); + break; + } + { + FLuaUserData* LuaCallContext = (FLuaUserData*)lua_newuserdata(State, sizeof(FLuaUserData)); + LuaCallContext->Type = ELuaValueType::MulticastDelegate; + LuaCallContext->Function = reinterpret_cast(LuaValue.Object); + LuaCallContext->MulticastScriptDelegate = LuaValue.MulticastScriptDelegate; + lua_newtable(State); + lua_pushcfunction(State, bRawLuaFunctionCall ? ULuaState::MetaTableFunction__rawbroadcast : ULuaState::MetaTableFunction__rawbroadcast); + lua_setfield(State, -2, "__call"); + lua_setmetatable(State, -2); + return; + } + break; + default: + lua_pushnil(State); + } +} + +FLuaValue ULuaState::ToLuaValue(int Index, lua_State* State) +{ + if (!State) + { + State = this->L; + } + + FLuaValue LuaValue; + + if (lua_isboolean(State, Index)) + { + LuaValue = FLuaValue(lua_toboolean(State, Index) != 0); + } + else if (lua_type(State, Index) == LUA_TSTRING) + { + size_t StringLength = 0; + const char* String = lua_tolstring(State, Index, &StringLength); + LuaValue = FLuaValue(String, StringLength); + } + else if (lua_isinteger(State, Index)) + { + LuaValue = FLuaValue(lua_tointeger(State, Index)); + } + else if (lua_type(State, Index) == LUA_TNUMBER) + { + LuaValue = FLuaValue(lua_tonumber(State, Index)); + } + else if (lua_istable(State, Index)) + { + lua_pushvalue(State, Index); + if (State != this->L) + lua_xmove(State, this->L, 1); + LuaValue.Type = ELuaValueType::Table; + LuaValue.LuaState = this; + LuaValue.LuaRef = luaL_ref(this->L, LUA_REGISTRYINDEX); + } + else if (lua_isthread(State, Index)) + { + lua_pushvalue(State, Index); + if (State != this->L) + lua_xmove(State, this->L, 1); + LuaValue.Type = ELuaValueType::Thread; + LuaValue.LuaState = this; + LuaValue.LuaRef = luaL_ref(this->L, LUA_REGISTRYINDEX); + } + else if (lua_isfunction(State, Index)) + { + lua_pushvalue(State, Index); + if (State != this->L) + lua_xmove(State, this->L, 1); + LuaValue.Type = ELuaValueType::Function; + LuaValue.LuaState = this; + LuaValue.LuaRef = luaL_ref(this->L, LUA_REGISTRYINDEX); + } + else if (lua_isuserdata(State, Index)) + { + FLuaUserData* UserData = (FLuaUserData*)lua_touserdata(State, Index); + switch (UserData->Type) + { + case(ELuaValueType::UObject): + if (UserData->Context.IsValid()) + { + LuaValue.Type = UserData->Type; + LuaValue.Object = UserData->Context.Get(); + LuaValue.LuaState = this; + } + break; + case(ELuaValueType::UFunction): + if (UserData->Context.IsValid() && UserData->Function.IsValid()) + { + LuaValue.Type = UserData->Type; + LuaValue.FunctionName = UserData->Function->GetFName(); + LuaValue.Object = UserData->Context.Get(); + LuaValue.LuaState = this; + } + break; + } + } + + return LuaValue; +} + +int32 ULuaState::GetTop() +{ + return lua_gettop(L); +} + +int ULuaState::MetaTableFunctionUserData__index(lua_State* L) +{ + + ULuaState* LuaState = ULuaState::GetFromExtraSpace(L); + FLuaUserData* UserData = (FLuaUserData*)lua_touserdata(L, 1); + + if (!UserData->Context.IsValid()) + { + return luaL_error(L, "invalid UObject for UserData %p", UserData); + } + + TMap* TablePtr = nullptr; + UObject* Context = UserData->Context.Get(); + + ULuaUserDataObject* LuaUserDataObject = nullptr; + ULuaComponent* LuaComponent = nullptr; + + FString Key = ANSI_TO_TCHAR(lua_tostring(L, 2)); + + LuaComponent = Cast(Context); + + if (LuaComponent) + { + TablePtr = &LuaComponent->Table; + } + else + { + LuaUserDataObject = Cast(Context); + if (LuaUserDataObject) + { + TablePtr = &LuaUserDataObject->Table; + } + } + + if (TablePtr) + { + FLuaValue* LuaValue = TablePtr->Find(Key); + if (LuaValue) + { + LuaState->FromLuaValue(*LuaValue, Context, L); + return 1; + + } + } + + if (LuaComponent) + { + FLuaValue MetaIndexReturnValue = LuaComponent->ReceiveLuaMetaIndex(Key); + LuaState->FromLuaValue(MetaIndexReturnValue, Context, L); + return 1; + } + + if (LuaUserDataObject) + { + FLuaValue MetaIndexReturnValue = LuaUserDataObject->ReceiveLuaMetaIndex(Key); + LuaState->FromLuaValue(MetaIndexReturnValue, MetaIndexReturnValue.Object ? MetaIndexReturnValue.Object : Context, L); + return 1; + } + + lua_pushnil(L); + return 1; +} + +int ULuaState::MetaTableFunctionUserData__newindex(lua_State* L) +{ + ULuaState* LuaState = ULuaState::GetFromExtraSpace(L); + FLuaUserData* UserData = (FLuaUserData*)lua_touserdata(L, 1); + if (!UserData->Context.IsValid()) + { + return luaL_error(L, "invalid UObject for UserData %p", UserData); + } + + TMap* TablePtr = nullptr; + UObject* Context = UserData->Context.Get(); + + ULuaComponent* LuaComponent = Cast(Context); + + if (LuaComponent) + { + TablePtr = &LuaComponent->Table; + } + else if (ULuaUserDataObject* LuaUserDataObject = Cast(Context)) + { + TablePtr = &LuaUserDataObject->Table; + } + + if (TablePtr) + { + FString Key = ANSI_TO_TCHAR(lua_tostring(L, 2)); + + FLuaValue* LuaValue = TablePtr->Find(Key); + if (LuaValue) + { + *LuaValue = LuaState->ToLuaValue(3, L); + } + else + { + if (LuaComponent) + { + if (LuaComponent->ReceiveLuaMetaNewIndex(LuaState->ToLuaValue(2, L), LuaState->ToLuaValue(3, L))) + { + return 0; + } + } + TablePtr->Add(Key, LuaState->ToLuaValue(3, L)); + } + } + + return 0; +} + +FLuaDebug ULuaState::LuaGetInfo(int32 Level) +{ + lua_Debug ar; + if (lua_getstack(L, Level, &ar) != 1) + return FLuaDebug(); + lua_getinfo(L, "lSn", &ar); + FLuaDebug LuaDebug; + LuaDebug.CurrentLine = ar.currentline; + LuaDebug.Source = ANSI_TO_TCHAR(ar.source); + LuaDebug.Name = ANSI_TO_TCHAR(ar.name); + LuaDebug.NameWhat = ANSI_TO_TCHAR(ar.namewhat); + LuaDebug.What = ANSI_TO_TCHAR(ar.what); + + return LuaDebug; +} + +TMap ULuaState::LuaGetLocals(int32 Level) +{ + TMap ReturnValue; + + lua_Debug ar; + if (lua_getstack(L, Level, &ar) != 1) + return ReturnValue; + + int Index = 1; + const char* name = lua_getlocal(L, &ar, Index); + while (name) + { + FLuaValue LuaValue = ToLuaValue(-1); + ReturnValue.Add(ANSI_TO_TCHAR(name), LuaValue); + Pop(); + name = lua_getlocal(L, &ar, ++Index); + } + return ReturnValue; +} + +void ULuaState::Debug_Hook(lua_State* L, lua_Debug* ar) +{ + ULuaState* LuaState = ULuaState::GetFromExtraSpace(L); + FLuaDebug LuaDebug; + lua_getinfo(L, "lSn", ar); + LuaDebug.CurrentLine = ar->currentline; + LuaDebug.Source = ANSI_TO_TCHAR(ar->source); + LuaDebug.Name = ANSI_TO_TCHAR(ar->name); + LuaDebug.NameWhat = ANSI_TO_TCHAR(ar->namewhat); + LuaDebug.What = ANSI_TO_TCHAR(ar->what); + + switch (ar->event) + { + case LUA_HOOKLINE: + LuaState->ReceiveLuaLineHook(LuaDebug); + break; + case LUA_HOOKCALL: + LuaState->ReceiveLuaCallHook(LuaDebug); + break; + case LUA_HOOKRET: + LuaState->ReceiveLuaReturnHook(LuaDebug); + break; + case LUA_HOOKCOUNT: + LuaState->ReceiveLuaCountHook(LuaDebug); + break; + default: + break; + } +} + +int ULuaState::MetaTableFunctionUserData__eq(lua_State* L) +{ + ULuaState* LuaState = ULuaState::GetFromExtraSpace(L); + + FLuaUserData* UserData = (FLuaUserData*)lua_touserdata(L, 1); + if (!UserData->Context.IsValid()) + { + return luaL_error(L, "invalid UObject for UserData %p", UserData); + } + + FLuaUserData* UserData2 = (FLuaUserData*)lua_touserdata(L, 2); + if (!UserData2->Context.IsValid()) + { + return luaL_error(L, "invalid UObject for UserData %p", UserData2); + } + + if (UserData->Type == UserData2->Type && UserData->Context.Get() == UserData2->Context.Get()) + { + if (UserData->Type == ELuaValueType::UFunction) + { + if (!UserData->Function.IsValid()) + { + return luaL_error(L, "invalid UFunction for UserData %p", UserData); + } + if (!UserData2->Function.IsValid()) + { + return luaL_error(L, "invalid UFunction for UserData %p", UserData2); + } + if (UserData->Function.Get() == UserData2->Function.Get()) + { + lua_pushboolean(L, 1); + return 1; + } + } + else if (UserData->Type == ELuaValueType::UObject) + { + lua_pushboolean(L, 1); + return 1; + } + } + + lua_pushboolean(L, 0); + return 1; +} + +int ULuaState::MetaTableFunctionUserData__gc(lua_State* L) +{ + ULuaState* LuaState = ULuaState::GetFromExtraSpace(L); + + FLuaUserData* UserData = (FLuaUserData*)lua_touserdata(L, 1); + if (!UserData->Context.IsValid()) + { + return luaL_error(L, "invalid UObject for UserData %p", UserData); + } + + ULuaUserDataObject* LuaUserDataObject = Cast(UserData->Context.Get()); + if (LuaUserDataObject) + { + LuaState->TrackedLuaUserDataObjects.Remove(LuaUserDataObject); + LuaUserDataObject->ReceiveLuaGC(); + } + + lua_pushnil(L); + return 1; +} + +int ULuaState::MetaTableFunction__call(lua_State* L) +{ + ULuaState* LuaState = ULuaState::GetFromExtraSpace(L); + FLuaUserData* LuaCallContext = (FLuaUserData*)lua_touserdata(L, 1); + + if (!LuaCallContext->Context.IsValid() || !LuaCallContext->Function.IsValid()) + { + return luaL_error(L, "invalid lua UFunction for UserData %p", LuaCallContext); + } + + int NArgs = lua_gettop(L); + + UObject* CallScope = LuaCallContext->Context.Get(); + bool bImplicitSelf = false; + int StackPointer = 2; + + if (ULuaComponent* LuaComponent = Cast(CallScope)) + { + CallScope = LuaComponent->GetOwner(); + if (NArgs > 0) + { + FLuaValue LuaFirstArgument = LuaState->ToLuaValue(StackPointer, L); + if (LuaFirstArgument.Type == ELuaValueType::UObject && LuaFirstArgument.Object == LuaComponent) + { + bImplicitSelf = LuaComponent->bImplicitSelf; + } + } + } + else if (ULuaUserDataObject* LuaUserDataObject = Cast(CallScope)) + { + if (NArgs > 0) + { + FLuaValue LuaFirstArgument = LuaState->ToLuaValue(StackPointer, L); + if (LuaFirstArgument.Type == ELuaValueType::UObject && LuaFirstArgument.Object == LuaUserDataObject) + { + bImplicitSelf = LuaUserDataObject->bImplicitSelf; + } + } + } + + FScopeCycleCounterUObject ObjectScope(CallScope); + FScopeCycleCounterUObject FunctionScope(LuaCallContext->Function.Get()); + + void* Parameters = FMemory_Alloca(LuaCallContext->Function->ParmsSize); + FMemory::Memzero(Parameters, LuaCallContext->Function->ParmsSize); + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + for (TFieldIterator It(LuaCallContext->Function.Get()); (It && It->HasAnyPropertyFlags(CPF_Parm)); ++It) + { + FProperty* Prop = *It; +#else + for (TFieldIterator It(LuaCallContext->Function.Get()); (It && It->HasAnyPropertyFlags(CPF_Parm)); ++It) + { + UProperty* Prop = *It; +#endif + if (!Prop->HasAnyPropertyFlags(CPF_ZeroConstructor)) + { + Prop->InitializeValue_InContainer(Parameters); + } + } + + if (bImplicitSelf) + { + NArgs--; + StackPointer++; + } + + // arguments +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + for (TFieldIterator FArgs(LuaCallContext->Function.Get()); FArgs && ((FArgs->PropertyFlags & (CPF_Parm | CPF_ReturnParm)) == CPF_Parm); ++FArgs) + { + FProperty* Prop = *FArgs; + FStructProperty* LuaProp = CastField(Prop); +#else + for (TFieldIterator FArgs(LuaCallContext->Function.Get()); FArgs && ((FArgs->PropertyFlags & (CPF_Parm | CPF_ReturnParm)) == CPF_Parm); ++FArgs) + { + UProperty* Prop = *FArgs; + UStructProperty* LuaProp = Cast(Prop); +#endif + if (!LuaProp) + { +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FArrayProperty* ArrayProp = CastField(Prop); + if (ArrayProp) + { + LuaProp = CastField(ArrayProp->Inner); +#else + UArrayProperty* ArrayProp = Cast(Prop); + if (ArrayProp) + { + LuaProp = Cast(ArrayProp->Inner); +#endif + if (!LuaProp) + { + break; + } + if (LuaProp->Struct != FLuaValue::StaticStruct()) + { + break; + } + // start filling the array with the rest of arguments + int ArgsToProcess = NArgs - StackPointer + 1; + if (ArgsToProcess < 1) + { + break; + } + FScriptArrayHelper_InContainer ArrayHelper(ArrayProp, LuaProp->ContainerPtrToValuePtr(Parameters)); + ArrayHelper.AddValues(ArgsToProcess); + for (int i = StackPointer; i < StackPointer + ArgsToProcess; i++) + { + FLuaValue LuaValue = LuaState->ToLuaValue(i, L); + *LuaProp->ContainerPtrToValuePtr(ArrayHelper.GetRawPtr(i - StackPointer)) = LuaValue; + } + } + break; + } + if (LuaProp->Struct != FLuaValue::StaticStruct()) + { + break; + } + + FLuaValue LuaValue = LuaState->ToLuaValue(StackPointer++, L); + *LuaProp->ContainerPtrToValuePtr(Parameters) = LuaValue; + } + + LuaState->InceptionLevel++; + CallScope->ProcessEvent(LuaCallContext->Function.Get(), Parameters); + check(LuaState->InceptionLevel > 0); + LuaState->InceptionLevel--; + + if (LuaState->InceptionLevel == 0) + { + FString Error; + while (LuaState->InceptionErrors.Dequeue(Error)) + { + ULuaComponent* LuaComponent = Cast(LuaCallContext->Context); + if (LuaComponent) + { + if (LuaComponent->bLogError) + { + LuaState->LogError(Error); + } + LuaComponent->OnLuaError.Broadcast(Error); + } + else + { + if (LuaState->bLogError) + { + LuaState->LogError(Error); + } + LuaState->ReceiveLuaError(Error); + } + } + } + + int ReturnedValues = 0; + + // get return value +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + for (TFieldIterator FArgs(LuaCallContext->Function.Get()); FArgs; ++FArgs) + { + FProperty* Prop = *FArgs; +#else + for (TFieldIterator FArgs(LuaCallContext->Function.Get()); FArgs; ++FArgs) + { + UProperty* Prop = *FArgs; +#endif + if (!Prop->HasAnyPropertyFlags(CPF_ReturnParm | CPF_OutParm)) + { + continue; + } + + // avoid input args (at all costs !) + if (Prop->HasAnyPropertyFlags(CPF_ConstParm | CPF_ReferenceParm)) + { + continue; + } +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FStructProperty* LuaProp = CastField(Prop); + if (!LuaProp) + { + FArrayProperty* ArrayProp = CastField(Prop); + if (ArrayProp) + { + LuaProp = CastField(ArrayProp->Inner); +#else + UStructProperty* LuaProp = Cast(Prop); + if (!LuaProp) + { + UArrayProperty* ArrayProp = Cast(Prop); + if (ArrayProp) + { + LuaProp = Cast(ArrayProp->Inner); +#endif + if (!LuaProp) + break; + if (LuaProp->Struct != FLuaValue::StaticStruct()) + break; + + FScriptArrayHelper_InContainer ArrayHelper(ArrayProp, LuaProp->ContainerPtrToValuePtr(Parameters)); + for (int i = 0; i < ArrayHelper.Num(); i++) + { + FLuaValue* LuaValue = LuaProp->ContainerPtrToValuePtr(ArrayHelper.GetRawPtr(i)); + ReturnedValues++; + LuaState->FromLuaValue(*LuaValue, nullptr, L); + } + + + } + break; + } + + if (LuaProp->Struct != FLuaValue::StaticStruct()) + break; + + FLuaValue* LuaValue = LuaProp->ContainerPtrToValuePtr(Parameters); + if (LuaValue) + { + ReturnedValues++; + LuaState->FromLuaValue(*LuaValue, nullptr, L); + } + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + for (TFieldIterator It(LuaCallContext->Function.Get()); (It && It->HasAnyPropertyFlags(CPF_Parm)); ++It) +#else + for (TFieldIterator It(LuaCallContext->Function.Get()); (It && It->HasAnyPropertyFlags(CPF_Parm)); ++It) +#endif + { + It->DestroyValue_InContainer(Parameters); + } + + + if (ReturnedValues > 0) + return ReturnedValues; + + lua_pushnil(L); + return 1; +} + +int ULuaState::MetaTableFunction__rawcall(lua_State * L) +{ + ULuaState* LuaState = ULuaState::GetFromExtraSpace(L); + FLuaUserData* LuaCallContext = (FLuaUserData*)lua_touserdata(L, 1); + + if (!LuaCallContext->Context.IsValid() || !LuaCallContext->Function.IsValid()) + { + return luaL_error(L, "invalid lua UFunction for UserData %p", LuaCallContext); + } + + int NArgs = lua_gettop(L); + + UObject* CallScope = LuaCallContext->Context.Get(); + bool bImplicitSelf = false; + int StackPointer = 2; + + if (ULuaComponent* LuaComponent = Cast(CallScope)) + { + CallScope = LuaComponent->GetOwner(); + if (NArgs > 0) + { + FLuaValue LuaFirstArgument = LuaState->ToLuaValue(StackPointer, L); + if (LuaFirstArgument.Type == ELuaValueType::UObject && LuaFirstArgument.Object == LuaComponent) + { + bImplicitSelf = LuaComponent->bImplicitSelf; + } + } + } + else if (ULuaUserDataObject* LuaUserDataObject = Cast(CallScope)) + { + if (NArgs > 0) + { + FLuaValue LuaFirstArgument = LuaState->ToLuaValue(StackPointer, L); + if (LuaFirstArgument.Type == ELuaValueType::UObject && LuaFirstArgument.Object == LuaUserDataObject) + { + bImplicitSelf = LuaUserDataObject->bImplicitSelf; + } + } + } + + FScopeCycleCounterUObject ObjectScope(CallScope); + FScopeCycleCounterUObject FunctionScope(LuaCallContext->Function.Get()); + + void* Parameters = FMemory_Alloca(LuaCallContext->Function->ParmsSize); + FMemory::Memzero(Parameters, LuaCallContext->Function->ParmsSize); + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + for (TFieldIterator It(LuaCallContext->Function.Get()); (It && It->HasAnyPropertyFlags(CPF_Parm)); ++It) + { + FProperty* Prop = *It; +#else + for (TFieldIterator It(LuaCallContext->Function.Get()); (It && It->HasAnyPropertyFlags(CPF_Parm)); ++It) + { + UProperty* Prop = *It; +#endif + if (!Prop->HasAnyPropertyFlags(CPF_ZeroConstructor)) + { + Prop->InitializeValue_InContainer(Parameters); + } + } + + if (bImplicitSelf) + { + NArgs--; + StackPointer++; + } + + // arguments +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + for (TFieldIterator FArgs(LuaCallContext->Function.Get()); FArgs && ((FArgs->PropertyFlags & (CPF_Parm | CPF_ReturnParm)) == CPF_Parm); ++FArgs) + { + FProperty* Prop = *FArgs; +#else + for (TFieldIterator FArgs(LuaCallContext->Function.Get()); FArgs && ((FArgs->PropertyFlags & (CPF_Parm | CPF_ReturnParm)) == CPF_Parm); ++FArgs) + { + UProperty* Prop = *FArgs; +#endif + bool bPropertySet = false; + LuaState->ToProperty(Parameters, Prop, LuaState->ToLuaValue(StackPointer++, L), bPropertySet, 0); + } + + LuaState->InceptionLevel++; + CallScope->ProcessEvent(LuaCallContext->Function.Get(), Parameters); + check(LuaState->InceptionLevel > 0); + LuaState->InceptionLevel--; + + if (LuaState->InceptionLevel == 0) + { + FString Error; + while (LuaState->InceptionErrors.Dequeue(Error)) + { + ULuaComponent* LuaComponent = Cast(LuaCallContext->Context); + if (LuaComponent) + { + if (LuaComponent->bLogError) + { + LuaState->LogError(Error); + } + LuaComponent->OnLuaError.Broadcast(Error); + } + else + { + if (LuaState->bLogError) + { + LuaState->LogError(Error); + } + LuaState->ReceiveLuaError(Error); + } + } + } + + int ReturnedValues = 0; + + // get return value +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + for (TFieldIterator FArgs(LuaCallContext->Function.Get()); FArgs; ++FArgs) + { + FProperty* Prop = *FArgs; +#else + for (TFieldIterator FArgs(LuaCallContext->Function.Get()); FArgs; ++FArgs) + { + UProperty* Prop = *FArgs; +#endif + if (!Prop->HasAnyPropertyFlags(CPF_ReturnParm | CPF_OutParm)) + { + continue; + } + + // avoid input args (at all costs !) + if (Prop->HasAnyPropertyFlags(CPF_ConstParm | CPF_ReferenceParm)) + { + continue; + } + + bool bPropertyGet = false; + FLuaValue LuaValue = LuaState->FromProperty(Parameters, Prop, bPropertyGet, 0); + ReturnedValues++; + LuaState->FromLuaValue(LuaValue, nullptr, L); + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + for (TFieldIterator It(LuaCallContext->Function.Get()); (It && It->HasAnyPropertyFlags(CPF_Parm)); ++It) +#else + for (TFieldIterator It(LuaCallContext->Function.Get()); (It && It->HasAnyPropertyFlags(CPF_Parm)); ++It) +#endif + { + It->DestroyValue_InContainer(Parameters); + } + + if (ReturnedValues > 0) + { + return ReturnedValues; + } + + lua_pushnil(L); + return 1; +} + +int ULuaState::MetaTableFunction__rawbroadcast(lua_State * L) +{ + ULuaState* LuaState = ULuaState::GetFromExtraSpace(L); + FLuaUserData* LuaCallContext = (FLuaUserData*)lua_touserdata(L, 1); + + if (!LuaCallContext->MulticastScriptDelegate || !LuaCallContext->Function.IsValid()) + { + return luaL_error(L, "invalid lua Multicast Delegate for UserData %p", LuaCallContext); + } + + int NArgs = lua_gettop(L); + int StackPointer = 2; + + FScopeCycleCounterUObject FunctionScope(LuaCallContext->Function.Get()); + + void* Parameters = FMemory_Alloca(LuaCallContext->Function->ParmsSize); + FMemory::Memzero(Parameters, LuaCallContext->Function->ParmsSize); + + for (TFieldIterator It(LuaCallContext->Function.Get()); (It && It->HasAnyPropertyFlags(CPF_Parm)); ++It) + { + FProperty* Prop = *It; + if (!Prop->HasAnyPropertyFlags(CPF_ZeroConstructor)) + { + Prop->InitializeValue_InContainer(Parameters); + } + } + + // arguments + for (TFieldIterator FArgs(LuaCallContext->Function.Get()); FArgs && ((FArgs->PropertyFlags & (CPF_Parm | CPF_ReturnParm)) == CPF_Parm); ++FArgs) + { + FProperty* Prop = *FArgs; + bool bPropertySet = false; + LuaState->ToProperty(Parameters, Prop, LuaState->ToLuaValue(StackPointer++, L), bPropertySet, 0); + } + + LuaState->InceptionLevel++; + LuaCallContext->MulticastScriptDelegate->ProcessMulticastDelegate(Parameters); + check(LuaState->InceptionLevel > 0); + LuaState->InceptionLevel--; + + if (LuaState->InceptionLevel == 0) + { + FString Error; + while (LuaState->InceptionErrors.Dequeue(Error)) + { + if (LuaState->bLogError) + { + LuaState->LogError(Error); + } + LuaState->ReceiveLuaError(Error); + } + } + + // no return values in multicast delegates + for (TFieldIterator It(LuaCallContext->Function.Get()); (It && It->HasAnyPropertyFlags(CPF_Parm)); ++It) + { + It->DestroyValue_InContainer(Parameters); + } + + lua_pushnil(L); + return 1; +} + +int ULuaState::TableFunction_print(lua_State * L) +{ + ULuaState* LuaState = ULuaState::GetFromExtraSpace(L); + TArray Messages; + + int n = lua_gettop(L); + lua_getglobal(L, "tostring"); + for (int i = 1; i <= n; i++) + { + lua_pushvalue(L, -1); + lua_pushvalue(L, i); + lua_call(L, 1, 1); + const char* s = lua_tostring(L, -1); + if (!s) + return luaL_error(L, "'tostring must return a string to 'print'"); + FString Value = ANSI_TO_TCHAR(s); + lua_pop(L, 1); + Messages.Add(Value); + } + LuaState->Log(FString::Join(Messages, TEXT("\t"))); + return 0; +} + +int ULuaState::TableFunction_package_loader_codeasset(lua_State * L) +{ + ULuaState* LuaState = ULuaState::GetFromExtraSpace(L); + + // use the second (sanitized by the loader) argument + FString Key = ANSI_TO_TCHAR(lua_tostring(L, 2)); + + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 0 + FAssetData AssetData = AssetRegistryModule.Get().GetAssetByObjectPath(Key); + if (AssetData.IsValid() && AssetData.AssetClassPath.ToString() == "LuaCode") +#else + FAssetData AssetData = AssetRegistryModule.Get().GetAssetByObjectPath(*Key); + if (AssetData.IsValid() && AssetData.AssetClass == "LuaCode") +#endif + { + ULuaCode* LuaCode = Cast(AssetData.GetAsset()); + if (LuaCode) + { + if (!LuaState->RunCodeAsset(LuaCode, 1)) + { + return luaL_error(L, "%s", lua_tostring(L, -1)); + } + return 1; + } + } + + return luaL_error(L, "unable to load asset '%s'", TCHAR_TO_UTF8(*Key)); +} + +int ULuaState::TableFunction_package_loader_asset(lua_State * L) +{ + ULuaState* LuaState = ULuaState::GetFromExtraSpace(L); + + // use the second (sanitized by the loader) argument + const FString Key = ANSI_TO_TCHAR(lua_tostring(L, 2)); + + if (LuaState->RunFile(Key, true, 1)) + { + return 1; + } + return luaL_error(L, "%s", lua_tostring(L, -1)); +} + +int ULuaState::TableFunction_package_loader(lua_State * L) +{ + ULuaState* LuaState = ULuaState::GetFromExtraSpace(L); + + FString Key = ANSI_TO_TCHAR(lua_tostring(L, 1)); + + // check for code assets + if (FPackageName::IsValidObjectPath(Key)) + { + // fix asset name (if required) + int32 LastSlashIndex = -1; + Key.FindLastChar('/', LastSlashIndex); + const FString LastPart = Key.RightChop(LastSlashIndex + 1); + if (!LastPart.Contains(".")) + { + Key = FString::Printf(TEXT("%s.%s"), *Key, *LastPart); + } + + lua_pushcfunction(L, ULuaState::TableFunction_package_loader_codeasset); + lua_pushstring(L, TCHAR_TO_UTF8(*Key)); + return 2; + } + else + { + // TODO: make it smarter by checking for file extension... + if (!Key.EndsWith(".lua")) + { + Key += ".lua"; + } + // search in root content... + FString AbsoluteFilename = FPaths::Combine(FPaths::ProjectContentDir(), Key); + if (FPaths::FileExists(AbsoluteFilename)) + { + lua_pushcfunction(L, ULuaState::TableFunction_package_loader_asset); + lua_pushstring(L, TCHAR_TO_UTF8(*Key)); + return 2; + } + else + { + // or search in additional paths + for (FString AdditionalPath : LuaState->AppendProjectContentDirSubDir) + { + AbsoluteFilename = FPaths::Combine(FPaths::ProjectContentDir(), AdditionalPath, Key); + if (FPaths::FileExists(AbsoluteFilename)) + { + lua_pushcfunction(L, ULuaState::TableFunction_package_loader_asset); + lua_pushstring(L, TCHAR_TO_UTF8(*(AdditionalPath / Key))); + return 2; + } + } + } + } + + // use UTF8 as the package name can contains non-ASCII chars + lua_pushstring(L, TCHAR_TO_UTF8(*FString::Printf(TEXT("\n\tno asset '%s'"), *Key))); + return 1; +} + +int ULuaState::TableFunction_package_preload(lua_State * L) +{ + ULuaState* LuaState = ULuaState::GetFromExtraSpace(L); + + if (LuaState->L != L) + { + return luaL_error(L, "you cannot call package.preload from a thread/coroutine (error while loading %s)", lua_tostring(L, 1)); + } + + FString Key = ANSI_TO_TCHAR(lua_tostring(L, 1)); + + // check for code assets + ULuaCode** LuaCodePtr = LuaState->RequireTable.Find(Key); + if (!LuaCodePtr) + { + if (LuaState->bAddProjectContentDirToPackagePath && LuaState->RunFile(Key + ".lua", true, 1)) + { + return 1; + } + + // now search in additional paths + for (FString AdditionalPath : LuaState->AppendProjectContentDirSubDir) + { + if (LuaState->RunFile(AdditionalPath / Key + ".lua", true, 1)) + { + return 1; + } + return luaL_error(L, "%s", lua_tostring(L, -1)); + + } + return luaL_error(L, "unable to find package %s", TCHAR_TO_ANSI(*Key)); + } + + ULuaCode* LuaCode = *LuaCodePtr; + if (!LuaCode) + { + return luaL_error(L, "LuaCodeAsset not set for package %s", TCHAR_TO_ANSI(*Key)); + } + + if (!LuaState->RunCodeAsset(LuaCode, 1)) + { + return luaL_error(L, "%s", lua_tostring(L, -1)); + } + + return 1; +} + +void ULuaState::ReceiveLuaError_Implementation(const FString & Message) +{ + +} + +void ULuaState::ReceiveLuaCallHook_Implementation(const FLuaDebug & LuaDebug) +{ + +} + +void ULuaState::ReceiveLuaReturnHook_Implementation(const FLuaDebug & LuaDebug) +{ + +} + +void ULuaState::ReceiveLuaLineHook_Implementation(const FLuaDebug & LuaDebug) +{ + +} + +void ULuaState::ReceiveLuaCountHook(const FLuaDebug & LuaDebug) +{ + +} + +void ULuaState::ReceiveLuaLevelRemovedFromWorld_Implementation(ULevel * Level, UWorld * World) +{ + +} + +void ULuaState::ReceiveLuaLevelAddedToWorld_Implementation(ULevel * Level, UWorld * World) +{ + +} + +void ULuaState::ReceiveLuaStatePreInitialized_Implementation() +{ + +} + +void ULuaState::ReceiveLuaStateInitialized_Implementation() +{ + +} + +void ULuaState::NewTable() +{ + lua_newtable(L); +} + +void ULuaState::SetMetaTable(int Index) +{ + lua_setmetatable(L, Index); +} + +void ULuaState::GetMetaTable(int Index) +{ + lua_getmetatable(L, Index); +} + +void ULuaState::SetField(int Index, const char* FieldName) +{ + lua_setfield(L, Index, FieldName); +} + +void ULuaState::GetField(int Index, const char* FieldName) +{ + lua_getfield(L, Index, FieldName); +} + +void ULuaState::RawGetI(int Index, int N) +{ + lua_rawgeti(L, Index, N); +} + +void ULuaState::RawSetI(int Index, int N) +{ + lua_rawseti(L, Index, N); +} + +void ULuaState::PushGlobalTable() +{ + lua_pushglobaltable(L); +} + +void ULuaState::PushRegistryTable() +{ + lua_pushvalue(L, LUA_REGISTRYINDEX); +} + +int32 ULuaState::GetFieldFromTree(const FString & Tree, bool bGlobal) +{ + TArray Parts; + Tree.ParseIntoArray(Parts, TEXT(".")); + if (Parts.Num() == 0) + { + LastError = FString::Printf(TEXT("invalid Lua key: \"%s\""), *Tree); + if (bLogError) + LogError(LastError); + ReceiveLuaError(LastError); + PushNil(); + return 1; + } + + int32 AdditionalPop = bGlobal ? 1 : 0; + + if (bGlobal) + { + PushGlobalTable(); + } + int32 i; + + for (i = 0; i < Parts.Num(); i++) + { + GetField(-1, TCHAR_TO_ANSI(*Parts[i])); + + if (lua_isnil(L, -1)) + { + if (i == Parts.Num() - 1) + { + return i + 1 + AdditionalPop; + } + LastError = FString::Printf(TEXT("Lua key \"%s\" is nil"), *Parts[i]); + if (bLogError) + LogError(LastError); + ReceiveLuaError(LastError); + return i + 1 + AdditionalPop; + } + } + return i + AdditionalPop; +} + +void ULuaState::SetFieldFromTree(const FString & Tree, FLuaValue & Value, bool bGlobal, UObject * CallContext) +{ + TArray Parts; + Tree.ParseIntoArray(Parts, TEXT(".")); + + int32 ItemsToPop = GetFieldFromTree(Tree, bGlobal); + // invalid key + if (ItemsToPop != (Parts.Num() + (bGlobal ? 1 : 0))) + { + Pop(ItemsToPop); + return; + } + + Pop(); + FromLuaValue(Value, CallContext); + SetField(-2, TCHAR_TO_ANSI(*Parts.Last())); + Pop(ItemsToPop - 1); +} + + +void ULuaState::NewUObject(UObject * Object, lua_State * State) +{ + if (!State) + { + State = this->L; + } + FLuaUserData* UserData = (FLuaUserData*)lua_newuserdata(State, sizeof(FLuaUserData)); + UserData->Type = ELuaValueType::UObject; + UserData->Context = Object; + UserData->Function = nullptr; +} + +void ULuaState::GetGlobal(const char* Name) +{ + lua_getglobal(L, Name); +} + +void ULuaState::SetGlobal(const char* Name) +{ + lua_setglobal(L, Name); +} + +void ULuaState::PushValue(int Index) +{ + lua_pushvalue(L, Index); +} + +bool ULuaState::PCall(int NArgs, FLuaValue & Value, int NRet) +{ + bool bSuccess = Call(NArgs, Value, NRet); + if (!bSuccess) + { + if (InceptionLevel > 0) + { + InceptionErrors.Enqueue(LastError); + } + else + { + if (bLogError) + LogError(LastError); + ReceiveLuaError(LastError); + } + } + return bSuccess; +} + +bool ULuaState::Call(int NArgs, FLuaValue & Value, int NRet) +{ + if (lua_pcall(L, NArgs, NRet, 0)) + { + LastError = FString::Printf(TEXT("Lua error: %s"), ANSI_TO_TCHAR(lua_tostring(L, -1))); + return false; + } + + if (NRet > 0) + { + Value = ToLuaValue(-1); + } + return true; +} + +void ULuaState::Pop(int32 Amount) +{ + lua_pop(L, Amount); +} + +void ULuaState::PushNil() +{ + lua_pushnil(L); +} + +void ULuaState::PushCFunction(lua_CFunction Function) +{ + lua_pushcfunction(L, Function); +} + +void* ULuaState::NewUserData(size_t DataSize) +{ + return lua_newuserdata(L, DataSize); +} + +void ULuaState::Unref(int Ref) +{ + luaL_unref(L, LUA_REGISTRYINDEX, Ref); +} + +void ULuaState::UnrefChecked(int Ref) +{ + // in case of moved value (like when compiling a blueprint), L should be nullptr + if (!L) + return; + + Unref(Ref); +} + +int ULuaState::NewRef() +{ + return luaL_ref(L, LUA_REGISTRYINDEX); +} + +void ULuaState::GetRef(int Ref) +{ + lua_rawgeti(L, LUA_REGISTRYINDEX, Ref); +} + +int ULuaState::Next(int Index) +{ + return lua_next(L, Index); +} + +bool ULuaState::Yield(int Index, int NArgs) +{ + lua_State* Coroutine = lua_tothread(L, Index); + if (!Coroutine) + return false; + + if (lua_status(Coroutine) == LUA_OK && lua_gettop(Coroutine) == 0) + { + lua_pushboolean(L, 0); + lua_pushstring(L, "Lua error: cannot yield a dead coroutine"); + return false; + } + + lua_xmove(L, Coroutine, NArgs); + + int Ret = lua_yield(Coroutine, NArgs); + + if (Ret != LUA_OK) + { + lua_pushboolean(L, 0); + lua_xmove(Coroutine, L, 1); + return false; + } + + int NRet = lua_gettop(Coroutine); + lua_pushboolean(L, 1); + lua_xmove(Coroutine, L, NRet); + return true; +} + +bool ULuaState::Resume(int Index, int NArgs) +{ + lua_State* Coroutine = lua_tothread(L, Index); + if (!Coroutine) + return false; + + if (lua_status(Coroutine) == LUA_OK && lua_gettop(Coroutine) == 0) + { + lua_pushboolean(L, 0); + lua_pushstring(L, "Lua error: cannot resume dead coroutine"); + return false; + } + + lua_xmove(L, Coroutine, NArgs); + int Ret = lua_resume(Coroutine, L, NArgs); + if (Ret != LUA_OK && Ret != LUA_YIELD) + { + lua_pushboolean(L, 0); + lua_xmove(Coroutine, L, 1); + return false; + } + + int NRet = lua_gettop(Coroutine); + lua_pushboolean(L, 1); + lua_xmove(Coroutine, L, NRet); + return true; +} + +int ULuaState::GC(int What, int Data) +{ + return lua_gc(L, What, Data); +} + +void ULuaState::Len(int Index) +{ + lua_len(L, Index); +} + +int32 ULuaState::ILen(int Index) +{ + return luaL_len(L, Index); +} + +int32 ULuaState::ToInteger(int Index) +{ + return lua_tointeger(L, Index); +} + +FLuaValue ULuaState::CreateLuaTable() +{ + FLuaValue NewTable; + NewTable.Type = ELuaValueType::Table; + NewTable.LuaState = this; + FromLuaValue(NewTable); + Pop(); + return NewTable; +} + +FLuaValue ULuaState::CreateLuaLazyTable() +{ + FLuaValue NewTable; + NewTable.Type = ELuaValueType::Table; + NewTable.LuaState = this; + return NewTable; +} + +FLuaValue ULuaState::CreateLuaThread(FLuaValue Value) +{ + FLuaValue NewThread; + NewThread.Type = ELuaValueType::Thread; + NewThread.LuaState = this; + FromLuaValue(NewThread); + lua_State* NewLuaThread = lua_tothread(L, -1); + FromLuaValue(Value, nullptr, NewLuaThread); + Pop(); + return NewThread; +} + +ELuaThreadStatus ULuaState::GetLuaThreadStatus(FLuaValue Value) +{ + if (Value.Type != ELuaValueType::Thread || Value.LuaState != this) + return ELuaThreadStatus::Invalid; + + FromLuaValue(Value); + lua_State* LuaThread = lua_tothread(L, -1); + int ReturnValue = lua_status(LuaThread); + Pop(); + + if (ReturnValue == 0) + return ELuaThreadStatus::Ok; + + if (ReturnValue == LUA_YIELD) + return ELuaThreadStatus::Suspended; + + return ELuaThreadStatus::Error; +} + +int32 ULuaState::GetLuaThreadStackTop(FLuaValue Value) +{ + if (Value.Type != ELuaValueType::Thread || Value.LuaState != this) + return MIN_int32; + + FromLuaValue(Value); + lua_State* LuaThread = lua_tothread(L, -1); + int ReturnValue = lua_gettop(LuaThread); + Pop(); + + return ReturnValue; +} + +TSharedRef ULuaState::AddLuaSmartReference(FLuaValue Value) +{ + TSharedRef Ref = MakeShared(); + Ref->LuaState = this; + Ref->Value = Value; + + LuaSmartReferences.Add(Ref); + + return Ref; +} + +void ULuaState::RemoveLuaSmartReference(TSharedRef Ref) +{ + LuaSmartReferences.Remove(Ref); +} + +ULuaState::~ULuaState() +{ + FCoreUObjectDelegates::GetPostGarbageCollect().Remove(GCLuaDelegatesHandle); + +#if WITH_EDITOR + if (LuaConsole.LuaState) + { + IModularFeatures::Get().UnregisterModularFeature(IConsoleCommandExecutor::ModularFeatureName(), &LuaConsole); + } +#endif + + FLuaMachineModule::Get().UnregisterLuaState(this); + + if (L) + { + lua_close(L); + L = nullptr; + } +} + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 +#define LUAVALUE_PROP_CAST(Type, Type2) F##Type* __##Type##__ = CastField(Property);\ + if (__##Type##__)\ + {\ + return FLuaValue((Type2)__##Type##__->GetPropertyValue_InContainer(Buffer, Index));\ + } + +#define LUAVALUE_PROP_CAST_TOSTRING(Type) F##Type* __##Type##__ = CastField(Property);\ + if (__##Type##__)\ + {\ + return FLuaValue(__##Type##__->GetPropertyValue_InContainer(Buffer, Index).ToString());\ + } + +#define LUAVALUE_PROP_SET(Type, Value) F##Type* __##Type##__ = CastField(Property);\ + if (__##Type##__)\ + {\ + __##Type##__->SetPropertyValue_InContainer(Buffer, Value, Index);\ + return;\ + } +#else +#define LUAVALUE_PROP_CAST(Type, Type2) U##Type* __##Type##__ = Cast(Property);\ + if (__##Type##__)\ + {\ + return FLuaValue((Type2)__##Type##__->GetPropertyValue_InContainer(Buffer, Index));\ + } + +#define LUAVALUE_PROP_CAST_TOSTRING(Type) U##Type* __##Type##__ = Cast(Property);\ + if (__##Type##__)\ + {\ + return FLuaValue(__##Type##__->GetPropertyValue_InContainer(Buffer, Index).ToString());\ + } + +#define LUAVALUE_PROP_SET(Type, Value) U##Type* __##Type##__ = Cast(Property);\ + if (__##Type##__)\ + {\ + __##Type##__->SetPropertyValue_InContainer(Buffer, Value, Index);\ + return;\ + } +#endif + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 +FLuaValue ULuaState::FromUProperty(void* Buffer, FProperty * Property, bool& bSuccess, int32 Index) +{ + return FromFProperty(Buffer, Property, bSuccess, Index); +} +void ULuaState::ToUProperty(void* Buffer, FProperty * Property, FLuaValue Value, bool& bSuccess, int32 Index) +{ + ToFProperty(Buffer, Property, Value, bSuccess, Index); +} +#endif + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 +FLuaValue ULuaState::FromFProperty(void* Buffer, FProperty * Property, bool& bSuccess, int32 Index) +#else +FLuaValue ULuaState::FromUProperty(void* Buffer, UProperty * Property, bool& bSuccess, int32 Index) +#endif +{ + bSuccess = true; + + LUAVALUE_PROP_CAST(BoolProperty, bool); + LUAVALUE_PROP_CAST(DoubleProperty, double); + LUAVALUE_PROP_CAST(FloatProperty, float); + LUAVALUE_PROP_CAST(Int64Property, int64); + LUAVALUE_PROP_CAST(UInt64Property, int64); + LUAVALUE_PROP_CAST(IntProperty, int32); + LUAVALUE_PROP_CAST(UInt32Property, int32); + LUAVALUE_PROP_CAST(Int16Property, int32); + LUAVALUE_PROP_CAST(Int8Property, int32); + LUAVALUE_PROP_CAST(ByteProperty, int32); + LUAVALUE_PROP_CAST(UInt16Property, int32); + + LUAVALUE_PROP_CAST(StrProperty, FString); + LUAVALUE_PROP_CAST_TOSTRING(NameProperty); + LUAVALUE_PROP_CAST_TOSTRING(TextProperty); + + LUAVALUE_PROP_CAST(ClassProperty, UObject*); + LUAVALUE_PROP_CAST(ObjectProperty, UObject*); + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FEnumProperty* EnumProperty = CastField(Property); + + if (EnumProperty) + { + const uint8* EnumValue = EnumProperty->ContainerPtrToValuePtr(Buffer, Index); + return FLuaValue((int32)*EnumValue); + } +#endif + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FObjectPropertyBase* ObjectPropertyBase = CastField(Property); +#else + UObjectPropertyBase* ObjectPropertyBase = Cast(Property); +#endif + if (ObjectPropertyBase) + { + return FLuaValue(ObjectPropertyBase->GetObjectPropertyValue_InContainer(Buffer, Index)); + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FWeakObjectProperty* WeakObjectProperty = CastField(Property); +#else + UWeakObjectProperty* WeakObjectProperty = Cast(Property); +#endif + if (WeakObjectProperty) + { + const FWeakObjectPtr& WeakPtr = WeakObjectProperty->GetPropertyValue_InContainer(Buffer, Index); + return FLuaValue(WeakPtr.Get()); + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + if (FMulticastDelegateProperty* MulticastProperty = CastField(Property)) +#else + if (UMulticastDelegateProperty* MulticastProperty = Cast(Property)) +#endif + { + FLuaValue MulticastValue; + MulticastValue.Type = ELuaValueType::MulticastDelegate; + MulticastValue.Object = MulticastProperty->SignatureFunction; + MulticastValue.MulticastScriptDelegate = reinterpret_cast(MulticastProperty->ContainerPtrToValuePtr(Buffer)); + return MulticastValue; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + if (FDelegateProperty* DelegateProperty = CastField(Property)) +#else + if (UDelegateProperty* DelegateProperty = Cast(Property)) +#endif + { + const FScriptDelegate& ScriptDelegate = DelegateProperty->GetPropertyValue_InContainer(Buffer, Index); + return FLuaValue::FunctionOfObject((UObject*)ScriptDelegate.GetUObject(), ScriptDelegate.GetFunctionName()); + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + if (FArrayProperty* ArrayProperty = CastField(Property)) +#else + if (UArrayProperty* ArrayProperty = Cast(Property)) +#endif + { + FLuaValue NewLuaArray = CreateLuaTable(); + FScriptArrayHelper_InContainer Helper(ArrayProperty, Buffer, Index); + for (int32 ArrayIndex = 0; ArrayIndex < Helper.Num(); ArrayIndex++) + { + uint8* ArrayItemPtr = Helper.GetRawPtr(ArrayIndex); + bool bArrayItemSuccess = false; + NewLuaArray.SetFieldByIndex(ArrayIndex + 1, FromProperty(ArrayItemPtr, ArrayProperty->Inner, bArrayItemSuccess, 0)); + } + return NewLuaArray; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + if (FMapProperty* MapProperty = CastField(Property)) +#else + if (UMapProperty* MapProperty = Cast(Property)) +#endif + { + FLuaValue NewLuaTable = CreateLuaTable(); + FScriptMapHelper_InContainer Helper(MapProperty, Buffer, Index); + for (int32 MapIndex = 0; MapIndex < Helper.Num(); MapIndex++) + { + uint8* ArrayKeyPtr = Helper.GetKeyPtr(MapIndex); + uint8* ArrayValuePtr = Helper.GetValuePtr(MapIndex); + bool bArrayItemSuccess = false; + NewLuaTable.SetField( + FromProperty(ArrayKeyPtr, MapProperty->KeyProp, bArrayItemSuccess, 0).ToString(), + FromProperty(ArrayValuePtr, MapProperty->ValueProp, bArrayItemSuccess, 0)); + } + return NewLuaTable; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + if (FSetProperty* SetProperty = CastField(Property)) +#else + if (USetProperty* SetProperty = Cast(Property)) +#endif + { + FLuaValue NewLuaArray = CreateLuaTable(); + FScriptSetHelper_InContainer Helper(SetProperty, Buffer, Index); + for (int32 SetIndex = 0; SetIndex < Helper.Num(); SetIndex++) + { + uint8* ArrayItemPtr = Helper.GetElementPtr(SetIndex); + bool bArrayItemSuccess = false; + NewLuaArray.SetFieldByIndex(SetIndex + 1, FromProperty(ArrayItemPtr, SetProperty->ElementProp, bArrayItemSuccess, 0)); + } + return NewLuaArray; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + if (FStructProperty* StructProperty = CastField(Property)) +#else + if (UStructProperty* StructProperty = Cast(Property)) +#endif + { + // fast path + if (StructProperty->Struct == FLuaValue::StaticStruct()) + { + FLuaValue* LuaValuePtr = StructProperty->ContainerPtrToValuePtr(Buffer); + // trick for allowing lazy tables creation + FromLuaValue(*LuaValuePtr); + Pop(); + return *LuaValuePtr; + } + + const uint8* StructContainer = StructProperty->ContainerPtrToValuePtr(Buffer, Index); + + return StructToLuaTable(StructProperty->Struct, StructContainer); + } + + bSuccess = false; + return FLuaValue(); +} + +FLuaValue ULuaState::StructToLuaTable(UScriptStruct * InScriptStruct, const uint8 * StructData) +{ + FLuaValue NewLuaTable = CreateLuaTable(); +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + for (TFieldIterator It(InScriptStruct); It; ++It) +#else + for (TFieldIterator It(InScriptStruct); It; ++It) +#endif + { +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FProperty* FieldProp = *It; +#else + UProperty* FieldProp = *It; +#endif + FString PropName = FieldProp->GetName(); + bool bTableItemSuccess = false; + NewLuaTable.SetField(PropName, FromProperty((void*)StructData, FieldProp, bTableItemSuccess, 0)); + } + return NewLuaTable; +} + +FLuaValue ULuaState::StructToLuaTable(UScriptStruct * InScriptStruct, const TArray&StructData) +{ + return StructToLuaTable(InScriptStruct, StructData.GetData()); +} + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 +void ULuaState::ToFProperty(void* Buffer, FProperty * Property, FLuaValue Value, bool& bSuccess, int32 Index) +#else +void ULuaState::ToUProperty(void* Buffer, UProperty * Property, FLuaValue Value, bool& bSuccess, int32 Index) +#endif +{ + bSuccess = true; + + LUAVALUE_PROP_SET(BoolProperty, Value.ToBool()); + LUAVALUE_PROP_SET(DoubleProperty, Value.ToFloat()); + LUAVALUE_PROP_SET(FloatProperty, Value.ToFloat()); + LUAVALUE_PROP_SET(IntProperty, Value.ToInteger()); + LUAVALUE_PROP_SET(UInt32Property, Value.ToInteger()); + LUAVALUE_PROP_SET(Int64Property, Value.ToInteger()); + LUAVALUE_PROP_SET(UInt64Property, Value.ToInteger()); + LUAVALUE_PROP_SET(Int16Property, Value.ToInteger()); + LUAVALUE_PROP_SET(Int8Property, Value.ToInteger()); + LUAVALUE_PROP_SET(ByteProperty, Value.ToInteger()); + LUAVALUE_PROP_SET(UInt16Property, Value.ToInteger()); + + LUAVALUE_PROP_SET(StrProperty, Value.ToString()); + LUAVALUE_PROP_SET(NameProperty, Value.ToName()); + LUAVALUE_PROP_SET(TextProperty, FText::FromString(Value.ToString())); + + LUAVALUE_PROP_SET(ClassProperty, Value.Object); + LUAVALUE_PROP_SET(ObjectProperty, Value.Object); + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FEnumProperty* EnumProperty = CastField(Property); + + if (EnumProperty) + { + uint8* EnumValue = EnumProperty->ContainerPtrToValuePtr(Buffer, Index); + *EnumValue = Value.ToInteger(); + return; + } +#endif + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FObjectPropertyBase* ObjectPropertyBase = CastField(Property); +#else + UObjectPropertyBase* ObjectPropertyBase = Cast(Property); +#endif + if (ObjectPropertyBase) + { + ObjectPropertyBase->SetObjectPropertyValue_InContainer(Buffer, Value.Object, Index); + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FWeakObjectProperty* WeakObjectProperty = CastField(Property); +#else + UWeakObjectProperty* WeakObjectProperty = Cast(Property); +#endif + if (WeakObjectProperty) + { + FWeakObjectPtr WeakPtr(Value.Object); + WeakObjectProperty->SetPropertyValue_InContainer(Buffer, WeakPtr, Index); + return; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + if (FMulticastDelegateProperty* MulticastProperty = CastField(Property)) +#else + if (UMulticastDelegateProperty* MulticastProperty = Cast(Property)) +#endif + { + if (Value.IsNil()) + { + UObject* Object = static_cast(Buffer); + UnregisterLuaDelegatesOfObject(Object); + MulticastProperty->ClearDelegate(Object); + return; + } + + ULuaDelegate* LuaDelegate = NewObject(); + LuaDelegate->SetupLuaDelegate(MulticastProperty->SignatureFunction, this, Value); + RegisterLuaDelegate((UObject*)Buffer, LuaDelegate); + + FScriptDelegate Delegate; + Delegate.BindUFunction(LuaDelegate, FName("LuaDelegateFunction")); + + MulticastProperty->AddDelegate(Delegate, (UObject*)Buffer); + return; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + if (FDelegateProperty* DelegateProperty = CastField(Property)) +#else + if (UDelegateProperty* DelegateProperty = Cast(Property)) +#endif + { + if (Value.IsNil()) + { + UObject* Object = static_cast(Buffer); + UnregisterLuaDelegatesOfObject(Object); + DelegateProperty->SetPropertyValue_InContainer(Buffer, FScriptDelegate(), Index); + return; + } + + ULuaDelegate* LuaDelegate = NewObject(); + LuaDelegate->SetupLuaDelegate(DelegateProperty->SignatureFunction, this, Value); + RegisterLuaDelegate((UObject*)Buffer, LuaDelegate); + + FScriptDelegate Delegate; + Delegate.BindUFunction(LuaDelegate, FName("LuaDelegateFunction")); + + DelegateProperty->SetPropertyValue_InContainer(Buffer, Delegate, Index); + return; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + if (FStructProperty* StructProperty = CastField(Property)) +#else + if (UStructProperty* StructProperty = Cast(Property)) +#endif + { + // fast path + if (StructProperty->Struct == FLuaValue::StaticStruct()) + { + FLuaValue* LuaValuePtr = StructProperty->ContainerPtrToValuePtr(Buffer); + *LuaValuePtr = Value; + return; + } + + const uint8* StructContainer = StructProperty->ContainerPtrToValuePtr(Buffer, Index); + LuaTableToStruct(Value, StructProperty->Struct, (uint8*)StructContainer); + return; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + if (FArrayProperty* ArrayProperty = CastField(Property)) +#else + if (UArrayProperty* ArrayProperty = Cast(Property)) +#endif + { + FScriptArrayHelper_InContainer Helper(ArrayProperty, Buffer, Index); + TArray ArrayValues = ULuaBlueprintFunctionLibrary::LuaTableGetValues(Value); + Helper.Resize(ArrayValues.Num()); + for (int32 ArrayIndex = 0; ArrayIndex < Helper.Num(); ArrayIndex++) + { + uint8* ArrayItemPtr = Helper.GetRawPtr(ArrayIndex); + bool bArrayItemSuccess = false; + ToProperty(ArrayItemPtr, ArrayProperty->Inner, ArrayValues[ArrayIndex], bArrayItemSuccess, 0); + } + return; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + if (FMapProperty* MapProperty = CastField(Property)) +#else + if (UMapProperty* MapProperty = Cast(Property)) +#endif + { + FScriptMapHelper_InContainer Helper(MapProperty, Buffer, Index); + Helper.EmptyValues(); + TArray TableKeys = ULuaBlueprintFunctionLibrary::LuaTableGetKeys(Value); + for (FLuaValue TableKey : TableKeys) + { + int32 NewIndex = Helper.AddUninitializedValue(); + uint8* KeyBuffer = Helper.GetKeyPtr(NewIndex); + uint8* ValueBuffer = Helper.GetValuePtr(NewIndex); + bool bTableItemSuccess = false; + ToProperty(KeyBuffer, Helper.GetKeyProperty(), TableKey, bTableItemSuccess, 0); + ToProperty(ValueBuffer, Helper.GetValueProperty(), TableKey, bTableItemSuccess, 0); + } + return; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + if (FSetProperty* SetProperty = CastField(Property)) +#else + if (USetProperty* SetProperty = Cast(Property)) +#endif + { + FScriptSetHelper_InContainer Helper(SetProperty, Buffer, Index); + TArray ArrayValues = ULuaBlueprintFunctionLibrary::LuaTableGetValues(Value); + Helper.EmptyElements(ArrayValues.Num()); + for (int32 ArrayIndex = 0; ArrayIndex < Helper.Num(); ArrayIndex++) + { + uint8* SetItemPtr = Helper.GetElementPtr(ArrayIndex); + bool bArrayItemSuccess = false; + ToProperty(SetItemPtr, SetProperty->ElementProp, ArrayValues[ArrayIndex], bArrayItemSuccess, 0); + } + return; + } + + bSuccess = false; +} + +void ULuaState::LuaTableToStruct(FLuaValue & LuaValue, UScriptStruct * InScriptStruct, uint8 * StructData) +{ + TArray TableKeys = ULuaBlueprintFunctionLibrary::LuaTableGetKeys(LuaValue); + for (FLuaValue TableKey : TableKeys) + { +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FProperty* StructProp = InScriptStruct->FindPropertyByName(TableKey.ToName()); +#else + UProperty* StructProp = InScriptStruct->FindPropertyByName(TableKey.ToName()); +#endif + if (StructProp) + { + bool bStructValueSuccess = false; + ToProperty((void*)StructData, StructProp, LuaValue.GetField(TableKey.ToString()), bStructValueSuccess, 0); + } + } +} + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 +void ULuaState::ToProperty(void* Buffer, FProperty * Property, FLuaValue Value, bool& bSuccess, int32 Index) +{ + ToFProperty(Buffer, Property, Value, bSuccess, Index); +} + +FLuaValue ULuaState::FromProperty(void* Buffer, FProperty * Property, bool& bSuccess, int32 Index) +{ + return FromFProperty(Buffer, Property, bSuccess, Index); +} +#else +void ULuaState::ToProperty(void* Buffer, UProperty * Property, FLuaValue Value, bool& bSuccess, int32 Index) +{ + ToUProperty(Buffer, Property, Value, bSuccess, Index); +} +FLuaValue ULuaState::FromProperty(void* Buffer, UProperty * Property, bool& bSuccess, int32 Index) +{ + return FromUProperty(Buffer, Property, bSuccess, Index); +} +#endif + + +FLuaValue ULuaState::GetLuaValueFromProperty(UObject * InObject, const FString & PropertyName) +{ + if (!InObject) + { + return FLuaValue(); + } + + UClass* Class = InObject->GetClass(); +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FProperty* Property = nullptr; +#else + UProperty* Property = nullptr; +#endif + Property = Class->FindPropertyByName(*PropertyName); + if (Property) + { + bool bSuccess = false; + return FromProperty(InObject, Property, bSuccess); + } + + return FLuaValue(); +} + +bool ULuaState::SetPropertyFromLuaValue(UObject * InObject, const FString & PropertyName, FLuaValue Value) +{ + if (!InObject) + { + return false; + } + + UClass* Class = InObject->GetClass(); +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FProperty* Property = nullptr; +#else + UProperty* Property = nullptr; +#endif + Property = Class->FindPropertyByName(*PropertyName); + if (Property) + { + bool bSuccess = false; + ToProperty(InObject, Property, Value, bSuccess); + return bSuccess; + } + + return false; +} + +void ULuaState::SetUserDataMetaTable(FLuaValue MetaTable) +{ + UserDataMetaTable = MetaTable; +} + +void ULuaState::SetupAndAssignUserDataMetatable(UObject * Context, TMap&Metatable, lua_State * State) +{ + if (!State) + { + State = this->L; + } + + lua_newtable(State); + lua_pushcfunction(State, ULuaState::MetaTableFunctionUserData__index); + lua_setfield(State, -2, "__index"); + lua_pushcfunction(State, ULuaState::MetaTableFunctionUserData__newindex); + lua_setfield(State, -2, "__newindex"); + lua_pushcfunction(State, ULuaState::MetaTableFunctionUserData__eq); + lua_setfield(State, -2, "__eq"); + if (Context->IsA()) + { + lua_pushcfunction(State, ULuaState::MetaTableFunctionUserData__gc); + lua_setfield(State, -2, "__gc"); + } + + for (TPair& Pair : Metatable) + { + // first check for UFunction + if (Pair.Value.Type == ELuaValueType::UFunction) + { + UObject* FunctionOwner = Context; + if (ULuaComponent* LuaComponent = Cast(Context)) + { + FunctionOwner = LuaComponent->GetOwner(); + } + + if (FunctionOwner) + { + UFunction* Function = FunctionOwner->FindFunction(Pair.Value.FunctionName); + if (Function) + { + FLuaUserData* LuaCallContext = (FLuaUserData*)lua_newuserdata(State, sizeof(FLuaUserData)); + LuaCallContext->Type = ELuaValueType::UFunction; + LuaCallContext->Context = Context; + LuaCallContext->Function = Function; + + lua_newtable(State); + lua_pushcfunction(State, bRawLuaFunctionCall ? ULuaState::MetaTableFunction__rawcall : ULuaState::MetaTableFunction__call); + lua_setfield(State, -2, "__call"); + lua_setmetatable(State, -2); + } + else + { + lua_pushnil(State); + } + } + } + else { + FromLuaValue(Pair.Value, nullptr, State); + } + lua_setfield(State, -2, TCHAR_TO_ANSI(*Pair.Key)); + } + + lua_setmetatable(State, -2); +} + +FLuaValue ULuaState::NewLuaUserDataObject(TSubclassOf LuaUserDataObjectClass, bool bTrackObject) +{ + ULuaUserDataObject* LuaUserDataObject = NewObject(this, LuaUserDataObjectClass); + if (LuaUserDataObject) + { + if (bTrackObject) + { + TrackedLuaUserDataObjects.Add(LuaUserDataObject); + } + LuaUserDataObject->ReceiveLuaUserDataTableInit(); + return FLuaValue(LuaUserDataObject); + } + + return FLuaValue(); +} + +void ULuaState::SetLuaUserDataField(FLuaValue UserData, const FString & Key, FLuaValue Value) +{ + if (UserData.Type != ELuaValueType::UObject || !UserData.Object) + return; + + if (ULuaComponent* LuaComponent = Cast(UserData.Object)) + { + LuaComponent->LuaSetField(Key, Value); + return; + } + + if (ULuaUserDataObject* LuaUserDataObject = Cast(UserData.Object)) + { + LuaUserDataObject->LuaSetField(Key, Value); + return; + } +} + +FLuaValue ULuaState::GetLuaUserDataField(FLuaValue UserData, const FString & Key) +{ + if (UserData.Type != ELuaValueType::UObject || !UserData.Object) + return FLuaValue(); + + if (ULuaComponent* LuaComponent = Cast(UserData.Object)) + { + return LuaComponent->LuaGetField(Key); + } + + if (ULuaUserDataObject* LuaUserDataObject = Cast(UserData.Object)) + { + return LuaUserDataObject->LuaGetField(Key); + } + + return FLuaValue(); +} + +const void* ULuaState::ToPointer(int Index) +{ + return lua_topointer(L, Index); +} + +void ULuaState::LuaStateInit() +{ +} + +void ULuaState::GCLuaDelegatesCheck() +{ + TSet> DeadObjects; + for (TPair, FLuaDelegateGroup>& Pair : LuaDelegatesMap) + { + if (!Pair.Key.IsValid()) + { + DeadObjects.Add(Pair.Key); + } + } + + for (TWeakObjectPtr& WeakObjectPtr : DeadObjects) + { + LuaDelegatesMap.Remove(WeakObjectPtr); + } +} + +void ULuaState::RegisterLuaDelegate(UObject * InObject, ULuaDelegate * InLuaDelegate) +{ + FLuaDelegateGroup* LuaDelegateGroup = LuaDelegatesMap.Find(InObject); + if (LuaDelegateGroup) + { + LuaDelegateGroup->LuaDelegates.Add(InLuaDelegate); + } + else + { + FLuaDelegateGroup NewLuaDelegateGroup; + NewLuaDelegateGroup.LuaDelegates.Add(InLuaDelegate); + LuaDelegatesMap.Add(InObject, NewLuaDelegateGroup); + } +} + +void ULuaState::UnregisterLuaDelegatesOfObject(UObject* InObject) +{ + LuaDelegatesMap.Remove(InObject); +} + +TArray ULuaState::GetPropertiesNames(UObject * InObject) +{ + TArray Names; + + if (!InObject) + { + return Names; + } + + UClass* Class = InObject->GetClass(); + if (!Class) + { + return Names; + } + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + for (TFieldIterator It(Class); It; ++It) +#else + for (TFieldIterator It(Class); It; ++It) +#endif + { + Names.Add((*It)->GetName()); + } + + return Names; +} + +TArray ULuaState::GetFunctionsNames(UObject * InObject) +{ + TArray Names; + + if (!InObject) + { + return Names; + } + + UClass* Class = InObject->GetClass(); + if (!Class) + { + return Names; + } + + for (TFieldIterator It(Class); It; ++It) + { + Names.Add((*It)->GetName()); + } + + return Names; +} + +void ULuaState::AddLuaValueToLuaState(const FString & Name, FLuaValue LuaValue) +{ + SetFieldFromTree(Name, LuaValue, true); +} + +FLuaValue ULuaState::RunString(const FString & CodeString, FString CodePath) +{ + FLuaValue ReturnValue; + if (CodePath.IsEmpty()) + { + CodePath = CodeString; + } + + if (!RunCode(CodeString, CodePath, 1)) + { + if (bLogError) + LogError(LastError); + ReceiveLuaError(LastError); + } + else + { + ReturnValue = ToLuaValue(-1); + } + + Pop(); + return ReturnValue; +} + +void ULuaState::Error(const FString& ErrorString) +{ + luaL_error(L, TCHAR_TO_UTF8(*ErrorString)); +} +``` + +`Source/LuaMachine/Private/LuaSyntaxHighlighter.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + +#include "LuaSyntaxHighlighter.h" +#include "Runtime/Slate/Public/Framework/Text/IRun.h" +#include "Runtime/Slate/Public/Framework/Text/TextLayout.h" +#include "Runtime/Slate/Public/Framework/Text/SlateTextRun.h" + +#define ADD_RULE(rule) TokenizerRules.Add(FSyntaxTokenizer::FRule(TEXT(rule))) + +#define ADD_RULE_BASIC(rule) ADD_RULE(rule); BasicTokens.Add(TEXT(rule)) + +#define ADD_RULE_STDLIB(rule) ADD_RULE(rule); StdLibTokens.Add(TEXT(rule)) + +FLuaMachineSyntaxHighlighterTextLayoutMarshaller::FLuaMachineSyntaxHighlighterTextLayoutMarshaller(TSharedPtr InTokenizer, TArray InBasicTokens, TArray InStdLibTokens, FLuaSyntaxTextStyle InLuaSyntaxTextStyle) : + FSyntaxHighlighterTextLayoutMarshaller(InTokenizer), + BasicTokens(InBasicTokens), + StdLibTokens(InStdLibTokens), + SyntaxTextStyle(InLuaSyntaxTextStyle) +{ + +} + +TSharedRef FLuaMachineSyntaxHighlighterTextLayoutMarshaller::Create(FLuaSyntaxTextStyle LuaSyntaxTextStyle) +{ + TArray TokenizerRules; + + TArray BasicTokens; + TArray StdLibTokens; + + ADD_RULE("--[["); + ADD_RULE("--]]"); + ADD_RULE("--"); + ADD_RULE("[["); + ADD_RULE("]]"); + ADD_RULE("\\'"); + ADD_RULE("\\\""); + ADD_RULE("'"); + ADD_RULE("\""); + + ADD_RULE("nil"); + ADD_RULE("self"); + ADD_RULE("..."); + ADD_RULE("_G"); + ADD_RULE("_VERSION"); + + ADD_RULE("and"); + ADD_RULE("end"); + ADD_RULE("in"); + ADD_RULE("repeat"); + ADD_RULE("break"); + ADD_RULE("false"); + ADD_RULE("local"); + ADD_RULE("return"); + ADD_RULE("do"); + ADD_RULE("for"); + ADD_RULE("then"); + ADD_RULE("else"); + ADD_RULE("elseif"); + ADD_RULE("function"); + ADD_RULE("not"); + ADD_RULE("true"); + ADD_RULE("if"); + ADD_RULE("or"); + ADD_RULE("until"); + ADD_RULE("while"); + + ADD_RULE_BASIC("assert"); + ADD_RULE_BASIC("collectgarbage"); + ADD_RULE_BASIC("require"); + ADD_RULE_BASIC("dofile"); + ADD_RULE_BASIC("error"); + ADD_RULE_BASIC("getmetatable"); + ADD_RULE_BASIC("ipairs"); + ADD_RULE_BASIC("load"); + ADD_RULE_BASIC("loadfile"); + ADD_RULE_BASIC("next"); + ADD_RULE_BASIC("pairs"); + ADD_RULE_BASIC("pcall"); + ADD_RULE_BASIC("print"); + ADD_RULE_BASIC("rawequal"); + ADD_RULE_BASIC("rawget"); + ADD_RULE_BASIC("rawlen"); + ADD_RULE_BASIC("rawset"); + ADD_RULE_BASIC("select"); + ADD_RULE_BASIC("setmetatable"); + ADD_RULE_BASIC("tonumber"); + ADD_RULE_BASIC("tostring"); + ADD_RULE_BASIC("type"); + ADD_RULE_BASIC("xpcall"); + + ADD_RULE_STDLIB("coroutine"); + ADD_RULE_STDLIB("package"); + ADD_RULE_STDLIB("string"); + ADD_RULE_STDLIB("utf8"); + ADD_RULE_STDLIB("table"); + ADD_RULE_STDLIB("math"); + ADD_RULE_STDLIB("io"); + ADD_RULE_STDLIB("file"); + ADD_RULE_STDLIB("os"); + ADD_RULE_STDLIB("debug"); + + for (TPair& Pair : LuaSyntaxTextStyle.CustomTextColorMapping) + { + LuaSyntaxTextStyle.CustomTextStyleMapping.Add(Pair.Key, FTextBlockStyle(LuaSyntaxTextStyle.NormalTextStyle).SetColorAndOpacity(Pair.Value)); + } + + for (TPair& Pair : LuaSyntaxTextStyle.CustomTextStyleMapping) + { + TokenizerRules.Add(FSyntaxTokenizer::FRule(*Pair.Key)); + } + + TokenizerRules.Sort([](const FSyntaxTokenizer::FRule& A, const FSyntaxTokenizer::FRule& B) { + return A.MatchText.Len() > B.MatchText.Len(); + }); + + + return MakeShareable(new FLuaMachineSyntaxHighlighterTextLayoutMarshaller(FSyntaxTokenizer::Create(TokenizerRules), BasicTokens, StdLibTokens, LuaSyntaxTextStyle)); +} + +void FLuaMachineSyntaxHighlighterTextLayoutMarshaller::ParseTokens(const FString& SourceString, FTextLayout& TargetTextLayout, TArray TokenizedLines) +{ + + enum class EParseState : uint8 + { + None, + LookingForSingleLineComment, + LookingForMultiLineComment, + LookingForSingleQuoteString, + LookingForDoubleQuoteString, + LookingForMultiLineString, + }; + + TArray LinesToAdd; + LinesToAdd.Reserve(TokenizedLines.Num()); + + + EParseState ParseState = EParseState::None; + + for (const FSyntaxTokenizer::FTokenizedLine& TokenizedLine : TokenizedLines) + { + TSharedRef ModelString = MakeShareable(new FString()); + TArray> Runs; + + if (ParseState == EParseState::LookingForSingleLineComment) + { + ParseState = EParseState::None; + } + + + + for (const FSyntaxTokenizer::FToken& Token : TokenizedLine.Tokens) + { + const FString TokenString = SourceString.Mid(Token.Range.BeginIndex, Token.Range.Len()); + const FTextRange ModelRange(ModelString->Len(), ModelString->Len() + TokenString.Len()); + + ModelString->Append(TokenString); + + FRunInfo RunInfo(TEXT("SyntaxHighlight.LuaMachine.Normal")); + + FTextBlockStyle CurrentBlockStyle = SyntaxTextStyle.NormalTextStyle; + + bool bIsWhitespace = FString(TokenString).TrimEnd().IsEmpty(); + if (!bIsWhitespace) + { + bool bHasMatchedSyntax = false; + if (Token.Type == FSyntaxTokenizer::ETokenType::Syntax) + { + if (ParseState == EParseState::None) + { + TCHAR NextChar = TEXT(" ")[0]; + TCHAR PrevChar = TEXT(" ")[0]; + if (Token.Range.EndIndex < SourceString.Len()) + { + NextChar = SourceString[Token.Range.EndIndex]; + } + if (Token.Range.BeginIndex > 0) + { + PrevChar = SourceString[Token.Range.BeginIndex - 1]; + } + if (TokenString == TEXT("--")) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.Comment"); + CurrentBlockStyle = SyntaxTextStyle.CommentTextStyle; + ParseState = EParseState::LookingForSingleLineComment; + } + else if (TokenString == TEXT("--[[")) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.Comment"); + CurrentBlockStyle = SyntaxTextStyle.CommentTextStyle; + ParseState = EParseState::LookingForMultiLineComment; + } + else if (TokenString == TEXT("[[")) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.String"); + CurrentBlockStyle = SyntaxTextStyle.StringTextStyle; + ParseState = EParseState::LookingForMultiLineString; + bHasMatchedSyntax = true; + } + else if (TokenString == TEXT("'")) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.String"); + CurrentBlockStyle = SyntaxTextStyle.StringTextStyle; + ParseState = EParseState::LookingForSingleQuoteString; + bHasMatchedSyntax = true; + } + else if (TokenString == TEXT("\"")) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.String"); + CurrentBlockStyle = SyntaxTextStyle.StringTextStyle; + ParseState = EParseState::LookingForDoubleQuoteString; + bHasMatchedSyntax = true; + } + else if (!TChar::IsAlpha(NextChar) && !TChar::IsDigit(NextChar) && !TChar::IsAlpha(PrevChar) && !TChar::IsDigit(PrevChar) && NextChar != TCHAR('_') && PrevChar != TCHAR('_')) + { + if (TokenString == TEXT("nil") || TokenString == TEXT("self") || TokenString == TEXT("_G") || TokenString == TEXT("_VERSION") || TokenString == TEXT("...")) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.Nil"); + CurrentBlockStyle = SyntaxTextStyle.NilTextStyle; + } + else if (BasicTokens.Contains(TokenString)) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.Basic"); + CurrentBlockStyle = SyntaxTextStyle.BasicTextStyle; + } + else if (StdLibTokens.Contains(TokenString)) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.StdLib"); + CurrentBlockStyle = SyntaxTextStyle.StdLibTextStyle; + } + else { + if (SyntaxTextStyle.CustomTextStyleMapping.Contains(TokenString)) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.Custom"); + CurrentBlockStyle = SyntaxTextStyle.CustomTextStyleMapping[TokenString]; + } + else + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.Keyword"); + CurrentBlockStyle = SyntaxTextStyle.KeywordTextStyle; + } + } + ParseState = EParseState::None; + } + } + else if (ParseState == EParseState::LookingForMultiLineComment && TokenString == TEXT("--]]")) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.Comment"); + CurrentBlockStyle = SyntaxTextStyle.CommentTextStyle; + ParseState = EParseState::None; + } + else if (ParseState == EParseState::LookingForMultiLineString && TokenString == TEXT("]]")) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.String"); + CurrentBlockStyle = SyntaxTextStyle.StringTextStyle; + ParseState = EParseState::None; + } + else if (ParseState == EParseState::LookingForSingleQuoteString && TokenString == TEXT("'")) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.String"); + CurrentBlockStyle = SyntaxTextStyle.StringTextStyle; + ParseState = EParseState::None; + } + else if (ParseState == EParseState::LookingForDoubleQuoteString && TokenString == TEXT("\"")) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.String"); + CurrentBlockStyle = SyntaxTextStyle.StringTextStyle; + ParseState = EParseState::None; + } + + } + + if (Token.Type == FSyntaxTokenizer::ETokenType::Literal || !bHasMatchedSyntax) + { + if (ParseState == EParseState::LookingForSingleLineComment) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.Comment"); + CurrentBlockStyle = SyntaxTextStyle.CommentTextStyle; + } + else if (ParseState == EParseState::LookingForMultiLineComment) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.Comment"); + CurrentBlockStyle = SyntaxTextStyle.CommentTextStyle; + } + else if (ParseState == EParseState::LookingForMultiLineString || ParseState == EParseState::LookingForSingleQuoteString || ParseState == EParseState::LookingForDoubleQuoteString) + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.String"); + CurrentBlockStyle = SyntaxTextStyle.StringTextStyle; + } + } + TSharedRef Run = FSlateTextRun::Create(RunInfo, ModelString, CurrentBlockStyle, ModelRange); + Runs.Add(Run); + } + else + { + RunInfo.Name = TEXT("SyntaxHighlight.LuaMachine.WhiteSpace"); + TSharedRef Run = FSlateTextRun::Create(RunInfo, ModelString, SyntaxTextStyle.NormalTextStyle, ModelRange); + Runs.Add(Run); + } + } + + LinesToAdd.Emplace(MoveTemp(ModelString), MoveTemp(Runs)); + } + + TargetTextLayout.AddLines(LinesToAdd); +} +``` + +`Source/LuaMachine/Private/LuaTableAsset.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + +#include "LuaTableAsset.h" + +FLuaValue ULuaTableAsset::ToLuaTable(ULuaState* LuaState) +{ + FLuaValue NewTable = LuaState->CreateLuaTable(); + for (TPair& Pair : Table) + { + NewTable.SetField(Pair.Key, Pair.Value); + } + + return NewTable; +} +``` + +`Source/LuaMachine/Private/LuaUserDataObject.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + +#include "LuaUserDataObject.h" + +TSubclassOf ULuaUserDataObject::GetLuaState() const +{ + ULuaState* LuaState = Cast(GetOuter()); + if (LuaState) + { + return LuaState->GetClass(); + } + return nullptr; +} + +ULuaState* ULuaUserDataObject::GetLuaStateInstance() const +{ + return Cast(GetOuter()); +} + +UWorld* ULuaUserDataObject::GetWorld() const +{ + ULuaState* LuaState = Cast(GetOuter()); + if (LuaState) + { + return LuaState->GetWorld(); + } + return nullptr; +} + +TSharedPtr ULuaUserDataObject::AddLuaSmartReference(FLuaValue Value) +{ + ULuaState* LuaState = Cast(GetOuter()); + if (LuaState) + { + return LuaState->AddLuaSmartReference(Value); + } + return nullptr; +} + +void ULuaUserDataObject::RemoveLuaSmartReference(TSharedPtr Ref) +{ + if (!Ref.IsValid()) + return; + + ULuaState* LuaState = Cast(GetOuter()); + if (LuaState) + { + return LuaState->RemoveLuaSmartReference(Ref.ToSharedRef()); + } +} + +FLuaValue ULuaUserDataObject::LuaGetField(const FString& Name) +{ + ULuaState* LuaState = Cast(GetOuter()); + if (!LuaState) + return FLuaValue(); + + // push component pointer as userdata + LuaState->NewUObject(this, nullptr); + LuaState->SetupAndAssignUserDataMetatable(this, Metatable, nullptr); + + int32 ItemsToPop = LuaState->GetFieldFromTree(Name, false); + FLuaValue ReturnValue = LuaState->ToLuaValue(-1); + + // we need to remove the return value and the object + LuaState->Pop(ItemsToPop + 1); + + return ReturnValue; +} + +void ULuaUserDataObject::LuaSetField(const FString& Name, FLuaValue Value) +{ + ULuaState* LuaState = Cast(GetOuter()); + if (!LuaState) + return; + + // push component pointer as userdata + LuaState->NewUObject(this, nullptr); + LuaState->SetupAndAssignUserDataMetatable(this, Metatable, nullptr); + + LuaState->SetFieldFromTree(Name, Value, false, this); + + // remove UObject + LuaState->Pop(); +} + +TArray ULuaUserDataObject::GetObjectUFunctions(bool bOnlyPublic) +{ + TArray FunctionNames; + + UClass* RelevantClass = GetClass(); + UClass* ParentClass = RelevantClass->GetSuperClass(); + while (ParentClass != ULuaUserDataObject::StaticClass()) + { + RelevantClass = ParentClass; + ParentClass = ParentClass->GetSuperClass(); + } + + for (TFieldIterator It(GetClass()); It; ++It) + { + UFunction* Function = *It; + bool bSuccess = true; + if (bOnlyPublic && !Function->HasAnyFunctionFlags(EFunctionFlags::FUNC_Public)) + { + bSuccess = false; + } + + if (bSuccess) + { + if (Function->GetOuterUClass()->IsChildOf(RelevantClass)) + { + UFunction* SuperFunction = Function->GetSuperFunction(); + if (SuperFunction && !SuperFunction->GetOuterUClass()->IsChildOf(RelevantClass)) + { + continue; + } + FunctionNames.Add(Function->GetName()); + } + } + } + + return FunctionNames; +} + +void ULuaUserDataObject::ReceiveLuaUserDataTableInit_Implementation() +{ + +} + +void ULuaUserDataObject::ReceiveLuaGC_Implementation() +{ + +} + +FLuaValue ULuaUserDataObject::ReceiveLuaMetaIndex_Implementation(FLuaValue Key) +{ + return FLuaValue(); +} + +FLuaValue ULuaUserDataObject::UFunctionToLuaValue(const FString& FunctionName) +{ + FName FunctionFName = FName(*FunctionName); + UFunction* Function = FindFunction(FunctionFName); + if (!Function) + { + return FLuaValue(); + } + + return FLuaValue::Function(FunctionFName); +} + +FLuaValue ULuaUserDataObject::LuaCallFunction(const FString& Name, TArray Args, bool bGlobal) +{ + FLuaValue ReturnValue; + + ULuaState* L = GetLuaStateInstance(); + if (!L) + return ReturnValue; + + // push userdata pointer as userdata + L->NewUObject(this, nullptr); + L->SetupAndAssignUserDataMetatable(this, Metatable, nullptr); + + int32 ItemsToPop = L->GetFieldFromTree(Name, bGlobal); + + // first argument (self/actor) + L->PushValue(-(ItemsToPop + 1)); + int NArgs = 1; + for (FLuaValue& Arg : Args) + { + L->FromLuaValue(Arg); + NArgs++; + } + + L->PCall(NArgs, ReturnValue); + + // the return value and the function has been removed, so we do not need to change ItemsToPop + L->Pop(ItemsToPop + 1); + + return ReturnValue; +} + +``` + +`Source/LuaMachine/Private/LuaValue.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + +#include "LuaValue.h" +#include "LuaState.h" +#include "Misc/Base64.h" + +FString FLuaValue::ToString() const +{ + switch (Type) + { + case ELuaValueType::Bool: + return Bool ? FString(TEXT("true")) : FString(TEXT("false")); + case ELuaValueType::Integer: + return FString::Printf(TEXT("%lld"), Integer); + case ELuaValueType::Number: + return FString::SanitizeFloat(Number); + case ELuaValueType::String: + return String; + case ELuaValueType::Table: + return FString::Printf(TEXT("table: %d"), LuaRef); + case ELuaValueType::Function: + return FString::Printf(TEXT("function: %d"), LuaRef); + case ELuaValueType::UObject: + return Object->GetFullName(); + case ELuaValueType::UFunction: + return Object ? (FunctionName.ToString() + " @ " + Object->GetClass()->GetPathName()) : FunctionName.ToString(); + case ELuaValueType::Thread: + return FString::Printf(TEXT("thread: %d"), LuaRef); + } + return FString(TEXT("nil")); +} + +FName FLuaValue::ToName() const +{ + return FName(*ToString()); +} + +int64 FLuaValue::ToInteger() const +{ + switch (Type) + { + case ELuaValueType::Bool: + return Bool ? 1 : 0; + case ELuaValueType::Integer: + return Integer; + case ELuaValueType::Number: + return Number; + case ELuaValueType::String: + return FCString::Atoi(*String); + } + return 0; +} + +double FLuaValue::ToFloat() const +{ + switch (Type) + { + case ELuaValueType::Bool: + return Bool ? 1.0 : 0.0; + case ELuaValueType::Integer: + return Integer; + case ELuaValueType::Number: + return Number; + case ELuaValueType::String: + return FCString::Atod(*String); + } + return 0.0; +} + +bool FLuaValue::ToBool() const +{ + switch (Type) + { + case ELuaValueType::Nil: + return false; + case ELuaValueType::Bool: + return Bool; + case ELuaValueType::Integer: + return Integer != 0; + case ELuaValueType::Number: + return Number != 0; + } + return true; +} + +void FLuaValue::Unref() +{ + if (!LuaState.IsValid()) + { + LuaRef = LUA_NOREF; + return; + } + + if (Type == ELuaValueType::Table || Type == ELuaValueType::Function || Type == ELuaValueType::Thread) + { + if (LuaRef != LUA_NOREF) + { + // special case for when the engine is shutting down +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 24 + if (IsEngineExitRequested()) +#else + if (GIsRequestingExit) +#endif + { + if (!LuaState->IsValidLowLevel()) + { + LuaRef = LUA_NOREF; + return; + } + } + // use UnrefCheck here to support moving of LuaState + LuaState->UnrefChecked(LuaRef); + } + LuaRef = LUA_NOREF; + } +} + +FLuaValue::~FLuaValue() +{ + Unref(); +} + +FLuaValue::FLuaValue(const FLuaValue& SourceValue) +{ + Type = SourceValue.Type; + Object = SourceValue.Object; + LuaRef = SourceValue.LuaRef; + LuaState = SourceValue.LuaState; + Bool = SourceValue.Bool; + Integer = SourceValue.Integer; + Number = SourceValue.Number; + String = SourceValue.String; + FunctionName = SourceValue.FunctionName; + MulticastScriptDelegate = SourceValue.MulticastScriptDelegate; + + // make a new reference to the table, to avoid it being destroyed + if (LuaRef != LUA_NOREF) + { + LuaState->GetRef(LuaRef); + LuaRef = LuaState->NewRef(); + } +} + +FLuaValue& FLuaValue::operator = (const FLuaValue& SourceValue) +{ + Type = SourceValue.Type; + Object = SourceValue.Object; + LuaRef = SourceValue.LuaRef; + LuaState = SourceValue.LuaState; + Bool = SourceValue.Bool; + Integer = SourceValue.Integer; + Number = SourceValue.Number; + String = SourceValue.String; + FunctionName = SourceValue.FunctionName; + MulticastScriptDelegate = SourceValue.MulticastScriptDelegate; + + // make a new reference to the table, to avoid it being destroyed + if (LuaRef != LUA_NOREF) + { + LuaState->GetRef(LuaRef); + LuaRef = LuaState->NewRef(); + } + + return *this; +} + +FLuaValue FLuaValue::SetField(const FString& Key, FLuaValue Value) +{ + if (Type != ELuaValueType::Table) + return *this; + + if (!LuaState.IsValid()) + return *this; + + LuaState->FromLuaValue(*this); + LuaState->FromLuaValue(Value); + LuaState->SetField(-2, TCHAR_TO_ANSI(*Key)); + LuaState->Pop(); + return *this; +} + +FLuaValue FLuaValue::SetField(const FString& Key, lua_CFunction CFunction) +{ + if (Type != ELuaValueType::Table) + return *this; + + if (!LuaState.IsValid()) + return *this; + + LuaState->FromLuaValue(*this); + LuaState->PushCFunction(CFunction); + LuaState->SetField(-2, TCHAR_TO_ANSI(*Key)); + LuaState->Pop(); + return *this; +} + +FLuaValue FLuaValue::SetMetaTable(FLuaValue MetaTable) +{ + if (Type != ELuaValueType::Table || MetaTable.Type != ELuaValueType::Table) + return *this; + + if (!LuaState.IsValid()) + return *this; + + LuaState->FromLuaValue(*this); + LuaState->FromLuaValue(MetaTable); + LuaState->SetMetaTable(-2); + LuaState->Pop(); + + return *this; +} + + +FLuaValue FLuaValue::GetField(const FString& Key) +{ + if (Type != ELuaValueType::Table) + return FLuaValue(); + + if (!LuaState.IsValid()) + return FLuaValue(); + + LuaState->FromLuaValue(*this); + LuaState->GetField(-1, TCHAR_TO_ANSI(*Key)); + FLuaValue ReturnValue = LuaState->ToLuaValue(-1); + LuaState->Pop(2); + return ReturnValue; +} + +FLuaValue FLuaValue::GetFieldByIndex(const int32 Index) +{ + if (Type != ELuaValueType::Table) + { + return FLuaValue(); + } + + if (!LuaState.IsValid()) + { + return FLuaValue(); + } + + LuaState->FromLuaValue(*this); + LuaState->RawGetI(-1, Index); + FLuaValue ReturnValue = LuaState->ToLuaValue(-1); + LuaState->Pop(2); + return ReturnValue; +} + +FLuaValue FLuaValue::SetFieldByIndex(const int32 Index, FLuaValue Value) +{ + if (Type != ELuaValueType::Table) + { + return *this; + } + + if (!LuaState.IsValid()) + { + return *this; + } + + LuaState->FromLuaValue(*this); + LuaState->FromLuaValue(Value); + LuaState->RawSetI(-2, Index); + LuaState->Pop(); + return *this; +} + +bool FLuaValue::IsReferencedInLuaRegistry() const +{ + return LuaRef != LUA_NOREF; +} + +bool FLuaValue::IsNil() const +{ + return Type == ELuaValueType::Nil; +} + +FLuaValue FLuaValue::FromJsonValue(ULuaState* L, FJsonValue& JsonValue) +{ + if (JsonValue.Type == EJson::String) + { + return FLuaValue(JsonValue.AsString()); + } + else if (JsonValue.Type == EJson::Number) + { + return FLuaValue(JsonValue.AsNumber()); + } + else if (JsonValue.Type == EJson::Boolean) + { + return FLuaValue(JsonValue.AsBool()); + } + else if (JsonValue.Type == EJson::Array) + { + FLuaValue LuaArray = L->CreateLuaTable(); + int32 Index = 1; + auto JsonValues = JsonValue.AsArray(); + for (auto JsonItem : JsonValues) + { + FLuaValue LuaItem; + if (JsonItem.IsValid()) + { + LuaItem = FromJsonValue(L, *JsonItem); + } + LuaArray.SetFieldByIndex(Index++, LuaItem); + } + return LuaArray; + } + else if (JsonValue.Type == EJson::Object) + { + FLuaValue LuaTable = L->CreateLuaTable(); + auto JsonObject = JsonValue.AsObject(); + for (TPair> Pair : JsonObject->Values) + { + FLuaValue LuaItem; + if (Pair.Value.IsValid()) + { + LuaItem = FromJsonValue(L, *Pair.Value); + } + LuaTable.SetField(Pair.Key, LuaItem); + } + return LuaTable; + } + + // default to nil + return FLuaValue(); +} + +TSharedPtr FLuaValue::ToJsonValue() +{ + switch (Type) + { + case ELuaValueType::Integer: + return MakeShared(Integer); + case ELuaValueType::Number: + return MakeShared(Number); + case ELuaValueType::String: + return MakeShared(String); + case ELuaValueType::UFunction: + return MakeShared(FunctionName.ToString()); + case ELuaValueType::UObject: + return MakeShared(Object ? Object->GetFullName() : ""); + case ELuaValueType::Table: + { + + ULuaState* L = LuaState.Get(); + if (!L) + return MakeShared(); + + bool bIsArray = true; + + TArray> Items; + L->FromLuaValue(*this); // push the table + L->PushNil(); // first key + while (L->Next(-2)) + { + auto Key = L->ToLuaValue(-2); + auto Value = L->ToLuaValue(-1); + Items.Add(TPair(Key, Value)); + if (Key.Type != ELuaValueType::Integer) + { + bIsArray = false; + } + L->Pop(); // pop the value + } + L->Pop(); // pop the table + + // check if it is a valid lua "array" + if (bIsArray) + { + TArray> JsonValues; + int32 Index = 1; + for (;;) + { + FLuaValue Item = GetFieldByIndex(Index++); + if (Item.Type == ELuaValueType::Nil) + break; + JsonValues.Add(Item.ToJsonValue()); + } + return MakeShared(JsonValues); + } + + auto JsonObject = MakeShared(); + for (auto Pair : Items) + { + JsonObject->SetField(Pair.Key.ToString(), Pair.Value.ToJsonValue()); + } + auto JsonValueObject = MakeShared(JsonObject); + return JsonValueObject; + } + return MakeShared(); + } + + return MakeShared(); +} + +TArray FLuaValue::ToBytes() const +{ + TArray Bytes; + if (Type != ELuaValueType::String) + return Bytes; + + const int32 StringLength = String.Len(); + Bytes.AddUninitialized(StringLength); + for (int32 i = 0; i < StringLength; i++) + { + uint16 CharValue = (uint16)String[i]; + if (CharValue == 0xffff) + { + Bytes[i] = 0; + } + else + { + Bytes[i] = (uint8)String[i]; + } + } + + return Bytes; +} + +FLuaValue FLuaValue::FromBase64(const FString& Base64) +{ + TArray Bytes; + FBase64::Decode(Base64, Bytes); + return FLuaValue(Bytes); +} + +FString FLuaValue::ToBase64() const +{ + return FBase64::Encode(ToBytes()); +} +``` + +`Source/LuaMachine/Public/LuaBlueprintFunctionLibrary.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "LuaState.h" +#include "LuaValue.h" +#include "LuaTableAsset.h" +#include "UObject/TextProperty.h" +#include "Runtime/Engine/Classes/Engine/World.h" +#include "Runtime/Online/HTTP/Public/HttpModule.h" +#include "Sound/SoundWave.h" +#include "LuaBlueprintFunctionLibrary.generated.h" + +/** + * + */ + +DECLARE_DYNAMIC_DELEGATE_ThreeParams(FLuaHttpSuccess, FLuaValue, ReturnValue, bool, bWasSuccessful, int32, StatusCode); +DECLARE_DYNAMIC_DELEGATE_TwoParams(FLuaHttpResponseReceived, FLuaValue, Context, FLuaValue, Response); +DECLARE_DYNAMIC_DELEGATE_OneParam(FLuaHttpError, FLuaValue, Context); + +UENUM(BlueprintType) +enum class ELuaReflectionType : uint8 +{ + Unknown, + Property, + Function, +}; + + +UCLASS() +class LUAMACHINE_API ULuaBlueprintFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static FLuaValue LuaCreateNil(); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static FLuaValue LuaCreateString(const FString& String); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static FLuaValue LuaCreateNumber(const float Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static FLuaValue LuaCreateInteger(const int32 Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static FLuaValue LuaCreateInteger64(const int64 Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static FLuaValue LuaCreateBool(const bool bInBool); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static FLuaValue LuaCreateTable(UObject* WorldContextObject, TSubclassOf State); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static FLuaValue LuaCreateThread(UObject* WorldContextObject, TSubclassOf State, FLuaValue Value /* Function */); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static FLuaValue LuaCreateObject(UObject* InObject); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static FLuaValue LuaCreateObjectInState(UObject* WorldContextObject, TSubclassOf State, UObject* InObject); + + UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject"), Category="Lua") + static FLuaValue LuaGetGlobal(UObject* WorldContextObject, TSubclassOf State, const FString& Name); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static void LuaSetGlobal(UObject* WorldContextObject, TSubclassOf State, const FString& Name, FLuaValue Value); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static void LuaSetUserDataMetaTable(UObject* WorldContextObject, TSubclassOf State, FLuaValue MetaTable); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static FLuaValue AssignLuaValueToLuaState(UObject* WorldContextObject, FLuaValue Value, TSubclassOf State); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static FLuaValue LuaTableGetField(FLuaValue Table, const FString& Key); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static FLuaValue GetLuaComponentAsLuaValue(AActor* Actor); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static FLuaValue GetLuaComponentByStateAsLuaValue(AActor* Actor, TSubclassOf State); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static FLuaValue GetLuaComponentByNameAsLuaValue(AActor* Actor, const FString& Name); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static FLuaValue GetLuaComponentByStateAndNameAsLuaValue(AActor* Actor, TSubclassOf State, const FString& Name); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static FLuaValue LuaComponentGetField(FLuaValue LuaComponent, const FString& Key); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static FLuaValue LuaTableGetByIndex(FLuaValue Table, const int32 Index); + + /* Assigns a value to a table index, returned value is the table itself */ + UFUNCTION(BlueprintCallable, Category="Lua") + static FLuaValue LuaTableSetByIndex(FLuaValue Table, const int32 Index, FLuaValue Value); + + /* Returns the array of keys in the table */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static TArray LuaTableGetKeys(FLuaValue Table); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static TArray LuaTableGetValues(FLuaValue Table); + + /* Assigns a value to a table key, returned value is the table itself */ + UFUNCTION(BlueprintCallable, Category="Lua") + static FLuaValue LuaTableSetField(FLuaValue Table, const FString& Key, FLuaValue Value); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", AutoCreateRefTerm = "Args"), Category="Lua") + static FLuaValue LuaGlobalCall(UObject* WorldContextObject, TSubclassOf State, const FString& Name, TArray Args); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", AutoCreateRefTerm = "Args"), Category="Lua") + static TArray LuaGlobalCallMulti(UObject* WorldContextObject, TSubclassOf State, const FString& Name, TArray Args); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", AutoCreateRefTerm = "Args"), Category="Lua") + static FLuaValue LuaGlobalCallValue(UObject* WorldContextObject, TSubclassOf State, FLuaValue Value, TArray Args); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", AutoCreateRefTerm = "Args"), Category="Lua") + static TArray LuaGlobalCallValueMulti(UObject* WorldContextObject, TSubclassOf State, FLuaValue Value, TArray Args); + + UFUNCTION(BlueprintPure, meta = (WorldContext = "WorldContextObject", AutoCreateRefTerm = "Args"), Category = "Lua") + static ULuaState* LuaGetState(UObject* WorldContextObject, TSubclassOf State); + + /* Calls a lua value (must be callable) */ + UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "Args"), Category="Lua") + static FLuaValue LuaValueCall(FLuaValue Value, TArray Args); + + /* Calls a lua value (must be callable and not nil) */ + UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "Args"), Category="Lua") + static FLuaValue LuaValueCallIfNotNil(FLuaValue Value, TArray Args); + + /* Calls a lua value taken from a table by key (must be callable) */ + UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "Args"), Category="Lua") + static FLuaValue LuaTableKeyCall(FLuaValue InTable, const FString& Key, TArray Args); + + /* Calls a lua value taken from a table by key (must be callable), passing the table itself as the first argument (useful for table:function syntax) */ + UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "Args"), Category = "Lua") + static FLuaValue LuaTableKeyCallWithSelf(FLuaValue InTable, const FString& Key, TArray Args); + + /* Calls a lua value taken from a table by index (must be callable) */ + UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "Args"), Category="Lua") + static FLuaValue LuaTableIndexCall(FLuaValue InTable, const int32 Index, TArray Args); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static TArray LuaTableUnpack(FLuaValue InTable); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static FLuaValue LuaTablePack(UObject* WorldContextObject, TSubclassOf State, TArray Values); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static FLuaValue LuaTableMergePack(UObject* WorldContextObject, TSubclassOf State, TArray Values1, TArray Values2); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static TArray LuaTableMergeUnpack(FLuaValue InTable1, FLuaValue InTable2); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static void LuaTableFillObject(FLuaValue InTable, UObject* InObject); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static FLuaValue LuaTableFromMap(UObject* WorldContextObject, TSubclassOf State, TMap Map); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static TArray LuaTableRange(FLuaValue InTable, const int32 First, const int32 Last); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static TArray LuaValueArrayMerge(TArray Array1, TArray Array2); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static TArray LuaValueArrayAppend(TArray Array, FLuaValue Value); + + /* Calls a lua value with multiple return values (must be callable) */ + UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "Args"), Category="Lua") + static TArray LuaValueCallMulti(FLuaValue Value, TArray Args); + + /* Resume a lua coroutine/thread with multiple return values (must be callable) */ + UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "Args"), Category = "Lua") + static TArray LuaValueResumeMulti(FLuaValue Value, TArray Args); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static ELuaThreadStatus LuaThreadGetStatus(FLuaValue Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static int32 LuaThreadGetStackTop(FLuaValue Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static int32 LuaValueLength(FLuaValue Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static bool LuaValueIsReferencedInLuaRegistry(FLuaValue Value); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static UClass* LuaValueToBlueprintGeneratedClass(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static UObject* LuaValueLoadObject(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static UClass* LuaValueLoadClass(const FLuaValue& Value, const bool bDetectBlueprintGeneratedClass); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static bool LuaValueFromJson(UObject* WorldContextObject, TSubclassOf State, const FString& Json, FLuaValue& Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static FString LuaValueToJson(FLuaValue Value); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static FLuaValue LuaValueFromBase64(const FString& Base64); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static FString LuaValueToBase64(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static FLuaValue LuaValueFromUTF16(const FString& String); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static FString LuaValueToUTF16(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static FLuaValue LuaValueFromUTF8(const FString& String); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static FString LuaValueToUTF8(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static FLuaValue LuaValueFromUTF32(const FString& String); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static FString LuaValueToUTF32(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static int64 LuaValueToPointer(UObject* WorldContextObject, TSubclassOf State, FLuaValue Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static FString LuaValueToHexPointer(UObject* WorldContextObject, TSubclassOf State, FLuaValue Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static int32 LuaGetTop(UObject* WorldContextObject, TSubclassOf State); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static void LuaStateDestroy(UObject* WorldContextObject, TSubclassOf State); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static void LuaStateReload(UObject* WorldContextObject, TSubclassOf State); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static FLuaValue LuaRunFile(UObject* WorldContextObject, TSubclassOf State, const FString& Filename, const bool bIgnoreNonExistent); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static FLuaValue LuaRunNonContentFile(UObject* WorldContextObject, TSubclassOf State, const FString& Filename, const bool bIgnoreNonExistent); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static FLuaValue LuaRunCodeAsset(UObject* WorldContextObject, TSubclassOf State, ULuaCode* CodeAsset); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static FLuaValue LuaRunByteCode(UObject* WorldContextObject, TSubclassOf State, const TArray& ByteCode, const FString& CodePath); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static FLuaValue LuaRunString(UObject* WorldContextObject, TSubclassOf State, const FString& CodeString, FString CodePath=""); + + /* Make an HTTP GET request to the specified URL to download the Lua script to run */ + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", AutoCreateRefTerm = "Headers"), Category = "Lua") + static void LuaRunURL(UObject* WorldContextObject, TSubclassOf State, const FString& URL, TMap Headers, const FString& SecurityHeader, const FString& SignaturePublicExponent, const FString& SignatureModulus, FLuaHttpSuccess Completed); + + /* Make an HTTP GET request to the specified URL to download the Lua script to run */ + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", AutoCreateRefTerm = "Headers,Error,ResponseReceived"), Category = "Lua") + static void LuaHttpRequest(UObject* WorldContextObject, TSubclassOf State, const FString& Method, const FString& URL, TMap Headers, FLuaValue Body, FLuaValue Context, const FLuaHttpResponseReceived& ResponseReceived, const FLuaHttpError& Error); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static UTexture2D* LuaValueToTransientTexture(const int32 Width, const int32 Height, const FLuaValue& Value, const EPixelFormat PixelFormat = EPixelFormat::PF_B8G8R8A8, bool bDetectFormat = false); + + + UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "Args"), Category = "Lua") + static void LuaValueYield(FLuaValue Value, TArray Args); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static bool LuaLoadPakFile(const FString& Filename, FString Mountpoint, TArray& Assets, FString ContentPath, FString AssetRegistryPath); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static FLuaValue LuaNewLuaUserDataObject(UObject* WorldContextObject, TSubclassOf State, TSubclassOf UserDataObjectClass, bool bTrackObject=true); + + UFUNCTION(BlueprintCallable, BlueprintPure, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static int32 LuaGetUsedMemory(UObject* WorldContextObject, TSubclassOf State); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static void LuaGCCollect(UObject* WorldContextObject, TSubclassOf State); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static void LuaGCStop(UObject* WorldContextObject, TSubclassOf State); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category="Lua") + static void LuaGCRestart(UObject* WorldContextObject, TSubclassOf State); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static FLuaValue LuaTableAssetToLuaTable(UObject* WorldContextObject, TSubclassOf State, ULuaTableAsset* TableAsset); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static bool LuaTableImplements(FLuaValue Table, ULuaTableAsset* TableAsset); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static bool LuaTableImplementsAll(FLuaValue Table, TArray TableAssets); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static bool LuaTableImplementsAny(FLuaValue Table, TArray TableAssets); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static bool LuaValueIsNil(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static bool LuaValueIsOwned(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static TSubclassOf LuaValueGetOwner(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static bool LuaValueIsNotNil(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static bool LuaValueIsTable(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static bool LuaValueIsBoolean(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static bool LuaValueIsNumber(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static bool LuaValueIsInteger(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static bool LuaValueIsString(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static bool LuaValueIsFunction(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + static bool LuaValueIsThread(const FLuaValue& Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static FVector LuaTableToVector(FLuaValue Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static FRotator LuaTableToRotator(FLuaValue Value); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static FLuaValue LuaTableSetMetaTable(FLuaValue InTable, FLuaValue InMetaTable); + + UFUNCTION(BlueprintPure, meta=(DisplayName = "To String (LuaValue)", BlueprintAutocast), Category="Lua") + static FString Conv_LuaValueToString(const FLuaValue& Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Text (LuaValue)", BlueprintAutocast), Category="Lua") + static FText Conv_LuaValueToText(const FLuaValue& Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Name (LuaValue)", BlueprintAutocast), Category="Lua") + static FName Conv_LuaValueToName(const FLuaValue& Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Object (LuaValue)", BlueprintAutocast), Category="Lua") + static UObject* Conv_LuaValueToObject(const FLuaValue& Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Class (LuaValue)", BlueprintAutocast), Category="Lua") + static UClass* Conv_LuaValueToClass(const FLuaValue& Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To LuaValue (Object)", BlueprintAutocast), Category="Lua") + static FLuaValue Conv_ObjectToLuaValue(UObject* Object); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To LuaValue (Float)", BlueprintAutocast), Category="Lua") + static FLuaValue Conv_FloatToLuaValue(const float Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Int (LuaValue)", BlueprintAutocast), Category="Lua") + static int32 Conv_LuaValueToInt(const FLuaValue& Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Int64 (LuaValue)", BlueprintAutocast), Category = "Lua") + static int64 Conv_LuaValueToInt64(const FLuaValue& Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Float (LuaValue)", BlueprintAutocast), Category="Lua") + static float Conv_LuaValueToFloat(const FLuaValue& Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Vector (LuaValue)", BlueprintAutocast), Category = "Lua") + static FVector Conv_LuaValueToFVector(const FLuaValue& Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Rotator (LuaValue)", BlueprintAutocast), Category = "Lua") + static FRotator Conv_LuaValueToFRotator(const FLuaValue& Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Bool (LuaValue)", BlueprintAutocast), Category="Lua") + static bool Conv_LuaValueToBool(const FLuaValue& Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To LuaValue (Int)", BlueprintAutocast), Category="Lua") + static FLuaValue Conv_IntToLuaValue(const int32 Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To LuaValue (Int64)", BlueprintAutocast), Category = "Lua") + static FLuaValue Conv_Int64ToLuaValue(const int64 Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To LuaValue (String)", BlueprintAutocast), Category="Lua") + static FLuaValue Conv_StringToLuaValue(const FString& Value); + + UFUNCTION(BlueprintPure, meta = (BDisplayName = "To LuaValue (Text)", lueprintAutocast), Category="Lua") + static FLuaValue Conv_TextToLuaValue(const FText& Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To LuaValue (Name)", BlueprintAutocast), Category="Lua") + static FLuaValue Conv_NameToLuaValue(const FName Value); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To LuaValue (Bool)", BlueprintAutocast), Category="Lua") + static FLuaValue Conv_BoolToLuaValue(const bool Value); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static FLuaValue LuaCreateLazyTable(UObject* WorldContextObject, TSubclassOf State); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static FLuaValue LuaCreateUFunction(UObject* InObject, const FString& FunctionName); + + UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "LuaValueTypes"), Category = "Lua") + static void SwitchOnLuaValueType(const FLuaValue& LuaValue, ELuaValueType& LuaValueTypes); + + UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "LuaReflectionTypes"), Category = "Lua") + static void GetLuaReflectionType(UObject* InObject, const FString& Name, ELuaReflectionType& LuaReflectionTypes); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static void RegisterLuaConsoleCommand(const FString& CommandName, const FLuaValue& LuaConsoleCommand); + + UFUNCTION(BlueprintCallable, Category = "Lua") + static void UnregisterLuaConsoleCommand(const FString& CommandName); + + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"), Category = "Lua") + static ULuaState* CreateDynamicLuaState(UObject* WorldContextObject, TSubclassOf LuaStateClass); + +private: + static void HttpRequestDone(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, TSubclassOf LuaState, TWeakObjectPtr World, const FString SecurityHeader, const FString SignaturePublicExponent, const FString SignatureModulus, FLuaHttpSuccess Completed); + static void HttpGenericRequestDone(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, TWeakPtr Context, FLuaHttpResponseReceived ResponseReceived, FLuaHttpError Error); + +}; + +``` + +`Source/LuaMachine/Public/LuaBlueprintPackage.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "LuaState.h" +#include "LuaBlueprintPackage.generated.h" + +/** + * + */ +UCLASS(Abstract, Blueprintable, HideDropdown) +class LUAMACHINE_API ULuaBlueprintPackage : public UObject +{ + GENERATED_BODY() + + +public: + UPROPERTY(EditAnywhere, Category = "Lua") + TMap Table; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + TSubclassOf GetLuaState() const; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + ULuaState* GetLuaStateInstance() const; + + virtual UWorld* GetWorld() const override; + + UPROPERTY() + FLuaValue SelfTable; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + FLuaValue GetSelfLuaTable() const { return SelfTable; } + + virtual void Init(); + + UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "Init"), Category = "Lua") + void ReceiveInit(); +}; + +``` + +`Source/LuaMachine/Public/LuaCode.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris +// Reimport system by yama2akira (Akira Yamamoto) + +#pragma once + +#include "CoreMinimal.h" +#include "Runtime/Launch/Resources/Version.h" +#include "Engine/DataAsset.h" +#if ENGINE_MAJOR_VERSION > 4 +#include "UObject/ObjectSaveContext.h" +#endif +#include "LuaCode.generated.h" + +struct LUAMACHINE_API FLuaCodeObjectVersion +{ + enum Type + { + // Before any version changes were made + BeforeCustomVersionWasAdded = 0, + + // Fixed duplication of properties + FixDuplicationOfProperties, + + VersionPlusOne, + LatestVersion = VersionPlusOne - 1 + + }; + + // The GUID for this custom version number + const static FGuid GUID; + +private: + FLuaCodeObjectVersion() {} +}; + + +/** + * + */ +UCLASS(BlueprintType) +class LUAMACHINE_API ULuaCode : public UDataAsset +{ + GENERATED_BODY() + +public: + + ULuaCode(); + + UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="Lua") + FText Code; + + UPROPERTY(EditAnywhere, Category="Lua") + bool bCookAsBytecode; + + UPROPERTY() + TArray ByteCode; + + UPROPERTY() + bool bCooked; + + virtual void Serialize(FArchive& Ar) override; + +#if ENGINE_MAJOR_VERSION > 4 + virtual void PreSave(FObjectPreSaveContext ObjectSaveContext) override; +#else + virtual void PreSave(const ITargetPlatform* TargetPlatform) override; +#endif + +#if WITH_EDITORONLY_DATA + virtual void PostInitProperties() override; +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 4 + virtual void GetAssetRegistryTags(FAssetRegistryTagsContext Context) const override; +#else + virtual void GetAssetRegistryTags(TArray& OutTags) const override; +#endif + + + UPROPERTY(VisibleAnywhere, Instanced, Category = Reimport) + class UAssetImportData* AssetImportData; +#endif +}; + +``` + +`Source/LuaMachine/Public/LuaCommandExecutor.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "HAL/IConsoleManager.h" +#include "Framework/Commands/InputChord.h" +#include "Runtime/Launch/Resources/Version.h" + +class ULuaState; + +/** + * + */ +class LUAMACHINE_API FLuaCommandExecutor : public IConsoleCommandExecutor +{ +public: + FLuaCommandExecutor(); + ~FLuaCommandExecutor(); + + FName GetName() const override; + FText GetDisplayName() const override; + FText GetDescription() const override; + FText GetHintText() const override; + void GetAutoCompleteSuggestions(const TCHAR* Input, TArray& Out) override; + void GetExecHistory(TArray& Out) override; + bool Exec(const TCHAR* Input) override; + bool AllowHotKeyClose() const override; + bool AllowMultiLine() const override; + FInputChord GetHotKey() const override; + +#if ENGINE_MAJOR_VERSION > 4 && ENGINE_MINOR_VERSION > 0 + FInputChord GetIterateExecutorHotKey() const + { + return {}; + } +#endif + +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 5 + void GetSuggestedCompletions(const TCHAR* Input, TArray& Out) override {} +#endif + + ULuaState* LuaState; +}; + +``` + +`Source/LuaMachine/Public/LuaComponent.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "LuaState.h" +#include "LuaValue.h" +#include "LuaComponent.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLuaComponentError, FString, Message); + + +UCLASS(Blueprintable, ClassGroup=(Scripting), meta=(BlueprintSpawnableComponent) ) +class LUAMACHINE_API ULuaComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + // Sets default values for this component's properties + ULuaComponent(); + +protected: + // Called when the game starts + virtual void BeginPlay() override; + +public: + // Called every frame + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Lua") + TSubclassOf LuaState; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Lua") + TMap Table; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Lua") + TMap Metatable; + + UPROPERTY(EditAnywhere, Category="Lua") + bool bLazy; + + UPROPERTY(EditAnywhere, Category="Lua") + bool bLogError; + + UPROPERTY(EditAnywhere, Category = "Lua") + bool bImplicitSelf; + + UPROPERTY(EditAnywhere, Category = "Lua") + TArray GlobalNames; + + UFUNCTION(BlueprintCallable, Category="Lua", meta = (AutoCreateRefTerm = "Args")) + FLuaValue LuaCallFunction(const FString& Name, TArray Args, bool bGlobal); + + UFUNCTION(BlueprintCallable, Category="Lua", meta = (AutoCreateRefTerm = "Args")) + FLuaValue LuaCallValue(FLuaValue Value, TArray Args); + + UFUNCTION(BlueprintCallable, Category="Lua", meta = (AutoCreateRefTerm = "Args")) + FLuaValue LuaCallValueIfNotNil(FLuaValue Value, TArray Args); + + UFUNCTION(BlueprintCallable, Category="Lua", meta = (AutoCreateRefTerm = "Args")) + FLuaValue LuaCallTableKey(FLuaValue InTable, FString Key, TArray Args); + + UFUNCTION(BlueprintCallable, Category="Lua", meta = (AutoCreateRefTerm = "Args")) + FLuaValue LuaCallTableIndex(FLuaValue InTable, int32 Index, TArray Args); + + UFUNCTION(BlueprintCallable, Category="Lua", meta = (AutoCreateRefTerm = "Args")) + TArray LuaCallFunctionMulti(FString Name, TArray Args, bool bGlobal); + + UFUNCTION(BlueprintCallable, Category="Lua", meta = (AutoCreateRefTerm = "Args")) + TArray LuaCallValueMulti(FLuaValue Value, TArray Args); + + UFUNCTION(BlueprintCallable, Category="Lua", meta = (AutoCreateRefTerm = "Args")) + TArray LuaCallValueMultiIfNotNil(FLuaValue Value, TArray Args); + + UFUNCTION(BlueprintCallable, Category="Lua", meta = (AutoCreateRefTerm = "Args")) + TArray LuaCallTableKeyMulti(FLuaValue InTable, FString Key, TArray Args); + + UFUNCTION(BlueprintCallable, Category="Lua", meta = (AutoCreateRefTerm = "Args")) + TArray LuaCallTableIndexMulti(FLuaValue InTable, int32 Index, TArray Args); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Lua") + FLuaValue LuaGetField(const FString& Name); + + UFUNCTION(BlueprintCallable, Category="Lua") + void LuaSetField(const FString& Name, FLuaValue Value); + + UPROPERTY(BlueprintAssignable, Category = "Lua", meta = (DisplayName = "On Lua Error")) + FLuaComponentError OnLuaError; + + UFUNCTION(BlueprintNativeEvent, Category = "Lua", meta = (DisplayName = "Lua Component Metatable __index")) + FLuaValue ReceiveLuaMetaIndex(FLuaValue Key); + + UFUNCTION(BlueprintNativeEvent, Category = "Lua", meta = (DisplayName = "Lua Component Metatable __newindex")) + bool ReceiveLuaMetaNewIndex(FLuaValue Key, FLuaValue Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + ULuaState* LuaComponentGetState(); + + virtual void OnRegister() override; + +}; + +``` + +`Source/LuaMachine/Public/LuaDelegate.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "LuaValue.h" +#include "UObject/NoExportTypes.h" +#include "LuaDelegate.generated.h" + +/** + * + */ +UCLASS() +class LUAMACHINE_API ULuaDelegate : public UObject +{ + GENERATED_BODY() + +public: + + void SetupLuaDelegate(UFunction* InSignature, ULuaState* InLuaState, FLuaValue InLuaValue); + + virtual void ProcessEvent(UFunction* Function, void* Parms) override; + + UFUNCTION() + void LuaDelegateFunction(); + +private: + TWeakObjectPtr LuaState; + FLuaValue LuaValue; + UFunction* LuaDelegateSignature; +}; + +``` + +`Source/LuaMachine/Public/LuaGlobalNameComponent.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "LuaState.h" +#include "LuaGlobalNameComponent.generated.h" + + +UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) +class LUAMACHINE_API ULuaGlobalNameComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + // Sets default values for this component's properties + ULuaGlobalNameComponent(); + +protected: + // Called when the game starts + virtual void BeginPlay() override; + +public: + // Called every frame + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + + virtual void OnRegister() override; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lua") + TSubclassOf LuaState; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lua") + FString LuaGlobalName; + +}; + +``` + +`Source/LuaMachine/Public/LuaMachine.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" +#include "UObject/GCObject.h" +#include "LuaState.h" +#include "HAL/IConsoleManager.h" + +DECLARE_MULTICAST_DELEGATE(FOnRegisteredLuaStatesChanged); +DECLARE_MULTICAST_DELEGATE_OneParam(FOnNewLuaState, ULuaState*); + +class LUAMACHINE_API FLuaMachineModule : public IModuleInterface, public FGCObject, public FSelfRegisteringExec +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + static FLuaMachineModule& Get(); + + void CleanupLuaStates(bool bIsSimulating); + void UnregisterLuaState(ULuaState* LuaState); + + ULuaState* GetLuaState(TSubclassOf LuaStateClass, UWorld* InWorld, bool bCheckOnly=false); + + TArray GetRegisteredLuaStates(); + + FOnNewLuaState OnNewLuaState; + FOnRegisteredLuaStatesChanged OnRegisteredLuaStatesChanged; + + void LuaLevelAddedToWorld(ULevel* Level, UWorld* World); + void LuaLevelRemovedFromWorld(ULevel* Level, UWorld* World); + + void AddReferencedObjects(FReferenceCollector& Collector) override; + + void RegisterLuaConsoleCommand(const FString& CommandName, const FLuaValue& LuaConsoleCommand); + void UnregisterLuaConsoleCommand(const FString& CommandName); + + void RunLuaConsoleCommand(const TArray& Args, FLuaValue LuaConsoleCommand); + +#if ENGINE_MAJOR_VERSION > 4 + virtual FString GetReferencerName() const override + { + return TEXT("FLuaMachineModule"); + } +#endif + + virtual bool Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar); + +private: +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 4 + TMap, TObjectPtr> LuaStates; +#else + TMap, ULuaState*> LuaStates; +#endif + TSet LuaConsoleCommands; +}; + +``` + +`Source/LuaMachine/Public/LuaMultiLineEditableTextBox.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "Components/TextWidgetTypes.h" +#include "Components/MultiLineEditableTextBox.h" +#include "Runtime/Launch/Resources/Version.h" +#include "LuaMultiLineEditableTextBox.generated.h" + +USTRUCT(BlueprintType) +struct LUAMACHINE_API FLuaCustomHighlighter +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category = "Lua") + FLinearColor Color; + + UPROPERTY(EditAnywhere, Category = "Lua") + TArray Tokens; + + FLuaCustomHighlighter(); +}; + +/** + * + */ +UCLASS(meta=(DisplayName="Lua Code Box")) +class LUAMACHINE_API ULuaMultiLineEditableTextBox : public UTextLayoutWidget +{ + GENERATED_BODY() + +public: + ULuaMultiLineEditableTextBox(); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style", meta = (DisplayName = "Style")) + FEditableTextBoxStyle WidgetStyle; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style", meta = (DisplayName = "Code Style")) + FTextBlockStyle CodeStyle; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style", meta = (DisplayName = "Comment Color")) + FLinearColor CommentColor; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style", meta = (DisplayName = "String Color")) + FLinearColor StringColor; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style", meta = (DisplayName = "Keyword Color")) + FLinearColor KeywordColor; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style", meta = (DisplayName = "Nil Color")) + FLinearColor NilColor; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style", meta = (DisplayName = "Basic Color")) + FLinearColor BasicColor; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style", meta = (DisplayName = "StdLib Color")) + FLinearColor StdLibColor; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lua Code", meta = (DisplayName = "Tab Size")) + int32 TabSize; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Appearance") + bool bIsReadonly; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lua Code") + bool bHandleTab; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lua Code") + bool bHandleArrows; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style") + TArray CustomTokensMapping; + + UFUNCTION(BlueprintPure, Category = "Widget", meta = (DisplayName = "GetText (Lua Code Box)")) + FText GetText() const; + + UFUNCTION(BlueprintCallable, Category = "Widget", meta = (DisplayName = "SetText (Lua Code Box)")) + void SetText(FText InText); + + UFUNCTION(BlueprintPure, Category = "Widget", meta = (DisplayName = "GetCursorLine (Lua Code Box)")) + int32 GetCursorLine() const; + + UFUNCTION(BlueprintPure, Category = "Widget", meta = (DisplayName = "GetCursorColumn (Lua Code Box)")) + int32 GetCursorColumn() const; + + UFUNCTION(BlueprintCallable, Category = "Widget", meta = (DisplayName = "MoveCursorUp (Lua Code Box)")) + void MoveCursorUp(); + + UFUNCTION(BlueprintCallable, Category = "Widget", meta = (DisplayName = "MoveCursorDown (Lua Code Box)")) + void MoveCursorDown(); + + UFUNCTION(BlueprintCallable, Category = "Widget", meta = (DisplayName = "MoveCursorRight (Lua Code Box)")) + void MoveCursorRight(); + + UFUNCTION(BlueprintCallable, Category = "Widget", meta = (DisplayName = "MoveCursorLeft (Lua Code Box)")) + void MoveCursorLeft(); + + UFUNCTION(BlueprintCallable, Category = "Widget", meta = (DisplayName = "CursorGoTo (Lua Code Box)")) + void CursorGoTo(int32 Line, int32 Column); + + UFUNCTION(BlueprintPure, Category = "Widget", meta = (DisplayName = "GetSelectedText (Lua Code Box)")) + FText GetSelectedText() const; + + virtual FReply OnKeyChar(const FGeometry& InGeometry, const FCharacterEvent& InCharacterEvent); + + virtual FReply OnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent); + + virtual void OnCursorMoved(const FTextLocation& Location); + + virtual void SynchronizeProperties() override; + +#if WITH_EDITOR + virtual const FText GetPaletteCategory() override; +#endif + + virtual void ReleaseSlateResources(bool bReleaseChildren) override; + +protected: + virtual TSharedRef RebuildWidget() override; + + TSharedPtr EditableTextBoxPtr; + + FTextLocation CursorLocation; + +#if ENGINE_MAJOR_VERSION >=5 && ENGINE_MINOR_VERSION >= 1 + FEditableTextBoxStyle EditableTextBoxStyle; +#endif +}; + +``` + +`Source/LuaMachine/Public/LuaState.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/Blueprint.h" +#include "ThirdParty/lua/lua.hpp" +#include "LuaValue.h" +#include "LuaCode.h" +#include "Runtime/Core/Public/Containers/Queue.h" +#include "Runtime/Launch/Resources/Version.h" +#include "LuaDelegate.h" +#include "LuaCommandExecutor.h" +#include "LuaState.generated.h" + +LUAMACHINE_API DECLARE_LOG_CATEGORY_EXTERN(LogLuaMachine, Log, All); + +/** + * + */ + +class ULuaBlueprintPackage; + +struct FLuaUserData +{ + ELuaValueType Type; + // we use weak pointers as both fields can eventually be garbage collected + // while the lua VM hold a reference to the userdata + TWeakObjectPtr Context; + TWeakObjectPtr Function; + + // meaningful only for multicast delegates broadcasting + FMulticastScriptDelegate* MulticastScriptDelegate; + + FLuaUserData(UObject* InObject) + { + Type = ELuaValueType::UObject; + Context = InObject; + MulticastScriptDelegate = nullptr; + } + + FLuaUserData(UObject* InObject, UFunction* InFunction) + { + Type = ELuaValueType::UFunction; + Context = InObject; + Function = InFunction; + MulticastScriptDelegate = nullptr; + } +}; + +UENUM(BlueprintType) +enum class ELuaThreadStatus : uint8 +{ + Invalid, + Ok, + Suspended, + Error, +}; + +USTRUCT() +struct FLuaLibsLoader +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category = "Lua", meta = (DisplayName = "Load base")) + bool bLoadBase; + + UPROPERTY(EditAnywhere, Category = "Lua", meta = (DisplayName = "Load coroutine")) + bool bLoadCoroutine; + + UPROPERTY(EditAnywhere, Category = "Lua", meta = (DisplayName = "Load table")) + bool bLoadTable; + + UPROPERTY(EditAnywhere, Category = "Lua", meta = (DisplayName = "Load io")) + bool bLoadIO; + + UPROPERTY(EditAnywhere, Category = "Lua", meta = (DisplayName = "Load os")) + bool bLoadOS; + + UPROPERTY(EditAnywhere, Category = "Lua", meta = (DisplayName = "Load string")) + bool bLoadString; + + UPROPERTY(EditAnywhere, Category = "Lua", meta = (DisplayName = "Load math")) + bool bLoadMath; + + UPROPERTY(EditAnywhere, Category = "Lua", meta = (DisplayName = "Load utf8")) + bool bLoadUTF8; + + UPROPERTY(EditAnywhere, Category = "Lua", meta = (DisplayName = "Load debug")) + bool bLoadDebug; + + FLuaLibsLoader() + : bLoadBase(true) + , bLoadCoroutine(true) + , bLoadTable(true) + , bLoadIO(true) + , bLoadOS(true) + , bLoadString(true) + , bLoadMath(true) + , bLoadUTF8(true) + , bLoadDebug(false) + {} + +}; + +USTRUCT(BlueprintType) +struct FLuaDebug +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Lua") + int32 CurrentLine; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Lua") + FString Source; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Lua") + FString Name; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Lua") + FString NameWhat; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Lua") + FString What; + + FLuaDebug() + : CurrentLine(0) + { + + } +}; + +USTRUCT(BlueprintType) +struct FLuaDelegateGroup +{ + GENERATED_BODY() + + UPROPERTY() + TArray LuaDelegates; +}; + + +struct FLuaSmartReference : public TSharedFromThis +{ + ULuaState* LuaState; + FLuaValue Value; +}; + + +class ULuaUserDataObject; + +UCLASS(Abstract, Blueprintable, HideDropdown) +class LUAMACHINE_API ULuaState : public UObject +{ + GENERATED_BODY() + +public: + ULuaState(); + ~ULuaState(); + + virtual UWorld* GetWorld() const override { return CurrentWorld; } + + UPROPERTY(EditAnywhere, Category = "Lua") + ULuaCode* LuaCodeAsset; + + UPROPERTY(EditAnywhere, Category = "Lua") + FString LuaFilename; + + UPROPERTY(EditAnywhere, Category = "Lua") + TMap Table; + + UPROPERTY(EditAnywhere, Category = "Lua", meta = (DisplayName = "Lua Blueprint Packages Table")) + TMap> LuaBlueprintPackagesTable; + + UPROPERTY(EditAnywhere, Category = "Lua") + TMap RequireTable; + + UPROPERTY(EditAnywhere, Category = "Lua") + bool bLuaOpenLibs; + + UPROPERTY(EditAnywhere, Category = "Lua", meta = (DisplayName = "Load Specific Lua Libraries (only if \"Lua Open Libs\" is false)")) + FLuaLibsLoader LuaLibsLoader; + + UPROPERTY(EditAnywhere, Category = "Lua") + bool bAddProjectContentDirToPackagePath; + + UPROPERTY(EditAnywhere, Category = "Lua") + TArray AppendProjectContentDirSubDir; + + UPROPERTY(EditAnywhere, Category = "Lua") + FString OverridePackagePath; + + UPROPERTY(EditAnywhere, Category = "Lua") + FString OverridePackageCPath; + + UPROPERTY(EditAnywhere, meta = (DisplayName = "UserData MetaTable from CodeAsset"), Category = "Lua") + ULuaCode* UserDataMetaTableFromCodeAsset; + + UFUNCTION(BlueprintNativeEvent, Category = "Lua", meta = (DisplayName = "Lua Error")) + void ReceiveLuaError(const FString& Message); + + UFUNCTION(BlueprintNativeEvent, Category = "Lua", meta = (DisplayName = "Lua Line Hook")) + void ReceiveLuaLineHook(const FLuaDebug& LuaDebug); + + UFUNCTION(BlueprintNativeEvent, Category = "Lua", meta = (DisplayName = "Lua Call Hook")) + void ReceiveLuaCallHook(const FLuaDebug& LuaDebug); + + UFUNCTION(BlueprintNativeEvent, Category = "Lua", meta = (DisplayName = "Lua Return Hook")) + void ReceiveLuaReturnHook(const FLuaDebug& LuaDebug); + + // Not BlueprintNativeEvent, as throwing a luaL_error from an RTTI call results in leaving the VM in an unexpected + // state and will result in exceptions + UFUNCTION(Category = "Lua", meta = (DisplayName = "Lua Count Hook")) + virtual void ReceiveLuaCountHook(const FLuaDebug& LuaDebug); + + UFUNCTION(BlueprintCallable, Category = "Lua") + FLuaValue NewLuaUserDataObject(TSubclassOf LuaUserDataObjectClass, bool bTrackObject=true); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + FLuaDebug LuaGetInfo(const int32 Level); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + TMap LuaGetLocals(const int32 Level); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + TSubclassOf GetSelfLuaState() const { return GetClass(); } + + template + FLuaValue NewLuaUserDataObject(bool bTrackObject = true) + { + return NewLuaUserDataObject(T::StaticClass(), bTrackObject); + } + + UFUNCTION(BlueprintCallable, Category = "Lua") + void SetLuaUserDataField(FLuaValue UserData, const FString& Key, FLuaValue Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + FLuaValue GetLuaUserDataField(FLuaValue UserData, const FString& Key); + + UFUNCTION(BlueprintCallable, Category = "Lua") + FLuaValue GetLuaValueFromProperty(UObject* InObject, const FString& PropertyName); + + UFUNCTION(BlueprintCallable, Category = "Lua") + bool SetPropertyFromLuaValue(UObject* InObject, const FString& PropertyName, FLuaValue Value); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + FLuaValue GetLuaBlueprintPackageTable(const FString& PackageName); + + void FromLuaValue(FLuaValue& LuaValue, UObject* CallContext = nullptr, lua_State* State = nullptr); + FLuaValue ToLuaValue(int Index, lua_State* State = nullptr); + + ELuaThreadStatus GetLuaThreadStatus(FLuaValue Value); + int32 GetLuaThreadStackTop(FLuaValue Value); + + UPROPERTY(EditAnywhere, Category = "Lua") + bool bLogError; + + /* Enable it if you want this Lua state to not be destroyed during PIE. Useful for editor scripting */ + UPROPERTY(EditAnywhere, Category = "Lua") + bool bPersistent; + + /* Enable debug of each Lua line. The LuaLineHook event will be triggered */ + UPROPERTY(EditAnywhere, Category = "Lua") + bool bEnableLineHook; + + /* Enable debug of each Lua call. The LuaCallHook event will be triggered */ + UPROPERTY(EditAnywhere, Category = "Lua") + bool bEnableCallHook; + + /* Enable debug of each Lua return. The LuaReturnHook event will be triggered */ + UPROPERTY(EditAnywhere, Category = "Lua") + bool bEnableReturnHook; + + /* Enable debug for reaching a number of Lua instruction. The LuaCountHook event will be triggered */ + UPROPERTY(EditAnywhere, Category = "Lua") + bool bEnableCountHook; + + /* Number of instructions to wait for when the Count Hook is enabled */ + UPROPERTY(EditAnywhere, Category = "Lua", Meta = (EditCondition = "bEnableCountHook")) + int32 HookInstructionCount = 25000; + + UPROPERTY() + TMap LuaBlueprintPackages; + + TArray> LuaSmartReferences; + + UPROPERTY() + TArray TrackedLuaUserDataObjects; + + UFUNCTION(BlueprintNativeEvent, Category = "Lua", meta = (DisplayName = "Lua Level Added To World")) + void ReceiveLuaLevelAddedToWorld(ULevel* Level, UWorld* World); + + UFUNCTION(BlueprintNativeEvent, Category = "Lua", meta = (DisplayName = "Lua Level Removed From World")) + void ReceiveLuaLevelRemovedFromWorld(ULevel* Level, UWorld* World); + + UFUNCTION(BlueprintNativeEvent, Category = "Lua", meta = (DisplayName = "Lua State Pre Initialization")) + void ReceiveLuaStatePreInitialized(); + + UFUNCTION(BlueprintNativeEvent, Category = "Lua", meta = (DisplayName = "Lua State Initialized")) + void ReceiveLuaStateInitialized(); + + int32 GetTop(); + + FString LastError; + + int32 InceptionLevel; + + TQueue InceptionErrors; + + void NewTable(); + + void SetMetaTable(int Index); + void GetMetaTable(int Index); + + void SetField(int Index, const char* FieldName); + + void GetField(int Index, const char* FieldName); + + void NewUObject(UObject* Object, lua_State* State); + + void* NewUserData(size_t DataSize); + + void GetGlobal(const char* Name); + + int32 GetFieldFromTree(const FString& Tree, bool bGlobal = true); + + void SetFieldFromTree(const FString& Tree, FLuaValue& Value, bool bGlobal, UObject* CallContext = nullptr); + + void SetGlobal(const char* Name); + + void PushValue(int Index); + + void PushGlobalTable(); + + bool PCall(int NArgs, FLuaValue& Value, int NRet = 1); + bool Call(int NArgs, FLuaValue& Value, int NRet = 1); + + void Pop(int32 Amount = 1); + + void PushNil(); + + void Unref(int Ref); + void UnrefChecked(int Ref); + int NewRef(); + void GetRef(int Ref); + int Next(int Index); + + bool Resume(int Index, int NArgs); + bool Yield(int Index, int NArgs); + + int GC(int What, int Data = 0); + + int32 ToInteger(int Index); + + void Len(int Index); + + int32 ILen(int Index); + + void RawGetI(int Index, int N); + void RawSetI(int Index, int N); + + void PushCFunction(lua_CFunction Function); + + ULuaState* GetLuaState(UWorld* InWorld); + + bool RunCode(const TArray& Code, const FString& CodePath, int NRet = 0); + bool RunCode(const FString& Code, const FString& CodePath, int NRet = 0); + + bool RunCodeAsset(ULuaCode* CodeAsset, int NRet = 0); + + FLuaValue CreateLuaTable(); + FLuaValue CreateLuaThread(FLuaValue Value); + + FLuaValue CreateLuaLazyTable(); + + bool RunFile(const FString& Filename, bool bIgnoreNonExistent, int NRet = 0, bool bNonContentDirectory=false); + + static int MetaTableFunctionUserData__index(lua_State* L); + static int MetaTableFunctionUserData__newindex(lua_State* L); + + static int TableFunction_print(lua_State* L); + static int TableFunction_package_preload(lua_State* L); + static int TableFunction_package_loader(lua_State* L); + static int TableFunction_package_loader_codeasset(lua_State* L); + static int TableFunction_package_loader_asset(lua_State* L); + + static int MetaTableFunction__call(lua_State* L); + static int MetaTableFunction__rawcall(lua_State* L); + static int MetaTableFunction__rawbroadcast(lua_State* L); + + static int MetaTableFunctionUserData__eq(lua_State* L); + static int MetaTableFunctionUserData__gc(lua_State* L); + + static int ToByteCode_Writer(lua_State* L, const void* Ptr, size_t Size, void* UserData); + + static void Debug_Hook(lua_State* L, lua_Debug* ar); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + static TArray ToByteCode(const FString& Code, const FString& CodePath, FString& ErrorString); + +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FLuaValue FromUProperty(void* Buffer, FProperty* Property, bool& bSuccess, int32 Index = 0); + void ToUProperty(void* Buffer, FProperty* Property, FLuaValue Value, bool& bSuccess, int32 Index = 0); + FLuaValue FromFProperty(void* Buffer, FProperty* Property, bool& bSuccess, int32 Index = 0); + void ToFProperty(void* Buffer, FProperty* Property, FLuaValue Value, bool& bSuccess, int32 Index = 0); + FLuaValue FromProperty(void* Buffer, FProperty* Property, bool& bSuccess, int32 Index = 0); + void ToProperty(void* Buffer, FProperty* Property, FLuaValue Value, bool& bSuccess, int32 Index = 0); +#else + FLuaValue FromUProperty(void* Buffer, UProperty* Property, bool& bSuccess, int32 Index = 0); + void ToUProperty(void* Buffer, UProperty* Property, FLuaValue Value, bool& bSuccess, int32 Index = 0); + FLuaValue FromProperty(void* Buffer, UProperty* Property, bool& bSuccess, int32 Index = 0); + void ToProperty(void* Buffer, UProperty* Property, FLuaValue Value, bool& bSuccess, int32 Index = 0); +#endif + + static ULuaState* GetFromExtraSpace(lua_State* L) + { + ULuaState** LuaExtraSpacePtr = (ULuaState**)lua_getextraspace(L); + return *LuaExtraSpacePtr; + } + + void Log(const FString& Message) + { + UE_LOG(LogLuaMachine, Log, TEXT("%s"), *Message); + } + + void LogWarning(const FString& Message) + { + UE_LOG(LogLuaMachine, Warning, TEXT("%s"), *Message); + } + + void LogError(const FString& Message) + { + UE_LOG(LogLuaMachine, Error, TEXT("%s"), *Message); + } + + void SetUserDataMetaTable(FLuaValue MetaTable); + + FORCEINLINE lua_State* GetInternalLuaState() const { return L; } + + void PushRegistryTable(); + + TSharedRef AddLuaSmartReference(FLuaValue Value); + void RemoveLuaSmartReference(TSharedRef Ref); + + void SetupAndAssignUserDataMetatable(UObject* Context, TMap& Metatable, lua_State* State); + + const void* ToPointer(int Index); + + UPROPERTY(EditAnywhere, Category = "Lua") + bool bRawLuaFunctionCall; + + void GCLuaDelegatesCheck(); + + void RegisterLuaDelegate(UObject* InObject, ULuaDelegate* InLuaDelegate); + void UnregisterLuaDelegatesOfObject(UObject* InObject); + + TArray GetPropertiesNames(UObject* InObject); + TArray GetFunctionsNames(UObject* InObject); + + FLuaValue StructToLuaTable(UScriptStruct* InScriptStruct, const uint8* StructData); + + UFUNCTION(BlueprintCallable, Category = "Lua") + FLuaValue StructToLuaTable(UScriptStruct* InScriptStruct, const TArray& StructData); + + void LuaTableToStruct(FLuaValue& LuaValue, UScriptStruct* InScriptStruct, uint8* StructData); + + template + FLuaValue StructToLuaValue(T& InStruct) + { + return StructToLuaTable(T::StaticStruct(), (const uint8*)&InStruct); + } + + template + T LuaValueToStruct(FLuaValue& LuaValue) + { + T InStruct; + LuaTableToStruct(LuaValue, T::StaticStruct(), (uint8*)&InStruct); + return InStruct; + } + + template + T LuaValueToBaseStruct(FLuaValue& LuaValue) + { + T InStruct; + LuaTableToStruct(LuaValue, TBaseStructure::Get(), (uint8*)&InStruct); + return InStruct; + } + + FORCEINLINE FLuaCommandExecutor* GetLuaConsole() { return &LuaConsole; } + + UFUNCTION(BlueprintCallable, Category = "Lua") + void AddLuaValueToLuaState(const FString& Name, FLuaValue LuaValue); + + UFUNCTION(BlueprintCallable, Category = "Lua") + FLuaValue RunString(const FString& CodeString, FString CodePath); + + UFUNCTION(BlueprintCallable, Category = "Lua") + void Error(const FString& ErrorString); + +protected: + lua_State* L; + bool bDisabled; + + UWorld* CurrentWorld; + + FLuaValue UserDataMetaTable; + + virtual void LuaStateInit(); + + FDelegateHandle GCLuaDelegatesHandle; + + UPROPERTY() + TMap, FLuaDelegateGroup> LuaDelegatesMap; + + FLuaCommandExecutor LuaConsole; +}; + +#define LUACFUNCTION(FuncClass, FuncName, NumRetValues, NumArgs) static int FuncName ## _C(lua_State* L)\ +{\ + FuncClass* LuaState = (FuncClass*)ULuaState::GetFromExtraSpace(L);\ + int TrueNumArgs = lua_gettop(L);\ + if (TrueNumArgs != NumArgs)\ + {\ + return luaL_error(L, "invalid number of arguments for %s (got %d, expected %d)", #FuncName, TrueNumArgs, NumArgs);\ + }\ + TArray LuaArgs;\ + for (int32 LuaArgIndex = 0; LuaArgIndex < NumArgs; LuaArgIndex++)\ + {\ + LuaArgs.Add(LuaState->ToLuaValue(LuaArgIndex + 1, L));\ + }\ + FLuaValue NilValue;\ + TArray RetValues = LuaState->FuncName(LuaArgs);\ + for (int32 RetIndex = 0; RetIndex < NumRetValues; RetIndex++)\ + {\ + if (RetIndex < RetValues.Num())\ + {\ + LuaState->FromLuaValue(RetValues[RetIndex]);\ + }\ + else\ + {\ + LuaState->FromLuaValue(NilValue);\ + }\ + }\ + return NumRetValues;\ +}\ +TArray FuncName(TArray LuaArgs) +``` + +`Source/LuaMachine/Public/LuaSyntaxHighlighter.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "Runtime/Slate/Public/Framework/Text/SyntaxHighlighterTextLayoutMarshaller.h" + +struct FLuaSyntaxTextStyle +{ + FTextBlockStyle NormalTextStyle; + FTextBlockStyle CommentTextStyle; + FTextBlockStyle StringTextStyle; + FTextBlockStyle KeywordTextStyle; + FTextBlockStyle NilTextStyle; + FTextBlockStyle BasicTextStyle; + FTextBlockStyle StdLibTextStyle; + TMap CustomTextColorMapping; + TMap CustomTextStyleMapping; +}; + + +class LUAMACHINE_API FLuaMachineSyntaxHighlighterTextLayoutMarshaller : public FSyntaxHighlighterTextLayoutMarshaller +{ +public: + + FLuaMachineSyntaxHighlighterTextLayoutMarshaller(TSharedPtr InTokenizer, TArray InBasicTokens, TArray InStdLibTokens, FLuaSyntaxTextStyle InLuaSyntaxTextStyle); + + static TSharedRef Create(FLuaSyntaxTextStyle LuaSyntaxTextStyle); + +protected: + virtual void ParseTokens(const FString& SourceString, FTextLayout& TargetTextLayout, TArray TokenizedLines) override; + + TArray BasicTokens; + TArray StdLibTokens; + + FLuaSyntaxTextStyle SyntaxTextStyle; +}; + +``` + +`Source/LuaMachine/Public/LuaTableAsset.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "LuaState.h" +#include "LuaValue.h" +#include "LuaTableAsset.generated.h" + +/** + * + */ +UCLASS(BlueprintType) +class LUAMACHINE_API ULuaTableAsset : public UDataAsset +{ + GENERATED_BODY() + +public: + + UPROPERTY(EditAnywhere, Category = "Lua") + TMap Table; + + FLuaValue ToLuaTable(ULuaState* LuaState); +}; + +``` + +`Source/LuaMachine/Public/LuaUserDataObject.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "LuaState.h" +#include "LuaUserDataObject.generated.h" + +/** + * + */ +UCLASS(Abstract, Blueprintable, HideDropdown) +class LUAMACHINE_API ULuaUserDataObject : public UObject +{ + GENERATED_BODY() + +public: + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + TSubclassOf GetLuaState() const; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + ULuaState* GetLuaStateInstance() const; + + virtual UWorld* GetWorld() const override; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Lua") + TMap Table; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Lua") + TMap Metatable; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Lua") + bool bImplicitSelf; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + FLuaValue LuaGetField(const FString& Name); + + UFUNCTION(BlueprintCallable, Category = "Lua") + void LuaSetField(const FString& Name, FLuaValue Value); + + UFUNCTION(BlueprintNativeEvent, Category = "Lua", meta = (DisplayName = "Lua UserData Metatable __gc")) + void ReceiveLuaGC(); + + UFUNCTION(BlueprintNativeEvent, Category = "Lua", meta = (DisplayName = "Lua UserData Metatable __index")) + FLuaValue ReceiveLuaMetaIndex(FLuaValue Key); + + UFUNCTION(BlueprintNativeEvent, Category = "Lua", meta = (DisplayName = "Lua UserData Table Init")) + void ReceiveLuaUserDataTableInit(); + + UFUNCTION(BlueprintCallable, Category = "Lua", meta = (AutoCreateRefTerm = "Args")) + FLuaValue LuaCallFunction(const FString& Name, TArray Args, bool bGlobal); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + FLuaValue UFunctionToLuaValue(const FString& FunctionName); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Lua") + TArray GetObjectUFunctions(bool bOnlyPublic=true); + +protected: + TSharedPtr AddLuaSmartReference(FLuaValue Value); + void RemoveLuaSmartReference(TSharedPtr Ref); +}; + +``` + +`Source/LuaMachine/Public/LuaValue.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "ThirdParty/lua/lua.hpp" +#include "Serialization/JsonSerializer.h" +#include "LuaValue.generated.h" + +// required for Mac +#ifdef Nil +#undef Nil +#endif + +/** + * + */ + +UENUM(BlueprintType) +enum class ELuaValueType : uint8 +{ + Nil, + Bool, + Integer, + Number, + String, + Function, + Table, + UFunction, + UObject, + Thread, + MulticastDelegate, +}; + +class ULuaState; + +USTRUCT(BlueprintType) +struct LUAMACHINE_API FLuaValue +{ + GENERATED_BODY() + + FLuaValue() + { + Type = ELuaValueType::Nil; + Object = nullptr; + LuaRef = LUA_NOREF; + LuaState = nullptr; + Bool = false; + Integer = 0; + Number = 0; + MulticastScriptDelegate = nullptr; + } + + FLuaValue(const FLuaValue& SourceValue); + FLuaValue& operator = (const FLuaValue &SourceValue); + + FLuaValue(const FString& InString) : FLuaValue() + { + Type = ELuaValueType::String; + String = InString; + } + + FLuaValue(const char* InChars) : FLuaValue(FString(InChars)) + { + } + + FLuaValue(const TCHAR* InChars) : FLuaValue(FString(InChars)) + { + } + + FLuaValue(const char* InChars, size_t Length) : FLuaValue() + { + Type = ELuaValueType::String; + for (size_t i = 0; i < Length; i++) + { + uint16 TChar = (uint16)InChars[i]; + // cleanup garbage + TChar &= 0xFF; + // hack for allowing binary data + if (TChar == 0) + { + TChar = 0xffff; + } + String += (TCHAR)TChar; + } + } + + FLuaValue(const TArray& InBytes) : FLuaValue(reinterpret_cast(InBytes.GetData()), InBytes.Num()) + { + } + + FLuaValue(const double Value) : FLuaValue() + { + Type = ELuaValueType::Number; + Number = Value; + } + + FLuaValue(const float Value) : FLuaValue() + { + Type = ELuaValueType::Number; + Number = Value; + } + + FLuaValue(const int64 Value) : FLuaValue() + { + Type = ELuaValueType::Integer; + Integer = Value; + } + + FLuaValue(const int32 Value) : FLuaValue() + { + Type = ELuaValueType::Integer; + Integer = Value; + } + + FLuaValue(const bool bInBool) : FLuaValue() + { + Type = ELuaValueType::Bool; + Bool = bInBool; + } + + FLuaValue(UObject* InObject) : FLuaValue() + { + if (InObject) + { + Type = ELuaValueType::UObject; + Object = InObject; + } + } + + ~FLuaValue(); + + static FLuaValue Function(FName FunctionName) + { + FLuaValue LuaValue; + LuaValue.Type = ELuaValueType::UFunction; + LuaValue.FunctionName = FunctionName; + return LuaValue; + } + + static FLuaValue FunctionOfObject(UObject* InObject, FName FunctionName) + { + FLuaValue LuaValue; + LuaValue.Type = ELuaValueType::UFunction; + LuaValue.Object = InObject; + LuaValue.FunctionName = FunctionName; + return LuaValue; + } + + FString ToString() const; + FName ToName() const; + int64 ToInteger() const; + double ToFloat() const; + bool ToBool() const; + + TArray ToBytes() const; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Lua") + ELuaValueType Type; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Lua") + bool Bool; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Lua") + int64 Integer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Lua") + double Number; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Lua") + FString String; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Lua") + UObject* Object; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Lua") + FName FunctionName; + + int LuaRef; + + TWeakObjectPtr LuaState; + + FLuaValue GetField(const FString& Key); + FLuaValue SetField(const FString& Key, FLuaValue Value); + + FLuaValue SetField(const FString& Key, lua_CFunction CFunction); + + FLuaValue GetFieldByIndex(const int32 Index); + FLuaValue SetFieldByIndex(const int32 Index, FLuaValue Value); + + FLuaValue SetMetaTable(FLuaValue MetaTable); + + bool IsReferencedInLuaRegistry() const; + + static FLuaValue FromJsonValue(ULuaState* L, FJsonValue& JsonValue); + TSharedPtr ToJsonValue(); + + static FLuaValue FromBase64(const FString& Base64); + FString ToBase64() const; + + bool IsNil() const; + + void Unref(); + + FMulticastScriptDelegate* MulticastScriptDelegate = nullptr; +}; + +``` + +`Source/LuaMachineEditor/LuaMachineEditor.Build.cs`: + +```cs +// Copyright 2018-2023 - Roberto De Ioris + +using UnrealBuildTool; + +public class LuaMachineEditor : ModuleRules +{ + public LuaMachineEditor(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + // ... add private dependencies that you statically link with here ... + "UnrealEd", + "Projects", + "InputCore", + "EditorStyle", + "LuaMachine" + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} + +``` + +`Source/LuaMachineEditor/Private/LuaCodeCustomization.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + +#include "LuaCodeCustomization.h" +#include "LuaMachineEditor.h" +#include "Runtime/Slate/Public/Widgets/Input/SMultiLineEditableTextBox.h" +#include "Editor/PropertyEditor/Public/PropertyHandle.h" +#include "Editor/PropertyEditor/Public/DetailLayoutBuilder.h" +#include "Editor/PropertyEditor/Public/DetailCategoryBuilder.h" +#include "Editor/PropertyEditor/Public/IDetailChildrenBuilder.h" +#include "Editor/PropertyEditor/Public/DetailWidgetRow.h" +#include "Runtime/Slate/Public/Widgets/Text/STextBlock.h" +#include "Runtime/SlateCore/Public/Brushes/SlateColorBrush.h" +#include "Runtime/Slate/Public/Widgets/Text/SlateEditableTextLayout.h" +#include "Runtime/SlateCore/Public/Fonts/FontMeasure.h" +#include "Runtime/Slate/Public/Framework/Application/SlateApplication.h" +#include "Runtime/Slate/Public/Framework/MultiBox/MultiBoxBuilder.h" +#include "LuaMachine/Public/LuaCode.h" +#include "LuaMachine/Public/LuaSyntaxHighlighter.h" + + +class SLuaMultiLineEditableTextBox : public SMultiLineEditableTextBox +{ +public: + SLATE_BEGIN_ARGS(SLuaMultiLineEditableTextBox) {} + + SLATE_ARGUMENT(TWeakObjectPtr, LuaCodeOwner); + + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs) + { + LuaCode = InArgs._LuaCodeOwner; + CurrentLine = 1; + CurrentColumn = 1; + + FLuaSyntaxTextStyle LuaTextStyle; + + LuaTextStyle.NormalTextStyle = FLuaMachineEditorModule::Get().GetStyleSet()->GetWidgetStyle("SyntaxHighlight.LuaMachine.Normal"); + LuaTextStyle.CommentTextStyle = FLuaMachineEditorModule::Get().GetStyleSet()->GetWidgetStyle("SyntaxHighlight.LuaMachine.Comment"); + LuaTextStyle.StringTextStyle = FLuaMachineEditorModule::Get().GetStyleSet()->GetWidgetStyle("SyntaxHighlight.LuaMachine.String"); + LuaTextStyle.KeywordTextStyle = FLuaMachineEditorModule::Get().GetStyleSet()->GetWidgetStyle("SyntaxHighlight.LuaMachine.Keyword"); + LuaTextStyle.NilTextStyle = FLuaMachineEditorModule::Get().GetStyleSet()->GetWidgetStyle("SyntaxHighlight.LuaMachine.Nil"); + LuaTextStyle.BasicTextStyle = FLuaMachineEditorModule::Get().GetStyleSet()->GetWidgetStyle("SyntaxHighlight.LuaMachine.Basic"); + LuaTextStyle.StdLibTextStyle = FLuaMachineEditorModule::Get().GetStyleSet()->GetWidgetStyle("SyntaxHighlight.LuaMachine.StdLib"); + + SyntaxHighlighter = FLuaMachineSyntaxHighlighterTextLayoutMarshaller::Create(LuaTextStyle); + + FMenuExtensionDelegate Delegate; + Delegate.BindLambda([&](FMenuBuilder &Builder) + { + Builder.AddWidget(SNew(STextBlock).Text_Lambda([&]() { + return FText::FromString(FString::Printf(TEXT("Line: %d Column: %d"), CurrentLine, CurrentColumn)); + }), FText::GetEmpty()); + + }); + + SMultiLineEditableTextBox::Construct( + SMultiLineEditableTextBox::FArguments() + .AutoWrapText(false) + .Margin(0.0f) + .Text(LuaCode->Code) + .Marshaller(SyntaxHighlighter) + .OnTextChanged(this, &SLuaMultiLineEditableTextBox::UpdateLuaCode) + .BackgroundColor(FSlateColor(FLinearColor::Black)) + .ForegroundColor(FSlateColor(FLinearColor::White)) + .ContextMenuExtender(Delegate) + .OnCursorMoved_Lambda([&](const FTextLocation& Location) { + CurrentLine = Location.GetLineIndex() + 1; + CurrentColumn = Location.GetOffset() + 1; + }) + ); + + } + + void UpdateLuaCode(const FText& InCode) + { + LuaCode->Code = InCode; + LuaCode->MarkPackageDirty(); + } + +protected: + + int32 CurrentLine; + int32 CurrentColumn; + + float GetFontHeight(const FSlateFontInfo& FontInfo) const + { + const TSharedRef FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); + return FontMeasure->GetMaxCharacterHeight(FontInfo); + } + + virtual FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override + { + float MinimumHeight = 500; + float MaxHeight = 800; + + FVector2D Size = SMultiLineEditableTextBox::ComputeDesiredSize(LayoutScaleMultiplier); + if (Size.Y < MinimumHeight) + Size.Y = MinimumHeight; + if (Size.Y > MaxHeight) + Size.Y = MaxHeight; + return Size; + } + + virtual FReply OnKeyChar(const FGeometry& InGeometry, const FCharacterEvent& InCharacterEvent) override + { + const TCHAR Character = InCharacterEvent.GetCharacter(); + if (Character == TEXT('\t')) + { + ClearSelection(); + InsertTextAtCursor(FString(" ")); + return FReply::Handled(); + } + return SMultiLineEditableTextBox::OnKeyChar(InGeometry, InCharacterEvent); + } + + virtual FReply OnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override + { + + if (InKeyEvent.GetKeyCode() == 9) + { + return FReply::Handled(); + } + return SMultiLineEditableTextBox::OnKeyDown(InGeometry, InKeyEvent); + } + +private: + TWeakObjectPtr LuaCode; + TSharedPtr SyntaxHighlighter; +}; + +class SLuaEditor : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SLuaEditor) {} + + SLATE_ARGUMENT(TWeakObjectPtr, LuaCodeOwner); + + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs) + { + LuaCode = InArgs._LuaCodeOwner; + + BackgroundColor = FSlateColorBrush(FLinearColor::Black); + + ChildSlot.Padding(4)[ + SNew(SBorder).BorderImage(&BackgroundColor).BorderBackgroundColor(FSlateColor(FLinearColor::White)) + [ + + SNew(SLuaMultiLineEditableTextBox) + .LuaCodeOwner(LuaCode) + + ] + ]; + } + + + +private: + TWeakObjectPtr LuaCode; + FSlateBrush BackgroundColor; +}; + +void FLuaCodeCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + TArray> Objects; + DetailBuilder.GetObjectsBeingCustomized(Objects); + + if (Objects.Num() != 1) + { + return; + } + + TWeakObjectPtr LuaCode = Cast(Objects[0].Get()); + + TSharedRef Prop = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(ULuaCode, Code)); + DetailBuilder.HideProperty(Prop); + + + IDetailCategoryBuilder& Category = DetailBuilder.EditCategory("Code"); + Category.AddCustomRow(FText::FromString("Code")).WholeRowContent()[ + + SNew(SLuaEditor).LuaCodeOwner(LuaCode) + + ]; + +} + + + + + +``` + +`Source/LuaMachineEditor/Private/LuaCodeFactory.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris +// Reimport system by yama2akira (Akira Yamamoto) + +#include "LuaCodeFactory.h" +#include "LuaCode.h" +#include "EditorFramework/AssetImportData.h" +#include "Misc/FileHelper.h" +#include "LuaMachineEditor.h" + +ULuaCodeFactory::ULuaCodeFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { + + Formats.Add(FString("lua;Lua Script")); + + bCreateNew = true; + bEditAfterNew = true; + + bEditorImport = true; + + SupportedClass = ULuaCode::StaticClass(); +} + +UObject* ULuaCodeFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn, FName CallingContext) +{ + return NewObject(InParent, InClass, InName, Flags); +} + +UObject* ULuaCodeFactory::FactoryCreateBinary(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const uint8*& Buffer, const uint8* BufferEnd, FFeedbackContext* Warn, bool& bOutOperationCanceled) +{ + bOutOperationCanceled = false; + FString Code = ANSI_TO_TCHAR((const char*)Buffer); + ULuaCode *NewAsset = NewObject(InParent, InClass, InName, Flags); + NewAsset->Code = FText::FromString(Code); + + if (NewAsset->AssetImportData != nullptr) { + NewAsset->AssetImportData->Update(GetCurrentFilename()); + NewAsset->MarkPackageDirty(); + } + return NewAsset; +} + +uint32 ULuaCodeFactory::GetMenuCategories() const +{ + return FLuaMachineEditorModule::Get().GetAssetCategory(); +} + +bool ULuaCodeFactory::CanReimport(UObject* Obj, TArray& OutFilenames) +{ + ULuaCode* Asset = Cast(Obj); + if (Asset && Asset->AssetImportData) + { + Asset->AssetImportData->ExtractFilenames(OutFilenames); + + for (auto FileName : OutFilenames) + { + FString FileContent; + if (FFileHelper::LoadFileToString(/*out*/ FileContent, *FileName)) + { + if (Asset->Code.ToString().Compare(FileContent) == 0) + { + return false; + } + } + } + return true; + } + return false; +} + +void ULuaCodeFactory::SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) +{ + ULuaCode* Asset = Cast(Obj); + if (Asset && ensure(NewReimportPaths.Num() == 1)) + { + Asset->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]); + } +} + +EReimportResult::Type ULuaCodeFactory::Reimport(UObject* Obj) +{ + ULuaCode* Asset = Cast(Obj); + if (!Asset) + { + return EReimportResult::Failed; + } + + const FString Filename = Asset->AssetImportData->GetFirstFilename(); + if (!Filename.Len() || IFileManager::Get().FileSize(*Filename) == INDEX_NONE) + { + return EReimportResult::Failed; + } + + EReimportResult::Type Result = EReimportResult::Failed; + if (UFactory::StaticImportObject( + Asset->GetClass(), Asset->GetOuter(), + *Asset->GetName(), RF_Public | RF_Standalone, *Filename, NULL, this)) + { + if (Asset->GetOuter()) + { + Asset->GetOuter()->MarkPackageDirty(); + } + else + { + Asset->MarkPackageDirty(); + } + return EReimportResult::Succeeded; + } + + return EReimportResult::Failed; +} + +FLuaCodeAssetTypeActions::FLuaCodeAssetTypeActions( EAssetTypeCategories::Type InAssetCategory ) +{ + +} + +FText FLuaCodeAssetTypeActions::GetName() const +{ + return NSLOCTEXT( "LuaMachine", "LuaCodeAssetActions", "Lua Code" ); +} + +FColor FLuaCodeAssetTypeActions::GetTypeColor() const +{ + return FColor::Red; +} + +UClass* FLuaCodeAssetTypeActions::GetSupportedClass() const +{ + return ULuaCode::StaticClass(); +} + +uint32 FLuaCodeAssetTypeActions::GetCategories() +{ + return FLuaMachineEditorModule::Get().GetAssetCategory(); +} + +bool FLuaCodeAssetTypeActions::IsImportedAsset() const +{ + return true; +} + +void FLuaCodeAssetTypeActions::GetResolvedSourceFilePaths( const TArray& TypeAssets, TArray& OutSourceFilePaths ) const +{ + for ( auto& Asset : TypeAssets ) + { + const auto LuaCode = CastChecked( Asset ); + if ( LuaCode->AssetImportData ) + { + LuaCode->AssetImportData->ExtractFilenames( OutSourceFilePaths ); + } + } +} + + +``` + +`Source/LuaMachineEditor/Private/LuaMachineEditor.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + +#include "LuaMachineEditor.h" +#include "Editor/UnrealEd/Public/Editor.h" +#include "Editor/PropertyEditor/Public/PropertyEditorModule.h" +#include "EditorStyleSet.h" +#include "Runtime/Projects/Public/Interfaces/IPluginManager.h" +#include "LuaCodeCustomization.h" +#include "LuaValueCustomization.h" +#include "Editor/WorkspaceMenuStructure/Public/WorkspaceMenuStructure.h" +#include "Editor/WorkspaceMenuStructure/Public/WorkspaceMenuStructureModule.h" +#include "Runtime/Slate/Public/Widgets/Text/STextBlock.h" +#include "Runtime/Slate/Public/Widgets/Views/STreeView.h" +#include "Runtime/Slate/Public/Widgets/Input/SButton.h" +#include "Runtime/Slate/Public/Widgets/Input/STextComboBox.h" +#include "LuaMachine/Public/LuaMachine.h" +#include "LuaMachine/Public/LuaBlueprintFunctionLibrary.h" +#include "Widgets/Layout/SScrollBox.h" +#include "LuaUserDataObject.h" +#include "LuaCodeFactory.h" + +#define LOCTEXT_NAMESPACE "FLuaMachineEditorModule" + +FLuaMachineEditorModule::FLuaMachineEditorModule() + : LuaMachineAssetCategoryBit(EAssetTypeCategories::Misc) +{ + +} + +void FLuaMachineEditorModule::StartupModule() +{ + FCoreDelegates::OnPostEngineInit.AddRaw(this, &FLuaMachineEditorModule::OnPostEngineInit); + + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module + + StyleSet = MakeShareable(new FSlateStyleSet("LuaMachineEditor")); + StyleSet->SetContentRoot(IPluginManager::Get().FindPlugin("LuaMachine")->GetBaseDir() / "Resources"); + StyleSet->Set("ClassThumbnail.LuaCode", new FSlateImageBrush(StyleSet->RootToContentDir("Icon128.png"), FVector2D(128.0f, 128.0f))); + StyleSet->Set("ClassThumbnail.LuaTableAsset", new FSlateImageBrush(StyleSet->RootToContentDir("LuaTableAsset256.png"), FVector2D(256.0f, 256.0f))); + + const FSlateFontInfo Consolas9 = FCoreStyle::GetDefaultFontStyle("Mono", 9); + + const FTextBlockStyle CodeBaseStyle = FTextBlockStyle() + .SetFont(Consolas9) + .SetColorAndOpacity(FLinearColor::White) + .SetShadowOffset(FVector2D::ZeroVector) + .SetShadowColorAndOpacity(FLinearColor::Black); + + StyleSet->Set("SyntaxHighlight.LuaMachine.Normal", CodeBaseStyle); + StyleSet->Set("SyntaxHighlight.LuaMachine.Comment", FTextBlockStyle(CodeBaseStyle).SetColorAndOpacity(FLinearColor::Green)); + StyleSet->Set("SyntaxHighlight.LuaMachine.String", FTextBlockStyle(CodeBaseStyle).SetColorAndOpacity(FLinearColor(FColor::Orange))); + StyleSet->Set("SyntaxHighlight.LuaMachine.Keyword", FTextBlockStyle(CodeBaseStyle).SetColorAndOpacity(FLinearColor(FColor::Cyan))); + StyleSet->Set("SyntaxHighlight.LuaMachine.Nil", FTextBlockStyle(CodeBaseStyle).SetColorAndOpacity(FLinearColor::Red)); + StyleSet->Set("SyntaxHighlight.LuaMachine.StdLib", FTextBlockStyle(CodeBaseStyle).SetColorAndOpacity(FLinearColor::Yellow)); + StyleSet->Set("SyntaxHighlight.LuaMachine.Basic", FTextBlockStyle(CodeBaseStyle).SetColorAndOpacity(FLinearColor::FromSRGBColor(FColor::Magenta))); + +#if ENGINE_MINOR_VERSION >= 3 + FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get()); +#endif + + FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked(TEXT("PropertyEditor")); + + PropertyModule.RegisterCustomPropertyTypeLayout(TEXT("LuaValue"), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FLuaValueCustomization::MakeInstance)); + PropertyModule.RegisterCustomClassLayout(TEXT("LuaCode"), FOnGetDetailCustomizationInstance::CreateStatic(&FLuaCodeCustomization::MakeInstance)); + + PropertyModule.NotifyCustomizationModuleChanged(); + + FGlobalTabmanager::Get()->RegisterNomadTabSpawner("LuaMachineDebugger", FOnSpawnTab::CreateStatic(&FLuaMachineEditorModule::CreateLuaMachineDebugger)) + .SetDisplayName(LOCTEXT("LuaMachine Debugger", "LuaMachine Debugger")) + .SetTooltipText(LOCTEXT("Open the LuaMachine Debugger", "Open the LuaMachine Debugger")) + .SetIcon(FSlateIcon( +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1 + FAppStyle::GetAppStyleSetName() +#else + FEditorStyle::GetStyleSetName() +#endif + , "DebugTools.TabIcon")) + .SetGroup(WorkspaceMenu::GetMenuStructure().GetDeveloperToolsMiscCategory()); +} + +void FLuaMachineEditorModule::OnPostEngineInit() +{ + IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); + + LuaMachineAssetCategoryBit = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("LuaMachine")), LOCTEXT("AssetCategory", "Lua Machine")); + + //Add LuaCode to Filters. + RegisterAssetTypeAction(AssetTools, MakeShareable(new FLuaCodeAssetTypeActions(LuaMachineAssetCategoryBit))); +} + +TSharedPtr FLuaMachineEditorModule::GetStyleSet() +{ + return StyleSet; +} + +void FLuaMachineEditorModule::RegisterAssetTypeAction(IAssetTools& AssetTools, TSharedRef Action) +{ + AssetTools.RegisterAssetTypeActions(Action); + CreatedAssetTypeActions.Add(Action); +} + +struct FTableViewLuaValue : public TSharedFromThis +{ + FString LuaTableKey; + FLuaValue LuaTableValue; + bool bExpanded; + TArray> Children; +}; + +class SLuaMachineDebugger : public SCompoundWidget, public FGCObject +{ + SLATE_BEGIN_ARGS(SLuaMachineDebugger) + {} + + SLATE_END_ARGS() + + void RebuildLuaValues() + { + LuaValues.Empty(); + + if (!SelectedLuaState) + { + return; + } + + SelectedLuaState->PushGlobalTable(); + SelectedLuaState->PushNil(); // first key + while (SelectedLuaState->Next(-2)) + { + FLuaValue CurrentLuaKey = SelectedLuaState->ToLuaValue(-2); + FLuaValue CurrentLuaValue = SelectedLuaState->ToLuaValue(-1); + TSharedRef LuaItem = MakeShared(); + LuaItem->LuaTableKey = CurrentLuaKey.ToString(); + LuaItem->LuaTableValue = CurrentLuaValue; + LuaItem->bExpanded = false; + LuaValues.Add(LuaItem); // add key + SelectedLuaState->Pop(); // pop the value + } + + SelectedLuaState->Pop(); // pop the table + + LuaValues.Sort([](const TSharedRef& LHS, const TSharedRef& RHS) { return LHS->LuaTableKey < RHS->LuaTableKey; }); + } + + void OnGetChildren(TSharedRef Item, TArray>& OutChildren) + { + if (!SelectedLuaState) + return; + + if (Item->LuaTableValue.Type == ELuaValueType::Table) + { + if (!Item->bExpanded) + { + SelectedLuaState->FromLuaValue(Item->LuaTableValue); + SelectedLuaState->PushNil(); // first key + while (SelectedLuaState->Next(-2)) + { + FLuaValue CurrentLuaKey = SelectedLuaState->ToLuaValue(-2); + FLuaValue CurrentLuaValue = SelectedLuaState->ToLuaValue(-1); + TSharedRef LuaItem = MakeShared(); + LuaItem->LuaTableKey = CurrentLuaKey.ToString(); + LuaItem->LuaTableValue = CurrentLuaValue; + LuaItem->bExpanded = false; + Item->Children.Add(LuaItem); // add key + SelectedLuaState->Pop(); // pop the value + } + SelectedLuaState->Pop(); // pop the table + Item->bExpanded = true; + Item->Children.Sort([](const TSharedRef& LHS, const TSharedRef& RHS) { return LHS->LuaTableKey < RHS->LuaTableKey; }); + } + OutChildren.Append(Item->Children); + } + else if (Item->LuaTableValue.Type == ELuaValueType::UObject && Item->LuaTableValue.Object) + { + if (Item->LuaTableValue.Object->IsA()) + { + if (!Item->bExpanded) + { + ULuaUserDataObject* LuaUserDataObject = Cast(Item->LuaTableValue.Object); + if (LuaUserDataObject) + { + for (TPair Pair : LuaUserDataObject->Table) + { + TSharedRef LuaItem = MakeShared(); + LuaItem->LuaTableKey = Pair.Key; + LuaItem->LuaTableValue = Pair.Value; + LuaItem->bExpanded = false; + Item->Children.Add(LuaItem); + } + } + Item->bExpanded = true; + Item->Children.Sort([](const TSharedRef& LHS, const TSharedRef& RHS) { return LHS->LuaTableKey < RHS->LuaTableKey; }); + } + OutChildren.Append(Item->Children); + } + } + } + + void OnRegisteredLuaStatesChanged() + { + DetectedLuaStates.Empty(); + TArray States = FLuaMachineModule::Get().GetRegisteredLuaStates(); + for (ULuaState* State : States) + { + DetectedLuaStates.Add(MakeShared(State->GetClass()->GetName())); + } + + if (LuaStatesComboBox.IsValid()) + { + LuaStatesComboBox->RefreshOptions(); + } + + RefreshDebugText(); + } + + FReply CallGC() + { + GEngine->ForceGarbageCollection(true); + return RefreshDebugger(); + } + + FReply CallLuaGC() + { + // avoid calling Lua GC on invalid lua state + RefreshDebugger(); + if (SelectedLuaState && SelectedLuaState->GetInternalLuaState()) + { + SelectedLuaState->GC(LUA_GCCOLLECT); + } + return RefreshDebugger(); + } + + void RefreshDebugText() + { + DebugTextContext.Empty(); + ReferencersTextContext.Empty(); + + for (TObjectIterator StatesIterator; StatesIterator; ++StatesIterator) + { + ULuaState* LuaState = *StatesIterator; +#if ENGINE_MAJOR_VERSION > 4 + if (LuaState->IsValidLowLevel() && !IsValid(LuaState)) +#else + if (LuaState->IsValidLowLevel() && !LuaState->IsPendingKill()) +#endif + { + TArray Referencers; + FReferenceFinder Collector(Referencers, nullptr, false, true, false, false); + Collector.FindReferences(LuaState); + + if (LuaState->GetInternalLuaState()) + { + LuaState->PushRegistryTable(); + int32 RegistrySize = LuaState->ILen(-1); + LuaState->Pop(); + DebugTextContext += FString::Printf(TEXT("%s at 0x%p (%sused memory: %dk) (top of the stack: %d) (registry size: %d) (uobject refs: %d) (tracked user data: %d)\n"), *LuaState->GetName(), LuaState, LuaState->bPersistent ? TEXT("persistent, ") : TEXT(""), LuaState->GC(LUA_GCCOUNT), LuaState->GetTop(), RegistrySize, Referencers.Num(), LuaState->TrackedLuaUserDataObjects.Num()); + } + else + { + DebugTextContext += FString::Printf(TEXT("%s at 0x%p (%sinactive) (uobject refs: %d)\n"), *LuaState->GetName(), LuaState, LuaState->bPersistent ? TEXT("persistent, ") : TEXT(""), Referencers.Num()); + } + } + } + + if (SelectedLuaState) + { + TArray Referencers; + FReferenceFinder Collector(Referencers, nullptr, false, true, false, false); + Collector.FindReferences(SelectedLuaState); + + for (UObject* Referencer : Referencers) + { + ReferencersTextContext += FString::Printf(TEXT("%s\n"), *Referencer->GetFullName()); + } + } + + if (DebugText.IsValid()) + { + DebugText->SetText(FText::FromString(DebugTextContext)); + } + + if (ReferencersText.IsValid()) + { + ReferencersText->SetText(FText::FromString(ReferencersTextContext)); + } + } + + FSlateColor GetLuaTypeColor(TSharedRef Item) + { + switch (Item->LuaTableValue.Type) + { + case ELuaValueType::Nil: + case ELuaValueType::Number: + case ELuaValueType::Integer: + return FSlateColor(FColor::Red); + case ELuaValueType::String: + return FSlateColor(FColor::Orange); + case ELuaValueType::Bool: + return FSlateColor(FColor::Purple); + case ELuaValueType::UFunction: + return FSlateColor(FColor::Magenta); + case ELuaValueType::UObject: + return FSlateColor(FColor::Cyan); + case ELuaValueType::Thread: + return FSlateColor(FColor::Yellow); + default: + return FSlateColor(FLinearColor::White); + } + } + + FText GetLuaTypeText(TSharedRef Item) + { + FString Value = "nil"; + switch (Item->LuaTableValue.Type) + { + case ELuaValueType::String: + Value = "string"; + break; + case ELuaValueType::Bool: + Value = "boolean"; + break; + case ELuaValueType::Integer: + Value = "integer"; + break; + case ELuaValueType::Number: + Value = "number"; + break; + case ELuaValueType::Function: + Value = "function"; + break; + case ELuaValueType::Table: + Value = "table"; + break; + case ELuaValueType::Thread: + Value = "thread"; + break; + case ELuaValueType::UFunction: + Value = "UFunction"; + break; + case ELuaValueType::UObject: + Value = "UObject"; + break; + default: + break; + } + + return FText::FromString(Value); + } + + FText GetLuaValueText(TSharedRef Item) + { + FString Value = ""; + switch (Item->LuaTableValue.Type) + { + case ELuaValueType::String: + case ELuaValueType::Bool: + case ELuaValueType::Integer: + case ELuaValueType::Number: + case ELuaValueType::UFunction: + case ELuaValueType::UObject: + Value = Item->LuaTableValue.ToString(); + break; + case ELuaValueType::Thread: + if (SelectedLuaState == Item->LuaTableValue.LuaState) + { + Value = "status: " + FindObject(nullptr, TEXT("/Script/LuaMachine.ELuaThreadStatus"), true)->GetNameStringByIndex((int32)SelectedLuaState->GetLuaThreadStatus(Item->LuaTableValue)) + ", stack top: " + FString::FromInt(SelectedLuaState->GetLuaThreadStackTop(Item->LuaTableValue)); + } + break; + default: + break; + } + + return FText::FromString(Value); + } + + FReply RefreshDebugger() + { + if (LuaTreeView.IsValid()) + { + if (LuaStatesComboBox.IsValid()) + { + SelectedLuaState = nullptr; + TSharedPtr SelectedText = LuaStatesComboBox->GetSelectedItem(); + if (SelectedText.IsValid()) + { + TArray States = FLuaMachineModule::Get().GetRegisteredLuaStates(); + for (ULuaState* State : States) + { + if (State->GetClass()->GetName() == *SelectedText.Get()) + { + if (State->GetInternalLuaState()) + SelectedLuaState = State; + break; + } + } + } + } + RebuildLuaValues(); + LuaTreeView->RequestTreeRefresh(); + } + + RefreshDebugText(); + return FReply::Handled(); + } + + TSharedRef OnGenerateDebuggerRow(TSharedRef Item, const TSharedRef& OwnerTable) + { + return SNew(STableRow>, OwnerTable) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + [ + SNew(STextBlock).Text(FText::FromString(Item->LuaTableKey)) + ] + + SHorizontalBox::Slot().FillWidth(0.2) + [ + SNew(STextBlock).Text(GetLuaTypeText(Item)) + ] + + SHorizontalBox::Slot() + [ + SNew(STextBlock).Text(GetLuaValueText(Item)).ColorAndOpacity(GetLuaTypeColor(Item)).ToolTipText(GetLuaValueText(Item)) + ] + ]; + } + + void Construct(const FArguments& InArgs) + { + SelectedLuaState = nullptr; + OnRegisteredLuaStatesChanged(); + RebuildLuaValues(); + ChildSlot[ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(EVerticalAlignment::VAlign_Center).HAlign(EHorizontalAlignment::HAlign_Left) + [ + SNew(STextBlock).Text(FText::FromString("Select LuaState to Debug: ")) + ] + + SHorizontalBox::Slot().FillWidth(0.6) + [ + SAssignNew(LuaStatesComboBox, STextComboBox).OptionsSource(&DetectedLuaStates) + ] + ] + + + SVerticalBox::Slot().AutoHeight() + [ + SNew(SButton).Text(FText::FromString("Refresh")).OnClicked(this, &SLuaMachineDebugger::RefreshDebugger) + ] + + SVerticalBox::Slot().AutoHeight() + [ + SNew(SButton).Text(FText::FromString("Call Unreal GC")).OnClicked(this, &SLuaMachineDebugger::CallGC) + ] + + SVerticalBox::Slot().AutoHeight() + [ + SNew(SButton).Text(FText::FromString("Call Lua GC")).OnClicked(this, &SLuaMachineDebugger::CallLuaGC) + ] + + SVerticalBox::Slot().FillHeight(1) + [ + SNew(SScrollBox).AllowOverscroll(EAllowOverscroll::Yes) + + SScrollBox::Slot() + [ + SAssignNew(LuaTreeView, STreeView>).TreeItemsSource(&LuaValues).OnGetChildren(this, &SLuaMachineDebugger::OnGetChildren).OnGenerateRow(this, &SLuaMachineDebugger::OnGenerateDebuggerRow) + ] + ] + + SVerticalBox::Slot().FillHeight(0.1) + [ + SNew(SBorder).BorderBackgroundColor(FColor::White).Padding(4) + [ + SNew(SScrollBox).AllowOverscroll(EAllowOverscroll::Yes) + + SScrollBox::Slot() + [ + SAssignNew(ReferencersText, STextBlock).Text(FText::FromString(ReferencersTextContext)) + ] + ] + ] + + SVerticalBox::Slot().VAlign(EVerticalAlignment::VAlign_Bottom).AutoHeight() + [ + SNew(SBorder).BorderBackgroundColor(FColor::Red).Padding(4) + [ + SAssignNew(DebugText, STextBlock).Text(FText::FromString(DebugTextContext)) + ] + ] + ]; + FLuaMachineModule::Get().OnRegisteredLuaStatesChanged.AddSP(this, &SLuaMachineDebugger::OnRegisteredLuaStatesChanged); + } + + void AddReferencedObjects(FReferenceCollector& Collector) override + { + Collector.AddReferencedObject(SelectedLuaState); + } + +#if ENGINE_MAJOR_VERSION > 4 + virtual FString GetReferencerName() const override + { + return TEXT("SLuaMachineDebugger"); + } +#endif + +protected: + TArray> LuaValues; + TSharedPtr>> LuaTreeView; +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 4 + TObjectPtr SelectedLuaState; +#else + ULuaState* SelectedLuaState; +#endif + TArray> DetectedLuaStates; + TSharedPtr LuaStatesComboBox; + TSharedPtr DebugText; + FString DebugTextContext; + TSharedPtr ReferencersText; + FString ReferencersTextContext; +}; + +TSharedRef FLuaMachineEditorModule::CreateLuaMachineDebugger(const FSpawnTabArgs& Args) +{ + return SNew(SDockTab).TabRole(ETabRole::NomadTab) + [ + SNew(SLuaMachineDebugger) + ]; +} + +void FLuaMachineEditorModule::ShutdownModule() +{ + FCoreDelegates::OnPostEngineInit.RemoveAll(this); + + // Unregister all the asset types that we registered + if (FModuleManager::Get().IsModuleLoaded("AssetTools")) + { + IAssetTools& AssetTools = FModuleManager::GetModuleChecked("AssetTools").Get(); + for (int32 Index = 0; Index < CreatedAssetTypeActions.Num(); ++Index) + { + AssetTools.UnregisterAssetTypeActions(CreatedAssetTypeActions[Index].ToSharedRef()); + } + } + CreatedAssetTypeActions.Empty(); +} + +FLuaMachineEditorModule& FLuaMachineEditorModule::Get() +{ + static FLuaMachineEditorModule* Singleton = nullptr; + if (Singleton == nullptr) + { + Singleton = &FModuleManager::LoadModuleChecked("LuaMachineEditor"); + } + return *Singleton; +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FLuaMachineEditorModule, LuaMachineEditor) +``` + +`Source/LuaMachineEditor/Private/LuaValueCustomization.cpp`: + +```cpp +// Copyright 2018-2023 - Roberto De Ioris + +#include "LuaValueCustomization.h" +#include "LuaMachine/Public/LuaValue.h" +#include "LuaMachine/Public/LuaComponent.h" +#include "GameFramework/Actor.h" +#include "Editor/PropertyEditor/Public/PropertyHandle.h" +#include "Editor/PropertyEditor/Public/DetailLayoutBuilder.h" +#include "Editor/PropertyEditor/Public/IDetailChildrenBuilder.h" +#include "Editor/PropertyEditor/Public/DetailWidgetRow.h" +#include "Runtime/Slate/Public/Widgets/Text/STextBlock.h" +#include "Runtime/Slate/Public/Widgets/Input/STextComboBox.h" +#include "Runtime/Engine/Classes/Engine/BlueprintGeneratedClass.h" +#include "Modules/ModuleManager.h" + + +void FLuaValueCustomization::CustomizeHeader(TSharedRef PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) +{ + TSharedPtr LuaValueTypeProperty = PropertyHandle->GetChildHandle(FName(TEXT("Type"))); + + HeaderRow.NameContent() + [ + PropertyHandle->CreatePropertyNameWidget() + ].ValueContent()[ + LuaValueTypeProperty->CreatePropertyValueWidget() + ]; +} + +EVisibility FLuaValueCustomization::IsPropertyVisible(TSharedRef PropertyHandle, ELuaValueType WantedValueType) +{ + TSharedPtr LuaValueTypeProperty = PropertyHandle->GetChildHandle(FName(TEXT("Type"))); + if (!LuaValueTypeProperty.IsValid()) + return EVisibility::Hidden; + + uint8 ValueType; + LuaValueTypeProperty->GetValue(ValueType); + + if ((ELuaValueType)ValueType == WantedValueType) + return EVisibility::Visible; + + return EVisibility::Hidden; +} + +void FLuaValueCustomization::LuaFunctionChanged(TSharedPtr Value, ESelectInfo::Type SelectionType, TSharedRef PropertyHandle) +{ + TArray Objects; + PropertyHandle->GetOuterObjects(Objects); + + if (Objects.Num() != 1) + return; + + UClass* ObjectClass = Objects[0]->GetClass(); + + ULuaComponent* LuaComponent = Cast(Objects[0]); + if (LuaComponent) + { + AActor* Actor = LuaComponent->GetOwner(); + if (Actor) + { + ObjectClass = Actor->GetClass(); + } + else + { + UBlueprintGeneratedClass* BlueprintClass = Cast(LuaComponent->GetOuter()); + if (BlueprintClass) + { + ObjectClass = BlueprintClass; + } + } + } + + + UFunction* FoundFunction = ObjectClass->FindFunctionByName(FName(*(*Value.Get()))); + if (FoundFunction) + { + PropertyHandle->SetValue(FoundFunction->GetName()); + } +} + +void FLuaValueCustomization::CustomizeChildren(TSharedRef PropertyHandle, IDetailChildrenBuilder& Builder, IPropertyTypeCustomizationUtils& CustomizationUtils) +{ + TArray Objects; + PropertyHandle->GetOuterObjects(Objects); + + if (Objects.Num() != 1) + return; + + TSharedPtr LuaValueBoolProperty = PropertyHandle->GetChildHandle(FName(TEXT("Bool"))); + IDetailPropertyRow& PropertyBoolRow = Builder.AddProperty(LuaValueBoolProperty.ToSharedRef()); + PropertyBoolRow.Visibility(TAttribute::Create(TAttribute::FGetter::CreateRaw(this, &FLuaValueCustomization::IsPropertyVisible, PropertyHandle, ELuaValueType::Bool))); + + TSharedPtr LuaValueStringProperty = PropertyHandle->GetChildHandle(FName(TEXT("String"))); + IDetailPropertyRow& PropertyStringRow = Builder.AddProperty(LuaValueStringProperty.ToSharedRef()); + PropertyStringRow.Visibility(TAttribute::Create(TAttribute::FGetter::CreateRaw(this, &FLuaValueCustomization::IsPropertyVisible, PropertyHandle, ELuaValueType::String))); + + TSharedPtr LuaValueIntegerProperty = PropertyHandle->GetChildHandle(FName(TEXT("Integer"))); + IDetailPropertyRow& PropertyIntegerRow = Builder.AddProperty(LuaValueIntegerProperty.ToSharedRef()); + PropertyIntegerRow.Visibility(TAttribute::Create(TAttribute::FGetter::CreateRaw(this, &FLuaValueCustomization::IsPropertyVisible, PropertyHandle, ELuaValueType::Integer))); + + TSharedPtr LuaValueNumberProperty = PropertyHandle->GetChildHandle(FName(TEXT("Number"))); + IDetailPropertyRow& PropertyNumberRow = Builder.AddProperty(LuaValueNumberProperty.ToSharedRef()); + PropertyNumberRow.Visibility(TAttribute::Create(TAttribute::FGetter::CreateRaw(this, &FLuaValueCustomization::IsPropertyVisible, PropertyHandle, ELuaValueType::Number))); + + TSharedPtr LuaValueObjectProperty = PropertyHandle->GetChildHandle(FName(TEXT("Object"))); + IDetailPropertyRow& PropertyObjectRow = Builder.AddProperty(LuaValueObjectProperty.ToSharedRef()); + PropertyObjectRow.Visibility(TAttribute::Create(TAttribute::FGetter::CreateRaw(this, &FLuaValueCustomization::IsPropertyVisible, PropertyHandle, ELuaValueType::UObject))); + + TSharedPtr LuaValueFunctionProperty = PropertyHandle->GetChildHandle(FName(TEXT("FunctionName"))); + + FString CurrentFunctionName; + LuaValueFunctionProperty->GetValue(CurrentFunctionName); + + TSharedPtr CurrentSelectedFunction; + + UClass* ObjectClass = Objects[0]->GetClass(); + + bool bAllowsRawCall = false; + + ULuaComponent* LuaComponent = Cast(Objects[0]); + if (LuaComponent) + { + AActor* Actor = LuaComponent->GetOwner(); + if (Actor) + { + ObjectClass = Actor->GetClass(); + } + else + { + UBlueprintGeneratedClass* BlueprintClass = Cast(LuaComponent->GetOuter()); + if (BlueprintClass) + { + ObjectClass = BlueprintClass; + } + } + } + else if (ULuaState* LuaState = Cast(Objects[0])) + { + bAllowsRawCall = LuaState->bRawLuaFunctionCall; + } + + for (TFieldIterator Funcs(ObjectClass); Funcs; ++Funcs) + { + UFunction* Function = *Funcs; + + /*if (!Function->HasAnyFunctionFlags(EFunctionFlags::FUNC_Public)) + continue;*/ + + bool bIsValid = true; + + if (!bAllowsRawCall) + { +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + for (TFieldIterator FArgs(Function); FArgs && FArgs->PropertyFlags & CPF_Parm; ++FArgs) +#else + for (TFieldIterator FArgs(Function); FArgs && FArgs->PropertyFlags & CPF_Parm; ++FArgs) +#endif + { +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FProperty* Prop = *FArgs; + FStructProperty* LuaProp = CastField(Prop); +#else + UProperty* Prop = *FArgs; + UStructProperty* LuaProp = Cast(Prop); +#endif + if (!LuaProp) + { + // check for array ? +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + FArrayProperty* ArrayProp = CastField(Prop); +#else + UArrayProperty* ArrayProp = Cast(Prop); +#endif + if (ArrayProp) + { +#if ENGINE_MAJOR_VERSION > 4 || ENGINE_MINOR_VERSION >= 25 + LuaProp = CastField(ArrayProp->Inner); +#else + LuaProp = Cast(ArrayProp->Inner); +#endif + if (!LuaProp) + { + bIsValid = false; + break; + } + } + else { + bIsValid = false; + break; + } + } + if (LuaProp->Struct != FLuaValue::StaticStruct()) + { + bIsValid = false; + break; + } + } + } + + if (!bIsValid) + { + continue; + } + + TSharedPtr FunctionNameSP = MakeShareable(new FString(Function->GetName())); + + if (CurrentFunctionName == Function->GetName()) + { + CurrentSelectedFunction = FunctionNameSP; + } + + ValidLuaFunctions.Add(FunctionNameSP); + } + + + Builder.AddCustomRow(FText::FromString(TEXT("Function"))).ValueContent()[ + SNew(STextComboBox).OptionsSource(&ValidLuaFunctions).OnSelectionChanged_Raw(this, &FLuaValueCustomization::LuaFunctionChanged, LuaValueFunctionProperty.ToSharedRef()).InitiallySelectedItem(CurrentSelectedFunction) + ].Visibility(TAttribute::Create(TAttribute::FGetter::CreateRaw(this, &FLuaValueCustomization::IsPropertyVisible, PropertyHandle, ELuaValueType::UFunction))); +} +``` + +`Source/LuaMachineEditor/Public/LuaCodeCustomization.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "Editor/PropertyEditor/Public/IDetailCustomization.h" + +/** + * + */ + +class FLuaCodeCustomization : public IDetailCustomization +{ +public: + // IDetailCustomization interface + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; + + static TSharedRef MakeInstance() + { + return MakeShareable(new FLuaCodeCustomization()); + } +}; + + +``` + +`Source/LuaMachineEditor/Public/LuaCodeFactory.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris +// Reimport system by yama2akira (Akira Yamamoto) + +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "Developer/AssetTools/Public/AssetTypeCategories.h" +#include "Runtime/Engine/Classes/Engine/Level.h" +#include "EditorReimportHandler.h" +#include "AssetTypeActions_Base.h" +#include "LuaCodeFactory.generated.h" + +/** + * + */ +UCLASS() +class LUAMACHINEEDITOR_API ULuaCodeFactory : public UFactory, public FReimportHandler +{ + GENERATED_UCLASS_BODY() + + virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn, FName CallingContext) override; + virtual UObject* FactoryCreateBinary(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const uint8*& Buffer, const uint8* BufferEnd, FFeedbackContext* Warn, bool& bOutOperationCanceled) override; + + virtual FText GetDisplayName() const override + { + return FText::FromString("Lua Code"); + } + + virtual uint32 GetMenuCategories() const override; + + virtual UObject* ImportObject(UClass* InClass, UObject* InOuter, FName InName, EObjectFlags Flags, const FString& Filename, const TCHAR* Parms, bool& OutCanceled) override + { + if (!Filename.IsEmpty()) + { + CurrentFilename = Filename; + return FactoryCreateFile(InClass, InOuter, InName, Flags, Filename, Parms, nullptr, OutCanceled); + } + + return FactoryCreateNew(InClass, InOuter, InName, Flags, nullptr, GWarn, NAME_None); + } + +public: + virtual bool CanReimport(UObject* Obj, TArray& OutFilenames) override; + virtual void SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) override; + virtual EReimportResult::Type Reimport(UObject* Obj) override; +}; + + +/** + * Add AssetType Actions, so it can show up in Filters, etc. + */ +class FLuaCodeAssetTypeActions : public FAssetTypeActions_Base +{ +public: + FLuaCodeAssetTypeActions( EAssetTypeCategories::Type InAssetCategory ); + // IAssetTypeActions interface + virtual FText GetName() const override; + virtual FColor GetTypeColor() const override; + virtual UClass* GetSupportedClass() const override; + //virtual bool HasActions(const TArray& InObjects) const override { return true; } + //virtual void GetActions(const TArray& InObjects, FMenuBuilder& MenuBuilder) override; + //virtual void OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor = TSharedPtr()) override; + virtual uint32 GetCategories() override; + virtual bool IsImportedAsset() const override; + virtual void GetResolvedSourceFilePaths( const TArray& TypeAssets, TArray& OutSourceFilePaths ) const override; + // End of IAssetTypeActions interface +}; + +``` + +`Source/LuaMachineEditor/Public/LuaMachineEditor.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "Runtime/Launch/Resources/Version.h" +#include "Modules/ModuleManager.h" +#include "LuaState.h" +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 +#include "Styling/SlateStyle.h" +#include "Styling/SlateStyleRegistry.h" +#else +#include "SlateCore/Public/Styling/SlateStyle.h" +#endif +#include "Runtime/Slate/Public/Widgets/Docking/SDockTab.h" +#include "IAssetTools.h" +#include "IAssetTypeActions.h" +#include "AssetTypeCategories.h" + +class FLuaMachineEditorModule : public IModuleInterface +{ +public: + FLuaMachineEditorModule(); + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + virtual void OnPostEngineInit(); + static FLuaMachineEditorModule& Get(); + + TSharedPtr GetStyleSet(); + + virtual uint32 GetAssetCategory() const { return LuaMachineAssetCategoryBit; } + void RegisterAssetTypeAction( IAssetTools& AssetTools, TSharedRef Action ); +private: + TSharedPtr StyleSet; + static TSharedRef CreateLuaMachineDebugger(const FSpawnTabArgs& Args); + EAssetTypeCategories::Type LuaMachineAssetCategoryBit; + /** All created asset type actions. Cached here so that we can unregister them during shutdown. */ + TArray< TSharedPtr > CreatedAssetTypeActions; +}; + +``` + +`Source/LuaMachineEditor/Public/LuaValueCustomization.h`: + +```h +// Copyright 2018-2023 - Roberto De Ioris + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "Editor/PropertyEditor/Public/IPropertyTypeCustomization.h" +#include "Runtime/SlateCore/Public/Layout/Visibility.h" +#include "LuaMachine/Public/LuaValue.h" + +class FLuaValueCustomization : public IPropertyTypeCustomization +{ +public: + // IPropertyTypeCustomization interface + virtual void CustomizeHeader(TSharedRef PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) override; + + virtual void CustomizeChildren(TSharedRef PropertyHandle, IDetailChildrenBuilder& Builder, IPropertyTypeCustomizationUtils& CustomizationUtils) override; + + static TSharedRef MakeInstance() + { + return MakeShareable(new FLuaValueCustomization()); + } + + EVisibility IsPropertyVisible(TSharedRef PropertyHandle, ELuaValueType WantedValueType); + + void LuaFunctionChanged(TSharedPtr Value, ESelectInfo::Type SelectionType, TSharedRef PropertyHandle); +protected: + TArray> ValidLuaFunctions; +}; + + +``` + +`Source/ThirdParty/lua/lauxlib.h`: + +```h +/* +** $Id: lauxlib.h,v 1.131.1.1 2017/04/19 17:20:42 roberto Exp $ +** Auxiliary functions for building Lua libraries +** See Copyright Notice in lua.h +*/ + + +#ifndef lauxlib_h +#define lauxlib_h + + +#include +#include + +#include "lua.h" + + + +/* extra error code for 'luaL_loadfilex' */ +#define LUA_ERRFILE (LUA_ERRERR+1) + + +/* key, in the registry, for table of loaded modules */ +#define LUA_LOADED_TABLE "_LOADED" + + +/* key, in the registry, for table of preloaded loaders */ +#define LUA_PRELOAD_TABLE "_PRELOAD" + + +typedef struct luaL_Reg { + const char *name; + lua_CFunction func; +} luaL_Reg; + + +#define LUAL_NUMSIZES (sizeof(lua_Integer)*16 + sizeof(lua_Number)) + +LUALIB_API void (luaL_checkversion_) (lua_State *L, lua_Number ver, size_t sz); +#define luaL_checkversion(L) \ + luaL_checkversion_(L, LUA_VERSION_NUM, LUAL_NUMSIZES) + +LUALIB_API int (luaL_getmetafield) (lua_State *L, int obj, const char *e); +LUALIB_API int (luaL_callmeta) (lua_State *L, int obj, const char *e); +LUALIB_API const char *(luaL_tolstring) (lua_State *L, int idx, size_t *len); +LUALIB_API int (luaL_argerror) (lua_State *L, int arg, const char *extramsg); +LUALIB_API const char *(luaL_checklstring) (lua_State *L, int arg, + size_t *l); +LUALIB_API const char *(luaL_optlstring) (lua_State *L, int arg, + const char *def, size_t *l); +LUALIB_API lua_Number (luaL_checknumber) (lua_State *L, int arg); +LUALIB_API lua_Number (luaL_optnumber) (lua_State *L, int arg, lua_Number def); + +LUALIB_API lua_Integer (luaL_checkinteger) (lua_State *L, int arg); +LUALIB_API lua_Integer (luaL_optinteger) (lua_State *L, int arg, + lua_Integer def); + +LUALIB_API void (luaL_checkstack) (lua_State *L, int sz, const char *msg); +LUALIB_API void (luaL_checktype) (lua_State *L, int arg, int t); +LUALIB_API void (luaL_checkany) (lua_State *L, int arg); + +LUALIB_API int (luaL_newmetatable) (lua_State *L, const char *tname); +LUALIB_API void (luaL_setmetatable) (lua_State *L, const char *tname); +LUALIB_API void *(luaL_testudata) (lua_State *L, int ud, const char *tname); +LUALIB_API void *(luaL_checkudata) (lua_State *L, int ud, const char *tname); + +LUALIB_API void (luaL_where) (lua_State *L, int lvl); +LUALIB_API int (luaL_error) (lua_State *L, const char *fmt, ...); + +LUALIB_API int (luaL_checkoption) (lua_State *L, int arg, const char *def, + const char *const lst[]); + +LUALIB_API int (luaL_fileresult) (lua_State *L, int stat, const char *fname); +LUALIB_API int (luaL_execresult) (lua_State *L, int stat); + +/* predefined references */ +#define LUA_NOREF (-2) +#define LUA_REFNIL (-1) + +LUALIB_API int (luaL_ref) (lua_State *L, int t); +LUALIB_API void (luaL_unref) (lua_State *L, int t, int ref); + +LUALIB_API int (luaL_loadfilex) (lua_State *L, const char *filename, + const char *mode); + +#define luaL_loadfile(L,f) luaL_loadfilex(L,f,NULL) + +LUALIB_API int (luaL_loadbufferx) (lua_State *L, const char *buff, size_t sz, + const char *name, const char *mode); +LUALIB_API int (luaL_loadstring) (lua_State *L, const char *s); + +LUALIB_API lua_State *(luaL_newstate) (void); + +LUALIB_API lua_Integer (luaL_len) (lua_State *L, int idx); + +LUALIB_API const char *(luaL_gsub) (lua_State *L, const char *s, const char *p, + const char *r); + +LUALIB_API void (luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup); + +LUALIB_API int (luaL_getsubtable) (lua_State *L, int idx, const char *fname); + +LUALIB_API void (luaL_traceback) (lua_State *L, lua_State *L1, + const char *msg, int level); + +LUALIB_API void (luaL_requiref) (lua_State *L, const char *modname, + lua_CFunction openf, int glb); + +/* +** =============================================================== +** some useful macros +** =============================================================== +*/ + + +#define luaL_newlibtable(L,l) \ + lua_createtable(L, 0, sizeof(l)/sizeof((l)[0]) - 1) + +#define luaL_newlib(L,l) \ + (luaL_checkversion(L), luaL_newlibtable(L,l), luaL_setfuncs(L,l,0)) + +#define luaL_argcheck(L, cond,arg,extramsg) \ + ((void)((cond) || luaL_argerror(L, (arg), (extramsg)))) +#define luaL_checkstring(L,n) (luaL_checklstring(L, (n), NULL)) +#define luaL_optstring(L,n,d) (luaL_optlstring(L, (n), (d), NULL)) + +#define luaL_typename(L,i) lua_typename(L, lua_type(L,(i))) + +#define luaL_dofile(L, fn) \ + (luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0)) + +#define luaL_dostring(L, s) \ + (luaL_loadstring(L, s) || lua_pcall(L, 0, LUA_MULTRET, 0)) + +#define luaL_getmetatable(L,n) (lua_getfield(L, LUA_REGISTRYINDEX, (n))) + +#define luaL_opt(L,f,n,d) (lua_isnoneornil(L,(n)) ? (d) : f(L,(n))) + +#define luaL_loadbuffer(L,s,sz,n) luaL_loadbufferx(L,s,sz,n,NULL) + + +/* +** {====================================================== +** Generic Buffer manipulation +** ======================================================= +*/ + +typedef struct luaL_Buffer { + char *b; /* buffer address */ + size_t size; /* buffer size */ + size_t n; /* number of characters in buffer */ + lua_State *L; + char initb[LUAL_BUFFERSIZE]; /* initial buffer */ +} luaL_Buffer; + + +#define luaL_addchar(B,c) \ + ((void)((B)->n < (B)->size || luaL_prepbuffsize((B), 1)), \ + ((B)->b[(B)->n++] = (c))) + +#define luaL_addsize(B,s) ((B)->n += (s)) + +LUALIB_API void (luaL_buffinit) (lua_State *L, luaL_Buffer *B); +LUALIB_API char *(luaL_prepbuffsize) (luaL_Buffer *B, size_t sz); +LUALIB_API void (luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l); +LUALIB_API void (luaL_addstring) (luaL_Buffer *B, const char *s); +LUALIB_API void (luaL_addvalue) (luaL_Buffer *B); +LUALIB_API void (luaL_pushresult) (luaL_Buffer *B); +LUALIB_API void (luaL_pushresultsize) (luaL_Buffer *B, size_t sz); +LUALIB_API char *(luaL_buffinitsize) (lua_State *L, luaL_Buffer *B, size_t sz); + +#define luaL_prepbuffer(B) luaL_prepbuffsize(B, LUAL_BUFFERSIZE) + +/* }====================================================== */ + + + +/* +** {====================================================== +** File handles for IO library +** ======================================================= +*/ + +/* +** A file handle is a userdata with metatable 'LUA_FILEHANDLE' and +** initial structure 'luaL_Stream' (it may contain other fields +** after that initial structure). +*/ + +#define LUA_FILEHANDLE "FILE*" + + +typedef struct luaL_Stream { + FILE *f; /* stream (NULL for incompletely created streams) */ + lua_CFunction closef; /* to close stream (NULL for closed streams) */ +} luaL_Stream; + +/* }====================================================== */ + + + +/* compatibility with old module system */ +#if defined(LUA_COMPAT_MODULE) + +LUALIB_API void (luaL_pushmodule) (lua_State *L, const char *modname, + int sizehint); +LUALIB_API void (luaL_openlib) (lua_State *L, const char *libname, + const luaL_Reg *l, int nup); + +#define luaL_register(L,n,l) (luaL_openlib(L,(n),(l),0)) + +#endif + + +/* +** {================================================================== +** "Abstraction Layer" for basic report of messages and errors +** =================================================================== +*/ + +/* print a string */ +#if !defined(lua_writestring) +#define lua_writestring(s,l) fwrite((s), sizeof(char), (l), stdout) +#endif + +/* print a newline and flush the output */ +#if !defined(lua_writeline) +#define lua_writeline() (lua_writestring("\n", 1), fflush(stdout)) +#endif + +/* print an error message */ +#if !defined(lua_writestringerror) +#define lua_writestringerror(s,p) \ + (fprintf(stderr, (s), (p)), fflush(stderr)) +#endif + +/* }================================================================== */ + + +/* +** {============================================================ +** Compatibility with deprecated conversions +** ============================================================= +*/ +#if defined(LUA_COMPAT_APIINTCASTS) + +#define luaL_checkunsigned(L,a) ((lua_Unsigned)luaL_checkinteger(L,a)) +#define luaL_optunsigned(L,a,d) \ + ((lua_Unsigned)luaL_optinteger(L,a,(lua_Integer)(d))) + +#define luaL_checkint(L,n) ((int)luaL_checkinteger(L, (n))) +#define luaL_optint(L,n,d) ((int)luaL_optinteger(L, (n), (d))) + +#define luaL_checklong(L,n) ((long)luaL_checkinteger(L, (n))) +#define luaL_optlong(L,n,d) ((long)luaL_optinteger(L, (n), (d))) + +#endif +/* }============================================================ */ + + + +#endif + + + +``` + +`Source/ThirdParty/lua/lua.h`: + +```h +/* +** $Id: lua.h,v 1.332.1.2 2018/06/13 16:58:17 roberto Exp $ +** Lua - A Scripting Language +** Lua.org, PUC-Rio, Brazil (http://www.lua.org) +** See Copyright Notice at the end of this file +*/ + + +#ifndef lua_h +#define lua_h + +#include +#include + + +#include "luaconf.h" + + +#define LUA_VERSION_MAJOR "5" +#define LUA_VERSION_MINOR "3" +#define LUA_VERSION_NUM 503 +#define LUA_VERSION_RELEASE "5" + +#define LUA_VERSION "Lua " LUA_VERSION_MAJOR "." LUA_VERSION_MINOR +#define LUA_RELEASE LUA_VERSION "." LUA_VERSION_RELEASE +#define LUA_COPYRIGHT LUA_RELEASE " Copyright (C) 1994-2018 Lua.org, PUC-Rio" +#define LUA_AUTHORS "R. Ierusalimschy, L. H. de Figueiredo, W. Celes" + + +/* mark for precompiled code ('Lua') */ +#define LUA_SIGNATURE "\x1bLua" + +/* option for multiple returns in 'lua_pcall' and 'lua_call' */ +#define LUA_MULTRET (-1) + + +/* +** Pseudo-indices +** (-LUAI_MAXSTACK is the minimum valid index; we keep some free empty +** space after that to help overflow detection) +*/ +#define LUA_REGISTRYINDEX (-LUAI_MAXSTACK - 1000) +#define lua_upvalueindex(i) (LUA_REGISTRYINDEX - (i)) + + +/* thread status */ +#define LUA_OK 0 +#define LUA_YIELD 1 +#define LUA_ERRRUN 2 +#define LUA_ERRSYNTAX 3 +#define LUA_ERRMEM 4 +#define LUA_ERRGCMM 5 +#define LUA_ERRERR 6 + + +typedef struct lua_State lua_State; + + +/* +** basic types +*/ +#define LUA_TNONE (-1) + +#define LUA_TNIL 0 +#define LUA_TBOOLEAN 1 +#define LUA_TLIGHTUSERDATA 2 +#define LUA_TNUMBER 3 +#define LUA_TSTRING 4 +#define LUA_TTABLE 5 +#define LUA_TFUNCTION 6 +#define LUA_TUSERDATA 7 +#define LUA_TTHREAD 8 + +#define LUA_NUMTAGS 9 + + + +/* minimum Lua stack available to a C function */ +#define LUA_MINSTACK 20 + + +/* predefined values in the registry */ +#define LUA_RIDX_MAINTHREAD 1 +#define LUA_RIDX_GLOBALS 2 +#define LUA_RIDX_LAST LUA_RIDX_GLOBALS + + +/* type of numbers in Lua */ +typedef LUA_NUMBER lua_Number; + + +/* type for integer functions */ +typedef LUA_INTEGER lua_Integer; + +/* unsigned integer type */ +typedef LUA_UNSIGNED lua_Unsigned; + +/* type for continuation-function contexts */ +typedef LUA_KCONTEXT lua_KContext; + + +/* +** Type for C functions registered with Lua +*/ +typedef int (*lua_CFunction) (lua_State *L); + +/* +** Type for continuation functions +*/ +typedef int (*lua_KFunction) (lua_State *L, int status, lua_KContext ctx); + + +/* +** Type for functions that read/write blocks when loading/dumping Lua chunks +*/ +typedef const char * (*lua_Reader) (lua_State *L, void *ud, size_t *sz); + +typedef int (*lua_Writer) (lua_State *L, const void *p, size_t sz, void *ud); + + +/* +** Type for memory-allocation functions +*/ +typedef void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize); + + + +/* +** generic extra include file +*/ +#if defined(LUA_USER_H) +#include LUA_USER_H +#endif + + +/* +** RCS ident string +*/ +extern const char lua_ident[]; + + +/* +** state manipulation +*/ +LUA_API lua_State *(lua_newstate) (lua_Alloc f, void *ud); +LUA_API void (lua_close) (lua_State *L); +LUA_API lua_State *(lua_newthread) (lua_State *L); + +LUA_API lua_CFunction (lua_atpanic) (lua_State *L, lua_CFunction panicf); + + +LUA_API const lua_Number *(lua_version) (lua_State *L); + + +/* +** basic stack manipulation +*/ +LUA_API int (lua_absindex) (lua_State *L, int idx); +LUA_API int (lua_gettop) (lua_State *L); +LUA_API void (lua_settop) (lua_State *L, int idx); +LUA_API void (lua_pushvalue) (lua_State *L, int idx); +LUA_API void (lua_rotate) (lua_State *L, int idx, int n); +LUA_API void (lua_copy) (lua_State *L, int fromidx, int toidx); +LUA_API int (lua_checkstack) (lua_State *L, int n); + +LUA_API void (lua_xmove) (lua_State *from, lua_State *to, int n); + + +/* +** access functions (stack -> C) +*/ + +LUA_API int (lua_isnumber) (lua_State *L, int idx); +LUA_API int (lua_isstring) (lua_State *L, int idx); +LUA_API int (lua_iscfunction) (lua_State *L, int idx); +LUA_API int (lua_isinteger) (lua_State *L, int idx); +LUA_API int (lua_isuserdata) (lua_State *L, int idx); +LUA_API int (lua_type) (lua_State *L, int idx); +LUA_API const char *(lua_typename) (lua_State *L, int tp); + +LUA_API lua_Number (lua_tonumberx) (lua_State *L, int idx, int *isnum); +LUA_API lua_Integer (lua_tointegerx) (lua_State *L, int idx, int *isnum); +LUA_API int (lua_toboolean) (lua_State *L, int idx); +LUA_API const char *(lua_tolstring) (lua_State *L, int idx, size_t *len); +LUA_API size_t (lua_rawlen) (lua_State *L, int idx); +LUA_API lua_CFunction (lua_tocfunction) (lua_State *L, int idx); +LUA_API void *(lua_touserdata) (lua_State *L, int idx); +LUA_API lua_State *(lua_tothread) (lua_State *L, int idx); +LUA_API const void *(lua_topointer) (lua_State *L, int idx); + + +/* +** Comparison and arithmetic functions +*/ + +#define LUA_OPADD 0 /* ORDER TM, ORDER OP */ +#define LUA_OPSUB 1 +#define LUA_OPMUL 2 +#define LUA_OPMOD 3 +#define LUA_OPPOW 4 +#define LUA_OPDIV 5 +#define LUA_OPIDIV 6 +#define LUA_OPBAND 7 +#define LUA_OPBOR 8 +#define LUA_OPBXOR 9 +#define LUA_OPSHL 10 +#define LUA_OPSHR 11 +#define LUA_OPUNM 12 +#define LUA_OPBNOT 13 + +LUA_API void (lua_arith) (lua_State *L, int op); + +#define LUA_OPEQ 0 +#define LUA_OPLT 1 +#define LUA_OPLE 2 + +LUA_API int (lua_rawequal) (lua_State *L, int idx1, int idx2); +LUA_API int (lua_compare) (lua_State *L, int idx1, int idx2, int op); + + +/* +** push functions (C -> stack) +*/ +LUA_API void (lua_pushnil) (lua_State *L); +LUA_API void (lua_pushnumber) (lua_State *L, lua_Number n); +LUA_API void (lua_pushinteger) (lua_State *L, lua_Integer n); +LUA_API const char *(lua_pushlstring) (lua_State *L, const char *s, size_t len); +LUA_API const char *(lua_pushstring) (lua_State *L, const char *s); +LUA_API const char *(lua_pushvfstring) (lua_State *L, const char *fmt, + va_list argp); +LUA_API const char *(lua_pushfstring) (lua_State *L, const char *fmt, ...); +LUA_API void (lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n); +LUA_API void (lua_pushboolean) (lua_State *L, int b); +LUA_API void (lua_pushlightuserdata) (lua_State *L, void *p); +LUA_API int (lua_pushthread) (lua_State *L); + + +/* +** get functions (Lua -> stack) +*/ +LUA_API int (lua_getglobal) (lua_State *L, const char *name); +LUA_API int (lua_gettable) (lua_State *L, int idx); +LUA_API int (lua_getfield) (lua_State *L, int idx, const char *k); +LUA_API int (lua_geti) (lua_State *L, int idx, lua_Integer n); +LUA_API int (lua_rawget) (lua_State *L, int idx); +LUA_API int (lua_rawgeti) (lua_State *L, int idx, lua_Integer n); +LUA_API int (lua_rawgetp) (lua_State *L, int idx, const void *p); + +LUA_API void (lua_createtable) (lua_State *L, int narr, int nrec); +LUA_API void *(lua_newuserdata) (lua_State *L, size_t sz); +LUA_API int (lua_getmetatable) (lua_State *L, int objindex); +LUA_API int (lua_getuservalue) (lua_State *L, int idx); + + +/* +** set functions (stack -> Lua) +*/ +LUA_API void (lua_setglobal) (lua_State *L, const char *name); +LUA_API void (lua_settable) (lua_State *L, int idx); +LUA_API void (lua_setfield) (lua_State *L, int idx, const char *k); +LUA_API void (lua_seti) (lua_State *L, int idx, lua_Integer n); +LUA_API void (lua_rawset) (lua_State *L, int idx); +LUA_API void (lua_rawseti) (lua_State *L, int idx, lua_Integer n); +LUA_API void (lua_rawsetp) (lua_State *L, int idx, const void *p); +LUA_API int (lua_setmetatable) (lua_State *L, int objindex); +LUA_API void (lua_setuservalue) (lua_State *L, int idx); + + +/* +** 'load' and 'call' functions (load and run Lua code) +*/ +LUA_API void (lua_callk) (lua_State *L, int nargs, int nresults, + lua_KContext ctx, lua_KFunction k); +#define lua_call(L,n,r) lua_callk(L, (n), (r), 0, NULL) + +LUA_API int (lua_pcallk) (lua_State *L, int nargs, int nresults, int errfunc, + lua_KContext ctx, lua_KFunction k); +#define lua_pcall(L,n,r,f) lua_pcallk(L, (n), (r), (f), 0, NULL) + +LUA_API int (lua_load) (lua_State *L, lua_Reader reader, void *dt, + const char *chunkname, const char *mode); + +LUA_API int (lua_dump) (lua_State *L, lua_Writer writer, void *data, int strip); + + +/* +** coroutine functions +*/ +LUA_API int (lua_yieldk) (lua_State *L, int nresults, lua_KContext ctx, + lua_KFunction k); +LUA_API int (lua_resume) (lua_State *L, lua_State *from, int narg); +LUA_API int (lua_status) (lua_State *L); +LUA_API int (lua_isyieldable) (lua_State *L); + +#define lua_yield(L,n) lua_yieldk(L, (n), 0, NULL) + + +/* +** garbage-collection function and options +*/ + +#define LUA_GCSTOP 0 +#define LUA_GCRESTART 1 +#define LUA_GCCOLLECT 2 +#define LUA_GCCOUNT 3 +#define LUA_GCCOUNTB 4 +#define LUA_GCSTEP 5 +#define LUA_GCSETPAUSE 6 +#define LUA_GCSETSTEPMUL 7 +#define LUA_GCISRUNNING 9 + +LUA_API int (lua_gc) (lua_State *L, int what, int data); + + +/* +** miscellaneous functions +*/ + +LUA_API int (lua_error) (lua_State *L); + +LUA_API int (lua_next) (lua_State *L, int idx); + +LUA_API void (lua_concat) (lua_State *L, int n); +LUA_API void (lua_len) (lua_State *L, int idx); + +LUA_API size_t (lua_stringtonumber) (lua_State *L, const char *s); + +LUA_API lua_Alloc (lua_getallocf) (lua_State *L, void **ud); +LUA_API void (lua_setallocf) (lua_State *L, lua_Alloc f, void *ud); + + + +/* +** {============================================================== +** some useful macros +** =============================================================== +*/ + +#define lua_getextraspace(L) ((void *)((char *)(L) - LUA_EXTRASPACE)) + +#define lua_tonumber(L,i) lua_tonumberx(L,(i),NULL) +#define lua_tointeger(L,i) lua_tointegerx(L,(i),NULL) + +#define lua_pop(L,n) lua_settop(L, -(n)-1) + +#define lua_newtable(L) lua_createtable(L, 0, 0) + +#define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n))) + +#define lua_pushcfunction(L,f) lua_pushcclosure(L, (f), 0) + +#define lua_isfunction(L,n) (lua_type(L, (n)) == LUA_TFUNCTION) +#define lua_istable(L,n) (lua_type(L, (n)) == LUA_TTABLE) +#define lua_islightuserdata(L,n) (lua_type(L, (n)) == LUA_TLIGHTUSERDATA) +#define lua_isnil(L,n) (lua_type(L, (n)) == LUA_TNIL) +#define lua_isboolean(L,n) (lua_type(L, (n)) == LUA_TBOOLEAN) +#define lua_isthread(L,n) (lua_type(L, (n)) == LUA_TTHREAD) +#define lua_isnone(L,n) (lua_type(L, (n)) == LUA_TNONE) +#define lua_isnoneornil(L, n) (lua_type(L, (n)) <= 0) + +#define lua_pushliteral(L, s) lua_pushstring(L, "" s) + +#define lua_pushglobaltable(L) \ + ((void)lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS)) + +#define lua_tostring(L,i) lua_tolstring(L, (i), NULL) + + +#define lua_insert(L,idx) lua_rotate(L, (idx), 1) + +#define lua_remove(L,idx) (lua_rotate(L, (idx), -1), lua_pop(L, 1)) + +#define lua_replace(L,idx) (lua_copy(L, -1, (idx)), lua_pop(L, 1)) + +/* }============================================================== */ + + +/* +** {============================================================== +** compatibility macros for unsigned conversions +** =============================================================== +*/ +#if defined(LUA_COMPAT_APIINTCASTS) + +#define lua_pushunsigned(L,n) lua_pushinteger(L, (lua_Integer)(n)) +#define lua_tounsignedx(L,i,is) ((lua_Unsigned)lua_tointegerx(L,i,is)) +#define lua_tounsigned(L,i) lua_tounsignedx(L,(i),NULL) + +#endif +/* }============================================================== */ + +/* +** {====================================================================== +** Debug API +** ======================================================================= +*/ + + +/* +** Event codes +*/ +#define LUA_HOOKCALL 0 +#define LUA_HOOKRET 1 +#define LUA_HOOKLINE 2 +#define LUA_HOOKCOUNT 3 +#define LUA_HOOKTAILCALL 4 + + +/* +** Event masks +*/ +#define LUA_MASKCALL (1 << LUA_HOOKCALL) +#define LUA_MASKRET (1 << LUA_HOOKRET) +#define LUA_MASKLINE (1 << LUA_HOOKLINE) +#define LUA_MASKCOUNT (1 << LUA_HOOKCOUNT) + +typedef struct lua_Debug lua_Debug; /* activation record */ + + +/* Functions to be called by the debugger in specific events */ +typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar); + + +LUA_API int (lua_getstack) (lua_State *L, int level, lua_Debug *ar); +LUA_API int (lua_getinfo) (lua_State *L, const char *what, lua_Debug *ar); +LUA_API const char *(lua_getlocal) (lua_State *L, const lua_Debug *ar, int n); +LUA_API const char *(lua_setlocal) (lua_State *L, const lua_Debug *ar, int n); +LUA_API const char *(lua_getupvalue) (lua_State *L, int funcindex, int n); +LUA_API const char *(lua_setupvalue) (lua_State *L, int funcindex, int n); + +LUA_API void *(lua_upvalueid) (lua_State *L, int fidx, int n); +LUA_API void (lua_upvaluejoin) (lua_State *L, int fidx1, int n1, + int fidx2, int n2); + +LUA_API void (lua_sethook) (lua_State *L, lua_Hook func, int mask, int count); +LUA_API lua_Hook (lua_gethook) (lua_State *L); +LUA_API int (lua_gethookmask) (lua_State *L); +LUA_API int (lua_gethookcount) (lua_State *L); + + +struct lua_Debug { + int event; + const char *name; /* (n) */ + const char *namewhat; /* (n) 'global', 'local', 'field', 'method' */ + const char *what; /* (S) 'Lua', 'C', 'main', 'tail' */ + const char *source; /* (S) */ + int currentline; /* (l) */ + int linedefined; /* (S) */ + int lastlinedefined; /* (S) */ + unsigned char nups; /* (u) number of upvalues */ + unsigned char nparams;/* (u) number of parameters */ + char isvararg; /* (u) */ + char istailcall; /* (t) */ + char short_src[LUA_IDSIZE]; /* (S) */ + /* private part */ + struct CallInfo *i_ci; /* active function */ +}; + +/* }====================================================================== */ + + +/****************************************************************************** +* Copyright (C) 1994-2018 Lua.org, PUC-Rio. +* +* Permission is hereby granted, free of charge, to any person obtaining +* a copy of this software and associated documentation files (the +* "Software"), to deal in the Software without restriction, including +* without limitation the rights to use, copy, modify, merge, publish, +* distribute, sublicense, and/or sell copies of the Software, and to +* permit persons to whom the Software is furnished to do so, subject to +* the following conditions: +* +* The above copyright notice and this permission notice shall be +* included in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +******************************************************************************/ + + +#endif + +``` + +`Source/ThirdParty/lua/lua.hpp`: + +```hpp +// lua.hpp +// Lua header files for C++ +// <> not supplied automatically because Lua also compiles as C++ + +extern "C" { +#include "lua.h" +#include "lualib.h" +#include "lauxlib.h" +} + +``` + +`Source/ThirdParty/lua/luaconf.h`: + +```h +/* +** $Id: luaconf.h,v 1.259.1.1 2017/04/19 17:29:57 roberto Exp $ +** Configuration file for Lua +** See Copyright Notice in lua.h +*/ + + +#ifndef luaconf_h +#define luaconf_h + +#include +#include + + +/* +** =================================================================== +** Search for "@@" to find all configurable definitions. +** =================================================================== +*/ + + +/* +** {==================================================================== +** System Configuration: macros to adapt (if needed) Lua to some +** particular platform, for instance compiling it with 32-bit numbers or +** restricting it to C89. +** ===================================================================== +*/ + +/* +@@ LUA_32BITS enables Lua with 32-bit integers and 32-bit floats. You +** can also define LUA_32BITS in the make file, but changing here you +** ensure that all software connected to Lua will be compiled with the +** same configuration. +*/ +/* #define LUA_32BITS */ + + +/* +@@ LUA_USE_C89 controls the use of non-ISO-C89 features. +** Define it if you want Lua to avoid the use of a few C99 features +** or Windows-specific features on Windows. +*/ +/* #define LUA_USE_C89 */ + + +/* +** By default, Lua on Windows use (some) specific Windows features +*/ +#if !defined(LUA_USE_C89) && defined(_WIN32) && !defined(_WIN32_WCE) +#define LUA_USE_WINDOWS /* enable goodies for regular Windows */ +#endif + + +#if defined(LUA_USE_WINDOWS) +#define LUA_DL_DLL /* enable support for DLL */ +#define LUA_USE_C89 /* broadly, Windows is C89 */ +#endif + + +#if defined(LUA_USE_LINUX) +#define LUA_USE_POSIX +#define LUA_USE_DLOPEN /* needs an extra library: -ldl */ +#define LUA_USE_READLINE /* needs some extra libraries */ +#endif + + +#if defined(LUA_USE_MACOSX) +#define LUA_USE_POSIX +#define LUA_USE_DLOPEN /* MacOS does not need -ldl */ +#define LUA_USE_READLINE /* needs an extra library: -lreadline */ +#endif + + +/* +@@ LUA_C89_NUMBERS ensures that Lua uses the largest types available for +** C89 ('long' and 'double'); Windows always has '__int64', so it does +** not need to use this case. +*/ +#if defined(LUA_USE_C89) && !defined(LUA_USE_WINDOWS) +#define LUA_C89_NUMBERS +#endif + + + +/* +@@ LUAI_BITSINT defines the (minimum) number of bits in an 'int'. +*/ +/* avoid undefined shifts */ +#if ((INT_MAX >> 15) >> 15) >= 1 +#define LUAI_BITSINT 32 +#else +/* 'int' always must have at least 16 bits */ +#define LUAI_BITSINT 16 +#endif + + +/* +@@ LUA_INT_TYPE defines the type for Lua integers. +@@ LUA_FLOAT_TYPE defines the type for Lua floats. +** Lua should work fine with any mix of these options (if supported +** by your C compiler). The usual configurations are 64-bit integers +** and 'double' (the default), 32-bit integers and 'float' (for +** restricted platforms), and 'long'/'double' (for C compilers not +** compliant with C99, which may not have support for 'long long'). +*/ + +/* predefined options for LUA_INT_TYPE */ +#define LUA_INT_INT 1 +#define LUA_INT_LONG 2 +#define LUA_INT_LONGLONG 3 + +/* predefined options for LUA_FLOAT_TYPE */ +#define LUA_FLOAT_FLOAT 1 +#define LUA_FLOAT_DOUBLE 2 +#define LUA_FLOAT_LONGDOUBLE 3 + +#if defined(LUA_32BITS) /* { */ +/* +** 32-bit integers and 'float' +*/ +#if LUAI_BITSINT >= 32 /* use 'int' if big enough */ +#define LUA_INT_TYPE LUA_INT_INT +#else /* otherwise use 'long' */ +#define LUA_INT_TYPE LUA_INT_LONG +#endif +#define LUA_FLOAT_TYPE LUA_FLOAT_FLOAT + +#elif defined(LUA_C89_NUMBERS) /* }{ */ +/* +** largest types available for C89 ('long' and 'double') +*/ +#define LUA_INT_TYPE LUA_INT_LONG +#define LUA_FLOAT_TYPE LUA_FLOAT_DOUBLE + +#endif /* } */ + + +/* +** default configuration for 64-bit Lua ('long long' and 'double') +*/ +#if !defined(LUA_INT_TYPE) +#define LUA_INT_TYPE LUA_INT_LONGLONG +#endif + +#if !defined(LUA_FLOAT_TYPE) +#define LUA_FLOAT_TYPE LUA_FLOAT_DOUBLE +#endif + +/* }================================================================== */ + + + + +/* +** {================================================================== +** Configuration for Paths. +** =================================================================== +*/ + +/* +** LUA_PATH_SEP is the character that separates templates in a path. +** LUA_PATH_MARK is the string that marks the substitution points in a +** template. +** LUA_EXEC_DIR in a Windows path is replaced by the executable's +** directory. +*/ +#define LUA_PATH_SEP ";" +#define LUA_PATH_MARK "?" +#define LUA_EXEC_DIR "!" + + +/* +@@ LUA_PATH_DEFAULT is the default path that Lua uses to look for +** Lua libraries. +@@ LUA_CPATH_DEFAULT is the default path that Lua uses to look for +** C libraries. +** CHANGE them if your machine has a non-conventional directory +** hierarchy or if you want to install your libraries in +** non-conventional directories. +*/ +#define LUA_VDIR LUA_VERSION_MAJOR "." LUA_VERSION_MINOR +#if defined(_WIN32) /* { */ +/* +** In Windows, any exclamation mark ('!') in the path is replaced by the +** path of the directory of the executable file of the current process. +*/ +#define LUA_LDIR "!\\lua\\" +#define LUA_CDIR "!\\" +#define LUA_SHRDIR "!\\..\\share\\lua\\" LUA_VDIR "\\" +#define LUA_PATH_DEFAULT \ + LUA_LDIR"?.lua;" LUA_LDIR"?\\init.lua;" \ + LUA_CDIR"?.lua;" LUA_CDIR"?\\init.lua;" \ + LUA_SHRDIR"?.lua;" LUA_SHRDIR"?\\init.lua;" \ + ".\\?.lua;" ".\\?\\init.lua" +#define LUA_CPATH_DEFAULT \ + LUA_CDIR"?.dll;" \ + LUA_CDIR"..\\lib\\lua\\" LUA_VDIR "\\?.dll;" \ + LUA_CDIR"loadall.dll;" ".\\?.dll" + +#else /* }{ */ + +#define LUA_ROOT "/usr/local/" +#define LUA_LDIR LUA_ROOT "share/lua/" LUA_VDIR "/" +#define LUA_CDIR LUA_ROOT "lib/lua/" LUA_VDIR "/" +#define LUA_PATH_DEFAULT \ + LUA_LDIR"?.lua;" LUA_LDIR"?/init.lua;" \ + LUA_CDIR"?.lua;" LUA_CDIR"?/init.lua;" \ + "./?.lua;" "./?/init.lua" +#define LUA_CPATH_DEFAULT \ + LUA_CDIR"?.so;" LUA_CDIR"loadall.so;" "./?.so" +#endif /* } */ + + +/* +@@ LUA_DIRSEP is the directory separator (for submodules). +** CHANGE it if your machine does not use "/" as the directory separator +** and is not Windows. (On Windows Lua automatically uses "\".) +*/ +#if defined(_WIN32) +#define LUA_DIRSEP "\\" +#else +#define LUA_DIRSEP "/" +#endif + +/* }================================================================== */ + + +/* +** {================================================================== +** Marks for exported symbols in the C code +** =================================================================== +*/ + +/* +@@ LUA_API is a mark for all core API functions. +@@ LUALIB_API is a mark for all auxiliary library functions. +@@ LUAMOD_API is a mark for all standard library opening functions. +** CHANGE them if you need to define those functions in some special way. +** For instance, if you want to create one Windows DLL with the core and +** the libraries, you may want to use the following definition (define +** LUA_BUILD_AS_DLL to get it). +*/ +#if defined(LUA_BUILD_AS_DLL) /* { */ + +#if defined(LUA_CORE) || defined(LUA_LIB) /* { */ +#define LUA_API __declspec(dllexport) +#else /* }{ */ +#define LUA_API __declspec(dllimport) +#endif /* } */ + +#else /* }{ */ + +#define LUA_API extern + +#endif /* } */ + + +/* more often than not the libs go together with the core */ +#define LUALIB_API LUA_API +#define LUAMOD_API LUALIB_API + + +/* +@@ LUAI_FUNC is a mark for all extern functions that are not to be +** exported to outside modules. +@@ LUAI_DDEF and LUAI_DDEC are marks for all extern (const) variables +** that are not to be exported to outside modules (LUAI_DDEF for +** definitions and LUAI_DDEC for declarations). +** CHANGE them if you need to mark them in some special way. Elf/gcc +** (versions 3.2 and later) mark them as "hidden" to optimize access +** when Lua is compiled as a shared library. Not all elf targets support +** this attribute. Unfortunately, gcc does not offer a way to check +** whether the target offers that support, and those without support +** give a warning about it. To avoid these warnings, change to the +** default definition. +*/ +#if defined(__GNUC__) && ((__GNUC__*100 + __GNUC_MINOR__) >= 302) && \ + defined(__ELF__) /* { */ +#define LUAI_FUNC __attribute__((visibility("hidden"))) extern +#else /* }{ */ +#define LUAI_FUNC extern +#endif /* } */ + +#define LUAI_DDEC LUAI_FUNC +#define LUAI_DDEF /* empty */ + +/* }================================================================== */ + + +/* +** {================================================================== +** Compatibility with previous versions +** =================================================================== +*/ + +/* +@@ LUA_COMPAT_5_2 controls other macros for compatibility with Lua 5.2. +@@ LUA_COMPAT_5_1 controls other macros for compatibility with Lua 5.1. +** You can define it to get all options, or change specific options +** to fit your specific needs. +*/ +#if defined(LUA_COMPAT_5_2) /* { */ + +/* +@@ LUA_COMPAT_MATHLIB controls the presence of several deprecated +** functions in the mathematical library. +*/ +#define LUA_COMPAT_MATHLIB + +/* +@@ LUA_COMPAT_BITLIB controls the presence of library 'bit32'. +*/ +#define LUA_COMPAT_BITLIB + +/* +@@ LUA_COMPAT_IPAIRS controls the effectiveness of the __ipairs metamethod. +*/ +#define LUA_COMPAT_IPAIRS + +/* +@@ LUA_COMPAT_APIINTCASTS controls the presence of macros for +** manipulating other integer types (lua_pushunsigned, lua_tounsigned, +** luaL_checkint, luaL_checklong, etc.) +*/ +#define LUA_COMPAT_APIINTCASTS + +#endif /* } */ + + +#if defined(LUA_COMPAT_5_1) /* { */ + +/* Incompatibilities from 5.2 -> 5.3 */ +#define LUA_COMPAT_MATHLIB +#define LUA_COMPAT_APIINTCASTS + +/* +@@ LUA_COMPAT_UNPACK controls the presence of global 'unpack'. +** You can replace it with 'table.unpack'. +*/ +#define LUA_COMPAT_UNPACK + +/* +@@ LUA_COMPAT_LOADERS controls the presence of table 'package.loaders'. +** You can replace it with 'package.searchers'. +*/ +#define LUA_COMPAT_LOADERS + +/* +@@ macro 'lua_cpcall' emulates deprecated function lua_cpcall. +** You can call your C function directly (with light C functions). +*/ +#define lua_cpcall(L,f,u) \ + (lua_pushcfunction(L, (f)), \ + lua_pushlightuserdata(L,(u)), \ + lua_pcall(L,1,0,0)) + + +/* +@@ LUA_COMPAT_LOG10 defines the function 'log10' in the math library. +** You can rewrite 'log10(x)' as 'log(x, 10)'. +*/ +#define LUA_COMPAT_LOG10 + +/* +@@ LUA_COMPAT_LOADSTRING defines the function 'loadstring' in the base +** library. You can rewrite 'loadstring(s)' as 'load(s)'. +*/ +#define LUA_COMPAT_LOADSTRING + +/* +@@ LUA_COMPAT_MAXN defines the function 'maxn' in the table library. +*/ +#define LUA_COMPAT_MAXN + +/* +@@ The following macros supply trivial compatibility for some +** changes in the API. The macros themselves document how to +** change your code to avoid using them. +*/ +#define lua_strlen(L,i) lua_rawlen(L, (i)) + +#define lua_objlen(L,i) lua_rawlen(L, (i)) + +#define lua_equal(L,idx1,idx2) lua_compare(L,(idx1),(idx2),LUA_OPEQ) +#define lua_lessthan(L,idx1,idx2) lua_compare(L,(idx1),(idx2),LUA_OPLT) + +/* +@@ LUA_COMPAT_MODULE controls compatibility with previous +** module functions 'module' (Lua) and 'luaL_register' (C). +*/ +#define LUA_COMPAT_MODULE + +#endif /* } */ + + +/* +@@ LUA_COMPAT_FLOATSTRING makes Lua format integral floats without a +@@ a float mark ('.0'). +** This macro is not on by default even in compatibility mode, +** because this is not really an incompatibility. +*/ +/* #define LUA_COMPAT_FLOATSTRING */ + +/* }================================================================== */ + + + +/* +** {================================================================== +** Configuration for Numbers. +** Change these definitions if no predefined LUA_FLOAT_* / LUA_INT_* +** satisfy your needs. +** =================================================================== +*/ + +/* +@@ LUA_NUMBER is the floating-point type used by Lua. +@@ LUAI_UACNUMBER is the result of a 'default argument promotion' +@@ over a floating number. +@@ l_mathlim(x) corrects limit name 'x' to the proper float type +** by prefixing it with one of FLT/DBL/LDBL. +@@ LUA_NUMBER_FRMLEN is the length modifier for writing floats. +@@ LUA_NUMBER_FMT is the format for writing floats. +@@ lua_number2str converts a float to a string. +@@ l_mathop allows the addition of an 'l' or 'f' to all math operations. +@@ l_floor takes the floor of a float. +@@ lua_str2number converts a decimal numeric string to a number. +*/ + + +/* The following definitions are good for most cases here */ + +#define l_floor(x) (l_mathop(floor)(x)) + +#define lua_number2str(s,sz,n) \ + l_sprintf((s), sz, LUA_NUMBER_FMT, (LUAI_UACNUMBER)(n)) + +/* +@@ lua_numbertointeger converts a float number to an integer, or +** returns 0 if float is not within the range of a lua_Integer. +** (The range comparisons are tricky because of rounding. The tests +** here assume a two-complement representation, where MININTEGER always +** has an exact representation as a float; MAXINTEGER may not have one, +** and therefore its conversion to float may have an ill-defined value.) +*/ +#define lua_numbertointeger(n,p) \ + ((n) >= (LUA_NUMBER)(LUA_MININTEGER) && \ + (n) < -(LUA_NUMBER)(LUA_MININTEGER) && \ + (*(p) = (LUA_INTEGER)(n), 1)) + + +/* now the variable definitions */ + +#if LUA_FLOAT_TYPE == LUA_FLOAT_FLOAT /* { single float */ + +#define LUA_NUMBER float + +#define l_mathlim(n) (FLT_##n) + +#define LUAI_UACNUMBER double + +#define LUA_NUMBER_FRMLEN "" +#define LUA_NUMBER_FMT "%.7g" + +#define l_mathop(op) op##f + +#define lua_str2number(s,p) strtof((s), (p)) + + +#elif LUA_FLOAT_TYPE == LUA_FLOAT_LONGDOUBLE /* }{ long double */ + +#define LUA_NUMBER long double + +#define l_mathlim(n) (LDBL_##n) + +#define LUAI_UACNUMBER long double + +#define LUA_NUMBER_FRMLEN "L" +#define LUA_NUMBER_FMT "%.19Lg" + +#define l_mathop(op) op##l + +#define lua_str2number(s,p) strtold((s), (p)) + +#elif LUA_FLOAT_TYPE == LUA_FLOAT_DOUBLE /* }{ double */ + +#define LUA_NUMBER double + +#define l_mathlim(n) (DBL_##n) + +#define LUAI_UACNUMBER double + +#define LUA_NUMBER_FRMLEN "" +#define LUA_NUMBER_FMT "%.14g" + +#define l_mathop(op) op + +#define lua_str2number(s,p) strtod((s), (p)) + +#else /* }{ */ + +#error "numeric float type not defined" + +#endif /* } */ + + + +/* +@@ LUA_INTEGER is the integer type used by Lua. +** +@@ LUA_UNSIGNED is the unsigned version of LUA_INTEGER. +** +@@ LUAI_UACINT is the result of a 'default argument promotion' +@@ over a lUA_INTEGER. +@@ LUA_INTEGER_FRMLEN is the length modifier for reading/writing integers. +@@ LUA_INTEGER_FMT is the format for writing integers. +@@ LUA_MAXINTEGER is the maximum value for a LUA_INTEGER. +@@ LUA_MININTEGER is the minimum value for a LUA_INTEGER. +@@ lua_integer2str converts an integer to a string. +*/ + + +/* The following definitions are good for most cases here */ + +#define LUA_INTEGER_FMT "%" LUA_INTEGER_FRMLEN "d" + +#define LUAI_UACINT LUA_INTEGER + +#define lua_integer2str(s,sz,n) \ + l_sprintf((s), sz, LUA_INTEGER_FMT, (LUAI_UACINT)(n)) + +/* +** use LUAI_UACINT here to avoid problems with promotions (which +** can turn a comparison between unsigneds into a signed comparison) +*/ +#define LUA_UNSIGNED unsigned LUAI_UACINT + + +/* now the variable definitions */ + +#if LUA_INT_TYPE == LUA_INT_INT /* { int */ + +#define LUA_INTEGER int +#define LUA_INTEGER_FRMLEN "" + +#define LUA_MAXINTEGER INT_MAX +#define LUA_MININTEGER INT_MIN + +#elif LUA_INT_TYPE == LUA_INT_LONG /* }{ long */ + +#define LUA_INTEGER long +#define LUA_INTEGER_FRMLEN "l" + +#define LUA_MAXINTEGER LONG_MAX +#define LUA_MININTEGER LONG_MIN + +#elif LUA_INT_TYPE == LUA_INT_LONGLONG /* }{ long long */ + +/* use presence of macro LLONG_MAX as proxy for C99 compliance */ +#if defined(LLONG_MAX) /* { */ +/* use ISO C99 stuff */ + +#define LUA_INTEGER long long +#define LUA_INTEGER_FRMLEN "ll" + +#define LUA_MAXINTEGER LLONG_MAX +#define LUA_MININTEGER LLONG_MIN + +#elif defined(LUA_USE_WINDOWS) /* }{ */ +/* in Windows, can use specific Windows types */ + +#define LUA_INTEGER __int64 +#define LUA_INTEGER_FRMLEN "I64" + +#define LUA_MAXINTEGER _I64_MAX +#define LUA_MININTEGER _I64_MIN + +#else /* }{ */ + +#error "Compiler does not support 'long long'. Use option '-DLUA_32BITS' \ + or '-DLUA_C89_NUMBERS' (see file 'luaconf.h' for details)" + +#endif /* } */ + +#else /* }{ */ + +#error "numeric integer type not defined" + +#endif /* } */ + +/* }================================================================== */ + + +/* +** {================================================================== +** Dependencies with C99 and other C details +** =================================================================== +*/ + +/* +@@ l_sprintf is equivalent to 'snprintf' or 'sprintf' in C89. +** (All uses in Lua have only one format item.) +*/ +#if !defined(LUA_USE_C89) +#define l_sprintf(s,sz,f,i) snprintf(s,sz,f,i) +#else +#define l_sprintf(s,sz,f,i) ((void)(sz), sprintf(s,f,i)) +#endif + + +/* +@@ lua_strx2number converts an hexadecimal numeric string to a number. +** In C99, 'strtod' does that conversion. Otherwise, you can +** leave 'lua_strx2number' undefined and Lua will provide its own +** implementation. +*/ +#if !defined(LUA_USE_C89) +#define lua_strx2number(s,p) lua_str2number(s,p) +#endif + + +/* +@@ lua_pointer2str converts a pointer to a readable string in a +** non-specified way. +*/ +#define lua_pointer2str(buff,sz,p) l_sprintf(buff,sz,"%p",p) + + +/* +@@ lua_number2strx converts a float to an hexadecimal numeric string. +** In C99, 'sprintf' (with format specifiers '%a'/'%A') does that. +** Otherwise, you can leave 'lua_number2strx' undefined and Lua will +** provide its own implementation. +*/ +#if !defined(LUA_USE_C89) +#define lua_number2strx(L,b,sz,f,n) \ + ((void)L, l_sprintf(b,sz,f,(LUAI_UACNUMBER)(n))) +#endif + + +/* +** 'strtof' and 'opf' variants for math functions are not valid in +** C89. Otherwise, the macro 'HUGE_VALF' is a good proxy for testing the +** availability of these variants. ('math.h' is already included in +** all files that use these macros.) +*/ +#if defined(LUA_USE_C89) || (defined(HUGE_VAL) && !defined(HUGE_VALF)) +#undef l_mathop /* variants not available */ +#undef lua_str2number +#define l_mathop(op) (lua_Number)op /* no variant */ +#define lua_str2number(s,p) ((lua_Number)strtod((s), (p))) +#endif + + +/* +@@ LUA_KCONTEXT is the type of the context ('ctx') for continuation +** functions. It must be a numerical type; Lua will use 'intptr_t' if +** available, otherwise it will use 'ptrdiff_t' (the nearest thing to +** 'intptr_t' in C89) +*/ +#define LUA_KCONTEXT ptrdiff_t + +#if !defined(LUA_USE_C89) && defined(__STDC_VERSION__) && \ + __STDC_VERSION__ >= 199901L +#include +#if defined(INTPTR_MAX) /* even in C99 this type is optional */ +#undef LUA_KCONTEXT +#define LUA_KCONTEXT intptr_t +#endif +#endif + + +/* +@@ lua_getlocaledecpoint gets the locale "radix character" (decimal point). +** Change that if you do not want to use C locales. (Code using this +** macro must include header 'locale.h'.) +*/ +#if !defined(lua_getlocaledecpoint) +#define lua_getlocaledecpoint() (localeconv()->decimal_point[0]) +#endif + +/* }================================================================== */ + + +/* +** {================================================================== +** Language Variations +** ===================================================================== +*/ + +/* +@@ LUA_NOCVTN2S/LUA_NOCVTS2N control how Lua performs some +** coercions. Define LUA_NOCVTN2S to turn off automatic coercion from +** numbers to strings. Define LUA_NOCVTS2N to turn off automatic +** coercion from strings to numbers. +*/ +/* #define LUA_NOCVTN2S */ +/* #define LUA_NOCVTS2N */ + + +/* +@@ LUA_USE_APICHECK turns on several consistency checks on the C API. +** Define it as a help when debugging C code. +*/ +#if defined(LUA_USE_APICHECK) +#include +#define luai_apicheck(l,e) assert(e) +#endif + +/* }================================================================== */ + + +/* +** {================================================================== +** Macros that affect the API and must be stable (that is, must be the +** same when you compile Lua and when you compile code that links to +** Lua). You probably do not want/need to change them. +** ===================================================================== +*/ + +/* +@@ LUAI_MAXSTACK limits the size of the Lua stack. +** CHANGE it if you need a different limit. This limit is arbitrary; +** its only purpose is to stop Lua from consuming unlimited stack +** space (and to reserve some numbers for pseudo-indices). +*/ +#if LUAI_BITSINT >= 32 +#define LUAI_MAXSTACK 1000000 +#else +#define LUAI_MAXSTACK 15000 +#endif + + +/* +@@ LUA_EXTRASPACE defines the size of a raw memory area associated with +** a Lua state with very fast access. +** CHANGE it if you need a different size. +*/ +#define LUA_EXTRASPACE (sizeof(void *)) + + +/* +@@ LUA_IDSIZE gives the maximum size for the description of the source +@@ of a function in debug information. +** CHANGE it if you want a different size. +*/ +#define LUA_IDSIZE 60 + + +/* +@@ LUAL_BUFFERSIZE is the buffer size used by the lauxlib buffer system. +** CHANGE it if it uses too much C-stack space. (For long double, +** 'string.format("%.99f", -1e4932)' needs 5034 bytes, so a +** smaller buffer would force a memory allocation for each call to +** 'string.format'.) +*/ +#if LUA_FLOAT_TYPE == LUA_FLOAT_LONGDOUBLE +#define LUAL_BUFFERSIZE 8192 +#else +#define LUAL_BUFFERSIZE ((int)(0x80 * sizeof(void*) * sizeof(lua_Integer))) +#endif + +/* }================================================================== */ + + +/* +@@ LUA_QL describes how error messages quote program elements. +** Lua does not use these macros anymore; they are here for +** compatibility only. +*/ +#define LUA_QL(x) "'" x "'" +#define LUA_QS LUA_QL("%s") + + + + +/* =================================================================== */ + +/* +** Local configuration. You can use this space to add your redefinitions +** without modifying the main part of the file. +*/ + + + + + +#endif + + +``` + +`Source/ThirdParty/lua/lualib.h`: + +```h +/* +** $Id: lualib.h,v 1.45.1.1 2017/04/19 17:20:42 roberto Exp $ +** Lua standard libraries +** See Copyright Notice in lua.h +*/ + + +#ifndef lualib_h +#define lualib_h + +#include "lua.h" + + +/* version suffix for environment variable names */ +#define LUA_VERSUFFIX "_" LUA_VERSION_MAJOR "_" LUA_VERSION_MINOR + + +LUAMOD_API int (luaopen_base) (lua_State *L); + +#define LUA_COLIBNAME "coroutine" +LUAMOD_API int (luaopen_coroutine) (lua_State *L); + +#define LUA_TABLIBNAME "table" +LUAMOD_API int (luaopen_table) (lua_State *L); + +#define LUA_IOLIBNAME "io" +LUAMOD_API int (luaopen_io) (lua_State *L); + +#define LUA_OSLIBNAME "os" +LUAMOD_API int (luaopen_os) (lua_State *L); + +#define LUA_STRLIBNAME "string" +LUAMOD_API int (luaopen_string) (lua_State *L); + +#define LUA_UTF8LIBNAME "utf8" +LUAMOD_API int (luaopen_utf8) (lua_State *L); + +#define LUA_BITLIBNAME "bit32" +LUAMOD_API int (luaopen_bit32) (lua_State *L); + +#define LUA_MATHLIBNAME "math" +LUAMOD_API int (luaopen_math) (lua_State *L); + +#define LUA_DBLIBNAME "debug" +LUAMOD_API int (luaopen_debug) (lua_State *L); + +#define LUA_LOADLIBNAME "package" +LUAMOD_API int (luaopen_package) (lua_State *L); + + +/* open all previous libraries */ +LUALIB_API void (luaL_openlibs) (lua_State *L); + + + +#if !defined(lua_assert) +#define lua_assert(x) ((void)0) +#endif + + +#endif + +``` + +`Tutorials/JsonLuaBlueprintPackage.md`: + +```md +```c++ + +#pragma once + +#include "CoreMinimal.h" +#include "LuaBlueprintPackage.h" +#include "JsonLuaBlueprintPackage.generated.h" + +/** + * + */ +UCLASS() +class RPGLUA_API UJsonLuaBlueprintPackage : public ULuaBlueprintPackage +{ + GENERATED_BODY() + +public: + + UJsonLuaBlueprintPackage(); + + UFUNCTION() + FLuaValue ToJson(FLuaValue Value); + + UFUNCTION() + FLuaValue FromJson(FLuaValue Value); + +}; + +``` + +```c++ +#include "JsonLuaBlueprintPackage.h" +#include "LuaBlueprintFunctionLibrary.h" + +UJsonLuaBlueprintPackage::UJsonLuaBlueprintPackage() +{ + Table.Add("to_json", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(UJsonLuaBlueprintPackage, ToJson))); + Table.Add("from_json", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(UJsonLuaBlueprintPackage, FromJson))); +} + +FLuaValue UJsonLuaBlueprintPackage::FromJson(FLuaValue Value) +{ + FLuaValue ReturnValue; + ULuaBlueprintFunctionLibrary::LuaValueFromJson(GetWorld(), GetLuaState(), Value.ToString(), ReturnValue); + return ReturnValue; +} + +FLuaValue UJsonLuaBlueprintPackage::ToJson(FLuaValue Value) +{ + return(ULuaBlueprintFunctionLibrary::LuaValueToJson(Value)); +} +``` + +``` + +`Tutorials/ReflectionShell.md`: + +```md +# Implementing a Lua Reflection Shell (WORK IN PROGRESS) + +The main objective of the LuaMachine project is to safely expose Lua apis to scripters (and modders), so the Unreal Engine reflection system is +completely hidden by default. + +If you know what you are doing, you can implement UObject's reflection easily in LuaMachine using C++. + +This tutorial will show you how (with a bunch of C++ lines) you can inspect/modify UObject's at runtime (and obviously make mess everywhere). + +Note: this has been tested with LuaMachine 20190122 and Unreal Engine 4.21 using the ThirdPersonCharacter template. + +## Creating the LuaReflectionState + +As always, we start by creating a new ULuaState. This will be the base (we will work on an inherited Blueprint class) for the Lua VM targeted at reflection. + +Just create a new C++ class named 'LuaReflectionStateBase' inheriting from 'LuaState', and create a blueprint subclass of it named 'LuaReflectionState' + +## Assigning a 'Lua name' to an Actor + +Next step is creating a component that will automatically assign an actor to a Lua global name: + +* Create a new Blueprint Class of type ActorComponent and name it 'LuaReflectionComponent' +* Add a variable of type 'class of LuaReflectionStateBase' named 'State', and assign it a default value of 'LuaReflectionState' +* Add a public variable of type 'String' (this will be the name exposed to the Lua VM) +* Finally assign the 'GetOwner()' return value to the Lua name (in the 'Begin Play' event) + +Note the usage of 'Lua Create Object in State' that will enforce this LuaValue to be mapped to a specific state (this will avoid mess in case of various Lua VM running in the same process) + +## Adding the Runtime Shell + +Time to add the 'overlay shell' to our screen. From this Multiline Editable Text we will issue lua commands + +## The core of our reflection system: adding a MetaTable to a UObject + +```cpp +LuaReflectionStateBase::ULuaReflectionStateBase() +{ + Table.Add("unreal_get_name", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(ULuaReflectionStateBase, UnrealGetName))); +} +``` + +```cpp +FLuaValue ULuaReflectionStateBase::UnrealGetName(FLuaValue Value) +{ + if (!Value.Object) + return FLuaValue(); + + return FLuaValue(Value.Object->GetName()); +} +``` + +```lua +local reflection = {} + +function reflection:__tostring() + return string.format('uobject: %s', unreal_get_name(self)) +end + +function reflection:__index(key) + return unreal_get_property(self, key) +end + +function reflection:__newindex(key, value) + return unreal_set_property(self, key, value) +end + +return reflection +``` + +## Getting the list of UProperties + +```cpp +FLuaValue ULuaReflectionStateBase::UnrealGetProperties(FLuaValue Value) +{ + if (!Value.Object) + return FLuaValue(); + + UStruct* Class = Cast(Value.Object); + if (!Class) + Class = Value.Object->GetClass(); + + FLuaValue PropertiesArray = CreateLuaTable(); + // remember that Lua starts counting from 1 + int32 Index = 1; + + for (TFieldIterator PropsIterator(Class); PropsIterator; ++PropsIterator) + { + UProperty* Property = *PropsIterator; + PropertiesArray.SetFieldByIndex(Index++, FLuaValue(Property->GetName())); + } + + return PropertiesArray; +} +``` + +Please py attention to the weird naming scheme as i am casting to UStruct but i am calling it 'Class'. This will be able to support USTRUCT's too if you want to improve the system. + +Now let's test it by calling Lua's unreal_get_properties() from the Mannequin's graph: + + + +## Getting/Setting Properties + +```cpp +FLuaValue ULuaReflectionStateBase::UnrealGetProperty(FLuaValue Value, FLuaValue Name, FLuaValue Index) +{ + if (!Value.Object) + return FLuaValue(); + + UStruct* Class = Cast(Value.Object); + if (!Class) + Class = Value.Object->GetClass(); + + UProperty* Property = Class->FindPropertyByName(Name.ToName()); + if (Property) + { + bool bSuccess; + return FromUProperty(Value.Object, Property, bSuccess, Index.ToInteger()); + } + + return FLuaValue(); +} + +void ULuaReflectionStateBase::UnrealSetProperty(FLuaValue Value, FLuaValue Name, FLuaValue NewValue, FLuaValue Index) +{ + if (!Value.Object) + return; + + UStruct* Class = Cast(Value.Object); + if (!Class) + Class = Value.Object->GetClass(); + + UProperty* Property = Class->FindPropertyByName(Name.ToName()); + if (Property) + { + bool bSuccess; + ToUProperty(Value.Object, Property, NewValue, bSuccess, Index.ToInteger()); + } +} +``` + + +## Managing ArrayProperties + +## Bonus: calling UObject's methods + +```cpp +FLuaValue ULuaReflectionState::CallUObjectMethod(FLuaValue Object, FLuaValue Method, FLuaValue Args) +{ + if (Object.Type != ELuaValueType::UObject) + return FLuaValue(); + + if (Method.Type != ELuaValueType::UObject) + return FLuaValue(); + + UFunction* Function = (UFunction *)Method.Object; + + FScopeCycleCounterUObject ObjectScope(Object.Object); + FScopeCycleCounterUObject FunctionScope(Function); + + void* Parameters = FMemory_Alloca(Function->ParmsSize); + FMemory::Memzero(Parameters, Function->ParmsSize); + + // arguments + if (Args.Type == ELuaValueType::Table) + { + int32 NArgs = 1; + for (TFieldIterator FArgs(Function); FArgs && ((FArgs->PropertyFlags & (CPF_Parm | CPF_ReturnParm)) == CPF_Parm); ++FArgs) + { + UProperty *Prop = *FArgs; + + FLuaValue Parameter = Args.GetFieldByIndex(NArgs++); + bool bSuccess; + ToUProperty((UObject *)Parameters, Prop, Parameter, bSuccess); + } + } + + Object.Object->ProcessEvent(Function, Parameters); + + // get return value + for (TFieldIterator FArgs(Function); FArgs; ++FArgs) + { + UProperty *Prop = *FArgs; + if (!Prop->HasAnyPropertyFlags(CPF_ReturnParm | CPF_OutParm)) + continue; + bool bSuccess; + return FromUProperty((UObject *)Parameters, Prop, bSuccess); + } + + return FLuaValue(); +} +``` + +## Bonus: calling static methods from Blueprint Function Libraries + +## Bonus: binding Lua functions to UObject's events + +```cpp +void ULuaReflectionState::BindEvent(FLuaValue Object, FLuaValue Name, FLuaValue Callable) +{ + if (Object.Type != ELuaValueType::UObject) + return; + + if (Name.Type != ELuaValueType::String) + return; + + UStruct* Class = Cast(Object.Object); + if (!Class) + Class = Object.Object->GetClass(); + + UProperty* Property = Class->FindPropertyByName(Name.ToName()); + if (Property) + { + UMulticastDelegateProperty* MulticastDelegateProperty = Cast(Property); + if (MulticastDelegateProperty) + { + FMulticastScriptDelegate* MulticastDelegate = MulticastDelegateProperty->GetPropertyValuePtr_InContainer(Object.Object); + ULuaDelegate *LuaDelegate = NewObject(); + LuaDelegate->SetLuaCallableAndSignature(Callable, MulticastDelegateProperty->SignatureFunction); + FScriptDelegate Delegate; + Delegate.BindUFunction(LuaDelegate, "LuaFakeUFunction"); + MulticastDelegate->Add(Delegate); + } + } +} + +void ULuaDelegate::ProcessEvent(UFunction *Function, void *Parms) +{ + if (!Callable.LuaState) + return; + + // arguments + TArray Args; + for (TFieldIterator FArgs(CallableSignature); FArgs && ((FArgs->PropertyFlags & (CPF_Parm | CPF_ReturnParm)) == CPF_Parm); ++FArgs) + { + UProperty *Prop = *FArgs; + bool bSuccess; + Args.Add(Callable.LuaState->FromUProperty((UObject *)Parms, Prop, bSuccess)); + } + + ULuaBlueprintFunctionLibrary::LuaValueCall(Callable, Args); +} + +void ULuaDelegate::LuaFakeUFunction() +{ + +} +``` + +## Bonus: working with USTRUCTs + +## Super Bonus: running lua scripts from the Unreal Editor + + + +``` + +`Tutorials/RickAndMortyAPI.md`: + +```md +# Using the HTTP + JSON Functions with RickAndMorty API + +LuaMachine includes out of the box UFUNCTION's for interfacing with HTTP services. + +In this tutorial we will see how to retrieve JSON data as well as raw images/textures from an HTTP service. + +The objective is to build a User Widget listing all of the Rick And Morty characters available at https://rickandmortyapi.com/ + +![RickAndMortyAPI](RickAndMortyAPI_Data/RickAndMorty.PNG?raw=true "RickAndMortyAPI") + +## Step 1: adding http_get() and from_json() lua functions + +By default a LuaMachine state has no function exposed to the user/scripter. You generally want to carefully choose what to expose to the final scripter/modder, so the default behaviour +is to expose nothing (except the lua standard functions [and this is configurable too]). + +We want to expose two functions: one for making http requests (we will call it http_get()) and another for converting json strings to lua tables (we will call it from_json()) + +Let's add a new LuaState called RickAndMortyLuaState: + +![RickAndMortyNewLuaState](RickAndMortyAPI_Data/RickAndMorty001.PNG?raw=true "RickAndMortyNewLuaState") + +![RickAndMortyNewLuaStateContentBrowser](RickAndMortyAPI_Data/RickAndMorty002.PNG?raw=true "RickAndMortyNewLuaStateContentBrowser") + +And add a new Custom Event (note: not a plain function!) named HttpGet: + +![RickAndMortyHttpGet](RickAndMortyAPI_Data/RickAndMorty003.PNG?raw=true "RickAndMortyHttpGet") + +As you can see the event will trigger the HTTP request, and two additional events are generated (one for successful connection, the other for failures) mapped to lua callbacks. + +Now expose the event as a lua function: + +![RickAndMortyHttpGetTable](RickAndMortyAPI_Data/RickAndMorty004.PNG?raw=true "RickAndMortyHttpGetTable") + +And create a Lua Code Asset for working with http_get(): + +```lua +http_get('https://rickandmortyapi.com/api/character', + function(status, headers, content, data) + print(status) + print(content) + end, + function(data) + error('unable to connect to http service') + end +) +``` + +The successful callback will get status, headers, content and a context object (nil for now), while the error one will only get the context. + +Now in your level blueprint add a node for calling the Lua Code Asset, play the level and check Unreal logs (they should show both http status and http response): + +![RickAndMortyLevelBlueprint](RickAndMortyAPI_Data/RickAndMorty010.PNG?raw=true "RickAndMortyLevelBlueprint") + +Time to add the from_json() function. This time is a standard UFUNCTION (not a custom event like http_get()): + +![RickAndMortyFromJSON](RickAndMortyAPI_Data/RickAndMorty009.PNG?raw=true "RickAndMortyFromJSON") + +Add from_json() to the Lua State Table and update the Lua Code Asset for parsing the json response: + +```lua +http_get('https://rickandmortyapi.com/api/character', + function(status, headers, content, data) + print(status) + foo = from_json(content) + end, + function(data) + error('unable to connect to http service') + end +) +``` + +Before hitting play again, open the Lua Machine Debugger (it is under the Window/Developer Tools menu). When the level runs hit "refresh" in the debugger and you should see the 'foo' object tree: + +![RickAndMortyLuaDebugger](RickAndMortyAPI_Data/RickAndMorty005.PNG?raw=true "RickAndMortyLuaDebugger") + +## Step 2: adding a LuaUserDataObject representing a RickAndMorty character + +In addition to Lua native types (strings, integers, booleans, tables...), LuaMachine allows you to define new types based on UObjects. + +We will now create a new Lua UserData type for representing a RickAndMorty Character: + +![RickAndMortyUserData](RickAndMortyAPI_Data/RickAndMorty006.PNG?raw=true "RickAndMortyUserData") + +This new UObject includes a variable named "Image" of type SlateBrush (will show the character image/texture retrieved via HTTP), a UFUNCTION SetImage (exposed as a lua method named set_image()) and 4 pure UFUNCTIONS for retrieving specific fields: + + +![RickAndMortyLuaUserDataObject](RickAndMortyAPI_Data/RickAndMorty012.PNG?raw=true "RickAndMortyLuaUserDataObject") + +Note that you have dozens of different ways to expose the UObject fields to the other systems, adding pure functions is only one of them. +As this object needs to be accessed by the widget subsystem, this approach reduces the amount of nodes in the widget itself. + +Time to add a new 'character()" function to the LuaState to create new lua userdata characters: + +![RickAndMortyNewCharacter](RickAndMortyAPI_Data/RickAndMorty007.PNG?raw=true "RickAndMortyNewCharacter") + +![RickAndMortyNewCharacterTable](RickAndMortyAPI_Data/RickAndMorty008.PNG?raw=true "RickAndMortyNewCharacterTable") + +Time to refactor the code to create a character() for each result returned by the HTTP api and to download the images/textures by issuing another http_get() call: + +```lua +http_get('https://rickandmortyapi.com/api/character', + function(status, headers, content, data) + if status ~= 200 then + error('HTTP error code: ' .. status) + end + local response = from_json(content) + for _, result in ipairs(response.results) do + local new_character = character() + new_character.name = result.name + new_character.gender = result.gender + new_character.status = result.status + new_character.species = result.species + http_get(result.image, + function(status, headers, content, data) + if status ~= 200 then + error('HTTP error code for image: ' .. status) + end + local current_character = data + current_character.set_image(content) + end, + function(data) + error('unable to connect to http image service') + end, + new_character + ) + end + end, + function(data) + error('unable to connect to http service') + end +) +``` + +## Step 3: creating the widgets + +The Lua Virtual Machine has now a bunch of 'lua character' objects hanging around just waiting to be visualized. + +Create a new User Widget containing a "List View" (the 'CharactersList' in the screenshot): + +![RickAndMortyListView](RickAndMortyAPI_Data/RickAndMorty013.PNG?raw=true "RickAndMortyListView") + +List Widgets require "Entry Widgets". They are UserWidget implementing the "User List Entry" interface. You can create +the Entry Widget for the List View by clicking on the "+" button of the "Entry Widget Class" property: + +![RickAndMortyEntryWidget](RickAndMortyAPI_Data/RickAndMorty015.PNG?raw=true "RickAndMortyEntryWidget") + +We now need to add an "Add Character" function to the main widget (not the entry widget!): + +![RickAndMortyAddCharacter](RickAndMortyAPI_Data/RickAndMorty016.PNG?raw=true "RickAndMortyAddCharacter") + +This function will populate the internal list of the "List View" widget, and will generate "Entry Widgets" on-demand. + +We are adding the Lua UserData Objects we created before as the list item. Our next task is to view them in the "Entry Widget". + +The "Entry Widget" (the one implementing the "User List Entry" interface) has one image widget and 4 text widgets, each of them exposing one of the Lua UserData Object fields: + +![RickAndMortyEntryWidgetDesign](RickAndMortyAPI_Data/RickAndMorty018.PNG?raw=true "RickAndMortyEntryWidgetDesign") + +In the Widget Editor Graph section add a variable of the Lua UserData Character type: + +![RickAndMortyEntryWidgetLua](RickAndMortyAPI_Data/RickAndMorty021.PNG?raw=true "RickAndMortyEntryWidgetLua") + +This is the object exposing values to the various sub-widgets (the image and the 4 text boxes). + +Now you can configure the image widget to read the 'Image' property from the Character User Data: + +![RickAndMortyEntryWidgetImage](RickAndMortyAPI_Data/RickAndMorty019.PNG?raw=true "RickAndMortyEntryWidgetImage") + +And specify the "pure function" to call for each text block: + +![RickAndMortyEntryWidgetTextBlock](RickAndMortyAPI_Data/RickAndMorty020.PNG?raw=true "RickAndMortyEntryWidgetTextBlock") + +Once compiled, you should see in the main widget preview something like this: + +![RickAndMortyWidgetPreview](RickAndMortyAPI_Data/RickAndMorty014.PNG?raw=true "RickAndMortyWidgetPreview") + +Last step is setting the Lua UserData Character field in the Entry widget whenever a new Entry widget is generated by the list view (by overriding the "On Entry Initialied" event: + +![RickAndMortyOnEntryInitialized](RickAndMortyAPI_Data/RickAndMorty017.PNG?raw=true "RickAndMortyOnEntryInitialized") + +## Step 4: placing all together + +Now we need to improve the 'character()' lua function to call the 'AddCharacter' method of the main widget. For doing this we are adding a reference to the main widget in the lua state (as a variable): + +![RickAndMortyNewCharacter2](RickAndMortyAPI_Data/RickAndMorty022.PNG?raw=true "RickAndMortyNewCharacter2") + +Finally, the level blueprint will be modified for generating the widget and passing its reference to the LuaState: + +![RickAndMortyLevelBlueprint2](RickAndMortyAPI_Data/RickAndMorty011.PNG?raw=true "RickAndMortyLevelBlueprint2") + +## Step 5: pagination +The RickAndMorty API is based on 'pages'. Currently we are only retrieving the page 1. + +For each page the api exposes the info.prev and info.next fields containing the urls of the previous page (if available) and the next page (if available). + +Let's refactor the code again: + +```lua + +function load_page_from_url(url) + + http_get(url, + function(status, headers, content, data) + if status ~= 200 then + error('HTTP error code: ' .. status) + end + local response = from_json(content) + prev_page_url = response.info.prev + next_page_url = response.info.next + for _, result in ipairs(response.results) do + local new_character = character() + new_character.name = result.name + new_character.gender = result.gender + new_character.status = result.status + new_character.species = result.species + http_get(result.image, + function(status, headers, content, data) + if status ~= 200 then + error('HTTP error code for image: ' .. status) + end + local current_character = data + current_character.set_image(content) + end, + function(data) + error('unable to connect to http image service') + end, + new_character + ) + end + end, + function(data) + error('unable to connect to http service') + end + ) + +end + +load_page_from_url('https://rickandmortyapi.com/api/character') +``` + +The 'load_page_from_url()' function, now takes an arbitrary url. + +Let's add two buttons to the main widget and set their 'click event': + +![RickAndMortyPrevNext](RickAndMortyAPI_Data/RickAndMorty023.PNG?raw=true "RickAndMortyPrevNext") + +![RickAndMortyPrevNextClick](RickAndMortyAPI_Data/RickAndMorty024.PNG?raw=true "RickAndMortyPrevNextClick") + +Note that we are using two different approaches: in the 'prev' case we retrieve each lua object (function and url) and then we call it. This approach is way more elegant than the 'next' one (where we simply invoke a lua string) but obviously requires more nodes. + +Clicking on 'next' will show the next api page (after having cleared the list of items): + +![RickAndMortyPage2](RickAndMortyAPI_Data/RickAndMorty025.PNG?raw=true "RickAndMortyPage2") + +## Optional Step 6: using C++ + +While it is pretty easy to define lua apis from blueprints, using C++ is way faster and less verbose. This is the equivalent C++ code for the previously defined LuaState as well as the Lua UserData Object: + +### RickAndMortyLuaStateBase.h +```cpp +#pragma once + +#include "CoreMinimal.h" +#include "LuaState.h" +#include "RickAndMortyLuaStateBase.generated.h" + +/** + * + */ +UCLASS() +class LUARICKANDMORTY_API URickAndMortyLuaStateBase : public ULuaState +{ + GENERATED_BODY() + +public: + URickAndMortyLuaStateBase(); + + UFUNCTION() + void HttpGet(FLuaValue Url, FLuaValue SuccessCallback, FLuaValue ErrorCallback, FLuaValue Data); + + UFUNCTION() + void HttpSuccessCallback(FLuaValue Context, FLuaValue Response); + + UFUNCTION() + void HttpErrorCallback(FLuaValue Data); + + UFUNCTION() + bool FromJSON(FLuaValue Data, FLuaValue& Value); + + UFUNCTION() + FLuaValue NewCharacter(); +}; + +``` + +### RickAndMortyCharacterBase.h +```cpp + +#pragma once + +#include "CoreMinimal.h" +#include "LuaUserDataObject.h" +#include "Styling/SlateBrush.h" +#include "RickAndMortyCharacterBase.generated.h" + +/** + * + */ +UCLASS() +class LUARICKANDMORTY_API URickAndMortyCharacterBase : public ULuaUserDataObject +{ + GENERATED_BODY() + +public: + URickAndMortyCharacterBase(); + + UPROPERTY(EditAnywhere) + FSlateBrush Image; + + UFUNCTION() + void SetImage(FLuaValue Data); + + UFUNCTION(BlueprintCallable, BlueprintPure) + FText GetName(); + + UFUNCTION(BlueprintCallable, BlueprintPure) + FText GetStatus(); + + UFUNCTION(BlueprintCallable, BlueprintPure) + FText GetGender(); + + UFUNCTION(BlueprintCallable, BlueprintPure) + FText GetSpecies(); + +}; + +``` + +### RickAndMortyLuaStateBase.cpp +```cpp + +#include "RickAndMortyLuaStateBase.h" +#include "LuaBlueprintFunctionLibrary.h" +#include "RickAndMortyCharacterBase.h" + +URickAndMortyLuaStateBase::URickAndMortyLuaStateBase() +{ + Table.Add("http_get", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(URickAndMortyLuaStateBase, HttpGet))); + Table.Add("from_json", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(URickAndMortyLuaStateBase, FromJSON))); + Table.Add("character", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(URickAndMortyLuaStateBase, NewCharacter))); +} + +void URickAndMortyLuaStateBase::HttpGet(FLuaValue Url, FLuaValue SuccessCallback, FLuaValue ErrorCallback, FLuaValue Data) +{ + FLuaValue Context = CreateLuaTable(); + Context.SetFieldByIndex(1, SuccessCallback); + Context.SetFieldByIndex(2, ErrorCallback); + Context.SetFieldByIndex(3, Data); + + FLuaHttpResponseReceived ResponseReceived; + ResponseReceived.BindUFunction(this, GET_FUNCTION_NAME_CHECKED(URickAndMortyLuaStateBase, HttpSuccessCallback)); + + FLuaHttpError ErrorReceived; + ErrorReceived.BindUFunction(this, GET_FUNCTION_NAME_CHECKED(URickAndMortyLuaStateBase, HttpErrorCallback)); + + ULuaBlueprintFunctionLibrary::LuaHttpRequest(GetWorld(), GetClass(), "GET", Url.ToString(), TMap(), FLuaValue(), Context, ResponseReceived, ErrorReceived); +} + +void URickAndMortyLuaStateBase::HttpSuccessCallback(FLuaValue Context, FLuaValue Response) +{ + FLuaValue Callback = Context.GetFieldByIndex(1); + FLuaValue StatusCode = Response.GetFieldByIndex(1); + FLuaValue Headers = Response.GetFieldByIndex(2); + FLuaValue Content = Response.GetFieldByIndex(3); + TArray Args = { StatusCode, Headers, Content, Context.GetFieldByIndex(3) }; + ULuaBlueprintFunctionLibrary::LuaValueCallIfNotNil(Callback, Args); +} + +void URickAndMortyLuaStateBase::HttpErrorCallback(FLuaValue Data) +{ + FLuaValue Callback = Data.GetFieldByIndex(2); + TArray Args = { Data.GetFieldByIndex(3) }; + ULuaBlueprintFunctionLibrary::LuaValueCallIfNotNil(Callback, Args); +} + +bool URickAndMortyLuaStateBase::FromJSON(FLuaValue Data, FLuaValue& Value) +{ + return ULuaBlueprintFunctionLibrary::LuaValueFromJson(GetWorld(), GetClass(), Data.ToString(), Value); +} + +FLuaValue URickAndMortyLuaStateBase::NewCharacter() +{ + return NewLuaUserDataObject(URickAndMortyCharacterBase::StaticClass()); +} +``` + +### RickAndMortyCharacterBase.cpp +```cpp + +#include "RickAndMortyCharacterBase.h" +#include "LuaBlueprintFunctionLibrary.h" +#include "Engine/Texture2D.h" + +URickAndMortyCharacterBase::URickAndMortyCharacterBase() +{ + Table.Add("set_image", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(URickAndMortyCharacterBase, SetImage))); +} + +void URickAndMortyCharacterBase::SetImage(FLuaValue Data) +{ + UTexture2D* Texture = ULuaBlueprintFunctionLibrary::LuaValueToTransientTexture(0, 0, Data, PF_B8G8R8A8, true); + if (Texture) + { + Image.DrawAs = ESlateBrushDrawType::Image; + Image.ImageSize = FVector2D(200, 200); + Image.SetResourceObject(Texture); + } +} + +FText URickAndMortyCharacterBase::GetName() +{ + return FText::FromString(LuaGetField("name").ToString()); +} + +FText URickAndMortyCharacterBase::GetGender() +{ + return FText::FromString(LuaGetField("gender").ToString()); +} + +FText URickAndMortyCharacterBase::GetSpecies() +{ + return FText::FromString(LuaGetField("species").ToString()); +} + +FText URickAndMortyCharacterBase::GetStatus() +{ + return FText::FromString(LuaGetField("status").ToString()); +} +``` + +``` + +`Tutorials/SimpleDialogueSystem.md`: + +```md +# A simple Dialogue System with LuaMachine + +In this tutorial we will see how to use the LuaMachine plugin to create a dialogue system (versatile enough to implement a shop/inventory too) controlled by Lua scripts. + +![Intro](SimpleDialogueSystem_Data/Intro.PNG?raw=true "Intro") + +The Lua scripts will be part of the final build (they will be cooked in the pak file as bytecode) so you will not be able to change them after the game is packaged (check the 'Variations' section at the end of the tutorial for alternative approaches like using normal scripts in the filesystem). + +The tutorial has been tested on Unreal Engine 4.21 and LuaMachine 20181230 starting from the 'Third Person' Blueprint Template. + +Blueprints will be used for the whole tutorial, at the end of the page you will find a C++ implementation too. + +## Installing the LuaMachine plugin + +Download the latest zip file for your specific Unreal Engine version and operating system from the releases page: (https://github.com/rdeioris/LuaMachine/releases) and unzip it into the Plugins/ directory of your project (create it if it does not exist). + +Restart Unreal Engine, and create the LuaState for our dialogue system: + +* add new Blueprint Class +* select 'LuaState' from 'all classes' +* name it 'DialogueLuaState' + +![DialogueLuaState](SimpleDialogueSystem_Data/DialogueLuaState.PNG?raw=true "DialogueLuaState") + +A LuaState represents a specific Lua virtual machine (there can be multiple isolated Lua virtual machines in the same project). + +For this project we need a single LuaState. + +## The Talking Character + +For the 'talking characters' (actors implementing dialogues in Lua) we will use the 'Twinblast' asset from Paragon. + +![Twinblast](SimpleDialogueSystem_Data/Twinblast.PNG?raw=true "Twinblast") + +It is a perfect choice as it includes facial morph targets that will give more emphasis to the dialogues by visually reporting emotional states. + +![ContentBrowser](SimpleDialogueSystem_Data/ContentBrowser.PNG?raw=true "ContentBrowser") + +Create a new Character Blueprint and add a camera component pointing to the face of Twinblast, a Sphere Collider for detecting when the player is near enough, a Text Render for simple in-world messages and (obviously) a LuaComponent. + +Finally, add a variable of type 'LuaValue' named 'TalkingTable' (i will explain it later) + +![TalkingCharacter](SimpleDialogueSystem_Data/TalkingCharacter.PNG?raw=true "TalkingCharacter") + +The camera will be useful if you want to point to the face of the character during a dialogue (you will control the camera switch from lua). + +Remember to assign an animation to the Mesh component to avoid Twinblast to be in 'A pose' while speaking (my choice is Idle_Relaxed) + +## Preparing the DialogueLuaState + +To allow the TalkingCharacter to be governed by Lua, we need to setup a new Lua virtual machine exposing a bunch of Unreal functions to Lua scripts. To accomplish this, we need to add Unreal/Blueprint functions to the DialogueLuaState class and expose them in the Table property of the LuaState. + +We start with a simple 'Print' function that we will expose as the lua 'print' (overriding the default one), a 'Quit' function ending the game exposed as 'quit' and a LuaError event that will be triggered automatically when a Lua call fails: + +![PrintAndLuaError](SimpleDialogueSystem_Data/PrintAndLuaError.PNG?raw=true "PrintAndLuaError") + +Note that 'Print' expects a LuaValue argument (named 'Message', check it at the right of the screenshot). The only allowed parameters type (included the return value) is the LuaValue one. Functions violating this rule will not be selectable in the interface. + +Once defined you can expose them using the 'Table' property of the LuaState (you can access it using the 'Class Defaults' button: + +![LuaStateTable1](SimpleDialogueSystem_Data/LuaStateTable1.PNG?raw=true "LuaStateTable1") + +## The first script + +Create a new LuaCode asset in your Content Browser (it is under the 'Miscellaneous' group) and insert the following code: + +```lua +local character = {} + +function character:begin_play() + print('Hello World') +end + +return character +``` + +You should get something very similar: (note syntax highlight ;) + +![CodeAsset](SimpleDialogueSystem_Data/CodeAsset.PNG?raw=true "CodeAsset") + +![TalkingScript](SimpleDialogueSystem_Data/TalkingScript.PNG?raw=true "TalkingScript") + +Now setup the TalkingCharacter Event Graph to load and execute the specific CodeAsset on BeginPlay event and to call the character:begin_play function (note the colon syntactic sugar to allow the Lua vm to automatically includes a 'self' variable mapped to the LuaComponent, more on this later) + +![BeginPlay](SimpleDialogueSystem_Data/BeginPlay.PNG?raw=true "BeginPlay") + +The 'LuaRunCodeAsset' node will load and execute a LuaCode asset and will store its return value (the 'character' local table) in the TalkingTable variable (we created it earlier). + +We can use this variable for the whole lifecycle of the TalkingCharacter to call the functions exposed by the lua table mapped to it (only begin_play() for now). + +Pay attention to the 'State' parameter of 'LuaRunCodeAsset' as you must set it to 'DialogueLuaState'. + +Finally, the 'LuaCallTableKey' node (exposed by the LuaComponent) will check if the passed table exposes the specified 'Key' and, if it is a function, will call it. + +Note that nodes exposed by the LuaComponent will automatically pass the LuaComponent itself as the first argument of called functions. As we are using the 'table:function' syntax in our script, we will get this parameter in the 'self' variable automagically. We will use the 'self' variable later in the tutorial. + +Drag the TalkingCharacter blueprint to the Level and hit Play. + +If all goes well you should get a 'Hello World' message on screen. + +## More Unreal Engine events with Lua + +Time to implement overlapping events for the sphere collider as well as a custom 'Speak' event that will be triggered by the player when wanting to talk to a specific 'TalkingCharacter': + +```lua +local character = {} + +function character:begin_play() + print('Hello World') +end + +function character:begin_overlap(other) + print('Begin Overlap') +end + +function character:end_overlap(other) + print('End Overlap') +end + +function character:speak() + print('Speaking...') +end + +return character +``` + +Add the new events to the TalkingCharacter's Event Graph (pay attention to the overlapping events as they must be related to the sphere, not the capsule of the character): + +![Overlap](SimpleDialogueSystem_Data/Overlap.PNG?raw=true "Overlap") + +Note that the overlapping actor is passed as the first argument of the Lua function + +'Speak' is a custom event: + +![Speak](SimpleDialogueSystem_Data/Speak.PNG?raw=true "Speak") + +Re-Play and try to overlap the TalkingCharacter Sphere with the Mannequin, if all goes well you should see 'Begin Overlap' printed when the player is near the TalkingCharacter and 'End Overlap' printed when the player is far from it. + +## Dealing with multiple TalkingCharacters and checking which actor is overlapping + +Our game could include dozens of characters or objects overlapping the collision sphere of the TalkingCharacter. For this reason +we need a way to check that the overlapping actor is the player pawn. + +As getting the player pawn from Lua is a pretty handy function, very probably the best thing to do is exposing it as a global Lua function in the same way we exposed 'print': + +![GetPlayer](SimpleDialogueSystem_Data/GetPlayer.PNG?raw=true "GetPlayer") + +![LuaStateTable2](SimpleDialogueSystem_Data/LuaStateTable2.PNG?raw=true "LuaStateTable2") + +Now we can simply do a comparison in the begin_overlap/end_overlap functions: + +```lua +function character:begin_overlap(other) + if get_player() == other then + print('Begin Overlap') + end +end + +function character:end_overlap(other) + if get_player() == other then + print('End Overlap') + end +end +``` + +Hit Play again and check if the overlap system still works (if you want, try placing other overlapping objects in the level to check the comparison works correctly) + +As said before, multiple TalkingCharacter's could be spawned in the level, so we need a way to 'mark' which one is the currently focused character, both visually and from the Lua point of view. + +Let's start with the visual part: we want to set a message (from Lua) in the TextRender component of the TalkingCharacter. + +This is a function specific of the TalkingCharacter so we should expose it in its LuaComponent (we can access it using the automagic 'self' table) + +Add the function to the TalkingCharacter Blueprint + +![SetFlash](SimpleDialogueSystem_Data/SetFlash.PNG?raw=true "SetFlash") + +And expose it to Lua (select the LuaComponent to access the properties in the Details panel) + +![SetFlashExposed](SimpleDialogueSystem_Data/SetFlashExposed.PNG?raw=true "SetFlashExposed") + +Update the code again: + +```lua + +function character:begin_play() + -- ensure the TextRender content is empty + self.flash('') +end + +function character:begin_overlap(other) + if get_player() == other then + self.flash('Speak with Twinblast') + end +end + +function character:end_overlap(other) + if get_player() == other then + self.flash('') + end +end +``` + +Play and check if the TextRender component activates itself when you are near the TalkingCharacter and disappears when you move away: + +![SpeakWithTwinblast](SimpleDialogueSystem_Data/SpeakWithTwinblast.PNG?raw=true "SpeakWithTwinblast") + +To set which one is the currently 'focused' TalkingCharacter we will use a global Lua variable. We can just set it in the DialogueLuaState Table (and set/unset it in the begin_overlap/end_overlap functions of the TalkingCharacter script). + +![CurrentTarget](SimpleDialogueSystem_Data/CurrentTarget.PNG?raw=true "CurrentTarget") + +Note that we want to store the Actor reference of the TalkingCharacter in the lua variable, but the 'self' table maps to the LuaComponent object. We can add a field to the 'self' table exposing the component's owner (the Actor) value: + +![Owner](SimpleDialogueSystem_Data/Owner.PNG?raw=true "Owner") + +The 'owner' field is now added to the LuaComponent's lua table (the one you get via 'self') + +This is the updated script: + +```lua +function character:begin_overlap(other) + if get_player() == other then + self.flash('Speak with Twinblast') + current_target = self.owner + end +end + +function character:end_overlap(other) + if get_player() == other then + self.flash('') + current_target = nil + end +end +``` + +## Triggering the 'Speak' event + +When a TalkingCharacter is highlighted/targeted we want to trigger its 'Speak' event by clicking the left mouse button. To accomplish +this we can manage the mouse click in the ThirdPersonCharacter Blueprint (would be the most logical choice), but to avoid clobbering the template blueprint we will use the Level Blueprint instead (suboptimal but it should work flawlessly): + +![LevelBlueprint](SimpleDialogueSystem_Data/LevelBlueprint.PNG?raw=true "LevelBlueprint") + +Play again, and if you click the left mouse button wile the TalkingCharacter is the current target, its 'speak' function will be called. + +## Showing dialogues and choices + +We are at the core of the tutorial: showing dialogues with questions/choices. + +We need two widgets: one for the dialogue text (a simple big multiline text box) and another one for questions/choices/answers (a simple text button triggering a Lua function when clicked) + +Let's start with the DialogueQuestion Widget: + +![DialogueQuestionDesigner](SimpleDialogueSystem_Data/DialogueQuestionDesigner.PNG?raw=true "DialogueQuestionDesigner") + +![DialogueQuestionGraph](SimpleDialogueSystem_Data/DialogueQuestionGraph.PNG?raw=true "DialogueQuestionGraph") + +We have a widget with a button and a text (note: no canvas panel is required, you can remove it). + +The QuestionTable variable is a LuaValue marked as 'Expose on Spawn'. To generate a question widget we need to pass it a lua table (well an array) composed of 2 items: the first one is the content of the button text, the second is a lua function triggered when the user click on the question/choice/answer (spoiler: the 'Question' name is probably not a good choice as no questions will be generated in the examples, only answers and choices :( ) + +Now the DialogueScreen Widget: + +![DialogueScreenDesigner](SimpleDialogueSystem_Data/DialogueScreenDesigner.PNG?raw=true "DialogueScreenDesigner") + +![DialogueScreenGraph](SimpleDialogueSystem_Data/DialogueScreenGraph.PNG?raw=true "DialogueScreenGraph") + +This widget requires two 'Expose on Spawn' variables (again LuaValue's). The first one is the content of the Multiline TextBox (note: it is marked as readonly in the properties), the second one is a table (well an array again) with the list of questions/answers/choices from which the various DialogueQuestion Widgets will be generated. + +To allow Lua opening and closing the dialogue widgets we need to expose two new functions: + +![OpenDialogue](SimpleDialogueSystem_Data/OpenDialogue.PNG?raw=true "OpenDialogue") + +(Note: you need to add the variable 'CurrentDialogueScreen' as a DialogueScreen Widget reference) + +![CloseDialogue](SimpleDialogueSystem_Data/CloseDialogue.PNG?raw=true "CloseDialogue") + +![LuaStateTable3](SimpleDialogueSystem_Data/LuaStateTable3.PNG?raw=true "LuaStateTable3") + +We can now update the code to manage the dialogues when the 'Speak' event is triggered + +```lua +function character:speak() + + function page1() + open_dialogue('Go to page 2?', { + {'Yes', page2}, + {'No', close_dialogue}, + }) + end + + function page2() + open_dialogue('Back to page 1?', { + {'Yes', page1}, + {'No', close_dialogue}, + {'Go to page 3, please...', page3} + }) + end + + function page3() + open_dialogue([[ +This is the last page, +What do you want to do ? + ]], { + {'Quit Game', quit}, + {'Close Dialogue', close_dialogue}, + }) + end + + -- called by the 'Speak' event + page1() +end +``` + +Note that the open/close dialogue logic implemented in the Blueprint will prevent multiple dialogues from opening concurrently. This will simplify scripting too as we just need to call +open_dialogue() again from the callback to change the content. + +If all goes well you should end with something very similar: + +![Page2](SimpleDialogueSystem_Data/Page2.PNG?raw=true "Page2") + +## More Lua functions: change the current camera and Character's face + +We will add two more blueprint functions to the TalkingCharacter Blueprint (remember to expose them the to the Table property of the LuaComponent) + +![SetCamera](SimpleDialogueSystem_Data/SetCamera.PNG?raw=true "SetCamera") + +![SetFace](SimpleDialogueSystem_Data/SetFace.PNG?raw=true "SetFace") + +Now refactor the code to change the camera when starting a dialogue session and resetting it when exiting: + +```lua +function character:speak() + + -- set camera to the character's one + self.set_camera(self.owner) + + -- implement a close callback that will reset both camera and face + function close() + close_dialogue() + -- we can use self here as we are in a closure + self.set_camera(get_player()) + self.set_face(0.0) + end + + function page1() + open_dialogue('Go to page 2?', { + {'Yes', page2}, + {'No', close}, + }) + end + + function page2() + open_dialogue('Back to page 1?', { + {'Yes', page1}, + {'No', close}, + {'Go to page 3, please...', page3} + }) + end + + function page3() + self.set_face(1.0) + open_dialogue([[ +This is the last page, +What do you want to do ? + ]], { + {'Quit Game', quit}, + {'Close Dialogue', close}, + }) + end + + -- called by the 'Speak' event + page1() +end +``` + +When going to page3 the facial expression of Twinblast should change to the 'angry' one: + +![Page3](SimpleDialogueSystem_Data/Page3.PNG?raw=true "Page3") + +Challenge: improve the set_camera() function to allow for custom blend time specified from Lua. + +Note: is it better to place set_camera() as a global function in the DialogueLuaState ? + +## More Talking Characters + +We can now refactor the TalkingCharacter Blueprint to allow to map a different script to each instance: + +![MultipleCharacters](SimpleDialogueSystem_Data/MultipleCharacters.PNG?raw=true "MultipleCharacters") + +Just create two more simple scripts (you are not required to implement all the events): + +```lua +local character = {} + +function character:begin_play() + self.flash('I am another begin_play() function...') +end + +return character +``` + +```lua +local character = {} + +function character:begin_play() + self.flash('Too far to see my name...') + self.set_face(1.0) +end + +return character +``` + +![ThreeScripts](SimpleDialogueSystem_Data/ThreeScripts.PNG?raw=true "ThreeScripts") + +## Inventory/Shop system + +We can easily implement shops/inventories: + +```lua +local merchant = {} + +merchant.shop_sign = string.format('Shop managed by %s', _VERSION) + +merchant.items = {'Shotgun', 'Grenade', 'Guns', 'LaserGun', 'Bazooka'} + +function merchant:begin_play() + self.flash(merchant.shop_sign) +end + +function merchant:begin_overlap(other) + if get_player() == other then + self.flash('Buy something') + current_target = self.owner + end +end + +function merchant:end_overlap(other) + if get_player() == other then + self.flash(merchant.shop_sign) + current_target = nil + end +end + +function merchant:speak() + + -- reset when closing the shop + function close() + self.set_camera(get_player()) + close_dialogue() + end + + -- special closure for generating a callback with the index of the item to buy + function buy(index) + return function() + table.remove(merchant.items, index) + show_items() + end + end + + function show_items() + self.set_camera(self.owner) + + -- build the list of items (and the related callbacks) + local items = {} + for k,v in pairs(merchant.items) do + -- v is the item name, k is its index (we use it as in lua we can only remove efficiently by index) + table.insert(items, {'Buy ' .. v, buy(k)}) + end + table.insert(items, {'Nothing, Thanks', close}) + + open_dialogue('Welcome to my humble shop', items) + end + + -- triggered by 'Speak' event + show_items() +end + +return merchant +``` + +![Shop](SimpleDialogueSystem_Data/Shop.PNG?raw=true "Shop") + +Let's improve it by adding the 'gold/money' concept: + +First, add a player_gold Integer item in the Table of the DialogueLuaState and set to something like '4'. In this +way the merchant will be able to access the amount of player gold. + +```lua +local merchant = {} + +merchant.shop_sign = string.format('Shop managed by %s', _VERSION) + +merchant.items = {'Shotgun', 'Grenade', 'Guns', 'LaserGun', 'Bazooka'} + +-- increases whenever the merchant sells something +merchant.gold = 0 + +function merchant:begin_play() + self.flash(merchant.shop_sign) +end + +function merchant:begin_overlap(other) + if get_player() == other then + self.flash('Buy something') + current_target = self.owner + end +end + +function merchant:end_overlap(other) + if get_player() == other then + self.flash(merchant.shop_sign) + current_target = nil + end +end + +function merchant:speak() + + -- reset when closing the shop + function close() + self.set_camera(get_player()) + close_dialogue() + end + + -- special closure for generating a callback with the index of the item to buy + function buy(index) + return function() + if player_gold <= 0 then + open_dialogue('Not enough gold', {{'Sorry', show_items}}) + else + table.remove(merchant.items, index) + merchant.gold = merchant.gold + 1 + player_gold = player_gold - 1 + show_items() + end + end + end + + function show_items() + self.set_camera(self.owner) + + -- build the list of items (and the related callbacks) + local items = {} + for k,v in pairs(merchant.items) do + -- v is the item name, k is its index (we use it as in lua we can only remove efficiently by index) + table.insert(items, {'Buy ' .. v, buy(k)}) + end + table.insert(items, {'Nothing, Thanks', close}) + + open_dialogue([[Welcome to my humble shop, + You have ]] .. player_gold .. ' gold', items) + end + + -- triggered by 'Speak' event + show_items() +end + +return merchant +``` + +## Cooking and Packaging + +One of the advantages of using LuaCode assets, is that they will be built as normal assets and they will be converted to bytecode by default. The cooking system is smart enough to adapt the bytecode even if you want to build the project for systems (like android) having low level differences (that would break on bytecode generated on another system). + +## Variations + +The tutorial aims at showing a simple approach, but you can obviously using various techniques. The following variations could be of interest: + +### From LuaCode Asset to filesystem-based scripts + +If you want to allow the user to modify/hack your game after it has been built, you can use normal filesystem-based lua scripts. + +Just place them in the Content directory (it is added by default to the Lua package search path) and change the TalkingCharacter Blueprint to use the filename as the source script: + +![FromFile](SimpleDialogueSystem_Data/FromFile.PNG?raw=true "FromFile") + +Eventually you can use both (to allow some form of modding): + +![Modding](SimpleDialogueSystem_Data/Modding.PNG?raw=true "Modding") + +We first check for a Lua file, if it is not available (returns nil in such a case), we use the LuaCode asset. + +### Using C++ instead of Blueprint Nodes + +Instead of exposing Blueprint functions to Lua, you can expose Cpp functions. + +This is the code for a CppDialogueLuaState class that you can use in place of the DialogueLuaState one: + +```cpp +#pragma once + +#include "CoreMinimal.h" +#include "LuaState.h" +#include "CppDialogueLuaState.generated.h" + +/** + * + */ +UCLASS() +class TUTORIALLUADIALOGUE_API UCppDialogueLuaState : public ULuaState +{ + GENERATED_BODY() + +public: + UCppDialogueLuaState(); + + UFUNCTION() + FLuaValue GetPlayer(); + + UFUNCTION() + void Quit(); + + UFUNCTION() + void Print(FLuaValue Message); + +}; +``` + +```cpp +#include "CppDialogueLuaState.h" +#include "Engine/World.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/Pawn.h" +#include "Engine/GameEngine.h" + +UCppDialogueLuaState::UCppDialogueLuaState() +{ + Table.Add("get_player", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(UCppDialogueLuaState, GetPlayer))); + Table.Add("quit", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(UCppDialogueLuaState, Quit))); + Table.Add("print", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(UCppDialogueLuaState, Print))); + + Table.Add("current_target", FLuaValue()); + Table.Add("player_gold", FLuaValue(4)); +} + +FLuaValue UCppDialogueLuaState::GetPlayer() +{ + APlayerController* PlayerController = GetWorld()->GetFirstPlayerController(); + return PlayerController ? FLuaValue(PlayerController->GetPawn()) : FLuaValue(); +} + +void UCppDialogueLuaState::Quit() +{ + APlayerController* PlayerController = GetWorld()->GetFirstPlayerController(); + if (PlayerController) + { + PlayerController->ConsoleCommand("quit"); + } +} + +void UCppDialogueLuaState::Print(FLuaValue Message) +{ + GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Cyan, *Message.ToString()); +} + +``` + +``` + +`Tutorials/WebSocketsWithLuaUserDataObjects.md`: + +```md +# Implementing a WebSockets API using LuaUserDataObjects + +## LuaUserDataObjects ? + +The 'ULuaUserDataObject' UCLASS allows you to create new kinds of LuaValues built over the UObject system. + +In addition to 'primitive' types (string, boolean, numbers, tables ...) Lua allows for so called "userdata". At the lowest level they are pointers to custom data with (eventually) a metatable +attached to them. Having a metatable allows them to expose parameters/methods like normal tables. + +If you want to expose methods or attributes for LuaValues wrapping UObjects, you can create a new Unreal Engine UCLASS (Blueprint or C++) inheriting from ULuaUserDataObject and add items to its Table and Metatable properties +(like with LuaStates, LuaBlueprintPackages and LuaComponents). + + +The following code shows what we are trying to accomplish: + +```lua +local ws = websocket() + +-- internally print() will call the __tostring metamethod of the ws object +print(ws) + +-- this will be called whenever a new message is received +function ws:on_message(message) + print(message) + --ws:close(17, 'End of the World') +end + +-- this is called on connection close +function ws:on_closed(code, reason, user) + print('websocket connection closed: ' .. reason) +end + +-- the first function/callback is called on successful connection +-- the second one on failure +ws:connect('wss://echo.websocket.org', function(self) + print('connected') + print(self) +end, function(self, reason) + print('unable to connect: ' .. reason) +end) + +-- this can be called from any part of your project to trigger sending of messages +function send_event(message) + print('sending...') + ws:send(message) +end +``` + +'Note' that is not responsibility of the Lua VM to manage the lifetime of the associated ULuaUserDataObject: if the Unreal Garbage Collector frees it, the FLuaValue will become nil. + +## The ULuaWebSocketConnection UCLASS + +The objective is to define a new lua type representing a WebSocket connection to a url exposing a bunch of methods (connect, disconnect, send and close) and attributes/callbacks (on_message, on_closed). + +We start by subclassing UluaUserDataObject: + +```c++ +#pragma once + +#include "CoreMinimal.h" +#include "LuaUserDataObject.h" +#include "IWebSocket.h" +#include "LuaWebsocketConnection.generated.h" + +/** + * + */ +UCLASS() +class RPGLUA_API ULuaWebsocketConnection : public ULuaUserDataObject +{ + GENERATED_BODY() + +public: + ULuaWebsocketConnection(); + ~ULuaWebsocketConnection(); + + UFUNCTION() + void Connect(FLuaValue Url, FLuaValue InOnConnectedCallback, FLuaValue InOnConnectionErrorCallback); + + UFUNCTION() + void Disconnect(); + + UFUNCTION() + void Send(FLuaValue Message); + + UFUNCTION() + FLuaValue ToLuaString(); + + UFUNCTION() + void Close(FLuaValue Code, FLuaValue Reason); + +protected: + TSharedPtr WebSocketConnection; + TSharedPtr OnConnectedCallback; + TSharedPtr OnConnectionErrorCallback; + + void OnMessageDelegate(const FString& Message); + void OnClosedDelegate(int32 Code, const FString& Reason, bool bUserClose); + + void OnConnectedDelegate(TWeakPtr OnConnectedCallbackRef); + void OnConnectionErrorDelegate(const FString& Message, TWeakPtr OnConnectionErrorCallbackRef); + + FString CurrentUrl; +}; +``` + +The 4 UFUNCTIONs will be exposed as lua functions (see the constructor below) while the two `TSharedPtr` members are required for the OnConnectedDelegate and OnConnectionErrorDelegate functions. + +Delegates can be triggered at any moment, even after the related FLuaValues (callback/functions) are destroyed. The FLuaSmartReference object allows you to link a LuaValue to a LuaState, avoiding it to be garbaged (note: remember to unlink them !). + +The OnMessageDelegate and OnClosedDelegate do not require a FLuaSmartReference as they are based on attributes of the object itself (see ws:on_message() and ws:on_closed() functions defined in the previous lua code). + +Time for functions definitions: + +```c++ +#include "LuaWebsocketConnection.h" +#include "LuaBlueprintFunctionLibrary.h" +#include "WebSocketsModule.h" + +ULuaWebsocketConnection::ULuaWebsocketConnection() +{ + Table.Add("connect", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(ULuaWebsocketConnection, Connect))); + Table.Add("disconnect", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(ULuaWebsocketConnection, Disconnect))); + Table.Add("send", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(ULuaWebsocketConnection, Send))); + Table.Add("close", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(ULuaWebsocketConnection, Close))); + + Metatable.Add("__tostring", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(ULuaWebsocketConnection, ToLuaString))); + + bImplicitSelf = true; +} + +ULuaWebsocketConnection::~ULuaWebsocketConnection() +{ + Disconnect(); + UE_LOG(LogLuaMachine, Log, TEXT("Websocket Destroyed")); +} + +void ULuaWebsocketConnection::Connect(FLuaValue Url, FLuaValue InOnConnectedCallback, FLuaValue InOnConnectionErrorCallback) +{ + Disconnect(); + + CurrentUrl = Url.ToString(); + + WebSocketConnection = FWebSocketsModule::Get().CreateWebSocket(Url.ToString(), TEXT("ws")); + WebSocketConnection->OnMessage().AddUObject(this, &ULuaWebsocketConnection::OnMessageDelegate); + WebSocketConnection->OnClosed().AddUObject(this, &ULuaWebsocketConnection::OnClosedDelegate); + + OnConnectedCallback = AddLuaSmartReference(InOnConnectedCallback); + OnConnectionErrorCallback = AddLuaSmartReference(InOnConnectionErrorCallback); + + WebSocketConnection->OnConnectionError().AddUObject(this, &ULuaWebsocketConnection::OnConnectionErrorDelegate, TWeakPtr(OnConnectionErrorCallback)); + WebSocketConnection->OnConnected().AddUObject(this, &ULuaWebsocketConnection::OnConnectedDelegate, TWeakPtr(OnConnectedCallback)); + WebSocketConnection->Connect(); +} + +void ULuaWebsocketConnection::Disconnect() +{ + OnConnectedCallback.Reset(); + OnConnectionErrorCallback.Reset(); + + if (WebSocketConnection.IsValid()) + { + WebSocketConnection->Close(); + WebSocketConnection.Reset(); + } +} + +void ULuaWebsocketConnection::Send(FLuaValue Message) +{ + if (WebSocketConnection.IsValid() && WebSocketConnection->IsConnected()) + { + WebSocketConnection->Send(Message.ToString()); + } +} + +void ULuaWebsocketConnection::Close(FLuaValue Code, FLuaValue Reason) +{ + if (WebSocketConnection.IsValid() && WebSocketConnection->IsConnected()) + { + WebSocketConnection->Close(Code.Integer, Reason.ToString()); + } +} + +FLuaValue ULuaWebsocketConnection::ToLuaString() +{ + if (WebSocketConnection.IsValid() && WebSocketConnection->IsConnected()) + { + return FLuaValue(FString::Printf(TEXT("WebSocket Connected to %s"), *CurrentUrl)); + } + return FLuaValue("Unconnected WebSocket"); +} + +void ULuaWebsocketConnection::OnMessageDelegate(const FString& Message) +{ + FLuaValue Callback = LuaGetField("on_message"); + TArray Args = { FLuaValue(this), FLuaValue(Message) }; + ULuaBlueprintFunctionLibrary::LuaValueCallIfNotNil(Callback, Args); +} + +void ULuaWebsocketConnection::OnClosedDelegate(int32 Code, const FString& Reason, bool bUserClose) +{ + FLuaValue Callback = LuaGetField("on_closed"); + TArray Args = { FLuaValue(this), FLuaValue(Code), FLuaValue(Reason), FLuaValue(bUserClose) }; + ULuaBlueprintFunctionLibrary::LuaValueCallIfNotNil(Callback, Args); +} + +void ULuaWebsocketConnection::OnConnectedDelegate(TWeakPtr OnConnectedCallbackRef) +{ + if (!OnConnectedCallbackRef.IsValid()) + return; + + TArray Args = { FLuaValue(this) }; + ULuaBlueprintFunctionLibrary::LuaValueCallIfNotNil(OnConnectedCallbackRef.Pin()->Value, Args); +} + +void ULuaWebsocketConnection::OnConnectionErrorDelegate(const FString& Message, TWeakPtr OnConnectionErrorCallbackRef) +{ + if (!OnConnectionErrorCallbackRef.IsValid()) + return; + + TArray Args = { FLuaValue(this), FLuaValue(Message) }; + ULuaBlueprintFunctionLibrary::LuaValueCallIfNotNil(OnConnectionErrorCallbackRef.Pin()->Value, Args); +} +``` + +Note the checks for .IsValid() for the OnConnectedDelegate() and OnConnectionErrorDelegate() functions: they can be called at any moment, so we must be sure the native lua callbacks/functions are still valid. This is not required for OnMessageDelegate() and OnClosedDelegate() as they make use of the LuaGetField() function to retrieve the related lua callback. + +Here the destructor prints a log message to allow the user/developer to track unexpected deallocations (see below) + +## The LuaState + +This is a simple LuaState showing how to exposed the previously create ULuaUserDataObject: + +```c++ +#pragma once + +#include "CoreMinimal.h" +#include "LuaState.h" +#include "LuaWebsocketConnection.h" +#include "WebSocketsLuaStateBase.generated.h" + +/** + * + */ +UCLASS() +class RPGLUA_API UWebSocketsLuaStateBase : public ULuaState +{ + GENERATED_BODY() + +public: + UWebSocketsLuaStateBase(); + + UFUNCTION() + FLuaValue GetWebSocketConnectionSingleton(); + +protected: + UPROPERTY() + ULuaWebsocketConnection* WebSocketConnectionSingleton; + +}; + +``` + +The WebSocketConnectionSingleton member allows you to have a single instance of the websocket() object and, in addition to this, it will avoid the Unreal Garbage Collector to destroy it. If you want to allow the user/scripter to create more instances just use a TArray for tracking them (but implement a way to "untrack" them or to limit the amount of instances). + +Let's go to the implementation: + +```c++ +#include "WebSocketsLuaStateBase.h" + +UWebSocketsLuaStateBase::UWebSocketsLuaStateBase() +{ + Table.Add("websocket", FLuaValue::Function(GET_FUNCTION_NAME_CHECKED(UWebSocketsLuaStateBase, GetWebSocketConnectionSingleton))); +} + +FLuaValue UWebSocketsLuaStateBase::GetWebSocketConnectionSingleton() +{ + if (!WebSocketConnectionSingleton) + { + WebSocketConnectionSingleton = NewObject(this); + } + return FLuaValue(WebSocketConnectionSingleton); +} +``` + + +``` \ No newline at end of file diff --git a/archive/sideeffects/HoudiniEngineForUnreal.txt b/archive/sideeffects/HoudiniEngineForUnreal.txt new file mode 100644 index 00000000..0413fdc7 --- /dev/null +++ b/archive/sideeffects/HoudiniEngineForUnreal.txt @@ -0,0 +1,302606 @@ +Project Path: arc_sideeffects_HoudiniEngineForUnreal_ugkgsjj2 + +Source Tree: + +```txt +arc_sideeffects_HoudiniEngineForUnreal_ugkgsjj2 +├── Config +│ └── BaseHoudiniEngine.ini +├── Content +│ ├── Examples +│ │ ├── Chaos +│ │ │ ├── unreal_gc_example.hda +│ │ │ ├── unreal_gc_example.uasset +│ │ │ ├── unreal_gc_example_array.hda +│ │ │ ├── unreal_gc_example_array.uasset +│ │ │ ├── unreal_gc_example_clustering.hda +│ │ │ └── unreal_gc_example_clustering.uasset +│ │ ├── Cpp +│ │ │ ├── CurveInputExample.cpp +│ │ │ ├── CurveInputExample.h +│ │ │ ├── IterateOverHoudiniActorsExample.cpp +│ │ │ └── IterateOverHoudiniActorsExample.h +│ │ ├── EUA +│ │ │ └── EUA_CurveInputExample.uasset +│ │ ├── EUW +│ │ │ └── EUW_APIExample.uasset +│ │ ├── Maps +│ │ │ ├── LandscapeInputExample.umap +│ │ │ └── LandscapeInputExample_BuiltData.uasset +│ │ ├── Python +│ │ │ ├── asset_input_example.py +│ │ │ ├── bake_all_outputs_example.py +│ │ │ ├── bake_output_object_example.py +│ │ │ ├── curve_input_example.py +│ │ │ ├── eau_curve_input_example.py +│ │ │ ├── geo_input_example.py +│ │ │ ├── instances_example.py +│ │ │ ├── iterate_over_houdini_asset_actors_example.py +│ │ │ ├── landscape_input_example.py +│ │ │ ├── outputs_example.py +│ │ │ ├── pdg_example.py +│ │ │ ├── process_hda_example.py +│ │ │ ├── ramp_parameter_example.py +│ │ │ ├── start_session_example.py +│ │ │ └── world_input_example.py +│ │ └── hda +│ │ ├── HEUE_Geometry_Fence.hda +│ │ ├── HEUE_Geometry_Fence.uasset +│ │ ├── copy_to_curve.1.0.hda +│ │ ├── copy_to_curve_1_0.uasset +│ │ ├── hilly_landscape_erode.1.0.hda +│ │ ├── hilly_landscape_erode_1_0.uasset +│ │ ├── pdg_pighead_grid.2.0.hda +│ │ ├── pdg_pighead_grid_2_0.uasset +│ │ ├── pig_head_subdivider_v01.hda +│ │ ├── pig_head_subdivider_v01.uasset +│ │ ├── ramp_example.1.0.hda +│ │ ├── ramp_example_1_0.uasset +│ │ ├── subnet_test.2.0.hda +│ │ └── subnet_test_2_0.uasset +│ ├── Icons +│ │ ├── icon_houdini_logo_128.png +│ │ ├── icon_houdini_logo_16.png +│ │ └── icon_houdini_logo_40.png +│ ├── NodeSync +│ │ ├── KineFXToUnreal.hda +│ │ └── UnrealToKineFX.hda +│ ├── Python +│ │ ├── HoudiniEngineV2 +│ │ │ ├── __init__.py +│ │ │ └── asyncprocessor.py +│ │ └── __init__.py +│ ├── Test +│ │ └── hda +│ │ ├── InputEcho.hda +│ │ ├── InputEcho.uasset +│ │ ├── TestBox.hda +│ │ ├── TestBox.uasset +│ │ ├── TestParams.hda +│ │ ├── TestParams.uasset +│ │ ├── TestPolyReduce.hda +│ │ └── TestPolyReduce.uasset +│ ├── Textures +│ │ └── icon_houdini_logo_128.uasset +│ ├── Tools +│ │ ├── he_sop_boolean.hda +│ │ ├── he_sop_boolean.png +│ │ ├── he_sop_boolean.uasset +│ │ ├── he_sop_curve_instancer.hda +│ │ ├── he_sop_curve_instancer.png +│ │ ├── he_sop_curve_instancer.uasset +│ │ ├── he_sop_polyreduce.hda +│ │ ├── he_sop_polyreduce.png +│ │ ├── he_sop_polyreduce.uasset +│ │ ├── rock_generator.hda +│ │ ├── rock_generator.png +│ │ └── rock_generator.uasset +│ ├── big_question_mark.uasset +│ ├── default_reference_static_mesh.uasset +│ ├── default_reference_static_mesh_material.uasset +│ ├── houdini_bgeo_import.uasset +│ ├── houdini_default_cop_material.uasset +│ ├── houdini_default_cop_mesh.uasset +│ ├── houdini_default_material.uasset +│ ├── houdini_logo.uasset +│ └── houdini_templated_material.uasset +├── HoudiniEngine.uplugin +├── LICENSE.md +├── README.md +├── Resources +│ ├── Icon128.png +│ ├── Icons +│ │ ├── asset_help16x16.png +│ │ ├── bake_all16x16.png +│ │ ├── bake_selected16x16.png +│ │ ├── clean_temp16x16.png +│ │ ├── cook_all16x16.png +│ │ ├── cook_log16x16.png +│ │ ├── cook_selected16x16.png +│ │ ├── curve_closed16x16.png +│ │ ├── curve_not_closed16x16.png +│ │ ├── curve_not_reversed16x16.png +│ │ ├── curve_reversed16x16.png +│ │ ├── digital_asset16x16.png +│ │ ├── houdini_digital_asset.png +│ │ ├── houdini_digital_asset_128.png +│ │ ├── icon_hengine_logo_128.png +│ │ ├── icon_hengine_logo_16.png +│ │ ├── icon_hengine_logo_40.png +│ │ ├── icon_houdini_logo_128.png +│ │ ├── icon_houdini_logo_16.png +│ │ ├── icon_houdini_logo_40.png +│ │ ├── online_forum16x16.png +│ │ ├── online_help16x16.png +│ │ ├── open_in_houdini16x16.png +│ │ ├── pause16x16.png +│ │ ├── pdg_cancel16x16.png +│ │ ├── pdg_dirty_all16x16.png +│ │ ├── pdg_dirty_node16x16.png +│ │ ├── pdg_link16x16.png +│ │ ├── pdg_pause16x16.png +│ │ ├── pdg_refresh16x16.png +│ │ ├── pdg_reset16x16.png +│ │ ├── rebuild_all16x16.png +│ │ ├── rebuild_selected16x16.png +│ │ ├── refine_all16x16.png +│ │ ├── refine_selected16x16.png +│ │ ├── report_bug16x16.png +│ │ ├── reset16x16.png +│ │ ├── reset_parameters16x16.png +│ │ ├── save_to_hip16x16.png +│ │ ├── session_connect16x16.png +│ │ ├── session_create16x16.png +│ │ ├── session_restart16x16.png +│ │ ├── session_stop16x16.png +│ │ ├── session_sync16x16.png +│ │ ├── session_sync_start16x16.png +│ │ ├── session_sync_stop16x16.png +│ │ ├── sync_unreal16x16.png +│ │ ├── viewport_sync16x16.png +│ │ ├── viewport_sync_both16x16.png +│ │ ├── viewport_sync_houdini16x16.png +│ │ ├── viewport_sync_off16x16.png +│ │ └── viewport_sync_unreal16x16.png +│ ├── hengine_banner.png +│ ├── hengine_banner_d.png +│ ├── hengine_logo_128.png +│ ├── hengine_recook_icon.png +│ ├── hengine_reload_icon.png +│ └── hengine_resetparameters_icon.png +└── Source + ├── HoudiniEngine + │ ├── HoudiniEngine.Build.cs + │ ├── Private + │ │ ├── GeometryToolsEngine.cpp + │ │ ├── GeometryToolsEngine.h + │ │ ├── HBSPOps.cpp + │ │ ├── HBSPOps.h + │ │ ├── HCsgUtils.cpp + │ │ ├── HCsgUtils.h + │ │ ├── HoudiniAlgorithm.h + │ │ ├── HoudiniAnimationTranslator.cpp + │ │ ├── HoudiniAnimationTranslator.h + │ │ ├── HoudiniApi.cpp + │ │ ├── HoudiniBakeLandscape.cpp + │ │ ├── HoudiniBakeLandscape.h + │ │ ├── HoudiniBakeLevelInstanceUtils.cpp + │ │ ├── HoudiniBakeLevelInstanceUtils.h + │ │ ├── HoudiniDataLayerUtils.cpp + │ │ ├── HoudiniDataLayerUtils.h + │ │ ├── HoudiniDataTableTranslator.cpp + │ │ ├── HoudiniDataTableTranslator.h + │ │ ├── HoudiniEngine.cpp + │ │ ├── HoudiniEngine.h + │ │ ├── HoudiniEngineAttributes.cpp + │ │ ├── HoudiniEngineAttributes.h + │ │ ├── HoudiniEngineBakeUtils.cpp + │ │ ├── HoudiniEngineBakeUtils.h + │ │ ├── HoudiniEngineCommandlet.cpp + │ │ ├── HoudiniEngineCommandlet.h + │ │ ├── HoudiniEngineManager.cpp + │ │ ├── HoudiniEngineManager.h + │ │ ├── HoudiniEngineOutputStats.cpp + │ │ ├── HoudiniEngineOutputStats.h + │ │ ├── HoudiniEnginePrivatePCH.h + │ │ ├── HoudiniEngineProcessor.cpp + │ │ ├── HoudiniEngineProcessor.h + │ │ ├── HoudiniEngineScheduler.cpp + │ │ ├── HoudiniEngineScheduler.h + │ │ ├── HoudiniEngineString.cpp + │ │ ├── HoudiniEngineString.h + │ │ ├── HoudiniEngineTask.cpp + │ │ ├── HoudiniEngineTask.h + │ │ ├── HoudiniEngineTaskInfo.cpp + │ │ ├── HoudiniEngineTaskInfo.h + │ │ ├── HoudiniEngineTimers.cpp + │ │ ├── HoudiniEngineTimers.h + │ │ ├── HoudiniEngineUtils.cpp + │ │ ├── HoudiniEngineUtils.h + │ │ ├── HoudiniFoliageTools.cpp + │ │ ├── HoudiniFoliageTools.h + │ │ ├── HoudiniGeoImportCommandlet.cpp + │ │ ├── HoudiniGeoImportCommandlet.h + │ │ ├── HoudiniGeoImporter.cpp + │ │ ├── HoudiniGeoImporter.h + │ │ ├── HoudiniGeometryCollectionTranslator.cpp + │ │ ├── HoudiniGeometryCollectionTranslator.h + │ │ ├── HoudiniHLODLayerUtils.cpp + │ │ ├── HoudiniHLODLayerUtils.h + │ │ ├── HoudiniHandleTranslator.cpp + │ │ ├── HoudiniHandleTranslator.h + │ │ ├── HoudiniInputTranslator.cpp + │ │ ├── HoudiniInputTranslator.h + │ │ ├── HoudiniInstanceTranslator.cpp + │ │ ├── HoudiniInstanceTranslator.h + │ │ ├── HoudiniLandscapeSplineTranslator.cpp + │ │ ├── HoudiniLandscapeSplineTranslator.h + │ │ ├── HoudiniLandscapeTranslator.cpp + │ │ ├── HoudiniLandscapeTranslator.h + │ │ ├── HoudiniLandscapeUtils.cpp + │ │ ├── HoudiniLandscapeUtils.h + │ │ ├── HoudiniLevelInstanceUtils.cpp + │ │ ├── HoudiniLevelInstanceUtils.h + │ │ ├── HoudiniMaterialTranslator.cpp + │ │ ├── HoudiniMaterialTranslator.h + │ │ ├── HoudiniMeshTranslator.cpp + │ │ ├── HoudiniMeshTranslator.h + │ │ ├── HoudiniMeshUtils.cpp + │ │ ├── HoudiniMeshUtils.h + │ │ ├── HoudiniOutputTranslator.cpp + │ │ ├── HoudiniOutputTranslator.h + │ │ ├── HoudiniPCGComponent.cpp + │ │ ├── HoudiniPCGComponent.h + │ │ ├── HoudiniPCGCookable.cpp + │ │ ├── HoudiniPCGCookable.h + │ │ ├── HoudiniPCGManagedResource.cpp + │ │ ├── HoudiniPCGManagedResource.h + │ │ ├── HoudiniPCGNode.cpp + │ │ ├── HoudiniPCGNode.h + │ │ ├── HoudiniPCGTranslator.cpp + │ │ ├── HoudiniPCGTranslator.h + │ │ ├── HoudiniPCGUtils.cpp + │ │ ├── HoudiniPCGUtils.h + │ │ ├── HoudiniPDGImporterMessages.cpp + │ │ ├── HoudiniPDGImporterMessages.h + │ │ ├── HoudiniPDGManager.cpp + │ │ ├── HoudiniPDGManager.h + │ │ ├── HoudiniPDGTranslator.cpp + │ │ ├── HoudiniPDGTranslator.h + │ │ ├── HoudiniPackageParams.cpp + │ │ ├── HoudiniPackageParams.h + │ │ ├── HoudiniParameterTranslator.cpp + │ │ ├── HoudiniParameterTranslator.h + │ │ ├── HoudiniSkeletalMeshTranslator.cpp + │ │ ├── HoudiniSkeletalMeshTranslator.h + │ │ ├── HoudiniSkeletalMeshUtils.cpp + │ │ ├── HoudiniSkeletalMeshUtils.h + │ │ ├── HoudiniSplineTranslator.cpp + │ │ ├── HoudiniSplineTranslator.h + │ │ ├── HoudiniStringResolver.cpp + │ │ ├── HoudiniStringResolver.h + │ │ ├── HoudiniTextureTranslator.cpp + │ │ ├── HoudiniTextureTranslator.h + │ │ ├── SAssetSelectionWidget.cpp + │ │ ├── SAssetSelectionWidget.h + │ │ ├── Tests + │ │ │ ├── HoudiniCoreTests.cpp + │ │ │ └── HoudiniCoreTests.h + │ │ ├── UnrealAnimationTranslator.cpp + │ │ ├── UnrealAnimationTranslator.h + │ │ ├── UnrealBrushTranslator.cpp + │ │ ├── UnrealBrushTranslator.h + │ │ ├── UnrealDataTableTranslator.cpp + │ │ ├── UnrealDataTableTranslator.h + │ │ ├── UnrealFoliageTypeTranslator.cpp + │ │ ├── UnrealFoliageTypeTranslator.h + │ │ ├── UnrealGeometryCollectionTranslator.cpp + │ │ ├── UnrealGeometryCollectionTranslator.h + │ │ ├── UnrealInstanceTranslator.cpp + │ │ ├── UnrealInstanceTranslator.h + │ │ ├── UnrealLandscapeSplineTranslator.cpp + │ │ ├── UnrealLandscapeSplineTranslator.h + │ │ ├── UnrealLandscapeTranslator.cpp + │ │ ├── UnrealLandscapeTranslator.h + │ │ ├── UnrealLevelInstanceTranslator.cpp + │ │ ├── UnrealLevelInstanceTranslator.h + │ │ ├── UnrealMeshTranslator.cpp + │ │ ├── UnrealMeshTranslator.h + │ │ ├── UnrealObjectInputManagerImpl.cpp + │ │ ├── UnrealObjectInputManagerImpl.h + │ │ ├── UnrealObjectInputTypes.cpp + │ │ ├── UnrealObjectInputTypes.h + │ │ ├── UnrealObjectInputUtils.cpp + │ │ ├── UnrealObjectInputUtils.h + │ │ ├── UnrealPCGDataTranslator.cpp + │ │ ├── UnrealPCGDataTranslator.h + │ │ ├── UnrealSkeletalMeshTranslator.cpp + │ │ ├── UnrealSkeletalMeshTranslator.h + │ │ ├── UnrealSplineTranslator.cpp + │ │ ├── UnrealSplineTranslator.h + │ │ ├── UnrealTextureTranslator.cpp + │ │ └── UnrealTextureTranslator.h + │ └── Public + │ ├── HAPI + │ │ ├── HAPI.h + │ │ ├── HAPI_API.h + │ │ ├── HAPI_Common.h + │ │ ├── HAPI_Helpers.h + │ │ └── HAPI_Version.h + │ └── HoudiniApi.h + ├── HoudiniEngineEditor + │ ├── HoudiniEngineEditor.Build.cs + │ ├── Private + │ │ ├── AssetDefinition_HoudiniToolsPackageAsset.h + │ │ ├── AssetTypeActions_HoudiniAsset.cpp + │ │ ├── AssetTypeActions_HoudiniAsset.h + │ │ ├── AssetTypeActions_HoudiniPreset.cpp + │ │ ├── AssetTypeActions_HoudiniPreset.h + │ │ ├── AssetTypeActions_HoudiniToolsPackageAsset.cpp + │ │ ├── AssetTypeActions_HoudiniToolsPackageAsset.h + │ │ ├── HoudiniAssetActorFactory.cpp + │ │ ├── HoudiniAssetActorFactory.h + │ │ ├── HoudiniAssetBroker.cpp + │ │ ├── HoudiniAssetBroker.h + │ │ ├── HoudiniAssetComponentDetails.cpp + │ │ ├── HoudiniAssetComponentDetails.h + │ │ ├── HoudiniAssetEditor.cpp + │ │ ├── HoudiniAssetEditor.h + │ │ ├── HoudiniAssetEditorViewportClient.cpp + │ │ ├── HoudiniAssetEditorViewportClient.h + │ │ ├── HoudiniAssetFactory.cpp + │ │ ├── HoudiniAssetFactory.h + │ │ ├── HoudiniCookableDetails.cpp + │ │ ├── HoudiniCookableDetails.h + │ │ ├── HoudiniEditorAssetStateSubsystem.cpp + │ │ ├── HoudiniEditorAssetStateSubsystem.h + │ │ ├── HoudiniEditorNodeSyncSubsystem.cpp + │ │ ├── HoudiniEngineCommands.cpp + │ │ ├── HoudiniEngineCommands.h + │ │ ├── HoudiniEngineDetails.cpp + │ │ ├── HoudiniEngineDetails.h + │ │ ├── HoudiniEngineEditor.cpp + │ │ ├── HoudiniEngineEditor.h + │ │ ├── HoudiniEngineEditorLocalization.cpp + │ │ ├── HoudiniEngineEditorLocalization.h + │ │ ├── HoudiniEngineEditorPrivatePCH.h + │ │ ├── HoudiniEngineEditorUtils.cpp + │ │ ├── HoudiniEngineEditorUtils.h + │ │ ├── HoudiniEngineStyle.cpp + │ │ ├── HoudiniEngineStyle.h + │ │ ├── HoudiniEngineTool.h + │ │ ├── HoudiniEngineToolTypes.h + │ │ ├── HoudiniGeoFactory.cpp + │ │ ├── HoudiniGeoFactory.h + │ │ ├── HoudiniHandleComponentVisualizer.cpp + │ │ ├── HoudiniHandleComponentVisualizer.h + │ │ ├── HoudiniHandleDetails.cpp + │ │ ├── HoudiniHandleDetails.h + │ │ ├── HoudiniInputDetails.cpp + │ │ ├── HoudiniInputDetails.h + │ │ ├── HoudiniInputWidgets.h + │ │ ├── HoudiniOutputDetails.cpp + │ │ ├── HoudiniOutputDetails.h + │ │ ├── HoudiniPCGComponentDetails.cpp + │ │ ├── HoudiniPCGComponentDetails.h + │ │ ├── HoudiniPCGDetails.cpp + │ │ ├── HoudiniPCGDetails.h + │ │ ├── HoudiniPCGSettingsDetails.cpp + │ │ ├── HoudiniPCGSettingsDetails.h + │ │ ├── HoudiniPDGDetails.cpp + │ │ ├── HoudiniPDGDetails.h + │ │ ├── HoudiniParameterDetails.cpp + │ │ ├── HoudiniParameterDetails.h + │ │ ├── HoudiniPreset.cpp + │ │ ├── HoudiniPreset.h + │ │ ├── HoudiniPresetActorFactory.cpp + │ │ ├── HoudiniPresetActorFactory.h + │ │ ├── HoudiniPresetFactory.h + │ │ ├── HoudiniPublicAPI.cpp + │ │ ├── HoudiniPublicAPIAssetWrapper.cpp + │ │ ├── HoudiniPublicAPIBlueprintLib.cpp + │ │ ├── HoudiniPublicAPIInputTypes.cpp + │ │ ├── HoudiniPublicAPIObjectBase.cpp + │ │ ├── HoudiniPublicAPIOutputTypes.cpp + │ │ ├── HoudiniPublicAPIProcessHDANode.cpp + │ │ ├── HoudiniRampEditorWindowSubsystem.cpp + │ │ ├── HoudiniRampEditorWindowSubsystem.h + │ │ ├── HoudiniRampView.h + │ │ ├── HoudiniRuntimeSettingsDetails.cpp + │ │ ├── HoudiniRuntimeSettingsDetails.h + │ │ ├── HoudiniSplineComponentVisualizer.cpp + │ │ ├── HoudiniSplineComponentVisualizer.h + │ │ ├── HoudiniToolTypesEditor.cpp + │ │ ├── HoudiniToolTypesEditor.h + │ │ ├── HoudiniToolsEditor.cpp + │ │ ├── HoudiniToolsEditor.h + │ │ ├── HoudiniToolsEditorUISubsystem.cpp + │ │ ├── HoudiniToolsEditorUISubsystem.h + │ │ ├── HoudiniToolsPackageAssetFactory.h + │ │ ├── SHoudiniAssetEditorViewport.cpp + │ │ ├── SHoudiniAssetEditorViewport.h + │ │ ├── SHoudiniColorRamp.cpp + │ │ ├── SHoudiniColorRamp.h + │ │ ├── SHoudiniFloatRamp.cpp + │ │ ├── SHoudiniFloatRamp.h + │ │ ├── SHoudiniNodeSyncPanel.cpp + │ │ ├── SHoudiniNodeSyncPanel.h + │ │ ├── SHoudiniNodeTreeView.cpp + │ │ ├── SHoudiniNodeTreeView.h + │ │ ├── SHoudiniPresets.cpp + │ │ ├── SHoudiniPresets.h + │ │ ├── SHoudiniRampBase.h + │ │ ├── SHoudiniToolsPanel.cpp + │ │ ├── SHoudiniToolsPanel.h + │ │ ├── SNewFilePathPicker.cpp + │ │ ├── SNewFilePathPicker.h + │ │ ├── SSelectFolderPathDialog.cpp + │ │ ├── SSelectFolderPathDialog.h + │ │ ├── SSelectHoudiniPathDialog.cpp + │ │ ├── SSelectHoudiniPathDialog.h + │ │ └── Tests + │ │ ├── HoudiniEditorColliderTests.cpp + │ │ ├── HoudiniEditorColliderTests.h + │ │ ├── HoudiniEditorEquivalenceUtils.cpp + │ │ ├── HoudiniEditorEquivalenceUtils.h + │ │ ├── HoudiniEditorHeightfieldTests.cpp + │ │ ├── HoudiniEditorHeightfieldTests.h + │ │ ├── HoudiniEditorInputTests.cpp + │ │ ├── HoudiniEditorInputTests.h + │ │ ├── HoudiniEditorInstancerTests.cpp + │ │ ├── HoudiniEditorInstancerTests.h + │ │ ├── HoudiniEditorMaterialTests.cpp + │ │ ├── HoudiniEditorMaterialTests.h + │ │ ├── HoudiniEditorMeshTests.cpp + │ │ ├── HoudiniEditorMeshTests.h + │ │ ├── HoudiniEditorParametersTests.cpp + │ │ ├── HoudiniEditorParametersTests.h + │ │ ├── HoudiniEditorTemplatedGeoTests.cpp + │ │ ├── HoudiniEditorTemplatedGeoTests.h + │ │ ├── HoudiniEditorTestAnimation.cpp + │ │ ├── HoudiniEditorTestAnimation.h + │ │ ├── HoudiniEditorTestBaking.cpp + │ │ ├── HoudiniEditorTestBaking.h + │ │ ├── HoudiniEditorTestDataLayers.cpp + │ │ ├── HoudiniEditorTestDataLayers.h + │ │ ├── HoudiniEditorTestGeometryCollections.cpp + │ │ ├── HoudiniEditorTestGeometryCollections.h + │ │ ├── HoudiniEditorTestHLOD.cpp + │ │ ├── HoudiniEditorTestHLOD.h + │ │ ├── HoudiniEditorTestInstances.cpp + │ │ ├── HoudiniEditorTestInstances.h + │ │ ├── HoudiniEditorTestLandscapeSplines.cpp + │ │ ├── HoudiniEditorTestLandscapeSplines.h + │ │ ├── HoudiniEditorTestLandscapes.cpp + │ │ ├── HoudiniEditorTestLandscapes.h + │ │ ├── HoudiniEditorTestMeshGroups.cpp + │ │ ├── HoudiniEditorTestMeshGroups.h + │ │ ├── HoudiniEditorTestMeshMisc.cpp + │ │ ├── HoudiniEditorTestMeshMisc.h + │ │ ├── HoudiniEditorTestOutputs.cpp + │ │ ├── HoudiniEditorTestOutputs.h + │ │ ├── HoudiniEditorTestPCG.cpp + │ │ ├── HoudiniEditorTestPCG.h + │ │ ├── HoudiniEditorTestPDG.cpp + │ │ ├── HoudiniEditorTestPDG.h + │ │ ├── HoudiniEditorTestPresets.cpp + │ │ ├── HoudiniEditorTestPresets.h + │ │ ├── HoudiniEditorTestProxyMesh.cpp + │ │ ├── HoudiniEditorTestProxyMesh.h + │ │ ├── HoudiniEditorTestSkeletalMesh.cpp + │ │ ├── HoudiniEditorTestSkeletalMesh.h + │ │ ├── HoudiniEditorTestUtils.cpp + │ │ ├── HoudiniEditorTestUtils.h + │ │ ├── HoudiniEditorTests.cpp + │ │ ├── HoudiniEditorTests.h + │ │ ├── HoudiniEditorUnitTestMeshUtils.cpp + │ │ ├── HoudiniEditorUnitTestMeshUtils.h + │ │ ├── HoudiniEditorUnitTestUtils.cpp + │ │ └── HoudiniEditorUnitTestUtils.h + │ └── Public + │ ├── HoudiniEditorNodeSyncSubsystem.h + │ ├── HoudiniPublicAPI.h + │ ├── HoudiniPublicAPIAssetWrapper.h + │ ├── HoudiniPublicAPIBlueprintLib.h + │ ├── HoudiniPublicAPIInputTypes.h + │ ├── HoudiniPublicAPIObjectBase.h + │ ├── HoudiniPublicAPIOutputTypes.h + │ ├── HoudiniPublicAPIProcessHDANode.h + │ └── IHoudiniEngineEditor.h + └── HoudiniEngineRuntime + ├── HoudiniEngineRuntime.Build.cs + ├── Private + │ ├── HoudiniAsset.cpp + │ ├── HoudiniAssetActor.cpp + │ ├── HoudiniAssetBlueprintComponent.cpp + │ ├── HoudiniAssetBlueprintComponent.h + │ ├── HoudiniAssetComponent.cpp + │ ├── HoudiniAssetComponent.h + │ ├── HoudiniAssetStateTypes.h + │ ├── HoudiniCookable.cpp + │ ├── HoudiniCookable.h + │ ├── HoudiniEditorAssetStateSubsystemInterface.cpp + │ ├── HoudiniEditorAssetStateSubsystemInterface.h + │ ├── HoudiniEngineCopyPropertiesInterface.cpp + │ ├── HoudiniEngineCopyPropertiesInterface.h + │ ├── HoudiniEngineEditorSettings.cpp + │ ├── HoudiniEngineEditorSettings.h + │ ├── HoudiniEngineRuntime.cpp + │ ├── HoudiniEngineRuntime.h + │ ├── HoudiniEngineRuntimePrivatePCH.h + │ ├── HoudiniEngineRuntimeUtils.cpp + │ ├── HoudiniEngineRuntimeUtils.h + │ ├── HoudiniFoliageUtils.cpp + │ ├── HoudiniFoliageUtils.h + │ ├── HoudiniGenericAttribute.cpp + │ ├── HoudiniGenericAttribute.h + │ ├── HoudiniGeoAsset.cpp + │ ├── HoudiniGeoAsset.h + │ ├── HoudiniGeoPartObject.cpp + │ ├── HoudiniGeoPartObject.h + │ ├── HoudiniHandleComponent.cpp + │ ├── HoudiniHandleComponent.h + │ ├── HoudiniInput.cpp + │ ├── HoudiniInput.h + │ ├── HoudiniInputObject.cpp + │ ├── HoudiniInputObject.h + │ ├── HoudiniInputTypes.cpp + │ ├── HoudiniInputTypes.h + │ ├── HoudiniInstancedActorComponent.cpp + │ ├── HoudiniInstancedActorComponent.h + │ ├── HoudiniLandscapeRuntimeUtils.cpp + │ ├── HoudiniLandscapeRuntimeUtils.h + │ ├── HoudiniMaterialMap.cpp + │ ├── HoudiniMaterialMap.h + │ ├── HoudiniNodeSyncComponent.cpp + │ ├── HoudiniNodeSyncComponent.h + │ ├── HoudiniOutput.cpp + │ ├── HoudiniOutput.h + │ ├── HoudiniOutputTypes.h + │ ├── HoudiniPCGDataObject.cpp + │ ├── HoudiniPDGAssetLink.cpp + │ ├── HoudiniPDGAssetLink.h + │ ├── HoudiniParameter.cpp + │ ├── HoudiniParameter.h + │ ├── HoudiniParameterButton.cpp + │ ├── HoudiniParameterButton.h + │ ├── HoudiniParameterButtonStrip.cpp + │ ├── HoudiniParameterButtonStrip.h + │ ├── HoudiniParameterChoice.cpp + │ ├── HoudiniParameterChoice.h + │ ├── HoudiniParameterColor.cpp + │ ├── HoudiniParameterColor.h + │ ├── HoudiniParameterFile.cpp + │ ├── HoudiniParameterFile.h + │ ├── HoudiniParameterFloat.cpp + │ ├── HoudiniParameterFloat.h + │ ├── HoudiniParameterFolder.cpp + │ ├── HoudiniParameterFolder.h + │ ├── HoudiniParameterFolderList.cpp + │ ├── HoudiniParameterFolderList.h + │ ├── HoudiniParameterInt.cpp + │ ├── HoudiniParameterInt.h + │ ├── HoudiniParameterLabel.cpp + │ ├── HoudiniParameterLabel.h + │ ├── HoudiniParameterMultiParm.cpp + │ ├── HoudiniParameterMultiParm.h + │ ├── HoudiniParameterOperatorPath.cpp + │ ├── HoudiniParameterOperatorPath.h + │ ├── HoudiniParameterRamp.cpp + │ ├── HoudiniParameterRamp.h + │ ├── HoudiniParameterSeparator.cpp + │ ├── HoudiniParameterSeparator.h + │ ├── HoudiniParameterString.cpp + │ ├── HoudiniParameterString.h + │ ├── HoudiniParameterToggle.cpp + │ ├── HoudiniParameterToggle.h + │ ├── HoudiniPluginSerializationVersion.cpp + │ ├── HoudiniPluginSerializationVersion.h + │ ├── HoudiniRuntimeSettings.cpp + │ ├── HoudiniRuntimeSettings.h + │ ├── HoudiniSplineComponent.cpp + │ ├── HoudiniSplineComponent.h + │ ├── HoudiniStaticMesh.cpp + │ ├── HoudiniStaticMesh.h + │ ├── HoudiniStaticMeshComponent.cpp + │ ├── HoudiniStaticMeshComponent.h + │ ├── HoudiniStaticMeshSceneProxy.cpp + │ ├── HoudiniStaticMeshSceneProxy.h + │ ├── HoudiniToolData.cpp + │ ├── HoudiniToolTypes.h + │ ├── HoudiniToolsPackageAsset.cpp + │ ├── HoudiniToolsPackageAsset.h + │ ├── HoudiniToolsRuntimeUtils.cpp + │ ├── HoudiniToolsRuntimeUtils.h + │ ├── HoudiniTranslatorTypes.cpp + │ ├── HoudiniTranslatorTypes.h + │ ├── IHoudiniAssetStateEvents.cpp + │ ├── IHoudiniAssetStateEvents.h + │ ├── Tests + │ │ ├── HoudiniRuntimeTests.cpp + │ │ └── HoudiniRuntimeTests.h + │ ├── UnrealObjectInputManager.cpp + │ ├── UnrealObjectInputManager.h + │ ├── UnrealObjectInputRuntimeTypes.cpp + │ ├── UnrealObjectInputRuntimeTypes.h + │ ├── UnrealObjectInputRuntimeUtils.cpp + │ └── UnrealObjectInputRuntimeUtils.h + └── Public + ├── HoudiniAsset.h + ├── HoudiniAssetActor.h + ├── HoudiniEngineRuntimeCommon.h + ├── HoudiniPCGDataObject.h + └── HoudiniToolData.h + +``` + +`Config/BaseHoudiniEngine.ini`: + +```ini +[CoreRedirects] ++EnumRedirects=(OldName="EHoudiniInputType", ValueChanges=(("Asset", "World"), ("Asset_DEPRECATED", "World"), ("Landscape", "World"), ("Landscape_DEPRECATED", "World"), ("Skeletal", "Geometry"), ("Skeletal_DEPRECATED", "Geometry"), ("GeometryCollection", "Geometry"), ("GeometryCollection_DEPRECATED", "Geometry"))) + +``` + +`Content/Examples/Cpp/CurveInputExample.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "CurveInputExample.h" + +#include "Engine/StaticMesh.h" + +#include "HoudiniAsset.h" +#include "HoudiniPublicAPI.h" +#include "HoudiniPublicAPIBlueprintLib.h" +#include "HoudiniPublicAPIAssetWrapper.h" +#include "HoudiniPublicAPIInputTypes.h" + +ACurveInputExample::ACurveInputExample() + : AssetWrapper(nullptr) +{ + +} + +void ACurveInputExample::RunCurveInputExample_Implementation() +{ + // Get the API instance + UHoudiniPublicAPI* const API = UHoudiniPublicAPIBlueprintLib::GetAPI(); + // Ensure we have a running session + if (!API->IsSessionValid()) + API->CreateSession(); + // Load our HDA uasset + UHoudiniAsset* const ExampleHDA = Cast(StaticLoadObject(UHoudiniAsset::StaticClass(), nullptr, TEXT("/HoudiniEngine/Examples/hda/copy_to_curve_1_0.copy_to_curve_1_0"))); + // Create an API wrapper instance for instantiating the HDA and interacting with it + // The public API does not manage the lifecycle of the UHoudiniPublicAPIAssetWrapper after creation, so use a + // UProperty to prevent garbage collection while the wrapper is in use + AssetWrapper = API->InstantiateAsset(ExampleHDA, FTransform::Identity); + if (IsValid(AssetWrapper)) + { + // Pre-instantiation is the earliest point where we can set parameter values + AssetWrapper->GetOnPreInstantiationDelegate().AddUniqueDynamic(this, &ACurveInputExample::SetInitialParameterValues); + // Post-instantiation is the earliest point where we can set inputs + AssetWrapper->GetOnPostInstantiationDelegate().AddUniqueDynamic(this, &ACurveInputExample::SetInputs); + // After a cook and after the plugin has created/updated objects/assets from the node's outputs + AssetWrapper->GetOnPostProcessingDelegate().AddUniqueDynamic(this, &ACurveInputExample::PrintOutputs); + } +} + +void ACurveInputExample::SetInitialParameterValues_Implementation(UHoudiniPublicAPIAssetWrapper* InWrapper) +{ + // Uncheck the upvectoratstart parameter + InWrapper->SetBoolParameterValue(TEXT("upvectoratstart"), false); + + // Set the scale to 0.2 + InWrapper->SetFloatParameterValue(TEXT("scale"), 0.2f); + + // Since we are done with setting the initial values, we can unbind from the delegate + InWrapper->GetOnPreInstantiationDelegate().RemoveDynamic(this, &ACurveInputExample::SetInitialParameterValues); +} + +void ACurveInputExample::SetInputs_Implementation(UHoudiniPublicAPIAssetWrapper* InWrapper) +{ + // Create an empty geometry input + UHoudiniPublicAPIGeoInput* const GeoInput = Cast(InWrapper->CreateEmptyInput(UHoudiniPublicAPIGeoInput::StaticClass())); + // Load the cube static mesh asset + UStaticMesh* const Cube = Cast(StaticLoadObject(UStaticMesh::StaticClass(), nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"))); + // Set the input object array for our geometry input, in this case containing only the cube + GeoInput->SetInputObjects({Cube}); + + // Set the input on the instantiated HDA via the wrapper + InWrapper->SetInputAtIndex(0, GeoInput); + + // Create an empty curve input + UHoudiniPublicAPICurveInput* const CurveInput = Cast(InWrapper->CreateEmptyInput(UHoudiniPublicAPICurveInput::StaticClass())); + // Create the curve input object + UHoudiniPublicAPICurveInputObject* const CurveObject = NewObject(CurveInput); + // Make it a Nurbs curve + CurveObject->SetCurveType(EHoudiniPublicAPICurveType::Nurbs); + // Set the points of the curve, for this example we create a helix consisting of 100 points + TArray CurvePoints; + CurvePoints.Reserve(100); + for (int32 i = 0; i < 100; ++i) + { + const float t = i / 20.0f * PI * 2.0f; + const float x = 100.0f * cos(t); + const float y = 100.0f * sin(t); + const float z = i; + CurvePoints.Emplace(FTransform(FVector(x, y, z))); + } + CurveObject->SetCurvePoints(CurvePoints); + // Set the curve wrapper as an input object + CurveInput->SetInputObjects({CurveObject}); + // Copy the input data to the HDA as node input 1 + InWrapper->SetInputAtIndex(1, CurveInput); + + // Since we are done with setting the initial values, we can unbind from the delegate + InWrapper->GetOnPostInstantiationDelegate().RemoveDynamic(this, &ACurveInputExample::SetInputs); +} + +void ACurveInputExample::PrintOutputs_Implementation(UHoudiniPublicAPIAssetWrapper* InWrapper) +{ + // Print out all outputs generated by the HDA + const int32 NumOutputs = InWrapper->GetNumOutputs(); + UE_LOG(LogTemp, Log, TEXT("NumOutputs: %d"), NumOutputs); + if (NumOutputs > 0) + { + for (int32 OutputIndex = 0; OutputIndex < NumOutputs; ++OutputIndex) + { + TArray Identifiers; + InWrapper->GetOutputIdentifiersAt(OutputIndex, Identifiers); + UE_LOG(LogTemp, Log, TEXT("\toutput index: %d"), OutputIndex); + UE_LOG(LogTemp, Log, TEXT("\toutput type: %d"), InWrapper->GetOutputTypeAt(OutputIndex)); + UE_LOG(LogTemp, Log, TEXT("\tnum_output_objects: %d"), Identifiers.Num()); + if (Identifiers.Num() > 0) + { + for (const FHoudiniPublicAPIOutputObjectIdentifier& Identifier : Identifiers) + { + UObject* const OutputObject = InWrapper->GetOutputObjectAt(OutputIndex, Identifier); + UObject* const OutputComponent = InWrapper->GetOutputComponentAt(OutputIndex, Identifier); + const bool bIsProxy = InWrapper->IsOutputCurrentProxyAt(OutputIndex, Identifier); + UE_LOG(LogTemp, Log, TEXT("\t\tidentifier: %s_%s"), *(Identifier.PartName), *(Identifier.SplitIdentifier)); + UE_LOG(LogTemp, Log, TEXT("\t\toutput_object: %s"), IsValid(OutputObject) ? *(OutputObject->GetFName().ToString()) : TEXT("None")) + UE_LOG(LogTemp, Log, TEXT("\t\toutput_component: %s"), IsValid(OutputComponent) ? *(OutputComponent->GetFName().ToString()) : TEXT("None")) + UE_LOG(LogTemp, Log, TEXT("\t\tis_proxy: %d"), bIsProxy) + UE_LOG(LogTemp, Log, TEXT("")) + } + } + } + } +} + +``` + +`Content/Examples/Cpp/CurveInputExample.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "EditorUtilityActor.h" + +#include "CurveInputExample.generated.h" + +class UHoudiniPublicAPIAssetWrapper; + +UCLASS() +class ACurveInputExample : public AEditorUtilityActor +{ +GENERATED_BODY() + +public: + ACurveInputExample(); + + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, CallInEditor) + void RunCurveInputExample(); + +protected: + /** Set our initial parameter values: disable upvectorstart and set the scale to 0.2. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, CallInEditor) + void SetInitialParameterValues(UHoudiniPublicAPIAssetWrapper* InWrapper); + + /** Configure our inputs: input 0 is a cube and input 1 a helix. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, CallInEditor) + void SetInputs(UHoudiniPublicAPIAssetWrapper* InWrapper); + + /** Print the outputs that were generated by the HDA (after a cook) */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, CallInEditor) + void PrintOutputs(UHoudiniPublicAPIAssetWrapper* InWrapper); + + UPROPERTY(BlueprintReadWrite) + UHoudiniPublicAPIAssetWrapper* AssetWrapper; +}; + +``` + +`Content/Examples/Cpp/IterateOverHoudiniActorsExample.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include "IterateOverHoudiniActorsExample.h" + +#include "EngineUtils.h" + +#include "HoudiniAssetActor.h" +#include "HoudiniPublicAPI.h" +#include "HoudiniPublicAPIBlueprintLib.h" +#include "HoudiniPublicAPIAssetWrapper.h" + + +// Sets default values +AIterateOverHoudiniActorsExample::AIterateOverHoudiniActorsExample() +{ +} + +void AIterateOverHoudiniActorsExample::RunIterateOverHoudiniActorsExample_Implementation() +{ + // Get the API instance + UHoudiniPublicAPI* const API = UHoudiniPublicAPIBlueprintLib::GetAPI(); + // Ensure we have a running Houdini Engine session + if (!API->IsSessionValid()) + API->CreateSession(); + + // Iterate over HoudiniAssetActors in the world (by default when instantiating an HDA in the world an + // AHoudiniAssetActor is spawned, which has a component that manages the instantiated HDA node in Houdini Engine). + UWorld* const OurWorld = GetWorld(); + for (TActorIterator It(OurWorld); It; ++It) + { + AHoudiniAssetActor* const HDAActor = *It; + if (!IsValid(HDAActor)) + continue; + + // Print the name of the actor + UE_LOG(LogTemp, Display, TEXT("HDA Actor (Name, Label): %s, %s"), *(HDAActor->GetName()), *(HDAActor->GetActorLabel())); + + // Wrap it with the API + UHoudiniPublicAPIAssetWrapper* const Wrapper = UHoudiniPublicAPIAssetWrapper::CreateWrapper(this, HDAActor); + if (!IsValid(Wrapper)) + continue; + + // Print its parameters via the API + TMap ParameterTuples; + if (!Wrapper->GetParameterTuples(ParameterTuples)) + { + // The operation failed, log the error + FString ErrorMessage; + if (Wrapper->GetLastErrorMessage(ErrorMessage)) + UE_LOG(LogTemp, Warning, TEXT("%s"), *ErrorMessage); + + continue; + } + + UE_LOG(LogTemp, Display, TEXT("# Parameter Tuples: %d"), ParameterTuples.Num()); + for (const auto& Entry : ParameterTuples) + { + UE_LOG(LogTemp, Display, TEXT("\tParameter Tuple Name: %s"), *(Entry.Key.ToString())); + + const FHoudiniParameterTuple& ParameterTuple = Entry.Value; + TArray Strings; + FString Type; + if (ParameterTuple.BoolValues.Num() > 0) + { + Type = TEXT("Bool"); + for (const bool Value : ParameterTuple.BoolValues) + { + Strings.Add(Value ? TEXT("1") : TEXT("0")); + } + } + else if (ParameterTuple.FloatValues.Num() > 0) + { + Type = TEXT("Float"); + for (const float Value : ParameterTuple.FloatValues) + { + Strings.Add(FString::Printf(TEXT("%.4f"), Value)); + } + } + else if (ParameterTuple.Int32Values.Num() > 0) + { + Type = TEXT("Int32"); + for (const int32 Value : ParameterTuple.Int32Values) + { + Strings.Add(FString::Printf(TEXT("%d"), Value)); + } + } + else if (ParameterTuple.StringValues.Num() > 0) + { + Type = TEXT("String"); + Strings = ParameterTuple.StringValues; + } + + if (Type.Len() == 0) + { + UE_LOG(LogTemp, Display, TEXT("\t\tEmpty")); + } + else + { + UE_LOG(LogTemp, Display, TEXT("\t\t%s Values: %s"), *Type, *FString::Join(Strings, TEXT("; "))); + } + } + } +} + +``` + +`Content/Examples/Cpp/IterateOverHoudiniActorsExample.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "EditorUtilityActor.h" + +#include "IterateOverHoudiniActorsExample.generated.h" + +UCLASS() +class AIterateOverHoudiniActorsExample : public AEditorUtilityActor +{ + GENERATED_BODY() + +public: + // Sets default values for this actor's properties + AIterateOverHoudiniActorsExample(); + + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, CallInEditor) + void RunIterateOverHoudiniActorsExample(); +}; + +``` + +`Content/Examples/Python/asset_input_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" An example script that uses the API to instantiate two HDAs. The first HDA +will be used as an input to the second HDA. For the second HDA we set 2 inputs: +an asset input (the first instantiated HDA) and a curve input (a helix). The +inputs are set during post instantiation (before the first cook). After the +first cook and output creation (post processing) the input structure is fetched +and logged. + +""" +import math + +import unreal + +_g_wrapper1 = None +_g_wrapper2 = None + + +def get_copy_curve_hda_path(): + return '/HoudiniEngine/Examples/hda/copy_to_curve_1_0.copy_to_curve_1_0' + + +def get_copy_curve_hda(): + return unreal.load_object(None, get_copy_curve_hda_path()) + + +def get_pig_head_hda_path(): + return '/HoudiniEngine/Examples/hda/pig_head_subdivider_v01.pig_head_subdivider_v01' + + +def get_pig_head_hda(): + return unreal.load_object(None, get_pig_head_hda_path()) + + +def configure_inputs(in_wrapper): + print('configure_inputs') + + # Unbind from the delegate + in_wrapper.on_post_instantiation_delegate.remove_callable(configure_inputs) + + # Create a geo input + asset_input = in_wrapper.create_empty_input(unreal.HoudiniPublicAPIAssetInput) + # Set the input objects/assets for this input + # asset_input.set_input_objects((_g_wrapper1.get_houdini_asset_actor().houdini_asset_component, )) + asset_input.set_input_objects((_g_wrapper1, )) + # copy the input data to the HDA as node input 0 + in_wrapper.set_input_at_index(0, asset_input) + # We can now discard the API input object + asset_input = None + + # Create a curve input + curve_input = in_wrapper.create_empty_input(unreal.HoudiniPublicAPICurveInput) + # Create a curve wrapper/helper + curve_object = unreal.HoudiniPublicAPICurveInputObject(curve_input) + # Make it a Nurbs curve + curve_object.set_curve_type(unreal.HoudiniPublicAPICurveType.NURBS) + # Set the points of the curve, for this example we create a helix + # consisting of 100 points + curve_points = [] + for i in range(10): + t = i / 10.0 * math.pi * 2.0 + x = 100.0 * math.cos(t) + y = 100.0 * math.sin(t) + z = i * 10.0 + curve_points.append(unreal.Transform([x, y, z], [0, 0, 0], [1, 1, 1])) + curve_object.set_curve_points(curve_points) + # Set the curve wrapper as an input object + curve_input.set_input_objects((curve_object, )) + # Copy the input data to the HDA as node input 1 + in_wrapper.set_input_at_index(1, curve_input) + # We can now discard the API input object + curve_input = None + + +def print_api_input(in_input): + print('\t\tInput type: {0}'.format(in_input.__class__)) + print('\t\tbKeepWorldTransform: {0}'.format(in_input.keep_world_transform)) + print('\t\tbImportAsReference: {0}'.format(in_input.import_as_reference)) + if isinstance(in_input, unreal.HoudiniPublicAPICurveInput): + print('\t\tbCookOnCurveChanged: {0}'.format(in_input.cook_on_curve_changed)) + print('\t\tbAddRotAndScaleAttributesOnCurves: {0}'.format(in_input.add_rot_and_scale_attributes_on_curves)) + + input_objects = in_input.get_input_objects() + if not input_objects: + print('\t\tEmpty input!') + else: + print('\t\tNumber of objects in input: {0}'.format(len(input_objects))) + for idx, input_object in enumerate(input_objects): + print('\t\t\tInput object #{0}: {1}'.format(idx, input_object)) + if isinstance(input_object, unreal.HoudiniPublicAPICurveInputObject): + print('\t\t\tbClosed: {0}'.format(input_object.is_closed())) + print('\t\t\tCurveMethod: {0}'.format(input_object.get_curve_method())) + print('\t\t\tCurveType: {0}'.format(input_object.get_curve_type())) + print('\t\t\tReversed: {0}'.format(input_object.is_reversed())) + print('\t\t\tCurvePoints: {0}'.format(input_object.get_curve_points())) + + +def print_inputs(in_wrapper): + print('print_inputs') + + # Unbind from the delegate + in_wrapper.on_post_processing_delegate.remove_callable(print_inputs) + + # Fetch inputs, iterate over it and log + node_inputs = in_wrapper.get_inputs_at_indices() + parm_inputs = in_wrapper.get_input_parameters() + + if not node_inputs: + print('No node inputs found!') + else: + print('Number of node inputs: {0}'.format(len(node_inputs))) + for input_index, input_wrapper in node_inputs.items(): + print('\tInput index: {0}'.format(input_index)) + print_api_input(input_wrapper) + + if not parm_inputs: + print('No parameter inputs found!') + else: + print('Number of parameter inputs: {0}'.format(len(parm_inputs))) + for parm_name, input_wrapper in parm_inputs.items(): + print('\tInput parameter name: {0}'.format(parm_name)) + print_api_input(input_wrapper) + + +def run(): + # get the API singleton + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + + global _g_wrapper1, _g_wrapper2 + # instantiate the input HDA with auto-cook enabled + _g_wrapper1 = api.instantiate_asset(get_pig_head_hda(), unreal.Transform()) + + # instantiate the copy curve HDA + _g_wrapper2 = api.instantiate_asset(get_copy_curve_hda(), unreal.Transform()) + + # Configure inputs on_post_instantiation, after instantiation, but before first cook + _g_wrapper2.on_post_instantiation_delegate.add_callable(configure_inputs) + # Print the input state after the cook and output creation. + _g_wrapper2.on_post_processing_delegate.add_callable(print_inputs) + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/bake_all_outputs_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unreal + +""" Example script for instantiating an asset, cooking it and baking all of +its outputs. + +""" + +_g_wrapper = None + + +def get_test_hda_path(): + return '/HoudiniEngine/Examples/hda/pig_head_subdivider_v01.pig_head_subdivider_v01' + + +def get_test_hda(): + return unreal.load_object(None, get_test_hda_path()) + + +def on_post_instantiation(in_wrapper): + print('on_post_instantiation') + # in_wrapper.on_post_instantiation_state_exited_delegate_delegate.remove_callable(on_post_instantiation) + + # Set parameter values for the next cook + # in_wrapper.set_bool_parameter_value('add_instances', True) + # in_wrapper.set_int_parameter_value('num_instances', 8) + in_wrapper.set_parameter_tuples({ + 'add_instances': unreal.HoudiniParameterTuple(bool_values=(True, )), + 'num_instances': unreal.HoudiniParameterTuple(int32_values=(8, )), + }) + + # Print all parameter values + param_tuples = in_wrapper.get_parameter_tuples() + print('parameter tuples: {}'.format(len(param_tuples) if param_tuples else 0)) + if param_tuples: + for param_tuple_name, param_tuple in param_tuples.items(): + print('parameter tuple name: {}'.format(param_tuple_name)) + print('\tbool_values: {}'.format(param_tuple.bool_values)) + print('\tfloat_values: {}'.format(param_tuple.float_values)) + print('\tint32_values: {}'.format(param_tuple.int32_values)) + print('\tstring_values: {}'.format(param_tuple.string_values)) + + # Force a cook/recook + in_wrapper.recook() + + +def on_post_bake(in_wrapper, success): + in_wrapper.on_post_bake_delegate.remove_callable(on_post_bake) + print('bake complete ... {}'.format('success' if success else 'failed')) + + # Delete the hda after the bake + in_wrapper.delete_instantiated_asset() + global _g_wrapper + _g_wrapper = None + + +def on_post_process(in_wrapper): + print('on_post_process') + + # in_wrapper.on_post_processing_delegate.remove_callable(on_post_process) + + # Print out all outputs generated by the HDA + num_outputs = in_wrapper.get_num_outputs() + print('num_outputs: {}'.format(num_outputs)) + if num_outputs > 0: + for output_idx in range(num_outputs): + identifiers = in_wrapper.get_output_identifiers_at(output_idx) + print('\toutput index: {}'.format(output_idx)) + print('\toutput type: {}'.format(in_wrapper.get_output_type_at(output_idx))) + print('\tnum_output_objects: {}'.format(len(identifiers))) + if identifiers: + for identifier in identifiers: + output_object = in_wrapper.get_output_object_at(output_idx, identifier) + output_component = in_wrapper.get_output_component_at(output_idx, identifier) + is_proxy = in_wrapper.is_output_current_proxy_at(output_idx, identifier) + print('\t\tidentifier: {}'.format(identifier)) + print('\t\toutput_object: {}'.format(output_object.get_name() if output_object else 'None')) + print('\t\toutput_component: {}'.format(output_component.get_name() if output_component else 'None')) + print('\t\tis_proxy: {}'.format(is_proxy)) + print('') + + # bind to the post bake delegate + in_wrapper.on_post_bake_delegate.add_callable(on_post_bake) + + # Bake all outputs to actors + print('baking all outputs to actors') + in_wrapper.bake_all_outputs_with_settings( + unreal.HoudiniEngineBakeOption.TO_ACTOR, + replace_previous_bake=False, + remove_temp_outputs_on_success=False) + + +def run(): + # get the API singleton + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + + global _g_wrapper + + # instantiate an asset, disabling auto-cook of the asset (so we have to + # call wrapper.reCook() to cook it) + _g_wrapper = api.instantiate_asset(get_test_hda(), unreal.Transform(), enable_auto_cook=False) + + # Bind to the on post instantiation delegate (before the first cook) + _g_wrapper.on_post_instantiation_delegate.add_callable(on_post_instantiation) + # Bind to the on post processing delegate (after a cook and after all + # outputs have been generated in Unreal) + _g_wrapper.on_post_processing_delegate.add_callable(on_post_process) + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/bake_output_object_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unreal + +""" Example script for instantiating an asset, cooking it and baking an +individual output object. + +""" + +_g_wrapper = None + + +def get_test_hda_path(): + return '/HoudiniEngine/Examples/hda/pig_head_subdivider_v01.pig_head_subdivider_v01' + + +def get_test_hda(): + return unreal.load_object(None, get_test_hda_path()) + + +def on_post_process(in_wrapper): + print('on_post_process') + # Print details about the outputs and record the first static mesh we find + + sm_index = None + sm_identifier = None + + # in_wrapper.on_post_processing_delegate.remove_callable(on_post_process) + + num_outputs = in_wrapper.get_num_outputs() + print('num_outputs: {}'.format(num_outputs)) + if num_outputs > 0: + for output_idx in range(num_outputs): + identifiers = in_wrapper.get_output_identifiers_at(output_idx) + output_type = in_wrapper.get_output_type_at(output_idx) + print('\toutput index: {}'.format(output_idx)) + print('\toutput type: {}'.format(output_type)) + print('\tnum_output_objects: {}'.format(len(identifiers))) + if identifiers: + for identifier in identifiers: + output_object = in_wrapper.get_output_object_at(output_idx, identifier) + output_component = in_wrapper.get_output_component_at(output_idx, identifier) + is_proxy = in_wrapper.is_output_current_proxy_at(output_idx, identifier) + print('\t\tidentifier: {}'.format(identifier)) + print('\t\toutput_object: {}'.format(output_object.get_name() if output_object else 'None')) + print('\t\toutput_component: {}'.format(output_component.get_name() if output_component else 'None')) + print('\t\tis_proxy: {}'.format(is_proxy)) + print('') + + if (output_type == unreal.HoudiniOutputType.MESH and + isinstance(output_object, unreal.StaticMesh)): + sm_index = output_idx + sm_identifier = identifier + + # Bake the first static mesh we found to the CB + if sm_index is not None and sm_identifier is not None: + print('baking {}'.format(sm_identifier)) + success = in_wrapper.bake_output_object_at(sm_index, sm_identifier) + print('success' if success else 'failed') + + # Delete the instantiated asset + in_wrapper.delete_instantiated_asset() + global _g_wrapper + _g_wrapper = None + + +def run(): + # get the API singleton + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + + global _g_wrapper + # instantiate an asset with auto-cook enabled + _g_wrapper = api.instantiate_asset(get_test_hda(), unreal.Transform()) + + # Bind to the on post processing delegate (after a cook and after all + # outputs have been generated in Unreal) + _g_wrapper.on_post_processing_delegate.add_callable(on_post_process) + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/curve_input_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" An example script that uses the API to instantiate an HDA and then +set 2 inputs: a geometry input (a cube) and a curve input (a helix). The +inputs are set during post instantiation (before the first cook). After the +first cook and output creation (post processing) the input structure is fetched +and logged. + +""" +import math + +import unreal + +_g_wrapper = None + + +def get_test_hda_path(): + return '/HoudiniEngine/Examples/hda/copy_to_curve_1_0.copy_to_curve_1_0' + + +def get_test_hda(): + return unreal.load_object(None, get_test_hda_path()) + + +def get_geo_asset_path(): + return '/Engine/BasicShapes/Cube.Cube' + + +def get_geo_asset(): + return unreal.load_object(None, get_geo_asset_path()) + + +def configure_inputs(in_wrapper): + print('configure_inputs') + + # Unbind from the delegate + in_wrapper.on_post_instantiation_delegate.remove_callable(configure_inputs) + + # Create a geo input + geo_input = in_wrapper.create_empty_input(unreal.HoudiniPublicAPIGeoInput) + # Set the input objects/assets for this input + geo_object = get_geo_asset() + if not geo_input.set_input_objects((geo_object, )): + # If any errors occurred, get the last error message + print('Error on geo_input: {0}'.format(geo_input.get_last_error_message())) + # copy the input data to the HDA as node input 0 + in_wrapper.set_input_at_index(0, geo_input) + # We can now discard the API input object + geo_input = None + + # Create a curve input + curve_input = in_wrapper.create_empty_input(unreal.HoudiniPublicAPICurveInput) + # Create a curve wrapper/helper + curve_object = unreal.HoudiniPublicAPICurveInputObject(curve_input) + # Make it a Nurbs curve + curve_object.set_curve_type(unreal.HoudiniPublicAPICurveType.NURBS) + # Set the points of the curve, for this example we create a helix + # consisting of 100 points + curve_points = [] + for i in range(100): + t = i / 20.0 * math.pi * 2.0 + x = 100.0 * math.cos(t) + y = 100.0 * math.sin(t) + z = i + curve_points.append(unreal.Transform([x, y, z], [0, 0, 0], [1, 1, 1])) + curve_object.set_curve_points(curve_points) + # Error handling/message example: try to set geo_object on curve input + if not curve_input.set_input_objects((geo_object, )): + print('Error (example) while setting \'{0}\' on curve input: {1}'.format( + geo_object.get_name(), curve_input.get_last_error_message() + )) + # Set the curve wrapper as an input object + curve_input.set_input_objects((curve_object, )) + # Copy the input data to the HDA as node input 1 + in_wrapper.set_input_at_index(1, curve_input) + # We can now discard the API input object + curve_input = None + + # Check for errors on the wrapper + last_error = in_wrapper.get_last_error_message() + if last_error: + print('Error on wrapper during input configuration: {0}'.format(last_error)) + + +def print_api_input(in_input): + print('\t\tInput type: {0}'.format(in_input.__class__)) + print('\t\tbKeepWorldTransform: {0}'.format(in_input.keep_world_transform)) + print('\t\tbImportAsReference: {0}'.format(in_input.import_as_reference)) + if isinstance(in_input, unreal.HoudiniPublicAPIGeoInput): + print('\t\tbPackBeforeMerge: {0}'.format(in_input.pack_before_merge)) + print('\t\tbExportLODs: {0}'.format(in_input.export_lo_ds)) + print('\t\tbExportSockets: {0}'.format(in_input.export_sockets)) + print('\t\tbExportColliders: {0}'.format(in_input.export_colliders)) + elif isinstance(in_input, unreal.HoudiniPublicAPICurveInput): + print('\t\tbCookOnCurveChanged: {0}'.format(in_input.cook_on_curve_changed)) + print('\t\tbAddRotAndScaleAttributesOnCurves: {0}'.format(in_input.add_rot_and_scale_attributes_on_curves)) + + input_objects = in_input.get_input_objects() + if not input_objects: + print('\t\tEmpty input!') + else: + print('\t\tNumber of objects in input: {0}'.format(len(input_objects))) + for idx, input_object in enumerate(input_objects): + print('\t\t\tInput object #{0}: {1}'.format(idx, input_object)) + if isinstance(input_object, unreal.HoudiniPublicAPICurveInputObject): + print('\t\t\tbClosed: {0}'.format(input_object.is_closed())) + print('\t\t\tCurveMethod: {0}'.format(input_object.get_curve_method())) + print('\t\t\tCurveType: {0}'.format(input_object.get_curve_type())) + print('\t\t\tReversed: {0}'.format(input_object.is_reversed())) + print('\t\t\tCurvePoints: {0}'.format(input_object.get_curve_points())) + + +def print_inputs(in_wrapper): + print('print_inputs') + + # Unbind from the delegate + in_wrapper.on_post_processing_delegate.remove_callable(print_inputs) + + # Fetch inputs, iterate over it and log + node_inputs = in_wrapper.get_inputs_at_indices() + parm_inputs = in_wrapper.get_input_parameters() + + if not node_inputs: + print('No node inputs found!') + else: + print('Number of node inputs: {0}'.format(len(node_inputs))) + for input_index, input_wrapper in node_inputs.items(): + print('\tInput index: {0}'.format(input_index)) + print_api_input(input_wrapper) + + if not parm_inputs: + print('No parameter inputs found!') + else: + print('Number of parameter inputs: {0}'.format(len(parm_inputs))) + for parm_name, input_wrapper in parm_inputs.items(): + print('\tInput parameter name: {0}'.format(parm_name)) + print_api_input(input_wrapper) + + +def run(): + # get the API singleton + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + + global _g_wrapper + # instantiate an asset with auto-cook enabled + _g_wrapper = api.instantiate_asset(get_test_hda(), unreal.Transform()) + + # Configure inputs on_post_instantiation, after instantiation, but before first cook + _g_wrapper.on_post_instantiation_delegate.add_callable(configure_inputs) + # Print the input state after the cook and output creation. + _g_wrapper.on_post_processing_delegate.add_callable(print_inputs) + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/eau_curve_input_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import math + +import unreal + +@unreal.uclass() +class CurveInputExample(unreal.PlacedEditorUtilityBase): + # Use a FProperty to hold the reference to the API wrapper we create in + # run_curve_input_example + _asset_wrapper = unreal.uproperty(unreal.HoudiniPublicAPIAssetWrapper) + + @unreal.ufunction(meta=dict(BlueprintCallable=True, CallInEditor=True)) + def run_curve_input_example(self): + # Get the API instance + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + # Ensure we have a running session + if not api.is_session_valid(): + api.create_session() + # Load our HDA uasset + example_hda = unreal.load_object(None, '/HoudiniEngine/Examples/hda/copy_to_curve_1_0.copy_to_curve_1_0') + # Create an API wrapper instance for instantiating the HDA and interacting with it + wrapper = api.instantiate_asset(example_hda, instantiate_at=unreal.Transform()) + if wrapper: + # Pre-instantiation is the earliest point where we can set parameter values + wrapper.on_pre_instantiation_delegate.add_function(self, '_set_initial_parameter_values') + # Jumping ahead a bit: we also want to configure inputs, but inputs are only available after instantiation + wrapper.on_post_instantiation_delegate.add_function(self, '_set_inputs') + # Jumping ahead a bit: we also want to print the outputs after the node has cook and the plug-in has processed the output + wrapper.on_post_processing_delegate.add_function(self, '_print_outputs') + self.set_editor_property('_asset_wrapper', wrapper) + + @unreal.ufunction(params=[unreal.HoudiniPublicAPIAssetWrapper], meta=dict(CallInEditor=True)) + def _set_initial_parameter_values(self, in_wrapper): + """ Set our initial parameter values: disable upvectorstart and set the scale to 0.2. """ + # Uncheck the upvectoratstart parameter + in_wrapper.set_bool_parameter_value('upvectoratstart', False) + + # Set the scale to 0.2 + in_wrapper.set_float_parameter_value('scale', 0.2) + + # Since we are done with setting the initial values, we can unbind from the delegate + in_wrapper.on_pre_instantiation_delegate.remove_function(self, '_set_initial_parameter_values') + + @unreal.ufunction(params=[unreal.HoudiniPublicAPIAssetWrapper], meta=dict(CallInEditor=True)) + def _set_inputs(self, in_wrapper): + """ Configure our inputs: input 0 is a cube and input 1 a helix. """ + # Create an empty geometry input + geo_input = in_wrapper.create_empty_input(unreal.HoudiniPublicAPIGeoInput) + # Load the cube static mesh asset + cube = unreal.load_object(None, '/Engine/BasicShapes/Cube.Cube') + # Set the input object array for our geometry input, in this case containing only the cube + geo_input.set_input_objects((cube, )) + + # Set the input on the instantiated HDA via the wrapper + in_wrapper.set_input_at_index(0, geo_input) + + # Create a curve input + curve_input = in_wrapper.create_empty_input(unreal.HoudiniPublicAPICurveInput) + # Create a curve wrapper/helper + curve_object = unreal.HoudiniPublicAPICurveInputObject(curve_input) + # Make it a Nurbs curve + curve_object.set_curve_type(unreal.HoudiniPublicAPICurveType.NURBS) + # Set the points of the curve, for this example we create a helix + # consisting of 100 points + curve_points = [] + for i in range(100): + t = i / 20.0 * math.pi * 2.0 + x = 100.0 * math.cos(t) + y = 100.0 * math.sin(t) + z = i + curve_points.append(unreal.Transform([x, y, z], [0, 0, 0], [1, 1, 1])) + curve_object.set_curve_points(curve_points) + # Set the curve wrapper as an input object + curve_input.set_input_objects((curve_object, )) + # Copy the input data to the HDA as node input 1 + in_wrapper.set_input_at_index(1, curve_input) + + # unbind from the delegate, since we are done with setting inputs + in_wrapper.on_post_instantiation_delegate.remove_function(self, '_set_inputs') + + @unreal.ufunction(params=[unreal.HoudiniPublicAPIAssetWrapper], meta=dict(CallInEditor=True)) + def _print_outputs(self, in_wrapper): + """ Print the outputs that were generated by the HDA (after a cook) """ + num_outputs = in_wrapper.get_num_outputs() + print('num_outputs: {}'.format(num_outputs)) + if num_outputs > 0: + for output_idx in range(num_outputs): + identifiers = in_wrapper.get_output_identifiers_at(output_idx) + print('\toutput index: {}'.format(output_idx)) + print('\toutput type: {}'.format(in_wrapper.get_output_type_at(output_idx))) + print('\tnum_output_objects: {}'.format(len(identifiers))) + if identifiers: + for identifier in identifiers: + output_object = in_wrapper.get_output_object_at(output_idx, identifier) + output_component = in_wrapper.get_output_component_at(output_idx, identifier) + is_proxy = in_wrapper.is_output_current_proxy_at(output_idx, identifier) + print('\t\tidentifier: {}'.format(identifier)) + print('\t\toutput_object: {}'.format(output_object.get_name() if output_object else 'None')) + print('\t\toutput_component: {}'.format(output_component.get_name() if output_component else 'None')) + print('\t\tis_proxy: {}'.format(is_proxy)) + print('') + + +def run(): + # Spawn CurveInputExample and call run_curve_input_example + curve_input_example_actor = unreal.EditorLevelLibrary.spawn_actor_from_class(CurveInputExample.static_class(), unreal.Vector.ZERO, unreal.Rotator()) + curve_input_example_actor.run_curve_input_example() + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/geo_input_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" An example script that uses the API to instantiate an HDA and then +set 2 geometry inputs: one node input and one object path parameter +input. The inputs are set during post instantiation (before the first +cook). After the first cook and output creation (post processing) the +input structure is fetched and logged. + +""" + +import unreal + +_g_wrapper = None + + +def get_test_hda_path(): + return '/HoudiniEngine/Examples/hda/subnet_test_2_0.subnet_test_2_0' + + +def get_test_hda(): + return unreal.load_object(None, get_test_hda_path()) + + +def get_geo_asset_path(): + return '/Engine/BasicShapes/Cube.Cube' + + +def get_geo_asset(): + return unreal.load_object(None, get_geo_asset_path()) + + +def get_cylinder_asset_path(): + return '/Engine/BasicShapes/Cylinder.Cylinder' + + +def get_cylinder_asset(): + return unreal.load_object(None, get_cylinder_asset_path()) + + +def configure_inputs(in_wrapper): + print('configure_inputs') + + # Unbind from the delegate + in_wrapper.on_post_instantiation_delegate.remove_callable(configure_inputs) + + # Deprecated input functions + # in_wrapper.set_input_type(0, unreal.HoudiniInputType.GEOMETRY) + # in_wrapper.set_input_objects(0, (get_geo_asset(), )) + + # in_wrapper.set_input_type(1, unreal.HoudiniInputType.GEOMETRY) + # in_wrapper.set_input_objects(1, (get_geo_asset(), )) + # in_wrapper.set_input_import_as_reference(1, True) + + # Create a geometry input + geo_input = in_wrapper.create_empty_input(unreal.HoudiniPublicAPIGeoInput) + # Set the input objects/assets for this input + geo_asset = get_geo_asset() + geo_input.set_input_objects((geo_asset, )) + # Set the transform of the input geo + geo_input.set_input_object_transform_offset( + 0, + unreal.Transform( + (200, 0, 100), + (45, 0, 45), + (2, 2, 2), + ) + ) + # copy the input data to the HDA as node input 0 + in_wrapper.set_input_at_index(0, geo_input) + # We can now discard the API input object + geo_input = None + + # Create a another geometry input + geo_input = in_wrapper.create_empty_input(unreal.HoudiniPublicAPIGeoInput) + # Set the input objects/assets for this input (cylinder in this case) + geo_asset = get_cylinder_asset() + geo_input.set_input_objects((geo_asset, )) + # Set the transform of the input geo + geo_input.set_input_object_transform_offset( + 0, + unreal.Transform( + (-200, 0, 0), + (0, 0, 0), + (2, 2, 2), + ) + ) + # copy the input data to the HDA as input parameter 'objpath1' + in_wrapper.set_input_parameter('objpath1', geo_input) + # We can now discard the API input object + geo_input = None + + # Set the subnet_test HDA to output its first input + in_wrapper.set_int_parameter_value('enable_geo', 1) + + +def print_api_input(in_input): + print('\t\tInput type: {0}'.format(in_input.__class__)) + print('\t\tbKeepWorldTransform: {0}'.format(in_input.keep_world_transform)) + print('\t\tbImportAsReference: {0}'.format(in_input.import_as_reference)) + if isinstance(in_input, unreal.HoudiniPublicAPIGeoInput): + print('\t\tbPackBeforeMerge: {0}'.format(in_input.pack_before_merge)) + print('\t\tbExportLODs: {0}'.format(in_input.export_lo_ds)) + print('\t\tbExportSockets: {0}'.format(in_input.export_sockets)) + print('\t\tbExportColliders: {0}'.format(in_input.export_colliders)) + + input_objects = in_input.get_input_objects() + if not input_objects: + print('\t\tEmpty input!') + else: + print('\t\tNumber of objects in input: {0}'.format(len(input_objects))) + for idx, input_object in enumerate(input_objects): + print('\t\t\tInput object #{0}: {1}'.format(idx, input_object)) + if hasattr(in_input, 'get_input_object_transform_offset'): + print('\t\t\tObject Transform Offset: {0}'.format(in_input.get_input_object_transform_offset(idx))) + + +def print_inputs(in_wrapper): + print('print_inputs') + + # Unbind from the delegate + in_wrapper.on_post_processing_delegate.remove_callable(print_inputs) + + # Fetch inputs, iterate over it and log + node_inputs = in_wrapper.get_inputs_at_indices() + parm_inputs = in_wrapper.get_input_parameters() + + if not node_inputs: + print('No node inputs found!') + else: + print('Number of node inputs: {0}'.format(len(node_inputs))) + for input_index, input_wrapper in node_inputs.items(): + print('\tInput index: {0}'.format(input_index)) + print_api_input(input_wrapper) + + if not parm_inputs: + print('No parameter inputs found!') + else: + print('Number of parameter inputs: {0}'.format(len(parm_inputs))) + for parm_name, input_wrapper in parm_inputs.items(): + print('\tInput parameter name: {0}'.format(parm_name)) + print_api_input(input_wrapper) + + +def run(): + # get the API singleton + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + + global _g_wrapper + # instantiate an asset with auto-cook enabled + _g_wrapper = api.instantiate_asset(get_test_hda(), unreal.Transform()) + + # Configure inputs on_post_instantiation, after instantiation, but before first cook + _g_wrapper.on_post_instantiation_delegate.add_callable(configure_inputs) + # Bind on_post_processing, after cook + output creation + _g_wrapper.on_post_processing_delegate.add_callable(print_inputs) + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/instances_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" Example script that instantiates an HDA using the API and then +setting some parameter values after instantiation but before the +first cook. + +""" + +import unreal + +_g_wrapper = None + + +def get_test_hda_path(): + return '/HoudiniEngine/Examples/hda/pig_head_subdivider_v01.pig_head_subdivider_v01' + + +def get_test_hda(): + return unreal.load_object(None, get_test_hda_path()) + + +def on_post_instantiation(in_wrapper): + print('on_post_instantiation') + # in_wrapper.on_post_instantiation_delegate.remove_callable(on_post_instantiation) + + # Set some parameters to create instances and enable a material + in_wrapper.set_bool_parameter_value('add_instances', True) + in_wrapper.set_int_parameter_value('num_instances', 8) + in_wrapper.set_bool_parameter_value('addshader', True) + + +def run(): + # get the API singleton + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + + global _g_wrapper + # instantiate an asset with auto-cook enabled + _g_wrapper = api.instantiate_asset(get_test_hda(), unreal.Transform()) + + # Bind on_post_instantiation (before the first cook) callback to set parameters + _g_wrapper.on_post_instantiation_delegate.add_callable(on_post_instantiation) + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/iterate_over_houdini_asset_actors_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +""" Example script that uses unreal.ActorIterator to iterate over all +HoudiniAssetActors in the editor world, creates API wrappers for them, and +logs all of their parameter tuples. + +""" + +import unreal + + +def run(): + # Get the API instance + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + + # Get the editor world + editor_subsystem = None + world = None + try: + # In UE5 unreal.EditorLevelLibrary.get_editor_world is deprecated and + # unreal.UnrealEditorSubsystem.get_editor_world is the replacement, + # but unreal.UnrealEditorSubsystem does not exist in UE4 + editor_subsystem = unreal.UnrealEditorSubsystem() + except AttributeError: + world = unreal.EditorLevelLibrary.get_editor_world() + else: + world = editor_subsystem.get_editor_world() + + # Iterate over all Houdini Asset Actors in the editor world + for houdini_actor in unreal.ActorIterator(world, unreal.HoudiniAssetActor): + if not houdini_actor: + continue + + # Print the name and label of the actor + actor_name = houdini_actor.get_name() + actor_label = houdini_actor.get_actor_label() + print(f'HDA Actor (Name, Label): {actor_name}, {actor_label}') + + # Wrap the Houdini asset actor with the API + wrapper = unreal.HoudiniPublicAPIAssetWrapper.create_wrapper(world, houdini_actor) + if not wrapper: + continue + + # Get all parameter tuples of the HDA + parameter_tuples = wrapper.get_parameter_tuples() + if parameter_tuples is None: + # The operation failed, log the error message + error_message = wrapper.get_last_error_message() + if error_message is not None: + print(error_message) + + continue + + print(f'# Parameter Tuples: {len(parameter_tuples)}') + for name, data in parameter_tuples.items(): + print(f'\tParameter Tuple Name: {name}') + + type_name = None + values = None + if data.bool_values: + type_name = 'Bool' + values = '; '.join(('1' if v else '0' for v in data.bool_values)) + elif data.float_values: + type_name = 'Float' + values = '; '.join((f'{v:.4f}' for v in data.float_values)) + elif data.int32_values: + type_name = 'Int32' + values = '; '.join((f'{v:d}' for v in data.int32_values)) + elif data.string_values: + type_name = 'String' + values = '; '.join(data.string_values) + + if not type_name: + print('\t\tEmpty') + else: + print(f'\t\t{type_name} Values: {values}') + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/landscape_input_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" Example script using the API to instantiate an HDA and set a landscape +input. + +""" + +import unreal + +_g_wrapper = None + + +def get_test_hda_path(): + return '/HoudiniEngine/Examples/hda/hilly_landscape_erode_1_0.hilly_landscape_erode_1_0' + + +def get_test_hda(): + return unreal.load_object(None, get_test_hda_path()) + + +def load_map(): + return unreal.EditorLoadingAndSavingUtils.load_map('/HoudiniEngine/Examples/Maps/LandscapeInputExample.LandscapeInputExample') + +def get_landscape(): + return unreal.EditorLevelLibrary.get_actor_reference('PersistentLevel.Landscape_0') + +def configure_inputs(in_wrapper): + print('configure_inputs') + + # Unbind from the delegate + in_wrapper.on_post_instantiation_delegate.remove_callable(configure_inputs) + + # Create a landscape input + landscape_input = in_wrapper.create_empty_input(unreal.HoudiniPublicAPILandscapeInput) + # Send landscapes as heightfields + landscape_input.landscape_export_type = unreal.HoudiniLandscapeExportType.HEIGHTFIELD + # Keep world transform + landscape_input.keep_world_transform = True + # Set the input objects/assets for this input + landscape_object = get_landscape() + landscape_input.set_input_objects((landscape_object, )) + # copy the input data to the HDA as node input 0 + in_wrapper.set_input_at_index(0, landscape_input) + # We can now discard the API input object + landscape_input = None + + +def print_api_input(in_input): + print('\t\tInput type: {0}'.format(in_input.__class__)) + print('\t\tbKeepWorldTransform: {0}'.format(in_input.keep_world_transform)) + if isinstance(in_input, unreal.HoudiniPublicAPILandscapeInput): + print('\t\tbLandscapeAutoSelectComponent: {0}'.format(in_input.landscape_auto_select_component)) + print('\t\tbLandscapeExportLighting: {0}'.format(in_input.landscape_export_lighting)) + print('\t\tbLandscapeExportMaterials: {0}'.format(in_input.landscape_export_materials)) + print('\t\tbLandscapeExportNormalizedUVs: {0}'.format(in_input.landscape_export_normalized_u_vs)) + print('\t\tbLandscapeExportSelectionOnly: {0}'.format(in_input.landscape_export_selection_only)) + print('\t\tbLandscapeExportTileUVs: {0}'.format(in_input.landscape_export_tile_u_vs)) + print('\t\tbLandscapeExportType: {0}'.format(in_input.landscape_export_type)) + + input_objects = in_input.get_input_objects() + if not input_objects: + print('\t\tEmpty input!') + else: + print('\t\tNumber of objects in input: {0}'.format(len(input_objects))) + for idx, input_object in enumerate(input_objects): + print('\t\t\tInput object #{0}: {1}'.format(idx, input_object)) + + +def print_inputs(in_wrapper): + print('print_inputs') + + # Unbind from the delegate + in_wrapper.on_post_processing_delegate.remove_callable(print_inputs) + + # Fetch inputs, iterate over it and log + node_inputs = in_wrapper.get_inputs_at_indices() + parm_inputs = in_wrapper.get_input_parameters() + + if not node_inputs: + print('No node inputs found!') + else: + print('Number of node inputs: {0}'.format(len(node_inputs))) + for input_index, input_wrapper in node_inputs.items(): + print('\tInput index: {0}'.format(input_index)) + print_api_input(input_wrapper) + + if not parm_inputs: + print('No parameter inputs found!') + else: + print('Number of parameter inputs: {0}'.format(len(parm_inputs))) + for parm_name, input_wrapper in parm_inputs.items(): + print('\tInput parameter name: {0}'.format(parm_name)) + print_api_input(input_wrapper) + + +def run(): + # Load the example map + load_map() + + # get the API singleton + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + + global _g_wrapper + # instantiate an asset with auto-cook enabled + _g_wrapper = api.instantiate_asset(get_test_hda(), unreal.Transform()) + + # Configure inputs on_post_instantiation, after instantiation, but before first cook + _g_wrapper.on_post_instantiation_delegate.add_callable(configure_inputs) + # Print the input state after the cook and output creation. + _g_wrapper.on_post_processing_delegate.add_callable(print_inputs) + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/outputs_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unreal + +""" Example script for instantiating an asset, setting parameters, cooking it +and iterating over and logging all of its output objects. + +""" + +_g_wrapper = None + + +def get_test_hda_path(): + return '/HoudiniEngine/Examples/hda/pig_head_subdivider_v01.pig_head_subdivider_v01' + + +def get_test_hda(): + return unreal.load_object(None, get_test_hda_path()) + + +def on_post_instantiation(in_wrapper): + print('on_post_instantiation') + # in_wrapper.on_post_instantiation_state_exited_delegate_delegate.remove_callable(on_post_instantiation) + + # Set parameter values for the next cook + # in_wrapper.set_bool_parameter_value('add_instances', True) + # in_wrapper.set_int_parameter_value('num_instances', 8) + in_wrapper.set_parameter_tuples({ + 'add_instances': unreal.HoudiniParameterTuple(bool_values=(True, )), + 'num_instances': unreal.HoudiniParameterTuple(int32_values=(8, )), + }) + + # Print all parameter values + param_tuples = in_wrapper.get_parameter_tuples() + print('parameter tuples: {}'.format(len(param_tuples) if param_tuples else 0)) + if param_tuples: + for param_tuple_name, param_tuple in param_tuples.items(): + print('parameter tuple name: {}'.format(param_tuple_name)) + print('\tbool_values: {}'.format(param_tuple.bool_values)) + print('\tfloat_values: {}'.format(param_tuple.float_values)) + print('\tint32_values: {}'.format(param_tuple.int32_values)) + print('\tstring_values: {}'.format(param_tuple.string_values)) + + # Force a cook/recook + in_wrapper.recook() + + +def on_post_process(in_wrapper): + print('on_post_process') + + # in_wrapper.on_post_processing_delegate.remove_callable(on_post_process) + + # Print out all outputs generated by the HDA + num_outputs = in_wrapper.get_num_outputs() + print('num_outputs: {}'.format(num_outputs)) + if num_outputs > 0: + for output_idx in range(num_outputs): + identifiers = in_wrapper.get_output_identifiers_at(output_idx) + print('\toutput index: {}'.format(output_idx)) + print('\toutput type: {}'.format(in_wrapper.get_output_type_at(output_idx))) + print('\tnum_output_objects: {}'.format(len(identifiers))) + if identifiers: + for identifier in identifiers: + output_object = in_wrapper.get_output_object_at(output_idx, identifier) + output_component = in_wrapper.get_output_component_at(output_idx, identifier) + is_proxy = in_wrapper.is_output_current_proxy_at(output_idx, identifier) + print('\t\tidentifier: {}'.format(identifier)) + print('\t\toutput_object: {}'.format(output_object.get_name() if output_object else 'None')) + print('\t\toutput_component: {}'.format(output_component.get_name() if output_component else 'None')) + print('\t\tis_proxy: {}'.format(is_proxy)) + print('') + + +def run(): + # get the API singleton + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + + global _g_wrapper + + # instantiate an asset, disabling auto-cook of the asset (so we have to + # call wrapper.reCook() to cook it) + _g_wrapper = api.instantiate_asset(get_test_hda(), unreal.Transform(), enable_auto_cook=False) + + # Bind to the on post instantiation delegate (before the first cook) + _g_wrapper.on_post_instantiation_delegate.add_callable(on_post_instantiation) + # Bind to the on post processing delegate (after a cook and after all + # outputs have been generated in Unreal) + _g_wrapper.on_post_processing_delegate.add_callable(on_post_process) + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/pdg_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" An example script that instantiates an HDA that contains a TOP network. +After the HDA itself has cooked (in on_post_process) we iterate over all +TOP networks in the HDA and print their paths. Auto-bake is then enabled for +PDG and the TOP networks are cooked. + +""" + +import os + +import unreal + +_g_wrapper = None + + +def get_test_hda_path(): + return '/HoudiniEngine/Examples/hda/pdg_pighead_grid_2_0.pdg_pighead_grid_2_0' + + +def get_test_hda(): + return unreal.load_object(None, get_test_hda_path()) + + +def get_hda_directory(): + """ Attempt to get the directory containing the .hda files. This is + /HoudiniEngine/Examples/hda. In newer versions of UE we can use + ``unreal.SystemLibrary.get_system_path(asset)``, in older versions + we manually construct the path to the plugin. + + Returns: + (str): the path to the example hda directory or ``None``. + + """ + if hasattr(unreal.SystemLibrary, 'get_system_path'): + return os.path.dirname(os.path.normpath( + unreal.SystemLibrary.get_system_path(get_test_hda()))) + else: + plugin_dir = os.path.join( + os.path.normpath(unreal.Paths.project_plugins_dir()), + 'Runtime', 'HoudiniEngine', 'Content', 'Examples', 'hda' + ) + if not os.path.exists(plugin_dir): + plugin_dir = os.path.join( + os.path.normpath(unreal.Paths.engine_plugins_dir()), + 'Runtime', 'HoudiniEngine', 'Content', 'Examples', 'hda' + ) + if not os.path.exists(plugin_dir): + return None + return plugin_dir + + +def delete_instantiated_asset(): + global _g_wrapper + if _g_wrapper: + result = _g_wrapper.delete_instantiated_asset() + _g_wrapper = None + return result + else: + return False + + +def on_pre_instantiation(in_wrapper): + print('on_pre_instantiation') + + # Set the hda_directory parameter to the directory that contains the + # example .hda files + hda_directory = get_hda_directory() + print('Setting "hda_directory" to {0}'.format(hda_directory)) + in_wrapper.set_string_parameter_value('hda_directory', hda_directory) + + # Cook the HDA (not PDG yet) + in_wrapper.recook() + + +def on_post_bake(in_wrapper, success): + # in_wrapper.on_post_bake_delegate.remove_callable(on_post_bake) + print('bake complete ... {}'.format('success' if success else 'failed')) + + +def on_post_process(in_wrapper): + print('on_post_process') + + # in_wrapper.on_post_processing_delegate.remove_callable(on_post_process) + + # Iterate over all PDG/TOP networks and nodes and log them + print('TOP networks:') + for network_path in in_wrapper.get_pdgtop_network_paths(): + print('\t{}'.format(network_path)) + for node_path in in_wrapper.get_pdgtop_node_paths(network_path): + print('\t\t{}'.format(node_path)) + + # Enable PDG auto-bake (auto bake TOP nodes after they are cooked) + in_wrapper.set_pdg_auto_bake_enabled(True) + # Bind to PDG post bake delegate (called after all baking is complete) + in_wrapper.on_post_pdg_bake_delegate.add_callable(on_post_bake) + # Cook the specified TOP node + in_wrapper.pdg_cook_node('topnet1', 'HE_OUT_PIGHEAD_GRID') + + +def run(): + # get the API singleton + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + + global _g_wrapper + + # instantiate an asset, disabling auto-cook of the asset + _g_wrapper = api.instantiate_asset(get_test_hda(), unreal.Transform(), enable_auto_cook=False) + + # Bind to the on pre instantiation delegate (before the first cook) and + # set parameters + _g_wrapper.on_pre_instantiation_delegate.add_callable(on_pre_instantiation) + # Bind to the on post processing delegate (after a cook and after all + # outputs have been generated in Unreal) + _g_wrapper.on_post_processing_delegate.add_callable(on_post_process) + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/process_hda_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" An example script that uses the API and +HoudiniEngineV2.asyncprocessor.ProcessHDA. ProcessHDA is configured with the +asset to instantiate, as well as 2 inputs: a geometry input (a cube) and a +curve input (a helix). + +ProcessHDA is then activiated upon which the asset will be instantiated, +inputs set, and cooked. The ProcessHDA class's on_post_processing() function is +overridden to fetch the input structure and logged. The other state/phase +functions (on_pre_instantiate(), on_post_instantiate() etc) are overridden to +simply log the function name, in order to observe progress in the log. + +""" +import math + +import unreal + +from HoudiniEngineV2.asyncprocessor import ProcessHDA + + +_g_processor = None + + +class ProcessHDAExample(ProcessHDA): + @staticmethod + def _print_api_input(in_input): + print('\t\tInput type: {0}'.format(in_input.__class__)) + print('\t\tbKeepWorldTransform: {0}'.format(in_input.keep_world_transform)) + print('\t\tbImportAsReference: {0}'.format(in_input.import_as_reference)) + if isinstance(in_input, unreal.HoudiniPublicAPIGeoInput): + print('\t\tbPackBeforeMerge: {0}'.format(in_input.pack_before_merge)) + print('\t\tbExportLODs: {0}'.format(in_input.export_lo_ds)) + print('\t\tbExportSockets: {0}'.format(in_input.export_sockets)) + print('\t\tbExportColliders: {0}'.format(in_input.export_colliders)) + elif isinstance(in_input, unreal.HoudiniPublicAPICurveInput): + print('\t\tbCookOnCurveChanged: {0}'.format(in_input.cook_on_curve_changed)) + print('\t\tbAddRotAndScaleAttributesOnCurves: {0}'.format(in_input.add_rot_and_scale_attributes_on_curves)) + + input_objects = in_input.get_input_objects() + if not input_objects: + print('\t\tEmpty input!') + else: + print('\t\tNumber of objects in input: {0}'.format(len(input_objects))) + for idx, input_object in enumerate(input_objects): + print('\t\t\tInput object #{0}: {1}'.format(idx, input_object)) + if hasattr(in_input, 'supports_transform_offset') and in_input.supports_transform_offset(): + print('\t\t\tObject Transform Offset: {0}'.format(in_input.get_input_object_transform_offset(idx))) + if isinstance(input_object, unreal.HoudiniPublicAPICurveInputObject): + print('\t\t\tbClosed: {0}'.format(input_object.is_closed())) + print('\t\t\tCurveMethod: {0}'.format(input_object.get_curve_method())) + print('\t\t\tCurveType: {0}'.format(input_object.get_curve_type())) + print('\t\t\tReversed: {0}'.format(input_object.is_reversed())) + print('\t\t\tCurvePoints: {0}'.format(input_object.get_curve_points())) + + def on_failure(self): + print('on_failure') + global _g_processor + _g_processor = None + + def on_complete(self): + print('on_complete') + global _g_processor + _g_processor = None + + def on_pre_instantiation(self): + print('on_pre_instantiation') + + def on_post_instantiation(self): + print('on_post_instantiation') + + def on_post_auto_cook(self, cook_success): + print('on_post_auto_cook, success = {0}'.format(cook_success)) + + def on_pre_process(self): + print('on_pre_process') + + def on_post_processing(self): + print('on_post_processing') + + # Fetch inputs, iterate over it and log + node_inputs = self.asset_wrapper.get_inputs_at_indices() + parm_inputs = self.asset_wrapper.get_input_parameters() + + if not node_inputs: + print('No node inputs found!') + else: + print('Number of node inputs: {0}'.format(len(node_inputs))) + for input_index, input_wrapper in node_inputs.items(): + print('\tInput index: {0}'.format(input_index)) + self._print_api_input(input_wrapper) + + if not parm_inputs: + print('No parameter inputs found!') + else: + print('Number of parameter inputs: {0}'.format(len(parm_inputs))) + for parm_name, input_wrapper in parm_inputs.items(): + print('\tInput parameter name: {0}'.format(parm_name)) + self._print_api_input(input_wrapper) + + def on_post_auto_bake(self, bake_success): + print('on_post_auto_bake, succes = {0}'.format(bake_success)) + + +def get_test_hda_path(): + return '/HoudiniEngine/Examples/hda/copy_to_curve_1_0.copy_to_curve_1_0' + + +def get_test_hda(): + return unreal.load_object(None, get_test_hda_path()) + + +def get_geo_asset_path(): + return '/Engine/BasicShapes/Cube.Cube' + + +def get_geo_asset(): + return unreal.load_object(None, get_geo_asset_path()) + + +def build_inputs(): + print('configure_inputs') + + # get the API singleton + houdini_api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + + node_inputs = {} + + # Create a geo input + geo_input = houdini_api.create_empty_input(unreal.HoudiniPublicAPIGeoInput) + # Set the input objects/assets for this input + geo_object = get_geo_asset() + geo_input.set_input_objects((geo_object, )) + # store the input data to the HDA as node input 0 + node_inputs[0] = geo_input + + # Create a curve input + curve_input = houdini_api.create_empty_input(unreal.HoudiniPublicAPICurveInput) + # Create a curve wrapper/helper + curve_object = unreal.HoudiniPublicAPICurveInputObject(curve_input) + # Make it a Nurbs curve + curve_object.set_curve_type(unreal.HoudiniPublicAPICurveType.NURBS) + # Set the points of the curve, for this example we create a helix + # consisting of 100 points + curve_points = [] + for i in range(100): + t = i / 20.0 * math.pi * 2.0 + x = 100.0 * math.cos(t) + y = 100.0 * math.sin(t) + z = i + curve_points.append(unreal.Transform([x, y, z], [0, 0, 0], [1, 1, 1])) + curve_object.set_curve_points(curve_points) + # Set the curve wrapper as an input object + curve_input.set_input_objects((curve_object, )) + # Store the input data to the HDA as node input 1 + node_inputs[1] = curve_input + + return node_inputs + + +def run(): + # Create the processor with preconfigured inputs + global _g_processor + _g_processor = ProcessHDAExample( + get_test_hda(), node_inputs=build_inputs()) + # Activate the processor, this will starts instantiation, and then cook + if not _g_processor.activate(): + unreal.log_warning('Activation failed.') + else: + unreal.log('Activated!') + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/ramp_parameter_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" An example script that uses the API to instantiate an HDA and then set +the ramp points of a float ramp and a color ramp. + +""" +import math + +import unreal + +_g_wrapper = None + + +def get_test_hda_path(): + return '/HoudiniEngine/Examples/hda/ramp_example_1_0.ramp_example_1_0' + + +def get_test_hda(): + return unreal.load_object(None, get_test_hda_path()) + + +def set_parameters(in_wrapper): + print('set_parameters') + + # Unbind from the delegate + in_wrapper.on_post_instantiation_delegate.remove_callable(set_parameters) + + # There are two ramps: heightramp and colorramp. The height ramp is a float + # ramp. As an example we'll set the number of ramp points and then set + # each point individually + in_wrapper.set_ramp_parameter_num_points('heightramp', 6) + in_wrapper.set_float_ramp_parameter_point_value('heightramp', 0, 0.0, 0.1) + in_wrapper.set_float_ramp_parameter_point_value('heightramp', 1, 0.2, 0.6) + in_wrapper.set_float_ramp_parameter_point_value('heightramp', 2, 0.4, 1.0) + in_wrapper.set_float_ramp_parameter_point_value('heightramp', 3, 0.6, 1.4) + in_wrapper.set_float_ramp_parameter_point_value('heightramp', 4, 0.8, 1.8) + in_wrapper.set_float_ramp_parameter_point_value('heightramp', 5, 1.0, 2.2) + + # For the color ramp, as an example, we can set the all the points via an + # array. + in_wrapper.set_color_ramp_parameter_points('colorramp', ( + unreal.HoudiniPublicAPIColorRampPoint(position=0.0, value=unreal.LinearColor.GRAY), + unreal.HoudiniPublicAPIColorRampPoint(position=0.5, value=unreal.LinearColor.GREEN), + unreal.HoudiniPublicAPIColorRampPoint(position=1.0, value=unreal.LinearColor.RED), + )) + + +def print_parameters(in_wrapper): + print('print_parameters') + + in_wrapper.on_post_processing_delegate.remove_callable(print_parameters) + + # Print the ramp points directly + print('heightramp: num points {0}:'.format(in_wrapper.get_ramp_parameter_num_points('heightramp'))) + heightramp_data = in_wrapper.get_float_ramp_parameter_points('heightramp') + if not heightramp_data: + print('\tNone') + else: + for idx, point_data in enumerate(heightramp_data): + print('\t\t{0}: position={1:.6f}; value={2:.6f}; interpoloation={3}'.format( + idx, + point_data.position, + point_data.value, + point_data.interpolation + )) + + print('colorramp: num points {0}:'.format(in_wrapper.get_ramp_parameter_num_points('colorramp'))) + colorramp_data = in_wrapper.get_color_ramp_parameter_points('colorramp') + if not colorramp_data: + print('\tNone') + else: + for idx, point_data in enumerate(colorramp_data): + print('\t\t{0}: position={1:.6f}; value={2}; interpoloation={3}'.format( + idx, + point_data.position, + point_data.value, + point_data.interpolation + )) + + # Print all parameter values + param_tuples = in_wrapper.get_parameter_tuples() + print('parameter tuples: {}'.format(len(param_tuples) if param_tuples else 0)) + if param_tuples: + for param_tuple_name, param_tuple in param_tuples.items(): + print('parameter tuple name: {}'.format(param_tuple_name)) + print('\tbool_values: {}'.format(param_tuple.bool_values)) + print('\tfloat_values: {}'.format(param_tuple.float_values)) + print('\tint32_values: {}'.format(param_tuple.int32_values)) + print('\tstring_values: {}'.format(param_tuple.string_values)) + if not param_tuple.float_ramp_points: + print('\tfloat_ramp_points: None') + else: + print('\tfloat_ramp_points:') + for idx, point_data in enumerate(param_tuple.float_ramp_points): + print('\t\t{0}: position={1:.6f}; value={2:.6f}; interpoloation={3}'.format( + idx, + point_data.position, + point_data.value, + point_data.interpolation + )) + if not param_tuple.color_ramp_points: + print('\tcolor_ramp_points: None') + else: + print('\tcolor_ramp_points:') + for idx, point_data in enumerate(param_tuple.color_ramp_points): + print('\t\t{0}: position={1:.6f}; value={2}; interpoloation={3}'.format( + idx, + point_data.position, + point_data.value, + point_data.interpolation + )) + + +def run(): + # get the API singleton + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + + global _g_wrapper + # instantiate an asset with auto-cook enabled + _g_wrapper = api.instantiate_asset(get_test_hda(), unreal.Transform()) + + # Set the float and color ramps on post instantiation, before the first + # cook. + _g_wrapper.on_post_instantiation_delegate.add_callable(set_parameters) + # Print the parameter state after the cook and output creation. + _g_wrapper.on_post_processing_delegate.add_callable(print_parameters) + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/start_session_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unreal + +""" Example for getting the API instance and starting/creating the Houdini +Engine Session. + +""" + + +def run(): + # Get the API singleton + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + # Check if there is an existing valid session + if not api.is_session_valid(): + # Create a new session + api.create_session() + + +if __name__ == '__main__': + run() + +``` + +`Content/Examples/Python/world_input_example.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" An example script that spawns some actors and then uses the API to +instantiate an HDA and set the actors as world inputs. The inputs are set +during post instantiation (before the first cook). After the first cook and +output creation (post processing) the input structure is fetched and logged. + +""" + +import unreal + +_g_wrapper = None + + +def get_test_hda_path(): + return '/HoudiniEngine/Examples/hda/subnet_test_2_0.subnet_test_2_0' + + +def get_test_hda(): + return unreal.load_object(None, get_test_hda_path()) + + +def get_geo_asset_path(): + return '/Engine/BasicShapes/Cube.Cube' + + +def get_geo_asset(): + return unreal.load_object(None, get_geo_asset_path()) + + +def get_cylinder_asset_path(): + return '/Engine/BasicShapes/Cylinder.Cylinder' + + +def get_cylinder_asset(): + return unreal.load_object(None, get_cylinder_asset_path()) + + +def configure_inputs(in_wrapper): + print('configure_inputs') + + # Unbind from the delegate + in_wrapper.on_post_instantiation_delegate.remove_callable(configure_inputs) + + # Spawn some actors + actors = spawn_actors() + + # Create a world input + world_input = in_wrapper.create_empty_input(unreal.HoudiniPublicAPIWorldInput) + # Set the input objects/assets for this input + world_input.set_input_objects(actors) + # copy the input data to the HDA as node input 0 + in_wrapper.set_input_at_index(0, world_input) + # We can now discard the API input object + world_input = None + + # Set the subnet_test HDA to output its first input + in_wrapper.set_int_parameter_value('enable_geo', 1) + + +def print_api_input(in_input): + print('\t\tInput type: {0}'.format(in_input.__class__)) + print('\t\tbKeepWorldTransform: {0}'.format(in_input.keep_world_transform)) + print('\t\tbImportAsReference: {0}'.format(in_input.import_as_reference)) + if isinstance(in_input, unreal.HoudiniPublicAPIWorldInput): + print('\t\tbPackBeforeMerge: {0}'.format(in_input.pack_before_merge)) + print('\t\tbExportLODs: {0}'.format(in_input.export_lo_ds)) + print('\t\tbExportSockets: {0}'.format(in_input.export_sockets)) + print('\t\tbExportColliders: {0}'.format(in_input.export_colliders)) + print('\t\tbIsWorldInputBoundSelector: {0}'.format(in_input.is_world_input_bound_selector)) + print('\t\tbWorldInputBoundSelectorAutoUpdate: {0}'.format(in_input.world_input_bound_selector_auto_update)) + + input_objects = in_input.get_input_objects() + if not input_objects: + print('\t\tEmpty input!') + else: + print('\t\tNumber of objects in input: {0}'.format(len(input_objects))) + for idx, input_object in enumerate(input_objects): + print('\t\t\tInput object #{0}: {1}'.format(idx, input_object)) + + +def print_inputs(in_wrapper): + print('print_inputs') + + # Unbind from the delegate + in_wrapper.on_post_processing_delegate.remove_callable(print_inputs) + + # Fetch inputs, iterate over it and log + node_inputs = in_wrapper.get_inputs_at_indices() + parm_inputs = in_wrapper.get_input_parameters() + + if not node_inputs: + print('No node inputs found!') + else: + print('Number of node inputs: {0}'.format(len(node_inputs))) + for input_index, input_wrapper in node_inputs.items(): + print('\tInput index: {0}'.format(input_index)) + print_api_input(input_wrapper) + + if not parm_inputs: + print('No parameter inputs found!') + else: + print('Number of parameter inputs: {0}'.format(len(parm_inputs))) + for parm_name, input_wrapper in parm_inputs.items(): + print('\tInput parameter name: {0}'.format(parm_name)) + print_api_input(input_wrapper) + + +def spawn_actors(): + actors = [] + # Spawn a static mesh actor and assign a cylinder to its static mesh + # component + actor = unreal.EditorLevelLibrary.spawn_actor_from_class( + unreal.StaticMeshActor, location=(0, 0, 0)) + actor.static_mesh_component.set_static_mesh(get_cylinder_asset()) + actor.set_actor_label('Cylinder') + actor.set_actor_transform( + unreal.Transform( + (-200, 0, 0), + (0, 0, 0), + (2, 2, 2), + ), + sweep=False, + teleport=True + ) + actors.append(actor) + + # Spawn a static mesh actor and assign a cube to its static mesh + # component + actor = unreal.EditorLevelLibrary.spawn_actor_from_class( + unreal.StaticMeshActor, location=(0, 0, 0)) + actor.static_mesh_component.set_static_mesh(get_geo_asset()) + actor.set_actor_label('Cube') + actor.set_actor_transform( + unreal.Transform( + (200, 0, 100), + (45, 0, 45), + (2, 2, 2), + ), + sweep=False, + teleport=True + ) + actors.append(actor) + + return actors + + +def run(): + # get the API singleton + api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + + global _g_wrapper + # instantiate an asset with auto-cook enabled + _g_wrapper = api.instantiate_asset(get_test_hda(), unreal.Transform()) + + # Configure inputs on_post_instantiation, after instantiation, but before first cook + _g_wrapper.on_post_instantiation_delegate.add_callable(configure_inputs) + # Bind on_post_processing, after cook + output creation + _g_wrapper.on_post_processing_delegate.add_callable(print_inputs) + + +if __name__ == '__main__': + run() + +``` + +`Content/Python/HoudiniEngineV2/asyncprocessor.py`: + +```py +# Copyright (c) <2021> Side Effects Software Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. The name of Side Effects Software may not be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +# NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unreal + + +class ProcessHDA(object): + """ An object that wraps async processing of an HDA (instantiating, + cooking/processing/baking an HDA), with functions that are called at the + various stages of the process, that can be overridden by subclasses for + custom funtionality: + + - on_failure() + - on_complete(): upon successful completion (could be PostInstantiation + if auto cook is disabled, PostProcessing if auto bake is disabled, or + after PostAutoBake if auto bake is enabled. + - on_pre_instantiation(): before the HDA is instantiated, a good place + to set parameter values before the first cook. + - on_post_instantiation(): after the HDA is instantiated, a good place + to set/configure inputs before the first cook. + - on_post_auto_cook(): right after a cook + - on_pre_process(): after a cook but before output objects have been + created/processed + - on_post_processing(): after output objects have been created + - on_post_auto_bake(): after outputs have been baked + + Instantiate the processor via the constructor and then call the activate() + function to start the asynchronous process. + + """ + def __init__( + self, + houdini_asset, + instantiate_at=unreal.Transform(), + parameters=None, + node_inputs=None, + parameter_inputs=None, + world_context_object=None, + spawn_in_level_override=None, + enable_auto_cook=True, + enable_auto_bake=False, + bake_directory_path="", + bake_method=unreal.HoudiniEngineBakeOption.TO_ACTOR, + remove_output_after_bake=False, + recenter_baked_actors=False, + replace_previous_bake=False, + delete_instantiated_asset_on_completion_or_failure=False): + """ Instantiates an HDA in the specified world/level. Sets parameters + and inputs supplied in InParameters, InNodeInputs and parameter_inputs. + If bInEnableAutoCook is true, cooks the HDA. If bInEnableAutoBake is + true, bakes the cooked outputs according to the supplied baking + parameters. + + This all happens asynchronously, with the various output pins firing at + the various points in the process: + + - PreInstantiation: before the HDA is instantiated, a good place + to set parameter values before the first cook (parameter values + from ``parameters`` are automatically applied at this point) + - PostInstantiation: after the HDA is instantiated, a good place + to set/configure inputs before the first cook (inputs from + ``node_inputs`` and ``parameter_inputs`` are automatically applied + at this point) + - PostAutoCook: right after a cook + - PreProcess: after a cook but before output objects have been + created/processed + - PostProcessing: after output objects have been created + - PostAutoBake: after outputs have been baked + - Completed: upon successful completion (could be PostInstantiation + if auto cook is disabled, PostProcessing if auto bake is disabled, + or after PostAutoBake if auto bake is enabled). + - Failed: If the process failed at any point. + + Args: + houdini_asset (HoudiniAsset): The HDA to instantiate. + instantiate_at (Transform): The Transform to instantiate the HDA with. + parameters (Map(Name, HoudiniParameterTuple)): The parameters to set before cooking the instantiated HDA. + node_inputs (Map(int32, HoudiniPublicAPIInput)): The node inputs to set before cooking the instantiated HDA. + parameter_inputs (Map(Name, HoudiniPublicAPIInput)): The parameter-based inputs to set before cooking the instantiated HDA. + world_context_object (Object): A world context object for identifying the world to spawn in, if spawn_in_level_override is null. + spawn_in_level_override (Level): If not nullptr, then the HoudiniAssetActor is spawned in that level. If both spawn_in_level_override and world_context_object are null, then the actor is spawned in the current editor context world's current level. + enable_auto_cook (bool): If true (the default) the HDA will cook automatically after instantiation and after parameter, transform and input changes. + enable_auto_bake (bool): If true, the HDA output is automatically baked after a cook. Defaults to false. + bake_directory_path (str): The directory to bake to if the bake path is not set via attributes on the HDA output. + bake_method (HoudiniEngineBakeOption): The bake target (to actor vs blueprint). @see HoudiniEngineBakeOption. + remove_output_after_bake (bool): If true, HDA temporary outputs are removed after a bake. Defaults to false. + recenter_baked_actors (bool): Recenter the baked actors to their bounding box center. Defaults to false. + replace_previous_bake (bool): If true, on every bake replace the previous bake's output (assets + actors) with the new bake's output. Defaults to false. + delete_instantiated_asset_on_completion_or_failure (bool): If true, deletes the instantiated asset actor on completion or failure. Defaults to false. + + """ + super(ProcessHDA, self).__init__() + self._houdini_asset = houdini_asset + self._instantiate_at = instantiate_at + self._parameters = parameters + self._node_inputs = node_inputs + self._parameter_inputs = parameter_inputs + self._world_context_object = world_context_object + self._spawn_in_level_override = spawn_in_level_override + self._enable_auto_cook = enable_auto_cook + self._enable_auto_bake = enable_auto_bake + self._bake_directory_path = bake_directory_path + self._bake_method = bake_method + self._remove_output_after_bake = remove_output_after_bake + self._recenter_baked_actors = recenter_baked_actors + self._replace_previous_bake = replace_previous_bake + self._delete_instantiated_asset_on_completion_or_failure = delete_instantiated_asset_on_completion_or_failure + + self._asset_wrapper = None + self._cook_success = False + self._bake_success = False + + @property + def asset_wrapper(self): + """ The asset wrapper for the instantiated HDA processed by this node. """ + return self._asset_wrapper + + @property + def cook_success(self): + """ True if the last cook was successful. """ + return self._cook_success + + @property + def bake_success(self): + """ True if the last bake was successful. """ + return self._bake_success + + @property + def houdini_asset(self): + """ The HDA to instantiate. """ + return self._houdini_asset + + @property + def instantiate_at(self): + """ The transform the instantiate the asset with. """ + return self._instantiate_at + + @property + def parameters(self): + """ The parameters to set on on_pre_instantiation """ + return self._parameters + + @property + def node_inputs(self): + """ The node inputs to set on on_post_instantiation """ + return self._node_inputs + + @property + def parameter_inputs(self): + """ The object path parameter inputs to set on on_post_instantiation """ + return self._parameter_inputs + + @property + def world_context_object(self): + """ The world context object: spawn in this world if spawn_in_level_override is not set. """ + return self._world_context_object + + @property + def spawn_in_level_override(self): + """ The level to spawn in. If both this and world_context_object is not set, spawn in the editor context's level. """ + return self._spawn_in_level_override + + @property + def enable_auto_cook(self): + """ Whether to set the instantiated asset to auto cook. """ + return self._enable_auto_cook + + @property + def enable_auto_bake(self): + """ Whether to set the instantiated asset to auto bake after a cook. """ + return self._enable_auto_bake + + @property + def bake_directory_path(self): + """ Set the fallback bake directory, for if output attributes do not specify it. """ + return self._bake_directory_path + + @property + def bake_method(self): + """ The bake method/target: for example, to actors vs to blueprints. """ + return self._bake_method + + @property + def remove_output_after_bake(self): + """ Remove temporary HDA output after a bake. """ + return self._remove_output_after_bake + + @property + def recenter_baked_actors(self): + """ Recenter the baked actors at their bounding box center. """ + return self._recenter_baked_actors + + @property + def replace_previous_bake(self): + """ Replace previous bake output on each bake. For the purposes of this + node, this would mostly apply to .uassets and not actors. + + """ + return self._replace_previous_bake + + @property + def delete_instantiated_asset_on_completion_or_failure(self): + """ Whether or not to delete the instantiated asset after Complete is called. """ + return self._delete_instantiated_asset_on_completion_or_failure + + def activate(self): + """ Activate the process. This will: + + - instantiate houdini_asset and wrap it as asset_wrapper + - call on_failure() for any immediate failures + - otherwise bind to delegates from asset_wrapper so that the + various self.on_*() functions are called as appropriate + + Returns immediately (does not block until cooking/processing is + complete). + + Returns: + (bool): False if activation failed. + + """ + # Get the API instance + houdini_api = unreal.HoudiniPublicAPIBlueprintLib.get_api() + if not houdini_api: + # Handle failures: this will unbind delegates and call on_failure() + self._handle_on_failure() + return False + + # Create an empty API asset wrapper + self._asset_wrapper = unreal.HoudiniPublicAPIAssetWrapper.create_empty_wrapper(houdini_api) + if not self._asset_wrapper: + # Handle failures: this will unbind delegates and call on_failure() + self._handle_on_failure() + return False + + # Bind to the wrapper's delegates for instantiation, cooking, baking + # etc events + self._asset_wrapper.on_pre_instantiation_delegate.add_callable( + self._handle_on_pre_instantiation) + self._asset_wrapper.on_post_instantiation_delegate.add_callable( + self._handle_on_post_instantiation) + self._asset_wrapper.on_post_cook_delegate.add_callable( + self._handle_on_post_auto_cook) + self._asset_wrapper.on_pre_process_state_exited_delegate.add_callable( + self._handle_on_pre_process) + self._asset_wrapper.on_post_processing_delegate.add_callable( + self._handle_on_post_processing) + self._asset_wrapper.on_post_bake_delegate.add_callable( + self._handle_on_post_auto_bake) + + # Begin the instantiation process of houdini_asset and wrap it with + # self.asset_wrapper + if not houdini_api.instantiate_asset_with_existing_wrapper( + self.asset_wrapper, + self.houdini_asset, + self.instantiate_at, + self.world_context_object, + self.spawn_in_level_override, + self.enable_auto_cook, + self.enable_auto_bake, + self.bake_directory_path, + self.bake_method, + self.remove_output_after_bake, + self.recenter_baked_actors, + self.replace_previous_bake): + # Handle failures: this will unbind delegates and call on_failure() + self._handle_on_failure() + return False + + return True + + def _unbind_delegates(self): + """ Unbinds from self.asset_wrapper's delegates (if valid). """ + if not self._asset_wrapper: + return + + self._asset_wrapper.on_pre_instantiation_delegate.add_callable( + self._handle_on_pre_instantiation) + self._asset_wrapper.on_post_instantiation_delegate.add_callable( + self._handle_on_post_instantiation) + self._asset_wrapper.on_post_cook_delegate.add_callable( + self._handle_on_post_auto_cook) + self._asset_wrapper.on_pre_process_state_exited_delegate.add_callable( + self._handle_on_pre_process) + self._asset_wrapper.on_post_processing_delegate.add_callable( + self._handle_on_post_processing) + self._asset_wrapper.on_post_bake_delegate.add_callable( + self._handle_on_post_auto_bake) + + def _check_wrapper(self, wrapper): + """ Checks that wrapper matches self.asset_wrapper. Logs a warning if + it does not. + + Args: + wrapper (HoudiniPublicAPIAssetWrapper): the wrapper to check + against self.asset_wrapper + + Returns: + (bool): True if the wrappers match. + + """ + if wrapper != self._asset_wrapper: + unreal.log_warning( + '[UHoudiniPublicAPIProcessHDANode] Received delegate event ' + 'from unexpected asset wrapper ({0} vs {1})!'.format( + self._asset_wrapper.get_name() if self._asset_wrapper else '', + wrapper.get_name() if wrapper else '' + ) + ) + return False + return True + + def _handle_on_failure(self): + """ Handle any failures during the lifecycle of the process. Calls + self.on_failure() and then unbinds from self.asset_wrapper and + optionally deletes the instantiated asset. + + """ + self.on_failure() + + self._unbind_delegates() + + if self.delete_instantiated_asset_on_completion_or_failure and self.asset_wrapper: + self.asset_wrapper.delete_instantiated_asset() + + def _handle_on_complete(self): + """ Handles completion of the process. This can happen at one of + three stages: + + - After on_post_instantiate(), if enable_auto_cook is False. + - After on_post_auto_cook(), if enable_auto_cook is True but + enable_auto_bake is False. + - After on_post_auto_bake(), if both enable_auto_cook and + enable_auto_bake are True. + + Calls self.on_complete() and then unbinds from self.asset_wrapper's + delegates and optionally deletes the instantiated asset. + + """ + self.on_complete() + + self._unbind_delegates() + + if self.delete_instantiated_asset_on_completion_or_failure and self.asset_wrapper: + self.asset_wrapper.delete_instantiated_asset() + + def _handle_on_pre_instantiation(self, wrapper): + """ Called during pre_instantiation. Sets ``parameters`` on the HDA + and calls self.on_pre_instantiation(). + + """ + if not self._check_wrapper(wrapper): + return + + # Set any parameters specified for the HDA + if self.asset_wrapper and self.parameters: + self.asset_wrapper.set_parameter_tuples(self.parameters) + + self.on_pre_instantiation() + + def _handle_on_post_instantiation(self, wrapper): + """ Called during post_instantiation. Sets inputs (``node_inputs`` and + ``parameter_inputs``) on the HDA and calls self.on_post_instantiation(). + + Completes execution if enable_auto_cook is False. + + """ + if not self._check_wrapper(wrapper): + return + + # Set any inputs specified when the node was created + if self.asset_wrapper: + if self.node_inputs: + self.asset_wrapper.set_inputs_at_indices(self.node_inputs) + if self.parameter_inputs: + self.asset_wrapper.set_input_parameters(self.parameter_inputs) + + self.on_post_instantiation() + + # If not set to auto cook, complete execution now + if not self.enable_auto_cook: + self._handle_on_complete() + + def _handle_on_post_auto_cook(self, wrapper, cook_success): + """ Called during post_cook. Sets self.cook_success and calls + self.on_post_auto_cook(). + + Args: + cook_success (bool): True if the cook was successful. + + """ + if not self._check_wrapper(wrapper): + return + + self._cook_success = cook_success + + self.on_post_auto_cook(cook_success) + + def _handle_on_pre_process(self, wrapper): + """ Called during pre_process. Calls self.on_pre_process(). + + """ + if not self._check_wrapper(wrapper): + return + + self.on_pre_process() + + def _handle_on_post_processing(self, wrapper): + """ Called during post_processing. Calls self.on_post_processing(). + + Completes execution if enable_auto_bake is False. + + """ + if not self._check_wrapper(wrapper): + return + + self.on_post_processing() + + # If not set to auto bake, complete execution now + if not self.enable_auto_bake: + self._handle_on_complete() + + def _handle_on_post_auto_bake(self, wrapper, bake_success): + """ Called during post_bake. Sets self.bake_success and calls + self.on_post_auto_bake(). + + Args: + bake_success (bool): True if the bake was successful. + + """ + if not self._check_wrapper(wrapper): + return + + self._bake_success = bake_success + + self.on_post_auto_bake(bake_success) + + self._handle_on_complete() + + def on_failure(self): + """ Called if the process fails to instantiate or fails to start + a cook. + + Subclasses can override this function implement custom functionality. + + """ + pass + + def on_complete(self): + """ Called if the process completes instantiation, cook and/or baking, + depending on enable_auto_cook and enable_auto_bake. + + Subclasses can override this function implement custom functionality. + + """ + pass + + def on_pre_instantiation(self): + """ Called during pre_instantiation. + + Subclasses can override this function implement custom functionality. + + """ + pass + + def on_post_instantiation(self): + """ Called during post_instantiation. + + Subclasses can override this function implement custom functionality. + + """ + pass + + def on_post_auto_cook(self, cook_success): + """ Called during post_cook. + + Subclasses can override this function implement custom functionality. + + Args: + cook_success (bool): True if the cook was successful. + + """ + pass + + def on_pre_process(self): + """ Called during pre_process. + + Subclasses can override this function implement custom functionality. + + """ + pass + + def on_post_processing(self): + """ Called during post_processing. + + Subclasses can override this function implement custom functionality. + + """ + pass + + def on_post_auto_bake(self, bake_success): + """ Called during post_bake. + + Subclasses can override this function implement custom functionality. + + Args: + bake_success (bool): True if the bake was successful. + + """ + pass + +``` + +`HoudiniEngine.uplugin`: + +```uplugin +{ + "FileVersion" : 3, + "FriendlyName" : "Houdini Engine", + "Version" : 21000631, + "VersionName" : "3.0 - H21 +.0 +.631", + "CreatedBy" : "Side Effects Software Inc.", + "CreatedByURL": "https://www.sidefx.com/", + "DocsURL": "https://www.sidefx.com/docs/unreal/", + "SupportURL" : "https://www.sidefx.com/bugs/submit/", + "Description" : "Houdini Engine for Unreal Engine.", + "IsBetaVersion" : false, + "Category": "Procedural", + "EnabledByDefault" : true, + + "Modules" : + [ + { + "Name": "HoudiniEngine", + "Type": "Editor", + "LoadingPhase": "Default" + }, + { + "Name" : "HoudiniEngineEditor", + "Type" : "Editor", + "LoadingPhase" : "PostEngineInit" + }, + { + "Name" : "HoudiniEngineRuntime", + "Type" : "Runtime", + "LoadingPhase" : "Default" + } + ], + + "Plugins" : + [ + { + "Name": "PCG", + "Enabled": true + } + ], + + "CanContainContent" : true, + "Installed": true +} + + +``` + +`LICENSE.md`: + +```md + + Copyright (c) 2021 + Side Effects Software Inc. All rights reserved. + + Redistribution and use of in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. The names Side Effects Software and SideFX may not be used to endorse or + promote products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE `AS IS' AND ANY EXPRESS + OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +``` + +`README.md`: + +```md +# Houdini Engine for Unreal - Version 3 + +> The source code in this branch is intended to be used with Unreal Engine 5.7, 5.6 and 5.5, but should also be compatible with earlier versions of UE5. + +Welcome to the repository for the Houdini Engine For Unreal Plugin. + +This plug-in brings Houdini's powerful and flexible procedural workflow into Unreal Engine through Houdini Digital Assets. Artists can interactively adjust asset parameters inside the editor and use Unreal assets as inputs. Houdini's procedural engine will then "cook" the asset and the results will be available in the editor without the need for baking. + +Documentation for the plugin is available on the Side FX [Website](https://www.sidefx.com/docs/unreal/). + + +# Feedback + +Please send bug reports, feature requests and questions to [Side FX's support](https://www.sidefx.com/bugs/submit/). + + +# Compatibility + +Currently, the plugins has [binaries](https://github.com/sideeffects/HoudiniEngineForUnreal/releases) that have been built for UE5.6, UE5.5, and is linked with the latest production build of Houdini. + +Source code for the plugin is available on this repository for UE5. + +> Please note that all UE5.X versions of the plugin use the same source files/branches, the sources in the 5.0 branch are also intended to be used with more recent versions of the plugin (ie 5.6 , 5.5 etc.) + +In general, we support the latest two releases of UE5.X, but will try to make the source code compatible with previous versions of Unreal. + +> For Houdini 21.0 - there is now two versions of the plugins: one with PCG support and one without PCG. Supporting PCG means that the Houdini Engine plugin now needs to depend on the PCG plugin, which is still considered experimental by Epic. We provide both versions of the plugin with your Houdini install and on this repo's release page, but the source code available here is with PCG support enabled. If you want to use the source code available here but would like to disable the PCG dependancy, instructions are provided at the bottom of this page. + +# Installing the plugin + +Windows/Mac OS: + +01. In this GitHub repository, click **Releases** on the right side. +02. Download the Houdini Engine version zip file that matches your Houdini version. +03. Extract the **HoudiniEngine** folder to the **Plugins\Runtime** of your Unreal Directory. You can either copy it to Unreal's engine version directory or your Unreal project directory. + + When installing the plugin to the engine, you want the final path to be: `PATH\TO\UE_5.X\Engine\Plugins\Runtime\HoudiniEngine` and when installing to an unral project `PATH\TO\PROJECT\Plugins\HoudiniEngine` + + **Note: For Unreal Engine 5, if you're using the Epic Games Launcher version of Unreal and are not building from source, we recommend you use Unreal's project directory.** + +Linux: + +Houdini Engine is not officially supported on Linux. However, you can follow the `Building from source` instructions and use the source code on this repository to build the plugin. +You can find the unreal plugin source in your Houdini install's `engine\unreal` subdirectory. + +## Verify the Plug-in works +Once you install the Houdini Engine plug-in, you can verify it's loaded properly. + +01. Open a new or existing Unreal project. +02. In the **main menu bar**, you can see **Houdini Engine** as a new selection. + +You should also check the Houdini Engine plug-in version matches your Houdini Version for the plug-in to work properly. + +01. In Unreal Engine main menu bar, click **Edit** then **Plugins**. +02. For Houdini Engine, check the **HX.Y.Z.** version number matches your Houdini version. X.Y.Z means your Houdini Version number. + +You can learn how to export an Houdini Digital Assets (HDA), import it into Unreal Engine, and update the asset from [Assets documentation.](https://www.sidefx.com/docs/unreal/_assets.html) + +# Building from source + +01. Get the UE source code from: https://github.com/EpicGames/UnrealEngine/releases +01. Within the UE source, navigate to `Engine/Plugins/Runtime`, and clone this repo into a folder named `HoudiniEngine`. Alternatively, you can also install the plugin in your project, in the `Plugins/Runtime` directory. +01. Download and install the correct build of 64-bit Houdini. To get the build number, look at the header of `Source/HoudiniEngine/HoudiniEngine.Build.cs`, under `Houdini Version`. +01. Generate the UE Project Files (by running `GenerateProjectFiles`) and build Unreal, either in x64 `Debug Editor` or x64 `Development Editor`. +01. When starting the Unreal Engine editor, go to Plug-ins menu and make sure to enable the `HoudiniEngine v2` plug-in (in the `Rendering` section). Restart UE if you had to enable it. +01. To confirm that the plug-in has been successfully installed and enabled, you can check that the editor main menu bar now has a new "Houdini Engine" menu, between "Edit" and "Window". +01. You should now be able to import Houdini Digital Assets (HDA) `.otl` or `.hda` files or drag and drop them into the `Content Browser`. +01. Once you have an HDA in the `Content Browser` you should be able to drag it into the Editor viewport. This will spawn a new Houdini Asset Actor. Geometry cooking will be done in a separate thread and geometry will be displayed once the cooking is complete. At this point you will be able to see asset parameters in the `Details` pane. Modifying any of the parameters will force the asset to recook and possibly update its geometry. + +# Disabling PCG support when building from source + +> This guide is only for users who download the source code directly from the git repo, and want to disable PCG support. If you're getting the plugin source code from your Houdini install or the release page, we already provide versions of the source code with/without PCG support. + +01. In the plugin's source folder, open the HoudiniEngine module's build.cs file: `Source/HoudiniEngine/HoudiniEngine.Build.cs` +01. Change `private int HOUDINI_USE_PCG = 1;` to `private int HOUDINI_USE_PCG = 0;` +01. Repeat the same change for the HoudiniEngineEditor and the HoudiniEngineRuntime modules'build.cs file. They're located respectively in `Source/HoudiniEngineEditor/HoudiniEngineEditor.Build.cs` and `Source/HoudiniEngineRuntime/HoudiniEngineRuntime.Build.cs` +01. Delete all the *HoudiniPCGXXX.h/cpp* files in the plugin's files. +01. Delete all the `Intermediate` and `Binaries` folder in the HoudiniEngine's plugin directory (if they exist). +01. Regenerate your project's file. On windows, this can be done by right-clicking on the uproject file, and selecting `Generate Viual Studio Project Files` +01. Build your project / the plugin. + + +``` + +`Source/HoudiniEngine/HoudiniEngine.Build.cs`: + +```cs +/* + * Copyright (c) <2021> Side Effects Software Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Produced by: + * Side Effects Software Inc + * 123 Front Street West, Suite 1401 + * Toronto, Ontario + * Canada M5J 2M2 + * 416-504-9876 + * + * COMMENTS: + * This file is generated. Do not modify directly. + */ + +/* + + Houdini Version: 21 +.0 +.631 + Houdini Engine Version: 8.0.3 + Unreal Version: 5.0.0 + +*/ + +using UnrealBuildTool; +using System; +using System.IO; +using EpicGames.Core; +#if UE_5_3_OR_LATER +using Microsoft.Extensions.Logging; +#endif + +public class HoudiniEngine : ModuleRules +{ + + private int HOUDINI_USE_PCG = 1; + + private void LogError(string err) + { +#if UE_5_3_OR_LATER + Logger.LogError(err); +#else + Log.TraceError(err); +#endif + } + + private void LogTrace(string log) + { +#if UE_5_3_OR_LATER + Logger.LogTrace(log); +#else + Log.TraceVerbose(log); +#endif + } + + private void LogInformation(string info) + { +#if UE_5_3_OR_LATER + Logger.LogInformation(info); +#else + Log.TraceInformationOnce(info); +#endif + } + + private string GetHFSPath() + { + string HoudiniVersion = "21 +.0 +.631"; + bool bIsRelease = true; + string HFSPath = ""; + string RegistryPath = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Side Effects Software"; + string Registry32Path = "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Side Effects Software"; + string log; + + if (!bIsRelease) + { + // Only use the preset build folder + LogTrace("Using stamped HFSPath:" + HFSPath); + return HFSPath; + } + + // Look for the Houdini install folder for this platform + PlatformID buildPlatformId = Environment.OSVersion.Platform; + if (buildPlatformId == PlatformID.Win32NT) + { + // Look for the HEngine install path in the registry + string HEngineRegistry = RegistryPath + string.Format(@"\Houdini Engine {0}", HoudiniVersion); + string HPath = Microsoft.Win32.Registry.GetValue(HEngineRegistry, "InstallPath", null) as string; + if (HPath != null) + { + log = string.Format("Houdini Engine : Looking for Houdini Engine {0} in {1}", HoudiniVersion, HPath); + LogTrace(log); + if (Directory.Exists(HPath)) + return HPath; + } + + HEngineRegistry = Registry32Path + string.Format(@"\Houdini Engine {0}", HoudiniVersion); + HPath = Microsoft.Win32.Registry.GetValue(HEngineRegistry, "InstallPath", null) as string; + if (HPath != null) + { + log = string.Format("Houdini Engine : Looking for Houdini Engine {0} in {1}", HoudiniVersion, HPath); + LogTrace(log); + if (Directory.Exists(HPath)) + return HPath; + } + + // If we couldn't find the Houdini Engine registry path, try the default one + string DefaultHPath = "C:/Program Files/Side Effects Software/Houdini Engine " + HoudiniVersion; + if (DefaultHPath != HPath) + { + log = string.Format("Houdini Engine : Looking for Houdini Engine {0} in {1}", HoudiniVersion, DefaultHPath); + LogTrace(log); + if (Directory.Exists(DefaultHPath)) + return DefaultHPath; + } + + // Look for the Houdini registry install path for the version the plug-in was compiled for + string HoudiniRegistry = RegistryPath + string.Format(@"\Houdini {0}", HoudiniVersion); + HPath = Microsoft.Win32.Registry.GetValue(HoudiniRegistry, "InstallPath", null) as string; + if (HPath != null) + { + log = string.Format("Houdini Engine : Looking for Houdini {0} in {1}", HoudiniVersion, HPath); + LogTrace(log); + if (Directory.Exists(HPath)) + return HPath; + } + + // Look for the Houdini registry install path for the version the plug-in was compiled for + HoudiniRegistry = Registry32Path + string.Format(@"\Houdini {0}", HoudiniVersion); + HPath = Microsoft.Win32.Registry.GetValue(HoudiniRegistry, "InstallPath", null) as string; + if (HPath != null) + { + log = string.Format("Houdini Engine : Looking for Houdini {0} in {1}", HoudiniVersion, HPath); + LogTrace(log); + if (Directory.Exists(HPath)) + return HPath; + } + + // If we couldn't find the Houdini registry path, try the default one + DefaultHPath = "C:/Program Files/Side Effects Software/Houdini " + HoudiniVersion; + if (DefaultHPath != HPath) + { + log = string.Format("Houdini Engine : Looking for Houdini {0} in {1}", HoudiniVersion, DefaultHPath); + LogTrace(log); + if (Directory.Exists(DefaultHPath)) + return DefaultHPath; + } + + // See if the preset build HFS exists + if (Directory.Exists(HFSPath)) + return HFSPath; + + log = string.Format("Houdini Engine : Failed to find Houdini {0}, will attempt to build using the latest installed version", HoudiniVersion); + LogTrace(log); + + // We couldn't find the exact version the plug-in was built for, we can still try with the active version in the registry + string ActiveHEngine = Microsoft.Win32.Registry.GetValue(RegistryPath, "ActiveEngineVersion", null) as string; + if (ActiveHEngine == null) + { + ActiveHEngine = Microsoft.Win32.Registry.GetValue(Registry32Path, "ActiveEngineVersion", null) as string; + } + if (ActiveHEngine != null) + { + // See if the latest active HEngine version has the proper major/minor version + if (ActiveHEngine.Substring(0, 4) == HoudiniVersion.Substring(0, 4)) + { + log = string.Format("Houdini Engine : Found Active Houdini Engine version: {0}", ActiveHEngine); + LogTrace(log); + + // Active version contain the patch version that we need to strip off + //string[] ActiveVersion = ActiveHEngine.Split("."); + + HEngineRegistry = RegistryPath + string.Format(@"\Houdini Engine {0}", ActiveHEngine); + HPath = Microsoft.Win32.Registry.GetValue(HEngineRegistry, "InstallPath", null) as string; + if (HPath != null) + { + log = string.Format("Houdini Engine : Looking for Houdini Engine {0} in {1}", ActiveHEngine, HPath); + LogTrace(log); + if (Directory.Exists(HPath)) + return HPath; + } + } + } + + // Active HEngine version didn't match, so try with the active Houdini version + string ActiveHoudini = Microsoft.Win32.Registry.GetValue(RegistryPath, "ActiveVersion", null) as string; + if (ActiveHoudini == null) + { + ActiveHoudini = Microsoft.Win32.Registry.GetValue(Registry32Path, "ActiveVersion", null) as string; + } + if (ActiveHoudini != null) + { + // See if the latest active Houdini version has the proper major/minor version + if (ActiveHoudini.Substring(0, 4) == HoudiniVersion.Substring(0, 4)) + { + log = string.Format("Houdini Engine : Found Active Houdini version: {0}", ActiveHoudini); + LogTrace(log); + + HoudiniRegistry = RegistryPath + string.Format(@"\Houdini {0}", ActiveHoudini); + HPath = Microsoft.Win32.Registry.GetValue(HoudiniRegistry, "InstallPath", null) as string; + if (HPath != null) + { + log = string.Format("Houdini Engine : Looking for Houdini {0} in {1}", ActiveHoudini, HPath); + LogTrace(log); + + if (Directory.Exists(HPath)) + return HPath; + } + } + } + } + else if (buildPlatformId == PlatformID.MacOSX || + (buildPlatformId == PlatformID.Unix && File.Exists("/System/Library/CoreServices/SystemVersion.plist"))) + { + // Check for Houdini installation. + string HPath = "/Applications/Houdini/Houdini" + HoudiniVersion + "/Frameworks/Houdini.framework/Versions/Current/Resources"; + if (Directory.Exists(HPath)) + return HPath; + + HPath = "/Users/Shared/Houdini/HoudiniIndieSteam/Frameworks/Houdini.framework/Versions/Current/Resources"; + if (Directory.Exists(HPath)) + return HPath; + + if (Directory.Exists(HFSPath)) + return HFSPath; + } + else if (buildPlatformId == PlatformID.Unix) + { + HFSPath = System.Environment.GetEnvironmentVariable("HFS"); + if (Directory.Exists(HFSPath)) + { + LogTrace("Unix using $HFS: " + HFSPath); + return HFSPath; + } + } + else + { + LogTrace(string.Format("Building on an unknown environment!")); + } + + string Info = string.Format("Houdini Engine : Houdini {0} could not be found. Houdini Engine will not be available in this build.", HoudiniVersion); + LogInformation(Info); + + return ""; + } + + public HoudiniEngine(ReadOnlyTargetRules Target) : base(Target) + { + bPrecompile = true; + PCHUsage = PCHUsageMode.NoSharedPCHs; + PrivatePCHHeaderFile = "Private/HoudiniEnginePrivatePCH.h"; + + // Check if we are compiling on unsupported platforms. + if (Target.Platform != UnrealTargetPlatform.Win64 && + Target.Platform != UnrealTargetPlatform.Mac && + Target.Platform != UnrealTargetPlatform.Linux) + { + string Err = string.Format("Houdini Engine : Compiling for unsupported platform."); + LogError(Err); + throw new BuildException(Err); + } + + if (Target.bBuildEditor == false) + { + // Actual Runtime Houdini Engine, currently not supported + string Err = string.Format("Houdini Engine : Building as a runtime module is currently not supported."); + LogError(Err); + throw new BuildException(Err); + } + + // Find HFS + string HFSPath = GetHFSPath(); + HFSPath = HFSPath.Replace("\\", "/"); + + if (HFSPath != "") + { + string log = string.Format("Houdini Engine : Found Houdini in {0}", HFSPath); + LogInformation(log); + + PlatformID buildPlatformId = Environment.OSVersion.Platform; + PublicDefinitions.Add("HOUDINI_ENGINE_HFS_PATH_DEFINE=" + HFSPath); + } + + PublicIncludePaths.AddRange( + new string[] + { + //Path.Combine(ModuleDirectory, "Public/HAPI"), + //"HoudiniEngineRuntime/Public/HAPI" + } + ); + + PrivateIncludePaths.AddRange( + new string[] + { + "HoudiniEngineRuntime/Private" + } + ); + + // Add common dependencies. + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "HoudiniEngineRuntime", + "RenderCore", + "InputCore", + "RHI", + "Foliage", + "FoliageEdit", + "Landscape", + "StaticMeshDescription", + "Chaos", + "GeometryCollectionEngine", + "FieldSystemEngine", + "BSPUtils", + "DataLayerEditor", + "MaterialEditor" + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "Landscape", + "PhysicsCore", + "PhysicsUtilities" + } + ); + + if (HOUDINI_USE_PCG == 1) + { + PrivateDependencyModuleNames.AddRange( + new string[] + { + "PCG" + } + ); + PrivateDefinitions.Add("HOUDINI_USE_PCG=1"); + } + + if (Target.bBuildEditor == true) + { + PrivateDependencyModuleNames.AddRange( + new string[] + { + "AppFramework", + "AssetTools", + "EditorStyle", + "EditorWidgets", + "Kismet", + "LevelEditor", + "MainFrame", + "MeshPaint", + "Projects", + "PropertyEditor", + "RawMesh", + "Settings", + "Slate", + "SlateCore", + "TargetPlatform", + "UnrealEd", + "ApplicationCore", + "LandscapeEditor", + "MeshDescription", + "MeshDescriptionOperations", + "WorldBrowser", + "Messaging", + "SlateNullRenderer", + "EditorFramework", + "SubobjectEditor", + "MeshMergeUtilities", + "MeshUtilitiesCommon", + "Json", + "SkeletalMeshDescription" + } + ); + } + +#if UE_5_3_OR_LATER + PrivateIncludePaths.AddRange(new string[] { + System.IO.Path.Combine(GetModuleDirectory("MeshMergeUtilities"), "Private"), + }); +#endif + + PrivateIncludePathModuleNames.AddRange( + new string[] + { + "DirectoryWatcher" + } + ); + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + "DirectoryWatcher" + } + ); + } +} + +``` + +`Source/HoudiniEngine/Private/GeometryToolsEngine.cpp`: + +```cpp +#include "GeometryToolsEngine.h" + +#include + +// ---------------------------------------------- +// GteLogger.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +using namespace houdini::gte; + +Logger::Logger(char const* file, char const* function, int line, + std::string const& message) +{ + mMessage = + "File: " + std::string(file) + "\n" + + "Func: " + std::string(function) + "\n" + +#ifdef _MSC_VER // std::to_string not available on some platforms + "Line: " + std::to_string(line) + "\n" + +#endif + message + "\n\n"; +} + +void Logger::Assertion() +{ + msMutex.lock(); + for (auto listener : msListeners) + { + if (listener->GetFlags() & Listener::LISTEN_FOR_ASSERTION) + { + listener->Assertion(mMessage); + } + } + msMutex.unlock(); +} + +void Logger::Error() +{ + msMutex.lock(); + for (auto listener : msListeners) + { + if (listener->GetFlags() & Listener::LISTEN_FOR_ERROR) + { + listener->Error(mMessage); + } + } + msMutex.unlock(); +} + +void Logger::Warning() +{ + msMutex.lock(); + for (auto listener : msListeners) + { + if (listener->GetFlags() & Listener::LISTEN_FOR_WARNING) + { + listener->Warning(mMessage); + } + } + msMutex.unlock(); +} + +void Logger::Information() +{ + msMutex.lock(); + for (auto listener : msListeners) + { + if (listener->GetFlags() & Listener::LISTEN_FOR_INFORMATION) + { + listener->Information(mMessage); + } + } + msMutex.unlock(); +} + +void Logger::Subscribe(Listener* listener) +{ + msMutex.lock(); + msListeners.insert(listener); + msMutex.unlock(); +} + +void Logger::Unsubscribe(Listener* listener) +{ + msMutex.lock(); + msListeners.erase(listener); + msMutex.unlock(); +} + + + +// Logger::Listener + +Logger::Listener::~Listener() +{ +} + +Logger::Listener::Listener(int flags) + : + mFlags(flags) +{ +} + +int Logger::Listener::GetFlags() const +{ + return mFlags; +} + +void Logger::Listener::Assertion(std::string const& message) +{ + Report("\nGTE ASSERTION:\n" + message); +} + +void Logger::Listener::Error(std::string const& message) +{ + Report("\nGTE ERROR:\n" + message); +} + +void Logger::Listener::Warning(std::string const& message) +{ + Report("\nGTE WARNING:\n" + message); +} + +void Logger::Listener::Information(std::string const& message) +{ + Report("\nGTE INFORMATION:\n" + message); +} + +void Logger::Listener::Report(std::string const&) +{ + // Stub for derived classes. +} + + +std::mutex Logger::msMutex; +std::set Logger::msListeners; + +// ---------------------------------------------- +// GteWrapper.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +namespace houdini::gte +{ + +void Memcpy(void* target, void const* source, size_t count) +{ +#if defined(__MSWINDOWS__) + errno_t result = memcpy_s(target, count, source, count); + (void)result; // 0 on success +#else + memcpy(target, source, count); +#endif +} + +void Memcpy(wchar_t* target, wchar_t const* source, size_t count) +{ +#if defined(__MSWINDOWS__) + errno_t result = wmemcpy_s(target, count, source, count); + (void)result; // 0 on success +#else + wmemcpy(target, source, count); +#endif +} + +} + + + +// ---------------------------------------------- +// GteETManifoldMesh.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.1 (2017/01/02) + +ETManifoldMesh::~ETManifoldMesh() +{ +} + +ETManifoldMesh::ETManifoldMesh(ECreator eCreator, TCreator tCreator) + : + mECreator(eCreator ? eCreator : CreateEdge), + mTCreator(tCreator ? tCreator : CreateTriangle), + mAssertOnNonmanifoldInsertion(true) +{ +} + +ETManifoldMesh::ETManifoldMesh(ETManifoldMesh const& mesh) +{ + *this = mesh; +} + +ETManifoldMesh& ETManifoldMesh::operator=(ETManifoldMesh const& mesh) +{ + Clear(); + + mECreator = mesh.mECreator; + mTCreator = mesh.mTCreator; + mAssertOnNonmanifoldInsertion = mesh.mAssertOnNonmanifoldInsertion; + for (auto const& element : mesh.mTMap) + { + Insert(element.first.V[0], element.first.V[1], element.first.V[2]); + } + + return *this; +} + +ETManifoldMesh::EMap const& ETManifoldMesh::GetEdges() const +{ + return mEMap; +} + +ETManifoldMesh::TMap const& ETManifoldMesh::GetTriangles() const +{ + return mTMap; +} + +bool ETManifoldMesh::AssertOnNonmanifoldInsertion(bool doAssert) +{ + std::swap(doAssert, mAssertOnNonmanifoldInsertion); + return doAssert; // return the previous state +} + +std::shared_ptr ETManifoldMesh::Insert(int v0, int v1, int v2) +{ + TriangleKey tkey(v0, v1, v2); + if (mTMap.find(tkey) != mTMap.end()) + { + // The triangle already exists. Return a null pointer as a signal to + // the caller that the insertion failed. + return nullptr; + } + + // Create the new triangle. It will be added to mTMap at the end of the + // function so that if an assertion is triggered and the function returns + // early, the (bad) triangle will not be part of the mesh. + std::shared_ptr tri = mTCreator(v0, v1, v2); + + // Add the edges to the mesh if they do not already exist. + for (int i0 = 2, i1 = 0; i1 < 3; i0 = i1++) + { + EdgeKey ekey(tri->V[i0], tri->V[i1]); + std::shared_ptr edge; + auto eiter = mEMap.find(ekey); + if (eiter == mEMap.end()) + { + // This is the first time the edge is encountered. + edge = mECreator(tri->V[i0], tri->V[i1]); + mEMap[ekey] = edge; + + // Update the edge and triangle. + edge->T[0] = tri; + tri->E[i0] = edge; + } + else + { + // This is the second time the edge is encountered. + edge = eiter->second; + if (!edge) + { + LogError("Unexpected condition."); + return nullptr; + } + + // Update the edge. + if (edge->T[1].lock()) + { + if (mAssertOnNonmanifoldInsertion) + { + LogInformation("The mesh must be manifold."); + } + return nullptr; + } + edge->T[1] = tri; + + // Update the adjacent triangles. + auto adjacent = edge->T[0].lock(); + if (!adjacent) + { + LogError("Unexpected condition."); + return nullptr; + } + for (int j = 0; j < 3; ++j) + { + if (adjacent->E[j].lock() == edge) + { + adjacent->T[j] = tri; + break; + } + } + + // Update the triangle. + tri->E[i0] = edge; + tri->T[i0] = adjacent; + } + } + + mTMap[tkey] = tri; + return tri; +} + +bool ETManifoldMesh::Remove(int v0, int v1, int v2) +{ + TriangleKey tkey(v0, v1, v2); + auto titer = mTMap.find(tkey); + if (titer == mTMap.end()) + { + // The triangle does not exist. + return false; + } + + // Get the triangle. + std::shared_ptr tri = titer->second; + + // Remove the edges and update adjacent triangles if necessary. + for (int i = 0; i < 3; ++i) + { + // Inform the edges the triangle is being deleted. + auto edge = tri->E[i].lock(); + if (!edge) + { + // The triangle edge should be nonnull. + LogError("Unexpected condition."); + return false; + } + + if (edge->T[0].lock() == tri) + { + // One-triangle edges always have pointer at index zero. + edge->T[0] = edge->T[1]; + edge->T[1].reset(); + } + else if (edge->T[1].lock() == tri) + { + edge->T[1].reset(); + } + else + { + LogError("Unexpected condition."); + return false; + } + + // Remove the edge if you have the last reference to it. + if (!edge->T[0].lock() && !edge->T[1].lock()) + { + EdgeKey ekey(edge->V[0], edge->V[1]); + mEMap.erase(ekey); + } + + // Inform adjacent triangles the triangle is being deleted. + auto adjacent = tri->T[i].lock(); + if (adjacent) + { + for (int j = 0; j < 3; ++j) + { + if (adjacent->T[j].lock() == tri) + { + adjacent->T[j].reset(); + break; + } + } + } + } + + mTMap.erase(tkey); + return true; +} + +void ETManifoldMesh::Clear() +{ + mEMap.clear(); + mTMap.clear(); +} + +bool ETManifoldMesh::IsClosed() const +{ + for (auto const& element : mEMap) + { + auto edge = element.second; + if (!edge->T[0].lock() || !edge->T[1].lock()) + { + return false; + } + } + return true; +} + +bool ETManifoldMesh::IsOriented() const +{ + for (auto const& element : mEMap) + { + auto edge = element.second; + if (edge->T[0].lock() && edge->T[1].lock()) + { + // In each triangle, find the ordered edge that corresponds to the + // unordered edge element.first. Also find the vertex opposite + // that edge. + bool edgePositive[2] = { false, false }; + int vOpposite[2] = { -1, -1 }; + for (int j = 0; j < 2; ++j) + { + auto tri = edge->T[j].lock(); + for (int i = 0; i < 3; ++i) + { + if (tri->V[i] == element.first.V[0]) + { + int vNext = tri->V[(i + 1) % 3]; + if (vNext == element.first.V[1]) + { + edgePositive[j] = true; + vOpposite[j] = tri->V[(i + 2) % 3]; + } + else + { + edgePositive[j] = false; + vOpposite[j] = vNext; + } + break; + } + } + } + + // To be oriented consistently, the edges must have reversed + // ordering and the oppositive vertices cannot match. + if (edgePositive[0] == edgePositive[1] || vOpposite[0] == vOpposite[1]) + { + return false; + } + } + } + return true; +} + +void ETManifoldMesh::GetComponents( + std::vector>>& components) const +{ + // visited: 0 (unvisited), 1 (discovered), 2 (finished) + std::map, int> visited; + for (auto const& element : mTMap) + { + visited.insert(std::make_pair(element.second, 0)); + } + + for (auto& element : mTMap) + { + auto tri = element.second; + if (visited[tri] == 0) + { + std::vector> component; + DepthFirstSearch(tri, visited, component); + components.push_back(component); + } + } +} + +void ETManifoldMesh::GetComponents( + std::vector>>& components) const +{ + // visited: 0 (unvisited), 1 (discovered), 2 (finished) + std::map, int> visited; + for (auto const& element : mTMap) + { + visited.insert(std::make_pair(element.second, 0)); + } + + for (auto& element : mTMap) + { + std::shared_ptr tri = element.second; + if (visited[tri] == 0) + { + std::vector> component; + DepthFirstSearch(tri, visited, component); + + std::vector> keyComponent; + keyComponent.reserve(component.size()); + for (auto const& t : component) + { + keyComponent.push_back(TriangleKey(t->V[0], t->V[1], t->V[2])); + } + components.push_back(keyComponent); + } + } +} + +void ETManifoldMesh::DepthFirstSearch(std::shared_ptr const& tInitial, + std::map, int>& visited, + std::vector>& component) const +{ + // Allocate the maximum-size stack that can occur in the depth-first + // search. The stack is empty when the index top is -1. + std::vector> tStack(mTMap.size()); + int top = -1; + tStack[++top] = tInitial; + while (top >= 0) + { + std::shared_ptr tri = tStack[top]; + visited[tri] = 1; + int i; + for (i = 0; i < 3; ++i) + { + std::shared_ptr adj = tri->T[i].lock(); + if (adj && visited[adj] == 0) + { + tStack[++top] = adj; + break; + } + } + if (i == 3) + { + visited[tri] = 2; + component.push_back(tri); + --top; + } + } +} + +std::shared_ptr ETManifoldMesh::CreateEdge(int v0, int v1) +{ + return std::make_shared(v0, v1); +} + +std::shared_ptr ETManifoldMesh::CreateTriangle(int v0, int v1, int v2) +{ + return std::make_shared(v0, v1, v2); +} + +ETManifoldMesh::Edge::~Edge() +{ +} + +ETManifoldMesh::Edge::Edge(int v0, int v1) +{ + V[0] = v0; + V[1] = v1; +} + +ETManifoldMesh::Triangle::~Triangle() +{ +} + +ETManifoldMesh::Triangle::Triangle(int v0, int v1, int v2) +{ + V[0] = v0; + V[1] = v1; + V[2] = v2; +} + + +``` + +`Source/HoudiniEngine/Private/GeometryToolsEngine.h`: + +```h + +#pragma once + +#include "CoreMinimal.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ---------------------------------------------- +// ProgressCancel.h +// ---------------------------------------------- + +// Copyright Epic Games, Inc. All Rights Reserved. + +// Port of geometry3cpp ProgressCancel + +/** + * FHEProgressCancel is an obejct that is intended to be passed to long-running + * computes to do two things: + * 1) provide progress info back to caller (not implemented yet) + * 2) allow caller to cancel the computation + */ +class FHEProgressCancel +{ +private: + bool WasCancelled = false; // will be set to true if CancelF() ever returns true + +public: + TFunction CancelF = []() { return false; }; + + /** + * @return true if client would like to cancel operation + */ + bool Cancelled() + { + if (WasCancelled) + { + return true; + } + WasCancelled = CancelF(); + return WasCancelled; + } + + +public: + + enum class EMessageLevel + { + // Note: Corresponds to EToolMessageLevel in InteractiveToolsFramework/ToolContextInterfaces.h + + /** Development message goes into development log */ + Internal = 0, + /** User message should appear in user-facing log */ + UserMessage = 1, + /** Notification message should be shown in a non-modal notification window */ + UserNotification = 2, + /** Warning message should be shown in a non-modal notification window with panache */ + UserWarning = 3, + /** Error message should be shown in a modal notification window */ + UserError = 4 + }; + + struct FMessageInfo + { + FText MessageText; + EMessageLevel MessageLevel; + FDateTime Timestamp; + }; + + void AddWarning(const FText& MessageText, EMessageLevel MessageLevel) + { + Warnings.Add(FMessageInfo{ MessageText , MessageLevel, FDateTime::Now() }); + } + + TArray Warnings; +}; + +// ---------------------------------------------- +// GTEngineDEF.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +//---------------------------------------------------------------------------- +// The platform specification. +// +// __MSWINDOWS__ : Microsoft Windows (WIN32 or WIN64) +// __APPLE__ : Macintosh OS X +// __LINUX__ : Linux or Cygwin +//---------------------------------------------------------------------------- + +#if !defined(__LINUX__) && (defined(WIN32) || defined(_WIN64)) +#define __MSWINDOWS__ + +#if !defined(_MSC_VER) +#error Microsoft Visual Studio 2013 or later is required. +#endif + +// MSVC 6 is version 12.0 +// MSVC 7.0 is version 13.0 (MSVS 2002) +// MSVC 7.1 is version 13.1 (MSVS 2003) +// MSVC 8.0 is version 14.0 (MSVS 2005) +// MSVC 9.0 is version 15.0 (MSVS 2008) +// MSVC 10.0 is version 16.0 (MSVS 2010) +// MSVC 11.0 is version 17.0 (MSVS 2012) +// MSVC 12.0 is version 18.0 (MSVS 2013) +// MSVC 14.0 is version 19.0 (MSVS 2015) +// Currently, projects are provided only for MSVC 12.0 and 14.0. +#if _MSC_VER < 1800 +#error Microsoft Visual Studio 2013 or later is required. +#endif + +// Debug build values (choose_your_value is 0, 1, or 2) +// 0: Disables checked iterators and disables iterator debugging. +// 1: Enables checked iterators and disables iterator debugging. +// 2: (default) Enables iterator debugging; checked iterators are not relevant. +// +// Release build values (choose_your_value is 0 or 1) +// 0: (default) Disables checked iterators. +// 1: Enables checked iterators; iterator debugging is not relevant. +// +// #define _ITERATOR_DEBUG_LEVEL choose_your_value + +#endif // WIN32 or _WIN64 + +// TODO: Windows DLL configurations have not yet been added to the project, +// but these defines are required to support them (when we do add them). +// +// Add GTE_EXPORT to project preprocessor options for dynamic library +// configurations to export their symbols. +#if defined(GTE_EXPORT) + // For the dynamic library configurations. + #define GTE_IMPEXP __declspec(dllexport) +#else + // For a client of the dynamic library or for the static library + // configurations. + #define GTE_IMPEXP +#endif + +// Expose exactly one of these. +#define GTE_USE_ROW_MAJOR +//#define GTE_USE_COL_MAJOR + +// Expose exactly one of these. +#define GTE_USE_MAT_VEC +//#define GTE_USE_VEC_MAT + +#if (defined(GTE_USE_ROW_MAJOR) && defined(GTE_USE_COL_MAJOR)) || (!defined(GTE_USE_ROW_MAJOR) && !defined(GTE_USE_COL_MAJOR)) +#error Exactly one storage order must be specified. +#endif + +#if (defined(GTE_USE_MAT_VEC) && defined(GTE_USE_VEC_MAT)) || (!defined(GTE_USE_MAT_VEC) && !defined(GTE_USE_VEC_MAT)) +#error Exactly one multiplication convention must be specified. +#endif + +// ---------------------------------------------- +// GteLogger.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// Uncomment this to turn off the logging system. The macros LogAssert, +// LogError, LogWarning, and LogInformation expand to nothing. (Do this for +// optimal performance.) +//#define GTE_NO_LOGGER + +namespace houdini::gte +{ + +class GTE_IMPEXP Logger +{ +public: + // Construction. The Logger object is designed to exist only for a + // single-line call. A string is generated from the input parameters and + // is used for reporting. + Logger(char const* file, char const* function, int line, + std::string const& message); + + // Notify current listeners about the logged information. + void Assertion(); + void Error(); + void Warning(); + void Information(); + + // Listeners subscribe to Logger to receive message strings. + class Listener + { + public: + enum + { + LISTEN_FOR_NOTHING = 0x00000000, + LISTEN_FOR_ASSERTION = 0x00000001, + LISTEN_FOR_ERROR = 0x00000002, + LISTEN_FOR_WARNING = 0x00000004, + LISTEN_FOR_INFORMATION = 0x00000008, + LISTEN_FOR_ALL = 0xFFFFFFFF + }; + + // Construction and destruction. + virtual ~Listener(); + Listener(int flags = LISTEN_FOR_NOTHING); + + // What the listener wants to hear. + int GetFlags() const; + + // Handlers for the messages received from the logger. + void Assertion(std::string const& message); + void Error(std::string const& message); + void Warning(std::string const& message); + void Information(std::string const& message); + + private: + virtual void Report(std::string const& message); + + int mFlags; + }; + + static void Subscribe(Listener* listener); + static void Unsubscribe(Listener* listener); + +private: + std::string mMessage; + + static std::mutex msMutex; + static std::set msListeners; +}; + +} + + +#if !defined(GTE_NO_LOGGER) + +#define LogAssert(condition, message) \ + if (!(condition)) \ + { \ + houdini::gte::Logger(__FILE__, __FUNCTION__, __LINE__, message).Assertion(); \ + } + +#define LogError(message) \ + houdini::gte::Logger(__FILE__, __FUNCTION__, __LINE__, message).Error() + +#define LogWarning(message) \ + houdini::gte::Logger(__FILE__, __FUNCTION__, __LINE__, message).Warning() + +#define LogInformation(message) \ + houdini::gte::Logger(__FILE__, __FUNCTION__, __LINE__, message).Information() + +#else + +// No logging of assertions, warnings, errors, or information. +#define LogAssert(condition, message) +#define LogError(message) +#define LogWarning(message) +#define LogInformation(message) + +#endif + + + +// ---------------------------------------------- +// GteVector.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.3 (2018/10/05) + +namespace houdini::gte +{ + +template +class Vector +{ +public: + // The tuple is uninitialized. + Vector(); + + // The tuple is fully initialized by the inputs. + Vector(std::array const& values); + + // At most N elements are copied from the initializer list, setting any + // remaining elements to zero. Create the zero vector using the syntax + // Vector zero{(Real)0}; + // WARNING: The C++ 11 specification states that + // Vector zero{}; + // will lead to a call of the default constructor, not the initializer + // constructor! + Vector(std::initializer_list values); + + // For 0 <= d < N, element d is 1 and all others are 0. If d is invalid, + // the zero vector is created. This is a convenience for creating the + // standard Euclidean basis vectors; see also MakeUnit(int) and Unit(int). + Vector(int d); + + // The copy constructor, destructor, and assignment operator are generated + // by the compiler. + + // Member access. The first operator[] returns a const reference rather + // than a Real value. This supports writing via standard file operations + // that require a const pointer to data. + inline int GetSize() const; // returns N + inline Real const& operator[](int i) const; + inline Real& operator[](int i); + + // Comparisons for sorted containers and geometric ordering. + inline bool operator==(Vector const& vec) const; + inline bool operator!=(Vector const& vec) const; + inline bool operator< (Vector const& vec) const; + inline bool operator<=(Vector const& vec) const; + inline bool operator> (Vector const& vec) const; + inline bool operator>=(Vector const& vec) const; + + // Special vectors. + void MakeZero(); // All components are 0. + void MakeUnit(int d); // Component d is 1, all others are zero. + static Vector Zero(); + static Vector Unit(int d); + +protected: + // This data structure takes advantage of the built-in operator[], + // range checking, and visualizers in MSVS. + std::array mTuple; +}; + +// Unary operations. +template +Vector operator+(Vector const& v); + +template +Vector operator-(Vector const& v); + +// Linear-algebraic operations. +template +Vector +operator+(Vector const& v0, Vector const& v1); + +template +Vector +operator-(Vector const& v0, Vector const& v1); + +template +Vector operator*(Vector const& v, Real scalar); + +template +Vector operator*(Real scalar, Vector const& v); + +template +Vector operator/(Vector const& v, Real scalar); + +template +Vector& operator+=(Vector& v0, Vector const& v1); + +template +Vector& operator-=(Vector& v0, Vector const& v1); + +template +Vector& operator*=(Vector& v, Real scalar); + +template +Vector& operator/=(Vector& v, Real scalar); + +// Componentwise algebraic operations. +template +Vector +operator*(Vector const& v0, Vector const& v1); + +template +Vector +operator/(Vector const& v0, Vector const& v1); + +template +Vector& operator*=(Vector& v0, Vector const& v1); + +template +Vector& operator/=(Vector& v0, Vector const& v1); + +// Geometric operations. The functions with 'robust' set to 'false' use the +// standard algorithm for normalizing a vector by computing the length as a +// square root of the squared length and dividing by it. The results can be +// infinite (or NaN) if the length is zero. When 'robust' is set to 'true', +// the algorithm is designed to avoid floating-point overflow and sets the +// normalized vector to zero when the length is zero. +template +Real Dot(Vector const& v0, Vector const& v1); + +template +Real Length(Vector const& v, bool robust = false); + +template +Real Normalize(Vector& v, bool robust = false); + +// Gram-Schmidt orthonormalization to generate orthonormal vectors from the +// linearly independent inputs. The function returns the smallest length of +// the unnormalized vectors computed during the process. If this value is +// nearly zero, it is possible that the inputs are linearly dependent (within +// numerical round-off errors). On input, 1 <= numElements <= N and v[0] +// through v[numElements-1] must be initialized. On output, the vectors +// v[0] through v[numElements-1] form an orthonormal set. +template +Real Orthonormalize(int numElements, Vector* v, bool robust = false); + +// Construct a single vector orthogonal to the nonzero input vector. If the +// maximum absolute component occurs at index i, then the orthogonal vector +// U has u[i] = v[i+1], u[i+1] = -v[i], and all other components zero. The +// index addition i+1 is computed modulo N. +template +Vector GetOrthogonal(Vector const& v, bool unitLength); + +// Compute the axis-aligned bounding box of the vectors. The return value is +// 'true' iff the inputs are valid, in which case vmin and vmax have valid +// values. +template +bool ComputeExtremes(int numVectors, Vector const* v, + Vector& vmin, Vector& vmax); + +// Lift n-tuple v to homogeneous (n+1)-tuple (v,last). +template +Vector HLift(Vector const& v, Real last); + +// Project homogeneous n-tuple v = (u,v[n-1]) to (n-1)-tuple u. +template +Vector HProject(Vector const& v); + +// Lift n-tuple v = (w0,w1) to (n+1)-tuple u = (w0,u[inject],w1). By +// inference, w0 is a (inject)-tuple [nonexistent when inject=0] and w1 is a +// (n-inject)-tuple [nonexistent when inject=n]. +template +Vector Lift(Vector const& v, int inject, Real value); + +// Project n-tuple v = (w0,v[reject],w1) to (n-1)-tuple u = (w0,w1). By +// inference, w0 is a (reject)-tuple [nonexistent when reject=0] and w1 is a +// (n-1-reject)-tuple [nonexistent when reject=n-1]. +template +Vector Project(Vector const& v, int reject); + + +template +Vector::Vector() +{ + // Uninitialized. +} + +template +Vector::Vector(std::array const& values) + : + mTuple(values) +{ +} +template +Vector::Vector(std::initializer_list values) +{ + int const numValues = static_cast(values.size()); + if (N == numValues) + { + std::copy(values.begin(), values.end(), mTuple.begin()); + } + else if (N > numValues) + { + std::copy(values.begin(), values.end(), mTuple.begin()); + std::fill(mTuple.begin() + numValues, mTuple.end(), (Real)0); + } + else // N < numValues + { + std::copy(values.begin(), values.begin() + N, mTuple.begin()); + } +} + +template +Vector::Vector(int d) +{ + MakeUnit(d); +} + +template inline +int Vector::GetSize() const +{ + return N; +} + +template inline +Real const& Vector::operator[](int i) const +{ + return mTuple[i]; +} + +template inline +Real& Vector::operator[](int i) +{ + return mTuple[i]; +} + +template inline +bool Vector::operator==(Vector const& vec) const +{ + return mTuple == vec.mTuple; +} + +template inline +bool Vector::operator!=(Vector const& vec) const +{ + return mTuple != vec.mTuple; +} + +template inline +bool Vector::operator<(Vector const& vec) const +{ + return mTuple < vec.mTuple; +} + +template inline +bool Vector::operator<=(Vector const& vec) const +{ + return mTuple <= vec.mTuple; +} + +template inline +bool Vector::operator>(Vector const& vec) const +{ + return mTuple > vec.mTuple; +} + +template inline +bool Vector::operator>=(Vector const& vec) const +{ + return mTuple >= vec.mTuple; +} + +template +void Vector::MakeZero() +{ + std::fill(mTuple.begin(), mTuple.end(), (Real)0); +} + +template +void Vector::MakeUnit(int d) +{ + std::fill(mTuple.begin(), mTuple.end(), (Real)0); + if (0 <= d && d < N) + { + mTuple[d] = (Real)1; + } +} + +template +Vector Vector::Zero() +{ + Vector v; + v.MakeZero(); + return v; +} + +template +Vector Vector::Unit(int d) +{ + Vector v; + v.MakeUnit(d); + return v; +} + + + +template +Vector operator+(Vector const& v) +{ + return v; +} + +template +Vector operator-(Vector const& v) +{ + Vector result; + for (int i = 0; i < N; ++i) + { + result[i] = -v[i]; + } + return result; +} + +template +Vector operator+(Vector const& v0, + Vector const& v1) +{ + Vector result = v0; + return result += v1; +} + +template +Vector operator-(Vector const& v0, + Vector const& v1) +{ + Vector result = v0; + return result -= v1; +} + +template +Vector operator*(Vector const& v, Real scalar) +{ + Vector result = v; + return result *= scalar; +} + +template +Vector operator*(Real scalar, Vector const& v) +{ + Vector result = v; + return result *= scalar; +} + +template +Vector operator/(Vector const& v, Real scalar) +{ + Vector result = v; + return result /= scalar; +} + +template +Vector& operator+=(Vector& v0, Vector const& v1) +{ + for (int i = 0; i < N; ++i) + { + v0[i] += v1[i]; + } + return v0; +} + +template +Vector& operator-=(Vector& v0, Vector const& v1) +{ + for (int i = 0; i < N; ++i) + { + v0[i] -= v1[i]; + } + return v0; +} + +template +Vector& operator*=(Vector& v, Real scalar) +{ + for (int i = 0; i < N; ++i) + { + v[i] *= scalar; + } + return v; +} + +template +Vector& operator/=(Vector& v, Real scalar) +{ + if (scalar != (Real)0) + { + Real invScalar = ((Real)1) / scalar; + for (int i = 0; i < N; ++i) + { + v[i] *= invScalar; + } + } + else + { + for (int i = 0; i < N; ++i) + { + v[i] = (Real)0; + } + } + return v; +} + +template +Vector operator*(Vector const& v0, + Vector const& v1) +{ + Vector result = v0; + return result *= v1; +} + +template +Vector operator/(Vector const& v0, + Vector const& v1) +{ + Vector result = v0; + return result /= v1; +} + +template +Vector& operator*=(Vector& v0, Vector const& v1) +{ + for (int i = 0; i < N; ++i) + { + v0[i] *= v1[i]; + } + return v0; +} + +template +Vector& operator/=(Vector& v0, Vector const& v1) +{ + for (int i = 0; i < N; ++i) + { + v0[i] /= v1[i]; + } + return v0; +} + +template +Real Dot(Vector const& v0, Vector const& v1) +{ + Real dot = v0[0] * v1[0]; + for (int i = 1; i < N; ++i) + { + dot += v0[i] * v1[i]; + } + return dot; +} + +template +Real Length(Vector const& v, bool robust) +{ + if (robust) + { + Real maxAbsComp = std::abs(v[0]); + for (int i = 1; i < N; ++i) + { + Real absComp = std::abs(v[i]); + if (absComp > maxAbsComp) + { + maxAbsComp = absComp; + } + } + + Real length; + if (maxAbsComp > (Real)0) + { + Vector scaled = v / maxAbsComp; + length = maxAbsComp * std::sqrt(Dot(scaled, scaled)); + } + else + { + length = (Real)0; + } + return length; + } + else + { + return std::sqrt(Dot(v, v)); + } +} + +template +Real Normalize(Vector& v, bool robust) +{ + if (robust) + { + Real maxAbsComp = std::abs(v[0]); + for (int i = 1; i < N; ++i) + { + Real absComp = std::abs(v[i]); + if (absComp > maxAbsComp) + { + maxAbsComp = absComp; + } + } + + Real length; + if (maxAbsComp > (Real)0) + { + v /= maxAbsComp; + length = std::sqrt(Dot(v, v)); + v /= length; + length *= maxAbsComp; + } + else + { + length = (Real)0; + for (int i = 0; i < N; ++i) + { + v[i] = (Real)0; + } + } + return length; + } + else + { + Real length = std::sqrt(Dot(v, v)); + if (length > (Real)0) + { + v /= length; + } + else + { + for (int i = 0; i < N; ++i) + { + v[i] = (Real)0; + } + } + return length; + } +} + +template +Real Orthonormalize(int numInputs, Vector* v, bool robust) +{ + if (v && 1 <= numInputs && numInputs <= N) + { + Real minLength = Normalize(v[0], robust); + for (int i = 1; i < numInputs; ++i) + { + for (int j = 0; j < i; ++j) + { + Real dot = Dot(v[i], v[j]); + v[i] -= v[j] * dot; + } + Real length = Normalize(v[i], robust); + if (length < minLength) + { + minLength = length; + } + } + return minLength; + } + + return (Real)0; +} + +template +Vector GetOrthogonal(Vector const& v, bool unitLength) +{ + Real cmax = std::abs(v[0]); + int imax = 0; + for (int i = 1; i < N; ++i) + { + Real c = std::abs(v[i]); + if (c > cmax) + { + cmax = c; + imax = i; + } + } + + Vector result; + result.MakeZero(); + int inext = imax + 1; + if (inext == N) + { + inext = 0; + } + result[imax] = v[inext]; + result[inext] = -v[imax]; + if (unitLength) + { + Real sqrDistance = result[imax] * result[imax] + result[inext] * result[inext]; + Real invLength = ((Real)1) / std::sqrt(sqrDistance); + result[imax] *= invLength; + result[inext] *= invLength; + } + return result; +} + +template +bool ComputeExtremes(int numVectors, Vector const* v, + Vector& vmin, Vector& vmax) +{ + if (v && numVectors > 0) + { + vmin = v[0]; + vmax = vmin; + for (int j = 1; j < numVectors; ++j) + { + Vector const& vec = v[j]; + for (int i = 0; i < N; ++i) + { + if (vec[i] < vmin[i]) + { + vmin[i] = vec[i]; + } + else if (vec[i] > vmax[i]) + { + vmax[i] = vec[i]; + } + } + } + return true; + } + + return false; +} + +template +Vector HLift(Vector const& v, Real last) +{ + Vector result; + for (int i = 0; i < N; ++i) + { + result[i] = v[i]; + } + result[N] = last; + return result; +} + +template +Vector HProject(Vector const& v) +{ + static_assert(N >= 2, "Invalid dimension."); + Vector result; + for (int i = 0; i < N - 1; ++i) + { + result[i] = v[i]; + } + return result; +} + +template +Vector Lift(Vector const& v, int inject, Real value) +{ + Vector result; + int i; + for (i = 0; i < inject; ++i) + { + result[i] = v[i]; + } + result[i] = value; + int j = i; + for (++j; i < N; ++i, ++j) + { + result[j] = v[i]; + } + return result; +} + +template +Vector Project(Vector const& v, int reject) +{ + static_assert(N >= 2, "Invalid dimension."); + Vector result; + for (int i = 0, j = 0; i < N - 1; ++i, ++j) + { + if (j == reject) + { + ++j; + } + result[i] = v[j]; + } + return result; +} + +} + +// ---------------------------------------------- +// GteVector3.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.2 (2018/10/04) + +namespace houdini::gte +{ + +// Template alias for convenience. +template +using Vector3 = Vector<3, Real>; + +// Cross, UnitCross, and DotCross have a template parameter N that should +// be 3 or 4. The latter case supports affine vectors in 4D (last component +// w = 0) when you want to use 4-tuples and 4x4 matrices for affine algebra. + +// Compute the cross product using the formal determinant: +// cross = det{{e0,e1,e2},{x0,x1,x2},{y0,y1,y2}} +// = (x1*y2-x2*y1, x2*y0-x0*y2, x0*y1-x1*y0) +// where e0 = (1,0,0), e1 = (0,1,0), e2 = (0,0,1), v0 = (x0,x1,x2), and +// v1 = (y0,y1,y2). +template +Vector Cross(Vector const& v0, Vector const& v1); + +// Compute the normalized cross product. +template +Vector UnitCross(Vector const& v0, Vector const& v1, + bool robust = false); + +// Compute Dot((x0,x1,x2),Cross((y0,y1,y2),(z0,z1,z2)), the triple scalar +// product of three vectors, where v0 = (x0,x1,x2), v1 = (y0,y1,y2), and +// v2 is (z0,z1,z2). +template +Real DotCross(Vector const& v0, Vector const& v1, + Vector const& v2); + +// Compute a right-handed orthonormal basis for the orthogonal complement +// of the input vectors. The function returns the smallest length of the +// unnormalized vectors computed during the process. If this value is nearly +// zero, it is possible that the inputs are linearly dependent (within +// numerical round-off errors). On input, numInputs must be 1 or 2 and +// v[0] through v[numInputs-1] must be initialized. On output, the +// vectors v[0] through v[2] form an orthonormal set. +template +Real ComputeOrthogonalComplement(int numInputs, Vector3* v, bool robust = false); + +// Compute the barycentric coordinates of the point P with respect to the +// tetrahedron , P = b0*V0 + b1*V1 + b2*V2 + b3*V3, where +// b0 + b1 + b2 + b3 = 1. The return value is 'true' iff {V0,V1,V2,V3} is +// a linearly independent set. Numerically, this is measured by +// |det[V0 V1 V2 V3]| <= epsilon. The values bary[] are valid only when the +// return value is 'true' but set to zero when the return value is 'false'. +template +bool ComputeBarycentrics(Vector3 const& p, Vector3 const& v0, + Vector3 const& v1, Vector3 const& v2, Vector3 const& v3, + Real bary[4], Real epsilon = (Real)0); + +// Get intrinsic information about the input array of vectors. The return +// value is 'true' iff the inputs are valid (numVectors > 0, v is not null, +// and epsilon >= 0), in which case the class members are valid. +template +class IntrinsicsVector3 +{ +public: + // The constructor sets the class members based on the input set. + IntrinsicsVector3(int numVectors, Vector3 const* v, Real inEpsilon); + + // A nonnegative tolerance that is used to determine the intrinsic + // dimension of the set. + Real epsilon; + + // The intrinsic dimension of the input set, computed based on the + // nonnegative tolerance mEpsilon. + int dimension; + + // Axis-aligned bounding box of the input set. The maximum range is + // the larger of max[0]-min[0], max[1]-min[1], and max[2]-min[2]. + Real min[3], max[3]; + Real maxRange; + + // Coordinate system. The origin is valid for any dimension d. The + // unit-length direction vector is valid only for 0 <= i < d. The + // extreme index is relative to the array of input points, and is also + // valid only for 0 <= i < d. If d = 0, all points are effectively + // the same, but the use of an epsilon may lead to an extreme index + // that is not zero. If d = 1, all points effectively lie on a line + // segment. If d = 2, all points effectively line on a plane. If + // d = 3, the points are not coplanar. + Vector3 origin; + Vector3 direction[3]; + + // The indices that define the maximum dimensional extents. The + // values extreme[0] and extreme[1] are the indices for the points + // that define the largest extent in one of the coordinate axis + // directions. If the dimension is 2, then extreme[2] is the index + // for the point that generates the largest extent in the direction + // perpendicular to the line through the points corresponding to + // extreme[0] and extreme[1]. Furthermore, if the dimension is 3, + // then extreme[3] is the index for the point that generates the + // largest extent in the direction perpendicular to the triangle + // defined by the other extreme points. The tetrahedron formed by the + // points V[extreme[0]], V[extreme[1]], V[extreme[2]], and + // V[extreme[3]] is clockwise or counterclockwise, the condition + // stored in extremeCCW. + int extreme[4]; + bool extremeCCW; +}; + + +template +Vector Cross(Vector const& v0, Vector const& v1) +{ + static_assert(N == 3 || N == 4, "Dimension must be 3 or 4."); + + Vector result; + result.MakeZero(); + result[0] = v0[1] * v1[2] - v0[2] * v1[1]; + result[1] = v0[2] * v1[0] - v0[0] * v1[2]; + result[2] = v0[0] * v1[1] - v0[1] * v1[0]; + return result; +} + +template +Vector UnitCross(Vector const& v0, Vector const& v1, bool robust) +{ + static_assert(N == 3 || N == 4, "Dimension must be 3 or 4."); + + Vector unitCross = Cross(v0, v1); + Normalize(unitCross, robust); + return unitCross; +} + +template +Real DotCross(Vector const& v0, Vector const& v1, + Vector const& v2) +{ + static_assert(N == 3 || N == 4, "Dimension must be 3 or 4."); + + return Dot(v0, Cross(v1, v2)); +} + +template +Real ComputeOrthogonalComplement(int numInputs, Vector3* v, bool robust) +{ + if (numInputs == 1) + { + if (std::abs(v[0][0]) > std::abs(v[0][1])) + { + v[1] = { -v[0][2], (Real)0, +v[0][0] }; + } + else + { + v[1] = { (Real)0, +v[0][2], -v[0][1] }; + } + numInputs = 2; + } + + if (numInputs == 2) + { + v[2] = Cross(v[0], v[1]); + return Orthonormalize<3, Real>(3, v, robust); + } + + return (Real)0; +} + +template +bool ComputeBarycentrics(Vector3 const& p, Vector3 const& v0, + Vector3 const& v1, Vector3 const& v2, Vector3 const& v3, + Real bary[4], Real epsilon) +{ + // Compute the vectors relative to V3 of the tetrahedron. + Vector3 diff[4] = { v0 - v3, v1 - v3, v2 - v3, p - v3 }; + + Real det = DotCross(diff[0], diff[1], diff[2]); + if (det < -epsilon || det > epsilon) + { + Real invDet = ((Real)1) / det; + bary[0] = DotCross(diff[3], diff[1], diff[2]) * invDet; + bary[1] = DotCross(diff[3], diff[2], diff[0]) * invDet; + bary[2] = DotCross(diff[3], diff[0], diff[1]) * invDet; + bary[3] = (Real)1 - bary[0] - bary[1] - bary[2]; + return true; + } + + for (int i = 0; i < 4; ++i) + { + bary[i] = (Real)0; + } + return false; +} + +template +IntrinsicsVector3::IntrinsicsVector3(int numVectors, + Vector3 const* v, Real inEpsilon) + : + epsilon(inEpsilon), + dimension(0), + maxRange((Real)0), + origin({ (Real)0, (Real)0, (Real)0 }), + extremeCCW(false) +{ + min[0] = (Real)0; + min[1] = (Real)0; + min[2] = (Real)0; + direction[0] = { (Real)0, (Real)0, (Real)0 }; + direction[1] = { (Real)0, (Real)0, (Real)0 }; + direction[2] = { (Real)0, (Real)0, (Real)0 }; + extreme[0] = 0; + extreme[1] = 0; + extreme[2] = 0; + extreme[3] = 0; + + if (numVectors > 0 && v && epsilon >= (Real)0) + { + // Compute the axis-aligned bounding box for the input vectors. Keep + // track of the indices into 'vectors' for the current min and max. + int j, indexMin[3], indexMax[3]; + for (j = 0; j < 3; ++j) + { + min[j] = v[0][j]; + max[j] = min[j]; + indexMin[j] = 0; + indexMax[j] = 0; + } + + int i; + for (i = 1; i < numVectors; ++i) + { + for (j = 0; j < 3; ++j) + { + if (v[i][j] < min[j]) + { + min[j] = v[i][j]; + indexMin[j] = i; + } + else if (v[i][j] > max[j]) + { + max[j] = v[i][j]; + indexMax[j] = i; + } + } + } + + // Determine the maximum range for the bounding box. + maxRange = max[0] - min[0]; + extreme[0] = indexMin[0]; + extreme[1] = indexMax[0]; + Real range = max[1] - min[1]; + if (range > maxRange) + { + maxRange = range; + extreme[0] = indexMin[1]; + extreme[1] = indexMax[1]; + } + range = max[2] - min[2]; + if (range > maxRange) + { + maxRange = range; + extreme[0] = indexMin[2]; + extreme[1] = indexMax[2]; + } + + // The origin is either the vector of minimum x0-value, vector of + // minimum x1-value, or vector of minimum x2-value. + origin = v[extreme[0]]; + + // Test whether the vector set is (nearly) a vector. + if (maxRange <= epsilon) + { + dimension = 0; + for (j = 0; j < 3; ++j) + { + extreme[j + 1] = extreme[0]; + } + return; + } + + // Test whether the vector set is (nearly) a line segment. We need + // {direction[2],direction[3]} to span the orthogonal complement of + // direction[0]. + direction[0] = v[extreme[1]] - origin; + Normalize(direction[0], false); + if (std::abs(direction[0][0]) > std::abs(direction[0][1])) + { + direction[1][0] = -direction[0][2]; + direction[1][1] = (Real)0; + direction[1][2] = +direction[0][0]; + } + else + { + direction[1][0] = (Real)0; + direction[1][1] = +direction[0][2]; + direction[1][2] = -direction[0][1]; + } + Normalize(direction[1], false); + direction[2] = Cross(direction[0], direction[1]); + + // Compute the maximum distance of the points from the line + // origin+t*direction[0]. + Real maxDistance = (Real)0; + Real distance, dot; + extreme[2] = extreme[0]; + for (i = 0; i < numVectors; ++i) + { + Vector3 diff = v[i] - origin; + dot = Dot(direction[0], diff); + Vector3 proj = diff - dot * direction[0]; + distance = Length(proj, false); + if (distance > maxDistance) + { + maxDistance = distance; + extreme[2] = i; + } + } + + if (maxDistance <= epsilon*maxRange) + { + // The points are (nearly) on the line origin+t*direction[0]. + dimension = 1; + extreme[2] = extreme[1]; + extreme[3] = extreme[1]; + return; + } + + // Test whether the vector set is (nearly) a planar polygon. The + // point v[extreme[2]] is farthest from the line: origin + + // t*direction[0]. The vector v[extreme[2]]-origin is not + // necessarily perpendicular to direction[0], so project out the + // direction[0] component so that the result is perpendicular to + // direction[0]. + direction[1] = v[extreme[2]] - origin; + dot = Dot(direction[0], direction[1]); + direction[1] -= dot * direction[0]; + Normalize(direction[1], false); + + // We need direction[2] to span the orthogonal complement of + // {direction[0],direction[1]}. + direction[2] = Cross(direction[0], direction[1]); + + // Compute the maximum distance of the points from the plane + // origin+t0*direction[0]+t1*direction[1]. + maxDistance = (Real)0; + Real maxSign = (Real)0; + extreme[3] = extreme[0]; + for (i = 0; i < numVectors; ++i) + { + Vector3 diff = v[i] - origin; + distance = Dot(direction[2], diff); + Real sign = (distance >(Real)0 ? (Real)1 : + (distance < (Real)0 ? (Real)-1 : (Real)0)); + distance = std::abs(distance); + if (distance > maxDistance) + { + maxDistance = distance; + maxSign = sign; + extreme[3] = i; + } + } + + if (maxDistance <= epsilon * maxRange) + { + // The points are (nearly) on the plane origin+t0*direction[0] + // +t1*direction[1]. + dimension = 2; + extreme[3] = extreme[2]; + return; + } + + dimension = 3; + extremeCCW = (maxSign > (Real)0); + return; + } +} + +} + +// ---------------------------------------------- +// GteHypersphere.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// The hypersphere is represented as |X-C| = R where C is the center and R is +// the radius. The hypersphere is a circle for dimension 2 or a sphere for +// dimension 3. + +namespace houdini::gte +{ + +template +class Hypersphere +{ +public: + // Construction and destruction. The default constructor sets the center + // to (0,...,0) and the radius to 1. + Hypersphere(); + Hypersphere(Vector const& inCenter, Real inRadius); + + // Public member access. + Vector center; + Real radius; + +public: + // Comparisons to support sorted containers. + bool operator==(Hypersphere const& hypersphere) const; + bool operator!=(Hypersphere const& hypersphere) const; + bool operator< (Hypersphere const& hypersphere) const; + bool operator<=(Hypersphere const& hypersphere) const; + bool operator> (Hypersphere const& hypersphere) const; + bool operator>=(Hypersphere const& hypersphere) const; +}; + +// Template aliases for convenience. +template +using Circle2 = Hypersphere<2, Real>; + +template +using Sphere3 = Hypersphere<3, Real>; + + +template +Hypersphere::Hypersphere() + : + radius((Real)1) +{ + center.MakeZero(); +} + +template +Hypersphere::Hypersphere(Vector const& inCenter, + Real inRadius) + : + center(inCenter), + radius(inRadius) +{ +} + +template +bool Hypersphere::operator==(Hypersphere const& hypersphere) const +{ + return center == hypersphere.center && radius == hypersphere.radius; +} + +template +bool Hypersphere::operator!=(Hypersphere const& hypersphere) const +{ + return !operator==(hypersphere); +} + +template +bool Hypersphere::operator<(Hypersphere const& hypersphere) const +{ + if (center < hypersphere.center) + { + return true; + } + + if (center > hypersphere.center) + { + return false; + } + + return radius < hypersphere.radius; +} + +template +bool Hypersphere::operator<=(Hypersphere const& hypersphere) const +{ + return operator<(hypersphere) || operator==(hypersphere); +} + +template +bool Hypersphere::operator>(Hypersphere const& hypersphere) const +{ + return !operator<=(hypersphere); +} + +template +bool Hypersphere::operator>=(Hypersphere const& hypersphere) const +{ + return !operator<(hypersphere); +} + + +} + + +// ---------------------------------------------- +// GteOrientedBox.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// A box has center C, axis directions U[i], and extents e[i]. The set +// {U[0],...,U[N-1]} is orthonormal, which means the vectors are +// unit-length and mutually perpendicular. The extents are nonnegative; +// zero is allowed, meaning the box is degenerate in the corresponding +// direction. A point X is represented in box coordinates by +// X = C + y[0]*U[0] + y[1]*U[1]. This point is inside or on the +// box whenever |y[i]| <= e[i] for all i. + +namespace houdini::gte +{ + +template +class OrientedBox +{ +public: + // Construction and destruction. The default constructor sets the center + // to (0,...,0), axis d to Vector::Unit(d), and extent d to +1. + OrientedBox(); + OrientedBox(Vector const& inCenter, + std::array, N> const& inAxis, + Vector const& inExtent); + + // Compute the vertices of the box. If index i has the bit pattern + // i = b[N-1]...b[0], then + // vertex[i] = center + sum_{d=0}^{N-1} sign[d] * extent[d] * axis[d] + // where sign[d] = 2*b[d] - 1. + void GetVertices(std::array, (1 << N)>& vertex) const; + + // Public member access. It is required that extent[i] >= 0. + Vector center; + std::array, N> axis; + Vector extent; + +public: + // Comparisons to support sorted containers. + bool operator==(OrientedBox const& box) const; + bool operator!=(OrientedBox const& box) const; + bool operator< (OrientedBox const& box) const; + bool operator<=(OrientedBox const& box) const; + bool operator> (OrientedBox const& box) const; + bool operator>=(OrientedBox const& box) const; +}; + +// Template aliases for convenience. +template +using OrientedBox2 = OrientedBox<2, Real>; + +template +using OrientedBox3 = OrientedBox<3, Real>; + + +template +OrientedBox::OrientedBox() +{ + center.MakeZero(); + for (int i = 0; i < N; ++i) + { + axis[i].MakeUnit(i); + extent[i] = (Real)1; + } +} + +template +OrientedBox::OrientedBox(Vector const& inCenter, + std::array, N> const& inAxis, + Vector const& inExtent) + : + center(inCenter), + axis(inAxis), + extent(inExtent) +{ +} + +template +void OrientedBox::GetVertices( + std::array, (1 << N)>& vertex) const +{ + unsigned int const dsup = static_cast(N); + std::array, N> product; + for (unsigned int d = 0; d < dsup; ++d) + { + product[d] = extent[d] * axis[d]; + } + + int const imax = (1 << N); + for (int i = 0; i < imax; ++i) + { + vertex[i] = center; + for (unsigned int d = 0, mask = 1; d < dsup; ++d, mask <<= 1) + { + Real sign = (i & mask ? (Real)1 : (Real)-1); + vertex[i] += sign * product[d]; + } + } +} + +template +bool OrientedBox::operator==(OrientedBox const& box) const +{ + return center == box.center && axis == box.axis && extent == box.extent; +} + +template +bool OrientedBox::operator!=(OrientedBox const& box) const +{ + return !operator==(box); +} + +template +bool OrientedBox::operator<(OrientedBox const& box) const +{ + if (center < box.center) + { + return true; + } + + if (center > box.center) + { + return false; + } + + if (axis < box.axis) + { + return true; + } + + if (axis > box.axis) + { + return false; + } + + return extent < box.extent; +} + +template +bool OrientedBox::operator<=(OrientedBox const& box) const +{ + return operator<(box) || operator==(box); +} + +template +bool OrientedBox::operator>(OrientedBox const& box) const +{ + return !operator<=(box); +} + +template +bool OrientedBox::operator>=(OrientedBox const& box) const +{ + return !operator<(box); +} + + +} + +// ---------------------------------------------- +// GteFeatureKey.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +namespace houdini::gte +{ + +template +class FeatureKey +{ +protected: + // Abstract base class with V[] uninitialized. The derived classes must + // set the V[] values accordingly. + // + // An ordered feature key has V[0] = min(V[]) with (V[0],V[1],...,V[N-1]) a + // permutation of N inputs with an even number of transpositions. + // + // An unordered feature key has V[0] < V[1] < ... < V[N-1]. + // + // Note that the word 'order' is about the geometry of the feature, not + // the comparison order for any sorting. + FeatureKey(); + +public: + bool operator<(FeatureKey const& key) const; + bool operator==(FeatureKey const& key) const; + + int V[N]; +}; + + +template +FeatureKey::FeatureKey() +{ +} + +template +bool FeatureKey::operator<(FeatureKey const& key) const +{ + for (int i = N - 1; i >= 0; --i) + { + if (V[i] < key.V[i]) + { + return true; + } + + if (V[i] > key.V[i]) + { + return false; + } + } + return false; +} + +template +bool FeatureKey::operator==(FeatureKey const& key) const +{ + for (int i = 0; i < N; ++i) + { + if (V[i] != key.V[i]) + { + return false; + } + } + return true; +} + + +} + + +// ---------------------------------------------- +// GteEdgeKey.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.1 (2018/05/22) + +namespace houdini::gte +{ + template + class EdgeKey : public FeatureKey<2, Ordered> + { + public: + // An ordered edge has (V[0],V[1]) = (v0,v1). An unordered edge has + // (V[0],V[1]) = (min(V[0],V[1]),max(V[0],V[1])). + EdgeKey(); // creates key (-1,-1) + explicit EdgeKey(int v0, int v1); + }; + + template<> + inline + EdgeKey::EdgeKey() + { + V[0] = -1; + V[1] = -1; + } + + template<> + inline + EdgeKey::EdgeKey(int v0, int v1) + { + V[0] = v0; + V[1] = v1; + } + + template<> + inline + EdgeKey::EdgeKey() + { + V[0] = -1; + V[1] = -1; + } + + template<> + inline + EdgeKey::EdgeKey(int v0, int v1) + { + if (v0 < v1) + { + // v0 is minimum + V[0] = v0; + V[1] = v1; + } + else + { + // v1 is minimum + V[0] = v1; + V[1] = v0; + } + } +} + + +// ---------------------------------------------- +// GteTriangleKey.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.1 (2018/05/22) + +namespace houdini::gte +{ + template + class TriangleKey : public FeatureKey<3, Ordered> + { + public: + // An ordered triangle has V[0] = min(v0,v1,v2). Choose + // (V[0],V[1],V[2]) to be a permutation of (v0,v1,v2) so that the + // final storage is one of + // (v0,v1,v2), (v1,v2,v0), (v2,v0,v1) + // The idea is that if v0 corresponds to (1,0,0), v1 corresponds to + // (0,1,0), and v2 corresponds to (0,0,1), the ordering (v0,v1,v2) + // corresponds to the 3x3 identity matrix I; the rows are the + // specified 3-tuples. The permutation (V[0],V[1],V[2]) induces a + // permutation of the rows of the identity matrix to form a + // permutation matrix P with det(P) = 1 = det(I). + // + // An unordered triangle stores a permutation of (v0,v1,v2) so that + // V[0] < V[1] < V[2]. + TriangleKey(); // creates key (-1,-1,-1) + explicit TriangleKey(int v0, int v1, int v2); + }; + + template<> + inline + TriangleKey::TriangleKey() + { + V[0] = -1; + V[1] = -1; + V[2] = -1; + } + + template<> + inline + TriangleKey::TriangleKey(int v0, int v1, int v2) + { + if (v0 < v1) + { + if (v0 < v2) + { + // v0 is minimum + V[0] = v0; + V[1] = v1; + V[2] = v2; + } + else + { + // v2 is minimum + V[0] = v2; + V[1] = v0; + V[2] = v1; + } + } + else + { + if (v1 < v2) + { + // v1 is minimum + V[0] = v1; + V[1] = v2; + V[2] = v0; + } + else + { + // v2 is minimum + V[0] = v2; + V[1] = v0; + V[2] = v1; + } + } + } + + template<> + inline + TriangleKey::TriangleKey() + { + V[0] = -1; + V[1] = -1; + V[2] = -1; + } + + template<> + inline + TriangleKey::TriangleKey(int v0, int v1, int v2) + { + if (v0 < v1) + { + if (v0 < v2) + { + // v0 is minimum + V[0] = v0; + V[1] = std::min(v1, v2); + V[2] = std::max(v1, v2); + } + else + { + // v2 is minimum + V[0] = v2; + V[1] = std::min(v0, v1); + V[2] = std::max(v0, v1); + } + } + else + { + if (v1 < v2) + { + // v1 is minimum + V[0] = v1; + V[1] = std::min(v2, v0); + V[2] = std::max(v2, v0); + } + else + { + // v2 is minimum + V[0] = v2; + V[1] = std::min(v0, v1); + V[2] = std::max(v0, v1); + } + } + } +} + + +// ---------------------------------------------- +// GteETManifoldMesh.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.1 (2017/01/02) + +namespace houdini::gte +{ + +class GTE_IMPEXP ETManifoldMesh +{ +public: + // Edge data types. + class Edge; + typedef std::shared_ptr (*ECreator)(int, int); + typedef std::map, std::shared_ptr> EMap; + + // Triangle data types. + class Triangle; + typedef std::shared_ptr (*TCreator)(int, int, int); + typedef std::map, std::shared_ptr> TMap; + + // Edge object. + class GTE_IMPEXP Edge + { + public: + virtual ~Edge(); + Edge(int v0, int v1); + + // Vertices of the edge. + int V[2]; + + // Triangles sharing the edge. + std::weak_ptr T[2]; + }; + + // Triangle object. + class GTE_IMPEXP Triangle + { + public: + virtual ~Triangle(); + Triangle(int v0, int v1, int v2); + + // Vertices, listed in counterclockwise order (V[0],V[1],V[2]). + int V[3]; + + // Adjacent edges. E[i] points to edge (V[i],V[(i+1)%3]). + std::weak_ptr E[3]; + + // Adjacent triangles. T[i] points to the adjacent triangle + // sharing edge E[i]. + std::weak_ptr T[3]; + }; + + + // Construction and destruction. + virtual ~ETManifoldMesh(); + ETManifoldMesh(ECreator eCreator = nullptr, TCreator tCreator = nullptr); + + // Support for a deep copy of the mesh. The mEMap and mTMap objects have + // dynamically allocated memory for edges and triangles. A shallow copy + // of the pointers to this memory is problematic. Allowing sharing, say, + // via std::shared_ptr, is an option but not really the intent of copying + // the mesh graph. + ETManifoldMesh(ETManifoldMesh const& mesh); + ETManifoldMesh& operator=(ETManifoldMesh const& mesh); + + // Member access. + EMap const& GetEdges() const; + TMap const& GetTriangles() const; + + // If the insertion of a triangle fails because the mesh would become + // nonmanifold, the default behavior is to trigger a LogError message. + // You can disable this behavior in situations where you want the Logger + // system on but you want to continue gracefully without an assertion. + // The return value is the previous value of the internal state + // mAssertOnNonmanifoldInsertion. + bool AssertOnNonmanifoldInsertion(bool doAssert); + + // If is not in the mesh, a Triangle object is created and + // returned; otherwise, is in the mesh and nullptr is returned. + // If the insertion leads to a nonmanifold mesh, the call fails with a + // nullptr returned. + virtual std::shared_ptr Insert(int v0, int v1, int v2); + + // If is in the mesh, it is removed and 'true' is returned; + // otherwise, is not in the mesh and 'false' is returned. + virtual bool Remove(int v0, int v1, int v2); + + // Destroy the edges and triangles to obtain an empty mesh. + virtual void Clear(); + + // A manifold mesh is closed if each edge is shared twice. A closed + // mesh is not necessarily oriented. For example, you could have a + // mesh with spherical topology. The upper hemisphere has outer-facing + // normals and the lower hemisphere has inner-facing normals. The + // discontinuity in orientation occurs on the circle shared by the + // hemispheres. + bool IsClosed() const; + + // Test whether all triangles in the mesh are oriented consistently and + // that no two triangles are coincident. The latter means that you + // cannot have both triangles and in the mesh to + // be considered oriented. + bool IsOriented() const; + + // Compute the connected components of the edge-triangle graph that the + // mesh represents. The first function returns pointers into 'this' + // object's containers, so you must consume the components before + // clearing or destroying 'this'. The second function returns triangle + // keys, which requires three times as much storage as the pointers but + // allows you to clear or destroy 'this' before consuming the components. + void GetComponents(std::vector>>& components) const; + void GetComponents(std::vector>>& components) const; + +protected: + // The edge data and default edge creation. + static std::shared_ptr CreateEdge(int v0, int v1); + ECreator mECreator; + EMap mEMap; + + // The triangle data and default triangle creation. + static std::shared_ptr CreateTriangle(int v0, int v1, int v2); + TCreator mTCreator; + TMap mTMap; + bool mAssertOnNonmanifoldInsertion; // default: true + + // Support for computing connected components. This is a straightforward + // depth-first search of the graph but uses a preallocated stack rather + // than a recursive function that could possibly overflow the call stack. + void DepthFirstSearch(std::shared_ptr const& tInitial, + std::map, int>& visited, + std::vector>& component) const; +}; + +} + +// ---------------------------------------------- +// GteLine.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// The line is represented by P+t*D, where P is an origin point, D is a +// unit-length direction vector, and t is any real number. The user must +// ensure that D is unit length. + +namespace houdini::gte +{ + +template +class Line +{ +public: + // Construction and destruction. The default constructor sets the origin + // to (0,...,0) and the line direction to (1,0,...,0). + Line(); + Line(Vector const& inOrigin, Vector const& inDirection); + + // Public member access. The direction must be unit length. + Vector origin, direction; + +public: + // Comparisons to support sorted containers. + bool operator==(Line const& line) const; + bool operator!=(Line const& line) const; + bool operator< (Line const& line) const; + bool operator<=(Line const& line) const; + bool operator> (Line const& line) const; + bool operator>=(Line const& line) const; +}; + +// Template aliases for convenience. +template +using Line2 = Line<2, Real>; + +template +using Line3 = Line<3, Real>; + + +template +Line::Line() +{ + origin.MakeZero(); + direction.MakeUnit(0); +} + +template +Line::Line(Vector const& inOrigin, + Vector const& inDirection) + : + origin(inOrigin), + direction(inDirection) +{ +} + +template +bool Line::operator==(Line const& line) const +{ + return origin == line.origin && direction == line.direction; +} + +template +bool Line::operator!=(Line const& line) const +{ + return !operator==(line); +} + +template +bool Line::operator<(Line const& line) const +{ + if (origin < line.origin) + { + return true; + } + + if (origin > line.origin) + { + return false; + } + + return direction < line.direction; +} + +template +bool Line::operator<=(Line const& line) const +{ + return operator<(line) || operator==(line); +} + +template +bool Line::operator>(Line const& line) const +{ + return !operator<=(line); +} + +template +bool Line::operator>=(Line const& line) const +{ + return !operator<(line); +} + + +} + + +// ---------------------------------------------- +// GtePrimalQuery3.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// Queries about the relation of a point to various geometric objects. The +// choices for N when using UIntegerFP32 for either BSNumber of BSRational +// are determined in GeometricTools/GTEngine/Tools/PrecisionCalculator. These +// N-values are worst case scenarios. Your specific input data might require +// much smaller N, in which case you can modify PrecisionCalculator to use the +// BSPrecision(int32_t,int32_t,int32_t,bool) constructors. + +namespace houdini::gte +{ + +template +class PrimalQuery3 +{ +public: + // The caller is responsible for ensuring that the array is not empty + // before calling queries and that the indices passed to the queries are + // valid. The class does no range checking. + PrimalQuery3(); + PrimalQuery3(int numVertices, Vector3 const* vertices); + + // Member access. + inline void Set(int numVertices, Vector3 const* vertices); + inline int GetNumVertices() const; + inline Vector3 const* GetVertices() const; + + // In the following, point P refers to vertices[i] or 'test' and Vi refers + // to vertices[vi]. + + // For a plane with origin V0 and normal N = Cross(V1-V0,V2-V0), ToPlane + // returns + // +1, P on positive side of plane (side to which N points) + // -1, P on negative side of plane (side to which -N points) + // 0, P on the plane + // + // Choice of N for UIntegerFP32. + // input type | compute type | N + // -----------+--------------+------ + // float | BSNumber | 27 + // double | BSNumber | 197 + // float | BSRational | 2882 + // double | BSRational | 21688 + int ToPlane(int i, int v0, int v1, int v2) const; + int ToPlane(Vector3 const& test, int v0, int v1, int v2) const; + + // For a tetrahedron with vertices ordered as described in the file + // GteTetrahedronKey.h, the function returns + // +1, P outside tetrahedron + // -1, P inside tetrahedron + // 0, P on tetrahedron + // + // Choice of N for UIntegerFP32. + // input type | compute type | N + // -----------+--------------+---- + // float | BSNumber | 27 + // double | BSNumber | 197 + // float | BSRational | 2882 + // double | BSRational | 21688 + // The query involves four calls of ToPlane, so the numbers match those + // of ToPlane. + int ToTetrahedron(int i, int v0, int v1, int v2, int v3) const; + int ToTetrahedron(Vector3 const& test, int v0, int v1, int v2, int v3) const; + + // For a tetrahedron with vertices ordered as described in the file + // GteTetrahedronKey.h, the function returns + // +1, P outside circumsphere of tetrahedron + // -1, P inside circumsphere of tetrahedron + // 0, P on circumsphere of tetrahedron + // + // Choice of N for UIntegerFP32. + // input type | compute type | N + // -----------+--------------+-------- + // float | BSNumber | 44 + // float | BSRational | 329 + // double | BSNumber | 298037 + // double | BSRational | 2254442 + int ToCircumsphere(int i, int v0, int v1, int v2, int v3) const; + int ToCircumsphere(Vector3 const& test, int v0, int v1, int v2, int v3) const; + +private: + int mNumVertices; + Vector3 const* mVertices; +}; + + +template +PrimalQuery3::PrimalQuery3() + : + mNumVertices(0), + mVertices(nullptr) +{ +} + +template +PrimalQuery3::PrimalQuery3(int numVertices, + Vector3 const* vertices) + : + mNumVertices(numVertices), + mVertices(vertices) +{ +} + +template inline +void PrimalQuery3::Set(int numVertices, Vector3 const* vertices) +{ + mNumVertices = numVertices; + mVertices = vertices; +} + +template inline +int PrimalQuery3::GetNumVertices() const +{ + return mNumVertices; +} + +template inline +Vector3 const* PrimalQuery3::GetVertices() const +{ + return mVertices; +} + +template +int PrimalQuery3::ToPlane(int i, int v0, int v1, int v2) const +{ + return ToPlane(mVertices[i], v0, v1, v2); +} + +template +int PrimalQuery3::ToPlane(Vector3 const& test, int v0, int v1, + int v2) const +{ + Vector3 const& vec0 = mVertices[v0]; + Vector3 const& vec1 = mVertices[v1]; + Vector3 const& vec2 = mVertices[v2]; + + Real x0 = test[0] - vec0[0]; + Real y0 = test[1] - vec0[1]; + Real z0 = test[2] - vec0[2]; + Real x1 = vec1[0] - vec0[0]; + Real y1 = vec1[1] - vec0[1]; + Real z1 = vec1[2] - vec0[2]; + Real x2 = vec2[0] - vec0[0]; + Real y2 = vec2[1] - vec0[1]; + Real z2 = vec2[2] - vec0[2]; + Real y1z2 = y1*z2; + Real y2z1 = y2*z1; + Real y2z0 = y2*z0; + Real y0z2 = y0*z2; + Real y0z1 = y0*z1; + Real y1z0 = y1*z0; + Real c0 = y1z2 - y2z1; + Real c1 = y2z0 - y0z2; + Real c2 = y0z1 - y1z0; + Real x0c0 = x0*c0; + Real x1c1 = x1*c1; + Real x2c2 = x2*c2; + Real term = x0c0 + x1c1; + Real det = term + x2c2; + Real const zero(0); + + return (det > zero ? +1 : (det < zero ? -1 : 0)); +} + +template +int PrimalQuery3::ToTetrahedron(int i, int v0, int v1, int v2, int v3) + const +{ + return ToTetrahedron(mVertices[i], v0, v1, v2, v3); +} + +template +int PrimalQuery3::ToTetrahedron(Vector3 const& test, int v0, + int v1, int v2, int v3) const +{ + int sign0 = ToPlane(test, v1, v2, v3); + if (sign0 > 0) + { + return +1; + } + + int sign1 = ToPlane(test, v0, v2, v3); + if (sign1 < 0) + { + return +1; + } + + int sign2 = ToPlane(test, v0, v1, v3); + if (sign2 > 0) + { + return +1; + } + + int sign3 = ToPlane(test, v0, v1, v2); + if (sign3 < 0) + { + return +1; + } + + return ((sign0 && sign1 && sign2 && sign3) ? -1 : 0); +} + +template +int PrimalQuery3::ToCircumsphere(int i, int v0, int v1, int v2, int v3) +const +{ + return ToCircumsphere(mVertices[i], v0, v1, v2, v3); +} + +template +int PrimalQuery3::ToCircumsphere(Vector3 const& test, int v0, + int v1, int v2, int v3) const +{ + Vector3 const& vec0 = mVertices[v0]; + Vector3 const& vec1 = mVertices[v1]; + Vector3 const& vec2 = mVertices[v2]; + Vector3 const& vec3 = mVertices[v3]; + + Real x0 = vec0[0] - test[0]; + Real y0 = vec0[1] - test[1]; + Real z0 = vec0[2] - test[2]; + Real s00 = vec0[0] + test[0]; + Real s01 = vec0[1] + test[1]; + Real s02 = vec0[2] + test[2]; + Real t00 = s00*x0; + Real t01 = s01*y0; + Real t02 = s02*z0; + Real t00pt01 = t00 + t01; + Real w0 = t00pt01 + t02; + + Real x1 = vec1[0] - test[0]; + Real y1 = vec1[1] - test[1]; + Real z1 = vec1[2] - test[2]; + Real s10 = vec1[0] + test[0]; + Real s11 = vec1[1] + test[1]; + Real s12 = vec1[2] + test[2]; + Real t10 = s10*x1; + Real t11 = s11*y1; + Real t12 = s12*z1; + Real t10pt11 = t10 + t11; + Real w1 = t10pt11 + t12; + + Real x2 = vec2[0] - test[0]; + Real y2 = vec2[1] - test[1]; + Real z2 = vec2[2] - test[2]; + Real s20 = vec2[0] + test[0]; + Real s21 = vec2[1] + test[1]; + Real s22 = vec2[2] + test[2]; + Real t20 = s20*x2; + Real t21 = s21*y2; + Real t22 = s22*z2; + Real t20pt21 = t20 + t21; + Real w2 = t20pt21 + t22; + + Real x3 = vec3[0] - test[0]; + Real y3 = vec3[1] - test[1]; + Real z3 = vec3[2] - test[2]; + Real s30 = vec3[0] + test[0]; + Real s31 = vec3[1] + test[1]; + Real s32 = vec3[2] + test[2]; + Real t30 = s30*x3; + Real t31 = s31*y3; + Real t32 = s32*z3; + Real t30pt31 = t30 + t31; + Real w3 = t30pt31 + t32; + + Real x0y1 = x0*y1; + Real x0y2 = x0*y2; + Real x0y3 = x0*y3; + Real x1y0 = x1*y0; + Real x1y2 = x1*y2; + Real x1y3 = x1*y3; + Real x2y0 = x2*y0; + Real x2y1 = x2*y1; + Real x2y3 = x2*y3; + Real x3y0 = x3*y0; + Real x3y1 = x3*y1; + Real x3y2 = x3*y2; + Real a0 = x0y1 - x1y0; + Real a1 = x0y2 - x2y0; + Real a2 = x0y3 - x3y0; + Real a3 = x1y2 - x2y1; + Real a4 = x1y3 - x3y1; + Real a5 = x2y3 - x3y2; + + Real z0w1 = z0*w1; + Real z0w2 = z0*w2; + Real z0w3 = z0*w3; + Real z1w0 = z1*w0; + Real z1w2 = z1*w2; + Real z1w3 = z1*w3; + Real z2w0 = z2*w0; + Real z2w1 = z2*w1; + Real z2w3 = z2*w3; + Real z3w0 = z3*w0; + Real z3w1 = z3*w1; + Real z3w2 = z3*w2; + Real b0 = z0w1 - z1w0; + Real b1 = z0w2 - z2w0; + Real b2 = z0w3 - z3w0; + Real b3 = z1w2 - z2w1; + Real b4 = z1w3 - z3w1; + Real b5 = z2w3 - z3w2; + Real a0b5 = a0*b5; + Real a1b4 = a1*b4; + Real a2b3 = a2*b3; + Real a3b2 = a3*b2; + Real a4b1 = a4*b1; + Real a5b0 = a5*b0; + Real term0 = a0b5 - a1b4; + Real term1 = term0 + a2b3; + Real term2 = term1 + a3b2; + Real term3 = term2 - a4b1; + Real det = term3 + a5b0; + Real const zero(0); + + return (det > zero ? 1 : (det < zero ? -1 : 0)); +} + + +} + + +// ---------------------------------------------- +// GteLexicoArray2.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +namespace houdini::gte +{ + +// A template class to provide 2D array access that conforms to row-major +// order (RowMajor = true) or column-major order (RowMajor = false). The +template +class LexicoArray2 {}; + +// The array dimensions are known only at run time. +template +class LexicoArray2 +{ +public: + inline LexicoArray2(int numRows, int numCols, Real* matrix); + + inline int GetNumRows() const; + inline int GetNumCols() const; + inline Real& operator()(int r, int c); + inline Real const& operator()(int r, int c) const; + +private: + int mNumRows, mNumCols; + Real* mMatrix; +}; + +template +class LexicoArray2 +{ +public: + inline LexicoArray2(int numRows, int numCols, Real* matrix); + + inline int GetNumRows() const; + inline int GetNumCols() const; + inline Real& operator()(int r, int c); + inline Real const& operator()(int r, int c) const; + +private: + int mNumRows, mNumCols; + Real* mMatrix; +}; + +// The array dimensions are known at compile time. +template +class LexicoArray2 +{ +public: + inline LexicoArray2(Real* matrix); + + inline int GetNumRows() const; + inline int GetNumCols() const; + inline Real& operator()(int r, int c); + inline Real const& operator()(int r, int c) const; + +private: + Real* mMatrix; +}; + +template +class LexicoArray2 +{ +public: + inline LexicoArray2(Real* matrix); + + inline int GetNumRows() const; + inline int GetNumCols() const; + inline Real& operator()(int r, int c); + inline Real const& operator()(int r, int c) const; + +private: + Real* mMatrix; +}; + + +template inline +LexicoArray2::LexicoArray2(int numRows, int numCols, Real* matrix) + : + mNumRows(numRows), + mNumCols(numCols), + mMatrix(matrix) +{ +} + +template inline +int LexicoArray2::GetNumRows() const +{ + return mNumRows; +} + +template inline +int LexicoArray2::GetNumCols() const +{ + return mNumCols; +} + +template inline +Real& LexicoArray2::operator()(int r, int c) +{ + return mMatrix[c + mNumCols*r]; +} + +template inline +Real const& LexicoArray2::operator()(int r, int c) const +{ + return mMatrix[c + mNumCols*r]; +} + + + +template inline +LexicoArray2::LexicoArray2(int numRows, int numCols, Real* matrix) + : + mNumRows(numRows), + mNumCols(numCols), + mMatrix(matrix) +{ +} + +template inline +int LexicoArray2::GetNumRows() const +{ + return mNumRows; +} + +template inline +int LexicoArray2::GetNumCols() const +{ + return mNumCols; +} + +template inline +Real& LexicoArray2::operator()(int r, int c) +{ + return mMatrix[r + mNumRows*c]; +} + +template inline +Real const& LexicoArray2::operator()(int r, int c) const +{ + return mMatrix[r + mNumRows*c]; +} + + + +template inline +LexicoArray2::LexicoArray2(Real* matrix) + : + mMatrix(matrix) +{ +} + +template inline +int LexicoArray2::GetNumRows() const +{ + return NumRows; +} + +template inline +int LexicoArray2::GetNumCols() const +{ + return NumCols; +} + +template inline +Real& LexicoArray2::operator()(int r, int c) +{ + return mMatrix[c + NumCols*r]; +} + +template inline +Real const& LexicoArray2::operator()(int r, int c) const +{ + return mMatrix[c + NumCols*r]; +} + + + +template inline +LexicoArray2::LexicoArray2(Real* matrix) + : + mMatrix(matrix) +{ +} + +template inline +int LexicoArray2::GetNumRows() const +{ + return NumRows; +} + +template inline +int LexicoArray2::GetNumCols() const +{ + return NumCols; +} + +template inline +Real& LexicoArray2::operator()(int r, int c) +{ + return mMatrix[r + NumRows*c]; +} + +template inline +Real const& LexicoArray2::operator()(int r, int c) const +{ + return mMatrix[r + NumRows*c]; +} + +} + + +// ---------------------------------------------- +// GteGaussianElimination.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// The input matrix M must be NxN. The storage convention for element lookup +// is determined by GTE_USE_ROW_MAJOR or GTE_USE_COL_MAJOR, whichever is +// active. If you want the inverse of M, pass a nonnull pointer inverseM; +// this matrix must also be NxN and use the same storage convention as M. If +// you do not want the inverse of M, pass a nullptr for inverseM. If you want +// to solve M*X = B for X, where X and B are Nx1, pass nonnull pointers for B +// and X. If you want to solve M*Y = C for Y, where X and C are NxK, pass +// nonnull pointers for C and Y and pass K to numCols. In all cases, pass +// N to numRows. + +namespace houdini::gte +{ + +template +class GaussianElimination +{ +public: + bool operator()(int numRows, + Real const* M, Real* inverseM, Real& determinant, + Real const* B, Real* X, + Real const* C, int numCols, Real* Y) const; + +private: + // Support for copying source to target or to set target to zero. If + // source is nullptr, then target is set to zero; otherwise source is + // copied to target. This function hides the type traits used to + // determine whether Real is native floating-point or otherwise (such + // as BSNumber or BSRational). + void Set(int numElements, Real const* source, Real* target) const; +}; + + +template +bool GaussianElimination::operator()(int numRows, Real const* M, + Real* inverseM, Real& determinant, Real const* B, Real* X, Real const* C, + int numCols, Real* Y) const +{ + if (numRows <= 0 || !M + || ((B != nullptr) != (X != nullptr)) + || ((C != nullptr) != (Y != nullptr)) + || (C != nullptr && numCols < 1)) + { + LogError("Invalid input."); + return false; + } + + int numElements = numRows * numRows; + bool wantInverse = (inverseM != nullptr); + std::vector localInverseM; + if (!wantInverse) + { + localInverseM.resize(numElements); + inverseM = localInverseM.data(); + } + Set(numElements, M, inverseM); + + if (B) + { + Set(numRows, B, X); + } + + if (C) + { + Set(numRows * numCols, C, Y); + } + +#if defined(GTE_USE_ROW_MAJOR) + LexicoArray2 matInvM(numRows, numRows, inverseM); + LexicoArray2 matY(numRows, numCols, Y); +#else + LexicoArray2 matInvM(numRows, numRows, inverseM); + LexicoArray2 matY(numRows, numCols, Y); +#endif + + std::vector colIndex(numRows), rowIndex(numRows), pivoted(numRows); + std::fill(pivoted.begin(), pivoted.end(), 0); + + Real const zero = (Real)0; + Real const one = (Real)1; + bool odd = false; + determinant = one; + + // Elimination by full pivoting. + int i1, i2, row = 0, col = 0; + for (int i0 = 0; i0 < numRows; ++i0) + { + // Search matrix (excluding pivoted rows) for maximum absolute entry. + Real maxValue = zero; + for (i1 = 0; i1 < numRows; ++i1) + { + if (!pivoted[i1]) + { + for (i2 = 0; i2 < numRows; ++i2) + { + if (!pivoted[i2]) + { + Real absValue = std::abs(matInvM(i1, i2)); + if (absValue > maxValue) + { + maxValue = absValue; + row = i1; + col = i2; + } + } + } + } + } + + if (maxValue == zero) + { + // The matrix is not invertible. + if (wantInverse) + { + Set(numElements, nullptr, inverseM); + } + determinant = zero; + + if (B) + { + Set(numRows, nullptr, X); + } + + if (C) + { + Set(numRows * numCols, nullptr, Y); + } + return false; + } + + pivoted[col] = true; + + // Swap rows so that the pivot entry is in row 'col'. + if (row != col) + { + odd = !odd; + for (int i = 0; i < numRows; ++i) + { + std::swap(matInvM(row, i), matInvM(col, i)); + } + + if (B) + { + std::swap(X[row], X[col]); + } + + if (C) + { + for (int i = 0; i < numCols; ++i) + { + std::swap(matY(row, i), matY(col, i)); + } + } + } + + // Keep track of the permutations of the rows. + rowIndex[i0] = row; + colIndex[i0] = col; + + // Scale the row so that the pivot entry is 1. + Real diagonal = matInvM(col, col); + determinant *= diagonal; + Real inv = one / diagonal; + matInvM(col, col) = one; + for (i2 = 0; i2 < numRows; ++i2) + { + matInvM(col, i2) *= inv; + } + + if (B) + { + X[col] *= inv; + } + + if (C) + { + for (i2 = 0; i2 < numCols; ++i2) + { + matY(col, i2) *= inv; + } + } + + // Zero out the pivot column locations in the other rows. + for (i1 = 0; i1 < numRows; ++i1) + { + if (i1 != col) + { + Real save = matInvM(i1, col); + matInvM(i1, col) = zero; + for (i2 = 0; i2 < numRows; ++i2) + { + matInvM(i1, i2) -= matInvM(col, i2) * save; + } + + if (B) + { + X[i1] -= X[col] * save; + } + + if (C) + { + for (i2 = 0; i2 < numCols; ++i2) + { + matY(i1, i2) -= matY(col, i2) * save; + } + } + } + } + } + + if (wantInverse) + { + // Reorder rows to undo any permutations in Gaussian elimination. + for (i1 = numRows - 1; i1 >= 0; --i1) + { + if (rowIndex[i1] != colIndex[i1]) + { + for (i2 = 0; i2 < numRows; ++i2) + { + std::swap(matInvM(i2, rowIndex[i1]), + matInvM(i2, colIndex[i1])); + } + } + } + } + + if (odd) + { + determinant = -determinant; + } + + return true; +} + +template +void GaussianElimination::Set(int numElements, Real const* source, + Real* target) const +{ + if (std::is_floating_point() == std::true_type()) + { + // Fast set/copy for native floating-point. + size_t numBytes = numElements * sizeof(Real); + if (source) + { + Memcpy(target, source, numBytes); + } + else + { + memset(target, 0, numBytes); + } + } + else + { + // The inputs are not std containers, so ensure assignment works + // correctly. + if (source) + { + for (int i = 0; i < numElements; ++i) + { + target[i] = source[i]; + } + } + else + { + Real const zero = (Real)0; + for (int i = 0; i < numElements; ++i) + { + target[i] = zero; + } + } + } +} + + +} + + + +// ---------------------------------------------- +// GteMatrix.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.2 (2018/10/05) + +namespace houdini::gte +{ + +template +class Matrix +{ +public: + // The table is initialized to zero. + Matrix(); + + // The table is fully initialized by the inputs. The 'values' must be + // specified in row-major order, regardless of the active storage scheme + // (GTE_USE_ROW_MAJOR or GTE_USE_COL_MAJOR). + Matrix(std::array const& values); + + // At most NumRows*NumCols are copied from the initializer list, setting + // any remaining elements to zero. The 'values' must be specified in + // row-major order, regardless of the active storage scheme + // (GTE_USE_ROW_MAJOR or GTE_USE_COL_MAJOR). Create the zero matrix using + // the syntax + // Matrix zero{(Real)0}; + // WARNING: The C++ 11 specification states that + // Matrix zero{}; + // will lead to a call of the default constructor, not the initializer + // constructor! + Matrix(std::initializer_list values); + + // For 0 <= r < NumRows and 0 <= c < NumCols, element (r,c) is 1 and all + // others are 0. If either of r or c is invalid, the zero matrix is + // created. This is a convenience for creating the standard Euclidean + // basis matrices; see also MakeUnit(int,int) and Unit(int,int). + Matrix(int r, int c); + + // The copy constructor, destructor, and assignment operator are generated + // by the compiler. + + // Member access for which the storage representation is transparent. The + // matrix entry in row r and column c is A(r,c). The first operator() + // returns a const reference rather than a Real value. This supports + // writing via standard file operations that require a const pointer to + // data. + inline Real const& operator()(int r, int c) const; + inline Real& operator()(int r, int c); + + // Member access by rows or by columns. + void SetRow(int r, Vector const& vec); + void SetCol(int c, Vector const& vec); + Vector GetRow(int r) const; + Vector GetCol(int c) const; + + // Member access by 1-dimensional index. NOTE: These accessors are + // useful for the manipulation of matrix entries when it does not + // matter whether storage is row-major or column-major. Do not use + // constructs such as M[c+NumCols*r] or M[r+NumRows*c] that expose the + // storage convention. + inline Real const& operator[](int i) const; + inline Real& operator[](int i); + + // Comparisons for sorted containers and geometric ordering. + inline bool operator==(Matrix const& mat) const; + inline bool operator!=(Matrix const& mat) const; + inline bool operator< (Matrix const& mat) const; + inline bool operator<=(Matrix const& mat) const; + inline bool operator> (Matrix const& mat) const; + inline bool operator>=(Matrix const& mat) const; + + // Special matrices. + void MakeZero(); // All components are 0. + void MakeUnit(int r, int c); // Component (r,c) is 1, all others zero. + void MakeIdentity(); // Diagonal entries 1, others 0, even when nonsquare + static Matrix Zero(); + static Matrix Unit(int r, int c); + static Matrix Identity(); + +protected: + class Table + { + public: + // Storage-order-independent element access as 2D array. + inline Real const& operator()(int r, int c) const; + inline Real& operator()(int r, int c); + + // Element access as 1D array. Use this internally only when + // the 2D storage order is not relevant. + inline Real const& operator[](int i) const; + inline Real& operator[](int i); + +#if defined(GTE_USE_ROW_MAJOR) + std::array,NumRows> mStorage; +#else + std::array,NumCols> mStorage; +#endif + }; + + Table mTable; +}; + +// Unary operations. +template +Matrix +operator+(Matrix const& M); + +template +Matrix +operator-(Matrix const& M); + +// Linear-algebraic operations. +template +Matrix +operator+( + Matrix const& M0, + Matrix const& M1); + +template +Matrix +operator-( + Matrix const& M0, + Matrix const& M1); + +template +Matrix +operator*(Matrix const& M, Real scalar); + +template +Matrix +operator*(Real scalar, Matrix const& M); + +template +Matrix +operator/(Matrix const& M, Real scalar); + +template +Matrix& +operator+=( + Matrix& M0, + Matrix const& M1); + +template +Matrix& +operator-=( + Matrix& M0, + Matrix const& M1); + +template +Matrix& +operator*=(Matrix& M, Real scalar); + +template +Matrix& +operator/=(Matrix& M, Real scalar); + +// Geometric operations. +template +Real L1Norm(Matrix const& M); + +template +Real L2Norm(Matrix const& M); + +template +Real LInfinityNorm(Matrix const& M); + +template +Matrix Inverse(Matrix const& M, + bool* reportInvertibility = nullptr); + +template +Real Determinant(Matrix const& M); + +// M^T +template +Matrix +Transpose(Matrix const& M); + +// M*V +template +Vector +operator*( + Matrix const& M, + Vector const& V); + +// V^T*M +template +Vector +operator*( + Vector const& V, + Matrix const& M); + +// A*B +template +Matrix +operator*( + Matrix const& A, + Matrix const& B); + +template +Matrix +MultiplyAB( + Matrix const& A, + Matrix const& B); + +// A*B^T +template +Matrix +MultiplyABT( + Matrix const& A, + Matrix const& B); + +// A^T*B +template +Matrix +MultiplyATB( + Matrix const& A, + Matrix const& B); + +// A^T*B^T +template +Matrix +MultiplyATBT( + Matrix const& A, + Matrix const& B); + +// M*D, D is diagonal NumCols-by-NumCols +template +Matrix +MultiplyMD( + Matrix const& M, + Vector const& D); + +// D*M, D is diagonal NumRows-by-NumRows +template +Matrix +MultiplyDM( + Vector const& D, + Matrix const& M); + +// U*V^T, U is NumRows-by-1, V is Num-Cols-by-1, result is NumRows-by-NumCols. +template +Matrix +OuterProduct(Vector const& U, Vector const& V); + +// Initialization to a diagonal matrix whose diagonal entries are the +// components of D. +template +void MakeDiagonal(Vector const& D, Matrix& M); + +// Create an (N+1)-by-(N+1) matrix H by setting the upper N-by-N block to the +// input N-by-N matrix and all other entries to 0 except for the last row +// and last column entry which is set to 1. +template +Matrix HLift(Matrix const& M); + +// Extract the upper (N-1)-by-(N-1) block of the input N-by-N matrix. +template +Matrix HProject(Matrix const& M); + + +template +Matrix::Matrix() +{ + MakeZero(); +} + +template +Matrix::Matrix( + std::array const& values) +{ + for (int r = 0, i = 0; r < NumRows; ++r) + { + for (int c = 0; c < NumCols; ++c, ++i) + { + mTable(r, c) = values[i]; + } + } +} + +template +Matrix::Matrix(std::initializer_list values) +{ + int const numValues = static_cast(values.size()); + auto iter = values.begin(); + int r, c, i; + for (r = 0, i = 0; r < NumRows; ++r) + { + for (c = 0; c < NumCols; ++c, ++i) + { + if (i < numValues) + { + mTable(r, c) = *iter++; + } + else + { + break; + } + } + + if (c < NumCols) + { + // Fill in the remaining columns of the current row with zeros. + for (/**/; c < NumCols; ++c) + { + mTable(r, c) = (Real)0; + } + ++r; + break; + } + } + + if (r < NumRows) + { + // Fill in the remain rows with zeros. + for (/**/; r < NumRows; ++r) + { + for (c = 0; c < NumCols; ++c) + { + mTable(r, c) = (Real)0; + } + } + } +} + +template +Matrix::Matrix(int r, int c) +{ + MakeUnit(r, c); +} + +template inline +Real const& Matrix::operator()(int r, int c) const +{ + return mTable(r, c); +} + +template inline +Real& Matrix::operator()(int r, int c) +{ + return mTable(r, c); +} + +template +void Matrix::SetRow(int r, + Vector const& vec) +{ + for (int c = 0; c < NumCols; ++c) + { + mTable(r, c) = vec[c]; + } +} + +template +void Matrix::SetCol(int c, + Vector const& vec) +{ + for (int r = 0; r < NumRows; ++r) + { + mTable(r, c) = vec[r]; + } +} + +template +Vector Matrix::GetRow(int r) const +{ + Vector vec; + for (int c = 0; c < NumCols; ++c) + { + vec[c] = mTable(r, c); + } + return vec; +} + +template +Vector Matrix::GetCol(int c) const +{ + Vector vec; + for (int r = 0; r < NumRows; ++r) + { + vec[r] = mTable(r, c); + } + return vec; +} + +template inline +Real const& Matrix::operator[](int i) const +{ + return mTable[i]; +} + +template inline +Real& Matrix::operator[](int i) +{ + return mTable[i]; +} + +template inline +bool Matrix::operator==(Matrix const& mat) const +{ + return mTable.mStorage == mat.mTable.mStorage; +} + +template inline +bool Matrix::operator!=(Matrix const& mat) const +{ + return mTable.mStorage != mat.mTable.mStorage; +} + +template inline +bool Matrix::operator<(Matrix const& mat) const +{ + return mTable.mStorage < mat.mTable.mStorage; +} + +template inline +bool Matrix::operator<=(Matrix const& mat) const +{ + return mTable.mStorage <= mat.mTable.mStorage; +} + +template inline +bool Matrix::operator>(Matrix const& mat) const +{ + return mTable.mStorage > mat.mTable.mStorage; +} + +template inline +bool Matrix::operator>=(Matrix const& mat) const +{ + return mTable.mStorage >= mat.mTable.mStorage; +} + +template +void Matrix::MakeZero() +{ + Real const zero = (Real)0; + for (int i = 0; i < NumRows * NumCols; ++i) + { + mTable[i] = zero; + } +} + +template +void Matrix::MakeUnit(int r, int c) +{ + MakeZero(); + if (0 <= r && r < NumRows && 0 <= c && c < NumCols) + { + mTable(r, c) = (Real)1; + } +} + +template +void Matrix::MakeIdentity() +{ + MakeZero(); + int const numDiagonal = (NumRows <= NumCols ? NumRows : NumCols); + for (int i = 0; i < numDiagonal; ++i) + { + mTable(i, i) = (Real)1; + } +} + +template +Matrix Matrix::Zero() +{ + Matrix M; + M.MakeZero(); + return M; +} + +template +Matrix Matrix::Unit(int r, + int c) +{ + Matrix M; + M.MakeUnit(r, c); + return M; +} + +template +Matrix Matrix::Identity() +{ + Matrix M; + M.MakeIdentity(); + return M; +} + + + +template +Matrix +operator+(Matrix const& M) +{ + return M; +} + +template +Matrix +operator-(Matrix const& M) +{ + Matrix result; + for (int i = 0; i < NumRows*NumCols; ++i) + { + result[i] = -M[i]; + } + return result; +} + +template +Matrix +operator+( + Matrix const& M0, + Matrix const& M1) +{ + Matrix result = M0; + return result += M1; +} + +template +Matrix +operator-( + Matrix const& M0, + Matrix const& M1) +{ + Matrix result = M0; + return result -= M1; +} + +template +Matrix +operator*(Matrix const& M, Real scalar) +{ + Matrix result = M; + return result *= scalar; +} + +template +Matrix +operator*(Real scalar, Matrix const& M) +{ + Matrix result = M; + return result *= scalar; +} + +template +Matrix +operator/(Matrix const& M, Real scalar) +{ + Matrix result = M; + return result /= scalar; +} + +template +Matrix& +operator+=( + Matrix& M0, + Matrix const& M1) +{ + for (int i = 0; i < NumRows*NumCols; ++i) + { + M0[i] += M1[i]; + } + return M0; +} + +template +Matrix& +operator-=( + Matrix& M0, + Matrix const& M1) +{ + for (int i = 0; i < NumRows*NumCols; ++i) + { + M0[i] -= M1[i]; + } + return M0; +} + +template +Matrix& +operator*=(Matrix& M, Real scalar) +{ + for (int i = 0; i < NumRows*NumCols; ++i) + { + M[i] *= scalar; + } + return M; +} + +template +Matrix& +operator/=(Matrix& M, Real scalar) +{ + if (scalar != (Real)0) + { + Real invScalar = ((Real)1) / scalar; + for (int i = 0; i < NumRows*NumCols; ++i) + { + M[i] *= invScalar; + } + } + else + { + for (int i = 0; i < NumRows*NumCols; ++i) + { + M[i] = (Real)0; + } + } + return M; +} + +template +Real L1Norm(Matrix const& M) +{ + Real sum = std::abs(M[0]); + for (int i = 1; i < NumRows*NumCols; ++i) + { + sum += std::abs(M[i]); + } + return sum; +} + +template +Real L2Norm(Matrix const& M) +{ + Real sum = M[0] * M[0]; + for (int i = 1; i < NumRows*NumCols; ++i) + { + sum += M[i] * M[i]; + } + return std::sqrt(sum); +} + +template +Real LInfinityNorm(Matrix const& M) +{ + Real maxAbsElement = M[0]; + for (int i = 1; i < NumRows*NumCols; ++i) + { + Real absElement = std::abs(M[i]); + if (absElement > maxAbsElement) + { + maxAbsElement = absElement; + } + } + return maxAbsElement; +} + +template +Matrix Inverse(Matrix const& M, + bool* reportInvertibility) +{ + Matrix invM; + Real determinant; + bool invertible = GaussianElimination()(N, &M[0], &invM[0], + determinant, nullptr, nullptr, nullptr, 0, nullptr); + if (reportInvertibility) + { + *reportInvertibility = invertible; + } + return invM; +} + +template +Real Determinant(Matrix const& M) +{ + Real determinant; + GaussianElimination()(N, &M[0], nullptr, determinant, nullptr, + nullptr, nullptr, 0, nullptr); + return determinant; +} + +template +Matrix +Transpose(Matrix const& M) +{ + Matrix result; + for (int r = 0; r < NumRows; ++r) + { + for (int c = 0; c < NumCols; ++c) + { + result(c, r) = M(r, c); + } + } + return result; +} + +template +Vector +operator*( + Matrix const& M, + Vector const& V) +{ + Vector result; + for (int r = 0; r < NumRows; ++r) + { + result[r] = (Real)0; + for (int c = 0; c < NumCols; ++c) + { + result[r] += M(r, c) * V[c]; + } + } + return result; +} + +template +Vector operator*(Vector const& V, + Matrix const& M) +{ + Vector result; + for (int c = 0; c < NumCols; ++c) + { + result[c] = (Real)0; + for (int r = 0; r < NumRows; ++r) + { + result[c] += V[r] * M(r, c); + } + } + return result; +} + +template +Matrix +operator*( + Matrix const& A, + Matrix const& B) +{ + return MultiplyAB(A, B); +} + +template +Matrix +MultiplyAB( + Matrix const& A, + Matrix const& B) +{ + Matrix result; + for (int r = 0; r < NumRows; ++r) + { + for (int c = 0; c < NumCols; ++c) + { + result(r, c) = (Real)0; + for (int i = 0; i < NumCommon; ++i) + { + result(r, c) += A(r, i) * B(i, c); + } + } + } + return result; +} + +template +Matrix +MultiplyABT( + Matrix const& A, + Matrix const& B) +{ + Matrix result; + for (int r = 0; r < NumRows; ++r) + { + for (int c = 0; c < NumCols; ++c) + { + result(r, c) = (Real)0; + for (int i = 0; i < NumCommon; ++i) + { + result(r, c) += A(r, i) * B(c, i); + } + } + } + return result; +} + +template +Matrix +MultiplyATB( + Matrix const& A, + Matrix const& B) +{ + Matrix result; + for (int r = 0; r < NumRows; ++r) + { + for (int c = 0; c < NumCols; ++c) + { + result(r, c) = (Real)0; + for (int i = 0; i < NumCommon; ++i) + { + result(r, c) += A(i, r) * B(i, c); + } + } + } + return result; +} + +template +Matrix +MultiplyATBT( + Matrix const& A, + Matrix const& B) +{ + Matrix result; + for (int r = 0; r < NumRows; ++r) + { + for (int c = 0; c < NumCols; ++c) + { + result(r, c) = (Real)0; + for (int i = 0; i < NumCommon; ++i) + { + result(r, c) += A(i, r) * B(c, i); + } + } + } + return result; +} + +template +Matrix +MultiplyMD( + Matrix const& M, + Vector const& D) +{ + Matrix result; + for (int r = 0; r < NumRows; ++r) + { + for (int c = 0; c < NumCols; ++c) + { + result(r, c) = M(r, c) * D[c]; + } + } + return result; +} + +template +Matrix +MultiplyDM( + Vector const& D, + Matrix const& M) +{ + Matrix result; + for (int r = 0; r < NumRows; ++r) + { + for (int c = 0; c < NumCols; ++c) + { + result(r, c) = D[r] * M(r, c); + } + } + return result; +} + +template +Matrix +OuterProduct(Vector const& U, Vector const& V) +{ + Matrix result; + for (int r = 0; r < NumRows; ++r) + { + for (int c = 0; c < NumCols; ++c) + { + result(r, c) = U[r] * V[c]; + } + } + return result; +} + +template +void MakeDiagonal(Vector const& D, Matrix& M) +{ + for (int i = 0; i < N*N; ++i) + { + M[i] = (Real)0; + } + + for (int i = 0; i < N; ++i) + { + M(i, i) = D[i]; + } +} + +template +Matrix HLift(Matrix const& M) +{ + Matrix result; + result.MakeIdentity(); + for (int r = 0; r < N; ++r) + { + for (int c = 0; c < N; ++c) + { + result(r, c) = M(r, c); + } + } + return result; +} + +// Extract the upper (N-1)-by-(N-1) block of the input N-by-N matrix. +template +Matrix HProject(Matrix const& M) +{ + static_assert(N >= 2, "Invalid matrix dimension."); + Matrix result; + for (int r = 0; r < N - 1; ++r) + { + for (int c = 0; c < N - 1; ++c) + { + result(r, c) = M(r, c); + } + } + return result; +} + + +// Matrix::Table + +template inline +Real const& Matrix::Table::operator()(int r, int c) + const +{ +#if defined(GTE_USE_ROW_MAJOR) + return mStorage[r][c]; +#else + return mStorage[c][r]; +#endif +} + +template inline +Real& Matrix::Table::operator()(int r, int c) +{ +#if defined(GTE_USE_ROW_MAJOR) + return mStorage[r][c]; +#else + return mStorage[c][r]; +#endif +} + +template inline +Real const& Matrix::Table::operator[](int i) const +{ + Real const* elements = &mStorage[0][0]; + return elements[i]; +} + +template inline +Real& Matrix::Table::operator[](int i) +{ + Real* elements = &mStorage[0][0]; + return elements[i]; +} + + +} + + +// ---------------------------------------------- +// GteRangeIteration.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// For information on range-based for-loops, see +// http://en.cppreference.com/w/cpp/language/range-for + +namespace houdini::gte +{ + +// The function gte::reverse supports reverse iteration in range-based +// for-loops using the auto keyword. For example, +// +// std::vector numbers(4); +// int i = 0; +// for (auto& number : numbers) +// { +// number = i++; +// std::cout << number << ' '; +// } +// // Output: 0 1 2 3 +// +// for (auto& number : gte::reverse(numbers)) +// { +// std::cout << number << ' '; +// } +// // Output: 3 2 1 0 + +template +class ReversalObject +{ +public: + ReversalObject(Iterator begin, Iterator end) + : + mBegin(begin), + mEnd(end) + { + } + + Iterator begin() const { return mBegin; } + Iterator end() const { return mEnd; } + +private: + Iterator mBegin, mEnd; +}; + +template +< + typename Iterable, + typename Iterator = decltype(std::begin(std::declval())), + typename ReverseIterator = std::reverse_iterator +> +ReversalObject reverse(Iterable&& range) +{ + return ReversalObject( + ReverseIterator(std::end(range)), + ReverseIterator(std::begin(range))); +} + +} + + +// ---------------------------------------------- +// GteWrapper.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// Wrappers around platform-specific low-level library calls. + +namespace houdini::gte +{ + +void Memcpy(void* target, void const* source, size_t count); +void Memcpy(wchar_t* target, wchar_t const* source, size_t count); + +} + +// ---------------------------------------------- +// GteSingularValueDecomposition.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.1 (2018/10/05) + +// The SingularValueDecomposition class is an implementation of Algorithm +// 8.3.2 (The SVD Algorithm) described in "Matrix Computations, 2nd +// edition" by G. H. Golub and Charles F. Van Loan, The Johns Hopkins +// Press, Baltimore MD, Fourth Printing 1993. Algorithm 5.4.2 (Householder +// Bidiagonalization) is used to reduce A to bidiagonal B. Algorithm 8.3.1 +// (Golub-Kahan SVD Step) is used for the iterative reduction from bidiagonal +// to diagonal. If A is the original matrix, S is the matrix whose diagonal +// entries are the singular values, and U and V are corresponding matrices, +// then theoretically U^T*A*V = S. Numerically, we have errors +// E = U^T*A*V - S. Algorithm 8.3.2 mentions that one expects |E| is +// approximately u*|A|, where |M| denotes the Frobenius norm of M and where +// u is the unit roundoff for the floating-point arithmetic: 2^{-23} for +// 'float', which is FLT_EPSILON = 1.192092896e-7f, and 2^{-52} for'double', +// which is DBL_EPSILON = 2.2204460492503131e-16. +// +// The condition |a(i,i+1)| <= epsilon*(|a(i,i) + a(i+1,i+1)|) used to +// determine when the reduction decouples to smaller problems is implemented +// as: sum = |a(i,i)| + |a(i+1,i+1)|; sum + |a(i,i+1)| == sum. The idea is +// that the superdiagonal term is small relative to its diagonal neighbors, +// and so it is effectively zero. The unit tests have shown that this +// interpretation of decoupling is effective. +// +// The condition |a(i,i)| <= epsilon*|B| used to determine when the +// reduction decouples (with a zero singular value) is implemented using +// the Frobenius norm of B and epsilon = multiplier*u, where for now the +// multiplier is hard-coded in Solve(...) as 8. +// +// The authors suggest that once you have the bidiagonal matrix, a practical +// implementation will store the diagonal and superdiagonal entries in linear +// arrays, ignoring the theoretically zero values not in the 2-band. This is +// good for cache coherence, and we have used the suggestion. The essential +// parts of the Householder u-vectors are stored in the lower-triangular +// portion of the matrix and the essential parts of the Householder v-vectors +// are stored in the upper-triangular portion of the matrix. To avoid having +// to recompute 2/Dot(u,u) and 2/Dot(v,v) when constructing orthogonal U and +// V, we store these quantities in additional memory during bidiagonalization. +// +// For matrices with randomly generated values in [0,1], the unit tests +// produce the following information for N-by-N matrices. +// +// N |A| |E| |E|/|A| iterations +// ------------------------------------------- +// 2 1.4831 4.1540e-16 2.8007e-16 1 +// 3 2.1065 3.5024e-16 1.6626e-16 4 +// 4 2.4979 7.4605e-16 2.9867e-16 6 +// 5 3.6591 1.8305e-15 5.0025e-16 9 +// 6 4.0572 2.0571e-15 5.0702e-16 10 +// 7 4.7745 2.9057e-15 6.0859e-16 12 +// 8 5.1964 2.7958e-15 5.3803e-16 13 +// 9 5.7599 3.3128e-15 5.7514e-16 16 +// 10 6.2700 3.7209e-15 5.9344e-16 16 +// 11 6.8220 5.0580e-15 7.4142e-16 18 +// 12 7.4540 5.2493e-15 7.0422e-16 21 +// 13 8.1225 5.6043e-15 6.8997e-16 24 +// 14 8.5883 5.8553e-15 6.8177e-16 26 +// 15 9.1337 6.9663e-15 7.6270e-16 27 +// 16 9.7884 9.1358e-15 9.3333e-16 29 +// 17 10.2407 8.2715e-15 8.0771e-16 34 +// 18 10.7147 8.9748e-15 8.3761e-16 33 +// 19 11.1887 1.0094e-14 9.0220e-16 32 +// 20 11.7739 9.7000e-15 8.2386e-16 35 +// 21 12.2822 1.1217e-14 9.1329e-16 36 +// 22 12.7649 1.1071e-14 8.6732e-16 37 +// 23 13.3366 1.1271e-14 8.4513e-16 41 +// 24 13.8505 1.2806e-14 9.2460e-16 43 +// 25 14.4332 1.3081e-14 9.0637e-16 43 +// 26 14.9301 1.4882e-14 9.9680e-16 46 +// 27 15.5214 1.5571e-14 1.0032e-15 48 +// 28 16.1029 1.7553e-14 1.0900e-15 49 +// 29 16.6407 1.6219e-14 9.7467e-16 53 +// 30 17.1891 1.8560e-14 1.0797e-15 55 +// 31 17.7773 1.8522e-14 1.0419e-15 56 +// +// The singularvalues and |E|/|A| values were compared to those generated by +// Mathematica Version 9.0; Wolfram Research, Inc., Champaign IL, 2012. +// The results were all comparable with singular values agreeing to a large +// number of decimal places. +// +// Timing on an Intel (R) Core (TM) i7-3930K CPU @ 3.20 GHZ (in seconds) +// for NxN matrices: +// +// N |E|/|A| iters bidiag QR U-and-V comperr +// ------------------------------------------------------- +// 512 3.8632e-15 848 0.341 0.016 1.844 2.203 +// 1024 5.6456e-15 1654 4.279 0.032 18.765 20.844 +// 2048 7.5499e-15 3250 40.421 0.141 186.607 213.216 +// +// where iters is the number of QR steps taken, bidiag is the computation +// of the Householder reflection vectors, U-and-V is the composition of +// Householder reflections and Givens rotations to obtain the orthogonal +// matrices of the decomposigion, and comperr is the computation E = +// U^T*A*V - S. + +namespace houdini::gte +{ + +template +class SingularValueDecomposition +{ +public: + // The solver processes MxN symmetric matrices, where M >= N > 1 + // ('numRows' is M and 'numCols' is N) and the matrix is stored in + // row-major order. The maximum number of iterations ('maxIterations') + // must be specified for the reduction of a bidiagonal matrix to a + // diagonal matrix. The goal is to compute MxM orthogonal U, NxN + // orthogonal V, and MxN matrix S for which U^T*A*V = S. The only + // nonzero entries of S are on the diagonal; the diagonal entries are + // the singular values of the original matrix. + SingularValueDecomposition(int numRows, int numCols, + unsigned int maxIterations); + + // A copy of the MxN input is made internally. The order of the singular + // values is specified by sortType: -1 (decreasing), 0 (no sorting), or +1 + // (increasing). When sorted, the columns of the orthogonal matrices + // are ordered accordingly. The return value is the number of iterations + // consumed when convergence occurred, 0xFFFFFFFF when convergence did not + // occur or 0 when N <= 1 or M < N was passed to the constructor. + unsigned int Solve(Real const* input, int sortType); + + // Get the singular values of the matrix passed to Solve(...). The input + // 'singularValues' must have N elements. + void GetSingularValues(Real* singularValues) const; + + // Accumulate the Householder reflections, the Givens rotations, and the + // diagonal fix-up matrix to compute the orthogonal matrices U and V for + // which U^T*A*V = S. The input uMatrix must be MxM and the input vMatrix + // must be NxN, both stored in row-major order. + void GetU(Real* uMatrix) const; + void GetV(Real* vMatrix) const; + + // Compute a single column of U or V. The reflections and rotations are + // applied incrementally. This is useful when you want only a small + // number of the singular values or vectors. + void GetUColumn(int index, Real* uColumn) const; + void GetVColumn(int index, Real* vColumn) const; + Real GetSingularValue(int index) const; + +private: + // Bidiagonalize using Householder reflections. On input, mMatrix is a + // copy of the input matrix and has one extra row. On output, the + // diagonal and superdiagonal contain the bidiagonalized results. The + // lower-triangular portion stores the essential parts of the Householder + // u vectors (the elements of u after the leading 1-valued component) and + // the upper-triangular portion stores the essential parts of the + // Householder v vectors. To avoid recomputing 2/Dot(u,u) and 2/Dot(v,v), + // these quantities are stored in mTwoInvUTU and mTwoInvVTV. + void Bidiagonalize(); + + // A helper for generating Givens rotation sine and cosine robustly. + void GetSinCos(Real u, Real v, Real& cs, Real& sn); + + // Test for (effectively) zero-valued diagonal entries (through all but + // the last). For each such entry, the B matrix decouples. Perform + // that decoupling. If there are no zero-valued entries, then the + // Golub-Kahan step must be performed. + bool DiagonalEntriesNonzero(int imin, int imax, Real threshold); + + // This is Algorithm 8.3.1 in "Matrix Computations, 2nd edition" by + // G. H. Golub and C. F. Van Loan. + void DoGolubKahanStep(int imin, int imax); + + // The diagonal entries are not guaranteed to be nonnegative during the + // construction. After convergence to a diagonal matrix S, test for + // negative entries and build a diagonal matrix that reverses the sign + // on the S-entry. + void EnsureNonnegativeDiagonal(); + + // Sort the singular values and compute the corresponding permutation of + // the indices of the array storing the singular values. The permutation + // is used for reordering the singular values and the corresponding + // columns of the orthogonal matrix in the calls to GetSingularValues(...) + // and GetOrthogonalMatrices(...). + void ComputePermutation(int sortType); + + // The number rows and columns of the matrices to be processed. + int mNumRows, mNumCols; + + // The maximum number of iterations for reducing the bidiagonal matrix + // to a diagonal matrix. + unsigned int mMaxIterations; + + // The internal copy of a matrix passed to the solver. See the comments + // about function Bidiagonalize() about what is stored in the matrix. + std::vector mMatrix; // MxN elements + + // After the initial bidiagonalization by Householder reflections, we no + // longer need the full mMatrix. Copy the diagonal and superdiagonal + // entries to linear arrays in order to be cache friendly. + std::vector mDiagonal; // N elements + std::vector mSuperdiagonal; // N-1 elements + + // The Givens rotations used to reduce the initial bidiagonal matrix to + // a diagonal matrix. A rotation is the identity with the following + // replacement entries: R(index0,index0) = cs, R(index0,index1) = sn, + // R(index1,index0) = -sn, and R(index1,index1) = cs. If N is the + // number of matrix columns and K is the maximum number of iterations, the + // maximum number of right or left Givens rotations is K*(N-1). The + // maximum amount of memory is allocated to store these. However, we also + // potentially need left rotations to decouple the matrix when a diagonal + // terms are zero. Worst case is a number of matrices quadratic in N, so + // for now we just use std::vector whose initial capacity is + // K*(N-1). + struct GivensRotation + { + GivensRotation(); + GivensRotation(int inIndex0, int inIndex1, Real inCs, Real inSn); + int index0, index1; + Real cs, sn; + }; + + std::vector mRGivens; + std::vector mLGivens; + + // The diagonal matrix that is used to convert S-entries to nonnegative. + std::vector mFixupDiagonal; // N elements + + // When sorting is requested, the permutation associated with the sort is + // stored in mPermutation. When sorting is not requested, mPermutation[0] + // is set to -1. mVisited is used for finding cycles in the permutation. + std::vector mPermutation; // N elements + mutable std::vector mVisited; // N elements + + // Temporary storage to compute Householder reflections and to support + // sorting of columns of the orthogonal matrices. + std::vector mTwoInvUTU; // N elements + std::vector mTwoInvVTV; // N-2 elements + mutable std::vector mUVector; // M elements + mutable std::vector mVVector; // N elements + mutable std::vector mWVector; // max(M,N) elements +}; + + +template +SingularValueDecomposition::SingularValueDecomposition(int numRows, + int numCols, unsigned int maxIterations) + : + mNumRows(0), + mNumCols(0), + mMaxIterations(0) +{ + if (numCols > 1 && numRows >= numCols && maxIterations > 0) + { + mNumRows = numRows; + mNumCols = numCols; + mMaxIterations = maxIterations; + mMatrix.resize(numRows * numCols); + mDiagonal.resize(numCols); + mSuperdiagonal.resize(numCols - 1); + mRGivens.reserve(maxIterations*(numCols - 1)); + mLGivens.reserve(maxIterations*(numCols - 1)); + mFixupDiagonal.resize(numCols); + mPermutation.resize(numCols); + mVisited.resize(numCols); + mTwoInvUTU.resize(numCols); + mTwoInvVTV.resize(numCols - 2); + mUVector.resize(numRows); + mVVector.resize(numCols); + mWVector.resize(numRows); + } +} + +template +unsigned int SingularValueDecomposition::Solve(Real const* input, + int sortType) +{ + if (mNumRows > 0) + { + int numElements = mNumRows * mNumCols; + std::copy(input, input + numElements, mMatrix.begin()); + Bidiagonalize(); + + // Compute 'threshold = multiplier*epsilon*|B|' as the threshold for + // diagonal entries effectively zero; that is, |d| <= |threshold| + // implies that d is (effectively) zero. TODO: Allow the caller to + // pass 'multiplier' to the constructor. + // + // We will use the L2-norm |B|, which is the length of the elements + // of B treated as an NM-tuple. The following code avoids overflow + // when accumulating the squares of the elements when those elements + // are large. + Real maxAbsComp = std::abs(input[0]); + for (int i = 1; i < numElements; ++i) + { + Real absComp = std::abs(input[i]); + if (absComp > maxAbsComp) + { + maxAbsComp = absComp; + } + } + + Real norm = (Real)0; + if (maxAbsComp > (Real)0) + { + Real invMaxAbsComp = ((Real)1) / maxAbsComp; + for (int i = 0; i < numElements; ++i) + { + Real ratio = input[i] * invMaxAbsComp; + norm += ratio * ratio; + } + norm = maxAbsComp * std::sqrt(norm); + } + + Real const multiplier = (Real)8; // TODO: Expose to caller. + Real const epsilon = std::numeric_limits::epsilon(); + Real const threshold = multiplier * epsilon * norm; + + mRGivens.clear(); + mLGivens.clear(); + for (unsigned int j = 0; j < mMaxIterations; ++j) + { + int imin = -1, imax = -1; + for (int i = mNumCols - 2; i >= 0; --i) + { + // When a01 is much smaller than its diagonal neighbors, it is + // effectively zero. + Real a00 = mDiagonal[i]; + Real a01 = mSuperdiagonal[i]; + Real a11 = mDiagonal[i + 1]; + Real sum = std::abs(a00) + std::abs(a11); + if (sum + std::abs(a01) != sum) + { + if (imax == -1) + { + imax = i; + } + imin = i; + } + else + { + // The superdiagonal term is effectively zero compared to + // the neighboring diagonal terms. + if (imin >= 0) + { + break; + } + } + } + + if (imax == -1) + { + // The algorithm has converged. + EnsureNonnegativeDiagonal(); + ComputePermutation(sortType); + return j; + } + + // We need to test diagonal entries of B for zero. For each zero + // diagonal entry, zero the superdiagonal. + if (DiagonalEntriesNonzero(imin, imax, threshold)) + { + // Process the lower-right-most unreduced bidiagonal block. + DoGolubKahanStep(imin, imax); + } + } + return 0xFFFFFFFF; + } + else + { + return 0; + } +} + +template +void SingularValueDecomposition::GetSingularValues( + Real* singularValues) const +{ + if (singularValues && mNumCols > 0) + { + if (mPermutation[0] >= 0) + { + // Sorting was requested. + for (int i = 0; i < mNumCols; ++i) + { + int p = mPermutation[i]; + singularValues[i] = mDiagonal[p]; + } + } + else + { + // Sorting was not requested. + size_t numBytes = mNumCols*sizeof(Real); + Memcpy(singularValues, &mDiagonal[0], numBytes); + } + } +} + +template +void SingularValueDecomposition::GetU(Real* uMatrix) const +{ + if (!uMatrix || mNumCols == 0) + { + // Invalid input or the constructor failed. + return; + } + + // Start with the identity matrix. + std::fill(uMatrix, uMatrix + mNumRows*mNumRows, (Real)0); + for (int d = 0; d < mNumRows; ++d) + { + uMatrix[d + mNumRows*d] = (Real)1; + } + + // Multiply the Householder reflections using backward accumulation. + int r, c; + for (int i0 = mNumCols - 1, i1 = i0 + 1; i0 >= 0; --i0, --i1) + { + // Copy the u vector and 2/Dot(u,u) from the matrix. + Real twoinvudu = mTwoInvUTU[i0]; + Real const* column = &mMatrix[i0]; + mUVector[i0] = (Real)1; + for (r = i1; r < mNumRows; ++r) + { + mUVector[r] = column[mNumCols*r]; + } + + // Compute the w vector. + mWVector[i0] = twoinvudu; + for (r = i1; r < mNumRows; ++r) + { + mWVector[r] = (Real)0; + for (c = i1; c < mNumRows; ++c) + { + mWVector[r] += mUVector[c] * uMatrix[r + mNumRows*c]; + } + mWVector[r] *= twoinvudu; + } + + // Update the matrix, U <- U - u*w^T. + for (r = i0; r < mNumRows; ++r) + { + for (c = i0; c < mNumRows; ++c) + { + uMatrix[c + mNumRows*r] -= mUVector[r] * mWVector[c]; + } + } + } + + // Multiply the Givens rotations. + for (auto const& givens : mLGivens) + { + int j0 = givens.index0; + int j1 = givens.index1; + for (r = 0; r < mNumRows; ++r, j0 += mNumRows, j1 += mNumRows) + { + Real& q0 = uMatrix[j0]; + Real& q1 = uMatrix[j1]; + Real prd0 = givens.cs * q0 - givens.sn * q1; + Real prd1 = givens.sn * q0 + givens.cs * q1; + q0 = prd0; + q1 = prd1; + } + } + + if (mPermutation[0] >= 0) + { + // Sorting was requested. + std::fill(mVisited.begin(), mVisited.end(), 0); + for (c = 0; c < mNumCols; ++c) + { + if (mVisited[c] == 0 && mPermutation[c] != c) + { + // The item starts a cycle with 2 or more elements. + int start = c, current = c, next; + for (r = 0; r < mNumRows; ++r) + { + mWVector[r] = uMatrix[c + mNumRows*r]; + } + while ((next = mPermutation[current]) != start) + { + mVisited[current] = 1; + for (r = 0; r < mNumRows; ++r) + { + uMatrix[current + mNumRows*r] = + uMatrix[next + mNumRows*r]; + } + current = next; + } + mVisited[current] = 1; + for (r = 0; r < mNumRows; ++r) + { + uMatrix[current + mNumRows*r] = mWVector[r]; + } + } + } + } +} + +template +void SingularValueDecomposition::GetV(Real* vMatrix) const +{ + if (!vMatrix || mNumCols == 0) + { + // Invalid input or the constructor failed. + return; + } + + // Start with the identity matrix. + std::fill(vMatrix, vMatrix + mNumCols*mNumCols, (Real)0); + for (int d = 0; d < mNumCols; ++d) + { + vMatrix[d + mNumCols*d] = (Real)1; + } + + // Multiply the Householder reflections using backward accumulation. + int i0 = mNumCols - 3; + int i1 = i0 + 1; + int i2 = i0 + 2; + int r, c; + for (/**/; i0 >= 0; --i0, --i1, --i2) + { + // Copy the v vector and 2/Dot(v,v) from the matrix. + Real twoinvvdv = mTwoInvVTV[i0]; + Real const* row = &mMatrix[mNumCols*i0]; + mVVector[i1] = (Real)1; + for (r = i2; r < mNumCols; ++r) + { + mVVector[r] = row[r]; + } + + // Compute the w vector. + mWVector[i1] = twoinvvdv; + for (r = i2; r < mNumCols; ++r) + { + mWVector[r] = (Real)0; + for (c = i2; c < mNumCols; ++c) + { + mWVector[r] += mVVector[c] * vMatrix[r + mNumCols*c]; + } + mWVector[r] *= twoinvvdv; + } + + // Update the matrix, V <- V - v*w^T. + for (r = i1; r < mNumCols; ++r) + { + for (c = i1; c < mNumCols; ++c) + { + vMatrix[c + mNumCols*r] -= mVVector[r] * mWVector[c]; + } + } + } + + // Multiply the Givens rotations. + for (auto const& givens : mRGivens) + { + int j0 = givens.index0; + int j1 = givens.index1; + for (c = 0; c < mNumCols; ++c, j0 += mNumCols, j1 += mNumCols) + { + Real& q0 = vMatrix[j0]; + Real& q1 = vMatrix[j1]; + Real prd0 = givens.cs * q0 - givens.sn * q1; + Real prd1 = givens.sn * q0 + givens.cs * q1; + q0 = prd0; + q1 = prd1; + } + } + + // Fix-up the diagonal. + for (r = 0; r < mNumCols; ++r) + { + for (c = 0; c < mNumCols; ++c) + { + vMatrix[c + mNumCols*r] *= mFixupDiagonal[c]; + } + } + + if (mPermutation[0] >= 0) + { + // Sorting was requested. + std::fill(mVisited.begin(), mVisited.end(), 0); + for (c = 0; c < mNumCols; ++c) + { + if (mVisited[c] == 0 && mPermutation[c] != c) + { + // The item starts a cycle with 2 or more elements. + int start = c, current = c, next; + for (r = 0; r < mNumCols; ++r) + { + mWVector[r] = vMatrix[c + mNumCols*r]; + } + while ((next = mPermutation[current]) != start) + { + mVisited[current] = 1; + for (r = 0; r < mNumCols; ++r) + { + vMatrix[current + mNumCols*r] = + vMatrix[next + mNumCols*r]; + } + current = next; + } + mVisited[current] = 1; + for (r = 0; r < mNumCols; ++r) + { + vMatrix[current + mNumCols*r] = mWVector[r]; + } + } + } + } +} + +template +void SingularValueDecomposition::GetUColumn(int index, + Real* uColumn) const +{ + if (0 <= index && index < mNumRows) + { + // y = H*x, then x and y are swapped for the next H + Real* x = uColumn; + Real* y = &mWVector[0]; + + // Start with the Euclidean basis vector. + memset(x, 0, mNumRows * sizeof(Real)); + if (index < mNumCols && mPermutation[0] >= 0) + { + // Sorting was requested. + x[mPermutation[index]] = (Real)1; + } + else + { + x[index] = (Real)1; + } + + // Apply the Givens rotations. + for (auto const& givens : houdini::gte::reverse(mLGivens)) + { + Real& xr0 = x[givens.index0]; + Real& xr1 = x[givens.index1]; + Real tmp0 = givens.cs * xr0 + givens.sn * xr1; + Real tmp1 = -givens.sn * xr0 + givens.cs * xr1; + xr0 = tmp0; + xr1 = tmp1; + } + + // Apply the Householder reflections. + for (int c = mNumCols - 1; c >= 0; --c) + { + // Get the Householder vector u. + int r; + for (r = 0; r < c; ++r) + { + y[r] = x[r]; + } + + // Compute s = Dot(x,u) * 2/u^T*u. + Real s = x[r]; // r = c, u[r] = 1 + for (int j = r + 1; j < mNumRows; ++j) + { + s += x[j] * mMatrix[c + mNumCols * j]; + } + s *= mTwoInvUTU[c]; + + // r = c, y[r] = x[r]*u[r] - s = x[r] - s because u[r] = 1 + y[r] = x[r] - s; + + // Compute the remaining components of y. + for (++r; r < mNumRows; ++r) + { + y[r] = x[r] - s * mMatrix[c + mNumCols * r]; + } + + std::swap(x, y); + } + + // The final product is stored in x. + if (x != uColumn) + { + size_t numBytes = mNumRows*sizeof(Real); + Memcpy(uColumn, x, numBytes); + } + } +} + +template +void SingularValueDecomposition::GetVColumn(int index, + Real* vColumn) const +{ + if (0 <= index && index < mNumCols) + { + // y = H*x, then x and y are swapped for the next H + Real* x = vColumn; + Real* y = &mWVector[0]; + + // Start with the Euclidean basis vector. + memset(x, 0, mNumCols * sizeof(Real)); + if (mPermutation[0] >= 0) + { + // Sorting was requested. + int p = mPermutation[index]; + x[p] = mFixupDiagonal[p]; + } + else + { + x[index] = mFixupDiagonal[index]; + } + + // Apply the Givens rotations. + for (auto const& givens : houdini::gte::reverse(mRGivens)) + { + Real& xr0 = x[givens.index0]; + Real& xr1 = x[givens.index1]; + Real tmp0 = givens.cs * xr0 + givens.sn * xr1; + Real tmp1 = -givens.sn * xr0 + givens.cs * xr1; + xr0 = tmp0; + xr1 = tmp1; + } + + // Apply the Householder reflections. + for (int r = mNumCols - 3; r >= 0; --r) + { + // Get the Householder vector v. + int c; + for (c = 0; c < r + 1; ++c) + { + y[c] = x[c]; + } + + // Compute s = Dot(x,v) * 2/v^T*v. + Real s = x[c]; // c = r+1, v[c] = 1 + for (int j = c + 1; j < mNumCols; ++j) + { + s += x[j] * mMatrix[j + mNumCols * r]; + } + s *= mTwoInvVTV[r]; + + // c = r+1, y[c] = x[c]*v[c] - s = x[c] - s because v[c] = 1 + y[c] = x[c] - s; + + // Compute the remaining components of y. + for (++c; c < mNumCols; ++c) + { + y[c] = x[c] - s * mMatrix[c + mNumCols * r]; + } + + std::swap(x, y); + } + + // The final product is stored in x. + if (x != vColumn) + { + size_t numBytes = mNumCols*sizeof(Real); + Memcpy(vColumn, x, numBytes); + } + } +} + +template +Real SingularValueDecomposition::GetSingularValue(int index) const +{ + if (0 <= index && index < mNumCols) + { + if (mPermutation[0] >= 0) + { + // Sorting was requested. + return mDiagonal[mPermutation[index]]; + } + else + { + // Sorting was not requested. + return mDiagonal[index]; + } + } + else + { + return (Real)0; + } +} + +template +void SingularValueDecomposition::Bidiagonalize() +{ + int r, c; + for (int i = 0, ip1 = 1; i < mNumCols; ++i, ++ip1) + { + // Compute the U-Householder vector. + Real length = (Real)0; + for (r = i; r < mNumRows; ++r) + { + Real ur = mMatrix[i + mNumCols*r]; + mUVector[r] = ur; + length += ur * ur; + } + Real udu = (Real)1; + length = std::sqrt(length); + if (length >(Real)0) + { + Real& u1 = mUVector[i]; + Real sgn = (u1 >= (Real)0 ? (Real)1 : (Real)-1); + Real invDenom = ((Real)1) / (u1 + sgn * length); + u1 = (Real)1; + for (r = ip1; r < mNumRows; ++r) + { + Real& ur = mUVector[r]; + ur *= invDenom; + udu += ur * ur; + } + } + + // Compute the rank-1 offset u*w^T. + Real invudu = (Real)1 / udu; + Real twoinvudu = invudu * (Real)2; + for (c = i; c < mNumCols; ++c) + { + mWVector[c] = (Real)0; + for (r = i; r < mNumRows; ++r) + { + mWVector[c] += mMatrix[c + mNumCols*r] * mUVector[r]; + } + mWVector[c] *= twoinvudu; + } + + // Update the input matrix. + for (r = i; r < mNumRows; ++r) + { + for (c = i; c < mNumCols; ++c) + { + mMatrix[c + mNumCols*r] -= mUVector[r] * mWVector[c]; + } + } + + if (i < mNumCols - 2) + { + // Compute the V-Householder vectors. + length = (Real)0; + for (c = ip1; c < mNumCols; ++c) + { + Real vc = mMatrix[c + mNumCols*i]; + mVVector[c] = vc; + length += vc * vc; + } + Real vdv = (Real)1; + length = std::sqrt(length); + if (length >(Real)0) + { + Real& v1 = mVVector[ip1]; + Real sgn = (v1 >= (Real)0 ? (Real)1 : (Real)-1); + Real invDenom = ((Real)1) / (v1 + sgn * length); + v1 = (Real)1; + for (c = ip1 + 1; c < mNumCols; ++c) + { + Real& vc = mVVector[c]; + vc *= invDenom; + vdv += vc * vc; + } + } + + // Compute the rank-1 offset w*v^T. + Real invvdv = (Real)1 / vdv; + Real twoinvvdv = invvdv * (Real)2; + for (r = i; r < mNumRows; ++r) + { + mWVector[r] = (Real)0; + for (c = ip1; c < mNumCols; ++c) + { + mWVector[r] += mMatrix[c + mNumCols*r] * mVVector[c]; + } + mWVector[r] *= twoinvvdv; + } + + // Update the input matrix. + for (r = i; r < mNumRows; ++r) + { + for (c = ip1; c < mNumCols; ++c) + { + mMatrix[c + mNumCols*r] -= mWVector[r] * mVVector[c]; + } + } + + mTwoInvVTV[i] = twoinvvdv; + for (c = i + 2; c < mNumCols; ++c) + { + mMatrix[c + mNumCols*i] = mVVector[c]; + } + } + + mTwoInvUTU[i] = twoinvudu; + for (r = ip1; r < mNumRows; ++r) + { + mMatrix[i + mNumCols*r] = mUVector[r]; + } + } + + // Copy the diagonal and subdiagonal for cache coherence in the + // Golub-Kahan iterations. + int k, ksup = mNumCols - 1, index = 0, delta = mNumCols + 1; + for (k = 0; k < ksup; ++k, index += delta) + { + mDiagonal[k] = mMatrix[index]; + mSuperdiagonal[k] = mMatrix[index + 1]; + } + mDiagonal[k] = mMatrix[index]; +} + +template +void SingularValueDecomposition::GetSinCos(Real x, Real y, Real& cs, + Real& sn) +{ + // Solves sn*x + cs*y = 0 robustly. + Real tau; + if (y != (Real)0) + { + if (std::abs(y) > std::abs(x)) + { + tau = -x / y; + sn = ((Real)1) / std::sqrt(((Real)1) + tau*tau); + cs = sn * tau; + } + else + { + tau = -y / x; + cs = ((Real)1) / std::sqrt(((Real)1) + tau*tau); + sn = cs * tau; + } + } + else + { + cs = (Real)1; + sn = (Real)0; + } +} + +template +bool SingularValueDecomposition::DiagonalEntriesNonzero(int imin, + int imax, Real threshold) +{ + for (int i = imin; i <= imax; ++i) + { + if (std::abs(mDiagonal[i]) <= threshold) + { + // Use planar rotations to case the superdiagonal entry out of + // the matrix, thus producing a row of zeros. + Real x, z, cs, sn; + Real y = mSuperdiagonal[i]; + mSuperdiagonal[i] = (Real)0; + for (int j = i + 1; j <= imax + 1; ++j) + { + x = mDiagonal[j]; + GetSinCos(x, y, cs, sn); + mLGivens.push_back(GivensRotation(i, j, cs, sn)); + mDiagonal[j] = cs*x - sn*y; + if (j <= imax) + { + z = mSuperdiagonal[j]; + mSuperdiagonal[j] = cs*z; + y = sn*z; + } + } + return false; + } + } + return true; +} + +template +void SingularValueDecomposition::DoGolubKahanStep(int imin, int imax) +{ + // The implicit shift. Compute the eigenvalue u of the lower-right 2x2 + // block of A = B^T*B that is closer to b11. + Real f0 = (imax >= (Real)1 ? mSuperdiagonal[imax - 1] : (Real)0); + Real d1 = mDiagonal[imax]; + Real f1 = mSuperdiagonal[imax]; + Real d2 = mDiagonal[imax + 1]; + Real a00 = d1*d1 + f0*f0; + Real a01 = d1*f1; + Real a11 = d2*d2 + f1*f1; + Real dif = (a00 - a11) * (Real)0.5; + Real sgn = (dif >= (Real)0 ? (Real)1 : (Real)-1); + Real a01sqr = a01 * a01; + Real u = a11 - a01sqr / (dif + sgn * std::sqrt(dif*dif + a01sqr)); + Real x = mDiagonal[imin] * mDiagonal[imin] - u; + Real y = mDiagonal[imin] * mSuperdiagonal[imin]; + + Real a12, a21, a22, a23, cs, sn; + Real a02 = (Real)0; + int i0 = imin - 1, i1 = imin, i2 = imin + 1; + for (/**/; i1 <= imax; ++i0, ++i1, ++i2) + { + // Compute the Givens rotation G and save it for use in computing + // V in U^T*A*V = S. + GetSinCos(x, y, cs, sn); + mRGivens.push_back(GivensRotation(i1, i2, cs, sn)); + + // Update B0 = B*G. + if (i1 > imin) + { + mSuperdiagonal[i0] = cs*mSuperdiagonal[i0] - sn*a02; + } + + a11 = mDiagonal[i1]; + a12 = mSuperdiagonal[i1]; + a22 = mDiagonal[i2]; + mDiagonal[i1] = cs*a11 - sn*a12; + mSuperdiagonal[i1] = sn*a11 + cs*a12; + mDiagonal[i2] = cs*a22; + a21 = -sn*a22; + + // Update the parameters for the next Givens rotations. + x = mDiagonal[i1]; + y = a21; + + // Compute the Givens rotation G and save it for use in computing + // U in U^T*A*V = S. + GetSinCos(x, y, cs, sn); + mLGivens.push_back(GivensRotation(i1, i2, cs, sn)); + + // Update B1 = G^T*B0. + a11 = mDiagonal[i1]; + a12 = mSuperdiagonal[i1]; + a22 = mDiagonal[i2]; + mDiagonal[i1] = cs*a11 - sn*a21; + mSuperdiagonal[i1] = cs*a12 - sn*a22; + mDiagonal[i2] = sn*a12 + cs*a22; + + if (i1 < imax) + { + a23 = mSuperdiagonal[i2]; + a02 = -sn*a23; + mSuperdiagonal[i2] = cs*a23; + + // Update the parameters for the next Givens rotations. + x = mSuperdiagonal[i1]; + y = a02; + } + } +} + +template +void SingularValueDecomposition::EnsureNonnegativeDiagonal() +{ + for (int i = 0; i < mNumCols; ++i) + { + if (mDiagonal[i] >= (Real)0) + { + mFixupDiagonal[i] = (Real)1; + } + else + { + mDiagonal[i] = -mDiagonal[i]; + mFixupDiagonal[i] = (Real)-1; + } + } +} + +template +void SingularValueDecomposition::ComputePermutation(int sortType) +{ + if (sortType == 0) + { + // Set a flag for GetSingularValues() and GetOrthogonalMatrices() to + // know that sorted output was not requested. + mPermutation[0] = -1; + return; + } + + // Compute the permutation induced by sorting. Initially, we start with + // the identity permutation I = (0,1,...,N-1). + struct SortItem + { + Real singularValue; + int index; + }; + + std::vector items(mNumCols); + int i; + for (i = 0; i < mNumCols; ++i) + { + items[i].singularValue = mDiagonal[i]; + items[i].index = i; + } + + if (sortType > 0) + { + std::sort(items.begin(), items.end(), + [](SortItem const& item0, SortItem const& item1) + { + return item0.singularValue < item1.singularValue; + } + ); + } + else + { + std::sort(items.begin(), items.end(), + [](SortItem const& item0, SortItem const& item1) + { + return item0.singularValue > item1.singularValue; + } + ); + } + + i = 0; + for (auto const& item : items) + { + mPermutation[i++] = item.index; + } + + // GetOrthogonalMatrices() has nontrivial code for computing the + // orthogonal U and V from the reflections and rotations. To avoid + // complicating the code further when sorting is requested, U and V are + // computed as in the unsorted case. We then need to swap columns of + // U and V to be consistent with the sorting of the singular values. To + // minimize copying due to column swaps, we use permutation P. The + // minimum number of transpositions to obtain P from I is N minus the + // number of cycles of P. Each cycle is reordered with a minimum number + // of transpositions; that is, the singular items are cyclically swapped, + // leading to a minimum amount of copying. For example, if there is a + // cycle i0 -> i1 -> i2 -> i3, then the copying is + // save = singularitem[i0]; + // singularitem[i1] = singularitem[i2]; + // singularitem[i2] = singularitem[i3]; + // singularitem[i3] = save; +} + +template +SingularValueDecomposition::GivensRotation::GivensRotation() +{ + // No default initialization for fast creation of std::vector of objects + // of this type. +} + +template +SingularValueDecomposition::GivensRotation::GivensRotation(int inIndex0, + int inIndex1, Real inCs, Real inSn) + : + index0(inIndex0), + index1(inIndex1), + cs(inCs), + sn(inSn) +{ +} + + +} + + +// ---------------------------------------------- +// GteHyperplane.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.1 (2016/07/28) + +// The plane is represented as Dot(U,X) = c where U is a unit-length normal +// vector, c is the plane constant, and X is any point on the plane. The user +// must ensure that the normal vector is unit length. + +namespace houdini::gte +{ + +template +class Hyperplane +{ +public: + // Construction and destruction. The default constructor sets the normal + // to (0,...,0,1) and the constant to zero (plane z = 0). + Hyperplane(); + + // Specify U and c directly. + Hyperplane(Vector const& inNormal, Real inConstant); + + // U is specified, c = Dot(U,p) where p is a point on the hyperplane. + Hyperplane(Vector const& inNormal, Vector const& p); + + // U is a unit-length vector in the orthogonal complement of the set + // {p[1]-p[0],...,p[n-1]-p[0]} and c = Dot(U,p[0]), where the p[i] are + // pointson the hyperplane. + Hyperplane(std::array, N> const& p); + + // Public member access. + Vector normal; + Real constant; + +public: + // Comparisons to support sorted containers. + bool operator==(Hyperplane const& hyperplane) const; + bool operator!=(Hyperplane const& hyperplane) const; + bool operator< (Hyperplane const& hyperplane) const; + bool operator<=(Hyperplane const& hyperplane) const; + bool operator> (Hyperplane const& hyperplane) const; + bool operator>=(Hyperplane const& hyperplane) const; +}; + +// Template alias for convenience. +template +using Plane3 = Hyperplane<3, Real>; + + +template +Hyperplane::Hyperplane() + : + constant((Real)0) +{ + normal.MakeUnit(N - 1); +} + +template +Hyperplane::Hyperplane(Vector const& inNormal, + Real inConstant) + : + normal(inNormal), + constant(inConstant) +{ +} + +template +Hyperplane::Hyperplane(Vector const& inNormal, + Vector const& p) + : + normal(inNormal), + constant(Dot(inNormal, p)) +{ +} + +template +Hyperplane::Hyperplane(std::array, N> const& p) +{ + Matrix edge; + for (int i = 0; i < N - 1; ++i) + { + edge.SetCol(i, p[i + 1] - p[0]); + } + + // Compute the 1-dimensional orthogonal complement of the edges of the + // simplex formed by the points p[]. + SingularValueDecomposition svd(N, N - 1, 32); + svd.Solve(&edge[0], -1); + svd.GetUColumn(N - 1, &normal[0]); + + constant = Dot(normal, p[0]); +} + +template +bool Hyperplane::operator==(Hyperplane const& hyperplane) const +{ + return normal == hyperplane.normal && constant == hyperplane.constant; +} + +template +bool Hyperplane::operator!=(Hyperplane const& hyperplane) const +{ + return !operator==(hyperplane); +} + +template +bool Hyperplane::operator<(Hyperplane const& hyperplane) const +{ + if (normal < hyperplane.normal) + { + return true; + } + + if (normal > hyperplane.normal) + { + return false; + } + + return constant < hyperplane.constant; +} + +template +bool Hyperplane::operator<=(Hyperplane const& hyperplane) const +{ + return operator<(hyperplane) || operator==(hyperplane); +} + +template +bool Hyperplane::operator>(Hyperplane const& hyperplane) const +{ + return !operator<=(hyperplane); +} + +template +bool Hyperplane::operator>=(Hyperplane const& hyperplane) const +{ + return !operator<(hyperplane); +} + + +} + + + +// ---------------------------------------------- +// GteConvexHull3.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// Compute the convex hull of 3D points using incremental insertion. The only +// way to ensure a correct result for the input vertices (assumed to be exact) +// is to choose ComputeType for exact rational arithmetic. You may use +// BSNumber. No divisions are performed in this computation, so you do not +// have to use BSRational. +// +// The worst-case choices of N for Real of type BSNumber or BSRational with +// integer storage UIntegerFP32 are listed in the next table. The numerical +// computations are encapsulated in PrimalQuery3::ToPlane. We recommend +// using only BSNumber, because no divisions are performed in the convex-hull +// computations. +// +// input type | compute type | N +// -----------+--------------+------ +// float | BSNumber | 27 +// double | BSNumber | 197 +// float | BSRational | 2882 +// double | BSRational | 21688 + +namespace houdini::gte +{ + +template +class ConvexHull3 +{ +public: + // The class is a functor to support computing the convex hull of multiple + // data sets using the same class object. For multithreading in Update, + // choose 'numThreads' subject to the constraints + // 1 <= numThreads <= std::thread::hardware_concurrency(). + ConvexHull3(unsigned int numThreads = 1); + + // The input is the array of points whose convex hull is required. The + // epsilon value is used to determine the intrinsic dimensionality of the + // vertices (d = 0, 1, 2, or 3). When epsilon is positive, the + // determination is fuzzy--points approximately the same point, + // approximately on a line, approximately planar, or volumetric. + bool operator()(int numPoints, Vector3 const* points, + InputType epsilon); + + // Dimensional information. If GetDimension() returns 1, the points lie + // on a line P+t*D (fuzzy comparison when epsilon > 0). You can sort + // these if you need a polyline output by projecting onto the line each + // vertex X = P+t*D, where t = Dot(D,X-P). If GetDimension() returns 2, + // the points line on a plane P+s*U+t*V (fuzzy comparison when + // epsilon > 0). You can project each point X = P+s*U+t*V, where + // s = Dot(U,X-P) and t = Dot(V,X-P), then apply ConvexHull2 to the (s,t) + // tuples. + inline InputType GetEpsilon() const; + inline int GetDimension() const; + inline Line3 const& GetLine() const; + inline Plane3 const& GetPlane() const; + + // Member access. + inline int GetNumPoints() const; + inline int GetNumUniquePoints() const; + inline Vector3 const* GetPoints() const; + inline PrimalQuery3 const& GetQuery() const; + + // The convex hull is a convex polyhedron with triangular faces. + inline std::vector> const& GetHullUnordered() const; + ETManifoldMesh const& GetHullMesh() const; + +private: + // Support for incremental insertion. + void Update(int i); + + // The epsilon value is used for fuzzy determination of intrinsic + // dimensionality. If the dimension is 0, 1, or 2, the constructor + // returns early. The caller is responsible for retrieving the dimension + // and taking an alternate path should the dimension be smaller than 3. + // If the dimension is 0, the caller may as well treat all points[] as a + // single point, say, points[0]. If the dimension is 1, the caller can + // query for the approximating line and project points[] onto it for + // further processing. If the dimension is 2, the caller can query for + // the approximating plane and project points[] onto it for further + // processing. + InputType mEpsilon; + int mDimension; + Line3 mLine; + Plane3 mPlane; + + // The array of points used for geometric queries. If you want to be + // certain of a correct result, choose ComputeType to be BSNumber. + std::vector> mComputePoints; + PrimalQuery3 mQuery; + + int mNumPoints; + int mNumUniquePoints; + Vector3 const* mPoints; + std::vector> mHullUnordered; + mutable ETManifoldMesh mHullMesh; + unsigned int mNumThreads; +}; + + +template +ConvexHull3::ConvexHull3(unsigned int numThreads) + : + mEpsilon((InputType)0), + mDimension(0), + mLine(Vector3::Zero(), Vector3::Zero()), + mPlane(Vector3::Zero(), (InputType)0), + mNumPoints(0), + mNumUniquePoints(0), + mPoints(nullptr), + mNumThreads(numThreads) +{ +} + +template +bool ConvexHull3::operator()(int numPoints, + Vector3 const* points, InputType epsilon) +{ + mEpsilon = std::max(epsilon, (InputType)0); + mDimension = 0; + mLine.origin = Vector3::Zero(); + mLine.direction = Vector3::Zero(); + mPlane.normal = Vector3::Zero(); + mPlane.constant = (InputType)0; + mNumPoints = numPoints; + mNumUniquePoints = 0; + mPoints = points; + mHullUnordered.clear(); + mHullMesh.Clear(); + + int i, j; + if (mNumPoints < 4) + { + // ConvexHull3 should be called with at least four points. + return false; + } + + IntrinsicsVector3 info(mNumPoints, mPoints, mEpsilon); + if (info.dimension == 0) + { + // The set is (nearly) a point. + return false; + } + + if (info.dimension == 1) + { + // The set is (nearly) collinear. + mDimension = 1; + mLine = Line3(info.origin, info.direction[0]); + return false; + } + + if (info.dimension == 2) + { + // The set is (nearly) coplanar. + mDimension = 2; + mPlane = Plane3(UnitCross(info.direction[0], + info.direction[1]), info.origin); + return false; + } + + mDimension = 3; + + // Compute the vertices for the queries. + mComputePoints.resize(mNumPoints); + mQuery.Set(mNumPoints, &mComputePoints[0]); + for (i = 0; i < mNumPoints; ++i) + { + for (j = 0; j < 3; ++j) + { + mComputePoints[i][j] = points[i][j]; + } + } + + // Insert the faces of the (nondegenerate) tetrahedron constructed by the + // call to GetInformation. + if (!info.extremeCCW) + { + std::swap(info.extreme[2], info.extreme[3]); + } + + mHullUnordered.push_back(TriangleKey(info.extreme[1], + info.extreme[2], info.extreme[3])); + mHullUnordered.push_back(TriangleKey(info.extreme[0], + info.extreme[3], info.extreme[2])); + mHullUnordered.push_back(TriangleKey(info.extreme[0], + info.extreme[1], info.extreme[3])); + mHullUnordered.push_back(TriangleKey(info.extreme[0], + info.extreme[2], info.extreme[1])); + + // Incrementally update the hull. The set of processed points is + // maintained to eliminate duplicates, either in the original input + // points or in the points obtained by snap rounding. + std::set> processed; + for (i = 0; i < 4; ++i) + { + processed.insert(points[info.extreme[i]]); + } + for (i = 0; i < mNumPoints; ++i) + { + if (processed.find(points[i]) == processed.end()) + { + Update(i); + processed.insert(points[i]); + } + } + mNumUniquePoints = static_cast(processed.size()); + return true; +} + +template inline +InputType ConvexHull3::GetEpsilon() const +{ + return mEpsilon; +} + +template inline +int ConvexHull3::GetDimension() const +{ + return mDimension; +} + +template inline +Line3 const& ConvexHull3::GetLine() const +{ + return mLine; +} + +template inline +Plane3 const& ConvexHull3::GetPlane() const +{ + return mPlane; +} + +template inline +int ConvexHull3::GetNumPoints() const +{ + return mNumPoints; +} + +template inline +int ConvexHull3::GetNumUniquePoints() const +{ + return mNumUniquePoints; +} + +template inline +Vector3 const* ConvexHull3::GetPoints() const +{ + return mPoints; +} + +template inline +PrimalQuery3 const& ConvexHull3::GetQuery() const +{ + return mQuery; +} + +template inline +std::vector> const& ConvexHull3::GetHullUnordered() const +{ + return mHullUnordered; +} + +template +ETManifoldMesh const& ConvexHull3::GetHullMesh() const +{ + // Create the mesh only on demand. + if (mHullMesh.GetTriangles().size() == 0) + { + for (auto const& tri : mHullUnordered) + { + mHullMesh.Insert(tri.V[0], tri.V[1], tri.V[2]); + } + } + + return mHullMesh; +} + +template +void ConvexHull3::Update(int i) +{ + // The terminator that separates visible faces from nonvisible faces is + // constructed by this code. Visible faces for the incoming hull are + // removed, and the boundary of that set of triangles is the terminator. + // New visible faces are added using the incoming point and the edges of + // the terminator. + // + // A simple algorithm for computing terminator edges is the following. + // Back-facing triangles are located and the three edges are processed. + // The first time an edge is visited, insert it into the terminator. If + // it is visited a second time, the edge is removed because it is shared + // by another back-facing triangle and, therefore, cannot be a terminator + // edge. After visiting all back-facing triangles, the only remaining + // edges in the map are the terminator edges. + // + // The order of vertices of an edge is important for adding new faces with + // the correct vertex winding. However, the edge "toggle" (insert edge, + // remove edge) should use edges with unordered vertices, because the + // edge shared by one triangle has opposite ordering relative to that of + // the other triangle. The map uses unordered edges as the keys but + // stores the ordered edge as the value. This avoids having to look up + // an edge twice in a map with ordered edge keys. + + unsigned int numFaces = static_cast(mHullUnordered.size()); + std::vector queryResult(numFaces); + if (mNumThreads > 1 && numFaces >= mNumThreads) + { + // Partition the data for multiple threads. + unsigned int numFacesPerThread = numFaces / mNumThreads; + std::vector jmin(mNumThreads), jmax(mNumThreads); + for (unsigned int t = 0; t < mNumThreads; ++t) + { + jmin[t] = t * numFacesPerThread; + jmax[t] = jmin[t] + numFacesPerThread - 1; + } + jmax[mNumThreads - 1] = numFaces - 1; + + // Execute the point-plane queries in multiple threads. + std::vector process(mNumThreads); + for (unsigned int t = 0; t < mNumThreads; ++t) + { + process[t] = std::thread([this, i, t, &jmin, &jmax, + &queryResult]() + { + for (unsigned int j = jmin[t]; j <= jmax[t]; ++j) + { + TriangleKey const& tri = mHullUnordered[j]; + queryResult[j] = mQuery.ToPlane(i, tri.V[0], tri.V[1], tri.V[2]); + } + }); + } + + // Wait for all threads to finish. + for (unsigned int t = 0; t < mNumThreads; ++t) + { + process[t].join(); + } + } + else + { + unsigned int j = 0; + for (auto const& tri : mHullUnordered) + { + queryResult[j++] = mQuery.ToPlane(i, tri.V[0], tri.V[1], tri.V[2]); + } + } + + std::map, std::pair> terminator; + std::vector> backFaces; + bool existsFrontFacingTriangle = false; + unsigned int j = 0; + for (auto const& tri : mHullUnordered) + { + if (queryResult[j++] <= 0) + { + // The triangle is back facing. These include triangles that + // are coplanar with the incoming point. + backFaces.push_back(tri); + + // The current hull is a 2-manifold watertight mesh. The + // terminator edges are those shared with a front-facing triangle. + // The first time an edge of a back-facing triangle is visited, + // insert it into the terminator. If it is visited a second time, + // the edge is removed because it is shared by another back-facing + // triangle. After all back-facing triangles are visited, the + // only remaining edges are shared by a single back-facing + // triangle, which makes them terminator edges. + for (int j0 = 2, j1 = 0; j1 < 3; j0 = j1++) + { + int v0 = tri.V[j0], v1 = tri.V[j1]; + EdgeKey edge(v0, v1); + auto iter = terminator.find(edge); + if (iter == terminator.end()) + { + // The edge is visited for the first time. + terminator.insert(std::make_pair(edge, std::make_pair(v0, v1))); + } + else + { + // The edge is visited for the second time. + terminator.erase(edge); + } + } + } + else + { + // If there are no strictly front-facing triangles, then the + // incoming point is inside or on the convex hull. If we get + // to this code, then the point is truly outside and we can + // update the hull. + existsFrontFacingTriangle = true; + } + } + + if (!existsFrontFacingTriangle) + { + // The incoming point is inside or on the current hull, so no + // update of the hull is necessary. + return; + } + + // The updated hull contains the triangles not visible to the incoming + // point. + mHullUnordered = backFaces; + + // Insert the triangles formed by the incoming point and the terminator + // edges. + for (auto const& edge : terminator) + { + mHullUnordered.push_back(TriangleKey(i, edge.second.second, edge.second.first)); + } +} + + +} + + +// ---------------------------------------------- +// GteVector2.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.2 (2018/10/04) + +namespace houdini::gte +{ + +// Template alias for convenience. +template +using Vector2 = Vector<2, Real>; + +// Compute the perpendicular using the formal determinant, +// perp = det{{e0,e1},{x0,x1}} = (x1,-x0) +// where e0 = (1,0), e1 = (0,1), and v = (x0,x1). +template +Vector2 Perp(Vector2 const& v); + +// Compute the normalized perpendicular. +template +Vector2 UnitPerp(Vector2 const& v, bool robust = false); + +// Compute Dot((x0,x1),Perp(y0,y1)) = x0*y1 - x1*y0, where v0 = (x0,x1) +// and v1 = (y0,y1). +template +Real DotPerp(Vector2 const& v0, Vector2 const& v1); + +// Compute a right-handed orthonormal basis for the orthogonal complement +// of the input vectors. The function returns the smallest length of the +// unnormalized vectors computed during the process. If this value is nearly +// zero, it is possible that the inputs are linearly dependent (within +// numerical round-off errors). On input, numInputs must be 1 and v[0] +// must be initialized. On output, the vectors v[0] and v[1] form an +// orthonormal set. +template +Real ComputeOrthogonalComplement(int numInputs, Vector2* v, bool robust = false); + +// Compute the barycentric coordinates of the point P with respect to the +// triangle , P = b0*V0 + b1*V1 + b2*V2, where b0 + b1 + b2 = 1. +// The return value is 'true' iff {V0,V1,V2} is a linearly independent set. +// Numerically, this is measured by |det[V0 V1 V2]| <= epsilon. The values +// bary[] are valid only when the return value is 'true' but set to zero when +// the return value is 'false'. +template +bool ComputeBarycentrics(Vector2 const& p, Vector2 const& v0, + Vector2 const& v1, Vector2 const& v2, Real bary[3], + Real epsilon = (Real)0); + +// Get intrinsic information about the input array of vectors. The return +// value is 'true' iff the inputs are valid (numVectors > 0, v is not null, +// and epsilon >= 0), in which case the class members are valid. +template +class IntrinsicsVector2 +{ +public: + // The constructor sets the class members based on the input set. + IntrinsicsVector2(int numVectors, Vector2 const* v, Real inEpsilon); + + // A nonnegative tolerance that is used to determine the intrinsic + // dimension of the set. + Real epsilon; + + // The intrinsic dimension of the input set, computed based on the + // nonnegative tolerance mEpsilon. + int dimension; + + // Axis-aligned bounding box of the input set. The maximum range is + // the larger of max[0]-min[0] and max[1]-min[1]. + Real min[2], max[2]; + Real maxRange; + + // Coordinate system. The origin is valid for any dimension d. The + // unit-length direction vector is valid only for 0 <= i < d. The + // extreme index is relative to the array of input points, and is also + // valid only for 0 <= i < d. If d = 0, all points are effectively + // the same, but the use of an epsilon may lead to an extreme index + // that is not zero. If d = 1, all points effectively lie on a line + // segment. If d = 2, the points are not collinear. + Vector2 origin; + Vector2 direction[2]; + + // The indices that define the maximum dimensional extents. The + // values extreme[0] and extreme[1] are the indices for the points + // that define the largest extent in one of the coordinate axis + // directions. If the dimension is 2, then extreme[2] is the index + // for the point that generates the largest extent in the direction + // perpendicular to the line through the points corresponding to + // extreme[0] and extreme[1]. The triangle formed by the points + // V[extreme[0]], V[extreme[1]], and V[extreme[2]] is clockwise or + // counterclockwise, the condition stored in extremeCCW. + int extreme[3]; + bool extremeCCW; +}; + + +template +Vector2 Perp(Vector2 const& v) +{ + return Vector2{ v[1], -v[0] }; +} + +template +Vector2 UnitPerp(Vector2 const& v, bool robust) +{ + Vector2 unitPerp{ v[1], -v[0] }; + Normalize(unitPerp, robust); + return unitPerp; +} + +template +Real DotPerp(Vector2 const& v0, Vector2 const& v1) +{ + return Dot(v0, Perp(v1)); +} + +template +Real ComputeOrthogonalComplement(int numInputs, Vector2* v, bool robust) +{ + if (numInputs == 1) + { + v[1] = -Perp(v[0]); + return Orthonormalize<2, Real>(2, v, robust); + } + + return (Real)0; +} + +template +bool ComputeBarycentrics(Vector2 const& p, Vector2 const& v0, + Vector2 const& v1, Vector2 const& v2, Real bary[3], + Real epsilon) +{ + // Compute the vectors relative to V2 of the triangle. + Vector2 diff[3] = { v0 - v2, v1 - v2, p - v2 }; + + Real det = DotPerp(diff[0], diff[1]); + if (det < -epsilon || det > epsilon) + { + Real invDet = ((Real)1) / det; + bary[0] = DotPerp(diff[2], diff[1])*invDet; + bary[1] = DotPerp(diff[0], diff[2])*invDet; + bary[2] = (Real)1 - bary[0] - bary[1]; + return true; + } + + for (int i = 0; i < 3; ++i) + { + bary[i] = (Real)0; + } + return false; +} + +template +IntrinsicsVector2::IntrinsicsVector2(int numVectors, + Vector2 const* v, Real inEpsilon) + : + epsilon(inEpsilon), + dimension(0), + maxRange((Real)0), + origin({ (Real)0, (Real)0 }), + extremeCCW(false) +{ + min[0] = (Real)0; + min[1] = (Real)0; + direction[0] = { (Real)0, (Real)0 }; + direction[1] = { (Real)0, (Real)0 }; + extreme[0] = 0; + extreme[1] = 0; + extreme[2] = 0; + + if (numVectors > 0 && v && epsilon >= (Real)0) + { + // Compute the axis-aligned bounding box for the input vectors. Keep + // track of the indices into 'vectors' for the current min and max. + int j, indexMin[2], indexMax[2]; + for (j = 0; j < 2; ++j) + { + min[j] = v[0][j]; + max[j] = min[j]; + indexMin[j] = 0; + indexMax[j] = 0; + } + + int i; + for (i = 1; i < numVectors; ++i) + { + for (j = 0; j < 2; ++j) + { + if (v[i][j] < min[j]) + { + min[j] = v[i][j]; + indexMin[j] = i; + } + else if (v[i][j] > max[j]) + { + max[j] = v[i][j]; + indexMax[j] = i; + } + } + } + + // Determine the maximum range for the bounding box. + maxRange = max[0] - min[0]; + extreme[0] = indexMin[0]; + extreme[1] = indexMax[0]; + Real range = max[1] - min[1]; + if (range > maxRange) + { + maxRange = range; + extreme[0] = indexMin[1]; + extreme[1] = indexMax[1]; + } + + // The origin is either the vector of minimum x0-value or vector of + // minimum x1-value. + origin = v[extreme[0]]; + + // Test whether the vector set is (nearly) a vector. + if (maxRange <= epsilon) + { + dimension = 0; + for (j = 0; j < 2; ++j) + { + extreme[j + 1] = extreme[0]; + } + return; + } + + // Test whether the vector set is (nearly) a line segment. We need + // direction[1] to span the orthogonal complement of direction[0]. + direction[0] = v[extreme[1]] - origin; + Normalize(direction[0], false); + direction[1] = -Perp(direction[0]); + + // Compute the maximum distance of the points from the line + // origin+t*direction[0]. + Real maxDistance = (Real)0; + Real maxSign = (Real)0; + extreme[2] = extreme[0]; + for (i = 0; i < numVectors; ++i) + { + Vector2 diff = v[i] - origin; + Real distance = Dot(direction[1], diff); + Real sign = (distance >(Real)0 ? (Real)1 : + (distance < (Real)0 ? (Real)-1 : (Real)0)); + distance = std::abs(distance); + if (distance > maxDistance) + { + maxDistance = distance; + maxSign = sign; + extreme[2] = i; + } + } + + if (maxDistance <= epsilon * maxRange) + { + // The points are (nearly) on the line origin+t*direction[0]. + dimension = 1; + extreme[2] = extreme[1]; + return; + } + + dimension = 2; + extremeCCW = (maxSign > (Real)0); + return; + } +} + + +} + + +// ---------------------------------------------- +// GtePrimalQuery2.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// Queries about the relation of a point to various geometric objects. The +// choices for N when using UIntegerFP32 for either BSNumber of BSRational +// are determined in GeometricTools/GTEngine/Tools/PrecisionCalculator. These +// N-values are worst case scenarios. Your specific input data might require +// much smaller N, in which case you can modify PrecisionCalculator to use the +// BSPrecision(int32_t,int32_t,int32_t,bool) constructors. + +namespace houdini::gte +{ + +template +class PrimalQuery2 +{ +public: + // The caller is responsible for ensuring that the array is not empty + // before calling queries and that the indices passed to the queries are + // valid. The class does no range checking. + PrimalQuery2(); + PrimalQuery2(int numVertices, Vector2 const* vertices); + + // Member access. + inline void Set(int numVertices, Vector2 const* vertices); + inline int GetNumVertices() const; + inline Vector2 const* GetVertices() const; + + // In the following, point P refers to vertices[i] or 'test' and Vi refers + // to vertices[vi]. + + // For a line with origin V0 and direction , ToLine returns + // +1, P on right of line + // -1, P on left of line + // 0, P on the line + // + // Choice of N for UIntegerFP32. + // input type | compute type | N + // -----------+--------------+----- + // float | BSNumber | 18 + // double | BSNumber | 132 + // float | BSRational | 214 + // double | BSRational | 1587 + int ToLine(int i, int v0, int v1) const; + int ToLine(Vector2 const& test, int v0, int v1) const; + + // For a line with origin V0 and direction , ToLine returns + // +1, P on right of line + // -1, P on left of line + // 0, P on the line + // The 'order' parameter is + // -3, points not collinear, P on left of line + // -2, P strictly left of V0 on the line + // -1, P = V0 + // 0, P interior to line segment [V0,V1] + // +1, P = V1 + // +2, P strictly right of V0 on the line + // + // Choice of N for UIntegerFP32. + // input type | compute type | N + // -----------+--------------+----- + // float | BSNumber | 18 + // double | BSNumber | 132 + // float | BSRational | 214 + // double | BSRational | 1587 + // This is the same as the first-listed ToLine calls because the worst-case + // path has the same computational complexity. + int ToLine(int i, int v0, int v1, int& order) const; + int ToLine(Vector2 const& test, int v0, int v1, int& order) const; + + // For a triangle with counterclockwise vertices V0, V1, and V2, + // ToTriangle returns + // +1, P outside triangle + // -1, P inside triangle + // 0, P on triangle + // + // Choice of N for UIntegerFP32. + // input type | compute type | N + // -----------+--------------+----- + // float | BSNumber | 18 + // double | BSNumber | 132 + // float | BSRational | 214 + // double | BSRational | 1587 + // The query involves three calls to ToLine, so the numbers match those + // of ToLine. + int ToTriangle(int i, int v0, int v1, int v2) const; + int ToTriangle(Vector2 const& test, int v0, int v1, int v2) const; + + // For a triangle with counterclockwise vertices V0, V1, and V2, + // ToCircumcircle returns + // +1, P outside circumcircle of triangle + // -1, P inside circumcircle of triangle + // 0, P on circumcircle of triangle + // + // Choice of N for UIntegerFP32. + // input type | compute type | N + // -----------+--------------+------ + // float | BSNumber | 35 + // double | BSNumber | 263 + // float | BSRational | 12302 + // double | BSRational | 92827 + // The query involves three calls of ToLine, so the numbers match those + // of ToLine. + int ToCircumcircle(int i, int v0, int v1, int v2) const; + int ToCircumcircle(Vector2 const& test, int v0, int v1, int v2) const; + + // An extended classification of the relationship of a point to a line + // segment. For noncollinear points, the return value is + // ORDER_POSITIVE when is a counterclockwise triangle + // ORDER_NEGATIVE when is a clockwise triangle + // For collinear points, the line direction is Q1-Q0. The return value is + // ORDER_COLLINEAR_LEFT when the line ordering is + // ORDER_COLLINEAR_RIGHT when the line ordering is + // ORDER_COLLINEAR_CONTAIN when the line ordering is + enum OrderType + { + ORDER_Q0_EQUALS_Q1, + ORDER_P_EQUALS_Q0, + ORDER_P_EQUALS_Q1, + ORDER_POSITIVE, + ORDER_NEGATIVE, + ORDER_COLLINEAR_LEFT, + ORDER_COLLINEAR_RIGHT, + ORDER_COLLINEAR_CONTAIN + }; + + // Choice of N for UIntegerFP32. + // input type | compute type | N + // -----------+--------------+----- + // float | BSNumber | 18 + // double | BSNumber | 132 + // float | BSRational | 214 + // double | BSRational | 1587 + // This is the same as the first-listed ToLine calls because the worst-case + // path has the same computational complexity. + OrderType ToLineExtended(Vector2 const& P, Vector2 const& Q0, Vector2 const& Q1) const; + +private: + int mNumVertices; + Vector2 const* mVertices; +}; + + +template +PrimalQuery2::PrimalQuery2() + : + mNumVertices(0), + mVertices(nullptr) +{ +} + +template +PrimalQuery2::PrimalQuery2(int numVertices, + Vector2 const* vertices) + : + mNumVertices(numVertices), + mVertices(vertices) +{ +} + +template inline +void PrimalQuery2::Set(int numVertices, Vector2 const* vertices) +{ + mNumVertices = numVertices; + mVertices = vertices; +} + +template inline +int PrimalQuery2::GetNumVertices() const +{ + return mNumVertices; +} + +template inline +Vector2 const* PrimalQuery2::GetVertices() const +{ + return mVertices; +} + +template +int PrimalQuery2::ToLine(int i, int v0, int v1) const +{ + return ToLine(mVertices[i], v0, v1); +} + +template +int PrimalQuery2::ToLine(Vector2 const& test, int v0, int v1) const +{ + Vector2 const& vec0 = mVertices[v0]; + Vector2 const& vec1 = mVertices[v1]; + + Real x0 = test[0] - vec0[0]; + Real y0 = test[1] - vec0[1]; + Real x1 = vec1[0] - vec0[0]; + Real y1 = vec1[1] - vec0[1]; + Real x0y1 = x0*y1; + Real x1y0 = x1*y0; + Real det = x0y1 - x1y0; + Real const zero(0); + + return (det > zero ? +1 : (det < zero ? -1 : 0)); +} + +template +int PrimalQuery2::ToLine(int i, int v0, int v1, int& order) const +{ + return ToLine(mVertices[i], v0, v1, order); +} + +template +int PrimalQuery2::ToLine(Vector2 const& test, int v0, int v1, int& order) const +{ + Vector2 const& vec0 = mVertices[v0]; + Vector2 const& vec1 = mVertices[v1]; + + Real x0 = test[0] - vec0[0]; + Real y0 = test[1] - vec0[1]; + Real x1 = vec1[0] - vec0[0]; + Real y1 = vec1[1] - vec0[1]; + Real x0y1 = x0*y1; + Real x1y0 = x1*y0; + Real det = x0y1 - x1y0; + Real const zero(0); + + if (det > zero) + { + order = +3; + return +1; + } + + if (det < zero) + { + order = -3; + return -1; + } + + Real x0x1 = x0*x1; + Real y0y1 = y0*y1; + Real dot = x0x1 + y0y1; + if (dot == zero) + { + order = -1; + } + else if (dot < zero) + { + order = -2; + } + else + { + Real x0x0 = x0*x0; + Real y0y0 = y0*y0; + Real sqrLength = x0x0 + y0y0; + if (dot == sqrLength) + { + order = +1; + } + else if (dot > sqrLength) + { + order = +2; + } + else + { + order = 0; + } + } + + return 0; +} + +template +int PrimalQuery2::ToTriangle(int i, int v0, int v1, int v2) const +{ + return ToTriangle(mVertices[i], v0, v1, v2); +} + +template +int PrimalQuery2::ToTriangle(Vector2 const& test, int v0, int v1, int v2) const +{ + int sign0 = ToLine(test, v1, v2); + if (sign0 > 0) + { + return +1; + } + + int sign1 = ToLine(test, v0, v2); + if (sign1 < 0) + { + return +1; + } + + int sign2 = ToLine(test, v0, v1); + if (sign2 > 0) + { + return +1; + } + + return ((sign0 && sign1 && sign2) ? -1 : 0); +} + +template +int PrimalQuery2::ToCircumcircle(int i, int v0, int v1, int v2) const +{ + return ToCircumcircle(mVertices[i], v0, v1, v2); +} + +template +int PrimalQuery2::ToCircumcircle(Vector2 const& test, int v0, int v1, int v2) const +{ + Vector2 const& vec0 = mVertices[v0]; + Vector2 const& vec1 = mVertices[v1]; + Vector2 const& vec2 = mVertices[v2]; + + Real x0 = vec0[0] - test[0]; + Real y0 = vec0[1] - test[1]; + Real s00 = vec0[0] + test[0]; + Real s01 = vec0[1] + test[1]; + Real t00 = s00*x0; + Real t01 = s01*y0; + Real z0 = t00 + t01; + + Real x1 = vec1[0] - test[0]; + Real y1 = vec1[1] - test[1]; + Real s10 = vec1[0] + test[0]; + Real s11 = vec1[1] + test[1]; + Real t10 = s10*x1; + Real t11 = s11*y1; + Real z1 = t10 + t11; + + Real x2 = vec2[0] - test[0]; + Real y2 = vec2[1] - test[1]; + Real s20 = vec2[0] + test[0]; + Real s21 = vec2[1] + test[1]; + Real t20 = s20*x2; + Real t21 = s21*y2; + Real z2 = t20 + t21; + + Real y0z1 = y0*z1; + Real y0z2 = y0*z2; + Real y1z0 = y1*z0; + Real y1z2 = y1*z2; + Real y2z0 = y2*z0; + Real y2z1 = y2*z1; + Real c0 = y1z2 - y2z1; + Real c1 = y2z0 - y0z2; + Real c2 = y0z1 - y1z0; + Real x0c0 = x0*c0; + Real x1c1 = x1*c1; + Real x2c2 = x2*c2; + Real term = x0c0 + x1c1; + Real det = term + x2c2; + Real const zero(0); + + return (det < zero ? 1 : (det > zero ? -1 : 0)); +} + +template +typename PrimalQuery2::OrderType PrimalQuery2::ToLineExtended( + Vector2 const& P, Vector2 const& Q0, Vector2 const& Q1) const +{ + Real const zero(0); + + Real x0 = Q1[0] - Q0[0]; + Real y0 = Q1[1] - Q0[1]; + if (x0 == zero && y0 == zero) + { + return ORDER_Q0_EQUALS_Q1; + } + + Real x1 = P[0] - Q0[0]; + Real y1 = P[1] - Q0[1]; + if (x1 == zero && y1 == zero) + { + return ORDER_P_EQUALS_Q0; + } + + Real x2 = P[0] - Q1[0]; + Real y2 = P[1] - Q1[1]; + if (x2 == zero && y2 == zero) + { + return ORDER_P_EQUALS_Q1; + } + + // The theoretical classification relies on computing exactly the sign of + // the determinant. Numerical roundoff errors can cause misclassification. + Real x0y1 = x0 * y1; + Real x1y0 = x1 * y0; + Real det = x0y1 - x1y0; + + if (det != zero) + { + if (det > zero) + { + // The points form a counterclockwise triangle . + return ORDER_POSITIVE; + } + else + { + // The points form a clockwise triangle . + return ORDER_NEGATIVE; + } + } + else + { + // The points are collinear; P is on the line through Q0 and Q1. + Real x0x1 = x0 * x1; + Real y0y1 = y0 * y1; + Real dot = x0x1 + y0y1; + if (dot < zero) + { + // The line ordering is . + return ORDER_COLLINEAR_LEFT; + } + + Real x0x0 = x0 * x0; + Real y0y0 = y0 * y0; + Real sqrLength = x0x0 + y0y0; + if (dot > sqrLength) + { + // The line ordering is . + return ORDER_COLLINEAR_RIGHT; + } + + // The line ordering is with P strictly between Q0 and Q1. + return ORDER_COLLINEAR_CONTAIN; + } +} + + +} + + +// ---------------------------------------------- +// GteConvexHull2.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// Compute the convex hull of 2D points using a divide-and-conquer algorithm. +// This is an O(N log N) algorithm for N input points. The only way to ensure +// a correct result for the input vertices (assumed to be exact) is to choose +// ComputeType for exact rational arithmetic. You may use BSNumber. No +// divisions are performed in this computation, so you do not have to use +// BSRational. +// +// The worst-case choices of N for Real of type BSNumber or BSRational with +// integer storage UIntegerFP32 are listed in the next table. The numerical +// computations are encapsulated in PrimalQuery2::ToLineExtended. We +// recommend using only BSNumber, because no divisions are performed in the +// convex-hull computations. +// +// input type | compute type | N +// -----------+--------------+------ +// float | BSNumber | 18 +// double | BSNumber | 132 +// float | BSRational | 214 +// double | BSRational | 1587 + +namespace houdini::gte +{ + +template +class ConvexHull2 +{ +public: + // The class is a functor to support computing the convex hull of multiple + // data sets using the same class object. + ConvexHull2(); + + // The input is the array of points whose convex hull is required. The + // epsilon value is used to determine the intrinsic dimensionality of the + // vertices (d = 0, 1, or 2). When epsilon is positive, the determination + // is fuzzy--points approximately the same point, approximately on a + // line, or planar. The return value is 'true' if and only if the hull + // construction is successful. + bool operator()(int numPoints, Vector2 const* points, InputType epsilon); + + // Dimensional information. If GetDimension() returns 1, the points lie + // on a line P+t*D (fuzzy comparison when epsilon > 0). You can sort + // these if you need a polyline output by projecting onto the line each + // vertex X = P+t*D, where t = Dot(D,X-P). + inline InputType GetEpsilon() const; + inline int GetDimension() const; + inline Line2 const& GetLine() const; + + // Member access. + inline int GetNumPoints() const; + inline int GetNumUniquePoints() const; + inline Vector2 const* GetPoints() const; + inline PrimalQuery2 const& GetQuery() const; + + // The convex hull is a convex polygon whose vertices are listed in + // counterclockwise order. + inline std::vector const& GetHull() const; + +private: + // Support for divide-and-conquer. + void GetHull(int& i0, int& i1); + void Merge(int j0, int j1, int j2, int j3, int& i0, int& i1); + void GetTangent(int j0, int j1, int j2, int j3, int& i0, int& i1); + + // The epsilon value is used for fuzzy determination of intrinsic + // dimensionality. If the dimension is 0 or 1, the constructor returns + // early. The caller is responsible for retrieving the dimension and + // taking an alternate path should the dimension be smaller than 2. If + // the dimension is 0, the caller may as well treat all points[] as a + // single point, say, points[0]. If the dimension is 1, the caller can + // query for the approximating line and project points[] onto it for + // further processing. + InputType mEpsilon; + int mDimension; + Line2 mLine; + + // The array of points used for geometric queries. If you want to be + // certain of a correct result, choose ComputeType to be BSNumber. + std::vector> mComputePoints; + PrimalQuery2 mQuery; + + int mNumPoints; + int mNumUniquePoints; + Vector2 const* mPoints; + std::vector mMerged, mHull; +}; + + +template +ConvexHull2::ConvexHull2() + : + mEpsilon((InputType)0), + mDimension(0), + mLine(Vector2::Zero(), Vector2::Zero()), + mNumPoints(0), + mNumUniquePoints(0), + mPoints(nullptr) +{ +} + +template +bool ConvexHull2::operator()(int numPoints, + Vector2 const* points, InputType epsilon) +{ + mEpsilon = std::max(epsilon, (InputType)0); + mDimension = 0; + mLine.origin = Vector2::Zero(); + mLine.direction = Vector2::Zero(); + mNumPoints = numPoints; + mNumUniquePoints = 0; + mPoints = points; + mMerged.clear(); + mHull.clear(); + + int i, j; + if (mNumPoints < 3) + { + // ConvexHull2 should be called with at least three points. + return false; + } + + IntrinsicsVector2 info(mNumPoints, mPoints, mEpsilon); + if (info.dimension == 0) + { + // mDimension is 0 + return false; + } + + if (info.dimension == 1) + { + // The set is (nearly) collinear. + mDimension = 1; + mLine = Line2(info.origin, info.direction[0]); + return false; + } + + mDimension = 2; + + // Compute the points for the queries. + mComputePoints.resize(mNumPoints); + mQuery.Set(mNumPoints, &mComputePoints[0]); + for (i = 0; i < mNumPoints; ++i) + { + for (j = 0; j < 2; ++j) + { + mComputePoints[i][j] = points[i][j]; + } + } + + // Sort the points. + mHull.resize(mNumPoints); + for (i = 0; i < mNumPoints; ++i) + { + mHull[i] = i; + } + std::sort(mHull.begin(), mHull.end(), + [points](int i0, int i1) + { + if (points[i0][0] < points[i1][0]) { return true; } + if (points[i0][0] > points[i1][0]) { return false; } + return points[i0][1] < points[i1][1]; + } + ); + + // Remove duplicates. + auto newEnd = std::unique(mHull.begin(), mHull.end(), + [points](int i0, int i1) + { + return points[i0] == points[i1]; + } + ); + mHull.erase(newEnd, mHull.end()); + mNumUniquePoints = static_cast(mHull.size()); + + // Use a divide-and-conquer algorithm. The merge step computes the + // convex hull of two convex polygons. + mMerged.resize(mNumUniquePoints); + int i0 = 0, i1 = mNumUniquePoints - 1; + GetHull(i0, i1); + mHull.resize(i1 - i0 + 1); + return true; +} + +template inline +InputType ConvexHull2::GetEpsilon() const +{ + return mEpsilon; +} + +template inline +int ConvexHull2::GetDimension() const +{ + return mDimension; +} + +template inline +Line2 const& ConvexHull2::GetLine() const +{ + return mLine; +} + +template inline +int ConvexHull2::GetNumPoints() const +{ + return mNumPoints; +} + +template inline +int ConvexHull2::GetNumUniquePoints() const +{ + return mNumUniquePoints; +} + +template inline +Vector2 const* ConvexHull2::GetPoints() const +{ + return mPoints; +} + +template inline +PrimalQuery2 const& ConvexHull2::GetQuery() const +{ + return mQuery; +} + +template inline +std::vector const& ConvexHull2::GetHull() const +{ + return mHull; +} + +template inline +void ConvexHull2::GetHull(int& i0, int& i1) +{ + int numVertices = i1 - i0 + 1; + if (numVertices > 1) + { + // Compute the middle index of input range. + int mid = (i0 + i1) / 2; + + // Compute the hull of subsets (mid-i0+1 >= i1-mid). + int j0 = i0, j1 = mid, j2 = mid + 1, j3 = i1; + GetHull(j0, j1); + GetHull(j2, j3); + + // Merge the convex hulls into a single convex hull. + Merge(j0, j1, j2, j3, i0, i1); + } + // else: The convex hull is a single point. +} + +template inline +void ConvexHull2::Merge(int j0, int j1, int j2, int j3, + int& i0, int& i1) +{ + // Subhull0 is to the left of subhull1 because of the initial sorting of + // the points by x-components. We need to find two mutually visible + // points, one on the left subhull and one on the right subhull. + int size0 = j1 - j0 + 1; + int size1 = j3 - j2 + 1; + + int i; + Vector2 p; + + // Find the right-most point of the left subhull. + Vector2 pmax0 = mComputePoints[mHull[j0]]; + int imax0 = j0; + for (i = j0 + 1; i <= j1; ++i) + { + p = mComputePoints[mHull[i]]; + if (pmax0 < p) + { + pmax0 = p; + imax0 = i; + } + } + + // Find the left-most point of the right subhull. + Vector2 pmin1 = mComputePoints[mHull[j2]]; + int imin1 = j2; + for (i = j2 + 1; i <= j3; ++i) + { + p = mComputePoints[mHull[i]]; + if (p < pmin1) + { + pmin1 = p; + imin1 = i; + } + } + + // Get the lower tangent to hulls (LL = lower-left, LR = lower-right). + int iLL = imax0, iLR = imin1; + GetTangent(j0, j1, j2, j3, iLL, iLR); + + // Get the upper tangent to hulls (UL = upper-left, UR = upper-right). + int iUL = imax0, iUR = imin1; + GetTangent(j2, j3, j0, j1, iUR, iUL); + + // Construct the counterclockwise-ordered merged-hull vertices. + int k; + int numMerged = 0; + + i = iUL; + for (k = 0; k < size0; ++k) + { + mMerged[numMerged++] = mHull[i]; + if (i == iLL) + { + break; + } + i = (i < j1 ? i + 1 : j0); + } + LogAssert(k < size0, "Unexpected condition."); + + i = iLR; + for (k = 0; k < size1; ++k) + { + mMerged[numMerged++] = mHull[i]; + if (i == iUR) + { + break; + } + i = (i < j3 ? i + 1 : j2); + } + LogAssert(k < size1, "Unexpected condition."); + + int next = j0; + for (k = 0; k < numMerged; ++k) + { + mHull[next] = mMerged[k]; + ++next; + } + + i0 = j0; + i1 = next - 1; +} + +template inline +void ConvexHull2::GetTangent(int j0, int j1, int j2, int j3, + int& i0, int& i1) +{ + // In theory the loop terminates in a finite number of steps, but the + // upper bound for the loop variable is used to trap problems caused by + // floating point roundoff errors that might lead to an infinite loop. + + int size0 = j1 - j0 + 1; + int size1 = j3 - j2 + 1; + int const imax = size0 + size1; + int i, iLm1, iRp1; + Vector2 L0, L1, R0, R1; + + for (i = 0; i < imax; i++) + { + // Get the endpoints of the potential tangent. + L1 = mComputePoints[mHull[i0]]; + R0 = mComputePoints[mHull[i1]]; + + // Walk along the left hull to find the point of tangency. + if (size0 > 1) + { + iLm1 = (i0 > j0 ? i0 - 1 : j1); + L0 = mComputePoints[mHull[iLm1]]; + auto order = mQuery.ToLineExtended(R0, L0, L1); + if (order == PrimalQuery2::ORDER_NEGATIVE + || order == PrimalQuery2::ORDER_COLLINEAR_RIGHT) + { + i0 = iLm1; + continue; + } + } + + // Walk along right hull to find the point of tangency. + if (size1 > 1) + { + iRp1 = (i1 < j3 ? i1 + 1 : j2); + R1 = mComputePoints[mHull[iRp1]]; + auto order = mQuery.ToLineExtended(L1, R0, R1); + if (order == PrimalQuery2::ORDER_NEGATIVE + || order == PrimalQuery2::ORDER_COLLINEAR_LEFT) + { + i1 = iRp1; + continue; + } + } + + // The tangent segment has been found. + break; + } + + // Detect an "infinite loop" caused by floating point round-off errors. + LogAssert(i < imax, "Unexpected condition."); +} + + +} + + + +// ---------------------------------------------- +// GteMinimumAreaBox2.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.2 (2018/10/05) + +// Compute a minimum-area oriented box containing the specified points. The +// algorithm uses the rotating calipers method. +// http://www-cgrl.cs.mcgill.ca/~godfried/research/calipers.html +// http://cgm.cs.mcgill.ca/~orm/rotcal.html +// The box is supported by the convex hull of the points, so the algorithm +// is really about computing the minimum-area box containing a convex polygon. +// The rotating calipers approach is O(n) in time for n polygon edges. +// +// A detailed description of the algorithm and implementation is found in +// http://www.geometrictools.com/Documentation/MinimumAreaRectangle.pdf +// +// NOTE: This algorithm guarantees a correct output only when ComputeType is +// an exact arithmetic type that supports division. In GTEngine, one such +// type is BSRational (arbitrary precision). Another such type +// is BSRational> (fixed precision), where N is chosen large +// enough for your input data sets. If you choose ComputeType to be 'float' +// or 'double', the output is not guaranteed to be correct. +// +// See GeometricTools/GTEngine/Samples/Geometrics/MinimumAreaBox2 for an +// example of how to use the code. + +namespace houdini::gte +{ + +template +class MinimumAreaBox2 +{ +public: + // The class is a functor to support computing the minimum-area box of + // multiple data sets using the same class object. + MinimumAreaBox2(); + + // The points are arbitrary, so we must compute the convex hull from + // them in order to compute the minimum-area box. The input parameters + // are necessary for using ConvexHull2. NOTE: ConvexHull2 guarantees + // that the hull does not have three consecutive collinear points. + OrientedBox2 operator()(int numPoints, + Vector2 const* points, + bool useRotatingCalipers = + !std::is_floating_point::value); + + // The points already form a counterclockwise, nondegenerate convex + // polygon. If the points directly are the convex polygon, set + // numIndices to 0 and indices to nullptr. If the polygon vertices + // are a subset of the incoming points, that subset is identified by + // numIndices >= 3 and indices having numIndices elements. + OrientedBox2 operator()(int numPoints, + Vector2 const* points, int numIndices, int const* indices, + bool useRotatingCalipers = + !std::is_floating_point::value); + + // Member access. + inline int GetNumPoints() const; + inline Vector2 const* GetPoints() const; + inline std::vector const& GetHull() const; + inline std::array const& GetSupportIndices() const; + inline InputType GetArea() const; + +private: + // The box axes are U[i] and are usually not unit-length in order to allow + // exact arithmetic. The box is supported by mPoints[index[i]], where i + // is one of the enumerations above. The box axes are not necessarily unit + // length, but they have the same length. They need to be normalized for + // conversion back to InputType. + struct Box + { + Vector2 U[2]; + std::array index; // order: bottom, right, top, left + ComputeType sqrLenU0, area; + }; + + // The rotating calipers algorithm has a loop invariant that requires + // the convex polygon not to have collinear points. Any such points + // must be removed first. The code is also executed for the O(n^2) + // algorithm to reduce the number of process edges. + void RemoveCollinearPoints(std::vector>& vertices); + + // This is the slow O(n^2) search. + Box ComputeBoxForEdgeOrderNSqr( + std::vector> const& vertices); + + // The fast O(n) search. + Box ComputeBoxForEdgeOrderN( + std::vector> const& vertices); + + // Compute the smallest box for the polygon edge . + Box SmallestBox(int i0, int i1, + std::vector> const& vertices); + + // Compute (sin(angle))^2 for the polygon edges emanating from the support + // vertices of the box. The return value is 'true' if at least one angle + // is in [0,pi/2); otherwise, the return value is 'false' and the original + // polygon must be a rectangle. + bool ComputeAngles(std::vector> const& vertices, + Box const& box, std::array, 4>& A, + int& numA) const; + + // Sort the angles indirectly. The sorted indices are returned. This + // avoids swapping elements of A[], which can be expensive when + // ComputeType is an exact rational type. + std::array SortAngles( + std::array, 4> const& A, int numA) const; + + bool UpdateSupport(std::array, 4> const& A, + int numA, std::array const& sort, + std::vector> const& vertices, + std::vector& visited, Box& box); + + // Convert the ComputeType box to the InputType box. When the ComputeType + // is an exact rational type, the conversions are performed to avoid + // precision loss until necessary at the last step. + void ConvertTo(Box const& minBox, + std::vector> const& computePoints, + OrientedBox2& itMinBox); + + // The input points to be bound. + int mNumPoints; + Vector2 const* mPoints; + + // The indices into mPoints/mComputePoints for the convex hull vertices. + std::vector mHull; + + // The support indices for the minimum-area box. + std::array mSupportIndices; + + // The area of the minimum-area box. The ComputeType value is exact, + // so the only rounding errors occur in the conversion from ComputeType + // to InputType (default rounding mode is round-to-nearest-ties-to-even). + InputType mArea; + + // Convenient values that occur regularly in the code. When using + // rational ComputeType, we construct these numbers only once. + ComputeType mZero, mOne, mNegOne, mHalf; +}; + + +template +MinimumAreaBox2::MinimumAreaBox2() + : + mNumPoints(0), + mPoints(nullptr), + mArea((InputType)0), + mZero(0), + mOne(1), + mNegOne(-1), + mHalf((InputType)0.5) +{ + mSupportIndices = { { 0, 0, 0, 0 } }; +} + +template +OrientedBox2 MinimumAreaBox2::operator()( + int numPoints, Vector2 const* points, bool useRotatingCalipers) +{ + mNumPoints = numPoints; + mPoints = points; + mHull.clear(); + + // Get the convex hull of the points. + ConvexHull2 ch2; + ch2(mNumPoints, mPoints, (InputType)0); + int dimension = ch2.GetDimension(); + + OrientedBox2 minBox; + + if (dimension == 0) + { + // The points are all effectively the same (using fuzzy epsilon). + minBox.center = mPoints[0]; + minBox.axis[0] = Vector2::Unit(0); + minBox.axis[1] = Vector2::Unit(1); + minBox.extent[0] = (InputType)0; + minBox.extent[1] = (InputType)0; + mHull.resize(1); + mHull[0] = 0; + return minBox; + } + + if (dimension == 1) + { + // The points effectively lie on a line (using fuzzy epsilon). + // Determine the extreme t-values for the points represented as + // P = origin + t*direction. We know that 'origin' is an input + // vertex, so we can start both t-extremes at zero. + Line2 const& line = ch2.GetLine(); + InputType tmin = (InputType)0, tmax = (InputType)0; + int imin = 0, imax = 0; + for (int i = 0; i < mNumPoints; ++i) + { + Vector2 diff = mPoints[i] - line.origin; + InputType t = Dot(diff, line.direction); + if (t > tmax) + { + tmax = t; + imax = i; + } + else if (t < tmin) + { + tmin = t; + imin = i; + } + } + + minBox.center = line.origin + + ((InputType)0.5)*(tmin + tmax) * line.direction; + minBox.extent[0] = ((InputType)0.5)*(tmax - tmin); + minBox.extent[1] = (InputType)0; + minBox.axis[0] = line.direction; + minBox.axis[1] = -Perp(line.direction); + mHull.resize(2); + mHull[0] = imin; + mHull[1] = imax; + return minBox; + } + + mHull = ch2.GetHull(); + Vector2 const* queryPoints = ch2.GetQuery().GetVertices(); + std::vector> computePoints(mHull.size()); + for (size_t i = 0; i < mHull.size(); ++i) + { + computePoints[i] = queryPoints[mHull[i]]; + } + + RemoveCollinearPoints(computePoints); + + Box box; + if (useRotatingCalipers) + { + box = ComputeBoxForEdgeOrderN(computePoints); + } + else + { + box = ComputeBoxForEdgeOrderNSqr(computePoints); + } + + ConvertTo(box, computePoints, minBox); + return minBox; +} + +template +OrientedBox2 MinimumAreaBox2::operator()( + int numPoints, Vector2 const* points, int numIndices, + int const* indices, bool useRotatingCalipers) +{ + mHull.clear(); + + OrientedBox2 minBox; + + if (numPoints < 3 || !points || (indices && numIndices < 3)) + { + minBox.center = Vector2::Zero(); + minBox.axis[0] = Vector2::Unit(0); + minBox.axis[1] = Vector2::Unit(1); + minBox.extent = Vector2::Zero(); + return minBox; + } + + if (indices) + { + mHull.resize(numIndices); + std::copy(indices, indices + numIndices, mHull.begin()); + } + else + { + numIndices = numPoints; + mHull.resize(numIndices); + for (int i = 0; i < numIndices; ++i) + { + mHull[i] = i; + } + } + + std::vector> computePoints(numIndices); + for (int i = 0; i < numIndices; ++i) + { + int h = mHull[i]; + computePoints[i][0] = (ComputeType)points[h][0]; + computePoints[i][1] = (ComputeType)points[h][1]; + } + + RemoveCollinearPoints(computePoints); + + Box box; + if (useRotatingCalipers) + { + box = ComputeBoxForEdgeOrderN(computePoints); + } + else + { + box = ComputeBoxForEdgeOrderNSqr(computePoints); + } + + ConvertTo(box, computePoints, minBox); + return minBox; +} + +template inline +int MinimumAreaBox2::GetNumPoints() const +{ + return mNumPoints; +} + +template inline +Vector2 const* MinimumAreaBox2::GetPoints() + const +{ + return mPoints; +} + +template inline +std::vector const& +MinimumAreaBox2::GetHull() const +{ + return mHull; +} + +template inline +std::array const& +MinimumAreaBox2::GetSupportIndices() const +{ + return mSupportIndices; +} + +template inline +InputType MinimumAreaBox2::GetArea() const +{ + return mArea; +} + +template +void MinimumAreaBox2::RemoveCollinearPoints( + std::vector>& vertices) +{ + std::vector> tmpVertices = vertices; + + int const numVertices = static_cast(vertices.size()); + int numNoncollinear = 0; + Vector2 ePrev = tmpVertices[0] - tmpVertices.back(); + for (int i0 = 0, i1 = 1; i0 < numVertices; ++i0) + { + Vector2 eNext = tmpVertices[i1] - tmpVertices[i0]; + + ComputeType dp = DotPerp(ePrev, eNext); + if (dp != mZero) + { + vertices[numNoncollinear++] = tmpVertices[i0]; + } + + ePrev = eNext; + if (++i1 == numVertices) + { + i1 = 0; + } + } + + vertices.resize(numNoncollinear); +} + +template +typename MinimumAreaBox2::Box +MinimumAreaBox2::ComputeBoxForEdgeOrderNSqr( + std::vector> const& vertices) +{ + Box minBox; + minBox.area = mNegOne; + int const numIndices = static_cast(vertices.size()); + for (int i0 = numIndices - 1, i1 = 0; i1 < numIndices; i0 = i1++) + { + Box box = SmallestBox(i0, i1, vertices); + if (minBox.area == mNegOne || box.area < minBox.area) + { + minBox = box; + } + } + return minBox; +} + +template +typename MinimumAreaBox2::Box +MinimumAreaBox2::ComputeBoxForEdgeOrderN( + std::vector> const& vertices) +{ + // The inputs are assumed to be the vertices of a convex polygon that + // is counterclockwise ordered. The input points must not contain three + // consecutive collinear points. + + // When the bounding box corresponding to a polygon edge is computed, + // we mark the edge as visited. If the edge is encountered later, the + // algorithm terminates. + std::vector visited(vertices.size()); + std::fill(visited.begin(), visited.end(), false); + + // Start the minimum-area rectangle search with the edge from the last + // polygon vertex to the first. When updating the extremes, we want the + // bottom-most point on the left edge, the top-most point on the right + // edge, the left-most point on the top edge, and the right-most point + // on the bottom edge. The polygon edges starting at these points are + // then guaranteed not to coincide with a box edge except when an extreme + // point is shared by two box edges (at a corner). + Box minBox = SmallestBox((int)vertices.size() - 1, 0, vertices); + visited[minBox.index[0]] = true; + + // Execute the rotating calipers algorithm. + Box box = minBox; + for (size_t i = 0; i < vertices.size(); ++i) + { + std::array, 4> A; + int numA; + if (!ComputeAngles(vertices, box, A, numA)) + { + // The polygon is a rectangle, so the search is over. + break; + } + + // Indirectly sort the A-array. + std::array sort = SortAngles(A, numA); + + // Update the supporting indices (box.index[]) and the box axis + // directions (box.U[]). + if (!UpdateSupport(A, numA, sort, vertices, visited, box)) + { + // We have already processed the box polygon edge, so the search + // is over. + break; + } + + if (box.area < minBox.area) + { + minBox = box; + } + } + + return minBox; +} + +template +typename MinimumAreaBox2::Box +MinimumAreaBox2::SmallestBox(int i0, int i1, + std::vector> const& vertices) +{ + Box box; + box.U[0] = vertices[i1] - vertices[i0]; + box.U[1] = -Perp(box.U[0]); + box.index = { { i1, i1, i1, i1 } }; + box.sqrLenU0 = Dot(box.U[0], box.U[0]); + + Vector2 const& origin = vertices[i1]; + Vector2 support[4]; + for (int j = 0; j < 4; ++j) + { + support[j] = { mZero, mZero }; + } + + int i = 0; + for (auto const& vertex : vertices) + { + Vector2 diff = vertex - origin; + Vector2 v = { Dot(box.U[0], diff), Dot(box.U[1], diff) }; + + // The right-most vertex of the bottom edge is vertices[i1]. The + // assumption of no triple of collinear vertices guarantees that + // box.index[0] is i1, which is the initial value assigned at the + // beginning of this function. Therefore, there is no need to test + // for other vertices farther to the right than vertices[i1]. + + if (v[0] > support[1][0] || + (v[0] == support[1][0] && v[1] > support[1][1])) + { + // New right maximum OR same right maximum but closer to top. + box.index[1] = i; + support[1] = v; + } + + if (v[1] > support[2][1] || + (v[1] == support[2][1] && v[0] < support[2][0])) + { + // New top maximum OR same top maximum but closer to left. + box.index[2] = i; + support[2] = v; + } + + if (v[0] < support[3][0] || + (v[0] == support[3][0] && v[1] < support[3][1])) + { + // New left minimum OR same left minimum but closer to bottom. + box.index[3] = i; + support[3] = v; + } + + ++i; + } + + // The comment in the loop has the implication that support[0] = { 0, 0 }, + // so the scaled height (support[2][1] - support[0][1]) is simply + // support[2][1]. + ComputeType scaledWidth = support[1][0] - support[3][0]; + ComputeType scaledHeight = support[2][1]; + box.area = scaledWidth * scaledHeight / box.sqrLenU0; + return box; +} + +template +bool MinimumAreaBox2::ComputeAngles( + std::vector> const& vertices, Box const& box, + std::array, 4>& A, int& numA) const +{ + int const numVertices = static_cast(vertices.size()); + numA = 0; + for (int k1 = 0; k1 < 4; k1++) + { + int k0 = (k1 == 0) ? 3 : (k1 - 1); + + if (box.index[k0] != box.index[k1]) + { + if (numA < 4) // Ensure index is within array bounds [0, 3] + { + // The box edges are ordered in k1 as U[0], U[1], -U[0], -U[1]. + Vector2 D = + ((k0 & 2) ? -box.U[k0 & 1] : box.U[k0 & 1]); + int j0 = box.index[k0], j1 = j0 + 1; + if (j1 == numVertices) + { + j1 = 0; + } + Vector2 E = vertices[j1] - vertices[j0]; + ComputeType dp = DotPerp(D, E); + ComputeType esqrlen = Dot(E, E); + ComputeType sinThetaSqr = (dp * dp) / esqrlen; + A[numA++] = std::make_pair(sinThetaSqr, k0); + + } + } + } + return numA > 0; +} + +template +std::array MinimumAreaBox2::SortAngles( + std::array, 4> const& A, int numA) const +{ + std::array sort = {{ 0, 1, 2, 3 }}; + if (numA > 1) + { + if (numA == 2) + { + if (A[sort[0]].first > A[sort[1]].first) + { + std::swap(sort[0], sort[1]); + } + } + else if (numA == 3) + { + if (A[sort[0]].first > A[sort[1]].first) + { + std::swap(sort[0], sort[1]); + } + if (A[sort[0]].first > A[sort[2]].first) + { + std::swap(sort[0], sort[2]); + } + if (A[sort[1]].first > A[sort[2]].first) + { + std::swap(sort[1], sort[2]); + } + } + else // numA == 4 + { + if (A[sort[0]].first > A[sort[1]].first) + { + std::swap(sort[0], sort[1]); + } + if (A[sort[2]].first > A[sort[3]].first) + { + std::swap(sort[2], sort[3]); + } + if (A[sort[0]].first > A[sort[2]].first) + { + std::swap(sort[0], sort[2]); + } + if (A[sort[1]].first > A[sort[3]].first) + { + std::swap(sort[1], sort[3]); + } + if (A[sort[1]].first > A[sort[2]].first) + { + std::swap(sort[1], sort[2]); + } + } + } + return sort; +} + +template +bool MinimumAreaBox2::UpdateSupport( + std::array, 4> const& A, int numA, + std::array const& sort, + std::vector> const& vertices, + std::vector& visited, Box& box) +{ + // Replace the support vertices of those edges attaining minimum angle + // with the other endpoints of the edges. + int const numVertices = static_cast(vertices.size()); + auto const& amin = A[sort[0]]; + for (int k = 0; k < numA; ++k) + { + auto const& a = A[sort[k]]; + if (a.first == amin.first) + { + if (++box.index[a.second] == numVertices) + { + box.index[a.second] = 0; + } + } + else + { + break; + } + } + + int bottom = box.index[amin.second]; + if (visited[bottom]) + { + // We have already processed this polygon edge. + return false; + } + visited[bottom] = true; + + // Cycle the vertices so that the bottom support occurs first. + std::array nextIndex; + for (int k = 0; k < 4; ++k) + { + nextIndex[k] = box.index[(amin.second + k) % 4]; + } + box.index = nextIndex; + + // Compute the box axis directions. + int j1 = box.index[0], j0 = j1 - 1; + if (j0 < 0) + { + j0 = numVertices - 1; + } + box.U[0] = vertices[j1] - vertices[j0]; + box.U[1] = -Perp(box.U[0]); + box.sqrLenU0 = Dot(box.U[0], box.U[0]); + + // Compute the box area. + Vector2 diff[2] = + { + vertices[box.index[1]] - vertices[box.index[3]], + vertices[box.index[2]] - vertices[box.index[0]] + }; + box.area = Dot(box.U[0], diff[0]) * Dot(box.U[1], diff[1]) / box.sqrLenU0; + return true; +} + +template +void MinimumAreaBox2::ConvertTo(Box const& minBox, + std::vector> const& computePoints, + OrientedBox2& itMinBox) +{ + // The sum, difference, and center are all computed exactly. + Vector2 sum[2] = + { + computePoints[minBox.index[1]] + computePoints[minBox.index[3]], + computePoints[minBox.index[2]] + computePoints[minBox.index[0]] + }; + + Vector2 difference[2] = + { + computePoints[minBox.index[1]] - computePoints[minBox.index[3]], + computePoints[minBox.index[2]] - computePoints[minBox.index[0]] + }; + + Vector2 center = mHalf * ( + Dot(minBox.U[0], sum[0]) * minBox.U[0] + + Dot(minBox.U[1], sum[1]) * minBox.U[1]) / minBox.sqrLenU0; + + // Calculate the squared extent using ComputeType to avoid loss of + // precision before computing a squared root. + Vector2 sqrExtent; + for (int i = 0; i < 2; ++i) + { + sqrExtent[i] = mHalf * Dot(minBox.U[i], difference[i]); + sqrExtent[i] *= sqrExtent[i]; + sqrExtent[i] /= minBox.sqrLenU0; + } + + for (int i = 0; i < 2; ++i) + { + itMinBox.center[i] = (InputType)center[i]; + itMinBox.extent[i] = std::sqrt((InputType)sqrExtent[i]); + + // Before converting to floating-point, factor out the maximum + // component using ComputeType to generate rational numbers in a + // range that avoids loss of precision during the conversion and + // normalization. + Vector2 const& axis = minBox.U[i]; + ComputeType cmax = std::max(std::abs(axis[0]), std::abs(axis[1])); + ComputeType invCMax = mOne / cmax; + for (int j = 0; j < 2; ++j) + { + itMinBox.axis[i][j] = (InputType)(axis[j] * invCMax); + } + Normalize(itMinBox.axis[i]); + } + + mSupportIndices = minBox.index; + mArea = (InputType)minBox.area; +} + + +} + + + +// ---------------------------------------------- +// GteMinimumVolumeBox3.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.1 (2018/10/05) + +// Compute a minimum-volume oriented box containing the specified points. The +// algorithm is really about computing the minimum-volume box containing the +// convex hull of the points, so we must compute the convex hull or you must +// pass an already built hull to the code. +// +// The minimum-volume oriented box has a face coincident with a hull face +// or has three mutually orthogonal edges coincident with three hull edges +// that (of course) are mutually orthogonal. +// J.O'Rourke, "Finding minimal enclosing boxes", +// Internat. J. Comput. Inform. Sci., 14:183-199, 1985. +// +// A detailed description of the algorithm and implementation is found in +// the documents +// http://www.geometrictools.com/Documentation/MinimumVolumeBox.pdf +// http://www.geometrictools.com/Documentation/MinimumAreaRectangle.pdf +// +// NOTE: This algorithm guarantees a correct output only when ComputeType is +// an exact arithmetic type that supports division. In GTEngine, one such +// type is BSRational (arbitrary precision). Another such type +// is BSRational> (fixed precision), where N is chosen large +// enough for your input data sets. If you choose ComputeType to be 'float' +// or 'double', the output is not guaranteed to be correct. +// +// See GeometricTools/GTEngine/Samples/Geometrics/MinimumVolumeBox3 for an +// example of how to use the code. + +namespace houdini::gte +{ + +template +class MinimumVolumeBox3 +{ +public: + // The class is a functor to support computing the minimum-volume box of + // multiple data sets using the same class object. For multithreading + // in ProcessFaces, choose 'numThreads' subject to the constraints + // 1 <= numThreads <= std::thread::hardware_concurrency() + // To execute ProcessEdges in a thread separate from the main thrad, + // choose 'threadProcessEdges' to 'true'. + MinimumVolumeBox3(unsigned int numThreads = 1, bool threadProcessEdges = false); + + // The points are arbitrary, so we must compute the convex hull from + // them in order to compute the minimum-area box. The input parameters + // are necessary for using ConvexHull3. + OrientedBox3 operator()(int numPoints, + Vector3 const* points, + FHEProgressCancel* Progress, + bool useRotatingCalipers = !std::is_floating_point::value); + + // The points form a nondegenerate convex polyhedron. The indices input + // must be nonnull and specify the triangle faces. + OrientedBox3 operator()(int numPoints, + Vector3 const* points, + int numIndices, + int const* indices, + FHEProgressCancel* Progress, + bool useRotatingCalipers = !std::is_floating_point::value); + + // Member access. + inline int GetNumPoints() const; + inline Vector3 const* GetPoints() const; + inline std::vector const& GetHull() const; + inline InputType GetVolume() const; + +private: + struct Box + { + Vector3 P, U[3]; + ComputeType sqrLenU[3], range[3][2], volume; + }; + + struct ExtrudeRectangle + { + Vector3 U[2]; + std::array index; + ComputeType sqrLenU[2], area; + }; + + // Compute the minimum-volume box relative to each hull face. + void ProcessFaces(ETManifoldMesh const& mesh, Box& minBox, FHEProgressCancel* Progress = nullptr); + + // Compute the minimum-volume box for each triple of orthgonal hull edges. + void ProcessEdges(ETManifoldMesh const& mesh, Box& minBox); + + // Compute the minimum-volume box relative to a single hull face. + typedef ETManifoldMesh::Triangle Triangle; + void ProcessFace(std::shared_ptr const& supportTri, + std::vector> const& normal, + std::map, int> const& triNormalMap, + ETManifoldMesh::EMap const& emap, Box& localMinBox); + + // The rotating calipers algorithm has a loop invariant that requires + // the convex polygon not to have collinear points. Any such points + // must be removed first. The code is also executed for the O(n^2) + // algorithm to reduce the number of process edges. + void RemoveCollinearPoints(Vector3 const& N, std::vector& polyline); + + // This is the slow order O(n^2) search. + void ComputeBoxForFaceOrderNSqr(Vector3 const& N, std::vector const& polyline, Box& box); + + // This is the rotating calipers version, which is O(n). + void ComputeBoxForFaceOrderN(Vector3 const& N, std::vector const& polyline, Box& box); + + // Compute the smallest rectangle for the polyline edge . + ExtrudeRectangle SmallestRectangle(int i0, int i1, Vector3 const& N, std::vector const& polyline); + + // Compute (sin(angle))^2 for the polyline edges emanating from the + // support vertices of the rectangle. The return value is 'true' if at + // least one angle is in [0,pi/2); otherwise, the return value is 'false' + // and the original polyline must project to a rectangle. + bool ComputeAngles(Vector3 const& N, + std::vector const& polyline, ExtrudeRectangle const& rct, + std::array, 4>& A, int& numA) const; + + // Sort the angles indirectly. The sorted indices are returned. This + // avoids swapping elements of A[], which can be expensive when + // ComputeType is an exact rational type. + std::array SortAngles(std::array, 4> const& A, int numA) const; + + bool UpdateSupport(std::array, 4> const& A, int numA, + std::array const& sort, Vector3 const& N, std::vector const& polyline, + std::vector& visited, ExtrudeRectangle& rct); + + // Convert the extruded box to the minimum-volume box of InputType. When + // the ComputeType is an exact rational type, the conversions are + // performed to avoid precision loss until necessary at the last step. + void ConvertTo(Box const& minBox, OrientedBox3& itMinBox); + + // The code is multithreaded, both for convex hull computation and + // computing minimum-volume extruded boxes for the hull faces. The + // default value is 1, which implies a single-threaded computation (on + // the main thread). + unsigned int mNumThreads; + bool mThreadProcessEdges; + + // The input points to be bound. + int mNumPoints; + Vector3 const* mPoints; + + // The ComputeType conversions of the input points. Only points of the + // convex hull (vertices of a convex polyhedron) are converted for + // performance when ComputeType is rational. + Vector3 const* mComputePoints; + + // The indices into mPoints/mComputePoints for the convex hull vertices. + std::vector mHull; + + // The unique indices in mHull. This set allows us to compute only for + // the hull vertices and avoids redundant computations if the indices + // were to have repeated indices into mPoints/mComputePoints. This is + // a performance improvement for rational ComputeType. + std::set mUniqueIndices; + + // The caller can specify whether to use rotating calipers or the slower + // all-edge processing for computing an extruded bounding box. + bool mUseRotatingCalipers; + + // The volume of the minimum-volume box. The ComputeType value is exact, + // so the only rounding errors occur in the conversion from ComputeType + // to InputType (default rounding mode is round-to-nearest-ties-to-even). + InputType mVolume; + + // Convenient values that occur regularly in the code. When using + // rational ComputeType, we construct these numbers only once. + ComputeType mZero, mOne, mNegOne, mHalf; +}; + + +template +MinimumVolumeBox3::MinimumVolumeBox3(unsigned int numThreads, bool threadProcessEdges) + : + mNumThreads(numThreads), + mThreadProcessEdges(threadProcessEdges), + mNumPoints(0), + mPoints(nullptr), + mComputePoints(nullptr), + mUseRotatingCalipers(true), + mVolume((InputType)0), + mZero(0), + mOne(1), + mNegOne(-1), + mHalf((InputType)0.5) +{ +} + +template +OrientedBox3 MinimumVolumeBox3::operator()( + int numPoints, Vector3 const* points, FHEProgressCancel* Progress, bool useRotatingCalipers) +{ + mNumPoints = numPoints; + mPoints = points; + mUseRotatingCalipers = useRotatingCalipers; + mHull.clear(); + mUniqueIndices.clear(); + + // Get the convex hull of the points. + ConvexHull3 ch3; + ch3(mNumPoints, mPoints, (InputType)0); + int dimension = ch3.GetDimension(); + + OrientedBox3 itMinBox; + + if (dimension == 0) + { + // The points are all effectively the same (using fuzzy epsilon). + itMinBox.center = mPoints[0]; + itMinBox.axis[0] = Vector3::Unit(0); + itMinBox.axis[1] = Vector3::Unit(1); + itMinBox.axis[2] = Vector3::Unit(2); + itMinBox.extent[0] = (InputType)0; + itMinBox.extent[1] = (InputType)0; + itMinBox.extent[2] = (InputType)0; + mHull.resize(1); + mHull[0] = 0; + return itMinBox; + } + + if (dimension == 1) + { + // The points effectively lie on a line (using fuzzy epsilon). + // Determine the extreme t-values for the points represented as + // P = origin + t*direction. We know that 'origin' is an input + // vertex, so we can start both t-extremes at zero. + Line3 const& line = ch3.GetLine(); + InputType tmin = (InputType)0, tmax = (InputType)0; + int imin = 0, imax = 0; + for (int i = 0; i < mNumPoints; ++i) + { + Vector3 diff = mPoints[i] - line.origin; + InputType t = Dot(diff, line.direction); + if (t > tmax) + { + tmax = t; + imax = i; + } + else if (t < tmin) + { + tmin = t; + imin = i; + } + } + + itMinBox.center = line.origin + ((InputType)0.5)*(tmin + tmax) * line.direction; + itMinBox.extent[0] = ((InputType)0.5)*(tmax - tmin); + itMinBox.extent[1] = (InputType)0; + itMinBox.extent[2] = (InputType)0; + itMinBox.axis[0] = line.direction; + ComputeOrthogonalComplement(1, &itMinBox.axis[0]); + mHull.resize(2); + mHull[0] = imin; + mHull[1] = imax; + return itMinBox; + } + + if (dimension == 2) + { + // The points effectively line on a plane (using fuzzy epsilon). + // Project the points onto the plane and compute the minimum-area + // bounding box of them. + Plane3 const& plane = ch3.GetPlane(); + + // Get a coordinate system relative to the plane of the points. + // Choose the origin to be any of the input points. + Vector3 origin = mPoints[0]; + Vector3 basis[3]; + basis[0] = plane.normal; + ComputeOrthogonalComplement(1, basis); + + // Project the input points onto the plane. + std::vector> projection(mNumPoints); + for (int i = 0; i < mNumPoints; ++i) + { + Vector3 diff = mPoints[i] - origin; + projection[i][0] = Dot(basis[1], diff); + projection[i][1] = Dot(basis[2], diff); + } + + // Compute the minimum area box in 2D. + MinimumAreaBox2 mab2; + OrientedBox2 rectangle = mab2(mNumPoints, &projection[0]); + + // Lift the values into 3D. + itMinBox.center = origin + rectangle.center[0] * basis[1] + rectangle.center[1] * basis[2]; + itMinBox.axis[0] = rectangle.axis[0][0] * basis[1] + rectangle.axis[0][1] * basis[2]; + itMinBox.axis[1] = rectangle.axis[1][0] * basis[1] + rectangle.axis[1][1] * basis[2]; + itMinBox.axis[2] = basis[0]; + itMinBox.extent[0] = rectangle.extent[0]; + itMinBox.extent[1] = rectangle.extent[1]; + itMinBox.extent[2] = (InputType)0; + mHull = mab2.GetHull(); + return itMinBox; + } + + // Get the set of unique indices of the hull. This is used to project + // hull vertices onto lines. + ETManifoldMesh const& mesh = ch3.GetHullMesh(); + mHull.resize(3 * mesh.GetTriangles().size()); + int h = 0; + for (auto const& element : mesh.GetTriangles()) + { + for (int i = 0; i < 3; ++i, ++h) + { + int index = element.first.V[i]; + mHull[h] = index; + mUniqueIndices.insert(index); + } + } + + mComputePoints = ch3.GetQuery().GetVertices(); + + Box minBox, minBoxEdges; + minBox.volume = mNegOne; + minBoxEdges.volume = mNegOne; + + if (mThreadProcessEdges) + { + std::thread doEdges([this, &mesh, &minBoxEdges]() + { + ProcessEdges(mesh, minBoxEdges); + }); + ProcessFaces(mesh, minBox); + doEdges.join(); + } + else + { + ProcessEdges(mesh, minBoxEdges); + ProcessFaces(mesh, minBox); + } + + if (minBoxEdges.volume != mNegOne + && minBoxEdges.volume < minBox.volume) + { + minBox = minBoxEdges; + } + + ConvertTo(minBox, itMinBox); + mComputePoints = nullptr; + return itMinBox; +} + +template +OrientedBox3 MinimumVolumeBox3::operator()( + int numPoints, Vector3 const* points, int numIndices, + int const* indices, FHEProgressCancel* Progress, bool useRotatingCalipers) +{ + mNumPoints = numPoints; + mPoints = points; + mUseRotatingCalipers = useRotatingCalipers; + mUniqueIndices.clear(); + + // Build the mesh from the indices. The box construction uses the edge + // map of the mesh. + ETManifoldMesh mesh; + int numTriangles = numIndices / 3; + for (int t = 0; t < numTriangles; ++t) + { + int v0 = *indices++; + int v1 = *indices++; + int v2 = *indices++; + mesh.Insert(v0, v1, v2); + } + + // Get the set of unique indices of the hull. This is used to project + // hull vertices onto lines. + mHull.resize(3 * mesh.GetTriangles().size()); + int h = 0; + for (auto const& element : mesh.GetTriangles()) + { + for (int i = 0; i < 3; ++i, ++h) + { + int index = element.first.V[i]; + mHull[h] = index; + mUniqueIndices.insert(index); + } + } + + // Create the ComputeType points to be used downstream. + std::vector> computePoints(mNumPoints); + for (auto i : mUniqueIndices) + { + for (int j = 0; j < 3; ++j) + { + computePoints[i][j] = (ComputeType)mPoints[i][j]; + } + } + + OrientedBox3 itMinBox; + mComputePoints = &computePoints[0]; + + Box minBox, minBoxEdges; + minBox.volume = mNegOne; + minBoxEdges.volume = mNegOne; + + if (mThreadProcessEdges) + { + std::thread doEdges([this, &mesh, &minBoxEdges]() + { + ProcessEdges(mesh, minBoxEdges); + }); + ProcessFaces(mesh, minBox); + doEdges.join(); + } + else + { + ProcessEdges(mesh, minBoxEdges); + ProcessFaces(mesh, minBox, Progress); + + if (Progress && Progress->Cancelled()) + { + return itMinBox; + } + } + + if (minBoxEdges.volume != mNegOne && minBoxEdges.volume < minBox.volume) + { + minBox = minBoxEdges; + } + + ConvertTo(minBox, itMinBox); + mComputePoints = nullptr; + return itMinBox; +} + +template inline +int MinimumVolumeBox3::GetNumPoints() const +{ + return mNumPoints; +} + +template inline +Vector3 const* +MinimumVolumeBox3::GetPoints() const +{ + return mPoints; +} + +template inline +std::vector const& MinimumVolumeBox3::GetHull() + const +{ + return mHull; +} + +template inline +InputType MinimumVolumeBox3::GetVolume() const +{ + return mVolume; +} + +template +void MinimumVolumeBox3::ProcessFaces(ETManifoldMesh const& mesh, Box& minBox, FHEProgressCancel* Progress) +{ + // Get the mesh data structures. + auto const& tmap = mesh.GetTriangles(); + auto const& emap = mesh.GetEdges(); + + // Compute inner-pointing face normals for searching boxes supported by + // a face and an extreme vertex. The indirection in triNormalMap, using + // an integer index instead of the normal/sqrlength pair itself, avoids + // expensive copies when using exact arithmetic. + std::vector> normal(tmap.size()); + std::map, int> triNormalMap; + int index = 0; + for (auto const& element : tmap) + { + auto tri = element.second; + Vector3 const& v0 = mComputePoints[tri->V[0]]; + Vector3 const& v1 = mComputePoints[tri->V[1]]; + Vector3 const& v2 = mComputePoints[tri->V[2]]; + Vector3 edge1 = v1 - v0; + Vector3 edge2 = v2 - v0; + normal[index] = Cross(edge2, edge1); // inner-pointing normal + triNormalMap[tri] = index++; + } + + // Process the triangle faces. For each face, compute the polyline of + // edges that supports the bounding box with a face coincident to the + // triangle face. The projection of the polyline onto the plane of the + // triangle face is a convex polygon, so we can use the method of rotating + // calipers to compute its minimum-area box efficiently. + unsigned int numFaces = static_cast(tmap.size()); + if (mNumThreads > 1 && numFaces >= mNumThreads) + { + // Repackage the triangle pointers to support the partitioning of + // faces for multithreaded face processing. + std::vector> triangles; + triangles.reserve(numFaces); + for (auto const& element : tmap) + { + triangles.push_back(element.second); + } + + // Partition the data for multiple threads. + unsigned int numFacesPerThread = numFaces / mNumThreads; + std::vector imin(mNumThreads), imax(mNumThreads); + std::vector localMinBox(mNumThreads); + for (unsigned int t = 0; t < mNumThreads; ++t) + { + imin[t] = t * numFacesPerThread; + imax[t] = imin[t] + numFacesPerThread - 1; + localMinBox[t].volume = mNegOne; + } + imax[mNumThreads - 1] = numFaces - 1; + + // Execute the face processing in multiple threads. + std::vector process(mNumThreads); + for (unsigned int t = 0; t < mNumThreads; ++t) + { + process[t] = std::thread([this, t, &imin, &imax, &triangles, + &normal, &triNormalMap, &emap, &localMinBox]() + { + for (unsigned int i = imin[t]; i <= imax[t]; ++i) + { + auto const& supportTri = triangles[i]; + ProcessFace(supportTri, normal, triNormalMap, emap, localMinBox[t]); + } + }); + } + + // Wait for all threads to finish. + for (unsigned int t = 0; t < mNumThreads; ++t) + { + process[t].join(); + + // Update the minimum-volume box candidate. + if (minBox.volume == mNegOne || localMinBox[t].volume < minBox.volume) + { + minBox = localMinBox[t]; + } + } + } + else + { + for (auto const& element : tmap) + { + auto const& supportTri = element.second; + ProcessFace(supportTri, normal, triNormalMap, emap, minBox); + + if (Progress && Progress->Cancelled()) + { + return; + } + } + } +} + +template +void MinimumVolumeBox3::ProcessEdges(ETManifoldMesh const& mesh, Box& minBox) +{ + // The minimum-volume box can also be supported by three mutually + // orthogonal edges of the convex hull. For each triple of orthogonal + // edges, compute the minimum-volume box for that coordinate frame by + // projecting the points onto the axes of the frame. Use a hull vertex + // as the origin. + int index = mesh.GetTriangles().begin()->first.V[0]; + Vector3 const& origin = mComputePoints[index]; + Vector3 U[3]; + std::array sqrLenU; + + auto const& emap = mesh.GetEdges(); + auto e2 = emap.begin(), end = emap.end(); + for (/**/; e2 != end; ++e2) + { + U[2] = mComputePoints[e2->first.V[1]] - mComputePoints[e2->first.V[0]]; + auto e1 = e2; + for (++e1; e1 != end; ++e1) + { + U[1] = mComputePoints[e1->first.V[1]] - mComputePoints[e1->first.V[0]]; + if (Dot(U[1], U[2]) != mZero) + { + continue; + } + sqrLenU[1] = Dot(U[1], U[1]); + + auto e0 = e1; + for (++e0; e0 != end; ++e0) + { + U[0] = mComputePoints[e0->first.V[1]] - mComputePoints[e0->first.V[0]]; + sqrLenU[0] = Dot(U[0], U[0]); + if (Dot(U[0], U[1]) != mZero || Dot(U[0], U[2]) != mZero) + { + continue; + } + + // The three edges are mutually orthogonal. To support exact + // rational arithmetic for volume computation, we replace U[2] + // by a parallel vector. + U[2] = Cross(U[0], U[1]); + sqrLenU[2] = sqrLenU[0] * sqrLenU[1]; + + // Project the vertices onto the lines containing the edges. + // Use vertex 0 as the origin. + std::array umin, umax; + for (int j = 0; j < 3; ++j) + { + umin[j] = mZero; + umax[j] = mZero; + } + + for (auto i : mUniqueIndices) + { + Vector3 diff = mComputePoints[i] - origin; + for (int j = 0; j < 3; ++j) + { + ComputeType dot = Dot(diff, U[j]); + if (dot < umin[j]) + { + umin[j] = dot; + } + else if (dot > umax[j]) + { + umax[j] = dot; + } + } + } + + ComputeType volume = (umax[0] - umin[0]) / sqrLenU[0]; + volume *= (umax[1] - umin[1]) / sqrLenU[1]; + volume *= (umax[2] - umin[2]); + + // Update current minimum-volume box (if necessary). + if (minBox.volume == mOne || volume < minBox.volume) + { + // The edge keys have unordered vertices, so it is + // possible that {U[0],U[1],U[2]} is a left-handed set. + // We need a right-handed set. + if (DotCross(U[0], U[1], U[2]) < mZero) + { + U[2] = -U[2]; + } + + minBox.P = origin; + for (int j = 0; j < 3; ++j) + { + minBox.U[j] = U[j]; + minBox.sqrLenU[j] = sqrLenU[j]; + for (int k = 0; k < 3; ++k) + { + minBox.range[k][0] = umin[k]; + minBox.range[k][1] = umax[k]; + } + } + minBox.volume = volume; + + } + } + } + } +} + +template +void MinimumVolumeBox3::ProcessFace(std::shared_ptr const& supportTri, + std::vector> const& normal, std::map, int> const& triNormalMap, + ETManifoldMesh::EMap const& emap, Box& localMinBox) +{ + // Get the supporting triangle information. + auto foundSupport = triNormalMap.find(supportTri); + if (foundSupport == triNormalMap.end()) return; + Vector3 const& supportNormal = normal[foundSupport->second]; + + // Build the polyline of supporting edges. The pair (v,polyline[v]) + // represents an edge directed appropriately (see next set of + // comments). + std::vector polyline(mNumPoints); + int polylineStart = -1; + for (auto const& edgeElement : emap) + { + auto const& edge = *edgeElement.second; + auto const& tri0 = edge.T[0].lock(); + auto const& tri1 = edge.T[1].lock(); + auto found0 = triNormalMap.find(tri0); + if (found0 == triNormalMap.end()) return; + auto found1 = triNormalMap.find(tri1); + if (found1 == triNormalMap.end()) return; + auto const& normal0 = normal[found0->second]; + auto const& normal1 = normal[found1->second]; + ComputeType dot0 = Dot(supportNormal, normal0); + ComputeType dot1 = Dot(supportNormal, normal1); + + std::shared_ptr tri; + if (dot0 < mZero && dot1 >= mZero) + { + tri = tri0; + } + else if (dot1 < mZero && dot0 >= mZero) + { + tri = tri1; + } + + if (tri) + { + // The edge supports the bounding box. Insert the edge in the + // list using clockwise order relative to tri. This will lead + // to a polyline whose projection onto the plane of the hull + // face is a convex polygon that is counterclockwise oriented. + for (int j0 = 2, j1 = 0; j1 < 3; j0 = j1++) + { + if (tri->V[j1] == edge.V[0]) + { + if (tri->V[j0] == edge.V[1]) + { + polyline[edge.V[1]] = edge.V[0]; + } + else + { + polyline[edge.V[0]] = edge.V[1]; + } + polylineStart = edge.V[0]; + break; + } + } + } + } + + if (polylineStart < 0) // abort if start search failed + { + return; + } + + // Rearrange the edges to form a closed polyline. For M vertices, each + // ComputeBoxFor*() function starts with the edge from closedPolyline[M-1] + // to closedPolyline[0]. + std::vector closedPolyline(mNumPoints); + int numClosedPolyline = 0; + int v = polylineStart; + for (auto& cp : closedPolyline) + { + cp = v; + ++numClosedPolyline; + v = polyline[v]; + if (v == polylineStart) + { + break; + } + } + closedPolyline.resize(numClosedPolyline); + + // This avoids redundant face testing in the O(n^2) or O(n) algorithms + // and it simplifies the O(n) implementation. + RemoveCollinearPoints(supportNormal, closedPolyline); + + if (closedPolyline.size() < 2) // abort if we no longer have a polygon + { + return; + } + + // Compute the box coincident to the hull triangle that has minimum + // area on the face coincident with the triangle. + Box faceBox; + if (mUseRotatingCalipers) + { + ComputeBoxForFaceOrderN(supportNormal, closedPolyline, faceBox); + } + else + { + ComputeBoxForFaceOrderNSqr(supportNormal, closedPolyline, faceBox); + } + + // Update the minimum-volume box candidate. + if (localMinBox.volume == mNegOne || faceBox.volume < localMinBox.volume) + { + localMinBox = faceBox; + } +} + +template +void MinimumVolumeBox3::RemoveCollinearPoints( + Vector3 const& N, std::vector& polyline) +{ + std::vector tmpPolyline = polyline; + + int const numPolyline = static_cast(polyline.size()); + int numNoncollinear = 0; + Vector3 ePrev = + mComputePoints[tmpPolyline[0]] - mComputePoints[tmpPolyline.back()]; + + for (int i0 = 0, i1 = 1; i0 < numPolyline; ++i0) + { + Vector3 eNext = + mComputePoints[tmpPolyline[i1]] - mComputePoints[tmpPolyline[i0]]; + + ComputeType tsp = DotCross(ePrev, eNext, N); + if (tsp != mZero) + { + polyline[numNoncollinear++] = tmpPolyline[i0]; + } + + ePrev = eNext; + if (++i1 == numPolyline) + { + i1 = 0; + } + } + + polyline.resize(numNoncollinear); +} + +template +void MinimumVolumeBox3::ComputeBoxForFaceOrderNSqr( + Vector3 const& N, std::vector const& polyline, + Box& box) +{ + // This code processes the polyline terminator associated with a convex + // hull face of inner-pointing normal N. The polyline is usually not + // contained by a plane, and projecting the polyline to a convex polygon + // in a plane perpendicular to N introduces floating-point rounding + // errors. The minimum-area box for the projected polyline is computed + // indirectly to support exact rational arithmetic. + + box.P = mComputePoints[polyline[0]]; + box.U[2] = N; + box.sqrLenU[2] = Dot(N, N); + box.range[1][0] = mZero; + box.volume = mNegOne; + int const numPolyline = static_cast(polyline.size()); + for (int i0 = numPolyline - 1, i1 = 0; i1 < numPolyline; i0 = i1++) + { + // Create a coordinate system for the plane perpendicular to the face + // normal and containing a polyline vertex. + Vector3 const& P = mComputePoints[polyline[i0]]; + Vector3 E = + mComputePoints[polyline[i1]] - mComputePoints[polyline[i0]]; + + Vector3 U1 = Cross(N, E); + Vector3 U0 = Cross(U1, N); + + // Compute the smallest rectangle containing the projected polyline. + ComputeType min0 = mZero, max0 = mZero, max1 = mZero; + for (int j = 0; j < numPolyline; ++j) + { + Vector3 diff = mComputePoints[polyline[j]] - P; + ComputeType dot = Dot(U0, diff); + if (dot < min0) + { + min0 = dot; + } + else if (dot > max0) + { + max0 = dot; + } + + dot = Dot(U1, diff); + if (dot > max1) + { + max1 = dot; + } + } + + // The true area is Area(rectangle)*Length(N). After the smallest + // scaled-area rectangle is computed and returned, the box.volume is + // updated to be the actual squared volume of the box. + ComputeType sqrLenU1 = Dot(U1, U1); + ComputeType volume = (max0 - min0) * max1 / sqrLenU1; + if (box.volume == mNegOne || volume < box.volume) + { + box.P = P; + box.U[0] = U0; + box.U[1] = U1; + box.sqrLenU[0] = sqrLenU1 * box.sqrLenU[2]; + box.sqrLenU[1] = sqrLenU1; + box.range[0][0] = min0; + box.range[0][1] = max0; + box.range[1][1] = max1; + box.volume = volume; + } + } + + // Compute the range of points in the support-normal direction. + box.range[2][0] = mZero; + box.range[2][1] = mZero; + for (auto i : mUniqueIndices) + { + Vector3 diff = mComputePoints[i] - box.P; + ComputeType height = Dot(box.U[2], diff); + if (height < box.range[2][0]) + { + box.range[2][0] = height; + } + else if (height > box.range[2][1]) + { + box.range[2][1] = height; + } + } + + // Compute the actual volume. + box.volume *= (box.range[2][1] - box.range[2][0]) / box.sqrLenU[2]; +} + +template +void MinimumVolumeBox3::ComputeBoxForFaceOrderN( + Vector3 const& N, std::vector const& polyline, Box& box) +{ + // This code processes the polyline terminator associated with a convex + // hull face of inner-pointing normal N. The polyline is usually not + // contained by a plane, and projecting the polyline to a convex polygon + // in a plane perpendicular to N introduces floating-point rounding + // errors. The minimum-area box for the projected polyline is computed + // indirectly to support exact rational arithmetic. + + // When the bounding box corresponding to a polyline edge is computed, + // we mark the edge as visited. If the edge is encountered later, the + // algorithm terminates. + std::vector visited(polyline.size()); + std::fill(visited.begin(), visited.end(), false); + + // Start the minimum-area rectangle search with the edge from the last + // polyline vertex to the first. When updating the extremes, we want the + // bottom-most point on the left edge, the top-most point on the right + // edge, the left-most point on the top edge, and the right-most point + // on the bottom edge. The polygon edges starting at these points are + // then guaranteed not to coincide with a box edge except when an extreme + // point is shared by two box edges (at a corner). + ExtrudeRectangle minRct = SmallestRectangle((int)polyline.size() - 1, 0, + N, polyline); + visited[minRct.index[0]] = true; + + // Execute the rotating calipers algorithm. + ExtrudeRectangle rct = minRct; + for (size_t i = 0; i < polyline.size(); ++i) + { + std::array, 4> A; + int numA; + if (!ComputeAngles(N, polyline, rct, A, numA)) + { + // The polyline projects to a rectangle, so the search is over. + break; + } + + // Indirectly sort the A-array. + std::array sort = SortAngles(A, numA); + + // Update the supporting indices (rct.index[]) and the rectangle axis + // directions (rct.U[]). + if (!UpdateSupport(A, numA, sort, N, polyline, visited, rct)) + { + // We have already processed the rectangle polygon edge, so the + // search is over. + break; + } + + if (rct.area < minRct.area) + { + minRct = rct; + } + } + + // Store relevant box information for computing volume and converting to + // an InputType bounding box. + box.P = mComputePoints[polyline[minRct.index[0]]]; + box.U[0] = minRct.U[0]; + box.U[1] = minRct.U[1]; + box.U[2] = N; + box.sqrLenU[0] = minRct.sqrLenU[0]; + box.sqrLenU[1] = minRct.sqrLenU[1]; + box.sqrLenU[2] = Dot(box.U[2], box.U[2]); + + // Compute the range of points in the plane perpendicular to the support + // normal. + box.range[0][0] = Dot(box.U[0], + mComputePoints[polyline[minRct.index[3]]] - box.P); + box.range[0][1] = Dot(box.U[0], + mComputePoints[polyline[minRct.index[1]]] - box.P); + box.range[1][0] = mZero; + box.range[1][1] = Dot(box.U[1], + mComputePoints[polyline[minRct.index[2]]] - box.P); + + // Compute the range of points in the support-normal direction. + box.range[2][0] = mZero; + box.range[2][1] = mZero; + for (auto i : mUniqueIndices) + { + Vector3 diff = mComputePoints[i] - box.P; + ComputeType height = Dot(box.U[2], diff); + if (height < box.range[2][0]) + { + box.range[2][0] = height; + } + else if (height > box.range[2][1]) + { + box.range[2][1] = height; + } + } + + // Compute the actual volume. + box.volume = + (box.range[0][1] - box.range[0][0]) * + ((box.range[1][1] - box.range[1][0]) / box.sqrLenU[1]) * + ((box.range[2][1] - box.range[2][0]) / box.sqrLenU[2]); +} + +template +typename MinimumVolumeBox3::ExtrudeRectangle +MinimumVolumeBox3::SmallestRectangle(int i0, int i1, + Vector3 const& N, std::vector const& polyline) +{ + ExtrudeRectangle rct; + Vector3 E = + mComputePoints[polyline[i1]] - mComputePoints[polyline[i0]]; + rct.U[1] = Cross(N, E); + rct.U[0] = Cross(rct.U[1], N); + rct.index = { { i1, i1, i1, i1 } }; + rct.sqrLenU[0] = Dot(rct.U[0], rct.U[0]); + rct.sqrLenU[1] = Dot(rct.U[1], rct.U[1]); + + Vector3 const& origin = mComputePoints[polyline[i1]]; + Vector2 support[4]; + for (int j = 0; j < 4; ++j) + { + support[j] = { mZero, mZero }; + } + + int i = 0; + for (auto p : polyline) + { + Vector3 diff = mComputePoints[p] - origin; + Vector2 v = { Dot(rct.U[0], diff), Dot(rct.U[1], diff) }; + + // The right-most vertex of the bottom edge is vertices[i1]. The + // assumption of no triple of collinear vertices guarantees that + // box.index[0] is i1, which is the initial value assigned at the + // beginning of this function. Therefore, there is no need to test + // for other vertices farther to the right than vertices[i1]. + + if (v[0] > support[1][0] || + (v[0] == support[1][0] && v[1] > support[1][1])) + { + // New right maximum OR same right maximum but closer to top. + rct.index[1] = i; + support[1] = v; + } + + if (v[1] > support[2][1] || + (v[1] == support[2][1] && v[0] < support[2][0])) + { + // New top maximum OR same top maximum but closer to left. + rct.index[2] = i; + support[2] = v; + } + + if (v[0] < support[3][0] || + (v[0] == support[3][0] && v[1] < support[3][1])) + { + // New left minimum OR same left minimum but closer to bottom. + rct.index[3] = i; + support[3] = v; + } + + ++i; + } + + // The comment in the loop has the implication that support[0] = { 0, 0 }, + // so the scaled height (support[2][1] - support[0][1]) is simply + // support[2][1]. + ComputeType scaledWidth = support[1][0] - support[3][0]; + ComputeType scaledHeight = support[2][1]; + rct.area = scaledWidth * scaledHeight / rct.sqrLenU[1]; + return rct; +} + +template +bool MinimumVolumeBox3::ComputeAngles( + Vector3 const& N, std::vector const& polyline, + ExtrudeRectangle const& rct, + std::array, 4>& A, int& numA) const +{ + int const numPolyline = static_cast(polyline.size()); + numA = 0; + for (int k1 = 0; k1 < 4; k1++) + { + int k0 = (k1 == 0) ? 3 : (k1 - 1); + + if (rct.index[k0] != rct.index[k1]) + { + // The rct edges are ordered in k1 as U[0], U[1], -U[0], -U[1]. + int lookup = (k0 & 1); + Vector3 D = + ((k0 & 2) ? -rct.U[lookup] : rct.U[lookup]); + int j0 = rct.index[k0], j1 = j0 + 1; + if (j1 == numPolyline) + { + j1 = 0; + } + Vector3 E = + mComputePoints[polyline[j1]] - mComputePoints[polyline[j0]]; + Vector3 Eperp = Cross(N, E); + E = Cross(Eperp, N); + Vector3 DxE = Cross(D, E); + ComputeType csqrlen = Dot(DxE, DxE); + ComputeType dsqrlen = rct.sqrLenU[lookup]; + ComputeType esqrlen = Dot(E, E); + ComputeType sinThetaSqr = csqrlen / (dsqrlen * esqrlen); + if (numA<4) + A[numA++] = std::make_pair(sinThetaSqr, k0); + } + } + return numA > 0; +} + +template +std::array MinimumVolumeBox3::SortAngles( + std::array, 4> const& A, int numA) const +{ + std::array sort = {{ 0, 1, 2, 3 }}; + if (numA > 1) + { + if (numA == 2) + { + if (A[sort[0]].first > A[sort[1]].first) + { + std::swap(sort[0], sort[1]); + } + } + else if (numA == 3) + { + if (A[sort[0]].first > A[sort[1]].first) + { + std::swap(sort[0], sort[1]); + } + if (A[sort[0]].first > A[sort[2]].first) + { + std::swap(sort[0], sort[2]); + } + if (A[sort[1]].first > A[sort[2]].first) + { + std::swap(sort[1], sort[2]); + } + } + else // numA == 4 + { + if (A[sort[0]].first > A[sort[1]].first) + { + std::swap(sort[0], sort[1]); + } + if (A[sort[2]].first > A[sort[3]].first) + { + std::swap(sort[2], sort[3]); + } + if (A[sort[0]].first > A[sort[2]].first) + { + std::swap(sort[0], sort[2]); + } + if (A[sort[1]].first > A[sort[3]].first) + { + std::swap(sort[1], sort[3]); + } + if (A[sort[1]].first > A[sort[2]].first) + { + std::swap(sort[1], sort[2]); + } + } + } + return sort; +} + +template +bool MinimumVolumeBox3::UpdateSupport( + std::array, 4> const& A, int numA, + std::array const& sort, Vector3 const& N, + std::vector const& polyline, std::vector& visited, + ExtrudeRectangle& rct) +{ + // Replace the support vertices of those edges attaining minimum angle + // with the other endpoints of the edges. + int const numPolyline = static_cast(polyline.size()); + auto const& amin = A[sort[0]]; + for (int k = 0; k < numA; ++k) + { + auto const& a = A[sort[k]]; + if (a.first == amin.first) + { + if (++rct.index[a.second] == numPolyline) + { + rct.index[a.second] = 0; + } + } + else + { + break; + } + } + + int bottom = rct.index[amin.second]; + if (visited[bottom]) + { + // We have already processed this polyline edge. + return false; + } + visited[bottom] = true; + + // Cycle the vertices so that the bottom support occurs first. + std::array nextIndex; + for (int k = 0; k < 4; ++k) + { + nextIndex[k] = rct.index[(amin.second + k) % 4]; + } + rct.index = nextIndex; + + // Compute the rectangle axis directions. + int j1 = rct.index[0], j0 = j1 - 1; + if (j1 < 0) + { + j1 = numPolyline - 1; + } + Vector3 E = + mComputePoints[polyline[j1]] - mComputePoints[polyline[j0]]; + rct.U[1] = Cross(N, E); + rct.U[0] = Cross(rct.U[1], N); + rct.sqrLenU[0] = Dot(rct.U[0], rct.U[0]); + rct.sqrLenU[1] = Dot(rct.U[1], rct.U[1]); + + // Compute the rectangle area. + Vector3 diff[2] = + { + mComputePoints[polyline[rct.index[1]]] + - mComputePoints[polyline[rct.index[3]]], + mComputePoints[polyline[rct.index[2]]] + - mComputePoints[polyline[rct.index[0]]] + }; + ComputeType scaledWidth = Dot(rct.U[0], diff[0]); + ComputeType scaledHeight = Dot(rct.U[1], diff[1]); + rct.area = scaledWidth * scaledHeight / rct.sqrLenU[1]; + return true; +} + +template +void MinimumVolumeBox3::ConvertTo(Box const& minBox, + OrientedBox3& itMinBox) +{ + Vector3 center = minBox.P; + for (int i = 0; i < 3; ++i) + { + ComputeType average = + mHalf * (minBox.range[i][0] + minBox.range[i][1]); + center += (average / minBox.sqrLenU[i]) * minBox.U[i]; + } + + // Calculate the squared extent using ComputeType to avoid loss of + // precision before computing a squared root. + Vector3 sqrExtent; + for (int i = 0; i < 3; ++i) + { + sqrExtent[i] = mHalf * (minBox.range[i][1] - minBox.range[i][0]); + sqrExtent[i] *= sqrExtent[i]; + sqrExtent[i] /= minBox.sqrLenU[i]; + } + + for (int i = 0; i < 3; ++i) + { + itMinBox.center[i] = (InputType)center[i]; + itMinBox.extent[i] = std::sqrt((InputType)sqrExtent[i]); + + // Before converting to floating-point, factor out the maximum + // component using ComputeType to generate rational numbers in a + // range that avoids loss of precision during the conversion and + // normalization. + Vector3 const& axis = minBox.U[i]; + ComputeType cmax = std::max(std::abs(axis[0]), std::abs(axis[1])); + cmax = std::max(cmax, std::abs(axis[2])); + ComputeType invCMax = mOne / cmax; + for (int j = 0; j < 3; ++j) + { + itMinBox.axis[i][j] = (InputType)(axis[j] * invCMax); + } + Normalize(itMinBox.axis[i]); + } + + mVolume = (InputType)minBox.volume; +} + +} + + +// ---------------------------------------------- +// GteMath.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.17.1 (2019/01/17) + +// This file extends the support to include convenient constants and +// functions. The shared constants for CPU, Intel SSE and GPU lead to +// correctly rounded approximations of the constants when using 'float' or +// 'double'. The file also includes a type trait, is_arbitrary_precision, +// to support selecting between floating-point arithmetic (float, double, +//long double) or arbitrary-precision arithmetic (BSNumber, BSRational) +// in an implementation using std::enable_if. There is also a type trait, +// has_division_operator, to support selecting between numeric types that +// have a division operator (BSRational) and those that do not have a +// division operator (BSNumber). + +// Maximum number of iterations for bisection before a subinterval +// degenerates to a single point. TODO: Verify these. I used the formula: +// 3 + std::numeric_limits::digits - std::numeric_limits::min_exponent. +// IEEEBinary16: digits = 11, min_exponent = -13 +// float: digits = 27, min_exponent = -125 +// double: digits = 53, min_exponent = -1021 +// BSNumber and BSRational use std::numeric_limits::max(), +// but maybe these should be set to a large number or be user configurable. +// The MAX_BISECTIONS_GENERIC is an arbitrary choice for now and is used +// in template code where Real is the template parameter. +#define GTE_C_MAX_BISECTIONS_FLOAT16 27u +#define GTE_C_MAX_BISECTIONS_FLOAT32 155u +#define GTE_C_MAX_BISECTIONS_FLOAT64 1077u +#define GTE_C_MAX_BISECTIONS_BSNUMBER 0xFFFFFFFFu +#define GTE_C_MAX_BISECTIONS_BSRATIONAL 0xFFFFFFFFu +#define GTE_C_MAX_BISECTIONS_GENERIC 2048u + +// Constants involving pi. +#define GTE_C_PI 3.1415926535897931 +#define GTE_C_HALF_PI 1.5707963267948966 +#define GTE_C_QUARTER_PI 0.7853981633974483 +#define GTE_C_TWO_PI 6.2831853071795862 +#define GTE_C_INV_PI 0.3183098861837907 +#define GTE_C_INV_TWO_PI 0.1591549430918953 +#define GTE_C_INV_HALF_PI 0.6366197723675813 + +// Conversions between degrees and radians. +#define GTE_C_DEG_TO_RAD 0.0174532925199433 +#define GTE_C_RAD_TO_DEG 57.295779513082321 + +// Common constants. +#define GTE_C_SQRT_2 1.4142135623730951 +#define GTE_C_INV_SQRT_2 0.7071067811865475 +#define GTE_C_LN_2 0.6931471805599453 +#define GTE_C_INV_LN_2 1.4426950408889634 +#define GTE_C_LN_10 2.3025850929940459 +#define GTE_C_INV_LN_10 0.43429448190325176 + +// Constants for minimax polynomial approximations to sqrt(x). +// The algorithm minimizes the maximum absolute error on [1,2]. +#define GTE_C_SQRT_DEG1_C0 +1.0 +#define GTE_C_SQRT_DEG1_C1 +4.1421356237309505e-01 +#define GTE_C_SQRT_DEG1_MAX_ERROR 1.7766952966368793e-2 + +#define GTE_C_SQRT_DEG2_C0 +1.0 +#define GTE_C_SQRT_DEG2_C1 +4.8563183076125260e-01 +#define GTE_C_SQRT_DEG2_C2 -7.1418268388157458e-02 +#define GTE_C_SQRT_DEG2_MAX_ERROR 1.1795695163108744e-3 + +#define GTE_C_SQRT_DEG3_C0 +1.0 +#define GTE_C_SQRT_DEG3_C1 +4.9750045320242231e-01 +#define GTE_C_SQRT_DEG3_C2 -1.0787308044477850e-01 +#define GTE_C_SQRT_DEG3_C3 +2.4586189615451115e-02 +#define GTE_C_SQRT_DEG3_MAX_ERROR 1.1309620116468910e-4 + +#define GTE_C_SQRT_DEG4_C0 +1.0 +#define GTE_C_SQRT_DEG4_C1 +4.9955939832918816e-01 +#define GTE_C_SQRT_DEG4_C2 -1.2024066151943025e-01 +#define GTE_C_SQRT_DEG4_C3 +4.5461507257698486e-02 +#define GTE_C_SQRT_DEG4_C4 -1.0566681694362146e-02 +#define GTE_C_SQRT_DEG4_MAX_ERROR 1.2741170151556180e-5 + +#define GTE_C_SQRT_DEG5_C0 +1.0 +#define GTE_C_SQRT_DEG5_C1 +4.9992197660031912e-01 +#define GTE_C_SQRT_DEG5_C2 -1.2378506719245053e-01 +#define GTE_C_SQRT_DEG5_C3 +5.6122776972699739e-02 +#define GTE_C_SQRT_DEG5_C4 -2.3128836281145482e-02 +#define GTE_C_SQRT_DEG5_C5 +5.0827122737047148e-03 +#define GTE_C_SQRT_DEG5_MAX_ERROR 1.5725568940708201e-6 + +#define GTE_C_SQRT_DEG6_C0 +1.0 +#define GTE_C_SQRT_DEG6_C1 +4.9998616695784914e-01 +#define GTE_C_SQRT_DEG6_C2 -1.2470733323278438e-01 +#define GTE_C_SQRT_DEG6_C3 +6.0388587356982271e-02 +#define GTE_C_SQRT_DEG6_C4 -3.1692053551807930e-02 +#define GTE_C_SQRT_DEG6_C5 +1.2856590305148075e-02 +#define GTE_C_SQRT_DEG6_C6 -2.6183954624343642e-03 +#define GTE_C_SQRT_DEG6_MAX_ERROR 2.0584155535630089e-7 + +#define GTE_C_SQRT_DEG7_C0 +1.0 +#define GTE_C_SQRT_DEG7_C1 +4.9999754817809228e-01 +#define GTE_C_SQRT_DEG7_C2 -1.2493243476353655e-01 +#define GTE_C_SQRT_DEG7_C3 +6.1859954146370910e-02 +#define GTE_C_SQRT_DEG7_C4 -3.6091595023208356e-02 +#define GTE_C_SQRT_DEG7_C5 +1.9483946523450868e-02 +#define GTE_C_SQRT_DEG7_C6 -7.5166134568007692e-03 +#define GTE_C_SQRT_DEG7_C7 +1.4127567687864939e-03 +#define GTE_C_SQRT_DEG7_MAX_ERROR 2.8072302919734948e-8 + +#define GTE_C_SQRT_DEG8_C0 +1.0 +#define GTE_C_SQRT_DEG8_C1 +4.9999956583056759e-01 +#define GTE_C_SQRT_DEG8_C2 -1.2498490369914350e-01 +#define GTE_C_SQRT_DEG8_C3 +6.2318494667579216e-02 +#define GTE_C_SQRT_DEG8_C4 -3.7982961896432244e-02 +#define GTE_C_SQRT_DEG8_C5 +2.3642612312869460e-02 +#define GTE_C_SQRT_DEG8_C6 -1.2529377587270574e-02 +#define GTE_C_SQRT_DEG8_C7 +4.5382426960713929e-03 +#define GTE_C_SQRT_DEG8_C8 -7.8810995273670414e-04 +#define GTE_C_SQRT_DEG8_MAX_ERROR 3.9460605685825989e-9 + +// Constants for minimax polynomial approximations to 1/sqrt(x). +// The algorithm minimizes the maximum absolute error on [1,2]. +#define GTE_C_INVSQRT_DEG1_C0 +1.0 +#define GTE_C_INVSQRT_DEG1_C1 -2.9289321881345254e-01 +#define GTE_C_INVSQRT_DEG1_MAX_ERROR 3.7814314552701983e-2 + +#define GTE_C_INVSQRT_DEG2_C0 +1.0 +#define GTE_C_INVSQRT_DEG2_C1 -4.4539812104566801e-01 +#define GTE_C_INVSQRT_DEG2_C2 +1.5250490223221547e-01 +#define GTE_C_INVSQRT_DEG2_MAX_ERROR 4.1953446330581234e-3 + +#define GTE_C_INVSQRT_DEG3_C0 +1.0 +#define GTE_C_INVSQRT_DEG3_C1 -4.8703230993068791e-01 +#define GTE_C_INVSQRT_DEG3_C2 +2.8163710486669835e-01 +#define GTE_C_INVSQRT_DEG3_C3 -8.7498013749463421e-02 +#define GTE_C_INVSQRT_DEG3_MAX_ERROR 5.6307702007266786e-4 + +#define GTE_C_INVSQRT_DEG4_C0 +1.0 +#define GTE_C_INVSQRT_DEG4_C1 -4.9710061558048779e-01 +#define GTE_C_INVSQRT_DEG4_C2 +3.4266247597676802e-01 +#define GTE_C_INVSQRT_DEG4_C3 -1.9106356536293490e-01 +#define GTE_C_INVSQRT_DEG4_C4 +5.2608486153198797e-02 +#define GTE_C_INVSQRT_DEG4_MAX_ERROR 8.1513919987605266e-5 + +#define GTE_C_INVSQRT_DEG5_C0 +1.0 +#define GTE_C_INVSQRT_DEG5_C1 -4.9937760586004143e-01 +#define GTE_C_INVSQRT_DEG5_C2 +3.6508741295133973e-01 +#define GTE_C_INVSQRT_DEG5_C3 -2.5884890281853501e-01 +#define GTE_C_INVSQRT_DEG5_C4 +1.3275782221320753e-01 +#define GTE_C_INVSQRT_DEG5_C5 -3.2511945299404488e-02 +#define GTE_C_INVSQRT_DEG5_MAX_ERROR 1.2289367475583346e-5 + +#define GTE_C_INVSQRT_DEG6_C0 +1.0 +#define GTE_C_INVSQRT_DEG6_C1 -4.9987029229547453e-01 +#define GTE_C_INVSQRT_DEG6_C2 +3.7220923604495226e-01 +#define GTE_C_INVSQRT_DEG6_C3 -2.9193067713256937e-01 +#define GTE_C_INVSQRT_DEG6_C4 +1.9937605991094642e-01 +#define GTE_C_INVSQRT_DEG6_C5 -9.3135712130901993e-02 +#define GTE_C_INVSQRT_DEG6_C6 +2.0458166789566690e-02 +#define GTE_C_INVSQRT_DEG6_MAX_ERROR 1.9001451223750465e-6 + +#define GTE_C_INVSQRT_DEG7_C0 +1.0 +#define GTE_C_INVSQRT_DEG7_C1 -4.9997357250704977e-01 +#define GTE_C_INVSQRT_DEG7_C2 +3.7426216884998809e-01 +#define GTE_C_INVSQRT_DEG7_C3 -3.0539882498248971e-01 +#define GTE_C_INVSQRT_DEG7_C4 +2.3976005607005391e-01 +#define GTE_C_INVSQRT_DEG7_C5 -1.5410326351684489e-01 +#define GTE_C_INVSQRT_DEG7_C6 +6.5598809723041995e-02 +#define GTE_C_INVSQRT_DEG7_C7 -1.3038592450470787e-02 +#define GTE_C_INVSQRT_DEG7_MAX_ERROR 2.9887724993168940e-7 + +#define GTE_C_INVSQRT_DEG8_C0 +1.0 +#define GTE_C_INVSQRT_DEG8_C1 -4.9999471066120371e-01 +#define GTE_C_INVSQRT_DEG8_C2 +3.7481415745794067e-01 +#define GTE_C_INVSQRT_DEG8_C3 -3.1023804387422160e-01 +#define GTE_C_INVSQRT_DEG8_C4 +2.5977002682930106e-01 +#define GTE_C_INVSQRT_DEG8_C5 -1.9818790717727097e-01 +#define GTE_C_INVSQRT_DEG8_C6 +1.1882414252613671e-01 +#define GTE_C_INVSQRT_DEG8_C7 -4.6270038088550791e-02 +#define GTE_C_INVSQRT_DEG8_C8 +8.3891541755747312e-03 +#define GTE_C_INVSQRT_DEG8_MAX_ERROR 4.7596926146947771e-8 + +// Constants for minimax polynomial approximations to sin(x). +// The algorithm minimizes the maximum absolute error on [-pi/2,pi/2]. +#define GTE_C_SIN_DEG3_C0 +1.0 +#define GTE_C_SIN_DEG3_C1 -1.4727245910375519e-01 +#define GTE_C_SIN_DEG3_MAX_ERROR 1.3481903639145865e-2 + +#define GTE_C_SIN_DEG5_C0 +1.0 +#define GTE_C_SIN_DEG5_C1 -1.6600599923812209e-01 +#define GTE_C_SIN_DEG5_C2 +7.5924178409012000e-03 +#define GTE_C_SIN_DEG5_MAX_ERROR 1.4001209384639779e-4 + +#define GTE_C_SIN_DEG7_C0 +1.0 +#define GTE_C_SIN_DEG7_C1 -1.6665578084732124e-01 +#define GTE_C_SIN_DEG7_C2 +8.3109378830028557e-03 +#define GTE_C_SIN_DEG7_C3 -1.8447486103462252e-04 +#define GTE_C_SIN_DEG7_MAX_ERROR 1.0205878936686563e-6 + +#define GTE_C_SIN_DEG9_C0 +1.0 +#define GTE_C_SIN_DEG9_C1 -1.6666656235308897e-01 +#define GTE_C_SIN_DEG9_C2 +8.3329962509886002e-03 +#define GTE_C_SIN_DEG9_C3 -1.9805100675274190e-04 +#define GTE_C_SIN_DEG9_C4 +2.5967200279475300e-06 +#define GTE_C_SIN_DEG9_MAX_ERROR 5.2010746265374053e-9 + +#define GTE_C_SIN_DEG11_C0 +1.0 +#define GTE_C_SIN_DEG11_C1 -1.6666666601721269e-01 +#define GTE_C_SIN_DEG11_C2 +8.3333303183525942e-03 +#define GTE_C_SIN_DEG11_C3 -1.9840782426250314e-04 +#define GTE_C_SIN_DEG11_C4 +2.7521557770526783e-06 +#define GTE_C_SIN_DEG11_C5 -2.3828544692960918e-08 +#define GTE_C_SIN_DEG11_MAX_ERROR 1.9295870457014530e-11 + +// Constants for minimax polynomial approximations to cos(x). +// The algorithm minimizes the maximum absolute error on [-pi/2,pi/2]. +#define GTE_C_COS_DEG2_C0 +1.0 +#define GTE_C_COS_DEG2_C1 -4.0528473456935105e-01 +#define GTE_C_COS_DEG2_MAX_ERROR 5.4870946878404048e-2 + +#define GTE_C_COS_DEG4_C0 +1.0 +#define GTE_C_COS_DEG4_C1 -4.9607181958647262e-01 +#define GTE_C_COS_DEG4_C2 +3.6794619653489236e-02 +#define GTE_C_COS_DEG4_MAX_ERROR 9.1879932449712154e-4 + +#define GTE_C_COS_DEG6_C0 +1.0 +#define GTE_C_COS_DEG6_C1 -4.9992746217057404e-01 +#define GTE_C_COS_DEG6_C2 +4.1493920348353308e-02 +#define GTE_C_COS_DEG6_C3 -1.2712435011987822e-03 +#define GTE_C_COS_DEG6_MAX_ERROR 9.2028470133065365e-6 + +#define GTE_C_COS_DEG8_C0 +1.0 +#define GTE_C_COS_DEG8_C1 -4.9999925121358291e-01 +#define GTE_C_COS_DEG8_C2 +4.1663780117805693e-02 +#define GTE_C_COS_DEG8_C3 -1.3854239405310942e-03 +#define GTE_C_COS_DEG8_C4 +2.3154171575501259e-05 +#define GTE_C_COS_DEG8_MAX_ERROR 5.9804533020235695e-8 + +#define GTE_C_COS_DEG10_C0 +1.0 +#define GTE_C_COS_DEG10_C1 -4.9999999508695869e-01 +#define GTE_C_COS_DEG10_C2 +4.1666638865338612e-02 +#define GTE_C_COS_DEG10_C3 -1.3888377661039897e-03 +#define GTE_C_COS_DEG10_C4 +2.4760495088926859e-05 +#define GTE_C_COS_DEG10_C5 -2.6051615464872668e-07 +#define GTE_C_COS_DEG10_MAX_ERROR 2.7006769043325107e-10 + +// Constants for minimax polynomial approximations to tan(x). +// The algorithm minimizes the maximum absolute error on [-pi/4,pi/4]. +#define GTE_C_TAN_DEG3_C0 1.0 +#define GTE_C_TAN_DEG3_C1 4.4295926544736286e-01 +#define GTE_C_TAN_DEG3_MAX_ERROR 1.1661892256204731e-2 + +#define GTE_C_TAN_DEG5_C0 1.0 +#define GTE_C_TAN_DEG5_C1 3.1401320403542421e-01 +#define GTE_C_TAN_DEG5_C2 2.0903948109240345e-01 +#define GTE_C_TAN_DEG5_MAX_ERROR 5.8431854390143118e-4 + +#define GTE_C_TAN_DEG7_C0 1.0 +#define GTE_C_TAN_DEG7_C1 3.3607213284422555e-01 +#define GTE_C_TAN_DEG7_C2 1.1261037305184907e-01 +#define GTE_C_TAN_DEG7_C3 9.8352099470524479e-02 +#define GTE_C_TAN_DEG7_MAX_ERROR 3.5418688397723108e-5 + +#define GTE_C_TAN_DEG9_C0 1.0 +#define GTE_C_TAN_DEG9_C1 3.3299232843941784e-01 +#define GTE_C_TAN_DEG9_C2 1.3747843432474838e-01 +#define GTE_C_TAN_DEG9_C3 3.7696344813028304e-02 +#define GTE_C_TAN_DEG9_C4 4.6097377279281204e-02 +#define GTE_C_TAN_DEG9_MAX_ERROR 2.2988173242199927e-6 + +#define GTE_C_TAN_DEG11_C0 1.0 +#define GTE_C_TAN_DEG11_C1 3.3337224456224224e-01 +#define GTE_C_TAN_DEG11_C2 1.3264516053824593e-01 +#define GTE_C_TAN_DEG11_C3 5.8145237645931047e-02 +#define GTE_C_TAN_DEG11_C4 1.0732193237572574e-02 +#define GTE_C_TAN_DEG11_C5 2.1558456793513869e-02 +#define GTE_C_TAN_DEG11_MAX_ERROR 1.5426257940140409e-7 + +#define GTE_C_TAN_DEG13_C0 1.0 +#define GTE_C_TAN_DEG13_C1 3.3332916426394554e-01 +#define GTE_C_TAN_DEG13_C2 1.3343404625112498e-01 +#define GTE_C_TAN_DEG13_C3 5.3104565343119248e-02 +#define GTE_C_TAN_DEG13_C4 2.5355038312682154e-02 +#define GTE_C_TAN_DEG13_C5 1.8253255966556026e-03 +#define GTE_C_TAN_DEG13_C6 1.0069407176615641e-02 +#define GTE_C_TAN_DEG13_MAX_ERROR 1.0550264249037378e-8 + +// Constants for minimax polynomial approximations to acos(x), where the +// approximation is of the form acos(x) = sqrt(1 - x)*p(x) with p(x) a +// polynomial. The algorithm minimizes the maximum error +// |acos(x)/sqrt(1-x) - p(x)| on [0,1]. At the same time we get an +// approximation for asin(x) = pi/2 - acos(x). +#define GTE_C_ACOS_DEG1_C0 +1.5707963267948966 +#define GTE_C_ACOS_DEG1_C1 -1.5658276442180141e-01 +#define GTE_C_ACOS_DEG1_MAX_ERROR 1.1659002803738105e-2 + +#define GTE_C_ACOS_DEG2_C0 +1.5707963267948966 +#define GTE_C_ACOS_DEG2_C1 -2.0347053865798365e-01 +#define GTE_C_ACOS_DEG2_C2 +4.6887774236182234e-02 +#define GTE_C_ACOS_DEG2_MAX_ERROR 9.0311602490029258e-4 + +#define GTE_C_ACOS_DEG3_C0 +1.5707963267948966 +#define GTE_C_ACOS_DEG3_C1 -2.1253291899190285e-01 +#define GTE_C_ACOS_DEG3_C2 +7.4773789639484223e-02 +#define GTE_C_ACOS_DEG3_C3 -1.8823635069382449e-02 +#define GTE_C_ACOS_DEG3_MAX_ERROR 9.3066396954288172e-5 + +#define GTE_C_ACOS_DEG4_C0 +1.5707963267948966 +#define GTE_C_ACOS_DEG4_C1 -2.1422258835275865e-01 +#define GTE_C_ACOS_DEG4_C2 +8.4936675142844198e-02 +#define GTE_C_ACOS_DEG4_C3 -3.5991475120957794e-02 +#define GTE_C_ACOS_DEG4_C4 +8.6946239090712751e-03 +#define GTE_C_ACOS_DEG4_MAX_ERROR 1.0930595804481413e-5 + +#define GTE_C_ACOS_DEG5_C0 +1.5707963267948966 +#define GTE_C_ACOS_DEG5_C1 -2.1453292139805524e-01 +#define GTE_C_ACOS_DEG5_C2 +8.7973089282889383e-02 +#define GTE_C_ACOS_DEG5_C3 -4.5130266382166440e-02 +#define GTE_C_ACOS_DEG5_C4 +1.9467466687281387e-02 +#define GTE_C_ACOS_DEG5_C5 -4.3601326117634898e-03 +#define GTE_C_ACOS_DEG5_MAX_ERROR 1.3861070257241426-6 + +#define GTE_C_ACOS_DEG6_C0 +1.5707963267948966 +#define GTE_C_ACOS_DEG6_C1 -2.1458939285677325e-01 +#define GTE_C_ACOS_DEG6_C2 +8.8784960563641491e-02 +#define GTE_C_ACOS_DEG6_C3 -4.8887131453156485e-02 +#define GTE_C_ACOS_DEG6_C4 +2.7011519960012720e-02 +#define GTE_C_ACOS_DEG6_C5 -1.1210537323478320e-02 +#define GTE_C_ACOS_DEG6_C6 +2.3078166879102469e-03 +#define GTE_C_ACOS_DEG6_MAX_ERROR 1.8491291330427484e-7 + +#define GTE_C_ACOS_DEG7_C0 +1.5707963267948966 +#define GTE_C_ACOS_DEG7_C1 -2.1459960076929829e-01 +#define GTE_C_ACOS_DEG7_C2 +8.8986946573346160e-02 +#define GTE_C_ACOS_DEG7_C3 -5.0207843052845647e-02 +#define GTE_C_ACOS_DEG7_C4 +3.0961594977611639e-02 +#define GTE_C_ACOS_DEG7_C5 -1.7162031184398074e-02 +#define GTE_C_ACOS_DEG7_C6 +6.7072304676685235e-03 +#define GTE_C_ACOS_DEG7_C7 -1.2690614339589956e-03 +#define GTE_C_ACOS_DEG7_MAX_ERROR 2.5574620927948377e-8 + +#define GTE_C_ACOS_DEG8_C0 +1.5707963267948966 +#define GTE_C_ACOS_DEG8_C1 -2.1460143648688035e-01 +#define GTE_C_ACOS_DEG8_C2 +8.9034700107934128e-02 +#define GTE_C_ACOS_DEG8_C3 -5.0625279962389413e-02 +#define GTE_C_ACOS_DEG8_C4 +3.2683762943179318e-02 +#define GTE_C_ACOS_DEG8_C5 -2.0949278766238422e-02 +#define GTE_C_ACOS_DEG8_C6 +1.1272900916992512e-02 +#define GTE_C_ACOS_DEG8_C7 -4.1160981058965262e-03 +#define GTE_C_ACOS_DEG8_C8 +7.1796493341480527e-04 +#define GTE_C_ACOS_DEG8_MAX_ERROR 3.6340015129032732e-9 + +// Constants for minimax polynomial approximations to atan(x). +// The algorithm minimizes the maximum absolute error on [-1,1]. +#define GTE_C_ATAN_DEG3_C0 +1.0 +#define GTE_C_ATAN_DEG3_C1 -2.1460183660255172e-01 +#define GTE_C_ATAN_DEG3_MAX_ERROR 1.5970326392614240e-2 + +#define GTE_C_ATAN_DEG5_C0 +1.0 +#define GTE_C_ATAN_DEG5_C1 -3.0189478312144946e-01 +#define GTE_C_ATAN_DEG5_C2 +8.7292946518897740e-02 +#define GTE_C_ATAN_DEG5_MAX_ERROR 1.3509832247372636e-3 + +#define GTE_C_ATAN_DEG7_C0 +1.0 +#define GTE_C_ATAN_DEG7_C1 -3.2570157599356531e-01 +#define GTE_C_ATAN_DEG7_C2 +1.5342994884206673e-01 +#define GTE_C_ATAN_DEG7_C3 -4.2330209451053591e-02 +#define GTE_C_ATAN_DEG7_MAX_ERROR 1.5051227215514412e-4 + +#define GTE_C_ATAN_DEG9_C0 +1.0 +#define GTE_C_ATAN_DEG9_C1 -3.3157878236439586e-01 +#define GTE_C_ATAN_DEG9_C2 +1.8383034738018011e-01 +#define GTE_C_ATAN_DEG9_C3 -8.9253037587244677e-02 +#define GTE_C_ATAN_DEG9_C4 +2.2399635968909593e-02 +#define GTE_C_ATAN_DEG9_MAX_ERROR 1.8921598624582064e-5 + +#define GTE_C_ATAN_DEG11_C0 +1.0 +#define GTE_C_ATAN_DEG11_C1 -3.3294527685374087e-01 +#define GTE_C_ATAN_DEG11_C2 +1.9498657165383548e-01 +#define GTE_C_ATAN_DEG11_C3 -1.1921576270475498e-01 +#define GTE_C_ATAN_DEG11_C4 +5.5063351366968050e-02 +#define GTE_C_ATAN_DEG11_C5 -1.2490720064867844e-02 +#define GTE_C_ATAN_DEG11_MAX_ERROR 2.5477724974187765e-6 + +#define GTE_C_ATAN_DEG13_C0 +1.0 +#define GTE_C_ATAN_DEG13_C1 -3.3324998579202170e-01 +#define GTE_C_ATAN_DEG13_C2 +1.9856563505717162e-01 +#define GTE_C_ATAN_DEG13_C3 -1.3374657325451267e-01 +#define GTE_C_ATAN_DEG13_C4 +8.1675882859940430e-02 +#define GTE_C_ATAN_DEG13_C5 -3.5059680836411644e-02 +#define GTE_C_ATAN_DEG13_C6 +7.2128853633444123e-03 +#define GTE_C_ATAN_DEG13_MAX_ERROR 3.5859104691865484e-7 + +// Constants for minimax polynomial approximations to exp2(x) = 2^x. +// The algorithm minimizes the maximum absolute error on [0,1]. +#define GTE_C_EXP2_DEG1_C0 1.0 +#define GTE_C_EXP2_DEG1_C1 1.0 +#define GTE_C_EXP2_DEG1_MAX_ERROR 8.6071332055934313e-2 + +#define GTE_C_EXP2_DEG2_C0 1.0 +#define GTE_C_EXP2_DEG2_C1 6.5571332605741528e-01 +#define GTE_C_EXP2_DEG2_C2 3.4428667394258472e-01 +#define GTE_C_EXP2_DEG2_MAX_ERROR 3.8132476831060358e-3 + +#define GTE_C_EXP2_DEG3_C0 1.0 +#define GTE_C_EXP2_DEG3_C1 6.9589012084456225e-01 +#define GTE_C_EXP2_DEG3_C2 2.2486494900110188e-01 +#define GTE_C_EXP2_DEG3_C3 7.9244930154334980e-02 +#define GTE_C_EXP2_DEG3_MAX_ERROR 1.4694877755186408e-4 + +#define GTE_C_EXP2_DEG4_C0 1.0 +#define GTE_C_EXP2_DEG4_C1 6.9300392358459195e-01 +#define GTE_C_EXP2_DEG4_C2 2.4154981722455560e-01 +#define GTE_C_EXP2_DEG4_C3 5.1744260331489045e-02 +#define GTE_C_EXP2_DEG4_C4 1.3701998859367848e-02 +#define GTE_C_EXP2_DEG4_MAX_ERROR 4.7617792624521371e-6 + +#define GTE_C_EXP2_DEG5_C0 1.0 +#define GTE_C_EXP2_DEG5_C1 6.9315298010274962e-01 +#define GTE_C_EXP2_DEG5_C2 2.4014712313022102e-01 +#define GTE_C_EXP2_DEG5_C3 5.5855296413199085e-02 +#define GTE_C_EXP2_DEG5_C4 8.9477503096873079e-03 +#define GTE_C_EXP2_DEG5_C5 1.8968500441332026e-03 +#define GTE_C_EXP2_DEG5_MAX_ERROR 1.3162098333463490e-7 + +#define GTE_C_EXP2_DEG6_C0 1.0 +#define GTE_C_EXP2_DEG6_C1 6.9314698914837525e-01 +#define GTE_C_EXP2_DEG6_C2 2.4023013440952923e-01 +#define GTE_C_EXP2_DEG6_C3 5.5481276898206033e-02 +#define GTE_C_EXP2_DEG6_C4 9.6838443037086108e-03 +#define GTE_C_EXP2_DEG6_C5 1.2388324048515642e-03 +#define GTE_C_EXP2_DEG6_C6 2.1892283501756538e-04 +#define GTE_C_EXP2_DEG6_MAX_ERROR 3.1589168225654163e-9 + +#define GTE_C_EXP2_DEG7_C0 1.0 +#define GTE_C_EXP2_DEG7_C1 6.9314718588750690e-01 +#define GTE_C_EXP2_DEG7_C2 2.4022637363165700e-01 +#define GTE_C_EXP2_DEG7_C3 5.5505235570535660e-02 +#define GTE_C_EXP2_DEG7_C4 9.6136265387940512e-03 +#define GTE_C_EXP2_DEG7_C5 1.3429234504656051e-03 +#define GTE_C_EXP2_DEG7_C6 1.4299202757683815e-04 +#define GTE_C_EXP2_DEG7_C7 2.1662892777385423e-05 +#define GTE_C_EXP2_DEG7_MAX_ERROR 6.6864513925679603e-11 + +// Constants for minimax polynomial approximations to log2(x). +// The algorithm minimizes the maximum absolute error on [1,2]. +// The polynomials all have constant term zero. +#define GTE_C_LOG2_DEG1_C1 +1.0 +#define GTE_C_LOG2_DEG1_MAX_ERROR 8.6071332055934202e-2 + +#define GTE_C_LOG2_DEG2_C1 +1.3465553856377803 +#define GTE_C_LOG2_DEG2_C2 -3.4655538563778032e-01 +#define GTE_C_LOG2_DEG2_MAX_ERROR 7.6362868906658110e-3 + +#define GTE_C_LOG2_DEG3_C1 +1.4228653756681227 +#define GTE_C_LOG2_DEG3_C2 -5.8208556916449616e-01 +#define GTE_C_LOG2_DEG3_C3 +1.5922019349637218e-01 +#define GTE_C_LOG2_DEG3_MAX_ERROR 8.7902902652883808e-4 + +#define GTE_C_LOG2_DEG4_C1 +1.4387257478171547 +#define GTE_C_LOG2_DEG4_C2 -6.7778401359918661e-01 +#define GTE_C_LOG2_DEG4_C3 +3.2118898377713379e-01 +#define GTE_C_LOG2_DEG4_C4 -8.2130717995088531e-02 +#define GTE_C_LOG2_DEG4_MAX_ERROR 1.1318551355360418e-4 + +#define GTE_C_LOG2_DEG5_C1 +1.4419170408633741 +#define GTE_C_LOG2_DEG5_C2 -7.0909645927612530e-01 +#define GTE_C_LOG2_DEG5_C3 +4.1560609399164150e-01 +#define GTE_C_LOG2_DEG5_C4 -1.9357573729558908e-01 +#define GTE_C_LOG2_DEG5_C5 +4.5149061716699634e-02 +#define GTE_C_LOG2_DEG5_MAX_ERROR 1.5521274478735858e-5 + +#define GTE_C_LOG2_DEG6_C1 +1.4425449435950917 +#define GTE_C_LOG2_DEG6_C2 -7.1814525675038965e-01 +#define GTE_C_LOG2_DEG6_C3 +4.5754919692564044e-01 +#define GTE_C_LOG2_DEG6_C4 -2.7790534462849337e-01 +#define GTE_C_LOG2_DEG6_C5 +1.2179791068763279e-01 +#define GTE_C_LOG2_DEG6_C6 -2.5841449829670182e-02 +#define GTE_C_LOG2_DEG6_MAX_ERROR 2.2162051216689793e-6 + +#define GTE_C_LOG2_DEG7_C1 +1.4426664401536078 +#define GTE_C_LOG2_DEG7_C2 -7.2055423726162360e-01 +#define GTE_C_LOG2_DEG7_C3 +4.7332419162501083e-01 +#define GTE_C_LOG2_DEG7_C4 -3.2514018752954144e-01 +#define GTE_C_LOG2_DEG7_C5 +1.9302965529095673e-01 +#define GTE_C_LOG2_DEG7_C6 -7.8534970641157997e-02 +#define GTE_C_LOG2_DEG7_C7 +1.5209108363023915e-02 +#define GTE_C_LOG2_DEG7_MAX_ERROR 3.2546531700261561e-7 + +#define GTE_C_LOG2_DEG8_C1 +1.4426896453621882 +#define GTE_C_LOG2_DEG8_C2 -7.2115893912535967e-01 +#define GTE_C_LOG2_DEG8_C3 +4.7861716616785088e-01 +#define GTE_C_LOG2_DEG8_C4 -3.4699935395019565e-01 +#define GTE_C_LOG2_DEG8_C5 +2.4114048765477492e-01 +#define GTE_C_LOG2_DEG8_C6 -1.3657398692885181e-01 +#define GTE_C_LOG2_DEG8_C7 +5.1421382871922106e-02 +#define GTE_C_LOG2_DEG8_C8 -9.1364020499895560e-03 +#define GTE_C_LOG2_DEG8_MAX_ERROR 4.8796219218050219e-8 + +// These functions are convenient for some applications. The classes +// BSNumber, BSRational and IEEEBinary16 have implementations that +// (for now) use typecasting to call the 'float' or 'double' versions. +namespace houdini::gte +{ + inline float atandivpi(float x) + { + return std::atan(x) * (float)GTE_C_INV_PI; + } + + inline float atan2divpi(float y, float x) + { + return std::atan2(y, x) * (float)GTE_C_INV_PI; + } + + inline float clamp(float x, float xmin, float xmax) + { + return (x <= xmin ? xmin : (x >= xmax ? xmax : x)); + } + + inline float cospi(float x) + { + return std::cos(x * (float)GTE_C_PI); + } + + inline float exp10(float x) + { + return std::exp(x * (float)GTE_C_LN_10); + } + + inline float invsqrt(float x) + { + return 1.0f / std::sqrt(x); + } + + inline int isign(float x) + { + return (x > 0.0f ? 1 : (x < 0.0f ? -1 : 0)); + } + + inline float saturate(float x) + { + return (x <= 0.0f ? 0.0f : (x >= 1.0f ? 1.0f : x)); + } + + inline float sign(float x) + { + return (x > 0.0f ? 1.0f : (x < 0.0f ? -1.0f : 0.0f)); + } + + inline float sinpi(float x) + { + return std::sin(x * (float)GTE_C_PI); + } + + inline float sqr(float x) + { + return x * x; + } + + + inline double atandivpi(double x) + { + return std::atan(x) * GTE_C_INV_PI; + } + + inline double atan2divpi(double y, double x) + { + return std::atan2(y, x) * GTE_C_INV_PI; + } + + inline double clamp(double x, double xmin, double xmax) + { + return (x <= xmin ? xmin : (x >= xmax ? xmax : x)); + } + + inline double cospi(double x) + { + return std::cos(x * GTE_C_PI); + } + + inline double exp10(double x) + { + return std::exp(x * GTE_C_LN_10); + } + + inline double invsqrt(double x) + { + return 1.0 / std::sqrt(x); + } + + inline double sign(double x) + { + return (x > 0.0 ? 1.0 : (x < 0.0 ? -1.0 : 0.0f)); + } + + inline int isign(double x) + { + return (x > 0.0 ? 1 : (x < 0.0 ? -1 : 0)); + } + + inline double saturate(double x) + { + return (x <= 0.0 ? 0.0 : (x >= 1.0 ? 1.0 : x)); + } + + inline double sinpi(double x) + { + return std::sin(x * GTE_C_PI); + } + + inline double sqr(double x) + { + return x * x; + } +} + +// Type traits to support std::enable_if conditional compilation for +// numerical computations. +namespace houdini::gte +{ + // The trait is_arbitrary_precision for type T of float, double or + // long double generates is_arbitrary_precision::value of false. The + // implementations for arbitrary-precision arithmetic are found in + // GteArbitraryPrecision.h. + template + struct is_arbitrary_precision_internal : std::false_type {}; + + template + struct is_arbitrary_precision : is_arbitrary_precision_internal>::type {}; + + // The trait has_division_operator for type T of float, double or + // long double generates has_division_operator::value of true. The + // implementations for arbitrary-precision arithmetic are found in + // GteArbitraryPrecision.h. + template + struct has_division_operator_internal : std::false_type {}; + + template + struct has_division_operator : has_division_operator_internal>::type {}; + + template <> + struct has_division_operator_internal : std::true_type {}; + + template <> + struct has_division_operator_internal : std::true_type {}; + + template <> + struct has_division_operator_internal : std::true_type {}; +} + + +// ---------------------------------------------- +// GteDCPQuery.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.1 (2018/10/05) + +namespace houdini::gte +{ + +// Distance and closest-point queries. +template +class DCPQuery +{ +public: + struct Result + { + // A DCPQuery-base class B must define a B::Result struct with member + // 'Real distance'. A DCPQuery-derived class D must also derive a + // D::Result from B:Result but may have no members. The idea is to + // allow Result to store closest-point information in addition to the + // distance. The operator() is non-const to allow DCPQuery to store + // and modify private state that supports the query. + }; + Result operator()(Type0 const& primitive0, Type1 const& primitive1); +}; + +} + + +// ---------------------------------------------- +// GteApprQuery.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// Class ApprQuery supports the RANSAC algorithm for fitting and uses the +// Curiously Recurring Template Pattern. The ModelType must be a class or +// struct with the following interfaces: +// +// // The minimum number of observations required to fit the model. +// int ModelType::GetMinimumRequired() const; +// +// // Compute the model error for the specified observation for the current +// // model parameters. +// Real Error(ObservationType const& observation) const; +// +// // Estimate the model parameters for all observations specified by the +// // indices. The three Fit() functions of ApprQuery manipulate their +// // inputs in order to pass them to ModelType::Fit(). +// ModelType::Fit(std::vector const& observations, +// std::vector const& indices); + +namespace houdini::gte +{ + +template +class ApprQuery +{ +public: + // Estimate the model parameters for all observations. + bool Fit(std::vector const& observations); + + // Estimate the model parameters for a contiguous subset of observations. + bool Fit(std::vector const& observations, + int const imin, int const imax); + + // Estimate the model parameters for the subset of observations specified + // by the indices and the number of indices that is possibly smaller than + // indices.size(). + bool Fit(std::vector const& observations, + std::vector const& indices, int const numIndices); + + // Apply the RANdom SAmple Consensus algorithm for fitting a model to + // observations. + static bool RANSAC( + ModelType& candidateModel, + std::vector const& observations, + int const numRequiredForGoodFit, Real const maxErrorForGoodFit, + int const numIterations, std::vector& bestConsensus, + ModelType& bestModel); +}; + + +template +bool ApprQuery::Fit( + std::vector const& observations) +{ + std::vector indices(observations.size()); + int i = 0; + for (auto& index : indices) + { + index = i++; + } + + return ((ModelType*)this)->Fit(observations, indices); +} + +template +bool ApprQuery::Fit( + std::vector const& observations, + int const imin, int const imax) +{ + if (imin <= imax) + { + int numIndices = imax - imin + 1; + std::vector indices(numIndices); + int i = imin; + for (auto& index : indices) + { + index = i++; + } + + return ((ModelType*)this)->Fit(observations, indices); + } + else + { + return false; + } +} + +template +bool ApprQuery::Fit( + std::vector const& observations, + std::vector const& indices, int const numIndices) +{ + int imax = std::min(numIndices, static_cast(observations.size())); + std::vector localindices(imax); + int i = 0; + for (auto& index : localindices) + { + index = indices[i++]; + } + + return ((ModelType*)this)->Fit(observations, indices); +} + +template +bool ApprQuery::RANSAC( + ModelType& candidateModel, + std::vector const& observations, + int const numRequiredForGoodFit, Real const maxErrorForGoodFit, + int const numIterations, std::vector& bestConsensus, + ModelType& bestModel) +{ + int const numObservations = static_cast(observations.size()); + int const minRequired = candidateModel.GetMinimumRequired(); + if (numObservations < minRequired) + { + // Too few observations for model fitting. + return false; + } + + // The first part of the array will store the consensus set, initially + // filled with the minimumu number of indices that correspond to the + // candidate inliers. The last part will store the remaining indices. + // These points are tested against the model and are added to the + // consensus set when they fit. All the index manipulation is done + // in place. Initially, the candidates are the identity permutation. + std::vector candidates(numObservations); + int j = 0; + for (auto& c : candidates) + { + c = j++; + } + + if (numObservations == minRequired) + { + // We have the minimum number of observations to generate the model, + // so RANSAC cannot be used. Compute the model with the entire set + // of observations. + bestConsensus = candidates; + return bestModel.Fit(observations); + } + + int bestNumFittedObservations = minRequired; + + for (int i = 0; i < numIterations; ++i) + { + // Randomly permute the previous candidates, partitioning the array + // into GetMinimumRequired() indices (the candidate inliers) followed + // by the remaining indices (candidates for testing against the + // model). + std::shuffle(candidates.begin(), candidates.end(), + std::default_random_engine()); + + // Fit the model to the inliers. + if (candidateModel.Fit(observations, candidates, minRequired)) + { + // Test each remaining observation whether it fits the model. If + // it does, include it in the consensus set. + int numFittedObservations = minRequired; + for (j = minRequired; j < numObservations; ++j) + { + if (candidateModel.Error(observations[candidates[j]]) + <= maxErrorForGoodFit) + { + std::swap(candidates[j], + candidates[numFittedObservations]); + ++numFittedObservations; + } + } + + if (numFittedObservations >= numRequiredForGoodFit) + { + // We have observations that fit the model. Update the best + // model using the consensus set. + candidateModel.Fit(observations, candidates, + numFittedObservations); + if (numFittedObservations > bestNumFittedObservations) + { + // The consensus set is larger than the previous consensus + // set, so its model becomes the best one. + bestModel = candidateModel; + bestConsensus.resize(numFittedObservations); + std::copy(candidates.begin(), + candidates.begin() + numFittedObservations, + bestConsensus.begin()); + bestNumFittedObservations = numFittedObservations; + } + } + } + } + + return bestNumFittedObservations >= numRequiredForGoodFit; +} + + +} + + +// ---------------------------------------------- +// GteSymmetricEigensolver3x3.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.5 (2018/10/09) + +// The document +// http://www.geometrictools.com/Documentation/RobustEigenSymmetric3x3.pdf +// describes algorithms for solving the eigensystem associated with a 3x3 +// symmetric real-valued matrix. The iterative algorithm is implemented +// by class SymmmetricEigensolver3x3. The noniterative algorithm is +// implemented by class NISymmetricEigensolver3x3. The code does not use +// GTEngine objects. + +namespace houdini::gte +{ + +template +class SymmetricEigensolver3x3 +{ +public: + // The input matrix must be symmetric, so only the unique elements must + // be specified: a00, a01, a02, a11, a12, and a22. + // + // If 'aggressive' is 'true', the iterations occur until a superdiagonal + // entry is exactly zero. If 'aggressive' is 'false', the iterations + // occur until a superdiagonal entry is effectively zero compared to the + // sum of magnitudes of its diagonal neighbors. Generally, the + // nonaggressive convergence is acceptable. + // + // The order of the eigenvalues is specified by sortType: -1 (decreasing), + // 0 (no sorting), or +1 (increasing). When sorted, the eigenvectors are + // ordered accordingly, and {evec[0], evec[1], evec[2]} is guaranteed to + // be a right-handed orthonormal set. The return value is the number of + // iterations used by the algorithm. + + int operator()(Real a00, Real a01, Real a02, Real a11, Real a12, Real a22, + bool aggressive, int sortType, std::array& eval, + std::array, 3>& evec) const; + +private: + // Update Q = Q*G in-place using G = {{c,0,-s},{s,0,c},{0,0,1}}. + void Update0(Real Q[3][3], Real c, Real s) const; + + // Update Q = Q*G in-place using G = {{0,1,0},{c,0,s},{-s,0,c}}. + void Update1(Real Q[3][3], Real c, Real s) const; + + // Update Q = Q*H in-place using H = {{c,s,0},{s,-c,0},{0,0,1}}. + void Update2(Real Q[3][3], Real c, Real s) const; + + // Update Q = Q*H in-place using H = {{1,0,0},{0,c,s},{0,s,-c}}. + void Update3(Real Q[3][3], Real c, Real s) const; + + // Normalize (u,v) robustly, avoiding floating-point overflow in the sqrt + // call. The normalized pair is (cs,sn) with cs <= 0. If (u,v) = (0,0), + // the function returns (cs,sn) = (-1,0). When used to generate a + // Householder reflection, it does not matter whether (cs,sn) or (-cs,-sn) + // is used. When generating a Givens reflection, cs = cos(2*theta) and + // sn = sin(2*theta). Having a negative cosine for the double-angle + // term ensures that the single-angle terms c = cos(theta) and + // s = sin(theta) satisfy |c| <= |s|. + void GetCosSin(Real u, Real v, Real& cs, Real& sn) const; + + // The convergence test. When 'aggressive' is 'true', the superdiagonal + // test is "bSuper == 0". When 'aggressive' is 'false', the superdiagonal + // test is "|bDiag0| + |bDiag1| + |bSuper| == |bDiag0| + |bDiag1|, which + // means bSuper is effectively zero compared to the sizes of the diagonal + // entries. + bool Converged(bool aggressive, Real bDiag0, Real bDiag1, + Real bSuper) const; + + // Support for sorting the eigenvalues and eigenvectors. The output + // (i0,i1,i2) is a permutation of (0,1,2) so that d[i0] <= d[i1] <= d[i2]. + // The 'bool' return indicates whether the permutation is odd. If it is + // not, the handedness of the Q matrix must be adjusted. + bool Sort(std::array const& d, int& i0, int& i1, int& i2) const; +}; + + +template +class NISymmetricEigensolver3x3 +{ +public: + // The input matrix must be symmetric, so only the unique elements must + // be specified: a00, a01, a02, a11, a12, and a22. The eigenvalues are + // sorted in ascending order: eval0 <= eval1 <= eval2. + + void operator()(Real a00, Real a01, Real a02, Real a11, Real a12, Real a22, + std::array& eval, std::array, 3>& evec) const; + +private: + static std::array Multiply(Real s, std::array const& U); + static std::array Subtract(std::array const& U, std::array const& V); + static std::array Divide(std::array const& U, Real s); + static Real Dot(std::array const& U, std::array const& V); + static std::array Cross(std::array const& U, std::array const& V); + + void ComputeOrthogonalComplement(std::array const& W, + std::array& U, std::array& V) const; + + void ComputeEigenvector0(Real a00, Real a01, Real a02, Real a11, Real a12, Real a22, + Real eval0, std::array& evec0) const; + + void ComputeEigenvector1(Real a00, Real a01, Real a02, Real a11, Real a12, Real a22, + std::array const& evec0, Real eval1, std::array& evec1) const; +}; + + +template +int SymmetricEigensolver3x3::operator()(Real a00, Real a01, Real a02, + Real a11, Real a12, Real a22, bool aggressive, int sortType, + std::array& eval, std::array, 3>& evec) const +{ + // Compute the Householder reflection H and B = H*A*H, where b02 = 0. + Real const zero = (Real)0, one = (Real)1, half = (Real)0.5; + bool isRotation = false; + Real c, s; + GetCosSin(a12, -a02, c, s); + Real Q[3][3] = { { c, s, zero }, { s, -c, zero }, { zero, zero, one } }; + Real term0 = c * a00 + s * a01; + Real term1 = c * a01 + s * a11; + Real b00 = c * term0 + s * term1; + Real b01 = s * term0 - c * term1; + term0 = s * a00 - c * a01; + term1 = s * a01 - c * a11; + Real b11 = s * term0 - c * term1; + Real b12 = s * a02 - c * a12; + Real b22 = a22; + + // Givens reflections, B' = G^T*B*G, preserve tridiagonal matrices. + int const maxIteration = 2 * (1 + std::numeric_limits::digits - + std::numeric_limits::min_exponent); + int iteration; + Real c2, s2; + + if (std::abs(b12) <= std::abs(b01)) + { + Real saveB00, saveB01, saveB11; + for (iteration = 0; iteration < maxIteration; ++iteration) + { + // Compute the Givens reflection. + GetCosSin(half * (b00 - b11), b01, c2, s2); + s = std::sqrt(half * (one - c2)); // >= 1/sqrt(2) + c = half * s2 / s; + + // Update Q by the Givens reflection. + Update0(Q, c, s); + isRotation = !isRotation; + + // Update B <- Q^T*B*Q, ensuring that b02 is zero and |b12| has + // strictly decreased. + saveB00 = b00; + saveB01 = b01; + saveB11 = b11; + term0 = c * saveB00 + s * saveB01; + term1 = c * saveB01 + s * saveB11; + b00 = c * term0 + s * term1; + b11 = b22; + term0 = c * saveB01 - s * saveB00; + term1 = c * saveB11 - s * saveB01; + b22 = c * term1 - s * term0; + b01 = s * b12; + b12 = c * b12; + + if (Converged(aggressive, b00, b11, b01)) + { + // Compute the Householder reflection. + GetCosSin(half * (b00 - b11), b01, c2, s2); + s = std::sqrt(half * (one - c2)); + c = half * s2 / s; // >= 1/sqrt(2) + + // Update Q by the Householder reflection. + Update2(Q, c, s); + isRotation = !isRotation; + + // Update D = Q^T*B*Q. + saveB00 = b00; + saveB01 = b01; + saveB11 = b11; + term0 = c * saveB00 + s * saveB01; + term1 = c * saveB01 + s * saveB11; + b00 = c * term0 + s * term1; + term0 = s * saveB00 - c * saveB01; + term1 = s * saveB01 - c * saveB11; + b11 = s * term0 - c * term1; + break; + } + } + } + else + { + Real saveB11, saveB12, saveB22; + for (iteration = 0; iteration < maxIteration; ++iteration) + { + // Compute the Givens reflection. + GetCosSin(half * (b22 - b11), b12, c2, s2); + s = std::sqrt(half * (one - c2)); // >= 1/sqrt(2) + c = half * s2 / s; + + // Update Q by the Givens reflection. + Update1(Q, c, s); + isRotation = !isRotation; + + // Update B <- Q^T*B*Q, ensuring that b02 is zero and |b12| has + // strictly decreased. MODIFY... + saveB11 = b11; + saveB12 = b12; + saveB22 = b22; + term0 = c * saveB22 + s * saveB12; + term1 = c * saveB12 + s * saveB11; + b22 = c * term0 + s * term1; + b11 = b00; + term0 = c * saveB12 - s * saveB22; + term1 = c * saveB11 - s * saveB12; + b00 = c * term1 - s * term0; + b12 = s * b01; + b01 = c * b01; + + if (Converged(aggressive, b11, b22, b12)) + { + // Compute the Householder reflection. + GetCosSin(half * (b11 - b22), b12, c2, s2); + s = std::sqrt(half * (one - c2)); + c = half * s2 / s; // >= 1/sqrt(2) + + // Update Q by the Householder reflection. + Update3(Q, c, s); + isRotation = !isRotation; + + // Update D = Q^T*B*Q. + saveB11 = b11; + saveB12 = b12; + saveB22 = b22; + term0 = c * saveB11 + s * saveB12; + term1 = c * saveB12 + s * saveB22; + b11 = c * term0 + s * term1; + term0 = s * saveB11 - c * saveB12; + term1 = s * saveB12 - c * saveB22; + b22 = s * term0 - c * term1; + break; + } + } + } + + std::array diagonal = { { b00, b11, b22 } }; + int i0, i1, i2; + if (sortType >= 1) + { + // diagonal[i0] <= diagonal[i1] <= diagonal[i2] + bool isOdd = Sort(diagonal, i0, i1, i2); + if (!isOdd) + { + isRotation = !isRotation; + } + } + else if (sortType <= -1) + { + // diagonal[i0] >= diagonal[i1] >= diagonal[i2] + bool isOdd = Sort(diagonal, i0, i1, i2); + std::swap(i0, i2); // (i0,i1,i2)->(i2,i1,i0) is odd + if (isOdd) + { + isRotation = !isRotation; + } + } + else + { + i0 = 0; + i1 = 1; + i2 = 2; + } + + eval[0] = diagonal[i0]; + eval[1] = diagonal[i1]; + eval[2] = diagonal[i2]; + evec[0][0] = Q[0][i0]; + evec[0][1] = Q[1][i0]; + evec[0][2] = Q[2][i0]; + evec[1][0] = Q[0][i1]; + evec[1][1] = Q[1][i1]; + evec[1][2] = Q[2][i1]; + evec[2][0] = Q[0][i2]; + evec[2][1] = Q[1][i2]; + evec[2][2] = Q[2][i2]; + + // Ensure the columns of Q form a right-handed set. + if (!isRotation) + { + for (int j = 0; j < 3; ++j) + { + evec[2][j] = -evec[2][j]; + } + } + + return iteration; +} + +template +void SymmetricEigensolver3x3::Update0(Real Q[3][3], Real c, Real s) +const +{ + for (int r = 0; r < 3; ++r) + { + Real tmp0 = c * Q[r][0] + s * Q[r][1]; + Real tmp1 = Q[r][2]; + Real tmp2 = c * Q[r][1] - s * Q[r][0]; + Q[r][0] = tmp0; + Q[r][1] = tmp1; + Q[r][2] = tmp2; + } +} + +template +void SymmetricEigensolver3x3::Update1(Real Q[3][3], Real c, Real s) +const +{ + for (int r = 0; r < 3; ++r) + { + Real tmp0 = c * Q[r][1] - s * Q[r][2]; + Real tmp1 = Q[r][0]; + Real tmp2 = c * Q[r][2] + s * Q[r][1]; + Q[r][0] = tmp0; + Q[r][1] = tmp1; + Q[r][2] = tmp2; + } +} + +template +void SymmetricEigensolver3x3::Update2(Real Q[3][3], Real c, Real s) +const +{ + for (int r = 0; r < 3; ++r) + { + Real tmp0 = c * Q[r][0] + s * Q[r][1]; + Real tmp1 = s * Q[r][0] - c * Q[r][1]; + Q[r][0] = tmp0; + Q[r][1] = tmp1; + } +} + +template +void SymmetricEigensolver3x3::Update3(Real Q[3][3], Real c, Real s) +const +{ + for (int r = 0; r < 3; ++r) + { + Real tmp0 = c * Q[r][1] + s * Q[r][2]; + Real tmp1 = s * Q[r][1] - c * Q[r][2]; + Q[r][1] = tmp0; + Q[r][2] = tmp1; + } +} + +template +void SymmetricEigensolver3x3::GetCosSin(Real u, Real v, Real& cs, + Real& sn) const +{ + Real maxAbsComp = std::max(std::abs(u), std::abs(v)); + if (maxAbsComp > (Real)0) + { + u /= maxAbsComp; // in [-1,1] + v /= maxAbsComp; // in [-1,1] + Real length = std::sqrt(u * u + v * v); + cs = u / length; + sn = v / length; + if (cs > (Real)0) + { + cs = -cs; + sn = -sn; + } + } + else + { + cs = (Real)-1; + sn = (Real)0; + } +} + +template +bool SymmetricEigensolver3x3::Converged(bool aggressive, Real bDiag0, + Real bDiag1, Real bSuper) const +{ + if (aggressive) + { + return bSuper == (Real)0; + } + else + { + Real sum = std::abs(bDiag0) + std::abs(bDiag1); + return sum + std::abs(bSuper) == sum; + } +} + +template +bool SymmetricEigensolver3x3::Sort(std::array const& d, + int& i0, int& i1, int& i2) const +{ + bool odd; + if (d[0] < d[1]) + { + if (d[2] < d[0]) + { + i0 = 2; i1 = 0; i2 = 1; odd = true; + } + else if (d[2] < d[1]) + { + i0 = 0; i1 = 2; i2 = 1; odd = false; + } + else + { + i0 = 0; i1 = 1; i2 = 2; odd = true; + } + } + else + { + if (d[2] < d[1]) + { + i0 = 2; i1 = 1; i2 = 0; odd = false; + } + else if (d[2] < d[0]) + { + i0 = 1; i1 = 2; i2 = 0; odd = true; + } + else + { + i0 = 1; i1 = 0; i2 = 2; odd = false; + } + } + return odd; +} + + +template +void NISymmetricEigensolver3x3::operator() (Real a00, Real a01, Real a02, + Real a11, Real a12, Real a22, std::array& eval, + std::array, 3>& evec) const +{ + // Precondition the matrix by factoring out the maximum absolute value + // of the components. This guards against floating-point overflow when + // computing the eigenvalues. + Real max0 = std::max(std::abs(a00), std::abs(a01)); + Real max1 = std::max(std::abs(a02), std::abs(a11)); + Real max2 = std::max(std::abs(a12), std::abs(a22)); + Real maxAbsElement = std::max(std::max(max0, max1), max2); + if (maxAbsElement == (Real)0) + { + // A is the zero matrix. + eval[0] = (Real)0; + eval[1] = (Real)0; + eval[2] = (Real)0; + evec[0] = { (Real)1, (Real)0, (Real)0 }; + evec[1] = { (Real)0, (Real)1, (Real)0 }; + evec[2] = { (Real)0, (Real)0, (Real)1 }; + return; + } + + Real invMaxAbsElement = (Real)1 / maxAbsElement; + a00 *= invMaxAbsElement; + a01 *= invMaxAbsElement; + a02 *= invMaxAbsElement; + a11 *= invMaxAbsElement; + a12 *= invMaxAbsElement; + a22 *= invMaxAbsElement; + + Real norm = a01 * a01 + a02 * a02 + a12 * a12; + if (norm > (Real)0) + { + // Compute the eigenvalues of A. + + // In the PDF mentioned previously, B = (A - q*I)/p, where q = tr(A)/3 + // with tr(A) the trace of A (sum of the diagonal entries of A) and where + // p = sqrt(tr((A - q*I)^2)/6). + Real q = (a00 + a11 + a22) / (Real)3; + + // The matrix A - q*I is represented by the following, where b00, b11 and + // b22 are computed after these comments, + // +- -+ + // | b00 a01 a02 | + // | a01 b11 a12 | + // | a02 a12 b22 | + // +- -+ + Real b00 = a00 - q; + Real b11 = a11 - q; + Real b22 = a22 - q; + + // The is the variable p mentioned in the PDF. + Real p = std::sqrt((b00 * b00 + b11 * b11 + b22 * b22 + norm * (Real)2) / (Real)6); + + // We need det(B) = det((A - q*I)/p) = det(A - q*I)/p^3. The value + // det(A - q*I) is computed using a cofactor expansion by the first + // row of A - q*I. The cofactors are c00, c01 and c02 and the + // determinant is b00*c00 - a01*c01 + a02*c02. The det(B) is then + // computed finally by the division with p^3. + Real c00 = b11 * b22 - a12 * a12; + Real c01 = a01 * b22 - a12 * a02; + Real c02 = a01 * a12 - b11 * a02; + Real det = (b00 * c00 - a01 * c01 + a02 * c02) / (p * p * p); + + // The halfDet value is cos(3*theta) mentioned in the PDF. The acos(z) + // function requires |z| <= 1, but will fail silently and return NaN + // if the input is larger than 1 in magnitude. To avoid this problem + // due to rounding errors, the halfDet/ value is clamped to [-1,1]. + Real halfDet = det * (Real)0.5; + halfDet = std::min(std::max(halfDet, (Real)-1), (Real)1); + + // The eigenvalues of B are ordered as beta0 <= beta1 <= beta2. The + // number of digits in twoThirdsPi is chosen so that, whether float or + // double, the floating-point number is the closest to theoretical 2*pi/3. + Real angle = std::acos(halfDet) / (Real)3; + Real const twoThirdsPi = (Real)2.09439510239319549; + Real beta2 = std::cos(angle) * (Real)2; + Real beta0 = std::cos(angle + twoThirdsPi) * (Real)2; + Real beta1 = -(beta0 + beta2); + + // The eigenvalues of A are ordered as alpha0 <= alpha1 <= alpha2. + eval[0] = q + p * beta0; + eval[1] = q + p * beta1; + eval[2] = q + p * beta2; + + // Compute the eigenvectors so that the set {evec[0], evec[1], evec[2]} + // is right handed and orthonormal. + if (halfDet >= (Real)0) + { + ComputeEigenvector0(a00, a01, a02, a11, a12, a22, eval[2], evec[2]); + ComputeEigenvector1(a00, a01, a02, a11, a12, a22, evec[2], eval[1], evec[1]); + evec[0] = Cross(evec[1], evec[2]); + } + else + { + ComputeEigenvector0(a00, a01, a02, a11, a12, a22, eval[0], evec[0]); + ComputeEigenvector1(a00, a01, a02, a11, a12, a22, evec[0], eval[1], evec[1]); + evec[2] = Cross(evec[0], evec[1]); + } + } + else + { + // The matrix is diagonal. + eval[0] = a00; + eval[1] = a11; + eval[2] = a22; + evec[0] = { (Real)1, (Real)0, (Real)0 }; + evec[1] = { (Real)0, (Real)1, (Real)0 }; + evec[2] = { (Real)0, (Real)0, (Real)1 }; + } + + // The preconditioning scaled the matrix A, which scales the eigenvalues. + // Revert the scaling. + eval[0] *= maxAbsElement; + eval[1] *= maxAbsElement; + eval[2] *= maxAbsElement; +} + +template +std::array NISymmetricEigensolver3x3::Multiply( + Real s, std::array const& U) +{ + std::array product = { s * U[0], s * U[1], s * U[2] }; + return product; +} + +template +std::array NISymmetricEigensolver3x3::Subtract( + std::array const& U, std::array const& V) +{ + std::array difference = { U[0] - V[0], U[1] - V[1], U[2] - V[2] }; + return difference; +} + +template +std::array NISymmetricEigensolver3x3::Divide( + std::array const& U, Real s) +{ + Real invS = (Real)1 / s; + std::array division = { U[0] * invS, U[1] * invS, U[2] * invS }; + return division; +} + +template +Real NISymmetricEigensolver3x3::Dot(std::array const& U, + std::array const& V) +{ + Real dot = U[0] * V[0] + U[1] * V[1] + U[2] * V[2]; + return dot; +} + +template +std::array NISymmetricEigensolver3x3::Cross(std::array const& U, + std::array const& V) +{ + std::array cross = + { + U[1] * V[2] - U[2] * V[1], + U[2] * V[0] - U[0] * V[2], + U[0] * V[1] - U[1] * V[0] + }; + return cross; +} + +template +void NISymmetricEigensolver3x3::ComputeOrthogonalComplement( + std::array const& W, std::array& U, std::array& V) const +{ + // Robustly compute a right-handed orthonormal set { U, V, W }. The + // vector W is guaranteed to be unit-length, in which case there is no + // need to worry about a division by zero when computing invLength. + Real invLength; + if (std::abs(W[0]) > std::abs(W[1])) + { + // The component of maximum absolute value is either W[0] or W[2]. + invLength = (Real)1 / std::sqrt(W[0] * W[0] + W[2] * W[2]); + U = { -W[2] * invLength, (Real)0, +W[0] * invLength }; + } + else + { + // The component of maximum absolute value is either W[1] or W[2]. + invLength = (Real)1 / std::sqrt(W[1] * W[1] + W[2] * W[2]); + U = { (Real)0, +W[2] * invLength, -W[1] * invLength }; + } + V = Cross(W, U); +} + +template +void NISymmetricEigensolver3x3::ComputeEigenvector0(Real a00, Real a01, + Real a02, Real a11, Real a12, Real a22, Real eval0, std::array& evec0) const +{ + // Compute a unit-length eigenvector for eigenvalue[i0]. The matrix is + // rank 2, so two of the rows are linearly independent. For a robust + // computation of the eigenvector, select the two rows whose cross product + // has largest length of all pairs of rows. + std::array row0 = { a00 - eval0, a01, a02 }; + std::array row1 = { a01, a11 - eval0, a12 }; + std::array row2 = { a02, a12, a22 - eval0 }; + std::array r0xr1 = Cross(row0, row1); + std::array r0xr2 = Cross(row0, row2); + std::array r1xr2 = Cross(row1, row2); + Real d0 = Dot(r0xr1, r0xr1); + Real d1 = Dot(r0xr2, r0xr2); + Real d2 = Dot(r1xr2, r1xr2); + + Real dmax = d0; + int imax = 0; + if (d1 > dmax) + { + dmax = d1; + imax = 1; + } + if (d2 > dmax) + { + imax = 2; + } + + if (imax == 0) + { + evec0 = Divide(r0xr1, std::sqrt(d0)); + } + else if (imax == 1) + { + evec0 = Divide(r0xr2, std::sqrt(d1)); + } + else + { + evec0 = Divide(r1xr2, std::sqrt(d2)); + } +} + +template +void NISymmetricEigensolver3x3::ComputeEigenvector1(Real a00, Real a01, + Real a02, Real a11, Real a12, Real a22, std::array const& evec0, + Real eval1, std::array& evec1) const +{ + // Robustly compute a right-handed orthonormal set { U, V, evec0 }. + std::array U, V; + ComputeOrthogonalComplement(evec0, U, V); + + // Let e be eval1 and let E be a corresponding eigenvector which is a + // solution to the linear system (A - e*I)*E = 0. The matrix (A - e*I) + // is 3x3, not invertible (so infinitely many solutions), and has rank 2 + // when eval1 and eval are different. It has rank 1 when eval1 and eval2 + // are equal. Numerically, it is difficult to compute robustly the rank + // of a matrix. Instead, the 3x3 linear system is reduced to a 2x2 system + // as follows. Define the 3x2 matrix J = [U V] whose columns are the U + // and V computed previously. Define the 2x1 vector X = J*E. The 2x2 + // system is 0 = M * X = (J^T * (A - e*I) * J) * X where J^T is the + // transpose of J and M = J^T * (A - e*I) * J is a 2x2 matrix. The system + // may be written as + // +- -++- -+ +- -+ + // | U^T*A*U - e U^T*A*V || x0 | = e * | x0 | + // | V^T*A*U V^T*A*V - e || x1 | | x1 | + // +- -++ -+ +- -+ + // where X has row entries x0 and x1. + + std::array AU = + { + a00 * U[0] + a01 * U[1] + a02 * U[2], + a01 * U[0] + a11 * U[1] + a12 * U[2], + a02 * U[0] + a12 * U[1] + a22 * U[2] + }; + + std::array AV = + { + a00 * V[0] + a01 * V[1] + a02 * V[2], + a01 * V[0] + a11 * V[1] + a12 * V[2], + a02 * V[0] + a12 * V[1] + a22 * V[2] + }; + + Real m00 = U[0] * AU[0] + U[1] * AU[1] + U[2] * AU[2] - eval1; + Real m01 = U[0] * AV[0] + U[1] * AV[1] + U[2] * AV[2]; + Real m11 = V[0] * AV[0] + V[1] * AV[1] + V[2] * AV[2] - eval1; + + // For robustness, choose the largest-length row of M to compute the + // eigenvector. The 2-tuple of coefficients of U and V in the + // assignments to eigenvector[1] lies on a circle, and U and V are + // unit length and perpendicular, so eigenvector[1] is unit length + // (within numerical tolerance). + Real absM00 = std::abs(m00); + Real absM01 = std::abs(m01); + Real absM11 = std::abs(m11); + Real maxAbsComp; + if (absM00 >= absM11) + { + maxAbsComp = std::max(absM00, absM01); + if (maxAbsComp > (Real)0) + { + if (absM00 >= absM01) + { + m01 /= m00; + m00 = (Real)1 / std::sqrt((Real)1 + m01 * m01); + m01 *= m00; + } + else + { + m00 /= m01; + m01 = (Real)1 / std::sqrt((Real)1 + m00 * m00); + m00 *= m01; + } + evec1 = Subtract(Multiply(m01, U), Multiply(m00, V)); + } + else + { + evec1 = U; + } + } + else + { + maxAbsComp = std::max(absM11, absM01); + if (maxAbsComp > (Real)0) + { + if (absM11 >= absM01) + { + m01 /= m11; + m11 = (Real)1 / std::sqrt((Real)1 + m01 * m01); + m01 *= m11; + } + else + { + m11 /= m01; + m01 = (Real)1 / std::sqrt((Real)1 + m11 * m11); + m11 *= m01; + } + evec1 = Subtract(Multiply(m11, U), Multiply(m01, V)); + } + else + { + evec1 = U; + } + } +} + +} + + +// ---------------------------------------------- +// TODO: GteApprOrthogonalLine3.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// Least-squares fit of a line to (x,y,z) data by using distance measurements +// orthogonal to the proposed line. The return value is 'true' iff the fit +// is unique (always successful, 'true' when a minimum eigenvalue is unique). +// The mParameters value is a line with (P,D) = (origin,direction). The +// error for S = (x0,y0,z0) is (S-P)^T*(I - D*D^T)*(S-P). + +namespace houdini::gte +{ + +template +class ApprOrthogonalLine3 + : + public ApprQuery, Vector3> +{ +public: + // Initialize the model parameters to zero. + ApprOrthogonalLine3(); + + // Basic fitting algorithm. + bool Fit(int numPoints, Vector3 const* points); + Line3 const& GetParameters() const; + + // Functions called by ApprQuery::RANSAC. See GteApprQuery.h for a + // detailed description. + int GetMinimumRequired() const; + Real Error(Vector3 const& observation) const; + bool Fit(std::vector> const& observations, + std::vector const& indices); + +private: + Line3 mParameters; +}; + + +template +ApprOrthogonalLine3::ApprOrthogonalLine3() + : + mParameters(Vector3::Zero(), Vector3::Zero()) +{ +} + +template +bool ApprOrthogonalLine3::Fit(int numPoints, Vector3 const* points) +{ + if (numPoints >= GetMinimumRequired() && points) + { + // Compute the mean of the points. + Vector3 mean = Vector3::Zero(); + for (int i = 0; i < numPoints; ++i) + { + mean += points[i]; + } + Real invSize = ((Real)1) / (Real)numPoints; + mean *= invSize; + + // Compute the covariance matrix of the points. + Real covar00 = (Real)0, covar01 = (Real)0, covar02 = (Real)0; + Real covar11 = (Real)0, covar12 = (Real)0, covar22 = (Real)0; + for (int i = 0; i < numPoints; ++i) + { + Vector3 diff = points[i] - mean; + covar00 += diff[0] * diff[0]; + covar01 += diff[0] * diff[1]; + covar02 += diff[0] * diff[2]; + covar11 += diff[1] * diff[1]; + covar12 += diff[1] * diff[2]; + covar22 += diff[2] * diff[2]; + } + covar00 *= invSize; + covar01 *= invSize; + covar02 *= invSize; + covar11 *= invSize; + covar12 *= invSize; + covar22 *= invSize; + + // Solve the eigensystem. + SymmetricEigensolver3x3 es; + std::array eval; + std::array, 3> evec; + es(covar00, covar01, covar02, covar11, covar12, covar22, false, +1, + eval, evec); + + // The line direction is the eigenvector in the direction of largest + // variance of the points. + mParameters.origin = mean; + mParameters.direction = evec[2]; + + // The fitted line is unique when the maximum eigenvalue has + // multiplicity 1. + return eval[1]< eval[2]; + } + + mParameters = Line3(Vector3::Zero(), Vector3::Zero()); + return false; +} + +template +Line3 const& ApprOrthogonalLine3::GetParameters() const +{ + return mParameters; +} + +template +int ApprOrthogonalLine3::GetMinimumRequired() const +{ + return 2; +} + +template +Real ApprOrthogonalLine3::Error(Vector3 const& observation) const +{ + Vector3 diff = observation - mParameters.origin; + Real sqrlen = Dot(diff, diff); + Real dot = Dot(diff, mParameters.direction); + Real error = std::abs(sqrlen - dot*dot); + return error; +} + +template +bool ApprOrthogonalLine3::Fit( + std::vector> const& observations, + std::vector const& indices) +{ + if (static_cast(indices.size()) >= GetMinimumRequired()) + { + // Compute the mean of the points. + Vector3 mean = Vector3::Zero(); + for (auto index : indices) + { + mean += observations[index]; + } + Real invSize = ((Real)1) / (Real)indices.size(); + mean *= invSize; + + // Compute the covariance matrix of the points. + Real covar00 = (Real)0, covar01 = (Real)0, covar02 = (Real)0; + Real covar11 = (Real)0, covar12 = (Real)0, covar22 = (Real)0; + for (auto index : indices) + { + Vector3 diff = observations[index] - mean; + covar00 += diff[0] * diff[0]; + covar01 += diff[0] * diff[1]; + covar02 += diff[0] * diff[2]; + covar11 += diff[1] * diff[1]; + covar12 += diff[1] * diff[2]; + covar22 += diff[2] * diff[2]; + } + covar00 *= invSize; + covar01 *= invSize; + covar02 *= invSize; + covar11 *= invSize; + covar12 *= invSize; + covar22 *= invSize; + + // Solve the eigensystem. + SymmetricEigensolver3x3 es; + std::array eval; + std::array, 3> evec; + es(covar00, covar01, covar02, covar11, covar12, covar22, false, +1, + eval, evec); + + // The line direction is the eigenvector in the direction of largest + // variance of the points. + mParameters.origin = mean; + mParameters.direction = evec[2]; + + // The fitted line is unique when the maximum eigenvalue has + // multiplicity 1. + return eval[1]< eval[2]; + } + + mParameters = Line3(Vector3::Zero(), Vector3::Zero()); + return false; +} + + +} + + + +// ---------------------------------------------- +// GteDistPointLine.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.1 (2018/10/05) + +namespace houdini::gte +{ + +template +class DCPQuery, Line> +{ +public: + struct Result + { + Real distance, sqrDistance; + Real lineParameter; // t in (-infinity,+infinity) + Vector lineClosest; // origin + t * direction + }; + + Result operator()(Vector const& point, + Line const& line); +}; + +// Template aliases for convenience. +template +using DCPPointLine = +DCPQuery, Line>; + +template +using DCPPoint2Line2 = DCPPointLine<2, Real>; + +template +using DCPPoint3Line3 = DCPPointLine<3, Real>; + + +template +typename DCPQuery, Line>::Result +DCPQuery, Line>::operator()( + Vector const& point, Line const& line) +{ + Result result; + + Vector diff = point - line.origin; + result.lineParameter = Dot(line.direction, diff); + result.lineClosest = line.origin + result.lineParameter*line.direction; + + diff = point - result.lineClosest; + result.sqrDistance = Dot(diff, diff); + result.distance = std::sqrt(result.sqrDistance); + + return result; +} + + +} + + +// ---------------------------------------------- +// GteSegment.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +// The segment is represented by (1-t)*P0 + t*P1, where P0 and P1 are the +// endpoints of the segment and 0 <= t <= 1. Some algorithms prefer a +// centered representation that is similar to how oriented bounding boxes are +// defined. This representation is C + s*D, where C = (P0 + P1)/2 is the +// center of the segment, D = (P1 - P0)/|P1 - P0| is a unit-length direction +// vector for the segment, and |t| <= e. The value e = |P1 - P0|/2 is the +// extent (or radius or half-length) of the segment. + +namespace houdini::gte +{ + +template +class Segment +{ +public: + // Construction and destruction. The default constructor sets p0 to + // (-1,0,...,0) and p1 to (1,0,...,0). NOTE: If you set p0 and p1; + // compute C, D, and e; and then recompute q0 = C-e*D and q1 = C+e*D, + // numerical round-off errors can lead to q0 not exactly equal to p0 + // and q1 not exactly equal to p1. + Segment(); + Segment(Vector const& p0, Vector const& p1); + Segment(std::array, 2> const& inP); + Segment(Vector const& center, Vector const& direction, + Real extent); + + // Manipulation via the centered form. + void SetCenteredForm(Vector const& center, + Vector const& direction, Real extent); + + void GetCenteredForm(Vector& center, Vector& direction, + Real& extent) const; + + // Public member access. + std::array, 2> p; + +public: + // Comparisons to support sorted containers. + bool operator==(Segment const& segment) const; + bool operator!=(Segment const& segment) const; + bool operator< (Segment const& segment) const; + bool operator<=(Segment const& segment) const; + bool operator> (Segment const& segment) const; + bool operator>=(Segment const& segment) const; +}; + +// Template aliases for convenience. +template +using Segment2 = Segment<2, Real>; + +template +using Segment3 = Segment<3, Real>; + + +template +Segment::Segment() +{ + p[1].MakeUnit(0); + p[0] = -p[1]; +} + +template +Segment::Segment(Vector const& p0, + Vector const& p1) +{ + p[0] = p0; + p[1] = p1; +} + +template +Segment::Segment(std::array, 2> const& inP) +{ + p = inP; +} + +template +Segment::Segment(Vector const& center, + Vector const& direction, Real extent) +{ + SetCenteredForm(center, direction, extent); +} + +template +void Segment::SetCenteredForm(Vector const& center, + Vector const& direction, Real extent) +{ + p[0] = center - extent * direction; + p[1] = center + extent * direction; +} + +template +void Segment::GetCenteredForm(Vector& center, + Vector& direction, Real& extent) const +{ + center = ((Real)0.5)*(p[0] + p[1]); + direction = p[1] - p[0]; + extent = ((Real)0.5)*Normalize(direction); +} + +template +bool Segment::operator==(Segment const& segment) const +{ + return p == segment.p; +} + +template +bool Segment::operator!=(Segment const& segment) const +{ + return !operator==(segment); +} + +template +bool Segment::operator<(Segment const& segment) const +{ + return p < segment.p; +} + +template +bool Segment::operator<=(Segment const& segment) const +{ + return operator<(segment) || operator==(segment); +} + +template +bool Segment::operator>(Segment const& segment) const +{ + return !operator<=(segment); +} + +template +bool Segment::operator>=(Segment const& segment) const +{ + return !operator<(segment); +} + + +} + + +// ---------------------------------------------- +// GteDistPointSegment.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.1 (2018/10/05) + +namespace houdini::gte +{ + +template +class DCPQuery, Segment> +{ +public: + struct Result + { + Real distance, sqrDistance; + Real segmentParameter; // t in [0,1] + Vector segmentClosest; // (1-t)*p[0] + t*p[1] + }; + + Result operator()(Vector const& point, + Segment const& segment); +}; + +// Template aliases for convenience. +template +using DCPPointSegment = +DCPQuery, Segment>; + +template +using DCPPoint2Segment2 = DCPPointSegment<2, Real>; + +template +using DCPPoint3Segment3 = DCPPointSegment<3, Real>; + + +template +typename DCPQuery, Segment>::Result +DCPQuery, Segment>::operator()( + Vector const& point, Segment const& segment) +{ + Result result; + + // The direction vector is not unit length. The normalization is deferred + // until it is needed. + Vector direction = segment.p[1] - segment.p[0]; + Vector diff = point - segment.p[1]; + Real t = Dot(direction, diff); + if (t >= (Real)0) + { + result.segmentParameter = (Real)1; + result.segmentClosest = segment.p[1]; + } + else + { + diff = point - segment.p[0]; + t = Dot(direction, diff); + if (t <= (Real)0) + { + result.segmentParameter = (Real)0; + result.segmentClosest = segment.p[0]; + } + else + { + Real sqrLength = Dot(direction, direction); + if (sqrLength > (Real)0) + { + t /= sqrLength; + result.segmentParameter = t; + result.segmentClosest = segment.p[0] + t * direction; + } + else + { + result.segmentParameter = (Real)0; + result.segmentClosest = segment.p[0]; + } + } + } + + diff = point - result.segmentClosest; + result.sqrDistance = Dot(diff, diff); + result.distance = std::sqrt(result.sqrDistance); + + return result; +} + + +} + + +// ---------------------------------------------- +// GteCapsule.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.0 (2016/06/19) + +namespace houdini::gte +{ + +// A capsule is the set of points that are equidistant from a segment, the +// common distance called the radius. + +template +class Capsule +{ +public: + // Construction and destruction. The default constructor sets the segment + // to have endpoints p0 = (-1,0,...,0) and p1 = (1,0,...,0), and the + // radius is 1. + Capsule(); + Capsule(Segment const& inSegment, Real inRadius); + + // Public member access. + Segment segment; + Real radius; + +public: + // Comparisons to support sorted containers. + bool operator==(Capsule const& capsule) const; + bool operator!=(Capsule const& capsule) const; + bool operator< (Capsule const& capsule) const; + bool operator<=(Capsule const& capsule) const; + bool operator> (Capsule const& capsule) const; + bool operator>=(Capsule const& capsule) const; +}; + +// Template alias for convenience. +template +using Capsule3 = Capsule<3, Real>; + + +template +Capsule::Capsule() + : + radius((Real)1) +{ +} + +template +Capsule::Capsule(Segment const& inSegment, Real inRadius) + : + segment(inSegment), + radius(inRadius) +{ +} + +template +bool Capsule::operator==(Capsule const& capsule) const +{ + return segment == capsule.segment && radius == capsule.radius; +} + +template +bool Capsule::operator!=(Capsule const& capsule) const +{ + return !operator==(capsule); +} + +template +bool Capsule::operator<(Capsule const& capsule) const +{ + if (segment < capsule.segment) + { + return true; + } + + if (segment > capsule.segment) + { + return false; + } + + return radius < capsule.radius; +} + +template +bool Capsule::operator<=(Capsule const& capsule) const +{ + return operator<(capsule) || operator==(capsule); +} + +template +bool Capsule::operator>(Capsule const& capsule) const +{ + return !operator<=(capsule); +} + +template +bool Capsule::operator>=(Capsule const& capsule) const +{ + return !operator<(capsule); +} + + +} + + +// ---------------------------------------------- +// GteContCapsule3.h +// ---------------------------------------------- + +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2019 +// Distributed under the Boost Software License, Version 1.0. +// http://www.boost.org/LICENSE_1_0.txt +// http://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// File Version: 3.0.1 (2018/10/05) + +namespace houdini::gte +{ + +// Compute the axis of the capsule segment using least-squares fitting. The +// radius is the maximum distance from the points to the axis. Hemispherical +// caps are chosen as close together as possible. +template +bool GetContainer(int numPoints, Vector3 const* points, + Capsule3& capsule); + +// Test for containment of a point by a capsule. +template +bool InContainer(Vector3 const& point, Capsule3 const& capsule); + +// Test for containment of a sphere by a capsule. +template +bool InContainer(Sphere3 const& sphere, Capsule3 const& capsule); + +// Test for containment of a capsule by a capsule. +template +bool InContainer(Capsule3 const& testCapsule, + Capsule3 const& capsule); + +// Compute a capsule that contains the input capsules. The returned capsule +// is not necessarily the one of smallest volume that contains the inputs. +template +bool MergeContainers(Capsule3 const& capsule0, + Capsule3 const& capsule1, Capsule3& merge); + + +template +bool GetContainer(int numPoints, Vector3 const* points, + Capsule3& capsule) +{ + ApprOrthogonalLine3 fitter; + fitter.Fit(numPoints, points); + Line3 line = fitter.GetParameters(); + + DCPQuery, Line3> plQuery; + Real maxRadiusSqr = (Real)0; + for (int i = 0; i < numPoints; ++i) + { + auto result = plQuery(points[i], line); + if (result.sqrDistance > maxRadiusSqr) + { + maxRadiusSqr = result.sqrDistance; + } + } + + Vector3 basis[3]; + basis[0] = line.direction; + ComputeOrthogonalComplement(1, basis); + + Real minValue = std::numeric_limits::max(); + Real maxValue = -std::numeric_limits::max(); + for (int i = 0; i < numPoints; ++i) + { + Vector3 diff = points[i] - line.origin; + Real uDotDiff = Dot(diff, basis[1]); + Real vDotDiff = Dot(diff, basis[2]); + Real wDotDiff = Dot(diff, basis[0]); + Real discr = maxRadiusSqr - (uDotDiff*uDotDiff + vDotDiff*vDotDiff); + Real radical = std::sqrt(std::max(discr, (Real)0)); + + Real test = wDotDiff + radical; + if (test < minValue) + { + minValue = test; + } + + test = wDotDiff - radical; + if (test > maxValue) + { + maxValue = test; + } + } + + Vector3 center = line.origin + + (((Real)0.5)*(minValue + maxValue))*line.direction; + + Real extent; + if (maxValue > minValue) + { + // Container is a capsule. + extent = ((Real)0.5)*(maxValue - minValue); + } + else + { + // Container is a sphere. + extent = (Real)0; + } + + capsule.segment = Segment3(center, line.direction, extent); + capsule.radius = std::sqrt(maxRadiusSqr); + return true; +} + +template +bool InContainer(Vector3 const& point, Capsule3 const& capsule) +{ + DCPQuery, Segment3> psQuery; + auto result = psQuery(point, capsule.segment); + return result.distance <= capsule.radius; +} + +template +bool InContainer(Sphere3 const& sphere, Capsule3 const& capsule) +{ + Real rDiff = capsule.radius - sphere.radius; + if (rDiff >= (Real)0) + { + DCPQuery, Segment3> psQuery; + auto result = psQuery(sphere.center, capsule.segment); + return result.distance <= rDiff; + } + return false; +} + +template +bool InContainer(Capsule3 const& testCapsule, + Capsule3 const& capsule) +{ + Sphere3 spherePosEnd(testCapsule.segment.p[1], testCapsule.radius); + Sphere3 sphereNegEnd(testCapsule.segment.p[0], testCapsule.radius); + return InContainer(spherePosEnd, capsule) + && InContainer(sphereNegEnd, capsule); +} + +template +bool MergeContainers(Capsule3 const& capsule0, + Capsule3 const& capsule1, Capsule3& merge) +{ + if (InContainer(capsule0, capsule1)) + { + merge = capsule1; + return true; + } + + if (InContainer(capsule1, capsule0)) + { + merge = capsule0; + return true; + } + + Vector3 P0, P1, D0, D1; + Real extent0, extent1; + capsule0.segment.GetCenteredForm(P0, D0, extent0); + capsule1.segment.GetCenteredForm(P1, D1, extent1); + + // Axis of final capsule. + Line3 line; + + // Axis center is average of input axis centers. + line.origin = ((Real)0.5)*(P0 + P1); + + // Axis unit direction is average of input axis unit directions. + if (Dot(D0, D1) >= (Real)0) + { + line.direction = D0 + D1; + } + else + { + line.direction = D0 - D1; + } + Normalize(line.direction); + + // Cylinder with axis 'line' must contain the spheres centered at the + // endpoints of the input capsules. + DCPQuery, Line3> plQuery; + Vector3 posEnd0 = capsule0.segment.p[1]; + Real radius = plQuery(posEnd0, line).distance + capsule0.radius; + + Vector3 negEnd0 = capsule0.segment.p[0]; + Real tmp = plQuery(negEnd0, line).distance + capsule0.radius; + + Vector3 posEnd1 = capsule1.segment.p[1]; + tmp = plQuery(posEnd1, line).distance + capsule1.radius; + if (tmp > radius) + { + radius = tmp; + } + + Vector3 negEnd1 = capsule1.segment.p[0]; + tmp = plQuery(negEnd1, line).distance + capsule1.radius; + if (tmp > radius) + { + radius = tmp; + } + + // Process sphere . + Real rDiff = radius - capsule0.radius; + Real rDiffSqr = rDiff*rDiff; + Vector3 diff = line.origin - posEnd0; + Real k0 = Dot(diff, diff) - rDiffSqr; + Real k1 = Dot(diff, line.direction); + Real discr = k1*k1 - k0; // assert: k1*k1-k0 >= 0, guard against anyway + Real root = std::sqrt(std::max(discr, (Real)0)); + Real tPos = -k1 - root; + Real tNeg = -k1 + root; + + // Process sphere . + diff = line.origin - negEnd0; + k0 = Dot(diff, diff) - rDiffSqr; + k1 = Dot(diff, line.direction); + discr = k1*k1 - k0; // assert: k1*k1-k0 >= 0, guard against anyway + root = std::sqrt(std::max(discr, (Real)0)); + tmp = -k1 - root; + if (tmp > tPos) + { + tPos = tmp; + } + tmp = -k1 + root; + if (tmp < tNeg) + { + tNeg = tmp; + } + + // Process sphere . + rDiff = radius - capsule1.radius; + rDiffSqr = rDiff*rDiff; + diff = line.origin - posEnd1; + k0 = Dot(diff, diff) - rDiffSqr; + k1 = Dot(diff, line.direction); + discr = k1*k1 - k0; // assert: k1*k1-k0 >= 0, guard against anyway + root = std::sqrt(std::max(discr, (Real)0)); + tmp = -k1 - root; + if (tmp > tPos) + { + tPos = tmp; + } + tmp = -k1 + root; + if (tmp < tNeg) + { + tNeg = tmp; + } + + // Process sphere . + diff = line.origin - negEnd1; + k0 = Dot(diff, diff) - rDiffSqr; + k1 = Dot(diff, line.direction); + discr = k1*k1 - k0; // assert: k1*k1-k0 >= 0, guard against anyway + root = std::sqrt(std::max(discr, (Real)0)); + tmp = -k1 - root; + if (tmp > tPos) + { + tPos = tmp; + } + tmp = -k1 + root; + if (tmp < tNeg) + { + tNeg = tmp; + } + + Vector3 center = line.origin + + ((Real)0.5)*(tPos + tNeg)*line.direction; + + Real extent; + if (tPos > tNeg) + { + // Container is a capsule. + extent = ((Real)0.5)*(tPos - tNeg); + } + else + { + // Container is a sphere. + extent = (Real)0; + } + + merge.segment = Segment3(center, line.direction, extent); + merge.radius = radius; + return true; +} + + +} + + + +// ---------------------------------------------- +// GteUtil.h +// ---------------------------------------------- + +template +houdini::gte::Vector3 Convert(const FVector& Vec) +{ + return houdini::gte::Vector3({ Vec.X, Vec.Y, Vec.Z }); +} + +template +FVector Convert(const houdini::gte::Vector3& Vec) +{ + return FVector(Vec[0], Vec[1], Vec[2]); +} + +``` + +`Source/HoudiniEngine/Private/HBSPOps.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HBSPOps.h" +#include "EngineDefines.h" +#include "Model.h" +#include "Materials/Material.h" +#include "Engine/BrushBuilder.h" +#include "Editor/EditorEngine.h" +#include "Components/BrushComponent.h" +#include "GameFramework/Volume.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "MaterialDomain.h" +#endif + +DEFINE_LOG_CATEGORY_STATIC(LogBSPOps, Log, All); + +/** Errors encountered in Csg operation. */ +int32 FHBSPOps::GErrors = 0; +bool FHBSPOps::GFastRebuild = false; + +static void TagReferencedNodes( UModel *Model, int32 *NodeRef, int32 *PolyRef, int32 iNode ) +{ + FBspNode &Node = Model->Nodes[iNode]; + + NodeRef[iNode ] = 0; + PolyRef[Node.iSurf] = 0; + + if( Node.iFront != INDEX_NONE ) TagReferencedNodes(Model,NodeRef,PolyRef,Node.iFront); + if( Node.iBack != INDEX_NONE ) TagReferencedNodes(Model,NodeRef,PolyRef,Node.iBack ); + if( Node.iPlane != INDEX_NONE ) TagReferencedNodes(Model,NodeRef,PolyRef,Node.iPlane); +} + +// +// Update a bounding volume by expanding it to enclose a list of polys. +// +static void UpdateBoundWithPolys( FBox& Bound, FPoly** PolyList, int32 nPolys ) +{ + for( int32 i=0; iVertices.Num(); j++ ) + Bound += (FVector)PolyList[i]->Vertices[j]; +} + +// +// Update a convolution hull with a list of polys. +// +static void UpdateConvolutionWithPolys( UModel *Model, int32 iNode, FPoly **PolyList, int32 nPolys ) +{ + FBox Box(ForceInit); + + FBspNode &Node = Model->Nodes[iNode]; + Node.iCollisionBound = Model->LeafHulls.Num(); + for( int32 i=0; iiBrushPoly != INDEX_NONE ) + { + int32 j; + for( j=0; jiBrushPoly == PolyList[i]->iBrushPoly ) + break; + if( j >= i ) + Model->LeafHulls.Add(PolyList[i]->iBrushPoly); + } + for( int32 j=0; jVertices.Num(); j++ ) + Box += (FVector)PolyList[i]->Vertices[j]; + } + Model->LeafHulls.Add(INDEX_NONE); + + // Add bounds. + Model->LeafHulls.Add( *(int32*)&Box.Min.X ); + Model->LeafHulls.Add( *(int32*)&Box.Min.Y ); + Model->LeafHulls.Add( *(int32*)&Box.Min.Z ); + Model->LeafHulls.Add( *(int32*)&Box.Max.X ); + Model->LeafHulls.Add( *(int32*)&Box.Max.Y ); + Model->LeafHulls.Add( *(int32*)&Box.Max.Z ); + +} + +// +// Cut a partitioning poly by a list of polys, and add the resulting inside pieces to the +// front list and back list. +// +static void SplitPartitioner +( + UModel* Model, + FPoly** PolyList, + FPoly** FrontList, + FPoly** BackList, + int32 n, + int32 nPolys, + int32& nFront, + int32& nBack, + FPoly InfiniteEdPoly, + TArray& AllocatedFPolys +) +{ + FPoly FrontPoly,BackPoly; + while( n < nPolys ) + { + FPoly* Poly = PolyList[n]; + switch( InfiniteEdPoly.SplitWithPlane(Poly->Vertices[0],Poly->Normal,&FrontPoly,&BackPoly,0) ) + { + case SP_Coplanar: + // May occasionally happen. +// UE_LOG(LogBSPOps, Log, TEXT("FilterBound: Got inficoplanar") ); + break; + + case SP_Front: + // Shouldn't happen if hull is correct. +// UE_LOG(LogBSPOps, Log, TEXT("FilterBound: Got infifront") ); + return; + + case SP_Split: + InfiniteEdPoly = BackPoly; + break; + + case SP_Back: + break; + } + n++; + } + + FPoly* New = new FPoly; + *New = InfiniteEdPoly; + New->Reverse(); + New->iBrushPoly |= 0x40000000; + FrontList[nFront++] = New; + AllocatedFPolys.Add( New ); + + New = new FPoly; + *New = InfiniteEdPoly; + BackList[nBack++] = New; + AllocatedFPolys.Add( New ); +} + +// +// Build an FPoly representing an "infinite" plane (which exceeds the maximum +// dimensions of the world in all directions) for a particular Bsp node. +// +FPoly FHBSPOps::BuildInfiniteFPoly( UModel* Model, int32 iNode ) +{ + FBspNode& Node = Model->Nodes[iNode]; + FBspSurf& Poly = Model->Surfs[Node.iSurf]; + FVector Base = FVector(Poly.Plane * Poly.Plane.W); + FVector Normal = (FVector)Poly.Plane; + FVector Axis1, Axis2; + + // Find two non-problematic axis vectors. + Normal.FindBestAxisVectors( Axis1, Axis2 ); + + // Set up the FPoly. + FPoly EdPoly; + EdPoly.Init(); + EdPoly.Normal = (FVector3f)Normal; + EdPoly.Base = (FVector3f)Base; + new(EdPoly.Vertices) FVector3f(Base + Axis1*WORLD_MAX + Axis2*WORLD_MAX); + new(EdPoly.Vertices) FVector3f(Base - Axis1*WORLD_MAX + Axis2*WORLD_MAX); + new(EdPoly.Vertices) FVector3f(Base - Axis1*WORLD_MAX - Axis2*WORLD_MAX); + new(EdPoly.Vertices) FVector3f(Base + Axis1*WORLD_MAX - Axis2*WORLD_MAX); + + return EdPoly; +} + +// +// Recursively filter a set of polys defining a convex hull down the Bsp, +// splitting it into two halves at each node and adding in the appropriate +// face polys at splits. +// +static void FilterBound +( + UModel* Model, + FBox* ParentBound, + int32 iNode, + FPoly** PolyList, + int32 nPolys, + int32 Outside +) +{ + FMemMark Mark(FMemStack::Get()); + FBspNode& Node = Model->Nodes [iNode]; + FBspSurf& Surf = Model->Surfs [Node.iSurf]; + FVector3f Base = Surf.Plane * Surf.Plane.W; + FVector3f& Normal = Model->Vectors[Surf.vNormal]; + FBox Bound(ForceInit); + + Bound.Min.X = Bound.Min.Y = Bound.Min.Z = +WORLD_MAX; + Bound.Max.X = Bound.Max.Y = Bound.Max.Z = -WORLD_MAX; + + // Split bound into front half and back half. + FPoly** FrontList = new(FMemStack::Get(),nPolys*2+16)FPoly*; int32 nFront=0; + FPoly** BackList = new(FMemStack::Get(),nPolys*2+16)FPoly*; int32 nBack=0; + + // Keeping track of allocated FPoly structures to delete later on. + TArray AllocatedFPolys; + + FPoly* FrontPoly = new FPoly; + FPoly* BackPoly = new FPoly; + + // Keep track of allocations. + AllocatedFPolys.Add( FrontPoly ); + AllocatedFPolys.Add( BackPoly ); + + for( int32 i=0; iSplitWithPlane( Base, Normal, FrontPoly, BackPoly, 0 ) ) + { + case SP_Coplanar: +// UE_LOG(LogBSPOps, Log, TEXT("FilterBound: Got coplanar") ); + FrontList[nFront++] = Poly; + BackList[nBack++] = Poly; + break; + + case SP_Front: + FrontList[nFront++] = Poly; + break; + + case SP_Back: + BackList[nBack++] = Poly; + break; + + case SP_Split: + FrontList[nFront++] = FrontPoly; + BackList [nBack++] = BackPoly; + + FrontPoly = new FPoly; + BackPoly = new FPoly; + + // Keep track of allocations. + AllocatedFPolys.Add( FrontPoly ); + AllocatedFPolys.Add( BackPoly ); + + break; + + default: + UE_LOG(LogBSPOps, Fatal, TEXT("FZoneFilter::FilterToLeaf: Unknown split code") ); + } + } + if( nFront && nBack ) + { + // Add partitioner plane to front and back. + FPoly InfiniteEdPoly = FHBSPOps::BuildInfiniteFPoly( Model, iNode ); + InfiniteEdPoly.iBrushPoly = iNode; + + SplitPartitioner(Model,PolyList,FrontList,BackList,0,nPolys,nFront,nBack,InfiniteEdPoly,AllocatedFPolys); + } + else + { +// if( !nFront ) UE_LOG(LogBSPOps, Log, TEXT("FilterBound: Empty fronthull") ); +// if( !nBack ) UE_LOG(LogBSPOps, Log, TEXT("FilterBound: Empty backhull") ); + } + + // Recursively update all our childrens' bounding volumes. + if( nFront > 0 ) + { + if( Node.iFront != INDEX_NONE ) + FilterBound( Model, &Bound, Node.iFront, FrontList, nFront, Outside || Node.IsCsg() ); + else if( Outside || Node.IsCsg() ) + UpdateBoundWithPolys( Bound, FrontList, nFront ); + else + UpdateConvolutionWithPolys( Model, iNode, FrontList, nFront ); + } + if( nBack > 0 ) + { + if( Node.iBack != INDEX_NONE) + FilterBound( Model, &Bound,Node.iBack, BackList, nBack, Outside && !Node.IsCsg() ); + else if( Outside && !Node.IsCsg() ) + UpdateBoundWithPolys( Bound, BackList, nBack ); + else + UpdateConvolutionWithPolys( Model, iNode, BackList, nBack ); + } + + // Update parent bound to enclose this bound. + if( ParentBound ) + *ParentBound += Bound; + + // Delete FPolys allocated above. We cannot use FMemStack::Get() for FPoly as the array data FPoly contains will be allocated in regular memory. + for( int32 i=0; i0); + + // No need to test if only one poly. + if( NumPolys==1 ) + return PolyList[0]; + + FPoly *Poly, *Best=NULL; + float Score, BestScore; + int32 i, Index, j, Inc; + int32 Splits, Front, Back, Coplanar, AllSemiSolids; + + //PortalBias -- added by Legend on 4/12/2000 + float PortalBias = InPortalBias / 100.0f; + Balance &= 0xFF; // keep only the low byte to recover "Balance" + //UE_LOG(LogBSPOps, Log, TEXT("Balance=%d PortalBias=%f"), Balance, PortalBias ); + + if (Opt==FHBSPOps::BSP_Optimal) Inc = 1; // Test lots of nodes. + else if (Opt==FHBSPOps::BSP_Good) Inc = FMath::Max(1,NumPolys/20); // Test 20 nodes. + else /* BSP_Lame */ Inc = FMath::Max(1,NumPolys/4); // Test 4 nodes. + + // See if there are any non-semisolid polygons here. + for( i=0; iPolyFlags & PF_AddLast) ) + break; + AllSemiSolids = (i>=NumPolys); + + // Search through all polygons in the pool and find: + // A. The number of splits each poly would make. + // B. The number of front and back nodes the polygon would create. + // C. Number of coplanars. + BestScore = 0; + for( i=0; iPolyFlags & PF_AddLast) && !(Poly->PolyFlags & PF_Portal) ) + && !AllSemiSolids ); + if( Index>=i+Inc || Index>=NumPolys ) + continue; + + for( j=0; jSplitWithPlaneFast( FPlane((FVector)Poly->Vertices[0], (FVector)Poly->Normal), NULL, NULL ) ) + { + case SP_Coplanar: + Coplanar++; + break; + + case SP_Front: + Front++; + break; + + case SP_Back: + Back++; + break; + + case SP_Split: + // Disfavor splitting polys that are zone portals. + if( !(OtherPoly->PolyFlags & PF_Portal) ) + Splits++; + else + Splits += 16; + break; + } + } + // added by Legend 1/31/1999 + // Score optimization: minimize cuts vs. balance tree (as specified in BSP Rebuilder dialog) + Score = ( 100.0 - float(Balance) ) * Splits + float(Balance) * FMath::Abs( Front - Back ); + if( Poly->PolyFlags & PF_Portal ) + { + // PortalBias -- added by Legend on 4/12/2000 + // + // PortalBias enables level designers to control the effect of Portals on the BSP. + // This effect can range from 0.0 (ignore portals), to 1.0 (portals cut everything). + // + // In builds prior to this (since the 221 build dating back to 1/31/1999) the bias + // has been 1.0 causing the portals to cut the BSP in ways that will potentially + // degrade level performance, and increase the BSP complexity. + // + // By setting the bias to a value between 0.3 and 0.7 the positive effects of + // the portals are preserved without giving them unreasonable priority in the BSP. + // + // Portals should be weighted high enough in the BSP to separate major parts of the + // level from each other (pushing entire rooms down the branches of the BSP), but + // should not be so high that portals cut through adjacent geometry in a way that + // increases complexity of the room being (typically, accidentally) cut. + // + Score -= ( 100.0 - float(Balance) ) * Splits * PortalBias; // ignore PortalBias of the split polys -- bias toward portal selection for cutting planes! + } + //UE_LOG(LogBSPOps, Log, " %4d: Score = %f (Front = %4d, Back = %4d, Splits = %4d, Flags = %08X)", Index, Score, Front, Back, Splits, Poly->PolyFlags ); //LEC + + if( Score AllocatedFPolys; + + // To account for big EdPolys split up. + int32 NumPolysToAlloc = NumPolys + 8 + NumPolys/4; + int32 NumFront=0; FPoly **FrontList = new(FMemStack::Get(),NumPolysToAlloc)FPoly*; + int32 NumBack =0; FPoly **BackList = new(FMemStack::Get(),NumPolysToAlloc)FPoly*; + + FPoly *SplitPoly = FindBestSplit( NumPolys, PolyList, Opt, Balance, PortalBias ); + + // Add the splitter poly to the Bsp with either a new BspSurf or an existing one. + if( RebuildSimplePolys ) + { + SplitPoly->iLinkSurf = Model->Surfs.Num(); + } + + int32 iOurNode = bspAddNode(Model,iParent,NodePlace,0,SplitPoly, BspPoints, BspVectors); + int32 iPlaneNode = iOurNode; + + // Now divide all polygons in the pool into (A) polygons that are + // in front of Poly, and (B) polygons that are in back of Poly. + // Coplanar polys are inserted immediately, before recursing. + + // If any polygons are split by Poly, we ignrore the original poly, + // split it into two polys, and add two new polys to the pool. + FPoly *FrontEdPoly = new FPoly; + FPoly *BackEdPoly = new FPoly; + // Keep track of allocations. + AllocatedFPolys.Add( FrontEdPoly ); + AllocatedFPolys.Add( BackEdPoly ); + + for( int32 i=0; iSplitWithPlane( SplitPoly->Vertices[0], SplitPoly->Normal, FrontEdPoly, BackEdPoly, 0 ) ) + { + case SP_Coplanar: + if( RebuildSimplePolys ) + { + EdPoly->iLinkSurf = Model->Surfs.Num()-1; + } + iPlaneNode = bspAddNode( Model, iPlaneNode, NODE_Plane, 0, EdPoly, BspPoints, BspVectors ); + break; + + case SP_Front: + FrontList[NumFront++] = PolyList[i]; + break; + + case SP_Back: + BackList[NumBack++] = PolyList[i]; + break; + + case SP_Split: + + // Create front & back nodes. + FrontList[NumFront++] = FrontEdPoly; + BackList [NumBack ++] = BackEdPoly; + + FrontEdPoly = new FPoly; + BackEdPoly = new FPoly; + // Keep track of allocations. + AllocatedFPolys.Add( FrontEdPoly ); + AllocatedFPolys.Add( BackEdPoly ); + + break; + } + } + + // Recursively split the front and back pools. + if( NumFront > 0 ) SplitPolyList( Model, iOurNode, NODE_Front, NumFront, FrontList, Opt, Balance, PortalBias, RebuildSimplePolys, BspPoints, BspVectors ); + if( NumBack > 0 ) SplitPolyList( Model, iOurNode, NODE_Back, NumBack, BackList, Opt, Balance, PortalBias, RebuildSimplePolys, BspPoints, BspVectors ); + + // Delete FPolys allocated above. We cannot use FMemStack::Get() for FPoly as the array data FPoly contains will be allocated in regular memory. + for( int32 i=0; iGetName() ); // moved here so that we can easily debug when an actor has lost parts of its brush + + check(Actor->GetBrushComponent()); + check(Actor->Brush); + check(Actor->Brush->RootOutside); + + RebuildBrush(Actor->Brush, BspPoints, BspVectors); + + // Make sure simplified collision is up to date. + Actor->GetBrushComponent()->BuildSimpleBrushCollision(); + Actor->RebuildNavigationData(); +} + +/** + * Duplicates the specified brush and makes it into a CSG-able level brush. + * @return The new brush, or NULL if the original was empty. + */ +void FHBSPOps::csgCopyBrush( ABrush* Dest, ABrush* Src, uint32 PolyFlags, EObjectFlags ResFlags, bool bNeedsPrep, bool bCopyPosRotScale, bool bAllowEmpty, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ) +{ + check(Src); + check(Src->GetBrushComponent()); + check(Src->Brush); + + // Handle empty brush. + if( !bAllowEmpty && !Src->Brush->Polys->Element.Num() ) + { + Dest->Brush = NULL; + Dest->GetBrushComponent()->Brush = NULL; + return; + } + + // Duplicate the brush and its polys. + Dest->PolyFlags = PolyFlags; + Dest->Brush = NewObject(Dest, NAME_None, ResFlags); + Dest->Brush->Initialize(nullptr, Src->Brush->RootOutside); + Dest->Brush->Polys = NewObject(Dest->Brush, NAME_None, ResFlags); + Dest->Brush->Polys->Element = Src->Brush->Polys->Element; + Dest->GetBrushComponent()->Brush = Dest->Brush; + if(Src->BrushBuilder != nullptr) + { + Dest->BrushBuilder = DuplicateObject(Src->BrushBuilder, Dest); + } + + // Update poly textures. + for( int32 i=0; iBrush->Polys->Element.Num(); i++ ) + { + Dest->Brush->Polys->Element[i].iBrushPoly = INDEX_NONE; + } + + // Copy positioning, and build bounding box. + if(bCopyPosRotScale) + { + Dest->CopyPosRotScaleFrom( Src ); + } + + // If it's a moving brush, prep it. + if( bNeedsPrep ) + { + csgPrepMovingBrush( Dest, BspPoints, BspVectors ); + } +} + +/** + * Adds a brush to the list of CSG brushes in the level, using a CSG operation. + * + * @return A newly-created copy of the brush. + */ +ABrush* FHBSPOps::csgAddOperation( ABrush* Actor, uint32 PolyFlags, EBrushType BrushType, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors) +{ + check(Actor); + check(Actor->GetBrushComponent()); + check(Actor->Brush); + check(Actor->Brush->Polys); + check(Actor->GetWorld()); + + // Can't do this if brush has no polys. + if( !Actor->Brush->Polys->Element.Num() ) + return NULL; + + // Spawn a new actor for the brush. + + ABrush* Result = Actor->GetWorld()->SpawnBrush(); + Result->SetNotForClientOrServer(); + + // Duplicate the brush. + csgCopyBrush + ( + Result, + Actor, + PolyFlags, + RF_Transactional, + 0, + true, + false, + BspPoints, + BspVectors + ); + check(Result->Brush); + + if( Result->GetBrushBuilder() ) + { + FActorLabelUtilities::SetActorLabelUnique(Result, FText::Format(NSLOCTEXT("BSPBrushOps", "BrushName", "{0} Brush"), FText::FromString(Result->GetBrushBuilder()->GetClass()->GetDescription())).ToString()); + } + // Assign the default material to the brush's polys. + for( int32 i=0; iBrush->Polys->Element.Num(); i++ ) + { + FPoly& CurrentPoly = Result->Brush->Polys->Element[i]; + if ( !CurrentPoly.Material ) + { + CurrentPoly.Material = UMaterial::GetDefaultMaterial(MD_Surface); + } + } + + // Set add-info. + Result->BrushType = BrushType; + + Result->ReregisterAllComponents(); + + return Result; +} + +/** Add a new point to the model (preventing duplicates) and return its index. */ +static int32 AddThing(TArray& Vectors, const FVector3f& V, float Thresh, int32 Check) +{ + if (Check) + { + // See if this is very close to an existing point/vector. + for (int32 i = 0; i < Vectors.Num(); i++) + { + const FVector3f& TableVect = Vectors[i]; + float Temp = (V.X - TableVect.X); + if ((Temp > -Thresh) && (Temp < Thresh)) + { + Temp = (V.Y - TableVect.Y); + if ((Temp > -Thresh) && (Temp < Thresh)) + { + Temp = (V.Z - TableVect.Z); + if ((Temp > -Thresh) && (Temp < Thresh)) + { + // Found nearly-matching vector. + return i; + } + } + } + } + } + return Vectors.Add(V); +} + + +/** Add a new vector to the model, merging near-duplicates, and return its index. */ +int32 FHBSPOps::bspAddVector( UModel* Model, FVector* V, bool Exact, UHBspPointsGrid* BspVectors ) +{ + const float Thresh = Exact ? THRESH_NORMALS_ARE_SAME : THRESH_VECTORS_ARE_NEAR; + + if (BspVectors) + { + // If a points grid has been built for quick vector lookup, use that instead of doing a linear search + const int32 NextIndex = Model->Vectors.Num(); + const int32 ReturnedIndex = BspVectors->FindOrAddPoint(*V, NextIndex, Thresh); + if (ReturnedIndex == NextIndex) + { + Model->Vectors.Add((FVector3f)*V); + } + + return ReturnedIndex; + } + + return AddThing + ( + Model->Vectors, + (FVector3f)*V, + Exact ? THRESH_NORMALS_ARE_SAME : THRESH_VECTORS_ARE_NEAR, + 1 + ); +} + +/** Add a new point to the model, merging near-duplicates, and return its index. */ +int32 FHBSPOps::bspAddPoint( UModel* Model, FVector* V, bool Exact, UHBspPointsGrid* BspPoints ) +{ + const float Thresh = Exact ? THRESH_POINTS_ARE_SAME : THRESH_POINTS_ARE_NEAR; + + if (BspPoints) + { + // If a points grid has been built for quick point lookup, use that instead of doing a linear search + const int32 NextIndex = Model->Points.Num(); + // Always look for points with a low threshold; a generous threshold can result in 'leaks' in the BSP and unwanted polys being generated + const int32 ReturnedIndex = BspPoints->FindOrAddPoint(*V, NextIndex, THRESH_POINTS_ARE_SAME); + if (ReturnedIndex == NextIndex) + { + Model->Points.Add((FVector3f)*V); + } + + return ReturnedIndex; + } + + // Try to find a match quickly from the Bsp. This finds all potential matches + // except for any dissociated from nodes/surfaces during a rebuild. + FVector3f Temp; + int32 pVertex; + float NearestDist = Model->FindNearestVertex((FVector3f)*V,Temp,Thresh,pVertex); + if( (NearestDist >= 0.0) && (NearestDist <= Thresh) ) + { + // Found an existing point. + return pVertex; + } + else + { + // No match found; add it slowly to find duplicates. + return AddThing(Model->Points, (FVector3f)*V, Thresh, !GFastRebuild); + } +} + + +/** + * Builds Bsp from the editor polygon set (EdPolys) of a model. + * + * Opt = Bsp optimization, BSP_Lame (fast), BSP_Good (medium), BSP_Optimal (slow) + * Balance = 0-100, 0=only worry about minimizing splits, 100=only balance tree. + */ +void FHBSPOps::bspBuild( UModel* Model, enum EBspOptimization Opt, int32 Balance, int32 PortalBias, int32 RebuildSimplePolys, int32 iNode, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ) +{ + int32 OriginalPolys = Model->Polys->Element.Num(); + + // Empty the model's tables. + if( RebuildSimplePolys==1 ) + { + // Empty everything but polys. + Model->EmptyModel( 1, 0 ); + } + else if( RebuildSimplePolys==0 ) + { + // Empty node vertices. + for( int32 i=0; iNodes.Num(); i++ ) + Model->Nodes[i].NumVertices = 0; + + // Refresh the Bsp. + bspRefresh(Model,1); + + // Empty nodes. + Model->EmptyModel( 0, 0 ); + } + if( Model->Polys->Element.Num() ) + { + // Allocate polygon pool. + FMemMark Mark(FMemStack::Get()); + FPoly** PolyList = new( FMemStack::Get(), Model->Polys->Element.Num() )FPoly*; + + // Add all FPolys to active list. + for( int32 i=0; iPolys->Element.Num(); i++ ) + if( Model->Polys->Element[i].Vertices.Num() ) + PolyList[i] = &Model->Polys->Element[i]; + + // Now split the entire Bsp by splitting the list of all polygons. + SplitPolyList + ( + Model, + INDEX_NONE, + NODE_Root, + Model->Polys->Element.Num(), + PolyList, + Opt, + Balance, + PortalBias, + RebuildSimplePolys, + BspPoints, + BspVectors + ); + + // Now build the bounding boxes for all nodes. + if( RebuildSimplePolys==0 ) + { + // Remove unreferenced things. + bspRefresh( Model, 1 ); + + // Rebuild all bounding boxes. + bspBuildBounds( Model ); + } + + Mark.Pop(); + } + +// UE_LOG(LogBSPOps, Log, TEXT("bspBuild built %i convex polys into %i nodes"), OriginalPolys, Model->Nodes.Num() ); +} + +/** + * If the Bsp's point and vector tables are nearly full, reorder them and delete unused ones. + */ +void FHBSPOps::bspRefresh( UModel* Model, bool NoRemapSurfs ) +{ + FMemStack& MemStack = FMemStack::Get(); + + FMemMark Mark(MemStack); + + int32 NumNodes = Model->Nodes.Num(); + int32 NumSurfs = Model->Surfs.Num(); + int32 NumVectors = Model->Vectors.Num(); + int32 NumPoints = Model->Points.Num(); + + // Remove unreferenced Bsp surfs. + int32* PolyRef; + if( NoRemapSurfs ) + { + PolyRef = NewZeroed(MemStack, NumSurfs); + } + else + { + PolyRef = NewOned(MemStack, NumSurfs); + } + + int32* NodeRef = NewOned(MemStack, NumNodes); + if( NumNodes > 0 ) + { + TagReferencedNodes( Model, NodeRef, PolyRef, 0 ); + } + + // Remap Bsp surfs. + { + int32 n=0; + for( int32 i=0; iSurfs[n] = Model->Surfs[i]; + PolyRef[i]=n++; + } + } + //UE_LOG(LogBSPOps, Log, TEXT("Polys: %i -> %i"), NumSurfs, n ); + Model->Surfs.RemoveAt( n, NumSurfs-n ); + NumSurfs = n; + } + + // Remap Bsp nodes. + { + int32 n=0; + for( int32 i=0; iNodes[n] = Model->Nodes[i]; + NodeRef[i]=n++; + } + } + //UE_LOG(LogBSPOps, Log, TEXT("Nodes: %i -> %i"), NumNodes, n ); + Model->Nodes.RemoveAt( n, NumNodes-n ); + NumNodes = n; + } + + // Update Bsp nodes. + for( int32 i=0; iNodes[i]; + Node->iSurf = PolyRef[Node->iSurf]; + if (Node->iFront != INDEX_NONE) Node->iFront = NodeRef[Node->iFront]; + if (Node->iBack != INDEX_NONE) Node->iBack = NodeRef[Node->iBack]; + if (Node->iPlane != INDEX_NONE) Node->iPlane = NodeRef[Node->iPlane]; + } + + // Remove unreferenced points and vectors. + int32* VectorRef = NewOned(MemStack, NumVectors); + int32* PointRef = NewOned(MemStack, NumPoints); + + // Check Bsp surfs. + TArray VertexRef; + for( int32 i=0; iSurfs[i]; + VectorRef [Surf->vNormal ] = 0; + VectorRef [Surf->vTextureU ] = 0; + VectorRef [Surf->vTextureV ] = 0; + PointRef [Surf->pBase ] = 0; + } + + // Check Bsp nodes. + for( int32 i=0; iNodes[i]; + FVert* VertPool = &Model->Verts[Node->iVertPool]; + for( int B=0; BNumVertices; B++ ) + { + PointRef[VertPool->pVertex] = 0; + VertPool++; + } + } + + // Remap points. + { + int32 n=0; + for( int32 i=0; iPoints[n] = Model->Points[i]; + PointRef[i] = n++; + } + //UE_LOG(LogBSPOps, Log, TEXT("Points: %i -> %i"), NumPoints, n ); + Model->Points.RemoveAt( n, NumPoints-n ); + NumPoints = n; + } + + // Remap vectors. + { + int32 n=0; + for (int32 i=0; iVectors[n] = Model->Vectors[i]; + VectorRef[i] = n++; + } + //UE_LOG(LogBSPOps, Log, TEXT("Vectors: %i -> %i"), NumVectors, n ); + Model->Vectors.RemoveAt( n, NumVectors-n ); + NumVectors = n; + } + + // Update Bsp surfs. + for( int32 i=0; iSurfs[i]; + Surf->vNormal = VectorRef [Surf->vNormal ]; + Surf->vTextureU = VectorRef [Surf->vTextureU]; + Surf->vTextureV = VectorRef [Surf->vTextureV]; + Surf->pBase = PointRef [Surf->pBase ]; + } + + // Update Bsp nodes. + for( int32 i=0; iNodes[i]; + FVert* VertPool = &Model->Verts[Node->iVertPool]; + for( int B=0; BNumVertices; B++ ) + { + VertPool->pVertex = PointRef [VertPool->pVertex]; + VertPool++; + } + } + + // Shrink the objects. + Model->ShrinkModel(); + + Mark.Pop(); +} + +// Build bounding volumes for all Bsp nodes. The bounding volume of the node +// completely encloses the "outside" space occupied by the nodes. Note that +// this is not the same as representing the bounding volume of all of the +// polygons within the node. +// +// We start with a practically-infinite cube and filter it down the Bsp, +// whittling it away until all of its convex volume fragments land in leaves. +void FHBSPOps::bspBuildBounds( UModel* Model ) +{ + if( Model->Nodes.Num()==0 ) + return; + + FPoly Polys[6], *PolyList[6]; + for( int32 i=0; i<6; i++ ) + { + PolyList[i] = &Polys[i]; + PolyList[i]->Init(); + PolyList[i]->iBrushPoly = INDEX_NONE; + } + + new(Polys[0].Vertices)FVector3f(-HALF_WORLD_MAX,-HALF_WORLD_MAX,HALF_WORLD_MAX); + new(Polys[0].Vertices)FVector3f( HALF_WORLD_MAX,-HALF_WORLD_MAX,HALF_WORLD_MAX); + new(Polys[0].Vertices)FVector3f( HALF_WORLD_MAX, HALF_WORLD_MAX,HALF_WORLD_MAX); + new(Polys[0].Vertices)FVector3f(-HALF_WORLD_MAX, HALF_WORLD_MAX,HALF_WORLD_MAX); + Polys[0].Normal = FVector3f( 0.000000, 0.000000, 1.000000 ); + Polys[0].Base =Polys[0].Vertices[0]; + + new(Polys[1].Vertices)FVector3f(-HALF_WORLD_MAX, HALF_WORLD_MAX,-HALF_WORLD_MAX); + new(Polys[1].Vertices)FVector3f( HALF_WORLD_MAX, HALF_WORLD_MAX,-HALF_WORLD_MAX); + new(Polys[1].Vertices)FVector3f( HALF_WORLD_MAX,-HALF_WORLD_MAX,-HALF_WORLD_MAX); + new(Polys[1].Vertices)FVector3f(-HALF_WORLD_MAX,-HALF_WORLD_MAX,-HALF_WORLD_MAX); + Polys[1].Normal = FVector3f( 0.000000, 0.000000, -1.000000 ); + Polys[1].Base =Polys[1].Vertices[0]; + + new(Polys[2].Vertices)FVector3f(-HALF_WORLD_MAX,HALF_WORLD_MAX,-HALF_WORLD_MAX); + new(Polys[2].Vertices)FVector3f(-HALF_WORLD_MAX,HALF_WORLD_MAX, HALF_WORLD_MAX); + new(Polys[2].Vertices)FVector3f( HALF_WORLD_MAX,HALF_WORLD_MAX, HALF_WORLD_MAX); + new(Polys[2].Vertices)FVector3f( HALF_WORLD_MAX,HALF_WORLD_MAX,-HALF_WORLD_MAX); + Polys[2].Normal = FVector3f( 0.000000, 1.000000, 0.000000 ); + Polys[2].Base =Polys[2].Vertices[0]; + + new(Polys[3].Vertices)FVector3f( HALF_WORLD_MAX,-HALF_WORLD_MAX,-HALF_WORLD_MAX); + new(Polys[3].Vertices)FVector3f( HALF_WORLD_MAX,-HALF_WORLD_MAX, HALF_WORLD_MAX); + new(Polys[3].Vertices)FVector3f(-HALF_WORLD_MAX,-HALF_WORLD_MAX, HALF_WORLD_MAX); + new(Polys[3].Vertices)FVector3f(-HALF_WORLD_MAX,-HALF_WORLD_MAX,-HALF_WORLD_MAX); + Polys[3].Normal =FVector3f( 0.000000, -1.000000, 0.000000 ); + Polys[3].Base =Polys[3].Vertices[0]; + + new(Polys[4].Vertices)FVector3f(HALF_WORLD_MAX, HALF_WORLD_MAX,-HALF_WORLD_MAX); + new(Polys[4].Vertices)FVector3f(HALF_WORLD_MAX, HALF_WORLD_MAX, HALF_WORLD_MAX); + new(Polys[4].Vertices)FVector3f(HALF_WORLD_MAX,-HALF_WORLD_MAX, HALF_WORLD_MAX); + new(Polys[4].Vertices)FVector3f(HALF_WORLD_MAX,-HALF_WORLD_MAX,-HALF_WORLD_MAX); + Polys[4].Normal =FVector3f( 1.000000, 0.000000, 0.000000 ); + Polys[4].Base =Polys[4].Vertices[0]; + + new(Polys[5].Vertices)FVector3f(-HALF_WORLD_MAX,-HALF_WORLD_MAX,-HALF_WORLD_MAX); + new(Polys[5].Vertices)FVector3f(-HALF_WORLD_MAX,-HALF_WORLD_MAX, HALF_WORLD_MAX); + new(Polys[5].Vertices)FVector3f(-HALF_WORLD_MAX, HALF_WORLD_MAX, HALF_WORLD_MAX); + new(Polys[5].Vertices)FVector3f(-HALF_WORLD_MAX, HALF_WORLD_MAX,-HALF_WORLD_MAX); + Polys[5].Normal =FVector3f(-1.000000, 0.000000, 0.000000 ); + Polys[5].Base =Polys[5].Vertices[0]; + // Empty hulls. + Model->LeafHulls.Empty(); + for( int32 i=0; iNodes.Num(); i++ ) + Model->Nodes[i].iCollisionBound = INDEX_NONE; + FilterBound( Model, NULL, 0, PolyList, 6, Model->RootOutside ); +// UE_LOG(LogBSPOps, Log, TEXT("bspBuildBounds: Generated %i hulls"), Model->LeafHulls.Num() ); +} + +/** + * Validate a brush, and set iLinks on all EdPolys to index of the + * first identical EdPoly in the list, or its index if it's the first. + * Not transactional. + */ +void FHBSPOps::bspValidateBrush( UModel* Brush, bool ForceValidate, bool DoStatusUpdate ) +{ + check(Brush != nullptr); + Brush->Modify(); + if( ForceValidate || !Brush->Linked ) + { + Brush->Linked = 1; + for( int32 i=0; iPolys->Element.Num(); i++ ) + { + Brush->Polys->Element[i].iLink = i; + } + int32 n=0; + for( int32 i=0; iPolys->Element.Num(); i++ ) + { + FPoly* EdPoly = &Brush->Polys->Element[i]; + if( EdPoly->iLink==i ) + { + for( int32 j=i+1; jPolys->Element.Num(); j++ ) + { + FPoly* OtherPoly = &Brush->Polys->Element[j]; + if + ( OtherPoly->iLink == j + && OtherPoly->Material == EdPoly->Material + && OtherPoly->TextureU == EdPoly->TextureU + && OtherPoly->TextureV == EdPoly->TextureV + && OtherPoly->PolyFlags == EdPoly->PolyFlags + && (OtherPoly->Normal | EdPoly->Normal)>0.9999 ) + { + float Dist = FVector3f::PointPlaneDist( OtherPoly->Vertices[0], EdPoly->Vertices[0], EdPoly->Normal ); + if( Dist>-0.001 && Dist<0.001 ) + { + OtherPoly->iLink = i; + n++; + } + } + } + } + } +// UE_LOG(LogBSPOps, Log, TEXT("BspValidateBrush linked %i of %i polys"), n, Brush->Polys->Element.Num() ); + } + + // Build bounds. + Brush->BuildBound(); +} + +void FHBSPOps::bspUnlinkPolys( UModel* Brush ) +{ + Brush->Modify(); + Brush->Linked = 1; + for( int32 i=0; iPolys->Element.Num(); i++ ) + { + Brush->Polys->Element[i].iLink = i; + } +} + +// Add an editor polygon to the Bsp, and also stick a reference to it +// in the editor polygon's BspNodes list. If the editor polygon has more sides +// than the Bsp will allow, split it up into several sub-polygons. +// +// Returns: Index to newly-created node of Bsp. If several nodes were created because +// of split polys, returns the parent (highest one up in the Bsp). +int32 FHBSPOps::bspAddNode( UModel* Model, int32 iParent, enum ENodePlace NodePlace, uint32 NodeFlags, FPoly* EdPoly, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ) +{ + if( NodePlace == NODE_Plane ) + { + // Make sure coplanars are added at the end of the coplanar list so that + // we don't insert NF_IsNew nodes with non NF_IsNew coplanar children. + while( Model->Nodes[iParent].iPlane != INDEX_NONE ) + { + iParent = Model->Nodes[iParent].iPlane; + } + } + FBspSurf* Surf = NULL; + if( EdPoly->iLinkSurf == Model->Surfs.Num() ) + { + int32 NewIndex = Model->Surfs.AddZeroed(); + Surf = &Model->Surfs[NewIndex]; + + // This node has a new polygon being added by bspBrushCSG; must set its properties here. + FVector Base = (FVector)EdPoly->Base; + FVector Normal = (FVector)EdPoly->Normal; + FVector TextureU = (FVector)EdPoly->TextureU; + FVector TextureV = (FVector)EdPoly->TextureV; + + Surf->pBase = bspAddPoint (Model,&Base,1,BspPoints); + Surf->vNormal = bspAddVector (Model,&Normal,1,BspVectors); + Surf->vTextureU = bspAddVector (Model,&TextureU,0,BspVectors); + Surf->vTextureV = bspAddVector (Model,&TextureV,0,BspVectors); + Surf->Material = EdPoly->Material; + Surf->Actor = NULL; + + Surf->PolyFlags = EdPoly->PolyFlags & ~PF_NoAddToBSP; + Surf->LightMapScale= EdPoly->LightMapScale; + + // Find the LightmassPrimitiveSettings in the UModel... + int32 FoundLightmassIndex = INDEX_NONE; + if (Model->LightmassSettings.Find(EdPoly->LightmassSettings, FoundLightmassIndex) == false) + { + FoundLightmassIndex = Model->LightmassSettings.Add(EdPoly->LightmassSettings); + } + Surf->iLightmassIndex = FoundLightmassIndex; + + Surf->Actor = EdPoly->Actor; + Surf->iBrushPoly = EdPoly->iBrushPoly; + + if (EdPoly->Actor) + { + Surf->bHiddenEdTemporary = EdPoly->Actor->IsTemporarilyHiddenInEditor(); + Surf->bHiddenEdLevel = EdPoly->Actor->bHiddenEdLevel; + Surf->bHiddenEdLayer = EdPoly->Actor->bHiddenEdLayer; + } + + Surf->Plane = FPlane4f(EdPoly->Vertices[0],EdPoly->Normal); + } + else + { + check(EdPoly->iLinkSurf!=INDEX_NONE); + check(EdPoly->iLinkSurfSurfs.Num()); + Surf = &Model->Surfs[EdPoly->iLinkSurf]; + } + + // Set NodeFlags. + if( Surf->PolyFlags & PF_NotSolid ) NodeFlags |= NF_NotCsg; + if( Surf->PolyFlags & (PF_Invisible|PF_Portal) ) NodeFlags |= NF_NotVisBlocking; + + if( EdPoly->Vertices.Num() > FBspNode::MAX_NODE_VERTICES ) + { + // Split up into two coplanar sub-polygons (one with MAX_NODE_VERTICES vertices and + // one with all the remaining vertices) and recursively add them. + + // EdPoly1 is just the first MAX_NODE_VERTICES from EdPoly. + FMemMark Mark(FMemStack::Get()); + FPoly *EdPoly1 = new FPoly; + *EdPoly1 = *EdPoly; + EdPoly1->Vertices.RemoveAt(FBspNode::MAX_NODE_VERTICES,EdPoly->Vertices.Num() - FBspNode::MAX_NODE_VERTICES); + + // EdPoly2 is the first vertex from EdPoly, and the last EdPoly->Vertices.Num() - MAX_NODE_VERTICES + 1. + FPoly *EdPoly2 = new FPoly; + *EdPoly2 = *EdPoly; + EdPoly2->Vertices.RemoveAt(1,FBspNode::MAX_NODE_VERTICES - 2); + + int32 iNode = bspAddNode( Model, iParent, NodePlace, NodeFlags, EdPoly1, BspPoints, BspVectors ); // Add this poly first. + bspAddNode( Model, iNode, NODE_Plane, NodeFlags, EdPoly2, BspPoints, BspVectors ); // Then add other (may be bigger). + + delete EdPoly1; + delete EdPoly2; + + Mark.Pop(); + return iNode; // Return coplanar "parent" node (not coplanar child) + } + else + { + // Add node. + int32 iNode = Model->Nodes.AddZeroed(); + FBspNode& Node = Model->Nodes[iNode]; + + // Tell transaction tracking system that parent is about to be modified. + FBspNode* Parent=NULL; + if( NodePlace!=NODE_Root ) + Parent = &Model->Nodes[iParent]; + + // Set node properties. + Node.iSurf = EdPoly->iLinkSurf; + Node.NodeFlags = NodeFlags; + Node.iCollisionBound = INDEX_NONE; + Node.Plane = FPlane4f( EdPoly->Vertices[0], EdPoly->Normal ); + Node.iVertPool = Model->Verts.AddUninitialized(EdPoly->Vertices.Num()); + Node.iFront = INDEX_NONE; + Node.iBack = INDEX_NONE; + Node.iPlane = INDEX_NONE; + if( NodePlace==NODE_Root ) + { + Node.iLeaf[0] = INDEX_NONE; + Node.iLeaf[1] = INDEX_NONE; + Node.iZone[0] = 0; + Node.iZone[1] = 0; + } + else if( NodePlace==NODE_Front || NodePlace==NODE_Back ) + { + int32 ZoneFront=NodePlace==NODE_Front; + Node.iLeaf[0] = Parent->iLeaf[ZoneFront]; + Node.iLeaf[1] = Parent->iLeaf[ZoneFront]; + Node.iZone[0] = Parent->iZone[ZoneFront]; + Node.iZone[1] = Parent->iZone[ZoneFront]; + } + else + { + int32 IsFlipped = (Node.Plane|Parent->Plane)<0.0; + Node.iLeaf[0] = Parent->iLeaf[IsFlipped ]; + Node.iLeaf[1] = Parent->iLeaf[1-IsFlipped]; + Node.iZone[0] = Parent->iZone[IsFlipped ]; + Node.iZone[1] = Parent->iZone[1-IsFlipped]; + } + + // Link parent to this node. + if (NodePlace == NODE_Front) + { + Parent->iFront = iNode; + } + else if (NodePlace == NODE_Back) + { + Parent->iBack = iNode; + } + else if (NodePlace == NODE_Plane) + { + Parent->iPlane = iNode; + } + + // Add all points to point table, merging nearly-overlapping polygon points + // with other points in the poly to prevent criscrossing vertices from + // being generated. + + // Must maintain Node->NumVertices on the fly so that bspAddPoint is always + // called with the Bsp in a clean state. + Node.NumVertices = 0; + FVert* VertPool = &Model->Verts[ Node.iVertPool ]; + for( uint8 i=0; iVertices.Num(); i++ ) + { + FVector Vertex = (FVector)EdPoly->Vertices[i]; + int32 pVertex = bspAddPoint(Model,&Vertex,0, BspPoints); + if( Node.NumVertices==0 || VertPool[Node.NumVertices-1].pVertex!=pVertex ) + { + VertPool[Node.NumVertices].iSide = INDEX_NONE; + VertPool[Node.NumVertices].pVertex = pVertex; + Node.NumVertices++; + } + } + if( Node.NumVertices>=2 && VertPool[0].pVertex==VertPool[Node.NumVertices-1].pVertex ) + { + Node.NumVertices--; + } + if( Node.NumVertices < 3 ) + { + GErrors++; +// UE_LOG(LogBSPOps, Warning, TEXT("bspAddNode: Infinitesimal polygon %i (%i)"), Node.NumVertices, EdPoly->Vertices.Num() ); + Node.NumVertices = 0; + } + + return iNode; + } +} + +/** + * Rebuild some brush internals + */ +void FHBSPOps::RebuildBrush(UModel* Brush, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors) +{ + Brush->Modify(); + Brush->EmptyModel(1, 0); + + // Build bounding box. + Brush->BuildBound(); + + // Build BSP for the brush. + bspBuild(Brush, BSP_Good, 15, 70, 1, 0, BspPoints, BspVectors); + bspRefresh(Brush, 1); + bspBuildBounds(Brush); +} + +/** + * Rotates the specified brush's vertices. + */ +void FHBSPOps::RotateBrushVerts(ABrush* Brush, const FRotator& Rotation, bool bClearComponents, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors) +{ + if(Brush->GetBrushComponent()->Brush && Brush->GetBrushComponent()->Brush->Polys) + { + for( int32 poly = 0 ; poly < Brush->GetBrushComponent()->Brush->Polys->Element.Num() ; poly++ ) + { + FPoly* Poly = &(Brush->GetBrushComponent()->Brush->Polys->Element[poly]); + + // Rotate the vertices. + const FRotationMatrix RotMatrix(Rotation); + for (int32 vertex = 0; vertex < Poly->Vertices.Num(); vertex++) + { + Poly->Vertices[vertex] = FVector3f(Brush->GetPivotOffset() + (FVector)RotMatrix.TransformVector((FVector)Poly->Vertices[vertex] - Brush->GetPivotOffset())); + } + Poly->Base = FVector3f(Brush->GetPivotOffset() + (FVector)RotMatrix.TransformVector(FVector(Poly->Base) - Brush->GetPivotOffset())); + + // Rotate the texture vectors. + Poly->TextureU = FVector4f(RotMatrix.TransformVector((FVector)Poly->TextureU)); + Poly->TextureV = FVector4f(RotMatrix.TransformVector((FVector)Poly->TextureV)); + + // Recalc the normal for the poly. + Poly->Normal = FVector3f::ZeroVector; + Poly->Finalize(Brush, 0); + } + + Brush->GetBrushComponent()->Brush->BuildBound(); + + if( !Brush->IsStaticBrush() ) + { + csgPrepMovingBrush( Brush, BspPoints, BspVectors ); + } + + if ( bClearComponents ) + { + Brush->ReregisterAllComponents(); + } + } +} + + +void FHBSPOps::HandleVolumeShapeChanged(AVolume& Volume, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors) +{ + // The default physics volume doesn't have an associated UModel, so we need to handle that case gracefully. + if(Volume.Brush) + { + FHBSPOps::csgPrepMovingBrush( &Volume, BspPoints, BspVectors); + } +} + +UHBspPointsGrid* UHBspPointsGrid::Create(float InGranularity, float InThreshold, int32 InitialSize) +{ + check(InThreshold / InGranularity <= 0.5f); + + UHBspPointsGrid* Obj = NewObject(GetTransientPackage(), UHBspPointsGrid::StaticClass()); + Obj->OneOverGranularity = 1.0f / InGranularity; + Obj->Threshold = InThreshold; + Obj->Clear(InitialSize); + + return Obj; +} + +void UHBspPointsGrid::Clear(int32 InitialSize) +{ + GridMap.Empty(InitialSize); +} + + +// Given a grid index in one axis, a real position on the grid and a threshold radius, +// return either: +// - the additional grid index it can overlap in that axis, or +// - the original grid index if there is no overlap. +int32 UHBspPointsGrid::GetAdjacentIndexIfOverlapping(int32 GridIndex, float GridPos, float GridThreshold) +{ + if (GridPos - GridIndex < GridThreshold) + { + return GridIndex - 1; + } + else if (1.0f - (GridPos - GridIndex) < GridThreshold) + { + return GridIndex + 1; + } + else + { + return GridIndex; + } +} + +int32 UHBspPointsGrid::FindOrAddPoint(const FVector& Point, int32 Index, float PointThreshold) +{ + // Offset applied to the grid coordinates so aligned vertices (the normal case) don't overlap several grid items (taking into account the threshold) + const float GridOffset = 0.12345f; + + const float AdjustedPointX = Point.X - GridOffset; + const float AdjustedPointY = Point.Y - GridOffset; + const float AdjustedPointZ = Point.Z - GridOffset; + + const float GridX = AdjustedPointX * OneOverGranularity; + const float GridY = AdjustedPointY * OneOverGranularity; + const float GridZ = AdjustedPointZ * OneOverGranularity; + + // Get the grid indices corresponding to the point coordinates + const int32 GridIndexX = FMath::FloorToInt(GridX); + const int32 GridIndexY = FMath::FloorToInt(GridY); + const int32 GridIndexZ = FMath::FloorToInt(GridZ); + + // Find grid item in map + FHBspPointsGridItem& GridItem = GridMap.FindOrAdd(FHBspPointsKey(GridIndexX, GridIndexY, GridIndexZ)); + + // Iterate through grid item points and return a point if it's close to the threshold + const float PointThresholdSquared = PointThreshold * PointThreshold; + for (const FHBspIndexedPoint& IndexedPoint : GridItem.IndexedPoints) + { + if (FVector::DistSquared(IndexedPoint.Point, Point) <= PointThresholdSquared) + { + return IndexedPoint.Index; + } + } + + // Otherwise, the point is new: add it to the grid item. + GridItem.IndexedPoints.Emplace(Point, Index); + + // The grid has a maximum threshold of a certain radius. If the point is near the edge of a grid cube, it may overlap into other items. + // Add it to all grid items it can be seen from. + const float GridThreshold = Threshold * OneOverGranularity; + const int32 NeighbourX = GetAdjacentIndexIfOverlapping(GridIndexX, GridX, GridThreshold); + const int32 NeighbourY = GetAdjacentIndexIfOverlapping(GridIndexY, GridY, GridThreshold); + const int32 NeighbourZ = GetAdjacentIndexIfOverlapping(GridIndexZ, GridZ, GridThreshold); + + const bool bOverlapsInX = (NeighbourX != GridIndexX); + const bool bOverlapsInY = (NeighbourY != GridIndexY); + const bool bOverlapsInZ = (NeighbourZ != GridIndexZ); + + if (bOverlapsInX) + { + GridMap.FindOrAdd(FHBspPointsKey(NeighbourX, GridIndexY, GridIndexZ)).IndexedPoints.Emplace(Point, Index); + + if (bOverlapsInY) + { + GridMap.FindOrAdd(FHBspPointsKey(NeighbourX, NeighbourY, GridIndexZ)).IndexedPoints.Emplace(Point, Index); + + if (bOverlapsInZ) + { + GridMap.FindOrAdd(FHBspPointsKey(NeighbourX, NeighbourY, NeighbourZ)).IndexedPoints.Emplace(Point, Index); + } + } + else if (bOverlapsInZ) + { + GridMap.FindOrAdd(FHBspPointsKey(NeighbourX, GridIndexY, NeighbourZ)).IndexedPoints.Emplace(Point, Index); + } + } + else + { + if (bOverlapsInY) + { + GridMap.FindOrAdd(FHBspPointsKey(GridIndexX, NeighbourY, GridIndexZ)).IndexedPoints.Emplace(Point, Index); + + if (bOverlapsInZ) + { + GridMap.FindOrAdd(FHBspPointsKey(GridIndexX, NeighbourY, NeighbourZ)).IndexedPoints.Emplace(Point, Index); + } + } + else if (bOverlapsInZ) + { + GridMap.FindOrAdd(FHBspPointsKey(GridIndexX, GridIndexY, NeighbourZ)).IndexedPoints.Emplace(Point, Index); + } + } + + return Index; +} + + +``` + +`Source/HoudiniEngine/Private/HBSPOps.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/Brush.h" +#include "Engine/Polys.h" + +#include "HBSPOps.generated.h" + +class AVolume; +class UModel; + +// This codebase have been localised from UnrealEd/HBSPOps to remove static/global variables. +class FHBSPOps +{ +public: + FHBSPOps(); + + /** Quality level for rebuilding Bsp. */ + enum EBspOptimization + { + BSP_Lame, + BSP_Good, + BSP_Optimal + }; + + /** Possible positions of a child Bsp node relative to its parent (for BspAddToNode) */ + enum ENodePlace + { + NODE_Back = 0, // Node is in back of parent -> Bsp[iParent].iBack. + NODE_Front = 1, // Node is in front of parent -> Bsp[iParent].iFront. + NODE_Plane = 2, // Node is coplanar with parent -> Bsp[iParent].iPlane. + NODE_Root = 3, // Node is the Bsp root and has no parent -> Bsp[0]. + }; + + static void csgPrepMovingBrush( ABrush* Actor, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ); + static void csgCopyBrush( ABrush* Dest, ABrush* Src, uint32 PolyFlags, EObjectFlags ResFlags, bool bNeedsPrep, bool bCopyPosRotScale, bool bAllowEmpty, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ); + static ABrush* csgAddOperation( ABrush* Actor, uint32 PolyFlags, EBrushType BrushType, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ); + + static int32 bspAddVector( UModel* Model, FVector* V, bool Exact, UHBspPointsGrid* BspVectors ); + static int32 bspAddPoint( UModel* Model, FVector* V, bool Exact, UHBspPointsGrid* BspPoints ); + static void bspBuild( UModel* Model, enum EBspOptimization Opt, int32 Balance, int32 PortalBias, int32 RebuildSimplePolys, int32 iNode, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ); + static void bspRefresh( UModel* Model, bool NoRemapSurfs ); + + static void bspBuildBounds( UModel* Model ); + + static void bspValidateBrush( UModel* Brush, bool ForceValidate, bool DoStatusUpdate ); + static void bspUnlinkPolys( UModel* Brush ); + static int32 bspAddNode( UModel* Model, int32 iParent, enum ENodePlace NodePlace, uint32 NodeFlags, FPoly* EdPoly, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ); + + /** + * Rebuild some brush internals + */ + static void RebuildBrush(UModel* Brush, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors); + + static FPoly BuildInfiniteFPoly( UModel* Model, int32 iNode ); + + /** + * Rotates the specified brush's vertices. + */ + static void RotateBrushVerts(ABrush* Brush, const FRotator& Rotation, bool bClearComponents, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors); + + /** Called when an AVolume shape is changed*/ + static void HandleVolumeShapeChanged(AVolume& Volume, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors); + + /** Errors encountered in Csg operation. */ + static int32 GErrors; + static bool GFastRebuild; + +protected: + static void SplitPolyList + ( + UModel *Model, + int32 iParent, + FHBSPOps::ENodePlace NodePlace, + int32 NumPolys, + FPoly **PolyList, + EBspOptimization Opt, + int32 Balance, + int32 PortalBias, + int32 RebuildSimplePolys, + UHBspPointsGrid* BspPoints, + UHBspPointsGrid* BspVectors + ); +}; + + +struct FHBspPointsKey +{ + int32 X; + int32 Y; + int32 Z; + + FHBspPointsKey(int32 InX, int32 InY, int32 InZ) + : X(InX) + , Y(InY) + , Z(InZ) + {} + + friend FORCEINLINE bool operator == (const FHBspPointsKey& A, const FHBspPointsKey& B) + { + return A.X == B.X && A.Y == B.Y && A.Z == B.Z; + } + + friend FORCEINLINE uint32 GetTypeHash(const FHBspPointsKey& Key) + { + return HashCombine(static_cast(Key.X), HashCombine(static_cast(Key.Y), static_cast(Key.Z))); + } +}; + +struct FHBspIndexedPoint +{ + FHBspIndexedPoint(const FVector& InPoint, int32 InIndex) + : Point(InPoint) + , Index(InIndex) + {} + + FVector Point; + int32 Index; +}; + + +struct FHBspPointsGridItem +{ + TArray> IndexedPoints; +}; + + +// Represents a sparse granular 3D grid into which points are added for quick (~O(1)) lookup. +// The 3D space is divided into a grid with a given granularity. +// Points are considered to have a given radius (threshold) and are added to the grid cube they fall in, and to up to seven neighbours if they overlap. +UCLASS() +class HOUDINIENGINE_API UHBspPointsGrid : public UObject +{ + GENERATED_BODY() +protected: + + UHBspPointsGrid() {} + +public: + // Create a new instance of this grid with the given arguments. + static UHBspPointsGrid* Create(float InGranularity, float InThreshold, int32 InitialSize = 0); + + void Clear(int32 InitialSize = 0); + + int32 FindOrAddPoint(const FVector& Point, int32 Index, float Threshold); + + static FORCEINLINE int32 GetAdjacentIndexIfOverlapping(int32 GridIndex, float GridPos, float GridThreshold); + +private: + float OneOverGranularity; + float Threshold; + + typedef TMap FGridMap; + FGridMap GridMap; +}; + +``` + +`Source/HoudiniEngine/Private/HCsgUtils.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HCsgUtils.h" + +#include "Engine/Engine.h" +#include "Engine/Polys.h" +#include "Engine/Selection.h" +#include "Materials/Material.h" +#include "Misc/FeedbackContext.h" + +#include "ActorEditorUtils.h" +#include "Misc/ScopedSlowTask.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "MaterialDomain.h" +#endif + + +DEFINE_LOG_CATEGORY_STATIC(LogHCsgUtils, Log, All); + +#if WITH_EDITOR +#include "Editor.h" +#endif + +// Magic numbers. +#define THRESH_OPTGEOM_COPLANAR (0.25) /* Threshold for Bsp geometry optimization */ +#define THRESH_OPTGEOM_COSIDAL (0.25) /* Threshold for Bsp geometry optimization */ + + +UHCsgUtils::UHCsgUtils() +{ + // A TempModel is allocated for the HCsgUtils instance to avoid reallocation during inner loops. + TempModel = NewObject(); + TempModel->Initialize(nullptr, 1); + + /*GBspPoints = NewObject(); + GBspVectors = NewObject();*/ +} + + +/*---------------------------------------------------------------------------- + CSG leaf filter callbacks. +----------------------------------------------------------------------------*/ + +void UHCsgUtils::AddBrushToWorldFunc( UModel* Model, int32 iNode, FPoly* EdPoly, + EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ) +{ + switch( Filter ) + { + case F_OUTSIDE: + case F_COPLANAR_OUTSIDE: + FHBSPOps::bspAddNode (Model,iNode,ENodePlace,NF_IsNew,EdPoly, BspPoints, BspVectors); + break; + case F_COSPATIAL_FACING_OUT: + if( !(EdPoly->PolyFlags & PF_Semisolid) ) + FHBSPOps::bspAddNode (Model,iNode,ENodePlace,NF_IsNew,EdPoly, BspPoints, BspVectors); + break; + case F_INSIDE: + case F_COPLANAR_INSIDE: + case F_COSPATIAL_FACING_IN: + break; + } +} + +void UHCsgUtils::AddWorldToBrushFunc( UModel* Model, int32 iNode, FPoly* EdPoly, + EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ) +{ + switch( Filter ) + { + case F_OUTSIDE: + case F_COPLANAR_OUTSIDE: + // Only affect the world poly if it has been cut. + if( EdPoly->PolyFlags & PF_EdCut ) + FHBSPOps::bspAddNode( GModel, GLastCoplanar, FHBSPOps::NODE_Plane, NF_IsNew, EdPoly, BspPoints, BspVectors ); + break; + case F_INSIDE: + case F_COPLANAR_INSIDE: + case F_COSPATIAL_FACING_IN: + case F_COSPATIAL_FACING_OUT: + // Discard original poly. + GDiscarded++; + if( GModel->Nodes[GNode].NumVertices ) + { + GModel->Nodes[GNode].NumVertices = 0; + } + break; + } +} + +void UHCsgUtils::SubtractBrushFromWorldFunc( UModel* Model, int32 iNode, FPoly* EdPoly, + EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ) +{ + switch (Filter) + { + case F_OUTSIDE: + case F_COSPATIAL_FACING_OUT: + case F_COSPATIAL_FACING_IN: + case F_COPLANAR_OUTSIDE: + break; + case F_COPLANAR_INSIDE: + case F_INSIDE: + EdPoly->Reverse(); + FHBSPOps::bspAddNode (Model,iNode,ENodePlace,NF_IsNew,EdPoly, BspPoints, BspVectors); // Add to Bsp back + EdPoly->Reverse(); + break; + } +} + +void UHCsgUtils::SubtractWorldToBrushFunc( UModel* Model, int32 iNode, FPoly* EdPoly, + EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ) +{ + switch( Filter ) + { + case F_OUTSIDE: + case F_COPLANAR_OUTSIDE: + case F_COSPATIAL_FACING_IN: + // Only affect the world poly if it has been cut. + if( EdPoly->PolyFlags & PF_EdCut ) + FHBSPOps::bspAddNode( GModel, GLastCoplanar, FHBSPOps::NODE_Plane, NF_IsNew, EdPoly, BspPoints, BspVectors); + break; + case F_INSIDE: + case F_COPLANAR_INSIDE: + case F_COSPATIAL_FACING_OUT: + // Discard original poly. + GDiscarded++; + if( GModel->Nodes[GNode].NumVertices ) + { + GModel->Nodes[GNode].NumVertices = 0; + } + break; + } +} + +void UHCsgUtils::IntersectBrushWithWorldFunc( UModel* Model, int32 iNode, FPoly *EdPoly, + EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ) +{ + switch( Filter ) + { + case F_OUTSIDE: + case F_COPLANAR_OUTSIDE: + case F_COSPATIAL_FACING_IN: + case F_COSPATIAL_FACING_OUT: + // Ignore. + break; + case F_INSIDE: + case F_COPLANAR_INSIDE: + if( EdPoly->Fix()>=3 ) + new(GModel->Polys->Element)FPoly(*EdPoly); + break; + } +} + +void UHCsgUtils::IntersectWorldWithBrushFunc( UModel *Model, int32 iNode, FPoly *EdPoly, + EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ) +{ + switch( Filter ) + { + case F_OUTSIDE: + case F_COPLANAR_OUTSIDE: + case F_COSPATIAL_FACING_IN: + // Ignore. + break; + case F_INSIDE: + case F_COPLANAR_INSIDE: + case F_COSPATIAL_FACING_OUT: + if( EdPoly->Fix() >= 3 ) + new(GModel->Polys->Element)FPoly(*EdPoly); + break; + } +} + +void UHCsgUtils::DeIntersectBrushWithWorldFunc( UModel* Model, int32 iNode, FPoly* EdPoly, + EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ) +{ + switch( Filter ) + { + case F_INSIDE: + case F_COPLANAR_INSIDE: + case F_COSPATIAL_FACING_OUT: + case F_COSPATIAL_FACING_IN: + // Ignore. + break; + case F_OUTSIDE: + case F_COPLANAR_OUTSIDE: + if( EdPoly->Fix()>=3 ) + new(GModel->Polys->Element)FPoly(*EdPoly); + break; + } +} + +void UHCsgUtils::DeIntersectWorldWithBrushFunc( UModel* Model, int32 iNode, FPoly* EdPoly, + EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ) +{ + switch( Filter ) + { + case F_OUTSIDE: + case F_COPLANAR_OUTSIDE: + case F_COSPATIAL_FACING_OUT: + // Ignore. + break; + case F_COPLANAR_INSIDE: + case F_INSIDE: + case F_COSPATIAL_FACING_IN: + if( EdPoly->Fix() >= 3 ) + { + EdPoly->Reverse(); + new(GModel->Polys->Element)FPoly(*EdPoly); + EdPoly->Reverse(); + } + break; + } +} + +/*---------------------------------------------------------------------------- + CSG polygon filtering routine (calls the callbacks). +----------------------------------------------------------------------------*/ + +// +// Handle a piece of a polygon that was filtered to a leaf. +// +void UHCsgUtils::FilterLeaf +( + BspFilterFunc FilterFunc, + UModel* Model, + int32 iNode, + FPoly* EdPoly, + FCoplanarInfo CoplanarInfo, + int32 LeafOutside, + FHBSPOps::ENodePlace ENodePlace, + UHBspPointsGrid* BspPoints, + UHBspPointsGrid* BspVectors +) +{ + EPolyNodeFilter FilterType; + + if( CoplanarInfo.iOriginalNode == INDEX_NONE ) + { + // Processing regular, non-coplanar polygons. + FilterType = LeafOutside ? F_OUTSIDE : F_INSIDE; + (this->*FilterFunc)( Model, iNode, EdPoly, FilterType, ENodePlace, BspPoints, BspVectors ); + } + else if( CoplanarInfo.ProcessingBack ) + { + // Finished filtering polygon through tree in back of parent coplanar. + DoneFilteringBack: + if ((!LeafOutside) && (!CoplanarInfo.FrontLeafOutside)) FilterType = F_COPLANAR_INSIDE; + else if (( LeafOutside) && ( CoplanarInfo.FrontLeafOutside)) FilterType = F_COPLANAR_OUTSIDE; + else if ((!LeafOutside) && ( CoplanarInfo.FrontLeafOutside)) FilterType = F_COSPATIAL_FACING_OUT; + else if (( LeafOutside) && (!CoplanarInfo.FrontLeafOutside)) FilterType = F_COSPATIAL_FACING_IN; + else + { + UE_LOG(LogHCsgUtils, Fatal, TEXT("FilterLeaf: Bad Locs")); + return; + } + (this->*FilterFunc)( Model, CoplanarInfo.iOriginalNode, EdPoly, FilterType, FHBSPOps::NODE_Plane, BspPoints, BspVectors ); + } + else + { + CoplanarInfo.FrontLeafOutside = LeafOutside; + + if( CoplanarInfo.iBackNode == INDEX_NONE ) + { + // Back tree is empty. + LeafOutside = CoplanarInfo.BackNodeOutside; + goto DoneFilteringBack; + } + else + { + // Call FilterEdPoly to filter through the back. This will result in + // another call to FilterLeaf with iNode = leaf this falls into in the + // back tree and EdPoly = the final EdPoly to insert. + CoplanarInfo.ProcessingBack=1; + FilterEdPoly( FilterFunc, Model, CoplanarInfo.iBackNode, EdPoly,CoplanarInfo, CoplanarInfo.BackNodeOutside, BspPoints, BspVectors ); + } + } +} + +// +// Filter an EdPoly through the Bsp recursively, calling FilterFunc +// for all chunks that fall into leaves. FCoplanarInfo is used to +// handle the tricky case of double-recursion for polys that must be +// filtered through a node's front, then filtered through the node's back, +// in order to handle coplanar CSG properly. +// +void UHCsgUtils::FilterEdPoly +( + BspFilterFunc FilterFunc, + UModel *Model, + int32 iNode, + FPoly *EdPoly, + FCoplanarInfo CoplanarInfo, + int32 Outside, + UHBspPointsGrid* BspPoints, + UHBspPointsGrid* BspVectors +) +{ + int32 SplitResult,iOurFront,iOurBack; + int32 NewFrontOutside,NewBackOutside; + + FilterLoop: + + // Split em. + FPoly TempFrontEdPoly,TempBackEdPoly; + SplitResult = EdPoly->SplitWithPlane + ( + Model->Points [Model->Verts[Model->Nodes[iNode].iVertPool].pVertex], + Model->Vectors[Model->Surfs[Model->Nodes[iNode].iSurf].vNormal], + &TempFrontEdPoly, + &TempBackEdPoly, + 0 + ); + + // Process split results. + if( SplitResult == SP_Front ) + { + Front: + + FBspNode *Node = &Model->Nodes[iNode]; + Outside = Outside || Node->IsCsg(); + + if( Node->iFront == INDEX_NONE ) + { + FilterLeaf(FilterFunc,Model,iNode,EdPoly,CoplanarInfo,Outside,FHBSPOps::NODE_Front, BspPoints, BspVectors); + } + else + { + iNode = Node->iFront; + goto FilterLoop; + } + } + else if( SplitResult == SP_Back ) + { + FBspNode *Node = &Model->Nodes[iNode]; + Outside = Outside && !Node->IsCsg(); + + if( Node->iBack == INDEX_NONE ) + { + FilterLeaf( FilterFunc, Model, iNode, EdPoly, CoplanarInfo, Outside, FHBSPOps::NODE_Back, BspPoints, BspVectors); + } + else + { + iNode=Node->iBack; + goto FilterLoop; + } + } + else if( SplitResult == SP_Coplanar ) + { + if( CoplanarInfo.iOriginalNode != INDEX_NONE ) + { + // This will happen once in a blue moon when a polygon is barely outside the + // coplanar threshold and is split up into a new polygon that is + // is barely inside the coplanar threshold. To handle this, just classify + // it as front and it will be handled propery. + FHBSPOps::GErrors++; +// UE_LOG(LogEditorBsp, Warning, TEXT("FilterEdPoly: Encountered out-of-place coplanar") ); + goto Front; + } + CoplanarInfo.iOriginalNode = iNode; + CoplanarInfo.iBackNode = INDEX_NONE; + CoplanarInfo.ProcessingBack = 0; + CoplanarInfo.BackNodeOutside = Outside; + NewFrontOutside = Outside; + + // See whether Node's iFront or iBack points to the side of the tree on the front + // of this polygon (will be as expected if this polygon is facing the same + // way as first coplanar in link, otherwise opposite). + if ((FVector(Model->Nodes[iNode].Plane) | (FVector)EdPoly->Normal) >= 0.0) + { + iOurFront = Model->Nodes[iNode].iFront; + iOurBack = Model->Nodes[iNode].iBack; + + if( Model->Nodes[iNode].IsCsg() ) + { + CoplanarInfo.BackNodeOutside = 0; + NewFrontOutside = 1; + } + } + else + { + iOurFront = Model->Nodes[iNode].iBack; + iOurBack = Model->Nodes[iNode].iFront; + + if( Model->Nodes[iNode].IsCsg() ) + { + CoplanarInfo.BackNodeOutside = 1; + NewFrontOutside = 0; + } + } + + // Process front and back. + if ((iOurFront==INDEX_NONE)&&(iOurBack==INDEX_NONE)) + { + // No front or back. + CoplanarInfo.ProcessingBack = 1; + CoplanarInfo.FrontLeafOutside = NewFrontOutside; + FilterLeaf + ( + FilterFunc, + Model, + iNode, + EdPoly, + CoplanarInfo, + CoplanarInfo.BackNodeOutside, + FHBSPOps::NODE_Plane, + BspPoints, + BspVectors + ); + } + else if( iOurFront==INDEX_NONE && iOurBack!=INDEX_NONE ) + { + // Back but no front. + CoplanarInfo.ProcessingBack = 1; + CoplanarInfo.iBackNode = iOurBack; + CoplanarInfo.FrontLeafOutside = NewFrontOutside; + + iNode = iOurBack; + Outside = CoplanarInfo.BackNodeOutside; + goto FilterLoop; + } + else + { + // Has a front and maybe a back. + + // Set iOurBack up to process back on next call to FilterLeaf, and loop + // to process front. Next call to FilterLeaf will set FrontLeafOutside. + CoplanarInfo.ProcessingBack = 0; + + // May be a node or may be INDEX_NONE. + CoplanarInfo.iBackNode = iOurBack; + + iNode = iOurFront; + Outside = NewFrontOutside; + goto FilterLoop; + } + } + else if( SplitResult == SP_Split ) + { + // Front half of split. + if( Model->Nodes[iNode].IsCsg() ) + { + NewFrontOutside = 1; + NewBackOutside = 0; + } + else + { + NewFrontOutside = Outside; + NewBackOutside = Outside; + } + + if( Model->Nodes[iNode].iFront==INDEX_NONE ) + { + FilterLeaf + ( + FilterFunc, + Model, + iNode, + &TempFrontEdPoly, + CoplanarInfo, + NewFrontOutside, + FHBSPOps::NODE_Front, + BspPoints, + BspVectors + ); + } + else + { + FilterEdPoly + ( + FilterFunc, + Model, + Model->Nodes[iNode].iFront, + &TempFrontEdPoly, + CoplanarInfo, + NewFrontOutside, + BspPoints, + BspVectors + ); + } + + // Back half of split. + if( Model->Nodes[iNode].iBack==INDEX_NONE ) + { + FilterLeaf + ( + FilterFunc, + Model, + iNode, + &TempBackEdPoly, + CoplanarInfo, + NewBackOutside, + FHBSPOps::NODE_Back, + BspPoints, + BspVectors + ); + } + else + { + FilterEdPoly + ( + FilterFunc, + Model, + Model->Nodes[iNode].iBack, + &TempBackEdPoly, + CoplanarInfo, + NewBackOutside, + BspPoints, + BspVectors + ); + } + } +} + +// +// Regular entry into FilterEdPoly (so higher-level callers don't have to +// deal with unnecessary info). Filters starting at root. +// +void UHCsgUtils::BspFilterFPoly( BspFilterFunc FilterFunc, UModel *Model, FPoly *EdPoly, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ) +{ + FCoplanarInfo StartingCoplanarInfo; + StartingCoplanarInfo.iOriginalNode = INDEX_NONE; + if( Model->Nodes.Num() == 0 ) + { + // If Bsp is empty, process at root. + (this->*FilterFunc)( Model, 0, EdPoly, Model->RootOutside ? F_OUTSIDE : F_INSIDE, FHBSPOps::NODE_Root, BspPoints, BspVectors ); + } + else + { + // Filter through Bsp. + FilterEdPoly( FilterFunc, Model, 0, EdPoly, StartingCoplanarInfo, Model->RootOutside, BspPoints, BspVectors ); + } +} + +int UHCsgUtils::bspNodeToFPoly +( + UModel* Model, + int32 iNode, + FPoly* EdPoly +) +{ + FPoly MasterEdPoly; + + FBspNode &Node = Model->Nodes[iNode]; + FBspSurf &Poly = Model->Surfs[Node.iSurf]; + FVert *VertPool = &Model->Verts[ Node.iVertPool ]; + + EdPoly->Base = Model->Points [Poly.pBase]; + EdPoly->Normal = Model->Vectors[Poly.vNormal]; + + EdPoly->PolyFlags = Poly.PolyFlags & ~(PF_EdCut | PF_EdProcessed | PF_Selected | PF_Memorized); + EdPoly->iLinkSurf = Node.iSurf; + EdPoly->Material = Poly.Material; + + EdPoly->Actor = Poly.Actor; + EdPoly->iBrushPoly = Poly.iBrushPoly; + + if( polyFindMaster(Model,Node.iSurf,MasterEdPoly) ) + EdPoly->ItemName = MasterEdPoly.ItemName; + else + EdPoly->ItemName = NAME_None; + + EdPoly->TextureU = Model->Vectors[Poly.vTextureU]; + EdPoly->TextureV = Model->Vectors[Poly.vTextureV]; + + EdPoly->LightMapScale = Poly.LightMapScale; + + EdPoly->LightmassSettings = Model->LightmassSettings[Poly.iLightmassIndex]; + + EdPoly->Vertices.Empty(); + + for(int32 VertexIndex = 0;VertexIndex < Node.NumVertices;VertexIndex++) + { + new(EdPoly->Vertices) FVector(Model->Points[VertPool[VertexIndex].pVertex]); + } + + if(EdPoly->Vertices.Num() < 3) + { + EdPoly->Vertices.Empty(); + } + else + { + // Remove colinear points and identical points (which will appear + // if T-joints were eliminated). + EdPoly->RemoveColinears(); + } + + return EdPoly->Vertices.Num(); +} + +/*--------------------------------------------------------------------------------------- + World filtering. +---------------------------------------------------------------------------------------*/ + +// +// Filter all relevant world polys through the brush. +// +void UHCsgUtils::FilterWorldThroughBrush +( + UModel* Model, + UModel* Brush, + EBrushType BrushType, + ECsgOper CSGOper, + int32 iNode, + FSphere* BrushSphere, + UHBspPointsGrid* BspPoints, + UHBspPointsGrid* BspVectors +) +{ + // Loop through all coplanars. + while( iNode != INDEX_NONE ) + { + // Get surface. + int32 iSurf = Model->Nodes[iNode].iSurf; + + // Skip new nodes and their children, which are guaranteed new. + if( Model->Nodes[iNode].NodeFlags & NF_IsNew ) + return; + + // Sphere reject. + int DoFront = 1, DoBack = 1; + if( BrushSphere ) + { + float Dist = Model->Nodes[iNode].Plane.PlaneDot((FVector3f)BrushSphere->Center); + DoFront = (Dist >= -BrushSphere->W); + DoBack = (Dist <= +BrushSphere->W); + } + + // Process only polys that aren't empty. + FPoly TempEdPoly; + if( DoFront && DoBack && (GEditor->bspNodeToFPoly(Model,iNode,&TempEdPoly)>0) ) + { + TempEdPoly.Actor = Model->Surfs[iSurf].Actor; + TempEdPoly.iBrushPoly = Model->Surfs[iSurf].iBrushPoly; + + if( BrushType==Brush_Add || BrushType==Brush_Subtract ) + { + // Add and subtract work the same in this step. + GNode = iNode; + GModel = Model; + GDiscarded = 0; + GNumNodes = Model->Nodes.Num(); + + // Find last coplanar in chain. + GLastCoplanar = iNode; + while( Model->Nodes[GLastCoplanar].iPlane != INDEX_NONE ) + GLastCoplanar = Model->Nodes[GLastCoplanar].iPlane; + + // Do the filter operation. + BspFilterFPoly + ( + BrushType==Brush_Add ? &UHCsgUtils::AddWorldToBrushFunc : &UHCsgUtils::SubtractWorldToBrushFunc, + Brush, + &TempEdPoly, + BspPoints, + BspVectors + ); + + if( GDiscarded == 0 ) + { + // Get rid of all the fragments we added. + Model->Nodes[GLastCoplanar].iPlane = INDEX_NONE; + const bool bAllowShrinking = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + Model->Nodes.RemoveAt( GNumNodes, Model->Nodes.Num()-GNumNodes, bAllowShrinking ? EAllowShrinking::Yes : EAllowShrinking::No ); +#else + Model->Nodes.RemoveAt(GNumNodes, Model->Nodes.Num() - GNumNodes, bAllowShrinking); +#endif + } + else + { + // Tag original world poly for deletion; has been deleted or replaced by partial fragments. + if( GModel->Nodes[GNode].NumVertices ) + { + GModel->Nodes[GNode].NumVertices = 0; + } + } + } + else if( CSGOper == CSG_Intersect ) + { + BspFilterFPoly( &UHCsgUtils::IntersectWorldWithBrushFunc, Brush, &TempEdPoly, BspPoints, BspVectors ); + } + else if( CSGOper == CSG_Deintersect ) + { + BspFilterFPoly( &UHCsgUtils::DeIntersectWorldWithBrushFunc, Brush, &TempEdPoly, BspPoints, BspVectors ); + } + } + + // Now recurse to filter all of the world's children nodes. + if( DoFront && (Model->Nodes[iNode].iFront != INDEX_NONE)) FilterWorldThroughBrush + ( + Model, + Brush, + BrushType, + CSGOper, + Model->Nodes[iNode].iFront, + BrushSphere, + BspPoints, + BspVectors + ); + if( DoBack && (Model->Nodes[iNode].iBack != INDEX_NONE) ) FilterWorldThroughBrush + ( + Model, + Brush, + BrushType, + CSGOper, + Model->Nodes[iNode].iBack, + BrushSphere, + BspPoints, + BspVectors + ); + iNode = Model->Nodes[iNode].iPlane; + } +} + +void UHCsgUtils::RebuildModelFromBrushes(UModel* Model, TArray& Brushes, bool bTreatMovableBrushesAsStatic) +{ + if (!IsValid(Model)) + return; + + UHCsgUtils* CsgUtils = NewObject(); + int32 CsgErrors = 0; + + UHBspPointsGrid* BspPoints = UHBspPointsGrid::Create(50.0f, THRESH_POINTS_ARE_SAME); + UHBspPointsGrid* BspVectors = UHBspPointsGrid::Create(1/16.0f, FMath::Max(THRESH_NORMALS_ARE_SAME, THRESH_VECTORS_ARE_NEAR)); + + // Empty the model out. + const int32 NumPoints = Model->Points.Num(); + const int32 NumNodes = Model->Nodes.Num(); + const int32 NumVerts = Model->Verts.Num(); + const int32 NumVectors = Model->Vectors.Num(); + const int32 NumSurfs = Model->Surfs.Num(); + + Model->Modify(); + Model->EmptyModel(1, 1); + + // Reserve arrays an eighth bigger than the previous allocation + Model->Points.Empty(NumPoints + NumPoints / 8); + Model->Nodes.Empty(NumNodes + NumNodes / 8); + Model->Verts.Empty(NumVerts + NumVerts / 8); + Model->Vectors.Empty(NumVectors + NumVectors / 8); + Model->Surfs.Empty(NumSurfs + NumSurfs / 8); + + // Build list of all static brushes, first structural brushes and portals + TArray StaticBrushes; + for (ABrush* Brush : Brushes) + { + if ((Brush && (Brush->IsStaticBrush() || bTreatMovableBrushesAsStatic) && !FActorEditorUtils::IsABuilderBrush(Brush)) && + (!(Brush->PolyFlags & PF_Semisolid) || (Brush->BrushType != Brush_Add) || (Brush->PolyFlags & PF_Portal))) + { + StaticBrushes.Add(Brush); + + // Treat portals as solids for cutting. + if (Brush->PolyFlags & PF_Portal) + { + Brush->PolyFlags = (Brush->PolyFlags & ~PF_Semisolid) | PF_NotSolid; + } + } + } + + // Next append all detail brushes + for (ABrush* Brush : Brushes) + { + if (Brush && Brush->IsStaticBrush() && !FActorEditorUtils::IsABuilderBrush(Brush) && + (Brush->PolyFlags & PF_Semisolid) && !(Brush->PolyFlags & PF_Portal) && (Brush->BrushType == Brush_Add)) + { + StaticBrushes.Add(Brush); + } + } + + // Build list of dynamic brushes + TArray DynamicBrushes; + if (!bTreatMovableBrushesAsStatic) + { + for (ABrush* DynamicBrush : Brushes) + { + if (DynamicBrush && DynamicBrush->Brush && !DynamicBrush->IsStaticBrush()) + { + DynamicBrushes.Add(DynamicBrush); + } + } + } + + FScopedSlowTask SlowTask(StaticBrushes.Num() + DynamicBrushes.Num()); + SlowTask.MakeDialogDelayed(3.0f); + + // Compose all static brushes + for (ABrush* Brush : StaticBrushes) + { + SlowTask.EnterProgressFrame(1); + Brush->Modify(); + int32 Errors = CsgUtils->ComposeBrushCSG(Brush, Model, Brush->PolyFlags, (EBrushType)Brush->BrushType, CSG_None, false, true, false, false, BspPoints, BspVectors); + if (Errors > 1) + CsgErrors += Errors - 1; + } + + // Rebuild dynamic brush BSP's (if they weren't handled earlier) + for (ABrush* DynamicBrush : DynamicBrushes) + { + SlowTask.EnterProgressFrame(1); + UHBspPointsGrid* LocalBspPoints = UHBspPointsGrid::Create(50.0f, THRESH_POINTS_ARE_SAME); + UHBspPointsGrid* LocalBspVectors = UHBspPointsGrid::Create(1 / 16.0f, FMath::Max(THRESH_NORMALS_ARE_SAME, THRESH_VECTORS_ARE_NEAR)); + + FHBSPOps::csgPrepMovingBrush(DynamicBrush, LocalBspPoints, LocalBspVectors); + } +} + + + +UModel* UHCsgUtils::BuildModelFromBrushes(TArray& Brushes) +{ + // Generally UModels are initialized using ABrush. Here we manually + // initialize using relevant parts from + UModel* OutModel = NewObject(); + OutModel->SetFlags(RF_Transactional); + OutModel->RootOutside = true; + OutModel->EmptyModel(1,1); + OutModel->UpdateVertices(); + + if (!IsValid(OutModel)) + return nullptr; + + // Can we combine the brushes without modifying the actors here ...? + + //FVector Location(0.0f, 0.0f, 0.0f); + //FRotator Rotation(0.0f, 0.0f, 0.0f); + //for(int32 BrushesIdx = 0; BrushesIdx < Brushes.Num(); ++BrushesIdx ) + //{ + // // Cache the location and rotation. + // Location = Brushes[BrushesIdx]->GetActorLocation(); + // Rotation = Brushes[BrushesIdx]->GetActorRotation(); + + + // // Leave the actor's rotation but move it to origin so the Static Mesh will generate correctly. + // Brushes[BrushesIdx]->TeleportTo(Location - InPivotLocation, Rotation, false, true); + //} + + RebuildModelFromBrushes(OutModel, Brushes, true); + //GEditor->bspBuildFPolys(OutModel, true, 0); + + //if (0 < ConversionTempModel->Polys->Element.Num()) + //{ + // UStaticMesh* NewMesh = CreateStaticMeshFromBrush(Pkg, ObjName, NULL, ConversionTempModel); + // NewActor = FActorFactoryAssetProxy::AddActorForAsset( NewMesh ); + + // NewActor->Modify(); + + // NewActor->InvalidateLightingCache(); + // NewActor->PostEditChange(); + // NewActor->PostEditMove( true ); + // NewActor->Modify(); + // ULayersSubsystem* LayersSubsystem = GetEditorSubsystem(); + // LayersSubsystem->InitializeNewActorLayers(NewActor); + + // // Teleport the new actor to the old location but not the old rotation. The static mesh is built to the rotation already. + // NewActor->TeleportTo(InPivotLocation, FRotator(0.0f, 0.0f, 0.0f), false, true); + + // // Destroy the old brushes. + // for( int32 BrushIdx = 0; BrushIdx < InBrushesToConvert.Num(); ++BrushIdx ) + // { + // LayersSubsystem->DisassociateActorFromLayers(InBrushesToConvert[BrushIdx]); + // GWorld->EditorDestroyActor( InBrushesToConvert[BrushIdx], true ); + // } + + // // Notify the asset registry + // FAssetRegistryModule::AssetCreated(NewMesh); + //} + + //ConversionTempModel->EmptyModel(1, 1); + //RebuildAlteredBSP(); + //RedrawLevelEditingViewports(); + + //return NewActor; + + return OutModel; +} + +int UHCsgUtils::ComposeBrushCSG +( + ABrush* Actor, + UModel* Model, + uint32 PolyFlags, + EBrushType BrushType, + ECsgOper CSGOper, + bool bBuildBounds, + bool bMergePolys, + bool bReplaceNULLMaterialRefs, + bool bShowProgressBar, /*=true*/ + UHBspPointsGrid* BspPoints, + UHBspPointsGrid* BspVectors +) +{ + uint32 NotPolyFlags = 0; + int32 NumPolysFromBrush=0,i,j,ReallyBig; + UModel* Brush = Actor->Brush; + int32 Errors = 0; + + // Make sure we're in an acceptable state. + if( !Brush ) + { + return 0; + } + + // Non-solid and semisolid stuff can only be added. + if( BrushType != Brush_Add ) + { + NotPolyFlags |= (PF_Semisolid | PF_NotSolid); + } + + TempModel->EmptyModel(1,1); + + // Update status. + ReallyBig = (Brush->Polys->Element.Num() > 200) && bShowProgressBar; + if( ReallyBig ) + { + FText Description = NSLOCTEXT("UnrealEd", "PerformingCSGOperation", "Performing CSG operation"); + + if (BrushType != Brush_MAX) + { + if (BrushType == Brush_Add) + { + Description = NSLOCTEXT("UnrealEd", "AddingBrushToWorld", "Adding brush to world"); + } + else if (BrushType == Brush_Subtract) + { + Description = NSLOCTEXT("UnrealEd", "SubtractingBrushFromWorld", "Subtracting brush from world"); + } + } + else if (CSGOper != CSG_None) + { + if (CSGOper == CSG_Intersect) + { + Description = NSLOCTEXT("UnrealEd", "IntersectingBrushWithWorld", "Intersecting brush with world"); + } + else if (CSGOper == CSG_Deintersect) + { + Description = NSLOCTEXT("UnrealEd", "DeintersectingBrushWithWorld", "Deintersecting brush with world"); + } + } + + GWarn->BeginSlowTask( Description, true ); + // Transform original brush poly into same coordinate system as world + // so Bsp filtering operations make sense. + GWarn->StatusUpdate(0, 0, NSLOCTEXT("UnrealEd", "Transforming", "Transforming")); + } + + + //UMaterialInterface* SelectedMaterialInstance = GEditor->GetSelectedObjects()->GetTop(); + UMaterialInterface* SelectedMaterialInstance = nullptr; + + const FVector Scale = Actor->GetActorScale(); + const FRotator Rotation = Actor->GetActorRotation(); + const FVector Location = Actor->GetActorLocation(); + + const bool bIsMirrored = (Scale.X * Scale.Y * Scale.Z < 0.0f); + + // Cache actor transform which is used for the geometry being built + Brush->OwnerLocationWhenLastBuilt = (FVector3f)Location; + Brush->OwnerRotationWhenLastBuilt = Rotation; + Brush->OwnerScaleWhenLastBuilt = (FVector3f)Scale; + Brush->bCachedOwnerTransformValid = true; + + for( i=0; iPolys->Element.Num(); i++ ) + { + FPoly& CurrentPoly = Brush->Polys->Element[i]; + + // Set texture the first time. + if ( bReplaceNULLMaterialRefs ) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + UMaterialInterface* PolyMat = CurrentPoly.Material.Get(); +#else + UMaterialInterface*& PolyMat = CurrentPoly.Material; +#endif + if ( !PolyMat || PolyMat == UMaterial::GetDefaultMaterial(MD_Surface) ) + { + CurrentPoly.Material = SelectedMaterialInstance; + } + } + + // Get the brush poly. + FPoly DestEdPoly = CurrentPoly; + check(CurrentPoly.iLinkPolys->Element.Num()); + + // Set its backward brush link. + DestEdPoly.Actor = Actor; + DestEdPoly.iBrushPoly = i; + + // Update its flags. + DestEdPoly.PolyFlags = (DestEdPoly.PolyFlags | PolyFlags) & ~NotPolyFlags; + + // Set its internal link. + if (DestEdPoly.iLink == INDEX_NONE) + { + DestEdPoly.iLink = i; + } + + // Transform it. + DestEdPoly.Scale((FVector3f)Scale); + DestEdPoly.Rotate(FRotator3f(Rotation)); // LWC_TODO: Precision loss? + DestEdPoly.Transform((FVector3f)Location); // LWC_TODO: Precision loss + + // Reverse winding and normal if the parent brush is mirrored + if (bIsMirrored) + { + DestEdPoly.Reverse(); + DestEdPoly.CalcNormal(); + } + + // Add poly to the temp model. + new(TempModel->Polys->Element)FPoly( DestEdPoly ); + } + if( ReallyBig ) GWarn->StatusUpdate( 0, 0, NSLOCTEXT("UnrealEd", "FilteringBrush", "Filtering brush") ); + + // Pass the brush polys through the world Bsp. + if( CSGOper==CSG_Intersect || CSGOper==CSG_Deintersect ) + { + // Empty the brush. + Brush->EmptyModel(1,1); + + // Intersect and deintersect. + for( i=0; iPolys->Element.Num(); i++ ) + { + FPoly EdPoly = TempModel->Polys->Element[i]; + GModel = Brush; + // TODO: iLink / iLinkSurf in EdPoly / TempModel->Polys->Element[i] ? + BspFilterFPoly( CSGOper==CSG_Intersect ? &UHCsgUtils::IntersectBrushWithWorldFunc : &UHCsgUtils::DeIntersectBrushWithWorldFunc, Model, &EdPoly, BspPoints, BspVectors ); + } + NumPolysFromBrush = Brush->Polys->Element.Num(); + } + else + { + // Add and subtract. + TMap SurfaceIndexRemap; + for( i=0; iPolys->Element.Num(); i++ ) + { + FPoly EdPoly = TempModel->Polys->Element[i]; + + // Mark the polygon as non-cut so that it won't be harmed unless it must + // be split, and set iLink so that BspAddNode will know to add its information + // if a node is added based on this poly. + EdPoly.PolyFlags &= ~(PF_EdCut); + const int32* SurfaceIndexPtr = SurfaceIndexRemap.Find(EdPoly.iLink); + if (SurfaceIndexPtr == nullptr) + { + const int32 NewSurfaceIndex = Model->Surfs.Num(); + SurfaceIndexRemap.Add(EdPoly.iLink, NewSurfaceIndex); + EdPoly.iLinkSurf = TempModel->Polys->Element[i].iLinkSurf = NewSurfaceIndex; + } + else + { + EdPoly.iLinkSurf = TempModel->Polys->Element[i].iLinkSurf = *SurfaceIndexPtr; + } + + // Filter brush through the world. + BspFilterFPoly( BrushType==Brush_Add ? &UHCsgUtils::AddBrushToWorldFunc : &UHCsgUtils::SubtractBrushFromWorldFunc, Model, &EdPoly, BspPoints, BspVectors ); + } + } + if( Model->Nodes.Num() && !(PolyFlags & (PF_NotSolid | PF_Semisolid)) ) + { + // Quickly build a Bsp for the brush, tending to minimize splits rather than balance + // the tree. We only need the cutting planes, though the entire Bsp struct (polys and + // all) is built. + + /*FHBspPointsGrid* LevelModelPointsGrid = FHBspPointsGrid::GBspPoints; + FHBspPointsGrid* LevelModelVectorsGrid = FHBspPointsGrid::GBspVectors;*/ + + // For the bspBuild call, temporarily create a new pair of BspPointsGrids for the TempModel. + UHBspPointsGrid* TempBspPoints = UHBspPointsGrid::Create(50.0f, THRESH_POINTS_ARE_SAME); + UHBspPointsGrid* TempBspVectors = UHBspPointsGrid::Create(1 / 16.0f, FMath::Max(THRESH_NORMALS_ARE_SAME, THRESH_VECTORS_ARE_NEAR)); + /*FHBspPointsGrid::GBspPoints = BspPoints.Get(); + FHBspPointsGrid::GBspVectors = BspVectors.Get();*/ + + if( ReallyBig ) GWarn->StatusUpdate( 0, 0, NSLOCTEXT("UnrealEd", "BuildingBSP", "Building BSP") ); + + FHBSPOps::bspBuild( TempModel, FHBSPOps::BSP_Lame, 0, 70, 1, 0, TempBspPoints, TempBspVectors ); + + // Reinstate the original BspPointsGrids used for building the level Model. + /*FHBspPointsGrid::GBspPoints = LevelModelPointsGrid; + FHBspPointsGrid::GBspVectors = LevelModelVectorsGrid;*/ + + if( ReallyBig ) GWarn->StatusUpdate( 0, 0, NSLOCTEXT("UnrealEd", "FilteringWorld", "Filtering world") ); + GModel = Brush; + TempModel->BuildBound(); + + FSphere BrushSphere = TempModel->Bounds.GetSphere(); + FilterWorldThroughBrush( Model, TempModel, BrushType, CSGOper, 0, &BrushSphere, BspPoints, BspVectors); + } + if( CSGOper==CSG_Intersect || CSGOper==CSG_Deintersect ) + { + if( ReallyBig ) GWarn->StatusUpdate( 0, 0, NSLOCTEXT("UnrealEd", "AdjustingBrush", "Adjusting brush") ); + + // Link polys obtained from the original brush. + for( i=NumPolysFromBrush-1; i>=0; i-- ) + { + FPoly *DestEdPoly = &Brush->Polys->Element[i]; + for( j=0; jiLink == Brush->Polys->Element[j].iLink ) + { + DestEdPoly->iLink = j; + break; + } + } + if( j >= i ) DestEdPoly->iLink = i; + } + + // Link polys obtained from the world. + for( i=Brush->Polys->Element.Num()-1; i>=NumPolysFromBrush; i-- ) + { + FPoly *DestEdPoly = &Brush->Polys->Element[i]; + for( j=NumPolysFromBrush; jiLink == Brush->Polys->Element[j].iLink ) + { + DestEdPoly->iLink = j; + break; + } + } + if( j >= i ) DestEdPoly->iLink = i; + } + Brush->Linked = 1; + + // Detransform the obtained brush back into its original coordinate system. + for (i = 0; i < Brush->Polys->Element.Num(); i++) + { + FPoly* DestEdPoly = &Brush->Polys->Element[i]; + DestEdPoly->Transform((FVector3f)-Location); + DestEdPoly->Rotate(FRotator3f(Rotation.GetInverse())); + DestEdPoly->Scale(FVector3f(1.0f) / (FVector3f)Scale); + DestEdPoly->Fix(); + DestEdPoly->Actor = NULL; + DestEdPoly->iBrushPoly = i; + } + } + + if( BrushType==Brush_Add || BrushType==Brush_Subtract ) + { + // Clean up nodes, reset node flags. + bspCleanup( Model ); + + // Rebuild bounding volumes. + if( bBuildBounds ) + { + FHBSPOps::bspBuildBounds( Model ); + } + } + + Brush->NumUniqueVertices = TempModel->Points.Num(); + // Release TempModel. + TempModel->EmptyModel(1,1); + + // Merge coplanars if needed. + if( CSGOper==CSG_Intersect || CSGOper==CSG_Deintersect ) + { + if( ReallyBig ) + { + GWarn->StatusUpdate( 0, 0, NSLOCTEXT("UnrealEd", "Merging", "Merging") ); + } + if( bMergePolys ) + { + bspMergeCoplanars( Brush, 1, 0 ); + } + } + if( ReallyBig ) + { + GWarn->EndSlowTask(); + } + + return 1 + FHBSPOps::GErrors; +} + +/*---------------------------------------------------------------------------- + EdPoly building and compacting. +----------------------------------------------------------------------------*/ + +// +// Trys to merge two polygons. If they can be merged, replaces Poly1 and emptys Poly2 +// and returns 1. Otherwise, returns 0. +// +int UHCsgUtils::TryToMerge( FPoly *Poly1, FPoly *Poly2 ) +{ + // Find one overlapping point. + int32 Start1 = 0, Start2 = 0; + for (Start1 = 0; Start1 < Poly1->Vertices.Num(); Start1++) + for (Start2 = 0; Start2 < Poly2->Vertices.Num(); Start2++) + if (FVector3f::PointsAreSame(Poly1->Vertices[Start1], Poly2->Vertices[Start2])) + goto FoundOverlap; + return 0; +FoundOverlap: + + // Wrap around trying to merge. + int32 End1 = Start1; + int32 End2 = Start2; + int32 Test1 = Start1 + 1; if (Test1 >= Poly1->Vertices.Num()) Test1 = 0; + int32 Test2 = Start2 - 1; if (Test2 < 0) Test2 = Poly2->Vertices.Num() - 1; + if (FVector3f::PointsAreSame(Poly1->Vertices[Test1], Poly2->Vertices[Test2])) + { + End1 = Test1; + Start2 = Test2; + } + else + { + Test1 = Start1 - 1; if (Test1 < 0) Test1 = Poly1->Vertices.Num() - 1; + Test2 = Start2 + 1; if (Test2 >= Poly2->Vertices.Num()) Test2 = 0; + if (FVector3f::PointsAreSame(Poly1->Vertices[Test1], Poly2->Vertices[Test2])) + { + Start1 = Test1; + End2 = Test2; + } + else return 0; + } + + // Build a new edpoly containing both polygons merged. + FPoly NewPoly = *Poly1; + NewPoly.Vertices.Empty(); + int32 Vertex = End1; + for (int32 i = 0; i < Poly1->Vertices.Num(); i++) + { + new(NewPoly.Vertices) FVector3f(Poly1->Vertices[Vertex]); + if (++Vertex >= Poly1->Vertices.Num()) + Vertex = 0; + } + Vertex = End2; + for (int32 i = 0; i < (Poly2->Vertices.Num() - 2); i++) + { + if (++Vertex >= Poly2->Vertices.Num()) + Vertex = 0; + new(NewPoly.Vertices) FVector3f(Poly2->Vertices[Vertex]); + } + + // Remove colinear vertices and check convexity. + if (NewPoly.RemoveColinears()) + { + *Poly1 = NewPoly; + Poly2->Vertices.Empty(); + return true; + } + else return 0; +} + +// +// Merge all polygons in coplanar list that can be merged convexly. +// +void UHCsgUtils::MergeCoplanars( UModel* Model, int32* PolyList, int32 PolyCount ) +{ + int32 MergeAgain = 1; + while( MergeAgain ) + { + MergeAgain = 0; + for( int32 i=0; iPolys->Element[PolyList[i]]; + if( Poly1.Vertices.Num() > 0 ) + { + for( int32 j=i+1; jPolys->Element[PolyList[j]]; + if( Poly2.Vertices.Num() > 0 ) + { + if( TryToMerge( &Poly1, &Poly2 ) ) + MergeAgain=1; + } + } + } + } + } +} + +void UHCsgUtils::bspMergeCoplanars( UModel* Model, bool RemapLinks, bool MergeDisparateTextures ) +{ + int32 OriginalNum = Model->Polys->Element.Num(); + + // Mark all polys as unprocessed. + for (int32 i = 0; i < Model->Polys->Element.Num(); i++) + Model->Polys->Element[i].PolyFlags &= ~PF_EdProcessed; + + // Find matching coplanars and merge them. + FMemMark Mark(FMemStack::Get()); + int32* PolyList = new(FMemStack::Get(), Model->Polys->Element.Num())int32; + int32 n = 0; + for (int32 i = 0; i < Model->Polys->Element.Num(); i++) + { + FPoly* EdPoly = &Model->Polys->Element[i]; + if (EdPoly->Vertices.Num() > 0 && !(EdPoly->PolyFlags & PF_EdProcessed)) + { + int32 PolyCount = 0; + PolyList[PolyCount++] = i; + EdPoly->PolyFlags |= PF_EdProcessed; + for (int32 j = i + 1; j < Model->Polys->Element.Num(); j++) + { + FPoly* OtherPoly = &Model->Polys->Element[j]; + if (OtherPoly->iLink == EdPoly->iLink && OtherPoly->Vertices.Num()) + { + float Dist = (OtherPoly->Vertices[0] - EdPoly->Vertices[0]) | EdPoly->Normal; + if + (Dist > -0.001 + && Dist < 0.001 + && (OtherPoly->Normal | EdPoly->Normal)>0.9999 + && (MergeDisparateTextures + || (FVector::PointsAreNear((FVector)OtherPoly->TextureU, (FVector)EdPoly->TextureU, THRESH_VECTORS_ARE_NEAR) + && FVector::PointsAreNear((FVector)OtherPoly->TextureV, (FVector)EdPoly->TextureV, THRESH_VECTORS_ARE_NEAR)))) + { + OtherPoly->PolyFlags |= PF_EdProcessed; + PolyList[PolyCount++] = j; + } + } + } + if (PolyCount > 1) + { + MergeCoplanars(Model, PolyList, PolyCount); + n++; + } + } + } + // UE_LOG(LogEditorBsp, Log, TEXT("Found %i coplanar sets in %i"), n, Model->Polys->Element.Num() ); + Mark.Pop(); + + // Get rid of empty EdPolys while remapping iLinks. + FMemMark Mark2(FMemStack::Get()); + int32 j = 0; + int32* Remap = new(FMemStack::Get(), Model->Polys->Element.Num())int32; + for (int32 i = 0; i < Model->Polys->Element.Num(); i++) + { + if (Model->Polys->Element[i].Vertices.Num()) + { + Remap[i] = j; + Model->Polys->Element[j] = Model->Polys->Element[i]; + j++; + } + } + Model->Polys->Element.RemoveAt(j, Model->Polys->Element.Num() - j); + if (RemapLinks) + { + for (int32 i = 0; i < Model->Polys->Element.Num(); i++) + { + if (Model->Polys->Element[i].iLink != INDEX_NONE) + { + CA_SUPPRESS(6001); // warning C6001: Using uninitialized memory '*Remap'. + Model->Polys->Element[i].iLink = Remap[Model->Polys->Element[i].iLink]; + } + } + } + // UE_LOG(LogEditorBsp, Log, TEXT("BspMergeCoplanars reduced %i->%i"), OriginalNum, Model->Polys->Element.Num() ); + Mark2.Pop(); +} + +bool UHCsgUtils::polyFindMaster(UModel* InModel, int32 iSurf, FPoly &Poly) +{ + FBspSurf &Surf = InModel->Surfs[iSurf]; + if( !Surf.Actor || !Surf.Actor->Brush->Polys->Element.IsValidIndex(Surf.iBrushPoly) ) + { + return false; + } + else + { + Poly = Surf.Actor->Brush->Polys->Element[Surf.iBrushPoly]; + return true; + } +} + +void UHCsgUtils::CleanupNodes( UModel *Model, int32 iNode, int32 iParent ) +{ + FBspNode *Node = &Model->Nodes[iNode]; + + // Transactionally empty vertices of tag-for-empty nodes. + Node->NodeFlags &= ~(NF_IsNew | NF_IsFront | NF_IsBack); + + // Recursively clean up front, back, and plane nodes. + if( Node->iFront != INDEX_NONE ) CleanupNodes( Model, Node->iFront, iNode ); + if( Node->iBack != INDEX_NONE ) CleanupNodes( Model, Node->iBack , iNode ); + if( Node->iPlane != INDEX_NONE ) CleanupNodes( Model, Node->iPlane, iNode ); + + // Reload Node since the recusive call aliases it. + Node = &Model->Nodes[iNode]; + + // If this is an empty node with a coplanar, replace it with the coplanar. + if( Node->NumVertices==0 && Node->iPlane!=INDEX_NONE ) + { + FBspNode* PlaneNode = &Model->Nodes[ Node->iPlane ]; + + // Stick our front, back, and parent nodes on the coplanar. + if( (Node->Plane | PlaneNode->Plane) >= 0.0 ) + { + PlaneNode->iFront = Node->iFront; + PlaneNode->iBack = Node->iBack; + } + else + { + PlaneNode->iFront = Node->iBack; + PlaneNode->iBack = Node->iFront; + } + + if( iParent == INDEX_NONE ) + { + // This node is the root. + *Node = *PlaneNode; // Replace root. + PlaneNode->NumVertices = 0; // Mark as unused. + } + else + { + // This is a child node. + FBspNode *ParentNode = &Model->Nodes[iParent]; + + if ( ParentNode->iFront == iNode ) ParentNode->iFront = Node->iPlane; + else if ( ParentNode->iBack == iNode ) ParentNode->iBack = Node->iPlane; + else if ( ParentNode->iPlane == iNode ) ParentNode->iPlane = Node->iPlane; + else UE_LOG(LogHCsgUtils, Fatal, TEXT("CleanupNodes: Parent and child are unlinked")); + } + } + else if( Node->NumVertices == 0 && ( Node->iFront==INDEX_NONE || Node->iBack==INDEX_NONE ) ) + { + // Delete empty nodes with no fronts or backs. + // Replace empty nodes with only fronts. + // Replace empty nodes with only backs. + int32 iReplacementNode; + if ( Node->iFront != INDEX_NONE ) iReplacementNode = Node->iFront; + else if( Node->iBack != INDEX_NONE ) iReplacementNode = Node->iBack; + else iReplacementNode = INDEX_NONE; + + if( iParent == INDEX_NONE ) + { + // Root. + if( iReplacementNode == INDEX_NONE ) + { + Model->Nodes.Empty(); + } + else + { + *Node = Model->Nodes[iReplacementNode]; + } + } + else + { + // Regular node. + FBspNode *ParentNode = &Model->Nodes[iParent]; + + if ( ParentNode->iFront == iNode ) ParentNode->iFront = iReplacementNode; + else if( ParentNode->iBack == iNode ) ParentNode->iBack = iReplacementNode; + else if( ParentNode->iPlane == iNode ) ParentNode->iPlane = iReplacementNode; + else UE_LOG(LogHCsgUtils, Fatal, TEXT("CleanupNodes: Parent and child are unlinked")); + } + } +} + + +void UHCsgUtils::bspCleanup( UModel *Model ) +{ + if( Model->Nodes.Num() > 0 ) + CleanupNodes( Model, 0, INDEX_NONE ); +} + + +``` + +`Source/HoudiniEngine/Private/HCsgUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#include "HBSPOps.h" +#include "Engine/Brush.h" +#include "Model.h" + +#include "HCsgUtils.generated.h" + +//USTRUCT() +//struct FHCsgContext +//{ +// GENERATED_BODY() +// +// int32 Errors; +// +// +// UPROPERTY() +// class UModel* TempModel; +// +// UPROPERTY() +// class UModel* ConversionTempModel; +//}; + +// This HCsgUtils is one big fork of the codebase located UnrealEd/Private/EditorBsp.cpp. +// The main purpose was to remove parts of the code that store state in global/static variables as well +// as dependency on editor state (such as retrieving selected brushes). +UCLASS() +class HOUDINIENGINE_API UHCsgUtils : public UObject +{ + GENERATED_BODY() +public: + + UHCsgUtils(); + + /** + * Builds up a model from a set of brushes. Used by RebuildLevel. + * + * @param Model The model to be rebuilt. + * @param bSelectedBrushesOnly Use all brushes in the current level or just the selected ones?. + * @param bTreatMovableBrushesAsStatic Treat moveable brushes as static?. + */ + static void RebuildModelFromBrushes(UModel* Model, TArray& Brushes, bool bTreatMovableBrushesAsStatic); + + /** + * Converts passed in brushes into a single static mesh actor. + * Note: This replaces all the brushes with a single actor. This actor will not be attached to anything unless a single brush was converted. + * + * @param InStaticMeshPackageName The name to save the brushes to. + * @param InBrushesToConvert A list of brushes being converted. + * + * @return Returns the newly created actor with the newly created static mesh. + */ + static UModel* BuildModelFromBrushes(TArray& Brushes); + + /** + * Forked version of UEditorEngine::bspBrushCSG() from UnrealEd/Private/EditorBsp.cpp. + * + * Apply the appropriate CSG operation required in order to compose the brush actor onto the given model. + * + * @param Actor The brush actor to apply. + * @param Model The model to apply the CSG operation to; typically the world's model. + * @param PolyFlags PolyFlags to set on brush's polys. + * @param BrushType The type of brush. + * @param CSGOper The CSG operation to perform. + * @param bBuildBounds If true, updates bounding volumes on Model for CSG_Add or CSG_Subtract operations. + * @param bMergePolys If true, coplanar polygons are merged for CSG_Intersect or CSG_Deintersect operations. + * @param bReplaceNULLMaterialRefs If true, replace NULL material references with a reference to the GB-selected material. + * @param bShowProgressBar If true, display progress bar for complex brushes + * @return 0 if nothing happened, 1 if the operation was error-free, or 1+N if N CSG errors occurred. + */ + int ComposeBrushCSG( + ABrush* Actor, + UModel* Model, + uint32 PolyFlags, + EBrushType BrushType, + ECsgOper CSGOper, + bool bBuildBounds, + bool bMergePolys, + bool bReplaceNULLMaterialRefs, + bool bShowProgressBar, /*=true*/ + UHBspPointsGrid* BspPoints, + UHBspPointsGrid* BspVectors + ); + +protected: + // + // Status of filtered polygons: + // + enum EPolyNodeFilter + { + F_OUTSIDE = 0, // Leaf is an exterior leaf (visible to viewers). + F_INSIDE = 1, // Leaf is an interior leaf (non-visible, hidden behind backface). + F_COPLANAR_OUTSIDE = 2, // Poly is coplanar and in the exterior (visible to viewers). + F_COPLANAR_INSIDE = 3, // Poly is coplanar and inside (invisible to viewers). + F_COSPATIAL_FACING_IN = 4, // Poly is coplanar, cospatial, and facing in. + F_COSPATIAL_FACING_OUT = 5, // Poly is coplanar, cospatial, and facing out. + }; + + + // + // Information used by FilterEdPoly. + // + class FCoplanarInfo + { + public: + int32 iOriginalNode; + int32 iBackNode; + int BackNodeOutside; + int FrontLeafOutside; + int ProcessingBack; + }; + + // + // Generic filter function called by BspFilterEdPolys. A and B are pointers + // to any integers that your specific routine requires (or NULL if not needed). + // + typedef void (UHCsgUtils::*BspFilterFunc) + ( + UModel* Model, + int32 iNode, + FPoly* EdPoly, + EPolyNodeFilter Leaf, + FHBSPOps::ENodePlace ENodePlace, + UHBspPointsGrid* BspPoints, + UHBspPointsGrid* BspVectors + ); + + // + // State shared between bspBrushCSG and AddWorldToBrushFunc. These are very + // tightly tied into the function AddWorldToBrush, not for general use. + // + int32 GDiscarded; // Number of polys discarded and not added. + int32 GNode; // Node AddBrushToWorld is adding to. + int32 GLastCoplanar; // Last coplanar beneath GNode at start of AddWorldToBrush. + int32 GNumNodes; // Number of Bsp nodes at start of AddWorldToBrush. + + UPROPERTY() + TObjectPtr GModel; // Level map Model we're adding to. + + UPROPERTY() + TObjectPtr TempModel; + + //// Globals removed from FBspPointsGrid + //UPROPERTY() + //UHBspPointsGrid* GBspPoints; + + //UPROPERTY() + //UHBspPointsGrid* GBspVectors; + + /*struct BspFilterOp { + void Apply(UHCsgUtils* Obj, UModel* Model, int32 iNode, FPoly* EdPoly, EPolyNodeFilter Filter, ENodePlace ENodePlace ) {}; + };*/ + + // + // Handle a piece of a polygon that was filtered to a leaf. + // + void FilterLeaf( + BspFilterFunc FilterFunc, + UModel* Model, + int32 iNode, + FPoly* EdPoly, + FCoplanarInfo CoplanarInfo, + int32 LeafOutside, + FHBSPOps::ENodePlace ENodePlace, + UHBspPointsGrid* BspPoints, + UHBspPointsGrid* BspVectors + ); + + // + // Function to filter an EdPoly through the Bsp, calling a callback + // function for all chunks that fall into leaves. + // + void FilterEdPoly + ( + BspFilterFunc FilterFunc, + UModel *Model, + int32 iNode, + FPoly *EdPoly, + FCoplanarInfo CoplanarInfo, + int32 Outside, + UHBspPointsGrid* BspPoints, + UHBspPointsGrid* BspVectors + ); + + // + // Regular entry into FilterEdPoly (so higher-level callers don't have to + // deal with unnecessary info). Filters starting at root. + // + void BspFilterFPoly + ( + BspFilterFunc FilterFunc, + UModel *Model, + FPoly *EdPoly, + UHBspPointsGrid* BspPoints, + UHBspPointsGrid* BspVectors + ); + + + int bspNodeToFPoly + ( + UModel* Model, + int32 iNode, + FPoly* EdPoly + ); + + + //---------------------------------------------------------------------------- + // World Filtering + //---------------------------------------------------------------------------- + + // + // Filter all relevant world polys through the brush. + // + void FilterWorldThroughBrush + ( + UModel* Model, + UModel* Brush, + EBrushType BrushType, + ECsgOper CSGOper, + int32 iNode, + FSphere* BrushSphere, + UHBspPointsGrid* BspPoints, + UHBspPointsGrid* BspVectors + ); + + //---------------------------------------------------------------------------- + // CSG leaf filter callbacks / operations. + // --------------------------------------------------------------------------- + void AddBrushToWorldFunc( UModel* Model, int32 iNode, FPoly* EdPoly, EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ); + void AddWorldToBrushFunc( UModel* Model, int32 iNode, FPoly* EdPoly, EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ); + void SubtractBrushFromWorldFunc( UModel* Model, int32 iNode, FPoly* EdPoly, EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ); + void SubtractWorldToBrushFunc( UModel* Model, int32 iNode, FPoly* EdPoly, EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ); + void IntersectBrushWithWorldFunc( UModel* Model, int32 iNode, FPoly *EdPoly, EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ); + void IntersectWorldWithBrushFunc( UModel *Model, int32 iNode, FPoly *EdPoly, EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ); + void DeIntersectBrushWithWorldFunc( UModel* Model, int32 iNode, FPoly* EdPoly, EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ); + void DeIntersectWorldWithBrushFunc( UModel* Model, int32 iNode, FPoly* EdPoly, EPolyNodeFilter Filter, FHBSPOps::ENodePlace ENodePlace, UHBspPointsGrid* BspPoints, UHBspPointsGrid* BspVectors ); + + + //---------------------------------------------------------------------------- + // Forked various functions located in: EditorBsp.cpp, EditorCsg.cpp + //---------------------------------------------------------------------------- + static int TryToMerge( FPoly *Poly1, FPoly *Poly2 ); + static void MergeCoplanars( UModel* Model, int32* PolyList, int32 PolyCount ); + void bspMergeCoplanars( UModel* Model, bool RemapLinks, bool MergeDisparateTextures ); + bool polyFindMaster(UModel* InModel, int32 iSurf, FPoly &Poly); + static void CleanupNodes( UModel *Model, int32 iNode, int32 iParent ); + void bspCleanup( UModel *Model ); + +}; + + +``` + +`Source/HoudiniEngine/Private/HoudiniAlgorithm.h`: + +```h +/* +* Copyright (c) <2024> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreTypes.h" +#include "Templates/Invoke.h" + +namespace Algo +{ +/** + * Heavily based on Unreal's Algo::StableRemoveIf. The difference is that the predicate is supplied + * an index in this version, whereas in Algo::StableRemoveIf, the predicate is supplied the range + * element. + * + * Moves all elements which do not match the predicate to the front of the range, while leaving all + * other elements is a constructed but unspecified state. The elements which were not removed are + * guaranteed to be kept in order (stable). + * + * @param Range The range of elements to manipulate. + * @param Pred A callable which maps elements to truthy values, specifying elements to be removed. + * + * @return The index of the first element after those which were not removed. + */ +template +int32 +StableRemoveIfByIndex(RangeType& Range, Predicate Pred) +{ + // We only use the index for the predicate, but not for retrieval of elements; in some range + // types using an index is slower than an iterator. + int32 Index = 0; + + auto* First = GetData(Range); + auto* Last = First + GetNum(Range); + + auto* IterStart = First; + + // Skip non-removed elements at the start + for (;;) + { + if (IterStart == Last) + { + return UE_PTRDIFF_TO_INT32(IterStart - First); + } + + if (Invoke(Pred, Index)) + { + break; + } + + ++IterStart; + ++Index; + } + + auto* IterKeep = IterStart; + ++IterKeep; + ++Index; + + for (;;) + { + if (IterKeep == Last) + { + return UE_PTRDIFF_TO_INT32(IterStart - First); + } + + if (!Invoke(Pred, Index)) + { + *IterStart++ = MoveTemp(*IterKeep); + } + + ++IterKeep; + ++Index; + } +} +} +``` + +`Source/HoudiniEngine/Private/HoudiniAnimationTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniAnimationTranslator.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniInputObject.h" + +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniSkeletalMeshUtils.h" + +#include "Animation/Skeleton.h" +#include "Animation/AnimSequence.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonSerializer.h" + + +bool +FHoudiniAnimationTranslator::IsMotionClipFrame(const HAPI_NodeId& GeoId, const HAPI_NodeId& PartId, bool bRequiresLocalTransform) +{ + + auto GetAttrInfo = [](const HAPI_NodeId& GeoId, const HAPI_NodeId& PartId, const char* AttrName, HAPI_AttributeOwner AttrOwner) -> HAPI_AttributeInfo + { + HAPI_AttributeInfo AttrInfo; + FHoudiniApi::AttributeInfo_Init(&AttrInfo); + HAPI_Result AttrInfoResult = FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, + AttrName, AttrOwner, &AttrInfo); + return AttrInfo; + }; + + if (!GetAttrInfo(GeoId, PartId, "time", HAPI_AttributeOwner::HAPI_ATTROWNER_PRIM).exists) + { + return false; + } + + if (!GetAttrInfo(GeoId, PartId, "clipinfo", HAPI_AttributeOwner::HAPI_ATTROWNER_DETAIL).exists) + { + return false; + } + + // Check for attributes inside this packed prim: + // point attributes: localtransform, transform, path, name + + // Assume that there is only one part per instance. This is always true for now but may need to be looked at later. + int NumInstancedParts = 1; + TArray InstancedPartIds; + InstancedPartIds.SetNumZeroed(NumInstancedParts); + if ( FHoudiniApi::GetInstancedPartIds( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, + InstancedPartIds.GetData(), + 0, NumInstancedParts ) != HAPI_RESULT_SUCCESS ) + { + return false; + } + + HAPI_PartId InstancedPartId = InstancedPartIds[0]; + if (bRequiresLocalTransform && + !GetAttrInfo(GeoId, InstancedPartId, "localtransform", HAPI_AttributeOwner::HAPI_ATTROWNER_POINT).exists) + { + return false; + } + + if (!GetAttrInfo(GeoId, InstancedPartId, "transform", HAPI_AttributeOwner::HAPI_ATTROWNER_POINT).exists) + { + return false; + } + + if (!GetAttrInfo(GeoId, InstancedPartId, "name", HAPI_AttributeOwner::HAPI_ATTROWNER_POINT).exists) + { + return false; + } + + return true; +} + +bool +FHoudiniAnimationTranslator::CreateAnimSequenceFromOutput( + UHoudiniOutput* InOutput, + const FHoudiniPackageParams& InPackageParams, + UObject* InOuterComponent) +{ + + //Loop over hgpo in Output cand + const TArray& HGPOs = InOutput->GetHoudiniGeoPartObjects(); + return CreateAnimationFromMotionClip(InOutput, HGPOs, InPackageParams, InOuterComponent); +} + +UAnimSequence* +FHoudiniAnimationTranslator::CreateNewAnimation(FHoudiniPackageParams& InPackageParams, const FHoudiniGeoPartObject& HGPO, const FString& InSplitIdentifier) +{ + // Update the current Obj/Geo/Part/Split IDs + InPackageParams.ObjectId = HGPO.ObjectId; + InPackageParams.GeoId = HGPO.GeoId; + InPackageParams.PartId = HGPO.PartId; + InPackageParams.SplitStr = InSplitIdentifier; + + UAnimSequence* NewAnimation = InPackageParams.CreateObjectAndPackage(); + + UE_LOG(LogTemp, Log, TEXT("Animation Path %s "), *NewAnimation->GetPathName()); + if (!IsValid(NewAnimation)) + return nullptr; + + FAssetRegistryModule::AssetCreated(NewAnimation); + + return NewAnimation; + +} + +HAPI_PartId FHoudiniAnimationTranslator::GetInstancedMeshPartID(const FHoudiniGeoPartObject& InstancerHGPO) +{ + constexpr int NumInstancedParts = 1; + TArray InstancedPartIds; + InstancedPartIds.SetNumZeroed(NumInstancedParts); + if (FHoudiniApi::GetInstancedPartIds( + FHoudiniEngine::Get().GetSession(), InstancerHGPO.GeoId, InstancerHGPO.PartId, + InstancedPartIds.GetData(), 0, NumInstancedParts) != HAPI_RESULT_SUCCESS) + { + return -1; + } + + return InstancedPartIds[0]; +} + +FString FHoudiniAnimationTranslator::GetUnrealSkeletonPath(const TArray& HGPOs) +{ + // Try to find the HAPI_UNREAL_SKELETALMESH attribute on the geometry + // We'll check both the topology frame and the first frame of the animation, just in case. + + // First check on the primitives + const int NumHGPOs = FMath::Min(HGPOs.Num(), 2); + for (int i = 0; i < NumHGPOs; i++) + { + const FHoudiniGeoPartObject& InstancerHGPO = HGPOs[i]; + + // First, check whether we have it anywhere on the packed prim (as a detail, prim or point attribute). + if (FHoudiniEngineUtils::HapiCheckAttributeExists(InstancerHGPO.GeoId, InstancerHGPO.PartId, HAPI_UNREAL_ATTRIB_SKELETON)) + { + TArray SkeletonAttrData; + FHoudiniHapiAccessor Accessor(InstancerHGPO.GeoId, InstancerHGPO.PartId, HAPI_UNREAL_ATTRIB_SKELETON); + Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, SkeletonAttrData); + + if (SkeletonAttrData.Num() > 0) + { + return SkeletonAttrData[0]; + } + } + } + + // Check the content of the packed primitives + for (int i = 0; i < NumHGPOs; i++) + { + const FHoudiniGeoPartObject& InstancerHGPO = HGPOs[i]; + + const HAPI_PartId MeshPartId = GetInstancedMeshPartID(InstancerHGPO); + if (MeshPartId < 0) + { + continue; + } + + if (FHoudiniEngineUtils::HapiCheckAttributeExists(InstancerHGPO.GeoId, MeshPartId, HAPI_UNREAL_ATTRIB_SKELETON)) + { + TArray SkeletonAttrData; + FHoudiniHapiAccessor Accessor(InstancerHGPO.GeoId, MeshPartId, HAPI_UNREAL_ATTRIB_SKELETON); + Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, SkeletonAttrData); + if (SkeletonAttrData.Num() > 0) + { + return SkeletonAttrData[0]; + } + } + + } + + return FString(); +} + +bool +FHoudiniAnimationTranslator::GetClipInfo(const FHoudiniGeoPartObject& InstancerHGPO, float& OutFrameRate) +{ + if (FHoudiniEngineUtils::HapiCheckAttributeExists(InstancerHGPO.GeoId, InstancerHGPO.PartId, "clipinfo", HAPI_ATTROWNER_DETAIL)) + { + HAPI_AttributeInfo ClipAttrInfo; + FHoudiniApi::AttributeInfo_Init(&ClipAttrInfo); + + HAPI_Result PointInfoResult = FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + InstancerHGPO.GeoId, InstancerHGPO.PartId, + "clipinfo", HAPI_AttributeOwner::HAPI_ATTROWNER_DETAIL, &ClipAttrInfo); + + if (!ClipAttrInfo.exists) + { + return false; + } + + TArray StringHandles; + StringHandles.SetNum(ClipAttrInfo.count); + + if (FHoudiniApi::GetAttributeDictionaryData( + FHoudiniEngine::Get().GetSession(), + InstancerHGPO.GeoId, InstancerHGPO.PartId, + "clipinfo", + &ClipAttrInfo, + &StringHandles[0], + 0, ClipAttrInfo.count + ) != HAPI_RESULT_SUCCESS) + { + return false; + } + + int StringLength = 0; + if (FHoudiniApi::GetStringBufLength( + FHoudiniEngine::Get().GetSession(), + StringHandles[0], + &StringLength) != HAPI_RESULT_SUCCESS) + { + return false; + } + + if (StringLength == 0) + { + return false; + } + + TArray ClipInfoStrBuffer; + ClipInfoStrBuffer.SetNum(StringLength); + if (FHoudiniApi::GetString( + FHoudiniEngine::Get().GetSession(), + StringHandles[0], + &ClipInfoStrBuffer[0], + StringLength) != HAPI_RESULT_SUCCESS) + { + return false; + } + + // At this point we have the clipinfo dict as a JSON string. Time to parse it! + FString ClipInfoStr = FString(ANSI_TO_TCHAR(&ClipInfoStrBuffer[0])); + + // Parse it as JSON + TSharedPtr JSONObject; + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create( ClipInfoStr ); + if (!FJsonSerializer::Deserialize(Reader, JSONObject) || !JSONObject.IsValid()) + { + return false; + } + + if (!JSONObject.IsValid()) + { + return false; + } + + // We're only interested in the frame rate, for now. + JSONObject->TryGetNumberField(TEXT("rate"), OutFrameRate); + } + + return true; +} + +bool +FHoudiniAnimationTranslator::GetFbxCustomAttributes( + int GeoId, + int MeshPartId, + int RootBoneIndex, + TSharedPtr& OutJSONObject) +{ + if (FHoudiniEngineUtils::HapiCheckAttributeExists(GeoId, MeshPartId, "fbx_custom_attributes", HAPI_ATTROWNER_POINT)) + { + HAPI_AttributeInfo AttrInfo; + FHoudiniApi::AttributeInfo_Init(&AttrInfo); + + HAPI_Result PointInfoResult = FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + GeoId, MeshPartId, + "fbx_custom_attributes", HAPI_AttributeOwner::HAPI_ATTROWNER_POINT, &AttrInfo); + + if (!AttrInfo.exists) + { + return false; + } + + TArray StringHandles; + StringHandles.SetNum(AttrInfo.count); + + if (FHoudiniApi::GetAttributeDictionaryData( + FHoudiniEngine::Get().GetSession(), + GeoId, MeshPartId, + "fbx_custom_attributes", + &AttrInfo, + &StringHandles[0], + 0, AttrInfo.count + ) != HAPI_RESULT_SUCCESS) + { + return false; + } + + if (!StringHandles.IsValidIndex(RootBoneIndex)) + { + HOUDINI_LOG_WARNING(TEXT("Could not retrieve 'fbx_custom_attributes'. Invalid root bone index.")); + return false; + } + + int StringLength = 0; + if (FHoudiniApi::GetStringBufLength( + FHoudiniEngine::Get().GetSession(), + StringHandles[RootBoneIndex], + &StringLength) != HAPI_RESULT_SUCCESS) + { + return false; + } + + if (StringLength == 0) + { + return false; + } + + TArray StrBuffer; + StrBuffer.SetNum(StringLength); + if (FHoudiniApi::GetString( + FHoudiniEngine::Get().GetSession(), + StringHandles[0], + &StrBuffer[0], + StringLength) != HAPI_RESULT_SUCCESS) + { + return false; + } + + // At this point we have the dict as a JSON string. Time to parse it! + const FString JSONData = FString(ANSI_TO_TCHAR(&StrBuffer[0])); + + // Parse it as JSON + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create( JSONData ); + if (!FJsonSerializer::Deserialize(Reader, OutJSONObject) || !OutJSONObject.IsValid()) + { + return false; + } + + if (!OutJSONObject.IsValid()) + { + return false; + } + + return true; + } + + return false; +} + +//Creates SkelatalMesh and Skeleton Assets and Packages, and adds them to OutputObjects +bool FHoudiniAnimationTranslator::CreateAnimationFromMotionClip(UHoudiniOutput* InOutput, const TArray& HGPOs, const FHoudiniPackageParams& InPackageParams, UObject* InOuterComponent) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniAnimationTranslator::CreateAnimationFromMotionClip); + + if (HGPOs.Num() == 0) + { + HOUDINI_LOG_WARNING(TEXT("Could not translate MotionClip. No Geo Part Objects.")); + return false; + } + + // Process the topology frame. + // Note: We require here that all the HGPO's that we receive in this function be packed primitives (instancers). + + const FHoudiniGeoPartObject& TopologyHGPO = HGPOs[0]; + const HAPI_PartId TopologyPartId = GetInstancedMeshPartID(TopologyHGPO); + + if (TopologyPartId < 0) + { + HOUDINI_LOG_WARNING(TEXT("Could not translate MotionClip. Invalid topology frame.")); + return false; + } + + FHoudiniOutputObjectIdentifier OutputObjectIdentifier(TopologyHGPO.ObjectId, TopologyHGPO.GeoId, TopologyPartId, ""); + OutputObjectIdentifier.PartName = TopologyHGPO.PartName; + + FString SkeletonAssetPathName = GetUnrealSkeletonPath(HGPOs); + if (SkeletonAssetPathName.IsEmpty()) + { + HOUDINI_LOG_WARNING(TEXT("Could not translate MotionClip. Empty Skeleton path.")); + return false; + } + + const FSoftObjectPath SkeletonAssetPath(SkeletonAssetPathName); + USkeleton* MySkeleton = nullptr; + MySkeleton = Cast(SkeletonAssetPath.TryLoad()); + if (!IsValid(MySkeleton)) + { + HOUDINI_LOG_WARNING(TEXT("Could not translate MotionClip. Missing '%s' attribute."), *FString(HAPI_UNREAL_ATTRIB_SKELETON)); + return false; + } + + // Process the topology frame. + // Collect Bone names from motion clip topology frame + + HAPI_AttributeInfo BoneNameInfo; + FHoudiniApi::AttributeInfo_Init(&BoneNameInfo); + HAPI_Result AttributeInfoResult = FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + TopologyHGPO.GeoId, TopologyPartId, + "name", HAPI_AttributeOwner::HAPI_ATTROWNER_POINT, &BoneNameInfo); + + TArray BoneNameData; + + FHoudiniHapiAccessor Accessor(TopologyHGPO.GeoId, TopologyPartId, "name"); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, BoneNameData); + + if (BoneNameData.Num() <= 0) + { + HOUDINI_LOG_WARNING(TEXT("Could not translate MotionClip. Missing BoneName data.")); + return false; + } + + // Retrieve the frame rate from the ClipInfo dict + float FrameRate = 30; + GetClipInfo(TopologyHGPO, FrameRate); + + //Get unique Set of Bones + TSet BoneNames; + for (auto BoneString : BoneNameData) + { + BoneNames.Add(FName(BoneString)); + } + + // Process the packed primitives + // Iterate over the packed primitives, retrieve each frame's anim data + + // For each frame, we'll retrieve the world transforms of each bone, transform them into + // Unreal space, then compute the local transforms. + + TMap> FrameBoneTransformMap; //For each frame, store bones and LOCAL transforms + + // Per-bone tracks for Pos, Rot and Scale as needed by the AnimController. + TMap> BonesPosTrack; + TMap> BonesRotTrack; + TMap> BonesScaleTrack; + TMap> FbxCustomAttributes; + + const int NumHGPOs = HGPOs.Num(); + + TArray Frames; + Frames.Reserve(NumHGPOs - 1); + for(int i = 1; i < NumHGPOs; i++) + { + const FHoudiniGeoPartObject& InstancerHGPO = HGPOs[i]; + HAPI_PartId MeshPartId = GetInstancedMeshPartID(InstancerHGPO); + if(InstancerHGPO.Type != EHoudiniPartType::MotionClip || MeshPartId == INDEX_NONE) + { + continue; + } + + const int GeoId = InstancerHGPO.GeoId; + const int PartId = MeshPartId; + HAPI_PartInfo PartInfo; + FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), GeoId, PartId, &PartInfo); + if(PartInfo.faceCount == 0) + continue; + + Frames.Add(&InstancerHGPO); + } + + for (int Frame = 0; Frame < Frames.Num(); Frame++) + { + const FHoudiniGeoPartObject& InstancerHGPO = *Frames[Frame]; + HAPI_PartId MeshPartId = GetInstancedMeshPartID(InstancerHGPO); + + const int GeoId = InstancerHGPO.GeoId; + const int FrameIndex = Frame; + const double FrameTime = (Frame-1) / static_cast(FrameRate); + + // Retrieve the WorldTransform for all the points in this Mesh (frame) + // Note, we need to retrieve "P" for the translation, and "transform" for the rotation/scale. + + HAPI_PartInfo PartInfo; + FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), GeoId, MeshPartId, &PartInfo); + + // Retrieve Position (World Space) + HAPI_AttributeInfo PointInfo; + FHoudiniApi::AttributeInfo_Init(&PointInfo); + + HAPI_Result PointInfoResult = FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + GeoId, MeshPartId, + HAPI_UNREAL_ATTRIB_POSITION, HAPI_AttributeOwner::HAPI_ATTROWNER_POINT, &PointInfo); + + TArray PositionData; + PositionData.SetNum(PointInfo.count); //dont need * PositionInfo.tupleSize, its already a vector container + FHoudiniApi::GetAttributeFloatData(FHoudiniEngine::Get().GetSession(), GeoId, MeshPartId, HAPI_UNREAL_ATTRIB_POSITION, &PointInfo, -1, (float*)&PositionData[0], 0, PointInfo.count); + + // Retrieve Rotation matrix (World Space) + HAPI_AttributeInfo WorldTransformInfo; + FHoudiniApi::AttributeInfo_Init(&WorldTransformInfo); + + HAPI_Result WorldTransformInfoResult = FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + GeoId, MeshPartId, + "transform", HAPI_AttributeOwner::HAPI_ATTROWNER_POINT, &WorldTransformInfo); + + TArray WorldTransformData; + TArray WorldTransformSizesFixedArray; + WorldTransformData.SetNum(WorldTransformInfo.count * WorldTransformInfo.tupleSize); + WorldTransformSizesFixedArray.SetNum(WorldTransformInfo.count); + HAPI_Result WorldTransformDataResult = FHoudiniApi::GetAttributeFloatArrayData(FHoudiniEngine::Get().GetSession(), GeoId, MeshPartId, "transform", &WorldTransformInfo, &WorldTransformData[0], WorldTransformInfo.count * WorldTransformInfo.tupleSize, &WorldTransformSizesFixedArray[0], 0, WorldTransformInfo.count); + + //Build the World Space transforms for the bones + + TMap BoneWSTransforms; + + // Iterate over all the joints to collect and convert their transform data and collect them into per-bone tracks + + // Find the RootBoneIndex so that we can extract the fbx_custom_attributes from that point + int RootBoneIndex = INDEX_NONE; + + for (int BoneIndex = 0; BoneIndex < PointInfo.count; BoneIndex++ ) + { + FString BoneString = BoneNameData[BoneIndex]; + FName BoneName = FName(BoneString); + + if (BoneString == "root") + { + RootBoneIndex = BoneIndex; + } + + FMatrix M44Pose = FHoudiniSkeletalMeshUtils::MakeMatrixFromHoudiniData(WorldTransformData.GetData() + 9 * BoneIndex, &PositionData[BoneIndex].X); + + FTransform UnrealPoseTransform = FTransform(M44Pose); + + BoneWSTransforms.Add(BoneName, UnrealPoseTransform); + } + + // Convert the bones to local transforms, and append them to the bone tracks for the current frame + TMap BoneTrack = FrameBoneTransformMap.FindOrAdd(FrameIndex); + + const FReferenceSkeleton& RefSkeleton = MySkeleton->GetReferenceSkeleton(); + for (auto& Elem : BoneWSTransforms) + { + const FName & CurrentBoneName = Elem.Key; + const FTransform & GlobalBoneTransform = Elem.Value; + const int32 BoneRefIndex = RefSkeleton.FindBoneIndex(CurrentBoneName); + FTransform LocalBoneTransform; + + if (BoneRefIndex > 0) + { + int32 ParentBoneIndex = RefSkeleton.GetParentIndex(BoneRefIndex); + const FName & ParentBoneName = RefSkeleton.GetBoneName(ParentBoneIndex); + const FTransform & ParentCSXform = *BoneWSTransforms.Find(ParentBoneName); + LocalBoneTransform = GlobalBoneTransform * ParentCSXform.Inverse(); + } + else + { + LocalBoneTransform = GlobalBoneTransform; + } + + TArray& KeyPosArray = BonesPosTrack.FindOrAdd(CurrentBoneName); + FVector Pos = LocalBoneTransform.GetLocation(); + KeyPosArray.Add(FVector3f(Pos)); + + TArray& KeyRotArray = BonesRotTrack.FindOrAdd(CurrentBoneName); + FQuat Q = LocalBoneTransform.GetRotation(); + KeyRotArray.Add(FQuat4f(Q)); + + TArray& KeyScaleArray = BonesScaleTrack.FindOrAdd(CurrentBoneName); + FVector Scale = LocalBoneTransform.GetScale3D(); + KeyScaleArray.Add(FVector3f(Scale)); + } + + if (RootBoneIndex != INDEX_NONE) + { + // Collect the fbx_custom_attributes for this frame + TSharedPtr JsonObject; + if (GetFbxCustomAttributes(GeoId, MeshPartId, RootBoneIndex, JsonObject)) + { + TArray Keys; + JsonObject->Values.GetKeys(Keys); + for (const FString& Key : Keys) + { + if (double Value; JsonObject->TryGetNumberField(Key, Value)) + { + TArray& CurveData = FbxCustomAttributes.FindOrAdd(Key); + CurveData.Add(FRichCurveKey(FrameTime, static_cast(Value))); + } + } + } + } + } + + FHoudiniOutputObject& OutputObject = InOutput->GetOutputObjects().FindOrAdd(OutputObjectIdentifier); + FHoudiniPackageParams PackageParams = InPackageParams; + UAnimSequence* AnimSequence = CreateNewAnimation(PackageParams, TopologyHGPO, OutputObjectIdentifier.SplitIdentifier); + OutputObject.OutputObject = AnimSequence; + OutputObject.bProxyIsCurrent = false; + + AnimSequence->ResetAnimation(); + AnimSequence->SetSkeleton(MySkeleton); + AnimSequence->ImportFileFramerate = FrameRate; + AnimSequence->ImportResampleFramerate = FrameRate; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + // Initialize data model + IAnimationDataController& AnimController = AnimSequence->GetController(); + AnimController.InitializeModel(); + + AnimController.OpenBracket(NSLOCTEXT("MyNamespace", "InitializeAnimation", "Initialize New Anim Sequence")); + { + // This line is to set actual frame rate + AnimController.SetFrameRate(FFrameRate(FrameRate, 1), true); + + const int32 NumKeys = Frames.Num(); + AnimController.SetNumberOfFrames(NumKeys - 1); + + //rgc + bool bShouldTransact = true; + for (const FName& Bone : BoneNames) //loop over all bones + { + //QUESTION: What to do if no tracks for bone? Skip? + if (BonesPosTrack.Contains(Bone) && BonesRotTrack.Contains(Bone) && BonesScaleTrack.Contains(Bone)) + { + AnimController.AddBoneCurve(Bone, bShouldTransact); + AnimController.SetBoneTrackKeys(Bone, *BonesPosTrack.Find(Bone), *BonesRotTrack.Find(Bone), *BonesScaleTrack.Find(Bone), bShouldTransact); + } + else + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + HOUDINI_LOG_ERROR(TEXT("Bone '%s' is missing from bone tracks."), *Bone.ToString()); +#else + HOUDINI_LOG_ERROR(TEXT("Bone '%s' is missing from bone tracks."), Bone); +#endif + } + } + + // Set the fbx_custom_attributes as anim curves. + for (const auto& Entry : FbxCustomAttributes ) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + FAnimationCurveIdentifier CurveId(FName(Entry.Key), ERawCurveTrackTypes::RCT_Float); +#else + FSmartName NewName; + MySkeleton->AddSmartNameAndModify(USkeleton::AnimCurveMappingName, FName(Entry.Key), NewName); + FAnimationCurveIdentifier CurveId(NewName, ERawCurveTrackTypes::RCT_Float); +#endif + if (Entry.Value.Num() && AnimController.AddCurve(CurveId)) + { + + AnimController.SetCurveKeys(CurveId, Entry.Value); + } + } + + AnimController.NotifyPopulated(); + } + AnimController.CloseBracket(); + + // Update the property attributes on the component, using the attributes already stored on the HGPO. + if (TopologyHGPO.GenericPropertyAttributes.Num() > 0) + { + // Default priority for any attribute is 0. + // Lower values will be processed first. Higher values will be processed later. + int Priority = 1; + TMap AttributePriority; + AttributePriority.Add("RetargetSourceAsset", Priority); + Priority++; + AttributePriority.Add("AdditiveAnimType", Priority); + Priority++; + AttributePriority.Add("RefPoseType", Priority); + AttributePriority.Add("BasePoseType", Priority); + Priority++; + AttributePriority.Add("RefPoseSeq", Priority); + AttributePriority.Add("BasePoseAnimation", Priority); + Priority++; + AttributePriority.Add("RefFrameIndex", Priority); + + TArray SortedAttributes = TopologyHGPO.GenericPropertyAttributes; + Algo::Sort(SortedAttributes, [&AttributePriority](const FHoudiniGenericAttribute& LHS, const FHoudiniGenericAttribute& RHS) -> bool + { + int LPriority = 0; + int RPriority = 0; + if (AttributePriority.Contains(LHS.GetStringValue())) + { + LPriority = AttributePriority.FindChecked(LHS.GetStringValue()); + } + + if (AttributePriority.Contains(RHS.GetStringValue())) + { + RPriority = AttributePriority.FindChecked(RHS.GetStringValue()); + } + + return LPriority < RPriority; + }); + + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes(AnimSequence, SortedAttributes); + } + +#endif + + return true; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniAnimationTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "HoudiniEnginePrivatePCH.h" + +#include "Containers/UnrealString.h" + +class FJsonObject; +struct FHoudiniGeoPartObject; +struct FHoudiniPackageParams; +class UHoudiniOutput; +class UAnimSequence; + +struct HOUDINIENGINE_API FHoudiniAnimationTranslator +{ +public: + // Check whether the given PartId looks like a frame from a motion clip + static bool IsMotionClipFrame(const HAPI_NodeId& GeoId, const HAPI_NodeId& PartId, bool bRequiresLocalTransform); + + static bool CreateAnimSequenceFromOutput(UHoudiniOutput* InOutput, const FHoudiniPackageParams& InPackageParams, UObject* InOuterComponent); + static bool CreateAnimationFromMotionClip(UHoudiniOutput* InOutput, const TArray& HGPOs, const FHoudiniPackageParams& InPackageParams, UObject* InOuterComponent); + static UAnimSequence* CreateNewAnimation(FHoudiniPackageParams& InPackageParams, const FHoudiniGeoPartObject& HGPO, const FString& InSplitIdentifier); + +private: + static HAPI_PartId GetInstancedMeshPartID(const FHoudiniGeoPartObject& InstancerHGPO); + static FString GetUnrealSkeletonPath(const TArray& HGPOs); + static bool GetClipInfo(const FHoudiniGeoPartObject& InstancerHGPO, float& OutFrameRate); + static bool GetFbxCustomAttributes(int GeoId, int MeshPartId, int RootBoneIndex, TSharedPtr& OutJSONObject); + + +}; + + +``` + +`Source/HoudiniEngine/Private/HoudiniApi.cpp`: + +```cpp +/* + * Copyright (c) <2025> Side Effects Software Inc. * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * + * COMMENTS: + * This file is generated. Do not modify directly. + */ + +#include "HoudiniApi.h" +#include "HoudiniEnginePrivatePCH.h" + + +FHoudiniApi::AddAttributeFuncPtr +FHoudiniApi::AddAttribute = &FHoudiniApi::AddAttributeEmptyStub; + +FHoudiniApi::AddGroupFuncPtr +FHoudiniApi::AddGroup = &FHoudiniApi::AddGroupEmptyStub; + +FHoudiniApi::AssetInfo_CreateFuncPtr +FHoudiniApi::AssetInfo_Create = &FHoudiniApi::AssetInfo_CreateEmptyStub; + +FHoudiniApi::AssetInfo_InitFuncPtr +FHoudiniApi::AssetInfo_Init = &FHoudiniApi::AssetInfo_InitEmptyStub; + +FHoudiniApi::AttributeInfo_CreateFuncPtr +FHoudiniApi::AttributeInfo_Create = &FHoudiniApi::AttributeInfo_CreateEmptyStub; + +FHoudiniApi::AttributeInfo_InitFuncPtr +FHoudiniApi::AttributeInfo_Init = &FHoudiniApi::AttributeInfo_InitEmptyStub; + +FHoudiniApi::BindCustomImplementationFuncPtr +FHoudiniApi::BindCustomImplementation = &FHoudiniApi::BindCustomImplementationEmptyStub; + +FHoudiniApi::CancelPDGCookFuncPtr +FHoudiniApi::CancelPDGCook = &FHoudiniApi::CancelPDGCookEmptyStub; + +FHoudiniApi::CheckForSpecificErrorsFuncPtr +FHoudiniApi::CheckForSpecificErrors = &FHoudiniApi::CheckForSpecificErrorsEmptyStub; + +FHoudiniApi::CleanupFuncPtr +FHoudiniApi::Cleanup = &FHoudiniApi::CleanupEmptyStub; + +FHoudiniApi::ClearConnectionErrorFuncPtr +FHoudiniApi::ClearConnectionError = &FHoudiniApi::ClearConnectionErrorEmptyStub; + +FHoudiniApi::CloseSessionFuncPtr +FHoudiniApi::CloseSession = &FHoudiniApi::CloseSessionEmptyStub; + +FHoudiniApi::CommitGeoFuncPtr +FHoudiniApi::CommitGeo = &FHoudiniApi::CommitGeoEmptyStub; + +FHoudiniApi::CommitWorkItemsFuncPtr +FHoudiniApi::CommitWorkItems = &FHoudiniApi::CommitWorkItemsEmptyStub; + +FHoudiniApi::CommitWorkitemsFuncPtr +FHoudiniApi::CommitWorkitems = &FHoudiniApi::CommitWorkitemsEmptyStub; + +FHoudiniApi::ComposeChildNodeListFuncPtr +FHoudiniApi::ComposeChildNodeList = &FHoudiniApi::ComposeChildNodeListEmptyStub; + +FHoudiniApi::ComposeNodeCookResultFuncPtr +FHoudiniApi::ComposeNodeCookResult = &FHoudiniApi::ComposeNodeCookResultEmptyStub; + +FHoudiniApi::ComposeObjectListFuncPtr +FHoudiniApi::ComposeObjectList = &FHoudiniApi::ComposeObjectListEmptyStub; + +FHoudiniApi::CompositorOptions_CreateFuncPtr +FHoudiniApi::CompositorOptions_Create = &FHoudiniApi::CompositorOptions_CreateEmptyStub; + +FHoudiniApi::CompositorOptions_InitFuncPtr +FHoudiniApi::CompositorOptions_Init = &FHoudiniApi::CompositorOptions_InitEmptyStub; + +FHoudiniApi::ConnectNodeInputFuncPtr +FHoudiniApi::ConnectNodeInput = &FHoudiniApi::ConnectNodeInputEmptyStub; + +FHoudiniApi::ConvertMatrixToEulerFuncPtr +FHoudiniApi::ConvertMatrixToEuler = &FHoudiniApi::ConvertMatrixToEulerEmptyStub; + +FHoudiniApi::ConvertMatrixToQuatFuncPtr +FHoudiniApi::ConvertMatrixToQuat = &FHoudiniApi::ConvertMatrixToQuatEmptyStub; + +FHoudiniApi::ConvertTransformFuncPtr +FHoudiniApi::ConvertTransform = &FHoudiniApi::ConvertTransformEmptyStub; + +FHoudiniApi::ConvertTransformEulerToMatrixFuncPtr +FHoudiniApi::ConvertTransformEulerToMatrix = &FHoudiniApi::ConvertTransformEulerToMatrixEmptyStub; + +FHoudiniApi::ConvertTransformQuatToMatrixFuncPtr +FHoudiniApi::ConvertTransformQuatToMatrix = &FHoudiniApi::ConvertTransformQuatToMatrixEmptyStub; + +FHoudiniApi::CookNodeFuncPtr +FHoudiniApi::CookNode = &FHoudiniApi::CookNodeEmptyStub; + +FHoudiniApi::CookOptions_AreEqualFuncPtr +FHoudiniApi::CookOptions_AreEqual = &FHoudiniApi::CookOptions_AreEqualEmptyStub; + +FHoudiniApi::CookOptions_CreateFuncPtr +FHoudiniApi::CookOptions_Create = &FHoudiniApi::CookOptions_CreateEmptyStub; + +FHoudiniApi::CookOptions_InitFuncPtr +FHoudiniApi::CookOptions_Init = &FHoudiniApi::CookOptions_InitEmptyStub; + +FHoudiniApi::CookPDGFuncPtr +FHoudiniApi::CookPDG = &FHoudiniApi::CookPDGEmptyStub; + +FHoudiniApi::CookPDGAllOutputsFuncPtr +FHoudiniApi::CookPDGAllOutputs = &FHoudiniApi::CookPDGAllOutputsEmptyStub; + +FHoudiniApi::CreateCOPImageFuncPtr +FHoudiniApi::CreateCOPImage = &FHoudiniApi::CreateCOPImageEmptyStub; + +FHoudiniApi::CreateCustomSessionFuncPtr +FHoudiniApi::CreateCustomSession = &FHoudiniApi::CreateCustomSessionEmptyStub; + +FHoudiniApi::CreateHeightFieldInputFuncPtr +FHoudiniApi::CreateHeightFieldInput = &FHoudiniApi::CreateHeightFieldInputEmptyStub; + +FHoudiniApi::CreateHeightfieldInputVolumeNodeFuncPtr +FHoudiniApi::CreateHeightfieldInputVolumeNode = &FHoudiniApi::CreateHeightfieldInputVolumeNodeEmptyStub; + +FHoudiniApi::CreateInProcessSessionFuncPtr +FHoudiniApi::CreateInProcessSession = &FHoudiniApi::CreateInProcessSessionEmptyStub; + +FHoudiniApi::CreateInputCurveNodeFuncPtr +FHoudiniApi::CreateInputCurveNode = &FHoudiniApi::CreateInputCurveNodeEmptyStub; + +FHoudiniApi::CreateInputNodeFuncPtr +FHoudiniApi::CreateInputNode = &FHoudiniApi::CreateInputNodeEmptyStub; + +FHoudiniApi::CreateNodeFuncPtr +FHoudiniApi::CreateNode = &FHoudiniApi::CreateNodeEmptyStub; + +FHoudiniApi::CreateThriftNamedPipeSessionFuncPtr +FHoudiniApi::CreateThriftNamedPipeSession = &FHoudiniApi::CreateThriftNamedPipeSessionEmptyStub; + +FHoudiniApi::CreateThriftSharedMemorySessionFuncPtr +FHoudiniApi::CreateThriftSharedMemorySession = &FHoudiniApi::CreateThriftSharedMemorySessionEmptyStub; + +FHoudiniApi::CreateThriftSocketSessionFuncPtr +FHoudiniApi::CreateThriftSocketSession = &FHoudiniApi::CreateThriftSocketSessionEmptyStub; + +FHoudiniApi::CreateWorkItemFuncPtr +FHoudiniApi::CreateWorkItem = &FHoudiniApi::CreateWorkItemEmptyStub; + +FHoudiniApi::CreateWorkitemFuncPtr +FHoudiniApi::CreateWorkitem = &FHoudiniApi::CreateWorkitemEmptyStub; + +FHoudiniApi::CurveInfo_CreateFuncPtr +FHoudiniApi::CurveInfo_Create = &FHoudiniApi::CurveInfo_CreateEmptyStub; + +FHoudiniApi::CurveInfo_InitFuncPtr +FHoudiniApi::CurveInfo_Init = &FHoudiniApi::CurveInfo_InitEmptyStub; + +FHoudiniApi::DeleteAttributeFuncPtr +FHoudiniApi::DeleteAttribute = &FHoudiniApi::DeleteAttributeEmptyStub; + +FHoudiniApi::DeleteGroupFuncPtr +FHoudiniApi::DeleteGroup = &FHoudiniApi::DeleteGroupEmptyStub; + +FHoudiniApi::DeleteNodeFuncPtr +FHoudiniApi::DeleteNode = &FHoudiniApi::DeleteNodeEmptyStub; + +FHoudiniApi::DirtyPDGNodeFuncPtr +FHoudiniApi::DirtyPDGNode = &FHoudiniApi::DirtyPDGNodeEmptyStub; + +FHoudiniApi::DisconnectNodeInputFuncPtr +FHoudiniApi::DisconnectNodeInput = &FHoudiniApi::DisconnectNodeInputEmptyStub; + +FHoudiniApi::DisconnectNodeOutputsAtFuncPtr +FHoudiniApi::DisconnectNodeOutputsAt = &FHoudiniApi::DisconnectNodeOutputsAtEmptyStub; + +FHoudiniApi::ExtractImageToFileFuncPtr +FHoudiniApi::ExtractImageToFile = &FHoudiniApi::ExtractImageToFileEmptyStub; + +FHoudiniApi::ExtractImageToMemoryFuncPtr +FHoudiniApi::ExtractImageToMemory = &FHoudiniApi::ExtractImageToMemoryEmptyStub; + +FHoudiniApi::GeoInfo_CreateFuncPtr +FHoudiniApi::GeoInfo_Create = &FHoudiniApi::GeoInfo_CreateEmptyStub; + +FHoudiniApi::GeoInfo_GetGroupCountByTypeFuncPtr +FHoudiniApi::GeoInfo_GetGroupCountByType = &FHoudiniApi::GeoInfo_GetGroupCountByTypeEmptyStub; + +FHoudiniApi::GeoInfo_InitFuncPtr +FHoudiniApi::GeoInfo_Init = &FHoudiniApi::GeoInfo_InitEmptyStub; + +FHoudiniApi::GetActiveCacheCountFuncPtr +FHoudiniApi::GetActiveCacheCount = &FHoudiniApi::GetActiveCacheCountEmptyStub; + +FHoudiniApi::GetActiveCacheNamesFuncPtr +FHoudiniApi::GetActiveCacheNames = &FHoudiniApi::GetActiveCacheNamesEmptyStub; + +FHoudiniApi::GetAssetDefinitionParmCountsFuncPtr +FHoudiniApi::GetAssetDefinitionParmCounts = &FHoudiniApi::GetAssetDefinitionParmCountsEmptyStub; + +FHoudiniApi::GetAssetDefinitionParmInfosFuncPtr +FHoudiniApi::GetAssetDefinitionParmInfos = &FHoudiniApi::GetAssetDefinitionParmInfosEmptyStub; + +FHoudiniApi::GetAssetDefinitionParmTagNameFuncPtr +FHoudiniApi::GetAssetDefinitionParmTagName = &FHoudiniApi::GetAssetDefinitionParmTagNameEmptyStub; + +FHoudiniApi::GetAssetDefinitionParmTagValueFuncPtr +FHoudiniApi::GetAssetDefinitionParmTagValue = &FHoudiniApi::GetAssetDefinitionParmTagValueEmptyStub; + +FHoudiniApi::GetAssetDefinitionParmValuesFuncPtr +FHoudiniApi::GetAssetDefinitionParmValues = &FHoudiniApi::GetAssetDefinitionParmValuesEmptyStub; + +FHoudiniApi::GetAssetInfoFuncPtr +FHoudiniApi::GetAssetInfo = &FHoudiniApi::GetAssetInfoEmptyStub; + +FHoudiniApi::GetAssetLibraryFilePathFuncPtr +FHoudiniApi::GetAssetLibraryFilePath = &FHoudiniApi::GetAssetLibraryFilePathEmptyStub; + +FHoudiniApi::GetAssetLibraryIdsFuncPtr +FHoudiniApi::GetAssetLibraryIds = &FHoudiniApi::GetAssetLibraryIdsEmptyStub; + +FHoudiniApi::GetAttributeDictionaryArrayDataFuncPtr +FHoudiniApi::GetAttributeDictionaryArrayData = &FHoudiniApi::GetAttributeDictionaryArrayDataEmptyStub; + +FHoudiniApi::GetAttributeDictionaryArrayDataAsyncFuncPtr +FHoudiniApi::GetAttributeDictionaryArrayDataAsync = &FHoudiniApi::GetAttributeDictionaryArrayDataAsyncEmptyStub; + +FHoudiniApi::GetAttributeDictionaryDataFuncPtr +FHoudiniApi::GetAttributeDictionaryData = &FHoudiniApi::GetAttributeDictionaryDataEmptyStub; + +FHoudiniApi::GetAttributeDictionaryDataAsyncFuncPtr +FHoudiniApi::GetAttributeDictionaryDataAsync = &FHoudiniApi::GetAttributeDictionaryDataAsyncEmptyStub; + +FHoudiniApi::GetAttributeFloat64ArrayDataFuncPtr +FHoudiniApi::GetAttributeFloat64ArrayData = &FHoudiniApi::GetAttributeFloat64ArrayDataEmptyStub; + +FHoudiniApi::GetAttributeFloat64ArrayDataAsyncFuncPtr +FHoudiniApi::GetAttributeFloat64ArrayDataAsync = &FHoudiniApi::GetAttributeFloat64ArrayDataAsyncEmptyStub; + +FHoudiniApi::GetAttributeFloat64DataFuncPtr +FHoudiniApi::GetAttributeFloat64Data = &FHoudiniApi::GetAttributeFloat64DataEmptyStub; + +FHoudiniApi::GetAttributeFloat64DataAsyncFuncPtr +FHoudiniApi::GetAttributeFloat64DataAsync = &FHoudiniApi::GetAttributeFloat64DataAsyncEmptyStub; + +FHoudiniApi::GetAttributeFloatArrayDataFuncPtr +FHoudiniApi::GetAttributeFloatArrayData = &FHoudiniApi::GetAttributeFloatArrayDataEmptyStub; + +FHoudiniApi::GetAttributeFloatArrayDataAsyncFuncPtr +FHoudiniApi::GetAttributeFloatArrayDataAsync = &FHoudiniApi::GetAttributeFloatArrayDataAsyncEmptyStub; + +FHoudiniApi::GetAttributeFloatDataFuncPtr +FHoudiniApi::GetAttributeFloatData = &FHoudiniApi::GetAttributeFloatDataEmptyStub; + +FHoudiniApi::GetAttributeFloatDataAsyncFuncPtr +FHoudiniApi::GetAttributeFloatDataAsync = &FHoudiniApi::GetAttributeFloatDataAsyncEmptyStub; + +FHoudiniApi::GetAttributeInfoFuncPtr +FHoudiniApi::GetAttributeInfo = &FHoudiniApi::GetAttributeInfoEmptyStub; + +FHoudiniApi::GetAttributeInt16ArrayDataFuncPtr +FHoudiniApi::GetAttributeInt16ArrayData = &FHoudiniApi::GetAttributeInt16ArrayDataEmptyStub; + +FHoudiniApi::GetAttributeInt16ArrayDataAsyncFuncPtr +FHoudiniApi::GetAttributeInt16ArrayDataAsync = &FHoudiniApi::GetAttributeInt16ArrayDataAsyncEmptyStub; + +FHoudiniApi::GetAttributeInt16DataFuncPtr +FHoudiniApi::GetAttributeInt16Data = &FHoudiniApi::GetAttributeInt16DataEmptyStub; + +FHoudiniApi::GetAttributeInt16DataAsyncFuncPtr +FHoudiniApi::GetAttributeInt16DataAsync = &FHoudiniApi::GetAttributeInt16DataAsyncEmptyStub; + +FHoudiniApi::GetAttributeInt64ArrayDataFuncPtr +FHoudiniApi::GetAttributeInt64ArrayData = &FHoudiniApi::GetAttributeInt64ArrayDataEmptyStub; + +FHoudiniApi::GetAttributeInt64ArrayDataAsyncFuncPtr +FHoudiniApi::GetAttributeInt64ArrayDataAsync = &FHoudiniApi::GetAttributeInt64ArrayDataAsyncEmptyStub; + +FHoudiniApi::GetAttributeInt64DataFuncPtr +FHoudiniApi::GetAttributeInt64Data = &FHoudiniApi::GetAttributeInt64DataEmptyStub; + +FHoudiniApi::GetAttributeInt64DataAsyncFuncPtr +FHoudiniApi::GetAttributeInt64DataAsync = &FHoudiniApi::GetAttributeInt64DataAsyncEmptyStub; + +FHoudiniApi::GetAttributeInt8ArrayDataFuncPtr +FHoudiniApi::GetAttributeInt8ArrayData = &FHoudiniApi::GetAttributeInt8ArrayDataEmptyStub; + +FHoudiniApi::GetAttributeInt8ArrayDataAsyncFuncPtr +FHoudiniApi::GetAttributeInt8ArrayDataAsync = &FHoudiniApi::GetAttributeInt8ArrayDataAsyncEmptyStub; + +FHoudiniApi::GetAttributeInt8DataFuncPtr +FHoudiniApi::GetAttributeInt8Data = &FHoudiniApi::GetAttributeInt8DataEmptyStub; + +FHoudiniApi::GetAttributeInt8DataAsyncFuncPtr +FHoudiniApi::GetAttributeInt8DataAsync = &FHoudiniApi::GetAttributeInt8DataAsyncEmptyStub; + +FHoudiniApi::GetAttributeIntArrayDataFuncPtr +FHoudiniApi::GetAttributeIntArrayData = &FHoudiniApi::GetAttributeIntArrayDataEmptyStub; + +FHoudiniApi::GetAttributeIntArrayDataAsyncFuncPtr +FHoudiniApi::GetAttributeIntArrayDataAsync = &FHoudiniApi::GetAttributeIntArrayDataAsyncEmptyStub; + +FHoudiniApi::GetAttributeIntDataFuncPtr +FHoudiniApi::GetAttributeIntData = &FHoudiniApi::GetAttributeIntDataEmptyStub; + +FHoudiniApi::GetAttributeIntDataAsyncFuncPtr +FHoudiniApi::GetAttributeIntDataAsync = &FHoudiniApi::GetAttributeIntDataAsyncEmptyStub; + +FHoudiniApi::GetAttributeNamesFuncPtr +FHoudiniApi::GetAttributeNames = &FHoudiniApi::GetAttributeNamesEmptyStub; + +FHoudiniApi::GetAttributeStringArrayDataFuncPtr +FHoudiniApi::GetAttributeStringArrayData = &FHoudiniApi::GetAttributeStringArrayDataEmptyStub; + +FHoudiniApi::GetAttributeStringArrayDataAsyncFuncPtr +FHoudiniApi::GetAttributeStringArrayDataAsync = &FHoudiniApi::GetAttributeStringArrayDataAsyncEmptyStub; + +FHoudiniApi::GetAttributeStringDataFuncPtr +FHoudiniApi::GetAttributeStringData = &FHoudiniApi::GetAttributeStringDataEmptyStub; + +FHoudiniApi::GetAttributeStringDataAsyncFuncPtr +FHoudiniApi::GetAttributeStringDataAsync = &FHoudiniApi::GetAttributeStringDataAsyncEmptyStub; + +FHoudiniApi::GetAttributeUInt8ArrayDataFuncPtr +FHoudiniApi::GetAttributeUInt8ArrayData = &FHoudiniApi::GetAttributeUInt8ArrayDataEmptyStub; + +FHoudiniApi::GetAttributeUInt8ArrayDataAsyncFuncPtr +FHoudiniApi::GetAttributeUInt8ArrayDataAsync = &FHoudiniApi::GetAttributeUInt8ArrayDataAsyncEmptyStub; + +FHoudiniApi::GetAttributeUInt8DataFuncPtr +FHoudiniApi::GetAttributeUInt8Data = &FHoudiniApi::GetAttributeUInt8DataEmptyStub; + +FHoudiniApi::GetAttributeUInt8DataAsyncFuncPtr +FHoudiniApi::GetAttributeUInt8DataAsync = &FHoudiniApi::GetAttributeUInt8DataAsyncEmptyStub; + +FHoudiniApi::GetAvailableAssetCountFuncPtr +FHoudiniApi::GetAvailableAssetCount = &FHoudiniApi::GetAvailableAssetCountEmptyStub; + +FHoudiniApi::GetAvailableAssetsFuncPtr +FHoudiniApi::GetAvailableAssets = &FHoudiniApi::GetAvailableAssetsEmptyStub; + +FHoudiniApi::GetBoxInfoFuncPtr +FHoudiniApi::GetBoxInfo = &FHoudiniApi::GetBoxInfoEmptyStub; + +FHoudiniApi::GetCachePropertyFuncPtr +FHoudiniApi::GetCacheProperty = &FHoudiniApi::GetCachePropertyEmptyStub; + +FHoudiniApi::GetComposedChildNodeListFuncPtr +FHoudiniApi::GetComposedChildNodeList = &FHoudiniApi::GetComposedChildNodeListEmptyStub; + +FHoudiniApi::GetComposedNodeCookResultFuncPtr +FHoudiniApi::GetComposedNodeCookResult = &FHoudiniApi::GetComposedNodeCookResultEmptyStub; + +FHoudiniApi::GetComposedObjectListFuncPtr +FHoudiniApi::GetComposedObjectList = &FHoudiniApi::GetComposedObjectListEmptyStub; + +FHoudiniApi::GetComposedObjectTransformsFuncPtr +FHoudiniApi::GetComposedObjectTransforms = &FHoudiniApi::GetComposedObjectTransformsEmptyStub; + +FHoudiniApi::GetCompositorOptionsFuncPtr +FHoudiniApi::GetCompositorOptions = &FHoudiniApi::GetCompositorOptionsEmptyStub; + +FHoudiniApi::GetConnectionErrorFuncPtr +FHoudiniApi::GetConnectionError = &FHoudiniApi::GetConnectionErrorEmptyStub; + +FHoudiniApi::GetConnectionErrorLengthFuncPtr +FHoudiniApi::GetConnectionErrorLength = &FHoudiniApi::GetConnectionErrorLengthEmptyStub; + +FHoudiniApi::GetCookingCurrentCountFuncPtr +FHoudiniApi::GetCookingCurrentCount = &FHoudiniApi::GetCookingCurrentCountEmptyStub; + +FHoudiniApi::GetCookingTotalCountFuncPtr +FHoudiniApi::GetCookingTotalCount = &FHoudiniApi::GetCookingTotalCountEmptyStub; + +FHoudiniApi::GetCurveCountsFuncPtr +FHoudiniApi::GetCurveCounts = &FHoudiniApi::GetCurveCountsEmptyStub; + +FHoudiniApi::GetCurveInfoFuncPtr +FHoudiniApi::GetCurveInfo = &FHoudiniApi::GetCurveInfoEmptyStub; + +FHoudiniApi::GetCurveKnotsFuncPtr +FHoudiniApi::GetCurveKnots = &FHoudiniApi::GetCurveKnotsEmptyStub; + +FHoudiniApi::GetCurveOrdersFuncPtr +FHoudiniApi::GetCurveOrders = &FHoudiniApi::GetCurveOrdersEmptyStub; + +FHoudiniApi::GetDisplayGeoInfoFuncPtr +FHoudiniApi::GetDisplayGeoInfo = &FHoudiniApi::GetDisplayGeoInfoEmptyStub; + +FHoudiniApi::GetEdgeCountOfEdgeGroupFuncPtr +FHoudiniApi::GetEdgeCountOfEdgeGroup = &FHoudiniApi::GetEdgeCountOfEdgeGroupEmptyStub; + +FHoudiniApi::GetEnvIntFuncPtr +FHoudiniApi::GetEnvInt = &FHoudiniApi::GetEnvIntEmptyStub; + +FHoudiniApi::GetFaceCountsFuncPtr +FHoudiniApi::GetFaceCounts = &FHoudiniApi::GetFaceCountsEmptyStub; + +FHoudiniApi::GetFirstVolumeTileFuncPtr +FHoudiniApi::GetFirstVolumeTile = &FHoudiniApi::GetFirstVolumeTileEmptyStub; + +FHoudiniApi::GetGeoInfoFuncPtr +FHoudiniApi::GetGeoInfo = &FHoudiniApi::GetGeoInfoEmptyStub; + +FHoudiniApi::GetGeoSizeFuncPtr +FHoudiniApi::GetGeoSize = &FHoudiniApi::GetGeoSizeEmptyStub; + +FHoudiniApi::GetGroupCountOnPackedInstancePartFuncPtr +FHoudiniApi::GetGroupCountOnPackedInstancePart = &FHoudiniApi::GetGroupCountOnPackedInstancePartEmptyStub; + +FHoudiniApi::GetGroupMembershipFuncPtr +FHoudiniApi::GetGroupMembership = &FHoudiniApi::GetGroupMembershipEmptyStub; + +FHoudiniApi::GetGroupMembershipOnPackedInstancePartFuncPtr +FHoudiniApi::GetGroupMembershipOnPackedInstancePart = &FHoudiniApi::GetGroupMembershipOnPackedInstancePartEmptyStub; + +FHoudiniApi::GetGroupNamesFuncPtr +FHoudiniApi::GetGroupNames = &FHoudiniApi::GetGroupNamesEmptyStub; + +FHoudiniApi::GetGroupNamesOnPackedInstancePartFuncPtr +FHoudiniApi::GetGroupNamesOnPackedInstancePart = &FHoudiniApi::GetGroupNamesOnPackedInstancePartEmptyStub; + +FHoudiniApi::GetHIPFileNodeCountFuncPtr +FHoudiniApi::GetHIPFileNodeCount = &FHoudiniApi::GetHIPFileNodeCountEmptyStub; + +FHoudiniApi::GetHIPFileNodeIdsFuncPtr +FHoudiniApi::GetHIPFileNodeIds = &FHoudiniApi::GetHIPFileNodeIdsEmptyStub; + +FHoudiniApi::GetHandleBindingInfoFuncPtr +FHoudiniApi::GetHandleBindingInfo = &FHoudiniApi::GetHandleBindingInfoEmptyStub; + +FHoudiniApi::GetHandleInfoFuncPtr +FHoudiniApi::GetHandleInfo = &FHoudiniApi::GetHandleInfoEmptyStub; + +FHoudiniApi::GetHeightFieldDataFuncPtr +FHoudiniApi::GetHeightFieldData = &FHoudiniApi::GetHeightFieldDataEmptyStub; + +FHoudiniApi::GetImageFilePathFuncPtr +FHoudiniApi::GetImageFilePath = &FHoudiniApi::GetImageFilePathEmptyStub; + +FHoudiniApi::GetImageInfoFuncPtr +FHoudiniApi::GetImageInfo = &FHoudiniApi::GetImageInfoEmptyStub; + +FHoudiniApi::GetImageMemoryBufferFuncPtr +FHoudiniApi::GetImageMemoryBuffer = &FHoudiniApi::GetImageMemoryBufferEmptyStub; + +FHoudiniApi::GetImagePlaneCountFuncPtr +FHoudiniApi::GetImagePlaneCount = &FHoudiniApi::GetImagePlaneCountEmptyStub; + +FHoudiniApi::GetImagePlanesFuncPtr +FHoudiniApi::GetImagePlanes = &FHoudiniApi::GetImagePlanesEmptyStub; + +FHoudiniApi::GetInputCurveInfoFuncPtr +FHoudiniApi::GetInputCurveInfo = &FHoudiniApi::GetInputCurveInfoEmptyStub; + +FHoudiniApi::GetInstanceTransformsOnPartFuncPtr +FHoudiniApi::GetInstanceTransformsOnPart = &FHoudiniApi::GetInstanceTransformsOnPartEmptyStub; + +FHoudiniApi::GetInstancedObjectIdsFuncPtr +FHoudiniApi::GetInstancedObjectIds = &FHoudiniApi::GetInstancedObjectIdsEmptyStub; + +FHoudiniApi::GetInstancedPartIdsFuncPtr +FHoudiniApi::GetInstancedPartIds = &FHoudiniApi::GetInstancedPartIdsEmptyStub; + +FHoudiniApi::GetInstancerPartTransformsFuncPtr +FHoudiniApi::GetInstancerPartTransforms = &FHoudiniApi::GetInstancerPartTransformsEmptyStub; + +FHoudiniApi::GetJobStatusFuncPtr +FHoudiniApi::GetJobStatus = &FHoudiniApi::GetJobStatusEmptyStub; + +FHoudiniApi::GetLoadedAssetLibraryCountFuncPtr +FHoudiniApi::GetLoadedAssetLibraryCount = &FHoudiniApi::GetLoadedAssetLibraryCountEmptyStub; + +FHoudiniApi::GetManagerNodeIdFuncPtr +FHoudiniApi::GetManagerNodeId = &FHoudiniApi::GetManagerNodeIdEmptyStub; + +FHoudiniApi::GetMaterialInfoFuncPtr +FHoudiniApi::GetMaterialInfo = &FHoudiniApi::GetMaterialInfoEmptyStub; + +FHoudiniApi::GetMaterialNodeIdsOnFacesFuncPtr +FHoudiniApi::GetMaterialNodeIdsOnFaces = &FHoudiniApi::GetMaterialNodeIdsOnFacesEmptyStub; + +FHoudiniApi::GetMessageNodeCountFuncPtr +FHoudiniApi::GetMessageNodeCount = &FHoudiniApi::GetMessageNodeCountEmptyStub; + +FHoudiniApi::GetMessageNodeIdsFuncPtr +FHoudiniApi::GetMessageNodeIds = &FHoudiniApi::GetMessageNodeIdsEmptyStub; + +FHoudiniApi::GetNextVolumeTileFuncPtr +FHoudiniApi::GetNextVolumeTile = &FHoudiniApi::GetNextVolumeTileEmptyStub; + +FHoudiniApi::GetNodeCookResultFuncPtr +FHoudiniApi::GetNodeCookResult = &FHoudiniApi::GetNodeCookResultEmptyStub; + +FHoudiniApi::GetNodeCookResultLengthFuncPtr +FHoudiniApi::GetNodeCookResultLength = &FHoudiniApi::GetNodeCookResultLengthEmptyStub; + +FHoudiniApi::GetNodeFromPathFuncPtr +FHoudiniApi::GetNodeFromPath = &FHoudiniApi::GetNodeFromPathEmptyStub; + +FHoudiniApi::GetNodeInfoFuncPtr +FHoudiniApi::GetNodeInfo = &FHoudiniApi::GetNodeInfoEmptyStub; + +FHoudiniApi::GetNodeInputNameFuncPtr +FHoudiniApi::GetNodeInputName = &FHoudiniApi::GetNodeInputNameEmptyStub; + +FHoudiniApi::GetNodeOutputNameFuncPtr +FHoudiniApi::GetNodeOutputName = &FHoudiniApi::GetNodeOutputNameEmptyStub; + +FHoudiniApi::GetNodePathFuncPtr +FHoudiniApi::GetNodePath = &FHoudiniApi::GetNodePathEmptyStub; + +FHoudiniApi::GetNumWorkItemsFuncPtr +FHoudiniApi::GetNumWorkItems = &FHoudiniApi::GetNumWorkItemsEmptyStub; + +FHoudiniApi::GetNumWorkitemsFuncPtr +FHoudiniApi::GetNumWorkitems = &FHoudiniApi::GetNumWorkitemsEmptyStub; + +FHoudiniApi::GetObjectInfoFuncPtr +FHoudiniApi::GetObjectInfo = &FHoudiniApi::GetObjectInfoEmptyStub; + +FHoudiniApi::GetObjectTransformFuncPtr +FHoudiniApi::GetObjectTransform = &FHoudiniApi::GetObjectTransformEmptyStub; + +FHoudiniApi::GetOutputGeoCountFuncPtr +FHoudiniApi::GetOutputGeoCount = &FHoudiniApi::GetOutputGeoCountEmptyStub; + +FHoudiniApi::GetOutputGeoInfosFuncPtr +FHoudiniApi::GetOutputGeoInfos = &FHoudiniApi::GetOutputGeoInfosEmptyStub; + +FHoudiniApi::GetOutputNodeIdFuncPtr +FHoudiniApi::GetOutputNodeId = &FHoudiniApi::GetOutputNodeIdEmptyStub; + +FHoudiniApi::GetPDGEventsFuncPtr +FHoudiniApi::GetPDGEvents = &FHoudiniApi::GetPDGEventsEmptyStub; + +FHoudiniApi::GetPDGGraphContextIdFuncPtr +FHoudiniApi::GetPDGGraphContextId = &FHoudiniApi::GetPDGGraphContextIdEmptyStub; + +FHoudiniApi::GetPDGGraphContextsFuncPtr +FHoudiniApi::GetPDGGraphContexts = &FHoudiniApi::GetPDGGraphContextsEmptyStub; + +FHoudiniApi::GetPDGGraphContextsCountFuncPtr +FHoudiniApi::GetPDGGraphContextsCount = &FHoudiniApi::GetPDGGraphContextsCountEmptyStub; + +FHoudiniApi::GetPDGStateFuncPtr +FHoudiniApi::GetPDGState = &FHoudiniApi::GetPDGStateEmptyStub; + +FHoudiniApi::GetParametersFuncPtr +FHoudiniApi::GetParameters = &FHoudiniApi::GetParametersEmptyStub; + +FHoudiniApi::GetParmChoiceListsFuncPtr +FHoudiniApi::GetParmChoiceLists = &FHoudiniApi::GetParmChoiceListsEmptyStub; + +FHoudiniApi::GetParmExpressionFuncPtr +FHoudiniApi::GetParmExpression = &FHoudiniApi::GetParmExpressionEmptyStub; + +FHoudiniApi::GetParmFileFuncPtr +FHoudiniApi::GetParmFile = &FHoudiniApi::GetParmFileEmptyStub; + +FHoudiniApi::GetParmFloatValueFuncPtr +FHoudiniApi::GetParmFloatValue = &FHoudiniApi::GetParmFloatValueEmptyStub; + +FHoudiniApi::GetParmFloatValuesFuncPtr +FHoudiniApi::GetParmFloatValues = &FHoudiniApi::GetParmFloatValuesEmptyStub; + +FHoudiniApi::GetParmIdFromNameFuncPtr +FHoudiniApi::GetParmIdFromName = &FHoudiniApi::GetParmIdFromNameEmptyStub; + +FHoudiniApi::GetParmInfoFuncPtr +FHoudiniApi::GetParmInfo = &FHoudiniApi::GetParmInfoEmptyStub; + +FHoudiniApi::GetParmInfoFromNameFuncPtr +FHoudiniApi::GetParmInfoFromName = &FHoudiniApi::GetParmInfoFromNameEmptyStub; + +FHoudiniApi::GetParmIntValueFuncPtr +FHoudiniApi::GetParmIntValue = &FHoudiniApi::GetParmIntValueEmptyStub; + +FHoudiniApi::GetParmIntValuesFuncPtr +FHoudiniApi::GetParmIntValues = &FHoudiniApi::GetParmIntValuesEmptyStub; + +FHoudiniApi::GetParmNodeValueFuncPtr +FHoudiniApi::GetParmNodeValue = &FHoudiniApi::GetParmNodeValueEmptyStub; + +FHoudiniApi::GetParmStringValueFuncPtr +FHoudiniApi::GetParmStringValue = &FHoudiniApi::GetParmStringValueEmptyStub; + +FHoudiniApi::GetParmStringValuesFuncPtr +FHoudiniApi::GetParmStringValues = &FHoudiniApi::GetParmStringValuesEmptyStub; + +FHoudiniApi::GetParmTagNameFuncPtr +FHoudiniApi::GetParmTagName = &FHoudiniApi::GetParmTagNameEmptyStub; + +FHoudiniApi::GetParmTagValueFuncPtr +FHoudiniApi::GetParmTagValue = &FHoudiniApi::GetParmTagValueEmptyStub; + +FHoudiniApi::GetParmWithTagFuncPtr +FHoudiniApi::GetParmWithTag = &FHoudiniApi::GetParmWithTagEmptyStub; + +FHoudiniApi::GetPartInfoFuncPtr +FHoudiniApi::GetPartInfo = &FHoudiniApi::GetPartInfoEmptyStub; + +FHoudiniApi::GetPresetFuncPtr +FHoudiniApi::GetPreset = &FHoudiniApi::GetPresetEmptyStub; + +FHoudiniApi::GetPresetBufLengthFuncPtr +FHoudiniApi::GetPresetBufLength = &FHoudiniApi::GetPresetBufLengthEmptyStub; + +FHoudiniApi::GetPresetCountFuncPtr +FHoudiniApi::GetPresetCount = &FHoudiniApi::GetPresetCountEmptyStub; + +FHoudiniApi::GetPresetNamesFuncPtr +FHoudiniApi::GetPresetNames = &FHoudiniApi::GetPresetNamesEmptyStub; + +FHoudiniApi::GetServerEnvIntFuncPtr +FHoudiniApi::GetServerEnvInt = &FHoudiniApi::GetServerEnvIntEmptyStub; + +FHoudiniApi::GetServerEnvStringFuncPtr +FHoudiniApi::GetServerEnvString = &FHoudiniApi::GetServerEnvStringEmptyStub; + +FHoudiniApi::GetServerEnvVarCountFuncPtr +FHoudiniApi::GetServerEnvVarCount = &FHoudiniApi::GetServerEnvVarCountEmptyStub; + +FHoudiniApi::GetServerEnvVarListFuncPtr +FHoudiniApi::GetServerEnvVarList = &FHoudiniApi::GetServerEnvVarListEmptyStub; + +FHoudiniApi::GetSessionEnvIntFuncPtr +FHoudiniApi::GetSessionEnvInt = &FHoudiniApi::GetSessionEnvIntEmptyStub; + +FHoudiniApi::GetSessionSyncInfoFuncPtr +FHoudiniApi::GetSessionSyncInfo = &FHoudiniApi::GetSessionSyncInfoEmptyStub; + +FHoudiniApi::GetSphereInfoFuncPtr +FHoudiniApi::GetSphereInfo = &FHoudiniApi::GetSphereInfoEmptyStub; + +FHoudiniApi::GetStatusFuncPtr +FHoudiniApi::GetStatus = &FHoudiniApi::GetStatusEmptyStub; + +FHoudiniApi::GetStatusStringFuncPtr +FHoudiniApi::GetStatusString = &FHoudiniApi::GetStatusStringEmptyStub; + +FHoudiniApi::GetStatusStringBufLengthFuncPtr +FHoudiniApi::GetStatusStringBufLength = &FHoudiniApi::GetStatusStringBufLengthEmptyStub; + +FHoudiniApi::GetStringFuncPtr +FHoudiniApi::GetString = &FHoudiniApi::GetStringEmptyStub; + +FHoudiniApi::GetStringBatchFuncPtr +FHoudiniApi::GetStringBatch = &FHoudiniApi::GetStringBatchEmptyStub; + +FHoudiniApi::GetStringBatchSizeFuncPtr +FHoudiniApi::GetStringBatchSize = &FHoudiniApi::GetStringBatchSizeEmptyStub; + +FHoudiniApi::GetStringBufLengthFuncPtr +FHoudiniApi::GetStringBufLength = &FHoudiniApi::GetStringBufLengthEmptyStub; + +FHoudiniApi::GetSupportedImageFileFormatCountFuncPtr +FHoudiniApi::GetSupportedImageFileFormatCount = &FHoudiniApi::GetSupportedImageFileFormatCountEmptyStub; + +FHoudiniApi::GetSupportedImageFileFormatsFuncPtr +FHoudiniApi::GetSupportedImageFileFormats = &FHoudiniApi::GetSupportedImageFileFormatsEmptyStub; + +FHoudiniApi::GetTimeFuncPtr +FHoudiniApi::GetTime = &FHoudiniApi::GetTimeEmptyStub; + +FHoudiniApi::GetTimelineOptionsFuncPtr +FHoudiniApi::GetTimelineOptions = &FHoudiniApi::GetTimelineOptionsEmptyStub; + +FHoudiniApi::GetTotalCookCountFuncPtr +FHoudiniApi::GetTotalCookCount = &FHoudiniApi::GetTotalCookCountEmptyStub; + +FHoudiniApi::GetUseHoudiniTimeFuncPtr +FHoudiniApi::GetUseHoudiniTime = &FHoudiniApi::GetUseHoudiniTimeEmptyStub; + +FHoudiniApi::GetVertexListFuncPtr +FHoudiniApi::GetVertexList = &FHoudiniApi::GetVertexListEmptyStub; + +FHoudiniApi::GetViewportFuncPtr +FHoudiniApi::GetViewport = &FHoudiniApi::GetViewportEmptyStub; + +FHoudiniApi::GetVolumeBoundsFuncPtr +FHoudiniApi::GetVolumeBounds = &FHoudiniApi::GetVolumeBoundsEmptyStub; + +FHoudiniApi::GetVolumeInfoFuncPtr +FHoudiniApi::GetVolumeInfo = &FHoudiniApi::GetVolumeInfoEmptyStub; + +FHoudiniApi::GetVolumeTileFloatDataFuncPtr +FHoudiniApi::GetVolumeTileFloatData = &FHoudiniApi::GetVolumeTileFloatDataEmptyStub; + +FHoudiniApi::GetVolumeTileIntDataFuncPtr +FHoudiniApi::GetVolumeTileIntData = &FHoudiniApi::GetVolumeTileIntDataEmptyStub; + +FHoudiniApi::GetVolumeVisualInfoFuncPtr +FHoudiniApi::GetVolumeVisualInfo = &FHoudiniApi::GetVolumeVisualInfoEmptyStub; + +FHoudiniApi::GetVolumeVoxelFloatDataFuncPtr +FHoudiniApi::GetVolumeVoxelFloatData = &FHoudiniApi::GetVolumeVoxelFloatDataEmptyStub; + +FHoudiniApi::GetVolumeVoxelIntDataFuncPtr +FHoudiniApi::GetVolumeVoxelIntData = &FHoudiniApi::GetVolumeVoxelIntDataEmptyStub; + +FHoudiniApi::GetWorkItemAttributeSizeFuncPtr +FHoudiniApi::GetWorkItemAttributeSize = &FHoudiniApi::GetWorkItemAttributeSizeEmptyStub; + +FHoudiniApi::GetWorkItemFloatAttributeFuncPtr +FHoudiniApi::GetWorkItemFloatAttribute = &FHoudiniApi::GetWorkItemFloatAttributeEmptyStub; + +FHoudiniApi::GetWorkItemInfoFuncPtr +FHoudiniApi::GetWorkItemInfo = &FHoudiniApi::GetWorkItemInfoEmptyStub; + +FHoudiniApi::GetWorkItemIntAttributeFuncPtr +FHoudiniApi::GetWorkItemIntAttribute = &FHoudiniApi::GetWorkItemIntAttributeEmptyStub; + +FHoudiniApi::GetWorkItemOutputFilesFuncPtr +FHoudiniApi::GetWorkItemOutputFiles = &FHoudiniApi::GetWorkItemOutputFilesEmptyStub; + +FHoudiniApi::GetWorkItemStringAttributeFuncPtr +FHoudiniApi::GetWorkItemStringAttribute = &FHoudiniApi::GetWorkItemStringAttributeEmptyStub; + +FHoudiniApi::GetWorkItemsFuncPtr +FHoudiniApi::GetWorkItems = &FHoudiniApi::GetWorkItemsEmptyStub; + +FHoudiniApi::GetWorkitemDataLengthFuncPtr +FHoudiniApi::GetWorkitemDataLength = &FHoudiniApi::GetWorkitemDataLengthEmptyStub; + +FHoudiniApi::GetWorkitemFloatDataFuncPtr +FHoudiniApi::GetWorkitemFloatData = &FHoudiniApi::GetWorkitemFloatDataEmptyStub; + +FHoudiniApi::GetWorkitemInfoFuncPtr +FHoudiniApi::GetWorkitemInfo = &FHoudiniApi::GetWorkitemInfoEmptyStub; + +FHoudiniApi::GetWorkitemIntDataFuncPtr +FHoudiniApi::GetWorkitemIntData = &FHoudiniApi::GetWorkitemIntDataEmptyStub; + +FHoudiniApi::GetWorkitemResultInfoFuncPtr +FHoudiniApi::GetWorkitemResultInfo = &FHoudiniApi::GetWorkitemResultInfoEmptyStub; + +FHoudiniApi::GetWorkitemStringDataFuncPtr +FHoudiniApi::GetWorkitemStringData = &FHoudiniApi::GetWorkitemStringDataEmptyStub; + +FHoudiniApi::GetWorkitemsFuncPtr +FHoudiniApi::GetWorkitems = &FHoudiniApi::GetWorkitemsEmptyStub; + +FHoudiniApi::HandleBindingInfo_CreateFuncPtr +FHoudiniApi::HandleBindingInfo_Create = &FHoudiniApi::HandleBindingInfo_CreateEmptyStub; + +FHoudiniApi::HandleBindingInfo_InitFuncPtr +FHoudiniApi::HandleBindingInfo_Init = &FHoudiniApi::HandleBindingInfo_InitEmptyStub; + +FHoudiniApi::HandleInfo_CreateFuncPtr +FHoudiniApi::HandleInfo_Create = &FHoudiniApi::HandleInfo_CreateEmptyStub; + +FHoudiniApi::HandleInfo_InitFuncPtr +FHoudiniApi::HandleInfo_Init = &FHoudiniApi::HandleInfo_InitEmptyStub; + +FHoudiniApi::ImageFileFormat_CreateFuncPtr +FHoudiniApi::ImageFileFormat_Create = &FHoudiniApi::ImageFileFormat_CreateEmptyStub; + +FHoudiniApi::ImageFileFormat_InitFuncPtr +FHoudiniApi::ImageFileFormat_Init = &FHoudiniApi::ImageFileFormat_InitEmptyStub; + +FHoudiniApi::ImageInfo_CreateFuncPtr +FHoudiniApi::ImageInfo_Create = &FHoudiniApi::ImageInfo_CreateEmptyStub; + +FHoudiniApi::ImageInfo_InitFuncPtr +FHoudiniApi::ImageInfo_Init = &FHoudiniApi::ImageInfo_InitEmptyStub; + +FHoudiniApi::InitializeFuncPtr +FHoudiniApi::Initialize = &FHoudiniApi::InitializeEmptyStub; + +FHoudiniApi::InputCurveInfo_CreateFuncPtr +FHoudiniApi::InputCurveInfo_Create = &FHoudiniApi::InputCurveInfo_CreateEmptyStub; + +FHoudiniApi::InputCurveInfo_InitFuncPtr +FHoudiniApi::InputCurveInfo_Init = &FHoudiniApi::InputCurveInfo_InitEmptyStub; + +FHoudiniApi::InsertMultiparmInstanceFuncPtr +FHoudiniApi::InsertMultiparmInstance = &FHoudiniApi::InsertMultiparmInstanceEmptyStub; + +FHoudiniApi::InterruptFuncPtr +FHoudiniApi::Interrupt = &FHoudiniApi::InterruptEmptyStub; + +FHoudiniApi::IsInitializedFuncPtr +FHoudiniApi::IsInitialized = &FHoudiniApi::IsInitializedEmptyStub; + +FHoudiniApi::IsNodeValidFuncPtr +FHoudiniApi::IsNodeValid = &FHoudiniApi::IsNodeValidEmptyStub; + +FHoudiniApi::IsSessionValidFuncPtr +FHoudiniApi::IsSessionValid = &FHoudiniApi::IsSessionValidEmptyStub; + +FHoudiniApi::Keyframe_CreateFuncPtr +FHoudiniApi::Keyframe_Create = &FHoudiniApi::Keyframe_CreateEmptyStub; + +FHoudiniApi::Keyframe_InitFuncPtr +FHoudiniApi::Keyframe_Init = &FHoudiniApi::Keyframe_InitEmptyStub; + +FHoudiniApi::LoadAssetLibraryFromFileFuncPtr +FHoudiniApi::LoadAssetLibraryFromFile = &FHoudiniApi::LoadAssetLibraryFromFileEmptyStub; + +FHoudiniApi::LoadAssetLibraryFromMemoryFuncPtr +FHoudiniApi::LoadAssetLibraryFromMemory = &FHoudiniApi::LoadAssetLibraryFromMemoryEmptyStub; + +FHoudiniApi::LoadGeoFromFileFuncPtr +FHoudiniApi::LoadGeoFromFile = &FHoudiniApi::LoadGeoFromFileEmptyStub; + +FHoudiniApi::LoadGeoFromMemoryFuncPtr +FHoudiniApi::LoadGeoFromMemory = &FHoudiniApi::LoadGeoFromMemoryEmptyStub; + +FHoudiniApi::LoadHIPFileFuncPtr +FHoudiniApi::LoadHIPFile = &FHoudiniApi::LoadHIPFileEmptyStub; + +FHoudiniApi::LoadNodeFromFileFuncPtr +FHoudiniApi::LoadNodeFromFile = &FHoudiniApi::LoadNodeFromFileEmptyStub; + +FHoudiniApi::MaterialInfo_CreateFuncPtr +FHoudiniApi::MaterialInfo_Create = &FHoudiniApi::MaterialInfo_CreateEmptyStub; + +FHoudiniApi::MaterialInfo_InitFuncPtr +FHoudiniApi::MaterialInfo_Init = &FHoudiniApi::MaterialInfo_InitEmptyStub; + +FHoudiniApi::MergeHIPFileFuncPtr +FHoudiniApi::MergeHIPFile = &FHoudiniApi::MergeHIPFileEmptyStub; + +FHoudiniApi::NodeInfo_CreateFuncPtr +FHoudiniApi::NodeInfo_Create = &FHoudiniApi::NodeInfo_CreateEmptyStub; + +FHoudiniApi::NodeInfo_InitFuncPtr +FHoudiniApi::NodeInfo_Init = &FHoudiniApi::NodeInfo_InitEmptyStub; + +FHoudiniApi::ObjectInfo_CreateFuncPtr +FHoudiniApi::ObjectInfo_Create = &FHoudiniApi::ObjectInfo_CreateEmptyStub; + +FHoudiniApi::ObjectInfo_InitFuncPtr +FHoudiniApi::ObjectInfo_Init = &FHoudiniApi::ObjectInfo_InitEmptyStub; + +FHoudiniApi::ParmChoiceInfo_CreateFuncPtr +FHoudiniApi::ParmChoiceInfo_Create = &FHoudiniApi::ParmChoiceInfo_CreateEmptyStub; + +FHoudiniApi::ParmChoiceInfo_InitFuncPtr +FHoudiniApi::ParmChoiceInfo_Init = &FHoudiniApi::ParmChoiceInfo_InitEmptyStub; + +FHoudiniApi::ParmHasExpressionFuncPtr +FHoudiniApi::ParmHasExpression = &FHoudiniApi::ParmHasExpressionEmptyStub; + +FHoudiniApi::ParmHasTagFuncPtr +FHoudiniApi::ParmHasTag = &FHoudiniApi::ParmHasTagEmptyStub; + +FHoudiniApi::ParmInfo_CreateFuncPtr +FHoudiniApi::ParmInfo_Create = &FHoudiniApi::ParmInfo_CreateEmptyStub; + +FHoudiniApi::ParmInfo_GetFloatValueCountFuncPtr +FHoudiniApi::ParmInfo_GetFloatValueCount = &FHoudiniApi::ParmInfo_GetFloatValueCountEmptyStub; + +FHoudiniApi::ParmInfo_GetIntValueCountFuncPtr +FHoudiniApi::ParmInfo_GetIntValueCount = &FHoudiniApi::ParmInfo_GetIntValueCountEmptyStub; + +FHoudiniApi::ParmInfo_GetStringValueCountFuncPtr +FHoudiniApi::ParmInfo_GetStringValueCount = &FHoudiniApi::ParmInfo_GetStringValueCountEmptyStub; + +FHoudiniApi::ParmInfo_InitFuncPtr +FHoudiniApi::ParmInfo_Init = &FHoudiniApi::ParmInfo_InitEmptyStub; + +FHoudiniApi::ParmInfo_IsFloatFuncPtr +FHoudiniApi::ParmInfo_IsFloat = &FHoudiniApi::ParmInfo_IsFloatEmptyStub; + +FHoudiniApi::ParmInfo_IsIntFuncPtr +FHoudiniApi::ParmInfo_IsInt = &FHoudiniApi::ParmInfo_IsIntEmptyStub; + +FHoudiniApi::ParmInfo_IsNodeFuncPtr +FHoudiniApi::ParmInfo_IsNode = &FHoudiniApi::ParmInfo_IsNodeEmptyStub; + +FHoudiniApi::ParmInfo_IsNonValueFuncPtr +FHoudiniApi::ParmInfo_IsNonValue = &FHoudiniApi::ParmInfo_IsNonValueEmptyStub; + +FHoudiniApi::ParmInfo_IsPathFuncPtr +FHoudiniApi::ParmInfo_IsPath = &FHoudiniApi::ParmInfo_IsPathEmptyStub; + +FHoudiniApi::ParmInfo_IsStringFuncPtr +FHoudiniApi::ParmInfo_IsString = &FHoudiniApi::ParmInfo_IsStringEmptyStub; + +FHoudiniApi::PartInfo_CreateFuncPtr +FHoudiniApi::PartInfo_Create = &FHoudiniApi::PartInfo_CreateEmptyStub; + +FHoudiniApi::PartInfo_GetAttributeCountByOwnerFuncPtr +FHoudiniApi::PartInfo_GetAttributeCountByOwner = &FHoudiniApi::PartInfo_GetAttributeCountByOwnerEmptyStub; + +FHoudiniApi::PartInfo_GetElementCountByAttributeOwnerFuncPtr +FHoudiniApi::PartInfo_GetElementCountByAttributeOwner = &FHoudiniApi::PartInfo_GetElementCountByAttributeOwnerEmptyStub; + +FHoudiniApi::PartInfo_GetElementCountByGroupTypeFuncPtr +FHoudiniApi::PartInfo_GetElementCountByGroupType = &FHoudiniApi::PartInfo_GetElementCountByGroupTypeEmptyStub; + +FHoudiniApi::PartInfo_InitFuncPtr +FHoudiniApi::PartInfo_Init = &FHoudiniApi::PartInfo_InitEmptyStub; + +FHoudiniApi::PausePDGCookFuncPtr +FHoudiniApi::PausePDGCook = &FHoudiniApi::PausePDGCookEmptyStub; + +FHoudiniApi::PythonThreadInterpreterLockFuncPtr +FHoudiniApi::PythonThreadInterpreterLock = &FHoudiniApi::PythonThreadInterpreterLockEmptyStub; + +FHoudiniApi::QueryNodeInputFuncPtr +FHoudiniApi::QueryNodeInput = &FHoudiniApi::QueryNodeInputEmptyStub; + +FHoudiniApi::QueryNodeOutputConnectedCountFuncPtr +FHoudiniApi::QueryNodeOutputConnectedCount = &FHoudiniApi::QueryNodeOutputConnectedCountEmptyStub; + +FHoudiniApi::QueryNodeOutputConnectedNodesFuncPtr +FHoudiniApi::QueryNodeOutputConnectedNodes = &FHoudiniApi::QueryNodeOutputConnectedNodesEmptyStub; + +FHoudiniApi::RemoveCustomStringFuncPtr +FHoudiniApi::RemoveCustomString = &FHoudiniApi::RemoveCustomStringEmptyStub; + +FHoudiniApi::RemoveMultiparmInstanceFuncPtr +FHoudiniApi::RemoveMultiparmInstance = &FHoudiniApi::RemoveMultiparmInstanceEmptyStub; + +FHoudiniApi::RemoveParmExpressionFuncPtr +FHoudiniApi::RemoveParmExpression = &FHoudiniApi::RemoveParmExpressionEmptyStub; + +FHoudiniApi::RenameNodeFuncPtr +FHoudiniApi::RenameNode = &FHoudiniApi::RenameNodeEmptyStub; + +FHoudiniApi::RenderCOPOutputToImageFuncPtr +FHoudiniApi::RenderCOPOutputToImage = &FHoudiniApi::RenderCOPOutputToImageEmptyStub; + +FHoudiniApi::RenderCOPToImageFuncPtr +FHoudiniApi::RenderCOPToImage = &FHoudiniApi::RenderCOPToImageEmptyStub; + +FHoudiniApi::RenderTextureToImageFuncPtr +FHoudiniApi::RenderTextureToImage = &FHoudiniApi::RenderTextureToImageEmptyStub; + +FHoudiniApi::ResetSimulationFuncPtr +FHoudiniApi::ResetSimulation = &FHoudiniApi::ResetSimulationEmptyStub; + +FHoudiniApi::RevertGeoFuncPtr +FHoudiniApi::RevertGeo = &FHoudiniApi::RevertGeoEmptyStub; + +FHoudiniApi::RevertParmToDefaultFuncPtr +FHoudiniApi::RevertParmToDefault = &FHoudiniApi::RevertParmToDefaultEmptyStub; + +FHoudiniApi::RevertParmToDefaultsFuncPtr +FHoudiniApi::RevertParmToDefaults = &FHoudiniApi::RevertParmToDefaultsEmptyStub; + +FHoudiniApi::SaveGeoToFileFuncPtr +FHoudiniApi::SaveGeoToFile = &FHoudiniApi::SaveGeoToFileEmptyStub; + +FHoudiniApi::SaveGeoToMemoryFuncPtr +FHoudiniApi::SaveGeoToMemory = &FHoudiniApi::SaveGeoToMemoryEmptyStub; + +FHoudiniApi::SaveHIPFileFuncPtr +FHoudiniApi::SaveHIPFile = &FHoudiniApi::SaveHIPFileEmptyStub; + +FHoudiniApi::SaveNodeToFileFuncPtr +FHoudiniApi::SaveNodeToFile = &FHoudiniApi::SaveNodeToFileEmptyStub; + +FHoudiniApi::SessionInfo_CreateFuncPtr +FHoudiniApi::SessionInfo_Create = &FHoudiniApi::SessionInfo_CreateEmptyStub; + +FHoudiniApi::SessionInfo_InitFuncPtr +FHoudiniApi::SessionInfo_Init = &FHoudiniApi::SessionInfo_InitEmptyStub; + +FHoudiniApi::SessionSyncInfo_CreateFuncPtr +FHoudiniApi::SessionSyncInfo_Create = &FHoudiniApi::SessionSyncInfo_CreateEmptyStub; + +FHoudiniApi::SetAnimCurveFuncPtr +FHoudiniApi::SetAnimCurve = &FHoudiniApi::SetAnimCurveEmptyStub; + +FHoudiniApi::SetAttributeDictionaryArrayDataFuncPtr +FHoudiniApi::SetAttributeDictionaryArrayData = &FHoudiniApi::SetAttributeDictionaryArrayDataEmptyStub; + +FHoudiniApi::SetAttributeDictionaryArrayDataAsyncFuncPtr +FHoudiniApi::SetAttributeDictionaryArrayDataAsync = &FHoudiniApi::SetAttributeDictionaryArrayDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeDictionaryDataFuncPtr +FHoudiniApi::SetAttributeDictionaryData = &FHoudiniApi::SetAttributeDictionaryDataEmptyStub; + +FHoudiniApi::SetAttributeDictionaryDataAsyncFuncPtr +FHoudiniApi::SetAttributeDictionaryDataAsync = &FHoudiniApi::SetAttributeDictionaryDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeFloat64ArrayDataFuncPtr +FHoudiniApi::SetAttributeFloat64ArrayData = &FHoudiniApi::SetAttributeFloat64ArrayDataEmptyStub; + +FHoudiniApi::SetAttributeFloat64ArrayDataAsyncFuncPtr +FHoudiniApi::SetAttributeFloat64ArrayDataAsync = &FHoudiniApi::SetAttributeFloat64ArrayDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeFloat64DataFuncPtr +FHoudiniApi::SetAttributeFloat64Data = &FHoudiniApi::SetAttributeFloat64DataEmptyStub; + +FHoudiniApi::SetAttributeFloat64DataAsyncFuncPtr +FHoudiniApi::SetAttributeFloat64DataAsync = &FHoudiniApi::SetAttributeFloat64DataAsyncEmptyStub; + +FHoudiniApi::SetAttributeFloat64UniqueDataFuncPtr +FHoudiniApi::SetAttributeFloat64UniqueData = &FHoudiniApi::SetAttributeFloat64UniqueDataEmptyStub; + +FHoudiniApi::SetAttributeFloat64UniqueDataAsyncFuncPtr +FHoudiniApi::SetAttributeFloat64UniqueDataAsync = &FHoudiniApi::SetAttributeFloat64UniqueDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeFloatArrayDataFuncPtr +FHoudiniApi::SetAttributeFloatArrayData = &FHoudiniApi::SetAttributeFloatArrayDataEmptyStub; + +FHoudiniApi::SetAttributeFloatArrayDataAsyncFuncPtr +FHoudiniApi::SetAttributeFloatArrayDataAsync = &FHoudiniApi::SetAttributeFloatArrayDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeFloatDataFuncPtr +FHoudiniApi::SetAttributeFloatData = &FHoudiniApi::SetAttributeFloatDataEmptyStub; + +FHoudiniApi::SetAttributeFloatDataAsyncFuncPtr +FHoudiniApi::SetAttributeFloatDataAsync = &FHoudiniApi::SetAttributeFloatDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeFloatUniqueDataFuncPtr +FHoudiniApi::SetAttributeFloatUniqueData = &FHoudiniApi::SetAttributeFloatUniqueDataEmptyStub; + +FHoudiniApi::SetAttributeFloatUniqueDataAsyncFuncPtr +FHoudiniApi::SetAttributeFloatUniqueDataAsync = &FHoudiniApi::SetAttributeFloatUniqueDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeIndexedStringDataFuncPtr +FHoudiniApi::SetAttributeIndexedStringData = &FHoudiniApi::SetAttributeIndexedStringDataEmptyStub; + +FHoudiniApi::SetAttributeIndexedStringDataAsyncFuncPtr +FHoudiniApi::SetAttributeIndexedStringDataAsync = &FHoudiniApi::SetAttributeIndexedStringDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeInt16ArrayDataFuncPtr +FHoudiniApi::SetAttributeInt16ArrayData = &FHoudiniApi::SetAttributeInt16ArrayDataEmptyStub; + +FHoudiniApi::SetAttributeInt16ArrayDataAsyncFuncPtr +FHoudiniApi::SetAttributeInt16ArrayDataAsync = &FHoudiniApi::SetAttributeInt16ArrayDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeInt16DataFuncPtr +FHoudiniApi::SetAttributeInt16Data = &FHoudiniApi::SetAttributeInt16DataEmptyStub; + +FHoudiniApi::SetAttributeInt16DataAsyncFuncPtr +FHoudiniApi::SetAttributeInt16DataAsync = &FHoudiniApi::SetAttributeInt16DataAsyncEmptyStub; + +FHoudiniApi::SetAttributeInt16UniqueDataFuncPtr +FHoudiniApi::SetAttributeInt16UniqueData = &FHoudiniApi::SetAttributeInt16UniqueDataEmptyStub; + +FHoudiniApi::SetAttributeInt16UniqueDataAsyncFuncPtr +FHoudiniApi::SetAttributeInt16UniqueDataAsync = &FHoudiniApi::SetAttributeInt16UniqueDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeInt64ArrayDataFuncPtr +FHoudiniApi::SetAttributeInt64ArrayData = &FHoudiniApi::SetAttributeInt64ArrayDataEmptyStub; + +FHoudiniApi::SetAttributeInt64ArrayDataAsyncFuncPtr +FHoudiniApi::SetAttributeInt64ArrayDataAsync = &FHoudiniApi::SetAttributeInt64ArrayDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeInt64DataFuncPtr +FHoudiniApi::SetAttributeInt64Data = &FHoudiniApi::SetAttributeInt64DataEmptyStub; + +FHoudiniApi::SetAttributeInt64DataAsyncFuncPtr +FHoudiniApi::SetAttributeInt64DataAsync = &FHoudiniApi::SetAttributeInt64DataAsyncEmptyStub; + +FHoudiniApi::SetAttributeInt64UniqueDataFuncPtr +FHoudiniApi::SetAttributeInt64UniqueData = &FHoudiniApi::SetAttributeInt64UniqueDataEmptyStub; + +FHoudiniApi::SetAttributeInt64UniqueDataAsyncFuncPtr +FHoudiniApi::SetAttributeInt64UniqueDataAsync = &FHoudiniApi::SetAttributeInt64UniqueDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeInt8ArrayDataFuncPtr +FHoudiniApi::SetAttributeInt8ArrayData = &FHoudiniApi::SetAttributeInt8ArrayDataEmptyStub; + +FHoudiniApi::SetAttributeInt8ArrayDataAsyncFuncPtr +FHoudiniApi::SetAttributeInt8ArrayDataAsync = &FHoudiniApi::SetAttributeInt8ArrayDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeInt8DataFuncPtr +FHoudiniApi::SetAttributeInt8Data = &FHoudiniApi::SetAttributeInt8DataEmptyStub; + +FHoudiniApi::SetAttributeInt8DataAsyncFuncPtr +FHoudiniApi::SetAttributeInt8DataAsync = &FHoudiniApi::SetAttributeInt8DataAsyncEmptyStub; + +FHoudiniApi::SetAttributeInt8UniqueDataFuncPtr +FHoudiniApi::SetAttributeInt8UniqueData = &FHoudiniApi::SetAttributeInt8UniqueDataEmptyStub; + +FHoudiniApi::SetAttributeInt8UniqueDataAsyncFuncPtr +FHoudiniApi::SetAttributeInt8UniqueDataAsync = &FHoudiniApi::SetAttributeInt8UniqueDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeIntArrayDataFuncPtr +FHoudiniApi::SetAttributeIntArrayData = &FHoudiniApi::SetAttributeIntArrayDataEmptyStub; + +FHoudiniApi::SetAttributeIntArrayDataAsyncFuncPtr +FHoudiniApi::SetAttributeIntArrayDataAsync = &FHoudiniApi::SetAttributeIntArrayDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeIntDataFuncPtr +FHoudiniApi::SetAttributeIntData = &FHoudiniApi::SetAttributeIntDataEmptyStub; + +FHoudiniApi::SetAttributeIntDataAsyncFuncPtr +FHoudiniApi::SetAttributeIntDataAsync = &FHoudiniApi::SetAttributeIntDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeIntUniqueDataFuncPtr +FHoudiniApi::SetAttributeIntUniqueData = &FHoudiniApi::SetAttributeIntUniqueDataEmptyStub; + +FHoudiniApi::SetAttributeIntUniqueDataAsyncFuncPtr +FHoudiniApi::SetAttributeIntUniqueDataAsync = &FHoudiniApi::SetAttributeIntUniqueDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeStringArrayDataFuncPtr +FHoudiniApi::SetAttributeStringArrayData = &FHoudiniApi::SetAttributeStringArrayDataEmptyStub; + +FHoudiniApi::SetAttributeStringArrayDataAsyncFuncPtr +FHoudiniApi::SetAttributeStringArrayDataAsync = &FHoudiniApi::SetAttributeStringArrayDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeStringDataFuncPtr +FHoudiniApi::SetAttributeStringData = &FHoudiniApi::SetAttributeStringDataEmptyStub; + +FHoudiniApi::SetAttributeStringDataAsyncFuncPtr +FHoudiniApi::SetAttributeStringDataAsync = &FHoudiniApi::SetAttributeStringDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeStringUniqueDataFuncPtr +FHoudiniApi::SetAttributeStringUniqueData = &FHoudiniApi::SetAttributeStringUniqueDataEmptyStub; + +FHoudiniApi::SetAttributeStringUniqueDataAsyncFuncPtr +FHoudiniApi::SetAttributeStringUniqueDataAsync = &FHoudiniApi::SetAttributeStringUniqueDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeUInt8ArrayDataFuncPtr +FHoudiniApi::SetAttributeUInt8ArrayData = &FHoudiniApi::SetAttributeUInt8ArrayDataEmptyStub; + +FHoudiniApi::SetAttributeUInt8ArrayDataAsyncFuncPtr +FHoudiniApi::SetAttributeUInt8ArrayDataAsync = &FHoudiniApi::SetAttributeUInt8ArrayDataAsyncEmptyStub; + +FHoudiniApi::SetAttributeUInt8DataFuncPtr +FHoudiniApi::SetAttributeUInt8Data = &FHoudiniApi::SetAttributeUInt8DataEmptyStub; + +FHoudiniApi::SetAttributeUInt8DataAsyncFuncPtr +FHoudiniApi::SetAttributeUInt8DataAsync = &FHoudiniApi::SetAttributeUInt8DataAsyncEmptyStub; + +FHoudiniApi::SetAttributeUInt8UniqueDataFuncPtr +FHoudiniApi::SetAttributeUInt8UniqueData = &FHoudiniApi::SetAttributeUInt8UniqueDataEmptyStub; + +FHoudiniApi::SetAttributeUInt8UniqueDataAsyncFuncPtr +FHoudiniApi::SetAttributeUInt8UniqueDataAsync = &FHoudiniApi::SetAttributeUInt8UniqueDataAsyncEmptyStub; + +FHoudiniApi::SetCachePropertyFuncPtr +FHoudiniApi::SetCacheProperty = &FHoudiniApi::SetCachePropertyEmptyStub; + +FHoudiniApi::SetCompositorOptionsFuncPtr +FHoudiniApi::SetCompositorOptions = &FHoudiniApi::SetCompositorOptionsEmptyStub; + +FHoudiniApi::SetCurveCountsFuncPtr +FHoudiniApi::SetCurveCounts = &FHoudiniApi::SetCurveCountsEmptyStub; + +FHoudiniApi::SetCurveInfoFuncPtr +FHoudiniApi::SetCurveInfo = &FHoudiniApi::SetCurveInfoEmptyStub; + +FHoudiniApi::SetCurveKnotsFuncPtr +FHoudiniApi::SetCurveKnots = &FHoudiniApi::SetCurveKnotsEmptyStub; + +FHoudiniApi::SetCurveOrdersFuncPtr +FHoudiniApi::SetCurveOrders = &FHoudiniApi::SetCurveOrdersEmptyStub; + +FHoudiniApi::SetCustomStringFuncPtr +FHoudiniApi::SetCustomString = &FHoudiniApi::SetCustomStringEmptyStub; + +FHoudiniApi::SetFaceCountsFuncPtr +FHoudiniApi::SetFaceCounts = &FHoudiniApi::SetFaceCountsEmptyStub; + +FHoudiniApi::SetGroupMembershipFuncPtr +FHoudiniApi::SetGroupMembership = &FHoudiniApi::SetGroupMembershipEmptyStub; + +FHoudiniApi::SetHeightFieldDataFuncPtr +FHoudiniApi::SetHeightFieldData = &FHoudiniApi::SetHeightFieldDataEmptyStub; + +FHoudiniApi::SetImageInfoFuncPtr +FHoudiniApi::SetImageInfo = &FHoudiniApi::SetImageInfoEmptyStub; + +FHoudiniApi::SetInputCurveInfoFuncPtr +FHoudiniApi::SetInputCurveInfo = &FHoudiniApi::SetInputCurveInfoEmptyStub; + +FHoudiniApi::SetInputCurvePositionsFuncPtr +FHoudiniApi::SetInputCurvePositions = &FHoudiniApi::SetInputCurvePositionsEmptyStub; + +FHoudiniApi::SetInputCurvePositionsRotationsScalesFuncPtr +FHoudiniApi::SetInputCurvePositionsRotationsScales = &FHoudiniApi::SetInputCurvePositionsRotationsScalesEmptyStub; + +FHoudiniApi::SetNodeDisplayFuncPtr +FHoudiniApi::SetNodeDisplay = &FHoudiniApi::SetNodeDisplayEmptyStub; + +FHoudiniApi::SetObjectTransformFuncPtr +FHoudiniApi::SetObjectTransform = &FHoudiniApi::SetObjectTransformEmptyStub; + +FHoudiniApi::SetParmExpressionFuncPtr +FHoudiniApi::SetParmExpression = &FHoudiniApi::SetParmExpressionEmptyStub; + +FHoudiniApi::SetParmFloatValueFuncPtr +FHoudiniApi::SetParmFloatValue = &FHoudiniApi::SetParmFloatValueEmptyStub; + +FHoudiniApi::SetParmFloatValuesFuncPtr +FHoudiniApi::SetParmFloatValues = &FHoudiniApi::SetParmFloatValuesEmptyStub; + +FHoudiniApi::SetParmIntValueFuncPtr +FHoudiniApi::SetParmIntValue = &FHoudiniApi::SetParmIntValueEmptyStub; + +FHoudiniApi::SetParmIntValuesFuncPtr +FHoudiniApi::SetParmIntValues = &FHoudiniApi::SetParmIntValuesEmptyStub; + +FHoudiniApi::SetParmNodeValueFuncPtr +FHoudiniApi::SetParmNodeValue = &FHoudiniApi::SetParmNodeValueEmptyStub; + +FHoudiniApi::SetParmStringValueFuncPtr +FHoudiniApi::SetParmStringValue = &FHoudiniApi::SetParmStringValueEmptyStub; + +FHoudiniApi::SetPartInfoFuncPtr +FHoudiniApi::SetPartInfo = &FHoudiniApi::SetPartInfoEmptyStub; + +FHoudiniApi::SetPresetFuncPtr +FHoudiniApi::SetPreset = &FHoudiniApi::SetPresetEmptyStub; + +FHoudiniApi::SetServerEnvIntFuncPtr +FHoudiniApi::SetServerEnvInt = &FHoudiniApi::SetServerEnvIntEmptyStub; + +FHoudiniApi::SetServerEnvStringFuncPtr +FHoudiniApi::SetServerEnvString = &FHoudiniApi::SetServerEnvStringEmptyStub; + +FHoudiniApi::SetSessionSyncFuncPtr +FHoudiniApi::SetSessionSync = &FHoudiniApi::SetSessionSyncEmptyStub; + +FHoudiniApi::SetSessionSyncInfoFuncPtr +FHoudiniApi::SetSessionSyncInfo = &FHoudiniApi::SetSessionSyncInfoEmptyStub; + +FHoudiniApi::SetTimeFuncPtr +FHoudiniApi::SetTime = &FHoudiniApi::SetTimeEmptyStub; + +FHoudiniApi::SetTimelineOptionsFuncPtr +FHoudiniApi::SetTimelineOptions = &FHoudiniApi::SetTimelineOptionsEmptyStub; + +FHoudiniApi::SetTransformAnimCurveFuncPtr +FHoudiniApi::SetTransformAnimCurve = &FHoudiniApi::SetTransformAnimCurveEmptyStub; + +FHoudiniApi::SetUseHoudiniTimeFuncPtr +FHoudiniApi::SetUseHoudiniTime = &FHoudiniApi::SetUseHoudiniTimeEmptyStub; + +FHoudiniApi::SetVertexListFuncPtr +FHoudiniApi::SetVertexList = &FHoudiniApi::SetVertexListEmptyStub; + +FHoudiniApi::SetViewportFuncPtr +FHoudiniApi::SetViewport = &FHoudiniApi::SetViewportEmptyStub; + +FHoudiniApi::SetVolumeInfoFuncPtr +FHoudiniApi::SetVolumeInfo = &FHoudiniApi::SetVolumeInfoEmptyStub; + +FHoudiniApi::SetVolumeTileFloatDataFuncPtr +FHoudiniApi::SetVolumeTileFloatData = &FHoudiniApi::SetVolumeTileFloatDataEmptyStub; + +FHoudiniApi::SetVolumeTileIntDataFuncPtr +FHoudiniApi::SetVolumeTileIntData = &FHoudiniApi::SetVolumeTileIntDataEmptyStub; + +FHoudiniApi::SetVolumeVoxelFloatDataFuncPtr +FHoudiniApi::SetVolumeVoxelFloatData = &FHoudiniApi::SetVolumeVoxelFloatDataEmptyStub; + +FHoudiniApi::SetVolumeVoxelIntDataFuncPtr +FHoudiniApi::SetVolumeVoxelIntData = &FHoudiniApi::SetVolumeVoxelIntDataEmptyStub; + +FHoudiniApi::SetWorkItemFloatAttributeFuncPtr +FHoudiniApi::SetWorkItemFloatAttribute = &FHoudiniApi::SetWorkItemFloatAttributeEmptyStub; + +FHoudiniApi::SetWorkItemIntAttributeFuncPtr +FHoudiniApi::SetWorkItemIntAttribute = &FHoudiniApi::SetWorkItemIntAttributeEmptyStub; + +FHoudiniApi::SetWorkItemStringAttributeFuncPtr +FHoudiniApi::SetWorkItemStringAttribute = &FHoudiniApi::SetWorkItemStringAttributeEmptyStub; + +FHoudiniApi::SetWorkitemFloatDataFuncPtr +FHoudiniApi::SetWorkitemFloatData = &FHoudiniApi::SetWorkitemFloatDataEmptyStub; + +FHoudiniApi::SetWorkitemIntDataFuncPtr +FHoudiniApi::SetWorkitemIntData = &FHoudiniApi::SetWorkitemIntDataEmptyStub; + +FHoudiniApi::SetWorkitemStringDataFuncPtr +FHoudiniApi::SetWorkitemStringData = &FHoudiniApi::SetWorkitemStringDataEmptyStub; + +FHoudiniApi::ShutdownFuncPtr +FHoudiniApi::Shutdown = &FHoudiniApi::ShutdownEmptyStub; + +FHoudiniApi::StartPerformanceMonitorProfileFuncPtr +FHoudiniApi::StartPerformanceMonitorProfile = &FHoudiniApi::StartPerformanceMonitorProfileEmptyStub; + +FHoudiniApi::StartThriftNamedPipeServerFuncPtr +FHoudiniApi::StartThriftNamedPipeServer = &FHoudiniApi::StartThriftNamedPipeServerEmptyStub; + +FHoudiniApi::StartThriftSharedMemoryServerFuncPtr +FHoudiniApi::StartThriftSharedMemoryServer = &FHoudiniApi::StartThriftSharedMemoryServerEmptyStub; + +FHoudiniApi::StartThriftSocketServerFuncPtr +FHoudiniApi::StartThriftSocketServer = &FHoudiniApi::StartThriftSocketServerEmptyStub; + +FHoudiniApi::StopPerformanceMonitorProfileFuncPtr +FHoudiniApi::StopPerformanceMonitorProfile = &FHoudiniApi::StopPerformanceMonitorProfileEmptyStub; + +FHoudiniApi::ThriftServerOptions_CreateFuncPtr +FHoudiniApi::ThriftServerOptions_Create = &FHoudiniApi::ThriftServerOptions_CreateEmptyStub; + +FHoudiniApi::ThriftServerOptions_InitFuncPtr +FHoudiniApi::ThriftServerOptions_Init = &FHoudiniApi::ThriftServerOptions_InitEmptyStub; + +FHoudiniApi::TimelineOptions_CreateFuncPtr +FHoudiniApi::TimelineOptions_Create = &FHoudiniApi::TimelineOptions_CreateEmptyStub; + +FHoudiniApi::TimelineOptions_InitFuncPtr +FHoudiniApi::TimelineOptions_Init = &FHoudiniApi::TimelineOptions_InitEmptyStub; + +FHoudiniApi::TransformEuler_CreateFuncPtr +FHoudiniApi::TransformEuler_Create = &FHoudiniApi::TransformEuler_CreateEmptyStub; + +FHoudiniApi::TransformEuler_InitFuncPtr +FHoudiniApi::TransformEuler_Init = &FHoudiniApi::TransformEuler_InitEmptyStub; + +FHoudiniApi::Transform_CreateFuncPtr +FHoudiniApi::Transform_Create = &FHoudiniApi::Transform_CreateEmptyStub; + +FHoudiniApi::Transform_InitFuncPtr +FHoudiniApi::Transform_Init = &FHoudiniApi::Transform_InitEmptyStub; + +FHoudiniApi::Viewport_CreateFuncPtr +FHoudiniApi::Viewport_Create = &FHoudiniApi::Viewport_CreateEmptyStub; + +FHoudiniApi::VolumeInfo_CreateFuncPtr +FHoudiniApi::VolumeInfo_Create = &FHoudiniApi::VolumeInfo_CreateEmptyStub; + +FHoudiniApi::VolumeInfo_InitFuncPtr +FHoudiniApi::VolumeInfo_Init = &FHoudiniApi::VolumeInfo_InitEmptyStub; + +FHoudiniApi::VolumeTileInfo_CreateFuncPtr +FHoudiniApi::VolumeTileInfo_Create = &FHoudiniApi::VolumeTileInfo_CreateEmptyStub; + +FHoudiniApi::VolumeTileInfo_InitFuncPtr +FHoudiniApi::VolumeTileInfo_Init = &FHoudiniApi::VolumeTileInfo_InitEmptyStub; + + +void +FHoudiniApi::InitializeHAPI(void* LibraryHandle) +{ + if(!LibraryHandle) return; + + FHoudiniApi::AddAttribute = (AddAttributeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_AddAttribute")); + FHoudiniApi::AddGroup = (AddGroupFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_AddGroup")); + FHoudiniApi::AssetInfo_Create = (AssetInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_AssetInfo_Create")); + FHoudiniApi::AssetInfo_Init = (AssetInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_AssetInfo_Init")); + FHoudiniApi::AttributeInfo_Create = (AttributeInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_AttributeInfo_Create")); + FHoudiniApi::AttributeInfo_Init = (AttributeInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_AttributeInfo_Init")); + FHoudiniApi::BindCustomImplementation = (BindCustomImplementationFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_BindCustomImplementation")); + FHoudiniApi::CancelPDGCook = (CancelPDGCookFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CancelPDGCook")); + FHoudiniApi::CheckForSpecificErrors = (CheckForSpecificErrorsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CheckForSpecificErrors")); + FHoudiniApi::Cleanup = (CleanupFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_Cleanup")); + FHoudiniApi::ClearConnectionError = (ClearConnectionErrorFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ClearConnectionError")); + FHoudiniApi::CloseSession = (CloseSessionFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CloseSession")); + FHoudiniApi::CommitGeo = (CommitGeoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CommitGeo")); + FHoudiniApi::CommitWorkItems = (CommitWorkItemsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CommitWorkItems")); + FHoudiniApi::CommitWorkitems = (CommitWorkitemsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CommitWorkitems")); + FHoudiniApi::ComposeChildNodeList = (ComposeChildNodeListFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ComposeChildNodeList")); + FHoudiniApi::ComposeNodeCookResult = (ComposeNodeCookResultFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ComposeNodeCookResult")); + FHoudiniApi::ComposeObjectList = (ComposeObjectListFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ComposeObjectList")); + FHoudiniApi::CompositorOptions_Create = (CompositorOptions_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CompositorOptions_Create")); + FHoudiniApi::CompositorOptions_Init = (CompositorOptions_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CompositorOptions_Init")); + FHoudiniApi::ConnectNodeInput = (ConnectNodeInputFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ConnectNodeInput")); + FHoudiniApi::ConvertMatrixToEuler = (ConvertMatrixToEulerFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ConvertMatrixToEuler")); + FHoudiniApi::ConvertMatrixToQuat = (ConvertMatrixToQuatFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ConvertMatrixToQuat")); + FHoudiniApi::ConvertTransform = (ConvertTransformFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ConvertTransform")); + FHoudiniApi::ConvertTransformEulerToMatrix = (ConvertTransformEulerToMatrixFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ConvertTransformEulerToMatrix")); + FHoudiniApi::ConvertTransformQuatToMatrix = (ConvertTransformQuatToMatrixFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ConvertTransformQuatToMatrix")); + FHoudiniApi::CookNode = (CookNodeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CookNode")); + FHoudiniApi::CookOptions_AreEqual = (CookOptions_AreEqualFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CookOptions_AreEqual")); + FHoudiniApi::CookOptions_Create = (CookOptions_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CookOptions_Create")); + FHoudiniApi::CookOptions_Init = (CookOptions_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CookOptions_Init")); + FHoudiniApi::CookPDG = (CookPDGFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CookPDG")); + FHoudiniApi::CookPDGAllOutputs = (CookPDGAllOutputsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CookPDGAllOutputs")); + FHoudiniApi::CreateCOPImage = (CreateCOPImageFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CreateCOPImage")); + FHoudiniApi::CreateCustomSession = (CreateCustomSessionFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CreateCustomSession")); + FHoudiniApi::CreateHeightFieldInput = (CreateHeightFieldInputFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CreateHeightFieldInput")); + FHoudiniApi::CreateHeightfieldInputVolumeNode = (CreateHeightfieldInputVolumeNodeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CreateHeightfieldInputVolumeNode")); + FHoudiniApi::CreateInProcessSession = (CreateInProcessSessionFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CreateInProcessSession")); + FHoudiniApi::CreateInputCurveNode = (CreateInputCurveNodeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CreateInputCurveNode")); + FHoudiniApi::CreateInputNode = (CreateInputNodeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CreateInputNode")); + FHoudiniApi::CreateNode = (CreateNodeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CreateNode")); + FHoudiniApi::CreateThriftNamedPipeSession = (CreateThriftNamedPipeSessionFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CreateThriftNamedPipeSession")); + FHoudiniApi::CreateThriftSharedMemorySession = (CreateThriftSharedMemorySessionFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CreateThriftSharedMemorySession")); + FHoudiniApi::CreateThriftSocketSession = (CreateThriftSocketSessionFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CreateThriftSocketSession")); + FHoudiniApi::CreateWorkItem = (CreateWorkItemFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CreateWorkItem")); + FHoudiniApi::CreateWorkitem = (CreateWorkitemFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CreateWorkitem")); + FHoudiniApi::CurveInfo_Create = (CurveInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CurveInfo_Create")); + FHoudiniApi::CurveInfo_Init = (CurveInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_CurveInfo_Init")); + FHoudiniApi::DeleteAttribute = (DeleteAttributeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_DeleteAttribute")); + FHoudiniApi::DeleteGroup = (DeleteGroupFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_DeleteGroup")); + FHoudiniApi::DeleteNode = (DeleteNodeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_DeleteNode")); + FHoudiniApi::DirtyPDGNode = (DirtyPDGNodeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_DirtyPDGNode")); + FHoudiniApi::DisconnectNodeInput = (DisconnectNodeInputFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_DisconnectNodeInput")); + FHoudiniApi::DisconnectNodeOutputsAt = (DisconnectNodeOutputsAtFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_DisconnectNodeOutputsAt")); + FHoudiniApi::ExtractImageToFile = (ExtractImageToFileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ExtractImageToFile")); + FHoudiniApi::ExtractImageToMemory = (ExtractImageToMemoryFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ExtractImageToMemory")); + FHoudiniApi::GeoInfo_Create = (GeoInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GeoInfo_Create")); + FHoudiniApi::GeoInfo_GetGroupCountByType = (GeoInfo_GetGroupCountByTypeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GeoInfo_GetGroupCountByType")); + FHoudiniApi::GeoInfo_Init = (GeoInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GeoInfo_Init")); + FHoudiniApi::GetActiveCacheCount = (GetActiveCacheCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetActiveCacheCount")); + FHoudiniApi::GetActiveCacheNames = (GetActiveCacheNamesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetActiveCacheNames")); + FHoudiniApi::GetAssetDefinitionParmCounts = (GetAssetDefinitionParmCountsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAssetDefinitionParmCounts")); + FHoudiniApi::GetAssetDefinitionParmInfos = (GetAssetDefinitionParmInfosFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAssetDefinitionParmInfos")); + FHoudiniApi::GetAssetDefinitionParmTagName = (GetAssetDefinitionParmTagNameFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAssetDefinitionParmTagName")); + FHoudiniApi::GetAssetDefinitionParmTagValue = (GetAssetDefinitionParmTagValueFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAssetDefinitionParmTagValue")); + FHoudiniApi::GetAssetDefinitionParmValues = (GetAssetDefinitionParmValuesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAssetDefinitionParmValues")); + FHoudiniApi::GetAssetInfo = (GetAssetInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAssetInfo")); + FHoudiniApi::GetAssetLibraryFilePath = (GetAssetLibraryFilePathFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAssetLibraryFilePath")); + FHoudiniApi::GetAssetLibraryIds = (GetAssetLibraryIdsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAssetLibraryIds")); + FHoudiniApi::GetAttributeDictionaryArrayData = (GetAttributeDictionaryArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeDictionaryArrayData")); + FHoudiniApi::GetAttributeDictionaryArrayDataAsync = (GetAttributeDictionaryArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeDictionaryArrayDataAsync")); + FHoudiniApi::GetAttributeDictionaryData = (GetAttributeDictionaryDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeDictionaryData")); + FHoudiniApi::GetAttributeDictionaryDataAsync = (GetAttributeDictionaryDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeDictionaryDataAsync")); + FHoudiniApi::GetAttributeFloat64ArrayData = (GetAttributeFloat64ArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeFloat64ArrayData")); + FHoudiniApi::GetAttributeFloat64ArrayDataAsync = (GetAttributeFloat64ArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeFloat64ArrayDataAsync")); + FHoudiniApi::GetAttributeFloat64Data = (GetAttributeFloat64DataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeFloat64Data")); + FHoudiniApi::GetAttributeFloat64DataAsync = (GetAttributeFloat64DataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeFloat64DataAsync")); + FHoudiniApi::GetAttributeFloatArrayData = (GetAttributeFloatArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeFloatArrayData")); + FHoudiniApi::GetAttributeFloatArrayDataAsync = (GetAttributeFloatArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeFloatArrayDataAsync")); + FHoudiniApi::GetAttributeFloatData = (GetAttributeFloatDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeFloatData")); + FHoudiniApi::GetAttributeFloatDataAsync = (GetAttributeFloatDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeFloatDataAsync")); + FHoudiniApi::GetAttributeInfo = (GetAttributeInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeInfo")); + FHoudiniApi::GetAttributeInt16ArrayData = (GetAttributeInt16ArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeInt16ArrayData")); + FHoudiniApi::GetAttributeInt16ArrayDataAsync = (GetAttributeInt16ArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeInt16ArrayDataAsync")); + FHoudiniApi::GetAttributeInt16Data = (GetAttributeInt16DataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeInt16Data")); + FHoudiniApi::GetAttributeInt16DataAsync = (GetAttributeInt16DataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeInt16DataAsync")); + FHoudiniApi::GetAttributeInt64ArrayData = (GetAttributeInt64ArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeInt64ArrayData")); + FHoudiniApi::GetAttributeInt64ArrayDataAsync = (GetAttributeInt64ArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeInt64ArrayDataAsync")); + FHoudiniApi::GetAttributeInt64Data = (GetAttributeInt64DataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeInt64Data")); + FHoudiniApi::GetAttributeInt64DataAsync = (GetAttributeInt64DataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeInt64DataAsync")); + FHoudiniApi::GetAttributeInt8ArrayData = (GetAttributeInt8ArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeInt8ArrayData")); + FHoudiniApi::GetAttributeInt8ArrayDataAsync = (GetAttributeInt8ArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeInt8ArrayDataAsync")); + FHoudiniApi::GetAttributeInt8Data = (GetAttributeInt8DataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeInt8Data")); + FHoudiniApi::GetAttributeInt8DataAsync = (GetAttributeInt8DataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeInt8DataAsync")); + FHoudiniApi::GetAttributeIntArrayData = (GetAttributeIntArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeIntArrayData")); + FHoudiniApi::GetAttributeIntArrayDataAsync = (GetAttributeIntArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeIntArrayDataAsync")); + FHoudiniApi::GetAttributeIntData = (GetAttributeIntDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeIntData")); + FHoudiniApi::GetAttributeIntDataAsync = (GetAttributeIntDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeIntDataAsync")); + FHoudiniApi::GetAttributeNames = (GetAttributeNamesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeNames")); + FHoudiniApi::GetAttributeStringArrayData = (GetAttributeStringArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeStringArrayData")); + FHoudiniApi::GetAttributeStringArrayDataAsync = (GetAttributeStringArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeStringArrayDataAsync")); + FHoudiniApi::GetAttributeStringData = (GetAttributeStringDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeStringData")); + FHoudiniApi::GetAttributeStringDataAsync = (GetAttributeStringDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeStringDataAsync")); + FHoudiniApi::GetAttributeUInt8ArrayData = (GetAttributeUInt8ArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeUInt8ArrayData")); + FHoudiniApi::GetAttributeUInt8ArrayDataAsync = (GetAttributeUInt8ArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeUInt8ArrayDataAsync")); + FHoudiniApi::GetAttributeUInt8Data = (GetAttributeUInt8DataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeUInt8Data")); + FHoudiniApi::GetAttributeUInt8DataAsync = (GetAttributeUInt8DataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAttributeUInt8DataAsync")); + FHoudiniApi::GetAvailableAssetCount = (GetAvailableAssetCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAvailableAssetCount")); + FHoudiniApi::GetAvailableAssets = (GetAvailableAssetsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetAvailableAssets")); + FHoudiniApi::GetBoxInfo = (GetBoxInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetBoxInfo")); + FHoudiniApi::GetCacheProperty = (GetCachePropertyFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetCacheProperty")); + FHoudiniApi::GetComposedChildNodeList = (GetComposedChildNodeListFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetComposedChildNodeList")); + FHoudiniApi::GetComposedNodeCookResult = (GetComposedNodeCookResultFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetComposedNodeCookResult")); + FHoudiniApi::GetComposedObjectList = (GetComposedObjectListFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetComposedObjectList")); + FHoudiniApi::GetComposedObjectTransforms = (GetComposedObjectTransformsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetComposedObjectTransforms")); + FHoudiniApi::GetCompositorOptions = (GetCompositorOptionsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetCompositorOptions")); + FHoudiniApi::GetConnectionError = (GetConnectionErrorFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetConnectionError")); + FHoudiniApi::GetConnectionErrorLength = (GetConnectionErrorLengthFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetConnectionErrorLength")); + FHoudiniApi::GetCookingCurrentCount = (GetCookingCurrentCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetCookingCurrentCount")); + FHoudiniApi::GetCookingTotalCount = (GetCookingTotalCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetCookingTotalCount")); + FHoudiniApi::GetCurveCounts = (GetCurveCountsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetCurveCounts")); + FHoudiniApi::GetCurveInfo = (GetCurveInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetCurveInfo")); + FHoudiniApi::GetCurveKnots = (GetCurveKnotsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetCurveKnots")); + FHoudiniApi::GetCurveOrders = (GetCurveOrdersFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetCurveOrders")); + FHoudiniApi::GetDisplayGeoInfo = (GetDisplayGeoInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetDisplayGeoInfo")); + FHoudiniApi::GetEdgeCountOfEdgeGroup = (GetEdgeCountOfEdgeGroupFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetEdgeCountOfEdgeGroup")); + FHoudiniApi::GetEnvInt = (GetEnvIntFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetEnvInt")); + FHoudiniApi::GetFaceCounts = (GetFaceCountsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetFaceCounts")); + FHoudiniApi::GetFirstVolumeTile = (GetFirstVolumeTileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetFirstVolumeTile")); + FHoudiniApi::GetGeoInfo = (GetGeoInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetGeoInfo")); + FHoudiniApi::GetGeoSize = (GetGeoSizeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetGeoSize")); + FHoudiniApi::GetGroupCountOnPackedInstancePart = (GetGroupCountOnPackedInstancePartFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetGroupCountOnPackedInstancePart")); + FHoudiniApi::GetGroupMembership = (GetGroupMembershipFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetGroupMembership")); + FHoudiniApi::GetGroupMembershipOnPackedInstancePart = (GetGroupMembershipOnPackedInstancePartFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetGroupMembershipOnPackedInstancePart")); + FHoudiniApi::GetGroupNames = (GetGroupNamesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetGroupNames")); + FHoudiniApi::GetGroupNamesOnPackedInstancePart = (GetGroupNamesOnPackedInstancePartFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetGroupNamesOnPackedInstancePart")); + FHoudiniApi::GetHIPFileNodeCount = (GetHIPFileNodeCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetHIPFileNodeCount")); + FHoudiniApi::GetHIPFileNodeIds = (GetHIPFileNodeIdsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetHIPFileNodeIds")); + FHoudiniApi::GetHandleBindingInfo = (GetHandleBindingInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetHandleBindingInfo")); + FHoudiniApi::GetHandleInfo = (GetHandleInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetHandleInfo")); + FHoudiniApi::GetHeightFieldData = (GetHeightFieldDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetHeightFieldData")); + FHoudiniApi::GetImageFilePath = (GetImageFilePathFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetImageFilePath")); + FHoudiniApi::GetImageInfo = (GetImageInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetImageInfo")); + FHoudiniApi::GetImageMemoryBuffer = (GetImageMemoryBufferFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetImageMemoryBuffer")); + FHoudiniApi::GetImagePlaneCount = (GetImagePlaneCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetImagePlaneCount")); + FHoudiniApi::GetImagePlanes = (GetImagePlanesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetImagePlanes")); + FHoudiniApi::GetInputCurveInfo = (GetInputCurveInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetInputCurveInfo")); + FHoudiniApi::GetInstanceTransformsOnPart = (GetInstanceTransformsOnPartFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetInstanceTransformsOnPart")); + FHoudiniApi::GetInstancedObjectIds = (GetInstancedObjectIdsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetInstancedObjectIds")); + FHoudiniApi::GetInstancedPartIds = (GetInstancedPartIdsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetInstancedPartIds")); + FHoudiniApi::GetInstancerPartTransforms = (GetInstancerPartTransformsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetInstancerPartTransforms")); + FHoudiniApi::GetJobStatus = (GetJobStatusFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetJobStatus")); + FHoudiniApi::GetLoadedAssetLibraryCount = (GetLoadedAssetLibraryCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetLoadedAssetLibraryCount")); + FHoudiniApi::GetManagerNodeId = (GetManagerNodeIdFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetManagerNodeId")); + FHoudiniApi::GetMaterialInfo = (GetMaterialInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetMaterialInfo")); + FHoudiniApi::GetMaterialNodeIdsOnFaces = (GetMaterialNodeIdsOnFacesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetMaterialNodeIdsOnFaces")); + FHoudiniApi::GetMessageNodeCount = (GetMessageNodeCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetMessageNodeCount")); + FHoudiniApi::GetMessageNodeIds = (GetMessageNodeIdsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetMessageNodeIds")); + FHoudiniApi::GetNextVolumeTile = (GetNextVolumeTileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetNextVolumeTile")); + FHoudiniApi::GetNodeCookResult = (GetNodeCookResultFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetNodeCookResult")); + FHoudiniApi::GetNodeCookResultLength = (GetNodeCookResultLengthFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetNodeCookResultLength")); + FHoudiniApi::GetNodeFromPath = (GetNodeFromPathFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetNodeFromPath")); + FHoudiniApi::GetNodeInfo = (GetNodeInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetNodeInfo")); + FHoudiniApi::GetNodeInputName = (GetNodeInputNameFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetNodeInputName")); + FHoudiniApi::GetNodeOutputName = (GetNodeOutputNameFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetNodeOutputName")); + FHoudiniApi::GetNodePath = (GetNodePathFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetNodePath")); + FHoudiniApi::GetNumWorkItems = (GetNumWorkItemsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetNumWorkItems")); + FHoudiniApi::GetNumWorkitems = (GetNumWorkitemsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetNumWorkitems")); + FHoudiniApi::GetObjectInfo = (GetObjectInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetObjectInfo")); + FHoudiniApi::GetObjectTransform = (GetObjectTransformFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetObjectTransform")); + FHoudiniApi::GetOutputGeoCount = (GetOutputGeoCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetOutputGeoCount")); + FHoudiniApi::GetOutputGeoInfos = (GetOutputGeoInfosFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetOutputGeoInfos")); + FHoudiniApi::GetOutputNodeId = (GetOutputNodeIdFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetOutputNodeId")); + FHoudiniApi::GetPDGEvents = (GetPDGEventsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetPDGEvents")); + FHoudiniApi::GetPDGGraphContextId = (GetPDGGraphContextIdFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetPDGGraphContextId")); + FHoudiniApi::GetPDGGraphContexts = (GetPDGGraphContextsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetPDGGraphContexts")); + FHoudiniApi::GetPDGGraphContextsCount = (GetPDGGraphContextsCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetPDGGraphContextsCount")); + FHoudiniApi::GetPDGState = (GetPDGStateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetPDGState")); + FHoudiniApi::GetParameters = (GetParametersFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParameters")); + FHoudiniApi::GetParmChoiceLists = (GetParmChoiceListsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmChoiceLists")); + FHoudiniApi::GetParmExpression = (GetParmExpressionFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmExpression")); + FHoudiniApi::GetParmFile = (GetParmFileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmFile")); + FHoudiniApi::GetParmFloatValue = (GetParmFloatValueFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmFloatValue")); + FHoudiniApi::GetParmFloatValues = (GetParmFloatValuesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmFloatValues")); + FHoudiniApi::GetParmIdFromName = (GetParmIdFromNameFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmIdFromName")); + FHoudiniApi::GetParmInfo = (GetParmInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmInfo")); + FHoudiniApi::GetParmInfoFromName = (GetParmInfoFromNameFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmInfoFromName")); + FHoudiniApi::GetParmIntValue = (GetParmIntValueFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmIntValue")); + FHoudiniApi::GetParmIntValues = (GetParmIntValuesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmIntValues")); + FHoudiniApi::GetParmNodeValue = (GetParmNodeValueFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmNodeValue")); + FHoudiniApi::GetParmStringValue = (GetParmStringValueFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmStringValue")); + FHoudiniApi::GetParmStringValues = (GetParmStringValuesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmStringValues")); + FHoudiniApi::GetParmTagName = (GetParmTagNameFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmTagName")); + FHoudiniApi::GetParmTagValue = (GetParmTagValueFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmTagValue")); + FHoudiniApi::GetParmWithTag = (GetParmWithTagFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetParmWithTag")); + FHoudiniApi::GetPartInfo = (GetPartInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetPartInfo")); + FHoudiniApi::GetPreset = (GetPresetFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetPreset")); + FHoudiniApi::GetPresetBufLength = (GetPresetBufLengthFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetPresetBufLength")); + FHoudiniApi::GetPresetCount = (GetPresetCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetPresetCount")); + FHoudiniApi::GetPresetNames = (GetPresetNamesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetPresetNames")); + FHoudiniApi::GetServerEnvInt = (GetServerEnvIntFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetServerEnvInt")); + FHoudiniApi::GetServerEnvString = (GetServerEnvStringFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetServerEnvString")); + FHoudiniApi::GetServerEnvVarCount = (GetServerEnvVarCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetServerEnvVarCount")); + FHoudiniApi::GetServerEnvVarList = (GetServerEnvVarListFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetServerEnvVarList")); + FHoudiniApi::GetSessionEnvInt = (GetSessionEnvIntFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetSessionEnvInt")); + FHoudiniApi::GetSessionSyncInfo = (GetSessionSyncInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetSessionSyncInfo")); + FHoudiniApi::GetSphereInfo = (GetSphereInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetSphereInfo")); + FHoudiniApi::GetStatus = (GetStatusFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetStatus")); + FHoudiniApi::GetStatusString = (GetStatusStringFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetStatusString")); + FHoudiniApi::GetStatusStringBufLength = (GetStatusStringBufLengthFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetStatusStringBufLength")); + FHoudiniApi::GetString = (GetStringFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetString")); + FHoudiniApi::GetStringBatch = (GetStringBatchFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetStringBatch")); + FHoudiniApi::GetStringBatchSize = (GetStringBatchSizeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetStringBatchSize")); + FHoudiniApi::GetStringBufLength = (GetStringBufLengthFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetStringBufLength")); + FHoudiniApi::GetSupportedImageFileFormatCount = (GetSupportedImageFileFormatCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetSupportedImageFileFormatCount")); + FHoudiniApi::GetSupportedImageFileFormats = (GetSupportedImageFileFormatsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetSupportedImageFileFormats")); + FHoudiniApi::GetTime = (GetTimeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetTime")); + FHoudiniApi::GetTimelineOptions = (GetTimelineOptionsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetTimelineOptions")); + FHoudiniApi::GetTotalCookCount = (GetTotalCookCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetTotalCookCount")); + FHoudiniApi::GetUseHoudiniTime = (GetUseHoudiniTimeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetUseHoudiniTime")); + FHoudiniApi::GetVertexList = (GetVertexListFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetVertexList")); + FHoudiniApi::GetViewport = (GetViewportFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetViewport")); + FHoudiniApi::GetVolumeBounds = (GetVolumeBoundsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetVolumeBounds")); + FHoudiniApi::GetVolumeInfo = (GetVolumeInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetVolumeInfo")); + FHoudiniApi::GetVolumeTileFloatData = (GetVolumeTileFloatDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetVolumeTileFloatData")); + FHoudiniApi::GetVolumeTileIntData = (GetVolumeTileIntDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetVolumeTileIntData")); + FHoudiniApi::GetVolumeVisualInfo = (GetVolumeVisualInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetVolumeVisualInfo")); + FHoudiniApi::GetVolumeVoxelFloatData = (GetVolumeVoxelFloatDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetVolumeVoxelFloatData")); + FHoudiniApi::GetVolumeVoxelIntData = (GetVolumeVoxelIntDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetVolumeVoxelIntData")); + FHoudiniApi::GetWorkItemAttributeSize = (GetWorkItemAttributeSizeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkItemAttributeSize")); + FHoudiniApi::GetWorkItemFloatAttribute = (GetWorkItemFloatAttributeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkItemFloatAttribute")); + FHoudiniApi::GetWorkItemInfo = (GetWorkItemInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkItemInfo")); + FHoudiniApi::GetWorkItemIntAttribute = (GetWorkItemIntAttributeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkItemIntAttribute")); + FHoudiniApi::GetWorkItemOutputFiles = (GetWorkItemOutputFilesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkItemOutputFiles")); + FHoudiniApi::GetWorkItemStringAttribute = (GetWorkItemStringAttributeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkItemStringAttribute")); + FHoudiniApi::GetWorkItems = (GetWorkItemsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkItems")); + FHoudiniApi::GetWorkitemDataLength = (GetWorkitemDataLengthFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkitemDataLength")); + FHoudiniApi::GetWorkitemFloatData = (GetWorkitemFloatDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkitemFloatData")); + FHoudiniApi::GetWorkitemInfo = (GetWorkitemInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkitemInfo")); + FHoudiniApi::GetWorkitemIntData = (GetWorkitemIntDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkitemIntData")); + FHoudiniApi::GetWorkitemResultInfo = (GetWorkitemResultInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkitemResultInfo")); + FHoudiniApi::GetWorkitemStringData = (GetWorkitemStringDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkitemStringData")); + FHoudiniApi::GetWorkitems = (GetWorkitemsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_GetWorkitems")); + FHoudiniApi::HandleBindingInfo_Create = (HandleBindingInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_HandleBindingInfo_Create")); + FHoudiniApi::HandleBindingInfo_Init = (HandleBindingInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_HandleBindingInfo_Init")); + FHoudiniApi::HandleInfo_Create = (HandleInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_HandleInfo_Create")); + FHoudiniApi::HandleInfo_Init = (HandleInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_HandleInfo_Init")); + FHoudiniApi::ImageFileFormat_Create = (ImageFileFormat_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ImageFileFormat_Create")); + FHoudiniApi::ImageFileFormat_Init = (ImageFileFormat_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ImageFileFormat_Init")); + FHoudiniApi::ImageInfo_Create = (ImageInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ImageInfo_Create")); + FHoudiniApi::ImageInfo_Init = (ImageInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ImageInfo_Init")); + FHoudiniApi::Initialize = (InitializeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_Initialize")); + FHoudiniApi::InputCurveInfo_Create = (InputCurveInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_InputCurveInfo_Create")); + FHoudiniApi::InputCurveInfo_Init = (InputCurveInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_InputCurveInfo_Init")); + FHoudiniApi::InsertMultiparmInstance = (InsertMultiparmInstanceFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_InsertMultiparmInstance")); + FHoudiniApi::Interrupt = (InterruptFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_Interrupt")); + FHoudiniApi::IsInitialized = (IsInitializedFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_IsInitialized")); + FHoudiniApi::IsNodeValid = (IsNodeValidFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_IsNodeValid")); + FHoudiniApi::IsSessionValid = (IsSessionValidFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_IsSessionValid")); + FHoudiniApi::Keyframe_Create = (Keyframe_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_Keyframe_Create")); + FHoudiniApi::Keyframe_Init = (Keyframe_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_Keyframe_Init")); + FHoudiniApi::LoadAssetLibraryFromFile = (LoadAssetLibraryFromFileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_LoadAssetLibraryFromFile")); + FHoudiniApi::LoadAssetLibraryFromMemory = (LoadAssetLibraryFromMemoryFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_LoadAssetLibraryFromMemory")); + FHoudiniApi::LoadGeoFromFile = (LoadGeoFromFileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_LoadGeoFromFile")); + FHoudiniApi::LoadGeoFromMemory = (LoadGeoFromMemoryFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_LoadGeoFromMemory")); + FHoudiniApi::LoadHIPFile = (LoadHIPFileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_LoadHIPFile")); + FHoudiniApi::LoadNodeFromFile = (LoadNodeFromFileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_LoadNodeFromFile")); + FHoudiniApi::MaterialInfo_Create = (MaterialInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_MaterialInfo_Create")); + FHoudiniApi::MaterialInfo_Init = (MaterialInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_MaterialInfo_Init")); + FHoudiniApi::MergeHIPFile = (MergeHIPFileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_MergeHIPFile")); + FHoudiniApi::NodeInfo_Create = (NodeInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_NodeInfo_Create")); + FHoudiniApi::NodeInfo_Init = (NodeInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_NodeInfo_Init")); + FHoudiniApi::ObjectInfo_Create = (ObjectInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ObjectInfo_Create")); + FHoudiniApi::ObjectInfo_Init = (ObjectInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ObjectInfo_Init")); + FHoudiniApi::ParmChoiceInfo_Create = (ParmChoiceInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmChoiceInfo_Create")); + FHoudiniApi::ParmChoiceInfo_Init = (ParmChoiceInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmChoiceInfo_Init")); + FHoudiniApi::ParmHasExpression = (ParmHasExpressionFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmHasExpression")); + FHoudiniApi::ParmHasTag = (ParmHasTagFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmHasTag")); + FHoudiniApi::ParmInfo_Create = (ParmInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmInfo_Create")); + FHoudiniApi::ParmInfo_GetFloatValueCount = (ParmInfo_GetFloatValueCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmInfo_GetFloatValueCount")); + FHoudiniApi::ParmInfo_GetIntValueCount = (ParmInfo_GetIntValueCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmInfo_GetIntValueCount")); + FHoudiniApi::ParmInfo_GetStringValueCount = (ParmInfo_GetStringValueCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmInfo_GetStringValueCount")); + FHoudiniApi::ParmInfo_Init = (ParmInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmInfo_Init")); + FHoudiniApi::ParmInfo_IsFloat = (ParmInfo_IsFloatFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmInfo_IsFloat")); + FHoudiniApi::ParmInfo_IsInt = (ParmInfo_IsIntFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmInfo_IsInt")); + FHoudiniApi::ParmInfo_IsNode = (ParmInfo_IsNodeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmInfo_IsNode")); + FHoudiniApi::ParmInfo_IsNonValue = (ParmInfo_IsNonValueFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmInfo_IsNonValue")); + FHoudiniApi::ParmInfo_IsPath = (ParmInfo_IsPathFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmInfo_IsPath")); + FHoudiniApi::ParmInfo_IsString = (ParmInfo_IsStringFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ParmInfo_IsString")); + FHoudiniApi::PartInfo_Create = (PartInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_PartInfo_Create")); + FHoudiniApi::PartInfo_GetAttributeCountByOwner = (PartInfo_GetAttributeCountByOwnerFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_PartInfo_GetAttributeCountByOwner")); + FHoudiniApi::PartInfo_GetElementCountByAttributeOwner = (PartInfo_GetElementCountByAttributeOwnerFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_PartInfo_GetElementCountByAttributeOwner")); + FHoudiniApi::PartInfo_GetElementCountByGroupType = (PartInfo_GetElementCountByGroupTypeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_PartInfo_GetElementCountByGroupType")); + FHoudiniApi::PartInfo_Init = (PartInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_PartInfo_Init")); + FHoudiniApi::PausePDGCook = (PausePDGCookFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_PausePDGCook")); + FHoudiniApi::PythonThreadInterpreterLock = (PythonThreadInterpreterLockFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_PythonThreadInterpreterLock")); + FHoudiniApi::QueryNodeInput = (QueryNodeInputFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_QueryNodeInput")); + FHoudiniApi::QueryNodeOutputConnectedCount = (QueryNodeOutputConnectedCountFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_QueryNodeOutputConnectedCount")); + FHoudiniApi::QueryNodeOutputConnectedNodes = (QueryNodeOutputConnectedNodesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_QueryNodeOutputConnectedNodes")); + FHoudiniApi::RemoveCustomString = (RemoveCustomStringFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_RemoveCustomString")); + FHoudiniApi::RemoveMultiparmInstance = (RemoveMultiparmInstanceFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_RemoveMultiparmInstance")); + FHoudiniApi::RemoveParmExpression = (RemoveParmExpressionFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_RemoveParmExpression")); + FHoudiniApi::RenameNode = (RenameNodeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_RenameNode")); + FHoudiniApi::RenderCOPOutputToImage = (RenderCOPOutputToImageFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_RenderCOPOutputToImage")); + FHoudiniApi::RenderCOPToImage = (RenderCOPToImageFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_RenderCOPToImage")); + FHoudiniApi::RenderTextureToImage = (RenderTextureToImageFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_RenderTextureToImage")); + FHoudiniApi::ResetSimulation = (ResetSimulationFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ResetSimulation")); + FHoudiniApi::RevertGeo = (RevertGeoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_RevertGeo")); + FHoudiniApi::RevertParmToDefault = (RevertParmToDefaultFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_RevertParmToDefault")); + FHoudiniApi::RevertParmToDefaults = (RevertParmToDefaultsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_RevertParmToDefaults")); + FHoudiniApi::SaveGeoToFile = (SaveGeoToFileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SaveGeoToFile")); + FHoudiniApi::SaveGeoToMemory = (SaveGeoToMemoryFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SaveGeoToMemory")); + FHoudiniApi::SaveHIPFile = (SaveHIPFileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SaveHIPFile")); + FHoudiniApi::SaveNodeToFile = (SaveNodeToFileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SaveNodeToFile")); + FHoudiniApi::SessionInfo_Create = (SessionInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SessionInfo_Create")); + FHoudiniApi::SessionInfo_Init = (SessionInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SessionInfo_Init")); + FHoudiniApi::SessionSyncInfo_Create = (SessionSyncInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SessionSyncInfo_Create")); + FHoudiniApi::SetAnimCurve = (SetAnimCurveFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAnimCurve")); + FHoudiniApi::SetAttributeDictionaryArrayData = (SetAttributeDictionaryArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeDictionaryArrayData")); + FHoudiniApi::SetAttributeDictionaryArrayDataAsync = (SetAttributeDictionaryArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeDictionaryArrayDataAsync")); + FHoudiniApi::SetAttributeDictionaryData = (SetAttributeDictionaryDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeDictionaryData")); + FHoudiniApi::SetAttributeDictionaryDataAsync = (SetAttributeDictionaryDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeDictionaryDataAsync")); + FHoudiniApi::SetAttributeFloat64ArrayData = (SetAttributeFloat64ArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeFloat64ArrayData")); + FHoudiniApi::SetAttributeFloat64ArrayDataAsync = (SetAttributeFloat64ArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeFloat64ArrayDataAsync")); + FHoudiniApi::SetAttributeFloat64Data = (SetAttributeFloat64DataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeFloat64Data")); + FHoudiniApi::SetAttributeFloat64DataAsync = (SetAttributeFloat64DataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeFloat64DataAsync")); + FHoudiniApi::SetAttributeFloat64UniqueData = (SetAttributeFloat64UniqueDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeFloat64UniqueData")); + FHoudiniApi::SetAttributeFloat64UniqueDataAsync = (SetAttributeFloat64UniqueDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeFloat64UniqueDataAsync")); + FHoudiniApi::SetAttributeFloatArrayData = (SetAttributeFloatArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeFloatArrayData")); + FHoudiniApi::SetAttributeFloatArrayDataAsync = (SetAttributeFloatArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeFloatArrayDataAsync")); + FHoudiniApi::SetAttributeFloatData = (SetAttributeFloatDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeFloatData")); + FHoudiniApi::SetAttributeFloatDataAsync = (SetAttributeFloatDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeFloatDataAsync")); + FHoudiniApi::SetAttributeFloatUniqueData = (SetAttributeFloatUniqueDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeFloatUniqueData")); + FHoudiniApi::SetAttributeFloatUniqueDataAsync = (SetAttributeFloatUniqueDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeFloatUniqueDataAsync")); + FHoudiniApi::SetAttributeIndexedStringData = (SetAttributeIndexedStringDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeIndexedStringData")); + FHoudiniApi::SetAttributeIndexedStringDataAsync = (SetAttributeIndexedStringDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeIndexedStringDataAsync")); + FHoudiniApi::SetAttributeInt16ArrayData = (SetAttributeInt16ArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt16ArrayData")); + FHoudiniApi::SetAttributeInt16ArrayDataAsync = (SetAttributeInt16ArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt16ArrayDataAsync")); + FHoudiniApi::SetAttributeInt16Data = (SetAttributeInt16DataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt16Data")); + FHoudiniApi::SetAttributeInt16DataAsync = (SetAttributeInt16DataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt16DataAsync")); + FHoudiniApi::SetAttributeInt16UniqueData = (SetAttributeInt16UniqueDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt16UniqueData")); + FHoudiniApi::SetAttributeInt16UniqueDataAsync = (SetAttributeInt16UniqueDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt16UniqueDataAsync")); + FHoudiniApi::SetAttributeInt64ArrayData = (SetAttributeInt64ArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt64ArrayData")); + FHoudiniApi::SetAttributeInt64ArrayDataAsync = (SetAttributeInt64ArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt64ArrayDataAsync")); + FHoudiniApi::SetAttributeInt64Data = (SetAttributeInt64DataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt64Data")); + FHoudiniApi::SetAttributeInt64DataAsync = (SetAttributeInt64DataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt64DataAsync")); + FHoudiniApi::SetAttributeInt64UniqueData = (SetAttributeInt64UniqueDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt64UniqueData")); + FHoudiniApi::SetAttributeInt64UniqueDataAsync = (SetAttributeInt64UniqueDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt64UniqueDataAsync")); + FHoudiniApi::SetAttributeInt8ArrayData = (SetAttributeInt8ArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt8ArrayData")); + FHoudiniApi::SetAttributeInt8ArrayDataAsync = (SetAttributeInt8ArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt8ArrayDataAsync")); + FHoudiniApi::SetAttributeInt8Data = (SetAttributeInt8DataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt8Data")); + FHoudiniApi::SetAttributeInt8DataAsync = (SetAttributeInt8DataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt8DataAsync")); + FHoudiniApi::SetAttributeInt8UniqueData = (SetAttributeInt8UniqueDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt8UniqueData")); + FHoudiniApi::SetAttributeInt8UniqueDataAsync = (SetAttributeInt8UniqueDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeInt8UniqueDataAsync")); + FHoudiniApi::SetAttributeIntArrayData = (SetAttributeIntArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeIntArrayData")); + FHoudiniApi::SetAttributeIntArrayDataAsync = (SetAttributeIntArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeIntArrayDataAsync")); + FHoudiniApi::SetAttributeIntData = (SetAttributeIntDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeIntData")); + FHoudiniApi::SetAttributeIntDataAsync = (SetAttributeIntDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeIntDataAsync")); + FHoudiniApi::SetAttributeIntUniqueData = (SetAttributeIntUniqueDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeIntUniqueData")); + FHoudiniApi::SetAttributeIntUniqueDataAsync = (SetAttributeIntUniqueDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeIntUniqueDataAsync")); + FHoudiniApi::SetAttributeStringArrayData = (SetAttributeStringArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeStringArrayData")); + FHoudiniApi::SetAttributeStringArrayDataAsync = (SetAttributeStringArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeStringArrayDataAsync")); + FHoudiniApi::SetAttributeStringData = (SetAttributeStringDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeStringData")); + FHoudiniApi::SetAttributeStringDataAsync = (SetAttributeStringDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeStringDataAsync")); + FHoudiniApi::SetAttributeStringUniqueData = (SetAttributeStringUniqueDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeStringUniqueData")); + FHoudiniApi::SetAttributeStringUniqueDataAsync = (SetAttributeStringUniqueDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeStringUniqueDataAsync")); + FHoudiniApi::SetAttributeUInt8ArrayData = (SetAttributeUInt8ArrayDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeUInt8ArrayData")); + FHoudiniApi::SetAttributeUInt8ArrayDataAsync = (SetAttributeUInt8ArrayDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeUInt8ArrayDataAsync")); + FHoudiniApi::SetAttributeUInt8Data = (SetAttributeUInt8DataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeUInt8Data")); + FHoudiniApi::SetAttributeUInt8DataAsync = (SetAttributeUInt8DataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeUInt8DataAsync")); + FHoudiniApi::SetAttributeUInt8UniqueData = (SetAttributeUInt8UniqueDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeUInt8UniqueData")); + FHoudiniApi::SetAttributeUInt8UniqueDataAsync = (SetAttributeUInt8UniqueDataAsyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetAttributeUInt8UniqueDataAsync")); + FHoudiniApi::SetCacheProperty = (SetCachePropertyFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetCacheProperty")); + FHoudiniApi::SetCompositorOptions = (SetCompositorOptionsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetCompositorOptions")); + FHoudiniApi::SetCurveCounts = (SetCurveCountsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetCurveCounts")); + FHoudiniApi::SetCurveInfo = (SetCurveInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetCurveInfo")); + FHoudiniApi::SetCurveKnots = (SetCurveKnotsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetCurveKnots")); + FHoudiniApi::SetCurveOrders = (SetCurveOrdersFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetCurveOrders")); + FHoudiniApi::SetCustomString = (SetCustomStringFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetCustomString")); + FHoudiniApi::SetFaceCounts = (SetFaceCountsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetFaceCounts")); + FHoudiniApi::SetGroupMembership = (SetGroupMembershipFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetGroupMembership")); + FHoudiniApi::SetHeightFieldData = (SetHeightFieldDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetHeightFieldData")); + FHoudiniApi::SetImageInfo = (SetImageInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetImageInfo")); + FHoudiniApi::SetInputCurveInfo = (SetInputCurveInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetInputCurveInfo")); + FHoudiniApi::SetInputCurvePositions = (SetInputCurvePositionsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetInputCurvePositions")); + FHoudiniApi::SetInputCurvePositionsRotationsScales = (SetInputCurvePositionsRotationsScalesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetInputCurvePositionsRotationsScales")); + FHoudiniApi::SetNodeDisplay = (SetNodeDisplayFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetNodeDisplay")); + FHoudiniApi::SetObjectTransform = (SetObjectTransformFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetObjectTransform")); + FHoudiniApi::SetParmExpression = (SetParmExpressionFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetParmExpression")); + FHoudiniApi::SetParmFloatValue = (SetParmFloatValueFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetParmFloatValue")); + FHoudiniApi::SetParmFloatValues = (SetParmFloatValuesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetParmFloatValues")); + FHoudiniApi::SetParmIntValue = (SetParmIntValueFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetParmIntValue")); + FHoudiniApi::SetParmIntValues = (SetParmIntValuesFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetParmIntValues")); + FHoudiniApi::SetParmNodeValue = (SetParmNodeValueFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetParmNodeValue")); + FHoudiniApi::SetParmStringValue = (SetParmStringValueFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetParmStringValue")); + FHoudiniApi::SetPartInfo = (SetPartInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetPartInfo")); + FHoudiniApi::SetPreset = (SetPresetFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetPreset")); + FHoudiniApi::SetServerEnvInt = (SetServerEnvIntFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetServerEnvInt")); + FHoudiniApi::SetServerEnvString = (SetServerEnvStringFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetServerEnvString")); + FHoudiniApi::SetSessionSync = (SetSessionSyncFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetSessionSync")); + FHoudiniApi::SetSessionSyncInfo = (SetSessionSyncInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetSessionSyncInfo")); + FHoudiniApi::SetTime = (SetTimeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetTime")); + FHoudiniApi::SetTimelineOptions = (SetTimelineOptionsFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetTimelineOptions")); + FHoudiniApi::SetTransformAnimCurve = (SetTransformAnimCurveFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetTransformAnimCurve")); + FHoudiniApi::SetUseHoudiniTime = (SetUseHoudiniTimeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetUseHoudiniTime")); + FHoudiniApi::SetVertexList = (SetVertexListFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetVertexList")); + FHoudiniApi::SetViewport = (SetViewportFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetViewport")); + FHoudiniApi::SetVolumeInfo = (SetVolumeInfoFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetVolumeInfo")); + FHoudiniApi::SetVolumeTileFloatData = (SetVolumeTileFloatDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetVolumeTileFloatData")); + FHoudiniApi::SetVolumeTileIntData = (SetVolumeTileIntDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetVolumeTileIntData")); + FHoudiniApi::SetVolumeVoxelFloatData = (SetVolumeVoxelFloatDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetVolumeVoxelFloatData")); + FHoudiniApi::SetVolumeVoxelIntData = (SetVolumeVoxelIntDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetVolumeVoxelIntData")); + FHoudiniApi::SetWorkItemFloatAttribute = (SetWorkItemFloatAttributeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetWorkItemFloatAttribute")); + FHoudiniApi::SetWorkItemIntAttribute = (SetWorkItemIntAttributeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetWorkItemIntAttribute")); + FHoudiniApi::SetWorkItemStringAttribute = (SetWorkItemStringAttributeFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetWorkItemStringAttribute")); + FHoudiniApi::SetWorkitemFloatData = (SetWorkitemFloatDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetWorkitemFloatData")); + FHoudiniApi::SetWorkitemIntData = (SetWorkitemIntDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetWorkitemIntData")); + FHoudiniApi::SetWorkitemStringData = (SetWorkitemStringDataFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_SetWorkitemStringData")); + FHoudiniApi::Shutdown = (ShutdownFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_Shutdown")); + FHoudiniApi::StartPerformanceMonitorProfile = (StartPerformanceMonitorProfileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_StartPerformanceMonitorProfile")); + FHoudiniApi::StartThriftNamedPipeServer = (StartThriftNamedPipeServerFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_StartThriftNamedPipeServer")); + FHoudiniApi::StartThriftSharedMemoryServer = (StartThriftSharedMemoryServerFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_StartThriftSharedMemoryServer")); + FHoudiniApi::StartThriftSocketServer = (StartThriftSocketServerFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_StartThriftSocketServer")); + FHoudiniApi::StopPerformanceMonitorProfile = (StopPerformanceMonitorProfileFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_StopPerformanceMonitorProfile")); + FHoudiniApi::ThriftServerOptions_Create = (ThriftServerOptions_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ThriftServerOptions_Create")); + FHoudiniApi::ThriftServerOptions_Init = (ThriftServerOptions_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_ThriftServerOptions_Init")); + FHoudiniApi::TimelineOptions_Create = (TimelineOptions_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_TimelineOptions_Create")); + FHoudiniApi::TimelineOptions_Init = (TimelineOptions_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_TimelineOptions_Init")); + FHoudiniApi::TransformEuler_Create = (TransformEuler_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_TransformEuler_Create")); + FHoudiniApi::TransformEuler_Init = (TransformEuler_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_TransformEuler_Init")); + FHoudiniApi::Transform_Create = (Transform_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_Transform_Create")); + FHoudiniApi::Transform_Init = (Transform_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_Transform_Init")); + FHoudiniApi::Viewport_Create = (Viewport_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_Viewport_Create")); + FHoudiniApi::VolumeInfo_Create = (VolumeInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_VolumeInfo_Create")); + FHoudiniApi::VolumeInfo_Init = (VolumeInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_VolumeInfo_Init")); + FHoudiniApi::VolumeTileInfo_Create = (VolumeTileInfo_CreateFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_VolumeTileInfo_Create")); + FHoudiniApi::VolumeTileInfo_Init = (VolumeTileInfo_InitFuncPtr) FPlatformProcess::GetDllExport(LibraryHandle, TEXT("HAPI_VolumeTileInfo_Init")); +} + + +void +FHoudiniApi::FinalizeHAPI() +{ + FHoudiniApi::AddAttribute = &FHoudiniApi::AddAttributeEmptyStub; + FHoudiniApi::AddGroup = &FHoudiniApi::AddGroupEmptyStub; + FHoudiniApi::AssetInfo_Create = &FHoudiniApi::AssetInfo_CreateEmptyStub; + FHoudiniApi::AssetInfo_Init = &FHoudiniApi::AssetInfo_InitEmptyStub; + FHoudiniApi::AttributeInfo_Create = &FHoudiniApi::AttributeInfo_CreateEmptyStub; + FHoudiniApi::AttributeInfo_Init = &FHoudiniApi::AttributeInfo_InitEmptyStub; + FHoudiniApi::BindCustomImplementation = &FHoudiniApi::BindCustomImplementationEmptyStub; + FHoudiniApi::CancelPDGCook = &FHoudiniApi::CancelPDGCookEmptyStub; + FHoudiniApi::CheckForSpecificErrors = &FHoudiniApi::CheckForSpecificErrorsEmptyStub; + FHoudiniApi::Cleanup = &FHoudiniApi::CleanupEmptyStub; + FHoudiniApi::ClearConnectionError = &FHoudiniApi::ClearConnectionErrorEmptyStub; + FHoudiniApi::CloseSession = &FHoudiniApi::CloseSessionEmptyStub; + FHoudiniApi::CommitGeo = &FHoudiniApi::CommitGeoEmptyStub; + FHoudiniApi::CommitWorkItems = &FHoudiniApi::CommitWorkItemsEmptyStub; + FHoudiniApi::CommitWorkitems = &FHoudiniApi::CommitWorkitemsEmptyStub; + FHoudiniApi::ComposeChildNodeList = &FHoudiniApi::ComposeChildNodeListEmptyStub; + FHoudiniApi::ComposeNodeCookResult = &FHoudiniApi::ComposeNodeCookResultEmptyStub; + FHoudiniApi::ComposeObjectList = &FHoudiniApi::ComposeObjectListEmptyStub; + FHoudiniApi::CompositorOptions_Create = &FHoudiniApi::CompositorOptions_CreateEmptyStub; + FHoudiniApi::CompositorOptions_Init = &FHoudiniApi::CompositorOptions_InitEmptyStub; + FHoudiniApi::ConnectNodeInput = &FHoudiniApi::ConnectNodeInputEmptyStub; + FHoudiniApi::ConvertMatrixToEuler = &FHoudiniApi::ConvertMatrixToEulerEmptyStub; + FHoudiniApi::ConvertMatrixToQuat = &FHoudiniApi::ConvertMatrixToQuatEmptyStub; + FHoudiniApi::ConvertTransform = &FHoudiniApi::ConvertTransformEmptyStub; + FHoudiniApi::ConvertTransformEulerToMatrix = &FHoudiniApi::ConvertTransformEulerToMatrixEmptyStub; + FHoudiniApi::ConvertTransformQuatToMatrix = &FHoudiniApi::ConvertTransformQuatToMatrixEmptyStub; + FHoudiniApi::CookNode = &FHoudiniApi::CookNodeEmptyStub; + FHoudiniApi::CookOptions_AreEqual = &FHoudiniApi::CookOptions_AreEqualEmptyStub; + FHoudiniApi::CookOptions_Create = &FHoudiniApi::CookOptions_CreateEmptyStub; + FHoudiniApi::CookOptions_Init = &FHoudiniApi::CookOptions_InitEmptyStub; + FHoudiniApi::CookPDG = &FHoudiniApi::CookPDGEmptyStub; + FHoudiniApi::CookPDGAllOutputs = &FHoudiniApi::CookPDGAllOutputsEmptyStub; + FHoudiniApi::CreateCOPImage = &FHoudiniApi::CreateCOPImageEmptyStub; + FHoudiniApi::CreateCustomSession = &FHoudiniApi::CreateCustomSessionEmptyStub; + FHoudiniApi::CreateHeightFieldInput = &FHoudiniApi::CreateHeightFieldInputEmptyStub; + FHoudiniApi::CreateHeightfieldInputVolumeNode = &FHoudiniApi::CreateHeightfieldInputVolumeNodeEmptyStub; + FHoudiniApi::CreateInProcessSession = &FHoudiniApi::CreateInProcessSessionEmptyStub; + FHoudiniApi::CreateInputCurveNode = &FHoudiniApi::CreateInputCurveNodeEmptyStub; + FHoudiniApi::CreateInputNode = &FHoudiniApi::CreateInputNodeEmptyStub; + FHoudiniApi::CreateNode = &FHoudiniApi::CreateNodeEmptyStub; + FHoudiniApi::CreateThriftNamedPipeSession = &FHoudiniApi::CreateThriftNamedPipeSessionEmptyStub; + FHoudiniApi::CreateThriftSharedMemorySession = &FHoudiniApi::CreateThriftSharedMemorySessionEmptyStub; + FHoudiniApi::CreateThriftSocketSession = &FHoudiniApi::CreateThriftSocketSessionEmptyStub; + FHoudiniApi::CreateWorkItem = &FHoudiniApi::CreateWorkItemEmptyStub; + FHoudiniApi::CreateWorkitem = &FHoudiniApi::CreateWorkitemEmptyStub; + FHoudiniApi::CurveInfo_Create = &FHoudiniApi::CurveInfo_CreateEmptyStub; + FHoudiniApi::CurveInfo_Init = &FHoudiniApi::CurveInfo_InitEmptyStub; + FHoudiniApi::DeleteAttribute = &FHoudiniApi::DeleteAttributeEmptyStub; + FHoudiniApi::DeleteGroup = &FHoudiniApi::DeleteGroupEmptyStub; + FHoudiniApi::DeleteNode = &FHoudiniApi::DeleteNodeEmptyStub; + FHoudiniApi::DirtyPDGNode = &FHoudiniApi::DirtyPDGNodeEmptyStub; + FHoudiniApi::DisconnectNodeInput = &FHoudiniApi::DisconnectNodeInputEmptyStub; + FHoudiniApi::DisconnectNodeOutputsAt = &FHoudiniApi::DisconnectNodeOutputsAtEmptyStub; + FHoudiniApi::ExtractImageToFile = &FHoudiniApi::ExtractImageToFileEmptyStub; + FHoudiniApi::ExtractImageToMemory = &FHoudiniApi::ExtractImageToMemoryEmptyStub; + FHoudiniApi::GeoInfo_Create = &FHoudiniApi::GeoInfo_CreateEmptyStub; + FHoudiniApi::GeoInfo_GetGroupCountByType = &FHoudiniApi::GeoInfo_GetGroupCountByTypeEmptyStub; + FHoudiniApi::GeoInfo_Init = &FHoudiniApi::GeoInfo_InitEmptyStub; + FHoudiniApi::GetActiveCacheCount = &FHoudiniApi::GetActiveCacheCountEmptyStub; + FHoudiniApi::GetActiveCacheNames = &FHoudiniApi::GetActiveCacheNamesEmptyStub; + FHoudiniApi::GetAssetDefinitionParmCounts = &FHoudiniApi::GetAssetDefinitionParmCountsEmptyStub; + FHoudiniApi::GetAssetDefinitionParmInfos = &FHoudiniApi::GetAssetDefinitionParmInfosEmptyStub; + FHoudiniApi::GetAssetDefinitionParmTagName = &FHoudiniApi::GetAssetDefinitionParmTagNameEmptyStub; + FHoudiniApi::GetAssetDefinitionParmTagValue = &FHoudiniApi::GetAssetDefinitionParmTagValueEmptyStub; + FHoudiniApi::GetAssetDefinitionParmValues = &FHoudiniApi::GetAssetDefinitionParmValuesEmptyStub; + FHoudiniApi::GetAssetInfo = &FHoudiniApi::GetAssetInfoEmptyStub; + FHoudiniApi::GetAssetLibraryFilePath = &FHoudiniApi::GetAssetLibraryFilePathEmptyStub; + FHoudiniApi::GetAssetLibraryIds = &FHoudiniApi::GetAssetLibraryIdsEmptyStub; + FHoudiniApi::GetAttributeDictionaryArrayData = &FHoudiniApi::GetAttributeDictionaryArrayDataEmptyStub; + FHoudiniApi::GetAttributeDictionaryArrayDataAsync = &FHoudiniApi::GetAttributeDictionaryArrayDataAsyncEmptyStub; + FHoudiniApi::GetAttributeDictionaryData = &FHoudiniApi::GetAttributeDictionaryDataEmptyStub; + FHoudiniApi::GetAttributeDictionaryDataAsync = &FHoudiniApi::GetAttributeDictionaryDataAsyncEmptyStub; + FHoudiniApi::GetAttributeFloat64ArrayData = &FHoudiniApi::GetAttributeFloat64ArrayDataEmptyStub; + FHoudiniApi::GetAttributeFloat64ArrayDataAsync = &FHoudiniApi::GetAttributeFloat64ArrayDataAsyncEmptyStub; + FHoudiniApi::GetAttributeFloat64Data = &FHoudiniApi::GetAttributeFloat64DataEmptyStub; + FHoudiniApi::GetAttributeFloat64DataAsync = &FHoudiniApi::GetAttributeFloat64DataAsyncEmptyStub; + FHoudiniApi::GetAttributeFloatArrayData = &FHoudiniApi::GetAttributeFloatArrayDataEmptyStub; + FHoudiniApi::GetAttributeFloatArrayDataAsync = &FHoudiniApi::GetAttributeFloatArrayDataAsyncEmptyStub; + FHoudiniApi::GetAttributeFloatData = &FHoudiniApi::GetAttributeFloatDataEmptyStub; + FHoudiniApi::GetAttributeFloatDataAsync = &FHoudiniApi::GetAttributeFloatDataAsyncEmptyStub; + FHoudiniApi::GetAttributeInfo = &FHoudiniApi::GetAttributeInfoEmptyStub; + FHoudiniApi::GetAttributeInt16ArrayData = &FHoudiniApi::GetAttributeInt16ArrayDataEmptyStub; + FHoudiniApi::GetAttributeInt16ArrayDataAsync = &FHoudiniApi::GetAttributeInt16ArrayDataAsyncEmptyStub; + FHoudiniApi::GetAttributeInt16Data = &FHoudiniApi::GetAttributeInt16DataEmptyStub; + FHoudiniApi::GetAttributeInt16DataAsync = &FHoudiniApi::GetAttributeInt16DataAsyncEmptyStub; + FHoudiniApi::GetAttributeInt64ArrayData = &FHoudiniApi::GetAttributeInt64ArrayDataEmptyStub; + FHoudiniApi::GetAttributeInt64ArrayDataAsync = &FHoudiniApi::GetAttributeInt64ArrayDataAsyncEmptyStub; + FHoudiniApi::GetAttributeInt64Data = &FHoudiniApi::GetAttributeInt64DataEmptyStub; + FHoudiniApi::GetAttributeInt64DataAsync = &FHoudiniApi::GetAttributeInt64DataAsyncEmptyStub; + FHoudiniApi::GetAttributeInt8ArrayData = &FHoudiniApi::GetAttributeInt8ArrayDataEmptyStub; + FHoudiniApi::GetAttributeInt8ArrayDataAsync = &FHoudiniApi::GetAttributeInt8ArrayDataAsyncEmptyStub; + FHoudiniApi::GetAttributeInt8Data = &FHoudiniApi::GetAttributeInt8DataEmptyStub; + FHoudiniApi::GetAttributeInt8DataAsync = &FHoudiniApi::GetAttributeInt8DataAsyncEmptyStub; + FHoudiniApi::GetAttributeIntArrayData = &FHoudiniApi::GetAttributeIntArrayDataEmptyStub; + FHoudiniApi::GetAttributeIntArrayDataAsync = &FHoudiniApi::GetAttributeIntArrayDataAsyncEmptyStub; + FHoudiniApi::GetAttributeIntData = &FHoudiniApi::GetAttributeIntDataEmptyStub; + FHoudiniApi::GetAttributeIntDataAsync = &FHoudiniApi::GetAttributeIntDataAsyncEmptyStub; + FHoudiniApi::GetAttributeNames = &FHoudiniApi::GetAttributeNamesEmptyStub; + FHoudiniApi::GetAttributeStringArrayData = &FHoudiniApi::GetAttributeStringArrayDataEmptyStub; + FHoudiniApi::GetAttributeStringArrayDataAsync = &FHoudiniApi::GetAttributeStringArrayDataAsyncEmptyStub; + FHoudiniApi::GetAttributeStringData = &FHoudiniApi::GetAttributeStringDataEmptyStub; + FHoudiniApi::GetAttributeStringDataAsync = &FHoudiniApi::GetAttributeStringDataAsyncEmptyStub; + FHoudiniApi::GetAttributeUInt8ArrayData = &FHoudiniApi::GetAttributeUInt8ArrayDataEmptyStub; + FHoudiniApi::GetAttributeUInt8ArrayDataAsync = &FHoudiniApi::GetAttributeUInt8ArrayDataAsyncEmptyStub; + FHoudiniApi::GetAttributeUInt8Data = &FHoudiniApi::GetAttributeUInt8DataEmptyStub; + FHoudiniApi::GetAttributeUInt8DataAsync = &FHoudiniApi::GetAttributeUInt8DataAsyncEmptyStub; + FHoudiniApi::GetAvailableAssetCount = &FHoudiniApi::GetAvailableAssetCountEmptyStub; + FHoudiniApi::GetAvailableAssets = &FHoudiniApi::GetAvailableAssetsEmptyStub; + FHoudiniApi::GetBoxInfo = &FHoudiniApi::GetBoxInfoEmptyStub; + FHoudiniApi::GetCacheProperty = &FHoudiniApi::GetCachePropertyEmptyStub; + FHoudiniApi::GetComposedChildNodeList = &FHoudiniApi::GetComposedChildNodeListEmptyStub; + FHoudiniApi::GetComposedNodeCookResult = &FHoudiniApi::GetComposedNodeCookResultEmptyStub; + FHoudiniApi::GetComposedObjectList = &FHoudiniApi::GetComposedObjectListEmptyStub; + FHoudiniApi::GetComposedObjectTransforms = &FHoudiniApi::GetComposedObjectTransformsEmptyStub; + FHoudiniApi::GetCompositorOptions = &FHoudiniApi::GetCompositorOptionsEmptyStub; + FHoudiniApi::GetConnectionError = &FHoudiniApi::GetConnectionErrorEmptyStub; + FHoudiniApi::GetConnectionErrorLength = &FHoudiniApi::GetConnectionErrorLengthEmptyStub; + FHoudiniApi::GetCookingCurrentCount = &FHoudiniApi::GetCookingCurrentCountEmptyStub; + FHoudiniApi::GetCookingTotalCount = &FHoudiniApi::GetCookingTotalCountEmptyStub; + FHoudiniApi::GetCurveCounts = &FHoudiniApi::GetCurveCountsEmptyStub; + FHoudiniApi::GetCurveInfo = &FHoudiniApi::GetCurveInfoEmptyStub; + FHoudiniApi::GetCurveKnots = &FHoudiniApi::GetCurveKnotsEmptyStub; + FHoudiniApi::GetCurveOrders = &FHoudiniApi::GetCurveOrdersEmptyStub; + FHoudiniApi::GetDisplayGeoInfo = &FHoudiniApi::GetDisplayGeoInfoEmptyStub; + FHoudiniApi::GetEdgeCountOfEdgeGroup = &FHoudiniApi::GetEdgeCountOfEdgeGroupEmptyStub; + FHoudiniApi::GetEnvInt = &FHoudiniApi::GetEnvIntEmptyStub; + FHoudiniApi::GetFaceCounts = &FHoudiniApi::GetFaceCountsEmptyStub; + FHoudiniApi::GetFirstVolumeTile = &FHoudiniApi::GetFirstVolumeTileEmptyStub; + FHoudiniApi::GetGeoInfo = &FHoudiniApi::GetGeoInfoEmptyStub; + FHoudiniApi::GetGeoSize = &FHoudiniApi::GetGeoSizeEmptyStub; + FHoudiniApi::GetGroupCountOnPackedInstancePart = &FHoudiniApi::GetGroupCountOnPackedInstancePartEmptyStub; + FHoudiniApi::GetGroupMembership = &FHoudiniApi::GetGroupMembershipEmptyStub; + FHoudiniApi::GetGroupMembershipOnPackedInstancePart = &FHoudiniApi::GetGroupMembershipOnPackedInstancePartEmptyStub; + FHoudiniApi::GetGroupNames = &FHoudiniApi::GetGroupNamesEmptyStub; + FHoudiniApi::GetGroupNamesOnPackedInstancePart = &FHoudiniApi::GetGroupNamesOnPackedInstancePartEmptyStub; + FHoudiniApi::GetHIPFileNodeCount = &FHoudiniApi::GetHIPFileNodeCountEmptyStub; + FHoudiniApi::GetHIPFileNodeIds = &FHoudiniApi::GetHIPFileNodeIdsEmptyStub; + FHoudiniApi::GetHandleBindingInfo = &FHoudiniApi::GetHandleBindingInfoEmptyStub; + FHoudiniApi::GetHandleInfo = &FHoudiniApi::GetHandleInfoEmptyStub; + FHoudiniApi::GetHeightFieldData = &FHoudiniApi::GetHeightFieldDataEmptyStub; + FHoudiniApi::GetImageFilePath = &FHoudiniApi::GetImageFilePathEmptyStub; + FHoudiniApi::GetImageInfo = &FHoudiniApi::GetImageInfoEmptyStub; + FHoudiniApi::GetImageMemoryBuffer = &FHoudiniApi::GetImageMemoryBufferEmptyStub; + FHoudiniApi::GetImagePlaneCount = &FHoudiniApi::GetImagePlaneCountEmptyStub; + FHoudiniApi::GetImagePlanes = &FHoudiniApi::GetImagePlanesEmptyStub; + FHoudiniApi::GetInputCurveInfo = &FHoudiniApi::GetInputCurveInfoEmptyStub; + FHoudiniApi::GetInstanceTransformsOnPart = &FHoudiniApi::GetInstanceTransformsOnPartEmptyStub; + FHoudiniApi::GetInstancedObjectIds = &FHoudiniApi::GetInstancedObjectIdsEmptyStub; + FHoudiniApi::GetInstancedPartIds = &FHoudiniApi::GetInstancedPartIdsEmptyStub; + FHoudiniApi::GetInstancerPartTransforms = &FHoudiniApi::GetInstancerPartTransformsEmptyStub; + FHoudiniApi::GetJobStatus = &FHoudiniApi::GetJobStatusEmptyStub; + FHoudiniApi::GetLoadedAssetLibraryCount = &FHoudiniApi::GetLoadedAssetLibraryCountEmptyStub; + FHoudiniApi::GetManagerNodeId = &FHoudiniApi::GetManagerNodeIdEmptyStub; + FHoudiniApi::GetMaterialInfo = &FHoudiniApi::GetMaterialInfoEmptyStub; + FHoudiniApi::GetMaterialNodeIdsOnFaces = &FHoudiniApi::GetMaterialNodeIdsOnFacesEmptyStub; + FHoudiniApi::GetMessageNodeCount = &FHoudiniApi::GetMessageNodeCountEmptyStub; + FHoudiniApi::GetMessageNodeIds = &FHoudiniApi::GetMessageNodeIdsEmptyStub; + FHoudiniApi::GetNextVolumeTile = &FHoudiniApi::GetNextVolumeTileEmptyStub; + FHoudiniApi::GetNodeCookResult = &FHoudiniApi::GetNodeCookResultEmptyStub; + FHoudiniApi::GetNodeCookResultLength = &FHoudiniApi::GetNodeCookResultLengthEmptyStub; + FHoudiniApi::GetNodeFromPath = &FHoudiniApi::GetNodeFromPathEmptyStub; + FHoudiniApi::GetNodeInfo = &FHoudiniApi::GetNodeInfoEmptyStub; + FHoudiniApi::GetNodeInputName = &FHoudiniApi::GetNodeInputNameEmptyStub; + FHoudiniApi::GetNodeOutputName = &FHoudiniApi::GetNodeOutputNameEmptyStub; + FHoudiniApi::GetNodePath = &FHoudiniApi::GetNodePathEmptyStub; + FHoudiniApi::GetNumWorkItems = &FHoudiniApi::GetNumWorkItemsEmptyStub; + FHoudiniApi::GetNumWorkitems = &FHoudiniApi::GetNumWorkitemsEmptyStub; + FHoudiniApi::GetObjectInfo = &FHoudiniApi::GetObjectInfoEmptyStub; + FHoudiniApi::GetObjectTransform = &FHoudiniApi::GetObjectTransformEmptyStub; + FHoudiniApi::GetOutputGeoCount = &FHoudiniApi::GetOutputGeoCountEmptyStub; + FHoudiniApi::GetOutputGeoInfos = &FHoudiniApi::GetOutputGeoInfosEmptyStub; + FHoudiniApi::GetOutputNodeId = &FHoudiniApi::GetOutputNodeIdEmptyStub; + FHoudiniApi::GetPDGEvents = &FHoudiniApi::GetPDGEventsEmptyStub; + FHoudiniApi::GetPDGGraphContextId = &FHoudiniApi::GetPDGGraphContextIdEmptyStub; + FHoudiniApi::GetPDGGraphContexts = &FHoudiniApi::GetPDGGraphContextsEmptyStub; + FHoudiniApi::GetPDGGraphContextsCount = &FHoudiniApi::GetPDGGraphContextsCountEmptyStub; + FHoudiniApi::GetPDGState = &FHoudiniApi::GetPDGStateEmptyStub; + FHoudiniApi::GetParameters = &FHoudiniApi::GetParametersEmptyStub; + FHoudiniApi::GetParmChoiceLists = &FHoudiniApi::GetParmChoiceListsEmptyStub; + FHoudiniApi::GetParmExpression = &FHoudiniApi::GetParmExpressionEmptyStub; + FHoudiniApi::GetParmFile = &FHoudiniApi::GetParmFileEmptyStub; + FHoudiniApi::GetParmFloatValue = &FHoudiniApi::GetParmFloatValueEmptyStub; + FHoudiniApi::GetParmFloatValues = &FHoudiniApi::GetParmFloatValuesEmptyStub; + FHoudiniApi::GetParmIdFromName = &FHoudiniApi::GetParmIdFromNameEmptyStub; + FHoudiniApi::GetParmInfo = &FHoudiniApi::GetParmInfoEmptyStub; + FHoudiniApi::GetParmInfoFromName = &FHoudiniApi::GetParmInfoFromNameEmptyStub; + FHoudiniApi::GetParmIntValue = &FHoudiniApi::GetParmIntValueEmptyStub; + FHoudiniApi::GetParmIntValues = &FHoudiniApi::GetParmIntValuesEmptyStub; + FHoudiniApi::GetParmNodeValue = &FHoudiniApi::GetParmNodeValueEmptyStub; + FHoudiniApi::GetParmStringValue = &FHoudiniApi::GetParmStringValueEmptyStub; + FHoudiniApi::GetParmStringValues = &FHoudiniApi::GetParmStringValuesEmptyStub; + FHoudiniApi::GetParmTagName = &FHoudiniApi::GetParmTagNameEmptyStub; + FHoudiniApi::GetParmTagValue = &FHoudiniApi::GetParmTagValueEmptyStub; + FHoudiniApi::GetParmWithTag = &FHoudiniApi::GetParmWithTagEmptyStub; + FHoudiniApi::GetPartInfo = &FHoudiniApi::GetPartInfoEmptyStub; + FHoudiniApi::GetPreset = &FHoudiniApi::GetPresetEmptyStub; + FHoudiniApi::GetPresetBufLength = &FHoudiniApi::GetPresetBufLengthEmptyStub; + FHoudiniApi::GetPresetCount = &FHoudiniApi::GetPresetCountEmptyStub; + FHoudiniApi::GetPresetNames = &FHoudiniApi::GetPresetNamesEmptyStub; + FHoudiniApi::GetServerEnvInt = &FHoudiniApi::GetServerEnvIntEmptyStub; + FHoudiniApi::GetServerEnvString = &FHoudiniApi::GetServerEnvStringEmptyStub; + FHoudiniApi::GetServerEnvVarCount = &FHoudiniApi::GetServerEnvVarCountEmptyStub; + FHoudiniApi::GetServerEnvVarList = &FHoudiniApi::GetServerEnvVarListEmptyStub; + FHoudiniApi::GetSessionEnvInt = &FHoudiniApi::GetSessionEnvIntEmptyStub; + FHoudiniApi::GetSessionSyncInfo = &FHoudiniApi::GetSessionSyncInfoEmptyStub; + FHoudiniApi::GetSphereInfo = &FHoudiniApi::GetSphereInfoEmptyStub; + FHoudiniApi::GetStatus = &FHoudiniApi::GetStatusEmptyStub; + FHoudiniApi::GetStatusString = &FHoudiniApi::GetStatusStringEmptyStub; + FHoudiniApi::GetStatusStringBufLength = &FHoudiniApi::GetStatusStringBufLengthEmptyStub; + FHoudiniApi::GetString = &FHoudiniApi::GetStringEmptyStub; + FHoudiniApi::GetStringBatch = &FHoudiniApi::GetStringBatchEmptyStub; + FHoudiniApi::GetStringBatchSize = &FHoudiniApi::GetStringBatchSizeEmptyStub; + FHoudiniApi::GetStringBufLength = &FHoudiniApi::GetStringBufLengthEmptyStub; + FHoudiniApi::GetSupportedImageFileFormatCount = &FHoudiniApi::GetSupportedImageFileFormatCountEmptyStub; + FHoudiniApi::GetSupportedImageFileFormats = &FHoudiniApi::GetSupportedImageFileFormatsEmptyStub; + FHoudiniApi::GetTime = &FHoudiniApi::GetTimeEmptyStub; + FHoudiniApi::GetTimelineOptions = &FHoudiniApi::GetTimelineOptionsEmptyStub; + FHoudiniApi::GetTotalCookCount = &FHoudiniApi::GetTotalCookCountEmptyStub; + FHoudiniApi::GetUseHoudiniTime = &FHoudiniApi::GetUseHoudiniTimeEmptyStub; + FHoudiniApi::GetVertexList = &FHoudiniApi::GetVertexListEmptyStub; + FHoudiniApi::GetViewport = &FHoudiniApi::GetViewportEmptyStub; + FHoudiniApi::GetVolumeBounds = &FHoudiniApi::GetVolumeBoundsEmptyStub; + FHoudiniApi::GetVolumeInfo = &FHoudiniApi::GetVolumeInfoEmptyStub; + FHoudiniApi::GetVolumeTileFloatData = &FHoudiniApi::GetVolumeTileFloatDataEmptyStub; + FHoudiniApi::GetVolumeTileIntData = &FHoudiniApi::GetVolumeTileIntDataEmptyStub; + FHoudiniApi::GetVolumeVisualInfo = &FHoudiniApi::GetVolumeVisualInfoEmptyStub; + FHoudiniApi::GetVolumeVoxelFloatData = &FHoudiniApi::GetVolumeVoxelFloatDataEmptyStub; + FHoudiniApi::GetVolumeVoxelIntData = &FHoudiniApi::GetVolumeVoxelIntDataEmptyStub; + FHoudiniApi::GetWorkItemAttributeSize = &FHoudiniApi::GetWorkItemAttributeSizeEmptyStub; + FHoudiniApi::GetWorkItemFloatAttribute = &FHoudiniApi::GetWorkItemFloatAttributeEmptyStub; + FHoudiniApi::GetWorkItemInfo = &FHoudiniApi::GetWorkItemInfoEmptyStub; + FHoudiniApi::GetWorkItemIntAttribute = &FHoudiniApi::GetWorkItemIntAttributeEmptyStub; + FHoudiniApi::GetWorkItemOutputFiles = &FHoudiniApi::GetWorkItemOutputFilesEmptyStub; + FHoudiniApi::GetWorkItemStringAttribute = &FHoudiniApi::GetWorkItemStringAttributeEmptyStub; + FHoudiniApi::GetWorkItems = &FHoudiniApi::GetWorkItemsEmptyStub; + FHoudiniApi::GetWorkitemDataLength = &FHoudiniApi::GetWorkitemDataLengthEmptyStub; + FHoudiniApi::GetWorkitemFloatData = &FHoudiniApi::GetWorkitemFloatDataEmptyStub; + FHoudiniApi::GetWorkitemInfo = &FHoudiniApi::GetWorkitemInfoEmptyStub; + FHoudiniApi::GetWorkitemIntData = &FHoudiniApi::GetWorkitemIntDataEmptyStub; + FHoudiniApi::GetWorkitemResultInfo = &FHoudiniApi::GetWorkitemResultInfoEmptyStub; + FHoudiniApi::GetWorkitemStringData = &FHoudiniApi::GetWorkitemStringDataEmptyStub; + FHoudiniApi::GetWorkitems = &FHoudiniApi::GetWorkitemsEmptyStub; + FHoudiniApi::HandleBindingInfo_Create = &FHoudiniApi::HandleBindingInfo_CreateEmptyStub; + FHoudiniApi::HandleBindingInfo_Init = &FHoudiniApi::HandleBindingInfo_InitEmptyStub; + FHoudiniApi::HandleInfo_Create = &FHoudiniApi::HandleInfo_CreateEmptyStub; + FHoudiniApi::HandleInfo_Init = &FHoudiniApi::HandleInfo_InitEmptyStub; + FHoudiniApi::ImageFileFormat_Create = &FHoudiniApi::ImageFileFormat_CreateEmptyStub; + FHoudiniApi::ImageFileFormat_Init = &FHoudiniApi::ImageFileFormat_InitEmptyStub; + FHoudiniApi::ImageInfo_Create = &FHoudiniApi::ImageInfo_CreateEmptyStub; + FHoudiniApi::ImageInfo_Init = &FHoudiniApi::ImageInfo_InitEmptyStub; + FHoudiniApi::Initialize = &FHoudiniApi::InitializeEmptyStub; + FHoudiniApi::InputCurveInfo_Create = &FHoudiniApi::InputCurveInfo_CreateEmptyStub; + FHoudiniApi::InputCurveInfo_Init = &FHoudiniApi::InputCurveInfo_InitEmptyStub; + FHoudiniApi::InsertMultiparmInstance = &FHoudiniApi::InsertMultiparmInstanceEmptyStub; + FHoudiniApi::Interrupt = &FHoudiniApi::InterruptEmptyStub; + FHoudiniApi::IsInitialized = &FHoudiniApi::IsInitializedEmptyStub; + FHoudiniApi::IsNodeValid = &FHoudiniApi::IsNodeValidEmptyStub; + FHoudiniApi::IsSessionValid = &FHoudiniApi::IsSessionValidEmptyStub; + FHoudiniApi::Keyframe_Create = &FHoudiniApi::Keyframe_CreateEmptyStub; + FHoudiniApi::Keyframe_Init = &FHoudiniApi::Keyframe_InitEmptyStub; + FHoudiniApi::LoadAssetLibraryFromFile = &FHoudiniApi::LoadAssetLibraryFromFileEmptyStub; + FHoudiniApi::LoadAssetLibraryFromMemory = &FHoudiniApi::LoadAssetLibraryFromMemoryEmptyStub; + FHoudiniApi::LoadGeoFromFile = &FHoudiniApi::LoadGeoFromFileEmptyStub; + FHoudiniApi::LoadGeoFromMemory = &FHoudiniApi::LoadGeoFromMemoryEmptyStub; + FHoudiniApi::LoadHIPFile = &FHoudiniApi::LoadHIPFileEmptyStub; + FHoudiniApi::LoadNodeFromFile = &FHoudiniApi::LoadNodeFromFileEmptyStub; + FHoudiniApi::MaterialInfo_Create = &FHoudiniApi::MaterialInfo_CreateEmptyStub; + FHoudiniApi::MaterialInfo_Init = &FHoudiniApi::MaterialInfo_InitEmptyStub; + FHoudiniApi::MergeHIPFile = &FHoudiniApi::MergeHIPFileEmptyStub; + FHoudiniApi::NodeInfo_Create = &FHoudiniApi::NodeInfo_CreateEmptyStub; + FHoudiniApi::NodeInfo_Init = &FHoudiniApi::NodeInfo_InitEmptyStub; + FHoudiniApi::ObjectInfo_Create = &FHoudiniApi::ObjectInfo_CreateEmptyStub; + FHoudiniApi::ObjectInfo_Init = &FHoudiniApi::ObjectInfo_InitEmptyStub; + FHoudiniApi::ParmChoiceInfo_Create = &FHoudiniApi::ParmChoiceInfo_CreateEmptyStub; + FHoudiniApi::ParmChoiceInfo_Init = &FHoudiniApi::ParmChoiceInfo_InitEmptyStub; + FHoudiniApi::ParmHasExpression = &FHoudiniApi::ParmHasExpressionEmptyStub; + FHoudiniApi::ParmHasTag = &FHoudiniApi::ParmHasTagEmptyStub; + FHoudiniApi::ParmInfo_Create = &FHoudiniApi::ParmInfo_CreateEmptyStub; + FHoudiniApi::ParmInfo_GetFloatValueCount = &FHoudiniApi::ParmInfo_GetFloatValueCountEmptyStub; + FHoudiniApi::ParmInfo_GetIntValueCount = &FHoudiniApi::ParmInfo_GetIntValueCountEmptyStub; + FHoudiniApi::ParmInfo_GetStringValueCount = &FHoudiniApi::ParmInfo_GetStringValueCountEmptyStub; + FHoudiniApi::ParmInfo_Init = &FHoudiniApi::ParmInfo_InitEmptyStub; + FHoudiniApi::ParmInfo_IsFloat = &FHoudiniApi::ParmInfo_IsFloatEmptyStub; + FHoudiniApi::ParmInfo_IsInt = &FHoudiniApi::ParmInfo_IsIntEmptyStub; + FHoudiniApi::ParmInfo_IsNode = &FHoudiniApi::ParmInfo_IsNodeEmptyStub; + FHoudiniApi::ParmInfo_IsNonValue = &FHoudiniApi::ParmInfo_IsNonValueEmptyStub; + FHoudiniApi::ParmInfo_IsPath = &FHoudiniApi::ParmInfo_IsPathEmptyStub; + FHoudiniApi::ParmInfo_IsString = &FHoudiniApi::ParmInfo_IsStringEmptyStub; + FHoudiniApi::PartInfo_Create = &FHoudiniApi::PartInfo_CreateEmptyStub; + FHoudiniApi::PartInfo_GetAttributeCountByOwner = &FHoudiniApi::PartInfo_GetAttributeCountByOwnerEmptyStub; + FHoudiniApi::PartInfo_GetElementCountByAttributeOwner = &FHoudiniApi::PartInfo_GetElementCountByAttributeOwnerEmptyStub; + FHoudiniApi::PartInfo_GetElementCountByGroupType = &FHoudiniApi::PartInfo_GetElementCountByGroupTypeEmptyStub; + FHoudiniApi::PartInfo_Init = &FHoudiniApi::PartInfo_InitEmptyStub; + FHoudiniApi::PausePDGCook = &FHoudiniApi::PausePDGCookEmptyStub; + FHoudiniApi::PythonThreadInterpreterLock = &FHoudiniApi::PythonThreadInterpreterLockEmptyStub; + FHoudiniApi::QueryNodeInput = &FHoudiniApi::QueryNodeInputEmptyStub; + FHoudiniApi::QueryNodeOutputConnectedCount = &FHoudiniApi::QueryNodeOutputConnectedCountEmptyStub; + FHoudiniApi::QueryNodeOutputConnectedNodes = &FHoudiniApi::QueryNodeOutputConnectedNodesEmptyStub; + FHoudiniApi::RemoveCustomString = &FHoudiniApi::RemoveCustomStringEmptyStub; + FHoudiniApi::RemoveMultiparmInstance = &FHoudiniApi::RemoveMultiparmInstanceEmptyStub; + FHoudiniApi::RemoveParmExpression = &FHoudiniApi::RemoveParmExpressionEmptyStub; + FHoudiniApi::RenameNode = &FHoudiniApi::RenameNodeEmptyStub; + FHoudiniApi::RenderCOPOutputToImage = &FHoudiniApi::RenderCOPOutputToImageEmptyStub; + FHoudiniApi::RenderCOPToImage = &FHoudiniApi::RenderCOPToImageEmptyStub; + FHoudiniApi::RenderTextureToImage = &FHoudiniApi::RenderTextureToImageEmptyStub; + FHoudiniApi::ResetSimulation = &FHoudiniApi::ResetSimulationEmptyStub; + FHoudiniApi::RevertGeo = &FHoudiniApi::RevertGeoEmptyStub; + FHoudiniApi::RevertParmToDefault = &FHoudiniApi::RevertParmToDefaultEmptyStub; + FHoudiniApi::RevertParmToDefaults = &FHoudiniApi::RevertParmToDefaultsEmptyStub; + FHoudiniApi::SaveGeoToFile = &FHoudiniApi::SaveGeoToFileEmptyStub; + FHoudiniApi::SaveGeoToMemory = &FHoudiniApi::SaveGeoToMemoryEmptyStub; + FHoudiniApi::SaveHIPFile = &FHoudiniApi::SaveHIPFileEmptyStub; + FHoudiniApi::SaveNodeToFile = &FHoudiniApi::SaveNodeToFileEmptyStub; + FHoudiniApi::SessionInfo_Create = &FHoudiniApi::SessionInfo_CreateEmptyStub; + FHoudiniApi::SessionInfo_Init = &FHoudiniApi::SessionInfo_InitEmptyStub; + FHoudiniApi::SessionSyncInfo_Create = &FHoudiniApi::SessionSyncInfo_CreateEmptyStub; + FHoudiniApi::SetAnimCurve = &FHoudiniApi::SetAnimCurveEmptyStub; + FHoudiniApi::SetAttributeDictionaryArrayData = &FHoudiniApi::SetAttributeDictionaryArrayDataEmptyStub; + FHoudiniApi::SetAttributeDictionaryArrayDataAsync = &FHoudiniApi::SetAttributeDictionaryArrayDataAsyncEmptyStub; + FHoudiniApi::SetAttributeDictionaryData = &FHoudiniApi::SetAttributeDictionaryDataEmptyStub; + FHoudiniApi::SetAttributeDictionaryDataAsync = &FHoudiniApi::SetAttributeDictionaryDataAsyncEmptyStub; + FHoudiniApi::SetAttributeFloat64ArrayData = &FHoudiniApi::SetAttributeFloat64ArrayDataEmptyStub; + FHoudiniApi::SetAttributeFloat64ArrayDataAsync = &FHoudiniApi::SetAttributeFloat64ArrayDataAsyncEmptyStub; + FHoudiniApi::SetAttributeFloat64Data = &FHoudiniApi::SetAttributeFloat64DataEmptyStub; + FHoudiniApi::SetAttributeFloat64DataAsync = &FHoudiniApi::SetAttributeFloat64DataAsyncEmptyStub; + FHoudiniApi::SetAttributeFloat64UniqueData = &FHoudiniApi::SetAttributeFloat64UniqueDataEmptyStub; + FHoudiniApi::SetAttributeFloat64UniqueDataAsync = &FHoudiniApi::SetAttributeFloat64UniqueDataAsyncEmptyStub; + FHoudiniApi::SetAttributeFloatArrayData = &FHoudiniApi::SetAttributeFloatArrayDataEmptyStub; + FHoudiniApi::SetAttributeFloatArrayDataAsync = &FHoudiniApi::SetAttributeFloatArrayDataAsyncEmptyStub; + FHoudiniApi::SetAttributeFloatData = &FHoudiniApi::SetAttributeFloatDataEmptyStub; + FHoudiniApi::SetAttributeFloatDataAsync = &FHoudiniApi::SetAttributeFloatDataAsyncEmptyStub; + FHoudiniApi::SetAttributeFloatUniqueData = &FHoudiniApi::SetAttributeFloatUniqueDataEmptyStub; + FHoudiniApi::SetAttributeFloatUniqueDataAsync = &FHoudiniApi::SetAttributeFloatUniqueDataAsyncEmptyStub; + FHoudiniApi::SetAttributeIndexedStringData = &FHoudiniApi::SetAttributeIndexedStringDataEmptyStub; + FHoudiniApi::SetAttributeIndexedStringDataAsync = &FHoudiniApi::SetAttributeIndexedStringDataAsyncEmptyStub; + FHoudiniApi::SetAttributeInt16ArrayData = &FHoudiniApi::SetAttributeInt16ArrayDataEmptyStub; + FHoudiniApi::SetAttributeInt16ArrayDataAsync = &FHoudiniApi::SetAttributeInt16ArrayDataAsyncEmptyStub; + FHoudiniApi::SetAttributeInt16Data = &FHoudiniApi::SetAttributeInt16DataEmptyStub; + FHoudiniApi::SetAttributeInt16DataAsync = &FHoudiniApi::SetAttributeInt16DataAsyncEmptyStub; + FHoudiniApi::SetAttributeInt16UniqueData = &FHoudiniApi::SetAttributeInt16UniqueDataEmptyStub; + FHoudiniApi::SetAttributeInt16UniqueDataAsync = &FHoudiniApi::SetAttributeInt16UniqueDataAsyncEmptyStub; + FHoudiniApi::SetAttributeInt64ArrayData = &FHoudiniApi::SetAttributeInt64ArrayDataEmptyStub; + FHoudiniApi::SetAttributeInt64ArrayDataAsync = &FHoudiniApi::SetAttributeInt64ArrayDataAsyncEmptyStub; + FHoudiniApi::SetAttributeInt64Data = &FHoudiniApi::SetAttributeInt64DataEmptyStub; + FHoudiniApi::SetAttributeInt64DataAsync = &FHoudiniApi::SetAttributeInt64DataAsyncEmptyStub; + FHoudiniApi::SetAttributeInt64UniqueData = &FHoudiniApi::SetAttributeInt64UniqueDataEmptyStub; + FHoudiniApi::SetAttributeInt64UniqueDataAsync = &FHoudiniApi::SetAttributeInt64UniqueDataAsyncEmptyStub; + FHoudiniApi::SetAttributeInt8ArrayData = &FHoudiniApi::SetAttributeInt8ArrayDataEmptyStub; + FHoudiniApi::SetAttributeInt8ArrayDataAsync = &FHoudiniApi::SetAttributeInt8ArrayDataAsyncEmptyStub; + FHoudiniApi::SetAttributeInt8Data = &FHoudiniApi::SetAttributeInt8DataEmptyStub; + FHoudiniApi::SetAttributeInt8DataAsync = &FHoudiniApi::SetAttributeInt8DataAsyncEmptyStub; + FHoudiniApi::SetAttributeInt8UniqueData = &FHoudiniApi::SetAttributeInt8UniqueDataEmptyStub; + FHoudiniApi::SetAttributeInt8UniqueDataAsync = &FHoudiniApi::SetAttributeInt8UniqueDataAsyncEmptyStub; + FHoudiniApi::SetAttributeIntArrayData = &FHoudiniApi::SetAttributeIntArrayDataEmptyStub; + FHoudiniApi::SetAttributeIntArrayDataAsync = &FHoudiniApi::SetAttributeIntArrayDataAsyncEmptyStub; + FHoudiniApi::SetAttributeIntData = &FHoudiniApi::SetAttributeIntDataEmptyStub; + FHoudiniApi::SetAttributeIntDataAsync = &FHoudiniApi::SetAttributeIntDataAsyncEmptyStub; + FHoudiniApi::SetAttributeIntUniqueData = &FHoudiniApi::SetAttributeIntUniqueDataEmptyStub; + FHoudiniApi::SetAttributeIntUniqueDataAsync = &FHoudiniApi::SetAttributeIntUniqueDataAsyncEmptyStub; + FHoudiniApi::SetAttributeStringArrayData = &FHoudiniApi::SetAttributeStringArrayDataEmptyStub; + FHoudiniApi::SetAttributeStringArrayDataAsync = &FHoudiniApi::SetAttributeStringArrayDataAsyncEmptyStub; + FHoudiniApi::SetAttributeStringData = &FHoudiniApi::SetAttributeStringDataEmptyStub; + FHoudiniApi::SetAttributeStringDataAsync = &FHoudiniApi::SetAttributeStringDataAsyncEmptyStub; + FHoudiniApi::SetAttributeStringUniqueData = &FHoudiniApi::SetAttributeStringUniqueDataEmptyStub; + FHoudiniApi::SetAttributeStringUniqueDataAsync = &FHoudiniApi::SetAttributeStringUniqueDataAsyncEmptyStub; + FHoudiniApi::SetAttributeUInt8ArrayData = &FHoudiniApi::SetAttributeUInt8ArrayDataEmptyStub; + FHoudiniApi::SetAttributeUInt8ArrayDataAsync = &FHoudiniApi::SetAttributeUInt8ArrayDataAsyncEmptyStub; + FHoudiniApi::SetAttributeUInt8Data = &FHoudiniApi::SetAttributeUInt8DataEmptyStub; + FHoudiniApi::SetAttributeUInt8DataAsync = &FHoudiniApi::SetAttributeUInt8DataAsyncEmptyStub; + FHoudiniApi::SetAttributeUInt8UniqueData = &FHoudiniApi::SetAttributeUInt8UniqueDataEmptyStub; + FHoudiniApi::SetAttributeUInt8UniqueDataAsync = &FHoudiniApi::SetAttributeUInt8UniqueDataAsyncEmptyStub; + FHoudiniApi::SetCacheProperty = &FHoudiniApi::SetCachePropertyEmptyStub; + FHoudiniApi::SetCompositorOptions = &FHoudiniApi::SetCompositorOptionsEmptyStub; + FHoudiniApi::SetCurveCounts = &FHoudiniApi::SetCurveCountsEmptyStub; + FHoudiniApi::SetCurveInfo = &FHoudiniApi::SetCurveInfoEmptyStub; + FHoudiniApi::SetCurveKnots = &FHoudiniApi::SetCurveKnotsEmptyStub; + FHoudiniApi::SetCurveOrders = &FHoudiniApi::SetCurveOrdersEmptyStub; + FHoudiniApi::SetCustomString = &FHoudiniApi::SetCustomStringEmptyStub; + FHoudiniApi::SetFaceCounts = &FHoudiniApi::SetFaceCountsEmptyStub; + FHoudiniApi::SetGroupMembership = &FHoudiniApi::SetGroupMembershipEmptyStub; + FHoudiniApi::SetHeightFieldData = &FHoudiniApi::SetHeightFieldDataEmptyStub; + FHoudiniApi::SetImageInfo = &FHoudiniApi::SetImageInfoEmptyStub; + FHoudiniApi::SetInputCurveInfo = &FHoudiniApi::SetInputCurveInfoEmptyStub; + FHoudiniApi::SetInputCurvePositions = &FHoudiniApi::SetInputCurvePositionsEmptyStub; + FHoudiniApi::SetInputCurvePositionsRotationsScales = &FHoudiniApi::SetInputCurvePositionsRotationsScalesEmptyStub; + FHoudiniApi::SetNodeDisplay = &FHoudiniApi::SetNodeDisplayEmptyStub; + FHoudiniApi::SetObjectTransform = &FHoudiniApi::SetObjectTransformEmptyStub; + FHoudiniApi::SetParmExpression = &FHoudiniApi::SetParmExpressionEmptyStub; + FHoudiniApi::SetParmFloatValue = &FHoudiniApi::SetParmFloatValueEmptyStub; + FHoudiniApi::SetParmFloatValues = &FHoudiniApi::SetParmFloatValuesEmptyStub; + FHoudiniApi::SetParmIntValue = &FHoudiniApi::SetParmIntValueEmptyStub; + FHoudiniApi::SetParmIntValues = &FHoudiniApi::SetParmIntValuesEmptyStub; + FHoudiniApi::SetParmNodeValue = &FHoudiniApi::SetParmNodeValueEmptyStub; + FHoudiniApi::SetParmStringValue = &FHoudiniApi::SetParmStringValueEmptyStub; + FHoudiniApi::SetPartInfo = &FHoudiniApi::SetPartInfoEmptyStub; + FHoudiniApi::SetPreset = &FHoudiniApi::SetPresetEmptyStub; + FHoudiniApi::SetServerEnvInt = &FHoudiniApi::SetServerEnvIntEmptyStub; + FHoudiniApi::SetServerEnvString = &FHoudiniApi::SetServerEnvStringEmptyStub; + FHoudiniApi::SetSessionSync = &FHoudiniApi::SetSessionSyncEmptyStub; + FHoudiniApi::SetSessionSyncInfo = &FHoudiniApi::SetSessionSyncInfoEmptyStub; + FHoudiniApi::SetTime = &FHoudiniApi::SetTimeEmptyStub; + FHoudiniApi::SetTimelineOptions = &FHoudiniApi::SetTimelineOptionsEmptyStub; + FHoudiniApi::SetTransformAnimCurve = &FHoudiniApi::SetTransformAnimCurveEmptyStub; + FHoudiniApi::SetUseHoudiniTime = &FHoudiniApi::SetUseHoudiniTimeEmptyStub; + FHoudiniApi::SetVertexList = &FHoudiniApi::SetVertexListEmptyStub; + FHoudiniApi::SetViewport = &FHoudiniApi::SetViewportEmptyStub; + FHoudiniApi::SetVolumeInfo = &FHoudiniApi::SetVolumeInfoEmptyStub; + FHoudiniApi::SetVolumeTileFloatData = &FHoudiniApi::SetVolumeTileFloatDataEmptyStub; + FHoudiniApi::SetVolumeTileIntData = &FHoudiniApi::SetVolumeTileIntDataEmptyStub; + FHoudiniApi::SetVolumeVoxelFloatData = &FHoudiniApi::SetVolumeVoxelFloatDataEmptyStub; + FHoudiniApi::SetVolumeVoxelIntData = &FHoudiniApi::SetVolumeVoxelIntDataEmptyStub; + FHoudiniApi::SetWorkItemFloatAttribute = &FHoudiniApi::SetWorkItemFloatAttributeEmptyStub; + FHoudiniApi::SetWorkItemIntAttribute = &FHoudiniApi::SetWorkItemIntAttributeEmptyStub; + FHoudiniApi::SetWorkItemStringAttribute = &FHoudiniApi::SetWorkItemStringAttributeEmptyStub; + FHoudiniApi::SetWorkitemFloatData = &FHoudiniApi::SetWorkitemFloatDataEmptyStub; + FHoudiniApi::SetWorkitemIntData = &FHoudiniApi::SetWorkitemIntDataEmptyStub; + FHoudiniApi::SetWorkitemStringData = &FHoudiniApi::SetWorkitemStringDataEmptyStub; + FHoudiniApi::Shutdown = &FHoudiniApi::ShutdownEmptyStub; + FHoudiniApi::StartPerformanceMonitorProfile = &FHoudiniApi::StartPerformanceMonitorProfileEmptyStub; + FHoudiniApi::StartThriftNamedPipeServer = &FHoudiniApi::StartThriftNamedPipeServerEmptyStub; + FHoudiniApi::StartThriftSharedMemoryServer = &FHoudiniApi::StartThriftSharedMemoryServerEmptyStub; + FHoudiniApi::StartThriftSocketServer = &FHoudiniApi::StartThriftSocketServerEmptyStub; + FHoudiniApi::StopPerformanceMonitorProfile = &FHoudiniApi::StopPerformanceMonitorProfileEmptyStub; + FHoudiniApi::ThriftServerOptions_Create = &FHoudiniApi::ThriftServerOptions_CreateEmptyStub; + FHoudiniApi::ThriftServerOptions_Init = &FHoudiniApi::ThriftServerOptions_InitEmptyStub; + FHoudiniApi::TimelineOptions_Create = &FHoudiniApi::TimelineOptions_CreateEmptyStub; + FHoudiniApi::TimelineOptions_Init = &FHoudiniApi::TimelineOptions_InitEmptyStub; + FHoudiniApi::TransformEuler_Create = &FHoudiniApi::TransformEuler_CreateEmptyStub; + FHoudiniApi::TransformEuler_Init = &FHoudiniApi::TransformEuler_InitEmptyStub; + FHoudiniApi::Transform_Create = &FHoudiniApi::Transform_CreateEmptyStub; + FHoudiniApi::Transform_Init = &FHoudiniApi::Transform_InitEmptyStub; + FHoudiniApi::Viewport_Create = &FHoudiniApi::Viewport_CreateEmptyStub; + FHoudiniApi::VolumeInfo_Create = &FHoudiniApi::VolumeInfo_CreateEmptyStub; + FHoudiniApi::VolumeInfo_Init = &FHoudiniApi::VolumeInfo_InitEmptyStub; + FHoudiniApi::VolumeTileInfo_Create = &FHoudiniApi::VolumeTileInfo_CreateEmptyStub; + FHoudiniApi::VolumeTileInfo_Init = &FHoudiniApi::VolumeTileInfo_InitEmptyStub; +} + + +bool +FHoudiniApi::IsHAPIInitialized() +{ + return ( FHoudiniApi::IsInitialized != &FHoudiniApi::IsInitializedEmptyStub ); +} + + +HAPI_Result +FHoudiniApi::AddAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::AddGroupEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_AssetInfo +FHoudiniApi::AssetInfo_CreateEmptyStub() +{ + return HAPI_AssetInfo(); +} + + +void +FHoudiniApi::AssetInfo_InitEmptyStub(HAPI_AssetInfo * in) +{ + return; +} + + +HAPI_AttributeInfo +FHoudiniApi::AttributeInfo_CreateEmptyStub() +{ + return HAPI_AttributeInfo(); +} + + +void +FHoudiniApi::AttributeInfo_InitEmptyStub(HAPI_AttributeInfo * in) +{ + return; +} + + +HAPI_Result +FHoudiniApi::BindCustomImplementationEmptyStub(HAPI_SessionType session_type, const char * dll_path) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CancelPDGCookEmptyStub(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CheckForSpecificErrorsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ErrorCodeBits errors_to_look_for, HAPI_ErrorCodeBits * errors_found) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CleanupEmptyStub(const HAPI_Session * session) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ClearConnectionErrorEmptyStub() +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CloseSessionEmptyStub(const HAPI_Session * session) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CommitGeoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CommitWorkItemsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CommitWorkitemsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ComposeChildNodeListEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeTypeBits node_type_filter, HAPI_NodeFlagsBits node_flags_filter, HAPI_Bool recursive, int * count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ComposeNodeCookResultEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_StatusVerbosity verbosity, int * buffer_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ComposeObjectListEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, const char * categories, int * object_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_CompositorOptions +FHoudiniApi::CompositorOptions_CreateEmptyStub() +{ + return HAPI_CompositorOptions(); +} + + +void +FHoudiniApi::CompositorOptions_InitEmptyStub(HAPI_CompositorOptions * in) +{ + return; +} + + +HAPI_Result +FHoudiniApi::ConnectNodeInputEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int input_index, HAPI_NodeId node_id_to_connect, int output_index) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ConvertMatrixToEulerEmptyStub(const HAPI_Session * session, const float * matrix, HAPI_RSTOrder rst_order, HAPI_XYZOrder rot_order, HAPI_TransformEuler * transform_out) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ConvertMatrixToQuatEmptyStub(const HAPI_Session * session, const float * matrix, HAPI_RSTOrder rst_order, HAPI_Transform * transform_out) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ConvertTransformEmptyStub(const HAPI_Session * session, const HAPI_TransformEuler * transform_in, HAPI_RSTOrder rst_order, HAPI_XYZOrder rot_order, HAPI_TransformEuler * transform_out) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ConvertTransformEulerToMatrixEmptyStub(const HAPI_Session * session, const HAPI_TransformEuler * transform, float * matrix) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ConvertTransformQuatToMatrixEmptyStub(const HAPI_Session * session, const HAPI_Transform * transform, float * matrix) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CookNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const HAPI_CookOptions * cook_options) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Bool +FHoudiniApi::CookOptions_AreEqualEmptyStub(const HAPI_CookOptions * left, const HAPI_CookOptions * right) +{ + return HAPI_Bool(); +} + + +HAPI_CookOptions +FHoudiniApi::CookOptions_CreateEmptyStub() +{ + return HAPI_CookOptions(); +} + + +void +FHoudiniApi::CookOptions_InitEmptyStub(HAPI_CookOptions * in) +{ + return; +} + + +HAPI_Result +FHoudiniApi::CookPDGEmptyStub(const HAPI_Session * session, HAPI_NodeId cook_node_id, int generate_only, int blocking) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CookPDGAllOutputsEmptyStub(const HAPI_Session* session, HAPI_NodeId cook_node_id, int generate_only, int blocking) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CreateCOPImageEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, const int width, const int height, const HAPI_ImagePacking packing, HAPI_Bool flip_x, HAPI_Bool flip_y, const float * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CreateCustomSessionEmptyStub(HAPI_SessionType session_type, void * session_info, HAPI_Session * session) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CreateHeightFieldInputEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, const char * name, int xsize, int ysize, float voxelsize, HAPI_HeightFieldSampling sampling, HAPI_NodeId * heightfield_node_id, HAPI_NodeId * height_node_id, HAPI_NodeId * mask_node_id, HAPI_NodeId * merge_node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CreateHeightfieldInputVolumeNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeId * new_node_id, const char * name, int xsize, int ysize, float voxelsize) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CreateInProcessSessionEmptyStub(HAPI_Session * session, const HAPI_SessionInfo * session_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CreateInputCurveNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeId * node_id, const char * name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CreateInputNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeId * node_id, const char * name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CreateNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, const char * operator_name, const char * node_label, HAPI_Bool cook_on_creation, HAPI_NodeId * new_node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CreateThriftNamedPipeSessionEmptyStub(HAPI_Session * session, const char * pipe_name, const HAPI_SessionInfo * session_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CreateThriftSharedMemorySessionEmptyStub(HAPI_Session * session, const char * shared_mem_name, const HAPI_SessionInfo * session_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CreateThriftSocketSessionEmptyStub(HAPI_Session * session, const char * host_name, int port, const HAPI_SessionInfo * session_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CreateWorkItemEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId * work_item_id, const char * name, int index) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::CreateWorkitemEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId * workitem_id, const char * name, int index) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_CurveInfo +FHoudiniApi::CurveInfo_CreateEmptyStub() +{ + return HAPI_CurveInfo(); +} + + +void +FHoudiniApi::CurveInfo_InitEmptyStub(HAPI_CurveInfo * in) +{ + return; +} + + +HAPI_Result +FHoudiniApi::DeleteAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::DeleteGroupEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::DeleteNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::DirtyPDGNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_Bool clean_results) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::DisconnectNodeInputEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int input_index) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::DisconnectNodeOutputsAtEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int output_index) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ExtractImageToFileEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, const char * image_file_format_name, const char * image_planes, const char * destination_folder_path, const char * destination_file_name, int * destination_file_path) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ExtractImageToMemoryEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, const char * image_file_format_name, const char * image_planes, int * buffer_size) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_GeoInfo +FHoudiniApi::GeoInfo_CreateEmptyStub() +{ + return HAPI_GeoInfo(); +} + + +int +FHoudiniApi::GeoInfo_GetGroupCountByTypeEmptyStub(HAPI_GeoInfo * in, HAPI_GroupType type) +{ + return -1; +} + + +void +FHoudiniApi::GeoInfo_InitEmptyStub(HAPI_GeoInfo * in) +{ + return; +} + + +HAPI_Result +FHoudiniApi::GetActiveCacheCountEmptyStub(const HAPI_Session * session, int * active_cache_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetActiveCacheNamesEmptyStub(const HAPI_Session * session, HAPI_StringHandle * cache_names_array, int active_cache_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAssetDefinitionParmCountsEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, int * parm_count, int * int_value_count, int * float_value_count, int * string_value_count, int * choice_value_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAssetDefinitionParmInfosEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, HAPI_ParmInfo * parm_infos_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAssetDefinitionParmTagNameEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, HAPI_ParmId parm_id, int tag_index, HAPI_StringHandle * tag_name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAssetDefinitionParmTagValueEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, HAPI_ParmId parm_id, const char * tag_name, HAPI_StringHandle * tag_value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAssetDefinitionParmValuesEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, int * int_values_array, int int_start, int int_length, float * float_values_array, int float_start, int float_length, HAPI_Bool string_evaluate, HAPI_StringHandle * string_values_array, int string_start, int string_length, HAPI_ParmChoiceInfo * choice_values_array, int choice_start, int choice_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAssetInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_AssetInfo * asset_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAssetLibraryFilePathEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId asset_library_id, HAPI_StringHandle * file_path_sh) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAssetLibraryIdsEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId * asset_library_ids_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeDictionaryArrayDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, HAPI_AttributeInfo* attr_info, HAPI_StringHandle* data_fixed_array, int data_fixed_length, int* sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeDictionaryArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeDictionaryDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, HAPI_AttributeInfo* attr_info, HAPI_StringHandle* data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeDictionaryDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeFloat64ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, double * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeFloat64ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, double * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeFloat64DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, double * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeFloat64DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, double * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeFloatArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, float * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeFloatArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, float * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, float * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeFloatDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, float * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeOwner owner, HAPI_AttributeInfo * attr_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeInt16ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_Int16 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeInt16ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_Int16 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeInt16DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int16 * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeInt16DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int16 * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeInt64ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_Int64 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeInt64ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_Int64 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeInt64DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int64 * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeInt64DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int64 * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeInt8ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_Int8 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeInt8ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_Int8 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeInt8DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int8 * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeInt8DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int8 * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeIntArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeIntArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, int * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, int * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeIntDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, int * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeNamesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_AttributeOwner owner, HAPI_StringHandle * attribute_names_array, int count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeStringArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeStringArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeStringDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeStringDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeUInt8ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_UInt8 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeUInt8ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_UInt8 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeUInt8DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_UInt8 * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAttributeUInt8DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_UInt8 * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAvailableAssetCountEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, int * asset_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetAvailableAssetsEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, HAPI_StringHandle * asset_names_array, int asset_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetBoxInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId geo_node_id, HAPI_PartId part_id, HAPI_BoxInfo * box_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetCachePropertyEmptyStub(const HAPI_Session * session, const char * cache_name, HAPI_CacheProperty cache_property, int * property_value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetComposedChildNodeListEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeId * child_node_ids_array, int count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetComposedNodeCookResultEmptyStub(const HAPI_Session * session, char * string_value, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetComposedObjectListEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_ObjectInfo * object_infos_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetComposedObjectTransformsEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_RSTOrder rst_order, HAPI_Transform * transform_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetCompositorOptionsEmptyStub(const HAPI_Session * session, HAPI_CompositorOptions * compositor_options) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetConnectionErrorEmptyStub(char * string_value, int length, HAPI_Bool clear) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetConnectionErrorLengthEmptyStub(int * buffer_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetCookingCurrentCountEmptyStub(const HAPI_Session * session, int * count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetCookingTotalCountEmptyStub(const HAPI_Session * session, int * count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetCurveCountsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * counts_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetCurveInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_CurveInfo * info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetCurveKnotsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, float * knots_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetCurveOrdersEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * orders_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetDisplayGeoInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId object_node_id, HAPI_GeoInfo * geo_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetEdgeCountOfEdgeGroupEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * group_name, int * edge_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetEnvIntEmptyStub(HAPI_EnvIntType int_type, int * value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetFaceCountsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * face_counts_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetFirstVolumeTileEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_VolumeTileInfo * tile) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetGeoInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_GeoInfo * geo_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetGeoSizeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * format, int * size) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetGroupCountOnPackedInstancePartEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * pointGroupCount, int * primitiveGroupCount) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetGroupMembershipEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name, HAPI_Bool * membership_array_all_equal, int * membership_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetGroupMembershipOnPackedInstancePartEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name, HAPI_Bool * membership_array_all_equal, int * membership_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetGroupNamesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_GroupType group_type, HAPI_StringHandle * group_names_array, int group_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetGroupNamesOnPackedInstancePartEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, HAPI_StringHandle * group_names_array, int group_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetHIPFileNodeCountEmptyStub(const HAPI_Session * session, HAPI_HIPFileId id, int * count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetHIPFileNodeIdsEmptyStub(const HAPI_Session * session, HAPI_HIPFileId id, HAPI_NodeId * node_ids, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetHandleBindingInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int handle_index, HAPI_HandleBindingInfo * handle_binding_infos_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetHandleInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_HandleInfo * handle_infos_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetHeightFieldDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, float * values_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetImageFilePathEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, const char * image_file_format_name, const char * image_planes, const char * destination_folder_path, const char * destination_file_name, HAPI_ParmId texture_parm_id, int * destination_file_path) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetImageInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, HAPI_ImageInfo * image_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetImageMemoryBufferEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, char * buffer, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetImagePlaneCountEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, int * image_plane_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetImagePlanesEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, HAPI_StringHandle * image_planes_array, int image_plane_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetInputCurveInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_InputCurveInfo * info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetInstanceTransformsOnPartEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_RSTOrder rst_order, HAPI_Transform * transforms_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetInstancedObjectIdsEmptyStub(const HAPI_Session * session, HAPI_NodeId object_node_id, HAPI_NodeId * instanced_node_id_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetInstancedPartIdsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_PartId * instanced_parts_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetInstancerPartTransformsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_RSTOrder rst_order, HAPI_Transform * transforms_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetJobStatusEmptyStub(const HAPI_Session * session, int job_id, HAPI_JobStatus * job_status) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetLoadedAssetLibraryCountEmptyStub(const HAPI_Session * session, int * count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetManagerNodeIdEmptyStub(const HAPI_Session * session, HAPI_NodeType node_type, HAPI_NodeId * node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetMaterialInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, HAPI_MaterialInfo * material_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetMaterialNodeIdsOnFacesEmptyStub(const HAPI_Session * session, HAPI_NodeId geometry_node_id, HAPI_PartId part_id, HAPI_Bool * are_all_the_same, HAPI_NodeId * material_ids_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetMessageNodeCountEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int * count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetMessageNodeIdsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeId * message_node_ids_array, int count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetNextVolumeTileEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_VolumeTileInfo * tile) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetNodeCookResultEmptyStub(const HAPI_Session * session, char * string_value, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetNodeCookResultLengthEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_StatusVerbosity verbosity, int * buffer_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetNodeFromPathEmptyStub(const HAPI_Session * session, const HAPI_NodeId parent_node_id, const char * path, HAPI_NodeId * node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetNodeInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeInfo * node_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetNodeInputNameEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int input_idx, HAPI_StringHandle * name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetNodeOutputNameEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int output_idx, HAPI_StringHandle * name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetNodePathEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeId relative_to_node_id, HAPI_StringHandle * path) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetNumWorkItemsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int * num) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetNumWorkitemsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int * num) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetObjectInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ObjectInfo * object_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetObjectTransformEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeId relative_to_node_id, HAPI_RSTOrder rst_order, HAPI_Transform * transform) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetOutputGeoCountEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, int* count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetOutputGeoInfosEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_GeoInfo* geo_infos_array, int count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetOutputNodeIdEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int output, HAPI_NodeId * output_node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetPDGEventsEmptyStub(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id, HAPI_PDG_EventInfo * event_array, int length, int * event_count, int * remaining_events) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetPDGGraphContextIdEmptyStub(const HAPI_Session * session, HAPI_NodeId top_node_id, HAPI_PDG_GraphContextId * context_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetPDGGraphContextsEmptyStub(const HAPI_Session * session, HAPI_StringHandle * context_names_array, HAPI_PDG_GraphContextId * context_id_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetPDGGraphContextsCountEmptyStub(const HAPI_Session* session, int* num_contexts) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetPDGStateEmptyStub(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id, int * pdg_state) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParametersEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmInfo * parm_infos_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmChoiceListsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmChoiceInfo * parm_choices_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmExpressionEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, HAPI_StringHandle * value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmFileEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, const char * destination_directory, const char * destination_file_name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmFloatValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, float * value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmFloatValuesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, float * values_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmIdFromNameEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, HAPI_ParmId * parm_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, HAPI_ParmInfo * parm_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmInfoFromNameEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, HAPI_ParmInfo * parm_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmIntValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, int * value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmIntValuesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int * values_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmNodeValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, HAPI_NodeId * value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmStringValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, HAPI_Bool evaluate, HAPI_StringHandle * value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmStringValuesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_Bool evaluate, HAPI_StringHandle * values_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmTagNameEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int tag_index, HAPI_StringHandle * tag_name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmTagValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, const char * tag_name, HAPI_StringHandle * tag_value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetParmWithTagEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * tag_name, HAPI_ParmId * parm_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetPartInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_PartInfo * part_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetPresetEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, char * buffer, int buffer_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetPresetBufLengthEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PresetType preset_type, const char * preset_name, int * buffer_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetPresetCountEmptyStub(const HAPI_Session * session, const char * buffer, int buffer_length, int * count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetPresetNamesEmptyStub(const HAPI_Session * session, const char * buffer, int buffer_length, HAPI_StringHandle * preset_names_array, int preset_names_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetServerEnvIntEmptyStub(const HAPI_Session * session, const char * variable_name, int * value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetServerEnvStringEmptyStub(const HAPI_Session * session, const char * variable_name, HAPI_StringHandle * value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetServerEnvVarCountEmptyStub(const HAPI_Session * session, int * env_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetServerEnvVarListEmptyStub(const HAPI_Session * session, HAPI_StringHandle * values_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetSessionEnvIntEmptyStub(const HAPI_Session * session, HAPI_SessionEnvIntType int_type, int * value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetSessionSyncInfoEmptyStub(const HAPI_Session * session, HAPI_SessionSyncInfo * session_sync_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetSphereInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId geo_node_id, HAPI_PartId part_id, HAPI_SphereInfo * sphere_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetStatusEmptyStub(const HAPI_Session * session, HAPI_StatusType status_type, int * status) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetStatusStringEmptyStub(const HAPI_Session * session, HAPI_StatusType status_type, char * string_value, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetStatusStringBufLengthEmptyStub(const HAPI_Session * session, HAPI_StatusType status_type, HAPI_StatusVerbosity verbosity, int * buffer_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetStringEmptyStub(const HAPI_Session * session, HAPI_StringHandle string_handle, char * string_value, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetStringBatchEmptyStub(const HAPI_Session * session, char * char_buffer, int char_array_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetStringBatchSizeEmptyStub(const HAPI_Session * session, const int * string_handle_array, int string_handle_count, int * string_buffer_size) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetStringBufLengthEmptyStub(const HAPI_Session * session, HAPI_StringHandle string_handle, int * buffer_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetSupportedImageFileFormatCountEmptyStub(const HAPI_Session * session, int * file_format_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetSupportedImageFileFormatsEmptyStub(const HAPI_Session * session, HAPI_ImageFileFormat * formats_array, int file_format_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetTimeEmptyStub(const HAPI_Session * session, double * time) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetTimelineOptionsEmptyStub(const HAPI_Session * session, HAPI_TimelineOptions * timeline_options) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetTotalCookCountEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeTypeBits node_type_filter, HAPI_NodeFlagsBits node_flags_filter, HAPI_Bool recursive, int * count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetUseHoudiniTimeEmptyStub(const HAPI_Session * session, HAPI_Bool * enabled) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetVertexListEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * vertex_list_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetViewportEmptyStub(const HAPI_Session * session, HAPI_Viewport * viewport) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetVolumeBoundsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, float * x_min, float * y_min, float * z_min, float * x_max, float * y_max, float * z_max, float * x_center, float * y_center, float * z_center) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetVolumeInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_VolumeInfo * volume_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetVolumeTileFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, float fill_value, const HAPI_VolumeTileInfo * tile, float * values_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetVolumeTileIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int fill_value, const HAPI_VolumeTileInfo * tile, int * values_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetVolumeVisualInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_VolumeVisualInfo * visual_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetVolumeVoxelFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int x_index, int y_index, int z_index, float * values_array, int value_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetVolumeVoxelIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int x_index, int y_index, int z_index, int * values_array, int value_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkItemAttributeSizeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, int * length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkItemFloatAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, float * data_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkItemInfoEmptyStub(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id, HAPI_PDG_WorkItemId work_item_id, HAPI_PDG_WorkItemInfo * work_item_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkItemIntAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, int * data_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkItemOutputFilesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, HAPI_PDG_WorkItemOutputFile * resultinfo_array, int resultinfo_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkItemStringAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, HAPI_StringHandle * data_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkItemsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int * work_item_ids_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkitemDataLengthEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, int * length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkitemFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, float * data_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkitemInfoEmptyStub(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id, HAPI_PDG_WorkItemId workitem_id, HAPI_PDG_WorkItemInfo * workitem_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkitemIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, int * data_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkitemResultInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, HAPI_PDG_WorkItemOutputFile * resultinfo_array, int resultinfo_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkitemStringDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, HAPI_StringHandle * data_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::GetWorkitemsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int * workitem_ids_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_HandleBindingInfo +FHoudiniApi::HandleBindingInfo_CreateEmptyStub() +{ + return HAPI_HandleBindingInfo(); +} + + +void +FHoudiniApi::HandleBindingInfo_InitEmptyStub(HAPI_HandleBindingInfo * in) +{ + return; +} + + +HAPI_HandleInfo +FHoudiniApi::HandleInfo_CreateEmptyStub() +{ + return HAPI_HandleInfo(); +} + + +void +FHoudiniApi::HandleInfo_InitEmptyStub(HAPI_HandleInfo * in) +{ + return; +} + + +HAPI_ImageFileFormat +FHoudiniApi::ImageFileFormat_CreateEmptyStub() +{ + return HAPI_ImageFileFormat(); +} + + +void +FHoudiniApi::ImageFileFormat_InitEmptyStub(HAPI_ImageFileFormat *in) +{ + return; +} + + +HAPI_ImageInfo +FHoudiniApi::ImageInfo_CreateEmptyStub() +{ + return HAPI_ImageInfo(); +} + + +void +FHoudiniApi::ImageInfo_InitEmptyStub(HAPI_ImageInfo * in) +{ + return; +} + + +HAPI_Result +FHoudiniApi::InitializeEmptyStub(const HAPI_Session * session, const HAPI_CookOptions * cook_options, HAPI_Bool use_cooking_thread, int cooking_thread_stack_size, const char * houdini_environment_files, const char * otl_search_path, const char * dso_search_path, const char * image_dso_search_path, const char * audio_dso_search_path) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_InputCurveInfo +FHoudiniApi::InputCurveInfo_CreateEmptyStub() +{ + return HAPI_InputCurveInfo(); +} + + +void +FHoudiniApi::InputCurveInfo_InitEmptyStub(HAPI_InputCurveInfo * in) +{ + return; +} + + +HAPI_Result +FHoudiniApi::InsertMultiparmInstanceEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int instance_position) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::InterruptEmptyStub(const HAPI_Session * session) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::IsInitializedEmptyStub(const HAPI_Session * session) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::IsNodeValidEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int unique_node_id, HAPI_Bool * answer) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::IsSessionValidEmptyStub(const HAPI_Session * session) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Keyframe +FHoudiniApi::Keyframe_CreateEmptyStub() +{ + return HAPI_Keyframe(); +} + + +void +FHoudiniApi::Keyframe_InitEmptyStub(HAPI_Keyframe * in) +{ + return; +} + + +HAPI_Result +FHoudiniApi::LoadAssetLibraryFromFileEmptyStub(const HAPI_Session * session, const char * file_path, HAPI_Bool allow_overwrite, HAPI_AssetLibraryId * library_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::LoadAssetLibraryFromMemoryEmptyStub(const HAPI_Session * session, const char * library_buffer, int library_buffer_length, HAPI_Bool allow_overwrite, HAPI_AssetLibraryId * library_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::LoadGeoFromFileEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * file_name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::LoadGeoFromMemoryEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * format, const char * buffer, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::LoadHIPFileEmptyStub(const HAPI_Session * session, const char * file_name, HAPI_Bool cook_on_load) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::LoadNodeFromFileEmptyStub(const HAPI_Session * session, const char * file_name, HAPI_NodeId parent_node_id, const char * node_label, HAPI_Bool cook_on_load, HAPI_NodeId * new_node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_MaterialInfo +FHoudiniApi::MaterialInfo_CreateEmptyStub() +{ + return HAPI_MaterialInfo(); +} + + +void +FHoudiniApi::MaterialInfo_InitEmptyStub(HAPI_MaterialInfo * in) +{ + return; +} + + +HAPI_Result +FHoudiniApi::MergeHIPFileEmptyStub(const HAPI_Session * session, const char * file_name, HAPI_Bool cook_on_load, HAPI_HIPFileId * file_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_NodeInfo +FHoudiniApi::NodeInfo_CreateEmptyStub() +{ + return HAPI_NodeInfo(); +} + + +void +FHoudiniApi::NodeInfo_InitEmptyStub(HAPI_NodeInfo * in) +{ + return; +} + + +HAPI_ObjectInfo +FHoudiniApi::ObjectInfo_CreateEmptyStub() +{ + return HAPI_ObjectInfo(); +} + + +void +FHoudiniApi::ObjectInfo_InitEmptyStub(HAPI_ObjectInfo * in) +{ + return; +} + + +HAPI_ParmChoiceInfo +FHoudiniApi::ParmChoiceInfo_CreateEmptyStub() +{ + return HAPI_ParmChoiceInfo(); +} + + +void +FHoudiniApi::ParmChoiceInfo_InitEmptyStub(HAPI_ParmChoiceInfo * in) +{ + return; +} + + +HAPI_Result +FHoudiniApi::ParmHasExpressionEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, HAPI_Bool * has_expression) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ParmHasTagEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, const char * tag_name, HAPI_Bool * has_tag) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_ParmInfo +FHoudiniApi::ParmInfo_CreateEmptyStub() +{ + return HAPI_ParmInfo(); +} + + +int +FHoudiniApi::ParmInfo_GetFloatValueCountEmptyStub(const HAPI_ParmInfo * in) +{ + return -1; +} + + +int +FHoudiniApi::ParmInfo_GetIntValueCountEmptyStub(const HAPI_ParmInfo * in) +{ + return -1; +} + + +int +FHoudiniApi::ParmInfo_GetStringValueCountEmptyStub(const HAPI_ParmInfo* in) +{ + return -1; +} + + +void +FHoudiniApi::ParmInfo_InitEmptyStub(HAPI_ParmInfo * in) +{ + return; +} + + +HAPI_Bool +FHoudiniApi::ParmInfo_IsFloatEmptyStub(const HAPI_ParmInfo * in) +{ + return HAPI_Bool(); +} + + +HAPI_Bool +FHoudiniApi::ParmInfo_IsIntEmptyStub(const HAPI_ParmInfo * in) +{ + return HAPI_Bool(); +} + + +HAPI_Bool +FHoudiniApi::ParmInfo_IsNodeEmptyStub(const HAPI_ParmInfo * in) +{ + return HAPI_Bool(); +} + + +HAPI_Bool +FHoudiniApi::ParmInfo_IsNonValueEmptyStub(const HAPI_ParmInfo * in) +{ + return HAPI_Bool(); +} + + +HAPI_Bool +FHoudiniApi::ParmInfo_IsPathEmptyStub(const HAPI_ParmInfo * in) +{ + return HAPI_Bool(); +} + + +HAPI_Bool +FHoudiniApi::ParmInfo_IsStringEmptyStub(const HAPI_ParmInfo * in) +{ + return HAPI_Bool(); +} + + +HAPI_PartInfo +FHoudiniApi::PartInfo_CreateEmptyStub() +{ + return HAPI_PartInfo(); +} + + +int +FHoudiniApi::PartInfo_GetAttributeCountByOwnerEmptyStub(HAPI_PartInfo * in, HAPI_AttributeOwner owner) +{ + return -1; +} + + +int +FHoudiniApi::PartInfo_GetElementCountByAttributeOwnerEmptyStub(HAPI_PartInfo * in, HAPI_AttributeOwner owner) +{ + return -1; +} + + +int +FHoudiniApi::PartInfo_GetElementCountByGroupTypeEmptyStub(HAPI_PartInfo * in, HAPI_GroupType type) +{ + return -1; +} + + +void +FHoudiniApi::PartInfo_InitEmptyStub(HAPI_PartInfo * in) +{ + return; +} + + +HAPI_Result +FHoudiniApi::PausePDGCookEmptyStub(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::PythonThreadInterpreterLockEmptyStub(const HAPI_Session * session, HAPI_Bool locked) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::QueryNodeInputEmptyStub(const HAPI_Session * session, HAPI_NodeId node_to_query, int input_index, HAPI_NodeId * connected_node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::QueryNodeOutputConnectedCountEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int output_idx, HAPI_Bool into_subnets, HAPI_Bool through_dots, int * connected_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::QueryNodeOutputConnectedNodesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int output_idx, HAPI_Bool into_subnets, HAPI_Bool through_dots, HAPI_NodeId * connected_node_ids_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::RemoveCustomStringEmptyStub(const HAPI_Session * session, const HAPI_StringHandle string_handle) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::RemoveMultiparmInstanceEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int instance_position) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::RemoveParmExpressionEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int index) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::RenameNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * new_name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::RenderCOPOutputToImageEmptyStub(const HAPI_Session * session, HAPI_NodeId cop_node_id, const char * cop_output_name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::RenderCOPToImageEmptyStub(const HAPI_Session * session, HAPI_NodeId cop_node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::RenderTextureToImageEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, HAPI_ParmId parm_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ResetSimulationEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::RevertGeoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::RevertParmToDefaultEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::RevertParmToDefaultsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SaveGeoToFileEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * file_name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SaveGeoToMemoryEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, char * buffer, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SaveHIPFileEmptyStub(const HAPI_Session * session, const char * file_path, HAPI_Bool lock_nodes) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SaveNodeToFileEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * file_name) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_SessionInfo +FHoudiniApi::SessionInfo_CreateEmptyStub() +{ + return HAPI_SessionInfo(); +} + + +void +FHoudiniApi::SessionInfo_InitEmptyStub(HAPI_SessionInfo * in) +{ + return; +} + + +HAPI_SessionSyncInfo +FHoudiniApi::SessionSyncInfo_CreateEmptyStub() +{ + return HAPI_SessionSyncInfo(); +} + + +HAPI_Result +FHoudiniApi::SetAnimCurveEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int parm_index, const HAPI_Keyframe * curve_keyframes_array, int keyframe_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeDictionaryArrayDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char** data_fixed_array, int data_fixed_length, const int* sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeDictionaryArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeDictionaryDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char** data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeDictionaryDataAsyncEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char** data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeFloat64ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const double * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeFloat64ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const double * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeFloat64DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const double * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeFloat64DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const double * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeFloat64UniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const double* data_array, int data_length, int start_index, int num_indices) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeFloat64UniqueDataAsyncEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const double* data_array, int data_length, int start_index, int num_indices, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeFloatArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const float * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeFloatArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const float * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const float * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeFloatDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const float * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeFloatUniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const float* data_array, int data_length, int start_index, int num_indices) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeFloatUniqueDataAsyncEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const float* data_array, int data_length, int start_index, int num_indices, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeIndexedStringDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char** string_array, int string_count, const int* indices_array, int indices_start, int indices_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeIndexedStringDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** string_array, int string_count, const int * indices_array, int indices_start, int indices_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt16ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int16 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt16ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int16 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt16DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int16 * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt16DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int16 * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt16UniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int16* data_array, int data_length, int start_index, int num_indices) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt16UniqueDataAsyncEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int16* data_array, int data_length, int start_index, int num_indices, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt64ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int64 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt64ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int64 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt64DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int64 * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt64DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int64 * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt64UniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int64* data_array, int data_length, int start_index, int num_indices) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt64UniqueDataAsyncEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int64* data_array, int data_length, int start_index, int num_indices, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt8ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt8ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt8DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8 * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt8DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8 * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt8UniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int8* data_array, int data_length, int start_index, int num_indices) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeInt8UniqueDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8* data_array, int data_length, int start_index, int num_indices, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeIntArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeIntArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeIntDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeIntUniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const int* data_array, int data_length, int start_index, int num_indices) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeIntUniqueDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_array, int data_length, int start_index, int num_indices, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeStringArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeStringArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeStringDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeStringDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeStringUniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char* data_array, int data_length, int start_index, int num_indices) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeStringUniqueDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char * data_array, int data_length, int start_index, int num_indices, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeUInt8ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeUInt8ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeUInt8DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeUInt8DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_array, int start, int length, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeUInt8UniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_UInt8* data_array, int data_length, int start_index, int num_indices) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetAttributeUInt8UniqueDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_array, int data_length, int start_index, int num_indices, int * job_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetCachePropertyEmptyStub(const HAPI_Session * session, const char * cache_name, HAPI_CacheProperty cache_property, int property_value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetCompositorOptionsEmptyStub(const HAPI_Session * session, const HAPI_CompositorOptions * compositor_options) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetCurveCountsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const int * counts_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetCurveInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_CurveInfo * info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetCurveKnotsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const float * knots_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetCurveOrdersEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const int * orders_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetCustomStringEmptyStub(const HAPI_Session * session, const char * string_value, HAPI_StringHandle * handle_value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetFaceCountsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const int * face_counts_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetGroupMembershipEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name, const int * membership_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetHeightFieldDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const float * values_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetImageInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, const HAPI_ImageInfo * image_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetInputCurveInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_InputCurveInfo * info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetInputCurvePositionsEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const float* positions_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetInputCurvePositionsRotationsScalesEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const float* positions_array, int positions_start, int positions_length, const float* rotations_array, int rotations_start, int rotations_length, const float * scales_array, int scales_start, int scales_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetNodeDisplayEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int onOff) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetObjectTransformEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const HAPI_TransformEuler * trans) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetParmExpressionEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * value, HAPI_ParmId parm_id, int index) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetParmFloatValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, float value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetParmFloatValuesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const float * values_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetParmIntValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, int value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetParmIntValuesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const int * values_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetParmNodeValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, HAPI_NodeId value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetParmStringValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * value, HAPI_ParmId parm_id, int index) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetPartInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_PartInfo * part_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetPresetEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PresetType preset_type, const char * preset_name, const char * buffer, int buffer_length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetServerEnvIntEmptyStub(const HAPI_Session * session, const char * variable_name, int value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetServerEnvStringEmptyStub(const HAPI_Session * session, const char * variable_name, const char * value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetSessionSyncEmptyStub(const HAPI_Session * session, HAPI_Bool enable) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetSessionSyncInfoEmptyStub(const HAPI_Session * session, const HAPI_SessionSyncInfo * session_sync_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetTimeEmptyStub(const HAPI_Session * session, double time) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetTimelineOptionsEmptyStub(const HAPI_Session * session, const HAPI_TimelineOptions * timeline_options) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetTransformAnimCurveEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_TransformComponent trans_comp, const HAPI_Keyframe * curve_keyframes_array, int keyframe_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetUseHoudiniTimeEmptyStub(const HAPI_Session * session, HAPI_Bool enabled) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetVertexListEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const int * vertex_list_array, int start, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetViewportEmptyStub(const HAPI_Session * session, const HAPI_Viewport * viewport) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetVolumeInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_VolumeInfo * volume_info) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetVolumeTileFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_VolumeTileInfo * tile, const float * values_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetVolumeTileIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_VolumeTileInfo * tile, const int * values_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetVolumeVoxelFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int x_index, int y_index, int z_index, const float * values_array, int value_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetVolumeVoxelIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int x_index, int y_index, int z_index, const int * values_array, int value_count) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetWorkItemFloatAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, const float * values_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetWorkItemIntAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, const int * values_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetWorkItemStringAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, int data_index, const char * value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetWorkitemFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, const float * values_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetWorkitemIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, const int * values_array, int length) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::SetWorkitemStringDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, int data_index, const char * value) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::ShutdownEmptyStub(const HAPI_Session * session) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::StartPerformanceMonitorProfileEmptyStub(const HAPI_Session * session, const char * title, int * profile_id) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::StartThriftNamedPipeServerEmptyStub(const HAPI_ThriftServerOptions * options, const char * pipe_name, HAPI_ProcessId * process_id, const char * log_file) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::StartThriftSharedMemoryServerEmptyStub(const HAPI_ThriftServerOptions * options, const char * shared_mem_name, HAPI_ProcessId * process_id, const char * log_file) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::StartThriftSocketServerEmptyStub(const HAPI_ThriftServerOptions * options, int port, HAPI_ProcessId * process_id, const char * log_file) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniApi::StopPerformanceMonitorProfileEmptyStub(const HAPI_Session * session, int profile_id, const char * file_path) +{ + return HAPI_RESULT_FAILURE; +} + + +HAPI_ThriftServerOptions +FHoudiniApi::ThriftServerOptions_CreateEmptyStub() +{ + return HAPI_ThriftServerOptions(); +} + + +void +FHoudiniApi::ThriftServerOptions_InitEmptyStub(HAPI_ThriftServerOptions * in) +{ + return; +} + + +HAPI_TimelineOptions +FHoudiniApi::TimelineOptions_CreateEmptyStub() +{ + return HAPI_TimelineOptions(); +} + + +void +FHoudiniApi::TimelineOptions_InitEmptyStub(HAPI_TimelineOptions * in) +{ + return; +} + + +HAPI_TransformEuler +FHoudiniApi::TransformEuler_CreateEmptyStub() +{ + return HAPI_TransformEuler(); +} + + +void +FHoudiniApi::TransformEuler_InitEmptyStub(HAPI_TransformEuler * in) +{ + return; +} + + +HAPI_Transform +FHoudiniApi::Transform_CreateEmptyStub() +{ + return HAPI_Transform(); +} + + +void +FHoudiniApi::Transform_InitEmptyStub(HAPI_Transform * in) +{ + return; +} + + +HAPI_Viewport +FHoudiniApi::Viewport_CreateEmptyStub() +{ + return HAPI_Viewport(); +} + + +HAPI_VolumeInfo +FHoudiniApi::VolumeInfo_CreateEmptyStub() +{ + return HAPI_VolumeInfo(); +} + + +void +FHoudiniApi::VolumeInfo_InitEmptyStub(HAPI_VolumeInfo * in) +{ + return; +} + + +HAPI_VolumeTileInfo +FHoudiniApi::VolumeTileInfo_CreateEmptyStub() +{ + return HAPI_VolumeTileInfo(); +} + + +void +FHoudiniApi::VolumeTileInfo_InitEmptyStub(HAPI_VolumeTileInfo * in) +{ + return; +} + + + +``` + +`Source/HoudiniEngine/Private/HoudiniBakeLandscape.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniAsset.h" +#include "HoudiniBakeLandscape.h" +#include "HoudiniCookable.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniEngineOutputStats.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniLandscapeRuntimeUtils.h" +#include "HoudiniLandscapeSplineTranslator.h" +#include "HoudiniLandscapeUtils.h" +#include "HoudiniMeshTranslator.h" +#include "HoudiniOutput.h" +#include "HoudiniPackageParams.h" +#include "HoudiniSplineComponent.h" +#include "HoudiniStringResolver.h" +#include "UnrealLandscapeTranslator.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "Components/AudioComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "Containers/UnrealString.h" +#include "Editor.h" +#include "Engine/World.h" +#include "FileHelpers.h" +#include "GameFramework/Actor.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Landscape.h" +#include "LandscapeEdit.h" +#include "LandscapeInfo.h" +#include "LandscapeProxy.h" +#include "LandscapeSplineActor.h" +#include "LandscapeStreamingProxy.h" +#include "PackageTools.h" +#include "UObject/Package.h" +#include "UObject/MetaData.h" +#include "WorldPartition/WorldPartition.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + #include "LandscapeEditLayer.h" +#endif + +bool +FHoudiniLandscapeBake::BakeLandscapeLayer( + FHoudiniPackageParams& PackageParams, + UHoudiniLandscapeTargetLayerOutput& LayerOutput, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData, + FHoudiniClearedEditLayers& ClearedLayers) +{ + ALandscape* OutputLandscape = LayerOutput.Landscape; + ULandscapeInfo* TargetLandscapeInfo = OutputLandscape->GetLandscapeInfo(); + FHoudiniExtents Extents = LayerOutput.Extents; + + // Layers are mandatory since 5.7 +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION < 7 + if (!OutputLandscape->bCanHaveLayersContent) + { + HOUDINI_LOG_MESSAGE(TEXT("Landscape {0} has no edit layers, so baking does nothing."), *OutputLandscape->GetActorLabel()); + return true; + } +#endif + //--------------------------------------------------------------------------------------------------------------------------- + // For landscape layers baking is the act of copying cooked data to a baked layer. We do not need to do that if we already + // wrote directly to the final layer. + //--------------------------------------------------------------------------------------------------------------------------- + + if (!LayerOutput.bCookedLayerRequiresBaking) + return true; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + const FLandscapeLayer* BakedLayer = FHoudiniLandscapeUtils::GetOrCreateEditLayer(OutputLandscape, FName(LayerOutput.BakedEditLayer)); +#else + FLandscapeLayer* BakedLayer = FHoudiniLandscapeUtils::GetOrCreateEditLayer(OutputLandscape, FName(LayerOutput.BakedEditLayer)); +#endif + ULandscapeLayerInfoObject* TargetLayerInfo = OutputLandscape->GetLandscapeInfo()->GetLayerInfoByName(FName(LayerOutput.TargetLayer)); + + bool bWasLocked = LayerOutput.bLockLayer; + if (LayerOutput.bWriteLockedLayers) + LayerOutput.bLockLayer = false; + + //--------------------------------------------------------------------------------------------------------------------------- + // Clear the layer, but only once per bake. + //--------------------------------------------------------------------------------------------------------------------------- + + bool bIsHeightFieldLayer = LayerOutput.TargetLayer == "height"; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FGuid BakedLayerGuid = BakedLayer->EditLayer->GetGuid(); +#else + FGuid BakedLayerGuid = BakedLayer->Guid; +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + if (LayerOutput.bClearLayer && !ClearedLayers.Contains(LayerOutput.BakedEditLayer, LayerOutput.TargetLayer)) +#else + if (OutputLandscape->bHasLayersContent && + LayerOutput.bClearLayer && !ClearedLayers.Contains(LayerOutput.BakedEditLayer, LayerOutput.TargetLayer)) +#endif + { + ClearedLayers.Add(LayerOutput.BakedEditLayer, LayerOutput.TargetLayer); + if (bIsHeightFieldLayer) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + OutputLandscape->ClearEditLayer(BakedLayerGuid, nullptr, ELandscapeToolTargetTypeFlags::Heightmap); +#else + OutputLandscape->ClearLayer(BakedLayerGuid, nullptr, ELandscapeClearMode::Clear_Heightmap); +#endif + } + else + { + HOUDINI_CHECK_RETURN(TargetLayerInfo, false); + OutputLandscape->ClearPaintLayer(BakedLayerGuid, TargetLayerInfo); + } + + } + + //--------------------------------------------------------------------------------------------------------------------------- + // Copy cooked layer to baked layer + //--------------------------------------------------------------------------------------------------------------------------- + + if (!bIsHeightFieldLayer) + { + HOUDINI_CHECK_RETURN(TargetLayerInfo, false); + FScopedSetLandscapeEditingLayer Scope(OutputLandscape, BakedLayerGuid, [&] { OutputLandscape->RequestLayersContentUpdate(ELandscapeLayerUpdateMode::Update_All); }); + + TArray Values = FHoudiniLandscapeUtils::GetLayerData(OutputLandscape, Extents, FName(LayerOutput.CookedEditLayer), FName(LayerOutput.TargetLayer)); + + if (LayerOutput.TargetLayer == HAPI_UNREAL_VISIBILITY_LAYER_NAME) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + TAlphamapAccessor AlphaAccessor(OutputLandscape->GetLandscapeInfo(), ALandscapeProxy::VisibilityLayer); +#else + FAlphamapAccessor AlphaAccessor(OutputLandscape->GetLandscapeInfo(), ALandscapeProxy::VisibilityLayer); +#endif + AlphaAccessor.SetData( + Extents.Min.X, Extents.Min.Y, Extents.Max.X, Extents.Max.Y, + Values.GetData(), + ELandscapeLayerPaintingRestriction::None); + } + else + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + TAlphamapAccessor AlphaAccessor(OutputLandscape->GetLandscapeInfo(), TargetLayerInfo); +#else + FAlphamapAccessor AlphaAccessor(OutputLandscape->GetLandscapeInfo(), TargetLayerInfo); +#endif + + AlphaAccessor.SetData( + Extents.Min.X, Extents.Min.Y, Extents.Max.X, Extents.Max.Y, + Values.GetData(), + ELandscapeLayerPaintingRestriction::None); + } + } + else + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + const FLandscapeLayer* EditLayer = FHoudiniLandscapeUtils::GetEditLayer(OutputLandscape, FName(LayerOutput.CookedEditLayer)); +#else + FLandscapeLayer* EditLayer = FHoudiniLandscapeUtils::GetEditLayer(OutputLandscape, FName(LayerOutput.CookedEditLayer)); +#endif + HOUDINI_CHECK_RETURN(EditLayer != nullptr, false); + TArray Values = FHoudiniLandscapeUtils::GetHeightData(OutputLandscape, Extents, EditLayer); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FGuid LayerGuid = BakedLayer->EditLayer->GetGuid(); +#else + FGuid LayerGuid = BakedLayer->Guid; +#endif + FScopedSetLandscapeEditingLayer Scope(OutputLandscape, LayerGuid, [&] { OutputLandscape->RequestLayersContentUpdate(ELandscapeLayerUpdateMode::Update_All); }); + + FLandscapeEditDataInterface LandscapeEdit(TargetLandscapeInfo); + FHeightmapAccessor HeightmapAccessor(TargetLandscapeInfo); + HeightmapAccessor.SetData(Extents.Min.X, Extents.Min.Y, Extents.Max.X, Extents.Max.Y, Values.GetData()); + + } + + int EditLayerIndex = OutputLandscape->GetLayerIndex(FName(LayerOutput.BakedEditLayer)); + + if (bWasLocked && BakedLayer) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + OutputLandscape->GetEditLayer(EditLayerIndex)->SetLocked(true, true); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + OutputLandscape->SetLayerLocked(EditLayerIndex, true); +#else + BakedLayer->bLocked = true; +#endif + } + //--------------------------------------------------------------------------------------------------------------------------- + // Make sure baked layer is visible. + //--------------------------------------------------------------------------------------------------------------------------- + if (EditLayerIndex != INDEX_NONE) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + OutputLandscape->GetEditLayer(EditLayerIndex)->SetVisible(true, true); +#else + OutputLandscape->SetLayerVisibility(EditLayerIndex, true); +#endif + } + + return true; +} + +bool +FHoudiniLandscapeBake::BakeLandscape( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniBakeSettings& BakeSettings, + const FDirectoryPath& BakePath, + TMap & ClearedLandscapeLayers, + FHoudiniBakedObjectData& BakedObjectData) +{ + // Check that index is not negative + if (InOutputIndex < 0) + return false; + + if (!InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + + UHoudiniOutput* Output = InAllOutputs[InOutputIndex]; + if (!IsValid(Output)) + return false; + + TMap& OutputObjects = Output->GetOutputObjects(); + TArray LandscapeWorldsToUpdate; + + const EPackageReplaceMode AssetPackageReplaceMode = BakeSettings.bReplaceAssets ? + EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; + + TArray CookedLayersToDelete; + + for (auto& Elem : OutputObjects) + { + const FHoudiniOutputObjectIdentifier& ObjectIdentifier = Elem.Key; + FHoudiniOutputObject& OutputObject = Elem.Value; + bool bHasPreviousBakeData = false; + + // Populate the package params for baking this output object. + if (!IsValid(OutputObject.OutputObject)) + continue; + + if (OutputObject.OutputObject->IsA()) + { + HOUDINI_LOG_ERROR(TEXT("Old Landscape Data found, rebuild the HDA Actor")); + continue; + } + + FHoudiniPackageParams PackageParams; + FHoudiniEngineBakeUtils::ResolvePackageParams( + InCookable, + Output, + Elem.Key, + Elem.Value, + bHasPreviousBakeData, + FString(""), + BakePath, + BakeSettings, + PackageParams, + BakedObjectData); + + UHoudiniLandscapeTargetLayerOutput* LayerOutput = Cast(OutputObject.OutputObject); + HOUDINI_CHECK_RETURN(IsValid(LayerOutput), false); + + if (!IsValid(LayerOutput->Landscape)) + { + HOUDINI_LOG_WARNING(TEXT("Cooked Landscape was not found, so nothing will be baked")); + continue; + } + + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(InOutputIndex, ObjectIdentifier, bHasPreviousBakeData); + + FHoudiniClearedEditLayers & ClearedLayers = ClearedLandscapeLayers.FindOrAdd(LayerOutput->Landscape); + FHoudiniLandscapeBake::BakeLandscapeLayer(PackageParams, *LayerOutput, BakeSettings, BakedObjectData, ClearedLayers); + + CookedLayersToDelete.Add(LayerOutput); + + InBakeState.SetNewBakedOutputObject(InOutputIndex, ObjectIdentifier, BakedOutputObject); + } + + // Once layers are baked, delete the cooked layers if they existed. + for(UHoudiniLandscapeTargetLayerOutput * LayerOutput : CookedLayersToDelete) + { + if (LayerOutput->bCookedLayerRequiresBaking) + { + FHoudiniLandscapeRuntimeUtils::DeleteEditLayer(LayerOutput->Landscape, FName(LayerOutput->CookedEditLayer)); + } + } + + // Once each layer has been modified see if they need to be locked. + for (auto& Elem : OutputObjects) + { + FHoudiniOutputObject& OutputObject = Elem.Value; + UHoudiniLandscapeTargetLayerOutput* LayerOutput = Cast(OutputObject.OutputObject); + if (IsValid(LayerOutput)) + FHoudiniLandscapeUtils::ApplyLocks(LayerOutput); + + } + + if (BakedObjectData.PackagesToSave.Num() > 0) + { + FEditorFileUtils::PromptForCheckoutAndSave(BakedObjectData.PackagesToSave, true, false); + } + + if (BakedObjectData.PackagesToSave.Num() > 0) + { + // These packages were either created during the Bake process or they weren't + // loaded in the first place so be sure to unload them again to preserve their "state". + + TArray PackagesToUnload; + for (UPackage* Package : BakedObjectData.PackagesToSave) + { + if (!Package->IsDirty()) + PackagesToUnload.Add(Package); + } + UPackageTools::UnloadPackages(PackagesToUnload); + } + +#if WITH_EDITOR + FEditorDelegates::RefreshLevelBrowser.Broadcast(); + FEditorDelegates::RefreshAllBrowsers.Broadcast(); +#endif + + return true; +} + + +TArray +FHoudiniLandscapeBake::MoveCookedToBakedLandscapes( + const UHoudiniCookable* InCookable, + const FName& InFallbackWorldOutlinerFolder, + const TArray& InOutputs, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniBakeSettings& BakeSettings, + const FDirectoryPath& BakeFolder, + FHoudiniBakedObjectData& BakedObjectData) +{ + TSet ProcessedLandscapes; + TArray Results; + + for(int OutputIndex = 0; OutputIndex < InOutputs.Num(); OutputIndex++) + { + UHoudiniOutput* HoudiniOutput = InOutputs[OutputIndex]; + + // Get a valid output layer from the output. + if (!IsValid(HoudiniOutput) || HoudiniOutput->GetType() != EHoudiniOutputType::Landscape) + continue; + + TMap& OutputObjects = HoudiniOutput->GetOutputObjects(); + + for (auto& Elem : OutputObjects) + { + UHoudiniLandscapeTargetLayerOutput* LayerOutput = Cast(Elem.Value.OutputObject); + if (!LayerOutput) + continue; + + if (!IsValid(LayerOutput->Landscape)) + continue; + + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(OutputIndex, Elem.Key, bHasPreviousBakeData); + + FDirectoryPath BakePath; + BakePath.Path = LayerOutput->BakeFolder; + + FHoudiniPackageParams PackageParams; + FHoudiniAttributeResolver Resolver; + FHoudiniEngineBakeUtils::ResolvePackageParamsWithResolver( + InCookable, + HoudiniOutput, + Elem.Key, + Elem.Value, + bHasPreviousBakeData, + FString(""), + BakePath, + BakeSettings, + PackageParams, + Resolver, + BakedObjectData); + + // Bake Material instance. + BakeMaterials(*LayerOutput, PackageParams, BakedObjectData); + + //--------------------------------------------------------------------------------------------------------------------------- + // Make sure we only process each parent landscape once. + //--------------------------------------------------------------------------------------------------------------------------- + + if (ProcessedLandscapes.Contains(LayerOutput->Landscape)) + continue; + ProcessedLandscapes.Add(LayerOutput->Landscape); + + //--------------------------------------------------------------------------------------------------------------------------- + // Bake all the LayerInfoObjects, if they were not specified by the user, and patch up the result inside the landscape. + //--------------------------------------------------------------------------------------------------------------------------- + + ULandscapeInfo* LandscapeInfo = LayerOutput->Landscape->GetLandscapeInfo(); + LandscapeInfo->Modify(); + + for(int Index = 0; Index < LayerOutput->LayerInfoObjects.Num(); Index++) + { + ULandscapeLayerInfoObject* CookedLayerInfoObject = LayerOutput->LayerInfoObjects[Index]; + + FString TempFolder = InCookable->GetTemporaryCookFolderOrDefault(); + + if (CookedLayerInfoObject->GetPathName().StartsWith(TempFolder)) + { + ULandscapeLayerInfoObject* BakedLayerInfo = CreateBakedLandscapeLayerInfoObject( + PackageParams, + LayerOutput->Landscape, + CookedLayerInfoObject, + BakedObjectData); + + LandscapeInfo->ReplaceLayer(CookedLayerInfoObject, BakedLayerInfo); + + FLandscapeInfoLayerSettings& LayerSettings = LandscapeInfo->Layers[Index]; + LayerSettings.LayerInfoObj = BakedLayerInfo; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + BakedOutputObject.LandscapeLayers.Emplace(BakedLayerInfo->GetLayerName(), FSoftObjectPath(BakedLayerInfo).ToString()); +#else + BakedOutputObject.LandscapeLayers.Emplace(BakedLayerInfo->LayerName, FSoftObjectPath(BakedLayerInfo).ToString()); +#endif + } + } + + LandscapeInfo->LandscapeActor->ForceLayersFullUpdate(); + + //--------------------------------------------------------------------------------------------------------------------------- + // Rename the landscape. Making a copy of the landscape and copying data would be slow, especially with world partition. + //--------------------------------------------------------------------------------------------------------------------------- + + if (LayerOutput->bCreatedLandscape) + { + // Rename the actor, make sure we only do this once. + AActor* FoundActor = nullptr; + ALandscape* ExistingLandscape = FHoudiniEngineUtils::FindOrRenameInvalidActor( + LayerOutput->Landscape->GetWorld(), LayerOutput->BakedLandscapeName, FoundActor); + + if (ExistingLandscape && PackageParams.ReplaceMode == EPackageReplaceMode::ReplaceExistingAssets) + { + // Even though we found an existing landscape with the desired type, we're just going to destroy/replace + // it for now. + FHoudiniEngineUtils::RenameToUniqueActor(ExistingLandscape, LayerOutput->BakedLandscapeName + "_0"); + FHoudiniLandscapeRuntimeUtils::DestroyLandscape(ExistingLandscape); + } + + BakedObjectData.BakeStats.NotifyPackageUpdated(1); + + FHoudiniEngineUtils::SafeRenameActor(LayerOutput->Landscape, LayerOutput->BakedLandscapeName); + + FName WorldOutlinerFolder = + FHoudiniEngineBakeUtils::GetOutlinerFolderPath(Resolver, InFallbackWorldOutlinerFolder); + + if (!WorldOutlinerFolder.ToString().IsEmpty()) + LayerOutput->Landscape->SetFolderPath(FName(WorldOutlinerFolder)); + + FHoudiniEngineBakedActor BakeActor( + LayerOutput->Landscape, + FName(LayerOutput->Landscape->GetActorLabel()), + FName(WorldOutlinerFolder), + OutputIndex, + Elem.Key, + nullptr, + nullptr, + nullptr, + PackageParams.BakeFolder, + PackageParams); + + Results.Add(BakeActor); + BakedOutputObject.Landscape = *BakeActor.Actor->GetPathName(); + + } + else + { + BakedOutputObject.Landscape = LayerOutput->Landscape->GetPathName(); + + if(LayerOutput->bLayerWasCreated) + BakedOutputObject.CreatedLandscapeLayers.Add(LayerOutput->BakedEditLayer); + + } + + // Set the updated baked output object in the state + InBakeState.SetNewBakedOutputObject(OutputIndex, Elem.Key, BakedOutputObject); + } + } + + // Clear old cooked data. + for (int OutputIndex = 0; OutputIndex < InOutputs.Num(); OutputIndex++) + { + UHoudiniOutput* HoudiniOutput = InOutputs[OutputIndex]; + if (!IsValid(HoudiniOutput) || HoudiniOutput->GetType() != EHoudiniOutputType::Landscape) + continue; + + TMap& OutputObjects = HoudiniOutput->GetOutputObjects(); + + for (auto& Elem : OutputObjects) + { + UHoudiniLandscapeTargetLayerOutput* LayerOutput = Cast(Elem.Value.OutputObject); + if (!LayerOutput) + continue; + + LayerOutput->Landscape = nullptr; + } + } + return Results; +} + + + +ULandscapeLayerInfoObject* +FHoudiniLandscapeBake::CreateBakedLandscapeLayerInfoObject( + const FHoudiniPackageParams& PackageParams, + ALandscape* Landscape, + ULandscapeLayerInfoObject* LandscapeLayerInfoObject, + FHoudiniBakedObjectData& BakedObjectData) +{ + ULandscapeLayerInfoObject * BakedObject = BakeGeneric( + LandscapeLayerInfoObject, + PackageParams, + LandscapeLayerInfoObject->GetName(), + BakedObjectData); + + BakedObject->MarkPackageDirty(); + + return BakedObject; + +} + + +void FHoudiniLandscapeBake::BakeMaterials( + const UHoudiniLandscapeTargetLayerOutput& Layer, + const FHoudiniPackageParams& PackageParams, + FHoudiniBakedObjectData& BakedObjectData) +{ + UMaterialInterface * MaterialInstance = Layer.MaterialInstance; + if (!IsValid(MaterialInstance)) + return; + + auto * BakedMaterialInstance = BakeGeneric( + MaterialInstance, + PackageParams, + MaterialInstance->GetName(), + BakedObjectData); + + //------------------------------------------------------------------------------------------------------------------------------ + // Replace the material instance. If this is a parent landscape actor apply material change to every proxy, because + // unreal will not do this for us. + //------------------------------------------------------------------------------------------------------------------------------ + + Layer.LandscapeProxy->LandscapeMaterial = BakedMaterialInstance; + + if (Layer.LandscapeProxy->IsA()) + { + ULandscapeInfo* Info = Layer.Landscape->GetLandscapeInfo(); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + const TArray>& Proxies = Info->GetSortedStreamingProxies(); +#else + TArray>& Proxies = Info->StreamingProxies; +#endif + for (auto ProxyPtr : Proxies) + { + ALandscapeStreamingProxy* Proxy = ProxyPtr.Get(); +#else + TArray& Proxies = Info->Proxies; + for (ALandscapeStreamingProxy* Proxy : Proxies) + { +#endif + if (!IsValid(Proxy)) + continue; + + Proxy->LandscapeMaterial = BakedMaterialInstance; + } + } + + Layer.LandscapeProxy->GetLandscapeActor()->ForceUpdateLayersContent(); +} + +template +UObjectType* FHoudiniLandscapeBake::BakeGeneric( + UObjectType* CookedObject, + const FHoudiniPackageParams& PackageParams, + const FString& ObjectName, + FHoudiniBakedObjectData& BakedObjectData) +{ + FHoudiniPackageParams LayerPackageParams = PackageParams; + LayerPackageParams.ObjectName = ObjectName; + FString CreatedPackageName; + UPackage* Package = LayerPackageParams.CreatePackageForObject(CreatedPackageName); + if (!Package->IsFullyLoaded()) + { + FlushAsyncLoading(); + if (!Package->GetOuter()) + { + Package->FullyLoad(); + } + else + { + Package->GetOutermost()->FullyLoad(); + } + } + + BakedObjectData.PackagesToSave.Add(Package); + + //------------------------------------------------------------------------------------------------------------------------------ + // Remove existing layer object + //------------------------------------------------------------------------------------------------------------------------------ + + UObjectType* ExistingObject = FindObject(Package, *ObjectName); + if (IsValid(ExistingObject)) + { + BakedObjectData.BakeStats.NotifyObjectsReplaced(UObjectType::StaticClass()->GetName(), 1); + BakedObjectData.BakeStats.NotifyPackageUpdated(1); + } + else + { + BakedObjectData.BakeStats.NotifyObjectsCreated(UObjectType::StaticClass()->GetName(), 1); + BakedObjectData.BakeStats.NotifyPackageCreated(1); + } + + //------------------------------------------------------------------------------------------------------------------------------ + // Replace the layer. + //------------------------------------------------------------------------------------------------------------------------------ + + UObjectType* DuplicatedObject = DuplicateObject(CookedObject, Package, *ObjectName); + Package->MarkPackageDirty(); + + return DuplicatedObject; + +} + + +bool +FHoudiniLandscapeBake::BakeLandscapeSplinesLayer( + FHoudiniPackageParams& PackageParams, + UHoudiniLandscapeSplineTargetLayerOutput& LayerOutput, + FHoudiniClearedEditLayers& ClearedLayers, + TMap, FHoudiniLandscapeSplineApplyLayerData>& SegmentsToApplyToLayers) +{ + ALandscape* const OutputLandscape = LayerOutput.Landscape; + ULandscapeInfo* const TargetLandscapeInfo = OutputLandscape->GetLandscapeInfo(); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 7 + if (!OutputLandscape->bCanHaveLayersContent) + { + HOUDINI_LOG_MESSAGE(TEXT("Landscape {0} has no edit layers, so baking does nothing."), *OutputLandscape->GetActorLabel()); + return true; + } +#endif + const FName BakedEditLayer = *LayerOutput.BakedEditLayer; + + // If the landscape has a reserved splines layer, then we don't create any named temp/bake layers on the landscape for splines + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + if (OutputLandscape->FindEditLayerOfType(ULandscapeEditLayerSplines::StaticClass())) +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + if (OutputLandscape->FindLayerOfType(ULandscapeEditLayerSplines::StaticClass())) +#else + if (OutputLandscape->GetLandscapeSplinesReservedLayer()) +#endif + { + FHoudiniLandscapeSplineApplyLayerData& LayerData = SegmentsToApplyToLayers.FindOrAdd({ OutputLandscape, BakedEditLayer }); + LayerData.bIsReservedSplineLayer = true; + LayerData.Landscape = OutputLandscape; + LayerData.EditLayerName = BakedEditLayer; + return true; + } + + //--------------------------------------------------------------------------------------------------------------------------- + // For landscape layers baking is the act of copying cooked data to a baked layer. We do not need to do that if we already + // wrote directly to the final layer. + //--------------------------------------------------------------------------------------------------------------------------- + + if (!LayerOutput.bCookedLayerRequiresBaking) + return true; + + // Ensure that the baked layer exists +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + const FLandscapeLayer* const BakedLayer = FHoudiniLandscapeUtils::GetOrCreateEditLayer(OutputLandscape, BakedEditLayer); +#else + FLandscapeLayer* const BakedLayer = FHoudiniLandscapeUtils::GetOrCreateEditLayer(OutputLandscape, BakedEditLayer); +#endif + + //--------------------------------------------------------------------------------------------------------------------------- + // Clear the layer, but only once per bake. + //--------------------------------------------------------------------------------------------------------------------------- + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + if (LayerOutput.bClearLayer + && !ClearedLayers.Contains(LayerOutput.BakedEditLayer, LayerOutput.TargetLayer)) +#else + if (OutputLandscape->HasLayersContent() + && LayerOutput.bClearLayer && !ClearedLayers.Contains(LayerOutput.BakedEditLayer, LayerOutput.TargetLayer)) +#endif + { + ClearedLayers.Add(LayerOutput.BakedEditLayer, LayerOutput.TargetLayer); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + OutputLandscape->ClearEditLayer(BakedLayer->EditLayer->GetGuid(), nullptr, ELandscapeToolTargetTypeFlags::Heightmap); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + OutputLandscape->ClearLayer(BakedLayer->EditLayer->GetGuid(), nullptr, ELandscapeClearMode::Clear_Heightmap); +#else + OutputLandscape->ClearLayer(BakedLayer->Guid, nullptr, ELandscapeClearMode::Clear_Heightmap); +#endif + } + + //--------------------------------------------------------------------------------------------------------------------------- + // Record the segments to apply to the baked layer + //--------------------------------------------------------------------------------------------------------------------------- + FHoudiniLandscapeSplineApplyLayerData& LayerData = SegmentsToApplyToLayers.FindOrAdd({ OutputLandscape, BakedEditLayer }); + LayerData.bIsReservedSplineLayer = false; + LayerData.Landscape = OutputLandscape; + LayerData.EditLayerName = BakedEditLayer; + LayerData.SegmentsToApply.Append(LayerOutput.Segments); + + // Delete the temp/cooked layer + FHoudiniLandscapeRuntimeUtils::DeleteEditLayer(OutputLandscape, FName(LayerOutput.CookedEditLayer)); + + //--------------------------------------------------------------------------------------------------------------------------- + // Make sure baked layer is visible. + //--------------------------------------------------------------------------------------------------------------------------- + int EditLayerIndex = OutputLandscape->GetLayerIndex(BakedEditLayer); + if (EditLayerIndex != INDEX_NONE) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + OutputLandscape->GetEditLayer(EditLayerIndex)->SetVisible(true, true); +#else + OutputLandscape->SetLayerVisibility(EditLayerIndex, true); +#endif + } + + return true; +} + +bool FHoudiniLandscapeBake::BakeLandscapeSplines( + const UHoudiniCookable* InCookable, + const int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniBakeSettings& BakeSettings, + const FDirectoryPath& BakePath, + TMap& ClearedLandscapeEditLayers, + FHoudiniBakedObjectData& BakedObjectData) +{ + // Check that index is not negative + if (InOutputIndex < 0) + return false; + + if (!InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + + UHoudiniOutput* const Output = InAllOutputs[InOutputIndex]; + if (!IsValid(Output) || Output->GetType() != EHoudiniOutputType::LandscapeSpline) + return false; + + TMap& OutputObjects = Output->GetOutputObjects(); + TArray PackagesToSave; + TArray LandscapeWorldsToUpdate; + + const EPackageReplaceMode AssetPackageReplaceMode = BakeSettings.bReplaceAssets ? + EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; + + TArray OutputObjectsBaked; + + TMap, FHoudiniLandscapeSplineApplyLayerData> SegmentsToApplyToLayers; + for (auto& Entry : OutputObjects) + { + const FHoudiniOutputObjectIdentifier& ObjectIdentifier = Entry.Key; + FHoudiniOutputObject& OutputObject = Entry.Value; + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(InOutputIndex, ObjectIdentifier, bHasPreviousBakeData); + + // Populate the package params for baking this output object. + if (!IsValid(OutputObject.OutputObject)) + continue; + + UHoudiniLandscapeSplinesOutput* const SplinesOutputObject = Cast(OutputObject.OutputObject); + if (!SplinesOutputObject) + continue; + + OutputObjectsBaked.Add(Entry.Key); + + FHoudiniPackageParams PackageParams; + FHoudiniEngineBakeUtils::ResolvePackageParams( + InCookable, + Output, + ObjectIdentifier, + OutputObject, + bHasPreviousBakeData, + FString(""), + BakePath, + BakeSettings, + PackageParams, + BakedObjectData); + + const FString DesiredBakeName = PackageParams.GetPackageName(); + ALandscape* const Landscape = SplinesOutputObject->GetLandscape(); + + // Bake the landscape spline actors: for this, in replace mode, delete the previous bake actor if any, and then rename + // the temp landscape spline actor to the bake name. + ALandscapeSplineActor* const ActorToBake = SplinesOutputObject->GetLandscapeSplineActor(); + if (IsValid(ActorToBake)) + { + // For a replace, delete previous baked actor for this output identifier, if any. Also check that + // it belongs to the same landscape. + if (BakeSettings.bReplaceActors && !BakedOutputObject.Actor.IsEmpty() + && BakedOutputObject.Landscape == FSoftObjectPath(Landscape).ToString()) + { + ULandscapeInfo* const LandscapeInfo = Landscape->GetLandscapeInfo(); + if (IsValid(LandscapeInfo)) + { + // Only remove the previous actor if it has the LandscapeInfo object + ALandscapeSplineActor* const PreviousActor = Cast(BakedOutputObject.GetActorIfValid()); + if (IsValid(PreviousActor) && PreviousActor->GetLandscapeInfo() == LandscapeInfo) + { + LandscapeInfo->UnregisterSplineActor(PreviousActor); + PreviousActor->Destroy(); + } + } + } + + // Rename to bake name + FHoudiniEngineBakeUtils::RenameAndRelabelActor(ActorToBake, DesiredBakeName); + + // Record in baked object entry + BakedOutputObject.Actor = FSoftObjectPath(ActorToBake).ToString(); + BakedOutputObject.BakedComponent = FSoftObjectPath(ActorToBake->GetSplinesComponent()).ToString(); + } + else + { + // Non-WP case, there are no landscape spline actors, so we just track the landscape's LandscapeSplinesComponent + BakedOutputObject.Actor.Empty(); + ULandscapeSplinesComponent* const SplinesComponent = SplinesOutputObject->GetLandscapeSplinesComponent(); + if (IsValid(SplinesComponent)) + BakedOutputObject.BakedComponent = FSoftObjectPath(SplinesComponent).ToString(); + else + BakedOutputObject.BakedComponent.Empty(); + } + + // Updated baked object entry + BakedOutputObject.ActorBakeName = *DesiredBakeName; + BakedOutputObject.BakedObject.Empty(); + BakedOutputObject.Landscape = FSoftObjectPath(Landscape).ToString(); + + // Delete temp edit layers, create baked layers and collect all segments per-landscape-layer. + for (const auto& LayerEntry : SplinesOutputObject->GetLayerOutputs()) + { + UHoudiniLandscapeSplineTargetLayerOutput* const LayerOutput = LayerEntry.Value; + if (!IsValid(LayerOutput)) + continue; + + if (!IsValid(LayerOutput->Landscape)) + { + HOUDINI_LOG_WARNING(TEXT("Cooked Landscape was not found, so nothing will be baked")); + continue; + } + + FHoudiniClearedEditLayers& ClearedLandscapeLayers = ClearedLandscapeEditLayers.FindOrAdd(LayerOutput->Landscape); + BakeLandscapeSplinesLayer(PackageParams, *LayerOutput, ClearedLandscapeLayers, SegmentsToApplyToLayers); + } + + // Save baked object entry in bake state + InBakeState.SetNewBakedOutputObject(InOutputIndex, ObjectIdentifier, BakedOutputObject); + } + + // Apply segments to baked/reserved layers + FHoudiniLandscapeUtils::ApplySegmentsToLandscapeEditLayers(SegmentsToApplyToLayers); + + // Remove all output objects: since we don't duplicate anything the temp actors/segments + // essentially become the baked ones. We have also already removed all temp layers. + for (const FHoudiniOutputObjectIdentifier& Identifier : OutputObjectsBaked) + { + OutputObjects.Remove(Identifier); + } + + if (PackagesToSave.Num() > 0) + { + FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, true, false); + } + + if (PackagesToSave.Num() > 0) + { + // These packages were either created during the Bake process or they weren't + // loaded in the first place so be sure to unload them again to preserve their "state". + + TArray PackagesToUnload; + for (UPackage* Package : PackagesToSave) + { + if (!Package->IsDirty()) + PackagesToUnload.Add(Package); + } + UPackageTools::UnloadPackages(PackagesToUnload); + } + +#if WITH_EDITOR + FEditorDelegates::RefreshLevelBrowser.Broadcast(); + FEditorDelegates::RefreshAllBrowsers.Broadcast(); +#endif + + return true; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniBakeLandscape.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#include "HoudiniEngineBakeUtils.h" + +struct FHoudiniBakeSettings; +struct FHoudiniEngineBakedActor; +struct FHoudiniOutputObject; +struct FHoudiniBakedOutputObject; +struct FHoudiniPackageParams; +struct FHoudiniAttributeResolver; +struct FHoudiniLandscapeSplineApplyLayerData; +class FHoudiniEngineBakeState; +class UWorld; +class UPackage; +class UHoudiniLandscapeTargetLayerOutput; +class UHoudiniLandscapeSplineTargetLayerOutput; + +struct HOUDINIENGINE_API FHoudiniLandscapeBake +{ + static bool BakeLandscape( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniBakeSettings& BakeSettings, + const FDirectoryPath& BakePath, + TMap& ClearedLayers, + FHoudiniBakedObjectData& BakedObjectData); + + static bool BakeLandscapeLayer( + FHoudiniPackageParams& PackageParams, + UHoudiniLandscapeTargetLayerOutput& LayerOutput, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData, + FHoudiniClearedEditLayers& ClearedLayers); + + static TArray MoveCookedToBakedLandscapes( + const UHoudiniCookable* InCookable, + const FName & OutlinerFolder, + const TArray& InOutputs, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniBakeSettings& BakeSettings, + const FDirectoryPath& BakeFolder, + FHoudiniBakedObjectData& BakedObjectData); + + static ULandscapeLayerInfoObject* CreateBakedLandscapeLayerInfoObject( + const FHoudiniPackageParams& PackageParams, + ALandscape* Landscape, + ULandscapeLayerInfoObject* LandscapeLayerInfoObject, + FHoudiniBakedObjectData& BakedObjectData); + + static void BakeMaterials( + const UHoudiniLandscapeTargetLayerOutput & Layer, + const FHoudiniPackageParams& PackageParams, + FHoudiniBakedObjectData& BakedObjectData); + + static void MoveToBakeFolder(); + + template + static UObjectType* BakeGeneric( + UObjectType* CookedObject, + const FHoudiniPackageParams& PackageParams, + const FString& ObjectName, + FHoudiniBakedObjectData& BakedObjectData); + + static bool BakeLandscapeSplinesLayer( + FHoudiniPackageParams& PackageParams, + UHoudiniLandscapeSplineTargetLayerOutput& LayerOutput, + FHoudiniClearedEditLayers& ClearedLayers, + TMap, FHoudiniLandscapeSplineApplyLayerData>& SegmentsToApplyToLayers); + + static bool BakeLandscapeSplines( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniBakeSettings& BakeSettings, + const FDirectoryPath& BakePath, + TMap& ClearedLandscapeEditLayers, + FHoudiniBakedObjectData& BakedObjectData); +}; + + +``` + +`Source/HoudiniEngine/Private/HoudiniBakeLevelInstanceUtils.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniBakeLevelInstanceUtils.h" + +#include "HoudiniCookable.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniOutput.h" + +#include "Editor.h" +#include "LevelInstance/LevelInstanceActor.h" +#include "LevelInstance/LevelInstanceTypes.h" +#include "LevelInstance/LevelInstanceSubsystem.h" + + +ILevelInstanceInterface * FHoudiniBakeLevelInstanceUtils::CreateLevelInstance( + const FHoudiniLevelInstanceParams& Params, + TArray & Actors, + const FString& BakeFolder, + FHoudiniBakedObjectData& BakedObjectData) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + ULevelInstanceSubsystem* LevelInstanceSubsystem = + GEditor->GetEditorWorldContext().World()->GetSubsystem(); + + // Convert out input params to Unreal's structure. + FNewLevelInstanceParams CreationParams; + CreationParams.Type = ELevelInstanceCreationType::PackedLevelActor; //Params.Type; + CreationParams.LevelPackageName = BakeFolder + TEXT("/") + Params.OutputName; + CreationParams.PivotType = ELevelInstancePivotType::CenterMinZ; + CreationParams.PivotActor = nullptr; + CreationParams.bAlwaysShowDialog = false; + CreationParams.TemplateWorld = nullptr; + CreationParams.LevelInstanceClass = ALevelInstance::StaticClass(); + + FString LongPackageName = FPackageName::FilenameToLongPackageName(CreationParams.LevelPackageName); + + // Make sure the proposed package name is valid, if not Unreal will crash. + FString PackageName; + bool bValidPackage = FPackageName::TryConvertLongPackageNameToFilename(CreationParams.LevelPackageName, PackageName, FPackageName::GetMapPackageExtension()); + if (!bValidPackage) + { + HOUDINI_LOG_ERROR(TEXT("Cannot create Level Instance due to invalid packaged name %s"), *CreationParams.LevelPackageName); + return nullptr; + } + + ILevelInstanceInterface* Result = LevelInstanceSubsystem->CreateLevelInstanceFrom(Actors, CreationParams); + + return Result; +#else + return nullptr; +#endif +} + +const UHoudiniOutput* +FHoudiniBakeLevelInstanceUtils::GetHoudiniObject( + const FHoudiniOutputObjectIdentifier Id, + const TArray& CookedOutputs) +{ + for (int Index = 0; Index < CookedOutputs.Num(); Index++) + { + if (CookedOutputs[Index]->GetOutputObjects().Contains(Id)) + return CookedOutputs[Index]; + } + return nullptr; +} + +bool +FHoudiniBakeLevelInstanceUtils::CreateLevelInstances( + UHoudiniCookable* HC, + const TArray& BakedActors, + FHoudiniBakedObjectData& BakedObjectData) +{ + if (!HC) + return false; + + TArray CookedOutputs; + HC->GetOutputs(CookedOutputs); + + FString BakeFolder = HC->GetBakeFolderOrDefault(); + + //---------------------------------------------------------------------------------------------------------------------------------- + // Go through all baked actors and find any which have valid information about level instancing. Build a list of level + // actors which share the same level instance information. + //---------------------------------------------------------------------------------------------------------------------------------- + + TMap> ActorsPerInstance; + TMap ActorToBakedActor; + + for (int Index = 0; Index < BakedActors.Num(); Index++) + { + const FHoudiniEngineBakedActor & BakedActor = BakedActors[Index]; + + const UHoudiniOutput* CookedOutput = GetHoudiniObject(BakedActor.OutputObjectIdentifier, CookedOutputs); + HOUDINI_CHECK_RETURN(CookedOutput, false); + + const FHoudiniOutputObject* CookedObject = CookedOutput->GetOutputObjects().Find(BakedActor.OutputObjectIdentifier); + + AActor* Actor = BakedActor.Actor; + if (!Actor || CookedObject->LevelInstanceParams.OutputName.IsEmpty()) + continue; + + const FHoudiniLevelInstanceParams LevelInstanceParams = CookedObject->LevelInstanceParams; + + auto& Array = ActorsPerInstance.FindOrAdd(LevelInstanceParams); + Array.Add(Actor); + ActorToBakedActor.Add(Actor, Index); + } + + //---------------------------------------------------------------------------------------------------------------------------------- + // Create each level instance + //---------------------------------------------------------------------------------------------------------------------------------- + + for(auto & It : ActorsPerInstance) + { + CreateLevelInstance(It.Key, It.Value, BakeFolder, BakedObjectData); + } + return true; + +} + + +``` + +`Source/HoudiniEngine/Private/HoudiniBakeLevelInstanceUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "CoreMinimal.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniEngineOutputStats.h" +#include "HoudiniOutput.h" + +class UHoudiniOutput; +class UHoudiniCookable; +class ILevelInstanceInterface; +class AActor; +struct FHoudiniBakedOutput; +struct FHoudiniLevelInstanceParams; +struct FHoudiniEngineBakedActor; + +class FHoudiniBakeLevelInstanceUtils +{ +public: + + static bool CreateLevelInstances( + UHoudiniCookable* HC, + const TArray & BakedActors, + FHoudiniBakedObjectData& BakedObjectData); + +protected: + static ILevelInstanceInterface* CreateLevelInstance( + const FHoudiniLevelInstanceParams& Params, + TArray& Actors, + const FString & BakeFolder, + FHoudiniBakedObjectData& BakedObjectData); + + static const UHoudiniOutput* GetHoudiniObject(const FHoudiniOutputObjectIdentifier Id, const TArray& CookedOutputs); + +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniDataLayerUtils.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include "HoudiniDataLayerUtils.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "LevelEditorMenuContext.h" +//#include "NaniteSceneProxy.h" +#include "HoudiniEngineAttributes.h" +#include "HAPI/HAPI_Common.h" +#include "WorldPartition/DataLayer/WorldDataLayers.h" +#include "HoudiniPackageParams.h" +#include "DataLayer/DataLayerEditorSubsystem.h" +#include "Landscape.h" +#include "LandscapeStreamingProxy.h" + +#if HOUDINI_ENABLE_DATA_LAYERS +#include "WorldPartition/DataLayer/DataLayerAsset.h" +#endif + +#if WITH_EDITOR +//#include "DataLayer/DataLayerEditorSubsystem.h" +#endif + +void FHoudiniDataLayerUtils::ApplyDataLayersToActor(AActor* Actor, TArray & DataLayers, TMap& DataLayerLookup) +{ +#if HOUDINI_ENABLE_DATA_LAYERS + + for (auto& Layer : DataLayers) + { + UDataLayerInstance** DataLayerInstance = DataLayerLookup.Find(Layer.Name); + if(DataLayerInstance) + (*DataLayerInstance)->AddActor(Actor); + } + + if (ALandscape* Landscape = Cast(Actor)) + { +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 7 + TArray> Proxies = Landscape->GetLandscapeInfo()->GetSortedStreamingProxies(); +#elif ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1 + TArray> Proxies = Landscape->GetLandscapeInfo()->StreamingProxies; +#else + TArray> Proxies = Proxy->GetLandscapeInfo()->Proxies; +#endif + + for(TWeakObjectPtr Child : Proxies) + { + ALandscapeStreamingProxy* LandscapeProxy = Child.Get(); + if (LandscapeProxy) + { + for(auto& Layer : DataLayers) + { + UDataLayerInstance** DataLayerInstance = DataLayerLookup.Find(Layer.Name); + if(DataLayerInstance) + (*DataLayerInstance)->AddActor(LandscapeProxy); + } + } + } + } +#endif +} + +#if HOUDINI_ENABLE_DATA_LAYERS +UDataLayerInstance * FHoudiniDataLayerUtils::FindOrCreateDataLayerInstance( + const FHoudiniPackageParams& Params, + AWorldDataLayers* WorldDataLayers, + const FHoudiniDataLayer& Layer) +{ + // Find the Data Layer Instance for this actor. + UDataLayerInstance* TargetDataLayerInstance = nullptr; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + WorldDataLayers->ForEachDataLayerInstance([&](UDataLayerInstance* DataLayer) +#else + WorldDataLayers->ForEachDataLayer([&](UDataLayerInstance* DataLayer) +#endif + { + FString DataLayerName = DataLayer->GetDataLayerShortName(); + if(DataLayerName != Layer.Name) + return true; + + TargetDataLayerInstance = DataLayer; + return false; + }); + + if(!TargetDataLayerInstance) + { + if(Layer.bCreateIfNeeded) + { + UDataLayerAsset* DataLayerAsset = CreateDataLayerAsset(Params, Layer.Name); + + FDataLayerCreationParameters CreationParams; + CreationParams.DataLayerAsset = DataLayerAsset; + CreationParams.WorldDataLayers = WorldDataLayers; + TargetDataLayerInstance = UDataLayerEditorSubsystem::Get()->CreateDataLayerInstance(CreationParams); + + if(!TargetDataLayerInstance) + { + HOUDINI_LOG_ERROR(TEXT("Could not create Data Layer: %s"), *Layer.Name); + return nullptr; + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("Could not find Data Layer: %s. Set " HAPI_UNREAL_ATTRIB_CREATE_DATA_LAYERS + " to create a default data layer asset."), *Layer.Name); + return nullptr; + } + } + return TargetDataLayerInstance; +} +#endif + +#if HOUDINI_ENABLE_DATA_LAYERS +void FHoudiniDataLayerUtils::AddActorToLayer( + const FHoudiniPackageParams& Params, + AWorldDataLayers* WorldDataLayers, + AActor* Actor, + const FHoudiniDataLayer& Layer) +{ + UDataLayerInstance* DataLayerInstance = FindOrCreateDataLayerInstance(Params, WorldDataLayers, Layer); + if (DataLayerInstance) + DataLayerInstance->AddActor(Actor); +} +#endif + +TArray +FHoudiniDataLayerUtils::GetDataLayers(HAPI_NodeId NodeId, HAPI_PartId PartId, HAPI_GroupType GroupType, int Index) +{ + TArray Results; +#if HOUDINI_ENABLE_DATA_LAYERS + // Get a list of all groups this part MAY be a member of. + TArray RawGroupNames; + bool bResult = FHoudiniEngineUtils::HapiGetGroupNames(NodeId, PartId, GroupType, false, RawGroupNames); + + // Check each group to see if we're a member. + FString NamePrefix = TEXT(HOUDINI_DATA_LAYER_PREFIX); + for(FString DataLayerName : RawGroupNames) + { + // Is a group that specifies an unreal data layer? + if(!DataLayerName.StartsWith(NamePrefix)) + continue; + + // Is a member of the group? + int32 GroupMembership = 0; + + FHoudiniEngineUtils::HapiGetGroupMembership(NodeId, PartId, GroupType, DataLayerName, GroupMembership, Index, 1); + + if(GroupMembership == 0) + continue; + + FHoudiniDataLayer Layer; + + // Get name, removing the prefix. + DataLayerName.RemoveFromStart(NamePrefix); + Layer.Name = DataLayerName; + + // Create flag that indicates if we should create missing data layers. + int32 CreateFlags = 0; + + FHoudiniHapiAccessor Accessor; + Accessor.Init(NodeId, PartId, HAPI_UNREAL_ATTRIB_CREATE_DATA_LAYERS); + bool bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, CreateFlags); + + Layer.bCreateIfNeeded = (CreateFlags == 1); + + Results.Add(Layer); + } +#endif + return Results; +} + +TArray +FHoudiniDataLayerUtils::GetDataLayers(HAPI_NodeId NodeId, HAPI_PartId PartId) +{ + TArray Results = GetDataLayers(NodeId, PartId, HAPI_GroupType::HAPI_GROUPTYPE_PRIM, 0); + if (Results.IsEmpty()) + Results = GetDataLayers(NodeId, PartId, HAPI_GroupType::HAPI_GROUPTYPE_POINT, 0); + return Results; +} + +#if HOUDINI_ENABLE_DATA_LAYERS +UDataLayerAsset* +FHoudiniDataLayerUtils::CreateDataLayerAsset(const FHoudiniPackageParams& Params, const FString & LayerName) +{ + // When creating a new data layer asset the name is taken from the asset package name, so we shouldn't + // append the HDA name of node/part labels. Also, we want the data layer to be available to other outputs + // in the same, or even other, HDA names, so keeping the name simple makes sense. + // + // Really we're just Package Params to get the back folder. + + FHoudiniPackageParams DataLayerParams = Params; + DataLayerParams.ObjectName = LayerName; + UDataLayerAsset* Result = DataLayerParams.CreateObjectAndPackage(); + return Result; + +} +#endif + + +TArray +FHoudiniDataLayerUtils::GetDataLayerInfoForActor(AActor* Actor) +{ + TArray Results; + +#if HOUDINI_ENABLE_DATA_LAYERS + TArray DataLayerInstances = Actor->GetDataLayerInstances(); + + for(const UDataLayerInstance * DataLayerInstance : DataLayerInstances) + { + FHoudiniUnrealDataLayerInfo Info = {}; + Info.Name = DataLayerInstance->GetDataLayerShortName(); + Results.Add(Info); + } +#endif + + return Results; + +} + +HAPI_NodeId +FHoudiniDataLayerUtils::AddGroupsFromDataLayers(AActor* Actor, HAPI_NodeId ParentNodeId, HAPI_NodeId InputNodeId) +{ + HAPI_NodeId VexNodeId; + + // Create a group node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CreateNode(FHoudiniEngine::Get().GetSession(), + ParentNodeId, + "attribwrangle", + "data_layers", + false, + &VexNodeId), + -1); + + // Hook the new node up to the input node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(FHoudiniEngine::Get().GetSession(), VexNodeId, 0, InputNodeId, 0), false); + + SetVexCode(VexNodeId, Actor); + + return VexNodeId; +} + +bool +FHoudiniDataLayerUtils::SetVexCode(HAPI_NodeId VexNodeId, AActor* Actor) +{ + auto DataLayers = FHoudiniDataLayerUtils::GetDataLayerInfoForActor(Actor); + + FString VexCode; + + for (auto& DataLayer : DataLayers) + { + FString PrefixedName = FString(HOUDINI_DATA_LAYER_PREFIX) + DataLayer.Name; + + const FString VexLine = FString::Format(TEXT("setprimgroup(0,\"{0}\", @primnum,1);\n"), { PrefixedName }); + VexCode += VexLine; + } + // Set the wrangle's class to prims + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(FHoudiniEngine::Get().GetSession(), VexNodeId, "class", 0, 1), false); + + // Set the snippet parameter to the VEXpression. + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniEngineUtils::HapiFindParameterByName(VexNodeId, "snippet", ParmInfo); + if (ParmId != -1) + { + FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), VexNodeId, H_TCHAR_TO_UTF8(*VexCode), ParmId, 0); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Invalid Parameter: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + } + return true; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniDataLayerUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "GameFramework/Actor.h" +#include "Editor.h" +#include "HAPI/HAPI_Common.h" +#include "Runtime/Launch/Resources/Version.h" + + +struct FHoudiniPackageParams; +struct FHoudiniDataLayer; +class AActor; +class FName; + +// Determine if we can enable data layers or not. The public API still exists in all versions +// to minimize the number of defines in the code. + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 && WITH_EDITOR +#define HOUDINI_ENABLE_DATA_LAYERS (1) +#else +#define HOUDINI_ENABLE_DATA_LAYERS (0) +#endif + +struct FHoudiniUnrealDataLayerInfo +{ + // Information about he data layer being exported. Currently, just the name. + FString Name; +}; + + +class HOUDINIENGINE_API FHoudiniDataLayerUtils +{ +public: + // Extracts the data layer from the Houdini Geo/Part and applies it to the Actor. Normally + // called after cooking/baking. + static void ApplyDataLayersToActor(AActor* Actor, TArray& DataLayers, TMap& DataLayerLookup); + + static TArray GetDataLayers(HAPI_NodeId NodeId, HAPI_PartId PartId); + static TArray GetDataLayers(HAPI_NodeId NodeId, HAPI_PartId PartId, HAPI_GroupType GroupType, int Index); + + // Using this cache, create Houdini Groups for this Actor. + static HAPI_NodeId AddGroupsFromDataLayers(AActor* Actor, HAPI_NodeId ParentNodeId, HAPI_NodeId InputNodeId); + + static HAPI_NodeId CreateGroupNode(HAPI_NodeId ParentNode, HAPI_NodeId InputNode, const FString & GorupName); + + static TArray GetHoudiniGroupNames(AActor* Actor); + + static TArray GetDataLayerInfoForActor(AActor* Actor); + + static bool SetVexCode(HAPI_NodeId VexNodeId, AActor* Actor); + +#if HOUDINI_ENABLE_DATA_LAYERS + static void AddActorToLayer(const FHoudiniPackageParams& Params, AWorldDataLayers* WorldDataLayers, AActor* Actor, const FHoudiniDataLayer& Layer); + static UDataLayerAsset* CreateDataLayerAsset(const FHoudiniPackageParams& Params, const FString & LayerName); + + static UDataLayerInstance* FindOrCreateDataLayerInstance( + const FHoudiniPackageParams& Params, + AWorldDataLayers* WorldDataLayers, + const FHoudiniDataLayer& Layer); + + +#endif + + + + +}; + + +``` + +`Source/HoudiniEngine/Private/HoudiniDataTableTranslator.cpp`: + +```cpp +#include "HoudiniDataTableTranslator.h" + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineString.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniPackageParams.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "EdGraph/EdGraphPin.h" +#include "Engine/DataTable.h" +#include "Kismet2/StructureEditorUtils.h" +#include "UObject/TextProperty.h" +#include "UObject/UObjectGlobals.h" +#include "UserDefinedStructure/UserDefinedStructEditorData.h" + +namespace +{ + template + void + WriteAttributeDataToStruct(const T* AttrData, + int32 Offset, + int32 RowSize, + int32 NumRows, + int32 TupleSize, + uint8* PropData) + { + for (int32 Idx = 0; Idx < NumRows; ++Idx) + { + for (int32 CompIdx = 0; CompIdx < TupleSize; ++CompIdx) + { + int32 DataIdx = Idx * RowSize + CompIdx * sizeof(T) + Offset; + P::SetPropertyValue(&PropData[DataIdx], AttrData[Idx * TupleSize + CompIdx]); + } + } + } + + template + void + WriteAttributeDataToStruct(const void* AttrData, + int32 Offset, + int32 RowSize, + int32 NumRows, + int32 TupleSize, + uint8* PropData, + const TArray& Order) + { + // Account for type conversions - ie Houdini type is int64 but the found + // struct property is float32. Needs an explicit conversion. + const T* CastedData = static_cast(AttrData); + + for (int32 Idx = 0; Idx < NumRows; ++Idx) + { + for (int32 CompIdx = 0; CompIdx < TupleSize; ++CompIdx) + { + int32 DataIdx = Idx * RowSize + Order[CompIdx] * sizeof(P) + Offset; + W::SetPropertyValue(&PropData[DataIdx], static_cast(CastedData[Idx * TupleSize + CompIdx])); + } + } + } + + template + void + WriteAttributeDataToStruct(const void* AttrData, + uint32 Offset, + uint32 RowSize, + uint32 NumRows, + uint32 TupleSize, + uint8* PropData) + { + TArray Order; + Order.Reserve(TupleSize); + for (uint32 Idx = 0; Idx < TupleSize; ++Idx) + { + Order.Add(Idx); + } + return WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData, + Order); + } + + template + bool + WriteAttributeDataToStruct(const void* AttrData, + uint32 RowSize, + const HAPI_AttributeInfo& AttribInfo, + uint8* PropData, + FProperty* Prop, + // Take in the attrib name for the transforms special case + const FString& AttribName) + { + uint32 NumRows = AttribInfo.count; + uint32 TupleSize = AttribInfo.tupleSize; + uint32 Offset = Prop->GetOffset_ForInternal(); + if (Prop->IsA()) + { + const T* CastedData = static_cast(AttrData); + FBoolProperty* BoolProp = CastField(Prop); + + for (uint32 Idx = 0; Idx < NumRows; ++Idx) + { + for (uint32 CompIdx = 0; CompIdx < TupleSize; ++CompIdx) + { + uint32 DataIdx = Idx * RowSize + CompIdx * BoolProp->GetSize() + Offset; + BoolProp->SetPropertyValue(&PropData[DataIdx], static_cast(CastedData[Idx * TupleSize + CompIdx])); + } + } + } + else if (Prop->IsA()) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (Prop->IsA()) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (Prop->IsA()) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (Prop->IsA()) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (Prop->IsA()) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (Prop->IsA()) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (Prop->IsA()) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (Prop->IsA()) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (Prop->IsA()) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (Prop->IsA()) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (FStructProperty* StructProp = CastField(Prop)) + { + FName StructName = StructProp->Struct->GetFName(); + // If the struct prop is a tuple of floats + if (StructName == NAME_Vector4f || StructName == NAME_LinearColor || StructName == NAME_Vector3f || StructName == NAME_Vector2f) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (StructName == NAME_Rotator3f) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData, + { 1, 0, 2 }); + } + // Tuple of doubles + else if (StructName == NAME_Vector4d || StructName == NAME_Vector4 || StructName == NAME_Vector || StructName == NAME_Vector3d || StructName == NAME_Vector2d || StructName == NAME_Vector2D) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (StructName == NAME_Rotator || StructName == NAME_Rotator3d) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData, + { 1, 0, 2 }); + } + else if (StructName == NAME_Color) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData, + { 2, 1, 0, 3 }); + } + else if (StructName == NAME_Transform3d || StructName == NAME_Transform) + { + if (AttribName.EndsWith("_rotation")) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (AttribName.EndsWith("_translate")) + { + WriteAttributeDataToStruct(AttrData, + Offset + 32, + RowSize, + NumRows, + TupleSize, + PropData); + } + else + { + WriteAttributeDataToStruct(AttrData, + Offset + 64, + RowSize, + NumRows, + TupleSize, + PropData); + } + } + else if (StructName == NAME_Transform3f) + { + if (AttribName.EndsWith("_rotation")) + { + WriteAttributeDataToStruct(AttrData, + Offset, + RowSize, + NumRows, + TupleSize, + PropData); + } + else if (AttribName.EndsWith("_translate")) + { + WriteAttributeDataToStruct(AttrData, + Offset + 16, + RowSize, + NumRows, + TupleSize, + PropData); + } + else + { + WriteAttributeDataToStruct(AttrData, + Offset + 32, + RowSize, + NumRows, + TupleSize, + PropData); + } + } + } + else + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + bool IsFloat = std::is_same::value || std::is_same::value; +#else + bool IsFloat = TIsSame::Value || TIsSame::Value; +#endif + const T* CastedData = static_cast(AttrData); + if (Prop->IsA() || Prop->IsA() || Prop->IsA()) + { + for (uint32 Idx = 0; Idx < NumRows; ++Idx) + { + if (TupleSize == 1) + { + FString Str = IsFloat ? FString::SanitizeFloat(CastedData[Idx]) : FString::FromInt(CastedData[Idx]); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + Prop->ImportText_Direct(*Str, &PropData[Idx * RowSize + Offset], nullptr, PPF_ExternalEditor); +#else + Prop->ImportText(*Str, &PropData[Idx * RowSize + Offset], PPF_ExternalEditor, nullptr); +#endif + continue; + } + FString Str = "("; + for (uint32 CompIdx = 0; CompIdx < TupleSize; ++CompIdx) + { + Str.Append(IsFloat ? FString::SanitizeFloat(CastedData[Idx * TupleSize + CompIdx]) : FString::FromInt(CastedData[Idx * TupleSize + CompIdx])); + if (CompIdx != TupleSize - 1) + { + Str.Append(","); + } + } + Str.Append(")"); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + Prop->ImportText_Direct(*Str, &PropData[Idx * RowSize + Offset], nullptr, PPF_ExternalEditor); +#else + Prop->ImportText(*Str, &PropData[Idx * RowSize + Offset], PPF_ExternalEditor, nullptr); +#endif + } + } + else + { + // Error + return false; + } + } + return true; + } + +}; + +void +FHoudiniDataTableTranslator::DeletePreviousOutput(UHoudiniOutput* CurOutput) +{ + // Delete tables first; deleting structures before tables leads to complaints from Unreal. + for (auto OutputObject : CurOutput->GetOutputObjects()) + { + if (!IsValid(OutputObject.Value.OutputObject)) + continue; + + UObject* Object = OutputObject.Value.OutputObject; + if (Object->IsA()) + { + + FHoudiniEngineUtils::ForceDeleteObject(Object); + } + } + + // Add delete structures. + for (auto OutputObject : CurOutput->GetOutputObjects()) + { + if (!IsValid(OutputObject.Value.OutputObject)) + continue; + + UObject* Object = OutputObject.Value.OutputObject; + if (Object->IsA() || Object->IsA()) + { + FHoudiniEngineUtils::ForceDeleteObject(Object); + } + } +} + +bool +FHoudiniDataTableTranslator::BuildDataTable( + const FHoudiniGeoPartObject& HGPO, + UHoudiniOutput* CurOutput, + const FHoudiniPackageParams& PackageParams) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniDataTableTranslator::BuildDataTable); + + DeletePreviousOutput(CurOutput); + + int32 GeoId = HGPO.GeoId; + int32 PartId = HGPO.PartId; + + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + HAPI_Result Error = HAPI_RESULT_FAILURE; + Error = FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), + GeoId, PartId, &PartInfo); + if (Error != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::BuildDataTable]: Error %d when trying to get part info."), Error); + return false; + } + + HAPI_AttributeOwner AttribOwner = HAPI_ATTROWNER_POINT; + int32 NumAttributes = PartInfo.attributeCounts[AttribOwner]; + int32 NumRows = PartInfo.pointCount; + if (NumAttributes < 1) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::BuildDataTable]: Asset %s has no attributes to import into datatable."), *HGPO.AssetName); + return false; + } + + FString BakeFolder; + FHoudiniEngineUtils::GetBakeFolderAttribute(GeoId, PartId, BakeFolder); + if (BakeFolder.IsEmpty()) + { + // HAPI_UNREAL_ATTRIB_OBJECT_PATH was previously documented, but it redundant. But support it anyway. + + HAPI_AttributeInfo AttrInfo; + FHoudiniApi::AttributeInfo_Init(&AttrInfo); + TArray AttrData; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_OBJECT_PATH); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, AttrData, 0, 1); + + if (bSuccess && AttrData.Num() == 1) + { + BakeFolder = AttrData[0]; + } + } + + FString OutputName; + FHoudiniEngineUtils::GetOutputNameAttribute(GeoId, PartId, OutputName, 0, 0); + + TArray RowNames; + bool Status = FHoudiniDataTableTranslator::GenerateRowNames(GeoId, PartId, NumRows, RowNames); + + HAPI_AttributeInfo AttribInfo; + + TArray AttribNames; + Status = FHoudiniDataTableTranslator::GetAttribNames(GeoId, PartId, NumAttributes, AttribOwner, AttribNames); + + // Find the struct to use as the basis for the data table. + AttribInfo.exists = false; + UScriptStruct* RowStruct = nullptr; + bool RowStructNeedsProps = true; + FString RowStructName; + Status = FHoudiniDataTableTranslator::GetRowStructAttrib(GeoId, PartId, AttribInfo, RowStruct, RowStructName); + // Row struct already exists, don't add props later + if (RowStruct) + { + RowStructNeedsProps = false; + } + + UUserDefinedStruct* NewStruct = nullptr; + FGuid DefaultPropId; + if (!RowStruct) + { + // Create the struct if it doesn't already exist + + Status = FHoudiniDataTableTranslator::CreateRowStruct(HGPO, NewStruct, DefaultPropId, PackageParams); + if (!Status) + { + return false; + } + + FHoudiniOutputObjectIdentifier OutputID(HGPO.ObjectId, GeoId, PartId, HGPO.PartName + FString("_user_defined_struct")); + FHoudiniOutputObject& FoundOutputObject = CurOutput->GetOutputObjects().FindOrAdd(OutputID); + TMap& OutputObjects = CurOutput->GetOutputObjects(); + FoundOutputObject.OutputComponents.Empty(); + FoundOutputObject.OutputObject = NewStruct; + + if (!BakeFolder.IsEmpty()) + { + FoundOutputObject.CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_FOLDER, BakeFolder); + } + + if (!OutputName.IsEmpty()) + FoundOutputObject.CachedAttributes.Add(HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2, OutputName); + + if (!RowStructName.IsEmpty()) + FoundOutputObject.CachedAttributes.Add(HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWSTRUCT, RowStructName); + + RowStruct = NewStruct; + RowStruct->PreEditChange(nullptr); + } + + TMap ActualProps; + TMap PropIndices; + int32 StructSize = RowStructNeedsProps ? 0 : RowStruct->GetStructureSize(); + if (!RowStructNeedsProps) + { + FHoudiniDataTableTranslator::PopulatePropList(RowStruct, ActualProps, PropIndices); + } + + TMap TransformCandidates; + TSet TransformAttribs; + FHoudiniDataTableTranslator::FindTransformAttribs(AttribNames, TransformCandidates, TransformAttribs); + + // Array of attribs that definitely represent a transform + TMap FoundProps; + TMap FoundInfos; + int32 AssignedIdx = 1; + TMap CreatedIds; + for (const FString& TAttrib : TransformAttribs) + { + const FHoudiniDataTableTranslator::TransformComponents& Comps = TransformCandidates[TAttrib]; + if (RowStructNeedsProps) + { + Status = FHoudiniDataTableTranslator::CreateTransformProp(TAttrib, NewStruct, AssignedIdx, CreatedIds); + } + else + { + Status = FHoudiniDataTableTranslator::ApplyTransformProp(TAttrib, Comps, PropIndices, ActualProps, FoundProps); + } + if (!Status) + { + continue; + } + FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), + GeoId, PartId, TCHAR_TO_ANSI(*Comps.Rotation), AttribOwner, &AttribInfo); + FoundInfos.Add(Comps.Rotation, AttribInfo); + FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), + GeoId, PartId, TCHAR_TO_ANSI(*Comps.Translate), AttribOwner, &AttribInfo); + FoundInfos.Add(Comps.Translate, AttribInfo); + FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), + GeoId, PartId, TCHAR_TO_ANSI(*Comps.Scale), AttribOwner, &AttribInfo); + FoundInfos.Add(Comps.Scale, AttribInfo); + + // At this point we know there is a valid transform property to map the attribs to, + // remove them from the list that we iterate over later. + AttribNames.Remove(Comps.Rotation); + AttribNames.Remove(Comps.Translate); + AttribNames.Remove(Comps.Scale); + } + + for (const FString& AttribName : AttribNames) + { + // Skip any attrib without the unreal_data_table prefix + // as well as any attribs we preprocess + if (!AttribName.StartsWith(HAPI_UNREAL_ATTRIB_DATA_TABLE_PREFIX) || + AttribName == HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWNAME || + AttribName == HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWSTRUCT) + { + continue; + } + // Slice off the prefix + FString Fragment = AttribName.Mid(FString(HAPI_UNREAL_ATTRIB_DATA_TABLE_PREFIX).Len()); + + if (RowStructNeedsProps) + { + // Need to create and add the prop to the struct + Status = FHoudiniDataTableTranslator::CreateRowStructProp(GeoId, + PartId, + AttribName, + Fragment, + AttribOwner, + AttribInfo, + NewStruct, + AssignedIdx, + CreatedIds, + FoundInfos); + } + else + { + Status = FHoudiniDataTableTranslator::ApplyRowStructProp(GeoId, + PartId, + AttribName, + Fragment, + AttribOwner, + AttribInfo, + ActualProps, + FoundProps, + PropIndices, + FoundInfos); + } + } + // Now that all props have been added to the created struct, + // remove the default bool prop that comes with it. + // This has to be done after because a struct must always have at least 1 prop. + if (RowStructNeedsProps) + { + Status = FHoudiniDataTableTranslator::RemoveDefaultProp(NewStruct, + DefaultPropId, + StructSize, + CreatedIds, + TransformAttribs, + TransformCandidates, + FoundProps); + if (!Status) + { + return false; + } + } + + if (!FoundProps.Num()) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::BuildDataTable]: No valid attributes to migrate to data table.")); + return false; + } + + // A packed list of rows, the rows are next to each other in memory. + uint8* RowData = (uint8*)FMemory::MallocZeroed(NumRows * StructSize); + // FoundProps is the map of properties that need to be set in the data table + // They are guaranteed to have an attribute in the point cloud input. + Status = FHoudiniDataTableTranslator::PopulateRowData(GeoId, + PartId, + FoundProps, + FoundInfos, + StructSize, + NumRows, + RowData); + if (!Status) + { + return false; + } + + UDataTable* DataTable = FHoudiniDataTableTranslator::CreateDataTable(HGPO, + NumRows, + RowNames, + StructSize, + RowStruct, + RowData, + PackageParams); + + FHoudiniOutputObjectIdentifier OutputID(HGPO.ObjectId, GeoId, PartId, HGPO.PartName); + FHoudiniOutputObject& FoundOutputObject = CurOutput->GetOutputObjects().FindOrAdd(OutputID); + FoundOutputObject.OutputComponents.Empty(); + FoundOutputObject.OutputObject = DataTable; + + // Cache any attributes we may need for baking. + + if (!BakeFolder.IsEmpty()) + FoundOutputObject.CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_FOLDER, BakeFolder); + + if (!OutputName.IsEmpty()) + FoundOutputObject.CachedAttributes.Add(HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2, OutputName); + + FMemory::Free(RowData); + + return true; +} + +bool +FHoudiniDataTableTranslator::GenerateRowNames(int32 GeoId, int32 PartId, int32 NumRows, TArray& RowNames) +{ + RowNames.Reserve(NumRows); + + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWNAME); + bool Status = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, RowNames); + + if (!Status || RowNames.Num() == 0) + { + RowNames.Add("NewRow"); + for (int32 Idx = 0; Idx < NumRows - 1; ++Idx) + { + RowNames.Add(FString("NewRow_") + FString::FromInt(Idx)); + } + } + return Status; +} + +bool +FHoudiniDataTableTranslator::SetOutputPath(int32 GeoId, + int32 PartId, + HAPI_AttributeInfo& AttribInfo, + FString& DataTableName, + FString& DataTableFolder) +{ + TArray DTNameHolder; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_OBJECT_PATH); + bool Status = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, DTNameHolder); + Accessor.GetInfo(AttribInfo); + if (Status && AttribInfo.exists && AttribInfo.storage == HAPI_STORAGETYPE_STRING && DTNameHolder.Num()) + { + const FString Folder = FPaths::GetPath(DTNameHolder[0]); + if (!FHoudiniEngineUtils::DoesFolderExist(Folder)) + { + return false; + } + DataTableName = FPaths::GetBaseFilename(DTNameHolder[0], true); + DataTableFolder = Folder; + } + return Status; +} + +bool +FHoudiniDataTableTranslator::GetAttribNames(int32 GeoId, + int32 PartId, + int32 NumAttributes, + HAPI_AttributeOwner AttribOwner, + TArray& AttribNames) +{ + // Get the list of attribute names + TArray AttribNameHandles; + AttribNameHandles.SetNum(NumAttributes); + HAPI_Result Error = FHoudiniApi::GetAttributeNames( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, AttribOwner, + AttribNameHandles.GetData(), NumAttributes); + if (Error != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::GetAttribNames]: Error %d when trying to get list of attribute names."), Error); + } + + FHoudiniEngineString::SHArrayToFStringArray(AttribNameHandles, AttribNames); + + return !Error; +} + +bool +FHoudiniDataTableTranslator::GetRowStructAttrib(int32 GeoId, + int32 PartId, + HAPI_AttributeInfo& AttribInfo, + UScriptStruct*& RowStruct, + FString& RowStructName) +{ + TArray StructPathHolder; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWSTRUCT); + bool Status = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, StructPathHolder); + Accessor.GetInfo(AttribInfo); + if (Status && AttribInfo.exists && AttribInfo.storage == HAPI_STORAGETYPE_STRING && StructPathHolder.Num()) + { + RowStructName = StructPathHolder[0]; + RowStruct = LoadObject(UScriptStruct::StaticClass(), *RowStructName); + } + return Status; +} + +bool +FHoudiniDataTableTranslator::CreateRowStruct(const FHoudiniGeoPartObject& HGPO, + UUserDefinedStruct*& NewStruct, + FGuid& DefaultPropId, + FHoudiniPackageParams PackageParams) +{ + PackageParams.ObjectId = HGPO.ObjectId; + PackageParams.GeoId = HGPO.GeoId; + PackageParams.PartId = HGPO.PartId; + PackageParams.ObjectName = FString::Printf(TEXT("%s_%d_%d_%d_%s"), *PackageParams.HoudiniAssetName, + PackageParams.ObjectId, PackageParams.GeoId, PackageParams.PartId, TEXT("rowstruct")); + + // Make sure to delete the package di + FString PackageName = ""; + UPackage* Package = PackageParams.CreatePackageForObject(PackageName); + if (!Package) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::CreateRowStruct]: Failed to create package.")); + return false; + } + + if (!Package->IsFullyLoaded()) + { + FlushAsyncLoading(); + if (!Package->GetOuter()) + { + Package->FullyLoad(); + } + else + { + Package->GetOutermost()->FullyLoad(); + } + } + + // Iterate through objects within the package, and delete and structs, otherwise Unreal asserts. + ForEachObjectWithOuter(Package, [&](UObject* Obj) + { + if(Obj && Obj->IsA(UUserDefinedStruct::StaticClass())) + { + ObjectTools::DeleteSingleObject(Obj); + } + }, true); + + NewStruct = FStructureEditorUtils::CreateUserDefinedStruct(Package, FName(PackageName), RF_Standalone | RF_Public); + TFieldIterator It(NewStruct); + DefaultPropId = FStructureEditorUtils::GetGuidForProperty(*It); + + FAssetRegistryModule::AssetCreated(NewStruct); + + return true; +} + +bool +FHoudiniDataTableTranslator::PopulatePropList(const UScriptStruct* RowStruct, + TMap& ActualProps, + TMap& PropIndices) +{ + ActualProps.Reserve(RowStruct->GetPropertiesSize()); + PropIndices.Reserve(RowStruct->GetPropertiesSize()); + int32 Idx = 1; + for (TFieldIterator It(RowStruct); It; ++It) + { + FString NiceName = It->GetAuthoredName(); + NiceName = NiceName.Replace(TEXT(" "), TEXT("_")); + NiceName = NiceName.Replace(TEXT(":"), TEXT("_")); + // These maps only used to match attrib names with struct props when the struct already exists + ActualProps.Add(NiceName, *It); + PropIndices.Add(NiceName, Idx++); + } + + return true; +} + +bool +FHoudiniDataTableTranslator::FindTransformAttribs(const TArray& AttribNames, + TMap& TransformCandidates, + TSet& TransformAttribs) +{ + for (const FString& AttribName : AttribNames) + { + if (AttribName.StartsWith(HAPI_UNREAL_ATTRIB_DATA_TABLE_PREFIX) && + (AttribName.EndsWith("_rotation") || + AttribName.EndsWith("_translate") || + AttribName.EndsWith("_scale"))) + { + int32 Start = FString(HAPI_UNREAL_ATTRIB_DATA_TABLE_PREFIX).Len(); + int32 End; + AttribName.FindLastChar('_', End); + FString RawName = AttribName.Mid(Start, End - Start); + FString Component = AttribName.Mid(End + 1); + FString FullName = AttribName.Mid(Start); + if (!TransformCandidates.Contains(RawName)) + { + TransformCandidates.Add(RawName, TransformComponents()); + } + TransformComponents& Comps = TransformCandidates[RawName]; + if (Component == "rotation") + { + Comps.Rotation = AttribName; + } + else if (Component == "translate") + { + Comps.Translate = AttribName; + } + else + { + Comps.Scale = AttribName; + } + + if (Comps.Rotation != "" && Comps.Translate != "" && Comps.Scale != "") + { + TransformAttribs.Add(RawName); + } + } + } + + return true; +} + +bool +FHoudiniDataTableTranslator::CreateTransformProp(const FString& TAttrib, + UUserDefinedStruct* NewStruct, + int32& AssignedIdx, + TMap& CreatedIds) +{ + bool Status = FStructureEditorUtils::AddVariable(NewStruct, FEdGraphPinType("struct", FName(TEXT("double")), TBaseStructure::Get(), EPinContainerType::None, false, FEdGraphTerminalType())); + + if (!Status) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::CreateTransformProp]: Failed to create a transform property for attribute %s."), *TAttrib); + return false; + } + + FString ExpectedName = DEFAULT_PROP_PREFIX + FString::FromInt(AssignedIdx++); + FStructVariableDescription* VarDesc = nullptr; + for (auto&& Desc : FStructureEditorUtils::GetVarDesc(NewStruct)) + { + if (Desc.FriendlyName == ExpectedName) + { + VarDesc = &Desc; + break; + } + } + if (!VarDesc) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::CreateTransformProp]: Could not find newly added transform prop by name %s."), *ExpectedName); + --AssignedIdx; + return false; + } + + Status = FStructureEditorUtils::RenameVariable(NewStruct, VarDesc->VarGuid, TAttrib); + if (!Status) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::CreateTransformProp]: Failed to rename the transform property %s to %s."), *ExpectedName, *TAttrib); + return false; + } + ++AssignedIdx; + CreatedIds.Add(TAttrib, VarDesc->VarGuid); + + return true; +} + +bool +FHoudiniDataTableTranslator::ApplyTransformProp(const FString& TAttrib, + const TransformComponents& Comps, + const TMap& PropIndices, + TMap& ActualProps, + TMap& FoundProps) +{ + int32 AttrIdx = 0; + int32 Slice = 0; + ExtractIndex(TAttrib, AttrIdx, Slice); + + // Slice off index, does nothing if no index specified. + FString Fragment = TAttrib.Mid(Slice); + + FProperty** FProp = ActualProps.Find(Fragment); + if (!FProp) + { + return false; + } + FStructProperty* SProp = CastField(*FProp); + if (!SProp) + { + return false; + } + const FName& PropName = SProp->Struct->GetFName(); + if (PropName != NAME_Transform3f && PropName != NAME_Transform3d && PropName != NAME_Transform) + { + return false; + } + // Make sure there is a corresponding prop in the actual struct and the index matches if present. + if (AttrIdx && AttrIdx != PropIndices[Fragment]) + { + return false; + } + + FoundProps.Add(Comps.Rotation, SProp); + FoundProps.Add(Comps.Translate, SProp); + FoundProps.Add(Comps.Scale, SProp); + + ActualProps.Remove(Fragment); + ActualProps.Add(Comps.Rotation, SProp); + ActualProps.Add(Comps.Translate, SProp); + ActualProps.Add(Comps.Scale, SProp); + + return true; +} + +bool +FHoudiniDataTableTranslator::CreateRowStructProp(int32 GeoId, + int32 PartId, + const FString& AttribName, + const FString& Fragment, + HAPI_AttributeOwner AttribOwner, + HAPI_AttributeInfo& AttribInfo, + UUserDefinedStruct* NewStruct, + int32& AssignedIdx, + TMap& CreatedIds, + TMap& FoundInfos) +{ + HAPI_Result Error = FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), + GeoId, PartId, TCHAR_TO_ANSI(*AttribName), AttribOwner, &AttribInfo); + if (!AttribInfo.exists || Error != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::CreateRowStructProp]: Error %d when trying to get info for attribute %s."), Error, *AttribName); + return false; + } + if (AttribInfo.tupleSize > 4) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::CreateRowStructProp]: Cannot convert tuple attribute %s of size %d (>4) to struct property."), *AttribName, AttribInfo.tupleSize); + return false; + } + + if (!UE_TYPE_NAMES.Contains(AttribInfo.storage)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::CreateRowStructProp]: Attribute %s is of an unsupported type %d."), *AttribName, AttribInfo.storage); + return false; + } + + FName SubCat = NAME_None; + if (AttribInfo.storage == HAPI_STORAGETYPE_FLOAT) + { + SubCat = FName(TEXT("float")); + } + else if (AttribInfo.storage == HAPI_STORAGETYPE_FLOAT64) + { + SubCat = FName(TEXT("double")); + } + + bool Status = FStructureEditorUtils::AddVariable(NewStruct, + FEdGraphPinType(UE_TYPE_NAMES[AttribInfo.storage][AttribInfo.tupleSize - 1], + SubCat, + VECTOR_STRUCTS[AttribInfo.storage][AttribInfo.tupleSize - 1], + EPinContainerType::None, + false, + FEdGraphTerminalType())); + if (!Status) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::CreateRowStructProp]: Failed to create a struct property for attribute %s."), *AttribName); + return false; + } + + // AddVariable doesn't return a handle to the created prop so we have to search it by name + FString ExpectedName = DEFAULT_PROP_PREFIX + FString::FromInt(AssignedIdx++); + FStructVariableDescription* VarDesc = nullptr; + for (auto&& Desc : FStructureEditorUtils::GetVarDesc(NewStruct)) + { + if (Desc.FriendlyName == ExpectedName) + { + VarDesc = &Desc; + break; + } + } + if (!VarDesc) + { + // Somehow the variable was added but couldn't be found afterwards + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::CreateRowStructProp]: Could not find newly added prop by name %s."), *ExpectedName); + --AssignedIdx; + return false; + } + + Status = FStructureEditorUtils::RenameVariable(NewStruct, VarDesc->VarGuid, Fragment); + if (!Status) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::CreateRowStructProp]: Failed to rename the property %s to %s."), *ExpectedName, *Fragment); + return false; + } + + // Renaming the variable increments the counter for the next created prop + ++AssignedIdx; + CreatedIds.Add(AttribName, VarDesc->VarGuid); + FoundInfos.Add(AttribName, AttribInfo); + + return true; +} + +bool +FHoudiniDataTableTranslator::ApplyRowStructProp(int32 GeoId, + int32 PartId, + const FString& AttribName, + const FString& Fragment, + HAPI_AttributeOwner AttribOwner, + HAPI_AttributeInfo& AttribInfo, + TMap& ActualProps, + TMap& FoundProps, + TMap& PropIndices, + TMap& FoundInfos) +{ + int32 AttrIdx = 0; + int32 Slice = 0; + ExtractIndex(Fragment, AttrIdx, Slice); + // Slice off index, does nothing if no index specified. + FString SlicedFragment = Fragment.Mid(Slice); + + FProperty** FProp = ActualProps.Find(SlicedFragment); + // Make sure there is a corresponding prop in the actual struct and the index matches if present. + if (FProp && (!AttrIdx || AttrIdx == PropIndices[SlicedFragment])) + { + HAPI_Result Error = FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), + GeoId, PartId, TCHAR_TO_ANSI(*AttribName), AttribOwner, &AttribInfo); + if (!AttribInfo.exists || Error != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::ApplyRowStructProp]: Error %d when trying to get info for attribute %s."), Error, *AttribName); + return false; + } + FoundProps.Add(AttribName, *FProp); + FoundInfos.Add(AttribName, AttribInfo); + } + else + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::ApplyRowStructProp]: Skipping attribute %s, could not find struct prop or index mismatch."), *AttribName); + } + + return true; +} + +bool +FHoudiniDataTableTranslator::RemoveDefaultProp(UUserDefinedStruct* NewStruct, + FGuid& DefaultPropId, + int32& StructSize, + TMap& CreatedIds, + const TSet& TransformAttribs, + const TMap& TransformCandidates, + TMap& FoundProps) +{ + bool Status = FStructureEditorUtils::RemoveVariable(NewStruct, DefaultPropId); + if (!Status) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::RemoveDefaultProp]: Failed to remove default property.")); + return false; + } + + StructSize = NewStruct->GetStructureSize(); + NewStruct->MarkPackageDirty(); + + for (auto&& KV : CreatedIds) + { + FProperty* Prop = FStructureEditorUtils::GetPropertyByGuid(NewStruct, KV.Value); + // If this prop is a transform + if (TransformAttribs.Contains(KV.Key)) + { + const FHoudiniDataTableTranslator::TransformComponents& Comps = TransformCandidates[KV.Key]; + FoundProps.Add(Comps.Rotation, Prop); + FoundProps.Add(Comps.Translate, Prop); + FoundProps.Add(Comps.Scale, Prop); + } + else + { + FoundProps.Add(KV.Key, Prop); + } + } + + return true; +} + +bool +FHoudiniDataTableTranslator::PopulateRowData(int32 GeoId, + int32 PartId, + const TMap& FoundProps, + const TMap& FoundInfos, + int32 StructSize, + int32 NumRows, + uint8* RowData) +{ + int32 RowIdx = 0; + for (auto&& KV : FoundProps) + { + HAPI_Result Result = HAPI_RESULT_FAILURE; + + auto Src = StringCast(*KV.Key); + const ANSICHAR* AttribName = Src.Get(); + + FProperty* Prop = KV.Value; + int32 Offset = Prop->GetOffset_ForInternal(); + + HAPI_AttributeInfo AttribInfo = FoundInfos[KV.Key]; + if (AttribInfo.count < 1) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::PopulateRowData]: Attribute %s has no values."), *KV.Key); + return false; + } + + if (AttribInfo.storage == HAPI_STORAGETYPE_INVALID) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::PopulateRowData]: Invalid storage type for Houdini attribute %s."), *KV.Key); + return false; + } + + if (!IsValidType(AttribInfo, Prop)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::PopulateRowData]: Incompatible types for column %s and attribute %s."), *Prop->GetAuthoredName(), *KV.Key); + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::PopulateRowData]: --- skipping values for attribute %s ---"), *KV.Key); + + continue; + } + + void* Data = nullptr; + + if (AttribInfo.storage == HAPI_STORAGETYPE_INT) + { + Data = FMemory::Malloc(AttribInfo.count * AttribInfo.tupleSize * sizeof(int32)); + Result = FHoudiniApi::GetAttributeIntData(FHoudiniEngine::Get().GetSession(), GeoId, PartId, AttribName, &AttribInfo, -1, static_cast(Data), 0, AttribInfo.count); + + WriteAttributeDataToStruct(Data, StructSize, AttribInfo, RowData, Prop, KV.Key); + } + else if (AttribInfo.storage == HAPI_STORAGETYPE_INT64) + { + Data = FMemory::Malloc(AttribInfo.count * AttribInfo.tupleSize * sizeof(int64)); + // int64 might be different from HAPI_Int64 on some linux platforms +#if PLATFORM_LINUX + if (sizeof(int64) != sizeof(HAPI_Int64)) + { + TArray HData; + HData.Reserve(AttribInfo.count * AttribInfo.tupleSize); + Result = FHoudiniApi::GetAttributeInt64Data(FHoudiniEngine::Get().GetSession(), GeoId, PartId, AttribName, &AttribInfo, -1, HData.GetData(), 0, AttribInfo.count); + int32 Idx = 0; + int64* Ptr = static_cast(Data); + for (const HAPI_Int64& Item : HData) + { + Ptr[Idx++] = static_cast(Item); + } + } + else + { + Result = FHoudiniApi::GetAttributeInt64Data(FHoudiniEngine::Get().GetSession(), GeoId, PartId, AttribName, &AttribInfo, -1, reinterpret_cast(Data), 0, AttribInfo.count); + } +#else + Result = FHoudiniApi::GetAttributeInt64Data(FHoudiniEngine::Get().GetSession(), GeoId, PartId, AttribName, &AttribInfo, -1, static_cast(Data), 0, AttribInfo.count); +#endif + WriteAttributeDataToStruct(Data, StructSize, AttribInfo, RowData, Prop, KV.Key); + } + else if (AttribInfo.storage == HAPI_STORAGETYPE_FLOAT) + { + Data = FMemory::Malloc(AttribInfo.count * AttribInfo.tupleSize * sizeof(float)); + Result = FHoudiniApi::GetAttributeFloatData(FHoudiniEngine::Get().GetSession(), GeoId, PartId, AttribName, &AttribInfo, -1, static_cast(Data), 0, AttribInfo.count); + + WriteAttributeDataToStruct(Data, StructSize, AttribInfo, RowData, Prop, KV.Key); + } + else if (AttribInfo.storage == HAPI_STORAGETYPE_FLOAT64) + { + Data = FMemory::Malloc(AttribInfo.count * AttribInfo.tupleSize * sizeof(double)); + Result = FHoudiniApi::GetAttributeFloat64Data(FHoudiniEngine::Get().GetSession(), GeoId, PartId, AttribName, &AttribInfo, -1, static_cast(Data), 0, AttribInfo.count); + + WriteAttributeDataToStruct(Data, StructSize, AttribInfo, RowData, Prop, KV.Key); + } + else if (AttribInfo.storage == HAPI_STORAGETYPE_UINT8) + { + Data = FMemory::Malloc(AttribInfo.count * AttribInfo.tupleSize * sizeof(uint8)); + Result = FHoudiniApi::GetAttributeUInt8Data(FHoudiniEngine::Get().GetSession(), GeoId, PartId, AttribName, &AttribInfo, -1, static_cast(Data), 0, AttribInfo.count); + + WriteAttributeDataToStruct(Data, StructSize, AttribInfo, RowData, Prop, KV.Key); + } + else if (AttribInfo.storage == HAPI_STORAGETYPE_INT8) + { + Data = FMemory::Malloc(AttribInfo.count * AttribInfo.tupleSize * sizeof(int8)); + Result = FHoudiniApi::GetAttributeInt8Data(FHoudiniEngine::Get().GetSession(), GeoId, PartId, AttribName, &AttribInfo, -1, static_cast(Data), 0, AttribInfo.count); + + WriteAttributeDataToStruct(Data, StructSize, AttribInfo, RowData, Prop, KV.Key); + } + else if (AttribInfo.storage == HAPI_STORAGETYPE_INT16) + { + Data = FMemory::Malloc(AttribInfo.count * AttribInfo.tupleSize * sizeof(int16)); + Result = FHoudiniApi::GetAttributeInt16Data(FHoudiniEngine::Get().GetSession(), GeoId, PartId, AttribName, &AttribInfo, -1, static_cast(Data), 0, AttribInfo.count); + + WriteAttributeDataToStruct(Data, StructSize, AttribInfo, RowData, Prop, KV.Key); + } + else if (AttribInfo.storage == HAPI_STORAGETYPE_STRING) + { + TArray StringData; + StringData.Reserve(NumRows); + + + FHoudiniHapiAccessor Accessor(GeoId, PartId, AttribName); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, StringData); + + if (!bSuccess) + Result = HAPI_RESULT_FAILURE; + else + Result = HAPI_RESULT_SUCCESS; + + if (AttribInfo.tupleSize != 1) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::PopulateRowData]: Tuples of strings are not supported, skipping attribute %s."), *KV.Key); + return false; + } + if (!Prop->IsA() && !Prop->IsA() && !Prop->IsA()) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::PopulateRowData]: Cannot convert Houdini string attribute to non string property, skipping attribute %s."), *KV.Key); + return false; + } + for (int32 Idx = 0; Idx < NumRows; ++Idx) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + Prop->ImportText_Direct(*StringData[Idx], &RowData[Idx * StructSize + Offset], nullptr, PPF_ExternalEditor); +#else + Prop->ImportText(*StringData[Idx], &RowData[Idx * StructSize + Offset], PPF_ExternalEditor, nullptr); +#endif + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::PopulateRowData]: Unknown attribute type %d."), AttribInfo.storage); + return false; + } + + if (Data) + { + FMemory::Free(Data); + Data = nullptr; + } + + if (Result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniDataTableTranslator::PopulateRowData]: Error %d when trying to get values for attribute %s."), Result, *KV.Key); + return false; + } + + ++RowIdx; + } + + return true; +} + +UDataTable* +FHoudiniDataTableTranslator::CreateDataTable(const FHoudiniGeoPartObject& HGPO, + int32 NumRows, + const TArray& RowNames, + int32 StructSize, + UScriptStruct* RowStruct, + uint8* RowData, + FHoudiniPackageParams PackageParams) +{ + TMap TableData; + UDataTable* CreatedDataTable; + TableData.Reserve(NumRows); + for (int32 Idx = 0; Idx < NumRows; ++Idx) + { + TableData.Add(FName(RowNames[Idx]), &RowData[Idx * StructSize]); + } + + PackageParams.ObjectId = HGPO.ObjectId; + PackageParams.GeoId = HGPO.GeoId; + PackageParams.PartId = HGPO.PartId; + PackageParams.SplitStr = "datatable"; + + CreatedDataTable = PackageParams.CreateObjectAndPackage(); + CreatedDataTable->PreEditChange(nullptr); + + // CreateTableFromRawData not available on UE4. +#if ENGINE_MAJOR_VERSION < 5 + CreatedDataTable->RowStruct = RowStruct; + for (auto RowIt = TableData.CreateIterator(); RowIt; ++RowIt) + { + uint8* RowBuf = FDataTableEditorUtils::AddRow(CreatedDataTable, RowIt.Key()); + FMemory::Memcpy(RowBuf, RowIt.Value(), StructSize); + } +#else + CreatedDataTable->CreateTableFromRawData(TableData, RowStruct); +#endif + + CreatedDataTable->MarkPackageDirty(); + + // make sure package is loaded. I'm not sure why this is needed, since it was just created, but it is... + CreatedDataTable->GetPackage()->FullyLoad(); + + FAssetRegistryModule::AssetCreated(CreatedDataTable); + + return CreatedDataTable; +} + +const FString FHoudiniDataTableTranslator::DEFAULT_PROP_PREFIX = "MemberVar_"; + +const TMap> FHoudiniDataTableTranslator::UE_TYPE_NAMES = { + // Names are constant presets taken from UEdGraphSchema_K2 + // Mappings found in UEdGraphSchema_K2::GetPropertyCategoryInfo + {HAPI_STORAGETYPE_INT, {FName(TEXT("int")), FName(TEXT("struct")), FName(TEXT("struct")), FName(TEXT("struct"))}}, + {HAPI_STORAGETYPE_INT64, {FName(TEXT("int64")), FName(TEXT("struct")), FName(TEXT("struct")), FName(TEXT("struct"))}}, + {HAPI_STORAGETYPE_FLOAT, {FName(TEXT("real")), FName(TEXT("struct")), FName(TEXT("struct")), FName(TEXT("struct"))}}, + {HAPI_STORAGETYPE_FLOAT64, {FName(TEXT("real")), FName(TEXT("struct")), FName(TEXT("struct")), FName(TEXT("struct"))}}, + {HAPI_STORAGETYPE_STRING, {FName(TEXT("string"))}}, + {HAPI_STORAGETYPE_UINT8, {FName(TEXT("byte")), FName(TEXT("struct")), FName(TEXT("struct")), FName(TEXT("struct"))}}, + // No presets, just expand to int32 + {HAPI_STORAGETYPE_INT8, {FName(TEXT("int")), FName(TEXT("struct")), FName(TEXT("struct")), FName(TEXT("struct"))}}, + {HAPI_STORAGETYPE_INT16, {FName(TEXT("int")), FName(TEXT("struct")), FName(TEXT("struct")), FName(TEXT("struct"))}} +}; + +const TMap> FHoudiniDataTableTranslator::VECTOR_STRUCTS = { + {HAPI_STORAGETYPE_INT, { nullptr, TBaseStructure::Get(), TBaseStructure::Get(), TBaseStructure::Get() }}, + {HAPI_STORAGETYPE_INT64, { nullptr, TBaseStructure::Get(), TBaseStructure::Get(), TBaseStructure::Get() }}, + {HAPI_STORAGETYPE_FLOAT, { nullptr, TBaseStructure::Get(), TBaseStructure::Get(), TBaseStructure::Get() }}, + {HAPI_STORAGETYPE_FLOAT64, { nullptr, TBaseStructure::Get(), TBaseStructure::Get(), TBaseStructure::Get() }}, + // cannot have tuple of strings + {HAPI_STORAGETYPE_STRING, { nullptr }}, + {HAPI_STORAGETYPE_UINT8, { nullptr, TBaseStructure::Get(), TBaseStructure::Get(), TBaseStructure::Get() }}, + {HAPI_STORAGETYPE_INT8, { nullptr, TBaseStructure::Get(), TBaseStructure::Get(), TBaseStructure::Get() }}, + {HAPI_STORAGETYPE_INT16, { nullptr, TBaseStructure::Get(), TBaseStructure::Get(), TBaseStructure::Get() }} +}; + +void +FHoudiniDataTableTranslator::ExtractIndex(const FString& Fragment, + int32& AttrIdx, + int32& Slice) +{ + // Determine if an index was specified in attrib name. + for (int32 Idx = 0; Idx < Fragment.Len(); ++Idx) + { + ++Slice; + const TCHAR& ch = Fragment[Idx]; + if (ch == '_' && AttrIdx != 0) + { + break; + } + if (ch >= '0' && ch <= '9') + { + AttrIdx *= 10; + AttrIdx += ch - '0'; + } + // If we encounter a letter before a _, the index is not present. + else + { + AttrIdx = 0; + Slice = 0; + break; + } + } +} + +bool +FHoudiniDataTableTranslator::IsValidType(const HAPI_AttributeInfo& AttribInfo, + const FProperty* Prop) +{ + // Invalid if attribute is string but property type is not + // Numeric to numeric is fine regardless of types, will just + // forcefully convert. + // Numeric to string is also fine, will convert the numbers to string. + if (AttribInfo.storage == HAPI_STORAGETYPE_STRING) + { + return Prop->IsA() || + Prop->IsA() || + Prop->IsA(); + } + // Check the destination prop has the same number of components. + if (AttribInfo.tupleSize == 1) + { + return Prop->IsA() || + Prop->IsA() || + Prop->IsA() || + Prop->IsA() || + Prop->IsA(); + } + const FStructProperty* SProp = CastField(Prop); + if (!SProp) + { + return false; + } + const FName Name = SProp->Struct->GetFName(); + if (AttribInfo.tupleSize == 2) + { + return Name == NAME_Vector2d || Name == NAME_Vector2D; + } + else if (AttribInfo.tupleSize == 3) + { + return Name == NAME_Vector3f || Name == NAME_Vector3d || + Name == NAME_Rotator3f || Name == NAME_Rotator3d || + Name == NAME_Vector || Name == NAME_Rotator || + Name == NAME_Transform || Name == NAME_Transform3f || + Name == NAME_Transform3d; + } + else if (AttribInfo.tupleSize == 4) + { + return Name == NAME_Vector4f || Name == NAME_Vector4d || + Name == NAME_Vector4 || Name == NAME_LinearColor || + Name == NAME_Color || Name == NAME_Transform || + Name == NAME_Transform3f || Name == NAME_Transform3d; + } + return false; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniDataTableTranslator.h`: + +```h +/* +* Copyright (c) <2022> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI.h" + +#include "HoudiniPackageParams.h" + +class UDataTable; + +struct HOUDINIENGINE_API FHoudiniDataTableTranslator +{ + static bool BuildDataTable(const FHoudiniGeoPartObject& HGPO, + UHoudiniOutput* CurOutput, + const FHoudiniPackageParams& PackageParams); + + static bool GenerateRowNames(int32 GeoId, + int32 PartId, + int32 NumRows, + TArray& RowNames); + + static bool SetOutputPath(int32 GeoId, + int32 PartId, + HAPI_AttributeInfo& AttribInfo, + FString& DataTableName, + FString& DataTableFolder); + + static bool GetAttribNames(int32 GeoId, + int32 PartId, + int32 NumAttributes, + HAPI_AttributeOwner AttribOwner, + TArray& AttribNames); + + static bool GetRowStructAttrib(int32 GeoId, + int32 PartId, + HAPI_AttributeInfo& AttribInfo, + UScriptStruct*& RowStruct, + FString& RowStructName); + + static bool CreateRowStruct(const FHoudiniGeoPartObject& HGPO, + UUserDefinedStruct*& NewStruct, + FGuid& DefaultPropId, + FHoudiniPackageParams PackageParams); + + static bool PopulatePropList(const UScriptStruct* RowStruct, + TMap& ActualProps, + TMap& PropIndices); + + struct TransformComponents + { + FString Rotation = ""; + FString Translate = ""; + FString Scale = ""; + }; + + static bool FindTransformAttribs(const TArray& AttribNames, + TMap& TransformCandidates, + TSet& TransformAttribs); + + static bool CreateTransformProp(const FString& TAttrib, + UUserDefinedStruct* NewStruct, + int32& AssignedIdx, + TMap& CreatedIds); + + static bool ApplyTransformProp(const FString& TAttrib, + const TransformComponents& Comps, + const TMap& PropIndices, + TMap& ActualProps, + TMap& FoundProps); + + static bool CreateRowStructProp(int32 GeoId, + int32 PartId, + const FString& AttribName, + const FString& Fragment, + HAPI_AttributeOwner AttribOwner, + HAPI_AttributeInfo& AttribInfo, + UUserDefinedStruct* NewStruct, + int32& AssignedIdx, + TMap& CreatedIds, + TMap& FoundInfos); + + static bool ApplyRowStructProp(int32 GeoId, + int32 PartId, + const FString& AttribName, + const FString& Fragment, + HAPI_AttributeOwner AttribOwner, + HAPI_AttributeInfo& AttribInfo, + TMap& ActualProps, + TMap& FoundProps, + TMap& PropIndices, + TMap& FoundInfos); + + static bool RemoveDefaultProp(UUserDefinedStruct* NewStruct, + FGuid& DefaultPropId, + int32& StructSize, + TMap& CreatedIds, + const TSet& TransformAttribs, + const TMap& TransformCandidates, + TMap& FoundProps); + + static bool PopulateRowData(int32 GeoId, + int32 PartId, + const TMap& FoundProps, + const TMap& FoundInfos, + int32 StructSize, + int32 NumRows, + uint8* RowData); + + static UDataTable* CreateDataTable(const FHoudiniGeoPartObject& HGPO, + int32 NumRows, + const TArray& RowNames, + int32 StructSize, + UScriptStruct* RowStruct, + uint8* RowData, + FHoudiniPackageParams PackageParams); + +private: + static const FString DEFAULT_PROP_PREFIX; + static const TMap> UE_TYPE_NAMES; + static const TMap> VECTOR_STRUCTS; + + static void ExtractIndex(const FString& Fragment, + int32& AttrIdx, + int32& Slice); + + static bool IsValidType(const HAPI_AttributeInfo& AttribInfo, + const FProperty* Prop); + + static void DeletePreviousOutput(UHoudiniOutput* CurOutput); +}; +``` + +`Source/HoudiniEngine/Private/HoudiniEngine.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngine.h" + +#include "HoudiniEnginePrivatePCH.h" + +#include "HoudiniApi.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniRuntimeSettings.h" +#include "HoudiniEngineScheduler.h" +#include "HoudiniEngineManager.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniEngineTask.h" +#include "HoudiniEngineTaskInfo.h" +#include "HoudiniAssetComponent.h" +#if defined(HOUDINI_USE_PCG) +#include "HoudiniPCGCookable.h" +#endif +#include "UnrealObjectInputManager.h" +#include "UnrealObjectInputManagerImpl.h" +#include "HAPI/HAPI_Version.h" + +#include "Modules/ModuleManager.h" +#include "Misc/ScopeLock.h" +#include "Engine/StaticMesh.h" +#include "Materials/Material.h" +#include "ISettingsModule.h" +#include "HAL/PlatformFileManager.h" +#include "Async/Async.h" +#include "Logging/LogMacros.h" +#include "Framework/Application/SlateApplication.h" +#include "Misc/FileHelper.h" + +#if WITH_EDITOR + #include "Widgets/Notifications/SNotificationList.h" + #include "Framework/Notifications/NotificationManager.h" +#endif + +#define LOCTEXT_NAMESPACE "HoudiniEngine" + +IMPLEMENT_MODULE(FHoudiniEngine, HoudiniEngine) +DEFINE_LOG_CATEGORY( LogHoudiniEngine ); + +FHoudiniEngine * +FHoudiniEngine::HoudiniEngineInstance = nullptr; + +FHoudiniEngine::FHoudiniEngine() + : LicenseType(HAPI_LICENSE_NONE) + , HoudiniEngineSchedulerThread(nullptr) + , HoudiniEngineScheduler(nullptr) + , HoudiniEngineManagerThread(nullptr) + , HoudiniEngineManager(nullptr) + //, bHAPIVersionMismatch(false) + , bEnableCookingGlobal(true) + , UIRefreshCountWhenPauseCooking(0) + , bFirstSessionCreated(false) + , bEnableSessionSync(false) + , bCookUsingHoudiniTime(true) + , bSyncViewport(false) + , bSyncHoudiniViewport(true) + , bSyncUnrealViewport(false) + , HoudiniLogoStaticMesh(nullptr) + , HoudiniDefaultMaterial(nullptr) + , HoudiniTemplateMaterial(nullptr) + , HoudiniCOPStaticMesh(nullptr) + , HoudiniDefaultCOPMaterial(nullptr) + , HoudiniLogoBrush(nullptr) + , HoudiniDefaultReferenceMesh(nullptr) + , HoudiniDefaultReferenceMeshMaterial(nullptr) + , HAPIPerfomanceProfileID(-1) +{ + SetSessionStatus(EHoudiniSessionStatus::Invalid); + +#if WITH_EDITOR + HapiNotificationStarted = 0.0; + TimeSinceLastPersistentNotification = 0.0; +#endif +} + +FHoudiniEngine& +FHoudiniEngine::Get() +{ + check(FHoudiniEngine::HoudiniEngineInstance); + return *FHoudiniEngine::HoudiniEngineInstance; +} + +bool +FHoudiniEngine::IsInitialized() +{ + return FHoudiniEngine::HoudiniEngineInstance != nullptr && FHoudiniEngineUtils::IsInitialized(); +} + +void +FHoudiniEngine::StartupModule() +{ + HOUDINI_LOG_MESSAGE(TEXT("Starting the Houdini Engine module...")); + +#if WITH_EDITOR + // Register settings. + if (ISettingsModule * SettingsModule = FModuleManager::GetModulePtr("Settings")) + { + SettingsModule->RegisterSettings( + "Project", "Plugins", "HoudiniEngine", + LOCTEXT("RuntimeSettingsName", "Houdini Engine"), + LOCTEXT("RuntimeSettingsDescription", "Configure the HoudiniEngine plugin"), + GetMutableDefault< UHoudiniRuntimeSettings >()); + } +#endif + + // Before starting the module, we need to locate and load HAPI library. + { + void * HAPILibraryHandle = FHoudiniEngineUtils::LoadLibHAPI(LibHAPILocation); + if ( HAPILibraryHandle ) + { + FHoudiniApi::InitializeHAPI( HAPILibraryHandle ); + } + else + { + // Get platform specific name of libHAPI. + FString LibHAPIName = FHoudiniEngineRuntimeUtils::GetLibHAPIName(); + HOUDINI_LOG_MESSAGE(TEXT("Failed locating or loading %s"), *LibHAPIName); + } + } + + // Create static mesh Houdini logo. + HoudiniLogoStaticMesh = LoadObject( + nullptr, HAPI_UNREAL_RESOURCE_HOUDINI_LOGO, nullptr, LOAD_None, nullptr); + if (HoudiniLogoStaticMesh.IsValid()) + HoudiniLogoStaticMesh->AddToRoot(); + + // Create default material. + HoudiniDefaultMaterial = LoadObject( + nullptr, HAPI_UNREAL_RESOURCE_HOUDINI_MATERIAL, nullptr, LOAD_None, nullptr); + if (HoudiniDefaultMaterial.IsValid()) + HoudiniDefaultMaterial->AddToRoot(); + + HoudiniTemplateMaterial = LoadObject( + nullptr, HAPI_UNREAL_RESOURCE_HOUDINI_TEMPLATE_MATERIAL, nullptr, LOAD_None, nullptr); + if (HoudiniTemplateMaterial.IsValid()) + HoudiniTemplateMaterial->AddToRoot(); + + // Create default COP mesh + // Try the editor plane first + HoudiniCOPStaticMesh = LoadObject( + nullptr, HAPI_UNREAL_RESOURCE_EDITOR_PLANE, nullptr, LOAD_None, nullptr); + + // .. use ours as backup? (TODO: remove me!) + if (!HoudiniCOPStaticMesh.IsValid()) + HoudiniCOPStaticMesh = LoadObject( + nullptr, HAPI_UNREAL_RESOURCE_HOUDINI_COP_MESH, nullptr, LOAD_None, nullptr); + + if (HoudiniCOPStaticMesh.IsValid()) + HoudiniCOPStaticMesh->AddToRoot(); + + // Create default COP material. + HoudiniDefaultCOPMaterial = LoadObject( + nullptr, HAPI_UNREAL_RESOURCE_HOUDINI_COP_MATERIAL, nullptr, LOAD_None, nullptr); + if (HoudiniDefaultCOPMaterial.IsValid()) + HoudiniDefaultCOPMaterial->AddToRoot(); + + // Houdini Logo Brush + FString Icon128FilePath = FHoudiniEngineUtils::GetHoudiniEnginePluginDir() / TEXT("Resources/Icon128.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*Icon128FilePath)) + { + const FName BrushName(*Icon128FilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniLogoBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // Houdini Engine Logo Brush + FString HEIcon128FilePath = FHoudiniEngineUtils::GetHoudiniEnginePluginDir() / TEXT("Resources/hengine_logo_128.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*HEIcon128FilePath)) + { + const FName BrushName(*HEIcon128FilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineLogoBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // Create Houdini default reference mesh + HoudiniDefaultReferenceMesh = LoadObject( + nullptr, HAPI_UNREAL_RESOURCE_HOUDINI_DEFAULT_REFERENCE_MESH, nullptr, LOAD_None, nullptr); + if (HoudiniDefaultReferenceMesh.IsValid()) + HoudiniDefaultReferenceMesh->AddToRoot(); + + // Create Houdini default reference mesh material + HoudiniDefaultReferenceMeshMaterial = LoadObject + (nullptr, HAPI_UNREAL_RESOURCE_HOUDINI_DEFAULT_REFERENCE_MESH_MATERIAL, nullptr, LOAD_None, nullptr); + if (HoudiniDefaultReferenceMeshMaterial.IsValid()) + HoudiniDefaultReferenceMeshMaterial->AddToRoot(); + + // We do not automatically try to start a session when starting up the module now. + bFirstSessionCreated = false; + + // Create HAPI scheduler and processing thread. + HoudiniEngineScheduler = new FHoudiniEngineScheduler(); + HoudiniEngineSchedulerThread = FRunnableThread::Create( + HoudiniEngineScheduler, TEXT("HoudiniSchedulerThread"), 0, TPri_Normal); + + // Create Houdini Asset Manager + HoudiniEngineManager = new FHoudiniEngineManager(); + + // Create Unreal Object Input manager and its implementation (the singleton takes ownership of the implementation) + FUnrealObjectInputManager::SetSingleton(new FUnrealObjectInputManagerImpl()); + + // Set the session status to Not Started + SetSessionStatus(EHoudiniSessionStatus::NotStarted); + + // Set the default value for pausing houdini engine cooking + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + bEnableCookingGlobal = !HoudiniRuntimeSettings->bPauseCookingOnStart; + + // Check if a null session is set + bool bNoneSession = (HoudiniRuntimeSettings->SessionType == EHoudiniRuntimeSettingsSessionType::HRSST_None); + if (bNoneSession) + SetSessionStatus(EHoudiniSessionStatus::None); + + // Initialize the singleton with this instance + FHoudiniEngine::HoudiniEngineInstance = this; + + // See if we need to start the manager ticking if needed + // Dont tick if we failed to load HAPI, if cooking is disabled or if we're using a null session + if (FHoudiniApi::IsHAPIInitialized()) + { + if (bEnableCookingGlobal && !bNoneSession) + { + PostEngineInitCallback = FCoreDelegates::OnPostEngineInit.AddLambda([]() + { + FHoudiniEngine& HEngine = FHoudiniEngine::Get(); + HEngine.UnregisterPostEngineInitCallback(); + FHoudiniEngineManager* const Manager = HEngine.GetHoudiniEngineManager(); + if (Manager) + Manager->StartHoudiniTicking(); + }); + } + } +} + +void +FHoudiniEngine::ShutdownModule() +{ + HOUDINI_LOG_MESSAGE(TEXT("Shutting down the Houdini Engine module.")); + + // We no longer need the Houdini logo static mesh. + if (HoudiniLogoStaticMesh.IsValid()) + { + HoudiniLogoStaticMesh->RemoveFromRoot(); + HoudiniLogoStaticMesh = nullptr; + } + + // We no longer need the Houdini default material. + if (HoudiniDefaultMaterial.IsValid()) + { + HoudiniDefaultMaterial->RemoveFromRoot(); + HoudiniDefaultMaterial = nullptr; + } + + // We no longer need the Houdini default material. + if (HoudiniTemplateMaterial.IsValid()) + { + HoudiniTemplateMaterial->RemoveFromRoot(); + HoudiniTemplateMaterial = nullptr; + } + + // We no longer need the Houdini COP static mesh. + if (HoudiniCOPStaticMesh.IsValid()) + { + HoudiniCOPStaticMesh->RemoveFromRoot(); + HoudiniCOPStaticMesh = nullptr; + } + + // We no longer need the COP default material + if (HoudiniDefaultCOPMaterial.IsValid()) + { + HoudiniDefaultCOPMaterial->RemoveFromRoot(); + HoudiniDefaultCOPMaterial = nullptr; + } + + // We no longer need the Houdini default reference mesh + if (HoudiniDefaultReferenceMesh.IsValid()) + { + HoudiniDefaultReferenceMesh->RemoveFromRoot(); + HoudiniDefaultReferenceMesh = nullptr; + } + + // We no longer need the Houdini default reference mesh material + if (HoudiniDefaultReferenceMeshMaterial.IsValid()) + { + HoudiniDefaultReferenceMeshMaterial->RemoveFromRoot(); + HoudiniDefaultReferenceMeshMaterial = nullptr; + } + /* + // We no longer need Houdini digital asset used for loading bgeo files. + if (HoudiniBgeoAsset.IsValid()) + { + HoudiniBgeoAsset->RemoveFromRoot(); + HoudiniBgeoAsset = nullptr; + } + */ + + /* + // Stop Houdini Session sync if it is still running! + FProcHandle PreviousHESS = GetHESSProcHandle(); + if (FPlatformProcess::IsProcRunning(PreviousHESS)) + { + FPlatformProcess::TerminateProc(PreviousHESS, true); + } + */ + +#if WITH_EDITOR + // Unregister settings. + ISettingsModule * SettingsModule = FModuleManager::GetModulePtr("Settings"); + if (SettingsModule) + SettingsModule->UnregisterSettings("Project", "Plugins", "HoudiniEngine"); +#endif + + // Destroy the Unreal Object Input manager + FUnrealObjectInputManager::DestroySingleton(); + + // Do scheduler and thread clean up. + if (HoudiniEngineScheduler) + HoudiniEngineScheduler->Stop(); + + if (HoudiniEngineSchedulerThread) + { + //HoudiniEngineSchedulerThread->Kill( true ); + HoudiniEngineSchedulerThread->WaitForCompletion(); + + delete HoudiniEngineSchedulerThread; + HoudiniEngineSchedulerThread = nullptr; + } + + if ( HoudiniEngineScheduler ) + { + delete HoudiniEngineScheduler; + HoudiniEngineScheduler = nullptr; + } + + // Do manager clean up. + if (HoudiniEngineManager) + HoudiniEngineManager->StopHoudiniTicking(); + + if (HoudiniEngineManager) + { + delete HoudiniEngineManager; + HoudiniEngineManager = nullptr; + } + + // Perform HAPI finalization. + if ( FHoudiniApi::IsHAPIInitialized() ) + { + // Only cleanup if we're not using SessionSync! + if (!bEnableSessionSync) + FHoudiniApi::Cleanup(GetSession()); + + FHoudiniApi::CloseSession(GetSession()); + SessionStatus = EHoudiniSessionStatus::Invalid; + } + + FHoudiniApi::FinalizeHAPI(); + + FHoudiniEngine::HoudiniEngineInstance = nullptr; +} + +void +FHoudiniEngine::AddTask(const FHoudiniEngineTask & InTask) +{ + if ( HoudiniEngineScheduler ) + HoudiniEngineScheduler->AddTask(InTask); + + FScopeLock ScopeLock(&CriticalSection); + FHoudiniEngineTaskInfo TaskInfo; + TaskInfo.TaskType = InTask.TaskType; + TaskInfo.TaskState = EHoudiniEngineTaskState::Working; + + TaskInfos.Add(InTask.HapiGUID, TaskInfo); +} + +void +FHoudiniEngine::AddTaskInfo(const FGuid& InHapiGUID, const FHoudiniEngineTaskInfo & InTaskInfo) +{ + FScopeLock ScopeLock(&CriticalSection); + TaskInfos.Add(InHapiGUID, InTaskInfo); +} + +void +FHoudiniEngine::RemoveTaskInfo(const FGuid& InHapiGUID) +{ + FScopeLock ScopeLock(&CriticalSection); + TaskInfos.Remove(InHapiGUID); +} + +bool +FHoudiniEngine::RetrieveTaskInfo(const FGuid& InHapiGUID, FHoudiniEngineTaskInfo & OutTaskInfo) +{ + FScopeLock ScopeLock(&CriticalSection); + + if (TaskInfos.Contains(InHapiGUID)) + { + OutTaskInfo = TaskInfos[InHapiGUID]; + return true; + } + + return false; +} + +const FString & +FHoudiniEngine::GetLibHAPILocation() const +{ + return LibHAPILocation; +} + +const FString +FHoudiniEngine::GetHoudiniExecutable() +{ + FString HoudiniExecutable = TEXT("houdini"); + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (HoudiniRuntimeSettings) + { + switch (HoudiniRuntimeSettings->HoudiniExecutable) + { + case EHoudiniExecutableType::HRSHE_HoudiniFX: + HoudiniExecutable = TEXT("houdinifx"); + break; + + case EHoudiniExecutableType::HRSHE_HoudiniCore: + HoudiniExecutable = TEXT("houdinicore"); + break; + + case EHoudiniExecutableType::HRSHE_HoudiniIndie: + HoudiniExecutable = TEXT("hindie"); + break; + + default: + case EHoudiniExecutableType::HRSHE_Houdini: + HoudiniExecutable = TEXT("houdini"); + break; + + } + } + + return HoudiniExecutable; +} + +const HAPI_Session* +FHoudiniEngine::GetSession(int32 Index) const +{ + if (!Sessions.IsValidIndex(Index)) + { + return nullptr; + } + + return Sessions[Index].type == HAPI_SESSION_MAX + ? nullptr + : &Sessions[Index]; +} + +const EHoudiniSessionStatus& +FHoudiniEngine::GetSessionStatus() const +{ + return SessionStatus; +} + +bool +FHoudiniEngine::GetSessionStatusAndColor( + FString& OutStatusString, FLinearColor& OutStatusColor) +{ + OutStatusString = FString(); + OutStatusColor = FLinearColor::White; + + switch (SessionStatus) + { + case EHoudiniSessionStatus::NotStarted: + // Session not initialized yet + OutStatusString = TEXT("Houdini Engine Session - Not Started"); + OutStatusColor = FLinearColor::White; + break; + + case EHoudiniSessionStatus::Connected: + // Session successfully started + OutStatusString = TEXT("Houdini Engine Session READY"); + OutStatusColor = FLinearColor::Green; + break; + case EHoudiniSessionStatus::Stopped: + // Session stopped + OutStatusString = TEXT("Houdini Engine Session STOPPED"); + OutStatusColor = FLinearColor(1.0f, 0.5f, 0.0f); + break; + case EHoudiniSessionStatus::Failed: + // Session failed to be created/connected + OutStatusString = TEXT("Houdini Engine Session FAILED"); + OutStatusColor = FLinearColor::Red; + break; + case EHoudiniSessionStatus::Lost: + // Session Lost (HARS/Houdini Crash?) + OutStatusString = TEXT("Houdini Engine Session LOST"); + OutStatusColor = FLinearColor::Red; + break; + case EHoudiniSessionStatus::NoLicense: + // Failed to acquire a license + OutStatusString = TEXT("Houdini Engine Session FAILED - No License"); + OutStatusColor = FLinearColor::Red; + break; + case EHoudiniSessionStatus::Connecting: + // Failed to acquire a license + OutStatusString = TEXT("Houdini Engine Session CONNECTING"); + OutStatusColor = FLinearColor::White; + break; + case EHoudiniSessionStatus::None: + // Session type set to None + OutStatusString = TEXT("Houdini Engine Session DISABLED"); + OutStatusColor = FLinearColor::White; + break; + default: + case EHoudiniSessionStatus::Invalid: + OutStatusString = TEXT("Houdini Engine Session INVALID"); + OutStatusColor = FLinearColor::Red; + break; + } + + // Handle a few specific case for active session + if (SessionStatus == EHoudiniSessionStatus::Connected) + { + bool bPaused = !FHoudiniEngine::Get().IsCookingEnabled(); + bool bSSync = FHoudiniEngine::Get().IsSessionSyncEnabled(); + if (bPaused) + { + OutStatusString = TEXT("Houdini Engine Session PAUSED"); + OutStatusColor = FLinearColor::Yellow; + } + /* + else if (bSSync) + { + OutStatusString = TEXT("Houdini Engine Session Sync READY"); + OutStatusColor = FLinearColor::Blue; + } + */ + } + + return true; +} + + + + +void +FHoudiniEngine::SetSessionStatus(const EHoudiniSessionStatus& InSessionStatus) +{ + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (HoudiniRuntimeSettings->SessionType == EHoudiniRuntimeSettingsSessionType::HRSST_None) + { + // Check for none sessions first + SessionStatus = EHoudiniSessionStatus::None; + return; + } + + if (!bFirstSessionCreated) + { + // Don't change the status unless we've attempted to start the session once + SessionStatus = EHoudiniSessionStatus::NotStarted; + return; + } + + switch (InSessionStatus) + { + case EHoudiniSessionStatus::NotStarted: + case EHoudiniSessionStatus::NoLicense: + case EHoudiniSessionStatus::Lost: + case EHoudiniSessionStatus::None: + case EHoudiniSessionStatus::Invalid: + case EHoudiniSessionStatus::Connected: + case EHoudiniSessionStatus::Connecting: + { + SessionStatus = InSessionStatus; + } + break; + + case EHoudiniSessionStatus::Stopped: + { + // Only set to stop status if the session was valid + if (SessionStatus == EHoudiniSessionStatus::Connected) + SessionStatus = EHoudiniSessionStatus::Stopped; + } + break; + + case EHoudiniSessionStatus::Failed: + { + // Preserve No License / Lost status + if (SessionStatus != EHoudiniSessionStatus::NoLicense && SessionStatus != EHoudiniSessionStatus::Lost) + SessionStatus = EHoudiniSessionStatus::Failed; + } + break; + } +} + +HAPI_CookOptions +FHoudiniEngine::GetDefaultCookOptions() +{ + // Default CookOptions + HAPI_CookOptions CookOptions; + FHoudiniApi::CookOptions_Init(&CookOptions); + + CookOptions.curveRefineLOD = 8.0f; + CookOptions.clearErrorsAndWarnings = false; + CookOptions.maxVerticesPerPrimitive = 3; + CookOptions.splitGeosByGroup = false; + CookOptions.splitGeosByAttribute = false; + CookOptions.splitAttrSH = 0; + CookOptions.refineCurveToLinear = true; + CookOptions.handleBoxPartTypes = false; + CookOptions.handleSpherePartTypes = false; + CookOptions.splitPointsByVertexAttributes = false; + CookOptions.packedPrimInstancingMode = HAPI_PACKEDPRIM_INSTANCING_MODE_FLAT; + CookOptions.cookTemplatedGeos = true; + + return CookOptions; +} + +bool +FHoudiniEngine::StartSessionInternal( + const bool bStartAutomaticServer, + const bool bUseSessionSyncForAutomaticServer, + const float AutomaticServerTimeout, + const bool bShowNotificationsAndMessages, + const EHoudiniRuntimeSettingsSessionType SessionType, + const FString& ServerPipeName, + const int32 ServerPort, + const FString& ServerHost, + const int32 Index, + const int64 SharedMemoryBufferSize, + const bool bSharedMemoryCyclicBuffer, + const bool bEnableSharedMemoryDataTransfer) +{ + auto UpdatePathForServer = [&] + { + // Get the existing PATH env var + FString OrigPathVar = FPlatformMisc::GetEnvironmentVariable(TEXT("PATH")); + // Make sure we only extend the PATH once! + if (OrigPathVar.Contains(LibHAPILocation)) + return; + + // Modify our PATH so that HARC will find HARS.exe + const TCHAR* PathDelimiter = FPlatformMisc::GetPathVarDelimiter(); + FString ModifiedPath = +#if PLATFORM_MAC + // On Mac our binaries are split between two folders + LibHAPILocation + TEXT("/../Resources/bin") + PathDelimiter + +#endif + LibHAPILocation + PathDelimiter + OrigPathVar; + + FPlatformMisc::SetEnvironmentVar(TEXT("PATH"), *ModifiedPath); + }; + + auto DisablePerfMon = [] + { + // Disable the performance monitor to prevent random crashed when under heavy load. + // TODO: remove me when the race condition in H is fixed. + FPlatformMisc::SetEnvironmentVar(TEXT("HARS_DISABLE_PERFMON_LOGGING"), TEXT("1")); + }; + + + HAPI_ThriftServerOptions ServerOptions; + FMemory::Memzero(ServerOptions); + ServerOptions.autoClose = true; + ServerOptions.timeoutMs = AutomaticServerTimeout; + ServerOptions.sharedMemoryBufferSize = SharedMemoryBufferSize; + ServerOptions.sharedMemoryBufferType = bSharedMemoryCyclicBuffer ? HAPI_THRIFT_SHARED_MEMORY_RING_BUFFER : HAPI_THRIFT_SHARED_MEMORY_FIXED_LENGTH_BUFFER; + + HAPI_SessionInfo SessionInfo; + FHoudiniApi::SessionInfo_Init(&SessionInfo); + SessionInfo.enableSharedMemoryDataTransfer = bEnableSharedMemoryDataTransfer; + SessionInfo.sharedMemoryBufferSize = ServerOptions.sharedMemoryBufferSize; + + HAPI_Result SessionResult = HAPI_RESULT_FAILURE; + switch (SessionType) + { + case EHoudiniRuntimeSettingsSessionType::HRSST_Socket: + { + // Try to connect to an existing socket session first + SessionResult = FHoudiniApi::CreateThriftSocketSession( + &Sessions[Index], H_TCHAR_TO_UTF8(*ServerHost), ServerPort, &SessionInfo); + + // Start a session and try to connect to it if we failed + if (bStartAutomaticServer && SessionResult != HAPI_RESULT_SUCCESS) + { + if(!bUseSessionSyncForAutomaticServer) + { + // TODO: remove me when the race condition in H is fixed. + DisablePerfMon(); + + UpdatePathForServer(); + FHoudiniApi::StartThriftSocketServer(&ServerOptions, ServerPort, nullptr, nullptr); + + // We've started the server manually, disable session sync + bEnableSessionSync = false; + + SessionResult = FHoudiniApi::CreateThriftSocketSession( + &Sessions[Index], H_TCHAR_TO_UTF8(*ServerHost), ServerPort, &SessionInfo); + } + else + { + return false; + } + } + } + break; + + case EHoudiniRuntimeSettingsSessionType::HRSST_NamedPipe: + { + // Try to connect to an existing pipe session first + SessionResult = FHoudiniApi::CreateThriftNamedPipeSession( + &Sessions[Index], H_TCHAR_TO_UTF8(*ServerPipeName), &SessionInfo); + + // Start a session and try to connect to it if we failed + if (bStartAutomaticServer && SessionResult != HAPI_RESULT_SUCCESS) + { + if(!bUseSessionSyncForAutomaticServer) + { + // TODO: remove me when the race condition in H is fixed. + DisablePerfMon(); + + UpdatePathForServer(); + FHoudiniApi::StartThriftNamedPipeServer( + &ServerOptions, H_TCHAR_TO_UTF8(*ServerPipeName), nullptr, nullptr); + + // We've started the server manually, disable session sync + bEnableSessionSync = false; + + SessionResult = FHoudiniApi::CreateThriftNamedPipeSession(&Sessions[Index], H_TCHAR_TO_UTF8(*ServerPipeName), &SessionInfo); + } + else + { + return false; + } + } + } + break; + + case EHoudiniRuntimeSettingsSessionType::HRSST_MemoryBuffer: + { + // Try to connect to an existing pipe session first + SessionInfo.sharedMemoryBufferSize = ServerOptions.sharedMemoryBufferSize; + SessionInfo.sharedMemoryBufferType = ServerOptions.sharedMemoryBufferType; + + // Make sure memory buffer size make sense (between 1MB and 128GB) + if ((SessionInfo.sharedMemoryBufferSize < 1) || (SessionInfo.sharedMemoryBufferSize > 131072)) + { + HOUDINI_LOG_ERROR(TEXT("Invalid Shared Memory Buffer size!")); + SessionResult = HAPI_RESULT_FAILURE; + break; + } + + SessionResult = FHoudiniApi::CreateThriftSharedMemorySession( + &Sessions[Index], H_TCHAR_TO_UTF8(*ServerPipeName), &SessionInfo); + + // Start a session and try to connect to it if we failed + if (bStartAutomaticServer && SessionResult != HAPI_RESULT_SUCCESS) + { + if(!bUseSessionSyncForAutomaticServer) + { + // TODO: remove me when the race condition in H is fixed. + DisablePerfMon(); + + UpdatePathForServer(); + HAPI_ProcessId ServerProcID = -1; + HAPI_Result ServerResult = FHoudiniApi::StartThriftSharedMemoryServer( + &ServerOptions, H_TCHAR_TO_UTF8(*ServerPipeName), &ServerProcID, nullptr); + if(ServerResult == HAPI_RESULT_SUCCESS) + { + // We've started the server manually, disable session sync + bEnableSessionSync = false; + + SessionResult = FHoudiniApi::CreateThriftSharedMemorySession( + &Sessions[Index], H_TCHAR_TO_UTF8(*ServerPipeName), &SessionInfo); + } + } + else + { + return false; + } + } + } + break; + + case EHoudiniRuntimeSettingsSessionType::HRSST_None: + { + HOUDINI_LOG_MESSAGE(TEXT("Session type set to None, Cooking is disabled.")); + // Disable session sync + bEnableSessionSync = false; + } + break; + + case EHoudiniRuntimeSettingsSessionType::HRSST_InProcess: + { + // As of Unreal 4.19, InProcess sessions are not supported anymore + SessionResult = FHoudiniApi::CreateInProcessSession(&Sessions[Index], &SessionInfo); + // Disable session sync + bEnableSessionSync = false; + } + break; + + default: + { + HOUDINI_LOG_ERROR(TEXT("Unsupported Houdini Engine session type")); + // Disable session sync + bEnableSessionSync = false; + } + break; + } + + // Stop here if we used a none session + if (SessionType == EHoudiniRuntimeSettingsSessionType::HRSST_None) + return false; + + FHoudiniEngine::Get().SetFirstSessionCreated(true); + + if (SessionResult != HAPI_RESULT_SUCCESS || Sessions.IsEmpty() || !&Sessions[Index]) + { + // Disable session sync as well? + bEnableSessionSync = false; + + if (SessionType != EHoudiniRuntimeSettingsSessionType::HRSST_InProcess && !bUseSessionSyncForAutomaticServer) + { + FString ConnectionError = FHoudiniEngineUtils::GetConnectionError(); + if (!ConnectionError.IsEmpty() && bShowNotificationsAndMessages) + HOUDINI_LOG_ERROR(TEXT("Houdini Engine Session failed to connect - %s"), *ConnectionError); + } + + return false; + } + + return true; +} + +bool +FHoudiniEngine::StartSessionsInternal( + const bool bStartAutomaticServer, + const bool bUseSessionSyncForAutomaticServer, + const float AutomaticServerTimeout, + const bool bShowNotificationsAndMessages, + const EHoudiniRuntimeSettingsSessionType SessionType, + const int32 MaxNumSessions, + const FString& ServerPipeName, + const int32 ServerPort, + const FString& ServerHost, + const int64 SharedMemoryBufferSize, + const bool bSharedMemoryCyclicBuffer, + const bool bEnableSharedMemoryDataTransfer) +{ + // HAPI needs to be initialized + if (!FHoudiniApi::IsHAPIInitialized()) + return false; + + // Only start a new Session if we dont already have a valid one + if (HAPI_RESULT_SUCCESS == FHoudiniApi::IsSessionValid(GetSession())) + return true; + + // Set the HAPI_CLIENT_NAME environment variable to "unreal" + // We need to do this before starting HARS. + FPlatformMisc::SetEnvironmentVar(TEXT("HAPI_CLIENT_NAME"), TEXT("unreal")); + + // Set custom $HOME env var if it's been specified + FHoudiniEngineRuntimeUtils::SetHoudiniHomeEnvironmentVariable(); + + // Unless we automatically start the server, + // consider we're in SessionSync mode + bEnableSessionSync = true; + + // Clear the connection error before starting new sessions + if(SessionType != EHoudiniRuntimeSettingsSessionType::HRSST_None) + FHoudiniApi::ClearConnectionError(); + + + // Setup number of sessions. + int NumSessions = MaxNumSessions; + if(SessionType == EHoudiniRuntimeSettingsSessionType::HRSST_MemoryBuffer && MaxNumSessions > 1) + { + HOUDINI_LOG_MESSAGE(TEXT("Limiting Number of Sessions to 1 when using Shared Memory.")); + NumSessions = 1; + } + Sessions.Empty(NumSessions); + + bool bSuccess = false; + + // Create the sessions... + for (int32 i = 0; i < NumSessions; ++i) + { + Sessions.Emplace(); + + bSuccess = StartSessionInternal( + bStartAutomaticServer, + bUseSessionSyncForAutomaticServer, + AutomaticServerTimeout, + bShowNotificationsAndMessages, + SessionType, + ServerPipeName, + ServerPort, + ServerHost, + i, + SharedMemoryBufferSize, + bSharedMemoryCyclicBuffer, + bEnableSharedMemoryDataTransfer); + + if (!bSuccess) + { + Sessions.Empty(); + break; + } + } + + if (bStartAutomaticServer && bUseSessionSyncForAutomaticServer && !bSuccess) + { + // We hit this if we failed to connect to an existing server and we've enabled automatic + // SessionSync. + bSuccess = OpenSessionSync(true); + if(!bSuccess) + return false; + } + + // Update this session's license type + HOUDINI_CHECK_ERROR(FHoudiniApi::GetSessionEnvInt(GetSession(), HAPI_SESSIONENVINT_LICENSE, (int32*)&LicenseType)); + + return true; +} + +bool +FHoudiniEngine::SessionSyncConnect( + const EHoudiniRuntimeSettingsSessionType SessionType, + const int32 NumSessions, + const FString& ServerPipeName, + const FString& ServerHost, + const int32 ServerPort, + const int64 BufferSize, + const bool BufferCyclic) +{ + FScopeLock Lock(&CriticalSection); + + // HAPI needs to be initialized + if (!FHoudiniApi::IsHAPIInitialized()) + return false; + + // Only start a new Session if we dont already have a valid one + if (HAPI_RESULT_SUCCESS == FHoudiniApi::IsSessionValid(GetSession())) + return true; + + FHoudiniEngine::Get().SetFirstSessionCreated(true); + + // Consider the session failed as long as we dont connect + SetSessionStatus(EHoudiniSessionStatus::Failed); + + HAPI_Result SessionResult = HAPI_RESULT_FAILURE; + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + + switch (SessionType) + { + case EHoudiniRuntimeSettingsSessionType::HRSST_Socket: + { + // Try to connect to an existing socket session first + HAPI_SessionInfo SessionInfo; + FHoudiniApi::SessionInfo_Init(&SessionInfo); + + Sessions.Empty(NumSessions); + for (int32 i = 0; i < NumSessions; ++i) + { + Sessions.Emplace(); + SessionResult = FHoudiniApi::CreateThriftSocketSession( + &Sessions[i], H_TCHAR_TO_UTF8(*ServerHost), ServerPort, &SessionInfo); + if (SessionResult != HAPI_RESULT_SUCCESS) + break; + + } + + } + break; + + case EHoudiniRuntimeSettingsSessionType::HRSST_NamedPipe: + { + // Try to connect to an existing pipe session first + HAPI_SessionInfo SessionInfo; + FHoudiniApi::SessionInfo_Init(&SessionInfo); + + Sessions.Empty(NumSessions); + for (int32 i = 0; i < NumSessions; ++i) + { + Sessions.Emplace(); + SessionResult = FHoudiniApi::CreateThriftNamedPipeSession( + &Sessions[i], H_TCHAR_TO_UTF8(*ServerPipeName), &SessionInfo); + if (SessionResult != HAPI_RESULT_SUCCESS) + break; + } + } + break; + + case EHoudiniRuntimeSettingsSessionType::HRSST_MemoryBuffer: + { + // Try to connect to an existing pipe session first + HAPI_SessionInfo SessionInfo; + FHoudiniApi::SessionInfo_Init(&SessionInfo); + SessionInfo.sharedMemoryBufferSize = BufferSize; + SessionInfo.sharedMemoryBufferType = BufferCyclic ? HAPI_THRIFT_SHARED_MEMORY_RING_BUFFER : HAPI_THRIFT_SHARED_MEMORY_FIXED_LENGTH_BUFFER; + + Sessions.Empty(NumSessions); + for (int32 i = 0; i < NumSessions; ++i) + { + Sessions.Emplace(); + SessionResult = FHoudiniApi::CreateThriftSharedMemorySession( + &Sessions[i], H_TCHAR_TO_UTF8(*ServerPipeName), &SessionInfo); + if (SessionResult != HAPI_RESULT_SUCCESS) + break; + } + } + break; + + case EHoudiniRuntimeSettingsSessionType::HRSST_None: + case EHoudiniRuntimeSettingsSessionType::HRSST_InProcess: + default: + HOUDINI_LOG_ERROR(TEXT("Unsupported Houdini Engine Session Sync Type!!")); + bEnableSessionSync = false; + break; + } + + if (SessionResult != HAPI_RESULT_SUCCESS) + return false; + + // Enable session sync + bEnableSessionSync = true; + SetSessionStatus(EHoudiniSessionStatus::Connected); + + OnSessionConnected(); + + // Update this session's license type + HOUDINI_CHECK_ERROR(FHoudiniApi::GetSessionEnvInt( + GetSession(), HAPI_SESSIONENVINT_LICENSE, (int32*)&LicenseType)); + + // Update the default viewport sync settings + bSyncViewport = HoudiniRuntimeSettings->bSyncViewport; + bSyncHoudiniViewport = HoudiniRuntimeSettings->bSyncHoudiniViewport; + bSyncUnrealViewport = HoudiniRuntimeSettings->bSyncUnrealViewport; + + return true; +} + +bool +FHoudiniEngine::InitializeHAPISession() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngine::InitializeHAPISession); + + // The HAPI stubs needs to be initialized + if (!FHoudiniApi::IsHAPIInitialized()) + { + HOUDINI_LOG_ERROR(TEXT("Failed to initialize HAPI: The Houdini API stubs have not been properly initialized.")); + return false; + } + + // We need a Valid Session + if (HAPI_RESULT_SUCCESS != FHoudiniApi::IsSessionValid(GetSession())) + { + HOUDINI_LOG_ERROR(TEXT("Failed to initialize HAPI: The session is invalid.")); + return false; + } + + // Now, initialize HAPI with the new session + // We need to make sure HAPI version is correct. + int32 RunningEngineMajor = 0; + int32 RunningEngineMinor = 0; + int32 RunningEngineApi = 0; + + // Retrieve version numbers for running Houdini Engine. + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_ENGINE_MAJOR, &RunningEngineMajor); + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_ENGINE_MINOR, &RunningEngineMinor); + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_ENGINE_API, &RunningEngineApi); + + // Compare defined and running versions. + if (RunningEngineMajor != HAPI_VERSION_HOUDINI_ENGINE_MAJOR + || RunningEngineMinor != HAPI_VERSION_HOUDINI_ENGINE_MINOR) + { + // Major or minor HAPI version differs, stop here + HOUDINI_LOG_ERROR( + TEXT("Starting up the Houdini Engine module failed: built and running versions do not match.")); + HOUDINI_LOG_ERROR( + TEXT("Defined version: %d.%d.api:%d vs Running version: %d.%d.api:%d"), + HAPI_VERSION_HOUDINI_ENGINE_MAJOR, HAPI_VERSION_HOUDINI_ENGINE_MINOR, HAPI_VERSION_HOUDINI_ENGINE_API, + RunningEngineMajor, RunningEngineMinor, RunningEngineApi); + + // Display an error message + + // + return false; + + } + else if (RunningEngineApi != HAPI_VERSION_HOUDINI_ENGINE_API) + { + // Major/minor HAPIversions match, but only the API version differs, + // Allow the user to continue but warn him of possible instabilities + HOUDINI_LOG_WARNING( + TEXT("Starting up the Houdini Engine module: built and running versions do not match.")); + HOUDINI_LOG_WARNING( + TEXT("Defined version: %d.%d.api:%d vs Running version: %d.%d.api:%d"), + HAPI_VERSION_HOUDINI_ENGINE_MAJOR, HAPI_VERSION_HOUDINI_ENGINE_MINOR, HAPI_VERSION_HOUDINI_ENGINE_API, + RunningEngineMajor, RunningEngineMinor, RunningEngineApi); + HOUDINI_LOG_WARNING( + TEXT("This could cause instabilities and crashes when using the Houdini Engine plugin")); + } + + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + + // Default CookOptions + HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + + bool bUseCookingThread = true; + HAPI_Result Result = FHoudiniApi::Initialize( + GetSession(), + &CookOptions, + bUseCookingThread, + HoudiniRuntimeSettings->CookingThreadStackSize, + H_TCHAR_TO_UTF8(*HoudiniRuntimeSettings->HoudiniEnvironmentFiles), + H_TCHAR_TO_UTF8(*HoudiniRuntimeSettings->OtlSearchPath), + H_TCHAR_TO_UTF8(*HoudiniRuntimeSettings->DsoSearchPath), + H_TCHAR_TO_UTF8(*HoudiniRuntimeSettings->ImageDsoSearchPath), + H_TCHAR_TO_UTF8(*HoudiniRuntimeSettings->AudioDsoSearchPath)); + + if (Result == HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_MESSAGE(TEXT("Successfully intialized the Houdini Engine module.")); + } + else if (Result == HAPI_RESULT_ALREADY_INITIALIZED) + { + // Reused session? just notify the user + HOUDINI_LOG_MESSAGE(TEXT("Successfully intialized the Houdini Engine module - HAPI was already initialzed.")); + } + else + { + HOUDINI_LOG_ERROR( + TEXT("Houdini Engine API initialization failed: %s"), + *FHoudiniEngineUtils::GetErrorDescription(Result)); + + return false; + } + + // Let HAPI know we are running inside UE4 + FHoudiniApi::SetServerEnvString(GetSession(), HAPI_ENV_CLIENT_NAME, HAPI_UNREAL_CLIENT_NAME); + + if (bEnableSessionSync) + { + // Set the session sync infos if needed + UploadSessionSyncInfoToHoudini(); + + // Indicate that Session Sync is enabled + FString Notification = TEXT("Houdini Engine Session enabled."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine Session enabled.")); + } + + return true; +} + + +void +FHoudiniEngine::OnSessionLost() +{ + // Mark the session as invalid + Sessions.Empty(); + SetSessionStatus(EHoudiniSessionStatus::Lost); + + bEnableSessionSync = false; + HoudiniEngineManager->StopHoudiniTicking(); + + // This indicates that we likely have lost the session due to a crash in HARS/Houdini + FString Notification = TEXT("Houdini Engine Session lost!"); + FHoudiniEngineUtils::CreateSlateNotification(Notification, 2.0, 4.0); + + HOUDINI_LOG_ERROR(TEXT("Houdini Engine Session lost! This could be caused by a crash in HARS.")); + + PrintHoudiniCrashLog(); +} + +void +FHoudiniEngine::PrintHoudiniCrashLog() +{ + // Attempts to find the latest Houdini Crash log and output it to the console. + // Ignores logs older than MaxAgeInHours old. + + // Try TEMP, then TMP as a fallback. If neither, do nothing. + FString TempDir = FPlatformMisc::GetEnvironmentVariable(TEXT("TEMP")); + if(TempDir.IsEmpty()) + TempDir = FPlatformMisc::GetEnvironmentVariable(TEXT("TMP")); + + if(TempDir.IsEmpty()) + return; + + + // %TEMP%\houdini_temp + const FString HoudiniTempDir = FPaths::Combine(TempDir, TEXT("houdini_temp")); + if(!IFileManager::Get().DirectoryExists(*HoudiniTempDir)) + return; + + // Find crash logs + TArray CrashLogs; + IFileManager::Get().FindFilesRecursive(CrashLogs, *HoudiniTempDir, TEXT("crash*log.txt"), true, false); + + const int MaxAgeInHours = 1; + const FDateTime NowUtc = FDateTime::UtcNow(); + const FTimespan MaxAge = FTimespan::FromHours(MaxAgeInHours); + + FString LatestPath; + FDateTime LatestTime = FDateTime::MinValue(); + + FTimespan FileAge; + + for(const FString& Path : CrashLogs) + { + const FFileStatData Stat = IFileManager::Get().GetStatData(*Path); + if(!Stat.bIsValid) + { + continue; + } + + const FDateTime ModUtc = Stat.ModificationTime; + const FTimespan Age = NowUtc - ModUtc; + if(Age <= MaxAge && ModUtc > LatestTime) + { + LatestTime = ModUtc; + LatestPath = Path; + FileAge = Age; + } + } + + if(LatestPath.IsEmpty()) + return; + + FString Content; + if(!FFileHelper::LoadFileToString(Content, *LatestPath)) + return; + + const TCHAR* FormatString = TEXT("%Y-%m-%d %H:%M:%S"); + + HOUDINI_LOG_ERROR(TEXT("=== Found a Houdini Crash Log (Latest <%dh) ==="), MaxAgeInHours); + HOUDINI_LOG_ERROR(TEXT("File: %s"), *LatestPath); + HOUDINI_LOG_ERROR(TEXT("Time Now (UTC): %s"), *NowUtc.ToString(FormatString)); + HOUDINI_LOG_ERROR(TEXT("Modified (UTC): %s"), *LatestTime.ToString(FormatString)); + HOUDINI_LOG_ERROR(TEXT("File age: %s"), *FileAge.ToString()); + HOUDINI_LOG_ERROR(TEXT("=======================================\n%s"), *Content); +} + +bool +FHoudiniEngine::StopSession() +{ + FScopeLock Lock(&CriticalSection); + + return StopSessionInternal(); +} + +bool +FHoudiniEngine::StopSessionInternal() +{ + FScopeLock Lock(&CriticalSection); + + // HAPI needs to be initialized + if (!FHoudiniApi::IsHAPIInitialized()) + return false; + + // If the current session is valid, clean up and close the session + if (HAPI_RESULT_SUCCESS == FHoudiniApi::IsSessionValid(GetSession())) + { + // Only cleanup if we're not using SessionSync! + if(!bEnableSessionSync) + FHoudiniApi::Cleanup(GetSession()); + + FHoudiniApi::CloseSession(GetSession()); + } + + Sessions.Empty(); + SetSessionStatus(EHoudiniSessionStatus::Stopped); + bEnableSessionSync = false; + + HoudiniEngineManager->StopHoudiniTicking(); + + return true; +} + +bool +FHoudiniEngine::RestartSession(bool bShowNotificationsAndMessages) +{ + FScopeLock Lock(&CriticalSection); + + FString StatusText = TEXT("Starting the Houdini Engine session..."); + if (bShowNotificationsAndMessages) + FHoudiniEngine::Get().CreateTaskSlateNotification(FText::FromString(StatusText), true, 4.0f); + + // Make sure we stop the current session if it is still valid + bool bSuccess = false; + if (!StopSessionInternal()) + { + // StopSession returns false only if Houdini is not initialized + HOUDINI_LOG_ERROR(TEXT("Failed to restart the Houdini Engine session - HAPI Not initialized")); + } + else + { + // Try to reconnect/start a new session + SetSessionStatus(EHoudiniSessionStatus::Connecting); + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (!StartSessionsInternal( + HoudiniRuntimeSettings->bStartAutomaticServer, + HoudiniRuntimeSettings->bUseSessionSyncForAutomaticServer, + HoudiniRuntimeSettings->AutomaticServerTimeout, + bShowNotificationsAndMessages, + HoudiniRuntimeSettings->SessionType, + HoudiniRuntimeSettings->NumSessions, + HoudiniRuntimeSettings->ServerPipeName, + HoudiniRuntimeSettings->ServerPort, + HoudiniRuntimeSettings->ServerHost, + HoudiniRuntimeSettings->SharedMemoryBufferSize, + HoudiniRuntimeSettings->bSharedMemoryBufferCyclic, + HoudiniRuntimeSettings->bEnableSharedMemoryDataTransfer)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to restart the Houdini Engine session - Failed to start the new Session")); + SetSessionStatus(EHoudiniSessionStatus::Failed); + } + else + { + // Now initialize HAPI with this session + if (!InitializeHAPISession()) + { + if (bShowNotificationsAndMessages) + HOUDINI_LOG_ERROR(TEXT("Failed to restart the Houdini Engine session - Failed to initialize HAPI")); + SetSessionStatus(EHoudiniSessionStatus::Failed); + } + else + { + bSuccess = true; + SetSessionStatus(EHoudiniSessionStatus::Connected); + } + } + } + + OnSessionConnected(); + + // Start ticking only if we successfully started the session + if (bSuccess) + { + StartTicking(bShowNotificationsAndMessages); + return true; + } + else + { + StopTicking(bShowNotificationsAndMessages); + return false; + } +} + +void +FHoudiniEngine::OnSessionConnected() +{ + // This function is called whenever the plugin connects to a new session. + // It exists because Houdini Cookables need to know when this happens so they can invalidate + // their HAPI info, eg. node ids, left over from previous sessions. + + // Do the same thing for cookable + int32 NumCookable = FHoudiniEngineRuntime::Get().GetRegisteredHoudiniCookableCount(); + for (int32 nCurrent = 0; nCurrent < NumCookable; nCurrent++) + { + UHoudiniCookable* CurCookable = FHoudiniEngineRuntime::Get().GetRegisteredHoudiniCookableAt(nCurrent); + if (!CurCookable || !IsValid(CurCookable)) + continue; + + CurCookable->OnSessionConnected(); + } +} + +bool +FHoudiniEngine::CreateSession(const EHoudiniRuntimeSettingsSessionType& SessionType, FName OverrideServerPipeName) +{ + FScopeLock Lock(&CriticalSection); + + FString StatusText = TEXT("Create the Houdini Engine session..."); + FHoudiniEngine::Get().CreateTaskSlateNotification(FText::FromString(StatusText), true, 4.0f); + + // Make sure we stop the current session if it is still valid + bool bSuccess = false; + + // Try to reconnect/start a new session + constexpr bool bStartAutomaticServer = true; + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (!StartSessionsInternal( + bStartAutomaticServer, + false, // bUseSessionSyncForAutomaticServer + HoudiniRuntimeSettings->AutomaticServerTimeout, + true, // bShowNotificationsAndMessages + SessionType, + HoudiniRuntimeSettings->NumSessions, + OverrideServerPipeName == NAME_None ? HoudiniRuntimeSettings->ServerPipeName : OverrideServerPipeName.ToString(), + HoudiniRuntimeSettings->ServerPort, + HoudiniRuntimeSettings->ServerHost, + HoudiniRuntimeSettings->SharedMemoryBufferSize, + HoudiniRuntimeSettings->bSharedMemoryBufferCyclic, + HoudiniRuntimeSettings->bEnableSharedMemoryDataTransfer)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to start the Houdini Engine Session")); + SetSessionStatus(EHoudiniSessionStatus::Failed); + } + else + { + // Now initialize HAPI with this session + if (!InitializeHAPISession()) + { + HOUDINI_LOG_ERROR(TEXT("Failed to start the Houdini Engine session - Failed to initialize HAPI")); + SetSessionStatus(EHoudiniSessionStatus::Failed); + } + else + { + bSuccess = true; + SetSessionStatus(EHoudiniSessionStatus::Connected); + } + } + + // Notify our objects that we've connected to a new session. + OnSessionConnected(); + + // Start ticking only if we successfully started the session + if (bSuccess) + { + StartTicking(true); + return true; + } + else + { + StopTicking(true); + return false; + } +} + +bool +FHoudiniEngine::ConnectSession(bool bShowNotificationsAndMessages) +{ + FScopeLock Lock(&CriticalSection); + + FString StatusText = TEXT("Connecting to a Houdini Engine session..."); + if (bShowNotificationsAndMessages) + FHoudiniEngine::Get().CreateTaskSlateNotification(FText::FromString(StatusText), true, 4.0f); + + // Make sure we stop the current session if it is still valid + bool bSuccess = false; + + // Try to reconnect/start new sessions + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (!StartSessionsInternal( + false, + false, + HoudiniRuntimeSettings->AutomaticServerTimeout, + bShowNotificationsAndMessages, + HoudiniRuntimeSettings->SessionType, + HoudiniRuntimeSettings->NumSessions, + HoudiniRuntimeSettings->ServerPipeName, + HoudiniRuntimeSettings->ServerPort, + HoudiniRuntimeSettings->ServerHost, + HoudiniRuntimeSettings->SharedMemoryBufferSize, + HoudiniRuntimeSettings->bSharedMemoryBufferCyclic, + HoudiniRuntimeSettings->bEnableSharedMemoryDataTransfer)) + { + if(bShowNotificationsAndMessages) + HOUDINI_LOG_ERROR(TEXT("Failed to connect to the Houdini Engine Session")); + SetSessionStatus(EHoudiniSessionStatus::Failed); + } + else + { + // Now initialize HAPI with this session + if (!InitializeHAPISession()) + { + if (bShowNotificationsAndMessages) + HOUDINI_LOG_ERROR(TEXT("Failed to connect to the Houdini Engine session - Failed to initialize HAPI")); + SetSessionStatus(EHoudiniSessionStatus::Failed); + } + else + { + bSuccess = true; + SetSessionStatus(EHoudiniSessionStatus::Connected); + } + } + + // Notify our objects that we've connected to a new session. + OnSessionConnected(); + + // Start ticking only if we successfully started the session + if (bSuccess) + { + StartTicking(bShowNotificationsAndMessages); + return true; + } + else + { + StopTicking(bShowNotificationsAndMessages); + return false; + } +} + +void +FHoudiniEngine::StartTicking(bool bShowNotificationsAndMessages) +{ + // Finish the notification and display the results + if (bShowNotificationsAndMessages) + { + FString StatusText = TEXT("Houdini Engine session connected."); + FHoudiniEngine::Get().FinishTaskSlateNotification(FText::FromString(StatusText)); + } + + HoudiniEngineManager->StartHoudiniTicking(); +} + +void +FHoudiniEngine::StopTicking(bool bShowNotificationsAndMessages, bool bStopSession/*=true*/) +{ + if(bShowNotificationsAndMessages) + { + // Finish the notification and display the results + FString StatusText = TEXT("Failed to start the Houdini Engine session..."); + FHoudiniEngine::Get().FinishTaskSlateNotification(FText::FromString(StatusText)); + } + HoudiniEngineManager->StopHoudiniTicking(); + + if(bStopSession) + StopSessionInternal(); +} + +bool FHoudiniEngine::IsTicking() const +{ + if (!HoudiniEngineManager) + return false; + return HoudiniEngineManager->IsTicking(); +} + +bool +FHoudiniEngine::IsCookingEnabled() const +{ + return bEnableCookingGlobal; +} + +void +FHoudiniEngine::SetCookingEnabled(const bool& bInEnableCooking) +{ + bEnableCookingGlobal = bInEnableCooking; +} + +bool +FHoudiniEngine::GetFirstSessionCreated() const +{ + return bFirstSessionCreated; +} + +bool +FHoudiniEngine::CreateTaskSlateNotification( + const FText& InText, const bool& bForceNow, const float& NotificationExpire, const float& NotificationFadeOut) +{ +#if WITH_EDITOR + static double NotificationUpdateFrequency = 2.0f; + + // Check whether we want to display Slate cooking and instantiation notifications. + bool bDisplaySlateCookingNotifications = false; + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (HoudiniRuntimeSettings) + bDisplaySlateCookingNotifications = HoudiniRuntimeSettings->bDisplaySlateCookingNotifications; + + if (!bDisplaySlateCookingNotifications) + return false; + + if (!bForceNow) + { + if ((FPlatformTime::Seconds() - HapiNotificationStarted) < NotificationUpdateFrequency) + return false; + } + + if (!NotificationPtr.IsValid()) + { + FNotificationInfo Info(InText); + Info.bFireAndForget = false; + Info.FadeOutDuration = NotificationFadeOut; + Info.ExpireDuration = NotificationExpire; + TSharedPtr< FSlateDynamicImageBrush > HoudiniBrush = FHoudiniEngine::Get().GetHoudiniEngineLogoBrush(); + if (HoudiniBrush.IsValid()) + Info.Image = HoudiniBrush.Get(); + + NotificationPtr = FSlateNotificationManager::Get().AddNotification(Info); + } +#endif + + return true; +} + +bool +FHoudiniEngine::UpdateTaskSlateNotification(const FText& InText) +{ +#if WITH_EDITOR + // task is till running + // Just update the slate notification + TSharedPtr NotificationItem = NotificationPtr.Pin(); + if (NotificationItem.IsValid()) + NotificationItem->SetText(InText); +#endif + + return true; +} + +bool +FHoudiniEngine::FinishTaskSlateNotification(const FText& InText) +{ +#if WITH_EDITOR + if (NotificationPtr.IsValid()) + { + TSharedPtr NotificationItem = NotificationPtr.Pin(); + if (NotificationItem.IsValid()) + { + NotificationItem->SetText(InText); + NotificationItem->ExpireAndFadeout(); + + NotificationPtr.Reset(); + } + } +#endif + + return true; +} + +bool FHoudiniEngine::UpdateCookingNotification(const FText& InText, const bool bExpireAndFade) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngine::UpdateCookingNotification); + +#if WITH_EDITOR + TimeSinceLastPersistentNotification = 0.0; + + // Check whether we want to display Slate cooking and instantiation notifications. + bool bDisplaySlateCookingNotifications = false; + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (HoudiniRuntimeSettings) + bDisplaySlateCookingNotifications = HoudiniRuntimeSettings->bDisplaySlateCookingNotifications; + + if (!bDisplaySlateCookingNotifications) + return false; + + if (!CookingNotificationPtr.IsValid()) + { + FNotificationInfo Info(InText); + Info.bFireAndForget = false; + Info.FadeOutDuration = HAPI_UNREAL_NOTIFICATION_FADEOUT; + Info.ExpireDuration = HAPI_UNREAL_NOTIFICATION_EXPIRE; + const TSharedPtr< FSlateDynamicImageBrush > HoudiniBrush = FHoudiniEngine::Get().GetHoudiniEngineLogoBrush(); + if (HoudiniBrush.IsValid()) + Info.Image = HoudiniBrush.Get(); + + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngine::UpdateCookingNotification__AddNotification); + CookingNotificationPtr = FSlateNotificationManager::Get().AddNotification(Info); + } + } + + TSharedPtr NotificationItem = CookingNotificationPtr.Pin(); + if (NotificationItem.IsValid()) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngine::UpdateCookingNotification__UpdateNotification); + + // Update the persistent notification. + NotificationItem->SetText(InText); + + // Instead of setting the boolean and fading the next tick, just fade & reset now + if (bExpireAndFade) + { + NotificationItem->ExpireAndFadeout(); + CookingNotificationPtr.Reset(); + } + } + +#endif + + return true; +} + +void FHoudiniEngine::TickCookingNotification(const float DeltaTime) +{ + if (CookingNotificationPtr.IsValid() && DeltaTime > 0.0f) + TimeSinceLastPersistentNotification += DeltaTime; +} + +void +FHoudiniEngine::UpdateSessionSyncInfoFromHoudini() +{ + if (!bEnableSessionSync) + return; + + // Set the Session Sync settings to Houdini + HAPI_SessionSyncInfo SessionSyncInfo; + //FHoudiniApi::SessionSyncInfo_Create(&SessionSyncInfo); + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetSessionSyncInfo(GetSession(), &SessionSyncInfo)) + { + bCookUsingHoudiniTime = SessionSyncInfo.cookUsingHoudiniTime; + bSyncViewport = SessionSyncInfo.syncViewport; + } +} + +void +FHoudiniEngine::UploadSessionSyncInfoToHoudini() +{ + // No need to set sessionsync info if we're not using session sync + if (!bEnableSessionSync) + return; + + // Set the Session Sync settings to Houdini + HAPI_SessionSyncInfo SessionSyncInfo; + SessionSyncInfo.cookUsingHoudiniTime = bCookUsingHoudiniTime; + SessionSyncInfo.syncViewport = bSyncViewport; + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::SetSessionSyncInfo(GetSession(), &SessionSyncInfo)) + HOUDINI_LOG_WARNING(TEXT("Failed to set the SessionSync Infos.")); +} + +void +FHoudiniEngine::StartPDGCommandlet() +{ + if (HoudiniEngineManager) + HoudiniEngineManager->StartPDGCommandlet(); +} + +void +FHoudiniEngine::StopPDGCommandlet() +{ + if (HoudiniEngineManager) + HoudiniEngineManager->StopPDGCommandlet(); +} + +bool +FHoudiniEngine::IsPDGCommandletRunningOrConnected() +{ + if (HoudiniEngineManager) + return HoudiniEngineManager->IsPDGCommandletRunningOrConnected(); + return false; +} + +bool +FHoudiniEngine::IsPDGCommandletConnected() +{ + if(HoudiniEngineManager) + return HoudiniEngineManager->IsPDGCommandletConnected(); + return false; +} + +EHoudiniBGEOCommandletStatus +FHoudiniEngine::GetPDGCommandletStatus() +{ + if (HoudiniEngineManager) + return HoudiniEngineManager->GetPDGCommandletStatus(); + return EHoudiniBGEOCommandletStatus::NotStarted; +} + +void +FHoudiniEngine::UnregisterPostEngineInitCallback() +{ + if (PostEngineInitCallback.IsValid()) + FCoreDelegates::OnPostEngineInit.Remove(PostEngineInitCallback); +} + +bool +FHoudiniEngine::IsSyncWithHoudiniCookEnabled() const +{ + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + return HoudiniRuntimeSettings ? HoudiniRuntimeSettings->bSyncWithHoudiniCook : false; +} + +void +FHoudiniEngine::StartHAPIPerformanceMonitoring() +{ + // The HAPI stubs needs to be initialized + if (!FHoudiniApi::IsHAPIInitialized()) + { + HOUDINI_LOG_ERROR(TEXT("Failed to Start a HAPI Performance Monitoring: The Houdini API stubs have not been properly initialized.")); + return; + } + + // We need a Valid Session + if (HAPI_RESULT_SUCCESS != FHoudiniApi::IsSessionValid(GetSession())) + { + HOUDINI_LOG_ERROR(TEXT("Failed to Start a HAPI Performance Monitoring: The session is invalid.")); + return; + } + + // Stop the current session if it was already started + if (HAPIPerfomanceProfileID != -1) + StopHAPIPerformanceMonitoring(FString()); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::StartPerformanceMonitorProfile( + GetSession(), "HoudiniEngineForUnreal-HAPI-Profiling", &HAPIPerfomanceProfileID)) + { + HAPIPerfomanceProfileID = -1; + HOUDINI_LOG_ERROR(TEXT("Failed to Start a HAPI Performance Monitoring.")); + } + else + { + HOUDINI_LOG_MESSAGE(TEXT("HAPI Performance Monitoring started.")); + } +} + +void +FHoudiniEngine::StopHAPIPerformanceMonitoring(const FString& TraceDirectory) +{ + // The HAPI stubs needs to be initialized + if (!FHoudiniApi::IsHAPIInitialized()) + { + HOUDINI_LOG_ERROR(TEXT("Failed to Start a HAPI Performance Monitoring: The Houdini API stubs have not been properly initialized.")); + return; + } + + // We need a Valid Session + if (HAPI_RESULT_SUCCESS != FHoudiniApi::IsSessionValid(GetSession())) + { + HOUDINI_LOG_ERROR(TEXT("Failed to Start a HAPI Performance Monitoring: The session is invalid.")); + return; + } + + if (HAPIPerfomanceProfileID == -1) + { + HOUDINI_LOG_ERROR(TEXT("Failed to Stop a HAPI Performance Monitoring: no performance profiling session was started.")); + } + + // Build the filename + int32 Year, Month, DayOfWeek, Day, Hour, Min, Sec, MSec; + FPlatformTime::SystemTime(Year, Month, DayOfWeek, Day, Hour, Min, Sec, MSec); + FString FileName = !TraceDirectory.IsEmpty() ? TraceDirectory + TEXT("\\") : TEXT(""); + FileName += TEXT("HAPI_UE_") + FString::Printf(TEXT("%d%02d%02d_%02d%02d%02d"), Year, Month, Day, Hour, Min, Sec) + TEXT(".hperf"); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::StopPerformanceMonitorProfile( + GetSession(), HAPIPerfomanceProfileID, H_TCHAR_TO_UTF8(*FileName))) + { + HAPIPerfomanceProfileID = -1; + HOUDINI_LOG_ERROR(TEXT("Failed to Stop HAPI Performance Monitoring.")); + } + else + { + HOUDINI_LOG_MESSAGE(TEXT("HAPI Performance Monitoring saved - %s."), *FPaths::ConvertRelativePathToFull(FileName)); + } +} + + +FString +FHoudiniEngine::RegisterNewHoudiniAssetEditor() +{ + FString Identifier = TEXT("HoudiniAssetEditor"); +#if WITH_EDITOR + if (HoudiniAssetEditorIdentifiers.Num() <= 0) + { + HoudiniAssetEditorIdentifiers.Add(0); + return Identifier; + } + + // Find the next available index + int32 Idx = 0; + for (auto CurId : HoudiniAssetEditorIdentifiers) + { + if (CurId == Idx) + { + // ID is taken - keep looking + Idx++; + continue; + } + else + { + // We found an available ID - return + break; + } + } + + HoudiniAssetEditorIdentifiers.Add(Idx); + if(Idx > 0) + Identifier += FString::FromInt(Idx); +#endif + return Identifier; +} + +void +FHoudiniEngine::UnRegisterHoudiniAssetEditor(const FString& InIdentifier) +{ + // Extract the ID from the string + FString StringID = InIdentifier.RightChop(18); + int32 ID = StringID.IsEmpty() ? 0 : FCString::Atoi(*StringID); + + // Remove the Id from the registered asset editor array + for (int32 Idx = HoudiniAssetEditorIdentifiers.Num() - 1; Idx >= 0; Idx--) + { + if (HoudiniAssetEditorIdentifiers[Idx] != ID) + continue; + + HoudiniAssetEditorIdentifiers.RemoveAt(Idx); + //break; + } +} + +TArray +FHoudiniEngine::GetAllHoudiniAssetEditorIdentifier() +{ + TArray IDArray; + FString BaseIdentifier = TEXT("HoudiniAssetEditor"); + + for (auto CurId : HoudiniAssetEditorIdentifiers) + { + FString CurIDAsString = CurId == 0 ? BaseIdentifier : BaseIdentifier + FString::FromInt(CurId); + IDArray.Add(FName(*CurIDAsString)); + } + + return IDArray; +} + +bool +FHoudiniEngine::OpenSessionSync(bool bWaitForCompletion) +{ + //if (!FHoudiniEngine::IsInitialized()) + // return; + + if(!FHoudiniEngine::Get().StopSession()) + { + // StopSession returns false only if Houdini is not initialized + HOUDINI_LOG_ERROR(TEXT("Failed to start Session Sync - HAPI Not initialized")); + return false; + } + + // Get the runtime settings to get the session/type and settings + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + + EHoudiniRuntimeSettingsSessionType SessionType = HoudiniRuntimeSettings->SessionType; + FString ServerPipeName = HoudiniRuntimeSettings->ServerPipeName; + int32 ServerPort = HoudiniRuntimeSettings->ServerPort; + int64 BufferSize = HoudiniRuntimeSettings->SharedMemoryBufferSize; + bool BufferCyclic = HoudiniRuntimeSettings->bSharedMemoryBufferCyclic; + bool SharedMemDataTransfer = HoudiniRuntimeSettings->bEnableSharedMemoryDataTransfer; + + FString SessionSyncArgs = TEXT("-hess="); + if(SessionType == EHoudiniRuntimeSettingsSessionType::HRSST_NamedPipe) + { + // Add the -hess=pipe:hapi argument + SessionSyncArgs += TEXT("pipe:") + ServerPipeName; + } + else if(SessionType == EHoudiniRuntimeSettingsSessionType::HRSST_Socket) + { + // Add the -hess=port:9090 argument + SessionSyncArgs += TEXT("port:") + FString::FromInt(ServerPort); + } + else if(SessionType == EHoudiniRuntimeSettingsSessionType::HRSST_MemoryBuffer) + { + // -hess=shared:TYPE:SIZE:NAME + // TYPE specifies the shared memory buffer type. (ring, fixed). + // SIZE specifies the size of the shared memory buffer in megabytes (MB). + // NAME specifies the name of the shared memory. Different sessions must have a unique name. + FString BufferType = BufferCyclic ? TEXT("ring") : TEXT("fixed"); + SessionSyncArgs += TEXT("shared:") + BufferType + TEXT(":") + FString::FromInt(BufferSize) + TEXT(":") + ServerPipeName; + } + else + { + // Invalid session type + HOUDINI_LOG_ERROR(TEXT("Failed to start Session Sync - Invalid session type")); + return false; + } + +#if PLATFORM_MAC || PLATFORM_LINUX + // Houdini forks into the background by default on macOS and Linux +// so we need to explicitly tell it to not fork. + SessionSyncArgs += TEXT(" -foreground"); +#endif + + // Add a slate notification + FString Notification = TEXT("Opening Houdini Session Sync..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // ... and a log message + HOUDINI_LOG_MESSAGE(TEXT("Opening Houdini Session Sync.")); + + // Only launch Houdini in Session sync if we havent started it already! + FProcHandle PreviousHESS = FHoudiniEngine::Get().GetHESSProcHandle(); + if(!FPlatformProcess::IsProcRunning(PreviousHESS)) + { + // Start houdini with the -hess commandline args + const FString ThisLibHAPILocation = FHoudiniEngine::Get().GetLibHAPILocation(); +#if PLATFORM_MAC + const FString HoudiniExeLocationRelativeToLibHAPI = TEXT("/../Resources/bin"); +#elif PLATFORM_LINUX + const FString HoudiniExeLocationRelativeToLibHAPI = TEXT("/../bin"); +#elif PLATFORM_WINDOWS + const FString HoudiniExeLocationRelativeToLibHAPI; +#else + // Treat an unknown platform the same as Windows for now + const FString HoudiniExeLocationRelativeToLibHAPI; +#endif + + // Set custom $HOME env var if it's been specified + FHoudiniEngineRuntimeUtils::SetHoudiniHomeEnvironmentVariable(); + + FString HoudiniExecutable = FHoudiniEngine::Get().GetHoudiniExecutable(); + FString HoudiniLocation = ThisLibHAPILocation + HoudiniExeLocationRelativeToLibHAPI + TEXT("/") + HoudiniExecutable; + HOUDINI_LOG_MESSAGE(TEXT("Path to houdini executable: %s"), *HoudiniLocation); + FProcHandle HESSHandle = FPlatformProcess::CreateProc( + *HoudiniLocation, + *SessionSyncArgs, + true, false, false, + nullptr, 0, + *FPlatformProcess::GetCurrentWorkingDirectory(), + nullptr, nullptr); + + if(!HESSHandle.IsValid()) + { + // Try with the steam version executable instead + HoudiniLocation = ThisLibHAPILocation + HoudiniExeLocationRelativeToLibHAPI + TEXT("/hindie.steam"); + HOUDINI_LOG_MESSAGE(TEXT("Path to hindie.steam executable: %s"), *HoudiniLocation); + + HESSHandle = FPlatformProcess::CreateProc( + *HoudiniLocation, + *SessionSyncArgs, + true, false, false, + nullptr, 0, + *FPlatformProcess::GetCurrentWorkingDirectory(), + nullptr, nullptr); + + if(!HESSHandle.IsValid()) + { + HOUDINI_LOG_ERROR(TEXT("Failed to launch Houdini in Session Sync mode.")); + return false; + } + } + + // Keep track of the SessionSync ProcHandle + FHoudiniEngine::Get().SetHESSProcHandle(HESSHandle); + } + + if(!bWaitForCompletion) + { + Async(EAsyncExecution::TaskGraphMainThread, [SessionType, ServerPipeName, ServerPort, BufferSize, BufferCyclic]() + { + StartAndConnectToSessionSync(SessionType, ServerPipeName, ServerPort, BufferSize, BufferCyclic); + }); + } + else + { + StartAndConnectToSessionSync(SessionType, ServerPipeName, ServerPort, BufferSize, BufferCyclic); + } + return true; +} + +bool +FHoudiniEngine::StartAndConnectToSessionSync( + const EHoudiniRuntimeSettingsSessionType SessionType, + const FString& ServerPipeName, + const int32 ServerPort, + const int64 BufferSize, + const bool BufferCyclic) +{ + // Use a timeout to avoid waiting indefinitely for H to start in session sync mode + const double Timeout = 180.0; // 3min + const double StartTimestamp = FPlatformTime::Seconds(); + + const FString ServerHost = TEXT("localhost"); + constexpr int32 NumSessions = 1; + while(!FHoudiniEngine::Get().SessionSyncConnect( + SessionType, NumSessions, ServerPipeName, ServerHost, ServerPort, BufferSize, BufferCyclic)) + { + // Houdini might not be done loading, sleep for one second + FPlatformProcess::Sleep(.5f); + + // Check for license error + int32 HESSReturnCode; + FProcHandle HESSHandle = FHoudiniEngine::Get().GetHESSProcHandle(); + if(FPlatformProcess::GetProcReturnCode(HESSHandle, &HESSReturnCode)) + { + FString Notification = TEXT("Failed to start SessionSync..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + switch(HESSReturnCode) + { + case 3: + HOUDINI_LOG_ERROR(TEXT("Failed to start SessionSync - No licenses were available")); + FHoudiniEngine::Get().SetSessionStatus(EHoudiniSessionStatus::NoLicense); + return false; + break; + default: + HOUDINI_LOG_ERROR(TEXT("Failed to start SessionSync - Unknown error")); + FHoudiniEngine::Get().SetSessionStatus(EHoudiniSessionStatus::Failed); + return false; + break; + } + } + + // Check for the timeout + if(FPlatformTime::Seconds() - StartTimestamp > Timeout) + { + // ... and a log message + HOUDINI_LOG_ERROR(TEXT("Failed to start SessionSync - Timeout...")); + return false; + } + } + + // Initialize HAPI with this session + if(!FHoudiniEngine::Get().InitializeHAPISession()) + { + FHoudiniEngine::Get().StopTicking(true); + return false; + } + + // Notify all cookables that they need to instantiate in the new session + FHoudiniEngineUtils::MarkAllCookablesAsNeedInstantiation(); + + // Start ticking + FHoudiniEngine::Get().StartTicking(true); + + // Add a slate notification + FString Notification = TEXT("Successfully connected to Session Sync..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // ... and a log message + HOUDINI_LOG_MESSAGE(TEXT("Successfully connected to Session Sync...")); + + return true; +} + +#undef LOCTEXT_NAMESPACE + + +``` + +`Source/HoudiniEngine/Private/HoudiniEngine.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineTaskInfo.h" +#include "HoudiniRuntimeSettings.h" + +#include "Modules/ModuleInterface.h" + +#include "HoudiniEngine.generated.h" + +class FRunnableThread; +class FHoudiniEngineScheduler; +class FHoudiniEngineManager; +class UHoudiniAssetComponent; +class UStaticMesh; +class UMaterial; + +struct FSlateDynamicImageBrush; + +enum class EHoudiniBGEOCommandletStatus : uint8; + +UENUM() +enum class EHoudiniSessionStatus : int8 +{ + Invalid = -1, + + NotStarted, // Session not initialized yet + Connected, // Session successfully started + None, // Session type set to None + Stopped, // Session stopped + Failed, // Session failed to connect + Lost, // Session Lost (HARS/Houdini Crash?) + NoLicense, // Failed to acquire a license + Connecting, // Connecting to a session +}; + +// Not using the IHoudiniEngine interface for now +class HOUDINIENGINE_API FHoudiniEngine : public IModuleInterface +{ + public: + + FHoudiniEngine(); + + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + // Return singleton instance of Houdini Engine, used internally. + static FHoudiniEngine & Get(); + // Return true if singleton instance has been created. + static bool IsInitialized(); + + // Return the location of the currently loaded LibHAPI + virtual const FString& GetLibHAPILocation() const; + + // Return the houdini executable to use + static const FString GetHoudiniExecutable(); + + /** Gets the main session; equivalent to calling @c GetSession(0) */ + virtual const HAPI_Session* GetSession() const + { + return GetSession(0); + } + + virtual const HAPI_Session* GetSession(int32 Index) const; + + virtual const int32 GetNumSessions() const + { + return Sessions.Num(); + } + + virtual const EHoudiniSessionStatus& GetSessionStatus() const; + + bool GetSessionStatusAndColor(FString& OutStatusString, FLinearColor& OutStatusColor); + + virtual void SetSessionStatus(const EHoudiniSessionStatus& InSessionStatus); + + // Default cook options + static HAPI_CookOptions GetDefaultCookOptions(); + + // Stops, then creates a new session + bool RestartSession(bool bShowNotifications = true); + + // Connect to an existing HE session + bool ConnectSession(bool bShowNotificationsAndMessages); + + // Open Session Sync + bool OpenSessionSync(bool bWaitForCompletion); + + // Stops the current session + bool StopSession(); + + // Creates a session, start HARS + bool CreateSession(const EHoudiniRuntimeSettingsSessionType& SessionType, FName OverrideServerPipeName = NAME_None); + + + // Creates a session sync session + bool SessionSyncConnect( + const EHoudiniRuntimeSettingsSessionType SessionType, + const int32 NumSessions, + const FString& ServerPipeName, + const FString& ServerHost, + const int32 ServerPort, + const int64 BufferSize, + const bool BufferCyclic); + +private: + // Stops the current session. This function must be called inside a critical section, hence the "internal" name; the public + // functions ensure this lock is in effect. + bool StopSessionInternal(); + + // Creates a new session. This function must be called inside a critical section, hence the "internal" name; the public + // functions ensure this lock is in effect. + bool StartSessionInternal( + const bool bStartAutomaticServer, + const bool bUseSessionSyncForAutomaticServer, + const float AutomaticServerTimeout, + const bool bShowNotificationsAndMessages, + const EHoudiniRuntimeSettingsSessionType SessionType, + const FString& ServerPipeName, + const int32 ServerPort, + const FString& ServerHost, + const int32 Index, + const int64 SharedMemoryBufferSize, + const bool bSharedMemoryCyclicBuffer, + const bool bEnableSharedMemoryDataTransfer); + + // Creates a new sessions. This function must be called inside a critical section, hence the "internal" name; the public + // functions ensure this lock is in effect. + bool StartSessionsInternal( + const bool bStartAutomaticServer, + const bool bUseSessionSyncForAutomaticServer, + const float AutomaticServerTimeout, + const bool bShowNotificationsAndMessages, + const EHoudiniRuntimeSettingsSessionType SessionType, + const int32 MaxSessions, + const FString& ServerPipeName, + const int32 ServerPort, + const FString& ServerHost, + const int64 SharedMemoryBufferSize, + const bool bSharedMemoryCyclicBuffer, + const bool bEnableSharedMemoryDataTransfer); + +public: + // Notifies active objects that a new session has been connected to. Called when sessions are None or Connected. + void OnSessionConnected(); + + // Starts the HoudiniEngineManager ticking + void StartTicking(bool bShowNotificationsAndMessages); + // Stops the HoudiniEngineManager ticking and invalidate the session + void StopTicking(bool bShowNotificationsAndMessages, bool bStopSession=true); + + bool IsTicking() const; + + // Initialize HAPI + bool InitializeHAPISession(); + + // Indicate to the plugin that the session is now invalid (HAPI has likely crashed...) + void OnSessionLost(); + + static void PrintHoudiniCrashLog(); + + bool CreateTaskSlateNotification( + const FText& InText, + const bool& bForceNow = false, + const float& NotificationExpire = HAPI_UNREAL_NOTIFICATION_EXPIRE, + const float& NotificationFadeOut = HAPI_UNREAL_NOTIFICATION_FADEOUT); + + bool UpdateTaskSlateNotification(const FText& InText); + bool FinishTaskSlateNotification(const FText& InText); + + // Update persistent cooking notification if enabled in the settings. + bool UpdateCookingNotification(const FText& InText, const bool bExpireAndFade); + + // Update the time since the last persistent cooking notification update + void TickCookingNotification(float DeltaTime); + + void SetHapiNotificationStartedTime(const double& InTime) { HapiNotificationStarted = InTime; }; + + // Register task for execution. + virtual void AddTask(const FHoudiniEngineTask & InTask); + // Register task info. + virtual void AddTaskInfo(const FGuid& InHapiGUID, const FHoudiniEngineTaskInfo & InTaskInfo); + // Remove task info. + virtual void RemoveTaskInfo(const FGuid& InHapiGUID); + // Remove task info. + virtual bool RetrieveTaskInfo(const FGuid& InHapiGUID, FHoudiniEngineTaskInfo & OutTaskInfo); + // Register asset to the manager + //virtual void AddHoudiniAssetComponent(UHoudiniAssetComponent* HAC); + + // Indicates whether or not cooking is currently enabled + bool IsCookingEnabled() const; + // Sets whether or not cooking is currently enabled + void SetCookingEnabled(const bool& bInEnableCooking); + + // Check if we need to refresh UI when cooking is paused + bool HasUIFinishRefreshingWhenPausingCooking() const { return UIRefreshCountWhenPauseCooking <= 0; }; + + // Reset number of registered HACs when cooking is paused + void SetUIRefreshCountWhenPauseCooking(const int32& bInCount) { UIRefreshCountWhenPauseCooking = bInCount; }; + // Reduce the count by 1 when an HAC UI is refreshed when cooking is paused + void RefreshUIDisplayedWhenPauseCooking() { UIRefreshCountWhenPauseCooking -= 1; }; + + // Indicates whether or not the first attempt to create a Houdini session was made + bool GetFirstSessionCreated() const; + // Sets whether or not the first attempt to create a Houdini session was made + void SetFirstSessionCreated(const bool& bInStarted) { bFirstSessionCreated = bInStarted; }; + + bool IsSessionSyncEnabled() const { return bEnableSessionSync; }; + + bool IsSyncWithHoudiniCookEnabled() const; + + bool IsCookUsingHoudiniTimeEnabled() const { return bCookUsingHoudiniTime; }; + + bool IsSyncViewportEnabled() const { return bSyncViewport; }; + + bool IsSyncHoudiniViewportEnabled() const { return bSyncHoudiniViewport; }; + + bool IsSyncUnrealViewportEnabled() const { return bSyncUnrealViewport; }; + + // Helper function to update our session sync infos from Houdini's + void UpdateSessionSyncInfoFromHoudini(); + + // Helper function to update Houdini's Session sync infos from ours + void UploadSessionSyncInfoToHoudini(); + + // Sets whether or not viewport sync is enabled + void SetSyncViewportEnabled(const bool& bInSync) { bSyncViewport = bInSync; }; + // Sets whether or not we want to sync the houdini viewport to unreal's + void SetSyncHoudiniViewportEnabled(const bool& bInSync) { bSyncHoudiniViewport = bInSync; }; + // Sets whether or not we want to sync unreal's viewport to Houdini's + void SetSyncUnrealViewportEnabled(const bool& bInSync) { bSyncUnrealViewport = bInSync; }; + + // Returns the default Houdini Logo Static Mesh + virtual TWeakObjectPtr GetHoudiniLogoStaticMesh() const { return HoudiniLogoStaticMesh; }; + + // Returns either the default Houdini material or the default template material + virtual TWeakObjectPtr GetHoudiniDefaultMaterial(const bool& bIsTemplate) const { return bIsTemplate ? HoudiniTemplateMaterial : HoudiniDefaultMaterial; }; + + // Returns the default Houdini Logo Static Mesh + virtual TWeakObjectPtr GetHoudiniCOPStaticMesh() const { return HoudiniCOPStaticMesh; }; + + // Returns the default material for COPs + virtual TWeakObjectPtr GetHoudiniDefaultCOPMaterial() const { return HoudiniDefaultCOPMaterial; }; + + // Returns the default Houdini material + virtual TWeakObjectPtr GetHoudiniDefaultMaterial() const { return HoudiniDefaultMaterial; }; + // Returns the default template Houdini material + virtual TWeakObjectPtr GetHoudiniTemplatedMaterial() const { return HoudiniTemplateMaterial; }; + // Returns a shared Ptr to the houdini logo + TSharedPtr GetHoudiniLogoBrush() const { return HoudiniLogoBrush; }; + // Returns a shared Ptr to the houdini engine logo + TSharedPtr GetHoudiniEngineLogoBrush() const { return HoudiniEngineLogoBrush; }; + + // Returns the default Houdini reference mesh + virtual TWeakObjectPtr GetHoudiniDefaultReferenceMesh() const { return HoudiniDefaultReferenceMesh; }; + // Returns the default Houdini reference mesh material + virtual TWeakObjectPtr GetHoudiniDefaultReferenceMeshMaterial() const { return HoudiniDefaultReferenceMeshMaterial; }; + + const HAPI_License GetLicenseType() const { return LicenseType; }; + + const bool IsLicenseIndie() const { return (LicenseType == HAPI_LICENSE_HOUDINI_ENGINE_INDIE || LicenseType == HAPI_LICENSE_HOUDINI_INDIE); }; + const bool IsLicenseEducation() const { return (LicenseType == HAPI_LICENSE_HOUDINI_ENGINE_EDUCATION || LicenseType == HAPI_LICENSE_HOUDINI_EDUCATION); }; + + // Session Sync ProcHandle accessor + FProcHandle GetHESSProcHandle() const { return HESS_ProcHandle; }; + void SetHESSProcHandle(const FProcHandle& InProcHandle) { HESS_ProcHandle = InProcHandle; }; + + void StartPDGCommandlet(); + + void StopPDGCommandlet(); + + bool IsPDGCommandletRunningOrConnected(); + + bool IsPDGCommandletConnected(); + + EHoudiniBGEOCommandletStatus GetPDGCommandletStatus(); + + FHoudiniEngineManager* GetHoudiniEngineManager() { return HoudiniEngineManager; } + + const FHoudiniEngineManager* GetHoudiniEngineManager() const { return HoudiniEngineManager; } + + void UnregisterPostEngineInitCallback(); + + void StartHAPIPerformanceMonitoring(); + void StopHAPIPerformanceMonitoring(const FString& TraceDirectory); + + + FString RegisterNewHoudiniAssetEditor(); + void UnRegisterHoudiniAssetEditor(const FString& InIdentifier); + TArray GetAllHoudiniAssetEditorIdentifier(); + + TArray HoudiniAssetEditorIdentifiers; + + // Start and connect to Session Sync + static bool StartAndConnectToSessionSync( + const EHoudiniRuntimeSettingsSessionType SessionType, + const FString& ServerPipeName, + const int32 ServerPort, + const int64 BufferSize, + const bool BufferCyclic); + + private: + + // Singleton instance of Houdini Engine. + static FHoudiniEngine * HoudiniEngineInstance; + + // Location of libHAPI binary. + FString LibHAPILocation; + + TArray Sessions; + + // The Houdini Engine session's status + EHoudiniSessionStatus SessionStatus; + + // The type of HE license used by the current session + HAPI_License LicenseType; + + // Synchronization primitive. + FCriticalSection CriticalSection; + + // Map of task statuses. + TMap TaskInfos; + + // Thread used to execute the scheduler. + FRunnableThread * HoudiniEngineSchedulerThread; + // Scheduler used to schedule HAPI instantiation and cook tasks. + FHoudiniEngineScheduler * HoudiniEngineScheduler; + + // Thread used to execute the manager. + FRunnableThread * HoudiniEngineManagerThread; + // Scheduler used to monitor and process Houdini Asset Components + FHoudiniEngineManager * HoudiniEngineManager; + + // Process Handle for session sync + FProcHandle HESS_ProcHandle; + + // Is set to true when mismatch between defined and running HAPI versions is detected. + //bool bHAPIVersionMismatch; + + // Global cooking flag, used to pause HEngine while using the editor + bool bEnableCookingGlobal; + // Counter of HACs that need to be refreshed when pause cooking + int32 UIRefreshCountWhenPauseCooking; + + // Indicates that the first attempt to create a session has been done + // This is to delay the first "automatic" session creation for the first cook + // or instantiation rather than when the module started. + bool bFirstSessionCreated; + + // Indicates if the current session is a SessionSync one + bool bEnableSessionSync; + + // If true and we're in SessionSync, keeps the assets on the plugin side synchronized with changes on the Houdini side. + //bool bSyncWithHoudiniCook; + + // If true and we're in SessionSync, use the Houdini Timeline time to cook assets. + bool bCookUsingHoudiniTime; + + // If true and we're in Session Sync, the Houdini and Unreal viewport will be synchronized. + bool bSyncViewport; + // If true and we're in Session Sync, the Houdini viewport will be synchronized to Unreal's. + bool bSyncHoudiniViewport; + // If true and we're in Session Sync, the Unreal viewport will be synchronized to Houdini's. + bool bSyncUnrealViewport; + + // Static mesh used for Houdini logo rendering. + TWeakObjectPtr HoudiniLogoStaticMesh; + + // Material used as default material. + TWeakObjectPtr HoudiniDefaultMaterial; + + // Material used as default template material. + TWeakObjectPtr HoudiniTemplateMaterial; + + // Static mesh used for texture/COPs rendering. + TWeakObjectPtr HoudiniCOPStaticMesh; + + // Material used as default COP material. + TWeakObjectPtr HoudiniDefaultCOPMaterial; + + // Houdini logo brush. + TSharedPtr HoudiniLogoBrush; + // Houdini logo brush. + TSharedPtr HoudiniEngineLogoBrush; + + // Static mesh used for default mesh reference + TWeakObjectPtr HoudiniDefaultReferenceMesh; + + // Material used for default mesh reference + TWeakObjectPtr HoudiniDefaultReferenceMeshMaterial; + + FDelegateHandle PostEngineInitCallback; + + int HAPIPerfomanceProfileID; + +#if WITH_EDITOR + /** Notification used by this component. **/ + TWeakPtr NotificationPtr; + + /** Persistent cooking notification. **/ + bool bPersistentAllowExpiry; + TWeakPtr CookingNotificationPtr; + float TimeSinceLastPersistentNotification; + + /** Used to delay notification updates for HAPI asynchronous work. **/ + double HapiNotificationStarted; +#endif +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineAttributes.cpp`: + +```cpp +/* +* Copyright (c) <2024> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include "HoudiniEngineAttributes.h" +#include + +#include "HoudiniEngine.h" +#include "HoudiniEngineTimers.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniApi.h" + +static TAutoConsoleVariable CVarHoudiniEngineAccessorTimers( + TEXT("HoudiniEngine.AccessorStats"), + 0.0, + TEXT("When non-zero, the plugin will output stats about attributes. 1 == on, 2 == more detail.\n") +); + +#define THRIFT_MAX_CHUNKSIZE 10 * 1024 * 1024 + +struct FHoudiniRawAttributeData +{ + // This structure is used to store data before it is converted to a different type. + // + // Only one of the following fields is valid at a time. Can't use a union due to ambiguous + // destructors. + + TArray RawDataUint8; + TArray RawDataInt8; + TArray RawDataInt16; + TArray RawDataInt64; + TArray RawDataInt; + TArray RawDataFloat; + TArray RawDataDouble; + TArray RawDataStrings; + +}; + +FHoudiniHapiAccessor::FHoudiniHapiAccessor(HAPI_NodeId NodeId, HAPI_NodeId PartId, const char* Name) +{ + Init(NodeId, PartId, Name); +} + +FHoudiniHapiAccessor::FHoudiniHapiAccessor(HAPI_NodeId NodeId, HAPI_NodeId PartId, const TCHAR* Name) +{ + Init(NodeId, PartId, H_TCHAR_TO_UTF8(Name)); +} + +void FHoudiniHapiAccessor::Init(HAPI_NodeId InNodeId, HAPI_NodeId InPartId, const char* InName) +{ + NodeId = InNodeId; + PartId = InPartId; + + // Copy in the attribute name. Previously we just stored a copy of the pointer, but some + // code used tempories which fell out of scope, so copy it to be safe. + + int Length = strlen(InName); + AttributeName.SetNumZeroed(Length + 1); + for (int Index = 0; Index < (Length + 1); Index++) + { + AttributeName[Index] = InName[Index]; + } +} + +bool FHoudiniHapiAccessor::AddAttribute(HAPI_AttributeOwner InOwner, HAPI_StorageType InStorageType, int InTupleSize, int InCount, HAPI_AttributeInfo* OutAttrInfo) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniApi::AttributeInfo_Init(&AttrInfo); + AttrInfo.tupleSize = InTupleSize; + AttrInfo.count = InCount; + AttrInfo.exists = true; + AttrInfo.owner = InOwner; + AttrInfo.storage = InStorageType; + AttrInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const HAPI_Session * Session = FHoudiniEngine::Get().GetSession(); + auto bResult = FHoudiniApi::AddAttribute(Session, NodeId, PartId, AttributeName.GetData(), &AttrInfo); + + if (OutAttrInfo) + *OutAttrInfo = AttrInfo; + + return (bResult == HAPI_RESULT_SUCCESS); +} + + +bool +FHoudiniHapiAccessor::GetInfo(HAPI_AttributeInfo& OutAttributeInfo, const HAPI_AttributeOwner InOwner) +{ + H_SCOPED_FUNCTION_TIMER() + + FHoudiniApi::AttributeInfo_Init(&OutAttributeInfo); + + const auto GetInfoLambda = + [&](const HAPI_AttributeOwner Owner) -> bool + { + const HAPI_Result Result = FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + NodeId, + PartId, + AttributeName.GetData(), + Owner, + &OutAttributeInfo); + + if (Result != HAPI_RESULT_SUCCESS) + OutAttributeInfo.exists = false; + + return Result == HAPI_RESULT_SUCCESS && OutAttributeInfo.exists; + }; + + if (InOwner == HAPI_ATTROWNER_INVALID) + { + for (int32 OwnerIdx = 0; OwnerIdx < HAPI_ATTROWNER_MAX; ++OwnerIdx) + { + if (GetInfoLambda(static_cast(OwnerIdx))) + { + return true; + } + } + return false; + } + else + { + return GetInfoLambda(InOwner); + } +} + +FString FHoudiniHapiAccessor::ToString(int32 Number) +{ + return FString::Printf(TEXT("%d"), Number); +} + +FString FHoudiniHapiAccessor::ToString(int64 Number) +{ + return FString::Printf(TEXT("%lld"), Number); +} + +FString FHoudiniHapiAccessor::ToString(float Number) +{ + return FString::Printf(TEXT("%f"), Number); +} + +FString FHoudiniHapiAccessor::ToString(double Number) +{ + return FString::Printf(TEXT("%f"), Number); +} + +double FHoudiniHapiAccessor::ToDouble(const FString& Str) +{ + return FCString::Atof(*Str); +} + +int FHoudiniHapiAccessor::ToInt(const FString& Str) +{ + return FCString::Atoi(*Str); +} + +template +void FHoudiniHapiAccessor::Convert(const SrcType* SourceData, DestType* DestData, int IndexCount) +{ + for (int Index = 0; Index < IndexCount; Index++) + { + if constexpr (std::is_arithmetic_v) + { + if constexpr (std::is_same_v) + { + if constexpr (std::is_same_v || std::is_same_v) + DestData[Index] = static_cast(ToDouble(SourceData[Index])); + else + DestData[Index] = static_cast(ToInt(SourceData[Index])); + } + else + { + DestData[Index] = static_cast(SourceData[Index]); + } + } + else if constexpr (std::is_same_v) + { + if constexpr (std::is_same_v) + { + DestData[Index] = SourceData[Index]; + } + else + { + DestData[Index] = ToString(SourceData[Index]); + } + } + } +} + +template +void FHoudiniHapiAccessor::ConvertFromRawData(const FHoudiniRawAttributeData & RawData, DataType* Data, size_t IndexCount) +{ + if (RawData.RawDataUint8.Num() > 0) + { + Convert(RawData.RawDataUint8.GetData(), Data, IndexCount); + } + else if (RawData.RawDataInt8.Num() > 0) + { + Convert(RawData.RawDataInt8.GetData(), Data, IndexCount); + } + else if(RawData.RawDataInt16.Num() > 0) + { + Convert(RawData.RawDataInt16.GetData(), Data, IndexCount); + } + else if (RawData.RawDataInt.Num() > 0) + { + Convert(RawData.RawDataInt.GetData(), Data, IndexCount); + } + else if (RawData.RawDataFloat.Num() > 0) + { + Convert(RawData.RawDataFloat.GetData(), Data, IndexCount); + } + else if (RawData.RawDataDouble.Num() > 0) + { + Convert(RawData.RawDataDouble.GetData(), Data, IndexCount); + } + else if (RawData.RawDataStrings.Num() > 0) + { + Convert(RawData.RawDataStrings.GetData(), Data, IndexCount); + } + else + { + // no data. + } +} + +template +void FHoudiniHapiAccessor::ConvertToRawData(HAPI_StorageType StorageType, FHoudiniRawAttributeData& RawData, const DataType* Data, size_t IndexCount) +{ + switch(StorageType) + { + case HAPI_STORAGETYPE_UINT8: + RawData.RawDataUint8.SetNum(IndexCount); + Convert(Data, RawData.RawDataUint8.GetData(), IndexCount); + break; + + case HAPI_STORAGETYPE_INT8: + RawData.RawDataInt8.SetNum(IndexCount); + Convert(Data, RawData.RawDataInt8.GetData(), IndexCount); + break; + + case HAPI_STORAGETYPE_INT16: + RawData.RawDataInt16.SetNum(IndexCount); + Convert(Data, RawData.RawDataInt16.GetData(), IndexCount); + break; + + case HAPI_STORAGETYPE_INT: + RawData.RawDataInt.SetNum(IndexCount); + Convert(Data, RawData.RawDataInt.GetData(), IndexCount); + break; + + case HAPI_STORAGETYPE_INT64: + RawData.RawDataInt64.SetNum(IndexCount); + Convert(Data, RawData.RawDataInt64.GetData(), IndexCount); + break; + + case HAPI_STORAGETYPE_FLOAT: + RawData.RawDataFloat.SetNum(IndexCount); + Convert(Data, RawData.RawDataFloat.GetData(), IndexCount); + break; + + case HAPI_STORAGETYPE_FLOAT64: + RawData.RawDataDouble.SetNum(IndexCount); + Convert(Data, RawData.RawDataDouble.GetData(), IndexCount); + break; + + case HAPI_STORAGETYPE_STRING: + RawData.RawDataStrings.SetNum(IndexCount); + Convert(Data, RawData.RawDataStrings.GetData(), IndexCount); + default: + break; + } + +} + + +struct FHoudiniAttributeTask +{ + int RawIndex; + int Count; + const FHoudiniHapiAccessor* Accessor; + const HAPI_AttributeInfo * StorageInfo; + const HAPI_Session* Session; + bool bSuccess; + + static bool CanAbandon() { return false; } + static void Abandon() { } + TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FHoudiniAttributeTask, STATGROUP_ThreadPoolAsyncTasks); } + +}; + +int FHoudiniHapiAccessor::CalculateNumberOfSessions(const HAPI_AttributeInfo& AttributeInfo) const +{ + // GetStringBatchSize does not seem to function correctly with multisession. Arrays are slower with more than one session. + if (AttributeInfo.storage == HAPI_STORAGETYPE_STRING || IsHapiArrayType(AttributeInfo.storage)) + return 1; + + int NumSessions = FHoudiniEngine::Get().GetNumSessions(); + + if (!bAllowMultiThreading) + NumSessions = 1; + + return NumSessions; +} + +int FHoudiniHapiAccessor::CalculateNumberOfTasks(const HAPI_AttributeInfo& AttributeInfo) const +{ + int64 NumSessions = CalculateNumberOfSessions(AttributeInfo); + int64 TotalSize = GetHapiSize(AttributeInfo.storage) * AttributeInfo.tupleSize * AttributeInfo.count; + return CalculateNumberOfTasks(TotalSize, NumSessions); +} + +int FHoudiniHapiAccessor::CalculateNumberOfTasks(int64 SizeInBytes, int NumSessions) const +{ + // By default assume one task per session. + int64 NumTasks = NumSessions; + + // Check to see if each session has too much data. + int64 SizePerSession = SizeInBytes / NumTasks; + + constexpr int ThriftChunkSize = THRIFT_MAX_CHUNKSIZE; + + int64 MaxSize = ThriftChunkSize; + + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + if (HoudiniRuntimeSettings->SessionType == EHoudiniRuntimeSettingsSessionType::HRSST_MemoryBuffer) + { + constexpr int64 OverheadSize = 1 * 1024 * 1024; + MaxSize = HoudiniRuntimeSettings->SharedMemoryBufferSize * 1024 * 1024 - OverheadSize; + if (MaxSize <= 0) + { + HOUDINI_LOG_ERROR(TEXT("Shared memory buffer size is too small.")); + return 0; + } + } + + if (SizePerSession > MaxSize) + { + NumTasks = (SizeInBytes + MaxSize - 1) / MaxSize; + } + return static_cast(NumTasks); +} + +template bool FHoudiniHapiAccessor::GetAttributeDataViaSession(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, DataType* Results, int IndexStart, int IndexCount) const +{ + if (IndexCount == 0) + return true; + + HAPI_StorageType StorageType = GetHapiType(); + if (StorageType == GetTypeWithoutArray(AttributeInfo.storage)) + { + HOUDINI_CHECK_ERROR_RETURN(FetchHapiData(Session, AttributeInfo, Results, IndexStart, IndexCount), false); + } + else + { + // Fetch data in Hapi format then convert it. + FHoudiniRawAttributeData RawData; + if (!GetRawAttributeData(Session, AttributeInfo, RawData, IndexStart, IndexCount)) + return false; + + ConvertFromRawData(RawData, Results, IndexCount * AttributeInfo.tupleSize); + } + return true; +} + +template bool FHoudiniHapiAccessor::GetAttributeArrayData(HAPI_AttributeOwner Owner, TArray& Data, TArray& Sizes, int IndexStart, int IndexCount) +{ + const HAPI_Session * Session = FHoudiniEngine::Get().GetSession(); + + HAPI_AttributeInfo AttrInfo; + if (!GetInfo(AttrInfo, Owner)) + return false; + + return GetAttributeArrayData(AttrInfo, Data, Sizes, IndexStart, IndexCount); + + +} + + +template bool FHoudiniHapiAccessor::GetAttributeArrayData(const HAPI_AttributeInfo& AttributeInfo, TArray& DataArray, TArray& Sizes,int IndexStart, int IndexCount) +{ + if (IndexCount == -1) + IndexCount = AttributeInfo.count; + + if (!IsHapiArrayType(AttributeInfo.storage)) + { + HOUDINI_LOG_ERROR(TEXT("Not an array storage types")); + return false; + } + DataArray.SetNum(AttributeInfo.totalArrayElements); + Sizes.SetNum(IndexCount); + + HAPI_Result Result = FetchHapiDataArray(FHoudiniEngine::Get().GetSession(), AttributeInfo, DataArray.GetData(), Sizes.GetData(), IndexStart, IndexCount); + return Result == HAPI_RESULT_SUCCESS; +} + + +template bool FHoudiniHapiAccessor::GetAttributeData(const HAPI_AttributeInfo& AttributeInfo, TArray& Results, int IndexStart, int IndexCount) +{ + bool bDoTimings = CVarHoudiniEngineAccessorTimers.GetValueOnAnyThread() != 0.0; + + FHoudiniPerfTimer Timer(TEXT(""), bDoTimings); + Timer.Start(); + + if (!AttributeInfo.exists) + return false; + + if (IndexCount == -1) + IndexCount = AttributeInfo.count; + + int TotalCount; + bool bSuccess = false; + if (IsHapiArrayType(AttributeInfo.storage)) + { + if(!bCanBeArray) + { + HOUDINI_LOG_ERROR(TEXT("Attribute was array, but this was not allowed: %hs"), this->AttributeName.GetData()); + return false; + } + + if (IndexCount != 1) + { + // only fetch the first entry, or we'd end up with an array of arrays. + HOUDINI_LOG_ERROR(TEXT("Attribute was array, but index count was not 1: %hs"), this->AttributeName.GetData()); + return false; + } + + TArray Sizes; + Results.SetNum(AttributeInfo.totalArrayElements); + bSuccess = GetAttributeArrayData(AttributeInfo, Results, Sizes, 0, 1); + + } + else + { + TotalCount = IndexCount * AttributeInfo.tupleSize; + Results.SetNum(TotalCount); + + bSuccess = GetAttributeData(AttributeInfo, Results.GetData(), IndexStart, IndexCount); + + } + + Timer.Stop(); + if((Timer.GetTime() > 0.0) && bDoTimings) + { + FString AttrText = this->AttributeName.GetData(); + double SizeInMb = (sizeof(DataType) * AttributeInfo.tupleSize * IndexCount) / 1000000.0; + double MbPerSec = SizeInMb / Timer.GetTime(); + HOUDINI_LOG_MESSAGE(TEXT("Received %s, %.3f MB in %.3f seconds (%.3fMB/s)"), *AttrText, SizeInMb, Timer.GetTime(), MbPerSec); + } + + return bSuccess; +} + +template +bool FHoudiniHapiAccessor::GetAttributeData(const HAPI_AttributeInfo& AttributeInfo, DataType* Results, int IndexStart, int IndexCount) +{ + return GetAttributeDataMultiSession(AttributeInfo, Results, IndexStart, IndexCount); +} + + +template +bool FHoudiniHapiAccessor::ExecuteTasksWithSessions(TArray & Tasks, int NumSessions) +{ + // Create a pool of all available sessions. + TArray AvailableSessions; + TSet< TaskType* > ActiveTasks; + + for (int Session = 0; Session < NumSessions; Session++) + { + const HAPI_Session* SessionToAdd = FHoudiniEngine::Get().GetSession(Session); + AvailableSessions.Add(SessionToAdd); + } + + // No sessions. + if (AvailableSessions.IsEmpty()) + return false; + + for (int NextTaskToSTart = 0; NextTaskToSTart < Tasks.Num(); NextTaskToSTart++) + { + // Allocate sessions to tasks. We cannot assign a session to multiple tasks at the same + // time or there will be conflicts. So grab a free session from the pool + + auto& Task = Tasks[NextTaskToSTart]; + + if (AvailableSessions.IsEmpty()) + { + // This should never happen. + HOUDINI_LOG_ERROR(TEXT("Internal Error. No Available Houdini Sessions")); + return false; + } + else + { + Task.GetTask().Session = AvailableSessions.Pop(); + } + + Task.StartBackgroundTask(); + ActiveTasks.Add(&Task); + + if (AvailableSessions.IsEmpty()) + { + // We have to wait for a free session. + bool bFreedSessions = false; + while (!bFreedSessions) + { + for (auto It : ActiveTasks) + { + auto& ActiveTask = It; + if (ActiveTask->IsDone()) + { + ActiveTasks.Remove(ActiveTask); + AvailableSessions.Add(ActiveTask->GetTask().Session); + bFreedSessions = true; + break; + + } + } + } + } + } + + // Wait for completion of all tasks + bool bSuccess = true; + for (int Task = 0; Task < Tasks.Num(); Task++) + { + Tasks[Task].EnsureCompletion(); + bSuccess &= Tasks[Task].GetTask().bSuccess; + } + return bSuccess; +} + +struct FHoudiniHeightFieldGetTask : public FHoudiniAttributeTask +{ + float* Results; + + void DoWork() + { + bSuccess = Accessor->GetHeightFieldDataViaSession(Session, *StorageInfo, Results, RawIndex, Count); + } +}; + +bool FHoudiniHapiAccessor::GetHeightFieldDataViaSession(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, float* Results, int IndexStart, int IndexCount) const +{ + HAPI_Result Result = FHoudiniApi::GetHeightFieldData(Session, NodeId, PartId, Results, IndexStart, IndexCount); + + if(Result == HAPI_Result::HAPI_RESULT_SUCCESS) + return true; + + // HAPI returned an error, handle it gracefully. + HOUDINI_LOG_ERROR(TEXT("FHoudiniApi::GetHeightFieldData Failed: %s"), *FHoudiniEngineUtils::GetErrorDescription(Result)); + + return false; +} + +bool FHoudiniHapiAccessor::GetHeightFieldData(TArray& Results, int IndexCount) +{ + H_SCOPED_FUNCTION_TIMER(); + + int64 TotalSize = Results.Num() * sizeof(Results[0]); + int64 NumSessions = bAllowMultiThreading ? FHoudiniEngine::Get().GetNumSessions() : 1; + int NumTasks = CalculateNumberOfTasks(TotalSize, NumSessions); + // Task array. + TArray> Tasks; + Tasks.SetNum(NumTasks); + + Results.SetNumUninitialized(IndexCount); + + for(int64 TaskId = 0; TaskId < NumTasks; TaskId++) + { + // Fill a task, one per session. + FHoudiniHeightFieldGetTask& Task = Tasks[TaskId].GetTask(); + + int StartOffset = static_cast(IndexCount * TaskId / NumTasks); + int EndOffset = static_cast(IndexCount * (TaskId + 1) / NumTasks); + Task.Accessor = this; + Task.RawIndex = StartOffset; + Task.Results = Results.GetData() + StartOffset; + Task.Count = EndOffset - StartOffset; + Task.Session = nullptr; + } + + bool bSuccess = ExecuteTasksWithSessions(Tasks, NumSessions); + if (!bSuccess) + Results.Empty(); + + return bSuccess; +} + +template +struct FHoudiniAttributeGetTask : public FHoudiniAttributeTask +{ + DataType* Results; + + void DoWork() + { + bSuccess = Accessor->GetAttributeDataViaSession(Session, *StorageInfo, Results, RawIndex, Count); + } +}; + +template +bool FHoudiniHapiAccessor::GetAttributeDataMultiSession(const HAPI_AttributeInfo& AttributeInfo, DataType * Results, int IndexStart, int IndexCount) +{ + // This is the actual main function for getting data. + + H_SCOPED_FUNCTION_DYNAMIC_LABEL(FString::Printf(TEXT("FHoudiniAttributeAccessor::GetAttributeDataMultiSession (%s)"), ANSI_TO_TCHAR(AttributeName.GetData()))); + + if (!AttributeInfo.exists) + return false; + + if (IndexCount == -1) + IndexCount = AttributeInfo.count; + + int NumTasks = CalculateNumberOfTasks(AttributeInfo); + int NumSessions = CalculateNumberOfSessions(AttributeInfo); + // Task array. + TArray>> Tasks; + Tasks.SetNum(NumTasks); + + for(int TaskId = 0; TaskId < NumTasks; TaskId++) + { + // Fill a task, one per session. + FHoudiniAttributeGetTask & Task = Tasks[TaskId].GetTask(); + + int StartOffset = IndexCount * TaskId / NumTasks; + int EndOffset = IndexCount * (TaskId + 1) / NumTasks; + Task.Accessor = this; + Task.StorageInfo = &AttributeInfo; + Task.RawIndex = StartOffset + IndexStart; + Task.Results = Results + StartOffset * AttributeInfo.tupleSize; + Task.Count = EndOffset - StartOffset; + Task.Session = nullptr; + } + + bool bSuccess = ExecuteTasksWithSessions(Tasks, NumSessions); + + return bSuccess; +} + +template +struct FHoudiniAttributeSetTask : public FHoudiniAttributeTask +{ + const DataType* Input; + + void DoWork() + { + bSuccess = Accessor->SetAttributeDataViaSession(Session, *StorageInfo, Input, RawIndex, Count); + } + +}; + +template +bool FHoudiniHapiAccessor::SetAttributeData(const HAPI_AttributeInfo& AttributeInfo, const TArray& Data) +{ + return SetAttributeData(AttributeInfo, Data.GetData()); +} + +template +bool FHoudiniHapiAccessor::SetAttributeData(const HAPI_AttributeInfo& AttributeInfo, const DataType* Data, int IndexStart, int IndexCount) const +{ + return SetAttributeDataMultiSession(AttributeInfo, Data, IndexStart, IndexCount); +} + +template +bool FHoudiniHapiAccessor::SetAttributeDataMultiSession(const HAPI_AttributeInfo& AttributeInfo, const DataType* Data, int IndexStart, int IndexCount) const +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(FString::Printf(TEXT("FHoudiniAttributeAccessor::SetAttributeDataMultiSession (%s)"), ANSI_TO_TCHAR(AttributeName.GetData()))); + + bool bDoTiming = CVarHoudiniEngineAccessorTimers.GetValueOnAnyThread() != 0.0; + FHoudiniPerfTimer Timer(TEXT(""), bDoTiming); + Timer.Start(); + + if (IndexCount == -1) + IndexCount = AttributeInfo.count; + + int NumTasks = CalculateNumberOfTasks(AttributeInfo); + int NumSessions = CalculateNumberOfSessions(AttributeInfo); + + // Task array. + TArray>> Tasks; + Tasks.SetNum(NumTasks); + + for (int TaskId = 0; TaskId < NumTasks; TaskId++) + { + // Fill a task, one per session. + FHoudiniAttributeSetTask& Task = Tasks[TaskId].GetTask(); + + int StartOffset = IndexCount * TaskId / NumTasks; + int EndOffset = IndexCount * (TaskId + 1) / NumTasks; + Task.Accessor = this; + Task.StorageInfo = &AttributeInfo; + Task.RawIndex = StartOffset + IndexStart; + Task.Input = Data + StartOffset * AttributeInfo.tupleSize; + Task.Count = EndOffset - StartOffset; + Task.Session = nullptr; + } + + bool bSuccess = ExecuteTasksWithSessions(Tasks, NumSessions); + + Timer.Stop(); + if((Timer.GetTime() > 0.0) && bDoTiming) + { + FString AttrText = this->AttributeName.GetData(); + double SizeInMb = (sizeof(DataType) * AttributeInfo.tupleSize * IndexCount) / 1000000.0; + double MbPerSec = SizeInMb / Timer.GetTime(); + HOUDINI_LOG_MESSAGE(TEXT("Sent %s, %.3f MB in %.3f seconds (%.3fMB/s)"), *AttrText, SizeInMb, Timer.GetTime(), MbPerSec); + } + + + return bSuccess; +} + + +template +HAPI_Result FHoudiniHapiAccessor::SendHapiData(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, const DataType* Data, int StartIndex, int IndexCount) const +{ + TArray RunLengths; + + if (bCanRunLengthEncode && (AttributeInfo.tupleSize * IndexCount >= MinElementsForRunLengthEncoding)) + RunLengths = FHoudiniEngineUtils::RunLengthEncode(Data, AttributeInfo.tupleSize, IndexCount); + + HAPI_Result Result = HAPI_RESULT_FAILURE; + + bool bDoTimings = CVarHoudiniEngineAccessorTimers.GetValueOnAnyThread() == 2.0; + + FString TimerName = UTF8_TO_TCHAR(this->AttributeName.GetData()); + FHoudiniPerfTimer Timer(FString::Printf(TEXT("Transmission Time %s"), *TimerName), bDoTimings); + + Timer.Start(); + + if (RunLengths.Num() > 0) + { + // Send run length encoded data. + + for (int Index = 0; Index < RunLengths.Num(); Index++) + { + int StartRLEIndex = RunLengths[Index]; + int EndRLEIndex = IndexCount; + + int RLEStart = StartIndex + StartRLEIndex; + int RLECount = EndRLEIndex - StartRLEIndex; + + if (Index != RunLengths.Num() - 1) + EndRLEIndex = RunLengths[Index + 1]; + + const DataType* TupleValues = &Data[StartRLEIndex * AttributeInfo.tupleSize]; + + if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeFloatUniqueData(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, TupleValues, AttributeInfo.tupleSize, RLEStart, RLECount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeFloat64UniqueData(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, TupleValues, AttributeInfo.tupleSize, RLEStart, RLECount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeUInt8UniqueData(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, TupleValues, AttributeInfo.tupleSize, RLEStart, RLECount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeInt8UniqueData(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, TupleValues, AttributeInfo.tupleSize, RLEStart, RLECount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeInt16UniqueData(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, TupleValues, AttributeInfo.tupleSize, RLEStart, RLECount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeIntUniqueData(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, TupleValues, AttributeInfo.tupleSize, RLEStart, RLECount); + } + else if constexpr (std::is_same_v) + { + const HAPI_Int64* Hapi64Data = reinterpret_cast(TupleValues); // worked around for some Linux variations. + Result = FHoudiniApi::SetAttributeInt64UniqueData(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, Hapi64Data, AttributeInfo.tupleSize, RLEStart, RLECount); + } + else if constexpr (std::is_same_v) + { + TArray StringDataArray; + for (int StringIndex = 0; StringIndex < AttributeInfo.tupleSize; StringIndex++) + StringDataArray.Add(FHoudiniEngineUtils::ExtractRawString(TupleValues[Index])); + + Result = FHoudiniApi::SetAttributeStringUniqueData(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, StringDataArray[0], AttributeInfo.tupleSize, RLEStart, RLECount); + + // ExtractRawString allocates memory using malloc, free it! + FHoudiniEngineUtils::FreeRawStringMemory(StringDataArray); + + } + + if (Result != HAPI_RESULT_SUCCESS) + return Result; + } + Result = HAPI_RESULT_SUCCESS; + } + else + { + if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeFloatData(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, Data, StartIndex, IndexCount);; + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeFloat64Data(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, Data, StartIndex, IndexCount);; + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeUInt8Data(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, Data, StartIndex, IndexCount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeInt8Data(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, Data, StartIndex, IndexCount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeInt16Data(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, Data, StartIndex, IndexCount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeIntData(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, Data, StartIndex, IndexCount); + } + else if constexpr (std::is_same_v) + { + const HAPI_Int64* Hapi64Data = reinterpret_cast(Data); // worked around for some Linux variations. + Result = FHoudiniApi::SetAttributeInt64Data(Session, NodeId, PartId, AttributeName.GetData(), &AttributeInfo, Hapi64Data, StartIndex, IndexCount); + } + else if constexpr (std::is_same_v) + { + TArray StringDataArray; + for (int Index = 0; Index < IndexCount; Index++) + { + auto& CurrentString = Data[Index]; + + // Append the converted string to the string array + StringDataArray.Add(FHoudiniEngineUtils::ExtractRawString(CurrentString)); + } + + // Set all the attribute values once + Result = FHoudiniApi::SetAttributeStringData( + Session, + NodeId, PartId, AttributeName.GetData(), + &AttributeInfo, StringDataArray.GetData(), StartIndex, IndexCount); + + // ExtractRawString allocates memory using malloc, free it! + FHoudiniEngineUtils::FreeRawStringMemory(StringDataArray); + } + } + + Timer.Stop(); + + return Result; +} + +template +bool FHoudiniHapiAccessor::SetAttributeDataViaSession(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, const DataType* Data, int StartIndex, int IndexCount) const +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(FString::Printf(TEXT("FHoudiniAttributeAccessor::SetAttributeDataMultiSession (%s)"), ANSI_TO_TCHAR(AttributeName.GetData()))); + + if (IndexCount == 0) + return true; + + HAPI_StorageType StorageType = GetHapiType(); + if (StorageType == AttributeInfo.storage) + { + // No conversion is necessary so send the data directly. + SendHapiData(Session, AttributeInfo, Data, StartIndex, IndexCount); + } + else + { + // Fetch data in Hapi format then convert it. + FHoudiniRawAttributeData RawData; + + ConvertToRawData(AttributeInfo.storage, RawData, Data, IndexCount * AttributeInfo.tupleSize); + if (!SetRawAttributeData(Session, AttributeInfo, RawData, StartIndex, IndexCount)) + return false; + + + } + return true; +} + +template +bool SetAttributeData(const HAPI_AttributeInfo& AttributeInfo, const TArray& Data) +{ + return SetAttributeData(AttributeInfo, Data.GetData(), 0, Data.Num()); +} + +bool FHoudiniHapiAccessor::GetRawAttributeData(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, FHoudiniRawAttributeData& Data) +{ + return GetRawAttributeData(Session, AttributeInfo, Data, 0, AttributeInfo.count); +} + +template +HAPI_Result FHoudiniHapiAccessor::FetchHapiData(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, DataType* Data, int IndexStart, int IndexCount) const +{ + HAPI_AttributeInfo TempAttributeInfo = AttributeInfo; + + HAPI_Result Result = HAPI_RESULT_FAILURE; + + if constexpr (std::is_same_v) + { + Result = FHoudiniApi::GetAttributeFloatData(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, -1, Data, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::GetAttributeFloat64Data(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, -1, Data, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::GetAttributeUInt8Data(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, -1, Data, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::GetAttributeInt8Data(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, -1, Data, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::GetAttributeInt16Data(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, -1, Data, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::GetAttributeIntData(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, -1, Data, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + HAPI_Int64* Hapi64Data = reinterpret_cast(Data); // worked around for some Linux variations. + Result = FHoudiniApi::GetAttributeInt64Data(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, -1, Hapi64Data, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + TArray StringHandles; + StringHandles.SetNum(IndexCount * TempAttributeInfo.tupleSize); + + Result = FHoudiniApi::GetAttributeStringData(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, StringHandles.GetData(), IndexStart, IndexCount); + + if (Result == HAPI_RESULT_SUCCESS) + FHoudiniEngineString::SHArrayToFStringArray(StringHandles, Data, Session); + } + + if (!TempAttributeInfo.exists) + Result = HAPI_RESULT_FAILURE; + return Result; +} + + +template +HAPI_Result FHoudiniHapiAccessor::FetchHapiDataArray(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, DataType* Data, int* Sizes, int IndexStart, int IndexCount) const +{ + HAPI_AttributeInfo TempAttributeInfo = AttributeInfo; + HAPI_Result Result = HAPI_RESULT_FAILURE; + + if constexpr (std::is_same_v) + { + Result = FHoudiniApi::GetAttributeFloatArrayData(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, Data, TempAttributeInfo.totalArrayElements, Sizes, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::GetAttributeFloat64ArrayData(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, Data, TempAttributeInfo.totalArrayElements, Sizes, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::GetAttributeUInt8ArrayData(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, Data, TempAttributeInfo.totalArrayElements, Sizes, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::GetAttributeInt8ArrayData(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, Data, TempAttributeInfo.totalArrayElements, Sizes, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::GetAttributeInt16ArrayData(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, Data, TempAttributeInfo.totalArrayElements, Sizes, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::GetAttributeIntArrayData(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, Data, TempAttributeInfo.totalArrayElements, Sizes, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + HAPI_Int64* Hapi64Data = reinterpret_cast(Data); // worked around for some Linux variations. + Result = FHoudiniApi::GetAttributeInt64ArrayData(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, Hapi64Data, TempAttributeInfo.totalArrayElements, Sizes, IndexStart, IndexCount); + } + else if constexpr (std::is_same_v) + { + TArray StringHandles; + StringHandles.SetNum(TempAttributeInfo.totalArrayElements); + + Result = FHoudiniApi::GetAttributeStringArrayData(Session, NodeId, PartId, AttributeName.GetData(), &TempAttributeInfo, StringHandles.GetData(), AttributeInfo.totalArrayElements, Sizes, IndexStart, IndexCount); + + if (Result == HAPI_RESULT_SUCCESS) + FHoudiniEngineString::SHArrayToFStringArray(StringHandles, Data, Session); + } + + + + if (!TempAttributeInfo.exists) + Result = HAPI_RESULT_FAILURE; + return Result; +} + +bool FHoudiniHapiAccessor::GetRawAttributeData(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, FHoudiniRawAttributeData& Data, int IndexStart, int IndexCount) const +{ + HAPI_Result Result = HAPI_RESULT_FAILURE; + + int TotalCount = AttributeInfo.count * AttributeInfo.tupleSize; + + // Make a copy of the Attribute Info because it can be modified during fetching of data. + HAPI_AttributeInfo TempAttributeInfo = AttributeInfo; + + int NumElement = IndexCount * AttributeInfo.tupleSize; + int NumArrayElements = AttributeInfo.totalArrayElements; + + TArray Sizes; + + switch(AttributeInfo.storage) + { + case HAPI_STORAGETYPE_UINT8: + Data.RawDataUint8.SetNum(NumElement); + Result = FetchHapiData(Session, AttributeInfo, Data.RawDataUint8.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_INT8: + Data.RawDataInt8.SetNum(NumElement); + Result = FetchHapiData(Session, AttributeInfo, Data.RawDataInt8.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_INT16: + Data.RawDataInt16.SetNum(NumElement); + Result = FetchHapiData(Session, AttributeInfo, Data.RawDataInt16.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_INT64: + Data.RawDataInt64.SetNum(NumElement); + Result = FetchHapiData(Session, AttributeInfo, Data.RawDataInt64.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_INT: + Data.RawDataInt.SetNum(NumElement); + Result = FetchHapiData(Session, AttributeInfo, Data.RawDataInt.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_FLOAT: + Data.RawDataFloat.SetNum(NumElement); + Result = FetchHapiData(Session, AttributeInfo, Data.RawDataFloat.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_FLOAT64: + Data.RawDataDouble.SetNum(NumElement); + Result = FetchHapiData(Session, AttributeInfo, Data.RawDataDouble.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_STRING: + Data.RawDataStrings.SetNum(NumElement); + Result = FetchHapiData(Session, AttributeInfo, Data.RawDataStrings.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_UINT8_ARRAY: + Data.RawDataUint8.SetNum(NumArrayElements); + Sizes.SetNum(NumElement); + Result = FetchHapiDataArray(Session, AttributeInfo, Data.RawDataUint8.GetData(), Sizes.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_INT8_ARRAY: + Data.RawDataInt8.SetNum(NumArrayElements); + Sizes.SetNum(NumElement); + Result = FetchHapiDataArray(Session, AttributeInfo, Data.RawDataInt8.GetData(), Sizes.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_INT16_ARRAY: + Data.RawDataInt16.SetNum(NumArrayElements); + Sizes.SetNum(NumElement); + Result = FetchHapiDataArray(Session, AttributeInfo, Data.RawDataInt16.GetData(), Sizes.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_INT64_ARRAY: + Data.RawDataInt64.SetNum(NumArrayElements); + Sizes.SetNum(NumElement); + Result = FetchHapiDataArray(Session, AttributeInfo, Data.RawDataInt64.GetData(), Sizes.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_INT_ARRAY: + Data.RawDataInt.SetNum(NumArrayElements); + Sizes.SetNum(NumElement); + Result = FetchHapiDataArray(Session, AttributeInfo, Data.RawDataInt.GetData(), Sizes.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_FLOAT_ARRAY: + Data.RawDataFloat.SetNum(NumArrayElements); + Sizes.SetNum(NumElement); + Result = FetchHapiDataArray(Session, AttributeInfo, Data.RawDataFloat.GetData(), Sizes.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_FLOAT64_ARRAY: + Data.RawDataDouble.SetNum(NumArrayElements); + Sizes.SetNum(NumElement); + Result = FetchHapiDataArray(Session, AttributeInfo, Data.RawDataDouble.GetData(), Sizes.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_STRING_ARRAY: + Data.RawDataStrings.SetNum(NumArrayElements); + Sizes.SetNum(NumElement); + Result = FetchHapiDataArray(Session, AttributeInfo, Data.RawDataStrings.GetData(), Sizes.GetData(), IndexStart, IndexCount); + break; + + default: + return false; + } + + if (!TempAttributeInfo.exists) + Result = HAPI_RESULT_FAILURE; + + return Result == HAPI_RESULT_SUCCESS; +} + +bool FHoudiniHapiAccessor::SetRawAttributeData(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, FHoudiniRawAttributeData& Data, int IndexStart, int IndexCount) const +{ + HAPI_Result Result = HAPI_RESULT_FAILURE; + + int TotalCount = AttributeInfo.count * AttributeInfo.tupleSize; + + // Make a copy of the Attribute Info because it can be modified during fetching of data. + HAPI_AttributeInfo TempAttributeInfo = AttributeInfo; + + int NumElement = IndexCount * AttributeInfo.tupleSize; + + switch (AttributeInfo.storage) + { + case HAPI_STORAGETYPE_UINT8: + Result = SendHapiData(Session, AttributeInfo, Data.RawDataUint8.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_INT8: + Result = SendHapiData(Session, AttributeInfo, Data.RawDataInt8.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_INT16: + Result = SendHapiData(Session, AttributeInfo, Data.RawDataInt16.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_INT64: + Result = SendHapiData(Session, AttributeInfo, Data.RawDataInt64.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_INT: + Result = SendHapiData(Session, AttributeInfo, Data.RawDataInt.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_FLOAT: + Result = SendHapiData(Session, AttributeInfo, Data.RawDataFloat.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_FLOAT64: + Result = SendHapiData(Session, AttributeInfo, Data.RawDataDouble.GetData(), IndexStart, IndexCount); + break; + case HAPI_STORAGETYPE_STRING: + Result = SendHapiData(Session, AttributeInfo, Data.RawDataStrings.GetData(), IndexStart, IndexCount); + break; + default: + return false; + } + + if (!TempAttributeInfo.exists) + Result = HAPI_RESULT_FAILURE; + + return Result == HAPI_RESULT_SUCCESS; +} + + +template +HAPI_StorageType FHoudiniHapiAccessor::GetHapiType() +{ + if constexpr (std::is_same_v) + { + return HAPI_STORAGETYPE_FLOAT; + } + else if constexpr (std::is_same_v) + { + return HAPI_STORAGETYPE_FLOAT64; + } + else if constexpr (std::is_same_v) + { + return HAPI_STORAGETYPE_INT; + } + else if constexpr (std::is_same_v) + { + return HAPI_STORAGETYPE_INT8; + } + else if constexpr (std::is_same_v) + { + return HAPI_STORAGETYPE_INT16; + } + else if constexpr (std::is_same_v) + { + return HAPI_STORAGETYPE_UINT8; + } + else if constexpr (std::is_same_v) + { + return HAPI_STORAGETYPE_INT64; + } + else if constexpr (std::is_same_v) + { + return HAPI_STORAGETYPE_STRING; + } + else + { + return HAPI_STORAGETYPE_INVALID; + } +} + +template<> bool FHoudiniHapiAccessor::GetAttributeData(HAPI_AttributeOwner Owner, TArray& Results, int IndexStart, int IndexCount) +{ + HAPI_AttributeInfo AttrInfo; + if (!GetInfo(AttrInfo, Owner)) + return false; + + if (AttrInfo.tupleSize != 3) + { + HOUDINI_LOG_ERROR(TEXT("Tried to get a Vector3f, but tuple size is not 3")); + return false; + } + Results.SetNum(AttrInfo.count); + + return GetAttributeData(AttrInfo, (float *)Results.GetData(), IndexStart, IndexCount); +} + +template bool FHoudiniHapiAccessor::GetAttributeData(HAPI_AttributeOwner Owner, TArray& Results, int IndexStart, int IndexCount) +{ + HAPI_AttributeInfo AttrInfo; + if (!GetInfo(AttrInfo, Owner)) + return false; + + return GetAttributeData(AttrInfo, Results, IndexStart, IndexCount); +} + +template bool FHoudiniHapiAccessor::GetAttributeData(HAPI_AttributeOwner Owner, DataType* Results, int IndexStart, int IndexCount) +{ + HAPI_AttributeInfo AttrInfo; + if (!GetInfo(AttrInfo, Owner)) + return false; + return GetAttributeData(AttrInfo, Results, IndexStart, IndexCount); +} + +template bool FHoudiniHapiAccessor::GetAttributeFirstValue(HAPI_AttributeOwner Owner, DataType& Result) +{ + return GetAttributeData(Owner, &Result, 0, 1); +} + +template bool FHoudiniHapiAccessor::GetAttributeData(HAPI_AttributeOwner Owner, int MaxTuples, TArray& Results, int IndexStart, int IndexCount) +{ + HAPI_AttributeInfo AttrInfo; + if (!GetInfo(AttrInfo, Owner)) + return false; + + AttrInfo.tupleSize = MaxTuples; + + return GetAttributeData(AttrInfo, Results, IndexStart, IndexCount); + +} +template bool FHoudiniHapiAccessor::GetAttributeData(HAPI_AttributeOwner Owner, int MaxTuples, DataType* Results, int IndexStart, int IndexCount) +{ + HAPI_AttributeInfo AttrInfo; + if (!GetInfo(AttrInfo, Owner)) + return false; + + AttrInfo.tupleSize = MaxTuples; + + return GetAttributeData(AttrInfo, Results, IndexStart, IndexCount); +} + +bool FHoudiniHapiAccessor::SetAttributeStringMap(const HAPI_AttributeInfo& AttributeInfo, const FHoudiniEngineIndexedStringMap& InIndexedStringMap) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(FString::Printf(TEXT("FHoudiniAttributeAccessor::SetAttributeStringMap (%s)"), ANSI_TO_TCHAR(AttributeName.GetData()))); + + FHoudiniEngineRawStrings IndexedRawStrings = InIndexedStringMap.GetRawStrings(); + TArray IndexArray = InIndexedStringMap.GetIds(); + + HAPI_Result Result = FHoudiniApi::SetAttributeIndexedStringData( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, AttributeName.GetData(), + &AttributeInfo, IndexedRawStrings.RawStrings.GetData(), IndexedRawStrings.RawStrings.Num(), IndexArray.GetData(), 0, IndexArray.Num()); + + return Result == HAPI_RESULT_SUCCESS; +} + +template bool FHoudiniHapiAccessor::SetAttributeUniqueData(const HAPI_AttributeInfo& AttributeInfo, const DataType& Data) +{ + HAPI_Result Result = HAPI_RESULT_FAILURE; + + FHoudiniRawAttributeData RawData; + ConvertToRawData(AttributeInfo.storage, RawData, &Data, 1); + + switch(AttributeInfo.storage) + { + case HAPI_STORAGETYPE_FLOAT: + Result = FHoudiniApi::SetAttributeFloatUniqueData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + RawData.RawDataFloat.GetData(), AttributeInfo.tupleSize, 0, AttributeInfo.count); + break; + + case HAPI_STORAGETYPE_FLOAT64: + Result = FHoudiniApi::SetAttributeFloat64UniqueData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + RawData.RawDataDouble.GetData(), AttributeInfo.tupleSize, 0, AttributeInfo.count); + break; + + case HAPI_STORAGETYPE_INT8: + Result = FHoudiniApi::SetAttributeInt8UniqueData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + RawData.RawDataInt8.GetData(), AttributeInfo.tupleSize, 0, AttributeInfo.count); + break; + + case HAPI_STORAGETYPE_UINT8: + Result = FHoudiniApi::SetAttributeUInt8UniqueData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + RawData.RawDataUint8.GetData(), AttributeInfo.tupleSize, 0, AttributeInfo.count); + break; + + case HAPI_STORAGETYPE_INT16: + Result = FHoudiniApi::SetAttributeInt16UniqueData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + RawData.RawDataInt16.GetData(), AttributeInfo.tupleSize, 0, AttributeInfo.count); + break; + + case HAPI_STORAGETYPE_INT: + Result = FHoudiniApi::SetAttributeIntUniqueData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + RawData.RawDataInt.GetData(), AttributeInfo.tupleSize, 0, AttributeInfo.count); + break; + + case HAPI_STORAGETYPE_INT64: + { + HAPI_Int64* Hapi64Data = reinterpret_cast(RawData.RawDataInt64.GetData()); // worked around for some Linux variations. + Result = FHoudiniApi::SetAttributeInt64UniqueData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + Hapi64Data, AttributeInfo.tupleSize, 0, AttributeInfo.count); + break; + } + case HAPI_STORAGETYPE_STRING: + Result = FHoudiniApi::SetAttributeStringUniqueData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + TCHAR_TO_ANSI(*RawData.RawDataStrings[0]), 1, 0, AttributeInfo.count); + break; + default: + Result = HAPI_RESULT_FAILURE; + } + return Result == HAPI_RESULT_SUCCESS; + +} + +bool FHoudiniHapiAccessor::SetAttributeDictionary(const HAPI_AttributeInfo& InAttributeInfo, const TArray& JSONData) +{ + TArray RawStringData; + for (const FString& Data : JSONData) + { + RawStringData.Add(FHoudiniEngineUtils::ExtractRawString(Data)); + } + + // Send strings in smaller chunks due to their potential size + int32 ChunkSize = (THRIFT_MAX_CHUNKSIZE / 100) / InAttributeInfo.tupleSize; + + HAPI_Result Result = HAPI_RESULT_FAILURE; + if (InAttributeInfo.count > ChunkSize) + { + // Set the attributes in chunks + for (int32 ChunkStart = 0; ChunkStart < InAttributeInfo.count; ChunkStart += ChunkSize) + { + const int32 CurCount = InAttributeInfo.count - ChunkStart > ChunkSize ? ChunkSize : InAttributeInfo.count - ChunkStart; + + Result = FHoudiniApi::SetAttributeDictionaryData( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, AttributeName.GetData(), + &InAttributeInfo, RawStringData.GetData() + ChunkStart * InAttributeInfo.tupleSize, + ChunkStart, CurCount); + + if (Result != HAPI_RESULT_SUCCESS) + break; + } + } + else + { + // Set all the attribute values once + Result = FHoudiniApi::SetAttributeDictionaryData( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, AttributeName.GetData(), + &InAttributeInfo, RawStringData.GetData(), + 0, RawStringData.Num()); + } + + // ExtractRawString allocates memory using malloc, free it! + FHoudiniEngineUtils::FreeRawStringMemory(RawStringData); + + return Result == HAPI_RESULT_SUCCESS; +} + +template +bool FHoudiniHapiAccessor::SetAttributeArrayData(const HAPI_AttributeInfo& AttributeInfo, const TArray& DataArray, const TArray& SizesFixedArray) +{ + HAPI_Result Result = HAPI_RESULT_FAILURE; + + if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeFloatArrayData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + DataArray.GetData(), DataArray.Num(), SizesFixedArray.GetData(), 0, SizesFixedArray.Num()); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeFloat64ArrayData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + DataArray.GetData(), DataArray.Num(), SizesFixedArray.GetData(), 0, SizesFixedArray.Num()); + + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeIntArrayData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + DataArray.GetData(), DataArray.Num(), SizesFixedArray.GetData(), 0, SizesFixedArray.Num()); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeInt8ArrayData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + DataArray.GetData(), DataArray.Num(), SizesFixedArray.GetData(), 0, SizesFixedArray.Num()); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeInt16ArrayData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + DataArray.GetData(), DataArray.Num(), SizesFixedArray.GetData(), 0, SizesFixedArray.Num()); + } + else if constexpr (std::is_same_v) + { + Result = FHoudiniApi::SetAttributeUInt8ArrayData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + DataArray.GetData(), DataArray.Num(), SizesFixedArray.GetData(), 0, SizesFixedArray.Num()); + } + else if constexpr (std::is_same_v) + { + const HAPI_Int64 * Hapi64Data = reinterpret_cast(DataArray.GetData()); // worked around for some Linux variations. + Result = FHoudiniApi::SetAttributeInt64ArrayData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, AttributeName.GetData(), &AttributeInfo, + Hapi64Data, DataArray.Num(), SizesFixedArray.GetData(), 0, SizesFixedArray.Num()); + } + else if constexpr (std::is_same_v) + { + TArray StringDataArray; + for (const auto& CurrentString : DataArray) + { + // Append the converted string to the string array + StringDataArray.Add(FHoudiniEngineUtils::ExtractRawString(CurrentString)); + } + + // Set all the attribute values once + Result = FHoudiniApi::SetAttributeStringArrayData( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, AttributeName.GetData(), + &AttributeInfo, StringDataArray.GetData(), StringDataArray.Num(), + SizesFixedArray.GetData(), 0, SizesFixedArray.Num()); + + // ExtractRawString allocates memory using malloc, free it! + FHoudiniEngineUtils::FreeRawStringMemory(StringDataArray); + } + else + { + return HAPI_STORAGETYPE_INVALID; + } + return Result == HAPI_RESULT_SUCCESS; +} + + +bool FHoudiniHapiAccessor::IsHapiArrayType(HAPI_StorageType StorageType) +{ + switch(StorageType) + { + case HAPI_STORAGETYPE_UINT8_ARRAY: + case HAPI_STORAGETYPE_INT8_ARRAY: + case HAPI_STORAGETYPE_INT16_ARRAY: + case HAPI_STORAGETYPE_INT64_ARRAY: + case HAPI_STORAGETYPE_INT_ARRAY: + case HAPI_STORAGETYPE_FLOAT_ARRAY: + case HAPI_STORAGETYPE_FLOAT64_ARRAY: + case HAPI_STORAGETYPE_STRING_ARRAY: + return true; + default: + return false; + } +} + +int64 FHoudiniHapiAccessor::GetHapiSize(HAPI_StorageType StorageType) +{ + switch (StorageType) + { + case HAPI_STORAGETYPE_UINT8: + return sizeof(uint8); + case HAPI_STORAGETYPE_INT8: + return sizeof(int8); + case HAPI_STORAGETYPE_INT16: + return sizeof(int16); + case HAPI_STORAGETYPE_INT64: + return sizeof(int64); + case HAPI_STORAGETYPE_INT: + return sizeof(int32); + case HAPI_STORAGETYPE_FLOAT: + return sizeof(float); + case HAPI_STORAGETYPE_FLOAT64: + return sizeof(int64); + case HAPI_STORAGETYPE_STRING: + return sizeof(int); // Use int for string handles. + default: + return 1; + } +} + +HAPI_StorageType FHoudiniHapiAccessor::GetTypeWithoutArray(HAPI_StorageType StorageType) +{ + if (StorageType >= HAPI_STORAGETYPE_INT_ARRAY && StorageType <= HAPI_STORAGETYPE_DICTIONARY_ARRAY) + return static_cast(StorageType - HAPI_STORAGETYPE_INT_ARRAY); + else + return StorageType; +} + +bool FHoudiniHapiAccessor::GetAttributeStrings(HAPI_AttributeOwner Owner, FHoudiniEngineIndexedStringMap& StringArray, int IndexStart, int IndexCount) +{ + StringArray = {}; + + HAPI_AttributeInfo AttrInfo; + if (!GetInfo(AttrInfo, Owner)) + return false; + + return GetAttributeStrings(AttrInfo, StringArray, IndexStart, IndexCount); +} + + + +bool FHoudiniHapiAccessor::GetAttributeStrings(const HAPI_AttributeInfo& InAttrInfo, FHoudiniEngineIndexedStringMap& StringArray, int IndexStart, int IndexCount) +{ + StringArray = {}; + + int Count = IndexCount == -1 ? InAttrInfo.count : IndexCount; + + const HAPI_Session* Session = FHoudiniEngine::Get().GetSession(); + + TArray StringHandles; + StringHandles.SetNum(Count); + HAPI_AttributeInfo AttrInfo = InAttrInfo; + + if (AttrInfo.storage == HAPI_STORAGETYPE_STRING) + { + auto Result = FHoudiniApi::GetAttributeStringData(Session, NodeId, PartId, AttributeName.GetData(), &AttrInfo, StringHandles.GetData(), IndexStart, Count); + + if (Result != HAPI_RESULT_SUCCESS) + return false; + + StringArray.InitializeFromStringHandles(StringHandles); + } + else + { + TArray Strings; + Strings.SetNum(Count); + if (!GetAttributeDataViaSession(Session, InAttrInfo, Strings.GetData(), IndexStart, Count)) + return false; + + TMap HandleIndices; + StringArray.Ids.SetNum(Strings.Num()); + StringArray.Strings.Empty(); + + for(int Index = 0; Index < Strings.Num(); Index++) + { + int * StringIndex = HandleIndices.Find(Strings[Index]); + if (StringIndex) + { + StringArray.Ids[Index] = *StringIndex; + } + else + { + StringArray.Ids[Index] = StringArray.Strings.Num(); + HandleIndices.Add(Strings[Index]) = StringArray.Strings.Num(); + StringArray.Strings.Add(Strings[Index]); + } + } + + } + return true; +} + +TArray FHoudiniHapiAccessor::GetAttributeNames(HAPI_AttributeOwner Owner) +{ + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), NodeId, PartId, &PartInfo); + + + int AttrCount = PartInfo.attributeCounts[Owner]; + TArray StringHandles; + StringHandles.SetNum(AttrCount); + + FHoudiniApi::GetAttributeNames(FHoudiniEngine::Get().GetSession(), NodeId, PartId, Owner, StringHandles.GetData(), AttrCount); + + TArray Results; + Results.SetNum(StringHandles.Num()); + for(int Index = 0; Index < Results.Num(); Index++) + { + FHoudiniEngineString::ToFString(StringHandles[Index], Results[Index], FHoudiniEngine::Get().GetSession()); + } + return Results; +} + + +#define IMPLEMENT_HOUDINI_ACCESSOR(DATA_TYPE)\ + template HOUDINIENGINE_API bool FHoudiniHapiAccessor::GetAttributeFirstValue(HAPI_AttributeOwner Owner, DATA_TYPE& Result);\ + template HOUDINIENGINE_API bool FHoudiniHapiAccessor::GetAttributeData(HAPI_AttributeOwner Owner, TArray& Results, int IndexStart, int IndexCount);\ + template HOUDINIENGINE_API bool FHoudiniHapiAccessor::GetAttributeData(HAPI_AttributeOwner Owner, DATA_TYPE * Results, int IndexStart, int IndexCount);\ + template HOUDINIENGINE_API bool FHoudiniHapiAccessor::GetAttributeData(HAPI_AttributeOwner Owner, int TupleSize, TArray& Results, int IndexStart, int IndexCount);\ + template HOUDINIENGINE_API bool FHoudiniHapiAccessor::GetAttributeData(HAPI_AttributeOwner Owner, int TupleSize, DATA_TYPE * Results, int IndexStart, int IndexCount);\ + template HOUDINIENGINE_API bool FHoudiniHapiAccessor::GetAttributeData(const HAPI_AttributeInfo& AttributeInfo, TArray& Results, int IndexStart , int IndexCount);\ + template HOUDINIENGINE_API bool FHoudiniHapiAccessor::SetAttributeData(const HAPI_AttributeInfo& AttributeInfo, const DATA_TYPE* Data, int IndexStart, int IndexCount) const;\ + template HOUDINIENGINE_API bool FHoudiniHapiAccessor::SetAttributeDataViaSession(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, const DATA_TYPE* Data, int IndexStart, int IndexCount) const;\ + template HOUDINIENGINE_API bool FHoudiniHapiAccessor::SetAttributeData(const HAPI_AttributeInfo& AttributeInfo, const TArray& Data);\ + template HOUDINIENGINE_API bool FHoudiniHapiAccessor::SetAttributeUniqueData(const HAPI_AttributeInfo& AttributeInfo, const DATA_TYPE& Data);\ + template HOUDINIENGINE_API bool FHoudiniHapiAccessor::SetAttributeArrayData(const HAPI_AttributeInfo& InAttributeInfo, const TArray& InStringArray, const TArray& SizesFixedArray);\ + template HOUDINIENGINE_API bool FHoudiniHapiAccessor::GetAttributeArrayData(HAPI_AttributeOwner Owner, TArray& StringArray, TArray& SizesFixedArray, int IndexStart, int IndexCount); + +IMPLEMENT_HOUDINI_ACCESSOR(uint8); +IMPLEMENT_HOUDINI_ACCESSOR(int8); +IMPLEMENT_HOUDINI_ACCESSOR(int16); +IMPLEMENT_HOUDINI_ACCESSOR(int64); +IMPLEMENT_HOUDINI_ACCESSOR(int); +IMPLEMENT_HOUDINI_ACCESSOR(float); +IMPLEMENT_HOUDINI_ACCESSOR(double); +IMPLEMENT_HOUDINI_ACCESSOR(FString); + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineAttributes.h`: + +```h +/* +* Copyright (c) <2024> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniEngineRuntimeCommon.h" +#include "HAPI/HAPI_Common.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineString.h" + +class FHoudiniEngineIndexedStringMap; +struct FHoudiniRawAttributeData; + +struct HOUDINIENGINE_API FHoudiniHapiAccessor +{ + // Public data. Can be set directly or use convenience functions below. + + HAPI_NodeId NodeId = -1; + HAPI_PartId PartId = -1; + TArray AttributeName;// avoid std::string + bool bAllowTypeConversion = true; + bool bAllowMultiThreading = true; + bool bCanBeArray = false; + bool bCanRunLengthEncode = true; + int MinElementsForRunLengthEncoding = 100 * 1000; + + // Initialization functions. use these functions to initialize the accessor. + FHoudiniHapiAccessor(HAPI_NodeId NodeId, HAPI_NodeId PartId, const char* Name); + FHoudiniHapiAccessor(HAPI_NodeId NodeId, HAPI_NodeId PartId, const TCHAR* Name); + FHoudiniHapiAccessor() {} + void Init(HAPI_NodeId InNodeId, HAPI_NodeId InPartId, const char * InName); + + // Create the attribute. Will fill in OutAttrInfo if supplied - You probably want this because calling + // GetInfo() will fail if the node is not commited. + bool AddAttribute(HAPI_AttributeOwner Owner, HAPI_StorageType StorageType, int TupleSize, int Count, HAPI_AttributeInfo * OutAttrInfo = nullptr); + + // Get HAPI_AttributeInfo from the accessor. + bool GetInfo(HAPI_AttributeInfo& OutAttributeInfo, HAPI_AttributeOwner InOwner = HAPI_ATTROWNER_INVALID); + + // Templated functions to return data. + // + // If the attribute is a different type from the templated data, conversion is optionally performed (bAllowTypeConversion) + // If the attribute owner is HAPI_ATTROWNER_INVALID, all attribute owners are searched. + // An optional tuple size can be specified. + // Note these templates are explicitly defined in the .cpp file. + + template HOUDINIENGINE_API bool GetAttributeData(HAPI_AttributeOwner Owner, TArray& Results, int IndexStart =0, int IndexCount =-1); + template HOUDINIENGINE_API bool GetAttributeData(HAPI_AttributeOwner Owner, DataType* Results, int IndexStart = 0, int IndexCount = -1); + template HOUDINIENGINE_API bool GetAttributeData(HAPI_AttributeOwner Owner, int MaxTuples, TArray& Results, int IndexStart = 0, int IndexCount = -1); + template HOUDINIENGINE_API bool GetAttributeData(HAPI_AttributeOwner Owner, int MaxTuples, DataType* Results, int IndexStart = 0, int IndexCount = -1); + template HOUDINIENGINE_API bool GetAttributeFirstValue(HAPI_AttributeOwner Owner, DataType & Result); + template HOUDINIENGINE_API bool GetAttributeArrayData(HAPI_AttributeOwner Owner, TArray& InStringArray, TArray& SizesFixedArray, int IndexStart = 0, int IndexCount = -1); + + template HOUDINIENGINE_API bool GetAttributeArrayData(const HAPI_AttributeInfo& AttributeInfo, TArray& InStringArray, TArray& SizesFixedArray, int IndexStart = 0, int IndexCount = -1); + template HOUDINIENGINE_API bool GetAttributeData(const HAPI_AttributeInfo& AttributeInfo, TArray& Results, int IndexStart =0, int IndexCount =-1); + template HOUDINIENGINE_API bool GetAttributeData(const HAPI_AttributeInfo& AttributeInfo, DataType* Results, int IndexStart, int IndexCount); + template HOUDINIENGINE_API bool GetAttributeDataViaSession(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, DataType* Results, int IndexStart, int IndexCount) const; + + bool GetAttributeStrings(HAPI_AttributeOwner Owner, FHoudiniEngineIndexedStringMap& StringMap, int IndexStart = 0, int IndexCount = -1); + bool GetAttributeStrings(const HAPI_AttributeInfo& AttributeInfo, FHoudiniEngineIndexedStringMap& StringMap, int IndexStart = 0, int IndexCount = -1); + + // Functions to set data, mostly templated on type. + // + //template bool SetAttributeData(HAPI_AttributeOwner Owner, const TArray& Data); + template bool SetAttributeData(const HAPI_AttributeInfo& AttributeInfo, const TArray& Data); + template bool SetAttributeData(const HAPI_AttributeInfo& AttributeInfo, const DataType* Data, int First = 0, int Count = -1) const; + template bool SetAttributeDataViaSession(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, const DataType* Data, int First = 0, int Count = -1) const; + template bool SetAttributeUniqueData(const HAPI_AttributeInfo& AttributeInfo, const DataType& Data); + template bool SetAttributeArrayData(const HAPI_AttributeInfo& InAttributeInfo, const TArray& InStringArray, const TArray& SizesFixedArray); + bool SetAttributeStringMap(const HAPI_AttributeInfo& AttributeInfo, const FHoudiniEngineIndexedStringMap& InIndexedStringMap); + bool SetAttributeDictionary(const HAPI_AttributeInfo& InAttributeInfo, const TArray& JSONData); + + TArray GetAttributeNames(HAPI_AttributeOwner Owner); + + bool GetHeightFieldData(TArray& Values, int IndexCount); + bool GetHeightFieldDataViaSession(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, float* Results, int IndexStart, int IndexCount) const; + +protected: + // + // Internal functions. + + int CalculateNumberOfTasks(const HAPI_AttributeInfo& AttributeInfo) const; + int CalculateNumberOfTasks(int64 SizeInBytes, int NumSessions) const; + int CalculateNumberOfSessions(const HAPI_AttributeInfo& AttributeInfo) const; + + template bool GetAttributeDataMultiSession(const HAPI_AttributeInfo& AttributeInfo, DataType* Results, int First, int Count); + template bool SetAttributeDataMultiSession(const HAPI_AttributeInfo& AttributeInfo, const DataType* Data, int First, int Count) const; + + // Internal functions for actually getting data from HAPI.No type conversion is performed. + + template HAPI_Result FetchHapiData(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, DataType* Data, int IndexStart, int IndexCount) const; + template HAPI_Result FetchHapiDataArray(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, DataType* Data, int* Sizes, int IndexStart, int IndexCount) const; + + // Internal functions for sending data to HAPI. No type conversion is performed. + template HAPI_Result SendHapiData(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, const DataType* Data, int StartIndex, int IndexCount) const; + + // Data conversion functions. + template static void ConvertFromRawData(const FHoudiniRawAttributeData& RawData, DataType* Data, size_t Count); + template static void ConvertToRawData(HAPI_StorageType StorageType, FHoudiniRawAttributeData& RawData, const DataType* Data, size_t Count); + + template static void Convert(const SrcType* SourceData, DestType* DestData, int Count); + static FString ToString(int32 Number); + static FString ToString(int64 Number); + static FString ToString(float Number); + static FString ToString(double Number); + static double ToDouble(const FString& Str); + static int ToInt(const FString& Str); + + // Raw functions fetch data before/after any type conversion. + bool GetRawAttributeData(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, FHoudiniRawAttributeData& Data); + bool GetRawAttributeData(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, FHoudiniRawAttributeData& Data, int Start, int Count) const; + bool SetRawAttributeData(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, FHoudiniRawAttributeData& Data, int IndexStart, int IndexCount) const; + + template static HAPI_StorageType GetHapiType(); + static int64 GetHapiSize(HAPI_StorageType StorageType); + + static bool IsHapiArrayType(HAPI_StorageType); + static HAPI_StorageType GetTypeWithoutArray(HAPI_StorageType StorageType); + + + template + static bool ExecuteTasksWithSessions(TArray& Tasks, int NumSessions); + + template + HAPI_Result SendHapiDataRunLengthEncoded(const HAPI_Session* Session, const HAPI_AttributeInfo& AttributeInfo, const DataType* Data, int StartIndex, int IndexCount) const; + +}; + + + + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineBakeUtils.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineBakeUtils.h" + +#include "HoudiniEnginePrivatePCH.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniBakeLandscape.h" +#include "HoudiniCookable.h" +#include "HoudiniDataLayerUtils.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineOutputStats.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniFoliageTools.h" +#include "HoudiniGeometryCollectionTranslator.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniInstancedActorComponent.h" +#include "HoudiniInstanceTranslator.h" +#include "HoudiniLandscapeTranslator.h" +#include "HoudiniMeshTranslator.h" +#include "HoudiniOutput.h" +#include "HoudiniOutputTranslator.h" +#include "HoudiniPackageParams.h" +#include "HoudiniPDGAssetLink.h" +#include "HoudiniRuntimeSettings.h" +#include "HoudiniSplineComponent.h" +#include "HoudiniStringResolver.h" +#include "UnrealLandscapeTranslator.h" + +#include "ActorFactories/ActorFactoryClass.h" +#include "ActorFactories/ActorFactoryEmptyActor.h" +#include "ActorFactories/ActorFactoryStaticMesh.h" +#include "Animation/SkeletalMeshActor.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetSelection.h" +#include "AssetToolsModule.h" +#include "BusyCursor.h" +#include "Components/AudioComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "Components/SplineComponent.h" +#include "Components/StaticMeshComponent.h" +#include "Containers/UnrealString.h" +#include "Editor.h" +#include "Editor/EditorEngine.h" +#include "EditorLevelUtils.h" +#include "Engine/LevelBounds.h" +#include "Engine/SimpleConstructionScript.h" +#include "Engine/SkeletalMesh.h" +#include "Engine/StaticMesh.h" +#include "Engine/StaticMeshActor.h" +#include "Engine/World.h" +#include "Engine/WorldComposition.h" +#include "Factories/BlueprintFactory.h" +#include "Factories/WorldFactory.h" +#include "FileHelpers.h" +#include "FoliageEditUtility.h" +#include "GameFramework/Actor.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollection/GeometryCollectionComponent.h" + #include "GeometryCollection/GeometryCollectionObject.h" +#else + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionComponent.h" + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionObject.h" +#endif +#include "HAL/FileManager.h" +#include "InstancedFoliageActor.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/ComponentEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "LandscapeEdit.h" +#include "LandscapeInfo.h" +#include "LandscapeInfoMap.h" +#include "LandscapeStreamingProxy.h" +#include "Kismet2/ComponentEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "MaterialEditingLibrary.h" +#else + #include "MaterialEditor/Public/MaterialEditingLibrary.h" +#endif + +#include "Materials/Material.h" +#include "Materials/Material.h" +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialInstance.h" +#include "Math/Box.h" +#include "Misc/Paths.h" +#include "Misc/ScopedSlowTask.h" +#include "PackageTools.h" +#include "Particles/ParticleSystemComponent.h" +#include "PhysicsEngine/BodySetup.h" +#include "PhysicalMaterials/PhysicalMaterial.h" +#include "SkeletalMeshTypes.h" +#include "Sound/SoundBase.h" +#include "UObject/MetaData.h" +#include "UObject/Package.h" +#include "UObject/UnrealType.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "Engine/SkinnedAssetCommon.h" +#endif +#include "HoudiniBakeLevelInstanceUtils.h" +#include "HoudiniLevelInstanceUtils.h" +#include "IDetailTreeNode.h" +#include "LevelInstance/LevelInstanceActor.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + #include "LevelInstance/LevelInstanceComponent.h" +#endif +#include "Materials/MaterialExpressionTextureSample.h" +#include "WorldPartition/WorldPartition.h" +#include "WorldPartition/WorldPartitionSubsystem.h" +#include "HoudiniHLODLayerUtils.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "Engine/SkinnedAssetCommon.h" +#endif + +#include "Animation/Skeleton.h" +#include "Engine/DataTable.h" +#include "Kismet2/StructureEditorUtils.h" +#include "UObject/TextProperty.h" +#include "UObject/UObjectGlobals.h" +#include "UserDefinedStructure/UserDefinedStructEditorData.h" +#include "HoudiniFoliageUtils.h" +#if defined(HOUDINI_USE_PCG) +#include "HoudiniPCGDataObject.h" +#endif +#include "PhysicsEngine/PhysicsAsset.h" +#include "Animation/AnimSequence.h" + + +HOUDINI_BAKING_DEFINE_LOG_CATEGORY(); + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + + +FHoudiniEngineBakeState::FHoudiniEngineBakeState(const int32 InNumOutputs, const TArray& InOldBakedOutputs) +{ + OldBakedOutputs = InOldBakedOutputs; + OldBakedOutputs.SetNum(InNumOutputs); + NewBakedOutputs.SetNum(InNumOutputs); +} + + +const FHoudiniBakedOutputObject* +FHoudiniEngineBakeState::FindOldBakedOutputObject(const int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InIdentifier) +{ + check(OldBakedOutputs.IsValidIndex(InOutputIndex)); + return OldBakedOutputs[InOutputIndex].BakedOutputObjects.Find(InIdentifier); +} + + +FHoudiniBakedOutputObject +FHoudiniEngineBakeState::MakeNewBakedOutputObject(const int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InIdentifier, bool& bOutHasPreviousBakeData) +{ + FHoudiniBakedOutputObject NewBakedOutputObject; + + if (FHoudiniBakedOutputObject const* const Entry = FindOldBakedOutputObject(InOutputIndex, InIdentifier)) + { + bOutHasPreviousBakeData = true; + NewBakedOutputObject = *Entry; + } + else + { + bOutHasPreviousBakeData = false; + } + + return NewBakedOutputObject; +} + + +const FHoudiniBakedOutputObject& +FHoudiniEngineBakeState::FindNewBakedOutputObjectChecked(const int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InIdentifier) +{ + check(NewBakedOutputs.IsValidIndex(InOutputIndex)); + return NewBakedOutputs[InOutputIndex].BakedOutputObjects.FindChecked(InIdentifier); +} + + +FHoudiniBakedOutputObject& +FHoudiniEngineBakeState::FindOrAddNewBakedOutputObject(const int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InIdentifier) +{ + check(NewBakedOutputs.IsValidIndex(InOutputIndex)); + return NewBakedOutputs[InOutputIndex].BakedOutputObjects.FindOrAdd(InIdentifier); +} + + +FHoudiniBakedOutputObject& +FHoudiniEngineBakeState::SetNewBakedOutputObject(const int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InIdentifier, const FHoudiniBakedOutputObject& InBakedOutputObject) +{ + check(NewBakedOutputs.IsValidIndex(InOutputIndex)); + return NewBakedOutputs[InOutputIndex].BakedOutputObjects.Emplace(InIdentifier, InBakedOutputObject); +} + + +FHoudiniBakedOutputObject& +FHoudiniEngineBakeState::SetNewBakedOutputObject(const int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InIdentifier, FHoudiniBakedOutputObject&& InBakedOutputObject) +{ + check(NewBakedOutputs.IsValidIndex(InOutputIndex)); + return NewBakedOutputs[InOutputIndex].BakedOutputObjects.Emplace(InIdentifier, InBakedOutputObject); +} + + +USkeleton* +FHoudiniEngineBakeState::FindBakedSkeleton(USkeleton const* const InTempSkeleton, bool& bFoundEntry) const +{ + USkeleton* const* const BakedSkeleton = BakedSkeletons.Find(InTempSkeleton); + if (!BakedSkeleton) + { + bFoundEntry = false; + return nullptr; + } + + bFoundEntry = true; + return *BakedSkeleton; +} + + +FHoudiniEngineBakedActor::FHoudiniEngineBakedActor() + : Actor(nullptr) + , OutputIndex(INDEX_NONE) + , OutputObjectIdentifier() + , ActorBakeName(NAME_None) + , BakedObject(nullptr) + , SourceObject(nullptr) + , BakeFolderPath() + , bInstancerOutput(false) + , bPostBakeProcessPostponed(false) +{ +} + +FHoudiniEngineBakedActor::FHoudiniEngineBakedActor( + AActor* InActor, + FName InActorBakeName, + FName InWorldOutlinerFolder, + int32 InOutputIndex, + const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, + UObject* InBakedObject, + UObject* InSourceObject, + UObject* InBakedComponent, + const FString& InBakeFolderPath, + const FHoudiniPackageParams& InBakedObjectPackageParams) + : Actor(InActor) + , OutputIndex(InOutputIndex) + , OutputObjectIdentifier(InOutputObjectIdentifier) + , ActorBakeName(InActorBakeName) + , WorldOutlinerFolder(InWorldOutlinerFolder) + , BakedObject(InBakedObject) + , SourceObject(InSourceObject) + , BakedComponent(InBakedComponent) + , BakeFolderPath(InBakeFolderPath) + , BakedObjectPackageParams(InBakedObjectPackageParams) + , bInstancerOutput(false) + , bPostBakeProcessPostponed(false) +{ +} + +bool +FHoudiniEngineBakeUtils::BakeCookable( + UHoudiniCookable* InCookableToBake, + const FHoudiniBakeSettings& BakeSettings, + EHoudiniEngineBakeOption InBakeOption, + bool bInRemoveHACOutputOnSuccess) +{ + if (!IsValid(InCookableToBake)) + return false; + + // Handle proxies: if the output has any current proxies, first refine them + bool bNeedsToReCook; + if (!CheckForAndRefineHoudiniProxyMesh(InCookableToBake, BakeSettings.bReplaceActors, InBakeOption, bInRemoveHACOutputOnSuccess, BakeSettings.bRecenterBakedActors, bNeedsToReCook)) + { + // Either the component is invalid, or needs a recook to refine a proxy mesh + return false; + } + + bool bSuccess = false; + switch (InBakeOption) + { + case EHoudiniEngineBakeOption::ToActor: + { + bSuccess = FHoudiniEngineBakeUtils::BakeCookableToActors(InCookableToBake, BakeSettings); + } + break; + + case EHoudiniEngineBakeOption::ToBlueprint: + { + bSuccess = FHoudiniEngineBakeUtils::BakeBlueprints(InCookableToBake, BakeSettings); + } + break; + + case EHoudiniEngineBakeOption::ToAsset: + { + bSuccess = FHoudiniEngineBakeUtils::BakeCookableToAssets(InCookableToBake, BakeSettings); + } + break; + } + + if (bSuccess && bInRemoveHACOutputOnSuccess) + { + FHoudiniOutputTranslator::ClearAndRemoveOutputs(InCookableToBake->GetOutputs(), EHoudiniClearFlags::EHoudiniClear_Actors); + } + + return bSuccess; +} + +bool +FHoudiniEngineBakeUtils::BakeCookableToActors( + UHoudiniCookable* InCookable, + const FHoudiniBakeSettings& BakeSettings) +{ + if (!IsValid(InCookable)) + return false; + + TArray NewActors; + FHoudiniBakedObjectData BakedObjectData; + const bool bBakedWithErrors = !FHoudiniEngineBakeUtils::BakeCookableToActors( + InCookable, BakeSettings, NewActors, BakedObjectData); + + if (bBakedWithErrors) + { + // TODO ? + HOUDINI_LOG_WARNING(TEXT("Errors when baking")); + } + + // Save the created packages + FHoudiniEngineBakeUtils::SaveBakedPackages(BakedObjectData.PackagesToSave); + + // Recenter and select the baked actors + if (!InCookable->GetIsPCG() && GEditor && NewActors.Num() > 0) + GEditor->SelectNone(false, true); + + for (const FHoudiniEngineBakedActor& Entry : NewActors) + { + if (!IsValid(Entry.Actor)) + continue; + + if (BakeSettings.bRecenterBakedActors) + CenterActorToBoundingBoxCenter(Entry.Actor); + + if (GEditor && !InCookable->GetIsPCG()) + GEditor->SelectActor(Entry.Actor, true, false); + } + + FHoudiniBakeLevelInstanceUtils::CreateLevelInstances( + InCookable, NewActors, BakedObjectData); + + if (GEditor && !InCookable->GetIsPCG() && NewActors.Num() > 0) + GEditor->NoteSelectionChange(); + + if (InCookable->GetDoSlateNotifications()) + { + const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); + FString Msg = FString::Format(*FinishedTemplate, { BakedObjectData.BakeStats.NumPackagesCreated, BakedObjectData.BakeStats.NumPackagesUpdated } ); + FHoudiniEngine::Get().FinishTaskSlateNotification( FText::FromString(Msg) ); + } + + // Broadcast that the bake is complete + InCookable->HandleOnPostBake(!bBakedWithErrors); + + return true; +} + +bool +FHoudiniEngineBakeUtils::BakeCookableToActors( + UHoudiniCookable* InCookable, + const FHoudiniBakeSettings & BakeSettings, + TArray& OutNewActors, + FHoudiniBakedObjectData& BakedObjectData, + TArray * InOutputTypesToBake, + TArray * InInstancerComponentTypesToBake, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder) +{ + if (!IsValid(InCookable)) + return false; + + // Get an array of the outputs + const int32 NumOutputs = InCookable->GetNumOutputs(); + TArray Outputs; + Outputs.Reserve(NumOutputs); + for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) + { + Outputs.Add(InCookable->GetOutputAt(OutputIdx)); + } + + FHoudiniEngineBakeState BakeState(NumOutputs, InCookable->GetBakedOutputs()); + + const TArray AllBakedActors; + const bool bSuccess = BakeHoudiniOutputsToActors( + InCookable, + Outputs, + BakeState, + InCookable->GetComponentTransform(), + InCookable->GetBakeFolder(), + InCookable->GetTemporaryCookFolder(), + BakeSettings, + AllBakedActors, + OutNewActors, + BakedObjectData, + InOutputTypesToBake, + InInstancerComponentTypesToBake, + InFallbackActor, + InFallbackWorldOutlinerFolder); + + // Copy any relevant new / update data from the bake state to the HAC + InCookable->GetBakedOutputs() = BakeState.GetNewBakedOutputs(); + + return bSuccess; +} + +bool +FHoudiniEngineBakeUtils::BakeCookableToAssets( + UHoudiniCookable* InCookable, + const FHoudiniBakeSettings& BakeSettings) +{ + if (!IsValid(InCookable)) + return false; + + FHoudiniBakedObjectData BakedObjectData; + // Get an array of the outputs + const int32 NumOutputs = InCookable->GetNumOutputs(); + TArray Outputs; + Outputs.Reserve(NumOutputs); + for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) + { + Outputs.Add(InCookable->GetOutputAt(OutputIdx)); + } + + FHoudiniEngineBakeState BakeState(NumOutputs, InCookable->GetBakedOutputs()); + + // DO THE BAKE! + + // First we bake everything except instancers, then bake instancers. + // As instancers might use meshes from the other outputs. + bool bHasAnyInstancers = false; + int32 NumProcessedOutputs = 0; + + // TODO: Handle bake errors + bool bBakedWithErrors = false; + + const FString MsgTemplate = TEXT("Baking output: {0}/{1}."); + FString Msg = FString::Format(*MsgTemplate, { 0, NumOutputs }); + if (InCookable->GetDoSlateNotifications()) + { + FHoudiniEngine::Get().CreateTaskSlateNotification(FText::FromString(Msg)); + } + + TMap AlreadyBakedMaterialsMap; + TMap AlreadyBakedStaticMeshMap; + for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) + { + UHoudiniOutput* Output = Outputs[OutputIdx]; + if (!IsValid(Output)) + { + NumProcessedOutputs++; + continue; + } + + if (InCookable->GetDoSlateNotifications()) + { + Msg = FString::Format(*MsgTemplate, { NumProcessedOutputs + 1, NumOutputs }); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Msg)); + } + + const EHoudiniOutputType OutputType = Output->GetType(); + switch (OutputType) + { + case EHoudiniOutputType::Mesh: + { + FHoudiniEngineBakeUtils::BakeStaticMeshOutputToAsset( + InCookable, + OutputIdx, + Outputs, + InCookable->GetTemporaryCookFolder(), + InCookable->GetBakeFolder(), + BakeState, + BakeSettings, + BakedObjectData); + } + break; + + case EHoudiniOutputType::Instancer: + { + if (!bHasAnyInstancers) + bHasAnyInstancers = true; + NumProcessedOutputs--; + } + break; + + case EHoudiniOutputType::Skeletal: + { + FHoudiniEngineBakeUtils::BakeSkeletalMeshOutputToAsset( + InCookable, + OutputIdx, + Outputs, + InCookable->GetTemporaryCookFolder(), + InCookable->GetBakeFolder(), + BakeState, + BakeSettings, + BakedObjectData); + } + break; + + case EHoudiniOutputType::GeometryCollection: + { + FHoudiniEngineBakeUtils::BakeGeometryCollectionOutputToAsset(InCookable, + OutputIdx, + Outputs, + InCookable->GetTemporaryCookFolder(), + InCookable->GetBakeFolder(), + BakeState, + BakeSettings, + BakedObjectData); + } + break; + + case EHoudiniOutputType::DataTable: + { + FHoudiniEngineBakeUtils::BakeDataTables( + InCookable, + OutputIdx, + Outputs, + BakeState, + InCookable->GetBakeFolder(), + BakeSettings, + BakedObjectData); + } + break; + + case EHoudiniOutputType::AnimSequence: + { + FHoudiniEngineBakeUtils::BakeAnimSequence( + InCookable, + OutputIdx, + Outputs, + BakeState, + InCookable->GetBakeFolder(), + BakeSettings, + BakedObjectData); + } + break; + + case EHoudiniOutputType::Cop: + { + FHoudiniEngineBakeUtils::BakeTexture( + InCookable, + OutputIdx, + Outputs, + BakeState, + InCookable->GetBakeFolder(), + BakeSettings, + BakedObjectData); + } + break; + + case EHoudiniOutputType::Landscape: + case EHoudiniOutputType::LandscapeSpline: + case EHoudiniOutputType::PCG: + case EHoudiniOutputType::Invalid: + case EHoudiniOutputType::Curve: + break; + } + + NumProcessedOutputs++; + } + + /* + if (bHasAnyInstancers) + { + FHoudiniEngineBakeUtils::BakeAllFoliageTypes( + InCookable, + AlreadyBakedStaticMeshMap, + InBakeState, + InOutputs, + InBakeFolder, + InTempCookFolder, + BakeSettings, + AllBakedActors, + AlreadyBakedMaterialsMap, + BakedObjectData); + + for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) + { + UHoudiniOutput* Output = InOutputs[OutputIdx]; + if (!IsValid(Output)) + { + continue; + } + + if (Output->GetType() == EHoudiniOutputType::Instancer) + { + OutputBakedActors.Reset(); + + Msg = FString::Format(*MsgTemplate, { NumProcessedOutputs + 1, NumOutputs }); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Msg)); + + FHoudiniEngineBakeUtils::BakeInstancerOutputToActors( + InCookable, + OutputIdx, + InOutputs, + InBakeState, + InParentTransform, + InBakeFolder, + InTempCookFolder, + BakeSettings, + AllBakedActors, + OutputBakedActors, + BakedObjectData, + AlreadyBakedStaticMeshMap, + AlreadyBakedMaterialsMap, + InInstancerComponentTypesToBake, + InFallbackActor, + InFallbackWorldOutlinerFolder); + + AllBakedActors.Append(OutputBakedActors); + NewBakedActors.Append(OutputBakedActors); + + NumProcessedOutputs++; + } + } + } + */ + + //---------------------------------------- + // BAKE DONE + + // Copy any relevant new / update data from the bake state to the HAC + InCookable->GetBakedOutputs() = BakeState.GetNewBakedOutputs(); + + // Save the created packages + FHoudiniEngineBakeUtils::SaveBakedPackages(BakedObjectData.PackagesToSave); + + + //FHoudiniBakeLevelInstanceUtils::CreateLevelInstances( + // InCookable, NewActors, BakedObjectData); + + if (InCookable->GetDoSlateNotifications()) + { + const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); + Msg = FString::Format(*FinishedTemplate, { BakedObjectData.BakeStats.NumPackagesCreated, BakedObjectData.BakeStats.NumPackagesUpdated }); + FHoudiniEngine::Get().FinishTaskSlateNotification(FText::FromString(Msg)); + } + + // Broadcast that the bake is complete + InCookable->HandleOnPostBake(!bBakedWithErrors); + + if (BakedObjectData.PackagesToSave.Num() > 0) + { + // Sync the CB to the baked objects + if (GEditor) + { + TArray Objects; + for (auto& CurrentPackage : BakedObjectData.PackagesToSave) + { + UObject* CurrentObj = Cast(CurrentPackage); + if (!CurrentObj) + continue; + + Objects.AddUnique(CurrentObj); + } + + GEditor->SyncBrowserToObjects(Objects); + } + } + + return true; +} + +void +FHoudiniEngineBakeUtils::DeleteBakedDataTableObjects(TArray& InBakedOutputs) +{ + // Must remove data tables before their structures to prevent Unreal complaining. + + for (FHoudiniBakedOutput& BakedOutput : InBakedOutputs) + { + for (auto& It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& BakedObjectOutput = It.Value; + UObject* Object = It.Value.GetBakedObjectIfValid(); + + if (!IsValid(Object)) + continue; + + if (Object->IsA()) + { + FHoudiniEngineUtils::ForceDeleteObject(Object); + It.Value.BakedObject.Empty(); + } + } + } + + // Now remove the structures. + for (FHoudiniBakedOutput& BakedOutput : InBakedOutputs) + { + for (auto& It : BakedOutput.BakedOutputObjects) + { + UObject* Object = It.Value.GetBakedObjectIfValid(); + + if (!IsValid(Object)) + continue; + + if (Object->IsA() || Object->IsA()) + { + FHoudiniEngineUtils::ForceDeleteObject(Object); + It.Value.BakedObject.Empty(); + } + } + } +} + +bool +FHoudiniEngineBakeUtils::BakeHoudiniOutputsToActors( + UHoudiniCookable* InCookable, + const TArray& InOutputs, + FHoudiniEngineBakeState& InBakeState, + const FTransform& InParentTransform, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutNewActors, + FHoudiniBakedObjectData& BakedObjectData, + TArray * InOutputTypesToBake, + TArray * InInstancerComponentTypesToBake, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder) +{ + const int32 NumOutputs = InOutputs.Num(); + + const FString MsgTemplate = TEXT("Baking output: {0}/{1}."); + FString Msg = FString::Format(*MsgTemplate, { 0, NumOutputs }); + + if(InCookable->GetDoSlateNotifications()) + { + FHoudiniEngine::Get().CreateTaskSlateNotification(FText::FromString(Msg)); + } + + RemoveBakedLevelInstances(InBakeState.GetOldBakedOutputs(), BakeSettings); + + if (BakeSettings.bReplaceAssets) + { + // Make sure all old data tables are removed prior to baking. Data tables must be fully deleted + // before creating new data tables with the same new or Unreal gets very upset. + DeleteBakedDataTableObjects(InBakeState.GetOldBakedOutputs()); + } + + + TArray AllBakedActors = InBakedActors; + TArray NewBakedActors; + + // Landscape layers needs to be cleared during baking, but only once. So keep track of which ones + // have been cleared. + TMap ClearedLandscapeLayers; + + // First bake everything except instancers, then bake instancers. Since instancers might use meshes in + // from the other outputs. + bool bHasAnyInstancers = false; + int32 NumProcessedOutputs = 0; + + TMap AlreadyBakedMaterialsMap; + TMap AlreadyBakedStaticMeshMap; + TArray OutputBakedActors; + for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) + { + UHoudiniOutput* Output = InOutputs[OutputIdx]; + if (!IsValid(Output)) + { + NumProcessedOutputs++; + continue; + } + + if(InCookable->GetDoSlateNotifications()) + { + Msg = FString::Format(*MsgTemplate, { NumProcessedOutputs + 1, NumOutputs }); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Msg)); + } + + const EHoudiniOutputType OutputType = Output->GetType(); + // Check if we should skip this output type + if (InOutputTypesToBake && InOutputTypesToBake->Find(OutputType) == INDEX_NONE) + { + NumProcessedOutputs++; + continue; + } + + OutputBakedActors.Reset(); + switch (OutputType) + { + case EHoudiniOutputType::Mesh: + { + FHoudiniEngineBakeUtils::BakeStaticMeshOutputToActors( + InCookable, + OutputIdx, + InOutputs, + InBakeState, + InBakeFolder, + InTempCookFolder, + BakeSettings, + AllBakedActors, + OutputBakedActors, + BakedObjectData, + AlreadyBakedStaticMeshMap, + AlreadyBakedMaterialsMap, + InFallbackActor, + InFallbackWorldOutlinerFolder); + } + break; + + case EHoudiniOutputType::Instancer: + { + if (!bHasAnyInstancers) + bHasAnyInstancers = true; + NumProcessedOutputs--; + } + break; + + case EHoudiniOutputType::Landscape: + { + const bool bResult = FHoudiniLandscapeBake::BakeLandscape( + InCookable, + OutputIdx, + InOutputs, + InBakeState, + BakeSettings, + InBakeFolder, + ClearedLandscapeLayers, + BakedObjectData); + } + break; + + case EHoudiniOutputType::Skeletal: + { + FHoudiniEngineBakeUtils::BakeSkeletalMeshOutputToActors( + InCookable, + OutputIdx, + InOutputs, + InBakeState, + InBakeFolder, + InTempCookFolder, + BakeSettings, + AllBakedActors, + OutputBakedActors, + BakedObjectData, + AlreadyBakedStaticMeshMap, + AlreadyBakedMaterialsMap, + InFallbackActor, + InFallbackWorldOutlinerFolder); + } + break; + + case EHoudiniOutputType::Curve: + { + FHoudiniEngineBakeUtils::BakeHoudiniCurveOutputToActors( + InCookable, + OutputIdx, + InOutputs, + InBakeState, + InBakeFolder, + BakeSettings, + AllBakedActors, + OutputBakedActors, + InFallbackActor, + InFallbackWorldOutlinerFolder); + } + break; + + case EHoudiniOutputType::GeometryCollection: + { + FHoudiniEngineBakeUtils::BakeGeometryCollectionOutputToActors( + InCookable, + OutputIdx, + InOutputs, + InBakeState, + InBakeFolder, + InTempCookFolder, + BakeSettings, + AllBakedActors, + OutputBakedActors, + BakedObjectData, + AlreadyBakedStaticMeshMap, + AlreadyBakedMaterialsMap, + InFallbackActor, + InFallbackWorldOutlinerFolder); + } + break; + + case EHoudiniOutputType::LandscapeSpline: + { + const bool bResult = FHoudiniLandscapeBake::BakeLandscapeSplines( + InCookable, + OutputIdx, + InOutputs, + InBakeState, + BakeSettings, + InBakeFolder, + ClearedLandscapeLayers, + BakedObjectData); + } + break; + + case EHoudiniOutputType::DataTable: + { + FHoudiniEngineBakeUtils::BakeDataTables( + InCookable, + OutputIdx, + InOutputs, + InBakeState, + InBakeFolder, + BakeSettings, + BakedObjectData); + } + break; + + case EHoudiniOutputType::PCG: + { + FHoudiniEngineBakeUtils::BakePCGData( + InCookable, + OutputIdx, + InOutputs, + InBakeState, + InBakeFolder, + InTempCookFolder, + BakeSettings, + AllBakedActors, + OutputBakedActors, + BakedObjectData, + AlreadyBakedStaticMeshMap, + AlreadyBakedMaterialsMap, + InFallbackActor, + InFallbackWorldOutlinerFolder); + } + break; + + case EHoudiniOutputType::AnimSequence: + { + FHoudiniEngineBakeUtils::BakeAnimSequence( + InCookable, + OutputIdx, + InOutputs, + InBakeState, + InBakeFolder, + BakeSettings, + BakedObjectData); + } + break; + + case EHoudiniOutputType::Invalid: + break; + } + + AllBakedActors.Append(OutputBakedActors); + NewBakedActors.Append(OutputBakedActors); + + NumProcessedOutputs++; + } + + if (bHasAnyInstancers) + { + FHoudiniEngineBakeUtils::BakeAllFoliageTypes( + InCookable, + AlreadyBakedStaticMeshMap, + InBakeState, + InOutputs, + InBakeFolder, + InTempCookFolder, + BakeSettings, + AllBakedActors, + AlreadyBakedMaterialsMap, + BakedObjectData); + + for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) + { + UHoudiniOutput* Output = InOutputs[OutputIdx]; + if (!IsValid(Output)) + { + continue; + } + + if (Output->GetType() == EHoudiniOutputType::Instancer) + { + OutputBakedActors.Reset(); + + Msg = FString::Format(*MsgTemplate, { NumProcessedOutputs + 1, NumOutputs }); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Msg)); + + FHoudiniEngineBakeUtils::BakeInstancerOutputToActors( + InCookable, + OutputIdx, + InOutputs, + InBakeState, + InParentTransform, + InBakeFolder, + InTempCookFolder, + BakeSettings, + AllBakedActors, + OutputBakedActors, + BakedObjectData, + AlreadyBakedStaticMeshMap, + AlreadyBakedMaterialsMap, + InInstancerComponentTypesToBake, + InFallbackActor, + InFallbackWorldOutlinerFolder); + + AllBakedActors.Append(OutputBakedActors); + NewBakedActors.Append(OutputBakedActors); + + NumProcessedOutputs++; + } + } + } + + // Moved Cooked to Baked Landscapes. + { + TArray BakedLandscapeActors = FHoudiniLandscapeBake::MoveCookedToBakedLandscapes( + InCookable, + FName(InFallbackWorldOutlinerFolder), + InOutputs, + InBakeState, + BakeSettings, + InBakeFolder, + BakedObjectData); + + AllBakedActors.Append(BakedLandscapeActors); + NewBakedActors.Append(BakedLandscapeActors); + } + + // Only do the post bake post-process once per Actor + TSet UniqueActors; + for (FHoudiniEngineBakedActor& BakedActor : NewBakedActors) + { + if (BakedActor.bPostBakeProcessPostponed && BakedActor.Actor) + { + BakedActor.bPostBakeProcessPostponed = false; + AActor* Actor = BakedActor.Actor; + bool bIsAlreadyInSet = false; + UniqueActors.Add(Actor, &bIsAlreadyInSet); + if (!bIsAlreadyInSet) + { + Actor->InvalidateLightingCache(); + Actor->PostEditMove(true); + Actor->MarkPackageDirty(); + } + } + } + + // Create package params we will use for data layers and HLODs. Code is simpler if we do this up front. + TArray PackageParams; + PackageParams.SetNum(NewBakedActors.Num()); + + for (int Index = 0; Index < NewBakedActors.Num(); Index++) + { + FHoudiniEngineBakedActor& BakedActor = NewBakedActors[Index]; + UHoudiniOutput* Output = InOutputs[BakedActor.OutputIndex]; + FHoudiniOutputObject& OutputObject = Output->GetOutputObjects()[BakedActor.OutputObjectIdentifier]; + + const bool bHasPreviousBakeData = InBakeState.FindOldBakedOutputObject(BakedActor.OutputIndex, BakedActor.OutputObjectIdentifier) != nullptr; + + const EPackageReplaceMode AssetPackageReplaceMode = BakeSettings.bReplaceAssets ? EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; + FHoudiniAttributeResolver Resolver; + FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( + BakedActor.Actor->GetWorld(), + InCookable, + BakedActor.OutputObjectIdentifier, + OutputObject, + bHasPreviousBakeData, + "", + PackageParams[Index], + Resolver, + InBakeFolder.Path, + AssetPackageReplaceMode); + } + + // Due to a bug in 5.3 and earlier we need to create all the data layers in one go and store their values, + // since there seem to be a delay in creating new data layers. + + TMap DataLayerLookup; + + for(int Index = 0; Index < NewBakedActors.Num(); Index++) + { + FHoudiniEngineBakedActor& BakedActor = NewBakedActors[Index]; + UHoudiniOutput* Output = InOutputs[BakedActor.OutputIndex]; + FHoudiniOutputObject& OutputObject = Output->GetOutputObjects()[BakedActor.OutputObjectIdentifier]; + + UWorld* World = BakedActor.Actor->GetWorld(); + + AWorldDataLayers* WorldDataLayers = World->GetWorldDataLayers(); + if(!WorldDataLayers) + { + if (!OutputObject.DataLayers.IsEmpty()) + HOUDINI_LOG_ERROR(TEXT("Unable to apply Data Layer because this map is not world partitioned.")); + continue; + } + + for(auto& DataLayer : OutputObject.DataLayers) + { + if (!DataLayerLookup.Contains(DataLayer.Name)) + { + UDataLayerInstance* DataLayerInstance = FHoudiniDataLayerUtils::FindOrCreateDataLayerInstance(PackageParams[Index], WorldDataLayers, DataLayer); + if(DataLayerInstance) + { + DataLayerLookup.Add(DataLayer.Name, DataLayerInstance); + } + } + } + } + + for(int Index = 0; Index < NewBakedActors.Num(); Index++) + { + FHoudiniEngineBakedActor& BakedActor = NewBakedActors[Index]; + UHoudiniOutput* Output = InOutputs[BakedActor.OutputIndex]; + FHoudiniOutputObject& OutputObject = Output->GetOutputObjects()[BakedActor.OutputObjectIdentifier]; + + const bool bHasPreviousBakeData = InBakeState.FindOldBakedOutputObject(BakedActor.OutputIndex, BakedActor.OutputObjectIdentifier) != nullptr; + + if (IsValid(BakedActor.Actor)) + { + FHoudiniDataLayerUtils::ApplyDataLayersToActor(BakedActor.Actor, OutputObject.DataLayers, DataLayerLookup); + FHoudiniHLODLayerUtils::ApplyHLODLayersToActor(PackageParams[Index], BakedActor.Actor, OutputObject.HLODLayers); + } + } + + OutNewActors = MoveTemp(NewBakedActors); + + return true; +} + +void +FHoudiniEngineBakeUtils::RemoveBakedFoliageInstances( + UWorld* InWorld, + TArray& InBakedOutputs) +{ + for(int Index = 0; Index < InBakedOutputs.Num(); Index++) + { + FHoudiniBakedOutput & BakedOutput = InBakedOutputs[Index]; + for(auto & BakedObject : BakedOutput.BakedOutputObjects) + { + if (IsValid(BakedObject.Value.FoliageType)) + { + FHoudiniFoliageTools::RemoveFoliageInstances( + InWorld, + BakedObject.Value.FoliageType, + BakedObject.Value.FoliageInstancePositions); + } + // Remember the foliage type in the previous bake data, but remove the instance positions (since + // that is what we cleared). We need the previous bake foliage type when baking in "replace existing assets" + // mode, so that we replace the correct increment. + // BakedObject.Value.FoliageType = nullptr; + BakedObject.Value.FoliageInstancePositions.Empty(); + } + } +} + +void +FHoudiniEngineBakeUtils::BakeAllFoliageTypes( + UHoudiniCookable* InCookable, + const TMap& AlreadyBakedStaticMeshMap, + FHoudiniEngineBakeState& InBakeState, + const TArray& InAllOutputs, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& BakeResults, + TMap& InOutAlreadyBakedMaterialsMap, + FHoudiniBakedObjectData& BakedObjectData) +{ + UWorld* World = InCookable->GetWorld(); + + // Remove previous bake if required. + if (BakeSettings.bReplaceAssets) + { + RemoveBakedFoliageInstances(World, InBakeState.GetOldBakedOutputs()); + } + + // Create Foliage Types associated with each output. + TMap FoliageMap; + for(int InOutputIndex = 0; InOutputIndex < InAllOutputs.Num(); InOutputIndex++) + { + BakeFoliageTypes( + FoliageMap, + InCookable, + InOutputIndex, + InBakeState, + InAllOutputs, + InBakeFolder, + InTempCookFolder, + BakeSettings, + BakeResults, + AlreadyBakedStaticMeshMap, + InOutAlreadyBakedMaterialsMap, + BakedObjectData); + } + + // Remove all cooked existing foliage + for (auto It : FoliageMap) + { + auto* CookedFoliageType = Cast(It.Key); + FHoudiniFoliageUtils::RemoveFoliageTypeFromWorld(World, CookedFoliageType); + } +} + + +bool +FHoudiniEngineBakeUtils::BakeFoliageTypes( + TMap & FoliageMap, + UHoudiniCookable* InCookable, + int32 InOutputIndex, + FHoudiniEngineBakeState& InBakeState, + const TArray& InAllOutputs, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& BakeResults, + const TMap& InAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + FHoudiniBakedObjectData& BakedObjectData) +{ + const EPackageReplaceMode AssetPackageReplaceMode = BakeSettings.bReplaceAssets + ? EPackageReplaceMode::ReplaceExistingAssets + : EPackageReplaceMode::CreateNewAssets; + + UHoudiniOutput* Output = InAllOutputs[InOutputIndex]; + + UWorld* DesiredWorld = Output ? Output->GetWorld() : GWorld; + auto & OutputObjects = Output->GetOutputObjects(); + + for (auto& Pair : OutputObjects) + { + const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + FHoudiniOutputObject* OutputObject = &Pair.Value; + + // Skip non foliage outputs + if (OutputObject->FoliageType == nullptr) + continue; + + if (FoliageMap.Contains(OutputObject->FoliageType)) + continue; + + FHoudiniBakedOutputObject BakedObject; + + bool bHasPreviousBakeData = false; + BakedObject = InBakeState.MakeNewBakedOutputObject(InOutputIndex, Identifier, bHasPreviousBakeData); + + UFoliageType* const UserFoliageType = IsValid(OutputObject->UserFoliageType) + ? Cast(OutputObject->UserFoliageType) + : nullptr; + + UFoliageType* TargetFoliageType = nullptr; + bool bUseUserFoliageType = false; + if (IsValid(UserFoliageType)) + { + // The user specified a Foliage Type. Only use it directly if there are no differences between it and + // the cooked version. For example, if the user specified material overrides, or different values for + // foliage parameters, such as radius, then we need to bake a new foliage type. + if (FHoudiniFoliageTools::AreFoliageTypesEqual(UserFoliageType, OutputObject->FoliageType)) + { + bUseUserFoliageType = true; + } + else + { + HOUDINI_LOG_WARNING(TEXT( + "Baking a new foliage type, since the cooked foliage type has been modified with respect to the " + "user specified foliage type %s"), *UserFoliageType->GetName()); + } + } + + if (bUseUserFoliageType) + { + // The user specified a Foliage Type, so store it. + TargetFoliageType = UserFoliageType; + FoliageMap.Add(OutputObject->FoliageType, TargetFoliageType); + } + else + { + // TODO: Support "ReplaceExistingAssets" + + // The Foliage Type was created by this plugin. Copy it to the Baked output. + FString ObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(OutputObject->FoliageType); + + FHoudiniPackageParams PackageParams; + FHoudiniAttributeResolver InstancerResolver; + FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( + DesiredWorld, InCookable, Identifier, *OutputObject, bHasPreviousBakeData, + ObjectName, PackageParams, InstancerResolver, InBakeFolder.Path, AssetPackageReplaceMode); + + UFoliageType* const PreviousBakeFoliageType = bHasPreviousBakeData ? BakedObject.FoliageType : nullptr; + TargetFoliageType = DuplicateFoliageTypeAndCreatePackageIfNeeded( + OutputObject->FoliageType, + PreviousBakeFoliageType, + PackageParams, + InAllOutputs, + BakeResults, + InTempCookFolder.Path, + FoliageMap, + InOutAlreadyBakedMaterialsMap, + BakeResults, + BakedObjectData); + + FoliageMap.Add(OutputObject->FoliageType, TargetFoliageType); + } + + check(IsValid(TargetFoliageType)); + + // Replace any mesh referenced in the cooked foliage with the new reference. + UFoliageType_InstancedStaticMesh* const CookedFoliageType = Cast(OutputObject->FoliageType); + UFoliageType_InstancedStaticMesh* const BakedFoliageType = Cast(TargetFoliageType); + if (CookedFoliageType && TargetFoliageType && InAlreadyBakedStaticMeshMap.Contains(CookedFoliageType->GetStaticMesh())) + { + BakedFoliageType->SetStaticMesh(InAlreadyBakedStaticMeshMap[CookedFoliageType->GetStaticMesh()]); + } + + // Copy all cooked instances to reference the baked instances. + auto Instances = FHoudiniFoliageTools::GetAllFoliageInstances(DesiredWorld, OutputObject->FoliageType); + + TArray FoliageActors = FHoudiniFoliageTools::SpawnFoliageInstances(DesiredWorld, TargetFoliageType, Instances, {}); + + TArray InstancesPositions; + InstancesPositions.Reserve(Instances.Num()); + for(auto & Instance : Instances) + { + InstancesPositions.Add(Instance.Location); + } + + TArray ActorInstancePaths; + ActorInstancePaths.Reserve(FoliageActors.Num()); + for (auto & Instance : FoliageActors) + { + ActorInstancePaths.Add(Instance->GetPathName()); + } + + // Store back output object. + BakedObject.FoliageType = TargetFoliageType; + BakedObject.FoliageInstancePositions = InstancesPositions; + BakedObject.FoliageActors = ActorInstancePaths; + InBakeState.SetNewBakedOutputObject(InOutputIndex, Identifier, BakedObject); + } + + return true; +} + +bool +FHoudiniEngineBakeUtils::BakeInstancerOutputToActors( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FTransform& InTransform, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + TArray const* InInstancerComponentTypesToBake, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder) +{ + if (!InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + + UHoudiniOutput* InOutput = InAllOutputs[InOutputIndex]; + if (!IsValid(InOutput)) + return false; + + // Geometry collection instancers will be done on the geometry collection output component. + if (FHoudiniGeometryCollectionTranslator::IsGeometryCollectionInstancer(InOutput)) + { + return true; + } + + const TArray& HGPOs = InOutput->GetHoudiniGeoPartObjects(); + TMap& OutputObjects = InOutput->GetOutputObjects(); + + TArray AllBakedActors = InBakedActors; + TArray NewBakedActors; + TArray OutputBakedActors; + + // Iterate on the output objects, baking their object/component as we go + for (auto& Pair : OutputObjects) + { + const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + FHoudiniOutputObject& CurrentOutputObject = Pair.Value; + + if (CurrentOutputObject.bProxyIsCurrent) + { + // TODO: we need to refine the SM first! + // ?? + } + + for(auto Component : CurrentOutputObject.OutputComponents) + { + if (!IsValid(Component)) + continue; + + OutputBakedActors.Reset(); + + if (Component->IsA() + && (!InInstancerComponentTypesToBake || InInstancerComponentTypesToBake->Contains(EHoudiniInstancerComponentType::InstancedStaticMeshComponent))) + { + BakeInstancerOutputToActors_ISMC( + InCookable, + InOutputIndex, + InAllOutputs, + InBakeState, + HGPOs, + Pair.Key, + CurrentOutputObject, + InTransform, + InBakeFolder, + InTempCookFolder, + BakeSettings, + AllBakedActors, + OutputBakedActors, + BakedObjectData, + InOutAlreadyBakedStaticMeshMap, + InOutAlreadyBakedMaterialsMap, + InFallbackActor, + InFallbackWorldOutlinerFolder); + } + else if (Component->IsA() + && (!InInstancerComponentTypesToBake || InInstancerComponentTypesToBake->Contains(EHoudiniInstancerComponentType::InstancedActorComponent))) + { + BakeInstancerOutputToActors_IAC( + InCookable, + InOutputIndex, + HGPOs, + Pair.Key, + CurrentOutputObject, + InBakeState, + InBakeFolder, + BakeSettings, + AllBakedActors, + OutputBakedActors, + BakedObjectData); + } + else if (Component->IsA() + && (!InInstancerComponentTypesToBake || InInstancerComponentTypesToBake->Contains(EHoudiniInstancerComponentType::StaticMeshComponent))) + { + FHoudiniEngineBakedActor BakedActorEntry; + if (BakeInstancerOutputToActors_SMC( + InCookable, + InOutputIndex, + InAllOutputs, + InBakeState, + HGPOs, + Pair.Key, + CurrentOutputObject, + InBakeFolder, + InTempCookFolder, + BakeSettings, + AllBakedActors, + BakedActorEntry, + BakedObjectData, + InOutAlreadyBakedStaticMeshMap, + InOutAlreadyBakedMaterialsMap, + InFallbackActor, + InFallbackWorldOutlinerFolder)) + { + OutputBakedActors.Add(BakedActorEntry); + } + + } + else + { + // Unsupported component! + } + + + AllBakedActors.Append(OutputBakedActors); + NewBakedActors.Append(OutputBakedActors); + } + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + + // Bake any level instances. They will be stored on the OutputActors member, + // but we want to return one output for all instances on this output, so do + // them all at once. + + if (!CurrentOutputObject.OutputActors.IsEmpty()) + { + OutputBakedActors.Reset(); + + FHoudiniEngineBakedActor BakedActorEntry; + if (BakeInstancerOutputToActors_LevelInstances( + InCookable, + InOutputIndex, + InAllOutputs, + InBakeState, + Pair.Key, + CurrentOutputObject, + InBakeFolder, + InTempCookFolder, + BakeSettings, + AllBakedActors, + BakedActorEntry, + BakedObjectData, + InOutAlreadyBakedStaticMeshMap, + InOutAlreadyBakedMaterialsMap, + InFallbackActor, + InFallbackWorldOutlinerFolder)) + { + OutputBakedActors.Add(BakedActorEntry); + } + + AllBakedActors.Append(OutputBakedActors); + NewBakedActors.Append(OutputBakedActors); + } + +#endif + } + + OutActors = MoveTemp(NewBakedActors); + + return true; +} + +bool +FHoudiniEngineBakeUtils::BakeInstancerOutputToActors_ISMC( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const TArray& InHGPOs, + const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, + const FHoudiniOutputObject& InOutputObject, + const FTransform& InTransform, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder) +{ + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(InOutputIndex, InOutputObjectIdentifier, bHasPreviousBakeData); + + for(auto Component : InOutputObject.OutputComponents) + { + UInstancedStaticMeshComponent * InISMC = Cast(Component); + if (!IsValid(InISMC)) + continue; + + AActor * OwnerActor = InISMC->GetOwner(); + if (!IsValid(OwnerActor)) + return false; + + UStaticMesh * StaticMesh = InISMC->GetStaticMesh(); + if (!IsValid(StaticMesh)) + return false; + + // Find the HGPO that matches this output identifier + const FHoudiniGeoPartObject* FoundHGPO = nullptr; + FindHGPO(InOutputObjectIdentifier, InHGPOs, FoundHGPO); + + // Certain SMC materials may need to be duplicated if we didn't generate the mesh object. + // Map of duplicated overrides materials (oldTempMaterial , newBakedMaterial) + TMap DuplicatedISMCOverrideMaterials; + + const EPackageReplaceMode AssetPackageReplaceMode = BakeSettings.bReplaceAssets ? + EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; + UWorld* DesiredWorld = OwnerActor ? OwnerActor->GetWorld() : GWorld; + + // Determine if the incoming mesh is temporary + // If not temporary set the ObjectName from its package. (Also use this as a fallback default) + FString ObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(StaticMesh); + UStaticMesh* PreviousStaticMesh = Cast(BakedOutputObject.GetBakedObjectIfValid()); + UStaticMesh* MeshForInstancing = nullptr; + + // Construct PackageParams for the instancer itself. When baking to actor we technically won't create a stand-alone + // disk package for the instancer, but certain attributes (such as level path) use tokens populated from the package params. + FHoudiniPackageParams InstancerPackageParams; + FHoudiniAttributeResolver InstancerResolver; + FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( + DesiredWorld, InCookable, InOutputObjectIdentifier, InOutputObject, bHasPreviousBakeData, ObjectName, + InstancerPackageParams, InstancerResolver, InBakeFolder.Path, AssetPackageReplaceMode); + + FHoudiniPackageParams MeshPackageParams; + FString BakeFolderPath = FString(); + const bool bIsTemporary = IsObjectTemporary(StaticMesh, EHoudiniOutputType::Mesh, InAllOutputs, InstancerPackageParams.TempCookFolder, InstancerPackageParams.ComponentGUID); + if (!bIsTemporary) + { + // We can reuse the mesh + MeshForInstancing = StaticMesh; + } + else + { + BakeFolderPath = InBakeFolder.Path; + + // See if we can find the mesh in the outputs + FHoudiniBakedOutputObject MeshBakedOutputObject; + int32 MeshOutputIndex = INDEX_NONE; + FHoudiniOutputObjectIdentifier MeshIdentifier; + const bool bFoundMeshOutput = FindOutputObject(StaticMesh, EHoudiniOutputType::Mesh, InAllOutputs, MeshOutputIndex, MeshIdentifier); + if(bFoundMeshOutput) + { + FHoudiniAttributeResolver MeshResolver; + // Found the instanced mesh in the mesh outputs + const FHoudiniOutputObject& MeshOutputObject = InAllOutputs[MeshOutputIndex]->GetOutputObjects().FindChecked(MeshIdentifier); + + // Check if the mesh itself has previous bake data + bool bMeshHasPreviousBakeData = false; + MeshBakedOutputObject = InBakeState.MakeNewBakedOutputObject(MeshOutputIndex, MeshIdentifier, bMeshHasPreviousBakeData); + + FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( + DesiredWorld, InCookable, MeshIdentifier, MeshOutputObject, bMeshHasPreviousBakeData, ObjectName, + MeshPackageParams, MeshResolver, InBakeFolder.Path, AssetPackageReplaceMode); + // Update with resolved object name + ObjectName = MeshPackageParams.ObjectName; + BakeFolderPath = MeshPackageParams.BakeFolder; + } + + // This will bake/duplicate the mesh if temporary, or return the input one if it is not + MeshForInstancing = FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( + StaticMesh, PreviousStaticMesh, MeshPackageParams, InAllOutputs, InBakedActors, InTempCookFolder.Path, + BakedObjectData, InOutAlreadyBakedStaticMeshMap, InOutAlreadyBakedMaterialsMap); + + MeshBakedOutputObject.BakedObject = FSoftObjectPath(MeshForInstancing).ToString(); + InBakeState.SetNewBakedOutputObject(MeshOutputIndex, MeshIdentifier, MeshBakedOutputObject); + } + + // We may need to duplicate materials overrides if they are temporary + // (typically, material instances generated by the plugin) + TArray Materials = InISMC->GetMaterials(); + for (int32 MaterialIdx = 0; MaterialIdx < Materials.Num(); ++MaterialIdx) + { + UMaterialInterface* MaterialInterface = Materials[MaterialIdx]; + if (!IsValid(MaterialInterface)) + continue; + + // Only duplicate the material if it is temporary + if (IsObjectTemporary(MaterialInterface, EHoudiniOutputType::Invalid, InAllOutputs, InTempCookFolder.Path, InstancerPackageParams.ComponentGUID)) + { + UMaterialInterface* DuplicatedMaterial = BakeSingleMaterialToPackage( + MaterialInterface, InstancerPackageParams, BakedObjectData, InOutAlreadyBakedMaterialsMap); + DuplicatedISMCOverrideMaterials.Add(MaterialInterface, DuplicatedMaterial); + } + } + + // Instancer name adds the split identifier (INSTANCERNUM_VARIATIONNUM) + FString InstancerName = ObjectName + "_instancer"; + if (InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2)) + InstancerName = InOutputObject.CachedAttributes[HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2]; + InstancerName += "_" + InOutputObjectIdentifier.SplitIdentifier; + + + const FName WorldOutlinerFolderPath = GetOutlinerFolderPath( + InstancerResolver, + FName(InFallbackWorldOutlinerFolder.IsEmpty() ? InstancerPackageParams.HoudiniAssetActorName : InFallbackWorldOutlinerFolder)); + + // By default spawn in the current level unless specified via the unreal_level_path attribute + ULevel* DesiredLevel = DesiredWorld ? DesiredWorld->GetCurrentLevel() : GWorld->GetCurrentLevel(); + bool bHasLevelPathAttribute = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_LEVEL_PATH); + if (bHasLevelPathAttribute) + { + // Get the package path from the unreal_level_apth attribute + FString LevelPackagePath = InstancerResolver.ResolveFullLevelPath(); + + bool bCreatedPackage = false; + if (!FHoudiniEngineBakeUtils::FindOrCreateDesiredLevelFromLevelPath( + LevelPackagePath, + DesiredLevel, + DesiredWorld, + bCreatedPackage)) + { + // TODO: LOG ERROR IF NO LEVEL + return false; + } + + // If we have created a new level, add it to the packages to save + // TODO: ? always add? + if (bCreatedPackage && DesiredLevel) + { + BakedObjectData.BakeStats.NotifyPackageCreated(1); + BakedObjectData.BakeStats.NotifyObjectsCreated(DesiredLevel->GetClass()->GetName(), 1); + // We can now save the package again, and unload it. + BakedObjectData.PackagesToSave.Add(DesiredLevel->GetOutermost()); + } + } + + if(!DesiredLevel) + return false; + + // Try to find the unreal_bake_actor, if specified, or fallback to the default named actor + FName BakeActorName; + AActor* FoundActor = nullptr; + bool bHasBakeActorName = false; + FName DefaultActorName = *InstancerName; + if (BakeSettings.ActorBakeOption == EHoudiniEngineActorBakeOption::OneActorPerHDA) + { + FHoudiniAttributeResolver OutResolver; + TMap Tokens = InOutputObject.CachedTokens; + InstancerPackageParams.UpdateTokensFromParams(InCookable->GetWorld(), InCookable->GetComponent(), Tokens); + OutResolver.SetTokensFromStringMap(Tokens); + DefaultActorName = FName(OutResolver.ResolveString(BakeSettings.DefaultBakeName)); + } + FindUnrealBakeActor( + InOutputObject, BakedOutputObject, InBakedActors, DesiredLevel, + DefaultActorName, BakeSettings, InFallbackActor, FoundActor, + bHasBakeActorName, BakeActorName); + + // Store the initial tags that the FoundActor spawned with. + // We will be adding additional tags from the HGPOs. + TArray ActorTags; + + // Should we create one actor with an ISMC or multiple actors with one SMC? + bool bSpawnMultipleSMC = false; + if (bSpawnMultipleSMC) + { + // Deactivated for now, as generating multiple actors curently has issues with BakeReplace mode. + // A similar result could be achieve by specifying individual actor names and splitting the instancer to multiple components + // (via unreal_bake_actor and unreal_split_attr) + // Get the StaticMesh ActorFactory + TSubclassOf BakeActorClass = nullptr; + UActorFactory* ActorFactory = GetActorFactory(NAME_None, BakeSettings, BakeActorClass, UActorFactoryStaticMesh::StaticClass(), MeshForInstancing); + if (!ActorFactory) + return false; + + // Split the instances to multiple StaticMeshActors + for (int32 InstanceIdx = 0; InstanceIdx < InISMC->GetInstanceCount(); InstanceIdx++) + { + FTransform InstanceTransform; + InISMC->GetInstanceTransform(InstanceIdx, InstanceTransform, true); + + FName BakeActorNameWithIndex = FName(BakeActorName.ToString() + "_instance_" + FString::FromInt(InstanceIdx), InstanceIdx); + FoundActor = nullptr; + FindUnrealBakeActor(InOutputObject, BakedOutputObject, InBakedActors, DesiredLevel, *InstancerName, BakeSettings, InFallbackActor, FoundActor, bHasBakeActorName, BakeActorName); + + if (!FoundActor) + { + FoundActor = SpawnBakeActor( + ActorFactory, MeshForInstancing, DesiredLevel, BakeSettings, + InstanceTransform, InCookable->GetComponent(), BakeActorClass); + + if (!IsValid(FoundActor)) + continue; + } + + // Capture the current tags on the actor, in case we need to keep them. + ActorTags = FoundActor->Tags; + + const FString NewNameStr = MakeUniqueObjectNameIfNeeded( + DesiredLevel, ActorFactory->NewActorClass, BakeActorName.ToString(), FoundActor); + RenameAndRelabelActor(FoundActor, NewNameStr, false); + + // The folder is named after the original actor and contains all generated actors + SetOutlinerFolderPath(FoundActor, WorldOutlinerFolderPath); + + AStaticMeshActor* SMActor = Cast(FoundActor); + if (!IsValid(SMActor)) + continue; + + // Copy properties from the existing component + CopyPropertyToNewActorAndComponent(FoundActor, SMActor->GetStaticMeshComponent(), InISMC); + + // Restore the actor tags, in case we want to keep them. + FHoudiniEngineUtils::KeepOrClearActorTags(FoundActor, true, true, FoundHGPO); + if (FoundHGPO) + { + FHoudiniEngineUtils::ApplyTagsToActorAndComponents(FoundActor, FHoudiniEngineUtils::IsKeepTagsEnabled(FoundHGPO), FoundHGPO->GenericPropertyAttributes); + } + + FHoudiniEngineBakedActor& OutputEntry = OutActors.Add_GetRef(FHoudiniEngineBakedActor( + FoundActor, + BakeActorName, + WorldOutlinerFolderPath, + InOutputIndex, + InOutputObjectIdentifier, + MeshForInstancing, + StaticMesh, + SMActor->GetStaticMeshComponent(), + BakeFolderPath, + MeshPackageParams)); + + OutputEntry.bInstancerOutput = true; + OutputEntry.InstancerPackageParams = InstancerPackageParams; + } + } + else + { + bool bSpawnedActor = false; + if (!FoundActor) + { + // Only create one actor + FActorSpawnParameters SpawnInfo; + SpawnInfo.OverrideLevel = DesiredLevel; + SpawnInfo.ObjectFlags = RF_Transactional; + + if (!DesiredLevel->bUseExternalActors) + { + SpawnInfo.Name = FName(MakeUniqueObjectNameIfNeeded(DesiredLevel, AActor::StaticClass(), BakeActorName.ToString())); + } + SpawnInfo.bDeferConstruction = true; + + // Spawn the new Actor + UClass* ActorClass = GetBakeActorClassOverride(InOutputObject); + if (!ActorClass) + ActorClass = AActor::StaticClass(); + FoundActor = DesiredLevel->OwningWorld->SpawnActor(ActorClass, SpawnInfo); + if (!IsValid(FoundActor)) + return false; + bSpawnedActor = true; + + BakedObjectData.BakeStats.NotifyObjectsCreated(FoundActor->GetClass()->GetName(), 1); + + FHoudiniEngineRuntimeUtils::SetActorLabel(FoundActor, BakeActorName.ToString()); + FoundActor->SetActorHiddenInGame(InISMC->bHiddenInGame); + } + else + { + // If there is a previously baked component, and we are in replace mode, remove it + if (BakeSettings.bReplaceAssets) + { + USceneComponent* InPrevComponent = Cast(BakedOutputObject.GetBakedComponentIfValid()); + if (IsValid(InPrevComponent) && InPrevComponent->GetOwner() == FoundActor) + RemovePreviouslyBakedComponent(InPrevComponent); + } + + const FString UniqueActorNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, AActor::StaticClass(), BakeActorName.ToString(), FoundActor); + RenameAndRelabelActor(FoundActor, UniqueActorNameStr, false); + + BakedObjectData.BakeStats.NotifyObjectsUpdated(FoundActor->GetClass()->GetName(), 1); + } + + // Capture the current actor tags, in case the user wants to keep them. + ActorTags = FoundActor->Tags; + + // The folder is named after the original actor and contains all generated actors + SetOutlinerFolderPath(FoundActor, WorldOutlinerFolderPath); + + // Get/create the actor's root component + const bool bCreateIfMissing = true; + USceneComponent* RootComponent = GetActorRootComponent(FoundActor, bCreateIfMissing); + if (bSpawnedActor && IsValid(RootComponent)) + RootComponent->SetWorldTransform(InTransform); + + // Duplicate the instancer component, create a Hierarchical ISMC if needed + UInstancedStaticMeshComponent* NewISMC = nullptr; + UHierarchicalInstancedStaticMeshComponent* InHISMC = Cast(InISMC); + if (InHISMC) + { + // Handle foliage: don't duplicate foliage component, create a new hierarchical one and copy what we can + // from the foliage component + if (InHISMC->IsA()) + { + NewISMC = NewObject( + FoundActor, + FName(MakeUniqueObjectNameIfNeeded(FoundActor, InHISMC->GetClass(), InISMC->GetName()))); + CopyPropertyToNewActorAndComponent(FoundActor, NewISMC, InISMC); + + BakedObjectData.BakeStats.NotifyObjectsCreated(UHierarchicalInstancedStaticMeshComponent::StaticClass()->GetName(), 1); + } + else + { + NewISMC = DuplicateObject( + InHISMC, + FoundActor, + FName(MakeUniqueObjectNameIfNeeded(FoundActor, InHISMC->GetClass(), InISMC->GetName()))); + + BakedObjectData.BakeStats.NotifyObjectsCreated(InHISMC->GetClass()->GetName(), 1); + } + } + else + { + NewISMC = DuplicateObject( + InISMC, + FoundActor, + FName(MakeUniqueObjectNameIfNeeded(FoundActor, InISMC->GetClass(), InISMC->GetName()))); + + BakedObjectData.BakeStats.NotifyObjectsCreated(InISMC->GetClass()->GetName(), 1); + } + + if (!NewISMC) + { + return false; + } + + BakedOutputObject.BakedComponent = FSoftObjectPath(NewISMC).ToString(); + + NewISMC->RegisterComponent(); + NewISMC->SetStaticMesh(MeshForInstancing); + FoundActor->AddInstanceComponent(NewISMC); + + if (DuplicatedISMCOverrideMaterials.Num() > 0) + { + // If we have baked some temporary materials, make sure to update them on the new component + for (int32 Idx = 0; Idx < NewISMC->OverrideMaterials.Num(); Idx++) + { + UMaterialInterface* CurMat = NewISMC->GetMaterial(Idx); + + UMaterialInterface** DuplicatedMat = DuplicatedISMCOverrideMaterials.Find(CurMat); + if (!DuplicatedMat || !IsValid(*DuplicatedMat)) + continue; + + NewISMC->SetMaterial(Idx, *DuplicatedMat); + } + } + + // NewActor->SetRootComponent(NewISMC); + if (IsValid(RootComponent)) + NewISMC->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); + NewISMC->SetWorldTransform(InISMC->GetComponentTransform()); + + // TODO: do we need to copy properties here, we duplicated the component + // // Copy properties from the existing component + // CopyPropertyToNewActorAndComponent(FoundActor, NewISMC, InISMC); + + FHoudiniEngineUtils::KeepOrClearActorTags(FoundActor, true, false, FoundHGPO); + if (FoundHGPO) + { + // Add actor tags from generic property attributes + FHoudiniEngineUtils::ApplyTagsToActorOnly(FoundHGPO->GenericPropertyAttributes, FoundActor->Tags); + } + + if (bSpawnedActor) + FoundActor->FinishSpawning(InTransform); + + BakedOutputObject.Actor = FSoftObjectPath(FoundActor).ToString(); + FHoudiniEngineBakedActor& OutputEntry = OutActors.Add_GetRef(FHoudiniEngineBakedActor( + FoundActor, + BakeActorName, + WorldOutlinerFolderPath, + InOutputIndex, + InOutputObjectIdentifier, + MeshForInstancing, + StaticMesh, + NewISMC, + BakeFolderPath, + MeshPackageParams)); + OutputEntry.bInstancerOutput = true; + OutputEntry.InstancerPackageParams = InstancerPackageParams; + + // Postpone post-bake calls to do them once per actor + OutActors.Last().bPostBakeProcessPostponed = true; + } + + // If we are baking in replace mode, remove previously baked components/instancers + if (BakeSettings.bReplaceActors && BakeSettings.bReplaceAssets) + { + const bool bInDestroyBakedComponent = false; + const bool bInDestroyBakedInstancedActors = true; + const bool bInDestroyBakedInstancedComponents = true; + DestroyPreviousBakeOutput( + BakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); + } + } + + // Set the updated baked output data in the state + InBakeState.SetNewBakedOutputObject(InOutputIndex, InOutputObjectIdentifier, BakedOutputObject); + + return true; +} + +bool FHoudiniEngineBakeUtils::BakeInstancerOutputToActors_LevelInstances( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, + const FHoudiniOutputObject& InOutputObject, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + FHoudiniEngineBakedActor& OutBakedActorEntry, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject( + InOutputIndex, InOutputObjectIdentifier, bHasPreviousBakeData); + + FString ObjectName; + UWorld* World = InCookable->GetWorld(); + + const EPackageReplaceMode AssetPackageReplaceMode = BakeSettings.bReplaceAssets ? + EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; + + FHoudiniPackageParams InstancerPackageParams; + FHoudiniAttributeResolver InstancerResolver; + FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( + World, InCookable, InOutputObjectIdentifier, InOutputObject, bHasPreviousBakeData, ObjectName, + InstancerPackageParams, InstancerResolver, InBakeFolder.Path, AssetPackageReplaceMode); + + const FName OutlinerPath = GetOutlinerFolderPath( + InstancerResolver, FName(InFallbackWorldOutlinerFolder.IsEmpty() ? InstancerPackageParams.HoudiniAssetActorName : InFallbackWorldOutlinerFolder)); + + for (auto & Actor : InOutputObject.OutputActors) + { + ALevelInstance* LevelInstance = Cast(Actor.Get()); + if (!IsValid(LevelInstance)) + continue; + + // Determine the name for the baked actor. Destroy any old ones if needed. + const FString* BackActorPrefix = InOutputObject.CachedAttributes.Find(HAPI_UNREAL_ATTRIB_BAKE_ACTOR); + FName BakedName; + + if (BackActorPrefix == nullptr || BackActorPrefix->IsEmpty()) + { + BakedName = LevelInstance->GetFName(); + } + else + { + BakedName = FName(*BackActorPrefix); + } + + // If replacing existing bake assets, find thsoe actors with the same name and delete them. But only if they are not + // attached to the HDA Actor as this means they are cooked (temp) objects. + + if(AssetPackageReplaceMode == EPackageReplaceMode::ReplaceExistingAssets) + { + TArray Actors = FHoudiniEngineUtils::FindActorsWithNameNoNumber(AActor::StaticClass(), World, BakedName.GetPlainNameString()); + for (AActor* OldBakedActor : Actors) + { + if (OldBakedActor->GetOwner() != InCookable->GetOwner()) + { + OldBakedActor->Destroy(); + } + } + } + + FActorSpawnParameters Parameters; + Parameters.Template = LevelInstance; + Parameters.Name = BakedName; + Parameters.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested; + AActor * BakedActor = World->SpawnActor(Parameters); + BakedActor->bDefaultOutlinerExpansionState = false; + BakedActor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); + BakedActor->SetActorTransform(LevelInstance->GetActorTransform()); // WHY IS THIS NEEDED? Don't know, but it is... + + BakedActor->SetActorLabel(LevelInstance->GetActorLabel()); + BakedObjectData.BakeStats.NotifyObjectsCreated(BakedActor->GetClass()->GetName(), 1); + BakedActor->SetActorLabel(BakedName.ToString()); + BakedActor->SetFolderPath(OutlinerPath); + + BakedOutputObject.LevelInstanceActors.Add(BakedActor->GetPathName()); + + if (InCookable->GetRemoveOutputAfterBake()) + { + LevelInstance->Destroy(); + } + + OutBakedActorEntry.OutputIndex = InOutputIndex; + OutBakedActorEntry.Actor = BakedActor; + OutBakedActorEntry.ActorBakeName = FName(BakedActor->GetName()); + OutBakedActorEntry.OutputObjectIdentifier = InOutputObjectIdentifier; + + } + + // Set the updated baked output object in the state + InBakeState.SetNewBakedOutputObject(InOutputIndex, InOutputObjectIdentifier, BakedOutputObject); + + return true; + +#else + return false; +#endif +} + +bool +FHoudiniEngineBakeUtils::BakeInstancerOutputToActors_SMC( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const TArray& InHGPOs, + const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, + const FHoudiniOutputObject& InOutputObject, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + FHoudiniEngineBakedActor& OutBakedActorEntry, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder) +{ + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject( + InOutputIndex, InOutputObjectIdentifier, bHasPreviousBakeData); + + for(auto Component : InOutputObject.OutputComponents) + { + UStaticMeshComponent* InSMC = Cast(Component); + if (!IsValid(InSMC)) + return false; + + AActor* OwnerActor = InSMC->GetOwner(); + if (!IsValid(OwnerActor)) + return false; + + UStaticMesh* StaticMesh = InSMC->GetStaticMesh(); + if (!IsValid(StaticMesh)) + return false; + + // Find the HGPO that matches this output identifier + const FHoudiniGeoPartObject* FoundHGPO = nullptr; + FindHGPO(InOutputObjectIdentifier, InHGPOs, FoundHGPO); + + UWorld* DesiredWorld = OwnerActor ? OwnerActor->GetWorld() : GWorld; + const EPackageReplaceMode AssetPackageReplaceMode = BakeSettings.bReplaceAssets ? + EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; + + // Certain SMC materials may need to be duplicated if we didn't generate the mesh object. + // Map of duplicated overrides materials (oldTempMaterial , newBakedMaterial) + TMap DuplicatedSMCOverrideMaterials; + + // Determine if the incoming mesh is temporary by looking for it in the mesh outputs. Populate mesh package params + // for baking from it. + // If not temporary set the ObjectName from the its package. (Also use this as a fallback default) + FString ObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(StaticMesh); + UStaticMesh* PreviousStaticMesh = Cast(BakedOutputObject.GetBakedObjectIfValid()); + UStaticMesh* BakedStaticMesh = nullptr; + + // Package params for the instancer + // See if the instanced static mesh is still a temporary Houdini created Static Mesh + // If it is, we need to bake the StaticMesh first + FHoudiniPackageParams InstancerPackageParams; + // Configure FHoudiniAttributeResolver and fill the package params with resolved object name and bake folder. + // The resolver is then also configured with the package params for subsequent resolving (level_path etc) + FHoudiniAttributeResolver InstancerResolver; + FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( + DesiredWorld, InCookable, InOutputObjectIdentifier, InOutputObject, bHasPreviousBakeData, ObjectName, + InstancerPackageParams, InstancerResolver, InBakeFolder.Path, AssetPackageReplaceMode); + + FHoudiniPackageParams MeshPackageParams; + FString BakeFolderPath = FString(); + const bool bIsTemporary = IsObjectTemporary( + StaticMesh, EHoudiniOutputType::Mesh, InAllOutputs, InstancerPackageParams.TempCookFolder, InstancerPackageParams.ComponentGUID); + if (!bIsTemporary) + { + // We can reuse the mesh + BakedStaticMesh = StaticMesh; + } + else + { + // See if we can find the mesh in the outputs + FHoudiniBakedOutputObject MeshBakedOutputObject; + int32 MeshOutputIndex = INDEX_NONE; + FHoudiniOutputObjectIdentifier MeshIdentifier = InOutputObjectIdentifier; + BakeFolderPath = InBakeFolder.Path; + const bool bFoundMeshOutput = FindOutputObject( + StaticMesh, EHoudiniOutputType::Mesh, InAllOutputs, MeshOutputIndex, MeshIdentifier); + if (bFoundMeshOutput) + { + FHoudiniAttributeResolver MeshResolver; + + // Found the instanced mesh in the mesh outputs + const FHoudiniOutputObject& MeshOutputObject = InAllOutputs[MeshOutputIndex]->GetOutputObjects().FindChecked(MeshIdentifier); + bool bMeshHasPreviousBakeData = false; + MeshBakedOutputObject = InBakeState.MakeNewBakedOutputObject(MeshOutputIndex, MeshIdentifier, bMeshHasPreviousBakeData); + + FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( + DesiredWorld, InCookable, MeshIdentifier, MeshOutputObject, bMeshHasPreviousBakeData, ObjectName, + MeshPackageParams, MeshResolver, InBakeFolder.Path, AssetPackageReplaceMode); + // Update with resolved object name + ObjectName = MeshPackageParams.ObjectName; + BakeFolderPath = MeshPackageParams.BakeFolder; + } + + // This will bake/duplicate the mesh if temporary, or return the input one if it is not + BakedStaticMesh = FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( + StaticMesh, PreviousStaticMesh, MeshPackageParams, InAllOutputs, InBakedActors, InTempCookFolder.Path, + BakedObjectData, InOutAlreadyBakedStaticMeshMap, InOutAlreadyBakedMaterialsMap); + + MeshBakedOutputObject.BakedObject = FSoftObjectPath(BakedStaticMesh).ToString(); + InBakeState.SetNewBakedOutputObject(MeshOutputIndex, MeshIdentifier, MeshBakedOutputObject); + } + + // We may need to duplicate materials overrides if they are temporary + // (typically, material instances generated by the plugin) + TArray Materials = InSMC->GetMaterials(); + for (int32 MaterialIdx = 0; MaterialIdx < Materials.Num(); ++MaterialIdx) + { + UMaterialInterface* MaterialInterface = Materials[MaterialIdx]; + if (!IsValid(MaterialInterface)) + continue; + + // Only duplicate the material if it is temporary + if (IsObjectTemporary(MaterialInterface, EHoudiniOutputType::Invalid, InAllOutputs, InTempCookFolder.Path, InstancerPackageParams.ComponentGUID)) + { + UMaterialInterface* DuplicatedMaterial = BakeSingleMaterialToPackage( + MaterialInterface, InstancerPackageParams, BakedObjectData, InOutAlreadyBakedMaterialsMap); + DuplicatedSMCOverrideMaterials.Add(MaterialInterface, DuplicatedMaterial); + } + } + + // Update the previous baked object + BakedOutputObject.BakedObject = FSoftObjectPath(BakedStaticMesh).ToString(); + + // Instancer name adds the split identifier (INSTANCERNUM_VARIATIONNUM) + FString InstancerName = ObjectName + "_instancer"; + if (InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2)) + InstancerName = InOutputObject.CachedAttributes[HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2]; + InstancerName += "_" + InOutputObjectIdentifier.SplitIdentifier; + + const FName WorldOutlinerFolderPath = GetOutlinerFolderPath( + InstancerResolver, + FName(InFallbackWorldOutlinerFolder.IsEmpty() ? InstancerPackageParams.HoudiniAssetActorName : InFallbackWorldOutlinerFolder)); + + // By default spawn in the current level unless specified via the unreal_level_path attribute + ULevel* DesiredLevel = DesiredWorld ? DesiredWorld->GetCurrentLevel() : GWorld->GetCurrentLevel(); + bool bHasLevelPathAttribute = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_LEVEL_PATH); + if (bHasLevelPathAttribute) + { + // Get the package path from the unreal_level_apth attribute + FString LevelPackagePath = InstancerResolver.ResolveFullLevelPath(); + + bool bCreatedPackage = false; + if (!FHoudiniEngineBakeUtils::FindOrCreateDesiredLevelFromLevelPath( + LevelPackagePath, + DesiredLevel, + DesiredWorld, + bCreatedPackage)) + { + // TODO: LOG ERROR IF NO LEVEL + return false; + } + + // If we have created a level, add it to the packages to save + // TODO: ? always add? + if (bCreatedPackage && DesiredLevel) + { + BakedObjectData.BakeStats.NotifyPackageCreated(1); + BakedObjectData.BakeStats.NotifyObjectsCreated(DesiredLevel->GetClass()->GetName(), 1); + // We can now save the package again, and unload it. + BakedObjectData.PackagesToSave.Add(DesiredLevel->GetOutermost()); + } + } + + if (!DesiredLevel) + return false; + + // Try to find the unreal_bake_actor, if specified + FName BakeActorName; + AActor* FoundActor = nullptr; + bool bHasBakeActorName = false; + FName DefaultBakeActorName = *InstancerName; + if (BakeSettings.ActorBakeOption == EHoudiniEngineActorBakeOption::OneActorPerHDA) + { + FHoudiniAttributeResolver OutResolver; + TMap Tokens = InOutputObject.CachedTokens; + InstancerPackageParams.UpdateTokensFromParams(InCookable->GetWorld(), InCookable->GetComponent(), Tokens); + OutResolver.SetTokensFromStringMap(Tokens); + DefaultBakeActorName = FName(OutResolver.ResolveString(BakeSettings.DefaultBakeName)); + } + FindUnrealBakeActor(InOutputObject, BakedOutputObject, InBakedActors, DesiredLevel, DefaultBakeActorName, BakeSettings, InFallbackActor, FoundActor, bHasBakeActorName, BakeActorName); + + UStaticMeshComponent* StaticMeshComponent = nullptr; + // Create an actor if we didn't find one + bool bCreatedNewActor = false; + if (!FoundActor) + { + // Get the actor factory for the unreal_bake_actor_class attribute. If not set, use an empty actor. + TSubclassOf BakeActorClass = nullptr; + UActorFactory* ActorFactory = GetActorFactory( + InOutputObject, BakeSettings, BakeActorClass, UActorFactoryEmptyActor::StaticClass(), BakedStaticMesh); + if (!ActorFactory) + { + return false; + } + + FoundActor = SpawnBakeActor( + ActorFactory, BakedStaticMesh, DesiredLevel, BakeSettings, + InSMC->GetComponentTransform(), InCookable->GetComponent(), BakeActorClass); + if (!IsValid(FoundActor)) + return false; + + BakedObjectData.BakeStats.NotifyObjectsCreated(FoundActor->GetClass()->GetName(), 1); + bCreatedNewActor = true; + + // If the factory created a static mesh actor, get its component. + AStaticMeshActor* SMActor = Cast(FoundActor); + if (IsValid(SMActor)) + StaticMeshComponent = SMActor->GetStaticMeshComponent(); + } + + // We have an existing actor (or we created one without a static mesh component) + if (!IsValid(StaticMeshComponent)) + { + USceneComponent* RootComponent = GetActorRootComponent(FoundActor); + if (!IsValid(RootComponent)) + return false; + + if (BakeSettings.bReplaceAssets && !bCreatedNewActor) + { + // Check if we have a previous bake component and that it belongs to FoundActor, if so, reuse it + UStaticMeshComponent* PrevSMC = Cast(BakedOutputObject.GetBakedComponentIfValid()); + if (IsValid(PrevSMC) && (PrevSMC->GetOwner() == FoundActor)) + { + StaticMeshComponent = PrevSMC; + } + } + + if (!IsValid(StaticMeshComponent)) + { + // Create a new static mesh component + StaticMeshComponent = NewObject(FoundActor, NAME_None, RF_Transactional); + + FoundActor->AddInstanceComponent(StaticMeshComponent); + StaticMeshComponent->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); + StaticMeshComponent->RegisterComponent(); + + BakedObjectData.BakeStats.NotifyObjectsCreated(StaticMeshComponent->GetClass()->GetName(), 1); + } + } + + const FString NewNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, FoundActor->GetClass(), BakeActorName.ToString(), FoundActor); + RenameAndRelabelActor(FoundActor, NewNameStr, false); + + // The folder is named after the original actor and contains all generated actors + SetOutlinerFolderPath(FoundActor, WorldOutlinerFolderPath); + + // Update the previous baked component + BakedOutputObject.BakedComponent = FSoftObjectPath(StaticMeshComponent).ToString(); + + if (!IsValid(StaticMeshComponent)) + return false; + + // Copy properties from the existing component + const bool bCopyWorldTransform = true; + CopyPropertyToNewActorAndComponent(FoundActor, StaticMeshComponent, InSMC, bCopyWorldTransform); + StaticMeshComponent->SetStaticMesh(BakedStaticMesh); + + // Keep or clear existing actor tags + FHoudiniEngineUtils::KeepOrClearActorTags(FoundActor, true, false, FoundHGPO); + if (FoundHGPO) + { + // Add actor tags from generic property attributes + FHoudiniEngineUtils::ApplyTagsToActorOnly(FoundHGPO->GenericPropertyAttributes, FoundActor->Tags); + } + + if (StaticMeshComponent && IsValid(StaticMeshComponent) && DuplicatedSMCOverrideMaterials.Num() > 0) + { + // If we have baked some temporary materials, make sure to update them on the new component + for (int32 Idx = 0; Idx < StaticMeshComponent->OverrideMaterials.Num(); Idx++) + { + UMaterialInterface* CurMat = StaticMeshComponent->GetMaterial(Idx); + + UMaterialInterface** DuplicatedMat = DuplicatedSMCOverrideMaterials.Find(CurMat); + if (!DuplicatedMat || !IsValid(*DuplicatedMat)) + continue; + + StaticMeshComponent->SetMaterial(Idx, *DuplicatedMat); + } + } + + BakedOutputObject.Actor = FSoftObjectPath(FoundActor).ToString(); + FHoudiniEngineBakedActor OutputEntry( + FoundActor, + BakeActorName, + WorldOutlinerFolderPath, + InOutputIndex, + InOutputObjectIdentifier, + BakedStaticMesh, + StaticMesh, + StaticMeshComponent, + BakeFolderPath, + MeshPackageParams); + OutputEntry.bInstancerOutput = true; + OutputEntry.InstancerPackageParams = InstancerPackageParams; + + OutBakedActorEntry = OutputEntry; + + // If we are baking in replace mode, remove previously baked components/instancers + if (BakeSettings.bReplaceActors && BakeSettings.bReplaceAssets) + { + const bool bInDestroyBakedComponent = false; + const bool bInDestroyBakedInstancedActors = true; + const bool bInDestroyBakedInstancedComponents = true; + DestroyPreviousBakeOutput( + BakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); + } + } + + // Set the updated baked output object in the state + InBakeState.SetNewBakedOutputObject(InOutputIndex, InOutputObjectIdentifier, BakedOutputObject); + + return true; +} + + +bool +FHoudiniEngineBakeUtils::BakeInstancerOutputToActors_IAC( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InHGPOs, + const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, + const FHoudiniOutputObject& InOutputObject, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData) +{ + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject( + InOutputIndex, InOutputObjectIdentifier, bHasPreviousBakeData); + + for (auto Component : InOutputObject.OutputComponents) + { + UHoudiniInstancedActorComponent* InIAC = Cast(Component); + if (!IsValid(InIAC)) + { + continue; + } + + AActor* OwnerActor = InIAC->GetOwner(); + if (!IsValid(OwnerActor)) + { + return false; + } + + // Get the object instanced by this IAC + UObject* InstancedObject = InIAC->GetInstancedObject(); + if (!IsValid(InstancedObject)) + { + return false; + } + + // Find the HGPO for this instanced output + bool FoundHGPO = false; + FHoudiniGeoPartObject InstancerHGPO; + for (const auto& curHGPO : InHGPOs) + { + if (InOutputObjectIdentifier.Matches(curHGPO)) + { + InstancerHGPO = curHGPO; + FoundHGPO = true; + break; + } + } + + // Set the default object name to the + const FString DefaultObjectName = InstancedObject->GetName(); + + FHoudiniPackageParams PackageParams; + const EPackageReplaceMode AssetPackageReplaceMode = BakeSettings.bReplaceAssets + ? EPackageReplaceMode::ReplaceExistingAssets + : EPackageReplaceMode::CreateNewAssets; + // Configure FHoudiniAttributeResolver and fill the package params with resolved object name and bake folder. + // The resolver is then also configured with the package params for subsequent resolving (level_path etc) + FHoudiniAttributeResolver Resolver; + UWorld* DesiredWorld = OwnerActor ? OwnerActor->GetWorld() : GWorld; + FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( + DesiredWorld, InCookable, InOutputObjectIdentifier, InOutputObject, bHasPreviousBakeData, + DefaultObjectName, PackageParams, Resolver, InBakeFolder.Path, AssetPackageReplaceMode); + + // By default spawn in the current level unless specified via the unreal_level_path attribute + ULevel* DesiredLevel = DesiredWorld ? DesiredWorld->GetCurrentLevel() : GWorld->GetCurrentLevel(); + + bool bHasLevelPathAttribute = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_LEVEL_PATH); + if (bHasLevelPathAttribute) + { + // Get the package path from the unreal_level_apth attribute + FString LevelPackagePath = Resolver.ResolveFullLevelPath(); + + bool bCreatedPackage = false; + if (!FindOrCreateDesiredLevelFromLevelPath( + LevelPackagePath, + DesiredLevel, + DesiredWorld, + bCreatedPackage)) + { + // TODO: LOG ERROR IF NO LEVEL + return false; + } + + // If we have created a level, add it to the packages to save + // TODO: ? always add? + if (bCreatedPackage && DesiredLevel) + { + BakedObjectData.BakeStats.NotifyPackageCreated(1); + BakedObjectData.BakeStats.NotifyObjectsCreated(DesiredLevel->GetClass()->GetName(), 1); + // We can now save the package again, and unload it. + BakedObjectData.PackagesToSave.Add(DesiredLevel->GetOutermost()); + } + } + + if (!DesiredLevel) + { + return false; + } + + FName WorldOutlinerFolderPath = GetOutlinerFolderPath(Resolver, *PackageParams.HoudiniAssetActorName); + + // Try to find the unreal_bake_actor, if specified. If we found the actor, we will attach the instanced actors + // to it. If we did not find an actor, but unreal_bake_actor was set, then we create a new actor with that name + // and parent the instanced actors to it. Otherwise, we don't attach the instanced actors to anything. + FName ParentActorName; + FName ParentBakeActorName; + AActor* ParentActor = nullptr; + bool bHasBakeActorName = false; + constexpr AActor* FallbackActor = nullptr; + FName DefaultBakeActorName = NAME_None; + if (BakeSettings.ActorBakeOption == EHoudiniEngineActorBakeOption::OneActorPerHDA) + { + FHoudiniAttributeResolver OutResolver; + TMap Tokens = InOutputObject.CachedTokens; + PackageParams.UpdateTokensFromParams(InCookable->GetWorld(), InCookable->GetComponent(), Tokens); + OutResolver.SetTokensFromStringMap(Tokens); + DefaultBakeActorName = FName(OutResolver.ResolveString(BakeSettings.DefaultBakeName)); + } + + FindUnrealBakeActor(InOutputObject, BakedOutputObject, InBakedActors, DesiredLevel, DefaultBakeActorName, + BakeSettings, FallbackActor, ParentActor, bHasBakeActorName, ParentActorName); + + OutActors.Reset(); + + if (!ParentActor && bHasBakeActorName) + { + // Get the actor factory for the unreal_bake_actor_class attribute. If not set, use an empty actor. + TSubclassOf BakeActorClass = nullptr; + UActorFactory* ActorFactory = GetActorFactory( + InOutputObject, BakeSettings, BakeActorClass, UActorFactoryEmptyActor::StaticClass()); + + if (!ActorFactory) + { + return false; + } + + constexpr UObject* AssetToSpawn = nullptr; + constexpr EObjectFlags ObjectFlags = RF_Transactional; + ParentBakeActorName = *MakeUniqueObjectNameIfNeeded(DesiredLevel, AActor::StaticClass(), ParentActorName.ToString()); + + FActorSpawnParameters SpawnParam; + SpawnParam.ObjectFlags = ObjectFlags; + SpawnParam.Name = ParentBakeActorName; + ParentActor = SpawnBakeActor( + ActorFactory, AssetToSpawn, DesiredLevel, BakeSettings, + InIAC->GetComponentTransform(), InCookable->GetComponent(), BakeActorClass, SpawnParam); + + if (!IsValid(ParentActor)) + { + ParentActor = nullptr; + } + else + { + BakedObjectData.BakeStats.NotifyObjectsCreated(ParentActor->GetClass()->GetName(), 1); + + ParentActor->SetActorLabel(ParentBakeActorName.ToString()); + OutActors.Emplace(FHoudiniEngineBakedActor( + ParentActor, + ParentActorName, + WorldOutlinerFolderPath, + InOutputIndex, + InOutputObjectIdentifier, + nullptr, // InBakedObject + nullptr, // InSourceObject + nullptr, // InBakedComponent + PackageParams.BakeFolder, + PackageParams)); + } + } + + if (ParentActor) + { + BakedOutputObject.ActorBakeName = ParentActorName; + BakedOutputObject.Actor = FSoftObjectPath(ParentActor).ToString(); + } + + // If we are baking in actor replacement mode, remove any previously baked instanced actors for this output + if (BakeSettings.bReplaceActors && BakedOutputObject.InstancedActors.Num() > 0) + { + UWorld* LevelWorld = DesiredLevel->GetWorld(); + if (IsValid(LevelWorld)) + { + for (const FString& ActorPathStr : BakedOutputObject.InstancedActors) + { + const FSoftObjectPath ActorPath(ActorPathStr); + + if (!ActorPath.IsValid()) + { + continue; + } + + AActor* Actor = Cast(ActorPath.TryLoad()); + // Destroy Actor if it is valid and part of DesiredLevel + if (IsValid(Actor) && Actor->GetLevel() == DesiredLevel) + { + // Just before we destroy the actor, rename it with a _DELETE suffix, so that we can re-use + // its original name before garbage collection + FHoudiniEngineUtils::SafeRenameActor(Actor, Actor->GetName() + TEXT("_DELETE")); +#if WITH_EDITOR + LevelWorld->EditorDestroyActor(Actor, true); +#else + LevelWorld->DestroyActor(Actor); +#endif + } + } + } + } + + // Empty and reserve enough space for new instanced actors + BakedOutputObject.InstancedActors.Empty(InIAC->GetInstancedActors().Num()); + + // Iterates on all the instances of the IAC + for (AActor* CurrentInstancedActor : InIAC->GetInstancedActors()) + { + if (!IsValid(CurrentInstancedActor)) + { + continue; + } + + // Make sure we have a globally unique name and use it to name the new actor at spawn time. + const FString NewNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, CurrentInstancedActor->GetClass(), + PackageParams.ObjectName); + + FTransform CurrentTransform = CurrentInstancedActor->GetTransform(); + + AActor* NewActor = FHoudiniInstanceTranslator::SpawnInstanceActor(CurrentTransform, DesiredLevel, InIAC, CurrentInstancedActor); + if (!IsValid(NewActor)) + { + continue; + } + + // The instance may be attached to the parent, so detach it. + if (IsValid(NewActor->GetRootComponent())) + NewActor->GetRootComponent()->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform); + + // Explicitly set the actor label as there appears to be a bug in AActor::GetActorLabel() which sets the first + // duplicate actor name to "name-1" (minus one) instead of leaving off the 0. + NewActor->SetActorLabel(NewNameStr); + + // Copy properties from the Instanced object, but only for actors. + constexpr auto CopyOptions = static_cast( + EditorUtilities::ECopyOptions::OnlyCopyEditOrInterpProperties | + EditorUtilities::ECopyOptions::PropagateChangesToArchetypeInstances | + EditorUtilities::ECopyOptions::CallPostEditChangeProperty | + EditorUtilities::ECopyOptions::CallPostEditMove); + + // BUG: CopyActorProperties are not copying properties for components (at least on Blueprint type actors). + EditorUtilities::CopyActorProperties(CurrentInstancedActor, NewActor, CopyOptions); + + // TODO: Copy over component properties! + + // Since we can't properly copy over component properties, the least we can do is apply actor and component tags + FHoudiniEngineUtils::ApplyTagsToActorAndComponents( + NewActor, FHoudiniEngineUtils::IsKeepTagsEnabled(&InstancerHGPO), InstancerHGPO.GenericPropertyAttributes); + + BakedObjectData.BakeStats.NotifyObjectsCreated(NewActor->GetClass()->GetName(), 1); + + SetOutlinerFolderPath(NewActor, WorldOutlinerFolderPath); + NewActor->SetActorTransform(CurrentTransform); + + if (ParentActor) + { + NewActor->AttachToActor(ParentActor, FAttachmentTransformRules::KeepWorldTransform); + } + + BakedOutputObject.InstancedActors.Add(FSoftObjectPath(NewActor).ToString()); + + FHoudiniEngineBakedActor& OutputEntry = OutActors.Add_GetRef(FHoudiniEngineBakedActor( + NewActor, + *PackageParams.ObjectName, + WorldOutlinerFolderPath, + InOutputIndex, + InOutputObjectIdentifier, + nullptr, + InstancedObject, + nullptr, + PackageParams.BakeFolder, + PackageParams)); + OutputEntry.bInstancerOutput = true; + OutputEntry.InstancerPackageParams = PackageParams; + } + + // TODO: + // Move Actors to DesiredLevel if needed?? + + // If we are baking in replace mode, remove previously baked components/instancers + if (BakeSettings.bReplaceActors && BakeSettings.bReplaceAssets) + { + constexpr bool bInDestroyBakedComponent = true; + constexpr bool bInDestroyBakedInstancedActors = false; + constexpr bool bInDestroyBakedInstancedComponents = true; + DestroyPreviousBakeOutput( + BakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, + bInDestroyBakedInstancedComponents); + } + } + + // Set the updated bake output object in the state + InBakeState.SetNewBakedOutputObject(InOutputIndex, InOutputObjectIdentifier, BakedOutputObject); + + return true; +} + +bool +FHoudiniEngineBakeUtils::FindHGPO( + const FHoudiniOutputObjectIdentifier& InIdentifier, + const TArray& InHGPOs, + FHoudiniGeoPartObject const*& OutHGPO) +{ + // Find the HGPO that matches this output identifier + const FHoudiniGeoPartObject* FoundHGPO = nullptr; + for (auto & NextHGPO : InHGPOs) + { + // We use Matches() here as it handles the case where the HDA was loaded, + // which likely means that the the obj/geo/part ids dont match the output identifier + if(InIdentifier.Matches(NextHGPO)) + { + FoundHGPO = &NextHGPO; + break; + } + } + + OutHGPO = FoundHGPO; + return !OutHGPO; +} + +void +FHoudiniEngineBakeUtils::GetTemporaryOutputObjectBakeName( + const UObject* InObject, + const FHoudiniOutputObject& InMeshOutputObject, + FString& OutBakeName) +{ + // The bake name override has priority + OutBakeName = InMeshOutputObject.BakeName; + if (OutBakeName.IsEmpty()) + { + FHoudiniAttributeResolver Resolver; + Resolver.SetCachedAttributes(InMeshOutputObject.CachedAttributes); + Resolver.SetTokensFromStringMap(InMeshOutputObject.CachedTokens); + const FString DefaultObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(InObject); + // The default output name (if not set via attributes) is {object_name}, which look for an object_name + // key-value token + if (!Resolver.GetCachedTokens().Contains(TEXT("object_name"))) + Resolver.SetToken(TEXT("object_name"), DefaultObjectName); + OutBakeName = Resolver.ResolveOutputName(); + // const TArray& HGPOs = InAllOutputs[MeshOutputIdx]->GetHoudiniGeoPartObjects(); + // const FHoudiniGeoPartObject* FoundHGPO = nullptr; + // FindHGPO(MeshIdentifier, HGPOs, FoundHGPO); + // // ... finally the part name + // if (FoundHGPO && FoundHGPO->bHasCustomPartName) + // OutBakeName = FoundHGPO->PartName; + if (OutBakeName.IsEmpty()) + OutBakeName = DefaultObjectName; + } +} + +bool +FHoudiniEngineBakeUtils::GetTemporaryOutputObjectBakeName( + const UObject* InObject, + EHoudiniOutputType InOutputType, + const TArray& InAllOutputs, + FString& OutBakeName) +{ + if (!IsValid(InObject)) + return false; + + OutBakeName.Empty(); + + int32 MeshOutputIdx = INDEX_NONE; + FHoudiniOutputObjectIdentifier MeshIdentifier; + if (FindOutputObject(InObject, InOutputType, InAllOutputs, MeshOutputIdx, MeshIdentifier)) + { + // Found the mesh, get its name + const FHoudiniOutputObject& MeshOutputObject = InAllOutputs[MeshOutputIdx]->GetOutputObjects().FindChecked(MeshIdentifier); + GetTemporaryOutputObjectBakeName(InObject, MeshOutputObject, OutBakeName); + + return true; + } + + return false; +} + +bool +FHoudiniEngineBakeUtils::BakeStaticMeshOutputObjectToActor( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + const FHoudiniOutputObjectIdentifier& InIdentifier, + const FHoudiniOutputObject& InOutputObject, + const TArray& InHGPOs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InTempCookFolder, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder, + const TArray& InAllBakedActors, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + FHoudiniBakedObjectData& BakedObjectData, + bool& bOutBakedToActor, + FHoudiniEngineBakedActor& OutBakedActorEntry +) +{ + if (!InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + UHoudiniOutput* const InOutput = InAllOutputs[InOutputIndex]; + + // Initialize the baked output object entry (use the previous bake's data, if available). + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(InOutputIndex, InIdentifier, bHasPreviousBakeData); + + UStaticMesh* StaticMesh = Cast(InOutputObject.OutputObject); + if (!IsValid(StaticMesh)) + return false; + + // Allow baking of static mesh output objects without components here: it could be complex collision meshes and + // meshes used in instancers / foliage + // HOUDINI_CHECK_RETURN(InOutputObject.OutputComponents.Num() == 1 || (InOutputObject.OutputComponents.IsEmpty() && InOutputObject.bIsImplicit), false); + + UStaticMeshComponent* InSMC = nullptr; + if (InOutputObject.OutputComponents.Num() >= 1) + InSMC = Cast(InOutputObject.OutputComponents[0]); + const bool bHasOutputSMC = IsValid(InSMC); + // if (!bHasOutputSMC && !InOutputObject.bIsImplicit) + // return false; + + // Find the HGPO that matches this output identifier + const FHoudiniGeoPartObject* FoundHGPO = nullptr; + FindHGPO(InIdentifier, InHGPOs, FoundHGPO); + + // We do not bake templated geos + if (FoundHGPO && FoundHGPO->bIsTemplated) + return true; + + const FString DefaultObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(StaticMesh); + + UWorld* DesiredWorld = InOutput ? InOutput->GetWorld() : GWorld; + ULevel* DesiredLevel = DesiredWorld ? DesiredWorld->GetCurrentLevel() : GWorld->GetCurrentLevel(); + + FHoudiniPackageParams PackageParams; + FHoudiniAttributeResolver Resolver; + if (!ResolvePackageParamsWithResolver( + InCookable, + InOutput, + InIdentifier, + InOutputObject, + bHasPreviousBakeData, + DefaultObjectName, + InBakeFolder, + BakeSettings, + PackageParams, + Resolver, + BakedObjectData)) + { + return false; + } + + // Bake the static mesh if it is still temporary + UStaticMesh* BakedSM = FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( + StaticMesh, + Cast(BakedOutputObject.GetBakedObjectIfValid()), + PackageParams, + InAllOutputs, + InAllBakedActors, + InTempCookFolder.Path, + BakedObjectData, + InOutAlreadyBakedStaticMeshMap, + InOutAlreadyBakedMaterialsMap); + + if (!IsValid(BakedSM)) + return false; + + // Record the baked object + BakedOutputObject.BakedObject = FSoftObjectPath(BakedSM).ToString(); + + if (bHasOutputSMC) + { + const FName WorldOutlinerFolderPath = GetOutlinerFolderPath( + Resolver, + FName(InFallbackWorldOutlinerFolder.IsEmpty() ? PackageParams.HoudiniAssetActorName : InFallbackWorldOutlinerFolder)); + + // Get the actor factory for the unreal_bake_actor_class attribute. If not set, use an empty actor. + TSubclassOf BakeActorClass = nullptr; + UActorFactory* Factory = GetActorFactory(InOutputObject, BakeSettings, BakeActorClass, UActorFactoryEmptyActor::StaticClass(), BakedSM); + + // If we could not find a factory, we have to skip this output object + if (!Factory) + return false; + + // Make sure we have a level to spawn to + if (!IsValid(DesiredLevel)) + return false; + + // Try to find the unreal_bake_actor, if specified + FName BakeActorName; + AActor* FoundActor = nullptr; + bool bHasBakeActorName = false; + FName DefaultActorName = FName(*(PackageParams.ObjectName)); + if (BakeSettings.ActorBakeOption == EHoudiniEngineActorBakeOption::OneActorPerHDA) + { + FHoudiniAttributeResolver OutResolver; + TMap Tokens = InOutputObject.CachedTokens; + PackageParams.UpdateTokensFromParams(InCookable->GetWorld(), InCookable->GetComponent(), Tokens); + OutResolver.SetTokensFromStringMap(Tokens); + DefaultActorName = FName(OutResolver.ResolveString(BakeSettings.DefaultBakeName)); + } + + FindUnrealBakeActor( + InOutputObject, + BakedOutputObject, + InAllBakedActors, + DesiredLevel, + DefaultActorName, + BakeSettings, + InFallbackActor, + FoundActor, + bHasBakeActorName, + BakeActorName); + + bool bCreatedNewActor = false; + UStaticMeshComponent* SMC = nullptr; + if (!FoundActor) + { + // Spawn the new actor + if (!InSMC || !IsValid(InSMC)) + return false; + + FoundActor = SpawnBakeActor( + Factory, BakedSM, DesiredLevel, BakeSettings, InSMC->GetComponentTransform(), + InCookable->GetComponent(), BakeActorClass); + if (!IsValid(FoundActor)) + return false; + + bCreatedNewActor = true; + + // Copy properties to new actor + AStaticMeshActor* SMActor = Cast(FoundActor); + if (IsValid(SMActor)) + SMC = SMActor->GetStaticMeshComponent(); + } + + if (!IsValid(SMC)) + { + if (BakeSettings.bReplaceAssets && !bCreatedNewActor) + { + // Check if we have a previous bake component and that it belongs to FoundActor, if so, reuse it + UStaticMeshComponent* PrevSMC = Cast(BakedOutputObject.GetBakedComponentIfValid()); + if (IsValid(PrevSMC) && (PrevSMC->GetOwner() == FoundActor)) + { + SMC = PrevSMC; + } + } + + const bool bCreateIfMissing = false; + USceneComponent* RootComponent = GetActorRootComponent(FoundActor, bCreateIfMissing); + + if (!IsValid(SMC)) + { + // Create a new static mesh component on the existing actor + SMC = NewObject(FoundActor, NAME_None, RF_Transactional); + + FoundActor->AddInstanceComponent(SMC); + if (IsValid(RootComponent)) + SMC->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); + else + FoundActor->SetRootComponent(SMC); + SMC->RegisterComponent(); + } + } + + // We need to make a unique name for the actor, renaming an object on top of another is a fatal error + const FString NewNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, Factory->NewActorClass, BakeActorName.ToString(), FoundActor); + RenameAndRelabelActor(FoundActor, NewNameStr, false); + SetOutlinerFolderPath(FoundActor, WorldOutlinerFolderPath); + + if (IsValid(SMC)) + { + constexpr bool bCopyWorldTransform = true; + CopyPropertyToNewActorAndComponent(FoundActor, SMC, InSMC, bCopyWorldTransform); + SMC->SetStaticMesh(BakedSM); + BakedOutputObject.BakedComponent = FSoftObjectPath(SMC).ToString(); + } + + FHoudiniEngineUtils::KeepOrClearActorTags(FoundActor, true, false, FoundHGPO); + if (FoundHGPO) + { + // Add actor tags from generic property attributes + FHoudiniEngineUtils::ApplyTagsToActorOnly(FoundHGPO->GenericPropertyAttributes, FoundActor->Tags); + } + + BakedOutputObject.Actor = FSoftObjectPath(FoundActor).ToString(); + OutBakedActorEntry = FHoudiniEngineBakedActor( + FoundActor, BakeActorName, WorldOutlinerFolderPath, InOutputIndex, InIdentifier, BakedSM, StaticMesh, SMC, + PackageParams.BakeFolder, PackageParams); + bOutBakedToActor = true; + } + else + { + // Implicit object, no component and no actor + BakedOutputObject.BakedComponent = nullptr; + BakedOutputObject.Actor = nullptr; + bOutBakedToActor = false; + } + + // If we are baking in replace mode, remove previously baked components/instancers + if (BakeSettings.bReplaceActors && BakeSettings.bReplaceAssets) + { + constexpr bool bInDestroyBakedComponent = false; + constexpr bool bInDestroyBakedInstancedActors = true; + constexpr bool bInDestroyBakedInstancedComponents = true; + DestroyPreviousBakeOutput( + BakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); + } + + // Record bake data + InBakeState.SetNewBakedOutputObject(InOutputIndex, InIdentifier, BakedOutputObject); + + return true; +} + +bool +FHoudiniEngineBakeUtils::BakeStaticMeshOutputToAsset( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + const FDirectoryPath& InTempFolder, + const FDirectoryPath& InBakeFolder, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData) +{ + // Check that index is not negative + if (InOutputIndex < 0) + return false; + + if (!InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + + UHoudiniOutput* InOutput = InAllOutputs[InOutputIndex]; + if (!IsValid(InOutput)) + return false; + + TMap& OutputObjects = InOutput->GetOutputObjects(); + const TArray& HGPOs = InOutput->GetHoudiniGeoPartObjects(); + + // Order the objects: Invisible complex colliders first + // We need to make sure we bake invisible complex colliders first, since they are static meshes themselves + // but are referenced by the main static mesh + TArray OrderedObjects; + for (auto& Pair : OutputObjects) + { + const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + const EHoudiniSplitType SplitType = FHoudiniMeshTranslator::GetSplitTypeFromSplitName(Identifier.SplitIdentifier); + if (SplitType == EHoudiniSplitType::InvisibleComplexCollider) + OrderedObjects.Insert(Identifier, 0); + else + OrderedObjects.Add(Identifier); + } + + // Needed for DuplicateStaticMeshAndCreatePackageIfNeeded + TArray BakedActors; + TMap AlreadyBakedMaterialsMap; + TMap AlreadyBakedStaticMeshMap; + + for (auto& Identifier : OrderedObjects) + { + const FHoudiniOutputObject& OutputObject = OutputObjects[Identifier]; + UStaticMesh* StaticMesh = Cast(OutputObject.OutputObject); + if (!IsValid(StaticMesh)) + return false; + + // Initialize the baked output object entry (use the previous bake's data, if available). + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(InOutputIndex, Identifier, bHasPreviousBakeData); + + // Find the HGPO that matches this output identifier + const FHoudiniGeoPartObject* FoundHGPO = nullptr; + FindHGPO(Identifier, HGPOs, FoundHGPO); + + // We do not bake templated geos + if (FoundHGPO && FoundHGPO->bIsTemplated) + return true; + + const FString DefaultObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(StaticMesh); + FHoudiniPackageParams PackageParams; + FHoudiniAttributeResolver Resolver; + if (!ResolvePackageParamsWithResolver( + InCookable, + InOutput, + Identifier, + OutputObject, + bHasPreviousBakeData, + DefaultObjectName, + InBakeFolder, + BakeSettings, + PackageParams, + Resolver, + BakedObjectData)) + { + return false; + } + + // Bake the static mesh if it is still temporary + UStaticMesh* BakedSM = FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( + StaticMesh, + Cast(BakedOutputObject.GetBakedObjectIfValid()), + PackageParams, + InAllOutputs, + BakedActors, + InTempFolder.Path, + BakedObjectData, + AlreadyBakedStaticMeshMap, + AlreadyBakedMaterialsMap); + + if (!IsValid(BakedSM)) + return false; + + // Record the baked object + BakedOutputObject.BakedObject = FSoftObjectPath(BakedSM).ToString(); + + // Implicit object, no component and no actor + BakedOutputObject.BakedComponent = nullptr; + BakedOutputObject.Actor = nullptr; + + // Record bake data + InBakeState.SetNewBakedOutputObject(InOutputIndex, Identifier, BakedOutputObject); + } + + return true; +} + +bool +FHoudiniEngineBakeUtils::BakeSkeletalMeshOutputObjectToActor( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + const FHoudiniOutputObjectIdentifier& InIdentifier, + const FHoudiniOutputObject& InOutputObject, + const TArray& InHGPOs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InTempCookFolder, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder, + const TArray& InAllBakedActors, + TMap& InOutAlreadyBakedSkeletalMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + FHoudiniBakedObjectData& BakedObjectData, + bool& bOutBakedToActor, + FHoudiniEngineBakedActor& OutBakedActorEntry +) +{ + if (!InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + UHoudiniOutput* const InOutput = InAllOutputs[InOutputIndex]; + + // Initialize the baked output object entry (use the previous bake's data, if available). + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(InOutputIndex, InIdentifier, bHasPreviousBakeData); + + USkeletalMesh* SkeletalMesh = Cast(InOutputObject.OutputObject); + if (!IsValid(SkeletalMesh)) + return false; + + HOUDINI_CHECK_RETURN(InOutputObject.OutputComponents.Num() == 1 || (InOutputObject.OutputComponents.IsEmpty() && InOutputObject.bIsImplicit), false); + + USkeletalMeshComponent* InSKC = nullptr; + if (InOutputObject.OutputComponents.Num() >= 1) + InSKC = Cast(InOutputObject.OutputComponents[0]); + const bool bHasOutputSKC = IsValid(InSKC); + if (!bHasOutputSKC && !InOutputObject.bIsImplicit) + return false; + + // Find the HGPO that matches this output identifier + const FHoudiniGeoPartObject* FoundHGPO = nullptr; + FindHGPO(InIdentifier, InHGPOs, FoundHGPO); + + // We do not bake templated geos + if (FoundHGPO && FoundHGPO->bIsTemplated) + return true; + + const FString DefaultObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(SkeletalMesh); + + UWorld* DesiredWorld = InOutput ? InOutput->GetWorld() : GWorld; + ULevel* DesiredLevel = DesiredWorld ? DesiredWorld->GetCurrentLevel() : GWorld->GetCurrentLevel(); + + // Bake Skeleton + FHoudiniPackageParams SkeletonPackageParams; + FHoudiniOutputObjectIdentifier SkeletonIdentifier = InIdentifier; + SkeletonIdentifier.SplitIdentifier = TEXT("skeleton"); + if (!ResolvePackageParams( + InCookable, + InOutput, + SkeletonIdentifier, + InOutputObject, + bHasPreviousBakeData, + DefaultObjectName + TEXT("_skeleton"), + InBakeFolder, + BakeSettings, + SkeletonPackageParams, + BakedObjectData)) + { + return false; + } + // TODO: add an attribute for controlling the skeleton's bake name? + if (!SkeletonPackageParams.ObjectName.Contains(TEXT("skeleton"), ESearchCase::IgnoreCase)) + SkeletonPackageParams.ObjectName += TEXT("_skeleton"); + + USkeleton* Skeleton = SkeletalMesh->GetSkeleton(); + USkeleton* BakedSkeleton = DuplicateSkeletonAndCreatePackageIfNeeded( + Skeleton, + BakedOutputObject.GetBakedSkeletonIfValid(), + SkeletonPackageParams, + InAllOutputs, + InAllBakedActors, + InTempCookFolder.Path, + BakedObjectData, + InBakeState.GetBakedSkeletons()); + + if (Skeleton != BakedSkeleton) + BakedOutputObject.BakedSkeleton = FSoftObjectPath(BakedSkeleton).ToString(); + else + BakedOutputObject.BakedSkeleton = FSoftObjectPath(nullptr).ToString(); + + // Bake Physics Asset + + FHoudiniPackageParams PhysicsAssetsPackageParams; + FHoudiniOutputObjectIdentifier PhysicsAsssetIdentifier = InIdentifier; + PhysicsAsssetIdentifier.SplitIdentifier = TEXT("physics_asset"); + if (!ResolvePackageParams( + InCookable, + InOutput, + PhysicsAsssetIdentifier, + InOutputObject, + bHasPreviousBakeData, + DefaultObjectName + TEXT("_physics_asset"), + InBakeFolder, + BakeSettings, + PhysicsAssetsPackageParams, + BakedObjectData)) + { + return false; + } + // TODO: add an attribute for controlling the bake name? + if (!PhysicsAssetsPackageParams.ObjectName.Contains(TEXT("physics_asset"), ESearchCase::IgnoreCase)) + PhysicsAssetsPackageParams.ObjectName += TEXT("physics_asset"); + + // Bake the skeleton if it is temporary + UPhysicsAsset* PhysicsAsset = SkeletalMesh->GetPhysicsAsset(); + UPhysicsAsset* BakedPhysicsAsset = DuplicatePhysicsAssetAndCreatePackageIfNeeded( + PhysicsAsset, + BakedOutputObject.GetBakedPhysicsAssetIfValid(), + PhysicsAssetsPackageParams, + InAllOutputs, + InAllBakedActors, + InTempCookFolder.Path, + BakedObjectData, + InBakeState.GetBakedPhysicsAssets()); + + if (PhysicsAsset != BakedPhysicsAsset) + BakedOutputObject.BakedPhysicsAsset = FSoftObjectPath(BakedPhysicsAsset).ToString(); + else + BakedOutputObject.BakedPhysicsAsset = FSoftObjectPath(nullptr).ToString(); + + FHoudiniAttributeResolver Resolver; + + FHoudiniPackageParams PackageParams; + if (!ResolvePackageParamsWithResolver( + InCookable, + InOutput, + InIdentifier, + InOutputObject, + bHasPreviousBakeData, + DefaultObjectName, + InBakeFolder, + BakeSettings, + PackageParams, + Resolver, + BakedObjectData)) + { + return false; + } + + // Bake the skeletal mesh if it is still temporary + USkeletalMesh* BakedSK = FHoudiniEngineBakeUtils::DuplicateSkeletalMeshAndCreatePackageIfNeeded( + SkeletalMesh, + Cast(BakedOutputObject.GetBakedObjectIfValid()), + PackageParams, + InAllOutputs, + InAllBakedActors, + InTempCookFolder.Path, + BakedObjectData, + InOutAlreadyBakedSkeletalMeshMap, + InOutAlreadyBakedMaterialsMap); + + if (!IsValid(BakedSK)) + return false; + + // Update the skeleton of the BakedSK if the skeleton was baked + if (BakedSK->GetSkeleton() != BakedSkeleton) + BakedSK->SetSkeleton(BakedSkeleton); + + if (BakedSK->GetPhysicsAsset() != BakedPhysicsAsset) + { + BakedSK->SetPhysicsAsset(BakedPhysicsAsset); + BakedPhysicsAsset->SetPreviewMesh(BakedSK); + } + + // Record the baked object + BakedOutputObject.BakedObject = FSoftObjectPath(BakedSK).ToString(); + + if (bHasOutputSKC) + { + const FName WorldOutlinerFolderPath = GetOutlinerFolderPath( + Resolver, + FName(InFallbackWorldOutlinerFolder.IsEmpty() ? PackageParams.HoudiniAssetActorName : InFallbackWorldOutlinerFolder)); + + // Get the actor factory for the unreal_bake_actor_class attribute. If not set, use an empty actor. + TSubclassOf BakeActorClass = nullptr; + UActorFactory* const Factory = GetActorFactory(InOutputObject, BakeSettings, BakeActorClass, UActorFactoryEmptyActor::StaticClass(), BakedSK); + + // If we could not find a factory, we have to skip this output object + if (!Factory) + return false; + + // Make sure we have a level to spawn to + if (!IsValid(DesiredLevel)) + return false; + + // Try to find the unreal_bake_actor, if specified + FName BakeActorName; + AActor* FoundActor = nullptr; + bool bHasBakeActorName = false; + FindUnrealBakeActor( + InOutputObject, + BakedOutputObject, + InAllBakedActors, + DesiredLevel, + *(PackageParams.ObjectName), + BakeSettings, + InFallbackActor, + FoundActor, + bHasBakeActorName, + BakeActorName); + + bool bCreatedNewActor = false; + USkeletalMeshComponent* SKC = nullptr; + if (!FoundActor) + { + // Spawn the new actor + if (InSKC) + { + FoundActor = SpawnBakeActor( + Factory, BakedSK, DesiredLevel, BakeSettings, + InSKC->GetComponentTransform(), InCookable->GetComponent(), BakeActorClass); + } + + if (!FoundActor || !IsValid(FoundActor)) + return false; + + bCreatedNewActor = true; + + // Copy properties to new actor + ASkeletalMeshActor* SMActor = Cast(FoundActor); + if (IsValid(SMActor)) + SKC = SMActor->GetSkeletalMeshComponent(); + } + + if (!IsValid(SKC)) + { + if (BakeSettings.bReplaceAssets && !bCreatedNewActor) + { + // Check if we have a previous bake component and that it belongs to FoundActor, if so, reuse it + USkeletalMeshComponent* PrevSKC = Cast(BakedOutputObject.GetBakedComponentIfValid()); + if (IsValid(PrevSKC) && (PrevSKC->GetOwner() == FoundActor)) + { + SKC = PrevSKC; + } + } + + const bool bCreateIfMissing = true; + USceneComponent* RootComponent = GetActorRootComponent(FoundActor, bCreateIfMissing); + + if (!IsValid(SKC)) + { + // Create a new static mesh component on the existing actor + SKC = NewObject(FoundActor, NAME_None, RF_Transactional); + + FoundActor->AddInstanceComponent(SKC); + if (IsValid(RootComponent)) + SKC->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); + else + FoundActor->SetRootComponent(SKC); + SKC->RegisterComponent(); + } + } + + // We need to make a unique name for the actor, renaming an object on top of another is a fatal error + const FString NewNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, Factory->NewActorClass, BakeActorName.ToString(), FoundActor); + RenameAndRelabelActor(FoundActor, NewNameStr, false); + SetOutlinerFolderPath(FoundActor, WorldOutlinerFolderPath); + + if (IsValid(SKC)) + { + constexpr bool bCopyWorldTransform = true; + CopyPropertyToNewActorAndSkeletalComponent(FoundActor, SKC, InSKC, bCopyWorldTransform); + SKC->SetSkeletalMesh(BakedSK); + BakedOutputObject.BakedComponent = FSoftObjectPath(SKC).ToString(); + } + + BakedOutputObject.Actor = FSoftObjectPath(FoundActor).ToString(); + OutBakedActorEntry = FHoudiniEngineBakedActor( + FoundActor, BakeActorName, WorldOutlinerFolderPath, InOutputIndex, InIdentifier, BakedSK, SkeletalMesh, SKC, + PackageParams.BakeFolder, PackageParams); + bOutBakedToActor = true; + } + else + { + // Implicit object, no component and no actor + BakedOutputObject.BakedComponent = nullptr; + BakedOutputObject.Actor = nullptr; + bOutBakedToActor = false; + } + + // If we are baking in replace mode, remove previously baked components/instancers + if (BakeSettings.bReplaceActors && BakeSettings.bReplaceAssets) + { + constexpr bool bInDestroyBakedComponent = false; + constexpr bool bInDestroyBakedInstancedActors = true; + constexpr bool bInDestroyBakedInstancedComponents = true; + DestroyPreviousBakeOutput( + BakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); + } + + // Record bake data + InBakeState.SetNewBakedOutputObject(InOutputIndex, InIdentifier, MoveTemp(BakedOutputObject)); + + return true; +} + + +bool +FHoudiniEngineBakeUtils::BakeSkeletalMeshOutputToActors( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder) +{ + // Check that index is not negative + if (InOutputIndex < 0) + return false; + + if (!InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + + UHoudiniOutput* InOutput = InAllOutputs[InOutputIndex]; + if (!IsValid(InOutput)) + return false; + + TMap& OutputObjects = InOutput->GetOutputObjects(); + const TArray& HGPOs = InOutput->GetHoudiniGeoPartObjects(); + + TArray AllBakedActors = InBakedActors; + TArray NewBakedActors; + + //DAMIEN - Do we need to do something with this + TMap AlreadyBakedSkeletalMeshMap; + + // We need to bake invisible complex colliders first, since they are static meshes themselves but are referenced + // by the main static mesh + for (auto& Pair : OutputObjects) + { + const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + + const EHoudiniSplitType SplitType = FHoudiniMeshTranslator::GetSplitTypeFromSplitName(Identifier.SplitIdentifier); + if (SplitType != EHoudiniSplitType::InvisibleComplexCollider) + continue; + + const FHoudiniOutputObject& OutputObject = Pair.Value; + + bool bBakedToActor = false; + FHoudiniEngineBakedActor BakedActorEntry; + bool WasBaked = false; + + USkeletalMesh* SkeletalMesh = Cast(OutputObject.OutputObject); + if (IsValid(SkeletalMesh)) + { + WasBaked = BakeSkeletalMeshOutputObjectToActor( + InCookable, + InOutputIndex, + InAllOutputs, + Identifier, + OutputObject, + HGPOs, + InBakeState, + InTempCookFolder, + InBakeFolder, + BakeSettings, + InFallbackActor, + InFallbackWorldOutlinerFolder, + AllBakedActors, + AlreadyBakedSkeletalMeshMap, + InOutAlreadyBakedMaterialsMap, + BakedObjectData, + bBakedToActor, + BakedActorEntry); + } + + if (WasBaked && bBakedToActor) + { + NewBakedActors.Add(BakedActorEntry); + AllBakedActors.Add(BakedActorEntry); + } + } + + // Now bake the other output objects + for (auto& Pair : OutputObjects) + { + const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + + const EHoudiniSplitType SplitType = FHoudiniMeshTranslator::GetSplitTypeFromSplitName(Identifier.SplitIdentifier); + if (SplitType == EHoudiniSplitType::InvisibleComplexCollider) + continue; + + const FHoudiniOutputObject& OutputObject = Pair.Value; + + bool bBakedToActor = false; + FHoudiniEngineBakedActor BakedActorEntry; + bool WasBaked = false; + + USkeletalMesh* SkeletalMesh = Cast(OutputObject.OutputObject); + if (IsValid(SkeletalMesh)) + { + WasBaked = BakeSkeletalMeshOutputObjectToActor( + InCookable, + InOutputIndex, + InAllOutputs, + Identifier, + OutputObject, + HGPOs, + InBakeState, + InTempCookFolder, + InBakeFolder, + BakeSettings, + InFallbackActor, + InFallbackWorldOutlinerFolder, + AllBakedActors, + AlreadyBakedSkeletalMeshMap, + InOutAlreadyBakedMaterialsMap, + BakedObjectData, + bBakedToActor, + BakedActorEntry); + } + + if (WasBaked && bBakedToActor) + { + NewBakedActors.Add(BakedActorEntry); + AllBakedActors.Add(BakedActorEntry); + } + } + + OutActors = MoveTemp(NewBakedActors); + + return true; +} + +bool +FHoudiniEngineBakeUtils::BakeStaticMeshOutputToActors( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder) +{ + // Check that index is not negative + if (InOutputIndex < 0) + return false; + + if (!InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + + UHoudiniOutput* InOutput = InAllOutputs[InOutputIndex]; + if (!IsValid(InOutput)) + return false; + + TMap& OutputObjects = InOutput->GetOutputObjects(); + const TArray& HGPOs = InOutput->GetHoudiniGeoPartObjects(); + + TArray AllBakedActors = InBakedActors; + TArray NewBakedActors; + + //DAMIEN - Do we need to do something with this + TMap AlreadyBakedSkeletalMeshMap; + + // We need to bake invisible complex colliders first, since they are static meshes themselves but are referenced + // by the main static mesh + for (auto& Pair : OutputObjects) + { + const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + + const EHoudiniSplitType SplitType = FHoudiniMeshTranslator::GetSplitTypeFromSplitName(Identifier.SplitIdentifier); + if (SplitType != EHoudiniSplitType::InvisibleComplexCollider) + continue; + + const FHoudiniOutputObject& OutputObject = Pair.Value; + + bool bBakedToActor = false; + FHoudiniEngineBakedActor BakedActorEntry; + bool WasBaked = false; + + UStaticMesh* StaticMesh = Cast(OutputObject.OutputObject); + if (IsValid(StaticMesh)) + { + WasBaked = BakeStaticMeshOutputObjectToActor( + InCookable, + InOutputIndex, + InAllOutputs, + Identifier, + OutputObject, + HGPOs, + InBakeState, + InTempCookFolder, + InBakeFolder, + BakeSettings, + InFallbackActor, + InFallbackWorldOutlinerFolder, + AllBakedActors, + InOutAlreadyBakedStaticMeshMap, + InOutAlreadyBakedMaterialsMap, + BakedObjectData, + bBakedToActor, + BakedActorEntry); + } + else + { + USkeletalMesh* SkeletalMesh = Cast(OutputObject.OutputObject); + if (IsValid(SkeletalMesh)) + { + WasBaked = BakeSkeletalMeshOutputObjectToActor( + InCookable, + InOutputIndex, + InAllOutputs, + Identifier, + OutputObject, + HGPOs, + InBakeState, + InTempCookFolder, + InBakeFolder, + BakeSettings, + InFallbackActor, + InFallbackWorldOutlinerFolder, + AllBakedActors, + AlreadyBakedSkeletalMeshMap, + InOutAlreadyBakedMaterialsMap, + BakedObjectData, + bBakedToActor, + BakedActorEntry); + } + } + + if (WasBaked && bBakedToActor) + { + NewBakedActors.Add(BakedActorEntry); + AllBakedActors.Add(BakedActorEntry); + } + } + + // Now bake the other output objects + for (auto& Pair : OutputObjects) + { + const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + + const EHoudiniSplitType SplitType = FHoudiniMeshTranslator::GetSplitTypeFromSplitName(Identifier.SplitIdentifier); + if (SplitType == EHoudiniSplitType::InvisibleComplexCollider) + continue; + + const FHoudiniOutputObject& OutputObject = Pair.Value; + + bool bBakedToActor = false; + FHoudiniEngineBakedActor BakedActorEntry; + bool WasBaked = false; + + UStaticMesh* StaticMesh = Cast(OutputObject.OutputObject); + if (IsValid(StaticMesh)) + { + WasBaked = BakeStaticMeshOutputObjectToActor( + InCookable, + InOutputIndex, + InAllOutputs, + Identifier, + OutputObject, + HGPOs, + InBakeState, + InTempCookFolder, + InBakeFolder, + BakeSettings, + InFallbackActor, + InFallbackWorldOutlinerFolder, + AllBakedActors, + InOutAlreadyBakedStaticMeshMap, + InOutAlreadyBakedMaterialsMap, + BakedObjectData, + bBakedToActor, + BakedActorEntry); + } + else + { + USkeletalMesh* SkeletalMesh = Cast(OutputObject.OutputObject); + if (IsValid(SkeletalMesh)) + { + WasBaked = BakeSkeletalMeshOutputObjectToActor( + InCookable, + InOutputIndex, + InAllOutputs, + Identifier, + OutputObject, + HGPOs, + InBakeState, + InTempCookFolder, + InBakeFolder, + BakeSettings, + InFallbackActor, + InFallbackWorldOutlinerFolder, + AllBakedActors, + AlreadyBakedSkeletalMeshMap, + InOutAlreadyBakedMaterialsMap, + BakedObjectData, + bBakedToActor, + BakedActorEntry); + } + } + if (WasBaked && bBakedToActor) + { + NewBakedActors.Add(BakedActorEntry); + AllBakedActors.Add(BakedActorEntry); + } + } + + OutActors = MoveTemp(NewBakedActors); + + return true; +} + +bool FHoudiniEngineBakeUtils::ResolvePackageParams( + const UHoudiniCookable* InCookable, + UHoudiniOutput* InOutput, + const FHoudiniOutputObjectIdentifier& Identifier, + const FHoudiniOutputObject& InOutputObject, + const bool bInHasPreviousBakeData, + const FString& DefaultObjectName, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniPackageParams& OutPackageParams, + FHoudiniBakedObjectData& BakedObjectData, + const FString& InHoudiniAssetName, + const FString& InHoudiniAssetActorName) +{ + FHoudiniAttributeResolver Resolver; + + return ResolvePackageParamsWithResolver( + InCookable, + InOutput, + Identifier, + InOutputObject, + bInHasPreviousBakeData, + DefaultObjectName, + InBakeFolder, + BakeSettings, + OutPackageParams, + Resolver, + BakedObjectData, + InHoudiniAssetName, + InHoudiniAssetActorName); + + +} +bool FHoudiniEngineBakeUtils::ResolvePackageParamsWithResolver( + const UHoudiniCookable* InCookable, + UHoudiniOutput* InOutput, + const FHoudiniOutputObjectIdentifier& Identifier, + const FHoudiniOutputObject& InOutputObject, + const bool bInHasPreviousBakeData, + const FString& DefaultObjectName, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniPackageParams& OutPackageParams, + FHoudiniAttributeResolver& Resolver, + FHoudiniBakedObjectData& BakedObjectData, + const FString& InHoudiniAssetName, + const FString& InHoudiniAssetActorName) +{ + UWorld* DesiredWorld = InOutput ? InOutput->GetWorld() : GWorld; + ULevel* DesiredLevel = DesiredWorld ? DesiredWorld->GetCurrentLevel() : GWorld->GetCurrentLevel(); + + // Set the replace mode based on if we are doing a replacement or incremental asset bake + const EPackageReplaceMode AssetPackageReplaceMode = BakeSettings.bReplaceAssets ? + EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; + + // Configure FHoudiniAttributeResolver and fill the package params with resolved object name and bake folder. + // The resolver is then also configured with the package params for subsequent resolving (level_path etc) + FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( + DesiredWorld, InCookable, Identifier, InOutputObject, bInHasPreviousBakeData, DefaultObjectName, + OutPackageParams, Resolver, InBakeFolder.Path, AssetPackageReplaceMode, + InHoudiniAssetName, InHoudiniAssetActorName); + + // See if this output object has an unreal_level_path attribute specified + // In which case, we need to create/find the desired level for baking instead of using the current one + bool bHasLevelPathAttribute = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_LEVEL_PATH); + if (bHasLevelPathAttribute) + { + // Get the package path from the unreal_level_path attribute + FString LevelPackagePath = Resolver.ResolveFullLevelPath(); + + bool bCreatedPackage = false; + if (!FHoudiniEngineBakeUtils::FindOrCreateDesiredLevelFromLevelPath( + LevelPackagePath, + DesiredLevel, + DesiredWorld, + bCreatedPackage)) + { + // TODO: LOG ERROR IF NO LEVEL + return false; + } + + // If we have created a level, add it to the packages to save + // TODO: ? always add the level to the packages to save? + if (bCreatedPackage && DesiredLevel) + { + // We can now save the package again, and unload it. + BakedObjectData.PackagesToSave.Add(DesiredLevel->GetOutermost()); + } + } + + return true; +} + +UUserDefinedStruct * FHoudiniEngineBakeUtils::CreateBakedUserDefinedStruct( + UHoudiniOutput* CookedOutput, + const FHoudiniOutputObjectIdentifier& Identifier, + const UHoudiniCookable* InCookable, + const FHoudiniBakedOutput& InPreviousBakedOutput, + FHoudiniBakedOutput& InNewBakedOutput, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData) +{ + FHoudiniPackageParams PackageParams; + + FHoudiniOutputObject& OutputObject = CookedOutput->GetOutputObjects().FindOrAdd(Identifier); + FHoudiniBakedOutputObject BakedOutputObject; + const bool bHasPreviousBakeData = InPreviousBakedOutput.BakedOutputObjects.Contains(Identifier); + if (bHasPreviousBakeData) + BakedOutputObject = InPreviousBakedOutput.BakedOutputObjects.FindChecked(Identifier); + + auto * UserStruct = Cast(OutputObject.OutputObject); + + FHoudiniOutputObjectIdentifier BakeIdentifier = Identifier; + BakeIdentifier.SplitIdentifier = TEXT("rowstruct"); + + if (!ResolvePackageParams(InCookable, + CookedOutput, + BakeIdentifier, + OutputObject, + bHasPreviousBakeData, + FString(""), + InBakeFolder, + BakeSettings, + PackageParams, + BakedObjectData)) + { + return nullptr; + } + + FString* OutputName = nullptr; + + if ((OutputName = OutputObject.CachedAttributes.Find(HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWSTRUCT))) + { + // use the name verbatim from the user. + PackageParams.ObjectName = *OutputName; + } + else if (!PackageParams.ObjectName.IsEmpty() && !PackageParams.ObjectName.Contains(TEXT("rowstruct"))) + { + PackageParams.ObjectName += TEXT("_rowstruct"); + } + + FString PackageName = PackageParams.GetPackagePath(); + FString CreatedPackageName; + UPackage* Package = PackageParams.CreatePackageForObject(CreatedPackageName); + + UUserDefinedStruct* BakedObject = DuplicateUserDefinedStruct(UserStruct, Package, CreatedPackageName); + BakedOutputObject.BakedObject = BakedObject->GetPathName(); + BakedObjectData.PackagesToSave.Add(Package); + + InNewBakedOutput.BakedOutputObjects.Emplace(Identifier, BakedOutputObject); + + return BakedObject; +} + +UDataTable* FHoudiniEngineBakeUtils::CreateBakedDataTable( + UScriptStruct* UserDefinedStruct, + const FString & ObjectName, + UHoudiniOutput* CookedOutput, + const FHoudiniOutputObjectIdentifier& Identifier, + const UHoudiniCookable* InCookable, + const FHoudiniBakedOutput& InPreviousBakedOutput, + FHoudiniBakedOutput& InNewBakedOutput, + const FDirectoryPath& BakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData) +{ + FHoudiniOutputObject& OutputObject = CookedOutput->GetOutputObjects().FindOrAdd(Identifier); + FHoudiniBakedOutputObject BakedOutputObject; + const bool bHasPreviousBakeData = InPreviousBakedOutput.BakedOutputObjects.Contains(Identifier); + if (bHasPreviousBakeData) + BakedOutputObject = InPreviousBakedOutput.BakedOutputObjects.FindChecked(Identifier); + + FHoudiniPackageParams PackageParams; + + FHoudiniOutputObjectIdentifier BakeIdentifier = Identifier; + BakeIdentifier.SplitIdentifier = "datatable"; + + if (!ResolvePackageParams( + InCookable, + CookedOutput, + BakeIdentifier, + OutputObject, + bHasPreviousBakeData, + FString(""), + BakeFolder, + BakeSettings, + PackageParams, + BakedObjectData)) + { + return nullptr; + } + + UDataTable* CookedDataTable = Cast(OutputObject.OutputObject); + + UDataTable* BakedDataTable = static_cast(PackageParams.CreateObjectAndPackageFromClass(UDataTable::StaticClass())); + + BakedDataTable->PreEditChange(nullptr); + + // Get Row Data. Due to type mismatches in Unreal, we need to make a copy of it. + TMap ConstMap; + auto& RowMap = CookedDataTable->GetRowMap(); + for (auto It : RowMap) + ConstMap.Add(It.Key, (const uint8*)It.Value); + + // If no User Defined Struct was specified, use the one from the cooked table. + UScriptStruct * StructToUse = UserDefinedStruct; + if (!IsValid(StructToUse)) + StructToUse = (UScriptStruct*)CookedDataTable->GetRowStruct(); + + BakedDataTable->CreateTableFromRawData(ConstMap, StructToUse); + + BakedObjectData.BakeStats.NotifyPackageCreated(1); + BakedObjectData.PackagesToSave.Add(BakedDataTable->GetPackage()); + BakedDataTable->MarkPackageDirty(); + + BakedOutputObject.BakedObject = BakedDataTable->GetPathName(); + InNewBakedOutput.BakedOutputObjects.Emplace(Identifier, BakedOutputObject); + + return BakedDataTable; +} + +bool +FHoudiniEngineBakeUtils::BakePCGData( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder) +{ +#if defined(HOUDINI_USE_PCG) + if((InOutputIndex < 0) || !InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + + // Get previously cooked output. + UHoudiniOutput* CookedOutput = InAllOutputs[InOutputIndex]; + if(!IsValid(CookedOutput)) + return false; + + FHoudiniBakedOutput & BakedOutputs = InBakeState.GetNewBakedOutputs()[InOutputIndex]; + + for (auto It : CookedOutput->GetOutputObjects()) + { + UHoudiniPCGOutputData* CookedPCGData = Cast(It.Value.OutputObject); + + UHoudiniPCGOutputData* BakedPCGData = Cast(StaticDuplicateObject(CookedPCGData, GetTransientPackage())); + + FHoudiniBakedOutputObject BakedOutputObject; + BakedOutputObject.PCGOutputData = BakedPCGData; + BakedOutputs.BakedOutputObjects.Emplace(It.Key, BakedOutputObject); + } + + return true; +#else + return false; +#endif +} + + +bool +FHoudiniEngineBakeUtils::BakeDataTables( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData) +{ + if ((InOutputIndex < 0) || !InAllOutputs.IsValidIndex(InOutputIndex) ) + return false; + + // Get previously cooked output. + UHoudiniOutput* CookedOutput = InAllOutputs[InOutputIndex]; + if (!IsValid(CookedOutput)) + return false; + + //---------------------------------------------------------------------------------------------------------- + // See if we created a UserDefinedStruct during COOKING. If so, we must create a new version in the Bake folder + //---------------------------------------------------------------------------------------------------------- + + UUserDefinedStruct * BakedUserStruct = nullptr; + FHoudiniPackageParams PackageParams; + + const FString DefaultObjectName = TEXT("Default"); + + for(auto & It : CookedOutput->GetOutputObjects()) + { + if (!IsValid(It.Value.OutputObject)) + continue; + + if (It.Value.OutputObject->IsA()) + { + FDirectoryPath BakeFolder = InBakeFolder; + FString* Attribute = It.Value.CachedAttributes.Find(HAPI_UNREAL_ATTRIB_BAKE_FOLDER); + if (Attribute != nullptr) + { + BakeFolder.Path = *Attribute; + } + + BakedUserStruct = CreateBakedUserDefinedStruct( + CookedOutput, + It.Key, + InCookable, + InBakeState.GetOldBakedOutputs()[InOutputIndex], + InBakeState.GetNewBakedOutputs()[InOutputIndex], + BakeFolder, + BakeSettings, + BakedObjectData); + + if (!BakedUserStruct) + return false; + + break; + } + } + + + //---------------------------------------------------------------------------------------------------------- + // Create a baked copy of the data table. We don't just duplicate the Data Table and change the UUserDefinedStruct + // because Unreal does not allow this. So we need to actually bake a new table and copy the data over. + //---------------------------------------------------------------------------------------------------------- + + UDataTable* BakedDataTable = nullptr; + for (auto& It : CookedOutput->GetOutputObjects()) + { + if (!IsValid(It.Value.OutputObject)) + continue; + + FHoudiniOutputObject & OutputObject = It.Value; + + if (OutputObject.OutputObject->IsA()) + { + FDirectoryPath BakeFolder = InBakeFolder; + FString * Attribute = It.Value.CachedAttributes.Find(HAPI_UNREAL_ATTRIB_BAKE_FOLDER); + if (Attribute != nullptr) + { + BakeFolder.Path = *Attribute; + } + + FString ObjectName = ""; + if (FString * Value = OutputObject.CachedAttributes.Find(HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2)) + { + ObjectName = *Value; + } + + BakedDataTable = CreateBakedDataTable( + BakedUserStruct, + ObjectName, + CookedOutput, + It.Key, + InCookable, + InBakeState.GetOldBakedOutputs()[InOutputIndex], + InBakeState.GetNewBakedOutputs()[InOutputIndex], + BakeFolder, + BakeSettings, + BakedObjectData); + + if (!BakedDataTable) + return false; + + break; + } + } + + return true; + + +} + +bool +FHoudiniEngineBakeUtils::BakeAnimSequence( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData) +{ + if ((InOutputIndex < 0) || !InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + + // Get previously cooked output. + UHoudiniOutput* CookedOutput = InAllOutputs[InOutputIndex]; + if (!IsValid(CookedOutput)) + return false; + + + FHoudiniPackageParams PackageParams; + const FString DefaultObjectName = TEXT("Default"); + for (auto& It : CookedOutput->GetOutputObjects()) + { + if (!IsValid(It.Value.OutputObject)) + continue; + + FHoudiniOutputObject& OutputObject = It.Value; + if (OutputObject.OutputObject->IsA()) + { + FDirectoryPath BakeFolder = InBakeFolder; + FString* Attribute = It.Value.CachedAttributes.Find(HAPI_UNREAL_ATTRIB_BAKE_FOLDER); + if (Attribute != nullptr) + { + BakeFolder.Path = *Attribute; + } + + FString ObjectName = ""; + if (FString* Value = OutputObject.CachedAttributes.Find(HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2)) + { + ObjectName = *Value; + } + + UAnimSequence * BakedAnimSequence = CreateBakedAnimSequence( + ObjectName, + CookedOutput, + It.Key, + InCookable, + InBakeState.GetOldBakedOutputs()[InOutputIndex], + InBakeState.GetNewBakedOutputs()[InOutputIndex], + BakeFolder, + BakeSettings, + BakedObjectData); + + if (!BakedAnimSequence) + return false; + + break; + } + } + + return true; +} + +UAnimSequence * FHoudiniEngineBakeUtils::CreateBakedAnimSequence( + const FString& ObjectName, + UHoudiniOutput* CookedOutput, + const FHoudiniOutputObjectIdentifier& Identifier, + const UHoudiniCookable* InCookable, + const FHoudiniBakedOutput& InPreviousBakedOutput, + FHoudiniBakedOutput& InNewBakedOutput, + const FDirectoryPath& BakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData) +{ + FHoudiniOutputObject& OutputObject = CookedOutput->GetOutputObjects().FindOrAdd(Identifier); + FHoudiniBakedOutputObject BakedOutputObject; + const bool bHasPreviousBakeData = InPreviousBakedOutput.BakedOutputObjects.Contains(Identifier); + if (bHasPreviousBakeData) + BakedOutputObject = InPreviousBakedOutput.BakedOutputObjects.FindChecked(Identifier); + + FHoudiniPackageParams PackageParams; + + FHoudiniOutputObjectIdentifier BakeIdentifier = Identifier; + BakeIdentifier.SplitIdentifier = "anim"; + + if (!ResolvePackageParams(InCookable, + CookedOutput, + BakeIdentifier, + OutputObject, + bHasPreviousBakeData, + FString(""), + BakeFolder, + BakeSettings, + PackageParams, + BakedObjectData)) + { + return nullptr; + } + + UAnimSequence * CookedAnimSequence = Cast(OutputObject.OutputObject); + + // Create the package for the object + FString NewObjectName; + UPackage* Package = PackageParams.CreatePackageForObject(NewObjectName); + if (!IsValid(Package)) + return nullptr; + + if (!Package->IsFullyLoaded()) + { + FlushAsyncLoading(); + if (!Package->GetOuter()) + { + Package->FullyLoad(); + } + else + { + Package->GetOutermost()->FullyLoad(); + } + } + UAnimSequence* BakedAnimSequence = Cast(DuplicateObject(CookedAnimSequence, Package, *NewObjectName)); + + //BakedAnimSequence->PreEditChange(nullptr); + + BakedObjectData.BakeStats.NotifyPackageCreated(1); + BakedObjectData.PackagesToSave.Add(BakedAnimSequence->GetPackage()); + BakedAnimSequence->MarkPackageDirty(); + + BakedOutputObject.BakedObject = BakedAnimSequence->GetPathName(); + InNewBakedOutput.BakedOutputObjects.Emplace(Identifier, BakedOutputObject); + + return BakedAnimSequence; +} + + +bool +FHoudiniEngineBakeUtils::BakeGeometryCollectionOutputToActors( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder) +{ + // Check that index is not negative + if (InOutputIndex < 0) + return false; + + if (!InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + + UHoudiniOutput* InOutput = InAllOutputs[InOutputIndex]; + if (!IsValid(InOutput)) + return false; + + if (!IsValid(InCookable)) + return false; + + AActor* OwnerActor = InCookable->GetOwner(); + FString HoudiniAssetActorName = IsValid(OwnerActor) ? OwnerActor->GetActorNameOrLabel() : FString(); + + TMap& OutputObjects = InOutput->GetOutputObjects(); + const TArray& HGPOs = InOutput->GetHoudiniGeoPartObjects(); + + FString GCOutputName = InOutput->GetName(); + + // Map from old static mesh map to new baked static mesh + TMap OldToNewStaticMeshMap; + TMap OldToNewMaterialMap; + + // Need to make sure that all geometry collection meshes are generated before we generate the geometry collection. + int32 NumOutputs = InAllOutputs.Num(); + for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) + { + UHoudiniOutput * Output = InAllOutputs[OutputIdx]; + if (!FHoudiniGeometryCollectionTranslator::IsGeometryCollectionMesh(Output)) + continue; + + for (auto Pair : Output->GetOutputObjects()) + { + const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + const FHoudiniOutputObject& OutputObject = Pair.Value; + + if (!OutputObject.GeometryCollectionPieceName.IsEmpty() && OutputObject.GeometryCollectionPieceName != GCOutputName) + { + continue; + } + + UStaticMesh * StaticMesh = Cast(OutputObject.OutputObject); + if (!IsValid(StaticMesh)) + continue; + + // Add a new baked output object entry and update it with the previous bake's data, if available + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(OutputIdx, Identifier, bHasPreviousBakeData); + + const FString DefaultObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(StaticMesh); + + FHoudiniPackageParams PackageParams; + + if (!ResolvePackageParams( + InCookable, + InOutput, + Identifier, + OutputObject, + bHasPreviousBakeData, + DefaultObjectName, + InBakeFolder, + BakeSettings, + PackageParams, + BakedObjectData)) + { + continue; + } + + UStaticMesh* BakedSM = FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( + StaticMesh, + Cast(BakedOutputObject.GetBakedObjectIfValid()), + PackageParams, + InAllOutputs, + InBakedActors, + InTempCookFolder.Path, + BakedObjectData, + InOutAlreadyBakedStaticMeshMap, + InOutAlreadyBakedMaterialsMap); + + if (!IsValid(BakedSM)) + continue; + + BakedOutputObject.BakedObject = FSoftObjectPath(BakedSM).ToString(); + + // If we are baking in replace mode, remove previously baked components/instancers + if (BakeSettings.bReplaceActors && BakeSettings.bReplaceAssets) + { + const bool bInDestroyBakedComponent = false; + const bool bInDestroyBakedInstancedActors = true; + const bool bInDestroyBakedInstancedComponents = true; + DestroyPreviousBakeOutput( + BakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); + } + + OldToNewStaticMeshMap.Add(FSoftObjectPath(StaticMesh), BakedSM); + + const TArray& StaticMaterials = StaticMesh->GetStaticMaterials(); + const TArray& BakedStaticMaterials = BakedSM->GetStaticMaterials(); + for (int32 i = 0; i < StaticMaterials.Num(); i++) + { + if (i >= BakedStaticMaterials.Num()) + continue; + + OldToNewMaterialMap.Add(StaticMaterials[i].MaterialInterface, BakedStaticMaterials[i].MaterialInterface); + } + + // Update baked output object entry in state + InBakeState.SetNewBakedOutputObject(OutputIdx, Identifier, BakedOutputObject); + } + } + + TArray AllBakedActors = InBakedActors; + TArray NewBakedActors; + for (auto& Pair : OutputObjects) + { + const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + const FHoudiniOutputObject& OutputObject = Pair.Value; + + // Add a new baked output object entry and update it with the previous bake's data, if available + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(InOutputIndex, Identifier, bHasPreviousBakeData); + + if (OutputObject.OutputActors.IsEmpty()) + continue; + + AGeometryCollectionActor* GeometryCollectionActor = Cast(OutputObject.OutputActors[0].Get()); + if (!IsValid(GeometryCollectionActor)) + return false; + + UGeometryCollectionComponent * GeometryCollectionComponent = GeometryCollectionActor->GeometryCollectionComponent; + if (!IsValid(GeometryCollectionComponent)) + return false; + + FGeometryCollectionEdit GeometryCollectionEdit = GeometryCollectionActor->GetGeometryCollectionComponent()->EditRestCollection(GeometryCollection::EEditUpdate::RestPhysicsDynamic); + UGeometryCollection* InGeometryCollection = GeometryCollectionEdit.GetRestCollection(); + + if (!IsValid(InGeometryCollection)) + return false; + + + // Find the HGPO that matches this output identifier + const FHoudiniGeoPartObject* FoundHGPO = nullptr; + FindHGPO(Identifier, HGPOs, FoundHGPO); + + // We do not bake templated geos + if (FoundHGPO && FoundHGPO->bIsTemplated) + continue; + + const FString DefaultObjectName = HoudiniAssetActorName + Identifier.SplitIdentifier; + + UWorld* DesiredWorld = InOutput ? InOutput->GetWorld() : GWorld; + ULevel* DesiredLevel = DesiredWorld ? DesiredWorld->GetCurrentLevel() : GWorld->GetCurrentLevel(); + + FHoudiniPackageParams PackageParams; + FHoudiniAttributeResolver Resolver; + if (!ResolvePackageParamsWithResolver( + InCookable, + InOutput, + Identifier, + OutputObject, + bHasPreviousBakeData, + DefaultObjectName, + InBakeFolder, + BakeSettings, + PackageParams, + Resolver, + BakedObjectData)) + { + continue; + } + + FString* Attribute = Pair.Value.CachedAttributes.Find(HAPI_UNREAL_ATTRIB_BAKE_FOLDER); + if(Attribute != nullptr) + { + PackageParams.BakeFolder = *Attribute; + } + + FName WorldOutlinerFolderPath = GetOutlinerFolderPath( + Resolver, + FName(InFallbackWorldOutlinerFolder.IsEmpty() ? PackageParams.HoudiniAssetActorName : InFallbackWorldOutlinerFolder)); + + // Bake the static mesh if it is still temporary + UGeometryCollection* BakedGC = FHoudiniEngineBakeUtils::DuplicateGeometryCollectionAndCreatePackageIfNeeded( + InGeometryCollection, + Cast(BakedOutputObject.GetBakedObjectIfValid()), + PackageParams, + InAllOutputs, + AllBakedActors, + InTempCookFolder.Path, + OldToNewStaticMeshMap, + OldToNewMaterialMap, + BakedObjectData); + + if (!IsValid(BakedGC)) + continue; + + // Record the baked object + BakedOutputObject.BakedObject = FSoftObjectPath(BakedGC).ToString(); + + // Make sure we have a level to spawn to + if (!IsValid(DesiredLevel)) + continue; + + // Try to find the unreal_bake_actor, if specified + FName BakeActorName; + AActor* FoundActor = nullptr; + bool bHasBakeActorName = false; + FindUnrealBakeActor(OutputObject, BakedOutputObject, AllBakedActors, DesiredLevel, *(PackageParams.ObjectName), BakeSettings, InFallbackActor, FoundActor, bHasBakeActorName, BakeActorName); + + AGeometryCollectionActor* NewGCActor = nullptr; + UGeometryCollectionComponent* NewGCC = nullptr; + if (!FoundActor) + { + + FoundActor = FHoudiniGeometryCollectionTranslator::CreateNewGeometryActor(DesiredWorld, BakeActorName.ToString(), GeometryCollectionComponent->GetComponentTransform()); + // Spawn the new actor + if (!IsValid(FoundActor)) + continue; + + BakedObjectData.BakeStats.NotifyObjectsCreated(FoundActor->GetClass()->GetName(), 1); + + // Copy properties to new actor + NewGCActor = Cast(FoundActor); + if (!IsValid(NewGCActor)) + continue; + + NewGCC = NewGCActor->GetGeometryCollectionComponent(); + } + else + { + if (BakeSettings.bReplaceAssets) + { + // Check if we have a previous bake component and that it belongs to FoundActor, if so, reuse it + UGeometryCollectionComponent* PrevGCC = Cast(BakedOutputObject.GetBakedComponentIfValid()); + if (IsValid(PrevGCC) && (PrevGCC->GetOwner() == FoundActor)) + { + NewGCC = PrevGCC; + } + } + + const bool bCreateIfMissing = true; + USceneComponent* RootComponent = GetActorRootComponent(FoundActor, bCreateIfMissing); + + if (!IsValid(NewGCC)) + { + // Create a new static mesh component on the existing actor + NewGCC = NewObject(FoundActor, NAME_None, RF_Transactional); + + FoundActor->AddInstanceComponent(NewGCC); + if (IsValid(RootComponent)) + NewGCC->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); + else + FoundActor->SetRootComponent(NewGCC); + NewGCC->RegisterComponent(); + } + + NewGCActor = Cast(FoundActor); + + BakedObjectData.BakeStats.NotifyObjectsUpdated(FoundActor->GetClass()->GetName(), 1); + } + + + if (BakeSettings.ActorBakeOption == EHoudiniEngineActorBakeOption::OneActorPerHDA) + { + // Parent to an actor + FHoudiniAttributeResolver OutResolver; + TMap Tokens = OutputObject.CachedTokens; + PackageParams.UpdateTokensFromParams(InCookable->GetWorld(), InCookable->GetComponent(), Tokens); + OutResolver.SetTokensFromStringMap(Tokens); + FName ParentActorName = FName(OutResolver.ResolveString(BakeSettings.DefaultBakeName)); + AActor* FoundParent = Cast(StaticFindObjectFast(AActor::StaticClass(), DesiredLevel, ParentActorName)); + + if (!IsValid(FoundParent)) + { + // Get the actor factory for the unreal_bake_actor_class attribute. If not set, use an empty actor. + TSubclassOf BakeActorClass = nullptr; + UActorFactory* ActorFactory = GetActorFactory(OutputObject, BakeSettings, BakeActorClass, UActorFactoryEmptyActor::StaticClass()); + if (!ActorFactory) + { + return false; + } + + constexpr UObject* AssetToSpawn = nullptr; + constexpr EObjectFlags ObjectFlags = RF_Transactional; + ParentActorName = *MakeUniqueObjectNameIfNeeded(DesiredLevel, AActor::StaticClass(), ParentActorName.ToString()); + + USceneComponent* CookableComponent = InCookable->GetComponent(); + FTransform CookableTransform = CookableComponent ? CookableComponent->GetComponentTransform() : FTransform::Identity; + + FActorSpawnParameters SpawnParam; + SpawnParam.ObjectFlags = ObjectFlags; + SpawnParam.Name = ParentActorName; + FoundParent = SpawnBakeActor( + ActorFactory, AssetToSpawn, DesiredLevel, BakeSettings, + CookableTransform, CookableComponent, BakeActorClass, SpawnParam); + + FoundParent->SetActorLabel(ParentActorName.ToString()); + } + + FoundActor->AttachToActor(FoundParent, FAttachmentTransformRules::KeepWorldTransform); + } + + // We need to make a unique name for the actor, renaming an object on top of another is a fatal error + FString NewNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, AGeometryCollectionActor::StaticClass(), BakeActorName.ToString(), FoundActor); + RenameAndRelabelActor(FoundActor, NewNameStr, false); + SetOutlinerFolderPath(FoundActor, WorldOutlinerFolderPath); + + if (IsValid(NewGCC)) + { + const bool bCopyWorldTransform = true; + CopyPropertyToNewGeometryCollectionActorAndComponent(NewGCActor, NewGCC, GeometryCollectionComponent, bCopyWorldTransform); + + NewGCC->SetRestCollection(BakedGC); + BakedOutputObject.BakedComponent = FSoftObjectPath(NewGCC).ToString(); + } + + BakedOutputObject.Actor = FSoftObjectPath(FoundActor).ToString(); + const FHoudiniEngineBakedActor& BakedActorEntry = AllBakedActors.Add_GetRef(FHoudiniEngineBakedActor( + FoundActor, BakeActorName, WorldOutlinerFolderPath, InOutputIndex, Identifier, BakedGC, InGeometryCollection, GeometryCollectionComponent, + PackageParams.BakeFolder, PackageParams)); + NewBakedActors.Add(BakedActorEntry); + + // If we are baking in replace mode, remove previously baked components/instancers + if (BakeSettings.bReplaceActors && BakeSettings.bReplaceAssets) + { + const bool bInDestroyBakedComponent = false; + const bool bInDestroyBakedInstancedActors = true; + const bool bInDestroyBakedInstancedComponents = true; + DestroyPreviousBakeOutput(BakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); + } + + // Update baked output object entry in state + InBakeState.SetNewBakedOutputObject(InOutputIndex, Identifier, BakedOutputObject); + } + + OutActors = MoveTemp(NewBakedActors); + + return true; +} + +bool +FHoudiniEngineBakeUtils::BakeHoudiniCurveOutputToActors( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder) +{ + // Check that index is not negative + if (InOutputIndex < 0) + return false; + + if (!InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + + UHoudiniOutput* const Output = InAllOutputs[InOutputIndex]; + if (!IsValid(Output)) + return false; + + if (!IsValid(InCookable)) + return false; + + AActor* OwnerActor = InCookable->GetOwner(); + const FString HoudiniAssetActorName = IsValid(OwnerActor) ? OwnerActor->GetActorNameOrLabel() : FString(); + + FHoudiniBakedObjectData BakedObjectData; + + TMap& OutputObjects = Output->GetOutputObjects(); + + const TArray & HGPOs = Output->GetHoudiniGeoPartObjects(); + + TArray AllBakedActors = InBakedActors; + TArray NewBakedActors; + + for (auto & Pair : OutputObjects) + { + FHoudiniOutputObject& OutputObject = Pair.Value; + + if (OutputObject.OutputComponents.IsEmpty()) + continue; + + HOUDINI_CHECK_RETURN(OutputObject.OutputComponents.Num() == 1, false); + + USplineComponent* SplineComponent = Cast(OutputObject.OutputComponents[0]); + if (!IsValid(SplineComponent)) + continue; + + const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(InOutputIndex, Identifier, bHasPreviousBakeData); + + // TODO: FIX ME!! May not work 100% + const FHoudiniGeoPartObject* FoundHGPO = nullptr; + for (auto & NextHGPO : HGPOs) + { + if (Identifier.GeoId == NextHGPO.GeoId && + Identifier.ObjectId == NextHGPO.ObjectId && + Identifier.PartId == NextHGPO.PartId) + { + FoundHGPO = &NextHGPO; + break; + } + } + + if (!FoundHGPO) + continue; + + const FString DefaultObjectName = HoudiniAssetActorName + "_" + SplineComponent->GetName(); + + FHoudiniPackageParams PackageParams; + // Set the replace mode based on if we are doing a replacement or incremental asset bake + EPackageReplaceMode AssetPackageReplaceMode = BakeSettings.bReplaceAssets ? EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; + + // Configure FHoudiniAttributeResolver and fill the package params with resolved object name and bake folder. + // The resolver is then also configured with the package params for subsequent resolving (level_path etc) + FHoudiniAttributeResolver Resolver; + UWorld* const DesiredWorld = SplineComponent ? SplineComponent->GetWorld() : GWorld; + FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( + DesiredWorld, InCookable, Identifier, OutputObject, bHasPreviousBakeData, DefaultObjectName, + PackageParams, Resolver, InBakeFolder.Path, AssetPackageReplaceMode); + + FHoudiniEngineBakedActor OutputBakedActor; + BakeCurve( + InCookable, OutputObject, BakedOutputObject, PackageParams, Resolver, BakeSettings, + AllBakedActors, OutputBakedActor, BakedObjectData, InFallbackActor); + + OutputBakedActor.OutputIndex = InOutputIndex; + OutputBakedActor.OutputObjectIdentifier = Identifier; + + // Don't forget to copy the tags to the curve's actor + if (FoundHGPO && IsValid(OutputBakedActor.Actor)) + { + FHoudiniEngineUtils::KeepOrClearActorTags(OutputBakedActor.Actor, true, false, FoundHGPO); + // Add actor tags from generic property attributes + FHoudiniEngineUtils::ApplyTagsToActorOnly(FoundHGPO->GenericPropertyAttributes, OutputBakedActor.Actor->Tags); + } + + AllBakedActors.Add(OutputBakedActor); + NewBakedActors.Add(OutputBakedActor); + + // Update the cached bake output results + InBakeState.SetNewBakedOutputObject(InOutputIndex, Identifier, BakedOutputObject); + } + + OutActors = MoveTemp(NewBakedActors); + + SaveBakedPackages(BakedObjectData.PackagesToSave); + + return true; +} + +bool +FHoudiniEngineBakeUtils::CopyActorContentsToBlueprint(AActor * InActor, UBlueprint * OutBlueprint, bool bInRenameComponentsWithInvalidNames) +{ + if (!IsValid(InActor)) + return false; + + if (!IsValid(OutBlueprint)) + return false; + + if (bInRenameComponentsWithInvalidNames) + { + for (UActorComponent* const Comp : InActor->GetInstanceComponents()) + { + if (!IsValid(Comp)) + continue; + + // If the component name would not be a valid variable name in the BP, rename it + if (!FComponentEditorUtils::IsValidVariableNameString(Comp, Comp->GetName())) + { + FString NewComponentName = FComponentEditorUtils::GenerateValidVariableName(Comp->GetClass(), Comp->GetOwner()); + NewComponentName = MakeUniqueObjectNameIfNeeded(Comp->GetOuter(), Comp->GetClass(), NewComponentName, Comp); + if (NewComponentName != Comp->GetName()) + Comp->Rename(*NewComponentName); + } + } + } + + if (InActor->GetInstanceComponents().Num() > 0) + FKismetEditorUtilities::AddComponentsToBlueprint( + OutBlueprint, + InActor->GetInstanceComponents()); + + if (OutBlueprint->GeneratedClass) + { + AActor * CDO = Cast< AActor >(OutBlueprint->GeneratedClass->GetDefaultObject()); + if (!IsValid(CDO)) + return false; + + const auto CopyOptions = (EditorUtilities::ECopyOptions::Type) + (EditorUtilities::ECopyOptions::OnlyCopyEditOrInterpProperties | + EditorUtilities::ECopyOptions::PropagateChangesToArchetypeInstances); + + EditorUtilities::CopyActorProperties(InActor, CDO, CopyOptions); + + USceneComponent * Scene = CDO->GetRootComponent(); + if (IsValid(Scene)) + { + Scene->SetRelativeLocation(FVector::ZeroVector); + Scene->SetRelativeRotation(FRotator::ZeroRotator); + + // Clear out the attachment info after having copied the properties from the source actor + Scene->SetupAttachment(nullptr); + while (true) + { + const int32 ChildCount = Scene->GetAttachChildren().Num(); + if (ChildCount < 1) + break; + + USceneComponent * Component = Scene->GetAttachChildren()[ChildCount - 1]; + if (IsValid(Component)) + Component->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + } + check(Scene->GetAttachChildren().Num() == 0); + + // Ensure the light mass information is cleaned up + Scene->InvalidateLightingCache(); + + // Copy relative scale from source to target. + if (USceneComponent* SrcSceneRoot = InActor->GetRootComponent()) + { + Scene->SetRelativeScale3D_Direct(SrcSceneRoot->GetRelativeScale3D()); + } + } + } + + // Compile our blueprint and notify asset system about blueprint. + FBlueprintEditorUtils::MarkBlueprintAsModified(OutBlueprint); + //FKismetEditorUtilities::CompileBlueprint(OutBlueprint); + //FAssetRegistryModule::AssetCreated(OutBlueprint); + + return true; +} + +bool +FHoudiniEngineBakeUtils::BakeBlueprints(UHoudiniCookable* InCookable, const FHoudiniBakeSettings& BakeSettings) +{ + FHoudiniBakedObjectData BakedObjectData; + const bool bSuccess = BakeBlueprints(InCookable, BakeSettings, BakedObjectData); + if (!bSuccess) + { + HOUDINI_LOG_WARNING(TEXT("Errors while baking to blueprints.")); + } + + // Compile the new/updated blueprints + for (UBlueprint* const Blueprint : BakedObjectData.Blueprints) + { + if (!IsValid(Blueprint)) + continue; + + FKismetEditorUtilities::CompileBlueprint(Blueprint); + } + + FHoudiniEngineBakeUtils::SaveBakedPackages(BakedObjectData.PackagesToSave); + + // Sync the CB to the baked objects + if(GEditor && BakedObjectData.Blueprints.Num() > 0) + { + TArray Assets; + Assets.Reserve(BakedObjectData.Blueprints.Num()); + for (UBlueprint* Blueprint : BakedObjectData.Blueprints) + { + Assets.Add(Blueprint); + } + GEditor->SyncBrowserToObjects(Assets); + } + + if (InCookable->GetDoSlateNotifications()) + { + const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); + FString Msg = FString::Format(*FinishedTemplate, { BakedObjectData.BakeStats.NumPackagesCreated, BakedObjectData.BakeStats.NumPackagesUpdated } ); + FHoudiniEngine::Get().FinishTaskSlateNotification( FText::FromString(Msg) ); + } + + TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); + + // Broadcast that the bake is complete + InCookable->HandleOnPostBake(bSuccess); + + return bSuccess; +} + +bool +FHoudiniEngineBakeUtils::BakeBlueprints( + UHoudiniCookable* InCookable, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData) +{ + if (!IsValid(InCookable)) + return false; + + AActor* OwnerActor = InCookable->GetOwner(); + const bool bIsOwnerActorValid = IsValid(OwnerActor); + + // Don't process outputs that are not supported in blueprints + TArray OutputsToBake = { + EHoudiniOutputType::Mesh, + EHoudiniOutputType::Instancer, + EHoudiniOutputType::Curve, + EHoudiniOutputType::GeometryCollection + }; + + TArray InstancerComponentTypesToBake = { + EHoudiniInstancerComponentType::StaticMeshComponent, + EHoudiniInstancerComponentType::InstancedStaticMeshComponent, + EHoudiniInstancerComponentType::MeshSplitInstancerComponent, + EHoudiniInstancerComponentType::FoliageAsHierarchicalInstancedStaticMeshComponent, + EHoudiniInstancerComponentType::GeometryCollectionComponent/*, + EHoudiniInstancerComponentType::InstancedActorComponent*/ + }; + + + // When baking blueprints we always create new actors since they are deleted from the world once copied into the + // blueprint + FHoudiniBakeSettings ActorBakeSettings = BakeSettings; + ActorBakeSettings.bReplaceActors = false; + + TArray TempActors; + bool bBakeSuccess = BakeCookableToActors( + InCookable, + ActorBakeSettings, + TempActors, + BakedObjectData, + &OutputsToBake, + &InstancerComponentTypesToBake); + + if (!bBakeSuccess) + { + HOUDINI_LOG_ERROR(TEXT("Could not create output actors for baking to blueprint.")); + return false; + } + + // Get the previous baked outputs + TArray& BakedOutputs = InCookable->GetBakedOutputs(); + + bBakeSuccess = BakeBlueprintsFromBakedActors( + TempActors, + BakeSettings, + InCookable->GetHoudiniAssetName(), + bIsOwnerActorValid ? OwnerActor->GetActorNameOrLabel() : FString(), + InCookable->GetBakeFolder(), + &BakedOutputs, + nullptr, + BakedObjectData); + + return bBakeSuccess; +} + +bool +FHoudiniEngineBakeUtils::BakeGeometryCollectionOutputToAsset( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + const FDirectoryPath& InTempFolder, + const FDirectoryPath& InBakeFolder, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData) +{ + // Check that index is not negative + if (InOutputIndex < 0) + return false; + + if (!InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + + UHoudiniOutput* InOutput = InAllOutputs[InOutputIndex]; + if (!IsValid(InOutput)) + return false; + + if (!IsValid(InCookable)) + return false; + + AActor* OwnerActor = InCookable->GetOwner(); + FString HoudiniAssetActorName = IsValid(OwnerActor) ? OwnerActor->GetActorNameOrLabel() : FString(); + + TMap& OutputObjects = InOutput->GetOutputObjects(); + const TArray& HGPOs = InOutput->GetHoudiniGeoPartObjects(); + + FString GCOutputName = InOutput->GetName(); + + // Map from old static mesh map to new baked static mesh + TMap OldToNewStaticMeshMap; + TMap OldToNewMaterialMap; + + // Needed for DuplicateStaticMeshAndCreatePackageIfNeeded + TArray BakedActors; + TMap AlreadyBakedMaterialsMap; + TMap AlreadyBakedStaticMeshMap; + + // Need to make sure that all geometry collection meshes are generated before we generate the geometry collection. + int32 NumOutputs = InAllOutputs.Num(); + for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) + { + UHoudiniOutput* Output = InAllOutputs[OutputIdx]; + if (!FHoudiniGeometryCollectionTranslator::IsGeometryCollectionMesh(Output)) + continue; + + for (auto Pair : Output->GetOutputObjects()) + { + const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + const FHoudiniOutputObject& OutputObject = Pair.Value; + + if (!OutputObject.GeometryCollectionPieceName.IsEmpty() && OutputObject.GeometryCollectionPieceName != GCOutputName) + { + continue; + } + + UStaticMesh* StaticMesh = Cast(OutputObject.OutputObject); + if (!IsValid(StaticMesh)) + continue; + + // Add a new baked output object entry and update it with the previous bake's data, if available + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(OutputIdx, Identifier, bHasPreviousBakeData); + + const FString DefaultObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(StaticMesh); + + FHoudiniPackageParams PackageParams; + + if (!ResolvePackageParams( + InCookable, + InOutput, + Identifier, + OutputObject, + bHasPreviousBakeData, + DefaultObjectName, + InBakeFolder, + BakeSettings, + PackageParams, + BakedObjectData)) + { + continue; + } + + UStaticMesh* BakedSM = FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( + StaticMesh, + Cast(BakedOutputObject.GetBakedObjectIfValid()), + PackageParams, + InAllOutputs, + BakedActors, + InTempFolder.Path, + BakedObjectData, + AlreadyBakedStaticMeshMap, + AlreadyBakedMaterialsMap); + + if (!IsValid(BakedSM)) + continue; + + BakedOutputObject.BakedObject = FSoftObjectPath(BakedSM).ToString(); + + OldToNewStaticMeshMap.Add(FSoftObjectPath(StaticMesh), BakedSM); + + const TArray& StaticMaterials = StaticMesh->GetStaticMaterials(); + const TArray& BakedStaticMaterials = BakedSM->GetStaticMaterials(); + for (int32 i = 0; i < StaticMaterials.Num(); i++) + { + if (i >= BakedStaticMaterials.Num()) + continue; + + OldToNewMaterialMap.Add(StaticMaterials[i].MaterialInterface, BakedStaticMaterials[i].MaterialInterface); + } + + // Update baked output object entry in state + InBakeState.SetNewBakedOutputObject(OutputIdx, Identifier, BakedOutputObject); + } + } + + //TArray AllBakedActors = InBakedActors; + //TArray NewBakedActors; + for (auto& Pair : OutputObjects) + { + const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + const FHoudiniOutputObject& OutputObject = Pair.Value; + + // Add a new baked output object entry and update it with the previous bake's data, if available + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(InOutputIndex, Identifier, bHasPreviousBakeData); + + if (OutputObject.OutputActors.IsEmpty()) + continue; + + AGeometryCollectionActor* GeometryCollectionActor = Cast(OutputObject.OutputActors[0].Get()); + if (!IsValid(GeometryCollectionActor)) + return false; + + UGeometryCollectionComponent* GeometryCollectionComponent = GeometryCollectionActor->GeometryCollectionComponent; + if (!IsValid(GeometryCollectionComponent)) + return false; + + FGeometryCollectionEdit GeometryCollectionEdit = GeometryCollectionActor->GetGeometryCollectionComponent()->EditRestCollection(GeometryCollection::EEditUpdate::RestPhysicsDynamic); + UGeometryCollection* InGeometryCollection = GeometryCollectionEdit.GetRestCollection(); + + if (!IsValid(InGeometryCollection)) + return false; + + + // Find the HGPO that matches this output identifier + const FHoudiniGeoPartObject* FoundHGPO = nullptr; + FindHGPO(Identifier, HGPOs, FoundHGPO); + + // We do not bake templated geos + if (FoundHGPO && FoundHGPO->bIsTemplated) + continue; + + const FString DefaultObjectName = HoudiniAssetActorName + Identifier.SplitIdentifier; + + //UWorld* DesiredWorld = InOutput ? InOutput->GetWorld() : GWorld; + //ULevel* DesiredLevel = DesiredWorld->GetCurrentLevel(); + + FHoudiniPackageParams PackageParams; + FHoudiniAttributeResolver Resolver; + if (!ResolvePackageParamsWithResolver( + InCookable, + InOutput, + Identifier, + OutputObject, + bHasPreviousBakeData, + DefaultObjectName, + InBakeFolder, + BakeSettings, + PackageParams, + Resolver, + BakedObjectData)) + { + continue; + } + + /* + FName WorldOutlinerFolderPath = GetOutlinerFolderPath( + Resolver, + FName(InFallbackWorldOutlinerFolder.IsEmpty() ? PackageParams.HoudiniAssetActorName : InFallbackWorldOutlinerFolder)); + */ + + // Bake the GC if it is still temporary + UGeometryCollection* BakedGC = FHoudiniEngineBakeUtils::DuplicateGeometryCollectionAndCreatePackageIfNeeded( + InGeometryCollection, + Cast(BakedOutputObject.GetBakedObjectIfValid()), + PackageParams, + InAllOutputs, + BakedActors, + InTempFolder.Path, + OldToNewStaticMeshMap, + OldToNewMaterialMap, + BakedObjectData); + + if (!IsValid(BakedGC)) + continue; + + // Record the baked object + BakedOutputObject.BakedObject = FSoftObjectPath(BakedGC).ToString(); + + /*// Make sure we have a level to spawn to + if (!IsValid(DesiredLevel)) + continue; + + // Try to find the unreal_bake_actor, if specified + FName BakeActorName; + AActor* FoundActor = nullptr; + bool bHasBakeActorName = false; + FindUnrealBakeActor(OutputObject, BakedOutputObject, AllBakedActors, DesiredLevel, *(PackageParams.ObjectName), BakeSettings, InFallbackActor, FoundActor, bHasBakeActorName, BakeActorName); + + AGeometryCollectionActor* NewGCActor = nullptr; + UGeometryCollectionComponent* NewGCC = nullptr; + if (!FoundActor) + { + + FoundActor = FHoudiniGeometryCollectionTranslator::CreateNewGeometryActor(DesiredWorld, BakeActorName.ToString(), GeometryCollectionComponent->GetComponentTransform()); + // Spawn the new actor + if (!IsValid(FoundActor)) + continue; + + BakedObjectData.BakeStats.NotifyObjectsCreated(FoundActor->GetClass()->GetName(), 1); + + // Copy properties to new actor + NewGCActor = Cast(FoundActor); + if (!IsValid(NewGCActor)) + continue; + + NewGCC = NewGCActor->GetGeometryCollectionComponent(); + } + else + { + if (BakeSettings.bReplaceAssets) + { + // Check if we have a previous bake component and that it belongs to FoundActor, if so, reuse it + UGeometryCollectionComponent* PrevGCC = Cast(BakedOutputObject.GetBakedComponentIfValid()); + if (IsValid(PrevGCC) && (PrevGCC->GetOwner() == FoundActor)) + { + NewGCC = PrevGCC; + } + } + + const bool bCreateIfMissing = true; + USceneComponent* RootComponent = GetActorRootComponent(FoundActor, bCreateIfMissing); + + if (!IsValid(NewGCC)) + { + // Create a new static mesh component on the existing actor + NewGCC = NewObject(FoundActor, NAME_None, RF_Transactional); + + FoundActor->AddInstanceComponent(NewGCC); + if (IsValid(RootComponent)) + NewGCC->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); + else + FoundActor->SetRootComponent(NewGCC); + NewGCC->RegisterComponent(); + } + + NewGCActor = Cast(FoundActor); + + BakedObjectData.BakeStats.NotifyObjectsUpdated(FoundActor->GetClass()->GetName(), 1); + } + + + if (BakeSettings.ActorBakeOption == EHoudiniEngineActorBakeOption::OneActorPerHDA) + { + // Parent to an actor + FHoudiniAttributeResolver OutResolver; + TMap Tokens = OutputObject.CachedTokens; + PackageParams.UpdateTokensFromParams(InCookable->GetWorld(), InCookable->GetComponent(), Tokens); + OutResolver.SetTokensFromStringMap(Tokens); + FName ParentActorName = FName(OutResolver.ResolveString(BakeSettings.DefaultBakeName)); + AActor* FoundParent = Cast(StaticFindObjectFast(AActor::StaticClass(), DesiredLevel, ParentActorName)); + + if (!IsValid(FoundParent)) + { + // Get the actor factory for the unreal_bake_actor_class attribute. If not set, use an empty actor. + TSubclassOf BakeActorClass = nullptr; + UActorFactory* ActorFactory = GetActorFactory(OutputObject, BakeSettings, BakeActorClass, UActorFactoryEmptyActor::StaticClass()); + if (!ActorFactory) + { + return false; + } + + constexpr UObject* AssetToSpawn = nullptr; + constexpr EObjectFlags ObjectFlags = RF_Transactional; + ParentActorName = *MakeUniqueObjectNameIfNeeded(DesiredLevel, AActor::StaticClass(), ParentActorName.ToString()); + + USceneComponent* CookableComponent = InCookable->GetComponent(); + FTransform CookableTransform = CookableComponent ? CookableComponent->GetComponentTransform() : FTransform::Identity; + + FActorSpawnParameters SpawnParam; + SpawnParam.ObjectFlags = ObjectFlags; + SpawnParam.Name = ParentActorName; + FoundParent = SpawnBakeActor( + ActorFactory, AssetToSpawn, DesiredLevel, BakeSettings, + CookableTransform, CookableComponent, BakeActorClass, SpawnParam); + + FoundParent->SetActorLabel(ParentActorName.ToString()); + } + + FoundActor->AttachToActor(FoundParent, FAttachmentTransformRules::KeepWorldTransform); + } + + // We need to make a unique name for the actor, renaming an object on top of another is a fatal error + FString NewNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, AGeometryCollectionActor::StaticClass(), BakeActorName.ToString(), FoundActor); + RenameAndRelabelActor(FoundActor, NewNameStr, false); + SetOutlinerFolderPath(FoundActor, WorldOutlinerFolderPath); + + if (IsValid(NewGCC)) + { + const bool bCopyWorldTransform = true; + CopyPropertyToNewGeometryCollectionActorAndComponent(NewGCActor, NewGCC, GeometryCollectionComponent, bCopyWorldTransform); + + NewGCC->SetRestCollection(BakedGC); + BakedOutputObject.BakedComponent = FSoftObjectPath(NewGCC).ToString(); + } + + BakedOutputObject.Actor = FSoftObjectPath(FoundActor).ToString(); + const FHoudiniEngineBakedActor& BakedActorEntry = AllBakedActors.Add_GetRef(FHoudiniEngineBakedActor( + FoundActor, BakeActorName, WorldOutlinerFolderPath, InOutputIndex, Identifier, BakedGC, InGeometryCollection, GeometryCollectionComponent, + PackageParams.BakeFolder, PackageParams)); + NewBakedActors.Add(BakedActorEntry); + + // If we are baking in replace mode, remove previously baked components/instancers + if (BakeSettings.bReplaceActors && BakeSettings.bReplaceAssets) + { + const bool bInDestroyBakedComponent = false; + const bool bInDestroyBakedInstancedActors = true; + const bool bInDestroyBakedInstancedComponents = true; + DestroyPreviousBakeOutput(BakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); + }*/ + + // Update baked output object entry in state + InBakeState.SetNewBakedOutputObject(InOutputIndex, Identifier, BakedOutputObject); + } + + return true; +} + +bool +FHoudiniEngineBakeUtils::BakeSkeletalMeshOutputToAsset( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + const FDirectoryPath& InTempFolder, + const FDirectoryPath& InBakeFolder, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData) +{ + // Check that index is not negative + if (InOutputIndex < 0) + return false; + + if (!InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + + UHoudiniOutput* InOutput = InAllOutputs[InOutputIndex]; + if (!IsValid(InOutput)) + return false; + + TMap& OutputObjects = InOutput->GetOutputObjects(); + const TArray& HGPOs = InOutput->GetHoudiniGeoPartObjects(); + + // Order the objects: Invisible complex colliders first + // We need to make sure we bake invisible complex colliders first, since they are static meshes themselves + // but are referenced by the main static mesh + TArray OrderedObjects; + for (auto& Pair : OutputObjects) + { + const FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + const EHoudiniSplitType SplitType = FHoudiniMeshTranslator::GetSplitTypeFromSplitName(Identifier.SplitIdentifier); + if (SplitType == EHoudiniSplitType::InvisibleComplexCollider) + OrderedObjects.Insert(Identifier, 0); + else + OrderedObjects.Add(Identifier); + } + + // Need for DuplicateStaticMeshAndCreatePackageIfNeeded + TArray BakedActors; + TMap AlreadyBakedMaterialsMap; + TMap AlreadyBakedSkeletalMeshMap; + + for (auto& Identifier : OrderedObjects) + { + const FHoudiniOutputObject& OutputObject = OutputObjects[Identifier]; + USkeletalMesh* SkeletalMesh = Cast(OutputObject.OutputObject); + if (!IsValid(SkeletalMesh)) + continue; + + // Initialize the baked output object entry (use the previous bake's data, if available). + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(InOutputIndex, Identifier, bHasPreviousBakeData); + + // Find the HGPO that matches this output identifier + const FHoudiniGeoPartObject* FoundHGPO = nullptr; + FindHGPO(Identifier, HGPOs, FoundHGPO); + + // We do not bake templated geos + if (FoundHGPO && FoundHGPO->bIsTemplated) + return true; + + const FString DefaultObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(SkeletalMesh); + + // + // 1. Bake the Skeleton asset + // + FHoudiniPackageParams SkeletonPackageParams; + FHoudiniOutputObjectIdentifier SkeletonIdentifier = Identifier; + SkeletonIdentifier.SplitIdentifier = TEXT("skeleton"); + if (!ResolvePackageParams( + InCookable, + InOutput, + SkeletonIdentifier, + OutputObject, + bHasPreviousBakeData, + DefaultObjectName + TEXT("_skeleton"), + InBakeFolder, + BakeSettings, + SkeletonPackageParams, + BakedObjectData)) + { + return false; + } + + // TODO: add an attribute for controlling the skeleton's bake name? + if (!SkeletonPackageParams.ObjectName.Contains(TEXT("skeleton"), ESearchCase::IgnoreCase)) + SkeletonPackageParams.ObjectName += TEXT("_skeleton"); + + USkeleton* Skeleton = SkeletalMesh->GetSkeleton(); + USkeleton* BakedSkeleton = DuplicateSkeletonAndCreatePackageIfNeeded( + Skeleton, + BakedOutputObject.GetBakedSkeletonIfValid(), + SkeletonPackageParams, + InAllOutputs, + BakedActors, + InTempFolder.Path, + BakedObjectData, + InBakeState.GetBakedSkeletons()); + + if (Skeleton != BakedSkeleton) + BakedOutputObject.BakedSkeleton = FSoftObjectPath(BakedSkeleton).ToString(); + else + BakedOutputObject.BakedSkeleton = FSoftObjectPath(nullptr).ToString(); + + + // + // 2. Bake Physics Asset + // + FHoudiniPackageParams PhysicsAssetsPackageParams; + FHoudiniOutputObjectIdentifier PhysicsAsssetIdentifier = Identifier; + PhysicsAsssetIdentifier.SplitIdentifier = TEXT("physics_asset"); + if (!ResolvePackageParams( + InCookable, + InOutput, + PhysicsAsssetIdentifier, + OutputObject, + bHasPreviousBakeData, + DefaultObjectName + TEXT("_physics_asset"), + InBakeFolder, + BakeSettings, + PhysicsAssetsPackageParams, + BakedObjectData)) + { + return false; + } + + // TODO: add an attribute for controlling the bake name? + if (!PhysicsAssetsPackageParams.ObjectName.Contains(TEXT("physics_asset"), ESearchCase::IgnoreCase)) + PhysicsAssetsPackageParams.ObjectName += TEXT("physics_asset"); + + // Bake the Physics Asset if it is temporary + UPhysicsAsset* PhysicsAsset = SkeletalMesh->GetPhysicsAsset(); + UPhysicsAsset* BakedPhysicsAsset = DuplicatePhysicsAssetAndCreatePackageIfNeeded( + PhysicsAsset, + BakedOutputObject.GetBakedPhysicsAssetIfValid(), + PhysicsAssetsPackageParams, + InAllOutputs, + BakedActors, + InTempFolder.Path, + BakedObjectData, + InBakeState.GetBakedPhysicsAssets()); + + if (PhysicsAsset != BakedPhysicsAsset) + BakedOutputObject.BakedPhysicsAsset = FSoftObjectPath(BakedPhysicsAsset).ToString(); + else + BakedOutputObject.BakedPhysicsAsset = FSoftObjectPath(nullptr).ToString(); + + // + // 3. Bake the Skeletal mesh + // + FHoudiniAttributeResolver Resolver; + FHoudiniPackageParams PackageParams; + if (!ResolvePackageParamsWithResolver( + InCookable, + InOutput, + Identifier, + OutputObject, + bHasPreviousBakeData, + DefaultObjectName, + InBakeFolder, + BakeSettings, + PackageParams, + Resolver, + BakedObjectData)) + { + return false; + } + + // Bake the skeletal mesh if it is still temporary + USkeletalMesh* BakedSKM = FHoudiniEngineBakeUtils::DuplicateSkeletalMeshAndCreatePackageIfNeeded( + SkeletalMesh, + Cast(BakedOutputObject.GetBakedObjectIfValid()), + PackageParams, + InAllOutputs, + BakedActors, + InTempFolder.Path, + BakedObjectData, + AlreadyBakedSkeletalMeshMap, + AlreadyBakedMaterialsMap); + + if (!IsValid(BakedSKM)) + return false; + + // + // Update the Skeleton/Physics Asset of the baked SKM if the skeleton was baked + // + if (BakedSKM->GetSkeleton() != BakedSkeleton) + BakedSKM->SetSkeleton(BakedSkeleton); + + if (BakedSKM->GetPhysicsAsset() != BakedPhysicsAsset) + { + BakedSKM->SetPhysicsAsset(BakedPhysicsAsset); + BakedPhysicsAsset->SetPreviewMesh(BakedSKM); + } + + // Record the baked object + BakedOutputObject.BakedObject = FSoftObjectPath(BakedSKM).ToString(); + + // Implicit object, no component and no actor + BakedOutputObject.BakedComponent = nullptr; + BakedOutputObject.Actor = nullptr; + + // Record bake data + InBakeState.SetNewBakedOutputObject(InOutputIndex, Identifier, MoveTemp(BakedOutputObject)); + } + + return true; +} + +UStaticMesh* +FHoudiniEngineBakeUtils::BakeStaticMesh( + UStaticMesh * StaticMesh, + const FHoudiniPackageParams& PackageParams, + const TArray& InAllOutputs, + const FDirectoryPath& InTempCookFolder, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap) +{ + if (!IsValid(StaticMesh)) + return nullptr; + + FHoudiniBakedObjectData BakedObjectData; + TArray Outputs; + const TArray BakedResults; + UStaticMesh* BakedStaticMesh = FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( + StaticMesh, nullptr, PackageParams, InAllOutputs, BakedResults, InTempCookFolder.Path, BakedObjectData, + InOutAlreadyBakedStaticMeshMap, InOutAlreadyBakedMaterialsMap); + + if (BakedStaticMesh) + { + FHoudiniEngineBakeUtils::SaveBakedPackages(BakedObjectData.PackagesToSave); + + // Sync the CB to the baked objects + if(GEditor) + { + TArray Objects; + Objects.Add(BakedStaticMesh); + GEditor->SyncBrowserToObjects(Objects); + } + } + + return BakedStaticMesh; +} + +UFoliageType* +FHoudiniEngineBakeUtils::DuplicateFoliageTypeAndCreatePackageIfNeeded( + UFoliageType* InFoliageType, + UFoliageType* InPreviousBakeFoliageType, + const FHoudiniPackageParams& PackageParams, + const TArray& InParentOutputs, + const TArray& InCurrentBakeResults, + const FString& InTemporaryCookFolder, + TMap& InOutAlreadyBakedFoliageTypes, + TMap& InOutAlreadyBakedMaterialsMap, + const TArray& BakedResults, + FHoudiniBakedObjectData& BakedObjectData) +{ + HOUDINI_CHECK_RETURN(IsValid(InFoliageType), nullptr); + + // The not a temporary one/already baked, we can simply reuse it + // instead of duplicating it. + + const bool bIsTemporary = IsObjectTemporary(InFoliageType, EHoudiniOutputType::Instancer, InParentOutputs, InTemporaryCookFolder, PackageParams.ComponentGUID); + if (!bIsTemporary) + { + return InFoliageType; + } + + // See if we already baked this one. If so, use it. + + UFoliageType ** AlreadyBaked = InOutAlreadyBakedFoliageTypes.Find(InFoliageType); + if (AlreadyBaked && IsValid(*AlreadyBaked)) + return *AlreadyBaked; + + // Look to see if its in the baked results. + + for (const FHoudiniEngineBakedActor& BakeResult : BakedResults) + { + if (BakeResult.SourceObject == InFoliageType && IsValid(BakeResult.BakedObject) + && BakeResult.BakedObject->IsA(InFoliageType->GetClass())) + { + // We have found a bake result where InStaticMesh was the source object and we have a valid BakedObject + // of a compatible class + return Cast(BakeResult.BakedObject); + } + } + + // Not previously baked, so make a copy of the cooked asset. + + // If we have a previously baked object, get the bake counter from it so that both replace and increment + // is consistent with the bake counter + int32 BakeCounter = 0; + bool bPreviousBakeObjectValid = IsValid(InPreviousBakeFoliageType); + TArray PreviousBakeMaterials; + if (bPreviousBakeObjectValid) + { + bPreviousBakeObjectValid = PackageParams.MatchesPackagePathNameExcludingBakeCounter(InPreviousBakeFoliageType); + if (bPreviousBakeObjectValid) + { + PackageParams.GetBakeCounterFromBakedAsset(InPreviousBakeFoliageType, BakeCounter); + UFoliageType_InstancedStaticMesh const* const PreviousBakeMeshFoliageType = Cast(InPreviousBakeFoliageType); + if (IsValid(PreviousBakeMeshFoliageType)) + PreviousBakeMaterials = PreviousBakeMeshFoliageType->OverrideMaterials; + } + } + + FString CreatedPackageName; + UPackage* Package = PackageParams.CreatePackageForObject(CreatedPackageName, BakeCounter); + HOUDINI_CHECK_RETURN(IsValid(Package), nullptr); + + BakedObjectData.BakeStats.NotifyPackageCreated(1); + BakedObjectData.PackagesToSave.Add(Package); + + // We need to be sure the package has been fully loaded before calling DuplicateObject + if (!Package->IsFullyLoaded()) + { + FlushAsyncLoading(); + if (!Package->GetOuter()) + { + Package->FullyLoad(); + } + else + { + Package->GetOutermost()->FullyLoad(); + } + } + + // Duplicate the foliage type. + + UFoliageType* DuplicatedFoliageType = nullptr; + UFoliageType* ExistingFoliageType = FindObject(Package, *CreatedPackageName); + bool bFoundExisting= false; + if (IsValid(ExistingFoliageType)) + { + bFoundExisting = true; + DuplicatedFoliageType = DuplicateObject(InFoliageType, Package, *CreatedPackageName); + BakedObjectData.BakeStats.NotifyObjectsReplaced(UFoliageType::StaticClass()->GetName(), 1); + BakedObjectData.BakeStats.NotifyPackageCreated(1); + } + else + { + DuplicatedFoliageType = DuplicateObject(InFoliageType, Package, *CreatedPackageName); + BakedObjectData.BakeStats.NotifyObjectsUpdated(UFoliageType::StaticClass()->GetName(), 1); + BakedObjectData.BakeStats.NotifyPackageUpdated(2); + } + + if (!IsValid(DuplicatedFoliageType)) + return nullptr; + + InOutAlreadyBakedFoliageTypes.Add(InFoliageType, DuplicatedFoliageType); + + // Add meta information. + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage(Package, DuplicatedFoliageType, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage(Package, DuplicatedFoliageType, HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *CreatedPackageName); + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage(Package, DuplicatedFoliageType, HAPI_UNREAL_PACKAGE_META_BAKED_OBJECT, TEXT("true")); + + // See if we need to duplicate materials and textures. + UFoliageType_InstancedStaticMesh* const DuplicatedMeshFoliageType = Cast(DuplicatedFoliageType); + const bool bIsMeshFoliageType = IsValid(DuplicatedMeshFoliageType); + TArray DuplicatedMaterials; + TArray Materials; + if (bIsMeshFoliageType) + Materials = DuplicatedMeshFoliageType->OverrideMaterials; + for (int32 MaterialIdx = 0; MaterialIdx < Materials.Num(); ++MaterialIdx) + { + UMaterialInterface* const MaterialInterface = Materials[MaterialIdx]; + + // Only duplicate the material if it is temporary + if (IsValid(MaterialInterface) && IsObjectTemporary(MaterialInterface, EHoudiniOutputType::Invalid, InParentOutputs, InTemporaryCookFolder, PackageParams.ComponentGUID)) + { + UPackage * MaterialPackage = Cast(MaterialInterface->GetOuter()); + if (IsValid(MaterialPackage)) + { + FString MaterialName; + if (FHoudiniEngineBakeUtils::GetHoudiniGeneratedNameFromMetaInformation( + Package, DuplicatedFoliageType, MaterialName)) + { + MaterialName = MaterialName + "_Material" + FString::FromInt(MaterialIdx + 1); + + // We only deal with materials. + if (!MaterialInterface->IsA(UMaterial::StaticClass()) && !MaterialInterface->IsA(UMaterialInstance::StaticClass())) + { + continue; + } + + UMaterialInterface * Material = MaterialInterface; + + if (IsValid(Material)) + { + // Look for a previous bake material at this index + UMaterialInterface* PreviousBakeMaterial = nullptr; + if (bPreviousBakeObjectValid && PreviousBakeMaterials.IsValidIndex(MaterialIdx)) + { + PreviousBakeMaterial = PreviousBakeMaterials[MaterialIdx]; + } + // Duplicate material resource. + UMaterialInterface * DuplicatedMaterial = FHoudiniEngineBakeUtils::DuplicateMaterialAndCreatePackage( + Material, PreviousBakeMaterial, MaterialName, PackageParams, BakedObjectData, InOutAlreadyBakedMaterialsMap); + + if (!IsValid(DuplicatedMaterial)) + continue; + + // Store duplicated material. + DuplicatedMaterials.Add(DuplicatedMaterial); + continue; + } + } + } + } + + // We can simply reuse the source material + DuplicatedMaterials.Add(Materials[MaterialIdx]); + } + + // Assign duplicated materials. + if (bIsMeshFoliageType) + DuplicatedMeshFoliageType->OverrideMaterials = DuplicatedMaterials; + + if (!bFoundExisting) + FAssetRegistryModule::AssetCreated(DuplicatedFoliageType); + + DuplicatedFoliageType->MarkPackageDirty(); + + return DuplicatedFoliageType; +} + + +/* +UStaticMesh* +FHoudiniEngineBakeUtils::DuplicateStaticMesh(UStaticMesh* SourceStaticMesh, UObject* Outer, const FName Name) +{ + // + // Copied from FDatasmithImporterUtils::DuplicateStaticMesh + // + + // Since static mesh can be quite heavy, remove source models for cloning to reduce useless work. + // Will be reinserted on the new duplicated asset or restored on the SourceStaticMesh if bIgnoreBulkData is true. + TArray SourceModels = SourceStaticMesh->MoveSourceModels(); + FStaticMeshSourceModel HiResSourceModel = SourceStaticMesh->MoveHiResSourceModel(); + + // Temporary flag to skip Postload during DuplicateObject + SourceStaticMesh->SetFlags(RF_ArchetypeObject); + + // Duplicate is used only to move our object from its temporary package into its final package replacing any asset + // already at that location. This function also takes care of fixing internal dependencies among the object's children. + // Since Duplicate has some rather heavy consequence, like calling PostLoad and doing all kind of stuff on an object + // that is not even fully initialized yet, we might want to find an alternative way of moving our objects in future + // releases but keep it for the current release cycle. + UStaticMesh* DuplicateMesh = ::DuplicateObject(SourceStaticMesh, Outer, Name); + + // Get rid of our temporary flag + SourceStaticMesh->ClearFlags(RF_ArchetypeObject); + DuplicateMesh->ClearFlags(RF_ArchetypeObject); + DuplicateMesh->GetHiResSourceModel().CreateSubObjects(DuplicateMesh); + + // The source mesh is stripped from it's source model, it is not buildable anymore. + // -> MarkPendingKill to avoid use-after-move crash in the StaticMesh::Build() + SourceStaticMesh->MarkAsGarbage(); + + // Apply source models to the duplicated mesh + DuplicateMesh->SetSourceModels(MoveTemp(SourceModels)); + DuplicateMesh->SetHiResSourceModel(MoveTemp(HiResSourceModel)); + + return DuplicateMesh; +} +*/ + + +UStaticMesh * +FHoudiniEngineBakeUtils::DuplicateStaticMeshAndCreatePackageIfNeeded( + UStaticMesh * InStaticMesh, + UStaticMesh * InPreviousBakeStaticMesh, + const FHoudiniPackageParams &PackageParams, + const TArray& InParentOutputs, + const TArray& BakedResults, + const FString& InTemporaryCookFolder, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap) +{ + if (!IsValid(InStaticMesh)) + return nullptr; + + const bool bIsTemporaryStaticMesh = IsObjectTemporary(InStaticMesh, EHoudiniOutputType::Mesh, InParentOutputs, InTemporaryCookFolder, PackageParams.ComponentGUID); + if (!bIsTemporaryStaticMesh) + { + // The Static Mesh is not a temporary one/already baked, we can simply reuse it + // instead of duplicating it + return InStaticMesh; + } + + UStaticMesh** AlreadyBakedSM = InOutAlreadyBakedStaticMeshMap.Find(InStaticMesh); + if (AlreadyBakedSM && IsValid(*AlreadyBakedSM)) + return *AlreadyBakedSM; + + // Look for InStaticMesh as the SourceObject in BakedResults (it could have already been baked along with + // a previous output: instancers etc) + for (const FHoudiniEngineBakedActor& BakeResult : BakedResults) + { + if (BakeResult.SourceObject == InStaticMesh && IsValid(BakeResult.BakedObject) + && BakeResult.BakedObject->IsA(InStaticMesh->GetClass())) + { + // We have found a bake result where InStaticMesh was the source object and we have a valid BakedObject + // of a compatible class + return Cast(BakeResult.BakedObject); + } + } + + // InStaticMesh is temporary and we didn't find a baked version of it in our current bake output, we need to bake it + + // If we have a previously baked static mesh, get the bake counter from it so that both replace and increment + // is consistent with the bake counter + int32 BakeCounter = 0; + bool bPreviousBakeStaticMeshValid = IsValid(InPreviousBakeStaticMesh); + TArray PreviousBakeMaterials; + if (bPreviousBakeStaticMeshValid) + { + bPreviousBakeStaticMeshValid = PackageParams.MatchesPackagePathNameExcludingBakeCounter(InPreviousBakeStaticMesh); + if (bPreviousBakeStaticMeshValid) + { + PackageParams.GetBakeCounterFromBakedAsset(InPreviousBakeStaticMesh, BakeCounter); + PreviousBakeMaterials = InPreviousBakeStaticMesh->GetStaticMaterials(); + } + } + FString CreatedPackageName; + UPackage* MeshPackage = PackageParams.CreatePackageForObject(CreatedPackageName, BakeCounter); + if (!IsValid(MeshPackage)) + return nullptr; + BakedObjectData.BakeStats.NotifyPackageCreated(1); + BakedObjectData.PackagesToSave.Add(MeshPackage); + + // We need to be sure the package has been fully loaded before calling DuplicateObject + if (!MeshPackage->IsFullyLoaded()) + { + FlushAsyncLoading(); + if (!MeshPackage->GetOuter()) + { + MeshPackage->FullyLoad(); + } + else + { + MeshPackage->GetOutermost()->FullyLoad(); + } + } + + FString ObjectName = PackageParams.ObjectName; + // If the a UStaticMesh with that name already exists then detach it from all of its components before replacing + // it so that its render resources can be safely replaced/updated, and then reattach it + UStaticMesh * DuplicatedStaticMesh = nullptr; + UStaticMesh* ExistingMesh = FindObject(MeshPackage, *CreatedPackageName); + bool bFoundExistingMesh = false; + if (IsValid(ExistingMesh)) + { + FStaticMeshComponentRecreateRenderStateContext SMRecreateContext(ExistingMesh); + DuplicatedStaticMesh = DuplicateObject(InStaticMesh, MeshPackage, *CreatedPackageName); + //DuplicatedStaticMesh = FHoudiniEngineBakeUtils::DuplicateStaticMesh(InStaticMesh, MeshPackage, *CreatedPackageName); + bFoundExistingMesh = true; + BakedObjectData.BakeStats.NotifyObjectsReplaced(UStaticMesh::StaticClass()->GetName(), 1); + } + else + { + DuplicatedStaticMesh = DuplicateObject(InStaticMesh, MeshPackage, *CreatedPackageName); + //DuplicatedStaticMesh = FHoudiniEngineBakeUtils::DuplicateStaticMesh(InStaticMesh, MeshPackage, *CreatedPackageName); + BakedObjectData.BakeStats.NotifyObjectsUpdated(UStaticMesh::StaticClass()->GetName(), 1); + } + + if (!IsValid(DuplicatedStaticMesh)) + return nullptr; + + InOutAlreadyBakedStaticMeshMap.Add(InStaticMesh, DuplicatedStaticMesh); + + // Add meta information. + // Houdini Generated + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + MeshPackage, DuplicatedStaticMesh, + HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + // Houdini Generated Name + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + MeshPackage, DuplicatedStaticMesh, + HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *CreatedPackageName); + // Baked object! this is not temporary anymore + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + MeshPackage, DuplicatedStaticMesh, + HAPI_UNREAL_PACKAGE_META_BAKED_OBJECT, TEXT("true")); + + // See if we need to duplicate materials and textures. + TArrayDuplicatedMaterials; + TArray Materials; + if(InStaticMesh->GetStaticMaterials().Num() > 0) + Materials = InStaticMesh->GetStaticMaterials(); + + for (int32 MaterialIdx = 0; MaterialIdx < Materials.Num(); ++MaterialIdx) + { + UMaterialInterface* MaterialInterface = Materials[MaterialIdx].MaterialInterface; + if (!IsValid(MaterialInterface)) + continue; + + // Only duplicate the material if it is temporary + if (IsObjectTemporary(MaterialInterface, EHoudiniOutputType::Invalid, InParentOutputs, InTemporaryCookFolder, PackageParams.ComponentGUID)) + { + UPackage * MaterialPackage = Cast(MaterialInterface->GetOuter()); + if (IsValid(MaterialPackage)) + { + FString MaterialName; + if (FHoudiniEngineBakeUtils::GetHoudiniGeneratedNameFromMetaInformation( + MeshPackage, DuplicatedStaticMesh, MaterialName)) + { + MaterialName = MaterialName + "_Material" + FString::FromInt(MaterialIdx + 1); + + // We only deal with materials. + if (!MaterialInterface->IsA(UMaterial::StaticClass()) && !MaterialInterface->IsA(UMaterialInstance::StaticClass())) + { + continue; + } + + UMaterialInterface * Material = MaterialInterface; + + if (IsValid(Material)) + { + // Look for a previous bake material at this index + UMaterialInterface* PreviousBakeMaterial = nullptr; + if (bPreviousBakeStaticMeshValid && PreviousBakeMaterials.IsValidIndex(MaterialIdx)) + { + PreviousBakeMaterial = Cast(PreviousBakeMaterials[MaterialIdx].MaterialInterface); + } + // Duplicate material resource. + UMaterialInterface * DuplicatedMaterial = FHoudiniEngineBakeUtils::DuplicateMaterialAndCreatePackage( + Material, PreviousBakeMaterial, MaterialName, PackageParams, BakedObjectData, InOutAlreadyBakedMaterialsMap); + + if (!IsValid(DuplicatedMaterial)) + continue; + + // Store duplicated material. + FStaticMaterial DupeStaticMaterial = Materials[MaterialIdx]; + DupeStaticMaterial.MaterialInterface = DuplicatedMaterial; + DuplicatedMaterials.Add(DupeStaticMaterial); + continue; + } + } + } + } + + // We can simply reuse the source material + DuplicatedMaterials.Add(Materials[MaterialIdx]); + } + + // Assign duplicated materials. + DuplicatedStaticMesh->SetStaticMaterials(DuplicatedMaterials); + + // Check if the complex collision mesh of the SM is a temporary SM, if so try to get its baked version + if (IsValid(DuplicatedStaticMesh->ComplexCollisionMesh) && + IsObjectTemporary(DuplicatedStaticMesh->ComplexCollisionMesh, EHoudiniOutputType::Mesh, InParentOutputs, InTemporaryCookFolder)) + { + UStaticMesh** BakedComplexCollisionMesh = InOutAlreadyBakedStaticMeshMap.Find(DuplicatedStaticMesh->ComplexCollisionMesh); + if (BakedComplexCollisionMesh && IsValid(*BakedComplexCollisionMesh)) + { + DuplicatedStaticMesh->ComplexCollisionMesh = *BakedComplexCollisionMesh; + } + } + + // Notify registry that we have created a new duplicate mesh. + if (!bFoundExistingMesh) + FAssetRegistryModule::AssetCreated(DuplicatedStaticMesh); + + // Dirty the static mesh package. + DuplicatedStaticMesh->MarkPackageDirty(); + + return DuplicatedStaticMesh; +} + +USkeletalMesh* +FHoudiniEngineBakeUtils::DuplicateSkeletalMeshAndCreatePackageIfNeeded( + USkeletalMesh* InSkeletalMesh, + USkeletalMesh* InPreviousBakeSkeletalMesh, + const FHoudiniPackageParams& PackageParams, + const TArray& InParentOutputs, + const TArray& InCurrentBakedActors, + const FString& InTemporaryCookFolder, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedSkeletalMeshMap, + TMap& InOutAlreadyBakedMaterialsMap) +{ + if (!IsValid(InSkeletalMesh)) + return nullptr; + + const bool bIsTemporarySkeletalMesh = IsObjectTemporary(InSkeletalMesh, EHoudiniOutputType::Mesh, InParentOutputs, InTemporaryCookFolder, PackageParams.ComponentGUID); + if (!bIsTemporarySkeletalMesh) + { + // The Skeletal Mesh is not a temporary one/already baked, we can simply reuse it + // instead of duplicating it + return InSkeletalMesh; + } + + USkeletalMesh** AlreadyBakedSK = InOutAlreadyBakedSkeletalMeshMap.Find(InSkeletalMesh); + if (AlreadyBakedSK && IsValid(*AlreadyBakedSK)) + return *AlreadyBakedSK; + + // Look for InSkeletalMesh as the SourceObject in InCurrentBakedActors (it could have already been baked along with + // a previous output: instancers etc) + for (const FHoudiniEngineBakedActor& BakedActor : InCurrentBakedActors) + { + if (BakedActor.SourceObject == InSkeletalMesh && IsValid(BakedActor.BakedObject) + && BakedActor.BakedObject->IsA(InSkeletalMesh->GetClass())) + { + // We have found a bake result where InStaticMesh was the source object and we have a valid BakedObject + // of a compatible class + return Cast(BakedActor.BakedObject); + } + } + + // InSkeletalMesh is temporary and we didn't find a baked version of it in our current bake output, we need to bake it + + // If we have a previously baked skeletal mesh, get the bake counter from it so that both replace and increment + // is consistent with the bake counter + int32 BakeCounter = 0; + bool bPreviousBakeStaticMeshValid = IsValid(InPreviousBakeSkeletalMesh); + TArray PreviousBakeMaterials; + if (bPreviousBakeStaticMeshValid) + { + bPreviousBakeStaticMeshValid = PackageParams.MatchesPackagePathNameExcludingBakeCounter(InPreviousBakeSkeletalMesh); + if (bPreviousBakeStaticMeshValid) + { + PackageParams.GetBakeCounterFromBakedAsset(InPreviousBakeSkeletalMesh, BakeCounter); + PreviousBakeMaterials = InPreviousBakeSkeletalMesh->GetMaterials(); + } + } + FString CreatedPackageName; + UPackage* MeshPackage = PackageParams.CreatePackageForObject(CreatedPackageName, BakeCounter); + if (!IsValid(MeshPackage)) + return nullptr; + BakedObjectData.BakeStats.NotifyPackageCreated(1); + BakedObjectData.PackagesToSave.Add(MeshPackage); + + // We need to be sure the package has been fully loaded before calling DuplicateObject + if (!MeshPackage->IsFullyLoaded()) + { + FlushAsyncLoading(); + if (!MeshPackage->GetOuter()) + { + MeshPackage->FullyLoad(); + } + else + { + MeshPackage->GetOutermost()->FullyLoad(); + } + } + + // If the a USkeletalMesh with that name already exists then detach it from all of its components before replacing + // it so that its render resources can be safely replaced/updated, and then reattach it + USkeletalMesh* DuplicatedSkeletalMesh = nullptr; + USkeletalMesh* ExistingMesh = FindObject(MeshPackage, *CreatedPackageName); + bool bFoundExistingMesh = false; + if (IsValid(ExistingMesh)) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + //FSkinnedMeshComponentRecreateRenderStateContext SMRecreateContext(ExistingMesh); +#else + FSkinnedMeshComponentRecreateRenderStateContext SMRecreateContext(ExistingMesh); +#endif + DuplicatedSkeletalMesh = DuplicateObject(InSkeletalMesh, MeshPackage, *CreatedPackageName); + bFoundExistingMesh = true; + BakedObjectData.BakeStats.NotifyObjectsReplaced(USkeletalMesh::StaticClass()->GetName(), 1); + } + else + { + DuplicatedSkeletalMesh = DuplicateObject(InSkeletalMesh, MeshPackage, *CreatedPackageName); + BakedObjectData.BakeStats.NotifyObjectsUpdated(USkeletalMesh::StaticClass()->GetName(), 1); + } + + if (!IsValid(DuplicatedSkeletalMesh)) + return nullptr; + + InOutAlreadyBakedSkeletalMeshMap.Add(InSkeletalMesh, DuplicatedSkeletalMesh); + + // Add meta information. + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + MeshPackage, DuplicatedSkeletalMesh, + HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + MeshPackage, DuplicatedSkeletalMesh, + HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *CreatedPackageName); + // Baked object! this is not temporary anymore + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + MeshPackage, DuplicatedSkeletalMesh, + HAPI_UNREAL_PACKAGE_META_BAKED_OBJECT, TEXT("true")); + + // See if we need to duplicate materials and textures. + TArrayDuplicatedMaterials; + TArray& Materials = DuplicatedSkeletalMesh->GetMaterials(); + for (int32 MaterialIdx = 0; MaterialIdx < Materials.Num(); ++MaterialIdx) + { + UMaterialInterface* MaterialInterface = Materials[MaterialIdx].MaterialInterface; + if (!IsValid(MaterialInterface)) + continue; + + // Only duplicate the material if it is temporary + if (IsObjectTemporary(MaterialInterface, EHoudiniOutputType::Invalid, InParentOutputs, InTemporaryCookFolder, PackageParams.ComponentGUID)) + { + UPackage* MaterialPackage = Cast(MaterialInterface->GetOuter()); + if (IsValid(MaterialPackage)) + { + FString MaterialName; + if (FHoudiniEngineBakeUtils::GetHoudiniGeneratedNameFromMetaInformation( + MeshPackage, DuplicatedSkeletalMesh, MaterialName)) + { + MaterialName = MaterialName + "_Material" + FString::FromInt(MaterialIdx + 1); + + // We only deal with materials. + if (!MaterialInterface->IsA(UMaterial::StaticClass()) && !MaterialInterface->IsA(UMaterialInstance::StaticClass())) + { + continue; + } + + UMaterialInterface* Material = MaterialInterface; + + if (IsValid(Material)) + { + // Look for a previous bake material at this index + UMaterialInterface* PreviousBakeMaterial = nullptr; + if (bPreviousBakeStaticMeshValid && PreviousBakeMaterials.IsValidIndex(MaterialIdx)) + { + PreviousBakeMaterial = Cast(PreviousBakeMaterials[MaterialIdx].MaterialInterface); + } + // Duplicate material resource. + UMaterialInterface* DuplicatedMaterial = FHoudiniEngineBakeUtils::DuplicateMaterialAndCreatePackage( + Material, PreviousBakeMaterial, MaterialName, PackageParams, BakedObjectData, InOutAlreadyBakedMaterialsMap); + + if (!IsValid(DuplicatedMaterial)) + continue; + + // Store duplicated material. + FSkeletalMaterial DupeStaticMaterial = Materials[MaterialIdx]; + DupeStaticMaterial.MaterialInterface = DuplicatedMaterial; + DuplicatedMaterials.Add(DupeStaticMaterial); + continue; + } + } + } + } + + // We can simply reuse the source material + DuplicatedMaterials.Add(Materials[MaterialIdx]); + } + + // Assign duplicated materials. + DuplicatedSkeletalMesh->SetMaterials(DuplicatedMaterials); + + // Notify registry that we have created a new duplicate mesh. + if (!bFoundExistingMesh) + FAssetRegistryModule::AssetCreated(DuplicatedSkeletalMesh); + + // Dirty the static mesh package. + DuplicatedSkeletalMesh->MarkPackageDirty(); + + return DuplicatedSkeletalMesh; +} + + +USkeleton* FHoudiniEngineBakeUtils::DuplicateSkeletonAndCreatePackageIfNeeded( + USkeleton* InSkeleton, + USkeleton const* InPreviousBakeSkeleton, + const FHoudiniPackageParams& PackageParams, + const TArray& InParentOutputs, + const TArray& InCurrentBakedActors, + const FString& InTemporaryCookFolder, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedSkeletonMap) +{ + if (!IsValid(InSkeleton)) + return nullptr; + + const bool bIsTemporarySkeleton = IsObjectTemporary(InSkeleton, EHoudiniOutputType::Mesh, InParentOutputs, InTemporaryCookFolder, PackageParams.ComponentGUID); + if (!bIsTemporarySkeleton) + { + // The Skeletal Mesh is not a temporary one/already baked, we can simply reuse it + // instead of duplicating it + return InSkeleton; + } + + USkeleton** AlreadyBakedSK = InOutAlreadyBakedSkeletonMap.Find(InSkeleton); + if (AlreadyBakedSK && IsValid(*AlreadyBakedSK)) + return *AlreadyBakedSK; + + // // Look for InSkeleton as the SourceObject in InCurrentBakedActors (it could have already been baked along with + // // a previous output: instancers etc) + // for (const FHoudiniEngineBakedActor& BakedActor : InCurrentBakedActors) + // { + // if (BakedActor.SourceObject == InSkeleton && IsValid(BakedActor.BakedObject) + // && BakedActor.BakedObject->IsA(InSkeleton->GetClass())) + // { + // // We have found a bake result where InStaticMesh was the source object and we have a valid BakedObject + // // of a compatible class + // return Cast(BakedActor.BakedObject); + // } + // } + + // InSkeleton is temporary and we didn't find a baked version of it in our current bake output, we need to bake it + + // If we have a previously baked skeletal mesh, get the bake counter from it so that both replace and increment + // is consistent with the bake counter + int32 BakeCounter = 0; + bool bPreviousBakeSkeletonValid = IsValid(InPreviousBakeSkeleton); + if (bPreviousBakeSkeletonValid) + { + bPreviousBakeSkeletonValid = PackageParams.MatchesPackagePathNameExcludingBakeCounter(InPreviousBakeSkeleton); + if (bPreviousBakeSkeletonValid) + { + PackageParams.GetBakeCounterFromBakedAsset(InPreviousBakeSkeleton, BakeCounter); + } + } + FString CreatedPackageName; + UPackage* SkeletonPackage = PackageParams.CreatePackageForObject(CreatedPackageName, BakeCounter); + if (!IsValid(SkeletonPackage)) + return nullptr; + BakedObjectData.BakeStats.NotifyPackageCreated(1); + BakedObjectData.PackagesToSave.Add(SkeletonPackage); + + // We need to be sure the package has been fully loaded before calling DuplicateObject + if (!SkeletonPackage->IsFullyLoaded()) + { + FlushAsyncLoading(); + if (!SkeletonPackage->GetOuter()) + { + SkeletonPackage->FullyLoad(); + } + else + { + SkeletonPackage->GetOutermost()->FullyLoad(); + } + } + + // If the a USkeleton with that name already exists then detach it from all of its components before replacing + // it so that its render resources can be safely replaced/updated, and then reattach it + USkeleton* DuplicatedSkeleton = nullptr; + USkeleton* ExistingSkeleton = FindObject(SkeletonPackage, *CreatedPackageName); + bool bFoundExistingSkeleton = false; + if (IsValid(ExistingSkeleton)) + { + DuplicatedSkeleton = DuplicateObject(InSkeleton, SkeletonPackage, *CreatedPackageName); + bFoundExistingSkeleton = true; + BakedObjectData.BakeStats.NotifyObjectsReplaced(USkeleton::StaticClass()->GetName(), 1); + } + else + { + DuplicatedSkeleton = DuplicateObject(InSkeleton, SkeletonPackage, *CreatedPackageName); + BakedObjectData.BakeStats.NotifyObjectsUpdated(USkeleton::StaticClass()->GetName(), 1); + } + + if (!IsValid(DuplicatedSkeleton)) + return nullptr; + + InOutAlreadyBakedSkeletonMap.Add(InSkeleton, DuplicatedSkeleton); + + // Add meta information. + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + SkeletonPackage, DuplicatedSkeleton, + HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + SkeletonPackage, DuplicatedSkeleton, + HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *CreatedPackageName); + // Baked object! this is not temporary anymore + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + SkeletonPackage, DuplicatedSkeleton, + HAPI_UNREAL_PACKAGE_META_BAKED_OBJECT, TEXT("true")); + + // Notify registry that we have created a new duplicate skeleton + if (!bFoundExistingSkeleton) + FAssetRegistryModule::AssetCreated(DuplicatedSkeleton); + + // Dirty the skeleton package. + DuplicatedSkeleton->MarkPackageDirty(); + + return DuplicatedSkeleton; +} + + +UPhysicsAsset* FHoudiniEngineBakeUtils::DuplicatePhysicsAssetAndCreatePackageIfNeeded( + UPhysicsAsset* InPhysicsAsset, + UPhysicsAsset * InPreviousBakePhysicsAsset, + const FHoudiniPackageParams& PackageParams, + const TArray& InParentOutputs, + const TArray& InCurrentBakedActors, + const FString& InTemporaryCookFolder, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedPhysicsAssetMap) +{ + if (!IsValid(InPhysicsAsset)) + return nullptr; + + // Don't bake a new object if this isn't temporary + bool bIsTemporary = IsObjectTemporary(InPhysicsAsset, EHoudiniOutputType::Mesh, InParentOutputs, InTemporaryCookFolder, PackageParams.ComponentGUID); + if (!bIsTemporary) + { + return InPhysicsAsset; + } + + // Was it already baked? + UPhysicsAsset** AlreadyBakedPhysicsAsset = InOutAlreadyBakedPhysicsAssetMap.Find(InPhysicsAsset); + if (AlreadyBakedPhysicsAsset && IsValid(*AlreadyBakedPhysicsAsset)) + return *AlreadyBakedPhysicsAsset; + + // If we have a previously baked object, get the bake counter from it so that both replace and increment + // is consistent with the bake counter + int32 BakeCounter = 0; + bool bPreviousBakeValid = IsValid(InPreviousBakePhysicsAsset); + if (bPreviousBakeValid) + { + bPreviousBakeValid = PackageParams.MatchesPackagePathNameExcludingBakeCounter(InPreviousBakePhysicsAsset); + if (bPreviousBakeValid) + { + PackageParams.GetBakeCounterFromBakedAsset(InPreviousBakePhysicsAsset, BakeCounter); + } + } + FString CreatedPackageName; + UPackage* PhysicsAssetPackage = PackageParams.CreatePackageForObject(CreatedPackageName, BakeCounter); + if (!IsValid(PhysicsAssetPackage)) + return nullptr; + BakedObjectData.BakeStats.NotifyPackageCreated(1); + BakedObjectData.PackagesToSave.Add(PhysicsAssetPackage); + + // We need to be sure the package has been fully loaded before calling DuplicateObject + if (!PhysicsAssetPackage->IsFullyLoaded()) + { + FlushAsyncLoading(); + if (!PhysicsAssetPackage->GetOuter()) + { + PhysicsAssetPackage->FullyLoad(); + } + else + { + PhysicsAssetPackage->GetOutermost()->FullyLoad(); + } + } + + // If an object with that name already exists then detach it from all of its components before replacing + // it so that its render resources can be safely replaced/updated, and then reattach it + UPhysicsAsset* DuplicatedPhysicsAsset = nullptr; + UPhysicsAsset* ExistingSkeleton = FindObject(PhysicsAssetPackage, *CreatedPackageName); + bool bFoundExistingPhysicsAsset = false; + if (IsValid(ExistingSkeleton)) + { + DuplicatedPhysicsAsset = DuplicateObject(InPhysicsAsset, PhysicsAssetPackage, *CreatedPackageName); + bFoundExistingPhysicsAsset = true; + BakedObjectData.BakeStats.NotifyObjectsReplaced(USkeleton::StaticClass()->GetName(), 1); + } + else + { + DuplicatedPhysicsAsset = DuplicateObject(InPhysicsAsset, PhysicsAssetPackage, *CreatedPackageName); + BakedObjectData.BakeStats.NotifyObjectsUpdated(UPhysicsAsset::StaticClass()->GetName(), 1); + } + + if (!IsValid(DuplicatedPhysicsAsset)) + return nullptr; + + InOutAlreadyBakedPhysicsAssetMap.Add(InPhysicsAsset, DuplicatedPhysicsAsset); + + // Add meta information. + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + PhysicsAssetPackage, DuplicatedPhysicsAsset, + HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + PhysicsAssetPackage, DuplicatedPhysicsAsset, + HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *CreatedPackageName); + // Baked object! this is not temporary anymore + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + PhysicsAssetPackage, DuplicatedPhysicsAsset, + HAPI_UNREAL_PACKAGE_META_BAKED_OBJECT, TEXT("true")); + + // Notify registry that we have created a new duplicate skeleton + if (!bFoundExistingPhysicsAsset) + FAssetRegistryModule::AssetCreated(DuplicatedPhysicsAsset); + + // Dirty the skeleton package. + DuplicatedPhysicsAsset->MarkPackageDirty(); + + return DuplicatedPhysicsAsset; +} + +UGeometryCollection* FHoudiniEngineBakeUtils::DuplicateGeometryCollectionAndCreatePackageIfNeeded( + UGeometryCollection* InGeometryCollection, + UGeometryCollection* InPreviousBakeGeometryCollection, + const FHoudiniPackageParams& InPackageParams, + const TArray& InParentOutputs, + const TArray& InCurrentBakedActors, + const FString& InTemporaryCookFolder, + const TMap& InOldToNewStaticMesh, + const TMap& InOldToNewMaterialMap, + FHoudiniBakedObjectData& BakedObjectData) +{ + if (!IsValid(InGeometryCollection)) + return nullptr; + + FHoudiniPackageParams PackageParams = InPackageParams; + PackageParams.ObjectName = InGeometryCollection->GetName(); + + const bool bIsTemporaryStaticMesh = IsObjectTemporary(InGeometryCollection, EHoudiniOutputType::GeometryCollection, InParentOutputs, InTemporaryCookFolder, PackageParams.ComponentGUID); + if (!bIsTemporaryStaticMesh) + { + // The output is not a temporary one/already baked, we can simply reuse it + // instead of duplicating it + return InGeometryCollection; + } + + // Look for InGeometryCollection as the SourceObject in InCurrentBakedActors (it could have already been baked along with + // a previous output: instancers etc) + for (const FHoudiniEngineBakedActor& BakedActor : InCurrentBakedActors) + { + if (BakedActor.SourceObject == InGeometryCollection && IsValid(BakedActor.BakedObject) + && BakedActor.BakedObject->IsA(InGeometryCollection->GetClass())) + { + // We have found a bake result where InStaticMesh was the source object and we have a valid BakedObject + // of a compatible class + return Cast(BakedActor.BakedObject); + } + } + + // InGeometryCollection is temporary and we didn't find a baked version of it in our current bake output, we need to bake it + + // If we have a previously baked static mesh, get the bake counter from it so that both replace and increment + // is consistent with the bake counter + + + int32 BakeCounter = 0; + bool bPreviousBakeStaticMeshValid = IsValid(InPreviousBakeGeometryCollection); + TArray PreviousBakeMaterials; + if (bPreviousBakeStaticMeshValid) + { + bPreviousBakeStaticMeshValid = PackageParams.MatchesPackagePathNameExcludingBakeCounter(InPreviousBakeGeometryCollection); + if (bPreviousBakeStaticMeshValid) + { + PackageParams.GetBakeCounterFromBakedAsset(InPreviousBakeGeometryCollection, BakeCounter); + PreviousBakeMaterials = InPreviousBakeGeometryCollection->Materials; + } + } + FString CreatedPackageName; + UPackage* MeshPackage = PackageParams.CreatePackageForObject(CreatedPackageName, BakeCounter); + if (!IsValid(MeshPackage)) + return nullptr; + BakedObjectData.BakeStats.NotifyPackageCreated(1); + BakedObjectData.PackagesToSave.Add(MeshPackage); + + // We need to be sure the package has been fully loaded before calling DuplicateObject + if (!MeshPackage->IsFullyLoaded()) + { + FlushAsyncLoading(); + if (!MeshPackage->GetOuter()) + { + MeshPackage->FullyLoad(); + } + else + { + MeshPackage->GetOutermost()->FullyLoad(); + } + } + + // If the a UGeometryCollection with that name already exists then detach it from all of its components before replacing + // it so that its render resources can be safely replaced/updated, and then reattach it + UGeometryCollection * DuplicatedGeometryCollection = nullptr; + UGeometryCollection * ExistingGeometryCollection = FindObject(MeshPackage, *CreatedPackageName); + bool bFoundExistingObject = false; + if (IsValid(ExistingGeometryCollection)) + { + DuplicatedGeometryCollection = DuplicateObject(InGeometryCollection, MeshPackage, *CreatedPackageName); + bFoundExistingObject = true; + } + else + { + DuplicatedGeometryCollection = DuplicateObject(InGeometryCollection, MeshPackage, *CreatedPackageName); + } + + if (!IsValid(DuplicatedGeometryCollection)) + return nullptr; + + BakedObjectData.BakeStats.NotifyObjectsCreated(DuplicatedGeometryCollection->GetClass()->GetName(), 1); + + // Add meta information. + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + MeshPackage, DuplicatedGeometryCollection, + HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + MeshPackage, DuplicatedGeometryCollection, + HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *CreatedPackageName); + // Baked object! this is not temporary anymore + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + MeshPackage, DuplicatedGeometryCollection, + HAPI_UNREAL_PACKAGE_META_BAKED_OBJECT, TEXT("true")); + + for (FGeometryCollectionSource& Source : DuplicatedGeometryCollection->GeometrySource) + { + if (!InOldToNewStaticMesh.Contains(Source.SourceGeometryObject)) + { + continue; + } + + UStaticMesh * BakedSM = InOldToNewStaticMesh[Source.SourceGeometryObject]; + + Source.SourceGeometryObject = FSoftObjectPath(BakedSM); + Source.SourceMaterial.Empty(); + + for (const auto& Material : BakedSM->GetStaticMaterials()) + { + Source.SourceMaterial.Add(Material.MaterialInterface); + } + } + + + // Duplicate geometry collection materials + for (int32 i = 0; i < DuplicatedGeometryCollection->Materials.Num(); i++) + { + if (!InOldToNewMaterialMap.Contains(DuplicatedGeometryCollection->Materials[i])) + { + continue; + } + + DuplicatedGeometryCollection->Materials[i] = InOldToNewMaterialMap[DuplicatedGeometryCollection->Materials[i]]; + } + + + // Notify registry that we have created a new duplicate mesh. + if (!bFoundExistingObject) + FAssetRegistryModule::AssetCreated(DuplicatedGeometryCollection); + + // Dirty the static mesh package. + DuplicatedGeometryCollection->MarkPackageDirty(); + + return DuplicatedGeometryCollection; +} + +ALandscapeProxy* +FHoudiniEngineBakeUtils::BakeHeightfield( + ALandscapeProxy * InLandscapeProxy, + const FHoudiniPackageParams & PackageParams, + const EHoudiniLandscapeOutputBakeType & LandscapeOutputBakeType, + FHoudiniBakedObjectData& BakedObjectData) +{ + if (!IsValid(InLandscapeProxy)) + return nullptr; + + const FString & BakeFolder = PackageParams.BakeFolder; + const FString & AssetName = PackageParams.HoudiniAssetName; + + switch (LandscapeOutputBakeType) + { + case EHoudiniLandscapeOutputBakeType::Detachment: + { + // Detach the landscape from the Houdini Asset Actor + InLandscapeProxy->DetachFromActor(FDetachmentTransformRules::KeepRelativeTransform); + } + break; + + case EHoudiniLandscapeOutputBakeType::BakeToImage: + { + // Create heightmap image to the bake folder + ULandscapeInfo * InLandscapeInfo = InLandscapeProxy->GetLandscapeInfo(); + if (!IsValid(InLandscapeInfo)) + return nullptr; + + // bake to image must use absoluate path, + // and the file name has a file extension (.png) + FString BakeFolderInFullPath = BakeFolder; + + // Figure absolute path, + if (!BakeFolderInFullPath.EndsWith("/")) + BakeFolderInFullPath += "/"; + + if (BakeFolderInFullPath.StartsWith("/Game")) + BakeFolderInFullPath = BakeFolderInFullPath.Mid(5, BakeFolderInFullPath.Len() - 5); + + if (BakeFolderInFullPath.StartsWith("/")) + BakeFolderInFullPath = BakeFolderInFullPath.Mid(1, BakeFolderInFullPath.Len() - 1); + + FString FullPath = FPaths::ProjectContentDir() + BakeFolderInFullPath + AssetName + "_" + InLandscapeProxy->GetName() + ".png"; + + InLandscapeInfo->ExportHeightmap(FullPath); + + // TODO: + // We should update this to have an asset/package.. + } + break; + + case EHoudiniLandscapeOutputBakeType::BakeToWorld: + { + ULandscapeInfo * InLandscapeInfo = InLandscapeProxy->GetLandscapeInfo(); + if (!IsValid(InLandscapeInfo)) + return nullptr; + + // 0. Get Landscape Data // + + // Extract landscape height data + TArray InLandscapeHeightData; + int32 XSize, YSize; + FVector3d Min, Max; + if (!FUnrealLandscapeTranslator::GetLandscapeData(InLandscapeProxy, InLandscapeHeightData, XSize, YSize, Min, Max)) + return nullptr; + + // Extract landscape Layers data + TArray InLandscapeImportLayerInfos; + for (int32 n = 0; n < InLandscapeInfo->Layers.Num(); ++n) + { + TArray CurrentLayerIntData; + FLinearColor LayerUsageDebugColor; + FString LayerName; + if (!FUnrealLandscapeTranslator::GetLandscapeTargetLayerData(InLandscapeProxy, InLandscapeInfo, n, CurrentLayerIntData, LayerUsageDebugColor, LayerName)) + continue; + + FLandscapeImportLayerInfo CurrentLayerInfo; + CurrentLayerInfo.LayerName = FName(LayerName); + CurrentLayerInfo.LayerInfo = InLandscapeInfo->Layers[n].LayerInfoObj; + CurrentLayerInfo.LayerData = CurrentLayerIntData; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + CurrentLayerInfo.LayerInfo->SetLayerUsageDebugColor(LayerUsageDebugColor, true, EPropertyChangeType::ValueSet); +#else + CurrentLayerInfo.LayerInfo->LayerUsageDebugColor = LayerUsageDebugColor; +#endif + + InLandscapeImportLayerInfos.Add(CurrentLayerInfo); + } + + // 1. Create package // + + FString PackagePath = PackageParams.GetPackagePath(); + FString PackageName = PackageParams.GetPackageName(); + + UPackage *CreatedPackage = nullptr; + FString CreatedPackageName; + + CreatedPackage = PackageParams.CreatePackageForObject(CreatedPackageName); + + if (!CreatedPackage) + return nullptr; + + BakedObjectData.BakeStats.NotifyPackageCreated(1); + + // 2. Create a new world asset with dialog // + UWorldFactory* Factory = NewObject(); + FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + + UObject* Asset = AssetToolsModule.Get().CreateAssetWithDialog( + PackageName, PackagePath, + UWorld::StaticClass(), Factory, FName("ContentBrowserNewAsset")); + + + UWorld* NewWorld = Cast(Asset); + if (!NewWorld) + return nullptr; + + BakedObjectData.BakeStats.NotifyObjectsCreated(NewWorld->GetClass()->GetName(), 1); + NewWorld->SetCurrentLevel(NewWorld->PersistentLevel); + + // 4. Spawn a landscape proxy actor in the created world + ALandscapeStreamingProxy * BakedLandscapeProxy = NewWorld->SpawnActor(); + if (!BakedLandscapeProxy) + return nullptr; + + BakedObjectData.BakeStats.NotifyObjectsCreated(BakedLandscapeProxy->GetClass()->GetName(), 1); + // Create a new GUID + FGuid currentGUID = FGuid::NewGuid(); + BakedLandscapeProxy->SetLandscapeGuid(currentGUID); + + // Deactivate CastStaticShadow on the landscape to avoid "grid shadow" issue + BakedLandscapeProxy->bCastStaticShadow = false; + + + // 5. Import data to the created landscape proxy + TMap> HeightmapDataPerLayers; + TMap> MaterialLayerDataPerLayer; + + HeightmapDataPerLayers.Add(FGuid(), InLandscapeHeightData); + MaterialLayerDataPerLayer.Add(FGuid(), InLandscapeImportLayerInfos); + + ELandscapeImportAlphamapType ImportLayerType = ELandscapeImportAlphamapType::Additive; + + BakedLandscapeProxy->Import( + currentGUID, + 0, 0, XSize-1, YSize-1, + InLandscapeInfo->ComponentNumSubsections, InLandscapeInfo->SubsectionSizeQuads, + HeightmapDataPerLayers, NULL, + MaterialLayerDataPerLayer, + ImportLayerType +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + , MakeArrayView({}) +#endif + ); + + BakedLandscapeProxy->StaticLightingLOD = FMath::DivideAndRoundUp(FMath::CeilLogTwo((XSize * YSize) / (2048 * 2048) + 1), (uint32)2); + + TMap AlreadyBakedMaterialsMap; + + if (IsValid(BakedLandscapeProxy->LandscapeMaterial)) + { + // No need to check the outputs for materials, only check if the material is in the temp folder + //if (IsObjectTemporary(BakedLandscapeProxy->LandscapeMaterial, EHoudiniOutputType::Invalid, InParentOutputs, PackageParams.TempCookFolder)) + if (IsObjectInTempFolder(BakedLandscapeProxy->LandscapeMaterial, PackageParams.TempCookFolder)) + { + UMaterialInterface* DuplicatedMaterial = BakeSingleMaterialToPackage( + BakedLandscapeProxy->LandscapeMaterial, PackageParams, BakedObjectData, AlreadyBakedMaterialsMap); + BakedLandscapeProxy->LandscapeMaterial = DuplicatedMaterial; + } + } + + if (IsValid(BakedLandscapeProxy->LandscapeHoleMaterial)) + { + // No need to check the outputs for materials, only check if the material is in the temp folder + //if (IsObjectTemporary(BakedLandscapeProxy->LandscapeHoleMaterial, EHoudiniOutputType::Invalid, InParentOutputs, PackageParams.TempCookFolder)) + if (IsObjectInTempFolder(BakedLandscapeProxy->LandscapeHoleMaterial, PackageParams.TempCookFolder)) + { + UMaterialInterface* DuplicatedMaterial = BakeSingleMaterialToPackage( + BakedLandscapeProxy->LandscapeHoleMaterial, PackageParams, BakedObjectData, AlreadyBakedMaterialsMap); + BakedLandscapeProxy->LandscapeHoleMaterial = DuplicatedMaterial; + } + } + + // 6. Register all the landscape components, and set landscape actor transform + BakedLandscapeProxy->RegisterAllComponents(); + BakedLandscapeProxy->SetActorTransform(InLandscapeProxy->GetTransform()); + + // 7. Save Package + BakedObjectData.PackagesToSave.Add(CreatedPackage); + FHoudiniEngineBakeUtils::SaveBakedPackages(BakedObjectData.PackagesToSave); + + // Sync the CB to the baked objects + if(GEditor) + { + TArray Objects; + Objects.Add(NewWorld); + GEditor->SyncBrowserToObjects(Objects); + } + } + break; + } + + return InLandscapeProxy; +} + +bool +FHoudiniEngineBakeUtils::BakeCurve( + const UHoudiniCookable* InCookable, + USplineComponent* InSplineComponent, + ULevel* InLevel, + const FHoudiniPackageParams &PackageParams, + const FHoudiniBakeSettings& BakeSettings, + const FName& InActorName, + AActor*& OutActor, + USplineComponent*& OutSplineComponent, + FHoudiniBakedObjectData& BakedObjectData, + FName InOverrideFolderPath, + AActor* InActor, + TSubclassOf BakeActorClass) +{ + if (!IsValid(InActor)) + { + UActorFactory* Factory = nullptr; + if (IsValid(BakeActorClass)) + { + Factory = GEditor->FindActorFactoryForActorClass(BakeActorClass); + if (!Factory) + Factory = GEditor->FindActorFactoryByClass(UActorFactoryClass::StaticClass()); + } + else + { + Factory = GetActorFactory(NAME_None, BakeSettings, BakeActorClass, UActorFactoryEmptyActor::StaticClass()); + } + if (!Factory) + return false; + + OutActor = SpawnBakeActor( + Factory, nullptr, InLevel, BakeSettings, + InSplineComponent->GetComponentTransform(), InCookable->GetComponent(), BakeActorClass); + + if (IsValid(OutActor)) + BakedObjectData.BakeStats.NotifyObjectsCreated(OutActor->GetClass()->GetName(), 1); + } + else + { + OutActor = InActor; + if (IsValid(OutActor)) + BakedObjectData.BakeStats.NotifyObjectsUpdated(OutActor->GetClass()->GetName(), 1); + } + + // Fallback to ObjectName from package params if InActorName is not set + const FName ActorName = InActorName.IsNone() ? FName(PackageParams.ObjectName) : InActorName; + const FString NewNameStr = MakeUniqueObjectNameIfNeeded(InLevel, OutActor->GetClass(), InActorName.ToString(), OutActor); + RenameAndRelabelActor(OutActor, NewNameStr, false); + OutActor->SetFolderPath(InOverrideFolderPath.IsNone() ? FName(PackageParams.HoudiniAssetActorName) : InOverrideFolderPath); + + USplineComponent* DuplicatedSplineComponent = DuplicateObject( + InSplineComponent, + OutActor, + FName(MakeUniqueObjectNameIfNeeded(OutActor, InSplineComponent->GetClass(), PackageParams.ObjectName))); + + if (IsValid(DuplicatedSplineComponent)) + BakedObjectData.BakeStats.NotifyObjectsCreated(DuplicatedSplineComponent->GetClass()->GetName(), 1); + + OutActor->AddInstanceComponent(DuplicatedSplineComponent); + const bool bCreateIfMissing = true; + USceneComponent* RootComponent = GetActorRootComponent(OutActor, bCreateIfMissing); + DuplicatedSplineComponent->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); + + // We duplicated the InSplineComponent, so we don't have to copy all of its properties, but we must set the + // world transform + DuplicatedSplineComponent->SetWorldTransform(InSplineComponent->GetComponentTransform()); + + FAssetRegistryModule::AssetCreated(DuplicatedSplineComponent); + DuplicatedSplineComponent->RegisterComponent(); + + OutSplineComponent = DuplicatedSplineComponent; + return true; +} + +bool +FHoudiniEngineBakeUtils::BakeCurve( + const UHoudiniCookable* InCookable, + const FHoudiniOutputObject& InOutputObject, + FHoudiniBakedOutputObject& InBakedOutputObject, + const FHoudiniPackageParams &PackageParams, + FHoudiniAttributeResolver& InResolver, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + FHoudiniEngineBakedActor& OutBakedActorEntry, + FHoudiniBakedObjectData& BakedObjectData, + AActor* InFallbackActor) +{ + if (InOutputObject.OutputComponents.IsEmpty()) + return false; + + HOUDINI_CHECK_RETURN(InOutputObject.OutputComponents.Num() == 1, false); + + USplineComponent* SplineComponent = Cast(InOutputObject.OutputComponents[0]); + if (!IsValid(SplineComponent)) + return false; + + // By default spawn in the current level unless specified via the unreal_level_path attribute + UWorld* DesiredWorld = SplineComponent ? SplineComponent->GetWorld() : GWorld; + ULevel* DesiredLevel = DesiredWorld ? DesiredWorld->GetCurrentLevel() : GWorld->GetCurrentLevel(); + + bool bHasLevelPathAttribute = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_LEVEL_PATH); + if (bHasLevelPathAttribute) + { + // Get the package path from the unreal_level_apth attribute + FString LevelPackagePath = InResolver.ResolveFullLevelPath(); + + bool bCreatedPackage = false; + if (!FHoudiniEngineBakeUtils::FindOrCreateDesiredLevelFromLevelPath( + LevelPackagePath, + DesiredLevel, + DesiredWorld, + bCreatedPackage)) + { + // TODO: LOG ERROR IF NO LEVEL + return false; + } + + // If we have created a new level, add it to the packages to save + // TODO: ? always add? + if (bCreatedPackage && DesiredLevel) + { + BakedObjectData.BakeStats.NotifyPackageCreated(1); + BakedObjectData.BakeStats.NotifyObjectsCreated(DesiredLevel->GetClass()->GetName(), 1); + // We can now save the package again, and unload it. + BakedObjectData.PackagesToSave.Add(DesiredLevel->GetOutermost()); + } + } + + if(!DesiredLevel) + return false; + + // Try to find the unreal_bake_actor, if specified, or fallback to the default named actor + FName BakeActorName; + AActor* FoundActor = nullptr; + bool bHasBakeActorName = false; + FindUnrealBakeActor(InOutputObject, InBakedOutputObject, InBakedActors, DesiredLevel, *(PackageParams.ObjectName), BakeSettings, InFallbackActor, FoundActor, bHasBakeActorName, BakeActorName); + + // If we are baking in replace mode, remove the previous bake component + if (BakeSettings.bReplaceAssets && !InBakedOutputObject.BakedComponent.IsEmpty()) + { + UActorComponent* PrevComponent = Cast(InBakedOutputObject.GetBakedComponentIfValid()); + if (PrevComponent && PrevComponent->GetOwner() == FoundActor) + { + RemovePreviouslyBakedComponent(PrevComponent); + } + } + + TSubclassOf BakeActorClass = GetBakeActorClassOverride(InOutputObject); + + USplineComponent* NewSplineComponent = nullptr; + const FName OutlinerFolderPath = GetOutlinerFolderPath(InResolver, *(PackageParams.HoudiniAssetActorName)); + if (!BakeCurve( + InCookable, + SplineComponent, + DesiredLevel, + PackageParams, + BakeSettings, + BakeActorName, + FoundActor, + NewSplineComponent, + BakedObjectData, + OutlinerFolderPath, + FoundActor, + BakeActorClass)) + return false; + + InBakedOutputObject.Actor = FSoftObjectPath(FoundActor).ToString(); + InBakedOutputObject.BakedComponent = FSoftObjectPath(NewSplineComponent).ToString(); + + // If we are baking in replace mode, remove previously baked components/instancers + if (BakeSettings.bReplaceAssets && BakeSettings.bReplaceActors) + { + const bool bInDestroyBakedComponent = false; + const bool bInDestroyBakedInstancedActors = true; + const bool bInDestroyBakedInstancedComponents = true; + DestroyPreviousBakeOutput( + InBakedOutputObject, bInDestroyBakedComponent, bInDestroyBakedInstancedActors, bInDestroyBakedInstancedComponents); + } + + FHoudiniEngineBakedActor Result( + FoundActor, + BakeActorName, + OutlinerFolderPath.IsNone() ? FName(PackageParams.HoudiniAssetActorName) : OutlinerFolderPath, + INDEX_NONE, // Output index + FHoudiniOutputObjectIdentifier(), + nullptr, // InBakedObject + nullptr, // InSourceObject + NewSplineComponent, + PackageParams.BakeFolder, + PackageParams); + + OutBakedActorEntry = Result; + + return true; +} + +AActor* +FHoudiniEngineBakeUtils::BakeInputHoudiniCurveToActor( + const UHoudiniCookable* InCookable, + UHoudiniSplineComponent * InHoudiniSplineComponent, + const FHoudiniPackageParams & PackageParams, + const FHoudiniBakeSettings& BakeSettings, + UWorld* WorldToSpawn, + const FTransform & SpawnTransform) +{ + if (!IsValid(InHoudiniSplineComponent)) + return nullptr; + + TArray& DisplayPoints = InHoudiniSplineComponent->DisplayPoints; + if (DisplayPoints.Num() < 2) + return nullptr; + + UWorld* DesiredWorld = InHoudiniSplineComponent ? InHoudiniSplineComponent->GetWorld() : GWorld; + ULevel* DesiredLevel = DesiredWorld ? DesiredWorld->GetCurrentLevel() : GWorld->GetCurrentLevel(); + + TSubclassOf BakeActorClass = nullptr; + UActorFactory* const Factory = GetActorFactory( + NAME_None, BakeSettings, BakeActorClass, UActorFactoryEmptyActor::StaticClass()); + + if (!Factory) + return nullptr; + + // Remove the actor if it exists + for (auto & Actor : DesiredLevel->Actors) + { + if (!Actor) + continue; + + if (Actor->GetActorNameOrLabel() == PackageParams.ObjectName) + { + UWorld* World = Actor->GetWorld(); + if (!World) + World = GWorld; + + Actor->RemoveFromRoot(); + Actor->ConditionalBeginDestroy(); + World->EditorDestroyActor(Actor, true); + + break; + } + } + + if (!InHoudiniSplineComponent || !InCookable) + { + return nullptr; + } + + AActor* NewActor = SpawnBakeActor( + Factory, nullptr, DesiredLevel, BakeSettings, + InHoudiniSplineComponent->GetComponentTransform(), InCookable->GetComponent(), BakeActorClass); + + USplineComponent* BakedUnrealSplineComponent = NewObject(NewActor); + if (!BakedUnrealSplineComponent) + return nullptr; + + // add display points to created unreal spline component + for (int32 n = 0; n < DisplayPoints.Num(); ++n) + { + FVector3d& NextPoint = DisplayPoints[n]; + BakedUnrealSplineComponent->AddSplinePoint(NextPoint, ESplineCoordinateSpace::Local); + // Set the curve point type to be linear, since we are using display points + BakedUnrealSplineComponent->SetSplinePointType(n, ESplinePointType::Linear); + } + NewActor->AddInstanceComponent(BakedUnrealSplineComponent); + + BakedUnrealSplineComponent->AttachToComponent( + NewActor->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform); + + FAssetRegistryModule::AssetCreated(NewActor); + FAssetRegistryModule::AssetCreated(BakedUnrealSplineComponent); + BakedUnrealSplineComponent->RegisterComponent(); + + // The default name will be based on the static mesh package, we would prefer it to be based on the Houdini asset + const FString NewNameStr = MakeUniqueObjectNameIfNeeded(DesiredLevel, Factory->NewActorClass, *(PackageParams.ObjectName), NewActor); + RenameAndRelabelActor(NewActor, NewNameStr, false); + NewActor->SetFolderPath(FName(PackageParams.HoudiniAssetName)); + + return NewActor; +} + +UBlueprint* +FHoudiniEngineBakeUtils::BakeInputHoudiniCurveToBlueprint( + UHoudiniCookable* InCookable, + UHoudiniSplineComponent * InHoudiniSplineComponent, + const FHoudiniPackageParams & PackageParams, + const FHoudiniBakeSettings& BakeSettings, + UWorld* WorldToSpawn, + const FTransform & SpawnTransform) +{ + if (!IsValid(InHoudiniSplineComponent)) + return nullptr; + + FGuid BakeGUID = FGuid::NewGuid(); + + if (!BakeGUID.IsValid()) + BakeGUID = FGuid::NewGuid(); + + // We only want half of generated guid string. + FString BakeGUIDString = BakeGUID.ToString().Left(FHoudiniEngineUtils::PackageGUIDItemNameLength); + + // Generate Blueprint name. + FString BlueprintName = PackageParams.ObjectName + "_BP"; + + // Generate unique package name. + FString PackageName = PackageParams.BakeFolder + "/" + BlueprintName; + PackageName = UPackageTools::SanitizePackageName(PackageName); + + // See if package exists, if it does, we need to regenerate the name. + UPackage * Package = FindPackage(nullptr, *PackageName); + + if (IsValid(Package)) + { + // Package does exist, there's a collision, we need to generate a new name. + BakeGUID.Invalidate(); + } + else + { + // Create actual package. + Package = CreatePackage(*PackageName); + } + + AActor * CreatedHoudiniSplineActor = FHoudiniEngineBakeUtils::BakeInputHoudiniCurveToActor( + InCookable, InHoudiniSplineComponent, PackageParams, BakeSettings, WorldToSpawn, SpawnTransform); + + FHoudiniBakedObjectData BakedObjectData; + + UBlueprint * Blueprint = nullptr; + if (IsValid(CreatedHoudiniSplineActor)) + { + + UObject* Asset = nullptr; + + Asset = StaticFindObjectFast(UObject::StaticClass(), Package, FName(*BlueprintName)); + if (!Asset) + { + UBlueprintFactory* Factory = NewObject(); + + FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + + Asset = AssetToolsModule.Get().CreateAsset( + BlueprintName, PackageParams.BakeFolder, + UBlueprint::StaticClass(), Factory, FName("ContentBrowserNewAsset")); + } + + TArray Components; + for (auto & Next : CreatedHoudiniSplineActor->GetComponents()) + { + Components.Add(Next); + } + + Blueprint = Cast(Asset); + + // Clear old Blueprint Node tree + USimpleConstructionScript* SCS = Blueprint->SimpleConstructionScript; + + int32 NodeSize = SCS->GetAllNodes().Num(); + for (int32 n = NodeSize - 1; n >= 0; --n) + SCS->RemoveNode(SCS->GetAllNodes()[n]); + + FKismetEditorUtilities::AddComponentsToBlueprint(Blueprint, Components); + + CreatedHoudiniSplineActor->RemoveFromRoot(); + CreatedHoudiniSplineActor->ConditionalBeginDestroy(); + + GWorld->EditorDestroyActor(CreatedHoudiniSplineActor, true); + + Package->MarkPackageDirty(); + BakedObjectData.PackagesToSave.Add(Package); + } + + // Compile the new/updated blueprints + if (!IsValid(Blueprint)) + FKismetEditorUtilities::CompileBlueprint(Blueprint); + // Save the created BP package. + FHoudiniEngineBakeUtils::SaveBakedPackages(BakedObjectData.PackagesToSave); + + return Blueprint; +} + + +void +FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + UPackage * Package, UObject * Object, const TCHAR * Key, + const TCHAR * Value) +{ + if (!IsValid(Package)) + return; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + Package->GetMetaData().SetValue(Object, Key, Value); +#else + UMetaData * MetaData = Package->GetMetaData(); + if (IsValid(MetaData)) + MetaData->SetValue(Object, Key, Value); +#endif +} + + +bool +FHoudiniEngineBakeUtils:: +GetHoudiniGeneratedNameFromMetaInformation( + UPackage * Package, UObject * Object, FString & HoudiniName) +{ + if (!IsValid(Package)) + return false; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FMetaData& MetaData = Package->GetMetaData(); +#else + UMetaData * MetaData = Package->GetMetaData(); + if (!IsValid(MetaData)) + return false; +#endif + + // Retrieve name used for package generation. +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + if (MetaData.HasValue(Object, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT)) + { + const FString NameFull = MetaData.GetValue(Object, HAPI_UNREAL_PACKAGE_META_GENERATED_NAME); +#else + if (MetaData->HasValue(Object, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT)) + { + const FString NameFull = MetaData->GetValue(Object, HAPI_UNREAL_PACKAGE_META_GENERATED_NAME); +#endif + //HoudiniName = NameFull.Left(FMath::Min(NameFull.Len(), FHoudiniEngineUtils::PackageGUIDItemNameLength)); + HoudiniName = NameFull; + return true; + } + + return false; +} + +UMaterialInterface * +FHoudiniEngineBakeUtils::DuplicateMaterialAndCreatePackage( + UMaterialInterface * Material, UMaterialInterface* PreviousBakeMaterial, const FString & MaterialName, const FHoudiniPackageParams& ObjectPackageParams, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedMaterialsMap) +{ + if (InOutAlreadyBakedMaterialsMap.Contains(Material)) + { + return InOutAlreadyBakedMaterialsMap[Material]; + } + + UMaterialInterface * DuplicatedMaterial = nullptr; + + FString CreatedMaterialName; + // Create material package. Use the same package params as static mesh, but with the material's name + FHoudiniPackageParams MaterialPackageParams = ObjectPackageParams; + MaterialPackageParams.ObjectName = MaterialName; + + // Check if there is a valid previous material. If so, get the bake counter for consistency in + // replace or iterative package naming + bool bIsPreviousBakeMaterialValid = IsValid(PreviousBakeMaterial); + int32 BakeCounter = 0; + TArray PreviousBakeMaterialExpressions; + + + if (bIsPreviousBakeMaterialValid && PreviousBakeMaterial->IsA(UMaterial::StaticClass())) + { + UMaterial * PreviousMaterialCast = Cast(PreviousBakeMaterial); + bIsPreviousBakeMaterialValid = MaterialPackageParams.MatchesPackagePathNameExcludingBakeCounter(PreviousBakeMaterial); + + if (bIsPreviousBakeMaterialValid && PreviousMaterialCast) + { + MaterialPackageParams.GetBakeCounterFromBakedAsset(PreviousBakeMaterial, BakeCounter); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + PreviousBakeMaterialExpressions = PreviousMaterialCast->GetExpressionCollection().Expressions; +#else + PreviousBakeMaterialExpressions = PreviousMaterialCast->Expressions; +#endif + } + } + + UPackage * MaterialPackage = MaterialPackageParams.CreatePackageForObject(CreatedMaterialName, BakeCounter); + if (!IsValid(MaterialPackage)) + return nullptr; + + BakedObjectData.BakeStats.NotifyPackageCreated(1); + + // Clone material. + DuplicatedMaterial = DuplicateObject< UMaterialInterface >(Material, MaterialPackage, *CreatedMaterialName); + if (!IsValid(DuplicatedMaterial)) + return nullptr; + + BakedObjectData.BakeStats.NotifyObjectsCreated(DuplicatedMaterial->GetClass()->GetName(), 1); + + // Add meta information. + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + MaterialPackage, DuplicatedMaterial, + HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + MaterialPackage, DuplicatedMaterial, + HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *CreatedMaterialName); + // Baked object! this is not temporary anymore + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + MaterialPackage, DuplicatedMaterial, + HAPI_UNREAL_PACKAGE_META_BAKED_OBJECT, TEXT("true")); + + // Retrieve and check various sampling expressions. If they contain textures, duplicate (and bake) them. + UMaterial * DuplicatedMaterialCast = Cast(DuplicatedMaterial); + if (DuplicatedMaterialCast) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + auto& MatExpressions = DuplicatedMaterialCast->GetExpressionCollection().Expressions; +#else + auto& MatExpressions = DuplicatedMaterialCast->Expressions; +#endif + const int32 NumExpressions = MatExpressions.Num(); + for (int32 ExpressionIdx = 0; ExpressionIdx < NumExpressions; ++ExpressionIdx) + { + UMaterialExpression* Expression = MatExpressions[ExpressionIdx]; + UMaterialExpression* PreviousBakeExpression = nullptr; + if (bIsPreviousBakeMaterialValid && PreviousBakeMaterialExpressions.IsValidIndex(ExpressionIdx)) + { + PreviousBakeExpression = PreviousBakeMaterialExpressions[ExpressionIdx]; + } + FHoudiniEngineBakeUtils::ReplaceDuplicatedMaterialTextureSample( + Expression, PreviousBakeExpression, MaterialPackageParams, BakedObjectData); + } + } + + // Notify registry that we have created a new duplicate material. + FAssetRegistryModule::AssetCreated(DuplicatedMaterial); + + // Dirty the material package. + DuplicatedMaterial->MarkPackageDirty(); + + // Recompile the baked material + // DuplicatedMaterial->ForceRecompileForRendering(); + // Use UMaterialEditingLibrary::RecompileMaterial since it correctly updates texture references in the material + // which ForceRecompileForRendering does not do + if (DuplicatedMaterialCast) + { + UMaterialEditingLibrary::RecompileMaterial(DuplicatedMaterialCast); + } + + BakedObjectData.PackagesToSave.Add(MaterialPackage); + + InOutAlreadyBakedMaterialsMap.Add(Material, DuplicatedMaterial); + + return DuplicatedMaterial; +} + +void +FHoudiniEngineBakeUtils::ReplaceDuplicatedMaterialTextureSample( + UMaterialExpression * MaterialExpression, UMaterialExpression* PreviousBakeMaterialExpression, + const FHoudiniPackageParams& PackageParams, FHoudiniBakedObjectData& BakedObjectData) +{ + UMaterialExpressionTextureSample * TextureSample = Cast< UMaterialExpressionTextureSample >(MaterialExpression); + if (!IsValid(TextureSample)) + return; + + UTexture2D * Texture = Cast< UTexture2D >(TextureSample->Texture); + if (!IsValid(Texture)) + return; + + UPackage * TexturePackage = Cast< UPackage >(Texture->GetOuter()); + if (!IsValid(TexturePackage)) + return; + + // Try to get the previous bake's texture + UTexture2D* PreviousBakeTexture = nullptr; + if (IsValid(PreviousBakeMaterialExpression)) + { + UMaterialExpressionTextureSample* PreviousBakeTextureSample = Cast< UMaterialExpressionTextureSample >(PreviousBakeMaterialExpression); + if (IsValid(PreviousBakeTextureSample)) + PreviousBakeTexture = Cast< UTexture2D >(PreviousBakeTextureSample->Texture); + } + + FString GeneratedTextureName; + if (FHoudiniEngineBakeUtils::GetHoudiniGeneratedNameFromMetaInformation( + TexturePackage, Texture, GeneratedTextureName)) + { + // Duplicate texture. + UTexture2D * DuplicatedTexture = FHoudiniEngineBakeUtils::DuplicateTextureAndCreatePackage( + Texture, PreviousBakeTexture, GeneratedTextureName, PackageParams, BakedObjectData); + + // Re-assign generated texture. + TextureSample->Texture = DuplicatedTexture; + } +} + +UTexture2D * +FHoudiniEngineBakeUtils::DuplicateTextureAndCreatePackage( + UTexture2D * Texture, + UTexture2D* PreviousBakeTexture, + const FString & SubTextureName, + const FHoudiniPackageParams& PackageParams, + FHoudiniBakedObjectData& BakedObjectData) +{ + UTexture2D* DuplicatedTexture = nullptr; +#if WITH_EDITOR + // Retrieve original package of this texture. + UPackage * TexturePackage = Cast(Texture->GetOuter()); + if (!IsValid(TexturePackage)) + return nullptr; + + FString GeneratedTextureName; + if (FHoudiniEngineBakeUtils::GetHoudiniGeneratedNameFromMetaInformation(TexturePackage, Texture, GeneratedTextureName)) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FMetaData& MetaData = TexturePackage->GetMetaData(); +#else + UMetaData* MetaData = TexturePackage->GetMetaData(); + if (!IsValid(MetaData)) + return nullptr; +#endif + + // Retrieve texture type. +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + const FString& TextureType = + MetaData.GetValue(Texture, HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_TYPE); +#else + // Retrieve texture type. + const FString & TextureType = + MetaData->GetValue(Texture, HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_TYPE); +#endif + FString CreatedTextureName; + + // Create texture package. Use the same package params as material's, but with object name appended by generated texture's name + FHoudiniPackageParams TexturePackageParams = PackageParams; + TexturePackageParams.ObjectName = TexturePackageParams.ObjectName + "_" + GeneratedTextureName; + + // Determine the bake counter of the previous bake's texture (if exists/valid) for naming consistency when + // replacing/iterating + bool bIsPreviousBakeTextureValid = IsValid(PreviousBakeTexture); + int32 BakeCounter = 0; + if (bIsPreviousBakeTextureValid) + { + bIsPreviousBakeTextureValid = TexturePackageParams.MatchesPackagePathNameExcludingBakeCounter(PreviousBakeTexture); + if (bIsPreviousBakeTextureValid) + { + TexturePackageParams.GetBakeCounterFromBakedAsset(PreviousBakeTexture, BakeCounter); + } + } + + UPackage * NewTexturePackage = TexturePackageParams.CreatePackageForObject(CreatedTextureName, BakeCounter); + + if (!IsValid(NewTexturePackage)) + return nullptr; + + BakedObjectData.BakeStats.NotifyPackageCreated(1); + + // Clone texture. + DuplicatedTexture = DuplicateObject< UTexture2D >(Texture, NewTexturePackage, *CreatedTextureName); + if (!IsValid(DuplicatedTexture)) + return nullptr; + + BakedObjectData.BakeStats.NotifyObjectsCreated(DuplicatedTexture->GetClass()->GetName(), 1); + + // Add meta information. + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + NewTexturePackage, DuplicatedTexture, + HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + NewTexturePackage, DuplicatedTexture, + HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *CreatedTextureName); + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + NewTexturePackage, DuplicatedTexture, + HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_TYPE, *TextureType); + // Baked object! this is not temporary anymore + FHoudiniEngineBakeUtils::AddHoudiniMetaInformationToPackage( + NewTexturePackage, DuplicatedTexture, + HAPI_UNREAL_PACKAGE_META_BAKED_OBJECT, TEXT("true")); + + // Notify registry that we have created a new duplicate texture. + FAssetRegistryModule::AssetCreated(DuplicatedTexture); + + // Dirty the texture package. + DuplicatedTexture->MarkPackageDirty(); + + BakedObjectData.PackagesToSave.Add(NewTexturePackage); + } +#endif + return DuplicatedTexture; +} + + +bool +FHoudiniEngineBakeUtils::DeleteBakedHoudiniAssetActor(UHoudiniCookable* InCookable) +{ + if (!IsValid(InCookable)) + return false; + + AActor * ActorOwner = InCookable->GetOwner(); + if (!IsValid(ActorOwner)) + return false; + + UWorld* World = ActorOwner->GetWorld(); + if (!World) + World = GWorld; + + World->EditorDestroyActor(ActorOwner, false); + + return true; +} + + +void +FHoudiniEngineBakeUtils::SaveBakedPackages(TArray & PackagesToSave, bool bSaveCurrentWorld) +{ + UWorld * CurrentWorld = nullptr; + if (bSaveCurrentWorld && GEditor) + CurrentWorld = GEditor->GetEditorWorldContext().World(); + + if (CurrentWorld) + { + // Save the current map + FString CurrentWorldPath = FPaths::GetBaseFilename(CurrentWorld->GetPathName(), false); + UPackage* CurrentWorldPackage = CreatePackage(*CurrentWorldPath); + + if (CurrentWorldPackage) + { + CurrentWorldPackage->MarkPackageDirty(); + PackagesToSave.Add(CurrentWorldPackage); + } + } + + FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, true, false); +} + +bool +FHoudiniEngineBakeUtils::FindOutputObject( + const UObject* InObjectToFind, + const EHoudiniOutputType& InOutputType, + const TArray InOutputs, + int32& OutOutputIndex, + FHoudiniOutputObjectIdentifier &OutIdentifier) +{ + if (!IsValid(InObjectToFind)) + return false; + + const int32 NumOutputs = InOutputs.Num(); + for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) + { + const UHoudiniOutput* CurOutput = InOutputs[OutputIdx]; + if (!IsValid(CurOutput)) + continue; + + if (CurOutput->GetType() != InOutputType) + continue; + + for (auto& CurOutputObject : CurOutput->GetOutputObjects()) + { + if (CurOutputObject.Value.OutputObject == InObjectToFind + || CurOutputObject.Value.ProxyObject == InObjectToFind + || CurOutputObject.Value.ProxyComponent == InObjectToFind) + { + OutOutputIndex = OutputIdx; + OutIdentifier = CurOutputObject.Key; + return true; + } + + for(auto CurrentComponent : CurOutputObject.Value.OutputComponents) + { + if (CurrentComponent == InObjectToFind) + { + OutOutputIndex = OutputIdx; + OutIdentifier = CurOutputObject.Key; + return true; + } + } + } + } + + return false; +} + +bool +FHoudiniEngineBakeUtils::IsObjectTemporary( + UObject* InObject, + const EHoudiniOutputType& InOutputType, + UHoudiniCookable* InCookable) +{ + if (!IsValid(InObject)) + return false; + + FString TempPath = FString(); + + TArray Outputs; + if (IsValid(InCookable)) + { + const int32 NumOutputs = InCookable->GetNumOutputs(); + Outputs.SetNum(NumOutputs); + for (int32 OutputIdx = 0; OutputIdx < NumOutputs; ++OutputIdx) + { + Outputs[OutputIdx] = InCookable->GetOutputAt(OutputIdx); + } + + TempPath = InCookable->GetTemporaryCookFolderOrDefault(); + } + + return IsObjectTemporary(InObject, InOutputType, Outputs, TempPath, InCookable->GetCookableGUID()); +} + +bool +FHoudiniEngineBakeUtils::IsObjectInTempFolder( + UObject* const InObject, + const FString& InTemporaryCookFolder) +{ + if (!IsValid(InObject)) + return false; + + // Check the package path for this object + // If it is in the HAC temp directory, assume it is temporary, and will need to be duplicated + UPackage* ObjectPackage = InObject->GetOutermost(); + if (IsValid(ObjectPackage)) + { + const FString PathName = ObjectPackage->GetPathName(); + if (PathName.StartsWith(InTemporaryCookFolder)) + return true; + + // Also check the default temp folder + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (PathName.StartsWith(HoudiniRuntimeSettings->DefaultTemporaryCookFolder)) + return true; + } + + return false; +} + +bool +FHoudiniEngineBakeUtils::IsObjectTemporary( + UObject* InObject, + const EHoudiniOutputType& InOutputType, + const TArray& InParentOutputs, + const FString& InTemporaryCookFolder, + const FGuid& InComponentGuid) +{ + if (!IsValid(InObject)) + return false; + + // Check the object's meta-data first + if (IsObjectTemporary(InObject, InComponentGuid)) + return true; + + // Previous IsObjectTemporary tests + // Only kept here for compatibility with assets previously baked before adding the "Baked" metadata. + // Check that the object is in the outputs, and stored in the temp directory + + // Object not in the outputs, assume not temp + int32 ParentOutputIndex = -1; + FHoudiniOutputObjectIdentifier Identifier; + // Generated materials will have an invalid output type, dont look for them in the outputs. + if (InOutputType != EHoudiniOutputType::Invalid && !FindOutputObject(InObject, InOutputType, InParentOutputs, ParentOutputIndex, Identifier)) + return false; + + // Check the package path for this object + // If it is in the temp directory, assume it is temporary, and will need to be duplicated + if (IsObjectInTempFolder(InObject, InTemporaryCookFolder)) + return true; + + return false; +} + +bool +FHoudiniEngineBakeUtils::IsObjectTemporary( + UObject* InObject, + const FGuid& InComponentGuid) +{ + if (!IsValid(InObject)) + return false; + + UPackage* ObjectPackage = InObject->GetOutermost(); + if (IsValid(ObjectPackage)) + { + // Look for the meta data +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FMetaData& MetaData = ObjectPackage->GetMetaData(); +#else + UMetaData* MetaData = ObjectPackage->GetMetaData(); + if (IsValid(MetaData)) +#endif + { + // This object was not generated by Houdini, so not a temp object +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + if (!MetaData.HasValue(InObject, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT)) +#else + if (!MetaData->HasValue(InObject, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT)) +#endif + return false; + + // The object has been baked, so not a temp object as well +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + if (MetaData.HasValue(InObject, HAPI_UNREAL_PACKAGE_META_BAKED_OBJECT)) +#else + if (MetaData->HasValue(InObject, HAPI_UNREAL_PACKAGE_META_BAKED_OBJECT)) +#endif + return false; + + // If InComponentGuid is valid, check that against HAPI_UNREAL_PACKAGE_META_COMPONENT_GUID + if (InComponentGuid.IsValid()) + { + const FString GuidStr = InComponentGuid.ToString(); + // If the object has the HAPI_UNREAL_PACKAGE_META_COMPONENT_GUID key but the value does not match GuidStr, + // then, while this is a temporary object, it was not generated by this HAC, so we should not bake it +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + if (MetaData.HasValue(InObject, HAPI_UNREAL_PACKAGE_META_COMPONENT_GUID) + && MetaData.GetValue(InObject, HAPI_UNREAL_PACKAGE_META_COMPONENT_GUID) != GuidStr) +#else + if (MetaData->HasValue(InObject, HAPI_UNREAL_PACKAGE_META_COMPONENT_GUID) + && MetaData->GetValue(InObject, HAPI_UNREAL_PACKAGE_META_COMPONENT_GUID) != GuidStr) +#endif + { + return false; + } + } + } + } + + return true; +} + +void +FHoudiniEngineBakeUtils::CopyPropertyToNewActorAndSkeletalComponent( + AActor* NewActor, + USkeletalMeshComponent* NewSKC, + USkeletalMeshComponent* InSKC, + bool bInCopyWorldTransform) +{ + if (!IsValid(NewSKC)) + return; + + if (!IsValid(InSKC)) + return; +} + + +void +FHoudiniEngineBakeUtils::CopyPropertyToNewActorAndComponent( + AActor* NewActor, + UStaticMeshComponent* NewSMC, + UStaticMeshComponent* InSMC, + bool bInCopyWorldTransform) +{ + if (!IsValid(NewSMC)) + return; + + if (!IsValid(InSMC)) + return; + + // Copy properties to new actor + //UStaticMeshComponent* OtherSMC_NonConst = const_cast(StaticMeshComponent); + NewSMC->SetCollisionProfileName(InSMC->GetCollisionProfileName()); + NewSMC->SetCollisionEnabled(InSMC->GetCollisionEnabled()); + NewSMC->LightmassSettings = InSMC->LightmassSettings; + NewSMC->CastShadow = InSMC->CastShadow; + NewSMC->SetMobility(InSMC->Mobility); + + UBodySetup* InBodySetup = InSMC->GetBodySetup(); + UBodySetup* NewBodySetup = NewSMC->GetBodySetup(); + + // See if we need to create a body setup for the new SMC + if (InBodySetup && !NewBodySetup) + { + if (NewSMC->GetStaticMesh()) + { + NewSMC->GetStaticMesh()->CreateBodySetup(); + NewBodySetup = NewSMC->GetBodySetup(); + } + } + + if (InBodySetup && NewBodySetup) + { + // Copy the BodySetup + NewBodySetup->CopyBodyPropertiesFrom(InBodySetup); + + // We need to recreate the physics mesh for the new body setup + NewBodySetup->InvalidatePhysicsData(); + NewBodySetup->CreatePhysicsMeshes(); + + // Only copy the physical material if it's different from the default one, + // As this was causing crashes on BakeToActors in some cases + if (GEngine != NULL && NewBodySetup->GetPhysMaterial() != GEngine->DefaultPhysMaterial) + NewSMC->SetPhysMaterialOverride(InBodySetup->GetPhysMaterial()); + } + + if (IsValid(NewActor)) + NewActor->SetActorHiddenInGame(InSMC->bHiddenInGame); + + NewSMC->SetVisibility(InSMC->IsVisible()); + + //-------------------------- + // Copy Actor Properties + //-------------------------- + // The below code is from EditorUtilities::CopyActorProperties and simplified + bool bCopyActorProperties = true; + AActor* SourceActor = InSMC->GetOwner(); + if (IsValid(SourceActor) && bCopyActorProperties) + { + // The actor's classes should be compatible, right? + UClass* ActorClass = SourceActor->GetClass(); + //check(NewActor->GetClass()->IsChildOf(ActorClass)); + + bool bTransformChanged = false; + EditorUtilities::FCopyOptions Options(EditorUtilities::ECopyOptions::Default); + + // Copy non-component properties from the old actor to the new actor + TSet ModifiedObjects; + + if (NewActor->GetClass()->IsChildOf(ActorClass)) + { + for (FProperty* Property = ActorClass->PropertyLink; Property != nullptr; Property = Property->PropertyLinkNext) + { + const bool bIsTransient = !!(Property->PropertyFlags & CPF_Transient); + const bool bIsComponentContainer = !!(Property->PropertyFlags & CPF_ContainsInstancedReference); + const bool bIsComponentProp = !!(Property->PropertyFlags & (CPF_InstancedReference | CPF_ContainsInstancedReference)); + const bool bIsBlueprintReadonly = !!(Property->PropertyFlags & CPF_BlueprintReadOnly); + const bool bIsIdentical = Property->Identical_InContainer(SourceActor, NewActor); + + if (!bIsTransient && !bIsIdentical && !bIsComponentContainer && !bIsComponentProp && !bIsBlueprintReadonly) + { + const bool bIsSafeToCopy = (Property->HasAnyPropertyFlags(CPF_Edit | CPF_Interp)) + && (!Property->HasAllPropertyFlags(CPF_DisableEditOnTemplate)); + if (bIsSafeToCopy) + { + if (!Options.CanCopyProperty(*Property, *SourceActor)) + { + continue; + } + + if (!ModifiedObjects.Contains(NewActor)) + { + // Start modifying the target object + NewActor->Modify(); + ModifiedObjects.Add(NewActor); + } + + if (Options.Flags & EditorUtilities::ECopyOptions::CallPostEditChangeProperty) + { + NewActor->PreEditChange(Property); + } + + EditorUtilities::CopySingleProperty(SourceActor, NewActor, Property); + + if (Options.Flags & EditorUtilities::ECopyOptions::CallPostEditChangeProperty) + { + FPropertyChangedEvent PropertyChangedEvent(Property); + NewActor->PostEditChangeProperty(PropertyChangedEvent); + } + } + } + } + } + } + + // TODO: + // // Reapply the uproperties modified by attributes on the new component + // FHoudiniEngineUtils::UpdateAllPropertyAttributesOnObject(InSMC, InHGPO); + // The below code is from EditorUtilities::CopyActorProperties and modified to only copy from one component to another + UClass* ComponentClass = nullptr; + if (InSMC->GetClass()->IsChildOf(NewSMC->GetClass())) + { + ComponentClass = NewSMC->GetClass(); + } + else if (NewSMC->GetClass()->IsChildOf(InSMC->GetClass())) + { + ComponentClass = InSMC->GetClass(); + } + else + { + HOUDINI_LOG_WARNING( + TEXT("Incompatible component classes in CopyPropertyToNewActorAndComponent: %s vs %s"), + *(InSMC->GetName()), + *(NewSMC->GetClass()->GetName())); + + NewSMC->PostEditChange(); + return; + } + + TSet SourceUCSModifiedProperties; + InSMC->GetUCSModifiedProperties(SourceUCSModifiedProperties); + + //AActor* SourceActor = InSMC->GetOwner(); + if (!IsValid(SourceActor)) + { + NewSMC->PostEditChange(); + return; + } + + TArray ModifiedObjects; + const EditorUtilities::FCopyOptions Options(EditorUtilities::ECopyOptions::CallPostEditChangeProperty); + // Copy component properties + for( FProperty* Property = ComponentClass->PropertyLink; Property != nullptr; Property = Property->PropertyLinkNext ) + { + const bool bIsTransient = !!( Property->PropertyFlags & CPF_Transient ); + const bool bIsIdentical = Property->Identical_InContainer( InSMC, NewSMC ); + const bool bIsComponent = !!( Property->PropertyFlags & ( CPF_InstancedReference | CPF_ContainsInstancedReference ) ); + const bool bIsTransform = + Property->GetFName() == USceneComponent::GetRelativeScale3DPropertyName() || + Property->GetFName() == USceneComponent::GetRelativeLocationPropertyName() || + Property->GetFName() == USceneComponent::GetRelativeRotationPropertyName(); + + // auto SourceComponentIsRoot = [&]() + // { + // USceneComponent* RootComponent = SourceActor->GetRootComponent(); + // if (InSMC == RootComponent) + // { + // return true; + // } + // return false; + // }; + + // if( !bIsTransient && !bIsIdentical && !bIsComponent && !SourceUCSModifiedProperties.Contains(Property) + // && ( !bIsTransform || !SourceComponentIsRoot() ) ) + if( !bIsTransient && !bIsIdentical && !bIsComponent && !SourceUCSModifiedProperties.Contains(Property) + && !bIsTransform ) + { + // const bool bIsSafeToCopy = (!(Options.Flags & EditorUtilities::ECopyOptions::OnlyCopyEditOrInterpProperties) || (Property->HasAnyPropertyFlags(CPF_Edit | CPF_Interp))) + // && (!(Options.Flags & EditorUtilities::ECopyOptions::SkipInstanceOnlyProperties) || (!Property->HasAllPropertyFlags(CPF_DisableEditOnTemplate))); + const bool bIsSafeToCopy = true; + if( bIsSafeToCopy ) + { + if (!Options.CanCopyProperty(*Property, *SourceActor)) + { + continue; + } + + if( !ModifiedObjects.Contains(NewSMC) ) + { + NewSMC->SetFlags(RF_Transactional); + NewSMC->Modify(); + ModifiedObjects.Add(NewSMC); + } + + if (Options.Flags & EditorUtilities::ECopyOptions::CallPostEditChangeProperty) + { + // @todo simulate: Should we be calling this on the component instead? + NewActor->PreEditChange( Property ); + } + + EditorUtilities::CopySingleProperty(InSMC, NewSMC, Property); + + if (Options.Flags & EditorUtilities::ECopyOptions::CallPostEditChangeProperty) + { + FPropertyChangedEvent PropertyChangedEvent( Property ); + NewActor->PostEditChangeProperty( PropertyChangedEvent ); + } + } + } + } + + if (bInCopyWorldTransform) + { + NewSMC->SetWorldTransform(InSMC->GetComponentTransform()); + } + + // Also copy custom primitive data on the SMC if any + FCustomPrimitiveData InPrimData = InSMC->GetCustomPrimitiveData(); + if (!InPrimData.Data.IsEmpty()) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 5 + NewSMC->SetCustomPrimitiveDataFloatArray(0, InPrimData.Data); + NewSMC->SetDefaultCustomPrimitiveDataFloatArray(0, InPrimData.Data); +#else + for (int Idx = 0; Idx < InPrimData.Data.Num(); Idx++) + { + NewSMC->SetCustomPrimitiveDataFloat(Idx, InPrimData.Data[Idx]); + NewSMC->SetDefaultCustomPrimitiveDataFloat(Idx, InPrimData.Data[Idx]); + } +#endif + } + + NewSMC->PostEditChange(); +} + +void FHoudiniEngineBakeUtils::CopyPropertyToNewGeometryCollectionActorAndComponent(AGeometryCollectionActor* NewActor, + UGeometryCollectionComponent* NewGCC, UGeometryCollectionComponent* InGCC, bool bInCopyWorldTransform) +{ + if (!IsValid(NewGCC)) + return; + + if (!IsValid(InGCC)) + return; + + // Copy properties to new actor + NewGCC->ChaosSolverActor = InGCC->ChaosSolverActor; + NewGCC->InitializationFields = InGCC->InitializationFields; + NewGCC->InitializationState = InGCC->InitializationState; + NewGCC->ObjectType= InGCC->ObjectType; + NewGCC->EnableClustering = InGCC->EnableClustering; + NewGCC->ClusterGroupIndex = InGCC->ClusterGroupIndex; + NewGCC->MaxClusterLevel = InGCC->MaxClusterLevel; + NewGCC->DamageThreshold = InGCC->DamageThreshold; + NewGCC->CollisionGroup = InGCC->CollisionGroup; + NewGCC->CollisionSampleFraction = InGCC->CollisionSampleFraction; + NewGCC->InitialVelocityType = InGCC->InitialVelocityType; + NewGCC->InitialLinearVelocity = InGCC->InitialLinearVelocity; + NewGCC->InitialAngularVelocity = InGCC->InitialAngularVelocity; + + if (IsValid(NewActor)) + NewActor->SetActorHiddenInGame(InGCC->bHiddenInGame); + + NewGCC->SetVisibility(InGCC->IsVisible()); + + // The below code is from EditorUtilities::CopyActorProperties and modified to only copy from one component to another + UClass* ComponentClass = nullptr; + if (InGCC->GetClass()->IsChildOf(NewGCC->GetClass())) + { + ComponentClass = NewGCC->GetClass(); + } + else if (NewGCC->GetClass()->IsChildOf(InGCC->GetClass())) + { + ComponentClass = InGCC->GetClass(); + } + else + { + HOUDINI_LOG_WARNING( + TEXT("Incompatible component classes in CopyPropertyToNewActorAndComponent: %s vs %s"), + *(InGCC->GetName()), + *(NewGCC->GetClass()->GetName())); + + NewGCC->PostEditChange(); + return; + } + + TSet SourceUCSModifiedProperties; + InGCC->GetUCSModifiedProperties(SourceUCSModifiedProperties); + + AActor* SourceActor = InGCC->GetOwner(); + if (!IsValid(SourceActor)) + { + NewGCC->PostEditChange(); + return; + } + + TArray ModifiedObjects; + const EditorUtilities::FCopyOptions Options(EditorUtilities::ECopyOptions::CallPostEditChangeProperty); + // Copy component properties + for( FProperty* Property = ComponentClass->PropertyLink; Property != nullptr; Property = Property->PropertyLinkNext ) + { + const bool bIsTransient = !!( Property->PropertyFlags & CPF_Transient ); + const bool bIsIdentical = Property->Identical_InContainer( InGCC, NewGCC ); + const bool bIsComponent = !!( Property->PropertyFlags & ( CPF_InstancedReference | CPF_ContainsInstancedReference ) ); + const bool bIsTransform = + Property->GetFName() == USceneComponent::GetRelativeScale3DPropertyName() || + Property->GetFName() == USceneComponent::GetRelativeLocationPropertyName() || + Property->GetFName() == USceneComponent::GetRelativeRotationPropertyName(); + + // auto SourceComponentIsRoot = [&]() + // { + // USceneComponent* RootComponent = SourceActor->GetRootComponent(); + // if (InSMC == RootComponent) + // { + // return true; + // } + // return false; + // }; + + // if( !bIsTransient && !bIsIdentical && !bIsComponent && !SourceUCSModifiedProperties.Contains(Property) + // && ( !bIsTransform || !SourceComponentIsRoot() ) ) + if( !bIsTransient && !bIsIdentical && !bIsComponent && !SourceUCSModifiedProperties.Contains(Property) + && !bIsTransform ) + { + // const bool bIsSafeToCopy = (!(Options.Flags & EditorUtilities::ECopyOptions::OnlyCopyEditOrInterpProperties) || (Property->HasAnyPropertyFlags(CPF_Edit | CPF_Interp))) + // && (!(Options.Flags & EditorUtilities::ECopyOptions::SkipInstanceOnlyProperties) || (!Property->HasAllPropertyFlags(CPF_DisableEditOnTemplate))); + const bool bIsSafeToCopy = true; + if( bIsSafeToCopy ) + { + if (!Options.CanCopyProperty(*Property, *SourceActor)) + { + continue; + } + + if( !ModifiedObjects.Contains(NewGCC) ) + { + NewGCC->SetFlags(RF_Transactional); + NewGCC->Modify(); + ModifiedObjects.Add(NewGCC); + } + + if (Options.Flags & EditorUtilities::ECopyOptions::CallPostEditChangeProperty) + { + // @todo simulate: Should we be calling this on the component instead? + NewActor->PreEditChange( Property ); + } + + EditorUtilities::CopySingleProperty(InGCC, NewGCC, Property); + + if (Options.Flags & EditorUtilities::ECopyOptions::CallPostEditChangeProperty) + { + FPropertyChangedEvent PropertyChangedEvent( Property ); + NewActor->PostEditChangeProperty( PropertyChangedEvent ); + } + } + } + } + + if (bInCopyWorldTransform) + { + NewGCC->SetWorldTransform(InGCC->GetComponentTransform()); + } + + NewGCC->PostEditChange(); +}; + +bool +FHoudiniEngineBakeUtils::RemovePreviouslyBakedActor( + AActor* InNewBakedActor, + ULevel* InLevel, + const FHoudiniPackageParams& InPackageParams) +{ + // Remove a previous bake actor if it exists + for (auto & Actor : InLevel->Actors) + { + if (!Actor) + continue; + + if (Actor != InNewBakedActor && Actor->GetActorNameOrLabel() == InPackageParams.ObjectName) + { + UWorld* World = Actor->GetWorld(); + if (!World) + World = GWorld; + + Actor->RemoveFromRoot(); + Actor->ConditionalBeginDestroy(); + World->EditorDestroyActor(Actor, true); + + return true; + } + } + + return false; +} + +bool +FHoudiniEngineBakeUtils::RemovePreviouslyBakedComponent(UActorComponent* InComponent) +{ + if (!IsValid(InComponent)) + return false; + + // Remove from its actor first + if (InComponent->GetOwner()) + InComponent->GetOwner()->RemoveOwnedComponent(InComponent); + + // Detach from its parent component if attached + USceneComponent* SceneComponent = Cast(InComponent); + if (IsValid(SceneComponent)) + SceneComponent->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + InComponent->UnregisterComponent(); + InComponent->DestroyComponent(); + + return true; +} + +FName +FHoudiniEngineBakeUtils::GetOutputFolderPath(UObject* InOutputOwner) +{ + // Get an output folder path for PDG outputs generated by InOutputOwner. + // The folder path is: / + FString FolderName; + FName FolderDirName; + AActor* OuterActor = Cast(InOutputOwner); + if (OuterActor) + { + FolderName = OuterActor->GetActorLabel(); + FolderDirName = OuterActor->GetFolderPath(); + } + else + { + FolderName = InOutputOwner->GetName(); + } + if (!FolderDirName.IsNone()) + return FName(FString::Printf(TEXT("%s/%s"), *FolderDirName.ToString(), *FolderName)); + else + return FName(FolderName); +} + +void +FHoudiniEngineBakeUtils::RenameAsset(UObject* InAsset, const FString& InNewName, bool bMakeUniqueIfNotUnique) +{ + FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked(TEXT("AssetTools")); + + const FSoftObjectPath OldPath = FSoftObjectPath(InAsset); + + FString NewName; + if (bMakeUniqueIfNotUnique) + NewName = MakeUniqueObjectNameIfNeeded(InAsset->GetPackage(), InAsset->GetClass(), InNewName, InAsset); + else + NewName = InNewName; + + FHoudiniEngineUtils::RenameObject(InAsset, *NewName); + + const FSoftObjectPath NewPath = FSoftObjectPath(InAsset); + if (OldPath != NewPath) + { + TArray RenameData; + RenameData.Add(FAssetRenameData(OldPath, NewPath, true)); + AssetToolsModule.Get().RenameAssets(RenameData); + } +} + +void +FHoudiniEngineBakeUtils::RenameAndRelabelActor(AActor* InActor, const FString& InNewName, bool bMakeUniqueIfNotUnique) +{ + if (!IsValid(InActor)) + return; + + FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked(TEXT("AssetTools")); + + const FSoftObjectPath OldPath = FSoftObjectPath(InActor); + + FString NewName; + if (bMakeUniqueIfNotUnique) + NewName = MakeUniqueObjectNameIfNeeded(InActor->GetOuter(), InActor->GetClass(), InNewName, InActor); + else + NewName = InNewName; + + FHoudiniEngineUtils::RenameObject(InActor, *NewName); + FHoudiniEngineRuntimeUtils::SetActorLabel(InActor, NewName); + + const FSoftObjectPath NewPath = FSoftObjectPath(InActor); + if (OldPath != NewPath) + { + TArray RenameData; + RenameData.Add(FAssetRenameData(OldPath, NewPath, true)); + AssetToolsModule.Get().RenameAssets(RenameData); + } +} + +bool +FHoudiniEngineBakeUtils::DetachAndRenameBakedPDGOutputActor( + AActor* InActor, + const FString& InNewName, + const FName& InFolderPath) +{ + if (!IsValid(InActor)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineUtils::DetachAndRenameBakedPDGOutputActor]: InActor is null.")); + return false; + } + + if (InNewName.TrimStartAndEnd().IsEmpty()) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineUtils::DetachAndRenameBakedPDGOutputActor]: A valid actor name must be specified.")); + return false; + } + + // Detach from parent + InActor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); + // Rename + const bool bMakeUniqueIfNotUnique = true; + RenameAndRelabelActor(InActor, InNewName, bMakeUniqueIfNotUnique); + + InActor->SetFolderPath(InFolderPath); + + return true; +} + +bool +FHoudiniEngineBakeUtils::BakePDGWorkResultObject( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InNode, + int32 InWorkResultArrayIndex, + int32 InWorkResultObjectArrayIndex, + const FHoudiniBakeSettings& BakeSettings, + bool bInBakeToWorkResultActor, + bool bInIsAutoBake, + const TArray& InBakedActors, + TArray& OutBakedActors, + FHoudiniBakedObjectData& BakedObjectData, + TArray * InOutputTypesToBake, + TArray * InInstancerComponentTypesToBake, + const FString& InFallbackWorldOutlinerFolder) +{ + if (!IsValid(InPDGAssetLink)) + return false; + + if (!IsValid(InNode)) + return false; + + if (!InNode->WorkResult.IsValidIndex(InWorkResultArrayIndex)) + return false; + + FTOPWorkResult& WorkResult = InNode->WorkResult[InWorkResultArrayIndex]; + if (!WorkResult.ResultObjects.IsValidIndex(InWorkResultObjectArrayIndex)) + return false; + + FTOPWorkResultObject& WorkResultObject = WorkResult.ResultObjects[InWorkResultObjectArrayIndex]; + TArray>& Outputs = WorkResultObject.GetResultOutputs(); + if (Outputs.Num() == 0) + return true; + + if (WorkResultObject.State != EPDGWorkResultState::Loaded) + { + if (bInIsAutoBake && WorkResultObject.AutoBakedSinceLastLoad()) + { + HOUDINI_LOG_MESSAGE(TEXT("[FHoudiniEngineBakeUtils::BakePDGTOPNodeOutputsKeepActors]: WorkResultObject (%s) is not loaded but was auto-baked since its last load."), *WorkResultObject.Name); + return true; + } + + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineBakeUtils::BakePDGTOPNodeOutputsKeepActors]: WorkResultObject (%s) is not loaded, cannot bake it."), *WorkResultObject.Name); + return false; + } + + AActor* WorkResultObjectActor = WorkResultObject.GetOutputActorOwner().GetOutputActor(); + if (!IsValid(WorkResultObjectActor)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineBakeUtils::BakePDGTOPNodeOutputsKeepActors]: WorkResultObjectActor (%s) is null (unexpected since # Outputs > 0)"), *WorkResultObject.Name); + return false; + } + + // Find the previous bake output for this work result object + FString Key; + InNode->GetBakedWorkResultObjectOutputsKey(InWorkResultArrayIndex, InWorkResultObjectArrayIndex, Key); + FHoudiniPDGWorkResultObjectBakedOutput& BakedOutputContainer = InNode->GetBakedWorkResultObjectsOutputs().FindOrAdd(Key); + + FHoudiniEngineBakeState BakeState(Outputs.Num(), BakedOutputContainer.BakedOutputs); + + UHoudiniCookable* HC = FHoudiniEngineUtils::GetOuterHoudiniCookable(InPDGAssetLink); + check(IsValid(HC)); + + TArray WROBakedActors; + BakeHoudiniOutputsToActors( + HC, + Outputs, + BakeState, + WorkResultObjectActor->GetActorTransform(), + InPDGAssetLink->BakeFolder, + InPDGAssetLink->GetTemporaryCookFolder(), + BakeSettings, + InBakedActors, + WROBakedActors, + BakedObjectData, + InOutputTypesToBake, + InInstancerComponentTypesToBake, + bInBakeToWorkResultActor ? WorkResultObjectActor : nullptr, + InFallbackWorldOutlinerFolder); + + // Set the PDG indices on the output baked actor entries + FOutputActorOwner& OutputActorOwner = WorkResultObject.GetOutputActorOwner(); + AActor* const WROActor = OutputActorOwner.GetOutputActor(); + FHoudiniEngineBakedActor const * BakedWROActorEntry = nullptr; + if (WROBakedActors.Num() > 0) + { + for (FHoudiniEngineBakedActor& BakedActorEntry : WROBakedActors) + { + BakedActorEntry.PDGWorkResultArrayIndex = InWorkResultArrayIndex; + BakedActorEntry.PDGWorkItemIndex = WorkResult.WorkItemIndex; + BakedActorEntry.PDGWorkResultObjectArrayIndex = InWorkResultObjectArrayIndex; + + if (WROActor && BakedActorEntry.Actor == WROActor) + { + BakedWROActorEntry = &BakedActorEntry; + } + } + } + + // If anything was baked to WorkResultObjectActor, detach it from its parent + if (bInBakeToWorkResultActor) + { + // if we re-used the temp actor as a bake actor, then remove its temp outputs + constexpr bool bDeleteOutputActor = false; + InNode->DeleteWorkResultObjectOutputs(InWorkResultArrayIndex, InWorkResultObjectArrayIndex, bDeleteOutputActor); + if (WROActor) + { + if (BakedWROActorEntry) + { + OutputActorOwner.SetOutputActor(nullptr); + const FString OldActorPath = FSoftObjectPath(WROActor).ToString(); + DetachAndRenameBakedPDGOutputActor( + WROActor, BakedWROActorEntry->ActorBakeName.ToString(), BakedWROActorEntry->WorldOutlinerFolder); + const FString NewActorPath = FSoftObjectPath(WROActor).ToString(); + if (OldActorPath != NewActorPath) + { + // Fix cached string reference in baked outputs to WROActor + for (FHoudiniBakedOutput& BakedOutput : BakedOutputContainer.BakedOutputs) + { + for (auto& Entry : BakedOutput.BakedOutputObjects) + { + if (Entry.Value.Actor == OldActorPath) + Entry.Value.Actor = NewActorPath; + } + } + } + } + else + { + OutputActorOwner.DestroyOutputActor(); + } + } + } + + if (bInIsAutoBake) + WorkResultObject.SetAutoBakedSinceLastLoad(true); + + OutBakedActors = MoveTemp(WROBakedActors); + + BakedOutputContainer.BakedOutputs = BakeState.GetNewBakedOutputs(); + + // Ensure that the outer level (or actor in the case of OFPA) is marked as dirty so that references to the + // output actors / objects and baked actor results are saved + InNode->MarkPackageDirty(); + + return true; +} + +void +FHoudiniEngineBakeUtils::CheckPDGAutoBakeAfterResultObjectLoaded( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InNode, + int32 InWorkItemHAPIIndex, + int32 InWorkItemResultInfoIndex) +{ + TArray BakedActors; + PDGAutoBakeAfterResultObjectLoaded(InPDGAssetLink, InNode, InWorkItemHAPIIndex, InWorkItemResultInfoIndex, BakedActors); +} + +void +FHoudiniEngineBakeUtils::PDGAutoBakeAfterResultObjectLoaded( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InNode, + int32 InWorkItemHAPIIndex, + int32 InWorkItemResultInfoIndex, + TArray& OutBakedActors) +{ + if (!IsValid(InPDGAssetLink)) + return; + + if (!InPDGAssetLink->bBakeAfterAllWorkResultObjectsLoaded) + return; + + if (!IsValid(InNode)) + return; + + // Check if the node is ready for baking: all work items must be complete + bool bDoNotBake = false; + if (!InNode->AreAllWorkItemsComplete() || (!InPDGAssetLink->IsAutoBakeNodesWithFailedWorkItemsEnabled() && InNode->AnyWorkItemsFailed())) + bDoNotBake = true; + + // Check if the node is ready for baking: all work items must be loaded + if (!bDoNotBake) + { + for (const FTOPWorkResult& WorkResult : InNode->WorkResult) + { + for (const FTOPWorkResultObject& WRO : WorkResult.ResultObjects) + { + if (WRO.State != EPDGWorkResultState::Loaded && !WRO.AutoBakedSinceLastLoad()) + { + bDoNotBake = true; + break; + } + } + if (bDoNotBake) + break; + } + } + + if (!bDoNotBake) + { + // Check which outputs are selected for baking: selected node, selected network or all + // And only bake if the node falls within the criteria + UTOPNetwork const * const SelectedTOPNetwork = InPDGAssetLink->GetSelectedTOPNetwork(); + UTOPNode const * const SelectedTOPNode = InPDGAssetLink->GetSelectedTOPNode(); + switch (InPDGAssetLink->PDGBakeSelectionOption) + { + case EPDGBakeSelectionOption::SelectedNetwork: + if (!IsValid(SelectedTOPNetwork) || !InNode->IsParentTOPNetwork(SelectedTOPNetwork)) + { + HOUDINI_LOG_WARNING( + TEXT("Not baking Node %s (Net %s): not in selected network"), + InNode ? *InNode->GetName() : TEXT(""), + SelectedTOPNetwork ? *SelectedTOPNetwork->GetName() : TEXT("")); + bDoNotBake = true; + } + break; + case EPDGBakeSelectionOption::SelectedNode: + if (InNode != SelectedTOPNode) + { + HOUDINI_LOG_WARNING( + TEXT("Not baking Node %s (Net %s): not the selected node"), + InNode ? *InNode->GetName() : TEXT(""), + SelectedTOPNetwork ? *SelectedTOPNetwork->GetName() : TEXT("")); + bDoNotBake = true; + } + break; + case EPDGBakeSelectionOption::All: + default: + break; + } + } + + if (bDoNotBake) + return; + + TArray BakedActors; + bool bSuccess = false; + const bool bIsAutoBake = true; + switch (InPDGAssetLink->HoudiniEngineBakeOption) + { + case EHoudiniEngineBakeOption::ToActor: + bSuccess = FHoudiniEngineBakeUtils::BakePDGTOPNodeOutputsKeepActors(InPDGAssetLink, InNode, bIsAutoBake, InPDGAssetLink->PDGBakePackageReplaceMode, InPDGAssetLink->bRecenterBakedActors, BakedActors); + break; + + case EHoudiniEngineBakeOption::ToBlueprint: + bSuccess = FHoudiniEngineBakeUtils::BakePDGTOPNodeBlueprints(InPDGAssetLink, InNode, bIsAutoBake, InPDGAssetLink->PDGBakePackageReplaceMode, InPDGAssetLink->bRecenterBakedActors); + break; + + default: + HOUDINI_LOG_WARNING(TEXT("Unsupported HoudiniEngineBakeOption %i"), InPDGAssetLink->HoudiniEngineBakeOption); + } + + if (bSuccess) + OutBakedActors = MoveTemp(BakedActors); + + InPDGAssetLink->OnNodeAutoBaked(InNode, bSuccess); +} + +bool +FHoudiniEngineBakeUtils::BakePDGTOPNodeOutputsKeepActors( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InNode, + bool bInBakeForBlueprint, + bool bInIsAutoBake, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + TArray& OutBakedActors, + FHoudiniBakedObjectData& BakedObjectData) +{ + if (!IsValid(InPDGAssetLink)) + return false; + + if (!IsValid(InNode)) + return false; + + // Determine the output world outliner folder path via the PDG asset link's + // owner's folder path and name + UObject* PDGOwner = InPDGAssetLink->GetOwnerActor(); + if (!PDGOwner) + PDGOwner = InPDGAssetLink->GetOuter(); + const FName& FallbackWorldOutlinerFolderPath = GetOutputFolderPath(PDGOwner); + + // Determine the actor/package replacement settings + FHoudiniBakeSettings BakeSettings; + BakeSettings.bReplaceActors = !bInBakeForBlueprint && InPDGBakePackageReplaceMode == EPDGBakePackageReplaceModeOption::ReplaceExistingAssets; + BakeSettings.bReplaceAssets = InPDGBakePackageReplaceMode == EPDGBakePackageReplaceModeOption::ReplaceExistingAssets; + + // Determine the output types to bake: don't bake landscapes in blueprint baking mode + TArray OutputTypesToBake; + TArray InstancerComponentTypesToBake; + if (bInBakeForBlueprint) + { + OutputTypesToBake.Add(EHoudiniOutputType::Mesh); + OutputTypesToBake.Add(EHoudiniOutputType::Instancer); + OutputTypesToBake.Add(EHoudiniOutputType::Curve); + + InstancerComponentTypesToBake.Add(EHoudiniInstancerComponentType::StaticMeshComponent); + InstancerComponentTypesToBake.Add(EHoudiniInstancerComponentType::InstancedStaticMeshComponent); + InstancerComponentTypesToBake.Add(EHoudiniInstancerComponentType::MeshSplitInstancerComponent); + InstancerComponentTypesToBake.Add(EHoudiniInstancerComponentType::FoliageAsHierarchicalInstancedStaticMeshComponent); + InstancerComponentTypesToBake.Add(EHoudiniInstancerComponentType::GeometryCollectionComponent); + //InstancerComponentTypesToBake.Add(EHoudiniInstancerComponentType::InstancedActorComponent); + } + + const int32 NumWorkResults = InNode->WorkResult.Num(); + FScopedSlowTask Progress(NumWorkResults, FText::FromString(FString::Printf(TEXT("Baking PDG Node Output %s ..."), *InNode->GetName()))); + Progress.MakeDialog(); + + TArray OurBakedActors; + TArray WorkResultObjectBakedActors; + for (int32 WorkResultArrayIdx = 0; WorkResultArrayIdx < NumWorkResults; ++WorkResultArrayIdx) + { + // Bug: #126086 + // Fixed ensure failure due to invalid amount of work passed to the FSlowTask + Progress.EnterProgressFrame(1.0f); + + FTOPWorkResult& WorkResult = InNode->WorkResult[WorkResultArrayIdx]; + const int32 NumWorkResultObjects = WorkResult.ResultObjects.Num(); + for (int32 WorkResultObjectArrayIdx = 0; WorkResultObjectArrayIdx < NumWorkResultObjects; ++WorkResultObjectArrayIdx) + { + WorkResultObjectBakedActors.Reset(); + + BakePDGWorkResultObject( + InPDGAssetLink, + InNode, + WorkResultArrayIdx, + WorkResultObjectArrayIdx, + BakeSettings, + !bInBakeForBlueprint, + bInIsAutoBake, + OurBakedActors, + WorkResultObjectBakedActors, + BakedObjectData, + OutputTypesToBake.Num() > 0 ? &OutputTypesToBake : nullptr, + InstancerComponentTypesToBake.Num() > 0 ? &InstancerComponentTypesToBake : nullptr, + FallbackWorldOutlinerFolderPath.ToString() + ); + + OurBakedActors.Append(WorkResultObjectBakedActors); + } + } + + OutBakedActors = MoveTemp(OurBakedActors); + + return true; +} + +bool +FHoudiniEngineBakeUtils::BakePDGTOPNodeOutputsKeepActors( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InTOPNode, + bool bInIsAutoBake, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors, + TArray& OutBakedActors) +{ + FHoudiniBakedObjectData BakedObjectData; + + const bool bBakeBlueprints = false; + + bool bSuccess = BakePDGTOPNodeOutputsKeepActors( + InPDGAssetLink, InTOPNode, bBakeBlueprints, bInIsAutoBake, InPDGBakePackageReplaceMode, OutBakedActors, BakedObjectData); + + SaveBakedPackages(BakedObjectData.PackagesToSave); + + // Recenter and select the baked actors + if (GEditor && OutBakedActors.Num() > 0) + GEditor->SelectNone(false, true); + + for (const FHoudiniEngineBakedActor& Entry : OutBakedActors) + { + if (!IsValid(Entry.Actor)) + continue; + + if (bInRecenterBakedActors) + CenterActorToBoundingBoxCenter(Entry.Actor); + + if (GEditor) + GEditor->SelectActor(Entry.Actor, true, false); + } + + if (GEditor && OutBakedActors.Num() > 0) + GEditor->NoteSelectionChange(); + + UHoudiniCookable* Cookable = Cast(InPDGAssetLink->GetOuter()); + bool bDoNotifications = Cookable ? Cookable->GetDoSlateNotifications() : true; + if (bDoNotifications) + { + const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); + FString Msg = FString::Format(*FinishedTemplate, { BakedObjectData.BakeStats.NumPackagesCreated, BakedObjectData.BakeStats.NumPackagesUpdated } ); + FHoudiniEngine::Get().FinishTaskSlateNotification( FText::FromString(Msg) ); + } + + return bSuccess; +} + +bool +FHoudiniEngineBakeUtils::BakePDGTOPNetworkOutputsKeepActors( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNetwork* InNetwork, + bool bInBakeForBlueprint, + bool bInIsAutoBake, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + TArray& BakedActors, + FHoudiniBakedObjectData& BakedObjectData) +{ + if (!IsValid(InPDGAssetLink)) + return false; + + if (!IsValid(InNetwork)) + return false; + + bool bSuccess = true; + for (UTOPNode* Node : InNetwork->AllTOPNodes) + { + if (!IsValid(Node)) + continue; + + bSuccess &= BakePDGTOPNodeOutputsKeepActors(InPDGAssetLink, Node, bInBakeForBlueprint, bInIsAutoBake, InPDGBakePackageReplaceMode, BakedActors, BakedObjectData); + } + + return bSuccess; +} + +bool FHoudiniEngineBakeUtils::BakePDGAssetLinkOutputsKeepActors( + UHoudiniPDGAssetLink* InPDGAssetLink, + EPDGBakeSelectionOption InBakeSelectionOption, + EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors, + FHoudiniBakedObjectData& BakedObjectData, + TArray& BakedActors) +{ + if (!IsValid(InPDGAssetLink)) + return false; + + const bool bBakeBlueprints = false; + const bool bIsAutoBake = false; + + bool bSuccess = true; + switch (InBakeSelectionOption) + { + case EPDGBakeSelectionOption::All: + for (UTOPNetwork* Network : InPDGAssetLink->AllTOPNetworks) + { + if (!IsValid(Network)) + continue; + + for (UTOPNode* Node : Network->AllTOPNodes) + { + if (!IsValid(Node)) + continue; + + bSuccess &= BakePDGTOPNodeOutputsKeepActors(InPDGAssetLink, Node, bBakeBlueprints, bIsAutoBake, InPDGBakePackageReplaceMode, BakedActors, BakedObjectData); + } + } + break; + + case EPDGBakeSelectionOption::SelectedNetwork: + bSuccess = BakePDGTOPNetworkOutputsKeepActors(InPDGAssetLink, InPDGAssetLink->GetSelectedTOPNetwork(), bBakeBlueprints, bIsAutoBake, InPDGBakePackageReplaceMode, BakedActors, BakedObjectData); + break; + + case EPDGBakeSelectionOption::SelectedNode: + bSuccess = BakePDGTOPNodeOutputsKeepActors(InPDGAssetLink, InPDGAssetLink->GetSelectedTOPNode(), bBakeBlueprints, bIsAutoBake, InPDGBakePackageReplaceMode, BakedActors, BakedObjectData); + break; + } + + SaveBakedPackages(BakedObjectData.PackagesToSave); + + // Recenter and select the baked actors + if (GEditor && BakedActors.Num() > 0) + GEditor->SelectNone(false, true); + + for (const FHoudiniEngineBakedActor& Entry : BakedActors) + { + if (!IsValid(Entry.Actor)) + continue; + + if (bInRecenterBakedActors) + CenterActorToBoundingBoxCenter(Entry.Actor); + + if (GEditor) + GEditor->SelectActor(Entry.Actor, true, false); + } + + if (GEditor && BakedActors.Num() > 0) + GEditor->NoteSelectionChange(); + + UHoudiniCookable* Cookable = Cast(InPDGAssetLink->GetOuter()); + bool bDoNotifications = Cookable ? Cookable->GetDoSlateNotifications() : true; + + if (bDoNotifications) + { + const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); + FString Msg = FString::Format(*FinishedTemplate, { BakedObjectData.BakeStats.NumPackagesCreated, BakedObjectData.BakeStats.NumPackagesUpdated }); + FHoudiniEngine::Get().FinishTaskSlateNotification(FText::FromString(Msg)); + } + + // Broadcast that the bake is complete + InPDGAssetLink->HandleOnPostBake(bSuccess); + + return bSuccess; +} + + +bool FHoudiniEngineBakeUtils::BakePDGAssetLink( + UHoudiniPDGAssetLink* InPDGAssetLink) +{ + bool Result; + + FHoudiniBakedObjectData BakedObjectData; + TArray BakedActors; + + switch(InPDGAssetLink->HoudiniEngineBakeOption) + { + case EHoudiniEngineBakeOption::ToActor: + { + Result = FHoudiniEngineBakeUtils::BakePDGAssetLinkOutputsKeepActors( + InPDGAssetLink, + InPDGAssetLink->PDGBakeSelectionOption, + InPDGAssetLink->PDGBakePackageReplaceMode, + InPDGAssetLink->bRecenterBakedActors, + BakedObjectData, + BakedActors); + } + break; + + case EHoudiniEngineBakeOption::ToBlueprint: + { + Result = FHoudiniEngineBakeUtils::BakePDGAssetLinkBlueprints( + InPDGAssetLink, + InPDGAssetLink->PDGBakeSelectionOption, + InPDGAssetLink->PDGBakePackageReplaceMode, + InPDGAssetLink->bRecenterBakedActors, + BakedObjectData); + } + break; + default: + Result = false; + } + return Result; +} + +bool +FHoudiniEngineBakeUtils::BakePDGAssetLinkOutputsKeepActors( + UHoudiniPDGAssetLink* InPDGAssetLink, + EPDGBakeSelectionOption InBakeSelectionOption, + EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors) +{ + FHoudiniBakedObjectData BakedObjectData; + TArray BakedActors; + + bool bSuccess = BakePDGAssetLinkOutputsKeepActors( + InPDGAssetLink, + InBakeSelectionOption, + InPDGBakePackageReplaceMode, + bInRecenterBakedActors, + BakedObjectData, + BakedActors); + + return bSuccess; +} +bool +FHoudiniEngineBakeUtils::BakeBlueprintsFromBakedActors( + const TArray& InBakedActors, + const FHoudiniBakeSettings& BakeSettings, + const FString& InHoudiniAssetName, + const FString& InHoudiniAssetActorName, + const FDirectoryPath& InBakeFolder, + TArray* const InNonPDGBakedOutputs, + TMap* const InPDGBakedOutputs, + FHoudiniBakedObjectData& BakedObjectData) +{ + // // Clear selection + // if (GEditor) + // { + // GEditor->SelectNone(false, true); + // GEditor->NoteSelectionChange(); + // } + + // Iterate over the baked actors. An actor might appear multiple times if multiple OutputComponents were + // baked to the same actor, so keep track of actors we have already processed in BakedActorSet + UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); + const bool bIsAssetEditorSubsystemValid = IsValid(AssetEditorSubsystem); + TArray AssetsToReOpenEditors; + TMap BakedActorMap; + + for (const FHoudiniEngineBakedActor& Entry : InBakedActors) + { + AActor *Actor = Entry.Actor; + + if (!IsValid(Actor)) + continue; + + // If we have a previously baked a blueprint, get the bake counter from it so that both replace and increment + // is consistent with the bake counter + int32 BakeCounter = 0; + FHoudiniBakedOutputObject* BakedOutputObject = nullptr; + // Get the baked output object + if (Entry.PDGWorkResultArrayIndex >= 0 && Entry.PDGWorkItemIndex >= 0 && Entry.PDGWorkResultObjectArrayIndex >= 0 && InPDGBakedOutputs) + { + const FString Key = UTOPNode::GetBakedWorkResultObjectOutputsKey(Entry.PDGWorkResultArrayIndex, Entry.PDGWorkResultObjectArrayIndex); + FHoudiniPDGWorkResultObjectBakedOutput* WorkResultObjectBakedOutput = InPDGBakedOutputs->Find(Key); + if (WorkResultObjectBakedOutput) + { + if (Entry.OutputIndex >= 0 && WorkResultObjectBakedOutput->BakedOutputs.IsValidIndex(Entry.OutputIndex)) + { + BakedOutputObject = WorkResultObjectBakedOutput->BakedOutputs[Entry.OutputIndex].BakedOutputObjects.Find(Entry.OutputObjectIdentifier); + } + } + } + else if (Entry.OutputIndex >= 0 && InNonPDGBakedOutputs) + { + if (Entry.OutputIndex >= 0 && InNonPDGBakedOutputs->IsValidIndex(Entry.OutputIndex)) + { + BakedOutputObject = (*InNonPDGBakedOutputs)[Entry.OutputIndex].BakedOutputObjects.Find(Entry.OutputObjectIdentifier); + } + } + + if (BakedActorMap.Contains(Actor)) + { + // Record the blueprint as the previous bake blueprint and clear the info of the temp bake actor/component + if (BakedOutputObject) + { + UBlueprint* const BakedBlueprint = BakedActorMap[Actor]; + if (BakedBlueprint) + BakedOutputObject->Blueprint = FSoftObjectPath(BakedBlueprint).ToString(); + else + BakedOutputObject->Blueprint.Empty(); + BakedOutputObject->Actor.Empty(); + // TODO: Set the baked component to the corresponding component in the blueprint? + BakedOutputObject->BakedComponent.Empty(); + } + continue; + } + + // Add a placeholder entry since we've started processing the actor, we'll replace the null with the blueprint + // if successful and leave it as null if the bake fails (the actor will then be skipped if it appears in the + // array again). + BakedActorMap.Add(Actor, nullptr); + + UObject* Asset = nullptr; + + // Recenter the actor to its bounding box center + if (BakeSettings.bRecenterBakedActors) + CenterActorToBoundingBoxCenter(Actor); + + // Create package for out Blueprint + FString BlueprintName; + + // For instancers we determine the bake folder from the instancer, + // for everything else we use the baked object's bake folder + // If all of that is blank, we fall back to InBakeFolder. + FString BakeFolderPath; + if (Entry.bInstancerOutput) + BakeFolderPath = Entry.InstancerPackageParams.BakeFolder; + else + BakeFolderPath = Entry.BakeFolderPath; + if (BakeFolderPath.IsEmpty()) + BakeFolderPath = InBakeFolder.Path; + + FHoudiniPackageParams PackageParams; + // Set the replace mode based on if we are doing a replacement or incremental asset bake + const EPackageReplaceMode AssetPackageReplaceMode = BakeSettings.bReplaceAssets ? + EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; + FHoudiniEngineUtils::FillInPackageParamsForBakingOutput( + PackageParams, + FHoudiniOutputObjectIdentifier(), + BakeFolderPath, + Entry.ActorBakeName.ToString() + "_BP", + InHoudiniAssetName, + InHoudiniAssetActorName, + AssetPackageReplaceMode); + + if (BakedOutputObject) + { + UBlueprint* const PreviousBlueprint = BakedOutputObject->GetBlueprintIfValid(); + if (IsValid(PreviousBlueprint)) + { + if (PackageParams.MatchesPackagePathNameExcludingBakeCounter(PreviousBlueprint)) + { + PackageParams.GetBakeCounterFromBakedAsset(PreviousBlueprint, BakeCounter); + } + } + } + + UPackage* Package = PackageParams.CreatePackageForObject(BlueprintName, BakeCounter); + + if (!IsValid(Package)) + { + HOUDINI_LOG_WARNING(TEXT("Could not find or create a package for the blueprint of %s"), *(Actor->GetPathName())); + continue; + } + + BakedObjectData.BakeStats.NotifyPackageCreated(1); + + if (!Package->IsFullyLoaded()) + Package->FullyLoad(); + + //Blueprint = FKismetEditorUtilities::CreateBlueprintFromActor(*BlueprintName, Package, Actor, false); + // Find existing asset first (only relevant if we are in replacement mode). If the existing asset has a + // different base class than the incoming actor, we reparent the blueprint to the new base class before + // clearing the SCS graph and repopulating it from the temp actor. + Asset = StaticFindObjectFast(UBlueprint::StaticClass(), Package, FName(*BlueprintName)); + if (IsValid(Asset)) + { + UBlueprint* Blueprint = Cast(Asset); + if (IsValid(Blueprint)) + { + if (Blueprint->GeneratedClass && Blueprint->GeneratedClass != Actor->GetClass()) + { + // Close editors opened on existing asset if applicable + if (Asset && bIsAssetEditorSubsystemValid && AssetEditorSubsystem->FindEditorForAsset(Asset, false) != nullptr) + { + AssetEditorSubsystem->CloseAllEditorsForAsset(Asset); + AssetsToReOpenEditors.Add(Asset); + } + + Blueprint->ParentClass = Actor->GetClass(); + + FBlueprintEditorUtils::RefreshAllNodes(Blueprint); + FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint); + FKismetEditorUtilities::CompileBlueprint(Blueprint); + } + } + } + else if (Asset && !IsValid(Asset)) + { + // Rename to pending kill so that we can use the desired name + const FString AssetPendingKillName(BlueprintName + "_PENDING_KILL"); + RenameAsset(Asset, AssetPendingKillName, true); + Asset = nullptr; + } + + bool bCreatedNewBlueprint = false; + if (!Asset) + { + UBlueprintFactory* Factory = NewObject(); + Factory->ParentClass = Actor->GetClass(); + + FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + + Asset = AssetToolsModule.Get().CreateAsset( + BlueprintName, PackageParams.GetPackagePath(), + UBlueprint::StaticClass(), Factory, FName("ContentBrowserNewAsset")); + + if (Asset) + bCreatedNewBlueprint = true; + } + + UBlueprint* Blueprint = Cast(Asset); + + if (!IsValid(Blueprint)) + { + HOUDINI_LOG_WARNING( + TEXT("Found an asset at %s/%s, but it was not a blueprint or was pending kill."), + *(InBakeFolder.Path), *BlueprintName); + + continue; + } + + if (bCreatedNewBlueprint) + { + BakedObjectData.BakeStats.NotifyObjectsCreated(Blueprint->GetClass()->GetName(), 1); + } + else + { + BakedObjectData.BakeStats.NotifyObjectsUpdated(Blueprint->GetClass()->GetName(), 1); + } + + // Close editors opened on existing asset if applicable + if (Blueprint && bIsAssetEditorSubsystemValid && AssetEditorSubsystem->FindEditorForAsset(Blueprint, false) != nullptr) + { + AssetEditorSubsystem->CloseAllEditorsForAsset(Blueprint); + AssetsToReOpenEditors.Add(Blueprint); + } + + // Record the blueprint as the previous bake blueprint and clear the info of the temp bake actor/component + if (BakedOutputObject) + { + BakedOutputObject->Blueprint = FSoftObjectPath(Blueprint).ToString(); + BakedOutputObject->Actor.Empty(); + // TODO: Set the baked component to the corresponding component in the blueprint? + BakedOutputObject->BakedComponent.Empty(); + } + + BakedObjectData.Blueprints.Add(Blueprint); + BakedActorMap[Actor] = Blueprint; + + // Clear old Blueprint Node tree + if (Blueprint) + { + USimpleConstructionScript* SCS = Blueprint->SimpleConstructionScript; + + int32 NodeSize = SCS->GetAllNodes().Num(); + for (int32 n = NodeSize - 1; n >= 0; --n) + SCS->RemoveNode(SCS->GetAllNodes()[n]); + } + + constexpr bool bRenameComponents = true; + FHoudiniEngineBakeUtils::CopyActorContentsToBlueprint(Actor, Blueprint, bRenameComponents); + + // Save the created BP package. + Package->MarkPackageDirty(); + BakedObjectData.PackagesToSave.Add(Package); + } + + // Destroy the actors that were baked + for (const auto& BakedActorEntry : BakedActorMap) + { + AActor* const Actor = BakedActorEntry.Key; + if (!IsValid(Actor)) + continue; + + UWorld* World = Actor->GetWorld(); + if (!World) + World = GWorld; + + if (World) + World->EditorDestroyActor(Actor, true); + } + + // Re-open asset editors for updated blueprints that were open in editors + if (bIsAssetEditorSubsystemValid && AssetsToReOpenEditors.Num() > 0) + { + for (UObject* Asset : AssetsToReOpenEditors) + { + if (IsValid(Asset)) + { + AssetEditorSubsystem->OpenEditorForAsset(Asset); + } + } + } + + return true; +} + +bool +FHoudiniEngineBakeUtils::BakePDGTOPNodeBlueprints( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InNode, + bool bInIsAutoBake, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors, + FHoudiniBakedObjectData& BakedObjectData) +{ + TArray BPActors; + + if (!IsValid(InPDGAssetLink)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineBakeUtils::BakePDGBlueprint]: InPDGAssetLink is null")); + return false; + } + + if (!IsValid(InNode)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineBakeUtils::BakePDGBlueprint]: InNode is null")); + return false; + } + + FHoudiniBakeSettings BakeSettings; + BakeSettings.bReplaceAssets = InPDGBakePackageReplaceMode == EPDGBakePackageReplaceModeOption::ReplaceExistingAssets; + BakeSettings.bRecenterBakedActors = bInRecenterBakedActors; + + // Bake PDG output to new actors + // bInBakeForBlueprint == true will skip landscapes and instanced actor components + const bool bInBakeForBlueprint = true; + TArray BakedActors; + bool bSuccess = BakePDGTOPNodeOutputsKeepActors( + InPDGAssetLink, + InNode, + bInBakeForBlueprint, + bInIsAutoBake, + InPDGBakePackageReplaceMode, + BakedActors, + BakedObjectData); + + if (bSuccess) + { + AActor* OwnerActor = InPDGAssetLink->GetOwnerActor(); + bSuccess = BakeBlueprintsFromBakedActors( + BakedActors, + BakeSettings, + InPDGAssetLink->AssetName, + IsValid(OwnerActor) ? OwnerActor->GetActorNameOrLabel() : FString(), + InPDGAssetLink->BakeFolder, + nullptr, + &InNode->GetBakedWorkResultObjectsOutputs(), + BakedObjectData); + } + + return bSuccess; +} + +bool +FHoudiniEngineBakeUtils::BakePDGTOPNodeBlueprints( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InTOPNode, + bool bInIsAutoBake, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors) +{ + FHoudiniBakedObjectData BakedObjectData; + + if (!IsValid(InPDGAssetLink)) + return false; + + const bool bSuccess = BakePDGTOPNodeBlueprints( + InPDGAssetLink, + InTOPNode, + bInIsAutoBake, + InPDGBakePackageReplaceMode, + bInRecenterBakedActors, + BakedObjectData); + + // Compile the new/updated blueprints + for (UBlueprint* const Blueprint : BakedObjectData.Blueprints) + { + if (!IsValid(Blueprint)) + continue; + + FKismetEditorUtilities::CompileBlueprint(Blueprint); + } + FHoudiniEngineBakeUtils::SaveBakedPackages(BakedObjectData.PackagesToSave); + + // Sync the CB to the baked objects + if(GEditor && BakedObjectData.Blueprints.Num() > 0) + { + TArray Assets; + Assets.Reserve(BakedObjectData.Blueprints.Num()); + for (UBlueprint* Blueprint : BakedObjectData.Blueprints) + { + Assets.Add(Blueprint); + } + GEditor->SyncBrowserToObjects(Assets); + } + + UHoudiniCookable* Cookable = Cast(InPDGAssetLink->GetOuter()); + bool bDoNotifications = Cookable ? Cookable->GetDoSlateNotifications() : true; + + if (bDoNotifications) + { + const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); + FString Msg = FString::Format(*FinishedTemplate, { BakedObjectData.BakeStats.NumPackagesCreated, BakedObjectData.BakeStats.NumPackagesUpdated } ); + FHoudiniEngine::Get().FinishTaskSlateNotification( FText::FromString(Msg) ); + } + + TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); + + return bSuccess; +} + +bool +FHoudiniEngineBakeUtils::BakePDGTOPNetworkBlueprints( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNetwork* InNetwork, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors, + FHoudiniBakedObjectData& BakedObjectData) +{ + if (!IsValid(InPDGAssetLink)) + return false; + + if (!IsValid(InNetwork)) + return false; + + const bool bIsAutoBake = false; + bool bSuccess = true; + for (UTOPNode* Node : InNetwork->AllTOPNodes) + { + if (!IsValid(Node)) + continue; + + bSuccess &= BakePDGTOPNodeBlueprints(InPDGAssetLink, Node, bIsAutoBake, InPDGBakePackageReplaceMode, bInRecenterBakedActors, BakedObjectData); + } + + return bSuccess; +} + +bool +FHoudiniEngineBakeUtils::BakePDGAssetLinkBlueprints( + UHoudiniPDGAssetLink* InPDGAssetLink, + const EPDGBakeSelectionOption InBakeSelectionOption, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors) +{ + FHoudiniBakedObjectData BakedObjectData; + + return BakePDGAssetLinkBlueprints(InPDGAssetLink, InBakeSelectionOption, InPDGBakePackageReplaceMode, bInRecenterBakedActors, BakedObjectData); +} + +bool +FHoudiniEngineBakeUtils::BakePDGAssetLinkBlueprints( + UHoudiniPDGAssetLink* InPDGAssetLink, + const EPDGBakeSelectionOption InBakeSelectionOption, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors, + FHoudiniBakedObjectData& BakedObjectData) +{ + + if (!IsValid(InPDGAssetLink)) + return false; + + const bool bIsAutoBake = false; + bool bSuccess = true; + switch(InBakeSelectionOption) + { + case EPDGBakeSelectionOption::All: + for (UTOPNetwork* Network : InPDGAssetLink->AllTOPNetworks) + { + if (!IsValid(Network)) + continue; + + for (UTOPNode* Node : Network->AllTOPNodes) + { + if (!IsValid(Node)) + continue; + + bSuccess &= BakePDGTOPNodeBlueprints( + InPDGAssetLink, + Node, + bIsAutoBake, + InPDGBakePackageReplaceMode, + bInRecenterBakedActors, + BakedObjectData); + } + } + break; + + case EPDGBakeSelectionOption::SelectedNetwork: + bSuccess &= BakePDGTOPNetworkBlueprints( + InPDGAssetLink, + InPDGAssetLink->GetSelectedTOPNetwork(), + InPDGBakePackageReplaceMode, + bInRecenterBakedActors, + BakedObjectData); + break; + + case EPDGBakeSelectionOption::SelectedNode: + bSuccess &= BakePDGTOPNodeBlueprints( + InPDGAssetLink, + InPDGAssetLink->GetSelectedTOPNode(), + bIsAutoBake, + InPDGBakePackageReplaceMode, + bInRecenterBakedActors, + BakedObjectData); + break; + } + + // Compile the new/updated blueprints + for (UBlueprint* const Blueprint : BakedObjectData.Blueprints) + { + if (!IsValid(Blueprint)) + continue; + + FKismetEditorUtilities::CompileBlueprint(Blueprint); + } + FHoudiniEngineBakeUtils::SaveBakedPackages(BakedObjectData.PackagesToSave); + + // Sync the CB to the baked objects + if(GEditor && BakedObjectData.Blueprints.Num() > 0) + { + TArray Assets; + Assets.Reserve(BakedObjectData.Blueprints.Num()); + for (UBlueprint* Blueprint : BakedObjectData.Blueprints) + { + Assets.Add(Blueprint); + } + GEditor->SyncBrowserToObjects(Assets); + } + + UHoudiniCookable* Cookable = Cast(InPDGAssetLink->GetOuter()); + bool bDoNotifications = Cookable ? Cookable->GetDoSlateNotifications() : true; + + if (bDoNotifications) + { + const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); + FString Msg = FString::Format(*FinishedTemplate, { BakedObjectData.BakeStats.NumPackagesCreated, BakedObjectData.BakeStats.NumPackagesUpdated } ); + FHoudiniEngine::Get().FinishTaskSlateNotification( FText::FromString(Msg) ); + } + + TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); + + // Broadcast that the bake is complete + InPDGAssetLink->HandleOnPostBake(bSuccess); + + return bSuccess; +} + +bool +FHoudiniEngineBakeUtils::FindOrCreateDesiredLevelFromLevelPath( + const FString& InLevelPath, + ULevel*& OutDesiredLevel, + UWorld*& OutDesiredWorld, + bool& OutCreatedPackage) +{ + OutDesiredLevel = nullptr; + OutDesiredWorld = nullptr; + if (InLevelPath.IsEmpty()) + { + OutDesiredWorld = GWorld; + OutDesiredLevel = GWorld->GetCurrentLevel(); + } + else + { + OutCreatedPackage = false; + + UWorld* FoundWorld = nullptr; + ULevel* FoundLevel = nullptr; + bool bActorInWorld = false; + if (FHoudiniEngineUtils::FindWorldAndLevelForSpawning( + GWorld, + InLevelPath, + true, + FoundWorld, + FoundLevel, + OutCreatedPackage, + bActorInWorld)) + { + OutDesiredLevel = FoundLevel; + OutDesiredWorld = FoundWorld; + } + } + + return ((OutDesiredWorld != nullptr) && (OutDesiredLevel != nullptr)); +} + + +bool +FHoudiniEngineBakeUtils::FindBakedActor( + const FString& InBakeActorName, + const TSubclassOf& InBakeActorClass, + ULevel* InLevel, + AActor*& OutActor, + bool bInNoPendingKillActors, + bool bRenamePendingKillActor) +{ + OutActor = nullptr; + + if (!IsValid(InLevel)) + return false; + + UWorld* const World = InLevel->GetWorld(); + if (!IsValid(World)) + return false; + + // Look for an actor with the given name in the world + const FName BakeActorFName(InBakeActorName); + const TSubclassOf ActorClass = IsValid(InBakeActorClass.Get()) ? InBakeActorClass.Get() : AActor::StaticClass(); + AActor* FoundActor = Cast(StaticFindObjectFast(ActorClass.Get(), InLevel, BakeActorFName)); + + // If we found an actor and it is pending kill, rename it and don't use it + if (FoundActor) + { + if (!IsValid(FoundActor)) + { + if (bRenamePendingKillActor) + { + RenameAndRelabelActor( + FoundActor, + *MakeUniqueObjectNameIfNeeded( + FoundActor->GetOuter(), + FoundActor->GetClass(), + FoundActor->GetActorNameOrLabel() + "_Pending_Kill", + FoundActor), + false); + } + if (bInNoPendingKillActors) + FoundActor = nullptr; + else + OutActor = FoundActor; + } + else + { + OutActor = FoundActor; + } + } + + return true; +} + +void FHoudiniEngineBakeUtils::FindUnrealBakeActor( + const FHoudiniOutputObject& InOutputObject, + const FHoudiniBakedOutputObject& InBakedOutputObject, + const TArray& InAllBakedActors, + ULevel* InLevel, + FName InDefaultActorName, + const FHoudiniBakeSettings& BakeSettings, + AActor* InFallbackActor, + AActor*& OutFoundActor, + bool& bOutHasBakeActorName, + FName& OutBakeActorName) +{ + // Determine the desired actor class via unreal_bake_actor_class + TSubclassOf BakeActorClass = GetBakeActorClassOverride(InOutputObject); + if (!IsValid(BakeActorClass.Get())) + BakeActorClass = AActor::StaticClass(); + + // Determine desired actor name via unreal_bake_actor, fallback to InDefaultActorName + OutBakeActorName = NAME_None; + OutFoundActor = nullptr; + bOutHasBakeActorName = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_BAKE_ACTOR); + + FString BakeActorNameStr; + + if (bOutHasBakeActorName) + { + BakeActorNameStr = InOutputObject.CachedAttributes[HAPI_UNREAL_ATTRIB_BAKE_ACTOR]; + } + + if (BakeSettings.ActorBakeOption == EHoudiniEngineActorBakeOption::OneActorPerHDA) + { + BakeActorNameStr = InDefaultActorName.ToString(); + bOutHasBakeActorName = true; + } + + if (bOutHasBakeActorName) + { + if (BakeActorNameStr.IsEmpty()) + { + OutBakeActorName = NAME_None; + bOutHasBakeActorName = false; + } + else + { + OutBakeActorName = FName(BakeActorNameStr, NAME_NO_NUMBER_INTERNAL); + // We have a bake actor name, look for the actor + AActor* BakeNameActor = nullptr; + if (FindBakedActor(BakeActorNameStr, BakeActorClass, InLevel, BakeNameActor)) + { + // Found an actor with that name, check that we "own" it (we created in during baking previously) + AActor* IncrementedBakedActor = nullptr; + for (const FHoudiniEngineBakedActor& BakedActor : InAllBakedActors) + { + if (!IsValid(BakedActor.Actor)) + continue; + // Ensure the class is of the appropriate type + if (!BakedActor.Actor->IsA(BakeActorClass.Get())) + continue; + if (BakedActor.Actor == BakeNameActor) + { + OutFoundActor = BakeNameActor; + break; + } + else if (!IncrementedBakedActor && BakedActor.ActorBakeName == OutBakeActorName) + { + // Found an actor we have baked named OutBakeActorName_# (incremental version of our desired name) + IncrementedBakedActor = BakedActor.Actor; + } + } + if (!OutFoundActor && IncrementedBakedActor) + OutFoundActor = IncrementedBakedActor; + } + } + } + + // If unreal_bake_actor is not set, or is blank, fallback to InDefaultActorName + if (!bOutHasBakeActorName || (OutBakeActorName.IsNone() || OutBakeActorName.ToString().TrimStartAndEnd().IsEmpty())) + { + OutBakeActorName = InDefaultActorName; + } + + if (!OutFoundActor) + { + // If in replace mode, use previous bake actor if valid and in InLevel + if (BakeSettings.bReplaceActors) + { + const FSoftObjectPath PrevActorPath(InBakedOutputObject.Actor); + const FString ActorPath = PrevActorPath.IsSubobject() + ? PrevActorPath.GetAssetPathString() + ":" + PrevActorPath.GetSubPathString() + : PrevActorPath.GetAssetPathString(); + const FString LevelPath = IsValid(InLevel) ? InLevel->GetPathName() : ""; + if (PrevActorPath.IsValid() && (LevelPath.IsEmpty() || ActorPath.StartsWith(LevelPath))) + { + AActor* PrevBakedActor = InBakedOutputObject.GetActorIfValid(); + if (IsValid(PrevBakedActor) && PrevBakedActor->IsA(BakeActorClass.Get())) + OutFoundActor = PrevBakedActor; + } + } + + // Fallback to InFallbackActor if valid and in InLevel + if (!OutFoundActor && IsValid(InFallbackActor) && (!InLevel || InFallbackActor->GetLevel() == InLevel)) + { + if (IsValid(InFallbackActor) && InFallbackActor->IsA(BakeActorClass.Get())) + OutFoundActor = InFallbackActor; + } + } +} + +AActor* +FHoudiniEngineBakeUtils::FindExistingActor_Bake( + UWorld* InWorld, + UHoudiniOutput* InOutput, + const FString& InActorName, + const FString& InPackagePath, + UWorld*& OutWorld, + ULevel*& OutLevel, + bool& bCreatedPackage) +{ + bCreatedPackage = false; + + // Try to Locate a previous actor + AActor* FoundActor = FHoudiniEngineUtils::FindOrRenameInvalidActor(InWorld, InActorName, FoundActor); + if (FoundActor) + FoundActor->Destroy(); // nuke it! + + if (FoundActor) + { + // TODO: make sure that the found is actor is actually assigned to the level defined by package path. + // If the found actor is not from that level, it should be moved there. + + OutWorld = FoundActor->GetWorld(); + OutLevel = FoundActor->GetLevel(); + } + else + { + // Actor is not present, BUT target package might be loaded. Find the appropriate World and Level for spawning. + bool bActorInWorld = false; + const bool bResult = FHoudiniEngineUtils::FindWorldAndLevelForSpawning( + InWorld, + InPackagePath, + true, + OutWorld, + OutLevel, + bCreatedPackage, + bActorInWorld); + + if (!bResult) + { + return nullptr; + } + + if (!bActorInWorld) + { + // The OutLevel is not present in the current world which means we might + // still find the tile actor in OutWorld. + FoundActor = FHoudiniEngineRuntimeUtils::FindActorInWorldByLabelOrName(OutWorld, InActorName); + } + } + + return FoundActor; +} + +bool +FHoudiniEngineBakeUtils::CheckForAndRefineHoudiniProxyMesh( + UHoudiniCookable* InCookable, + bool bInReplacePreviousBake, + EHoudiniEngineBakeOption InBakeOption, + bool bInRemoveHACOutputOnSuccess, + bool bInRecenterBakedActors, + bool& bOutNeedsReCook) +{ + if (!IsValid(InCookable)) + return false; + + // Handle proxies: if the output has any current proxies, first refine them + bOutNeedsReCook = false; + if (InCookable->HasAnyCurrentProxyOutput()) + { + bool bNeedsRebuildOrDelete; + bool bInvalidState; + const bool bCookedDataAvailable = InCookable->IsHoudiniCookedDataAvailable(bNeedsRebuildOrDelete, bInvalidState); + + if (bCookedDataAvailable) + { + // Cook data is available, refine the mesh + AHoudiniAssetActor* HoudiniActor = Cast(InCookable->GetOwner()); + if (IsValid(HoudiniActor)) + { + FHoudiniEngineUtils::RefineHoudiniProxyMeshActorArrayToStaticMeshes({ HoudiniActor }); + } + } + else if (!bNeedsRebuildOrDelete && !bInvalidState) + { + // A cook is needed: request the cook, but with no proxy and with a bake after cook + InCookable->SetNoProxyMeshNextCookRequested(true); + // Only + if (!InCookable->IsBakeAfterNextCookEnabled()) + InCookable->SetBakeAfterNextCook(EHoudiniBakeAfterNextCook::Once); + + InCookable->MarkAsNeedCook(); + + bOutNeedsReCook = true; + + // The cook has to complete first (asynchronously) before the bake can happen + // The SetBakeAfterNextCookEnabled flag will result in a bake after cook + return false; + } + else + { + // The HC is in an unsupported state + const EHoudiniAssetState CurrentState = InCookable->GetCurrentState(); + HOUDINI_LOG_ERROR(TEXT("Could not refine (in order to bake) %s, the asset is in an unsupported state: %s"), *(InCookable->GetPathName()), *(UEnum::GetValueAsString(CurrentState))); + return false; + } + } + + return true; +} + +void +FHoudiniEngineBakeUtils::CenterActorToBoundingBoxCenter(AActor* InActor) +{ + if (!IsValid(InActor)) + return; + + USceneComponent * const RootComponent = InActor->GetRootComponent(); + if (!IsValid(RootComponent)) + return; + + const bool bOnlyCollidingComponents = false; + const bool bIncludeFromChildActors = true; + + // InActor->GetActorBounds(bOnlyCollidingComponents, Origin, BoxExtent, bIncludeFromChildActors); + FBox Box(ForceInit); + InActor->ForEachComponent(bIncludeFromChildActors, [&](const UPrimitiveComponent* InPrimComp) + { + // Only use non-editor-only components for the bounds calculation (to exclude things like editor only sprite/billboard components) + if (InPrimComp->IsRegistered() && !InPrimComp->IsEditorOnly() && + (!bOnlyCollidingComponents || InPrimComp->IsCollisionEnabled())) + { + Box += InPrimComp->Bounds.GetBox(); + } + }); + + FVector3d Origin; + FVector3d BoxExtent; + Box.GetCenterAndExtents(Origin, BoxExtent); + + const FVector3d Delta = Origin - RootComponent->GetComponentLocation(); + // Actor->SetActorLocation(Origin); + RootComponent->SetWorldLocation(Origin); + + for (USceneComponent* SceneComponent : RootComponent->GetAttachChildren()) + { + if (!IsValid(SceneComponent)) + continue; + + SceneComponent->SetWorldLocation(SceneComponent->GetComponentLocation() - Delta); + } +} + +void +FHoudiniEngineBakeUtils::CenterActorsToBoundingBoxCenter(const TArray& InActors) +{ + for (AActor* Actor : InActors) + { + if (!IsValid(Actor)) + continue; + + CenterActorToBoundingBoxCenter(Actor); + } +} + +USceneComponent* +FHoudiniEngineBakeUtils::GetActorRootComponent(AActor* InActor, bool bCreateIfMissing, EComponentMobility::Type InMobilityIfCreated) +{ + USceneComponent* RootComponent = InActor->GetRootComponent(); + if (!IsValid(RootComponent)) + { + RootComponent = NewObject(InActor, USceneComponent::GetDefaultSceneRootVariableName(), RF_Transactional); + + // Change the creation method so the component is listed in the details panels + InActor->SetRootComponent(RootComponent); + InActor->AddInstanceComponent(RootComponent); + RootComponent->RegisterComponent(); + RootComponent->SetMobility(InMobilityIfCreated); + } + + return RootComponent; +} + +FString +FHoudiniEngineBakeUtils::MakeUniqueObjectNameIfNeeded(UObject* InOuter, const UClass* InClass, const FString& InName, UObject* InObjectThatWouldBeRenamed) +{ + if (IsValid(InObjectThatWouldBeRenamed)) + { + const FName CurrentName = InObjectThatWouldBeRenamed->GetFName(); + if (CurrentName.ToString() == InName) + return InName; + + // Check if the prefix matches (without counter suffix) the new name + // In other words, if InName is 'my_actor' and the object is already an increment of it, 'my_actor_5' then + // don't we can just keep the current name + if (CurrentName.GetPlainNameString() == InName) + return CurrentName.ToString(); + } + + UObject* ExistingObject = nullptr; + FName CandidateName(InName); + bool bAppendedNumber = false; + // Do our own loop for generate suffixes as sequentially as possible. If this turns out to be expensive we can + // revert to MakeUniqueObjectName. + // return MakeUniqueObjectName(InOuter, InClass, CandidateName).ToString(); + do + { + if (!IsValid(InOuter)) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + // UE5.1 deprecated ANY_PACKAGE + ExistingObject = StaticFindFirstObject(nullptr, *(CandidateName.ToString()), EFindFirstObjectOptions::NativeFirst); +#else + ExistingObject = StaticFindObject(nullptr, ANY_PACKAGE, *(CandidateName.ToString())); +#endif + } + else + { + ExistingObject = StaticFindObjectFast(nullptr, InOuter, CandidateName); + } + + if (ExistingObject) + { + // We don't want to create unique names when actors are saved in their own package + // because we don't care about the name, we only care about the label. + AActor* ExistingActor = Cast(ExistingObject); + AActor* RenamedActor = Cast(InObjectThatWouldBeRenamed); + if (ExistingActor && ExistingActor->IsPackageExternal() && RenamedActor && RenamedActor->IsPackageExternal()) + { + return InName; + } + + if (!bAppendedNumber) + { + const bool bSplitName = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + CandidateName = FName(*InName, NAME_EXTERNAL_TO_INTERNAL(1), bSplitName); +#else + CandidateName = FName(*InName, NAME_EXTERNAL_TO_INTERNAL(1), FNAME_Add, bSplitName); +#endif + bAppendedNumber = true; + } + else + { + CandidateName.SetNumber(CandidateName.GetNumber() + 1); + } + // CandidateName = FString::Printf(TEXT("%s_%d"), *InName, ++Counter); + } + } while (ExistingObject); + + return CandidateName.ToString(); +} + +FName +FHoudiniEngineBakeUtils::GetOutlinerFolderPath(const FHoudiniAttributeResolver& Resolver, FName DefaultFolder) +{ + FString ResolvedString = Resolver.ResolveAttribute( HAPI_UNREAL_ATTRIB_BAKE_OUTLINER_FOLDER, DefaultFolder.ToString(), true); + return FName(ResolvedString); +} + +bool +FHoudiniEngineBakeUtils::SetOutlinerFolderPath(AActor* InActor, FName Folder) +{ + if (!IsValid(InActor)) + return false; + + InActor->SetFolderPath(Folder); + return true; +} + +uint32 +FHoudiniEngineBakeUtils::DestroyPreviousBakeOutput( + FHoudiniBakedOutputObject& InBakedOutputObject, + bool bInDestroyBakedComponent, + bool bInDestroyBakedInstancedActors, + bool bInDestroyBakedInstancedComponents) +{ + uint32 NumDeleted = 0; + + if (bInDestroyBakedComponent) + { + UActorComponent* Component = Cast(InBakedOutputObject.GetBakedComponentIfValid()); + if (Component) + { + if (RemovePreviouslyBakedComponent(Component)) + { + InBakedOutputObject.BakedComponent = nullptr; + NumDeleted++; + } + } + } + + if (bInDestroyBakedInstancedActors) + { + for (const FString& ActorPathStr : InBakedOutputObject.InstancedActors) + { + const FSoftObjectPath ActorPath(ActorPathStr); + + if (!ActorPath.IsValid()) + continue; + + AActor* Actor = Cast(ActorPath.TryLoad()); + if (IsValid(Actor)) + { + UWorld* World = Actor->GetWorld(); + if (IsValid(World)) + { +#if WITH_EDITOR + World->EditorDestroyActor(Actor, true); +#else + World->DestroyActor(Actor); +#endif + NumDeleted++; + } + } + } + InBakedOutputObject.InstancedActors.Empty(); + } + + if (bInDestroyBakedInstancedComponents) + { + for (const FString& ComponentPathStr : InBakedOutputObject.InstancedComponents) + { + const FSoftObjectPath ComponentPath(ComponentPathStr); + + if (!ComponentPath.IsValid()) + continue; + + UActorComponent* Component = Cast(ComponentPath.TryLoad()); + if (IsValid(Component)) + { + if (RemovePreviouslyBakedComponent(Component)) + NumDeleted++; + } + } + InBakedOutputObject.InstancedComponents.Empty(); + } + + return NumDeleted; +} + +UMaterialInterface* +FHoudiniEngineBakeUtils::BakeSingleMaterialToPackage( + UMaterialInterface* InOriginalMaterial, + const FHoudiniPackageParams& InPackageParams, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedMaterialsMap) +{ + if (!IsValid(InOriginalMaterial)) + { + return nullptr; + } + + // We only deal with materials. + if (!InOriginalMaterial->IsA(UMaterial::StaticClass()) && !InOriginalMaterial->IsA(UMaterialInstance::StaticClass())) + { + return nullptr; + } + + FString MaterialName = InOriginalMaterial->GetName(); + + // Duplicate material resource. + UMaterialInterface * DuplicatedMaterial = FHoudiniEngineBakeUtils::DuplicateMaterialAndCreatePackage( + InOriginalMaterial, nullptr, MaterialName, InPackageParams, BakedObjectData, InOutAlreadyBakedMaterialsMap); + + if (!IsValid(DuplicatedMaterial)) + return nullptr; + + return DuplicatedMaterial; +} + +bool +FHoudiniEngineBakeUtils::BakeTexture( + UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData) +{ + if ((InOutputIndex < 0) || !InAllOutputs.IsValidIndex(InOutputIndex)) + return false; + + // Get previously cooked output. + UHoudiniOutput* CookedOutput = InAllOutputs[InOutputIndex]; + if (!IsValid(CookedOutput)) + return false; + + + // Determine the relevant WorldContext based on the output owner + UWorld* WorldContext = InCookable ? InCookable->GetWorld() : GWorld; + + TArray& AllBakedOutputs = InCookable->GetBakedOutputs(); + + /* + // Check if we have previously baked this object on this Cookable + FHoudiniBakedOutputObject BakedObjectEntry; + if (InOutputIndex >= 0 && AllBakedOutputs.IsValidIndex(InOutputIndex)) + { + FHoudiniBakedOutputObject const* const PrevBakedOutputObject = AllBakedOutputs[InOutputIndex].BakedOutputObjects.Find(CookedOutput->GetIdentifier()); + if (PrevBakedOutputObject) + { + bHasPreviousBakeData = true; + BakedObjectEntry = *PrevBakedOutputObject; + } + } + */ + + const FString HoudiniAssetName = InCookable->GetHoudiniAssetName(); + const FString HoudiniAssetActorName = IsValid(InCookable->GetOwner()) ? InCookable->GetOwner()->GetActorNameOrLabel() : TEXT(""); + const bool bAutomaticallySetAttemptToLoadMissingPackages = true; + const bool bSkipBakeFolderResolutionAndUseDefault = false; + + /* + // First get names from previous bake entry + if (bHasPreviousBakeData) + { + UObject const* const PrevObject = BakedObjectEntry.GetBakedObjectIfValid(); + if (PrevObject) + { + DefaultObjectName = PrevObject->GetName(); + } + else + { + AActor const* const PrevActor = BakedObjectEntry.GetActorIfValid(); + if (PrevActor) + DefaultObjectName = PrevActor->GetName(); + } + } + + // fallback to name from BakedOutputObject + if (DefaultObjectName.IsEmpty()) + DefaultObjectName = BakedOutputObject->GetName(); + */ + + + FHoudiniBakedObjectData NewBakeOutput; + bool bReturn = false; + //const FString DefaultObjectName = TEXT("Default"); + for (auto& It : CookedOutput->GetOutputObjects()) + { + if (!IsValid(It.Value.OutputObject)) + continue; + + FHoudiniOutputObject& OutputObject = It.Value; + if (!OutputObject.OutputObject->IsA()) + continue; + + UTexture2D* Texture = Cast(OutputObject.OutputObject); + if (!Texture) + continue; + + FDirectoryPath BakeFolder = InBakeFolder; + FString* Attribute = OutputObject.CachedAttributes.Find(HAPI_UNREAL_ATTRIB_BAKE_FOLDER); + if (Attribute != nullptr) + { + BakeFolder.Path = *Attribute; + } + + FString ObjectName = FHoudiniPackageParams::GetPackageNameExcludingGUID(Texture); + if (FString* Value = OutputObject.CachedAttributes.Find(HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2)) + { + ObjectName = *Value; + } + + FHoudiniOutputObjectIdentifier OutputObjectId = It.Key; + + // Initialize the baked output object entry (use the previous bake's data, if available). + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedOutputObject = InBakeState.MakeNewBakedOutputObject(InOutputIndex, OutputObjectId, bHasPreviousBakeData); + + const EPackageReplaceMode AssetPackageReplaceMode = BakeSettings.bReplaceAssets ? + EPackageReplaceMode::ReplaceExistingAssets : EPackageReplaceMode::CreateNewAssets; + + // Configure FHoudiniAttributeResolver and fill the package params with resolved object name and bake folder. + // The resolver is then also configured with the package params for subsequent resolving (level_path etc) + FHoudiniAttributeResolver Resolver; + FHoudiniPackageParams PackageParams; + FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( + WorldContext, + InCookable, + OutputObjectId, + OutputObject, + bHasPreviousBakeData, + ObjectName, + PackageParams, + Resolver, + BakeFolder.Path, + AssetPackageReplaceMode, + HoudiniAssetName, + HoudiniAssetActorName, + bAutomaticallySetAttemptToLoadMissingPackages, + false, + bSkipBakeFolderResolutionAndUseDefault); + + UTexture2D* PreviousTexture = Cast(BakedOutputObject.GetBakedObjectIfValid()); + UTexture2D* BakedTexture = FHoudiniEngineBakeUtils::BakeTextureToPackage(Texture, PackageParams, PreviousTexture); + if (!BakedTexture) + continue; + + // Record the baked object + BakedOutputObject.BakedObject = FSoftObjectPath(BakedTexture).ToString(); + // No component and no actor + BakedOutputObject.BakedComponent = nullptr; + BakedOutputObject.Actor = nullptr; + + // Record bake data + InBakeState.SetNewBakedOutputObject(InOutputIndex, OutputObjectId, BakedOutputObject); + + bReturn = true; + } + + return bReturn; +} + +UTexture2D* +FHoudiniEngineBakeUtils::BakeTextureToPackage( + UTexture2D* InOriginalTexture, + const FHoudiniPackageParams& InPackageParams, + UTexture2D* InPreviousBakeTexture) +{ + if (!IsValid(InOriginalTexture)) + { + return nullptr; + } + + // We only deal with textures. + if (!InOriginalTexture->IsA(UTexture2D::StaticClass())) + { + return nullptr; + } + + FString TextureName = InOriginalTexture->GetName(); + + // Duplicate the texture + FHoudiniBakedObjectData BakedObjectData; + UTexture2D* DuplicatedTexture = FHoudiniEngineBakeUtils::DuplicateTextureAndCreatePackage( + InOriginalTexture, InPreviousBakeTexture, TextureName, InPackageParams, BakedObjectData); + + if (!IsValid(DuplicatedTexture)) + return nullptr; + + FHoudiniEngineBakeUtils::SaveBakedPackages(BakedObjectData.PackagesToSave); + // Sync the CB to the baked objects + if (GEditor) + { + TArray Objects; + Objects.Add(DuplicatedTexture); + GEditor->SyncBrowserToObjects(Objects); + } + + return DuplicatedTexture; +} + +UClass* +FHoudiniEngineBakeUtils::GetBakeActorClassOverride(const FName& InActorClassName) +{ + if (InActorClassName.IsNone()) + return nullptr; + + // Try to the find the user specified actor class + const FString ActorClassNameString = *InActorClassName.ToString(); + UClass* FoundClass = LoadClass(nullptr, *ActorClassNameString); + if (!IsValid(FoundClass)) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + // UE5.1 deprecated ANY_PACKAGE + FoundClass = FindFirstObjectSafe(*ActorClassNameString, EFindFirstObjectOptions::NativeFirst); +#else + FoundClass = FindObjectSafe(ANY_PACKAGE, *ActorClassNameString); +#endif + } + + if (!IsValid(FoundClass)) + return nullptr; + + // The class must be a child of AActor + if (!FoundClass->IsChildOf()) + return nullptr; + + return FoundClass; +} + +UClass* +FHoudiniEngineBakeUtils::GetBakeActorClassOverride(const FHoudiniOutputObject& InOutputObject) +{ + // Find the unreal_bake_actor_class attribute in InOutputObject + const FName ActorClassName = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_BAKE_ACTOR_CLASS) ? + FName(InOutputObject.CachedAttributes.FindChecked(HAPI_UNREAL_ATTRIB_BAKE_ACTOR_CLASS)) : NAME_None; + return GetBakeActorClassOverride(ActorClassName); +} + +UActorFactory* +FHoudiniEngineBakeUtils::GetActorFactory( + const FName& InActorClassName, + const FHoudiniBakeSettings& BakeSettings, + TSubclassOf& OutActorClass, + const TSubclassOf& InFactoryClass, + UObject* const InAsset) +{ + if (!GEditor) + return nullptr; + + // If grouping components under one actor, choose an empty actor factory. + if (BakeSettings.ActorBakeOption == EHoudiniEngineActorBakeOption::OneActorPerHDA) + { + OutActorClass = GetBakeActorClassOverride(InActorClassName); + + UActorFactory* ActorFactory = GEditor->FindActorFactoryByClass(UActorFactoryClass::StaticClass()); + return ActorFactory; + + } + + // If InActorClassName is not blank, try to find an actor factory that spawns actors of this class. + OutActorClass = nullptr; + if (!InActorClassName.IsNone()) + { + UClass* ActorClass = GetBakeActorClassOverride(InActorClassName); + if (IsValid(ActorClass)) + { + OutActorClass = ActorClass; + UActorFactory* ActorFactory = GEditor->FindActorFactoryForActorClass(ActorClass); + if (IsValid(ActorFactory)) + return ActorFactory; + // If we could not find a factory that specifically spawns ActorClass, then fallback to the + // UActorFactoryClass factory + ActorFactory = GEditor->FindActorFactoryByClass(UActorFactoryClass::StaticClass()); + if (IsValid(ActorFactory)) + return ActorFactory; + } + } + + // If InActorClassName was blank, or we could not find a factory for it, + // Then if InFactoryClass was specified, try to find a factory of that class + UClass * ActorFactoryClass = InFactoryClass.Get(); + if (IsValid(ActorFactoryClass) && ActorFactoryClass != UActorFactoryEmptyActor::StaticClass()) + { + UActorFactory* ActorFactory = GEditor->FindActorFactoryByClass(ActorFactoryClass); + if (IsValid(ActorFactory)) + return ActorFactory; + } + + // If we couldn't find a factory via InActorClassName or InFactoryClass, then if InAsset was specified try to find + // a factory that spawns actors for InAsset + if (IsValid(InAsset)) + { + UActorFactory* ActorFactory = FActorFactoryAssetProxy::GetFactoryForAssetObject(InAsset); + if (IsValid(ActorFactory)) + return ActorFactory; + } + + if (IsValid(ActorFactoryClass)) + { + // Return the empty actor factory if we had ignored it above + UActorFactory* ActorFactory = GEditor->FindActorFactoryByClass(ActorFactoryClass); + if (IsValid(ActorFactory)) + return ActorFactory; + } + + HOUDINI_LOG_ERROR( + TEXT("[FHoudiniEngineBakeUtils::GetActorFactory] Could not find actor factory:\n\tunreal_bake_actor_class = %s\n\tfallback actor factory class = %s\n\tasset = %s"), + InActorClassName.IsNone() ? TEXT("not specified") : *InActorClassName.ToString(), + IsValid(InFactoryClass.Get()) ? *InFactoryClass->GetName() : TEXT("null"), + IsValid(InAsset) ? *InAsset->GetFullName() : TEXT("null")); + + return nullptr; +} + +UActorFactory* +FHoudiniEngineBakeUtils::GetActorFactory( + const FHoudiniOutputObject& InOutputObject, + const FHoudiniBakeSettings& BakeSettings, + TSubclassOf& OutActorClass, + const TSubclassOf& InFactoryClass, UObject* InAsset) +{ + // Find the unreal_bake_actor_class attribute in InOutputObject + const FName ActorClassName = InOutputObject.CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_BAKE_ACTOR_CLASS) ? + FName(InOutputObject.CachedAttributes.FindChecked(HAPI_UNREAL_ATTRIB_BAKE_ACTOR_CLASS)) : NAME_None; + return GetActorFactory(ActorClassName, BakeSettings, OutActorClass, InFactoryClass, InAsset); +} + +AActor* +FHoudiniEngineBakeUtils::SpawnBakeActor( + UActorFactory* InActorFactory, + UObject* InAsset, + ULevel* InLevel, + const FHoudiniBakeSettings & BakeSettings, + const FTransform& InTransform, + const USceneComponent* InOuterComponent, + const TSubclassOf& InActorClass, + const FActorSpawnParameters& InSpawnParams) +{ + if (!IsValid(InActorFactory)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineBakeUtils::SpawnBakeActor] Could not spawn an actor, since InActorFactory is nullptr.")); + return nullptr; + } + + AActor* SpawnedActor = nullptr; + + if (BakeSettings.ActorBakeOption == EHoudiniEngineActorBakeOption::OneActorPerHDA) + { + // If we are grouping components, just create a default actor. + UClass * ActorClass = InActorClass.Get(); + if (ActorClass == nullptr) + ActorClass = AActor::StaticClass(); + + SpawnedActor = InActorFactory->CreateActor(ActorClass, InLevel, InTransform, InSpawnParams); + + // Ensure there is a root component. It seems empty actor's don't have one. + if (SpawnedActor->GetRootComponent() == nullptr) + { + USceneComponent* RootComponent = NewObject(SpawnedActor, USceneComponent::GetDefaultSceneRootVariableName()); + SpawnedActor->SetRootComponent(RootComponent); + } + } + else if (InActorFactory->IsA()) + { + if (!IsValid(InActorClass.Get())) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineBakeUtils::SpawnBakeActor] Could not spawn an actor: InActorFactory is a UActorFactoryClass, but InActorClass is nullptr.")); + return nullptr; + } + + // UActorFactoryClass spawns an actor of the specified class (specified via InAsset to the CreateActor function) + SpawnedActor = InActorFactory->CreateActor(InActorClass.Get(), InLevel, InTransform, InSpawnParams); + } + else + { + SpawnedActor = InActorFactory->CreateActor(InAsset, InLevel, InTransform, InSpawnParams); + } + + if (IsValid(SpawnedActor)) + { + PostSpawnBakeActor(SpawnedActor, InOuterComponent); + } + + return SpawnedActor; +} + +void +FHoudiniEngineBakeUtils::PostSpawnBakeActor(AActor* const InSpawnedActor, const USceneComponent* InOuterComponent) +{ + if (!IsValid(InSpawnedActor)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineBakeUtils::PostSpawnBakeActor] InSpawnedActor is null.")); + return; + } + + if (!IsValid(InOuterComponent)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineBakeUtils::PostSpawnBakeActor] InOuterComponent is null.")); + return; + } + + // Copy the mobility of the HAC (root component of the houdini asset actor) to the root component of the spawned + // bake actor + USceneComponent* const BakedRootComponent = InSpawnedActor->GetRootComponent(); + if (IsValid(BakedRootComponent)) + { + BakedRootComponent->SetMobility(InOuterComponent->Mobility); + } +} + +void +FHoudiniEngineBakeUtils::RemoveBakedLevelInstances( + TArray& InBakedOutputs, + const FHoudiniBakeSettings& BakeSettings) +{ + // Re-using previously baked outputs for level instances is problematic, so to simplfiy + // everything we just delete the previous outputs. If we are Replacing Actors, we delete + // the old actors first. + for (int Index = 0; Index < InBakedOutputs.Num(); Index++) + { + FHoudiniBakedOutput& BakedOutput = InBakedOutputs[Index]; + + TSet OutputObjectsToRemove; + + for (auto& BakedOutputObject : BakedOutput.BakedOutputObjects) + { + auto & BakedObj = BakedOutputObject.Value; + + // If there are no level instance actors associated with this output object, ignore. + if (BakedObj.LevelInstanceActors.IsEmpty()) + continue; + + if (BakeSettings.bReplaceActors) + { + for(const FString & Name : BakedObj.LevelInstanceActors) + { + ALevelInstance* LevelInstance = Cast( + StaticLoadObject( + ALevelInstance::StaticClass(), + nullptr, + *Name, + nullptr, + LOAD_NoWarn, + nullptr)); + + if (!IsValid(LevelInstance)) + continue; + + LevelInstance->Destroy(); + } + } + OutputObjectsToRemove.Add(BakedOutputObject.Key); + } + + // Clean up any references + for(auto Id : OutputObjectsToRemove) + { + BakedOutput.BakedOutputObjects.Remove(Id); + } + } +} + +UUserDefinedStruct* FHoudiniEngineBakeUtils::DuplicateUserDefinedStruct(UUserDefinedStruct* UserStruct, UPackage * Package, FString & PackageName) +{ + ObjectTools::FPackageGroupName PGN; + PGN.PackageName = Package->GetPathName(); + PGN.GroupName = TEXT(""); + PGN.ObjectName = PackageName; + + Package->FullyLoad(); + + TSet Others; + UUserDefinedStruct* DuplicatedStruct = DuplicateObject(UserStruct, Package, *PackageName); + CastChecked(DuplicatedStruct->EditorData)->RecreateDefaultInstance(); + + return DuplicatedStruct; +} + + +void +FHoudiniBakeSettings::SetFromCookable(UHoudiniCookable* HC) +{ + if (!HC->IsOutputSupported()) + return; + + UCookableBakingData* BakingData = HC->GetBakingData(); + if (!BakingData) + return; + + bReplaceActors = BakingData->bReplacePreviousBake; + bReplaceAssets = BakingData->bReplacePreviousBake; + bRecenterBakedActors = BakingData->bRecenterBakedActors; + ActorBakeOption = BakingData->ActorBakeOption; +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineBakeUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniPDGAssetLink.h" +#include "HoudiniOutput.h" +#include "HoudiniPackageParams.h" +#include "LevelInstance/LevelInstanceActor.h" +#include "Materials/MaterialExpression.h" +#include "HoudiniEngineOutputStats.h" + +#include "HoudiniEngineBakeUtils.generated.h" + +struct FHoudiniEngineBakedActor; +class UDataTable; +class ULevelInstanceComponent; +class UHoudiniOutput; +class ALandscapeProxy; +class UStaticMesh; +class USkeletalMesh; +class USplineComponent; +class UPackage; +class UWorld; +class AActor; +class UHoudiniSplineComponent; +class UStaticMeshComponent; +class UHoudiniPDGAssetLink; +class UTOPNetwork; +class UTOPNode; +class UGeometryCollection; +class UGeometryCollectionComponent; +class AGeometryCollectionActor; +class UMaterialExpression; +class UPhysicsAsset; +class UHoudiniCookable; + +struct FHoudiniPackageParams; +struct FHoudiniGeoPartObject; +struct FHoudiniOutputObject; +struct FHoudiniOutputObjectIdentifier; +struct FHoudiniEngineOutputStats; +struct FHoudiniBakedOutputObject; +struct FHoudiniAttributeResolver; + +enum class EHoudiniLandscapeOutputBakeType : uint8; + +// Use this structure to pass bake settings to bake functions. Previously the code would +// pass each of there individually which was hard to maintain. +struct HOUDINIENGINE_API FHoudiniBakeSettings +{ + void SetFromCookable(UHoudiniCookable* HC); + + bool bReplaceAssets = false; + bool bReplaceActors = false; + bool bRecenterBakedActors = false; + EHoudiniEngineActorBakeOption ActorBakeOption = EHoudiniEngineActorBakeOption::OneActorPerComponent; + FString DefaultBakeName = TEXT("{hda_actor_name}_{guid8}"); +}; + +// Use this structure to return bake output data. Previously the code would +// pass each of there individually which was hard to maintain. +struct FHoudiniBakedObjectData +{ + TArray Blueprints; + TArray PackagesToSave; + FHoudiniEngineOutputStats BakeStats; +}; + + +// An enum of the different types for instancer component/bake types +UENUM() +enum class EHoudiniInstancerComponentType : uint8 +{ + // Single static mesh component + StaticMeshComponent, + // (Hierarichal)InstancedStaticMeshComponent + InstancedStaticMeshComponent, + MeshSplitInstancerComponent, + InstancedActorComponent, + // For baking foliage as foliage + FoliageInstancedStaticMeshComponent, + // Baking foliage as HISMC + FoliageAsHierarchicalInstancedStaticMeshComponent, + GeometryCollectionComponent +}; + +// Helper struct to track actors created/used when baking, with +// the intended bake name (before making it unique), and their +// output index and output object identifier. +struct HOUDINIENGINE_API FHoudiniEngineBakedActor +{ + FHoudiniEngineBakedActor(); + + FHoudiniEngineBakedActor( + AActor* InActor, + FName InActorBakeName, + FName InWorldOutlinerFolder, + int32 InOutputIndex, + const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, + UObject* InBakedObject, + UObject* InSourceObject, + UObject* InBakedComponent, + const FString& InBakeFolderPath, + const FHoudiniPackageParams& InBakedObjectPackageParams); + + // The actor that the baked output was associated with + AActor* Actor = nullptr; + + // The output index on the HAC for the baked object + int32 OutputIndex = INDEX_NONE; + + // The output object identifier for the baked object + FHoudiniOutputObjectIdentifier OutputObjectIdentifier; + + // The intended bake actor name. The actor's actual name could have a numeric suffix for uniqueness. + FName ActorBakeName = NAME_None; + + // The world outliner folder the actor is placed in + FName WorldOutlinerFolder = NAME_None; + + // The array index of the work result when baking PDG + int32 PDGWorkResultArrayIndex = INDEX_NONE; + + // The work item index (as returned by HAPI) for the work item/work result, used when baking PDG + int32 PDGWorkItemIndex = INDEX_NONE; + + // The array index of the work result object of the work result when baking PDG + int32 PDGWorkResultObjectArrayIndex = INDEX_NONE; + + // The baked primary asset (such as static mesh) + UObject* BakedObject = nullptr; + + // The temp asset that was baked to BakedObject + UObject* SourceObject = nullptr; + + // The baked component or foliage type in the case of foliage + UObject* BakedComponent = nullptr; + + // The bake folder path to where BakedObject was baked + FString BakeFolderPath; + + // The package params for the BakedObject + FHoudiniPackageParams BakedObjectPackageParams; + + // True if this entry was created by an instancer output. + bool bInstancerOutput; + + // The package params built for the instancer part of the output, if this was an instancer. + // This would mostly be useful in situations for we later need the resolver and/or cached attributes and + // tokens, such as for blueprint baking. + FHoudiniPackageParams InstancerPackageParams; + + // Used to delay all post bake calls so they are done only once per baked actor + bool bPostBakeProcessPostponed = false; +}; + +// Helper class to keep track of various parts of baking state while baking outputs from a HAC. +class FHoudiniEngineBakeState +{ +public: + + FHoudiniEngineBakeState() = delete; + + // Initialize the bake state by the number of outputs and previous baked outputs. + FHoudiniEngineBakeState(const int32 InNumOutputs, const TArray& InOldBakedOutputs); + + // Find an entry from OldBakedOutputs by index an identifier. + const FHoudiniBakedOutputObject* FindOldBakedOutputObject( + const int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InIdentifier); + + // Make a new baked output object that is populated from the old entry with index and identifier. Does not add the + // new entry to NewBakedOutputs, use SetNewBakedOutputObject() for that. + FHoudiniBakedOutputObject MakeNewBakedOutputObject( + const int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InIdentifier, bool& bOutHasPreviousBakeData); + + // Find an entry in NewBakedOutputs by index and identifier. + const FHoudiniBakedOutputObject& FindNewBakedOutputObjectChecked( + const int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InIdentifier); + + // Find an entry in NewBakedOutputs by index and identifier. + FHoudiniBakedOutputObject& FindOrAddNewBakedOutputObject( + const int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InIdentifier); + + // Set an entry in NewBakedOutputs by index and identifier. + FHoudiniBakedOutputObject& SetNewBakedOutputObject( + const int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InIdentifier, const FHoudiniBakedOutputObject& InBakedOutputObject); + + // Set an entry in NewBakedOutputs by index and identifier. + FHoudiniBakedOutputObject& SetNewBakedOutputObject( + const int32 InOutputIndex, const FHoudiniOutputObjectIdentifier& InIdentifier, FHoudiniBakedOutputObject&& InBakedOutputObject); + + // Get a const reference to OldBakedOutputs + const TArray& GetOldBakedOutputs() const { return OldBakedOutputs; } + // Get a reference to OldBakedOutputs + TArray& GetOldBakedOutputs() { return OldBakedOutputs; } + + // Get a const reference to NewBakedOutputs + const TArray& GetNewBakedOutputs() const { return NewBakedOutputs; } + // Get a reference to NewBakedOutputs + TArray& GetNewBakedOutputs() { return NewBakedOutputs; } + + // Get a const reference to BakedSkeletons + const TMap& GetBakedSkeletons() const { return BakedSkeletons; } + // Get a reference to BakedSkeletons + TMap& GetBakedSkeletons() { return BakedSkeletons; } + + // Get a const reference to BakedSkeletons + const TMap& GetBakedPhysicsAssets() const { return BakedPhysicsAssets; } + // Get a reference to BakedSkeletons + TMap& GetBakedPhysicsAssets() { return BakedPhysicsAssets; } + + // Add a temp -> baked skeleton entry + void AddBakedSkeleton(USkeleton* InTempSkeleton, USkeleton* InBakedSkeleton) { BakedSkeletons.Add(InTempSkeleton, InBakedSkeleton); } + // Look for a baked skeleton for InTempSkeleton in BakedSkeletons map. + USkeleton* FindBakedSkeleton(USkeleton const* InTempSkeleton, bool& bFoundEntry) const; + +protected: + // Array of old / previous baked outputs + TArray OldBakedOutputs; + // Array of new baked outputs + TArray NewBakedOutputs; + + // Map of temp to baked Skeletons + TMap BakedSkeletons; + + // Map of temp to baked Physics Assets + TMap BakedPhysicsAssets; +}; + + +struct HOUDINIENGINE_API FHoudiniEngineBakeUtils +{ +public: + + static ALandscapeProxy* BakeHeightfield( + ALandscapeProxy * InLandscapeProxy, + const FHoudiniPackageParams &PackageParams, + const EHoudiniLandscapeOutputBakeType & LandscapeOutputBakeType, + FHoudiniBakedObjectData& BakedObjectData); + + static bool BakeCurve( + const UHoudiniCookable* InCookable, + USplineComponent* InSplineComponent, + ULevel* InLevel, + const FHoudiniPackageParams &PackageParams, + const FHoudiniBakeSettings& BakeSettings, + const FName& InActorName, + AActor*& OutActor, + USplineComponent*& OutSplineComponent, + FHoudiniBakedObjectData& BakedObjectData, + FName InOverrideFolderPath=NAME_None, + AActor* InActor=nullptr, + TSubclassOf BakeActorClass = nullptr); + + static bool BakeCurve( + const UHoudiniCookable* InCookable, + const FHoudiniOutputObject& InOutputObject, + FHoudiniBakedOutputObject& InBakedOutputObject, + const FHoudiniPackageParams &PackageParams, + FHoudiniAttributeResolver& InResolver, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + FHoudiniEngineBakedActor& OutBakedActorEntry, + FHoudiniBakedObjectData& BakedObjectData, + AActor* InFallbackActor=nullptr); + + static AActor* BakeInputHoudiniCurveToActor( + const UHoudiniCookable* InCookable, + UHoudiniSplineComponent * InHoudiniSplineComponent, + const FHoudiniPackageParams & PackageParams, + const FHoudiniBakeSettings& BakeSettings, + UWorld* WorldToSpawn, + const FTransform & SpawnTransform); + + static UBlueprint* BakeInputHoudiniCurveToBlueprint( + UHoudiniCookable* InCookable, + UHoudiniSplineComponent * InHoudiniSplineComponent, + const FHoudiniPackageParams & PackageParams, + const FHoudiniBakeSettings& BakeSettings, + UWorld* WorldToSpawn, + const FTransform & SpawnTransform); + + static UStaticMesh* BakeStaticMesh( + UStaticMesh * StaticMesh, + const FHoudiniPackageParams & PackageParams, + const TArray& InAllOutputs, + const FDirectoryPath& InTempCookFolder, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap); + + static bool BakeStaticMeshOutputToAsset( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + const FDirectoryPath& InTempFolder, + const FDirectoryPath& InBakeFolder, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData); + + static bool BakeSkeletalMeshOutputToAsset( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + const FDirectoryPath& InTempFolder, + const FDirectoryPath& InBakeFolder, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData); + + static bool BakeGeometryCollectionOutputToAsset( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + const FDirectoryPath& InTempFolder, + const FDirectoryPath& InBakeFolder, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData); + + static bool BakeInstancerOutputToActors( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FTransform& InTransform, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + TArray const* InInstancerComponentTypesToBake=nullptr, + AActor* InFallbackActor=nullptr, + const FString& InFallbackWorldOutlinerFolder=TEXT("")); + + static bool BakeInstancerOutputToActors_ISMC( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const TArray& InHGPOs, + const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, + const FHoudiniOutputObject& InOutputObject, + const FTransform& InTransform, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor=nullptr, + const FString& InFallbackWorldOutlinerFolder=TEXT("")); + + static bool BakeInstancerOutputToActors_IAC( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InHGPOs, + const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, + const FHoudiniOutputObject& InOutputObject, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData); + + static bool BakeInstancerOutputToActors_SMC( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const TArray& InHGPOs, + const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, + const FHoudiniOutputObject& InOutputObject, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + FHoudiniEngineBakedActor& OutBakedActorEntry, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor=nullptr, + const FString& InFallbackWorldOutlinerFolder=""); + + static bool BakeInstancerOutputToActors_LevelInstances( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, + const FHoudiniOutputObject& InOutputObject, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + FHoudiniEngineBakedActor& OutBakedActorEntry, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor = nullptr, + const FString& InFallbackWorldOutlinerFolder = ""); + + static UStaticMesh * DuplicateStaticMeshAndCreatePackageIfNeeded( + UStaticMesh * InStaticMesh, + UStaticMesh * InPreviousBakeStaticMesh, + const FHoudiniPackageParams &PackageParams, + const TArray& InParentOutputs, + const TArray& InCurrentBakedActors, + const FString& InTemporaryCookFolder, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap); + + static USkeletalMesh* DuplicateSkeletalMeshAndCreatePackageIfNeeded( + USkeletalMesh* InSkeletalMesh, + USkeletalMesh* InPreviousBakeSkeletalMesh, + const FHoudiniPackageParams& PackageParams, + const TArray& InParentOutputs, + const TArray& InCurrentBakedActors, + const FString& InTemporaryCookFolder, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap); + + static USkeleton* DuplicateSkeletonAndCreatePackageIfNeeded( + USkeleton* InSkeleton, + USkeleton const* InPreviousBakeSkeleton, + const FHoudiniPackageParams& PackageParams, + const TArray& InParentOutputs, + const TArray& InCurrentBakedActors, + const FString& InTemporaryCookFolder, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedSkeletonMap); + + static UPhysicsAsset* DuplicatePhysicsAssetAndCreatePackageIfNeeded( + UPhysicsAsset * InPhysicsAsset, + UPhysicsAsset * InPreviousBakeSkeleton, + const FHoudiniPackageParams& PackageParams, + const TArray& InParentOutputs, + const TArray& InCurrentBakedActors, + const FString& InTemporaryCookFolder, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedPhysicsAssetMap); + + static UGeometryCollection * DuplicateGeometryCollectionAndCreatePackageIfNeeded( + UGeometryCollection * InGeometryCollection, + UGeometryCollection * InPreviousBakeGeometryCollection, + const FHoudiniPackageParams &PackageParams, + const TArray& InParentOutputs, + const TArray& InCurrentBakedActors, + const FString& InTemporaryCookFolder, + const TMap& InOldToNewStaticMesh, + const TMap& InOldToNewMaterialMap, + FHoudiniBakedObjectData& BakedObjectData); + + static UMaterialInterface * DuplicateMaterialAndCreatePackage( + UMaterialInterface * Material, + UMaterialInterface * PreviousBakeMaterial, + const FString & SubMaterialName, + const FHoudiniPackageParams& ObjectPackageParams, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedMaterialsMap); + + + static UFoliageType * DuplicateFoliageTypeAndCreatePackageIfNeeded( + UFoliageType* InFoliageType, + UFoliageType* InPreviousBakeFoliageType, + const FHoudiniPackageParams& PackageParams, + const TArray& InParentOutputs, + const TArray& InCurrentBakeResults, + const FString& InTemporaryCookFolder, + TMap& InOutAlreadyBakedFoliageTypes, + TMap& InOutAlreadyBakedMaterialsMap, + const TArray& InCurrentBakedActors, + FHoudiniBakedObjectData& BakedObjectData); + + static void ReplaceDuplicatedMaterialTextureSample( + UMaterialExpression * MaterialExpression, + UMaterialExpression* PreviousBakeMaterialExpression, + const FHoudiniPackageParams& PackageParams, + FHoudiniBakedObjectData& BakedObjectData); + + static bool BakeTexture( + UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData); + + static UTexture2D* BakeTextureToPackage( + UTexture2D* InOriginalTexture, + const FHoudiniPackageParams& InPackageParams, + UTexture2D* InPreviousBakeTexture); + + static UTexture2D * DuplicateTextureAndCreatePackage( + UTexture2D * Texture, + UTexture2D* PreviousBakeTexture, + const FString & SubTextureName, + const FHoudiniPackageParams& PackageParams, + FHoudiniBakedObjectData& BakedObjectData); + + // Bake a cookable based on the bInReplace and BakeOption arguments. + // Returns true if the underlying bake function (for example, BakeHoudiniActorToActors, returns true (or a valid UObject*)) + static bool BakeCookable( + UHoudiniCookable* InCookableToBake, + const FHoudiniBakeSettings& BakeSettings, + EHoudiniEngineBakeOption InBakeOption, + bool bInRemoveHACOutputOnSuccess); + + static bool BakeCookableToActors( + UHoudiniCookable* InCookable, + const FHoudiniBakeSettings& BakeSettings); + + static bool BakeCookableToActors( + UHoudiniCookable* InCookable, + const FHoudiniBakeSettings& BakeSettings, + TArray& OutNewActors, + FHoudiniBakedObjectData& BakedObjectData, + TArray * InOutputTypesToBake=nullptr, + TArray * InInstancerComponentTypesToBake=nullptr, + AActor* InFallbackActor=nullptr, + const FString& InFallbackWorldOutlinerFolder=""); + + static bool BakeCookableToAssets( + UHoudiniCookable* InCookable, + const FHoudiniBakeSettings& BakeSettings); + + static bool BakeHoudiniOutputsToActors( + UHoudiniCookable* InCookable, + const TArray& InOutputs, + FHoudiniEngineBakeState& InBakeState, + const FTransform& InParentTransform, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutNewActors, + FHoudiniBakedObjectData& BakedObjectData, + TArray * InOutputTypesToBake=nullptr, + TArray * InInstancerComponentTypesToBake=nullptr, + AActor* InFallbackActor=nullptr, + const FString& InFallbackWorldOutlinerFolder=TEXT("")); + + static bool BakeStaticMeshOutputToActors( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor=nullptr, + const FString& InFallbackWorldOutlinerFolder=""); + + static bool BakeSkeletalMeshOutputToActors( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor = nullptr, + const FString& InFallbackWorldOutlinerFolder = ""); + + static bool ResolvePackageParams( + const UHoudiniCookable* InCookable, + UHoudiniOutput* InOutput, + const FHoudiniOutputObjectIdentifier& Identifier, + const FHoudiniOutputObject& InOutputObject, + const bool bInHasPreviousBakeData, + const FString& DefaultObjectName, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniPackageParams& OutPackageParams, + FHoudiniBakedObjectData& BakedObjectData, + const FString& InHoudiniAssetName=TEXT(""), + const FString& InHoudiniAssetActorName=TEXT("")); + + static bool ResolvePackageParamsWithResolver( + const UHoudiniCookable* InCookable, + UHoudiniOutput* InOutput, + const FHoudiniOutputObjectIdentifier& Identifier, + const FHoudiniOutputObject& InOutputObject, + const bool bInHasPreviousBakeData, + const FString& DefaultObjectName, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniPackageParams& OutPackageParams, + FHoudiniAttributeResolver & Resolver, + FHoudiniBakedObjectData& BakedObjectData, + const FString& InHoudiniAssetName = TEXT(""), + const FString& InHoudiniAssetActorName = TEXT("")); + + static bool BakeGeometryCollectionOutputToActors( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor=nullptr, + const FString& InFallbackWorldOutlinerFolder=TEXT("")); + + static UUserDefinedStruct * CreateBakedUserDefinedStruct( + UHoudiniOutput* CookedOutput, + const FHoudiniOutputObjectIdentifier& Identifier, + const UHoudiniCookable* InCookable, + const FHoudiniBakedOutput& InPreviousBakedOutput, + FHoudiniBakedOutput& InNewBakedOutput, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData); + + static UDataTable* CreateBakedDataTable( + UScriptStruct * UserDefinedStruct, + const FString & ObjectName, + UHoudiniOutput* CookedOutput, + const FHoudiniOutputObjectIdentifier& Identifier, + const UHoudiniCookable* InCookable, + const FHoudiniBakedOutput& InPreviousBakedOutput, + FHoudiniBakedOutput& InNewBakedOutput, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData); + + + static bool BakeDataTables( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData); + + static bool BakePCGData( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + AActor* InFallbackActor = nullptr, + const FString& InFallbackWorldOutlinerFolder = TEXT("")); + + static UAnimSequence* CreateBakedAnimSequence( + const FString& ObjectName, + UHoudiniOutput* CookedOutput, + const FHoudiniOutputObjectIdentifier& Identifier, + const UHoudiniCookable* InCookable, + const FHoudiniBakedOutput& InPreviousBakedOutput, + FHoudiniBakedOutput& InNewBakedOutput, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData); + + static bool BakeAnimSequence( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData); + + static bool BakeHoudiniCurveOutputToActors( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& InBakedActors, + TArray& OutActors, + AActor* InFallbackActor=nullptr, + const FString& InFallbackWorldOutlinerFolder=TEXT("")); + + static bool BakeBlueprintsFromBakedActors( + const TArray& InBakedActors, + const FHoudiniBakeSettings& BakeSettings, + const FString& InHoudiniAssetName, + const FString& InHoudiniAssetActorName, + const FDirectoryPath& InBakeFolder, + TArray* const InNonPDGBakedOutputs, + TMap* const InPDGBakedOutputs, + FHoudiniBakedObjectData& BakedObjectData); + + static bool BakeBlueprints( + UHoudiniCookable* InCookable, + const FHoudiniBakeSettings& BakeSettings); + + static bool BakeBlueprints( + UHoudiniCookable* InCookable, + const FHoudiniBakeSettings& BakeSettings, + FHoudiniBakedObjectData& BakedObjectData); + + static bool CopyActorContentsToBlueprint( + AActor* InActor, + UBlueprint* OutBlueprint, + bool bInRenameComponentsWithInvalidNames=false); + + static void AddHoudiniMetaInformationToPackage( + UPackage* Package, + UObject* Object, + const TCHAR * Key, + const TCHAR * Value); + + static bool GetHoudiniGeneratedNameFromMetaInformation( + UPackage* Package, + UObject* Object, + FString & HoudiniName); + + static bool DeleteBakedHoudiniAssetActor( + UHoudiniCookable* InCookable); + + static void SaveBakedPackages( + TArray & PackagesToSave, + bool bSaveCurrentWorld = false); + + // Look for InObjectToFind among InOutputs. Return true if found and set OutOutputIndex and OutIdentifier. + static bool FindOutputObject( + const UObject* InObjectToFind, + const EHoudiniOutputType& InOutputType, + const TArray InOutputs, + int32& OutOutputIndex, + FHoudiniOutputObjectIdentifier& OutIdentifier); + + // Returns true if the object is a temporary, houdini-generated object + static bool IsObjectTemporary( + UObject* InObject, + const FGuid& InComponentGuid=FGuid()); + + // Returns true if the object is a temporary, houdini-generated object + static bool IsObjectTemporary( + UObject* InObject, + const EHoudiniOutputType& InOutputType, + UHoudiniCookable* InCookable); + + // Returns true if the object is a temporary, houdini-generated object + static bool IsObjectTemporary( + UObject* InObject, + const EHoudiniOutputType& InOutputType, + const TArray& InParentOutputs, + const FString& InTemporaryCookFolder, + const FGuid& InComponentGuid=FGuid()); + + // Returns true if InObject is in InTemporaryCookFolder, + // or in the default Temporary cook folder from the runtime settings. + static bool IsObjectInTempFolder( + UObject* const InObject, + const FString& InTemporaryCookFolder); + + // Function used to copy properties from the source Static Mesh Component to the new (baked) one + static void CopyPropertyToNewActorAndComponent( + AActor* NewActor, + UStaticMeshComponent* NewSMC, + UStaticMeshComponent* InSMC, + bool bInCopyWorldTransform=false); + + static void CopyPropertyToNewActorAndSkeletalComponent( + AActor* NewActor, + USkeletalMeshComponent* NewSKC, + USkeletalMeshComponent* InSKC, + bool bInCopyWorldTransform = false); + + // Function used to copy properties from the source GeometryCollection Component to the new (baked) one + static void CopyPropertyToNewGeometryCollectionActorAndComponent( + AGeometryCollectionActor* NewActor, + UGeometryCollectionComponent* NewGCC, + UGeometryCollectionComponent* InGCC, + bool bInCopyWorldTransform=false); + + // Finds the world/level indicated by the package path. + // If the level doesn't exists, it will be created. + // If InLevelPath is empty, outputs the editor world and current level + // Returns true if the world/level were found, false otherwise + static bool FindOrCreateDesiredLevelFromLevelPath( + const FString& InLevelPath, + ULevel*& OutDesiredLevel, + UWorld*& OutDesiredWorld, + bool& OutCreatedPackage); + + // Finds the actor indicated by InBakeActorName in InLevel. + // Returns false if any input was invalid (InLevel is invalid for example), true otherwise + // If an actor was found OutActor is set + // If bInNoPendingKillActors is true, then if an actor called InBakeActorName is found but is pending kill, then + // it is not set in OutActor + // If bRenamePendingKillActor is true, then if a pending kill actor call InBakeActorName is found it is renamed + // (uniquely) with a _Pending_Kill suffix (regardless of bInNoPendingKillActors). + static bool FindBakedActor( + const FString& InBakeActorName, + const TSubclassOf& InBakeActorClass, + ULevel* InLevel, + AActor*& OutActor, + bool bInNoPendingKillActors=true, + bool bRenamePendingKillActor=true); + + // Helper that determines the desired bake actor name with unreal_bake_actor attribute, falling + // back to InDefaultActorName if the attribute is not set. + // If unreal_bake_actor is set, we look for such in InLevel, and use it *if* it is present in InAlLBakedOutputs. + // Otherwise if we are baking in replace mode, and the previous bake actor is available and in InLevel, return it + // as OutFoundActor. Otherwise return InFallbackActor as OutFoundActor. + // bOutHasBakeActorName indicates if the output has the unreal_bake_actor attribute set. + // OutFoundActor is the actor that was found (if one was found) + static void FindUnrealBakeActor( + const FHoudiniOutputObject& InOutputObject, + const FHoudiniBakedOutputObject& InBakedOutputObject, + const TArray& InAllBakedActors, + ULevel* InLevel, + FName InDefaultActorName, + const FHoudiniBakeSettings& BakeSettings, + AActor* InFallbackActor, + AActor*& OutFoundActor, + bool& bOutHasBakeActorName, + FName& OutBakeActorName); + + // Try to find an actor that we can use for baking. + // If the requested actor could not be found, then `OutWorld` and `OutLevel` + // should be used to spawn the new bake actor. + // @returns AActor* if found. Otherwise, returns nullptr. + static AActor* FindExistingActor_Bake( + UWorld* InWorld, + UHoudiniOutput* InOutput, + const FString& InActorName, + const FString& InPackagePath, + UWorld*& OutWorld, + ULevel*& OutLevel, + bool& bCreatedPackage); + + // Remove a previously baked actor + static bool RemovePreviouslyBakedActor( + AActor* InNewBakedActor, + ULevel* InLevel, + const FHoudiniPackageParams& InPackageParams); + + static bool RemovePreviouslyBakedComponent(UActorComponent* InComponent); + + // Get the world outliner folder path for output generated by InOutputOwner + static FName GetOutputFolderPath(UObject* InOutputOwner); + + static void RenameAsset(UObject* InAsset, const FString& InNewName, bool bMakeUniqueIfNotUnique=true); + + // Helper function for renaming and relabelling an actor + static void RenameAndRelabelActor(AActor* InActor, const FString& InNewName, bool bMakeUniqueIfNotUnique=true); + + // Helper function for duplicating Static Meshes + //static UStaticMesh* DuplicateStaticMesh(UStaticMesh* SourceStaticMesh, UObject* Outer, const FName Name); + + // Start: PDG Baking + + // Detach InActor from its parent, and rename to InNewName (attaches a numeric suffix to make it unique via + // MakeUniqueObjectName). Place it in the world outliner folder InFolderPath. + static bool DetachAndRenameBakedPDGOutputActor( + AActor* InActor, + const FString& InNewName, + const FName& InFolderPath); + + static bool BakePDGWorkResultObject( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InNode, + int32 InWorkResultArrayIndex, + int32 InWorkResultObjectArrayIndex, + const FHoudiniBakeSettings& BakeSettings, + bool bInBakeToWorkResultActor, + bool bInIsAutoBake, + const TArray& InBakedActors, + TArray& OutBakedActors, + FHoudiniBakedObjectData& BakedObjectData, + TArray * InOutputTypesToBake=nullptr, + TArray * InInstancerComponentTypesToBake=nullptr, + const FString& InFallbackWorldOutlinerFolder=""); + + // Checks if auto-bake is enabled on InPDGAssetLink, and if it is, calls BakePDGWorkResultObject. + static void CheckPDGAutoBakeAfterResultObjectLoaded( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InNode, + int32 InWorkItemHAPIIndex, + int32 InWorkItemResultInfoIndex); + + //FROSTY_BEGIN_CHANGE + // Checks if auto-bake is enabled on InPDGAssetLink, and if it is, calls BakePDGWorkResultObject. + static void PDGAutoBakeAfterResultObjectLoaded( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InNode, + int32 InWorkItemHAPIIndex, + int32 InWorkItemResultInfoIndex, + TArray& OutBakedActors); + //FROSTY_END_CHANGE + + // Bake PDG output. This bakes all assets from all work items in the specified InNode (FTOPNode). + // It uses the existing output actors in the level, but breaks any links from these actors to the PDG link and + // moves the actors out of the parent Folder/ detaches from the parent PDG output actor. + static bool BakePDGTOPNodeOutputsKeepActors( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InNode, + bool bInBakeForBlueprint, + bool bInIsAutoBake, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + TArray& OutBakedActors, + FHoudiniBakedObjectData& BakedObjectData); + + // Helper function to bake only a specific PDG TOP node's outputs to actors. + static bool BakePDGTOPNodeOutputsKeepActors( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InTOPNode, + bool bInIsAutoBake, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors, + //FROSTY_BEGIN_CHANGE + TArray& OutBakedActors); + //FROSTY_END_CHANGE + + // Bake PDG output. This bakes all assets from all work items in the specified TOP network. + // It uses the existing output actors in the level, but breaks any links + // from these actors to the PDG link and moves the actors out of the parent Folder/ detaches from the parent + // PDG output actor. + static bool BakePDGTOPNetworkOutputsKeepActors( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNetwork* InNetwork, + bool bInBakeForBlueprint, + bool bInIsAutoBake, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + TArray& OutBakedActors, + FHoudiniBakedObjectData& BakedObjectData); + + static bool BakePDGAssetLink(UHoudiniPDGAssetLink* InPDGAssetLink); + + // Bake PDG output. This bakes assets from TOP networks and nodes according to + // InPDGAssetLink->PDGBakeSelectionOption. It uses the existing output actors in the level, but breaks any links + // from these actors to the PDG link and moves the actors out of the parent Folder/ detaches from the parent + // PDG output actor. + static bool BakePDGAssetLinkOutputsKeepActors( + UHoudiniPDGAssetLink* InPDGAssetLink, + EPDGBakeSelectionOption InBakeSelectionOption, + EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors); + + static bool BakePDGAssetLinkOutputsKeepActors( + UHoudiniPDGAssetLink* InPDGAssetLink, + EPDGBakeSelectionOption InBakeSelectionOption, + EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors, + FHoudiniBakedObjectData& BakedObjectData, + TArray & BakedActors); + + // Bake PDG output. This bakes all supported assets from all work items in the specified InNode (FTOPNode). + // It duplicates the output actors and bakes them to blueprints. Assets that were baked are removed from + // PDG output actors. + static bool BakePDGTOPNodeBlueprints( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InNode, + bool bInIsAutoBake, + EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors, + FHoudiniBakedObjectData& BakedObjectData); + + // Helper to bake only a specific PDG TOP node's outputs to blueprint(s). + static bool BakePDGTOPNodeBlueprints( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNode* InTOPNode, + bool bInIsAutoBake, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors); + + // Bake PDG output. This bakes all supported assets from all work items in the specified TOP network. + // It duplicates the output actors and bakes them to blueprints. Assets that were baked are removed from + // PDG output actors. + static bool BakePDGTOPNetworkBlueprints( + UHoudiniPDGAssetLink* InPDGAssetLink, + UTOPNetwork* InNetwork, + EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors, + FHoudiniBakedObjectData& BakedObjectData); + + // Bake PDG output. This bakes assets from TOP networks and nodes according to + // InPDGAssetLink->PDGBakeSelectionOption. It duplicates the output actors and bakes them to blueprints. Assets + // that were baked are removed from PDG output actors. + static bool BakePDGAssetLinkBlueprints( + UHoudiniPDGAssetLink* InPDGAssetLink, + const EPDGBakeSelectionOption InBakeSelectionOption, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors); + + static bool BakePDGAssetLinkBlueprints( + UHoudiniPDGAssetLink* InPDGAssetLink, + const EPDGBakeSelectionOption InBakeSelectionOption, + const EPDGBakePackageReplaceModeOption InPDGBakePackageReplaceMode, + bool bInRecenterBakedActors, + FHoudiniBakedObjectData& BakedObjectData); + + // End: PDG Baking + +public: + + // Find the HGPO with matching identifier. Returns true if the HGPO was found. + static bool FindHGPO( + const FHoudiniOutputObjectIdentifier& InIdentifier, + const TArray& InHGPOs, + FHoudiniGeoPartObject const*& OutHGPO); + + // Set OutBakeName to the resolved output name of InMeshOutputObject / InObject. OutBakeName is set to the object's + // BakeName (the BakeName on the InMeshOutputObject, or if that is not set, the custom part name or finally the + // package name. + static void GetTemporaryOutputObjectBakeName( + const UObject* InObject, + const FHoudiniOutputObject& InMeshOutputObject, + FString& OutBakeName); + + // Look for InObject in InAllOutputs. If found the function returns true and OutBakeName is set to the object's + // BakeName (the BakeName on the OutputObject, or if that is not set, the custom part name or finally the package + // name. + static bool GetTemporaryOutputObjectBakeName( + const UObject* InObject, + EHoudiniOutputType InOutputType, + const TArray& InAllOutputs, + FString& OutBakeName); + + // Checks if InCookable has any current proxy mesh. Refines if it possible. + // Returns true if baking can continue, false otherwise. + // If the cookable has a proxy, but no cook data, then false is returned, and the cookable is set to recook without + // proxies and with bake after cook, and bOutNeedsReCook is set to true. + // bInReplace and BakeOption represents the baking settings to use if a delayed bake (post-cook) needs to be triggered. + static bool CheckForAndRefineHoudiniProxyMesh( + UHoudiniCookable* InCookable, + bool bInReplacePreviousBake, + EHoudiniEngineBakeOption BakeOption, + bool bInRemoveHACOutputOnSuccess, + bool bInRecenterBakedActors, + bool& bOutNeedsReCook); + + // Position InActor at its bounding box center (keep components' world location) + static void CenterActorToBoundingBoxCenter(AActor* InActor); + + // Position each of the actors in InActors at its bounding box center (keep components' world location) + static void CenterActorsToBoundingBoxCenter(const TArray& InActors); + + // Helper to get or optionally create a RootComponent for an actor + static USceneComponent* GetActorRootComponent( + AActor* InActor, + bool bCreateIfMissing = true, + EComponentMobility::Type InMobilityIfCreated = EComponentMobility::Static); + + // Helper function to return a unique object name if the given is already in use + static FString MakeUniqueObjectNameIfNeeded( + UObject* InOuter, + const UClass* InClass, + const FString& InName, + UObject* InObjectThatWouldBeRenamed = nullptr); + + // Helper for getting the actor folder path for the world outliner, based unreal_bake_outliner_folder + static FName GetOutlinerFolderPath( + const FHoudiniAttributeResolver& Resolver, + FName InDefaultFolder); + + // Helper for setting the actor folder path in the world outliner + static bool SetOutlinerFolderPath( + AActor* InActor, + FName InDefaultFolder); + + // Helper for destroying previous bake components/actors + static uint32 DestroyPreviousBakeOutput( + FHoudiniBakedOutputObject& InBakedOutputObject, + bool bInDestroyBakedComponent, + bool bInDestroyBakedInstancedActors, + bool bInDestroyBakedInstancedComponents); + + static UMaterialInterface * BakeSingleMaterialToPackage( + UMaterialInterface * InOriginalMaterial, + const FHoudiniPackageParams & PackageParams, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedMaterialsMap); + + // Returns the AActor class with classname InActorClassName, or nullptr if InActorClassName is none or not a valid + // actor class name. + static UClass* GetBakeActorClassOverride(const FName& InActorClassName); + + // Returns the AActor class with classname read from the unreal_bake_actor_class attribute, or nullptr if + // attribute is not set or is invalid is none or not a valid actor class name. + static UClass* GetBakeActorClassOverride(const FHoudiniOutputObject& InOutputObject); + + // Helper function for getting the appropriate ActorFactory by unreal_bake_actor_class attribute, failing that by + // the specified ActorFactoryClass, and lastly by the asset that would be spawned + // OutActorClass is set to the Actor UClass specified by InActorClassName or null if InActorClassName is invalid. + static UActorFactory* GetActorFactory( + const FName& InActorClassName, + const FHoudiniBakeSettings& BakeSettings, + TSubclassOf& OutActorClass, + const TSubclassOf& InFactoryClass=nullptr, + UObject* InAsset=nullptr); + + // Helper function for getting the appropriate ActorFactory by unreal_bake_actor_class attribute (read from + // InOutputObject.CachedAttributes), failing that by the specified ActorFactoryClass, and lastly by the asset that + // would be spawned + // OutActorClass is set to the Actor UClass specified by unreal_bake_actor_class or null if unreal_bake_actor_class + // is not set or invalid. + static UActorFactory* GetActorFactory( + const FHoudiniOutputObject& InOutputObject, + const FHoudiniBakeSettings& BakeSettings, + TSubclassOf& OutActorClass, + const TSubclassOf& InFactoryClass=nullptr, + UObject* InAsset=nullptr); + + // Helper function that spawns an actor via InActorFactory. + static AActor* SpawnBakeActor( + UActorFactory* InActorFactory, + UObject* InAsset, + ULevel* InLevel, + const FHoudiniBakeSettings& BakeSettings, + const FTransform& InTransform, + const USceneComponent* InOuterComponent, + const TSubclassOf& InActorClass=nullptr, + const FActorSpawnParameters& InSpawnParams=FActorSpawnParameters()); + + // Called by SpawnBakeActor after the actor was successfully spawned. Used to copy any settings we need from the + // HAC or its owner to the spawned actor and/or its root component. + static void PostSpawnBakeActor( + AActor* const InSpawnedActor, + const USceneComponent* InOuterComponent); + + // Helper for baking a static mesh output to actors. Returns true if anything was baked. If the mesh had an + // associated component and was baked to an actor then bOutBakedToActor is set to true and OutBakedActorEntry + // is populated. Some meshes, such as invisible colliders, are not baked to actors, the mesh asset itself is + // just baked. In that case bOutBakedToActor is false and OutBakedActorEntry is not populated. + static bool BakeStaticMeshOutputObjectToActor( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + const FHoudiniOutputObjectIdentifier& InIdentifier, + const FHoudiniOutputObject& InOutputObject, + const TArray& InHGPOs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InTempCookFolder, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder, + const TArray& InAllBakedActors, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + FHoudiniBakedObjectData& BakedObjectData, + bool& bOutBakedToActor, + FHoudiniEngineBakedActor& OutBakedActorEntry); + + // Skeletal Mesh Version + static bool BakeSkeletalMeshOutputObjectToActor( + const UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + const FHoudiniOutputObjectIdentifier& InIdentifier, + const FHoudiniOutputObject& InOutputObject, + const TArray& InHGPOs, + FHoudiniEngineBakeState& InBakeState, + const FDirectoryPath& InTempCookFolder, + const FDirectoryPath& InBakeFolder, + const FHoudiniBakeSettings& BakeSettings, + AActor* InFallbackActor, + const FString& InFallbackWorldOutlinerFolder, + const TArray& InAllBakedActors, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + FHoudiniBakedObjectData& BakedObjectData, + bool& bOutBakedToActor, + FHoudiniEngineBakedActor& OutBakedActorEntry); + + TArray GetBakes( + UHoudiniCookable* InCookable, + int32 InOutputIndex, + const TArray& InAllOutputs, + const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, + const FHoudiniOutputObject& InOutputObject, + FHoudiniBakedOutputObject& InBakedOutputObject, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& BakeResults, + FHoudiniEngineBakedActor& OutBakeActorEntry, + FHoudiniBakedObjectData& BakedObjectData, + TMap& InOutAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap); + + static bool BakeFoliageTypes( + TMap & FoliageMap, + UHoudiniCookable* InCookable, + int32 InOutputIndex, + FHoudiniEngineBakeState& InBakeState, + const TArray& InAllOutputs, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& BakeResults, + const TMap& InAlreadyBakedStaticMeshMap, + TMap& InOutAlreadyBakedMaterialsMap, + FHoudiniBakedObjectData& BakedObjectData); + + static void BakeAllFoliageTypes( + UHoudiniCookable* InCookable, + const TMap& AlreadyBakedStaticMeshMap, + FHoudiniEngineBakeState& InBakeState, + const TArray& InAllOutputs, + const FDirectoryPath& InBakeFolder, + const FDirectoryPath& InTempCookFolder, + const FHoudiniBakeSettings& BakeSettings, + const TArray& BakeResults, + TMap& InOutAlreadyBakedMaterialsMap, + FHoudiniBakedObjectData& BakedObjectData); + + static void RemoveBakedFoliageInstances( + UWorld* InWorld, + TArray& InBakedOutputs); + + static void RemoveBakedLevelInstances( + TArray& InBakedOutputs, + const FHoudiniBakeSettings& BakeSettings); + + static void DeleteBakedDataTableObjects(TArray& InBakedOutputs); + + static UUserDefinedStruct * DuplicateUserDefinedStruct( + UUserDefinedStruct* OldStruct, UPackage* Pacakge, FString& PackageName); + +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineCommandlet.cpp`: + +```cpp +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +``` + +`Source/HoudiniEngine/Private/HoudiniEngineCommandlet.h`: + +```h +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +``` + +`Source/HoudiniEngine/Private/HoudiniEngineManager.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineManager.h" + +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniAsset.h" +#include "HoudiniAssetBlueprintComponent.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniEngineString.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniParameterTranslator.h" +#include "HoudiniPDGManager.h" +#include "HoudiniInputTranslator.h" +#include "HoudiniNodeSyncComponent.h" +#include "HoudiniOutputTranslator.h" +#include "HoudiniHandleTranslator.h" +#include "HoudiniLandscapeRuntimeUtils.h" + +#include "Misc/MessageDialog.h" +#include "Misc/ScopedSlowTask.h" +#include "Containers/Ticker.h" +#include "HAL/IConsoleManager.h" +#if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 0) + #include "LevelInstance/LevelInstanceInterface.h" +#endif + +#if WITH_EDITOR + #include "Editor.h" + #include "EditorViewportClient.h" + #include "Kismet/KismetMathLibrary.h" + + //#include "UnrealEd.h" + #include "UnrealEdGlobals.h" + #include "Editor/UnrealEdEngine.h" + #include "IPackageAutoSaver.h" +#endif + +static TAutoConsoleVariable CVarHoudiniEngineTickTimeLimit( + TEXT("HoudiniEngine.TickTimeLimit"), + 1.0, + TEXT("Time limit after which HDA processing will be stopped, until the next tick of the Houdini Engine Manager.\n") + TEXT("<= 0.0: No Limit\n") + TEXT("1.0: Default\n") +); + +static TAutoConsoleVariable CVarHoudiniEngineLiveSyncTickTime( + TEXT("HoudiniEngine.LiveSyncTickTime"), + 1.0, + TEXT("Frequency at which to look for update when using Session Sync.\n") + TEXT("<= 0.0: No Limit\n") + TEXT("1.0: Default\n") +); + +FHoudiniEngineManager::FHoudiniEngineManager() + : CurrentIndex(0) + , ComponentCount(0) + , bMustStopTicking(false) + , SyncedHoudiniViewportPivotPosition(FVector::ZeroVector) + , SyncedHoudiniViewportQuat(FQuat::Identity) + , SyncedHoudiniViewportOffset(0.0f) + , SyncedUnrealViewportPosition(FVector::ZeroVector) + , SyncedUnrealViewportRotation(FRotator::ZeroRotator) + , SyncedUnrealViewportLookatPosition(FVector::ZeroVector) + , ZeroOffsetValue(0.f) + , bOffsetZeroed(false) +{ + +} + +FHoudiniEngineManager::~FHoudiniEngineManager() +{ + PDGManager.StopBGEOCommandletAndEndpoint(); +} + +void +FHoudiniEngineManager::StartHoudiniTicking() +{ + // If we have no timer delegate spawned, spawn one. + if (!TickerHandle.IsValid() && GEditor) + { + // We use the ticker manager so we get ticked once per frame, no more. + TickerHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this, &FHoudiniEngineManager::Tick)); + + // Grab current time for delayed notification. + FHoudiniEngine::Get().SetHapiNotificationStartedTime(FPlatformTime::Seconds()); + } +} + +void +FHoudiniEngineManager::StopHoudiniTicking() +{ + if (TickerHandle.IsValid() && GEditor) + { + if (IsInGameThread()) + { + FTSTicker::GetCoreTicker().RemoveTicker(TickerHandle); + TickerHandle.Reset(); + + // Reset time for delayed notification. + FHoudiniEngine::Get().SetHapiNotificationStartedTime(0.0); + + bMustStopTicking = false; + } + else + { + // We can't stop ticking now as we're not in the game Thread, + // and accessing the timer would crash, indicate that we want to stop ticking asap + // This can happen when loosing a session due to a Houdini crash + bMustStopTicking = true; + } + } +} + +bool FHoudiniEngineManager::IsTicking() const +{ + return TickerHandle.IsValid(); +} + +bool +FHoudiniEngineManager::Tick(float DeltaTime) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::Tick); + + EnableEditorAutoSave(nullptr); + + FHoudiniEngine::Get().TickCookingNotification(DeltaTime); + + if (bMustStopTicking) + { + // Ticking should be stopped immediately + StopHoudiniTicking(); + return true; + } + + // COOKABLE LOOP + + // Build a set of cookables that need to be processed + // 1 - selected Cookables with Components + // 2 - "Active" Cookables + // 3 - The "next" inactive Cookable + TArray CookablesToProcess; + if (FHoudiniEngineRuntime::IsInitialized()) + { + FHoudiniEngineRuntime::Get().CleanUpRegisteredHoudiniCookables(); + CookableCount = FHoudiniEngineRuntime::Get().GetRegisteredHoudiniCookableCount(); + + // Wrap around if needed + if (CurrentCookableIndex >= CookableCount) + CurrentCookableIndex = 0; + + for (uint32 nIdx = 0; nIdx < CookableCount; nIdx++) + { + UHoudiniCookable* CurrentCookable = FHoudiniEngineRuntime::Get().GetRegisteredHoudiniCookableAt(nIdx); + if (!CurrentCookable || !CurrentCookable->IsValidLowLevelFast()) + { + // Invalid cookable, do not process + continue; + } + else if (!IsValid(CurrentCookable) || CurrentCookable->GetCurrentState() == EHoudiniAssetState::Deleting) + { + // cookable being deleted, do not process + continue; + } + + { + UWorld* World = CurrentCookable->GetWorld(); + if (World && (World->IsPlayingReplay() || World->IsPlayInEditor())) + { + UCookableProxyData* ProxyData = CurrentCookable->GetProxyData(); + if (ProxyData && !ProxyData->bAllowPlayInEditorRefinement) + { + // This cookable's component's world is current in PIE and this HDA is NOT allowed to cook / refine in PIE. + continue; + } + } + } + + // TODO COOKABLE ?? + if (!CurrentCookable->bFullyLoaded) + { + /* + // TODO COOKABLE: BP Support + // Let the component figure out whether it's fully loaded or not. + CurrentCookable->HoudiniEngineTick(); + if (!CurrentCookable->IsFullyLoaded()) + continue; // We need to wait some more. + */ + + // For non BP case - just set fully loaded + CurrentCookable->bFullyLoaded = true; + } + + /* + * // TODO COOKABLE: BP Support + if (!CurrentCookable->IsFullyLoaded()) + { + // Let the component figure out whether it's fully loaded or not. + CurrentCookable->HoudiniEngineTick(); + if (!CurrentCookable->IsFullyLoaded()) + continue; // We need to wait some more. + } + + if (!CurrentComponent->IsValidComponent()) + { + // This component is no longer valid. Prevent it from being processed, and remove it. + FHoudiniEngineRuntime::Get().UnRegisterHoudiniComponent(CurrentComponent); + continue; + } + */ + + AActor* Owner = CurrentCookable->GetOwner(); + if (Owner && Owner->IsSelectedInEditor()) + { + // 1. Add selected cookables + CookablesToProcess.Add(CurrentCookable); + } + else if (CurrentCookable->GetCurrentState() != EHoudiniAssetState::NeedInstantiation + && CurrentCookable->GetCurrentState() != EHoudiniAssetState::None) + { + // 2. Add "Active" cookables, the only two non-active states are: + // NeedInstantiation (loaded, not instantiated in H yet, not modified) + // None (no processing currently) + CookablesToProcess.Add(CurrentCookable); + } + else if (nIdx == CurrentCookableIndex) + { + // 3. Add the "Current" cookable + CookablesToProcess.Add(CurrentCookable); + } + if (CurrentCookable->GetCurrentState() == EHoudiniAssetState::Dormant) + { + CurrentCookable->UpdateDormantStatus(); + } + // Set the LastTickTime on the "current" HAC to 0 to ensure it's treated first + if (nIdx == CurrentIndex) + { + CurrentCookable->LastTickTime = 0.0; + } + } + + // Increment the current index for the next tick + CurrentCookableIndex++; + } + + // Sort the components by last tick time + CookablesToProcess.Sort([](const UHoudiniCookable& A, const UHoudiniCookable& B) { return A.LastTickTime < B.LastTickTime; }); + + // Time limit for processing + double dCookableProcessTimeLimit = CVarHoudiniEngineTickTimeLimit.GetValueOnAnyThread(); + double dCookableProcessStartTime = FPlatformTime::Seconds(); + + // Process all the cookables in the list + for (UHoudiniCookable* CurrentCookable : CookablesToProcess) + { + double dNow = FPlatformTime::Seconds(); + if (dCookableProcessTimeLimit > 0.0 + && dNow - dCookableProcessStartTime > dCookableProcessTimeLimit) + { + HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine Manager: Stopped processing after %f seconds."), (dNow - dCookableProcessStartTime)); + break; + } + + // Update the tick time for this cookable + CurrentCookable->LastTickTime = dNow; + + // Handle template processing (for BP) first + // We don't want to the template component processing to trigger session creation + if (CurrentCookable->GetCurrentState() == EHoudiniAssetState::ProcessTemplate) + { + // TODO COOKABLE: PROCESSBP TEMPLATE + continue; + } + + // Process the cookable + bool bKeepProcessing = true; + while (bKeepProcessing) + { + // See if we should start the default "first" session + if (CurrentCookable->ShouldTryToStartFirstSession()) + { + AutoStartFirstSessionIfNeeded(); + } + + EHoudiniAssetState PrevState = CurrentCookable->GetCurrentState(); + ProcessCookable(CurrentCookable); + EHoudiniAssetState NewState = CurrentCookable->GetCurrentState(); + + // In order to process components faster / with less ticks, + // we may continue processing the component if it ends up in certain states + switch (NewState) + { + case EHoudiniAssetState::NewHDA: + case EHoudiniAssetState::PreInstantiation: + case EHoudiniAssetState::PreCook: + case EHoudiniAssetState::PostCook: + case EHoudiniAssetState::PreProcess: + case EHoudiniAssetState::Processing: + bKeepProcessing = true; + break; + + case EHoudiniAssetState::NeedInstantiation: + case EHoudiniAssetState::Instantiating: + case EHoudiniAssetState::Cooking: + case EHoudiniAssetState::None: + case EHoudiniAssetState::ProcessTemplate: + case EHoudiniAssetState::NeedRebuild: + case EHoudiniAssetState::NeedDelete: + case EHoudiniAssetState::Deleting: + case EHoudiniAssetState::Dormant: + bKeepProcessing = false; + break; + } + + // Stop processing if the state hasn't changed + // for example, if we're waiting for HDA inputs to finish cooking/instantiating + if (PrevState == NewState) + bKeepProcessing = false; + + dNow = FPlatformTime::Seconds(); + if (dCookableProcessTimeLimit > 0.0 && dNow - dCookableProcessStartTime > dCookableProcessTimeLimit) + { + HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine Manager: Stopped processing after %f seconds."), (dNow - dCookableProcessStartTime)); + break; + } + + // Update the tick time for this component + CurrentCookable->LastTickTime = dNow; + } +#if WITH_EDITORONLY_DATA + // See if we need to update this HDA's details panel + if (CurrentCookable->bNeedToUpdateEditorProperties && CurrentCookable->bAllowUpdateEditorProperties) + { + // Only do an update if the HAC is selected + bool bDoUpdateProperties = false; + AActor* Owner = CurrentCookable->GetOwner(); + if (Owner && Owner->IsSelectedInEditor()) + bDoUpdateProperties = true; + else if (!CurrentCookable->AssetEditorId.IsNone()) + bDoUpdateProperties = true; + + if (bDoUpdateProperties) + FHoudiniEngineUtils::UpdateEditorProperties(true); + + CurrentCookable->bNeedToUpdateEditorProperties = false; + } +#endif + } + + // + // Node Deletion + // + + // Handle node delete + if (FHoudiniEngineRuntime::IsInitialized()) + { + int32 PendingDeleteCount = FHoudiniEngineRuntime::Get().GetNodeIdsPendingDeleteCount(); + for (int32 DeleteIdx = PendingDeleteCount - 1; DeleteIdx >= 0; DeleteIdx--) + { + HAPI_NodeId NodeIdToDelete = (HAPI_NodeId)FHoudiniEngineRuntime::Get().GetNodeIdsPendingDeleteAt(DeleteIdx); + FGuid HapiDeletionGUID; + bool bShouldDeleteParent = FHoudiniEngineRuntime::Get().IsParentNodePendingDelete(NodeIdToDelete); + if (StartTaskAssetDelete(NodeIdToDelete, HapiDeletionGUID, bShouldDeleteParent)) + { + FHoudiniEngineRuntime::Get().RemoveNodeIdPendingDeleteAt(DeleteIdx); + if (bShouldDeleteParent) + FHoudiniEngineRuntime::Get().RemoveParentNodePendingDelete(NodeIdToDelete); + } + } + } + + // Update PDG Contexts and asset link if needed + PDGManager.Update(); + + // Session Sync Updates + if (FHoudiniEngine::Get().IsSessionSyncEnabled()) + { + // See if the session sync settings have changed on the houdini side, update ours if they did + FHoudiniEngine::Get().UpdateSessionSyncInfoFromHoudini(); +#if WITH_EDITOR + // Update the Houdini viewport from unreal if needed + if (FHoudiniEngine::Get().IsSyncViewportEnabled()) + { + // Sync the Houdini viewport to Unreal + if (!SyncHoudiniViewportToUnreal()) + { + // If the unreal viewport hasnt changed, + // See if we need to sync the Unreal viewport from Houdini's + SyncUnrealViewportToHoudini(); + } + } +#endif + } + else + { + // reset zero offset variables when session sync is off + if (ZeroOffsetValue != 0.f) + ZeroOffsetValue = 0.f; + + if (bOffsetZeroed) + bOffsetZeroed = false; + } + + return true; +} + +void +FHoudiniEngineManager::AutoStartFirstSessionIfNeeded() +{ + // See if we should start the default "first" session + if (FHoudiniEngine::Get().GetSession() || FHoudiniEngine::Get().GetFirstSessionCreated()) + return; + + FString StatusText = TEXT("Initializing Houdini Engine..."); + FHoudiniEngine::Get().CreateTaskSlateNotification(FText::FromString(StatusText), true, 4.0f); + + // Indicates that we've tried to start the session once no matter if it failed or succeed + FHoudiniEngine::Get().SetFirstSessionCreated(true); + + // Attempt to restart the session + if (!FHoudiniEngine::Get().RestartSession()) + { + // We failed to start the session + // Stop ticking until it's manually restarted + StopHoudiniTicking(); + + StatusText = TEXT("Houdini Engine failed to initialize."); + } + else + { + StatusText = TEXT("Houdini Engine successfully initialized."); + } + + // Finish the notification and display the results + FHoudiniEngine::Get().FinishTaskSlateNotification(FText::FromString(StatusText)); +} + + +void +FHoudiniEngineManager::ProcessCookable(UHoudiniCookable* HC) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable); + + if (!IsValid(HC)) + return; + + // No need to process an HDA cookable that is not tied to an HDA yet.. + if (HC->IsHoudiniAssetSupported() && !HC->HoudiniAssetData->HoudiniAsset) + return; + + UHoudiniAssetComponent* MyHAC = HC->IsComponentSupported() ? Cast(HC->ComponentData->Component) : nullptr; + UHoudiniNodeSyncComponent* MyHNSC = HC->IsComponentSupported() ? Cast(HC->ComponentData->Component) : nullptr; + UHoudiniAssetBlueprintComponent* MyHABC = HC->IsComponentSupported() ? Cast(HC->ComponentData->Component) : nullptr; + + const EHoudiniAssetState CurrentStateToProcess = HC->GetCurrentState(); + + // If cooking is paused, stay in the current state until cooking's resumed, unless we are in NewHDA + if (!FHoudiniEngine::Get().IsCookingEnabled() && CurrentStateToProcess != EHoudiniAssetState::NewHDA) + { + // Refresh UI when pause cooking + if (!FHoudiniEngine::Get().HasUIFinishRefreshingWhenPausingCooking()) + { +#if WITH_EDITORONLY_DATA + // Trigger a details panel update if the Houdini asset actor is selected + if (HC->IsOwnerSelected()) + HC->bNeedToUpdateEditorProperties = true; +#endif + + // Finished refreshing UI of one HDA. + FHoudiniEngine::Get().RefreshUIDisplayedWhenPauseCooking(); + } + + // Prevent any other state change to happen + return; + } + + switch (CurrentStateToProcess) + { + case EHoudiniAssetState::NeedInstantiation: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable - NeedInstantiation); + +#if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 0) + // If this cookable is part of an uneditable level instance, mark it as dormant. + auto* LevelInstance = HC->GetLevelInstance(); + if (LevelInstance && !LevelInstance->IsEditing()) + { + HC->SetCurrentState(EHoudiniAssetState::Dormant); + break; + } +#endif + + // Do nothing unless the HAC has been updated + if (HC->NeedUpdate()) + { + // Call PrePreInstantation on BP component + if(MyHABC) + MyHABC->OnPrePreInstantiation(); + + HC->bForceNeedUpdate = false; + // Update the HAC's state + HC->SetCurrentState(EHoudiniAssetState::PreInstantiation); + } + + /* + // TODO COOKABLE: Might not be needed anymore ? + else if (HC->NeedUpdateInstancedOutputs()) + { + // Output updates do not recquire the HDA to be instantiated + FHoudiniOutputTranslator::UpdateChangedOutputs(MyHAC); + } + */ + + // Update world input if we have any + if(HC->IsInputSupported()) + FHoudiniInputTranslator::UpdateWorldInputs(HC->InputData->Inputs, HC->GetOwner()); + + break; + } + + case EHoudiniAssetState::NewHDA: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable - NewHDA); + + // Update parameters. Since there is no instantiated node yet, this will only fetch the defaults from + // the asset definition. + // + // TODO COOKABLE: this only works if we have both an asset AND parameters! + // + if (HC->IsParameterSupported()) + { + //FHoudiniParameterTranslator::UpdateParameters(HC); + const bool bForceFullUpdate = HC->HasRebuildBeenRequested() || HC->HasRecookBeenRequested() || HC->IsParameterDefinitionUpdateNeeded(); + const bool bCacheRampParms = !HC->HasBeenLoaded() && !HC->HasBeenDuplicated(); + + // Update the parameters + FHoudiniParameterTranslator::UpdateParameters( + HC->GetNodeId(), + HC, + HC->ParameterData->Parameters, + HC->IsHoudiniAssetSupported() ? HC->HoudiniAssetData->HoudiniAsset : nullptr, + HC->IsHoudiniAssetSupported() ? HC->HoudiniAssetData->HapiAssetName : FString(), + bForceFullUpdate, + bCacheRampParms, + HC->bNeedToUpdateEditorProperties); + + // Since the HAC only has the asset definition's default parameter interface, without any asset or node ids, + // we mark it has requiring a parameter definition sync. This will be carried out pre-cook. + HC->ParameterData->bParameterDefinitionUpdateNeeded = true; + } + + HC->SetCurrentState(EHoudiniAssetState::PreInstantiation); + + break; + } + + case EHoudiniAssetState::PreInstantiation: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable - PreInstantiation); + + // + // TODO COOKABLE: HANDLE ASSET INPUT!! + // Only proceed forward if we don't need to wait for our input HoudiniAssets to finish cooking/instantiating + //if (HAC->NeedsToWaitForInputHoudiniAssets()) + // break; + + // Make sure we empty the nodes to cook array to avoid cook errors caused by stale nodes + //if (HC->IsOutputsSupported()) + HC->ClearNodesToCook(); + + EHoudiniAssetState NextState = EHoudiniAssetState::NeedInstantiation; + + // TODO COOKABLE: Better handling of NodeSync components! + if(IsValid(MyHNSC)) + { + // Directly fetch the node + HAPI_NodeId FetchNodeId = -1; + bool bFetchOK = (HAPI_RESULT_SUCCESS == FHoudiniApi::GetNodeFromPath( + FHoudiniEngine::Get().GetSession(), -1, TCHAR_TO_ANSI(*MyHNSC->GetFetchNodePath()), &FetchNodeId)); + + if (bFetchOK) + { + // Set the new node ID + HC->NodeId = FetchNodeId; + + // Assign a unique name to the actor if needed + FHoudiniEngineUtils::AssignUniqueActorLabelIfNeeded(HC->NodeId, HC->GetOwner()); + + // Reset the cook counter. + HC->CookCount = 0; + //if (HC->IsOutputsSupported()) + HC->ClearNodesToCook(); + + // We can go to PreCook + NextState = EHoudiniAssetState::PreCook; + } + else + { + // We couldn't create the node, change the state back to NeedInstantiation + NextState = EHoudiniAssetState::NeedInstantiation; + HC->bRecookRequested = false; + } + } + else + { + // TODO COOKABLE: UPDATE ME! this only supports Cookable with assets... + if (HC->IsHoudiniAssetSupported()) + { + FGuid TaskGuid; + FString HapiAssetName; + UHoudiniAsset* HoudiniAsset = HC->HoudiniAssetData->HoudiniAsset; + if (StartTaskAssetInstantiation(HoudiniAsset, HC->GetDisplayName(), HC->GetNodeLabelPrefix(), TaskGuid, HapiAssetName)) + { + // The cookable is now instantiating + NextState = EHoudiniAssetState::Instantiating; + + // Update the Task GUID + HC->HapiGUID = TaskGuid; + + // Update the HapiAssetName + HC->HoudiniAssetData->HapiAssetName = HapiAssetName; + } + else + { + // We couldnt instantiate the asset, change the state back to NeedInstantiation + NextState = EHoudiniAssetState::NeedInstantiation; + } + } + } + + // Update the Cookable's state + HC->SetCurrentState(NextState); + + break; + } + + case EHoudiniAssetState::Instantiating: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable - Instantiating); + EHoudiniAssetState NewState = EHoudiniAssetState::Instantiating; + if (UpdateInstantiating(HC, NewState , HC->bDoSlateNotifications)) + { + // We need to update the HAC's state + HC->SetCurrentState(NewState); + EnableEditorAutoSave(HC); + } + else + { + DisableEditorAutoSave(HC); + } + break; + } + + case EHoudiniAssetState::PreCook: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable - PreCook); + // Only proceed forward if we don't need to wait for our input + // HoudiniAssets to finish cooking/instantiating + if(HC->IsInputSupported() && HC->InputData->NeedsToWaitForInputHoudiniAssets()) + break; + + if(MyHABC) + MyHABC->OnPrePreCook(); + + // Update all the HAPI nodes, parameters, inputs etc... + PreCook(HC); + + if (MyHABC) + MyHABC->OnPostPreCook(); + + // Create a Cooking task only if necessary + bool bCookStarted = false; + if (IsCookingEnabledForCookable(HC)) + { + TArray NodesToCook; + + bool bUseOutputNodes = true; + bool bOutputTemplateGeos = false; + if (HC->IsOutputSupported()) + { + bUseOutputNodes = HC->OutputData->bUseOutputNodes; + bOutputTemplateGeos = HC->OutputData->bOutputTemplateGeos; + + // Gather output nodes for the HAC + FHoudiniEngineUtils::GatherAllAssetOutputs( + HC->GetNodeId(), + bUseOutputNodes, + bOutputTemplateGeos, + HC->GetEnableCurveEditing(), + NodesToCook); + } + + + HC->SetNodeIdsToCook(NodesToCook); + + FGuid TaskGUID = HC->HapiGUID; + if (StartTaskAssetCooking( + HC->GetNodeId(), + NodesToCook, + HC->GetDisplayName(), + bUseOutputNodes, + bOutputTemplateGeos, + TaskGUID)) + { + // Updates the cookable's state + HC->SetCurrentState(EHoudiniAssetState::Cooking); + HC->HapiGUID = TaskGUID; + bCookStarted = true; + } + } + + if (!bCookStarted) + { + #if WITH_EDITORONLY_DATA + // Just refresh editor properties? + HC->bNeedToUpdateEditorProperties = true; + #endif + HC->SetCurrentState(EHoudiniAssetState::None); + } + break; + } + + case EHoudiniAssetState::Cooking: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable - Cooking); + + bool bCookSuccess = false; + EHoudiniAssetState NewState = EHoudiniAssetState::Cooking; + bool state = UpdateCooking(HC->HapiGUID, HC->GetDisplayName(), NewState, HC->bDoSlateNotifications, bCookSuccess); + if (state) + { + HC->bLastCookSuccess = bCookSuccess; + + // We need to update the HAC's state + HC->SetCurrentState(NewState); + EnableEditorAutoSave(HC); + } + else + { + DisableEditorAutoSave(HC); + } + break; + } + + case EHoudiniAssetState::PostCook: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable - PostCook); + // Handle PostCook + EHoudiniAssetState NewState = EHoudiniAssetState::None; + bool bSuccess = HC->bLastCookSuccess; + + HC->HandleOnPreOutputProcessing(); + + if(MyHABC) + MyHABC->OnPreOutputProcessing(); + + if (PostCook(HC)) + { + // Cook was successful, process the results + NewState = EHoudiniAssetState::PreProcess; + } + else + { + // Cook failed, skip output processing + NewState = EHoudiniAssetState::None; + } + HC->SetCurrentState(NewState); + break; + } + + case EHoudiniAssetState::PreProcess: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable - PreProcess); + StartTaskAssetProcess(HC); + break; + } + + case EHoudiniAssetState::Processing: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable - Processing); + + UpdateProcess(HC); + + HC->HandleOnPostOutputProcessing(); + if (MyHABC) + { + MyHABC->OnPostOutputProcessing(); + FHoudiniEngineUtils::UpdateBlueprintEditor(MyHABC); + } + + // Update the cook count to prevent a cook loop + const int32 CookCount = FHoudiniEngineUtils::HapiGetCookCount(HC->GetNodeId()); + HC->CookCount = CookCount; + + HC->SetCurrentState(EHoudiniAssetState::None); + + break; + } + + case EHoudiniAssetState::None: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable - None); + + // Update world input if we have any + if (HC->IsInputSupported()) + FHoudiniInputTranslator::UpdateWorldInputs(HC->InputData->Inputs, HC->GetOwner()); + + // Update our handles if needed + // This may modify parameters so we need to call this before NeedUpdate + if(HC->IsComponentSupported()) + FHoudiniHandleTranslator::UpdateHandlesIfNeeded(HC->ComponentData->HandleComponents); + + // Do nothing unless the HAC has been updated + if (HC->NeedUpdate()) + { + HC->bForceNeedUpdate = false; + + // Update the HAC's state + // Cook for valid nodes - instantiate for invalid nodes + if (FHoudiniEngineUtils::IsHoudiniNodeValid(HC->GetNodeId())) + HC->SetCurrentState(EHoudiniAssetState::PreCook); + else + { + // Mark as "NeedCook" first to make sure we preserve/upload all params/inputs + HC->MarkAsNeedCook(); + HC->SetCurrentState(EHoudiniAssetState::PreInstantiation); + } + } + else if ( HC->IsComponentSupported() + && HC->ComponentData->bCookOnTransformChange + && HC->ComponentData->bUploadTransformsToHoudiniEngine + && HC->ComponentData->bHasComponentTransformChanged) + { + FHoudiniEngineUtils::UploadCookableTransform(HC); + } + + if (HC->IsComponentSupported()) + { + // See if we need to get an update from Session Sync + bool bEnableLiveSync = FHoudiniEngine::Get().IsSessionSyncEnabled() + && FHoudiniEngine::Get().IsSyncWithHoudiniCookEnabled() + && HC->GetCurrentState() == EHoudiniAssetState::None; + + if (MyHNSC) + { + bEnableLiveSync = MyHNSC ? MyHNSC->GetLiveSyncEnabled() : false; + } + + if (bEnableLiveSync) + { + double dNow = FPlatformTime::Seconds(); + double dLiveSyncTick = CVarHoudiniEngineLiveSyncTickTime.GetValueOnAnyThread(); + if ((dNow - HC->ComponentData->LastLiveSyncPingTime) > dLiveSyncTick) + { + // Update the last live sync ping time for this component + HC->ComponentData->LastLiveSyncPingTime = dNow; + + int32 CookCount = FHoudiniEngineUtils::HapiGetCookCount(HC->GetNodeId()); + if (CookCount >= 0 && CookCount != HC->CookCount) + { + if (HC->bAutoCook) + { + // The cook count has changed on the Houdini side, + // this indicates that the user has changed something in Houdini so we need to trigger an update + HC->SetCurrentState(EHoudiniAssetState::PreCook); + // Make sure to update the cookcount to prevent loop cooking + HC->CookCount = CookCount; + } + } + } + } + } + break; + } + + case EHoudiniAssetState::NeedRebuild: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable - NeedRebuild); + if(HC->IsParameterSupported() && HC->GetNodeId() >= 0) + { + // Make sure no parameters are changed before getting the preset + bool bCleanParamPreset = false; + if (FHoudiniParameterTranslator::UploadChangedParameters( + HC->ParameterData->Parameters, HC->GetNodeId())) + { + if (!FHoudiniEngineUtils::GetAssetPreset(HC->GetNodeId(), HC->ParameterData->ParameterPresetBuffer)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to get the asset's parameter preset, rebuilt asset may have lost its parameters.")); + bCleanParamPreset = true; + } + } + else + { + bCleanParamPreset = true; + } + + // If we failed to update params or get the preset buffer, dont use it + if (bCleanParamPreset) + HC->ParameterData->ParameterPresetBuffer.Empty(); + } + + if (!MyHNSC) + { + // Do not delete nodes for NodeSync components! + StartTaskAssetRebuild(HC->GetNodeId(), HC->HapiGUID); + } + + if (HC->IsPDGSupported()) + { + // We want to check again for PDG after a rebuild + HC->PDGData->bIsPDGAssetLinkInitialized = false; + } + + //HC->MarkAsNeedCook(); + HC->SetCurrentState(EHoudiniAssetState::PreInstantiation); + break; + } + + case EHoudiniAssetState::NeedDelete: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable - NeedDelete); + if (!MyHNSC) + { + // Do not delete nodes for NodeSync components! + FGuid HapiDeletionGUID; + StartTaskAssetDelete(HC->GetNodeId(), HapiDeletionGUID, true); + } + + // Update the HAC's state + HC->SetCurrentState(EHoudiniAssetState::Deleting); + break; + } + + case EHoudiniAssetState::Deleting: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::ProcessCookable - Deleting); + break; + } + + case EHoudiniAssetState::Dormant: + break; + } +} + + + +bool +FHoudiniEngineManager::StartTaskAssetInstantiation( + UHoudiniAsset* HoudiniAsset, + const FString& DisplayName, + const FString& NodeLabelPrefix, + FGuid& OutTaskGUID, + FString& OutHAPIAssetName) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::StartTaskAssetInstantiation); + + // Make sure we have a valid session before attempting anything + if (!FHoudiniEngine::Get().GetSession()) + return false; + + OutTaskGUID.Invalidate(); + + // Load the HDA file + if (!IsValid(HoudiniAsset)) + { + HOUDINI_LOG_ERROR(TEXT("Cancelling asset instantiation - null or invalid Houdini Asset.")); + return false; + } + + HAPI_AssetLibraryId AssetLibraryId = -1; + if (!FHoudiniEngineUtils::LoadHoudiniAsset(HoudiniAsset, AssetLibraryId) ) + { + HOUDINI_LOG_ERROR(TEXT("Cancelling asset instantiation - could not load Houdini Asset.")); + return false; + } + + // Handle hda files that contain multiple assets + TArray AssetNames; + if (!FHoudiniEngineUtils::GetSubAssetNames(AssetLibraryId, AssetNames)) + { + HOUDINI_LOG_ERROR(TEXT("Cancelling asset instantiation - unable to retrieve asset names.")); + return false; + } + + // By default, assume we want to load the first Asset + HAPI_StringHandle PickedAssetName = AssetNames[0]; + +#if WITH_EDITOR + // Should we show the multi asset dialog? + bool bShowMultiAssetDialog = false; + + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (HoudiniRuntimeSettings && AssetNames.Num() > 1) + bShowMultiAssetDialog = HoudiniRuntimeSettings->bShowMultiAssetDialog; + + if (bShowMultiAssetDialog ) + { + if(!FHoudiniEngineUtils::OpenSubassetSelectionWindow(AssetNames, PickedAssetName)) + { + HOUDINI_LOG_ERROR(TEXT("Cancelling asset instantiation - no asset choosen in the selection window.")); + return false; + } + } +#endif + + // Give the HAC a new GUID to identify this request. + OutTaskGUID = FGuid::NewGuid(); + + // Create a new instantiation task + FHoudiniEngineTask Task(EHoudiniEngineTaskType::AssetInstantiation, OutTaskGUID); + Task.Asset = HoudiniAsset; + Task.ActorName = DisplayName; + //Task.bLoadedComponent = bLocalLoadedComponent; + Task.AssetLibraryId = AssetLibraryId; + Task.AssetHapiName = PickedAssetName; + Task.NodeLabelPrefix = NodeLabelPrefix; + + FHoudiniEngineString(PickedAssetName).ToFString(OutHAPIAssetName); + + // Add the task to the stack + FHoudiniEngine::Get().AddTask(Task); + + return true; +} + +bool +FHoudiniEngineManager::UpdateInstantiating(UHoudiniCookable* HC, EHoudiniAssetState& NewState, bool bDoNotifications) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::UpdateInstantiating); + + check(HC); + + // Will return true if the cookable's state need to be updated + NewState = HC->GetCurrentState(); + bool bUpdateState = false; + + // Get the HAC display name for the logs + FString DisplayName = HC->GetDisplayName(); + + // Get the current task's progress + FHoudiniEngineTaskInfo TaskInfo; + if (!UpdateTaskStatus(HC->HapiGUID, TaskInfo, bDoNotifications) + || TaskInfo.TaskType != EHoudiniEngineTaskType::AssetInstantiation) + { + // Couldnt get a valid task info + HOUDINI_LOG_ERROR(TEXT(" %s Failed to instantiate - invalid task"), *DisplayName); + NewState = EHoudiniAssetState::NeedInstantiation; + bUpdateState = true; + return bUpdateState; + } + + bool bSuccess = false; + bool bFinished = false; + switch (TaskInfo.TaskState) + { + case EHoudiniEngineTaskState::Success: + { + bSuccess = true; + bFinished = true; + break; + } + + case EHoudiniEngineTaskState::Aborted: + case EHoudiniEngineTaskState::FinishedWithError: + case EHoudiniEngineTaskState::FinishedWithFatalError: + { + bSuccess = false; + bFinished = true; + break; + } + + case EHoudiniEngineTaskState::None: + case EHoudiniEngineTaskState::Working: + { + bFinished = false; + break; + } + } + + if (!bFinished) + { + // Task is still in progress, nothing to do for now + return false; + } + + if (bSuccess && (TaskInfo.AssetId < 0)) + { + // Task finished successfully but we received an invalid asset ID, error out + HOUDINI_LOG_ERROR(TEXT(" %s Finished Instantiation but received invalid asset id."), *DisplayName); + bSuccess = false; + } + + if (bSuccess) + { + HOUDINI_LOG_MESSAGE(TEXT(" %s FinishedInstantiation."), *DisplayName); + + // Set the new node ID + HC->NodeId = TaskInfo.AssetId; + + // Assign a unique name to the actor if needed + FHoudiniEngineUtils::AssignUniqueActorLabelIfNeeded(HC->NodeId, HC->GetOwner()); + + // Reset the cook counter and nodes to cook + HC->CookCount = 0; + HC->ClearNodesToCook(); + + // If necessary, set asset transform to the component's transform. + if (HC->IsComponentSupported() + && HC->ComponentData->bUploadTransformsToHoudiniEngine + && IsValid(HC->ComponentData->Component.Get())) + { + // Retrieve the current component-to-world transform for this component. + if (!FHoudiniEngineUtils::HapiSetAssetTransform(HC->NodeId, HC->ComponentData->Component->GetComponentTransform())) + HOUDINI_LOG_MESSAGE(TEXT("Failed to upload the initial Transform back to HAPI.")); + } + + // Initial update/create of inputs + if (HC->IsInputSupported()) + { + FHoudiniInputTranslator::UpdateInputs( + HC->GetNodeId(), + HC, + HC->InputData->Inputs, + HC->ParameterData->Parameters, + HC->HasBeenLoaded()); + } + + + // Update the HAC's state + NewState = EHoudiniAssetState::PreCook; + return true; + } + else + { + HOUDINI_LOG_ERROR(TEXT(" %s FinishedInstantiationWithErrors."), *DisplayName); + + bool bLicensingIssue = false; + switch (TaskInfo.Result) + { + case HAPI_RESULT_NO_LICENSE_FOUND: + case HAPI_RESULT_DISALLOWED_NC_LICENSE_FOUND: + { + // No license / Apprentice license found + //FHoudiniEngine::Get().SetHapiState(HAPI_RESULT_NO_LICENSE_FOUND); + FHoudiniEngine::Get().SetSessionStatus(EHoudiniSessionStatus::NoLicense); + bLicensingIssue = true; + break; + } + + case HAPI_RESULT_DISALLOWED_NC_ASSET_WITH_C_LICENSE: + case HAPI_RESULT_DISALLOWED_NC_ASSET_WITH_LC_LICENSE: + case HAPI_RESULT_DISALLOWED_LC_ASSET_WITH_C_LICENSE: + { + bLicensingIssue = true; + break; + } + + default: + { + break; + } + } + + if (bLicensingIssue) + { + const FString& StatusMessage = TaskInfo.StatusText.ToString(); + HOUDINI_LOG_MESSAGE(TEXT("%s"), *StatusMessage); + + FString WarningTitle = TEXT("Houdini Engine Plugin Warning"); + FText WarningTitleText = FText::FromString(WarningTitle); + FString WarningMessage = FString::Printf(TEXT("Houdini License issue - %s."), *StatusMessage); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + FMessageDialog::Debugf(FText::FromString(WarningMessage), WarningTitleText); +#else + FMessageDialog::Debugf(FText::FromString(WarningMessage), &WarningTitleText); +#endif + } + + // Reset the cook counter. + HC->CookCount = 0; + + // Make sure the asset ID is invalid + HC->NodeId = -1; + + // Prevent the HAC from triggering updates in its current state + HC->PreventAutoUpdates(); + + // Update the HAC's state + HC->SetCurrentState(EHoudiniAssetState::NeedInstantiation); + //HAC->AssetStateResult = EHoudiniAssetStateResult::Success; + + return true; + } +} + +bool +FHoudiniEngineManager::StartTaskAssetCooking( + const HAPI_NodeId& AssetId, + const TArray& NodeIdsToCook, + const FString& DisplayName, + bool bUseOutputNodes, + bool bOutputTemplateGeos, + FGuid& OutTaskGUID) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::StartTaskAssetCooking); + + // Make sure we have a valid session before attempting anything + if (!FHoudiniEngine::Get().GetSession()) + return false; + + // Check we have a valid AssetId + if (AssetId < 0) + return false; + + // Check this HAC doesn't already have a running task + if (OutTaskGUID.IsValid()) + return false; + + // Generate a GUID for our new task. + OutTaskGUID = FGuid::NewGuid(); + + // Add a new cook task + FHoudiniEngineTask Task(EHoudiniEngineTaskType::AssetCooking, OutTaskGUID); + Task.ActorName = DisplayName; + Task.AssetId = AssetId; + + if (NodeIdsToCook.Num() > 0) + Task.OtherNodeIds = NodeIdsToCook; + + Task.bUseOutputNodes = bUseOutputNodes; + Task.bOutputTemplateGeos = bOutputTemplateGeos; + + FHoudiniEngine::Get().AddTask(Task); + + return true; +} + +bool +FHoudiniEngineManager::UpdateCooking( + FGuid& HapiTaskGUID, const FString& DisplayName, EHoudiniAssetState& OutNewState, bool bDoNotifications, bool& OutSuccess) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::UpdateCooking); + + // Will return true if the asset's state need to be updated + bool bUpdateState = false; + OutNewState = EHoudiniAssetState::Cooking; + + // Get the current task's progress + FHoudiniEngineTaskInfo TaskInfo; + if (!UpdateTaskStatus(HapiTaskGUID, TaskInfo, bDoNotifications) + || TaskInfo.TaskType != EHoudiniEngineTaskType::AssetCooking) + { + // Couldnt get a valid task info + HOUDINI_LOG_ERROR(TEXT(" %s Failed to cook - invalid task"), *DisplayName); + OutNewState = EHoudiniAssetState::None; + bUpdateState = true; + return bUpdateState; + } + + OutSuccess = false; + switch (TaskInfo.TaskState) + { + case EHoudiniEngineTaskState::Success: + { + HOUDINI_LOG_MESSAGE(TEXT(" %s FinishedCooking."), *DisplayName); + OutSuccess = true; + bUpdateState = true; + } + break; + + case EHoudiniEngineTaskState::FinishedWithError: + { + // We finished with cook error, will still try to process the results + HOUDINI_LOG_MESSAGE(TEXT(" %s FinishedCooking with errors - will try to process the available results."), *DisplayName); + OutSuccess = true; + bUpdateState = true; + } + break; + + case EHoudiniEngineTaskState::Aborted: + case EHoudiniEngineTaskState::FinishedWithFatalError: + { + HOUDINI_LOG_MESSAGE(TEXT(" %s FinishedCooking with fatal errors - aborting."), *DisplayName); + OutSuccess = false; + bUpdateState = true; + } + break; + + case EHoudiniEngineTaskState::None: + case EHoudiniEngineTaskState::Working: + { + // Task is still in progress, nothing to do for now + // return false so we do not update the state + bUpdateState = false; + } + break; + } + + // If the task is still in progress, return now + if (!bUpdateState) + return false; + + // Handle PostCook + OutNewState = EHoudiniAssetState::PostCook; + //HAC->bLastCookSuccess = bSuccess; + + return true; +} + + +bool +FHoudiniEngineManager::PreCook(UHoudiniCookable* HC) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::PreCook); + + if (HC->IsOutputSupported()) + { + // Remove all Cooked (layers) before cooking so we don't received cooked data in Houdini + // if a landscape is input back to the HDA. + for (auto CurOutput : HC->OutputData->Outputs) + { + FHoudiniLandscapeRuntimeUtils::DeleteLandscapeCookedData(CurOutput); + } + } + + + // Handle duplicated HAC + // We need to clean/duplicate some of the HAC's output data manually here + if (HC->HasBeenDuplicated()) + { + HC->UpdatePostDuplicate(); + } + + if (HC->IsParameterSupported()) + { + FHoudiniParameterTranslator::OnPreCookParameters(HC->ParameterData->Parameters); + + if (HC->HasBeenLoaded() || HC->IsParameterDefinitionUpdateNeeded()) + { + if (!HC->ParameterData->ParameterPresetBuffer.IsEmpty()) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::PreCook - SetPreset); + + // We only apply parameters presets for rebuilds + if (HC->HasRebuildBeenRequested()) + { + FHoudiniEngineUtils::SetAssetPreset(HC->GetNodeId(), HC->ParameterData->ParameterPresetBuffer); + } + + HC->ParameterData->ParameterPresetBuffer.Empty(); + } + + // This will sync parameter definitions but not upload values to HAPI or fetch values for existing parameters + // in Unreal. It will creating missing parameters in Unreal. + //FHoudiniParameterTranslator::UpdateLoadedParameters(HAC); + + bool bForceFullUpdate = HC->HasRebuildBeenRequested() || HC->HasRecookBeenRequested() || HC->IsParameterDefinitionUpdateNeeded(); + bool bCacheRampParms = !HC->HasBeenLoaded() && !HC->HasBeenDuplicated(); + FHoudiniParameterTranslator::UpdateLoadedParameters( + HC->GetNodeId(), + HC->ParameterData->Parameters, + HC, + bForceFullUpdate, + bCacheRampParms, + HC->bNeedToUpdateEditorProperties); + + HC->ParameterData->bParameterDefinitionUpdateNeeded = false; + } + } + + // Upload the changed/parameters back to HAPI + // If cooking is disabled, we still try to upload parameters + if (HC->HasBeenLoaded()) + { + // // Handle loaded parameters + // FHoudiniParameterTranslator::UpdateLoadedParameters(HAC); + + if(HC->IsInputSupported()) + { + // Handle loaded inputs + FHoudiniInputTranslator::UpdateInputs( + HC->GetNodeId(), HC, HC->InputData->Inputs, HC->ParameterData->Parameters, HC->HasBeenLoaded()); + } + + if (HC->IsOutputSupported()) + { + // Handle loaded outputs + FHoudiniOutputTranslator::UpdateLoadedOutputs( + HC->GetNodeId(), + HC->GetOutputs(), + HC->GetComponent(), + HC->GetEnableCurveEditing()); + } + + // TODO: Handle loaded curve ? + } + + if (HC->IsParameterSupported()) + { + // Try to upload changed parameters + FHoudiniParameterTranslator::UploadChangedParameters( + HC->ParameterData->Parameters, HC->GetNodeId()); + } + + if (HC->IsInputSupported()) + { + // Try to upload changed inputs + FHoudiniInputTranslator::UploadChangedInputs(HC->InputData->Inputs, HC->GetOwner()); + } + + if (HC->IsOutputSupported()) + { + // Try to upload changed editable nodes + FHoudiniOutputTranslator::UploadChangedEditableOutput(HC->OutputData->Outputs); + } + + // Upload the cookable's transform if needed + if (HC->IsComponentSupported()) + { + if (HC->ComponentData->bHasComponentTransformChanged + && HC->ComponentData->bUploadTransformsToHoudiniEngine) + { + FHoudiniEngineUtils::UploadCookableTransform(HC); + } + } + + HC->ClearRefineMeshesTimer(); + + return true; +} + +bool +FHoudiniEngineManager::PostCook(UHoudiniCookable* HC) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::PostCook); + + // Get the HAC display name for the logs + FString DisplayName = HC->GetDisplayName(); + + //bool bCookSuccess = bLSuccess; + if (HC->bLastCookSuccess && (HC->GetNodeId() < 0)) + { + // Task finished successfully but we received an invalid asset ID, error out + HOUDINI_LOG_ERROR(TEXT(" %s received an invalid asset id - aborting."), *DisplayName); + HC->bLastCookSuccess = false; + } + + // Update the asset cook count using the node infos + const int32 CookCount = FHoudiniEngineUtils::HapiGetCookCount(HC->GetNodeId()); + HC->CookCount = CookCount; + + bool bNeedsToTriggerViewportUpdate = false; + if (HC->bLastCookSuccess) + { + if (HC->bDoSlateNotifications) + FHoudiniEngine::Get().UpdateCookingNotification(FText::FromString(DisplayName + " :\nProcessing outputs..."), false); + + // + // PARAMETERS + // + if (HC->IsParameterSupported()) + { + //FHoudiniParameterTranslator::UpdateParameters(HAC); + + // When recooking/rebuilding the HDA, force a full update of all params + const bool bForceFullUpdate = HC->HasRebuildBeenRequested() || HC->HasRecookBeenRequested() || HC->IsParameterDefinitionUpdateNeeded(); + const bool bCacheRampParms = !HC->HasBeenLoaded() && !HC->HasBeenDuplicated(); + FHoudiniParameterTranslator::UpdateParameters( + HC->GetNodeId(), + HC, + HC->ParameterData->Parameters, + HC->IsHoudiniAssetSupported() ? HC->HoudiniAssetData->HoudiniAsset : nullptr, + HC->IsHoudiniAssetSupported() ? HC->HoudiniAssetData->HapiAssetName : FString(), + bForceFullUpdate, + bCacheRampParms, + HC->bNeedToUpdateEditorProperties); + } + + // + // INPUTS + // + // TODO COOKABLE: Do asset preset after input (obj path param?). + // Set bLoadedInputs to false so as not to delete outputs. + if (HC->IsInputSupported()) + { + // Update our inputs + FHoudiniInputTranslator::UpdateInputs( + HC->GetNodeId(), + HC, + HC->InputData->Inputs, + HC->ParameterData->Parameters, + false); + } + + // Update the HDA's parameter preset + // This needs to be done after inputs and parameters updates + if (HC->IsParameterSupported()) + { + if (!FHoudiniEngineUtils::GetAssetPreset(HC->GetNodeId(), HC->ParameterData->ParameterPresetBuffer)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to get the asset's preset.")); + HC->ParameterData->ParameterPresetBuffer.Empty(); + } + } + + // + // OUTPUTS + // + if (HC->IsOutputSupported()) + { + // Update our output objects + // We will process them at the processing stage + FHoudiniOutputTranslator::UpdateOutputs(HC); + + if(HC->IsProxySupported()) + HC->ProxyData->bNoProxyMeshNextCookRequested = false; + } + + // + // HANDLES + // + // Handles have to be built after the parameters + FHoudiniHandleTranslator::BuildHandles(HC); + + // We can clear the duplication flag + if (HC->HasBeenDuplicated()) + { + HC->bHasBeenDuplicated = false; + } + } + + // Cache the current cook counts of the nodes so that we can more reliable determine + // whether content has changed next time build outputs. + for (int32 NodeId : HC->NodeIdsToCook) + { + int32 NodeCookCount = FHoudiniEngineUtils::HapiGetCookCount(NodeId); + HC->NodesToCookCookCounts.Add(NodeId, CookCount); + } + + // See if we need to initialize the PDG Asset Link for this HDA + if (HC->IsPDGSupported()) + { + if (!HC->PDGData->bIsPDGAssetLinkInitialized) + { + if (FHoudiniPDGManager::IsPDGAsset(HC->NodeId)) + { + UHoudiniPDGAssetLink* PDGAssetLink = HC->PDGData->PDGAssetLink; + if (!PDGManager.InitializePDGAssetLink(HC->NodeId, HC, PDGAssetLink, HC->bHasBeenLoaded)) + HC->PDGData->SetPDGAssetLink(nullptr); + else + HC->PDGData->SetPDGAssetLink(PDGAssetLink); + } + + // Only do this once per cookable - only check again on rebuild + HC->PDGData->bIsPDGAssetLinkInitialized = true; + } + + // Notify the PDG manager that the HDA is done cooking + FHoudiniPDGManager::NotifyAssetCooked(HC->PDGData->PDGAssetLink, HC->bLastCookSuccess); + } + + + // Clear the HasBeenLoaded flag + if (HC->HasBeenLoaded()) + { + HC->bHasBeenLoaded = false; + } + + // If we have downstream cookables, we need to tell them we're done cooking + HC->NotifyCookedToDownstreamCookables(); + + // Clear the rebuild/recook flags + HC->bRecookRequested = false; + HC->bRebuildRequested = false; + + //HC->SyncToBlueprintGeneratedClass(); + + return HC->bLastCookSuccess; +} + + +bool +FHoudiniEngineManager::StartTaskAssetProcess(UHoudiniCookable* HC) +{ + HC->SetCurrentState(EHoudiniAssetState::Processing); + + return true; +} + +bool +FHoudiniEngineManager::UpdateProcess(UHoudiniCookable* HC) +{ + // We should only process after a succesfull cook + if (!HC->bLastCookSuccess) + return false; + + bool bNeedsToTriggerViewportUpdate = false; + bool bHasHoudiniStaticMeshOutput = false; + + // ?? this was unused + //bool bForceOutputUpdate = HAC->HasRebuildBeenRequested() || HAC->HasRecookBeenRequested(); + FHoudiniOutputTranslator::ProcessOutputs(HC, bHasHoudiniStaticMeshOutput); + + if (HC->IsProxySupported()) + HC->ProxyData->bNoProxyMeshNextCookRequested = false; + + // + // COMPONENTS + // + // Component updates if supported + if (HC->IsComponentSupported()) + { + USceneComponent* MyComponent = Cast(HC->GetComponent()); + UHoudiniAssetComponent* MyHAC = Cast(HC->GetComponent()); + + // Update Physics state. + if (MyHAC) + MyHAC->UpdatePhysicsState(); + + if (MyComponent) + { + // Mark Render State as dirty + MyComponent->MarkRenderStateDirty(); + + // Since we have new asset, we need to update bounds. + MyComponent->UpdateBounds(); + } + + if (HC->IsProxySupported()) + { + // If any outputs have HoudiniStaticMeshes, and if timer based refinement is enabled on the HAC, + // set the RefineMeshesTimer and ensure BuildStaticMeshesForAllHoudiniStaticMeshes is bound to + // the RefineMeshesTimerFired delegate of the HAC + if (bHasHoudiniStaticMeshOutput && HC->IsProxyStaticMeshRefinementByTimerEnabled()) + { + if (!HC->ProxyData->GetOnRefineMeshesTimerDelegate().IsBoundToObject(this)) + HC->ProxyData->GetOnRefineMeshesTimerDelegate().AddRaw(this, &FHoudiniEngineManager::BuildStaticMeshesForAllHoudiniStaticMeshes); + HC->SetRefineMeshesTimer(); + } + } + } + +#if WITH_EDITORONLY_DATA + // Indicate we want to trigger a details panel update + HC->bNeedToUpdateEditorProperties = true; +#endif + + if (bHasHoudiniStaticMeshOutput) + bNeedsToTriggerViewportUpdate = true; + + if (bNeedsToTriggerViewportUpdate && GEditor) + { + // We need to manually update the viewport with HoudiniMeshProxies + // if not, modification made in H with the two way debugger wont be visible in Unreal until the viewports gets focus + GEditor->RedrawAllViewports(false); + } + + // Indicate we're done processing the asset + FString DisplayName = HC->GetDisplayName(); + if (HC->bDoSlateNotifications) + FHoudiniEngine::Get().UpdateCookingNotification(FText::FromString(DisplayName + " :\nFinished processing outputs"), true); + + return true; +} + +bool +FHoudiniEngineManager::StartTaskAssetRebuild(const HAPI_NodeId& InAssetId, FGuid& OutTaskGUID) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::StartTaskAssetRebuild); + // Check this HAC doesn't already have a running task + if (OutTaskGUID.IsValid()) + return false; + + if (InAssetId >= 0) + { + // Delete the asset + if (!StartTaskAssetDelete(InAssetId, OutTaskGUID, true)) + { + return false; + } + } + + // Create a new task GUID for this asset + OutTaskGUID = FGuid::NewGuid(); + + return true; +} + +bool +FHoudiniEngineManager::StartTaskAssetDelete(const HAPI_NodeId& InNodeId, FGuid& OutTaskGUID, bool bShouldDeleteParent) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::StartTaskAssetDelete); + + if (InNodeId < 0) + return false; + + // Get the Asset's NodeInfo + HAPI_NodeInfo AssetNodeInfo; + FHoudiniApi::NodeInfo_Init(&AssetNodeInfo); + HOUDINI_CHECK_ERROR(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), InNodeId, &AssetNodeInfo)); + + HAPI_NodeId OBJNodeToDelete = InNodeId; + if (AssetNodeInfo.type == HAPI_NODETYPE_SOP) + { + // For SOP Asset, we want to delete their parent's OBJ node + if (bShouldDeleteParent) + { + HAPI_NodeId ParentId = FHoudiniEngineUtils::HapiGetParentNodeId(OBJNodeToDelete); + OBJNodeToDelete = ParentId != -1 ? ParentId : OBJNodeToDelete; + } + } + + // Generate GUID for our new task. + OutTaskGUID = FGuid::NewGuid(); + + // Create asset deletion task object and submit it for processing. + FHoudiniEngineTask Task(EHoudiniEngineTaskType::AssetDeletion, OutTaskGUID); + Task.AssetId = OBJNodeToDelete; + FHoudiniEngine::Get().AddTask(Task); + + return true; +} + +bool +FHoudiniEngineManager::UpdateTaskStatus(FGuid& OutTaskGUID, FHoudiniEngineTaskInfo& OutTaskInfo, bool bDoNotifications) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::UpdateTaskStatus); + + if (!OutTaskGUID.IsValid()) + return false; + + if (!FHoudiniEngine::Get().RetrieveTaskInfo(OutTaskGUID, OutTaskInfo)) + { + // Task information does not exist + OutTaskGUID.Invalidate(); + return false; + } + + if (bDoNotifications && EHoudiniEngineTaskState::None != OutTaskInfo.TaskState) + { + FHoudiniEngine::Get().UpdateCookingNotification(OutTaskInfo.StatusText, false); + } + + switch (OutTaskInfo.TaskState) + { + case EHoudiniEngineTaskState::Aborted: + case EHoudiniEngineTaskState::FinishedWithError: + case EHoudiniEngineTaskState::FinishedWithFatalError: + { + // If the current task is finished + // Terminate the slate notification if they exist and delete/invalidate the task + if(bDoNotifications) + FHoudiniEngine::Get().UpdateCookingNotification(OutTaskInfo.StatusText, true); + FHoudiniEngine::Get().RemoveTaskInfo(OutTaskGUID); + OutTaskGUID.Invalidate(); + } + break; + + + case EHoudiniEngineTaskState::Success: + { + // End the task + FHoudiniEngine::Get().RemoveTaskInfo(OutTaskGUID); + OutTaskGUID.Invalidate(); + + // Do not terminate cooking/processing notifications + if (OutTaskInfo.TaskType == EHoudiniEngineTaskType::AssetCooking || OutTaskInfo.TaskType == EHoudiniEngineTaskType::AssetProcess) + break; + + // Terminate the current notification + if (bDoNotifications) + FHoudiniEngine::Get().UpdateCookingNotification(OutTaskInfo.StatusText, false); + + } + break; + + case EHoudiniEngineTaskState::Working: + case EHoudiniEngineTaskState::None: + default: + { + break; + } + } + + return true; +} + + +bool +FHoudiniEngineManager::IsCookingEnabledForCookable(UHoudiniCookable* HC) +{ + bool bManualRecook = false; + bool bComponentEnable = false; + if (IsValid(HC)) + { + bManualRecook = HC->HasRecookBeenRequested(); + bComponentEnable = HC->IsCookingEnabled(); + } + + if (bManualRecook) + return true; + + if (bComponentEnable && FHoudiniEngine::Get().IsCookingEnabled()) + return true; + + return false; +} + +void +FHoudiniEngineManager::BuildStaticMeshesForAllHoudiniStaticMeshes(UHoudiniCookable* HC) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::BuildStaticMeshesForAllHoudiniStaticMeshes); + + if (!IsValid(HC)) + { + HOUDINI_LOG_ERROR(TEXT("FHoudiniEngineManager::BuildStaticMeshesForAllHoudiniStaticMeshes called with HC=nullptr")); + return; + } + +#if WITH_EDITOR + AActor *Owner = HC->GetOwner(); + FString Name = Owner ? Owner->GetName() : HC->GetName(); + + FScopedSlowTask Progress(2.0f, FText::FromString(FString::Printf(TEXT("Refining Proxy Mesh to Static Mesh on %s"), *Name))); + Progress.MakeDialog(); + Progress.EnterProgressFrame(1.0f); +#endif + + FHoudiniOutputTranslator::BuildStaticMeshesOnHoudiniProxyMeshOutputs(HC); + +#if WITH_EDITOR + Progress.EnterProgressFrame(1.0f); +#endif +} + + +/* Unreal's viewport representation rules: + Viewport location is the actual camera location; + Lookat position is always right in front of the camera, which means the camera is looking at; + The rotator rotates the forward vector to a direction & orientation, and this dir and orientation is the camera's; + The identity direction and orientation of the camera is facing positive X-axis. +*/ + +/* Hapi's viewport representation rules: + The camera is located at a point on the sphere, which the center is the pivot position and the radius is offset; + Quat determines the location on the sphere and which direction the camera is facing towards, as well as the camera orientation; + The identity location, direction and orientation of the camera is facing positive Z-axis (in Hapi coords); +*/ +bool +FHoudiniEngineManager::SyncHoudiniViewportToUnreal() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::SyncHoudiniViewportToUnreal); + + if (!FHoudiniEngine::Get().IsSyncHoudiniViewportEnabled()) + return false; + +#if WITH_EDITOR + // Get the editor viewport LookAt position to spawn the new objects + if (!GEditor || !GEditor->GetActiveViewport()) + return false; + + FEditorViewportClient* ViewportClient = (FEditorViewportClient*)GEditor->GetActiveViewport()->GetClient(); + if (!ViewportClient) + return false; + + // Get the current UE viewport location, lookat position, and rotation + FVector UnrealViewportPosition = ViewportClient->GetViewLocation(); + FRotator UnrealViewportRotation = ViewportClient->GetViewRotation(); + FVector UnrealViewportLookatPosition = ViewportClient->GetLookAtLocation(); + + /* Check if the Unreal viewport has changed */ + if (UnrealViewportPosition.Equals(SyncedUnrealViewportPosition) && + UnrealViewportRotation.Equals(SyncedUnrealViewportRotation) && + UnrealViewportLookatPosition.Equals(SyncedUnrealViewportLookatPosition)) + { + // No need to sync if the viewport camera hasn't changed + return false; + } + + /* Calculate Hapi Quaternion */ + // Initialize Hapi Quat with Unreal Quat. + // Note that rotations are in general non-commutative *** + FQuat HapiQuat = UnrealViewportRotation.Quaternion(); + + // We're in orbit mode, forward vector is Y-axis + if (ViewportClient->bUsingOrbitCamera) + { + // The forward vector is Y-negative direction when on orbiting mode + HapiQuat = HapiQuat * FQuat::MakeFromEuler(FVector(0.f, 0.f, 180.f)); + + // rotations around X and Y axis are reversed + float TempX = HapiQuat.X; + HapiQuat.X = HapiQuat.Y; + HapiQuat.Y = TempX; + HapiQuat.W = -HapiQuat.W; + + } + // We're not in orbiting mode, forward vector is X-axis + else + { + // Rotate the Quat arount Z-axis by 90 degree. + HapiQuat = HapiQuat * FQuat::MakeFromEuler(FVector(0.f, 0.f, 90.f)); + } + + + /* Update Hapi H_View */ + // Note: There are infinte number of H_View representation for current viewport + // Each choice of pivot point determines an equivalent representation. + // We just find an equivalent when the pivot position is the view position, and offset is 0 + + HAPI_Viewport H_View; + H_View.position[0] = UnrealViewportPosition.X / HAPI_UNREAL_SCALE_FACTOR_TRANSLATION; + H_View.position[1] = UnrealViewportPosition.Z / HAPI_UNREAL_SCALE_FACTOR_TRANSLATION; + H_View.position[2] = UnrealViewportPosition.Y / HAPI_UNREAL_SCALE_FACTOR_TRANSLATION; + + // Set HAPI_Offset always 0 when syncing Houdini to UE viewport + H_View.offset = 0.f; + + H_View.rotationQuaternion[0] = -HapiQuat.X; + H_View.rotationQuaternion[1] = -HapiQuat.Z; + H_View.rotationQuaternion[2] = -HapiQuat.Y; + H_View.rotationQuaternion[3] = HapiQuat.W; + + FHoudiniApi::SetViewport(FHoudiniEngine::Get().GetSession(), &H_View); + + /* Update the Synced viewport values + We need to syced both the viewport representation values in Hapi and UE whenever the viewport is changed. + Since the 2 representations are multi-multi correspondence, the values could be changed even though the viewport is not changing. */ + + // We need to get the H_Viewport again, since it is possible the value is a different equivalence of what we set. + HAPI_Viewport Cur_H_View; + FHoudiniApi::GetViewport( + FHoudiniEngine::Get().GetSession(), &Cur_H_View); + + // Hapi values are in Houdini coordinate and scale + SyncedHoudiniViewportPivotPosition = FVector(Cur_H_View.position[0], Cur_H_View.position[1], Cur_H_View.position[2]); + SyncedHoudiniViewportQuat = FQuat(Cur_H_View.rotationQuaternion[0], Cur_H_View.rotationQuaternion[1], Cur_H_View.rotationQuaternion[2], Cur_H_View.rotationQuaternion[3]); + SyncedHoudiniViewportOffset = Cur_H_View.offset; + + SyncedUnrealViewportPosition = ViewportClient->GetViewLocation(); + SyncedUnrealViewportRotation = ViewportClient->GetViewRotation(); + SyncedUnrealViewportLookatPosition = ViewportClient->GetLookAtLocation(); + + // When sync Houdini to UE, we set offset to be 0. + // So we need to zero out offset for the next time syncing UE to Houdini + bOffsetZeroed = true; + + return true; +#endif + + return false; +} + + +bool +FHoudiniEngineManager::SyncUnrealViewportToHoudini() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineManager::SyncUnrealViewportToHoudini); + + if (!FHoudiniEngine::Get().IsSyncUnrealViewportEnabled()) + return false; + +#if WITH_EDITOR + // Get the editor viewport LookAt position to spawn the new objects + if (!GEditor || !GEditor->GetActiveViewport()) + return false; + + FEditorViewportClient* ViewportClient = (FEditorViewportClient*)GEditor->GetActiveViewport()->GetClient(); + if (!ViewportClient) + return false; + + // Get the current HAPI_Viewport + HAPI_Viewport H_View; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetViewport( + FHoudiniEngine::Get().GetSession(), &H_View)) + { + return false; + } + + + // Get Hapi viewport's PivotPosition, Offset and Quat, w.r.t Houdini's coordinate and scale. + FVector HapiViewportPivotPosition = FVector(H_View.position[0], H_View.position[1], H_View.position[2]); + float HapiViewportOffset = H_View.offset; + FQuat HapiViewportQuat = FQuat(H_View.rotationQuaternion[0], H_View.rotationQuaternion[1], H_View.rotationQuaternion[2], H_View.rotationQuaternion[3]); + + /* Check if the Houdini viewport has changed */ + if (SyncedHoudiniViewportPivotPosition.Equals(HapiViewportPivotPosition) && + SyncedHoudiniViewportQuat.Equals(HapiViewportQuat) && + SyncedHoudiniViewportOffset == HapiViewportOffset) + { + // Houdini viewport hasn't changed, nothing to do + return false; + } + + // Set zero value of offset when needed + if (bOffsetZeroed) + { + ZeroOffsetValue = H_View.offset; + bOffsetZeroed = false; + } + + + /* Translate the hapi camera transfrom to Unreal's representation system */ + + // Get pivot point in UE's coordinate and scale + FVector UnrealViewportPivotPosition = FVector(H_View.position[0], H_View.position[2], H_View.position[1]) * HAPI_UNREAL_SCALE_FACTOR_TRANSLATION; + + // HAPI bug? After we set the H_View, offset becomes a lot bigger when move the viewport just a little bit in Houdini. + // But the pivot point doesn't change. Which caused UE viewport jumping far suddenly. + // So we get rid of this problem by setting the first HAPI_offset value after syncing Houdini viewport as the base. + + // Get offset in UE's scale. The actual offset after 'zero out' + float UnrealOffset = (H_View.offset - ZeroOffsetValue) * HAPI_UNREAL_SCALE_FACTOR_TRANSLATION; + + /* Calculate Quaternion in UE */ + // Rotate the resulting Quat around Z-axis by -90 degree. + // Note that rotation is in general non-commutative *** + FQuat UnrealQuat = FQuat(H_View.rotationQuaternion[0], H_View.rotationQuaternion[2], H_View.rotationQuaternion[1], -H_View.rotationQuaternion[3]); + UnrealQuat = UnrealQuat * FQuat::MakeFromEuler(FVector(0.f, 0.f, -90.f)); + + FVector UnrealBaseVector(1.f, 0.f, 0.f); // Forward vector in Unreal viewport + + /* Get UE viewport location*/ + FVector UnrealViewPosition = - UnrealQuat.RotateVector(UnrealBaseVector) * UnrealOffset + UnrealViewportPivotPosition; + + /* Set the viewport's value */ + ViewportClient->SetViewLocation(UnrealViewPosition); + ViewportClient->SetViewRotation(UnrealQuat.Rotator()); + + // Invalidate the viewport + ViewportClient->Invalidate(); + + /* Update the synced viewport values */ + // We need to syced both the viewport representation values in Hapi and UE whenever the viewport is changed. + // Since the 2 representations are multi-multi correspondence, the values could be changed even though the viewport is not changing. + + // Hapi values are in Houdini coordinate and scale + SyncedHoudiniViewportPivotPosition = HapiViewportPivotPosition; + SyncedHoudiniViewportQuat = HapiViewportQuat; + SyncedHoudiniViewportOffset = HapiViewportOffset; + + SyncedUnrealViewportPosition = ViewportClient->GetViewLocation(); + SyncedUnrealViewportRotation = ViewportClient->GetViewRotation(); + SyncedUnrealViewportLookatPosition = ViewportClient->GetLookAtLocation(); + + return true; +#endif + + return false; +} + + +void +FHoudiniEngineManager::DisableEditorAutoSave(const UObject* InObject) +{ +#if WITH_EDITOR + if (!IsValid(InObject)) + return; + + if (!GUnrealEd) + return; + + if (AutosaveDisablerObjects.Contains(InObject)) + return; + // Add the object to the set + AutosaveDisablerObjects.Add(InObject); + + // Return if auto-saving has already been disabled by some other objects. + if (AutosaveDisablerObjects.Num() > 1) + return; + + // Disable auto-saving by setting min time till auto-save to max float value + IPackageAutoSaver &AutoSaver = GUnrealEd->GetPackageAutoSaver(); + AutoSaver.ForceMinimumTimeTillAutoSave(TNumericLimits::Max()); +#endif +} + + +void +FHoudiniEngineManager::EnableEditorAutoSave(const UObject* InObject = nullptr) +{ +#if WITH_EDITOR + if (!GUnrealEd) + return; + + if (!InObject) + { + // When the object is null, go through all Objects in the set, + // and remove them if it has been deleted. + if (AutosaveDisablerObjects.Num() <= 0) + return; + + TSet> ValidObjects; + for (auto& CurObject : AutosaveDisablerObjects) + { + if (CurObject.IsValid()) + { + ValidObjects.Add(CurObject); + } + } + AutosaveDisablerObjects = MoveTemp(ValidObjects); + } + else + { + // Otherwise, remove the HAC from the set + if (AutosaveDisablerObjects.Contains(InObject)) + AutosaveDisablerObjects.Remove(InObject); + } + + if (AutosaveDisablerObjects.Num() > 0) + return; + + // When no HAC disables cooking, reset min time till auto-save to default value, then reset the timer + IPackageAutoSaver &AutoSaver = GUnrealEd->GetPackageAutoSaver(); + AutoSaver.ResetAutoSaveTimer(); +#endif +} + + + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineManager.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "TimerManager.h" +#include "Containers/Ticker.h" + +//#include "HAL/Runnable.h" +//#include "HAL/RunnableThread.h" +//#include "Misc/SingleThreadRunnable.h" + +#include "HoudiniPDGManager.h" + +class UHoudiniAsset; +class UHoudiniAssetComponent; +class UHoudiniCookable; + +struct FHoudiniEngineTaskInfo; +struct FGuid; + +enum class EHoudiniAssetState : uint8; + +class HOUDINIENGINE_API FHoudiniEngineManager +{ +public: + + FHoudiniEngineManager(); + virtual ~FHoudiniEngineManager(); + + void StartHoudiniTicking(); + void StopHoudiniTicking(); + bool IsTicking() const; + + bool Tick(float DeltaTime); + + // Updates / Process a cookable + void ProcessCookable(UHoudiniCookable* HC); + + // Build UStaticMesh for all UHoudiniStaticMesh on a Cookable. + // This is fired by the OnRefinedMeshesTimerDelegate on a Cookable. + void BuildStaticMeshesForAllHoudiniStaticMeshes(UHoudiniCookable* HC); + + void StartPDGCommandlet() + { + if (!IsPDGCommandletRunningOrConnected()) + PDGManager.CreateBGEOCommandletAndEndpoint(); + } + + void StopPDGCommandlet() + { + if (IsPDGCommandletRunningOrConnected()) + PDGManager.StopBGEOCommandletAndEndpoint(); + } + + bool IsPDGCommandletRunningOrConnected() + { + const EHoudiniBGEOCommandletStatus Status = PDGManager.UpdateAndGetBGEOCommandletStatus(); + return Status == EHoudiniBGEOCommandletStatus::Running || Status == EHoudiniBGEOCommandletStatus::Connected; + } + + bool IsPDGCommandletConnected() + { + const EHoudiniBGEOCommandletStatus Status = PDGManager.UpdateAndGetBGEOCommandletStatus(); + return Status == EHoudiniBGEOCommandletStatus::Connected; + } + + EHoudiniBGEOCommandletStatus GetPDGCommandletStatus() { return PDGManager.UpdateAndGetBGEOCommandletStatus(); } + + // Automatically try to start the First HE session if needed + void AutoStartFirstSessionIfNeeded(); + +protected: + + // Updates a given task's status + // Returns true if the given task's status was properly found + bool UpdateTaskStatus( + FGuid& OutTaskGUID, + FHoudiniEngineTaskInfo& OutTaskInfo, + bool bNotifications = true); + + // Start a task to instantiate the given HoudiniAsset + // Return true if the task was successfully created + bool StartTaskAssetInstantiation( + UHoudiniAsset* HoudiniAsset, + const FString& DisplayName, + const FString& NodeLabelPrefix, + FGuid& OutTaskGUID, + FString& OutHAPIAssetName); + + // Updates progress of the instantiation task + // Returns true if a state change should be made + bool UpdateInstantiating( + UHoudiniCookable* HC, EHoudiniAssetState& NewState, bool bDoNotifications); + + // Start a task to instantiate the Houdini Asset with the given node Id + // Returns true if the task was successfully created + bool StartTaskAssetCooking( + const HAPI_NodeId& AssetId, + const TArray& NodeIdsToCook, + const FString& DisplayName, + bool bUseOutputNodes, + bool bOutputTemplateGeos, + FGuid& OutTaskGUID); + + // Updates progress of the cooking task + // Returns true if a state change should be made + bool UpdateCooking( + FGuid& HapiGUID, + const FString& DisplayName, + EHoudiniAssetState& OutNewState, + bool bDoNotifications, + bool& OutSuccess); + + // Called to update template components. + //bool PreCookTemplate(UHoudiniAssetComponent* HAC); + + // Called to update all houdini nodes/params/inputs before a cook has started + bool PreCook(UHoudiniCookable* HC); + + // Called after a cook has finished + bool PostCook(UHoudiniCookable* HC); + + bool StartTaskAssetProcess(UHoudiniCookable* HC); + bool UpdateProcess(UHoudiniCookable* HC); + + // Starts a rebuild task (delete then re instantiate) + // The NodeID should be invalidated after a successful call + bool StartTaskAssetRebuild( + const HAPI_NodeId& InAssetId, + FGuid& OutTaskGUID); + + // Starts a node delete task + // The NodeID should be invalidated after a successful call + bool StartTaskAssetDelete( + const HAPI_NodeId& InAssetId, + FGuid& OutTaskGUID, + bool bShouldDeleteParent); + + bool IsCookingEnabledForCookable(UHoudiniCookable* HC); + + // Syncs the houdini viewport to Unreal's viewport + // Returns true if the Houdini viewport has been modified + bool SyncHoudiniViewportToUnreal(); + + // Syncs the unreal viewport to Houdini's viewport + // Returns true if the Unreal viewport has been modified + bool SyncUnrealViewportToHoudini(); + + // Disable auto save by setting min time till auto save to the max value + void DisableEditorAutoSave(const UObject* InObject); + // Enable auto-save if we no longer have an object preventing autosaving + void EnableEditorAutoSave(const UObject* InObject); + +private: + + // Ticker handle, used for processing HAC. + FTSTicker::FDelegateHandle TickerHandle; + + // Current position in the component array + uint32 CurrentIndex; + // Current number of components in the array + uint32 ComponentCount; + + // Current position in the cookable array + uint32 CurrentCookableIndex; + // Current number of cookables in the array + uint32 CookableCount; + + // Stopping flag. + // Indicates that we should stop ticking asap + bool bMustStopTicking; + + // The PDG Manager, handles all registered PDG Asset Links + FHoudiniPDGManager PDGManager; + + // For ViewportSync: The camera transform that Hapi and Unreal currently agree with. + FVector SyncedHoudiniViewportPivotPosition; + FQuat SyncedHoudiniViewportQuat; + float SyncedHoudiniViewportOffset; + + FVector SyncedUnrealViewportPosition; + FRotator SyncedUnrealViewportRotation; + FVector SyncedUnrealViewportLookatPosition; + + // We need these two variables to get rid of a HAPI bug + // Note: When sync Houdini to UE, we set the pivot position to be the view position, and offset to be 0.0. + // but when we switch to control viewport in Houdini, HAPI returns an H_View with a large offset, but pivot unchanged. + // so, we need these two variables to 'zero out' offset. + float ZeroOffsetValue; // in HAPI scale + bool bOffsetZeroed; + + // Indicates which objects disable auto-saving + TSet> AutosaveDisablerObjects; +}; +``` + +`Source/HoudiniEngine/Private/HoudiniEngineOutputStats.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineOutputStats.h" + +FHoudiniEngineOutputStats::FHoudiniEngineOutputStats() + : NumPackagesCreated(0) + , NumPackagesUpdated(0) +{ } + +void FHoudiniEngineOutputStats::NotifyPackageCreated(int32 NumCreated) +{ + NumPackagesCreated += NumCreated; +} + +void FHoudiniEngineOutputStats::NotifyPackageUpdated(int32 NumUpdated) +{ + NumPackagesUpdated += NumUpdated; +} + +void FHoudiniEngineOutputStats::NotifyObjectsCreated(const FString& ObjectTypeName, int32 NumCreated) +{ + const int32 Count = OutputObjectsCreated.FindOrAdd(ObjectTypeName, 0); + OutputObjectsCreated[ObjectTypeName] = Count + NumCreated; +} + +void FHoudiniEngineOutputStats::NotifyObjectsUpdated(const FString& ObjectTypeName, int32 NumUpdated) +{ + const int32 Count = OutputObjectsUpdated.FindOrAdd(ObjectTypeName, 0); + OutputObjectsUpdated[ObjectTypeName] = Count + NumUpdated; +} + +void FHoudiniEngineOutputStats::NotifyObjectsReplaced(const FString& ObjectTypeName, int32 NumReplaced) +{ + const int32 Count = OutputObjectsReplaced.FindOrAdd(ObjectTypeName, 0); + OutputObjectsReplaced[ObjectTypeName] = Count + NumReplaced; +} +``` + +`Source/HoudiniEngine/Private/HoudiniEngineOutputStats.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Class.h" + +struct HOUDINIENGINE_API FHoudiniEngineOutputStats +{ + FHoudiniEngineOutputStats(); + + int32 NumPackagesCreated; + int32 NumPackagesUpdated; + + // These FStrings should preferably be EHoudiniOutputType enum + // Move the OUtput enums into a separate header to avoid circular dependencies. + TMap OutputObjectsCreated; + TMap OutputObjectsUpdated; + TMap OutputObjectsReplaced; + + void NotifyPackageCreated(int32 NumCreated); + void NotifyPackageUpdated(int32 NumUpdated); + + // Objects created + void NotifyObjectsCreated(const FString& ObjectTypeName, int32 NumCreated); + template + void NotifyObjectsCreated(EnumT EnumValue, int32 NumCreated) + { + NotifyObjectsCreated( UEnum::GetValueAsString(EnumValue), NumCreated ); + } + + // Object updated + void NotifyObjectsUpdated(const FString& ObjectTypeName, int32 NumUpdated); + template + void NotifyObjectsUpdated(EnumT EnumValue, int32 NumUpdated) + { + NotifyObjectsUpdated( UEnum::GetValueAsString(EnumValue), NumUpdated ); + } + + // Objects replaced + void NotifyObjectsReplaced(const FString& ObjectTypeName, int32 NumReplaced); + template + void NotifyObjectsReplaced(EnumT EnumValue, int32 NumReplaced) + { + NotifyObjectsReplaced( UEnum::GetValueAsString(EnumValue), NumReplaced ); + } +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniEnginePrivatePCH.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +// Indicate we're in the HoudiniEngine module +#define HOUDINI_ENGINE +#include "HoudiniEngineRuntimePrivatePCH.h" + +// HFS path definition coming from UBT/build.cs files. +#ifndef HOUDINI_ENGINE_HFS_PATH_DEFINE + #define HOUDINI_ENGINE_HFS_PATH "" +#else + #define HOUDINI_ENGINE_STRINGIFY_HELPER(X) #X + #define HOUDINI_ENGINE_STRINGIFY(X) HOUDINI_ENGINE_STRINGIFY_HELPER(X) + #define HOUDINI_ENGINE_HFS_PATH HOUDINI_ENGINE_STRINGIFY(HOUDINI_ENGINE_HFS_PATH_DEFINE) +#endif + +// HFS subfolder containing HAPI lib. +#define HAPI_HFS_SUBFOLDER_WINDOWS TEXT( "bin" ) +#define HAPI_HFS_SUBFOLDER_MAC TEXT( "dsolib" ) +#define HAPI_HFS_SUBFOLDER_LINUX TEXT( "dsolib" ) + +// Unreal HAPI Resources. +#define HAPI_UNREAL_RESOURCE_HOUDINI_LOGO TEXT( "/HoudiniEngine/houdini_logo.houdini_logo" ) +#define HAPI_UNREAL_RESOURCE_HOUDINI_MATERIAL TEXT( "/HoudiniEngine/houdini_default_material.houdini_default_material" ) +#define HAPI_UNREAL_RESOURCE_HOUDINI_TEMPLATE_MATERIAL TEXT( "/HoudiniEngine/houdini_templated_material.houdini_templated_material") +#define HAPI_UNREAL_RESOURCE_BGEO_IMPORT TEXT( "/HoudiniEngine/houdini_bgeo_import.houdini_bgeo_import" ) +#define HAPI_UNREAL_RESOURCE_HOUDINI_COP_MESH TEXT( "/HoudiniEngine/houdini_default_cop_mesh" ) +#define HAPI_UNREAL_RESOURCE_EDITOR_PLANE TEXT( "/Engine/EditorMeshes/EditorPlane" ) +#define HAPI_UNREAL_RESOURCE_HOUDINI_COP_MATERIAL TEXT( "/HoudiniEngine/houdini_default_cop_material" ) + +#define HAPI_UNREAL_RESOURCE_HOUDINI_DEFAULT_REFERENCE_MESH TEXT("/HoudiniEngine/default_reference_static_mesh.default_reference_static_mesh") +#define HAPI_UNREAL_RESOURCE_HOUDINI_DEFAULT_REFERENCE_MESH_MATERIAL TEXT("/HoudiniEngine/default_reference_static_mesh_material.default_reference_static_mesh_material") + +// Client name so HAPI knows we're running inside unreal +#define HAPI_UNREAL_CLIENT_NAME "unreal" + +// Error checking - this macro will check the status and return specified parameter. +#define HOUDINI_CHECK_ERROR_RETURN_HELPER( HAPI_PARAM_CALL, HAPI_PARAM_RETURN, HAPI_LOG_ROUTINE ) \ + do \ + { \ + HAPI_Result ResultVariable = HAPI_PARAM_CALL; \ + if ( ResultVariable != HAPI_RESULT_SUCCESS ) \ + { \ + HAPI_LOG_ROUTINE( TEXT( "Hapi failed: %s (%s:%d)" ), *FHoudiniEngineUtils::GetErrorDescription(), TEXT(__FILE__), __LINE__); \ + return HAPI_PARAM_RETURN; \ + } \ + } \ + while ( 0 ) + +#define HOUDINI_CHECK_ERROR_RETURN( HAPI_PARAM_CALL, HAPI_PARAM_RETURN ) \ + HOUDINI_CHECK_ERROR_RETURN_HELPER( HAPI_PARAM_CALL, HAPI_PARAM_RETURN, HOUDINI_LOG_ERROR ) + +// Simple Error checking - this macro will check the status. +#define HOUDINI_CHECK_ERROR_HELPER( HAPI_PARAM_CALL, HAPI_LOG_ROUTINE ) \ + do \ + { \ + HAPI_Result ResultVariable = HAPI_PARAM_CALL; \ + if ( ResultVariable != HAPI_RESULT_SUCCESS ) \ + { \ + HAPI_LOG_ROUTINE( TEXT( "Hapi failed: %s (%s:%d)" ), *FHoudiniEngineUtils::GetErrorDescription(), TEXT(__FILE__), __LINE__); \ + } \ + } \ + while ( 0 ) + +#define HOUDINI_CHECK_ERROR( HAPI_PARAM_CALL ) \ + HOUDINI_CHECK_ERROR_HELPER( HAPI_PARAM_CALL, HOUDINI_LOG_ERROR ) + +// Error checking - this macro will check the status and returns it. +#define HOUDINI_CHECK_ERROR_GET_HELPER( HAPI_PARAM_RESULT, HAPI_PARAM_CALL, HAPI_LOG_ROUTINE ) \ + do \ + { \ + *HAPI_PARAM_RESULT = HAPI_PARAM_CALL; \ + if ( *HAPI_PARAM_RESULT != HAPI_RESULT_SUCCESS ) \ + { \ + HAPI_LOG_ROUTINE( TEXT( "Hapi failed: %s (%s:%d)" ), *FHoudiniEngineUtils::GetErrorDescription(), TEXT(__FILE__), __LINE__); \ + } \ + } \ + while ( 0 ) + +#define HOUDINI_CHECK_ERROR_GET( HAPI_PARAM_RESULT, HAPI_PARAM_CALL ) \ + HOUDINI_CHECK_ERROR_GET_HELPER( HAPI_PARAM_RESULT, HAPI_PARAM_CALL, HOUDINI_LOG_ERROR ) + +// For Transform conversion between UE4 / Houdini +// Set to 0 to stop converting Houdini's coordinate space to unreal's +#define HAPI_UNREAL_CONVERT_COORDINATE_SYSTEM 1 + +#define HAPI_UNREAL_SCALE_FACTOR_POSITION 100.0f +#define HAPI_UNREAL_SCALE_FACTOR_TRANSLATION 100.0f + +#define HAPI_UNREAL_SCALE_SMALL_VALUE KINDA_SMALL_NUMBER * 2.0f + +#define HAPI_UNREAL_DEFAULT_MATERIAL_NAME TEXT( "default_material" ) + +// Attributes +#define HAPI_UNREAL_ATTRIB_POSITION HAPI_ATTRIB_POSITION +#define HAPI_UNREAL_ATTRIB_ROTATION "rot" +#define HAPI_UNREAL_ATTRIB_SCALE "scale" +#define HAPI_UNREAL_ATTRIB_UNIFORM_SCALE "pscale" +#define HAPI_UNREAL_ATTRIB_BBOX_MIN "unreal_bbox_min" +#define HAPI_UNREAL_ATTRIB_BBOX_MAX "unreal_bbox_max" +#define HAPI_UNREAL_ATTRIB_COLOR HAPI_ATTRIB_COLOR +#define HAPI_UNREAL_ATTRIB_LIGHTMAP_COLOR "unreal_lightmap_color" +#define HAPI_UNREAL_ATTRIB_ALPHA "Alpha" +#define HAPI_UNREAL_ATTRIB_UV HAPI_ATTRIB_UV +#define HAPI_UNREAL_ATTRIB_NORMAL HAPI_ATTRIB_NORMAL +#define HAPI_UNREAL_ATTRIB_TANGENTU HAPI_ATTRIB_TANGENT +#define HAPI_UNREAL_ATTRIB_TANGENTV HAPI_ATTRIB_TANGENT2 +#define HAPI_UNREAL_ATTRIB_DISABLE_GAMMA_CORRECTION "unreal_disable_gamma_correction" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_TILE "tile" +// Always the name of the main landscape actor. +// Names for landscape tile actors will be taken from 'unreal_output_name'. +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SHARED_ACTOR_NAME "unreal_landscape_shared_actor_name" + +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_MIN "unreal_landscape_layer_min" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_MAX "unreal_landscape_layer_max" + +// Landscape controls +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_PARTITION_GRID_SIZE "unreal_landscape_partition_grid_size" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SECTION_SIZE "unreal_landscape_section_size" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SECTIONS_PER_COMPONENT "unreal_landscape_sections_per_component" + +// Path to the level in which an actor should be generated or which contained the input data +// "." - (Default) Generate geometry in the the current persistent world +// "Junk/Background" - Path to a Map that is relative to the current persistent world's Map. +// "/Game/Maps/Level01/Junk/Background" - Absolute path to the map in which the primitive should be output +#define HAPI_UNREAL_ATTRIB_LEVEL_PATH "unreal_level_path" + +// Path to the actor that contained the input data/should be generated +#define HAPI_UNREAL_ATTRIB_ACTOR_PATH "unreal_actor_path" + +// Path to the object plugged in a geo in +#define HAPI_UNREAL_ATTRIB_OBJECT_PATH "unreal_object_path" + +// Attributes used for data exchange between UE4 and Houdini +#define HAPI_UNREAL_ATTRIB_MATERIAL "unreal_material" +#define HAPI_UNREAL_ATTRIB_MATERIAL_SLOT "unreal_material_slot" +#define HAPI_UNREAL_ATTRIB_MATERIAL_FALLBACK "unreal_face_material" +#define HAPI_UNREAL_ATTRIB_MATERIAL_INSTANCE "unreal_material_instance" +#define HAPI_UNREAL_ATTRIB_MATERIAL_HOLE "unreal_material_hole" +#define HAPI_UNREAL_ATTRIB_MATERIAL_HOLE_INSTANCE "unreal_material_hole_instance" +#define HAPI_UNREAL_ATTRIB_PHYSICAL_MATERIAL "unreal_physical_material" +#define HAPI_UNREAL_ATTRIB_SIMPLE_PHYSICAL_MATERIAL "unreal_simple_physical_material" +#define HAPI_UNREAL_ATTRIB_FACE_SMOOTHING_MASK "unreal_face_smoothing_mask" +#define HAPI_UNREAL_ATTRIB_LIGHTMAP_RESOLUTION "unreal_lightmap_resolution" +#define HAPI_UNREAL_ATTRIB_LOD_SCREENSIZE "lod_screensize" +#define HAPI_UNREAL_ATTRIB_LOD_SCREENSIZE_PREFIX "lod" +#define HAPI_UNREAL_ATTRIB_LOD_SCREENSIZE_POSTFIX "_screensize" +#define HAPI_UNREAL_ATTRIB_TAG_PREFIX "unreal_uproperty_tag_" +#define HAPI_UNREAL_ATTRIB_TAG_KEEP "unreal_keep_tags" +#define HAPI_UNREAL_ATTRIB_HLOD_LAYER "unreal_hlod_layer" + +#define HAPI_UNREAL_ATTRIB_MESH_SOCKET_PREFIX "mesh_socket" +#define HAPI_UNREAL_ATTRIB_MESH_SOCKET_NAME "mesh_socket_name" +#define HAPI_UNREAL_ATTRIB_MESH_SOCKET_BONE_NAME "mesh_socket_bone_name" +#define HAPI_UNREAL_ATTRIB_MESH_SOCKET_NAME_OLD "unreal_mesh_socket_name" +#define HAPI_UNREAL_ATTRIB_MESH_SOCKET_ACTOR "mesh_socket_actor" +#define HAPI_UNREAL_ATTRIB_MESH_SOCKET_ACTOR_OLD "unreal_mesh_socket_actor" +#define HAPI_UNREAL_ATTRIB_MESH_SOCKET_TAG "mesh_socket_tag" +#define HAPI_UNREAL_ATTRIB_MESH_SOCKET_TAG_OLD "unreal_mesh_socket_tag" + +#define HAPI_UNREAL_ATTRIB_NANITE_ENABLED "unreal_nanite_enabled" +#define HAPI_UNREAL_ATTRIB_NANITE_POSITION_PRECISION "unreal_nanite_position_precision" +#define HAPI_UNREAL_ATTRIB_NANITE_PERCENT_TRIANGLES "unreal_nanite_percent_triangles" +#define HAPI_UNREAL_ATTRIB_NANITE_FB_RELATIVE_ERROR "unreal_nanite_fallback_relative_error" +#define HAPI_UNREAL_ATTRIB_NANITE_TRIM_RELATIVE_ERROR "unreal_nanite_trim_relative_error" + +#define HAPI_UNREAL_ATTRIB_INPUT_MESH_NAME "unreal_input_mesh_name" +#define HAPI_UNREAL_ATTRIB_INPUT_SOURCE_FILE "unreal_input_source_file" + +#define HAPI_UNREAL_ATTRIB_NUM_CUSTOM_PRIM_DATA "unreal_num_custom_primitive_data" +#define HAPI_UNREAL_ATTRIB_CUSTOM_PRIMITIVE_DATA_PREFIX "unreal_custom_primitive_data" + +#define HAPI_UNREAL_ATTRIB_INSTANCE "instance" +#define HAPI_UNREAL_ATTRIB_INSTANCE_OVERRIDE "unreal_instance" +#define HAPI_UNREAL_ATRTIB_INSTANCE_ORIGIN "unreal_instance_origin" +#define HAPI_UNREAL_ATTRIB_FOLIAGE_INSTANCER "unreal_foliage" +#define HAPI_UNREAL_ATTRIB_FOLIAGE_ATTACHMENT_TYPE "unreal_foliage_attachment_type" +#define HAPI_UNREAL_ATTRIB_FOLIAGE_ATTACHMENT_DISTANCE "unreal_foliage_attachment_distance" +#define HAPI_UNREAL_ATTRIB_INSTANCE_ROTATION "rot" +#define HAPI_UNREAL_ATTRIB_INSTANCE_SCALE "scale" +#define HAPI_UNREAL_ATTRIB_INSTANCE_POSITION HAPI_ATTRIB_POSITION +#define HAPI_UNREAL_ATTRIB_SPLIT_ATTR "unreal_split_attr" +#define HAPI_UNREAL_ATTRIB_HIERARCHICAL_INSTANCED_SM "unreal_hierarchical_instancer" +#define HAPI_UNREAL_ATTRIB_INSTANCE_NUM_CUSTOM_FLOATS "unreal_num_custom_floats" +#define HAPI_UNREAL_ATTRIB_INSTANCE_CUSTOM_DATA_PREFIX "unreal_per_instance_custom_data" +#define HAPI_UNREAL_ATTRIB_FORCE_INSTANCER "unreal_force_instancer" + +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_TILE_NAME HAPI_ATTRIB_NAME +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_VERTEX_INDEX "unreal_vertex_index" +#define HAPI_UNREAL_ATTRIB_UNIT_LANDSCAPE_LAYER "unreal_unit_landscape_layer" +#define HAPI_UNREAL_ATTRIB_NONWEIGHTBLENDED_LAYERS "unreal_landscape_layer_nonweightblended" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_STREAMING_PROXY "unreal_landscape_streaming_proxy" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_INFO "unreal_landscape_layer_info" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SIZE "unreal_landscape_size" + +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_WRITE_LOCKED_LAYERS "unreal_landscape_write_locked_layer" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_LOCK_LAYERS "unreal_landscape_lock_layer" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_NORMALIZE "unreal_landscape_layer_normalize" + +#define HAPI_UNREAL_ATTRIB_CREATE_DATA_LAYERS "unreal_create_data_layers" + +#define HAPI_UNREAL_ATTRIB_CREATE_DEFAULT_PHYSICS_ASSET "unreal_create_default_physics_asset" +#define HAPI_UNREAL_ATTRIB_PHYSICS_BONE "unreal_physics_bone" + +// Enable or disable the NoWeightBlend setting for landscape paint layers. +// Note this attribute supercedes the unreal_landscape_layer_nonweightblended string attribute. +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_NOWEIGHTBLEND "unreal_landscape_layer_noweightblend" +#define HAPI_UNREAL_LANDSCAPE_LAYER_NOWEIGHTBLEND_OFF 0 +#define HAPI_UNREAL_LANDSCAPE_LAYER_NOWEIGHTBLEND_ON 1 + +// Landscape output mode: +// 0 - Generate (generate a landscape from scratch) +// 1 - Modify Layer (modify one or more landscape layers only) +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_OUTPUT_MODE "unreal_landscape_output_mode" +#define HAPI_UNREAL_LANDSCAPE_OUTPUT_MODE_GENERATE 0 +#define HAPI_UNREAL_LANDSCAPE_OUTPUT_MODE_MODIFY_LAYER 1 + +// Edit layer +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_NAME "unreal_landscape_editlayer_name" +// Clear the editlayer before blitting new data +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_CLEAR "unreal_landscape_editlayer_clear" +// Place the output layer "after" the given layer +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_AFTER "unreal_landscape_editlayer_after" +// Landscape that is being targeted by "edit layer" outputs (only used in Modify Layer mode) +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_TARGET "unreal_landscape_editlayer_target" + +// Edit Layer types: +// 0 - Base layer: Values will be fit to the min/max height range in UE for optimal resolution. +// 1 - Additive layer: Values will be scaled similar to the base layer but will NOT be offset +// so that it will remain centered around the zero value. +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_TYPE "unreal_landscape_editlayer_type" +#define HAPI_UNREAL_LANDSCAPE_EDITLAYER_TYPE_BASE 0 +#define HAPI_UNREAL_LANDSCAPE_EDITLAYER_TYPE_ADDITIVE 1 + +// Subtractive mode for paint layers on landscape edit layers +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_SUBTRACTIVE "unreal_landscape_editlayer_subtractive" +#define HAPI_UNREAL_LANDSCAPE_EDITLAYER_SUBTRACTIVE_ON 0 +#define HAPI_UNREAL_LANDSCAPE_EDITLAYER_SUBTRACTIVE_OFF 1 + +// landscape splines +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE "unreal_landscape_spline" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_TARGET_LANDSCAPE "unreal_landscape_spline_target_landscape" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_ID "unreal_landscape_spline_control_point_id" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_PAINT_LAYER_NAME "unreal_landscape_spline_control_point_paint_layer_name" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_RAISE_TERRAIN "unreal_landscape_spline_control_point_raise_terrain" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_LOWER_TERRAIN "unreal_landscape_spline_control_point_lower_terrain" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_MESH "unreal_landscape_spline_control_point_mesh" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_PAINT_LAYER_NAME "unreal_landscape_spline_segment_paint_layer_name" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_RAISE_TERRAIN "unreal_landscape_spline_segment_raise_terrain" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_LOWER_TERRAIN "unreal_landscape_spline_segment_lower_terrain" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_MESH "unreal_landscape_spline_segment_mesh" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_MATERIAL_OVERRIDE_SUFFIX "_material_override" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_SCALE_SUFFIX "_scale" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_CENTER_ADJUST_SUFFIX "_center_adjust" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_SOCKET_NAME "unreal_landscape_spline_mesh_socket_name" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_TANGENT_LENGTH "unreal_landscape_spline_tangent_length" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONNECTION0_MESH_SOCKET_NAME "unreal_landscape_spline_connection0_mesh_socket_name" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONNECTION1_MESH_SOCKET_NAME "unreal_landscape_spline_connection1_mesh_socket_name" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONNECTION0_TANGENT_LENGTH "unreal_landscape_spline_connection0_tangent_length" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONNECTION1_TANGENT_LENGTH "unreal_landscape_spline_connection1_tangent_length" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_HALF_WIDTH "unreal_landscape_spline_half_width" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SIDE_FALLOFF "unreal_landscape_spline_side_falloff" +#define HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_END_FALLOFF "unreal_landscape_spline_end_falloff" + +#define HAPI_UNREAL_ATTRIB_SKELETON "unreal_skeleton" +#define HAPI_UNREAL_ATTRIB_PHYSICS_ASSET "unreal_physics_asset" +#define HAPI_UNREAL_ATTRIB_SKELETON_IMPORT_SCALE "unreal_sk_import_scale" + +#define HAPI_UNREAL_ATTRIB_GENERIC_UPROP_PREFIX "unreal_uproperty_" +#define HAPI_UNREAL_ATTRIB_GENERIC_MAT_PARAM_PREFIX "unreal_material_parameter_" + +#define HAPI_UNREAL_ATTRIB_BAKE_FOLDER "unreal_bake_folder" +#define HAPI_UNREAL_ATTRIB_TEMP_FOLDER "unreal_temp_folder" +#define HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V1 "unreal_generated_mesh_name" +#define HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2 "unreal_output_name" +#define HAPI_UNREAL_ATTRIB_BAKE_NAME "unreal_bake_name" +#define HAPI_UNREAL_ATTRIB_BAKE_ACTOR "unreal_bake_actor" +#define HAPI_UNREAL_ATTRIB_BAKE_ACTOR_CLASS "unreal_bake_actor_class" +#define HAPI_UNREAL_ATTRIB_BAKE_OUTLINER_FOLDER "unreal_bake_outliner_folder" + +// data tables +#define HAPI_UNREAL_ATTRIB_DATA_TABLE_PREFIX "unreal_data_table_" +#define HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWSTRUCT "unreal_data_table_rowstruct" +#define HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWNAME "unreal_data_table_rowname" + +// Level instances +#define HAPI_UNREAL_ATTRIB_LEVEL_INSTANCE_NAME "unreal_level_instance_name" +#define HAPI_UNREAL_ATTRIB_LEVEL_INSTANCE_TYPE "unreal_level_instance_type" + +// Attributes for Curve Outputs +#define HAPI_UNREAL_ATTRIB_OUTPUT_UNREAL_CURVE "unreal_output_curve" +#define HAPI_UNREAL_ATTRIB_OUTPUT_UNREAL_CURVE_LINEAR "unreal_output_curve_linear" +#define HAPI_UNREAL_ATTRIB_OUTPUT_UNREAL_CURVE_CLOSED "unreal_output_curve_closed" +// We only support Unreal spline outputs for now +//#define HAPI_UNREAL_ATTRIB_OUTPUT_HOUDINI_CURVE "houdini_output_curve" + +// PCG Attributes +#define HOUDINI_PCG_PARAMS_OUTPUT_NAME "unreal_pcg_params" +#define HAPI_UNREAL_ATTRIB_PCG_STEEPNESS "steepness" +#define HAPI_UNREAL_ATTRIB_PCG_DENSITY "density" +#define HAPI_UNREAL_ATTRIB_PCG_BOUNDING_BOX_MIN "bounding_box_min" +#define HAPI_UNREAL_ATTRIB_PCG_BOUNDING_BOX_MAX "bounding_box_max" +#define HAPI_UNREAL_ATTRIB_PCG_SEED "seed" + +// PDG Attributes +#define HAPI_UNREAL_ATTRIB_PDG_ASSET "unreal_pdg_asset" + +// Geometry Node +#define HAPI_UNREAL_PARAM_TRANSLATE "t" +#define HAPI_UNREAL_PARAM_ROTATE "r" +#define HAPI_UNREAL_PARAM_SCALE "s" +#define HAPI_UNREAL_PARAM_PIVOT "p" +#define HAPI_UNREAL_PARAM_UNIFORMSCALE "scale" + +// Houdini Curve +#define HAPI_UNREAL_PARAM_CURVE_TYPE "type" +#define HAPI_UNREAL_PARAM_CURVE_METHOD "method" +#define HAPI_UNREAL_PARAM_CURVE_COORDS "coords" +#define HAPI_UNREAL_PARAM_CURVE_CLOSED "close" +#define HAPI_UNREAL_PARAM_CURVE_REVERSED "reverse" +#define HAPI_UNREAL_ATTRIB_HAPI_INPUT_CURVE_COORDS "hapi_input_curve_coords" +#define HAPI_UNREAL_ATTRIB_HAPI_INPUT_CURVE_ORDER_MIN 2 +#define HAPI_UNREAL_ATTRIB_HAPI_INPUT_CURVE_ORDER_MAX 11 + +#define HAPI_UNREAL_PARAM_INPUT_CURVE_COORDS_DEFAULT "0.0, 0.0, 3.0 3.0, 0.0, 3.0" + +// String Params tags +#define HOUDINI_PARAMETER_STRING_REF_TAG TEXT("unreal_ref") +#define HOUDINI_PARAMETER_STRING_REF_CLASS_TAG TEXT("unreal_ref_class") +#define HOUDINI_PARAMETER_STRING_MULTILINE_TAG TEXT("editor") + +// Parameter tags +#define HAPI_PARAM_TAG_NOSWAP "hengine_noswap" +#define HAPI_PARAM_TAG_FILE_READONLY "filechooser_mode" +#define HAPI_PARAM_TAG_UNITS "units" +#define HAPI_PARAM_TAG_DEFAULT_DIR "default_dir" + +// TODO: unused, remove! +#define HAPI_PARAM_TAG_ASSET_REF "asset_ref" + +// Groups +#define HAPI_UNREAL_GROUP_LOD_PREFIX TEXT("lod") +#define HAPI_UNREAL_GROUP_SOCKET_PREFIX TEXT("mesh_socket") +#define HAPI_UNREAL_GROUP_SOCKET_PREFIX_OLD TEXT("socket") +//#define HAPI_UNREAL_GROUP_USER_SPLIT_PREFIX TEXT("unreal_split") + +#define HAPI_UNREAL_GROUP_GEOMETRY_NOT_COLLISION TEXT("main_geo") + +#define HAPI_UNREAL_GROUP_INVISIBLE_COLLISION_PREFIX TEXT("collision_geo") +#define HAPI_UNREAL_GROUP_RENDERED_COLLISION_PREFIX TEXT("rendered_collision_geo") +#define HAPI_UNREAL_GROUP_RENDERED_PREFIX TEXT("rendered") + +#define HAPI_UNREAL_GROUP_INVISIBLE_UCX_COLLISION_PREFIX TEXT("collision_geo_ucx") +#define HAPI_UNREAL_GROUP_RENDERED_UCX_COLLISION_PREFIX TEXT("rendered_collision_geo_ucx") + +#define HAPI_UNREAL_GROUP_INVISIBLE_SIMPLE_COLLISION_PREFIX TEXT("collision_geo_simple") +#define HAPI_UNREAL_GROUP_RENDERED_SIMPLE_COLLISION_PREFIX TEXT("rendered_collision_geo_simple") + +// Default material name. +#define HAPI_UNREAL_DEFAULT_MATERIAL_NAME TEXT( "default_material" ) + +// Visibility layer name +#define HAPI_UNREAL_VISIBILITY_LAYER_NAME TEXT( "visibility" ) + +// Various variable names used to store meta information in generated packages. +#define HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT TEXT( "HoudiniGeneratedObject" ) +#define HAPI_UNREAL_PACKAGE_META_GENERATED_NAME TEXT( "HoudiniGeneratedName" ) +#define HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_TYPE TEXT( "HoudiniGeneratedTextureType" ) +#define HAPI_UNREAL_PACKAGE_META_NODE_PATH TEXT( "HoudiniNodePath" ) +#define HAPI_UNREAL_PACKAGE_META_BAKE_COUNTER TEXT( "HoudiniPackageBakeCounter" ) +#define HAPI_UNREAL_PACKAGE_META_BAKED_OBJECT TEXT( "HoudiniBakedObject" ) + +#define HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_NORMAL TEXT( "N" ) +#define HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_DIFFUSE TEXT( "C_A" ) +#define HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_SPECULAR TEXT( "S" ) +#define HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_ROUGHNESS TEXT( "R" ) +#define HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_METALLIC TEXT( "M" ) +#define HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_EMISSIVE TEXT( "E" ) +#define HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_OPACITY_MASK TEXT( "O" ) + +// Texture planes. +#define HAPI_UNREAL_MATERIAL_TEXTURE_COLOR_ALPHA "C A" +#define HAPI_UNREAL_MATERIAL_TEXTURE_COLOR "C" +#define HAPI_UNREAL_MATERIAL_TEXTURE_ALPHA "A" +#define HAPI_UNREAL_MATERIAL_TEXTURE_NORMAL "N" + +// Materials Diffuse. +#define HAPI_UNREAL_PARAM_COLOR_DIFFUSE "basecolor" +#define HAPI_UNREAL_PARAM_MAP_DIFFUSE "basecolor_texture" +#define HAPI_UNREAL_PARAM_MAP_DIFFUSE_ENABLED "basecolor_useTexture" + +#define HAPI_UNREAL_PARAM_COLOR_DIFFUSE_OGL "ogl_diff" +#define HAPI_UNREAL_PARAM_MAP_DIFFUSE_OGL "ogl_tex1" +#define HAPI_UNREAL_PARAM_MAP_DIFFUSE_OGL_ENABLED "ogl_use_tex1" + +#define HAPI_UNREAL_PARAM_COLOR_DIFFUSE_CPM "constantbasecolor" +#define HAPI_UNREAL_PARAM_COLOR_DIFFUSE_CPM_DEFAULT "default_basecolor" +#define HAPI_UNREAL_PARAM_MAP_DIFFUSE_CPM "basecolor" +#define HAPI_UNREAL_PARAM_MAP_DIFFUSE_CPM_SWITCH "basecolorswitch" + +#define HAPI_UNREAL_PARAM_MAP_DIFFUSE_COLOR_SPACE "basecolor_textureColorSpace" + +#define HAPI_UNREAL_PARAM_TEXTURE_LAYERS_NUM "ogl_numtex" + +#define HAPI_UNREAL_PARAM_MAP_DIFFUSE_USE_POINT_COLOR "basecolor_usePointColor" + +// Materials Normal. +//#define HAPI_UNREAL_PARAM_MAP_NORMAL "normalTexture" +//#define HAPI_UNREAL_PARAM_MAP_NORMAL_ENABLED "normalUseTexture" +#define HAPI_UNREAL_PARAM_MAP_NORMAL "baseNormal_texture" +#define HAPI_UNREAL_PARAM_MAP_NORMAL_ENABLED "baseBumpAndNormal_enable" + +#define HAPI_UNREAL_PARAM_MAP_NORMAL_OGL "ogl_normalmap" + +#define HAPI_UNREAL_PARAM_MAP_NORMAL_CPM "normal_map" + +#define HAPI_UNREAL_PARAM_MAP_NORMAL_TYPE "ogl_normalmap_type" +#define HAPI_UNREAL_PARAM_MAP_NORMAL_TYPE_TANGENT "Tangent Space" +#define HAPI_UNREAL_PARAM_MAP_NORMAL_TYPE_WORLD "World Space" + +#define HAPI_UNREAL_PARAM_MAP_NORMAL_COLOR_SPACE "normalTexColorSpace" + +// Materials Specular. +#define HAPI_UNREAL_PARAM_VALUE_SPECULAR "reflect" +#define HAPI_UNREAL_PARAM_MAP_SPECULAR "reflect_texture" +#define HAPI_UNREAL_PARAM_MAP_SPECULAR_ENABLED "reflect_useTexture" + +#define HAPI_UNREAL_PARAM_VALUE_SPECULAR_OGL "ogl_reflect" //"ogl_spec" +#define HAPI_UNREAL_PARAM_MAP_SPECULAR_OGL "ogl_reflect_map" //"ogl_specmap" +#define HAPI_UNREAL_PARAM_MAP_SPECULAR_OGL_ENABLED "ogl_use_reflect_map" //"ogl_use_specmap" + +#define HAPI_UNREAL_PARAM_VALUE_SPECULAR_CPM "constantspecularamount" +#define HAPI_UNREAL_PARAM_VALUE_SPECULAR_CPM_DEFAULT "default_specular" +#define HAPI_UNREAL_PARAM_MAP_SPECULAR_CPM "specular" +#define HAPI_UNREAL_PARAM_MAP_SPECULAR_CPM_SWITCH "specularamountswitch" + +#define HAPI_UNREAL_PARAM_MAP_SPECULAR_COLOR_SPACE "reflect_textureColorSpace" + +// Materials Roughness. +#define HAPI_UNREAL_PARAM_VALUE_ROUGHNESS "rough" +#define HAPI_UNREAL_PARAM_MAP_ROUGHNESS "rough_texture" +#define HAPI_UNREAL_PARAM_MAP_ROUGHNESS_ENABLED "rough_useTexture" + +#define HAPI_UNREAL_PARAM_VALUE_ROUGHNESS_OGL "ogl_rough" +#define HAPI_UNREAL_PARAM_MAP_ROUGHNESS_OGL "ogl_roughmap" +#define HAPI_UNREAL_PARAM_MAP_ROUGHNESS_OGL_ENABLED "ogl_use_roughmap" + +#define HAPI_UNREAL_PARAM_VALUE_ROUGHNESS_CPM "constantspecularroughness" +#define HAPI_UNREAL_PARAM_VALUE_ROUGHNESS_CPM_DEFAULT "default_specular_roughness" +#define HAPI_UNREAL_PARAM_MAP_ROUGHNESS_CPM "roughness" +#define HAPI_UNREAL_PARAM_MAP_ROUGHNESS_CPM_SWITCH "specularoughnessswitch" // CPM misspelt this parameter + +#define HAPI_UNREAL_PARAM_MAP_ROUGHNESS_COLOR_SPACE "rough_textureColorSpace" + +// Materials Metallic. +#define HAPI_UNREAL_PARAM_VALUE_METALLIC "metallic" +#define HAPI_UNREAL_PARAM_MAP_METALLIC "metallic_texture" +#define HAPI_UNREAL_PARAM_MAP_METALLIC_ENABLED "metallic_useTexture" + +#define HAPI_UNREAL_PARAM_VALUE_METALLIC_OGL "ogl_metallic" +#define HAPI_UNREAL_PARAM_MAP_METALLIC_OGL "ogl_metallicmap" +#define HAPI_UNREAL_PARAM_MAP_METALLIC_OGL_ENABLED "ogl_use_metallicmap" + +#define HAPI_UNREAL_PARAM_VALUE_METALLIC_CPM "constantmetalness" +#define HAPI_UNREAL_PARAM_VALUE_METALLIC_CPM_DEFAULT "default_metalness" +#define HAPI_UNREAL_PARAM_MAP_METALLIC_CPM "metalness" +#define HAPI_UNREAL_PARAM_MAP_METALLIC_CPM_SWITCH "metalnessswitch" + +#define HAPI_UNREAL_PARAM_MAP_METALLIC_COLOR_SPACE "metallic_textureColorSpace" + +// Materials Emissive. +#define HAPI_UNREAL_PARAM_VALUE_EMISSIVE "emitcolor" +#define HAPI_UNREAL_PARAM_VALUE_EMISSIVE_INTENSITY "emitint" +#define HAPI_UNREAL_PARAM_MAP_EMISSIVE "emitcolor_texture" +#define HAPI_UNREAL_PARAM_MAP_EMISSIVE_ENABLED "emitcolor_useTexture" + +#define HAPI_UNREAL_PARAM_VALUE_EMISSIVE_OGL "ogl_emit" +#define HAPI_UNREAL_PARAM_VALUE_EMISSIVE_INTENSITY_OGL "ogl_emit_intensity" +#define HAPI_UNREAL_PARAM_MAP_EMISSIVE_OGL "ogl_emissionmap" +#define HAPI_UNREAL_PARAM_MAP_EMISSIVE_OGL_ENABLED "ogl_use_emissionmap" + +#define HAPI_UNREAL_PARAM_VALUE_EMISSIVE_CPM "constantemissioncolor" +#define HAPI_UNREAL_PARAM_VALUE_EMISSIVE_CPM_DEFAULT "default_emission_color" +#define HAPI_UNREAL_PARAM_MAP_EMISSIVE_CPM "emission_color" +#define HAPI_UNREAL_PARAM_MAP_EMISSIVE_CPM_SWITCH "emissioncolorswitch" + +#define HAPI_UNREAL_PARAM_VALUE_EMISSIVE_INTENSITY_CPM "constantemissionamount" +#define HAPI_UNREAL_PARAM_VALUE_EMISSIVE_INTENSITY_CPM_DEFAULT "default_emission_amount" +#define HAPI_UNREAL_PARAM_MAP_EMISSIVE_INTENSITY_CPM "emission" +#define HAPI_UNREAL_PARAM_MAP_EMISSIVE_INTENSITY_CPM_SWITCH "emissionamountswitch" + +#define HAPI_UNREAL_PARAM_MAP_EMISSIVE_COLOR_SPACE "emitcolor_textureColorSpace" + +// Materials Opacity. +#define HAPI_UNREAL_PARAM_ALPHA "opac" +#define HAPI_UNREAL_PARAM_MAP_OPACITY "opaccolor_texture" +#define HAPI_UNREAL_PARAM_MAP_OPACITY_ENABLED "opaccolor_useTexture" + +#define HAPI_UNREAL_PARAM_ALPHA_OGL "ogl_alpha" +#define HAPI_UNREAL_PARAM_MAP_OPACITY_OGL "ogl_opacitymap" +#define HAPI_UNREAL_PARAM_MAP_OPACITY_OGL_ENABLED "ogl_use_opacitymap" + +#define HAPI_UNREAL_PARAM_ALPHA_CPM "constantopacity" +#define HAPI_UNREAL_PARAM_ALPHA_CPM_DEFAULT "default_opacity_amount" +#define HAPI_UNREAL_PARAM_MAP_OPACITY_CPM "opacity" +#define HAPI_UNREAL_PARAM_MAP_OPACITY_CPM_SWITCH "opacityswitch" + +// Number of GUID characters to keep for packages +#define PACKAGE_GUID_LENGTH 8 +#define PACKAGE_GUID_COMPONENT_LENGTH 12 + +/** Ramp related defines. **/ +#define HAPI_UNREAL_RAMP_FLOAT_AXIS_X "position" +#define HAPI_UNREAL_RAMP_FLOAT_AXIS_Y "value" +#define HAPI_UNREAL_RAMP_COLOR_AXIS_X "position" +#define HAPI_UNREAL_RAMP_COLOR_AXIS_Y "color" + +/** Handle types. **/ +#define HAPI_UNREAL_HANDLE_TRANSFORM "xform" +#define HAPI_UNREAL_HANDLE_BOUNDER "bound" + +#define HAPI_UNREAL_LANDSCAPE_HEIGHT_VOLUME_NAME "height" + +#define HAPI_UNREAL_NOTIFICATION_FADEOUT 2.0f +#define HAPI_UNREAL_NOTIFICATION_EXPIRE 2.0f + +// Chaos/Geometry collection output attributes +#define HAPI_UNREAL_ATTRIB_GC_PIECE "unreal_gc_piece" +#define HAPI_UNREAL_ATTRIB_GC_CLUSTER_PIECE "unreal_gc_cluster" +#define HAPI_UNREAL_ATTRIB_GC_NAME "unreal_gc_name" + +#define HAPI_UNREAL_ATTRIB_GC_CLUSTERING_DAMAGE_THRESHOLD "unreal_gc_clustering_damage_threshold" +#define HAPI_UNREAL_ATTRIB_GC_CLUSTERING_CLUSTER_CONNECTION_TYPE "unreal_gc_clustering_cluster_connection_type" + +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MAX_SIZE "unreal_gc_collisions_max_size" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_DAMAGE_THRESHOLD "unreal_gc_collisions_damage_threshold" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_COLLISION_TYPE "unreal_gc_collisions_collision_type" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_IMPLICIT_TYPE "unreal_gc_collisions_implicit_type" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MIN_LEVEL_SET_RESOLUTION "unreal_gc_collisions_min_level_set_resolution" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MAX_LEVEL_SET_RESOLUTION "unreal_gc_collisions_max_level_set_resolution" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MIN_CLUSTER_LEVEL_SET_RESOLUTION "unreal_gc_collisions_min_cluster_level_set_resolution" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MAX_CLUSTER_LEVEL_SET_RESOLUTION "unreal_gc_collisions_max_cluster_level_set_resolution" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_COLLISION_OBJECT_REDUCTION_PERCENTAGE "unreal_gc_collisions_collision_object_reduction_percentage" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_COLLISION_MARGIN_FRACTION "unreal_gc_collisions_collision_margin_fraction" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MASS_AS_DENSITY "unreal_gc_collisions_mass_as_density" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MASS "unreal_gc_collisions_mass" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MINIMUM_MASS_CLAMP "unreal_gc_collisions_minimum_mass_clamp" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_COLLISION_PARTICLES_FRACTION "unreal_gc_collisions_collision_particles_fraction" +#define HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MAXIMUM_COLLISION_PARTICLES "unreal_gc_collisions_maximum_collision_particles" + +// Chaos/Geometry collection input attributes +#define HAPI_UNREAL_ATTRIB_INPUT_GC_NAME "unreal_input_gc_name" + +// Pack node parameters. Required by GC input. +#define HAPI_UNREAL_PARAM_PACK_BY_NAME "packbyname" +#define HAPI_UNREAL_PARAM_PACKED_FRAGMENTS "packedfragments" + +#define HOUDINI_DATA_LAYER_PREFIX "unreal_data_layer_" + + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineProcessor.cpp`: + +```cpp +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +``` + +`Source/HoudiniEngine/Private/HoudiniEngineProcessor.h`: + +```h +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +``` + +`Source/HoudiniEngine/Private/HoudiniEngineScheduler.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineScheduler.h" + +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineString.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngine.h" + +const uint32 +FHoudiniEngineScheduler::InitialTaskSize = 256u; + +// Update frequency in (ms) for polling the scheduler +const float +FHoudiniEngineScheduler::UpdateFrequency = 0.1f; + +FHoudiniEngineScheduler::FHoudiniEngineScheduler() + : WakeUpEvent(FEventRef(EEventMode::AutoReset)) + , Tasks(nullptr) + , PositionWrite(0u) + , PositionRead(0u) + , bStopping(false) +{ + // Make sure size is power of two. + TaskCount = FPlatformMath::RoundUpToPowerOfTwo(FHoudiniEngineScheduler::InitialTaskSize); + + if (TaskCount) + { + // Allocate buffer to store all tasks. + Tasks = static_cast(FMemory::Malloc(TaskCount * sizeof(FHoudiniEngineTask))); + + if (Tasks) + { + // Zero memory. + FMemory::Memset(Tasks, 0x0, TaskCount * sizeof(FHoudiniEngineTask)); + } + } +} + +FHoudiniEngineScheduler::~FHoudiniEngineScheduler() +{ + if (TaskCount) + { + FMemory::Free(Tasks); + Tasks = nullptr; + } +} + +void +FHoudiniEngineScheduler::TaskDescription( + FHoudiniEngineTaskInfo & TaskInfo, + const FString & ActorName, + const FString & StatusString) +{ + FFormatNamedArguments Args; + + if (!ActorName.IsEmpty()) + { + Args.Add(TEXT("AssetName"), FText::FromString(ActorName)); + Args.Add(TEXT("AssetStatus"), FText::FromString(StatusString)); + TaskInfo.StatusText = + FText::Format(NSLOCTEXT("TaskDescription", "TaskDescriptionProgress", "{AssetName} :\n{AssetStatus}"), Args); + } + else + { + Args.Add(TEXT("AssetStatus"), FText::FromString(StatusString)); + TaskInfo.StatusText = + FText::Format(NSLOCTEXT("TaskDescription", "TaskDescriptionProgress", "{AssetStatus}"), Args); + } +} + +void +FHoudiniEngineScheduler::TaskInstantiateAsset(const FHoudiniEngineTask & Task) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineScheduler::TaskInstantiateAsset); + + FString AssetN; + FHoudiniEngineString(Task.AssetHapiName).ToFString(AssetN); + + HOUDINI_LOG_MESSAGE( + TEXT("HAPI Asynchronous Instantiation Started for %s: Asset=%s, HoudiniAsset = 0x%x"), + *Task.ActorName, *AssetN, Task.Asset.Get()); + + if (!FHoudiniEngineUtils::IsInitialized()) + { + HOUDINI_LOG_ERROR( + TEXT("TaskInstantiateAsset failed for %s: %s"), + *Task.ActorName, + *FHoudiniEngineUtils::GetErrorDescription(HAPI_RESULT_NOT_INITIALIZED)); + + AddResponseMessageTaskInfo( + HAPI_RESULT_NOT_INITIALIZED, + EHoudiniEngineTaskType::AssetInstantiation, + EHoudiniEngineTaskState::FinishedWithFatalError, + -1, Task, TEXT("HAPI is not initialized.")); + + return; + } + + if (!Task.Asset.IsValid()) + { + // Asset is no longer valid, return. + AddResponseMessageTaskInfo( + HAPI_RESULT_FAILURE, + EHoudiniEngineTaskType::AssetInstantiation, + EHoudiniEngineTaskState::FinishedWithFatalError, + -1, Task, TEXT("Asset is no longer valid.")); + + return; + } + + if (Task.AssetHapiName < 0) + { + // Asset is no longer valid, return. + AddResponseMessageTaskInfo( + HAPI_RESULT_FAILURE, + EHoudiniEngineTaskType::AssetInstantiation, + EHoudiniEngineTaskState::FinishedWithFatalError, + -1, Task, TEXT("Asset name is invalid.")); + + return; + } + + HAPI_Result Result = HAPI_RESULT_SUCCESS; + int32 AssetCount = 0; + HAPI_NodeId AssetId = -1; + std::string AssetNameString; + double LastUpdateTime; + + FHoudiniEngineString HoudiniEngineString(Task.AssetHapiName); + if (!HoudiniEngineString.ToStdString(AssetNameString)) + { + AddResponseMessageTaskInfo( + HAPI_RESULT_FAILURE, + EHoudiniEngineTaskType::AssetInstantiation, + EHoudiniEngineTaskState::FinishedWithFatalError, + -1, Task, TEXT("Error retrieving asset name.")); + + return; + } + + // Translate asset name into Unreal string. + FString AssetName = ANSI_TO_TCHAR(AssetNameString.c_str()); + + // Initialize last update time. + LastUpdateTime = FPlatformTime::Seconds(); + + // We instantiate without cooking. + + std::string StdString; + const char* NativeNodeLabel = nullptr; + + if (!Task.NodeLabelPrefix.IsEmpty()) + { + FString NodeLabel = Task.NodeLabelPrefix; + NodeLabel += FString(AssetNameString.c_str()); + FHoudiniEngineUtils::SanitizeHAPIVariableName(NodeLabel); + StdString = TCHAR_TO_UTF8(*NodeLabel); + } + + if(!StdString.empty()) + NativeNodeLabel = StdString.c_str(); + + Result = FHoudiniApi::CreateNode(FHoudiniEngine::Get().GetSession(), -1, &AssetNameString[0], NativeNodeLabel, false, &AssetId); + + if (Result != HAPI_RESULT_SUCCESS) + { + AddResponseMessageTaskInfo( + Result, + EHoudiniEngineTaskType::AssetInstantiation, + EHoudiniEngineTaskState::FinishedWithFatalError, + -1, Task, TEXT("Error instantiating asset.")); + + return; + } + + // Add processing notification. + FHoudiniEngineTaskInfo TaskInfo( + HAPI_RESULT_SUCCESS, -1, + EHoudiniEngineTaskType::AssetInstantiation, + EHoudiniEngineTaskState::Working); + + //TaskInfo.bLoadedComponent = Task.bLoadedComponent; + TaskDescription(TaskInfo, Task.ActorName, TEXT("Started Instantiation")); + FHoudiniEngine::Get().AddTaskInfo(Task.HapiGUID, TaskInfo); + + bool bTryCOPNet = false; + if (AssetName.Contains(TEXT("::Cop/"))) + bTryCOPNet = true; + + // We need to spin until instantiation is finished. + while (true) + { + int Status = HAPI_STATE_STARTING_COOK; + HOUDINI_CHECK_ERROR_GET(&Result, FHoudiniApi::GetStatus( + FHoudiniEngine::Get().GetSession(), HAPI_STATUS_COOK_STATE, &Status)); + + if (Status == HAPI_STATE_READY) + { + // Cooking has been successful. + AddResponseMessageTaskInfo( + HAPI_RESULT_SUCCESS, + EHoudiniEngineTaskType::AssetInstantiation, + EHoudiniEngineTaskState::Success, AssetId, Task, + TEXT("Finished Instantiation.")); + + break; + } + else if (Status == HAPI_STATE_READY_WITH_FATAL_ERRORS || Status == HAPI_STATE_READY_WITH_COOK_ERRORS) + { + // There was an error while instantiating. + FString CookResultString = FHoudiniEngineUtils::GetCookResult(); + int32 CookResult = static_cast(HAPI_RESULT_SUCCESS); + FHoudiniApi::GetStatus(FHoudiniEngine::Get().GetSession(), HAPI_STATUS_COOK_RESULT, &CookResult); + + // If the asset is a cop node, we need to create obj/geo/cop subnet + // in order to be able to instantiate it properly + if (Status == HAPI_STATE_READY_WITH_FATAL_ERRORS + && CookResult == HAPI_RESULT_INVALID_ARGUMENT + && bTryCOPNet) + { + // Only try once + bTryCOPNet = false; + + // Extract the operator name, as we've just created the appropriate subnets + FString opName = AssetName; + int32 opNameStart = AssetName.Find("/"); + if (opNameStart > 0) + { + opName = AssetName.RightChop(opNameStart + 1); + AssetNameString = TCHAR_TO_UTF8(*opName); + } + + // Create an OBJ node + HAPI_NodeId UnrealContentNodeId = -1; + Result = FHoudiniEngineUtils::CreateNode( + -1, "Object/subnet", opName, true, &UnrealContentNodeId); + + // Create a geo "grandparent" node + HAPI_NodeId GrandParentGEOId = -1; + if (Result == HAPI_RESULT_SUCCESS) + { + Result = FHoudiniEngineUtils::CreateNode( + UnrealContentNodeId, TEXT("geo"), opName, true, &GrandParentGEOId); + } + + // And a parent COP net + HAPI_NodeId ParentCOPId = -1; + if (Result == HAPI_RESULT_SUCCESS) + { + // Create the node and wait for the ready status + Result = FHoudiniEngineUtils::CreateNode( + GrandParentGEOId, TEXT("copnet"), opName, true, &ParentCOPId); + } + + if (Result == HAPI_RESULT_SUCCESS && ParentCOPId >= 0) + { + // Attempt to create the HDA in the copnet we just created + Result = FHoudiniApi::CreateNode( + FHoudiniEngine::Get().GetSession(), + ParentCOPId, + &AssetNameString[0], + NativeNodeLabel, + false, + &AssetId); + + if (HAPI_RESULT_SUCCESS != Result) + { + // Failed - clean up any parent node we might have created + if(ParentCOPId >= 0) + FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), ParentCOPId); + + if (GrandParentGEOId >= 0) + FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), GrandParentGEOId); + + if (UnrealContentNodeId >= 0) + FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), UnrealContentNodeId); + } + else + { + // Retry with the cop node in the subnets + continue; + } + } + else + { + Status = HAPI_STATE_READY_WITH_FATAL_ERRORS; + CookResult = HAPI_RESULT_INVALID_ARGUMENT; + } + } + + EHoudiniEngineTaskState TaskStateResult = EHoudiniEngineTaskState::FinishedWithFatalError; + if (Status == HAPI_STATE_READY_WITH_COOK_ERRORS) + TaskStateResult = EHoudiniEngineTaskState::FinishedWithError; + + AddResponseMessageTaskInfo( + static_cast(CookResult), + EHoudiniEngineTaskType::AssetInstantiation, + TaskStateResult, + AssetId, Task, + FString::Printf(TEXT("Finished Instantiation with Errors: %s"), *CookResultString)); + + break; + } + + if (Result == HAPI_RESULT_FAILURE) + { + // There was an error while getting the status - likely we've lost the session. + // Log an error and ensure we break cleanly in that case + HOUDINI_LOG_ERROR(TEXT("Unable to instantiate asset: %s - session failed"), *Task.ActorName); + + EHoudiniEngineTaskState TaskStateResult = EHoudiniEngineTaskState::FinishedWithFatalError; + + AddResponseMessageTaskInfo( + static_cast(Result), + EHoudiniEngineTaskType::AssetInstantiation, + TaskStateResult, + AssetId, Task, + TEXT("Unable to instantiate the asset: session failed.")); + + return; + } + + static const double NotificationUpdateFrequency = 0.5; + if ((FPlatformTime::Seconds() - LastUpdateTime) >= NotificationUpdateFrequency) + { + // Reset update time. + LastUpdateTime = FPlatformTime::Seconds(); + const FString& CookStateMessage = FHoudiniEngineUtils::GetCookState(); + + AddResponseMessageTaskInfo( + HAPI_RESULT_SUCCESS, + EHoudiniEngineTaskType::AssetInstantiation, + EHoudiniEngineTaskState::Working, + AssetId, Task, CookStateMessage); + } + + // We want to yield. + FPlatformProcess::SleepNoStats(UpdateFrequency); + } +} + +void +FHoudiniEngineScheduler::TaskCookAsset(const FHoudiniEngineTask & Task) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineScheduler::TaskCookAsset); + + if (!FHoudiniEngineUtils::IsInitialized()) + { + HOUDINI_LOG_ERROR( + TEXT("TaskCookAsset failed for %s: %s"), + *Task.ActorName, + *FHoudiniEngineUtils::GetErrorDescription(HAPI_RESULT_NOT_INITIALIZED)); + + AddResponseMessageTaskInfo( + HAPI_RESULT_NOT_INITIALIZED, + EHoudiniEngineTaskType::AssetCooking, + EHoudiniEngineTaskState::FinishedWithFatalError, + -1, Task, TEXT("HAPI is not initialized.")); + + return; + } + + // Retrieve asset id. + HAPI_NodeId AssetId = Task.AssetId; + HAPI_Result Result = HAPI_RESULT_SUCCESS; + + HOUDINI_LOG_MESSAGE( + TEXT("HAPI Asynchronous Cooking Started for %s., AssetId = %d"), + *Task.ActorName, AssetId); + + if (AssetId == -1) + { + // We have an invalid asset id. + HOUDINI_LOG_ERROR(TEXT("TaskCookAsset failed for %s: Invalid Asset Id."), *Task.ActorName); + + AddResponseMessageTaskInfo( + HAPI_RESULT_FAILURE, + EHoudiniEngineTaskType::AssetCooking, + EHoudiniEngineTaskState::FinishedWithFatalError, + -1, Task, TEXT("Asset has invalid id.")); + + return; + } + + // Get the extra node Ids that we want to process if needed + TArray NodesToCook; + NodesToCook.Add(AssetId); + for (auto& CurrentNodeId : Task.OtherNodeIds) + { + if (CurrentNodeId < 0) + continue; + + NodesToCook.AddUnique(CurrentNodeId); + } + + // Default CookOptions + HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + + EHoudiniEngineTaskState GlobalTaskResult = EHoudiniEngineTaskState::Success; + for (auto& CurrentNodeId : NodesToCook) + { + Result = FHoudiniApi::CookNode(FHoudiniEngine::Get().GetSession(), CurrentNodeId, &CookOptions); + if (Result != HAPI_RESULT_SUCCESS) + { + AddResponseMessageTaskInfo( + Result, + EHoudiniEngineTaskType::AssetCooking, + EHoudiniEngineTaskState::FinishedWithFatalError, + AssetId, + Task, + TEXT("Error cooking asset.")); + + return; + } + + // Add processing notification. + AddResponseMessageTaskInfo( + HAPI_RESULT_SUCCESS, + EHoudiniEngineTaskType::AssetCooking, + EHoudiniEngineTaskState::Working, + AssetId, Task, TEXT("Started Cooking")); + + // Initialize last update time. + double LastUpdateTime = FPlatformTime::Seconds(); + + // We need to spin until cooking is finished. + while (true) + { + int32 Status = HAPI_STATE_STARTING_COOK; + HOUDINI_CHECK_ERROR_GET(&Result, FHoudiniApi::GetStatus( + FHoudiniEngine::Get().GetSession(), HAPI_STATUS_COOK_STATE, &Status)); + + if (Status == HAPI_STATE_READY) + { + // Cooking has been successful. + // Break to process the next node + break; + } + else if (Status == HAPI_STATE_READY_WITH_FATAL_ERRORS || Status == HAPI_STATE_READY_WITH_COOK_ERRORS) + { + GlobalTaskResult = EHoudiniEngineTaskState::FinishedWithFatalError; + + if (Status == HAPI_STATE_READY_WITH_COOK_ERRORS) + GlobalTaskResult = EHoudiniEngineTaskState::FinishedWithError; + + break; + } + + if (Result == HAPI_RESULT_FAILURE) + { + // There was an error while getting the status - likely we've lost the session. + // Log an error and ensure we break cleanly in that case + HOUDINI_LOG_ERROR(TEXT("Unable to cook asset: %s - session failed"), *Task.ActorName); + GlobalTaskResult = EHoudiniEngineTaskState::FinishedWithFatalError; + + // There was an error while cooking. + AddResponseMessageTaskInfo( + HAPI_RESULT_FAILURE, + EHoudiniEngineTaskType::AssetCooking, + EHoudiniEngineTaskState::FinishedWithFatalError, + AssetId, + Task, + TEXT("Unable to cook - session failed")); + + return; + } + + static const double NotificationUpdateFrequency = 0.5; + if (FPlatformTime::Seconds() - LastUpdateTime >= NotificationUpdateFrequency) + { + // Reset update time. + LastUpdateTime = FPlatformTime::Seconds(); + + // Retrieve status string. + const FString & CookStateMessage = FHoudiniEngineUtils::GetCookState(); + + AddResponseMessageTaskInfo( + HAPI_RESULT_SUCCESS, + EHoudiniEngineTaskType::AssetCooking, + EHoudiniEngineTaskState::Working, + AssetId, Task, CookStateMessage); + } + + // We want to yield. + FPlatformProcess::SleepNoStats(UpdateFrequency); + } + } + + switch (GlobalTaskResult) + { + case EHoudiniEngineTaskState::Success: + { + // Cooking has been successful + AddResponseMessageTaskInfo( + HAPI_RESULT_SUCCESS, + EHoudiniEngineTaskType::AssetCooking, + EHoudiniEngineTaskState::Success, + AssetId, + Task, + TEXT("Finished Cooking")); + } + break; + + case EHoudiniEngineTaskState::FinishedWithError: + { + // There was an error while Cooking. + AddResponseMessageTaskInfo( + HAPI_RESULT_SUCCESS, + EHoudiniEngineTaskType::AssetCooking, + EHoudiniEngineTaskState::FinishedWithError, + AssetId, + Task, + TEXT("Finished Cooking with Errors")); + } + break; + + case EHoudiniEngineTaskState::FinishedWithFatalError: + case EHoudiniEngineTaskState::Aborted: + case EHoudiniEngineTaskState::None: + case EHoudiniEngineTaskState::Working: + { + // There was an error while cooking. + AddResponseMessageTaskInfo( + HAPI_RESULT_SUCCESS, + EHoudiniEngineTaskType::AssetCooking, + EHoudiniEngineTaskState::FinishedWithFatalError, + AssetId, + Task, + TEXT("Finished Cooking with Fatal Errors")); + } + break; + } +} + +void +FHoudiniEngineScheduler::TaskDeleteAsset(const FHoudiniEngineTask & Task) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineScheduler::TaskDeleteAsset); + + HOUDINI_LOG_MESSAGE( + TEXT("HAPI Asynchronous Destruction Started for %s. ") + TEXT("AssetId = %d"), + *Task.ActorName, Task.AssetId); + + if (FHoudiniEngineUtils::IsHoudiniNodeValid(Task.AssetId)) + FHoudiniEngineUtils::DestroyHoudiniAsset(Task.AssetId); + + // We do not insert task info as this is a fire and forget operation. + // At this point component most likely does not exist. +} + +void +FHoudiniEngineScheduler::AddResponseTaskInfo( + HAPI_Result Result, EHoudiniEngineTaskType TaskType, EHoudiniEngineTaskState TaskState, + HAPI_NodeId AssetId, const FHoudiniEngineTask & Task) +{ + FHoudiniEngineTaskInfo TaskInfo(Result, AssetId, TaskType, TaskState); + FString StatusString = FHoudiniEngineUtils::GetErrorDescription(); + + //TaskInfo.bLoadedComponent = Task.bLoadedComponent; + + TaskDescription(TaskInfo, Task.ActorName, StatusString); + FHoudiniEngine::Get().AddTaskInfo(Task.HapiGUID, TaskInfo); +} + +void +FHoudiniEngineScheduler::AddResponseMessageTaskInfo( + HAPI_Result Result, EHoudiniEngineTaskType TaskType, EHoudiniEngineTaskState TaskState, + HAPI_NodeId AssetId, const FHoudiniEngineTask & Task, const FString & ErrorMessage) +{ + FHoudiniEngineTaskInfo TaskInfo(Result, AssetId, TaskType, TaskState); + + //TaskInfo.bLoadedComponent = Task.bLoadedComponent; + + TaskDescription(TaskInfo, Task.ActorName, ErrorMessage); + FHoudiniEngine::Get().AddTaskInfo(Task.HapiGUID, TaskInfo); +} + +void +FHoudiniEngineScheduler::ProcessQueuedTasks() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineScheduler::ProcessQueuedTasks); + + while (!bStopping) + { + while (true) + { + FHoudiniEngineTask Task; + + { + FScopeLock ScopeLock(&CriticalSection); + + // We have no tasks left. + if (PositionWrite == PositionRead) + break; + + // Retrieve task. + Task = Tasks[PositionRead]; + PositionRead++; + + // Wrap around if required. + PositionRead &= (TaskCount - 1); + } + + bool bTaskProcessed = true; + + switch (Task.TaskType) + { + case EHoudiniEngineTaskType::AssetInstantiation: + { + TaskInstantiateAsset(Task); + break; + } + + case EHoudiniEngineTaskType::AssetCooking: + { + TaskCookAsset(Task); + break; + } + + case EHoudiniEngineTaskType::AssetDeletion: + { + TaskDeleteAsset(Task); + break; + } + + case EHoudiniEngineTaskType::AssetProcess: + { + TaskProccessAsset(Task); + break; + } + + default: + { + bTaskProcessed = false; + break; + } + } + + if (!bTaskProcessed) + break; + } + + if (FPlatformProcess::SupportsMultithreading()) + { + // We want to yield for a bit. + WakeUpEvent->Wait(UpdateFrequency * 1000.0f); + } + else + { + // If we are running in single threaded mode, return so we don't block everything else. + return; + } + } +} + +void +FHoudiniEngineScheduler::TaskProccessAsset(const FHoudiniEngineTask & Task) +{ + if (!FHoudiniEngineUtils::IsInitialized()) + { + HOUDINI_LOG_ERROR( + TEXT("TaskProccessAsset failed for %s: %s"), + *Task.ActorName, + *FHoudiniEngineUtils::GetErrorDescription(HAPI_RESULT_NOT_INITIALIZED)); + + AddResponseMessageTaskInfo( + HAPI_RESULT_NOT_INITIALIZED, + EHoudiniEngineTaskType::AssetProcess, + EHoudiniEngineTaskState::FinishedWithFatalError, + -1, Task, TEXT("HAPI is not initialized.")); + + return; + } + + // Retrieve asset id. + HAPI_NodeId AssetId = Task.AssetId; + if (AssetId == -1) + { + // We have an invalid asset id. + HOUDINI_LOG_ERROR(TEXT("TaskProcessAsset failed for %s: Invalid Asset Id."), *Task.ActorName); + + AddResponseMessageTaskInfo( + HAPI_RESULT_FAILURE, + EHoudiniEngineTaskType::AssetProcess, + EHoudiniEngineTaskState::FinishedWithFatalError, + -1, Task, TEXT("Asset has invalid id.")); + + return; + } + + HOUDINI_LOG_MESSAGE( + TEXT("HAPI Asynchronous Processing started for %s., AssetId = %d"), + *Task.ActorName, AssetId); + + // Add processing notification. + AddResponseMessageTaskInfo( + HAPI_RESULT_SUCCESS, + EHoudiniEngineTaskType::AssetProcess, + EHoudiniEngineTaskState::Working, + AssetId, Task, TEXT("Started Cooking")); + + // + + + // TODO: Process results! +} + +bool FHoudiniEngineScheduler::HasPendingTasks() +{ + FScopeLock ScopeLock(&CriticalSection); + return (PositionWrite != PositionRead); +} + +void +FHoudiniEngineScheduler::AddTask(const FHoudiniEngineTask & Task) +{ + FScopeLock ScopeLock(&CriticalSection); + + // Check if we need to grow our circular buffer. + if (PositionWrite + 1 == PositionRead) + { + // Calculate next size (next power of two). + uint32 NextTaskCount = FPlatformMath::RoundUpToPowerOfTwo(TaskCount + 1); + + // Allocate new buffer. + FHoudiniEngineTask * Buffer = static_cast( + FMemory::Malloc(NextTaskCount * sizeof(FHoudiniEngineTask))); + + if (!Buffer) + return; + + // Zero memory. + FMemory::Memset(Buffer, 0x0, NextTaskCount * sizeof(FHoudiniEngineTask)); + + // Copy elements from old buffer to new one. + if (PositionRead < PositionWrite) + { + FMemory::Memcpy(Buffer, Tasks + PositionRead, sizeof(FHoudiniEngineTask) * (PositionWrite - PositionRead)); + + // Update index positions. + PositionRead = 0; + PositionWrite = PositionWrite - PositionRead; + } + else + { + FMemory::Memcpy(Buffer, Tasks + PositionRead, sizeof(FHoudiniEngineTask) * (TaskCount - PositionRead)); + FMemory::Memcpy(Buffer + TaskCount - PositionRead, Tasks, sizeof(FHoudiniEngineTask) * PositionWrite); + + // Update index positions. + PositionRead = 0; + PositionWrite = TaskCount - PositionRead + PositionWrite; + } + + // Deallocate old buffer. + FMemory::Free(Tasks); + + // Bookkeeping. + Tasks = Buffer; + TaskCount = NextTaskCount; + } + + // Store task. + Tasks[PositionWrite] = Task; + PositionWrite++; + + // Wrap around if required. + PositionWrite &= (TaskCount - 1); + + // Wake up the thread to process the task. + WakeUpEvent->Trigger(); +} + +uint32 +FHoudiniEngineScheduler::Run() +{ + ProcessQueuedTasks(); + return 0; +} + +void +FHoudiniEngineScheduler::Stop() +{ + bStopping = true; +} + +void +FHoudiniEngineScheduler::Tick() +{ + ProcessQueuedTasks(); +} + +FSingleThreadRunnable * +FHoudiniEngineScheduler::GetSingleThreadInterface() +{ + return this; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineScheduler.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniEngineTask.h" +#include "HoudiniEngineTaskInfo.h" +#include "HAL/Event.h" +#include "HAL/Runnable.h" +#include "HAL/RunnableThread.h" +#include "Misc/SingleThreadRunnable.h" + +class FHoudiniEngineScheduler : public FRunnable, FSingleThreadRunnable +{ +public: + + FHoudiniEngineScheduler(); + virtual ~FHoudiniEngineScheduler(); + + // FRunnable methods. + virtual uint32 Run() override; + virtual void Stop() override; + FSingleThreadRunnable * GetSingleThreadInterface() override; + + // FSingleThreadRunnable methods. + virtual void Tick() override; + + bool HasPendingTasks(); + + // Adds a task. + void AddTask(const FHoudiniEngineTask & Task); + + // Adds instantiation response task info. + void AddResponseTaskInfo( + HAPI_Result Result, + EHoudiniEngineTaskType TaskType, + EHoudiniEngineTaskState TaskState, + HAPI_NodeId AssetId, + const FHoudiniEngineTask & Task); + + void AddResponseMessageTaskInfo( + HAPI_Result Result, + EHoudiniEngineTaskType TaskType, + EHoudiniEngineTaskState TaskState, + HAPI_NodeId AssetId, + const FHoudiniEngineTask & Task, + const FString & ErrorMessage); + +protected: + + // Process queued tasks. + void ProcessQueuedTasks(); + + // Task : instantiate an asset. + void TaskInstantiateAsset(const FHoudiniEngineTask & Task); + + // Task : cook an asset. + void TaskCookAsset(const FHoudiniEngineTask & Task); + + // Create description of task's state. + void TaskDescription( + FHoudiniEngineTaskInfo & Task, + const FString & ActorName, + const FString & StatusString); + + // Delete an asset. + void TaskDeleteAsset(const FHoudiniEngineTask & Task); + + // Process the result of a sucesfull cook + void TaskProccessAsset(const FHoudiniEngineTask & Task); + +private: + + // Initial number of tasks in our circular queue. + static const uint32 InitialTaskSize; + + // Frequency update (sleep time between each update) + static const float UpdateFrequency; + + // Synchronization primitive. + FCriticalSection CriticalSection; + + // Event to wake up thread when tasks become available. + FEventRef WakeUpEvent; + + // List of scheduled tasks. + FHoudiniEngineTask* Tasks; + + // Head of the circular queue. + uint32 PositionWrite; + + // Tail of the circular queue. + uint32 PositionRead; + + // Size of the circular queue. + uint32 TaskCount; + + // Stopping flag. + bool bStopping; +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineString.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineString.h" + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEnginePrivatePCH.h" + +#include + +#include "HoudiniEngineUtils.h" + +FCriticalSection FHoudiniEngineString::GetStringCriticalSection; + +FHoudiniEngineString::FHoudiniEngineString() + : StringId(-1) +{} + +FHoudiniEngineString::FHoudiniEngineString(int32 InStringId) + : StringId(InStringId) +{} + +FHoudiniEngineString::FHoudiniEngineString(const FHoudiniEngineString& Other) + : StringId(Other.StringId) +{} + +FHoudiniEngineString & +FHoudiniEngineString::operator=(const FHoudiniEngineString& Other) +{ + if (this != &Other) + StringId = Other.StringId; + + return *this; +} + +bool +FHoudiniEngineString::operator==(const FHoudiniEngineString& Other) const +{ + return Other.StringId == StringId; +} + +bool +FHoudiniEngineString::operator!=(const FHoudiniEngineString& Other) const +{ + return Other.StringId != StringId; +} + +int32 +FHoudiniEngineString::GetId() const +{ + return StringId; +} + +bool +FHoudiniEngineString::HasValidId() const +{ + return StringId > 0; +} + +bool +FHoudiniEngineString::ToStdString(std::string& String, const HAPI_Session* InSession) const +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineString::ToStdString); + + String = ""; + + // Null string ID / zero should be considered invalid + // (or we'd get the "null string, should never see this!" text) + if (StringId <= 0) + { + return false; + } + + int32 NameLength = 0; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetStringBufLength( + InSession ? InSession : FHoudiniEngine::Get().GetSession(), StringId, &NameLength)) + { + return false; + } + + if (NameLength <= 0) + return false; + + std::vector NameBuffer(NameLength, '\0'); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetString( + InSession ? InSession : FHoudiniEngine::Get().GetSession(), + StringId, &NameBuffer[0], NameLength ) ) + { + return false; + } + + String = std::string(NameBuffer.begin(), NameBuffer.end()); + + return true; +} + +bool +FHoudiniEngineString::ToFName(FName& Name, const HAPI_Session* InSession) const +{ + Name = NAME_None; + FString NameString = TEXT(""); + if (ToFString(NameString, InSession)) + { + Name = FName(*NameString); + return true; + } + + return false; +} + +bool +FHoudiniEngineString::ToFString(FString& String, const HAPI_Session* InSession) const +{ + String = TEXT(""); + std::string NamePlain = ""; + + if (ToStdString(NamePlain, InSession)) + { + String = UTF8_TO_TCHAR(NamePlain.c_str()); + return true; + } + + return false; +} + +bool +FHoudiniEngineString::ToFText(FText& Text, const HAPI_Session* InSession) const +{ + Text = FText::GetEmpty(); + FString NameString = TEXT(""); + + if (ToFString(NameString, InSession)) + { + Text = FText::FromString(NameString); + return true; + } + + return false; +} + +bool +FHoudiniEngineString::ToStdString( + const int32& InStringId, + std::string& OutStdString, + const HAPI_Session* InSession) +{ + FHoudiniEngineString HAPIString(InStringId); + return HAPIString.ToStdString(OutStdString, InSession); +} + +bool +FHoudiniEngineString::ToFName( + const int32& InStringId, + FName& OutName, + const HAPI_Session* InSession) +{ + FHoudiniEngineString HAPIString(InStringId); + return HAPIString.ToFName(OutName, InSession); +} + +bool +FHoudiniEngineString::ToFString( + const int32& InStringId, + FString& OutString, + const HAPI_Session* InSession) +{ + FHoudiniEngineString HAPIString(InStringId); + return HAPIString.ToFString(OutString, InSession); +} + +bool +FHoudiniEngineString::ToFText( + const int32& InStringId, + FText& OutText, + const HAPI_Session* InSession) +{ + FHoudiniEngineString HAPIString(InStringId); + return HAPIString.ToFText(OutText, InSession); +} + +FString +FHoudiniEngineString::ToFString(const HAPI_Session* InSession) const +{ + FString Result; + ToFString(Result, InSession); + return Result; +} + +bool +FHoudiniEngineString::SHArrayToFStringArray( + const TArray& InStringIdArray, + TArray& OutStringArray, + const HAPI_Session* InSession) +{ + OutStringArray.SetNumZeroed(InStringIdArray.Num()); + return SHArrayToFStringArray(InStringIdArray, OutStringArray.GetData(), InSession); +} + +bool +FHoudiniEngineString::SHArrayToFStringArray( + const TArray& InStringIdArray, + FString* OutStringArray, + const HAPI_Session* InSession) +{ + if (SHArrayToFStringArray_Batch(InStringIdArray, OutStringArray, InSession)) + return true; + + return SHArrayToFStringArray_Singles(InStringIdArray, OutStringArray, InSession); +} + +bool +FHoudiniEngineString::SHArrayToFStringArray_Batch( + const TArray& InStringIdArray, + FString* OutStringArray, + const HAPI_Session* InSession) +{ + bool bReturn = true; + + TSet UniqueSH; + for (const auto& CurrentSH : InStringIdArray) + { + UniqueSH.Add(CurrentSH); + } + + TArray UniqueSHArray = UniqueSH.Array(); + + int32 BufferSize = 0; + TArray Buffer; + { + // We can only get one string batch at a time. Otherwise another thread could clear the + // string table data before actually get to retrieve it. + FScopeLock GetStringDataScopeLock(&GetStringCriticalSection); + + if (HAPI_RESULT_SUCCESS + != FHoudiniApi::GetStringBatchSize(InSession ? InSession : FHoudiniEngine::Get().GetSession(), UniqueSHArray.GetData(), + UniqueSHArray.Num(), &BufferSize)) + return false; + + if (BufferSize <= 0) + return false; + + Buffer.SetNumZeroed(BufferSize); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetStringBatch(InSession ? InSession : FHoudiniEngine::Get().GetSession(), &Buffer[0], BufferSize)) + return false; + } + + // Parse the buffer to a string array + TMap StringMap; + int Index = 0; + int StringOffset = 0; + while (StringOffset < BufferSize) + { + // Add the current string to our dictionary. + FString StringValue = UTF8_TO_TCHAR(& Buffer[StringOffset]); + StringMap.Add(UniqueSHArray[Index], StringValue); + + // Move on to next indexed string + Index++; + while (Buffer[StringOffset] != 0 && StringOffset < BufferSize) + StringOffset++; + + StringOffset++; + } + + if (StringMap.Num() != UniqueSH.Num()) + return false; + + // Fill the output array using the map + for (int32 IdxSH = 0; IdxSH < InStringIdArray.Num(); IdxSH++) + { + OutStringArray[IdxSH] = StringMap[InStringIdArray[IdxSH]]; + } + + return true; +} + +bool +FHoudiniEngineString::SHArrayToFStringArray_Singles( + const TArray& InStringIdArray, + FString* OutStringArray, + const HAPI_Session* InSession) +{ + bool bReturn = true; + + // Avoid calling HAPI to resolve the same strings again and again + TMap ResolvedStrings; + for (int32 IdxSH = 0; IdxSH < InStringIdArray.Num(); IdxSH++) + { + const int32* ResolvedString = ResolvedStrings.Find(InStringIdArray[IdxSH]); + if (ResolvedString) + { + // Already resolved earlier, copy the string instead of calling HAPI. + OutStringArray[IdxSH] = OutStringArray[*ResolvedString]; + } + else + { + FString CurrentString = FString(); + if(!FHoudiniEngineString::ToFString(InStringIdArray[IdxSH], CurrentString)) + bReturn = false; + + OutStringArray[IdxSH] = CurrentString; + ResolvedStrings.Add(InStringIdArray[IdxSH], IdxSH); + } + } + + return bReturn; +} + +const FString& FHoudiniEngineIndexedStringMap::GetStringForIndex(int Index) const +{ + StringId Id = Ids[Index]; + return Strings[Id]; +} + +void FHoudiniEngineIndexedStringMap::SetString(int Index, const FString& Value) +{ + StringId Id; + const auto * Found = StringToId.Find(Value); + if (Found != nullptr) + { + Id = *Found; + } + else + { + Id = Strings.Num(); + Strings.Add(Value); + StringToId.Add(Value, Id); + } + + if (Index >= Ids.Num()) + Ids.SetNum(Index + 1); + + Ids[Index] = Id; +} + + +FHoudiniEngineRawStrings FHoudiniEngineIndexedStringMap::GetRawStrings() const +{ + FHoudiniEngineRawStrings Results; + Results.CreateRawStrings(Strings); + return Results; +} + +void FHoudiniEngineIndexedStringMap::Reset(int ExpectedStringCount, int ExpectedIndexCount) +{ + FHoudiniEngineIndexedStringMap Map; + *this = Map; + Ids.Reserve(ExpectedIndexCount); + Strings.Reserve(ExpectedStringCount); +} + +void FHoudiniEngineRawStrings::CreateRawStrings(const TArrayView Strings) +{ + RawStrings.SetNumZeroed(Strings.Num()); + Buffer.SetNum(0); + + // Calculate buffer size up front. + int BufferSize = 0; + for (int Id = 0; Id < Strings.Num(); Id++) + { + const FString& Str = Strings[Id]; + std::string ConvertedString = H_TCHAR_TO_UTF8(*Str); + const char* TempString = ConvertedString.c_str(); + + int TempStringLen = strlen(TempString); + BufferSize += TempStringLen + 1; + } + + Buffer.SetNum(BufferSize); + int StringStart = 0; + for (int Id = 0; Id < Strings.Num(); Id++) + { + const FString& Str = Strings[Id]; + std::string ConvertedString = H_TCHAR_TO_UTF8(*Str); + const char* TempString = ConvertedString.c_str(); + + RawStrings[Id] = &Buffer[StringStart]; + int TempStringLen = strlen(TempString); + FMemory::Memcpy(&Buffer[StringStart], TempString, TempStringLen + 1); + StringStart += TempStringLen + 1; + } +} + +void FHoudiniEngineIndexedStringMap::InitializeFromStringHandles(const TArray& StringHandles) +{ + const HAPI_Session* Session = FHoudiniEngine::Get().GetSession(); + + Ids.SetNum(StringHandles.Num()); + Strings.Empty(); + StringToId.Empty(); + + for (int StringHandleIndex = 0; StringHandleIndex < StringHandles.Num(); StringHandleIndex++) + { + FString NewString; + FHoudiniEngineString::ToFString(StringHandles[StringHandleIndex], NewString, Session); + SetString(StringHandleIndex, NewString); + } +} + +void FHoudiniEngineIndexedStringMap::InitializeFromStrings(const TArray& StringsToUse) +{ + Ids.SetNum(Strings.Num()); + Strings.Empty(); + StringToId.Empty(); + + for (int Index = 0; Index < Strings.Num(); Index++) + { + SetString(Index, StringsToUse[Index]); + } +} + + +bool FHoudiniEngineIndexedStringMap::HasEntries() +{ + // If there are no entries, there are... no entries! + if (Strings.Num() == 0) + return false; + + // If there is one entry and it is an empty string.. no entries. + if (Strings.Num() == 1 && Strings[0].IsEmpty()) + return false; + + return true; +} + + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineString.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include +#include "HoudiniApi.h" +#include "Containers/Map.h" +#include "HAL/CriticalSection.h" +class FText; +class FString; +class FName; + +class HOUDINIENGINE_API FHoudiniEngineString +{ + public: + + FHoudiniEngineString(); + FHoudiniEngineString(int32 InStringId); + FHoudiniEngineString(const FHoudiniEngineString & Other); + + FHoudiniEngineString & operator=(const FHoudiniEngineString & Other); + + bool operator==(const FHoudiniEngineString & Other) const; + bool operator!=(const FHoudiniEngineString & Other) const; + + // Conversion functions + bool ToStdString(std::string & String, const HAPI_Session* InSession = nullptr) const; + bool ToFName(FName & Name, const HAPI_Session* InSession = nullptr) const; + bool ToFString(FString & String, const HAPI_Session* InSession = nullptr) const; + bool ToFText(FText & Text, const HAPI_Session* InSession = nullptr) const; + FString ToFString(const HAPI_Session* InSession = nullptr) const; + + // Static converters + static bool ToStdString( + const int32& InStringId, + std::string& String, + const HAPI_Session* InSession = nullptr); + static bool ToFName( + const int32& InStringId, + FName& Name, + const HAPI_Session* InSession = nullptr); + static bool ToFString( + const int32& InStringId, + FString& String, + const HAPI_Session* InSession = nullptr); + static bool ToFText( + const int32& InStringId, + FText& Text, + const HAPI_Session* InSession = nullptr); + + // Array converter, uses a map to avoid redudant calls to HAPI + static bool SHArrayToFStringArray( const TArray& InStringIdArray, FString* OutStringArray, const HAPI_Session* InSession = nullptr); + static bool SHArrayToFStringArray(const TArray& InStringIdArray, TArray & OutStringArray, const HAPI_Session* InSession = nullptr); + + // Array converter, uses string batches and a map to reduce HAPI calls + static bool SHArrayToFStringArray_Batch( + const TArray& InStringIdArray, + FString* OutStringArray, + const HAPI_Session* InSession = nullptr); + + // Array converter, uses a map to reduce HAPI calls + static bool SHArrayToFStringArray_Singles( + const TArray& InStringIdArray, + FString* OutStringArray, + const HAPI_Session* InSession = nullptr); + + // Return id of this string. + int32 GetId() const; + + // Return true if this string has a valid id. + bool HasValidId() const; + + protected: + + // Id of the underlying Houdini Engine string. + int32 StringId; + + private: + + // Needed to lock the HAPI string table, otherwise it may get cleared by a HAPI call from + // another thread. + static FCriticalSection GetStringCriticalSection; +}; + +class FHoudiniEngineRawStrings +{ +public: + // This class coverts an array of Unreal FStrings to raw C++ strings. + // Since the strings need memory the pointers are stored in RawStrings + // and the data is stored in "Buffer". Destructing this class automatically + // deletes the temporary memory memory needed. + + void CreateRawStrings(const TArrayView Strings); + + TArray RawStrings; + TArray Buffer; +}; + +class FHoudiniEngineIndexedStringMap +{ +public: + // This class stores indexed strings which can be used with HAPI. Every time a new + // string is added via SetString(index,string) the string is checked to see if it + // is already present and, if so, its re-used. + + using StringId = int; + + void InitializeFromStringHandles(const TArray& StringHandles); + void InitializeFromStrings(const TArray& Strings); + + void Reset(int ExpectedStringCount, int ExpectedIndexCount); + const FString& GetStringForIndex(int index) const; + void SetString(int Index, const FString & value); + + FHoudiniEngineRawStrings GetRawStrings() const; + + const TArray & GetIds() const { return Ids; } + + bool HasEntries(); + + TArray Strings; // Each unique string. + TMap StringToId; // Backwards mapping to optimize lookup + TArray Ids; // Strings[Ids[i]] is the string to use. +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineTask.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineTask.h" + +#include "HoudiniApi.h" + +FHoudiniEngineTask::FHoudiniEngineTask() + : TaskType(EHoudiniEngineTaskType::None) + , ActorName(TEXT("")) + , AssetId(-1) + , bUseOutputNodes(false) + , bOutputTemplateGeos(false) + , AssetLibraryId(-1) + , AssetHapiName(-1) +{ + HapiGUID.Invalidate(); + OtherNodeIds.Empty(); +} + +FHoudiniEngineTask::FHoudiniEngineTask(EHoudiniEngineTaskType InTaskType, FGuid InHapiGUID) + : HapiGUID(InHapiGUID) + , TaskType(InTaskType) + , ActorName(TEXT("")) + , AssetId(-1) + , bUseOutputNodes(false) + , bOutputTemplateGeos(false) + , AssetLibraryId(-1) + , AssetHapiName(-1) +{ + OtherNodeIds.Empty(); +} + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineTask.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniApi.h" +#include "CoreMinimal.h" +#include "Misc/Guid.h" +#include "UObject/WeakObjectPtr.h" + +#include "HoudiniEngineTask.generated.h" + +/* +namespace EHoudiniEngineTaskType +{ + enum Type + { + None, + + // This type corresponds to Houdini asset instantiation (without cooking). + AssetInstantiation, + + // This type corresponds to Houdini asset cooking request. + AssetCooking, + + // This type is used for asynchronous asset deletion. + AssetDeletion + }; +} +*/ + +UENUM() +enum class EHoudiniEngineTaskType : uint8 +{ + None, + + // This type corresponds to Houdini asset instantiation (without cooking). + AssetInstantiation, + + // This type corresponds to Houdini asset cooking request. + AssetCooking, + + // This type is used for asynchronous asset deletion. + AssetDeletion, + + // This type is used when processing the results of a sucessful cook + AssetProcess, +}; + +struct HOUDINIENGINE_API FHoudiniEngineTask +{ + // Constructors. + FHoudiniEngineTask(); + FHoudiniEngineTask(EHoudiniEngineTaskType InTaskType, FGuid InHapiGUID); + + // GUID of this request. + FGuid HapiGUID; + + // Type of this task. + EHoudiniEngineTaskType TaskType; + + // Houdini asset for instantiation. + TWeakObjectPtr< class UHoudiniAsset > Asset; + + // Name of the actor requesting this task. + FString ActorName; + + // Asset Id. + HAPI_NodeId AssetId; + + // Additional Node Id for the task + // Can be used to apply a task to multiple nodes in the same HDA + TArray OtherNodeIds; + // Cook results for each output node. + TMap CookResults; + + bool bUseOutputNodes; + bool bOutputTemplateGeos; + + FString NodeLabelPrefix; + + // Library Id. + HAPI_AssetLibraryId AssetLibraryId; + + // HAPI name of the asset. + int32 AssetHapiName; + + // Is set to true if component has been loaded. + //bool bLoadedComponent; +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineTaskInfo.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineTaskInfo.h" + +#include "HAPI/HAPI_Common.h" + +FHoudiniEngineTaskInfo::FHoudiniEngineTaskInfo() + : Result(HAPI_RESULT_SUCCESS) + , AssetId(-1) + , TaskType(EHoudiniEngineTaskType::None) + , TaskState(EHoudiniEngineTaskState::None) +{} + +FHoudiniEngineTaskInfo::FHoudiniEngineTaskInfo( + HAPI_Result InResult, + HAPI_NodeId InAssetId, + EHoudiniEngineTaskType InTaskType, + EHoudiniEngineTaskState InTaskState) + : Result(InResult) + , AssetId(InAssetId) + , TaskType(InTaskType) + , TaskState(InTaskState) +{} +``` + +`Source/HoudiniEngine/Private/HoudiniEngineTaskInfo.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniEngineTask.h" + +#include "HoudiniEngineTaskInfo.generated.h" + +UENUM() +enum class EHoudiniEngineTaskState : uint8 +{ + None, + + // Indicates the current task is still running + Working, + + // Indicates the task has successfully finished + Success, + + // Indicates the task has finished with non fatal errors + FinishedWithError, + + // Indicates the task has finished with fatal errors and should be terminated + FinishedWithFatalError, + + // Indicates the task has been aborted (unused) + Aborted +}; + +struct HOUDINIENGINE_API FHoudiniEngineTaskInfo +{ + // Constructors. + FHoudiniEngineTaskInfo(); + FHoudiniEngineTaskInfo( + HAPI_Result InResult, HAPI_NodeId InAssetId, + EHoudiniEngineTaskType InTaskType, + EHoudiniEngineTaskState InTaskState); + + // Current HAPI result. + HAPI_Result Result; + + // Current Asset Id. + HAPI_NodeId AssetId; + + // Type of task. + EHoudiniEngineTaskType TaskType; + + // Current status. + EHoudiniEngineTaskState TaskState; + + // String used for status / progress bar. + FText StatusText; + + // Is set to true if corresponding task was issued for loaded component. + //bool bLoadedComponent; +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineTimers.cpp`: + +```cpp +/* + * Copyright (c) <2023> Side Effects Software Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. The name of Side Effects Software may not be used to endorse or + * promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineTimers.h" + +#include "HAL/IConsoleManager.h" + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineTimers.h`: + +```h +/* + * Copyright (c) <2021> Side Effects Software Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. The name of Side Effects Software may not be used to endorse or + * promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +// Adds a new cpu profile event with the name of the function +#define H_SCOPED_FUNCTION_TIMER() \ + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) + +// Adds a new cpu profile event with the name of an FString +#define H_SCOPED_FUNCTION_DYNAMIC_LABEL(__LABEL) \ + TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(TCHAR_TO_ANSI(*__LABEL)) + +// Adds a new cpu profile event with a static label. For some reason, adding __FUNCTION__ +// fails to compile on Mac. +#define H_SCOPED_FUNCTION_STATIC_LABEL(__LABEL) \ + TRACE_CPUPROFILER_EVENT_SCOPE_STR(__LABEL) + + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineUtils.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineUtils.h" + +#if PLATFORM_WINDOWS + #include "Windows/WindowsHWrapper.h" + + // Of course, Windows defines its own GetGeoInfo, + // So we need to undefine that before including HoudiniApi.h to avoid collision... + #ifdef GetGeoInfo + #undef GetGeoInfo + #endif +#endif + +#include "HAPI/HAPI_Version.h" + +#include "HoudiniApi.h" +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineEditorSettings.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniEngineString.h" +#include "HoudiniEngineTimers.h" +#include "HoudiniGenericAttribute.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniInput.h" +#include "HoudiniParameter.h" +#include "HoudiniRuntimeSettings.h" +#include "HoudiniOutputTranslator.h" + +#if WITH_EDITOR + #include "SAssetSelectionWidget.h" +#endif + +#include "AssetRegistry/AssetRegistryModule.h" +#include "Async/Async.h" +#include "BlueprintEditor.h" +#include "Editor/EditorEngine.h" +#include "Editor/UnrealEdEngine.h" +#include "Engine/BlueprintGeneratedClass.h" +#include "Engine/SkeletalMesh.h" +#include "Engine/StaticMeshSocket.h" +#include "Engine/WorldComposition.h" +#include "Factories/WorldFactory.h" +#include "FileHelpers.h" +#include "FoliageEditUtility.h" +#include "FoliageType_InstancedStaticMesh.h" +#include "Framework/Application/SlateApplication.h" +#include "Framework/Notifications/NotificationManager.h" +#include "HAL/FileManager.h" +#include "InstancedFoliageActor.h" +#include "Interfaces/IPluginManager.h" +#include "LandscapeStreamingProxy.h" +#include "Misc/Paths.h" +#include "Misc/StringFormatArg.h" +#include "Modules/ModuleManager.h" +#include "PropertyEditorModule.h" +#include "SSCSEditor.h" +#include "SSubobjectEditor.h" +#include "UnrealEdGlobals.h" +#include "UObject/MetaData.h" +#include "Widgets/Notifications/SNotificationList.h" + +#include + +#include "HoudiniEngineAttributes.h" +#include "Serialization/JsonSerializer.h" + +#if WITH_EDITOR + #include "EditorFramework/AssetImportData.h" + #include "EditorModeManager.h" + #include "EditorModes.h" + #include "Interfaces/IMainFrameModule.h" +#endif +#include "UObject/ObjectSaveContext.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +TAutoConsoleVariable CVarHoudiniEngineMeshBuildTimer( + TEXT("HoudiniEngine.MeshBuildTimer"), + 0.0, + TEXT("When enabled, the plugin will output timings during the Mesh creation.\n") +); + +FHoudiniEngineUtils::FOnHoudiniProxyMeshesRefinedDelegate FHoudiniEngineUtils::OnHoudiniProxyMeshesRefinedDelegate = FHoudiniEngineUtils::FOnHoudiniProxyMeshesRefinedDelegate(); + +// HAPI_Result strings +const FString kResultStringSuccess(TEXT("Success")); +const FString kResultStringFailure(TEXT("Generic Failure")); +const FString kResultStringAlreadyInitialized(TEXT("Already Initialized")); +const FString kResultStringNotInitialized(TEXT("Not Initialized")); +const FString kResultStringCannotLoadFile(TEXT("Unable to Load File")); +const FString kResultStringParmSetFailed(TEXT("Failed Setting Parameter")); +const FString kResultStringInvalidArgument(TEXT("Invalid Argument")); +const FString kResultStringCannotLoadGeo(TEXT("Uneable to Load Geometry")); +const FString kResultStringCannotGeneratePreset(TEXT("Uneable to Generate Preset")); +const FString kResultStringCannotLoadPreset(TEXT("Uneable to Load Preset")); +const FString kResultStringAssetDefAlrealdyLoaded(TEXT("Asset definition already loaded")); +const FString kResultStringNoLicenseFound(TEXT("No License Found")); +const FString kResultStringDisallowedNCLicenseFound(TEXT("Disallowed Non Commercial License found")); +const FString kResultStringDisallowedNCAssetWithCLicense(TEXT("Disallowed Non Commercial Asset With Commercial License")); +const FString kResultStringDisallowedNCAssetWithLCLicense(TEXT("Disallowed Non Commercial Asset With Limited Commercial License")); +const FString kResultStringDisallowedLCAssetWithCLicense(TEXT("Disallowed Limited Commercial Asset With Commercial License")); +const FString kResultStringDisallowedHengineIndieWith3PartyPlugin(TEXT("Disallowed Houdini Engine Indie With 3rd Party Plugin")); +const FString kResultStringAssetInvalid(TEXT("Invalid Asset")); +const FString kResultStringNodeInvalid(TEXT("Invalid Node")); +const FString kResultStringUserInterrupted(TEXT("User Interrupt")); +const FString kResultStringInvalidSession(TEXT("Invalid Session")); +const FString kResultStringUnknowFailure(TEXT("Unknown Failure")); + +#define DebugTextLine TEXT("===================================") + +const int32 +FHoudiniEngineUtils::PackageGUIDComponentNameLength = 12; + +const int32 +FHoudiniEngineUtils::PackageGUIDItemNameLength = 8; + + + +// Maximum size of the data that can be sent via thrift +//#define THRIFT_MAX_CHUNKSIZE 100 * 1024 * 1024 // This is supposedly the current limit in thrift, but still seems to be too large +#define THRIFT_MAX_CHUNKSIZE 10 * 1024 * 1024 +//#define THRIFT_MAX_CHUNKSIZE 2048 * 2048 +//#define THRIFT_MAX_CHUNKSIZE_STRING 256 * 256 + +const FString +FHoudiniEngineUtils::GetErrorDescription(HAPI_Result Result) +{ + if (Result == HAPI_RESULT_SUCCESS) + { + return kResultStringSuccess; + } + else + { + switch (Result) + { + case HAPI_RESULT_FAILURE: + { + return kResultStringFailure; + } + + case HAPI_RESULT_ALREADY_INITIALIZED: + { + return kResultStringAlreadyInitialized; + } + + case HAPI_RESULT_NOT_INITIALIZED: + { + return kResultStringNotInitialized; + } + + case HAPI_RESULT_CANT_LOADFILE: + { + return kResultStringCannotLoadFile; + } + + case HAPI_RESULT_PARM_SET_FAILED: + { + return kResultStringParmSetFailed; + } + + case HAPI_RESULT_INVALID_ARGUMENT: + { + return kResultStringInvalidArgument; + } + + case HAPI_RESULT_CANT_LOAD_GEO: + { + return kResultStringCannotLoadGeo; + } + + case HAPI_RESULT_CANT_GENERATE_PRESET: + { + return kResultStringCannotGeneratePreset; + } + + case HAPI_RESULT_CANT_LOAD_PRESET: + { + return kResultStringCannotLoadPreset; + } + + case HAPI_RESULT_ASSET_DEF_ALREADY_LOADED: + { + return kResultStringAssetDefAlrealdyLoaded; + } + + case HAPI_RESULT_NO_LICENSE_FOUND: + { + return kResultStringNoLicenseFound; + } + + case HAPI_RESULT_DISALLOWED_NC_LICENSE_FOUND: + { + return kResultStringDisallowedNCLicenseFound; + } + + case HAPI_RESULT_DISALLOWED_NC_ASSET_WITH_C_LICENSE: + { + return kResultStringDisallowedNCAssetWithCLicense; + } + + case HAPI_RESULT_DISALLOWED_NC_ASSET_WITH_LC_LICENSE: + { + return kResultStringDisallowedNCAssetWithLCLicense; + } + + case HAPI_RESULT_DISALLOWED_LC_ASSET_WITH_C_LICENSE: + { + return kResultStringDisallowedLCAssetWithCLicense; + } + + case HAPI_RESULT_DISALLOWED_HENGINEINDIE_W_3PARTY_PLUGIN: + { + return kResultStringDisallowedHengineIndieWith3PartyPlugin; + } + + case HAPI_RESULT_ASSET_INVALID: + { + return kResultStringAssetInvalid; + } + + case HAPI_RESULT_NODE_INVALID: + { + return kResultStringNodeInvalid; + } + + case HAPI_RESULT_USER_INTERRUPTED: + { + return kResultStringUserInterrupted; + } + + case HAPI_RESULT_INVALID_SESSION: + { + return kResultStringInvalidSession; + } + + default: + { + return kResultStringUnknowFailure; + } + }; + } +} + +const FString +FHoudiniEngineUtils::GetStatusString(HAPI_StatusType status_type, HAPI_StatusVerbosity verbosity) +{ + const HAPI_Session* SessionPtr = FHoudiniEngine::Get().GetSession(); + if (!SessionPtr) + { + // No valid session + return FString(TEXT("No valid Houdini Engine session.")); + } + + int32 StatusBufferLength = 0; + HAPI_Result Result = FHoudiniApi::GetStatusStringBufLength( + SessionPtr, status_type, verbosity, &StatusBufferLength); + + if (Result == HAPI_RESULT_INVALID_SESSION) + { + // Let FHoudiniEngine know that the sesion is now invalid to "Stop" the invalid session + // and clean things up + FHoudiniEngine::Get().OnSessionLost(); + } + + if (StatusBufferLength > 0) + { + TArray< char > StatusStringBuffer; + StatusStringBuffer.SetNumZeroed(StatusBufferLength); + FHoudiniApi::GetStatusString( + SessionPtr, status_type, &StatusStringBuffer[0], StatusBufferLength); + + return FString(UTF8_TO_TCHAR(&StatusStringBuffer[0])); + } + + return FString(TEXT("")); +} + +FString +FHoudiniEngineUtils::HapiGetString(int32 StringHandle) +{ + int32 StringLength = 0; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetStringBufLength( + FHoudiniEngine::Get().GetSession(), StringHandle, &StringLength)) + { + return FString(); + } + + if (StringLength <= 0) + return FString(); + + std::vector NameBuffer(StringLength, '\0'); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetString( + FHoudiniEngine::Get().GetSession(), + StringHandle, &NameBuffer[0], StringLength ) ) + { + return FString(); + } + + return FString(std::string(NameBuffer.begin(), NameBuffer.end()).c_str()); +} + + +const FString +FHoudiniEngineUtils::GetCookResult() +{ + return FHoudiniEngineUtils::GetStatusString(HAPI_STATUS_COOK_RESULT, HAPI_STATUSVERBOSITY_MESSAGES); +} + +const FString +FHoudiniEngineUtils::GetCookState() +{ + return FHoudiniEngineUtils::GetStatusString(HAPI_STATUS_COOK_STATE, HAPI_STATUSVERBOSITY_ERRORS); +} + +const FString +FHoudiniEngineUtils::GetErrorDescription() +{ + return FHoudiniEngineUtils::GetStatusString(HAPI_STATUS_CALL_RESULT, HAPI_STATUSVERBOSITY_ERRORS); +} + +const FString +FHoudiniEngineUtils::GetConnectionError() +{ + int32 ErrorLength = 0; + FHoudiniApi::GetConnectionErrorLength(&ErrorLength); + + if(ErrorLength <= 0) + return FString(TEXT("")); + + TArray ConnectionStringBuffer; + ConnectionStringBuffer.SetNumZeroed(ErrorLength); + FHoudiniApi::GetConnectionError(&ConnectionStringBuffer[0], ErrorLength, true); + + return FString(UTF8_TO_TCHAR(&ConnectionStringBuffer[0])); +} + +void +FHoudiniEngineUtils::MarkAllCookablesAsNeedInstantiation() +{ + // Notify all the Cookables that they need to re instantiate themselves in the new Houdini engine session. + for (TObjectIterator Itr; Itr; ++Itr) + { + UHoudiniCookable* HC = *Itr; + if (!IsValid(HC)) + continue; + + HC->MarkAsNeedInstantiation(); + } +} + +const FString +FHoudiniEngineUtils::GetNodeErrorsWarningsAndMessages(HAPI_NodeId InNodeId) +{ + int32 NodeErrorLength = 0; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::ComposeNodeCookResult( + FHoudiniEngine::Get().GetSession(), + InNodeId, HAPI_StatusVerbosity::HAPI_STATUSVERBOSITY_ALL, &NodeErrorLength)) + { + NodeErrorLength = 0; + } + + FString NodeError; + if (NodeErrorLength > 0) + { + TArray NodeErrorBuffer; + NodeErrorBuffer.SetNumZeroed(NodeErrorLength); + FHoudiniApi::GetComposedNodeCookResult( + FHoudiniEngine::Get().GetSession(), &NodeErrorBuffer[0], NodeErrorLength); + + NodeError = FString(UTF8_TO_TCHAR(&NodeErrorBuffer[0])); + } + + return NodeError; +} + +const FString +FHoudiniEngineUtils::GetCookLog(const TArray& InNodeIds) +{ + FString CookLog; + + // Get fetch cook status. + FString CookResult = FHoudiniEngineUtils::GetCookResult(); + if (!CookResult.IsEmpty()) + CookLog += TEXT("Cook Results:\n") + CookResult + TEXT("\n\n"); + + // Add the cook state + FString CookState = FHoudiniEngineUtils::GetCookState(); + if (!CookState.IsEmpty()) + CookLog += TEXT("Cook State:\n") + CookState + TEXT("\n\n"); + + // Error Description + FString Error = FHoudiniEngineUtils::GetErrorDescription(); + if (!Error.IsEmpty()) + CookLog += TEXT("Error Description:\n") + Error + TEXT("\n\n"); + + // Iterates on all the selected HAC and get their node errors + for (auto& NodeId : InNodeIds) + { + if (NodeId < 0) + continue; + + // Get the node errors, warnings and messages + FString NodeErrors = FHoudiniEngineUtils::GetNodeErrorsWarningsAndMessages(NodeId); + if (NodeErrors.IsEmpty()) + continue; + + CookLog += NodeErrors; + } + + if (CookLog.IsEmpty()) + { + // See if a failed HAPI initialization / invalid session is preventing us from getting the cook log + if (!FHoudiniApi::IsHAPIInitialized()) + { + CookLog += TEXT("\n\nThe Houdini Engine API Library (HAPI) has not been initialized properly.\n\n"); + } + else + { + const HAPI_Session * SessionPtr = FHoudiniEngine::Get().GetSession(); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::IsSessionValid(SessionPtr)) + { + CookLog += TEXT("\n\nThe current Houdini Engine Session is not valid.\n\n"); + } + else if (HAPI_RESULT_SUCCESS != FHoudiniApi::IsInitialized(SessionPtr)) + { + CookLog += TEXT("\n\nThe current Houdini Engine Session has not been initialized properly.\n\n"); + } + } + + if (!CookLog.IsEmpty()) + { + CookLog += TEXT("Please try to restart the current Houdini Engine session via File > Restart Houdini Engine Session.\n\n"); + } + else + { + CookLog = TEXT("\n\nThe cook log is empty...\n\n"); + } + } + + return CookLog; +} + +const FString +FHoudiniEngineUtils::GetAssetHelp(HAPI_NodeId InNodeId) +{ + FString HelpString = TEXT(""); + if (InNodeId < 0) + return HelpString; + + HAPI_AssetInfo AssetInfo; + FHoudiniApi::AssetInfo_Init(&AssetInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAssetInfo( + FHoudiniEngine::Get().GetSession(), InNodeId, &AssetInfo), HelpString); + + if (FHoudiniEngineString::ToFString(AssetInfo.helpTextSH, HelpString)) + return HelpString; + + if (HelpString.IsEmpty()) + HelpString = TEXT("No Asset Help Found"); + + return HelpString; +} + +const FString +FHoudiniEngineUtils::GetAssetHelpURL(HAPI_NodeId InNodeId) +{ + FString HelpString = TEXT(""); + if (InNodeId < 0) + return HelpString; + + HAPI_AssetInfo AssetInfo; + FHoudiniApi::AssetInfo_Init(&AssetInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAssetInfo( + FHoudiniEngine::Get().GetSession(), InNodeId, &AssetInfo), HelpString); + + // If we have a help url, use it first + if (FHoudiniEngineString::ToFString(AssetInfo.helpURLSH, HelpString)) + return HelpString; + + return HelpString; +} + +void +FHoudiniEngineUtils::ConvertUnrealString(const FString & UnrealString, std::string & String) +{ + String = H_TCHAR_TO_UTF8(*UnrealString); +} + +UWorld* +FHoudiniEngineUtils::FindWorldInPackage(const FString& PackagePath, bool bCreateMissingPackage, bool& bOutCreatedPackage) +{ + AActor* Result = nullptr; + UWorld* PackageWorld = nullptr; + + bOutCreatedPackage = false; + + // Try to load existing UWorld from the tile package path. + UPackage* Package = FindPackage(nullptr, *PackagePath); + if (!Package) + Package = LoadPackage(nullptr, *PackagePath, LOAD_None); + + if (IsValid(Package)) + { + PackageWorld = UWorld::FindWorldInPackage(Package); + } + else if (Package != nullptr) + { + // If the package is not valid (pending kill) rename it + if (bCreateMissingPackage) + { + Package->Rename( + *MakeUniqueObjectName(Package->GetOuter(), Package->GetClass(), FName(PackagePath + TEXT("_pending_kill"))).ToString()); + } + } + + if (!IsValid(PackageWorld) && bCreateMissingPackage) + { + const FName ShortName(FPackageName::GetShortName(PackagePath)); + // The map for this tile does not exist. Create one + UWorldFactory* Factory = NewObject(); + Factory->WorldType = EWorldType::Inactive; // World that is being loaded but not currently edited by editor. + PackageWorld = CastChecked(Factory->FactoryCreateNew(UWorld::StaticClass(), Package, ShortName, RF_Public | RF_Standalone, NULL, GWarn)); + + if (IsValid(PackageWorld)) + { + PackageWorld->PostEditChange(); + PackageWorld->MarkPackageDirty(); + + if(FPackageName::IsValidLongPackageName(PackagePath)) + { + const FString PackageFilename = FPackageName::LongPackageNameToFilename(PackagePath); + bool bSaved = FEditorFileUtils::SaveLevel(PackageWorld->PersistentLevel, *PackageFilename); + } + + FAssetRegistryModule::AssetCreated(PackageWorld); + + bOutCreatedPackage = true; + } + } + + return PackageWorld; +} + +bool +FHoudiniEngineUtils::FindWorldAndLevelForSpawning( + UWorld* CurrentWorld, + const FString& PackagePath, + bool bCreateMissingPackage, + UWorld*& OutWorld, + ULevel*& OutLevel, + bool& bOutPackageCreated, + bool& bPackageInWorld) +{ + UWorld* PackageWorld = FindWorldInPackage(PackagePath, bCreateMissingPackage, bOutPackageCreated); + if (!IsValid(PackageWorld)) + return false; + + if (PackageWorld->PersistentLevel == CurrentWorld->PersistentLevel) + { + // The loaded world and the package world is one and the same. + OutWorld = CurrentWorld; + OutLevel = CurrentWorld->PersistentLevel; + bPackageInWorld = true; + return true; + } + + if (CurrentWorld->GetLevels().Contains(PackageWorld->PersistentLevel)) + { + // The package level is loaded into CurrentWorld. + OutWorld = CurrentWorld; + OutLevel = PackageWorld->PersistentLevel; + bPackageInWorld = true; + return true; + } + + // The package level is not loaded at all. Send back the on-disk assets. + OutWorld = PackageWorld; + OutLevel = PackageWorld->PersistentLevel; + bPackageInWorld = false; + return true; +} + +void +FHoudiniEngineUtils::RescanWorldPath(UWorld* InWorld) +{ + FString WorldPath = FPaths::GetPath(InWorld->GetPathName()); + IAssetRegistry& AssetRegistry = FAssetRegistryModule::GetRegistry(); + TArray Packages; + Packages.Add(WorldPath); + AssetRegistry.ScanPathsSynchronous(Packages, true); +} + +TArray +FHoudiniEngineUtils::FindActorsWithNameNoNumber(UClass* InClass, UWorld* InWorld, const FString& InActorName) +{ + TArray Results; + + for (TActorIterator ActorIt(InWorld, InClass); ActorIt; ++ActorIt) + { + AActor * Actor = *ActorIt; + if (Actor->GetFName().GetPlainNameString() == InActorName) + Results.Add(Actor); + } + return Results; +} + +AActor* +FHoudiniEngineUtils::FindOrRenameInvalidActorGeneric(UClass* InClass, UWorld* InWorld, const FString& InName, AActor*& OutFoundActor) +{ + // AActor* NamedActor = FindObject(Outer, *InName, false); + // Find ANY actor in the world matching the given name. + AActor* NamedActor = FindActorInWorld(InWorld, FName(InName)); + OutFoundActor = NamedActor; + + FString Suffix; + if (IsValid(NamedActor)) + { + if (NamedActor->GetClass()->IsChildOf(InClass)) + { + return NamedActor; + } + else + { + // A previous actor that had the same name. + Suffix = "_0"; + } + } + else + { + if (!NamedActor) + return nullptr; + else + Suffix = "_pendingkill"; + } + + // Rename the invalid/previous actor + const FName NewName = FHoudiniEngineUtils::RenameToUniqueActor(NamedActor, InName + Suffix); + + return nullptr; +} + +void +FHoudiniEngineUtils::LogPackageInfo(const FString& InLongPackageName) +{ + LogPackageInfo( LoadPackage(nullptr, *InLongPackageName, 0) ); +} + +void FHoudiniEngineUtils::LogPackageInfo(const UPackage* InPackage) +{ + HOUDINI_LOG_MESSAGE(DebugTextLine); + HOUDINI_LOG_MESSAGE(TEXT("= LogPackageInfo")); + if (!IsValid(InPackage)) + { + HOUDINI_LOG_MESSAGE(TEXT(" = Invalid package.")); + HOUDINI_LOG_MESSAGE(DebugTextLine); + return; + } + + HOUDINI_LOG_MESSAGE(TEXT(" = Filename: %s"), *(InPackage->GetLoadedPath().GetPackageName())); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + HOUDINI_LOG_MESSAGE(TEXT(" = Package Id: %s"), *(LexToString(InPackage->GetPackageId()))); +#else + HOUDINI_LOG_MESSAGE(TEXT(" = Package Id: %d"), InPackage->GetPackageId().ValueForDebugging()); +#endif + HOUDINI_LOG_MESSAGE(TEXT(" = File size: %d"), InPackage->GetFileSize()); + HOUDINI_LOG_MESSAGE(TEXT(" = Contains map: %d"), InPackage->ContainsMap()); + HOUDINI_LOG_MESSAGE(TEXT(" = Is Fully Loaded: %d"), InPackage->IsFullyLoaded()); + HOUDINI_LOG_MESSAGE(TEXT(" = Is Dirty: %d"), InPackage->IsDirty()); + + FWorldTileInfo* WorldTileInfo = InPackage->GetWorldTileInfo(); + if (WorldTileInfo) + { + HOUDINI_LOG_MESSAGE(TEXT(" = WorldTileInfo - Position: %s"), *(WorldTileInfo->Position.ToString())); + HOUDINI_LOG_MESSAGE(TEXT(" = WorldTileInfo - Absolute Position: %s"), *(WorldTileInfo->AbsolutePosition.ToString())); + HOUDINI_LOG_MESSAGE(TEXT(" = WorldTileInfo - Bounds: %s"), *(WorldTileInfo->Bounds.ToString())); + HOUDINI_LOG_MESSAGE(TEXT(" = WorldTileInfo - HidInTileView: %d"), WorldTileInfo->bHideInTileView); + HOUDINI_LOG_MESSAGE(TEXT(" = WorldTileInfo - ZOrder: %d"), WorldTileInfo->ZOrder); + HOUDINI_LOG_MESSAGE(TEXT(" = WorldTileInfo - Parent tile package: %s"), *(WorldTileInfo->ParentTilePackageName)); + } + else + { + HOUDINI_LOG_MESSAGE(TEXT(" = WorldTileInfo: NULL")); + } + + HOUDINI_LOG_MESSAGE(DebugTextLine); +} + +void +FHoudiniEngineUtils::LogWorldInfo(const FString& InLongPackageName) +{ + UPackage* Package = LoadPackage(nullptr, *InLongPackageName, 0); + UWorld* World = nullptr; + + if (IsValid(Package)) + { + World = UWorld::FindWorldInPackage(Package); + } + + LogWorldInfo(World); +} + +void +FHoudiniEngineUtils::LogWorldInfo(const UWorld* InWorld) +{ + + HOUDINI_LOG_MESSAGE(DebugTextLine); + HOUDINI_LOG_MESSAGE(TEXT("= LogWorldInfo")); + if (!IsValid(InWorld)) + { + HOUDINI_LOG_MESSAGE(TEXT(" = Invalid world.")); + HOUDINI_LOG_MESSAGE(DebugTextLine); + return; + } + + // UWorld lacks const-correctness on certain accessors + UWorld* NonConstWorld = const_cast(InWorld); + + HOUDINI_LOG_MESSAGE(TEXT(" = Path Name: %s"), *(InWorld->GetPathName())); + HOUDINI_LOG_MESSAGE(TEXT(" = Is Editor World: %d"), InWorld->IsEditorWorld() ); + HOUDINI_LOG_MESSAGE(TEXT(" = Is Game World: %d"), InWorld->IsGameWorld() ); + HOUDINI_LOG_MESSAGE(TEXT(" = Is Preview World: %d"), InWorld->IsPreviewWorld() ); + HOUDINI_LOG_MESSAGE(TEXT(" = Actor Count: %d"), NonConstWorld->GetActorCount() ); + HOUDINI_LOG_MESSAGE(TEXT(" = Num Levels: %d"), InWorld->GetNumLevels() ); + + if (IsValid(InWorld->WorldComposition)) + { + HOUDINI_LOG_MESSAGE(TEXT(" = Composition - Num Tiles: %d"), InWorld->WorldComposition->GetTilesList().Num() ); + } + else + { + HOUDINI_LOG_MESSAGE(TEXT(" = World Composition NULL") ); + } + + + + HOUDINI_LOG_MESSAGE(DebugTextLine); +} + +FString +FHoudiniEngineUtils::HapiGetEventTypeAsString(const HAPI_PDG_EventType& InEventType) +{ + switch (InEventType) + { + case HAPI_PDG_EVENT_NULL: + return TEXT("HAPI_PDG_EVENT_NULL"); + + case HAPI_PDG_EVENT_WORKITEM_ADD: + return TEXT("HAPI_PDG_EVENT_WORKITEM_ADD"); + case HAPI_PDG_EVENT_WORKITEM_REMOVE: + return TEXT("HAPI_PDG_EVENT_WORKITEM_REMOVE"); + case HAPI_PDG_EVENT_WORKITEM_STATE_CHANGE: + return TEXT("HAPI_PDG_EVENT_WORKITEM_STATE_CHANGE"); + + case HAPI_PDG_EVENT_WORKITEM_ADD_DEP: + return TEXT("HAPI_PDG_EVENT_WORKITEM_ADD_DEP"); + case HAPI_PDG_EVENT_WORKITEM_REMOVE_DEP: + return TEXT("HAPI_PDG_EVENT_WORKITEM_REMOVE_DEP"); + + case HAPI_PDG_EVENT_WORKITEM_ADD_PARENT: + return TEXT("HAPI_PDG_EVENT_WORKITEM_ADD_PARENT"); + case HAPI_PDG_EVENT_WORKITEM_REMOVE_PARENT: + return TEXT("HAPI_PDG_EVENT_WORKITEM_REMOVE_PARENT"); + + case HAPI_PDG_EVENT_NODE_CLEAR: + return TEXT("HAPI_PDG_EVENT_NODE_CLEAR"); + + case HAPI_PDG_EVENT_COOK_ERROR: + return TEXT("HAPI_PDG_EVENT_COOK_ERROR"); + case HAPI_PDG_EVENT_COOK_WARNING: + return TEXT("HAPI_PDG_EVENT_COOK_WARNING"); + + case HAPI_PDG_EVENT_COOK_COMPLETE: + return TEXT("HAPI_PDG_EVENT_COOK_COMPLETE"); + + case HAPI_PDG_EVENT_DIRTY_START: + return TEXT("HAPI_PDG_EVENT_DIRTY_START"); + case HAPI_PDG_EVENT_DIRTY_STOP: + return TEXT("HAPI_PDG_EVENT_DIRTY_STOP"); + + case HAPI_PDG_EVENT_DIRTY_ALL: + return TEXT("HAPI_PDG_EVENT_DIRTY_ALL"); + + case HAPI_PDG_EVENT_UI_SELECT: + return TEXT("HAPI_PDG_EVENT_UI_SELECT"); + + case HAPI_PDG_EVENT_NODE_CREATE: + return TEXT("HAPI_PDG_EVENT_NODE_CREATE"); + case HAPI_PDG_EVENT_NODE_REMOVE: + return TEXT("HAPI_PDG_EVENT_NODE_REMOVE"); + case HAPI_PDG_EVENT_NODE_RENAME: + return TEXT("HAPI_PDG_EVENT_NODE_RENAME"); + case HAPI_PDG_EVENT_NODE_CONNECT: + return TEXT("HAPI_PDG_EVENT_NODE_CONNECT"); + case HAPI_PDG_EVENT_NODE_DISCONNECT: + return TEXT("HAPI_PDG_EVENT_NODE_DISCONNECT"); + + case HAPI_PDG_EVENT_WORKITEM_SET_INT: + return TEXT("HAPI_PDG_EVENT_WORKITEM_SET_INT"); // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_SET_FLOAT: + return TEXT("HAPI_PDG_EVENT_WORKITEM_SET_FLOAT"); // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_SET_STRING: + return TEXT("HAPI_PDG_EVENT_WORKITEM_SET_STRING"); // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_SET_FILE: + return TEXT("HAPI_PDG_EVENT_WORKITEM_SET_FILE"); // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_SET_PYOBJECT: + return TEXT("HAPI_PDG_EVENT_WORKITEM_SET_PYOBJECT"); // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_SET_GEOMETRY: + return TEXT("HAPI_PDG_EVENT_WORKITEM_SET_GEOMETRY"); // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_MERGE: + return TEXT("HAPI_PDG_EVENT_WORKITEM_MERGE"); // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_RESULT: + return TEXT("HAPI_PDG_EVENT_WORKITEM_RESULT"); + + case HAPI_PDG_EVENT_WORKITEM_PRIORITY: // DEPRECATED + return TEXT("HAPI_PDG_EVENT_WORKITEM_PRIORITY"); + + case HAPI_PDG_EVENT_COOK_START: + return TEXT("HAPI_PDG_EVENT_COOK_START"); + + case HAPI_PDG_EVENT_WORKITEM_ADD_STATIC_ANCESTOR: + return TEXT("HAPI_PDG_EVENT_WORKITEM_ADD_STATIC_ANCESTOR"); + case HAPI_PDG_EVENT_WORKITEM_REMOVE_STATIC_ANCESTOR: + return TEXT("HAPI_PDG_EVENT_WORKITEM_REMOVE_STATIC_ANCESTOR"); + + case HAPI_PDG_EVENT_NODE_PROGRESS_UPDATE: + return TEXT("HAPI_PDG_EVENT_NODE_PROGRESS_UPDATE"); + + case HAPI_PDG_EVENT_BATCH_ITEM_INITIALIZED: + return TEXT("HAPI_PDG_EVENT_BATCH_ITEM_INITIALIZED"); + + case HAPI_PDG_EVENT_ALL: + return TEXT("HAPI_PDG_EVENT_ALL"); + case HAPI_PDG_EVENT_LOG: + return TEXT("HAPI_PDG_EVENT_LOG"); + + case HAPI_PDG_EVENT_SCHEDULER_ADDED: + return TEXT("HAPI_PDG_EVENT_SCHEDULER_ADDED"); + case HAPI_PDG_EVENT_SCHEDULER_REMOVED: + return TEXT("HAPI_PDG_EVENT_SCHEDULER_REMOVED"); + case HAPI_PDG_EVENT_SET_SCHEDULER: + return TEXT("HAPI_PDG_EVENT_SET_SCHEDULER"); + + case HAPI_PDG_EVENT_SERVICE_MANAGER_ALL: + return TEXT("HAPI_PDG_EVENT_SERVICE_MANAGER_ALL"); + + case HAPI_PDG_CONTEXT_EVENTS: + return TEXT("HAPI_PDG_CONTEXT_EVENTS"); + default: + break; + } + + return FString::Printf(TEXT("Unknown HAPI_PDG_EventType %d"), InEventType); +} + +FString +FHoudiniEngineUtils::HapiGetWorkItemStateAsString(const HAPI_PDG_WorkItemState& InWorkItemState) +{ + switch (InWorkItemState) + { + case HAPI_PDG_WORKITEM_UNDEFINED: + return TEXT("HAPI_PDG_WORKITEM_UNDEFINED"); + case HAPI_PDG_WORKITEM_UNCOOKED: + return TEXT("HAPI_PDG_WORKITEM_UNCOOKED"); + case HAPI_PDG_WORKITEM_WAITING: + return TEXT("HAPI_PDG_WORKITEM_WAITING"); + case HAPI_PDG_WORKITEM_SCHEDULED: + return TEXT("HAPI_PDG_WORKITEM_SCHEDULED"); + case HAPI_PDG_WORKITEM_COOKING: + return TEXT("HAPI_PDG_WORKITEM_COOKING"); + case HAPI_PDG_WORKITEM_COOKED_SUCCESS: + return TEXT("HAPI_PDG_WORKITEM_COOKED_SUCCESS"); + case HAPI_PDG_WORKITEM_COOKED_CACHE: + return TEXT("HAPI_PDG_WORKITEM_COOKED_CACHE"); + case HAPI_PDG_WORKITEM_COOKED_FAIL: + return TEXT("HAPI_PDG_WORKITEM_COOKED_FAIL"); + case HAPI_PDG_WORKITEM_COOKED_CANCEL: + return TEXT("HAPI_PDG_WORKITEM_COOKED_CANCEL"); + case HAPI_PDG_WORKITEM_DIRTY: + return TEXT("HAPI_PDG_WORKITEM_DIRTY"); + default: + break; + } + + return FString::Printf(TEXT("Unknown HAPI_PDG_WorkItemState %d"), InWorkItemState); +} + +// Centralized call to track renaming of objects +bool FHoudiniEngineUtils::RenameObject(UObject* Object, const TCHAR* NewName /*= nullptr*/, UObject* NewOuter /*= nullptr*/, ERenameFlags Flags /*= REN_None*/) +{ + check(Object); + if (AActor* Actor = Cast(Object)) + { + if (Actor->IsPackageExternal()) + { + // There should be no need to choose a specific name for an actor in Houdini Engine, instead setting its label should be enough. + FHoudiniEngineRuntimeUtils::SetActorLabel(Actor, NewName); + // Force to return false (make sure nothing in Houdini Engine plugin relies on actor being renamed to provided name) + return false; + } + } + return Object->Rename(NewName, NewOuter, Flags); +} + +FName +FHoudiniEngineUtils::RenameToUniqueActor(AActor* InActor, const FString& InName) +{ + const FName NewName = MakeUniqueObjectName(InActor->GetOuter(), InActor->GetClass(), FName(InName)); + + FHoudiniEngineUtils::RenameObject(InActor, *(NewName.ToString())); + FHoudiniEngineRuntimeUtils::SetActorLabel(InActor, NewName.ToString()); + + return NewName; +} + +UObject* +FHoudiniEngineUtils::SafeRenameActor(AActor* InActor, const FString& InName, bool UpdateLabel) +{ + check(InActor); + + UObject* PrevObj = nullptr; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + UObject* ExistingObject = StaticFindObject(/*Class=*/ NULL, InActor->GetOuter(), *InName, EFindObjectFlags::ExactClass); +#else + UObject* ExistingObject = StaticFindObject(/*Class=*/ NULL, InActor->GetOuter(), *InName, true); +#endif + if (ExistingObject && ExistingObject != InActor) + { + // Rename the existing object + const FName NewName = MakeUniqueObjectName(ExistingObject->GetOuter(), ExistingObject->GetClass(), FName(InName + TEXT("_old"))); + FHoudiniEngineUtils::RenameObject(ExistingObject, *(NewName.ToString())); + PrevObj = ExistingObject; + } + + FHoudiniEngineUtils::RenameObject(InActor, *InName); + + if (UpdateLabel) + { + //InActor->SetActorLabel(InName, true); + FHoudiniEngineRuntimeUtils::SetActorLabel(InActor, InName); + InActor->Modify(true); + } + + return PrevObj; +} + +bool +FHoudiniEngineUtils::ValidatePath(const FString& InPath, FText* OutInvalidPathReason) +{ + FString AbsolutePath; + if (InPath.StartsWith("/Game/")) + { + const FString RelativePath = FPaths::ProjectContentDir() + InPath.Mid(6, InPath.Len() - 6); + AbsolutePath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*RelativePath); + } + else + { + AbsolutePath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*InPath); + } + + return FPaths::ValidatePath(AbsolutePath, OutInvalidPathReason); +} + +bool +FHoudiniEngineUtils::DoesFolderExist(const FString& InPath) +{ + FString AbsolutePath; + if (InPath.StartsWith("/Game")) + { + const FString RelativePath = FPaths::ProjectContentDir() + InPath.Mid(6, InPath.Len() - 6); + AbsolutePath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*RelativePath); + } + else + { + AbsolutePath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*InPath); + } + + return FPaths::DirectoryExists(AbsolutePath); +} + +void +FHoudiniEngineUtils::FillInPackageParamsForBakingOutput( + FHoudiniPackageParams& OutPackageParams, + const FHoudiniOutputObjectIdentifier& InIdentifier, + const FString &BakeFolder, + const FString &ObjectName, + const FString &HoudiniAssetName, + const FString &HoudiniAssetActorName, + EPackageReplaceMode InReplaceMode, + bool bAutomaticallySetAttemptToLoadMissingPackages, + const TOptional& InComponentGuid) +{ + OutPackageParams.GeoId = InIdentifier.GeoId; + OutPackageParams.ObjectId = InIdentifier.ObjectId; + OutPackageParams.PartId = InIdentifier.PartId; + OutPackageParams.SplitStr = InIdentifier.SplitIdentifier; + OutPackageParams.BakeFolder = BakeFolder; + OutPackageParams.PackageMode = EPackageMode::Bake; + OutPackageParams.ReplaceMode = InReplaceMode; + OutPackageParams.HoudiniAssetName = HoudiniAssetName; + OutPackageParams.HoudiniAssetActorName = HoudiniAssetActorName; + OutPackageParams.ObjectName = ObjectName; + if (InComponentGuid.IsSet()) + OutPackageParams.ComponentGUID = InComponentGuid.GetValue(); +} + +void +FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( + UWorld* const InWorldContext, + const UHoudiniCookable* InCookable, + const FHoudiniOutputObjectIdentifier& InIdentifier, + const FHoudiniOutputObject& InOutputObject, + const bool bInHasPreviousBakeData, + const FString &InDefaultObjectName, + FHoudiniPackageParams& OutPackageParams, + FHoudiniAttributeResolver& OutResolver, + const FString &InDefaultBakeFolder, + EPackageReplaceMode InReplaceMode, + const FString &InHoudiniAssetName, + const FString &InHoudiniAssetActorName, + bool bAutomaticallySetAttemptToLoadMissingPackages, + bool bInSkipObjectNameResolutionAndUseDefault, + bool bInSkipBakeFolderResolutionAndUseDefault) +{ + // Configure OutPackageParams with the default (UI value first then fallback to default from settings) object name + // and bake folder. We use the "initial" PackageParams as a helper to populate tokens for the resolver. + // + // User specified attributes (eg unreal_bake_folder) are then resolved, with the defaults being those tokens configured + // from the initial PackageParams. Once resolved, we updated the relevant fields in PackageParams + // (ObjectName and BakeFolder), and update the resolver tokens with these final values. + // + // The resolver is then ready to be used to resolve the rest of the user attributes, such as unreal_level_path. + // + const FString DefaultBakeFolder = !InDefaultBakeFolder.IsEmpty() ? InDefaultBakeFolder : + FHoudiniEngineRuntime::Get().GetDefaultBakeFolder(); + + const bool bIsHCValid = IsValid(InCookable); + + // If InHoudiniAssetName was specified, use that, otherwise use the name of the UHoudiniAsset used by the + // HoudiniAssetComponent + FString HoudiniAssetName(TEXT("")); + if (!InHoudiniAssetName.IsEmpty()) + { + HoudiniAssetName = InHoudiniAssetName; + } + else if (bIsHCValid) + { + HoudiniAssetName = InCookable->GetHoudiniAssetName(); + } + + // If InHoudiniAssetActorName was specified, use that, otherwise use the name of the owner of HoudiniAssetComponent + FString HoudiniAssetActorName(TEXT("")); + if (!InHoudiniAssetActorName.IsEmpty()) + { + HoudiniAssetActorName = InHoudiniAssetActorName; + } + else if (bIsHCValid && IsValid(InCookable->GetOwner())) + { + HoudiniAssetActorName = InCookable->GetOwner()->GetActorNameOrLabel(); + } + + // Get the HAC's GUID, if the HAC is valid + TOptional CookableGuid; + if (bIsHCValid) + CookableGuid = InCookable->GetCookableGUID(); + + const bool bHasBakeNameUIOverride = !InOutputObject.BakeName.IsEmpty(); + FillInPackageParamsForBakingOutput( + OutPackageParams, + InIdentifier, + DefaultBakeFolder, + bHasBakeNameUIOverride ? InOutputObject.BakeName : InDefaultObjectName, + HoudiniAssetName, + HoudiniAssetActorName, + InReplaceMode, + bAutomaticallySetAttemptToLoadMissingPackages, + CookableGuid); + + // If ObjectName is empty and InDefaultObjectName are empty, generate a default via GetPackageName + const FString DefaultObjectName = OutPackageParams.ObjectName.IsEmpty() && InDefaultObjectName.IsEmpty() + ? OutPackageParams.GetPackageName().TrimChar('_') : InDefaultObjectName; + if (OutPackageParams.ObjectName.IsEmpty()) + OutPackageParams.ObjectName = DefaultObjectName; + + const TMap& CachedAttributes = InOutputObject.CachedAttributes; + TMap Tokens = InOutputObject.CachedTokens; + OutPackageParams.UpdateTokensFromParams(InWorldContext, InCookable->GetComponent(), Tokens); + OutResolver.SetCachedAttributes(CachedAttributes); + OutResolver.SetTokensFromStringMap(Tokens); + +#if defined(HOUDINI_ENGINE_DEBUG_BAKING) && HOUDINI_ENGINE_DEBUG_BAKING + // Log the cached attributes and tokens for debugging + OutResolver.LogCachedAttributesAndTokens(); +#endif + + bool bUsedDefaultBakeName = !bHasBakeNameUIOverride; + if (!bInSkipObjectNameResolutionAndUseDefault) + { + // Resolve the object name + // TODO: currently the UI override is checked first (this should probably change so that attributes are used first) + FString ObjectName; + if (bHasBakeNameUIOverride) + { + ObjectName = InOutputObject.BakeName; + bUsedDefaultBakeName = false; + } + else + { + constexpr bool bForBake = true; + ObjectName = OutResolver.ResolveOutputName(bForBake, &bUsedDefaultBakeName); + if (ObjectName.IsEmpty()) + { + ObjectName = DefaultObjectName; + bUsedDefaultBakeName = true; + } + } + // Update the object name in the package params and also update its token + OutPackageParams.ObjectName = ObjectName; + OutResolver.SetToken("object_name", OutPackageParams.ObjectName); + } + + if (!bInSkipBakeFolderResolutionAndUseDefault) + { + // Now resolve the bake folder + const FString BakeFolder = OutResolver.ResolveBakeFolder(); + if (!BakeFolder.IsEmpty()) + OutPackageParams.BakeFolder = BakeFolder; + } + + if (!bInSkipObjectNameResolutionAndUseDefault || !bInSkipBakeFolderResolutionAndUseDefault) + { + // Update the tokens from the package params + OutPackageParams.UpdateTokensFromParams(InWorldContext, InCookable->GetComponent(), Tokens); + OutResolver.SetTokensFromStringMap(Tokens); + +#if defined(HOUDINI_ENGINE_DEBUG_BAKING) && HOUDINI_ENGINE_DEBUG_BAKING + // Log the final tokens + OutResolver.LogCachedAttributesAndTokens(); +#endif + } + + // If the default bake name is being used, and we haven't baked this output identifier on this output before, + // then do not allow replacement bakes. + if (bUsedDefaultBakeName && !bInHasPreviousBakeData && OutPackageParams.ReplaceMode == EPackageReplaceMode::ReplaceExistingAssets) + { + HOUDINI_BAKING_WARNING(TEXT( + "[FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver] Disabling replace bake mode: " + "default bake name is being used with no previous bake output for the object.")); + OutPackageParams.ReplaceMode = EPackageReplaceMode::CreateNewAssets; + } +} + +void +FHoudiniEngineUtils::UpdatePackageParamsForTempOutputWithResolver( + const FHoudiniPackageParams& InPackageParams, + const UWorld* InWorldContext, + const UObject* InOuterComponent, + const TMap& InCachedAttributes, + const TMap& InCachedTokens, + FHoudiniPackageParams& OutPackageParams, + FHoudiniAttributeResolver& OutResolver, + bool bInSkipTempFolderResolutionAndUseDefault) +{ + // Populate OutPackageParams from InPackageParams and then update it by resolving user attributes using string tokens. + // + // User specified attributes (eg unreal_temp_folder) are then resolved, with the defaults being those tokens configured + // from the initial PackageParams. Once resolved, we updated the relevant fields in PackageParams and update the + // resolver tokens with these final values. + // + OutPackageParams = InPackageParams; + + TMap Tokens = InCachedTokens; + OutPackageParams.UpdateTokensFromParams(InWorldContext, InOuterComponent, Tokens); + OutResolver.SetCachedAttributes(InCachedAttributes); + OutResolver.SetTokensFromStringMap(Tokens); + + if (!bInSkipTempFolderResolutionAndUseDefault) + { + // Now resolve the temp folder + const FString TempFolder = OutResolver.ResolveTempFolder(); + if (!TempFolder.IsEmpty()) + OutPackageParams.TempCookFolder = TempFolder; + } + + if (!bInSkipTempFolderResolutionAndUseDefault) + { + // Update the tokens from the package params + OutPackageParams.UpdateTokensFromParams(InWorldContext, InOuterComponent, Tokens); + OutResolver.SetTokensFromStringMap(Tokens); + } +} + +bool +FHoudiniEngineUtils::RepopulateFoliageTypeListInUI() +{ + // When running this as a commandlet there is no UI, + // so GLevelEditorModeTools() is cranky. + if (IsRunningCommandlet()) + return false; + + // Update / repopulate the foliage editor mode's mesh list if the foliage editor mode is active. + // TODO: find a better way to do this, the relevant functions are in FEdModeFoliage and FFoliageEdModeToolkit are not API exported + // + // This used to deactivate Foliage then Activate it again. But this crashed in UE 5.0, so for now go back to + // Placement mode. + FEditorModeTools& EditorModeTools = GLevelEditorModeTools(); + if (EditorModeTools.IsModeActive(FBuiltinEditorModes::EM_Foliage)) + { + EditorModeTools.DeactivateMode(FBuiltinEditorModes::EM_Foliage); + EditorModeTools.ActivateMode(FBuiltinEditorModes::EM_Placement); + return true; + } + + return false; +} + +void +FHoudiniEngineUtils::GatherLandscapeInputs( + const TArray>& Inputs, + TArray& AllInputLandscapes) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GatherLandscapeInputs); + + for(auto CurrentInput : Inputs) + { + if (!CurrentInput) + continue; + + if (CurrentInput->GetInputType() != EHoudiniInputType::World) + continue; + + // Check if we have any landscapes as world inputs. + CurrentInput->ForAllHoudiniInputObjects([&AllInputLandscapes](UHoudiniInputObject* InputObject) + { + UHoudiniInputLandscape* InputLandscape = Cast(InputObject); + if (InputLandscape) + { + ALandscapeProxy* LandscapeProxy = InputLandscape->GetLandscapeProxy(); + if (IsValid(LandscapeProxy)) + { + AllInputLandscapes.Add(LandscapeProxy); + } + } + }, true); + } +} + +USceneComponent* +FHoudiniEngineUtils::GetOuterSceneComponent(const UObject* Obj) +{ + if(!Obj) + return nullptr; + + // TODO: ? test cookable? + UObject* Outer = Obj->GetOuter(); + while (Outer) + { + USceneComponent* SceneComponent = Cast(Outer); + if(SceneComponent) + return SceneComponent; + Outer = Outer->GetOuter(); + } + return nullptr; +} + +UHoudiniCookable* +FHoudiniEngineUtils::GetOuterHoudiniCookable(const UObject* Obj) +{ + if (!IsValid(Obj)) + return nullptr; + + // Check the direct Outer + UHoudiniCookable* OuterHC = Cast(Obj->GetOuter()); + if (IsValid(OuterHC)) + return OuterHC; + + // Check the whole outer chain + OuterHC = Obj->GetTypedOuter(); + if (IsValid(OuterHC)) + return OuterHC; + + // Finally check if the Object itself is a HC + UObject* NonConstObj = const_cast(Obj); + OuterHC = Cast(NonConstObj); + if (IsValid(OuterHC)) + return OuterHC; + + return nullptr; +} + +UHoudiniAssetComponent* +FHoudiniEngineUtils::GetOuterHoudiniAssetComponent(const UObject* Obj) +{ + if (!IsValid(Obj)) + return nullptr; + + // Start by looking for a Cookable outer + UHoudiniCookable* OuterHC = FHoudiniEngineUtils::GetOuterHoudiniCookable(Obj); + if (IsValid(OuterHC)) + return Cast(OuterHC->GetComponent()); + + // Check the direct Outer + UHoudiniAssetComponent* OuterHAC = Cast(Obj->GetOuter()); + if(IsValid(OuterHAC)) + return OuterHAC; + + // Check the whole outer chain + OuterHAC = Obj->GetTypedOuter(); + if (IsValid(OuterHAC)) + return OuterHAC; + + // Finally check if the Object itself is a HaC + UObject* NonConstObj = const_cast(Obj); + OuterHAC = Cast(NonConstObj); + if (IsValid(OuterHAC)) + return OuterHAC; + + return nullptr; +} + + +FString +FHoudiniEngineUtils::ComputeVersionString(bool ExtraDigit) +{ + // Compute Houdini version string. + FString HoudiniVersionString = FString::Printf( + TEXT("%d.%d.%s%d"), HAPI_VERSION_HOUDINI_MAJOR, + HAPI_VERSION_HOUDINI_MINOR, + (ExtraDigit ? (TEXT("0.")) : TEXT("")), + HAPI_VERSION_HOUDINI_BUILD); + + // If we have a patch version, we need to append it. + if (HAPI_VERSION_HOUDINI_PATCH > 0) + HoudiniVersionString = FString::Printf(TEXT("%s.%d"), *HoudiniVersionString, HAPI_VERSION_HOUDINI_PATCH); + return HoudiniVersionString; +} + + +void * +FHoudiniEngineUtils::LoadLibHAPI(FString & StoredLibHAPILocation) +{ + FString HFSPath = TEXT(""); + void * HAPILibraryHandle = nullptr; + + // Look up HAPI_PATH environment variable; if it is not defined, 0 will stored in HFS_ENV_VARIABLE . + FString HFS_ENV_VAR = FPlatformMisc::GetEnvironmentVariable(TEXT("HAPI_PATH")); + if (!HFS_ENV_VAR.IsEmpty()) + HFSPath = HFS_ENV_VAR; + + // Look up environment variable; if it is not defined, 0 will stored in HFS_ENV_VARIABLE . + HFS_ENV_VAR = FPlatformMisc::GetEnvironmentVariable(TEXT("HFS")); + if (!HFS_ENV_VAR.IsEmpty()) + HFSPath = HFS_ENV_VAR; + + // Get platform specific name of libHAPI. + FString LibHAPIName = FHoudiniEngineRuntimeUtils::GetLibHAPIName(); + + // If we have a custom location specified through settings, attempt to use that. + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + const UHoudiniEngineEditorSettings * HoudiniEngineEditorSettings = GetDefault< UHoudiniEngineEditorSettings >(); + bool bCustomPathFound = false; + if (IsValid(HoudiniEngineEditorSettings) || IsValid(HoudiniRuntimeSettings)) + { + bool bUseCustomPath = false; + FString CustomHoudiniLocationPath; + + // The user can set a editor per-project user setting in UHoudiniEngineEditorSettings to determine if + // the custom location should be disabled, read from the editor per-project user settings or read from the + // per-project settings. + if (HoudiniEngineEditorSettings && HoudiniEngineEditorSettings->UseCustomHoudiniLocation == EHoudiniEngineEditorSettingUseCustomLocation::Enabled) + { + bUseCustomPath = true; + CustomHoudiniLocationPath = HoudiniEngineEditorSettings->CustomHoudiniLocation.Path; + } + else if ((!HoudiniEngineEditorSettings || HoudiniEngineEditorSettings->UseCustomHoudiniLocation == EHoudiniEngineEditorSettingUseCustomLocation::Project) && + HoudiniRuntimeSettings && HoudiniRuntimeSettings->bUseCustomHoudiniLocation) + { + bUseCustomPath = true; + CustomHoudiniLocationPath = HoudiniRuntimeSettings->CustomHoudiniLocation.Path; + } + + if (bUseCustomPath && !CustomHoudiniLocationPath.IsEmpty()) + { + // Convert path to absolute if it is relative. + if (FPaths::IsRelative(CustomHoudiniLocationPath)) + CustomHoudiniLocationPath = FPaths::ConvertRelativePathToFull(CustomHoudiniLocationPath); + + const FString LibHAPICustomPath = FString::Printf(TEXT("%s/%s"), *CustomHoudiniLocationPath, *LibHAPIName); + + if (FPaths::FileExists(LibHAPICustomPath)) + { + HFSPath = CustomHoudiniLocationPath; + bCustomPathFound = true; + } + } + } + + // We have HFS environment variable defined (or custom location), attempt to load libHAPI from it. + if (!HFSPath.IsEmpty()) + { + if (!bCustomPathFound) + { +#if PLATFORM_WINDOWS + HFSPath += FString::Printf(TEXT("/%s"), HAPI_HFS_SUBFOLDER_WINDOWS); +#elif PLATFORM_MAC + HFSPath += FString::Printf(TEXT("/%s"), HAPI_HFS_SUBFOLDER_MAC); +#elif PLATFORM_LINUX + HFSPath += FString::Printf(TEXT("/%s"), HAPI_HFS_SUBFOLDER_LINUX); +#endif + } + + // Create full path to libHAPI binary. +#if PLATFORM_MAC + FString LibHAPIPath = FString::Printf(TEXT("%s/../Libraries/%s"), *HFSPath, *LibHAPIName); + + // TODO: Handle developer environment paths on macOS. +// if (!FPaths::FileExists(LibHAPIPath)) +// LibHAPIPath = FString::Printf(TEXT("%s/dsolib/%s"), *HFSPath, *LibHAPIName); + +#else + FString LibHAPIPath = FString::Printf(TEXT("%s/%s"), *HFSPath, *LibHAPIName); +#endif + + if (FPaths::FileExists(LibHAPIPath)) + { + // libHAPI binary exists at specified location, attempt to load it. + FPlatformProcess::PushDllDirectory(*HFSPath); +#if PLATFORM_WINDOWS + HAPILibraryHandle = FPlatformProcess::GetDllHandle(*LibHAPIName); +#elif PLATFORM_MAC || PLATFORM_LINUX + HAPILibraryHandle = FPlatformProcess::GetDllHandle(*LibHAPIPath); +#endif + FPlatformProcess::PopDllDirectory(*HFSPath); + + // If library has been loaded successfully we can stop. + if ( HAPILibraryHandle ) + { + if (bCustomPathFound) + HOUDINI_LOG_MESSAGE(TEXT("Loaded %s from custom path %s"), *LibHAPIName, *HFSPath); + else + HOUDINI_LOG_MESSAGE(TEXT("Loaded %s from HFS environment path %s"), *LibHAPIName, *HFSPath); + + StoredLibHAPILocation = HFSPath; + return HAPILibraryHandle; + } + } + } + + // Otherwise, we will attempt to detect Houdini installation. + FString HoudiniLocation = TEXT(HOUDINI_ENGINE_HFS_PATH); + FString LibHAPIPath; + + // Compute Houdini version string. + FString HoudiniVersionString = ComputeVersionString(false); + +#if PLATFORM_WINDOWS + + // On Windows, we have also hardcoded HFS path in plugin configuration file; attempt to load from it. + HFSPath = FString::Printf(TEXT("%s/%s"), *HoudiniLocation, HAPI_HFS_SUBFOLDER_WINDOWS); + + // Create full path to libHAPI binary. + LibHAPIPath = FString::Printf(TEXT("%s/%s"), *HFSPath, *LibHAPIName); + + if (FPaths::FileExists(LibHAPIPath)) + { + FPlatformProcess::PushDllDirectory(*HFSPath); + HAPILibraryHandle = FPlatformProcess::GetDllHandle(*LibHAPIName); + FPlatformProcess::PopDllDirectory(*HFSPath); + + if (HAPILibraryHandle) + { + HOUDINI_LOG_MESSAGE(TEXT("Loaded %s from Plugin defined HFS path %s"), *LibHAPIName, *HFSPath); + StoredLibHAPILocation = HFSPath; + return HAPILibraryHandle; + } + } + + // As a second attempt, on Windows, we try to look up location of Houdini Engine in the registry. + HAPILibraryHandle = FHoudiniEngineUtils::LocateLibHAPIInRegistry( + TEXT("Houdini Engine"), StoredLibHAPILocation, false); + if (HAPILibraryHandle) + return HAPILibraryHandle; + + // As a third attempt, we try to look up location of Houdini installation (not Houdini Engine) in the registry. + HAPILibraryHandle = FHoudiniEngineUtils::LocateLibHAPIInRegistry( + TEXT("Houdini"), StoredLibHAPILocation, false); + if (HAPILibraryHandle) + return HAPILibraryHandle; + + // Do similar registry lookups for the 32 bits registry + // Look for the Houdini Engine registry install path + HAPILibraryHandle = FHoudiniEngineUtils::LocateLibHAPIInRegistry( + TEXT("Houdini Engine"), StoredLibHAPILocation, true); + if (HAPILibraryHandle) + return HAPILibraryHandle; + + // ... and for the Houdini registry install path + HAPILibraryHandle = FHoudiniEngineUtils::LocateLibHAPIInRegistry( + TEXT("Houdini"), StoredLibHAPILocation, true); + if (HAPILibraryHandle) + return HAPILibraryHandle; + + // Finally, try to load from a hardcoded program files path. + HoudiniLocation = FString::Printf( + TEXT("C:\\Program Files\\Side Effects Software\\Houdini %s\\%s"), *HoudiniVersionString, HAPI_HFS_SUBFOLDER_WINDOWS); + +#else + +# if PLATFORM_MAC + + // Attempt to load from standard Mac OS X installation. + HoudiniLocation = FString::Printf( + TEXT("/Applications/Houdini/Houdini%s/Frameworks/Houdini.framework/Versions/Current/Libraries"), *HoudiniVersionString); + + // Fallback in case the previous one doesnt exist + if (!FPaths::DirectoryExists(HoudiniLocation)) + HoudiniLocation = FString::Printf( + TEXT("/Applications/Houdini/Houdini%s/Frameworks/Houdini.framework/Versions/%s/Libraries"), *HoudiniVersionString, *HoudiniVersionString); + + // Fallback in case we're using the steam version + if (!FPaths::DirectoryExists(HoudiniLocation)) + HoudiniLocation = FString::Printf( + TEXT("/Applications/Houdini/HoudiniIndieSteam/Frameworks/Houdini.framework/Versions/Current/Libraries")); + + // Backup Fallback in case we're using the steam version + // (this could probably be removed as paths have changed) + if (!FPaths::DirectoryExists(HoudiniLocation)) + HoudiniLocation = FString::Printf( + TEXT("/Users/Shared/Houdini/HoudiniIndieSteam/Frameworks/Houdini.framework/Versions/Current/Libraries")); + +# elif PLATFORM_LINUX + + // Attempt to load from standard Linux installation. + HoudiniLocation = FString::Printf( + TEXT("/opt/hfs%s/%s"), *HoudiniVersionString, HAPI_HFS_SUBFOLDER_LINUX); + +# endif + +#endif + + // Create full path to libHAPI binary. + LibHAPIPath = FString::Printf(TEXT("%s/%s"), *HoudiniLocation, *LibHAPIName); + + if (FPaths::FileExists(LibHAPIPath)) + { + FPlatformProcess::PushDllDirectory(*HoudiniLocation); + HAPILibraryHandle = FPlatformProcess::GetDllHandle(*LibHAPIPath); + FPlatformProcess::PopDllDirectory(*HoudiniLocation); + + if (HAPILibraryHandle) + { + HOUDINI_LOG_MESSAGE(TEXT("Loaded %s from expected installation %s"), *LibHAPIName, *HoudiniLocation); + StoredLibHAPILocation = HoudiniLocation; + return HAPILibraryHandle; + } + } + + StoredLibHAPILocation = TEXT(""); + return HAPILibraryHandle; +} + +bool +FHoudiniEngineUtils::IsInitialized() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::IsInitialized); + if (!FHoudiniApi::IsHAPIInitialized()) + return false; + + const HAPI_Session * SessionPtr = FHoudiniEngine::Get().GetSession(); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::IsSessionValid(SessionPtr)) + return false; + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::IsInitialized(SessionPtr)) + return false; + + return true; +} + +bool +FHoudiniEngineUtils::IsHoudiniNodeValid(HAPI_NodeId NodeId) +{ + if (NodeId < 0) + return false; + + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + bool ValidationAnswer = true; + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), NodeId, &NodeInfo)) + { + return false; + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::IsNodeValid( + FHoudiniEngine::Get().GetSession(), NodeId, + NodeInfo.uniqueHoudiniNodeId, &ValidationAnswer)) + { + return false; + } + + return ValidationAnswer; +} + +bool +FHoudiniEngineUtils::HapiDisconnectAsset(HAPI_NodeId HostAssetId, int32 InputIndex) +{ + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::DisconnectNodeInput( + FHoudiniEngine::Get().GetSession(), HostAssetId, InputIndex), false); + + return true; +} + +bool +FHoudiniEngineUtils::DestroyHoudiniAsset(HAPI_NodeId AssetId) +{ + if (HAPI_RESULT_SUCCESS == FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), AssetId)) + { + return true; + } + + return false; +} + +bool +FHoudiniEngineUtils::DeleteHoudiniNode(HAPI_NodeId InNodeId) +{ + if (HAPI_RESULT_SUCCESS == FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), InNodeId)) + { + return true; + } + + return false; +} + +#if PLATFORM_WINDOWS +void * +FHoudiniEngineUtils::LocateLibHAPIInRegistry( + const FString & HoudiniInstallationType, + FString & StoredLibHAPILocation, + bool LookIn32bitRegistry) +{ + auto FindDll = [&](const FString& InHoudiniInstallationPath) + { + FString HFSPath = FString::Printf(TEXT("%s/%s"), *InHoudiniInstallationPath, HAPI_HFS_SUBFOLDER_WINDOWS); + + // Create full path to libHAPI binary. + FString LibHAPIPath = FString::Printf(TEXT("%s/%s"), *HFSPath, HAPI_LIB_OBJECT_WINDOWS); + + if (FPaths::FileExists(LibHAPIPath)) + { + FPlatformProcess::PushDllDirectory(*HFSPath); + void* HAPILibraryHandle = FPlatformProcess::GetDllHandle(HAPI_LIB_OBJECT_WINDOWS); + FPlatformProcess::PopDllDirectory(*HFSPath); + + if (HAPILibraryHandle) + { + HOUDINI_LOG_MESSAGE( + TEXT("Loaded %s from Registry path %s"), HAPI_LIB_OBJECT_WINDOWS, + *HFSPath); + + StoredLibHAPILocation = HFSPath; + return HAPILibraryHandle; + } + } + return (void*)0; + }; + + FString HoudiniInstallationPath; + FString HoudiniVersionString = ComputeVersionString(true); + FString RegistryKey = FString::Printf( + TEXT("Software\\%sSide Effects Software\\%s"), + (LookIn32bitRegistry ? TEXT("WOW6432Node\\") : TEXT("")), *HoudiniInstallationType); + + if (FWindowsPlatformMisc::QueryRegKey( + HKEY_LOCAL_MACHINE, *RegistryKey, *HoudiniVersionString, HoudiniInstallationPath)) + { + FPaths::NormalizeDirectoryName(HoudiniInstallationPath); + return FindDll(HoudiniInstallationPath); + } + + return nullptr; +} +#endif + +bool +FHoudiniEngineUtils::LoadHoudiniAsset(const UHoudiniAsset * HoudiniAsset, HAPI_AssetLibraryId& OutAssetLibraryId) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::LoadHoudiniAsset); + + OutAssetLibraryId = -1; + + if (!IsValid(HoudiniAsset)) + return false; + + if (!FHoudiniEngineUtils::IsInitialized()) + { + // If we're not initialized now, it likely means the session has been lost + FHoudiniEngine::Get().OnSessionLost(); + return false; + } + + // Get the preferences + bool bMemoryCopyFirst = false; + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (HoudiniRuntimeSettings) + bMemoryCopyFirst = HoudiniRuntimeSettings->bPreferHdaMemoryCopyOverHdaSourceFile; + + // Get the HDA's file path, using the AssetImportData if we have it + FString AssetFileName = (HoudiniAsset->AssetImportData != nullptr) ? HoudiniAsset->AssetImportData->GetFirstFilename() : HoudiniAsset->GetAssetFileName(); + // We need to convert relative file path to absolute + if (FPaths::IsRelative(AssetFileName)) + AssetFileName = FPaths::ConvertRelativePathToFull(AssetFileName); + + // We need to modify the file name for expanded .hdas + FString FileExtension = FPaths::GetExtension(AssetFileName); + if (FileExtension.Compare(TEXT("hdalibrary"), ESearchCase::IgnoreCase) == 0) + { + // the .hda directory is what we should be loading + AssetFileName = FPaths::GetPath(AssetFileName); + } + + //Check whether we can Load from file/memory + bool bCanLoadFromMemory = (!HoudiniAsset->IsExpandedHDA() && HoudiniAsset->GetAssetBytesCount() > 0); + + // If the hda file exists, we can simply load it directly + bool bCanLoadFromFile = false; + if ( !AssetFileName.IsEmpty() ) + { + if (FPaths::FileExists(AssetFileName) + || (HoudiniAsset->IsExpandedHDA() && FPaths::DirectoryExists(AssetFileName))) + { + bCanLoadFromFile = true; + } + } + + HAPI_Result Result = HAPI_RESULT_FAILURE; + + // Lambda to detect license issues + auto CheckLicenseValid = [&AssetFileName](const HAPI_Result& Result) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::LoadHoudiniAsset - CheckLicenseValid); + + // HoudiniEngine acquires a license when creating/loading a node, not when creating a session + if (Result >= HAPI_RESULT_NO_LICENSE_FOUND && Result < HAPI_RESULT_ASSET_INVALID) + { + FString ErrorDesc = GetErrorDescription(Result); + HOUDINI_LOG_ERROR(TEXT("Error loading Asset %s: License failed: %s."), *AssetFileName, *ErrorDesc); + + // We must stop the session to prevent further attempts at loading an HDA + // as this could lead to unreal becoming stuck and unresponsive due to license timeout + FHoudiniEngine::Get().StopSession(); + + // Set the HE status to "no license" + FHoudiniEngine::Get().SetSessionStatus(EHoudiniSessionStatus::NoLicense); + + return false; + } + else + { + return true; + } + }; + + // Lambda to load an HDA from file + auto LoadAssetFromFile = [&Result, &OutAssetLibraryId](const FString& InAssetFileName) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::LoadHoudiniAsset - LoadAssetFromFile); + + // Load the asset from file. + std::string AssetFileNamePlain; + FHoudiniEngineUtils::ConvertUnrealString(InAssetFileName, AssetFileNamePlain); + Result = FHoudiniApi::LoadAssetLibraryFromFile( + FHoudiniEngine::Get().GetSession(), AssetFileNamePlain.c_str(), true, &OutAssetLibraryId); + + }; + + // Lambda to load an HDA from memory + auto LoadAssetFromMemory = [&Result, &OutAssetLibraryId](const UHoudiniAsset* InHoudiniAsset) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::LoadHoudiniAsset - LoadAssetFromMemory); + + // Load the asset from the cached memory buffer + Result = FHoudiniApi::LoadAssetLibraryFromMemory( + FHoudiniEngine::Get().GetSession(), + reinterpret_cast(InHoudiniAsset->GetAssetBytes()), + InHoudiniAsset->GetAssetBytesCount(), + true, + &OutAssetLibraryId); + }; + + if (!bMemoryCopyFirst) + { + // Load from File first + if (bCanLoadFromFile) + { + LoadAssetFromFile(AssetFileName); + + // Detect license issues when loading the HDA + if (!CheckLicenseValid(Result)) + return false; + } + + // If we failed to load from file ... + if (Result != HAPI_RESULT_SUCCESS) + { + // ... warn the user that we will be loading from memory. + HOUDINI_LOG_WARNING(TEXT("Asset %s, loading from Memory: source asset file not found."), *AssetFileName); + + // Attempt to load from memory + if (bCanLoadFromMemory) + { + LoadAssetFromMemory(HoudiniAsset); + + // Detect license issues when loading the HDA + if (!CheckLicenseValid(Result)) + return false; + } + else + { + HOUDINI_LOG_ERROR(TEXT("Error loading Asset %s: source asset file not found and no memory copy available."), *AssetFileName); + return false; + } + } + } + else + { + // Load from Memory first + if(bCanLoadFromMemory) + { + LoadAssetFromMemory(HoudiniAsset); + + // Detect license issues when loading the HDA + if (!CheckLicenseValid(Result)) + return false; + } + + // If we failed to load from memory ... + if (Result != HAPI_RESULT_SUCCESS) + { + // ... warn the user that we will be loading from file + HOUDINI_LOG_WARNING(TEXT("Asset %s, loading from File: no memory copy available."), *AssetFileName); + + // Attempt to load from file + if (bCanLoadFromFile) + { + LoadAssetFromFile(AssetFileName); + + // Detect license issues when loading the HDA + if (!CheckLicenseValid(Result)) + return false; + } + else + { + HOUDINI_LOG_ERROR(TEXT("Error loading Asset %s: source asset file not found and no memory copy available."), *AssetFileName); + return false; + } + } + } + + if (Result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_MESSAGE(TEXT("Error loading asset library for %s: %s"), *AssetFileName, *FHoudiniEngineUtils::GetErrorDescription()); + return false; + } + + return true; +} + +bool +FHoudiniEngineUtils::GetSubAssetNames( + const HAPI_AssetLibraryId& AssetLibraryId, + TArray< HAPI_StringHandle >& OutAssetNames) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GetSubAssetNames); + + if (AssetLibraryId < 0) + return false; + + int32 AssetCount = 0; + HAPI_Result Result = HAPI_RESULT_FAILURE; + Result = FHoudiniApi::GetAvailableAssetCount(FHoudiniEngine::Get().GetSession(), AssetLibraryId, &AssetCount); + if (Result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_ERROR(TEXT("Error getting asset count: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + return false; + } + + if (AssetCount <= 0) + { + HOUDINI_LOG_ERROR(TEXT("Could not find an asset.")); + return false; + } + + OutAssetNames.SetNum(AssetCount); + Result = FHoudiniApi::GetAvailableAssets(FHoudiniEngine::Get().GetSession(), AssetLibraryId, &OutAssetNames[0], AssetCount); + if (Result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_ERROR(TEXT("Unable to retrieve sub asset names: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + return false; + } + + if (!AssetCount) + { + HOUDINI_LOG_ERROR(TEXT("No assets found")); + return false; + } + + // Recipes show as subassets - and can't be instantiated by HAPI (even potentially crash?) + // So, get all the subasset names - and remove the recipes (::Data/) from the list + FString RecipeString = TEXT("::Data/"); + for (int32 n = OutAssetNames.Num() - 1; n >= 0; n--) + { + // Get the name string + FHoudiniEngineString HapiStr(OutAssetNames[n]); + FString AssetName; + HapiStr.ToFString(AssetName, FHoudiniEngine::Get().GetSession()); + + // If the HDA names matches the "recipes" substring - remove this subasset from the list to prevent its instantiation + if (AssetName.Contains(RecipeString)) + OutAssetNames.RemoveAt(n); + } + + return OutAssetNames.Num() > 0; +} + + +bool +FHoudiniEngineUtils::OpenSubassetSelectionWindow(TArray& AssetNames, HAPI_StringHandle& OutPickedAssetName ) +{ + OutPickedAssetName = -1; + + if (AssetNames.Num() <= 0) + return false; + + // Default to the first asset + OutPickedAssetName = AssetNames[0]; + +#if WITH_EDITOR + // Present the user with a dialog for choosing which asset to instantiate. + TSharedPtr ParentWindow; + if (FModuleManager::Get().IsModuleLoaded("MainFrame")) + { + // Check if the main frame is loaded. When using the old main frame it may not be. + IMainFrameModule& MainFrame = FModuleManager::LoadModuleChecked("MainFrame"); + ParentWindow = MainFrame.GetParentWindow(); + } + + if (!ParentWindow.IsValid()) + { + return false; + } + + TSharedPtr AssetSelectionWidget; + TSharedRef Window = SNew(SWindow) + .Title(LOCTEXT("WindowTitle", "Select an asset to instantiate")) + .ClientSize(FVector2D(640, 480)) + .SupportsMinimize(true) + .SupportsMaximize(true) + .HasCloseButton(true); + + Window->SetContent(SAssignNew(AssetSelectionWidget, SAssetSelectionWidget) + .WidgetWindow(Window) + .AvailableAssetNames(AssetNames)); + + if (!AssetSelectionWidget->IsValidWidget()) + { + return false; + } + + FSlateApplication::Get().AddModalWindow(Window, ParentWindow, false); + + int32 DialogPickedAssetName = AssetSelectionWidget->GetSelectedAssetName(); + if (DialogPickedAssetName != -1) + { + OutPickedAssetName = DialogPickedAssetName; + return true; + } + else + { + return false; + } +#endif + + return true; +} + +/* +bool +FHoudiniEngineUtils::IsValidNodeId(HAPI_NodeId NodeId) +{ + return NodeId != -1; +} +*/ + +bool +FHoudiniEngineUtils::GetHoudiniAssetName(HAPI_NodeId InNodeId, FString& NameString) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GetHoudiniAssetName); + + if (InNodeId < 0) + return false; + + HAPI_AssetInfo AssetInfo; + if (FHoudiniApi::GetAssetInfo(FHoudiniEngine::Get().GetSession(), InNodeId, &AssetInfo) == HAPI_RESULT_SUCCESS) + { + FHoudiniEngineString HoudiniEngineString(AssetInfo.nameSH); + return HoudiniEngineString.ToFString(NameString); + } + else + { + // If the node is not an asset, return the node name + HAPI_NodeInfo NodeInfo; + if (FHoudiniApi::GetNodeInfo(FHoudiniEngine::Get().GetSession(), InNodeId, &NodeInfo) == HAPI_RESULT_SUCCESS) + { + FHoudiniEngineString HoudiniEngineString(NodeInfo.nameSH); + return HoudiniEngineString.ToFString(NameString); + } + } + + return false; +} + +bool +FHoudiniEngineUtils::GetAssetPreset(HAPI_NodeId InNodeId, TArray& PresetBuffer) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GetAssetPreset); + PresetBuffer.Empty(); + + // See if param presets usage is disabled + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + bool bEnabled = HoudiniRuntimeSettings ? HoudiniRuntimeSettings->bUsePresetsForParameters : true; + if (!bEnabled) + return false; + + HAPI_NodeId NodeId; + HAPI_AssetInfo AssetInfo; + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetAssetInfo( + FHoudiniEngine::Get().GetSession(), InNodeId, &AssetInfo)) + { + NodeId = AssetInfo.nodeId; + } + else + NodeId = InNodeId; + + if (NodeId < 0) + return false; + + int32 BufferLength = 0; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetPresetBufLength( + FHoudiniEngine::Get().GetSession(), NodeId, + HAPI_PRESETTYPE_BINARY, NULL, &BufferLength), false); + + if (BufferLength <= 0) + return false; + + PresetBuffer.SetNumZeroed(BufferLength); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetPreset( + FHoudiniEngine::Get().GetSession(), NodeId, + (char*)(PresetBuffer.GetData()), PresetBuffer.Num()), false); + + return true; +} + + +bool +FHoudiniEngineUtils::SetAssetPreset(HAPI_NodeId InNodeId, const TArray& PresetBuffer) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::SetAssetPreset); + if (InNodeId < 0) + return false; + + // See if param presets usage is disabled + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + bool bEnabled = HoudiniRuntimeSettings ? HoudiniRuntimeSettings->bUsePresetsForParameters : true; + if (!bEnabled) + return false; + + // If we have stored parameter preset - restore them + HAPI_Result Res = FHoudiniApi::SetPreset( + FHoudiniEngine::Get().GetSession(), + InNodeId, + HAPI_PRESETTYPE_BINARY, + "hapi", + (char*)(PresetBuffer.GetData()), + PresetBuffer.Num()); + + if (Res != HAPI_RESULT_SUCCESS) + return false; + + return true; +} + +bool +FHoudiniEngineUtils::HapiGetAbsNodePath(HAPI_NodeId InNodeId, FString& OutPath) +{ + // Retrieve Path to the given Node, relative to the other given Node + if (InNodeId < 0) + return false; + + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(InNodeId)) + return false; + + HAPI_StringHandle StringHandle; + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetNodePath( + FHoudiniEngine::Get().GetSession(), + InNodeId, -1, &StringHandle)) + { + if(FHoudiniEngineString::ToFString(StringHandle, OutPath)) + { + return true; + } + } + return false; +} + + +bool +FHoudiniEngineUtils::HapiGetNodePath(HAPI_NodeId InNodeId, HAPI_NodeId InRelativeToNodeId, FString& OutPath) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::HapiGetNodePath); + + // Retrieve Path to the given Node, relative to the other given Node + if ((InNodeId < 0) || (InRelativeToNodeId < 0)) + return false; + + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(InNodeId)) + return false; + + HAPI_StringHandle StringHandle; + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetNodePath( + FHoudiniEngine::Get().GetSession(), + InNodeId, InRelativeToNodeId, &StringHandle)) + { + if(FHoudiniEngineString::ToFString(StringHandle, OutPath)) + { + return true; + } + } + return false; +} + +bool +FHoudiniEngineUtils::HapiGetNodePath(const FHoudiniGeoPartObject& InHGPO, FString& OutPath) +{ + // Do the HAPI query only on first-use + if (!InHGPO.NodePath.IsEmpty()) + return true; + + FString NodePathTemp; + if (InHGPO.AssetId == InHGPO.GeoId) + { + HAPI_NodeId NodeId = -1; + + // This is a SOP asset, just return the asset name in this case + HAPI_AssetInfo AssetInfo; + FHoudiniApi::AssetInfo_Init(&AssetInfo); + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetAssetInfo( + FHoudiniEngine::Get().GetSession(), InHGPO.AssetId, &AssetInfo)) + { + // Get the asset info node id + NodeId = AssetInfo.nodeId; + } + else + { + // Not an asset, just use the node id directly + NodeId = InHGPO.AssetId; + } + + HAPI_NodeInfo AssetNodeInfo; + FHoudiniApi::NodeInfo_Init(&AssetNodeInfo); + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), NodeId, &AssetNodeInfo)) + { + if (FHoudiniEngineString::ToFString(AssetNodeInfo.nameSH, NodePathTemp)) + { + OutPath = FString::Printf(TEXT("%s_%d"), *NodePathTemp, InHGPO.PartId); + } + } + } + else + { + // This is an OBJ asset, return the path to this geo relative to the asset + if (FHoudiniEngineUtils::HapiGetNodePath(InHGPO.GeoId, InHGPO.AssetId, NodePathTemp)) + { + OutPath = FString::Printf(TEXT("%s_%d"), *NodePathTemp, InHGPO.PartId); + } + } + + /*if (OutPath.IsEmpty()) + { + OutPath = TEXT("Empty"); + } + + return NodePath; + */ + + return !OutPath.IsEmpty(); +} + + +bool +FHoudiniEngineUtils::HapiGetNodeFromPath( + const FString& InPath, + const HAPI_NodeId InPathRelativeToId, + HAPI_NodeId& OutNodeId) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::HapiGetNodePath); + + if (InPath.IsEmpty()) + return false; + + // Invalid relative node id (-1 is fine as it means the path is absolute) + if (InPathRelativeToId >= 0 && !FHoudiniEngineUtils::IsHoudiniNodeValid(InPathRelativeToId)) + return false; + + // Fetch the actual texture node ID from the path + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetNodeFromPath( + FHoudiniEngine::Get().GetSession(), InPathRelativeToId, TCHAR_TO_ANSI(*InPath), &OutNodeId)) + { + return true; + } + + return false; +} + + +bool +FHoudiniEngineUtils::HapiGetObjectInfos( + HAPI_NodeId InNodeId, + TArray& OutObjectInfos, + TArray& OutObjectTransforms) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::HapiGetObjectInfos); + + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), + InNodeId, &NodeInfo), false); + + int32 ObjectCount = 0; + if (NodeInfo.type == HAPI_NODETYPE_SOP) + { + // Add one object info + ObjectCount = 1; + OutObjectInfos.SetNumUninitialized(1); + FHoudiniApi::ObjectInfo_Init(&(OutObjectInfos[0])); + + // Use the identity transform + OutObjectTransforms.SetNumUninitialized(1); + FHoudiniApi::Transform_Init(&(OutObjectTransforms[0])); + + OutObjectTransforms[0].rotationQuaternion[3] = 1.0f; + OutObjectTransforms[0].scale[0] = 1.0f; + OutObjectTransforms[0].scale[1] = 1.0f; + OutObjectTransforms[0].scale[2] = 1.0f; + OutObjectTransforms[0].rstOrder = HAPI_SRT; + + // Make sure our parent is an OBJ node + HAPI_NodeId ParentId = NodeInfo.parentId; + bool bParentIsObj = false; + while (!bParentIsObj && ParentId >= 0) + { + HAPI_NodeInfo ParentNodeInfo; + FHoudiniApi::NodeInfo_Init(&ParentNodeInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), + ParentId, &ParentNodeInfo), false); + + if (ParentNodeInfo.type == HAPI_NODETYPE_OBJ) + bParentIsObj = true; + else + ParentId = ParentNodeInfo.parentId; + } + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetObjectInfo( + FHoudiniEngine::Get().GetSession(), + ParentId, &OutObjectInfos[0]), false); + } + else if (NodeInfo.type == HAPI_NODETYPE_OBJ) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ComposeObjectList( + FHoudiniEngine::Get().GetSession(), InNodeId, nullptr, &ObjectCount), false); + + if (ObjectCount <= 0) + { + // This asset is an OBJ that has no object as children, use the object itself + ObjectCount = 1; + OutObjectInfos.SetNumUninitialized(1); + FHoudiniApi::ObjectInfo_Init(&(OutObjectInfos[0])); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetObjectInfo( + FHoudiniEngine::Get().GetSession(), InNodeId, + &OutObjectInfos[0]), false); + + // Use the identity transform + OutObjectTransforms.SetNumUninitialized(1); + FHoudiniApi::Transform_Init(&(OutObjectTransforms[0])); + + OutObjectTransforms[0].rotationQuaternion[3] = 1.0f; + OutObjectTransforms[0].scale[0] = 1.0f; + OutObjectTransforms[0].scale[1] = 1.0f; + OutObjectTransforms[0].scale[2] = 1.0f; + OutObjectTransforms[0].rstOrder = HAPI_SRT; + } + else + { + int32 ImmediateSOP = 0; + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::HapiGetObjectInfos-ComposeChildNodeList); + + // This OBJ has children + // See if we should add ourself by looking for immediate display SOP + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), NodeInfo.id, + HAPI_NODETYPE_SOP, HAPI_NODEFLAGS_DISPLAY, + false, &ImmediateSOP), false); + } + + bool bAddSelf = ImmediateSOP > 0; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ComposeObjectList( + FHoudiniEngine::Get().GetSession(), InNodeId, nullptr, &ObjectCount), false); + + // Increment the object count by one if we should add ourself + OutObjectInfos.SetNumUninitialized(bAddSelf ? ObjectCount + 1 : ObjectCount); + OutObjectTransforms.SetNumUninitialized(bAddSelf ? ObjectCount + 1 : ObjectCount); + for (int32 Idx = 0; Idx < OutObjectInfos.Num(); Idx++) + { + FHoudiniApi::ObjectInfo_Init(&(OutObjectInfos[Idx])); + FHoudiniApi::Transform_Init(&(OutObjectTransforms[Idx])); + } + + // Get our object info in 0 if needed + if (bAddSelf) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetObjectInfo( + FHoudiniEngine::Get().GetSession(), InNodeId, + &OutObjectInfos[0]), false); + + // Use the identity transform + OutObjectTransforms[0].rotationQuaternion[3] = 1.0f; + OutObjectTransforms[0].scale[0] = 1.0f; + OutObjectTransforms[0].scale[1] = 1.0f; + OutObjectTransforms[0].scale[2] = 1.0f; + OutObjectTransforms[0].rstOrder = HAPI_SRT; + } + + // Get the other object infos + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetComposedObjectList( + FHoudiniEngine::Get().GetSession(), InNodeId, + &OutObjectInfos[bAddSelf ? 1 : 0], 0, ObjectCount), false); + + // Get the composed object transforms for the others (1 - Count) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetComposedObjectTransforms( + FHoudiniEngine::Get().GetSession(), + InNodeId, HAPI_SRT, &OutObjectTransforms[bAddSelf ? 1 : 0], 0, ObjectCount), false); + } + } + else + return false; + + return true; +} + +bool +FHoudiniEngineUtils::IsObjNodeFullyVisible(const TSet& AllObjectIds, HAPI_NodeId InRootNodeId, HAPI_NodeId InChildNodeId) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::IsObjNodeFullyVisible); + + // Walk up the hierarchy from child to root. + // If any node in that hierarchy is not in the "AllObjectIds" set, the OBJ node is considered to + // be hidden. + + if (InChildNodeId == InRootNodeId) + return true; + + HAPI_NodeId ChildNodeId = InChildNodeId; + + HAPI_ObjectInfo ChildObjInfo; + HAPI_NodeInfo ChildNodeInfo; + + FHoudiniApi::ObjectInfo_Init(&ChildObjInfo); + FHoudiniApi::NodeInfo_Init(&ChildNodeInfo); + + do + { + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetObjectInfo( + FHoudiniEngine::Get().GetSession(), + ChildNodeId, &ChildObjInfo)) + { + // If can't get info for this object, we can't say whether it's visible (or not). + return false; + } + + if (!ChildObjInfo.isVisible || ChildObjInfo.nodeId < 0) + { + // We have an object in the chain that is not visible. Return false immediately! + return false; + } + + if (ChildNodeId != InChildNodeId) + { + // Only perform this check for 'parents' of the incoming child node + if ( !AllObjectIds.Contains(ChildNodeId)) + { + // There is a non-object node in the hierarchy between the child and asset root, rendering the + // child object node invisible. + return false; + } + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), + ChildNodeId, &ChildNodeInfo)) + { + // Could not retrieve node info. + return false; + } + + // Go up the hierarchy. + ChildNodeId = ChildNodeInfo.parentId; + } while (ChildNodeId != InRootNodeId && ChildNodeId >= 0); + + // We have traversed the whole hierarchy up to the root and nothing indicated that + // we _shouldn't_ be visible. + return true; +} + + +bool +FHoudiniEngineUtils::HapiGetNodeType(HAPI_NodeId InNodeId, HAPI_NodeType& OutNodeType) +{ + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), + InNodeId, + &NodeInfo + ), + false + ); + OutNodeType = NodeInfo.type; + return true; +} + + +bool +FHoudiniEngineUtils::IsSopNode(HAPI_NodeId NodeId) +{ + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), + NodeId, + &NodeInfo + ), + false + ); + return NodeInfo.type == HAPI_NODETYPE_SOP; +} + + +bool FHoudiniEngineUtils::ContainsSopNodes(HAPI_NodeId NodeId) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::ContainsSopNodes); + int ChildCount = 0; + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), + NodeId, + HAPI_NODETYPE_SOP, + HAPI_NODEFLAGS_NON_BYPASS, + false, + &ChildCount + ), + false + ); + return ChildCount > 0; +} + +bool FHoudiniEngineUtils::GetOutputIndex(HAPI_NodeId InNodeId, int32& OutOutputIndex) +{ + int TempValue = -1; + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetParmIntValue( + FHoudiniEngine::Get().GetSession(), + InNodeId, + TCHAR_TO_ANSI(TEXT("outputidx")), + 0, // index + &TempValue)) + { + OutOutputIndex = TempValue; + return true; + } + + return false; +} + +bool +FHoudiniEngineUtils::GatherAllAssetOutputs( + HAPI_NodeId AssetId, + bool bUseOutputNodes, + bool bOutputTemplatedGeos, + bool bGatherEditableCurves, + TArray& OutOutputNodes) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GatherAllAssetOutputs); + + OutOutputNodes.Empty(); + + // Ensure the asset has a valid node ID + if (AssetId < 0) + return false; + + // Get the AssetInfo + HAPI_AssetInfo AssetInfo; + bool bAssetInfoResult = false; + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GatherAllAssetOutputs-GetAssetInfo); + FHoudiniApi::AssetInfo_Init(&AssetInfo); + bAssetInfoResult = HAPI_RESULT_SUCCESS == FHoudiniApi::GetAssetInfo( + FHoudiniEngine::Get().GetSession(), AssetId, &AssetInfo); + } + + // Get the Asset NodeInfo + HAPI_NodeInfo AssetNodeInfo; + HAPI_Result NodeResult = HAPI_RESULT_FAILURE; + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GatherAllAssetOutputs-GetNodeInfo); + FHoudiniApi::NodeInfo_Init(&AssetNodeInfo); + NodeResult = FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), AssetId, &AssetNodeInfo); + } + + if (HAPI_RESULT_SUCCESS != NodeResult) + { + // Don't log invalid argument errors here + if (NodeResult != HAPI_RESULT_INVALID_ARGUMENT) + HOUDINI_CHECK_ERROR_RETURN(NodeResult, false); + else + return false; + } + + // We only handle SOP and OBJ nodes here. + if (AssetNodeInfo.type != HAPI_NODETYPE_SOP && AssetNodeInfo.type != HAPI_NODETYPE_OBJ) + return false; + + FString CurrentAssetName; + { + FHoudiniEngineString hapiSTR(bAssetInfoResult ? AssetInfo.nameSH : AssetNodeInfo.nameSH); + hapiSTR.ToFString(CurrentAssetName); + } + + // In certain cases, such as PDG output processing we might end up with a SOP node instead of a + // container. In that case, don't try to run child queries on this node. They will fail. + const bool bAssetHasChildren = !(AssetNodeInfo.type == HAPI_NODETYPE_SOP && AssetNodeInfo.childNodeCount == 0); + + // Retrieve information about each object contained within our asset. + TArray ObjectInfos; + TArray ObjectTransforms; + if (!FHoudiniEngineUtils::HapiGetObjectInfos(AssetId, ObjectInfos, ObjectTransforms)) + return false; + + // + // LEGACY WORKFLOW - Editable Curves + // + // Find the editable nodes in the asset if the EnableEditableCurve option is enabled + TArray EditableGeoInfos; + if (bGatherEditableCurves) + { + int32 EditableNodeCount = 0; + if (bAssetHasChildren) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GatherAllAssetOutputs - ComposeChildNodeList); + HOUDINI_CHECK_ERROR(FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), + AssetId, HAPI_NODETYPE_SOP, HAPI_NODEFLAGS_EDITABLE | HAPI_NODEFLAGS_NON_BYPASS, + true, &EditableNodeCount)); + } + + // All editable nodes will be output, regardless + // of whether the subnet is considered visible or not. + if (EditableNodeCount > 0) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GatherAllAssetOutputs - GetComposedChildNodeList); + TArray EditableNodeIds; + EditableNodeIds.SetNumUninitialized(EditableNodeCount); + HOUDINI_CHECK_ERROR(FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), + AssetId, EditableNodeIds.GetData(), EditableNodeCount)); + + for (int32 nEditable = 0; nEditable < EditableNodeCount; nEditable++) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GatherAllAssetOutputs - GetEditableGeoInfo); + HAPI_GeoInfo CurrentEditableGeoInfo; + FHoudiniApi::GeoInfo_Init(&CurrentEditableGeoInfo); + HOUDINI_CHECK_ERROR(FHoudiniApi::GetGeoInfo( + FHoudiniEngine::Get().GetSession(), + EditableNodeIds[nEditable], &CurrentEditableGeoInfo)); + + // TODO: Check whether this display geo is actually being output + // Just because this is a display node doesn't mean that it will be output (it + // might be in a hidden subnet) + + // Do not process the main display geo twice! + if (CurrentEditableGeoInfo.isDisplayGeo) + continue; + + // We only handle editable curves for now + if (CurrentEditableGeoInfo.type != HAPI_GEOTYPE_CURVE || !bGatherEditableCurves) + continue; + + // Add this geo to the geo info array + EditableGeoInfos.Add(CurrentEditableGeoInfo); + } + } + } + + const bool bIsSopAsset = bAssetInfoResult ? (AssetInfo.nodeId != AssetInfo.objectNodeId) : AssetNodeInfo.type == HAPI_NODETYPE_SOP; + bool bUseOutputFromSubnets = true; + if (bAssetHasChildren) + { + if (FHoudiniEngineUtils::ContainsSopNodes(bAssetInfoResult ? AssetInfo.nodeId : AssetNodeInfo.id)) + { + // This HDA contains immediate SOP nodes. Don't look for subnets to output. + bUseOutputFromSubnets = false; + } + else + { + // Assume we're using a subnet-based HDA + bUseOutputFromSubnets = true; + } + } + else + { + // This asset doesn't have any children. Don't try to find subnets. + bUseOutputFromSubnets = false; + } + + // Before we can perform visibility checks on the Object nodes, we have + // to build a set of all the Object node ids. The 'AllObjectIds' act + // as a visibility filter. If an Object node is not present in this + // list, the content of that node will not be displayed (display / output / templated nodes). + // NOTE that if the HDA contains immediate SOP nodes we will ignore + // all subnets and only use the data outputs directly from the HDA. + + TSet AllObjectIds; + if (bUseOutputFromSubnets) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GatherAllAssetOutputs-GetComposedChildNodeList2); + int NumObjSubnets; + TArray ObjectIds; + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), + AssetId, + HAPI_NODETYPE_OBJ, + HAPI_NODEFLAGS_OBJ_SUBNET | HAPI_NODEFLAGS_NON_BYPASS, + true, + &NumObjSubnets + ), + false); + + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GatherAllAssetOutputs-GetComposedChildNodeList2); + ObjectIds.SetNumUninitialized(NumObjSubnets); + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), + AssetId, + ObjectIds.GetData(), + NumObjSubnets + ), + false); + AllObjectIds.Append(ObjectIds); + } + else + { + AllObjectIds.Add(AssetInfo.objectNodeId); + } + + // Iterate through all objects to determine visibility and + // gather output nodes that needs to be cooked. + int32 OutputIdx = 1; + for (int32 ObjectIdx = 0; ObjectIdx < ObjectInfos.Num(); ObjectIdx++) + { + // Retrieve the object info + const HAPI_ObjectInfo& CurrentHapiObjectInfo = ObjectInfos[ObjectIdx]; + + // Determine whether this object node is fully visible. + bool bObjectIsVisible = false; + HAPI_NodeId GatherOutputsNodeId = -1; // Outputs will be gathered from this node. + if (!bAssetHasChildren) + { + // If the asset doesn't have children, we have to gather outputs from the asset's parent in order to output + // this asset node + bObjectIsVisible = true; + GatherOutputsNodeId = AssetNodeInfo.parentId; + } + else if (bIsSopAsset && CurrentHapiObjectInfo.nodeId == AssetInfo.objectNodeId) + { + // When dealing with a SOP asset, be sure to gather outputs from the SOP node, not the + // outer object node. + bObjectIsVisible = true; + GatherOutputsNodeId = AssetInfo.nodeId; + } + else + { + bObjectIsVisible = FHoudiniEngineUtils::IsObjNodeFullyVisible(AllObjectIds, AssetId, CurrentHapiObjectInfo.nodeId); + GatherOutputsNodeId = CurrentHapiObjectInfo.nodeId; + } + + // Build an array of the geos we'll need to process + // In most case, it will only be the display geo, + // but we may also want to process editable geos as well + TArray GeoInfos; + + // These node ids may need to be cooked in order to extract part counts. + TSet ForceNodesToCook; + + // Append the initial set of editable geo infos here + // then clear the editable geo infos array since we + // only want to process them once. + GeoInfos.Append(EditableGeoInfos); + EditableGeoInfos.Empty(); + + if (bObjectIsVisible) + { + // NOTE: The HAPI_GetDisplayGeoInfo will not always return the expected Geometry subnet's + // Display flag geometry. If the Geometry subnet contains an Object subnet somewhere, the + // GetDisplayGeoInfo will sometimes fetch the display SOP from within the subnet which is + // not what we want. + + // Resolve and gather outputs (display / output / template nodes) from the GatherOutputsNodeId. + FHoudiniEngineUtils::GatherImmediateOutputGeoInfos(GatherOutputsNodeId, + bUseOutputNodes, + bOutputTemplatedGeos, + GeoInfos, + ForceNodesToCook); + + } // if (bObjectIsVisible) + + for (HAPI_NodeId NodeId : ForceNodesToCook) + { + OutOutputNodes.AddUnique(NodeId); + } + } + return true; +} + +bool FHoudiniEngineUtils::GatherImmediateOutputGeoInfos(HAPI_NodeId InNodeId, + const bool bUseOutputNodes, + const bool bGatherTemplateNodes, + TArray& OutGeoInfos, + TSet& OutForceNodesCook +) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GatherImmediateOutputGeoInfos); + + TSet GatheredNodeIds; + + // NOTE: This function assumes that the incoming node is a Geometry container that contains immediate + // outputs / display nodes / template nodes. + + // First we look for (immediate) output nodes (if bUseOutputNodes have been enabled). + // If we didn't find an output node, we'll look for a display node. + + bool bHasOutputs = false; + if (bUseOutputNodes) + { + int NumOutputs = -1; + FHoudiniApi::GetOutputGeoCount( + FHoudiniEngine::Get().GetSession(), + InNodeId, + &NumOutputs + ); + + if (NumOutputs > 0) + { + bHasOutputs = true; + + // ------------------------------------------------- + // Extract GeoInfo from the immediate output nodes. + // ------------------------------------------------- + TArray OutputGeoInfos; + OutputGeoInfos.SetNumUninitialized(NumOutputs); + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetOutputGeoInfos( + FHoudiniEngine::Get().GetSession(), + InNodeId, + OutputGeoInfos.GetData(), + NumOutputs)) + { + // Gather all the output nodes + for (HAPI_GeoInfo& OutputGeoInfo : OutputGeoInfos) + { + // This geo should be output. Be sure to ignore any template flags. + OutputGeoInfo.isTemplated = false; + OutGeoInfos.Add(OutputGeoInfo); + GatheredNodeIds.Add(OutputGeoInfo.nodeId); + OutForceNodesCook.Add(OutputGeoInfo.nodeId); // Ensure this output node gets cooked + } + } + } + } + + if (!bHasOutputs) + { + // If we didn't get any output data, pull our output data directly from the Display node. + + // --------------------------------- + // Look for display nodes. + // --------------------------------- + int DisplayNodeCount = 0; + if (HAPI_RESULT_SUCCESS == FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), + InNodeId, + HAPI_NODETYPE_SOP, + HAPI_NODEFLAGS_DISPLAY, + false, + &DisplayNodeCount + )) + { + if (DisplayNodeCount > 0) + { + // Retrieve all the display node ids + TArray DisplayNodeIds; + DisplayNodeIds.SetNumUninitialized(DisplayNodeCount); + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), + InNodeId, + DisplayNodeIds.GetData(), + DisplayNodeCount + )) + { + HAPI_GeoInfo GeoInfo; + FHoudiniApi::GeoInfo_Init(&GeoInfo); + // Retrieve the Geo Infos for each display node + for(HAPI_NodeId DisplayNodeId : DisplayNodeIds) + { + if (GatheredNodeIds.Contains(DisplayNodeId)) + continue; // This node has already been gathered from this subnet. + + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetGeoInfo( + FHoudiniEngine::Get().GetSession(), + DisplayNodeId, + &GeoInfo) + ) + { + // This geo should be output. Be sure to ignore any templated flags. + GeoInfo.isTemplated = false; + OutGeoInfos.Add(GeoInfo); + GatheredNodeIds.Add(DisplayNodeId); + // If this node doesn't have a part_id count, ensure it gets cooked. + OutForceNodesCook.Add(DisplayNodeId); + } + } + } + } // if (DisplayNodeCount > 0) + } + } // if (!HasOutputNode) + + // Gather templated nodes. + if (bGatherTemplateNodes) + { + int NumTemplateNodes = 0; + // Gather all template nodes + if (HAPI_RESULT_SUCCESS == FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), + InNodeId, + HAPI_NODETYPE_SOP, HAPI_NODEFLAGS_TEMPLATED, + false, + &NumTemplateNodes + )) + { + TArray TemplateNodeIds; + TemplateNodeIds.SetNumUninitialized(NumTemplateNodes); + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), + InNodeId, + TemplateNodeIds.GetData(), + NumTemplateNodes + )) + { + + for(HAPI_NodeId TemplateNodeId : TemplateNodeIds) + { + if (GatheredNodeIds.Contains(TemplateNodeId)) + { + continue; // This geometry has already been gathered. + } + + HAPI_GeoInfo GeoInfo; + FHoudiniApi::GeoInfo_Init(&GeoInfo); + FHoudiniApi::GetGeoInfo( + FHoudiniEngine::Get().GetSession(), + TemplateNodeId, + &GeoInfo + ); + // For some reason the return type is always "HAPI_RESULT_INVALID_ARGUMENT", so we + // just check the GeoInfo.type manually. + if (GeoInfo.type != HAPI_GEOTYPE_INVALID) + { + // Add this template node to the gathered outputs. + GatheredNodeIds.Add(TemplateNodeId); + OutGeoInfos.Add(GeoInfo); + // If this node doesn't have a part_id count, ensure it gets cooked. + OutForceNodesCook.Add(TemplateNodeId); + } + } + } + } + } + return true; +} + + +bool +FHoudiniEngineUtils::HapiGetAssetTransform(HAPI_NodeId InNodeId, FTransform& OutTransform) +{ + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), InNodeId, + &NodeInfo), false); + + HAPI_Transform HapiTransform; + FHoudiniApi::Transform_Init(&HapiTransform); + + if (NodeInfo.type == HAPI_NODETYPE_SOP) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetObjectTransform( + FHoudiniEngine::Get().GetSession(), + NodeInfo.parentId, -1, HAPI_SRT, &HapiTransform), false); + } + else if (NodeInfo.type == HAPI_NODETYPE_OBJ) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetObjectTransform( + FHoudiniEngine::Get().GetSession(), + InNodeId, -1, HAPI_SRT, &HapiTransform), false); + } + else + return false; + + // Convert HAPI transform to Unreal one. + FHoudiniEngineUtils::TranslateHapiTransform(HapiTransform, OutTransform); + + return true; +} + +void +FHoudiniEngineUtils::TranslateHapiTransform(const HAPI_Transform& HapiTransform, FTransform& UnrealTransform) +{ + if ( HAPI_UNREAL_CONVERT_COORDINATE_SYSTEM ) + { + // Swap Y/Z, invert W + FQuat4d ObjectRotation( + (double)HapiTransform.rotationQuaternion[0], + (double)HapiTransform.rotationQuaternion[2], + (double)HapiTransform.rotationQuaternion[1], + (double)-HapiTransform.rotationQuaternion[3]); + + // Swap Y/Z and scale + FVector3d ObjectTranslation( + (double)HapiTransform.position[0], + (double)HapiTransform.position[2], + (double)HapiTransform.position[1]); + ObjectTranslation *= HAPI_UNREAL_SCALE_FACTOR_TRANSLATION; + + // Swap Y/Z + FVector3d ObjectScale3D( + (double)HapiTransform.scale[0], + (double)HapiTransform.scale[2], + (double)HapiTransform.scale[1]); + + UnrealTransform.SetComponents(ObjectRotation, ObjectTranslation, ObjectScale3D); + } + else + { + FQuat4d ObjectRotation( + (double)HapiTransform.rotationQuaternion[0], + (double)HapiTransform.rotationQuaternion[1], + (double)HapiTransform.rotationQuaternion[2], + (double)HapiTransform.rotationQuaternion[3]); + + FVector3d ObjectTranslation( + (double)HapiTransform.position[0], + (double)HapiTransform.position[1], + (double)HapiTransform.position[2]); + ObjectTranslation *= HAPI_UNREAL_SCALE_FACTOR_TRANSLATION; + + FVector3d ObjectScale3D( + (double)HapiTransform.scale[0], + (double)HapiTransform.scale[1], + (double)HapiTransform.scale[2]); + + UnrealTransform.SetComponents(ObjectRotation, ObjectTranslation, ObjectScale3D); + } +} + +void +FHoudiniEngineUtils::TranslateHapiTransform(const HAPI_TransformEuler& HapiTransformEuler, FTransform& UnrealTransform) +{ + float HapiMatrix[16]; + FHoudiniApi::ConvertTransformEulerToMatrix(FHoudiniEngine::Get().GetSession(), &HapiTransformEuler, HapiMatrix); + + HAPI_Transform HapiTransformQuat; + FMemory::Memzero< HAPI_Transform >(HapiTransformQuat); + FHoudiniApi::ConvertMatrixToQuat(FHoudiniEngine::Get().GetSession(), HapiMatrix, HAPI_SRT, &HapiTransformQuat); + + FHoudiniEngineUtils::TranslateHapiTransform(HapiTransformQuat, UnrealTransform); +} + +void +FHoudiniEngineUtils::TranslateUnrealTransform(const FTransform& UnrealTransform, HAPI_Transform& HapiTransform) +{ + FMemory::Memzero< HAPI_Transform >(HapiTransform); + HapiTransform.rstOrder = HAPI_SRT; + + FQuat4d UnrealRotation = UnrealTransform.GetRotation(); + FVector3d UnrealTranslation = UnrealTransform.GetTranslation(); + FVector3d UnrealScale = UnrealTransform.GetScale3D(); + + if (HAPI_UNREAL_CONVERT_COORDINATE_SYSTEM) + { + // Swap Y/Z, invert XYZ + HapiTransform.rotationQuaternion[0] = (float)-UnrealRotation.X; + HapiTransform.rotationQuaternion[1] = (float)-UnrealRotation.Z; + HapiTransform.rotationQuaternion[2] = (float)-UnrealRotation.Y; + HapiTransform.rotationQuaternion[3] = (float)UnrealRotation.W; + + // Swap Y/Z, scale + HapiTransform.position[0] = (float)UnrealTranslation.X / HAPI_UNREAL_SCALE_FACTOR_TRANSLATION; + HapiTransform.position[1] = (float)UnrealTranslation.Z / HAPI_UNREAL_SCALE_FACTOR_TRANSLATION; + HapiTransform.position[2] = (float)UnrealTranslation.Y / HAPI_UNREAL_SCALE_FACTOR_TRANSLATION; + + // Swap Y/Z + HapiTransform.scale[0] = (float)UnrealScale.X; + HapiTransform.scale[1] = (float)UnrealScale.Z; + HapiTransform.scale[2] = (float)UnrealScale.Y; + } + else + { + HapiTransform.rotationQuaternion[0] = (float)UnrealRotation.X; + HapiTransform.rotationQuaternion[1] = (float)UnrealRotation.Y; + HapiTransform.rotationQuaternion[2] = (float)UnrealRotation.Z; + HapiTransform.rotationQuaternion[3] = (float)UnrealRotation.W; + + HapiTransform.position[0] = (float)UnrealTranslation.X; + HapiTransform.position[1] = (float)UnrealTranslation.Y; + HapiTransform.position[2] = (float)UnrealTranslation.Z; + + HapiTransform.scale[0] = (float)UnrealScale.X; + HapiTransform.scale[1] = (float)UnrealScale.Y; + HapiTransform.scale[2] = (float)UnrealScale.Z; + } +} + +void +FHoudiniEngineUtils::TranslateUnrealTransform( + const FTransform& UnrealTransform, + HAPI_TransformEuler& HapiTransformEuler) +{ + FHoudiniApi::TransformEuler_Init(&HapiTransformEuler); + + HapiTransformEuler.rstOrder = HAPI_SRT; + HapiTransformEuler.rotationOrder = HAPI_XYZ; + + FQuat UnrealRotation = UnrealTransform.GetRotation(); + FVector UnrealTranslation = UnrealTransform.GetTranslation(); + FVector UnrealScale = UnrealTransform.GetScale3D(); + + if (HAPI_UNREAL_CONVERT_COORDINATE_SYSTEM) + { + // switch the quaternion to Y-up, LHR by Swapping Y/Z and negating W + Swap(UnrealRotation.Y, UnrealRotation.Z); + UnrealRotation.W = -UnrealRotation.W; + const FRotator Rotator = UnrealRotation.Rotator(); + + // Negate roll and pitch since they are actually RHR + HapiTransformEuler.rotationEuler[0] = -(float)Rotator.Roll; + HapiTransformEuler.rotationEuler[1] = -(float)Rotator.Pitch; + HapiTransformEuler.rotationEuler[2] = (float)Rotator.Yaw; + + // Swap Y/Z, scale + HapiTransformEuler.position[0] = (float)(UnrealTranslation.X) / HAPI_UNREAL_SCALE_FACTOR_TRANSLATION; + HapiTransformEuler.position[1] = (float)(UnrealTranslation.Z) / HAPI_UNREAL_SCALE_FACTOR_TRANSLATION; + HapiTransformEuler.position[2] = (float)(UnrealTranslation.Y) / HAPI_UNREAL_SCALE_FACTOR_TRANSLATION; + + // Swap Y/Z + HapiTransformEuler.scale[0] = (float)UnrealScale.X; + HapiTransformEuler.scale[1] = (float)UnrealScale.Z; + HapiTransformEuler.scale[2] = (float)UnrealScale.Y; + } + else + { + const FRotator Rotator = UnrealRotation.Rotator(); + HapiTransformEuler.rotationEuler[0] = (float)Rotator.Roll; + HapiTransformEuler.rotationEuler[1] = (float)Rotator.Yaw; + HapiTransformEuler.rotationEuler[2] = (float)Rotator.Pitch; + + HapiTransformEuler.position[0] = (float)UnrealTranslation.X; + HapiTransformEuler.position[1] = (float)UnrealTranslation.Y; + HapiTransformEuler.position[2] = (float)UnrealTranslation.Z; + + HapiTransformEuler.scale[0] = (float)UnrealScale.X; + HapiTransformEuler.scale[1] = (float)UnrealScale.Y; + HapiTransformEuler.scale[2] = (float)UnrealScale.Z; + } +} + +void +FHoudiniEngineUtils::ConvertHoudiniPositionToUnrealVector(const TArray& InRawData, TArray& OutVectorData) +{ + OutVectorData.SetNum(InRawData.Num() / 3); + + for (int32 OutIndex = 0; OutIndex < OutVectorData.Num(); OutIndex++) + { + int32 InIndex = OutIndex * 3; + + // Swap Y/Z and scale meters to centimeters + OutVectorData[OutIndex].X = (double)(InRawData[InIndex + 0] * HAPI_UNREAL_SCALE_FACTOR_POSITION); + OutVectorData[OutIndex].Y = (double)(InRawData[InIndex + 2] * HAPI_UNREAL_SCALE_FACTOR_POSITION); + OutVectorData[OutIndex].Z = (double)(InRawData[InIndex + 1] * HAPI_UNREAL_SCALE_FACTOR_POSITION); + } +} + +FVector3f +FHoudiniEngineUtils::ConvertHoudiniPositionToUnrealVector3f(const FVector3f& InVector) +{ + FVector3f ConvertedPoint; + ConvertedPoint.X = InVector.X * HAPI_UNREAL_SCALE_FACTOR_POSITION; + ConvertedPoint.Y = InVector.Z * HAPI_UNREAL_SCALE_FACTOR_POSITION; + ConvertedPoint.Z = InVector.Y * HAPI_UNREAL_SCALE_FACTOR_POSITION; + return ConvertedPoint; +} + + +void +FHoudiniEngineUtils::ConvertHoudiniScaleToUnrealVector(const TArray& InRawData, TArray& OutVectorData) +{ + OutVectorData.SetNum(InRawData.Num() / 3); + + for (int32 OutIndex = 0; OutIndex < OutVectorData.Num(); OutIndex++) + { + int32 InIndex = OutIndex * 3; + + // Just swap Y/Z + OutVectorData[OutIndex].X = (double)InRawData[InIndex + 0]; + OutVectorData[OutIndex].Y = (double)InRawData[InIndex + 2]; + OutVectorData[OutIndex].Z = (double)InRawData[InIndex + 1]; + } +} + +void +FHoudiniEngineUtils::ConvertHoudiniRotQuatToUnrealVector(const TArray& InRawData, TArray& OutVectorData) +{ + OutVectorData.SetNum(InRawData.Num() / 4); + + for (int32 OutIndex = 0; OutIndex < OutVectorData.Num(); OutIndex++) + { + int32 InIndex = OutIndex * 4; + + // Extract a quaternion: Swap Y/Z, invert W + FQuat ObjectRotation( + (double)InRawData[InIndex + 0], + (double)InRawData[InIndex + 2], + (double)InRawData[InIndex + 1], + (double)-InRawData[InIndex + 3]); + + // Get Euler angles + OutVectorData[OutIndex] = ObjectRotation.Euler(); + } +} + +void +FHoudiniEngineUtils::ConvertHoudiniRotEulerToUnrealVector(const TArray& InRawData, TArray& OutVectorData) +{ + OutVectorData.SetNum(InRawData.Num() / 3); + + for (int32 OutIndex = 0; OutIndex < OutVectorData.Num(); OutIndex++) + { + int32 InIndex = OutIndex * 3; + + // Just swap Y/Z + OutVectorData[OutIndex].X = (double)InRawData[InIndex + 0]; + OutVectorData[OutIndex].Y = (double)InRawData[InIndex + 2]; + OutVectorData[OutIndex].Z = (double)InRawData[InIndex + 1]; + } +} + +bool +FHoudiniEngineUtils::UploadCookableTransform(UHoudiniCookable* HC) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::UploadCookableTransform); + + if (!HC || !HC->IsComponentSupported()) + return false; + + if (!HC->ComponentData->bUploadTransformsToHoudiniEngine) + return false; + + if (!IsValid(HC->ComponentData->Component.Get())) + return false; + + // Indicates the Cookable has been fully loaded + if (!HC->IsFullyLoaded()) + return false; + + if (HC->CookCount > 0 && HC->GetNodeId() >= 0) + { + if (!FHoudiniEngineUtils::HapiSetAssetTransform(HC->GetNodeId(), HC->ComponentData->Component->GetComponentTransform())) + return false; + } + + HC->SetHasComponentTransformChanged(false); + + return true; +} + + +bool +FHoudiniEngineUtils::HapiSetAssetTransform(HAPI_NodeId AssetId, const FTransform & Transform) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::HapiSetAssetTransform); + if (AssetId < 0) + return false; + + // Translate Unreal transform to HAPI Euler one. + HAPI_TransformEuler TransformEuler; + FMemory::Memzero< HAPI_TransformEuler >(TransformEuler); + FHoudiniEngineUtils::TranslateUnrealTransform(Transform, TransformEuler); + + // Get the NodeInfo + HAPI_NodeInfo LocalAssetNodeInfo; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), AssetId, + &LocalAssetNodeInfo), false); + + if (LocalAssetNodeInfo.type == HAPI_NODETYPE_SOP) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + FHoudiniEngine::Get().GetSession(), + LocalAssetNodeInfo.parentId, + &TransformEuler), false); + } + else if (LocalAssetNodeInfo.type == HAPI_NODETYPE_OBJ) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + FHoudiniEngine::Get().GetSession(), + AssetId, &TransformEuler), false); + } + else + return false; + + return true; +} + +HAPI_NodeId +FHoudiniEngineUtils::HapiGetParentNodeId(HAPI_NodeId NodeId) +{ + HAPI_NodeId ParentId = -1; + if (NodeId >= 0) + { + HAPI_NodeInfo NodeInfo; + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetNodeInfo(FHoudiniEngine::Get().GetSession(), NodeId, &NodeInfo)) + ParentId = NodeInfo.parentId; + } + + return ParentId; +} + + +// Assign a unique Actor Label if needed +void +FHoudiniEngineUtils::AssignUniqueActorLabelIfNeeded(HAPI_NodeId InNodeId, AActor* InActorOwner) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::AssignUniqueActorLabelIfNeeded); + +#if WITH_EDITOR + if (InNodeId < 0) + return; + + if (!InActorOwner) + return; + + // Make sure we only create a unique name for a new Houdini Actor + // We don't want to loose custom/manual names + if (!InActorOwner->GetActorNameOrLabel().StartsWith(AHoudiniAssetActor::StaticClass()->GetName())) + return; + + if (!InActorOwner->GetName().StartsWith(AHoudiniAssetActor::StaticClass()->GetName())) + return; + + // Assign unique actor label based on asset name if it seems to have not been renamed already + FString UniqueName; + if (FHoudiniEngineUtils::GetHoudiniAssetName(InNodeId, UniqueName)) + FActorLabelUtilities::SetActorLabelUnique(InActorOwner, UniqueName); +#endif +} + +bool +FHoudiniEngineUtils::GetLicenseType(FString & LicenseType) +{ + LicenseType = TEXT(""); + HAPI_License LicenseTypeValue = HAPI_LICENSE_NONE; + + if (FHoudiniEngine::Get().GetSession()) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetSessionEnvInt( + FHoudiniEngine::Get().GetSession(), HAPI_SESSIONENVINT_LICENSE, + (int32*)&LicenseTypeValue), false); + } + + switch (LicenseTypeValue) + { + case HAPI_LICENSE_NONE: + { + LicenseType = TEXT("No License Acquired"); + break; + } + + case HAPI_LICENSE_HOUDINI_ENGINE: + { + LicenseType = TEXT("Houdini Engine"); + break; + } + + case HAPI_LICENSE_HOUDINI: + { + LicenseType = TEXT("Houdini"); + break; + } + + case HAPI_LICENSE_HOUDINI_FX: + { + LicenseType = TEXT("Houdini FX"); + break; + } + + case HAPI_LICENSE_HOUDINI_ENGINE_INDIE: + { + LicenseType = TEXT("Houdini Engine Indie"); + break; + } + + case HAPI_LICENSE_HOUDINI_INDIE: + { + LicenseType = TEXT("Houdini Indie"); + break; + } + + case HAPI_LICENSE_HOUDINI_ENGINE_UNITY_UNREAL: + { + LicenseType = TEXT("Houdini Engine for Unity/Unreal"); + break; + } + + case HAPI_LICENSE_HOUDINI_EDUCATION: + { + LicenseType = TEXT("Houdini Education"); + break; + } + + case HAPI_LICENSE_HOUDINI_ENGINE_EDUCATION: + { + LicenseType = TEXT("Houdini Engine Education"); + break; + } + + case HAPI_LICENSE_MAX: + default: + { + return false; + } + } + + return true; +} + +// Check if the cookable (or parent cookable) is being cooked +bool +FHoudiniEngineUtils::IsHoudiniCookableCooking(UObject* InObj) +{ + if (!InObj) + return false; + + UHoudiniCookable* Cookable = nullptr; + if (InObj->IsA()) + { + Cookable = Cast(InObj); + } + else + { + Cookable = Cast(InObj->GetOuter()); + } + + if (!Cookable) + return false; + + EHoudiniAssetState AssetState = Cookable->GetCurrentState(); + return AssetState >= EHoudiniAssetState::PreCook && AssetState <= EHoudiniAssetState::PostCook; +} + +void +FHoudiniEngineUtils::UpdateEditorProperties(const bool bInForceFullUpdate) +{ + if (!IsInGameThread()) + { + // We need to be in the game thread to trigger editor properties update + AsyncTask(ENamedThreads::GameThread, [bInForceFullUpdate]() + { + FHoudiniEngineUtils::UpdateEditorProperties_Internal(bInForceFullUpdate); + }); + } + else + { + // We're in the game thread, no need for an async task + FHoudiniEngineUtils::UpdateEditorProperties_Internal(bInForceFullUpdate); + } +} + +void FHoudiniEngineUtils::UpdateBlueprintEditor(UHoudiniAssetComponent* HAC) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::UpdateBlueprintEditor); + + if (!IsInGameThread()) + { + // We need to be in the game thread to trigger editor properties update + AsyncTask(ENamedThreads::GameThread, [HAC]() + { + FHoudiniEngineUtils::UpdateBlueprintEditor_Internal(HAC); + }); + } + else + { + // We're in the game thread, no need for an async task + FHoudiniEngineUtils::UpdateBlueprintEditor_Internal(HAC); + } +} + +void +FHoudiniEngineUtils::UpdateEditorProperties_Internal(const bool bInForceFullUpdate) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::UpdateEditorProperties_Internal); + +#if WITH_EDITOR +#define HOUDINI_USE_DETAILS_FOCUS_HACK 1 + + // TODO: As an optimization, it might be worth adding an extra parameter to control if we + // update floating property windows. We need to do this whenever we update something visible in + // actor details, such as adding/removing a component. + if (GUnrealEd) + { + GUnrealEd->UpdateFloatingPropertyWindows(); + } + + if (!bInForceFullUpdate) + { + // bNeedFullUpdate is false only when small changes (parameters value) have been made + // We do not refresh the details view to avoid loosing the currently selected parameter + return; + } + + // Get the property editor module + FPropertyEditorModule& PropertyModule = + FModuleManager::Get().GetModuleChecked("PropertyEditor"); + + // + // We want to iterate over all the details panels. + // Note that Unreal can have up to 4 of them open at once! + // + // TODO: These shouldn't be hardcoded strings, but when building on Mac, we get linking errors + // when trying to use LevelEditorTabIds::LevelEditorSelectionDetails + // + /* + static const FName DetailsTabIdentifiers[] = { + "LevelEditorSelectionDetails", + "LevelEditorSelectionDetails2", + "LevelEditorSelectionDetails3", + "LevelEditorSelectionDetails4" }; + */ + + TArray DetailsTabIdentifiers; + DetailsTabIdentifiers.SetNum(4); + DetailsTabIdentifiers[0] = FName("LevelEditorSelectionDetails"); + DetailsTabIdentifiers[1] = FName("LevelEditorSelectionDetails2"); + DetailsTabIdentifiers[2] = FName("LevelEditorSelectionDetails3"); + DetailsTabIdentifiers[3] = FName("LevelEditorSelectionDetails4"); + + // Add the Houdini Asset editor identifiers to the Details tab array + { + TArray AssetEditorId = FHoudiniEngine::Get().GetAllHoudiniAssetEditorIdentifier(); + for (auto CurId : AssetEditorId) + DetailsTabIdentifiers.Add(CurId); + } + + for (const FName DetailsPanelName : DetailsTabIdentifiers) + { + // Locate the details panel. + TSharedPtr DetailsView = PropertyModule.FindDetailView(DetailsPanelName); + + if (!DetailsView.IsValid()) + { + // We have no details panel, nothing to update. + continue; + } + +#if HOUDINI_USE_DETAILS_FOCUS_HACK + // + // Unreal does not maintain focus on the currently focused widget after refreshing the + // details view. Since we are constantly refreshing the details view when tweaking + // parameters, users cannot navigate the UI via keyboard. + // + // HACK: Attach meta data to parameter widgets to make them identifiable. Before triggering + // a refresh, save the meta data of the currently focused widget. Then restore focus + // on the newly created widget using this meta data. + // + + TSharedPtr ParameterWidgetMetaData = + GetFocusedParameterWidgetMetaData(DetailsView); +#endif // HOUDINI_USE_DETAILS_FOCUS_HACK + + DetailsView->ForceRefresh(); + +#if HOUDINI_USE_DETAILS_FOCUS_HACK + if (ParameterWidgetMetaData.IsValid()) + { + FocusUsingParameterWidgetMetaData( + DetailsView.ToSharedRef(), + *ParameterWidgetMetaData); + } +#endif // HOUDINI_USE_DETAILS_FOCUS_HACK + } +#endif // WITH_EDITOR +} + +TSharedPtr +FHoudiniEngineUtils::GetFocusedParameterWidgetMetaData(TSharedPtr DetailsView) +{ +#if WITH_EDITOR + if (!DetailsView.IsValid()) + { + return nullptr; + } + + TSharedPtr FocusedWidget = FSlateApplication::Get().GetKeyboardFocusedWidget(); + if (FocusedWidget.IsValid()) + { + // Before we grab the meta data of the focused widget, we want to make sure that it is + // inside our details view. To do this, check if any of its ancestors are the current + // details view. + for (auto Widget = FocusedWidget; Widget.IsValid(); Widget = Widget->GetParentWidget()) + { + if (Widget == DetailsView) + { + return FocusedWidget->GetMetaData(); + } + } + } +#endif // WITH_EDITOR + + return nullptr; +} + +bool +FHoudiniEngineUtils::FocusUsingParameterWidgetMetaData( + TSharedRef AncestorWidget, + const FHoudiniParameterWidgetMetaData& ParameterWidgetMetaData) +{ + // No need to keyboard select / focus spinboxes + // As this will instead select the TextEdit... + if (ParameterWidgetMetaData.UniqueName.Contains("SSpinBox")) + return true; + +#if WITH_EDITOR + // + // HACK: Manually tick the widget before accessing its children. We need to do this because + // refreshing a details view will only mark the child detail tree as dirty, without + // actually adding the newly created widgets as children. + // + // - See SDetailsViewBase::RefreshTree which requests the refresh. + // - See STableViewBase::Tick which actually does the refresh. + // + // As a result, Slate cannot construct a path to the new widgets we wish to focus, since + // before the tick, the widgets in our detail rows do not have a parent. + // + // Unfortunately there doesn't seem to be a way to subscribe to a "post tick" event in + // Slate, so we resort to manually ticking these widgets. + // + // We could also manually tick the entire Slate application, however we cannot control the + // delta time this way and this introduces a delay before the new widget is re-focused. + // + // We use cached widget geometry with the hope that it is correct. + // + AncestorWidget->Tick(AncestorWidget->GetTickSpaceGeometry(), 0.f, 0.f); + + // Important: We use GetAllChildren and not GetChildren. + // Widgets might choose to not expose some of their children via GetChildren. + FChildren* const Children = AncestorWidget->GetAllChildren(); + + for (int32 i = 0; i < Children->Num(); ++i) + { + const TSharedRef Child = Children->GetChildAt(i); + const TSharedPtr ChildMetaData = Child->GetMetaData(); + + if (ChildMetaData.IsValid() && ParameterWidgetMetaData == *ChildMetaData) + { + TSharedPtr WidgetToSelect = Child; + + // + // Try focus the desired widget. + // - If this fails, it is possible that Slate cannot construct a path to it. + // - However, usually the parent can be focused. + // - Thus we go over all ancestors to try focus them. + // + // TODO: Explore the possibility of constructing the path to the widget manually. + // Maybe this would allow focusing widgets that currently cannot not be focused. + // + while (WidgetToSelect.IsValid()) + { + /* + // No need to keyboard select spinboxes + if (WidgetToSelect->GetTypeAsString().StartsWith("SSpinBox")) + return true; + */ + + if (FSlateApplication::Get().SetKeyboardFocus(WidgetToSelect)) + return true; + + WidgetToSelect = Child->GetParentWidget(); + } + + return false; // Failed to reselect keyboard focused widget! + + } + + if (FocusUsingParameterWidgetMetaData(Child, ParameterWidgetMetaData)) + { + return true; + } + } +#endif // WITH_EDITOR + + return false; +} + +void +FHoudiniEngineUtils::UpdateBlueprintEditor_Internal(UHoudiniAssetComponent* HAC) +{ + FBlueprintEditor* BlueprintEditor = FHoudiniEngineRuntimeUtils::GetBlueprintEditor(HAC); + if (!BlueprintEditor) + return; + + TSharedPtr SSubObjEditor = BlueprintEditor->GetSubobjectEditor(); + if (SSubObjEditor.IsValid()) + { + SSubObjEditor->UpdateTree(true); + SSubObjEditor->DumpTree(); + } + BlueprintEditor->RefreshMyBlueprint(); + + //BlueprintEditor->RefreshMyBlueprint(); + //BlueprintEditor->RefreshInspector(); + //BlueprintEditor->RefreshEditors(); + + // Also somehow reselect ? +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeFloatData( + const TArray& InFloatData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo, + bool bAttemptRunLengthEncoding) +{ + if (InFloatData.Num() != InAttributeInfo.count * InAttributeInfo.tupleSize) + return HAPI_RESULT_INVALID_ARGUMENT; + + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName)); + bool bSuccess = Accessor.SetAttributeData(InAttributeInfo, InFloatData); + + return bSuccess ? HAPI_RESULT_SUCCESS : HAPI_RESULT_FAILURE; +} + + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeFloatData( + const float* InFloatData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo, + bool bAttemptRunLengthEncoding) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + if (InAttributeInfo.count <= 0 || InAttributeInfo.tupleSize < 1) + return HAPI_RESULT_INVALID_ARGUMENT; + + HAPI_Result Result = HAPI_RESULT_FAILURE; + + if (bAttemptRunLengthEncoding) + { + TArray RunLengths = RunLengthEncode(InFloatData, InAttributeInfo.tupleSize, InAttributeInfo.count); + if (RunLengths.Num() != 0) + { + for(int Index = 0; Index < RunLengths.Num(); Index++) + { + int StartIndex = RunLengths[Index]; + int EndIndex = InAttributeInfo.count; + if (Index != RunLengths.Num() - 1) + EndIndex = RunLengths[Index + 1]; + + const float* TupleValues = &InFloatData[StartIndex * InAttributeInfo.tupleSize]; + Result = FHoudiniApi::SetAttributeFloatUniqueData(FHoudiniEngine::Get().GetSession(), InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, TupleValues, InAttributeInfo.tupleSize, StartIndex, EndIndex - StartIndex); + + if (Result != HAPI_RESULT_SUCCESS) + return Result; + } + return HAPI_RESULT_SUCCESS; + } + } + + int32 ChunkSize = THRIFT_MAX_CHUNKSIZE / InAttributeInfo.tupleSize; + if (InAttributeInfo.count > ChunkSize) + { + // Send the attribte in chunks + for (int32 ChunkStart = 0; ChunkStart < InAttributeInfo.count; ChunkStart += ChunkSize) + { + int32 CurCount = InAttributeInfo.count - ChunkStart > ChunkSize ? ChunkSize : InAttributeInfo.count - ChunkStart; + + Result = FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InFloatData + ChunkStart * InAttributeInfo.tupleSize, + ChunkStart, CurCount); + + if (Result != HAPI_RESULT_SUCCESS) + break; + } + } + else + { + // Send all the attribute values once + Result = FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InFloatData, + 0, InAttributeInfo.count); + } + + return Result; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeIntData( + const TArray& InIntData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo, + bool bAttemptRunLengthEncoding) +{ + if (InIntData.Num() != InAttributeInfo.count * InAttributeInfo.tupleSize) + return HAPI_RESULT_INVALID_ARGUMENT; + + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName)); + bool bSuccess = Accessor.SetAttributeData(InAttributeInfo, InIntData); + + return bSuccess ? HAPI_RESULT_SUCCESS : HAPI_RESULT_FAILURE; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeFloatUniqueData( + const float InFloatData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + if (InAttributeInfo.count <= 0 || InAttributeInfo.tupleSize < 1) + return HAPI_RESULT_INVALID_ARGUMENT; + + HAPI_Result Result = FHoudiniApi::SetAttributeFloatUniqueData( + FHoudiniEngine::Get().GetSession(), InNodeId, InPartId, + TCHAR_TO_ANSI(*InAttributeName), &InAttributeInfo, &InFloatData, InAttributeInfo.tupleSize, + 0, InAttributeInfo.count); + + return Result; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeIntUniqueData( + const int32 InIntData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + if (InAttributeInfo.count <= 0 || InAttributeInfo.tupleSize < 1) + return HAPI_RESULT_INVALID_ARGUMENT; + + HAPI_Result Result = FHoudiniApi::SetAttributeIntUniqueData( + FHoudiniEngine::Get().GetSession(), InNodeId, InPartId, + TCHAR_TO_ANSI(*InAttributeName), &InAttributeInfo, &InIntData, InAttributeInfo.tupleSize, + 0, InAttributeInfo.count); + + return Result; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeIntData( + const int32* InIntData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo, + bool bAttemptRunLengthEncoding) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + if (InAttributeInfo.count <= 0 || InAttributeInfo.tupleSize < 1) + return HAPI_RESULT_INVALID_ARGUMENT; + + if (bAttemptRunLengthEncoding) + { + TArray RunLengths = RunLengthEncode(InIntData, InAttributeInfo.tupleSize, InAttributeInfo.count); + if (RunLengths.Num() != 0) + { + for (int Index = 0; Index < RunLengths.Num(); Index++) + { + int StartIndex = RunLengths[Index]; + int EndIndex = InAttributeInfo.count / InAttributeInfo.tupleSize; + if (Index != RunLengths.Num() - 1) + EndIndex = RunLengths[Index + 1]; + + const int* TupleValues = &InIntData[StartIndex * InAttributeInfo.tupleSize]; + HAPI_Result Result = FHoudiniApi::SetAttributeIntUniqueData( + FHoudiniEngine::Get().GetSession(), InNodeId, InPartId, + TCHAR_TO_ANSI(*InAttributeName), &InAttributeInfo, TupleValues, InAttributeInfo.tupleSize, + StartIndex, EndIndex - StartIndex); + + if (Result != HAPI_RESULT_SUCCESS) + return Result; + } + return HAPI_RESULT_SUCCESS; + } + } + + HAPI_Result Result = HAPI_RESULT_FAILURE; + int32 ChunkSize = THRIFT_MAX_CHUNKSIZE / InAttributeInfo.tupleSize; + if (InAttributeInfo.count > ChunkSize) + { + // Send the attribte in chunks + for (int32 ChunkStart = 0; ChunkStart < InAttributeInfo.count; ChunkStart += ChunkSize) + { + int32 CurCount = InAttributeInfo.count - ChunkStart > ChunkSize ? ChunkSize : InAttributeInfo.count - ChunkStart; + + Result = FHoudiniApi::SetAttributeIntData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InIntData + ChunkStart * InAttributeInfo.tupleSize, + ChunkStart, CurCount); + + if (Result != HAPI_RESULT_SUCCESS) + break; + } + } + else + { + // Send all the attribute values once + Result = FHoudiniApi::SetAttributeIntData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InIntData, + 0, InAttributeInfo.count); + } + + return Result; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeUIntData( + const TArray& InIntData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName)); + bool bSuccess = Accessor.SetAttributeData(InAttributeInfo, InIntData); + + return bSuccess ? HAPI_RESULT_SUCCESS : HAPI_RESULT_FAILURE; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeUIntData( + const int64* InIntData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName)); + bool bSuccess = Accessor.SetAttributeData(InAttributeInfo, InIntData); + + return bSuccess ? HAPI_RESULT_SUCCESS : HAPI_RESULT_FAILURE; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeInt8Data( + const TArray& InByteData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + if (InByteData.Num() != InAttributeInfo.count * InAttributeInfo.tupleSize) + return HAPI_RESULT_INVALID_ARGUMENT; + + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName)); + bool bSuccess = Accessor.SetAttributeData(InAttributeInfo, InByteData); + + return bSuccess ? HAPI_RESULT_SUCCESS : HAPI_RESULT_FAILURE; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeInt8Data( + const int8* InByteData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + if (InAttributeInfo.count <= 0 || InAttributeInfo.tupleSize < 1) + return HAPI_RESULT_INVALID_ARGUMENT; + + HAPI_Result Result = HAPI_RESULT_FAILURE; + int32 ChunkSize = THRIFT_MAX_CHUNKSIZE / InAttributeInfo.tupleSize; + if (InAttributeInfo.count > ChunkSize) + { + // Send the attribte in chunks + for (int32 ChunkStart = 0; ChunkStart < InAttributeInfo.count; ChunkStart += ChunkSize) + { + int32 CurCount = InAttributeInfo.count - ChunkStart > ChunkSize ? ChunkSize : InAttributeInfo.count - ChunkStart; + + Result = FHoudiniApi::SetAttributeInt8Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InByteData + ChunkStart * InAttributeInfo.tupleSize, + ChunkStart, CurCount); + + if (Result != HAPI_RESULT_SUCCESS) + break; + } + } + else + { + // Send all the attribute values once + Result = FHoudiniApi::SetAttributeInt8Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InByteData, + 0, InAttributeInfo.count); + } + + return Result; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeUInt8Data( + const TArray& InByteData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + if (InByteData.Num() != InAttributeInfo.count * InAttributeInfo.tupleSize) + return HAPI_RESULT_INVALID_ARGUMENT; + + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName)); + bool bSuccess = Accessor.SetAttributeData(InAttributeInfo, InByteData); + + return bSuccess ? HAPI_RESULT_SUCCESS : HAPI_RESULT_FAILURE; + +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeUInt8Data( + const uint8* InByteData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + if (InAttributeInfo.count <= 0 || InAttributeInfo.tupleSize < 1) + return HAPI_RESULT_INVALID_ARGUMENT; + + HAPI_Result Result = HAPI_RESULT_FAILURE; + int32 ChunkSize = THRIFT_MAX_CHUNKSIZE / InAttributeInfo.tupleSize; + if (InAttributeInfo.count > ChunkSize) + { + // Send the attribte in chunks + for (int32 ChunkStart = 0; ChunkStart < InAttributeInfo.count; ChunkStart += ChunkSize) + { + int32 CurCount = InAttributeInfo.count - ChunkStart > ChunkSize ? ChunkSize : InAttributeInfo.count - ChunkStart; + + Result = FHoudiniApi::SetAttributeUInt8Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InByteData + ChunkStart * InAttributeInfo.tupleSize, + ChunkStart, CurCount); + + if (Result != HAPI_RESULT_SUCCESS) + break; + } + } + else + { + // Send all the attribute values once + Result = FHoudiniApi::SetAttributeUInt8Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InByteData, + 0, InAttributeInfo.count); + } + + return Result; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeInt16Data( + const TArray& InShortData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + if (InShortData.Num() != InAttributeInfo.count * InAttributeInfo.tupleSize) + return HAPI_RESULT_INVALID_ARGUMENT; + + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName)); + bool bSuccess = Accessor.SetAttributeData(InAttributeInfo, InShortData); + + return bSuccess ? HAPI_RESULT_SUCCESS : HAPI_RESULT_FAILURE; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeInt16Data( + const int16* InShortData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + if (InAttributeInfo.count <= 0 || InAttributeInfo.tupleSize < 1) + return HAPI_RESULT_INVALID_ARGUMENT; + + HAPI_Result Result = HAPI_RESULT_FAILURE; + int32 ChunkSize = THRIFT_MAX_CHUNKSIZE / InAttributeInfo.tupleSize; + if (InAttributeInfo.count > ChunkSize) + { + // Send the attribte in chunks + for (int32 ChunkStart = 0; ChunkStart < InAttributeInfo.count; ChunkStart += ChunkSize) + { + int32 CurCount = InAttributeInfo.count - ChunkStart > ChunkSize ? ChunkSize : InAttributeInfo.count - ChunkStart; + + Result = FHoudiniApi::SetAttributeInt16Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InShortData + ChunkStart * InAttributeInfo.tupleSize, + ChunkStart, CurCount); + + if (Result != HAPI_RESULT_SUCCESS) + break; + } + } + else + { + // Send all the attribute values once + Result = FHoudiniApi::SetAttributeInt16Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InShortData, + 0, InAttributeInfo.count); + } + + return Result; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeUInt16Data( + const TArray& InShortData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName)); + bool bSuccess = Accessor.SetAttributeData(InAttributeInfo, InShortData); + + return bSuccess ? HAPI_RESULT_SUCCESS : HAPI_RESULT_FAILURE; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeUInt16Data( + const int32* InShortData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName)); + bool bSuccess = Accessor.SetAttributeData(InAttributeInfo, InShortData); + + return bSuccess ? HAPI_RESULT_SUCCESS : HAPI_RESULT_FAILURE; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeInt64Data( + const TArray& InInt64Data, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + if (InInt64Data.Num() != InAttributeInfo.count * InAttributeInfo.tupleSize) + return HAPI_RESULT_INVALID_ARGUMENT; + + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName)); + bool bSuccess = Accessor.SetAttributeData(InAttributeInfo, InInt64Data); + + return bSuccess ? HAPI_RESULT_SUCCESS : HAPI_RESULT_FAILURE; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeInt64Data( + const int64* InInt64Data, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + if (InAttributeInfo.count <= 0 || InAttributeInfo.tupleSize < 1) + return HAPI_RESULT_INVALID_ARGUMENT; + +#if PLATFORM_LINUX + TArray HData; + HData.Reserve(InAttributeInfo.count * InAttributeInfo.tupleSize); + if (sizeof(int64) != sizeof(HAPI_Int64)) + { + for (int32 Idx = 0; Idx < InAttributeInfo.count * InAttributeInfo.tupleSize; ++Idx) + { + HData.Add(static_cast(InInt64Data[Idx])); + } + } +#endif + + HAPI_Result Result = HAPI_RESULT_FAILURE; + int32 ChunkSize = THRIFT_MAX_CHUNKSIZE / InAttributeInfo.tupleSize; + if (InAttributeInfo.count > ChunkSize) + { + // Send the attribute in chunks + for (int32 ChunkStart = 0; ChunkStart < InAttributeInfo.count; ChunkStart += ChunkSize) + { + int32 CurCount = InAttributeInfo.count - ChunkStart > ChunkSize ? ChunkSize : InAttributeInfo.count - ChunkStart; +#if PLATFORM_LINUX + if (sizeof(int64) != sizeof(HAPI_Int64)) + { + Result = FHoudiniApi::SetAttributeInt64Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, HData.GetData() + ChunkStart * InAttributeInfo.tupleSize, + ChunkStart, CurCount); + } + else + { + Result = FHoudiniApi::SetAttributeInt64Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, reinterpret_cast(InInt64Data + ChunkStart * InAttributeInfo.tupleSize), + ChunkStart, CurCount); + } +#else + Result = FHoudiniApi::SetAttributeInt64Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InInt64Data + ChunkStart * InAttributeInfo.tupleSize, + ChunkStart, CurCount); +#endif + if (Result != HAPI_RESULT_SUCCESS) + break; + } + } + else + { + // Send all the attribute values once +#if PLATFORM_LINUX + if (sizeof(int64) != sizeof(HAPI_Int64)) + { + Result = FHoudiniApi::SetAttributeInt64Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, HData.GetData(), + 0, InAttributeInfo.count); + } + else + { + Result = FHoudiniApi::SetAttributeInt64Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, reinterpret_cast(InInt64Data), + 0, InAttributeInfo.count); + } +#else + Result = FHoudiniApi::SetAttributeInt64Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InInt64Data, + 0, InAttributeInfo.count); +#endif + } + + return Result; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeUInt64Data( + const TArray& InInt64Data, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName)); + bool bSuccess = Accessor.SetAttributeData(InAttributeInfo, InInt64Data); + + return bSuccess ? HAPI_RESULT_SUCCESS : HAPI_RESULT_FAILURE; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeDoubleData( + const TArray& InDoubleData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + if (InDoubleData.Num() != InAttributeInfo.count * InAttributeInfo.tupleSize) + return HAPI_RESULT_INVALID_ARGUMENT; + + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName)); + bool bSuccess = Accessor.SetAttributeData(InAttributeInfo, InDoubleData); + + return bSuccess ? HAPI_RESULT_SUCCESS : HAPI_RESULT_FAILURE; + +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeDoubleData( + const double* InDoubleData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + if (InAttributeInfo.count <= 0 || InAttributeInfo.tupleSize < 1) + return HAPI_RESULT_INVALID_ARGUMENT; + + HAPI_Result Result = HAPI_RESULT_FAILURE; + int32 ChunkSize = THRIFT_MAX_CHUNKSIZE / InAttributeInfo.tupleSize; + if (InAttributeInfo.count > ChunkSize) + { + // Send the attribte in chunks + for (int32 ChunkStart = 0; ChunkStart < InAttributeInfo.count; ChunkStart += ChunkSize) + { + int32 CurCount = InAttributeInfo.count - ChunkStart > ChunkSize ? ChunkSize : InAttributeInfo.count - ChunkStart; + + Result = FHoudiniApi::SetAttributeFloat64Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InDoubleData + ChunkStart * InAttributeInfo.tupleSize, + ChunkStart, CurCount); + + if (Result != HAPI_RESULT_SUCCESS) + break; + } + } + else + { + // Send all the attribute values once + Result = FHoudiniApi::SetAttributeFloat64Data( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, InDoubleData, + 0, InAttributeInfo.count); + } + + return Result; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetVertexList( + const TArray& InVertexListData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId) +{ + H_SCOPED_FUNCTION_TIMER(); + + int32 ListNum = InVertexListData.Num(); + if (ListNum < 1) + return HAPI_RESULT_INVALID_ARGUMENT; + + int32 ChunkSize = THRIFT_MAX_CHUNKSIZE; + HAPI_Result Result = HAPI_RESULT_FAILURE; + if (ListNum > ChunkSize) + { + // Send the vertex list in chunks + for (int32 ChunkStart = 0; ChunkStart < ListNum; ChunkStart += ChunkSize) + { + int32 CurCount = ListNum - ChunkStart > ChunkSize ? ChunkSize : ListNum - ChunkStart; + Result = FHoudiniApi::SetVertexList( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, InVertexListData.GetData() + ChunkStart, ChunkStart, CurCount); + + if (Result != HAPI_RESULT_SUCCESS) + break; + } + } + else + { + Result = FHoudiniApi::SetVertexList( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, InVertexListData.GetData(), 0, InVertexListData.Num()); + } + + return Result; +} + + +HAPI_Result +FHoudiniEngineUtils::HapiSetFaceCounts( + const TArray& InFaceCounts, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId) +{ + H_SCOPED_FUNCTION_TIMER(); + + int32 FaceCountsNum = InFaceCounts.Num(); + if (FaceCountsNum < 1) + return HAPI_RESULT_INVALID_ARGUMENT; + + int32 ChunkSize = THRIFT_MAX_CHUNKSIZE; + HAPI_Result Result = HAPI_RESULT_FAILURE; + if (FaceCountsNum > ChunkSize) + { + // Send the vertex list in chunks + for (int32 ChunkStart = 0; ChunkStart < FaceCountsNum; ChunkStart += ChunkSize) + { + int32 CurCount = FaceCountsNum - ChunkStart > ChunkSize ? ChunkSize : FaceCountsNum - ChunkStart; + Result = FHoudiniApi::SetFaceCounts( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, InFaceCounts.GetData() + ChunkStart, ChunkStart, CurCount); + + if (Result != HAPI_RESULT_SUCCESS) + break; + } + } + else + { + Result = FHoudiniApi::SetFaceCounts( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, InFaceCounts.GetData(), 0, InFaceCounts.Num()); + } + + return Result; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeStringUniqueData( + const FString& InString, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + return FHoudiniApi::SetAttributeStringUniqueData( + FHoudiniEngine::Get().GetSession(), + InNodeId, + InPartId, + TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, + TCHAR_TO_ANSI(*InString), + InAttributeInfo.tupleSize, + 0, + InAttributeInfo.count); +} + + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeStringMap( + const FHoudiniEngineIndexedStringMap& InIndexedStringMap, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + FHoudiniEngineRawStrings IndexedRawStrings = InIndexedStringMap.GetRawStrings(); + TArray IndexArray = InIndexedStringMap.GetIds(); + + HAPI_Result Result = FHoudiniApi::SetAttributeIndexedStringData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, IndexedRawStrings.RawStrings.GetData(), IndexedRawStrings.RawStrings.Num(), IndexArray.GetData(), 0, IndexArray.Num()); + + return Result; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeStringData( + const TArray& InStringArray, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo ) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + TArray StringDataArray; + for (const auto& CurrentString : InStringArray) + { + // Append the converted string to the string array + StringDataArray.Add(FHoudiniEngineUtils::ExtractRawString(CurrentString)); + } + + // Send strings in smaller chunks due to their potential size + int32 ChunkSize = (THRIFT_MAX_CHUNKSIZE / 100) / InAttributeInfo.tupleSize; + + HAPI_Result Result = HAPI_RESULT_FAILURE; + if (InAttributeInfo.count > ChunkSize) + { + // Set the attributes in chunks + for (int32 ChunkStart = 0; ChunkStart < InAttributeInfo.count; ChunkStart += ChunkSize) + { + int32 CurCount = InAttributeInfo.count - ChunkStart > ChunkSize ? ChunkSize : InAttributeInfo.count - ChunkStart; + + Result = FHoudiniApi::SetAttributeStringData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, StringDataArray.GetData() + ChunkStart * InAttributeInfo.tupleSize, + ChunkStart, CurCount); + + if (Result != HAPI_RESULT_SUCCESS) + break; + } + } + else + { + // Set all the attribute values once + Result = FHoudiniApi::SetAttributeStringData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, StringDataArray.GetData(), + 0, InAttributeInfo.count); + } + + // ExtractRawString allocates memory using malloc, free it! + FreeRawStringMemory(StringDataArray); + + return Result; +} + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeStringArrayData( + const TArray& InStringArray, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo, + const TArray& SizesFixedArray) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + TArray StringDataArray; + for (const auto& CurrentString : InStringArray) + { + // Append the converted string to the string array + StringDataArray.Add(FHoudiniEngineUtils::ExtractRawString(CurrentString)); + } + + // Send strings in smaller chunks due to their potential size + int32 ChunkSize = (THRIFT_MAX_CHUNKSIZE / 100) / InAttributeInfo.tupleSize; + + HAPI_Result Result = HAPI_RESULT_FAILURE; + if (InAttributeInfo.count > ChunkSize) + { + // Set the attributes in chunks + int32 StringStart = 0; + for (int32 ChunkStart = 0; ChunkStart < InAttributeInfo.count; ChunkStart += ChunkSize) + { + int32 CurCount = SizesFixedArray.Num() - ChunkStart > ChunkSize ? ChunkSize : SizesFixedArray.Num() - ChunkStart; + int32 NumSent = 0; + for (int32 Idx = 0; Idx < CurCount; ++Idx) + { + NumSent += SizesFixedArray[Idx + ChunkStart * InAttributeInfo.tupleSize]; + } + + Result = FHoudiniApi::SetAttributeStringArrayData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, StringDataArray.GetData() + StringStart, NumSent, + SizesFixedArray.GetData() + ChunkStart * InAttributeInfo.tupleSize, ChunkStart, CurCount); + + if (Result != HAPI_RESULT_SUCCESS) + break; + + StringStart += NumSent; + } + } + else + { + // Set all the attribute values once + Result = FHoudiniApi::SetAttributeStringArrayData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, StringDataArray.GetData(), StringDataArray.Num(), + SizesFixedArray.GetData(),0, SizesFixedArray.Num()); + } + + // ExtractRawString allocates memory using malloc, free it! + FreeRawStringMemory(StringDataArray); + + return Result; +} + + +HAPI_Result +FHoudiniEngineUtils::HapiSetAttributeDictionaryData(const TArray& JSONData, + HAPI_NodeId InNodeId, HAPI_PartId InPartId, const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo) +{ + H_SCOPED_FUNCTION_DYNAMIC_LABEL(InAttributeName); + + TArray RawStringData; + for (const FString& Data : JSONData) + { + RawStringData.Add(FHoudiniEngineUtils::ExtractRawString(Data)); + } + + // Send strings in smaller chunks due to their potential size + int32 ChunkSize = (THRIFT_MAX_CHUNKSIZE / 100) / InAttributeInfo.tupleSize; + + HAPI_Result Result = HAPI_RESULT_FAILURE; + if (InAttributeInfo.count > ChunkSize) + { + // Set the attributes in chunks + for (int32 ChunkStart = 0; ChunkStart < InAttributeInfo.count; ChunkStart += ChunkSize) + { + const int32 CurCount = InAttributeInfo.count - ChunkStart > ChunkSize ? ChunkSize : InAttributeInfo.count - ChunkStart; + + Result = FHoudiniApi::SetAttributeDictionaryData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, RawStringData.GetData() + ChunkStart * InAttributeInfo.tupleSize, + ChunkStart, CurCount); + + if (Result != HAPI_RESULT_SUCCESS) + break; + } + } + else + { + // Set all the attribute values once + Result = FHoudiniApi::SetAttributeDictionaryData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, TCHAR_TO_ANSI(*InAttributeName), + &InAttributeInfo, RawStringData.GetData(), + 0, RawStringData.Num()); + } + + // ExtractRawString allocates memory using malloc, free it! + FreeRawStringMemory(RawStringData); + + return Result; +} + + +HAPI_Result +FHoudiniEngineUtils::HapiSetHeightFieldData( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const TArray& InFloatValues, + const FString& InHeightfieldName) +{ + H_SCOPED_FUNCTION_TIMER(); + + int32 NumValues = InFloatValues.Num(); + if (NumValues < 1) + return HAPI_RESULT_INVALID_ARGUMENT; + + // Get the volume name as std::string + std::string NameStr; + FHoudiniEngineUtils::ConvertUnrealString(InHeightfieldName, NameStr); + + // Get the Heighfield float data + const float* HeightData = InFloatValues.GetData(); + + int32 ChunkSize = THRIFT_MAX_CHUNKSIZE; + HAPI_Result Result = HAPI_RESULT_FAILURE; + if (NumValues > ChunkSize) + { + // Send the heightfield data in chunks + for (int32 ChunkStart = 0; ChunkStart < NumValues; ChunkStart += ChunkSize) + { + int32 CurCount = NumValues - ChunkStart > ChunkSize ? ChunkSize : NumValues - ChunkStart; + + Result = FHoudiniApi::SetHeightFieldData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, NameStr.c_str(), &HeightData[ChunkStart], ChunkStart, CurCount); + + if (Result != HAPI_RESULT_SUCCESS) + break; + } + } + else + { + Result = FHoudiniApi::SetHeightFieldData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, NameStr.c_str(), HeightData, 0, InFloatValues.Num()); + } + + return Result; +} + + +char * +FHoudiniEngineUtils::ExtractRawString(const FString& InString) +{ + // Return an empty string instead of returning null to avoid potential crashes + std::string ConvertedString(""); + if (!InString.IsEmpty()) + ConvertedString = H_TCHAR_TO_UTF8(*InString); + + // Allocate space for unique string. + int32 UniqueStringBytes = ConvertedString.size() + 1; + char * UniqueString = static_cast(FMemory::Malloc(UniqueStringBytes)); + + FMemory::Memzero(UniqueString, UniqueStringBytes); + FMemory::Memcpy(UniqueString, ConvertedString.c_str(), ConvertedString.size()); + + return UniqueString; +} + +void +FHoudiniEngineUtils::FreeRawStringMemory(const char*& InRawString) +{ + if (InRawString == nullptr) + return; + + // Do not attempt to free empty strings! + if (!InRawString[0]) + return; + + FMemory::Free((void*)InRawString); + InRawString = nullptr; +} + +void +FHoudiniEngineUtils::FreeRawStringMemory(TArray& InRawStringArray) +{ + // ExtractRawString allocates memory using malloc, free it! + for (auto CurrentStrPtr : InRawStringArray) + { + FreeRawStringMemory(CurrentStrPtr); + } + InRawStringArray.Empty(); +} + +bool +FHoudiniEngineUtils::AddHoudiniLogoToComponent(USceneComponent* InComponent) +{ + if (!IsValid(InComponent)) + return false; + + // No need to add another component if we already show the logo + if (FHoudiniEngineUtils::HasHoudiniLogo(InComponent)) + return true; + + // Remove the texture preview mesh if we have it somehow + if (FHoudiniEngineUtils::HasTextureMesh(InComponent)) + FHoudiniEngineUtils::RemoveTextureMeshFromComponent(InComponent); + + UStaticMesh* HoudiniLogoSM = FHoudiniEngine::Get().GetHoudiniLogoStaticMesh().Get(); + if (!HoudiniLogoSM) + return false; + + UStaticMeshComponent * HoudiniLogoSMC = NewObject( + InComponent, UStaticMeshComponent::StaticClass(), NAME_None, RF_Transactional); + + if (!HoudiniLogoSMC) + return false; + + HoudiniLogoSMC->SetStaticMesh(HoudiniLogoSM); + HoudiniLogoSMC->SetVisibility(true); + HoudiniLogoSMC->SetHiddenInGame(true); + // Attach created static mesh component to our Houdini component. + HoudiniLogoSMC->AttachToComponent(InComponent, FAttachmentTransformRules::KeepRelativeTransform); + HoudiniLogoSMC->RegisterComponent(); + + return true; +} + +bool +FHoudiniEngineUtils::RemoveHoudiniLogoFromComponent(USceneComponent* InComponent) +{ + if (!IsValid(InComponent)) + return false; + + // Get the Houdini Logo SM + UStaticMesh* HoudiniLogoSM = FHoudiniEngine::Get().GetHoudiniLogoStaticMesh().Get(); + if (!HoudiniLogoSM) + return false; + + // Iterate on the HAC's component + for (USceneComponent* CurrentSceneComp : InComponent->GetAttachChildren()) + { + if (!IsValid(CurrentSceneComp) || !CurrentSceneComp->IsA()) + continue; + + // Get the static mesh component + UStaticMeshComponent* SMC = Cast(CurrentSceneComp); + if (!IsValid(SMC)) + continue; + + // Check if the SMC is the Houdini Logo + if (SMC->GetStaticMesh() != HoudiniLogoSM) + continue; + + SMC->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + SMC->UnregisterComponent(); + SMC->DestroyComponent(); + + return true; + } + + return false; +} + +bool +FHoudiniEngineUtils::HasHoudiniLogo(USceneComponent* InComponent) +{ + if (!IsValid(InComponent)) + return false; + + // Get the Houdini Logo SM + UStaticMesh* HoudiniLogoSM = FHoudiniEngine::Get().GetHoudiniLogoStaticMesh().Get(); + if (!HoudiniLogoSM) + return false; + + // Iterate on the HAC's component + for (USceneComponent* CurrentSceneComp : InComponent->GetAttachChildren()) + { + if (!IsValid(CurrentSceneComp) || !CurrentSceneComp->IsA()) + continue; + + // Get the static mesh component + UStaticMeshComponent* SMC = Cast(CurrentSceneComp); + if (!IsValid(SMC)) + continue; + + // Check if the SMC is the Houdini Logo + if (SMC->GetStaticMesh() == HoudiniLogoSM) + return true; + } + + return false; +} + + +bool +FHoudiniEngineUtils::AddTextureMeshToComponent( + USceneComponent* InComponent, + UTexture2D* InTexture, + UMaterialInterface* InMaterial) +{ + if (!IsValid(InComponent)) + return false; + + // Remove the Houdini logo if it was there before + if (FHoudiniEngineUtils::HasHoudiniLogo(InComponent)) + FHoudiniEngineUtils::RemoveHoudiniLogoFromComponent(InComponent); + + // No need to do anything if we already show the default mesh + if (FHoudiniEngineUtils::HasTextureMesh(InComponent)) + return true; + + // Get the COP SM + UStaticMesh* HoudiniCOPMesh = FHoudiniEngine::Get().GetHoudiniCOPStaticMesh().Get(); + if (!HoudiniCOPMesh) + return false; + + UStaticMeshComponent* HoudiniCOPSMC = NewObject( + InComponent, UStaticMeshComponent::StaticClass(), NAME_None, RF_Transactional); + + if (!HoudiniCOPSMC) + return false; + + HoudiniCOPSMC->SetStaticMesh(HoudiniCOPMesh); + HoudiniCOPSMC->SetVisibility(true); + HoudiniCOPSMC->SetHiddenInGame(true); + + // Attach created static mesh component to our Houdini component. + HoudiniCOPSMC->AttachToComponent(InComponent, FAttachmentTransformRules::KeepRelativeTransform); + HoudiniCOPSMC->RegisterComponent(); + + // We need to update the mesh's material, + // to the one that was created with the texture + HoudiniCOPSMC->SetMaterial(0, InMaterial); + + // That EditorPlane isn't properly oriented + HoudiniCOPSMC->SetRelativeRotation(FRotator(0.0, -90.0, 0.0)); + + return true; +} + +bool +FHoudiniEngineUtils::RemoveTextureMeshFromComponent(USceneComponent* InComponent) +{ + if (!IsValid(InComponent)) + return false; + + // Get the COP SM + UStaticMesh* HoudiniCOPMesh = FHoudiniEngine::Get().GetHoudiniCOPStaticMesh().Get(); + if (!HoudiniCOPMesh) + return false; + + // Iterate on the HAC's component + for (USceneComponent* CurrentSceneComp : InComponent->GetAttachChildren()) + { + if (!IsValid(CurrentSceneComp) || !CurrentSceneComp->IsA()) + continue; + + // Get the static mesh component + UStaticMeshComponent* SMC = Cast(CurrentSceneComp); + if (!IsValid(SMC)) + continue; + + // Check if the SMC is the defauilt COP mesh + if (SMC->GetStaticMesh() != HoudiniCOPMesh) + continue; + + SMC->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + SMC->UnregisterComponent(); + SMC->DestroyComponent(); + + return true; + } + + return false; +} + +bool +FHoudiniEngineUtils::HasTextureMesh(USceneComponent* InComponent) +{ + if (!IsValid(InComponent)) + return false; + + // Get the COP SM + UStaticMesh* HoudiniCOPMesh = FHoudiniEngine::Get().GetHoudiniCOPStaticMesh().Get(); + if (!HoudiniCOPMesh) + return false; + + // Iterate on the HAC's component + for (USceneComponent* CurrentSceneComp : InComponent->GetAttachChildren()) + { + if (!IsValid(CurrentSceneComp) || !CurrentSceneComp->IsA()) + continue; + + // Get the static mesh component + UStaticMeshComponent* SMC = Cast(CurrentSceneComp); + if (!IsValid(SMC)) + continue; + + // Check if the SMC is the Houdini Logo + if (SMC->GetStaticMesh() == HoudiniCOPMesh) + return true; + } + + return false; +} + + + +int32 +FHoudiniEngineUtils::HapiGetVertexListForGroup( + HAPI_NodeId GeoId, + const HAPI_PartInfo& PartInfo, + const FString& GroupName, + const TArray& FullVertexList, + TArray& NewVertexList, + TArray& UsedVertices, + TArray& AllFaceList, + TArray& AllGroupFaceIndices, + int32& FirstValidVertex, + int32& FirstValidPrim, + bool isPackedPrim) +{ + int32 ProcessedWedges = 0; + AllFaceList.Empty(); + FirstValidPrim = 0; + FirstValidVertex = 0; + + NewVertexList.SetNumUninitialized(FullVertexList.Num()); + for(int32 n = 0; n < NewVertexList.Num(); n++) + NewVertexList[n] = -1; + + // Get the faces membership for this group + bool bAllEquals = false; + TArray PartGroupMembership; + if (!FHoudiniEngineUtils::HapiGetGroupMembership( + GeoId, PartInfo, HAPI_GROUPTYPE_PRIM, GroupName, PartGroupMembership, bAllEquals)) + return false; + + // Go through all primitives. + for (int32 FaceIdx = 0; FaceIdx < PartGroupMembership.Num(); ++FaceIdx) + { + if (PartGroupMembership[FaceIdx] <= 0) + { + // The face is not in the group, skip + continue; + } + + // Add the face's index. + AllFaceList.Add(FaceIdx); + + // Get the index of this face's vertices + int32 FirstVertexIdx = FaceIdx * 3; + int32 SecondVertexIdx = FirstVertexIdx + 1; + int32 LastVertexIdx = FirstVertexIdx + 2; + + // This face is a member of specified group. + // Add all 3 vertices + if (FullVertexList.IsValidIndex(LastVertexIdx)) + { + NewVertexList[FirstVertexIdx] = FullVertexList[FirstVertexIdx]; + NewVertexList[SecondVertexIdx] = FullVertexList[SecondVertexIdx]; + NewVertexList[LastVertexIdx] = FullVertexList[LastVertexIdx]; + } + + // Mark these vertex indices as used. + if (UsedVertices.IsValidIndex(LastVertexIdx)) + { + UsedVertices[FirstVertexIdx] = 1; + UsedVertices[SecondVertexIdx] = 1; + UsedVertices[LastVertexIdx] = 1; + } + + // Mark this face as used. + if (AllGroupFaceIndices.IsValidIndex(FaceIdx)) + AllGroupFaceIndices[FaceIdx] = 1; + + if (ProcessedWedges == 0) + { + // Keep track of the first valid vertex/face indices for this group + // This will be useful later on when extracting attributes + FirstValidVertex = FirstVertexIdx; + FirstValidPrim = FaceIdx; + } + + ProcessedWedges += 3; + } + + return ProcessedWedges; +} + +bool +FHoudiniEngineUtils::HapiGetGroupNames( + const HAPI_NodeId GeoId, const HAPI_PartId PartId, + const HAPI_GroupType GroupType, const bool isPackedPrim, + TArray& OutGroupNames) +{ + int32 GroupCount = 0; + if (!isPackedPrim) + { + // Get group count on the geo + HAPI_GeoInfo GeoInfo; + FHoudiniApi::GeoInfo_Init(&GeoInfo); + + HAPI_Result Result = FHoudiniApi::GetGeoInfo( + FHoudiniEngine::Get().GetSession(), GeoId, &GeoInfo); + + // We may get invalid argument here with COP assets, + // this is fine, so don't spam an error in that case. + if (HAPI_RESULT_INVALID_ARGUMENT == Result) + return true; + else + HOUDINI_CHECK_ERROR_RETURN(Result, false); + + if (GroupType == HAPI_GROUPTYPE_POINT) + GroupCount = GeoInfo.pointGroupCount; + else if (GroupType == HAPI_GROUPTYPE_PRIM) + GroupCount = GeoInfo.primitiveGroupCount; + } + else + { + // We need the group count for this packed prim + int32 PointGroupCount = 0, PrimGroupCount = 0; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetGroupCountOnPackedInstancePart( + FHoudiniEngine::Get().GetSession(), GeoId, PartId, &PointGroupCount, &PrimGroupCount), false); + + if (GroupType == HAPI_GROUPTYPE_POINT) + GroupCount = PointGroupCount; + else if (GroupType == HAPI_GROUPTYPE_PRIM) + GroupCount = PrimGroupCount; + } + + if (GroupCount <= 0) + return true; + + TArray GroupNameStringHandles; + GroupNameStringHandles.SetNumZeroed(GroupCount); + if (!isPackedPrim) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetGroupNames( + FHoudiniEngine::Get().GetSession(), + GeoId, GroupType, &GroupNameStringHandles[0], GroupCount), false); + } + else + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetGroupNamesOnPackedInstancePart( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, GroupType, &GroupNameStringHandles[0], GroupCount), false); + } + + /* + OutGroupNames.SetNum(GroupCount); + for (int32 NameIdx = 0; NameIdx < GroupCount; ++NameIdx) + { + FString CurrentGroupName = TEXT(""); + FHoudiniEngineString::ToFString(GroupNameStringHandles[NameIdx], CurrentGroupName); + OutGroupNames[NameIdx] = CurrentGroupName; + } + */ + + FHoudiniEngineString::SHArrayToFStringArray(GroupNameStringHandles, OutGroupNames); + + return true; +} + +bool FHoudiniEngineUtils::HapiGetGroupMembership( + HAPI_NodeId GeoId, const HAPI_PartId PartId, + const HAPI_GroupType& GroupType, const FString& GroupName, + int32 & OutGroupMembership, int Start, int Length) +{ + OutGroupMembership = 0; + + std::string ConvertedGroupName = H_TCHAR_TO_UTF8(*GroupName); + + bool AllEqual; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetGroupMembership( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, GroupType, ConvertedGroupName.c_str(), + &AllEqual, &OutGroupMembership, Start, Length), false); + + return true; +} + +bool +FHoudiniEngineUtils::HapiGetGroupMembership( + HAPI_NodeId GeoId, const HAPI_PartInfo& PartInfo, + const HAPI_GroupType& GroupType, const FString & GroupName, + TArray& OutGroupMembership, bool& OutAllEquals) +{ + int32 ElementCount = (GroupType == HAPI_GROUPTYPE_POINT) ? PartInfo.pointCount : PartInfo.faceCount; + if (ElementCount < 1) + return false; + OutGroupMembership.SetNum(ElementCount); + + OutAllEquals = false; + std::string ConvertedGroupName = H_TCHAR_TO_UTF8(*GroupName); + if (!PartInfo.isInstanced) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetGroupMembership( + FHoudiniEngine::Get().GetSession(), + GeoId, PartInfo.id, GroupType,ConvertedGroupName.c_str(), + &OutAllEquals, &OutGroupMembership[0], 0, ElementCount), false); + } + else + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetGroupMembershipOnPackedInstancePart( + FHoudiniEngine::Get().GetSession(), GeoId, PartInfo.id, GroupType, + ConvertedGroupName.c_str(), &OutAllEquals, &OutGroupMembership[0], 0, ElementCount), false); + } + + return true; +} + +bool +FHoudiniEngineUtils::HapiGetAttributeDataAsStringFromInfo( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + const char * InAttribName, + HAPI_AttributeInfo& InAttributeInfo, + TArray& OutData, + int32 InStartIndex, + int32 InCount) +{ + if (!InAttributeInfo.exists) + return false; + + // Handle partial reading of attributes + int32 Start = 0; + if (InStartIndex > 0 && InStartIndex < InAttributeInfo.count) + Start = InStartIndex; + + int32 Count = InAttributeInfo.count; + if (InCount > 0) + { + if ((Start + InCount) <= InAttributeInfo.count) + Count = InCount; + else + Count = InAttributeInfo.count - Start; + } + + // Extract the StringHandles + TArray StringHandles; + StringHandles.SetNumUninitialized(Count * InAttributeInfo.tupleSize); + for (int32 n = 0; n < StringHandles.Num(); n++) + StringHandles[n] = -1; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAttributeStringData( + FHoudiniEngine::Get().GetSession(), + InGeoId, InPartId, InAttribName, + &InAttributeInfo, &StringHandles[0], + Start, Count), false); + + // Set the output data size + OutData.SetNum(StringHandles.Num()); + + // Convert the StringHandles to FString. + // using a map to minimize the number of HAPI calls + FHoudiniEngineString::SHArrayToFStringArray(StringHandles, OutData); + + return true; +} + + +bool +FHoudiniEngineUtils::HapiCheckAttributeExists( + HAPI_NodeId GeoId, HAPI_PartId PartId, + const char * AttribName, HAPI_AttributeOwner Owner) +{ + if (Owner == HAPI_ATTROWNER_INVALID) + { + for (int32 OwnerIdx = 0; OwnerIdx < HAPI_ATTROWNER_MAX; OwnerIdx++) + { + if (HapiCheckAttributeExists(GeoId, PartId, AttribName, (HAPI_AttributeOwner)OwnerIdx)) + { + return true; + } + } + } + else + { + HAPI_AttributeInfo AttribInfo; + FHoudiniApi::AttributeInfo_Init(&AttribInfo); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, AttribName, Owner, &AttribInfo), false); + + return AttribInfo.exists; + } + + return false; +} + +bool +FHoudiniEngineUtils::IsAttributeInstancer(HAPI_NodeId GeoId, HAPI_PartId PartId, EHoudiniInstancerType& OutInstancerType) +{ + // Check for + // - HAPI_UNREAL_ATTRIB_INSTANCE_OVERRIDE (unreal_instance) on points/detail + if (FHoudiniEngineUtils::HapiCheckAttributeExists(GeoId, PartId, HAPI_UNREAL_ATTRIB_INSTANCE_OVERRIDE, HAPI_ATTROWNER_POINT)) + { + OutInstancerType = EHoudiniInstancerType::AttributeInstancer; + return true; + } + + if (FHoudiniEngineUtils::HapiCheckAttributeExists(GeoId, PartId, HAPI_UNREAL_ATTRIB_INSTANCE_OVERRIDE, HAPI_ATTROWNER_DETAIL)) + { + OutInstancerType = EHoudiniInstancerType::AttributeInstancer; + return true; + } + + // - HAPI_UNREAL_ATTRIB_INSTANCE (instance) on points + if (FHoudiniEngineUtils::HapiCheckAttributeExists(GeoId, PartId, HAPI_UNREAL_ATTRIB_INSTANCE, HAPI_ATTROWNER_POINT)) + { + OutInstancerType = EHoudiniInstancerType::OldSchoolAttributeInstancer; + return true; + } + + return false; +} + +bool +FHoudiniEngineUtils::IsValidDataTable(HAPI_NodeId GeoId, HAPI_PartId PartId) +{ + HAPI_PartInfo PartInfo; + HAPI_Result Error = FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), + GeoId, PartId, &PartInfo); + if (Error != HAPI_RESULT_SUCCESS) + { + return false; + } + TArray AttribNameHandles; + AttribNameHandles.SetNum(PartInfo.attributeCounts[HAPI_ATTROWNER_POINT]); + Error = FHoudiniApi::GetAttributeNames(FHoudiniEngine::Get().GetSession(), + GeoId, + PartId, + HAPI_ATTROWNER_POINT, + AttribNameHandles.GetData(), + PartInfo.attributeCounts[HAPI_ATTROWNER_POINT]); + if (Error != HAPI_RESULT_SUCCESS) + { + return false; + } + TArray AttribNames; + FHoudiniEngineString::SHArrayToFStringArray(AttribNameHandles, AttribNames); + for (const FString & Name : AttribNames) + { + if (Name.StartsWith(HAPI_UNREAL_ATTRIB_DATA_TABLE_PREFIX) && Name != HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWNAME && Name != HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWSTRUCT) + { + return true; + } + } + + return false; +} + +bool +FHoudiniEngineUtils::IsLandscapeSpline(HAPI_NodeId GeoId, HAPI_PartId PartId) +{ + // Check for + // - HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE on points/prim/detail with true/non-zero value + TArray OutData; + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, OutData, 0, 1); + + if (!bSuccess) + { + return false; + } + + return OutData.Num() > 0 && static_cast(OutData[0]); +} + +bool +FHoudiniEngineUtils::IsValidHeightfield(HAPI_NodeId GeoId, HAPI_PartId PartId) +{ + // Make sure the volume is a heightfield by ensuring we have + // the "volvis" detail attribute + return FHoudiniEngineUtils::HapiCheckAttributeExists( + GeoId, PartId, "volvis", HAPI_ATTROWNER_DETAIL); +} + +bool +FHoudiniEngineUtils::HapiGetParameterDataAsString( + HAPI_NodeId NodeId, + const std::string& ParmName, + const FString& DefaultValue, + FString& OutValue) +{ + OutValue = DefaultValue; + + // Try to find the parameter by name + HAPI_ParmId ParmId = -1; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), + NodeId, ParmName.c_str(), &ParmId), false); + + if (ParmId < 0) + return false; + + // Get the param info... + HAPI_ParmInfo FoundParamInfo; + FHoudiniApi::ParmInfo_Init(&FoundParamInfo); + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmInfo( + FHoudiniEngine::Get().GetSession(), + NodeId, ParmId, &FoundParamInfo), false); + + // .. and value + HAPI_StringHandle StringHandle; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmStringValues( + FHoudiniEngine::Get().GetSession(), NodeId, false, + &StringHandle, FoundParamInfo.stringValuesIndex, 1), false); + + // Convert the string handle to FString + return FHoudiniEngineString::ToFString(StringHandle, OutValue); +} + +bool +FHoudiniEngineUtils::HapiGetParameterDataAsInteger( + HAPI_NodeId NodeId, + const std::string& ParmName, + int32 DefaultValue, + int32& OutValue) +{ + OutValue = DefaultValue; + + // Try to find the parameter by its name + HAPI_ParmId ParmId = -1; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), + NodeId, ParmName.c_str(), &ParmId), false); + + if (ParmId < 0) + return false; + + // Get the param info... + HAPI_ParmInfo FoundParmInfo; + FHoudiniApi::ParmInfo_Init(&FoundParmInfo); + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmInfo( + FHoudiniEngine::Get().GetSession(), + NodeId, ParmId, &FoundParmInfo), false); + + // .. and value + int32 Value = DefaultValue; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmIntValues( + FHoudiniEngine::Get().GetSession(), NodeId, &Value, + FoundParmInfo.intValuesIndex, 1), false); + + OutValue = Value; + + return true; +} + + +bool +FHoudiniEngineUtils::HapiGetParameterDataAsFloat( + HAPI_NodeId NodeId, + const std::string& ParmName, + float DefaultValue, + float& OutValue) +{ + OutValue = DefaultValue; + + // Try to find the parameter by its name + HAPI_ParmId ParmId = -1; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), + NodeId, ParmName.c_str(), &ParmId), false); + + if (ParmId < 0) + return false; + + // Get the param info... + HAPI_ParmInfo FoundParmInfo; + FHoudiniApi::ParmInfo_Init(&FoundParmInfo); + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmInfo( + FHoudiniEngine::Get().GetSession(), + NodeId, ParmId, &FoundParmInfo), false); + + // .. and value + float Value = DefaultValue; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmFloatValues( + FHoudiniEngine::Get().GetSession(), NodeId, &Value, + FoundParmInfo.floatValuesIndex, 1), false); + + OutValue = Value; + + return true; +} + +HAPI_ParmId +FHoudiniEngineUtils::HapiFindParameterByName(HAPI_NodeId InNodeId, const std::string& InParmName, HAPI_ParmInfo& OutFoundParmInfo) +{ + // Try to find the parameter by its name + HAPI_ParmId ParmId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), + InNodeId, InParmName.c_str(), &ParmId), -1); + + if (ParmId < 0) + return -1; + + FHoudiniApi::ParmInfo_Init(&OutFoundParmInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmInfo( + FHoudiniEngine::Get().GetSession(), + InNodeId, ParmId, &OutFoundParmInfo), -1); + + return ParmId; +} + +HAPI_ParmId +FHoudiniEngineUtils::HapiFindParameterByTag(HAPI_NodeId InNodeId, const std::string& InParmTag, HAPI_ParmInfo& OutFoundParmInfo) +{ + // Try to find the parameter by its tag + HAPI_ParmId ParmId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmWithTag( + FHoudiniEngine::Get().GetSession(), + InNodeId, InParmTag.c_str(), &ParmId), -1); + + if (ParmId < 0) + return -1; + + FHoudiniApi::ParmInfo_Init(&OutFoundParmInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmInfo( + FHoudiniEngine::Get().GetSession(), + InNodeId, ParmId, &OutFoundParmInfo), -1); + + return ParmId; +} + +int32 +FHoudiniEngineUtils::HapiGetAttributeOfType( + HAPI_NodeId GeoId, + HAPI_NodeId PartId, + const HAPI_AttributeOwner& AttributeOwner, + const HAPI_AttributeTypeInfo& AttributeType, + TArray& MatchingAttributesInfo, + TArray& MatchingAttributesName) +{ + int32 NumberOfAttributeFound = 0; + + // Get the part infos + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetPartInfo( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, &PartInfo), NumberOfAttributeFound); + + // Get All attribute names for that part + int32 nAttribCount = PartInfo.attributeCounts[AttributeOwner]; + + TArray AttribNameSHArray; + AttribNameSHArray.SetNum(nAttribCount); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAttributeNames( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, AttributeOwner, + AttribNameSHArray.GetData(), nAttribCount), NumberOfAttributeFound); + + TArray AttribNameArray; + FHoudiniEngineString::SHArrayToFStringArray(AttribNameSHArray, AttribNameArray); + + // Iterate on all the attributes, and get their part infos to get their type + for (int32 Idx = 0; Idx < AttribNameArray.Num(); Idx++) + { + FString HapiString = AttribNameArray[Idx]; + + // ... then the attribute info + HAPI_AttributeInfo AttrInfo; + FHoudiniApi::AttributeInfo_Init(&AttrInfo); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*HapiString), + AttributeOwner, &AttrInfo)) + continue; + + if (!AttrInfo.exists) + continue; + + // ... check the type + if (AttrInfo.typeInfo != AttributeType) + continue; + + MatchingAttributesInfo.Add(AttrInfo); + MatchingAttributesName.Add(HapiString); + + NumberOfAttributeFound++; + } + + return NumberOfAttributeFound; +} + +HAPI_PartInfo +FHoudiniEngineUtils::ToHAPIPartInfo(const FHoudiniPartInfo& InHPartInfo) +{ + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + + PartInfo.id = InHPartInfo.PartId; + //PartInfo.nameSH = InHPartInfo.Name; + + switch (InHPartInfo.Type) + { + case EHoudiniPartType::Mesh: + PartInfo.type = HAPI_PARTTYPE_MESH; + break; + case EHoudiniPartType::Curve: + PartInfo.type = HAPI_PARTTYPE_CURVE; + break; + case EHoudiniPartType::Instancer: + PartInfo.type = HAPI_PARTTYPE_INSTANCER; + break; + case EHoudiniPartType::Volume: + PartInfo.type = HAPI_PARTTYPE_VOLUME; + break; + default: + case EHoudiniPartType::Invalid: + PartInfo.type = HAPI_PARTTYPE_INVALID; + break; + } + + PartInfo.faceCount = InHPartInfo.FaceCount; + PartInfo.vertexCount = InHPartInfo.VertexCount; + PartInfo.pointCount = InHPartInfo.PointCount; + + PartInfo.attributeCounts[HAPI_ATTROWNER_POINT] = InHPartInfo.PointAttributeCounts; + PartInfo.attributeCounts[HAPI_ATTROWNER_VERTEX] = InHPartInfo.VertexAttributeCounts; + PartInfo.attributeCounts[HAPI_ATTROWNER_PRIM] = InHPartInfo.PrimitiveAttributeCounts; + PartInfo.attributeCounts[HAPI_ATTROWNER_DETAIL] = InHPartInfo.DetailAttributeCounts; + + PartInfo.isInstanced = InHPartInfo.bIsInstanced; + + PartInfo.instancedPartCount = InHPartInfo.InstancedPartCount; + PartInfo.instanceCount = InHPartInfo.InstanceCount; + + PartInfo.hasChanged = InHPartInfo.bHasChanged; + + return PartInfo; +} + +int32 +FHoudiniEngineUtils::AddMeshSocketsToArray_DetailAttribute( + HAPI_NodeId GeoId, + HAPI_PartId PartId, + TArray< FHoudiniMeshSocket >& AllSockets, + bool isPackedPrim) +{ + int32 FoundSocketCount = 0; + + // Attributes we are interested in: + TArray Positions; + + // Rotation + bool bHasRotation = false; + TArray Rotations; + + // Scale + bool bHasScale = false; + TArray Scales; + + // Socket Name + bool bHasNames = false; + TArray Names; + + // Socket Actor + bool bHasActors = false; + TArray Actors; + + // Socket Tags + bool bHasTags = false; + TArray Tags; + + // Lambda function for creating the socket and adding it to the array + // Shared between the by Attribute / by Group methods + auto AddSocketToArray = [&](int32 PointIdx) + { + FHoudiniMeshSocket CurrentSocket; + FVector currentPosition = FVector::ZeroVector; + if (Positions.IsValidIndex(PointIdx * 3 + 2)) + { + currentPosition.X = Positions[PointIdx * 3] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + currentPosition.Y = Positions[PointIdx * 3 + 2] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + currentPosition.Z = Positions[PointIdx * 3 + 1] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + + FVector currentScale = FVector::OneVector; + if (bHasScale && Scales.IsValidIndex(PointIdx * 3 + 2)) + { + currentScale.X = Scales[PointIdx * 3]; + currentScale.Y = Scales[PointIdx * 3 + 2]; + currentScale.Z = Scales[PointIdx * 3 + 1]; + } + + FQuat currentRotation = FQuat::Identity; + if (bHasRotation && Rotations.IsValidIndex(PointIdx * 4 + 3)) + { + currentRotation.X = Rotations[PointIdx * 4]; + currentRotation.Y = Rotations[PointIdx * 4 + 2]; + currentRotation.Z = Rotations[PointIdx * 4 + 1]; + currentRotation.W = -Rotations[PointIdx * 4 + 3]; + } + + if (bHasNames && Names.IsValidIndex(PointIdx)) + CurrentSocket.Name = Names[PointIdx]; + + if (bHasActors && Actors.IsValidIndex(PointIdx)) + CurrentSocket.Actor = Actors[PointIdx]; + + if (bHasTags && Tags.IsValidIndex(PointIdx)) + CurrentSocket.Tag = Tags[PointIdx]; + + // If the scale attribute wasn't set on all socket, we might end up + // with a zero scale socket, avoid that. + if (currentScale == FVector::ZeroVector) + currentScale = FVector::OneVector; + + CurrentSocket.Transform.SetLocation(currentPosition); + CurrentSocket.Transform.SetRotation(currentRotation); + CurrentSocket.Transform.SetScale3D(currentScale); + + // We want to make sure we're not adding the same socket multiple times + AllSockets.AddUnique(CurrentSocket); + + FoundSocketCount++; + + return true; + }; + + + // Lambda function for reseting the arrays/attributes + auto ResetArraysAndAttr = [&]() + { + Positions.Empty(); + + bHasRotation = false; + Rotations.Empty(); + + bHasScale = false; + Scales.Empty(); + + bHasNames = false; + Names.Empty(); + + bHasActors = false; + Actors.Empty(); + + bHasTags = false; + Tags.Empty(); + }; + + //------------------------------------------------------------------------- + // FIND SOCKETS BY DETAIL ATTRIBUTES + //------------------------------------------------------------------------- + + int32 SocketIdx = 0; + bool HasSocketAttributes = true; + while (HasSocketAttributes) + { + // Build the current socket's prefix + FString SocketAttrPrefix = TEXT(HAPI_UNREAL_ATTRIB_MESH_SOCKET_PREFIX) + FString::FromInt(SocketIdx); + + // Reset the arrays and attributes + ResetArraysAndAttr(); + + // Retrieve position data. + FString SocketPosAttr = SocketAttrPrefix + TEXT("_pos"); + + FHoudiniHapiAccessor Accessor(GeoId, PartId, TCHAR_TO_ANSI(*SocketPosAttr)); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Positions); + if (!bSuccess) + { + // No need to keep looking for socket attributes + HasSocketAttributes = false; + break; + } + + // Retrieve rotation data. + FString SocketRotAttr = SocketAttrPrefix + TEXT("_rot"); + + Accessor.Init(GeoId, PartId, TCHAR_TO_ANSI(*SocketRotAttr)); + if (Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Rotations)) + bHasRotation = true; + + // Retrieve scale data. + FString SocketScaleAttr = SocketAttrPrefix + TEXT("_scale"); + Accessor.Init(GeoId, PartId, TCHAR_TO_ANSI(*SocketScaleAttr)); + if (Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Scales)) + bHasScale = true; + + // Retrieve mesh socket names. + FString SocketNameAttr = SocketAttrPrefix + TEXT("_name"); + Accessor.Init(GeoId, PartId, TCHAR_TO_ANSI(*SocketNameAttr)); + bHasNames = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, Names); + + // Retrieve mesh socket actor. + FString SocketActorAttr = SocketAttrPrefix + TEXT("_actor"); + Accessor.Init(GeoId, PartId, TCHAR_TO_ANSI(*SocketActorAttr)); + bHasActors = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, Actors); + + // Retrieve mesh socket tags. + FString SocketTagAttr = SocketAttrPrefix + TEXT("_tag"); + Accessor.Init(GeoId, PartId, TCHAR_TO_ANSI(*SocketTagAttr)); + bHasTags = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, Tags); + + // Add the socket to the array + AddSocketToArray(0); + + // Try to find the next socket + SocketIdx++; + } + + return FoundSocketCount; +} + + +int32 +FHoudiniEngineUtils::AddMeshSocketsToArray_Group( + HAPI_NodeId GeoId, + HAPI_PartId PartId, + TArray& AllSockets, + bool isPackedPrim) +{ + TArray Positions; + bool bHasRotation = false; + TArray Rotations; + bool bHasScale = false; + TArray Scales; + bool bHasNormals = false; + TArray Normals; + bool bHasNames = false; + TArray Names; + bool bHasActors = false; + TArray Actors; + bool bHasTags = false; + TArray Tags; + + // Lambda function for creating the socket and adding it to the array + // Shared between the by Attribute / by Group methods + int32 FoundSocketCount = 0; + auto AddSocketToArray = [&](int32 PointIdx) + { + FHoudiniMeshSocket CurrentSocket; + FVector currentPosition = FVector::ZeroVector; + if (Positions.IsValidIndex(PointIdx * 3 + 2)) + { + currentPosition.X = Positions[PointIdx * 3] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + currentPosition.Y = Positions[PointIdx * 3 + 2] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + currentPosition.Z = Positions[PointIdx * 3 + 1] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + + FVector currentScale = FVector::OneVector; + if (bHasScale && Scales.IsValidIndex(PointIdx * 3 + 2)) + { + currentScale.X = Scales[PointIdx * 3]; + currentScale.Y = Scales[PointIdx * 3 + 2]; + currentScale.Z = Scales[PointIdx * 3 + 1]; + } + + FQuat currentRotation = FQuat::Identity; + if (bHasRotation && Rotations.IsValidIndex(PointIdx * 4 + 3)) + { + currentRotation.X = Rotations[PointIdx * 4]; + currentRotation.Y = Rotations[PointIdx * 4 + 2]; + currentRotation.Z = Rotations[PointIdx * 4 + 1]; + currentRotation.W = -Rotations[PointIdx * 4 + 3]; + } + else if (bHasNormals && Normals.IsValidIndex(PointIdx * 3 + 2)) + { + FVector vNormal; + vNormal.X = Normals[PointIdx * 3]; + vNormal.Y = Normals[PointIdx * 3 + 2]; + vNormal.Z = Normals[PointIdx * 3 + 1]; + + if (vNormal != FVector::ZeroVector) + currentRotation = FQuat::FindBetween(FVector::UpVector, vNormal); + } + + if (bHasNames && Names.IsValidIndex(PointIdx)) + CurrentSocket.Name = Names[PointIdx]; + + if (bHasActors && Actors.IsValidIndex(PointIdx)) + CurrentSocket.Actor = Actors[PointIdx]; + + if (bHasTags && Tags.IsValidIndex(PointIdx)) + CurrentSocket.Tag = Tags[PointIdx]; + + // If the scale attribute wasn't set on all socket, we might end up + // with a zero scale socket, avoid that. + if (currentScale == FVector::ZeroVector) + currentScale = FVector::OneVector; + + CurrentSocket.Transform.SetLocation(currentPosition); + CurrentSocket.Transform.SetRotation(currentRotation); + CurrentSocket.Transform.SetScale3D(currentScale); + + // We want to make sure we're not adding the same socket multiple times + AllSockets.AddUnique(CurrentSocket); + + FoundSocketCount++; + + return true; + }; + + + // Lambda function for reseting the arrays/attributes + auto ResetArraysAndAttr = [&]() + { + // Position + Positions.Empty(); + + // Rotation + bHasRotation = false; + Rotations.Empty(); + + // Scale + bHasScale = false; + Scales.Empty(); + + // When using socket groups, we can also get the sockets rotation from the normal + bHasNormals = false; + Normals.Empty(); + + // Socket Name + bHasNames = false; + Names.Empty(); + + // Socket Actor + bHasActors = false; + Actors.Empty(); + + // Socket Tags + bHasTags = false; + Tags.Empty(); + }; + + //------------------------------------------------------------------------- + // FIND SOCKETS BY POINT GROUPS + //------------------------------------------------------------------------- + + // Get object / geo group memberships for primitives. + TArray GroupNames; + if (!FHoudiniEngineUtils::HapiGetGroupNames( + GeoId, PartId, HAPI_GROUPTYPE_POINT, isPackedPrim, GroupNames)) + { + HOUDINI_LOG_MESSAGE(TEXT("GetMeshSocketList: Geo [%d] Part [%d] non-fatal error reading point group names"), GeoId, PartId); + } + + // First, we want to make sure we have at least one socket group before continuing + bool bHasSocketGroup = false; + for (int32 GeoGroupNameIdx = 0; GeoGroupNameIdx < GroupNames.Num(); ++GeoGroupNameIdx) + { + const FString & GroupName = GroupNames[GeoGroupNameIdx]; + if (GroupName.StartsWith(HAPI_UNREAL_GROUP_SOCKET_PREFIX, ESearchCase::IgnoreCase) + || GroupName.StartsWith(HAPI_UNREAL_GROUP_SOCKET_PREFIX_OLD, ESearchCase::IgnoreCase)) + { + bHasSocketGroup = true; + break; + } + } + + if (!bHasSocketGroup) + return FoundSocketCount; + + // Get the part info + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetPartInfo( + FHoudiniEngine::Get().GetSession(), GeoId, PartId, &PartInfo)) + return false; + + // Reset the data arrays and attributes + ResetArraysAndAttr(); + + FHoudiniHapiAccessor Accessor; + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_POSITION); + if (!Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, Positions)) + return false; + + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_ROTATION); + if (Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, Rotations)) + bHasRotation = true; + + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_NORMAL); + if (Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, Normals)) + bHasNormals = true; + + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_SCALE); + if (Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, Scales)) + bHasScale = true; + + // Retrieve mesh socket names. + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_MESH_SOCKET_NAME); + bHasNames = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, Names); + if (!bHasNames) + { + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_MESH_SOCKET_NAME_OLD); + bHasNames = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, Names); + } + + // Retrieve mesh actors + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_MESH_SOCKET_ACTOR); + bHasActors = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, Actors); + if (!bHasActors) + { + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_MESH_SOCKET_ACTOR_OLD); + bHasActors = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, Actors); + } + + // Retrieve mesh socket tags. + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_MESH_SOCKET_TAG); + bHasTags = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, Tags); + if (!bHasTags) + { + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_MESH_SOCKET_TAG_OLD); + bHasTags = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, Tags); + } + + // Extracting Sockets vertices + for (int32 GeoGroupNameIdx = 0; GeoGroupNameIdx < GroupNames.Num(); ++GeoGroupNameIdx) + { + const FString & GroupName = GroupNames[GeoGroupNameIdx]; + if (!GroupName.StartsWith(HAPI_UNREAL_GROUP_SOCKET_PREFIX, ESearchCase::IgnoreCase) + && !GroupName.StartsWith(HAPI_UNREAL_GROUP_SOCKET_PREFIX_OLD, ESearchCase::IgnoreCase)) + continue; + + bool AllEquals = false; + TArray< int32 > PointGroupMembership; + FHoudiniEngineUtils::HapiGetGroupMembership( + GeoId, PartInfo, HAPI_GROUPTYPE_POINT, GroupName, PointGroupMembership, AllEquals); + + // Go through all primitives. + for (int32 PointIdx = 0; PointIdx < PointGroupMembership.Num(); ++PointIdx) + { + if (PointGroupMembership[PointIdx] == 0) + { + if (AllEquals) + break; + else + continue; + } + + // Add the corresponding socket to the array + AddSocketToArray(PointIdx); + } + } + + return FoundSocketCount; +} + +bool +FHoudiniEngineUtils::AddMeshSocketsToStaticMesh( + UStaticMesh* StaticMesh, + TArray& AllSockets, + bool CleanImportSockets) +{ + if (!IsValid(StaticMesh)) + return false; + + // Remove the sockets from the previous cook! + if (CleanImportSockets) + { + StaticMesh->Sockets.RemoveAll([=](UStaticMeshSocket* Socket) { return Socket ? Socket->bSocketCreatedAtImport : true; }); + } + + if (AllSockets.Num() <= 0) + return true; + + // Having sockets with empty names can lead to various issues, so we'll create one now + for (int32 Idx = 0; Idx < AllSockets.Num(); ++Idx) + { + // Assign the unnamed sockets with default names + if (AllSockets[Idx].Name.IsEmpty()) + AllSockets[Idx].Name = TEXT("Socket ") + FString::FromInt(Idx); + } + + // ensure the socket names are unique. (Unreal will use the first socket if multiple socket have the same name) + for (int32 Idx_i = 0; Idx_i < AllSockets.Num(); ++Idx_i) + { + int32 Count = 0; + for (int32 Idx_j = Idx_i + 1; Idx_j < AllSockets.Num(); ++Idx_j) + { + if (AllSockets[Idx_i].Name.Equals(AllSockets[Idx_j].Name)) + { + Count += 1; + AllSockets[Idx_j].Name = AllSockets[Idx_j].Name + "_" + FString::FromInt(Count); + } + } + } + + // Clear all the sockets of the output static mesh. + StaticMesh->Sockets.Empty(); + + for (int32 nSocket = 0; nSocket < AllSockets.Num(); nSocket++) + { + // Create a new Socket + UStaticMeshSocket* Socket = NewObject(StaticMesh); + if (!IsValid(Socket)) + continue; + + Socket->RelativeLocation = AllSockets[nSocket].Transform.GetLocation(); + Socket->RelativeRotation = FRotator(AllSockets[nSocket].Transform.GetRotation()); + Socket->RelativeScale = AllSockets[nSocket].Transform.GetScale3D(); + Socket->SocketName = FName(*AllSockets[nSocket].Name); + + // Socket Tag + FString Tag; + if (!AllSockets[nSocket].Tag.IsEmpty()) + Tag = AllSockets[nSocket].Tag; + + // The actor will be stored temporarily in the socket's Tag as we need a StaticMeshComponent to add an actor to the socket + if(!AllSockets[nSocket].Actor.IsEmpty()) + Tag += TEXT("|") + AllSockets[nSocket].Actor; + + Socket->Tag = Tag; + Socket->bSocketCreatedAtImport = true; + + StaticMesh->Sockets.Add(Socket); + } + + return true; +} + +bool +FHoudiniEngineUtils::CreateAttributesFromTags( + HAPI_NodeId NodeId, + HAPI_PartId PartId, + const TArray& Tags ) +{ + if (Tags.Num() <= 0) + return false; + + HAPI_Result Result = HAPI_RESULT_FAILURE; + + // Get the destination part info + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetPartInfo( + FHoudiniEngine::Get().GetSession(), NodeId, PartId, &PartInfo), false); + + bool NeedToCommitGeo = false; + for (int32 TagIdx = 0; TagIdx < Tags.Num(); TagIdx++) + { + FString TagString; + Tags[TagIdx].ToString(TagString); + SanitizeHAPIVariableName(TagString); + + // Create a primitive attribute for the tag + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + + AttributeInfo.count = PartInfo.faceCount; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + AttributeInfo.typeInfo = HAPI_ATTRIBUTE_TYPE_NONE; + + FString AttributeName = TEXT(HAPI_UNREAL_ATTRIB_TAG_PREFIX) + FString::FromInt(TagIdx); + AttributeName.RemoveSpacesInline(); + + Result = FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, TCHAR_TO_ANSI(*AttributeName), &AttributeInfo); + + if (Result != HAPI_RESULT_SUCCESS) + continue; + + TArray TagStr; + TagStr.Add(FHoudiniEngineUtils::ExtractRawString(TagString)); + + Result = FHoudiniApi::SetAttributeStringData( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, TCHAR_TO_ANSI(*AttributeName), &AttributeInfo, + TagStr.GetData(), 0, AttributeInfo.count); + + if (HAPI_RESULT_SUCCESS == Result) + NeedToCommitGeo = true; + + // Free memory for allocated by ExtractRawString + FHoudiniEngineUtils::FreeRawStringMemory(TagStr); + } + + return NeedToCommitGeo; +} + +bool +FHoudiniEngineUtils::CreateGroupsFromTags( + HAPI_NodeId NodeId, + HAPI_PartId PartId, + const TArray& Tags ) +{ + if (Tags.Num() <= 0) + return false; + + HAPI_Result Result = HAPI_RESULT_FAILURE; + + // Get the destination part info + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetPartInfo( + FHoudiniEngine::Get().GetSession(), NodeId, PartId, &PartInfo), false); + + bool NeedToCommitGeo = false; + for (int32 TagIdx = 0; TagIdx < Tags.Num(); TagIdx++) + { + FString TagString; + Tags[TagIdx].ToString(TagString); + SanitizeHAPIVariableName(TagString); + + const char * TagStr = FHoudiniEngineUtils::ExtractRawString(TagString); + + // Create a primitive group for this tag + if ( HAPI_RESULT_SUCCESS == FHoudiniApi::AddGroup( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_GROUPTYPE_PRIM, TagStr) ) + { + // Set the group's Memberships + TArray GroupArray; + GroupArray.SetNumUninitialized(PartInfo.faceCount); + for (int32 n = 0; n < GroupArray.Num(); n++) + GroupArray[n] = 1; + + if ( HAPI_RESULT_SUCCESS == FHoudiniApi::SetGroupMembership( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, HAPI_GROUPTYPE_PRIM, TagStr, + GroupArray.GetData(), 0, PartInfo.faceCount) ) + { + NeedToCommitGeo = true; + } + } + + // Free memory allocated by ExtractRawString() + FHoudiniEngineUtils::FreeRawStringMemory(TagStr); + } + + return NeedToCommitGeo; +} + +/* +bool +FHoudiniEngineUtils::ForceValidVariableNameInline(FString& InOutString) +{ + // Reproduces the behaviour of UT_String::forceValidVariableName() + // Return true if a change occurred + bool bHasChanged = false; + + // Replace any special character by `_` + for (TCHAR& CurrentChar : InOutString) + { + if (!FChar::IsAlnum(CurrentChar) && !FChar::IsUnderscore(CurrentChar)) + { + CurrentChar = '_'; + bHasChanged = true; + } + } + + // If the string starts with a digit, clean it up by prefixing an _. + if (FChar::IsDigit(InOutString[0])) + { + InOutString.InsertAt(0, '_'); + bHasChanged = true; + } + + return bHasChanged; +} +*/ + +bool +FHoudiniEngineUtils::SanitizeHAPIVariableName(FString& String) +{ + // Only keep alphanumeric characters, underscores + // Also, if the first character is a digit, append an underscore at the beginning + TArray& StrArray = String.GetCharArray(); + if (StrArray.Num() <= 0) + return false; + + bool bHasChanged = false; + for (auto& CurChar : StrArray) + { + const bool bIsValid = (CurChar >= TEXT('A') && CurChar <= TEXT('Z')) + || (CurChar >= TEXT('a') && CurChar <= TEXT('z')) + || (CurChar >= TEXT('0') && CurChar <= TEXT('9')) + || (CurChar == TEXT('_')) || (CurChar == TEXT('\0')); + + if(bIsValid) + continue; + + CurChar = TEXT('_'); + bHasChanged = true; + } + + if (StrArray.Num() > 0) + { + TCHAR FirstChar = StrArray[0]; + if (FirstChar >= TEXT('0') && FirstChar <= TEXT('9')) + { + StrArray.Insert(TEXT('_'), 0); + bHasChanged = true; + } + } + + return bHasChanged; +} + + +int32 +FHoudiniEngineUtils::GetGenericAttributeList( + HAPI_NodeId InGeoNodeId, + HAPI_PartId InPartId, + const FString& InGenericAttributePrefix, + TArray& OutFoundAttributes, + const HAPI_AttributeOwner& AttributeOwner, + int32 InAttribIndex) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GetGenericAttributeList); + + // Get the part info to get the attribute counts for the specified owner + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetPartInfo( + FHoudiniEngine::Get().GetSession(), InGeoNodeId, InPartId, &PartInfo), false); + + int32 nAttribCount = PartInfo.attributeCounts[AttributeOwner]; + + // Get all attribute names for that part + TArray AttribNameSHArray; + AttribNameSHArray.SetNum(nAttribCount); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetAttributeNames( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, AttributeOwner, + AttribNameSHArray.GetData(), nAttribCount)) + { + return 0; + } + + // For everything but detail attribute, + // if an attribute index was specified, only extract the attribute value for that specific index + // if not, extract all values for the given attribute + bool HandleSplit = false; + int32 AttribIndex = -1; + if ((AttributeOwner != HAPI_ATTROWNER_DETAIL) && (InAttribIndex != -1)) + { + // The index has already been specified so we'll use it + HandleSplit = true; + AttribIndex = InAttribIndex; + } + + int32 FoundCount = 0; + for (int32 Idx = 0; Idx < AttribNameSHArray.Num(); ++Idx) + { + int32 AttribNameSH = (int32)AttribNameSHArray[Idx]; + FString AttribName = TEXT(""); + FHoudiniEngineString::ToFString(AttribNameSH, AttribName); + if (!AttribName.StartsWith(InGenericAttributePrefix, ESearchCase::IgnoreCase)) + continue; + + // Get the Attribute Info + HAPI_AttributeInfo AttribInfo; + FHoudiniApi::AttributeInfo_Init(&AttribInfo); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, + H_TCHAR_TO_UTF8(*AttribName), AttributeOwner, &AttribInfo)) + { + // failed to get that attribute's info + continue; + } + + int32 AttribStart = 0; + int32 AttribCount = AttribInfo.count; + if (HandleSplit) + { + // For split primitives, we need to only get only one value for the proper split prim + // Make sure that the split index is valid + if (AttribIndex >= 0 && AttribIndex < AttribInfo.count) + { + AttribStart = AttribIndex; + AttribCount = 1; + } + } + + // + FHoudiniGenericAttribute CurrentGenericAttribute; + // Remove the generic attribute prefix + CurrentGenericAttribute.AttributeName = AttribName.Right(AttribName.Len() - InGenericAttributePrefix.Len()); + + CurrentGenericAttribute.AttributeOwner = (EAttribOwner)AttribInfo.owner; + + // Get the attribute type and tuple size + CurrentGenericAttribute.AttributeType = (EAttribStorageType)AttribInfo.storage; + CurrentGenericAttribute.AttributeCount = AttribInfo.count; + CurrentGenericAttribute.AttributeTupleSize = AttribInfo.tupleSize; + + if (CurrentGenericAttribute.AttributeType == EAttribStorageType::FLOAT64) + { + // Initialize the value array + CurrentGenericAttribute.DoubleValues.SetNumZeroed(AttribCount * AttribInfo.tupleSize); + + // Get the value(s) + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetAttributeFloat64Data( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, + H_TCHAR_TO_UTF8(*AttribName), &AttribInfo, 0, + CurrentGenericAttribute.DoubleValues.GetData(), + AttribStart, AttribCount)) + { + // failed to get that attribute's data + continue; + } + } + else if (CurrentGenericAttribute.AttributeType == EAttribStorageType::FLOAT) + { + // Initialize the value array + TArray FloatValues; + FloatValues.SetNumZeroed(AttribCount * AttribInfo.tupleSize); + + // Get the value(s) + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, + H_TCHAR_TO_UTF8(*AttribName), &AttribInfo, + 0, FloatValues.GetData(), + AttribStart, AttribCount)) + { + // failed to get that attribute's data + continue; + } + + // Convert them to double + CurrentGenericAttribute.DoubleValues.SetNumZeroed(AttribCount * AttribInfo.tupleSize); + for (int32 n = 0; n < FloatValues.Num(); n++) + CurrentGenericAttribute.DoubleValues[n] = (double)FloatValues[n]; + + } + else if (CurrentGenericAttribute.AttributeType == EAttribStorageType::INT64) + { +#if PLATFORM_LINUX + // On Linux, we unfortunately cannot guarantee that int64 and HAPI_Int64 + // are of the same type, to properly read the value, we must first check the + // size, then either cast them (if sizes match) or convert the values (if sizes don't match) + if (sizeof(int64) != sizeof(HAPI_Int64)) + { + // int64 and HAPI_Int64 are of different size, we need to cast + TArray HAPIIntValues; + HAPIIntValues.SetNumZeroed(AttribCount * AttribInfo.tupleSize); + + // Get the value(s) + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetAttributeInt64Data( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, + H_TCHAR_TO_UTF8(*AttribName), &AttribInfo, + 0, HAPIIntValues.GetData(), + AttribStart, AttribCount)) + { + // failed to get that attribute's data + continue; + } + + // Convert them to int64 + CurrentGenericAttribute.IntValues.SetNumZeroed(AttribCount * AttribInfo.tupleSize); + for (int32 n = 0; n < HAPIIntValues.Num(); n++) + CurrentGenericAttribute.IntValues[n] = (int64)HAPIIntValues[n]; + } + else + { + // Initialize the value array + CurrentGenericAttribute.IntValues.SetNumZeroed(AttribCount * AttribInfo.tupleSize); + + // Get the value(s) with a reinterpret_cast since sizes match + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetAttributeInt64Data( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, + H_TCHAR_TO_UTF8(*AttribName), &AttribInfo, + 0, reinterpret_cast(CurrentGenericAttribute.IntValues.GetData()), + AttribStart, AttribCount)) + { + // failed to get that attribute's data + continue; + } + } +#else + // Initialize the value array + CurrentGenericAttribute.IntValues.SetNumZeroed(AttribCount * AttribInfo.tupleSize); + + // Get the value(s) + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetAttributeInt64Data( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, + H_TCHAR_TO_UTF8(*AttribName), &AttribInfo, + 0, CurrentGenericAttribute.IntValues.GetData(), + AttribStart, AttribCount)) + { + // failed to get that attribute's data + continue; + } +#endif + } + else if (CurrentGenericAttribute.AttributeType == EAttribStorageType::INT) + { + // Initialize the value array + TArray IntValues; + IntValues.SetNumZeroed(AttribCount * AttribInfo.tupleSize); + + // Get the value(s) + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetAttributeIntData( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, + H_TCHAR_TO_UTF8(*AttribName), &AttribInfo, + 0, IntValues.GetData(), + AttribStart, AttribCount)) + { + // failed to get that attribute's data + continue; + } + + // Convert them to int64 + CurrentGenericAttribute.IntValues.SetNumZeroed(AttribCount * AttribInfo.tupleSize); + for (int32 n = 0; n < IntValues.Num(); n++) + CurrentGenericAttribute.IntValues[n] = (int64)IntValues[n]; + + } + else if (CurrentGenericAttribute.AttributeType == EAttribStorageType::STRING) + { + // Initialize a string handle array + TArray HapiSHArray; + HapiSHArray.SetNumZeroed(AttribCount * AttribInfo.tupleSize); + + // Get the string handle(s) + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetAttributeStringData( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, + H_TCHAR_TO_UTF8(*AttribName), &AttribInfo, + HapiSHArray.GetData(), + AttribStart, AttribCount)) + { + // failed to get that attribute's data + continue; + } + + // Convert the String Handles to FStrings + // using a map to minimize the number of HAPI calls + FHoudiniEngineString::SHArrayToFStringArray(HapiSHArray, CurrentGenericAttribute.StringValues); + } + else + { + // Unsupported type, skipping! + continue; + } + + // We can add the UPropertyAttribute to the array + OutFoundAttributes.Add(CurrentGenericAttribute); + FoundCount++; + } + + return FoundCount; +} + + +bool +FHoudiniEngineUtils::GetGenericPropertiesAttributes( + HAPI_NodeId InGeoNodeId, + HAPI_PartId InPartId, + const bool InbFindDetailAttributes, + int32 InFirstValidPrimIndex, + int32 InFirstValidVertexIndex, + int32 InFirstValidPointIndex, + TArray& OutPropertyAttributes) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::GetGenericPropertiesAttributes); + + int32 FoundCount = 0; + // List all the generic property detail attributes ... + if (InbFindDetailAttributes) + { + FoundCount += FHoudiniEngineUtils::GetGenericAttributeList( + InGeoNodeId, InPartId, HAPI_UNREAL_ATTRIB_GENERIC_UPROP_PREFIX, OutPropertyAttributes, HAPI_ATTROWNER_DETAIL); + } + + // .. then the primitive property attributes for the given prim + if (InFirstValidPrimIndex != INDEX_NONE) + { + FoundCount += FHoudiniEngineUtils::GetGenericAttributeList( + InGeoNodeId, InPartId, HAPI_UNREAL_ATTRIB_GENERIC_UPROP_PREFIX, OutPropertyAttributes, HAPI_ATTROWNER_PRIM, InFirstValidPrimIndex); + } + + if (InFirstValidVertexIndex != INDEX_NONE) + { + // .. then finally, point uprop attributes for the given point + FoundCount += FHoudiniEngineUtils::GetGenericAttributeList( + InGeoNodeId, InPartId, HAPI_UNREAL_ATTRIB_GENERIC_UPROP_PREFIX, OutPropertyAttributes, HAPI_ATTROWNER_VERTEX, InFirstValidVertexIndex); + } + + if (InFirstValidPointIndex != INDEX_NONE) + { + // .. then finally, point uprop attributes for the given point + FoundCount += FHoudiniEngineUtils::GetGenericAttributeList( + InGeoNodeId, InPartId, HAPI_UNREAL_ATTRIB_GENERIC_UPROP_PREFIX, OutPropertyAttributes, HAPI_ATTROWNER_POINT, InFirstValidPointIndex); + } + + return FoundCount > 0; +} + +bool +FHoudiniEngineUtils::UpdateGenericPropertiesAttributes( + UObject* InObject, + const TArray& InAllPropertyAttributes, + int32 AtIndex, + bool bInDeferPostEditChangePropertyCalls, + const FHoudiniGenericAttribute::FFindPropertyFunctionType& InProcessFunction) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::UpdateGenericPropertiesAttributes); + if (!IsValid(InObject)) + return false; + + // Iterate over the found Property attributes + TArray ChangedProperties; + if (bInDeferPostEditChangePropertyCalls) + ChangedProperties.Reserve(InAllPropertyAttributes.Num()); + + int32 NumSuccess = 0; + for (const auto& CurrentPropAttribute : InAllPropertyAttributes) + { + // Update the current Property Attribute + if (!FHoudiniGenericAttribute::UpdatePropertyAttributeOnObject(InObject, CurrentPropAttribute, AtIndex, bInDeferPostEditChangePropertyCalls, &ChangedProperties, InProcessFunction)) + continue; + + // Success! + NumSuccess++; +#if defined(HOUDINI_ENGINE_LOGGING) + const FString ClassName = InObject->GetClass() ? InObject->GetClass()->GetName() : TEXT("Object"); + const FString ObjectName = InObject->GetName(); + HOUDINI_LOG_MESSAGE(TEXT("Modified UProperty %s on %s named %s"), *CurrentPropAttribute.AttributeName, *ClassName, *ObjectName); +#endif + } + + // Handle call PostEditChangeProperty if we deferred the calls + if (bInDeferPostEditChangePropertyCalls && ChangedProperties.Num() > 0) + { + TMap PostEditChangePropertyCalledPerObject; + for (FHoudiniGenericAttributeChangedProperty& ChangeData : ChangedProperties) + { + if (!ChangeData.Property || !IsValid(ChangeData.Object)) + continue; + + // Log that we are calling PostEditChangeProperty for the object / property chain + if (ChangeData.PropertyChain.Num() == 0) + { + HOUDINI_LOG_MESSAGE( + TEXT("Calling deferred PostEditChangeProperty for %s on %s named %s"), + *ChangeData.Property->GetName(), + *(ChangeData.Object->GetClass() ? ChangeData.Object->GetClass()->GetName() : TEXT("Object")), + *(ChangeData.Object->GetName())); + } + else if (ChangeData.PropertyChain.Num() > 0) + { + HOUDINI_LOG_MESSAGE( + TEXT("Calling deferred PostEditChangeProperty for %s on %s named %s"), + *FString::JoinBy(ChangeData.PropertyChain, TEXT("."), [](FProperty const* const Property) + { + if (!Property) + return FString(); + return Property->GetName(); + }), + *(ChangeData.Object->GetClass() ? ChangeData.Object->GetClass()->GetName() : TEXT("Object")), + *(ChangeData.Object->GetName())); + } + + // Record if we successfully called PostEditChangeProperty at least once for each changed object + const bool bPostEditChangePropertyCalled = FHoudiniGenericAttribute::HandlePostEditChangeProperty(ChangeData.Object, ChangeData.PropertyChain, ChangeData.Property); + if (bPostEditChangePropertyCalled) + PostEditChangePropertyCalledPerObject.Add(ChangeData.Object, true); + else + PostEditChangePropertyCalledPerObject.FindOrAdd(ChangeData.Object, false); + } + + // For each changed object where we did not call PostEditChangeProperty, call PostEditChange + for (const auto& Entry : PostEditChangePropertyCalledPerObject) + { + if (Entry.Value) + continue; + + UObject* const ChangedObject = Entry.Key; + if (!IsValid(ChangedObject)) + continue; + + ChangedObject->PostEditChange(); + AActor* const OwnerActor = Cast(ChangedObject->GetOuter()); + if (OwnerActor) + { + OwnerActor->PostEditChange(); + } + } + } + + return (NumSuccess > 0); +} + +bool +FHoudiniEngineUtils::SetGenericPropertyAttribute( + HAPI_NodeId InGeoNodeId, + HAPI_PartId InPartId, + const FHoudiniGenericAttribute& InPropertyAttribute) +{ + HAPI_AttributeOwner AttribOwner; + switch (InPropertyAttribute.AttributeOwner) + { + case EAttribOwner::Point: + AttribOwner = HAPI_ATTROWNER_POINT; + break; + case EAttribOwner::Vertex: + AttribOwner = HAPI_ATTROWNER_VERTEX; + break; + case EAttribOwner::Prim: + AttribOwner = HAPI_ATTROWNER_PRIM; + break; + case EAttribOwner::Detail: + AttribOwner = HAPI_ATTROWNER_DETAIL; + break; + case EAttribOwner::Invalid: + default: + HOUDINI_LOG_WARNING(TEXT("Unsupported Attribute Owner: %d"), InPropertyAttribute.AttributeOwner); + return false; + } + + // Create the attribute via HAPI + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.tupleSize = InPropertyAttribute.AttributeTupleSize; + AttributeInfo.count = InPropertyAttribute.AttributeCount; + AttributeInfo.exists = true; + AttributeInfo.owner = AttribOwner; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + switch(InPropertyAttribute.AttributeType) + { + case (EAttribStorageType::INT): + AttributeInfo.storage = HAPI_STORAGETYPE_INT; + break; + case (EAttribStorageType::INT64): + AttributeInfo.storage = HAPI_STORAGETYPE_INT64; + break; + case (EAttribStorageType::FLOAT): + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT; + break; + case (EAttribStorageType::FLOAT64): + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT64; + break; + case (EAttribStorageType::STRING): + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + break; + case (EAttribStorageType::Invalid): + default: + HOUDINI_LOG_WARNING(TEXT("Unsupported Attribute Storage Type: %d"), InPropertyAttribute.AttributeType); + return false; + } + + // Create the new attribute + if (HAPI_RESULT_SUCCESS != FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, TCHAR_TO_ANSI(*InPropertyAttribute.AttributeName), &AttributeInfo)) + { + return false; + } + + // The New attribute has been successfully created, set its value + switch (InPropertyAttribute.AttributeType) + { + case EAttribStorageType::INT: + { + TArray TempArray; + TempArray.Reserve(InPropertyAttribute.IntValues.Num()); + for (auto Value : InPropertyAttribute.IntValues) + { + TempArray.Add(static_cast(Value)); + } + if (HAPI_RESULT_SUCCESS != FHoudiniApi::SetAttributeIntData( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, TCHAR_TO_ANSI(*InPropertyAttribute.AttributeName), &AttributeInfo, + TempArray.GetData(), 0, AttributeInfo.count)) + { + HOUDINI_LOG_WARNING(TEXT("Could not set attribute %s"), *InPropertyAttribute.AttributeName); + } + break; + } + + case EAttribStorageType::INT64: + { +#if PLATFORM_LINUX + // On Linux, we unfortunately cannot guarantee that int64 and HAPI_Int64 are of the same type, + TArray HAPIIntValues; + HAPIIntValues.SetNumZeroed(InPropertyAttribute.IntValues.Num()); + for (int32 n = 0; n < HAPIIntValues.Num(); n++) + HAPIIntValues[n] = (HAPI_Int64)InPropertyAttribute.IntValues[n]; + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::SetAttributeInt64Data( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, TCHAR_TO_ANSI(*InPropertyAttribute.AttributeName), &AttributeInfo, + HAPIIntValues.GetData(), 0, AttributeInfo.count)) + { + HOUDINI_LOG_WARNING(TEXT("Could not set attribute %s"), *InPropertyAttribute.AttributeName); + } +#else + if (HAPI_RESULT_SUCCESS != FHoudiniApi::SetAttributeInt64Data( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, TCHAR_TO_ANSI(*InPropertyAttribute.AttributeName), &AttributeInfo, + InPropertyAttribute.IntValues.GetData(), 0, AttributeInfo.count)) + { + HOUDINI_LOG_WARNING(TEXT("Could not set attribute %s"), *InPropertyAttribute.AttributeName); + } +#endif + break; + } + + case EAttribStorageType::FLOAT: + { + + TArray TempArray; + TempArray.Reserve(InPropertyAttribute.DoubleValues.Num()); + for (auto Value : InPropertyAttribute.DoubleValues) + { + TempArray.Add(static_cast(Value)); + } + if (HAPI_RESULT_SUCCESS != FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, TCHAR_TO_ANSI(*InPropertyAttribute.AttributeName), &AttributeInfo, + TempArray.GetData(), 0, AttributeInfo.count)) + { + HOUDINI_LOG_WARNING(TEXT("Could not set attribute %s"), *InPropertyAttribute.AttributeName); + } + break; + } + + case EAttribStorageType::FLOAT64: + { + if (HAPI_RESULT_SUCCESS != FHoudiniApi::SetAttributeFloat64Data( + FHoudiniEngine::Get().GetSession(), + InGeoNodeId, InPartId, TCHAR_TO_ANSI(*InPropertyAttribute.AttributeName), &AttributeInfo, + InPropertyAttribute.DoubleValues.GetData(), 0, AttributeInfo.count)) + { + HOUDINI_LOG_WARNING(TEXT("Could not set attribute %s"), *InPropertyAttribute.AttributeName); + } + break; + } + + case EAttribStorageType::STRING: + { + FHoudiniHapiAccessor Accessor(InGeoNodeId, InPartId, TCHAR_TO_ANSI(*InPropertyAttribute.AttributeName)); + if (!Accessor.SetAttributeData(AttributeInfo, InPropertyAttribute.StringValues)) + { + HOUDINI_LOG_WARNING(TEXT("Could not set attribute %s"), *InPropertyAttribute.AttributeName); + } + break; + } + + default: + // Unsupported storage type + HOUDINI_LOG_WARNING(TEXT("Unsupported storage type: %d"), InPropertyAttribute.AttributeType); + break; + } + + return true; +} + + +TArray +FHoudiniEngineUtils::GetDefaultActorTags(const AActor* InActor) +{ + if (!IsValid(InActor)) + { + return {}; + } + + return InActor->GetClass()->GetDefaultObject()->Tags; +} + + +TArray +FHoudiniEngineUtils::GetDefaultComponentTags(const UActorComponent* InComponent) +{ + if (!IsValid(InComponent)) + { + return {}; + } + + return InComponent->GetClass()->GetDefaultObject()->ComponentTags; +} + + +void +FHoudiniEngineUtils::ApplyTagsToActorOnly(const TArray& GenericPropertyAttributes, + TArray& OutActorTags) +{ + for (const FHoudiniGenericAttribute& Attribute : GenericPropertyAttributes) + { + if (Attribute.AttributeName.StartsWith("ActorTag") || Attribute.AttributeName.StartsWith("Tag")) + { + OutActorTags.AddUnique(FName(Attribute.GetStringValue())); + } + } +} + + +void +FHoudiniEngineUtils::ApplyTagsToActorAndComponents(AActor* InActor, bool bKeepActorTags, const TArray& GenericPropertyAttributes) +{ + auto ForEachComponentFn = [InActor](TFunctionRef Fn) + { + for (UActorComponent* Component : InActor->GetComponents()) + { + if (!IsValid(Component)) + { + continue; + } + Fn(Component); + } + }; + + if (!bKeepActorTags) + { + InActor->Tags = FHoudiniEngineUtils::GetDefaultActorTags(InActor); + ForEachComponentFn([](UActorComponent* Component) + { + Component->ComponentTags = FHoudiniEngineUtils::GetDefaultComponentTags(Component); + }); + } + + + for (const FHoudiniGenericAttribute& Attribute : GenericPropertyAttributes) + { + + bool bApplyTagToActor = false; + bool bApplyTagToMainComponent = false; + bool bApplyTagToAllComponents = false; + + if (Attribute.AttributeName.StartsWith("ActorTag")) + { + bApplyTagToActor = true; + } + if (Attribute.AttributeName.StartsWith("MainComponentTag")) + { + bApplyTagToMainComponent = true; + } + if (Attribute.AttributeName.StartsWith("ComponentTag")) + { + bApplyTagToAllComponents = true; + } + if (Attribute.AttributeName.StartsWith("Tag")) + { + bApplyTagToActor = true; + bApplyTagToAllComponents = true; + } + + if (bApplyTagToActor) + { + InActor->Tags.AddUnique(FName(Attribute.GetStringValue())); + } + + if (bApplyTagToAllComponents) + { + ForEachComponentFn([&Attribute](UActorComponent* Component) + { + Component->ComponentTags.AddUnique(FName(Attribute.GetStringValue())); + }); + } + else if(bApplyTagToMainComponent) + { + InActor->GetRootComponent()->ComponentTags.AddUnique(FName(Attribute.GetStringValue())); + } + } +} + + +bool +FHoudiniEngineUtils::IsKeepTagsEnabled(const TArray& InHGPOs) +{ + for (const FHoudiniGeoPartObject& CurHGPO : InHGPOs) + { + if (CurHGPO.bKeepTags) + { + return true; + } + } + return false; +} + + +bool +FHoudiniEngineUtils::IsKeepTagsEnabled(const FHoudiniGeoPartObject* InHGPO) +{ + if (InHGPO) + { + return InHGPO->bKeepTags; + } + + return false; +} + + +void +FHoudiniEngineUtils::KeepOrClearComponentTags( + UActorComponent* ActorComponent, + const TArray& InHGPOs) +{ + if (!IsValid(ActorComponent)) + { + return; + } + const bool bKeepTags = IsKeepTagsEnabled(InHGPOs); + KeepOrClearComponentTags(ActorComponent, bKeepTags); +} + + +void +FHoudiniEngineUtils::KeepOrClearComponentTags(UActorComponent* ActorComponent, const FHoudiniGeoPartObject* InHGPO) +{ + if (!IsValid(ActorComponent)) + { + return; + } + const bool bKeepTags = IsKeepTagsEnabled(InHGPO); + KeepOrClearComponentTags(ActorComponent, bKeepTags); +} + + +void +FHoudiniEngineUtils::KeepOrClearComponentTags(UActorComponent* ActorComponent, bool bKeepTags) +{ + if (!bKeepTags) + { + // Ensure that we revert existing tags to their default state if this is an actor component. + const UActorComponent* DefaultComponent = ActorComponent->GetClass()->GetDefaultObject(); + ActorComponent->ComponentTags = DefaultComponent->ComponentTags; + } +} + + +void +FHoudiniEngineUtils::KeepOrClearActorTags(AActor* Actor, bool bApplyToActor, bool bApplyToComponents, const FHoudiniGeoPartObject* InHGPO) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::KeepOrClearActorTags); + if (!IsValid(Actor)) + { + return; + } + + if (InHGPO && InHGPO->bKeepTags) + { + return; + } + + if (bApplyToActor) + { + // Revert actor tags to their default value + Actor->Tags = GetDefaultActorTags(Actor); + } + + if (bApplyToComponents) + { + // Revert all component tags to their default value + const TSet& Components = Actor->GetComponents(); + for (UActorComponent* Component : Components) + { + if (!IsValid(Component)) + { + continue; + } + + // Ensure that we revert existing tags + const UActorComponent* DefaultComponent = Component->GetClass()->GetDefaultObject(); + Component->ComponentTags = DefaultComponent->ComponentTags; + } + } +} + +void +FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + UPackage * Package, UObject * Object, const FString& Key, const FString& Value) +{ + if (!IsValid(Package)) + return; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + Package->GetMetaData().SetValue(Object, *Key, *Value); +#else + UMetaData * MetaData = Package->GetMetaData(); + if (IsValid(MetaData)) + MetaData->SetValue(Object, *Key, *Value); +#endif +} + + +bool +FHoudiniEngineUtils::AddLevelPathAttribute( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + ULevel* InLevel, + int32 InCount, + const HAPI_AttributeOwner& InAttrOwner) +{ + if (InNodeId < 0 || InCount <= 0) + return false; + + if (!IsValid(InLevel)) + return false; + + // Extract the level path from the level + FString LevelPath = InLevel->GetPathName(); + + // We just want the path up to the first point + int32 DotIndex; + if (LevelPath.FindChar('.', DotIndex)) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + LevelPath.LeftInline(DotIndex, EAllowShrinking::No); +#else + LevelPath.LeftInline(DotIndex, false); +#endif + + // Marshall in level path. + HAPI_AttributeInfo AttributeInfoLevelPath; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoLevelPath); + AttributeInfoLevelPath.count = InCount; + AttributeInfoLevelPath.tupleSize = 1; + AttributeInfoLevelPath.exists = true; + AttributeInfoLevelPath.owner = InAttrOwner; + AttributeInfoLevelPath.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoLevelPath.originalOwner = HAPI_ATTROWNER_INVALID; + + HAPI_Result Result = FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InNodeId, InPartId, + HAPI_UNREAL_ATTRIB_LEVEL_PATH, &AttributeInfoLevelPath); + + if (HAPI_RESULT_SUCCESS == Result) + { + // Set the attribute's string data + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LEVEL_PATH); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoLevelPath, LevelPath), false); + } + + if (Result != HAPI_RESULT_SUCCESS) + { + // Failed to create the attribute + HOUDINI_LOG_WARNING( + TEXT("Failed to upload unreal_level_path attribute for mesh: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + + return false; + } + + return true; +} + + +bool +FHoudiniEngineUtils::AddActorPathAttribute( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + AActor* InActor, + int32 InCount, + const HAPI_AttributeOwner& InAttrOwner) +{ + if (InNodeId < 0 || InCount <= 0) + return false; + + if (!IsValid(InActor)) + return false; + + // Extract the actor path + FString ActorPath = InActor->GetPathName(); + + // Marshall in Actor path. + HAPI_AttributeInfo AttributeInfoActorPath; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoActorPath); + AttributeInfoActorPath.count = InCount; + AttributeInfoActorPath.tupleSize = 1; + AttributeInfoActorPath.exists = true; + AttributeInfoActorPath.owner = InAttrOwner; + AttributeInfoActorPath.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoActorPath.originalOwner = HAPI_ATTROWNER_INVALID; + + HAPI_Result Result = FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InNodeId, InPartId, + HAPI_UNREAL_ATTRIB_ACTOR_PATH, &AttributeInfoActorPath); + + if (HAPI_RESULT_SUCCESS == Result) + { + // Set the attribute's string data + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_ACTOR_PATH); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoActorPath, ActorPath), false); + } + + if (Result != HAPI_RESULT_SUCCESS) + { + // Failed to create the attribute + HOUDINI_LOG_WARNING( + TEXT("Failed to upload unreal_actor_path attribute for mesh: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + + return false; + } + + return true; +} + +bool +FHoudiniEngineUtils::AddLandscapeTypeAttribute( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + AActor* InActor, + int32 InCount) +{ + HOUDINI_CHECK_RETURN(IsValid(InActor), false); + + // Currently we only add an attribute for landscaping streaming proxies. + bool bIsStreamingProxy = InActor->IsA(ALandscapeStreamingProxy::StaticClass()); + if (!bIsStreamingProxy) return false; + + HAPI_AttributeInfo AttributeInfoActorPath; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoActorPath); + AttributeInfoActorPath.count = InCount; + AttributeInfoActorPath.tupleSize = 1; + AttributeInfoActorPath.exists = true; + AttributeInfoActorPath.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoActorPath.storage = HAPI_STORAGETYPE_INT; + AttributeInfoActorPath.originalOwner = HAPI_ATTROWNER_INVALID; + + HAPI_Result Result = FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InNodeId, InPartId, + HAPI_UNREAL_ATTRIB_LANDSCAPE_STREAMING_PROXY, &AttributeInfoActorPath); + + if (Result == HAPI_RESULT_SUCCESS ) + { + // Set the attribute's string data + + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_STREAMING_PROXY); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoActorPath, 1), false); + } + + if (Result != HAPI_RESULT_SUCCESS) + { + // Failed to create the attribute + HOUDINI_LOG_WARNING( + TEXT("Failed to upload unreal_actor_path attribute for mesh: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + return false; + } + return true; +} + +void +FHoudiniEngineUtils::CreateSlateNotification( + const FString& NotificationString, float NotificationExpire, float NotificationFadeOut ) +{ +#if WITH_EDITOR + // Trying to display SlateNotifications while in a background thread will crash UE + if (!IsInGameThread() && !IsInSlateThread() && !IsInAsyncLoadingThread()) + return; + + // Check whether we want to display Slate notifications. + bool bDisplaySlateCookingNotifications = true; + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (HoudiniRuntimeSettings) + bDisplaySlateCookingNotifications = HoudiniRuntimeSettings->bDisplaySlateCookingNotifications; + + if (!bDisplaySlateCookingNotifications) + return; + + FText NotificationText = FText::FromString(NotificationString); + FNotificationInfo Info(NotificationText); + + Info.bFireAndForget = true; + Info.FadeOutDuration = NotificationFadeOut; + Info.ExpireDuration = NotificationExpire; + + TSharedPtr HoudiniBrush = FHoudiniEngine::Get().GetHoudiniEngineLogoBrush(); + if (HoudiniBrush.IsValid()) + Info.Image = HoudiniBrush.Get(); + + FSlateNotificationManager::Get().AddNotification(Info); +#endif + + return; +} + +FString +FHoudiniEngineUtils::GetHoudiniEnginePluginDir() +{ + FString EnginePluginDir = FPaths::EnginePluginsDir() / TEXT("Runtime/HoudiniEngine/"); + if (FPaths::FileExists(EnginePluginDir + "HoudiniEngine.uplugin")) + return EnginePluginDir; + + FString ProjectPluginDir = FPaths::ProjectPluginsDir() / TEXT("Runtime/HoudiniEngine/"); + if (FPaths::FileExists(ProjectPluginDir + "HoudiniEngine.uplugin")) + return ProjectPluginDir; + + TSharedPtr HoudiniPlugin = IPluginManager::Get().FindPlugin(TEXT("HoudiniEngine")); + FString PluginBaseDir = HoudiniPlugin.IsValid() ? HoudiniPlugin->GetBaseDir() : EnginePluginDir; + if (FPaths::DirectoryExists(PluginBaseDir)) + return PluginBaseDir; + + HOUDINI_LOG_WARNING(TEXT("Could not find the Houdini Engine plugin's directory")); + + return EnginePluginDir; +} + + +HAPI_Result +FHoudiniEngineUtils::CreateNode( + HAPI_NodeId InParentNodeId, + const FString& InOperatorName, + const FString& InNodeLabel, + HAPI_Bool bInCookOnCreation, + HAPI_NodeId* OutNewNodeId) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::CreateNode); + + // Call HAPI::CreateNode + HAPI_Result Result = FHoudiniApi::CreateNode( + FHoudiniEngine::Get().GetSession(), + InParentNodeId, H_TCHAR_TO_UTF8(*InOperatorName), H_TCHAR_TO_UTF8(*InNodeLabel), bInCookOnCreation, OutNewNodeId); + + // Return now if CreateNode failed + if (Result != HAPI_RESULT_SUCCESS) + return Result; + + // Loop on the cook_state status until it's ready + int CurrentStatus = HAPI_State::HAPI_STATE_STARTING_LOAD; + while (CurrentStatus > HAPI_State::HAPI_STATE_MAX_READY_STATE) + { + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetStatus( + FHoudiniEngine::Get().GetSession(), + HAPI_StatusType::HAPI_STATUS_COOK_STATE, &CurrentStatus)) + { + // Exit the loop if GetStatus somehow fails + break; + } + } + + if (CurrentStatus == HAPI_STATE_READY_WITH_FATAL_ERRORS) + { + // Fatal errors - failed + HOUDINI_LOG_ERROR(TEXT("Failed to create node %s - %s"), *InOperatorName, *InNodeLabel); + return HAPI_RESULT_FAILURE; + } + else if (CurrentStatus == HAPI_STATE_READY_WITH_COOK_ERRORS) + { + // Mention the errors - still return success + HOUDINI_LOG_WARNING(TEXT("Cook errors when creating node %s - %s"), *InOperatorName, *InNodeLabel); + } + + return HAPI_RESULT_SUCCESS; +} + + +int32 +FHoudiniEngineUtils::HapiGetCookCount(HAPI_NodeId InNodeId) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::HapiGetCookCount); + + // To reduce the "cost" of the call on big HDAs - limit or search to non bypassed SOP/OBJ nodes + int32 CookCount = -1; + if (HAPI_RESULT_FAILURE == FHoudiniApi::GetTotalCookCount( + FHoudiniEngine::Get().GetSession(), + InNodeId, HAPI_NODETYPE_OBJ | HAPI_NODETYPE_SOP | HAPI_NODETYPE_COP, HAPI_NODEFLAGS_NON_BYPASS, true, &CookCount)) + { + return -1; + } + + return CookCount; +} + +bool +FHoudiniEngineUtils::GetLevelPathAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + TArray& OutLevelPaths, + HAPI_AttributeOwner InAttributeOwner, + int32 InStartIndex, + int32 InCount) +{ + // --------------------------------------------- + // Attribute: unreal_level_path + // --------------------------------------------- + + FHoudiniHapiAccessor Accessor(InGeoId, InPartId, HAPI_UNREAL_ATTRIB_LEVEL_PATH); + bool bSuccess = Accessor.GetAttributeData(InAttributeOwner, 1, OutLevelPaths, InStartIndex, InCount); + + if (bSuccess && OutLevelPaths.Num() > 0) + return true; + + OutLevelPaths.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetLevelPathAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& OutLevelPath, + int32 InPointIndex, + int32 InPrimIndex) +{ + constexpr int32 Count = 1; + TArray StringData; + + if (InPointIndex >= 0) + { + if (GetLevelPathAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_POINT, InPointIndex, Count)) + { + if (StringData.Num() > 0) + { + OutLevelPath = StringData[0]; + return true; + } + } + } + + if (InPrimIndex >= 0) + { + if (GetLevelPathAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_PRIM, InPrimIndex, Count)) + { + if (StringData.Num() > 0) + { + OutLevelPath = StringData[0]; + return true; + } + } + } + + if (GetLevelPathAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_DETAIL, 0, Count)) + { + if (StringData.Num() > 0) + { + OutLevelPath = StringData[0]; + return true; + } + } + + OutLevelPath.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetOutputNameAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + TArray& OutOutputNames, + int32 InStartIndex, + int32 InCount) +{ + FHoudiniHapiAccessor Accessor; + Accessor.Init(InGeoId, InPartId, HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, OutOutputNames, InStartIndex, InCount); + if (bSuccess && OutOutputNames.Num() > 0) + return true; + + OutOutputNames.Empty(); + + Accessor.Init(InGeoId, InPartId, HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V1); + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, OutOutputNames, InStartIndex, InCount); + if (bSuccess && OutOutputNames.Num() > 0) + return true; + + OutOutputNames.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetOutputNameAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& OutOutputName, + int32 InPointIndex, + int32 InPrimIndex) +{ + constexpr int32 Count = 1; + TArray StringData; + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + + FHoudiniHapiAccessor Accessor; + Accessor.Init(InGeoId, InPartId, HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2); + bool bSuccess; + + // HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2 + if (InPointIndex >= 0) + { + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, StringData, InPointIndex, Count); + if (bSuccess && StringData.Num() > 0) + { + OutOutputName = StringData[0]; + return true; + } + } + + if (InPrimIndex >= 0) + { + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, 1, StringData, InPrimIndex, Count); + if (bSuccess && StringData.Num() > 0) + { + OutOutputName = StringData[0]; + return true; + } + } + + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, 1, StringData, 0, Count); + if (bSuccess && StringData.Num() > 0) + { + OutOutputName = StringData[0]; + return true; + } + + Accessor.Init(InGeoId, InPartId, HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V1); + + // HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V1 + if (InPointIndex >= 0) + { + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, StringData, InPointIndex, Count); + if (bSuccess && StringData.Num() > 0) + { + OutOutputName = StringData[0]; + return true; + } + } + + if (InPrimIndex >= 0) + { + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, 1, StringData, InPrimIndex, Count); + if (bSuccess && StringData.Num() > 0) + { + OutOutputName = StringData[0]; + return true; + } + } + + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, 1, StringData, 0, Count); + if (bSuccess && StringData.Num() > 0) + { + OutOutputName = StringData[0]; + return true; + } + + OutOutputName.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetBakeNameAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + TArray& OutBakeNames, + const HAPI_AttributeOwner& InAttribOwner, + int32 InStartIndex, + int32 InCount) +{ + // --------------------------------------------- + // Attribute: unreal_bake_name + // --------------------------------------------- + + FHoudiniHapiAccessor Accessor(InGeoId, InPartId, HAPI_UNREAL_ATTRIB_BAKE_NAME); + bool bSuccess = Accessor.GetAttributeData(InAttribOwner, 1, OutBakeNames, InStartIndex, InCount); + + if (bSuccess && OutBakeNames.Num() > 0) + return true; + + OutBakeNames.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetBakeNameAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& OutBakeName, + int32 InPointIndex, + int32 InPrimIndex) +{ + constexpr int32 Count = 1; + TArray StringData; + + if (InPointIndex >= 0) + { + if (GetBakeNameAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_POINT, InPointIndex, Count)) + { + if (StringData.Num() > 0) + { + OutBakeName = StringData[0]; + return true; + } + } + } + + if (InPrimIndex >= 0) + { + if (GetBakeNameAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_PRIM, InPrimIndex, Count)) + { + if (StringData.Num() > 0) + { + OutBakeName = StringData[0]; + return true; + } + } + } + + if (GetBakeNameAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_DETAIL, 0, Count)) + { + if (StringData.Num() > 0) + { + OutBakeName = StringData[0]; + return true; + } + } + + OutBakeName.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetTileAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + TArray& OutTileValues, + const HAPI_AttributeOwner& InAttribOwner, + int32 InStart, + int32 InCount) +{ + // --------------------------------------------- + // Attribute: tile + // --------------------------------------------- + + + FHoudiniHapiAccessor Accessor(InGeoId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_TILE); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, OutTileValues, InStart, InCount); + if (bSuccess) + { + if (OutTileValues.Num() > 0) + return true; + } + + OutTileValues.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetTileAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + int32& OutTileValue, + int32 InPointIndex, + int32 InPrimIndex) +{ + constexpr int32 Count = 1; + TArray IntData; + + if (InPointIndex >= 0) + { + if (GetTileAttribute(InGeoId, InPartId, IntData, HAPI_ATTROWNER_POINT, InPointIndex, Count)) + { + if (IntData.Num() > 0) + { + OutTileValue = IntData[0]; + return true; + } + } + } + + if (InPrimIndex >= 0) + { + if (GetTileAttribute(InGeoId, InPartId, IntData, HAPI_ATTROWNER_PRIM, InPrimIndex, Count)) + { + if (IntData.Num() > 0) + { + OutTileValue = IntData[0]; + return true; + } + } + } + + if (GetTileAttribute(InGeoId, InPartId, IntData, HAPI_ATTROWNER_DETAIL, 0, Count)) + { + if (IntData.Num() > 0) + { + OutTileValue = IntData[0]; + return true; + } + } + + return false; +} + +bool +FHoudiniEngineUtils::GetEditLayerName( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& EditLayerName, + const HAPI_AttributeOwner& InAttribOwner) +{ + // --------------------------------------------- + // Attribute: tile + // --------------------------------------------- + HAPI_AttributeInfo AttribInfo; + FHoudiniApi::AttributeInfo_Init(&AttribInfo); + + TArray StrData; + + FHoudiniHapiAccessor Accessor(InGeoId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_NAME); + bool bSuccess = Accessor.GetAttributeData(InAttribOwner, 1, StrData); + + if (bSuccess && StrData.Num() > 0) + { + EditLayerName = StrData[0]; + return true; + } + + EditLayerName = FString(); + return false; +} + +bool FHoudiniEngineUtils::HasEditLayerName(HAPI_NodeId InGeoId, HAPI_PartId InPartId, + const HAPI_AttributeOwner& InAttribOwner) +{ + // --------------------------------------------- + // Attribute: unreal_landscape_ + // --------------------------------------------- + + return FHoudiniEngineUtils::HapiCheckAttributeExists( + InGeoId, InPartId, + HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_NAME, + InAttribOwner); +} + +bool +FHoudiniEngineUtils::GetTempFolderAttribute( + HAPI_NodeId InNodeId, + const HAPI_AttributeOwner& InAttributeOwner, + TArray& OutTempFolder, + HAPI_PartId InPartId, + int32 InStart, + int32 InCount) +{ + OutTempFolder.Empty(); + + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_TEMP_FOLDER); + bool bSuccess = Accessor.GetAttributeData(InAttributeOwner, 1, OutTempFolder, InStart, InCount); + + if (bSuccess && OutTempFolder.Num() > 0) + return true; + + OutTempFolder.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetTempFolderAttribute( + HAPI_NodeId InGeoId, + FString& OutTempFolder, + HAPI_PartId InPartId, + int32 InPrimIndex) +{ + constexpr int32 Count = 1; + TArray StringData; + if (GetTempFolderAttribute(InGeoId, HAPI_ATTROWNER_PRIM, StringData, InPartId, InPrimIndex, Count)) + { + if (StringData.Num() > 0) + { + OutTempFolder = StringData[0]; + return true; + } + } + + if (GetTempFolderAttribute(InGeoId, HAPI_ATTROWNER_DETAIL, StringData, InPartId, 0, Count)) + { + if (StringData.Num() > 0) + { + OutTempFolder = StringData[0]; + return true; + } + } + + OutTempFolder.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetBakeFolderAttribute( + HAPI_NodeId InNodeId, + const HAPI_AttributeOwner& InAttributeOwner, + TArray& OutBakeFolder, + HAPI_PartId InPartId, + int32 InStart, + int32 InCount) +{ + OutBakeFolder.Empty(); + + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_BAKE_FOLDER); + bool bSuccess = Accessor.GetAttributeData(InAttributeOwner, 1, OutBakeFolder, InStart, InCount); + + if (bSuccess && OutBakeFolder.Num() > 0) + return true; + + OutBakeFolder.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetBakeFolderAttribute( + HAPI_NodeId InGeoId, + TArray& OutBakeFolder, + HAPI_PartId InPartId, + int32 InStart, + int32 InCount) +{ + OutBakeFolder.Empty(); + + if (GetBakeFolderAttribute(InGeoId, HAPI_ATTROWNER_PRIM, OutBakeFolder, InPartId, InStart, InCount)) + { + if (OutBakeFolder.Num() > 0) + return true; + } + + if (GetBakeFolderAttribute(InGeoId, HAPI_ATTROWNER_DETAIL, OutBakeFolder, InPartId, InStart, InCount)) + { + if (OutBakeFolder.Num() > 0) + return true; + } + + OutBakeFolder.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetBakeFolderAttribute( + const HAPI_NodeId InGeoId, + const HAPI_PartId InPartId, + FString& OutBakeFolder, + const int32 InPrimIndex) +{ + constexpr int32 Count = 1; + TArray StringData; + if (InPrimIndex >= 0) + { + if (GetBakeFolderAttribute(InGeoId, HAPI_ATTROWNER_PRIM, StringData, InPartId, InPrimIndex, Count)) + { + if (StringData.Num() > 0) + { + OutBakeFolder = StringData[0]; + return true; + } + } + } + + if (GetBakeFolderAttribute(InGeoId, HAPI_ATTROWNER_POINT, StringData, InPartId, 0, Count)) + { + if (StringData.Num() > 0) + { + OutBakeFolder = StringData[0]; + return true; + } + } + + if (GetBakeFolderAttribute(InGeoId, HAPI_ATTROWNER_DETAIL, StringData, InPartId, 0, Count)) + { + if (StringData.Num() > 0) + { + OutBakeFolder = StringData[0]; + return true; + } + } + + OutBakeFolder.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetBakeActorAttribute( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + TArray& OutBakeActorNames, + const HAPI_AttributeOwner& InAttributeOwner, + int32 InStart, + int32 InCount) +{ + // --------------------------------------------- + // Attribute: unreal_bake_actor + // --------------------------------------------- + + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_BAKE_ACTOR); + bool bSuccess = Accessor.GetAttributeData(InAttributeOwner, 1, OutBakeActorNames, InStart, InCount); + + if (bSuccess && OutBakeActorNames.Num() > 0) + return true; + + OutBakeActorNames.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetBakeActorAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& OutBakeActorName, + int32 InPointIndex, + int32 InPrimIndex) +{ + constexpr int32 Count = 1; + TArray StringData; + + if (InPointIndex >= 0) + { + if (GetBakeActorAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_POINT, InPointIndex, Count)) + { + if (StringData.Num() > 0) + { + OutBakeActorName = StringData[0]; + return true; + } + } + } + + if (InPrimIndex >= 0) + { + if (GetBakeActorAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_PRIM, InPrimIndex, Count)) + { + if (StringData.Num() > 0) + { + OutBakeActorName = StringData[0]; + return true; + } + } + } + + if (GetBakeActorAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_DETAIL, 0, Count)) + { + if (StringData.Num() > 0) + { + OutBakeActorName = StringData[0]; + return true; + } + } + + OutBakeActorName.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetBakeActorClassAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + TArray& OutBakeActorClassNames, + const HAPI_AttributeOwner& InAttributeOwner, + int32 InStart, + int32 InCount) +{ + // --------------------------------------------- + // Attribute: unreal_bake_actor + // --------------------------------------------- + + FHoudiniHapiAccessor Accessor(InGeoId, InPartId, HAPI_UNREAL_ATTRIB_BAKE_ACTOR_CLASS); + bool bSuccess = Accessor.GetAttributeData(InAttributeOwner, 1, OutBakeActorClassNames, InStart, InCount); + + if (bSuccess && OutBakeActorClassNames.Num() > 0) + return true; + + OutBakeActorClassNames.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetBakeActorClassAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& OutBakeActorClassName, + int32 InPointIndex, + int32 InPrimIndex) +{ + constexpr int32 Count = 1; + TArray StringData; + + if (InPointIndex >= 0) + { + if (GetBakeActorClassAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_POINT, InPointIndex, Count)) + { + if (StringData.Num() > 0) + { + OutBakeActorClassName = StringData[0]; + return true; + } + } + } + + if (InPrimIndex >= 0) + { + if (GetBakeActorClassAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_PRIM, InPrimIndex, Count)) + { + if (StringData.Num() > 0) + { + OutBakeActorClassName = StringData[0]; + return true; + } + } + } + + if (GetBakeActorClassAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_DETAIL, 0, Count)) + { + if (StringData.Num() > 0) + { + OutBakeActorClassName = StringData[0]; + return true; + } + } + + OutBakeActorClassName.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetBakeOutlinerFolderAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + TArray& OutBakeOutlinerFolders, + const HAPI_AttributeOwner& InAttributeOwner, + int32 InStart, + int32 InCount) +{ + // --------------------------------------------- + // Attribute: unreal_bake_outliner_folder + // --------------------------------------------- + + FHoudiniHapiAccessor Accessor(InGeoId, InPartId, HAPI_UNREAL_ATTRIB_BAKE_OUTLINER_FOLDER); + bool bSuccess = Accessor.GetAttributeData(InAttributeOwner, 1, OutBakeOutlinerFolders, InStart, InCount); + if (bSuccess && OutBakeOutlinerFolders.Num() > 0) + return true; + + OutBakeOutlinerFolders.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::GetBakeOutlinerFolderAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& OutBakeOutlinerFolder, + int32 InPointIndex, + int32 InPrimIndex) +{ + constexpr int32 Count = 1; + TArray StringData; + + if (InPointIndex >= 0) + { + if (GetBakeOutlinerFolderAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_POINT, InPointIndex, Count)) + { + if (StringData.Num() > 0) + { + OutBakeOutlinerFolder = StringData[0]; + return true; + } + } + } + + if (InPrimIndex >= 0) + { + if (GetBakeOutlinerFolderAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_PRIM, InPrimIndex, Count)) + { + if (StringData.Num() > 0) + { + OutBakeOutlinerFolder = StringData[0]; + return true; + } + } + } + + if (GetBakeOutlinerFolderAttribute(InGeoId, InPartId, StringData, HAPI_ATTROWNER_DETAIL, 0, Count)) + { + if (StringData.Num() > 0) + { + OutBakeOutlinerFolder = StringData[0]; + return true; + } + } + + OutBakeOutlinerFolder.Empty(); + return false; +} + +bool +FHoudiniEngineUtils::MoveActorToLevel(AActor* InActor, ULevel* InDesiredLevel) +{ + if (!InActor || !InDesiredLevel) + return false; + + ULevel* PreviousLevel = InActor->GetLevel(); + if (PreviousLevel == InDesiredLevel) + return true; + + UWorld* CurrentWorld = InActor->GetWorld(); + if(CurrentWorld) + CurrentWorld->RemoveActor(InActor, true); + + //Set the outer of Actor to NewLevel + FHoudiniEngineUtils::RenameObject(InActor, (const TCHAR*)0, InDesiredLevel); + InDesiredLevel->Actors.Add(InActor); + + return true; +} + +HAPI_Result +FHoudiniEngineUtils::HapiCommitGeo(HAPI_NodeId InNodeId) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::HapiCommitGeo); + return FHoudiniApi::CommitGeo(FHoudiniEngine::Get().GetSession(), InNodeId); +} + +bool +FHoudiniEngineUtils::HapiCookNode(HAPI_NodeId InNodeId, HAPI_CookOptions* InCookOptions, bool bWaitForCompletion) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::HapiCookNode); + + // Check for an invalid node id + if (InNodeId < 0) + return false; + + // No Cook Options were specified, use the default one + if (InCookOptions == nullptr) + { + // Use the default cook options + HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CookNode( + FHoudiniEngine::Get().GetSession(), InNodeId, &CookOptions), false); + } + else + { + // Use the provided CookOptions + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CookNode( + FHoudiniEngine::Get().GetSession(), InNodeId, InCookOptions), false); + } + + // If we don't need to wait for completion, return now + if (!bWaitForCompletion) + return true; + + // Wait for the cook to finish + HAPI_Result Result = HAPI_RESULT_SUCCESS; + while (true) + { + // Get the current cook status + int Status = HAPI_STATE_STARTING_COOK; + HOUDINI_CHECK_ERROR_GET(&Result, FHoudiniApi::GetStatus( + FHoudiniEngine::Get().GetSession(), HAPI_STATUS_COOK_STATE, &Status)); + + if (Status == HAPI_STATE_READY) + { + // The cook has been successful. + return true; + } + else if (Status == HAPI_STATE_READY_WITH_FATAL_ERRORS || Status == HAPI_STATE_READY_WITH_COOK_ERRORS) + { + // There was an error while cooking the node. + //FString CookResultString = FHoudiniEngineUtils::GetCookResult(); + //HOUDINI_LOG_ERROR(); + return false; + } + + // We want to yield a bit. + FPlatformProcess::Sleep(0.1f); + } +} + +HAPI_NodeId FHoudiniEngineUtils::CreateInputHapiNode(const FString& InNodeLabel, HAPI_NodeId InParentNodeId) +{ + HAPI_NodeId OutNodeId = INDEX_NONE; + HAPI_Result Result = CreateInputNode(InNodeLabel, OutNodeId, InParentNodeId); + if(Result == HAPI_Result::HAPI_RESULT_SUCCESS) + return OutNodeId; + else + return INDEX_NONE; +} + +HAPI_Result +FHoudiniEngineUtils::CreateInputNode(const FString& InNodeLabel, HAPI_NodeId& OutNodeId, const int32 InParentNodeId) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::CreateInputNode); + + HAPI_NodeId NodeId = -1; + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + + if (InParentNodeId < 0) + { + const HAPI_Result Result = FHoudiniApi::CreateInputNode(Session, -1, &NodeId, H_TCHAR_TO_UTF8(*InNodeLabel)); + if (Result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineUtils::CreateInputNode]: CreateInputNode failed: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + return Result; + } + + OutNodeId = NodeId; + return Result; + } + + if (!IsHoudiniNodeValid(InParentNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineUtils::CreateInputNode]: InParentNodeId (%d) is not valid."), InParentNodeId); + return HAPI_RESULT_NODE_INVALID; + } + + const FString NodeLabel = TEXT("input_") + InNodeLabel; + constexpr bool bCookOnCreation = true; + HAPI_NodeId ObjectNodeId = -1; + HAPI_Result Result = CreateNode(InParentNodeId, TEXT("geo"), NodeLabel, bCookOnCreation, &ObjectNodeId); + if (Result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineUtils::CreateInputNode]: CreateNode failed: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + return Result; + } + Result = CreateNode(ObjectNodeId, TEXT("null"), NodeLabel, bCookOnCreation, &NodeId); + if (Result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineUtils::CreateInputNode]: CreateNode failed: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + return Result; + } + + OutNodeId = NodeId; + return Result; +} + +bool +FHoudiniEngineUtils::HapiConnectNodeInput(int32 InNodeId, int32 InputIndex, int32 InNodeIdToConnect, int32 OutputIndex, int32 InXFormType) +{ + // Connect the node ids + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), InNodeId, InputIndex, InNodeIdToConnect, OutputIndex), false); + + // When connecting two nodes that are NOT in the same subnet, + // HAPI creates an object merge node for the connection + // See if we have specified a TransformType for that object merge! + if(InXFormType <= 0 || InXFormType <= 2) + { + HAPI_NodeId ObjMergeNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::QueryNodeInput( + FHoudiniEngine::Get().GetSession(), InNodeId, InputIndex, &ObjMergeNodeId), false); + + // Set the transform value to "None" + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue(FHoudiniEngine::Get().GetSession(), ObjMergeNodeId, H_TCHAR_TO_UTF8(TEXT("xformtype")), 0, InXFormType), false); + } + + return true; +} + + +FString +FHoudiniEngineUtils::JSONToString(const TSharedPtr& JSONObject) +{ + FString OutputString; + const TSharedRef< TJsonWriter<> > Writer = TJsonWriterFactory<>::Create(&OutputString); + FJsonSerializer::Serialize(JSONObject.ToSharedRef(), Writer); + return OutputString; +} + + +bool +FHoudiniEngineUtils::JSONFromString(const FString& JSONString, TSharedPtr& OutJSONObject) +{ + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create( JSONString ); + if (!FJsonSerializer::Deserialize(Reader, OutJSONObject) || !OutJSONObject.IsValid()) + { + return false; + } + + return true; +} + + +bool +FHoudiniEngineUtils::UpdateMeshPartUVSets( + const int GeoId, + const int PartId, + bool bRemoveUnused, + TArray>& OutPartUVSets, + TArray& OutAttribInfoUVSets) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniEngineUtils::UpdateMeshPartUVSets); + + // Only Retrieve uvs if necessary + if (OutPartUVSets.Num() > 0) + return true; + + OutPartUVSets.SetNum(MAX_STATIC_TEXCOORDS); + OutAttribInfoUVSets.SetNum(MAX_STATIC_TEXCOORDS); + + // The second UV set should be called uv2, but we will still check if need to look for a uv1 set. + // If uv1 exists, we'll look for uv, uv1, uv2 etc.. if not we'll look for uv, uv2, uv3 etc.. + bool bUV1Exists = FHoudiniEngineUtils::HapiCheckAttributeExists(GeoId, PartId, "uv1"); + + // Retrieve UVs. + for (int32 TexCoordIdx = 0; TexCoordIdx < MAX_STATIC_TEXCOORDS; ++TexCoordIdx) + { + FString UVAttributeName = HAPI_UNREAL_ATTRIB_UV; + if (TexCoordIdx > 0) + UVAttributeName += FString::Printf(TEXT("%d"), bUV1Exists ? TexCoordIdx : TexCoordIdx + 1); + + FHoudiniApi::AttributeInfo_Init(&OutAttribInfoUVSets[TexCoordIdx]); + + FHoudiniHapiAccessor Accessor(GeoId, PartId, TCHAR_TO_ANSI(*UVAttributeName)); + Accessor.GetInfo(OutAttribInfoUVSets[TexCoordIdx], HAPI_ATTROWNER_INVALID); + OutAttribInfoUVSets[TexCoordIdx].tupleSize = 2; + Accessor.GetAttributeData(OutAttribInfoUVSets[TexCoordIdx], OutPartUVSets[TexCoordIdx]); + } + + // Also look for 16.5 uvs (attributes with a Texture type) + // For that, we'll have to iterate through ALL the attributes and check their types + TArray FoundAttributeNames; + TArray FoundAttributeInfos; + for (int32 AttrIdx = 0; AttrIdx < HAPI_ATTROWNER_MAX; ++AttrIdx) + { + FHoudiniEngineUtils::HapiGetAttributeOfType( + GeoId, PartId, (HAPI_AttributeOwner)AttrIdx, + HAPI_ATTRIBUTE_TYPE_TEXTURE, FoundAttributeInfos, FoundAttributeNames); + } + + if (FoundAttributeInfos.Num() <= 0) + return true; + + // We found some additionnal uv attributes + int32 AvailableIdx = 0; + for (int32 attrIdx = 0; attrIdx < FoundAttributeInfos.Num(); attrIdx++) + { + // Ignore the old uvs + if (FoundAttributeNames[attrIdx] == TEXT("uv") + || FoundAttributeNames[attrIdx] == TEXT("uv1") + || FoundAttributeNames[attrIdx] == TEXT("uv2") + || FoundAttributeNames[attrIdx] == TEXT("uv3") + || FoundAttributeNames[attrIdx] == TEXT("uv4") + || FoundAttributeNames[attrIdx] == TEXT("uv5") + || FoundAttributeNames[attrIdx] == TEXT("uv6") + || FoundAttributeNames[attrIdx] == TEXT("uv7") + || FoundAttributeNames[attrIdx] == TEXT("uv8")) + continue; + + HAPI_AttributeInfo CurrentAttrInfo = FoundAttributeInfos[attrIdx]; + if (!CurrentAttrInfo.exists) + continue; + + // Look for the next available index in the return arrays + for (; AvailableIdx < OutAttribInfoUVSets.Num(); AvailableIdx++) + { + if (!OutAttribInfoUVSets[AvailableIdx].exists) + break; + } + + // We are limited to MAX_STATIC_TEXCOORDS uv sets! + // If we already have too many uv sets, skip the rest + if ((AvailableIdx >= MAX_STATIC_TEXCOORDS) || (AvailableIdx >= OutAttribInfoUVSets.Num())) + { + HOUDINI_LOG_WARNING(TEXT("Too many UV sets found. Unreal only supports %d , skipping the remaining uv sets."), (int32)MAX_STATIC_TEXCOORDS); + break; + } + + // Force the tuple size to 2 ? + CurrentAttrInfo.tupleSize = 2; + + // Add the attribute infos we found + OutAttribInfoUVSets[AvailableIdx] = CurrentAttrInfo; + + // Allocate sufficient buffer for the attribute's data. + OutPartUVSets[AvailableIdx].SetNumUninitialized(CurrentAttrInfo.count * CurrentAttrInfo.tupleSize); + + // Get the texture coordinates + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*(FoundAttributeNames[attrIdx])), + &OutAttribInfoUVSets[AvailableIdx], -1, + &OutPartUVSets[AvailableIdx][0], 0, CurrentAttrInfo.count)) + { + // Something went wrong when trying to access the uv values, invalidate this set + OutAttribInfoUVSets[AvailableIdx].exists = false; + } + } + + // Remove unused UV sets + if (bRemoveUnused) + { + for (int32 Idx = OutPartUVSets.Num() - 1; Idx >= 0; Idx--) + { + if (OutPartUVSets[Idx].Num() > 0) + continue; + + OutPartUVSets.RemoveAt(Idx); + } + } + + return true; +} + + +void +FHoudiniEngineUtils::ForceDeleteObject(UObject* Object) +{ + // This function came into existence to ensure Data Tables are fully deleted before recooking. + // Just normally destroying Data Tables doesn't remove some internal data, which causes problems + // when recreating a package with the same name. + + if (!IsValid(Object)) + return; + + // Make sure object is loaded before we destroyed it. + if (IsValid(Object->GetPackage()) && !Object->GetPackage()->IsFullyLoaded()) + Object->GetPackage()->FullyLoad(); + + // First we must nullify references, or DeleteSingleObject will do nothing. + TArray Objects = { Object }; + ObjectTools::ForceReplaceReferences(nullptr, Objects); + + // Now delete the object. + const bool bDeleteSucceeded = ObjectTools::DeleteSingleObject(Object, false); + + // CollectGarbage so we don't get stale objects. + if (bDeleteSucceeded) + CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS, true); +} + +TArray FHoudiniEngineUtils::GetAttributeNames(const HAPI_Session* Session, HAPI_NodeId NodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + HAPI_PartInfo PartInfo; + TArray Results; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetPartInfo(Session, NodeId, PartId, &PartInfo), Results); + + TArray StringHandles; + StringHandles.SetNum(PartInfo.attributeCounts[Owner]); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAttributeNames(Session, NodeId, PartId, Owner, StringHandles.GetData(), StringHandles.Num()), Results); + + FHoudiniEngineString::SHArrayToFStringArray(StringHandles, Results, Session); + + return Results; +} + +TMap< HAPI_AttributeOwner, TArray> FHoudiniEngineUtils::GetAllAttributeNames(const HAPI_Session* Session, HAPI_NodeId NodeId, HAPI_PartId PartId) +{ + TMap< HAPI_AttributeOwner, TArray> Results; + + Results.Add(HAPI_AttributeOwner::HAPI_ATTROWNER_VERTEX, GetAttributeNames(Session, NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_VERTEX)); + Results.Add(HAPI_AttributeOwner::HAPI_ATTROWNER_POINT, GetAttributeNames(Session, NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_POINT)); + Results.Add(HAPI_AttributeOwner::HAPI_ATTROWNER_PRIM, GetAttributeNames(Session, NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_PRIM)); + Results.Add(HAPI_AttributeOwner::HAPI_ATTROWNER_DETAIL, GetAttributeNames(Session, NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_DETAIL)); + + return Results; +} + +void FHoudiniEngineUtils::DumpNode(const FString& NodePath) +{ + HAPI_NodeId UnrealContentNodeId = -1; + HAPI_Result result = FHoudiniApi::GetNodeFromPath( + FHoudiniEngine::Get().GetSession(), -1, TCHAR_TO_ANSI(*NodePath), &UnrealContentNodeId); + if (result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_DISPLAY(TEXT("Failed to get node from path: %s"), *NodePath); + return; + } + FString Output = DumpNode(UnrealContentNodeId); + HOUDINI_LOG_DISPLAY(TEXT("%s"), *Output); +} + +#define H_CASE_ENUM_TO_STRING(X) case X: return TEXT(#X); + +FString FHoudiniEngineUtils::NodeTypeToString(HAPI_NodeType NodeType) +{ + switch (NodeType) + { + H_CASE_ENUM_TO_STRING(HAPI_NODETYPE_ANY) + H_CASE_ENUM_TO_STRING(HAPI_NODETYPE_NONE) + H_CASE_ENUM_TO_STRING(HAPI_NODETYPE_OBJ) + H_CASE_ENUM_TO_STRING(HAPI_NODETYPE_SOP) + H_CASE_ENUM_TO_STRING(HAPI_NODETYPE_CHOP) + H_CASE_ENUM_TO_STRING(HAPI_NODETYPE_ROP) + H_CASE_ENUM_TO_STRING(HAPI_NODETYPE_SHOP) + H_CASE_ENUM_TO_STRING(HAPI_NODETYPE_COP) + H_CASE_ENUM_TO_STRING(HAPI_NODETYPE_VOP) + H_CASE_ENUM_TO_STRING(HAPI_NODETYPE_DOP) + H_CASE_ENUM_TO_STRING(HAPI_NODETYPE_TOP) + default: + return TEXT("Unknown"); + } +} + +FString FHoudiniEngineUtils::PartTypeToString(HAPI_PartType PartType) +{ + switch (PartType) + { + H_CASE_ENUM_TO_STRING(HAPI_PARTTYPE_INVALID); + H_CASE_ENUM_TO_STRING(HAPI_PARTTYPE_MESH); + H_CASE_ENUM_TO_STRING(HAPI_PARTTYPE_CURVE) + H_CASE_ENUM_TO_STRING(HAPI_PARTTYPE_VOLUME); + H_CASE_ENUM_TO_STRING(HAPI_PARTTYPE_INSTANCER); + H_CASE_ENUM_TO_STRING(HAPI_PARTTYPE_BOX); + H_CASE_ENUM_TO_STRING(HAPI_PARTTYPE_SPHERE); + H_CASE_ENUM_TO_STRING(HAPI_PARTTYPE_MAX); + default: + return TEXT("Unknown"); + } +} + +FString FHoudiniEngineUtils::AttributeTypeToString(HAPI_AttributeTypeInfo AttributeType) +{ + switch (AttributeType) + { + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_INVALID); + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_NONE); + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_POINT); + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_HPOINT); + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_VECTOR); + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_NORMAL); + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_COLOR); + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_QUATERNION); + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_MATRIX3); + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_MATRIX); + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_ST); + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_HIDDEN); + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_BOX2); + H_CASE_ENUM_TO_STRING(HAPI_ATTRIBUTE_TYPE_BOX); + default: + return TEXT("Unknown"); + } +} + +FString FHoudiniEngineUtils::StorageTypeToString(HAPI_StorageType StorageType) +{ + switch (StorageType) + { + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_INVALID); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_INT); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_INT64); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_FLOAT); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_FLOAT64); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_STRING); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_UINT8); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_INT8); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_INT16); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_DICTIONARY); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_INT_ARRAY); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_INT64_ARRAY); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_FLOAT_ARRAY); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_FLOAT64_ARRAY); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_STRING_ARRAY); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_UINT8_ARRAY); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_INT8_ARRAY); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_INT16_ARRAY); + H_CASE_ENUM_TO_STRING(HAPI_STORAGETYPE_DICTIONARY_ARRAY); + default: return TEXT("Unknown"); + } +} + +FString FHoudiniEngineUtils::CurveTypeToString(HAPI_CurveType CurveType) +{ + switch (CurveType) + { + H_CASE_ENUM_TO_STRING(HAPI_CURVETYPE_INVALID) + H_CASE_ENUM_TO_STRING(HAPI_CURVETYPE_LINEAR) + H_CASE_ENUM_TO_STRING(HAPI_CURVETYPE_NURBS) + H_CASE_ENUM_TO_STRING(HAPI_CURVETYPE_BEZIER) + H_CASE_ENUM_TO_STRING(HAPI_CURVETYPE_MAX) + default: + return TEXT("Unknown"); + } +} + +FString FHoudiniEngineUtils::RSTOrderToString(HAPI_RSTOrder RstOrder) +{ + switch (RstOrder) + { + H_CASE_ENUM_TO_STRING(HAPI_TRS) + H_CASE_ENUM_TO_STRING(HAPI_TSR) + H_CASE_ENUM_TO_STRING(HAPI_RST) + H_CASE_ENUM_TO_STRING(HAPI_RTS) + H_CASE_ENUM_TO_STRING(HAPI_STR) + H_CASE_ENUM_TO_STRING(HAPI_SRT) + default: + return TEXT("Unknown"); + } +} + +#undef H_CASE_ENUM_TO_STRING + +FString FHoudiniEngineUtils::HapiTransformToString(HAPI_Transform Transform) +{ + FStringBuilderBase Output; + Output.Appendf(TEXT("P: %f, %f, %f "), Transform.position[0], Transform.position[1], Transform.position[2]); + Output.Appendf(TEXT("Q: %f, %f, %f, %f "), Transform.rotationQuaternion[0], Transform.rotationQuaternion[1], + Transform.rotationQuaternion[2], Transform.rotationQuaternion[3]); + Output.Appendf(TEXT("S: %f, %f, %f "), Transform.scale[0], Transform.scale[1], Transform.scale[2]); + Output.Appendf(TEXT("SH: %f, %f, %f "), Transform.shear[0], Transform.shear[1], Transform.shear[2]); + Output.Appendf(TEXT("RST Order: %s\n"), *RSTOrderToString(Transform.rstOrder)); + return Output.ToString(); +} + +FString FHoudiniEngineUtils::DumpNode(HAPI_NodeId NodeId) +{ + if (NodeId == INDEX_NONE) + return TEXT("Invalid Node ID\n"); + + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + + HAPI_Result Result = FHoudiniApi::GetNodeInfo(FHoudiniEngine::Get().GetSession(), NodeId, &NodeInfo); + if(Result != HAPI_RESULT_SUCCESS) + return FString::Printf(TEXT("Failed to get node info: %s\n"), *FHoudiniEngineUtils::GetErrorDescription()); + + FStringBuilderBase Output; + + + Output.Appendf(TEXT("Node ID: %d\n"), NodeId); + Output.Appendf(TEXT(" Name: %s\n"), *FHoudiniEngineString(NodeInfo.nameSH).ToFString()); + Output.Appendf(TEXT(" Type: %s\n"), *NodeTypeToString(NodeInfo.type)); + + // Get GeoInfo for this node + HAPI_GeoInfo GeoInfo; + FHoudiniApi::GeoInfo_Init(&GeoInfo); + Result = FHoudiniApi::GetGeoInfo(FHoudiniEngine::Get().GetSession(), NodeId, &GeoInfo); + if(Result != HAPI_RESULT_SUCCESS) + { + Output.Appendf(TEXT(" No GeoInfo, reason: %s\n"), *FHoudiniEngineUtils::GetErrorDescription()); + return Output.ToString(); + } + + Output.Append(TEXT(" Part Count: %d\n"), GeoInfo.partCount); + + for (int PartIndex = 0; PartIndex < GeoInfo.partCount; PartIndex++) + { + DumpPart(NodeId, PartIndex, Output); + } + return Output.ToString(); +} + +FString FHoudiniEngineUtils::DumpAttribute(HAPI_NodeId NodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner, + const FString& Name) +{ + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + HAPI_Result Result = FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), NodeId, PartId, + TCHAR_TO_ANSI(*Name), Owner, &AttributeInfo); + if(Result != HAPI_RESULT_SUCCESS) + { + return FString::Printf(TEXT("Failed to get attribute info: %s\n"), *FHoudiniEngineUtils::GetErrorDescription()); + } + + FStringBuilderBase Output; + Output.Appendf(TEXT(" Storage: %s\n"), *StorageTypeToString(AttributeInfo.storage)); + Output.Appendf(TEXT(" Type: %s\n"), *AttributeTypeToString(AttributeInfo.typeInfo)); + Output.Appendf(TEXT(" Tuple Size: %d\n"), AttributeInfo.tupleSize); + Output.Appendf(TEXT(" Count: %d\n"), AttributeInfo.count); + Output.Appendf(TEXT(" Total Array Elements: %d\n"), AttributeInfo.totalArrayElements); + return Output.ToString(); +} + + +void FHoudiniEngineUtils::DumpPart(HAPI_NodeId NodeId, HAPI_PartId PartId, FStringBuilderBase& Output) +{ + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + HAPI_Result Result = FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), NodeId, PartId, &PartInfo); + if(Result != HAPI_RESULT_SUCCESS) + { + Output.Appendf(TEXT(" Failed to get part info: %s\n"), *FHoudiniEngineUtils::GetErrorDescription()); + return; + } + + Output.Appendf(TEXT("Part %d\n"), PartId); + Output.Appendf(TEXT(" Part Name: %s\n"), *FHoudiniEngineString(PartInfo.nameSH).ToFString()); + Output.Appendf(TEXT(" Part Type: %s\n"), *PartTypeToString(PartInfo.type)); + Output.Appendf(TEXT(" Part Face Count: %d\n"), PartInfo.faceCount); + Output.Appendf(TEXT(" Part Vertex Count: %d\n"), PartInfo.vertexCount); + Output.Appendf(TEXT(" Part Point Count: %d\n"), PartInfo.pointCount); + Output.Appendf(TEXT(" Part Vertex Attribute Count: %d\n"), PartInfo.attributeCounts[HAPI_ATTROWNER_VERTEX]); + Output.Appendf(TEXT(" Part Point Attribute Count: %d\n"), PartInfo.attributeCounts[HAPI_ATTROWNER_POINT]); + Output.Appendf(TEXT(" Part Primitive Attribute Count: %d\n"), PartInfo.attributeCounts[HAPI_ATTROWNER_PRIM]); + Output.Appendf(TEXT(" Part Detail Attribute Count: %d\n"), PartInfo.attributeCounts[HAPI_ATTROWNER_DETAIL]); + Output.Appendf(TEXT(" Part Is Instanced: %d\n"), PartInfo.isInstanced ? 1 : 0); + Output.Appendf(TEXT(" Instance Count: %d\n"), PartInfo.instanceCount); + Output.Appendf(TEXT(" Instance Part Count: %d\n"), PartInfo.instancedPartCount ? 1 : 0); + + switch (PartInfo.type) + { + case HAPI_PARTTYPE_CURVE: + HAPI_CurveInfo CurveInfo; + FHoudiniApi::CurveInfo_Init(&CurveInfo); + Result = FHoudiniApi::GetCurveInfo(FHoudiniEngine::Get().GetSession(), NodeId, PartId, &CurveInfo); + if(Result != HAPI_RESULT_SUCCESS) + { + Output.Appendf(TEXT(" Failed to get curve info: %s\n"), *FHoudiniEngineUtils::GetErrorDescription()); + return; + } + Output.Appendf(TEXT(" Curve:\n")); + Output.Appendf(TEXT(" Curve Type: %s\n"), *CurveTypeToString(CurveInfo.curveType)); + Output.Appendf(TEXT(" Curve Count: %d\n"), CurveInfo.curveCount); + Output.Appendf(TEXT(" Vertex Count: %d\n"), CurveInfo.vertexCount); + Output.Appendf(TEXT(" Knot Count: %d\n"), CurveInfo.knotCount); + Output.Appendf(TEXT(" Periodic: %d\n"), CurveInfo.isPeriodic ? 1 : 0); + Output.Appendf(TEXT(" Rational: %d\n"), CurveInfo.isRational ? 1 : 0); + Output.Appendf(TEXT(" Order: %d\n"), CurveInfo.order); + Output.Appendf(TEXT(" Has Knots: %d\n"), CurveInfo.hasKnots); + Output.Appendf(TEXT(" Is Closed: %d\n"), CurveInfo.isClosed ? 1 : 0); + break; + case HAPI_PARTTYPE_VOLUME: + HAPI_VolumeInfo VolumeInfo; + FHoudiniApi::VolumeInfo_Init(&VolumeInfo); + Result = FHoudiniApi::GetVolumeInfo(FHoudiniEngine::Get().GetSession(), NodeId, PartId, &VolumeInfo); + if(Result != HAPI_RESULT_SUCCESS) + { + Output.Appendf(TEXT(" Failed to get volume info: %s\n"), *FHoudiniEngineUtils::GetErrorDescription()); + return; + } + Output.Appendf(TEXT(" Volume:\n")); + Output.Appendf(TEXT(" X Length: %d\n"), VolumeInfo.xLength); + Output.Appendf(TEXT(" Y Length: %d\n"), VolumeInfo.yLength); + Output.Appendf(TEXT(" Z Length: %d\n"), VolumeInfo.zLength); + Output.Appendf(TEXT(" Tuple Size: %d\n"), VolumeInfo.tupleSize); + Output.Appendf(TEXT(" Storage: %s\n"), *StorageTypeToString(VolumeInfo.storage)); + Output.Appendf(TEXT(" Tile Size: %d\n"), VolumeInfo.tileSize); + Output.Appendf(TEXT(" Has Taper: %d\n"), VolumeInfo.hasTaper); + Output.Appendf(TEXT(" X Taper: %f\n"), VolumeInfo.xTaper); + Output.Appendf(TEXT(" Y Taper: %f\n"), VolumeInfo.yTaper); + break; + + case HAPI_PARTTYPE_INSTANCER: + { + TArray InstancedPartIds; + InstancedPartIds.SetNum(PartInfo.instancedPartCount); + + Result = FHoudiniApi::GetInstancedPartIds(FHoudiniEngine::Get().GetSession(), + NodeId, PartId, InstancedPartIds.GetData(), 0, PartInfo.instancedPartCount); + if(Result != HAPI_RESULT_SUCCESS) + { + Output.Appendf(TEXT(" Failed to get instanced part ids: %s\n"), *FHoudiniEngineUtils::GetErrorDescription()); + return; + } + + Output.Append(TEXT(" Instance Ids: ")); + for (int Index = 0; Index < InstancedPartIds.Num(); Index++) + { + Output.Appendf(TEXT("%d "), InstancedPartIds[Index]); + } + Output.Appendf(TEXT("\n")); + } + break; + default: + break; + } + + TArray AttrNames = FHoudiniEngineUtils::GetAttributeNames(FHoudiniEngine::Get().GetSession(), + NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_VERTEX); + + for (int32 AttrIdx = 0; AttrIdx < AttrNames.Num(); ++AttrIdx) + { + Output.Appendf(TEXT(" Vertex Attribute: %s\n"), *AttrNames[AttrIdx]); + Output.Append(DumpAttribute(NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_VERTEX, AttrNames[AttrIdx])); + } + + AttrNames = FHoudiniEngineUtils::GetAttributeNames(FHoudiniEngine::Get().GetSession(), NodeId, PartId, + HAPI_AttributeOwner::HAPI_ATTROWNER_POINT); + for (int32 AttrIdx = 0; AttrIdx < AttrNames.Num(); ++AttrIdx) + { + Output.Appendf(TEXT(" Point Attribute: %s\n"), *AttrNames[AttrIdx]); + Output.Append(DumpAttribute(NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_POINT, AttrNames[AttrIdx])); + } + + AttrNames = FHoudiniEngineUtils::GetAttributeNames(FHoudiniEngine::Get().GetSession(), NodeId, PartId, + HAPI_AttributeOwner::HAPI_ATTROWNER_PRIM); + for (int32 AttrIdx = 0; AttrIdx < AttrNames.Num(); ++AttrIdx) + { + Output.Appendf(TEXT(" Prims Attribute: %s\n"), *AttrNames[AttrIdx]); + Output.Append(DumpAttribute(NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_PRIM, AttrNames[AttrIdx])); + } + + AttrNames = FHoudiniEngineUtils::GetAttributeNames(FHoudiniEngine::Get().GetSession(), NodeId, PartId, + HAPI_AttributeOwner::HAPI_ATTROWNER_DETAIL); + for (int32 AttrIdx = 0; AttrIdx < AttrNames.Num(); ++AttrIdx) + { + Output.Appendf(TEXT(" Detail Attribute: %s\n"), *AttrNames[AttrIdx]); + Output.Append(DumpAttribute(NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_DETAIL, AttrNames[AttrIdx])); + } +} + +EHoudiniProxyRefineRequestResult +FHoudiniEngineUtils::RefineHoudiniProxyMeshActorArrayToStaticMeshes(const TArray& InActorsToRefine, bool bSilent) +{ + const bool bRefineAll = true; + const bool bOnPreSaveWorld = false; + UWorld* OnPreSaveWorld = nullptr; + const bool bOnPreBeginPIE = false; + + // First find the Cookables that have meshes that we must refine + TArray CookablesToRefine; + TArray CookablesToCook; + // Cookables that would be candidates for refinement/cooking, but have errors + TArray SkippedCookables; + for(const AHoudiniAssetActor* HoudiniAssetActor : InActorsToRefine) + { + if(!IsValid(HoudiniAssetActor)) + continue; + + UHoudiniCookable* HoudiniCookable = HoudiniAssetActor->GetHoudiniCookable(); + if(!IsValid(HoudiniCookable)) + continue; + + // Check if we should consider this component for proxy mesh refinement or cooking, based on its settings and + // flags passed to the function. + TriageHoudiniCookablesForProxyMeshRefinement(HoudiniCookable, bRefineAll, bOnPreSaveWorld, OnPreSaveWorld, bOnPreBeginPIE, CookablesToRefine, CookablesToCook, SkippedCookables); + } + + return RefineTriagedHoudiniProxyMeshesToStaticMeshes( + CookablesToRefine, + CookablesToCook, + SkippedCookables, + bSilent, + bRefineAll, + bOnPreSaveWorld, + OnPreSaveWorld, + bOnPreBeginPIE + ); +} + + +void +FHoudiniEngineUtils::TriageHoudiniCookablesForProxyMeshRefinement( + UHoudiniCookable* InHC, + bool bRefineAll, + bool bOnPreSaveWorld, + UWorld* OnPreSaveWorld, + bool bOnPreBeginPIE, + TArray& OutToRefine, + TArray& OutToCook, + TArray& OutSkipped) +{ + if(!IsValid(InHC)) + return; + + // Make sure that the cookable's World and Owner are valid + AActor* Owner = InHC->GetOwner(); + if(!IsValid(Owner)) + return; + + UWorld* World = InHC->GetWorld(); + + // No need to return here if we're just starting PIE + if(bOnPreSaveWorld && !IsValid(World)) + return; + + if(bOnPreSaveWorld && OnPreSaveWorld && OnPreSaveWorld != World) + return; + + // Check if we should consider this component for proxy mesh refinement based on its settings and + // flags passed to the function + if(bRefineAll || + (bOnPreSaveWorld && InHC->IsProxyStaticMeshRefinementOnPreSaveWorldEnabled()) || + (bOnPreBeginPIE && InHC->IsProxyStaticMeshRefinementOnPreBeginPIEEnabled())) + { + TArray ProxyMeshPackagesToSave; + TArray CookablesWithProxiesToSave; + + if(InHC->HasAnyCurrentProxyOutput()) + { + // Get the state of the asset and check if it is cooked + // If it is not cook, request a cook. We can only build the UStaticMesh + // if the data from the cook is available + // If the state is not pre-cook, or None (cooked), then the state is invalid, + // log an error and skip the component + bool bNeedsRebuildOrDelete = false; + bool bUnsupportedState = false; + const bool bCookedDataAvailable = InHC->IsHoudiniCookedDataAvailable(bNeedsRebuildOrDelete, bUnsupportedState); + if(bCookedDataAvailable) + { + OutToRefine.Add(InHC); + CookablesWithProxiesToSave.Add(InHC); + } + else if(!bUnsupportedState && !bNeedsRebuildOrDelete) + { + InHC->MarkAsNeedCook(); + // Force the output of the cook to be directly created as a UStaticMesh and not a proxy + InHC->SetNoProxyMeshNextCookRequested(true); + OutToCook.Add(InHC); + CookablesWithProxiesToSave.Add(InHC); + } + else + { + OutSkipped.Add(InHC); + const EHoudiniAssetState State = InHC->GetCurrentState(); + HOUDINI_LOG_ERROR(TEXT("Could not refine %s, the asset is in an unsupported state: %s"), *(InHC->GetPathName()), *(UEnum::GetValueAsString(State))); + } + } + else if(InHC->HasAnyProxyOutput()) + { + // If the HC has non-current proxies, destroy them + // TODO: Make this its own command? + const uint32 NumOutputs = InHC->GetNumOutputs(); + for(uint32 Index = 0; Index < NumOutputs; ++Index) + { + UHoudiniOutput* Output = InHC->GetOutputAt(Index); + if(!IsValid(Output)) + continue; + + TMap& OutputObjects = Output->GetOutputObjects(); + for(auto& CurrentPair : OutputObjects) + { + FHoudiniOutputObject& CurrentOutputObject = CurrentPair.Value; + if(!CurrentOutputObject.bProxyIsCurrent) + { + // The proxy is not current, delete it and its component + USceneComponent* FoundProxyComponent = Cast(CurrentOutputObject.ProxyComponent); + if(IsValid(FoundProxyComponent)) + { + // Remove from the HoudiniAssetActor + if(FoundProxyComponent->GetOwner()) + FoundProxyComponent->GetOwner()->RemoveOwnedComponent(FoundProxyComponent); + + FoundProxyComponent->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + FoundProxyComponent->UnregisterComponent(); + FoundProxyComponent->DestroyComponent(); + } + + UObject* ProxyObject = CurrentOutputObject.ProxyObject; + if(!IsValid(ProxyObject)) + continue; + + // Just mark the object as garbage and his package as dirty + // Do not save the package automatically - as will cause crashes in PIE + ProxyObject->MarkAsGarbage(); + ProxyObject->MarkPackageDirty(); + } + } + } + } + + for(UHoudiniCookable* const HC : CookablesWithProxiesToSave) + { + const uint32 NumOutputs = HC->GetNumOutputs(); + for(uint32 Index = 0; Index < NumOutputs; ++Index) + { + UHoudiniOutput* Output = HC->GetOutputAt(Index); + if(!IsValid(Output)) + continue; + + TMap& OutputObjects = Output->GetOutputObjects(); + for(auto& CurrentPair : OutputObjects) + { + FHoudiniOutputObject& CurrentOutputObject = CurrentPair.Value; + if(CurrentOutputObject.bProxyIsCurrent && CurrentOutputObject.ProxyObject) + { + UPackage* const Package = CurrentOutputObject.ProxyObject->GetPackage(); + if(IsValid(Package) && Package->IsDirty()) + ProxyMeshPackagesToSave.Add(Package); + } + } + } + } + + if(ProxyMeshPackagesToSave.Num() > 0) + { + TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); + + FEditorFileUtils::PromptForCheckoutAndSave(ProxyMeshPackagesToSave, true, false); + } + } +} + + +EHoudiniProxyRefineRequestResult +FHoudiniEngineUtils::RefineTriagedHoudiniProxyMeshesToStaticMeshes( + const TArray& InCookablesToRefine, + const TArray& InCookablesToCook, + const TArray& InSkippedCookables, + bool bInSilent, + bool bInRefineAll, + bool bInOnPreSaveWorld, + UWorld* InOnPreSaveWorld, + bool bInOnPrePIEBeginPlay) +{ + // Slate notification text + FString Notification = TEXT("Refining Houdini proxy meshes to static meshes..."); + + const uint32 NumCookablesToCook = InCookablesToCook.Num(); + const uint32 NumCookablesToRefine = InCookablesToRefine.Num(); + const uint32 NumCookablesToProcess = NumCookablesToCook + NumCookablesToRefine; + + TArray SuccessfulCookables; + TArray FailedCookables; + TArray SkippedCookables(InSkippedCookables); + + auto AllowPlayInEditorRefinementFn = [&bInOnPrePIEBeginPlay, &InCookablesToCook, &InCookablesToRefine](bool bEnabled, bool bRefinementDone) { + if(bInOnPrePIEBeginPlay) + { + // Flag the cookables that need cooking / refinement as cookable in PIE mode. + // No other cooking will be allowed. + // Once refinement is done, we'll unset these flags again. + SetAllowPlayInEditorRefinement(InCookablesToCook, true); + SetAllowPlayInEditorRefinement(InCookablesToRefine, true); + if(bRefinementDone) + { + // Don't tick during PIE. We'll resume ticking when PIE is stopped. + FHoudiniEngine::Get().StopTicking(true, false); + } + } + }; + + AllowPlayInEditorRefinementFn(true, false); + + if(NumCookablesToProcess > 0) + { + // The task progress pointer is potentially going to be shared with a background thread and tasks + // on the main thread, so make it thread safe + TSharedPtr TaskProgress = MakeShareable(new FSlowTask((float)NumCookablesToProcess, FText::FromString(Notification))); + TaskProgress->Initialize(); + if(!bInSilent) + TaskProgress->MakeDialog(true); + + // Iterate over the Cookables for which we can build UStaticMesh, and build the meshes + bool bCancelled = false; + for(uint32 ComponentIndex = 0; ComponentIndex < NumCookablesToRefine; ++ComponentIndex) + { + UHoudiniCookable* Cookable = InCookablesToRefine[ComponentIndex]; + TaskProgress->EnterProgressFrame(1.0f); + const bool bDestroyProxies = true; + FHoudiniOutputTranslator::BuildStaticMeshesOnHoudiniProxyMeshOutputs(Cookable, bDestroyProxies); + + SuccessfulCookables.Add(Cookable); + + bCancelled = TaskProgress->ShouldCancel(); + if(bCancelled) + { + for(uint32 SkippedIndex = ComponentIndex + 1; SkippedIndex < NumCookablesToRefine; ++SkippedIndex) + { + SkippedCookables.Add(InCookablesToRefine[ComponentIndex]); + } + break; + } + } + + if(bCancelled && NumCookablesToCook > 0) + { + for(UHoudiniCookable* const HC : InCookablesToCook) + { + SkippedCookables.Add(HC); + } + } + + if(NumCookablesToCook > 0 && !bCancelled) + { + // Now use an async task to check on the progress of the cooking Cookables + Async(EAsyncExecution::Thread, [InCookablesToCook, TaskProgress, NumCookablesToProcess, + bInOnPreSaveWorld, InOnPreSaveWorld, + SuccessfulCookables, FailedCookables, SkippedCookables]() { + RefineHoudiniProxyMeshesToStaticMeshesWithCookInBackgroundThread( + InCookablesToCook, TaskProgress, NumCookablesToProcess, bInOnPreSaveWorld, InOnPreSaveWorld, + SuccessfulCookables, FailedCookables, SkippedCookables); + }); + + // We have to wait for cook(s) before completing refinement + return EHoudiniProxyRefineRequestResult::PendingCooks; + } + else + { + RefineHoudiniProxyMeshesToStaticMeshesNotifyDone( + NumCookablesToProcess, TaskProgress.Get(), bCancelled, bInOnPreSaveWorld, InOnPreSaveWorld, + SuccessfulCookables, FailedCookables, SkippedCookables); + + // We didn't have to cook anything, so refinement is complete. + AllowPlayInEditorRefinementFn(false, true); + return EHoudiniProxyRefineRequestResult::Refined; + } + } + + // Nothing to refine + AllowPlayInEditorRefinementFn(false, true); + return EHoudiniProxyRefineRequestResult::None; +} + +void +FHoudiniEngineUtils::RefineHoudiniProxyMeshesToStaticMeshesNotifyDone( + const uint32 InNumTotalCookables, + FSlowTask* const InTaskProgress, + const bool bCancelled, + const bool bOnPreSaveWorld, + UWorld* const InOnPreSaveWorld, + const TArray& InSuccessfulCookables, + const TArray& InFailedCookables, + const TArray& InSkippedCookables) +{ + FString Notification; + const uint32 NumSkippedCookables = InSkippedCookables.Num(); + const uint32 NumFailedToCook = InFailedCookables.Num(); + if(NumSkippedCookables + NumFailedToCook > 0) + { + if(bCancelled) + { + Notification = FString::Printf(TEXT("Refinement cancelled after completing %d / %d cookables. The remaining Cookables were skipped, in an invalid state, or could not be cooked. See the log for details."), NumSkippedCookables + NumFailedToCook, InNumTotalCookables); + } + else + { + Notification = FString::Printf(TEXT("Failed to refine %d / %d Cookables, the Cookables were in an invalid state, and were either not cooked or could not be cooked. See the log for details."), NumSkippedCookables + NumFailedToCook, InNumTotalCookables); + } + FHoudiniEngineUtils::CreateSlateNotification(Notification); + HOUDINI_LOG_ERROR(TEXT("%s"), *Notification); + } + else if(InNumTotalCookables > 0) + { + Notification = TEXT("Done: Refining Houdini proxy meshes to static meshes."); + HOUDINI_LOG_MESSAGE(TEXT("%s"), *Notification); + } + if(InTaskProgress) + { + InTaskProgress->Destroy(); + } + if(bOnPreSaveWorld && InSuccessfulCookables.Num() > 0) + { + FDelegateHandle& OnPostSaveWorldHandle = FHoudiniEngineUtils::GetOnPostSaveWorldRefineProxyMeshesHandle(); + if(OnPostSaveWorldHandle.IsValid()) + { + if(FEditorDelegates::PostSaveWorldWithContext.Remove(OnPostSaveWorldHandle)) + OnPostSaveWorldHandle.Reset(); + } + + // Save the dirty static meshes in InSuccessfulCookables OnPostSaveWorld + // TODO: Remove? This may not be necessary now as we save all dirty temporary cook data in + // PostSaveWorldWithContext() already (Static Meshes, Materials...) + OnPostSaveWorldHandle = FEditorDelegates::PostSaveWorldWithContext.AddLambda( + [InSuccessfulCookables, bOnPreSaveWorld, InOnPreSaveWorld](UWorld* InWorld, FObjectPostSaveContext InContext) + { + if(bOnPreSaveWorld && InOnPreSaveWorld && InOnPreSaveWorld != InWorld) + return; + + RefineProxyMeshesHandleOnPostSaveWorld(InSuccessfulCookables, InContext.GetSaveFlags(), InWorld, InContext.SaveSucceeded()); + + FDelegateHandle& OnPostSaveWorldHandle = FHoudiniEngineUtils::GetOnPostSaveWorldRefineProxyMeshesHandle(); + if(OnPostSaveWorldHandle.IsValid()) + { + if(FEditorDelegates::PostSaveWorldWithContext.Remove(OnPostSaveWorldHandle)) + OnPostSaveWorldHandle.Reset(); + } + }); + } + + SetAllowPlayInEditorRefinement(InSuccessfulCookables, false); + SetAllowPlayInEditorRefinement(InFailedCookables, false); + SetAllowPlayInEditorRefinement(InSkippedCookables, false); + + // Broadcast refinement result per cookable + for(UHoudiniCookable* const HC : InSuccessfulCookables) + { + if(OnHoudiniProxyMeshesRefinedDelegate.IsBound()) + OnHoudiniProxyMeshesRefinedDelegate.Broadcast(HC, EHoudiniProxyRefineResult::Success); + } + for(UHoudiniCookable* const HC : InFailedCookables) + { + if(OnHoudiniProxyMeshesRefinedDelegate.IsBound()) + OnHoudiniProxyMeshesRefinedDelegate.Broadcast(HC, EHoudiniProxyRefineResult::Failed); + } + for(UHoudiniCookable* const HC : InSkippedCookables) + { + if(OnHoudiniProxyMeshesRefinedDelegate.IsBound()) + OnHoudiniProxyMeshesRefinedDelegate.Broadcast(HC, EHoudiniProxyRefineResult::Skipped); + } + + // Update details to display the new inputs + FHoudiniEngineUtils::UpdateEditorProperties(true); +} + +void +FHoudiniEngineUtils::RefineProxyMeshesHandleOnPostSaveWorld(const TArray& InSuccessfulCookables, uint32 InSaveFlags, UWorld* InWorld, bool bInSuccess) +{ + TArray PackagesToSave; + + for(UHoudiniCookable* HC : InSuccessfulCookables) + { + if(!IsValid(HC)) + continue; + + const int32 NumOutputs = HC->GetNumOutputs(); + for(int32 Index = 0; Index < NumOutputs; ++Index) + { + UHoudiniOutput* Output = HC->GetOutputAt(Index); + if(!IsValid(Output)) + continue; + + if(Output->GetType() != EHoudiniOutputType::Mesh) + continue; + + for(auto& OutputObjectPair : Output->GetOutputObjects()) + { + UObject* Obj = OutputObjectPair.Value.OutputObject; + if(!IsValid(Obj)) + continue; + + UStaticMesh* SM = Cast(Obj); + if(!SM) + continue; + + UPackage* Package = SM->GetOutermost(); + if(!IsValid(Package)) + continue; + + if(Package->IsDirty() && Package->IsFullyLoaded() && Package != GetTransientPackage()) + { + PackagesToSave.Add(Package); + } + } + } + } + + UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, true); +} + +void +FHoudiniEngineUtils::RefineHoudiniProxyMeshesToStaticMeshesWithCookInBackgroundThread( + const TArray& InCookablesToCook, + TSharedPtr InTaskProgress, + const uint32 InNumCookablesToProcess, + bool bInOnPreSaveWorld, + UWorld* InOnPreSaveWorld, + const TArray& InSuccessfulCookables, + const TArray& InFailedCookables, + const TArray& InSkippedCookables) +{ + // Copy to a double linked list so that we can loop through + // to check progress of each component and remove it easily + // if it has completed/failed + TDoubleLinkedList CookList; + for(UHoudiniCookable* HC : InCookablesToCook) + { + CookList.AddTail(HC); + } + + // Add the successfully cooked Cookables to the incoming successful Cookables (previously refined) + TArray SuccessfulCookables(InSuccessfulCookables); + TArray FailedCookables(InFailedCookables); + TArray SkippedCookables(InSkippedCookables); + + bool bCancelled = false; + uint32 NumFailedToCook = 0; + while(CookList.Num() > 0 && !bCancelled) + { + TDoubleLinkedList::TDoubleLinkedListNode* Node = CookList.GetHead(); + while(Node && !bCancelled) + { + TDoubleLinkedList::TDoubleLinkedListNode* Next = Node->GetNextNode(); + UHoudiniCookable* HC = Node->GetValue(); + + if(IsValid(HC)) + { + const EHoudiniAssetState State = HC->GetCurrentState(); + const EHoudiniAssetStateResult ResultState = HC->GetCurrentStateResult(); + bool bUpdateProgress = false; + if(State == EHoudiniAssetState::None) + { + // Cooked, count as success, remove node + CookList.RemoveNode(Node); + SuccessfulCookables.Add(HC); + bUpdateProgress = true; + } + else if(ResultState != EHoudiniAssetStateResult::None && ResultState != EHoudiniAssetStateResult::Working) + { + // Failed, remove node + HOUDINI_LOG_ERROR(TEXT("Failed to cook %s to obtain static mesh."), *(HC->GetPathName())); + CookList.RemoveNode(Node); + FailedCookables.Add(HC); + bUpdateProgress = true; + NumFailedToCook++; + } + + if(bUpdateProgress && InTaskProgress.IsValid()) + { + // Update progress only on the main thread, and check for cancellation request + bCancelled = Async(EAsyncExecution::TaskGraphMainThread, [InTaskProgress]() { + InTaskProgress->EnterProgressFrame(1.0f); + return InTaskProgress->ShouldCancel(); + }).Get(); + } + } + else + { + SkippedCookables.Add(HC); + CookList.RemoveNode(Node); + } + + Node = Next; + } + FPlatformProcess::Sleep(0.01f); + } + + if(bCancelled) + { + HOUDINI_LOG_WARNING(TEXT("Mesh refinement cancelled while waiting for %d Cookables to cook."), CookList.Num()); + // Mark any remaining HCs in the cook list as skipped + TDoubleLinkedList::TDoubleLinkedListNode* Node = CookList.GetHead(); + while(Node) + { + TDoubleLinkedList::TDoubleLinkedListNode* const Next = Node->GetNextNode(); + UHoudiniCookable* HC = Node->GetValue(); + if(HC) + SkippedCookables.Add(HC); + CookList.RemoveNode(Node); + Node = Next; + } + } + + // Cooking is done, or failed, display the notifications on the main thread + Async(EAsyncExecution::TaskGraphMainThread, [InNumCookablesToProcess, InTaskProgress, bCancelled, + bInOnPreSaveWorld, InOnPreSaveWorld, + SuccessfulCookables, FailedCookables, SkippedCookables]() + { + RefineHoudiniProxyMeshesToStaticMeshesNotifyDone( + InNumCookablesToProcess, InTaskProgress.Get(), bCancelled, + bInOnPreSaveWorld, InOnPreSaveWorld, + SuccessfulCookables, FailedCookables, SkippedCookables); + }); +} + +FDelegateHandle FHoudiniEngineUtils::OnPostSaveWorldRefineProxyMeshesHandle = FDelegateHandle(); + +void +FHoudiniEngineUtils::SetAllowPlayInEditorRefinement( + const TArray& InCookables, + bool bEnabled) +{ +#if WITH_EDITORONLY_DATA + for(UHoudiniCookable* Cookable : InCookables) + { + Cookable->SetAllowPlayInEditorRefinement(false); + } +#endif +} + +FHoudiniPerfTimer::FHoudiniPerfTimer(const FString & InText, bool bPrint) +{ + TotalTime = 0.0; + CurrentStart = -1.0; + Text = InText; + bPrintStats = bPrint; +} + +FHoudiniPerfTimer::~FHoudiniPerfTimer() +{ + if (CurrentStart >= 0) + { + Stop(); + } + + if(bPrintStats && !Text.IsEmpty()) + { + HOUDINI_LOG_MESSAGE(TEXT("Timer: %-20s %23f secs."), *Text, TotalTime); + } +} + + +double FHoudiniPerfTimer::GetTime() +{ + return TotalTime; +} + +void FHoudiniPerfTimer::Start() +{ + CurrentStart = FPlatformTime::Seconds(); + +} + +void FHoudiniPerfTimer::Stop() +{ + if(CurrentStart >= 0) + { + TotalTime += FPlatformTime::Seconds() - CurrentStart; + } + CurrentStart = -1.0; + +} + +TArray HoudiniTCHARToUTF(const TCHAR* Text) +{ + int32 Length = FCString::Strlen(Text); + TArray Result; + Result.SetNumZeroed(Length + 1); + + FTCHARToUTF8_Convert::Convert(&Result[0], Length, Text, Length); + + return Result; +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngine/Private/HoudiniEngineUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "HoudiniEnginePrivatePCH.h" +#include "EngineUtils.h" +#include "Misc/Optional.h" +#include + +#include "HoudiniAssetActor.h" +#include "HoudiniGenericAttribute.h" +#include "HoudiniOutput.h" +#include "HoudiniPackageParams.h" +#include "Containers/UnrealString.h" +#include "HoudiniEngineString.h" +#include "HAL/Platform.h" + +class FString; +class UStaticMesh; +class UHoudiniAsset; +class UHoudiniAssetComponent; +class UHoudiniCookable; + +struct FHoudiniPartInfo; +struct FHoudiniMeshSocket; +struct FHoudiniGeoPartObject; + +enum class EHoudiniCurveType : int8; +enum class EHoudiniCurveMethod : int8; +enum class EHoudiniInstancerType : uint8; + +#define H_DEPRECATED_OLD_ATTRIBUTE_API(Version, Message) [[deprecated(Message " Please update your code to the new API before upgrading to the next release, otherwise your project will no longer compile.")]] + +TArray HOUDINIENGINE_API HoudiniTCHARToUTF(const TCHAR * Text); + +#define H_TCHAR_TO_UTF8(_H_UNREAL_STRING) HoudiniTCHARToUTF(_H_UNREAL_STRING).GetData() + +extern TAutoConsoleVariable CVarHoudiniEngineMeshBuildTimer; + +class FHoudiniParameterWidgetMetaData : public ISlateMetaData +{ +public: + SLATE_METADATA_TYPE(FHoudiniParameterWidgetMetaData, ISlateMetaData) + + FHoudiniParameterWidgetMetaData(const FString& UniqueName, const uint32 Index) + : UniqueName(UniqueName) + , Index(Index) + { + } + + bool operator==(const FHoudiniParameterWidgetMetaData& Other) const + { + return UniqueName == Other.UniqueName && Index == Other.Index; + } + + const FString UniqueName; + const uint32 Index; +}; + +struct FHoudiniPerfTimer +{ + // Accumulative poerformance timer, can be start and stopped to accumulate time. + // Will print out stats if bEnabled is true on destruction. Very light weight + // so can be used all the time. + + FHoudiniPerfTimer(const FString & Text, bool bPrintStats); + ~FHoudiniPerfTimer(); + void Start(); + void Stop(); + double GetTime(); + +protected: + double TotalTime; + double CurrentStart; + FString Text; + bool bPrintStats; +}; + +struct HOUDINIENGINE_API FHoudiniEngineUtils +{ + friend struct FUnrealMeshTranslator; + + public: + + // Multi-cast delegate type for broadcasting when proxy mesh refinement of a Cookable is complete. + DECLARE_MULTICAST_DELEGATE_TwoParams(FOnHoudiniProxyMeshesRefinedDelegate, UHoudiniCookable* const, const EHoudiniProxyRefineResult); + + // Force deletes an Unreal Object without throwing up warning dialogs. + static void ForceDeleteObject(UObject* Object); + + // Load libHAPI and return handle to it, also store location of loaded libHAPI in passed argument. + static void* LoadLibHAPI(FString& StoredLibHAPILocation); + + // Return true if module has been properly initialized. + static bool IsInitialized(); + + // Return type of license used. + static bool GetLicenseType(FString & LicenseType); + + // Cook the specified node id + // if the cook options are null, the defualt one will be used + // if bWaitForCompletion is true, this call will be blocking until the cook is finished + static bool HapiCookNode( + HAPI_NodeId InNodeId, + HAPI_CookOptions* InCookOptions = nullptr, + bool bWaitForCompletion = false); + + // Wrapper for CommitGeo - adds a profiler scope wrapper + static HAPI_Result HapiCommitGeo(HAPI_NodeId InNodeId); + + // Return a specified HAPI status string. + static const FString GetStatusString( + HAPI_StatusType status_type, + HAPI_StatusVerbosity verbosity); + + // HAPI : Return the string that corresponds to the given string handle. + static FString HapiGetString(int32 StringHandle); + + // Return a string representing cooking result. + static const FString GetCookResult(); + + // Return a string indicating cook state. + static const FString GetCookState(); + + // Return a string error description. + static const FString GetErrorDescription(); + + // Return a string description of error from a given error code. + static const FString GetErrorDescription(HAPI_Result Result); + + // Return a string description for a Houdini Engine session connection error. + static const FString GetConnectionError(); + + // Helper function used to indicate to all cookables that they need to be instantiated in the new HE session + // Needs to be call after starting/restarting/connecting/session syncing a HE session.. + static void MarkAllCookablesAsNeedInstantiation(); + + // Return the errors, warning and messages on a specified node + static const FString GetNodeErrorsWarningsAndMessages(HAPI_NodeId InNodeId); + + static const FString GetCookLog(const TArray& InNodeIds); + + static const FString GetAssetHelp(HAPI_NodeId InNodeId); + + static const FString GetAssetHelpURL(HAPI_NodeId InNodeId); + + // Updates the Object transform of a cookable + static bool UploadCookableTransform(UHoudiniCookable* HC); + + // Convert FString to std::string + static void ConvertUnrealString( + const FString& UnrealString, + std::string& String); + + // Wrapper for the CreateNode function + // As HAPI_CreateNode is an async call, this function actually waits for the node creation to be done before returning + static HAPI_Result CreateNode( + HAPI_NodeId InParentNodeId, + const FString& InOperatorName, + const FString& InNodeLabel, + HAPI_Bool bInCookOnCreation, + HAPI_NodeId* OutNewNodeId); + + static int32 HapiGetCookCount(HAPI_NodeId InNodeId); + + // HAPI : Retrieve the asset node's object transform. **/ + static bool HapiGetAssetTransform( + HAPI_NodeId InNodeId, + FTransform& OutTransform); + + // HAPI : Translate HAPI transform to Unreal one. + static void TranslateHapiTransform( + const HAPI_Transform& HapiTransform, + FTransform& UnrealTransform); + + // HAPI : Translate HAPI Euler transform to Unreal one. + static void TranslateHapiTransform( + const HAPI_TransformEuler& HapiTransformEuler, + FTransform& UnrealTransform); + + // HAPI : Translate Unreal transform to HAPI one. + static void TranslateUnrealTransform( + const FTransform& UnrealTransform, + HAPI_Transform& HapiTransform); + + // HAPI : Translate Unreal transform to HAPI Euler one. + static void TranslateUnrealTransform( + const FTransform& UnrealTransform, + HAPI_TransformEuler& HapiTransformEuler); + + // Translate an array of float position values from Houdini to Unreal + static void ConvertHoudiniPositionToUnrealVector( + const TArray& InRawData, + TArray& OutVectorData); + static FVector3f ConvertHoudiniPositionToUnrealVector3f( + const FVector3f& InVector); + + // Translate an array of float scale values from Houdini to Unreal + static void ConvertHoudiniScaleToUnrealVector( + const TArray& InRawData, + TArray& OutVectorData); + + // Translate an array of float quaternion rotation values from Houdini to Unreal + static void ConvertHoudiniRotQuatToUnrealVector( + const TArray& InRawData, + TArray& OutVectorData); + + // Translate an array of float euler rotation values from Houdini to Unreal + static void ConvertHoudiniRotEulerToUnrealVector( + const TArray& InRawData, + TArray& OutVectorData); + + // Return true if asset is valid. + static bool IsHoudiniNodeValid(HAPI_NodeId AssetId); + + // HAPI : Retrieve HAPI_ObjectInfo's from given asset node id. + static bool HapiGetObjectInfos( + HAPI_NodeId InNodeId, + TArray& OutObjectInfos, + TArray& OutObjectTransforms); + + // Traverse from the Child up to the Root node to determine whether the ChildNode is fully visible + // inside the RootNode. + // - The Obj node itself is visible + // - All parent nodes are visible + // - Only has Object subnet parents (if we find a parent with non-Object nodetype then it's not visible). + static bool IsObjNodeFullyVisible( + const TSet& AllObjectIds, + HAPI_NodeId RootNodeId, + HAPI_NodeId ChildNodeId); + + static bool HapiGetNodeType( + HAPI_NodeId InNodeId, + HAPI_NodeType& OutNodeType); + + static bool IsSopNode(HAPI_NodeId NodeId); + static bool ContainsSopNodes(HAPI_NodeId NodeId); + + // Get the output index of InNodeId (assuming InNodeId is an Output node). + // This is done by getting the value of the outputidx parameter on + // InNodeId. + // Returns false if outputidx could not be found/read. Sets OutOutputIndex to the + // value of the outputidx parameter. + static bool GetOutputIndex( + HAPI_NodeId InNodeId, + int32& OutOutputIndex); + + static bool GatherAllAssetOutputs( + HAPI_NodeId InAssetId, + bool bUseOutputNodes, + bool bOutputTemplatedGeos, + bool bGatherEditableCurves, + TArray& OutOutputNodes); + + // Get the immediate output geo infos for the given Geometry object network. + // Find immediate Display and output nodes (if enabled). + // If bIgnoreOutputNodes is false, only Display nodes will be retrieved. + // If bIgnoreOutputNodes is true, any output nodes will take precedence over display nodes. + static bool GatherImmediateOutputGeoInfos( + int InNodeId, + const bool bUseOutputNodes, + const bool bGatherTemplateNodes, + TArray& OutGeoInfos, + TSet& OutForceNodesCook); + + // HAPI: Retrieve absolute path to the given Node + static bool HapiGetAbsNodePath( + HAPI_NodeId InNodeId, + FString& OutPath); + + // HAPI: Retrieve Path to the given Node, relative to the given Node + static bool HapiGetNodePath( + HAPI_NodeId InNodeId, + HAPI_NodeId InRelativeToNodeId, + FString& OutPath); + + // HAPI: Retrieve the relative for the given HGPO Node + static bool HapiGetNodePath( + const FHoudiniGeoPartObject& InHGPO, + FString& OutPath); + + // HAPI: Retrieve a NodeId from a path + static bool HapiGetNodeFromPath( + const FString& InPath, + const HAPI_NodeId InPathRelativeToId, + HAPI_NodeId& OutNodeId); + + // HAPI : Return all group names for a given Geo. + static bool HapiGetGroupNames( + const HAPI_NodeId GeoId, + const HAPI_PartId PartId, + const HAPI_GroupType GroupType, + bool isPackedPrim, + TArray& OutGroupNames); + + // HAPI : Retrieve group membership. + static bool HapiGetGroupMembership( + HAPI_NodeId GeoId, + const HAPI_PartInfo& PartInfo, + const HAPI_GroupType& GroupType, + const FString & GroupName, + TArray& OutGroupMembership, + bool& OutAllEquals); + + static bool HapiGetGroupMembership( + HAPI_NodeId GeoId, + HAPI_PartId PartId, + const HAPI_GroupType& GroupType, + const FString& GroupName, + int32 & OutGroupMembership, + int Start = 0, + int Length = 1); + + // HAPI : Given vertex list, retrieve new vertex list for a specified group. + // Return number of processed valid index vertices for this split. + static int32 HapiGetVertexListForGroup( + HAPI_NodeId GeoId, + const HAPI_PartInfo& PartInfo, + const FString& GroupName, + const TArray& FullVertexList, + TArray& NewVertexList, + TArray& AllVertexList, + TArray& AllFaceList, + TArray& AllGroupFaceIndices, + int32& FirstValidVertex, + int32& FirstValidPrim, + bool isPackedPrim); + + // HAPI : Get attribute data as float. + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static bool HapiGetAttributeDataAsFloat( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + const char * InAttribName, + HAPI_AttributeInfo& OutAttributeInfo, + TArray& OutData, + int32 InTupleSize = 0, + HAPI_AttributeOwner InOwner = HAPI_ATTROWNER_INVALID, + int32 InStartIndex = 0, + int32 InCount = -1); + + // HAPI : Get attribute data as Integer. + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static bool HapiGetAttributeDataAsInteger( + const HAPI_NodeId InGeoId, + const HAPI_PartId InPartId, + const char * InAttribName, + HAPI_AttributeInfo& OutAttributeInfo, + TArray& OutData, + const int32 InTupleSize = 0, + const HAPI_AttributeOwner& InOwner = HAPI_ATTROWNER_INVALID, + const int32 InStartIndex = 0, + const int32 InCount = -1); + + // HAPI : Get attribute data as strings. + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static bool HapiGetAttributeDataAsString( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + const char * InAttribName, + HAPI_AttributeInfo& OutAttributeInfo, + TArray& OutData, + int32 InTupleSize = 0, + HAPI_AttributeOwner InOwner = HAPI_ATTROWNER_INVALID, + int32 InStartIndex = 0, + int32 InCount = -1); + + // HAPI : Get attribute data as strings. + static bool HapiGetAttributeDataAsStringFromInfo( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + const char * InAttribName, + HAPI_AttributeInfo& InAttributeInfo, + TArray& OutData, + int32 InStartIndex = 0, + int32 InCount = -1); + + // HAPI : Check if given attribute exists. + static bool HapiCheckAttributeExists( + HAPI_NodeId GeoId, + HAPI_PartId PartId, + const char * AttribName, + HAPI_AttributeOwner Owner = HAPI_ATTROWNER_INVALID); + + // HAPI: Returns all the attributes of a given type for a given owner + static int32 HapiGetAttributeOfType( + HAPI_NodeId GeoId, + HAPI_NodeId PartId, + const HAPI_AttributeOwner& AttributeOwner, + const HAPI_AttributeTypeInfo& AttributeType, + TArray& MatchingAttributesInfo, + TArray& MatchingAttributesName); + + // HAPI: Gets either a int or a int array + static bool HapiGetAttributeIntOrIntArray( + HAPI_NodeId GeoId, + HAPI_NodeId PartId, + const FString & AttribName, + const HAPI_AttributeOwner& AttributeOwner, + HAPI_AttributeInfo& OutAttributeInfo, + TArray& OutData + ); + + // HAPI: Gets either a float or a float array + static bool HapiGetAttributeFloatOrFloatArray( + HAPI_NodeId GeoId, + HAPI_NodeId PartId, + const FString & AttribName, + const HAPI_AttributeOwner& AttributeOwner, + HAPI_AttributeInfo& OutAttributeInfo, + TArray& OutData + ); + + // Retreives the first value of an attribute. OutData is left unchanged + // if there is an error, so you can initialize it with a default. + static bool HapiGetFirstAttributeValueAsInteger( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + const char* InAttribName, + const HAPI_AttributeOwner InAttribOwner, + int32 & OutData); + + // Retreives the first value of an attribute. OutData is left unchanged + // if there is an error. + static bool HapiGetFirstAttributeValueAsFloat( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + const char* InAttribName, + const HAPI_AttributeOwner InAttribOwner, + float& OutData); + + // Retreives the first value of an attribute. OutData is left unchanged + // if there is an error. + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static bool HapiGetFirstAttributeValueAsString( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + const char* InAttribName, + const HAPI_AttributeOwner InAttribOwner, + FString& OutData); + + + // HAPI : Look for a parameter by name and returns its index. Returns -1 if not found. + static HAPI_ParmId HapiFindParameterByName( + HAPI_NodeId InNodeId, + const std::string& InParmName, + HAPI_ParmInfo& OutFoundParmInfo); + + // HAPI : Look for a parameter by tag and returns its index. Returns -1 if not found. + static HAPI_ParmId HapiFindParameterByTag( + HAPI_NodeId InNodeId, + const std::string& InParmTag, + HAPI_ParmInfo& OutFoundParmInfo); + + // Returns true is the given Geo-Part is an attribute instancer + static bool IsAttributeInstancer( + HAPI_NodeId GeoId, + HAPI_PartId PartId, + EHoudiniInstancerType& OutInstancerType); + + static bool IsValidDataTable( + HAPI_NodeId GeoId, + HAPI_PartId PartId); + + // Returns true if the given Geo-Part is a landscape spline + static bool IsLandscapeSpline( + HAPI_NodeId GeoId, + HAPI_PartId PartId); + + // Returns true if the given Geo - Part is a landscape spline + static bool IsValidHeightfield( + HAPI_NodeId GeoId, + HAPI_PartId PartId); + + // HAPI : Return a give node's parent ID, -1 if none + static HAPI_NodeId HapiGetParentNodeId(HAPI_NodeId NodeId); + + // HAPI : Marshaling, disconnect input asset from a given slot. + static bool HapiDisconnectAsset( + HAPI_NodeId HostAssetId, + int32 InputIndex); + + // Destroy asset, returns the status. + static bool DestroyHoudiniAsset(HAPI_NodeId AssetId); + + // Deletes the specified HAPI node by id. + static bool DeleteHoudiniNode(HAPI_NodeId InNodeId); + + // Loads an HDA file and returns its AssetLibraryId + static bool LoadHoudiniAsset( + const UHoudiniAsset * HoudiniAsset, + HAPI_AssetLibraryId & OutAssetLibraryId); + + // Returns the name of the available subassets in a loaded HDA + static bool GetSubAssetNames( + const HAPI_AssetLibraryId& AssetLibraryId, + TArray< HAPI_StringHandle > & OutAssetNames); + + static bool OpenSubassetSelectionWindow( + TArray& AssetNames, + HAPI_StringHandle& OutPickedAssetName ); + + // Returns the name of a Houdini asset. + static bool GetHoudiniAssetName( + HAPI_NodeId AssetNodeId, + FString & NameString); + + // Gets preset data for a given node. + static bool GetAssetPreset( + HAPI_NodeId InNodeId, + TArray& PresetBuffer); + + // Sets preset data for a given node. + static bool SetAssetPreset( + HAPI_NodeId InNodeId, + const TArray& PresetBuffer); + + // HAPI : Set asset transform. + static bool HapiSetAssetTransform( + HAPI_NodeId AssetNodeId, + const FTransform & Transform); + + // TODO: Move me somewhere else + static void AssignUniqueActorLabelIfNeeded( + HAPI_NodeId InNodeId, + AActor* InActorOwner); + + // Triggers an update the details panel + // Will use an AsyncTask if we're not in the game thread + // NOTE: Prefer using IDetailLayoutBuilder::ForceRefreshDetails() instead. + static void UpdateEditorProperties(const bool bInForceFullUpdate); + + // Triggers an update the details panel + static void UpdateBlueprintEditor(UHoudiniAssetComponent* HAC); + + // Check if a cookable or parent cookable is being cooked + static bool IsHoudiniCookableCooking(UObject* InObj); + + // Helper function to set float attribute data + // The data will be sent in chunks if too large for thrift + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeFloatData( + const TArray& InFloatData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo, + bool bAttemptRunLengthEncoding = false); + + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeFloatData( + const float* InFloatData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo, + bool bAttemptRunLengthEncoding = false); + + // Helper function for setting unique float values + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeFloatUniqueData( + const float InFloatData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + // Helper function to set Int attribute data + // The data will be sent in chunks if too large for thrift + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeIntData( + const TArray& InIntData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo, + bool bAttemptRunLengthEncoding = false); + + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeIntData( + const int32* InIntData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo, + bool bAttemptRunLengthEncoding = false); + + // Helper function for setting unique int values + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeIntUniqueData( + const int32 InIntData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + // Helper function to set unsigned Int attribute data + // The data will be sent in chunks if too large for thrift + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeUIntData( + const TArray& InIntData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeUIntData( + const int64* InIntData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + // Helper function to set signed int8 attribute data + // The data will be sent in chunks if too large for thrift + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeInt8Data( + const TArray& InByteData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeInt8Data( + const int8* InByteData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + // Helper function to set Byte attribute data + // The data will be sent in chunks if too large for thrift + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeUInt8Data( + const TArray& InByteData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeUInt8Data( + const uint8* InByteData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + // Helper function to set signed int16 attribute data + // The data will be sent in chunks if too large for thrift + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeInt16Data( + const TArray& InShortData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeInt16Data( + const int16* InShortData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + // Helper function to set uint16 attribute data + // The data will be sent in chunks if too large for thrift + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeUInt16Data( + const TArray& InShortData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeUInt16Data( + const int32* InShortData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + // Helper function to set Int64 attribute data + // The data will be sent in chunks if too large for thrift + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeInt64Data( + const TArray& InInt64Data, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeInt64Data( + const int64* InInt64Data, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + // Helper function to set unsigned Int64 attribute data + // The data will be sent in chunks if too large for thrift + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeUInt64Data( + const TArray& InInt64Data, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + // Helper function to set Double attribute data + // The data will be sent in chunks if too large for thrift + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeDoubleData( + const TArray& InDoubleData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeDoubleData( + const double* InDoubleData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + // Helper function to set Vertex Lists + // The data will be sent in chunks if too large for thrift + static HAPI_Result HapiSetVertexList( + const TArray& InVertexListData, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId); + + // Helper function to set Face Counts + // The data will be sent in chunks if too large for thrift + static HAPI_Result HapiSetFaceCounts( + const TArray& InFaceCounts, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId); + + // Helper function to set attribute string data for a single FString + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeStringUniqueData( + const FString& InString, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + // Helper function to set attribute string data for a FString array + // The data will be sent in chunks if too large for thrift + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeStringData( + const TArray& InStringArray, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeStringMap( + const FHoudiniEngineIndexedStringMap& InIndexedStringMap, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeStringArrayData( + const TArray& InStringArray, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo, + const TArray& SizesFixedArray); + + // Helper function to set attribute dict data for a FString array + // The data will be sent in chunks if too large for thrift + H_DEPRECATED_OLD_ATTRIBUTE_API(20.5, "Use FHoudiniHapiAccessor instead.") + static HAPI_Result HapiSetAttributeDictionaryData( + const TArray& InStringArray, + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const FString& InAttributeName, + const HAPI_AttributeInfo& InAttributeInfo); + + + // Helper function to set Heightfield data + // The data will be sent in chunks if too large for thrift + static HAPI_Result HapiSetHeightFieldData( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + const TArray& InFloatValues, + const FString& InHeightfieldName); + + static bool HapiGetParameterDataAsString( + HAPI_NodeId NodeId, + const std::string& ParmName, + const FString& DefaultValue, + FString& OutValue); + + static bool HapiGetParameterDataAsInteger( + HAPI_NodeId NodeId, + const std::string& ParmName, + int32 DefaultValue, + int32 & OutValue); + + static bool HapiGetParameterDataAsFloat( + HAPI_NodeId NodeId, + const std::string& ParmName, + float DefaultValue, + float& OutValue); + + // Returns a list of all the generic attributes for a given attribute owner + static int32 GetGenericAttributeList( + HAPI_NodeId InGeoNodeId, + HAPI_PartId InPartId, + const FString& InGenericAttributePrefix, + TArray& OutFoundAttributes, + const HAPI_AttributeOwner& AttributeOwner, + int32 InAttribIndex = -1); + + // Helper functions for generic property attributes + static bool GetGenericPropertiesAttributes( + HAPI_NodeId InGeoNodeId, + HAPI_PartId InPartId, + const bool InFindDetailAttributes, // if true, find default attributes + int32 InFirstValidPrimIndex, // If not INDEX_NONE, look for primitive attribute + int32 InFirstValidVertexIndex, // If this is not INDEX_NONE, look for vertex attribute + int32 InFirstValidPointIndex, // If this is not INDEX_NONE, look for point attribute + TArray& OutPropertyAttributes); + + static bool UpdateGenericPropertiesAttributes( + UObject* InObject, + const TArray& InAllPropertyAttributes, + int32 AtIndex = 0, + bool bInDeferPostEditChangePropertyCalls=false, + const FHoudiniGenericAttribute::FFindPropertyFunctionType& InProcessFunction=nullptr); + + // Helper function for setting a generic attribute on geo (UE -> HAPI) + static bool SetGenericPropertyAttribute( + HAPI_NodeId InGeoNodeId, + HAPI_PartId InPartId, + const FHoudiniGenericAttribute& InPropertyAttribute); + + // Helper functions to retrieve the default tag values from the actor CDO. + static TArray GetDefaultActorTags(const AActor* InActor); + // Helper functions to retrieve the default tag values from the component CDO. + static TArray GetDefaultComponentTags(const UActorComponent* InComponent); + + // Helper to add actor tags from the generic attributes and to the OutActorTags array. + // The ApplyTags* helpers are typically used during Bake phases to reapply tags since the structure + // of outputs typically change. + static void ApplyTagsToActorOnly( + const TArray& GenericPropertyAttributes, TArray& OutActorTags); + static void ApplyTagsToActorAndComponents( + AActor* InActor, + bool bKeepActorTags, + const TArray& GenericPropertyAttributes); + + // Helpers to check whether KeepTags is enabled in any of the HGPOs + static bool IsKeepTagsEnabled(const TArray& InHGPOs); + static bool IsKeepTagsEnabled(const FHoudiniGeoPartObject* InHGPO); + + // Helper to clear a component's tags based on the KeepTags settings in a set of HGPOs + static void KeepOrClearComponentTags( + UActorComponent* ActorComponent, + const TArray& InHGPOs); + static void KeepOrClearComponentTags( + UActorComponent* ActorComponent, + const FHoudiniGeoPartObject* InHGPO); + static void KeepOrClearComponentTags( + UActorComponent* ActorComponent, + bool bKeepTags); + + // Helper to reset all the actor component's tags based on the KeepTags settings in a set of HGPOs + static void KeepOrClearActorTags( + AActor* Actor, + bool bApplyToActor, + bool bApplyToComponents, + const FHoudiniGeoPartObject* InHGPO); + + /* + // Tries to update values for all the UProperty attributes to apply on the object. + static void ApplyUPropertyAttributesOnObject( + UObject* MeshComponent, const TArray< UGenericAttribute >& UPropertiesToModify ); + */ + /* + static bool TryToFindInStructProperty( + UObject* Object, FString UPropertyNameToFind, UStructProperty* ArrayProperty, UProperty*& FoundProperty, void*& StructContainer ); + */ + /* + static bool TryToFindInArrayProperty( + UObject* Object, FString UPropertyNameToFind, UArrayProperty* ArrayProperty, UProperty*& FoundProperty, void*& StructContainer ); + */ + + static void AddHoudiniMetaInformationToPackage( + UPackage* Package, + UObject* Object, + const FString& Key, + const FString& Value); + + // Adds the HoudiniLogo mesh to a Houdini Asset Component + static bool AddHoudiniLogoToComponent(USceneComponent* InComponent); + + // Removes the default Houdini logo mesh from a HAC + static bool RemoveHoudiniLogoFromComponent(USceneComponent* InComponent); + + // Indicates if a HAC has the Houdini logo mesh + static bool HasHoudiniLogo(USceneComponent* InComponent); + + // Adds a default quad mesh for texture output + static bool AddTextureMeshToComponent( + USceneComponent* InComponent, + UTexture2D* InTexture, + UMaterialInterface* InMaterial); + + // Removes the default quad mesh for texture output + static bool RemoveTextureMeshFromComponent(USceneComponent* InComponent); + + // Indicates if a HAC has the default texture mesh + static bool HasTextureMesh(USceneComponent* InComponent); + + // + static HAPI_PartInfo ToHAPIPartInfo(const FHoudiniPartInfo& InHPartInfo); + + // + static int32 AddMeshSocketsToArray_Group( + HAPI_NodeId GeoId, + HAPI_PartId PartId, + TArray& AllSockets, + bool isPackedPrim); + + // + static int32 AddMeshSocketsToArray_DetailAttribute( + HAPI_NodeId GeoId, + HAPI_PartId PartId, + TArray& AllSockets, + bool isPackedPrim); + + static bool AddMeshSocketsToStaticMesh( + UStaticMesh* StaticMesh, + TArray& AllSockets, + bool CleanImportSockets); + + // + static bool CreateGroupsFromTags( + HAPI_NodeId NodeId, + HAPI_PartId PartId, + const TArray& Tags); + + // + static bool CreateAttributesFromTags( + HAPI_NodeId NodeId, + HAPI_PartId PartId, + const TArray& Tags); + + // Helper function to access the "unreal_level_path" attribute + static bool GetLevelPathAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + TArray& OutLevelPath, + HAPI_AttributeOwner InAttributeOwner=HAPI_AttributeOwner::HAPI_ATTROWNER_INVALID, + int32 InStart = 0, + int32 InCount = -1); + + // Helper function to access the "unreal_level_path" attribute + static bool GetLevelPathAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& OutLevelPath, + int32 InPointIndex = 0, + int32 InPrimIndex = 0); + + // Helper function to access the custom output name attribute + static bool GetOutputNameAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + TArray& OutOutputName, + int32 InStart = 0, + int32 InCount = -1); + + // Helper function to access the custom output name attribute + static bool GetOutputNameAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& OutOutputName, + int32 InPointIndex = 0, + int32 InPrimIndex = 0); + + // Helper function to access the custom bake name attribute + static bool GetBakeNameAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + TArray& OutBakeName, + const HAPI_AttributeOwner& InAttribOwner = HAPI_ATTROWNER_INVALID, + int32 InStart = 0, + int32 InCount = -1); + + // Helper function to access the custom bake name attribute + static bool GetBakeNameAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& OutBakeName, + int32 InPointIndex = 0, + int32 InPrimIndex = 0); + + // Helper function to access the "tile" attribute + static bool GetTileAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + TArray& OutTileValue, + const HAPI_AttributeOwner& InAttribOwner = HAPI_ATTROWNER_INVALID, + int32 InStart = 0, + int32 InCount = -1); + + // Helper function to access the "tile" attribute + static bool GetTileAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + int32& OutTileValue, + int32 InPointIndex = 0, + int32 InPrimIndex = 0); + + static bool GetEditLayerName( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& EditLayerName, + const HAPI_AttributeOwner& InAttribOwner = HAPI_ATTROWNER_INVALID); + + static bool HasEditLayerName( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + const HAPI_AttributeOwner& InAttribOwner = HAPI_ATTROWNER_INVALID); + + // Helper function to access the "unreal_temp_folder" attribute + static bool GetTempFolderAttribute( + HAPI_NodeId InNodeId, + const HAPI_AttributeOwner& InAttributeOwner, + TArray& OutTempFolder, + HAPI_PartId InPartId=0, + int32 InStart=0, + int32 InCount=-1); + + // Helper function to access the "unreal_temp_folder" attribute + // We check for a primitive attribute first, if the primitive attribute does not exist, we check for a + // detail attribute. + static bool GetTempFolderAttribute( + HAPI_NodeId InNodeId, + FString& OutTempFolder, + HAPI_PartId InPartId=0, + int32 InPrimIndex=0); + + // Helper function to access the "unreal_bake_folder" attribute + static bool GetBakeFolderAttribute( + HAPI_NodeId InGeoId, + const HAPI_AttributeOwner& InAttributeOwner, + TArray& OutBakeFolder, + HAPI_PartId InPartId = 0, + int32 InStart = 0, + int32 InCount = -1); + + // Helper function to access the "unreal_bake_folder" attribute + // We check for a primitive attribute first, if the primitive attribute does not exist, we check for a + // detail attribute. + static bool GetBakeFolderAttribute( + HAPI_NodeId InGeoId, + TArray& OutBakeFolder, + HAPI_PartId InPartId = 0, + int32 InStart = 0, + int32 InCount = -1); + + // Helper function to access the "unreal_bake_folder" attribute + // We check for a primitive attribute first, if the primitive attribute does not exist, we check for a + // detail attribute. + static bool GetBakeFolderAttribute( + const HAPI_NodeId InGeoId, + const HAPI_PartId InPartId, + FString& OutBakeFolder, + const int32 InPrimIndex = 0); + + // Helper function to access the bake output actor attribute (unreal_bake_actor) + static bool GetBakeActorAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + TArray& OutBakeActorNames, + const HAPI_AttributeOwner& InAttributeOwner = HAPI_AttributeOwner::HAPI_ATTROWNER_INVALID, + int32 InStart = 0, + int32 InCount = -1); + + // Helper function to access the bake output actor attribute (unreal_bake_actor) + static bool GetBakeActorAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& OutBakeActorName, + int32 InPointIndex = 0, + int32 InPrimIndex = 0); + + // Helper function to access the bake output actor attribute (unreal_bake_actor_class) + static bool GetBakeActorClassAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + TArray& OutBakeActorClassNames, + const HAPI_AttributeOwner& InAttributeOwner = HAPI_AttributeOwner::HAPI_ATTROWNER_INVALID, + int32 InStart = 0, + int32 InCount = -1); + + // Helper function to access the bake output actor attribute (unreal_bake_actor_class) + static bool GetBakeActorClassAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& OutBakeActorClassName, + int32 InPointIndex = 0, + int32 InPrimIndex = 0); + + // Helper function to access the bake output actor attribute (unreal_bake_outliner_folder) + static bool GetBakeOutlinerFolderAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + TArray& OutBakeOutlinerFolders, + const HAPI_AttributeOwner& InAttributeOwner = HAPI_AttributeOwner::HAPI_ATTROWNER_INVALID, + int32 InStart = 0, + int32 InCount = -1); + + // Helper function to access the bake output actor attribute (unreal_bake_outliner_folder) + static bool GetBakeOutlinerFolderAttribute( + HAPI_NodeId InGeoId, + HAPI_PartId InPartId, + FString& OutBakeOutlinerFolder, + int32 InPointIndex = 0, + int32 InPrimIndex = 0); + + // Adds the "unreal_level_path" primitive attribute + static bool AddLevelPathAttribute( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + ULevel* InLevel, + int32 InCount, + const HAPI_AttributeOwner& InAttributeOwner = HAPI_AttributeOwner::HAPI_ATTROWNER_PRIM); + + // Adds the "unreal_actor_path" primitive attribute + static bool AddActorPathAttribute( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + AActor* InActor, + int32 InCount, + const HAPI_AttributeOwner& InAttributeOwner = HAPI_AttributeOwner::HAPI_ATTROWNER_PRIM); + + // Adds the landscape type primitive attribute based off InActor's type. + // (currently just ALandscapeStreamingProxy) + static bool AddLandscapeTypeAttribute( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + AActor* InActor, + int32 InCount); + + // Helper function used to extract a const char* from a FString + // !! Allocates memory using malloc that will need to be freed afterwards! + static char * ExtractRawString(const FString& Name); + + // Frees memory allocated by ExtractRawString() + static void FreeRawStringMemory(const char*& InRawString); + + // Frees memory allocated by ExtractRawString() + static void FreeRawStringMemory(TArray& InRawStringArray); + + // Make sure a string complies with Houdini's internal variable naming convention (group, attr etc..) + static bool SanitizeHAPIVariableName(FString& String); + + // Ensures a string is a valid variable (mainly for attributes name) + // Reproduces the behaviour of UT_String::forceValidVariableName() + // Return true if a change occured, the string is modified inline + //static bool ForceValidVariableNameInline(FString& InOutString); + + /** How many GUID symbols are used for package component name generation. **/ + static const int32 PackageGUIDComponentNameLength; + + /** How many GUID symbols are used for package item name generation. **/ + static const int32 PackageGUIDItemNameLength; + + // Helper function for creating a temporary Slate notification. + static void CreateSlateNotification( + const FString& NotificationString, + float NotificationExpire = HAPI_UNREAL_NOTIFICATION_EXPIRE, + float NotificationFadeOut = HAPI_UNREAL_NOTIFICATION_FADEOUT); + + static FString GetHoudiniEnginePluginDir(); + + // ------------------------------------------------- + // UWorld and UPackage utilities + // ------------------------------------------------- + + // Find actor in a given world by label or name + template + static T* FindActorInWorldByLabelOrName(UWorld* InWorld, FString ActorLabelOrName, EActorIteratorFlags Flags = EActorIteratorFlags::AllActors) + { + T* OutActor = nullptr; + for (TActorIterator ActorIt(InWorld, T::StaticClass(), Flags); ActorIt; ++ActorIt) + { + OutActor = *ActorIt; + if (!OutActor) + continue; + + // Try the label first, then the name + if (OutActor->GetActorLabel() == ActorLabelOrName) + return OutActor; + if (OutActor->GetFName().ToString() == ActorLabelOrName) + return OutActor; + } + return nullptr; + } + + // Find actor in a given world by label + template + static T* FindActorInWorldByLabel(UWorld* InWorld, FString ActorLabel, EActorIteratorFlags Flags = EActorIteratorFlags::AllActors) + { + T* OutActor = nullptr; + for (TActorIterator ActorIt(InWorld, T::StaticClass(), Flags); ActorIt; ++ActorIt) + { + OutActor = *ActorIt; + if (!OutActor) + continue; + if (OutActor->GetActorLabel() == ActorLabel) + return OutActor; + } + return nullptr; + } + + // Find actor in a given world by name + template + static T* FindActorInWorld(UWorld* InWorld, FName ActorName, EActorIteratorFlags Flags = EActorIteratorFlags::AllActors) + { + T* OutActor = nullptr; + for (TActorIterator ActorIt(InWorld, T::StaticClass(), Flags); ActorIt; ++ActorIt) + { + OutActor = *ActorIt; + if (!OutActor) + continue; + if (OutActor->GetFName().Compare(ActorName)==0) + return OutActor; + } + return nullptr; + } + + // Find an actor by name + static UWorld* FindWorldInPackage(const FString& PackagePath, bool bCreatedMissingPackage, bool& bOutPackageCreated); + + // Determine the appropriate world and level in which to spawn a new actor. + static bool FindWorldAndLevelForSpawning( + UWorld* CurrentWorld, + const FString& PackagePath, + bool bCreateMissingPackage, + UWorld*& OutWorld, + ULevel*& OutLevel, + bool& bOutPackageCreated, + bool& bPackageInWorld); + + template + static T* SpawnActorInLevel(UWorld* InWorld, ULevel* InLevel) + { + FActorSpawnParameters SpawnParams; + SpawnParams.OverrideLevel = InLevel; + return InWorld->SpawnActor(SpawnParams); + } + + // Force the AssetRegistry to recursively rescan a path for + // any new packages that it may not know about, starting at the directory + // in which the given world package is located. This is typically useful + // for WorldComposition to detect new packages immediately after they + // were created. + static void RescanWorldPath(UWorld* InWorld); + + // ------------------------------------------------- + // Actor Utilities + // ------------------------------------------------- + + // Find in actor that belongs to the given outer matching the specified name. + // If the actor doesn't match the type, or is in a PendingKill state, rename it + // so that a new actor can be created with the given name. + // Note that if an actor with the give name was found, it will be returned via `OutFoundActor`. + static AActor* FindOrRenameInvalidActorGeneric(UClass* Class, UWorld* InWorld, const FString& InName, AActor*& OutFoundActor); + + template + static T* FindOrRenameInvalidActor(UWorld* InWorld, const FString& InName, AActor*& OutFoundActor) + { + return Cast( FindOrRenameInvalidActorGeneric(T::StaticClass(), InWorld, InName, OutFoundActor) ); + } + + // Finds actors with the same Name, but without the post fix number. + static TArray FindActorsWithNameNoNumber(UClass* InClass, UWorld* InWorld, const FString & InName); + + // Moves an actor to the specified level + static bool MoveActorToLevel(AActor* InActor, ULevel* InDesiredLevel); + + // ------------------------------------------------- + // Debug Utilities + // ------------------------------------------------- + + // Log debug info for the given package + static void LogPackageInfo(const FString& InLongPackageName); + static void LogPackageInfo(const UPackage* InPackage); + + static void LogWorldInfo(const FString& InLongPackageName); + static void LogWorldInfo(const UWorld* InWorld); + + static FString HapiGetEventTypeAsString(const HAPI_PDG_EventType& InEventType); + static FString HapiGetWorkItemStateAsString(const HAPI_PDG_WorkItemState& InWorkItemState); + + static TArray GetAttributeNames(const HAPI_Session* Session, HAPI_NodeId Node, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + static TMap< HAPI_AttributeOwner, TArray> GetAllAttributeNames(const HAPI_Session * Session, HAPI_NodeId Node, HAPI_PartId PartId); + + // ------------------------------------------------- + // Generic naming / pathing utilities + // ------------------------------------------------- + + static bool RenameObject(UObject* Object, const TCHAR* NewName = nullptr, UObject* NewOuter = nullptr, ERenameFlags Flags = REN_None); + + // Rename the actor to a unique / generated name. + static FName RenameToUniqueActor(AActor* InActor, const FString& InName); + + // Safely rename the actor by ensuring that there aren't any existing objects left + // in the actor's outer with the same name. If an existing object was found, rename it and return it. + static UObject* SafeRenameActor(AActor* InActor, const FString& InName, bool UpdateLabel=true); + + // Validates InPath by converting it to an absolute path for the platform and then calling FPaths::ValidatePath. + static bool ValidatePath(const FString& InPath, FText* OutInvalidPathReason=nullptr); + + static bool DoesFolderExist(const FString& InPath); + + // ------------------------------------------------- + // PackageParam utilities + // ------------------------------------------------- + + // Helper for populating FHoudiniPackageParams. + // If bAutomaticallySetAttemptToLoadMissingPackages is true, then + // OutPackageParams.bAttemptToLoadMissingPackages is set to true in EPackageReplaceMode::CreateNewAssets mode. + static void FillInPackageParamsForBakingOutput( + FHoudiniPackageParams& OutPackageParams, + const FHoudiniOutputObjectIdentifier& InIdentifier, + const FString &BakeFolder, + const FString &ObjectName, + const FString &HoudiniAssetName, + const FString &HoudiniAssetActorName, + EPackageReplaceMode InReplaceMode=EPackageReplaceMode::ReplaceExistingAssets, + bool bAutomaticallySetAttemptToLoadMissingPackages=true, + const TOptional& InComponentGuid=TOptional()); + + // Helper for populating FHoudiniPackageParams when baking. This includes configuring the resolver to + // resolve the object name and unreal_bake_folder and setting these resolved values on the PackageParams. + // If bAutomaticallySetAttemptToLoadMissingPackages is true, then + // OutPackageParams.bAttemptToLoadMissingPackages is set to true in EPackageReplaceMode::CreateNewAssets mode. + // If InHoudiniAssetName or InHoudiniAssetActorName is blank, then the values are determined via + // HoudiniAssetComponent. + static void FillInPackageParamsForBakingOutputWithResolver( + UWorld* const InWorldContext, + const UHoudiniCookable* InCookable, + const FHoudiniOutputObjectIdentifier& InIdentifier, + const FHoudiniOutputObject& InOutputObject, + const bool bInHasPreviousBakeData, + const FString &InDefaultObjectName, + FHoudiniPackageParams& OutPackageParams, + FHoudiniAttributeResolver& OutResolver, + const FString &InDefaultBakeFolder=FString(), + EPackageReplaceMode InReplaceMode=EPackageReplaceMode::ReplaceExistingAssets, + const FString& InHoudiniAssetName=TEXT(""), + const FString& InHoudiniAssetActorName=TEXT(""), + bool bAutomaticallySetAttemptToLoadMissingPackages=true, + bool bInSkipObjectNameResolutionAndUseDefault=false, + bool bInSkipBakeFolderResolutionAndUseDefault=false); + + // Helper for updating FHoudiniPackageParams for temp outputs. This includes configuring the resolver to + // resolve the unreal_temp_folder and setting the resolved values on the PackageParams. + // If bAutomaticallySetAttemptToLoadMissingPackages is true, then + // OutPackageParams.bAttemptToLoadMissingPackages is set to true in EPackageReplaceMode::CreateNewAssets mode. + static void UpdatePackageParamsForTempOutputWithResolver( + const FHoudiniPackageParams& InPackageParams, + const UWorld* InWorldContext, + const UObject* OuterComponent, + const TMap& InCachedAttributes, + const TMap& InCachedTokens, + FHoudiniPackageParams& OutPackageParams, + FHoudiniAttributeResolver& OutResolver, + bool bInSkipTempFolderResolutionAndUseDefault=false); + + // ------------------------------------------------- + template + static TArray> ToObjectPtr(const TArray& In) + { + TArray> Result; + Result.Reserve(In.Num()); + for(T* Ptr : In) + Result.Add(TObjectPtr(Ptr)); + return Result; + } + + template + static TSet> ToObjectPtr(const TSet& In) + { + TSet> Result; + Result.Reserve(In.Num()); + for(T* Ptr : In) + Result.Add(TObjectPtr(Ptr)); + return Result; + } + + template + static TSet RemoveObjectPtr(const TSet>& In) + { + TSet Result; + Result.Reserve(In.Num()); + for(T* Ptr : In) + Result.Add(Ptr); + return Result; + } + + // ------------------------------------------------- + + // ------------------------------------------------- + // Houdini Engine debug functions + // ------------------------------------------------- + + static FString DumpNode(HAPI_NodeId NodeId); + static void DumpPart(HAPI_NodeId NodeId, HAPI_PartId PartId, FStringBuilderBase& Output); + static void DumpNode(const FString & NodePath); + static FString CurveTypeToString(HAPI_CurveType CurveType); + static FString StorageTypeToString(HAPI_StorageType StorageType); + static FString AttributeTypeToString(HAPI_AttributeTypeInfo AttributeType); + static FString PartTypeToString(HAPI_PartType PartType); + static FString NodeTypeToString(HAPI_NodeType NodeType); + static FString DumpAttribute(HAPI_NodeId NodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner, const FString& Name); + static FString RSTOrderToString(HAPI_RSTOrder RstOrder); + static FString HapiTransformToString(HAPI_Transform Transform); + + // ------------------------------------------------- + // Foliage utilities + // ------------------------------------------------- + + // If the foliage editor mode is active, repopulate the list of foliage types in the UI. + // NOTE: this is a currently a bit of a hack: we deactive and reactive the foliage mode (if it was active), + // since the relevant functions are not API exported. + // Returns true if the list was repopulated. + static bool RepopulateFoliageTypeListInUI(); + + // ------------------------------------------------- + // Landscape utilities + // ------------------------------------------------- + + // Iterate over the input objects and gather only the landscape inputs. + static void GatherLandscapeInputs( + const TArray>& Inputs, + TArray& OutAllInputLandscapes); + + static UHoudiniCookable* GetOuterHoudiniCookable(const UObject* Obj); + + static UHoudiniAssetComponent* GetOuterHoudiniAssetComponent(const UObject* Obj); + + static USceneComponent* GetOuterSceneComponent(const UObject* Obj); + + // Helper to create an input node (similar to the HAPI version, but allows for specifying a parent node id + static HAPI_Result CreateInputNode(const FString& InNodeLabel, HAPI_NodeId& OutNodeId, const int32 InParentNodeId=-1); + + // Simplified version of above version, using correct types. + static HAPI_NodeId CreateInputHapiNode(const FString& InNodeLabel, HAPI_NodeId InParentNodeId = INDEX_NONE); + + // Helper to connect two nodes together + // Connects InNodeIdToConnect's OutputIndex to InNodeId's InputIndex + // (similar to the HAPI function, but allows for specifying a XformType for the created object merge when the two nodes aren't in the same subnet) + static bool HapiConnectNodeInput(int32 InNodeId, int32 InputIndex, int32 InNodeIdToConnect, int32 OutputIndex, int32 InXFormType); + + + // ------------------------------------------------- + // JSON Utilities + // ------------------------------------------------- + static FString JSONToString(const TSharedPtr& JSONObject); + static bool JSONFromString(const ::FString& JSONString, TSharedPtr& OutJSONObject); + + // ------------------------------------------------- + // Mesh Attribute Utilities + // ------------------------------------------------- + + // Retrieve Houdini UV sets from the given Mesh part GeoId/PartId. + static bool UpdateMeshPartUVSets( + const int GeoId, + const int PartId, + bool bRemoveUnused, + TArray>& OutPartUVSets, + TArray& OutAttribInfoUVSets); + + template + static TArray RunLengthEncode(const DataType* Data, int TupleSize, int Count, float MaxCompressionRatio = 0.25f, int MaxPackets = 500) + { + // Run length encode the data. + // If this function returns an empty array it means the desired compression ratio could not be met. + + auto CompareTuple = [TupleSize](const DataType* StartA, const DataType* StartB) + { + for (int Index = 0; Index < TupleSize; Index++) + { + if (StartA[Index] != StartB[Index]) + return false; + } + return true; + }; + + TArray EncodedData; + if (Count == 0) + return EncodedData; + + // Guess of size needed. + EncodedData.Reserve(MaxPackets); + + // The first run always begins on element zero. + int Start = 0; + EncodedData.Add(Start); + + // None a run length encoded array based off the input data. eg. + // [ 0, 0, 0, 1, 1, 2, 3 ] will return [ 0, 3, 5, 6] + + for (int Index = 0; Index < Count * TupleSize; Index += TupleSize) + { + if (!CompareTuple(&Data[Start], &Data[Index])) + { + // The value changed, so start a new run + if (EncodedData.Num() == MaxPackets) + return {}; + Start = Index; + EncodedData.Add(Start / TupleSize); + } + } + + // Check we've made a decent compression ratio. If not return an empty array. + float Ratio = float(EncodedData.Num() / float(Count)); + if (Ratio > MaxCompressionRatio) + EncodedData.SetNum(0); + + return EncodedData; + } + protected: + + // Computes the XX.YY.ZZZ version string using HAPI_Version + static FString ComputeVersionString(bool ExtraDigit); + +#if PLATFORM_WINDOWS + // Attempt to locate libHAPI on Windows in the registry. Return handle if located and return location. + static void* LocateLibHAPIInRegistry( + const FString& HoudiniInstallationType, FString& StoredLibHAPILocation, bool LookIn32bitRegistry); +#endif + + // Triggers an update the details panel + static void UpdateEditorProperties_Internal(const bool bInForceFullUpdate); + + // Trigger an update of the Blueprint Editor on the game thread + static void UpdateBlueprintEditor_Internal(UHoudiniAssetComponent* HAC); + + private: + + /** + * Gets FHoudiniParameterWidgetMetaData from focused widget if it exists and has DetailsView + * as a parent. + * + * @see UpdateEditorProperties_Internal + * @see FocusUsingParameterWidgetMetaData + */ + static TSharedPtr GetFocusedParameterWidgetMetaData( + TSharedPtr DetailsView); + + /** + * Sets user focus on a descendant widget that has matching parameter widget metadata. + * + * This is used for a hack needed to maintain user focus on parameter widgets after the + * Details panel is forcibly refreshed. + * + * @param AncestorWidget All descendant widgets will be searched for matching metadata. + * @param ParameterWidgetMetaData Should be unique to the widget which we want to select. + * @return true if a widget was successfully selected, false otherwise. + * + * @see UpdateEditorProperties_Internal + */ + static bool FocusUsingParameterWidgetMetaData( + TSharedRef AncestorWidget, + const FHoudiniParameterWidgetMetaData& ParameterWidgetMetaData); + +public: + // Refine all proxy meshes on UHoudiniAssetCompoments of InActorsToRefine. + static EHoudiniProxyRefineRequestResult RefineHoudiniProxyMeshActorArrayToStaticMeshes(const TArray& InActorsToRefine, bool bSilent = false); + + // Triage a cookable with UHoudiniStaticMesh as needing cooking or if a UStaticMesh can be immediately built + static void TriageHoudiniCookablesForProxyMeshRefinement( + UHoudiniCookable* InHC, + bool bRefineAll, + bool bOnPreSaveWorld, + UWorld* OnPreSaveWorld, + bool bOnPreBeginPIE, + TArray& OutToRefine, + TArray& OutToCook, + TArray& OutSkipped); + + static EHoudiniProxyRefineRequestResult RefineTriagedHoudiniProxyMeshesToStaticMeshes( + const TArray& InCookablesToRefine, + const TArray& InCookablesToCook, + const TArray& InSkippedCookables, + bool bInSilent = false, + bool bInRefineAll = true, + bool bInOnPreSaveWorld = false, + UWorld* InOnPreSaveWorld = nullptr, + bool bInOnPrePIEBeginPlay = false); + + static void SetAllowPlayInEditorRefinement( + const TArray& InCookables, bool bEnabled); + + // Called in a background thread by RefineHoudiniProxyMeshesToStaticMeshes when some Cookables need to be cooked to generate UStaticMeshes. Checks and waits for + // cooking of each component to complete, and then calls RefineHoudiniProxyMeshesToStaticMeshesNotifyDone on the main thread. + static void RefineHoudiniProxyMeshesToStaticMeshesWithCookInBackgroundThread( + const TArray& InCookablesToCook, + TSharedPtr InTaskProgress, + const uint32 InNumSkippedCookables, + bool bInOnPreSaveWorld, + UWorld* InOnPreSaveWorld, + const TArray& InSuccessfulCookables, + const TArray& InFailedCookables, + const TArray& InSkippedCookables); + + // Display a notification / end/close progress dialog, when refining mesh proxies to static meshes is complete + static void RefineHoudiniProxyMeshesToStaticMeshesNotifyDone( + const uint32 InNumTotalCookables, + FSlowTask* const InTaskProgress, + const bool bCancelled, + const bool bOnPreSaveWorld, + UWorld* const InOnPreSaveWorld, + const TArray& InSuccessfulCookables, + const TArray& InFailedCookables, + const TArray& InSkippedCookables); + + static FDelegateHandle& GetOnPostSaveWorldRefineProxyMeshesHandle() { return OnPostSaveWorldRefineProxyMeshesHandle; } + + + // Delegate that is set up to refined proxy meshes post save world (it removes itself afterwards) + static FDelegateHandle OnPostSaveWorldRefineProxyMeshesHandle; + + // Delegate for broadcasting when proxy mesh refinement of a HAC's output is complete. + static FOnHoudiniProxyMeshesRefinedDelegate OnHoudiniProxyMeshesRefinedDelegate; + + static FOnHoudiniProxyMeshesRefinedDelegate& GetOnHoudiniProxyMeshesRefinedDelegate() { return OnHoudiniProxyMeshesRefinedDelegate; } + + // Handle OnPostSaveWorld for refining proxy meshes: this saves all the dirty UPackages of the UStaticMeshes + // that were created during RefineHoudiniProxyMeshesToStaticMeshes if it was called as a result of a PreSaveWorld. + static void RefineProxyMeshesHandleOnPostSaveWorld( + const TArray& InSuccessfulCookables, + uint32 InSaveFlags, + UWorld* InWorld, + bool bInSuccess); +}; + + +``` + +`Source/HoudiniEngine/Private/HoudiniFoliageTools.cpp`: + +```cpp +/* +* Copyright (c) <2022> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniFoliageTools.h" +#include "HoudiniEngineUtils.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniAssetActor.h" +#include "HoudiniInput.h" +#include "HoudiniAssetComponent.h" +#include "Editor/UnrealEdEngine.h" +#include "Async/Async.h" +#include "Components/BrushComponent.h" +#include "Components/ModelComponent.h" +#include "LandscapeHeightfieldCollisionComponent.h" +#include "FoliageType_InstancedStaticMesh.h" +#include "HoudiniEngineAttributes.h" +#include "InstancedFoliageActor.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Spatial/PointHashGrid3.h" +#include "Curves/RichCurve.h" + +#if WITH_EDITOR +#include "EditorModeManager.h" +#include "EditorModes.h" +#endif + +FString FHoudiniFoliageTools::MakeObjectName(const FString& HDAName, const FHoudiniOutputObjectIdentifier& Id) +{ + FString Result = FString::Printf(TEXT("%s_%d_%d_%s_%s"), *HDAName, Id.GeoId, Id.PartId, *Id.SplitIdentifier, TEXT("foliage_type")); + return Result; +} + +UFoliageType* +FHoudiniFoliageTools::CreateFoliageType(const FHoudiniPackageParams& Params, const FHoudiniOutputObjectIdentifier& Id, UStaticMesh* InstancedStaticMesh) +{ + UFoliageType* FoliageType = nullptr; + + // With world partition, Foliage Types must be assets. Create a package and save it. + FHoudiniPackageParams FoliageParams = Params; + FoliageParams.ObjectName = MakeObjectName(FoliageParams.HoudiniAssetName, Id); + + if (UFoliageType_InstancedStaticMesh* InstancedMeshFoliageType = FoliageParams.CreateObjectAndPackage()) + { + InstancedMeshFoliageType->SetStaticMesh(InstancedStaticMesh); + InstancedMeshFoliageType->MarkPackageDirty(); + FAssetRegistryModule::AssetCreated(InstancedMeshFoliageType); + FoliageType = InstancedMeshFoliageType; + } + return FoliageType; + +} + +// Duplicate foliage asset. +UFoliageType* FHoudiniFoliageTools::DuplicateFoliageType(const FHoudiniPackageParams& Params, const FHoudiniOutputObjectIdentifier& Id, UFoliageType* OrigFoliageType) +{ + UFoliageType* FoliageType = nullptr; + + FHoudiniPackageParams FoliageParams = Params; + FoliageParams.ObjectName = MakeObjectName(FoliageParams.HoudiniAssetName, Id); + + UFoliageType_InstancedStaticMesh * FTISM = Cast(OrigFoliageType); + + if (UFoliageType_InstancedStaticMesh* InstancedMeshFoliageType = FoliageParams.CreateObjectAndPackage(FTISM)) + { + InstancedMeshFoliageType->MarkPackageDirty(); + FAssetRegistryModule::AssetCreated(InstancedMeshFoliageType); + FoliageType = InstancedMeshFoliageType; + } + return FoliageType; +} + + +UFoliageType* +FHoudiniFoliageTools::GetFoliageType(const ULevel* DesiredLevel, const UStaticMesh* InstancedStaticMesh) +{ + TArray FoliageTypes = FHoudiniFoliageTools::GetFoliageTypes(DesiredLevel, InstancedStaticMesh); + if (FoliageTypes.Num() > 1) + { + HOUDINI_LOG_WARNING(TEXT("More than one Folage Type found for instanced static mesh: %s"), *InstancedStaticMesh->GetName()); + } + + return (FoliageTypes.Num() > 0) ? FoliageTypes[0] : nullptr; +} + +TArray FHoudiniFoliageTools::GetFoliageTypes(AInstancedFoliageActor* IFA) +{ + TArray Results; + + const TMap>& FoliageInfos = IFA->GetFoliageInfos(); + + for (const auto& FoliageIt : FoliageInfos) + { + UFoliageType* FoliageType = FoliageIt.Key; + Results.Add(FoliageType); + } + return Results; +} + +TArray +FHoudiniFoliageTools::GetFoliageTypes(const ULevel* DesiredLevel, const UStaticMesh* InstancedStaticMesh) +{ + TArray Results; + + // Iterate through all AInstancedFoliageActor actors in the world and combine their foliage types + // into a single array. + + for (TActorIterator It(DesiredLevel->GetWorld(), AInstancedFoliageActor::StaticClass()); It; ++It) + { + AInstancedFoliageActor* IFA = Cast(*It); + + TArray FoliageTypes; + IFA->GetAllFoliageTypesForSource(InstancedStaticMesh, FoliageTypes); + + // GetAllFoliageTypesForSource() returns an array of const UFoliageTypes while the rest of the Unreal + // API is pretty const ignorant for foliage types. So remove consting. + + for(auto FoliageType : FoliageTypes) + Results.Add(const_cast(FoliageType)); + } + return Results; +} + +TArray FHoudiniFoliageTools::GetAllFoliageInfo(UWorld * World, UFoliageType* FoliageType) +{ + TArray Results; + if (!IsValid(World)) + return Results; + + for (TActorIterator It(World, AInstancedFoliageActor::StaticClass()); It; ++It) + { + AInstancedFoliageActor* IFA = Cast(*It); + + const TMap>& FoliageInfos = IFA->GetFoliageInfos(); + + for (const auto& FoliageIt : FoliageInfos) + { + if (FoliageIt.Key == FoliageType) + { + const FFoliageInfo& FoliageInfo = *FoliageIt.Value; + Results.Add(const_cast(&FoliageInfo)); + } + + } + } + return Results; +} + +TArray FHoudiniFoliageTools::GetAllFoliageInstances(UWorld* InWorld, UFoliageType* FoliageType) +{ + TArray Results; + + for (TActorIterator It(InWorld, AInstancedFoliageActor::StaticClass()); It; ++It) + { + AInstancedFoliageActor* IFA = Cast(*It); + + auto InstanceMap = IFA->GetAllInstancesFoliageType(); + for(auto & InstanceIt : InstanceMap) + { + FFoliageInfo * FoliageInfo = InstanceIt.Value; + if (InstanceIt.Key == FoliageType) + Results.Append(FoliageInfo->Instances); + + } + } + return Results; +} + +TArray FHoudiniFoliageTools::SpawnFoliageInstances(UWorld* InWorld, UFoliageType* Settings, const TArray& InstancesToPlace, const TArray& AttachmentInfo) +{ + // This code is largely cribbed from SpawnFoliageInstance() in UE5's FoliageEdMode.cpp. It has UI specific functionality removed. + + TMap> PerIFAInstances; + const bool bSpawnInCurrentLevel = true; + ULevel* CurrentLevel = InWorld->GetCurrentLevel(); + const bool bCreate = true; + TSet FoliageActors; + for (int Index = 0; Index < InstancesToPlace.Num(); Index++) + { + const FFoliageInstance& PlacedInstance = InstancesToPlace[Index]; + ULevel* LevelHint = bSpawnInCurrentLevel ? CurrentLevel : PlacedInstance.BaseComponent ? PlacedInstance.BaseComponent->GetComponentLevel() : nullptr; + if (AInstancedFoliageActor* IFA = AInstancedFoliageActor::Get(InWorld, bCreate, LevelHint, PlacedInstance.Location)) + { + PerIFAInstances.FindOrAdd(IFA).Add(Index); + FoliageActors.Add(IFA); + } + } + + for (const auto& PlacedLevelInstances : PerIFAInstances) + { + AInstancedFoliageActor* IFA = PlacedLevelInstances.Key; + + FFoliageInfo* Info = nullptr; + UFoliageType* FoliageSettings = IFA->AddFoliageType(Settings, &Info); + + + for(int InstanceIndex = 0; InstanceIndex < PlacedLevelInstances.Value.Num(); InstanceIndex++) + { + FFoliageInstance Instance = InstancesToPlace[PlacedLevelInstances.Value[InstanceIndex]]; + if (AttachmentInfo.Num() > 0) + { + SetInstanceAttachment(IFA, Info, FoliageSettings, Instance, AttachmentInfo[InstanceIndex]); + } + Info->AddInstance(FoliageSettings, Instance); + } + Info->Refresh(false, true); + + } + + TArray FoliageInfos = FHoudiniFoliageTools::GetAllFoliageInfo(InWorld, Settings); + for(auto & FoliageInfo : FoliageInfos) + { + if (FoliageInfo != nullptr) + { + FoliageInfo->Refresh(true, false); + } + } + + return FoliageActors.Array(); +} + +void +FHoudiniFoliageTools::RemoveInstancesFromWorld(UWorld* World, UFoliageType* FoliageType) +{ + TArray FoliageInfos = FHoudiniFoliageTools::GetAllFoliageInfo(World, FoliageType); + for (auto& FoliageInfo : FoliageInfos) + { + if (FoliageInfo != nullptr) + { + TArray InstancesToRemove; + InstancesToRemove.SetNum(FoliageInfo->Instances.Num()); + for(int Index = 0; Index < FoliageInfo->Instances.Num(); Index++) + { + InstancesToRemove[Index] = Index; + } + FoliageInfo->RemoveInstances(InstancesToRemove, true); + } + } +} + +void +FHoudiniFoliageTools::RemoveFoliageInstances(UWorld* World, UFoliageType* FoliageType, const TArray& Positions) +{ + const float Spacing = 1.0f; + // Create a spatial hash for fast look up of positions. + UE::Geometry::TPointHashGrid3d< int> SpatialHash(Spacing, -1); + for (int Index = 0; Index < Positions.Num(); ++Index) + SpatialHash.InsertPoint(Index, Positions[Index]); + + + TArray FoliageInfos = FHoudiniFoliageTools::GetAllFoliageInfo(World, FoliageType); + for (auto& FoliageInfo : FoliageInfos) + { + if (FoliageInfo == nullptr) continue; + + // Query all instances in the Foliage Type, see if they are close to one in the set we are trying to remove + + TArray InstancesToRemove; + for (int Index = 0; Index < FoliageInfo->Instances.Num(); Index++) + { + TPair Result = SpatialHash.FindNearestInRadius(FoliageInfo->Instances[Index].Location, Spacing, [&](int PosIndex) + { + return FVector3d::DistSquared(FoliageInfo->Instances[Index].Location, Positions[PosIndex]); + }); + + if (Result.Key != -1) + { + // Remove this instance of foliage. + InstancesToRemove.Add(Index); + + // Remove the matched point from the spatial hash. This means we will only remove one instance + // for each point. + SpatialHash.RemovePoint(Result.Value, Positions[Result.Value]); + } + + } + FoliageInfo->RemoveInstances(InstancesToRemove, true); + } +} + +void +FHoudiniFoliageTools::SetInstanceAttachment(AInstancedFoliageActor * IFA, FFoliageInfo * FoliageInfo, UFoliageType * FoliageType, FFoliageInstance& FoliageInstance, const FFoliageAttachmentInfo& AttachmentInfo) +{ + UWorld * World = IFA->GetWorld(); + + if (AttachmentInfo.Type == EFoliageAttachmentType::None) + { + // Use whatever was in the instance - this is used when baking. + return; + } + + TFunction FilterFunc = [AttachmentInfo] (const UPrimitiveComponent* Component) + { + switch(AttachmentInfo.Type) + { + case EFoliageAttachmentType::All: + return true; + + case EFoliageAttachmentType::LandscapeOnly: + return Component->IsA(); + + default: + return false; + } + }; + + FVector TraceStart = FoliageInstance.Location + FVector(0.f, 0.f, AttachmentInfo.Distance); + FVector TraceEnd = FoliageInstance.Location - FVector(0.f, 0.f, AttachmentInfo.Distance); + FHitResult Hit; + static FName TraceName = FName("HoudiniFoliageTrace"); + if (AInstancedFoliageActor::FoliageTrace(World, Hit, FDesiredFoliageInstance(TraceStart, TraceEnd, FoliageType), TraceName, false, FilterFunc, false)) + { + UPrimitiveComponent* BaseComponent = Hit.Component.Get(); + if (!IsValid(BaseComponent) || BaseComponent->GetComponentLevel() != IFA->GetLevel()) + { + return; + } + + if (UModelComponent* ModelComponent = Cast(BaseComponent)) + { + if (ABrush* BrushActor = ModelComponent->GetModel()->FindBrush((FVector3f)Hit.Location)) + { + BaseComponent = BrushActor->GetBrushComponent(); + } + } + + FoliageInstance.BaseId = IFA->InstanceBaseCache.AddInstanceBaseId(FoliageInfo->ShouldAttachToBaseComponent() ? BaseComponent : nullptr); + if (FoliageInstance.BaseId == FFoliageInstanceBaseCache::InvalidBaseId) + { + FoliageInstance.BaseComponent = nullptr; + } + else + { + FoliageInstance.BaseComponent = BaseComponent; + } + + FoliageInstance.Location = Hit.Location; + FoliageInstance.ZOffset = 0.f; + +#if 0 + // For now, we do not support this. Possibly this could be controlled by an attribute or setting in the future + if (FoliageInstance.Flags & FOLIAGE_AlignToNormal) + { + FoliageInstance.Rotation = FoliageInstance.PreAlignRotation; + FoliageInstance.AlignToNormal(Hit.Normal, FoliageType->AlignMaxAngle); + } +#endif + } +} + +TArray FHoudiniFoliageTools::GetAttachmentInfo(int GeoId, int PartId, int Count) +{ + TArray Result; + Result.SetNum(Count); + + TArray IntData; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_FOLIAGE_ATTACHMENT_TYPE); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, IntData, 0, Count); + + if (!bSuccess || IntData.Num() != Count) + return Result; + + for(int Index = 0; Index < IntData.Num(); Index++) + { + switch (IntData[Index]) + { + case 0: Result[Index].Type = EFoliageAttachmentType::None; break; + case 1: Result[Index].Type = EFoliageAttachmentType::All; break; + case 2: Result[Index].Type = EFoliageAttachmentType::LandscapeOnly; break; + default: break; + } + } + + // Get (optional) attachment distance + TArray FloatData; + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_FOLIAGE_ATTACHMENT_DISTANCE); + + if (!Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, FloatData, 0, Count)) + { + return Result; + } + + for (int Index = 0; Index < FloatData.Num(); Index++) + { + Result[Index].Distance = FloatData[Index]; + } + + return Result; +} + +bool FHoudiniFoliageTools::AreFoliageTypesEqual(UFoliageType const* InLhs, UFoliageType const* InRhs) +{ + if (!IsValid(InLhs) || !IsValid(InRhs)) + return false; + + // Check that the foliage types are of the same class + if (InLhs->GetClass() != InRhs->GetClass()) + return false; + + if (InLhs->Density != InRhs->Density || + !FMath::IsNearlyEqual(InLhs->DensityAdjustmentFactor, InRhs->DensityAdjustmentFactor) || + !FMath::IsNearlyEqual(InLhs->Radius, InRhs->Radius) || + InLhs->bSingleInstanceModeOverrideRadius != InRhs->bSingleInstanceModeOverrideRadius || + !FMath::IsNearlyEqual(InLhs->SingleInstanceModeRadius, InRhs->SingleInstanceModeRadius) || + InLhs->Scaling != InRhs->Scaling || + !IsIntervalNearlyEqual(InLhs->ScaleX, InRhs->ScaleX) || + !IsIntervalNearlyEqual(InLhs->ScaleY, InRhs->ScaleY) || + !IsIntervalNearlyEqual(InLhs->ScaleZ, InRhs->ScaleZ) || + !IsIntervalNearlyEqual(InLhs->ZOffset, InRhs->ZOffset) || + InLhs->AlignToNormal != InRhs->AlignToNormal || + InLhs->AverageNormal != InRhs->AverageNormal || + InLhs->AverageNormalSingleComponent != InRhs->AverageNormalSingleComponent || + !FMath::IsNearlyEqual(InLhs->AlignMaxAngle, InRhs->AlignMaxAngle) || + InLhs->RandomYaw != InRhs->RandomYaw || + !FMath::IsNearlyEqual(InLhs->RandomPitchAngle, InRhs->RandomPitchAngle) || + !IsIntervalNearlyEqual(InLhs->GroundSlopeAngle, InRhs->GroundSlopeAngle) || + !IsIntervalNearlyEqual(InLhs->Height, InRhs->Height) || + InLhs->LandscapeLayers != InRhs->LandscapeLayers || + !FMath::IsNearlyEqual(InLhs->MinimumLayerWeight, InRhs->MinimumLayerWeight) || + InLhs->ExclusionLandscapeLayers != InRhs->ExclusionLandscapeLayers || + !FMath::IsNearlyEqual(InLhs->MinimumExclusionLayerWeight, InRhs->MinimumExclusionLayerWeight) || + InLhs->CollisionWithWorld != InRhs->CollisionWithWorld || + !InLhs->CollisionScale.Equals(InRhs->CollisionScale) || + InLhs->AverageNormalSampleCount != InRhs->AverageNormalSampleCount || + InLhs->MeshBounds != InRhs->MeshBounds || + !InLhs->LowBoundOriginRadius.Equals(InRhs->LowBoundOriginRadius) || + InLhs->Mobility != InRhs->Mobility || + !IsIntervalEqual(InLhs->CullDistance, InRhs->CullDistance) || + InLhs->CastShadow != InRhs->CastShadow || + InLhs->bAffectDynamicIndirectLighting != InRhs->bAffectDynamicIndirectLighting || + InLhs->bAffectDistanceFieldLighting != InRhs->bAffectDistanceFieldLighting || + InLhs->bCastDynamicShadow != InRhs->bCastDynamicShadow || + InLhs->bCastStaticShadow != InRhs->bCastStaticShadow || + InLhs->bCastContactShadow != InRhs->bCastContactShadow || + InLhs->bCastShadowAsTwoSided != InRhs->bCastShadowAsTwoSided || + InLhs->bReceivesDecals != InRhs->bReceivesDecals || + InLhs->bOverrideLightMapRes != InRhs->bOverrideLightMapRes || +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 2) + InLhs->ShadowCacheInvalidationBehavior != InRhs->ShadowCacheInvalidationBehavior || +#endif + InLhs->OverriddenLightMapRes != InRhs->OverriddenLightMapRes || + InLhs->LightmapType != InRhs->LightmapType || + InLhs->bUseAsOccluder != InRhs->bUseAsOccluder || + InLhs->bVisibleInRayTracing != InRhs->bVisibleInRayTracing || + InLhs->bEvaluateWorldPositionOffset != InRhs->bEvaluateWorldPositionOffset || +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0) + InLhs->WorldPositionOffsetDisableDistance != InRhs->WorldPositionOffsetDisableDistance || +#endif + InLhs->CustomNavigableGeometry != InRhs->CustomNavigableGeometry || + InLhs->LightingChannels.bChannel0 != InRhs->LightingChannels.bChannel0 || + InLhs->LightingChannels.bChannel1 != InRhs->LightingChannels.bChannel1 || + InLhs->LightingChannels.bChannel2 != InRhs->LightingChannels.bChannel2 || + InLhs->bRenderCustomDepth != InRhs->bRenderCustomDepth || + InLhs->CustomDepthStencilWriteMask != InRhs->CustomDepthStencilWriteMask || + InLhs->CustomDepthStencilValue != InRhs->CustomDepthStencilValue || + InLhs->TranslucencySortPriority != InRhs->TranslucencySortPriority || + !FMath::IsNearlyEqual(InLhs->CollisionRadius, InRhs->CollisionRadius) || + !FMath::IsNearlyEqual(InLhs->ShadeRadius, InRhs->ShadeRadius) || + InLhs->NumSteps != InRhs->NumSteps || + !FMath::IsNearlyEqual(InLhs->InitialSeedDensity, InRhs->InitialSeedDensity) || + !FMath::IsNearlyEqual(InLhs->AverageSpreadDistance, InRhs->AverageSpreadDistance) || + !FMath::IsNearlyEqual(InLhs->SpreadVariance, InRhs->SpreadVariance) || + InLhs->SeedsPerStep != InRhs->SeedsPerStep || + InLhs->DistributionSeed != InRhs->DistributionSeed || + !FMath::IsNearlyEqual(InLhs->MaxInitialSeedOffset, InRhs->MaxInitialSeedOffset) || + InLhs->bCanGrowInShade != InRhs->bCanGrowInShade || + InLhs->bSpawnsInShade != InRhs->bSpawnsInShade || + !FMath::IsNearlyEqual(InLhs->MaxInitialAge, InRhs->MaxInitialAge) || + !FMath::IsNearlyEqual(InLhs->MaxAge, InRhs->MaxAge) || + !FMath::IsNearlyEqual(InLhs->OverlapPriority, InRhs->OverlapPriority) || + !IsIntervalNearlyEqual(InLhs->ProceduralScale, InRhs->ProceduralScale)) + { + return false; + } + + constexpr uint8 NumVertexColorMaskChannels = static_cast(EVertexColorMaskChannel::MAX_None); + for (uint8 Idx = 0; Idx < NumVertexColorMaskChannels; ++Idx) + { + const FFoliageVertexColorChannelMask& Lhs = InLhs->VertexColorMaskByChannel[Idx]; + const FFoliageVertexColorChannelMask& Rhs = InRhs->VertexColorMaskByChannel[Idx]; + if (Lhs.UseMask != Rhs.UseMask || Lhs.InvertMask != Rhs.InvertMask || Lhs.MaskThreshold != Rhs.MaskThreshold) + return false; + } + + FRichCurve const* const LhsScaleCurve = InLhs->ScaleCurve.GetRichCurveConst(); + FRichCurve const* const RhsScaleCurve = InRhs->ScaleCurve.GetRichCurveConst(); + if ((LhsScaleCurve == nullptr && RhsScaleCurve != nullptr) || (LhsScaleCurve != nullptr && RhsScaleCurve == nullptr)) + return false; + + if (!LhsScaleCurve || !RhsScaleCurve) + return true; + + if (!(*LhsScaleCurve == *RhsScaleCurve)) + return false; + + if (InLhs->DensityFalloff.bUseFalloffCurve != InRhs->DensityFalloff.bUseFalloffCurve) + return false; + if (InLhs->DensityFalloff.bUseFalloffCurve) + { + FRichCurve const* const LhsFalloffCurve = InLhs->DensityFalloff.FalloffCurve.GetRichCurveConst(); + FRichCurve const* const RhsFalloffCurve = InRhs->DensityFalloff.FalloffCurve.GetRichCurveConst(); + if ((LhsFalloffCurve == nullptr && RhsFalloffCurve != nullptr) || (LhsFalloffCurve != nullptr && RhsFalloffCurve == nullptr)) + return false; + + if (!LhsFalloffCurve || !RhsFalloffCurve) + return true; + + if (!(*LhsFalloffCurve == *RhsFalloffCurve)) + return false; + } + + // Check the SM and override materials if this is a UFoliateType_InstancedStaticMesh + UFoliageType_InstancedStaticMesh const* const LhsSM = Cast(InLhs); + UFoliageType_InstancedStaticMesh const* const RhsSM = Cast(InRhs); + if (!IsValid(LhsSM) || !IsValid(RhsSM)) + return true; + + if (LhsSM->Mesh != RhsSM->Mesh || + LhsSM->OverrideMaterials != RhsSM->OverrideMaterials || +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0) + LhsSM->NaniteOverrideMaterials != RhsSM->NaniteOverrideMaterials || +#endif + LhsSM->ComponentClass != RhsSM->ComponentClass) + { + return false; + } + + return true; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniFoliageTools.h`: + +```h +/* +* Copyright (c) <2022> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#include "InstancedFoliage.h" + +struct FHoudiniEngineOutputStats; +class UHoudiniOutput; +struct FHoudiniOutputObjectIdentifier; +struct FHoudiniOutputObject; +class AInstancedFoliageActor; +class ULevel; +struct FHoudiniPackageParams; +class UStaticMesh; +class USceneComponent; +class UFoliageType; + +enum class EFoliageAttachmentType : uint8 +{ + None = 0, + All = 1, + LandscapeOnly = 2 +}; + +struct FFoliageAttachmentInfo +{ + EFoliageAttachmentType Type = EFoliageAttachmentType::None; + float Distance = 1000.0f; +}; + +class HOUDINIENGINE_API FHoudiniFoliageTools +{ +public: + // Create a new Foliage type using the InstancedStaticMesh as an Asset. + static UFoliageType* CreateFoliageType(const FHoudiniPackageParams& Params, const FHoudiniOutputObjectIdentifier& Id, UStaticMesh* InstancedStaticMesh); + + // Duplicate foliage asset. + static UFoliageType* DuplicateFoliageType(const FHoudiniPackageParams& Params, const FHoudiniOutputObjectIdentifier& Id, UFoliageType* FoliageType); + + // Get the Foliage Type which uses the Instanced Static Mesh. If more than one is found, a warning is printed. + static UFoliageType* GetFoliageType(const ULevel* DesiredLevel, const UStaticMesh* InstancedStaticMesh); + + // Get all Foliage Types which use the Instanced Static Mesh. + static TArray GetFoliageTypes(const ULevel* DesiredLevel, const UStaticMesh* InstancedStaticMesh); + + // Spawn the Foliage Instances into the given World/Foliage Type. + static TArray SpawnFoliageInstances(UWorld* InWorld, UFoliageType* Settings, const TArray& InstancesToPlace, const TArray & AttachementInfos); + + // Returns Foliage Instances used in the given World by the Foliage Type. + static TArray GetAllFoliageInstances(UWorld* InWorld, UFoliageType* Settings); + + // Return all Foliage Types used by the AInstancedFoliageActor. + static TArray GetFoliageTypes(AInstancedFoliageActor* IFA); + + // Return all FFoliageInfo which reference the FoliageType in the given world. + static TArray GetAllFoliageInfo(UWorld * World, UFoliageType * FoliageType); + + // Remove all instances using this Foliage Type from the world + static void RemoveInstancesFromWorld(UWorld* World, UFoliageType* FoliageType); + + // Remove foliage + static void RemoveFoliageInstances(UWorld* World, UFoliageType* FoliageType, const TArray& Positions); + + // Sets the instance attachment based off parameters. + static void SetInstanceAttachment(AInstancedFoliageActor* IFA, FFoliageInfo* FoliageInfo, UFoliageType* FoliageType, FFoliageInstance& FoliageInstance, const FFoliageAttachmentInfo& AttachmentInfo); + + // Get all attachment info for a part. + static TArray GetAttachmentInfo(int GeoId, int PartId, int Count); + + // Return true if the two foliage types are of the same class and all property values are the same. + static bool AreFoliageTypesEqual(UFoliageType const* InLhs, UFoliageType const* InRhs); + + template + static bool IsIntervalEqual(const TInterval& InLhs, const TInterval& InRhs); + + template + static bool IsIntervalNearlyEqual(const TInterval& InLhs, const TInterval& InRhs); + + static FString MakeObjectName(const FString& HDAName, const FHoudiniOutputObjectIdentifier& Id); + +}; + +template +bool FHoudiniFoliageTools::IsIntervalEqual(const TInterval& InLhs, const TInterval& InRhs) +{ + return InLhs.Min == InRhs.Min && InLhs.Max == InRhs.Max; +} + + +template +bool FHoudiniFoliageTools::IsIntervalNearlyEqual(const TInterval& InLhs, const TInterval& InRhs) +{ + return FMath::IsNearlyEqual(InLhs.Min, InRhs.Min) + && FMath::IsNearlyEqual(InLhs.Max, InRhs.Max); +} + +``` + +`Source/HoudiniEngine/Private/HoudiniGeoImportCommandlet.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniGeoImportCommandlet.h" + +#include "DirectoryWatcherModule.h" +#include "Modules/ModuleManager.h" +#include "Misc/Guid.h" +#include "EditorFramework/AssetImportData.h" + +#include "Editor.h" +#include "FileHelpers.h" + +#include "MessageEndpointBuilder.h" + +#include "PackageTools.h" + +#include "IDirectoryWatcher.h" + +#include "Internationalization/Regex.h" + +#include "Interfaces/ISlateNullRendererModule.h" +#include "Rendering/SlateRenderer.h" +#include "Framework/Application/SlateApplication.h" +#include "HAL/ThreadManager.h" + +#include "HoudiniPackageParams.h" +#include "HoudiniGeoImporter.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniOutput.h" +#include "HoudiniPDGImporterMessages.h" +#include "HoudiniEngineRuntimeUtils.h" + + +UHoudiniGeoImportCommandlet::UHoudiniGeoImportCommandlet() +{ + HelpDescription = TEXT("Import BGEOs as UAssets. Includes an option to watch a directories and include new .bgeos created there."); + + HelpUsage = TEXT("HoudiniGeoImport Usage: HoudiniGeoImport {options} [filename.bgeo]"); + // "Options:\n" + // "\t-help or -?\n" + // "\t\tDisplays this help.\n\n" + // "\t-listen=manager_messaging_address\n\n" + // "\t\tListen for connections from the HoudiniEngine plug-in's PDG manager, and import bgeo files/assets requested by the PDG manager.\n\n" + // "\t-watch=directory\n\n" + // "\t\tA directory to watch for new .bgeo files to import.\n\n" + // "\t-managerpid=owner_pid\n\n" + // "\t\tThe PID of the owner/manager process. If the manager process dies the commandlet also quits.\n\n" + // "\t-bake\n\n" + // "\t\tBake generated assets. Instancers are baked to blueprints. Not supported in -listen mode.\n\n" + // "\t[filename.bgeo]\n" + // "\t\tWhen not using -listen or -watch, the path to a .bgeo file must be specified for import.\n" + //); + + HelpParamNames = { + "help", + "listen", + "guid", + "watch", + "managerpid", + "bake" + }; + + HelpParamDescriptions = { + "Displays this help.", + "Listen for connections from the HoudiniEngine plug-in's PDG manager, and import bgeo files/assets requested by the PDG manager. Expects the owning process' PID.", + "Specify a GUID for the commandlet. Useful to identify the commandlet when the messaging system is used.", + "A directory to watch for new .bgeo files to import.", + "The PID of the owner/manager process. If the manager process dies the commandlet also quits.", + "Bake generated assets. Instancers are baked to blueprints. Not supported in -listen mode." + }; + + IsClient = false; + IsEditor = true; + IsServer = false; + LogToConsole = true; + ShowProgress = false; + ShowErrorCount = false; + + // LogToConsole = false; + + Mode = EHoudiniGeoImportCommandletMode::None; + bBakeOutputs = false; +} + +void UHoudiniGeoImportCommandlet::PrintUsage() const +{ + HOUDINI_LOG_DISPLAY(TEXT("%s"), *HelpDescription); + HOUDINI_LOG_DISPLAY(TEXT("%s"), *HelpUsage); + const int32 NumOptions = HelpParamNames.Num(); + for (int32 Idx = 0; Idx < NumOptions; ++Idx) + { + HOUDINI_LOG_DISPLAY(TEXT("-%s\t%s"), *HelpParamNames[Idx], *HelpParamDescriptions[Idx]); + } +} + +void UHoudiniGeoImportCommandlet::PopulatePackageParams(const FString &InBGEOFilename, FHoudiniPackageParams& OutPackageParams) +{ + UObject* InParent = this; + + if (bBakeOutputs) + { + OutPackageParams.PackageMode = EPackageMode::Bake; + } + else + { + OutPackageParams.PackageMode = EPackageMode::CookToTemp; + } + OutPackageParams.ReplaceMode = EPackageReplaceMode::CreateNewAssets; + + OutPackageParams.TempCookFolder = FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder(); + OutPackageParams.BakeFolder = FHoudiniEngineRuntime::Get().GetDefaultBakeFolder(); + + OutPackageParams.HoudiniAssetName = FPaths::GetBaseFilename(InBGEOFilename); + OutPackageParams.HoudiniAssetActorName = FString(); + OutPackageParams.ObjectName = FPaths::GetBaseFilename(InBGEOFilename); + + if (!OutPackageParams.OuterPackage) + { + OutPackageParams.OuterPackage = InParent; + } + + if (!OutPackageParams.ComponentGUID.IsValid()) + { + // TODO: will need to reuse the GUID when reimporting? + OutPackageParams.ComponentGUID = FGuid::NewGuid(); + } +} + +void UHoudiniGeoImportCommandlet::TickDiscoveredFiles() +{ + for (auto &FileDataEntry : DiscoveredFiles) + { + FDiscoveredFileData &FileData = FileDataEntry.Value; + if (FileData.bImportNextTick && !FileData.bImported) + { + FileData.bImportNextTick = false; + FileData.ImportAttempts++; + + FHoudiniPackageParams PackageParams; + PopulatePackageParams(FileData.FileName, PackageParams); + TArray> Outputs; + int32 Error = ImportBGEO(FileData.FileName, PackageParams, Outputs); + if (Error == 0) + { + FileData.bImported = true; + HOUDINI_LOG_DISPLAY(TEXT("Importing %s... Done"), *FileData.FileName); + } + else + { + FileData.bImported = false; + HOUDINI_LOG_DISPLAY(TEXT("Importing %s... Failed (%d)"), *FileData.FileName, Error); + } + } + } +} + +int32 UHoudiniGeoImportCommandlet::MainLoop() +{ + GIsRunning = true; + + IDirectoryWatcher* DirectoryWatcher = nullptr; + + if (Mode == EHoudiniGeoImportCommandletMode::Listen) + { + PDGEndpoint = FMessageEndpoint::Builder("PDG/BGEO Commandlet") + .Handling(this, &UHoudiniGeoImportCommandlet::HandleImportBGEOMessage) + .ReceivingOnThread(ENamedThreads::GameThread); + if (!PDGEndpoint.IsValid()) + { + GIsRunning = false; + return 3; + } + // Notify the manager that we are running + HOUDINI_LOG_DISPLAY(TEXT("Notifying the manager (%s) that we are running"), *ManagerAddress.ToString()); + // Try to send directly to the manager + // TODO: this initially direct message does not work, the address looks to be correct, perhaps there is some + // additional set up needed to connect / discover the endpoints? + PDGEndpoint->Send(new FHoudiniPDGImportBGEODiscoverMessage(Guid), ManagerAddress); + } + else if (Mode == EHoudiniGeoImportCommandletMode::Watch) + { + FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::Get().LoadModuleChecked(TEXT("DirectoryWatcher")); + DirectoryWatcher = DirectoryWatcherModule.Get(); + } + + // In UnrealEngine 4.25 and older we cannot tick the editor engine without slate being initialized. + if (!FSlateApplication::IsInitialized()) + { + FSlateApplication::InitHighDPI(false); + FSlateApplication::Create(); + } + + // If slate is initialized, make sure it has a renderer. If we have to create a renderer, create the null renderer. + if (FSlateApplication::IsInitialized() && !FSlateApplication::Get().GetRenderer()) + { + const TSharedPtr SlateRenderer = FModuleManager::Get().LoadModuleChecked("SlateNullRenderer").CreateSlateNullRenderer(); + const TSharedRef SlateRendererSharedRef = SlateRenderer.ToSharedRef(); + FSlateApplication::Get().InitializeRenderer(SlateRendererSharedRef); + } + + // in listen mode broadcast our presence every 60 seconds + // This is an attempt to test if it solves a rare issue where the endpoints appear to get + // "disconnected" and sending a message to a previously valid message address stops working, even though + // both processes are still running (happens especially when debugging with breakpoints) + const float BroadcastIntervalSeconds = 60.0f; + float LastbroadcastTimeSeconds = 0.0f; + + // main loop + while (GIsRunning && !IsEngineExitRequested()) + { + GEngine->UpdateTimeAndHandleMaxTickRate(); + GEngine->Tick(FApp::GetDeltaTime(), false); + + if (FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().PumpMessages(); + FSlateApplication::Get().Tick(); + } + + // Required for FTimerManager to function - as it blocks ticks, if the frame counter doesn't change + GFrameCounter++; + + // update task graph + FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread); + + FTSTicker::GetCoreTicker().Tick(FApp::GetDeltaTime()); + FThreadManager::Get().Tick(); + GEngine->TickDeferredCommands(); + + if (DirectoryWatcherHandle.IsValid() && DirectoryWatcher) + { + // DirectoryWatcher->Tick(FApp::GetDeltaTime()); + + // Process the discovered files + TickDiscoveredFiles(); + } + + if (OwnerProcHandle.IsValid() && !FPlatformProcess::IsProcRunning(OwnerProcHandle)) + { + // Our once valid owner has disappeared, so quit. + RequestEngineExit(TEXT("OwnerDisappeared")); + } + + if (Mode == EHoudiniGeoImportCommandletMode::Listen && PDGEndpoint.IsValid()) + { + const float TimeSeconds = FPlatformTime::Seconds(); + if (TimeSeconds - LastbroadcastTimeSeconds >= BroadcastIntervalSeconds) + { + LastbroadcastTimeSeconds = TimeSeconds; + // Broadcast a discover message to notify that we are still available + PDGEndpoint->Publish(new FHoudiniPDGImportBGEODiscoverMessage(Guid)); + + HOUDINI_LOG_MESSAGE(TEXT("Publishing FHoudiniPDGImportBGEODiscoverMessage(%s)"), *Guid.ToString()); + } + } + + FPlatformProcess::Sleep(0); + } + + PDGEndpoint.Reset(); + if (DirectoryWatcherHandle.IsValid() && DirectoryWatcher) + { + DirectoryWatcher->UnregisterDirectoryChangedCallback_Handle(DirectoryToWatch, DirectoryWatcherHandle); + } + + if (FSlateApplication::IsInitialized()) + FSlateApplication::Shutdown(); + + GIsRunning = false; + + return 0; +} + +void +UHoudiniGeoImportCommandlet::HandleImportBGEOMessage( + const FHoudiniPDGImportBGEOMessage& InMessage, + const TSharedRef& InContext) +{ + HOUDINI_LOG_DISPLAY(TEXT("Received BGEO import request from %s"), *InContext->GetSender().ToString()); + + FHoudiniPackageParams PackageParams; + InMessage.PopulatePackageParams(PackageParams); + + TArray> Outputs; + TMap> OutputObjectAttributes; + TMap InstancedOutputPartData; + if (ImportBGEO(InMessage.FilePath, PackageParams, Outputs, &InMessage.StaticMeshGenerationProperties, &InMessage.MeshBuildSettings, &OutputObjectAttributes, &InstancedOutputPartData) == 0) + { + FHoudiniPDGImportBGEOResultMessage* Reply = new FHoudiniPDGImportBGEOResultMessage(); + (*Reply) = InMessage; + // Reply->PopulateFromPackageParams(PackageParams); + Reply->ImportResult = EHoudiniPDGImportBGEOResult::HPIBR_Success; + + const int32 NumOutputs = Outputs.Num(); + Reply->Outputs.SetNum(NumOutputs); + for (int32 n = 0; n < Reply->Outputs.Num(); n++) + Reply->Outputs[n] = FHoudiniPDGImportNodeOutput(); + + for (int32 Index = 0; Index < NumOutputs; ++Index) + { + FHoudiniPDGImportNodeOutput &MessageOutput = Reply->Outputs[Index]; + UHoudiniOutput* Output = Outputs[Index]; + for (const FHoudiniGeoPartObject& HGPO : Output->GetHoudiniGeoPartObjects()) + { + HOUDINI_LOG_WARNING(TEXT("HGPO %d %d %d"), HGPO.ObjectId, HGPO.GeoId, HGPO.PartId); + MessageOutput.HoudiniGeoPartObjects.Add(HGPO); + + // Get instancer data if this is an instancer output + if (Output->GetType() == EHoudiniOutputType::Instancer) + { + FHoudiniOutputObjectIdentifier OutputIdentifier; + OutputIdentifier.ObjectId = HGPO.ObjectId; + OutputIdentifier.GeoId = HGPO.GeoId; + OutputIdentifier.PartId = HGPO.PartId; + OutputIdentifier.PartName = HGPO.PartName; + + FHoudiniInstancerPartData* InstancedOutputPartDataPtr = InstancedOutputPartData.Find(OutputIdentifier); + if (InstancedOutputPartDataPtr) + { + MessageOutput.InstancedOutputPartData.Add(*InstancedOutputPartDataPtr); + } + else + { + MessageOutput.InstancedOutputPartData.Add(FHoudiniInstancerPartData()); + } + } + } + for (const auto& Entry : Output->GetOutputObjects()) + { + HOUDINI_LOG_WARNING(TEXT("Identifier %d %d %d"), Entry.Key.ObjectId, Entry.Key.GeoId, Entry.Key.PartId); + + MessageOutput.OutputObjects.AddDefaulted(); + FHoudiniPDGImportNodeOutputObject& MessageOutputObject = MessageOutput.OutputObjects.Last(); + + FString PackagePath = IsValid(Entry.Value.OutputObject) ? Entry.Value.OutputObject->GetPathName() : ""; + MessageOutputObject.Identifier = Entry.Key; + MessageOutputObject.PackagePath = PackagePath; + const TArray* PropertyAttributes = OutputObjectAttributes.Find(Entry.Key); + if (PropertyAttributes) + MessageOutputObject.GenericAttributes = *PropertyAttributes; + MessageOutputObject.CachedAttributes = Entry.Value.CachedAttributes; + } + } + + PDGEndpoint->Send(Reply, InContext->GetSender()); + } + else + { + HOUDINI_LOG_WARNING(TEXT("BGEO import failed.")); + FHoudiniPDGImportBGEOResultMessage* Reply = new FHoudiniPDGImportBGEOResultMessage(); + Reply->ImportResult = EHoudiniPDGImportBGEOResult::HPIBR_Failed; + PDGEndpoint->Send(Reply, InContext->GetSender()); + } + + // Cleanup the outputs (remove from root) + TArray PackagesToUnload; + for (UHoudiniOutput *CurOutput : Outputs) + { + if (!IsValid(CurOutput)) + continue; + + for (const auto& Entry : CurOutput->GetOutputObjects()) + { + if (IsValid(Entry.Value.OutputObject)) + { + UPackage *Outermost = Entry.Value.OutputObject->GetOutermost(); + if (IsValid(Outermost)) + { + PackagesToUnload.Add(Outermost); + } + + Entry.Value.OutputObject->RemoveFromRoot(); + } + } + + CurOutput->RemoveFromRoot(); + } + Outputs.Empty(); + OutputObjectAttributes.Empty(); + + if (PackagesToUnload.Num() > 0) + { + HOUDINI_LOG_DISPLAY(TEXT("Unloading %d packages ..."), PackagesToUnload.Num()); + FText ErrorMessage; + if (!UPackageTools::UnloadPackages(PackagesToUnload, ErrorMessage)) + { + HOUDINI_LOG_WARNING(TEXT("Unload packages failed: %s"), *ErrorMessage.ToString()); + } + else + { + HOUDINI_LOG_DISPLAY(TEXT("Unloading %d packages ... Success"), PackagesToUnload.Num()); + } + PackagesToUnload.Empty(); + } + + CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); +} + +bool UHoudiniGeoImportCommandlet::StartHoudiniEngineSession() +{ + // Start Houdini Engine session + HOUDINI_LOG_DISPLAY(TEXT("Starting Houdini Engine session...")); + FHoudiniEngine& HoudiniEngine = FHoudiniEngine::Get(); + if (!HoudiniEngine.CreateSession( + EHoudiniRuntimeSettingsSessionType::HRSST_NamedPipe, + "hapi_bgeo_cmdlet")) + { + HOUDINI_LOG_ERROR(TEXT("Failed to start Houdini Engine session.")); + return false; + } + + return true; +} + +int32 UHoudiniGeoImportCommandlet::ImportBGEO( + const FString &InFilename, + const FHoudiniPackageParams &InPackageParams, + TArray>& OutOutputs, + const FHoudiniStaticMeshGenerationProperties* InStaticMeshGenerationProperties, + const FMeshBuildSettings* InMeshBuildSettings, + TMap>* OutGenericAttributes, + TMap* OutInstancedOutputPartData) +{ + if (!IsHoudiniEngineSessionRunning() && !StartHoudiniEngineSession()) + { + return 2; + } + + FHoudiniPackageParams PackageParams = InPackageParams; + UHoudiniGeoImporter* GeoImporter = NewObject(this); + + TArray> OldOutputs; + OutOutputs.Empty(); + + // 2. Update the file paths + HOUDINI_LOG_DISPLAY(TEXT("SetFilePath %s"), *InFilename); + if (!GeoImporter->SetFilePath(InFilename)) + return 1; + + // 3. Load the BGEO file in HAPI + HAPI_NodeId NodeId; + HOUDINI_LOG_DISPLAY(TEXT("LoadBGEOFileInHAPI")); + if (!GeoImporter->LoadBGEOFileInHAPI(NodeId)) + return 1; + + // Look for a bake folder override in the BGEO file + if (PackageParams.PackageMode == EPackageMode::Bake) + { + HOUDINI_LOG_DISPLAY(TEXT("Looking for bake folder override attribute...")); + // Get the geo id for the node id + HAPI_GeoInfo DisplayGeoInfo; + FHoudiniApi::GeoInfo_Init(&DisplayGeoInfo); + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetDisplayGeoInfo(FHoudiniEngine::Get().GetSession(), NodeId, &DisplayGeoInfo)) + { + TArray BakeFolderOverrideArray; + FString BakeFolderOverride; + const bool bFoundOverride = FHoudiniEngineUtils::GetBakeFolderAttribute( + DisplayGeoInfo.nodeId, HAPI_ATTROWNER_DETAIL, BakeFolderOverrideArray, 0, 1); + + if (bFoundOverride && BakeFolderOverrideArray.Num() > 0) + BakeFolderOverride = BakeFolderOverrideArray[0]; + + if (!BakeFolderOverride.IsEmpty()) + { + PackageParams.BakeFolder = BakeFolderOverride; + HOUDINI_LOG_DISPLAY(TEXT("Found bake folder override (detail attrib): %s"), *PackageParams.BakeFolder); + } + else + { + HOUDINI_LOG_DISPLAY(TEXT("No bake folder override, using: %s"), *PackageParams.BakeFolder); + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("Could not find display geo node id (when looking for bake folder override).")); + } + } + + auto CleanUpAndExit = [&OutOutputs, GeoImporter, NodeId](int32 InExitCode) + { + GeoImporter->GetOutputObjects().Empty(); + for (UHoudiniOutput* Output : OutOutputs) + { + Output->RemoveFromRoot(); + } + OutOutputs.Empty(); + + if (NodeId >= 0) + GeoImporter->DeleteCreatedNode(NodeId); + + return InExitCode; + }; + + // 4. Get the output from the file node + HOUDINI_LOG_DISPLAY(TEXT("BuildOutputsForNode %d"), NodeId); + if (!GeoImporter->BuildOutputsForNode(NodeId, OldOutputs, OutOutputs, true)) + return CleanUpAndExit(1); + + // Create uniquely named packages, commandlet runs in conjunction + // with a main editor instance, so we cannot modify existing files + PackageParams.ReplaceMode = EPackageReplaceMode::CreateNewAssets; + + // FString PackageName; + // UPackage* Outer = PackageParams.CreatePackageForObject(PackageName); + UObject* Outer = this; + + // 5. Create the static meshes in the outputs + const FHoudiniStaticMeshGenerationProperties& StaticMeshGenerationProperties = + InStaticMeshGenerationProperties? + *InStaticMeshGenerationProperties : + FHoudiniEngineRuntimeUtils::GetDefaultStaticMeshGenerationProperties(); + + const FMeshBuildSettings& MeshBuildSettings = + InMeshBuildSettings ? *InMeshBuildSettings : FHoudiniEngineRuntimeUtils::GetDefaultMeshBuildSettings(); + + HOUDINI_LOG_DISPLAY(TEXT("Creating Objects from Outputs")); + if (!GeoImporter->CreateObjectsFromOutputs(OutOutputs, PackageParams, StaticMeshGenerationProperties, MeshBuildSettings, OutInstancedOutputPartData)) + return CleanUpAndExit(1); + + if (OutGenericAttributes) + { + // Collect all generic properties from Houdini, we need to pass these + // through to PDG manager + HOUDINI_LOG_DISPLAY(TEXT("Get Generic Attributes for static meshes")); + for (UHoudiniOutput* CurOutput : OutOutputs) + { + if (CurOutput->GetType() != EHoudiniOutputType::Mesh) + continue; + + for (const auto& Entry : CurOutput->GetOutputObjects()) + { + const FHoudiniOutputObjectIdentifier OutputIdentifier = Entry.Key; + TArray PropertyAttributes; + FHoudiniEngineUtils::GetGenericPropertiesAttributes( + OutputIdentifier.GeoId, OutputIdentifier.PartId, + true, OutputIdentifier.PrimitiveIndex, INDEX_NONE, OutputIdentifier.PointIndex, + PropertyAttributes); + OutGenericAttributes->Add(OutputIdentifier, PropertyAttributes); + } + } + } + + // 8. Delete the created node in Houdini + HOUDINI_LOG_DISPLAY(TEXT("DeleteCreatedNode %d"), NodeId); + if (!GeoImporter->DeleteCreatedNode(NodeId)) + { + // Not good, but not fatal.. + //return false; + } + + TArray PackagesToSave; + TArray& OutputObjects = GeoImporter->GetOutputObjects(); + for (UObject* Object : OutputObjects) + { + if (!IsValid(Object)) + continue; + + HOUDINI_LOG_DISPLAY(TEXT("Created object: %s"), *Object->GetFullName()); + + UAssetImportData* AssetImportData = nullptr; + if (Object->IsA()) + { + UStaticMesh* SM = Cast(Object); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + AssetImportData = SM->GetAssetImportData(); +#else + AssetImportData = SM->AssetImportData; +#endif + // Create reimport information. + if (!AssetImportData) + { + AssetImportData = NewObject< UAssetImportData >(SM, UAssetImportData::StaticClass()); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + SM->SetAssetImportData(AssetImportData); +#else + SM->AssetImportData = AssetImportData; +#endif + } + } + + if (AssetImportData) + AssetImportData->Update(InFilename); + + Object->MarkPackageDirty(); + Object->PostEditChange(); + + UPackage* Package = Object->GetOutermost(); + if (IsValid(Package)) + { + PackagesToSave.AddUnique(Package); + } + } + + if (PackagesToSave.Num() > 0) + { + UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, true); + } + + PackagesToSave.Empty(); + OutputObjects.Empty(); + + return 0; +} + +void UHoudiniGeoImportCommandlet::HandleDirectoryChanged(const TArray& InFileChangeDatas) +{ + const FRegexPattern BGEOPattern(TEXT(R"((.*)\.(bgeo(\.[^\.]*)?)$)")); + + for (const FFileChangeData& FileChangeData : InFileChangeDatas) + { + HOUDINI_LOG_MESSAGE(TEXT("HandleDirectoryChanged %d %s"), FileChangeData.Action, *FileChangeData.Filename); + + FRegexMatcher BGEOMatcher(BGEOPattern, FileChangeData.Filename.ToLower()); + if (BGEOMatcher.FindNext() && BGEOMatcher.GetCaptureGroup(2).StartsWith(TEXT("bgeo"))) + { + HOUDINI_LOG_DISPLAY(TEXT("Updating entry for %s..."), *FileChangeData.Filename); + const uint32 MaxImportAttempts = 3; + switch(FileChangeData.Action) + { + case FFileChangeData::FCA_Added: + case FFileChangeData::FCA_Modified: + if (DiscoveredFiles.Contains(FileChangeData.Filename)) + { + FDiscoveredFileData &FileData = DiscoveredFiles[FileChangeData.Filename]; + if (!FileData.bImported && FileData.ImportAttempts < MaxImportAttempts) + FileData.bImportNextTick = true; + else if (FileData.ImportAttempts >= MaxImportAttempts) + HOUDINI_LOG_WARNING(TEXT("Not importing %s, max attempts exceeded %d"), *FileData.FileName, FileData.ImportAttempts); + } + else + { + DiscoveredFiles.Add(FileChangeData.Filename, FDiscoveredFileData(FileChangeData.Filename, true)); + } + break; + + case FFileChangeData::FCA_Removed: + DiscoveredFiles.Remove(FileChangeData.Filename); + break; + + default: + HOUDINI_LOG_WARNING(TEXT("Unknown file change event %d for %s"), FileChangeData.Action, *FileChangeData.Filename); + } + } + } +} + +int32 UHoudiniGeoImportCommandlet::Main(const FString& InParams) +{ + TArray Tokens; + TArray Switches; + TMap Params; + ParseCommandLine(*InParams, Tokens, Switches, Params); + + if (Switches.Contains(TEXT("help")) || Switches.Contains(TEXT("?"))) + { + PrintUsage(); + return 0; + } + + if (Params.Contains(TEXT("guid"))) + { + const FString GuidStr = Params.FindChecked(TEXT("guid")); + FGuid::Parse(GuidStr, Guid); + + HOUDINI_LOG_DISPLAY(TEXT("GUID received: %s"), *Guid.ToString()); + } + else + { + Guid = FGuid::NewGuid(); + } + + // Set bake mode + if (Switches.Contains(TEXT("bake"))) + bBakeOutputs = true; + else + bBakeOutputs = false; + + if (Params.Contains(TEXT("listen"))) + { + Mode = EHoudiniGeoImportCommandletMode::Listen; + + if (!Params.Contains(TEXT("managerpid"))) + { + HOUDINI_LOG_ERROR(TEXT("'managerpid' is required when in -listen mode.")); + return 1; + } + + if (bBakeOutputs) + { + HOUDINI_LOG_ERROR(TEXT("'listen' mode does not support baking outputs (-bake).")); + return 1; + } + + // Get the manager's messaging address from the -listen param + const FString ManagerAddressStr = Params.FindChecked(TEXT("listen")); + if (!FMessageAddress::Parse(ManagerAddressStr, ManagerAddress)) + { + HOUDINI_LOG_ERROR(TEXT("The manager messaging address passed to -listen=%s is invalid."), *ManagerAddressStr); + return 1; + } + + // Get the manager pid and proc handle + uint32 OwnerProcessId = FCString::Strtoi(*Params.FindChecked(TEXT("managerpid")), nullptr, 10); + HOUDINI_LOG_DISPLAY(TEXT("Owner process Id: %d"), OwnerProcessId); + OwnerProcHandle = FPlatformProcess::OpenProcess(OwnerProcessId); + + return MainLoop(); + } + else if (Params.Contains(TEXT("watch"))) + { + Mode = EHoudiniGeoImportCommandletMode::Watch; + + HOUDINI_LOG_DISPLAY(TEXT("directory watch mode")); + FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::LoadModuleChecked(TEXT("DirectoryWatcher")); + IDirectoryWatcher* DirectoryWatcher = DirectoryWatcherModule.Get(); + if (DirectoryWatcher) + { + DirectoryToWatch = Params.FindChecked(TEXT("watch")); + if (FPaths::IsRelative(DirectoryToWatch)) + DirectoryToWatch = FPaths::ConvertRelativePathToFull(DirectoryToWatch); + + HOUDINI_LOG_DISPLAY(TEXT("Watching %s"), *DirectoryToWatch); + + DirectoryWatcher->RegisterDirectoryChangedCallback_Handle( + DirectoryToWatch, + IDirectoryWatcher::FDirectoryChanged::CreateUObject(this, &UHoudiniGeoImportCommandlet::HandleDirectoryChanged), + DirectoryWatcherHandle); + + return MainLoop(); + } + else + { + return 10; + } + } + else if (Tokens.Num() > 0) + { + Mode = EHoudiniGeoImportCommandletMode::SpecifiedFiles; + + if (!StartHoudiniEngineSession()) + return 2; + + const FString Filename = FPaths::IsRelative(Tokens[0]) ? FPaths::ConvertRelativePathToFull(Tokens[0]) : Tokens[0]; + FHoudiniPackageParams PackageParams; + PopulatePackageParams(Filename, PackageParams); + + TArray> Outputs; + const int32 Result = ImportBGEO(Tokens[0], PackageParams, Outputs); + + for (UHoudiniOutput* Output : Outputs) + { + Output->RemoveFromRoot(); + } + Outputs.Empty(); + + return Result; + } + + return 0; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniGeoImportCommandlet.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/ObjectMacros.h" +#include "Commandlets/Commandlet.h" +#include "MessageEndpoint.h" + +#include "HoudiniEngine.h" +#include "HoudiniGenericAttribute.h" +#include "HoudiniPDGImporterMessages.h" +#include "HoudiniGeoPartObject.h" + +#include "HoudiniGeoImportCommandlet.generated.h" + +class FSocket; + +class UHoudiniGeoImporter; +class UHoudiniOutput; + +struct FHoudiniPackageParams; + +enum class EHoudiniGeoImportCommandletMode : uint8 +{ + // Unspecified + None, + // Import of specified file + SpecifiedFiles, + // Directory watch mode + Watch, + // Listen mode (via PDGManager) + Listen +}; + +struct FDiscoveredFileData +{ +public: + FDiscoveredFileData() : FileName(), bImportNextTick(false), ImportAttempts(0), bImported(false) {} + + FDiscoveredFileData(const FString& InFileName, bool bInImportNextTick=false) : FileName(InFileName), bImportNextTick(bInImportNextTick), ImportAttempts(0), bImported(false) {} + + FDiscoveredFileData(FString&& InFileName, bool bInImportNextTick=false) : FileName(InFileName), bImportNextTick(bInImportNextTick), ImportAttempts(0), bImported(false) {} + + // Full/absolute file path + FString FileName; + + // Try to import this file on the next tick + bool bImportNextTick; + + // Number of attempts at importing this file + uint32 ImportAttempts; + + // The file has been imported successfully + bool bImported; +}; + +UCLASS() +class HOUDINIENGINE_API UHoudiniGeoImportCommandlet : public UCommandlet +{ + GENERATED_BODY() + +public: + + UHoudiniGeoImportCommandlet(); + + void PrintUsage() const; + + /** + * Entry point for your commandlet + * + * @param Params the string containing the parameters for the commandlet + */ + virtual int32 Main(const FString& Params) override; + + void HandleImportBGEOMessage( + const struct FHoudiniPDGImportBGEOMessage& InMessage, + const TSharedRef& InContext); + + void HandleDirectoryChanged(const TArray& InFileChangeDatas); + +protected: + + void PopulatePackageParams(const FString& InBGEOFilename, FHoudiniPackageParams& OutPackageParams); + + bool StartHoudiniEngineSession(); + + bool IsHoudiniEngineSessionRunning() { return FHoudiniEngine::Get().GetSession() != nullptr; }; + + int32 MainLoop(); + + int32 ImportBGEO( + const FString& InFilename, + const FHoudiniPackageParams& InPackageParams, + TArray>& OutOutputs, + const FHoudiniStaticMeshGenerationProperties* InStaticMeshGenerationProperties=nullptr, + const FMeshBuildSettings* InMeshBuildSettings=nullptr, + TMap>* OutGenericAttributes=nullptr, + TMap* OutInstancedOutputPartData=nullptr); + + void TickDiscoveredFiles(); + +private: + + // Messaging end point for receiving messages from PDG manager + TSharedPtr PDGEndpoint; + + // The messaging address of the manager + FMessageAddress ManagerAddress; + + // Unique ID of the commandlet. + FGuid Guid; + + // The proc handle of our owner (if in listen mode, quit when the owner stops running). + FProcHandle OwnerProcHandle; + + // TODO: Map so that we can watch multiple directories? + // Directory to watch + FString DirectoryToWatch; + // Handle if we are watching a directory for changes. + FDelegateHandle DirectoryWatcherHandle; + + // Keep track of files discovered by the watcher, and their state + TMap DiscoveredFiles; + + // Mode in which commandlet is running + EHoudiniGeoImportCommandletMode Mode; + + // Bake outputs via FHoudiniEngineBakeUtils + bool bBakeOutputs; +}; +``` + +`Source/HoudiniEngine/Private/HoudiniGeoImporter.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniGeoImporter.h" + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniPackageParams.h" +#include "HoudiniOutput.h" +#include "HoudiniOutputTranslator.h" +#include "HoudiniMeshTranslator.h" +#include "HoudiniLandscapeTranslator.h" +#include "HoudiniInstanceTranslator.h" +#include "HoudiniSplineTranslator.h" +#include "HoudiniDataTableTranslator.h" +#include "HoudiniSkeletalMeshTranslator.h" +#include "HoudiniAnimationTranslator.h" +#include "HoudiniTextureTranslator.h" +#include "HoudiniGeometryCollectionTranslator.h" +#include "HoudiniSplineComponent.h" +#include "HoudiniEngineRuntimeUtils.h" + +#include "CoreMinimal.h" +#include "Misc/Paths.h" +#include "Misc/PackageName.h" +#include "UObject/Package.h" +#include "PackageTools.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Editor.h" +#include "HoudiniEngineBakeUtils.h" + +#include "ImageUtils.h" // Included for FCreateTexture2DParameters +#include "Materials/MaterialInterface.h" +#include "Materials/Material.h" + + +UHoudiniGeoImporter::UHoudiniGeoImporter(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) + , SourceFilePath() + , AbsoluteFilePath() + , AbsoluteFileDirectory() + , FileName() + , FileExtension() + , BakeRootFolder(TEXT("/Game/HoudiniEngine/Bake/")) +{ +} + +bool +UHoudiniGeoImporter::SetFilePath(const FString& InFilePath) +{ + SourceFilePath = InFilePath; + if (!FPaths::FileExists(SourceFilePath)) + { + // Cant find BGEO file + HOUDINI_LOG_ERROR(TEXT("Houdini GEO Importer: could not find file %s!"), *InFilePath); + return false; + } + + // Make sure we're using absolute path! + AbsoluteFilePath = FPaths::ConvertRelativePathToFull(SourceFilePath); + + // Split the file path + FPaths::Split(AbsoluteFilePath, AbsoluteFileDirectory, FileName, FileExtension); + + // Handle .bgeo.sc correctly + if (FileExtension.Equals(TEXT("sc"))) + { + // append the bgeo to .sc + FileExtension = FPaths::GetExtension(FileName) +TEXT(".") + FileExtension; + // update the filename + FileName = FPaths::GetBaseFilename(FileName); + } + + if (FileExtension.IsEmpty()) + FileExtension = TEXT("bgeo"); + + if (!FileExtension.StartsWith(TEXT("bgeo"), ESearchCase::IgnoreCase)) + { + // Not a bgeo file! + HOUDINI_LOG_ERROR(TEXT("Houdini GEO Importer: File %s is not a .bgeo or .bgeo.sc file!"), *SourceFilePath); + return false; + } + + //BGEOFilePath = BGEOPath + TEXT("/") + BGEOFileName + TEXT(".") + BGEOExtension; + + // Only use "/" for the output file path + BakeRootFolder.ReplaceInline(TEXT("\\"), TEXT("/")); + // Make sure the output folder ends with a "/" + if (!BakeRootFolder.EndsWith("/")) + BakeRootFolder += TEXT("/"); + + // If we have't specified an outpout file name yet, use the input file name + if (OutputFilename.IsEmpty()) + OutputFilename = FileName; + + return true; +} + +bool +UHoudiniGeoImporter::AutoStartHoudiniEngineSessionIfNeeded() +{ + if (FHoudiniEngine::Get().GetSession()) + return true; + + // Default first session already attempted to be created ? stop here? + /* + if (FHoudiniEngine::Get().GetFirstSessionCreated()) + return false; + */ + + // Indicates that we've tried to start a session once no matter if it failed or succeed + FHoudiniEngine::Get().SetFirstSessionCreated(true); + if (!FHoudiniEngine::Get().RestartSession()) + { + HOUDINI_LOG_ERROR(TEXT("Houdini GEO Importer: Couldn't start the default HoudiniEngine session!")); + return false; + } + + return true; +} + +bool +UHoudiniGeoImporter::BuildOutputsForNode( + const HAPI_NodeId& InNodeId, + TArray>& InOldOutputs, + TArray>& OutNewOutputs, + bool bInUseOutputNodes) +{ + FString Notification = TEXT("BGEO Importer: Getting output geos..."); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Notification)); + + const bool bInAddOutputsToRootSet = true; + return BuildAllOutputsForNode(InNodeId, this, InOldOutputs, OutNewOutputs, bInAddOutputsToRootSet, bInUseOutputNodes); +} + +bool UHoudiniGeoImporter::CreateObjectsFromOutputs( + TArray>& InOutputs, + FHoudiniPackageParams InPackageParams, + const FHoudiniStaticMeshGenerationProperties& InStaticMeshGenerationProperties, + const FMeshBuildSettings& InMeshBuildSettings, + TMap* OutInstancedOutputPartData) +{ + // + // This isn't ideal but the reason we do this is because previously each + // method we call (i.e. CreateCurves) would filter the outputs we pass in. + // This way, we loop through the array of outputs only once. + // + // The reason we cannot have methods for individual outputs is that some + // output types, like curves, are merged together into a single blueprint. + // + TArray MeshOutputs; + TArray CurveOutputs; + TArray LandscapeOutputs; + TArray LandscapeSplineOutputs; + TArray InstancerOutputs; + TArray DataTableOutputs; + TArray SkeletalOutputs; + TArray AnimSequenceOutputs; + TArray CopTextureOutputs; + + for (UHoudiniOutput* const Output : InOutputs) + { + switch (Output->GetType()) + { + case EHoudiniOutputType::Mesh: + MeshOutputs.Add(Output); + break; + case EHoudiniOutputType::Curve: + CurveOutputs.Add(Output); + break; + case EHoudiniOutputType::Landscape: + LandscapeOutputs.Add(Output); + break; + case EHoudiniOutputType::LandscapeSpline: + LandscapeSplineOutputs.Add(Output); + break; + case EHoudiniOutputType::Instancer: + InstancerOutputs.Add(Output); + break; + case EHoudiniOutputType::DataTable: + DataTableOutputs.Add(Output); + break; + case EHoudiniOutputType::Skeletal: + SkeletalOutputs.Add(Output); + break; + case EHoudiniOutputType::AnimSequence: + AnimSequenceOutputs.Add(Output); + break; + case EHoudiniOutputType::Cop: + CopTextureOutputs.Add(Output); + break; + } + } + + if (!CreateStaticMeshes(MeshOutputs, InPackageParams, InStaticMeshGenerationProperties, InMeshBuildSettings)) + return false; + + if (!CreateCurves(CurveOutputs, InPackageParams)) + return false; + + if (!CreateLandscapes(LandscapeOutputs, InPackageParams)) + return false; + + if (!CreateLandscapeSplines(LandscapeSplineOutputs, InPackageParams)) + return false; + + if (OutInstancedOutputPartData) + { + if (!CreateInstancerOutputPartData(InstancerOutputs, *OutInstancedOutputPartData)) + return false; + } + else + { + if (!CreateInstancers(InOutputs, InstancerOutputs, InPackageParams)) + return false; + } + + if (!CreateDataTables(DataTableOutputs, InPackageParams)) + return false; + + if (!CreateSkeletalMeshes(SkeletalOutputs, InPackageParams)) + return false; + + if (!CreateAnimSequences(AnimSequenceOutputs, InPackageParams)) + return false; + + if (!CreateCopTextures(CopTextureOutputs, InPackageParams)) + return false; + + return true; +} + +bool +UHoudiniGeoImporter::CreateStaticMeshes( + const TArray& InOutputs, + FHoudiniPackageParams InPackageParams, + const FHoudiniStaticMeshGenerationProperties& InStaticMeshGenerationProperties, + const FMeshBuildSettings& InMeshBuildSettings) +{ + TMap> AllOutputMaterials; + for (UHoudiniOutput* const CurOutput : InOutputs) + { + if (CurOutput->GetType() != EHoudiniOutputType::Mesh) + continue; + + FString Notification = TEXT("BGEO Importer: Creating Static Meshes..."); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Notification)); + + //FHoudiniMeshTranslator::CreateAllMeshesFromHoudiniOutput(CurOutput, OuterPackage, OuterComponent, OuterAsset); + + TMap NewOutputObjects; + TMap OldOutputObjects = CurOutput->GetOutputObjects(); + TMap>& AssignementMaterials = CurOutput->GetAssignementMaterials(); + TMap>& ReplacementMaterials = CurOutput->GetReplacementMaterials(); + + // Iterate on all of the output's HGPO, creating meshes as we go + for (const FHoudiniGeoPartObject& CurHGPO : CurOutput->GetHoudiniGeoPartObjects()) + { + // Not a mesh, skip + if (CurHGPO.Type != EHoudiniPartType::Mesh) + continue; + + FHoudiniPackageParams PackageParams = GetPackageParamsForHGPO(CurHGPO, InPackageParams); + + UObject* const OuterComponent = nullptr; + constexpr bool bForceRebuild = true; + constexpr bool bSplitMeshSupport = false; + FHoudiniMeshTranslator::CreateStaticMeshFromHoudiniGeoPartObject( + CurHGPO, + PackageParams, + OldOutputObjects, + NewOutputObjects, + AssignementMaterials, + ReplacementMaterials, + AllOutputMaterials, + OuterComponent, + bForceRebuild, + EHoudiniStaticMeshMethod::FMeshDescription, + bSplitMeshSupport, + InStaticMeshGenerationProperties, + InMeshBuildSettings); + + for (auto& CurMat : AssignementMaterials) + { + // Adds the newly generated materials to the output materials array + // This is to avoid recreating those same materials again + if (!AllOutputMaterials.Contains(CurMat.Key)) + AllOutputMaterials.Add(CurMat); + } + } + + // Add all output objects and materials + for (auto CurOutputPair : NewOutputObjects) + { + UObject* CurObj = CurOutputPair.Value.OutputObject; + if (!IsValid(CurObj)) + continue; + + OutputObjects.Add(CurObj); + } + + // Do the same for materials + for (auto CurAssignmentMatPair : AssignementMaterials) + { + UObject* CurObj = CurAssignmentMatPair.Value; + if (!IsValid(CurObj)) + continue; + + bool bIsTemp = FHoudiniEngineBakeUtils::IsObjectTemporary(CurObj); + if (bIsTemp) + { + OutputObjects.Add(CurObj); + } + } + + // Also assign to the output objects map as we may need the meshes to create instancers later + CurOutput->SetOutputObjects(NewOutputObjects); + } + + return true; +} + +bool +UHoudiniGeoImporter::CreateCurves(const TArray& InOutputs, FHoudiniPackageParams InPackageParams) +{ + if (InOutputs.IsEmpty()) + { + return true; + } + + FString Notification = TEXT("BGEO Importer: Creating Curves..."); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Notification)); + + FHoudiniPackageParams PackageParams = GetPackageParamsForType( + InOutputs, + InPackageParams, + EHoudiniOutputType::Curve, + EHoudiniPartType::Curve); + + // Create a Package for the BP + PackageParams.ObjectName = TEXT("BP_") + PackageParams.ObjectName; + PackageParams.ReplaceMode = EPackageReplaceMode::CreateNewAssets; + + FString PackageName; + UPackage* BPPackage = PackageParams.CreatePackageForObject(PackageName); + check(BPPackage); + + // Create and init a new Blueprint Actor + UBlueprint* Blueprint = FKismetEditorUtilities::CreateBlueprint(AActor::StaticClass(), BPPackage, *PackageName, BPTYPE_Normal, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass(), FName("HoudiniGeoImporter")); + if (!Blueprint) + return false; + + // Create a fake outer component that we'll use as a temporary outer for our curves + UWorld* TempWorld = UWorld::CreateWorld(EWorldType::Inactive, false, TEXT("BGEOImporterTemp"), GetTransientPackage(), false); + const FActorSpawnParameters ActorSpawnParameters; + AActor* OuterActor = TempWorld->SpawnActor(ActorSpawnParameters); + USceneComponent* OuterComponent = + NewObject(OuterActor, USceneComponent::GetDefaultSceneRootVariableName()); + + for (UHoudiniOutput* const CurOutput : InOutputs) + { + check(CurOutput->GetType() == EHoudiniOutputType::Curve); + + // Output curve + FHoudiniSplineTranslator::CreateAllSplinesFromHoudiniOutput(CurOutput, OuterComponent); + + // Prepare an ActorComponent array for AddComponentsToBlueprint() + TArray OutputComp; + for (auto CurOutputPair : CurOutput->GetOutputObjects()) + { + for (auto Component : CurOutputPair.Value.OutputComponents) + { + UActorComponent* CurObj = Cast(Component); + if (!IsValid(CurObj)) + continue; + + OutputComp.Add(CurObj); + } + } + + // Transfer all the instancer components to the BP + if (OutputComp.Num() > 0) + { + FKismetEditorUtilities::FAddComponentsToBlueprintParams Params; + Params.HarvestMode = FKismetEditorUtilities::EAddComponentToBPHarvestMode::None; + Params.OptionalNewRootNode = nullptr; + Params.bKeepMobility = false; + FKismetEditorUtilities::AddComponentsToBlueprint(Blueprint, OutputComp, Params); + } + } + + // Compile the blueprint + FKismetEditorUtilities::CompileBlueprint(Blueprint); + + // Add it to our output objects + OutputObjects.Add(Blueprint); + + return true; +} + +bool +UHoudiniGeoImporter::CreateLandscapes(const TArray& InOutputs, FHoudiniPackageParams InPackageParams) +{ + if (InOutputs.IsEmpty()) + { + return true; + } + + HOUDINI_LOG_WARNING(TEXT("Importing a landscape directly from BGEOs is not currently supported.")); + return false; +} + + +bool +UHoudiniGeoImporter::CreateLandscapeSplines(const TArray& InOutputs, FHoudiniPackageParams InPackageParams) +{ + if (InOutputs.IsEmpty()) + { + return true; + } + + HOUDINI_LOG_WARNING(TEXT("Importing landscape splines directly from BGEOs is not currently supported.")); + return false; +} + + +bool +UHoudiniGeoImporter::CreateInstancers( + TArray>& InAllOutputs, + const TArray& InInstancerOutputs, + FHoudiniPackageParams InPackageParams) +{ + if (InInstancerOutputs.IsEmpty()) + { + return true; + } + + FString Notification = TEXT("BGEO Importer: Creating Instancers..."); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Notification)); + + FHoudiniPackageParams PackageParams = GetPackageParamsForType( + InInstancerOutputs, + InPackageParams, + EHoudiniOutputType::Instancer, + EHoudiniPartType::Instancer); + + // Create a fake outer component that we'll use as a temporary outer for our instancers + USceneComponent* OuterComponent = NewObject(); + + UWorld* const World = nullptr; + + // Now that all meshes have been created, process the instancers + FHoudiniInstanceTranslator::CreateAllInstancersFromHoudiniOutputs( + InAllOutputs, OuterComponent, PackageParams); + + bool bHasGeometryCollection = false; + for (UHoudiniOutput* const CurOutput : InInstancerOutputs) + { + if (!bHasGeometryCollection && FHoudiniGeometryCollectionTranslator::IsGeometryCollectionInstancer(CurOutput)) + { + bHasGeometryCollection = true; + break; + } + } + + if (bHasGeometryCollection) + { + // TODO: We need to know who to parent new outputs too. This should be a Cookable, but we don't have one... + UObject* OutputOwner = OuterComponent; + + FHoudiniGeometryCollectionTranslator::SetupGeometryCollectionComponentFromOutputs( + InAllOutputs, OutputOwner, OuterComponent, PackageParams, World); + + if (InAllOutputs.Last()->GetType() != EHoudiniOutputType::GeometryCollection) + { + return false; + } + + for (auto CurOutputPair : InAllOutputs.Last()->GetOutputObjects()) + { + UObject* CurObj = CurOutputPair.Value.OutputObject; + if (!IsValid(CurObj)) + continue; + + OutputObjects.Add(CurObj); + } + + return true; + } + + // Create a Package for the BP + PackageParams.ObjectName = TEXT("BP_") + PackageParams.ObjectName; + PackageParams.ReplaceMode = EPackageReplaceMode::CreateNewAssets; + + FString PackageName; + UPackage* BPPackage = PackageParams.CreatePackageForObject(PackageName); + check(BPPackage); + + // Create and init a new Blueprint Actor + UBlueprint* Blueprint = FKismetEditorUtilities::CreateBlueprint(AActor::StaticClass(), BPPackage, *PackageName, BPTYPE_Normal, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass(), FName("HoudiniGeoImporter")); + if (!Blueprint) + return false; + + for (auto& CurOutput : InInstancerOutputs) + { + // Transfer all the instancer components to the BP + check(CurOutput->GetType() == EHoudiniOutputType::Instancer); + + // Prepare an ActorComponent array for AddComponentsToBlueprint() + TArray OutputComp; + for (auto CurOutputPair : CurOutput->GetOutputObjects()) + { + for (auto Component : CurOutputPair.Value.OutputComponents) + { + UActorComponent* CurObj = Cast(Component); + if (!IsValid(CurObj)) + continue; + + OutputComp.Add(CurObj); + } + } + + if (OutputComp.Num() > 0) + { + FKismetEditorUtilities::FAddComponentsToBlueprintParams Params; + Params.HarvestMode = FKismetEditorUtilities::EAddComponentToBPHarvestMode::None; + Params.OptionalNewRootNode = nullptr; + Params.bKeepMobility = false; + FKismetEditorUtilities::AddComponentsToBlueprint(Blueprint, OutputComp, Params); + } + } + + // Compile the blueprint + FKismetEditorUtilities::CompileBlueprint(Blueprint); + + // Add it to our output objects + OutputObjects.Add(Blueprint); + + return true; +} + +bool +UHoudiniGeoImporter::CreateDataTables( + const TArray& InOutputs, + FHoudiniPackageParams InPackageParams) +{ + if (InOutputs.IsEmpty()) + { + return true; + } + + for (UHoudiniOutput* const CurOutput : InOutputs) + { + check(CurOutput->GetType() == EHoudiniOutputType::DataTable); + + FString Notification = TEXT("BGEO Importer: Creating Data Tables..."); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Notification)); + + for (const FHoudiniGeoPartObject& CurHGPO : CurOutput->GetHoudiniGeoPartObjects()) + { + if (CurHGPO.Type != EHoudiniPartType::DataTable) + { + continue; + } + + // Check for a unreal_output_name if we are in bake mode + FHoudiniPackageParams PackageParams = GetPackageParamsForHGPO(CurHGPO, InPackageParams); + + if (!FHoudiniDataTableTranslator::BuildDataTable(CurHGPO, CurOutput, PackageParams)) + { + return false; + } + } + + // Add all output objects + for (auto& CurOutputPair : CurOutput->GetOutputObjects()) + { + UObject* const CurObj = CurOutputPair.Value.OutputObject; + if (!IsValid(CurObj)) + { + continue; + } + + OutputObjects.Add(CurObj); + } + } + + return true; +} + +bool +UHoudiniGeoImporter::CreateSkeletalMeshes( + const TArray& InOutputs, + FHoudiniPackageParams InPackageParams) +{ + if (InOutputs.IsEmpty()) + { + return true; + } + + FString Notification = TEXT("BGEO Importer: Creating Skeletal Meshes..."); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Notification)); + + FHoudiniPackageParams PackageParams = GetPackageParamsForType( + InOutputs, + InPackageParams, + EHoudiniOutputType::Skeletal, + EHoudiniPartType::SkeletalMeshShape); + + for (UHoudiniOutput* const CurOutput : InOutputs) + { + check(CurOutput->GetType() == EHoudiniOutputType::Skeletal); + + TMap> OutputMaterials; + UObject* const OuterComponent = nullptr; + if (!FHoudiniSkeletalMeshTranslator::ProcessSkeletalMeshOutputs(CurOutput, PackageParams, OutputMaterials, OuterComponent)) + { + return false; + } + + // Add all output objects + for (auto& CurOutputPair : CurOutput->GetOutputObjects()) + { + UObject* const CurObj = CurOutputPair.Value.OutputObject; + if (!IsValid(CurObj)) + { + continue; + } + + OutputObjects.Add(CurObj); + } + + // Do the same for materials + for (auto& CurOutputPair : OutputMaterials) + { + UObject* CurObj = CurOutputPair.Value; + if (!IsValid(CurObj)) + { + continue; + } + + OutputObjects.Add(CurObj); + } + } + + return true; +} + +bool +UHoudiniGeoImporter::CreateAnimSequences( + const TArray& InOutputs, + FHoudiniPackageParams InPackageParams) +{ + if (InOutputs.IsEmpty()) + { + return true; + } + + FString Notification = TEXT("BGEO Importer: Creating Animation Sequences..."); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Notification)); + + // Look for the first unreal_output_name attribute on the animation sequence outputs and use that + // for ObjectName + FHoudiniPackageParams PackageParams = GetPackageParamsForType( + InOutputs, + InPackageParams, + EHoudiniOutputType::AnimSequence, + EHoudiniPartType::MotionClip); + + for (UHoudiniOutput* const CurOutput : InOutputs) + { + check(CurOutput->GetType() == EHoudiniOutputType::AnimSequence); + + UObject* const OuterComponent = nullptr; + if (!FHoudiniAnimationTranslator::CreateAnimSequenceFromOutput(CurOutput, PackageParams, OuterComponent)) + { + return false; + } + + // Add all output objects + for (auto& CurOutputPair : CurOutput->GetOutputObjects()) + { + UObject* const CurObj = CurOutputPair.Value.OutputObject; + if (!IsValid(CurObj)) + { + continue; + } + + OutputObjects.Add(CurObj); + } + } + + return true; +} + +bool +UHoudiniGeoImporter::CreateCopTextures( + const TArray& InOutputs, + FHoudiniPackageParams InPackageParams) +{ + if (InOutputs.IsEmpty()) + { + return true; + } + + TArray DummyPackages; + for (UHoudiniOutput* const CurOutput : InOutputs) + { + check(CurOutput->GetType() == EHoudiniOutputType::Cop); + + FString Notification = TEXT("BGEO Importer: Creating Cop Textures..."); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Notification)); + + const TArray& GeoPartObjects = CurOutput->GetHoudiniGeoPartObjects(); + if (GeoPartObjects.Num() <= 0) + continue; + HAPI_NodeId CopNode = GeoPartObjects[0].GeoId; + + bool bRenderSuccessful = FHoudiniTextureTranslator::HapiRenderCOPTexture(CopNode); + if (bRenderSuccessful) + { + FCreateTexture2DParameters CreateTexture2DParameters; + CreateTexture2DParameters.SourceGuidHash = FGuid(); + CreateTexture2DParameters.bUseAlpha = true; + CreateTexture2DParameters.CompressionSettings = TC_Default; + CreateTexture2DParameters.bDeferCompression = true; + CreateTexture2DParameters.bSRGB = true; + + UTexture2D* Texture = nullptr; + FHoudiniTextureTranslator::CreateTexture( + CopNode, + HAPI_UNREAL_MATERIAL_TEXTURE_COLOR_ALPHA, + HAPI_IMAGE_DATA_INT8, + HAPI_IMAGE_PACKING_RGBA, + Texture, + "", + "", + InPackageParams, + CreateTexture2DParameters, + TEXTUREGROUP_World, + DummyPackages); + + OutputObjects.Add(Texture); + } + } + + return true; +} + +bool +UHoudiniGeoImporter::CreateInstancerOutputPartData( + const TArray& InOutputs, + TMap& OutInstancedOutputPartData) +{ + if (InOutputs.IsEmpty()) + { + return true; + } + + for (UHoudiniOutput* const CurOutput : InOutputs) + { + check(CurOutput->GetType() == EHoudiniOutputType::Instancer) + + for (auto& HGPO : CurOutput->GetHoudiniGeoPartObjects()) + { + FHoudiniOutputObjectIdentifier OutputIdentifier; + OutputIdentifier.ObjectId = HGPO.ObjectId; + OutputIdentifier.GeoId = HGPO.GeoId; + OutputIdentifier.PartId = HGPO.PartId; + OutputIdentifier.PartName = HGPO.PartName; + + OutInstancedOutputPartData.Add(OutputIdentifier, FHoudiniInstancerPartData()); + FHoudiniInstancerPartData *InstancedOutputData = OutInstancedOutputPartData.Find(OutputIdentifier); + // Create all the instancers and attach them to a fake outer component + *InstancedOutputData = FHoudiniInstanceTranslator::PopulateInstancedOutputPartData(HGPO, InOutputs); + } + } + + return true; +} + +bool +UHoudiniGeoImporter::DeleteCreatedNode(const HAPI_NodeId& InNodeId) +{ + if (InNodeId < 0) + return false; + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), InNodeId)) + { + // Could not delete the bgeo's file sop ! + HOUDINI_LOG_WARNING(TEXT("Houdini GEO Importer: Could not delete HAPI File SOP.")); + return false; + } + + return true; +} + +bool +UHoudiniGeoImporter::ImportBGEOFile( + const FString& InBGEOFile, UObject* InParent, const FHoudiniPackageParams* InPackageParams, + const FHoudiniStaticMeshGenerationProperties* InStaticMeshGenerationProperties, + const FMeshBuildSettings* InMeshBuildSettings) +{ + if (InBGEOFile.IsEmpty()) + return false; + + // 1. Houdini Engine Session + // See if we should/can start the default "first" HE session + if (!AutoStartHoudiniEngineSessionIfNeeded()) + return false; + + // 2. Update the file paths + if (!SetFilePath(InBGEOFile)) + return false; + + // 3. Load the BGEO file in HAPI + HAPI_NodeId NodeId; + if (!LoadBGEOFileInHAPI(NodeId)) + return false; + + // 4. Get the output from the file node + TArray> NewOutputs; + TArray> OldOutputs; + if (!BuildOutputsForNode(NodeId, OldOutputs, NewOutputs, true)) + return false; + + // Failure lambda + auto CleanUpAndReturn = [&NewOutputs](const bool& bReturnValue) + { + // Remove the output objects from the root set before returning false + for (auto Out : NewOutputs) + Out->RemoveFromRoot(); + + return bReturnValue; + }; + + // Prepare the package used for creating the mesh, landscape and instancer pacakges + FHoudiniPackageParams PackageParams; + if (InPackageParams) + { + PackageParams = *InPackageParams; + } + else + { + PackageParams.PackageMode = EPackageMode::Bake; + PackageParams.ReplaceMode = EPackageReplaceMode::ReplaceExistingAssets; + + PackageParams.BakeFolder = FPackageName::GetLongPackagePath(InParent->GetOutermost()->GetName()); + PackageParams.TempCookFolder = FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder(); + + PackageParams.HoudiniAssetName = FString(); + PackageParams.HoudiniAssetActorName = FString(); + PackageParams.ObjectName = FPaths::GetBaseFilename(InParent->GetName()); + } + + if (!PackageParams.OuterPackage) + { + PackageParams.OuterPackage = InParent; + } + + if (!PackageParams.ComponentGUID.IsValid()) + { + // TODO: will need to reuse the GUID when reimporting? + PackageParams.ComponentGUID = FGuid::NewGuid(); + } + + // 5. Create the static meshes in the outputs + const FHoudiniStaticMeshGenerationProperties& StaticMeshGenerationProperties = + InStaticMeshGenerationProperties? + *InStaticMeshGenerationProperties : + FHoudiniEngineRuntimeUtils::GetDefaultStaticMeshGenerationProperties(); + + const FMeshBuildSettings& MeshBuildSettings = + InMeshBuildSettings ? *InMeshBuildSettings : FHoudiniEngineRuntimeUtils::GetDefaultMeshBuildSettings(); + + if (!CreateObjectsFromOutputs(NewOutputs, PackageParams, StaticMeshGenerationProperties, MeshBuildSettings)) + return CleanUpAndReturn(false); + + // Clean up and return true + return CleanUpAndReturn(true); +} + +bool +UHoudiniGeoImporter::OpenBGEOFile(const FString& InBGEOFile, HAPI_NodeId& OutNodeId, bool bInUseWorldComposition) +{ + if (InBGEOFile.IsEmpty()) + return false; + + // 1. Houdini Engine Session + // See if we should/can start the default "first" HE session + if (!AutoStartHoudiniEngineSessionIfNeeded()) + return false; + + if (!FPaths::FileExists(InBGEOFile)) + { + // Cant find BGEO file + HOUDINI_LOG_ERROR(TEXT("Houdini GEO Importer: could not find file %s!"), *InBGEOFile); + return false; + } + + // Make sure we're using absolute path! + const FString AbsoluteFilePath = FPaths::ConvertRelativePathToFull(InBGEOFile); + + if (AbsoluteFilePath.IsEmpty()) + return false; + + FString AbsoluteFileDirectory; + FString FileName; + FString FileExtension; + + // Split the file path + FPaths::Split(AbsoluteFilePath, AbsoluteFileDirectory, FileName, FileExtension); + + // Handle .bgeo.sc correctly + if (FileExtension.Equals(TEXT("sc"))) + { + // append the bgeo to .sc + FileExtension = FPaths::GetExtension(FileName) + TEXT(".") + FileExtension; + // update the filename + FileName = FPaths::GetBaseFilename(FileName); + } + + if (FileExtension.IsEmpty()) + FileExtension = TEXT("bgeo"); + + if (!FileExtension.StartsWith(TEXT("bgeo"), ESearchCase::IgnoreCase)) + { + // Not a bgeo file! + HOUDINI_LOG_ERROR(TEXT("Houdini GEO Importer: File %s is not a .bgeo or .bgeo.sc file!"), *InBGEOFile); + return false; + } + + OutNodeId = -1; + + // Check HoudiniEngine / HAPI init? + if (!FHoudiniEngine::IsInitialized()) + { + HOUDINI_LOG_ERROR(TEXT("Couldn't initialize HoudiniEngine!")); + return false; + } + + FString Notification = TEXT("BGEO Importer: Loading bgeo file..."); + FHoudiniEngine::Get().CreateTaskSlateNotification(FText::FromString(Notification), true); + + // Create a file SOP + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + -1, "SOP/file", "bgeo", true, &OutNodeId), false); + + /* + // Set the file path parameter + HAPI_ParmId ParmId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), + OutNodeId, "file", &ParmId), false); + + const std::string ConvertedString = H_TCHAR_TO_UTF8(*AbsoluteFilePath); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue( + FHoudiniEngine::Get().GetSession(), OutNodeId, ConvertedString.c_str(), ParmId, 0), false); + */ + + // Simply use LoadGeoFrom file + std::string ConvertedString = H_TCHAR_TO_UTF8(*AbsoluteFilePath); + FHoudiniApi::LoadGeoFromFile(FHoudiniEngine::Get().GetSession(), OutNodeId, ConvertedString.c_str()); + + return true; +} + +bool +UHoudiniGeoImporter::MergeGeoFromNode(const FString& InNodePath, HAPI_NodeId& OutNodeId) +{ + if (InNodePath.IsEmpty()) + return false; + + // 1. Houdini Engine Session + // See if we should/can start the default "first" HE session + if (!AutoStartHoudiniEngineSessionIfNeeded()) + return false; + + // TODO + // Check that InNodePath is valid + OutNodeId = -1; + + // Check HoudiniEngine / HAPI init? + if (!FHoudiniEngine::IsInitialized()) + { + HOUDINI_LOG_ERROR(TEXT("Couldn't initialize HoudiniEngine!")); + return false; + } + + FString Notification = TEXT("Merging node data..."); + FHoudiniEngine::Get().CreateTaskSlateNotification(FText::FromString(Notification), true); + + // Create an object merge SOP + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + -1, "SOP/object_merge", "NodeSyncFetch", true, &OutNodeId), false); + + // Set the object path parameter + HAPI_ParmId ParmId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), + OutNodeId, "objpath1", &ParmId), false); + + const std::string ConvertedString = H_TCHAR_TO_UTF8(*InNodePath); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue( + FHoudiniEngine::Get().GetSession(), OutNodeId, ConvertedString.c_str(), ParmId, 0), false); + + // Cook the node + HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + if(!FHoudiniEngineUtils::HapiCookNode(OutNodeId, &CookOptions, true)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to cook NodeSyncFetch node!")); + return false; + } + + return true; +} + + +bool +UHoudiniGeoImporter::CloseBGEOFile(const HAPI_NodeId& InNodeId) +{ + // 8. Delete the created node in Houdini + if (!DeleteCreatedNode(InNodeId)) + return false; + + return true; +} + +bool +UHoudiniGeoImporter::LoadBGEOFileInHAPI(HAPI_NodeId& NodeId) +{ + NodeId = -1; + + if (AbsoluteFilePath.IsEmpty()) + return false; + + // Check HoudiniEngine / HAPI init? + if (!FHoudiniEngine::IsInitialized()) + { + HOUDINI_LOG_ERROR(TEXT("Couldn't initialize HoudiniEngine!")); + return false; + } + + FString Notification = TEXT("BGEO Importer: Loading bgeo file..."); + FHoudiniEngine::Get().CreateTaskSlateNotification(FText::FromString(Notification), true); + + // Create a file SOP + // We still create a file SOP as we need a Node that LoadGeoFromFile can use to store the data on + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + -1, "SOP/file", "bgeo", true, &NodeId), false); + + /* + // Set the file path parameter + HAPI_ParmId ParmId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), + NodeId, "file", &ParmId), false); + + std::string ConvertedString = H_TCHAR_TO_UTF8(*AbsoluteFilePath); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue( + FHoudiniEngine::Get().GetSession(), NodeId, ConvertedString.c_str(), ParmId, 0), false); + */ + + // Simply use LoadGeoFrom file + std::string ConvertedString = H_TCHAR_TO_UTF8(*AbsoluteFilePath); + FHoudiniApi::LoadGeoFromFile(FHoudiniEngine::Get().GetSession(), NodeId, ConvertedString.c_str()); + + return CookFileNode(NodeId); +} + +bool +UHoudiniGeoImporter::CookFileNode(const HAPI_NodeId& InNodeId) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(UHoudiniGeoImporter::CookFileNode); + + // Cook the node + HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CookNode( + FHoudiniEngine::Get().GetSession(), InNodeId, &CookOptions), false); + + // Wait for the cook to finish + int32 status = HAPI_STATE_MAX_READY_STATE + 1; + while (status > HAPI_STATE_MAX_READY_STATE) + { + // Retrieve the status + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetStatus( + FHoudiniEngine::Get().GetSession(), + HAPI_STATUS_COOK_STATE, &status), false); + + HOUDINI_LOG_MESSAGE(TEXT("Still Cooking, current status: %s."), *FHoudiniEngineUtils::GetStatusString(HAPI_STATUS_COOK_STATE, HAPI_STATUSVERBOSITY_ERRORS)); + + // Go to bed.. + if (status > HAPI_STATE_MAX_READY_STATE) + FPlatformProcess::Sleep(0.5f); + } + + if (status != HAPI_STATE_READY) + { + // There was some cook errors + HOUDINI_LOG_ERROR(TEXT("Finished Cooking with errors!")); + return false; + } + + HOUDINI_LOG_MESSAGE(TEXT("Finished Cooking!")); + + return true; +} + +bool +UHoudiniGeoImporter::BuildAllOutputsForNode( + const HAPI_NodeId& InNodeId, + UObject* InOuter, + TArray>& InOldOutputs, + TArray>& OutNewOutputs, + bool bInAddOutputsToRootSet, + bool bInUseOutputNodes, + bool bGatherEditableCurves, + bool bCreateSceneComponents) +{ + bool bOutputTemplateGeos = false; + + // Gather output nodes for the HAC + TArray OutputNodes; + FHoudiniEngineUtils::GatherAllAssetOutputs(InNodeId, bInUseOutputNodes, bOutputTemplateGeos, bGatherEditableCurves, OutputNodes); + + // TArray OldOutputs; + TMap OutputNodeCookCount; + if (!FHoudiniOutputTranslator::BuildAllOutputs(InNodeId, InOuter, OutputNodes, OutputNodeCookCount, InOldOutputs, OutNewOutputs, bOutputTemplateGeos, bInUseOutputNodes, bGatherEditableCurves, bCreateSceneComponents)) + { + // Couldn't create the package + HOUDINI_LOG_ERROR(TEXT("Houdini GEO Importer: Failed to process the File SOP's outputs!")); + return false; + } + + if (bInAddOutputsToRootSet) + { + // Add the output objects to the RootSet to prevent them from being GCed + for (auto& Out : OutNewOutputs) + Out->AddToRoot(); + } + + return true; +} + +FHoudiniPackageParams +UHoudiniGeoImporter::GetPackageParamsForType( + const TArray& InOutputs, + FHoudiniPackageParams InPackageParams, + const EHoudiniOutputType InOutputType, + const EHoudiniPartType InPartType) +{ + // Look for the first unreal_output_name attribute for the desired output + // type and use that for ObjectName + for (const UHoudiniOutput* const CurOutput : InOutputs) + { + if (CurOutput->GetType() != InOutputType) + { + continue; + } + + bool bFoundOutputName = false; + bool bFoundBakeFolder = InPackageParams.PackageMode != EPackageMode::Bake; + bool bFoundTempFolder = false; + for (const FHoudiniGeoPartObject& HGPO : CurOutput->GetHoudiniGeoPartObjects()) + { + if (HGPO.Type != InPartType) + { + continue; + } + + if (!bFoundOutputName) + { + FString OutputName; + if (FHoudiniEngineUtils::GetOutputNameAttribute(HGPO.GeoId, HGPO.PartId, OutputName)) + { + if (!OutputName.IsEmpty()) + { + InPackageParams.ObjectName = OutputName; + bFoundOutputName = true; + } + } + } + + if (!bFoundTempFolder) + { + FString TempFolder; + if (FHoudiniEngineUtils::GetTempFolderAttribute(HGPO.GeoId, TempFolder, HGPO.PartId)) + { + if (!TempFolder.IsEmpty()) + { + InPackageParams.TempCookFolder = TempFolder; + bFoundTempFolder = true; + } + } + } + + if (!bFoundBakeFolder) + { + FString BakeFolder; + if (FHoudiniEngineUtils::GetBakeFolderAttribute(HGPO.GeoId, HGPO.PartId, BakeFolder)) + { + if (!BakeFolder.IsEmpty()) + { + InPackageParams.BakeFolder = BakeFolder; + bFoundBakeFolder = true; + } + } + } + + if (bFoundOutputName && bFoundBakeFolder && bFoundTempFolder) + { + return InPackageParams; + } + } + } + + return InPackageParams; +} + +FHoudiniPackageParams +UHoudiniGeoImporter::GetPackageParamsForHGPO( + const FHoudiniGeoPartObject& InHGPO, + FHoudiniPackageParams InPackageParams) +{ + // Check for a unreal_output_name if we are in bake mode + if (InPackageParams.PackageMode == EPackageMode::Bake) + { + FString OutputName; + if (FHoudiniEngineUtils::GetOutputNameAttribute(InHGPO.GeoId, InHGPO.PartId, OutputName)) + { + if (!OutputName.IsEmpty()) + { + InPackageParams.ObjectName = OutputName; + } + } + // Could have prim attribute unreal_bake_folder override + FString BakeFolder; + if (FHoudiniEngineUtils::GetBakeFolderAttribute(InHGPO.GeoId, InHGPO.PartId, BakeFolder)) + { + if (!BakeFolder.IsEmpty()) + { + InPackageParams.BakeFolder = BakeFolder; + } + } + } + return InPackageParams; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniGeoImporter.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" + +#include "HoudiniGeoImporter.generated.h" + +class UHoudiniOutput; + +struct FHoudiniPackageParams; +struct FHoudiniStaticMeshGenerationProperties; +struct FHoudiniGeoPartObject; +struct FMeshBuildSettings; +struct FHoudiniOutputObjectIdentifier; +struct FHoudiniInstancerPartData; + +enum class EHoudiniOutputType : uint8; +enum class EHoudiniPartType : uint8; + +UCLASS() +class HOUDINIENGINE_API UHoudiniGeoImporter : public UObject +{ + GENERATED_UCLASS_BODY() + +public: + + void SetBakeRootFolder(const FString& InFolder) { BakeRootFolder = InFolder; }; + void SetOutputFilename(const FString& InOutFilename) { OutputFilename = InOutFilename; }; + + TArray& GetOutputObjects() { return OutputObjects; }; + + // BEGIN: Static API + // Open a BGEO file: create a file node in HAPI and cook it + static bool OpenBGEOFile(const FString& InBGEOFile, HAPI_NodeId& OutNodeId, bool bInUseWorldComposition=false); + + // Cook the file node specified by the valid NodeId. + static bool CookFileNode(const HAPI_NodeId& InNodeId); + + // Extract the outputs for a given node ID + static bool BuildAllOutputsForNode( + const HAPI_NodeId& InNodeId, + UObject* InOuter, + TArray>& InOldOutputs, + TArray>& OutNewOutputs, + bool bInAddOutputsToRootSet = false, + bool bInUseOutputNodes = true, + bool bGatherEditableCurves = true, + bool bCreateSceneComponents = true); + + // Delete the HAPI node and remove InOutputs from the root set. + static bool CloseBGEOFile(const HAPI_NodeId& InNodeId); + // END: Static API + + // Import the BGEO file + bool ImportBGEOFile( + const FString& InBGEOFile, + UObject* InParent, + const FHoudiniPackageParams* InPackageParams=nullptr, + const FHoudiniStaticMeshGenerationProperties* InStaticMeshGenerationProperties=nullptr, + const FMeshBuildSettings* InMeshBuildSettings=nullptr); + + // 1. Start a HE session if needed + static bool AutoStartHoudiniEngineSessionIfNeeded(); + + // 2. Update our file members fromn the input file path + bool SetFilePath(const FString& InFilePath); + + // 3. Creates a new file node and loads the bgeo file in HAPI + bool LoadBGEOFileInHAPI(HAPI_NodeId& NodeId); + + // 3.2 (alternative) Uses an object merge node to load the geo data in HAPI (used for node sync fetch) + bool MergeGeoFromNode(const FString& InNodePath, HAPI_NodeId& OutNodeId); + + // 4. Extract the outputs for a given node ID + bool BuildOutputsForNode( + const HAPI_NodeId& InNodeId, + TArray>& InOldOutputs, + TArray>& OutNewOutputs, + bool bInUseOutputNodes = true); + + // 5. Create objects using outputs + bool CreateObjectsFromOutputs( + TArray>& InOutputs, + FHoudiniPackageParams InPackageParams, + const FHoudiniStaticMeshGenerationProperties& InStaticMeshGenerationProperties, + const FMeshBuildSettings& InMeshBuildSettings, + TMap* OutInstancedOutputPartData = nullptr); + + // 6. Clean up the created node + static bool DeleteCreatedNode(const HAPI_NodeId& InNodeId); + +private: + + static FHoudiniPackageParams GetPackageParamsForType( + const TArray& InOutputs, + FHoudiniPackageParams InPackageParams, + const EHoudiniOutputType InOutputType, + const EHoudiniPartType InPartType); + + static FHoudiniPackageParams GetPackageParamsForHGPO( + const FHoudiniGeoPartObject& InHGPO, + FHoudiniPackageParams InPackageParams); + + /** @param InOutputs Must all have type EHoudiniOutput::Mesh. */ + bool CreateStaticMeshes( + const TArray& InOutputs, + FHoudiniPackageParams InPackageParams, + const FHoudiniStaticMeshGenerationProperties& InStaticMeshGenerationProperties, + const FMeshBuildSettings& InMeshBuildSettings); + + /** @param InOutputs Must all have type EHoudiniOutput::Curve. */ + bool CreateCurves(const TArray& InOutputs, FHoudiniPackageParams InPackageParams); + + /** @param InOutputs Must all have type EHoudiniOutput::Landscape. */ + bool CreateLandscapes(const TArray& InOutputs, FHoudiniPackageParams InPackageParams); + + /** @param InOutputs Must all have type EHoudiniOutput::LandscapeSpline. */ + bool CreateLandscapeSplines(const TArray& InOutputs, FHoudiniPackageParams InPackageParams); + + /** @param InInstancerOutputs Must all have type EHoudiniOutput::Instancer. */ + bool CreateInstancers( + TArray>& InAllOutputs, + const TArray& InInstancerOutputs, + FHoudiniPackageParams InPackageParams); + + /** @param InOutputs Must all have type EHoudiniOutput::Instancer. */ + static bool CreateInstancerOutputPartData( + const TArray& InOutputs, + TMap& OutInstancedOutputPartData); + + /** @param InOutputs Must all have type EHoudiniOutput::DataTable. */ + bool CreateDataTables(const TArray& InOutputs, FHoudiniPackageParams InPackageParams); + + /** @param InOutputs Must all have type EHoudiniOutput::Skeletal. */ + bool CreateSkeletalMeshes(const TArray& InOutputs, FHoudiniPackageParams InPackageParams); + + /** @param InOutputs Must all have type EHoudiniOutput::AnimSequence. */ + bool CreateAnimSequences(const TArray& InOutputs, FHoudiniPackageParams InPackageParams); + + /** @param InOutputs Must all have type EHoudiniOutput::Cop. */ + bool CreateCopTextures(const TArray& InOutputs, FHoudiniPackageParams InPackageParams); + + // Path to the file we're currently loading + FString SourceFilePath; + + // Absolute Path to the file + FString AbsoluteFilePath; + FString AbsoluteFileDirectory; + + // Input File Name / Extension + FString FileName; + FString FileExtension; + + // Output filename, if empty, will be set to the input filename + FString OutputFilename; + // Root Folder for storing the created files + FString BakeRootFolder; + + // Output Objects + TArray OutputObjects; +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniGeometryCollectionTranslator.cpp`: + +```cpp +#include "HoudiniGeometryCollectionTranslator.h" + +#include "HoudiniApi.h" +#include "HoudiniCookable.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniEngineString.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniInstanceTranslator.h" +#include "HoudiniMeshTranslator.h" + +#include "StaticMeshAttributes.h" +#include "StaticMeshOperations.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "GeometryCollection/GeometryCollection.h" +#include "GeometryCollection/GeometryCollectionClusteringUtility.h" +#include "GeometryCollection/GeometryCollectionComponent.h" +#include "GeometryCollection/GeometryCollectionObject.h" +#include "GeometryCollection/GeometryCollectionActor.h" +#include "GeometryCollection/GeometryCollectionConvexUtility.h" +#include "GeometryCollection/GeometryCollectionEngineSizeSpecificUtility.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "MaterialDomain.h" +#endif +#include "HoudiniEngineAttributes.h" +#include "Animation/AnimRootMotionProvider.h" +#include "Materials/Material.h" +#include "UObject/UObjectIterator.h" + +void +FHoudiniGeometryCollectionTranslator::SetupGeometryCollectionComponentFromOutputs( + TArray>& InAllOutputs, + UObject* InOutputParent, // Probably a cookable, but not for GeoImporter. + UObject* InOuterComponent, + const FHoudiniPackageParams& InPackageParams, + UWorld * InWorld) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniGeometryCollectionTranslator::SetupGeometryCollectionComponentFromOutputs); + + USceneComponent* ParentComponent = Cast(InOuterComponent); + if (!ParentComponent) + return; + + TMap GeometryCollectionData; + GetGeometryCollectionData(InAllOutputs, InPackageParams, GeometryCollectionData); + + for (auto & Pair : GeometryCollectionData) + { + FString& GCName = Pair.Key; + FHoudiniGeometryCollectionData& GCData = Pair.Value; + + // Find or create new UHoudiniOutput + UHoudiniOutput * HoudiniOutput = nullptr; + bool bNewOutput = true; + + // Find UHoudiniOutput + for (auto& Output : InAllOutputs) + { + if (Output->Type != EHoudiniOutputType::GeometryCollection) + { + continue; + } + + if (!Output->GetOutputObjects().Contains(GCData.Identifier)) + { + continue; + } + + HoudiniOutput = Output; + bNewOutput = false; + break; + } + + if (!HoudiniOutput) + { + // Create the actual HoudiniOutput. + HoudiniOutput = NewObject( + InOutputParent, + UHoudiniOutput::StaticClass(), + FName(*GCName), // Used for baking identification + RF_NoFlags); + + // Make sure the created object is valid + if (!IsValid(HoudiniOutput)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to create asset output")); + return; + } + + HoudiniOutput->Type = EHoudiniOutputType::GeometryCollection; + InAllOutputs.Add(HoudiniOutput); + } + + FHoudiniOutputObject& OutputObject = HoudiniOutput->OutputObjects.FindOrAdd(GCData.Identifier); + + TArray& GeometryCollectionPieces = GCData.GeometryCollectionPieces; + if (GeometryCollectionPieces.Num() <= 0) + { + return; + } + + UGeometryCollection* GeometryCollection = GCData.PackParams.CreateObjectAndPackage(); + if (!IsValid(GeometryCollection)) + return; + + // Ensure we have a SizeSpecificData + if (!GeometryCollection->SizeSpecificData.Num()) + GeometryCollection->SizeSpecificData.Add(FGeometryCollectionSizeSpecificData()); + + UGeometryCollectionComponent* GeometryCollectionComponent = nullptr; + FTransform ActorTransform; + if (IsValid(ParentComponent->GetOwner())) + { + FString AssetName = ParentComponent->GetOwner()->GetName() + "_" + GCName; + FString ActorName = AssetName + "_Actor"; + + AGeometryCollectionActor* GeometryCollectionActor = Cast(OutputObject.OutputObject); + + // Initialize GC, GC Actor, GC Component + FTransform AssetTransform = ParentComponent->GetOwner()->GetTransform(); + + if (!GeometryCollectionActor) + GeometryCollectionActor = CreateNewGeometryActor(InWorld, ActorName, AssetTransform); + + if (!IsValid(GeometryCollectionActor)) + return; + + GeometryCollectionComponent = GeometryCollectionActor->GetGeometryCollectionComponent(); + if (!IsValid(GeometryCollectionComponent)) + return; + + GeometryCollectionActor->GetGeometryCollectionComponent()->SetRestCollection(GeometryCollection); + + if (IsValid(ParentComponent)) + { + GeometryCollectionActor->AttachToComponent(ParentComponent, FAttachmentTransformRules::KeepWorldTransform); + } + + ActorTransform = ParentComponent->GetOwner()->GetTransform(); + } + + // Mark relevant stuff dirty + FAssetRegistryModule::AssetCreated(GeometryCollection); + GeometryCollection->MarkPackageDirty(); + + UPackage *OuterPackage = GeometryCollection->GetOutermost(); + if (IsValid(OuterPackage)) + { + OuterPackage->MarkPackageDirty(); + } + + // Used to get the number of levels + // Pair of + TMap, TArray> Clusters; + + // Append the static meshes build from instancers to the UGeometryCollection, destroying the StaticMeshComponents as you go + // Kind of similar to UFractureToolGenerateAsset::ConvertStaticMeshToGeometryCollection + for (auto & GeometryCollectionPiece : GeometryCollectionPieces) + { + for(auto Component : GeometryCollectionPiece.InstancerOutput->OutputComponents) + { + if (!GeometryCollectionPiece.InstancerOutput || !IsValid(Component)) + continue; + + if (!Component->IsA(UStaticMeshComponent::StaticClass())) + continue; + + TPair ClusterKey = TPair(GeometryCollectionPiece.FractureIndex, GeometryCollectionPiece.ClusterIndex); + TArray & Cluster = Clusters.FindOrAdd(ClusterKey); + Cluster.Add(&GeometryCollectionPiece); + + UObject * OldComponent = Component; + + UStaticMeshComponent * StaticMeshComponent = Cast(Component); + UStaticMesh * ComponentStaticMesh = StaticMeshComponent->GetStaticMesh(); + FTransform ComponentTransform(StaticMeshComponent->GetComponentTransform()); + ComponentTransform.SetTranslation(ComponentTransform.GetTranslation() - ActorTransform.GetTranslation()); + FSoftObjectPath SourceSoftObjectPath(ComponentStaticMesh); + decltype(FGeometryCollectionSource::SourceMaterial) SourceMaterials(StaticMeshComponent->GetMaterials()); + GeometryCollection->GeometrySource.Add({ SourceSoftObjectPath, ComponentTransform, SourceMaterials }); + + FHoudiniGeometryCollectionTranslator::AppendStaticMesh(ComponentStaticMesh, SourceMaterials, ComponentTransform, GeometryCollection, true); + + RemoveAndDestroyComponent(OldComponent); + + // Sets the GeometryIndex, to identify which this piece is when dealing with the geometry collection + int32 GeometryIndex = GeometryCollection->NumElements(FGeometryCollection::TransformGroup) - 1; + GeometryCollectionPiece.GeometryIndex = GeometryIndex; + } + GeometryCollectionPiece.InstancerOutput->OutputComponents.Empty(); + } + + GeometryCollection->InitializeMaterials(); + + // Adds a singular root node to all of the meshes. This automatically makes Level 0 = 1, and Level 1 = the rest of your meshs. + AddSingleRootNodeIfRequired(GeometryCollection); + + for (auto & Cluster : Clusters) + { + int32 FractureIndex = Cluster.Key.Key; + int32 ClusterIndex = Cluster.Key.Value; + + if (FractureIndex <= 1) + { + // If level is 1, then no need to bump up the levels + continue; + } + + // Getting the minimum insertion index to figure out the parent/transform of the new bone. + int32 MinInsertionPoint = INT_MAX; + + TArray BoneIndices; + BoneIndices.Empty(); + for (auto & Piece : Cluster.Value) + { + if (Piece == nullptr) continue; + + BoneIndices.Add(Piece->GeometryIndex); + MinInsertionPoint = std::min(MinInsertionPoint, Piece->GeometryIndex); + } + + + for (int32 i = 0; i < FractureIndex - 1; i++) + { + TSharedPtr GeometryCollectionPtr = GeometryCollection->GetGeometryCollection(); + if (FGeometryCollection* GeometryCollectionObj = GeometryCollectionPtr.Get()) + { + FGeometryCollectionClusteringUtility::ClusterBonesUnderNewNode(GeometryCollectionObj, MinInsertionPoint, BoneIndices, false); + } + } + } + + // One-of detail attributes, so only need to take the first GeometryCollectionPiece + FHoudiniGeometryCollectionPiece& FirstPiece = GeometryCollectionPieces[0]; + + // Apply all geometry collection attributes that are static across all pieces. + ApplyGeometryCollectionAttributes(GeometryCollection, FirstPiece); + + // Set output object + OutputObject.OutputObject = GeometryCollection; + check(OutputObject.OutputComponents.Num() < 2); // Multiple components not supported yet. + OutputObject.OutputComponents.Empty(); + if (IsValid(GeometryCollectionComponent)) + { + OutputObject.OutputActors.Add(GeometryCollectionComponent->GetOwner()); + } + + FString BakeFolder; + if (FHoudiniEngineUtils::GetBakeFolderAttribute(GCData.Identifier.GeoId, GCData.Identifier.PartId, BakeFolder)) + OutputObject.CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_FOLDER, BakeFolder); + + // See if we need to force the generation of convex hull data for the GC + if (GeometryCollection::SizeSpecific::UsesImplicitCollisionType(GeometryCollection->SizeSpecificData, EImplicitTypeEnum::Chaos_Implicit_Convex) + && !FGeometryCollectionConvexUtility::HasConvexHullData(GeometryCollection->GetGeometryCollection().Get())) + { + GeometryCollection::SizeSpecific::SetImplicitCollisionType(GeometryCollection->SizeSpecificData, EImplicitTypeEnum::Chaos_Implicit_Box, EImplicitTypeEnum::Chaos_Implicit_Convex); + GeometryCollection->CreateSimulationData(); + } + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + // TODO: Improve me! + // Somehow, in 5.4 - the Component doesnt seem to update properly if we set the GC too early + // (which is what we used to do) - resetting the whole GC as temporary fix + // New: In addition to the above the Thumbnail generator creates its own components and if we + // don't update them - crash! So just go thru every component that uses this Geometry Collection + + for(TObjectIterator It(RF_ClassDefaultObject, false, EInternalObjectFlags::Garbage); It; ++It) + { + if(It->RestCollection == GeometryCollection) + { + It->SetRestCollection(GeometryCollection); + } + } +#endif + + if (IsValid(GeometryCollectionComponent)) + { + // See if we need to update Custom Prim Data on the GCC + TArray CustomPrimData; + FHoudiniMeshTranslator::GetCustomPrimitiveData( + GCData.Identifier.GeoId, + GCData.Identifier.PartId, + GCData.Identifier.PrimitiveIndex, + CustomPrimData); + + if (!CustomPrimData.IsEmpty()) + { + FHoudiniMeshTranslator::SetCustomPrimitiveData( + CustomPrimData, GeometryCollectionComponent); + } + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + // Rebuild render data on the GeometryCollection itself otherwise the asset won't update in UE5.3 + GeometryCollection->RebuildRenderData(); +#endif + + // Mark the render state dirty otherwise it won't appear until you move it + GeometryCollectionComponent->MarkRenderStateDirty(); + } + } +} + + +UGeometryCollectionComponent* +FHoudiniGeometryCollectionTranslator::CreateGeometryCollectionComponent(UObject *InOuterComponent) +{ + // Create a new SMC as we couldn't find an existing one + USceneComponent* OuterSceneComponent = Cast(InOuterComponent); + UObject * Outer = nullptr; + if (IsValid(OuterSceneComponent)) + Outer = OuterSceneComponent->GetOwner() ? OuterSceneComponent->GetOwner() : OuterSceneComponent->GetOuter(); + + UGeometryCollectionComponent *GeometryCollectionComponent = NewObject(Outer, UGeometryCollectionComponent::StaticClass(), NAME_None, RF_Transactional); + + // Initialize it + GeometryCollectionComponent->SetVisibility(true); + + // Change the creation method so the component is listed in the details panels + FHoudiniEngineRuntimeUtils::AddOrSetAsInstanceComponent(GeometryCollectionComponent); + + // Attach created static mesh component to our Houdini component. + GeometryCollectionComponent->AttachToComponent(OuterSceneComponent, FAttachmentTransformRules::KeepRelativeTransform); + GeometryCollectionComponent->OnComponentCreated(); + GeometryCollectionComponent->RegisterComponent(); + + return GeometryCollectionComponent; +} + + +bool +FHoudiniGeometryCollectionTranslator::RemoveAndDestroyComponent(UObject* InComponent) +{ + if (!IsValid(InComponent)) + return false; + + USceneComponent* SceneComponent = Cast(InComponent); + if (IsValid(SceneComponent)) + { + // Remove from the HoudiniAssetActor + if (SceneComponent->GetOwner()) + SceneComponent->GetOwner()->RemoveOwnedComponent(SceneComponent); + + SceneComponent->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + SceneComponent->UnregisterComponent(); + SceneComponent->DestroyComponent(); + + return true; + } + + return false; +} + + +bool +FHoudiniGeometryCollectionTranslator::GetGeometryCollectionData( + const TArray& InAllOutputs, + const FHoudiniPackageParams& InPackageParams, + TMap& OutGeometryCollectionData) +{ + for (auto & HoudiniOutput : InAllOutputs) + { + if (!IsGeometryCollectionInstancer(HoudiniOutput)) + { + continue; + } + + FHoudiniGeometryCollectionPiece NewPiece; + + for (auto & Pair : HoudiniOutput->OutputObjects) + { + NewPiece.InstancerOutputIdentifier = &(Pair.Key); + NewPiece.InstancerOutput = &(Pair.Value); + + int GeoId = NewPiece.InstancerOutputIdentifier->GeoId; + int PartId = NewPiece.InstancerOutputIdentifier->PartId; + + // Assume that there is only one part per instance. This is always true for now but may need to be looked at later. + int NumInstancedParts = 1; + + TArray InstancedPartIds; + InstancedPartIds.SetNumZeroed(NumInstancedParts); + if (FHoudiniApi::GetInstancedPartIds( + FHoudiniEngine::Get().GetSession(), GeoId, PartId, + InstancedPartIds.GetData(), 0, NumInstancedParts) != HAPI_RESULT_SUCCESS) + { + return false; + } + + if (InstancedPartIds.Num() > 0) + { + NewPiece.InstancedPartId = InstancedPartIds[0]; + } + } + + GetFracturePieceAttribute(NewPiece.InstancerOutputIdentifier->GeoId, NewPiece.InstancedPartId, NewPiece.FractureIndex); + GetClusterPieceAttribute(NewPiece.InstancerOutputIdentifier->GeoId, NewPiece.InstancedPartId, NewPiece.ClusterIndex); + GetGeometryCollectionNameAttribute(NewPiece.InstancerOutputIdentifier->GeoId, NewPiece.InstancedPartId, NewPiece.GeometryCollectionName); + + FHoudiniGeometryCollectionData * GeometryCollectionData = nullptr; + + if (!OutGeometryCollectionData.Contains(NewPiece.GeometryCollectionName)) + { + FHoudiniGeometryCollectionData & NewData = OutGeometryCollectionData.Add(NewPiece.GeometryCollectionName, FHoudiniGeometryCollectionData(*NewPiece.InstancerOutputIdentifier, InPackageParams)); + GeometryCollectionData = &NewData; + } + else + { + GeometryCollectionData = &OutGeometryCollectionData[NewPiece.GeometryCollectionName]; + } + + // Add _GM suffix to the split str, to distinguish GeometryCollections from StaticMeshes + // Additionally add the name to distinguish it from other GC outputs + + TArray OutputNames; + if (!HoudiniOutput->GetHoudiniGeoPartObjects().IsEmpty()) + { + auto& HGPO = HoudiniOutput->GetHoudiniGeoPartObjects()[0]; + FHoudiniEngineUtils::GetOutputNameAttribute(HGPO.GeoId, HGPO.PartId, OutputNames, 0, 1); + } + + if (!OutputNames.IsEmpty() && !OutputNames[0].IsEmpty()) + { + GeometryCollectionData->PackParams.ObjectName = OutputNames[0]; + GeometryCollectionData->PackParams.SplitStr.Empty(); + } + else + { + GeometryCollectionData->PackParams.ObjectName.Empty(); + GeometryCollectionData->PackParams.SplitStr = NewPiece.GeometryCollectionName + "_GC"; + } + + GeometryCollectionData->Identifier.SplitIdentifier = GeometryCollectionData->PackParams.SplitStr; + + GeometryCollectionData->GeometryCollectionPieces.Add(NewPiece); + } + + return OutGeometryCollectionData.Num() > 0; +} + + +AGeometryCollectionActor* +FHoudiniGeometryCollectionTranslator::CreateNewGeometryActor( + UWorld * InWorld, + const FString& InActorName, + const FTransform& InTransform) +{ + // Create the new Geometry Collection actor + FActorSpawnParameters SpawnParameters; + SpawnParameters.Name = MakeUniqueObjectName(InWorld->GetCurrentLevel(), AGeometryCollectionActor::StaticClass(), FName(InActorName)); + // SpawnParameters.Name = FName(InActorName); + + AGeometryCollectionActor* NewActor = InWorld->SpawnActor(SpawnParameters); + check(NewActor->GetGeometryCollectionComponent()); + + // Set the Geometry Collection asset in the new actor + + // copy transform of original static mesh actor to this new actor + NewActor->SetActorLabel(InActorName); + // Only copy position, or else we might end up with double rotations afterwards + // if the parent Actor is rotated as well. + NewActor->SetActorLocation(InTransform.GetLocation()); + //NewActor->SetActorTransform(InTransform); + + return NewActor; +} + + +bool +FHoudiniGeometryCollectionTranslator::GetGeometryCollectionNames( + TArray>& InAllOutputs, + TSet& Names) +{ + for (auto & HoudiniOutput : InAllOutputs) + { + if (!IsGeometryCollectionInstancer(HoudiniOutput)) + { + continue; + } + + int32 InstancedPartId = -1; + + int GeoId = -1; + int PartId = -1; + + for (auto & Pair : HoudiniOutput->OutputObjects) + { + GeoId = Pair.Key.GeoId; + PartId = Pair.Key.PartId; + + // Assume that there is only one part per instance. This is always true for now but may need to be looked at later. + int NumInstancedParts = 1; + + TArray InstancedPartIds; + InstancedPartIds.SetNumZeroed(NumInstancedParts); + if (FHoudiniApi::GetInstancedPartIds( + FHoudiniEngine::Get().GetSession(), GeoId, PartId, + InstancedPartIds.GetData(), 0, NumInstancedParts) != HAPI_RESULT_SUCCESS) + { + return false; + } + + if (InstancedPartIds.Num() > 0) + { + InstancedPartId = InstancedPartIds[0]; + } + } + + FString GCName; + GetGeometryCollectionNameAttribute(GeoId, InstancedPartId, GCName); + + Names.Add(GCName); + } + + return true; +} + +bool +FHoudiniGeometryCollectionTranslator::GetFracturePieceAttribute( + const HAPI_NodeId& GeoId, + const HAPI_NodeId& PartId, + int32& OutInt) +{ + bool HasFractureAttribute = false; + + TArray IntData; + IntData.Empty(); + + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_GC_PIECE); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, IntData); + + if (bSuccess) + { + if (IntData.Num() > 0) + { + HasFractureAttribute = true; + OutInt = IntData[0]; + } + } + + return HasFractureAttribute; +} + +bool +FHoudiniGeometryCollectionTranslator::GetClusterPieceAttribute( + const HAPI_NodeId& GeoId, + const HAPI_NodeId& PartId, + int& OutInt) +{ + bool HasClusterAttribute = false; + TArray IntData; + IntData.Empty(); + + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_GC_CLUSTER_PIECE); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, IntData); + + if (bSuccess) + { + if (IntData.Num() > 0) + { + HasClusterAttribute = true; + OutInt = IntData[0]; + } + + } + + if (!HasClusterAttribute) OutInt = -1; // Cluster piece can be null + + return HasClusterAttribute; +} + +bool +FHoudiniGeometryCollectionTranslator::GetGeometryCollectionNameAttribute( + const HAPI_NodeId& GeoId, + const HAPI_NodeId& PartId, + FString& OutName) +{ + bool HasAttribute = false; + TArray StrData; + StrData.Empty(); + + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_GC_NAME); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, StrData); + + if (bSuccess && StrData.Num() > 0) + { + HasAttribute = true; + OutName = StrData[0]; + } + + if (!HasAttribute) + { + OutName = TEXT(""); + } + + return HasAttribute; +} + +bool +FHoudiniGeometryCollectionTranslator::IsGeometryCollectionInstancer( + const UHoudiniOutput* HoudiniOutput) +{ + if (HoudiniOutput == nullptr) + { + return false; + } + + EHoudiniOutputType OutputType = HoudiniOutput->Type; + if (OutputType != EHoudiniOutputType::Instancer) + { + return false; + } + + // Check geoparts type so we don't have to get a Houdini session to call this function if not necessary + for (auto & GeoPart : HoudiniOutput->GetHoudiniGeoPartObjects()) + { + if (GeoPart.InstancerType == EHoudiniInstancerType::GeometryCollection) + { + return true; + } + } + + int InstancerGeoId = -1; + int InstancerPartId = -1; + + for (auto & Pair : HoudiniOutput->OutputObjects) + { + InstancerGeoId = Pair.Key.GeoId; + InstancerPartId = Pair.Key.PartId; + } + + if (InstancerGeoId == -1 || InstancerPartId == -1) + { + return false; + } + + if (!IsGeometryCollectionInstancerPart(InstancerGeoId, InstancerPartId)) + { + return false; + } + + return true; +} + +bool +FHoudiniGeometryCollectionTranslator::IsGeometryCollectionMesh( + const UHoudiniOutput* HoudiniOutput) +{ + EHoudiniOutputType OutputType = HoudiniOutput->Type; + if (OutputType != EHoudiniOutputType::Mesh) + { + return false; + } + + int32 GeoId = -1; + int32 PartId = -1; + + for (auto & Pair : HoudiniOutput->OutputObjects) + { + GeoId = Pair.Key.GeoId; + PartId = Pair.Key.PartId; + + if (Pair.Value.bIsGeometryCollectionPiece) + { + return true; + } + } + + if (GeoId == -1 || PartId == -1) + { + return false; + } + + int FractureIndex = 0; + + if (!GetFracturePieceAttribute(GeoId, PartId, FractureIndex)) + { + return false; + } + else if (FractureIndex <= 0) + { + return false; + } + + return true; +} + +bool +FHoudiniGeometryCollectionTranslator::IsGeometryCollectionInstancerPart( + const HAPI_NodeId& InstancerGeoId, + const HAPI_PartId& InstancerPartId) +{ + HAPI_ParmId InstancedPartId = -1; + + // Assume that there is only one part per instance. This is always true for now but may need to be looked at later. + int NumInstancedParts = 1; + + TArray InstancedPartIds; + InstancedPartIds.SetNumZeroed(NumInstancedParts); + if (FHoudiniApi::GetInstancedPartIds( + FHoudiniEngine::Get().GetSession(), InstancerGeoId, InstancerPartId, + InstancedPartIds.GetData(), 0, NumInstancedParts) != HAPI_RESULT_SUCCESS) + { + return false; + } + + if (InstancedPartIds.Num() <= 0) + { + return false; + } + else + { + InstancedPartId = InstancedPartIds[0]; + } + + if (InstancedPartId == -1) + { + return false; + } + + int FractureIndex = 0; + + if (!GetFracturePieceAttribute(InstancerGeoId, InstancedPartId, FractureIndex)) + { + return false; + } + else if (FractureIndex <= 0) + { + return false; + } + + return true; +} + + +void +FHoudiniGeometryCollectionTranslator::ApplyGeometryCollectionAttributes( + UGeometryCollection* GeometryCollection, + FHoudiniGeometryCollectionPiece FirstPiece) +{ + int32 GeoId = FirstPiece.InstancerOutputIdentifier->GeoId; + int32 PartId = FirstPiece.InstancedPartId; + + { + // Damage thresholds are not yet available in vanilla 4.26. UNCOMMENT THIS IN FUTURE VERSIONS. + // Clustering - Damage thresholds + TArray FloatData; + FloatData.Empty(); + + const char * AttributeName = HAPI_UNREAL_ATTRIB_GC_CLUSTERING_DAMAGE_THRESHOLD; + + TArray FloatDataSizes; + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), GeoId, PartId, + AttributeName, HAPI_AttributeOwner::HAPI_ATTROWNER_DETAIL, &AttributeInfo) && AttributeInfo.exists) + { + FloatData.SetNumZeroed(AttributeInfo.totalArrayElements); + FloatDataSizes.SetNumZeroed(AttributeInfo.totalArrayElements); + + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetAttributeFloatArrayData( + FHoudiniEngine::Get().GetSession(), GeoId, PartId, + AttributeName, &AttributeInfo, (float*)FloatData.GetData(), AttributeInfo.totalArrayElements, FloatDataSizes.GetData(), 0, AttributeInfo.count)) + { + if (FloatData.Num() > 0) + { + GeometryCollection->DamageThreshold = FloatData; + } + } + } + } + + { + // Clustering - Cluster connection type + TArray IntData; + IntData.Empty(); + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_GC_CLUSTERING_CLUSTER_CONNECTION_TYPE); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, IntData, 0, 1); + + if (bSuccess) + { + if (IntData.Num() > 0) + { + int32 Result = IntData[0]; + EClusterConnectionTypeEnum ConnectionType = EClusterConnectionTypeEnum::Chaos_PointImplicit; + + if (Result == 0) + { + ConnectionType = EClusterConnectionTypeEnum::Chaos_PointImplicit; + } + else + { + ConnectionType = (EClusterConnectionTypeEnum)(Result + 1); // +1 because of the hidden DelaunayTriangulation option + } + + GeometryCollection->ClusterConnectionType = ConnectionType; + } + } + } + + { + // Collisions - Mass as density + TArray IntData; + IntData.Empty(); + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MASS_AS_DENSITY); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, IntData, 0, 1); + + if (bSuccess) + { + if (IntData.Num() > 0) + { + bool Result = (IntData[0] == 1); + GeometryCollection->bMassAsDensity = Result; + } + } + } + + { + // Collisions - Mass + TArray FloatData; + FloatData.Empty(); + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MASS); + if (Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, FloatData)) + { + if (FloatData.Num() > 0) + { + GeometryCollection->Mass = FloatData[0]; + } + } + } + + { + // Collisions - Minimum Mass Clamp + TArray FloatData; + FloatData.Empty(); + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MINIMUM_MASS_CLAMP); + if (Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, FloatData)) + { + if (FloatData.Num() > 0) + { + GeometryCollection->MinimumMassClamp = FloatData[0]; + } + } + } + + // Size specific data - Precompute valid attribute names as we want to compute 2D array of structs, but want to + // keep default equal to index (0,0) + // Get the part infos for attribute names + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), GeoId, PartId, &PartInfo); + + // Get All attribute names for that part + int32 nAttribCount = PartInfo.attributeCounts[HAPI_AttributeOwner::HAPI_ATTROWNER_DETAIL]; + + TArray AttribNameSHArray; + AttribNameSHArray.SetNum(nAttribCount); + + // Gets attribute names + FHoudiniApi::GetAttributeNames( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_DETAIL, + AttribNameSHArray.GetData(), nAttribCount); + + TArray AttribNameArray; + FHoudiniEngineString::SHArrayToFStringArray(AttribNameSHArray, AttribNameArray); + + TSet AttributeNames(AttribNameArray); + { + // Collisions - Size specific data - Max size. + // Only add new size specific data if we actually have the sizes + TArray Data; + const char * AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MAX_SIZE; + FHoudiniHapiAccessor Accessor(GeoId, PartId, AttributeName); + Accessor.bCanBeArray = true; + + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Data); + + if (bSuccess) + { + if (Data.Num() > 0) + { + GeometryCollection->SizeSpecificData.SetNum(FMath::Max(GeometryCollection->SizeSpecificData.Num(), Data.Num())); + + for (int32 i = 0; i < Data.Num(); i++) + { + GeometryCollection->SizeSpecificData[i].MaxSize = Data[i]; + } + } + } + } + + { + // Collisions - Size specific data - Damage threshold. + // Only add new size specific data if we actually have the sizes + TArray Data; + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_GC_COLLISIONS_DAMAGE_THRESHOLD); + Accessor.bCanBeArray = true; + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Data); + if (bSuccess) + { + if (Data.Num() > 0) + { + for (int32 i = 0; i < Data.Num(); i++) + { + if (i < Data.Num()) + { + GeometryCollection->SizeSpecificData[i].DamageThreshold = Data[i]; + } + } + } + } + } + + + // Get the default Size Specific data for this GC + TArray &GCSizeSpecDatas = GeometryCollection->SizeSpecificData; + + for (int32 GCSizeSpecIdx = 0; GCSizeSpecIdx < GCSizeSpecDatas.Num(); GCSizeSpecIdx++) + { + FGeometryCollectionSizeSpecificData & GCSizeSpecData = GCSizeSpecDatas[GCSizeSpecIdx]; + + { + // Collisions - Size specific data - Collision Type + TArray Data; + FString AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_COLLISION_TYPE; + const FString AttributeNameWithPostfix = FString::Printf(TEXT("%s_%d"), *AttributeName, GCSizeSpecIdx); + bool bUseDefaultName = (GCSizeSpecIdx == 0 && AttributeNames.Contains(AttributeName)); + if (!bUseDefaultName) + AttributeName = AttributeNameWithPostfix; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName)); + Accessor.bCanBeArray = true; + + if (AttributeNames.Contains(AttributeName) && Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Data)) + { + if (Data.Num() > 0) + { + GCSizeSpecData.CollisionShapes.SetNum(FMath::Max(GCSizeSpecData.CollisionShapes.Num(), Data.Num())); + for (int32 i = 0; i < Data.Num(); i++) + { + int32 Result = Data[i]; + if (Result < (int32)ECollisionTypeEnum::Chaos_Max) + { + ECollisionTypeEnum CollisionType = (ECollisionTypeEnum)Result; + + GCSizeSpecData.CollisionShapes[i].CollisionType = CollisionType; + } + } + } + } + } + + { + // Collisions - Size specific data - Implicit Type + TArray Data; + FString AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_IMPLICIT_TYPE; + const FString AttributeNameWithPostfix = FString::Printf(TEXT("%s_%d"), *AttributeName, GCSizeSpecIdx); + bool bUseDefaultName = (GCSizeSpecIdx == 0 && AttributeNames.Contains(AttributeName)); + if (!bUseDefaultName) + AttributeName = AttributeNameWithPostfix; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName)); + Accessor.bCanBeArray = true; + + if (AttributeNames.Contains(AttributeName) && Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Data)) + { + if (Data.Num() > 0) + { + GCSizeSpecData.CollisionShapes.SetNum(FMath::Max(GCSizeSpecData.CollisionShapes.Num(), Data.Num())); + for (int32 i = 0; i < Data.Num(); i++) + { + int32 Result = Data[i]; + // 0 Box 1 Sphere, 2 Capsule. 3 Level Set, 4 None, 5 Convex + EImplicitTypeEnum ImplicitType = EImplicitTypeEnum::Chaos_Implicit_None; + if (Result >= 0 && Result < (int32)EImplicitTypeEnum::Chaos_Max) + { + ImplicitType = (EImplicitTypeEnum)(Result); + } + + GCSizeSpecData.CollisionShapes[i].ImplicitType = ImplicitType; + } + } + } + } + + { + // Collisions - Size specific data - Min Level Set Resolution + TArray Data; + FString AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MIN_LEVEL_SET_RESOLUTION; + const FString AttributeNameWithPostfix = FString::Printf(TEXT("%s_%d"), *AttributeName, GCSizeSpecIdx); + bool bUseDefaultName = (GCSizeSpecIdx == 0 && AttributeNames.Contains(AttributeName)); + if (!bUseDefaultName) + AttributeName = AttributeNameWithPostfix; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName)); + Accessor.bCanBeArray = true; + + if (AttributeNames.Contains(AttributeName) && Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Data)) + { + if (Data.Num() > 0) + { + GCSizeSpecData.CollisionShapes.SetNum(FMath::Max(GCSizeSpecData.CollisionShapes.Num(), Data.Num())); + for (int32 i = 0; i < Data.Num(); i++) + { + GCSizeSpecData.CollisionShapes[i].LevelSet.MinLevelSetResolution = Data[i]; + } + } + } + } + + { + // Collisions - Size specific data - Max Level Set Resolution + TArray Data; + FString AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MAX_LEVEL_SET_RESOLUTION; + const FString AttributeNameWithPostfix = FString::Printf(TEXT("%s_%d"), *AttributeName, GCSizeSpecIdx); + bool bUseDefaultName = (GCSizeSpecIdx == 0 && AttributeNames.Contains(AttributeName)); + if (!bUseDefaultName) + AttributeName = AttributeNameWithPostfix; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName)); + Accessor.bCanBeArray = true; + + if (AttributeNames.Contains(AttributeName) && Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Data)) + { + if (Data.Num() > 0) + { + GCSizeSpecData.CollisionShapes.SetNum(FMath::Max(GCSizeSpecData.CollisionShapes.Num(), Data.Num())); + for (int32 i = 0; i < Data.Num(); i++) + { + GCSizeSpecData.CollisionShapes[i].LevelSet.MaxLevelSetResolution = Data[i]; + } + } + } + } + + { + // Collisions - Size specific data - Min cluster level set resolution + TArray Data; + FString AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MIN_CLUSTER_LEVEL_SET_RESOLUTION; + const FString AttributeNameWithPostfix = FString::Printf(TEXT("%s_%d"), *AttributeName, GCSizeSpecIdx); + bool bUseDefaultName = (GCSizeSpecIdx == 0 && AttributeNames.Contains(AttributeName)); + if (!bUseDefaultName) + AttributeName = AttributeNameWithPostfix; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName)); + Accessor.bCanBeArray = true; + + if (AttributeNames.Contains(AttributeName) && Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Data)) + { + if (Data.Num() > 0) + { + GCSizeSpecData.CollisionShapes.SetNum(FMath::Max(GCSizeSpecData.CollisionShapes.Num(), Data.Num())); + for (int32 i = 0; i < Data.Num(); i++) + { + GCSizeSpecData.CollisionShapes[i].LevelSet.MinClusterLevelSetResolution = Data[i]; + } + } + } + } + + { + // Collisions - Max cluster level set resolution + TArray Data; + FString AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MAX_CLUSTER_LEVEL_SET_RESOLUTION; + const FString AttributeNameWithPostfix = FString::Printf(TEXT("%s_%d"), *AttributeName, GCSizeSpecIdx); + bool bUseDefaultName = (GCSizeSpecIdx == 0 && AttributeNames.Contains(AttributeName)); + if (!bUseDefaultName) + AttributeName = AttributeNameWithPostfix; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName)); + Accessor.bCanBeArray = true; + + if (AttributeNames.Contains(AttributeName) && Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Data)) + { + if (Data.Num() > 0) + { + GCSizeSpecData.CollisionShapes.SetNum(FMath::Max(GCSizeSpecData.CollisionShapes.Num(), Data.Num())); + for (int32 i = 0; i < Data.Num(); i++) + { + GCSizeSpecData.CollisionShapes[i].LevelSet.MaxClusterLevelSetResolution = Data[i]; + } + } + } + } + + { + // Collisions - Size specific data - Object reduction percentage + TArray Data; + FString AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_COLLISION_OBJECT_REDUCTION_PERCENTAGE; + const FString AttributeNameWithPostfix = FString::Printf(TEXT("%s_%d"), *AttributeName, GCSizeSpecIdx); + bool bUseDefaultName = (GCSizeSpecIdx == 0 && AttributeNames.Contains(AttributeName)); + if (!bUseDefaultName) + AttributeName = AttributeNameWithPostfix; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName)); + Accessor.bCanBeArray = true; + + if (AttributeNames.Contains(AttributeName) && Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Data)) + { + if (Data.Num() > 0) + { + GCSizeSpecData.CollisionShapes.SetNum(FMath::Max(GCSizeSpecData.CollisionShapes.Num(), Data.Num())); + for (int32 i = 0; i < Data.Num(); i++) + { + GCSizeSpecData.CollisionShapes[i].CollisionObjectReductionPercentage = Data[i]; + } + } + } + } + + { + // Collisions - Size specific data - Collision margin fraction + TArray Data; + FString AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_COLLISION_MARGIN_FRACTION; + const FString AttributeNameWithPostfix = FString::Printf(TEXT("%s_%d"), *AttributeName, GCSizeSpecIdx); + bool bUseDefaultName = (GCSizeSpecIdx == 0 && AttributeNames.Contains(AttributeName)); + if (!bUseDefaultName) + AttributeName = AttributeNameWithPostfix; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName)); + Accessor.bCanBeArray = true; + + if (AttributeNames.Contains(AttributeName) && Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Data)) + { + if (Data.Num() > 0) + { + GCSizeSpecData.CollisionShapes.SetNum(FMath::Max(GCSizeSpecData.CollisionShapes.Num(), Data.Num())); + for (int32 i = 0; i < Data.Num(); i++) + { + GCSizeSpecData.CollisionShapes[i].CollisionMarginFraction = Data[i]; + } + } + } + } + + { + // Collisions - Collision particles fraction + TArray Data; + FString AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_COLLISION_PARTICLES_FRACTION; + const FString AttributeNameWithPostfix = FString::Printf(TEXT("%s_%d"), *AttributeName, GCSizeSpecIdx); + bool bUseDefaultName = (GCSizeSpecIdx == 0 && AttributeNames.Contains(AttributeName)); + if (!bUseDefaultName) + AttributeName = AttributeNameWithPostfix; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName)); + Accessor.bCanBeArray = true; + + if (AttributeNames.Contains(AttributeName) && Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Data)) + { + if (Data.Num() > 0) + { + GCSizeSpecData.CollisionShapes.SetNum(FMath::Max(GCSizeSpecData.CollisionShapes.Num(), Data.Num())); + for (int32 i = 0; i < Data.Num(); i++) + { + GCSizeSpecData.CollisionShapes[i].CollisionParticles.CollisionParticlesFraction = Data[i]; + } + } + } + } + + { + // Collisions - Maximum collision particles + TArray Data; + FString AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MAXIMUM_COLLISION_PARTICLES; + const FString AttributeNameWithPostfix = FString::Printf(TEXT("%s_%d"), *AttributeName, GCSizeSpecIdx); + bool bUseDefaultName = (GCSizeSpecIdx == 0 && AttributeNames.Contains(AttributeName)); + if (!bUseDefaultName) + AttributeName = AttributeNameWithPostfix; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName)); + Accessor.bCanBeArray = true; + + if (AttributeNames.Contains(AttributeName) && Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, Data)) + { + if (Data.Num() > 0) + { + GCSizeSpecData.CollisionShapes.SetNum(FMath::Max(GCSizeSpecData.CollisionShapes.Num(), Data.Num())); + for (int32 i = 0; i < Data.Num(); i++) + { + GCSizeSpecData.CollisionShapes[i].CollisionParticles.MaximumCollisionParticles = Data[i]; + } + } + } + } + } +} + +//---------------------------------------------------------------------------------------------------------- +// FUniqueVertex struct copied from GeometryCollectionConversion.h +// Replace this when you can figure out a way to access this code without depending on the GC plugin +//---------------------------------------------------------------------------------------------------------- +struct FUniqueVertex +{ + FVector3f Normal; + FVector3f Tangent; + TArray UVs; + + bool operator==(const FUniqueVertex& Other) const + { + if (this->UVs.Num() != Other.UVs.Num()) + { + return false; + } + + bool bEquality = true; + bEquality &= (this->Normal == Other.Normal); + bEquality &= (this->Tangent == Other.Tangent); + for (int32 UVLayerIdx = 0; UVLayerIdx < UVs.Num(); ++UVLayerIdx) + { + bEquality &= (this->UVs[UVLayerIdx] == Other.UVs[UVLayerIdx]); + } + + return bEquality; + } +}; + +FORCEINLINE uint32 GetTypeHash(const FUniqueVertex& UniqueVertex) +{ + uint32 VertexHash = GetTypeHash(UniqueVertex.Normal); + VertexHash = HashCombine(VertexHash, GetTypeHash(UniqueVertex.Tangent)); + for (int32 UVLayerIdx = 0; UVLayerIdx < UniqueVertex.UVs.Num(); ++UVLayerIdx) + { + VertexHash = HashCombine(VertexHash, GetTypeHash(UniqueVertex.UVs[UVLayerIdx])); + } + + return VertexHash; +} + + + +//---------------------------------------------------------------------------------------------------------- +// AppendStaticMesh function copied from GeometryCollectionConversion.h +// Replace this when you can figure out a way to access this code without depending on the GC plugin +//---------------------------------------------------------------------------------------------------------- +void +FHoudiniGeometryCollectionTranslator::AppendStaticMesh( + const UStaticMesh* StaticMesh, + const TArray& Materials, + const FTransform& StaticMeshTransform, + UGeometryCollection* GeometryCollectionObject, + bool ReindexMaterials) +{ + if (StaticMesh == nullptr) + { + return; + } + + // Prefer the HiRes description, although this isn't always available. + FMeshDescription* MeshDescription = nullptr; + if (StaticMesh->IsHiResMeshDescriptionValid()) + { + MeshDescription = StaticMesh->GetHiResMeshDescription(); + } + else + { + MeshDescription = StaticMesh->GetMeshDescription(0); + } + + check(GeometryCollectionObject); + TSharedPtr GeometryCollectionPtr = GeometryCollectionObject->GetGeometryCollection(); + FGeometryCollection* GeometryCollection = GeometryCollectionPtr.Get(); + check(GeometryCollection); + + if (MeshDescription) + { + FStaticMeshOperations::ComputeTriangleTangentsAndNormals(*MeshDescription); + FStaticMeshOperations::RecomputeNormalsAndTangentsIfNeeded(*MeshDescription, EComputeNTBsFlags::UseMikkTSpace); + + // source vertex information + FStaticMeshAttributes Attributes(*MeshDescription); + TArrayView SourcePosition = Attributes.GetVertexPositions().GetRawArray(); + TArrayView SourceTangent = Attributes.GetVertexInstanceTangents().GetRawArray(); + TArrayView SourceBinormalSign = Attributes.GetVertexInstanceBinormalSigns().GetRawArray(); + TArrayView SourceNormal = Attributes.GetVertexInstanceNormals().GetRawArray(); + TArrayView SourceColor = Attributes.GetVertexInstanceColors().GetRawArray(); + + TVertexInstanceAttributesConstRef InstanceUVs = Attributes.GetVertexInstanceUVs(); + const int32 NumUVLayers = InstanceUVs.GetNumChannels(); + TArray> SourceUVArrays; + SourceUVArrays.SetNum(NumUVLayers); + for (int32 UVLayerIdx = 0; UVLayerIdx < NumUVLayers; ++UVLayerIdx) + { + SourceUVArrays[UVLayerIdx] = InstanceUVs.GetRawArray(UVLayerIdx); + } + + // Dont forgot to set the numbers of UV layers on the GC! + GeometryCollection->SetNumUVLayers(NumUVLayers); + + // target vertex information + TManagedArray& TargetVertex = GeometryCollection->Vertex; + TManagedArray& TargetTangentU = GeometryCollection->TangentU; + TManagedArray& TargetTangentV = GeometryCollection->TangentV; + TManagedArray& TargetNormal = GeometryCollection->Normal; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + // UE5.2 removed direct access to UVs + // We need to use ModifyUVs to edit them... +#else + TManagedArray>& TargetUVs = GeometryCollection->UVs; +#endif + TManagedArray& TargetColor = GeometryCollection->Color; + TManagedArray& TargetBoneMap = GeometryCollection->BoneMap; + TManagedArray& TargetBoneColor = GeometryCollection->BoneColor; + TManagedArray& TargetBoneName = GeometryCollection->BoneName; + + const int32 VertexStart = GeometryCollection->NumElements(FGeometryCollection::VerticesGroup); + int32 VertexCount = 0; + + FVector Scale = StaticMeshTransform.GetScale3D(); + + // We'll need to re-introduce UV seams, etc. by splitting vertices. + // A new mapping of MeshDescription vertex instances to the split vertices is maintained. + TMap VertexInstanceToGeometryCollectionVertex; + VertexInstanceToGeometryCollectionVertex.Reserve(Attributes.GetVertexInstanceNormals().GetNumElements()); + + for (const FVertexID VertexIndex : MeshDescription->Vertices().GetElementIDs()) + { + TArrayView ReferencingVertexInstances = MeshDescription->GetVertexVertexInstanceIDs(VertexIndex); + + // Generate per instance hash of splittable attributes. + TMap> SplitVertices; + for (const FVertexInstanceID& InstanceID : ReferencingVertexInstances) + { + TArray SourceUVs; + SourceUVs.SetNum(NumUVLayers); + for (int32 UVLayerIdx = 0; UVLayerIdx < NumUVLayers; ++UVLayerIdx) + { + SourceUVs[UVLayerIdx] = SourceUVArrays[UVLayerIdx][InstanceID]; + } + + FUniqueVertex UniqueVertex{ SourceNormal[InstanceID], SourceTangent[InstanceID], SourceUVs }; + TArray& SplitVertex = SplitVertices.FindOrAdd(UniqueVertex); + SplitVertex.Add(InstanceID); + } + + int32 CurrentVertex = GeometryCollection->AddElements(SplitVertices.Num(), FGeometryCollection::VerticesGroup); + + // Create a new vertex for each split vertex and map the mesh description instance to it. + for (const TTuple>& SplitVertex : SplitVertices) + { + const TArray& InstanceIDs = SplitVertex.Value; + const FVertexInstanceID& ExemplarInstanceID = InstanceIDs[0]; + + TargetVertex[CurrentVertex] = SourcePosition[VertexIndex] * (FVector3f)Scale; + TargetBoneMap[CurrentVertex] = GeometryCollection->NumElements(FGeometryCollection::TransformGroup); + + TargetNormal[CurrentVertex] = SourceNormal[ExemplarInstanceID]; + TargetTangentU[CurrentVertex] = SourceTangent[ExemplarInstanceID]; + TargetTangentV[CurrentVertex] = (FVector3f)SourceBinormalSign[ExemplarInstanceID] * FVector3f::CrossProduct(TargetNormal[CurrentVertex], TargetTangentU[CurrentVertex]); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + for (int32 LayerIdx = 0; LayerIdx < SplitVertex.Key.UVs.Num(); ++LayerIdx) + { + GeometryCollection->ModifyUV(CurrentVertex, LayerIdx) = SplitVertex.Key.UVs[LayerIdx]; + } +#else + TargetUVs[CurrentVertex] = SplitVertex.Key.UVs; +#endif + + if (SourceColor.Num() > 0) + { + TargetColor[CurrentVertex] = FLinearColor(SourceColor[ExemplarInstanceID]); + } + else + { + TargetColor[CurrentVertex] = FLinearColor::White; + } + + for (const FVertexInstanceID& InstanceID : InstanceIDs) + { + VertexInstanceToGeometryCollectionVertex.Add(InstanceID, CurrentVertex); + } + + ++CurrentVertex; + ++VertexCount; + } + } + + + // for each material, add a reference in our GeometryCollectionObject + const int32 MaterialStart = GeometryCollectionObject->Materials.Num(); + const int32 NumMeshMaterials = Materials.Num(); + GeometryCollectionObject->Materials.Reserve(MaterialStart + NumMeshMaterials); + + for (int32 Index = 0; Index < NumMeshMaterials; ++Index) + { + UMaterialInterface* CurrMaterial = Materials[Index]; + + // Possible we have a null entry - replace with default + if (CurrMaterial == nullptr) + { + CurrMaterial = UMaterial::GetDefaultMaterial(MD_Surface); + } + + // We add the material twice, once for interior and again for exterior. + GeometryCollectionObject->Materials.Add(CurrMaterial); + GeometryCollectionObject->Materials.Add(CurrMaterial); + } + + // target triangle indices + TManagedArray& TargetIndices = GeometryCollection->Indices; + TManagedArray& TargetVisible = GeometryCollection->Visible; + TManagedArray& TargetMaterialID = GeometryCollection->MaterialID; + TManagedArray& TargetMaterialIndex = GeometryCollection->MaterialIndex; + + const int32 IndicesCount = MeshDescription->Triangles().Num(); + const int32 InitialNumIndices = GeometryCollection->NumElements(FGeometryCollection::FacesGroup); + const int32 IndicesStart = GeometryCollection->AddElements(IndicesCount, FGeometryCollection::FacesGroup); + int32 TargetIndex = IndicesStart; + for (const int32 TriangleIndex : MeshDescription->Triangles().GetElementIDs()) + { + TArrayView TriangleVertices = MeshDescription->GetTriangleVertexInstances(TriangleIndex); + + TargetIndices[TargetIndex] = FIntVector( + VertexInstanceToGeometryCollectionVertex[TriangleVertices[0]], + VertexInstanceToGeometryCollectionVertex[TriangleVertices[1]], + VertexInstanceToGeometryCollectionVertex[TriangleVertices[2]] + ); + + TargetVisible[TargetIndex] = true; + + // Materials are ganged in pairs and we want the id to associate with the first of each pair. + TargetMaterialID[TargetIndex] = MaterialStart + (MeshDescription->GetTrianglePolygonGroup(TriangleIndex) * 2); + + // Is this right? + TargetMaterialIndex[TargetIndex] = TargetIndex; + + ++TargetIndex; + } + + // Geometry transform +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + TManagedArray& Transform = GeometryCollection->Transform; +#else + TManagedArray& Transform = GeometryCollection->Transform; +#endif + + int32 TransformIndex1 = GeometryCollection->AddElements(1, FGeometryCollection::TransformGroup); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + Transform[TransformIndex1] = FTransform3f(StaticMeshTransform); + Transform[TransformIndex1].SetScale3D(FVector3f::OneVector); +#else + Transform[TransformIndex1] = StaticMeshTransform; + Transform[TransformIndex1].SetScale3D(FVector::OneVector); +#endif + + // Bone Hierarchy - Added at root with no common parent + TManagedArray& Parent = GeometryCollection->Parent; + TManagedArray& SimulationType = GeometryCollection->SimulationType; + Parent[TransformIndex1] = FGeometryCollection::Invalid; + SimulationType[TransformIndex1] = FGeometryCollection::ESimulationTypes::FST_Rigid; + + const FColor RandBoneColor(FMath::Rand() % 100 + 5, FMath::Rand() % 100 + 5, FMath::Rand() % 100 + 5, 255); + TargetBoneColor[TransformIndex1] = FLinearColor(RandBoneColor); + TargetBoneName[TransformIndex1] = StaticMesh->GetName(); + + // GeometryGroup + int GeometryIndex = GeometryCollection->AddElements(1, FGeometryCollection::GeometryGroup); + + TManagedArray& TransformIndex = GeometryCollection->TransformIndex; + TManagedArray& BoundingBox = GeometryCollection->BoundingBox; + TManagedArray& InnerRadius = GeometryCollection->InnerRadius; + TManagedArray& OuterRadius = GeometryCollection->OuterRadius; + TManagedArray& VertexStartArray = GeometryCollection->VertexStart; + TManagedArray& VertexCountArray = GeometryCollection->VertexCount; + TManagedArray& FaceStartArray = GeometryCollection->FaceStart; + TManagedArray& FaceCountArray = GeometryCollection->FaceCount; + + TransformIndex[GeometryIndex] = TargetBoneMap[VertexStart]; + VertexStartArray[GeometryIndex] = VertexStart; + VertexCountArray[GeometryIndex] = VertexCount; + FaceStartArray[GeometryIndex] = InitialNumIndices; + FaceCountArray[GeometryIndex] = IndicesCount; + + // TransformGroup + TManagedArray& TransformToGeometryIndexArray = GeometryCollection->TransformToGeometryIndex; + TransformToGeometryIndexArray[TransformIndex1] = GeometryIndex; + + FVector Center(FVector::ZeroVector); + for (int32 VertexIndex = VertexStart; VertexIndex < VertexStart + VertexCount; VertexIndex++) + { + Center += (FVector)TargetVertex[VertexIndex]; + } + if (VertexCount) Center /= VertexCount; + + // Inner/Outer edges, bounding box + BoundingBox[GeometryIndex] = FBox(ForceInitToZero); + InnerRadius[GeometryIndex] = FLT_MAX; + OuterRadius[GeometryIndex] = -FLT_MAX; + for (int32 VertexIndex = VertexStart; VertexIndex < VertexStart + VertexCount; VertexIndex++) + { + BoundingBox[GeometryIndex] += (FVector)TargetVertex[VertexIndex]; + + float Delta = (Center - (FVector)TargetVertex[VertexIndex]).Size(); + InnerRadius[GeometryIndex] = FMath::Min(InnerRadius[GeometryIndex], Delta); + OuterRadius[GeometryIndex] = FMath::Max(OuterRadius[GeometryIndex], Delta); + } + + // Inner/Outer centroid + for (int fdx = IndicesStart; fdx < IndicesStart + IndicesCount; fdx++) + { + FVector Centroid(0); + for (int e = 0; e < 3; e++) + { + Centroid += (FVector)TargetVertex[TargetIndices[fdx][e]]; + } + Centroid /= 3; + + float Delta = (Center - Centroid).Size(); + InnerRadius[GeometryIndex] = FMath::Min(InnerRadius[GeometryIndex], Delta); + OuterRadius[GeometryIndex] = FMath::Max(OuterRadius[GeometryIndex], Delta); + } + + // Inner/Outer edges + for (int fdx = IndicesStart; fdx < IndicesStart + IndicesCount; fdx++) + { + for (int e = 0; e < 3; e++) + { + int i = e, j = (e + 1) % 3; + FVector Edge = (FVector)TargetVertex[TargetIndices[fdx][i]] + 0.5 * FVector(TargetVertex[TargetIndices[fdx][j]] - TargetVertex[TargetIndices[fdx][i]]); + float Delta = (Center - Edge).Size(); + InnerRadius[GeometryIndex] = FMath::Min(InnerRadius[GeometryIndex], Delta); + OuterRadius[GeometryIndex] = FMath::Max(OuterRadius[GeometryIndex], Delta); + } + } + + if (ReindexMaterials) + { + GeometryCollection->ReindexMaterials(); + } + } +} + + +// Copied from FractureToolEmbed.h +void FHoudiniGeometryCollectionTranslator::AddSingleRootNodeIfRequired(UGeometryCollection* GeometryCollectionObject) +{ + TSharedPtr GeometryCollectionPtr = GeometryCollectionObject->GetGeometryCollection(); + if (FGeometryCollection* GeometryCollection = GeometryCollectionPtr.Get()) + { + if (FGeometryCollectionClusteringUtility::ContainsMultipleRootBones(GeometryCollection)) + { + FGeometryCollectionClusteringUtility::ClusterAllBonesUnderNewRoot(GeometryCollection); + } + } +} +``` + +`Source/HoudiniEngine/Private/HoudiniGeometryCollectionTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniOutput.h" +#include "HoudiniPackageParams.h" +#include "HoudiniAssetComponent.h" + +#include "CoreMinimal.h" + +class AGeometryCollectionActor; +class UGeometryCollection; +class UGeometryCollectionComponent; +struct FHoudiniGenericAttribute; + +struct HOUDINIENGINE_API FHoudiniGeometryCollectionTranslator +{ + // Helper to contain geometry collection data + struct FHoudiniGeometryCollectionPiece + { + // Set initially + FHoudiniOutputObjectIdentifier* InstancerOutputIdentifier = nullptr; + FHoudiniOutputObject* InstancerOutput = nullptr; + int32 InstancedPartId = -1; + int32 FractureIndex = -1; + int32 ClusterIndex = -1; + FString GeometryCollectionName = TEXT(""); + + // Set On second pass + // Specifies the geometry index in the geometry collection of this piece + int32 GeometryIndex = -1; + }; + + // Helper struct to contain data regarding a single geometry collection. + struct FHoudiniGeometryCollectionData + { + TArray GeometryCollectionPieces; + FHoudiniOutputObjectIdentifier Identifier; + FHoudiniPackageParams PackParams; + + FHoudiniGeometryCollectionData(const FHoudiniOutputObjectIdentifier& InIdentifier, const FHoudiniPackageParams& InPackParams) + { + Identifier = InIdentifier; + PackParams = InPackParams; + } + }; + + public: + static void SetupGeometryCollectionComponentFromOutputs( + TArray>& InAllOutputs, + UObject* InOutputParent, + UObject* InOuterComponent, + const FHoudiniPackageParams& InPackageParams, + UWorld * InWorld); + + static bool GetFracturePieceAttribute(const HAPI_NodeId& GeoId, const HAPI_NodeId& PartId, int& OutInt); + static bool GetClusterPieceAttribute(const HAPI_NodeId& GeoId, const HAPI_NodeId& PartId, int& OutInt); + + static bool GetGeometryCollectionNameAttribute(const HAPI_NodeId& GeoId, const HAPI_NodeId& PartId, FString& OutName); + + static bool IsGeometryCollectionInstancer(const UHoudiniOutput* HoudiniOutput); + static bool IsGeometryCollectionMesh(const UHoudiniOutput* HoudiniOutput); + static bool IsGeometryCollectionInstancerPart(const HAPI_NodeId& InstancerGeoId, const HAPI_PartId& InstancerPartId); + + static AGeometryCollectionActor * CreateNewGeometryActor(UWorld * InWorld, const FString& InActorName, const FTransform& InTransform); + + static bool GetGeometryCollectionNames(TArray>& InAllOutputs, TSet& Names); + + private: + + static UGeometryCollectionComponent* CreateGeometryCollectionComponent(UObject *InOuterComponent); + + static bool RemoveAndDestroyComponent(UObject* InComponent); + + + // Extracts all Geometry collection data from the outputs + // Map is representing gc_name -> gc_data + static bool GetGeometryCollectionData(const TArray& InAllOutputs, const FHoudiniPackageParams& InPackageParams, TMap& OutGeometryCollectionData); + + static void ApplyGeometryCollectionAttributes(UGeometryCollection* GeometryCollection, FHoudiniGeometryCollectionPiece FirstPiece); + + // Copied from GeometryCollectionConversion.h + // As we cannot access the UE function without depending on the GC plugin. + /** + * Appends a static mesh to a GeometryCollectionComponent. + * @param StaticMesh : Const mesh to read vertex/normals/index data from + * @param Materials : Materials fetched from the StaticMeshComponent used to configure this geometry + * @param StaticMeshTransform : Mesh transform. + * @param GeometryCollection : Collection to append the mesh into. + */ + static void AppendStaticMesh(const UStaticMesh* StaticMesh, const TArray& Materials, const FTransform& StaticMeshTransform, UGeometryCollection* GeometryCollectionObject, bool ReindexMaterials = true); + + // Copied from FractureToolEmbed.h + static void AddSingleRootNodeIfRequired(UGeometryCollection* GeometryCollectionObject); +}; + + +``` + +`Source/HoudiniEngine/Private/HoudiniHLODLayerUtils.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniHLODLayerUtils.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "WorldPartition/HLOD/HLODLayer.h" +#include "Landscape.h" +#include "LandscapeStreamingProxy.h" + +void FHoudiniHLODLayerUtils::AddActorToHLOD(AActor* Actor, const FString& AssetRef) +{ +#if WITH_EDITORONLY_DATA + UHLODLayer* HLODLayer = Cast( + StaticLoadObject( + UHLODLayer::StaticClass(), + nullptr, + *AssetRef, + nullptr, + LOAD_NoWarn, + nullptr)); + + Actor->SetHLODLayer(HLODLayer); +#endif +} + +TArray +FHoudiniHLODLayerUtils::GetHLODLayers(HAPI_NodeId NodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner, int Index) +{ + TArray HLODNames; + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_HLOD_LAYER); + Accessor.GetAttributeData(Owner, HLODNames, Index, 1); + if(HLODNames.IsEmpty()) + return {}; + + FHoudiniHLODLayer Layer; + Layer.Name = HLODNames[0]; + + TArray Results; + Results.Add(Layer); + return Results; + +} + +TArray +FHoudiniHLODLayerUtils::GetHLODLayers(HAPI_NodeId NodeId, HAPI_PartId PartId) +{ + TArray Results; + + // Get a list of all groups this part MAY be a member of. + TArray HLODNames; + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_HLOD_LAYER); + bool bResult = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, HLODNames); + + for (FString HLODLayerName : HLODNames) + { + FHoudiniHLODLayer Layer; + + Layer.Name = HLODLayerName; + Results.Add(Layer); + } + + return Results; +} + +void FHoudiniHLODLayerUtils::ApplyHLODLayersToActor(const FHoudiniPackageParams& Params, AActor* Actor, const TArray& Layers) +{ + UWorld* World = Actor->GetWorld(); + + if (Layers.Num() == 0) + return; + + const FHoudiniHLODLayer & Layer = Layers[0]; + + AddActorToHLOD(Actor, Layer.Name); + + if(ALandscape* Landscape = Cast(Actor)) + { +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 7 + TArray> Proxies = Landscape->GetLandscapeInfo()->GetSortedStreamingProxies(); +#elif ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1 + TArray> Proxies = Landscape->GetLandscapeInfo()->StreamingProxies; +#else + TArray> Proxies = Proxy->GetLandscapeInfo()->Proxies; +#endif + for(TWeakObjectPtr Child : Proxies) + { + ALandscapeStreamingProxy* LandscapeProxy = Child.Get(); + AddActorToHLOD(Cast(LandscapeProxy), Layer.Name); + } + } + +} + +void FHoudiniHLODLayerUtils::SetVexCode(HAPI_NodeId VexNodeId, AActor * Actor) +{ + UHLODLayer* Layer = Actor->GetHLODLayer(); + + // If there is no layer, just leave the empty node. Its not an error. + if (!Layer) + return; + + FString LayerName = Layer->GetPathName(); + + FString VexCode = FString::Format(TEXT("s@{0} = \"{1}\";\n"), { TEXT(HAPI_UNREAL_ATTRIB_HLOD_LAYER), LayerName }); + + // Set the snippet parameter to the code. + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniEngineUtils::HapiFindParameterByName(VexNodeId, "snippet", ParmInfo); + if (ParmId != -1) + { + FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), VexNodeId, H_TCHAR_TO_UTF8(*VexCode), ParmId, 0); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Invalid Parameter: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + } +} + +HAPI_NodeId FHoudiniHLODLayerUtils::AddHLODAttributes(AActor* Actor, HAPI_NodeId ParentNodeId, HAPI_NodeId InputNodeId) +{ + HAPI_NodeId VexNode = -1; + + // Create a group node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CreateNode(FHoudiniEngine::Get().GetSession(), + ParentNodeId, + "attribwrangle", + "hlod_layers", + false, + &VexNode), + -1); + + // Hook the new node up to the input node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(FHoudiniEngine::Get().GetSession(), VexNode, 0, InputNodeId, 0), -1); + + // Set the wrangle's class to prims + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(FHoudiniEngine::Get().GetSession(), VexNode, "class", 0, 1), -1); + + // Set the Vex code which will generate the attributes. + SetVexCode(VexNode, Actor); + + return VexNode; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniHLODLayerUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniEngineUtils.h" +#include "CoreMinimal.h" + +class AActor; + +class HOUDINIENGINE_API FHoudiniHLODLayerUtils +{ +public: + static TArray GetHLODLayers(HAPI_NodeId NodeId, HAPI_PartId PartId); + static TArray GetHLODLayers(HAPI_NodeId NodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner, int Index); + + static void ApplyHLODLayersToActor(const FHoudiniPackageParams& Params, AActor* Actor, const TArray& Layers); + + static HAPI_NodeId AddHLODAttributes(AActor * Actor, HAPI_NodeId ParentNodeId, HAPI_NodeId InputNodeId); + + static void SetVexCode(HAPI_NodeId VexNodeId, AActor* Actor); + +private: + static void AddActorToHLOD(AActor* Actor, const FString& AssetRef); + +}; +``` + +`Source/HoudiniEngine/Private/HoudiniHandleTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniHandleTranslator.h" + +#include "HoudiniApi.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniEngine.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineString.h" +#include "HoudiniHandleComponent.h" +#include "HoudiniParameter.h" + + +bool +FHoudiniHandleTranslator::BuildHandles(UHoudiniCookable* HC) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniHandleTranslator::BuildHandles); + + if (!IsValid(HC)) + return false; + + // For handles, we need to support parameters and components! + if (!HC->IsComponentSupported() || !HC->IsParameterSupported()) + return false; + + TArray> NewHandles; + if (FHoudiniHandleTranslator::BuildAllHandles( + HC->GetNodeId(), + HC->ComponentData->Component.Get(), + HC->ComponentData->HandleComponents, + NewHandles, + HC->ParameterData->Parameters)) + { + HC->ComponentData->HandleComponents = NewHandles; + } + + return true; +} + +bool +FHoudiniHandleTranslator::BuildAllHandles( + const HAPI_NodeId& AssetId, + USceneComponent* OuterComponent, + TArray>& CurrentHandles, + TArray>& NewHandles, + TArray>& InParameters ) +{ + if (AssetId < 0) + return false; + + HAPI_AssetInfo AssetInfo; + FHoudiniApi::AssetInfo_Init(&AssetInfo); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetAssetInfo(FHoudiniEngine::Get().GetSession(), AssetId, &AssetInfo)) + return false; + + TMap CurrentHandlesByName; + + for (auto& Handle : CurrentHandles) + { + if (!Handle) + continue; + + CurrentHandlesByName.Add(Handle->GetHandleName(), Handle); + } + + // If there are handles + if (AssetInfo.handleCount > 0) + { + TArray HandleInfos; + HandleInfos.SetNumZeroed(AssetInfo.handleCount); + + for (int32 Idx = 0; Idx < HandleInfos.Num(); Idx++) + { + FHoudiniApi::HandleInfo_Init(&(HandleInfos[Idx])); + } + + if (FHoudiniApi::GetHandleInfo( + FHoudiniEngine::Get().GetSession(), AssetId, + &HandleInfos[0], 0, AssetInfo.handleCount) != HAPI_RESULT_SUCCESS) + { + return false; + } + + + for (int32 HandleIdx = 0; HandleIdx < AssetInfo.handleCount; ++HandleIdx) + { + const HAPI_HandleInfo & HandleInfo = HandleInfos[HandleIdx]; + + // If we do not have bindings, we can skip. + if (HandleInfo.bindingsCount <= 0) + continue; + + FString TypeName = TEXT(""); + EHoudiniHandleType HandleType = EHoudiniHandleType::Unsupported; + { + FHoudiniEngineString HoudiniEngineString(HandleInfo.typeNameSH); + if (!HoudiniEngineString.ToFString(TypeName)) + { + continue; + } + + if (TypeName.Equals(TEXT(HAPI_UNREAL_HANDLE_TRANSFORM))) + HandleType = EHoudiniHandleType::Xform; + else if (TypeName.Equals(TEXT(HAPI_UNREAL_HANDLE_BOUNDER))) + HandleType = EHoudiniHandleType::Bounder; + } + + FString HandleName = TEXT(""); + + { + FHoudiniEngineString HoudiniEngineString(HandleInfo.nameSH); + if (!HoudiniEngineString.ToFString(HandleName)) + continue; + } + + if (HandleType == EHoudiniHandleType::Unsupported) + { + HOUDINI_LOG_DISPLAY(TEXT("%s: Unsupported Handle Type %s for handle %s"), + OuterComponent ? *(OuterComponent->GetName()) : TEXT("?"), *TypeName, *HandleName); + continue; + } + + UHoudiniHandleComponent* HandleComponent = nullptr; + UHoudiniHandleComponent** FoundHandleComponent = CurrentHandlesByName.Find(HandleName); + if (FoundHandleComponent) + { + HandleComponent = *FoundHandleComponent; + + CurrentHandles.Remove(*FoundHandleComponent); + CurrentHandlesByName.Remove(HandleName); + } + else + { + HandleComponent = NewObject( + OuterComponent, + UHoudiniHandleComponent::StaticClass(), + NAME_None, RF_Public | RF_Transactional); + + HandleComponent->SetHandleName(HandleName); + HandleComponent->SetHandleType(HandleType); + + // Change the creation method so the component is listed in the details panels + FHoudiniEngineRuntimeUtils::AddOrSetAsInstanceComponent(HandleComponent); + } + + if (!HandleComponent) + continue; + + // If we have no parent, we need to re-attach. + if (!HandleComponent->GetAttachParent()) + { + HandleComponent->AttachToComponent(OuterComponent, FAttachmentTransformRules::KeepRelativeTransform); + } + + HandleComponent->SetVisibility(true); + + // If component is not registered, register it + if (!HandleComponent->IsRegistered()) + HandleComponent->RegisterComponent(); + + // Get handle's bindings + TArray BindingInfos; + BindingInfos.SetNumZeroed(HandleInfo.bindingsCount); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetHandleBindingInfo( + FHoudiniEngine::Get().GetSession(), + AssetId, HandleIdx, &BindingInfos[0], 0, HandleInfo.bindingsCount)) + continue; + + HAPI_TransformEuler HapiEulerXform; + FMemory::Memzero(HapiEulerXform); + FHoudiniApi::TransformEuler_Init(&HapiEulerXform); + + TSharedPtr RSTOrderStrPtr, XYZOrderStrPtr; + + for (const auto& BindingInfo : BindingInfos) + { + FString HandleParmName = TEXT(""); + FHoudiniEngineString HoudiniEngineString(BindingInfo.handleParmNameSH); + HoudiniEngineString.ToFString(HandleParmName); + + const HAPI_ParmId ParamId = BindingInfo.assetParmId; + + TArray> &XformParms = HandleComponent->XformParms; + + UHoudiniParameter* FoundParam = nullptr; + + for (auto Param : InParameters) + { + if (Param->GetParmId() == ParamId) + { + FoundParam = Param; + break; + } + } + + HandleComponent->InitializeHandleParameters(); + + if (!HandleComponent->CheckHandleValid()) + continue; + + (void)(XformParms[int32(EXformParameter::TX)]->Bind(HapiEulerXform.position[0], "tx", 0, HandleParmName, FoundParam) + || XformParms[int32(EXformParameter::TY)]->Bind(HapiEulerXform.position[1], "ty", 1, HandleParmName, FoundParam) + || XformParms[int32(EXformParameter::TZ)]->Bind(HapiEulerXform.position[2], "tz", 2, HandleParmName, FoundParam) + + || XformParms[int32(EXformParameter::RX)]->Bind(HapiEulerXform.rotationEuler[0], "rx", 0, HandleParmName, FoundParam) + || XformParms[int32(EXformParameter::RY)]->Bind(HapiEulerXform.rotationEuler[1], "ry", 1, HandleParmName, FoundParam) + || XformParms[int32(EXformParameter::RZ)]->Bind(HapiEulerXform.rotationEuler[2], "rz", 2, HandleParmName, FoundParam) + + || XformParms[int32(EXformParameter::SX)]->Bind(HapiEulerXform.scale[0], "sx", 0, HandleParmName, FoundParam) + || XformParms[int32(EXformParameter::SY)]->Bind(HapiEulerXform.scale[1], "sy", 1, HandleParmName, FoundParam) + || XformParms[int32(EXformParameter::SZ)]->Bind(HapiEulerXform.scale[2], "sz", 2, HandleParmName, FoundParam) + + || HandleComponent->RSTParm->Bind(RSTOrderStrPtr, "trs_order", 0, HandleParmName, FoundParam) + || HandleComponent->RotOrderParm->Bind(XYZOrderStrPtr, "xyz_order", 0, HandleParmName, FoundParam) + ); + } + + HapiEulerXform.rstOrder = GetHapiRSTOrder(RSTOrderStrPtr); + HapiEulerXform.rotationOrder = GetHapiXYZOrder(XYZOrderStrPtr); + + constexpr float MaxFloat = TNumericLimits::Max(); + constexpr float MinFloat = TNumericLimits::Min(); + + HapiEulerXform.scale[0] = FMath::Clamp(HapiEulerXform.scale[0], MinFloat, MaxFloat); + HapiEulerXform.scale[1] = FMath::Clamp(HapiEulerXform.scale[1], MinFloat, MaxFloat); + HapiEulerXform.scale[2] = FMath::Clamp(HapiEulerXform.scale[2], MinFloat, MaxFloat); + + FTransform UnrealXform; + FHoudiniEngineUtils::TranslateHapiTransform(HapiEulerXform, UnrealXform); + + HandleComponent->SetRelativeTransform(UnrealXform); + + NewHandles.Add(HandleComponent); + } + } + + return true; +} + +HAPI_RSTOrder +FHoudiniHandleTranslator::GetHapiRSTOrder(const TSharedPtr & StrPtr) +{ + if (StrPtr.Get()) + { + FString & Str = *StrPtr; + + if (Str == "trs") return HAPI_TRS; + else if (Str == "tsr") return HAPI_TSR; + else if (Str == "rts") return HAPI_RTS; + else if (Str == "rst") return HAPI_RST; + else if (Str == "str") return HAPI_STR; + else if (Str == "srt") return HAPI_SRT; + } + + return HAPI_SRT; +} + +HAPI_XYZOrder +FHoudiniHandleTranslator::GetHapiXYZOrder(const TSharedPtr & StrPtr) +{ + if (StrPtr.Get()) + { + FString & Str = *StrPtr; + + if (Str == "xyz") return HAPI_XYZ; + else if (Str == "xzy") return HAPI_XZY; + else if (Str == "yxz") return HAPI_YXZ; + else if (Str == "yzx") return HAPI_YZX; + else if (Str == "zxy") return HAPI_ZXY; + else if (Str == "zyx") return HAPI_ZYX; + } + + return HAPI_XYZ; +} + + +void +FHoudiniHandleTranslator::UpdateTransformParameters(UHoudiniHandleComponent* HandleComponent) +{ + if (!IsValid(HandleComponent)) + return; + + if (!HandleComponent->CheckHandleValid()) + return; + + TArray>& XformParms = HandleComponent->XformParms; + if (XformParms.Num() < (int32)EXformParameter::COUNT) + return; + + FTransform NewTransform = HandleComponent->GetRelativeTransform(); + HAPI_Transform HapiXform; + FHoudiniApi::Transform_Init(&HapiXform); + FHoudiniEngineUtils::TranslateUnrealTransform(NewTransform, HapiXform); + + const HAPI_Session* Session = FHoudiniEngine::Get().GetSession(); + float HapiMatrix[16]; + FHoudiniApi::ConvertTransformQuatToMatrix(Session, &HapiXform, HapiMatrix); + + HAPI_TransformEuler HapiEulerXform; + FHoudiniApi::TransformEuler_Init(&HapiEulerXform); + FHoudiniApi::ConvertMatrixToEuler( + Session, + HapiMatrix, + GetHapiRSTOrder(HandleComponent->RSTParm->Get(TSharedPtr< FString >())), + GetHapiXYZOrder(HandleComponent->RotOrderParm->Get(TSharedPtr< FString >())), + &HapiEulerXform + ); + + *XformParms[int32(EXformParameter::TX)] = HapiEulerXform.position[0]; + *XformParms[int32(EXformParameter::TY)] = HapiEulerXform.position[1]; + *XformParms[int32(EXformParameter::TZ)] = HapiEulerXform.position[2]; + + *XformParms[int32(EXformParameter::RX)] = HapiEulerXform.rotationEuler[0]; + *XformParms[int32(EXformParameter::RY)] = HapiEulerXform.rotationEuler[1]; + *XformParms[int32(EXformParameter::RZ)] = HapiEulerXform.rotationEuler[2]; + + constexpr float MaxFloat = TNumericLimits::Max(); + constexpr float MinFloat = TNumericLimits::Min(); + HapiEulerXform.scale[0] = FMath::Clamp(HapiEulerXform.scale[0], MinFloat, MaxFloat); + HapiEulerXform.scale[1] = FMath::Clamp(HapiEulerXform.scale[1], MinFloat, MaxFloat); + HapiEulerXform.scale[2] = FMath::Clamp(HapiEulerXform.scale[2], MinFloat, MaxFloat); + + *XformParms[int32(EXformParameter::SX)] = HapiEulerXform.scale[0]; + *XformParms[int32(EXformParameter::SY)] = HapiEulerXform.scale[1]; + *XformParms[int32(EXformParameter::SZ)] = HapiEulerXform.scale[2]; + + HandleComponent->LastSentTransform = NewTransform; + HandleComponent->bNeedToUpdateTransform = false; +} + +void +FHoudiniHandleTranslator::UpdateHandlesIfNeeded(TArray>& InHandleComponents) +{ + for (auto& CurHandle : InHandleComponents) + { + if (!CurHandle) + continue; + + if (CurHandle->IsTransformUpdateNeeded()) + FHoudiniHandleTranslator::UpdateTransformParameters(CurHandle); + } +} +``` + +`Source/HoudiniEngine/Private/HoudiniHandleTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "HoudiniApi.h" + +#include "Templates/SharedPointer.h" + +class UHoudiniCookable; +class UHoudiniHandleComponent; +class UHoudiniParameter; +class USceneComponent; + +struct HOUDINIENGINE_API FHoudiniHandleTranslator +{ + static bool BuildHandles(UHoudiniCookable* HC); + + static bool BuildAllHandles( + const HAPI_NodeId& AssetId, + USceneComponent* OuterComponent, + TArray>& CurrentHandles, + TArray>& NewHandles, + TArray>& InParameters); + + static HAPI_RSTOrder GetHapiRSTOrder(const TSharedPtr & StrPtr); + + static HAPI_XYZOrder GetHapiXYZOrder(const TSharedPtr & StrPtr); + + static void UpdateHandlesIfNeeded(TArray>& InHandleComponents); + + static void UpdateTransformParameters(UHoudiniHandleComponent* HandleComponent); + +}; +``` + +`Source/HoudiniEngine/Private/HoudiniInputTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniInputTranslator.h" + +#include "HCsgUtils.h" +#include "HoudiniApi.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniDataLayerUtils.h" +#include "HoudiniEngine.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineString.h" +#include "HoudiniInput.h" +#include "HoudiniInputObject.h" +#include "HoudiniMeshUtils.h" +#include "HoudiniOutputTranslator.h" +#include "HoudiniParameter.h" +#include "HoudiniParameterOperatorPath.h" +#include "HoudiniSplineComponent.h" +#include "HoudiniSplineTranslator.h" +#include "UnrealAnimationTranslator.h" +#include "UnrealBrushTranslator.h" +#include "UnrealDataTableTranslator.h" +#include "UnrealFoliageTypeTranslator.h" +#include "UnrealGeometryCollectionTranslator.h" +#include "UnrealInstanceTranslator.h" +#include "UnrealLandscapeSplineTranslator.h" +#include "UnrealLandscapeTranslator.h" +#include "UnrealLevelInstanceTranslator.h" +#include "UnrealMeshTranslator.h" +#include "UnrealObjectInputManager.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputRuntimeUtils.h" +#include "UnrealObjectInputTypes.h" +#if defined(HOUDINI_USE_PCG) +#include "UnrealPCGDataTranslator.h" +#include "UnrealObjectInputUtils.h" +#endif +#include "UnrealSkeletalMeshTranslator.h" +#include "UnrealSplineTranslator.h" +#include "UnrealTextureTranslator.h" + +#include "Animation/AnimSequence.h" +#include "Async/Async.h" +#include "Engine/StaticMesh.h" +#include "Engine/SkeletalMesh.h" +#include "Camera/CameraComponent.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "Components/SplineComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "Components/StaticMeshComponent.h" +#include "Engine/Brush.h" +#include "Engine/DataTable.h" +#include "Engine/SCS_Node.h" +#include "Engine/SimpleConstructionScript.h" +#include "FoliageType_InstancedStaticMesh.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniHLODLayerUtils.h" +#include "GeometryCollection/GeometryCollection.h" +#include "Landscape.h" +#include "LandscapeInfo.h" +#include "LandscapeSplinesComponent.h" +#include "UnrealObjectInputUtils.h" +#include "LevelInstance/LevelInstanceActor.h" +#include "PackedLevelActor/PackedLevelActor.h" +#include "UObject/TextProperty.h" +#if defined(HOUDINI_USE_PCG) +#include "PCGData.h" +#include "HoudiniPCGDataObject.h" +#endif + +#if WITH_EDITOR + #include "Editor.h" + #include "Editor/UnrealEdEngine.h" + #include "UnrealEdGlobals.h" +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + #include "MeshMerge/MeshMergingSettings.h" +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollection/GeometryCollectionComponent.h" + #include "GeometryCollection/GeometryCollectionObject.h" +#else + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionComponent.h" + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionObject.h" +#endif + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +#if WITH_EDITOR +// Allows checking of objects currently being dragged around +struct FHoudiniMoveTracker +{ + FHoudiniMoveTracker() : IsObjectMoving(false) + { + GEditor->OnBeginObjectMovement().AddLambda([this](UObject&) { IsObjectMoving = true; }); + GEditor->OnEndObjectMovement().AddLambda([this](UObject&) { IsObjectMoving = false; }); + + GEditor->OnActorsMoved().AddLambda([this](TArray&) { IsObjectMoving = false; }); + + GEditor->OnBeginCameraMovement().AddLambda([this](UObject&) { IsObjectMoving = false; }); + GEditor->OnEndCameraMovement().AddLambda([this](UObject&) { IsObjectMoving = false; }); + } + static FHoudiniMoveTracker& Get() { static FHoudiniMoveTracker Instance; return Instance; } + + bool IsObjectMoving; +}; +#endif + +bool +FHoudiniInputTranslator::BuildAllInputs( + HAPI_NodeId NodeId, + class UObject* InOuterObject, + TArray>& Inputs, + TArray>& Parameters) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::BuildAllInputs); + + // Ensure the asset has a valid node ID + if (NodeId < 0) + { + return false; + } + + // Start by getting the asset's info + HAPI_AssetInfo AssetInfo; + bool bAssetInfoSuccess = (HAPI_RESULT_SUCCESS == FHoudiniApi::GetAssetInfo( + FHoudiniEngine::Get().GetSession(), NodeId, &AssetInfo)); + + // Get the number of geo (SOP) inputs + // It's best to update the input count even if the hda hasnt cooked + // as it can cause loaded geo inputs to disappear upon loading the level + int32 InputCount = bAssetInfoSuccess ? AssetInfo.geoInputCount : 0; + + // Get the Asset NodeInfo + HAPI_NodeInfo AssetNodeInfo; + FHoudiniApi::NodeInfo_Init(&AssetNodeInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), NodeId, &AssetNodeInfo), false); + + // If the node is a COP node, look for the node info's input count + bool bIsCOPInput = false; + if (AssetNodeInfo.type == HAPI_NODETYPE_COP || AssetNodeInfo.type == HAPI_NODETYPE_COP2) + { + InputCount += AssetNodeInfo.inputCount; + bIsCOPInput = true; + } + + // Keep track of the number of node inputs (not counting params) + int32 NodeInputCount = InputCount; + + // Also look for object path parameters inputs + // Helper map to get the parameter index, given the parameter name + TMap ParameterNameToIndexMap; + TArray> InputParameters; + TArray InputParameterNames; + for (auto Param : Parameters) + { + if (!Param) + continue; + + if (Param->GetParameterType() == EHoudiniParameterType::Input) + { + int InsertionIndex = InputParameters.Num(); + ParameterNameToIndexMap.Add(Param->GetParameterName(), InsertionIndex); + InputParameters.Add(Param); + InputParameterNames.Add(Param->GetParameterName()); + } + } + + InputCount += InputParameters.Num(); + + // Append new inputs as needed + if (InputCount > Inputs.Num()) + { + int32 NumNewInputs = InputCount - Inputs.Num(); + for (int32 InputIdx = Inputs.Num(); InputIdx < InputCount; ++InputIdx) + { + FString InputObjectName = TEXT("Input") + FString::FromInt(InputIdx + 1); + UHoudiniInput * NewInput = NewObject< UHoudiniInput >( + InOuterObject, + UHoudiniInput::StaticClass(), + FName(*InputObjectName), + RF_Transactional); + + if (!IsValid(NewInput)) + { + //HOUDINI_LOG_WARNING("Failed to create asset input"); + continue; + } + // Create a default curve object here to avoid Transaction issue + //NewInput->CreateDefaultCurveInputObject(); + + Inputs.Add(NewInput); + } + } + else if (InputCount < Inputs.Num()) + { + // DO NOT DELETE PARAM INPUTS THAT ARE STILL PRESENT! + // This can cause issues with some input type when recooking the HDA after removing inputs! + // Make sure that we only delete inputs that are not present anymore! + for (int32 InputIdx = Inputs.Num() - 1; InputIdx >= 0; InputIdx--) + { + UHoudiniInput* CurrentInput = Inputs[InputIdx]; + if (IsValid(CurrentInput)) + { + // Do not delete a param input that is still present! + if (CurrentInput->IsObjectPathParameter() + && InputParameterNames.Contains(CurrentInput->GetInputName())) + continue; + + FHoudiniInputTranslator::DisconnectAndDestroyInput(CurrentInput, CurrentInput->GetInputType()); + + // DO NOT MANUALLY DESTROY THE OLD/DANGLING INPUTS! + // This messes up unreal's Garbage collection and would cause crashes on duplication + //CurrentInput->ConditionalBeginDestroy(); + //CurrentInput = nullptr; + } + + Inputs.RemoveAt(InputIdx); + + // Stop deleting inputs once we've removed enough + if (Inputs.Num() <= InputCount) + break; + } + + Inputs.SetNum(InputCount); + } + + // Input index -> InputParameter index + // Special values: -1 = SOP input. Ignore completely. -2 = To be determined later + // Used to preserve inputs after insertion/deletion + TArray InputIdxToInputParamIndex; + InputIdxToInputParamIndex.SetNum(Inputs.Num()); + + // Keep a set of used indices, to figure out the unused indices later + TSet UsedParameterIndices; + for (int32 InputIdx = 0; InputIdx < Inputs.Num(); InputIdx++) + { + // SOP input -> Parameter map doesn't make sense - ignore this + if (InputIdx < NodeInputCount) + { + // Ignore completely + InputIdxToInputParamIndex[InputIdx] = -1; + } + else + { + UHoudiniInput* CurrentInput = Inputs[InputIdx]; + if (!IsValid(CurrentInput)) + continue; + + if (ParameterNameToIndexMap.Contains(CurrentInput->GetInputName())) + { + const int32 ParameterIndex = ParameterNameToIndexMap[CurrentInput->GetInputName()]; + InputIdxToInputParamIndex[InputIdx] = ParameterIndex; + UsedParameterIndices.Add(ParameterIndex); + } + else + { + // To be determined in the second pass + InputIdxToInputParamIndex[InputIdx] = -2; + } + } + } + + // Second pass for InputIdxToInputParamIndex + // Fill in the inputs that could not be mapped onto old inputs. Used when inserting a new element. + for (int32 NewInputIndex = 0; NewInputIndex < Inputs.Num(); NewInputIndex++) + { + if (InputIdxToInputParamIndex[NewInputIndex] == -2) + { + // Find the first free index + for (int32 FreeIdx = 0; FreeIdx < InputParameters.Num(); FreeIdx++) + { + if (!UsedParameterIndices.Contains(FreeIdx)) + { + InputIdxToInputParamIndex[NewInputIndex] = FreeIdx; + UsedParameterIndices.Add(FreeIdx); + break; + } + } + } + } + + // Now, check the inputs in the array match the geo inputs + //for (int32 GeoInIdx = 0; GeoInIdx < NodeInputCount; GeoInIdx++) + bool bBlueprintStructureChanged = false; + for (int32 InputIdx = 0; InputIdx < Inputs.Num(); InputIdx++) + { + UHoudiniInput* CurrentInput = Inputs[InputIdx]; + if (!IsValid(CurrentInput)) + continue; + + // Create default Name/Label/Help + FString CurrentInputName = TEXT("Input") + FString::FromInt(InputIdx + 1); + FString CurrentInputLabel = CurrentInputName; + FString CurrentInputHelp; + + // Set the nodeId + CurrentInput->SetAssetNodeId(NodeId); + + // Is this an object path parameter input? + bool bIsObjectPath = InputIdx >= NodeInputCount; + if (!bIsObjectPath) + { + if (!bIsCOPInput) + { + // Mark this input as a SOP input + CurrentInput->SetSOPInput(InputIdx); + } + else + { + // Mark this input as a COP input + CurrentInput->SetCOPInput(InputIdx); + } + + // Get and set the name + HAPI_StringHandle InputStringHandle; + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetNodeInputName( + FHoudiniEngine::Get().GetSession(), + NodeId, InputIdx, &InputStringHandle)) + { + FHoudiniEngineString HoudiniEngineString(InputStringHandle); + HoudiniEngineString.ToFString(CurrentInputLabel); + } + } + else + { + // Get this input's parameter index in the objpath param array + int32 CurrentParmIdx = InputIdxToInputParamIndex[InputIdx]; + + UHoudiniParameter* CurrentParm = nullptr; + if (InputParameters.IsValidIndex(CurrentParmIdx)) + { + if (InputParameters[CurrentParmIdx].IsValid()) + CurrentParm = InputParameters[CurrentParmIdx].Get(); + } + + int32 ParmId = -1; + if (IsValid(CurrentParm)) + { + ParmId = CurrentParm->GetParmId(); + CurrentInputName = CurrentParm->GetParameterName(); + CurrentInputLabel = CurrentParm->GetParameterLabel(); + CurrentInputHelp = CurrentParm->GetParameterHelp(); + } + + UHoudiniParameterOperatorPath* CurrentObjPathParm = Cast(CurrentParm); + if (IsValid(CurrentObjPathParm)) + { + CurrentObjPathParm->HoudiniInput = CurrentInput; + } + + // Mark this input as an object path parameter input + CurrentInput->SetObjectPathParameter(ParmId, bIsCOPInput); + } + + CurrentInput->SetName(CurrentInputName); + CurrentInput->SetLabel(CurrentInputLabel); + + if ( CurrentInputHelp.IsEmpty() ) + { + CurrentInputHelp = CurrentInputLabel + TEXT("(") + CurrentInputName + TEXT(")"); + } + CurrentInput->SetHelp(CurrentInputHelp); + + // If the input type is invalid, + // We need to initialize its default + if (CurrentInput->GetInputType() == EHoudiniInputType::Invalid) + { + // Initialize it to the default corresponding to its name + CurrentInput->SetInputType(GetDefaultInputTypeFromLabel(CurrentInputLabel), bBlueprintStructureChanged); + + // Preset the default HDA for objpath input + SetDefaultInputFromParameterValue(CurrentInput, bBlueprintStructureChanged); + } + + // Update input objects data on UE side for all types of inputs. + switch (CurrentInput->GetInputType()) + { + case EHoudiniInputType::Curve: + FHoudiniSplineTranslator::UpdateHoudiniInputCurves(CurrentInput); + break; + case EHoudiniInputType::Geometry: + break; + case EHoudiniInputType::World: + break; + default: + break; + } + } + + return true; +} + +bool +FHoudiniInputTranslator::DisconnectInput(UHoudiniInput* InputToDestroy, const EHoudiniInputType& InputType) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::DisconnectInput); + + if (!IsValid(InputToDestroy)) + return false; + + // Start by disconnecting the input / nullifying the object path parameter + if (InputToDestroy->IsObjectPathParameter()) + { + // Just set the objpath parameter to null + FHoudiniApi::SetParmStringValue( + FHoudiniEngine::Get().GetSession(), + InputToDestroy->GetAssetNodeId(), "", + InputToDestroy->GetParameterId(), 0); + } + else + { + // Get the asset / created input node ID + HAPI_NodeId HostAssetId = InputToDestroy->GetAssetNodeId(); + HAPI_NodeId CreatedInputId = InputToDestroy->GetInputNodeId(); + + // Only disconnect if both are valid + if (HostAssetId >= 0 && CreatedInputId >= 0) + { + FHoudiniApi::DisconnectNodeInput( + FHoudiniEngine::Get().GetSession(), + HostAssetId, InputToDestroy->GetInputIndex()); + } + } + + return true; +} + +bool +FHoudiniInputTranslator::DestroyInputNodes(UHoudiniInput* InputToDestroy, const EHoudiniInputType& InputType) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::DestroyInputNodes); + + if (!IsValid(InputToDestroy)) + return false; + + if (!InputToDestroy->CanDeleteHoudiniNodes()) + return false; + + // When using the new input system, get all HAPI NodeIds managed by the system as a set. Do not delete any nodes + // here if their ids are in the set. The manager will handle deletion of those nodes when needed. + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + TSet ManagedNodeIdSet; + if (Manager) + { + TArray ManagedNodeIds; + Manager->GetAllHAPINodeIds(ManagedNodeIds); + ManagedNodeIdSet.Append(ManagedNodeIds); + } + + // Destroy the nodes created by all the input objects + TArray CreatedInputDataAssetIds = InputToDestroy->GetCreatedDataNodeIds(); + TArray>* InputObjectNodes = InputToDestroy->GetHoudiniInputObjectArray(InputType); + if (InputObjectNodes) + { + TArray ManagedHAPINodeIds; + for (auto CurInputObject : *InputObjectNodes) + { + if (!IsValid(CurInputObject)) + continue; + + if (CurInputObject->Type == EHoudiniInputObjectType::HoudiniAssetComponent || CurInputObject->Type == EHoudiniInputObjectType::HoudiniCookable) + { + // Houdini Asset Input, we don't want to destroy / invalidate the input HDA! + // Just remove this input object's node Id from the CreatedInputDataAssetIds array + // to avoid its deletion further down + const int32 InputNodeId = CurInputObject->GetInputNodeId(); + if (InputNodeId >= 0) + CreatedInputDataAssetIds.Remove(InputNodeId); + //CurInputObject->InputNodeId = -1; + //CurInputObject->InputObjectNodeId = -1; + continue; + } + + // For Actor/BP input objects, set the input node id for all component objects to -1, + if (CurInputObject->Type == EHoudiniInputObjectType::Actor || CurInputObject->Type == EHoudiniInputObjectType::Blueprint) + { + UHoudiniInputActor* CurActorInputObject = Cast(CurInputObject); + UHoudiniInputBlueprint* CurBPInputObject = Cast(CurInputObject); + if (CurActorInputObject || CurBPInputObject) + { + for (auto & CurComponent : CurActorInputObject ? CurActorInputObject->GetActorComponents() : CurBPInputObject->GetComponents()) + { + if (!IsValid(CurComponent)) + continue; + + if (!CurComponent->CanDeleteHoudiniNodes()) + { + const int32 InputNodeId = CurComponent->GetInputNodeId(); + if (InputNodeId >= 0) + CreatedInputDataAssetIds.Remove(InputNodeId); + const int32 InputObjectNodeId = CurComponent->GetInputObjectNodeId(); + if (InputObjectNodeId >= 0) + CreatedInputDataAssetIds.Remove(InputObjectNodeId); + continue; + } + + // No need to delete the nodes created for an asset component manually here, + // As they will be deleted when we clean up the CreateNodeIds array + CurComponent->SetInputNodeId(-1); + } + } + } + // No need to delete the nodes created for an asset component manually here, + // As they will be deleted when we clean up the CreateNodeIds array + + const int32 InputNodeId = CurInputObject->GetInputNodeId(); + if (InputNodeId >= 0 && !ManagedNodeIdSet.Contains(InputNodeId)) + { + FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), InputNodeId); + CurInputObject->SetInputNodeId(-1); + } + + const int32 InputObjectNodeId = CurInputObject->GetInputObjectNodeId(); + if(InputObjectNodeId >= 0 && !ManagedNodeIdSet.Contains(InputObjectNodeId)) + { + FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), InputObjectNodeId); + CurInputObject->SetInputObjectNodeId(-1); + + // TODO: CHECK ME! + //HAPI_NodeId ParentNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(CurInputObject->InputObjectNodeId); + + //// Delete its parent node as well + //if (FHoudiniEngineUtils::IsHoudiniNodeValid(ParentNodeId)) + // FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), ParentNodeId); + } + // Also directly invalidate HoudiniSplineComponent's node IDs. + UHoudiniInputHoudiniSplineComponent* HoudiniSplineInputObject = Cast(CurInputObject); + if (IsValid(HoudiniSplineInputObject) && !IsGarbageCollecting()) + { + UHoudiniSplineComponent* SplineComponent = HoudiniSplineInputObject->GetCurveComponent(); + if (IsValid(SplineComponent)) + { + SplineComponent->SetNodeId(-1); + } + } + + CurInputObject->MarkChanged(true); + } + } + + // Destroy all the input assets + for (HAPI_NodeId AssetNodeId : CreatedInputDataAssetIds) + { + if (AssetNodeId < 0 || ManagedNodeIdSet.Contains(AssetNodeId)) + continue; + + FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), AssetNodeId); + } + CreatedInputDataAssetIds.Empty(); + + // Then simply destroy the input's parent OBJ node + if (InputToDestroy->GetInputNodeId() >= 0) + { + HAPI_NodeId CreatedInputId = InputToDestroy->GetInputNodeId(); + HAPI_NodeId ParentId = FHoudiniEngineUtils::HapiGetParentNodeId(CreatedInputId); + + if (CreatedInputId >= 0) + { + FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), CreatedInputId); + InputToDestroy->SetInputNodeId(-1); + } + + if (FHoudiniEngineUtils::IsHoudiniNodeValid(ParentId)) + { + FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), ParentId); + } + } + + return true; +} + +bool +FHoudiniInputTranslator::DisconnectAndDestroyInput(UHoudiniInput* InputToDestroy, const EHoudiniInputType& InputType) +{ + // Start by disconnecting the input/object merge + bool bSuccess = DisconnectInput(InputToDestroy, InputType); + + // Then destroy the created input nodes + bSuccess &= DestroyInputNodes(InputToDestroy, InputType); + + return bSuccess; +} + + +EHoudiniInputType +FHoudiniInputTranslator::GetDefaultInputTypeFromLabel(const FString& InputName) +{ + // We'll try to find these magic words to try to detect the default input type + //FString geoPrefix = TEXT("geo"); + FString curvePrefix = TEXT("curve"); + + FString landscapePrefix = TEXT("landscape"); + FString landscapePrefix2 = TEXT("terrain"); + FString landscapePrefix3 = TEXT("heightfield"); + + FString worldPrefix = TEXT("world"); + FString worldPrefix2 = TEXT("outliner"); + + FString assetPrefix = TEXT("asset"); + FString assetPrefix2 = TEXT("hda"); + + // By default, geometry input is chosen. + EHoudiniInputType InputType = EHoudiniInputType::Geometry; + + if (InputName.Contains(curvePrefix, ESearchCase::IgnoreCase)) + InputType = EHoudiniInputType::Curve; + + else if ((InputName.Contains(landscapePrefix, ESearchCase::IgnoreCase)) + || (InputName.Contains(landscapePrefix2, ESearchCase::IgnoreCase)) + || (InputName.Contains(landscapePrefix3, ESearchCase::IgnoreCase))) + InputType = EHoudiniInputType::World; // Landscape; + + else if ((InputName.Contains(worldPrefix, ESearchCase::IgnoreCase)) + || (InputName.Contains(worldPrefix2, ESearchCase::IgnoreCase))) + InputType = EHoudiniInputType::World; + + else if ((InputName.Contains(assetPrefix, ESearchCase::IgnoreCase)) + || (InputName.Contains(assetPrefix2, ESearchCase::IgnoreCase))) + InputType = EHoudiniInputType::World; // Asset; + + return InputType; +} + +bool +FHoudiniInputTranslator::ChangeInputType(UHoudiniInput* InInput, const bool& bForce) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::ChangeInputType); + + if (!IsValid(InInput)) + return false; + + if (!InInput->HasInputTypeChanged() && !bForce) + return true; + + // - Handle switching AWAY from an input type + DisconnectAndDestroyInput(InInput, InInput->GetPreviousInputType()); + + // Invalidate the previous input type now that we've actually changed + //InInput->SetPreviousInputType(EHoudiniInputType::Invalid); + + //ChangeInputType(InInput, NewType); + + // TODO: + // - Handle updating to the new input type + // downstream asset connection, static mesh update, curve creation... + + // Mark all the objects from this input has changed so they upload themselves + InInput->MarkAllInputObjectsChanged(true); + + return true; +} + +bool +FHoudiniInputTranslator::SetDefaultInputFromParameterValue(UHoudiniInput* Input, bool& bOutBlueprintStructureModified) +{ + // + if (!IsValid(Input)) + return false; + + // Make sure we're linked to a valid object path parameter + if (Input->GetParameterId() < 0) + return false; + + // Get our ParmInfo + HAPI_ParmInfo FoundParamInfo; + FHoudiniApi::ParmInfo_Init(&FoundParamInfo); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetParmInfo( + FHoudiniEngine::Get().GetSession(), + Input->GetAssetNodeId(), Input->GetParameterId(), &FoundParamInfo)) + { + return false; + } + + // Get our string value + HAPI_StringHandle StringHandle; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetParmStringValues( + FHoudiniEngine::Get().GetSession(), + Input->GetAssetNodeId(), + false, + &StringHandle, + FoundParamInfo.stringValuesIndex, + 1)) + { + return false; + } + + FString ParamValue; + FHoudiniEngineString HoudiniEngineString(StringHandle); + if (!HoudiniEngineString.ToFString(ParamValue)) + { + return false; + } + + if (ParamValue.Len() <= 0) + { + return false; + } + + // Chop the default value using semi-colons as separators + TArray Tokens; + ParamValue.ParseIntoArray(Tokens, TEXT(";"), true); + + // Start by setting geometry input objects + int32 GeoIdx = 0; + for (auto& CurToken : Tokens) + { + if (CurToken.IsEmpty()) + continue; + + // Set default objects on the HDA instance - will override the parameter string + // and apply the object input local-path thing for the HDA cook. + UObject * pObject = LoadObject(nullptr, *CurToken); + if (!pObject) + continue; + + Input->SetInputObjectAt(EHoudiniInputType::Geometry, GeoIdx++, pObject); + } + + if (IsValid(Input->GetWorld())) + { + // See if we can preset world objects as well + int32 WorldIdx = 0; + for(TActorIterator ActorIt(Input->GetWorld(), AActor::StaticClass(), EActorIteratorFlags::SkipPendingKill); ActorIt; ++ActorIt) + { + AActor* CurActor = *ActorIt; + if(!CurActor) + continue; + + AActor* FoundActor = nullptr; + int32 FoundIdx = Tokens.Find(CurActor->GetFName().ToString()); + if(FoundIdx == INDEX_NONE) + FoundIdx = Tokens.Find(CurActor->GetActorLabel()); + + if(FoundIdx != INDEX_NONE) + FoundActor = CurActor; + + if(!FoundActor) + continue; + + // Select the found actor in the world input + Input->SetInputObjectAt(EHoudiniInputType::World, WorldIdx++, FoundActor); + + // Remove the Found Token + Tokens.RemoveAt(FoundIdx); + } + + // See if we should change the default input type + if(Input->GetInputType() == EHoudiniInputType::Geometry && WorldIdx > 0 && GeoIdx == 0) + { + // Can just set the input type NewWorld Input Type + Input->SetInputType(EHoudiniInputType::World, bOutBlueprintStructureModified); + } + } + + return true; +} + +bool +FHoudiniInputTranslator::UploadChangedInputs( + TArray>& InInputs, + AActor* InOwnerActor) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::UploadChangedInputs); + + for(int32 InputIdx = 0; InputIdx < InInputs.Num(); InputIdx++) + { + TObjectPtr& CurrentInput = InInputs[InputIdx]; + if (!IsValid(CurrentInput) || !CurrentInput->HasChanged()) + continue; + + // Delete any previous InputNodeIds of this HoudiniInput that are pending delete + for (const HAPI_NodeId InputNodeIdPendingDelete : CurrentInput->GetInputNodesPendingDelete()) + { + if (InputNodeIdPendingDelete < 0) + continue; + + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), InputNodeIdPendingDelete, &NodeInfo)) + { + continue; + } + + HAPI_NodeId NodeToDelete = InputNodeIdPendingDelete; + if (NodeInfo.type == HAPI_NODETYPE_SOP) + { + // Input nodes are Merge SOPs in a geo object, delete the geo object + const HAPI_NodeId ParentId = FHoudiniEngineUtils::HapiGetParentNodeId(InputNodeIdPendingDelete); + NodeToDelete = ParentId != -1 ? ParentId : InputNodeIdPendingDelete; + } + + HOUDINI_CHECK_ERROR(FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), NodeToDelete)); + } + CurrentInput->ClearInputNodesPendingDelete(); + + // First thing, see if we need to change the input type + if (CurrentInput->HasInputTypeChanged()) + { + ChangeInputType(CurrentInput, false); + } + + if ((CurrentInput->IsLandscapeInput()) + && CurrentInput->HasLandscapeExportTypeChanged()) + { + DisconnectAndDestroyInput(CurrentInput, CurrentInput->GetInputType()); + CurrentInput->MarkAllInputObjectsChanged(true); + CurrentInput->SetHasLandscapeExportTypeChanged(false); + } + + bool bSuccess = true; + if (CurrentInput->IsDataUploadNeeded()) + { + FTransform OwnerTransform = FTransform::Identity; + if (InOwnerActor) + { + OwnerTransform = InOwnerActor->GetTransform(); + } + + bSuccess &= UploadInputData(CurrentInput, OwnerTransform); + CurrentInput->MarkDataUploadNeeded(!bSuccess); + } + + if (CurrentInput->IsTransformUploadNeeded()) + { + bSuccess &= UploadInputTransform(CurrentInput); + } + + // Update the input properties AFTER eventually uploading it + bSuccess = UpdateInputProperties(CurrentInput); + + if (bSuccess) + { + CurrentInput->MarkChanged(false); + CurrentInput->MarkAllInputObjectsChanged(false); + } + + if (CurrentInput->HasInputTypeChanged()) + CurrentInput->SetPreviousInputType(EHoudiniInputType::Invalid); + + // Even if we failed, no need to try updating again. + CurrentInput->SetNeedsToTriggerUpdate(false); + } + + return true; +} + +bool +FHoudiniInputTranslator::UpdateInputProperties(UHoudiniInput* InInput) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::UpdateInputProperties); + + bool bSucess = UpdateTransformType(InInput); + + bSucess &= UpdatePackBeforeMerge(InInput); + + bSucess &= UpdateTransformOffset(InInput); + + return bSucess; +} + +bool +FHoudiniInputTranslator::UpdateTransformType(UHoudiniInput* InInput) +{ + if (!IsValid(InInput)) + return false; + + bool nTransformType = InInput->GetKeepWorldTransform(); + + // Geometry inputs are always set to none + EHoudiniInputType InputType = InInput->GetInputType(); + if (InputType == EHoudiniInputType::Geometry) + nTransformType = 0; + + // Get the Input node ID from the host ID + HAPI_NodeId InputNodeId = -1; + HAPI_NodeId HostAssetId = InInput->GetAssetNodeId(); + + bool bSuccess = true; + const std::string sXformType = "xformtype"; + if (InInput->IsObjectPathParameter()) + { + // Directly change the Parameter xformtype + // (This will only work if the object merge is editable/unlocked) + if (HAPI_RESULT_SUCCESS != FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), + HostAssetId, sXformType.c_str(), 0, nTransformType)) + bSuccess = false; + } + else + { + // Query the object merge's node ID via the input + if (HAPI_RESULT_SUCCESS == FHoudiniApi::QueryNodeInput( + FHoudiniEngine::Get().GetSession(), + HostAssetId, InInput->GetInputIndex(), &InputNodeId)) + { + // Change its Parameter xformtype + if (HAPI_RESULT_SUCCESS != FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), + InputNodeId, sXformType.c_str(), 0, nTransformType)) + bSuccess = false; + } + } + + // Since our input objects are all plugged into a merge node + // We want to also update the transform type on the object merge plugged into the merge node + // TODO: Also do it for Geo IN with multiple objects? or BP? + HAPI_NodeId ParentNodeId = InInput->GetInputNodeId(); + if ((ParentNodeId >= 0) + && (InputType != EHoudiniInputType::Geometry)) + { + HAPI_NodeId InputObjectNodeId = -1; + int32 NumberOfInputMeshes = InInput->GetNumberOfInputMeshes(InputType); + for (int n = 0; n < NumberOfInputMeshes; n++) + { + // Get the Input node ID from the host ID + InputObjectNodeId = -1; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::QueryNodeInput( + FHoudiniEngine::Get().GetSession(), + ParentNodeId, n, &InputObjectNodeId)) + continue; + + if (InputObjectNodeId == -1) + continue; + + // Change the xformtype parameter on the object merge + if (HAPI_RESULT_SUCCESS != FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), InputObjectNodeId, + sXformType.c_str(), 0, nTransformType)) + bSuccess = false; + } + } + + return bSuccess; +} + +bool +FHoudiniInputTranslator::UpdatePackBeforeMerge(UHoudiniInput* InInput) +{ + if (!IsValid(InInput)) + return false; + + // Pack before merge is only available for Geo/World input + EHoudiniInputType InputType = InInput->GetInputType(); + if (InputType != EHoudiniInputType::World + && InputType != EHoudiniInputType::Geometry) + { + // Nothing to change + return true; + } + + uint32 nPackValue = InInput->GetPackBeforeMerge() ? 1 : 0; + + // Get the Input node ID from the host ID + HAPI_NodeId HostAssetId = InInput->GetAssetNodeId(); + + bool bSuccess = true; + const std::string sPack = "pack"; + const std::string sPivot = "pivot"; + + // We'll be going through each input object plugged in the input's merge node + // and change the pack parameter there + HAPI_NodeId ParentNodeId = InInput->GetInputNodeId(); + if (ParentNodeId >= 0) + { + HAPI_NodeId InputObjectNodeId = -1; + int32 NumberOfInputMeshes = InInput->GetNumberOfInputMeshes(InputType); + for (int n = 0; n < NumberOfInputMeshes; n++) + { + // Get the Input node ID from the host ID + InputObjectNodeId = -1; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::QueryNodeInput( + FHoudiniEngine::Get().GetSession(), + ParentNodeId, n, &InputObjectNodeId)) + continue; + + if (InputObjectNodeId == -1) + continue; + + // Change the pack parameter on the object merge + if (HAPI_RESULT_SUCCESS != FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), InputObjectNodeId, + sPack.c_str(), 0, nPackValue)) + bSuccess = false; + + // Change the pivot parameter on the object merge to "origin" + if (HAPI_RESULT_SUCCESS != FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), InputObjectNodeId, + sPivot.c_str(), 0, 0)) + bSuccess = false; + } + } + + return bSuccess; +} + +bool +FHoudiniInputTranslator::UpdateTransformOffset(UHoudiniInput* InInput) +{ + if (!IsValid(InInput)) + return false; + + // Transform offsets are only for geometry inputs + EHoudiniInputType InputType = InInput->GetInputType(); + if (InputType != EHoudiniInputType::Geometry) + return true; + + // Get the input objects + TArray>* InputObjectsArray = InInput->GetHoudiniInputObjectArray(InputType); + if (!ensure(InputObjectsArray)) + return false; + + // Update each object's transform offset + for (int32 ObjIdx = 0; ObjIdx < InputObjectsArray->Num(); ObjIdx++) + { + UHoudiniInputObject* CurrentInputObject = (*InputObjectsArray)[ObjIdx]; + if (!IsValid(CurrentInputObject)) + continue; + + // If the Input mesh has a Transform offset + FTransform TransformOffset = CurrentInputObject->GetHoudiniObjectTransform(); + + // Updating the Transform + HAPI_TransformEuler HapiTransform; + FHoudiniApi::TransformEuler_Init(&HapiTransform); + FHoudiniEngineUtils::TranslateUnrealTransform(TransformOffset, HapiTransform); + + // Set the transform on the OBJ parent + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + FHoudiniEngine::Get().GetSession(), CurrentInputObject->GetInputObjectNodeId(), &HapiTransform), false); + } + + return true; +} + +bool +FHoudiniInputTranslator::UploadInputData(UHoudiniInput* InInput, const FTransform & InActorTransform) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::UploadInputData); + + if (!IsValid(InInput)) + return false; + + EHoudiniInputType InputType = InInput->GetInputType(); + TArray>* InputObjectsArray = InInput->GetHoudiniInputObjectArray(InInput->GetInputType()); + if (!ensure(InputObjectsArray)) + return false; + + // When using the ref counted input system we use the UpdateScope to record all input nodes in the input + // manager that are updated/created. Afterwards we look for all reference nodes and update/fix any objmerge + // paths that are out of date (depending on node creation/deletion order, new nodes could use the same names + // but now have a numeric suffix). The scope registers itself with the manager on construction and is then notified + // of each entry in the manager that is created, updated or deleted. On destruction the scope unregisters itself + // from the manager. + FUnrealObjectInputUpdateScope UpdateScope; + + // Iterate on all the input objects and see if they need to be uploaded + bool bSuccess = true; + TArray CreatedNodeIds; + TSet Handles; + TArray ValidNodeIds; + TArray ChangedInputObjects; + for (int32 ObjIdx = 0; ObjIdx < InputObjectsArray->Num(); ObjIdx++) + { + UHoudiniInputObject* CurrentInputObject = (*InputObjectsArray)[ObjIdx]; + if (!IsValid(CurrentInputObject)) + continue; + + ValidNodeIds.Reset(); + ChangedInputObjects.Reset(); + // The input object could have child objects: GetChangedObjectsAndValidNodes finds if the object itself or + // any its children has changed, and also returns the NodeIds of those objects that are still valid and + // unchanged + CurrentInputObject->GetChangedObjectsAndValidNodes(ChangedInputObjects, ValidNodeIds); + + // Keep track of the node ids for unchanged objects that already exist + if (ValidNodeIds.Num() > 0) + CreatedNodeIds.Append(ValidNodeIds); + + // Upload the changed input objects + for (UHoudiniInputObject* ChangedInputObject : ChangedInputObjects) + { + // Upload the current input object to Houdini + if (!UploadHoudiniInputObject(InInput, ChangedInputObject, InActorTransform, CreatedNodeIds, Handles, ChangedInputObject->CanDeleteHoudiniNodes())) + bSuccess = false; + } + } + + // When using the ref counted input system, update objmerge paths in reference nodes that are potentially out of + // date after the update above + { + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (Manager) + { + // Get nodes that were created / update from the UpdateScope + TSet UpdatedNodes(UpdateScope.GetNodesCreatedOrUpdated()); + TSet ProcessedNodes(UpdatedNodes); + TSet ReferencedBy; + for (const FUnrealObjectInputIdentifier& Identifier : UpdatedNodes) + { + ReferencedBy.Reset(); + // Look for all reference nodes that reference this node + Manager->GetReferencedBy(Identifier, ReferencedBy); + for (const FUnrealObjectInputIdentifier& RefToUpdate : ReferencedBy) + { + if (ProcessedNodes.Contains(RefToUpdate)) + continue; + ProcessedNodes.Add(RefToUpdate); + + if (RefToUpdate.GetNodeType() != EUnrealObjectInputNodeType::Reference) + continue; + + FUnrealObjectInputUtils::ConnectReferencedNodesToMerge(RefToUpdate); + } + } + } + } + + // If we haven't created any input, invalidate our input node id + if (CreatedNodeIds.Num() == 0) + { + if (!InInput->HasInputTypeChanged()) + { + int32 InputNodeId = InInput->GetInputNodeId(); + TArray PreviousInputObjectNodeIds = InInput->GetCreatedDataNodeIds(); + + // TODO: CHECK THIS - remive ? + //if (InInput->GetInputType() == EHoudiniInputType::Asset_DEPRECATED) + if (InInput->IsAssetInput()) + { + UHoudiniCookable * OuterHC = Cast(InInput->GetOuter()); + HAPI_NodeId OuterNodeId = OuterHC->GetNodeId(); + + // Disconnect the asset input + if (InputNodeId >= 0 && InInput->GetInputIndex() >= 0) + { + HOUDINI_CHECK_ERROR(FHoudiniApi::DisconnectNodeInput( + FHoudiniEngine::Get().GetSession(), OuterNodeId, InInput->GetInputIndex())); + } + } + else if (InInput->GetInputType() == EHoudiniInputType::World) + { + // World nodes are handled by InputObjects () (with FHoudiniEngineRuntime::Get().MarkNodeIdAsPendingDelete) + } + else + { + if (InputNodeId >= 0) + { + for (int32 Idx = PreviousInputObjectNodeIds.Num() - 1; Idx >= 0; --Idx) + { + + // Get the object merge connected to the merge node + HAPI_NodeId InputObjectMergeId = -1; + HOUDINI_CHECK_ERROR(FHoudiniApi::QueryNodeInput( + FHoudiniEngine::Get().GetSession(), InputNodeId, Idx, &InputObjectMergeId)); + + // Disconnect the two nodes + HOUDINI_CHECK_ERROR(FHoudiniApi::DisconnectNodeInput( + FHoudiniEngine::Get().GetSession(), InputNodeId, Idx)); + + // Destroy the object merge node, do not delete other HDA (Asset input type) + HOUDINI_CHECK_ERROR(FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), InputObjectMergeId)); + } + } + } + } + InInput->GetCreatedDataNodeIds().Empty(); + InInput->SetInputNodeId(-1); + return bSuccess; + } + + // Get the current input's NodeId + HAPI_NodeId InputNodeId = InInput->GetInputNodeId(); + // Check that the current input's node ID is still valid + if (InputNodeId < 0 || !FHoudiniEngineUtils::IsHoudiniNodeValid(InputNodeId)) + { + if (!InInput->IsCOPInput()) + { + // This input doesn't have a valid NodeId yet, + // we need to create this input's merge node and update this input's node ID + FString MergeName = InInput->GetNodeBaseName() + TEXT("_Merge"); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + -1, TEXT("SOP/merge"), MergeName, true, &InputNodeId), false); + } + else + { + // This input doesn't have a valid NodeId yet, + // we need to create this input's merge node and update this input's node ID + // We use a cable pack node for COPs + + // Create the node and wait for the ready status + FString MergeName = InInput->GetNodeBaseName() + TEXT("_Merge"); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + -1, TEXT("COP/cablepack"), MergeName, true, &InputNodeId), false); + } + + InInput->SetInputNodeId(InputNodeId); + } + + //TODO: + // Do we want to update the input's transform? + if (false) + { + FTransform ComponentTransform = FTransform::Identity; + UHoudiniCookable* OuterHC = Cast(InInput->GetOuter()); + USceneComponent* OuterComp = OuterHC ? OuterHC->GetComponent() : Cast(InInput->GetOuter()); + if (IsValid(OuterComp)) + ComponentTransform = OuterComp->GetComponentTransform(); + + FHoudiniEngineUtils::HapiSetAssetTransform(InputNodeId, ComponentTransform); + } + + // Connect all the input objects to the merge node now + int32 InputIndex = 0; + for (auto CurrentNodeId : CreatedNodeIds) + { + if (CurrentNodeId < 0) + continue; + + if (InputNodeId == CurrentNodeId) + continue; + + // Connect the current input object to the merge node + HOUDINI_CHECK_ERROR(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + InputNodeId, InputIndex++, CurrentNodeId, 0)); + } + + // Check if we need to disconnect extra input objects nodes from the merge + // This can be needed when the input had more input objects on the previous cook + TArray& PreviousInputObjectNodeIds = InInput->GetCreatedDataNodeIds(); + if (!InInput->HasInputTypeChanged()) + { + for (int32 Idx = PreviousInputObjectNodeIds.Num() - 1; Idx >= CreatedNodeIds.Num(); --Idx) + { + // Get the object merge connected to the merge node + HAPI_NodeId InputObjectMergeId = -1; + + // TODO: CHECK THIS - remove if?! + //if (InInput->GetInputType() != EHoudiniInputType::Asset_DEPRECATED) + if (!InInput->IsAssetInput()) + HOUDINI_CHECK_ERROR(FHoudiniApi::QueryNodeInput( + FHoudiniEngine::Get().GetSession(), InputNodeId, Idx, &InputObjectMergeId)); + + // Disconnect the two nodes + HOUDINI_CHECK_ERROR(FHoudiniApi::DisconnectNodeInput( + FHoudiniEngine::Get().GetSession(), InputNodeId, Idx)); + + // TODO: CHECK THIS - remove if?! + // Destroy the object merge node, do not destroy other HDA (Asset input type) + //if (InInput->GetInputType() != EHoudiniInputType::Asset_DEPRECATED) + if (!InInput->IsAssetInput()) + { + HOUDINI_CHECK_ERROR(FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), InputObjectMergeId)); + } + } + } + + // Keep track of all the nodes plugged into our input's merge + PreviousInputObjectNodeIds = CreatedNodeIds; + + // Finally, connect our main input node to the asset + bSuccess = ConnectInputNode(InInput); + + return bSuccess; +} + +bool +FHoudiniInputTranslator::UploadInputTransform(UHoudiniInput* InInput) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::UploadInputTransform); + + if (!IsValid(InInput)) + return false; + + EHoudiniInputType InputType = InInput->GetInputType(); + TArray>* InputObjectsArray = InInput->GetHoudiniInputObjectArray(InInput->GetInputType()); + if (!ensure(InputObjectsArray)) + return false; + + // Iterate on all the input objects and see if their transform needs to be uploaded + bool bSuccess = true; + for (int32 ObjIdx = 0; ObjIdx < InputObjectsArray->Num(); ObjIdx++) + { + UHoudiniInputObject* CurrentInputObject = (*InputObjectsArray)[ObjIdx]; + if (!IsValid(CurrentInputObject)) + continue; + + if (!CurrentInputObject->HasTransformChanged()) + continue; + + // Upload the current input object's transform to Houdini + if (!UploadHoudiniInputTransform(InInput, CurrentInputObject)) + { + bSuccess = false; + continue; + } + } + + return bSuccess; +} + +bool +FHoudiniInputTranslator::ConnectInputNode(UHoudiniInput* InInput) +{ + if (!IsValid(InInput)) + return false; + + HAPI_NodeId AssetNodeId = InInput->GetAssetNodeId(); + if (AssetNodeId < 0) + return false; + + HAPI_NodeId InputNodeId = InInput->GetInputNodeId(); + if (InputNodeId < 0) + return false; + + // Helper for connecting our input or setting the object path parameter + if (InInput->IsObjectPathParameter()) + { + // Now we can assign the input node path to the parameter + std::string ParamNameString = H_TCHAR_TO_UTF8(*(InInput->GetInputName())); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmNodeValue( + FHoudiniEngine::Get().GetSession(), AssetNodeId, + ParamNameString.c_str(), InputNodeId), false); + } + else + { + // TODO: CHECK ME! + //if (!FHoudiniEngineUtils::IsHoudiniNodeValid(InputNodeId)) + // return false; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), AssetNodeId, + InInput->GetInputIndex(), InputNodeId, 0), false); + } + + return true; +} + +bool +FHoudiniInputTranslator::UploadHoudiniInputObject( + UHoudiniInput* InInput, + UHoudiniInputObject* InInputObject, + const FTransform& InActorTransform, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::UploadHoudiniInputObject); + + if (!InInput || !InInputObject) + return false; + + FString ObjBaseName = InInput->GetNodeBaseName(); + + FHoudiniInputObjectSettings InputSettings(InInput); + + bool bSuccess = true; + switch (InInputObject->Type) + { + case EHoudiniInputObjectType::Object: + { + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForObject(ObjBaseName, InInputObject); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::StaticMesh: + { + UHoudiniInputStaticMesh* InputSM = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForStaticMesh( + ObjBaseName, + InputSM, + InputSettings, + bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::SkeletalMesh: + { + UHoudiniInputSkeletalMesh* InputSkelMesh = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForSkeletalMesh( + ObjBaseName, + InputSkelMesh, + InputSettings, + bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::Animation: + { + UHoudiniInputAnimation* InputAnimation = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForAnimation( + ObjBaseName, + InputAnimation, + InputSettings, + bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::SkeletalMeshComponent: + { + UHoudiniInputSkeletalMeshComponent* InputSKC = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForSkeletalMeshComponent( + ObjBaseName, + InputSKC, + InputSettings, + bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + case EHoudiniInputObjectType::GeometryCollection: + { + UHoudiniInputGeometryCollection* InputGeometryCollection = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForGeometryCollection( + ObjBaseName, + InputGeometryCollection, + InputSettings, + bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + case EHoudiniInputObjectType::GeometryCollectionComponent: + { + UHoudiniInputGeometryCollectionComponent* InputGeometryCollection = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForGeometryCollectionComponent( + ObjBaseName, + InputGeometryCollection, + InputSettings, + bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + case EHoudiniInputObjectType::SceneComponent: + { + UHoudiniInputSceneComponent* InputSceneComp = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForSceneComponent( + ObjBaseName, + InputSceneComp, + InputSettings, + bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::StaticMeshComponent: + { + UHoudiniInputMeshComponent* InputSMC = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForStaticMeshComponent( + ObjBaseName, + InputSMC, + InputSettings, + bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::InstancedStaticMeshComponent: + { + UHoudiniInputInstancedMeshComponent* InputISMC = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForInstancedStaticMeshComponent( + ObjBaseName, + InputISMC, + InputSettings, + bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::SplineComponent: + { + UHoudiniInputSplineComponent* InputSpline = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForSplineComponent( + ObjBaseName, + InputSpline, + InputSettings, + bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::HoudiniSplineComponent: + { + UHoudiniInputHoudiniSplineComponent* InputCurve = Cast(InInputObject); + + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForHoudiniSplineComponent( + ObjBaseName, + InputCurve, + InputSettings); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::HoudiniAssetActor: + case EHoudiniInputObjectType::HoudiniAssetComponent: + case EHoudiniInputObjectType::HoudiniCookable: + { + UHoudiniInputHoudiniAsset* InputHAC = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForHoudiniCookable( + ObjBaseName, + InputHAC, + InputSettings); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::Actor: + case EHoudiniInputObjectType::GeometryCollectionActor_Deprecated: + case EHoudiniInputObjectType::LandscapeSplineActor: + { + UHoudiniInputActor* InputActor = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForActor( + InInput, + InputActor, + InActorTransform, + OutCreatedNodeIds, + OutHandles, + bInputNodesCanBeDeleted); + + break; + } + + case EHoudiniInputObjectType::Landscape: + { + UHoudiniInputLandscape* InputLandscape = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForLandscape( + ObjBaseName, + InputLandscape, + InInput, + OutCreatedNodeIds, + OutHandles, + bInputNodesCanBeDeleted); + + break; + } + + case EHoudiniInputObjectType::LevelInstance: + { + UHoudiniInputLevelInstance* InputLevelInstance = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForLevelInstance( + ObjBaseName, + InputLevelInstance, + InputSettings, + InInput, + OutCreatedNodeIds, + OutHandles, + bInputNodesCanBeDeleted); + + break; + } + + case EHoudiniInputObjectType::PackedLevelActor: + { + UHoudiniInputPackedLevelActor* const InputPackedLevelActor = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForPackedLevelActor( + ObjBaseName, + InputPackedLevelActor, + InputSettings, + InInput, + OutCreatedNodeIds, + OutHandles, + bInputNodesCanBeDeleted); + + break; + } + + case EHoudiniInputObjectType::Brush: + { + UHoudiniInputBrush* InputBrush = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForBrush( + ObjBaseName, + InputBrush, + InInput->GetBoundSelectorObjectArray(), + InputSettings, + bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::CameraComponent: + { + UHoudiniInputCameraComponent* InputCamera = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForCamera( + ObjBaseName, InputCamera, InputSettings); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::PCGData: + { + UHoudiniInputPCGData* InputPCGData = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForPCGData( + ObjBaseName, InputPCGData, InputSettings, bInputNodesCanBeDeleted); + + if(bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::DataTable: + { + UHoudiniInputDataTable* InputDT = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForDataTable( + ObjBaseName, InputDT, InputSettings, bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::FoliageType_InstancedStaticMesh: + { + UHoudiniInputFoliageType_InstancedStaticMesh* const InputFoliageTypeSM = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForFoliageType_InstancedStaticMesh( + ObjBaseName, + InputFoliageTypeSM, + InputSettings, + bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + + break; + } + + case EHoudiniInputObjectType::Blueprint: + { + UHoudiniInputBlueprint* InputBP = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForBP( + InInput, InputBP, OutCreatedNodeIds, OutHandles, bInputNodesCanBeDeleted); + break; + } + + case EHoudiniInputObjectType::LandscapeSplinesComponent: + { + UHoudiniInputLandscapeSplinesComponent* const InputLandscapeSplinesComponent = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForLandscapeSplinesComponent( + ObjBaseName, + InputLandscapeSplinesComponent, + InputSettings, + OutCreatedNodeIds, + OutHandles, + bInputNodesCanBeDeleted); + break; + } + + case EHoudiniInputObjectType::SplineMeshComponent: + { + UHoudiniInputMeshComponent* InputSMC = Cast(InInputObject); + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForStaticMeshComponent( + ObjBaseName, + InputSMC, + InputSettings, + bInputNodesCanBeDeleted); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputObjectNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + break; + } + + case EHoudiniInputObjectType::Texture: + { + UHoudiniInputTexture* InputTexture = Cast(InInputObject); + + bSuccess = FHoudiniInputTranslator::HapiCreateInputNodeForTexture2D( + ObjBaseName, + InputTexture, + InputSettings, + bInputNodesCanBeDeleted, + InInput->IsCOPInput()); + + if (bSuccess) + { + OutCreatedNodeIds.Add(InInputObject->GetInputNodeId()); + OutHandles.Add(InInputObject->InputNodeHandle); + } + break; + } + } + + // Mark that input object as not changed + if (bSuccess) + { + InInputObject->MarkChanged(false); + InInputObject->SetNeedsToTriggerUpdate(false); + } + else + { + // We couldn't update/create that input object, keep it changed but prevent it from trigger updates + InInputObject->SetNeedsToTriggerUpdate(false); + } + + // Mark the outer package as dirty, to ensure that the changes are saved when using OFPA / World partition + InInputObject->MarkPackageDirty(); + + return bSuccess; +} + + +// Upload transform for an input's InputObject +bool +FHoudiniInputTranslator::UploadHoudiniInputTransform( + UHoudiniInput* InInput, UHoudiniInputObject* InInputObject) +{ + if (!InInput || !InInputObject) + return false; + + auto UpdateTransform = [](const FTransform& InTransform, const HAPI_NodeId& InNodeId) + { + // Translate the Transform to HAPI + HAPI_TransformEuler HapiTransform; + FHoudiniApi::TransformEuler_Init(&HapiTransform); + FHoudiniEngineUtils::TranslateUnrealTransform(InTransform, HapiTransform); + + // Set the transform on the OBJ parent + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + FHoudiniEngine::Get().GetSession(), InNodeId, &HapiTransform), false); + + return true; + }; + + // Check if the new input system is being used + bool bSuccess = true; + switch (InInputObject->Type) + { + case EHoudiniInputObjectType::StaticMesh: + { + // Simply update the Input mesh's Transform offset + if (!UpdateTransform(InInputObject->GetHoudiniObjectTransform(), InInputObject->GetInputObjectNodeId())) + bSuccess = false; + + break; + } + + + case EHoudiniInputObjectType::CameraComponent: + case EHoudiniInputObjectType::GeometryCollectionComponent: + case EHoudiniInputObjectType::InstancedStaticMeshComponent: + case EHoudiniInputObjectType::SceneComponent: + case EHoudiniInputObjectType::SplineComponent: + case EHoudiniInputObjectType::StaticMeshComponent: + { + // Default behaviour for components derived from SceneComponent. + + // Update using the component's transform + UHoudiniInputSceneComponent* InComponent = Cast(InInputObject); + if (!IsValid(InComponent)) + { + bSuccess = false; + break; + } + + // Update the InputObject's transform + InComponent->UpdateTransform(); + if(!UpdateTransform(InComponent->GetHoudiniObjectTransform(), InInputObject->GetInputObjectNodeId())) + bSuccess = false; + + break; + } + + case EHoudiniInputObjectType::HoudiniSplineComponent: + { + // TODO: Simply update the curve's transform? + break; + } + + case EHoudiniInputObjectType::HoudiniAssetActor: + case EHoudiniInputObjectType::HoudiniAssetComponent: + { + // TODO: Check, nothing to do? + break; + } + + case EHoudiniInputObjectType::Actor: + case EHoudiniInputObjectType::GeometryCollectionActor_Deprecated: + case EHoudiniInputObjectType::LandscapeSplineActor: + case EHoudiniInputObjectType::LevelInstance: + case EHoudiniInputObjectType::PackedLevelActor: + { + UHoudiniInputActor* InputActor = Cast(InInputObject); + if (!IsValid(InputActor)) + { + bSuccess = false; + break; + } + + // Update the actor's transform + // To avoid further updates + if (InputActor->GetActor()) + InputActor->SetTransform(InputActor->GetActor()->GetTransform()); + + { + const HAPI_NodeId ObjectNodeId = InputActor->GetInputObjectNodeId(); + if (ObjectNodeId >= 0) + UpdateTransform(InputActor->GetHoudiniObjectTransform(), ObjectNodeId); + } + + // Iterate on all the actor input objects and see if their transform needs to be uploaded + // TODO? Also update the component's actor transform?? + for (auto& CurrentComponent : InputActor->GetActorComponents()) + { + if (!IsValid(CurrentComponent)) + continue; + + if (!CurrentComponent->HasTransformChanged()) + continue; + + // Upload the current input object's transform to Houdini + if (!UploadHoudiniInputTransform(InInput, CurrentComponent)) + { + bSuccess = false; + continue; + } + } + break; + } + + case EHoudiniInputObjectType::Landscape: + { + const FTransform NewTransform = InInputObject->GetHoudiniObjectTransform(); + if (!UpdateTransform(InInputObject->GetHoudiniObjectTransform(), InInputObject->GetInputObjectNodeId())) + bSuccess = false; + } + + case EHoudiniInputObjectType::Brush: + { + // TODO: Update the Brush's transform + break; + } + + case EHoudiniInputObjectType::FoliageType_InstancedStaticMesh: + { + // Simply update the Input mesh's Transform offset + if (!UpdateTransform(InInputObject->GetHoudiniObjectTransform(), InInputObject->GetInputObjectNodeId())) + bSuccess = false; + + break; + } + + // Unsupported + case EHoudiniInputObjectType::Object: + case EHoudiniInputObjectType::SkeletalMesh: + { + break; + } + case EHoudiniInputObjectType::GeometryCollection: + { + // Simply update the Input mesh's Transform offset + if (!UpdateTransform(InInputObject->GetHoudiniObjectTransform(), InInputObject->GetInputObjectNodeId())) + bSuccess = false; + + break; + } + + case EHoudiniInputObjectType::Blueprint: + { + UHoudiniInputBlueprint* InputBP = Cast(InInputObject); + if (!IsValid(InputBP)) + { + bSuccess = false; + break; + } + + const HAPI_NodeId InputObjNodeId = InputBP->GetInputObjectNodeId(); + if (InputObjNodeId >= 0) + { + UpdateTransform(InputBP->GetHoudiniObjectTransform(), InputObjNodeId); + } + + // Iterate on all the BP's input objects and see if their transform needs to be uploaded + for (auto& CurrentComponent : InputBP->GetComponents()) + { + if (!IsValid(CurrentComponent)) + continue; + + if (!CurrentComponent->HasTransformChanged()) + continue; + + // Upload the current input object's transform to Houdini + if (!UploadHoudiniInputTransform(InInput, CurrentComponent)) + { + bSuccess = false; + continue; + } + } + break; + } + case EHoudiniInputObjectType::Invalid: + default: + break; + } + + // Mark that input object as not changed + if (bSuccess) + { + InInputObject->MarkTransformChanged(false); + InInputObject->SetNeedsToTriggerUpdate(false); + } + else + { + // We couldn't update/create that input object, keep it changed but prevent it from trigger updates + InInputObject->SetNeedsToTriggerUpdate(false); + } + + return bSuccess; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForObject(const FString& InObjNodeName, UHoudiniInputObject* InObject) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForObject); + + if (!InObject) + return false; + + UObject* Object = InObject->GetObject(); + if (!IsValid(Object)) + return true; + + FString NodeName = InObjNodeName + TEXT("_") + Object->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(NodeName); + + // For UObjects we can't upload much, but can still create an input node + // with a single point, with an attribute pointing to the input object's path + HAPI_NodeId InputNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::CreateInputNode( + FHoudiniEngine::Get().GetSession(), -1, &InputNodeId, H_TCHAR_TO_UTF8(*NodeName)), false); + + // Update this input object's NodeId and ObjectNodeId + InObject->SetInputNodeId((int32)InputNodeId); + InObject->SetInputObjectNodeId((int32)FHoudiniEngineUtils::HapiGetParentNodeId(InputNodeId)); + + // Create a part + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 2; + Part.vertexCount = 0; + Part.faceCount = 0; + Part.pointCount = 1; + Part.type = HAPI_PARTTYPE_MESH; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, &Part), false); + + { + // Create point attribute info for P. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = 1; + AttributeInfoPoint.tupleSize = 3; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint), false); + + // Set the point's position + FVector3f ObjectPosition = (FVector3f)InObject->GetHoudiniObjectTransform().GetLocation(); + TArray Position = + { + ObjectPosition.X * HAPI_UNREAL_SCALE_FACTOR_POSITION, + ObjectPosition.Z * HAPI_UNREAL_SCALE_FACTOR_POSITION, + ObjectPosition.Y * HAPI_UNREAL_SCALE_FACTOR_POSITION + }; + + // Now that we have raw positions, we can upload them for our attribute. + FHoudiniHapiAccessor Accessor(InputNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPoint, Position), false); + + } + + { + // Create point attribute info for the path. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = 1; + AttributeInfoPoint.tupleSize = 1; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + HAPI_UNREAL_ATTRIB_OBJECT_PATH, &AttributeInfoPoint), false); + + // Set the point's path attribute + FString ObjectPathName = Object->GetPathName(); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, HAPI_UNREAL_ATTRIB_OBJECT_PATH); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoPoint, ObjectPathName), false); + } + + // Commit the geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(InputNodeId), false); + + return true; +} + +bool +FHoudiniInputTranslator::HapiSetGeoObjectTransform(const HAPI_NodeId& InObjectNodeId, const FTransform& InTransform) +{ + if (InObjectNodeId < 0) + return true; + + // Updating the Transform + HAPI_TransformEuler HapiTransform; + FHoudiniApi::TransformEuler_Init(&HapiTransform); + + FHoudiniEngineUtils::TranslateUnrealTransform(InTransform, HapiTransform); + + // Set the transform on the OBJ parent + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + FHoudiniEngine::Get().GetSession(), InObjectNodeId, &HapiTransform), false); + + return true; +} + +bool +FHoudiniInputTranslator::HapiCreateOrUpdateGeoObjectMergeAndSetTransform( + const int32 InParentNodeId, + HAPI_NodeId InNodeToObjectMerge, + const FString& InObjNodeName, + HAPI_NodeId& InOutObjectMergeNodeId, + HAPI_NodeId& InOutGeoObjectNodeId, + bool bInCreateIfMissingInvalid, + const FTransform& InTransform, + int32 InTransformType) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateOrUpdateGeoObjectMergeAndSetTransform); + + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(InNodeToObjectMerge)) + return false; + + bool bCreatedGeoObject = false; + constexpr bool bCookOnCreation = true; + + // Check that InOutGeoObjectNodeId is valid + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(InOutGeoObjectNodeId)) + { + if (!bInCreateIfMissingInvalid) + return false; + + // Create Geo object in InParentNodeId's network + const FString ObjOperatorName = InParentNodeId >= 0 ? TEXT("geo") : TEXT("Object/geo"); + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniEngineUtils::CreateNode(InParentNodeId, ObjOperatorName, InObjNodeName, bCookOnCreation, &InOutGeoObjectNodeId), false); + bCreatedGeoObject = true; + } + + HAPI_NodeId CurrentGeoNodeId = -1; + if (InOutObjectMergeNodeId < 0) + { + // See if the node already exists + HAPI_Result result = FHoudiniApi::GetNodeFromPath( + FHoudiniEngine::Get().GetSession(), InOutGeoObjectNodeId, TCHAR_TO_ANSI(*InObjNodeName), &CurrentGeoNodeId); + + if (CurrentGeoNodeId >= 0) + InOutObjectMergeNodeId = CurrentGeoNodeId; + } + + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(InOutObjectMergeNodeId)) + { + if (!bInCreateIfMissingInvalid) + return false; + + // Create the objmerge SOP in InOutGeoObjectNodeId if non existent + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniEngineUtils::CreateNode(InOutGeoObjectNodeId, TEXT("object_merge"), InObjNodeName, bCookOnCreation, &InOutObjectMergeNodeId), false); + } + + // Set the objpath1 on the object merge + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmNodeValue(Session, InOutObjectMergeNodeId, H_TCHAR_TO_UTF8(TEXT("objpath1")), InNodeToObjectMerge), false); + + // Set Transform type if needed + if (InTransformType >= 0 && InTransformType <= 2) + { + // 0 None + // 1 Into this object + // 2 Into Specified + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue(Session, InOutObjectMergeNodeId, H_TCHAR_TO_UTF8(TEXT("xformtype")), 0, InTransformType), false); + } + + if (!InTransform.Equals(FTransform::Identity) || !bCreatedGeoObject) + { + if (!HapiSetGeoObjectTransform(InOutGeoObjectNodeId, InTransform)) + return false; + } + + return true; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForStaticMesh( + const FString& InObjNodeName, + UHoudiniInputStaticMesh* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForStaticMesh); + + if (!IsValid(InObject)) + return false; + + // Get the StaticMesh + UStaticMesh* SM = InObject->GetStaticMesh(); + if (!IsValid(SM)) + return true; + + FString SMName = InObjNodeName + TEXT("_") + SM->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(SMName); + + // Marshall the Static Mesh to Houdini + FUnrealObjectInputHandle SMInputNodeHandle; + HAPI_NodeId CreatedNodeId = -1; + + // Get the existing node id, if any + + // For the ref counted system the handle on the input object represents a reference node that has a single node + // it references: the static mesh. The reference node represents InObject with its Transform (geometry input). + { + TSet ReferencedNodes; + if (FUnrealObjectInputUtils::GetReferencedNodes(InObject->InputNodeHandle, ReferencedNodes) && ReferencedNodes.Num() == 1) + { + const FUnrealObjectInputHandle Handle = ReferencedNodes.Array()[0]; + FUnrealObjectInputUtils::GetHAPINodeId(Handle, CreatedNodeId); + } + } + + bool bSuccess = true; + if (InInputSettings.bImportAsReference) + { + FBox InBbox = InInputSettings.bImportAsReferenceBboxEnabled ? + SM->GetBoundingBox() : + FBox(EForceInit::ForceInit); + + const TArray& MaterialReferences = InInputSettings.bImportAsReferenceMaterialEnabled ? + InObject->GetMaterialReferences() : + TArray(); + + bSuccess = FHoudiniInputTranslator::CreateInputNodeForReference( + CreatedNodeId, + SM, + SMName, + InObject->GetTransform(), + InInputSettings.bImportAsReferenceRotScaleEnabled, + SMInputNodeHandle, + bInputNodesCanBeDeleted, + InInputSettings.bImportAsReferenceBboxEnabled, + InBbox, + InInputSettings.bImportAsReferenceMaterialEnabled, + MaterialReferences); + } + else + { + FUnrealMeshExportOptions ExportOptions; + ExportOptions.bLODs = InInputSettings.bExportLODs; + ExportOptions.bSockets = InInputSettings.bExportSockets; + ExportOptions.bColliders = InInputSettings.bExportColliders; + ExportOptions.bMainMesh = InInputSettings.bExportMainGeometry; + ExportOptions.bMaterialParameters = InInputSettings.bExportMaterialParameters; + ExportOptions.bPreferNaniteFallbackMesh = InInputSettings.bPreferNaniteFallbackMesh; + + bSuccess = FUnrealMeshTranslator::CreateInputNodeForStaticMesh( + CreatedNodeId, + SMInputNodeHandle, + SM, + nullptr, + SMName, + ExportOptions, + bInputNodesCanBeDeleted, + false); + } + + { + // The static mesh can have its own transform (geometry input), so we have to create a reference node that + // represents InObject in the new input system that references the StaticMesh asset's input node handle + FUnrealObjectInputOptions Options; + static constexpr bool bIsLeaf = false; + FUnrealObjectInputIdentifier GeoInputRefNodeId(InObject, Options, bIsLeaf); + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(GeoInputRefNodeId, { SMInputNodeHandle }, InObject->InputNodeHandle, true, bInputNodesCanBeDeleted); + } + + if (!HapiSetGeoObjectTransform(InObject->GetInputObjectNodeId(), InObject->GetHoudiniObjectTransform())) + return false; + + // Update the cached data and input settings + InObject->Update(SM, InInputSettings); + + // // If the Input mesh has a Transform offset + // FTransform TransformOffset = InObject->Transform; + // if (!TransformOffset.Equals(FTransform::Identity)) + // { + // // Updating the Transform + // HAPI_TransformEuler HapiTransform; + // FHoudiniApi::TransformEuler_Init(&HapiTransform); + // FHoudiniEngineUtils::TranslateUnrealTransform(TransformOffset, HapiTransform); + // + // // Set the transform on the OBJ parent + // HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + // FHoudiniEngine::Get().GetSession(), InObject->InputObjectNodeId, &HapiTransform), false); + // } + + return bSuccess; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForReference( + const FString& InObjNodeName, + UHoudiniInputObject* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForReference); + + if (!IsValid(InObject)) + return false; + + // Marshall the Object to Houdini + FUnrealObjectInputHandle InputNodeHandle; + HAPI_NodeId CreatedNodeId = -1; + + // Get the existing node id, if any + + // For the ref counted system the handle on the input object represents a reference node that has a single node + // it references: the static mesh. The reference node represents InObject with its Transform (geometry input). + { + TSet ReferencedNodes; + if (FUnrealObjectInputUtils::GetReferencedNodes(InObject->InputNodeHandle, ReferencedNodes) && ReferencedNodes.Num() == 1) + { + const FUnrealObjectInputHandle Handle = ReferencedNodes.Array()[0]; + FUnrealObjectInputUtils::GetHAPINodeId(Handle, CreatedNodeId); + } + } + + bool bSuccess = true; + FBox InBbox = FBox(EForceInit::ForceInit); + + const TArray& MaterialReferences + = InInputSettings.bImportAsReferenceMaterialEnabled ? + InObject->GetMaterialReferences() : + TArray(); + + bSuccess = FHoudiniInputTranslator::CreateInputNodeForReference( + CreatedNodeId, InObject->GetObject(), InObjNodeName, + InObject->GetTransform(), InInputSettings.bImportAsReferenceRotScaleEnabled, + InputNodeHandle, bInputNodesCanBeDeleted, + InInputSettings.bImportAsReferenceBboxEnabled, InBbox, + InInputSettings.bImportAsReferenceMaterialEnabled, MaterialReferences); + + { + // The input object can have its own transform (geometry input), so we have to create a reference node that + // represents InObject in the new input system that references the StaticMesh asset's input node handle + FUnrealObjectInputOptions Options; + static constexpr bool bIsLeaf = false; + FUnrealObjectInputIdentifier GeoInputRefNodeId(InObject, Options, bIsLeaf); + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(GeoInputRefNodeId, { InputNodeHandle }, InObject->InputNodeHandle, true, bInputNodesCanBeDeleted); + } + + if (!HapiSetGeoObjectTransform(InObject->GetInputObjectNodeId(), InObject->GetHoudiniObjectTransform())) + return false; + + // Ensure bImportAsReference is recorded as true + FHoudiniInputObjectSettings InputSettings(InInputSettings); + InputSettings.bImportAsReference = true; + // Update the cached data and input settings + InObject->Update(InObject->GetObject(), InputSettings); + + return bSuccess; +} + + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForActorReference( + UHoudiniInputActor* InActorObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForActorReference); + + if (!IsValid(InActorObject)) + return false; + + // Get the Actor we want to send as reference + AActor* InActor = InActorObject->GetActor(); + if (!InActor) + return false; + + // Actors properties + FTransform ActorTransform = InActor->GetTransform(); + FString ActorPath = InActor->GetPathName(); + FString ActorLevelPath = InActor->GetLevel()->GetPathName(); + { + // We just want the path up to the first point + int32 DotIndex; + if (ActorLevelPath.FindChar('.', DotIndex)) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + ActorLevelPath.LeftInline(DotIndex, EAllowShrinking::No); +#else + ActorLevelPath.LeftInline(DotIndex, false); +#endif + } + + // Component properties + TArray ComponentTransforms; + TArray ComponentBBoxes; + TArray ComponentReferences; + TArray> ComponentMaterials; + for (UHoudiniInputSceneComponent* CurComponent : InActorObject->GetActorComponents()) + { + switch (CurComponent->Type) + { + case EHoudiniInputObjectType::StaticMeshComponent: + { + UHoudiniInputMeshComponent* InputSMC = Cast(CurComponent); + if (InputSMC) + { + // Ref + UStaticMesh* SM = InputSMC->GetStaticMesh(); + FString AssetRef = FString(); + if(IsValid(SM)) + AssetRef = UHoudiniInputObject::FormatAssetReference(SM->GetFullName()); + ComponentReferences.Add(AssetRef); + + // Bounding box + FBox Bbox = InInputSettings.bImportAsReferenceBboxEnabled ? + SM->GetBoundingBox() : + FBox(EForceInit::ForceInit); + ComponentBBoxes.Add(Bbox); + + // Transforms + // For some reason - object merge seem to ignore point rotations/scale values? + FTransform ComponentTransform = InputSMC->GetTransform(); + ComponentTransform.SetLocation(InputSMC->GetTransformRelativeToOwner().GetLocation()); + ComponentTransforms.Add(ComponentTransform); + + // Materials + if (InInputSettings.bImportAsReferenceMaterialEnabled) + ComponentMaterials.Add(InputSMC->GetMaterialReferences()); + else + ComponentMaterials.Add(TArray()); + } + } + break; + + case EHoudiniInputObjectType::InstancedStaticMeshComponent: + { + UHoudiniInputInstancedMeshComponent* InputIMC = Cast(CurComponent); + if (InputIMC) + { + // Ref + UStaticMesh* SM = InputIMC->GetStaticMesh(); + FString AssetRef = FString(); + if (IsValid(SM)) + AssetRef = UHoudiniInputObject::FormatAssetReference(SM->GetFullName()); + ComponentReferences.Add(AssetRef); + + // Bounding box + FBox Bbox = InInputSettings.bImportAsReferenceBboxEnabled ? + SM->GetBoundingBox() : + FBox(EForceInit::ForceInit); + ComponentBBoxes.Add(Bbox); + + // Transforms + ComponentTransforms.Add(InputIMC->GetTransformRelativeToOwner()); + + // Materials + if (InInputSettings.bImportAsReferenceMaterialEnabled) + ComponentMaterials.Add(InputIMC->GetMaterialReferences()); + else + ComponentMaterials.Add(TArray()); + } + } + break; + + case EHoudiniInputObjectType::SkeletalMeshComponent: + { + UHoudiniInputSkeletalMeshComponent* InputSKMC = Cast(CurComponent); + if (IsValid(InputSKMC)) + { + // Ref + USkeletalMesh* SKM = InputSKMC->GetSkeletalMesh(); + FString AssetRef = FString(); + if (IsValid(SKM)) + AssetRef = UHoudiniInputObject::FormatAssetReference(SKM->GetFullName()); + ComponentReferences.Add(AssetRef); + + // Bounding box + FBox Bbox = InInputSettings.bImportAsReferenceBboxEnabled ? + SKM->GetBounds().GetBox() : + FBox(EForceInit::ForceInit); + ComponentBBoxes.Add(Bbox); + + // Transforms + ComponentTransforms.Add(InputSKMC->GetTransformRelativeToOwner()); + + // Materials + if (InInputSettings.bImportAsReferenceMaterialEnabled) + ComponentMaterials.Add(InputSKMC->GetMaterialReferences()); + else + ComponentMaterials.Add(TArray()); + } + } + break; + + case EHoudiniInputObjectType::GeometryCollectionComponent: + { + UHoudiniInputGeometryCollectionComponent* InputGCC = Cast(CurComponent); + if (IsValid(InputGCC)) + { + // Ref + UGeometryCollection* GC = InputGCC->GetGeometryCollection(); + FString AssetRef = FString(); + if (IsValid(GC)) + AssetRef = UHoudiniInputObject::FormatAssetReference(GC->GetFullName()); + ComponentReferences.Add(AssetRef); + + // Bounding box + TManagedArray& BboxArray = GC->GetGeometryCollection()->BoundingBox; + FBox GCBbox = FBox(EForceInit::ForceInitToZero); + if (InInputSettings.bImportAsReferenceBboxEnabled) + { + for (FBox& Bbox : BboxArray) + GCBbox += Bbox; + } + ComponentBBoxes.Add(GCBbox); + + // Transforms + ComponentTransforms.Add(InputGCC->GetTransformRelativeToOwner()); + + // Materials + if (InInputSettings.bImportAsReferenceMaterialEnabled) + ComponentMaterials.Add(InputGCC->GetMaterialReferences()); + else + ComponentMaterials.Add(TArray()); + } + } + break; + + case EHoudiniInputObjectType::SplineMeshComponent: + case EHoudiniInputObjectType::SplineComponent: + case EHoudiniInputObjectType::HoudiniSplineComponent: + case EHoudiniInputObjectType::CameraComponent: + case EHoudiniInputObjectType::SceneComponent: + case EHoudiniInputObjectType::HoudiniAssetComponent: + case EHoudiniInputObjectType::LandscapeSplinesComponent: + { + // Ref + UObject* Obj = CurComponent->GetObject(); + if (Obj->IsA(UHoudiniCookable::StaticClass())) + { + UHoudiniCookable* HC = Cast(Obj); + Obj = HC->GetComponent(); + } + + FString AssetRef = FString(); + if (IsValid(Obj)) + AssetRef = UHoudiniInputObject::FormatAssetReference(Obj->GetFullName()); + ComponentReferences.Add(AssetRef); + + // Bounding box + USceneComponent* SC = CurComponent->GetSceneComponent(); + FBox SCBbox = FBox(EForceInit::ForceInitToZero); + if (InInputSettings.bImportAsReferenceBboxEnabled && IsValid(SC)) + { + SCBbox = SC->GetLocalBounds().GetBox(); + } + ComponentBBoxes.Add(SCBbox); + + // Transforms + ComponentTransforms.Add(CurComponent->GetTransformRelativeToOwner()); + + // Materials + if (InInputSettings.bImportAsReferenceMaterialEnabled) + ComponentMaterials.Add(CurComponent->GetMaterialReferences()); + else + ComponentMaterials.Add(TArray()); + } + break; + + + default: + { + // Do Nothing + } + break; + } + } + + // The identifier to the node in the input system + FString InputNodeName; + HAPI_NodeId ParentNodeId = -1; + HAPI_NodeId NodeId = -1; + FUnrealObjectInputHandle ParentHandle; + FUnrealObjectInputIdentifier Identifier; + { + // Build the identifier for the entry in the manager + constexpr bool bIsLeaf = true; + FUnrealObjectInputOptions Options; + Options.bImportAsReference = true; + Options.bImportAsReferenceRotScaleEnabled = InInputSettings.bImportAsReferenceRotScaleEnabled; + Identifier = FUnrealObjectInputIdentifier(InActor, Options, bIsLeaf); + + // If the entry exists in the manager, the associated HAPI nodes are valid, and it is not marked as dirty, then + // return the existing entry + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId)) + { + // Make sure the node cant be deleted if needed + if (!bInputNodesCanBeDeleted) + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + + //OutHandle = Handle; + //InputNodeId = NodeId; + return true; + } + } + + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId)) + NodeId = -1; + + // If the entry does not exist, or is invalid, then we need create it + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, InputNodeName); + // Create any parent/container nodes that we would need, and get the node id of the immediate parent + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + } + + // Create a single input node + HAPI_NodeId NewNodeId = -1; + if (ParentNodeId >= 0) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateInputNode(InputNodeName, NewNodeId, ParentNodeId), false); + } + else + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CreateInputNode( + FHoudiniEngine::Get().GetSession(), -1, &NewNodeId, H_TCHAR_TO_UTF8(*InputNodeName)), false); + } + + // Check if we have a valid id for this new input asset. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(NewNodeId)) + return false; + + // We have now created a valid new input node, we can delete the previous one + HAPI_NodeId PreviousInputNodeId = NodeId; + if (PreviousInputNodeId >= 0) + { + // Get the parent OBJ node ID before deleting! + HAPI_NodeId PreviousInputOBJNode = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousInputNodeId); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input node for %s."), *InputNodeName); + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputOBJNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input OBJ node for %s."), *InputNodeName); + } + } + + // We will create one point per component + int32 NumPoints = ComponentReferences.Num(); + + // Create and initialize a part with a point attribute + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + PartInfo.attributeCounts[HAPI_ATTROWNER_POINT] = 1; + PartInfo.vertexCount = 0; + PartInfo.faceCount = 0; + PartInfo.pointCount = NumPoints; + PartInfo.type = HAPI_PARTTYPE_MESH; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, &PartInfo), false); + + // Point Position Attribute + { + // Create point attribute info for P. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = NumPoints; + AttributeInfoPoint.tupleSize = 3; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint), false); + + // Extract/Convert the positions from the Transform + TArray AllPositions; + AllPositions.SetNum(3 * NumPoints); + for (int32 Idx = 0; Idx < ComponentTransforms.Num(); Idx++) + { + FVector3f CurPos = (FVector3f)ComponentTransforms[Idx].GetLocation(); + AllPositions[3 * Idx] = CurPos.X / HAPI_UNREAL_SCALE_FACTOR_POSITION; + AllPositions[3 * Idx + 1] = CurPos.Z / HAPI_UNREAL_SCALE_FACTOR_POSITION; + AllPositions[3 * Idx + 2] = CurPos.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + + // Now that we have raw positions, we can upload them for our attribute. + FHoudiniHapiAccessor Accessor(NewNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPoint, AllPositions), false); + } + + if(InInputSettings.bImportAsReferenceRotScaleEnabled) + { + // Extract/Convert the rotation/scale values from the Transform + TArray AllRotations; + AllRotations.SetNum(4 * NumPoints); + TArray AllScales; + AllScales.SetNum(3 * NumPoints); + for (int32 Idx = 0; Idx < ComponentTransforms.Num(); Idx++) + { + FQuat InputRotation = ComponentTransforms[Idx].GetRotation(); + AllRotations[4 * Idx] = (float)InputRotation.X; + AllRotations[4 * Idx + 1] = (float)InputRotation.Z; + AllRotations[4 * Idx + 2] = (float)InputRotation.Y; + AllRotations[4 * Idx + 3] = (float)-InputRotation.W; + + FVector3f InputScale = (FVector3f)ComponentTransforms[Idx].GetScale3D(); + AllScales[3 * Idx] = InputScale.X; + AllScales[3 * Idx + 1] = InputScale.Z; + AllScales[3 * Idx + 2] = InputScale.Y; + } + + // Create ROTATION attribute info + HAPI_AttributeInfo AttributeInfoRotation; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoRotation); + AttributeInfoRotation.count = NumPoints; + AttributeInfoRotation.tupleSize = 4; + AttributeInfoRotation.exists = true; + AttributeInfoRotation.owner = HAPI_ATTROWNER_POINT; + AttributeInfoRotation.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoRotation.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, + HAPI_UNREAL_ATTRIB_ROTATION, + &AttributeInfoRotation), false); + + //we can now upload to our attribute. + FHoudiniHapiAccessor Accessor(NewNodeId, 0, HAPI_UNREAL_ATTRIB_ROTATION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoRotation, AllRotations), false); + + // Create SCALE attribute info + HAPI_AttributeInfo AttributeInfoScale; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoScale); + AttributeInfoScale.count = NumPoints; + AttributeInfoScale.tupleSize = 3; + AttributeInfoScale.exists = true; + AttributeInfoScale.owner = HAPI_ATTROWNER_POINT; + AttributeInfoScale.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoScale.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, + HAPI_UNREAL_ATTRIB_SCALE, + &AttributeInfoScale), false); + + //we can now upload to our attribute. + Accessor.Init(NewNodeId, 0, HAPI_UNREAL_ATTRIB_SCALE); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoScale, AllScales), false); + } + + if (InInputSettings.bImportAsReferenceBboxEnabled) + { + // Extract/Convert the bbox min/max values from the Transform + TArray AllBBoxMins; + AllBBoxMins.SetNum(3 * NumPoints); + TArray AllBBoxMaxs; + AllBBoxMaxs.SetNum(3 * NumPoints); + for (int32 Idx = 0; Idx < ComponentBBoxes.Num(); Idx++) + { + FVector3f CurMin = (FVector3f)ComponentBBoxes[Idx].Min; + FVector3f CurMax = (FVector3f)ComponentBBoxes[Idx].Max; + + AllBBoxMins[3 * Idx] = CurMin.X / HAPI_UNREAL_SCALE_FACTOR_POSITION; + AllBBoxMins[3 * Idx + 1] = CurMin.Z / HAPI_UNREAL_SCALE_FACTOR_POSITION; + AllBBoxMins[3 * Idx + 2] = CurMin.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION; + + AllBBoxMaxs[3 * Idx] = CurMax.X / HAPI_UNREAL_SCALE_FACTOR_POSITION; + AllBBoxMaxs[3 * Idx + 1] = CurMax.Z / HAPI_UNREAL_SCALE_FACTOR_POSITION; + AllBBoxMaxs[3 * Idx + 2] = CurMax.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + + // Create attribute info for both bbox min and bbox max + HAPI_AttributeInfo AttributeInfoBboxPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoBboxPoint); + AttributeInfoBboxPoint.count = NumPoints; + AttributeInfoBboxPoint.tupleSize = 3; + AttributeInfoBboxPoint.exists = true; + AttributeInfoBboxPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoBboxPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoBboxPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + // bbox min + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + HAPI_UNREAL_ATTRIB_BBOX_MIN, &AttributeInfoBboxPoint), false); + + FHoudiniHapiAccessor Accessor(NewNodeId, 0, HAPI_UNREAL_ATTRIB_BBOX_MIN); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoBboxPoint, AllBBoxMins), false); + + // bbox max + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + HAPI_UNREAL_ATTRIB_BBOX_MAX, &AttributeInfoBboxPoint), false); + + Accessor.Init(NewNodeId, 0, HAPI_UNREAL_ATTRIB_BBOX_MAX); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoBboxPoint, AllBBoxMaxs), false); + } + + // Material Reference String Array Attribute + if (InInputSettings.bImportAsReferenceMaterialEnabled) + { + // Create point attribute info. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = NumPoints; + AttributeInfoPoint.tupleSize = 1; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + int32 MaxNumMaterials = 0; + for (int32 CompIdx = 0; CompIdx < ComponentMaterials.Num(); CompIdx++) + { + if (ComponentMaterials[CompIdx].Num() > MaxNumMaterials) + MaxNumMaterials = ComponentMaterials[CompIdx].Num(); + } + + // We set it to be multiple string attributes rather than a single string array attribute to not conflict + // with any existing HDA's that use the attribute name unreal_material + for (int32 MatIdx = 0; MatIdx < MaxNumMaterials; MatIdx++) + { + FString AttributeName = HAPI_UNREAL_ATTRIB_MATERIAL; + if (MatIdx > 0) + AttributeName.AppendInt(MatIdx); + + // Create an array for the current Material + TArray CurrentMaterials; + CurrentMaterials.SetNum(ComponentMaterials.Num()); + for (int32 CompIdx = 0; CompIdx < ComponentMaterials.Num(); CompIdx++) + { + if (ComponentMaterials[CompIdx].IsValidIndex(MatIdx)) + CurrentMaterials[CompIdx] = ComponentMaterials[CompIdx][MatIdx]; + else + CurrentMaterials[CompIdx] = FString(); + } + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + TCHAR_TO_ANSI(*AttributeName), &AttributeInfoPoint), false); + + FHoudiniHapiAccessor Accessor(NewNodeId, 0, TCHAR_TO_ANSI(*AttributeName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPoint, CurrentMaterials), false); + } + } + + // Unreal Reference String Attribute + { + // Create point attribute info. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = NumPoints; + AttributeInfoPoint.tupleSize = 1; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + HAPI_UNREAL_ATTRIB_INSTANCE_OVERRIDE, &AttributeInfoPoint), false); + + FHoudiniHapiAccessor Accessor(NewNodeId, 0, HAPI_UNREAL_ATTRIB_INSTANCE_OVERRIDE); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPoint, ComponentReferences), false); + } + + // Commit the geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(NewNodeId), false); + + FUnrealObjectInputHandle OutHandle; + { + // Record the node in the manager + const HAPI_NodeId ObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(NewNodeId); + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, NewNodeId, Handle, ObjectNodeId, nullptr, bInputNodesCanBeDeleted)) + OutHandle = Handle; + + InActorObject->SetInputObjectNodeId(ObjectNodeId); + InActorObject->SetInputNodeId(NewNodeId); + InActorObject->InputNodeHandle = OutHandle; + } + + if (!HapiSetGeoObjectTransform(InActorObject->GetInputObjectNodeId(), InActorObject->GetHoudiniObjectTransform())) + return false; + + /* + // Ensure bImportAsReference is recorded as true + FHoudiniInputObjectSettings InputSettings(InInputSettings); + InputSettings.bImportAsReference = true; + // Update the cached data and input settings + InActorObject->Update(InObject->GetObject(), InputSettings); + */ + + return true; +} + + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForAnimation( + const FString& InObjNodeName, + UHoudiniInputAnimation* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForAnimation); + + if (!IsValid(InObject)) + return false; + + UAnimSequence* Animation = InObject->GetAnimation(); + if (!IsValid(Animation)) + return true; + + FString SKName = InObjNodeName + TEXT("_") + Animation->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(SKName); + + FUnrealObjectInputHandle AnimInputNodeHandle; + HAPI_NodeId CreatedNodeId = -1; + + // Get the existing node id, if any + + // For the ref counted system the handle on the input object represents a reference node that has a single node + // it references: the animation. The reference node represents InObject with its Transform (geometry input). + { + TSet ReferencedNodes; + if (FUnrealObjectInputUtils::GetReferencedNodes(InObject->InputNodeHandle, ReferencedNodes) && ReferencedNodes.Num() == 1) + { + const FUnrealObjectInputHandle Handle = ReferencedNodes.Array()[0]; + FUnrealObjectInputUtils::GetHAPINodeId(Handle, CreatedNodeId); + } + } + + // Marshall the SkeletalMesh to Houdini + bool bSuccess = true; + + + if (InInputSettings.bImportAsReference) + { + // Get the SM's bbox + FBox InBbox = FBox(EForceInit::ForceInit); + + //FBox InBbox = bImportAsReferenceBboxEnabled ? + // SkelMesh->GetBounds().GetBox() : + // FBox(EForceInit::ForceInit); + + const TArray& MaterialReferences = InInputSettings.bImportAsReferenceMaterialEnabled ? + InObject->GetMaterialReferences() : + TArray(); + + bSuccess = FHoudiniInputTranslator::CreateInputNodeForReference( + CreatedNodeId, + Animation, + SKName, + InObject->GetTransform(), + InInputSettings.bImportAsReferenceRotScaleEnabled, + AnimInputNodeHandle, + bInputNodesCanBeDeleted, + InInputSettings.bImportAsReferenceBboxEnabled, + InBbox, + InInputSettings.bImportAsReferenceMaterialEnabled, + MaterialReferences); + } + else + { + bSuccess = FUnrealAnimationTranslator::HapiCreateInputNodeForAnimation( + Animation, CreatedNodeId, SKName, AnimInputNodeHandle, false, false, false, bInputNodesCanBeDeleted); + if (!bSuccess) + { + return false; + } + + } + + { + // The animation can have its own transform (geometry input), so we have to create a reference node that + // represents InObject in the new input system that references the UAnimSequence asset's input node handle + FUnrealObjectInputOptions Options; + static constexpr bool bIsLeaf = false; + FUnrealObjectInputIdentifier GeoInputRefNodeId(InObject, Options, bIsLeaf); + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(GeoInputRefNodeId, { AnimInputNodeHandle }, InObject->InputNodeHandle, true, bInputNodesCanBeDeleted); + } + + if (!HapiSetGeoObjectTransform(InObject->GetInputObjectNodeId(), InObject->GetHoudiniObjectTransform())) + return false; + + // Update the cached data and input settings + InObject->Update(Animation, InInputSettings); + + + + + return bSuccess; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForSkeletalMesh( + const FString& InObjNodeName, + UHoudiniInputSkeletalMesh* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForSkeletalMesh); + + if (!IsValid(InObject)) + return false; + + USkeletalMesh* SkelMesh = InObject->GetSkeletalMesh(); + if (!IsValid(SkelMesh)) + return true; + + FString SKName = InObjNodeName + TEXT("_") + SkelMesh->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(SKName); + + FUnrealObjectInputHandle SKMInputNodeHandle; + HAPI_NodeId CreatedNodeId = -1; + + // Get the existing node id, if any + + // For the ref counted system the handle on the input object represents a reference node that has a single node + // it references: the skeletal mesh. The reference node represents InObject with its Transform (geometry input). + { + TSet ReferencedNodes; + if (FUnrealObjectInputUtils::GetReferencedNodes(InObject->InputNodeHandle, ReferencedNodes) && ReferencedNodes.Num() == 1) + { + const FUnrealObjectInputHandle Handle = ReferencedNodes.Array()[0]; + FUnrealObjectInputUtils::GetHAPINodeId(Handle, CreatedNodeId); + } + } + + // Marshall the SkeletalMesh to Houdini + bool bSuccess = true; + if (InInputSettings.bImportAsReference) + { + // Get the SM's bbox + FBox InBbox = InInputSettings.bImportAsReferenceBboxEnabled ? + SkelMesh->GetBounds().GetBox() : + FBox(EForceInit::ForceInit); + + const TArray& MaterialReferences = InInputSettings.bImportAsReferenceMaterialEnabled ? + InObject->GetMaterialReferences() : + TArray(); + + bSuccess = FHoudiniInputTranslator::CreateInputNodeForReference( + CreatedNodeId, + SkelMesh, + SKName, + InObject->GetTransform(), + InInputSettings.bImportAsReferenceRotScaleEnabled, + SKMInputNodeHandle, + bInputNodesCanBeDeleted, + InInputSettings.bImportAsReferenceBboxEnabled, + InBbox, + InInputSettings.bImportAsReferenceMaterialEnabled, + MaterialReferences); + } + else + { + FUnrealMeshExportOptions ExportInputOptons; + ExportInputOptons.bLODs = InInputSettings.bExportLODs; + ExportInputOptons.bSockets = InInputSettings.bExportSockets; + ExportInputOptons.bColliders = InInputSettings.bExportColliders; + ExportInputOptons.bMainMesh = InInputSettings.bExportMainGeometry; + + bSuccess = FUnrealSkeletalMeshTranslator::HapiCreateInputNodeForSkeletalMesh( + SkelMesh, + CreatedNodeId, + SKName, + SKMInputNodeHandle, + nullptr, + ExportInputOptons, + bInputNodesCanBeDeleted, + InInputSettings.bExportMaterialParameters ); + + if(!bSuccess) + { + return false; + } + + } + + { + // The skeletal mesh can have its own transform (geometry input), so we have to create a reference node that + // represents InObject in the new input system that references the SkeletalMesh asset's input node handle + FUnrealObjectInputOptions Options; + static constexpr bool bIsLeaf = false; + FUnrealObjectInputIdentifier GeoInputRefNodeId(InObject, Options, bIsLeaf); + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(GeoInputRefNodeId, { SKMInputNodeHandle }, InObject->InputNodeHandle, true, bInputNodesCanBeDeleted); + } + + if (!HapiSetGeoObjectTransform(InObject->GetInputObjectNodeId(), InObject->GetHoudiniObjectTransform())) + return false; + + // Update the cached data and input settings + InObject->Update(SkelMesh, InInputSettings); + + return bSuccess; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForSkeletalMeshComponent( + const FString& InObjNodeName, + UHoudiniInputSkeletalMeshComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForSkeletalMeshComponent); + + if (!IsValid(InObject)) + return false; + + USkeletalMeshComponent* SKC = InObject->GetSkeletalMeshComponent(); + if (!IsValid(SKC)) + return true; + + // Get the component's Skeletal Mesh + USkeletalMesh* SK = InObject->GetSkeletalMesh(); + if (!IsValid(SK)) + return true; + + HAPI_NodeId CreatedNodeId = InObject->GetInputNodeId(); + + // Marshall the Skeletal Mesh to Houdini + FString SKCName = InObjNodeName + TEXT("_") + SKC->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(SKCName); + + FUnrealObjectInputHandle InputNodeHandle; + bool bSuccess = true; + if (InInputSettings.bImportAsReference) + { + FTransform ImportAsReferenceTransform = InObject->GetTransform(); + + // Previously, ImportAsReferenceTransform was multiplied by + // InActorTransform.Inverse() if bKeepWorldTransform was true, + // but this created a double transform issue. + ImportAsReferenceTransform.SetLocation(FVector::ZeroVector); + + // Get the SM's bbox + FBox InBbox = InInputSettings.bImportAsReferenceBboxEnabled ? + SK->GetBounds().GetBox() : + FBox(EForceInit::ForceInit); + + const TArray& MaterialReferences = InInputSettings.bImportAsReferenceMaterialEnabled ? + InObject->GetMaterialReferences() : + TArray(); + + bSuccess = FHoudiniInputTranslator::CreateInputNodeForReference( + CreatedNodeId, + SK, + SKCName, + ImportAsReferenceTransform, + InInputSettings.bImportAsReferenceRotScaleEnabled, + InputNodeHandle, + bInputNodesCanBeDeleted, + InInputSettings.bImportAsReferenceBboxEnabled, + InBbox, + InInputSettings.bImportAsReferenceMaterialEnabled, + MaterialReferences); + } + else + { + FUnrealMeshExportOptions ExportInputOptions; + ExportInputOptions.bLODs = InInputSettings.bExportLODs; + ExportInputOptions.bSockets = InInputSettings.bExportSockets; + ExportInputOptions.bColliders = InInputSettings.bExportColliders; + ExportInputOptions.bMainMesh = InInputSettings.bExportMainGeometry; + + bSuccess = FUnrealSkeletalMeshTranslator::HapiCreateInputNodeForSkeletalMesh( + SK, + CreatedNodeId, + SKCName, + InputNodeHandle, + SKC, + ExportInputOptions, + bInputNodesCanBeDeleted, + InInputSettings.bExportMaterialParameters); + } + + // Create/update the node in the input manager + { + FUnrealObjectInputOptions Options = InputNodeHandle.GetIdentifier().GetOptions(); + constexpr bool bIsLeaf = false; + FUnrealObjectInputIdentifier SKCIdentifier(SKC, Options, bIsLeaf); + FUnrealObjectInputHandle Handle; + if (!FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(SKCIdentifier, { InputNodeHandle }, Handle, true, bInputNodesCanBeDeleted)) + return false; + + FUnrealObjectInputUtils::GetHAPINodeId(Handle, CreatedNodeId); + InObject->InputNodeHandle = Handle; + + // Create the output modifier chain if missing + const FName ModifierChainName(FUnrealObjectInputNode::OutputChainName); + if (!FUnrealObjectInputUtils::DoesModifierChainExist(InObject->InputNodeHandle, ModifierChainName)) + FUnrealObjectInputUtils::AddModifierChain(InObject->InputNodeHandle, ModifierChainName, CreatedNodeId); + else + FUnrealObjectInputUtils::SetModifierChainNodeToConnectTo(InObject->InputNodeHandle, ModifierChainName, CreatedNodeId); + + // Make sure that material overrides modifier exists and is correctly configured for this component's input node + if (FUnrealObjectInputModifier* MatOverridesModifier = FUnrealObjectInputUtils::FindFirstModifierOfType(InObject->InputNodeHandle, ModifierChainName, EUnrealObjectInputModifierType::MaterialOverrides)) + { + FUnrealObjectInputMaterialOverrides* const MatOverrides = static_cast(MatOverridesModifier); + if (InInputSettings.bImportAsReference) + { + if (InInputSettings.bImportAsReferenceMaterialEnabled) + { + if (MatOverrides) + MatOverrides->SetUsePrimWrangle(false); + } + else + { + FUnrealObjectInputUtils::DestroyModifier(InObject->InputNodeHandle, ModifierChainName, MatOverridesModifier); + } + } + else + { + if (MatOverrides) + MatOverrides->SetUsePrimWrangle(true); + } + } + else + { + if (InInputSettings.bImportAsReference) + { + if (InInputSettings.bImportAsReferenceMaterialEnabled) + FUnrealObjectInputUtils::CreateAndAddModifier(InObject->InputNodeHandle, ModifierChainName, SKC, false); + } + else + { + FUnrealObjectInputUtils::CreateAndAddModifier(InObject->InputNodeHandle, ModifierChainName, SKC, true); + } + } + + // Ensure that the physical material override modifier exists for this component's input node and is correctly configured + const HAPI_AttributeOwner PhysMatOverrideAttrOwner = InInputSettings.bImportAsReference ? HAPI_ATTROWNER_POINT : HAPI_ATTROWNER_PRIM; + if (FUnrealObjectInputModifier* PhysMatOverrideModifier = FUnrealObjectInputUtils::FindFirstModifierOfType(InObject->InputNodeHandle, ModifierChainName, EUnrealObjectInputModifierType::PhysicalMaterialOverride)) + { + if (FUnrealObjectInputPhysicalMaterialOverride* const PhysMatOverride = static_cast(PhysMatOverrideModifier)) + PhysMatOverride->SetAttributeOwner(PhysMatOverrideAttrOwner); + } + else + { + FUnrealObjectInputUtils::CreateAndAddModifier(InObject->InputNodeHandle, ModifierChainName, SKC, PhysMatOverrideAttrOwner); + } + + // Data layer Modifier + FUnrealObjectInputModifier* DataLayerModifier = FUnrealObjectInputUtils::FindFirstModifierOfType(InObject->InputNodeHandle, ModifierChainName, EUnrealObjectInputModifierType::DataLayerGroups); + if (!DataLayerModifier) + { + FUnrealObjectInputUtils::CreateAndAddModifier(InObject->InputNodeHandle, ModifierChainName, SKC->GetOwner()); + } + + // HLODs + FUnrealObjectInputModifier* HLODModifier = FUnrealObjectInputUtils::FindFirstModifierOfType(Handle, ModifierChainName, EUnrealObjectInputModifierType::HLODAttributes); + if (!HLODModifier) + { + FUnrealObjectInputUtils::CreateAndAddModifier(Handle, ModifierChainName, SKC->GetOwner()); + } + + // Actor and Component tags + FUnrealObjectInputModifier* TagsModifier = FUnrealObjectInputUtils::FindFirstModifierOfType(Handle, ModifierChainName, EUnrealObjectInputModifierType::ActorProperties); + if(!TagsModifier) + { + FUnrealObjectInputUtils::CreateAndAddModifier(Handle, ModifierChainName, SKC); + } + + // Custom Primitive Data - as prim attributes + if (FUnrealObjectInputModifier* Modifier = FUnrealObjectInputUtils::FindFirstModifierOfType(Handle, ModifierChainName, EUnrealObjectInputModifierType::CustomPrimitiveData)) + { + FUnrealObjectInputCustomPrimitiveData* const CustomPrimDataModifier + = static_cast(Modifier); + if (CustomPrimDataModifier) + CustomPrimDataModifier->SetUsePrimWrangle(true); + } + else + { + FUnrealObjectInputUtils::CreateAndAddModifier( + Handle, ModifierChainName, SKC, true); + } + + // Update all modifiers + FUnrealObjectInputUtils::UpdateAllModifierChains(InObject->InputNodeHandle); + } + + // Update this input object's OBJ NodeId + InObject->SetInputNodeId(CreatedNodeId); + InObject->SetInputObjectNodeId(FHoudiniEngineUtils::HapiGetParentNodeId(CreatedNodeId)); + + // Update this input object's cache data + InObject->Update(SKC, InInputSettings); + + // Update the component's transform + FTransform ComponentTransform = InObject->GetHoudiniObjectTransform(); + + { + // convert to HAPI_Transform + HAPI_TransformEuler HapiTransform; + FHoudiniApi::TransformEuler_Init(&HapiTransform); + FHoudiniEngineUtils::TranslateUnrealTransform(ComponentTransform, HapiTransform); + + // Set the transform on the OBJ parent + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + FHoudiniEngine::Get().GetSession(), InObject->GetInputObjectNodeId(), &HapiTransform), false); + } + + return bSuccess; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForGeometryCollection( + const FString& InObjNodeName, + UHoudiniInputGeometryCollection* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForGeometryCollection); + + if (!IsValid(InObject)) + return false; + + UGeometryCollection* GeometryCollection = InObject->GetGeometryCollection(); + if (!IsValid(GeometryCollection)) + return false; + + FString GCName = InObjNodeName + TEXT("_") + GeometryCollection->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(GCName); + + // TODO: Add support for the new input sytem! + FUnrealObjectInputHandle GCInputNodeHandle; + HAPI_NodeId CreatedNodeId = -1; + + // Get the existing node id, if any + + // For the ref counted system the handle on the input object represents a reference node that has a single node + // it references: the geometry collection. The reference node represents InObject with its Transform (geometry input). + { + TSet ReferencedNodes; + if (FUnrealObjectInputUtils::GetReferencedNodes(InObject->InputNodeHandle, ReferencedNodes) && ReferencedNodes.Num() == 1) + { + const FUnrealObjectInputHandle Handle = ReferencedNodes.Array()[0]; + FUnrealObjectInputUtils::GetHAPINodeId(Handle, CreatedNodeId); + } + } + + // Marshall the GeometryCollection to Houdini + bool bSuccess = true; + if (InInputSettings.bImportAsReference) + { + TManagedArray& BboxArray = GeometryCollection->GetGeometryCollection()->BoundingBox; + FBox InBbox = FBox(EForceInit::ForceInitToZero); + if (InInputSettings.bImportAsReferenceBboxEnabled) + { + for (FBox& Bbox : BboxArray) + InBbox += Bbox; + } + + const TArray& MaterialReferences = InInputSettings.bImportAsReferenceMaterialEnabled ? + InObject->GetMaterialReferences() : + TArray(); + + bSuccess = FHoudiniInputTranslator::CreateInputNodeForReference( + CreatedNodeId, + GeometryCollection, + GCName, + InObject->GetTransform(), + InInputSettings.bImportAsReferenceRotScaleEnabled, + GCInputNodeHandle, + bInputNodesCanBeDeleted, + InInputSettings.bImportAsReferenceBboxEnabled, + InBbox, + InInputSettings.bImportAsReferenceMaterialEnabled, + MaterialReferences); + } + else + { + bSuccess = FUnrealGeometryCollectionTranslator::HapiCreateInputNodeForGeometryCollection( + GeometryCollection, + CreatedNodeId, + GCName, + GCInputNodeHandle, + InInputSettings.bExportMaterialParameters, + nullptr, + bInputNodesCanBeDeleted); + } + + { + // The geometry collection can have its own transform (geometry input), so we have to create a reference node that + // represents InObject in the new input system that references the GeometryCollection asset's input node handle + FUnrealObjectInputOptions Options; + static constexpr bool bIsLeaf = false; + FUnrealObjectInputIdentifier GeoInputRefNodeId(InObject, Options, bIsLeaf); + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(GeoInputRefNodeId, { GCInputNodeHandle }, InObject->InputNodeHandle, true, bInputNodesCanBeDeleted); + } + + if (!HapiSetGeoObjectTransform(InObject->GetInputObjectNodeId(), InObject->GetHoudiniObjectTransform())) + return false; + + // Update the cached data and input settings + InObject->Update(GeometryCollection, InInputSettings); + + // // If the Input mesh has a Transform offset + // FTransform TransformOffset = InObject->Transform; + // if (!TransformOffset.Equals(FTransform::Identity)) + // { + // // Updating the Transform + // HAPI_TransformEuler HapiTransform; + // FHoudiniApi::TransformEuler_Init(&HapiTransform); + // FHoudiniEngineUtils::TranslateUnrealTransform(TransformOffset, HapiTransform); + // + // // Set the transform on the OBJ parent + // HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + // FHoudiniEngine::Get().GetSession(), InObject->InputObjectNodeId, &HapiTransform), false); + // } + + return bSuccess; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForGeometryCollectionComponent( + const FString& InObjNodeName, + UHoudiniInputGeometryCollectionComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForGeometryCollectionComponent); + + if (!IsValid(InObject)) + return false; + + UGeometryCollectionComponent* GCC = InObject->GetGeometryCollectionComponent(); + if (!IsValid(GCC)) + return true; + + // Get the component's GeometryCollection + UGeometryCollection* GC = InObject->GetGeometryCollection(); + if (!IsValid(GC)) + return true; + + HAPI_NodeId CreatedNodeId = InObject->GetInputNodeId(); + + // Marshall the GeometryCollection to Houdini + FString GCCName = InObjNodeName + TEXT("_") + GCC->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(GCCName); + + FUnrealObjectInputHandle InputNodeHandle; + bool bSuccess = true; + if (InInputSettings.bImportAsReference) + { + FTransform ImportAsReferenceTransform = InObject->GetTransform(); + + // Previously, ImportAsReferenceTransform was multiplied by + // InActorTransform.Inverse() if bKeepWorldTransform was true, + // but this created a double transform issue. + ImportAsReferenceTransform.SetLocation(FVector::ZeroVector); + + TManagedArray& BboxArray = GC->GetGeometryCollection()->BoundingBox; + FBox InBbox = FBox(EForceInit::ForceInitToZero); + if (InInputSettings.bImportAsReferenceBboxEnabled) + { + for (FBox& Bbox : BboxArray) + InBbox += Bbox; + } + + const TArray& MaterialReferences = InInputSettings.bImportAsReferenceMaterialEnabled ? + InObject->GetMaterialReferences() : + TArray(); + + bSuccess = FHoudiniInputTranslator::CreateInputNodeForReference( + CreatedNodeId, + GC, + GCCName, + ImportAsReferenceTransform, + InInputSettings.bImportAsReferenceRotScaleEnabled, + InputNodeHandle, + bInputNodesCanBeDeleted, + InInputSettings.bImportAsReferenceBboxEnabled, + InBbox, + InInputSettings.bImportAsReferenceMaterialEnabled, + MaterialReferences); + } + else + { + bSuccess = FUnrealGeometryCollectionTranslator::HapiCreateInputNodeForGeometryCollection( + GC, + CreatedNodeId, + GCCName, + InputNodeHandle, + InInputSettings.bExportMaterialParameters, + GCC, + bInputNodesCanBeDeleted); + } + + // Create/update the node in the input manager + { + FUnrealObjectInputOptions Options = InputNodeHandle.GetIdentifier().GetOptions(); + constexpr bool bIsLeaf = false; + FUnrealObjectInputIdentifier GCCIdentifier(GCC, Options, bIsLeaf); + FUnrealObjectInputHandle Handle; + if (!FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(GCCIdentifier, { InputNodeHandle }, Handle, true, bInputNodesCanBeDeleted)) + return false; + + FUnrealObjectInputUtils::GetHAPINodeId(Handle, CreatedNodeId); + InObject->InputNodeHandle = Handle; + } + + // Update this input object's OBJ NodeId + InObject->SetInputNodeId(CreatedNodeId); + InObject->SetInputObjectNodeId(FHoudiniEngineUtils::HapiGetParentNodeId(CreatedNodeId)); + + // Update this input object's cache data + InObject->Update(GCC, InInputSettings); + + // Update the component's transform + FTransform ComponentTransform = InObject->GetHoudiniObjectTransform(); + { + // convert to HAPI_Transform + HAPI_TransformEuler HapiTransform; + FHoudiniApi::TransformEuler_Init(&HapiTransform); + FHoudiniEngineUtils::TranslateUnrealTransform(ComponentTransform, HapiTransform); + + // Set the transform on the OBJ parent + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform(FHoudiniEngine::Get().GetSession(), InObject->GetInputObjectNodeId(), &HapiTransform), false); + } + + return bSuccess; +} + + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForSceneComponent( + const FString& InObjNodeName, + UHoudiniInputSceneComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForSceneComponent); + + if (!IsValid(InObject)) + return false; + + USceneComponent* SceneComp = InObject->GetSceneComponent(); + if (!IsValid(SceneComp)) + return true; + + // Get the Scene Component's transform + FTransform TransformOffset = InObject->GetTransform(); + + // Get the parent Actor's transform + FTransform ParentTransform = InObject->ActorTransform; + + // Don't do that! + return false; + /* + // TODO + // Support this type of input object + return HapiCreateInputNodeForObject(InObjNodeName, InObject); + */ +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForStaticMeshComponent( + const FString& InObjNodeName, + UHoudiniInputMeshComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForStaticMeshComponent); + + if (!IsValid(InObject)) + return false; + + UStaticMeshComponent* SMC = InObject->GetStaticMeshComponent(); + + if (!IsValid(SMC)) + return true; + + // Get the component's Static Mesh + UStaticMesh* SM = SMC->GetStaticMesh(); + if (!IsValid(SM)) + return true; + + HAPI_NodeId CreatedNodeId = InObject->GetInputNodeId(); + + // Marshall the Static Mesh to Houdini + FString SMCName = InObjNodeName + TEXT("_") + SMC->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(SMCName); + + // Does the component generate unique to it, or does it use an asset directly? In cases where the component + // generates its own data (perhaps derived from an asset, such as a static mesh) there will be no separation + // between the component and the data (asset, for example StaticMesh input) in the ref counted input system. For + // example StaticMeshComponent uses a StaticMesh, those create separate nodes for the component and the asset (and + // its variations) in the input system. But a SplineMeshComponent generates a deformed mesh unique to it, so + // the component's node also acts as the main/reference node for what would be the asset data it uses (although + // additional nodes can be created for options/variations). + const bool bComponentGeneratesData = SMC->IsA(); + + FUnrealObjectInputHandle InputNodeHandle; + bool bSuccess = true; + if (InInputSettings.bImportAsReference) + { + FTransform ImportAsReferenceTransform = InObject->GetTransform(); + + // Previously, ImportAsReferenceTransform was multiplied by + // InActorTransform.Inverse() if bKeepWorldTransform was true, + // but this created a double transform issue. + ImportAsReferenceTransform.SetLocation(FVector::ZeroVector); + + FBox InBbox = InInputSettings.bImportAsReferenceBboxEnabled ? + SM->GetBoundingBox() : + FBox(EForceInit::ForceInit); + + const TArray& MaterialReferences = InInputSettings.bImportAsReferenceMaterialEnabled ? + InObject->GetMaterialReferences() : + TArray(); + + bSuccess = FHoudiniInputTranslator::CreateInputNodeForReference( + CreatedNodeId, + SM, + SMCName, + ImportAsReferenceTransform, + InInputSettings.bImportAsReferenceRotScaleEnabled, + InputNodeHandle, + true, + InInputSettings.bImportAsReferenceBboxEnabled, + InBbox, + InInputSettings.bImportAsReferenceMaterialEnabled, + MaterialReferences); + } + else + { + FUnrealMeshExportOptions ExportOptions; + ExportOptions.bLODs = InInputSettings.bExportLODs; + ExportOptions.bSockets = InInputSettings.bExportSockets; + ExportOptions.bColliders = InInputSettings.bExportColliders; + ExportOptions.bMainMesh = InInputSettings.bExportMainGeometry; + ExportOptions.bMaterialParameters = InInputSettings.bExportMaterialParameters; + ExportOptions.bPreferNaniteFallbackMesh = InInputSettings.bPreferNaniteFallbackMesh; + + bSuccess = FUnrealMeshTranslator::CreateInputNodeForStaticMesh( + CreatedNodeId, + InputNodeHandle, + SM, + SMC, + SMCName, + ExportOptions, + bInputNodesCanBeDeleted, + bComponentGeneratesData); + } + + // Create/update the node in the input manager if the static mesh component uses an asset directly. + { + if (!bComponentGeneratesData) + { + FUnrealObjectInputOptions Options = InputNodeHandle.GetIdentifier().GetOptions(); + constexpr bool bIsLeaf = false; + FUnrealObjectInputIdentifier SMCIdentifier(SMC, Options, bIsLeaf); + FUnrealObjectInputHandle Handle; + if (!FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(SMCIdentifier, {InputNodeHandle}, Handle, true, bInputNodesCanBeDeleted)) + return false; + + FUnrealObjectInputUtils::GetHAPINodeId(Handle, CreatedNodeId); + InObject->InputNodeHandle = Handle; + } + else + { + InObject->InputNodeHandle = InputNodeHandle; + } + + // Create the output modifier chain if missing + const FName ModifierChainName(FUnrealObjectInputNode::OutputChainName); + if (!FUnrealObjectInputUtils::DoesModifierChainExist(InObject->InputNodeHandle, ModifierChainName)) + FUnrealObjectInputUtils::AddModifierChain(InObject->InputNodeHandle, ModifierChainName, CreatedNodeId); + else + FUnrealObjectInputUtils::SetModifierChainNodeToConnectTo(InObject->InputNodeHandle, ModifierChainName, CreatedNodeId); + + // Make sure that material overrides modifier exists and is correctly configured for this component's input node + if (FUnrealObjectInputModifier* MatOverridesModifier = FUnrealObjectInputUtils::FindFirstModifierOfType(InObject->InputNodeHandle, ModifierChainName, EUnrealObjectInputModifierType::MaterialOverrides)) + { + FUnrealObjectInputMaterialOverrides* const MatOverrides = static_cast(MatOverridesModifier); + if (InInputSettings.bImportAsReference) + { + if (InInputSettings.bImportAsReferenceMaterialEnabled) + { + if (MatOverrides) + MatOverrides->SetUsePrimWrangle(false); + } + else + { + FUnrealObjectInputUtils::DestroyModifier(InObject->InputNodeHandle, ModifierChainName, MatOverridesModifier); + } + } + else + { + if (MatOverrides) + MatOverrides->SetUsePrimWrangle(true); + } + } + else + { + if (InInputSettings.bImportAsReference) + { + if (InInputSettings.bImportAsReferenceMaterialEnabled) + FUnrealObjectInputUtils::CreateAndAddModifier(InObject->InputNodeHandle, ModifierChainName, SMC, false); + } + else + { + FUnrealObjectInputUtils::CreateAndAddModifier(InObject->InputNodeHandle, ModifierChainName, SMC, true); + } + } + + // Ensure that the physical material override modifier exists for this component's input node and is correctly configured + const HAPI_AttributeOwner PhysMatOverrideAttrOwner = InInputSettings.bImportAsReference ? HAPI_ATTROWNER_POINT : HAPI_ATTROWNER_PRIM; + if (FUnrealObjectInputModifier* PhysMatOverrideModifier = FUnrealObjectInputUtils::FindFirstModifierOfType(InObject->InputNodeHandle, ModifierChainName, EUnrealObjectInputModifierType::PhysicalMaterialOverride)) + { + FUnrealObjectInputPhysicalMaterialOverride* const PhysMatOverride = static_cast(PhysMatOverrideModifier); + if (PhysMatOverride) + PhysMatOverride->SetAttributeOwner(PhysMatOverrideAttrOwner); + } + else + { + FUnrealObjectInputUtils::CreateAndAddModifier(InObject->InputNodeHandle, ModifierChainName, SMC, PhysMatOverrideAttrOwner); + } + + // Data layer Modifier + if (FUnrealObjectInputModifier* DataLayerModifier = FUnrealObjectInputUtils::FindFirstModifierOfType(InObject->InputNodeHandle, ModifierChainName, EUnrealObjectInputModifierType::DataLayerGroups)) + { + // nothing for now + } + else + { + FUnrealObjectInputUtils::CreateAndAddModifier(InObject->InputNodeHandle, ModifierChainName, SMC->GetOwner()); + } + + // HLODs + FUnrealObjectInputModifier* HLODModifier = FUnrealObjectInputUtils::FindFirstModifierOfType(InObject->InputNodeHandle, ModifierChainName, EUnrealObjectInputModifierType::HLODAttributes); + if (!HLODModifier) + { + FUnrealObjectInputUtils::CreateAndAddModifier(InObject->InputNodeHandle, ModifierChainName, SMC->GetOwner()); + } + + // Actor properties + FUnrealObjectInputModifier* TagsModifier = FUnrealObjectInputUtils::FindFirstModifierOfType(InObject->InputNodeHandle, ModifierChainName, EUnrealObjectInputModifierType::ActorProperties); + if(!TagsModifier) + { + FUnrealObjectInputUtils::CreateAndAddModifier(InObject->InputNodeHandle, ModifierChainName, SMC); + } + + // Custom Primitive Data + //const HAPI_AttributeOwner CPDAttrOwner = InInputSettings.bImportAsReference ? HAPI_ATTROWNER_POINT : HAPI_ATTROWNER_PRIM; + const bool bUsePrimWrangle = !InInputSettings.bImportAsReference; + if (FUnrealObjectInputModifier* Modifier = FUnrealObjectInputUtils::FindFirstModifierOfType(InObject->InputNodeHandle, ModifierChainName, EUnrealObjectInputModifierType::CustomPrimitiveData)) + { + FUnrealObjectInputCustomPrimitiveData* const CustomPrimDataModifier + = static_cast(Modifier); + if (CustomPrimDataModifier) + CustomPrimDataModifier->SetUsePrimWrangle(bUsePrimWrangle); + } + else + { + FUnrealObjectInputUtils::CreateAndAddModifier( + InObject->InputNodeHandle, ModifierChainName, SMC, bUsePrimWrangle); + } + + // Update all modifiers + FUnrealObjectInputUtils::UpdateAllModifierChains(InObject->InputNodeHandle); + } + + // Update this input object's OBJ NodeId + InObject->SetInputNodeId(CreatedNodeId); + InObject->SetInputObjectNodeId(FHoudiniEngineUtils::HapiGetParentNodeId(CreatedNodeId)); + + // Update this input object's cache data + InObject->Update(SMC, InInputSettings); + + // Update the component's transform + FTransform ComponentTransform = InObject->GetHoudiniObjectTransform(); + { + // convert to HAPI_Transform + HAPI_TransformEuler HapiTransform; + FHoudiniApi::TransformEuler_Init(&HapiTransform); + FHoudiniEngineUtils::TranslateUnrealTransform(ComponentTransform, HapiTransform); + + // Set the transform on the OBJ parent + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + FHoudiniEngine::Get().GetSession(), InObject->GetInputObjectNodeId(), &HapiTransform), false); + } + + return bSuccess; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForSplineMeshComponents( + const FString& InObjNodeName, + UHoudiniInputActor* InParentActorObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForSplineMeshComponents); + + if (!IsValid(InParentActorObject)) + return false; + + UHoudiniInputSplineMeshComponent* FirstSMCObject = nullptr; + TArray MeshComponents; + TArray SMCObjects; + for (UHoudiniInputSceneComponent* Component : InParentActorObject->GetActorComponents()) + { + if (!IsValid(Component)) + continue; + + UHoudiniInputSplineMeshComponent* const SMCObject = Cast(Component); + if (!IsValid(SMCObject)) + continue; + + SMCObjects.Add(SMCObject); + + // Since we are going to send this SMC as part of a merged mesh for this input we can invalidate the single + // mesh case here + SMCObject->InvalidateData(); + + if (!FirstSMCObject) + FirstSMCObject = SMCObject; + + USplineMeshComponent* const SMC = SMCObject->GetSplineMeshComponent(); + if (!IsValid(SMC)) + continue; + + MeshComponents.Add(SMC); + } + + if (MeshComponents.IsEmpty()) + return true; + + if (!IsValid(FirstSMCObject)) + return true; + + USplineMeshComponent* FirstSMC = FirstSMCObject->GetSplineMeshComponent(); + + // Generate a static mesh from the spline mesh components + AActor* const ParentActor = InParentActorObject->GetActor(); + + FHoudiniPackageParams PackageParams; + PackageParams.PackageMode = EPackageMode::CookToTemp; + PackageParams.ReplaceMode = EPackageReplaceMode::ReplaceExistingAssets; + PackageParams.HoudiniAssetActorName = ParentActor->GetActorNameOrLabel(); + PackageParams.HoudiniAssetName = ParentActor->GetClass()->GetName(); + PackageParams.ObjectName = FirstSMC->GetName(); + PackageParams.ComponentGUID = InParentActorObject->GetSplinesMeshPackageGuid(); + FMeshMergingSettings Settings; + UStaticMesh* SM = nullptr; + + FVector MergedLocation = FVector::ZeroVector; + if (!FHoudiniMeshUtils::MergeMeshes(MeshComponents, PackageParams, Settings, SM, MergedLocation)) + return true; + + if (!IsValid(SM)) + return true; + + InParentActorObject->SetGeneratedSplineMesh(SM); + + HAPI_NodeId CreatedNodeId = InParentActorObject->SplinesMeshNodeId; + + // Marshall the Static Mesh to Houdini + FString SMCName = InObjNodeName + TEXT("_") + FirstSMC->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(SMCName); + + FUnrealMeshExportOptions ExportOptions; + ExportOptions.bLODs = InInputSettings.bExportLODs; + ExportOptions.bSockets = InInputSettings.bExportSockets; + ExportOptions.bColliders = InInputSettings.bExportColliders; + ExportOptions.bMainMesh = InInputSettings.bExportMainGeometry; + ExportOptions.bMaterialParameters = InInputSettings.bExportMaterialParameters; + ExportOptions.bPreferNaniteFallbackMesh = InInputSettings.bPreferNaniteFallbackMesh; + + FUnrealObjectInputHandle InputNodeHandle; + bool bSuccess = FUnrealMeshTranslator::CreateInputNodeForStaticMesh( + CreatedNodeId, + InputNodeHandle, + SM, + nullptr, + SMCName, + ExportOptions, + bInputNodesCanBeDeleted, + false); + + // Create/update the node in the input manager + { + FUnrealObjectInputOptions Options = InputNodeHandle.GetIdentifier().GetOptions(); + constexpr bool bIsLeaf = false; + FUnrealObjectInputIdentifier SMCIdentifier(FirstSMC, Options, bIsLeaf); + FUnrealObjectInputHandle Handle; + if (!FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(SMCIdentifier, {InputNodeHandle}, Handle, true, bInputNodesCanBeDeleted)) + return false; + + FUnrealObjectInputUtils::GetHAPINodeId(Handle, CreatedNodeId); + InParentActorObject->SplinesMeshInputNodeHandle = Handle; + } + + // Update this input object's OBJ NodeId + InParentActorObject->SplinesMeshNodeId = CreatedNodeId; + InParentActorObject->SplinesMeshObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(InParentActorObject->SplinesMeshNodeId); + + for (UHoudiniInputSplineMeshComponent* const SMCObject : SMCObjects) + { + if (!IsValid(SMCObject)) + continue; + + // Update this input object's cache data + SMCObject->Update(SMCObject->GetSplineMeshComponent(), InInputSettings); + } + + // Update the component's transform + FTransform Transform = FTransform::Identity; + Transform.SetTranslation(MergedLocation); + // When using the ref counted system we expected this transform to be relative to actor + { + Transform = Transform.GetRelativeTransform(InParentActorObject->GetTransform()); + } + { + // convert to HAPI_Transform + HAPI_TransformEuler HapiTransform; + FHoudiniApi::TransformEuler_Init(&HapiTransform); + FHoudiniEngineUtils::TranslateUnrealTransform(Transform, HapiTransform); + + // Set the transform on the OBJ parent + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + FHoudiniEngine::Get().GetSession(), InParentActorObject->SplinesMeshObjectNodeId, &HapiTransform), false); + } + + return bSuccess; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForInstancedStaticMeshComponent( + const FString& InObjNodeName, + UHoudiniInputInstancedMeshComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForInstancedStaticMeshComponent); + + if (!IsValid(InObject)) + return false; + + UObject* Object = InObject->GetObject(); + if (!IsValid(Object)) + return true; + + // Get the ISMC + UInstancedStaticMeshComponent* ISMC = InObject->GetInstancedStaticMeshComponent(); + if (!IsValid(ISMC)) + return true; + + HAPI_NodeId NewNodeId = -1; + FUnrealObjectInputHandle InputNodeHandle; + if (!FUnrealInstanceTranslator::HapiCreateInputNodeForInstancer( + ISMC, + InObjNodeName, + NewNodeId, + InputNodeHandle, + InInputSettings.bExportLODs, + InInputSettings.bExportSockets, + InInputSettings.bExportColliders, + false, + InInputSettings.bPreferNaniteFallbackMesh, + InInputSettings.bExportMaterialParameters, + bInputNodesCanBeDeleted)) + return false; + + // Update this input object's node IDs + InObject->SetInputNodeId(NewNodeId); + InObject->SetInputObjectNodeId(FHoudiniEngineUtils::HapiGetParentNodeId(NewNodeId)); + InObject->InputNodeHandle = InputNodeHandle; + + // Update the component's cached instances + InObject->Update(ISMC, InInputSettings); + + // Update the component's transform + const FTransform ComponentTransform = InObject->GetHoudiniObjectTransform(); + { + // convert to HAPI_Transform + HAPI_TransformEuler HapiTransform; + FHoudiniApi::TransformEuler_Init(&HapiTransform); + FHoudiniEngineUtils::TranslateUnrealTransform(ComponentTransform, HapiTransform); + + // Set the transform on the OBJ parent + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + FHoudiniEngine::Get().GetSession(), InObject->GetInputObjectNodeId(), &HapiTransform), false); + } + + return true; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForSplineComponent( + const FString& InObjNodeName, + UHoudiniInputSplineComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForSplineComponent); + + if (!IsValid(InObject)) + return false; + + USplineComponent* Spline = InObject->GetSplineComponent(); + if (!IsValid(Spline)) + return true; + + int32 NumberOfSplineControlPoints = InObject->NumberOfSplineControlPoints; + TArray SplineControlPoints = InObject->SplineControlPoints; + + FString SplineName = InObjNodeName + TEXT("_") + InObject->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(SplineName); + + FUnrealObjectInputHandle InputNodeHandle; + HAPI_NodeId CreatedNodeId = InObject->GetInputNodeId(); + + if (!FUnrealSplineTranslator::CreateInputNodeForSplineComponent( + Spline, CreatedNodeId, InputNodeHandle, InInputSettings.UnrealSplineResolution, SplineName, InInputSettings.bUseLegacyInputCurves, bInputNodesCanBeDeleted)) + return false; + + // Cache the exported curve's data to the input object + InObject->InputNodeHandle = InputNodeHandle; + InObject->SetInputNodeId((int32)CreatedNodeId); + InObject->SetInputObjectNodeId((int32)FHoudiniEngineUtils::HapiGetParentNodeId(CreatedNodeId)); + + InObject->MarkChanged(true); + + // Update the component's cached data + InObject->Update(Spline, InInputSettings); + + if (!HapiSetGeoObjectTransform(InObject->GetInputObjectNodeId(), InObject->GetHoudiniObjectTransform())) + return false; + + return true; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForHoudiniSplineComponent( + const FString& InObjNodeName, + UHoudiniInputHoudiniSplineComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForHoudiniSplineComponent); + + if (!IsValid(InObject)) + return false; + + UHoudiniSplineComponent* Curve = InObject->GetCurveComponent(); + if (!IsValid(Curve)) + return true; + + Curve->SetIsLegacyInputCurve(InInputSettings.bUseLegacyInputCurves); + + if (!FHoudiniSplineTranslator::HapiUpdateNodeForHoudiniSplineComponent(Curve, InInputSettings.bAddRotAndScaleAttributesOnCurves)) + return false; + + // See if the component needs it node Id invalidated + //if (InObject->InputNodeId < 0) + // Curve->SetNodeId(InObject->InputNodeId); + + // Cache the exported curve's data to the input object + InObject->SetInputNodeId(Curve->GetNodeId()); + InObject->SetInputObjectNodeId(FHoudiniEngineUtils::HapiGetParentNodeId(InObject->GetInputNodeId())); + + //InObject->CurveType = Curve->GetCurveType(); + //InObject->CurveMethod = Curve->GetCurveMethod(); + //InObject->Reversed = Curve->IsReversed(); + InObject->Update(Curve, InInputSettings); + + InObject->MarkChanged(true); + + return true; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForHoudiniCookable( + const FString& InObjNodeName, + UHoudiniInputHoudiniAsset* InObject, + const FHoudiniInputObjectSettings& InInputSettings) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForHoudiniCookable); + if (!IsValid(InObject)) + return false; + + UHoudiniCookable* InputHC = InObject->GetHoudiniCookable(); + if (!IsValid(InputHC)) + { + UHoudiniAssetComponent* InputHAC = InObject->GetHoudiniAssetComponent(); + if (!IsValid(InputHAC)) + return true; + + if (!InputHAC->CanDeleteHoudiniNodes()) + return true; + + InputHC = InputHAC->GetCookable(); + if (!IsValid(InputHC)) + return true; + } + + UHoudiniInput* HoudiniInput = Cast(InObject->GetOuter()); + if (!IsValid(HoudiniInput)) + return true; + + UHoudiniCookable* OuterHC = Cast(HoudiniInput->GetOuter()); + if (!IsValid(OuterHC)) + return true; + + // Do not allow using ourself as an input, terrible things would happen + if (InputHC->GetNodeId() == OuterHC->GetNodeId()) + return false; + + // If previously imported as ref, delete the input node. + if (InObject->GetInputNodeId() > -1 && InObject->GetImportAsReference()) + { + int32 PreviousInputNodeId = InObject->GetInputNodeId(); + // Get the parent OBJ node ID before deleting! + HAPI_NodeId PreviousInputOBJNode = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousInputNodeId); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input node for %s."), *InObject->GetName()); + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputOBJNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input OBJ node for %s."), *InObject->GetName()); + } + } + + // If this object is in an Asset input, we need to set the InputNodeId directly + // to avoid creating extra merge nodes. World inputs should not do that! + bool bIsAssetInput = HoudiniInput->IsAssetInput(); + + if (InInputSettings.bImportAsReference) + { + InObject->SetInputNodeId(-1); + InObject->SetInputObjectNodeId(-1); + + if(bIsAssetInput) + HoudiniInput->SetInputNodeId(-1); + + FString HAName = InObject->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(HAName); + + HAPI_NodeId InputNodeId = InObject->GetInputNodeId(); + constexpr bool bUseRefCountedInputSystem = false; + FUnrealObjectInputHandle InputNodeHandle; + if (!FHoudiniInputTranslator::CreateInputNodeForReference( + InputNodeId, + InputHC->GetOwner(), + HAName, + InObject->GetTransform(), + InInputSettings.bImportAsReferenceRotScaleEnabled, + InputNodeHandle, + InObject->CanDeleteHoudiniNodes())) // do not delete previous node if it was HC + return false; + + InObject->SetInputNodeId(InputNodeId); + if (bIsAssetInput) + HoudiniInput->SetInputNodeId(InObject->GetInputNodeId()); + } + + InputHC->AddDownstreamCookable(OuterHC); + + // TODO: This might be uneeded as this function should only be called + // after we're not waiting on the input asset... + if (InputHC->GetCurrentState() == EHoudiniAssetState::NeedInstantiation) + { + // If the input HAC needs to be instantiated, tell it do so + InputHC->SetCurrentState(EHoudiniAssetState::PreInstantiation); + // Mark this object's input as changed so we can properly update after the input HDA's done instantiating/cooking + HoudiniInput->MarkChanged(true); + } + + //if (InputHC->NeedsInitialization() || InputHC->NeedUpdate()) + if (InputHC->NeedUpdate()) + return false; + + if (!InInputSettings.bImportAsReference) + { + if (bIsAssetInput) + HoudiniInput->SetInputNodeId(InputHC->GetNodeId()); + + InObject->SetInputNodeId(InputHC->GetNodeId()); + } + + InObject->SetInputObjectNodeId(InObject->GetInputNodeId()); + + bool bReturn = InObject->GetInputNodeId() > -1; + if(bIsAssetInput) + bReturn = FHoudiniInputTranslator::ConnectInputNode(HoudiniInput); + + // Update the cached data and input settings + InObject->Update(InputHC, InInputSettings); + + return bReturn; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodesForActorComponents( + UHoudiniInput* const InInput, + UHoudiniInputActor* const InInputActorObject, + AActor* const InActor, + const FTransform& InActorTransform, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodesForActorComponents); + + if (!IsValid(InInput)) + return false; + + if (!IsValid(InInputActorObject)) + return false; + + if (!IsValid(InActor)) + return true; + + const FHoudiniInputObjectSettings InputSettings(InInput); + + const bool bMergeSplineMeshes = InputSettings.bMergeSplineMeshComponents && InInputActorObject->GetNumSplineMeshComponents() > 1; + // If we are not sending a merged mesh, invalidate any previous merge mesh data so that it can be cleaned up + if (!bMergeSplineMeshes) + { + InInputActorObject->InvalidateSplinesMeshData(); + } + + // Now, commit all of this actor's component + bool bHasSplineMeshComponentsToMerge = false; + int32 ComponentIdx = 0; + for (UHoudiniInputSceneComponent* CurComponent : InInputActorObject->GetActorComponents()) + { + const bool bIsSplineMeshComponent = CurComponent->IsA(); + if (bMergeSplineMeshes && bIsSplineMeshComponent) + { + CurComponent->InvalidateData(); + bHasSplineMeshComponentsToMerge = true; + continue; + } + + if (UploadHoudiniInputObject(InInput, CurComponent, InActorTransform, OutCreatedNodeIds, OutHandles, bInputNodesCanBeDeleted)) + ComponentIdx++; + + // If we're importing the actor as ref, add the level path / actor path attribute to the created nodes + if (InInput->GetImportAsReference()) + { + // When using the ref counted input system: the nodes are created differently so we cannot just add attributes + // to the input node (it is likely a merge and not an input null). For the new system we add a modifier to + // the output modifier chain. + if (!CurComponent->InputNodeHandle.IsValid()) + { + bool bNeedCommit = false; + if (FHoudiniEngineUtils::AddLevelPathAttribute(CurComponent->GetInputNodeId(), 0, InActor->GetLevel(), 1, HAPI_ATTROWNER_POINT)) + bNeedCommit = true; + + if(FHoudiniEngineUtils::AddActorPathAttribute(CurComponent->GetInputNodeId(), 0, InActor, 1, HAPI_ATTROWNER_POINT)) + bNeedCommit = true; + + // Commit the geo if needed + if(bNeedCommit) + FHoudiniEngineUtils::HapiCommitGeo(CurComponent->GetInputNodeId()); + } + else + { + const FName ChainName(FUnrealObjectInputNode::OutputChainName); + if (!FUnrealObjectInputUtils::DoesModifierChainExist(CurComponent->InputNodeHandle, ChainName)) + FUnrealObjectInputUtils::AddModifierChain(CurComponent->InputNodeHandle, ChainName, CurComponent->GetInputNodeId()); + if (!FUnrealObjectInputUtils::FindFirstModifierOfType(CurComponent->InputNodeHandle, ChainName, EUnrealObjectInputModifierType::ActorAsReference)) + FUnrealObjectInputUtils::CreateAndAddModifier(CurComponent->InputNodeHandle, ChainName, InActor); + + FUnrealObjectInputUtils::UpdateModifiers(CurComponent->InputNodeHandle, ChainName); + } + } + } + + if (bHasSplineMeshComponentsToMerge) + { + InInputActorObject->SetUsedMergeSplinesMeshAtLastTranslate(true); + if (HapiCreateInputNodeForSplineMeshComponents( + InInput->GetNodeBaseName(), + InInputActorObject, + InputSettings, + bInputNodesCanBeDeleted)) + { + OutCreatedNodeIds.Add(InInputActorObject->SplinesMeshObjectNodeId); + OutHandles.Add(InInputActorObject->SplinesMeshInputNodeHandle); + } + } + else + { + InInputActorObject->SetUsedMergeSplinesMeshAtLastTranslate(false); + } + + return true; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForActor( + UHoudiniInput* InInput, + UHoudiniInputActor* InObject, + const FTransform & InActorTransform, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForActor); + + if (!IsValid(InInput)) + return false; + + if (!IsValid(InObject)) + return false; + + AActor* Actor = InObject->GetActor(); + if (!IsValid(Actor)) + return true; + + const FHoudiniInputObjectSettings InputSettings(InInput); + + // Check if this is a world input and if this is a HoudiniAssetActor + // If so we need to build static meshes for any proxy meshes + if ((InInput->GetInputType() == EHoudiniInputType::World) + && Actor->IsA()) + { + AHoudiniAssetActor *HAA = Cast(Actor); + UHoudiniCookable* HC = HAA->GetHoudiniCookable(); + if (IsValid(HC)) + { + if (HC->HasAnyCurrentProxyOutput()) + { + bool bPendingDeleteOrRebuild = false; + bool bInvalidState = false; + const bool bIsHoudiniCookedDataAvailable = HC->IsHoudiniCookedDataAvailable(bPendingDeleteOrRebuild, bInvalidState); + if (bIsHoudiniCookedDataAvailable) + { + // Build the static mesh + FHoudiniOutputTranslator::BuildStaticMeshesOnHoudiniProxyMeshOutputs(HC); + // Update the input object since a new StaticMeshComponent could have been created + UObject *InputObject = InObject->GetObject(); + if (IsValid(InputObject)) + { + InObject->Update(InputObject, InputSettings); + TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); + } + } + else if (!bPendingDeleteOrRebuild && !bInvalidState) + { + // Request a cook with no proxy output + HC->MarkAsNeedCook(); + HC->SetNoProxyMeshNextCookRequested(true); + } + } + else if (InObject->GetActorComponents().Num() == 0 && HC->HasAnyOutputComponent()) + { + // The HAC has non-proxy output components, but the InObject does not have any + // actor components. This can arise after a cook if previously there were only + // proxies and the input was created when there were only proxies + // Try to update the input to find new components + UObject *InputObject = InObject->GetObject(); + if (IsValid(InputObject)) + { + InObject->Update(InputObject, InputSettings); + TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); + } + } + } + } + + // Now, commit all of this actor's component + TArray CreatedNodeIds; + TSet Handles; + bool bSuccess = false; + if (InputSettings.bImportAsReference) + bSuccess = HapiCreateInputNodeForActorReference(InObject, InputSettings, bInputNodesCanBeDeleted); + else + bSuccess = HapiCreateInputNodesForActorComponents(InInput, InObject, Actor, InActorTransform, CreatedNodeIds, Handles, bInputNodesCanBeDeleted); + + // Cache our transformn + InObject->SetTransform(Actor->GetTransform()); + + InObject->Update(Actor, InputSettings); + + if (!InputSettings.bImportAsReference) + { + // Make a reference node for the actor + const FUnrealObjectInputOptions Options = FUnrealObjectInputOptions::MakeOptionsForGenericActor(InputSettings); + const FUnrealObjectInputIdentifier ActorInputNodeId(Actor, Options, false); + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(ActorInputNodeId, Handles, InObject->InputNodeHandle, true, bInputNodesCanBeDeleted); + if (!HapiSetGeoObjectTransform(InObject->GetInputObjectNodeId(), InObject->GetHoudiniObjectTransform())) + return false; + + OutCreatedNodeIds.Add(InObject->GetInputObjectNodeId()); + OutHandles.Add(InObject->InputNodeHandle); + } + else + { + // We only created one node for the actor as ref + OutCreatedNodeIds.Add(InObject->GetInputObjectNodeId()); + OutHandles.Add(InObject->InputNodeHandle); + } + + //TODO? Check + // return true if we have at least uploaded one component + // return (ComponentIdx > 0); + + return bSuccess; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForBP( + UHoudiniInput* InInput, + UHoudiniInputBlueprint* InObject, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForBP); + + if (!IsValid(InInput)) + return false; + + if (!IsValid(InObject)) + return false; + + UBlueprint* BP = InObject->GetBlueprint(); + if (!IsValid(BP)) + return true; + + // If importing as reference, we want to send the whole BP, not its components + if (InInput->GetImportAsReference()) + { + FString BPName = InInput->GetNodeBaseName() + TEXT("_") + BP->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(BPName); + + const FHoudiniInputObjectSettings InputSettings(InInput); + if (!FHoudiniInputTranslator::HapiCreateInputNodeForReference( + BPName, + InObject, + InputSettings, + bInputNodesCanBeDeleted)) + { + return false; + } + + OutCreatedNodeIds.Add(InObject->GetInputObjectNodeId()); + OutHandles.Add(InObject->InputNodeHandle); + } + else + { + // Now, commit all of this BP's component + TSet ComponentHandles; + TArray CreatedNodeIds; + TSet Handles; + for (UHoudiniInputSceneComponent* CurComponent : InObject->GetComponents()) + { + if (UploadHoudiniInputObject(InInput, CurComponent, FTransform::Identity, CreatedNodeIds, Handles, bInputNodesCanBeDeleted)) + { + ComponentHandles.Add(CurComponent->InputNodeHandle); + } + } + + { + // The BP can have its own transform (geometry input), so we have to create a reference node that + // represents InObject in the new input system that references the StaticMesh asset's input node handle + FUnrealObjectInputOptions Options; + static constexpr bool bIsLeaf = false; + FUnrealObjectInputIdentifier GeoInputRefNodeId(InObject, Options, bIsLeaf); + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(GeoInputRefNodeId, ComponentHandles, InObject->InputNodeHandle, true, bInputNodesCanBeDeleted); + + // Set the transform on the InputObject's geo object node + if (!HapiSetGeoObjectTransform(InObject->GetInputObjectNodeId(), InObject->GetHoudiniObjectTransform())) + return false; + + OutCreatedNodeIds.Add(InObject->GetInputObjectNodeId()); + OutHandles.Add(InObject->InputNodeHandle); + } + } + + const FHoudiniInputObjectSettings InputSettings(InInput); + InObject->Update(BP, InputSettings); + + return true; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForLandscapeSplinesComponent( + const FString& InObjNodeName, + UHoudiniInputLandscapeSplinesComponent* const InObject, + const FHoudiniInputObjectSettings& InInputSettings, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + const bool bInInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForLandscapeSplinesComponent); + + if (!IsValid(InObject)) + return false; + + ULandscapeSplinesComponent* const SplinesComponent = InObject->GetLandscapeSplinesComponent(); + + if (!IsValid(SplinesComponent)) + return true; + + HAPI_NodeId CreatedNodeId = InObject->GetInputNodeId(); + + FString SplinesComponentName = InObjNodeName + TEXT("_") + SplinesComponent->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(SplinesComponentName); + + TMap, int32> ControlPointIdMap(InObject->GetControlPointIdMap()); + int32 NextControlPointId = InObject->GetNextControlPointId(); + + static constexpr bool bForceReferenceInputNodeCreation = true; + static constexpr bool bLandscapeSplinesExportCurves = true; + FUnrealObjectInputHandle CreatedSplinesNodeHandle; + const bool bSuccess = FUnrealLandscapeSplineTranslator::CreateInputNode( + SplinesComponent, + bForceReferenceInputNodeCreation, + CreatedNodeId, + CreatedSplinesNodeHandle, + SplinesComponentName, + ControlPointIdMap, + NextControlPointId, + InInputSettings.UnrealSplineResolution, + bLandscapeSplinesExportCurves, + InInputSettings.bLandscapeSplinesExportControlPoints, + InInputSettings.bLandscapeSplinesExportLeftRightCurves, + bInInputNodesCanBeDeleted); + + // Update this input object's OBJ NodeId + InObject->SetInputNodeId(CreatedNodeId); + InObject->SetInputObjectNodeId(CreatedNodeId >= 0 ? FHoudiniEngineUtils::HapiGetParentNodeId(CreatedNodeId) : -1); + InObject->InputNodeHandle = CreatedSplinesNodeHandle; + + InObject->SetControlPointIdMap(MoveTemp(ControlPointIdMap)); + InObject->SetNextControlPointId(NextControlPointId); + + // Even if the function failed, some nodes may have been created, so check the node ID + if (InObject->GetInputObjectNodeId() >= 0) + { + OutCreatedNodeIds.Emplace(InObject->GetInputObjectNodeId()); + } + OutHandles.Add(InObject->InputNodeHandle); + + // Update this input object's cache data + InObject->Update(SplinesComponent, InInputSettings); + + // Update the component's transform + const FTransform ComponentTransform = InObject->GetHoudiniObjectTransform(); + { + // convert to HAPI_Transform + HAPI_TransformEuler HapiTransform; + FHoudiniApi::TransformEuler_Init(&HapiTransform); + FHoudiniEngineUtils::TranslateUnrealTransform(ComponentTransform, HapiTransform); + + // Set the transform on the OBJ parent + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + FHoudiniEngine::Get().GetSession(), InObject->GetInputObjectNodeId(), &HapiTransform), false); + } + + return bSuccess; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForLevelInstance( + const FString& InObjNodeName, + UHoudiniInputLevelInstance* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + UHoudiniInput* InInput, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForLevelInstance); + + if (!IsValid(InObject) || !IsValid(InInput)) + return false; + + ALevelInstance * LevelInstance = InObject->GetLevelInstance(); + if (!IsValid(LevelInstance)) + return true; + + FString LevelInstanceName = InObjNodeName + TEXT("_") + LevelInstance->GetActorLabel(); + FUnrealObjectInputHandle InputNodeHandle; + HAPI_NodeId InputNodeId = InObject->GetInputNodeId(); + + if (InInputSettings.bExportLevelInstanceContent) + { + { + const FUnrealObjectInputOptions LevelInstanceNodeOptions = FUnrealObjectInputOptions::MakeOptionsForLevelInstanceActor(InInputSettings); + const FUnrealObjectInputIdentifier LevelInstanceId(LevelInstance->GetWorldAsset().LoadSynchronous(), LevelInstanceNodeOptions, false); + if (!FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(LevelInstanceId, InputNodeHandle)) + { + // Process each actor in the level instance + TArray NodeIds; + TSet Handles; + for (auto& Entry : InObject->GetTrackedActorObjects()) + { + UHoudiniInputObject* const InputObject = Entry.Value; + if (!IsValid(InputObject)) + continue; + + UploadHoudiniInputObject(InInput, InputObject, FTransform::Identity, NodeIds, Handles, bInputNodesCanBeDeleted); + } + + // Create/Update the level instance' merge / reference node + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(LevelInstanceId, Handles, InputNodeHandle, true, bInputNodesCanBeDeleted); + } + + // Make a reference node for the actor + const FUnrealObjectInputIdentifier ActorInputNodeId(LevelInstance, LevelInstanceNodeOptions, false); + // Create/update the input-specific merge node for this level instance, on which we can apply the actor transform + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(ActorInputNodeId, { InputNodeHandle }, InObject->InputNodeHandle, true, bInputNodesCanBeDeleted); + + OutCreatedNodeIds.Add(InObject->GetInputObjectNodeId()); + OutHandles.Add(InObject->InputNodeHandle); + } + } + else + { + if (!FUnrealLevelInstanceTranslator::AddLevelInstance( + LevelInstance, InInput, InputNodeId, LevelInstanceName, InputNodeHandle, bInputNodesCanBeDeleted)) + return false; + InObject->InputNodeHandle = InputNodeHandle; + + OutCreatedNodeIds.Add(InObject->GetInputObjectNodeId()); + OutHandles.Add(InObject->InputNodeHandle); + } + + if (!HapiSetGeoObjectTransform(InObject->GetInputObjectNodeId(), InObject->GetHoudiniObjectTransform())) + return false; + + InObject->Update(LevelInstance, InInputSettings); + + return true; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForPackedLevelActor( + const FString& InObjNodeName, + UHoudiniInputPackedLevelActor* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + UHoudiniInput* InInput, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForPackedLevelActor); + + if (!IsValid(InObject) || !IsValid(InInput)) + return false; + + APackedLevelActor* PackedLevelActor = InObject->GetPackedLevelActor(); + if (!IsValid(PackedLevelActor)) + return true; + + FString LevelInstanceName = InObjNodeName + TEXT("_") + PackedLevelActor->GetActorLabel(); + FUnrealObjectInputHandle InputNodeHandle; + HAPI_NodeId InputNodeId = InObject->GetInputNodeId(); + + if (InInputSettings.bExportLevelInstanceContent) + { + { + // Process the underlying BP of the packed level actor + UHoudiniInputBlueprint* const InputBP = InObject->GetBlueprintInputObject(); + if (!IsValid(InputBP)) + return false; + + TArray NodeIds; + TSet Handles; + // Now, commit all of this BP's component + TSet ComponentHandles; + for (UHoudiniInputSceneComponent* CurComponent : InputBP->GetComponents()) + { + if (UploadHoudiniInputObject(InInput, CurComponent, FTransform::Identity, NodeIds, Handles, bInputNodesCanBeDeleted)) + { + ComponentHandles.Add(CurComponent->InputNodeHandle); + } + } + + // Make a reference node for the BP asset + const FUnrealObjectInputOptions Options = FUnrealObjectInputOptions::MakeOptionsForPackedLevelActor(InInputSettings); + const FUnrealObjectInputIdentifier BPAssetNodeId(InputBP->GetBlueprint(), Options, false); + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(BPAssetNodeId, Handles, InputBP->InputNodeHandle, true, bInputNodesCanBeDeleted); + + // Make a reference node for the actor + const FUnrealObjectInputIdentifier ActorInputNodeId(PackedLevelActor, Options, false); + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(ActorInputNodeId, { InputBP->InputNodeHandle }, InObject->InputNodeHandle, true, bInputNodesCanBeDeleted); + + if (!HapiSetGeoObjectTransform(InObject->GetInputObjectNodeId(), InObject->GetHoudiniObjectTransform())) + return false; + + OutCreatedNodeIds.Add(InObject->GetInputObjectNodeId()); + OutHandles.Add(InObject->InputNodeHandle); + } + } + else + { + if (!FUnrealLevelInstanceTranslator::AddLevelInstance( + PackedLevelActor, InInput, InputNodeId, LevelInstanceName, InputNodeHandle, bInputNodesCanBeDeleted)) + return false; + InObject->InputNodeHandle = InputNodeHandle; + + if (!HapiSetGeoObjectTransform(InObject->GetInputObjectNodeId(), InObject->GetHoudiniObjectTransform())) + return false; + + OutCreatedNodeIds.Add(InObject->GetInputObjectNodeId()); + OutHandles.Add(InObject->InputNodeHandle); + } + + InObject->Update(PackedLevelActor, InInputSettings); + + return true; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForLandscape( + const FString& InObjNodeName, + UHoudiniInputLandscape* InObject, + UHoudiniInput* InInput, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForLandscape); + + if (!IsValid(InObject)) + return false; + + if (!IsValid(InInput)) + return false; + + ALandscapeProxy* Landscape = InObject->GetLandscapeProxy(); + if (!IsValid(Landscape)) + return true; + + FString LandscapeName = InObjNodeName + TEXT("_") + Landscape->GetActorLabel(); + FUnrealObjectInputHandle InputNodeHandle; + HAPI_NodeId InputNodeId = InObject->GetInputNodeId(); + + if (!FUnrealLandscapeTranslator::CreateInputNodeForLandscapeObject( + Landscape, InInput, InputNodeId, LandscapeName, InputNodeHandle, bInputNodesCanBeDeleted)) + return false; + + FTransform Transform = InObject->GetHoudiniObjectTransform(); + // Now, commit all of the input components of the landscape + TArray CreatedNodeIds; + TSet Handles; + bool bSuccess = HapiCreateInputNodesForActorComponents(InInput, InObject, Landscape, Transform, CreatedNodeIds, Handles, bInputNodesCanBeDeleted); + + const FHoudiniInputObjectSettings InputSettings(InInput); + InObject->Update(Landscape, InputSettings); + + { + TSet> SelectedLandscapeComponents = InInput->GetLandscapeSelectedComponents(); + FUnrealObjectInputOptions Options = FUnrealObjectInputOptions::MakeOptionsForLandscapeActor(InputSettings, &SelectedLandscapeComponents); + FUnrealObjectInputIdentifier LandscapeInputNodeId(Landscape, Options, false); + + Handles.Add(InputNodeHandle); + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(LandscapeInputNodeId, Handles, InObject->InputNodeHandle, true, bInputNodesCanBeDeleted); + if (!HapiSetGeoObjectTransform(InObject->GetInputObjectNodeId(), Transform)) + return false; + + OutCreatedNodeIds.Add(InObject->GetInputObjectNodeId()); + OutHandles.Add(InObject->InputNodeHandle); + } + + return bSuccess; +} + + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForBrush( + const FString& InObjNodeName, + UHoudiniInputBrush* InObject, + TArray>* ExcludeActors, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForBrush); + + if (!IsValid(InObject)) + return false; + + ABrush* BrushActor = InObject->GetBrush(); + if (!IsValid(BrushActor)) + return true; + + FString BrushName = InObjNodeName + TEXT("_") + BrushActor->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(BrushName); + + FUnrealObjectInputHandle InputNodeHandle; + + HAPI_NodeId InputNodeId = InObject->GetInputNodeId(); + + if (!FUnrealBrushTranslator::CreateInputNodeForBrush(InObject, BrushActor, ExcludeActors, InputNodeId, BrushName, InInputSettings.bExportMaterialParameters, InputNodeHandle, bInputNodesCanBeDeleted)) + return false; + + InObject->InputNodeHandle = InputNodeHandle; + InObject->SetInputNodeId((int32)InputNodeId); + InObject->SetInputObjectNodeId((int32)FHoudiniEngineUtils::HapiGetParentNodeId(InputNodeId)); + + // InObject->MarkChanged(true); + InObject->Update(BrushActor, InInputSettings); + + if (!HapiSetGeoObjectTransform(InObject->GetInputObjectNodeId(), InObject->GetHoudiniObjectTransform())) + return false; + + return true; +} + + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForCamera( + const FString& InNodeName, UHoudiniInputCameraComponent* InInputObject, const FHoudiniInputObjectSettings& InInputSettings) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForCamera); + + if (!IsValid(InInputObject)) + return false; + + UCameraComponent* Camera = InInputObject->GetCameraComponent(); + if (!IsValid(Camera)) + return true; + + FString NodeName = InNodeName + TEXT("_") + Camera->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(NodeName); + + // Create the camera OBJ. + int32 CameraNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + -1, TEXT("Object/cam"), InNodeName, true, &CameraNodeId), false); + + // set "Pixel Aspect Ratio" (aspect) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), CameraNodeId, "aspect", 0, InInputObject->AspectRatio), false); + + // set "Projection" (projection) (0 persp, 1 ortho) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), CameraNodeId, "projection", 0, InInputObject->bIsOrthographic ? 1 : 0), false); + + // set Ortho Width (orthowidth) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), CameraNodeId, "orthowidth", 0, InInputObject->OrthoWidth), false); + + // set Near Clippin (near) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), CameraNodeId, "near", 0, InInputObject->OrthoNearClipPlane), false); + + // set far clipping (far) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), CameraNodeId, "far", 0, InInputObject->OrthoFarClipPlane), false); + + // Set the transform - rotate by 90 degrees to align with where Houdini expects camera to be pointing. + FTransform RotationTransform(FQuat::MakeFromEuler(FVector(0.0, 0.0, 90.0))); + FTransform RotatedCamera = RotationTransform * Camera->GetComponentTransform(); + HAPI_TransformEuler H_Transform; + FHoudiniApi::TransformEuler_Init(&H_Transform); + FHoudiniEngineUtils::TranslateUnrealTransform(RotatedCamera, H_Transform); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + FHoudiniEngine::Get().GetSession(), CameraNodeId, &H_Transform), false); + + // Update this input's NodeId and ObjectNodeId + InInputObject->SetInputNodeId(-1);// (int32)CameraNodeId; + InInputObject->SetInputObjectNodeId((int32)CameraNodeId); + + // Update this input object's cache data + InInputObject->Update(Camera, InInputSettings); + + return true; +} + + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForTexture2D( + const FString& InObjNodeName, + UHoudiniInputTexture* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted, + bool bIsCOPInput) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FUnrealTextureTranslator::HapiCreateInputNodeForTexture2D); + + if (!IsValid(InObject)) + return false; + + // Get the input texture + UTexture2D* InputTexture = InObject->GetTexture(); + if (!IsValid(InputTexture)) + return true; + + HAPI_NodeId CreatedNodeId = InObject->GetInputNodeId(); + HAPI_NodeId ParentNodeId = InObject->GetInputObjectNodeId(); + + // Marshall the Texture to Houdini + FString TextureName = InObjNodeName + TEXT("_") + InputTexture->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(TextureName); + + // Only the ExportMainGeo and ImportAsRef options matter for Textures + FUnrealObjectInputOptions Options; + Options.bExportMainGeometry = InInputSettings.bExportMainGeometry; + Options.bImportAsReference = InInputSettings.bImportAsReference; + + // .. if we are a COP input - no need to add extra geometry - ignore bExportMainGeometry + if(bIsCOPInput) + Options.bExportMainGeometry = false; + + FUnrealObjectInputIdentifier Identifier; + Identifier = FUnrealObjectInputIdentifier(InputTexture, Options, true); + + FUnrealObjectInputHandle InputNodeHandle; + FUnrealObjectInputHandle ParentHandle; + { + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, InputNodeHandle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(InputNodeHandle, NodeId)) + { + if (!bInputNodesCanBeDeleted) + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(InputNodeHandle, bInputNodesCanBeDeleted); + + InObject->InputNodeHandle = InputNodeHandle; + CreatedNodeId = NodeId; + return true; + } + } + + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, TextureName); + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // Set InputNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that InputNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (InputNodeHandle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(InputNodeHandle, CreatedNodeId)) + CreatedNodeId = -1; + } + else + { + CreatedNodeId = -1; + } + } + + HAPI_NodeId GeoOutId = -1; + bool bSuccess = true; + if (Options.bImportAsReference) + { + FTransform ImportAsReferenceTransform = FTransform::Identity; + FBox InBbox = FBox(EForceInit::ForceInit); + const TArray& MaterialReferences = TArray(); + + bSuccess = FHoudiniInputTranslator::CreateInputNodeForReference( + CreatedNodeId, + InputTexture, + TextureName, + ImportAsReferenceTransform, + Options.bImportAsReferenceRotScaleEnabled, + InputNodeHandle, + true, + InInputSettings.bImportAsReferenceBboxEnabled, + InBbox, + InInputSettings.bImportAsReferenceMaterialEnabled, + MaterialReferences); + + ParentNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(CreatedNodeId); + } + else + { + // Clean previously created nodes if needed + HAPI_NodeId PreviousInputNodeId = CreatedNodeId; + if (PreviousInputNodeId >= 0) + { + HAPI_NodeId PreviousInputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousInputNodeId); + if (FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), PreviousInputNodeId) != HAPI_RESULT_SUCCESS) + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input node for %s."), *TextureName); + + if (FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), PreviousInputObjectNodeId) != HAPI_RESULT_SUCCESS) + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input object node for %s."), *TextureName); + + // We've deleted the parent - make sure it exists and updates its ID + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + } + + + // Create the geo node that will hold the COPnet and the quad geometry (if needed) + HAPI_Result Result = FHoudiniEngineUtils::CreateNode(ParentNodeId, TEXT("geo"), TextureName, true, &CreatedNodeId); + if (Result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniInputTranslator::HapiCreateInputNodeForTexture2D]: CreateNode failed: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + return false; + } + + if (Options.bExportMainGeometry) + { + // Create a quad with uvs and a coppreview material to "hold" the texture + if (!FUnrealTextureTranslator::CreateGeometryForTexture(CreatedNodeId, GeoOutId)) + HOUDINI_LOG_WARNING(TEXT("[FHoudiniInputTranslator::HapiCreateInputNodeForTexture2D]: Unable to create geometry for a texture input")); + } + + ParentNodeId = CreatedNodeId; + CreatedNodeId = -1; + + // Send the texture to COPs + bSuccess = FUnrealTextureTranslator::HapiCreateCOPTexture( + InputTexture, ParentNodeId); + + // Now that the textures has been created, cook the grid + // (this helps with visual issues when using session sync) + if (GeoOutId >= 0) + { + FHoudiniEngineUtils::HapiCookNode(GeoOutId); + CreatedNodeId = GeoOutId; + } + + // This will no longer be necessary after the update to HAPI_CreateCOPImage + // as the created node id will be returned by HAPI + // If we're a COP Input - we need to get the COP output node ID - not the geo container + if (bIsCOPInput && CreatedNodeId < 0) + { + // Get the absolute node path to our COP's geo container + FString NodePath; + FHoudiniEngineUtils::HapiGetAbsNodePath(ParentNodeId, NodePath); + + // Append the path to the COP node itself + NodePath += TEXT("/copmemoryimport1/texture1"); + + // Fetch the actual texture node ID from the updated path + HAPI_NodeId TextureNodeId = -1; + if (FHoudiniEngineUtils::HapiGetNodeFromPath(NodePath, -1, TextureNodeId)) + CreatedNodeId = TextureNodeId; + + HAPI_NodeInfo COPNodeInfo; + FHoudiniApi::GetNodeInfo(FHoudiniEngine::Get().GetSession(), CreatedNodeId, &COPNodeInfo); + if (COPNodeInfo.type != HAPI_NODETYPE_COP) + HOUDINI_LOG_WARNING(TEXT("Not a COP!")); + } + } + + if(bSuccess) + { + // Record the node in the manager + //const HAPI_NodeId ObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(CreatedNodeId); + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode( + Identifier, + CreatedNodeId, + Handle, + ParentNodeId, + nullptr, + bInputNodesCanBeDeleted)) + { + InputNodeHandle = Handle; + } + } + + // Update this input object's OBJ NodeId + InObject->SetInputNodeId(CreatedNodeId); + InObject->SetInputObjectNodeId(ParentNodeId); + InObject->InputNodeHandle = InputNodeHandle; + + // Update this input object's cache data + InObject->Update(InputTexture, InInputSettings); + + return bSuccess; +} + + +bool +FHoudiniInputTranslator::UpdateInputs( + HAPI_NodeId InNodeId, + UObject* InOuter, + TArray>& Inputs, + TArray>& Parameters, + bool bLoadedInputs) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::UpdateInputs); + + // We need to call BuildAllInputs here to update all the inputs, + // and make sure that the object path parameter inputs' parameter ids are up to date + if (!FHoudiniInputTranslator::BuildAllInputs(InNodeId, InOuter, Inputs, Parameters)) + return false; + + // If we weren't loaded - we're done + if (!bLoadedInputs) + return true; + + // If we were loaded - we also need to update the NodeId stored on all the inputs + for (auto CurrentInput : Inputs) + { + if (!IsValid(CurrentInput)) + continue; + + // + CurrentInput->SetAssetNodeId(InNodeId); + + // We need to delete the nodes created for the input objects if they are valid + // (since the node IDs are transients, this likely means we're handling a recook/rebuild + // and therefore expect to recreate the input nodes) + DestroyInputNodes(CurrentInput, CurrentInput->GetInputType()); + } + + return true; +} + + + +bool +FHoudiniInputTranslator::UpdateWorldInputs(TArray>& InInputs, AActor* InActorOwner) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::UpdateWorldInputs); + + // Only tick/cook when in Editor + // This prevents PIE cooks or runtime cooks due to inputs moving + if (InActorOwner) + { + if (!InActorOwner->GetWorld() || (InActorOwner->GetWorld()->WorldType != EWorldType::Editor)) + return false; + } + +#if WITH_EDITOR + // Stop outliner objects from causing recooks while input objects are dragged around + if (FHoudiniMoveTracker::Get().IsObjectMoving) + { + //HOUDINI_LOG_MESSAGE(TEXT("Object moving, not updating world inputs!")); + return false; + } +#endif + + for (auto CurrentInput : InInputs) + { + if (!CurrentInput) + continue; + if (CurrentInput->GetInputType() != EHoudiniInputType::World) + continue; + + UpdateWorldInput(CurrentInput); + } + + return true; +} + +bool +FHoudiniInputTranslator::UpdateWorldInput(UHoudiniInput* InInput) +{ + if (!IsValid(InInput)) + return false; + + EHoudiniInputType WorldType = InInput->GetInputType(); + if (WorldType != EHoudiniInputType::World) + return false; + + // No need to update world inputs with auto-updates off + if (!InInput->GetWorldInputAutoUpdates()) + return false; + + TArray>* InputObjectsPtr = InInput->GetHoudiniInputObjectArray(WorldType); + if (!InputObjectsPtr) + return false; + + bool bHasChanged = false; + if (InInput->IsWorldInputBoundSelector() && InInput->GetWorldInputBoundSelectorAutoUpdates()) + { + // If the input is in bound selector mode, and auto-update is enabled + // update the actors selected by the bounds first + bHasChanged = InInput->UpdateWorldSelectionFromBoundSelectors(); + } + + const FHoudiniInputObjectSettings InputSettings(InInput); + + // See if we need to update the components for this input + // look for deleted actors/components + TArray ObjectToDeleteIndices; + for(int32 InputObjIdx = 0; InputObjIdx < InputObjectsPtr->Num(); InputObjIdx++) + { + UHoudiniInputActor* ActorObject = Cast((*InputObjectsPtr)[InputObjIdx]); + if (!IsValid(ActorObject)) + continue; + + // Make sure the actor is still valid + AActor* const Actor = ActorObject->GetActor(); + bool bValidActorObject = IsValid(Actor); + + // For BrushActors, the brush and actors must be valid as well + UHoudiniInputBrush* BrushActorObject = Cast(ActorObject); + if (bValidActorObject && BrushActorObject) + { + ABrush* BrushActor = BrushActorObject->GetBrush(); + if (!IsValid(BrushActor)) + bValidActorObject = false; + else if (!IsValid(BrushActor->Brush)) + bValidActorObject = false; + } + + // The actor is no longer valid, mark it for deletion + if (!bValidActorObject) + { + if ((ActorObject->GetInputNodeId() > 0) || (ActorObject->GetInputObjectNodeId() > 0)) + { + ActorObject->InvalidateData(); + // We only need to update the input if the actors nodes were created in Houdini + bHasChanged = true; + } + + // Delete the Actor object + ObjectToDeleteIndices.Add(InputObjIdx); + continue; + } + + // If we send our input objects as references, we should recreate the whole input node for + // a transform change (as the transform is stored as a point attribute, not as a geo/object transform) + bool bImportAsRef = InInput->GetImportAsReference(); + + // We'll keep track of whether the actor transform changed so that + // we can mark all the components as having changed transforms -- everything + // needs to be updated. + bool bActorTransformChanged = false; + if (ActorObject->HasActorTransformChanged()) + { + if (bImportAsRef) + ActorObject->MarkChanged(true); + else + ActorObject->MarkTransformChanged(true); + + bHasChanged = true; + bActorTransformChanged = true; + } + + if (ActorObject->HasContentChanged(InputSettings)) + { + ActorObject->MarkChanged(true); + bHasChanged = true; + } + + // Ensure we are aware of all the components of the actor + ActorObject->Update(Actor, InputSettings); + + // Check if any components have content or transform changes + for (auto CurActorComp : ActorObject->GetActorComponents()) + { + if (bActorTransformChanged || CurActorComp->HasComponentTransformChanged()) + { + if (bImportAsRef) + CurActorComp->MarkChanged(true); + else + CurActorComp->MarkTransformChanged(true); + + bHasChanged = true; + } + + if (CurActorComp->HasComponentChanged(InputSettings)) + { + CurActorComp->MarkChanged(true); + bHasChanged = true; + } + + USceneComponent* Component = CurActorComp->GetSceneComponent(); + if (IsValid(Component)) + { + CurActorComp->Update(Component, InputSettings); + } + } + + // Check if we added/removed any components in the call to update + if (ActorObject->GetLastUpdateNumComponentsAdded() > 0 || ActorObject->GetLastUpdateNumComponentsRemoved() > 0) + { + bHasChanged = true; + if (ActorObject->GetLastUpdateNumComponentsRemoved() > 0) + TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); + } + } + + // Delete the actor objects that were marked for deletion + for (int32 ToDeleteIdx = ObjectToDeleteIndices.Num() - 1; ToDeleteIdx >= 0; ToDeleteIdx--) + { + InputObjectsPtr->RemoveAt(ObjectToDeleteIndices[ToDeleteIdx]); + bHasChanged = true; + } + + // If not a bound selector and auto select landscape splines is enabled, add all landscape splines of input + // landscapes to our input objects + if (!InInput->IsWorldInputBoundSelector() && InInput->IsLandscapeAutoSelectSplinesEnabled()) + { + InInput->AddAllLandscapeSplineActorsForInputLandscapes(); + } + + // Mark the input as changed if need so it will trigger an upload + if (bHasChanged) + { + InInput->MarkChanged(true); + + // Mark the outer package as dirty, to ensure that the changes are saved when using OFPA / World partition + //InInput->MarkPackageDirty(); + + } + + return true; +} + + +bool +FHoudiniInputTranslator::CreateInputNodeForReference( + const HAPI_NodeId InParentNodeId, + HAPI_NodeId& InputNodeId, + const FString& InRef, + const FString& InputNodeName, + const FTransform& InTransform, + bool bImportAsReferenceRotScaleEnabled, + bool bImportAsReferenceBboxEnabled, + const FBox& InBbox, + bool bImportAsReferenceMaterialEnabled, + const TArray& MaterialReferences) +{ + HAPI_NodeId NewNodeId = -1; + + // Create a single input node + if (InParentNodeId >= 0) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateInputNode(InputNodeName, NewNodeId, InParentNodeId), false); + } + else + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CreateInputNode( + FHoudiniEngine::Get().GetSession(), -1, &NewNodeId, H_TCHAR_TO_UTF8(*InputNodeName)), false); + } + + /* + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CookNode( + FHoudiniEngine::Get().GetSession(), NewNodeId, nullptr), false); + */ + + // Check if we have a valid id for this new input asset. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(NewNodeId)) + return false; + + // We have now created a valid new input node, delete the previous one + HAPI_NodeId PreviousInputNodeId = InputNodeId; + if (PreviousInputNodeId >= 0) + { + // Get the parent OBJ node ID before deleting! + HAPI_NodeId PreviousInputOBJNode = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousInputNodeId); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input node for %s."), *InputNodeName); + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputOBJNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input OBJ node for %s."), *InputNodeName); + } + } + + // Create and initialize a part containing one point with a point attribute + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + + PartInfo.attributeCounts[HAPI_ATTROWNER_POINT] = 1; + PartInfo.vertexCount = 0; + PartInfo.faceCount = 0; + PartInfo.pointCount = 1; + PartInfo.type = HAPI_PARTTYPE_MESH; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, &PartInfo), false); + + // Point Position Attribute + { + // Create point attribute info for P. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = 1; + AttributeInfoPoint.tupleSize = 3; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint), false); + + // Set the point's position + FVector3f ObjectPosition = (FVector3f)InTransform.GetLocation(); + TArray Position = + { + ObjectPosition.X / HAPI_UNREAL_SCALE_FACTOR_POSITION, + ObjectPosition.Z / HAPI_UNREAL_SCALE_FACTOR_POSITION, + ObjectPosition.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION + }; + + // Now that we have raw positions, we can upload them for our attribute. + FHoudiniHapiAccessor Accessor(NewNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPoint, Position), false); + + } + + if (bImportAsReferenceRotScaleEnabled) + { + // Create ROTATION attribute info + HAPI_AttributeInfo AttributeInfoRotation; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoRotation); + AttributeInfoRotation.count = 1; + AttributeInfoRotation.tupleSize = 4; + AttributeInfoRotation.exists = true; + AttributeInfoRotation.owner = HAPI_ATTROWNER_POINT; + AttributeInfoRotation.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoRotation.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, + HAPI_UNREAL_ATTRIB_ROTATION, + &AttributeInfoRotation), false); + + TArray InputRotations; + InputRotations.SetNumZeroed(4); + + FQuat InputRotation = InTransform.GetRotation(); + InputRotations[0] = (float)InputRotation.X; + InputRotations[1] = (float)InputRotation.Z; + InputRotations[2] = (float)InputRotation.Y; + InputRotations[3] = (float)-InputRotation.W; + + //we can now upload them to our attribute. + FHoudiniHapiAccessor Accessor(NewNodeId, 0, HAPI_UNREAL_ATTRIB_ROTATION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoRotation, InputRotations), false); + + // Create SCALE attribute info + HAPI_AttributeInfo AttributeInfoScale; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoScale); + AttributeInfoScale.count = 1; + AttributeInfoScale.tupleSize = 3; + AttributeInfoScale.exists = true; + AttributeInfoScale.owner = HAPI_ATTROWNER_POINT; + AttributeInfoScale.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoScale.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, + HAPI_UNREAL_ATTRIB_SCALE, + &AttributeInfoScale), false); + + TArray InputScales; + InputScales.SetNumZeroed(3); + + FVector3f InputScale = (FVector3f)InTransform.GetScale3D(); + InputScales[0] = InputScale.X; + InputScales[1] = InputScale.Z; + InputScales[2] = InputScale.Y; + + //we can now upload them to our attribute. + Accessor.Init(NewNodeId, 0, HAPI_UNREAL_ATTRIB_SCALE); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoScale, InputScales), false); + + } + + if (bImportAsReferenceBboxEnabled) + { + // Create attribute info for both bbox min and bbox max + HAPI_AttributeInfo AttributeInfoBboxPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoBboxPoint); + AttributeInfoBboxPoint.count = 1; + AttributeInfoBboxPoint.tupleSize = 3; + AttributeInfoBboxPoint.exists = true; + AttributeInfoBboxPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoBboxPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoBboxPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + // bbox min + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + HAPI_UNREAL_ATTRIB_BBOX_MIN, &AttributeInfoBboxPoint), false); + + FVector3f InBboxMin = (FVector3f)InBbox.Min; + TArray BboxMin = + { + InBboxMin.X / HAPI_UNREAL_SCALE_FACTOR_POSITION, + InBboxMin.Z / HAPI_UNREAL_SCALE_FACTOR_POSITION, + InBboxMin.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION + }; + + FHoudiniHapiAccessor Accessor(NewNodeId, 0, HAPI_UNREAL_ATTRIB_BBOX_MIN); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoBboxPoint, BboxMin), false); + + // bbox max + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + HAPI_UNREAL_ATTRIB_BBOX_MAX, &AttributeInfoBboxPoint), false); + + FVector3f InBboxMax = (FVector3f)InBbox.Max; + TArray BboxMax = + { + InBboxMax.X / HAPI_UNREAL_SCALE_FACTOR_POSITION, + InBboxMax.Z / HAPI_UNREAL_SCALE_FACTOR_POSITION, + InBboxMax.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION + }; + + Accessor.Init(NewNodeId, 0, HAPI_UNREAL_ATTRIB_BBOX_MAX); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoBboxPoint, BboxMax), false); + } + + // Material Reference String Array Attribute + if (bImportAsReferenceMaterialEnabled) + { + // Create point attribute info. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = 1; + AttributeInfoPoint.tupleSize = 1; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + // We set it to be multiple string attributes rather than a single string array attribute to not conflict + // with any existing HDA's that use the attribute name unreal_material + for (int i = 0; i < MaterialReferences.Num(); ++i) + { + FString AttributeName = HAPI_UNREAL_ATTRIB_MATERIAL; + if (i > 0) + AttributeName.AppendInt(i); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + TCHAR_TO_ANSI(*AttributeName), &AttributeInfoPoint), false); + + FHoudiniHapiAccessor Accessor(NewNodeId, 0, TCHAR_TO_ANSI(*AttributeName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoPoint, MaterialReferences[i]), false); + } + } + + + // Unreal Reference String Attribute + { + // Create point attribute info. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = 1; + AttributeInfoPoint.tupleSize = 1; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + HAPI_UNREAL_ATTRIB_INSTANCE_OVERRIDE, &AttributeInfoPoint), false); + + FHoudiniHapiAccessor Accessor(NewNodeId, 0, HAPI_UNREAL_ATTRIB_INSTANCE_OVERRIDE); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoPoint, InRef), false); + } + + // Commit the geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(NewNodeId), false); + + InputNodeId = NewNodeId; + return true; +} + +bool FHoudiniInputTranslator::CreateInputNodeForReference( + HAPI_NodeId& InputNodeId, + UObject const* const InObjectToRef, + const FString& InputNodeName, + const FTransform& InTransform, + bool bImportAsReferenceRotScaleEnabled, + FUnrealObjectInputHandle& OutHandle, + bool bInputNodesCanBeDeleted, + bool bImportAsReferenceBboxEnabled, + const FBox& InBbox, + bool bImportAsReferenceMaterialEnabled, + const TArray& MaterialReferences) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::CreateInputNodeForReference); + + // The identifier to the node in the input system + FString FinalInputNodeName = InputNodeName; + HAPI_NodeId ParentNodeId = -1; + FUnrealObjectInputHandle ParentHandle; + FUnrealObjectInputIdentifier Identifier; + { + // Build the identifier for the entry in the manager + constexpr bool bIsLeaf = true; + FUnrealObjectInputOptions Options; + Options.bImportAsReference = true; + Options.bImportAsReferenceRotScaleEnabled = bImportAsReferenceRotScaleEnabled; + Identifier = FUnrealObjectInputIdentifier(InObjectToRef, Options, bIsLeaf); + + // If the entry exists in the manager, the associated HAPI nodes are valid, and it is not marked as dirty, then + // return the existing entry + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId)) + { + // Make sure the node cant be deleted if needed + if (!bInputNodesCanBeDeleted) + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + + OutHandle = Handle; + InputNodeId = NodeId; + return true; + } + } + // If the entry does not exist, or is invalid, then we need create it + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + // Create any parent/container nodes that we would need, and get the node id of the immediate parent + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + } + + const FString AssetReference = + UHoudiniInputObject::FormatAssetReference(InObjectToRef->GetFullName()); + + HAPI_NodeId NodeId = -1; + const bool bSuccess = CreateInputNodeForReference( + ParentNodeId, + NodeId, + AssetReference, + FinalInputNodeName, + InTransform, + bImportAsReferenceRotScaleEnabled, + bImportAsReferenceBboxEnabled, + InBbox, + bImportAsReferenceMaterialEnabled, + MaterialReferences); + + if (!bSuccess) + return false; + + InputNodeId = NodeId; + + { + // Record the node in the manager + const HAPI_NodeId ObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(InputNodeId); + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, InputNodeId, Handle, ObjectNodeId, nullptr, bInputNodesCanBeDeleted)) + OutHandle = Handle; + } + + return bSuccess; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForPCGData( + const FString& InNodeName, + UHoudiniInputPCGData* InInputObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ +#if defined(HOUDINI_USE_PCG) + if(!IsValid(InInputObject)) + return false; + + UHoudiniPCGDataCollection* DataCollection = InInputObject->GetPCGData(); + if(!DataCollection) + return true; + + FString PCGDataName = InNodeName; + FHoudiniEngineUtils::SanitizeHAPIVariableName(PCGDataName); + + FUnrealObjectInputHandle PCGInputNodeHandle; + if(!FUnrealPCGDataTranslator::CreateInputNodeForPCGData(DataCollection, PCGDataName, PCGInputNodeHandle, bInputNodesCanBeDeleted)) + { + return false; + } + + if(!HapiSetGeoObjectTransform(InInputObject->GetInputObjectNodeId(), InInputObject->GetHoudiniObjectTransform())) + return false; + + // Update the cached data and input settings + InInputObject->Update(DataCollection, InInputSettings); + InInputObject->InputNodeHandle = PCGInputNodeHandle; +#endif + return true; +} + + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForDataTable( + const FString& InNodeName, + UHoudiniInputDataTable* InInputObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForDataTable); + + if (!IsValid(InInputObject)) + return false; + + UDataTable* DataTable = InInputObject->GetDataTable(); + if (!IsValid(DataTable)) + return true; + + FString DataTableName = InNodeName + TEXT("_") + DataTable->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(DataTableName); + + FUnrealObjectInputHandle DTInputNodeHandle; + HAPI_NodeId InputNodeId = -1; + + // Get the existing node id, if any + + // For the ref counted system the handle on the input object represents a reference node that has a single node + // it references: the data table. The reference node represents InObject with its Transform (geometry input). + { + TSet ReferencedNodes; + if (FUnrealObjectInputUtils::GetReferencedNodes(InInputObject->InputNodeHandle, ReferencedNodes) && ReferencedNodes.Num() == 1) + { + const FUnrealObjectInputHandle Handle = ReferencedNodes.Array()[0]; + FUnrealObjectInputUtils::GetHAPINodeId(Handle, InputNodeId); + } + } + + if (!FUnrealDataTableTranslator::CreateInputNodeForDataTable(DataTable, InputNodeId, DataTableName, DTInputNodeHandle, bInputNodesCanBeDeleted)) + return false; + + + { + // The data table can have its own transform (geometry input), so we have to create a reference node that + // represents InInputObject in the new input system that references the DataTable asset's input node handle + FUnrealObjectInputOptions Options; + static constexpr bool bIsLeaf = false; + FUnrealObjectInputIdentifier GeoInputRefNodeId(InInputObject, Options, bIsLeaf); + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(GeoInputRefNodeId, { DTInputNodeHandle }, InInputObject->InputNodeHandle, true, bInputNodesCanBeDeleted); + } + + if (!HapiSetGeoObjectTransform(InInputObject->GetInputObjectNodeId(), InInputObject->GetHoudiniObjectTransform())) + return false; + + // Update the cached data and input settings + InInputObject->Update(DataTable, InInputSettings); + + return true; +} + +bool +FHoudiniInputTranslator::HapiCreateInputNodeForFoliageType_InstancedStaticMesh( + const FString& InObjNodeName, + UHoudiniInputFoliageType_InstancedStaticMesh* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInputTranslator::HapiCreateInputNodeForFoliageType_InstancedStaticMesh); + + if (!IsValid(InObject)) + return false; + + UFoliageType_InstancedStaticMesh* FoliageType = Cast(InObject->GetObject()); + if (!IsValid(FoliageType)) + return true; + + UStaticMesh* const SM = FoliageType->GetStaticMesh(); + if (!IsValid(SM)) + return true; + + FString FTName = InObjNodeName + TEXT("_") + FoliageType->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(FTName); + + // Marshall the Static Mesh to Houdini + FUnrealObjectInputHandle InputNodeHandle; + constexpr bool bUseRefCountedInputSystem = false; + bool bSuccess = true; + + HAPI_NodeId InputNodeId = InObject->GetInputNodeId(); + if (InInputSettings.bImportAsReference) + { + FBox InBbox = InInputSettings.bImportAsReferenceBboxEnabled ? SM->GetBoundingBox() : FBox(EForceInit::ForceInit); + + const TArray& MaterialReferences = InInputSettings.bImportAsReferenceMaterialEnabled ? + InObject->GetMaterialReferences() : + TArray(); + + bSuccess = FUnrealFoliageTypeTranslator::CreateInputNodeForReference( + InputNodeId, + FoliageType, + FTName, + InObject->GetTransform(), + InInputSettings.bImportAsReferenceRotScaleEnabled, + InputNodeHandle, + bInputNodesCanBeDeleted, + InInputSettings.bImportAsReferenceBboxEnabled, + InBbox, + InInputSettings.bImportAsReferenceMaterialEnabled, + MaterialReferences); + } + else + { + bSuccess = FUnrealFoliageTypeTranslator::HapiCreateInputNodeForFoliageType_InstancedStaticMesh( + FoliageType, + InputNodeId, + FTName, + InputNodeHandle, + InInputSettings.bExportLODs, + InInputSettings.bExportSockets, + InInputSettings.bExportColliders, + InInputSettings.bExportMaterialParameters); + } + + // Update this input object's OBJ NodeId + InObject->SetInputNodeId(InputNodeId); + InObject->SetInputObjectNodeId(FHoudiniEngineUtils::HapiGetParentNodeId(InputNodeId)); + InObject->InputNodeHandle = InputNodeHandle; + + // Update the cached data and input settings + InObject->Update(FoliageType, InInputSettings); + + // If the Input mesh has a Transform offset + const FTransform TransformOffset = InObject->GetHoudiniObjectTransform(); + if (bUseRefCountedInputSystem || !TransformOffset.Equals(FTransform::Identity)) + { + // Updating the Transform + HAPI_TransformEuler HapiTransform; + FHoudiniApi::TransformEuler_Init(&HapiTransform); + FHoudiniEngineUtils::TranslateUnrealTransform(TransformOffset, HapiTransform); + + // Set the transform on the OBJ parent + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + FHoudiniEngine::Get().GetSession(), InObject->GetInputObjectNodeId(), &HapiTransform), false); + } + + return bSuccess; +} + +// Upload all the input's data layers to Houdini +bool FHoudiniInputTranslator::UploadDataLayers(UHoudiniInput* InInput, FHoudiniUnrealDataLayersCache* Cache) +{ + return true; +} + +bool +FHoudiniInputTranslator::CreateMergeSOP( + HAPI_NodeId& InOutMergeNodeId, + const TArray& InNodeIdsToConnect, + const FString& InMergeNodeName) +{ + HAPI_NodeId NodeId = -1; + + // Create the merge node + if (FHoudiniEngineUtils::CreateNode(-1, TEXT("SOP/merge"), InMergeNodeName, true, &NodeId) != HAPI_RESULT_SUCCESS) + return false; + + // If the previous node was valid, attempt to delete it + if (InOutMergeNodeId >= 0 && FHoudiniEngineUtils::IsHoudiniNodeValid(InOutMergeNodeId)) + { + const HAPI_NodeId ObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(InOutMergeNodeId); + if (ObjectNodeId >= 0) + FHoudiniEngineUtils::DeleteHoudiniNode(ObjectNodeId); + else + FHoudiniEngineUtils::DeleteHoudiniNode(InOutMergeNodeId); + } + + InOutMergeNodeId = NodeId; + + if (!SetMergeSOPInputs(InOutMergeNodeId, InNodeIdsToConnect)) + return false; + + return true; +} + +bool +FHoudiniInputTranslator::SetMergeSOPInputs(const HAPI_NodeId InMergeNodeId, const TArray& InNodeIdsToConnect) +{ + FUnrealObjectInputManager* SM = FUnrealObjectInputManager::Get(); + + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(InMergeNodeId)) + return false; + + const HAPI_Session* const Session = FHoudiniEngine::Get().GetSession(); + + // Get currently connected inputs + TArray PrevConnectedNodes; + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + if (FHoudiniApi::GetNodeInfo(Session, InMergeNodeId, &NodeInfo) == HAPI_RESULT_SUCCESS) + { + // There is no function in HAPI currently to directly get the number of _connected_ input nodes or to + // compose a list of the connected input nodes. + // Nodes with "infinite" inputs, such as the Merge SOP, always have NodeInfo.inputCount == 9999. So we + // stop iteration at the first disconnected input instead of visiting all 9999 possible indices. + for (int32 InputIndex = 0; InputIndex < NodeInfo.inputCount; ++InputIndex) + { + HAPI_NodeId ConnectedInputNodeId = -1; + if (FHoudiniApi::QueryNodeInput(Session, InMergeNodeId, InputIndex, &ConnectedInputNodeId) != HAPI_RESULT_SUCCESS || ConnectedInputNodeId < 0) + break; + PrevConnectedNodes.Add(ConnectedInputNodeId); + } + } + + // Connect referenced nodes + TSet ConnectedNodeSet; + int32 InputIndex = 0; + for (const HAPI_NodeId& NodeId : InNodeIdsToConnect) + { + if (NodeId < 0) + continue; + + // Connect the current input object to the merge node + if (FHoudiniApi::ConnectNodeInput(Session, InMergeNodeId, InputIndex, NodeId, 0) != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FUnrealObjectInputUtils::ConnectReferencedNodes] Failed to connected node input: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + continue; + } + // HAPI will automatically create a object_merge node (we are expecting that NodeId and RefNodeId are never in + // the same network), we need to set the xformtype to "Into specified object" + HAPI_NodeId ConnectedNodeId = -1; + if (FHoudiniApi::QueryNodeInput(Session, InMergeNodeId, InputIndex, &ConnectedNodeId) != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FUnrealObjectInputUtils::ConnectReferencedNodes] Failed to query connected node input: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + continue; + + } + InputIndex++; + + if (ConnectedNodeId < 0) + { + // No connection was made even though the previous functions were successful!? + continue; + } + + ConnectedNodeSet.Add(ConnectedNodeId); + + // Set the transform value to "Into Specified Object" + // Set the transform object to the world origin null from the manager + FUnrealObjectInputUtils::SetObjectMergeXFormTypeToWorldOrigin(ConnectedNodeId); + } + + // Disconnect previously connected nodes at indices >= FirstUnusedInputIndex + // Disconnect in reverse: inputs are consolidated on disconnect on "infinite" input nodes like the Merge SOP + const int32 FirstUnusedInputIndex = InputIndex; + for (int32 InputIndexToDelete = PrevConnectedNodes.Num(); InputIndexToDelete >= FirstUnusedInputIndex; --InputIndexToDelete) + { + HOUDINI_CHECK_ERROR(FHoudiniApi::DisconnectNodeInput(Session, InMergeNodeId, InputIndexToDelete)); + } + + // Delete nodes from previous connections that are no longer used (the object merge SOPs automatically created + // by HAPI) + for (const HAPI_NodeId& NodeToDeleteId : PrevConnectedNodes) + { + if (ConnectedNodeSet.Contains(NodeToDeleteId)) + continue; + // Check that the node is valid / still exists before attempting to delete the node + HAPI_NodeInfo NodeToDeleteInfo; + FHoudiniApi::NodeInfo_Init(&NodeToDeleteInfo); + if (FHoudiniApi::GetNodeInfo(Session, NodeToDeleteId, &NodeInfo) != HAPI_RESULT_SUCCESS) + continue; + bool bNodeIsValid = false; + if (FHoudiniApi::IsNodeValid(Session, NodeToDeleteId, NodeInfo.uniqueHoudiniNodeId, &bNodeIsValid) != HAPI_RESULT_SUCCESS || !bNodeIsValid) + continue; + HOUDINI_CHECK_ERROR(FHoudiniApi::DeleteNode(Session, NodeToDeleteId)); + } + + return true; +} + + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngine/Private/HoudiniInputTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "CoreMinimal.h" + +#include "HoudiniEnginePrivatePCH.h" + +struct FHoudiniUnrealDataLayersCache; +class AActor; + +class UHoudiniInput; +class UHoudiniParameter; +class UHoudiniAssetComponent; +class UHoudiniInputObject; +class UHoudiniInputStaticMesh; +class UHoudiniInputSkeletalMesh; +class UHoudiniInputAnimation; +class UHoudiniInputSkeletalMeshComponent; +class UHoudiniInputGeometryCollection; +class UHoudiniInputGeometryCollectionComponent; +class UHoudiniInputGeometryCollectionActor; +class UHoudiniInputSceneComponent; +class UHoudiniInputMeshComponent; +class UHoudiniInputInstancedMeshComponent; +class UHoudiniInputSplineComponent; +class UHoudiniInputHoudiniSplineComponent; +class UHoudiniInputHoudiniAsset; +class UHoudiniInputActor; +class UHoudiniInputLandscape; +class UHoudiniInputBrush; +class UHoudiniSplineComponent; +class UHoudiniInputCameraComponent; +class UHoudiniInputPCGData; +class UHoudiniInputDataTable; +class UHoudiniInputFoliageType_InstancedStaticMesh; +class UHoudiniInputBlueprint; +class UHoudiniInputLandscapeSplinesComponent; +class UHoudiniInputSplineMeshComponent; +class UHoudiniInputLevelInstance; +class UHoudiniInputPackedLevelActor; +class UHoudiniInputTexture; +class FUnrealObjectInputHandle; +class FUnrealObjectInputIdentifier; + +struct FHoudiniInputObjectSettings; + +class AActor; + +enum class EHoudiniInputType : uint8; +enum class EHoudiniLandscapeExportType : uint8; + +struct HOUDINIENGINE_API FHoudiniInputTranslator +{ + // TODO COOKABLE: + // UpdateInputs just call BuildAllInputs + // both should be merged + static bool UpdateInputs( + HAPI_NodeId InNodeId, + UObject* InOuter, + TArray>& Inputs, + TArray>& Parameters, + bool bLoadedInputs); + + // Update inputs from the asset + // @AssetId: NodeId of the digital asset + // @OuterObject: Object to use for transactions and as Outer for new inputs + // @CurrentInputs: pre: current & post: invalid inputs + // @NewParameters: pre: empty & post: new inputs + // On Return: CurrentInputs are the old inputs that are no longer valid, + // NewInputs are new and re-used inputs. + static bool BuildAllInputs( + HAPI_NodeId AssetId, + class UObject* OuterObject, + TArray>& Inputs, + TArray>& Parameters); + + // Update all the inputs that have been marked as change + static bool UploadChangedInputs( + TArray>& InInputs, + AActor* InActorOwner); + + // Only update simple input properties + static bool UpdateInputProperties(UHoudiniInput* InInput); + + // Update the KeepWorldTransform / Object merge transform type property + static bool UpdateTransformType(UHoudiniInput* InInput); + + // Update the pack before merge parameter for World/Geometry inputs + static bool UpdatePackBeforeMerge(UHoudiniInput* InInput); + + // Update the transform offset for geometry inputs + static bool UpdateTransformOffset(UHoudiniInput* InInput); + + // Upload all the input's data to Houdini + static bool UploadInputData(UHoudiniInput* InInput, const FTransform & InActorTransform = FTransform::Identity); + + // Upload all the input's transforms to Houdini + static bool UploadInputTransform(UHoudiniInput* InInput); + + // Upload all the input's data layers to Houdini + static bool UploadDataLayers(UHoudiniInput* InInput, FHoudiniUnrealDataLayersCache* Cache); + + // Upload data for an input's InputObject + static bool UploadHoudiniInputObject( + UHoudiniInput* InInput, + UHoudiniInputObject* InInputObject, + const FTransform& InActorTransform, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted); + + // Upload transform for an input's InputObject + static bool UploadHoudiniInputTransform( + UHoudiniInput* InInput, UHoudiniInputObject* InInputObject); + + // Updates/ticks world inputs + static bool UpdateWorldInputs(TArray>& InInputs, AActor* InActorOwner); + + // Updates/ticks the given world input + static bool UpdateWorldInput(UHoudiniInput* InInput); + + // Connect an input's nodes to its linked HDA node + static bool ConnectInputNode(UHoudiniInput* InInput); + + // Destroys an input + static bool DisconnectAndDestroyInput(UHoudiniInput* InputToDestroy, const EHoudiniInputType& InputType); + static bool DisconnectInput(UHoudiniInput* InputToDestroy, const EHoudiniInputType& InputType); + static bool DestroyInputNodes(UHoudiniInput* InputToDestroy, const EHoudiniInputType& InputType); + + static EHoudiniInputType GetDefaultInputTypeFromLabel(const FString& InputName); + + static bool SetDefaultInputFromParameterValue(UHoudiniInput* Input, bool& bOutBlueprintStructureModified); + + static bool ChangeInputType(UHoudiniInput* Input, const bool& bForce); + + static bool HapiCreateInputNodeForObject( + const FString& InObjNodeName, UHoudiniInputObject* InObject); + + static bool HapiSetGeoObjectTransform(const HAPI_NodeId& InObjectNodeId, const FTransform& InTransform); + + static bool HapiCreateOrUpdateGeoObjectMergeAndSetTransform( + const int32 InParentNodeId, + HAPI_NodeId InNodeToObjectMerge, + const FString& InObjNodeName, + HAPI_NodeId& InOutObjectMergeNodeId, + HAPI_NodeId& InOutGeoObjectNodeId, + const bool bInCreateIfMissingInvalid=true, + const FTransform& InTransform=FTransform::Identity, + int32 InTransformType=-1); + + static bool HapiCreateInputNodeForStaticMesh( + const FString& InObjNodeName, + UHoudiniInputStaticMesh* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForHoudiniSplineComponent( + const FString& InObjNodeName, + UHoudiniInputHoudiniSplineComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings); + + static bool HapiCreateInputNodeForLandscapeSplinesComponent( + const FString& InObjNodeName, + UHoudiniInputLandscapeSplinesComponent* const InObject, + const FHoudiniInputObjectSettings& InInputSettings, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + const bool bInInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForLandscape( + const FString& InObjNodeName, + UHoudiniInputLandscape* InObject, + UHoudiniInput* InInput, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForLevelInstance( + const FString& InObjNodeName, + UHoudiniInputLevelInstance* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + UHoudiniInput* InInput, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForPackedLevelActor( + const FString& InObjNodeName, + UHoudiniInputPackedLevelActor* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + UHoudiniInput* InInput, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForSkeletalMesh( + const FString& InObjNodeName, + UHoudiniInputSkeletalMesh* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForAnimation( + const FString& InObjNodeName, + UHoudiniInputAnimation* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForSkeletalMeshComponent( + const FString& InObjNodeName, + UHoudiniInputSkeletalMeshComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForGeometryCollection( + const FString& InObjNodeName, + UHoudiniInputGeometryCollection* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForGeometryCollectionComponent( + const FString& InObjNodeName, + UHoudiniInputGeometryCollectionComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForSceneComponent( + const FString& InObjNodeName, + UHoudiniInputSceneComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForStaticMeshComponent( + const FString& InObjNodeName, + UHoudiniInputMeshComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForSplineMeshComponents( + const FString& InObjNodeName, + UHoudiniInputActor* InParentActorObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForInstancedStaticMeshComponent( + const FString& InObjNodeName, + UHoudiniInputInstancedMeshComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForSplineComponent( + const FString& InObjNodeName, + UHoudiniInputSplineComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForHoudiniCookable( + const FString& InObjNodeName, + UHoudiniInputHoudiniAsset* InObject, + const FHoudiniInputObjectSettings& InInputSettings); + + static bool HapiCreateInputNodesForActorComponents( + UHoudiniInput* const InInput, + UHoudiniInputActor* const InInputActorObject, + AActor* const InActor, + const FTransform& InActorTransform, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForActor( + UHoudiniInput* InInput, + UHoudiniInputActor* InObject, + const FTransform & InActorTransform, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForBP( + UHoudiniInput* InInput, + UHoudiniInputBlueprint* InObject, + TArray& OutCreatedNodeIds, + TSet& OutHandles, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForCamera( + const FString& InObjNodeName, + UHoudiniInputCameraComponent* InObject, + const FHoudiniInputObjectSettings& InInputSettings); + + // Create input node for Brush. Optionally exclude actors when combining + // brush with other intersecting brushes. This is typically used to + // exclude Selector objects. + static bool HapiCreateInputNodeForBrush( + const FString& InObjNodeName, + UHoudiniInputBrush* InObject, + TArray>* ExcludeActors, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForPCGData( + const FString& InNodeName, + UHoudiniInputPCGData* InInputObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForDataTable( + const FString& InNodeName, + UHoudiniInputDataTable* InInputObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForFoliageType_InstancedStaticMesh( + const FString& InObjNodeName, + UHoudiniInputFoliageType_InstancedStaticMesh* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForTexture2D( + const FString& InObjNodeName, + UHoudiniInputTexture* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted, + bool bIsCOPInput); + + // Import as reference, wrapper function + static bool HapiCreateInputNodeForReference( + const FString& InObjNodeName, + UHoudiniInputObject* InObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForActorReference( + UHoudiniInputActor* InActorObject, + const FHoudiniInputObjectSettings& InInputSettings, + bool bInputNodesCanBeDeleted); + + // HAPI: Create an input node for reference + static bool CreateInputNodeForReference( + const HAPI_NodeId InParentNodeId, + HAPI_NodeId& InputNodeId, + const FString& InRef, + const FString& InputNodeName, + const FTransform& InTransform, + bool bImportAsReferenceRotScaleEnabled, + bool bImportAsReferenceBboxEnabled = false, + const FBox& InBbox = FBox(EForceInit::ForceInit), + bool bImportAsReferenceMaterialEnabled = false, + const TArray& MaterialReferences = TArray()); + + // HAPI: Create an input node for reference + static bool CreateInputNodeForReference( + HAPI_NodeId& InputNodeId, + UObject const* const InObjectToRef, + const FString& InputNodeName, + const FTransform& InTransform, + bool bImportAsReferenceRotScaleEnabled, + FUnrealObjectInputHandle& OutHandle, + bool bInputNodesCanBeDeleted, + bool bImportAsReferenceBboxEnabled = false, + const FBox& InBbox = FBox(EForceInit::ForceInit), + bool bImportAsReferenceMaterialEnabled = false, + const TArray& MaterialReferences = TArray()); + + //static bool HapiUpdateInputNodeTransform(const HAPI_NodeId InputNodeId, const FTransform& Transform); + + // Create a Merge SOP. If InInOutMergeNodeId is set to >= 0 then the node will be deleted, if valid, after the + // merge SOP is created. The inputs of the Merge SOP are set to InNodeIdsToConnect. + static bool CreateMergeSOP( + HAPI_NodeId& InOutMergeNodeId, + const TArray& InNodeIdsToConnect, + const FString& InMergeNodeName); + + // Set InNodeIdsToConenct as the inputs of the Merge SOP InMergeNodeId. + static bool SetMergeSOPInputs( + HAPI_NodeId InMergeNodeId, + const TArray& InNodeIdsToConnect); +}; + + +``` + +`Source/HoudiniEngine/Private/HoudiniInstanceTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniInstanceTranslator.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniEngineString.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniGenericAttribute.h" +#include "HoudiniInstancedActorComponent.h" +#include "HoudiniMaterialTranslator.h" +#include "HoudiniOutput.h" +#include "HoudiniStaticMeshComponent.h" +#include "HoudiniStaticMesh.h" +#include "HoudiniFoliageTools.h" + +//#include "HAPI/HAPI_Common.h" + +#include "Engine/StaticMesh.h" +#include "ComponentReregisterContext.h" +#include "HoudiniMaterialTranslator.h" +#include "Components/StaticMeshComponent.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "Components/HierarchicalInstancedStaticMeshComponent.h" +#include "InstancedFoliageActor.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "GeometryCollection/GeometryCollectionComponent.h" +#else + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionComponent.h" +#endif +#include "FoliageEditUtility.h" +#include "LevelInstance/LevelInstanceActor.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 +#include "LevelInstance/LevelInstanceComponent.h" +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + #include "StaticMeshComponentLODInfo.h" +#endif + +#if WITH_EDITOR + //#include "ScopedTransaction.h" + #include "LevelEditorViewport.h" + #include "MeshPaintHelpers.h" +#endif +#include "HoudiniEngineAttributes.h" +#include "HoudiniFoliageUtils.h" +#include "HoudiniMeshTranslator.h" +#include "GeometryCollection/GeometryCollectionRenderLevelSetActor.h" +#include + +#include "HoudiniHLODLayerUtils.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +// +FHoudiniInstancerPartData +FHoudiniInstanceTranslator::PopulateInstancedOutputPartData( + const FHoudiniGeoPartObject& InHGPO, + const TArray& InAllOutputs) +{ + FHoudiniInstancerPartData PartData; + bool bSuccess = false; + + switch (InHGPO.InstancerType) + { + case EHoudiniInstancerType::GeometryCollection: + case EHoudiniInstancerType::PackedPrimitive: + bSuccess = GetPackedPrimitiveInstancerPartData(InHGPO, InAllOutputs, PartData); + break; + + case EHoudiniInstancerType::AttributeInstancer: + bSuccess = GetAttributeInstancerPartData(InHGPO, PartData); + break; + case EHoudiniInstancerType::OldSchoolAttributeInstancer: + HOUDINI_LOG_ERROR(TEXT("Old School Attribute Instancers are deprecated")); + break; + + case EHoudiniInstancerType::ObjectInstancer: + HOUDINI_LOG_ERROR(TEXT("Object Instancers are deprecated")); + break; + } + + if (!bSuccess) + return {}; + + GetGenericPropertiesAttributes(InHGPO.GeoId, InHGPO.PartId, PartData.AllPropertyAttributes); + GetPerInstanceCustomData(InHGPO.GeoId, InHGPO.PartId, PartData); + GetMaterialOverridesFromAttributes(InHGPO.GeoId, InHGPO.PartId, 0, InHGPO.InstancerType, PartData.MaterialAttributes); + + // Store custom prim data in the instancer + for (auto& Instancer : PartData.Instancers) + { + int NumInstances = Instancer.AttributeIndices.Num(); + if (NumInstances <= 0) + continue; + + TArray CustomPrimitiveData; + FHoudiniMeshTranslator::GetCustomPrimitiveData( + InHGPO.GeoId, InHGPO.PartId, Instancer.AttributeIndices[0], CustomPrimitiveData); + + Instancer.CustomPrimData = CustomPrimitiveData; + } + + return PartData; +} + +int +FHoudiniInstanceTranslator::CreateAllInstancersFromHoudiniOutputs( + const TArray& InAllOutputs, + UObject* InOuterComponent, + const FHoudiniPackageParams& InPackageParms, + const TMap* InPreBuiltInstancedOutputPartData) +{ + return CreateAllInstancersFromHoudiniOutputs( + InAllOutputs, + InAllOutputs, + InOuterComponent, + InPackageParms, + InPreBuiltInstancedOutputPartData); +} + +int +FHoudiniInstanceTranslator::CreateAllInstancersFromHoudiniOutputs( + const TArray& OutputsToUpdate, + const TArray& InAllOutputs, + UObject* InOuterComponent, + const FHoudiniPackageParams& InPackageParms, + const TMap* InPreBuiltInstancedOutputPartData) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInstanceTranslator::CreateAllInstancersFromHoudiniOutputs); + + USceneComponent* ParentComponent = Cast(InOuterComponent); + if (!ParentComponent) + return false; + + int InstanceCount = 0; + for (auto Output : OutputsToUpdate) + { + if (Output->GetType() != EHoudiniOutputType::Instancer) + continue; + + bool bSuccess = FHoudiniInstanceTranslator::CreateAllInstancersFromHoudiniOutput( + Output, + InAllOutputs, + InOuterComponent, + InPackageParms, + InPreBuiltInstancedOutputPartData); + + if (bSuccess) + ++InstanceCount; + } + + bool bOutputFoliage = false; + for (auto Output : OutputsToUpdate) + { + for(auto It : Output->OutputObjects) + { + if (It.Value.FoliageType) + { + bOutputFoliage = true; + break; + } + } + if (bOutputFoliage) + break; + + } + if (bOutputFoliage) + { + FHoudiniEngineUtils::RepopulateFoliageTypeListInUI(); + } + return InstanceCount; +} + + +bool +FHoudiniInstanceTranslator::CreateAllInstancersFromHoudiniOutput( + UHoudiniOutput* InOutput, + const TArray& InAllOutputs, + UObject* InOuterComponent, + const FHoudiniPackageParams& InPackageParams, + const TMap* InPreBuiltInstancedOutputPartData) +{ + if (!IsValid(InOutput)) + return false; + + if (!IsValid(InOuterComponent)) + return false; + + if (InOutput->Type == EHoudiniOutputType::GeometryCollection) + return true; + + // Keep track of the previous cook's component to clean them up after + TMap OutputObjects; + + USceneComponent* ParentComponent = Cast(InOuterComponent); + if (!ParentComponent) + return false; + + //------------------------------------------------------------------------------------------------------------------------------ + // If Part Data was passed in, use that. If not fetch part data + //------------------------------------------------------------------------------------------------------------------------------ + + TMap LocalPartData; + const TMap * Parts = InPreBuiltInstancedOutputPartData; + if (!Parts) + { + for (const FHoudiniGeoPartObject& HGPO : InOutput->HoudiniGeoPartObjects) + { + // Not an instancer, skip + if (HGPO.Type != EHoudiniPartType::Instancer) + continue; + + // Prepare this output object's output identifier + FHoudiniOutputObjectIdentifier OutputIdentifier; + OutputIdentifier.ObjectId = HGPO.ObjectId; + OutputIdentifier.GeoId = HGPO.GeoId; + OutputIdentifier.PartId = HGPO.PartId; + OutputIdentifier.PartName = HGPO.PartName; + + FHoudiniInstancerPartData InstancedOutputPartDataTmp = PopulateInstancedOutputPartData(HGPO, InAllOutputs); + LocalPartData.Add(OutputIdentifier, InstancedOutputPartDataTmp); + } + Parts = &LocalPartData; + } + + //------------------------------------------------------------------------------------------------------------------------------ + // Iterate on all the outputs' HGPOs, creating instancers as we go + //------------------------------------------------------------------------------------------------------------------------------ + + TMap InstancedOutputs; + + for (auto It : *Parts) + { + const FHoudiniInstancerPartData& InstancePartData = It.Value; + + // Create the instancer components now + for (int32 InstanceObjectIdx = 0; InstanceObjectIdx < InstancePartData.Instancers.Num(); InstanceObjectIdx++) + { + auto& Instancer = InstancePartData.Instancers[InstanceObjectIdx]; + + // Prepare this output object's output identifier + FHoudiniOutputObjectIdentifier OutputIdentifier = It.Key; + OutputIdentifier.SplitIdentifier = Instancer.SplitName; + + FHoudiniPackageParams InstancerPackageParams = InPackageParams; + InstancerPackageParams.ObjectId = OutputIdentifier.ObjectId; + InstancerPackageParams.GeoId = OutputIdentifier.GeoId; + InstancerPackageParams.PartId = OutputIdentifier.PartId; + + // Find the matching instance output now + // Instanced output only use the original object index for their split identifier + FHoudiniOutputObjectIdentifier InstancedOutputIdentifier = OutputIdentifier; + InstancedOutputIdentifier.SplitIdentifier = Instancer.SplitName; + + // Get all the materials needed for this object + // Multiple material slots are supported, as well as creating new material instances if needed + TArray InstanceMaterials; + InstanceMaterials = GetAllInstancerMaterials(Instancer.AttributeIndices[0], InstancePartData.GeoPartObject, InPackageParams); + + + FHoudiniOutputObject OutputObject; + UObject* InstancedObject; + + bool bSuccess = CreateInstancer( + InstancedOutputIdentifier, + OutputObject, + InstancedObject, + Instancer, + InstancePartData, + InstancerPackageParams, + ParentComponent, + InstanceMaterials); + + // Make sure the output is valid, if so add it to the outputs. + if (bSuccess) + { + FHoudiniOutputObject& NewOutputObject = OutputObjects.FindOrAdd(InstancedOutputIdentifier); + NewOutputObject = MoveTemp(OutputObject); + + FHoudiniInstancedOutput& InstancedOutput = InstancedOutputs.FindOrAdd(InstancedOutputIdentifier); + InstancedOutput.NumInstances = Instancer.AttributeIndices.Num(); + InstancedOutput.InstancedObject = InstancedObject; + } + } + } + + InOutput->SetInstancedOutputs(InstancedOutputs); + InOutput->SetOutputObjects(OutputObjects); + + return true; +} + +bool +FHoudiniInstanceTranslator::FindInstancedOutputObject( + const FHoudiniGeoPartObject& HGPO, + const TArray& InAllOutputs, + FHoudiniOutputObjectIdentifier & OutId, + const UHoudiniOutput* & OutOutput) +{ + for (const auto& Output : InAllOutputs) + { + if (!Output || Output->Type != EHoudiniOutputType::Mesh) + continue; + + for (const auto& OutObjPair : Output->OutputObjects) + { + if (!OutObjPair.Key.Matches(HGPO)) + continue; + + const FHoudiniOutputObject& CurrentOutputObject = OutObjPair.Value; + + if (CurrentOutputObject.bIsImplicit) + continue; + + OutOutput = Output; + OutId = OutObjPair.Key; + return true; + } + } + + return false; +} + +void +FHoudiniInstanceTranslator::SetInstancerObject( + FHoudiniInstancer & InstancerData, + const FHoudiniGeoPartObject& HGPO, + const TArray& InAllOutputs) +{ + FHoudiniOutputObjectIdentifier OutId; + const UHoudiniOutput* OutOutput; + + bool bFound = FindInstancedOutputObject(HGPO, InAllOutputs, OutId, OutOutput); + if (!bFound) + return; + + const FHoudiniOutputObject * OutputObject = OutOutput->OutputObjects.Find(OutId); + + if (InstancerData.AttributeIndices.Num() == 1 && OutputObject->bProxyIsCurrent && IsValid(OutputObject->ProxyObject)) + { + InstancerData.ObjectPath = OutputObject->ProxyObject->GetPathName(); + } + else if (IsValid(OutputObject->OutputObject)) + { + InstancerData.ObjectPath = OutputObject->OutputObject->GetPathName(); + + EHoudiniSplitType SplitType = FHoudiniMeshTranslator::GetSplitTypeFromSplitName(OutId.SplitIdentifier); + if (SplitType == EHoudiniSplitType::InvisibleComplexCollider) + { + InstancerData.bVisible = false; + } + } +} + +TArray FHoudiniInstanceTranslator::GetInstancerTransforms(const FHoudiniGeoPartObject& InHGPO) +{ + TArray InstancerPartTransforms; + InstancerPartTransforms.SetNumZeroed(InHGPO.PartInfo.InstanceCount); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetInstancerPartTransforms(FHoudiniEngine::Get().GetSession(), + InHGPO.GeoId, InHGPO.PartInfo.PartId, + HAPI_RSTORDER_DEFAULT, + InstancerPartTransforms.GetData(), 0, InHGPO.PartInfo.InstanceCount), {}); + + // Convert the transform to Unreal's coordinate system + TArray InstancerUnrealTransforms; + InstancerUnrealTransforms.SetNum(InstancerPartTransforms.Num()); + for (int32 InstanceIdx = 0; InstanceIdx < InstancerPartTransforms.Num(); InstanceIdx++) + { + const auto& InstanceTransform = InstancerPartTransforms[InstanceIdx]; + FHoudiniEngineUtils::TranslateHapiTransform(InstanceTransform, InstancerUnrealTransforms[InstanceIdx]); + } + return InstancerUnrealTransforms; +} +TTuple FHoudiniInstanceTranslator::GetSplitData(const FHoudiniGeoPartObject& HGPO, HAPI_AttributeOwner Owner) +{ + //--------------------------------------------------------------------------------------------------------------------------- + // Get split information, if it exists + //--------------------------------------------------------------------------------------------------------------------------- + + FHoudiniHapiAccessor Accessor; + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_SPLIT_ATTR); + FString SplitAttrName; + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, SplitAttrName); + FHoudiniEngineIndexedStringMap SplitAttributes; + if (!SplitAttrName.IsEmpty()) + { + Accessor.Init(HGPO.GeoId, HGPO.PartId, H_TCHAR_TO_UTF8(*SplitAttrName)); + Accessor.GetAttributeStrings(Owner, SplitAttributes); + } + + return TTuple(SplitAttrName, SplitAttributes); +} + +bool FHoudiniInstanceTranslator::GetPackedPrimitiveInstancerPartData( + const FHoudiniGeoPartObject& HGPO, + const TArray& InAllOutputs, + FHoudiniInstancerPartData& PartData) +{ + PartData.GeoPartObject = HGPO; + + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), HGPO.GeoId, HGPO.PartId, &PartInfo), {}); + + //--------------------------------------------------------------------------------------------------------------------------- + // Create a unique instancer for each object/split group for each part being instanced in the part instance. + //--------------------------------------------------------------------------------------------------------------------------- + + TTuple SplitData = GetSplitData(HGPO, HAPI_AttributeOwner::HAPI_ATTROWNER_PRIM); + + TMap InstancerMap; + for (int PartInstance = 0; PartInstance < PartInfo.instancedPartCount; PartInstance++) + { + for (int InstanceIndex = 0; InstanceIndex < PartInfo.instanceCount; InstanceIndex++) + { + + int64_t Id = static_cast(PartInstance) << 32; + if (!SplitData.Value.Ids.IsEmpty()) + { + Id += SplitData.Value.Ids[InstanceIndex]; + } + if (!InstancerMap.Contains(Id)) + { + InstancerMap.Add(Id, PartData.Instancers.Num()); + PartData.Instancers.Add({}); + } + + FHoudiniInstancer& Instancer = PartData.Instancers[InstancerMap[Id]]; + Instancer.AttributeIndices.Add(InstanceIndex); + } + } + + FHoudiniInstancerSettings DefaultSettings = GetDefaultInstancerSettings(HGPO, HAPI_ATTROWNER_DETAIL); + + TArray InstancedPartIds; + InstancedPartIds.SetNum(PartInfo.instancedPartCount); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetInstancedPartIds(FHoudiniEngine::Get().GetSession(), + HGPO.GeoId, HGPO.PartInfo.PartId, + InstancedPartIds.GetData(), 0, PartInfo.instancedPartCount), {}); + + for (auto It : InstancerMap) + { + int32 PartNumber = It.Key >> 32; + int32 SplitStringHandle = It.Key & INT_MAX; + + FHoudiniInstancer& Instancer = PartData.Instancers[It.Value]; + Instancer.SplitName = FString::Printf(TEXT("%d"), PartNumber); + if (!SplitData.Value.Ids.IsEmpty()) + { + Instancer.SplitName += TEXT("_"); + Instancer.SplitName += SplitData.Value.Strings[SplitStringHandle]; + } + + int FirstIndex = Instancer.AttributeIndices[0]; + Instancer.Settings = GetInstancerSettings(HGPO, HAPI_ATTROWNER_PRIM, FirstIndex, DefaultSettings); + + FHoudiniGeoPartObject InstancedHGPO = HGPO; + InstancedHGPO.PartId = InstancedPartIds[PartNumber]; + SetInstancerObject(Instancer, InstancedHGPO, InAllOutputs); + + } + + + PartData.InstanceTransforms = GetInstancerTransforms(HGPO); + + return true; +} + + + +bool +FHoudiniInstanceTranslator::GetAttributeInstancerPartData( + const FHoudiniGeoPartObject& HGPO, + FHoudiniInstancerPartData & PartData) +{ + PartData.GeoPartObject = HGPO; + + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), HGPO.GeoId, HGPO.PartId, &PartInfo), {}); + + //--------------------------------------------------------------------------------------------------------------------------- + // Set UnrealInstanceObjects to the names (object refs) of all the objects that will be instantiated. One per point. + //--------------------------------------------------------------------------------------------------------------------------- + + FHoudiniEngineIndexedStringMap UnrealInstanceObjects; + FHoudiniHapiAccessor Accessor; + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_INSTANCE_OVERRIDE); + Accessor.GetAttributeStrings(HAPI_ATTROWNER_POINT, UnrealInstanceObjects); + if (UnrealInstanceObjects.Ids.IsEmpty()) + { + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_INSTANCE); + Accessor.GetAttributeStrings(HAPI_ATTROWNER_DETAIL, UnrealInstanceObjects); + } + + if (UnrealInstanceObjects.Ids.IsEmpty()) + { + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_INSTANCE_OVERRIDE); + FString DetailString; + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_DETAIL, DetailString); + + UnrealInstanceObjects.Ids.SetNum(PartInfo.pointCount); + UnrealInstanceObjects.Strings.Add(DetailString); + } + + //--------------------------------------------------------------------------------------------------------------------------- + // Create a unique instancer for each object/split group. Combine the split group id and object id into a single int64 for + // quick look up + //--------------------------------------------------------------------------------------------------------------------------- + + TTuple SplitData = GetSplitData(HGPO, HAPI_AttributeOwner::HAPI_ATTROWNER_POINT); + + TMap InstancerMap; + for(int PointIndex = 0; PointIndex < PartInfo.pointCount; PointIndex++) + { + int StringIndex = UnrealInstanceObjects.Ids[PointIndex]; + + int64_t Id = static_cast(UnrealInstanceObjects.Ids[PointIndex]) << 32; + if (!SplitData.Value.Ids.IsEmpty()) + { + Id += SplitData.Value.Ids[PointIndex]; + } + if (!InstancerMap.Contains(Id)) + { + InstancerMap.Add(Id, PartData.Instancers.Num()); + PartData.Instancers.Add({}); + } + + FHoudiniInstancer & Instancer = PartData.Instancers[InstancerMap[Id]]; + Instancer.AttributeIndices.Add(PointIndex); + } + + //--------------------------------------------------------------------------------------------------------------------------- + // Pull all instancer data from Houdini + //--------------------------------------------------------------------------------------------------------------------------- + + FHoudiniInstancerSettings DefaultSettings = GetDefaultInstancerSettings(HGPO, HAPI_ATTROWNER_DETAIL); + + for(auto It : InstancerMap) + { + int32 ObjectStringHandle = It.Key >> 32; + int32 SplitStringHandle = It.Key & INT_MAX; + + FHoudiniInstancer & Instancer = PartData.Instancers[It.Value]; + + Instancer.ObjectPath = UnrealInstanceObjects.Strings[ObjectStringHandle]; + Instancer.SplitName = FString::Printf(TEXT("%d"), ObjectStringHandle); + if (!SplitData.Value.Ids.IsEmpty()) + { + Instancer.SplitName += TEXT("_"); + Instancer.SplitName += SplitData.Value.Strings[SplitStringHandle]; + } + + int FirstIndex = Instancer.AttributeIndices[0]; + Instancer.Settings = GetInstancerSettings(HGPO, HAPI_ATTROWNER_POINT, FirstIndex, DefaultSettings); + } + + //--------------------------------------------------------------------------------------------------------------------------- + // Pull part data + //--------------------------------------------------------------------------------------------------------------------------- + + HapiGetInstanceTransforms(HGPO, PartData.InstanceTransforms); + + return true; +} + +UObject* +FHoudiniInstanceTranslator::LoadInstancedObject(const FString & ObjectPath) +{ + // Load the object using its path. Resolve redirectors if necessary. + UObject * InstanceObject = StaticLoadObject(UObject::StaticClass(), nullptr, *ObjectPath, nullptr, LOAD_None, nullptr); + + while (UObjectRedirector* Redirector = Cast(InstanceObject)) + InstanceObject = Redirector->DestinationObject; + + if (IsValid(InstanceObject)) + return InstanceObject; + + // If could not load the actor, try to load it as a class. + UClass* FoundClass = FHoudiniEngineRuntimeUtils::GetClassByName(ObjectPath); + if (FoundClass != nullptr) + { + // TODO: ensure we'll be able to create an actor from this class! + InstanceObject = FoundClass; + } + + if (IsValid(InstanceObject)) + return InstanceObject; + + // We failed to load anything. Try the default Houdini object if enabled. + if (GetDefault< UHoudiniRuntimeSettings>()->bShowDefaultMesh) + { + HOUDINI_LOG_WARNING(TEXT("Failed to load instanced object '%s', using default instance mesh (hidden in game)."), *(ObjectPath)); + + // Couldn't load the referenced object, use the default reference mesh + UStaticMesh* DefaultReferenceSM = FHoudiniEngine::Get().GetHoudiniDefaultReferenceMesh().Get(); + if (!IsValid(DefaultReferenceSM)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to load the default instance mesh.")); + } + InstanceObject = DefaultReferenceSM; + } + return InstanceObject; +} + + +bool +FHoudiniInstanceTranslator::CreateInstancer( + const FHoudiniOutputObjectIdentifier& Id, + FHoudiniOutputObject & Output, + UObject* & InstanceObject, + const FHoudiniInstancer& Instancers, + const FHoudiniInstancerPartData& InstancerPartData, + const FHoudiniPackageParams& PackageParams, + USceneComponent* ParentComponent, + const TArray& InstancerMaterials) +{ + // See what type of component we want to create + InstancerComponentType InstancerType = InstancerComponentType::Invalid; + + InstanceObject = LoadInstancedObject(*Instancers.ObjectPath); + + while (UObjectRedirector* Redirector = Cast(InstanceObject)) + InstanceObject = Redirector->DestinationObject; + + if (!IsValid(InstanceObject)) + { + HOUDINI_LOG_ERROR(TEXT("Could not find %s"), *Instancers.ObjectPath); + return {}; + } + + if (InstanceObject->IsA() || Instancers.Settings.bIsFoliage) + { + // We must test for foliage type first, or FT will be considered as meshes + InstancerType = Foliage; + } + else if(InstanceObject->IsA()) + { + bool bMustUseInstancerComponent = Instancers.AttributeIndices.Num() > 1 || Instancers.Settings.bForceInstancer; + + UStaticMesh* StaticMesh = Cast(InstanceObject); + bool bNaniteEnabled = +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + StaticMesh->IsNaniteEnabled(); +#else + StaticMesh->NaniteSettings.bEnabled; +#endif + + // It is recommended to avoid putting Nanite mesh in HISM since they have their own LOD mechanism. + // Will also improve performance by avoiding access to the render data to fetch the LOD count which could + // trigger an async mesh wait until it has been computed. + if (bNaniteEnabled && Instancers.Settings.bForceHISM) + { + // Warn the user that forcing HISM with a Nanite mesh is a bad idea + HOUDINI_LOG_WARNING(TEXT("Forcing Hierarchical Instancers with Nanite meshes is not recommended as it will reduce performance compared to using an Instanced Static Mesh Component.")); + } + + if(Instancers.Settings.bForceHISM) + InstancerType = HierarchicalInstancedStaticMeshComponent; + else if (!bNaniteEnabled && (bMustUseInstancerComponent && StaticMesh->GetNumLODs() > 1)) + InstancerType = HierarchicalInstancedStaticMeshComponent; + else if (bMustUseInstancerComponent) + InstancerType = InstancedStaticMeshComponent; + else + InstancerType = StaticMeshComponent; + } + else if (InstanceObject->IsA()) + { + if (Instancers.AttributeIndices.Num() == 1) + { + InstancerType = HoudiniStaticMeshComponent; + } + else + { + HOUDINI_LOG_ERROR(TEXT("More than one instance transform encountered for UHoudiniStaticMesh: %s"), *(InstanceObject->GetPathName())); + InstancerType = Invalid; + return {}; + } + } + else if (InstanceObject->IsA()) + { + if (Instancers.Settings.bIsFoliage) + { + HOUDINI_LOG_ERROR(TEXT("Cannot use a level instance as foliage")); + return {}; + } + InstancerType = LevelInstance; + } + else + { + InstancerType = HoudiniInstancedActorComponent; + } + + bool bCheckRenderState = false; + bool bSuccess = false; + + switch (InstancerType) + { + case InstancedStaticMeshComponent: + case HierarchicalInstancedStaticMeshComponent: + { + // Create an Instanced Static Mesh Component + bSuccess = CreateInstancedStaticMeshInstancer( + Output, + Instancers, + InstanceObject, + InstancerPartData, + ParentComponent, + InstancerMaterials, + InstancerType); + bCheckRenderState = true; + } + break; + + case HoudiniInstancedActorComponent: + { + bSuccess = CreateInstancedActorInstancer( + Output, + Instancers, + InstanceObject, + InstancerPartData, + ParentComponent); + } + break; + + case StaticMeshComponent: + { + // Create a Static Mesh Component + bSuccess = CreateStaticMeshInstancer( + Output, + Instancers, + InstanceObject, + InstancerPartData, + ParentComponent, + InstancerMaterials); + bCheckRenderState = true; + } + break; + + case HoudiniStaticMeshComponent: + { + // Create a Houdini Static Mesh Component + bSuccess = CreateHoudiniStaticMeshInstancer( + Output, + Instancers, + InstanceObject, + InstancerPartData, + ParentComponent, + InstancerMaterials); + } + break; + + case Foliage: + { + bSuccess = CreateFoliageInstancer( + Id, + Output, + Instancers, + InstanceObject, + InstancerPartData, + PackageParams, + ParentComponent, + InstancerMaterials); + + } + break; + case LevelInstance: + { + // Create a Houdini Static Mesh Component + bSuccess = CreateLevelInstanceInstancer( + Output, + Instancers, + InstanceObject, + InstancerPartData, + ParentComponent, + InstancerMaterials); + } + break; + default: + + break; + } + + if (!bSuccess) + return false; + + for(auto Object : Output.OutputComponents) + { + USceneComponent * NewComponentToSet = Cast(Object); + + // UE5: Make sure we update/recreate the Component's render state + // after the update or the mesh component will not be rendered! + if (bCheckRenderState) + { + UMeshComponent* NewMeshComponent = Cast(NewComponentToSet); + if (IsValid(NewMeshComponent)) + { + if (NewMeshComponent->IsRenderStateCreated()) + { + // Need to send this to render thread at some point + NewMeshComponent->MarkRenderStateDirty(); + } + else if (NewMeshComponent->ShouldCreateRenderState()) + { + // If we didn't have a valid StaticMesh assigned before + // our render state might not have been created so + // do it now. + NewMeshComponent->RecreateRenderState_Concurrent(); + } + } + } + + NewComponentToSet->SetMobility(ParentComponent->Mobility); + + if (InstancerType != Foliage && InstancerType != LevelInstance) + NewComponentToSet->AttachToComponent(ParentComponent, FAttachmentTransformRules::KeepRelativeTransform); + + // Only register if we have a valid component + if (NewComponentToSet->GetOwner() && NewComponentToSet->GetWorld()) + NewComponentToSet->RegisterComponent(); + } + + if (!Instancers.bVisible) + { + for (auto Object : Output.OutputComponents) + { + USceneComponent* InstancerComponent = Cast(Object); + InstancerComponent->SetVisibleFlag(false); + } + } + + UStaticMesh* DefaultReferenceSM = FHoudiniEngine::Get().GetHoudiniDefaultReferenceMesh().Get(); + + for (auto Object : Output.OutputComponents) + { + USceneComponent* InstancerComponent = Cast(Object); + SetPerInstanceCustomData(Instancers, InstancerComponent); + FHoudiniMeshTranslator::SetCustomPrimitiveData(Instancers.CustomPrimData, InstancerComponent); + + // If the instanced object (by ref) wasn't found, hide the component in game + if (Output.OutputObject == DefaultReferenceSM) + { + InstancerComponent->SetHiddenInGame(true); + } + else + { + // TODO: Revisit why this is need. + // See if the HiddenInGame property is overriden + bool bOverridesHiddenInGame = false; + for (auto& CurPropAttr : InstancerPartData.AllPropertyAttributes) + { + if (CurPropAttr.AttributeName.Equals(TEXT("HiddenInGame")) || CurPropAttr.AttributeName.Equals(TEXT("bHiddenInGame"))) + { + bOverridesHiddenInGame = true; + break; + } + } + + // Don't force the property if it is overriden by generic attributes + if (!bOverridesHiddenInGame) + InstancerComponent->SetHiddenInGame(false); + } + } + + Output.CachedAttributes.Empty(); + Output.CachedTokens.Empty(); + + if (!Instancers.Settings.LevelPath.IsEmpty()) + { + Output.CachedAttributes.Add(HAPI_UNREAL_ATTRIB_LEVEL_PATH, Instancers.Settings.LevelPath); + } + if (!Instancers.Settings.OutputName.IsEmpty()) + { + Output.CachedAttributes.Add(HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2, Instancers.Settings.OutputName); + } + if (!Instancers.Settings.BakeActorClassName.IsEmpty()) + { + Output.CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_ACTOR_CLASS, Instancers.Settings.BakeActorClassName); + } + if (!Instancers.Settings.BakeActorName.IsEmpty()) + { + Output.CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_ACTOR, Instancers.Settings.BakeActorName); + } + if (!Instancers.Settings.BakeOutlinerFolder.IsEmpty()) + { + Output.CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_OUTLINER_FOLDER, Instancers.Settings.BakeOutlinerFolder); + } + if (!Instancers.Settings.BakeFolder.IsEmpty()) + { + Output.CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_FOLDER, Instancers.Settings.BakeFolder); + } + + Output.DataLayers = Instancers.Settings.DataLayers; + Output.HLODLayers = Instancers.Settings.HLODLayers; + + // For Houdini Mesh Proxy - we need to make sure the HSMC is only set on the output's proxy component + if (InstancerType == HoudiniStaticMeshComponent && Output.ProxyComponent != nullptr) + { + Output.OutputComponents.Empty(); + } + + return true; +} + +bool +FHoudiniInstanceTranslator::CreateInstancedStaticMeshInstancer( + FHoudiniOutputObject& Output, + const FHoudiniInstancer& Instancer, + UObject* InstanceObject, + const FHoudiniInstancerPartData& InstancerPartData, + USceneComponent* ParentComponent, + const TArray& InstancerMaterials, + const InstancerComponentType& InstancerType) +{ + + UStaticMesh* InstancedStaticMesh = Cast(InstanceObject); + + if (!InstancedStaticMesh) + return false; + + if (!IsValid(ParentComponent)) + return false; + + UObject* ComponentOuter = ParentComponent; + if (IsValid(ParentComponent->GetOwner())) + ComponentOuter = ParentComponent->GetOwner(); + + UInstancedStaticMeshComponent* InstancedStaticMeshComponent = nullptr; + if(InstancerType == HierarchicalInstancedStaticMeshComponent) + { + // Use Hierarchical ISMC + // Either forced, or if the mesh isn't Nanite and has LODs + InstancedStaticMeshComponent = NewObject( + ComponentOuter, UHierarchicalInstancedStaticMeshComponent::StaticClass(), NAME_None, RF_Transactional); + } + else + { + // If the mesh doesnt have LODs, we can use a regular ISMC + InstancedStaticMeshComponent = NewObject( + ComponentOuter, UInstancedStaticMeshComponent::StaticClass(), NAME_None, RF_Transactional); + } + + // Change the creation method so the component is listed in the details panels + if (InstancedStaticMeshComponent) + FHoudiniEngineRuntimeUtils::AddOrSetAsInstanceComponent(InstancedStaticMeshComponent); + else + return false; + + Output.OutputComponents.Add(InstancedStaticMeshComponent); + + FHoudiniEngineUtils::KeepOrClearComponentTags(InstancedStaticMeshComponent, &InstancerPartData.GeoPartObject); + + InstancedStaticMeshComponent->SetStaticMesh(InstancedStaticMesh); + + if (InstancedStaticMeshComponent->GetBodyInstance()) + { + InstancedStaticMeshComponent->GetBodyInstance()->bAutoWeld = false; + } + + InstancedStaticMeshComponent->OverrideMaterials.Empty(); + if (InstancerMaterials.Num() > 0) + { + int32 MeshMaterialCount = InstancedStaticMesh->GetStaticMaterials().Num(); + for (int32 Idx = 0; Idx < MeshMaterialCount; ++Idx) + { + if (InstancerMaterials.IsValidIndex(Idx) && IsValid(InstancerMaterials[Idx])) + InstancedStaticMeshComponent->SetMaterial(Idx, InstancerMaterials[Idx]); + } + } + + // Set the transform of the Component relative to its parents. + InstancedStaticMeshComponent->SetRelativeTransform(Instancer.Settings.ComponentRelativeTransform); + + // Get the transforms of all instances. In Houdini the origin is relative to the HDA Component in Unreal. + TArray Transforms = UnpackTransforms(Instancer, InstancerPartData); + + // Offset all transforms relative to the component. + FTransform InvComponentTransform = Instancer.Settings.ComponentRelativeTransform.Inverse(); + for(FTransform & Transform : Transforms) + { + Transform = InvComponentTransform * Transform; + } + + InstancedStaticMeshComponent->AddInstances(Transforms, false); + + // Apply generic attributes if we have any. Just use attributes on the first point. + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes(InstancedStaticMeshComponent, InstancerPartData.AllPropertyAttributes, Instancer.AttributeIndices[0]); + return true; +} + +bool +FHoudiniInstanceTranslator::CreateInstancedActorInstancer( + FHoudiniOutputObject& Output, + const FHoudiniInstancer& Instancer, + UObject* InstanceObject, + const FHoudiniInstancerPartData& InstancerPartData, + USceneComponent* ParentComponent) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInstanceTranslator::CreateInstancedActorInstancer); + + if (!InstanceObject) + return false; + + if (!IsValid(ParentComponent)) + return false; + + // Get the level where we want to spawn the actors + ULevel* SpawnLevel = ParentComponent->GetOwner() ? ParentComponent->GetOwner()->GetLevel() : nullptr; + if (!SpawnLevel) + return {}; + + UObject* ComponentOuter = ParentComponent; + if (IsValid(ParentComponent->GetOwner())) + ComponentOuter = ParentComponent->GetOwner(); + + UHoudiniInstancedActorComponent* InstancedActorComponent = NewObject( + ComponentOuter, UHoudiniInstancedActorComponent::StaticClass(), NAME_None, RF_Transactional); + + Output.OutputComponents.Add(InstancedActorComponent); + + // Change the creation method so the component is listed in the details panels + FHoudiniEngineRuntimeUtils::AddOrSetAsInstanceComponent(InstancedActorComponent); + + FHoudiniEngineUtils::KeepOrClearComponentTags(InstancedActorComponent, &InstancerPartData.GeoPartObject); + + InstancedActorComponent->ClearAllInstances(); + InstancedActorComponent->SetInstancedObject(InstanceObject); + + // Set the number of needed instances + TArray Transforms = UnpackTransforms(Instancer, InstancerPartData); + InstancedActorComponent->SetNumberOfInstances(Transforms.Num()); + + AActor* ReferenceActor = nullptr; + for (int32 Idx = 0; Idx < Transforms.Num(); Idx++) + { + // if we already have an actor, we can reuse it + const FTransform& CurTransform = Transforms[Idx]; + + // Get the current instance + // If null, we need to create a new one, else we can reuse the actor + // TODO: ?? we cant reuse previous actors since we clear everything above? + AActor* CurInstance = InstancedActorComponent->GetInstancedActorAt(Idx); + if (!IsValid(CurInstance)) + { + CurInstance = SpawnInstanceActor(CurTransform, SpawnLevel, InstancedActorComponent, ReferenceActor); + InstancedActorComponent->SetInstanceAt(Idx, CurTransform, CurInstance); + } + else + { + // We can simply update the actor's transform + InstancedActorComponent->SetInstanceTransformAt(Idx, CurTransform); + } + + if (!ReferenceActor) + ReferenceActor = CurInstance; + + FHoudiniEngineUtils::KeepOrClearActorTags(CurInstance, true, true, &InstancerPartData.GeoPartObject); + + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes(CurInstance, + InstancerPartData.AllPropertyAttributes, Instancer.AttributeIndices[Idx]); + } + + // Update generic properties for the component managing the instances + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes(InstancedActorComponent, InstancerPartData.AllPropertyAttributes, Instancer.AttributeIndices[0]); + + // Make sure Post edit change is called on all generated actors + TArray NewActors = InstancedActorComponent->GetInstancedActors(); + for (auto& CurActor : NewActors) + { + if (CurActor) + CurActor->PostEditChange(); + } + + return true; +} + +bool +FHoudiniInstanceTranslator::CreateStaticMeshInstancer( + FHoudiniOutputObject& Output, + const FHoudiniInstancer& Instancers, + UObject* InstanceObject, + const FHoudiniInstancerPartData& InstancerPartData, + USceneComponent* ParentComponent, + const TArray& InstancerMaterials) +{ + UStaticMesh* InstancedStaticMesh = Cast(InstanceObject); + + if (!InstancedStaticMesh) + return false; + + if (!IsValid(ParentComponent)) + return false; + + UObject* ComponentOuter = ParentComponent; + if (IsValid(ParentComponent->GetOwner())) + ComponentOuter = ParentComponent->GetOwner(); + + UStaticMeshComponent* SMC = NewObject(ComponentOuter, UStaticMeshComponent::StaticClass(), NAME_None, RF_Transactional); + + Output.OutputComponents.Add(SMC); + + // Change the creation method so the component is listed in the details panels + FHoudiniEngineRuntimeUtils::AddOrSetAsInstanceComponent(SMC); + + if (!SMC) + return {}; + + SMC->SetStaticMesh(InstancedStaticMesh); + SMC->GetBodyInstance()->bAutoWeld = false; + + FHoudiniEngineUtils::KeepOrClearComponentTags(SMC, &InstancerPartData.GeoPartObject); + + SMC->OverrideMaterials.Empty(); + if (InstancerMaterials.Num() > 0) + { + int32 MeshMaterialCount = InstancedStaticMesh->GetStaticMaterials().Num(); + for (int32 Idx = 0; Idx < MeshMaterialCount; ++Idx) + { + if (InstancerMaterials.IsValidIndex(Idx) && IsValid(InstancerMaterials[Idx])) + SMC->SetMaterial(Idx, InstancerMaterials[Idx]); + } + } + + TArray Transforms = UnpackTransforms(Instancers, InstancerPartData); + + // Now add the instances Transform + if (Transforms.Num() > 0) + { + SMC->SetRelativeTransform(Transforms[0]); + } + + SetGenericPropertyAttributes(SMC, Instancers, InstancerPartData); + + FHoudiniMeshTranslator::SetCustomPrimitiveData(Instancers.CustomPrimData, SMC); + + return true; +} + +bool +FHoudiniInstanceTranslator::CreateHoudiniStaticMeshInstancer( + FHoudiniOutputObject& Output, + const FHoudiniInstancer& Instancers, + UObject* InstanceObject, + const FHoudiniInstancerPartData& InstancerPartData, + USceneComponent* ParentComponent, + const TArray& InstancerMaterials) +{ + + UHoudiniStaticMesh * InstancedProxyStaticMesh = Cast(InstanceObject); + + if (!InstancedProxyStaticMesh) + return false; + + if (!IsValid(ParentComponent)) + return false; + + UObject* ComponentOuter = ParentComponent; + if (IsValid(ParentComponent->GetOwner())) + ComponentOuter = ParentComponent->GetOwner(); + + UHoudiniStaticMeshComponent* HSMC = NewObject(ComponentOuter, UHoudiniStaticMeshComponent::StaticClass(), NAME_None, RF_Transactional); + Output.ProxyComponent = HSMC; + Output.OutputComponents.Add(HSMC); + + // Change the creation method so the component is listed in the details panels + FHoudiniEngineRuntimeUtils::AddOrSetAsInstanceComponent(HSMC); + + if (!HSMC) + return {}; + + HSMC->SetMesh(InstancedProxyStaticMesh); + + FHoudiniEngineUtils::KeepOrClearComponentTags(HSMC, &InstancerPartData.GeoPartObject); + + HSMC->OverrideMaterials.Empty(); + if (InstancerMaterials.Num() > 0) + { + int32 MeshMaterialCount = InstancedProxyStaticMesh->GetStaticMaterials().Num(); + for (int32 Idx = 0; Idx < MeshMaterialCount; ++Idx) + { + if (InstancerMaterials.IsValidIndex(Idx) && IsValid(InstancerMaterials[Idx])) + HSMC->SetMaterial(Idx, InstancerMaterials[Idx]); + } + } + + // Now add the instances Transform + TArray Transforms = UnpackTransforms(Instancers, InstancerPartData); + HSMC->SetRelativeTransform(Transforms[0]); + + // Apply generic attributes if we have any + SetGenericPropertyAttributes(HSMC, Instancers, InstancerPartData); + return true; +} + + +bool +FHoudiniInstanceTranslator::CreateFoliageInstancer( + const FHoudiniOutputObjectIdentifier& Id, + FHoudiniOutputObject& Output, + const FHoudiniInstancer& Instancers, + UObject* InstanceObject, + const FHoudiniInstancerPartData& InstancerPartData, + const FHoudiniPackageParams& InPackageParams, + USceneComponent* ParentComponent, + const TArray& InstancerMaterials) +{ + UStaticMesh* StaticMesh = Cast(InstanceObject); + UFoliageType* FoliageType = Cast(InstanceObject); + + HOUDINI_CHECK_RETURN(IsValid(StaticMesh) || IsValid(FoliageType), {}); + HOUDINI_CHECK_RETURN(IsValid(ParentComponent), {}); + + AActor* OwnerActor = ParentComponent->GetOwner(); + HOUDINI_CHECK_RETURN(IsValid(OwnerActor), {}); + + // We want to spawn the foliage in the same level as the parent HDA + // as spawning in the current level may cause reference issue later on. + ULevel* DesiredLevel = OwnerActor->GetLevel(); + HOUDINI_CHECK_RETURN(IsValid(DesiredLevel), {}); + + Output.World = DesiredLevel->GetWorld(); + HOUDINI_CHECK_RETURN(IsValid(Output.World), {}); + + // Previously, (pre 2023) we used to try to find existing foliage types in the current world, but this is dangerous + // because it can trash the users data if they non-HDA foliage. This can get fairly confusing if there are two HDA + // in the same level, and doesn't make it clear what is baked where. So always create a custom foliage type. + + FHoudiniPackageParams FoliageTypePackageParams = InPackageParams; + + if (FoliageType) + { + Output.FoliageType = FHoudiniFoliageTools::DuplicateFoliageType(FoliageTypePackageParams, Id, FoliageType); + Output.UserFoliageType = FoliageType; + } + else + { + Output.FoliageType = FHoudiniFoliageTools::CreateFoliageType(FoliageTypePackageParams, Id, StaticMesh); + Output.UserFoliageType = nullptr; + } + + // Set material overrides on the cooked foliage type + if (InstancerMaterials.Num() > 0) + { + UFoliageType_InstancedStaticMesh* const CookedMeshFoliageType = Cast(Output.FoliageType); + if (IsValid(CookedMeshFoliageType)) + { + UStaticMesh const* const FoliageMesh = CookedMeshFoliageType->GetStaticMesh(); + const int32 MeshMaterialSlotCount = IsValid(FoliageMesh) ? FoliageMesh->GetStaticMaterials().Num() : 0; + const int32 MaterialOverrideSlotCount = FMath::Min(InstancerMaterials.Num(), MeshMaterialSlotCount); + for (int32 Idx = 0; Idx < MaterialOverrideSlotCount; ++Idx) + { + if (IsValid(InstancerMaterials[Idx])) + { + if (!CookedMeshFoliageType->OverrideMaterials.IsValidIndex(Idx)) + CookedMeshFoliageType->OverrideMaterials.SetNum(Idx + 1); + CookedMeshFoliageType->OverrideMaterials[Idx] = InstancerMaterials[Idx]; + } + else if (CookedMeshFoliageType->OverrideMaterials.IsValidIndex(Idx) && CookedMeshFoliageType->OverrideMaterials[Idx]) + { + CookedMeshFoliageType->OverrideMaterials[Idx] = nullptr; + } + } + } + } + + FTransform HoudiniAssetTransform = ParentComponent->GetComponentTransform(); + + TArray Transforms = UnpackTransforms(Instancers, InstancerPartData); + + TArray FoliageInstances; + FoliageInstances.SetNum(Transforms.Num()); + + //for (auto CurrentTransform : InstancedObjectTransforms) + for(int32 n = 0; n < Transforms.Num(); n++) + { + // Instances transforms are relative to the HDA, + // But we need world transform for the Foliage Types + FTransform CurrentTransform = Transforms[n] * HoudiniAssetTransform; + + FoliageInstances[n].Location = CurrentTransform.GetLocation(); + FoliageInstances[n].Rotation = CurrentTransform.GetRotation().Rotator(); + FoliageInstances[n].DrawScale3D = (FVector3f)CurrentTransform.GetScale3D(); + } + + TArray AttachmentTypes = + FHoudiniFoliageTools::GetAttachmentInfo(InstancerPartData.GeoPartObject.GeoId, InstancerPartData.GeoPartObject.PartId, FoliageInstances.Num()); + + FHoudiniFoliageTools::SpawnFoliageInstances(Output.World, Output.FoliageType, FoliageInstances, AttachmentTypes); + + // Clear the returned component. This should be set, but doesn't make in world partition. + // In future, this should be an array of components. + + TArray FoliageInfos = FHoudiniFoliageTools::GetAllFoliageInfo(DesiredLevel->GetWorld(), Output.FoliageType); + for(FFoliageInfo * FoliageInfo : FoliageInfos) + { + + UHierarchicalInstancedStaticMeshComponent* FoliageHISMC = FoliageInfo->GetComponent(); + if (IsValid(FoliageHISMC)) + { + // TODO: This was due to a bug in UE4.22-20, check if still needed! + FoliageHISMC->BuildTreeIfOutdated(true, true); + + FHoudiniEngineUtils::KeepOrClearComponentTags(FoliageHISMC, &InstancerPartData.GeoPartObject); + + Output.OutputComponents.Add(FoliageHISMC); + + SetGenericPropertyAttributes(FoliageHISMC, Instancers, InstancerPartData); + } + } + + SetGenericPropertyAttributes(StaticMesh, Instancers, InstancerPartData); + SetGenericPropertyAttributes(Output.FoliageType, Instancers, InstancerPartData); + + return true; +} + +bool +FHoudiniInstanceTranslator::CreateLevelInstanceInstancer( + FHoudiniOutputObject& Output, + const FHoudiniInstancer& Instancers, + UObject* InstanceObject, + const FHoudiniInstancerPartData& InstancerPartData, + USceneComponent* ParentComponent, + const TArray& InstancerMaterials) +{ + UWorld* LevelInstanceWorld = Cast(InstanceObject); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UWorld* SpawnWorld = ParentComponent->GetWorld(); + + TArray Transforms = UnpackTransforms(Instancers, InstancerPartData); + + for(int Index = 0; Index < Transforms.Num(); Index++) + { + FTransform HoudiniAssetTransform = ParentComponent->GetComponentTransform(); + FTransform CurrentTransform = Transforms[Index] * HoudiniAssetTransform; + FString Name = FString::Printf(TEXT("%s_%d_%d_%d_%d"), + *InstancerPartData.GeoPartObject.ObjectName, + InstancerPartData.GeoPartObject.ObjectId, + InstancerPartData.GeoPartObject.GeoId, + InstancerPartData.GeoPartObject.PartId, + Index); + + FActorSpawnParameters SpawnInfo; + SpawnInfo.Name = FName(Name); + SpawnInfo.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested; + SpawnInfo.Owner = ParentComponent->GetOwner(); + ALevelInstance* LevelInstance = Cast(SpawnWorld->SpawnActor(ALevelInstance::StaticClass(), &CurrentTransform, SpawnInfo)); + LevelInstance->bDefaultOutlinerExpansionState = false; + LevelInstance->SetWorldAsset(LevelInstanceWorld); + LevelInstance->LoadLevelInstance(); + LevelInstance->SetActorLabel(Name); + LevelInstance->AttachToActor(ParentComponent->GetOwner(), FAttachmentTransformRules::KeepWorldTransform); + + SetGenericPropertyAttributes(LevelInstance, Instancers, InstancerPartData); + + Output.OutputActors.Add(LevelInstance); + } + + return true; +#else + return false; +#endif +} + +bool +FHoudiniInstanceTranslator::HapiGetInstanceTransforms( + const FHoudiniGeoPartObject& InHGPO, + TArray& OutInstancerUnrealTransforms) +{ + // Get the instance transforms + int32 PointCount = InHGPO.PartInfo.PointCount; + if (PointCount <= 0) + return false; + + TArray InstanceTransforms; + InstanceTransforms.SetNum(PointCount); + for (int32 Idx = 0; Idx < InstanceTransforms.Num(); Idx++) + FHoudiniApi::Transform_Init(&(InstanceTransforms[Idx])); + + auto Result = FHoudiniApi::GetInstanceTransformsOnPart(FHoudiniEngine::Get().GetSession(), + InHGPO.GeoId, InHGPO.PartId, HAPI_SRT, &InstanceTransforms[0], 0, PointCount); + + if(Result == HAPI_RESULT_SUCCESS) + { + // Convert the transform to Unreal's coordinate system + OutInstancerUnrealTransforms.SetNumZeroed(InstanceTransforms.Num()); + for(int32 InstanceIdx = 0; InstanceIdx < InstanceTransforms.Num(); InstanceIdx++) + { + const auto& InstanceTransform = InstanceTransforms[InstanceIdx]; + FHoudiniEngineUtils::TranslateHapiTransform(InstanceTransform, OutInstancerUnrealTransforms[InstanceIdx]); + } + } + else + { + HOUDINI_LOG_ERROR(TEXT("Failed to fetch instance transforms.")); + OutInstancerUnrealTransforms.SetNum(InstanceTransforms.Num()); + for(int32 InstanceIdx = 0; InstanceIdx < InstanceTransforms.Num(); InstanceIdx++) + { + OutInstancerUnrealTransforms[InstanceIdx] = FTransform::Identity; + } + + return false; + } + + + + return true; +} + +bool +FHoudiniInstanceTranslator::GetGenericPropertiesAttributes( + int32 InGeoNodeId, + int32 InPartId, + TArray& OutPropertyAttributes) +{ + // List all the generic property detail attributes ... + int32 FoundCount = FHoudiniEngineUtils::GetGenericAttributeList( + (HAPI_NodeId)InGeoNodeId, (HAPI_PartId)InPartId, HAPI_UNREAL_ATTRIB_GENERIC_UPROP_PREFIX, OutPropertyAttributes, HAPI_ATTROWNER_DETAIL); + + // .. then get all the values for the primitive property attributes + FoundCount += FHoudiniEngineUtils::GetGenericAttributeList( + (HAPI_NodeId)InGeoNodeId, (HAPI_PartId)InPartId, HAPI_UNREAL_ATTRIB_GENERIC_UPROP_PREFIX, OutPropertyAttributes, HAPI_ATTROWNER_PRIM, -1); + + // .. then finally, all values for point uproperty attributes + // TODO: !! get the correct Index here? + FoundCount += FHoudiniEngineUtils::GetGenericAttributeList( + (HAPI_NodeId)InGeoNodeId, (HAPI_PartId)InPartId, HAPI_UNREAL_ATTRIB_GENERIC_UPROP_PREFIX, OutPropertyAttributes, HAPI_ATTROWNER_POINT, -1); + + return FoundCount > 0; +} + +bool +FHoudiniInstanceTranslator::GetMaterialOverridesFromAttributes( + int32 InGeoNodeId, + int32 InPartId, + int32 InAttributeIndex, + EHoudiniInstancerType InInstancerType, + TArray& OutMaterialAttributes) +{ + const HAPI_AttributeOwner AttribOwner = InInstancerType == EHoudiniInstancerType::AttributeInstancer ? HAPI_ATTROWNER_POINT : HAPI_ATTROWNER_PRIM; + + // Get the part info + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), InGeoNodeId, InPartId, &PartInfo), false); + + // Get all the part's attribute names + int32 NumAttribs = PartInfo.attributeCounts[AttribOwner]; + TArray AttribNameHandles; + AttribNameHandles.SetNum(NumAttribs); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAttributeNames(FHoudiniEngine::Get().GetSession(), InGeoNodeId, InPartId, AttribOwner, AttribNameHandles.GetData(), NumAttribs), false); + + // Extract the attribute names' strings + TArray AllAttribNames; + FHoudiniEngineString::SHArrayToFStringArray(AttribNameHandles, AllAttribNames); + + // Remove all unneeded attributes, only keep valid materials attr + for (int32 Idx = AllAttribNames.Num() - 1; Idx >= 0; Idx--) + { + if (AllAttribNames[Idx].StartsWith(HAPI_UNREAL_ATTRIB_MATERIAL_INSTANCE)) + continue; + + if(AllAttribNames[Idx].StartsWith(HAPI_UNREAL_ATTRIB_MATERIAL)) + continue; + + if (AllAttribNames[Idx].StartsWith(HAPI_UNREAL_ATTRIB_MATERIAL_FALLBACK)) + continue; + + // Not a valid mat, remove from the array + AllAttribNames.RemoveAt(Idx); + } + + // Now look for different material attributes in the found attributes + bool bFoundMaterialAttributes = false; + // TODO: We need to turn this to an array in order to support a mix of material AND material instances + //OutMaterialOverrideNeedsCreateInstance = false; + + if (AllAttribNames.IsEmpty()) + return false; + + TArray MaterialInstanceAttributes; + TArray MaterialAttributes; + + // Get material instances overrides attributes + if (GetMaterialOverridesFromAttributes(InGeoNodeId, InPartId, InAttributeIndex, HAPI_UNREAL_ATTRIB_MATERIAL_INSTANCE, AllAttribNames, MaterialInstanceAttributes)) + bFoundMaterialAttributes = true; + + // Get the "main" material override attributes + if(GetMaterialOverridesFromAttributes(InGeoNodeId, InPartId, InAttributeIndex, HAPI_UNREAL_ATTRIB_MATERIAL, AllAttribNames, MaterialAttributes)) + bFoundMaterialAttributes = true; + + // If we haven't found anything, try the fallback attribute + if (!bFoundMaterialAttributes && GetMaterialOverridesFromAttributes(InGeoNodeId, InPartId, InAttributeIndex, HAPI_UNREAL_ATTRIB_MATERIAL_FALLBACK, AllAttribNames, MaterialAttributes)) + bFoundMaterialAttributes = true; + + // We couldnt find any mat attribute? early return + if (!bFoundMaterialAttributes) + { + OutMaterialAttributes.Empty(); + return false; + } + + // Fetch material instance parameters (detail + AttribOwner) specified via attributes + TArray AllMatParams; + FHoudiniMaterialTranslator::GetMaterialParameterAttributes(InGeoNodeId, InPartId, AttribOwner, AllMatParams, InAttributeIndex); + + // Consolidate the final material (or material instance) selection into OutMaterialAttributes + // Use unreal_material if non-empty. If empty, fallback to unreal_material_instance. + const int32 MaxNumSlots = FMath::Max(MaterialInstanceAttributes.Num(), MaterialAttributes.Num()); + OutMaterialAttributes.Reset(MaxNumSlots); + for (int32 MatIdx = 0; MatIdx < MaxNumSlots; ++MatIdx) + { + FHoudiniMaterialInfo& MatInfo = OutMaterialAttributes.AddDefaulted_GetRef(); + MatInfo.MaterialIndex = MatIdx; + // unreal_material takes precedence. If it is missing / empty, check unreal_material_instance + if (MaterialAttributes.IsValidIndex(MatIdx) && !MaterialAttributes[MatIdx].IsEmpty()) + { + MatInfo.MaterialObjectPath = MaterialAttributes[MatIdx]; + } + else if (MaterialInstanceAttributes.IsValidIndex(MatIdx) && !MaterialInstanceAttributes[MatIdx].IsEmpty()) + { + MatInfo.MaterialObjectPath = MaterialInstanceAttributes[MatIdx]; + MatInfo.bMakeMaterialInstance = true; + // Get any material parameters for the instance, specified via attributes. + // We use 0 for the index because we only loaded a specific index's attribute values into AllMatParams for + // AttribOwner. So the underlying FHoudiniGenericAttribute only contains one entry per attribute. + FHoudiniMaterialTranslator::GetMaterialParameters(MatInfo, AllMatParams, 0); + } + } + + return true; +} + +bool +FHoudiniInstanceTranslator::GetMaterialOverridesFromAttributes( + int32 InGeoNodeId, + int32 InPartId, + int32 InAttributeIndex, + const FString& InAttributeName, + const TArray& InAllAttribNames, + TArray& OutMaterialAttributes) +{ + // See if the "materialX" attributes were added as zero-based or not by searching for "material0" + // If they are not (so the attributes starts at unreal_material1). then we'll need to decrement the idx + FString MatZero = InAttributeName; + MatZero += "0"; + bool bZeroBased = false; + if (InAllAttribNames.Contains(MatZero)) + bZeroBased = true; + + bool bFoundMatAttributes = false; + int32 PrefixLength = InAttributeName.Len(); + TArray MatName; + for (const FString& AttribName : InAllAttribNames) + { + if (!AttribName.StartsWith(InAttributeName)) + { + continue; + } + + FString Fragment = AttribName.Mid(PrefixLength); + + int32 OverrideIdx = -1; + if (Fragment == "") + { + // The attribute is exactly "unreal_material", use it as the default mat (index 0) + OverrideIdx = 0; + } + else if (!Fragment.IsNumeric()) + { + continue; + } + else + { + OverrideIdx = FCString::Atoi(*Fragment); + if (!bZeroBased) + { + OverrideIdx--; + } + } + + if (OverrideIdx < 0) + { + continue; + } + + // Increase the size of the array with empty materials + while (OutMaterialAttributes.Num() <= OverrideIdx) + { + OutMaterialAttributes.Add(""); + } + + FHoudiniHapiAccessor Accessor(InGeoNodeId, InPartId, TCHAR_TO_ANSI(*AttribName)); + bool Res = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, MatName, InAttributeIndex, 1); + + if (!Res) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniInstanceTranslator::GetMaterialOverridesFromAttributes]: Failed to get material override index %d."), OverrideIdx); + continue; + } + + OutMaterialAttributes[OverrideIdx] = MatName[0]; + bFoundMatAttributes = true; + } + + return bFoundMatAttributes; +} + +bool +FHoudiniInstanceTranslator::GetInstancerMaterials( + const TArray& MaterialAttributes, TArray& OutInstancerMaterials) +{ + // Use a map to avoid attempting to load the object for each instance + TMap MaterialMap; + + // Non-instanced materials check material attributes one by one + const int32 NumSlots = MaterialAttributes.Num(); + OutInstancerMaterials.SetNumZeroed(NumSlots); + for (int32 MatIdx = 0; MatIdx < NumSlots; ++MatIdx) + { + const FHoudiniMaterialInfo& CurrentMatInfo = MaterialAttributes[MatIdx]; + // Only process cases where we are not making material instances + if (CurrentMatInfo.bMakeMaterialInstance) + continue; + + const FHoudiniMaterialIdentifier MaterialIdentifier = CurrentMatInfo.MakeIdentifier(); + + UMaterialInterface* CurrentMaterialInterface = nullptr; + UMaterialInterface** FoundMaterial = MaterialMap.Find(MaterialIdentifier); + if (!FoundMaterial) + { + // See if we can find a material interface that matches the attribute + CurrentMaterialInterface = Cast( + StaticLoadObject(UMaterialInterface::StaticClass(), nullptr, *CurrentMatInfo.MaterialObjectPath, nullptr, LOAD_NoWarn, nullptr)); + + // Check validity + if (!IsValid(CurrentMaterialInterface)) + CurrentMaterialInterface = nullptr; + + // Add what we found to the material map to avoid unnecessary loads + MaterialMap.Add(MaterialIdentifier, CurrentMaterialInterface); + } + else + { + // Reuse what we previously found + CurrentMaterialInterface = *FoundMaterial; + } + + OutInstancerMaterials[MatIdx] = CurrentMaterialInterface; + } + + return true; +} + +bool +FHoudiniInstanceTranslator::GetInstancerMaterialInstances( + const TArray& MaterialAttribute, + const FHoudiniGeoPartObject& InHGPO, + const FHoudiniPackageParams& InPackageParams, + TArray& OutInstancerMaterials) +{ + TMap MaterialInstanceOverrides; + TArray MaterialIdentifiers; + for (const FHoudiniMaterialInfo& MatInfo : MaterialAttribute) + { + if (!MatInfo.bMakeMaterialInstance) + { + MaterialIdentifiers.Add(FHoudiniMaterialIdentifier()); + continue; + } + const FHoudiniMaterialIdentifier MaterialIdentifier = MatInfo.MakeIdentifier(); + MaterialIdentifiers.Add(MaterialIdentifier); + MaterialInstanceOverrides.Add(MaterialIdentifier, MatInfo); + } + + // We have no material instances to create + if (MaterialInstanceOverrides.Num() <= 0) + return true; + + TArray MaterialAndTexturePackages; + TMap> InputAssignmentMaterials; + TMap> OutputAssignmentMaterials; + static constexpr bool bForceRecookAll = false; + bool bSuccess = false; + if (FHoudiniMaterialTranslator::CreateMaterialInstances( + InHGPO, + InPackageParams, + MaterialInstanceOverrides, + MaterialAndTexturePackages, + InputAssignmentMaterials, + OutputAssignmentMaterials, + bForceRecookAll)) + { + bSuccess = true; + // Make sure that the OutInstancerMaterials array is the correct size + OutInstancerMaterials.SetNumZeroed(MaterialAttribute.Num()); + const int32 NumSlots = MaterialIdentifiers.Num(); + for (int32 SlotIdx = 0; SlotIdx < NumSlots; ++SlotIdx) + { + const FHoudiniMaterialIdentifier& MaterialIdentifier = MaterialIdentifiers[SlotIdx]; + // skip the invalid ids (non material instance) + if (!MaterialIdentifier.IsValid()) + continue; + TObjectPtr* Material = OutputAssignmentMaterials.Find(MaterialIdentifier); + if (!Material || !IsValid(*Material)) + { + OutInstancerMaterials[SlotIdx] = nullptr; + bSuccess = false; + } + else + { + OutInstancerMaterials[SlotIdx] = *Material; + } + } + } + + return bSuccess; +} + +TArray +FHoudiniInstanceTranslator::GetAllInstancerMaterials( + int32 InAttributeIndex, + const FHoudiniGeoPartObject& InHGPO, + const FHoudiniPackageParams& InPackageParams) +{ + TArray InstancerMaterials; + + // Get all the material attributes for that variation + TArray MaterialAttributes; + GetMaterialOverridesFromAttributes(InHGPO.GeoId, InHGPO.PartId, InAttributeIndex, InHGPO.InstancerType, MaterialAttributes); + + // Get the materials (for which we don't create material instances) + // OutInstancerMaterials is grown to the same length as MaterialAttributes (# slots). Sets materials in + // corresponding slots. + InstancerMaterials.SetNumZeroed(MaterialAttributes.Num()); + bool bSuccess = GetInstancerMaterials(MaterialAttributes, InstancerMaterials); + + // Get/create the material instances (if any were specified, see FHoudiniMaterialInfo.bMakeMaterialInstace + // OutInstancerMaterials is grown to the same length as MaterialAttributes (# slots). Sets material instances + // in corresponding slots. + bSuccess &= GetInstancerMaterialInstances(MaterialAttributes, InHGPO, InPackageParams, InstancerMaterials); + + return InstancerMaterials; +} + +AActor* +FHoudiniInstanceTranslator::SpawnInstanceActor( + const FTransform& InTransform, + ULevel* InSpawnLevel, + UHoudiniInstancedActorComponent* InIAC, + AActor* InReferenceActor, + const FName Name) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniInstanceTranslator::SpawnInstanceActor); + + if (!IsValid(InIAC)) + return nullptr; + + UObject* InstancedObject = InIAC->GetInstancedObject(); + if (!IsValid(InstancedObject)) + return nullptr; + + AActor* NewActor = nullptr; + + UWorld* SpawnWorld = InSpawnLevel->GetWorld(); + UClass* InstancedActorClass = InIAC->GetInstancedActorClass(); + if (InstancedActorClass == nullptr || SpawnWorld == nullptr) + { +#if WITH_EDITOR + // Try to spawn a new actor for the given transform + GEditor->ClickLocation = InTransform.GetTranslation(); + GEditor->ClickPlane = FPlane(GEditor->ClickLocation, FVector::UpVector); + + // Using this function lets unreal find the appropriate actor class for us + // We only use it for the first instanced actors just to get the best actor class for that object + // Once we have that class - it is much faster (~25x) to just use SpawnActor instead + TArray NewActors = FLevelEditorViewportClient::TryPlacingActorFromObject(InSpawnLevel, InstancedObject, false, RF_Transactional, nullptr, Name); + if (NewActors.Num() > 0) + { + if (IsValid(NewActors[0])) + { + NewActor = NewActors[0]; + } + } + + // Set the instanced actor class on the IAC so we can reuse it + if(NewActor) + InIAC->SetInstancedActorClass(NewActor->GetClass()); +#endif + } + else + { + FActorSpawnParameters SpawnParams; + SpawnParams.ObjectFlags = RF_Transactional; + //SpawnParams.Owner = ComponentOuter; + SpawnParams.OverrideLevel = InSpawnLevel; + SpawnParams.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested; + SpawnParams.Template = nullptr; + SpawnParams.bNoFail = true; + // We need to use the previously instantiated actor as template when instantiating a decal material. + SpawnParams.Template = InReferenceActor; + + NewActor = SpawnWorld->SpawnActor(InstancedActorClass, &InTransform, SpawnParams); + } + + // Make sure that the actor was spawned in the proper level + FHoudiniEngineUtils::MoveActorToLevel(NewActor, InSpawnLevel); + + return NewActor; +} + + +FString +FHoudiniInstanceTranslator::GetInstancerTypeFromComponent(UObject* InObject) +{ + USceneComponent* InComponent = Cast(InObject); + + FString InstancerType = TEXT("Instancer"); + if (IsValid(InComponent)) + { + if (InComponent->IsA()) + { + InstancerType = TEXT("(Actor Instancer)"); + } + else if (InComponent->IsA()) + { + if (InComponent->GetOwner() && InComponent->GetOwner()->IsA()) + InstancerType = TEXT("(Foliage Instancer)"); + else + InstancerType = TEXT("(Hierarchical Instancer)"); + } + else if (InComponent->IsA()) + { + InstancerType = TEXT("(Mesh Instancer)"); + } + else if (InComponent->IsA()) + { + InstancerType = TEXT("(Static Mesh Component)"); + } + } + + return InstancerType; +} + +bool +FHoudiniInstanceTranslator::IsHISM(HAPI_NodeId GeoId, HAPI_NodeId PartId, HAPI_AttributeOwner Owner, int Index) +{ + TArray IntData; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_HIERARCHICAL_INSTANCED_SM); + Accessor.GetAttributeData(Owner, 1, IntData, Index, 1); + if (!IntData.IsEmpty()) + return IntData[0] != 0; + + return false; +} + +bool +FHoudiniInstanceTranslator::IsForceInstancer(HAPI_NodeId GeoId, HAPI_NodeId PartId, HAPI_AttributeOwner Owner, int Index) +{ + TArray IntData; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_FORCE_INSTANCER); + Accessor.GetAttributeData(Owner, 1, IntData, Index, 1); + if (!IntData.IsEmpty()) + return IntData[0] != 0; + + return false; +} + +void +FHoudiniInstanceTranslator::GetPerInstanceCustomData( + int32 InGeoNodeId, + int32 InPartId, + FHoudiniInstancerPartData& OutInstancedOutputPartData) +{ + TArray CustomFloatsArray; + FHoudiniHapiAccessor Accessor(InGeoNodeId, InPartId, HAPI_UNREAL_ATTRIB_INSTANCE_NUM_CUSTOM_FLOATS); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, CustomFloatsArray); + if (CustomFloatsArray.IsEmpty()) + return; + + int MaxCustomFloats = 0; + for( auto & Instancer : OutInstancedOutputPartData.Instancers) + { + int NumInstances = Instancer.AttributeIndices.Num(); + Instancer.NumCustomFloats = CustomFloatsArray[Instancer.AttributeIndices[0]]; + Instancer.CustomFloats.SetNum(NumInstances * Instancer.NumCustomFloats); + MaxCustomFloats = FMath::Max(MaxCustomFloats, Instancer.NumCustomFloats); + } + + for(int CustomFloatIndex = 0; CustomFloatIndex < MaxCustomFloats; CustomFloatIndex++) + { + TArray Values; + + FString CurrentAttr = TEXT(HAPI_UNREAL_ATTRIB_INSTANCE_CUSTOM_DATA_PREFIX) + FString::FromInt(CustomFloatIndex); + Accessor.Init(InGeoNodeId, InPartId, TCHAR_TO_ANSI(*CurrentAttr)); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, Values); + if (Values.IsEmpty()) + { + HOUDINI_LOG_ERROR(TEXT("Could found attribute "), *CurrentAttr); + return; + } + + for (auto& Instancer : OutInstancedOutputPartData.Instancers) + { + if (CustomFloatIndex < Instancer.NumCustomFloats) + { + for(int Index = 0; Index < Instancer.AttributeIndices.Num(); Index++) + { + int Offset = Index * Instancer.NumCustomFloats + CustomFloatIndex; + Instancer.CustomFloats[Offset] = Values[Instancer.AttributeIndices[Index]]; + } + } + } + } +} + + +void +FHoudiniInstanceTranslator::SetPerInstanceCustomData( + const FHoudiniInstancer& Instancers, + USceneComponent* InComponentToUpdate) +{ + if (Instancers.NumCustomFloats == 0) + return; + + UInstancedStaticMeshComponent* ISMC = Cast(InComponentToUpdate); + if (!IsValid(ISMC)) + return; + + ISMC->NumCustomDataFloats = Instancers.NumCustomFloats; + + ISMC->Modify(); + + // Clear out and reinit to 0 the PerInstanceCustomData array + ISMC->PerInstanceSMCustomData = Instancers.CustomFloats; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + // TODO:5.4 ?? fix me!! +#else + // Cant call the edit function above because the function is defined in a different cpp file than the .h it is declared in... + ISMC->InstanceUpdateCmdBuffer.NumEdits++; +#endif + + ISMC->MarkRenderStateDirty(); + + return; +} + +void +FHoudiniInstanceTranslator::SetGenericPropertyAttributes(UObject* Object, const FHoudiniInstancer& InstancerData, const FHoudiniInstancerPartData& InstancerPartData) +{ + if (!Object) + return; + + // Apply generic attributes if we have any + if (!InstancerPartData.AllPropertyAttributes.IsEmpty()) + { + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes( + Object, + InstancerPartData.AllPropertyAttributes, + InstancerData.AttributeIndices[0]); + } +} + +FHoudiniInstancerSettings +FHoudiniInstanceTranslator::GetDefaultInstancerSettings(const FHoudiniGeoPartObject& HGPO, HAPI_AttributeOwner Owner) +{ + FHoudiniInstancerSettings DefaultSettings; + if (HGPO.PartInfo.InstanceCount) + DefaultSettings.ComponentRelativeTransform = HGPO.TransformMatrix; + + FHoudiniInstancerSettings Result = GetInstancerSettings(HGPO, HAPI_ATTROWNER_DETAIL, 0, DefaultSettings); + return Result; +} + + +FHoudiniInstancerSettings +FHoudiniInstanceTranslator::GetInstancerSettings( + const FHoudiniGeoPartObject& HGPO, + HAPI_AttributeOwner AttributeOwner, + int AttrIndex, + const FHoudiniInstancerSettings& Defaults) +{ + FHoudiniInstancerSettings Result = Defaults; + FHoudiniHapiAccessor Accessor; + FHoudiniEngineIndexedStringMap AttributeValues; + + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_LEVEL_PATH); + Accessor.GetAttributeStrings(AttributeOwner, AttributeValues, AttrIndex, 1); + if (!AttributeValues.Strings.IsEmpty()) + Result.LevelPath = AttributeValues.Strings[0]; + + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_BAKE_ACTOR); + Accessor.GetAttributeStrings(AttributeOwner, AttributeValues, AttrIndex, 1); + if (!AttributeValues.Strings.IsEmpty()) + Result.BakeActorName = AttributeValues.Strings[0]; + + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_BAKE_ACTOR_CLASS); + Accessor.GetAttributeStrings(AttributeOwner, AttributeValues, AttrIndex, 1); + if (!AttributeValues.Strings.IsEmpty()) + Result.BakeActorClassName = AttributeValues.Strings[0]; + + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_BAKE_FOLDER); + Accessor.GetAttributeStrings(AttributeOwner, AttributeValues, AttrIndex, 1); + if (!AttributeValues.Strings.IsEmpty()) + Result.BakeFolder = AttributeValues.Strings[0]; + + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_BAKE_OUTLINER_FOLDER); + Accessor.GetAttributeStrings(AttributeOwner, AttributeValues, AttrIndex, 1); + if (!AttributeValues.Strings.IsEmpty()) + Result.BakeOutlinerFolder = AttributeValues.Strings[0]; + + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2); + Accessor.GetAttributeStrings(AttributeOwner, AttributeValues, AttrIndex, 1); + if (!AttributeValues.Strings.IsEmpty()) + Result.OutputName = AttributeValues.Strings[0]; + + int BoolValue = 0; + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_FOLIAGE_INSTANCER); + Accessor.GetAttributeData(AttributeOwner, &BoolValue, AttrIndex, 1); + Result.bIsFoliage = BoolValue != 0; + + BoolValue = 0; + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_HIERARCHICAL_INSTANCED_SM); + Accessor.GetAttributeData(AttributeOwner, &BoolValue, AttrIndex, 1); + Result.bForceHISM = BoolValue != 0; + + BoolValue = 0; + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_FORCE_INSTANCER); + Accessor.GetAttributeData(AttributeOwner, &BoolValue, AttrIndex, 1); + Result.bForceInstancer = BoolValue != 0; + + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATRTIB_INSTANCE_ORIGIN); + HAPI_AttributeInfo AttrInfo; + if (Accessor.GetInfo(AttrInfo, AttributeOwner) && AttrInfo.tupleSize == 3) + { + TArray Center; + Accessor.GetAttributeData(AttrInfo, Center, AttrIndex, 1); + FVector Location = FVector(Center[0], Center[2], Center[1]); + Result.ComponentRelativeTransform.SetIdentity(); + Result.ComponentRelativeTransform.SetLocation(Location * 100.0); + } + + Result.DataLayers = FHoudiniDataLayerUtils::GetDataLayers(HGPO.GeoId, HGPO.PartId, HAPI_GroupType::HAPI_GROUPTYPE_POINT, AttrIndex); + if (Result.DataLayers.IsEmpty()) + Result.DataLayers = FHoudiniDataLayerUtils::GetDataLayers(HGPO.GeoId, HGPO.PartId, HAPI_GroupType::HAPI_GROUPTYPE_PRIM, 0); + + Result.HLODLayers = FHoudiniHLODLayerUtils::GetHLODLayers(HGPO.GeoId, HGPO.PartId, HAPI_ATTROWNER_POINT, AttrIndex); + if (Result.HLODLayers.IsEmpty()) + Result.HLODLayers = FHoudiniHLODLayerUtils::GetHLODLayers(HGPO.GeoId, HGPO.PartId, HAPI_ATTROWNER_PRIM, 0); + if(Result.HLODLayers.IsEmpty()) + Result.HLODLayers = FHoudiniHLODLayerUtils::GetHLODLayers(HGPO.GeoId, HGPO.PartId, HAPI_ATTROWNER_DETAIL, 0); + + return Result; +} + +TArray FHoudiniInstanceTranslator::UnpackTransforms(const FHoudiniInstancer& InstanceData, const FHoudiniInstancerPartData& PartData) +{ + // Use the attribute indices to create an array of transforms for this instancer. + TArray Results; + Results.SetNumUninitialized(InstanceData.AttributeIndices.Num()); + + for(int Index = 0; Index < InstanceData.AttributeIndices.Num(); Index++) + { + Results[Index] = PartData.InstanceTransforms[InstanceData.AttributeIndices[Index]]; + } + return Results; +} + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngine/Private/HoudiniInstanceTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "HoudiniOutput.h" +#include "CoreMinimal.h" +#include "HoudiniEngineString.h" +#include "UObject/ObjectMacros.h" +#include "HoudiniGenericAttribute.h" +#include "HoudiniMaterialTranslator.h" +#include "HoudiniInstanceTranslator.generated.h" + +struct FHoudiniInstancer; +class UStaticMesh; +class UFoliageType; +class UHoudiniStaticMesh; +class UHoudiniInstancedActorComponent; +struct FHoudiniPackageParams; + +enum InstancerComponentType +{ + Invalid = -1, + InstancedStaticMeshComponent = 0, + HierarchicalInstancedStaticMeshComponent = 1, + //MeshSplitInstancerComponent = 2, + HoudiniInstancedActorComponent = 3, + StaticMeshComponent = 4, + HoudiniStaticMeshComponent = 5, + Foliage = 6, + GeometryCollectionComponent = 7, + LevelInstance = 8 +}; + +USTRUCT() +struct HOUDINIENGINE_API FHoudiniInstancerSettings +{ +public: + + GENERATED_BODY() + + UPROPERTY() + FString LevelPath; + + UPROPERTY() + FString BakeActorName; + + UPROPERTY() + FString BakeActorClassName; + + UPROPERTY() + FString BakeOutlinerFolder; + + UPROPERTY() + FString BakeFolder; + + UPROPERTY() + FString OutputName; + + UPROPERTY() + bool bIsFoliage = false; + + UPROPERTY() + bool bForceHISM = false; + + UPROPERTY() + bool bForceInstancer = false; + + // Transform relative to the parent (HAC) Transform. + UPROPERTY() + FTransform ComponentRelativeTransform = FTransform::Identity; + + // Data Layers which should be applied (during Baking only). + UPROPERTY() + TArray DataLayers; + + // HLOD Layers which should be applied (during Baking only). + UPROPERTY() + TArray HLODLayers; + +}; + +USTRUCT() +struct HOUDINIENGINE_API FHoudiniInstancer +{ + // This structure holds data about a specific instancer. AttributeIndices references + // data in the FHoudiniInstancerPartData which is a part of (eg. transforms). + + GENERATED_BODY() + + UPROPERTY() + FString ObjectPath; + + UPROPERTY() + TArray AttributeIndices; + + UPROPERTY() + FString SplitName; + + UPROPERTY() + FHoudiniInstancerSettings Settings; + + UPROPERTY() + int NumCustomFloats = 0; + + UPROPERTY() + TArray CustomFloats; + + UPROPERTY() + TArray CustomPrimData; + + UPROPERTY() + bool bVisible = true; +}; + +USTRUCT() +struct HOUDINIENGINE_API FHoudiniInstancerPartData +{ +public: + + GENERATED_BODY() + + UPROPERTY() + FHoudiniGeoPartObject GeoPartObject; + + UPROPERTY() + TArray Instancers; + + UPROPERTY() + TArray InstanceTransforms; + + UPROPERTY() + FString SplitAttributeName; + + UPROPERTY() + TArray AllPropertyAttributes; + + // Array of material attributes + // If multiple slots are defined, we store all the different attributes values in a flat array + // Such that the size of MaterialAttributes is NumberOfAttributes * NumberOfMaterialSlots + UPROPERTY() + TArray MaterialAttributes; +}; + +struct HOUDINIENGINE_API FHoudiniInstanceTranslator +{ + public: + + static bool IsHISM(HAPI_NodeId GeoId, HAPI_NodeId PartId, HAPI_AttributeOwner Owner, int Index); + static FHoudiniInstancerPartData PopulateInstancedOutputPartData( + const FHoudiniGeoPartObject& InHGPO, + const TArray& InAllOutputs); + + static int CreateAllInstancersFromHoudiniOutputs( + const TArray& InAllOutputs, + UObject* InOuterComponent, + const FHoudiniPackageParams& InPackageParms, + const TMap* InPreBuiltInstancedOutputPartData = nullptr); + + static int CreateAllInstancersFromHoudiniOutputs( + const TArray& OutputsToUpdate, + const TArray& InAllOutputs, + UObject* InOuterComponent, + const FHoudiniPackageParams& InPackageParms, + const TMap* InPreBuiltInstancedOutputPartData = nullptr); + + static bool CreateAllInstancersFromHoudiniOutput( + UHoudiniOutput* InOutput, + const TArray& InAllOutputs, + UObject* InOuterComponent, + const FHoudiniPackageParams& InPackageParams, + const TMap* InPreBuiltInstancedOutputPartData = nullptr); + + static bool GetAttributeInstancerPartData(const FHoudiniGeoPartObject& HGPO, FHoudiniInstancerPartData& PartData); + + static bool GetPackedPrimitiveInstancerPartData( + const FHoudiniGeoPartObject& HGPO, + const TArray & InAllOutputs, + FHoudiniInstancerPartData& PartData); + + static bool FindInstancedOutputObject( + const FHoudiniGeoPartObject& HGPO, + const TArray& InAllOutputs, + FHoudiniOutputObjectIdentifier& OutId, + const UHoudiniOutput*& OutOutput); + + static void SetInstancerObject( + FHoudiniInstancer& InstancerData, + const FHoudiniGeoPartObject& HGPO, + const TArray& InAllOutputs); + + // Creates a new component or updates the previous one if possible + static bool CreateInstancer( + const FHoudiniOutputObjectIdentifier& Id, + FHoudiniOutputObject & Output, + UObject*& InstanceObject, + const FHoudiniInstancer& Instancer, + const FHoudiniInstancerPartData& InstancerPartData, + const FHoudiniPackageParams& InPackageParams, + USceneComponent* ParentComponent, + const TArray& InstancerMaterials); + + // Create or update an ISMC / HISMC + static bool CreateInstancedStaticMeshInstancer( + FHoudiniOutputObject& Output, + const FHoudiniInstancer& Instancer, + UObject* InstanceObject, + const FHoudiniInstancerPartData& InstancerPartData, + USceneComponent* ParentComponent, + const TArray& InstancerMaterials, + const InstancerComponentType& InstancerType); + + // Create or update an IAC + static bool CreateInstancedActorInstancer( + FHoudiniOutputObject& Output, + const FHoudiniInstancer& Instancer, + UObject* InstanceObject, + const FHoudiniInstancerPartData& InstancePartData, + USceneComponent* ParentComponent); + + // Create or update a StaticMeshComponent (when we have only one instance) + static bool CreateStaticMeshInstancer( + FHoudiniOutputObject& Output, + const FHoudiniInstancer& Instancer, + UObject* InstanceObject, + const FHoudiniInstancerPartData& InstancerPartData, + USceneComponent* ParentComponent, + const TArray& InstancerMaterials); + + // Create or update a HoudiniStaticMeshComponent (when we have only one instance) + static bool CreateHoudiniStaticMeshInstancer( + FHoudiniOutputObject& Output, + const FHoudiniInstancer& Instancer, + UObject* InstanceObject, + const FHoudiniInstancerPartData& InstancerPartData, + USceneComponent* ParentComponent, + const TArray& InstancerMaterials); + + // Create or update a Foliage instances + static bool CreateFoliageInstancer( + const FHoudiniOutputObjectIdentifier& Id, + FHoudiniOutputObject& Output, + const FHoudiniInstancer& Instancer, + UObject* InstanceObject, + const FHoudiniInstancerPartData& InstancerPartData, + const FHoudiniPackageParams& InPackageParams, + USceneComponent* ParentComponent, + const TArray& InstancerMaterials); + + // Create or update Level instances + static bool CreateLevelInstanceInstancer( + FHoudiniOutputObject& Output, + const FHoudiniInstancer& Instancer, + UObject* InstanceObject, + const FHoudiniInstancerPartData& InstancerPartData, + USceneComponent* ParentComponent, + const TArray& InstancerMaterials); + + // Utility function + // Fetches instance transforms and convert them to unreal coordinates + static bool HapiGetInstanceTransforms( + const FHoudiniGeoPartObject& InHGPO, + TArray& OutInstancerUnrealTransforms); + + // Helper function used to spawn a new Actor for UHoudiniInstancedActorComponent + // Relies on editor-only functionalities, so this function is not on the IAC itself + static AActor* SpawnInstanceActor( + const FTransform& InTransform, + ULevel* InSpawnLevel, + UHoudiniInstancedActorComponent* InIAC, + AActor* InReferenceActor, + FName Name = NAME_None); + + // Helper functions for generic property attributes + static bool GetGenericPropertiesAttributes( + int32 InGeoNodeId, + int32 InPartId, + TArray& OutPropertyAttributes); + + static bool GetMaterialOverridesFromAttributes( + int32 InGeoNodeId, + int32 InPartId, + int32 InAttributeIndex, + const FString& InAttributeName, + const TArray& InAllAttribNames, + TArray& OutMaterialAttributes); + + static bool GetMaterialOverridesFromAttributes( + int32 InGeoNodeId, + int32 InPartId, + int32 InAttributeIndex, + EHoudiniInstancerType InInstancerType, + TArray& OutMaterialAttributes); + + static bool GetInstancerMaterials( + const TArray& MaterialAttribute, + TArray& OutInstancerMaterials); + + static bool GetInstancerMaterialInstances( + const TArray& MaterialAttribute, + const FHoudiniGeoPartObject& InHGPO, const FHoudiniPackageParams& InPackageParams, + TArray& OutInstancerMaterials); + + static TArray GetAllInstancerMaterials( + int32 InAttributeIndex, + const FHoudiniGeoPartObject& InHGPO, + const FHoudiniPackageParams& InPackageParams); + + static FString GetInstancerTypeFromComponent(UObject* InComponent); + + static bool IsForceInstancer(HAPI_NodeId GeoId, HAPI_NodeId PartId, HAPI_AttributeOwner Owner, int Index); + + // Checks for PerInstanceCustomData on the instancer part + static void GetPerInstanceCustomData( + int32 InGeoNodeId, + int32 InPartId, + FHoudiniInstancerPartData& OutInstancedOutputPartData); + + // Update PerInstanceCustom data on the given component if possible + static void SetPerInstanceCustomData( + const FHoudiniInstancer& Instancers, + USceneComponent* InComponentToUpdate); + + static void SetGenericPropertyAttributes(UObject* Object, + const FHoudiniInstancer& InstancerData, + const FHoudiniInstancerPartData& InInstancedOutputPartData); + + static FHoudiniInstancerSettings GetDefaultInstancerSettings(const FHoudiniGeoPartObject& HGPO, HAPI_AttributeOwner Owner); + + static FHoudiniInstancerSettings GetInstancerSettings(const FHoudiniGeoPartObject& HGPO, HAPI_AttributeOwner AttributeOwner, int Index, const FHoudiniInstancerSettings & Defaults); + + static TArray UnpackTransforms(const FHoudiniInstancer& InstanceData, const FHoudiniInstancerPartData & PartData); + + static TTuple GetSplitData(const FHoudiniGeoPartObject& HGPO, HAPI_AttributeOwner Owner); + + static TArray GetInstancerTransforms(const FHoudiniGeoPartObject& InHGPO); + + static UObject* LoadInstancedObject(const FString& ObjectPath); + +}; +``` + +`Source/HoudiniEngine/Private/HoudiniLandscapeSplineTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniLandscapeSplineTranslator.h" + +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniLandscapeRuntimeUtils.h" +#include "HoudiniLandscapeUtils.h" +#include "HoudiniPackageParams.h" +#include "HoudiniSplineTranslator.h" + +#include "Landscape.h" +#include "LandscapeSplineActor.h" +#include "LandscapeSplineControlPoint.h" +#include "LandscapeSplineSegment.h" +#include "LandscapeSplinesComponent.h" +#include "Materials/Material.h" +#include "WorldPartition/WorldPartition.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + #include "LandscapeEditLayer.h" +#endif + + +FVector +FHoudiniLandscapeSplineTranslator::ConvertPositionToVector(const float* InPosition) +{ + // Swap Y/Z and convert meters to centimeters + return { + static_cast(InPosition[0] * HAPI_UNREAL_SCALE_FACTOR_POSITION), + static_cast(InPosition[2] * HAPI_UNREAL_SCALE_FACTOR_POSITION), + static_cast(InPosition[1] * HAPI_UNREAL_SCALE_FACTOR_POSITION) + }; +} + + +bool +FHoudiniLandscapeSplineTranslator::ProcessLandscapeSplineOutput( + UHoudiniOutput* InOutput, + const TArray& InAllInputLandscapes, + UWorld* InWorld, + const FHoudiniPackageParams& InPackageParams, + TMap>& InClearedLayers) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniLandscapeSplineTranslator::ProcessLandscapeSplineOutput); + if (!IsValid(InOutput)) + return false; + + if (!IsValid(InWorld)) + return false; + + // Only run on landscape spline inputs + if (InOutput->GetType() != EHoudiniOutputType::LandscapeSpline) + return false; + + UHoudiniCookable* HC = FHoudiniEngineUtils::GetOuterHoudiniCookable(InOutput); + + // Delete any temporary landscape layers created during the last cook + DeleteTempLandscapeLayers(InOutput); + + // If we have a valid HAC, look for the first valid output landscape to use as a fallback if the spline does + // not specify a landscape target + ALandscapeProxy* FallbackLandscape = nullptr; + if (IsValid(HC)) + { + TArray Outputs; + HC->GetOutputs(Outputs); + for (UHoudiniOutput const* const Output : Outputs) + { + if (!IsValid(Output) || Output->GetType() == EHoudiniOutputType::Landscape) + continue; + + for (const auto& Entry : Output->GetOutputObjects()) + { + const FHoudiniOutputObject& OutputObject = Entry.Value; + if (!IsValid(OutputObject.OutputObject)) + continue; + ALandscapeProxy* Proxy = Cast(OutputObject.OutputObject); + if (IsValid(Proxy)) + { + FallbackLandscape = Proxy; + break; + } + } + + if (FallbackLandscape) + break; + } + } + + // Keep track of segments we need to apply to edit layers per landscape. We apply after processing all HGPO for + // this output. + TMap, FHoudiniLandscapeSplineApplyLayerData> SegmentsToApplyToLayers; + TMap NewOutputObjects; + + // Iterate on all the output's HGPOs + for (const FHoudiniGeoPartObject& CurHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + // Skip any HGPO that is not a landscape spline + if (CurHGPO.Type != EHoudiniPartType::LandscapeSpline) + continue; + + // Create / update landscape splines from this HGPO + static constexpr bool bForceRebuild = false; + CreateOutputLandscapeSpline( + CurHGPO, + InOutput, + InAllInputLandscapes, + InWorld, + InPackageParams, + FallbackLandscape, + InClearedLayers, + SegmentsToApplyToLayers, + NewOutputObjects, + HC); + } + + // Apply splines to user specified edit layers and reserved spline layers + FHoudiniLandscapeUtils::ApplySegmentsToLandscapeEditLayers(SegmentsToApplyToLayers); + + InOutput->SetOutputObjects(NewOutputObjects); + // Mark the output as dirty when we update it with the new output objects. This ensures that the outer, the Actor + // in the case of OFPA/World Partition, is marked as dirty and the output objects will then be saved when the user + // saves the level. + InOutput->MarkPackageDirty(); + +#if WITH_EDITORONLY_DATA + if(IsValid(HC) && HC->IsOwnerSelected()) + HC->SetNeedToUpdateEditorProperties(true); +#endif + + return true; +} + +void +FHoudiniLandscapeSplineTranslator::UpdateNonReservedEditLayers( + const FHoudiniLandscapeSplineInfo& InSplineInfo, + TMap>& InClearedLayers, + TMap, FHoudiniLandscapeSplineApplyLayerData>& InSegmentsToApplyToLayers) +{ + if (!IsValid(InSplineInfo.Landscape)) + return; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 7 + if (!InSplineInfo.Landscape->CanHaveLayersContent()) + return; +#endif + + TSet& ClearedLayersForLandscape = InClearedLayers.FindOrAdd(InSplineInfo.Landscape); + + // If the landscape has a reserved splines layer, then we don't have track the segments to apply. Just record the + // landscape with its reserved layer. + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + ULandscapeEditLayerBase* ReservedSplinesLayer = InSplineInfo.Landscape->FindEditLayerOfType(ULandscapeEditLayerSplines::StaticClass()); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + const FLandscapeLayer* ReservedSplinesLayer = InSplineInfo.Landscape->FindLayerOfType(ULandscapeEditLayerSplines::StaticClass()); +#else + FLandscapeLayer* const ReservedSplinesLayer = InSplineInfo.Landscape->GetLandscapeSplinesReservedLayer(); +#endif + if (ReservedSplinesLayer) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FName ReservedName = ReservedSplinesLayer->GetName(); +#else + FName ReservedName = ReservedSplinesLayer->Name; +#endif + FHoudiniLandscapeSplineApplyLayerData& LayerData = InSegmentsToApplyToLayers.FindOrAdd({ InSplineInfo.Landscape, ReservedName }); + LayerData.Landscape = InSplineInfo.Landscape; + LayerData.EditLayerName = ReservedName; + LayerData.bIsReservedSplineLayer = true; + return; + } + + if (!IsValid(InSplineInfo.SplinesOutputObject)) + return; + + for (const auto& Entry : InSplineInfo.SplinesOutputObject->GetLayerOutputs()) + { + if (Entry.Key == NAME_None) + continue; + + UHoudiniLandscapeSplineTargetLayerOutput* const LayerOutput = Entry.Value; + if (!IsValid(LayerOutput)) + continue; + + const FName CookedEditLayer = *LayerOutput->CookedEditLayer; + + // Create layer if it does not exist +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + const FLandscapeLayer* const UnrealEditLayer = FHoudiniLandscapeUtils::GetOrCreateEditLayer(InSplineInfo.Landscape, CookedEditLayer); +#else + FLandscapeLayer* const UnrealEditLayer = FHoudiniLandscapeUtils::GetOrCreateEditLayer(InSplineInfo.Landscape, CookedEditLayer); +#endif + if (!UnrealEditLayer) + { + HOUDINI_LOG_ERROR(TEXT("Could not find edit layer %s and failed to create it: %s"), *CookedEditLayer.ToString(), *(InSplineInfo.Landscape->GetActorLabel())); + continue; + } + + // Re-order layers + if (LayerOutput->AfterEditLayer != NAME_None) + FHoudiniLandscapeUtils::MoveEditLayerAfter(InSplineInfo.Landscape, CookedEditLayer, LayerOutput->AfterEditLayer); + + // Clear layer if requested and not yet cleared + if (LayerOutput->bClearLayer && !ClearedLayersForLandscape.Contains(CookedEditLayer)) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + InSplineInfo.Landscape->ClearEditLayer(UnrealEditLayer->EditLayer->GetGuid(), nullptr, ELandscapeToolTargetTypeFlags::Heightmap); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + InSplineInfo.Landscape->ClearLayer(UnrealEditLayer->EditLayer->GetGuid(), nullptr, ELandscapeClearMode::Clear_Heightmap); +#else + InSplineInfo.Landscape->ClearLayer(UnrealEditLayer->Guid, nullptr, ELandscapeClearMode::Clear_Heightmap); +#endif + ClearedLayersForLandscape.Add(CookedEditLayer); + } + + // Record segments to be applied to layer + FHoudiniLandscapeSplineApplyLayerData& LayerData = InSegmentsToApplyToLayers.FindOrAdd({ InSplineInfo.Landscape, CookedEditLayer }); + LayerData.bIsReservedSplineLayer = false; + LayerData.Landscape = InSplineInfo.Landscape; + LayerData.EditLayerName = CookedEditLayer; + LayerData.SegmentsToApply.Append(LayerOutput->Segments); + } +} + +void +FHoudiniLandscapeSplineTranslator::DeleteTempLandscapeLayers(UHoudiniOutput* const InOutput) +{ + if (!IsValid(InOutput)) + return; + + // Loop over the output objects and delete all temporary layers + TMap> DeletedLandscapeLayers; + for (const auto& OutputObjectEntry : InOutput->GetOutputObjects()) + { + const FHoudiniOutputObject& ObjectEntry = OutputObjectEntry.Value; + if (!IsValid(ObjectEntry.OutputObject)) + continue; + + UHoudiniLandscapeSplinesOutput* const OutputObject = Cast(ObjectEntry.OutputObject); + if (!OutputObject) + continue; + + ALandscape* const Landscape = OutputObject->GetLandscape(); + if (!IsValid(Landscape)) + continue; + + TSet& DeletedLayers = DeletedLandscapeLayers.FindOrAdd(Landscape); + for (const auto& LayerEntry : OutputObject->GetLayerOutputs()) + { + UHoudiniLandscapeSplineTargetLayerOutput const* const LayerOutput = LayerEntry.Value; + // Temp layers have a different EditLayerName from their BakedLayerName + if (LayerOutput->CookedEditLayer.IsEmpty() + || LayerOutput->BakedEditLayer == LayerOutput->CookedEditLayer) + { + continue; + } + + const FName CookedEditLayer = *LayerOutput->CookedEditLayer; + if (DeletedLayers.Contains(CookedEditLayer)) + continue; + + FHoudiniLandscapeRuntimeUtils::DeleteEditLayer(Landscape, CookedEditLayer); + DeletedLayers.Add(CookedEditLayer); + } + } +} + +void +FHoudiniLandscapeSplineTranslator::AddSegmentToOutputObject( + ULandscapeSplineSegment* InSegment, + const FHoudiniLandscapeSplineData& InSplineData, + int InVertexIndex, + UHoudiniCookable* InHC, + const FHoudiniPackageParams& InPackageParams, + UHoudiniLandscapeSplinesOutput& InOutputObject) +{ + if (!IsValid(InSegment)) + return; + + InOutputObject.GetSegments().Add(InSegment); + + // Check for edit layer attributes, for each check vertex first, then prim + + FName EditLayerName = NAME_None; + // Edit layer name + if (InSplineData.SegmentEditLayers.IsValidIndex(InVertexIndex)) + { + EditLayerName = *InSplineData.SegmentEditLayers[InVertexIndex]; + } + else + { + EditLayerName = *InSplineData.DefaultEditLayer; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Set Layer names. The Baked layer name is always what the user specifies; If we are modifying an existing landscape, + // use temporary names if specified or user name baked names. + // ----------------------------------------------------------------------------------------------------------------- + + const FName BakedLayerName = EditLayerName; + + // For the cooked name, but the layer name first so it is easier to read in the Landscape Editor UI. + if (IsValid(InHC) && InHC->GetLandscapeUseTempLayers()) + { + EditLayerName = *( + EditLayerName.ToString() + FString(" : ") + + InPackageParams.GetPackageName() + InHC->GetCookableGUID().ToString()); + } + + // Now that we have the final cooked / temp edit layer name, find or create the Layer Output object for this layer + TMap>& LayerOutputs = InOutputObject.GetLayerOutputs(); + UHoudiniLandscapeSplineTargetLayerOutput* LayerOutput = nullptr; + if (!LayerOutputs.Contains(EditLayerName)) + { + // Create the layer output + LayerOutput = NewObject(&InOutputObject); + LayerOutputs.Add(EditLayerName, LayerOutput); + + // Set the properties on the newly created layer + LayerOutput->Landscape = InOutputObject.GetLandscape(); + LayerOutput->LandscapeProxy = InOutputObject.GetLandscapeProxy(); + LayerOutput->CookedEditLayer = EditLayerName.ToString(); + LayerOutput->BakedEditLayer = BakedLayerName.ToString(); + LayerOutput->bCreatedLandscape = false; + + LayerOutput->bCookedLayerRequiresBaking = (BakedLayerName != EditLayerName); + + // Edit layer clear + if (InSplineData.SegmentEditLayersClear.IsValidIndex(InVertexIndex)) + { + LayerOutput->bClearLayer = InSplineData.SegmentEditLayersClear[InVertexIndex] != 0; + } + else + { + LayerOutput->bClearLayer = InSplineData.DefaultEditLayerClear; + } + + // Edit layer create after + if (InSplineData.SegmentEditLayersAfter.IsValidIndex(InVertexIndex)) + { + LayerOutput->AfterEditLayer = *InSplineData.SegmentEditLayersAfter[InVertexIndex]; + } + else + { + LayerOutput->AfterEditLayer = *InSplineData.DefaultEditLayerAfter; + } + } + else + { + // Layer entry already exists, just fetch it, don't reset properties + LayerOutput = LayerOutputs[EditLayerName]; + } + + // Add the segment to the layer output + LayerOutput->Segments.Add(InSegment); +} + +bool +FHoudiniLandscapeSplineTranslator::CreateOutputLandscapeSpline( + const FHoudiniGeoPartObject& InHGPO, + UHoudiniOutput* InOutput, + const TArray& InAllInputLandscapes, + UWorld* InWorld, + const FHoudiniPackageParams& InPackageParams, + ALandscapeProxy* InFallbackLandscape, + TMap>& InClearedLayers, + TMap, FHoudiniLandscapeSplineApplyLayerData>& SegmentsToApplyToLayers, + TMap& OutputObjects, + UHoudiniCookable* InHC) +{ +#if ENGINE_MAJOR_VERSION < 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0) + HOUDINI_LOG_WARNING(TEXT("Landscape Spline Output is only supported in UE5.1+")); + return false; +#else + + FTransform HCTransform = IsValid(InHC) ? InHC->GetComponentTransform() : FTransform::Identity; + + // Find the fallback landscape to use, either InFallbackLandscape if valid, + // otherwise the first one we find in the world + bool bIsUsingWorldPartition = IsValid(InWorld->GetWorldPartition()); + ALandscapeProxy* FallbackLandscape = InFallbackLandscape; + if (!IsValid(FallbackLandscape)) + { + TActorIterator LandscapeIt(InWorld, ALandscape::StaticClass()); + if (LandscapeIt) + FallbackLandscape = *LandscapeIt; + } + + const HAPI_Session * Session = FHoudiniEngine::Get().GetSession(); + if (!Session) + return false; + + // Get the curve info from HAPI + HAPI_CurveInfo CurveInfo; + FHoudiniApi::CurveInfo_Init(&CurveInfo); + FHoudiniApi::GetCurveInfo(Session, InHGPO.GeoId, InHGPO.PartId, &CurveInfo); + + // Get the point/vertex count for each curve primitive + int NumCurves = CurveInfo.curveCount; + TArray CurvePointCounts; + CurvePointCounts.SetNumZeroed(NumCurves); + FHoudiniApi::GetCurveCounts(Session, InHGPO.GeoId, InHGPO.PartId, CurvePointCounts.GetData(), 0, NumCurves); + + // Bug #134941: Unreal may crash when there are a large number of control points. At least output a warning. + for(int Index = 0; Index < CurvePointCounts.Num(); Index++) + { + int NumPoints = CurvePointCounts[Index]; + + if (NumPoints > 1000) + { + HOUDINI_LOG_ERROR(TEXT( + "A landscape spline contains more than 1000 control points. " + "This may lead to instability when saving levels in Unreal, limiting the number of points to 1000. " + "Consider splitting splines inside Houdini.")); + CurvePointCounts[Index] = 1000; + } + } + + // Extract all target landscapes refs as prim attributes + TArray LandscapeRefs; + + FHoudiniHapiAccessor Accessor(InHGPO.GeoId, InHGPO.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_TARGET_LANDSCAPE); + Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, 1, LandscapeRefs); + + // Extract all custom output name as prim attributes (used for landscape spline actor names in WP, not applicable to non-WP). + TArray OutputNames; + Accessor.Init(InHGPO.GeoId, InHGPO.PartId, HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2); + Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, 1, OutputNames); + + //-------------------------------------------------------------------------------------------------------------------------------------------------- + // Iterate over curves first, use prim attributes to find the landscape that the splines should be attached to, + // and for world partition look at unreal_output_name to determine the landscape spline actor name. + //-------------------------------------------------------------------------------------------------------------------------------------------------- + + TMap LandscapeSplineInfos; + LandscapeSplineInfos.Reserve(NumCurves); + for (int CurveIdx = 0, NextCurveStartPointIdx = 0; CurveIdx < NumCurves; ++CurveIdx) + { + int NumPointsInCurve = CurvePointCounts[CurveIdx]; + int CurveFirstPointIndex = NextCurveStartPointIdx; + NextCurveStartPointIdx += NumPointsInCurve; + + // + // Determine the name (or NAME_None in non-WP) + // + FName OutputName = NAME_None; + if (bIsUsingWorldPartition && OutputNames.IsValidIndex(CurveIdx)) + { + OutputName = *OutputNames[CurveIdx]; + } + + // + // Use the landscape specified with the landscape target attribute + // + + FString LandscapeRef; + ALandscapeProxy* TargetLandscape = nullptr; + if (LandscapeRefs.IsValidIndex(CurveIdx)) + { + LandscapeRef = LandscapeRefs[CurveIdx]; + TargetLandscape = FHoudiniLandscapeUtils::FindTargetLandscapeProxy(LandscapeRef, InWorld, InAllInputLandscapes); + } + + if (!IsValid(TargetLandscape)) + TargetLandscape = FallbackLandscape; + + // If at this point we don't have a valid target landscape then we cannot proceed, return false + if (!IsValid(TargetLandscape)) + { + HOUDINI_LOG_ERROR(TEXT("Could not find target landscape: '%s', and also could not find a " + "fallback landscape from the HAC or World."), *LandscapeRef); + return false; + } + + FString IdentifierName = FString::Printf(TEXT("%s-%s-%s"), *InHGPO.PartName, *TargetLandscape->GetFName().ToString(), *OutputName.ToString()); + + FHoudiniOutputObjectIdentifier Identifier(InHGPO.ObjectId, InHGPO.GeoId, InHGPO.PartId, IdentifierName); + + // Get/create the FHoudiniLandscapeSplineInfo entry that we use to manage the data for each + // ULandscapeSplinesComponent / ALandscapeSplineActor that we will output to + + if (!LandscapeSplineInfos.Find(IdentifierName)) + { + FHoudiniLandscapeSplineInfo SplineInfo; + + // Initialize NextControlPointId to 0. For each curve primitive added to this SplineInfo we will increase + // NextControlPointId based on the control point ids of the curve primitive, so that NextControlPointId + // is greater than all of the control point ids of all curves in the SplineInfo + + SplineInfo.Identifier = Identifier; + SplineInfo.OutputName = OutputName; + SplineInfo.NextControlPointId = 0; + + SplineInfo.LayerPackageParams = InPackageParams; + SplineInfo.LayerPackageParams.ObjectId = Identifier.ObjectId; + SplineInfo.LayerPackageParams.GeoId = Identifier.GeoId; + SplineInfo.LayerPackageParams.PartId = Identifier.PartId; + + SplineInfo.SplineActorPackageParams = SplineInfo.LayerPackageParams; + SplineInfo.SplineActorPackageParams.SplitStr = OutputName.ToString(); + + SplineInfo.LandscapeProxy = TargetLandscape; + SplineInfo.Landscape = SplineInfo.LandscapeProxy->GetLandscapeActor(); + + // Validation + ULandscapeInfo* LandscapeInfo = SplineInfo.LandscapeProxy->GetLandscapeInfo(); + if (!IsValid(LandscapeInfo)) + { + HOUDINI_LOG_ERROR(TEXT("landscape: has no information!")); + return false; + } + + // If the world is using world partition we need to create a landscape spline actor, or manipulate + // the landscape splines component on the landscape directly (non-world partition) + if (bIsUsingWorldPartition) + { + // In world partition, create a new spline actor and name it to OutputName via PackageParams. + if (!SplineInfo.LandscapeSplineActor) + { + SplineInfo.LandscapeSplineActor = LandscapeInfo->CreateSplineActor(FVector::ZeroVector); + if (!IsValid(SplineInfo.LandscapeSplineActor)) + return false; + + FHoudiniEngineUtils::SafeRenameActor(SplineInfo.LandscapeSplineActor, SplineInfo.SplineActorPackageParams.GetPackageName()); + } + + SplineInfo.LandscapeSplineActor->SetActorTransform(HCTransform); + SplineInfo.SplinesComponent = SplineInfo.LandscapeSplineActor->GetSplinesComponent(); + } + else + { + SplineInfo.SplinesComponent = SplineInfo.LandscapeProxy->GetSplinesComponent(); + if (!IsValid(SplineInfo.SplinesComponent)) + { + SplineInfo.LandscapeProxy->CreateSplineComponent(); + SplineInfo.SplinesComponent = SplineInfo.LandscapeProxy->GetSplinesComponent(); + } + } + + if (!SplineInfo.SplinesComponent) + { + HOUDINI_LOG_ERROR(TEXT("landscape: failed to create a spline component!")); + return false; + } + // Update the objects on the SplinesOutputObject to match the SplineInfo + SplineInfo.SplinesOutputObject = NewObject(InOutput); + SplineInfo.SplinesOutputObject->SetLandscapeProxy(SplineInfo.LandscapeProxy); + SplineInfo.SplinesOutputObject->SetLandscape(SplineInfo.Landscape); + SplineInfo.SplinesOutputObject->SetLandscapeSplinesComponent(SplineInfo.SplinesComponent); + SplineInfo.SplinesOutputObject->SetLandscapeSplineActor(SplineInfo.LandscapeSplineActor); + + FHoudiniOutputObject OutputObject; + OutputObject.OutputObject = SplineInfo.SplinesOutputObject; + + OutputObjects.Add(SplineInfo.Identifier, OutputObject); + + LandscapeSplineInfos.Add(IdentifierName, SplineInfo); + } + + FHoudiniLandscapeSplineInfo* SplineInfo = LandscapeSplineInfos.Find(IdentifierName); + + // Add the primitive and point indices of this curve to the SplineInfo + SplineInfo->CurveIndices.Add(CurveIdx); + SplineInfo->PerCurvePointCount.Add(CurvePointCounts[CurveIdx]); + SplineInfo->PerCurveFirstPointIndex.Add(CurveFirstPointIndex); + + // Copy the attributes for this curve primitive from Houdini / HAPI + + SplineInfo->SplineData.AddDefaulted_GetRef() = GetSplineDataFromAttributes( + InHGPO.GeoId, + InHGPO.PartId, + CurveIdx, + CurveFirstPointIndex, + NumPointsInCurve); + + // Ensure that NextControlPointId is greater than all of the PointIds from this curve + FHoudiniLandscapeSplineData& SplineData = SplineInfo->SplineData.Last(); + for (int ControlPointId : SplineData.PointIds) + { + if (ControlPointId >= SplineInfo->NextControlPointId) + SplineInfo->NextControlPointId = ControlPointId + 1; + } + } + + // Fetch generic attributes + TArray GenericPointAttributes; + FHoudiniEngineUtils::GetGenericAttributeList(InHGPO.GeoId, InHGPO.PartId, HAPI_UNREAL_ATTRIB_GENERIC_UPROP_PREFIX, GenericPointAttributes, HAPI_ATTROWNER_POINT); + TArray GenericPrimAttributes; + FHoudiniEngineUtils::GetGenericAttributeList(InHGPO.GeoId, InHGPO.PartId, HAPI_UNREAL_ATTRIB_GENERIC_UPROP_PREFIX, GenericPrimAttributes, HAPI_ATTROWNER_PRIM); + + // Process each SplineInfo entry + for (auto& Entry : LandscapeSplineInfos) + { + FHoudiniLandscapeSplineInfo& SplineInfo = Entry.Value; + + // When not using world partition, we have to transform the splines: apply the HAC's world transform and then + // the inverse of the LandscapeSplinesComponent's transform. + // For world partition, we set the LandscapeSplineActor's transform to the HAC's transform + const FTransform TransformToApply = !bIsUsingWorldPartition + ? HCTransform.GetRelativeTransform(SplineInfo.SplinesComponent->GetComponentTransform()) + : FTransform::Identity; + + TArray>& ControlPoints = SplineInfo.SplinesComponent->GetControlPoints(); + TArray>& Segments = SplineInfo.SplinesComponent->GetSegments(); + + // Process each curve primitive recorded in SplineInfo. Each curve primitive will be at least one segment (with + // at least the first and last points of the primitive being control points). + int NumCurvesInSpline = SplineInfo.PerCurveFirstPointIndex.Num(); + for (int CurveEntryIdx = 0; CurveEntryIdx < NumCurvesInSpline; ++CurveEntryIdx) + { + const FHoudiniLandscapeSplineData& HoudiniCurveData = SplineInfo.SplineData[CurveEntryIdx]; + ULandscapeSplineControlPoint* PreviousControlPoint = nullptr; + int PreviousControlPointArrayIdx = INDEX_NONE; + + int NumPointsInCurve = SplineInfo.PerCurvePointCount[CurveEntryIdx]; + for (int CurvePointArrayIdx = 0; CurvePointArrayIdx < NumPointsInCurve; ++CurvePointArrayIdx) + { + int HGPOPointIndex = SplineInfo.PerCurveFirstPointIndex[CurveEntryIdx] + CurvePointArrayIdx; + + // Check if this is a control point: it has a control point id attribute >= 0, or is the first or last + // point of the curve prim. + int ControlPointId = INDEX_NONE; + if (HoudiniCurveData.PointIds.IsValidIndex(CurvePointArrayIdx)) + { + ControlPointId = HoudiniCurveData.PointIds[CurvePointArrayIdx]; + if (ControlPointId < 0) + ControlPointId = INDEX_NONE; + } + + bool bControlPointCreated = false; + ULandscapeSplineControlPoint* ThisControlPoint = nullptr; + // A point is a control point if: + // 1. It is the first or last point of the curve, or + // 2. It has a valid (>=0) control point id, or + // 3. The control point id attribute does not exist + if (!PreviousControlPoint || CurvePointArrayIdx == NumPointsInCurve - 1 || ControlPointId >= 0 || HoudiniCurveData.PointIds.IsEmpty()) + ThisControlPoint = GetOrCreateControlPoint(SplineInfo, ControlPointId, bControlPointCreated); + + if (bControlPointCreated && IsValid(ThisControlPoint)) + { + SplineInfo.SplinesOutputObject->GetControlPoints().Add(ThisControlPoint); + ControlPoints.Add(ThisControlPoint); + ThisControlPoint->Location = TransformToApply.TransformPosition(ConvertPositionToVector(&HoudiniCurveData.PointPositions[CurvePointArrayIdx * 3])); + + // Update generic properties attributes on the control point + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes(ThisControlPoint, GenericPointAttributes, HGPOPointIndex); + + // Apply point attributes + SetControlPointData(ThisControlPoint, HoudiniCurveData, TransformToApply, CurvePointArrayIdx); + } + + // If we have two control points, create a segment + if (PreviousControlPoint && ThisControlPoint) + { + // Create the segment + ULandscapeSplineSegment* Segment = NewObject(SplineInfo.SplinesComponent, ULandscapeSplineSegment::StaticClass()); + Segment->Connections[0].ControlPoint = PreviousControlPoint; + Segment->Connections[1].ControlPoint = ThisControlPoint; + + // Update generic properties attributes on the segment + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes(Segment, GenericPointAttributes, SplineInfo.PerCurveFirstPointIndex[CurveEntryIdx]); + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes(Segment, GenericPrimAttributes, SplineInfo.CurveIndices[CurveEntryIdx]); + + // Apply attributes to segment + SetSegmentData(Segment, HoudiniCurveData, CurvePointArrayIdx); + + // Apply attributes for connections + SetConnectionData(Segment->Connections[0], 0, HoudiniCurveData, PreviousControlPointArrayIdx); + SetConnectionData(Segment->Connections[1], 1, HoudiniCurveData, CurvePointArrayIdx); + + FVector StartLocation; + FRotator StartRotation; + PreviousControlPoint->GetConnectionLocationAndRotation(Segment->Connections[0].SocketName, StartLocation, StartRotation); + FVector EndLocation; + FRotator EndRotation; + ThisControlPoint->GetConnectionLocationAndRotation(Segment->Connections[1].SocketName, EndLocation, EndRotation); + + // Set up tangent lengths if not in vertex/prim connection attributes + if (!(HoudiniCurveData.SegmentConnectionTangentLengths[0].IsValidIndex(PreviousControlPointArrayIdx)) + || !(HoudiniCurveData.DefaultConnectionTangentLengths[0])) + { + Segment->Connections[0].TangentLen = (EndLocation - StartLocation).Size(); + } + if (!(HoudiniCurveData.SegmentConnectionTangentLengths[1].IsValidIndex(CurvePointArrayIdx)) + || !(HoudiniCurveData.DefaultConnectionTangentLengths[1])) + { + Segment->Connections[1].TangentLen = Segment->Connections[0].TangentLen; + } + + Segment->AutoFlipTangents(); + + PreviousControlPoint->ConnectedSegments.Add(FLandscapeSplineConnection(Segment, 0)); + ThisControlPoint->ConnectedSegments.Add(FLandscapeSplineConnection(Segment, 1)); + + // Auto-calculate rotation if we didn't receive rotation attributes + if (!HoudiniCurveData.PointRotations.IsValidIndex(PreviousControlPointArrayIdx)) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + PreviousControlPoint->AutoCalcRotation(true); +#else + PreviousControlPoint->AutoCalcRotation(); +#endif + if (!HoudiniCurveData.PointRotations.IsValidIndex(CurvePointArrayIdx)) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + ThisControlPoint->AutoCalcRotation(true); +#else + ThisControlPoint->AutoCalcRotation(); +#endif + + // Add the segment to the appropriate LayerOutput. Will create the LayerOutput if necessary. + AddSegmentToOutputObject(Segment, HoudiniCurveData, CurvePointArrayIdx, InHC, SplineInfo.LayerPackageParams, *SplineInfo.SplinesOutputObject); + Segments.Add(Segment); + } + + // If we created a control point in this iteration, record that as the previous control point for the + // next iteration + if (ThisControlPoint) + { + PreviousControlPoint = ThisControlPoint; + PreviousControlPointArrayIdx = CurvePointArrayIdx; + } + } + } + + SplineInfo.SplinesComponent->RebuildAllSplines(); + + FHoudiniOutputObject* OutputObject = OutputObjects.Find(SplineInfo.Identifier); + + GetCachedAttributes(OutputObject, InHGPO, SplineInfo); + + // Handle user specified landscape layers for these segments + UpdateNonReservedEditLayers(SplineInfo, InClearedLayers, SegmentsToApplyToLayers); + } + + return true; +#endif +} + +void FHoudiniLandscapeSplineTranslator::GetCachedAttributes(FHoudiniOutputObject * OutputObject, const FHoudiniGeoPartObject& InHGPO, const FHoudiniLandscapeSplineInfo& SplineInfo) +{ + // Cache commonly supported Houdini attributes on the OutputAttributes + TArray LevelPaths; + FHoudiniEngineUtils::GetLevelPathAttribute(InHGPO.GeoId, InHGPO.PartId, LevelPaths, HAPI_ATTROWNER_INVALID, 0, 1); + if (LevelPaths.Num() > 0 && !LevelPaths[0].IsEmpty()) + { + // cache the level path attribute on the output object + OutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_LEVEL_PATH, LevelPaths[0]); + } + + // cache the output name attribute on the output object + OutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2, SplineInfo.OutputName.ToString()); + + int FirstCurvePrimIndex = SplineInfo.CurveIndices.Num() > 0 ? SplineInfo.CurveIndices[0] : INDEX_NONE; + + if (FirstCurvePrimIndex != INDEX_NONE) + { + TArray BakeNames; + FHoudiniEngineUtils::GetBakeNameAttribute(InHGPO.GeoId, InHGPO.PartId, BakeNames, HAPI_ATTROWNER_PRIM, FirstCurvePrimIndex, 1); + + if (BakeNames.Num() > 0 && !BakeNames[0].IsEmpty()) + { + // cache the output name attribute on the output object + OutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_NAME, BakeNames[0]); + } + + TArray BakeOutputActorNames; + FHoudiniEngineUtils::GetBakeActorAttribute(InHGPO.GeoId, InHGPO.PartId, BakeOutputActorNames, HAPI_ATTROWNER_PRIM, FirstCurvePrimIndex, 1); + if (BakeOutputActorNames.Num() > 0 && !BakeOutputActorNames[0].IsEmpty()) + { + // cache the bake actor attribute on the output object + OutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_ACTOR, BakeOutputActorNames[0]); + } + + TArray BakeOutputActorClassNames; + if (FHoudiniEngineUtils::GetBakeActorClassAttribute(InHGPO.GeoId, InHGPO.PartId, BakeOutputActorClassNames, HAPI_ATTROWNER_PRIM, FirstCurvePrimIndex, 1)) + if (BakeOutputActorClassNames.Num() > 0 && !BakeOutputActorClassNames[0].IsEmpty()) + { + // cache the bake actor attribute on the output object + OutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_ACTOR_CLASS, BakeOutputActorClassNames[0]); + } + + TArray BakeFolders; + FHoudiniEngineUtils::GetBakeFolderAttribute(InHGPO.GeoId, BakeFolders, InHGPO.PartId, 0, 1); + if (BakeFolders.Num() > 0 && !BakeFolders[0].IsEmpty()) + { + // cache the unreal_bake_folder attribute on the output object + OutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_FOLDER, BakeFolders[0]); + } + + TArray BakeOutlinerFolders; + FHoudiniEngineUtils::GetBakeOutlinerFolderAttribute(InHGPO.GeoId, InHGPO.PartId, BakeOutlinerFolders, HAPI_ATTROWNER_PRIM, FirstCurvePrimIndex, 1); + if (BakeOutlinerFolders.Num() > 0 && !BakeOutlinerFolders[0].IsEmpty()) + { + // cache the bake actor attribute on the output object + OutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_OUTLINER_FOLDER, BakeOutlinerFolders[0]); + } + } +} + +ULandscapeSplineControlPoint* +FHoudiniLandscapeSplineTranslator::GetOrCreateControlPoint(FHoudiniLandscapeSplineInfo& SplineInfo, const int InControlPointId, bool& bOutCreated) +{ + ULandscapeSplineControlPoint* ControlPoint = nullptr; + if (InControlPointId >= 0 && SplineInfo.ControlPointMap.Contains(InControlPointId)) + ControlPoint = SplineInfo.ControlPointMap[InControlPointId]; + if (!IsValid(ControlPoint)) + { + // Point is null/invalid or has not yet been created, so create it + ControlPoint = NewObject(SplineInfo.SplinesComponent, ULandscapeSplineControlPoint::StaticClass()); + + // Assign a control point Id to the new point + // InControlPointId if its valid + // otherwise, generate a new Id from SplineInfo.NextControlPointId + int ControlPointId = InControlPointId; + if (ControlPointId < 0) + { + ControlPointId = SplineInfo.NextControlPointId; + SplineInfo.NextControlPointId++; + } + + SplineInfo.ControlPointMap.Add(ControlPointId, ControlPoint); + bOutCreated = true; + } + else + { + // Found the previously created valid point, just return it + bOutCreated = false; + } + + return ControlPoint; +} + + +bool +FHoudiniLandscapeSplineTranslator::CopySegmentMeshAttributesFromHoudini( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + HAPI_AttributeOwner InAttrOwner, + int InStartIndex, + int InCount, + TArray& AllSegmentMeshData) +{ + AllSegmentMeshData.SetNum(InCount); + int MeshIndex = 0; + + while (true) + { + // If MeshIndex == 0 then don't add the numeric suffix + const FString AttrNamePrefix = MeshIndex > 0 + ? FString::Printf(TEXT(HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_MESH "%d"), MeshIndex) + : FString(TEXT(HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_MESH)); + TArray MeshReferences; + FHoudiniHapiAccessor Accessor; + Accessor.Init(InNodeId, InPartId, TCHAR_TO_ANSI(*AttrNamePrefix)); + Accessor.GetAttributeData(InAttrOwner, 1, MeshReferences, InStartIndex, InCount); + + // mesh scale + FString MeshScaleAttrName = FString::Printf(TEXT("%s%s"), *AttrNamePrefix, TEXT(HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_SCALE_SUFFIX)); + TArray MeshScales; + Accessor.Init(InNodeId, InPartId, TCHAR_TO_ANSI(*MeshScaleAttrName)); + Accessor.GetAttributeData(InAttrOwner, 3, MeshScales, InStartIndex, InCount); + + // center adjust + TArray CenterAdjust; + FString MeshCenterAdjustAttrName = FString::Printf(TEXT("%s%s"), *AttrNamePrefix, TEXT(HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_CENTER_ADJUST_SUFFIX)); + Accessor.Init(InNodeId, InPartId, TCHAR_TO_ANSI(*MeshCenterAdjustAttrName)); + Accessor.GetAttributeData(InAttrOwner, 2, CenterAdjust, InStartIndex, InCount); + + // material overrides + TArray> MeterialOverideList; + FString MaterialAttrNamePrefix = AttrNamePrefix + TEXT(HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_MATERIAL_OVERRIDE_SUFFIX); + + // The same as with the MeshIndex above, loop until the first iteration where we cannot find a material + // override attribute + int MaterialOverrideIdx = 0; + while (true) + { + TArray MaterialOverrides; + + // Add the MaterialOverrideIdx as a suffix to the attribute name when > 0 + FString MaterialOverrideAttrName = MaterialOverrideIdx > 0 + ? MaterialAttrNamePrefix + FString::Printf(TEXT("%d"), MaterialOverrideIdx) + : MaterialAttrNamePrefix; + + Accessor.Init(InNodeId, InPartId, TCHAR_TO_ANSI(*MaterialOverrideAttrName)); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, MaterialOverrides); + + if (!bSuccess) + { + break; + } + + MeterialOverideList.Emplace(MoveTemp(MaterialOverrides)); + MaterialOverrideIdx++; + } + + bool bNoData = MeshReferences.IsEmpty() && MeshScales.IsEmpty() && CenterAdjust.IsEmpty() && MeterialOverideList.IsEmpty(); + if (bNoData) + break; + + for(int Index = 0; Index < InCount; Index++) + { + FHoudiniLandscapeSplineMesh SegmentMeshData; + + bool bSetOne = false; + + if (MeshReferences.IsValidIndex(Index)) + { + SegmentMeshData.MeshRef = MeshReferences[Index]; + bSetOne = true; + } + if (MeshScales.IsValidIndex(Index * 3)) + { + SegmentMeshData.MeshScale = FVector( + MeshScales[Index * 3 + 0], + MeshScales[Index * 3 + 2], + MeshScales[Index * 3 + 1]); + bSetOne = true; + } + if (CenterAdjust.IsValidIndex(Index * 2)) + { + SegmentMeshData.CenterAdjust = FVector2d( + CenterAdjust[Index * 2 + 0], + CenterAdjust[Index * 2 + 1]); + bSetOne = true; + } + if (MeterialOverideList.IsValidIndex(Index)) + { + SegmentMeshData.MaterialOverrideRef = MeterialOverideList[Index]; + bSetOne = true; + } + + if (bSetOne) + AllSegmentMeshData[Index].Meshes.Add(SegmentMeshData); + + } + + MeshIndex++; + } + + AllSegmentMeshData.Shrink(); + return true; +} + +FHoudiniLandscapeSplineData +FHoudiniLandscapeSplineTranslator::GetSplineDataFromAttributes( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + int InPrimIndex, + int InFirstPointIndex, + int InNumPoints) +{ + FHoudiniLandscapeSplineData SplineData; + + // point positions + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_POSITION); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 3, SplineData.PointPositions, InFirstPointIndex, InNumPoints); + + // rot attribute (quaternion) -- control point rotations + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_ROTATION); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 4, SplineData.PointRotations, InFirstPointIndex, InNumPoints); + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_PAINT_LAYER_NAME); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.PointPaintLayerNames, InFirstPointIndex, InNumPoints); + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_RAISE_TERRAIN); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.PointRaiseTerrains, InFirstPointIndex, InNumPoints); + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_LOWER_TERRAIN); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.PointLowerTerrains, InFirstPointIndex, InNumPoints); + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_MESH); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.PointMeshRefs, InFirstPointIndex, InNumPoints); + + // control point material overrides + SplineData.PerMaterialOverridePointRefs.Reset(); + const FString ControlPointMaterialOverrideAttrNamePrefix = TEXT(HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_MATERIAL_OVERRIDE_SUFFIX); + + // Loop until the first iteration where we don't find any material override attributes. + int MaterialOverrideIdx = 0; + while (true) + { + TArray MaterialOverrides; + + // If the index > 0 add it as a suffix to the attribute name + const FString AttrName = MaterialOverrideIdx > 0 + ? ControlPointMaterialOverrideAttrNamePrefix + FString::Printf(TEXT("%d"), MaterialOverrideIdx) + : ControlPointMaterialOverrideAttrNamePrefix; + + Accessor.Init(InNodeId, InPartId, TCHAR_TO_ANSI(*AttrName)); + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, MaterialOverrides); + if (!bSuccess) + break; + + SplineData.PerMaterialOverridePointRefs.Emplace(MoveTemp(MaterialOverrides)); + MaterialOverrideIdx++; + } + + // control point mesh scales + + Accessor.Init(InNodeId,InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_MESH HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_SCALE_SUFFIX); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 3, SplineData.PointMeshScales, InFirstPointIndex, InNumPoints); + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_ID); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.PointIds, InFirstPointIndex, InNumPoints); + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_HALF_WIDTH); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.PointHalfWidths, InFirstPointIndex, InNumPoints); + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SIDE_FALLOFF); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.PointSideFalloffs, InFirstPointIndex, InNumPoints); + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_END_FALLOFF); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.PointEndFalloffs, InFirstPointIndex, InNumPoints); + + // Connection attributes -- there are separate attributes for the two ends of the connection + static const char* ConnectionMeshSocketNameAttrNames[] + { + HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONNECTION0_MESH_SOCKET_NAME, + HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONNECTION1_MESH_SOCKET_NAME + }; + static const char* ConnectionTangentLengthAttrNames[] + { + HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONNECTION0_TANGENT_LENGTH, + HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONNECTION1_TANGENT_LENGTH + }; + for (int ConnectionIndex = 0; ConnectionIndex < 2; ++ConnectionIndex) + { + // segment connection[ConnectionIndex] socket names -- vertex/point attribute + Accessor.Init(InNodeId, InPartId, ConnectionMeshSocketNameAttrNames[ConnectionIndex]); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.SegmentConnectionSocketNames[ConnectionIndex]); + + Accessor.Init(InNodeId, InPartId, ConnectionTangentLengthAttrNames[ConnectionIndex]); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.SegmentConnectionTangentLengths[ConnectionIndex], InFirstPointIndex, InNumPoints); + } + + // segment paint layer name -- vertex/point + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_PAINT_LAYER_NAME); + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.SegmentPaintLayerNames, InFirstPointIndex, InNumPoints); + + // segment raise terrains -- vertex/point + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_RAISE_TERRAIN); + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.SegmentRaiseTerrains, InFirstPointIndex, InNumPoints); + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_LOWER_TERRAIN); + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.SegmentLowerTerrains, InFirstPointIndex, InNumPoints); + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_NAME); + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.SegmentEditLayers, InFirstPointIndex, InNumPoints); + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_CLEAR); + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.SegmentEditLayersClear, InFirstPointIndex, InNumPoints); + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_AFTER); + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, 1, SplineData.SegmentEditLayersAfter, InFirstPointIndex, InNumPoints); + + // segment paint layer name + if (SplineData.SegmentPaintLayerNames.IsEmpty()) + { + TArray SegmentPaintLayerName; + + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_PAINT_LAYER_NAME); + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, 1, SegmentPaintLayerName, InPrimIndex, 1); + + if (bSuccess && SegmentPaintLayerName.Num() > 0) + { + SplineData.DefaultPaintLayerName = SegmentPaintLayerName[0]; + } + } + + // segment raise terrains + if (SplineData.SegmentRaiseTerrains.IsEmpty()) + { + TArray RaiseTerrains; + HAPI_AttributeInfo PrimRaiseTerrainAttrInfo; + + Accessor.Init(InNodeId,InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_RAISE_TERRAIN); + Accessor.GetInfo(PrimRaiseTerrainAttrInfo, HAPI_ATTROWNER_INVALID); + PrimRaiseTerrainAttrInfo.tupleSize = 1; + bSuccess = Accessor.GetAttributeData(PrimRaiseTerrainAttrInfo, RaiseTerrains, InPrimIndex, 1); + + if (bSuccess && RaiseTerrains.Num() > 0) + { + SplineData.DefaultRaiseTerrain = RaiseTerrains[0] != 0; + } + } + // segment lower terrains + if (SplineData.SegmentLowerTerrains.IsEmpty()) + { + TArray LowerTerrains; + + HAPI_AttributeInfo PrimLowerTerrainAttrInfo; + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_LOWER_TERRAIN); + Accessor.GetInfo(PrimLowerTerrainAttrInfo, HAPI_ATTROWNER_PRIM); + bSuccess = Accessor.GetAttributeData(PrimLowerTerrainAttrInfo, LowerTerrains); + + if (bSuccess && LowerTerrains.Num() > 0) + { + SplineData.DefaultLowerTerrain = LowerTerrains[0]; + } + } + + // segment edit layer -- prim + if (SplineData.SegmentEditLayers.IsEmpty()) + { + TArray EditLayers; + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_NAME); + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, 1, EditLayers); + + if (bSuccess && EditLayers.Num() > 0) + { + SplineData.DefaultEditLayer = EditLayers[0]; + } + } + + // segment edit layer clear -- prim + if (SplineData.SegmentEditLayersClear.IsEmpty()) + { + TArray EditLayersClear; + Accessor.Init(InNodeId,InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_CLEAR); + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, EditLayersClear, InPrimIndex, 1); + + if (bSuccess && EditLayersClear.Num() > 0) + { + SplineData.DefaultEditLayerClear = static_cast(EditLayersClear[0]); + } + } + + // segment edit layer after -- prim + if (SplineData.SegmentEditLayersAfter.IsEmpty()) + { + TArray EditLayersAfter; + Accessor.Init(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_AFTER); + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, 1, EditLayersAfter, InPrimIndex, 1); + + if (bSuccess && EditLayersAfter.Num() > 0) + { + SplineData.DefaultEditLayerAfter = EditLayersAfter[0]; + } + } + + // Copy segment mesh attributes from Houdini -- vertex/point attributes + if (!CopySegmentMeshAttributesFromHoudini(InNodeId, InPartId, HAPI_ATTROWNER_POINT, InFirstPointIndex, InNumPoints, SplineData.SegmentMeshData)) + { + return SplineData; + } + + // Copy segment mesh attributes from Houdini -- prim attributes + if (!CopySegmentMeshAttributesFromHoudini(InNodeId, InPartId, HAPI_ATTROWNER_PRIM, InPrimIndex, 1, SplineData.DefaultMeshSegmentData)) + { + return SplineData; + } + + return SplineData; +} + +bool +FHoudiniLandscapeSplineTranslator::SetControlPointData( + ULandscapeSplineControlPoint* InPoint, + const FHoudiniLandscapeSplineData& InSplineData, + const FTransform& InTransformToApply, + int InPointIndex) +{ + if (!IsValid(InPoint)) + return false; + + // Apply the attributes from Houdini (SplineData) to the control point InPoint + + // Rotation + if (InSplineData.PointRotations.IsValidIndex(InPointIndex * 4) && InSplineData.PointRotations.IsValidIndex(InPointIndex * 4 + 3)) + { +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0) + static constexpr float HalfPI = UE_HALF_PI; +#else + static constexpr float HalfPI = HALF_PI; +#endif + // Convert Houdini Y-up to UE Z-up and also Houdini Z-forward to UE X-forward + InPoint->Rotation = (InTransformToApply.TransformRotation({ + InSplineData.PointRotations[InPointIndex * 4 + 0], + InSplineData.PointRotations[InPointIndex * 4 + 2], + InSplineData.PointRotations[InPointIndex * 4 + 1], + -InSplineData.PointRotations[InPointIndex * 4 + 3] + }) * FQuat(FVector::UpVector, HalfPI)).Rotator(); + } + + // (Paint) layer name + if (InSplineData.PointPaintLayerNames.IsValidIndex(InPointIndex)) + { + InPoint->LayerName = *InSplineData.PointPaintLayerNames[InPointIndex]; + } + + // bRaiseTerrain + if (InSplineData.PointRaiseTerrains.IsValidIndex(InPointIndex)) + { + InPoint->bRaiseTerrain = InSplineData.PointRaiseTerrains[InPointIndex]; + } + + // bLowerTerrain + if (InSplineData.PointLowerTerrains.IsValidIndex(InPointIndex)) + { + InPoint->bLowerTerrain = InSplineData.PointLowerTerrains[InPointIndex]; + } + + // Control point static mesh + if (InSplineData.PointMeshRefs.IsValidIndex(InPointIndex)) + { + if (!InSplineData.PointMeshRefs[InPointIndex].IsEmpty()) + { + UObject* Mesh = StaticFindObject(UStaticMesh::StaticClass(), nullptr, *InSplineData.PointMeshRefs[InPointIndex]); + if (!Mesh) + Mesh = StaticLoadObject(UStaticMesh::StaticClass(), nullptr, *InSplineData.PointMeshRefs[InPointIndex]); + UStaticMesh* const SM = Mesh ? Cast(Mesh) : nullptr; + if (IsValid(SM)) + InPoint->Mesh = Cast(Mesh); + else + InPoint->Mesh = nullptr; + } + } + + // Control point static mesh material overrides + if (InSplineData.PerMaterialOverridePointRefs.Num() > 0) + { + InPoint->MaterialOverrides.Reset(InSplineData.PerMaterialOverridePointRefs.Num()); + for (const TArray& PerPointMaterialOverrideX : InSplineData.PerMaterialOverridePointRefs) + { + if (!PerPointMaterialOverrideX.IsValidIndex(InPointIndex)) + continue; + + const FString& MaterialRef = PerPointMaterialOverrideX[InPointIndex]; + + UObject* Material = StaticFindObject(UMaterialInterface::StaticClass(), nullptr, *MaterialRef); + if (!Material) + Material = StaticLoadObject(UMaterialInterface::StaticClass(), nullptr, *MaterialRef); + UMaterialInterface* MaterialInterface = Material ? Cast(Material) : nullptr; + if (IsValid(MaterialInterface)) + InPoint->MaterialOverrides.Add(MaterialInterface); + else + InPoint->MaterialOverrides.Add(nullptr); + } + InPoint->MaterialOverrides.Shrink(); + } + + // Control point mesh scale + if (InSplineData.PointMeshScales.IsValidIndex(InPointIndex * 3) && InSplineData.PointMeshScales.IsValidIndex(InPointIndex * 3 + 2)) + { + InPoint->MeshScale = FVector( + InSplineData.PointMeshScales[InPointIndex * 3 + 0], + InSplineData.PointMeshScales[InPointIndex * 3 + 2], + InSplineData.PointMeshScales[InPointIndex * 3 + 1]); + } + + // Control point half-width + if (InSplineData.PointHalfWidths.IsValidIndex(InPointIndex)) + { + // Convert from Houdini units to UE units + InPoint->Width = InSplineData.PointHalfWidths[InPointIndex] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + + // Control point side-falloff + if (InSplineData.PointSideFalloffs.IsValidIndex(InPointIndex)) + { + // Convert from Houdini units to UE units + InPoint->SideFalloff = InSplineData.PointSideFalloffs[InPointIndex] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + + // Control point end-falloff + if (InSplineData.PointEndFalloffs.IsValidIndex(InPointIndex)) + { + // Convert from Houdini units to UE units + InPoint->EndFalloff = InSplineData.PointEndFalloffs[InPointIndex] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + + return true; +} + +bool +FHoudiniLandscapeSplineTranslator::SetSegmentData( + ULandscapeSplineSegment* InSegment, + const FHoudiniLandscapeSplineData& InSplineData, + int InVertexIndex) +{ + if (!IsValid(InSegment)) + return false; + + // Update the segment (InSegment) with the attributes copied from Houdini (InSplineData) + + // (Paint) layer name + InSegment->LayerName = *InSplineData.DefaultPaintLayerName; + if (InSplineData.SegmentPaintLayerNames.IsValidIndex(InVertexIndex)) + InSegment->LayerName = *InSplineData.SegmentPaintLayerNames[InVertexIndex]; + + // bRaiseTerrain + InSegment->bRaiseTerrain = InSplineData.DefaultRaiseTerrain; + if (InSplineData.SegmentRaiseTerrains.IsValidIndex(InVertexIndex)) + InSegment->bRaiseTerrain = InSplineData.SegmentRaiseTerrains[InVertexIndex]; + + // bLowerTerrain + InSegment->bLowerTerrain = InSplineData.DefaultLowerTerrain; + if (InSplineData.SegmentLowerTerrains.IsValidIndex(InVertexIndex)) + InSegment->bLowerTerrain = InSplineData.SegmentLowerTerrains[InVertexIndex]; + + // Segment static meshes + + const TArray * Meshes = nullptr; + if (InSplineData.SegmentMeshData.IsValidIndex(InVertexIndex)) + { + if (!InSplineData.SegmentMeshData.IsEmpty()) + Meshes = &InSplineData.SegmentMeshData[InVertexIndex].Meshes; + } + else if (!InSplineData.DefaultMeshSegmentData.IsEmpty()) + { + Meshes = &InSplineData.DefaultMeshSegmentData[0].Meshes; + } + + int NumMeshes = Meshes != nullptr ? Meshes->Num() : 0; + for (int MeshIdx = 0; MeshIdx < NumMeshes; ++MeshIdx) + { + const FHoudiniLandscapeSplineMesh & InputMesh = (*Meshes)[MeshIdx]; + FLandscapeSplineMeshEntry SplineMeshEntry; + if (!InputMesh.MeshRef.IsEmpty()) + { + SplineMeshEntry.Mesh = Cast(StaticFindObject(UStaticMesh::StaticClass(), nullptr, *InputMesh.MeshRef)); + if (!SplineMeshEntry.Mesh) + SplineMeshEntry.Mesh = Cast(StaticLoadObject(UStaticMesh::StaticClass(), nullptr, *InputMesh.MeshRef)); + } + + SplineMeshEntry.Scale = InputMesh.MeshScale; + SplineMeshEntry.CenterAdjust = InputMesh.CenterAdjust; + + for(int MaterialIdx = 0; MaterialIdx < SplineMeshEntry.MaterialOverrides.Num(); MaterialIdx++) + { + // Found a material override at this index, try to find / load it + + const FString& MaterialOverride = *InputMesh.MaterialOverrideRef[MaterialIdx]; + UObject* Material = StaticFindObject(UMaterialInterface::StaticClass(), nullptr, *MaterialOverride); + if (!Material) + Material = StaticLoadObject(UMaterialInterface::StaticClass(), nullptr, *MaterialOverride); + + UMaterialInterface* MaterialInterface = Material ? Cast(Material) : nullptr; + SplineMeshEntry.MaterialOverrides.Add(MaterialInterface); + } + + InSegment->SplineMeshes.Emplace(SplineMeshEntry); + + } + + return true; +} + +bool +FHoudiniLandscapeSplineTranslator::SetConnectionData( + FLandscapeSplineSegmentConnection& InConnection, + int InConnectionIndex, + const FHoudiniLandscapeSplineData& InSplineData, + int InPointIndex) +{ + // Update the InConnection's properties from the attributes copied from Houdini. + // Check the vertex/point attribute first, if that is not set, use the prim attribute. + + // socket name + if (InSplineData.SegmentConnectionSocketNames[InConnectionIndex].IsValidIndex(InPointIndex)) + { + InConnection.SocketName = *InSplineData.SegmentConnectionSocketNames[InConnectionIndex][InPointIndex]; + } + else + { + InConnection.SocketName = *InSplineData.DefaultConnectionSocketNames[InConnectionIndex]; + } + + // tangent length + if (InSplineData.SegmentConnectionTangentLengths[InConnectionIndex].IsValidIndex(InPointIndex)) + { + InConnection.TangentLen = InSplineData.SegmentConnectionTangentLengths[InConnectionIndex][InPointIndex]; + } + else + { + InConnection.TangentLen = InSplineData.DefaultConnectionTangentLengths[InConnectionIndex]; + } + + return true; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniLandscapeSplineTranslator.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +// This file contains the "public" API for translating curves/splines from Houdini to Unreal Landscape Splines. + +#include "CoreMinimal.h" + +#include "HAPI/HAPI_Common.h" + +#include "HoudiniOutput.h" +#include "LandscapeSplineControlPoint.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniLandscapeRuntimeUtils.h" +#include "HoudiniLandscapeUtils.h" +#include "HoudiniPackageParams.h" +#include "HoudiniSplineTranslator.h" + +#include "Landscape.h" +#include "LandscapeSplineActor.h" +#include "LandscapeSplineSegment.h" +#include "LandscapeSplinesComponent.h" + +class ULandscapeSplineControlPoint; +class ULandscapeSplineSegment; +struct FLandscapeSplineSegmentConnection; +class UHoudiniCookable; +struct FHoudiniPackageParams; +struct FHoudiniLandscapeSplineInfo; +struct FHoudiniLandscapeSplineData; +struct FHoudiniLandscapeSplineSegmentMeshData; +struct FHoudiniLandscapeSplineApplyLayerData; + + + + +struct HOUDINIENGINE_API FHoudiniLandscapeSplineTranslator +{ + // Process the landscape spline output InOutput and create/update the relevant ULandscapeSplinesComponents. + static bool ProcessLandscapeSplineOutput( + UHoudiniOutput* InOutput, + const TArray& InAllInputLandscapes, + UWorld* InWorld, + const FHoudiniPackageParams& InPackageParams, + TMap>& InClearedLayers); + + // Create / update ULandscapeSplinesComponents from the geo in InHGPO. + static bool CreateOutputLandscapeSpline( + const FHoudiniGeoPartObject& InHGPO, + UHoudiniOutput* InOutput, + const TArray& InAllInputLandscapes, + UWorld* InWorld, + const FHoudiniPackageParams& InPackageParams, + ALandscapeProxy* InFallbackLandscape, + TMap>& InClearedLayers, + TMap, FHoudiniLandscapeSplineApplyLayerData>& SegmentsToApplyToLayers, + TMap& OutputObjects, + UHoudiniCookable* InHC = nullptr); + +private: + static void DeleteTempLandscapeLayers(UHoudiniOutput* InOutput); + + static void AddSegmentToOutputObject( + ULandscapeSplineSegment* InSegment, + const FHoudiniLandscapeSplineData& InSplineData, + int InVertexIndex, + UHoudiniCookable* InHC, + const FHoudiniPackageParams& InPackageParams, + UHoudiniLandscapeSplinesOutput& InOutputObject); + + static void UpdateNonReservedEditLayers( + const FHoudiniLandscapeSplineInfo& InSplineInfo, + TMap>& InClearedLayers, + TMap, FHoudiniLandscapeSplineApplyLayerData>& InSegmentsToApplyToLayers); + + static ULandscapeSplineControlPoint* GetOrCreateControlPoint( + FHoudiniLandscapeSplineInfo& SplineInfo, int InControlPointId, bool& bOutCreated); + + static bool CopySegmentMeshAttributesFromHoudini( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + HAPI_AttributeOwner InAttrOwner, + int InStartIndex, + int InCount, + TArray& AllSegmentMeshData); + + static FHoudiniLandscapeSplineData GetSplineDataFromAttributes( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + int InPrimIndex, + int InFirstPointIndex, + int InNumPoints); + + static bool SetControlPointData( + ULandscapeSplineControlPoint* InPoint, + const FHoudiniLandscapeSplineData& SplineData, + const FTransform& InTransformToApply, + int InPointIndex); + + static bool SetSegmentData( + ULandscapeSplineSegment* InSegment, + const FHoudiniLandscapeSplineData& InSplineData, + int InVertexIndex); + + static bool SetConnectionData( + FLandscapeSplineSegmentConnection& InConnection, + int InConnectionIndex, + const FHoudiniLandscapeSplineData& InSplineData, + int InPointIndex); + + static void GetCachedAttributes( + FHoudiniOutputObject * OutputObject, + const FHoudiniGeoPartObject& InHGPO, + const FHoudiniLandscapeSplineInfo& SplineInfo); + + static FVector ConvertPositionToVector(const float* InPosition); + +}; + +struct FHoudiniLandscapeSplineMesh +{ + FString MeshRef; + TArray MaterialOverrideRef; // the outer index is material 0, 1, 2 ... + FVector MeshScale = FVector::OneVector; + FVector2d CenterAdjust = FVector2d::Zero(); +}; + + +struct FHoudiniLandscapeSplineSegmentMeshData +{ + TArray Meshes; +}; + + +struct FHoudiniLandscapeSplineData +{ + //------------------------------------------------------------------------------------------------------------------------------------- + // Attribute data associate with control points. This is the raw attribute data pulled from Houdini. + // Since Houdini Engine doesn't really distinguish between curve vertices and points, this data can be store on either. + //------------------------------------------------------------------------------------------------------------------------------------- + + TArray PointPositions; + TArray PointRotations; + TArray PointPaintLayerNames; + TArray PointRaiseTerrains; + TArray PointLowerTerrains; + TArray PointMeshRefs; + TArray PointMeshScales; + TArray PointIds; + TArray PointHalfWidths; + TArray PointSideFalloffs; + TArray PointEndFalloffs; + TArray> PerMaterialOverridePointRefs; + + //------------------------------------------------------------------------------------------------------------------------------------- + // Attribute data associated with segments. This is the raw attribute data pulled from Houdini. + // Since Houdini Engine doesn't really distinguish between curve vertices and points, this data can be store on either. + //------------------------------------------------------------------------------------------------------------------------------------- + + TArray SegmentConnectionSocketNames[2]; // 0 - is the start, 1 - is the end. + TArray SegmentConnectionTangentLengths[2]; // 0 - is the start, 1 - is the end. + TArray SegmentPaintLayerNames; + TArray SegmentRaiseTerrains; + TArray SegmentLowerTerrains; + TArray SegmentEditLayers; + TArray SegmentEditLayersClear; + TArray SegmentEditLayersAfter; + TArray SegmentMeshData; + + //------------------------------------------------------------------------------------------------------------------------------------- + // Default values for spline control points and segments. These are set to sensible defaults, but can be overriden + // with primitive attributes + //------------------------------------------------------------------------------------------------------------------------------------- + + float DefaultConnectionTangentLengths[2]; // near side (0) and far side (1) of the segment connection. + FString DefaultPaintLayerName; + int DefaultRaiseTerrain = 1; + int DefaultLowerTerrain = 1; + FString DefaultEditLayer; + bool DefaultEditLayerClear = false; + FString DefaultEditLayerAfter; + TArray DefaultMeshSegmentData; + + /** + * The mesh socket names on the splines' prims. The index is the near side (0) and far side (1) of the + * segment connection. + */ + FString DefaultConnectionSocketNames[2]; +}; + +// Transient/transactional struct for processing landscape spline output. +struct FHoudiniLandscapeSplineInfo +{ + // OutputObject Id. + FHoudiniOutputObjectIdentifier Identifier; + + // Target output info. + ALandscapeProxy* LandscapeProxy = nullptr; + ALandscape* Landscape = nullptr; + ULandscapeSplinesComponent* SplinesComponent = nullptr; + + + // Package Params. + FHoudiniPackageParams LayerPackageParams; + FHoudiniPackageParams SplineActorPackageParams; + + // Data for World Partition only + ALandscapeSplineActor* LandscapeSplineActor = nullptr; + FName OutputName = NAME_None; + + // Array of curve indices in the HGPO that will be used to create segments for this landscape spline. There can + // be more than one segment per curve. + TArray CurveIndices; + + // An array per-curve that stores the index of the first point (corresponding to the P attribute) for the curve + // info in the HGPO. + TArray PerCurveFirstPointIndex; + + // An array per-curve that stores the number of points for the curve in the HGPO. + TArray PerCurvePointCount; + + // Curve prim and point attributes read from Houdini to apply to ULandscapeSplineControlPoint/Segment. + TArray SplineData; + + // Control points mapped by id that have been created for this splines component. + TMap ControlPointMap; + + // The next control point ID (largest ID seen + 1). + int32 NextControlPointId = 0; + + // Object used to keep track of segments/control points that we create for the FHoudiniOutputObject. + UHoudiniLandscapeSplinesOutput* SplinesOutputObject = nullptr; +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniLandscapeTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniLandscapeTranslator.h" + + +#include "HoudiniApi.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniEngineString.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniInput.h" +#include "HoudiniLandscapeRuntimeUtils.h" +#include "HoudiniLandscapeUtils.h" +#include "HoudiniMaterialTranslator.h" +#include "HoudiniPackageParams.h" +#include "HoudiniRuntimeSettings.h" +#include "HoudiniEngineTimers.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" +#include "Async/ParallelFor.h" +#include "Editor.h" +#include "Engine/AssetManager.h" +#include "Engine/LevelBounds.h" +#include "FileHelpers.h" +#include "HAL/IConsoleManager.h" +#include "LandscapeEdit.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 +#include "LandscapeEditLayer.h" +#endif +#if defined(HOUDINI_USE_PCG) +#include "HoudiniPCGCookable.h" +#endif +#include "LandscapeInfo.h" +#include "LandscapeLayerInfoObject.h" +#include "Misc/Guid.h" +#include "PhysicalMaterials/PhysicalMaterial.h" + +#if WITH_EDITOR + #include "EditorLevelUtils.h" +#endif + +typedef FHoudiniEngineUtils FHUtils; + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +HOUDINI_LANDSCAPE_DEFINE_LOG_CATEGORY(); + +bool +FHoudiniLandscapeTranslator::ProcessLandscapeOutput( + UHoudiniOutput* InOutput, + const TArray& InAllInputLandscapes, + const FString& CoookedLandscapeActorPrefix, + UWorld* InWorld, // Persistent / root world for the landscape + const FHoudiniPackageParams & InPackageParams, + TMap& LandscapeMap, + FHoudiniClearedEditLayers& ClearedLayers, + TArray& OutCreatedPackages) +{ + H_SCOPED_FUNCTION_TIMER(); + + FHoudiniLandscapeSettings LandscapeSettings; + + if (UHoudiniCookable* HC = FHoudiniEngineUtils::GetOuterHoudiniCookable(InOutput)) + { + // Pull settings from the HAC + LandscapeSettings.LocalToWorldTransform = HC->GetComponent() ? HC->GetComponent()->GetComponentToWorld() : FTransform::Identity; + LandscapeSettings.bUseTempLayers = HC->GetLandscapeUseTempLayers(); + LandscapeSettings.TempLayerSuffix = InPackageParams.GetPackageName() + HC->GetCookableGUID().ToString(); + } + else if (USceneComponent* SceneComponent = FHoudiniEngineUtils::GetOuterSceneComponent(InOutput)) + { + // TODO: ? unecessary? + // If attached to a scene component (which a cookable may be), use its transform. + LandscapeSettings.LocalToWorldTransform = SceneComponent->GetComponentToWorld(); + LandscapeSettings.bUseTempLayers = HC->GetLandscapeUseTempLayers(); + } + + //------------------------------------------------------------------------------------------------------------------------------ + // Get a list of layers to update from HDA + //------------------------------------------------------------------------------------------------------------------------------ + + TArray Parts = GetPartsToTranslate(InOutput); + + //------------------------------------------------------------------------------------------------------------------------------ + // Remove any layers from last cook. + //------------------------------------------------------------------------------------------------------------------------------ + + FHoudiniLandscapeRuntimeUtils::DeleteLandscapeCookedData(InOutput); + + //------------------------------------------------------------------------------------------------------------------------------ + // Resolve landscape Actors. This means we either find existing landscapes, if modifying landscapes, or create new landscapes + // as needed. Most landscape operations do not create new packages, however Target Layers do. + //------------------------------------------------------------------------------------------------------------------------------ + + FHoudiniLayersToUnrealLandscapeMapping LandscapeMapping = FHoudiniLandscapeUtils::ResolveLandscapes( + CoookedLandscapeActorPrefix, + InPackageParams, + LandscapeSettings, + LandscapeMap, + Parts, + InWorld, + InAllInputLandscapes); + + OutCreatedPackages += LandscapeMapping.CreatedPackages; + + //------------------------------------------------------------------------------------------------------------------------------ + // Process each layer, cooking to a temporary object. + //------------------------------------------------------------------------------------------------------------------------------ + + TArray AllOutputs; + + for (FHoudiniHeightFieldPartData& Part : Parts) + { + if (!LandscapeMapping.HoudiniLayerToUnrealLandscape.Contains(&Part)) + { + HOUDINI_LOG_WARNING(TEXT("Part was ignored: %s"), *Part.TargetLayerName); + continue; + } + + int Index = LandscapeMapping.HoudiniLayerToUnrealLandscape[&Part]; + FHoudiniUnrealLandscapeTarget& Landscape = LandscapeMapping.TargetLandscapes[Index]; + UHoudiniLandscapeTargetLayerOutput* Result = TranslateHeightFieldPart(InOutput, Landscape, Part, LandscapeSettings, ClearedLayers, InPackageParams); + if (!Result) + continue; + AllOutputs.Add(Result); + + FHoudiniOutputObjectIdentifier OutputObjectIdentifier(Part.HeightField->ObjectId, Part.HeightField->GeoId, Part.HeightField->PartId, "EditableLayer"); + FHoudiniOutputObject& OutputObj = InOutput->GetOutputObjects().FindOrAdd(OutputObjectIdentifier); + OutputObj.OutputObject = Result; + + // Hide baked layer, make cooked layer visible, if using temporary landscape layers + if (Result->BakedEditLayer != Result->CookedEditLayer) + { + int EditLayerIndex = Result->Landscape->GetLayerIndex(FName(Result->BakedEditLayer)); + if (EditLayerIndex != INDEX_NONE) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + Result->Landscape->GetEditLayer(EditLayerIndex)->SetVisible(false, true); +#else + Result->Landscape->SetLayerVisibility(EditLayerIndex, false); +#endif + } + + EditLayerIndex = Result->Landscape->GetLayerIndex(FName(Result->CookedEditLayer)); + if (EditLayerIndex != INDEX_NONE) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + Result->Landscape->GetEditLayer(EditLayerIndex)->SetVisible(true, true); +#else + Result->Landscape->SetLayerVisibility(EditLayerIndex, true); +#endif + } + } + + OutputObj.CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_OUTLINER_FOLDER, Part.BakeOutlinerFolder); + } + + + // ------------------------------------------------------------------------------------------------------------------ + // Once done with processing parts, lock layers + // ------------------------------------------------------------------------------------------------------------------ + + for (UHoudiniLandscapeTargetLayerOutput* Output : AllOutputs) + { + FHoudiniLandscapeUtils::ApplyLocks(Output); + } + + return true; +} + + +TArray FHoudiniLandscapeTranslator::GetPartsToTranslate(UHoudiniOutput* InOutput) +{ + UHoudiniCookable* Cookable = Cast(InOutput->GetOuter()); + + TArray Results; + const TArray& GeoObjects = InOutput->GetHoudiniGeoPartObjects(); + for (const FHoudiniGeoPartObject& PartObj : GeoObjects) + { + if (PartObj.Type != EHoudiniPartType::Volume) + continue; + + FHoudiniHeightFieldPartData PartData; + PartData.ObjectId = PartObj.ObjectId; + PartData.GeoId = PartObj.GeoId; + PartData.PartId = PartObj.PartId; + PartData.UnrealLayerName = PartObj.VolumeLayerName; + PartData.TargetLayerName = PartObj.VolumeName; + PartData.HeightField = &PartObj; + + // If no layer was specified, set the name to the default "Layer" + if (PartData.UnrealLayerName.IsEmpty()) + { + PartData.UnrealLayerName = TEXT("Layer"); + } + + //----------------------------------------------------------------------------------------------------------------------------- + // Transform + //----------------------------------------------------------------------------------------------------------------------------- + + PartData.Transform = FTransform::Identity; + + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + if (FHoudiniApi::GetNodeInfo(FHoudiniEngine::Get().GetSession(), PartObj.GeoId, &NodeInfo) == HAPI_RESULT_SUCCESS) + { + HAPI_Transform HapiTransform; + FHoudiniApi::Transform_Init(&HapiTransform); + + HAPI_Result Result = HAPI_RESULT_FAILURE; + + if(NodeInfo.type == HAPI_NODETYPE_SOP) + { + Result = FHoudiniApi::GetObjectTransform(FHoudiniEngine::Get().GetSession(), NodeInfo.parentId, -1, HAPI_SRT, &HapiTransform); + } + else if(NodeInfo.type == HAPI_NODETYPE_OBJ) + { + Result = FHoudiniApi::GetObjectTransform(FHoudiniEngine::Get().GetSession(), PartObj.GeoId, -1, HAPI_SRT, &HapiTransform); + } + + if (Result == HAPI_RESULT_SUCCESS) + FHoudiniEngineUtils::TranslateHapiTransform(HapiTransform, PartData.Transform); + } + + //----------------------------------------------------------------------------------------------------------------------------- + // Read attributes. + //----------------------------------------------------------------------------------------------------------------------------- + + // Get Edit layer type. + PartData.EditLayerType = HAPI_UNREAL_LANDSCAPE_EDITLAYER_TYPE_BASE; + FHoudiniHapiAccessor Accessor; + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_TYPE); + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.EditLayerType); + + //----------------------------------------------------------------------------------------------------------------------------- + // Get clear type. + //----------------------------------------------------------------------------------------------------------------------------- + + int ClearLayer = 0; + Accessor.Init(PartObj.GeoId, PartObj.PartId,HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_CLEAR); + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, ClearLayer); + PartData.bClearLayer = (ClearLayer == 1); + + + //----------------------------------------------------------------------------------------------------------------------------- + // Treat as Unit Data? + //----------------------------------------------------------------------------------------------------------------------------- + + int UnitData = 0; + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_UNIT_LANDSCAPE_LAYER); + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, UnitData); + PartData.bIsUnitData = (UnitData == 1); + + //----------------------------------------------------------------------------------------------------------------------------- + // After Layer name + //----------------------------------------------------------------------------------------------------------------------------- + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_AFTER); + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.AfterLayerName); + + //----------------------------------------------------------------------------------------------------------------------------- + // Output Mode + //----------------------------------------------------------------------------------------------------------------------------- + + int LandscapeOutputMode = HAPI_UNREAL_LANDSCAPE_OUTPUT_MODE_GENERATE; + FHoudiniLandscapeUtils::GetOutputMode(PartObj.GeoId, PartObj.PartId, HAPI_ATTROWNER_INVALID, LandscapeOutputMode); + PartData.bCreateNewLandscape = LandscapeOutputMode == HAPI_UNREAL_LANDSCAPE_OUTPUT_MODE_GENERATE; + + if (LandscapeOutputMode != HAPI_UNREAL_LANDSCAPE_OUTPUT_MODE_GENERATE && !Cookable->IsLandscapeModificationEnabled()) + { + HOUDINI_LOG_ERROR(TEXT("Ignoring Landscape Modification")); +#if defined(HOUDINI_USE_PCG) + UHoudiniPCGCookable* PCGCookable = Cast(Cookable->GetOuter()); + + if (PCGCookable) + { + FString Error = TEXT("'Ignore Landscape Tracking' must be set on the PCG Component to enable HDA landscape modification."); + PCGCookable->AddCookError(Error); + } +#endif + return {}; + } + + //----------------------------------------------------------------------------------------------------------------------------- + // Landscape Locking / Unlocking + //----------------------------------------------------------------------------------------------------------------------------- + + int LockValue = 0; + + Accessor.Init(PartObj.GeoId, PartObj.PartId,HAPI_UNREAL_ATTRIB_LANDSCAPE_WRITE_LOCKED_LAYERS); + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, LockValue); + PartData.bWriteLockedLayers = LockValue != 0; + + LockValue = 0; + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_LOCK_LAYERS); + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, LockValue); + PartData.bLockLayer = LockValue != 0; + + //----------------------------------------------------------------------------------------------------------------------------- + // HAPI_UNREAL_ATTRIB_LANDSCAPE_NORMALIZE + //----------------------------------------------------------------------------------------------------------------------------- + + int NormalizeValue = 0; + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_NORMALIZE); + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, NormalizeValue); + PartData.bNormalizePaintLayers = NormalizeValue != 0; + + //----------------------------------------------------------------------------------------------------------------------------- + // unreal_landscape_editlayer_subtractive + //----------------------------------------------------------------------------------------------------------------------------- + + int SubtractiveMode = HAPI_UNREAL_LANDSCAPE_EDITLAYER_SUBTRACTIVE_OFF; + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_SUBTRACTIVE); + bool bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, SubtractiveMode); + PartData.bSubtractiveEditLayer = SubtractiveMode == HAPI_UNREAL_LANDSCAPE_EDITLAYER_SUBTRACTIVE_ON; + + //----------------------------------------------------------------------------------------------------------------------------- + // Properties + //----------------------------------------------------------------------------------------------------------------------------- + + FHoudiniEngineUtils::GetGenericPropertiesAttributes(PartObj.GeoId, PartObj.PartId, true, 0, 0, 0, PartData.PropertyAttributes); + + //----------------------------------------------------------------------------------------------------------------------------- + // See if this HAPI Volume is part of a larger landscape, ie. it is a tile. we do this by looking for the + // HAPI_UNREAL_ATTRIB_LANDSCAPE_SIZE attribute, which tells us the overall size of the landscape. + //----------------------------------------------------------------------------------------------------------------------------- + + FHoudiniTileInfo TileInfo; + bool bValid = true; + + // Look for the landscape attribute + TArray LandscapeDimensions; + LandscapeDimensions.SetNumZeroed(2); + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SIZE); + bValid &= Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 2, LandscapeDimensions, 0, 1); + + + HAPI_AttributeInfo PositionInfo; + FHoudiniApi::AttributeInfo_Init(&PositionInfo); + + // Get the center of the tile - this is stored in the "P" attribute. + + TArray TIleCenterRelativeToOrigin; + TIleCenterRelativeToOrigin.SetNumZeroed(3); + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_POSITION); + bValid &= Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, TIleCenterRelativeToOrigin); + + if (bValid && TIleCenterRelativeToOrigin.Num() == 3) + { + // Convert "center" of tile (supplied by HAPI) to global lower corner (x,y) + // of entire landscape. + float TileX = PartData.HeightField->VolumeInfo.XLength; + float TileY = PartData.HeightField->VolumeInfo.YLength; + float HalfTileX = TileX * 0.5f; + float HalfTileY = TileY * 0.5f; + float CornerX = TIleCenterRelativeToOrigin[0] - HalfTileX; + float CornerY = TIleCenterRelativeToOrigin[2] - HalfTileY; + float CenterX = CornerX + (LandscapeDimensions[0]) * 0.5f; + float CenterY = CornerY + (LandscapeDimensions[1]) * 0.5f; + + TileInfo.TileStart.X = static_cast(CenterX); + TileInfo.TileStart.Y = static_cast(CenterY); + TileInfo.LandscapeDimensions.X = LandscapeDimensions[0]; + TileInfo.LandscapeDimensions.Y = LandscapeDimensions[1]; + + // A valid tile is stored. + PartData.TileInfo = TileInfo; + } + + //----------------------------------------------------------------------------------------------------------------------------- + // If creating a new landscape, look for size info. + //----------------------------------------------------------------------------------------------------------------------------- + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_PARTITION_GRID_SIZE); + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.SizeInfo.WorldPartitionGridSize); + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SECTION_SIZE); + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.SizeInfo.NumQuadsPerSection); + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SECTIONS_PER_COMPONENT); + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.SizeInfo.NumSectionsPerComponent); + + // Make sure both Sections Per Component and Section Size were specified. + bool bSectionSizeSet = PartData.SizeInfo.NumQuadsPerSection != 0; + bool bSectionsPerComponentSet = PartData.SizeInfo.NumSectionsPerComponent != 0; + if ((bSectionSizeSet && !bSectionsPerComponentSet) || (bSectionsPerComponentSet && !bSectionSizeSet)) + { + HOUDINI_LOG_ERROR(TEXT("Both " HAPI_UNREAL_ATTRIB_LANDSCAPE_SECTION_SIZE " and " HAPI_UNREAL_ATTRIB_LANDSCAPE_SECTIONS_PER_COMPONENT " must be set. Ignoring.")); + PartData.SizeInfo.NumQuadsPerSection = 0; + PartData.SizeInfo.NumSectionsPerComponent = 0; + } + else + { + bool bValidSizes = true; + if (!FMath::IsPowerOfTwo(PartData.SizeInfo.NumQuadsPerSection + 1)) + { + HOUDINI_LOG_ERROR(TEXT(HAPI_UNREAL_ATTRIB_LANDSCAPE_SECTION_SIZE " must be 1 less than a power of two.")); + bValidSizes = false; + } + if (PartData.SizeInfo.NumSectionsPerComponent < 0 || PartData.SizeInfo.NumSectionsPerComponent > 2) + { + HOUDINI_LOG_ERROR(TEXT(HAPI_UNREAL_ATTRIB_LANDSCAPE_SECTIONS_PER_COMPONENT " must be 1 or 2.")); + bValidSizes = false; + } + if (!bValidSizes) + { + PartData.SizeInfo.NumQuadsPerSection = 0; + PartData.SizeInfo.NumSectionsPerComponent = 0; + } + } + + //----------------------------------------------------------------------------------------------------------------------------- + // At this point we have all the information to determine the landscapes size in Unreal. + //----------------------------------------------------------------------------------------------------------------------------- + + // Start with the size of the height field Part. + PartData.SizeInfo.UnrealGridDimensions = FHoudiniLandscapeUtils::GetVolumeDimensionsInUnrealSpace(PartObj); + + // If we have a tile, use the specified size to override. + if (PartData.TileInfo.IsSet()) + PartData.SizeInfo.UnrealGridDimensions = PartData.TileInfo.GetValue().LandscapeDimensions; + + // Determine Quads Per section and Num Sections Per Component if they were not specified. + if (PartData.SizeInfo.NumQuadsPerSection == 0 || PartData.SizeInfo.NumSectionsPerComponent == 0) + { + FHoudiniLandscapeUtils::CalcLandscapeSizeFromHeightFieldSize(PartData.SizeInfo.UnrealGridDimensions.X, PartData.SizeInfo.UnrealGridDimensions.Y, PartData.SizeInfo); + + } + + // if the SizeInfo is invalid, print errors and exit. + if (PartData.SizeInfo.Validate()) + { + HOUDINI_LOG_ERROR(TEXT("Skipping part %s as it has generated invalid Unreal size info."), *PartObj.VolumeName ); + continue; + } + + //----------------------------------------------------------------------------------------------------------------------------- + // Weight blend. + //----------------------------------------------------------------------------------------------------------------------------- + + int NonWeightBlend = HAPI_UNREAL_LANDSCAPE_LAYER_NOWEIGHTBLEND_OFF; + + TSet NonWeightBlendedLayers = FHoudiniLandscapeUtils::GetNonWeightBlendedLayerNames(PartObj); + if (NonWeightBlendedLayers.Contains(PartData.TargetLayerName)) + NonWeightBlend = HAPI_UNREAL_LANDSCAPE_LAYER_NOWEIGHTBLEND_ON; + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_NOWEIGHTBLEND); + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, NonWeightBlend); + + // Note layer stores Weight Blended, not NOT Weight Blended. I can't deal with double negatives. + PartData.bIsWeightBlended = NonWeightBlend == HAPI_UNREAL_LANDSCAPE_LAYER_NOWEIGHTBLEND_OFF; + + //--------------------------------------------------------------------------------------------------------------------------------- + // Layer Info Object, if it exists. + //--------------------------------------------------------------------------------------------------------------------------------- + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_INFO); + bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.LayerInfoObjectName); + + //----------------------------------------------------------------------------------------------------------------------------- + // target landscape name. + //----------------------------------------------------------------------------------------------------------------------------- + + PartData.TargetLandscapeName.Empty(); + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2); + bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.TargetLandscapeName); + + if (PartData.TargetLandscapeName.IsEmpty()) + { + // The previous implementation of the "Edit Layer" mode used the Shared Landscape Actor name. If the (new) + // Edit Layer Target attribute wasn't specified, check whether the old attribute is present. + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SHARED_ACTOR_NAME); + bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.TargetLandscapeName); + } + + if (PartData.TargetLandscapeName.IsEmpty()) + { + // The previous implementation of the "Edit Layer" mode used the Shared Landscape Actor name. If the (new) + // Edit Layer Target attribute wasn't specified, check whether the old attribute is present. + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_EDITLAYER_TARGET); + bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.TargetLandscapeName); + } + + if (PartData.TargetLandscapeName.IsEmpty()) + { + // No name was specified, set a default depending on whether the Output is creating or modifying a landscape + PartData.TargetLandscapeName = (LandscapeOutputMode == HAPI_UNREAL_LANDSCAPE_OUTPUT_MODE_GENERATE) ? FString("Landscape") : FString("Input0"); + } + + //----------------------------------------------------------------------------------------------------------------------------- + // Height Range + //----------------------------------------------------------------------------------------------------------------------------- + + FHoudiniMinMax MinMax; + + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings->MarshallingLandscapesForceMinMaxValues) + { + MinMax.MinValue = HoudiniRuntimeSettings->MarshallingLandscapesForcedMinValue; + MinMax.MaxValue = HoudiniRuntimeSettings->MarshallingLandscapesForcedMaxValue; + PartData.HeightRange = MinMax; + } + else + { + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_MIN); + bool bHasMin = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, MinMax.MinValue); + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_MAX); + bool bHasMax = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, MinMax.MaxValue); + + if (bHasMin != bHasMax) + HOUDINI_LOG_ERROR(TEXT("Must specify both " HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_MIN " and " HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_MAX)); + + if (bHasMin && bHasMax) + PartData.HeightRange = MinMax; + } + + + //----------------------------------------------------------------------------------------------------------------------------- + // Material name + //----------------------------------------------------------------------------------------------------------------------------- + + // Regular material + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_MATERIAL); + bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.Materials.Material); + + if (PartData.Materials.Material.IsEmpty()) + { + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_MATERIAL_INSTANCE); + bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.Materials.Material); + PartData.Materials.bCreateMaterialInstance = !PartData.Materials.Material.IsEmpty(); + } + + // Hole material + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_MATERIAL_HOLE); + bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.Materials.HoleMaterial); + + if (PartData.Materials.HoleMaterial.IsEmpty()) + { + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_MATERIAL_HOLE_INSTANCE); + bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.Materials.HoleMaterial); + } + + // Physical Material + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_PHYSICAL_MATERIAL); + bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.Materials.PhysicalMaterial); + + //----------------------------------------------------------------------------------------------------------------------------- + // Bake folders + //----------------------------------------------------------------------------------------------------------------------------- + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_BAKE_OUTLINER_FOLDER); + bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.BakeOutlinerFolder); + + Accessor.Init(PartObj.GeoId, PartObj.PartId, HAPI_UNREAL_ATTRIB_BAKE_FOLDER); + bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, PartData.BakeFolder); + + //----------------------------------------------------------------------------------------------------------------------------- + // Add new layer. + //----------------------------------------------------------------------------------------------------------------------------- + + Results.Add(std::move(PartData)); + } + return Results; + +} + + + +UHoudiniLandscapeTargetLayerOutput* +FHoudiniLandscapeTranslator::TranslateHeightFieldPart( + UHoudiniOutput* OwningOutput, + FHoudiniUnrealLandscapeTarget& Landscape, + FHoudiniHeightFieldPartData& Part, + const FHoudiniLandscapeSettings& LandscapeSettings, + FHoudiniClearedEditLayers& ClearedLayers, + const FHoudiniPackageParams& InPackageParams) +{ + H_SCOPED_FUNCTION_TIMER(); + + enum TargetLayerType + { + Height, Visibility, Paint + }; + + //---------------------------------------------------------------------------------------------------------------------------------------- + // Resolve Landscape Actors + //---------------------------------------------------------------------------------------------------------------------------------------- + + ALandscapeProxy& LandscapeProxy = *Landscape.Proxy.Get(); + ALandscape * OutputLandscape = nullptr; + + OutputLandscape = LandscapeProxy.GetLandscapeActor(); + if (!IsValid(OutputLandscape)) + { + HOUDINI_LOG_WARNING(TEXT("Could not retrieve the landscape actor for: %s"), *(Part.TargetLandscapeName)); + return nullptr; + } + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 7 + if (!OutputLandscape->bCanHaveLayersContent) + { + HOUDINI_LOG_WARNING(TEXT("Target landscape does not have edit layers enabled. Cooking will directly affect the landscape: %s"), *(Part.TargetLandscapeName)); + } +#endif + + // ----------------------------------------------------------------------------------------------------------------- + // Set Layer names. The Baked layer name is always what the user specifies; If we are modifying an existing landscape, + // use temporary names if specified or user name baked names. + // ----------------------------------------------------------------------------------------------------------------- + + FString BakedLayerName = Part.UnrealLayerName; + + // For the cooked name, but the layer name first so it is easier to read in the Landscape Editor UI. + auto LayerPackageParams = InPackageParams; + FString CookedLayerName = BakedLayerName; + + if (LandscapeSettings.bUseTempLayers) + { + CookedLayerName = CookedLayerName + FString(" : ") + LandscapeSettings.TempLayerSuffix; + } + + // ------------------------------------------------------------------------------------------------------------------ + // Make sure the target layer exists before we do anything else. If its missing, we can't do anything + //------------------------------------------------------------------------------------------------------------------- + + ULandscapeLayerInfoObject* TargetLayerInfo = OutputLandscape->GetLandscapeInfo()->GetLayerInfoByName(FName(Part.TargetLayerName)); + + if (!TargetLayerInfo && Part.TargetLayerName != "height" && Part.TargetLayerName != "visibility" && Part.TargetLayerName != "mask") + { + HOUDINI_LOG_WARNING(TEXT("Could not find target layer: %s. This should be 'height', 'visibility' or the name of a layer in the material."), *(Part.TargetLayerName)); + return nullptr; + } + + // ------------------------------------------------------------------------------------------------------------------ + // Create the Edit Layer if it doesn't exist + // ------------------------------------------------------------------------------------------------------------------ + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + const FLandscapeLayer* UnrealEditLayer = nullptr; +#else + FLandscapeLayer* UnrealEditLayer = nullptr; +#endif + bool bWasLocked = false; + bool bLayerWasCreated = false; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 7 + if (OutputLandscape->bCanHaveLayersContent) +#endif + { + UnrealEditLayer = FHoudiniLandscapeUtils::GetOrCreateEditLayer(OutputLandscape, FName(CookedLayerName), &bLayerWasCreated); + if (!UnrealEditLayer) + return nullptr; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + bWasLocked = UnrealEditLayer->EditLayer->IsLocked(); +#else + bWasLocked = UnrealEditLayer->bLocked; +#endif + if (bWasLocked) + { + if (Part.bWriteLockedLayers) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + UnrealEditLayer->EditLayer->SetLocked(false, true); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + OutputLandscape->SetLayerLocked(OutputLandscape->GetLayerIndex(UnrealEditLayer->Name), false); +#else + UnrealEditLayer->bLocked = false; +#endif + } + else + { + HOUDINI_LOG_ERROR(TEXT("Cannot write to locked Edit Layer: %s"), *CookedLayerName); + return nullptr; + } + } + + + // Move this layer after another layer if required. + if (!Part.AfterLayerName.IsEmpty()) + { + UnrealEditLayer = FHoudiniLandscapeUtils::MoveEditLayerAfter(OutputLandscape, FName(CookedLayerName), FName(Part.AfterLayerName)); + } + } + int UnrealEditLayerIndex = INDEX_NONE; + if (UnrealEditLayer != nullptr) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + UnrealEditLayerIndex = OutputLandscape->GetLayerIndex(UnrealEditLayer->EditLayer->GetName()); +#else + UnrealEditLayerIndex = OutputLandscape->GetLayerIndex(UnrealEditLayer->Name); +#endif + + // ------------------------------------------------------------------------------------------------------------------ + // Clear layer + // ------------------------------------------------------------------------------------------------------------------ + + TargetLayerType LayerType = TargetLayerType::Paint; + if (Part.TargetLayerName == "height") + LayerType = TargetLayerType::Height; + else if (Part.TargetLayerName == "visibility") + LayerType = TargetLayerType::Visibility; + + if (UnrealEditLayer != nullptr && +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 7 + OutputLandscape->bHasLayersContent && +#endif + Part.bClearLayer && + !ClearedLayers.Contains(CookedLayerName, Part.TargetLayerName)) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FGuid LayerGUID = UnrealEditLayer->EditLayer->GetGuid(); +#else + FGuid LayerGUID = UnrealEditLayer->Guid; +#endif + if (LayerType != TargetLayerType::Paint) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + OutputLandscape->ClearEditLayer(LayerGUID, nullptr, ELandscapeToolTargetTypeFlags::Heightmap); +#else + OutputLandscape->ClearLayer(LayerGUID, nullptr, ELandscapeClearMode::Clear_Heightmap); +#endif + } + else + { + OutputLandscape->ClearPaintLayer(LayerGUID, TargetLayerInfo); + } + ClearedLayers.Add(CookedLayerName, Part.TargetLayerName); + } + + + if (LayerType == TargetLayerType::Paint && TargetLayerInfo == nullptr) + { + // The target layer doesn't exist, so report an error and do nothing. The target layers are defined by the material + // and trying to create new ones is probably not correct. Note, this is different from what we do if a Layer is missing. + + // mask is very common, so silently ignore it. + if (Part.TargetLayerName != "mask") + HOUDINI_LOG_WARNING(TEXT("Tried to export to a target layer called %s but it does not exist"), *Part.TargetLayerName); + + return nullptr; + } + + // ------------------------------------------------------------------------------------------------------------------ + // Layer controls + // ------------------------------------------------------------------------------------------------------------------ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 7 + if (OutputLandscape->bHasLayersContent) +#endif + { + bool bLayerSubractive = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + PRAGMA_DISABLE_INTERNAL_WARNINGS + + ULandscapeEditLayerBase* EditLayer = OutputLandscape->GetEditLayer(UnrealEditLayerIndex); + if (EditLayer != nullptr) + { + // Great, these are the Epic recommended replacements for Is/SetLayerBlendSubstractive, + // But this throws a warning anyway... Disable/Enable internal warnings temporarily +PRAGMA_DISABLE_INTERNAL_WARNINGS + const bool* AllocationBlend = EditLayer->GetWeightmapLayerAllocationBlend().Find(TargetLayerInfo); + if (AllocationBlend != nullptr && (Part.bSubtractiveEditLayer != *AllocationBlend)) + { + EditLayer->AddOrUpdateWeightmapAllocationLayerBlend(TargetLayerInfo, Part.bSubtractiveEditLayer, true); + } +PRAGMA_ENABLE_INTERNAL_WARNINGS + } + + PRAGMA_ENABLE_INTERNAL_WARNINGS +#else + if (Part.bSubtractiveEditLayer != OutputLandscape->IsLayerBlendSubstractive(UnrealEditLayerIndex, TargetLayerInfo)) + OutputLandscape->SetLayerSubstractiveBlendStatus(UnrealEditLayerIndex, Part.bSubtractiveEditLayer, TargetLayerInfo); +#endif + + if (TargetLayerInfo) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + // bNoWeightBlend has been replaced by BlendMethod (false is ELandscapeTargetLayerBlendMethod::FinalWeightBlending, true is ELandscapeTargetLayerBlendMethod::None) + TargetLayerInfo->SetBlendMethod(!Part.bIsWeightBlended ? ELandscapeTargetLayerBlendMethod::None : ELandscapeTargetLayerBlendMethod::FinalWeightBlending, false); +#else + TargetLayerInfo->bNoWeightBlend = !Part.bIsWeightBlended; +#endif + } + + // ------------------------------------------------------------------------------------------------------------------ + // Fetch the actual height field data + // ------------------------------------------------------------------------------------------------------------------ + + // Fetch the height field data from Houdini into Unreal Space. This data may have already been fetched during landscape + // creation, so if it's already present. + + const bool bFetchData = !Landscape.bWasCreated || LayerType != TargetLayerType::Height; + + FHoudiniHeightFieldData HeightFieldData = FHoudiniLandscapeUtils::FetchVolumeInUnrealSpace( + *Part.HeightField, + Part.SizeInfo.UnrealGridDimensions, + bFetchData); + + // The transform we get from Houdini should be relative to the HDA: + HeightFieldData.Transform = HeightFieldData.Transform * LandscapeSettings.LocalToWorldTransform; + + // If a new landscape was created, resize the layer to match the created landscape size. (We resize the landscape if it does + // not fit one of Unreal's predetermined sizes. Only do this for non-tiles. + if (Landscape.bWasCreated && !Part.TileInfo.IsSet() && bFetchData) + { + if (Landscape.Dimensions != HeightFieldData.Dimensions) + HeightFieldData = FHoudiniLandscapeUtils::ReDimensionLandscape(HeightFieldData, Landscape.Dimensions); + } + + auto Extents = FHoudiniLandscapeUtils::GetExtents(OutputLandscape, HeightFieldData); + + // ------------------------------------------------------------------------------------------------------------------ + // Is a paint layer or visibility layer + // ------------------------------------------------------------------------------------------------------------------ + + ULandscapeInfo* TargetLandscapeInfo = OutputLandscape->GetLandscapeInfo(); + if (LayerType == TargetLayerType::Paint || LayerType == TargetLayerType::Visibility) + { + FGuid LayerGUID; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 7 + if (OutputLandscape->bCanHaveLayersContent) +#endif + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + LayerGUID = UnrealEditLayer->EditLayer->GetGuid(); +#else + LayerGUID = UnrealEditLayer->Guid; +#endif + } + + bool bExceededRange = FHoudiniLandscapeUtils::NormalizePaintLayers(HeightFieldData.Values, Part.bNormalizePaintLayers); + if (bExceededRange) + HOUDINI_LOG_WARNING(TEXT("Target layer %s contains values outside the range 0 to 1."), *Part.TargetLayerName); + + TArray Values; + Values.SetNum(HeightFieldData.Values.Num()); + + ParallelFor(Values.Num(), [&](int Index) + { + float Value = HeightFieldData.Values[Index]; + Values[Index] = static_cast(Value * 255); + }); + + FScopedSetLandscapeEditingLayer Scope(OutputLandscape, LayerGUID, [&] { OutputLandscape->RequestLayersContentUpdate(ELandscapeLayerUpdateMode::Update_All); }); + + if (LayerType == TargetLayerType::Visibility) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + TAlphamapAccessor AlphaAccessor(OutputLandscape->GetLandscapeInfo(), ALandscapeProxy::VisibilityLayer); +#else + FAlphamapAccessor AlphaAccessor(OutputLandscape->GetLandscapeInfo(), ALandscapeProxy::VisibilityLayer); +#endif + + AlphaAccessor.SetData( + Extents.Min.X, Extents.Min.Y, Extents.Max.X, Extents.Max.Y, + Values.GetData(), + ELandscapeLayerPaintingRestriction::None); + } + else + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + TAlphamapAccessor AlphaAccessor(OutputLandscape->GetLandscapeInfo(), TargetLayerInfo); +#else + FAlphamapAccessor AlphaAccessor(OutputLandscape->GetLandscapeInfo(), TargetLayerInfo); +#endif + + AlphaAccessor.SetData( + Extents.Min.X, Extents.Min.Y, Extents.Max.X, Extents.Max.Y, + Values.GetData(), + ELandscapeLayerPaintingRestriction::None); + } + } + + // ------------------------------------------------------------------------------------------------------------------ + // Is this the height layer and we did not create a new landscape, apply it. + // ------------------------------------------------------------------------------------------------------------------ + + if (LayerType == TargetLayerType::Height && !Landscape.bWasCreated) + { + + TArray QuantizedData = FHoudiniLandscapeUtils::ConvertHeightFieldData(OutputLandscape, HeightFieldData.Values); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FGuid LayerGUID = UnrealEditLayer->EditLayer->GetGuid(); +#else + FGuid LayerGUID = UnrealEditLayer->Guid; +#endif + FScopedSetLandscapeEditingLayer Scope(OutputLandscape, LayerGUID, [&] { OutputLandscape->ForceUpdateLayersContent(); }); + + FLandscapeEditDataInterface LandscapeEdit(TargetLandscapeInfo); + FHeightmapAccessor HeightMapAccessor(TargetLandscapeInfo); + HeightMapAccessor.SetData( + Extents.Min.X, Extents.Min.Y, Extents.Max.X, Extents.Max.Y, + QuantizedData.GetData()); + } + + if (bWasLocked && UnrealEditLayer) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + UnrealEditLayer->EditLayer->SetLocked(true, true); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + OutputLandscape->SetLayerLocked(OutputLandscape->GetLayerIndex(UnrealEditLayer->Name), true); +#else + UnrealEditLayer->bLocked = true; +#endif + } + + // ------------------------------------------------------------------------------------------------------------------ + // We successfully did what we came to, return an Object + // ------------------------------------------------------------------------------------------------------------------ + + UHoudiniLandscapeTargetLayerOutput * Obj = NewObject(OwningOutput); + Obj->BakedEditLayer = BakedLayerName; + Obj->CookedEditLayer = CookedLayerName; + Obj->Landscape = OutputLandscape; + Obj->LandscapeProxy = &LandscapeProxy; + Obj->Extents = Extents; + Obj->bCreatedLandscape = Part.bCreateNewLandscape; + Obj->TargetLayer = Part.TargetLayerName; + Obj->bClearLayer = Part.bClearLayer; + Obj->BakedLandscapeName = Landscape.BakedName.ToString(); + Obj->LayerInfoObjects = Landscape.CreatedLayerInfoObjects; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + Obj->bCookedLayerRequiresBaking = (CookedLayerName != BakedLayerName); +#else + Obj->bCookedLayerRequiresBaking = OutputLandscape->bCanHaveLayersContent && (CookedLayerName != BakedLayerName); +#endif + Obj->BakeFolder = Part.BakeFolder; + Obj->MaterialInstance = Part.MaterialInstance; + Obj->bWriteLockedLayers = Part.bWriteLockedLayers; + Obj->bLockLayer = Part.bLockLayer; + Obj->PropertyAttributes = Part.PropertyAttributes; + Obj->bLayerWasCreated = bLayerWasCreated; + return Obj; + + +} + +template +TArray ResampleData(const TArray& Data, int32 OldWidth, int32 OldHeight, int32 NewWidth, int32 NewHeight) +{ + TArray Result; + Result.Empty(NewWidth * NewHeight); + Result.AddUninitialized(NewWidth * NewHeight); + + const float XScale = (float)(OldWidth - 1) / (NewWidth - 1); + const float YScale = (float)(OldHeight - 1) / (NewHeight - 1); + for (int32 Y = 0; Y < NewHeight; ++Y) + { + for (int32 X = 0; X < NewWidth; ++X) + { + const float OldY = Y * YScale; + const float OldX = X * XScale; + const int32 X0 = FMath::FloorToInt(OldX); + const int32 X1 = FMath::Min(FMath::FloorToInt(OldX) + 1, OldWidth - 1); + const int32 Y0 = FMath::FloorToInt(OldY); + const int32 Y1 = FMath::Min(FMath::FloorToInt(OldY) + 1, OldHeight - 1); + const T& Original00 = Data[Y0 * OldWidth + X0]; + const T& Original10 = Data[Y0 * OldWidth + X1]; + const T& Original01 = Data[Y1 * OldWidth + X0]; + const T& Original11 = Data[Y1 * OldWidth + X1]; + Result[Y * NewWidth + X] = FMath::BiLerp(Original00, Original10, Original01, Original11, FMath::Fractional(OldX), FMath::Fractional(OldY)); + } + } + + return Result; +} + +template +void ExpandData(T* OutData, const T* InData, + int32 OldMinX, int32 OldMinY, int32 OldMaxX, int32 OldMaxY, + int32 NewMinX, int32 NewMinY, int32 NewMaxX, int32 NewMaxY) +{ + const int32 OldWidth = OldMaxX - OldMinX + 1; + const int32 OldHeight = OldMaxY - OldMinY + 1; + const int32 NewWidth = NewMaxX - NewMinX + 1; + const int32 NewHeight = NewMaxY - NewMinY + 1; + const int32 OffsetX = NewMinX - OldMinX; + const int32 OffsetY = NewMinY - OldMinY; + + for (int32 Y = 0; Y < NewHeight; ++Y) + { + const int32 OldY = FMath::Clamp(Y + OffsetY, 0, OldHeight - 1); + + // Pad anything to the left + const T PadLeft = InData[OldY * OldWidth + 0]; + for (int32 X = 0; X < -OffsetX; ++X) + { + OutData[Y * NewWidth + X] = PadLeft; + } + + // Copy one row of the old data + { + const int32 X = FMath::Max(0, -OffsetX); + const int32 OldX = FMath::Clamp(X + OffsetX, 0, OldWidth - 1); + FMemory::Memcpy(&OutData[Y * NewWidth + X], &InData[OldY * OldWidth + OldX], FMath::Min(OldWidth, NewWidth) * sizeof(T)); + } + + const T PadRight = InData[OldY * OldWidth + OldWidth - 1]; + for (int32 X = -OffsetX + OldWidth; X < NewWidth; ++X) + { + OutData[Y * NewWidth + X] = PadRight; + } + } +} + +template +TArray ExpandData(const TArray& Data, + int32 OldMinX, int32 OldMinY, int32 OldMaxX, int32 OldMaxY, + int32 NewMinX, int32 NewMinY, int32 NewMaxX, int32 NewMaxY, + int32* PadOffsetX = nullptr, int32* PadOffsetY = nullptr) +{ + const int32 NewWidth = NewMaxX - NewMinX + 1; + const int32 NewHeight = NewMaxY - NewMinY + 1; + + TArray Result; + Result.Empty(NewWidth * NewHeight); + Result.AddUninitialized(NewWidth * NewHeight); + + ExpandData(Result.GetData(), Data.GetData(), + OldMinX, OldMinY, OldMaxX, OldMaxY, + NewMinX, NewMinY, NewMaxX, NewMaxY); + + // Return the padding so we can offset the terrain position after + if (PadOffsetX) + *PadOffsetX = NewMinX; + + if (PadOffsetY) + *PadOffsetY = NewMinY; + + return Result; +} + +const FHoudiniGeoPartObject* +FHoudiniLandscapeTranslator::GetHoudiniHeightFieldFromOutput( + UHoudiniOutput* InOutput, + const bool bMatchEditLayer, + const FName& EditLayerName) +{ + if (!IsValid(InOutput)) + return nullptr; + + if (InOutput->GetHoudiniGeoPartObjects().Num() < 1) + return nullptr; + + for (const FHoudiniGeoPartObject& HGPO : InOutput->GetHoudiniGeoPartObjects()) + { + if (HGPO.Type != EHoudiniPartType::Volume) + continue; + + FHoudiniVolumeInfo CurVolumeInfo = HGPO.VolumeInfo; + if (!CurVolumeInfo.Name.Contains("height")) + continue; + + if (bMatchEditLayer) + { + if (!HGPO.bHasEditLayers) + continue; + FName LayerName(HGPO.VolumeLayerName); + if (!LayerName.IsEqual(EditLayerName)) + continue; + } + + // We're only handling single values for now + if (CurVolumeInfo.TupleSize != 1) + { + HOUDINI_LOG_ERROR(TEXT("Failed to create landscape output: the height volume has an invalide tuple size!")); + return nullptr; + } + + // Terrains always have a ZSize of 1. + if (CurVolumeInfo.ZLength != 1) + { + HOUDINI_LOG_ERROR(TEXT("Failed to create landscape output: the height volume's z length is not 1!")); + return nullptr; + } + + // Values should be float + if (!CurVolumeInfo.bIsFloat) + { + HOUDINI_LOG_ERROR(TEXT("Failed to create landscape output, the height volume's data is not stored as floats!")); + return nullptr; + } + + return &HGPO; + } + + return nullptr; +} +void +FHoudiniLandscapeTranslator::CalcHeightFieldsArrayGlobalZMinZMax( + const TArray< FHoudiniGeoPartObject > & InHeightfieldArray, + TMap& GlobalMinimums, + TMap& GlobalMaximums, + bool bShouldEmptyMaps) +{ + if (bShouldEmptyMaps) + { + GlobalMinimums.Empty(); + GlobalMaximums.Empty(); + } + + // Get runtime settings. + float ForcedZMin = 0.0f; + float ForcedZMax = 0.0f; + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + bool bUseForcedMinMax = (HoudiniRuntimeSettings && HoudiniRuntimeSettings->MarshallingLandscapesForceMinMaxValues); + if(bUseForcedMinMax) + { + ForcedZMin = HoudiniRuntimeSettings->MarshallingLandscapesForcedMinValue; + ForcedZMax = HoudiniRuntimeSettings->MarshallingLandscapesForcedMaxValue; + } + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + TArray FloatData; + + for (const FHoudiniGeoPartObject& CurrentHeightfield: InHeightfieldArray) + { + // Get the current Height Field GeoPartObject + if ( CurrentHeightfield.VolumeInfo.TupleSize != 1) + continue; + + // Retrieve node id from geo part. + HAPI_NodeId NodeId = CurrentHeightfield.GeoId; + if (NodeId == -1) + continue; + + // Retrieve the VolumeInfo + HAPI_VolumeInfo CurrentVolumeInfo; + FHoudiniApi::VolumeInfo_Init(&CurrentVolumeInfo); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetVolumeInfo( + FHoudiniEngine::Get().GetSession(), + NodeId, CurrentHeightfield.PartId, + &CurrentVolumeInfo)) + continue; + + // Retrieve the volume name. + FString VolumeName; + FHoudiniEngineString HoudiniEngineStringPartName(CurrentVolumeInfo.nameSH); + HoudiniEngineStringPartName.ToFString(VolumeName); + + bool bHasMinAttr = false; + bool bHasMaxAttr = false; + + // If this volume has an attribute defining a minimum value use it as is. + FloatData.Empty(); + + FHoudiniHapiAccessor LayerMinAccessor(CurrentHeightfield.GeoId, CurrentHeightfield.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_MIN); + LayerMinAccessor.GetInfo(AttributeInfo); + AttributeInfo.tupleSize = 1; + + if (LayerMinAccessor.GetAttributeData(HAPI_ATTROWNER_INVALID, FloatData, 0, 1)) + { + if (FloatData.Num() > 0) + { + GlobalMinimums.Add(VolumeName, FloatData[0]); + bHasMinAttr = true; + } + } + + // If this volume has an attribute defining maximum value use it as is. + FloatData.Empty(); + FHoudiniHapiAccessor LayerMaxAccessor(CurrentHeightfield.GeoId, CurrentHeightfield.PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_LAYER_MAX); + LayerMaxAccessor.GetInfo(AttributeInfo); + AttributeInfo.tupleSize = 1; + + if (LayerMaxAccessor.GetAttributeData(HAPI_ATTROWNER_INVALID, FloatData, 0, 1)) + { + if (FloatData.Num() > 0) + { + GlobalMaximums.Add(VolumeName, FloatData[0]); + bHasMaxAttr = true; + } + } + + if (!bHasMinAttr || !bHasMaxAttr) + { + // We haven't specified both min/max values + // Unreal's Z values are Y in Houdini + float ymin, ymax; + + if (bUseForcedMinMax) + { + // First, see if we should use the forced min/max values from the settings + ymin = ForcedZMin; + ymax = ForcedZMax; + } + else + { + // Get the min/max value from the volume + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetVolumeBounds( + FHoudiniEngine::Get().GetSession(), + NodeId, + CurrentHeightfield.PartId, + nullptr, &ymin, nullptr, + nullptr, &ymax, nullptr, + nullptr, nullptr, nullptr)) + continue; + } + + if (!bHasMinAttr) + { + // Read the global min value for this volume + if (!GlobalMinimums.Contains(VolumeName)) + { + GlobalMinimums.Add(VolumeName, ymin); + } + else + { + // Update the min if necessary + if (ymin < GlobalMinimums[VolumeName]) + GlobalMinimums[VolumeName] = ymin; + } + } + + if (!bHasMaxAttr) + { + // Read the global max value for this volume + if (!GlobalMaximums.Contains(VolumeName)) + { + GlobalMaximums.Add(VolumeName, ymax); + } + else + { + // Update the max if necessary + if (ymax > GlobalMaximums[VolumeName]) + GlobalMaximums[VolumeName] = ymax; + } + } + } + } +} + +bool FHoudiniLandscapeCreationInfo::Validate() +{ + bool bError = false; + + if (this->NumQuadsPerSection == 0) + { + HOUDINI_LOG_ERROR(TEXT("Error creating landscape, NumQuadsPerSection is zero")); + bError = true; + } + if (this->NumSectionsPerComponent == 0) + { + HOUDINI_LOG_ERROR(TEXT("Error creating landscape, NumSectionsPerComponent is zero")); + bError = true; + } + if (this->UnrealGridDimensions.X == 0) + { + HOUDINI_LOG_ERROR(TEXT("Error creating landscape, X Size is zero")); + bError = true; + } + if (this->UnrealGridDimensions.Y == 0) + { + HOUDINI_LOG_ERROR(TEXT("Error creating landscape, Y Size is zero")); + bError = true; + } + return bError; +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngine/Private/HoudiniLandscapeTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +// This file contains the "public" API for translating Houdini Height Fields to Unreal Landscapes. + +#include "Landscape.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniOutput.h" +#include "EngineUtils.h" +#include "HoudiniPackageParams.h" + +struct FHoudiniLandscapeSettings; +class UHoudiniAssetComponent; +class ULandscapeLayerInfoObject; +struct FHoudiniGenericAttribute; +struct FHoudiniPackageParams; +struct FHoudiniHeightFieldPartData; +struct FHoudiniUnrealLandscapeTarget; + +struct FHoudiniLandscapeCreationInfo +{ + FIntPoint UnrealGridDimensions; + int NumSectionsPerComponent = 0; + int NumQuadsPerSection = 0; + int WorldPartitionGridSize = 4; + + bool Validate(); +}; + +struct HOUDINIENGINE_API FHoudiniLandscapeTranslator +{ + static TArray GetPartsToTranslate(UHoudiniOutput* InOutput); + + static bool ProcessLandscapeOutput( + UHoudiniOutput* InOutput, + const TArray& InAllInputLandscapes, + const FString& CookedLandscapeActorPrefix, + UWorld* World, + const FHoudiniPackageParams& InPackageParams, + TMap & LandscapeMap, + FHoudiniClearedEditLayers & ClearedLayers, + TArray& OutCreatedPackages); + + static const FHoudiniGeoPartObject* GetHoudiniHeightFieldFromOutput( + UHoudiniOutput* InOutput, + const bool bMatchEditLayer, + const FName& EditLayerName); + + // Returns the min/max values per layer/volume for an array of volumes/heightfields + static void CalcHeightFieldsArrayGlobalZMinZMax( + const TArray< FHoudiniGeoPartObject > & InHeightfieldArray, + TMap& GlobalMinimums, + TMap& GlobalMaximums, + bool bShouldEmptyMaps=true); + +private: + + static UHoudiniLandscapeTargetLayerOutput* TranslateHeightFieldPart( + UHoudiniOutput* OwningOutput, + FHoudiniUnrealLandscapeTarget& Landscape, + FHoudiniHeightFieldPartData& Part, + const FHoudiniLandscapeSettings& LandscapeSettings, + FHoudiniClearedEditLayers& ClearedLayers, + const FHoudiniPackageParams& InPackageParams); +}; + + + +``` + +`Source/HoudiniEngine/Private/HoudiniLandscapeUtils.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniLandscapeUtils.h" +#include "LandscapeEdit.h" +#include "HoudiniCookable.h" +#include "Landscape.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "UObject/UObjectGlobals.h" +#include "LandscapeDataAccess.h" +#include "HoudiniAsset.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineTimers.h" +#include "HoudiniEngineUtils.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "HoudiniPackageParams.h" +#include "PhysicalMaterials/PhysicalMaterial.h" +#include "LandscapeConfigHelper.h" +#include "Factories/MaterialInstanceConstantFactoryNew.h" +#include "Materials/MaterialInstanceConstant.h" +#include "HoudiniMaterialTranslator.h" +#include "PackageTools.h" +#include "LandscapeSplineControlPoint.h" +#include "LandscapeSplineSegment.h" +#include "LandscapeUtils.h" +#include "Async/ParallelFor.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + #include "LandscapeEditLayer.h" +#endif + +TSet +FHoudiniLandscapeUtils::GetEditLayers(UHoudiniOutput& Output) +{ + TSet Result; + + for (auto& OutputObjectPair : Output.GetOutputObjects()) + { + const FHoudiniOutputObject& PrevObj = OutputObjectPair.Value; + if (PrevObj.OutputObject->IsA()) + { + UHoudiniLandscapeTargetLayerOutput* Layer = Cast(PrevObj.OutputObject); + Result.Add(Layer); + } + } + return Result; +} + +TSet +FHoudiniLandscapeUtils::GetCookedLandscapeLayers(UHoudiniCookable& HC, ALandscape& Landscape) +{ + TSet Layers; + for(int OutputIndex = 0; OutputIndex < HC.GetNumOutputs(); OutputIndex++) + { + UHoudiniOutput* Output = HC.GetOutputAt(OutputIndex); + if (!IsValid(Output)) + continue; + + TSet LandscapeEditLayers = GetEditLayers(*Output); + for(UHoudiniLandscapeTargetLayerOutput* Layer : LandscapeEditLayers) + { + if (Layer->Landscape == &Landscape) + Layers.Add(Layer->CookedEditLayer); + } + } + return Layers; +} + +void +FHoudiniLandscapeUtils::SetNonCookedLayersVisibility(UHoudiniCookable& HC, ALandscape& Landscape, bool bVisible) +{ + FString LayerName; + TSet CookedLayers = GetCookedLandscapeLayers(HC, Landscape); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + TArrayView Layers = Landscape.GetLayersConst(); + for (int LayerIndex = 0; LayerIndex < Landscape.GetLayersConst().Num(); LayerIndex++) + { + LayerName = Layers[LayerIndex].EditLayer->GetName().ToString(); + if (!CookedLayers.Contains(LayerName)) + { + // Non cooked Layer + Layers[LayerIndex].EditLayer->SetVisible(bVisible, true); + } + } +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + TArrayView Layers = Landscape.GetLayers(); + for (int LayerIndex = 0; LayerIndex < Landscape.GetLayerCount(); LayerIndex++) + { + LayerName = Layers[LayerIndex].Name.ToString(); + if (!CookedLayers.Contains(LayerName)) + { + // Non cooked Layer + Landscape.SetLayerVisibility(LayerIndex, bVisible); + } + } +#else + for (int LayerIndex = 0; LayerIndex < Landscape.GetLayerCount(); LayerIndex++) + { + LayerName = Landscape.LandscapeLayers[LayerIndex].Name.ToString(); + if (!CookedLayers.Contains(LayerName)) + { + // Non cooked Layer + Landscape.SetLayerVisibility(LayerIndex, bVisible); + } + } +#endif +} + +void +FHoudiniLandscapeUtils::SetCookedLayersVisibility(UHoudiniCookable& HC, ALandscape& Landscape, bool bVisible) +{ + FString LayerName; + TSet CookedLayers = GetCookedLandscapeLayers(HC, Landscape); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + TArrayView Layers = Landscape.GetLayersConst(); + for (int LayerIndex = 0; LayerIndex < Landscape.GetLayersConst().Num(); LayerIndex++) + { + LayerName = Layers[LayerIndex].EditLayer->GetName().ToString(); + if (CookedLayers.Contains(LayerName)) + { + // Cooked Layer + Layers[LayerIndex].EditLayer->SetVisible(bVisible, true); + } + } +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + TArrayView Layers = Landscape.GetLayers(); + for (int LayerIndex = 0; LayerIndex < Landscape.GetLayerCount(); LayerIndex++) + { + LayerName = Layers[LayerIndex].Name.ToString(); + if (CookedLayers.Contains(LayerName)) + { + // Cooked Layer + Landscape.SetLayerVisibility(LayerIndex, bVisible); + } + } +#else + for (int LayerIndex = 0; LayerIndex < Landscape.GetLayerCount(); LayerIndex++) + { + LayerName = Landscape.LandscapeLayers[LayerIndex].Name.ToString(); + if (CookedLayers.Contains(LayerName)) + { + // Cooked Layer + Landscape.SetLayerVisibility(LayerIndex, bVisible); + } + } +#endif +} + +void FHoudiniLandscapeUtils::RealignHeightFieldData(TArray& Data, float ZeroPoint, float Scale) +{ + H_SCOPED_FUNCTION_TIMER(); + + for(int Index = 0; Index < Data.Num(); Index++) + { + Data[Index] = Data[Index] * Scale + ZeroPoint; + } +} + + +bool FHoudiniLandscapeUtils::ClampHeightFieldData(TArray& Data, float MinValue, float MaxValue) +{ + H_SCOPED_FUNCTION_TIMER(); + + TArray Result; + bool bClamped = false; + Result.SetNumUninitialized(Data.Num()); + for (int Index = 0; Index < Result.Num(); Index++) + { + float Value = Data[Index]; + Data[Index] = FMath::Clamp(Value, MinValue, MaxValue); + bClamped |= (Data[Index] != Value); + } + return bClamped; +} + +TArray +FHoudiniLandscapeUtils::QuantizeNormalizedDataTo16Bit(const TArray& Data) +{ + H_SCOPED_FUNCTION_TIMER(); + + TArray Result; + Result.SetNumUninitialized(Data.Num()); + ParallelFor(Result.Num(), [&](int Index) { + int Quantized = static_cast(Data[Index] * 65535); + Result[Index] = FMath::Clamp(Quantized, 0, 65535); + }); + + return Result; +} + +float +FHoudiniLandscapeUtils::GetLandscapeHeightRangeInCM(const ALandscape& Landscape) +{ + float Scale = Landscape.GetTransform().GetScale3D().Z; + + return Scale * 256.0f; + +} + +TArray +FHoudiniLandscapeUtils::GetHeightData( + ALandscape* Landscape, + const FHoudiniExtents& Extents, +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + const FLandscapeLayer* EditLayer) +#else + FLandscapeLayer* EditLayer) +#endif +{ + int DiffX = 1 + Extents.Max.X - Extents.Min.X; + int DiffY = 1 + Extents.Max.Y - Extents.Min.Y; + int NumPoints = DiffX * DiffY; + + TArray Values; + Values.SetNum(NumPoints); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FGuid LayerGuid = EditLayer->EditLayer->GetGuid(); +#else + FGuid LayerGuid = EditLayer->Guid; +#endif + FScopedSetLandscapeEditingLayer Scope(Landscape, LayerGuid, [&] { /*Landscape->RequestLayersContentUpdate(ELandscapeLayerUpdateMode::Update_All); */}); + + FLandscapeEditDataInterface LandscapeEdit(Landscape->GetLandscapeInfo()); + LandscapeEdit.SetShouldDirtyPackage(false); + LandscapeEdit.GetHeightDataFast(Extents.Min.X, Extents.Min.Y, Extents.Max.X, Extents.Max.Y, Values.GetData(), 0); + + return Values; +} + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 +const FLandscapeLayer* +#else +FLandscapeLayer* +#endif +FHoudiniLandscapeUtils::GetOrCreateEditLayer(ALandscape* Landscape, const FName& LayerName, bool* bCreated) +{ + if (bCreated) + *bCreated = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + const FLandscapeLayer* UnrealEditLayer = GetEditLayer(Landscape, LayerName); +#else + FLandscapeLayer* UnrealEditLayer = GetEditLayer(Landscape, LayerName); +#endif + if (UnrealEditLayer == nullptr) + { + int EditLayerIndex = Landscape->CreateLayer(LayerName); + if (EditLayerIndex == INDEX_NONE) + { + HOUDINI_LOG_ERROR(TEXT("Could not create edit layer %s"), *LayerName.ToString()); + return nullptr; + } + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + UnrealEditLayer = Landscape->GetLayerConst(EditLayerIndex); +#else + UnrealEditLayer = Landscape->GetLayer(EditLayerIndex); +#endif + if (bCreated) + *bCreated = true; + } + return UnrealEditLayer; +} + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 +const FLandscapeLayer* +#else +FLandscapeLayer* +#endif +FHoudiniLandscapeUtils::GetEditLayer(ALandscape* Landscape, const FName& LayerName) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + // Landscape layers are mandatory since 5.7 + // Nothing to do +#else + if (!Landscape->bCanHaveLayersContent) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + return Landscape->GetLayerConst(0); +#else + return Landscape->GetLayer(0); +#endif +#endif + + int32 EditLayerIndex = Landscape->GetLayerIndex(LayerName); + if (EditLayerIndex == INDEX_NONE) + return nullptr; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + return Landscape->GetLayerConst(EditLayerIndex); +#else + return Landscape->GetLayer(EditLayerIndex); +#endif +} + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 +const FLandscapeLayer* +#else +FLandscapeLayer* +#endif +FHoudiniLandscapeUtils::MoveEditLayerAfter(ALandscape* Landscape, const FName& LayerName, const FName& AfterLayerName) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + // Landscape layers are mandatory since 5.7 + // Nothing to do +#else + if (!Landscape->bCanHaveLayersContent) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + return Landscape->GetLayerConst(0); +#else + return Landscape->GetLayer(0); +#endif +#endif + + int32 EditLayerIndex = Landscape->GetLayerIndex(LayerName); + int32 NewLayerIndex = Landscape->GetLayerIndex(AfterLayerName); + if (NewLayerIndex == INDEX_NONE || EditLayerIndex == INDEX_NONE) + return nullptr; + + if (NewLayerIndex < EditLayerIndex) + { + NewLayerIndex += 1; + } + Landscape->ReorderLayer(EditLayerIndex, NewLayerIndex); + + // Ensure we have the correct layer/index + EditLayerIndex = Landscape->GetLayerIndex(LayerName); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + return Landscape->GetLayerConst(EditLayerIndex); +#else + return Landscape->GetLayer(EditLayerIndex); +#endif +} + +TArray +FHoudiniLandscapeUtils::GetLayerData(ALandscape* Landscape, const FHoudiniExtents& Extents, const FName& EditLayerName, const FName& TargetLayerName) +{ + int DiffX = 1 + Extents.Max.X - Extents.Min.X; + int DiffY = 1 + Extents.Max.Y - Extents.Min.Y; + int NumPoints = DiffX * DiffY; + + TArray Values; + Values.SetNum(NumPoints); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + const FLandscapeLayer* EditLayer = FHoudiniLandscapeUtils::GetEditLayer(Landscape, EditLayerName); +#else + FLandscapeLayer* EditLayer = FHoudiniLandscapeUtils::GetEditLayer(Landscape, EditLayerName); +#endif + ULandscapeLayerInfoObject* TargetLayerInfo = Landscape->GetLandscapeInfo()->GetLayerInfoByName(TargetLayerName); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FGuid LayerGuid = EditLayer->EditLayer->GetGuid(); +#else + FGuid LayerGuid = EditLayer->Guid; +#endif + FScopedSetLandscapeEditingLayer Scope(Landscape, LayerGuid, [&] { /*Landscape->RequestLayersContentUpdate(ELandscapeLayerUpdateMode::Update_All); */}); + + FLandscapeEditDataInterface LandscapeEdit(Landscape->GetLandscapeInfo()); + LandscapeEdit.SetShouldDirtyPackage(false); + LandscapeEdit.GetWeightDataFast(TargetLayerInfo, Extents.Min.X, Extents.Min.Y, Extents.Max.X, Extents.Max.Y, Values.GetData(), 0); + + return Values; +} + + +bool +FHoudiniLandscapeUtils::CalcLandscapeSizeFromHeightFieldSize( + int32 ProposedUnrealSizeX, int32 ProposedUnrealSizeY, + FHoudiniLandscapeCreationInfo & Info) +{ + // TODO: We already know Proposed Size will fit, so some of this function is redundant. + + if ((ProposedUnrealSizeX < 2) || (ProposedUnrealSizeY < 2)) + return false; + + Info.NumSectionsPerComponent = 1; + Info.NumQuadsPerSection = 1; + Info.UnrealGridDimensions.X = -1; + Info.UnrealGridDimensions.Y = -1; + + // Unreal's default sizes + int32 SectionSizes[] = { 7, 15, 31, 63, 127, 255 }; + int32 NumSections[] = { 1, 2 }; + + // Component count used to calculate the final size of the landscape + int32 ComponentsCountX = 1; + int32 ComponentsCountY = 1; + + // Lambda for clamping the number of component in X/Y + auto ClampLandscapeSize = [&]() + { + // Max size is either whole components below 8192 verts, or 32 components + ComponentsCountX = FMath::Clamp(ComponentsCountX, 1, FMath::Min(32, FMath::FloorToInt(8191.0f / (float)(Info.NumSectionsPerComponent * Info.NumQuadsPerSection)))); + ComponentsCountY = FMath::Clamp(ComponentsCountY, 1, FMath::Min(32, FMath::FloorToInt(8191.0f / (float)(Info.NumSectionsPerComponent * Info.NumQuadsPerSection)))); + }; + + // Try to find a section size and number of sections that exactly matches the dimensions of the heightfield + bool bFoundMatch = false; + for (int32 SectionSizesIdx = UE_ARRAY_COUNT(SectionSizes) - 1; SectionSizesIdx >= 0; SectionSizesIdx--) + { + for (int32 NumSectionsIdx = UE_ARRAY_COUNT(NumSections) - 1; NumSectionsIdx >= 0; NumSectionsIdx--) + { + int32 SectionSize = SectionSizes[SectionSizesIdx]; + int32 NumSection = NumSections[NumSectionsIdx]; + int32 Total = SectionSize * NumSection; + if (((ProposedUnrealSizeX - 1) % Total == 0 && ((ProposedUnrealSizeX - 1) / Total) <= 32 && + ((ProposedUnrealSizeY - 1) % Total) == 0 && ((ProposedUnrealSizeY - 1) / Total) <= 32)) + { + bFoundMatch = true; + Info.NumQuadsPerSection = SectionSize; + Info.NumSectionsPerComponent = NumSection; + ComponentsCountX = (ProposedUnrealSizeX - 1) / Total; + ComponentsCountY = (ProposedUnrealSizeY - 1) / Total; + ClampLandscapeSize(); + break; + } + } + if (bFoundMatch) + { + break; + } + } + + if (!bFoundMatch) + { + // if there was no exact match, try increasing the section size until we encompass the whole height field + const int32 CurrentSectionSize = Info.NumQuadsPerSection; + const int32 CurrentNumSections = Info.NumSectionsPerComponent; + for (int32 SectionSizesIdx = 0; SectionSizesIdx < UE_ARRAY_COUNT(SectionSizes); SectionSizesIdx++) + { + if (SectionSizes[SectionSizesIdx] < CurrentSectionSize) + { + continue; + } + + const int32 ComponentsX = FMath::DivideAndRoundUp((ProposedUnrealSizeX - 1), SectionSizes[SectionSizesIdx] * CurrentNumSections); + const int32 ComponentsY = FMath::DivideAndRoundUp((ProposedUnrealSizeY - 1), SectionSizes[SectionSizesIdx] * CurrentNumSections); + if (ComponentsX <= 32 && ComponentsY <= 32) + { + bFoundMatch = true; + Info.NumQuadsPerSection = SectionSizes[SectionSizesIdx]; + ComponentsCountX = ComponentsX; + ComponentsCountY = ComponentsY; + ClampLandscapeSize(); + break; + } + } + } + + if (!bFoundMatch) + { + // if the heightmap is very large, fall back to using the largest values we support + const int32 MaxSectionSize = SectionSizes[UE_ARRAY_COUNT(SectionSizes) - 1]; + const int32 MaxNumSubSections = NumSections[UE_ARRAY_COUNT(NumSections) - 1]; + const int32 ComponentsX = FMath::DivideAndRoundUp((ProposedUnrealSizeX - 1), MaxSectionSize * MaxNumSubSections); + const int32 ComponentsY = FMath::DivideAndRoundUp((ProposedUnrealSizeY - 1), MaxSectionSize * MaxNumSubSections); + + bFoundMatch = true; + Info.NumQuadsPerSection = MaxSectionSize; + Info.NumSectionsPerComponent = MaxNumSubSections; + ComponentsCountX = ComponentsX; + ComponentsCountY = ComponentsY; + ClampLandscapeSize(); + } + + if (!bFoundMatch) + { + // Using default size just to not crash.. + Info.UnrealGridDimensions.X = 512; + Info.UnrealGridDimensions.Y = 512; + Info.NumSectionsPerComponent = 1; + Info.NumQuadsPerSection = 511; + ComponentsCountX = 1; + ComponentsCountY = 1; + } + else + { + // Calculating the desired size + int32 QuadsPerComponent = Info.NumSectionsPerComponent * Info.NumQuadsPerSection; + + Info.UnrealGridDimensions.X = ComponentsCountX * QuadsPerComponent + 1; + Info.UnrealGridDimensions.Y = ComponentsCountY * QuadsPerComponent + 1; + } + + return bFoundMatch; +} + +FHoudiniLayersToUnrealLandscapeMapping +FHoudiniLandscapeUtils::ResolveLandscapes( + const FString& CookedLandscapePrefix, + const FHoudiniPackageParams& PackageParams, + const FHoudiniLandscapeSettings& LandscapeSettings, + TMap& LandscapeMap, + TArray& Parts, + UWorld * World, + const TArray& LandscapeInputs) +{ + H_SCOPED_FUNCTION_TIMER(); + + FHoudiniLayersToUnrealLandscapeMapping Result; + + //-------------------------------------------------------------------------------------------------------------------------- + // Go through each layer and find the Landscape actor. If "Create New Landscape" is specified then do nothing and create + // the actor landscape later; this is so we can create one new landscape of the correct name. + //-------------------------------------------------------------------------------------------------------------------------- + + TMap > LandscapesToCreate; + TMap > ExistingLandscapes; + + for(int Index = 0; Index < Parts.Num(); Index++) + { + FHoudiniHeightFieldPartData& Part = Parts[Index]; + if (Part.bCreateNewLandscape) + { + if (LandscapeMap.Contains(Part.TargetLandscapeName)) + { + // reuse a previously cooked landscape. We assume that any primitve on the same HDA want's the same + // actor if they have the same name. + //LandscapeMap[Part.TargetLandscapeName]; + Result.HoudiniLayerToUnrealLandscape.Add(&Part, Result.TargetLandscapes.Num()); + FHoudiniUnrealLandscapeTarget LandscapeTarget; + LandscapeTarget.Proxy = LandscapeMap[Part.TargetLandscapeName]; + LandscapeTarget.bWasCreated = false; + Result.TargetLandscapes.Add(LandscapeTarget); + } + else + { + auto & LandscapeParts = LandscapesToCreate.FindOrAdd(Part.TargetLandscapeName); + + if (LandscapeParts.Contains(Part.TargetLayerName)) + { + HOUDINI_LOG_WARNING(TEXT("Duplicate Layer \"%s\" for landscape \"%s\" was ignored."), *Part.TargetLayerName, *Part.TargetLandscapeName); + } + else + { + LandscapeParts.Add(Part.TargetLayerName, &Part); + } + } + } + else + { + ALandscapeProxy * LandscapeProxy = FindTargetLandscapeProxy(Part.TargetLandscapeName, World, LandscapeInputs); + if (!IsValid(LandscapeProxy) || !LandscapeProxy->IsA()) + { + HOUDINI_LOG_ERROR(TEXT("%s is not a valid Landscape Actor."), *Part.TargetLandscapeName); + continue; + } + + Result.HoudiniLayerToUnrealLandscape.Add(&Part, Result.TargetLandscapes.Num()); + FHoudiniUnrealLandscapeTarget LandscapeTarget; + LandscapeTarget.Proxy = LandscapeProxy; + LandscapeTarget.bWasCreated = false; + Result.TargetLandscapes.Add(LandscapeTarget); + auto& PartArray = ExistingLandscapes.FindOrAdd(LandscapeProxy); + PartArray.Add(Part.TargetLayerName, &Part); + } + } + + //-------------------------------------------------------------------------------------------------------------------------- + // Create new actors, stored off above. + //-------------------------------------------------------------------------------------------------------------------------- + + for(auto It : LandscapesToCreate) + { + const FString LandscapeActorName = It.Key; + + TMap & PartsForLandscape = It.Value; + + //--------------------------------------------------------------------------------------------------------------------------------- + // Look for a height field and use that to initialize the height field to zero. + // If no height field exists then use the first layer. + //--------------------------------------------------------------------------------------------------------------------------------- + + FHoudiniHeightFieldPartData* HeightPart = GetPartWithHeightData(PartsForLandscape); + if (!HeightPart) + { + HeightPart = PartsForLandscape.CreateIterator().Value(); + HOUDINI_BAKING_WARNING(TEXT("No height primitve was found, using %s"), *BaseLayer->TargetLayer); + } + + auto LayerPackageParams = PackageParams; + LayerPackageParams.ObjectId = HeightPart->ObjectId; + LayerPackageParams.GeoId = HeightPart->GeoId; + LayerPackageParams.PartId = HeightPart->PartId; + LayerPackageParams.SplitStr = CookedLandscapePrefix + LandscapeActorName; + FString CookingActorName = LayerPackageParams.GetPackageName(); + + // Spawn the new Landscape Actor. Note that we only create ALandscape actors here, not Proxies or Streaming Proxies + // as was the case in World Composition. The name is a temporary one derived from the HDA name. + + FActorSpawnParameters SpawnParameters; + SpawnParameters.Name = FName(CookingActorName); + SpawnParameters.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested; + ALandscape * LandscapeActor = World->SpawnActor(SpawnParameters); + if (!LandscapeActor) + { + HOUDINI_BAKING_ERROR(TEXT("Failed to create actor: %s"), *LandscapeActorName); + continue; + } + LandscapeMap.Add(LandscapeActorName, LandscapeActor); + + //--------------------------------------------------------------------------------------------------------------------------------- + // Set world transform of the landscape. In this version of the code we no longer parent the landscape to the HDA as it breaks + // up World Partition. + //--------------------------------------------------------------------------------------------------------------------------------- + + FTransform LocalHeightFieldTransform = GetHeightFieldTransformInUnrealSpace(HeightPart->HeightField->VolumeInfo, HeightPart->SizeInfo.UnrealGridDimensions); + + if (HeightPart->TileInfo.IsSet()) + { + // Adjust the transform of the Landscape actor we are creating if this is a single tile. + LocalHeightFieldTransform = GetLandscapeActorTransformFromTileTransform(LocalHeightFieldTransform, HeightPart->TileInfo.GetValue()); + } + FTransform LandscapeTransform = LocalHeightFieldTransform * HeightPart->Transform * LandscapeSettings.LocalToWorldTransform; + LandscapeActor->SetActorTransform(LandscapeTransform); + + //--------------------------------------------------------------------------------------------------------------------------------- + // Initial settings for landscapes created by Houdini in Unreal. + //--------------------------------------------------------------------------------------------------------------------------------- + + LandscapeActor->PreEditChange(nullptr); + LandscapeActor->SetLandscapeGuid(FGuid::NewGuid()); + LandscapeActor->bCastStaticShadow = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + // Landscape layers are mandatory since 5.7 - Nothing to do +#else + LandscapeActor->bCanHaveLayersContent = true; +#endif + + //--------------------------------------------------------------------------------------------------------------------------------- + // Order is important: Assign materials, create landscape info, Create TargetLayerInfo assets. + //--------------------------------------------------------------------------------------------------------------------------------- + + TArray CreatedPackages; + + HeightPart->MaterialInstance = AssignGraphicsMaterialsToLandscape( + LandscapeActor, + HeightPart->Materials, + PackageParams, + CreatedPackages); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 7 + LandscapeActor->CreateLandscapeInfo(); +#endif + //--------------------------------------------------------------------------------------------------------------------------------- + // Fetch the data for the height field and use to create the landscape. + //--------------------------------------------------------------------------------------------------------------------------------- + + FHoudiniHeightFieldData HeightFieldData = FHoudiniLandscapeUtils::FetchVolumeInUnrealSpace( + *HeightPart->HeightField, HeightPart->SizeInfo.UnrealGridDimensions, true); + + HeightFieldData = FHoudiniLandscapeUtils::ReDimensionLandscape(HeightFieldData, HeightPart->SizeInfo.UnrealGridDimensions); + + FHoudiniLandscapeUtils::AdjustLandscapeTransformToLayerHeight(*LandscapeActor, *HeightPart, HeightFieldData); + + TArray QuantizedData = FHoudiniLandscapeUtils::ConvertHeightFieldData(LandscapeActor, HeightFieldData.Values); + + + ImportLandscape(LandscapeActor, HeightPart->SizeInfo, QuantizedData); + + + TArray CreateLayerInfoObjects = + CreateTargetLayerInfoAssets(LandscapeActor, PackageParams, PartsForLandscape, CreatedPackages); + + // Rename the default height layer if needed. + const FString DefaultLayerName = TEXT("Layer"); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + // Landscape layers are mandatory since 5.7 + if (HeightPart->UnrealLayerName != DefaultLayerName) + { + LandscapeActor->GetEditLayer(0)->SetName(FName(HeightPart->UnrealLayerName), true); + } +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + if (LandscapeActor->HasLayersContent() && HeightPart->UnrealLayerName != DefaultLayerName) + { + LandscapeActor->GetEditLayer(0)->SetName(FName(HeightPart->UnrealLayerName), true); + } +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 5 + if (LandscapeActor->HasLayersContent() && HeightPart->UnrealLayerName != DefaultLayerName) + { + LandscapeActor->SetLayerName(0, FName(HeightPart->UnrealLayerName)); + } +#else + if (!LandscapeActor->LandscapeLayers.IsEmpty() && HeightPart->UnrealLayerName != DefaultLayerName) + { + LandscapeActor->LandscapeLayers[0].Name = FName(HeightPart->UnrealLayerName); + } +#endif + + //--------------------------------------------------------------------------------------------------------------------------------- + // Set label. Doing this earlier results in Unreal errors as the Landscape is not fully initialized. + //--------------------------------------------------------------------------------------------------------------------------------- + + LandscapeActor->SetActorLabel(CookingActorName); + + + //--------------------------------------------------------------------------------------------------------------------------------- + // World Partition + //--------------------------------------------------------------------------------------------------------------------------------- + + SetWorldPartitionGridSize(LandscapeActor, HeightPart->SizeInfo.WorldPartitionGridSize); + + //--------------------------------------------------------------------------------------------------------------------------------- + // and store the results. + //--------------------------------------------------------------------------------------------------------------------------------- + + for (auto PartIt : PartsForLandscape) + { + FHoudiniHeightFieldPartData* Part = PartIt.Value; + Result.HoudiniLayerToUnrealLandscape.Add(Part, Result.TargetLandscapes.Num()); + } + FHoudiniUnrealLandscapeTarget Output; + Output.Proxy = LandscapeActor; + Output.BakedName = FName(LandscapeActorName); + Output.CreatedLayerInfoObjects = CreateLayerInfoObjects; + Output.bWasCreated = true; + Output.Dimensions = HeightPart->SizeInfo.UnrealGridDimensions; + Result.TargetLandscapes.Add(Output); + Result.CreatedPackages = CreatedPackages; + } + + //-------------------------------------------------------------------------------------------------------------------------- + // For existing landscape, go through and apply new data. eg. material assignments + //-------------------------------------------------------------------------------------------------------------------------- + + for (auto It : ExistingLandscapes) + { + ALandscapeProxy* LandscapeProxy = It.Key; + TMap& PartsForLandscape = It.Value; + + ApplyMaterialsFromParts(LandscapeProxy, PartsForLandscape, PackageParams, Result.CreatedPackages); + + } + + return Result; +} + +void FHoudiniLandscapeUtils::ImportLandscape(ALandscape* LandscapeActor, const FHoudiniLandscapeCreationInfo& Info, const TArray& Values) +{ + TMap> HeightMapDataPerLayers; + HeightMapDataPerLayers.Add(FGuid(), Values); + + // Create Material Layer data. + + TArray CustomImportLayerInfos; + TMap> MaterialLayerDataPerLayer; + MaterialLayerDataPerLayer.Add(FGuid(), CustomImportLayerInfos); + + // Now call the UE Import() function to actually create the layer + LandscapeActor->Import( + LandscapeActor->GetLandscapeGuid(), + 0, 0, Info.UnrealGridDimensions.X - 1, Info.UnrealGridDimensions.Y - 1, + Info.NumSectionsPerComponent, + Info.NumQuadsPerSection, + HeightMapDataPerLayers, + NULL, + MaterialLayerDataPerLayer, + ELandscapeImportAlphamapType::Layered +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + ,MakeArrayView({}) +#endif + ); +} + +ALandscapeProxy* FHoudiniLandscapeUtils::FindTargetLandscapeProxy(const FString& ActorName, UWorld* World, + const TArray& LandscapeInputs) +{ + int32 InputIndex = INDEX_NONE; + if (ActorName.StartsWith(TEXT("Input"))) + { + // Extract the numeric value after 'Input'. + FString IndexStr; + ActorName.Split(TEXT("Input"), nullptr, &IndexStr); + if (IndexStr.IsNumeric()) + { + InputIndex = FPlatformString::Atoi(*IndexStr); + } + } + + if (InputIndex != INDEX_NONE) + { + if (!LandscapeInputs.IsValidIndex(InputIndex)) + return nullptr; + return LandscapeInputs[InputIndex]; + } + + return FHoudiniEngineRuntimeUtils::FindActorInWorldByLabelOrName(World, ActorName); +} + +FHoudiniHeightFieldPartData* FHoudiniLandscapeUtils::GetPartWithHeightData(TMap & Parts) +{ + FString PartNameForHeight = "height"; + + if (Parts.Find(PartNameForHeight)) + return Parts[PartNameForHeight]; + else + return nullptr; +} + +FTransform FHoudiniLandscapeUtils::GetHeightFieldTransformInUnrealSpace(const FHoudiniVolumeInfo& VolumeInfo, const FIntPoint & UnrealDimensions) +{ + FTransform Result; + Result.SetIdentity(); + + Result.SetLocation(VolumeInfo.Transform.GetLocation()); + + // Unreal has a X/Y resolution of 1m per point while Houdini is dependent on the height field's grid spacing + // Swap Y/Z axis from H to UE. We must also take into account that the landscape may have been resized. + + FVector LandscapeScale; + LandscapeScale.X = VolumeInfo.Transform.GetScale3D().X * 2.0f * (VolumeInfo.YLength - 1) / (UnrealDimensions.X - 1); + LandscapeScale.Y = VolumeInfo.Transform.GetScale3D().Z * 2.0f * (VolumeInfo.XLength - 1) / (UnrealDimensions.Y - 1); + + // NOTE: Ignore vertical scaling intentionally; the height field grid scale is also applied to the volume's scale.y + // received from HAPI, however the actual height values do not change. So we can ignore it. + LandscapeScale.Z = 1.0f; + LandscapeScale *= 100.0f; + + Result.SetScale3D(LandscapeScale); + + // Rotate the vector using the H rotation + FRotator Rotator = VolumeInfo.Transform.GetRotation().Rotator(); + // We need to compensate for the "default" HF Transform + Rotator.Yaw -= 90.0f; + Rotator.Roll += 90.0f; + + // Only rotate if the rotator is far from zero + if (!Rotator.IsNearlyZero()) + Result.SetRotation(FQuat(Rotator)); + + return Result; + +} + + +UMaterialInterface* +FHoudiniLandscapeUtils::AssignGraphicsMaterialsToLandscape( + ALandscapeProxy* LandscapeProxy, + FHoudiniLandscapeMaterial& Materials, + const FHoudiniPackageParams& Params, + TArray & CreatedPackages) +{ + UMaterialInterface* MaterialInstance = nullptr; + + if (!Materials.Material.IsEmpty()) + { + UMaterialInterface* Material = Cast(StaticLoadObject(UMaterialInterface::StaticClass(), + nullptr, *(Materials.Material), nullptr, LOAD_NoWarn, nullptr)); + + if (!IsValid(Material)) + { + HOUDINI_LOG_ERROR(TEXT("Could not load material: %s"), *Materials.Material); + } + + if (Materials.bCreateMaterialInstance && IsValid(Material)) + { + MaterialInstance = CreateMaterialInstance(LandscapeProxy->GetName(), Material, Params, CreatedPackages); + Material = MaterialInstance; + } + + LandscapeProxy->LandscapeMaterial = Material; + } + + if (!Materials.HoleMaterial.IsEmpty()) + { + UMaterialInterface* Material = Cast(StaticLoadObject(UMaterialInterface::StaticClass(), + nullptr, *(Materials.HoleMaterial), nullptr, LOAD_NoWarn, nullptr)); + LandscapeProxy->LandscapeHoleMaterial = Material; + + if (!IsValid(Material)) + { + HOUDINI_LOG_ERROR(TEXT("Could not load material: %s"), *Materials.HoleMaterial); + } + + } + + LandscapeProxy->GetLandscapeActor()->ForceUpdateLayersContent(); + + return MaterialInstance; +} + +void FHoudiniLandscapeUtils::AssignPhysicsMaterialsToLandscape(ALandscapeProxy* LandscapeProxy, const FString& LayerName, FHoudiniLandscapeMaterial& Materials) +{ + if (Materials.PhysicalMaterial.IsEmpty()) + return; + + UPhysicalMaterial* Material = Cast(StaticLoadObject(UPhysicalMaterial::StaticClass(), + nullptr, *(Materials.PhysicalMaterial), nullptr, LOAD_NoWarn, nullptr)); + + ULandscapeLayerInfoObject* TargetLayerInfo = LandscapeProxy->GetLandscapeInfo()->GetLayerInfoByName(FName(LayerName)); + if (!TargetLayerInfo) + { + HOUDINI_LOG_ERROR(TEXT("Missing target layer: %s"), *LayerName); + return; + } + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + TargetLayerInfo->SetPhysicalMaterial(Material, true); +#else + TargetLayerInfo->PhysMaterial = Material; +#endif + + +} + +TArray FHoudiniLandscapeUtils::CreateTargetLayerInfoAssets( + ALandscapeProxy* LandscapeProxy, + const FHoudiniPackageParams& PackageParams, + TMap& PartsForLandscape, + TArray& CreatedPackages) +{ +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 5 + TArray Results; + + auto* LandscapeInfo = LandscapeProxy->GetLandscapeInfo(); + + FHoudiniPackageParams LayerPackageParams = PackageParams; + + TSet LayerNames; + LandscapeProxy->GetLandscapeInfo()->ForEachLandscapeProxy([&LayerNames](ALandscapeProxy* LandscapeProxy) + { + LayerNames.Append(LandscapeProxy->RetrieveTargetLayerNamesFromMaterials()); + return true; + }); + + auto LandscapeTargetLayers = LandscapeProxy->GetTargetLayers(); + for(FName TargetLayerName : LayerNames) + { + // if the landscape info already exists, don't create one. + if(LandscapeTargetLayers.Find(TargetLayerName)) + continue; + + // if the user did not specify the target info, do not create it + if(!PartsForLandscape.Contains(TargetLayerName.ToString())) + continue; + + // Normally we create packages with a name based off geo/part ids. But this doesn't make sense here + // as we're creating a layer info based off the material and name of the landscape. + ALandscape* ParentLandscape = LandscapeProxy->GetLandscapeActor(); + FString PackageName = ParentLandscape->GetName() + FString("_") + TargetLayerName.ToString(); + FString PackagePath = LayerPackageParams.GetPackagePath(); + UPackage* Package = nullptr; + ULandscapeLayerInfoObject* LandscapeLayerInfo = FindOrCreateLandscapeLayerInfoObject(TargetLayerName.ToString(), PackagePath, PackageName, Package); + CreatedPackages.Add(Package); + + FLandscapeTargetLayerSettings LayerSettings(LandscapeLayerInfo); + LandscapeProxy->AddTargetLayer(TargetLayerName, LayerSettings); + Results.Add(LandscapeLayerInfo); + } + + LandscapeInfo->UpdateLayerInfoMap(LandscapeProxy, false); + + return Results; +#else + TArray Results; + + auto * LandscapeInfo = LandscapeProxy->GetLandscapeInfo(); + + FHoudiniPackageParams LayerPackageParams = PackageParams; + + for(int Index = 0; Index < LandscapeInfo->Layers.Num(); Index++) + { + FLandscapeInfoLayerSettings & TargetLayerSettings = LandscapeInfo->Layers[Index]; + if (!IsValid(TargetLayerSettings.LayerInfoObj)) + { + FString TargetLayerName = TargetLayerSettings.LayerName.ToString(); + + ULandscapeLayerInfoObject* Layer = nullptr; + if (PartsForLandscape.Contains(TargetLayerName)) + { + if (!PartsForLandscape[TargetLayerName]->LayerInfoObjectName.IsEmpty()) + { + // Load an existing layer object if the user specified it. + Layer = LoadObject( + nullptr, *PartsForLandscape[TargetLayerName]->LayerInfoObjectName, nullptr, LOAD_None, nullptr); + } + else + { + // Normally we create packages with a name based off geo/part ids. But this doesn't make sense here + // as we're creating a layer info based off the material and name of the landscape. + ALandscape * ParentLandscape = LandscapeProxy->GetLandscapeActor(); + FString PackageName = ParentLandscape->GetName() + FString("_") + TargetLayerName; + FString PackagePath = LayerPackageParams.GetPackagePath(); + UPackage* Package = nullptr; + Layer = FindOrCreateLandscapeLayerInfoObject(TargetLayerName, PackagePath, PackageName, Package); + CreatedPackages.Add(Package); + } + } + + if (IsValid(Layer)) + { + Results.Add(Layer); + LandscapeProxy->EditorLayerSettings.Add(FLandscapeEditorLayerSettings(Layer)); + TargetLayerSettings.LayerInfoObj = Layer; + } + + } + } + + LandscapeInfo->UpdateLayerInfoMap(LandscapeProxy, false); + + return Results; +#endif +} + +UPackage* FHoudiniLandscapeUtils::FindOrCreate(const FString& PackageFullPath) +{ + UPackage* OutPackage; + + // See if package exists, if it does, reuse it + OutPackage = FindPackage(nullptr, *PackageFullPath); + if (!IsValid(OutPackage)) + OutPackage = CreatePackage(*PackageFullPath); + + if (!IsValid(OutPackage)) + return nullptr; + + if (!OutPackage->IsFullyLoaded()) + OutPackage->FullyLoad(); + + return OutPackage; +} + +ULandscapeLayerInfoObject* +FHoudiniLandscapeUtils::FindOrCreateLandscapeLayerInfoObject(const FString & InLayerName, const FString & InPackagePath, const FString & InPackageName, UPackage * &OutPackage) +{ + FString PackageFullName = InPackagePath + TEXT("/") + InPackageName; + OutPackage = FindOrCreate(PackageFullName); + if (!OutPackage) + return nullptr; + + ULandscapeLayerInfoObject* LayerInfo = NewObject(OutPackage, FName(*InPackageName), RF_Public | RF_Standalone /*| RF_Transactional*/); + + // Notify the asset registry + FAssetRegistryModule::AssetCreated(LayerInfo); + + if (IsValid(LayerInfo)) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + LayerInfo->SetLayerName(FName(*InLayerName), true); +#else + LayerInfo->LayerName = FName(*InLayerName); +#endif + + // Trigger update of the Layer Info + LayerInfo->PreEditChange(nullptr); + LayerInfo->PostEditChange(); + LayerInfo->MarkPackageDirty(); + + // Mark the package dirty... + OutPackage->MarkPackageDirty(); + } + + return LayerInfo; +} + +void FHoudiniLandscapeUtils::SetWorldPartitionGridSize(ALandscape* LandscapeProxy, int WorldPartitionGridSize) +{ + UWorld * World = LandscapeProxy->GetWorld(); + if (!World->IsPartitionedWorld()) + return; + + FLandscapeConfig Config(LandscapeProxy->GetLandscapeInfo()); + + if (Config.GridSizeInComponents == WorldPartitionGridSize) + return; + + if (World->IsPartitionedWorld()) + { + TSet ActorsToDelete; + FLandscapeConfigHelper::ChangeGridSize(LandscapeProxy->GetLandscapeInfo(), WorldPartitionGridSize, ActorsToDelete); + for (AActor* ActorToDelete : ActorsToDelete) + { + World->DestroyActor(ActorToDelete); + } + } +} + + +FHoudiniExtents FHoudiniLandscapeUtils::GetLandscapeExtents(ALandscapeProxy* LandscapeProxy) +{ + FHoudiniExtents Extents; + + // Get the landscape X/Y Size + Extents.Min.X = MAX_int32; + Extents.Min.Y = MAX_int32; + Extents.Max.X = -MAX_int32; + Extents.Max.Y = -MAX_int32; + + ALandscape* Landscape = LandscapeProxy->GetLandscapeActor(); + if (LandscapeProxy == Landscape) + { + // The proxy is a landscape actor, so we have to use the landscape extent (landscape components + // may have been moved to proxies and may not be present on this actor). + Landscape->GetLandscapeInfo()->GetLandscapeExtent(Extents.Min.X, Extents.Min.Y, Extents.Max.X, Extents.Max.Y); + } + else + { + // We only want to get the data for this landscape proxy. + // To handle streaming proxies correctly, get the extents via all the components, + // not by calling GetLandscapeExtent or we'll end up sending ALL the streaming proxies. + for (const ULandscapeComponent* Comp : LandscapeProxy->LandscapeComponents) + { + Comp->GetComponentExtent(Extents.Min.X, Extents.Min.Y, Extents.Max.X, Extents.Max.Y); + } + } + + return Extents; +} + +FHoudiniExtents +FHoudiniLandscapeUtils::GetExtents( + const ALandscape* TargetLandscape, + const FHoudiniHeightFieldData& HeightFieldData) +{ + FHoudiniExtents Extents; + + const FTransform TargetLandscapeTransform = TargetLandscape->GetActorTransform(); + + FTransform UnscaledLandscapeTransform = TargetLandscapeTransform; + UnscaledLandscapeTransform.SetScale3D(FVector::OneVector); + + const FTransform RelativeTileTransform = HeightFieldData.Transform * UnscaledLandscapeTransform.Inverse(); + + FVector LandscapeScale = TargetLandscapeTransform.GetScale3D(); + const FVector RelativeTileCoordinate = RelativeTileTransform.GetLocation() / LandscapeScale; + +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 7 + const FIntPoint LandscapeBaseLoc = TargetLandscape->GetSectionBase(); +#else + const FIntPoint LandscapeBaseLoc = TargetLandscape->GetSectionBaseOffset(); +#endif + + // Calculate the final draw coordinates + FIntPoint TargetTileLoc; + TargetTileLoc.X = LandscapeBaseLoc.X + FMath::RoundToInt(RelativeTileCoordinate.X); + TargetTileLoc.Y = LandscapeBaseLoc.Y + FMath::RoundToInt(RelativeTileCoordinate.Y); + + Extents.Min.X = TargetTileLoc.X; + Extents.Min.Y = TargetTileLoc.Y; + Extents.Max.X = TargetTileLoc.X + HeightFieldData.Dimensions.X - 1; + Extents.Max.Y = TargetTileLoc.Y + HeightFieldData.Dimensions.Y - 1; + + return Extents; +} + + +FIntPoint +FHoudiniLandscapeUtils::GetVolumeDimensionsInUnrealSpace(const FHoudiniGeoPartObject& HeightField) +{ + FIntPoint Dimension; + Dimension.X = HeightField.VolumeInfo.YLength; + Dimension.Y = HeightField.VolumeInfo.XLength; + return Dimension; +} + +void FHoudiniLandscapeUtils::TransposeValues(TArray & Values, const FIntPoint & Dimensions) +{ + H_SCOPED_FUNCTION_TIMER(); + + TArray Result; + Result.SetNumUninitialized(Values.Num()); + + ParallelFor(Dimensions.Y, [&](int Y) + { + for(int X = 0; X < Dimensions.X; X++) + { + int Index1 = X + Dimensions.X * Y; + int Index2 = Y + Dimensions.Y * X; + Result[Index1] = Values[Index2]; + } + }); + Values = Result; +} + +FHoudiniHeightFieldData FHoudiniLandscapeUtils::FetchVolumeInUnrealSpace( + const FHoudiniGeoPartObject& HeightField, + const FIntPoint& UnrealLandscapeDimensions, + bool bFetchData) +{ + H_SCOPED_FUNCTION_TIMER(); + + FHoudiniHeightFieldData Result; + Result.Dimensions = GetVolumeDimensionsInUnrealSpace(HeightField); + Result.Transform = GetHeightFieldTransformInUnrealSpace(HeightField.VolumeInfo, UnrealLandscapeDimensions); + + if (bFetchData) + { + // We have to fetch the volume before fetching height field data so HAPI knows which volume we're using. + HAPI_VolumeInfo VolumeInfo; + FHoudiniApi::GetVolumeInfo(FHoudiniEngine::Get().GetSession(), HeightField.GeoId, HeightField.PartId, &VolumeInfo); + + FHoudiniHapiAccessor Accessor(HeightField.GeoId, HeightField.PartId, ""); + + bool bSuccess = Accessor.GetHeightFieldData(Result.Values, Result.GetNumPoints()); + + if (!bSuccess) + { + HOUDINI_LOG_ERROR(TEXT("Failed to fetch height field data")); + Result.Values.SetNumZeroed(Result.GetNumPoints()); + + } + + TransposeValues(Result.Values, Result.Dimensions); + } + + return Result; +} + +FHoudiniHeightFieldData +FHoudiniLandscapeUtils::ReDimensionLandscape(const FHoudiniHeightFieldData & HeightField, FIntPoint NewDimensions) +{ + H_SCOPED_FUNCTION_TIMER(); + + FHoudiniHeightFieldData Result; + Result.Transform = HeightField.Transform; + Result.Dimensions = NewDimensions; + Result.Values.SetNumZeroed(Result.GetNumPoints()); + + const float XScale = (float)(HeightField.Dimensions.X - 1) / (Result.Dimensions.X - 1); + const float YScale = (float)(HeightField.Dimensions.Y - 1) / (Result.Dimensions.Y - 1); + + ParallelFor(Result.Dimensions.Y, [&](int Y) + { + float OldY = Y * YScale; + int32 Y0 = FMath::FloorToInt(OldY); + int32 Y1 = FMath::Min(FMath::FloorToInt(OldY) + 1, HeightField.Dimensions.Y - 1); + + for (int32 X = 0; X < Result.Dimensions.X; ++X) + { + float OldX = X * XScale; + int32 X0 = FMath::FloorToInt(OldX); + int32 X1 = FMath::Min(FMath::FloorToInt(OldX) + 1, HeightField.Dimensions.X - 1); + float Original00 = HeightField.Values[Y0 * HeightField.Dimensions.X + X0]; + float Original10 = HeightField.Values[Y0 * HeightField.Dimensions.X + X1]; + float Original01 = HeightField.Values[Y1 * HeightField.Dimensions.X + X0]; + float Original11 = HeightField.Values[Y1 * HeightField.Dimensions.X + X1]; + float NewValue = FMath::BiLerp(Original00, Original10, Original01, Original11, FMath::Fractional(OldX), FMath::Fractional(OldY)); + Result.Values[Y * Result.Dimensions.X + X] = NewValue; + } + } + ); + + return Result; +} + + +FHoudiniMinMax +FHoudiniLandscapeUtils::GetHeightFieldRange(const FHoudiniHeightFieldData& HeightField) +{ + FHoudiniMinMax Range; + for(float Value : HeightField.Values) + { + Range.Add(Value); + } + return Range; +} + + +float FHoudiniLandscapeUtils::GetAbsRange(const FHoudiniMinMax& Range, float MaxUsableRange) +{ + // Divide them by the useable range + float MinValue = Range.MinValue / MaxUsableRange; + float MaxValue = Range.MaxValue / MaxUsableRange; + + // Find the absolute range of values. + float AbsRange = FMath::Max(FMath::Abs(MinValue), FMath::Abs(MaxValue)); + + return AbsRange; +} + +void FHoudiniLandscapeUtils::AdjustLandscapeTransformToLayerHeight(ALandscape& TargetLandscape, const FHoudiniHeightFieldPartData& LayerData, const FHoudiniHeightFieldData& HeightFieldData) +{ + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + + if (!HoudiniRuntimeSettings->MarshallingLandscapesUseDefaultUnrealScaling) + { + // If we are applying the height field for the first time we must work out scaling. The range of values + + // Get the max absolute range of the landscape in meters. + FHoudiniMinMax Range = FHoudiniLandscapeUtils::GetHeightFieldRange(HeightFieldData); + const float MaxUseableRange = HoudiniRuntimeSettings->MarshallingLandscapesUseFullResolution ? 1.0f : 0.75f; + + float MaxAbsRange = FHoudiniLandscapeUtils::GetAbsRange(Range, MaxUseableRange); + + // a UE Landscape can hold is -256cm -> + 256cm. To get larger scales we adjust the Landscape Actor's + // transform z-scale. So to get a range of -512cm -> 512cm we would set a scale of 2.0. Also, + // multiple by 100.0 to convert Houdini Meters to Centimeters. + + float TransformScale = 100.0f * MaxAbsRange / 256.0f; + if (TransformScale < 100.0f) + { + // Don't go smaller than the default scale. + return; + } + + FTransform Transform = TargetLandscape.GetTransform(); + auto Scale = Transform.GetScale3D(); + Scale.Z = TransformScale; + Transform.SetScale3D(Scale); + TargetLandscape.SetActorTransform(Transform); + } + else if (LayerData.HeightRange.IsSet()) + { + FHoudiniMinMax HeightRange = LayerData.HeightRange.GetValue(); + FTransform Transform = TargetLandscape.GetTransform(); + + // Move the position of the landscape to be in the middle of the range. + auto Position = Transform.GetLocation(); + Position.Z = 0.5f * (HeightRange.MinValue + HeightRange.MaxValue); + Transform.SetLocation(Position); + + // Adjust the scale to accomodate the full range. The default range of -1 to 1 + // encompasses 256cm, so scale accordingly. + auto Scale = Transform.GetScale3D(); + Scale.Z = HeightRange.Diff() / 256.0f; + Transform.SetScale3D(Scale); + + TargetLandscape.SetActorTransform(Transform); + } +} + +TSet +FHoudiniLandscapeUtils::GetNonWeightBlendedLayerNames(const FHoudiniGeoPartObject& InHGPO) +{ + TSet Results; + + // Check the attribute exists on primitive or detail + HAPI_AttributeOwner Owner = HAPI_ATTROWNER_INVALID; + if (FHoudiniEngineUtils::HapiCheckAttributeExists(InHGPO.GeoId, InHGPO.PartId, HAPI_UNREAL_ATTRIB_NONWEIGHTBLENDED_LAYERS, HAPI_ATTROWNER_PRIM)) + Owner = HAPI_ATTROWNER_PRIM; + else if (FHoudiniEngineUtils::HapiCheckAttributeExists(InHGPO.GeoId, InHGPO.PartId, HAPI_UNREAL_ATTRIB_NONWEIGHTBLENDED_LAYERS, HAPI_ATTROWNER_DETAIL)) + Owner = HAPI_ATTROWNER_DETAIL; + else + return Results; + + // Get the values + TArray AttribValues; + FHoudiniHapiAccessor Accessor(InHGPO.GeoId, InHGPO.PartId, HAPI_UNREAL_ATTRIB_NONWEIGHTBLENDED_LAYERS); + Accessor.GetAttributeData(Owner, 1, AttribValues); + + if (AttribValues.Num() <= 0) + return Results; + + // Convert them to FString + for (int32 Idx = 0; Idx < AttribValues.Num(); Idx++) + { + TArray Tokens; + AttribValues[Idx].ParseIntoArray(Tokens, TEXT(" "), true); + + for (int32 n = 0; n < Tokens.Num(); n++) + Results.Add(Tokens[n]); + } + + return Results; +} + +FTransform FHoudiniLandscapeUtils::GetLandscapeActorTransformFromTileTransform(const FTransform& TileTransform, const FHoudiniTileInfo& TileInfo) +{ + FTransform Result = TileTransform; + + FVector3d OffsetX = TileTransform.GetScaledAxis(EAxis::Y) * TileInfo.TileStart.Y; + FVector3d OffsetY = TileTransform.GetScaledAxis(EAxis::X) * TileInfo.TileStart.X; + + Result.SetLocation(Result.GetLocation() - OffsetX - OffsetY); + return Result; +} + +bool FHoudiniLandscapeUtils::GetOutputMode(int GeoId, int PartId, HAPI_AttributeOwner Owner, int& LandscapeOutputMode) +{ + FHoudiniHapiAccessor Accessor; + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_OUTPUT_MODE); + bool bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, LandscapeOutputMode); + return bSuccess; +} + +UMaterialInterface* +FHoudiniLandscapeUtils::CreateMaterialInstance( + const FString & Prefix, + UMaterialInterface* Material, + const FHoudiniPackageParams& Params, + TArray& CreatedPackages) +{ + if (!IsValid(Material)) + { + return nullptr; + } + + + // Factory to create materials. + UMaterialInstanceConstantFactoryNew* MaterialInstanceFactory = NewObject< UMaterialInstanceConstantFactoryNew >(); + if (!MaterialInstanceFactory) + return nullptr; + + + FString MaterialName = Prefix + FString("_") + Material->GetName() + TEXT("_instance"); + MaterialName = UPackageTools::SanitizePackageName(MaterialName); + + FString MaterialInstanceName; + + FHoudiniPackageParams MaterialParams = Params; + MaterialParams.ObjectName = MaterialName; + UPackage* MaterialPackage = MaterialParams.CreatePackageForObject(MaterialInstanceName); + + // Create the new material instance + MaterialInstanceFactory->AddToRoot(); + MaterialInstanceFactory->InitialParent = Material; + auto MaterialInstance = (UMaterialInstanceConstant*)MaterialInstanceFactory->FactoryCreateNew( + UMaterialInstanceConstant::StaticClass(), MaterialPackage, FName(*MaterialInstanceName), + RF_Public | RF_Standalone, NULL, GWarn); + + MaterialInstanceFactory->RemoveFromRoot(); + + FAssetRegistryModule::AssetCreated(MaterialInstance); + MaterialPackage->MarkPackageDirty(); + CreatedPackages.Add(MaterialPackage); + + return MaterialInstance; +} + +void FHoudiniLandscapeUtils::ApplyMaterialsFromParts( + ALandscapeProxy* LandscapeProxy, + TMap & Parts, + const FHoudiniPackageParams& PackageParams, + TArray& CreatedPackages) +{ + // The user can specify a different materials per part, but this is an error. + // Pick the first one we find. TODO: Add checks to see if they conflict. + + for(auto & It : Parts) + { + It.Value->MaterialInstance = AssignGraphicsMaterialsToLandscape( + LandscapeProxy, It.Value->Materials, PackageParams, CreatedPackages); + + AssignPhysicsMaterialsToLandscape(LandscapeProxy, It.Value->TargetLayerName, It.Value->Materials); + + break; // just the first one. + } + + + + +} + + +bool +FHoudiniLandscapeUtils::ApplyLandscapeSplinesToReservedLayer(ALandscape* const InLandscape) +{ + if (!IsValid(InLandscape) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + || !InLandscape->FindEditLayerOfType(ULandscapeEditLayerSplines::StaticClass())) +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + || !InLandscape->FindLayerOfType(ULandscapeEditLayerSplines::StaticClass())) +#else + || !InLandscape->GetLandscapeSplinesReservedLayer()) +#endif + { + return false; + } + + InLandscape->RequestSplineLayerUpdate(); + + return true; +} + + +bool +FHoudiniLandscapeUtils::ApplySegmentsToLandscapeEditLayers( + const TMap, FHoudiniLandscapeSplineApplyLayerData>& InSegmentsToApplyToLayers) +{ + bool bSuccess = true; + for (const auto& Entry : InSegmentsToApplyToLayers) + { + ALandscape* const Landscape = Entry.Key.Key; + const FName LayerName = Entry.Key.Value; + const FHoudiniLandscapeSplineApplyLayerData& LayerData = Entry.Value; + + if (!IsValid(Landscape) || LayerName == NAME_None) + continue; + + // For landscapes with reserved layers all splines of the landscape are applied to the reserved layer + if (LayerData.bIsReservedSplineLayer) + { + if (!ApplyLandscapeSplinesToReservedLayer(Landscape)) + bSuccess = false; + continue; + } + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + FLandscapeLayer const* const Layer = Landscape->GetLayerConst(LayerName); +#else + FLandscapeLayer const* const Layer = Landscape->GetLayer(LayerName); +#endif + + if (!Layer) + { + HOUDINI_LOG_WARNING( + TEXT("Layer '%s' unexpectedly not found on landscape '%s': cannot apply splines to layer."), + *LayerName.ToString(), *Landscape->GetFName().ToString()); + continue; + } + + // Not a landscape + reserved layer, so we must select each segment and its control points and then apply it + // to the specified edit layer + if (LayerData.SegmentsToApply.Num() == 0) + continue; + + // Select the segments and their control points + for (ULandscapeSplineSegment* const Segment : LayerData.SegmentsToApply) + { + if (!IsValid(Segment)) + continue; + + Segment->SetSplineSelected(true); + + ULandscapeSplineControlPoint* const CP0 = Segment->Connections[0].ControlPoint; + if (IsValid(CP0)) + CP0->SetSplineSelected(true); + + ULandscapeSplineControlPoint* const CP1 = Segment->Connections[1].ControlPoint; + if (IsValid(CP1)) + CP1->SetSplineSelected(true); + } + + // Apply splines to layer +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + Landscape->UpdateAllLandscapeSplines(Layer->EditLayer->GetGuid()); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + static constexpr bool bUpdateOnlySelected = true; + Landscape->UpdateLandscapeSplines(Layer->EditLayer->GetGuid(), bUpdateOnlySelected); +#else + static constexpr bool bUpdateOnlySelected = true; + Landscape->UpdateLandscapeSplines(Layer->Guid, bUpdateOnlySelected); +#endif + // Unselect the segments and their control points + for (ULandscapeSplineSegment* const Segment : LayerData.SegmentsToApply) + { + if (!IsValid(Segment)) + continue; + + Segment->SetSplineSelected(false); + + ULandscapeSplineControlPoint* const CP0 = Segment->Connections[0].ControlPoint; + if (IsValid(CP0)) + CP0->SetSplineSelected(false); + + ULandscapeSplineControlPoint* const CP1 = Segment->Connections[1].ControlPoint; + if (IsValid(CP1)) + CP1->SetSplineSelected(false); + } + } + + return bSuccess; +} + +void FHoudiniLandscapeUtils::DeleteCookedLayer(UHoudiniLandscapeTargetLayerOutput* Layer) +{ + int32 EditLayerIndex = Layer->Landscape->GetLayerIndex(FName(Layer->CookedEditLayer)); + if(EditLayerIndex == INDEX_NONE) + return; + + Layer->Landscape->DeleteLayer(EditLayerIndex); + +} + +void FHoudiniLandscapeUtils::ApplyLocks(UHoudiniLandscapeTargetLayerOutput* Output) +{ + if (!Output->bLockLayer) + return; + + int32 EditLayerIndex = Output->Landscape->GetLayerIndex(FName(Output->CookedEditLayer)); + if (EditLayerIndex == INDEX_NONE) + return; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + Output->Landscape->GetEditLayer(EditLayerIndex)->SetLocked(true, true); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 5 + Output->Landscape->SetLayerLocked(EditLayerIndex, true); +#else + FLandscapeLayer* UnrealEditLayer = Output->Landscape->GetLayer(EditLayerIndex); + UnrealEditLayer->bLocked = true; +#endif +} + +bool +FHoudiniLandscapeUtils::NormalizePaintLayers(TArray& Data, bool bNormalize) +{ + H_SCOPED_FUNCTION_TIMER(); + + if (Data.Num() == 0) + return false; + + float MaxValue = Data[0]; + bool bExceedsRange = false; + + // Scan data to see if any value exceeds 1.0, while keeping track of the max values. + for(float & Value : Data) + { + MaxValue = FMath::Max(Value, MaxValue); + + if (Value > 1.0) + { + bExceedsRange = true; + } + } + + if (!bExceedsRange) + return false; + + if (bNormalize) + { + for (float & Value : Data) + { + if (Value < 0.0f) + Value = 0.0f; + else + Value = Value / MaxValue; + } + } + else + { + // If value exceeded range and not normalizing, clamp + for (float & Value : Data) + { + Value = FMath::Clamp(Value, 0.0f, 1.0f); + } + + } + return true; + +} + +TArray FHoudiniLandscapeUtils::ConvertHeightFieldData(const ALandscape* LandscapeActor, const TArray& Values) +{ + H_SCOPED_FUNCTION_TIMER(); + + float Range = FHoudiniLandscapeUtils::GetLandscapeHeightRangeInCM(*LandscapeActor); + + float Scale = 100.0f; // Scale from Meters to CM. + Scale /= Range; // Remap to -1.0f to 1.0 Range + + TArray AlignedValues = Values; + FHoudiniLandscapeUtils::RealignHeightFieldData(AlignedValues, 0.5f, Scale * 0.5f); + + // Explicitly clamp the values, and report if clamped. + bool bClamped = FHoudiniLandscapeUtils::ClampHeightFieldData(AlignedValues, 0.0, 1.0f); + if (bClamped) + { + HOUDINI_BAKING_WARNING(TEXT("Landscape layer exceeded max heights so was clamped.")); + } + + // Quantized to 16-bit and set the data. + auto QuantizedData = FHoudiniLandscapeUtils::QuantizeNormalizedDataTo16Bit(AlignedValues); + return QuantizedData; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniLandscapeUtils.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +// This file contains utility functions for translating Houdini Height Fields to Unreal Landscapes. +// Do not include this header in other headers; the code is separated out to speed compilation times. + +#include "HoudiniLandscapeTranslator.h" +#include "LandscapeInfo.h" +#include "HAPI/HAPI_Common.h" +#include "UObject/Class.h" + +class UHoudiniCookable; +class UHoudiniLandscapeTargetLayerOutput; +class UHoudiniOutput; +class ALandscape; +struct FHoudiniExtents; +class FName; +struct FLandscapeLayer; +struct FHoudiniGeoPartObject; +struct FHoudiniVolumeInfo; +struct FHoudiniPackageParams; +struct FHoudiniEngineOutputStats; +struct FHoudiniEngineOutputStats; +class UMaterialInstance; + +struct FHoudiniMinMax +{ + float MinValue = TNumericLimits::Max(); + float MaxValue = TNumericLimits::Min(); + + void Add(float Value) + { + MinValue = FMath::Min(MinValue, Value); + MaxValue = FMath::Max(MaxValue, Value); + } + + float Diff() const + { + return MaxValue - MinValue; + } + + bool IsValid() const + { + return MinValue <= MaxValue; + } +}; + +struct FHoudiniLandscapeMaterial +{ + FString Material; + bool bCreateMaterialInstance = false; + FString HoleMaterial; + FString PhysicalMaterial; +}; + +struct FHoudiniHeightFieldData +{ + FIntPoint Dimensions; + FTransform Transform; + TArray Values; + + int GetNumPoints() const { return Dimensions.X * Dimensions.Y; } + +}; + +struct FHoudiniTileInfo +{ + FIntPoint TileStart; // Position of this tile + FIntPoint LandscapeDimensions; // Dimensions of the entire landscape. +}; + +struct FHoudiniHeightFieldPartData +{ + int ObjectId = 0; + int GeoId = 0; + int PartId = 0; + + // The Edit Layer name to use. + FString UnrealLayerName; + + // Target layer, eg. "height" or a layer associated with a material. + FString TargetLayerName; + + // Houdini Height Field Object + const FHoudiniGeoPartObject* HeightField; + + // HAPI_UNREAL_LANDSCAPE_EDITLAYER_TYPE_BASE or HAPI_UNREAL_LANDSCAPE_EDITLAYER_TYPE_ADDITIVE + int EditLayerType; + + // Should the layer be cleared + bool bClearLayer; + + // Help place new Edit Layers in order. + FString AfterLayerName; + + // Name of the target landscape. + FString TargetLandscapeName; + + // Whether to update or create an existing landscape. + bool bCreateNewLandscape; + + // Will be a subtractive layer. + bool bSubtractiveEditLayer; + + // Will be a weight blended landscape. + bool bIsWeightBlended; + + // Materials to assign. + FHoudiniLandscapeMaterial Materials; + + // Optional Height Range to use + TOptional HeightRange; + + // Whether to treast data as zero to one. + bool bIsUnitData; + + // Houdini Tile Dimensions. + TOptional TileInfo; + + // Existing Layer Info Name + FString LayerInfoObjectName; + + // Bake Outliner Folder + FString BakeOutlinerFolder; + + // Bake Folder + FString BakeFolder; + + // Size info - number of sections and components. + FHoudiniLandscapeCreationInfo SizeInfo; + + // Any material instance created. + UMaterialInterface* MaterialInstance = nullptr; + + // whether or not to unlock the layer (if locked). + bool bWriteLockedLayers = false; + + // whether to lock layer after cooking. + bool bLockLayer = false; + + // PropertyAttributes + TArray PropertyAttributes; + + // Whether or not to normalize paint layers that fall outside of the range 0 to 1. + bool bNormalizePaintLayers = false; + + // Transform, obtained from node. + FTransform Transform; + +}; + +struct FHoudiniUnrealLandscapeTarget +{ + // Pointer to the landscape to where the layers will be cooked. + TWeakObjectPtr Proxy; + + // If true, the landscape above was a new one, if not it was an existing landscape + bool bWasCreated = false; + + // Name of the landscape when it it is baked. + FName BakedName; + + // Dimensions of landscape when created. + FIntPoint Dimensions; + + // Any created layer info assets. + TArray CreatedLayerInfoObjects; +}; + +struct FHoudiniLayersToUnrealLandscapeMapping +{ + // All the target landscapes for this output. + TArray TargetLandscapes; + + // Packages created during landscape creation. + TArray CreatedPackages; + + // Maps the layer to an entry in TargetLandscapes. + TMap HoudiniLayerToUnrealLandscape; +}; + + +struct FHoudiniLandscapeSplineApplyLayerData +{ + // The landscape. + TObjectPtr Landscape; + + // The edit layer name to apply the segments to. + FName EditLayerName; + + // True if this layer is the reserved spline layer. + bool bIsReservedSplineLayer = false; + + // The segments to apply to EditLayerName on Landscape. + TArray> SegmentsToApply; +}; + +struct FHoudiniLandscapeSettings +{ + FTransform LocalToWorldTransform = FTransform::Identity; + bool bUseTempLayers = false; + FString TempLayerSuffix; +}; + +struct HOUDINIENGINE_API FHoudiniLandscapeUtils +{ + + static TSet GetCookedLandscapeLayers(UHoudiniCookable& HC, ALandscape& Landscape); + + static TSet GetEditLayers(UHoudiniOutput& Output); + + static void SetNonCookedLayersVisibility(UHoudiniCookable& HC, ALandscape& Landscape, bool bVisible); + + static void SetCookedLayersVisibility(UHoudiniCookable& HC, ALandscape& Landscape, bool bVisible); + + static void RealignHeightFieldData(TArray& Data, float ZeroPoint, float Scale); + + static bool ClampHeightFieldData(TArray& Data, float MinValue, float MaxValue); + + static TArray QuantizeNormalizedDataTo16Bit(const TArray& Data); + + static float GetLandscapeHeightRangeInCM(const ALandscape& Landscape); + + static TArray GetHeightData( + ALandscape* Landscape, + const FHoudiniExtents& Extents, +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + const FLandscapeLayer* EditLayer); +#else + FLandscapeLayer* EditLayer); +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + static const FLandscapeLayer* GetEditLayer(ALandscape* Landscape, const FName& LayerName); + static const FLandscapeLayer* GetOrCreateEditLayer(ALandscape* Landscape, const FName& LayerName, bool* bCreated=nullptr); + static const FLandscapeLayer* MoveEditLayerAfter(ALandscape* Landscape, const FName& LayerName, const FName& AfterLayerName); +#else + static FLandscapeLayer* GetEditLayer(ALandscape* Landscape, const FName& LayerName); + static FLandscapeLayer* GetOrCreateEditLayer(ALandscape* Landscape, const FName& LayerName, bool* bCreated = nullptr); + static FLandscapeLayer* MoveEditLayerAfter(ALandscape* Landscape, const FName& LayerName, const FName& AfterLayerName); +#endif + + static TArray GetLayerData(ALandscape* Landscape, const FHoudiniExtents& Extents, const FName& EditLayerName, const FName& TargetLayerName); + + static FHoudiniLayersToUnrealLandscapeMapping ResolveLandscapes(const FString & CookedLandscapePrefix, + const FHoudiniPackageParams& PackageParams, + const FHoudiniLandscapeSettings& LandscapeSettings, + TMap& LandscapeMap, + TArray& Parts, + UWorld* World, + const TArray& LandscapeInputs); + + static bool CalcLandscapeSizeFromHeightFieldSize(int32 ProposedUnrealSizeX, int32 ProposedUnrealSizeY, FHoudiniLandscapeCreationInfo& Info); + + static void ImportLandscape(ALandscape* LandscapeActor, const FHoudiniLandscapeCreationInfo& Info, const TArray & Values); + + static ALandscapeProxy* FindTargetLandscapeProxy(const FString& ActorName, UWorld* World, const TArray& LandscapeInputs); + + static FHoudiniHeightFieldPartData* GetPartWithHeightData(TMap& Parts); + + static FTransform GetHeightFieldTransformInUnrealSpace(const FHoudiniVolumeInfo& VolumeInfo, const FIntPoint& UnrealDimensions); + + static UMaterialInterface* AssignGraphicsMaterialsToLandscape( + ALandscapeProxy* LandscapeProxy, + FHoudiniLandscapeMaterial & Materials, + const FHoudiniPackageParams& Params, + TArray & CreatedPacakages); + + static void AssignPhysicsMaterialsToLandscape(ALandscapeProxy* LandscapeProxy, const FString& LayerName, FHoudiniLandscapeMaterial& Materials); + + static TArray CreateTargetLayerInfoAssets( + ALandscapeProxy* LandscapeProxy, + const FHoudiniPackageParams& PackageParams, + TMap& PartsForLandscape, + TArray & CreatedPackages); + + static ULandscapeLayerInfoObject* FindOrCreateLandscapeLayerInfoObject(const FString& InLayerName, const FString& InPackagePath, const FString& InPackageName, UPackage*& OutPackage); + + static void SetWorldPartitionGridSize(ALandscape* Landscape, int GridSize); + + static UPackage* FindOrCreate(const FString& PackageFullPath); + + static FHoudiniExtents GetExtents(const ALandscape* TargetLandscape, const FHoudiniHeightFieldData& HeightFieldData); + + static FHoudiniHeightFieldData FetchVolumeInUnrealSpace( + const FHoudiniGeoPartObject& HeightField, + const FIntPoint & UnrealLandscapeDimensions, + bool bFetchData); + + static FIntPoint GetVolumeDimensionsInUnrealSpace(const FHoudiniGeoPartObject& HeightField); + + static FHoudiniHeightFieldData ReDimensionLandscape(const FHoudiniHeightFieldData& HeightField, FIntPoint NewDimensions); + + static void TransposeValues(TArray & Values, const FIntPoint & Dimensions); + + static FHoudiniMinMax GetHeightFieldRange(const FHoudiniHeightFieldData& HeightField); + + static float GetAbsRange(const FHoudiniMinMax& Range, float MaxUsableRange); + + static void AdjustLandscapeTransformToLayerHeight(ALandscape& TargetLandscape , const FHoudiniHeightFieldPartData& LayerData, const FHoudiniHeightFieldData & HeightFieldData); + + static TSet GetNonWeightBlendedLayerNames(const FHoudiniGeoPartObject& InHGPO); + + static FTransform GetLandscapeActorTransformFromTileTransform(const FTransform& Transform, const FHoudiniTileInfo& TileInfo); + + static bool GetOutputMode(int GeoId, int PartId, HAPI_AttributeOwner Owner, int& LandscapeOutputMode); + + static UMaterialInterface* CreateMaterialInstance(const FString & Prefix, UMaterialInterface * Material, const FHoudiniPackageParams & Params, TArray& CreatedPackages); + + static void ApplyMaterialsFromParts( + ALandscapeProxy * LandscapeProxy, + TMap & LayerParts, + const FHoudiniPackageParams& PackageParams, + TArray & CreatedPackages); + + static FHoudiniExtents GetLandscapeExtents(ALandscapeProxy * Landscape); + + static void ApplyLocks(UHoudiniLandscapeTargetLayerOutput* Output); + + static void DeleteCookedLayer(UHoudiniLandscapeTargetLayerOutput * Layer); + + //------------------- + // Landscape splines + //------------------- + + // Apply the landscape splines of InLandscape to its reserved layer. + static bool ApplyLandscapeSplinesToReservedLayer(ALandscape* const InLandscape); + + // Apply the segments to the specified edit layers per landscape. + static bool ApplySegmentsToLandscapeEditLayers( + const TMap, FHoudiniLandscapeSplineApplyLayerData>& InSegmentsToApplyToLayers); + + static bool NormalizePaintLayers(TArray & Data, bool bNormalize); + + static TArray ConvertHeightFieldData(const ALandscape* LandscapeActor, const TArray& Values); + +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniLevelInstanceUtils.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniLevelInstanceUtils.h" +#include "Editor.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineUtils.h" +#include "LevelInstance/LevelInstanceActor.h" +#include "LevelInstance/LevelInstanceTypes.h" +#include "HoudiniOutput.h" + +TOptional FHoudiniLevelInstanceUtils::GetParams(int NodeId, int PartId) +{ + // See if a level instance was specified. If it wasn't ignore. + FString OutputName; + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_LEVEL_INSTANCE_NAME); + bool bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, OutputName); + + if (!bSuccess || OutputName.IsEmpty()) + return {}; + + // Create default params. + FHoudiniLevelInstanceParams Params; + Params.OutputName = OutputName; + Params.Type = ELevelInstanceCreationType::LevelInstance; + + // See if a type was specified. + int32 Type = 0; + + Accessor.Init(NodeId, PartId, HAPI_UNREAL_ATTRIB_LEVEL_INSTANCE_TYPE); + bSuccess = Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, Type); + + if (bSuccess) + { + Params.Type = Type == 0 ? ELevelInstanceCreationType::LevelInstance : ELevelInstanceCreationType::PackedLevelActor; + } + + return Params; + +} + +bool FHoudiniLevelInstanceUtils::FetchLevelInstanceParameters(TArray>& CookedOutputs) +{ + for (int Index = 0; Index < CookedOutputs.Num(); Index++) + { + UHoudiniOutput& CookedOutput = *CookedOutputs[Index]; + for(auto & It : CookedOutput.GetOutputObjects()) + { + TOptional Params = GetParams(It.Key.GeoId, It.Key.PartId); + if (Params.IsSet()) + { + It.Value.LevelInstanceParams = Params.GetValue(); + } + } + } + return true; +} + + + +``` + +`Source/HoudiniEngine/Private/HoudiniLevelInstanceUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "CoreMinimal.h" +#include "HoudiniOutput.h" + +class UHoudiniOutput; +class ILevelInstanceInterface; +class AActor; +struct FHoudiniBakedOutput; +struct FHoudiniLevelInstanceParams; +struct FHoudiniEngineBakedActor; + +class FHoudiniLevelInstanceUtils +{ +public: + static bool FetchLevelInstanceParameters(TArray>& CookedOutputs); + + static TOptional GetParams(int NodeId, int PartId); + +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniMaterialTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniMaterialTranslator.h" + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineString.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniGenericAttribute.h" +#include "HoudiniOutput.h" +#include "HoudiniPackageParams.h" +#include "HoudiniMeshTranslator.h" +#include "HoudiniTextureTranslator.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 +#include "Materials/MaterialParameters.h" +#else +#include "MaterialTypes.h" +#endif +#include "Materials/Material.h" +#include "Materials/MaterialInstance.h" +#include "Materials/MaterialInstanceConstant.h" +#include "PhysicalMaterials/PhysicalMaterial.h" + +#include "Materials/MaterialExpressionTextureSample.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionConstant4Vector.h" +#include "Materials/MaterialExpressionConstant.h" +#include "Materials/MaterialExpressionMultiply.h" +#include "Materials/MaterialExpressionVertexColor.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "ImageUtils.h" +#include "PackageTools.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "UObject/MetaData.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "MaterialShared.h" +#endif +#include "Engine/Texture2D.h" +#include "Factories/MaterialFactoryNew.h" +#include "Serialization/BufferWriter.h" + +#if WITH_EDITOR + #include "Factories/MaterialFactoryNew.h" + #include "Factories/MaterialInstanceConstantFactoryNew.h" +#endif + +const int32 FHoudiniMaterialTranslator::MaterialExpressionNodeX = -400; +const int32 FHoudiniMaterialTranslator::MaterialExpressionNodeY = -150; +const int32 FHoudiniMaterialTranslator::MaterialExpressionNodeStepX = 220; +const int32 FHoudiniMaterialTranslator::MaterialExpressionNodeStepY = 220; + +// Helper to get StaticParameters from UMaterialInterface in <=5.1 +// This copied from 5.3's UMaterialInterface::GetStaticParameterValues() function +#if ENGINE_MAJOR_VERSION < 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION <= 1) +void GetStaticParameterValues(UMaterialInterface* MaterialInterface, FStaticParameterSet& OutStaticParameters) +{ + if (!IsValid(MaterialInterface)) + return; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 1 + TArray& StaticSwitchParameters = OutStaticParameters.EditorOnly.StaticSwitchParameters; +#else + TArray& StaticSwitchParameters = OutStaticParameters.StaticSwitchParameters; +#endif +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + TArray& StaticComponentMaskParameters = OutStaticParameters.EditorOnly.StaticComponentMaskParameters; +#else + TArray& StaticComponentMaskParameters = OutStaticParameters.StaticComponentMaskParameters; +#endif + TMap ParameterValues; + for (int32 ParameterTypeIndex = 0; ParameterTypeIndex < NumMaterialParameterTypes; ++ParameterTypeIndex) + { + const EMaterialParameterType ParameterType = (EMaterialParameterType)ParameterTypeIndex; + if (IsStaticMaterialParameter(ParameterType)) + { + ParameterValues.Reset(); + MaterialInterface->GetAllParametersOfType(ParameterType, ParameterValues); + // OutStaticParameters.AddParametersOfType(ParameterType, ParameterValues); + // Copied the code from FStaticParameterSet::AddParametersOfType due to missing API export + switch (ParameterType) + { + case EMaterialParameterType::StaticSwitch: + StaticSwitchParameters.Empty(ParameterValues.Num()); + for (const auto& It : ParameterValues) + { + const FMaterialParameterMetadata& Meta = It.Value; + check(Meta.Value.Type == ParameterType); +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2) + if(!Meta.bDynamicSwitchParameter) + { + StaticSwitchParameters.Emplace(It.Key, Meta.Value.AsStaticSwitch(), Meta.bOverride, Meta.ExpressionGuid); + } +#else + StaticSwitchParameters.Emplace(It.Key, Meta.Value.AsStaticSwitch(), Meta.bOverride, Meta.ExpressionGuid); +#endif + } + break; + case EMaterialParameterType::StaticComponentMask: + StaticComponentMaskParameters.Empty(ParameterValues.Num()); + for (const auto& It : ParameterValues) + { + const FMaterialParameterMetadata& Meta = It.Value; + check(Meta.Value.Type == ParameterType); + StaticComponentMaskParameters.Emplace(It.Key, + Meta.Value.Bool[0], + Meta.Value.Bool[1], + Meta.Value.Bool[2], + Meta.Value.Bool[3], + Meta.bOverride, + Meta.ExpressionGuid); + } + break; + default: checkNoEntry(); + } + } + } + + if(UMaterialInstance* MaterialInstance = Cast(MaterialInterface)) + { +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1) + if (UMaterialInstanceEditorOnlyData* EditorOnly = MaterialInstance->GetEditorOnlyData()) + { + OutStaticParameters.EditorOnly.TerrainLayerWeightParameters = EditorOnly->StaticParameters.TerrainLayerWeightParameters; + } +#else + OutStaticParameters.TerrainLayerWeightParameters = MaterialInstance->GetStaticParameters().TerrainLayerWeightParameters; +#endif + } + + FMaterialLayersFunctions MaterialLayers; + OutStaticParameters.bHasMaterialLayers = MaterialInterface->GetMaterialLayers(MaterialLayers); + if (OutStaticParameters.bHasMaterialLayers) + { +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1) + OutStaticParameters.MaterialLayers = MoveTemp(MaterialLayers.GetRuntime()); + OutStaticParameters.EditorOnly.MaterialLayers = MoveTemp(MaterialLayers.EditorOnly); +#else + OutStaticParameters.MaterialLayers = MoveTemp(MaterialLayers); +#endif + } + + // OutStaticParameters.Validate(); + // Copied the code from FStaticParameterSet::Validate() due to missing API export +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1) + FMaterialLayersFunctions::Validate(OutStaticParameters.MaterialLayers, OutStaticParameters.EditorOnly.MaterialLayers); +#endif +} +#endif + +FHoudiniMaterialParameterValue::FHoudiniMaterialParameterValue() + : ParamType(EHoudiniUnrealMaterialParameterType::Invalid) + , DataType(EHoudiniUnrealMaterialParameterDataType::Invalid) + , ByteValue(0) + , FloatValue(0) + , StringValue() + , VectorValue(0, 0, 0, 0) +{ +} + +void +FHoudiniMaterialParameterValue::SetValue(const uint8 InValue) +{ + DataType = EHoudiniUnrealMaterialParameterDataType::Byte; + ByteValue = InValue; + CleanValue(); +} + +void +FHoudiniMaterialParameterValue::SetValue(const float InValue) +{ + DataType = EHoudiniUnrealMaterialParameterDataType::Float; + FloatValue = InValue; + CleanValue(); +} + +void +FHoudiniMaterialParameterValue::SetValue(const FString& InValue) +{ + DataType = EHoudiniUnrealMaterialParameterDataType::String; + StringValue = InValue; + CleanValue(); +} + +void +FHoudiniMaterialParameterValue::SetValue(const FLinearColor& InValue) +{ + DataType = EHoudiniUnrealMaterialParameterDataType::Vector; + VectorValue = InValue; + CleanValue(); +} + +void +FHoudiniMaterialParameterValue::CleanValue() +{ + if (DataType != EHoudiniUnrealMaterialParameterDataType::Byte) + ByteValue = 0; + if (DataType != EHoudiniUnrealMaterialParameterDataType::Float) + FloatValue = 0; + if (DataType != EHoudiniUnrealMaterialParameterDataType::String) + StringValue.Empty(); + if (DataType != EHoudiniUnrealMaterialParameterDataType::Vector) + VectorValue = {0, 0, 0, 0}; +} + +FString +FHoudiniMaterialInfo::MakeMaterialInstanceParametersSlug() const +{ + if (!bMakeMaterialInstance) + return FString(); + + TArray Keys; + MaterialInstanceParameters.GetKeys(Keys); + Algo::SortBy(Keys, [](const FName& InName) + { + return InName.ToString().ToLower(); + } + ); + + FBufferWriter Ar(nullptr, 0, EBufferWriterFlags::AllowResize | EBufferWriterFlags::TakeOwnership); + int32 NumKeys = Keys.Num(); + Ar << NumKeys; + for (const FName& Key : Keys) + { + FHoudiniMaterialParameterValue ParamValue = MaterialInstanceParameters.FindChecked(Key); + ParamValue.CleanValue(); + FString Name = Key.ToString().ToLower(); + Ar << Name; + FHoudiniMaterialParameterValue::StaticStruct()->SerializeBin(Ar, &ParamValue); + } + + const FString OutputString = BytesToString(static_cast(Ar.GetWriterData()), Ar.Tell()); + Ar.Close(); + + return OutputString; +} + +FHoudiniMaterialIdentifier +FHoudiniMaterialInfo::MakeIdentifier() const +{ + return FHoudiniMaterialIdentifier( + MaterialObjectPath, + bMakeMaterialInstance, + MakeMaterialInstanceParametersSlug() + ); +} + +bool +FHoudiniMaterialTranslator::CreateHoudiniMaterials( + const HAPI_NodeId& InAssetId, + const FHoudiniPackageParams& InPackageParams, + const TArray& InUniqueMaterialIds, + const TArray& InUniqueMaterialInfos, + const TMap>& InMaterials, + const TMap>& InAllOutputMaterials, + TMap>& OutMaterials, + TArray& OutMaterialArray, + TArray& OutPackages, + const bool& bForceRecookAll, + bool bInTreatExistingMaterialsAsUpToDate, + bool bAddDefaultMaterial) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMaterialTranslator::CreateHoudiniMaterials); + + if (InUniqueMaterialIds.Num() <= 0) + return false; + + if (InUniqueMaterialInfos.Num() != InUniqueMaterialIds.Num()) + return false; + + // Empty returned materials. + OutMaterials.Empty(); + + // Update context for generated materials (will trigger when object goes out of scope). + FMaterialUpdateContext MaterialUpdateContext; + + // Default Houdini material. + if (bAddDefaultMaterial) + { + UMaterial* DefaultMaterial = FHoudiniEngine::Get().GetHoudiniDefaultMaterial().Get(); + OutMaterials.Add( + FHoudiniMaterialIdentifier(HAPI_UNREAL_DEFAULT_MATERIAL_NAME, false), + DefaultMaterial); + } + + // Factory to create materials. + UMaterialFactoryNew* MaterialFactory = NewObject(); + MaterialFactory->AddToRoot(); + + OutMaterialArray.SetNumZeroed(InUniqueMaterialIds.Num()); + + for (int32 MaterialIdx = 0; MaterialIdx < InUniqueMaterialIds.Num(); MaterialIdx++) + { + HAPI_NodeId MaterialId = (HAPI_NodeId)InUniqueMaterialIds[MaterialIdx]; + + const HAPI_MaterialInfo& MaterialInfo = InUniqueMaterialInfos[MaterialIdx]; + if (!MaterialInfo.exists) + { + // The material does not exist, + // we will use the default Houdini material in this case. + continue; + } + + // Get the material node's node information. + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), + MaterialInfo.nodeId, &NodeInfo)) + { + continue; + } + + FString MaterialName = TEXT(""); + if (!FHoudiniEngineString::ToFString(NodeInfo.nameSH, MaterialName)) + { + // shouldnt happen, give a generic name + HOUDINI_LOG_WARNING(TEXT("Failed to retrieve material name!")); + MaterialName = TEXT("Material_") + FString::FromInt(MaterialInfo.nodeId); + } + + FString MaterialPathName = TEXT(""); + if (!FHoudiniMaterialTranslator::GetMaterialRelativePath(InAssetId, MaterialInfo, MaterialPathName)) + continue; + const FHoudiniMaterialIdentifier MaterialIdentifier(MaterialPathName, true); + + // Check first in the existing material map + UMaterial* Material = nullptr; + const TObjectPtr * FoundMaterial = InMaterials.Find(MaterialIdentifier); + bool bCanReuseExistingMaterial = false; + if (FoundMaterial) + { + bCanReuseExistingMaterial = (bInTreatExistingMaterialsAsUpToDate || !MaterialInfo.hasChanged) && !bForceRecookAll; + Material = Cast(*FoundMaterial); + } + + if(!Material || !bCanReuseExistingMaterial) + { + // Try to see if another output/part of this HDA has already recreated this material + // Since those materials have just been recreated, they are considered up to date and can always be reused. + FoundMaterial = InAllOutputMaterials.Find(MaterialIdentifier); + if (FoundMaterial) + { + Material = Cast(*FoundMaterial); + bCanReuseExistingMaterial = true; + } + } + + // Check that the existing material is in the expected directory (temp folder could have been changed between + // cooks). + if (IsValid(Material) && !InPackageParams.HasMatchingPackageDirectories(Material)) + { + bCanReuseExistingMaterial = false; + Material = nullptr; + } + + bool bCreatedNewMaterial = false; + if (IsValid(Material)) + { + // If the cached material exists and is up to date, we can reuse it. + if (bCanReuseExistingMaterial) + { + OutMaterialArray[MaterialIdx] = Material; + OutMaterials.Add(MaterialIdentifier, Material); + continue; + } + } + else + { + // Previous Material was not found, we need to create a new one. + EObjectFlags ObjFlags = RF_Public | RF_Standalone; + + // Create material package and get material name. + FString MaterialPackageName; + UPackage* MaterialPackage = FHoudiniMaterialTranslator::CreatePackageForMaterial( + MaterialInfo.nodeId, MaterialName, InPackageParams, MaterialPackageName); + + Material = (UMaterial*)MaterialFactory->FactoryCreateNew( + UMaterial::StaticClass(), MaterialPackage, *MaterialPackageName, ObjFlags, NULL, GWarn); + + // Add meta information to this package. + FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + MaterialPackage, Material, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + MaterialPackage, Material, HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *MaterialName); + + bCreatedNewMaterial = true; + } + + if (!IsValid(Material)) + continue; + + OutMaterialArray[MaterialIdx] = Material; + + // Get the asset name from the package params + FString AssetName = InPackageParams.HoudiniAssetName.IsEmpty() ? TEXT("HoudiniAsset") : InPackageParams.HoudiniAssetName; + + // Get the package and add it to our list + UPackage* Package = Material->GetOutermost(); + OutPackages.AddUnique(Package); + + /* + // TODO: This should be handled in the mesh/instance translator + // If this is an instancer material, enable the instancing flag. + if (UniqueInstancerMaterialIds.Contains(MaterialId)) + Material->bUsedWithInstancedStaticMeshes = true; + */ + + // Reset material expressions. +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + Material->GetExpressionCollection().Empty(); +#else + Material->Expressions.Empty(); +#endif + + // Generate various components for this material. + bool bMaterialComponentCreated = false; + int32 MaterialNodeY = FHoudiniMaterialTranslator::MaterialExpressionNodeY; + + // By default we mark material as opaque. Some of component creators can change this. + Material->BlendMode = BLEND_Opaque; + + // Extract diffuse plane. + bMaterialComponentCreated |= FHoudiniMaterialTranslator::CreateMaterialComponentDiffuse( + InAssetId, AssetName, MaterialInfo, InPackageParams, Material, OutPackages, MaterialNodeY); + + // Extract metallic plane. + bMaterialComponentCreated |= FHoudiniMaterialTranslator::CreateMaterialComponentMetallic( + InAssetId, AssetName, MaterialInfo, InPackageParams, Material, OutPackages, MaterialNodeY); + + // Extract specular plane. + bMaterialComponentCreated |= FHoudiniMaterialTranslator::CreateMaterialComponentSpecular( + InAssetId, AssetName, MaterialInfo, InPackageParams, Material, OutPackages, MaterialNodeY); + + // Extract roughness plane. + bMaterialComponentCreated |= FHoudiniMaterialTranslator::CreateMaterialComponentRoughness( + InAssetId, AssetName, MaterialInfo, InPackageParams, Material, OutPackages, MaterialNodeY); + + // Extract emissive plane. + bMaterialComponentCreated |= FHoudiniMaterialTranslator::CreateMaterialComponentEmissive( + InAssetId, AssetName, MaterialInfo, InPackageParams, Material, OutPackages, MaterialNodeY); + + // Extract opacity plane. + bMaterialComponentCreated |= FHoudiniMaterialTranslator::CreateMaterialComponentOpacity( + InAssetId, AssetName, MaterialInfo, InPackageParams, Material, OutPackages, MaterialNodeY); + + // Extract opacity mask plane. + bMaterialComponentCreated |= FHoudiniMaterialTranslator::CreateMaterialComponentOpacityMask( + InAssetId, AssetName, MaterialInfo, InPackageParams, Material, OutPackages, MaterialNodeY); + + // Extract normal plane. + bMaterialComponentCreated |= FHoudiniMaterialTranslator::CreateMaterialComponentNormal( + InAssetId, AssetName, MaterialInfo, InPackageParams, Material, OutPackages, MaterialNodeY); + + // Set other material properties. + Material->TwoSided = true; + Material->SetShadingModel(MSM_DefaultLit); + + // Schedule this material for update. + MaterialUpdateContext.AddMaterial(Material); + + // Cache material. + OutMaterials.Add(MaterialIdentifier, Material); + + // Propagate and trigger material updates. + if (bCreatedNewMaterial) + FAssetRegistryModule::AssetCreated(Material); + + Material->PreEditChange(nullptr); + Material->PostEditChange(); + Material->MarkPackageDirty(); + } + + MaterialFactory->RemoveFromRoot(); + + return true; +} + +// +bool +FHoudiniMaterialTranslator::CreateMaterialInstances( + const FHoudiniGeoPartObject& InHGPO, + const FHoudiniPackageParams& InPackageParams, + const TMap& UniqueMaterialInstanceOverrides, + const TArray& InPackages, + const TMap>& InMaterials, + TMap>& OutMaterials, + const bool& bForceRecookAll) +{ + // Check the node ID is valid + if (InHGPO.AssetId < 0) + return false; + + // No material instance attributes + if (UniqueMaterialInstanceOverrides.Num() <= 0) + return false; + + for (const auto& Entry : UniqueMaterialInstanceOverrides) + { + const FHoudiniMaterialIdentifier& Identifier = Entry.Key; + const FHoudiniMaterialInfo& MatInfo = Entry.Value; + + const uint32 InstanceParametersGUID = FCrc::StrCrc32(*Identifier.MaterialInstanceParametersSlug); + + if (!MatInfo.bMakeMaterialInstance) + continue; + + // Try to find the material we want to create an instance of + UMaterialInterface* CurrentSourceMaterialInterface = Cast( + StaticLoadObject(UMaterialInterface::StaticClass(), nullptr, *MatInfo.MaterialObjectPath, nullptr, LOAD_NoWarn, nullptr)); + + if (!IsValid(CurrentSourceMaterialInterface)) + { + // Couldn't find the source material + HOUDINI_LOG_WARNING(TEXT("Couldn't find the source material %s to create a material instance."), *MatInfo.MaterialObjectPath); + continue; + } + + // Create/Retrieve the package for the MI + FString MaterialInstanceName; + FString MaterialInstanceNamePrefix = UPackageTools::SanitizePackageName( + CurrentSourceMaterialInterface->GetName() + TEXT("_instance_") + FString::Printf(TEXT("%u"), InstanceParametersGUID)); + + // See if we can find an existing package for that instance + UPackage* MaterialInstancePackage = nullptr; + const TObjectPtr * FoundMatPtr = InMaterials.Find(Identifier); + if (FoundMatPtr && *FoundMatPtr) + { + // We found an already existing MI, get its package + MaterialInstancePackage = Cast((*FoundMatPtr)->GetOuter()); + } + + if (MaterialInstancePackage) + { + MaterialInstanceName = MaterialInstancePackage->GetName(); + } + else + { + // We couldnt find the corresponding M_I package, so create a new one + MaterialInstancePackage = CreatePackageForMaterial(InHGPO.AssetId, MaterialInstanceNamePrefix, InPackageParams, MaterialInstanceName); + } + + // Couldn't create a package for that Material Instance + if (!MaterialInstancePackage) + continue; + + bool bNewMaterialCreated = false; + UMaterialInstanceConstant* NewMaterialInstance = LoadObject(MaterialInstancePackage, *MaterialInstanceName, nullptr, LOAD_None, nullptr); + if (!NewMaterialInstance) + { + // Factory to create materials. + UMaterialInstanceConstantFactoryNew* MaterialInstanceFactory = NewObject(); + if (!MaterialInstanceFactory) + continue; + + // Create the new material instance + MaterialInstanceFactory->AddToRoot(); + MaterialInstanceFactory->InitialParent = CurrentSourceMaterialInterface; + NewMaterialInstance = (UMaterialInstanceConstant*)MaterialInstanceFactory->FactoryCreateNew( + UMaterialInstanceConstant::StaticClass(), MaterialInstancePackage, FName(*MaterialInstanceName), + RF_Public | RF_Standalone, NULL, GWarn); + + if (NewMaterialInstance) + bNewMaterialCreated = true; + + MaterialInstanceFactory->RemoveFromRoot(); + } + + if (!NewMaterialInstance) + { + HOUDINI_LOG_WARNING(TEXT("Couldn't access the material instance for %s"), *MatInfo.MaterialObjectPath); + continue; + } + + // Update context for generated materials (will trigger when the object goes out of scope). + FMaterialUpdateContext MaterialUpdateContext; + + // Apply material instance parameters + bool bModifiedMaterialParameters = false; + for (const auto& MatParamEntry : MatInfo.MaterialInstanceParameters) + { + const FName& MaterialParameterName = MatParamEntry.Key; + const FHoudiniMaterialParameterValue& MaterialParameterValue = MatParamEntry.Value; + + // Try to update the material instance parameter corresponding to the attribute + if (UpdateMaterialInstanceParameter(MaterialParameterName, MaterialParameterValue, NewMaterialInstance, InPackages)) + bModifiedMaterialParameters = true; + } + + // Schedule this material for update if needed. + if (bNewMaterialCreated || bModifiedMaterialParameters) + MaterialUpdateContext.AddMaterialInstance(NewMaterialInstance); + + if (bNewMaterialCreated) + { + // Add meta information to this package. + FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + MaterialInstancePackage, NewMaterialInstance, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + MaterialInstancePackage, NewMaterialInstance, HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *MaterialInstanceName); + // Notify registry that we have created a new material. + FAssetRegistryModule::AssetCreated(NewMaterialInstance); + } + + if (bNewMaterialCreated || bModifiedMaterialParameters) + { + // Dirty the material + NewMaterialInstance->MarkPackageDirty(); + + // Update the material instance + NewMaterialInstance->InitStaticPermutation(); + NewMaterialInstance->PreEditChange(nullptr); + NewMaterialInstance->PostEditChange(); + /* + // Automatically save the package to avoid further issue + MaterialInstancePackage->SetDirtyFlag(true); + MaterialInstancePackage->FullyLoad(); + UPackage::SavePackage( + MaterialInstancePackage, nullptr, EObjectFlags::RF_Public | EObjectFlags::RF_Standalone, + *FPackageName::LongPackageNameToFilename(MaterialInstancePackage->GetName(), FPackageName::GetAssetPackageExtension())); + */ + } + + // Add the created material to the output assignement map + // Use the "source" material name as we want the instance to replace it + OutMaterials.Add(Identifier, NewMaterialInstance); + } + + return true; +} + +bool +FHoudiniMaterialTranslator::GetMaterialParameterAttributes( + const int32 InGeoId, + const int32 InPartId, + const HAPI_AttributeOwner InAttributeOwner, + TArray& OutAllMatParams, + const int32 InAttributeIndex) +{ + ensure(InAttributeOwner == HAPI_ATTROWNER_PRIM || InAttributeOwner == HAPI_ATTROWNER_POINT || InAttributeOwner == HAPI_ATTROWNER_DETAIL); + if (InAttributeOwner != HAPI_ATTROWNER_PRIM && InAttributeOwner != HAPI_ATTROWNER_POINT && InAttributeOwner != HAPI_ATTROWNER_DETAIL) + { + HOUDINI_LOG_WARNING(TEXT( + "[FHoudiniMaterialTranslator::GetMaterialParameterAttributes] Invalid InAttributeOwner: must be detail, " + "prim or point. Not fetching material parameters via attributes.")); + return false; + } + + OutAllMatParams.Empty(); + // Get the detail material parameters + FHoudiniEngineUtils::GetGenericAttributeList( + InGeoId, InPartId, HAPI_UNREAL_ATTRIB_GENERIC_MAT_PARAM_PREFIX, + OutAllMatParams, HAPI_ATTROWNER_DETAIL, -1); + + if (InAttributeOwner != HAPI_ATTROWNER_DETAIL) + { + FHoudiniEngineUtils::GetGenericAttributeList( + InGeoId, InPartId, HAPI_UNREAL_ATTRIB_GENERIC_MAT_PARAM_PREFIX, + OutAllMatParams, InAttributeOwner, InAttributeIndex); + } + + return true; +} + +bool FHoudiniMaterialTranslator::GetMaterialParameters( + FHoudiniMaterialInfo& MaterialInfo, + const TArray& InAllMatParams, + int32 InAttributeIndex) +{ + MaterialInfo.MaterialInstanceParameters.Empty(); + + // Material parameters are only relevant if we are making material instances + if (!MaterialInfo.bMakeMaterialInstance) + return true; + + // We have no material parameters, so nothing to do + if (InAllMatParams.Num() <= 0) + return true; + + // Try to find the material we want to create an instance of, so that we can determine valid parameter names + UMaterialInterface* CurrentSourceMaterialInterface = Cast( + StaticLoadObject(UMaterialInterface::StaticClass(), nullptr, *MaterialInfo.MaterialObjectPath, nullptr, LOAD_NoWarn, nullptr)); + + if (!IsValid(CurrentSourceMaterialInterface)) + { + // Couldn't find the source material + HOUDINI_LOG_WARNING(TEXT("Couldn't find the source material %s to create a material instance."), *MaterialInfo.MaterialObjectPath); + return false; + } + + const int32 NumMatParams = InAllMatParams.Num(); + for (int32 ParamIdx = 0; ParamIdx < NumMatParams; ParamIdx++) + { + const FHoudiniGenericAttribute& ParamOverride = InAllMatParams[ParamIdx]; + // Skip if the AttribName is empty + if (ParamOverride.AttributeName.IsEmpty()) + continue; + + // Copy the name since we'll remove the slot prefix if present + FString AttribName = ParamOverride.AttributeName; + + // If no index specified, assume it applies to all mats + int32 OverrideIndex = -1; + int32 TentativeIndex = 0; + char CurChar = AttribName[0]; + int32 AttribNameIndex = 0; + while (CurChar >= '0' && CurChar <= '9') + { + TentativeIndex *= 10; + TentativeIndex += CurChar - '0'; + CurChar = AttribName[++AttribNameIndex]; + } + if (CurChar == '_') + { + AttribName = AttribName.Mid(AttribNameIndex + 1); + OverrideIndex = TentativeIndex; + } + + if (OverrideIndex != -1 && OverrideIndex != MaterialInfo.MaterialIndex) + continue; + + // Check if AttribName is a valid parameter of our source material + FHoudiniMaterialParameterValue MaterialParameterValue; + if (!GetAndValidateMaterialInstanceParameterValue( + FName(AttribName), ParamOverride, InAttributeIndex, CurrentSourceMaterialInterface, MaterialParameterValue)) + { + continue; + } + + MaterialInfo.MaterialInstanceParameters.Add(FName(AttribName), MaterialParameterValue); + } + + return true; +} + +bool FHoudiniMaterialTranslator::GetMaterialParameters( + TArray& MaterialsByAttributeIndex, + const int32 InGeoId, + const int32 InPartId, + const HAPI_AttributeOwner InAttributeOwner) +{ + ensure(InAttributeOwner == HAPI_ATTROWNER_PRIM || InAttributeOwner == HAPI_ATTROWNER_POINT); + if (InAttributeOwner != HAPI_ATTROWNER_PRIM && InAttributeOwner != HAPI_ATTROWNER_POINT) + { + HOUDINI_LOG_WARNING(TEXT( + "[FHoudiniMaterialTranslator::GetMaterialParameters] Invalid InAttributeOwner: must be prim or point. Not " + "fetching material parameters via attributes.")); + return false; + } + + bool bHaveMaterialInstances = false; + for (const FHoudiniMaterialInfo& MatInfo : MaterialsByAttributeIndex) + { + if (!MatInfo.bMakeMaterialInstance) + continue; + + bHaveMaterialInstances = true; + break; + } + + // We have no material instances, no need to process material parameters + if (!bHaveMaterialInstances) + return true; + + TArray AllMatParams; + // See if we need to override some of the material instance's parameters + GetMaterialParameterAttributes(InGeoId, InPartId, InAttributeOwner, AllMatParams); + + // Map containing unique face materials override attribute + // and their first valid prim index + // We create only one material instance per attribute + for (int32 AttributeIndex = 0; AttributeIndex < MaterialsByAttributeIndex.Num(); AttributeIndex++) + { + FHoudiniMaterialInfo& MatInfo = MaterialsByAttributeIndex[AttributeIndex]; + if (!MatInfo.bMakeMaterialInstance) + continue; + + if (!GetMaterialParameters(MatInfo, AllMatParams, AttributeIndex)) + return false; + } + + return true; +} + +bool FHoudiniMaterialTranslator::SortUniqueFaceMaterialOverridesAndCreateMaterialInstances( + const TArray& Materials, + const FHoudiniGeoPartObject& InHGPO, + const FHoudiniPackageParams& InPackageParams, + const TArray& InPackages, + const TMap>& InMaterials, + TMap>& OutMaterials, + const bool& bForceRecookAll) +{ + // Map containing unique face materials override attribute + // and their first valid prim index + // We create only one material instance per attribute + int MaterialIndex = 0; + TMap UniqueFaceMaterialOverrides; + for (int MatArrayIdx = 0; MatArrayIdx < Materials.Num(); MatArrayIdx++) + { + FHoudiniMaterialInfo MatInfo = Materials[MatArrayIdx]; + + MatInfo.MaterialIndex = MaterialIndex; + if (!MatInfo.bMakeMaterialInstance) + { + FHoudiniMaterialIdentifier Identifier = MatInfo.MakeIdentifier(); + if (!UniqueFaceMaterialOverrides.Contains(Identifier)) + { + UniqueFaceMaterialOverrides.Add(Identifier, MatInfo); + MaterialIndex++; + } + continue; + } + + FHoudiniMaterialIdentifier Identifier = MatInfo.MakeIdentifier(); + if (UniqueFaceMaterialOverrides.Contains(Identifier)) + continue; + + UniqueFaceMaterialOverrides.Add(Identifier, MatInfo); + MaterialIndex++; + } + + return FHoudiniMaterialTranslator::CreateMaterialInstances( + InHGPO, InPackageParams, + UniqueFaceMaterialOverrides, InPackages, + InMaterials, OutMaterials, + bForceRecookAll); +} + +bool +FHoudiniMaterialTranslator::GetMaterialRelativePath(const HAPI_NodeId& InAssetId, const HAPI_NodeId& InMaterialNodeId, FString& OutRelativePath) +{ + HAPI_MaterialInfo MaterialInfo; + FHoudiniApi::MaterialInfo_Init(&MaterialInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetMaterialInfo( + FHoudiniEngine::Get().GetSession(), InMaterialNodeId, + &MaterialInfo), false); + + return GetMaterialRelativePath(InAssetId, MaterialInfo, OutRelativePath); +} +bool +FHoudiniMaterialTranslator::GetMaterialRelativePath(const HAPI_NodeId& InAssetId, const HAPI_MaterialInfo& InMaterialInfo, FString& OutRelativePath) +{ + if (InAssetId < 0 || !InMaterialInfo.exists) + return false; + + // We want to get the asset node path so we can remove it from the material name + FString AssetNodeName = TEXT(""); + { + HAPI_NodeId AssetNodeId = -1; + + HAPI_AssetInfo AssetInfo; + FHoudiniApi::AssetInfo_Init(&AssetInfo); + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetAssetInfo( + FHoudiniEngine::Get().GetSession(), InAssetId, &AssetInfo)) + { + AssetNodeId = AssetInfo.nodeId; + } + else + { + AssetNodeId = InAssetId; + } + + HAPI_NodeInfo AssetNodeInfo; + FHoudiniApi::NodeInfo_Init(&AssetNodeInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), AssetNodeId, &AssetNodeInfo), false); + + FHoudiniEngineString::ToFString(AssetNodeInfo.internalNodePathSH, AssetNodeName); + } + + // Get the material name from the info + FString MaterialNodeName = TEXT(""); + { + HAPI_NodeInfo MaterialNodeInfo; + FHoudiniApi::NodeInfo_Init(&MaterialNodeInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), InMaterialInfo.nodeId, &MaterialNodeInfo), false); + + FHoudiniEngineString::ToFString(MaterialNodeInfo.internalNodePathSH, MaterialNodeName); + } + + if (AssetNodeName.Len() > 0 && MaterialNodeName.Len() > 0) + { + // Remove AssetNodeName part from MaterialNodeName. Extra position is for separator. + OutRelativePath = MaterialNodeName.Mid(AssetNodeName.Len() + 1); + return true; + } + + return false; +} + +UPackage* +FHoudiniMaterialTranslator::CreatePackageForMaterial( + const HAPI_NodeId& InMaterialNodeId, + const FString& InMaterialName, + const FHoudiniPackageParams& InPackageParams, + FString& OutMaterialName) +{ + FString MaterialDescriptor = TEXT("_material_"); + if (InMaterialNodeId >= 0) + MaterialDescriptor += FString::FromInt(InMaterialNodeId) + TEXT("_"); + MaterialDescriptor += InMaterialName; + + FHoudiniPackageParams MyPackageParams = InPackageParams; + if (!MyPackageParams.ObjectName.IsEmpty()) + { + MyPackageParams.ObjectName += MaterialDescriptor; + } + else if (!MyPackageParams.HoudiniAssetName.IsEmpty()) + { + MyPackageParams.ObjectName = MyPackageParams.HoudiniAssetName + MaterialDescriptor; + } + else + { + MyPackageParams.ObjectName = MaterialDescriptor; + } + + return MyPackageParams.CreatePackageForObject(OutMaterialName); +} + +UMaterialExpression* +FHoudiniMaterialTranslator::MaterialLocateExpression(UMaterialExpression* Expression, UClass* ExpressionClass) +{ + if (!Expression) + return nullptr; + +#if WITH_EDITOR + if (ExpressionClass == Expression->GetClass()) + return Expression; + + // If this is a channel multiply expression, we can recurse. + UMaterialExpressionMultiply* MaterialExpressionMultiply = Cast(Expression); + if (MaterialExpressionMultiply) + { + { + UMaterialExpression* MaterialExpression = MaterialExpressionMultiply->A.Expression; + if (MaterialExpression) + { + if (MaterialExpression->GetClass() == ExpressionClass) + return MaterialExpression; + + MaterialExpression = FHoudiniMaterialTranslator::MaterialLocateExpression( + Cast(MaterialExpression), ExpressionClass); + + if (MaterialExpression) + return MaterialExpression; + } + } + + { + UMaterialExpression* MaterialExpression = MaterialExpressionMultiply->B.Expression; + if (MaterialExpression) + { + if (MaterialExpression->GetClass() == ExpressionClass) + return MaterialExpression; + + MaterialExpression = FHoudiniMaterialTranslator::MaterialLocateExpression( + Cast(MaterialExpression), ExpressionClass); + + if (MaterialExpression) + return MaterialExpression; + } + } + } +#endif + + return nullptr; +} + +void +FHoudiniMaterialTranslator::_AddMaterialExpression(UMaterial* InMaterial, UMaterialExpression* InMatExp) +{ + if (!InMaterial || !InMatExp) + return; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + // Access to material expressions has changed in UE5.1 + InMaterial->GetExpressionCollection().AddExpression(InMatExp); +#else + InMaterial->Expressions.Add(InMatExp); +#endif +} + +bool +FHoudiniMaterialTranslator::CreateTextureExpression( + UTexture2D* Texture, + UMaterialExpressionTextureSampleParameter2D*& TextureExpression, + UMaterialExpression*& MatInputExpression, + const bool SetMatInputExpression, + UMaterial* Material, + const EObjectFlags ObjectFlag, + const FString& GeneratingParameterName, + const EMaterialSamplerType SamplerType) +{ + // Create the sampling expression, if it hasn't been created yet. + if (!TextureExpression) + TextureExpression = NewObject( + Material, UMaterialExpressionTextureSampleParameter2D::StaticClass(), NAME_None, ObjectFlag); + + // Record generating parameter. + TextureExpression->Desc = GeneratingParameterName; + TextureExpression->ParameterName = *GeneratingParameterName; + + TextureExpression->Texture = Texture; + TextureExpression->SamplerType = SamplerType; + + // Assign expression to material. + _AddMaterialExpression(Material, TextureExpression); + if (SetMatInputExpression) + MatInputExpression = TextureExpression; + + return true; +} + +bool +FHoudiniMaterialTranslator::CreateMaterialComponentDiffuse( + const HAPI_NodeId& InAssetId, + const FString& InHoudiniAssetName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY) +{ + if (!IsValid(Material)) + return false; + + EObjectFlags ObjectFlag = (InPackageParams.PackageMode == EPackageMode::Bake) ? RF_Standalone : RF_NoFlags; + + // Names of generating Houdini parameters. + FString GeneratingParameterNameDiffuseTexture = TEXT(""); + FString GeneratingParameterNameUniformColor = TEXT(""); + FString GeneratingParameterNameVertexColor = TEXT(HAPI_UNREAL_ATTRIB_COLOR); + + // Diffuse texture creation parameters. + FCreateTexture2DParameters CreateTexture2DParameters; + CreateTexture2DParameters.SourceGuidHash = FGuid(); + CreateTexture2DParameters.bUseAlpha = false; + CreateTexture2DParameters.CompressionSettings = TC_Default; + CreateTexture2DParameters.bDeferCompression = true; + CreateTexture2DParameters.bSRGB = true; + + // Attempt to look up previously created expressions. +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UMaterialEditorOnlyData* MaterialEditorOnly = Material->GetEditorOnlyData(); + FColorMaterialInput& MatInputDiffuse = MaterialEditorOnly->BaseColor; +#else + FColorMaterialInput& MatInputDiffuse = Material->BaseColor; +#endif + + UMaterialExpressionVectorParameter* ExpressionBaseColor = + FHoudiniMaterialTranslator::CreateColorExpression(MatInputDiffuse.Expression, Material, ObjectFlag); + + // The diffuse constant color parameter on Principled Shaders is "basecolor", but COP Preview Material's diffuse texture parameter is also "basecolor". + // We search for parameters by name without checking the node type. If we're on a CPM, then we would accidentally find the texture parameter when we want a constant color. + // So, we check if we're in a CPM by looking for the CPM Diffuse Switch parameter. If we are in a CPM, don't search for "basecolor". + HAPI_ParmInfo CPMSwitchInfo; + HAPI_ParmId CPMSwitchId = FHoudiniEngineUtils::HapiFindParameterByName(InMaterialInfo.nodeId, HAPI_UNREAL_PARAM_MAP_DIFFUSE_CPM_SWITCH, CPMSwitchInfo); + const char* DiffuseString = CPMSwitchId >= 0 ? "" : HAPI_UNREAL_PARAM_COLOR_DIFFUSE; + + FHoudiniMaterialTranslator::SetColorExpression( + InMaterialInfo.nodeId, + DiffuseString, + HAPI_UNREAL_PARAM_COLOR_DIFFUSE_OGL, + HAPI_UNREAL_PARAM_COLOR_DIFFUSE_CPM, + HAPI_UNREAL_PARAM_COLOR_DIFFUSE_CPM_DEFAULT, + HAPI_UNREAL_PARAM_MAP_DIFFUSE_CPM_SWITCH, + ExpressionBaseColor, + GeneratingParameterNameUniformColor); + + UMaterialExpressionVertexColor* ExpressionVertexColor = + FHoudiniMaterialTranslator::CreateVertexColorExpression(MatInputDiffuse.Expression, Material, ObjectFlag, GeneratingParameterNameVertexColor); + + // Locate sampling expression and its texture. + UTexture2D* TextureDiffuse; + UMaterialExpressionTextureSampleParameter2D* ExpressionTextureSample; + FHoudiniMaterialTranslator::GetTextureAndExpression(MatInputDiffuse.Expression, true, TextureDiffuse, ExpressionTextureSample); + + // See if a diffuse texture is available. + // The diffuse constant color parameter on Principled Shaders is "basecolor", but COP Preview Material's diffuse texture parameter is also "basecolor". + // We search for parameters by name without checking the node type. If we're on a Principled Shader, then we would accidentally find the constant color parameter when we want a texture. + // So, we check if we're in a CPM by looking for the CPM Diffuse Switch parameter. If we aren't in a CPM, don't search for "basecolor". + const char* CPMDiffuseString = CPMSwitchId >= 0 ? HAPI_UNREAL_PARAM_MAP_DIFFUSE_CPM : ""; + HAPI_ParmInfo ParmDiffuseTextureInfo; + HAPI_ParmId ParmDiffuseTextureId = FHoudiniMaterialTranslator::FindTextureParam( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_MAP_DIFFUSE, + HAPI_UNREAL_PARAM_MAP_DIFFUSE_ENABLED, + HAPI_UNREAL_PARAM_MAP_DIFFUSE_OGL, + HAPI_UNREAL_PARAM_MAP_DIFFUSE_OGL_ENABLED, + CPMDiffuseString, + HAPI_UNREAL_PARAM_MAP_DIFFUSE_CPM_SWITCH, + ParmDiffuseTextureInfo, + GeneratingParameterNameDiffuseTexture); + + // If we have diffuse texture parameter. + if (ParmDiffuseTextureId >= 0) + { + HAPI_ImagePacking ImagePacking; + const char* PlaneType; + bool bFoundImagePlanes = FHoudiniTextureTranslator::GetPlaneInfo( + ParmDiffuseTextureId, InMaterialInfo, + ImagePacking, PlaneType, CreateTexture2DParameters.bUseAlpha); + + if (bFoundImagePlanes) + { + // Get the node path for the texture package's meta data + FString NodePath; + FHoudiniMaterialTranslator::GetMaterialRelativePath(InAssetId, InMaterialInfo.nodeId, NodePath); + + bool bTextureCreated = FHoudiniTextureTranslator::CreateTexture( + InMaterialInfo.nodeId, + PlaneType, + HAPI_IMAGE_DATA_INT8, + ImagePacking, + TextureDiffuse, + NodePath, + HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_DIFFUSE, + InPackageParams, + CreateTexture2DParameters, + TEXTUREGROUP_World, + OutPackages); + + if (bTextureCreated) + { + FHoudiniMaterialTranslator::CreateTextureExpression( + TextureDiffuse, + ExpressionTextureSample, + MatInputDiffuse.Expression, + false, + Material, + ObjectFlag, + GeneratingParameterNameDiffuseTexture, + SAMPLERTYPE_Color); + } + } + } + + MatInputDiffuse.Expression = FHoudiniMaterialTranslator::CreateMultiplyExpressions( + MatInputDiffuse.Expression, + ExpressionBaseColor, + ExpressionVertexColor, + ExpressionTextureSample, + Material, MaterialNodeY, ObjectFlag); + + return true; +} + +bool +FHoudiniMaterialTranslator::CreateMaterialComponentOpacityMask( + const HAPI_NodeId& InAssetId, + const FString& InHoudiniAssetName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY) +{ + if (!IsValid(Material)) + return false; + + bool bExpressionCreated = false; + + EObjectFlags ObjectFlag = (InPackageParams.PackageMode == EPackageMode::Bake) ? RF_Standalone : RF_NoFlags; + + // Name of generating Houdini parameters. + FString GeneratingParameterNameTexture = TEXT(""); + + // Opacity texture creation parameters. + FCreateTexture2DParameters CreateTexture2DParameters; + CreateTexture2DParameters.SourceGuidHash = FGuid(); + CreateTexture2DParameters.bUseAlpha = false; + CreateTexture2DParameters.CompressionSettings = TC_Grayscale; + CreateTexture2DParameters.bDeferCompression = true; + CreateTexture2DParameters.bSRGB = true; + + // Attempt to look up previously created expressions. +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UMaterialEditorOnlyData* MaterialEditorOnly = Material->GetEditorOnlyData(); + FScalarMaterialInput& MatInputOpacityMask = MaterialEditorOnly->OpacityMask; +#else + FScalarMaterialInput& MatInputOpacityMask = Material->OpacityMask; +#endif + + // See if opacity texture is available. + HAPI_ParmInfo ParmOpacityTextureInfo; + HAPI_ParmId ParmOpacityTextureId = FHoudiniMaterialTranslator::FindTextureParam( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_MAP_OPACITY, + HAPI_UNREAL_PARAM_MAP_OPACITY_ENABLED, + HAPI_UNREAL_PARAM_MAP_OPACITY_OGL, + HAPI_UNREAL_PARAM_MAP_OPACITY_OGL_ENABLED, + HAPI_UNREAL_PARAM_MAP_OPACITY_CPM, + HAPI_UNREAL_PARAM_MAP_OPACITY_CPM_SWITCH, + ParmOpacityTextureInfo, + GeneratingParameterNameTexture); + + // If we have opacity texture parameter. + if (ParmOpacityTextureId >= 0) + { + HAPI_ImagePacking ImagePacking; + const char* PlaneType; + bool bFoundImagePlanes = FHoudiniTextureTranslator::GetPlaneInfo( + ParmOpacityTextureId, InMaterialInfo, + ImagePacking, PlaneType, CreateTexture2DParameters.bUseAlpha); + + if (bFoundImagePlanes) + { + UTexture2D* TextureOpacity; + UMaterialExpressionTextureSampleParameter2D* ExpressionTextureOpacitySample; + FHoudiniMaterialTranslator::GetTextureAndExpression(MatInputOpacityMask.Expression, true, TextureOpacity, ExpressionTextureOpacitySample); + + // Get the node path for the texture package's meta data + FString NodePath; + FHoudiniMaterialTranslator::GetMaterialRelativePath(InAssetId, InMaterialInfo.nodeId, NodePath); + + bool bTextureCreated = FHoudiniTextureTranslator::CreateTexture( + InMaterialInfo.nodeId, + PlaneType, + HAPI_IMAGE_DATA_INT8, + ImagePacking, + TextureOpacity, + NodePath, + HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_OPACITY_MASK, + InPackageParams, + CreateTexture2DParameters, + TEXTUREGROUP_World, + OutPackages); + + if (bTextureCreated) + { + bExpressionCreated = FHoudiniMaterialTranslator::CreateTextureExpression( + TextureOpacity, + ExpressionTextureOpacitySample, + MatInputOpacityMask.Expression, + true, + Material, + ObjectFlag, + GeneratingParameterNameTexture, + SAMPLERTYPE_Grayscale); + } + + if (bExpressionCreated) + { + Material->BlendMode = BLEND_Masked; + + FHoudiniMaterialTranslator::PositionExpression(ExpressionTextureOpacitySample, MaterialNodeY, 0.0f); + + // We need to set material type to masked. + TArray ExpressionOutputs = ExpressionTextureOpacitySample->GetOutputs(); + FExpressionOutput* ExpressionOutput = ExpressionOutputs.GetData(); + + MatInputOpacityMask.Mask = ExpressionOutput->Mask; + MatInputOpacityMask.MaskR = 1; + MatInputOpacityMask.MaskG = 0; + MatInputOpacityMask.MaskB = 0; + MatInputOpacityMask.MaskA = 0; + } + } + } + + return bExpressionCreated; +} + +bool +FHoudiniMaterialTranslator::CreateMaterialComponentOpacity( + const HAPI_NodeId& InAssetId, + const FString& InHoudiniAssetName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY) +{ + if (!IsValid(Material)) + return false; + + bool bExpressionCreated = false; + bool bNeedsTranslucency = false; + + EObjectFlags ObjectFlag = (InPackageParams.PackageMode == EPackageMode::Bake) ? RF_Standalone : RF_NoFlags; + + // Name of generating Houdini parameters. + FString GeneratingParameterNameScalar = TEXT(""); + FString GeneratingParameterNameTexture = TEXT(""); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UMaterialEditorOnlyData* MaterialEditorOnly = Material->GetEditorOnlyData(); + FScalarMaterialInput& MatInputOpacity = MaterialEditorOnly->Opacity; +#else + FScalarMaterialInput& MatInputOpacity = Material->Opacity; +#endif + + // Opacity expressions. + UMaterialExpressionTextureSampleParameter2D* ExpressionTextureOpacitySample = nullptr; + UMaterialExpressionScalarParameter* ExpressionScalarOpacity = nullptr; + UTexture2D* TextureOpacity = nullptr; + + // If opacity sampling expression was not created, check if diffuse contains an alpha plane. + if (!ExpressionTextureOpacitySample) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UMaterialExpression* MaterialExpressionDiffuse = MaterialEditorOnly->BaseColor.Expression; +#else + UMaterialExpression* MaterialExpressionDiffuse = Material->BaseColor.Expression; +#endif + + if (MaterialExpressionDiffuse) + { + // Locate diffuse sampling expression. + UMaterialExpressionTextureSampleParameter2D* ExpressionTextureDiffuseSample = + Cast( + FHoudiniMaterialTranslator::MaterialLocateExpression( + MaterialExpressionDiffuse, + UMaterialExpressionTextureSampleParameter2D::StaticClass())); + + // See if there's an alpha plane in this expression's texture. + if (ExpressionTextureDiffuseSample) + { + UTexture2D* DiffuseTexture = Cast(ExpressionTextureDiffuseSample->Texture); + if (DiffuseTexture && !DiffuseTexture->CompressionNoAlpha) + { + // The diffuse texture has an alpha channel (that wasn't discarded), so we can use it + ExpressionTextureOpacitySample = ExpressionTextureDiffuseSample; + bNeedsTranslucency = true; + } + } + } + } + + // Retrieve opacity value + HAPI_ParmInfo ParmOpacityValueInfo; + HAPI_ParmId ParmOpacityValueId = + FHoudiniMaterialTranslator::FindConstantParam( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_ALPHA, + HAPI_UNREAL_PARAM_ALPHA_OGL, + HAPI_UNREAL_PARAM_ALPHA_CPM, + HAPI_UNREAL_PARAM_ALPHA_CPM_DEFAULT, + HAPI_UNREAL_PARAM_MAP_OPACITY_CPM_SWITCH, + ParmOpacityValueInfo, + GeneratingParameterNameScalar); + + if (ParmOpacityValueId >= 0) + { + if (ParmOpacityValueInfo.size > 0 && ParmOpacityValueInfo.floatValuesIndex >= 0) + { + float OpacityValue = 1.0f; + if (FHoudiniApi::GetParmFloatValues( + FHoudiniEngine::Get().GetSession(), InMaterialInfo.nodeId, + (float*)&OpacityValue, ParmOpacityValueInfo.floatValuesIndex, 1) == HAPI_RESULT_SUCCESS) + { + if (!ExpressionScalarOpacity) + { + ExpressionScalarOpacity = NewObject( + Material, UMaterialExpressionScalarParameter::StaticClass(), NAME_None, ObjectFlag); + } + + // Clamp retrieved value. + OpacityValue = FMath::Clamp(OpacityValue, 0.0f, 1.0f); + + // Set expression fields. + ExpressionScalarOpacity->DefaultValue = OpacityValue; + ExpressionScalarOpacity->SliderMin = 0.0f; + ExpressionScalarOpacity->SliderMax = 1.0f; + ExpressionScalarOpacity->Desc = GeneratingParameterNameScalar; + ExpressionScalarOpacity->ParameterName = *GeneratingParameterNameScalar; + + // Add expression. + _AddMaterialExpression(Material, ExpressionScalarOpacity); + + // If alpha is less than 1, we need translucency. + bNeedsTranslucency |= (OpacityValue != 1.0f); + } + } + } + + if (bNeedsTranslucency) + Material->BlendMode = BLEND_Translucent; + + if (ExpressionScalarOpacity && ExpressionTextureOpacitySample) + { + // We have both alpha and alpha uniform, attempt to locate multiply expression. + UMaterialExpressionMultiply* ExpressionMultiply = + Cast( + FHoudiniMaterialTranslator::MaterialLocateExpression( + MatInputOpacity.Expression, + UMaterialExpressionMultiply::StaticClass())); + + if (!ExpressionMultiply) + ExpressionMultiply = NewObject( + Material, UMaterialExpressionMultiply::StaticClass(), NAME_None, ObjectFlag); + + _AddMaterialExpression(Material, ExpressionMultiply); + + TArray ExpressionOutputs = ExpressionTextureOpacitySample->GetOutputs(); + FExpressionOutput* ExpressionOutput = ExpressionOutputs.GetData(); + + ExpressionMultiply->A.Expression = ExpressionTextureOpacitySample; + ExpressionMultiply->A.Mask = ExpressionOutput->Mask; + ExpressionMultiply->A.MaskR = 0; + ExpressionMultiply->A.MaskG = 0; + ExpressionMultiply->A.MaskB = 0; + ExpressionMultiply->A.MaskA = 1; + ExpressionMultiply->B.Expression = ExpressionScalarOpacity; + + MatInputOpacity.Expression = ExpressionMultiply; + + ExpressionMultiply->MaterialExpressionEditorX = FHoudiniMaterialTranslator::MaterialExpressionNodeX; + ExpressionMultiply->MaterialExpressionEditorY = MaterialNodeY; + + FHoudiniMaterialTranslator::PositionExpression(ExpressionScalarOpacity, MaterialNodeY, 1.0); + + bExpressionCreated = true; + } + else if (ExpressionScalarOpacity) + { + MatInputOpacity.Expression = ExpressionScalarOpacity; + + FHoudiniMaterialTranslator::PositionExpression(ExpressionScalarOpacity, MaterialNodeY, 0.0); + + bExpressionCreated = true; + } + else if (ExpressionTextureOpacitySample) + { + TArray ExpressionOutputs = ExpressionTextureOpacitySample->GetOutputs(); + FExpressionOutput* ExpressionOutput = ExpressionOutputs.GetData(); + + MatInputOpacity.Expression = ExpressionTextureOpacitySample; + MatInputOpacity.Mask = ExpressionOutput->Mask; + MatInputOpacity.MaskR = 0; + MatInputOpacity.MaskG = 0; + MatInputOpacity.MaskB = 0; + MatInputOpacity.MaskA = 1; + + bExpressionCreated = true; + } + + return bExpressionCreated; +} + +bool +FHoudiniMaterialTranslator::CreateMaterialComponentNormal( + const HAPI_NodeId& InAssetId, + const FString& InHoudiniAssetName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY) +{ + if (!IsValid(Material)) + return false; + + bool bExpressionCreated = false; + bool bTangentSpaceNormal = true; + + EObjectFlags ObjectFlag = (InPackageParams.PackageMode == EPackageMode::Bake) ? RF_Standalone : RF_NoFlags; + + // Name of generating Houdini parameter. + FString GeneratingParameterName = TEXT(""); + + // Normal texture creation parameters. + FCreateTexture2DParameters CreateTexture2DParameters; + CreateTexture2DParameters.SourceGuidHash = FGuid(); + CreateTexture2DParameters.bUseAlpha = false; + CreateTexture2DParameters.CompressionSettings = TC_Normalmap; + CreateTexture2DParameters.bDeferCompression = true; + CreateTexture2DParameters.bSRGB = false; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UMaterialEditorOnlyData* MaterialEditorOnly = Material->GetEditorOnlyData(); + FVectorMaterialInput& MatInputNormal = MaterialEditorOnly->Normal; +#else + FVectorMaterialInput& MatInputNormal = Material->Normal; +#endif + + // See if separate normal texture is available. + HAPI_ParmInfo ParmNormalTextureInfo; + HAPI_ParmId ParmNormalTextureId = FHoudiniMaterialTranslator::FindTextureParam( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_MAP_NORMAL, + HAPI_UNREAL_PARAM_MAP_NORMAL_ENABLED, + HAPI_UNREAL_PARAM_MAP_NORMAL_OGL, + "", + HAPI_UNREAL_PARAM_MAP_NORMAL_CPM, + "", + ParmNormalTextureInfo, + GeneratingParameterName); + + if (ParmNormalTextureId >= 0) + { + bTangentSpaceNormal = FHoudiniMaterialTranslator::RequiresWorldSpaceNormals(InMaterialInfo.nodeId); + + UTexture2D* Texture; + UMaterialExpressionTextureSampleParameter2D* TextureExpression; + FHoudiniMaterialTranslator::GetTextureAndExpression(MatInputNormal.Expression, false, Texture, TextureExpression); + + // Get the node path for the texture package's meta data + FString NodePath; + FHoudiniMaterialTranslator::GetMaterialRelativePath(InAssetId, InMaterialInfo.nodeId, NodePath); + + bool bRenderSuccessful = FHoudiniTextureTranslator::HapiRenderTexture(InMaterialInfo.nodeId, ParmNormalTextureId); + if (bRenderSuccessful) + { + bool bTextureCreated = FHoudiniTextureTranslator::CreateTexture( + InMaterialInfo.nodeId, + HAPI_UNREAL_MATERIAL_TEXTURE_COLOR, + HAPI_IMAGE_DATA_INT8, + HAPI_IMAGE_PACKING_RGBA, + Texture, + NodePath, + HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_NORMAL, + InPackageParams, + CreateTexture2DParameters, + TEXTUREGROUP_WorldNormalMap, + OutPackages); + + if (bTextureCreated) + { + bExpressionCreated = FHoudiniMaterialTranslator::CreateTextureExpression( + Texture, + TextureExpression, + MatInputNormal.Expression, + true, + Material, + ObjectFlag, + GeneratingParameterName, + SAMPLERTYPE_Normal); + } + } + + if (bExpressionCreated) + { + FHoudiniMaterialTranslator::PositionExpression(MatInputNormal.Expression, MaterialNodeY, 0.0f); + Material->bTangentSpaceNormal = bTangentSpaceNormal; + } + } + + // If separate normal map was not found, see if normal plane exists in diffuse map. + if (!bExpressionCreated) + { + // See if diffuse texture is available. + // The diffuse constant color parameter on Principled Shaders is "basecolor", but COP Preview Material's diffuse texture parameter is also "basecolor". + // We search for parameters by name without checking the node type. If we're on a Principled Shader, then we would accidentally find the constant color parameter when we want a texture. + // So, we check if we're in a CPM by looking for the CPM Diffuse Switch parameter. If we aren't in a CPM, don't search for "basecolor". + HAPI_ParmInfo CPMSwitchInfo; + HAPI_ParmId CPMSwitchId = FHoudiniEngineUtils::HapiFindParameterByName(InMaterialInfo.nodeId, HAPI_UNREAL_PARAM_MAP_DIFFUSE_CPM_SWITCH, CPMSwitchInfo); + const char* CPMDiffuseString = CPMSwitchId >= 0 ? HAPI_UNREAL_PARAM_MAP_DIFFUSE_CPM : ""; + HAPI_ParmInfo ParmDiffuseTextureInfo; + HAPI_ParmId ParmDiffuseTextureId = FHoudiniMaterialTranslator::FindTextureParam( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_MAP_DIFFUSE, + HAPI_UNREAL_PARAM_MAP_DIFFUSE_ENABLED, + HAPI_UNREAL_PARAM_MAP_DIFFUSE_OGL, + HAPI_UNREAL_PARAM_MAP_DIFFUSE_OGL_ENABLED, + CPMDiffuseString, + HAPI_UNREAL_PARAM_MAP_DIFFUSE_CPM_SWITCH, + ParmDiffuseTextureInfo, + GeneratingParameterName); + + // If normal plane is available in diffuse map. + if (ParmDiffuseTextureId >= 0) + { + UTexture2D* Texture; + UMaterialExpressionTextureSampleParameter2D* TextureExpression; + FHoudiniMaterialTranslator::GetTextureAndExpression(MatInputNormal.Expression, false, Texture, TextureExpression); + + // Get the node path for the texture package's meta data + FString NodePath; + FHoudiniMaterialTranslator::GetMaterialRelativePath(InAssetId, InMaterialInfo.nodeId, NodePath); + + bool bRenderSuccessful = FHoudiniTextureTranslator::HapiRenderTexture(InMaterialInfo.nodeId, ParmDiffuseTextureId); + if (bRenderSuccessful) + { + bool bTextureCreated = FHoudiniTextureTranslator::CreateTexture( + InMaterialInfo.nodeId, + HAPI_UNREAL_MATERIAL_TEXTURE_NORMAL, + HAPI_IMAGE_DATA_INT8, + HAPI_IMAGE_PACKING_RGB, + Texture, + NodePath, + HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_NORMAL, + InPackageParams, + CreateTexture2DParameters, + TEXTUREGROUP_WorldNormalMap, + OutPackages); + + if (bTextureCreated) + { + bExpressionCreated = FHoudiniMaterialTranslator::CreateTextureExpression( + Texture, + TextureExpression, + MatInputNormal.Expression, + true, + Material, + ObjectFlag, + GeneratingParameterName, + SAMPLERTYPE_Normal); + } + } + + if (bExpressionCreated) + { + FHoudiniMaterialTranslator::PositionExpression(MatInputNormal.Expression, MaterialNodeY, 0.0f); + Material->bTangentSpaceNormal = bTangentSpaceNormal; + } + } + } + + return bExpressionCreated; +} + +bool +FHoudiniMaterialTranslator::CreateMaterialComponentSpecular( + const HAPI_NodeId& InAssetId, + const FString& InHoudiniAssetName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY) +{ + if (!IsValid(Material)) + return false; + + bool bExpressionCreated = false; + + EObjectFlags ObjectFlag = (InPackageParams.PackageMode == EPackageMode::Bake) ? RF_Standalone : RF_NoFlags; + + // Name of generating Houdini parameter. + FString GeneratingParameterName = TEXT(""); + + // Specular texture creation parameters. + FCreateTexture2DParameters CreateTexture2DParameters; + CreateTexture2DParameters.SourceGuidHash = FGuid(); + CreateTexture2DParameters.bUseAlpha = false; + CreateTexture2DParameters.CompressionSettings = TC_Grayscale; + CreateTexture2DParameters.bDeferCompression = true; + CreateTexture2DParameters.bSRGB = false; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UMaterialEditorOnlyData* MaterialEditorOnly = Material->GetEditorOnlyData(); + FScalarMaterialInput& MatInputSpecular = MaterialEditorOnly->Specular; +#else + FScalarMaterialInput& MatInputSpecular = Material->Specular; +#endif + + // See if specular texture is available. + HAPI_ParmInfo ParmSpecularTextureInfo; + HAPI_ParmId ParmSpecularTextureId = FHoudiniMaterialTranslator::FindTextureParam( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_MAP_SPECULAR, + HAPI_UNREAL_PARAM_MAP_SPECULAR_ENABLED, + HAPI_UNREAL_PARAM_MAP_SPECULAR_OGL, + HAPI_UNREAL_PARAM_MAP_SPECULAR_OGL_ENABLED, + HAPI_UNREAL_PARAM_MAP_SPECULAR_CPM, + HAPI_UNREAL_PARAM_MAP_SPECULAR_CPM_SWITCH, + ParmSpecularTextureInfo, + GeneratingParameterName); + + if (ParmSpecularTextureId >= 0) + { + UTexture2D* Texture; + UMaterialExpressionTextureSampleParameter2D* TextureExpression; + FHoudiniMaterialTranslator::GetTextureAndExpression(MatInputSpecular.Expression, false, Texture, TextureExpression); + + // Get the node path for the texture package's meta data + FString NodePath; + FHoudiniMaterialTranslator::GetMaterialRelativePath(InAssetId, InMaterialInfo.nodeId, NodePath); + + bool bRenderSuccessful = FHoudiniTextureTranslator::HapiRenderTexture(InMaterialInfo.nodeId, ParmSpecularTextureId); + if (bRenderSuccessful) + { + bool bTextureCreated = FHoudiniTextureTranslator::CreateTexture( + InMaterialInfo.nodeId, + HAPI_UNREAL_MATERIAL_TEXTURE_COLOR, + HAPI_IMAGE_DATA_INT8, + HAPI_IMAGE_PACKING_RGBA, + Texture, + NodePath, + HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_SPECULAR, + InPackageParams, + CreateTexture2DParameters, + TEXTUREGROUP_World, + OutPackages); + + if (bTextureCreated) + { + bExpressionCreated = FHoudiniMaterialTranslator::CreateTextureExpression( + Texture, + TextureExpression, + MatInputSpecular.Expression, + true, + Material, + ObjectFlag, + GeneratingParameterName, + SAMPLERTYPE_LinearGrayscale); + } + } + + if (bExpressionCreated) + FHoudiniMaterialTranslator::PositionExpression(MatInputSpecular.Expression, MaterialNodeY, 0.0f); + } + + if (!bExpressionCreated) + bExpressionCreated = FHoudiniMaterialTranslator::CreateScalarExpressionFromFloatParam( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_VALUE_SPECULAR, + HAPI_UNREAL_PARAM_VALUE_SPECULAR_OGL, + HAPI_UNREAL_PARAM_VALUE_SPECULAR_CPM, + HAPI_UNREAL_PARAM_VALUE_SPECULAR_CPM_DEFAULT, + HAPI_UNREAL_PARAM_MAP_SPECULAR_CPM_SWITCH, + MatInputSpecular.Expression, Material, MaterialNodeY, ObjectFlag); + + return bExpressionCreated; +} + +bool +FHoudiniMaterialTranslator::CreateMaterialComponentRoughness( + const HAPI_NodeId& InAssetId, + const FString& InHoudiniAssetName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY) +{ + if (!IsValid(Material)) + return false; + + bool bExpressionCreated = false; + + EObjectFlags ObjectFlag = (InPackageParams.PackageMode == EPackageMode::Bake) ? RF_Standalone : RF_NoFlags; + + // Name of generating Houdini parameter. + FString GeneratingParameterName = TEXT(""); + + // Roughness texture creation parameters. + FCreateTexture2DParameters CreateTexture2DParameters; + CreateTexture2DParameters.SourceGuidHash = FGuid(); + CreateTexture2DParameters.bUseAlpha = false; + CreateTexture2DParameters.CompressionSettings = TC_Grayscale; + CreateTexture2DParameters.bDeferCompression = true; + CreateTexture2DParameters.bSRGB = false; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UMaterialEditorOnlyData* MaterialEditorOnly = Material->GetEditorOnlyData(); + FScalarMaterialInput& MatInputRoughness = MaterialEditorOnly->Roughness; +#else + FScalarMaterialInput& MatInputRoughness = Material->Roughness; +#endif + + // See if roughness texture is available. + HAPI_ParmInfo ParmRoughnessTextureInfo; + HAPI_ParmId ParmRoughnessTextureId = FHoudiniMaterialTranslator::FindTextureParam( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_MAP_ROUGHNESS, + HAPI_UNREAL_PARAM_MAP_ROUGHNESS_ENABLED, + HAPI_UNREAL_PARAM_MAP_ROUGHNESS_OGL, + HAPI_UNREAL_PARAM_MAP_ROUGHNESS_OGL_ENABLED, + HAPI_UNREAL_PARAM_MAP_ROUGHNESS_CPM, + HAPI_UNREAL_PARAM_MAP_ROUGHNESS_CPM_SWITCH, + ParmRoughnessTextureInfo, + GeneratingParameterName); + + if (ParmRoughnessTextureId >= 0) + { + UTexture2D* Texture; + UMaterialExpressionTextureSampleParameter2D* TextureExpression; + FHoudiniMaterialTranslator::GetTextureAndExpression(MatInputRoughness.Expression, false, Texture, TextureExpression); + + // Get the node path for the texture package's meta data + FString NodePath; + FHoudiniMaterialTranslator::GetMaterialRelativePath(InAssetId, InMaterialInfo.nodeId, NodePath); + + bool bRenderSuccessful = FHoudiniTextureTranslator::HapiRenderTexture(InMaterialInfo.nodeId, ParmRoughnessTextureId); + if (bRenderSuccessful) + { + bool bTextureCreated = FHoudiniTextureTranslator::CreateTexture( + InMaterialInfo.nodeId, + HAPI_UNREAL_MATERIAL_TEXTURE_COLOR, + HAPI_IMAGE_DATA_INT8, + HAPI_IMAGE_PACKING_RGBA, + Texture, + NodePath, + HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_ROUGHNESS, + InPackageParams, + CreateTexture2DParameters, + TEXTUREGROUP_World, + OutPackages); + + if (bTextureCreated) + { + bExpressionCreated = FHoudiniMaterialTranslator::CreateTextureExpression( + Texture, + TextureExpression, + MatInputRoughness.Expression, + true, + Material, + ObjectFlag, + GeneratingParameterName, + SAMPLERTYPE_LinearGrayscale); + } + } + + if (bExpressionCreated) + FHoudiniMaterialTranslator::PositionExpression(MatInputRoughness.Expression, MaterialNodeY, 0.0f); + } + + if (!bExpressionCreated) + bExpressionCreated = FHoudiniMaterialTranslator::CreateScalarExpressionFromFloatParam( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_VALUE_ROUGHNESS, + HAPI_UNREAL_PARAM_VALUE_ROUGHNESS_OGL, + HAPI_UNREAL_PARAM_VALUE_ROUGHNESS_CPM, + HAPI_UNREAL_PARAM_VALUE_ROUGHNESS_CPM_DEFAULT, + HAPI_UNREAL_PARAM_MAP_ROUGHNESS_CPM_SWITCH, + MatInputRoughness.Expression, Material, MaterialNodeY, ObjectFlag); + + return bExpressionCreated; +} + +bool +FHoudiniMaterialTranslator::CreateMaterialComponentMetallic( + const HAPI_NodeId& InAssetId, + const FString& InHoudiniAssetName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY) +{ + if (!IsValid(Material)) + return false; + + bool bExpressionCreated = false; + + EObjectFlags ObjectFlag = (InPackageParams.PackageMode == EPackageMode::Bake) ? RF_Standalone : RF_NoFlags; + + // Name of generating Houdini parameter. + FString GeneratingParameterName = TEXT(""); + + // Metallic texture creation parameters. + FCreateTexture2DParameters CreateTexture2DParameters; + CreateTexture2DParameters.SourceGuidHash = FGuid(); + CreateTexture2DParameters.bUseAlpha = false; + CreateTexture2DParameters.CompressionSettings = TC_Grayscale; + CreateTexture2DParameters.bDeferCompression = true; + CreateTexture2DParameters.bSRGB = false; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UMaterialEditorOnlyData* MaterialEditorOnly = Material->GetEditorOnlyData(); + FScalarMaterialInput& MatInputMetallic = MaterialEditorOnly->Metallic; +#else + FScalarMaterialInput& MatInputMetallic = Material->Metallic; +#endif + + // See if metallic texture is available. + HAPI_ParmInfo ParmMetallicTextureInfo; + HAPI_ParmId ParmMetallicTextureId = FHoudiniMaterialTranslator::FindTextureParam( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_MAP_METALLIC, + HAPI_UNREAL_PARAM_MAP_METALLIC_ENABLED, + HAPI_UNREAL_PARAM_MAP_METALLIC_OGL, + HAPI_UNREAL_PARAM_MAP_METALLIC_OGL_ENABLED, + HAPI_UNREAL_PARAM_MAP_METALLIC_CPM, + HAPI_UNREAL_PARAM_MAP_METALLIC_CPM_SWITCH, + ParmMetallicTextureInfo, + GeneratingParameterName); + + if (ParmMetallicTextureId >= 0) + { + UTexture2D* Texture; + UMaterialExpressionTextureSampleParameter2D* TextureExpression; + FHoudiniMaterialTranslator::GetTextureAndExpression(MatInputMetallic.Expression, false, Texture, TextureExpression); + + // Get the node path for the texture package's meta data + FString NodePath; + FHoudiniMaterialTranslator::GetMaterialRelativePath(InAssetId, InMaterialInfo.nodeId, NodePath); + + bool bRenderSuccessful = FHoudiniTextureTranslator::HapiRenderTexture(InMaterialInfo.nodeId, ParmMetallicTextureId); + if (bRenderSuccessful) + { + bool bTextureCreated = FHoudiniTextureTranslator::CreateTexture( + InMaterialInfo.nodeId, + HAPI_UNREAL_MATERIAL_TEXTURE_COLOR, + HAPI_IMAGE_DATA_INT8, + HAPI_IMAGE_PACKING_RGBA, + Texture, + NodePath, + HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_METALLIC, + InPackageParams, + CreateTexture2DParameters, + TEXTUREGROUP_World, + OutPackages); + + if (bTextureCreated) + { + bExpressionCreated = FHoudiniMaterialTranslator::CreateTextureExpression( + Texture, + TextureExpression, + MatInputMetallic.Expression, + true, + Material, + ObjectFlag, + GeneratingParameterName, + SAMPLERTYPE_LinearGrayscale); + } + } + + if (bExpressionCreated) + FHoudiniMaterialTranslator::PositionExpression(MatInputMetallic.Expression, MaterialNodeY, 0.0f); + } + + if (!bExpressionCreated) + bExpressionCreated = FHoudiniMaterialTranslator::CreateScalarExpressionFromFloatParam( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_VALUE_METALLIC, + HAPI_UNREAL_PARAM_VALUE_METALLIC_OGL, + HAPI_UNREAL_PARAM_VALUE_METALLIC_CPM, + HAPI_UNREAL_PARAM_VALUE_METALLIC_CPM_DEFAULT, + HAPI_UNREAL_PARAM_MAP_METALLIC_CPM_SWITCH, + MatInputMetallic.Expression, Material, MaterialNodeY, ObjectFlag); + + return bExpressionCreated; +} + +bool +FHoudiniMaterialTranslator::CreateMaterialComponentEmissive( + const HAPI_NodeId& InAssetId, + const FString& InHoudiniAssetName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY) +{ + if (!IsValid(Material)) + return false; + + EObjectFlags ObjectFlag = (InPackageParams.PackageMode == EPackageMode::Bake) ? RF_Standalone : RF_NoFlags; + + // Names of generating Houdini parameters. + FString GeneratingParameterNameEmissiveTexture = TEXT(""); + FString GeneratingParameterNameEmissiveColor = TEXT(""); + FString GeneratingParameterNameEmissiveIntensity = TEXT(""); + + // Emissive texture creation parameters. + FCreateTexture2DParameters CreateTexture2DParameters; + CreateTexture2DParameters.SourceGuidHash = FGuid(); + CreateTexture2DParameters.bUseAlpha = false; + CreateTexture2DParameters.CompressionSettings = TC_Default; + CreateTexture2DParameters.bDeferCompression = true; + CreateTexture2DParameters.bSRGB = true; + + // Attempt to look up previously created expressions. +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UMaterialEditorOnlyData* MaterialEditorOnly = Material->GetEditorOnlyData(); + FColorMaterialInput& MatInputEmissive = MaterialEditorOnly->EmissiveColor; +#else + FColorMaterialInput& MatInputEmissive = Material->EmissiveColor; +#endif + + // Locate emissive color expression. + UMaterialExpressionVectorParameter* ExpressionEmissiveColor = + FHoudiniMaterialTranslator::CreateColorExpression(MatInputEmissive.Expression, Material, ObjectFlag); + FHoudiniMaterialTranslator::SetColorExpression( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_VALUE_EMISSIVE, + HAPI_UNREAL_PARAM_VALUE_EMISSIVE_OGL, + HAPI_UNREAL_PARAM_VALUE_EMISSIVE_CPM, + HAPI_UNREAL_PARAM_VALUE_EMISSIVE_CPM_DEFAULT, + HAPI_UNREAL_PARAM_MAP_EMISSIVE_CPM_SWITCH, + ExpressionEmissiveColor, + GeneratingParameterNameEmissiveColor); + + // Locate emissive intensity expression. + UMaterialExpressionScalarParameter* ExpressionEmissiveIntensity = + FHoudiniMaterialTranslator::CreateScalarExpression(MatInputEmissive.Expression, Material, ObjectFlag, GeneratingParameterNameEmissiveIntensity); + + // See if emissive intensity is available. + HAPI_ParmInfo ParmEmissiveIntensityInfo; + HAPI_ParmId ParmEmissiveIntensityId = FHoudiniMaterialTranslator::FindConstantParam( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_VALUE_EMISSIVE_INTENSITY, + HAPI_UNREAL_PARAM_VALUE_EMISSIVE_INTENSITY_OGL, + HAPI_UNREAL_PARAM_VALUE_EMISSIVE_INTENSITY_CPM, + HAPI_UNREAL_PARAM_VALUE_EMISSIVE_INTENSITY_CPM_DEFAULT, + HAPI_UNREAL_PARAM_MAP_EMISSIVE_INTENSITY_CPM_SWITCH, + ParmEmissiveIntensityInfo, + GeneratingParameterNameEmissiveIntensity); + + bool bHasEmissiveIntensity = false; + float EmmissiveIntensity = 0.0f; + if (ParmEmissiveIntensityId >= 0) + { + if (FHoudiniApi::GetParmFloatValues( + FHoudiniEngine::Get().GetSession(), InMaterialInfo.nodeId, (float*)&EmmissiveIntensity, + ParmEmissiveIntensityInfo.floatValuesIndex, 1) == HAPI_RESULT_SUCCESS) + { + bHasEmissiveIntensity = true; + } + } + + ExpressionEmissiveIntensity->DefaultValue = EmmissiveIntensity; + ExpressionEmissiveIntensity->Desc = GeneratingParameterNameEmissiveIntensity; + ExpressionEmissiveIntensity->ParameterName = *GeneratingParameterNameEmissiveIntensity; + + // Locate sampling expression and its texture. + UTexture2D* TextureEmissive; + UMaterialExpressionTextureSampleParameter2D* ExpressionTextureSample; + FHoudiniMaterialTranslator::GetTextureAndExpression(MatInputEmissive.Expression, true, TextureEmissive, ExpressionTextureSample); + + // See if an emissive texture is available. + HAPI_ParmInfo ParmEmissiveTextureInfo; + HAPI_ParmId ParmEmissiveTextureId = FHoudiniMaterialTranslator::FindTextureParam( + InMaterialInfo.nodeId, + HAPI_UNREAL_PARAM_MAP_EMISSIVE, + HAPI_UNREAL_PARAM_MAP_EMISSIVE_ENABLED, + HAPI_UNREAL_PARAM_MAP_EMISSIVE_OGL, + HAPI_UNREAL_PARAM_MAP_EMISSIVE_OGL_ENABLED, + HAPI_UNREAL_PARAM_MAP_EMISSIVE_CPM, + HAPI_UNREAL_PARAM_MAP_EMISSIVE_CPM_SWITCH, + ParmEmissiveTextureInfo, + GeneratingParameterNameEmissiveTexture); + + // If we have an emissive texture parameter. + if (ParmEmissiveTextureId >= 0) + { + HAPI_ImagePacking ImagePacking; + const char* PlaneType; + bool bFoundImagePlanes = FHoudiniTextureTranslator::GetPlaneInfo( + ParmEmissiveTextureId, InMaterialInfo, + ImagePacking, PlaneType, CreateTexture2DParameters.bUseAlpha); + + if (bFoundImagePlanes) + { + // Get the node path for the texture package's meta data + FString NodePath; + FHoudiniMaterialTranslator::GetMaterialRelativePath(InAssetId, InMaterialInfo.nodeId, NodePath); + + bool bTextureCreated = FHoudiniTextureTranslator::CreateTexture( + InMaterialInfo.nodeId, + PlaneType, + HAPI_IMAGE_DATA_INT8, + ImagePacking, + TextureEmissive, + NodePath, + HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_EMISSIVE, + InPackageParams, + CreateTexture2DParameters, + TEXTUREGROUP_World, + OutPackages); + + if (bTextureCreated) + { + FHoudiniMaterialTranslator::CreateTextureExpression( + TextureEmissive, + ExpressionTextureSample, + MatInputEmissive.Expression, + false, + Material, + ObjectFlag, + GeneratingParameterNameEmissiveTexture, + SAMPLERTYPE_Color); + } + } + } + + MatInputEmissive.Expression = FHoudiniMaterialTranslator::CreateMultiplyExpressions( + MatInputEmissive.Expression, + ExpressionEmissiveColor, + ExpressionEmissiveIntensity, + ExpressionTextureSample, + Material, MaterialNodeY, ObjectFlag); + + return true; +} + +bool +FHoudiniMaterialTranslator::GetAndValidateMaterialInstanceParameterValue( + const FName& InMaterialParameterName, + const FHoudiniGenericAttribute& MaterialParameterAttribute, + const int32 InAttributeIndex, + UMaterialInterface* const MaterialInterface, + FHoudiniMaterialParameterValue& OutMaterialParameterValue) +{ + // This function is tightly coupled with UpdateMaterialInstanceParameter(): changes to the one function likely + // require changes to the other! +#if WITH_EDITOR + if (!MaterialInterface) + return false; + + if (InMaterialParameterName.IsNone()) + return false; + + if (MaterialParameterAttribute.AttributeOwner == EAttribOwner::Invalid) + return false; + + int32 ValueIdx = InAttributeIndex; + if (ValueIdx < 0 || MaterialParameterAttribute.AttributeOwner == EAttribOwner::Detail) + ValueIdx = 0; + + // Check if the MaterialParameter corresponds to material instance property first + static const FName FloatProperties[] + { + "EmissiveBoost", + "DiffuseBoost", + "ExportResolutionScale", + "OpacityMaskClipValue", + }; + static const FName BoolProperties[] + { + "CastShadowAsMasked", + "TwoSided", + "DitheredLODTransition", + }; + + for (const FName& PropertyName : FloatProperties) + { + if (InMaterialParameterName != PropertyName) + continue; + + if (MaterialParameterAttribute.AttributeTupleSize != 1) + return false; + + OutMaterialParameterValue.ParamType = EHoudiniUnrealMaterialParameterType::StandardParameter; + OutMaterialParameterValue.SetValue((float)MaterialParameterAttribute.GetDoubleValue(ValueIdx)); + + return true; + } + + for (const FName& PropertyName : BoolProperties) + { + if (InMaterialParameterName != PropertyName) + continue; + + if (MaterialParameterAttribute.AttributeTupleSize != 1) + return false; + + OutMaterialParameterValue.ParamType = EHoudiniUnrealMaterialParameterType::StandardParameter; + OutMaterialParameterValue.SetValue(static_cast(MaterialParameterAttribute.GetBoolValue(ValueIdx))); + + return true; + } + + if (InMaterialParameterName == "BlendMode") + { + if (MaterialParameterAttribute.AttributeTupleSize != 1) + return false; + + OutMaterialParameterValue.ParamType = EHoudiniUnrealMaterialParameterType::StandardParameter; + EBlendMode EnumValue = (EBlendMode)MaterialParameterAttribute.GetIntValue(ValueIdx); + if (MaterialParameterAttribute.AttributeType == EAttribStorageType::STRING) + { + FString StringValue = MaterialParameterAttribute.GetStringValue(ValueIdx); + if (StringValue.Compare("Opaque", ESearchCase::IgnoreCase) == 0) + EnumValue = EBlendMode::BLEND_Opaque; + else if (StringValue.Compare("Masked", ESearchCase::IgnoreCase) == 0) + EnumValue = EBlendMode::BLEND_Masked; + else if (StringValue.Compare("Translucent", ESearchCase::IgnoreCase) == 0) + EnumValue = EBlendMode::BLEND_Translucent; + else if (StringValue.Compare("Additive", ESearchCase::IgnoreCase) == 0) + EnumValue = EBlendMode::BLEND_Additive; + else if (StringValue.Compare("Modulate", ESearchCase::IgnoreCase) == 0) + EnumValue = EBlendMode::BLEND_Modulate; + else if (StringValue.StartsWith("Alpha", ESearchCase::IgnoreCase)) + EnumValue = EBlendMode::BLEND_AlphaComposite; + } + OutMaterialParameterValue.SetValue(static_cast(EnumValue)); + + return true; + } + + if (InMaterialParameterName == "ShadingModel") + { + if (MaterialParameterAttribute.AttributeTupleSize != 1) + return false; + + OutMaterialParameterValue.ParamType = EHoudiniUnrealMaterialParameterType::StandardParameter; + EMaterialShadingModel EnumValue = (EMaterialShadingModel)MaterialParameterAttribute.GetIntValue(ValueIdx); + if (MaterialParameterAttribute.AttributeType == EAttribStorageType::STRING) + { + FString StringValue = MaterialParameterAttribute.GetStringValue(ValueIdx); + if (StringValue.Compare("Unlit", ESearchCase::IgnoreCase) == 0) + EnumValue = EMaterialShadingModel::MSM_Unlit; + else if (StringValue.StartsWith("Default", ESearchCase::IgnoreCase)) + EnumValue = EMaterialShadingModel::MSM_DefaultLit; + else if (StringValue.Compare("Subsurface", ESearchCase::IgnoreCase) == 0) + EnumValue = EMaterialShadingModel::MSM_Subsurface; + else if (StringValue.StartsWith("Preintegrated", ESearchCase::IgnoreCase)) + EnumValue = EMaterialShadingModel::MSM_PreintegratedSkin; + else if (StringValue.StartsWith("Clear", ESearchCase::IgnoreCase)) + EnumValue = EMaterialShadingModel::MSM_ClearCoat; + else if (StringValue.Compare("SubsurfaceProfile", ESearchCase::IgnoreCase) == 0) + EnumValue = EMaterialShadingModel::MSM_SubsurfaceProfile; + else if (StringValue.Compare("TwoSidedFoliage", ESearchCase::IgnoreCase) == 0) + EnumValue = EMaterialShadingModel::MSM_TwoSidedFoliage; + else if (StringValue.Compare("Hair", ESearchCase::IgnoreCase) == 0) + EnumValue = EMaterialShadingModel::MSM_Hair; + else if (StringValue.Compare("Cloth", ESearchCase::IgnoreCase) == 0) + EnumValue = EMaterialShadingModel::MSM_Cloth; + else if (StringValue.Compare("Eye", ESearchCase::IgnoreCase) == 0) + EnumValue = EMaterialShadingModel::MSM_Eye; + } + OutMaterialParameterValue.SetValue(static_cast(EnumValue)); + + return true; + } + + if (InMaterialParameterName == "PhysMaterial") + { + if (MaterialParameterAttribute.AttributeTupleSize != 1) + return false; + + OutMaterialParameterValue.ParamType = EHoudiniUnrealMaterialParameterType::StandardParameter; + OutMaterialParameterValue.SetValue(MaterialParameterAttribute.GetStringValue(ValueIdx)); + + return true; + } + + // Handling custom material parameters + if (MaterialParameterAttribute.AttributeType == EAttribStorageType::STRING) + { + // If there is no texture parameter by this name, return false (parameter should be excluded) + UTexture* OldTexture = nullptr; + if (!MaterialInterface->GetTextureParameterValue(InMaterialParameterName, OldTexture)) + return false; + + TArray StringTuple; + MaterialParameterAttribute.GetStringTuple(StringTuple, ValueIdx); + if (StringTuple.Num() <= 0) + return false; + + OutMaterialParameterValue.ParamType = EHoudiniUnrealMaterialParameterType::Texture; + OutMaterialParameterValue.SetValue(StringTuple[0]); + + return true; + } + + if (MaterialParameterAttribute.AttributeTupleSize == 1) + { + // Single attributes are either for scalar parameters or static switches + float OldValue; + if (MaterialInterface->GetScalarParameterValue(InMaterialParameterName, OldValue)) + { + // The material parameter is a scalar + OutMaterialParameterValue.ParamType = EHoudiniUnrealMaterialParameterType::Scalar; + OutMaterialParameterValue.SetValue((float)MaterialParameterAttribute.GetDoubleValue(ValueIdx)); + + return true; + } + + // See if the underlying parameter is a static switch + // We need to iterate over the material's static parameter set + FStaticParameterSet StaticParameters; +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2) + MaterialInterface->GetStaticParameterValues(StaticParameters); +#else + GetStaticParameterValues(MaterialInterface, StaticParameters); +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + TArray& StaticSwitchParams = StaticParameters.StaticSwitchParameters; +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 1 + TArray& StaticSwitchParams = StaticParameters.EditorOnly.StaticSwitchParameters; +#else + TArray& StaticSwitchParams = StaticParameters.StaticSwitchParameters; +#endif + for (int32 SwitchParameterIdx = 0; SwitchParameterIdx < StaticSwitchParams.Num(); ++SwitchParameterIdx) + { + const FStaticSwitchParameter& SwitchParameter = StaticSwitchParams[SwitchParameterIdx]; + if (SwitchParameter.ParameterInfo.Name != InMaterialParameterName) + continue; + + OutMaterialParameterValue.ParamType = EHoudiniUnrealMaterialParameterType::StaticSwitch; + OutMaterialParameterValue.SetValue(static_cast(MaterialParameterAttribute.GetBoolValue(ValueIdx))); + return true; + } + + return false; + } + + // Tuple attributes are for vector parameters + FLinearColor OldValue; + if (!MaterialInterface->GetVectorParameterValue(InMaterialParameterName, OldValue)) + return false; + + FLinearColor NewLinearColor(0, 0, 0, 0); + // if the attribute is stored in an int, we'll have to convert a color to a linear color + if (MaterialParameterAttribute.AttributeType == EAttribStorageType::INT || MaterialParameterAttribute.AttributeType == EAttribStorageType::INT64) + { + TArray IntTuple; + MaterialParameterAttribute.GetIntTuple(IntTuple, ValueIdx); + + FColor IntColor(0, 0, 0, 0); + if (IntTuple.IsValidIndex(0)) + IntColor.R = (int8)IntTuple[0]; + if (IntTuple.IsValidIndex(1)) + IntColor.G = (int8)IntTuple[1]; + if (IntTuple.IsValidIndex(2)) + IntColor.B = (int8)IntTuple[2]; + if (IntTuple.IsValidIndex(3)) + IntColor.A = (int8)IntTuple[3]; + else + IntColor.A = 1; + + NewLinearColor = FLinearColor(IntColor); + } + else + { + TArray DoubleTuple; + MaterialParameterAttribute.GetDoubleTuple(DoubleTuple, ValueIdx); + if (DoubleTuple.IsValidIndex(0)) + NewLinearColor.R = (float)DoubleTuple[0]; + if (DoubleTuple.IsValidIndex(1)) + NewLinearColor.G = (float)DoubleTuple[1]; + if (DoubleTuple.IsValidIndex(2)) + NewLinearColor.B = (float)DoubleTuple[2]; + if (DoubleTuple.IsValidIndex(3)) + NewLinearColor.A = (float)DoubleTuple[3]; + else + NewLinearColor.A = 1.0f; + } + + OutMaterialParameterValue.ParamType = EHoudiniUnrealMaterialParameterType::Vector; + OutMaterialParameterValue.SetValue(NewLinearColor); + return true; +#else + return false; +#endif +} + +bool +FHoudiniMaterialTranslator::UpdateMaterialInstanceParameter( + const FName& InMaterialParameterName, + const FHoudiniMaterialParameterValue& InMaterialParameterValue, + UMaterialInstanceConstant* MaterialInstance, + const TArray& InPackages) +{ + // This function is tightly coupled with GetAndValidateMaterialInstanceParameterValue(): changes to the one function + // likely require changes to the other! + +#if WITH_EDITOR + if (!MaterialInstance) + return false; + + if (InMaterialParameterName.IsNone()) + return false; + + // The default material instance parameters needs to be handled manually as they cant be changed via generic SetParameters functions + switch (InMaterialParameterValue.ParamType) + { + case EHoudiniUnrealMaterialParameterType::Invalid: + return false; + case EHoudiniUnrealMaterialParameterType::StandardParameter: + { + if (InMaterialParameterName == "CastShadowAsMasked") + { + if (InMaterialParameterValue.DataType != EHoudiniUnrealMaterialParameterDataType::Byte) + return false; + const bool Value = static_cast(InMaterialParameterValue.ByteValue); + + // Update the parameter value only if necessary + if (MaterialInstance->GetOverrideCastShadowAsMasked() && (MaterialInstance->GetCastShadowAsMasked() == Value)) + return false; + + MaterialInstance->SetOverrideCastShadowAsMasked(true); + MaterialInstance->SetCastShadowAsMasked(Value); + + return true; + } + + if (InMaterialParameterName == "EmissiveBoost") + { + if (InMaterialParameterValue.DataType != EHoudiniUnrealMaterialParameterDataType::Float) + return false; + const float Value = InMaterialParameterValue.FloatValue; + + // Update the parameter value only if necessary + if (MaterialInstance->GetOverrideEmissiveBoost() && (MaterialInstance->GetEmissiveBoost() == Value)) + return false; + + MaterialInstance->SetOverrideEmissiveBoost(true); + MaterialInstance->SetEmissiveBoost(Value); + + return true; + } + + if (InMaterialParameterName == "DiffuseBoost") + { + if (InMaterialParameterValue.DataType != EHoudiniUnrealMaterialParameterDataType::Float) + return false; + const float Value = InMaterialParameterValue.FloatValue; + + // Update the parameter value only if necessary + if (MaterialInstance->GetOverrideDiffuseBoost() && (MaterialInstance->GetDiffuseBoost() == Value)) + return false; + + MaterialInstance->SetOverrideDiffuseBoost(true); + MaterialInstance->SetDiffuseBoost(Value); + + return true; + } + + if (InMaterialParameterName == "ExportResolutionScale") + { + if (InMaterialParameterValue.DataType != EHoudiniUnrealMaterialParameterDataType::Float) + return false; + const float Value = InMaterialParameterValue.FloatValue; + + // Update the parameter value only if necessary + if (MaterialInstance->GetOverrideExportResolutionScale() && (MaterialInstance->GetExportResolutionScale() == Value)) + return false; + + MaterialInstance->SetOverrideExportResolutionScale(true); + MaterialInstance->SetExportResolutionScale(Value); + + return true; + } + + if (InMaterialParameterName == "OpacityMaskClipValue") + { + if (InMaterialParameterValue.DataType != EHoudiniUnrealMaterialParameterDataType::Float) + return false; + const float Value = InMaterialParameterValue.FloatValue; + + // Update the parameter value only if necessary + if (MaterialInstance->BasePropertyOverrides.bOverride_OpacityMaskClipValue && (MaterialInstance->BasePropertyOverrides.OpacityMaskClipValue == Value)) + return false; + + MaterialInstance->BasePropertyOverrides.bOverride_OpacityMaskClipValue = true; + MaterialInstance->BasePropertyOverrides.OpacityMaskClipValue = Value; + + return true; + } + + if (InMaterialParameterName == "BlendMode") + { + if (InMaterialParameterValue.DataType != EHoudiniUnrealMaterialParameterDataType::Byte) + return false; + const EBlendMode EnumValue = (EBlendMode)InMaterialParameterValue.ByteValue; + + // Update the parameter value only if necessary + if (MaterialInstance->BasePropertyOverrides.bOverride_BlendMode && (MaterialInstance->BasePropertyOverrides.BlendMode == EnumValue)) + return false; + + MaterialInstance->BasePropertyOverrides.bOverride_BlendMode = true; + MaterialInstance->BasePropertyOverrides.BlendMode = EnumValue; + + return true; + } + + if (InMaterialParameterName == "ShadingModel") + { + if (InMaterialParameterValue.DataType != EHoudiniUnrealMaterialParameterDataType::Byte) + return false; + const EMaterialShadingModel EnumValue = (EMaterialShadingModel)InMaterialParameterValue.ByteValue; + + // Update the parameter value only if necessary + if (MaterialInstance->BasePropertyOverrides.bOverride_ShadingModel && (MaterialInstance->BasePropertyOverrides.ShadingModel == EnumValue)) + return false; + + MaterialInstance->BasePropertyOverrides.bOverride_ShadingModel = true; + MaterialInstance->BasePropertyOverrides.ShadingModel = EnumValue; + + return true; + } + + if (InMaterialParameterName == "TwoSided") + { + if (InMaterialParameterValue.DataType != EHoudiniUnrealMaterialParameterDataType::Byte) + return false; + const bool Value = static_cast(InMaterialParameterValue.ByteValue); + + // Update the parameter value only if necessary + if (MaterialInstance->BasePropertyOverrides.bOverride_TwoSided && (MaterialInstance->BasePropertyOverrides.TwoSided == Value)) + return false; + + MaterialInstance->BasePropertyOverrides.bOverride_TwoSided = true; + MaterialInstance->BasePropertyOverrides.TwoSided = Value; + + return true; + } + + if (InMaterialParameterName == "DitheredLODTransition") + { + if (InMaterialParameterValue.DataType != EHoudiniUnrealMaterialParameterDataType::Byte) + return false; + const bool Value = static_cast(InMaterialParameterValue.ByteValue); + + // Update the parameter value only if necessary + if (MaterialInstance->BasePropertyOverrides.bOverride_DitheredLODTransition && (MaterialInstance->BasePropertyOverrides.DitheredLODTransition == Value)) + return false; + + MaterialInstance->BasePropertyOverrides.bOverride_DitheredLODTransition = true; + MaterialInstance->BasePropertyOverrides.DitheredLODTransition = Value; + + return true; + } + + if (InMaterialParameterName == "PhysMaterial") + { + // Try to load a Material corresponding to the parameter value + if (InMaterialParameterValue.DataType != EHoudiniUnrealMaterialParameterDataType::String) + return false; + + UPhysicalMaterial* FoundPhysMaterial = Cast( + StaticLoadObject(UPhysicalMaterial::StaticClass(), nullptr, *InMaterialParameterValue.StringValue, nullptr, LOAD_NoWarn, nullptr)); + + // Update the parameter value if necessary + if (!FoundPhysMaterial || (MaterialInstance->PhysMaterial == FoundPhysMaterial)) + return false; + + MaterialInstance->PhysMaterial = FoundPhysMaterial; + + return true; + } + break; + } + // Handling custom parameters + case EHoudiniUnrealMaterialParameterType::Texture: + { + // String attributes are used for textures parameters + // We need to find the texture corresponding to the param + UTexture* FoundTexture = nullptr; + const FString ParamValue = InMaterialParameterValue.StringValue; + + // Texture can either be already existing texture assets in UE4, or a newly generated textures by this asset + // Try to find the texture corresponding to the param value in the existing assets first. + FoundTexture = Cast( + StaticLoadObject(UTexture::StaticClass(), nullptr, *ParamValue, nullptr, LOAD_NoWarn, nullptr)); + + if (!FoundTexture) + { + // We couldn't find a texture corresponding to the parameter in the existing UE4 assets + // Try to find the corresponding texture in the cooked temporary package we just generated + FoundTexture = FHoudiniMaterialTranslator::FindGeneratedTexture(ParamValue, InPackages); + } + + if (!FoundTexture) + return false; + + // Do not update if unnecessary + UTexture* OldTexture = nullptr; + bool FoundOldParam = MaterialInstance->GetTextureParameterValue(InMaterialParameterName, OldTexture); + if (FoundOldParam && (OldTexture == FoundTexture)) + return false; + + MaterialInstance->SetTextureParameterValueEditorOnly(InMaterialParameterName, FoundTexture); + return true; + } + case EHoudiniUnrealMaterialParameterType::Scalar: + { + // Single attributes are either for scalar parameters or static switches + float OldValue; + if (!MaterialInstance->GetScalarParameterValue(InMaterialParameterName, OldValue)) + return false; + // The material parameter is a scalar + if (InMaterialParameterValue.DataType != EHoudiniUnrealMaterialParameterDataType::Float) + return false; + const float NewValue = InMaterialParameterValue.FloatValue; + + // Do not update if unnecessary + if (OldValue == NewValue) + return false; + + MaterialInstance->SetScalarParameterValueEditorOnly(InMaterialParameterName, NewValue); + + return true; + } + case EHoudiniUnrealMaterialParameterType::StaticSwitch: + { + // See if the underlying parameter is a static switch + if (InMaterialParameterValue.DataType != EHoudiniUnrealMaterialParameterDataType::Byte) + return false; + const bool NewBoolValue = static_cast(InMaterialParameterValue.ByteValue); + + // We need to iterate over the material's static parameter set + FStaticParameterSet StaticParameters; + MaterialInstance->GetStaticParameterValues(StaticParameters); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + TArray& StaticSwitchParams = StaticParameters.StaticSwitchParameters; +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 1 + TArray& StaticSwitchParams = StaticParameters.EditorOnly.StaticSwitchParameters; +#else + TArray& StaticSwitchParams = StaticParameters.StaticSwitchParameters; +#endif + for (int32 SwitchParameterIdx = 0; SwitchParameterIdx < StaticSwitchParams.Num(); ++SwitchParameterIdx) + { + FStaticSwitchParameter& SwitchParameter = StaticSwitchParams[SwitchParameterIdx]; + if (SwitchParameter.ParameterInfo.Name != InMaterialParameterName) + continue; + + if (SwitchParameter.Value == NewBoolValue) + return false; + + SwitchParameter.Value = NewBoolValue; + SwitchParameter.bOverride = true; + + MaterialInstance->UpdateStaticPermutation(StaticParameters); + return true; + } + + return false; + } + case EHoudiniUnrealMaterialParameterType::Vector: + { + if (InMaterialParameterValue.DataType != EHoudiniUnrealMaterialParameterDataType::Vector) + return false; + const FLinearColor NewLinearColor = InMaterialParameterValue.VectorValue; + + // Do not update if unnecessary + FLinearColor OldValue; + bool FoundOldParam = MaterialInstance->GetVectorParameterValue(InMaterialParameterName, OldValue); + if (FoundOldParam && (OldValue == NewLinearColor)) + return false; + + MaterialInstance->SetVectorParameterValueEditorOnly(InMaterialParameterName, NewLinearColor); + return true; + } + } +#endif + + return false; +} + +UTexture* +FHoudiniMaterialTranslator::FindGeneratedTexture(const FString& TextureString, const TArray& InPackages) +{ + if (TextureString.IsEmpty()) + return nullptr; + + // Try to find the corresponding texture in the cooked temporary package generated by an HDA + UTexture* FoundTexture = nullptr; + for (const auto& CurrentPackage : InPackages) + { + // Iterate through the cooked packages + if (!IsValid(CurrentPackage)) + continue; + + // First, check if the package contains a texture + FString CurrentPackageName = CurrentPackage->GetName(); + UTexture* PackageTexture = LoadObject(CurrentPackage, *CurrentPackageName, nullptr, LOAD_None, nullptr); + if (!PackageTexture) + continue; + + // Then check if the package's metadata match what we're looking for + // Make sure this texture was generated by Houdini Engine +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FMetaData MetaData = CurrentPackage->GetMetaData(); + if (!MetaData.HasValue(PackageTexture, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT)) + continue; +#else + UMetaData* MetaData = CurrentPackage->GetMetaData(); + if (!MetaData || !MetaData->HasValue(PackageTexture, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT)) + continue; +#endif + // Get the texture type from the meta data + // Texture type store has meta data will be C_A, N, S, R etc.. +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + const FString TextureTypeString = MetaData.GetValue(PackageTexture, HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_TYPE); +#else + const FString TextureTypeString = MetaData->GetValue(PackageTexture, HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_TYPE); +#endif + if (TextureTypeString.Compare(TextureString, ESearchCase::IgnoreCase) == 0) + { + FoundTexture = PackageTexture; + break; + } + + // Convert the texture type to a "friendly" version + // C_A to diffuse, N to Normal, S to Specular etc... + FString TextureTypeFriendlyString = TextureTypeString; + FString TextureTypeFriendlyAlternateString = TEXT(""); + if (TextureTypeString.Compare(HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_DIFFUSE, ESearchCase::IgnoreCase) == 0) + { + TextureTypeFriendlyString = TEXT("diffuse"); + TextureTypeFriendlyAlternateString = TEXT("basecolor"); + } + else if (TextureTypeString.Compare(HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_NORMAL, ESearchCase::IgnoreCase) == 0) + TextureTypeFriendlyString = TEXT("normal"); + else if (TextureTypeString.Compare(HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_EMISSIVE, ESearchCase::IgnoreCase) == 0) + TextureTypeFriendlyString = TEXT("emissive"); + else if (TextureTypeString.Compare(HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_SPECULAR, ESearchCase::IgnoreCase) == 0) + TextureTypeFriendlyString = TEXT("specular"); + else if (TextureTypeString.Compare(HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_ROUGHNESS, ESearchCase::IgnoreCase) == 0) + TextureTypeFriendlyString = TEXT("roughness"); + else if (TextureTypeString.Compare(HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_METALLIC, ESearchCase::IgnoreCase) == 0) + TextureTypeFriendlyString = TEXT("metallic"); + else if (TextureTypeString.Compare(HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_OPACITY_MASK, ESearchCase::IgnoreCase) == 0) + TextureTypeFriendlyString = TEXT("opacity"); + + // See if we have a match between the texture string and the friendly name + if ((TextureTypeFriendlyString.Compare(TextureString, ESearchCase::IgnoreCase) == 0) + || (!TextureTypeFriendlyAlternateString.IsEmpty() && TextureTypeFriendlyAlternateString.Compare(TextureString, ESearchCase::IgnoreCase) == 0)) + { + FoundTexture = PackageTexture; + break; + } + + // Get the node path from the meta data +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + const FString NodePath = MetaData.GetValue(PackageTexture, HAPI_UNREAL_PACKAGE_META_NODE_PATH); +#else + const FString NodePath = MetaData->GetValue(PackageTexture, HAPI_UNREAL_PACKAGE_META_NODE_PATH); +#endif + if (NodePath.IsEmpty()) + continue; + + // See if we have a match with the path and texture type + FString PathAndType = NodePath + TEXT("/") + TextureTypeString; + if (PathAndType.Compare(TextureString, ESearchCase::IgnoreCase) == 0) + { + FoundTexture = PackageTexture; + break; + } + + // See if we have a match with the friendly path and texture type + FString PathAndFriendlyType = NodePath + TEXT("/") + TextureTypeFriendlyString; + if (PathAndFriendlyType.Compare(TextureString, ESearchCase::IgnoreCase) == 0) + { + FoundTexture = PackageTexture; + break; + } + + // Try the alternate friendly string + if (!TextureTypeFriendlyAlternateString.IsEmpty()) + { + PathAndFriendlyType = NodePath + TEXT("/") + TextureTypeFriendlyAlternateString; + if (PathAndFriendlyType.Compare(TextureString, ESearchCase::IgnoreCase) == 0) + { + FoundTexture = PackageTexture; + break; + } + } + } + + return FoundTexture; +} + +bool +FHoudiniMaterialTranslator::FindTextureParamByNameOrTag( + const HAPI_NodeId& InNodeId, + const std::string& InTextureParmName, + const std::string& InUseTextureParmName, + const bool& bFindByTag, + const bool& bIsCPM, + HAPI_ParmId& OutParmId, + HAPI_ParmInfo& OutParmInfo) +{ + OutParmId = -1; + + if(bFindByTag) + OutParmId = FHoudiniEngineUtils::HapiFindParameterByTag(InNodeId, InTextureParmName, OutParmInfo); + else + OutParmId = FHoudiniEngineUtils::HapiFindParameterByName(InNodeId, InTextureParmName, OutParmInfo); + + if (OutParmId < 0) + { + // Failed to find the texture + return false; + } + + // We found a valid parameter, check if the matching "use" parameter exists + HAPI_ParmInfo FoundUseParmInfo; + HAPI_ParmId FoundUseParmId = -1; + if(bFindByTag) + FoundUseParmId = FHoudiniEngineUtils::HapiFindParameterByTag(InNodeId, InUseTextureParmName, FoundUseParmInfo); + else + FoundUseParmId = FHoudiniEngineUtils::HapiFindParameterByName(InNodeId, InUseTextureParmName, FoundUseParmInfo); + + if (FoundUseParmId >= 0) + { + // We found a valid "use" parameter, check its value to see if the texture should be used + if (!bIsCPM) + { + int32 UseValue = 0; + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetParmIntValues( + FHoudiniEngine::Get().GetSession(), + InNodeId, &UseValue, FoundUseParmInfo.intValuesIndex, 1)) + { + if (UseValue == 0) + { + // We found the texture parm, but the "use" param/tag is disabled, so don't use it! + // We still return true as we found the parameter, this will prevent looking for other parms + OutParmId = -1; + return true; + } + } + } + else + { + float UseValue = 0; + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + InNodeId, &UseValue, FoundUseParmInfo.floatValuesIndex, 1)) + { + // For some reason, in CPMs the switch parameter is a float, but it's used like a boolean. + // 0.0 means the source is either File or COP, and 1.0 means the source is Constant. + if (UseValue != 0.0f) + { + // We still return true as we found the parameter, this will prevent looking for other parms + OutParmId = -1; + return true; + } + } + } + } + + // Finally, make sure that the found texture Parm is not empty! + FString ParmValue = FString(); + HAPI_StringHandle StringHandle; + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetParmStringValues( + FHoudiniEngine::Get().GetSession(), + InNodeId, false, &StringHandle, OutParmInfo.stringValuesIndex, 1)) + { + // Convert the string handle to FString + FHoudiniEngineString::ToFString(StringHandle, ParmValue); + } + + if (ParmValue.IsEmpty()) + { + // We found the parm, but it's empty, don't use it! + // We still return true as we found the parameter, this will prevent looking for other parms + OutParmId = -1; + return true; + } + + return true; +} + +HAPI_ParmId +FHoudiniMaterialTranslator::FindConstantParam( + const HAPI_NodeId& NodeId, + const char* Name, + const char* Tag, + const char* CPMConst, + const char* CPMDefault, + const char* CPMSwitch, + HAPI_ParmInfo& Info, + FString& GeneratingParameterName) +{ + // Attempt to get the parameter by name. + HAPI_ParmId ValueId = FHoudiniEngineUtils::HapiFindParameterByName(NodeId, Name, Info); + if (ValueId >= 0) + { + GeneratingParameterName = FString(Name); + return ValueId; + } + + // Attempt to get the parameter by tag. + ValueId = FHoudiniEngineUtils::HapiFindParameterByTag(NodeId, Tag, Info); + if (ValueId >= 0) + { + GeneratingParameterName = FString(Tag); + return ValueId; + } + + // Attempt to get the COP Preview Material constant parameter. + // First, check that the switch is set to 1.0. + HAPI_ParmInfo CPMSwitchInfo; + HAPI_ParmId CPMSwitchId = FHoudiniEngineUtils::HapiFindParameterByName(NodeId, CPMSwitch, CPMSwitchInfo); + if (CPMSwitchId >= 0) + { + float CPMSwitchValue = 0; + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + NodeId, &CPMSwitchValue, CPMSwitchInfo.floatValuesIndex, 1)) + { + // For some reason, in CPMs the switch parameter is a float, but it's used like a boolean. + // 0.0 means the source is either File or COP, and 1.0 means the source is Constant. + if (CPMSwitchValue != 0.0) { + ValueId = FHoudiniEngineUtils::HapiFindParameterByName(NodeId, CPMConst, Info); + if (ValueId >= 0) + { + GeneratingParameterName = FString(CPMConst); + return ValueId; + } + } + } + } + + // Attempt to get the COP Preview Material default parameter. + ValueId = FHoudiniEngineUtils::HapiFindParameterByName(NodeId, CPMDefault, Info); + if (ValueId >= 0) + { + GeneratingParameterName = FString(CPMDefault); + return ValueId; + } + + return -1; +} + +HAPI_ParmId +FHoudiniMaterialTranslator::FindTextureParam( + const HAPI_NodeId& NodeId, + const char* Name, + const char* NameEnabled, + const char* Tag, + const char* TagEnabled, + const char* CPMName, + const char* CPMSwitch, + HAPI_ParmInfo& TextureInfo, + FString& GeneratingParameterName) +{ + HAPI_ParmId TextureId = -1; + + if (FHoudiniMaterialTranslator::FindTextureParamByNameOrTag( + NodeId, Name, NameEnabled, false, false, TextureId, TextureInfo)) + { + // Found via parm name + GeneratingParameterName = FString(Name); + return TextureId; + } + + if (FHoudiniMaterialTranslator::FindTextureParamByNameOrTag( + NodeId, Tag, TagEnabled, true, false, TextureId, TextureInfo)) + { + // Found via OGL tag + GeneratingParameterName = FString(Tag); + return TextureId; + } + + if (FHoudiniMaterialTranslator::FindTextureParamByNameOrTag( + NodeId, CPMName, CPMSwitch, false, true, TextureId, TextureInfo)) + { + // Found via COP Preview Material parm name + GeneratingParameterName = FString(CPMName); + return TextureId; + } + + return -1; +} + +void +FHoudiniMaterialTranslator::PositionExpression( + UMaterialExpression* Expression, + int32& MaterialNodeY, + const float HorizontalPositionScale) +{ + Expression->MaterialExpressionEditorX = FHoudiniMaterialTranslator::MaterialExpressionNodeX - + FHoudiniMaterialTranslator::MaterialExpressionNodeStepX * HorizontalPositionScale; + Expression->MaterialExpressionEditorY = MaterialNodeY; + MaterialNodeY += FHoudiniMaterialTranslator::MaterialExpressionNodeStepY; +} + +bool +FHoudiniMaterialTranslator::RequiresWorldSpaceNormals(HAPI_NodeId HapiMaterial) +{ + // Retrieve space for this normal texture. + HAPI_ParmInfo ParmInfoNormalType; + int32 ParmNormalTypeId = + FHoudiniEngineUtils::HapiFindParameterByTag(HapiMaterial, HAPI_UNREAL_PARAM_MAP_NORMAL_TYPE, ParmInfoNormalType); + + // Retrieve value for normal type choice list (if exists). + if (ParmNormalTypeId >= 0) + { + FString NormalType = TEXT(HAPI_UNREAL_PARAM_MAP_NORMAL_TYPE_TANGENT); + if (ParmInfoNormalType.size > 0 && ParmInfoNormalType.stringValuesIndex >= 0) + { + HAPI_StringHandle StringHandle; + if (FHoudiniApi::GetParmStringValues( + FHoudiniEngine::Get().GetSession(), + HapiMaterial, false, &StringHandle, ParmInfoNormalType.stringValuesIndex, ParmInfoNormalType.size) == HAPI_RESULT_SUCCESS) + { + // Get the actual string value. + FString NormalTypeString = TEXT(""); + FHoudiniEngineString HoudiniEngineString(StringHandle); + if (HoudiniEngineString.ToFString(NormalTypeString)) + NormalType = NormalTypeString; + } + } + + if (NormalType.Equals(TEXT(HAPI_UNREAL_PARAM_MAP_NORMAL_TYPE_WORLD), ESearchCase::IgnoreCase)) + return false; + } + + return true; +} + +UMaterialExpressionVertexColor* +FHoudiniMaterialTranslator::CreateVertexColorExpression( + UMaterialExpression* ExistingExpression, + UMaterial* Material, + const EObjectFlags& ObjectFlag, + const FString& GeneratingParameterName) +{ + // If the expression already exists, use that. + UMaterialExpressionVertexColor* VertexExpression = + Cast(FHoudiniMaterialTranslator::MaterialLocateExpression( + ExistingExpression, UMaterialExpressionVertexColor::StaticClass())); + + // Otherwise, create it. + if (!IsValid(VertexExpression)) + { + VertexExpression = NewObject( + Material, UMaterialExpressionVertexColor::StaticClass(), NAME_None, ObjectFlag); + VertexExpression->Desc = GeneratingParameterName; + } + + _AddMaterialExpression(Material, VertexExpression); + + return VertexExpression; +} + +void +FHoudiniMaterialTranslator::GetTextureAndExpression( + UMaterialExpression*& MatInputExpression, + const bool bLocateExpression, + UTexture2D*& OutTexture, + UMaterialExpressionTextureSampleParameter2D*& OutExpression) +{ + if (bLocateExpression) + OutExpression = + Cast(FHoudiniMaterialTranslator::MaterialLocateExpression( + MatInputExpression, UMaterialExpressionTextureSampleParameter2D::StaticClass())); + else + OutExpression = + Cast(MatInputExpression); + + if (IsValid(OutExpression)) + OutTexture = Cast(OutExpression->Texture); + else + { + OutTexture = nullptr; + + // The input expression is not a texture expression. Destroy it. + if (MatInputExpression) + { + MatInputExpression->ConditionalBeginDestroy(); + MatInputExpression = nullptr; + } + } +} + +UMaterialExpressionScalarParameter* +FHoudiniMaterialTranslator::CreateScalarExpression( + UMaterialExpression* ExistingExpression, + UMaterial* Material, + const EObjectFlags& ObjectFlag, + const FString& GeneratingParameterName) +{ + // If the expression already exists, use that. + UMaterialExpressionScalarParameter* ScalarExpression = + Cast(FHoudiniMaterialTranslator::MaterialLocateExpression( + ExistingExpression, UMaterialExpressionScalarParameter::StaticClass())); + + // Otherwise, create it. + if (!IsValid(ScalarExpression)) + { + ScalarExpression = NewObject( + Material, UMaterialExpressionScalarParameter::StaticClass(), NAME_None, ObjectFlag); + ScalarExpression->Desc = GeneratingParameterName; + } + + _AddMaterialExpression(Material, ScalarExpression); + + return ScalarExpression; +} + +UMaterialExpressionVectorParameter* +FHoudiniMaterialTranslator::CreateColorExpression( + UMaterialExpression* ExistingExpression, + UMaterial* Material, + const EObjectFlags& ObjectFlag) +{ + // If the expression already exists, use that. + UMaterialExpressionVectorParameter* ColorExpression = + Cast(FHoudiniMaterialTranslator::MaterialLocateExpression( + ExistingExpression, UMaterialExpressionVectorParameter::StaticClass())); + + // Otherwise, create it. + if (!IsValid(ColorExpression)) + { + ColorExpression = NewObject( + Material, UMaterialExpressionVectorParameter::StaticClass(), NAME_None, ObjectFlag); + ColorExpression->DefaultValue = FLinearColor::White; + } + + _AddMaterialExpression(Material, ColorExpression); + + return ColorExpression; +} + +bool +FHoudiniMaterialTranslator::SetColorExpression( + const HAPI_NodeId& NodeId, + const char* ParamName, + const char* ParamTag, + const char* ParamCPMConst, + const char* ParamCPMDefault, + const char* ParamCPMSwitch, + UMaterialExpressionVectorParameter* ColorExpression, + FString& GeneratingParameterName) +{ + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniMaterialTranslator::FindConstantParam( + NodeId, ParamName, ParamTag, ParamCPMConst, ParamCPMDefault, ParamCPMSwitch, ParmInfo, GeneratingParameterName); + + if (ParmId >= 0) + { + FLinearColor Color = FLinearColor::White; + if (FHoudiniApi::GetParmFloatValues( + FHoudiniEngine::Get().GetSession(), NodeId, (float*)&Color.R, + ParmInfo.floatValuesIndex, ParmInfo.size) == HAPI_RESULT_SUCCESS) + { + if (ParmInfo.size == 3) + Color.A = 1.0f; + + // Record generating parameter. + ColorExpression->Desc = GeneratingParameterName; + ColorExpression->ParameterName = *GeneratingParameterName; + ColorExpression->DefaultValue = Color; + + return true; + } + } + + return false; +} + +UMaterialExpressionMultiply* +FHoudiniMaterialTranslator::CreateMultiplyExpressions( + UMaterialExpression* MatInputExpression, + UMaterialExpression* ExpressionA, + UMaterialExpression* ExpressionB, + UMaterialExpression* ExpressionC, + UMaterial* Material, + int32& MaterialNodeY, + const EObjectFlags& ObjectFlag) +{ + // If a multiply expression is already attached to the material's input, use that one. Otherwise, create a new one. + UMaterialExpressionMultiply* Multiply = Cast(MatInputExpression); + if (!IsValid(Multiply)) + Multiply = NewObject( + Material, UMaterialExpressionMultiply::StaticClass(), NAME_None, ObjectFlag); + + _AddMaterialExpression(Material, Multiply); + + // See if primary multiplication has secondary multiplication as input A. + UMaterialExpressionMultiply* MultiplySecondary = nullptr; + if (Multiply->A.Expression) + MultiplySecondary = Cast(Multiply->A.Expression); + + if (ExpressionC) + { + // If third expression is provided, create the second multiplication expression. + if (!MultiplySecondary) + { + MultiplySecondary = NewObject( + Material, UMaterialExpressionMultiply::StaticClass(), NAME_None, ObjectFlag); + + _AddMaterialExpression(Material, MultiplySecondary); + } + } + else + { + // If third expression does not exist, destroy the second multiplication expression. + if (MultiplySecondary) + { + MultiplySecondary->A.Expression = nullptr; + MultiplySecondary->B.Expression = nullptr; + MultiplySecondary->ConditionalBeginDestroy(); + } + } + + Multiply->A.Expression = ExpressionA; + Multiply->B.Expression = ExpressionB; + + // Position the expressions. + float HorizontalPositionScale = 1.0f; + if (MultiplySecondary) + HorizontalPositionScale = 1.5f; + + FHoudiniMaterialTranslator::PositionExpression(ExpressionA, MaterialNodeY, HorizontalPositionScale); + FHoudiniMaterialTranslator::PositionExpression(ExpressionB, MaterialNodeY, HorizontalPositionScale); + + Multiply->MaterialExpressionEditorX = FHoudiniMaterialTranslator::MaterialExpressionNodeX; + Multiply->MaterialExpressionEditorY = + (ExpressionB->MaterialExpressionEditorY + ExpressionA->MaterialExpressionEditorY) / 2; + + // Hook up secondary multiplication expression. + if (MultiplySecondary) + { + MultiplySecondary->A.Expression = Multiply; + MultiplySecondary->B.Expression = ExpressionC; + + FHoudiniMaterialTranslator::PositionExpression(ExpressionC, MaterialNodeY, HorizontalPositionScale); + + MultiplySecondary->MaterialExpressionEditorX = FHoudiniMaterialTranslator::MaterialExpressionNodeX; + MultiplySecondary->MaterialExpressionEditorY = + Multiply->MaterialExpressionEditorY + FHoudiniMaterialTranslator::MaterialExpressionNodeStepY; + + return MultiplySecondary; + } + else + { + return Multiply; + } +} + +bool +FHoudiniMaterialTranslator::CreateScalarExpressionFromFloatParam( + HAPI_NodeId Node, + const char* ParamName, + const char* ParamTag, + const char* ParamCPMConst, + const char* ParamCPMDefault, + const char* ParamCPMSwitch, + UMaterialExpression*& ExistingExpression, + UMaterial* Material, + int32& MaterialNodeY, + const EObjectFlags& ObjectFlag) +{ + // Find parameter. + FString GeneratingParameterName = TEXT(""); + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniMaterialTranslator::FindConstantParam( + Node, ParamName, ParamTag, ParamCPMConst, ParamCPMDefault, ParamCPMSwitch, ParmInfo, GeneratingParameterName); + if (ParmId < 0) + return false; + + // Get parameter value. + float Value = 0.0f; + HAPI_Result Result = FHoudiniApi::GetParmFloatValues( + FHoudiniEngine::Get().GetSession(), Node, (float*)&Value, ParmInfo.floatValuesIndex, 1); + if (Result != HAPI_RESULT_SUCCESS) + return false; + + // Clamp retrieved value. + Value = FMath::Clamp(Value, 0.0f, 1.0f); + + // If there's already an input expression, check if it's a scalar expression. + UMaterialExpressionScalarParameter* Expression = + Cast(ExistingExpression); + + if (!Expression) + { + if (ExistingExpression) + { + // The input expression is not a scalar expression. Destroy it. + ExistingExpression->ConditionalBeginDestroy(); + ExistingExpression = nullptr; + } + + Expression = NewObject( + Material, UMaterialExpressionScalarParameter::StaticClass(), NAME_None, ObjectFlag); + } + + Expression->Desc = GeneratingParameterName; + Expression->ParameterName = *GeneratingParameterName; + + Expression->DefaultValue = Value; + Expression->SliderMin = 0.0f; + Expression->SliderMax = 1.0f; + + FHoudiniMaterialTranslator::PositionExpression(Expression, MaterialNodeY, 0.0f); + + _AddMaterialExpression(Material, Expression); + ExistingExpression = Expression; + + return true; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniMaterialTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "HoudiniGeoPartObject.h" +#include "CoreMinimal.h" +#include "UObject/ObjectMacros.h" +#include "Engine/TextureDefines.h" +#include "Engine/EngineTypes.h" + +#include + +#include "HoudiniMaterialTranslator.generated.h" + +class UMaterial; +class UMaterialInterface; +class UMaterialExpression; +class UMaterialInstanceConstant; +class UTexture2D; +class UTexture; +class UPackage; +class UMaterialExpression; +class UMaterialExpressionMultiply; +class UMaterialExpressionVectorParameter; +class UMaterialExpressionScalarParameter; +class UMaterialExpressionVertexColor; +class UMaterialExpressionTextureSampleParameter2D; + +struct FHoudiniPackageParams; +struct FCreateTexture2DParameters; +struct FHoudiniGenericAttribute; +struct FHoudiniMaterialIdentifier; + +// Forward declared enums do not work with 4.24 builds on Linux with the Clang 8.0.1 toolchain: ISO C++ forbids forward references to 'enum' types +// enum TextureGroup; + +// Enum of material parameter type for Houdini material parameter attributes. +UENUM() +enum class EHoudiniUnrealMaterialParameterType : uint8 +{ + Invalid = 0, + // The parameter is a standard parameter of the material instance (such as BlendMode, DiffuseBoost etc) + StandardParameter, + // The following are the types for custom parameters + // A scalar material parameter + Scalar, + // A static switch (bool) parameter + StaticSwitch, + // A vector parameter (represented with a color) + Vector, + // A texture parameter (represented with a string) + Texture, +}; + +// Material parameter data types from Houdini attributes +UENUM() +enum class EHoudiniUnrealMaterialParameterDataType : uint8 +{ + Invalid = 0, + // One byte, used for enums and bools + Byte, + // Floating point value (double is cast down to float) + Float, + // String + String, + // Vector represented as a RGB or RGBA linear color + Vector +}; + +// Struct for storing material parameter type and value information from Houdini attributes after processing via +// FHoudiniMaterialTranslator::GetAndValidateMaterialInstanceParameterValue() +USTRUCT() +struct FHoudiniMaterialParameterValue +{ + GENERATED_BODY() + + FHoudiniMaterialParameterValue(); + + // The parameter type (standard parameter, scalar, static switch, texture, vector) + UPROPERTY() + EHoudiniUnrealMaterialParameterType ParamType; + + // The data type (byte, float, string, vector) + UPROPERTY() + EHoudiniUnrealMaterialParameterDataType DataType; + + // The value used if DataType is Byte + UPROPERTY() + uint8 ByteValue; + + // The value used if DataType is Float + UPROPERTY() + float FloatValue; + + // The value used if DataType is String + UPROPERTY() + FString StringValue; + + // The value used if DataType is Vector + UPROPERTY() + FLinearColor VectorValue; + + // Convenience functions for setting values + void SetValue(const uint8 InValue); + void SetValue(const float InValue); + void SetValue(const FString& InValue); + void SetValue(const FLinearColor& InValue); + // Reset all *Value properties, other than the active DataType, to their default values. + void CleanValue(); +}; + +// Material info collected for each unreal_material / unreal_material_instance attribute value +USTRUCT() +struct HOUDINIENGINE_API FHoudiniMaterialInfo +{ + GENERATED_BODY() + + // Make a string that represents the map of MaterialInstanceParameters (used for hashing) + FString MakeMaterialInstanceParametersSlug() const; + + // Make an identifier that uniquely identifies this material struct: includes the MaterialObjectPath, + // bMakeMaterialInstance and the slug returned by MakeMaterialInstanceParametersSlug + FHoudiniMaterialIdentifier MakeIdentifier() const; + + // The object path the UMaterial + UPROPERTY() + FString MaterialObjectPath; + + // Whether a material instance should be created from MaterialObjectPath + UPROPERTY() + bool bMakeMaterialInstance = false; + + // The material slot index + UPROPERTY() + int32 MaterialIndex = INDEX_NONE; + + // If bMakeMaterialInstance is true (a material instance must be made), then this is the material parameters to apply + // to the instance. + UPROPERTY() + TMap MaterialInstanceParameters; +}; + + +struct HOUDINIENGINE_API FHoudiniMaterialTranslator +{ +public: + + // Helper function to handle difference with material expressions collection in UE5.1 + static void _AddMaterialExpression( + UMaterial* InMaterial, UMaterialExpression* InMatExp); + + // + static bool CreateHoudiniMaterials( + const HAPI_NodeId& InNodeId, + const FHoudiniPackageParams& InPackageParams, + const TArray& InUniqueMaterialIds, + const TArray& InUniqueMaterialInfos, + const TMap>& InMaterials, + const TMap>& InAllOutputMaterials, + TMap>& OutMaterials, + TArray & OutMaterialArray, + TArray& OutPackages, + const bool& bForceRecookAll, + bool bInTreatExistingMaterialsAsUpToDate=false, + bool bAddDefaultMaterial=true); + + // + static bool CreateMaterialInstances( + const FHoudiniGeoPartObject& InHGPO, + const FHoudiniPackageParams& InPackageParams, + const TMap& UniqueMaterialInstanceOverrides, + const TArray& InPackages, + const TMap>& InMaterials, + TMap>& OutMaterials, + const bool& bForceRecookAll); + + + // Helper for getting material parameters from Houdini attributes (detail + InAttributeOwner (prim or point)). + static bool GetMaterialParameterAttributes( + int32 InGeoId, + int32 InPartId, + HAPI_AttributeOwner InAttributeOwner, + TArray& OutAllMatParams, + const int32 InAttributeIndex=-1); + + // Helper for getting material parameters for a material instance from attributes via HAPI + static bool GetMaterialParameters( + FHoudiniMaterialInfo& MaterialInfo, + const TArray& InAllMatParams, + int32 InAttributeIndex); + + // Helper for getting material parameters for material instances from attributes via HAPI + static bool GetMaterialParameters( + TArray& MaterialsByAttributeIndex, + int32 InGeoId, + int32 InPartId, + HAPI_AttributeOwner InAttributeOwner); + + // Helper for CreateMaterialInstances so you don't have to sort unique face materials overrides + static bool SortUniqueFaceMaterialOverridesAndCreateMaterialInstances( + const TArray& Materials, + const FHoudiniGeoPartObject& InHGPO, + const FHoudiniPackageParams& InPackageParams, + const TArray& InPackages, + const TMap>& InMaterials, + TMap>& OutMaterials, + const bool& bForceRecookAll); + + // Helper to check if MaterialParameter is supported by instances of MaterialInterface and to resolve the value to + // apply from MaterialParameter. This is tightly coupled with UpdateMaterialInstanceParameter(): changes to the one + // function likely require changes to the other! + static bool GetAndValidateMaterialInstanceParameterValue( + const FName& InMaterialParameterName, + const FHoudiniGenericAttribute& MaterialParameterAttribute, + const int32 InAttributeIndex, + UMaterialInterface* MaterialInterface, + FHoudiniMaterialParameterValue& OutMaterialParameterValue); + + // Update a material instance parameter. This is tightly coupled with GetAndValidateMaterialInstanceParameterValue(): + // changes to the one function likely require changes to the other! + static bool UpdateMaterialInstanceParameter( + const FName& InMaterialParameterName, + const FHoudiniMaterialParameterValue& InMaterialParameterValue, + UMaterialInstanceConstant* MaterialInstance, + const TArray& InPackages); + + static UTexture* FindGeneratedTexture( + const FString& TextureString, + const TArray& InPackages); + + // + static UPackage* CreatePackageForMaterial( + const HAPI_NodeId& InMaterialNodeId, + const FString& InMaterialName, + const FHoudiniPackageParams& InPackageParams, + FString& OutMaterialName); + + static void GetTextureAndExpression( + UMaterialExpression*& MatInputExpression, + const bool bLocateExpression, + UTexture2D*& OutTexture, + UMaterialExpressionTextureSampleParameter2D*& OutExpression); + + // Create a scalar expression in the material graph, reusing the existing expression if possible. + static UMaterialExpressionScalarParameter* CreateScalarExpression( + UMaterialExpression* ExistingExpression, + UMaterial* Material, + const EObjectFlags& ObjectFlag, + const FString& GeneratingParameterName); + + // Create a vertex color expression in the material graph, reusing the existing expression if possible. + static UMaterialExpressionVertexColor* CreateVertexColorExpression( + UMaterialExpression* ExistingExpression, + UMaterial* Material, + const EObjectFlags& ObjectFlag, + const FString& GeneratingParameterName); + + // Create a color expression in the material graph, reusing the existing expression if possible. + static UMaterialExpressionVectorParameter* CreateColorExpression( + UMaterialExpression* ExistingExpression, + UMaterial* Material, + const EObjectFlags& ObjectFlag); + + // Set a color expression's color to that of a HAPI color parameter. + // Records the GeneratingParameterName if the HAPI parameter is found. + // Returns true if successfully set, false otherwise. + static bool SetColorExpression( + const HAPI_NodeId& NodeId, + const char* ParamName, + const char* ParamTag, + const char* ParamCPMConst, + const char* ParamCPMDefault, + const char* ParamCPMSwitch, + UMaterialExpressionVectorParameter* ColorExpression, + FString& GeneratingParameterName); + + // Connect expressions A, B, and optionally C with multiply expressions. + // If C is not provided, creates one multiply (A*B). Otherwise, creates two ((A*B)*C). + // Returns the last multiply expression in the chain. + static UMaterialExpressionMultiply* CreateMultiplyExpressions( + UMaterialExpression* MatInputExpression, + UMaterialExpression* ExpressionA, + UMaterialExpression* ExpressionB, + UMaterialExpression* ExpressionC, + UMaterial* Material, + int32& MaterialNodeY, + const EObjectFlags& ObjectFlag); + + // Creates a scalar parameter expression from a HAPI float parameter. + // Returns true if the expression was successfully created. False otherwise. + static bool CreateScalarExpressionFromFloatParam( + HAPI_NodeId Node, + const char* ParamName, + const char* ParamTag, + const char* ParamCPMConst, + const char* ParamCPMDefault, + const char* ParamCPMSwitch, + UMaterialExpression*& ExistingExpression, + UMaterial* Material, + int32& MaterialNodeY, + const EObjectFlags& ObjectFlag); + + // Positions the expressions in the material graph. + static void PositionExpression( + UMaterialExpression* Expression, + int32& MaterialNodeY, + const float HorizontalPositionScale); + + // Determines if world space normals are required for the material created from this node. + static bool RequiresWorldSpaceNormals(HAPI_NodeId HapiMaterial); + + // Returns a unique name for a given material, its relative path (to the asset) + static bool GetMaterialRelativePath( + const HAPI_NodeId& InAssetId, const HAPI_MaterialInfo& InMaterialNodeInfo, FString& OutRelativePath); + static bool GetMaterialRelativePath( + const HAPI_NodeId& InAssetId, const HAPI_NodeId& InMaterialNodeId, FString& OutRelativePath); + + // Finds a HAPI parameter (which represents a constant value for a plane) based on its name/tag. + // Returns its ParmId, ParmInfo, and sets the GeneratingParameterName. + static HAPI_ParmId FindConstantParam( + const HAPI_NodeId& NodeId, + const char* Name, + const char* Tag, + const char* CPMConst, + const char* CPMDefault, + const char* CPMSwitch, + HAPI_ParmInfo& Info, + FString& GeneratingParameterName); + + // Finds a HAPI texture parameter based on its name/tag. + // Returns its ParmId, ParmInfo, and sets the GeneratingParameterName. + static HAPI_ParmId FindTextureParam( + const HAPI_NodeId& NodeId, + const char* Name, + const char* NameEnabled, + const char* Tag, + const char* TagEnabled, + const char* CPMName, + const char* CPMSwitch, + HAPI_ParmInfo& TextureInfo, + FString& GeneratingParameterName); + + // Returns true if a texture parameter was found + // Ensures that the texture is not disabled via the "UseTexture" Parm name/tag + static bool FindTextureParamByNameOrTag( + const HAPI_NodeId& InNodeId, + const std::string& InTextureParmName, + const std::string& InUseTextureParmName, + const bool& bFindByTag, + const bool& bIsCPM, + HAPI_ParmId& OutParmId, + HAPI_ParmInfo& OutParmInfo); + +protected: + + // Helper function to locate first Material expression of given class within given expression subgraph. + static UMaterialExpression* MaterialLocateExpression(UMaterialExpression* Expression, UClass* ExpressionClass); + + // Assigns a texture to an expression in the Unreal material graph. + // Returns true if the expression is successfully created, false otherwise. + static bool CreateTextureExpression( + UTexture2D* Texture, + UMaterialExpressionTextureSampleParameter2D*& TextureExpression, + UMaterialExpression*& MatInputExpression, + const bool SetMatInputExpression, + UMaterial* Material, + const EObjectFlags ObjectFlag, + const FString& GeneratingParameterName, + const EMaterialSamplerType SamplerType); + + // Create various material components. + static bool CreateMaterialComponentDiffuse( + const HAPI_NodeId& InAssetId, + const FString& InMaterialName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY); + + static bool CreateMaterialComponentNormal( + const HAPI_NodeId& InAssetId, + const FString& InMaterialName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY); + + static bool CreateMaterialComponentSpecular( + const HAPI_NodeId& InAssetId, + const FString& InMaterialName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY); + + static bool CreateMaterialComponentRoughness( + const HAPI_NodeId& InAssetId, + const FString& InMaterialName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY); + + static bool CreateMaterialComponentMetallic( + const HAPI_NodeId& InAssetId, + const FString& InMaterialName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY); + + static bool CreateMaterialComponentEmissive( + const HAPI_NodeId& InAssetId, + const FString& InMaterialName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY); + + static bool CreateMaterialComponentOpacity( + const HAPI_NodeId& InAssetId, + const FString& InMaterialName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY); + + static bool CreateMaterialComponentOpacityMask( + const HAPI_NodeId& InAssetId, + const FString& InMaterialName, + const HAPI_MaterialInfo& InMaterialInfo, + const FHoudiniPackageParams& InPackageParams, + UMaterial* Material, + TArray& OutPackages, + int32& MaterialNodeY); + +public: + + // Material node construction offsets. + static const int32 MaterialExpressionNodeX; + static const int32 MaterialExpressionNodeY; + static const int32 MaterialExpressionNodeStepX; + static const int32 MaterialExpressionNodeStepY; +}; +``` + +`Source/HoudiniEngine/Private/HoudiniMeshTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniMeshTranslator.h" + +#include "HoudiniApi.h" +#include "HoudiniCookable.h" +#include "HoudiniEngine.h" +#include "HoudiniOutput.h" +#include "HoudiniGenericAttribute.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniGenericAttribute.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniMaterialTranslator.h" +#include "HoudiniAssetActor.h" +#include "HoudiniInstanceTranslator.h" +#include "HoudiniStaticMesh.h" +#include "HoudiniStaticMeshComponent.h" +#include "HoudiniSkeletalMeshTranslator.h" + +#include "Engine/StaticMeshSocket.h" + +#include "PhysicsEngine/BodySetup.h" +#include "Engine/StaticMesh.h" +#include "PackageTools.h" +#include "Materials/MaterialInterface.h" +#include "Materials/Material.h" +#include "MeshDescription.h" +#include "StaticMeshAttributes.h" +#include "MeshDescriptionOperations.h" + +#include "BSPOps.h" +#include "Model.h" +#include "Engine/Polys.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Interfaces/ITargetPlatform.h" +#include "Interfaces/ITargetPlatformManagerModule.h" +#include "GeometryToolsEngine.h" +#include "Engine/SkeletalMesh.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniEngineString.h" + +#include "Components/SkeletalMeshComponent.h" + +#include "ProfilingDebugging/CpuProfilerTrace.h" + +#include "EditorSupportDelegates.h" +#include "HoudiniGeometryCollectionTranslator.h" +#include "PhysicalMaterials/PhysicalMaterial.h" + +#if WITH_EDITOR + #include "ConvexDecompTool.h" + #include "Editor/UnrealEd/Private/GeomFitUtils.h" + #include "LevelEditorViewport.h" + #include "FileHelpers.h" +#endif +#include + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +bool +FHoudiniMeshTranslator::CreateAllMeshesAndComponentsFromHoudiniOutput( + UHoudiniOutput* InOutput, + const FHoudiniPackageParams& InPackageParams, + EHoudiniStaticMeshMethod InStaticMeshMethod, + bool bSplitMeshSupport, + const FHoudiniStaticMeshGenerationProperties& InSMGenerationProperties, + const FMeshBuildSettings& InMeshBuildSettings, + const TMap>& InAllOutputMaterials, + UObject* InOuterComponent, + bool bInTreatExistingMaterialsAsUpToDate, + bool bInDestroyProxies) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateAllMeshesAndComponentsFromHoudiniOutput); + + if (!IsValid(InOutput)) + return false; + + if (!IsValid(InPackageParams.OuterPackage)) + return false; + + if (!IsValid(InOuterComponent)) + return false; + + TMap NewOutputObjects; + TMap OldOutputObjects = InOutput->GetOutputObjects(); + TMap>& AssignementMaterials = InOutput->GetAssignementMaterials(); + TMap>& ReplacementMaterials = InOutput->GetReplacementMaterials(); + + bool InForceRebuild = false; + if (InOutput->HasAnyCurrentProxy() && InStaticMeshMethod != EHoudiniStaticMeshMethod::UHoudiniStaticMesh) + { + // Make sure we're not preventing refinement + InForceRebuild = true; + } + + // Iterate on all of the output's HGPO, creating meshes as we go + for (const FHoudiniGeoPartObject& CurHGPO : InOutput->HoudiniGeoPartObjects) + { + // Not a mesh, skip + if (CurHGPO.Type != EHoudiniPartType::Mesh) + continue; + + // See if we have some uproperty attributes to update on + // the outer component (in most case, the HAC) + TArray PropertyAttributes; + if (FHoudiniEngineUtils::GetGenericPropertiesAttributes( + CurHGPO.GeoId, CurHGPO.PartId, + true, 0, 0, 0, + PropertyAttributes)) + { + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes( + InOuterComponent, PropertyAttributes); + + UHoudiniAssetComponent* HAC = Cast(InOuterComponent); + if (HAC) + { + // Also try on the HAC's cookable + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes( + HAC->GetCookable(), PropertyAttributes); + } + } + + CreateStaticMeshFromHoudiniGeoPartObject( + CurHGPO, + InPackageParams, + OldOutputObjects, + NewOutputObjects, + AssignementMaterials, + ReplacementMaterials, + InAllOutputMaterials, + InOuterComponent, + InForceRebuild, + InStaticMeshMethod, + bSplitMeshSupport, + InSMGenerationProperties, + InMeshBuildSettings, + bInTreatExistingMaterialsAsUpToDate); + } + + return FHoudiniMeshTranslator::CreateOrUpdateAllComponents( + InOutput, + InOuterComponent, + NewOutputObjects, + bInDestroyProxies); +} + +bool +FHoudiniMeshTranslator::CreateOrUpdateAllComponents( + UHoudiniOutput* InOutput, + UObject* InOuterComponent, + TMap& InNewOutputObjects, + bool bInDestroyProxies, + bool bInApplyGenericProperties) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateOrUpdateAllComponents); + + if (!IsValid(InOutput)) + return false; + + TMap OldOutputObjects = InOutput->GetOutputObjects(); + + // Remove Static Meshes and their components from the old map + // to avoid their deletion if new proxies were created for them + for (auto& NewOutputObj : InNewOutputObjects) + { + FHoudiniOutputObjectIdentifier OutputIdentifier = NewOutputObj.Key; + + // See if we already had that pair in the old map of static mesh + FHoudiniOutputObject* FoundOldOutputObj = OldOutputObjects.Find(NewOutputObj.Key); + if (!FoundOldOutputObj) + continue; + + UObject* NewStaticMesh = NewOutputObj.Value.OutputObject; + UObject* NewProxyMesh = NewOutputObj.Value.ProxyObject; + + UObject* OldStaticMesh = FoundOldOutputObj->OutputObject; + if (IsValid(OldStaticMesh)) + { + // If a proxy was created for an existing static mesh, keep the existing static + // mesh (will be hidden) + if (NewProxyMesh && NewOutputObj.Value.bProxyIsCurrent) + { + // Remove it from the old map to avoid its destruction + OldOutputObjects.Remove(OutputIdentifier); + } + else if (NewStaticMesh && NewStaticMesh == OldStaticMesh) + { + // Remove it from the old map to avoid its destruction + OldOutputObjects.Remove(OutputIdentifier); + } + } + + UObject* OldProxyMesh = FoundOldOutputObj->ProxyObject; + if (IsValid(OldProxyMesh)) + { + // If a new static mesh was created for a proxy, keep the proxy (will be hidden) + // ... unless we want to explicitly destroy proxies + if (NewStaticMesh && !bInDestroyProxies) + { + // Remove it from the old map to avoid its destruction + OldOutputObjects.Remove(OutputIdentifier); + } + else if (NewProxyMesh && (NewProxyMesh == OldProxyMesh)) + { + if (NewOutputObj.Value.ProxyComponent == FoundOldOutputObj->ProxyComponent) + { + // Remove it from the old map to avoid its destruction + OldOutputObjects.Remove(OutputIdentifier); + } + else + { + // The new proxy is the same as the old one + // Only destroy the proxy's component + FoundOldOutputObj->ProxyObject = nullptr; + } + } + } + } + + // The old map now only contains unused/stale Meshes/Components, delete them + for (auto& OldPair : OldOutputObjects) + { + // Get the old Identifier / StaticMesh + FHoudiniOutputObjectIdentifier& OutputIdentifier = OldPair.Key; + FHoudiniOutputObject& OldOutputObject = OldPair.Value; + + // Remove the old component from the map + for(auto Component : OldOutputObject.OutputComponents) + RemoveAndDestroyComponent(Component); + OldOutputObject.OutputComponents.Empty(); + + // Remove the old proxy component from the map + RemoveAndDestroyComponent(OldOutputObject.ProxyComponent); + OldOutputObject.ProxyComponent = nullptr; + + if (IsValid(OldOutputObject.OutputObject)) + { + OldOutputObject.OutputObject->MarkAsGarbage(); + } + + if (IsValid(OldOutputObject.ProxyObject)) + { + OldOutputObject.ProxyObject->MarkAsGarbage(); + } + } + OldOutputObjects.Empty(); + + /* + // Remove any stale components, these are components with OutputIdentifiers that are not + // in NewOutputObjects. This seems to happen mostly with the first or second cook after a + // "Rebuild Asset" + if (OutputComponents.Num() > 0 || OutputProxyComponents.Num() > 0) + { + TArray> StaleComponents; + const uint32 MaxNumStale = FMath::Max(OutputComponents.Num(), OutputProxyComponents.Num()); + StaleComponents.Reserve(MaxNumStale); + for (auto& ComponentPair : OutputComponents) + { + if (!NewOutputObjects.Contains(ComponentPair.Key) && !OldOutputObjectsReplacedByProxy.Contains(ComponentPair.Key)) + { + StaleComponents.Add(ComponentPair); + } + } + for (auto& ComponentPair : StaleComponents) + { + RemoveAndDestroyComponent(ComponentPair.Key, OutputComponents); + } + StaleComponents.Empty(MaxNumStale); + + for (auto& ComponentPair : OutputProxyComponents) + { + if (!NewOutputProxyObjects.Contains(ComponentPair.Key) && !OldOutputProxyObjectsReplacedByStaticMesh.Contains(ComponentPair.Key)) + { + StaleComponents.Add(ComponentPair); + } + } + for (auto& ComponentPair : StaleComponents) + { + RemoveAndDestroyComponent(ComponentPair.Key, OutputProxyComponents); + } + StaleComponents.Empty(); + } + */ + + // Exit early if we have no component to update + if (!IsValid(InOuterComponent)) + { + // Assign the new output objects to the output + InOutput->SetOutputObjects(InNewOutputObjects); + + return true; + } + + // Now create/update the new static mesh components + for (auto& NewPair : InNewOutputObjects) + { + // Get the old Identifier / StaticMesh + const FHoudiniOutputObjectIdentifier& OutputIdentifier = NewPair.Key; + FHoudiniOutputObject& OutputObject = NewPair.Value; + + if (OutputObject.bIsImplicit) + { + // This output is implicit and shouldn't have a representative component/proxy in the scene + // Remove the old component from the map + for(auto Component : OutputObject.OutputComponents) + { + RemoveAndDestroyComponent(Component); + } + OutputObject.OutputComponents.Empty(); + + // Remove the old proxy component from the map + RemoveAndDestroyComponent(OutputObject.ProxyComponent); + OutputObject.ProxyComponent = nullptr; + + continue; // Skip any proxy / component creation below + } + + // Check if we should create a Proxy/SMC + if (OutputObject.bProxyIsCurrent && InOutput->bCreateSceneComponents) + { + UObject *Mesh = OutputObject.ProxyObject; + if (!IsValid(Mesh) || !Mesh->IsA()) + { + HOUDINI_LOG_ERROR(TEXT("Proxy Mesh is invalid (wrong type or pending kill)...")); + continue; + } + + // Create or update a new proxy component + TSubclassOf ComponentType = UHoudiniStaticMeshComponent::StaticClass(); + const FHoudiniGeoPartObject *FoundHGPO = nullptr; + bool bCreated = false; + UMeshComponent *MeshComponent = CreateOrUpdateMeshComponent( + InOutput, + InOuterComponent, + OutputIdentifier, + ComponentType, + OutputObject, + FoundHGPO, + bCreated); + if (MeshComponent) + { + UHoudiniStaticMeshComponent *HSMC = Cast(MeshComponent); + UpdateMeshComponent( + MeshComponent, + Mesh, + OutputIdentifier, + OutputObject, + FoundHGPO, + InOutput->HoudiniCreatedSocketActors, + InOutput->HoudiniAttachedSocketActors, + bInApplyGenericProperties); + + if (!bCreated) + { + // For proxy meshes: notify that the mesh has been updated + HSMC->NotifyMeshUpdated(); + HSMC->SetHoudiniIconVisible(true); + } + } + + // Now, ensure that meshes replaced by proxies are still kept but hidden + for(auto Component : OutputObject.OutputComponents) + { + USceneComponent* SceneComponent = Cast(Component); + if (SceneComponent) + { + SceneComponent->SetVisibility(false); + SceneComponent->SetHiddenInGame(true); + } + } + + // If the proxy mesh we just created is templated, hide it in game + bool bIsTemplated = FoundHGPO ? FoundHGPO->bIsTemplated : false; + if (bIsTemplated) + { + MeshComponent->SetHiddenInGame(true); + } + } + else + { + // Create a new SMC if needed + UObject* Mesh = OutputObject.OutputObject; + if (!IsValid(Mesh)) + { + HOUDINI_LOG_ERROR(TEXT("Mesh is invalid (wrong type or pending kill)...")); + continue; + } + + const FHoudiniGeoPartObject* FoundHGPO = nullptr; + UMeshComponent* MeshComponent = nullptr; + if (Mesh->IsA() && InOutput->bCreateSceneComponents) + { + TSubclassOf ComponentType = UStaticMeshComponent::StaticClass(); + bool bCreated = false; + MeshComponent = CreateOrUpdateMeshComponent(InOutput, InOuterComponent, OutputIdentifier, ComponentType, OutputObject, FoundHGPO, bCreated); + if (MeshComponent) + { + UActorComponent* ProxyComponent = Cast(OutputObject.ProxyComponent); + if (IsValid(ProxyComponent)) + { + // If this static mesh component has a proxy component, it was likely refined from the proxy component. + // This means that if the user enabled KeepTags, tags would have accumulated on the Proxy component so + // we need to copy the proxy mesh component tags over to the static mesh. + MeshComponent->ComponentTags = ProxyComponent->ComponentTags; + } + + UpdateMeshComponent( + MeshComponent, + Mesh, + OutputIdentifier, + OutputObject, + FoundHGPO, + InOutput->HoudiniCreatedSocketActors, + InOutput->HoudiniAttachedSocketActors, + bInApplyGenericProperties); + + // UE5: Make sure we update/recreate the Component's render state + // after the update or the mesh component will not be rendered! + if (MeshComponent->IsRenderStateCreated()) + { + // Need to send this to render thread at some point + MeshComponent->MarkRenderStateDirty(); + } + else if (MeshComponent->ShouldCreateRenderState()) + { + // If we didn't have a valid StaticMesh assigned before + // our render state might not have been created so + // do it now. + MeshComponent->RecreateRenderState_Concurrent(); + } + } + } + else if (Mesh->IsA() && InOutput->bCreateSceneComponents) + { + + TSubclassOf SKComponentType = USkeletalMeshComponent::StaticClass(); + bool bSKCreated = false; + MeshComponent = CreateOrUpdateMeshComponent(InOutput, InOuterComponent, OutputIdentifier, SKComponentType, OutputObject, FoundHGPO, bSKCreated); + if (MeshComponent) + { + UpdateMeshComponent( + MeshComponent, + Mesh, + OutputIdentifier, + OutputObject, + FoundHGPO, + InOutput->HoudiniCreatedSocketActors, + InOutput->HoudiniAttachedSocketActors, + bInApplyGenericProperties); + + USkeletalMeshComponent* SKMC = Cast(MeshComponent); + if (IsValid(SKMC)) + { + USkeletalMesh* SKMesh = Cast(Mesh); + if (IsValid(SKMesh)) + { + SKMC->SetSkeletalMesh(SKMesh); + } + + // TODO: we're currently unable to retrieve point position on the SK mesh/pose? + /* + // Skeletal Mesh need to get their transform set separtely from the shape instancer's point transform + //if (FoundHGPO != nullptr) + for (auto& CurHGPO : InOutput->HoudiniGeoPartObjects) + { + // Retrieve Position + HAPI_AttributeInfo PointInfo; + FHoudiniApi::AttributeInfo_Init(&PointInfo); + + HAPI_Result PointInfoResult = FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + CurHGPO.GeoId, CurHGPO.PartId, + HAPI_UNREAL_ATTRIB_POSITION, HAPI_AttributeOwner::HAPI_ATTROWNER_POINT, &PointInfo); + + TArray PositionData; + PositionData.SetNum(PointInfo.count); //dont need * PositionInfo.tupleSize, its already a vector container + FHoudiniApi::GetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + CurHGPO.GeoId, CurHGPO.PartId, HAPI_UNREAL_ATTRIB_POSITION, &PointInfo, -1, (float*)&PositionData[0], 0, PointInfo.count); + + if (PositionData.Num() > 0) + { + SKMC->SetRelativeLocation(FVector3d(FHoudiniEngineUtils::ConvertHoudiniPositionToUnrealVector3f(PositionData[0]))); + } + + } + else + { + TArray Transforms; + for (auto& CurHGPO : InOutput->HoudiniGeoPartObjects) + { + if (FHoudiniInstanceTranslator::HapiGetInstanceTransforms(CurHGPO, Transforms)) + { + SKMC->SetRelativeTransform(Transforms[0]); + } + } + } + */ + } + } + } + + // Now, ensure that proxies replaced by meshes are still kept but hidden + UHoudiniStaticMeshComponent* HSMC = Cast(OutputObject.ProxyComponent); + if (HSMC) + { + HSMC->SetVisibility(false); + HSMC->SetHiddenInGame(true); + HSMC->SetHoudiniIconVisible(false); + } + + // If the mesh we just created is templated, hide it in game + bool bIsTemplated = FoundHGPO ? FoundHGPO->bIsTemplated : false; + if (IsValid(MeshComponent) && bIsTemplated) + { + MeshComponent->SetHiddenInGame(true); + } + } + } + + // Assign the new output objects to the output + InOutput->SetOutputObjects(InNewOutputObjects); + + return true; +} + +void +FHoudiniMeshTranslator::UpdateMeshComponent( + UMeshComponent *InMeshComponent, + UObject* InMesh, + const FHoudiniOutputObjectIdentifier &InOutputIdentifier, + const FHoudiniOutputObject& OutputObject, + const FHoudiniGeoPartObject *InHGPO, + TArray> &HoudiniCreatedSocketActors, + TArray> &HoudiniAttachedSocketActors, + bool bInApplyGenericProperties) +{ + UStaticMeshComponent* const SMC = Cast(InMeshComponent); + UHoudiniStaticMeshComponent* const HSMC = Cast(InMeshComponent); + if (IsValid(SMC)) + { + UpdateMeshOnStaticMeshComponent(SMC, InMesh); + } + else if (IsValid(HSMC)) + { + UpdateMeshOnHoudiniStaticMeshComponent(HSMC, InMesh); + } + + // Update collision/visibility + EHoudiniSplitType SplitType = GetSplitTypeFromSplitName(InOutputIdentifier.SplitIdentifier); + if (SplitType == EHoudiniSplitType::InvisibleComplexCollider || OutputObject.bIsInvisibleCollisionMesh) + { + // Invisible complex collider should not be seen + InMeshComponent->SetVisibility(false); + InMeshComponent->SetHiddenInGame(true); + InMeshComponent->SetCollisionProfileName(FName(TEXT("InvisibleWall"))); + InMeshComponent->SetCastShadow(false); + } + else + { + // Update visiblity + bool bVisible = InHGPO ? InHGPO->bIsVisible : true; + InMeshComponent->SetVisibility(bVisible); + InMeshComponent->SetHiddenInGame(!bVisible); + + FPropertyChangedEvent Evt(FindFieldChecked(InMeshComponent->GetClass(), "bVisible")); + InMeshComponent->PostEditChangeProperty(Evt); + } + + // TODO: + // Update navmesh? + + // Transform the component by transformation provided by HAPI. + InMeshComponent->SetRelativeTransform(InHGPO ? InHGPO->TransformMatrix : FTransform::Identity); + + // If the static mesh had sockets, we can assign the desired actor to them now + UStaticMeshComponent * StaticMeshComponent = Cast(InMeshComponent); + UStaticMesh * StaticMesh = nullptr; + if (IsValid(StaticMeshComponent)) + StaticMesh = StaticMeshComponent->GetStaticMesh(); + + if (StaticMesh && IsValid(StaticMesh)) + { + int32 NumberOfSockets = StaticMesh == nullptr ? 0 : StaticMesh->Sockets.Num(); + for (int32 nSocket = 0; nSocket < NumberOfSockets; nSocket++) + { + UStaticMeshSocket* MeshSocket = StaticMesh->Sockets[nSocket]; + if (IsValid(MeshSocket) && (MeshSocket->Tag.IsEmpty())) + continue; + + AddActorsToMeshSocket(StaticMesh->Sockets[nSocket], StaticMeshComponent, HoudiniCreatedSocketActors, HoudiniAttachedSocketActors); + } + + // Iterate all remaining created socket actors, destroy the ones that are not assigned to socket after re-cook + { + for (int32 Idx = HoudiniCreatedSocketActors.Num() - 1; Idx >= 0; --Idx) + { + AActor * CurActor = HoudiniCreatedSocketActors[Idx]; + + if (!IsValid(CurActor)) + { + HoudiniCreatedSocketActors.RemoveAt(Idx); + continue; + } + + bool bFoundSocket = false; + for (auto & CurSocket : StaticMesh->Sockets) + { + if (CurSocket->SocketName == CurActor->GetAttachParentSocketName()) + { + bFoundSocket = true; + break; + } + } + // cur actor's attaching socket is found, skip + if (bFoundSocket) + continue; + + // Destroy the previous created socket actor if not found + HoudiniCreatedSocketActors.RemoveAt(Idx); + CurActor->Destroy(); + } + } + + // Detach the in level actors which is not attached to any socket now + { + for (int32 Idx = HoudiniAttachedSocketActors.Num() - 1; Idx >= 0; --Idx) + { + AActor* CurActor = HoudiniAttachedSocketActors[Idx]; + if (!IsValid(CurActor)) + { + HoudiniAttachedSocketActors.RemoveAt(Idx); + continue; + } + + bool bFoundSocket = false; + for (auto & CurSocket : StaticMesh->Sockets) + { + if (CurSocket->SocketName == CurActor->GetAttachParentSocketName()) + { + bFoundSocket = true; + break; + } + } + + if (bFoundSocket) + continue; + + // If the attached socket name is not found in current socket, detach it and remove from the array + CurActor->DetachFromActor(FDetachmentTransformRules::KeepRelativeTransform); + HoudiniAttachedSocketActors.RemoveAt(Idx); + } + } + + } + + if (bInApplyGenericProperties) + { + // Clear the component tags, if permitted by HGPOs + FHoudiniEngineUtils::KeepOrClearComponentTags(InMeshComponent, InHGPO); + + // Update the property attributes on the component + TArray PropertyAttributes; + if (FHoudiniEngineUtils::GetGenericPropertiesAttributes( + InOutputIdentifier.GeoId, InOutputIdentifier.PartId, + true, + InOutputIdentifier.PrimitiveIndex, + INDEX_NONE, + InOutputIdentifier.PointIndex, + PropertyAttributes)) + { + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes(InMeshComponent, PropertyAttributes); + } + } + + // Custom Primitive Data + { + TArray CustomPrimitiveData; + FHoudiniMeshTranslator::GetCustomPrimitiveData( + InOutputIdentifier.GeoId, InOutputIdentifier.PartId, InOutputIdentifier.PrimitiveIndex, CustomPrimitiveData); + + FHoudiniMeshTranslator::SetCustomPrimitiveData(CustomPrimitiveData, InMeshComponent); + } +} + +bool +FHoudiniMeshTranslator::CreateStaticMeshFromHoudiniGeoPartObject( + const FHoudiniGeoPartObject& InHGPO, + const FHoudiniPackageParams& InPackageParams, + const TMap& InOutputObjects, + TMap& OutOutputObjects, + TMap>& AssignmentMaterialMap, + TMap>& ReplacementMaterialMap, + const TMap>& InAllOutputMaterials, + UObject* const InOuterComponent, + const bool& InForceRebuild, + EHoudiniStaticMeshMethod InStaticMeshMethod, + bool bSplitMeshSupport, + const FHoudiniStaticMeshGenerationProperties& InSMGenerationProperties, + const FMeshBuildSettings& InSMBuildSettings, + bool bInTreatExistingMaterialsAsUpToDate) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateStaticMeshFromHoudiniGeoPartObject); + + // If we're not forcing the rebuild + // No need to recreate something that hasn't changed + if (!InForceRebuild && !InHGPO.bHasGeoChanged && !InHGPO.bHasPartChanged && InOutputObjects.Num() > 0) + { + // Simply reuse the existing meshes + OutOutputObjects = InOutputObjects; + return true; + } + + // Create a new mesh translator to handle the output data creation + FHoudiniMeshTranslator CurrentTranslator; + CurrentTranslator.ForceRebuild = InForceRebuild; + CurrentTranslator.SetHoudiniGeoPartObject(InHGPO); + CurrentTranslator.SetInputObjects(InOutputObjects); + CurrentTranslator.SetOutputObjects(OutOutputObjects); + CurrentTranslator.SetInputAssignmentMaterials(AssignmentMaterialMap); + CurrentTranslator.SetAllOutputMaterials(InAllOutputMaterials); + CurrentTranslator.SetReplacementMaterials(ReplacementMaterialMap); + CurrentTranslator.SetPackageParams(InPackageParams, true); + CurrentTranslator.SetTreatExistingMaterialsAsUpToDate(bInTreatExistingMaterialsAsUpToDate); + CurrentTranslator.SetStaticMeshGenerationProperties(InSMGenerationProperties); + CurrentTranslator.SetStaticMeshBuildSettings(InSMBuildSettings); + CurrentTranslator.SetOuterComponent(InOuterComponent); + + // TODO: Fetch from settings/HAC + CurrentTranslator.DefaultMeshSmoothing = 1; + if (false) + CurrentTranslator.DefaultMeshSmoothing = 0; + + // Create the Static Mesh with the desired method + switch (InStaticMeshMethod) + { + case EHoudiniStaticMeshMethod::FMeshDescription: + if (bSplitMeshSupport) + CurrentTranslator.CreateStaticMeshesFromSplitGroups(); + else + CurrentTranslator.CreateStaticMesh_MeshDescription(); + break; + + case EHoudiniStaticMeshMethod::UHoudiniStaticMesh: + if (bSplitMeshSupport) + CurrentTranslator.CreateHoudiniStaticMeshesFromSplitGroups(); + else + CurrentTranslator.CreateHoudiniStaticMesh(); + + break; + } + + // Copy the output objects/materials + OutOutputObjects = CurrentTranslator.OutputObjects; + AssignmentMaterialMap = CurrentTranslator.OutputAssignmentMaterials; + + return true; +} + +bool +FHoudiniMeshTranslator::UpdatePartVertexList() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::UpdatePartVertexList); + + if (HGPO.PartInfo.VertexCount <= 0) + return false; + + // Get the vertex List + PartVertexList.SetNumUninitialized(HGPO.PartInfo.VertexCount); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetVertexList( + FHoudiniEngine::Get().GetSession(), + HGPO.GeoId, HGPO.PartId, &PartVertexList[0], 0, HGPO.PartInfo.VertexCount)) + { + // Error getting the vertex list. + HOUDINI_LOG_MESSAGE( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s] unable to retrieve vertex list - skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName); + + return false; + } + + return true; +} + +void +FHoudiniMeshTranslator::SortSplitGroups() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::SortSplitGroups); + + // Sort the splits in the order that we want to process them: + // Simple/Convex invisible colliders should be treated first as they will need to be attached to the visible meshes + TArray First; + + // The main geo and its LODs should be created after. + TArray Main; + TArray LODs; + + // Finally, visible colliders and invisible complex colliders as they need their own static mesh + TArray Last; + + for (auto& curSplit : HGPO.SplitGroups) + { + EHoudiniSplitType curSplitType = GetSplitTypeFromSplitName(curSplit); + switch (curSplitType) + { + case EHoudiniSplitType::InvisibleSimpleCollider: + case EHoudiniSplitType::InvisibleUCXCollider: + First.Add(curSplit); + break; + + case EHoudiniSplitType::Normal: + Main.Add(curSplit); + break; + + case EHoudiniSplitType::LOD: + LODs.Add(curSplit); + break; + + case EHoudiniSplitType::RenderedSimpleCollider: + case EHoudiniSplitType::RenderedUCXCollider: + case EHoudiniSplitType::RenderedComplexCollider: + case EHoudiniSplitType::InvisibleComplexCollider: + Last.Add(curSplit); + break; + } + } + + // Make sure LODs are order by name + LODs.Sort(); + + // Copy the split names in order + AllSplitGroups.Empty(); + for (auto& splitName : First) + AllSplitGroups.Add(splitName); + + for (auto& splitName : Main) + AllSplitGroups.Add(splitName); + + for (auto& splitName : LODs) + AllSplitGroups.Add(splitName); + + for (auto& splitName : Last) + AllSplitGroups.Add(splitName); +} + +bool +FHoudiniMeshTranslator::UpdateSplitsFacesAndIndices() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::UpdateSplitsFacesAndIndices); + + // Reset the splits faces/indices arrays + AllSplitVertexLists.Empty(); + AllSplitVertexCounts.Empty(); + AllSplitFaceIndices.Empty(); + AllSplitFirstValidVertexIndex.Empty(); + AllSplitFirstValidPrimIndex.Empty(); + + bool bHasSplit = AllSplitGroups.Num() > 0; + if (bHasSplit) + { + HAPI_PartInfo PartInfo = FHoudiniEngineUtils::ToHAPIPartInfo(HGPO.PartInfo); + + // Buffer for all vertex indices used for split groups. + // We need this to figure out all vertex indices that are not part of them. + TArray PartUsedVertices; + PartUsedVertices.SetNumZeroed(PartVertexList.Num()); + + // Buffer for all face indices used for split groups. + // We need this to figure out all face indices that are not part of them. + TArray AllGroupFaceIndices; + AllGroupFaceIndices.SetNumZeroed(HGPO.PartInfo.FaceCount); + + // Some of the groups may contain invalid geometry + // Store them here so we can remove them afterwards + TArray InvalidGroupNameIndices; + + // Extract the vertices/faces for each of the split groups + for (int32 SplitIdx = 0; SplitIdx < AllSplitGroups.Num(); SplitIdx++) + { + const FString& GroupName = AllSplitGroups[SplitIdx]; + + // New vertex list just for this group. + TArray< int32 > GroupVertexList; + TArray< int32 > AllFaceList; + + int32 FirstValidPrimIndex = 0; + int32 FirstValidVertexIndex = 0; + // Extract vertex indices for this split. + int32 GroupVertexListCount = FHoudiniEngineUtils::HapiGetVertexListForGroup( + HGPO.GeoId, PartInfo, GroupName, + PartVertexList, GroupVertexList, + PartUsedVertices, AllFaceList, AllGroupFaceIndices, + FirstValidVertexIndex, FirstValidPrimIndex, + HGPO.PartInfo.bIsInstanced); + + if (GroupVertexListCount <= 0) + { + // This group doesn't have vertices/faces, mark it as invalid + InvalidGroupNameIndices.Add(SplitIdx); + + // Error getting the vertex list. + HOUDINI_LOG_MESSAGE( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s] unable to retrieve vertex list for group %s - skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, *GroupName); + + continue; + } + + // If list is not empty, we store it for this group - this will define new mesh. + AllSplitVertexLists.Add(GroupName, GroupVertexList); + AllSplitVertexCounts.Add(GroupName, GroupVertexListCount); + AllSplitFaceIndices.Add(GroupName, AllFaceList); + AllSplitFirstValidVertexIndex.Add(GroupName, FirstValidVertexIndex); + AllSplitFirstValidPrimIndex.Add(GroupName, FirstValidPrimIndex); + } + + if (InvalidGroupNameIndices.Num() > 0) + { + // Remove all invalid split groups + for (int32 InvalIdx = InvalidGroupNameIndices.Num() - 1; InvalIdx >= 0; InvalIdx--) + { + int32 Index = InvalidGroupNameIndices[InvalIdx]; + AllSplitGroups.RemoveAt(Index); + } + } + + // We also need to figure out / construct the vertex list for everything that's not in a split group + TArray GroupSplitFacesRemaining; + GroupSplitFacesRemaining.SetNumUninitialized(PartVertexList.Num()); + for (int32 n = 0; n < GroupSplitFacesRemaining.Num(); n++) + GroupSplitFacesRemaining[n] = -1; + + int32 GroupVertexListCount = 0; + bool bHasMainSplitGroup = false; + TArray< int32 > GroupSplitFaceIndicesRemaining; + int32 FistUnusedVertexIndex = -1; + for (int32 SplitVertexIdx = 0; SplitVertexIdx < PartUsedVertices.Num(); SplitVertexIdx++) + { + if (PartUsedVertices[SplitVertexIdx] == 0) + { + // This is an unused index, we need to add it to unused vertex list. + FistUnusedVertexIndex = SplitVertexIdx; + GroupSplitFacesRemaining[SplitVertexIdx] = PartVertexList[SplitVertexIdx]; + bHasMainSplitGroup = true; + GroupVertexListCount++; + } + } + + int32 FistUnusedPrimIndex = -1; + for (int32 SplitFaceIdx = 0; SplitFaceIdx < AllGroupFaceIndices.Num(); SplitFaceIdx++) + { + if (AllGroupFaceIndices[SplitFaceIdx] == 0) + { + // This is unused face, we need to add it to unused faces list. + GroupSplitFaceIndicesRemaining.Add(SplitFaceIdx); + FistUnusedPrimIndex = SplitFaceIdx; + } + } + + // We store the remaining geo vertex list as a special split named "main geo" + // and make sure its treated before the collider meshes + if (bHasMainSplitGroup) + { + static const FString RemainingGroupName = HAPI_UNREAL_GROUP_GEOMETRY_NOT_COLLISION; + AllSplitGroups.Add(RemainingGroupName); + AllSplitVertexLists.Add(RemainingGroupName, GroupSplitFacesRemaining); + AllSplitVertexCounts.Add(RemainingGroupName, GroupVertexListCount); + AllSplitFaceIndices.Add(RemainingGroupName, GroupSplitFaceIndicesRemaining); + AllSplitFirstValidPrimIndex.Add(RemainingGroupName, FistUnusedPrimIndex); + AllSplitFirstValidVertexIndex.Add(RemainingGroupName, FistUnusedVertexIndex); + } + } + else + { + // No splitting required + // Mark everything as the main geo group + static const FString RemainingGroupName = HAPI_UNREAL_GROUP_GEOMETRY_NOT_COLLISION; + AllSplitGroups.Add(RemainingGroupName); + AllSplitVertexLists.Add(RemainingGroupName, PartVertexList); + AllSplitVertexCounts.Add(RemainingGroupName, PartVertexList.Num()); + AllSplitFirstValidPrimIndex.Add(RemainingGroupName, 0); + AllSplitFirstValidVertexIndex.Add(RemainingGroupName, 0); + + TArray AllFaces; + for (int32 FaceIdx = 0; FaceIdx < HGPO.PartInfo.FaceCount; ++FaceIdx) + AllFaces.Add(FaceIdx); + + AllSplitFaceIndices.Add(RemainingGroupName, AllFaces); + } + + return true; +} + +void +FHoudiniMeshTranslator::ResetPartCache() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::ResetPartCache); + + // Vertex Positions + PartPositions.Empty(); + FHoudiniApi::AttributeInfo_Init(&AttribInfoPositions); + + // Vertex Normals + PartNormals.Empty(); + FHoudiniApi::AttributeInfo_Init(&AttribInfoNormals); + + // Vertex TangentU + PartTangentU.Empty(); + FHoudiniApi::AttributeInfo_Init(&AttribInfoTangentU); + + // Vertex TangentV + PartTangentV.Empty(); + FHoudiniApi::AttributeInfo_Init(&AttribInfoTangentV); + + // Vertex Colors + PartColors.Empty(); + FHoudiniApi::AttributeInfo_Init(&AttribInfoColors); + + // Vertex Alpha values + PartAlphas.Empty(); + FHoudiniApi::AttributeInfo_Init(&AttribInfoAlpha); + + // FaceSmoothing values + PartFaceSmoothingMasks.Empty(); + FHoudiniApi::AttributeInfo_Init(&AttribInfoFaceSmoothingMasks); + + // UVs + PartUVSets.Empty(); + AttribInfoUVSets.Empty(); + + // UVs + PartLightMapResolutions.Empty(); + FHoudiniApi::AttributeInfo_Init(&AttribInfoLightmapResolution); + + // Material IDs per face + PartFaceMaterialIds.Empty(); + FHoudiniApi::AttributeInfo_Init(&AttribInfoFaceMaterialIds); + // Unique material IDs + PartUniqueMaterialIds.Empty(); + // Material infos for each unique Material + PartUniqueMaterialInfos.Empty(); + // + bOnlyOneFaceMaterial = false; + + // Face Materials override + PartFaceMaterialOverrides.Empty(); + bHaveMaterialOverrides = false; + bHavePrimMaterialOverrides = false; + bMaterialOverrideNeedsCreateInstance = false; + + // LOD Screensize + PartLODScreensize.Empty(); +} + +bool +FHoudiniMeshTranslator::UpdatePartPositionIfNeeded() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::UpdatePartPositionIfNeeded); + + // Only Retrieve the vertices positions if necessary + if (PartPositions.Num() > 0) + return true; + + FHoudiniHapiAccessor Accessor(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, HAPI_UNREAL_ATTRIB_POSITION); + Accessor.GetInfo(AttribInfoPositions); + + if (!Accessor.GetAttributeData(AttribInfoPositions, PartPositions)) + { + // Error retrieving positions. + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], unable to retrieve position data") + TEXT("- skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName); + return false; + } + return true; +} + +bool +FHoudiniMeshTranslator::UpdatePartNormalsIfNeeded() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::UpdatePartNormalsIfNeeded); + + // No need to read the normals if we want unreal to recompute them after + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + bool bReadNormals = HoudiniRuntimeSettings ? HoudiniRuntimeSettings->RecomputeNormalsFlag != EHoudiniRuntimeSettingsRecomputeFlag::HRSRF_Always : true; + if (!bReadNormals) + return true; + + // Only Retrieve the normals if we haven't already + if (PartNormals.Num() > 0) + return true; + + FHoudiniHapiAccessor Accessor(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, HAPI_UNREAL_ATTRIB_NORMAL); + Accessor.GetInfo(AttribInfoNormals); + + // Retrieve normal data for this part + bool Success = Accessor.GetAttributeData(AttribInfoNormals, PartNormals); + + // There is no normals to fetch + if (!AttribInfoNormals.exists) + return true; + + if (!Success && AttribInfoNormals.exists) + { + // Error retrieving normals. + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], unable to retrieve normal data"), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName); + return false; + } + + return true; +} + +bool +FHoudiniMeshTranslator::UpdatePartTangentsIfNeeded() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::UpdatePartTangentsIfNeeded); + + bool bReturn = true; + if (PartTangentU.Num() <= 0) + { + // Retrieve TangentU data for this part + FHoudiniHapiAccessor Accessor(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, HAPI_UNREAL_ATTRIB_TANGENTU); + Accessor.GetInfo(AttribInfoTangentU); + bool Success = Accessor.GetAttributeData(AttribInfoTangentU, PartTangentU); + + if (!Success && AttribInfoTangentU.exists) + { + // Error retrieving tangent. + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], unable to retrieve tangentU data"), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName); + bReturn = false; + } + } + + if (PartTangentV.Num() <= 0) + { + FHoudiniHapiAccessor Accessor(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, HAPI_UNREAL_ATTRIB_TANGENTV); + Accessor.GetInfo(AttribInfoTangentV); + bool Success = Accessor.GetAttributeData(AttribInfoTangentV, PartTangentV); + + if (!Success && AttribInfoTangentV.exists) + { + // Error retrieving tangent. + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], unable to retrieve tangentV data"), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName); + bReturn = false; + } + } + + return bReturn; +} + +bool +FHoudiniMeshTranslator::UpdatePartColorsIfNeeded() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::UpdatePartColorsIfNeeded); + + // Only Retrieve the vertices colors if necessary + if (PartColors.Num() > 0) + return true; + + FHoudiniHapiAccessor Accessor(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, HAPI_UNREAL_ATTRIB_COLOR); + Accessor.GetInfo(AttribInfoColors); + bool Success = Accessor.GetAttributeData(AttribInfoColors, PartColors); + + if (!Success && AttribInfoColors.exists) + { + // Error retrieving colors. + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], unable to retrieve color data"), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName); + return false; + } + + return true; +} + +bool +FHoudiniMeshTranslator::UpdatePartAlphasIfNeeded() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::UpdatePartAlphasIfNeeded); + + // Only Retrieve the vertices alphas if necessary + if (PartAlphas.Num() > 0) + return true; + + FHoudiniHapiAccessor Accessor(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, HAPI_UNREAL_ATTRIB_ALPHA); + Accessor.GetInfo(AttribInfoAlpha); + bool Success = Accessor.GetAttributeData(AttribInfoAlpha, PartAlphas); + + if (!Success && AttribInfoAlpha.exists) + { + // Error retrieving alpha values. + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], unable to retrieve alpha data"), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName); + return false; + } + + return true; +} + +bool +FHoudiniMeshTranslator::UpdatePartFaceSmoothingIfNeeded() +{ + // Only Retrieve the vertices FaceSmoothing if necessary + if (PartFaceSmoothingMasks.Num() > 0) + return true; + + FHoudiniHapiAccessor Accessor(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, HAPI_UNREAL_ATTRIB_FACE_SMOOTHING_MASK); + Accessor.GetInfo(AttribInfoFaceSmoothingMasks); + bool Success = Accessor.GetAttributeData(AttribInfoFaceSmoothingMasks, PartFaceSmoothingMasks); + + if (!Success && AttribInfoFaceSmoothingMasks.exists) + { + // Error retrieving FaceSmoothing values. + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], unable to retrieve FaceSmoothing data"), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName); + return false; + } + + return true; +} + +bool +FHoudiniMeshTranslator::UpdatePartUVSetsIfNeeded(const bool& bRemoveUnused) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::UpdatePartUVSetsIfNeeded); + + FHoudiniEngineUtils::UpdateMeshPartUVSets(HGPO.GeoId, HGPO.PartId, bRemoveUnused, PartUVSets, AttribInfoUVSets); + + return true; +} + +bool +FHoudiniMeshTranslator::UpdatePartLightmapResolutionsIfNeeded() +{ + // Only Retrieve the vertices light map resolution if necessary + if (PartLightMapResolutions.Num() > 0) + return true; + + FHoudiniHapiAccessor Accessor(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, HAPI_UNREAL_ATTRIB_LIGHTMAP_RESOLUTION); + Accessor.GetInfo(AttribInfoLightmapResolution); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, PartLightMapResolutions); + + if (!bSuccess && AttribInfoLightmapResolution.exists) + { + // Error retrieving lightmap resolution values. + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], unable to retrieve lightmap resolution data"), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName); + return false; + } + + return true; +} + +bool +FHoudiniMeshTranslator::UpdatePartFaceMaterialIDsIfNeeded() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::UpdatePartFaceMaterialIDsIfNeeded); + + // Only Retrieve the material IDs if necessary + if (PartFaceMaterialIds.Num() > 0) + return true; + + int32 NumFaces = HGPO.PartInfo.FaceCount; + if (NumFaces <= 0) + return true; + + PartFaceMaterialIds.SetNum(NumFaces); + + // Get the materials IDs per face + HAPI_Bool bSingleFaceMaterial = false; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetMaterialNodeIdsOnFaces( + FHoudiniEngine::Get().GetSession(), + HGPO.GeoId, HGPO.PartId, &bSingleFaceMaterial, + &PartFaceMaterialIds[0], 0, NumFaces)) + { + // Error retrieving material face assignments. + HOUDINI_LOG_MESSAGE( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s] unable to retrieve material face assignments"), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName); + return false; + } + + bOnlyOneFaceMaterial = bSingleFaceMaterial; + + return true; +} + +bool +FHoudiniMeshTranslator::UpdatePartFaceMaterialOverridesIfNeeded() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::UpdatePartFaceMaterialOverridesIfNeeded); + + // Only Retrieve the material overrides if necessary + if (PartFaceMaterialOverrides.Num() > 0) + return true; + + bHaveMaterialOverrides = false; + bHavePrimMaterialOverrides = false; + bMaterialOverrideNeedsCreateInstance = false; + + TArray MaterialOverrides; + TArray MaterialInstanceOverrides; + HAPI_AttributeInfo AttribInfoFaceMaterialOverrides; + FHoudiniApi::AttributeInfo_Init(&AttribInfoFaceMaterialOverrides); + + FHoudiniHapiAccessor Accessor(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, HAPI_UNREAL_ATTRIB_MATERIAL); + Accessor.GetInfo(AttribInfoFaceMaterialOverrides, HAPI_ATTROWNER_INVALID); + Accessor.GetAttributeData(AttribInfoFaceMaterialOverrides, MaterialOverrides); + + bool bMaterialAttributeExists = AttribInfoFaceMaterialOverrides.exists; + HAPI_AttributeOwner MaterialAttrOwner = bMaterialAttributeExists ? AttribInfoFaceMaterialOverrides.owner : HAPI_ATTROWNER_INVALID; + if (bMaterialAttributeExists && MaterialAttrOwner != HAPI_ATTROWNER_DETAIL && MaterialAttrOwner != HAPI_ATTROWNER_PRIM) + { + HOUDINI_LOG_WARNING(TEXT("Static Mesh [%d %s], Geo [%d], Part [%d %s]: " HAPI_UNREAL_ATTRIB_MATERIAL " must be a primitive or detail attribute, ignoring attribute."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName); + MaterialOverrides.Empty(); + bMaterialAttributeExists = false; + } + + // If material attribute and fallbacks were not found, check the material instance attribute. + Accessor.Init(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, HAPI_UNREAL_ATTRIB_MATERIAL_INSTANCE); + Accessor.GetInfo(AttribInfoFaceMaterialOverrides, HAPI_ATTROWNER_INVALID); + Accessor.GetAttributeData(AttribInfoFaceMaterialOverrides, MaterialInstanceOverrides); + + bool bMaterialInstanceAttributeExists = AttribInfoFaceMaterialOverrides.exists; + const HAPI_AttributeOwner MaterialInstanceAttrOwner = bMaterialInstanceAttributeExists ? AttribInfoFaceMaterialOverrides.owner : HAPI_ATTROWNER_INVALID; + if (bMaterialInstanceAttributeExists && MaterialInstanceAttrOwner != HAPI_ATTROWNER_DETAIL && MaterialInstanceAttrOwner != HAPI_ATTROWNER_PRIM) + { + HOUDINI_LOG_WARNING(TEXT("Static Mesh [%d %s], Geo [%d], Part [%d %s]: " HAPI_UNREAL_ATTRIB_MATERIAL_INSTANCE " must be a primitive or detail attribute, ignoring attribute."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName); + MaterialInstanceOverrides.Empty(); + bMaterialInstanceAttributeExists = false; + } + + // If material attribute was not found, check fallback compatibility attribute. + if ((!bMaterialAttributeExists && !bMaterialInstanceAttributeExists) || (MaterialOverrides.Num() == 0 && MaterialInstanceOverrides.Num() == 0)) + { + PartFaceMaterialOverrides.Empty(); + + Accessor.Init(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, HAPI_UNREAL_ATTRIB_MATERIAL_FALLBACK); + Accessor.GetInfo(AttribInfoFaceMaterialOverrides, HAPI_ATTROWNER_INVALID); + Accessor.GetAttributeData(AttribInfoFaceMaterialOverrides, MaterialOverrides); + + bMaterialAttributeExists = AttribInfoFaceMaterialOverrides.exists; + MaterialAttrOwner = bMaterialAttributeExists ? AttribInfoFaceMaterialOverrides.owner : HAPI_ATTROWNER_INVALID; + if (bMaterialAttributeExists && MaterialAttrOwner != HAPI_ATTROWNER_DETAIL && MaterialAttrOwner != HAPI_ATTROWNER_PRIM) + { + HOUDINI_LOG_WARNING(TEXT("Static Mesh [%d %s], Geo [%d], Part [%d %s]: " HAPI_UNREAL_ATTRIB_MATERIAL_FALLBACK " must be a primitive or detail attribute, ignoring attribute."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName); + MaterialOverrides.Empty(); + bMaterialAttributeExists = false; + } + } + + // If no override attributes exist then there is nothing more to do + if (!bMaterialAttributeExists && !bMaterialInstanceAttributeExists) + return false; + + if ((!bMaterialAttributeExists || MaterialAttrOwner == HAPI_ATTROWNER_DETAIL) && (!bMaterialInstanceAttributeExists || MaterialInstanceAttrOwner == HAPI_ATTROWNER_DETAIL)) + { + // either only one attribute exists and is a detail attribute, or both exist and are detail attributes + bHavePrimMaterialOverrides = false; + FHoudiniMaterialInfo MatInfo; + if (MaterialOverrides.IsValidIndex(0) && !MaterialOverrides[0].IsEmpty()) + { + MatInfo.MaterialObjectPath = MaterialOverrides[0]; + ExtractMaterialIndex(MatInfo.MaterialObjectPath, MatInfo.MaterialIndex); + } + else if (MaterialInstanceOverrides.IsValidIndex(0) && !MaterialInstanceOverrides[0].IsEmpty()) + { + MatInfo.bMakeMaterialInstance = true; + bMaterialOverrideNeedsCreateInstance = true; + MatInfo.MaterialObjectPath = MaterialInstanceOverrides[0]; + ExtractMaterialIndex(MatInfo.MaterialObjectPath, MatInfo.MaterialIndex); + } + else + { + MatInfo.MaterialObjectPath = FString(); + } + // PartFaceMaterialOverrides must have an entry for each face, or be empty + if (!MatInfo.MaterialObjectPath.IsEmpty()) + PartFaceMaterialOverrides.Init(MatInfo, HGPO.PartInfo.FaceCount); + else + PartFaceMaterialOverrides.Empty(); + } + else + { + // Cases to handle here: both exist and are prim, or one is prim and one detail, or only one exists and is prim + bHavePrimMaterialOverrides = true; + // PartFaceMaterialOverrides must have an entry for each face, or be empty + PartFaceMaterialOverrides.Reset(HGPO.PartInfo.FaceCount); + for (int32 Index = 0; Index < HGPO.PartInfo.FaceCount; ++Index) + { + FHoudiniMaterialInfo& MatInfo = PartFaceMaterialOverrides.AddDefaulted_GetRef(); + + // Determine the potential indexes: Index for primitive attributes and 0 for detail attribute + int32 MaterialOverridesIndex = INDEX_NONE; + int32 MaterialInstanceOverridesIndex = INDEX_NONE; + if (MaterialAttrOwner == MaterialInstanceAttrOwner) + { + // Both attributes are prim + MaterialOverridesIndex = Index; + MaterialInstanceOverridesIndex = Index; + } + else if (MaterialAttrOwner == HAPI_ATTROWNER_PRIM) + { + // MaterialOverrides is prim, MaterialInstanceOverrides is detail or does not exist + MaterialOverridesIndex = Index; + MaterialInstanceOverridesIndex = 0; + } + else + { + // MaterialOverrides is detail, MaterialInstanceOverrides is prim (one of the two might not exist) + MaterialOverridesIndex = 0; + MaterialInstanceOverridesIndex = Index; + } + + // MaterialOverrides (unreal_material) takes precedence, if non-empty, over MaterialInstanceOverrides (unreal_material_instance) + if (MaterialOverrides.IsValidIndex(MaterialOverridesIndex) && !MaterialOverrides[MaterialOverridesIndex].IsEmpty()) + { + MatInfo.MaterialObjectPath = MaterialOverrides[MaterialOverridesIndex]; + ExtractMaterialIndex(MatInfo.MaterialObjectPath, MatInfo.MaterialIndex); + } + else if (MaterialInstanceOverrides.IsValidIndex(MaterialInstanceOverridesIndex) && !MaterialInstanceOverrides[MaterialInstanceOverridesIndex].IsEmpty()) + { + MatInfo.bMakeMaterialInstance = true; + bMaterialOverrideNeedsCreateInstance = true; + MatInfo.MaterialObjectPath = MaterialInstanceOverrides[MaterialInstanceOverridesIndex]; + ExtractMaterialIndex(MatInfo.MaterialObjectPath, MatInfo.MaterialIndex); + } + else + { + MatInfo.MaterialObjectPath = FString(); + } + } + } + + bHaveMaterialOverrides = PartFaceMaterialOverrides.Num() > 0; + if (bHaveMaterialOverrides) + FHoudiniMaterialTranslator::GetMaterialParameters(PartFaceMaterialOverrides, HGPO.GeoId, HGPO.PartId, HAPI_ATTROWNER_PRIM); + + return true; +} + +bool +FHoudiniMeshTranslator::UpdatePartNeededMaterials() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::UpdatePartNeededMaterials); + + // Update the per face material IDs + UpdatePartFaceMaterialIDsIfNeeded(); + + // See if we have some material overides + UpdatePartFaceMaterialOverridesIfNeeded(); + + // If we have houdini materials AND overrides: + // We want to only create the Houdini materials that are not "covered" by overrides + // If we have material instance attributes, create all the houdini material anyway + // as their textures could be referenced by the material instance parameters + if (PartFaceMaterialOverrides.Num() > 0 && !bMaterialOverrideNeedsCreateInstance) + { + // If the material override was set on the detail, no need to look for houdini material IDs, as only the override will be used + if (bHaveMaterialOverrides && bHavePrimMaterialOverrides) + { + for (int32 MaterialIdx = 0; MaterialIdx < PartFaceMaterialIds.Num(); ++MaterialIdx) + { + // Add a material ID to the unique array only if that face is not using the override + if (PartFaceMaterialOverrides[MaterialIdx].MaterialObjectPath.IsEmpty()) + PartUniqueMaterialIds.AddUnique(PartFaceMaterialIds[MaterialIdx]); + } + } + } + else + { + // No material overrides, simply update the unique material array + for (int32 MaterialIdx = 0; MaterialIdx < PartFaceMaterialIds.Num(); ++MaterialIdx) + PartUniqueMaterialIds.AddUnique(PartFaceMaterialIds[MaterialIdx]); + } + + // Remove the invalid material ID from the unique array + PartUniqueMaterialIds.RemoveSingle(-1); + + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::UpdatePartNeededMaterials - Get the unique material infos); + // Get the unique material infos + PartUniqueMaterialInfos.SetNum(PartUniqueMaterialIds.Num()); + for (int32 MaterialIdx = 0; MaterialIdx < PartUniqueMaterialIds.Num(); MaterialIdx++) + { + + FHoudiniApi::MaterialInfo_Init(&PartUniqueMaterialInfos[MaterialIdx]); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetMaterialInfo( + FHoudiniEngine::Get().GetSession(), + PartUniqueMaterialIds[MaterialIdx], + &PartUniqueMaterialInfos[MaterialIdx])) + { + // Error retrieving material face assignments. + HOUDINI_LOG_MESSAGE( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s] unable to retrieve material info for material %d"), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, PartUniqueMaterialIds[MaterialIdx]); + continue; + } + } + } + return true; +} + +bool +FHoudiniMeshTranslator::UpdatePartLODScreensizeIfNeeded() +{ + // Only retrieve LOD screensizes if necessary + if (PartLODScreensize.Num() > 0) + return true; + + FHoudiniHapiAccessor Accessor(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, HAPI_UNREAL_ATTRIB_LOD_SCREENSIZE); + bool Success = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, PartLODScreensize); + return Success; +} + + +void +FHoudiniMeshTranslator::UpdateStaticMeshNaniteSettings(const int32& GeoId, const int32& PartId, const int32& PrimIndex, UStaticMesh* StaticMesh) +{ + if (!StaticMesh) + return; + + // + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + + // Start by looking for the nanite enabled attribute, disabled by default + bool bEnableNanite = false; + TArray IntData; + + FHoudiniHapiAccessor Accessor; + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_NANITE_ENABLED); + if (!Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, IntData, PrimIndex, 1)) + { + Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, IntData, 0, 1); + } + if (IntData.Num() > 0) + { + bEnableNanite = (IntData[0] != 0); + } + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + FMeshNaniteSettings NaniteSettings = StaticMesh->GetNaniteSettings(); +#endif + + // Then look for the position precision attribute, auto by default (MIN_int32) + IntData.Empty(); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + NaniteSettings.PositionPrecision = MIN_int32; +#else + StaticMesh->NaniteSettings.PositionPrecision = MIN_int32; +#endif + // Look for a specific prim attribute first + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_NANITE_POSITION_PRECISION); + if (!Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, IntData, PrimIndex, 1)) + { + Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, IntData, 0, 1); + } + + if (IntData.Num() > 0) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + NaniteSettings.PositionPrecision = IntData[0]; +#else + StaticMesh->NaniteSettings.PositionPrecision = IntData[0]; +#endif + } + + // Look for the percent triangle attribute, one by default (all triangles) + // as this mesh is also used in the physics engine as the complex collision version + + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + NaniteSettings.FallbackPercentTriangles = 1.0f; +#else + StaticMesh->NaniteSettings.FallbackPercentTriangles = 1.0f; +#endif + TArray FloatData; + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_NANITE_PERCENT_TRIANGLES); + + // Look for a specific prim attribute first + if (!Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, 1, FloatData, PrimIndex, 1)) + { + //Global search for the attribute + Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, FloatData, PrimIndex, 1); + } + + if (FloatData.Num() > 0) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + // If a nanite percent triangles attribute was found, we likely also want to set the fallback target to PercentTriangles +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + NaniteSettings.FallbackTarget = ENaniteFallbackTarget::PercentTriangles; +#else + StaticMesh->NaniteSettings.FallbackTarget = ENaniteFallbackTarget::PercentTriangles; +#endif +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + NaniteSettings.FallbackPercentTriangles = FMath::Clamp(FloatData[0], 0.0f, 1.0f); +#else + StaticMesh->NaniteSettings.FallbackPercentTriangles = FMath::Clamp(FloatData[0], 0.0f, 1.0f); +#endif + } + } + + { + // Also look for an attribute setting the relative error (default to 1) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + NaniteSettings.FallbackRelativeError = 1.0f; +#else + StaticMesh->NaniteSettings.FallbackRelativeError = 1.0f; +#endif + + TArray FloatData; + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_NANITE_FB_RELATIVE_ERROR); + if (!Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, FloatData, PrimIndex, 1)) + { + //Global search for the attribute + Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, FloatData, PrimIndex, 1); + } + + if (FloatData.Num() > 0) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + // If a nanite relative error attribute was found, we likely also want to set the fallback target to RelativeError +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + NaniteSettings.FallbackTarget = ENaniteFallbackTarget::RelativeError; +#else + StaticMesh->NaniteSettings.FallbackTarget = ENaniteFallbackTarget::RelativeError; +#endif +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + NaniteSettings.FallbackRelativeError = FMath::Clamp(FloatData[0], 0.0f, 1.0f); +#else + StaticMesh->NaniteSettings.FallbackRelativeError = FMath::Clamp(FloatData[0], 0.0f, 1.0f); +#endif + } + } + + { + // And do the same for the trim relative error (default to 0) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + NaniteSettings.TrimRelativeError = 0.0f; +#else + StaticMesh->NaniteSettings.TrimRelativeError = 0.0f; +#endif + TArray FloatData; + Accessor.Init(GeoId, PartId, HAPI_UNREAL_ATTRIB_NANITE_TRIM_RELATIVE_ERROR); + if (!Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, FloatData, PrimIndex, 1)) + { + //Global search for the attribute + Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, FloatData, PrimIndex, 1); + } + + if (FloatData.Num() > 0) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + NaniteSettings.TrimRelativeError = FMath::Clamp(FloatData[0], 0.0f, 1.0f); +#else + StaticMesh->NaniteSettings.TrimRelativeError = FMath::Clamp(FloatData[0], 0.0f, 1.0f); +#endif + } + } +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + NaniteSettings.bEnabled = bEnableNanite; + StaticMesh->SetNaniteSettings(NaniteSettings); +#else + StaticMesh->NaniteSettings.bEnabled = bEnableNanite; +#endif +} + +void FHoudiniMeshTranslator::CopyAttributesFromHGPOForSplit( + const FHoudiniGeoPartObject& InHGPO, const int32 InPointIndex, const int32 InPrimIndex, TMap& OutAttributes, TMap& OutTokens) +{ + // Get all the supported attributes from the HGPO + { + FString TempFolder; + if (FHoudiniEngineUtils::GetTempFolderAttribute(InHGPO.GeoId, TempFolder, InHGPO.PartId, InPrimIndex)) + { + if (!TempFolder.IsEmpty()) + { + // cache the level path attribute on the output object + OutAttributes.Add(HAPI_UNREAL_ATTRIB_TEMP_FOLDER, TempFolder); + } + } + } + + { + FString LevelPath; + if (FHoudiniEngineUtils::GetLevelPathAttribute(InHGPO.GeoId, InHGPO.PartId, LevelPath, InPointIndex, InPrimIndex)) + { + if (!LevelPath.IsEmpty()) + { + // cache the level path attribute on the output object + OutAttributes.Add(HAPI_UNREAL_ATTRIB_LEVEL_PATH, LevelPath); + } + } + } + + { + FString OutputName; + if (FHoudiniEngineUtils::GetOutputNameAttribute(InHGPO.GeoId, InHGPO.PartId, OutputName, InPointIndex, InPrimIndex)) + { + if (!OutputName.IsEmpty()) + { + // cache the output name attribute on the output object + OutAttributes.Add(HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2, OutputName); + } + } + } + + { + FString BakeName; + if (FHoudiniEngineUtils::GetBakeNameAttribute(InHGPO.GeoId, InHGPO.PartId, BakeName, InPointIndex, InPrimIndex)) + { + if (!BakeName.IsEmpty()) + { + // cache the bake name attribute on the output object + OutAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_NAME, BakeName); + } + } + } + + { + int32 TileValue; + if (FHoudiniEngineUtils::GetTileAttribute(InHGPO.GeoId, InHGPO.PartId, TileValue, InPointIndex, InPrimIndex)) + { + if (TileValue >= 0) + { + // cache the tile attribute as a token on the output object + OutTokens.Add(TEXT("tile"), FString::FromInt(TileValue)); + } + } + } + + { + FString BakeOutputActorName; + if (FHoudiniEngineUtils::GetBakeActorAttribute(InHGPO.GeoId, InHGPO.PartId, BakeOutputActorName, InPointIndex, InPrimIndex)) + { + if (!BakeOutputActorName.IsEmpty()) + { + // cache the bake actor attribute on the output object + OutAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_ACTOR, BakeOutputActorName); + } + } + } + + { + FString BakeOutputActorClassName; + if (FHoudiniEngineUtils::GetBakeActorClassAttribute(InHGPO.GeoId, InHGPO.PartId, BakeOutputActorClassName, InPointIndex, InPrimIndex)) + { + if (!BakeOutputActorClassName.IsEmpty()) + { + // cache the bake actor attribute on the output object + OutAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_ACTOR_CLASS, BakeOutputActorClassName); + } + } + } + + { + FString BakeFolder; + if (FHoudiniEngineUtils::GetBakeFolderAttribute(InHGPO.GeoId, InHGPO.PartId, BakeFolder, InPrimIndex)) + { + if (!BakeFolder.IsEmpty()) + { + // cache the unreal_bake_folder attribute on the output object + OutAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_FOLDER, BakeFolder); + } + } + } + + { + FString BakeOutlinerFolder; + if (FHoudiniEngineUtils::GetBakeOutlinerFolderAttribute(InHGPO.GeoId, InHGPO.PartId, BakeOutlinerFolder, InPointIndex, InPrimIndex)) + { + if (!BakeOutlinerFolder.IsEmpty()) + { + // cache the bake actor attribute on the output object + OutAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_OUTLINER_FOLDER, BakeOutlinerFolder); + } + } + } +} + +void FHoudiniMeshTranslator::CopyAttributesFromHGPOForSplit( + const FString& InSplitGroupName, TMap& OutAttributes, TMap& OutTokens) +{ + int32 PointIndex = INDEX_NONE; + const int32 PrimIndex = AllSplitFirstValidPrimIndex[InSplitGroupName]; + + const int32 FirstValidVertexIndex = AllSplitFirstValidVertexIndex[InSplitGroupName]; + if (FirstValidVertexIndex >= 0 && AllSplitVertexLists[InSplitGroupName].IsValidIndex(FirstValidVertexIndex)) + { + PointIndex = AllSplitVertexLists[InSplitGroupName][FirstValidVertexIndex]; + } + + CopyAttributesFromHGPOForSplit(HGPO, PointIndex, PrimIndex, OutAttributes, OutTokens); +} + +void FHoudiniMeshTranslator::CopyAttributesFromHGPOForSplit( + const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, TMap& OutAttributes, TMap& OutTokens) +{ + const int32 PrimIndex = InOutputObjectIdentifier.PrimitiveIndex; + const int32 PointIndex = InOutputObjectIdentifier.PointIndex; + + CopyAttributesFromHGPOForSplit(HGPO, PointIndex, PrimIndex, OutAttributes, OutTokens); +} + + +UStaticMesh* +FHoudiniMeshTranslator::CreateNewUnrealStaticMesh(const FString& InSplitIdentifier) +{ + // Update the current Obj/Geo/Part/Split IDs + PackageParams.ObjectId = HGPO.ObjectId; + PackageParams.GeoId = HGPO.GeoId; + PackageParams.PartId = HGPO.PartId; + PackageParams.SplitStr = InSplitIdentifier; + + UStaticMesh * NewStaticMesh = PackageParams.CreateObjectAndPackage(); + if (!IsValid(NewStaticMesh)) + return nullptr; + + return NewStaticMesh; +} + +UHoudiniStaticMesh* +FHoudiniMeshTranslator::CreateNewHoudiniStaticMesh(const FString& InSplitIdentifier) +{ + // Update the current Obj/Geo/Part/Split IDs + PackageParams.ObjectId = HGPO.ObjectId; + PackageParams.GeoId = HGPO.GeoId; + PackageParams.PartId = HGPO.PartId; + // Add _HSM suffix to the split str, to distinguish the temporary HoudiniStaticMesh + // from the UStaticMesh + PackageParams.SplitStr = InSplitIdentifier + "_HSM"; + + UHoudiniStaticMesh * NewStaticMesh = PackageParams.CreateObjectAndPackage(); + if (!IsValid(NewStaticMesh)) + return nullptr; + + return NewStaticMesh; +} + + +FHoudiniOutputObjectIdentifier +FHoudiniMeshTranslator::MakeOutputObjectIdentifier(const FString& InSplitGroupName, const EHoudiniSplitType InSplitType) +{ + FHoudiniOutputObjectIdentifier OutputObjectIdentifier( + HGPO.ObjectId, HGPO.GeoId, HGPO.PartId, GetMeshIdentifierFromSplit(InSplitGroupName, InSplitType)); + OutputObjectIdentifier.PartName = HGPO.PartName; + OutputObjectIdentifier.PrimitiveIndex = AllSplitFirstValidPrimIndex[InSplitGroupName]; + const int32 FirstValidVertexIndex = AllSplitFirstValidVertexIndex[InSplitGroupName]; + if (FirstValidVertexIndex >= 0 && AllSplitVertexLists[InSplitGroupName].IsValidIndex(FirstValidVertexIndex)) + { + OutputObjectIdentifier.PointIndex = AllSplitVertexLists[InSplitGroupName][FirstValidVertexIndex]; + } + else + { + OutputObjectIdentifier.PointIndex = -1; + } + + return OutputObjectIdentifier; +} + +bool +FHoudiniMeshTranslator::CreateStaticMesh_MeshDescription() +{ + /////////////////////////////////////////////////////////////////////// + // THIS FUNCTION IS DEPRECATED AND WILL REMOVED IN THE NEXT RELEASE. + /////////////////////////////////////////////////////////////////////// + + // Time limit for processing + bDoTiming = CVarHoudiniEngineMeshBuildTimer.GetValueOnAnyThread() != 0.0; + + double time_start = FPlatformTime::Seconds(); + + bool bIsGammaCorrectionDisabled = IsGammaCorrectionDisabled(HGPO.GeoId, HGPO.PartId); + + // Keep a copy of the initial package params, since PackageParams is modified in place when resolving attributes + FHoudiniPackageParams InitialPackageParams = PackageParams; + + // Start by updating the vertex list + if (!UpdatePartVertexList()) + return false; + + // Sort the split groups + // Simple colliders first, lods and finally, invisible colliders (that are separate Static Mesh) + SortSplitGroups(); + + // Handles the split groups found in the part + // and builds the corresponding faces and indices arrays + if (!UpdateSplitsFacesAndIndices()) + return true; + + // Resets the containers used for the raw data extraction. + ResetPartCache(); + + // Prepare the object that will store UCX and simple colliders + AllAggregateCollisions.Empty(); + + // We need to know the number of LODs that will be needed for this part, and we also need the identifier for the + // main split (Normal or LOD0) + int32 NumberOfLODs = 0; + bool bHasMainGeo = false; + FHoudiniOutputObjectIdentifier MainIdentifier; + bool bHasMainIdentifier = false; + for (auto& curSplit : AllSplitGroups) + { + if (GetSplitTypeFromSplitName(curSplit) == EHoudiniSplitType::LOD) + { + NumberOfLODs++; + if (NumberOfLODs == 1 && !bHasMainGeo) + { + MainIdentifier = MakeOutputObjectIdentifier(curSplit, GetSplitTypeFromSplitName(curSplit)); + bHasMainIdentifier = true; + } + } + else if (GetSplitTypeFromSplitName(curSplit) == EHoudiniSplitType::Normal) + { + bHasMainGeo = true; + MainIdentifier = MakeOutputObjectIdentifier(curSplit, GetSplitTypeFromSplitName(curSplit)); + bHasMainIdentifier = true; + } + } + + if (NumberOfLODs > MAX_STATIC_MESH_LODS) + { + HOUDINI_LOG_ERROR(TEXT("Maximum number of LODs is %d, found %d "), MAX_STATIC_MESH_LODS, NumberOfLODs); + NumberOfLODs = MAX_STATIC_MESH_LODS; + } + // Update the part's material's IDS and info now + CreateNeededMaterials(); + + // Check if the materials were updated + bool bMaterialHasChanged = false; + for (const auto& MatInfo : PartUniqueMaterialInfos) + { + if (MatInfo.hasChanged) + { + bMaterialHasChanged = true; + break; + } + } + + // Get the current target platform for default lod policies + ITargetPlatform * CurrentPlatform = GetTargetPlatformManagerRef().GetRunningTargetPlatform(); + check(CurrentPlatform); + + // New mesh list + TMap StaticMeshToBuild; + + // Map of Houdini Material IDs to Unreal Material Interface + TMap> MapHoudiniMatIdToUnrealInterface; + // Map of Houdini Material Attributes to Unreal Material Interface + TMap> MapHoudiniMatAttributesToUnrealInterface; + // Map of Unreal Material Interface to Unreal Material Index, per visible mesh + TMap, int32>> MapUnrealMaterialInterfaceToUnrealIndexPerMesh; + + bool MeshMaterialsHaveBeenReset = false; + + double tick = FPlatformTime::Seconds(); + if (bDoTiming) + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - Pre Split-Loop in %f seconds."), tick - time_start); + + UStaticMesh* MainStaticMesh = nullptr; + bool bAssignedCustomCollisionMesh = false; + ECollisionTraceFlag MainStaticMeshCTF = StaticMeshGenerationProperties.GeneratedCollisionTraceFlag; + + // Map of object identifiers to package params + TMap ObjectIdentifiersToPackageParams; + + // Iterate through all detected split groups we care about and split geometry. + // The split are ordered in the following way: + // Invisible Simple/Convex Colliders > LODs > MainGeo > Visible Colliders > Invisible Colliders + for (int32 SplitId = 0; SplitId < AllSplitGroups.Num(); SplitId++) + { + double split_tick = FPlatformTime::Seconds(); + + // Get split group name + const FString& SplitGroupName = AllSplitGroups[SplitId]; + + // Get the vertex indices for this group + TArray& SplitVertexList = AllSplitVertexLists[SplitGroupName]; + + // Get valid count of vertex indices for this split. + const int32& SplitVertexCount = AllSplitVertexCounts[SplitGroupName]; + + // Make sure we have a valid vertex count for this split + if (SplitVertexCount % 3 != 0 || SplitVertexList.Num() % 3 != 0) + { + // Invalid vertex count, skip this split or we'd crash trying to create a mesh for it. + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%d %s] invalid vertex count.") + TEXT("- skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, SplitId, *SplitGroupName); + + continue; + } + + // Get the current split type + EHoudiniSplitType SplitType = GetSplitTypeFromSplitName(SplitGroupName); + if (SplitType == EHoudiniSplitType::Invalid) + { + // Invalid split, skip + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%d %s] unknown split type.") + TEXT("- skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, SplitId, *SplitGroupName); + continue; + } + + // Get the output identifer for this split + FHoudiniOutputObjectIdentifier OutputObjectIdentifier = MakeOutputObjectIdentifier(SplitGroupName, SplitType); + + // Get/Create the Aggregate Collisions for this mesh identifier + FKAggregateGeom& AggregateCollisions = AllAggregateCollisions.FindOrAdd(OutputObjectIdentifier); + + // Handle UCX / Convex Hull colliders + if (SplitType == EHoudiniSplitType::InvisibleUCXCollider || SplitType == EHoudiniSplitType::RenderedUCXCollider) + { + // Get the part position if needed + UpdatePartPositionIfNeeded(); + + // Create the convex hull colliders and add them to the Aggregate + if (!AddConvexCollisionToAggregate(SplitGroupName, AggregateCollisions)) + { + MainStaticMeshCTF = ECollisionTraceFlag::CTF_UseDefault; + // Failed to generate a convex collider + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%d %s] failed to create convex collider."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, SplitId, *SplitGroupName); + } + + // If the collider is not visible, stop here - unless if we dont have mainGeo + if (SplitType == EHoudiniSplitType::InvisibleUCXCollider && bHasMainGeo) + continue; + } + else if (SplitType == EHoudiniSplitType::InvisibleSimpleCollider || SplitType == EHoudiniSplitType::RenderedSimpleCollider) + { + MainStaticMeshCTF = ECollisionTraceFlag::CTF_UseDefault; + // Get the part position if needed + UpdatePartPositionIfNeeded(); + + // Create the simple colliders and add them to the aggregate + if (!AddSimpleCollisionToAggregate(SplitGroupName, AggregateCollisions)) + { + // Failed to generate a convex collider + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%d %s] failed to create simple collider."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, SplitId, *SplitGroupName); + } + + // If the collider is not visible, stop here - unless if we dont have a main geo + if (SplitType == EHoudiniSplitType::InvisibleSimpleCollider && bHasMainGeo) + continue; + } + + // Try to find existing properties for this identifier + // First check the OutputObjects (for LODs and Normal geo the same FHoudiniOutputObject entry is used, so + // we check OutputObjects first to see if we have already created/updated it for the main mesh) + FHoudiniOutputObject* FoundOutputObject = OutputObjects.Find(OutputObjectIdentifier); + if (!FoundOutputObject) + FoundOutputObject = InputObjects.Find(OutputObjectIdentifier); + + // If we don't yet have package params for this object identifier, fetch and resolve attributes for the split + // and update the package params + TMap TempAttributes; + TMap TempTokens; + bool bCopyAttributesAndTokens = false; + if (!ObjectIdentifiersToPackageParams.Contains(OutputObjectIdentifier)) + { + // Get all the supported attributes from the HGPO + // For LOD / Normal mesh we use the MainIdentifier to read attributes (Normal or LOD0) since they all + // go into the same mesh/package + if ((SplitType == EHoudiniSplitType::Normal || SplitType == EHoudiniSplitType::LOD) && bHasMainIdentifier) + CopyAttributesFromHGPOForSplit(MainIdentifier, TempAttributes, TempTokens); + else + CopyAttributesFromHGPOForSplit(OutputObjectIdentifier, TempAttributes, TempTokens); + + // Resolve our final package params + FHoudiniAttributeResolver Resolver; + FHoudiniPackageParams FinalPackageParams; + FHoudiniEngineUtils::UpdatePackageParamsForTempOutputWithResolver( + InitialPackageParams, + IsValid(OuterComponent) ? OuterComponent->GetWorld() : nullptr, + OuterComponent, + TempAttributes, + TempTokens, + PackageParams, + Resolver); + + bCopyAttributesAndTokens = true; + ObjectIdentifiersToPackageParams.Emplace(OutputObjectIdentifier, PackageParams); + } + else + { + PackageParams = ObjectIdentifiersToPackageParams.FindChecked(OutputObjectIdentifier); + } + + // Try to find an existing SM from a previous cook + UStaticMesh* FoundStaticMesh = FindExistingStaticMesh(OutputObjectIdentifier); + + // Flag whether or not we need to rebuild the mesh + bool bRebuildStaticMesh = false; + if (HGPO.GeoInfo.bHasGeoChanged || HGPO.PartInfo.bHasChanged || ForceRebuild || !FoundStaticMesh || !FoundOutputObject) + bRebuildStaticMesh = true; + + // TODO: Handle materials + if (!bRebuildStaticMesh && !bMaterialHasChanged) + { + // We can simply reuse the found static mesh + OutputObjects.Add(OutputObjectIdentifier, *FoundOutputObject); + continue; + } + + // Prepare LOD Group data for this static mesh + FStaticMeshLODGroup LODGroup; + + bool bNewStaticMeshCreated = false; + if (!FoundStaticMesh) + { + // If we couldn't find a valid existing static mesh, create a new one + FoundStaticMesh = CreateNewUnrealStaticMesh(OutputObjectIdentifier.SplitIdentifier); + if (!IsValid(FoundStaticMesh)) + continue; + + bNewStaticMeshCreated = true; + + // Use the platform's default LODGroup policy + // TODO? Add setting for default LOD Group? + LODGroup = CurrentPlatform->GetStaticMeshLODSettings().GetLODGroup(NAME_None); + } + else + { + // Try to reuse the existing SM's LOD group instead of the default one +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + LODGroup = CurrentPlatform->GetStaticMeshLODSettings().GetLODGroup(FoundStaticMesh->GetLODGroup()); +#else + LODGroup = CurrentPlatform->GetStaticMeshLODSettings().GetLODGroup(FoundStaticMesh->LODGroup); +#endif + } + + if (SplitType == EHoudiniSplitType::Normal) + { + MainStaticMesh = FoundStaticMesh; + MainStaticMesh->ComplexCollisionMesh = nullptr; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + MainStaticMesh->SetCustomizedCollision(false); +#else + MainStaticMesh->bCustomizedCollision = false; +#endif + } + + if (!FoundOutputObject) + { + FHoudiniOutputObject NewOutputObject; + FoundOutputObject = &OutputObjects.Add(OutputObjectIdentifier, NewOutputObject); + } + FoundOutputObject->bProxyIsCurrent = false; + + // Update the attributes and tokens if this is the first split for this object identifier + if (bCopyAttributesAndTokens) + { + FoundOutputObject->CachedAttributes = MoveTemp(TempAttributes); + FoundOutputObject->CachedTokens = MoveTemp(TempTokens); + } + + // TODO: Needed? + // Free any RHI resources for existing mesh before we re-create in place. + FoundStaticMesh->PreEditChange(nullptr); + + // Check that the Static Mesh we found has the appropriate number of Source models/LODs + int32 NeededNumberOfLODs = FMath::Max(NumberOfLODs + (bHasMainGeo ? 1 : 0), LODGroup.GetDefaultNumLODs()); + + // LODs are only for the "main" mesh, not for complex colliders! + if (SplitType == EHoudiniSplitType::InvisibleComplexCollider || SplitType == EHoudiniSplitType::RenderedComplexCollider) + NeededNumberOfLODs = FMath::Max(1, LODGroup.GetDefaultNumLODs()); + + + if (FoundStaticMesh->GetNumSourceModels() != NeededNumberOfLODs) + { + while (FoundStaticMesh->GetNumSourceModels() < NeededNumberOfLODs) + FoundStaticMesh->AddSourceModel(); + + // We may have to remove excessive LOD levels + if (FoundStaticMesh->GetNumSourceModels() > NeededNumberOfLODs) + FoundStaticMesh->SetNumSourceModels(NeededNumberOfLODs); + + // Initialize their default reduction setting + for (int32 ModelLODIndex = 0; ModelLODIndex < NeededNumberOfLODs; ModelLODIndex++) + { + FoundStaticMesh->GetSourceModel(ModelLODIndex).ReductionSettings = LODGroup.GetDefaultSettings(ModelLODIndex); + } + FoundStaticMesh->SetLightMapResolution(LODGroup.GetDefaultLightMapResolution()); + } + + FoundOutputObject->DataLayers = FHoudiniDataLayerUtils::GetDataLayers(OutputObjectIdentifier.GeoId, OutputObjectIdentifier.PartId); + FoundOutputObject->HLODLayers = FHoudiniHLODLayerUtils::GetHLODLayers(OutputObjectIdentifier.GeoId, OutputObjectIdentifier.PartId); + + // By default, always work on the first source model, unless we're a LOD + int32 SrcModelIndex = 0; + int32 LODIndex = 0; + if (SplitType == EHoudiniSplitType::LOD) + { + for (auto& curSplit : AllSplitGroups) + { + EHoudiniSplitType CurrentSplitType = GetSplitTypeFromSplitName(curSplit); + if (CurrentSplitType == EHoudiniSplitType::LOD + || CurrentSplitType == EHoudiniSplitType::Normal) + { + LODIndex++; + } + + if (curSplit == SplitGroupName) + break; + } + + // Fix for the case where we don't have a main geo + if(!bHasMainGeo) + LODIndex--; + } + + // Grab the appropriate SourceModel + FStaticMeshSourceModel* SrcModel = (FoundStaticMesh->IsSourceModelValid(LODIndex)) ? &(FoundStaticMesh->GetSourceModel(LODIndex)) : nullptr; + if (!SrcModel) + { + HOUDINI_LOG_ERROR( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%d, %s] Could not access SourceModel for the LOD %d - skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, SplitId, *SplitGroupName, LODIndex); + continue; + } + + if (bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - PreMeshDescription in %f seconds."), FPlatformTime::Seconds() - tick); + tick = FPlatformTime::Seconds(); + } + + bool bHasNormal = false; + bool bHasTangents = false; + + // Load the existing mesh description if we don't need to rebuild the mesh + FMeshDescription* MeshDescription; + if (!bRebuildStaticMesh) + { + // We dont need to rebuild the mesh itself: + // the geometry hasn't changed, but the materials have. + // We can just reuse the old MeshDescription and reuse it. + MeshDescription = FoundStaticMesh->GetMeshDescription(LODIndex); + } + else + { + // Extract all the data needed for this split + // Start by initializing the MeshDescription for this LOD + MeshDescription = FoundStaticMesh->CreateMeshDescription(LODIndex); + FStaticMeshAttributes(*MeshDescription).Register(); + + // Mesh description uses material to create its PolygonGroups, + // so we first need to know how many different materials we have for this split + // and what vertices/indices belong to each material for remapping + + //--------------------------------------------------------------------------------------------------------------------- + // INDICES + //--------------------------------------------------------------------------------------------------------------------- + + // + // Because of the splits, we don't need to declare all the vertices in the Part, + // but only the one that are currently used by the split's faces. + // The indicesMapper array is used to map those indices from Part Vertices to Split Vertices. + // We also keep track of the needed vertices index to declare them easily afterwards. + // + + // SplitNeededVertices + // Array containing the (unique) part indices for the vertices that are needed for this split + TArray SplitNeededVertices; + + // IndicesMapper: + // Maps index values for all vertices in the Part: + // - Vertices unused by the split will be set to -1 + // - Used vertices will have their value set to the "NewIndex" so that IndicesMapper[ partIndex ] => splitIndex + TArray PartToSplitIndicesMapper; + PartToSplitIndicesMapper.SetNumUninitialized(SplitVertexList.Num()); + for (int32 n = 0; n < PartToSplitIndicesMapper.Num(); n++) + PartToSplitIndicesMapper[n] = -1; + + // SplitIndices + // Array of SplitIndices used to describe this split's polygons + TArray SplitIndices; + SplitIndices.SetNumZeroed(SplitVertexCount); + + int32 CurrentSplitIndex = 0; + int32 ValidVertexId = 0; + bool bHasInvalidFaceIndices = false; + for (int32 VertexIdx = 0; VertexIdx < SplitVertexList.Num(); VertexIdx += 3) + { + int32 WedgeCheck = SplitVertexList[VertexIdx + 0]; + if (WedgeCheck == -1) + continue; + + int32 WedgeIndices[3] = + { + SplitVertexList[VertexIdx + 0], + SplitVertexList[VertexIdx + 1], + SplitVertexList[VertexIdx + 2] + }; + + // Ensure the indices are valid + if (!PartToSplitIndicesMapper.IsValidIndex(WedgeIndices[0]) + || !PartToSplitIndicesMapper.IsValidIndex(WedgeIndices[1]) + || !PartToSplitIndicesMapper.IsValidIndex(WedgeIndices[2])) + { + // Invalid face index. + bHasInvalidFaceIndices = true; + continue; + } + + // Converting Old (Part) Indices to New (Split) Indices: + for (int32 i = 0; i < 3; i++) + { + if (PartToSplitIndicesMapper[WedgeIndices[i]] < 0) + { + // This part index has not yet been "converted" to a new split index + SplitNeededVertices.Add(WedgeIndices[i]); + PartToSplitIndicesMapper[WedgeIndices[i]] = CurrentSplitIndex; + CurrentSplitIndex++; + } + + // Replace the old part index with the new split index + WedgeIndices[i] = PartToSplitIndicesMapper[WedgeIndices[i]]; + } + + if (!SplitIndices.IsValidIndex(ValidVertexId + 2)) + break; + + // Flip wedge indices to fix the winding order. + SplitIndices[ValidVertexId + 0] = WedgeIndices[0]; + SplitIndices[ValidVertexId + 1] = WedgeIndices[2]; + SplitIndices[ValidVertexId + 2] = WedgeIndices[1]; + + ValidVertexId += 3; + } + if (bHasInvalidFaceIndices) + { + HOUDINI_LOG_MESSAGE( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%d %s] has some invalid face indices"), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, SplitId, *SplitGroupName); + } + + if (bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - Indices in %f seconds."), FPlatformTime::Seconds() - tick); + tick = FPlatformTime::Seconds(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // POSITIONS + //--------------------------------------------------------------------------------------------------------------------- + + // Extract position for this part + UpdatePartPositionIfNeeded(); + + // Transfer vertex positions: + // + // Because of the split, we're only interested in the needed vertices. + // Instead of declaring all the Positions, we'll only declare the vertices + // needed by the current split. + // + TVertexAttributesRef VertexPositions = + MeshDescription->VertexAttributes().GetAttributesRef(MeshAttribute::Vertex::Position); + + bool bHasInvalidPositionIndexData = false; + MeshDescription->ReserveNewVertices(SplitNeededVertices.Num()); + for (const int32& NeededVertexIndex : SplitNeededVertices) + { + // Create a new Vertex + FVertexID VertexID = MeshDescription->CreateVertex(); + if (PartPositions.IsValidIndex(NeededVertexIndex * 3 + 2)) + { + // We need to swap Z and Y coordinate here, and convert from m to cm. + VertexPositions[VertexID].X = PartPositions[NeededVertexIndex * 3 + 0] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + VertexPositions[VertexID].Y = PartPositions[NeededVertexIndex * 3 + 2] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + VertexPositions[VertexID].Z = PartPositions[NeededVertexIndex * 3 + 1] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + else + { + // Error when retrieving positions. + bHasInvalidPositionIndexData = true; + + continue; + } + } + + if (bHasInvalidPositionIndexData) + { + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%d %s] invalid position/index data ") + TEXT("- skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, SplitId, *SplitGroupName); + } + + if (bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - Positions in %f seconds."), FPlatformTime::Seconds() - tick); + tick = FPlatformTime::Seconds(); + } + + + //--------------------------------------------------------------------------------------------------------------------- + // MATERIALS + //--------------------------------------------------------------------------------------------------------------------- + + TArray& FoundStaticMaterials = FoundStaticMesh->GetStaticMaterials(); + + // // TODO: Check if still needed for MeshDescription + // // We need to reset the Static Mesh's materials once per SM: + // // so, for the first lod, or the main geo... + // if (!MeshMaterialsHaveBeenReset && (SplitType == EHoudiniSplitType::LOD || SplitType == EHoudiniSplitType::Normal)) + // { + // FoundStaticMaterials.Empty(); + // MeshMaterialsHaveBeenReset = true; + // } + // + // // .. or for each visible complex collider + // if (SplitType == EHoudiniSplitType::RenderedComplexCollider) + // FoundStaticMaterials.Empty(); + + // Clear the materials array of the mesh the first time we encounter it + if (!MapUnrealMaterialInterfaceToUnrealIndexPerMesh.Contains(FoundStaticMesh)) + { + FoundStaticMaterials.Empty(); + } + TMap, int32>& MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh = MapUnrealMaterialInterfaceToUnrealIndexPerMesh.FindOrAdd(FoundStaticMesh); + + // Get this split's faces + TArray& SplitGroupFaceIndices = AllSplitFaceIndices[SplitGroupName]; + // Array holding the materials needed for this split + //TArray SplitMaterials; + // Split Material indices per face, by default all faces are set to use the first Material + TArray SplitFaceMaterialIndices; + SplitFaceMaterialIndices.SetNumZeroed(SplitGroupFaceIndices.Num()); + + bool HasHoudiniMaterials = PartUniqueMaterialIds.Num() > 0; + bool HasMaterialOverrides = PartFaceMaterialOverrides.Num() > 0; + if (!HasHoudiniMaterials && !HasMaterialOverrides) + { + // We don't have any material override or houdini material + // we just need one polygon group using the default Houdini material. + UMaterialInterface* MaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get()); + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterial = ReplacementMaterials.Find( + FHoudiniMaterialIdentifier(HAPI_UNREAL_DEFAULT_MATERIAL_NAME, false, "")); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + FoundStaticMaterials.Empty(); + FoundStaticMaterials.Add(MaterialInterface); + + // TODO: ? Add default mat to the assignement map? + } + else if (HasHoudiniMaterials && !HasMaterialOverrides) + { + // We have Houdini Material but no overrides + if (bOnlyOneFaceMaterial || PartUniqueMaterialIds.Num() == 1) + { + // We have only one Houdini material. + // Use default Houdini material if no valid material is assigned to any of the faces. + UMaterialInterface* MaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get()); + + // Get id of this single material. + FString MaterialPathName = HAPI_UNREAL_DEFAULT_MATERIAL_NAME; + const bool bFoundHoudiniMaterial = FHoudiniMaterialTranslator::GetMaterialRelativePath(HGPO.AssetId, PartFaceMaterialIds[0], MaterialPathName); + const FHoudiniMaterialIdentifier DefaultMatIdentifier(MaterialPathName, bFoundHoudiniMaterial); + TObjectPtr * FoundMaterial = OutputAssignmentMaterials.Find(DefaultMatIdentifier); + if (FoundMaterial) + MaterialInterface = *FoundMaterial; + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr* ReplacementMaterial = ReplacementMaterials.Find(DefaultMatIdentifier); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + FoundStaticMaterials.Empty(); + FoundStaticMaterials.Add(MaterialInterface); + + // TODO: ? Add the mat to the assignement map? + } + else + { + // We have multiple houdini materials + // Get default Houdini material. + UMaterial* MaterialDefault = FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get(); + + // Reset material face assignments. + for (int32 FaceIdx = 0; FaceIdx < SplitGroupFaceIndices.Num(); ++FaceIdx) + { + int32 SplitFaceIndex = SplitGroupFaceIndices[FaceIdx]; + if (!PartFaceMaterialIds.IsValidIndex(SplitFaceIndex)) + continue; + + // Get material id for this face. + HAPI_NodeId MaterialId = PartFaceMaterialIds[SplitFaceIndex]; + + // See if we have already treated that material + TObjectPtr* FoundMaterialInterface = MapHoudiniMatIdToUnrealInterface.Find(MaterialId); + UMaterialInterface* MaterialInterface = nullptr; + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + if (MaterialInterface) + { + int32 const* FoundUnrealMatIndex = MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh.Find(MaterialInterface); + if (FoundUnrealMatIndex) + { + // This material has been mapped already, just assign the mat index + SplitFaceMaterialIndices[FaceIdx] = *FoundUnrealMatIndex; + continue; + } + } + else + { + MaterialInterface = Cast(MaterialDefault); + + FString MaterialPathName = HAPI_UNREAL_DEFAULT_MATERIAL_NAME; + const bool bFoundHoudiniMaterial = FHoudiniMaterialTranslator::GetMaterialRelativePath(HGPO.AssetId, MaterialId, MaterialPathName); + const FHoudiniMaterialIdentifier DefaultMatIdentifier(MaterialPathName, bFoundHoudiniMaterial); + TObjectPtr * FoundMaterial = OutputAssignmentMaterials.Find(DefaultMatIdentifier); + if (FoundMaterial) + MaterialInterface = *FoundMaterial; + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterial = ReplacementMaterials.Find(DefaultMatIdentifier); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + MapHoudiniMatIdToUnrealInterface.Add(MaterialId, MaterialInterface); + } + + if (MaterialInterface) + { + // Add the material to the Static mesh + //int32 UnrealMatIndex = SplitMaterials.Add(Material); + int32 UnrealMatIndex = FoundStaticMaterials.Add(MaterialInterface); + + // Map the houdini ID to the unreal one + MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh.Add(MaterialInterface, UnrealMatIndex); + + // Update the face index + SplitFaceMaterialIndices[FaceIdx] = UnrealMatIndex; + } + } + } + } + else + { + // Array used to avoid constantly attempting to load invalid materials + TArray InvalidMaterials; + + // If we have material overrides + for (int32 FaceIdx = 0; FaceIdx < SplitGroupFaceIndices.Num(); ++FaceIdx) + { + int32 SplitFaceIndex = SplitGroupFaceIndices[FaceIdx]; + + UMaterialInterface * MaterialInterface = nullptr; + int32 CurrentFaceMaterialIdx = -1; + int32 MatIndex = -1; + if (PartFaceMaterialOverrides.IsValidIndex(SplitFaceIndex)) + { + // For MeshDescription specifically, we need OutputAssignmentMaterials/MapHoudiniMatAttributesToUnrealInterface + // to have an entry for each index (distinguish between identical materials in different slots). + // This is so we have the correct size and create enough polygon groups later. + const FHoudiniMaterialInfo& MatInfo = PartFaceMaterialOverrides[SplitFaceIndex]; + const FHoudiniMaterialIdentifier MaterialIdentifier = MatInfo.MakeIdentifier(); + FString MaterialName = MatInfo.MaterialObjectPath; + ExtractMaterialIndex(MaterialName, MatIndex); + const TObjectPtr* FoundMaterialInterface = MapHoudiniMatAttributesToUnrealInterface.Find(MaterialIdentifier); + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + if (!MaterialInterface) + { + // Try to locate the corresponding material interface + + // Start by looking in our assignment map + FoundMaterialInterface = OutputAssignmentMaterials.Find(MaterialIdentifier); + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + if (!MaterialInterface && !MaterialName.IsEmpty() && !InvalidMaterials.Contains(MaterialName)) + { + // Only try to load a material if has a chance to be valid! + MaterialInterface = Cast< UMaterialInterface >( + StaticLoadObject(UMaterialInterface::StaticClass(), + nullptr, *MaterialName, nullptr, LOAD_NoWarn, nullptr)); + + if (!MaterialInterface) + InvalidMaterials.Add(MaterialName); + } + + if (MaterialInterface) + { + // We managed to load the UE4 material + // Make sure this material is in the assignments before replacing it. + OutputAssignmentMaterials.Add(MaterialIdentifier, MaterialInterface); + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr *ReplacementMaterialInterface = ReplacementMaterials.Find(MaterialIdentifier); + if (ReplacementMaterialInterface && *ReplacementMaterialInterface) + MaterialInterface = *ReplacementMaterialInterface; + + // Add this material to the map + MapHoudiniMatAttributesToUnrealInterface.Add(MaterialIdentifier, MaterialInterface); + } + } + + if (!MaterialInterface) + { + // The attribute Material or its replacement do not exist + // See if we can fallback to the Houdini material assigned on the face + + // Get the unreal material corresponding to this houdini one + HAPI_NodeId MaterialId = PartFaceMaterialIds[SplitFaceIndex]; + + // See if we have already treated that material + FoundMaterialInterface = MapHoudiniMatIdToUnrealInterface.Find(MaterialId); + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + if (!MaterialInterface) + { + // If everything else fails, we'll use the default material + MaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get()); + + // We need to add this material to the map + FString MaterialPathName = HAPI_UNREAL_DEFAULT_MATERIAL_NAME; + const bool bFoundHoudiniMaterial = FHoudiniMaterialTranslator::GetMaterialRelativePath(HGPO.AssetId, MaterialId, MaterialPathName); + const FHoudiniMaterialIdentifier DefaultMatIdentifier(MaterialPathName, bFoundHoudiniMaterial); + TObjectPtr * FoundMaterial = OutputAssignmentMaterials.Find(DefaultMatIdentifier); + if (FoundMaterial) + MaterialInterface = *FoundMaterial; + + if (MaterialInterface) + { + // Make sure this material is in the assignments before replacing it. + OutputAssignmentMaterials.Add(MaterialIdentifier, MaterialInterface); + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterialInterface = ReplacementMaterials.Find(DefaultMatIdentifier); + if (ReplacementMaterialInterface && *ReplacementMaterialInterface) + MaterialInterface = *ReplacementMaterialInterface; + + // Map the Houdini ID to the unreal one + MapHoudiniMatIdToUnrealInterface.Add(MaterialId, MaterialInterface); + } + } + } + } + + CurrentFaceMaterialIdx = GetFaceMaterialIndex(MaterialInterface, + MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh, + MatIndex, + FoundStaticMaterials); + + // Update the Face Material on the mesh + SplitFaceMaterialIndices[FaceIdx] = CurrentFaceMaterialIdx; + } + } + + // Create a Polygon Group for each material slot + TPolygonGroupAttributesRef PolygonGroupImportedMaterialSlotNames = + MeshDescription->PolygonGroupAttributes().GetAttributesRef(MeshAttribute::PolygonGroup::ImportedMaterialSlotName); + + // We must use the number of assignment materials found to reserve the number of material slots + // Don't use the SM's StaticMaterials here as we may not reserve enough polygon groups when adding more materials + // Create a polygon group for each material slot. + int32 NumberOfMaterials = OutputAssignmentMaterials.Num(); + if (NumberOfMaterials <= 0) + { + // No materials, create a polygon group for the default one + const FPolygonGroupID& PolygonGroupID = MeshDescription->CreatePolygonGroup(); + PolygonGroupImportedMaterialSlotNames[PolygonGroupID] = FName(HAPI_UNREAL_DEFAULT_MATERIAL_NAME); + } + else + { + MeshDescription->ReserveNewPolygonGroups(NumberOfMaterials); + //for (int32 MatIndex = 0; MatIndex < NumberOfMaterials; ++MatIndex) + for (auto& CurrentMatAssignement : OutputAssignmentMaterials) + { + const FPolygonGroupID& PolygonGroupID = MeshDescription->CreatePolygonGroup(); + PolygonGroupImportedMaterialSlotNames[PolygonGroupID] = + FName(CurrentMatAssignement.Value ? *(CurrentMatAssignement.Value->GetName()) : *(CurrentMatAssignement.Key.MaterialObjectPath)); + } + } + + if (bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - Materials in %f seconds."), FPlatformTime::Seconds() - tick); + tick = FPlatformTime::Seconds(); + } + + // + // VERTEX INSTANCE ATTRIBUTES + // NORMALS, TANGENTS, COLORS, UVS, Alpha + // + + // Extract the normals + UpdatePartNormalsIfNeeded(); + // Get the normals for this split + TArray SplitNormals; + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoNormals, PartNormals, SplitNormals); + + TVertexInstanceAttributesRef VertexInstanceNormals = MeshDescription->VertexInstanceAttributes().GetAttributesRef(MeshAttribute::VertexInstance::Normal); + + // No need to read the tangents if we want unreal to recompute them after + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + bool bReadTangents = HoudiniRuntimeSettings ? HoudiniRuntimeSettings->RecomputeTangentsFlag != EHoudiniRuntimeSettingsRecomputeFlag::HRSRF_Always : true; + + // Extract the tangents + TArray SplitTangentU; + TArray SplitTangentV; + if (bReadTangents) + { + // Extract this part's Tangents if needed + UpdatePartTangentsIfNeeded(); + + // Get the Tangents for this split + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoTangentU, PartTangentU, SplitTangentU); + + // Get the binormals for this split + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoTangentV, PartTangentV, SplitTangentV); + + // We need to manually generate tangents if: + // - we have normals but dont have tangentu or tangentv attributes + // - we have not specified that we wanted unreal to generate them + int32 NormalCount = SplitNormals.Num(); + bool bGenerateTangents = (NormalCount > 0) && (SplitTangentU.Num() <= 0 || SplitTangentV.Num() <= 0); + // Check that the number of tangents read matches the number of normals + if (SplitTangentU.Num() != NormalCount || SplitTangentV.Num() != NormalCount) + bGenerateTangents = true; + + if (bGenerateTangents && (HoudiniRuntimeSettings->RecomputeTangentsFlag == EHoudiniRuntimeSettingsRecomputeFlag::HRSRF_Always)) + { + // No need to generate tangents if we want unreal to recompute them after + bGenerateTangents = false; + } + + // Generate the tangents if needed + if (bGenerateTangents) + { + SplitTangentU.SetNumZeroed(NormalCount); + SplitTangentV.SetNumZeroed(NormalCount); + for (int32 Idx = 0; Idx + 2 < NormalCount; Idx += 3) + { + FVector3f TangentZ; + TangentZ.X = SplitNormals[Idx + 0]; + TangentZ.Y = SplitNormals[Idx + 2]; + TangentZ.Z = SplitNormals[Idx + 1]; + + FVector3f TangentX, TangentY; + TangentZ.FindBestAxisVectors(TangentX, TangentY); + + SplitTangentU[Idx + 0] = TangentX.X; + SplitTangentU[Idx + 2] = TangentX.Y; + SplitTangentU[Idx + 1] = TangentX.Z; + + SplitTangentV[Idx + 0] = TangentY.X; + SplitTangentV[Idx + 2] = TangentY.Y; + SplitTangentV[Idx + 1] = TangentY.Z; + } + } + } + TVertexInstanceAttributesRef VertexInstanceTangents = + MeshDescription->VertexInstanceAttributes().GetAttributesRef(MeshAttribute::VertexInstance::Tangent); + TVertexInstanceAttributesRef VertexInstanceBinormalSigns = + MeshDescription->VertexInstanceAttributes().GetAttributesRef(MeshAttribute::VertexInstance::BinormalSign); + + // Extract the color values + UpdatePartColorsIfNeeded(); + // Get the colors values for this split + TArray SplitColors; + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoColors, PartColors, SplitColors); + + // Extract the alpha values + UpdatePartAlphasIfNeeded(); + // Get the colors values for this split + TArray SplitAlphas; + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoAlpha, PartAlphas, SplitAlphas); + TVertexInstanceAttributesRef VertexInstanceColors = + MeshDescription->VertexInstanceAttributes().GetAttributesRef(MeshAttribute::VertexInstance::Color); + + // Extract UVs + UpdatePartUVSetsIfNeeded(true); + // See if we need to transfer uv point attributes to vertex attributes. + int32 UVSetCount = PartUVSets.Num(); + TArray> SplitUVSets; + SplitUVSets.SetNum(UVSetCount); + for (int32 TexCoordIdx = 0; TexCoordIdx < UVSetCount; TexCoordIdx++) + { + FHoudiniMeshTranslator::TransferPartAttributesToSplit( + SplitVertexList, AttribInfoUVSets[TexCoordIdx], PartUVSets[TexCoordIdx], SplitUVSets[TexCoordIdx]); + } + TVertexInstanceAttributesRef VertexInstanceUVs = + MeshDescription->VertexInstanceAttributes().GetAttributesRef(MeshAttribute::VertexInstance::TextureCoordinate); + VertexInstanceUVs.SetNumChannels(UVSetCount); + + if (bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - VertexAttr extracted in %f seconds."), FPlatformTime::Seconds() - tick); + tick = FPlatformTime::Seconds(); + } + + // Allocate space for the vertex instances and polygons + MeshDescription->ReserveNewVertexInstances(SplitIndices.Num()); + MeshDescription->ReserveNewPolygons(SplitIndices.Num() / 3); + //Approximately 2.5 edges per polygons + MeshDescription->ReserveNewEdges(SplitIndices.Num() * 2.5f / 3); + + bHasNormal = SplitNormals.Num() > 0; + bHasTangents = SplitTangentU.Num() > 0 && SplitTangentV.Num() > 0; + bool bHasRGB = SplitColors.Num() > 0; + bool bHasRGBA = bHasRGB && AttribInfoColors.tupleSize == 4; + bool bHasAlpha = SplitAlphas.Num() > 0; + + // Bug #147854 + // Use SplitUVs here - as that where we'll read UVs from + TArray HasUVSets; + HasUVSets.SetNumZeroed(SplitUVSets.Num()); + for (int32 Idx = 0; Idx < SplitUVSets.Num(); Idx++) + HasUVSets[Idx] = SplitUVSets[Idx].Num() > 0; + + uint32 FaceCount = SplitIndices.Num() / 3; + for (uint32 FaceIndex = 0; FaceIndex < FaceCount; FaceIndex++) + { + TArray FaceVertexInstanceIDs; + FaceVertexInstanceIDs.SetNum(3); + + // Ignore degenerate triangles + FVertexID VertexIDs[3]; + for (int32 Corner = 0; Corner < 3; ++Corner) + { + VertexIDs[Corner] = FVertexID(SplitIndices[(FaceIndex * 3) + Corner]); + } + if (VertexIDs[0] == VertexIDs[1] || VertexIDs[0] == VertexIDs[2] || VertexIDs[1] == VertexIDs[2]) + continue; + + //FVertexID FaceVertexIDs[3]; + for (int32 Corner = 0; Corner < 3; Corner++) + { + uint32 SplitIndex = (FaceIndex * 3) + Corner; + uint32 SplitVertexIndex = SplitIndices[SplitIndex]; + const FVertexInstanceID& VertexInstanceID = MeshDescription->CreateVertexInstance(FVertexID(SplitVertexIndex)); + + // Fix the winding order by updating the SplitIndex (invert corner 1 and 2) + // instead of going 0 1 2 go 0 2 1 + // TODO; this slows down StaticMesh->Build() considerably! + Corner == 1 ? SplitIndex++ : Corner == 2 ? SplitIndex-- : SplitIndex; + + const uint32 SplitVertexIndex_X = SplitIndex * 3 + 0; + const uint32 SplitVertexIndex_Y = SplitIndex * 3 + 2; + const uint32 SplitVertexIndex_Z = SplitIndex * 3 + 1; + // Normals + if (bHasNormal) + { + // We need to swap Z and Y coordinate here, and convert from m to cm. + VertexInstanceNormals[VertexInstanceID].X = SplitNormals[SplitVertexIndex_X]; + VertexInstanceNormals[VertexInstanceID].Y = SplitNormals[SplitVertexIndex_Y]; + VertexInstanceNormals[VertexInstanceID].Z = SplitNormals[SplitVertexIndex_Z]; + } + + // Tangents and binormals + if (bHasTangents) + { + // We need to swap Z and Y coordinate here, and convert from m to cm. + VertexInstanceTangents[VertexInstanceID].X = SplitTangentU[SplitVertexIndex_X]; + VertexInstanceTangents[VertexInstanceID].Y = SplitTangentU[SplitVertexIndex_Y]; + VertexInstanceTangents[VertexInstanceID].Z = SplitTangentU[SplitVertexIndex_Z]; + + FVector3f TangentY; + TangentY.X = SplitTangentV[SplitVertexIndex_X]; + TangentY.Y = SplitTangentV[SplitVertexIndex_Y]; + TangentY.Z = SplitTangentV[SplitVertexIndex_Z]; + + VertexInstanceBinormalSigns[VertexInstanceID] = GetBasisDeterminantSign( + (FVector)VertexInstanceTangents[VertexInstanceID].GetSafeNormal(), + (FVector)TangentY.GetSafeNormal(), + (FVector)VertexInstanceNormals[VertexInstanceID].GetSafeNormal()); + } + + // Color + FLinearColor Color = FLinearColor::White; + if (bHasRGB) + { + Color.R = FMath::Clamp( + SplitColors[SplitIndex * AttribInfoColors.tupleSize + 0], 0.0f, 1.0f); + Color.G = FMath::Clamp( + SplitColors[SplitIndex * AttribInfoColors.tupleSize + 1], 0.0f, 1.0f); + Color.B = FMath::Clamp( + SplitColors[SplitIndex * AttribInfoColors.tupleSize + 2], 0.0f, 1.0f); + } + // Alpha + if (bHasAlpha) + { + Color.A = FMath::Clamp(SplitAlphas[SplitIndex], 0.0f, 1.0f); + } + else if (bHasRGBA) + { + Color.A = FMath::Clamp(SplitColors[SplitIndex * AttribInfoColors.tupleSize + 3], 0.0f, 1.0f); + } + + if (bIsGammaCorrectionDisabled) + { + // Mesh Description colors are always gamma corrected by Unreal. So we have to reverse the correction + // if this flag is enabled. + Color = FLinearColor::FromSRGBColor(Color.ToFColor(false)); + } + FVector4f VertexColor = FVector4f(Color); + VertexInstanceColors[VertexInstanceID] = VertexColor; + + // UVs + for (int32 UVIndex = 0; UVIndex < SplitUVSets.Num(); UVIndex++) + { + if (HasUVSets[UVIndex]) + { + // We need to flip V coordinate when it's coming from HAPI. + FVector2f CurrentUV; + CurrentUV.X = SplitUVSets[UVIndex][SplitIndex * 2 + 0]; + CurrentUV.Y = 1.0f - SplitUVSets[UVIndex][SplitIndex * 2 + 1]; + + VertexInstanceUVs.Set(VertexInstanceID, UVIndex, CurrentUV); + } + } + + FaceVertexInstanceIDs[Corner] = VertexInstanceID; + } + + const FPolygonGroupID PolygonGroupID(SplitFaceMaterialIndices[FaceIndex]); + + // Insert a triangle into the mesh + MeshDescription->CreateTriangle(PolygonGroupID, FaceVertexInstanceIDs); + } + + if (bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - VertexAttr filled in %f seconds."), FPlatformTime::Seconds() - tick); + tick = FPlatformTime::Seconds(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // FACE SMOOTHING + //--------------------------------------------------------------------------------------------------------------------- + + // Extract this part's FaceSmoothing values if needed + UpdatePartFaceSmoothingIfNeeded(); + + // Get the FaceSmoothing values for this split + TArray SplitFaceSmoothingMasks; + FHoudiniMeshTranslator::TransferPartAttributesToSplit( + SplitVertexList, AttribInfoFaceSmoothingMasks, PartFaceSmoothingMasks, SplitFaceSmoothingMasks); + + // FaceSmoothing masks must be initialized even if we don't have a value from Houdini! + // TODO: Expose the default FaceSmoothing value + // 0 will make hard face + TArray FaceSmoothingMasks; + FaceSmoothingMasks.SetNumUninitialized(SplitVertexCount / 3); + for (int32 n = 0; n < FaceSmoothingMasks.Num(); n++) + FaceSmoothingMasks[n] = DefaultMeshSmoothing; + + // Check that the number of face smoothing values we retrieved is correct + int32 WedgeFaceSmoothCount = SplitFaceSmoothingMasks.Num() / 3; + if (SplitFaceSmoothingMasks.Num() != 0 && !SplitFaceSmoothingMasks.IsValidIndex((WedgeFaceSmoothCount - 1) * 3 + 2)) + { + // Ignore our face smoothing values + WedgeFaceSmoothCount = 0; + HOUDINI_LOG_WARNING(TEXT("Invalid face smoothing mask count detected - Skipping them.")); + } + + // Transfer the face smoothing masks to the raw mesh if we have any + for (int32 WedgeFaceSmoothIdx = 0; WedgeFaceSmoothIdx < WedgeFaceSmoothCount; WedgeFaceSmoothIdx += 3) + { + FaceSmoothingMasks[WedgeFaceSmoothIdx] = SplitFaceSmoothingMasks[WedgeFaceSmoothIdx * 3]; + } + + // TODO + // Check + FStaticMeshOperations::ConvertSmoothGroupToHardEdges(FaceSmoothingMasks, *MeshDescription); + + if (bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - FaceSoothing filled in %f seconds."), FPlatformTime::Seconds() - tick); + tick = FPlatformTime::Seconds(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // LIGHTMAP RESOLUTION + //--------------------------------------------------------------------------------------------------------------------- + // Extract this part's LightmapResolution values if needed + UpdatePartLightmapResolutionsIfNeeded(); + + // make sure the mesh has a new lighting guid + FoundStaticMesh->SetLightingGuid(FGuid::NewGuid()); + } + + // Update the Build Settings using the default setting values + UpdateMeshBuildSettings( + SrcModel->BuildSettings, + bHasNormal, + bHasTangents, + PartUVSets.Num() > 0); + + // Set the lightmap Coordinate Index + // If we have more than one UV set, the 2nd valid set is used for lightmaps by convention + FoundStaticMesh->SetLightMapCoordinateIndex(PartUVSets.Num() > 1 ? 1 : 0); + + // Check for a lightmapa resolution override + int32 LightMapResolutionOverride = -1; + if ( PartLightMapResolutions.Num() > 0) + LightMapResolutionOverride = PartLightMapResolutions[0]; + + if (LightMapResolutionOverride > 0) + FoundStaticMesh->SetLightMapResolution(LightMapResolutionOverride); + else + FoundStaticMesh->SetLightMapResolution(64); + + // TODO: + // Turnoff bGenerateLightmapUVs if lightmap uv sets has bad uvs ? + + // RAW MESH CHECKS + + // Check if the mesh has at least one triangle, if not, log a message + if (MeshDescription->Triangles().Num() == 0) + { + HOUDINI_LOG_WARNING( + TEXT("[CreateStaticMesh_MeshDescription]: 0 valid triangles in StaticMesh data for %s LOD %i! Please check the log."), + *FoundStaticMesh->GetName(), LODIndex); + } + + // Store the new MeshDescription + FoundStaticMesh->CommitMeshDescription(LODIndex); + //Set the Imported version before calling the build +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + FoundStaticMesh->SetImportVersion(EImportStaticMeshVersion::LastVersion); +#else + FoundStaticMesh->ImportVersion = EImportStaticMeshVersion::LastVersion; +#endif + + // LOD Screensize + // default values has already been set, see if we have any attribute override for this + float screensize = GetLODSCreensizeForSplit(SplitGroupName); + if (screensize >= 0.0f) + { + // Only apply the LOD screensize if it's valid + SrcModel->ScreenSize = screensize; + //FoundStaticMesh->GetSourceModel(LodIndex).ScreenSize = screensize; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + FoundStaticMesh->SetAutoComputeLODScreenSize(false); +#else + FoundStaticMesh->bAutoComputeLODScreenSize = false; +#endif + } + + UpdateStaticMeshNaniteSettings(HGPO.GeoId, HGPO.PartId, OutputObjectIdentifier.PrimitiveIndex, FoundStaticMesh); + + // SET STATIC MESH GENERATION PARAM + // HANDLE COLLIDERS + // REMOVE OLD COLLIDERS + // CUSTOM BAKE NAME OVERRIDE + + // UPDATE UPROPERTY ATTRIBS + // Update property attributes on the source model + TArray PropertyAttributes; + if (FHoudiniEngineUtils::GetGenericPropertiesAttributes( + HGPO.GeoId, + HGPO.PartId, + true, + OutputObjectIdentifier.PrimitiveIndex, + INDEX_NONE, + OutputObjectIdentifier.PointIndex, + PropertyAttributes)) + { + auto FindPropertyOnSourceModelLamba = [LODIndex](UObject* const InObject, const FString& InPropertyName, bool& bOutSkipDefaultIfPropertyNotFound, FEditPropertyChain& InPropertyChain, FProperty*& OutFoundProperty, UObject*& OutFoundPropertyObject, void*& OutContainer) + { + if (!IsValid(InObject)) + return false; + + UStaticMesh* const SM = Cast(InObject); + if (!IsValid(SM)) + return false; + + return TryToFindPropertyOnSourceModel( + SM, LODIndex, InPropertyName, InPropertyChain, bOutSkipDefaultIfPropertyNotFound, OutFoundProperty, OutFoundPropertyObject, OutContainer); + }; + + // Defer post edit change calls until after all property values have been set, since the static mesh + // build function is called from PostEditChangeProperty. + constexpr bool bDeferPostEditChangePropertyCalls = true; + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes( + FoundStaticMesh, PropertyAttributes, 0, bDeferPostEditChangePropertyCalls, FindPropertyOnSourceModelLamba); + } + + + // Notify that we created a new Static Mesh if needed + if(bNewStaticMeshCreated) + FAssetRegistryModule::AssetCreated(FoundStaticMesh); + + // Add the Static mesh to the output maps and the build map if we haven't already + if (FoundOutputObject) + { + FoundOutputObject->OutputObject = FoundStaticMesh; + FoundOutputObject->bProxyIsCurrent = false; + FoundOutputObject->bIsImplicit = false; + OutputObjects.FindOrAdd(OutputObjectIdentifier, *FoundOutputObject); + } + + StaticMeshToBuild.FindOrAdd(OutputObjectIdentifier, FoundStaticMesh); + + if (bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - Finished Split in %f seconds."), FPlatformTime::Seconds() - tick); + tick = FPlatformTime::Seconds(); + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - Total Split time: %f seconds."), tick - split_tick); + } + } + + // Look if we only have colliders + // If we do, we'll allow attaching sockets to the collider meshes + bool bCollidersOnly = true; + for (auto& Current : StaticMeshToBuild) + { + EHoudiniSplitType CurrentSplitType = GetSplitTypeFromSplitName(Current.Key.SplitIdentifier); + if (CurrentSplitType == EHoudiniSplitType::LOD || CurrentSplitType == EHoudiniSplitType::Normal) + { + bCollidersOnly = false; + break; + } + } + + FHoudiniScopedGlobalSilence ScopedGlobalSilence; + for (auto& Current : StaticMeshToBuild) + { + tick = FPlatformTime::Seconds(); + + UStaticMesh* SM = Current.Value; + if (!IsValid(SM)) + continue; + + const FHoudiniOutputObjectIdentifier& CurrentObjId = Current.Key; + + UBodySetup * BodySetup = SM->GetBodySetup(); + if (!BodySetup) + { + SM->CreateBodySetup(); + BodySetup = SM->GetBodySetup(); + } + + // Set the default Static Mesh Generation Properties + if (IsValid(BodySetup)) + { + // Set flag whether physics triangle mesh will use double sided faces when doing scene queries. + BodySetup->bDoubleSidedGeometry = StaticMeshGenerationProperties.bGeneratedDoubleSidedGeometry; + // Assign physical material for simple collision. + BodySetup->PhysMaterial = StaticMeshGenerationProperties.GeneratedPhysMaterial; + // Body Instance Properties + BodySetup->DefaultInstance.CopyBodyInstancePropertiesFrom(&StaticMeshGenerationProperties.DefaultBodyInstance); + } + + EHoudiniSplitType SplitType = GetSplitTypeFromSplitName(CurrentObjId.SplitIdentifier); + + // Handle the Static Mesh's colliders + if (IsValid(BodySetup)) + { + // Make sure rendering is done - so we are not changing data being used by collision drawing. + FlushRenderingCommands(); + + // Clean up old colliders from a previous cook + BodySetup->Modify(); + BodySetup->RemoveSimpleCollision(); + // Create new GUID + BodySetup->InvalidatePhysicsData(); + + FKAggregateGeom* CurrentAggColl = AllAggregateCollisions.Find(CurrentObjId); + if (CurrentAggColl && CurrentAggColl->GetElementCount() > 0) + { + BodySetup->AddCollisionFrom(*CurrentAggColl); + BodySetup->CollisionTraceFlag = ECollisionTraceFlag::CTF_UseDefault; + } + + // Set physical material if present + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + + TArray AttributeValues; + + FHoudiniHapiAccessor Accessor(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_SIMPLE_PHYSICAL_MATERIAL); + Accessor.GetInfo(AttributeInfo, HAPI_ATTROWNER_PRIM); + AttributeInfo.tupleSize = 1; + bool bSuccess = Accessor.GetAttributeData(AttributeInfo, AttributeValues); + + if (bSuccess && AttributeValues.Num() > 0) + { + // Fetch the physics material name based off the first primitve attribute + auto& MaterialName = AttributeValues[0]; + if (!MaterialName.IsEmpty() && MaterialName != "None") + { + BodySetup->PhysMaterial = LoadObject(nullptr, *MaterialName, nullptr, LOAD_NoWarn, nullptr); + if (!BodySetup->PhysMaterial) + { + HOUDINI_LOG_HELPER(Error, TEXT("Physical Material not found: %s."), *MaterialName); + } + } + } + + // Moved RefreshCollisionChange to after the SM->Build call + // RefreshCollisionChange(*SM); + // SM->bCustomizedCollision = true; + + // See if we need to enable collisions on the whole mesh + if (SplitType == EHoudiniSplitType::InvisibleComplexCollider || SplitType == EHoudiniSplitType::RenderedComplexCollider) + { + // Complex collider, enable collisions for this static mesh. + BodySetup->CollisionTraceFlag = ECollisionTraceFlag::CTF_UseComplexAsSimple; + ApplyComplexColliderHelper( + MainStaticMesh, + SM, + SplitType, + bAssignedCustomCollisionMesh, + OutputObjects.Find(CurrentObjId)); + } + else + { + // TODO + // if the LODForCollision uproperty attribute is set, we need to activate complex collision + // on the static mesh for that lod to be picked up properly as a collider + if ( FHoudiniEngineUtils::HapiCheckAttributeExists( HGPO.GeoId, HGPO.PartId, + "unreal_uproperty_LODForCollision", HAPI_ATTROWNER_DETAIL)) + { + BodySetup->CollisionTraceFlag = ECollisionTraceFlag::CTF_UseComplexAsSimple; + } + } + } + + // Add the Sockets to the StaticMesh + // We only add them to the main geo, or to the colliders if we only generate colliders + bool bAddSocket = SplitType == EHoudiniSplitType::Normal ? true : bCollidersOnly ? true : false; + if (bAddSocket) + { + if (!FHoudiniEngineUtils::AddMeshSocketsToStaticMesh(SM, HGPO.AllMeshSockets, true)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to import sockets for StaticMesh %s."), *(SM->GetName())); + } + } + + if (MainStaticMesh) + { + UBodySetup* MainBodySetup = MainStaticMesh->GetBodySetup(); + if (!IsValid(MainBodySetup)) + { + MainStaticMesh->CreateBodySetup(); + MainBodySetup = MainStaticMesh->GetBodySetup(); + } + + check(MainBodySetup); + // Set the main static mesh to whatever the final CTF should be. + MainBodySetup->CollisionTraceFlag = MainStaticMeshCTF; + } + + // Update property attributes on the SM + TArray PropertyAttributes; + if (FHoudiniEngineUtils::GetGenericPropertiesAttributes( + CurrentObjId.GeoId, + CurrentObjId.PartId, + true, + CurrentObjId.PrimitiveIndex, + INDEX_NONE, + CurrentObjId.PointIndex, + PropertyAttributes)) + { + // Defer post edit change calls until after all property values have been set, since the static mesh + // build function is called from PostEditChangeProperty. + constexpr bool bDeferPostEditChangePropertyCalls = true; + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes(SM, PropertyAttributes, 0, bDeferPostEditChangePropertyCalls); + } + + if (bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - Pre SM->Build() in %f seconds."), FPlatformTime::Seconds() - tick); + tick = FPlatformTime::Seconds(); + } + + // BUILD the Static Mesh + // bSilent doesnt add the Build Errors... + double build_start = FPlatformTime::Seconds(); + TArray SMBuildErrors; + SM->Build(true, &SMBuildErrors); + + if (bDoTiming) + { + tick = FPlatformTime::Seconds(); + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - StaticMesh->Build() executed in %f seconds."), tick - build_start); + } + + // This replaces the call to RefreshCollision below, but without CreateNavCollision + // as it is already called by UStaticMesh::PostBuildInternal as part of the ::Build call, + // and can be expensive depending on the vert/poly count of the mesh + // RefreshCollisionChange(*SM); + { + for (FThreadSafeObjectIterator Iter(UStaticMeshComponent::StaticClass()); Iter; ++Iter) + { + UStaticMeshComponent* StaticMeshComponent = Cast(*Iter); + if (StaticMeshComponent->GetStaticMesh() == SM) + { + // it needs to recreate IF it already has been created + if (StaticMeshComponent->IsPhysicsStateCreated()) + { + StaticMeshComponent->RecreatePhysicsState(); + } + } + } + + FEditorSupportDelegates::RedrawAllViewports.Broadcast(); + } + + SM->GetOnMeshChanged().Broadcast(); + + UPackage* MeshPackage = SM->GetOutermost(); + if (IsValid(MeshPackage)) + { + MeshPackage->MarkPackageDirty(); + } + + if (bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - Post SM->Build() in %f seconds."), FPlatformTime::Seconds() - tick); + } + } + + // !!! No need to call InvalidatePhysicsData / CreatePhysicsMeshes / GetNavCollision()->Setup + // Here as it has already been handled by the StaticMesh Build call + + double time_end = FPlatformTime::Seconds(); + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() executed in %f seconds."), time_end - time_start); + + return true; +} + +bool +FHoudiniMeshTranslator::CreateHoudiniStaticMesh() +{ + /////////////////////////////////////////////////////////////////////// + // THIS FUNCTION IS DEPRECATED AND WILL REMOVED IN THE NEXT RELEASE. + /////////////////////////////////////////////////////////////////////// + + // Time limit for processing + + bDoTiming = CVarHoudiniEngineMeshBuildTimer.GetValueOnAnyThread() != 0.0; + + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh); + + const double time_start = FPlatformTime::Seconds(); + + // Keep a copy of the initial package params, since PackageParams is modified in place when resolving attributes + FHoudiniPackageParams InitialPackageParams = PackageParams; + + bool bIsGammaCorrectionDisabled = IsGammaCorrectionDisabled(HGPO.GeoId, HGPO.PartId); + + // Start by updating the vertex list + if (!UpdatePartVertexList()) + return false; + + // Sort the split groups + SortSplitGroups(); + + // Handles the split groups found in the part + // and builds the corresponding faces and indices arrays + if (!UpdateSplitsFacesAndIndices()) + return true; + + // Resets the containers used for the raw data extraction. + ResetPartCache(); + + // Determine if there is "main" geo, if not we'll use the first LOD + // as main geo + bool bHasMainGeo = false; + for (auto& curSplit : AllSplitGroups) + { + if (GetSplitTypeFromSplitName(curSplit) == EHoudiniSplitType::Normal) + { + bHasMainGeo = true; + break; + } + } + + // Update the part's material's IDS and info now + //UpdatePartFaceMaterialsIfNeeded(); + CreateNeededMaterials(); + + // Check if the materials were updated + bool bMaterialHasChanged = false; + for (const auto& MatInfo : PartUniqueMaterialInfos) + { + if (MatInfo.hasChanged) + { + bMaterialHasChanged = true; + break; + } + } + + // Map of Houdini Material IDs to Unreal Material Interface + TMap> MapHoudiniMatIdToUnrealInterface; + // Map of Houdini Material Attributes to Unreal Material Interface + TMap> MapHoudiniMatAttributesToUnrealInterface; + // Map of Unreal Material Interface to Unreal Material Index, per visible mesh + TMap, int32>> MapUnrealMaterialInterfaceToUnrealIndexPerMesh; + + // bool MeshMaterialsHaveBeenReset = false; + + double tick = FPlatformTime::Seconds(); + if(bDoTiming) + HOUDINI_LOG_MESSAGE(TEXT("CreateHoudiniStaticMesh() - Pre Split-Loop in %f seconds."), tick - time_start); + + // Map of object identifiers to package params + TMap ObjectIdentifiersToPackageParams; + + // Iterate through all detected split groups we care about and split geometry. + bool bMainGeoOrFirstLODFound = false; + for (int32 SplitId = 0; SplitId < AllSplitGroups.Num(); SplitId++) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Per Split); + + // Get split group name + const FString& SplitGroupName = AllSplitGroups[SplitId]; + + // Get the current split type + EHoudiniSplitType SplitType = GetSplitTypeFromSplitName(SplitGroupName); + if (SplitType == EHoudiniSplitType::Invalid) + { + // Invalid split, skip + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%d %s] unknown split type.") + TEXT("- skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, SplitId, *SplitGroupName); + continue; + } + + // We are only interested in the Normal/main geo and visible colliders + if (SplitType != EHoudiniSplitType::Normal && + SplitType != EHoudiniSplitType::LOD && + SplitType != EHoudiniSplitType::RenderedComplexCollider && + SplitType != EHoudiniSplitType::RenderedSimpleCollider && + SplitType != EHoudiniSplitType::RenderedUCXCollider) + { + // Only create the invisible colliders if we don't have other splits + if(AllSplitGroups.Num() > 1) + continue; + } + + // We only use LOD if there is no Normal geo + if (SplitType == EHoudiniSplitType::Normal) + { + bMainGeoOrFirstLODFound = true; + HOUDINI_LOG_MESSAGE(TEXT("Found Normal geo for mesh.")); + } + else if (SplitType == EHoudiniSplitType::LOD) + { + if (bHasMainGeo) + { + HOUDINI_LOG_MESSAGE(TEXT("Skipping LOD since the mesh has Normal geo.")); + continue; + } + else if (bMainGeoOrFirstLODFound) + { + HOUDINI_LOG_MESSAGE(TEXT("Skipping LOD since we have already processed the first LOD.")); + continue; + } + else + { + bMainGeoOrFirstLODFound = true; + HOUDINI_LOG_MESSAGE(TEXT("Mesh does not have Normal geo, found first LOD.")); + } + } + + // Get the vertex indices for this group + TArray& SplitVertexList = AllSplitVertexLists[SplitGroupName]; + + // Get valid count of vertex indices for this split. + const int32& SplitVertexCount = AllSplitVertexCounts[SplitGroupName]; + + // Make sure we have a valid vertex count for this split + if (SplitVertexCount % 3 != 0 || SplitVertexList.Num() % 3 != 0) + { + // Invalid vertex count, skip this split or we'd crash trying to create a mesh for it. + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%d %s] invalid vertex count.") + TEXT("- skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, SplitId, *SplitGroupName); + + continue; + } + + // Get the output identifer for this split + FHoudiniOutputObjectIdentifier OutputObjectIdentifier = MakeOutputObjectIdentifier(SplitGroupName, SplitType); + + // Try to find existing properties for this identifier + FHoudiniOutputObject* FoundOutputObject = InputObjects.Find(OutputObjectIdentifier); + + // If we don't yet have package params for this object identifier, fetch and resolve attributes for the split + // and update the package params + TMap TempAttributes; + TMap TempTokens; + bool bCopyAttributesAndTokens = false; + if (!ObjectIdentifiersToPackageParams.Contains(OutputObjectIdentifier)) + { + // Get all the supported attributes from the HGPO + CopyAttributesFromHGPOForSplit(OutputObjectIdentifier, TempAttributes, TempTokens); + + // Resolve our final package params + FHoudiniAttributeResolver Resolver; + FHoudiniPackageParams FinalPackageParams; + FHoudiniEngineUtils::UpdatePackageParamsForTempOutputWithResolver( + InitialPackageParams, + IsValid(OuterComponent) ? OuterComponent->GetWorld() : nullptr, + OuterComponent, + TempAttributes, + TempTokens, + PackageParams, + Resolver); + + bCopyAttributesAndTokens = true; + ObjectIdentifiersToPackageParams.Emplace(OutputObjectIdentifier, PackageParams); + } + else + { + PackageParams = ObjectIdentifiersToPackageParams.FindChecked(OutputObjectIdentifier); + } + + // Try to find an existing DM from a previous cook + UHoudiniStaticMesh* FoundStaticMesh = FindExistingHoudiniStaticMesh(OutputObjectIdentifier); + + // Flag whether or not we need to rebuild the mesh + bool bRebuildStaticMesh = false; + if (HGPO.GeoInfo.bHasGeoChanged || HGPO.PartInfo.bHasChanged || ForceRebuild || !FoundStaticMesh || !FoundOutputObject) + bRebuildStaticMesh = true; + + // TODO: Handle materials + if (!bRebuildStaticMesh && !bMaterialHasChanged) + { + // We can simply reuse the found static mesh + OutputObjects.Add(OutputObjectIdentifier, *FoundOutputObject); + continue; + } + + bool bNewStaticMeshCreated = false; + if (!FoundStaticMesh) + { + // If we couldn't find a valid existing dynamic mesh, create a new one + FoundStaticMesh = CreateNewHoudiniStaticMesh(OutputObjectIdentifier.SplitIdentifier); + if (!IsValid(FoundStaticMesh)) + continue; + + bNewStaticMeshCreated = true; + } + + if (!FoundOutputObject) + { + // If we couldnt find a previous output object, create a new one + FHoudiniOutputObject NewOutputObject; + FoundOutputObject = &OutputObjects.Add(OutputObjectIdentifier, NewOutputObject); + } + FoundOutputObject->bProxyIsCurrent = true; + + // Update the attributes and tokens if this is the first split for this object identifier + if (bCopyAttributesAndTokens) + { + FoundOutputObject->CachedAttributes = MoveTemp(TempAttributes); + FoundOutputObject->CachedTokens = MoveTemp(TempTokens); + } + + if (bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateHoudiniStaticMesh() - PreBuildMesh in %f seconds."), FPlatformTime::Seconds() - tick); + tick = FPlatformTime::Seconds(); + } + + if (bRebuildStaticMesh) + { + //--------------------------------------------------------------------------------------------------------------------- + // INDICES + //--------------------------------------------------------------------------------------------------------------------- + + // + // Because of the splits, we don't need to declare all the vertices in the Part, + // but only the one that are currently used by the split's faces. + // The indicesMapper array is used to map those indices from Part Vertices to Split Vertices. + // We also keep track of the needed vertices index to declare them easily afterwards. + // + + // IndicesMapper: + // Maps index values for all vertices in the Part: + // - Vertices unused by the split will be set to -1 + // - Used vertices will have their value set to the "NewIndex" + // So that IndicesMapper[ oldIndex ] => newIndex + TArray IndicesMapper; + IndicesMapper.SetNumUninitialized(SplitVertexList.Num()); + for (int32 n = 0; n < IndicesMapper.Num(); n++) + IndicesMapper[n] = -1; + + int32 CurrentMapperIndex = 0; + + // NeededVertices: + // Array containing the old index of the needed vertices for the current split + // NeededVertices[ newIndex ] => oldIndex + TArray< int32 > NeededVertices; + NeededVertices.Reserve(SplitVertexList.Num() / 3); + TArray< int32 > TriangleIndices; + TriangleIndices.Reserve(SplitVertexList.Num()); + + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Build IndicesMapper and NeededVertices); + + bool bHasInvalidFaceIndices = false; + int32 ValidVertexId = 0; + for (int32 VertexIdx = 0; VertexIdx < SplitVertexList.Num(); VertexIdx += 3) + { + int32 WedgeCheck = SplitVertexList[VertexIdx + 0]; + if (WedgeCheck == -1) + continue; + + int32 WedgeIndices[3] = + { + SplitVertexList[VertexIdx + 0], + SplitVertexList[VertexIdx + 1], + SplitVertexList[VertexIdx + 2] + }; + + // Ensure the indices are valid + if (!IndicesMapper.IsValidIndex(WedgeIndices[0]) + || !IndicesMapper.IsValidIndex(WedgeIndices[1]) + || !IndicesMapper.IsValidIndex(WedgeIndices[2])) + { + // Invalid face index. Don't log in the loop. + bHasInvalidFaceIndices = true; + continue; + } + + // Converting Old (Part) Indices to New (Split) Indices: + for (int32 i = 0; i < 3; i++) + { + if (IndicesMapper[WedgeIndices[i]] < 0) + { + // This old index has not yet been "converted" to a new index + NeededVertices.Add(WedgeIndices[i]); + IndicesMapper[WedgeIndices[i]] = CurrentMapperIndex; + CurrentMapperIndex++; + } + + // Replace the old index with the new one + WedgeIndices[i] = IndicesMapper[WedgeIndices[i]]; + } + + // Flip wedge indices to fix the winding order. + TriangleIndices.Add(WedgeIndices[0]); + TriangleIndices.Add(WedgeIndices[2]); + TriangleIndices.Add(WedgeIndices[1]); + + ValidVertexId += 3; + } + + if (bHasInvalidFaceIndices) + { + HOUDINI_LOG_MESSAGE( + TEXT("Creating Dynamic Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%d %s] has some invalid face indices"), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, SplitId, *SplitGroupName); + } + } + + //--------------------------------------------------------------------------------------------------------------------- + // NORMALS + //--------------------------------------------------------------------------------------------------------------------- + + // Extract this part's normal if needed + UpdatePartNormalsIfNeeded(); + + // Get the normals for this split + TArray SplitNormals; + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoNormals, PartNormals, SplitNormals); + + // Check that the number of normal we retrieved is correct + int32 NormalCount = SplitNormals.Num() / 3; + if (NormalCount < 0 || NormalCount < NeededVertices.Num()) + { + // Ignore normals + NormalCount = 0; + if (SplitNormals.Num() != 0) + HOUDINI_LOG_WARNING(TEXT("Invalid normal count detected - Skipping normals.")); + } + + //--------------------------------------------------------------------------------------------------------------------- + // TANGENTS + //--------------------------------------------------------------------------------------------------------------------- + + TArray SplitTangentU; + TArray SplitTangentV; + int32 TangentUCount = 0; + int32 TangentVCount = 0; + // No need to read the tangents if we want unreal to recompute them after + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + bool bReadTangents = HoudiniRuntimeSettings ? HoudiniRuntimeSettings->RecomputeTangentsFlag != EHoudiniRuntimeSettingsRecomputeFlag::HRSRF_Always : true; + + bool bGenerateTangentsFromNormalAttribute = false; + if (bReadTangents) + { + // Extract this part's Tangents if needed + UpdatePartTangentsIfNeeded(); + + // Get the Tangents for this split + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoTangentU, PartTangentU, SplitTangentU); + + // Get the binormals for this split + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoTangentV, PartTangentV, SplitTangentV); + + if ((SplitTangentU.Num() <= 0 || SplitTangentV.Num() <= 0)) + bReadTangents = false; + + // We need to manually generate tangents if: + // - we have normals but dont have tangentu or tangentv attributes + // - we have not specified that we wanted unreal to generate them + bGenerateTangentsFromNormalAttribute = (NormalCount > 0) && !bReadTangents; + + // Check that the number of tangents read matches the number of normals + TangentUCount = SplitTangentU.Num() / 3; + TangentVCount = SplitTangentV.Num() / 3; + if (NormalCount > 0 && (TangentUCount != NormalCount || TangentVCount != NormalCount)) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateHoudiniStaticMesh: Generate tangents due to count mismatch (# U Tangents = %d; # V Tangents = %d; # Normals = %d)"), TangentUCount, TangentVCount, NormalCount); + bGenerateTangentsFromNormalAttribute = true; + bReadTangents = false; + } + + if (bGenerateTangentsFromNormalAttribute && (HoudiniRuntimeSettings->RecomputeTangentsFlag == EHoudiniRuntimeSettingsRecomputeFlag::HRSRF_Always)) + { + // No need to generate tangents if we want unreal to recompute them after + bGenerateTangentsFromNormalAttribute = false; + } + } + else + { + bGenerateTangentsFromNormalAttribute = (NormalCount > 0); + } + + //--------------------------------------------------------------------------------------------------------------------- + // VERTEX COLORS AND ALPHAS + //--------------------------------------------------------------------------------------------------------------------- + + // Extract this part's colors if needed + UpdatePartColorsIfNeeded(); + + // Get the colors values for this split + TArray SplitColors; + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoColors, PartColors, SplitColors); + + // Extract this part's alpha values if needed + UpdatePartAlphasIfNeeded(); + + // Get the colors values for this split + TArray SplitAlphas; + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoAlpha, PartAlphas, SplitAlphas); + + const int32 ColorsCount = AttribInfoColors.exists ? SplitColors.Num() / AttribInfoColors.tupleSize : 0; + const bool bSplitColorValid = AttribInfoColors.exists && (AttribInfoColors.tupleSize >= 3) && ColorsCount > 0; + const bool bSplitAlphaValid = AttribInfoAlpha.exists && (SplitAlphas.Num() == ColorsCount); + + //--------------------------------------------------------------------------------------------------------------------- + // UVS + //--------------------------------------------------------------------------------------------------------------------- + + // Extract this part's UV sets if needed + UpdatePartUVSetsIfNeeded(); + + // See if we need to transfer uv point attributes to vertex attributes. + int32 NumUVLayers = 0; + TArray> SplitUVSets; + SplitUVSets.SetNum(MAX_STATIC_TEXCOORDS); + for (int32 TexCoordIdx = 0; TexCoordIdx < MAX_STATIC_TEXCOORDS; ++TexCoordIdx) + { + FHoudiniMeshTranslator::TransferPartAttributesToSplit( + SplitVertexList, AttribInfoUVSets[TexCoordIdx], PartUVSets[TexCoordIdx], SplitUVSets[TexCoordIdx]); + if (SplitUVSets[TexCoordIdx].Num() > 0) + { + NumUVLayers++; + } + } + + //--------------------------------------------------------------------------------------------------------------------- + // MATERIAL ATTRIBUTE OVERRIDES + //--------------------------------------------------------------------------------------------------------------------- + + // TODO: These are actually per faces, not per vertices... + // Need to update!! + UpdatePartFaceMaterialOverridesIfNeeded(); + + // + // Initialize mesh + // + const int32 NumVertexPositions = NeededVertices.Num(); + const int32 NumTriangles = TriangleIndices.Num() / 3; + const bool bHasPerFaceMaterials = PartFaceMaterialOverrides.Num() > 0 || (PartUniqueMaterialIds.Num() > 0 && !bOnlyOneFaceMaterial); + + FoundStaticMesh->Initialize( + NumVertexPositions, + NumTriangles, + NumUVLayers, // NumUVLayers + 0, // InitialNumStaticMaterials + NormalCount > 0, // HasNormals + bReadTangents || bGenerateTangentsFromNormalAttribute, // HasTangents + bSplitColorValid, // HasColors + bHasPerFaceMaterials // HasPerFaceMaterials + ); + + //--------------------------------------------------------------------------------------------------------------------- + // POSITIONS + //--------------------------------------------------------------------------------------------------------------------- + UpdatePartPositionIfNeeded(); + + // + // Transfer vertex positions: + // + // Because of the split, we're only interested in the needed vertices. + // Instead of declaring all the Positions, we'll only declare the vertices + // needed by the current split. + // + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Set Vertex Positions); + + bool bHasInvalidPositionIndexData = false; + for (int32 VertexPositionIdx = 0; VertexPositionIdx < NumVertexPositions; ++VertexPositionIdx) + //ParallelFor(NumVertexPositions, [&](uint32 VertexPositionIdx) + { + int32 NeededVertexIndex = NeededVertices[VertexPositionIdx]; + if (!PartPositions.IsValidIndex(NeededVertexIndex * 3 + 2)) + { + // Error retrieving positions. + bHasInvalidPositionIndexData = true; + continue; + } + + // We need to swap Z and Y coordinate here, and convert from m to cm. + FoundStaticMesh->SetVertexPosition(VertexPositionIdx, FVector3f( + PartPositions[NeededVertexIndex * 3 + 0] * HAPI_UNREAL_SCALE_FACTOR_POSITION, + PartPositions[NeededVertexIndex * 3 + 2] * HAPI_UNREAL_SCALE_FACTOR_POSITION, + PartPositions[NeededVertexIndex * 3 + 1] * HAPI_UNREAL_SCALE_FACTOR_POSITION + )); + }//); + + if (bHasInvalidPositionIndexData) + { + HOUDINI_LOG_WARNING( + TEXT("Creating Dynamic Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%d %s] invalid position/index data ") + TEXT("- skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, SplitId, *SplitGroupName); + } + } + + //--------------------------------------------------------------------------------------------------------------------- + // FACES / TRIS + // Now set Normals, UVs and Colors on mesh points and AttributeSet + //--------------------------------------------------------------------------------------------------------------------- + + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Set Triangle Indices & Per Vertex Instance Attribute Values); + + // Now add the triangles to the mesh + for (int32 TriangleIdx = 0; TriangleIdx < NumTriangles; ++TriangleIdx) + // ParallelFor(NumTriangles, [&](uint32 TriangleIdx) + { + // TODO: add some additional intermediate consts for index calculations to make the indexing + // TODO: code a bit more readable + const int32 TriVertIdx0 = TriangleIdx * 3; + FoundStaticMesh->SetTriangleVertexIndices(TriangleIdx, FIntVector( + TriangleIndices[TriVertIdx0 + 0], + TriangleIndices[TriVertIdx0 + 1], + TriangleIndices[TriVertIdx0 + 2] + )); + + const int32 TriWindingIndex[3] = { 0, 2, 1 }; + // Normals and tangents (either getting tangents from attributes or generating tangents from the + // normals + if (NormalCount > 0 || bReadTangents) + { + for (int32 ElementIdx = 0; ElementIdx < 3; ++ElementIdx) + { + const bool bHasNormal = (NormalCount > 0 && SplitNormals.IsValidIndex(TriVertIdx0 * 3 + 3 * 3 - 1)); + FVector3f Normal = FVector3f::ZeroVector; + if (bHasNormal) + { + // Flip Z and Y coordinate for normal, but don't scale + Normal.Set( + SplitNormals[TriVertIdx0 * 3 + 3 * ElementIdx + 0], + SplitNormals[TriVertIdx0 * 3 + 3 * ElementIdx + 2], + SplitNormals[TriVertIdx0 * 3 + 3 * ElementIdx + 1] + ); + + FoundStaticMesh->SetTriangleVertexNormal(TriangleIdx, TriWindingIndex[ElementIdx], Normal); + } + + if (bReadTangents || bGenerateTangentsFromNormalAttribute) + { + FVector3f TangentU, TangentV; + if (bGenerateTangentsFromNormalAttribute) + { + if (bHasNormal) + { + // Generate the tangents if needed + Normal.FindBestAxisVectors(TangentU, TangentV); + + FoundStaticMesh->SetTriangleVertexUTangent(TriangleIdx, TriWindingIndex[ElementIdx], TangentU); + FoundStaticMesh->SetTriangleVertexVTangent(TriangleIdx, TriWindingIndex[ElementIdx], TangentV); + } + } + else + { + // Transfer the tangents from Houdini + TangentU.X = SplitTangentU[TriVertIdx0 * 3 + 3 * ElementIdx + 0]; + TangentU.Y = SplitTangentU[TriVertIdx0 * 3 + 3 * ElementIdx + 2]; + TangentU.Z = SplitTangentU[TriVertIdx0 * 3 + 3 * ElementIdx + 1]; + + TangentU.X = SplitTangentV[TriVertIdx0 * 3 + 3 * ElementIdx + 0]; + TangentU.Y = SplitTangentV[TriVertIdx0 * 3 + 3 * ElementIdx + 2]; + TangentU.Z = SplitTangentV[TriVertIdx0 * 3 + 3 * ElementIdx + 1]; + + FoundStaticMesh->SetTriangleVertexUTangent(TriangleIdx, TriWindingIndex[ElementIdx], TangentU); + FoundStaticMesh->SetTriangleVertexVTangent(TriangleIdx, TriWindingIndex[ElementIdx], TangentV); + } + } + } + } + + // Vertex Colors + if (bSplitColorValid && SplitColors.IsValidIndex(TriVertIdx0 * AttribInfoColors.tupleSize + 3 * AttribInfoColors.tupleSize - 1)) + { + FLinearColor VertexLinearColor; + for (int32 ElementIdx = 0; ElementIdx < 3; ++ElementIdx) + { + VertexLinearColor.R = FMath::Clamp( + SplitColors[TriVertIdx0 * AttribInfoColors.tupleSize + AttribInfoColors.tupleSize * ElementIdx + 0], 0.0f, 1.0f); + VertexLinearColor.G = FMath::Clamp( + SplitColors[TriVertIdx0 * AttribInfoColors.tupleSize + AttribInfoColors.tupleSize * ElementIdx + 1], 0.0f, 1.0f); + VertexLinearColor.B = FMath::Clamp( + SplitColors[TriVertIdx0 * AttribInfoColors.tupleSize + AttribInfoColors.tupleSize * ElementIdx + 2], 0.0f, 1.0f); + + if (bSplitAlphaValid) + { + VertexLinearColor.A = FMath::Clamp(SplitAlphas[TriVertIdx0 + ElementIdx], 0.0f, 1.0f); + } + else if (AttribInfoColors.tupleSize >= 4) + { + VertexLinearColor.A = FMath::Clamp( + SplitColors[TriVertIdx0 * AttribInfoColors.tupleSize + AttribInfoColors.tupleSize * ElementIdx + 3], 0.0f, 1.0f); + } + else + { + VertexLinearColor.A = 1.0f; + } + + FColor VertexColor = VertexLinearColor.ToFColor(false); + + // If Gamma correction is disabled, de-convert the color. Since SetTriangleVertexColor() will apply gamma. + if (bIsGammaCorrectionDisabled) + VertexColor = FLinearColor::FromSRGBColor(VertexColor).ToFColor(false); + + FoundStaticMesh->SetTriangleVertexColor(TriangleIdx, TriWindingIndex[ElementIdx], VertexColor); + } + } + + // UVs + if (NumUVLayers > 0) + { + // Dynamic mesh supports only 1 UV layer on the mesh it self. So we set the first layer + // on the mesh itself only, and we set all layers on the AttributeSet + for (int32 TexCoordIdx = 0; TexCoordIdx < NumUVLayers; ++TexCoordIdx) + { + const TArray& SplitUVs = SplitUVSets[TexCoordIdx]; + if (SplitUVs.IsValidIndex(TriVertIdx0 * 2 + 3 * 2 - 1)) + { + for (int32 ElementIdx = 0; ElementIdx < 3; ++ElementIdx) + { + const int32 UVIdx = TriVertIdx0 * 2 + ElementIdx * 2; + // We need to flip V coordinate when it's coming from HAPI. + const FVector2f UV(SplitUVs[UVIdx + 0], 1.0f - SplitUVs[UVIdx + 1]); + // Set the UV on the vertex instance in the UVLayer + FoundStaticMesh->SetTriangleVertexUV(TriangleIdx, TriWindingIndex[ElementIdx], TexCoordIdx, UV); + } + } + } + } + } + } + + FMeshBuildSettings BuildSettings; + UpdateMeshBuildSettings( + BuildSettings, + FoundStaticMesh->HasNormals(), + FoundStaticMesh->HasTangents(), + false); + // Compute normals if requested or needed/missing + if (BuildSettings.bRecomputeNormals) + { + FoundStaticMesh->CalculateNormals(BuildSettings.bComputeWeightedNormals); + } + + // Compute tangents if requested or needed/missing + if (BuildSettings.bRecomputeTangents) + { + FoundStaticMesh->CalculateTangents(BuildSettings.bComputeWeightedNormals); + } + } + + //--------------------------------------------------------------------------------------------------------------------- + // MATERIALS / FACE MATERIALS + //--------------------------------------------------------------------------------------------------------------------- + + // Get face indices for this split. + TArray& SplitFaceIndices = AllSplitFaceIndices[SplitGroupName]; + + // Fetch the FoundMesh's Static Materials array + TArray& FoundStaticMaterials = FoundStaticMesh->GetStaticMaterials(); + + // Clear the materials array of the mesh the first time we encounter it + if (!MapUnrealMaterialInterfaceToUnrealIndexPerMesh.Contains(FoundStaticMesh)) + { + FoundStaticMaterials.Empty(); + } + TMap, int32>& MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh = MapUnrealMaterialInterfaceToUnrealIndexPerMesh.FindOrAdd(FoundStaticMesh); + + // Process material overrides first + if (PartFaceMaterialOverrides.Num() > 0) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Set Per Face Material Overrides); + + // Array used to avoid constantly attempting to load invalid materials + TArray InvalidMaterials; + + for (int32 FaceIdx = 0; FaceIdx < SplitFaceIndices.Num(); ++FaceIdx) + { + int32 SplitFaceIndex = SplitFaceIndices[FaceIdx]; + if (!PartFaceMaterialOverrides.IsValidIndex(SplitFaceIndex)) + continue; + + UMaterialInterface * MaterialInterface = nullptr; + int32 CurrentFaceMaterialIdx = 0; + const FHoudiniMaterialInfo& MatInfo = PartFaceMaterialOverrides[SplitFaceIndex]; + const FHoudiniMaterialIdentifier MaterialIdentifier = MatInfo.MakeIdentifier(); + TObjectPtr* FoundMaterialInterface = MapHoudiniMatAttributesToUnrealInterface.Find(MaterialIdentifier); + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + if (!MaterialInterface) + { + // Try to locate the corresponding material interface + + // Start by looking in our assignment map + FoundMaterialInterface = OutputAssignmentMaterials.Find(MaterialIdentifier); + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + // Only try to load a material if it has a chance to be valid! + if (!MaterialInterface && MaterialIdentifier.IsValid() && !InvalidMaterials.Contains(MaterialIdentifier)) + { + MaterialInterface = Cast( + StaticLoadObject(UMaterialInterface::StaticClass(), + nullptr, *MaterialIdentifier.MaterialObjectPath, nullptr, LOAD_NoWarn, nullptr)); + + if (!MaterialInterface) + InvalidMaterials.Add(MaterialIdentifier); + } + + if (MaterialInterface) + { + // We managed to load the UE4 material + // Make sure this material is in the assignments before replacing it. + OutputAssignmentMaterials.Add(MaterialIdentifier, MaterialInterface); + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr* ReplacementMaterialInterface = ReplacementMaterials.Find(MaterialIdentifier); + if (ReplacementMaterialInterface && *ReplacementMaterialInterface) + MaterialInterface = *ReplacementMaterialInterface; + + // Add this material to the map + MapHoudiniMatAttributesToUnrealInterface.Add(MaterialIdentifier, MaterialInterface); + } + else + { + // The Attribute Material and its replacement do not exist + // See if we can fallback to the Houdini material assigned on the face + + // Get the unreal material corresponding to this houdini one + HAPI_NodeId MaterialId = PartFaceMaterialIds[SplitFaceIndex]; + + // See if we have already treated that material + FoundMaterialInterface = MapHoudiniMatIdToUnrealInterface.Find(MaterialId); + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + if (!MaterialInterface) + { + // If everything fails, we'll use the default material + MaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get()); + + // We need to add this material to the map + FString MaterialPathName = HAPI_UNREAL_DEFAULT_MATERIAL_NAME; + const bool bFoundHoudiniMaterial = FHoudiniMaterialTranslator::GetMaterialRelativePath(HGPO.AssetId, MaterialId, MaterialPathName); + const FHoudiniMaterialIdentifier DefaultMatIdentifier(MaterialPathName, bFoundHoudiniMaterial); + TObjectPtr* FoundMaterial = OutputAssignmentMaterials.Find(DefaultMatIdentifier); + if (FoundMaterial) + MaterialInterface = *FoundMaterial; + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr* ReplacementMaterial = ReplacementMaterials.Find(DefaultMatIdentifier); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + // Map the Houdini ID to the unreal one + MapHoudiniMatIdToUnrealInterface.Add(MaterialId, MaterialInterface); + } + } + } + + if (MaterialInterface) + { + CurrentFaceMaterialIdx = GetFaceMaterialIndex(MaterialInterface, + MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh, + MatInfo.MaterialIndex, + FoundStaticMaterials); + // Update the Face Material on the mesh + FoundStaticMesh->SetTriangleMaterialID(FaceIdx, CurrentFaceMaterialIdx); + } + } + } + else if (PartUniqueMaterialIds.Num() > 0) + { + // The part has houdini materials + if (bOnlyOneFaceMaterial) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Set Single Material); + + // Use default Houdini material if no valid material is assigned to any of the faces. + UMaterialInterface * MaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get()); + + // Get id of this single material. + FString MaterialPathName = HAPI_UNREAL_DEFAULT_MATERIAL_NAME; + const bool bFoundHoudiniMaterial = FHoudiniMaterialTranslator::GetMaterialRelativePath(HGPO.AssetId, PartFaceMaterialIds[0], MaterialPathName); + const FHoudiniMaterialIdentifier DefaultMatIdentifier(MaterialPathName, bFoundHoudiniMaterial); + TObjectPtr * FoundMaterial = OutputAssignmentMaterials.Find(DefaultMatIdentifier); + if (FoundMaterial) + MaterialInterface = *FoundMaterial; + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterial = ReplacementMaterials.Find(DefaultMatIdentifier); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + FoundStaticMaterials.Empty(); + FoundStaticMaterials.Add(FStaticMaterial(MaterialInterface)); + } + else + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Set Per Face Materials); + + // We have multiple houdini materials + // Get default Houdini material. + UMaterial * DefaultMaterial = FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get(); + + for (int32 FaceIdx = 0; FaceIdx < SplitFaceIndices.Num(); ++FaceIdx) + { + int32 SplitFaceIndex = SplitFaceIndices[FaceIdx]; + if (!PartFaceMaterialIds.IsValidIndex(SplitFaceIndex)) + continue; + + // Get material id for this face. + HAPI_NodeId MaterialId = PartFaceMaterialIds[SplitFaceIndex]; + + // See if we have already treated that material + TObjectPtr* FoundMaterialInterface = MapHoudiniMatIdToUnrealInterface.Find(MaterialId); + UMaterialInterface* MaterialInterface = nullptr; + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + if (MaterialInterface) + { + int32 const * FoundUnrealMatIndex = MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh.Find(MaterialInterface); + if (FoundUnrealMatIndex) + { + // This material has been mapped already, just assign the mat index + FoundStaticMesh->SetTriangleMaterialID(FaceIdx, *FoundUnrealMatIndex); + continue; + } + } + else + { + MaterialInterface = Cast(DefaultMaterial); + + FString MaterialPathName = HAPI_UNREAL_DEFAULT_MATERIAL_NAME; + const bool bFoundHoudiniMaterial = FHoudiniMaterialTranslator::GetMaterialRelativePath(HGPO.AssetId, MaterialId, MaterialPathName); + const FHoudiniMaterialIdentifier DefaultMatIdentifier(MaterialPathName, bFoundHoudiniMaterial); + TObjectPtr * FoundMaterial = OutputAssignmentMaterials.Find(DefaultMatIdentifier); + if (FoundMaterial) + MaterialInterface = *FoundMaterial; + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterial = ReplacementMaterials.Find(DefaultMatIdentifier); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + // Map the houdini ID to the unreal one + MapHoudiniMatIdToUnrealInterface.Add(MaterialId, MaterialInterface); + } + + if (MaterialInterface) + { + // Add the material to the Static mesh + int32 UnrealMatIndex = FoundStaticMaterials.Add(FStaticMaterial(MaterialInterface)); + + // Map the houdini ID to the unreal one + MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh.Add(MaterialInterface, UnrealMatIndex); + + // Update the face index + FoundStaticMesh->SetTriangleMaterialID(FaceIdx, UnrealMatIndex); + } + } + } + } + else + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Set Default Material); + + // No materials were found, we need to use default Houdini material. + UMaterialInterface * MaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get()); + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterial = ReplacementMaterials.Find( + FHoudiniMaterialIdentifier(HAPI_UNREAL_DEFAULT_MATERIAL_NAME, false, "")); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + FoundStaticMaterials.Empty(); + FoundStaticMaterials.Add(FStaticMaterial(MaterialInterface)); + } + + //// Update property attributes on the mesh + //TArray PropertyAttributes; + //if (GetGenericPropertiesAttributes( + // HGPO.GeoId, HGPO.PartId, + // AllSplitFirstValidVertexIndex[SplitGroupName], + // AllSplitFirstValidPrimIndex[SplitGroupName], + // PropertyAttributes)) + //{ + // UpdateGenericPropertiesAttributes( + // FoundStaticMesh, PropertyAttributes); + //} + + FoundStaticMesh->Optimize(); + + // Check if the mesh is valid (check all the counts (vertex, triangles, vertex instances, UVs etc) but skip + // looping over each individual triangle vertex index to check if the value is valid). + const bool bSkipVertexIndicesCheck = true; + if (!FoundStaticMesh->IsValid(bSkipVertexIndicesCheck)) + { + HOUDINI_LOG_WARNING( + TEXT("[CreateHoudiniStaticMesh]: Invalid StaticMesh data for %s in cook output! Please check the log."), + *FoundStaticMesh->GetName()); + } + + //// Try to find the outer package so we can dirty it up + //if (FoundStaticMesh->GetOuter()) + //{ + // FoundStaticMesh->GetOuter()->MarkPackageDirty(); + //} + //else + //{ + // FoundStaticMesh->MarkPackageDirty(); + //} + UPackage *MeshPackage = FoundStaticMesh->GetOutermost(); + if (IsValid(MeshPackage)) + { + MeshPackage->MarkPackageDirty(); + + /* + // DPT: deactivated auto saving mesh/material package + // only dirty for now, as we'll save them when saving the world. + // Save the created/updated package + FEditorFileUtils::PromptForCheckoutAndSave({ MeshPackage }, false, false); + */ + } + + // Add the Proxy mesh to the output maps + if (FoundOutputObject) + { + FoundOutputObject->ProxyObject = FoundStaticMesh; + FoundOutputObject->bProxyIsCurrent = true; + OutputObjects.FindOrAdd(OutputObjectIdentifier, *FoundOutputObject); + } + } + + const double time_end = FPlatformTime::Seconds(); + HOUDINI_LOG_MESSAGE(TEXT("CreateHoudiniStaticMesh() executed in %f seconds."), time_end - time_start); + + return true; +} + +void +FHoudiniMeshTranslator::ApplyComplexColliderHelper( + UStaticMesh* TargetStaticMesh, + UStaticMesh* ComplexStaticMesh, + const EHoudiniSplitType SplitType, + bool& bAssignedCustomCollisionMesh, + FHoudiniOutputObject* OutputObject) +{ + if (SplitType == EHoudiniSplitType::InvisibleComplexCollider && TargetStaticMesh) + { + if (!bAssignedCustomCollisionMesh) + { + bAssignedCustomCollisionMesh = true; + TargetStaticMesh->ComplexCollisionMesh = ComplexStaticMesh; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + TargetStaticMesh->SetCustomizedCollision(true); +#else + TargetStaticMesh->bCustomizedCollision = true; +#endif + bAssignedCustomCollisionMesh = true; + // We don't want an actor/component for this object in the scene, so flag it as an implicit output. + if (OutputObject) + { + OutputObject->bIsImplicit = true; + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("More than one (invisible) complex collision mesh found. Static Mesh assets only support a single complex collision mesh. Creating additional collision geo as Static Mesh Components.")); + } + } +} + + +bool +FHoudiniMeshTranslator::CreateNeededMaterials() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateNeededMaterials); + + UpdatePartNeededMaterials(); + + // Update package params with resolved attributes + TMap Attributes; + TMap Tokens; + FHoudiniAttributeResolver Resolver; + FHoudiniPackageParams FinalPackageParams; + + // Get the attributes from normal geo, or first LOD if there is no normal geo. Fallback to use index 0. + FString SplitToUse; + for (const FString& SplitGroupName : AllSplitGroups) + { + const EHoudiniSplitType SplitType = GetSplitTypeFromSplitName(SplitGroupName); + if (SplitType == EHoudiniSplitType::Normal) + { + SplitToUse = SplitGroupName; + break; + } + else if (SplitType == EHoudiniSplitType::LOD && SplitToUse.IsEmpty()) + { + SplitToUse = SplitGroupName; + // don't break here since we might still find normal geo after the LOD splits + } + } + + if (!SplitToUse.IsEmpty()) + { + CopyAttributesFromHGPOForSplit(SplitToUse, Attributes, Tokens); + } + else + { + CopyAttributesFromHGPOForSplit(HGPO, 0, 0, Attributes, Tokens); + } + + FHoudiniEngineUtils::UpdatePackageParamsForTempOutputWithResolver( + PackageParams, + IsValid(OuterComponent) ? OuterComponent->GetWorld() : nullptr, + OuterComponent, + Attributes, + Tokens, + FinalPackageParams, + Resolver); + + TArray MaterialAndTexturePackages; + + TArray OutMaterialArray; + + FinalPackageParams.OverideEnabled = false; + FHoudiniMaterialTranslator::CreateHoudiniMaterials( + HGPO.AssetId, + FinalPackageParams, + PartUniqueMaterialIds, + PartUniqueMaterialInfos, + InputAssignmentMaterials, + AllOutputMaterials, + OutputAssignmentMaterials, + OutMaterialArray, + MaterialAndTexturePackages, + false, + bTreatExistingMaterialsAsUpToDate); + + if (bMaterialOverrideNeedsCreateInstance && PartFaceMaterialOverrides.Num() > 0) + { + // Map containing unique face materials override attribute + // and their first valid prim index + // We create only one material instance per attribute + + FHoudiniMaterialTranslator::SortUniqueFaceMaterialOverridesAndCreateMaterialInstances(PartFaceMaterialOverrides, HGPO, PackageParams, MaterialAndTexturePackages, + InputAssignmentMaterials, OutputAssignmentMaterials, + false); + } + + return true; +} + +bool +FHoudiniMeshTranslator::ExtractMaterialIndex(FString& MaterialName, int32& MatIndex) +{ + // Attempt to extract an index + if (MaterialName.StartsWith("[")) + { + int32 End = MaterialName.Find("]"); + if (End != -1) + { + FString Fragment = MaterialName.Mid(1, End - 1); + if (FCString::IsNumeric(*Fragment)) + { + MatIndex = FCString::Atoi(*Fragment); + MaterialName = MaterialName.Mid(End + 1); + } + } + } + // correct invalid index + if (MatIndex < -1) + { + MaterialName = "[" + FString::FromInt(MatIndex) + "]" + MaterialName; + MatIndex = -1; + } + + return true; +} + +int32 +FHoudiniMeshTranslator::GetFaceMaterialIndex(UMaterialInterface* MaterialInterface, + TMap, int32>& MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh, + int32 MatIndex, + TArray& FoundStaticMaterials) +{ + int32 CurrentFaceMaterialIdx = 0; + // only rely on our map if no index specified in the attribute value + int32 const* FoundFaceMaterialIdx = MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh.Find(MaterialInterface); + if (FoundFaceMaterialIdx && MatIndex == -1) + { + // We already know what material index to use for that override + CurrentFaceMaterialIdx = *FoundFaceMaterialIdx; + } + else + { + // assign a default index for this mat + if (MatIndex == -1) + { + CurrentFaceMaterialIdx = FoundStaticMaterials.Add(FStaticMaterial(MaterialInterface)); + MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh.Add(MaterialInterface, CurrentFaceMaterialIdx); + } + // else, there is a specific index specified in the attrib value + else + { + // add the mat to the mesh's material list if needed + if (FoundStaticMaterials.Num() <= MatIndex || FoundStaticMaterials[MatIndex].MaterialInterface == nullptr) + { + FoundStaticMaterials.Reserve(MatIndex + 1); + // pad with empty materials + while (FoundStaticMaterials.Num() <= MatIndex) + { + FoundStaticMaterials.Add(FStaticMaterial()); + } + FoundStaticMaterials[MatIndex] = FStaticMaterial(MaterialInterface); + } + CurrentFaceMaterialIdx = MatIndex; + } + } + + return CurrentFaceMaterialIdx; +} + +FString +FHoudiniMeshTranslator::GetMeshIdentifierFromSplit(const FString& InSplitName, const EHoudiniSplitType& InSplitType) +{ + FString MeshIdentifier = TEXT(""); + switch (InSplitType) + { + case EHoudiniSplitType::Normal: + case EHoudiniSplitType::LOD: + case EHoudiniSplitType::InvisibleUCXCollider: + case EHoudiniSplitType::InvisibleSimpleCollider: + // LODs and Invisible simple colliders use the main mesh + MeshIdentifier = HAPI_UNREAL_GROUP_GEOMETRY_NOT_COLLISION; + break; + + case EHoudiniSplitType::InvisibleComplexCollider: + case EHoudiniSplitType::RenderedComplexCollider: + case EHoudiniSplitType::RenderedUCXCollider: + case EHoudiniSplitType::RenderedSimpleCollider: + // Rendered colliders or invisible complex colliders have their own static mesh + MeshIdentifier = InSplitName; + break; + + default: + break; + } + + return MeshIdentifier; +} + +UStaticMesh* +FHoudiniMeshTranslator::FindExistingStaticMesh(const FHoudiniOutputObjectIdentifier& InIdentifier) +{ + // See if we already have an input object for that output identifier + FHoudiniOutputObject const * FoundOutputObjectPtr = InputObjects.Find(InIdentifier); + UStaticMesh* FoundStaticMesh = nullptr; + if (FoundOutputObjectPtr) + { + // Make sure it's a valid static mesh + FoundStaticMesh = Cast(FoundOutputObjectPtr->OutputObject); + if (!IsValid(FoundStaticMesh)) + FoundStaticMesh = nullptr; + } + + if (!FoundStaticMesh) + { + // No input object matching this identifier, see if we have created an output object that matches + FoundOutputObjectPtr = OutputObjects.Find(InIdentifier); + if (!FoundOutputObjectPtr) + return nullptr; + + // Make sure it's a valid static mesh + FoundStaticMesh = Cast(FoundOutputObjectPtr->OutputObject); + if (!IsValid(FoundStaticMesh)) + return nullptr; + } + + if (FoundStaticMesh) + { + UObject* OuterMost = FoundStaticMesh->GetOutermostObject(); + if (OuterMost->IsA()) + { + // The Outermost for this static mesh is a level + // This is likely a SM created by V1, and we should not reuse it. + // This will force the plugin to recreate a "proper" SM in the temp folder. + FoundStaticMesh->MarkAsGarbage(); + FoundStaticMesh = nullptr; + } + + // Check that the existing mesh is in the expected directory (temp folder could have been changed between + // cooks). + if (!PackageParams.HasMatchingPackageDirectories(FoundStaticMesh)) + { + FoundStaticMesh = nullptr; + } + } + + return FoundStaticMesh; +} + +UHoudiniStaticMesh* +FHoudiniMeshTranslator::FindExistingHoudiniStaticMesh(const FHoudiniOutputObjectIdentifier& InIdentifier) +{ + // See if we already have an input object for that output identifier + FHoudiniOutputObject const * FoundOutputObjectPtr = InputObjects.Find(InIdentifier); + UHoudiniStaticMesh* FoundStaticMesh = nullptr; + if (FoundOutputObjectPtr) + { + // Make sure it's a valid static mesh + FoundStaticMesh = Cast(FoundOutputObjectPtr->ProxyObject); + if (!IsValid(FoundStaticMesh)) + FoundStaticMesh = nullptr; + } + + if (!FoundStaticMesh) + { + // No input object matching this identifier, see if we have created an output object that matches + FoundOutputObjectPtr = OutputObjects.Find(InIdentifier); + if (!FoundOutputObjectPtr) + return nullptr; + + // Make sure it's a valid static mesh + FoundStaticMesh = Cast(FoundOutputObjectPtr->ProxyObject); + if (!IsValid(FoundStaticMesh)) + return nullptr; + } + + // Check that the existing mesh is in the expected directory (temp folder could have been changed between + // cooks). + if (!PackageParams.HasMatchingPackageDirectories(FoundStaticMesh)) + { + FoundStaticMesh = nullptr; + } + + return FoundStaticMesh; +} + +EHoudiniSplitType +FHoudiniMeshTranslator::GetSplitTypeFromSplitName(const FString& InSplitName) +{ + const FString MainGroup = HAPI_UNREAL_GROUP_GEOMETRY_NOT_COLLISION; + if (InSplitName.StartsWith(MainGroup, ESearchCase::IgnoreCase)) + return EHoudiniSplitType::Normal; + + const FString LODGroupPrefix = HAPI_UNREAL_GROUP_LOD_PREFIX; + if (InSplitName.StartsWith(LODGroupPrefix, ESearchCase::IgnoreCase)) + { + return EHoudiniSplitType::LOD; + } + + const FString RenderedCollisionPrefix = HAPI_UNREAL_GROUP_RENDERED_COLLISION_PREFIX; + if (InSplitName.StartsWith(RenderedCollisionPrefix, ESearchCase::IgnoreCase)) + { + // Rendered colliders + // See if it is a simple/ucx/complex + const FString RenderedUCXCollisionPrefix = HAPI_UNREAL_GROUP_RENDERED_UCX_COLLISION_PREFIX; + const FString RenderedSimpleCollisionPrefix = HAPI_UNREAL_GROUP_RENDERED_SIMPLE_COLLISION_PREFIX; + if (InSplitName.StartsWith(RenderedUCXCollisionPrefix, ESearchCase::IgnoreCase)) + { + return EHoudiniSplitType::RenderedUCXCollider; + } + else if (InSplitName.StartsWith(RenderedSimpleCollisionPrefix, ESearchCase::IgnoreCase)) + { + return EHoudiniSplitType::RenderedSimpleCollider; + } + else + { + return EHoudiniSplitType::RenderedComplexCollider; + } + } + + const FString InvisibleCollisionPrefix = HAPI_UNREAL_GROUP_INVISIBLE_COLLISION_PREFIX; + if (InSplitName.StartsWith(InvisibleCollisionPrefix, ESearchCase::IgnoreCase)) + { + // Invisible colliders + // See if it is a simple/ucx/complex + const FString InvisibleUCXCollisionPrefix = HAPI_UNREAL_GROUP_INVISIBLE_UCX_COLLISION_PREFIX; + const FString InvisibleSimpleCollisionPrefix = HAPI_UNREAL_GROUP_INVISIBLE_SIMPLE_COLLISION_PREFIX; + if (InSplitName.StartsWith(InvisibleUCXCollisionPrefix, ESearchCase::IgnoreCase)) + { + return EHoudiniSplitType::InvisibleUCXCollider; + } + else if (InSplitName.StartsWith(InvisibleSimpleCollisionPrefix, ESearchCase::IgnoreCase)) + { + return EHoudiniSplitType::InvisibleSimpleCollider; + } + else + { + return EHoudiniSplitType::InvisibleComplexCollider; + } + } + + // ? + return EHoudiniSplitType::Invalid; + //return EHoudiniSplitType::Normal; +} + +bool +FHoudiniMeshTranslator::AddConvexCollisionToAggregate(const FString& SplitGroupName, FKAggregateGeom& AggCollisions) +{ + // Get the vertex indices for the split group + TArray& SplitGroupVertexList = AllSplitVertexLists[SplitGroupName]; + + // We're only interested in unique vertices + TArray UniqueVertexIndexes; + for (int32 VertexIdx = 0; VertexIdx < SplitGroupVertexList.Num(); VertexIdx++) + { + int32 Index = SplitGroupVertexList[VertexIdx]; + if (!PartPositions.IsValidIndex(Index)) + continue; + + UniqueVertexIndexes.AddUnique(Index); + } + + // Extract the collision geo's vertices + TArray< FVector > VertexArray; + VertexArray.SetNum(UniqueVertexIndexes.Num()); + for (int32 Idx = 0; Idx < UniqueVertexIndexes.Num(); Idx++) + { + int32 VertexIndex = UniqueVertexIndexes[Idx]; + if (!PartPositions.IsValidIndex(VertexIndex * 3 + 2)) + continue; + + VertexArray[Idx].X = PartPositions[VertexIndex * 3 + 0] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + VertexArray[Idx].Y = PartPositions[VertexIndex * 3 + 2] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + VertexArray[Idx].Z = PartPositions[VertexIndex * 3 + 1] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + +#if WITH_EDITOR + // Do we want to create multiple convex hulls? + bool bDoMultiHullDecomp = false; + if (SplitGroupName.Contains(TEXT("ucx_multi"), ESearchCase::IgnoreCase)) + bDoMultiHullDecomp = true; + + uint32 HullCount = 8; + int32 MaxHullVerts = 16; + if (bDoMultiHullDecomp) + { + // TODO: + // Look for extra attributes for the decomposition parameters? (HullCount/MaxHullVerts) + } + + if (bDoMultiHullDecomp && (VertexArray.Num() >= 3 || UniqueVertexIndexes.Num() >= 3)) + { + // creating multiple convex hull collision + // ... this might take a while + + // We're only interested in the valid indices! + TArray Indices; + for (int32 VertexIdx = 0; VertexIdx < SplitGroupVertexList.Num(); VertexIdx++) + { + int32 Index = SplitGroupVertexList[VertexIdx]; + if (!PartPositions.IsValidIndex(Index)) + continue; + + Indices.Add(Index); + } + + // But we need all the positions as vertex + TArray Vertices; + Vertices.SetNum(PartPositions.Num() / 3); + + for (int32 Idx = 0; Idx < Vertices.Num(); Idx++) + { + Vertices[Idx].X = PartPositions[Idx * 3 + 0] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + Vertices[Idx].Y = PartPositions[Idx * 3 + 2] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + Vertices[Idx].Z = PartPositions[Idx * 3 + 1] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + + // We are using Unreal's DecomposeMeshToHulls() + // We need a BodySetup so create a fake/transient one + UBodySetup* BodySetup = NewObject(); + + // Run actual util to do the work (if we have some valid input) + DecomposeMeshToHulls(BodySetup, Vertices, Indices, HullCount, MaxHullVerts); + + // If we succeed, return here + // If not, keep going and we'll try to do a single hull decomposition + if (BodySetup->AggGeom.ConvexElems.Num() > 0) + { + // Copy the convex elem to our aggregate + for (int32 n = 0; n < BodySetup->AggGeom.ConvexElems.Num(); n++) + AggCollisions.ConvexElems.Add(BodySetup->AggGeom.ConvexElems[n]); + + return true; + } + } +#endif + + // Creating a single Convex collision + FKConvexElem ConvexCollision; + ConvexCollision.VertexData = VertexArray; + ConvexCollision.UpdateElemBox(); + + AggCollisions.ConvexElems.Add(ConvexCollision); + + return true; +} + +TArray FHoudiniMeshTranslator::GetKdopDirections(const FString& SplitGroupName) +{ + uint32 NumDirections = 26; + const FVector* Directions = KDopDir26; + if (SplitGroupName.Contains("kdop10X")) + { + NumDirections = 10; + Directions = KDopDir10X; + } + else if (SplitGroupName.Contains("kdop10Y")) + { + NumDirections = 10; + Directions = KDopDir10Y; + } + else if (SplitGroupName.Contains("kdop10Z")) + { + NumDirections = 10; + Directions = KDopDir10Z; + } + else if (SplitGroupName.Contains("kdop18")) + { + NumDirections = 18; + Directions = KDopDir18; + } + + // Converting the directions to a TArray + TArray DirArray; + DirArray.SetNum(NumDirections); + for (uint32 DirectionIndex = 0; DirectionIndex < NumDirections; DirectionIndex++) + { + DirArray[DirectionIndex] = Directions[DirectionIndex]; + } + return DirArray; +} + +bool +FHoudiniMeshTranslator::AddSimpleCollisionToAggregate(const FString& SplitGroupName, FKAggregateGeom& AggCollisions) +{ + // Get the vertex indices for the split group + TArray& SplitGroupVertexList = AllSplitVertexLists[SplitGroupName]; + + // We're only interested in unique vertices + TArray UniqueVertexIndexes; + + for (int32 VertexIdx = 0; VertexIdx < SplitGroupVertexList.Num(); VertexIdx++) + { + int32 Index = SplitGroupVertexList[VertexIdx]; + if (!PartPositions.IsValidIndex(Index)) + continue; + + UniqueVertexIndexes.AddUnique(Index); + } + + // Extract the collision geo's vertices + TArray< FVector > VertexArray; + VertexArray.SetNum(UniqueVertexIndexes.Num()); + for (int32 Idx = 0; Idx < UniqueVertexIndexes.Num(); Idx++) + { + int32 VertexIndex = UniqueVertexIndexes[Idx]; + if (!PartPositions.IsValidIndex(VertexIndex * 3 + 2)) + continue; + + VertexArray[Idx].X = PartPositions[VertexIndex * 3 + 0] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + VertexArray[Idx].Y = PartPositions[VertexIndex * 3 + 2] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + VertexArray[Idx].Z = PartPositions[VertexIndex * 3 + 1] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + + int32 NewColliders = 0; + if (SplitGroupName.Contains("Box")) + { + NewColliders = FHoudiniMeshTranslator::GenerateOrientedBoxAsSimpleCollision(VertexArray, AggCollisions); + } + else if (SplitGroupName.Contains("Sphere")) + { + NewColliders = FHoudiniMeshTranslator::GenerateSphereAsSimpleCollision(VertexArray, AggCollisions); + } + else if (SplitGroupName.Contains("Capsule")) + { + NewColliders = FHoudiniMeshTranslator::GenerateOrientedSphylAsSimpleCollision(VertexArray, AggCollisions); + } + else + { + TArray DirArray = GetKdopDirections(SplitGroupName); + + NewColliders = FHoudiniMeshTranslator::GenerateKDopAsSimpleCollision(VertexArray, DirArray, AggCollisions); + } + + return (NewColliders > 0); +} + +int32 +FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + const TArray& InVertexList, + const HAPI_AttributeInfo& InAttribInfo, + const TArray& InData, + TArray& OutVertexData) +{ + return FHoudiniMeshTranslator::TransferPartAttributesToSplit( + InVertexList, InAttribInfo, InData, OutVertexData); +} + +/* +int32 +FHoudiniMeshTranslator::GetSplitNormals( + const TArray& InSplitVertexList, TArray& OutNormals) +{ + // Extract the normals + UpdatePartNormalsIfNeeded(); + + // Get the normals for this split + TArray VertexData; + int32 WedgeCount = FHoudiniMeshTranslator::TransferPartAttributesToSplit( + InSplitVertexList, AttribInfoNormals, PartNormals, VertexData); + + // Convert the float data to Vectors and fix the winding order + OutNormals.SetNum(WedgeCount); + for (int32 WedgeIdx = 0; WedgeIdx + 2 < WedgeCount; WedgeIdx += 3) + { + OutNormals[WedgeIdx].X = VertexData[WedgeIdx * 3 + 0]; + OutNormals[WedgeIdx].Y = VertexData[WedgeIdx * 3 + 2]; + OutNormals[WedgeIdx].Z = VertexData[WedgeIdx * 3 + 1]; + + OutNormals[WedgeIdx + 2].X = VertexData[(WedgeIdx + 1) * 3 + 0]; + OutNormals[WedgeIdx + 2].Y = VertexData[(WedgeIdx + 1) * 3 + 2]; + OutNormals[WedgeIdx + 2].Z = VertexData[(WedgeIdx + 1) * 3 + 1]; + + OutNormals[WedgeIdx + 1].X = VertexData[(WedgeIdx + 2) * 3 + 0]; + OutNormals[WedgeIdx + 1].Y = VertexData[(WedgeIdx + 2) * 3 + 2]; + OutNormals[WedgeIdx + 1].Z = VertexData[(WedgeIdx + 2) * 3 + 1]; + } + + return WedgeCount; +} + +int32 +FHoudiniMeshTranslator::GetSplitUVs( + const TArray& InSplitVertexList, TArray& OutUVs) +{ + // Extract the normals + UpdatePartUVSetsIfNeeded(); + + // Get the normals for this split + TArray VertexData; + int32 WedgeCount = FHoudiniMeshTranslator::TransferPartAttributesToSplit( + InSplitVertexList, AttribInfoNormals, PartNormals, VertexData); + + // Convert the float data to Vectors and fix the winding order + OutNormals.SetNum(WedgeCount); + for (int32 WedgeIdx = 0; WedgeIdx + 2 < WedgeCount; WedgeIdx += 3) + { + OutNormals[WedgeIdx].X = VertexData[WedgeIdx * 3 + 0]; + OutNormals[WedgeIdx].Y = VertexData[WedgeIdx * 3 + 2]; + OutNormals[WedgeIdx].Z = VertexData[WedgeIdx * 3 + 1]; + + OutNormals[WedgeIdx + 2].X = VertexData[(WedgeIdx + 1) * 3 + 0]; + OutNormals[WedgeIdx + 2].Y = VertexData[(WedgeIdx + 1) * 3 + 2]; + OutNormals[WedgeIdx + 2].Z = VertexData[(WedgeIdx + 1) * 3 + 1]; + + OutNormals[WedgeIdx + 1].X = VertexData[(WedgeIdx + 2) * 3 + 0]; + OutNormals[WedgeIdx + 1].Y = VertexData[(WedgeIdx + 2) * 3 + 2]; + OutNormals[WedgeIdx + 1].Z = VertexData[(WedgeIdx + 2) * 3 + 1]; + } + + return WedgeCount; +} + + +int32 +FHoudiniMeshTranslator::TransferPartAttributesToSplit( + const TArray& InVertexList, + const HAPI_AttributeInfo& InAttribInfo, + const TArray& InData, + TArray& OutData, + const float& ScaleFactor) +{ + TArray VertexData; + int32 WedgeCount = FHoudiniMeshTranslator::TransferPartAttributesToSplit( + InVertexList, InAttribInfo, InData, VertexData); + + // Convert the float data to Vectors and fix the winding order + OutData.SetNum(WedgeCount); + for (int32 WedgeIdx = 0; WedgeIdx + 2 < WedgeCount; WedgeIdx += 3) + { + OutData[WedgeIdx].X = VertexData[WedgeIdx * 2 + 0]; + OutData[WedgeIdx].Y = VertexData[WedgeIdx * 2 + 1]; + + OutData[WedgeIdx + 2].X = VertexData[(WedgeIdx + 1) * 2 + 0]; + OutData[WedgeIdx + 2].Y = VertexData[(WedgeIdx + 1) * 2 + 1]; + + OutData[WedgeIdx + 1].X = VertexData[(WedgeIdx + 2) * 2 + 0]; + OutData[WedgeIdx + 1].Y = VertexData[(WedgeIdx + 2) * 2 + 1]; + } + + return WedgeCount; +} + +int32 +FHoudiniMeshTranslator::TransferPartAttributesToSplit( + const TArray& InVertexList, + const HAPI_AttributeInfo& InAttribInfo, + const TArray& InData, + TArray& OutData, + const float& ScaleFactor) +{ + TArray VertexData; + int32 WedgeCount = FHoudiniMeshTranslator::TransferPartAttributesToSplit( + InVertexList, InAttribInfo, InData, VertexData); + + // Convert the float data to Vectors and fix the winding order + OutData.SetNum(WedgeCount); + for (int32 WedgeIdx = 0; WedgeIdx + 2 < WedgeCount; WedgeIdx += 3) + { + OutData[WedgeIdx].X = VertexData[WedgeIdx * 2 + 0]; + OutData[WedgeIdx].Y = VertexData[WedgeIdx * 2 + 1]; + + OutData[WedgeIdx + 2].X = VertexData[(WedgeIdx + 1) * 2 + 0]; + OutData[WedgeIdx + 2].Y = VertexData[(WedgeIdx + 1) * 2 + 1]; + + OutData[WedgeIdx + 1].X = VertexData[(WedgeIdx + 2) * 2 + 0]; + OutData[WedgeIdx + 1].Y = VertexData[(WedgeIdx + 2) * 2 + 1]; + } + + return WedgeCount; +} +*/ + + +template +int32 FHoudiniMeshTranslator::TransferPartAttributesToSplit( + const TArray& InVertexList, + const HAPI_AttributeInfo& InAttribInfo, + const TArray& InData, + TArray& OutVertexData) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::TransferPartAttributesToSplit); + + if (!InAttribInfo.exists || InAttribInfo.tupleSize <= 0) + return 0; + + if (InData.Num() <= 0) + return 0; + + int32 ValidWedgeCount = 0; + + // Future optimization - see if we can do direct vertex transfer. + int32 WedgeCount = InVertexList.Num(); + int32 LastValidWedgeIdx = 0; + if (InAttribInfo.owner == HAPI_ATTROWNER_POINT) + { + // Point attribute transfer + OutVertexData.SetNumZeroed(WedgeCount * InAttribInfo.tupleSize); + for (int32 WedgeIdx = 0; WedgeIdx < WedgeCount; ++WedgeIdx) + { + int32 VertexIdx = InVertexList[WedgeIdx]; + if (VertexIdx < 0) + { + // This is an index/wedge we are skipping due to split. + continue; + } + + int32 OutIdx = LastValidWedgeIdx * InAttribInfo.tupleSize; + for (int32 TupleIdx = 0; TupleIdx < InAttribInfo.tupleSize; TupleIdx++) + { + OutVertexData[OutIdx + TupleIdx] = InData[VertexIdx * InAttribInfo.tupleSize + TupleIdx]; + } + + // We are re-indexing wedges. + LastValidWedgeIdx++; + // Increment wedge count, since this is a valid wedge. + ValidWedgeCount++; + } + } + else if (InAttribInfo.owner == HAPI_ATTROWNER_VERTEX) + { + // Vertex attribute transfer + OutVertexData.SetNumZeroed(WedgeCount * InAttribInfo.tupleSize); + for (int32 WedgeIdx = 0; WedgeIdx < WedgeCount; ++WedgeIdx) + { + if (InVertexList[WedgeIdx] < 0) + { + // This is an index/wedge we are skipping due to split. + continue; + } + + int32 OutIdx = LastValidWedgeIdx * InAttribInfo.tupleSize; + for (int32 TupleIdx = 0; TupleIdx < InAttribInfo.tupleSize; TupleIdx++) + { + OutVertexData[OutIdx + TupleIdx] = InData[WedgeIdx * InAttribInfo.tupleSize + TupleIdx]; + } + + // We are re-indexing wedges. + LastValidWedgeIdx++; + // Increment wedge count, since this is a valid wedge. + ValidWedgeCount++; + } + } + else if (InAttribInfo.owner == HAPI_ATTROWNER_PRIM) + { + // Primitive attribute transfer + OutVertexData.SetNumZeroed(WedgeCount * InAttribInfo.tupleSize); + for (int32 WedgeIdx = 0; WedgeIdx < WedgeCount; ++WedgeIdx) + { + if (InVertexList[WedgeIdx] < 0) + { + // This is an index/wedge we are skipping due to split. + continue; + } + + int32 PrimIdx = WedgeIdx / 3; + int32 OutIdx = LastValidWedgeIdx * InAttribInfo.tupleSize; + for (int32 TupleIdx = 0; TupleIdx < InAttribInfo.tupleSize; TupleIdx++) + { + OutVertexData[OutIdx + TupleIdx] = InData[PrimIdx * InAttribInfo.tupleSize + TupleIdx]; + } + + // We are re-indexing wedges. + LastValidWedgeIdx++; + // Increment wedge count, since this is a valid wedge. + ValidWedgeCount++; + } + } + else if (InAttribInfo.owner == HAPI_ATTROWNER_DETAIL) + { + // Detail attribute transfer + // We have one value to copy for all output split vertices + // if the attribute is a single value (not a tuple) + // then we can simply use the array init function instead of looping + if (InAttribInfo.tupleSize == 1) + { + OutVertexData.SetNumUninitialized(WedgeCount); + for (int32 n = 0; n < OutVertexData.Num(); n++) + OutVertexData[n] = InData[0]; + } + else + { + OutVertexData.SetNumZeroed(WedgeCount * InAttribInfo.tupleSize); + for (int32 WedgeIdx = 0; WedgeIdx < WedgeCount; ++WedgeIdx) + { + if (InVertexList[WedgeIdx] < 0) + { + // This is an index/wedge we are skipping due to split. + continue; + } + + int32 OutIdx = LastValidWedgeIdx * InAttribInfo.tupleSize; + for (int32 TupleIdx = 0; TupleIdx < InAttribInfo.tupleSize; TupleIdx++) + { + OutVertexData[OutIdx + TupleIdx] = InData[TupleIdx]; + } + + // We are re-indexing wedges. + LastValidWedgeIdx++; + // Increment wedge count, since this is a valid wedge. + ValidWedgeCount++; + } + } + } + else + { + // Invalid attribute owner, shouldn't happen + check(false); + } + + OutVertexData.SetNumZeroed(ValidWedgeCount * InAttribInfo.tupleSize); + + return ValidWedgeCount; +} + +bool +FHoudiniMeshTranslator::TryToFindPropertyOnSourceModel( + UStaticMesh* const InStaticMesh, + const int32 InSourceModelIndex, + const FString& InPropertyName, + FEditPropertyChain& InPropertyChain, + bool& bOutSkipDefaultIfPropertyNotFound, + FProperty*& OutFoundProperty, + UObject*& OutFoundPropertyObject, + void*& OutContainer + ) +{ + if (!IsValid(InStaticMesh)) + return false; + + if (!InStaticMesh->IsSourceModelValid(InSourceModelIndex)) + return false; + + FStaticMeshSourceModel& SourceModel = InStaticMesh->GetSourceModel(InSourceModelIndex); + + // Restrict to only applying properties to the source model -- if the property is not found on the + // source model, don't search via the static mesh + bOutSkipDefaultIfPropertyNotFound = true; + FProperty* const SourceModelsProperty = InStaticMesh->GetClass()->FindPropertyByName(FName(TEXT("SourceModels"))); + if (SourceModelsProperty) + InPropertyChain.AddTail(SourceModelsProperty); + + if (!TryToFindPropertyOnSourceModel(SourceModel, InPropertyName, InPropertyChain, OutFoundProperty, OutContainer)) + return false; + + OutFoundPropertyObject = InStaticMesh; + return true; +} + +bool +FHoudiniMeshTranslator::TryToFindPropertyOnSourceModel( + FStaticMeshSourceModel& InSourceModel, + const FString& InPropertyName, + FEditPropertyChain& InPropertyChain, + FProperty*& OutFoundProperty, + void*& OutContainer) +{ + bool bFoundProperty = false; + FHoudiniGenericAttribute::TryToFindProperty( + &InSourceModel, InSourceModel.StaticStruct(), InPropertyName, InPropertyChain, OutFoundProperty, bFoundProperty, OutContainer, false); + return bFoundProperty; +} + +float +FHoudiniMeshTranslator::GetLODSCreensizeForSplit(const FString& SplitGroupName) +{ + // LOD Screensize + // default values has already been set, see if we have any attribute override for this + float screensize = -1.0f; + + // Start by looking at the lod_screensize primitive attribute + bool bAttribValid = false; + UpdatePartLODScreensizeIfNeeded(); + + if (PartLODScreensize.Num() > 0) + { + // use the "lod_screensize" primitive attribute + int32 FirstValidPrimIndex = AllSplitFirstValidPrimIndex[SplitGroupName]; + if (PartLODScreensize.IsValidIndex(FirstValidPrimIndex)) + screensize = PartLODScreensize[FirstValidPrimIndex]; + } + + if (screensize < 0.0f) + { + // We couldn't find the primitive attribute, look for a "lodX_screensize" detail attribute + FString LODAttributeName = SplitGroupName + HAPI_UNREAL_ATTRIB_LOD_SCREENSIZE_POSTFIX; + + TArray LODScreenSizes; + + FHoudiniHapiAccessor Accessor(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, TCHAR_TO_ANSI(*LODAttributeName)); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, LODScreenSizes, 0, 1); + + if (bSuccess) + { + screensize = LODScreenSizes[0]; + } + } + + if (screensize < 0.0f) + { + // finally, look for a potential uproperty style attribute + // aka, "unreal_uproperty_screensize" + TArray LODScreenSizes; + HAPI_AttributeInfo AttribInfoScreenSize; + FHoudiniApi::AttributeInfo_Init(&AttribInfoScreenSize); + + FHoudiniHapiAccessor Accessor(HGPO.GeoInfo.NodeId, HGPO.PartInfo.PartId, HAPI_UNREAL_ATTRIB_LOD_SCREENSIZE); + bool bSuccess = Accessor.GetInfo(AttribInfoScreenSize); + + if (bSuccess) + Accessor.GetAttributeData(AttribInfoScreenSize, PartLODScreensize, 0, 1); + + if (bSuccess && AttribInfoScreenSize.exists) + { + if (AttribInfoScreenSize.owner == HAPI_ATTROWNER_DETAIL && LODScreenSizes.Num() > 0) + { + screensize = LODScreenSizes[0]; + } + else if (AttribInfoScreenSize.owner == HAPI_ATTROWNER_PRIM) + { + int32 FirstValidPrimIndex = AllSplitFirstValidPrimIndex[SplitGroupName]; + if (LODScreenSizes.IsValidIndex(FirstValidPrimIndex)) + screensize = LODScreenSizes[FirstValidPrimIndex]; + } + } + } + + // Make sure the screensize is in percent, so if its above 1, divide by 100 + if (screensize > 1.0f) + screensize /= 100.0f; + + return screensize; +} + +int32 +FHoudiniMeshTranslator::GenerateBoxAsSimpleCollision(const TArray& InPositionArray, FKAggregateGeom& OutAggregateCollisions) +{ + // + // Code simplified and adapted to work with a simple vector array from GeomFitUtils.cpp + // + + // Calculate bounding Box. + FVector Center, Extents; + FVector unitVec = FVector::OneVector;// bs->BuildScale3D; + CalcBoundingBox(InPositionArray, Center, Extents, unitVec); + + FKBoxElem BoxElem; + BoxElem.Center = Center; + BoxElem.X = Extents.X * 2.0f; + BoxElem.Y = Extents.Y * 2.0f; + BoxElem.Z = Extents.Z * 2.0f; + OutAggregateCollisions.BoxElems.Add(BoxElem); + + return 1; +} + +int32 +FHoudiniMeshTranslator::GenerateOrientedBoxAsSimpleCollision(const TArray& InPositionArray, FKAggregateGeom& OutAggregateCollisions) +{ + // + // Code adapted from Experimental GeometryProcessing plugin for simple mesh approximation. + // + + const int32 NumPoints = InPositionArray.Num(); + TArray> Points; + Points.SetNumUninitialized(InPositionArray.Num()); + for(int32 i = 0; i < NumPoints; ++i) + { + Points[i] = Convert(InPositionArray[i]); + } + // Calculate bounding Box. + houdini::gte::OrientedBox3 MinimalBox = houdini::gte::OrientedBox3(); + houdini::gte::MinimumVolumeBox3 BoxCompute; + MinimalBox = BoxCompute(NumPoints, Points.GetData(), nullptr); + + // FVector unitVec = FVector::OneVector;// bs->BuildScale3D; + // CalcBoundingBox(InPositionArray, Center, Extents, unitVec); + + const FVector X = Convert(MinimalBox.axis[0]); + const FVector Y = Convert(MinimalBox.axis[1]); + const FRotator Rot = FRotationMatrix::MakeFromXY(X,Y).Rotator(); + + const FVector Extents = Convert(MinimalBox.extent); + FKBoxElem BoxElem; + BoxElem.Center = Convert(MinimalBox.center); + BoxElem.X = Extents.X * 2.0f; + BoxElem.Y = Extents.Y * 2.0f; + BoxElem.Z = Extents.Z * 2.0f; + BoxElem.Rotation = Rot; + OutAggregateCollisions.BoxElems.Add(BoxElem); + + return 1; +} + +void +FHoudiniMeshTranslator::CalcBoundingBox(const TArray& PositionArray, FVector& Center, FVector& Extents, FVector& LimitVec) +{ + // + // Code simplified and adapted to work with a simple vector array from GeomFitUtils.cpp + // + + FBox Box(ForceInit); + for (const FVector& CurPos : PositionArray) + { + Box += CurPos; + } + Box.GetCenterAndExtents(Center, Extents); +} + +int32 +FHoudiniMeshTranslator::GenerateSphereAsSimpleCollision(const TArray& InPositionArray, FKAggregateGeom& OutAggregateCollisions) +{ + // + // Code simplified and adapted to work with a simple vector array from GeomFitUtils.cpp + // + + FSphere bSphere, bSphere2, bestSphere; + FVector unitVec = FVector::OneVector; + + // Calculate bounding sphere. + CalcBoundingSphere(InPositionArray, bSphere, unitVec); + CalcBoundingSphere2(InPositionArray, bSphere2, unitVec); + + if (bSphere.W < bSphere2.W) + bestSphere = bSphere; + else + bestSphere = bSphere2; + + // Don't use if radius is zero. + if (bestSphere.W <= 0.f) + { + HOUDINI_LOG_WARNING(TEXT("Failed to generate a simple Sphere collider.")); + return 0; + } + + FKSphereElem SphereElem; + SphereElem.Center = bestSphere.Center; + SphereElem.Radius = bestSphere.W; + OutAggregateCollisions.SphereElems.Add(SphereElem); + + return 1; +} + +void +FHoudiniMeshTranslator::CalcBoundingSphere(const TArray& PositionArray, FSphere& sphere, FVector& LimitVec) +{ + // + // Code simplified and adapted to work with a simple vector array from GeomFitUtils.cpp + // + + if (PositionArray.Num() == 0) + return; + + FBox Box; + FVector MinIx[3] = {}; + FVector MaxIx[3] = {}; + + bool bFirstVertex = true; + for (const FVector& CurPosition : PositionArray) + { + FVector p = CurPosition * LimitVec; + if (bFirstVertex) + { + // First, find AABB, remembering furthest points in each dir. + Box.Min = p; + Box.Max = Box.Min; + + MinIx[0] = CurPosition; + MinIx[1] = CurPosition; + MinIx[2] = CurPosition; + + MaxIx[0] = CurPosition; + MaxIx[1] = CurPosition; + MaxIx[2] = CurPosition; + bFirstVertex = false; + continue; + } + + // X // + if (p.X < Box.Min.X) + { + Box.Min.X = p.X; + MinIx[0] = CurPosition; + } + else if (p.X > Box.Max.X) + { + Box.Max.X = p.X; + MaxIx[0] = CurPosition; + } + + // Y // + if (p.Y < Box.Min.Y) + { + Box.Min.Y = p.Y; + MinIx[1] = CurPosition; + } + else if (p.Y > Box.Max.Y) + { + Box.Max.Y = p.Y; + MaxIx[1] = CurPosition; + } + + // Z // + if (p.Z < Box.Min.Z) + { + Box.Min.Z = p.Z; + MinIx[2] = CurPosition; + } + else if (p.Z > Box.Max.Z) + { + Box.Max.Z = p.Z; + MaxIx[2] = CurPosition; + } + } + + const FVector Extremes[3] = { (MaxIx[0] - MinIx[0]) * LimitVec, + (MaxIx[1] - MinIx[1]) * LimitVec, + (MaxIx[2] - MinIx[2]) * LimitVec }; + + // Now find extreme points furthest apart, and initial center and radius of sphere. + float d2 = 0.f; + for (int32 i = 0; i < 3; i++) + { + const float tmpd2 = Extremes[i].SizeSquared(); + if (tmpd2 > d2) + { + d2 = tmpd2; + sphere.Center = (MinIx[i] + (0.5f * Extremes[i])) * LimitVec; + sphere.W = 0.f; + } + } + + const FVector Extents = FVector(Extremes[0].X, Extremes[1].Y, Extremes[2].Z); + + // radius and radius squared + float r = 0.5f * Extents.GetMax(); + float r2 = FMath::Square(r); + + // Now check each point lies within this sphere. If not - expand it a bit. + for (const FVector& curPos : PositionArray) + { + const FVector cToP = (curPos * LimitVec) - sphere.Center; + + const float pr2 = cToP.SizeSquared(); + + // If this point is outside our current bounding sphere's radius + if (pr2 > r2) + { + // ..expand radius just enough to include this point. + const float pr = FMath::Sqrt(pr2); + r = 0.5f * (r + pr); + r2 = FMath::Square(r); + + sphere.Center += ((pr - r) / pr * cToP); + } + } + + sphere.W = r; +} + +void +FHoudiniMeshTranslator::CalcBoundingSphere2(const TArray& PositionArray, FSphere& sphere, FVector& LimitVec) +{ + // + // Code simplified and adapted to work with a simple vector array from GeomFitUtils.cpp + // + + FVector Center, Extents; + CalcBoundingBox(PositionArray, Center, Extents, LimitVec); + + sphere.Center = Center; + sphere.W = 0.0f; + + for (const FVector& curPos : PositionArray) + { + float Dist = FVector::DistSquared(curPos * LimitVec, sphere.Center); + if (Dist > sphere.W) + sphere.W = Dist; + } + sphere.W = FMath::Sqrt(sphere.W); +} + +int32 +FHoudiniMeshTranslator::GenerateSphylAsSimpleCollision(const TArray& InPositionArray, FKAggregateGeom& OutAggregateCollisions) +{ + // + // Code simplified and adapted to work with a simple vector array from GeomFitUtils.cpp + // + + FSphere sphere; + float length; + FRotator rotation; + FVector unitVec = FVector::OneVector; + + // Calculate bounding sphyl. + CalcBoundingSphyl(InPositionArray, sphere, length, rotation, unitVec); + + // Dont use if radius is zero. + if (sphere.W <= 0.f) + { + HOUDINI_LOG_WARNING(TEXT("Failed to generate a simple Capsule collider.")); + return 0; + } + + // If height is zero, then a sphere would be better (should we just create one instead?) + if (length <= 0.f) + { + length = SMALL_NUMBER; + } + + FKSphylElem SphylElem; + SphylElem.Center = sphere.Center; + SphylElem.Rotation = rotation; + SphylElem.Radius = sphere.W; + SphylElem.Length = length; + OutAggregateCollisions.SphylElems.Add(SphylElem); + + return 1; +} + +int32 +FHoudiniMeshTranslator::GenerateOrientedSphylAsSimpleCollision(const TArray& InPositionArray, FKAggregateGeom& OutAggregateCollisions) +{ + // + // Code simplified and adapted to work with a simple vector array from GeomFitUtils.cpp + // + + // + // Code adapted from Experimental GeometryProcessing plugin for simple mesh approximation. + // + + const int32 NumPoints = InPositionArray.Num(); + TArray> Points; + Points.SetNumUninitialized(InPositionArray.Num()); + for(int32 i = 0; i < NumPoints; ++i) + { + Points[i] = Convert(InPositionArray[i]); + } + + houdini::gte::Capsule3 FitCapsule; + const bool bResultValid = GetContainer(NumPoints, Points.GetData(), FitCapsule); + if (!bResultValid) + { + return 0; + } + + houdini::gte::Vector3 GteCenter, GteDirection; + double Extent; + FitCapsule.segment.GetCenteredForm(GteCenter, GteDirection, Extent); + const FVector Direction = Convert(GteDirection); + const FRotator Rot = FRotationMatrix::MakeFromZ(Direction).Rotator(); + + // Dont use if radius is zero. + if (FitCapsule.radius <= 0.f) + { + HOUDINI_LOG_WARNING(TEXT("Failed to generate a simple Capsule collider.")); + return 0; + } + + // If height is zero, then a sphere would be better (should we just create one instead?) + if (Extent <= 0.f) + { + Extent = SMALL_NUMBER; + } + + FKSphylElem SphylElem; + SphylElem.Center = Convert(GteCenter); + SphylElem.Rotation = Rot; + SphylElem.Radius = FitCapsule.radius; + SphylElem.Length = Extent*2.f; + OutAggregateCollisions.SphylElems.Add(SphylElem); + + return 1; +} + +void +FHoudiniMeshTranslator::CalcBoundingSphyl(const TArray& PositionArray, FSphere& sphere, float& length, FRotator& rotation, FVector& LimitVec) +{ + // + // Code simplified and adapted to work with a simple vector array from GeomFitUtils.cpp + // + + if (PositionArray.Num() == 0) + return; + + FVector Center, Extents; + CalcBoundingBox(PositionArray, Center, Extents, LimitVec); + + // @todo sphere.Center could perhaps be adjusted to best fit if model is non-symmetric on it's longest axis + sphere.Center = Center; + + // Work out best axis aligned orientation (longest side) + float Extent = Extents.GetMax(); + if (Extent == Extents.X) + { + rotation = FRotator(90.f, 0.f, 0.f); + Extents.X = 0.0f; + } + else if (Extent == Extents.Y) + { + rotation = FRotator(0.f, 0.f, 90.f); + Extents.Y = 0.0f; + } + else + { + rotation = FRotator(0.f, 0.f, 0.f); + Extents.Z = 0.0f; + } + + // Cleared the largest axis above, remaining determines the radius + float r = Extents.GetMax(); + float r2 = FMath::Square(r); + + // Now check each point lies within this the radius. If not - expand it a bit. + for (const FVector& CurPos : PositionArray) + { + FVector cToP = (CurPos * LimitVec) - sphere.Center; + cToP = rotation.UnrotateVector(cToP); + + const float pr2 = cToP.SizeSquared2D(); // Ignore Z here... + + // If this point is outside our current bounding sphere's radius + if (pr2 > r2) + { + // ..expand radius just enough to include this point. + const float pr = FMath::Sqrt(pr2); + r = 0.5f * (r + pr); + r2 = FMath::Square(r); + } + } + + // The length is the longest side minus the radius. + float hl = FMath::Max(0.0f, Extent - r); + + // Now check each point lies within the length. If not - expand it a bit. + for (const FVector& CurPos : PositionArray) + { + FVector cToP = (CurPos * LimitVec) - sphere.Center; + cToP = rotation.UnrotateVector(cToP); + + // If this point is outside our current bounding sphyl's length + if (FMath::Abs(cToP.Z) > hl) + { + const bool bFlip = (cToP.Z < 0.f ? true : false); + const FVector cOrigin(0.f, 0.f, (bFlip ? -hl : hl)); + + const float pr2 = (cOrigin - cToP).SizeSquared(); + + // If this point is outside our current bounding sphyl's radius + if (pr2 > r2) + { + FVector cPoint; + FMath::SphereDistToLine(cOrigin, r, cToP, (bFlip ? FVector(0.f, 0.f, 1.f) : FVector(0.f, 0.f, -1.f)), cPoint); + + // Don't accept zero as a valid diff when we know it's outside the sphere (saves needless retest on further iterations of like points) + hl += FMath::Max(FMath::Abs(cToP.Z - cPoint.Z), 1.e-6f); + } + } + } + + sphere.W = r; + length = hl * 2.0f; +} + +int32 +FHoudiniMeshTranslator::GenerateKDopAsSimpleCollision(const TArray& InPositionArray, const TArray& Dirs, FKAggregateGeom& OutAggregateCollisions) +{ + // + // Code simplified and adapted to work with a simple vector array from GeomFitUtils.cpp + // + + const float my_flt_max = 3.402823466e+38F; + + // Do k- specific stuff. + int32 kCount = Dirs.Num(); + + TArray maxDist; + maxDist.SetNumUninitialized(kCount); + for (int32 n = 0; n < maxDist.Num(); n++) + maxDist[n] = -my_flt_max; + + // Construct temporary UModel for kdop creation. We keep no refs to it, so it can be GC'd. + auto TempModel = NewObject(); + TempModel->Initialize(nullptr, 1); + + // For each vertex, project along each kdop direction, to find the max in that direction. + for (int32 i = 0; i < InPositionArray.Num(); i++) + { + for (int32 j = 0; j < kCount; j++) + { + float dist = InPositionArray[i] | Dirs[j]; + maxDist[j] = FMath::Max(dist, maxDist[j]); + } + } + + // Inflate kdop to ensure it is no degenerate + const float MinSize = 0.1f; + for (int32 i = 0; i < kCount; i++) + { + maxDist[i] += MinSize; + } + + // Now we have the planes of the kdop, we work out the face polygons. + TArray planes; + for (int32 i = 0; i < kCount; i++) + planes.Add(FPlane4f((FVector3f)Dirs[i], maxDist[i])); + + for (int32 i = 0; i < planes.Num(); i++) + { + FPoly* Polygon = new(TempModel->Polys->Element) FPoly(); + FVector3f Base, AxisX, AxisY; + + Polygon->Init(); + Polygon->Normal = planes[i]; + Polygon->Normal.FindBestAxisVectors(AxisX, AxisY); + + Base = planes[i] * planes[i].W; + + new(Polygon->Vertices) FVector3f(Base + AxisX * UE_OLD_HALF_WORLD_MAX + AxisY * UE_OLD_HALF_WORLD_MAX); + new(Polygon->Vertices) FVector3f(Base + AxisX * UE_OLD_HALF_WORLD_MAX - AxisY * UE_OLD_HALF_WORLD_MAX); + new(Polygon->Vertices) FVector3f(Base - AxisX * UE_OLD_HALF_WORLD_MAX - AxisY * UE_OLD_HALF_WORLD_MAX); + new(Polygon->Vertices) FVector3f(Base - AxisX * UE_OLD_HALF_WORLD_MAX + AxisY * UE_OLD_HALF_WORLD_MAX); + + for (int32 j = 0; j < planes.Num(); j++) + { + if (i != j) + { + if (!Polygon->Split(-FVector3f(planes[j]), planes[j] * planes[j].W)) + { + Polygon->Vertices.Empty(); + break; + } + } + } + + if (Polygon->Vertices.Num() < 3) + { + // If poly resulted in no verts, remove from array + TempModel->Polys->Element.RemoveAt(TempModel->Polys->Element.Num() - 1); + } + else + { + // Other stuff... + Polygon->iLink = i; + Polygon->CalcNormal(1); + } + } + + if (TempModel->Polys->Element.Num() < 4) + { + TempModel = NULL; + HOUDINI_LOG_WARNING(TEXT("Failed to generate a simple KDOP collider.")); + return 0; + } + + // Build bounding box. + TempModel->BuildBound(); + + // Build BSP for the brush. + FBSPOps::bspBuild(TempModel, FBSPOps::BSP_Good, 15, 70, 1, 0); + FBSPOps::bspRefresh(TempModel, 1); + FBSPOps::bspBuildBounds(TempModel); + + // Now, create a temporary BodySetup to build the colliders + UBodySetup* TempBS = NewObject(); + TempBS->CreateFromModel(TempModel, false); + + // Copy the convex elements back to our aggregate + int32 NewConvexElems = 0; + if (TempBS && TempBS->AggGeom.ConvexElems.Num() > 0) + { + for (const auto& CurConvexElem : TempBS->AggGeom.ConvexElems) + { + OutAggregateCollisions.ConvexElems.Add(CurConvexElem); + NewConvexElems++; + } + } + + return NewConvexElems; +} + + +void +FHoudiniMeshTranslator::SetPackageParams(const FHoudiniPackageParams& InPackageParams, const bool& bUpdateHGPO) +{ + PackageParams = InPackageParams; + + if (bUpdateHGPO) + { + PackageParams.ObjectId = HGPO.ObjectId; + PackageParams.GeoId = HGPO.GeoId; + PackageParams.PartId = HGPO.PartId; + } +} + +bool +FHoudiniMeshTranslator::RemoveAndDestroyComponent(UObject* InComponent) +{ + if (!IsValid(InComponent)) + return false; + + USceneComponent* SceneComponent = Cast(InComponent); + if (IsValid(SceneComponent)) + { + // Remove from the HoudiniAssetActor + if (SceneComponent->GetOwner()) + SceneComponent->GetOwner()->RemoveOwnedComponent(SceneComponent); + + SceneComponent->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + SceneComponent->UnregisterComponent(); + SceneComponent->DestroyComponent(); + + return true; + } + + return false; +} + +UMeshComponent* +FHoudiniMeshTranslator::CreateMeshComponent(UObject *InOuterComponent, const TSubclassOf &InComponentType) +{ + // Create a new SMC as we couldn't find an existing one + USceneComponent* OuterSceneComponent = Cast(InOuterComponent); + UObject * Outer = nullptr; + if (IsValid(OuterSceneComponent)) + Outer = OuterSceneComponent->GetOwner() ? OuterSceneComponent->GetOwner() : OuterSceneComponent->GetOuter(); + + UMeshComponent *MeshComponent = NewObject(Outer, InComponentType, NAME_None, RF_Transactional); + + // Initialize it + MeshComponent->SetVisibility(true); + //MeshComponent->SetMobility(Mobility); + + // TODO: + // Property propagation: set the new SMC's properties to the HAC's current settings + //CopyComponentPropertiesTo(MeshComponent); + + UPrimitiveComponent* PrimComponent = Cast(OuterSceneComponent); + if (PrimComponent) + { + // The outer component is a primitive, so we can copy the collision profile from the scene component. + MeshComponent->SetCollisionProfileName(PrimComponent->GetCollisionProfileName()); + } + + // Change the creation method so the component is listed in the details panels + FHoudiniEngineRuntimeUtils::AddOrSetAsInstanceComponent(MeshComponent); + + // Attach created static mesh component to our Houdini component. + MeshComponent->AttachToComponent(OuterSceneComponent, FAttachmentTransformRules::KeepRelativeTransform); + MeshComponent->OnComponentCreated(); + if(MeshComponent->GetOwner()) + MeshComponent->RegisterComponent(); + + return MeshComponent; +} + +bool +FHoudiniMeshTranslator::UpdateMeshOnStaticMeshComponent(UStaticMeshComponent *InComponent, UObject *InMesh) +{ + if (!IsValid(InComponent)) + return false; + + if (UStaticMesh* const Mesh = Cast(InMesh)) + { + if (Mesh != InComponent->GetStaticMesh()) + InComponent->SetStaticMesh(Mesh); + return true; + } + + return false; +} + +bool +FHoudiniMeshTranslator::UpdateMeshOnHoudiniStaticMeshComponent(UHoudiniStaticMeshComponent *InComponent, UObject *InMesh) +{ + if (!IsValid(InComponent)) + return false; + + if (UHoudiniStaticMesh* const Mesh = Cast(InMesh)) + { + if (Mesh != InComponent->GetMesh()) + InComponent->SetMesh(Mesh); + return true; + } + + return false; +} + +UMeshComponent* +FHoudiniMeshTranslator::CreateOrUpdateMeshComponent( + const UHoudiniOutput *InOutput, + UObject *InOuterComponent, + const FHoudiniOutputObjectIdentifier& InOutputIdentifier, + const TSubclassOf& InComponentType, + FHoudiniOutputObject& OutputObject, + FHoudiniGeoPartObject const *& OutFoundHGPO, + bool& bCreated) +{ + bCreated = false; + OutFoundHGPO = nullptr; + + // Find the HGPO that matches this mesh + for (auto& curHGPO : InOutput->HoudiniGeoPartObjects) + { + if (curHGPO.ObjectId != InOutputIdentifier.ObjectId + || curHGPO.GeoId != InOutputIdentifier.GeoId + || curHGPO.PartId != InOutputIdentifier.PartId) + { + continue; + } + + if (InOutputIdentifier.SplitIdentifier.Equals(HAPI_UNREAL_GROUP_GEOMETRY_NOT_COLLISION) + || curHGPO.SplitGroups.Contains(InOutputIdentifier.SplitIdentifier)) + { + OutFoundHGPO = &curHGPO; + break; + } + + if (InOutput->GetType() == EHoudiniOutputType::Skeletal) + { + OutFoundHGPO = &curHGPO; + break; + } + } + + // No need to create a component for instanced static meshes! + if (OutFoundHGPO && OutFoundHGPO->bIsInstanced)// && InOutput->GetType() != EHoudiniOutputType::Skeletal) + return nullptr; + + bool bIsProxyComponent = InComponentType == UHoudiniStaticMeshComponent::StaticClass(); + + // See if we already have a component for that mesh + UMeshComponent* MeshComponent = nullptr; + if (bIsProxyComponent) + { + MeshComponent = Cast(OutputObject.ProxyComponent); + } + else if (OutputObject.OutputComponents.Num() > 0) + { + MeshComponent = Cast(OutputObject.OutputComponents[0]); + } + + // If there is an existing component, but it is pending kill, then it was likely + // deleted by some other process, such as by the user in the editor, so don't use it + if ((!MeshComponent || !IsValid(MeshComponent)) || !MeshComponent->IsA(InComponentType)) + { + // If the component is not of type InComponentType, or the found component is pending kill, destroy + // the existing component (a new one is then created below) + RemoveAndDestroyComponent(MeshComponent); + MeshComponent = nullptr; + } + + if (!MeshComponent) + { + // Create a new SMC/HSMC as we couldn't find an existing one + MeshComponent = CreateMeshComponent(InOuterComponent, InComponentType); + + if (MeshComponent) + { + // Add to the output object + if (bIsProxyComponent) + OutputObject.ProxyComponent = MeshComponent; + else + { + check(OutputObject.OutputComponents.Num() < 2); // Multiple components not supported yet. + OutputObject.OutputComponents.Empty(); + OutputObject.OutputComponents.Add(MeshComponent); + } + + bCreated = true; + } + } + + return MeshComponent; +} + +bool +FHoudiniMeshTranslator::AddActorsToMeshSocket(UStaticMeshSocket * Socket, UStaticMeshComponent * StaticMeshComponent, + TArray> & HoudiniCreatedSocketActors, TArray> & HoudiniAttachedSocketActors) +{ + if (!IsValid(Socket) || !IsValid(StaticMeshComponent)) + return false; + + // The actor to assign is stored is the socket's tag + FString ActorString = Socket->Tag; + if (ActorString.IsEmpty()) + return false; + + // The actor to assign are listed after a | + TArray ActorStringArray; + ActorString.ParseIntoArray(ActorStringArray, TEXT("|"), false); + + // The "real" Tag is the first + if (ActorStringArray.Num() > 0) + Socket->Tag = ActorStringArray[0]; + + // We just add a Tag, no Actor + if (ActorStringArray.Num() == 1) + return false; + + // Extract the parsed actor string to split it further + ActorString = ActorStringArray[1]; + + // Converting the string to a string array using delimiters + const TCHAR* Delims[] = { TEXT(","), TEXT(";") }; + ActorString.ParseIntoArray(ActorStringArray, Delims, 2); + + // And try to find the corresponding HoudiniAssetActor in the editor world + // to avoid finding "deleted" assets with the same name + //UWorld* editorWorld = GEditor->GetEditorWorldContext().World(); +#if WITH_EDITOR + UWorld* EditorWorld = StaticMeshComponent->GetOwner() ? StaticMeshComponent->GetOwner()->GetWorld() : nullptr; + if (!IsValid(EditorWorld)) + return false; + + // Remove the previously created actors which were attached to this socket + { + for (int32 Idx = HoudiniCreatedSocketActors.Num() - 1; Idx >= 0; --Idx) + { + AActor * CurActor = HoudiniCreatedSocketActors[Idx]; + if (!IsValid(CurActor)) + { + HoudiniCreatedSocketActors.RemoveAt(Idx); + continue; + } + + if (CurActor->GetAttachParentSocketName() == Socket->SocketName) + { + HoudiniCreatedSocketActors.RemoveAt(Idx); + CurActor->Destroy(); + } + } + } + + // Detach the previous in level actors which was attached to this socket + { + for (int32 Idx = HoudiniAttachedSocketActors.Num() - 1; Idx >= 0; --Idx) + { + AActor * CurActor = HoudiniAttachedSocketActors[Idx]; + if (!IsValid(CurActor)) + { + HoudiniAttachedSocketActors.RemoveAt(Idx); + continue; + } + + if (CurActor->GetAttachParentSocketName() == Socket->SocketName) + { + CurActor->DetachFromActor(FDetachmentTransformRules::KeepRelativeTransform); + HoudiniAttachedSocketActors.RemoveAt(Idx); + } + } + } + + auto CreateDefaultActor = [EditorWorld, StaticMeshComponent, Socket, HoudiniCreatedSocketActors]() + { + AActor * CreatedDefaultActor = nullptr; + + UStaticMesh * DefaultReferenceSM = FHoudiniEngine::Get().GetHoudiniDefaultReferenceMesh().Get(); + if (IsValid(DefaultReferenceSM)) + { + TArray NewActors = FLevelEditorViewportClient::TryPlacingActorFromObject( + EditorWorld->GetCurrentLevel(), DefaultReferenceSM, false, RF_Transactional, nullptr); + + if (NewActors.Num() <= 0 || !IsValid(NewActors[0])) + { + HOUDINI_LOG_WARNING( + TEXT("Failed to load default mesh.")); + } + else + { + + // Set the default mesh actor components mobility to the same as output SMC's + EComponentMobility::Type OutputSMCMobility = StaticMeshComponent->Mobility; + for (auto & CurComp : NewActors[0]->GetComponents()) + { + UStaticMeshComponent * CurSMC = Cast(CurComp); + if (IsValid(CurSMC)) + CurSMC->SetMobility(OutputSMCMobility); + } + + // Set the default mesh actor hidden in game. + NewActors[0]->SetActorHiddenInGame(true); + + Socket->AttachActor(NewActors[0], StaticMeshComponent); + CreatedDefaultActor = NewActors[0]; + + FVector SocketScale = Socket->RelativeScale; + if (!SocketScale.IsZero() && !SocketScale.Equals(FVector::OneVector)) + { + NewActors[0]->SetActorRelativeScale3D(SocketScale); + } + //HoudiniCreatedSocketActors.Add(NewActors[0]); + } + } + else + { + HOUDINI_LOG_WARNING( + TEXT("Failed to load default mesh.")); + } + + return CreatedDefaultActor; + }; + + // If nothing was specified, we're done + if (ActorStringArray.Num() <= 0) + return true; + + bool bUseDefaultActor = true; + // Get from the Houdini runtime setting if use default object when the reference is invalid + // true by default if fail to access HoudiniRuntimeSettings + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings) + { + bUseDefaultActor = HoudiniRuntimeSettings->bShowDefaultMesh; + } + + /* + // !! Only use the default mesh if we failed to find/spawn the actor to attach + // not if we didn't specify any actor to attach! + if (ActorStringArray.Num() <= 0) + { + if (!bUseDefaultActor) + return true; + + HOUDINI_LOG_WARNING( + TEXT("Output static mesh: Socket '%s' actor is not specified. Spawn a default mesh (hidden in game)."), *(Socket->GetName())); + + AActor * DefaultActor = CreateDefaultActor(); + if (IsValid(DefaultActor)) + HoudiniCreatedSocketActors.Add(DefaultActor); + + return true; + } + */ + + // try to find the actor in level first + for (TActorIterator ActorItr(EditorWorld); ActorItr; ++ActorItr) + { + // Same as with the Object Iterator, access the subclass instance with the * or -> operators. + AActor *Actor = *ActorItr; + if (!IsValid(Actor) || Actor->IsUnreachable()) + continue; + + for (int32 StringIdx = 0; StringIdx < ActorStringArray.Num(); StringIdx++) + { + if (Actor->GetName() != ActorStringArray[StringIdx] + && Actor->GetActorLabel() != ActorStringArray[StringIdx]) + continue; + + // Set the actor components mobility to the same as output SMC's + EComponentMobility::Type OutputSMCMobility = StaticMeshComponent->Mobility; + for (auto & CurComp : Actor->GetComponents()) + { + UStaticMeshComponent * SMC = Cast(CurComp); + if (IsValid(SMC)) + SMC->SetMobility(OutputSMCMobility); + } + + Socket->AttachActor(Actor, StaticMeshComponent); + HoudiniAttachedSocketActors.Add(Actor); + + FVector SocketScale = Socket->RelativeScale; + if (!SocketScale.IsZero() && !SocketScale.Equals(FVector::OneVector)) + { + Actor->SetActorRelativeScale3D(SocketScale); + } + + // Remove the string if the actor is found in the editor level + ActorStringArray.RemoveAt(StringIdx); + break; + } + } + + bool bSuccess = true; + // If some of the actors are not found in the level, try to find them in the content browser. Spawn one if existed + for (int32 Idx = ActorStringArray.Num() - 1; Idx>= 0; --Idx) + { + UObject * Obj = StaticLoadObject(UObject::StaticClass(), nullptr, *ActorStringArray[Idx]); + if (!IsValid(Obj)) + { + bSuccess = false; + continue; + } + + if (!EditorWorld) + { + bSuccess = false; + continue; + } + + // Spawn a new actor with the found object + TArray NewActors = FLevelEditorViewportClient::TryPlacingActorFromObject( + EditorWorld->GetCurrentLevel(), Obj, false, RF_Transactional, nullptr); + + if (NewActors.Num() <= 0 || !IsValid(NewActors[0])) + { + bSuccess = false; + continue; + } + + // Set the new actor components mobility to the same as output SMC's + EComponentMobility::Type OutputSMCMobility = StaticMeshComponent->Mobility; + for (auto & CurComp : NewActors[0]->GetComponents()) + { + UStaticMeshComponent * CurSMC = Cast(CurComp); + if (IsValid(CurSMC)) + CurSMC->SetMobility(OutputSMCMobility); + } + + Socket->AttachActor(NewActors[0], StaticMeshComponent); + HoudiniCreatedSocketActors.Add(NewActors[0]); + + FVector SocketScale = Socket->RelativeScale; + if (!SocketScale.IsZero() && !SocketScale.Equals(FVector::OneVector)) + { + NewActors[0]->SetActorRelativeScale3D(SocketScale); + } + + ActorStringArray.RemoveAt(Idx); + } + + // Failed to find actors in both level and content browser + // Spawn default actors if enabled + if (bUseDefaultActor) + { + for (int32 Idx = ActorStringArray.Num() - 1; Idx >= 0; --Idx) + { + HOUDINI_LOG_WARNING( + TEXT("Output static mesh: Failed to attach '%s' to socket '%s', spawn a default mesh (hidden in game)."), *(ActorStringArray[Idx]), *(Socket->GetName())); + + // If failed to load this object, spawn a default mesh + AActor * CurDefaultActor = CreateDefaultActor(); + if (IsValid(CurDefaultActor)) + HoudiniCreatedSocketActors.Add(CurDefaultActor); + } + } + + if (ActorStringArray.Num() > 0) + return false; +#endif + + return bSuccess; +} + + +bool FHoudiniMeshTranslator::HasFracturePieceAttribute(const HAPI_NodeId& GeoId, const HAPI_NodeId& PartId) +{ + bool bHISM = false; + TArray IntData; + IntData.Empty(); + + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_GC_PIECE); + + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, IntData); + + if (bSuccess) + { + if (IntData.Num() > 0) + bHISM = true; + } + + return bHISM; +} + +void +FHoudiniMeshTranslator::UpdateMeshBuildSettings( + FMeshBuildSettings& OutMeshBuildSettings, + const bool& bHasNormals, + const bool& bHasTangents, + const bool& bHasLightmapUVSet) +{ + // Use the values provided to the translator + OutMeshBuildSettings = StaticMeshBuildSettings; + + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + + // Recomputing normals. + EHoudiniRuntimeSettingsRecomputeFlag RecomputeNormalFlag = HoudiniRuntimeSettings ? (EHoudiniRuntimeSettingsRecomputeFlag)HoudiniRuntimeSettings->RecomputeNormalsFlag : HRSRF_OnlyIfMissing; + if(RecomputeNormalFlag == HRSRF_OnlyIfMissing) + OutMeshBuildSettings.bRecomputeNormals = !bHasNormals; + + // Recomputing tangents. + EHoudiniRuntimeSettingsRecomputeFlag RecomputeTangentFlag = HoudiniRuntimeSettings ? (EHoudiniRuntimeSettingsRecomputeFlag)HoudiniRuntimeSettings->RecomputeTangentsFlag : HRSRF_OnlyIfMissing; + if (RecomputeTangentFlag == HRSRF_OnlyIfMissing) + OutMeshBuildSettings.bRecomputeTangents = !bHasTangents; + + // Lightmap UV generation. + EHoudiniRuntimeSettingsRecomputeFlag GenerateLightmapUVsFlag = HoudiniRuntimeSettings ? (EHoudiniRuntimeSettingsRecomputeFlag)HoudiniRuntimeSettings->GenerateLightmapUVsFlag : HRSRF_OnlyIfMissing; + if (GenerateLightmapUVsFlag == HRSRF_OnlyIfMissing) + OutMeshBuildSettings.bGenerateLightmapUVs = !bHasLightmapUVSet; +} + + +void +FHoudiniMeshTranslator::BuildMeshDescription(FMeshDescription* MeshDescription, FHoudiniGroupedMeshPrimitives& SplitMeshData) +{ + bool bHasNormal = SplitMeshData.Normals.Num() > 0; + bool bHasTangents = SplitMeshData.TangentU.Num() > 0 && SplitMeshData.TangentV.Num() > 0; + bool bHasRGB = SplitMeshData.Colors.Num() > 0; + bool bHasRGBA = bHasRGB && AttribInfoColors.tupleSize == 4; + bool bHasAlpha = SplitMeshData.Alphas.Num() > 0; + int UVSetCount = PartUVSets.Num(); + uint32 FaceCount = SplitMeshData.Indices.Num() / 3; + + // Create a Polygon Group for each material slot + TPolygonGroupAttributesRef PolygonGroupImportedMaterialSlotNames = + MeshDescription->PolygonGroupAttributes().GetAttributesRef(MeshAttribute::PolygonGroup::ImportedMaterialSlotName); + + // We must use the number of assignment materials found to reserve the number of material slots + // Don't use the SM's StaticMaterials here as we may not reserve enough polygon groups when adding more materials + // Create a polygon group for each material slot. + int32 NumberOfMaterials = OutputAssignmentMaterials.Num(); + if (NumberOfMaterials <= 0) + { + // No materials, create a polygon group for the default one + const FPolygonGroupID& PolygonGroupID = MeshDescription->CreatePolygonGroup(); + PolygonGroupImportedMaterialSlotNames[PolygonGroupID] = FName(HAPI_UNREAL_DEFAULT_MATERIAL_NAME); + } + else + { + MeshDescription->ReserveNewPolygonGroups(NumberOfMaterials); + //for (int32 MatIndex = 0; MatIndex < NumberOfMaterials; ++MatIndex) + for (auto& CurrentMatAssignement : OutputAssignmentMaterials) + { + const FPolygonGroupID& PolygonGroupID = MeshDescription->CreatePolygonGroup(); + PolygonGroupImportedMaterialSlotNames[PolygonGroupID] = + FName(CurrentMatAssignement.Value ? *(CurrentMatAssignement.Value->GetName()) : *(CurrentMatAssignement.Key.MaterialObjectPath)); + } + } + + TVertexAttributesRef VertexPositions = MeshDescription->VertexAttributes().GetAttributesRef(MeshAttribute::Vertex::Position); + + bool bHasInvalidPositionIndexData = false; + MeshDescription->ReserveNewVertices(SplitMeshData.NeededVertices.Num()); + for (const int32& NeededVertexIndex : SplitMeshData.NeededVertices) + { + // Create a new Vertex + FVertexID VertexID = MeshDescription->CreateVertex(); + if (PartPositions.IsValidIndex(NeededVertexIndex * 3 + 2)) + { + // We need to swap Z and Y coordinate here, and convert from m to cm. + VertexPositions[VertexID].X = PartPositions[NeededVertexIndex * 3 + 0] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + VertexPositions[VertexID].Y = PartPositions[NeededVertexIndex * 3 + 2] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + VertexPositions[VertexID].Z = PartPositions[NeededVertexIndex * 3 + 1] * HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + else + { + // Error when retrieving positions. + bHasInvalidPositionIndexData = true; + + continue; + } + } + + if (bHasInvalidPositionIndexData) + { + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%d %s] invalid position/index data ") + TEXT("- skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, SplitMeshData.SplitId, *SplitMeshData.SplitGroupName); + } + + //--------------------------------------------------------------------------------------------------------------------- + // BUILD GEOMETRY (FACE/VERTICES) + //--------------------------------------------------------------------------------------------------------------------- + + MeshDescription->ReserveNewVertexInstances(SplitMeshData.Indices.Num()); + MeshDescription->ReserveNewPolygons(SplitMeshData.Indices.Num() / 3); + //Approximately 2.5 edges per polygons + MeshDescription->ReserveNewEdges(SplitMeshData.Indices.Num() * 2.5f / 3); + + TVertexInstanceAttributesRef VertexInstanceNormals = MeshDescription->VertexInstanceAttributes().GetAttributesRef(MeshAttribute::VertexInstance::Normal); + TVertexInstanceAttributesRef VertexInstanceTangents = MeshDescription->VertexInstanceAttributes().GetAttributesRef(MeshAttribute::VertexInstance::Tangent); + TVertexInstanceAttributesRef VertexInstanceBinormalSigns = MeshDescription->VertexInstanceAttributes().GetAttributesRef(MeshAttribute::VertexInstance::BinormalSign); + TVertexInstanceAttributesRef VertexInstanceColors = MeshDescription->VertexInstanceAttributes().GetAttributesRef(MeshAttribute::VertexInstance::Color); + TVertexInstanceAttributesRef VertexInstanceUVs = MeshDescription->VertexInstanceAttributes().GetAttributesRef(MeshAttribute::VertexInstance::TextureCoordinate); + VertexInstanceUVs.SetNumChannels(UVSetCount); + + // Bug #147854 + // Use SplitUVs here - as that where we'll read UVs from + TArray HasUVSets; + HasUVSets.SetNumZeroed(SplitMeshData.UVSets.Num()); + for (int32 Idx = 0; Idx < SplitMeshData.UVSets.Num(); Idx++) + HasUVSets[Idx] = SplitMeshData.UVSets[Idx].Num() > 0; + + for (uint32 FaceIndex = 0; FaceIndex < FaceCount; FaceIndex++) + { + TArray FaceVertexInstanceIDs; + FaceVertexInstanceIDs.SetNum(3); + + // Ignore degenerate triangles + FVertexID VertexIDs[3]; + for (int32 Corner = 0; Corner < 3; ++Corner) + { + VertexIDs[Corner] = FVertexID(SplitMeshData.Indices[(FaceIndex * 3) + Corner]); + } + if (VertexIDs[0] == VertexIDs[1] || VertexIDs[0] == VertexIDs[2] || VertexIDs[1] == VertexIDs[2]) + continue; + + //FVertexID FaceVertexIDs[3]; + for (int32 Corner = 0; Corner < 3; Corner++) + { + uint32 SplitIndex = (FaceIndex * 3) + Corner; + uint32 SplitVertexIndex = SplitMeshData.Indices[SplitIndex]; + const FVertexInstanceID& VertexInstanceID = MeshDescription->CreateVertexInstance(FVertexID(SplitVertexIndex)); + + // Fix the winding order by updating the SplitIndex (invert corner 1 and 2) + // instead of going 0 1 2 go 0 2 1 + // TODO; this slows down StaticMesh->Build() considerably! + Corner == 1 ? SplitIndex++ : Corner == 2 ? SplitIndex-- : SplitIndex; + + const uint32 SplitVertexIndex_X = SplitIndex * 3 + 0; + const uint32 SplitVertexIndex_Y = SplitIndex * 3 + 2; + const uint32 SplitVertexIndex_Z = SplitIndex * 3 + 1; + // Normals + if (bHasNormal) + { + // We need to swap Z and Y coordinate here, and convert from m to cm. + VertexInstanceNormals[VertexInstanceID].X = SplitMeshData.Normals[SplitVertexIndex_X]; + VertexInstanceNormals[VertexInstanceID].Y = SplitMeshData.Normals[SplitVertexIndex_Y]; + VertexInstanceNormals[VertexInstanceID].Z = SplitMeshData.Normals[SplitVertexIndex_Z]; + } + + // Tangents and binormals + if (bHasTangents) + { + // We need to swap Z and Y coordinate here, and convert from m to cm. + VertexInstanceTangents[VertexInstanceID].X = SplitMeshData.TangentU[SplitVertexIndex_X]; + VertexInstanceTangents[VertexInstanceID].Y = SplitMeshData.TangentU[SplitVertexIndex_Y]; + VertexInstanceTangents[VertexInstanceID].Z = SplitMeshData.TangentU[SplitVertexIndex_Z]; + + FVector3f TangentY; + TangentY.X = SplitMeshData.TangentV[SplitVertexIndex_X]; + TangentY.Y = SplitMeshData.TangentV[SplitVertexIndex_Y]; + TangentY.Z = SplitMeshData.TangentV[SplitVertexIndex_Z]; + + VertexInstanceBinormalSigns[VertexInstanceID] = GetBasisDeterminantSign( + (FVector)VertexInstanceTangents[VertexInstanceID].GetSafeNormal(), + (FVector)TangentY.GetSafeNormal(), + (FVector)VertexInstanceNormals[VertexInstanceID].GetSafeNormal()); + } + + // Color + FLinearColor Color = FLinearColor::White; + if (bHasRGB) + { + Color.R = FMath::Clamp(SplitMeshData.Colors[SplitIndex * AttribInfoColors.tupleSize + 0], 0.0f, 1.0f); + Color.G = FMath::Clamp(SplitMeshData.Colors[SplitIndex * AttribInfoColors.tupleSize + 1], 0.0f, 1.0f); + Color.B = FMath::Clamp(SplitMeshData.Colors[SplitIndex * AttribInfoColors.tupleSize + 2], 0.0f, 1.0f); + } + // Alpha + if (bHasAlpha) + { + Color.A = FMath::Clamp(SplitMeshData.Alphas[SplitIndex], 0.0f, 1.0f); + } + else if (bHasRGBA) + { + Color.A = FMath::Clamp(SplitMeshData.Colors[SplitIndex * AttribInfoColors.tupleSize + 3], 0.0f, 1.0f); + } + VertexInstanceColors[VertexInstanceID] = FVector4f(Color); + + // UVs + for (int32 UVIndex = 0; UVIndex < SplitMeshData.UVSets.Num(); UVIndex++) + { + if (HasUVSets[UVIndex]) + { + // We need to flip V coordinate when it's coming from HAPI. + FVector2f CurrentUV; + CurrentUV.X = SplitMeshData.UVSets[UVIndex][SplitIndex * 2 + 0]; + CurrentUV.Y = 1.0f - SplitMeshData.UVSets[UVIndex][SplitIndex * 2 + 1]; + + VertexInstanceUVs.Set(VertexInstanceID, UVIndex, CurrentUV); + } + } + + FaceVertexInstanceIDs[Corner] = VertexInstanceID; + } + + const FPolygonGroupID PolygonGroupID(SplitMeshData.FaceMaterialIndices[FaceIndex]); + + // Insert a triangle into the mesh + MeshDescription->CreateTriangle(PolygonGroupID, FaceVertexInstanceIDs); + } + + //--------------------------------------------------------------------------------------------------------------------- + // FACE SMOOTHING + //--------------------------------------------------------------------------------------------------------------------- + + // Check that the number of face smoothing values we retrieved is correct + int32 WedgeFaceSmoothCount = SplitMeshData.FaceSmoothingMasks.Num() / 3; + + // Get valid count of vertex indices for this split. + const int32 SplitVertexCount = AllSplitVertexCounts[SplitMeshData.SplitGroupName]; + + + // FaceSmoothing masks must be initialized even if we don't have a value from Houdini! + // TODO: Expose the default FaceSmoothing value 0 will make hard face + TArray FaceSmoothingMasks; + FaceSmoothingMasks.SetNumUninitialized(SplitVertexCount); + for (int32 n = 0; n < FaceSmoothingMasks.Num(); n++) + FaceSmoothingMasks[n] = DefaultMeshSmoothing; + + + if (SplitMeshData.FaceSmoothingMasks.Num() != 0 && !SplitMeshData.FaceSmoothingMasks.IsValidIndex((WedgeFaceSmoothCount - 1) * 3 + 2)) + { + // Ignore our face smoothing values + WedgeFaceSmoothCount = 0; + HOUDINI_LOG_WARNING(TEXT("Invalid face smoothing mask count detected - Skipping them.")); + } + + // Transfer the face smoothing masks to the raw mesh if we have any + for (int32 WedgeFaceSmoothIdx = 0; WedgeFaceSmoothIdx < WedgeFaceSmoothCount; WedgeFaceSmoothIdx += 3) + { + FaceSmoothingMasks[WedgeFaceSmoothIdx] = SplitMeshData.FaceSmoothingMasks[WedgeFaceSmoothIdx * 3]; + } + + FStaticMeshOperations::ConvertSmoothGroupToHardEdges(FaceSmoothingMasks, *MeshDescription); +} + +void FHoudiniMeshTranslator::ProcessMaterials(UStaticMesh* FoundStaticMesh, FHoudiniGroupedMeshPrimitives& SplitMeshData) +{ + // Map of Houdini Material IDs to Unreal Material Interface + TMap> MapHoudiniMatIdToUnrealInterface; + // Map of Houdini Material Attributes to Unreal Material Interface + TMap> MapHoudiniMatAttributesToUnrealInterface; + // Map of Unreal Material Interface to Unreal Material Index, per visible mesh + TMap, int32>> MapUnrealMaterialInterfaceToUnrealIndexPerMesh; + + //ONEONE + + TArray& FoundStaticMaterials = FoundStaticMesh->GetStaticMaterials(); + + // // TODO: Check if still needed for MeshDescription + // // We need to reset the Static Mesh's materials once per SM: + // // so, for the first lod, or the main geo... + // if (!MeshMaterialsHaveBeenReset && (SplitType == EHoudiniSplitType::LOD || SplitType == EHoudiniSplitType::Normal)) + // { + // FoundStaticMaterials.Empty(); + // MeshMaterialsHaveBeenReset = true; + // } + // + // // .. or for each visible complex collider + // if (SplitType == EHoudiniSplitType::RenderedComplexCollider) + // FoundStaticMaterials.Empty(); + + // Clear the materials array of the mesh the first time we encounter it + if (!MapUnrealMaterialInterfaceToUnrealIndexPerMesh.Contains(FoundStaticMesh)) + { + FoundStaticMaterials.Empty(); + } + TMap, int32>& MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh = MapUnrealMaterialInterfaceToUnrealIndexPerMesh.FindOrAdd(FoundStaticMesh); + + // Get this split's faces + TArray& SplitGroupFaceIndices = AllSplitFaceIndices[SplitMeshData.SplitGroupName]; + // Array holding the materials needed for this split + //TArray SplitMaterials; + // Split Material indices per face, by default all faces are set to use the first Material + + SplitMeshData.FaceMaterialIndices.SetNumZeroed(SplitGroupFaceIndices.Num()); + + bool HasHoudiniMaterials = PartUniqueMaterialIds.Num() > 0; + bool HasMaterialOverrides = PartFaceMaterialOverrides.Num() > 0; + if (!HasHoudiniMaterials && !HasMaterialOverrides) + { + // We don't have any material override or houdini material + // we just need one polygon group using the default Houdini material. + UMaterialInterface* MaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get()); + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterial = ReplacementMaterials.Find( + FHoudiniMaterialIdentifier(HAPI_UNREAL_DEFAULT_MATERIAL_NAME, false, "")); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + FoundStaticMaterials.Empty(); + FoundStaticMaterials.Add(MaterialInterface); + + // TODO: ? Add default mat to the assignement map? + } + else if (HasHoudiniMaterials && !HasMaterialOverrides) + { + // We have Houdini Material but no overrides + if (bOnlyOneFaceMaterial || PartUniqueMaterialIds.Num() == 1) + { + // We have only one Houdini material. + // Use default Houdini material if no valid material is assigned to any of the faces. + UMaterialInterface* MaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get()); + + // Get id of this single material. + FString MaterialPathName = HAPI_UNREAL_DEFAULT_MATERIAL_NAME; + const bool bFoundHoudiniMaterial = FHoudiniMaterialTranslator::GetMaterialRelativePath(HGPO.AssetId, PartFaceMaterialIds[0], MaterialPathName); + const FHoudiniMaterialIdentifier DefaultMatIdentifier(MaterialPathName, bFoundHoudiniMaterial); + TObjectPtr* FoundMaterial = OutputAssignmentMaterials.Find(DefaultMatIdentifier); + if (FoundMaterial) + MaterialInterface = *FoundMaterial; + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterial = ReplacementMaterials.Find(DefaultMatIdentifier); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + FoundStaticMaterials.Empty(); + FoundStaticMaterials.Add(MaterialInterface); + + // TODO: ? Add the mat to the assignement map? + } + else + { + // We have multiple houdini materials + // Get default Houdini material. + UMaterial* MaterialDefault = FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get(); + + // Reset material face assignments. + for (int32 FaceIdx = 0; FaceIdx < SplitGroupFaceIndices.Num(); ++FaceIdx) + { + int32 SplitFaceIndex = SplitGroupFaceIndices[FaceIdx]; + if (!PartFaceMaterialIds.IsValidIndex(SplitFaceIndex)) + continue; + + // Get material id for this face. + HAPI_NodeId MaterialId = PartFaceMaterialIds[SplitFaceIndex]; + + // See if we have already treated that material + TObjectPtr* FoundMaterialInterface = MapHoudiniMatIdToUnrealInterface.Find(MaterialId); + UMaterialInterface* MaterialInterface = nullptr; + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + if (MaterialInterface) + { + int32 const* FoundUnrealMatIndex = MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh.Find(MaterialInterface); + if (FoundUnrealMatIndex) + { + // This material has been mapped already, just assign the mat index + SplitMeshData.FaceMaterialIndices[FaceIdx] = *FoundUnrealMatIndex; + continue; + } + } + else + { + MaterialInterface = Cast(MaterialDefault); + + FString MaterialPathName = HAPI_UNREAL_DEFAULT_MATERIAL_NAME; + const bool bFoundHoudiniMaterial = FHoudiniMaterialTranslator::GetMaterialRelativePath(HGPO.AssetId, MaterialId, MaterialPathName); + const FHoudiniMaterialIdentifier DefaultMatIdentifier(MaterialPathName, bFoundHoudiniMaterial); + TObjectPtr* FoundMaterial = OutputAssignmentMaterials.Find(DefaultMatIdentifier); + if (FoundMaterial) + MaterialInterface = *FoundMaterial; + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterial = ReplacementMaterials.Find(DefaultMatIdentifier); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + MapHoudiniMatIdToUnrealInterface.Add(MaterialId, MaterialInterface); + } + + if (MaterialInterface) + { + // Add the material to the Static mesh + //int32 UnrealMatIndex = SplitMaterials.Add(Material); + int32 UnrealMatIndex = FoundStaticMaterials.Add(MaterialInterface); + + // Map the houdini ID to the unreal one + MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh.Add(MaterialInterface, UnrealMatIndex); + + // Update the face index + SplitMeshData.Indices[FaceIdx] = UnrealMatIndex; + } + } + } + } + else + { + // Array used to avoid constantly attempting to load invalid materials + TArray InvalidMaterials; + + // If we have material overrides + for (int32 FaceIdx = 0; FaceIdx < SplitGroupFaceIndices.Num(); ++FaceIdx) + { + int32 SplitFaceIndex = SplitGroupFaceIndices[FaceIdx]; + + UMaterialInterface* MaterialInterface = nullptr; + int32 CurrentFaceMaterialIdx = -1; + int32 MatIndex = -1; + if (PartFaceMaterialOverrides.IsValidIndex(SplitFaceIndex)) + { + // For MeshDescription specifically, we need OutputAssignmentMaterials/MapHoudiniMatAttributesToUnrealInterface + // to have an entry for each index (distinguish between identical materials in different slots). + // This is so we have the correct size and create enough polygon groups later. + const FHoudiniMaterialInfo& MatInfo = PartFaceMaterialOverrides[SplitFaceIndex]; + const FHoudiniMaterialIdentifier MaterialIdentifier = MatInfo.MakeIdentifier(); + FString MaterialName = MatInfo.MaterialObjectPath; + ExtractMaterialIndex(MaterialName, MatIndex); + TObjectPtr* FoundMaterialInterface = MapHoudiniMatAttributesToUnrealInterface.Find(MaterialIdentifier); + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + if (!MaterialInterface) + { + // Try to locate the corresponding material interface + + // Start by looking in our assignment map + FoundMaterialInterface = OutputAssignmentMaterials.Find(MaterialIdentifier); + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + if (!MaterialInterface && !MaterialName.IsEmpty() && !InvalidMaterials.Contains(MaterialName)) + { + // Only try to load a material if has a chance to be valid! + MaterialInterface = Cast< UMaterialInterface >( + StaticLoadObject(UMaterialInterface::StaticClass(), + nullptr, *MaterialName, nullptr, LOAD_NoWarn, nullptr)); + + if (!MaterialInterface) + InvalidMaterials.Add(MaterialName); + } + + if (MaterialInterface) + { + // We managed to load the UE4 material + // Make sure this material is in the assignments before replacing it. + OutputAssignmentMaterials.Add(MaterialIdentifier, MaterialInterface); + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterialInterface = ReplacementMaterials.Find(MaterialIdentifier); + if (ReplacementMaterialInterface && *ReplacementMaterialInterface) + MaterialInterface = *ReplacementMaterialInterface; + + // Add this material to the map + MapHoudiniMatAttributesToUnrealInterface.Add(MaterialIdentifier, MaterialInterface); + } + } + + if (!MaterialInterface) + { + // The attribute Material or its replacement do not exist + // See if we can fallback to the Houdini material assigned on the face + + // Get the unreal material corresponding to this houdini one + HAPI_NodeId MaterialId = PartFaceMaterialIds[SplitFaceIndex]; + + // See if we have already treated that material + FoundMaterialInterface = MapHoudiniMatIdToUnrealInterface.Find(MaterialId); + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + if (!MaterialInterface) + { + // If everything else fails, we'll use the default material + MaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get()); + + // We need to add this material to the map + FString MaterialPathName = HAPI_UNREAL_DEFAULT_MATERIAL_NAME; + const bool bFoundHoudiniMaterial = FHoudiniMaterialTranslator::GetMaterialRelativePath(HGPO.AssetId, MaterialId, MaterialPathName); + const FHoudiniMaterialIdentifier DefaultMatIdentifier(MaterialPathName, bFoundHoudiniMaterial); + TObjectPtr* FoundMaterial = OutputAssignmentMaterials.Find(DefaultMatIdentifier); + if (FoundMaterial) + MaterialInterface = *FoundMaterial; + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterialInterface = ReplacementMaterials.Find(DefaultMatIdentifier); + if (ReplacementMaterialInterface && *ReplacementMaterialInterface) + MaterialInterface = *ReplacementMaterialInterface; + + // Map the Houdini ID to the unreal one + MapHoudiniMatIdToUnrealInterface.Add(MaterialId, MaterialInterface); + } + } + } + + CurrentFaceMaterialIdx = GetFaceMaterialIndex(MaterialInterface, + MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh, + MatIndex, + FoundStaticMaterials); + + // Update the Face Material on the mesh + SplitMeshData.FaceMaterialIndices[FaceIdx] = CurrentFaceMaterialIdx; + } + } +} + +void FHoudiniMeshTranslator::PullMeshData(FHoudiniGroupedMeshPrimitives& SplitMeshData, UStaticMesh* FoundStaticMesh, int LODIndex, bool bReadTangents) +{ + auto tick = FPlatformTime::Seconds(); + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + + // Mesh description uses material to create its PolygonGroups, + // so we first need to know how many different materials we have for this split + // and what vertices/indices belong to each material for remapping + + //--------------------------------------------------------------------------------------------------------------------- + // INDICES + //--------------------------------------------------------------------------------------------------------------------- + + // + // Because of the splits, we don't need to declare all the vertices in the Part, + // but only the one that are currently used by the split's faces. + // The indicesMapper array is used to map those indices from Part Vertices to Split Vertices. + // We also keep track of the needed vertices index to declare them easily afterwards. + // + + // IndicesMapper: + // Maps index values for all vertices in the Part: + // - Vertices unused by the split will be set to -1 + // - Used vertices will have their value set to the "NewIndex" so that IndicesMapper[ partIndex ] => splitIndex + TArray PartToSplitIndicesMapper; + PartToSplitIndicesMapper.SetNumUninitialized(SplitMeshData.VertexList.Num()); + for (int32 n = 0; n < PartToSplitIndicesMapper.Num(); n++) + PartToSplitIndicesMapper[n] = -1; + + // SplitIndices + // Array of SplitIndices used to describe this split's polygons + + const int32 SplitVertexCount = AllSplitVertexCounts[SplitMeshData.SplitGroupName]; + SplitMeshData.Indices.SetNumZeroed(SplitVertexCount); + + int32 CurrentSplitIndex = 0; + int32 ValidVertexId = 0; + bool bHasInvalidFaceIndices = false; + for (int32 VertexIdx = 0; VertexIdx < SplitMeshData.VertexList.Num(); VertexIdx += 3) + { + int32 WedgeCheck = SplitMeshData.VertexList[VertexIdx + 0]; + if (WedgeCheck == -1) + continue; + + int32 WedgeIndices[3] = + { + SplitMeshData.VertexList[VertexIdx + 0], + SplitMeshData.VertexList[VertexIdx + 1], + SplitMeshData.VertexList[VertexIdx + 2] + }; + + // Ensure the indices are valid + if (!PartToSplitIndicesMapper.IsValidIndex(WedgeIndices[0]) + || !PartToSplitIndicesMapper.IsValidIndex(WedgeIndices[1]) + || !PartToSplitIndicesMapper.IsValidIndex(WedgeIndices[2])) + { + // Invalid face index. + bHasInvalidFaceIndices = true; + continue; + } + + // Converting Old (Part) Indices to New (Split) Indices: + for (int32 i = 0; i < 3; i++) + { + if (PartToSplitIndicesMapper[WedgeIndices[i]] < 0) + { + // This part index has not yet been "converted" to a new split index + SplitMeshData.NeededVertices.Add(WedgeIndices[i]); + PartToSplitIndicesMapper[WedgeIndices[i]] = CurrentSplitIndex; + CurrentSplitIndex++; + } + + // Replace the old part index with the new split index + WedgeIndices[i] = PartToSplitIndicesMapper[WedgeIndices[i]]; + } + + if (!SplitMeshData.Indices.IsValidIndex(ValidVertexId + 2)) + break; + + // Flip wedge indices to fix the winding order. + SplitMeshData.Indices[ValidVertexId + 0] = WedgeIndices[0]; + SplitMeshData.Indices[ValidVertexId + 1] = WedgeIndices[2]; + SplitMeshData.Indices[ValidVertexId + 2] = WedgeIndices[1]; + + ValidVertexId += 3; + } + if (bHasInvalidFaceIndices) + { + HOUDINI_LOG_MESSAGE( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%d %s] has some invalid face indices"), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, SplitMeshData.SplitId, *SplitMeshData.SplitGroupName); + } + if (SplitMeshData.Indices.Num() == 0) + { + HOUDINI_LOG_WARNING( + TEXT("[CreateStaticMesh_MeshDescription]: 0 valid triangles in StaticMesh data for %s LOD %i! Please check the log."), + *SplitMeshData.SplitGroupName, LODIndex); + } + + //--------------------------------------------------------------------------------------------------------------------- + // POSITIONS + //--------------------------------------------------------------------------------------------------------------------- + + UpdatePartPositionIfNeeded(); + + //--------------------------------------------------------------------------------------------------------------------- + // MATERIALS + //--------------------------------------------------------------------------------------------------------------------- + + ProcessMaterials(FoundStaticMesh, SplitMeshData); + + + if (bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMesh_MeshDescription() - Materials in %f seconds."), FPlatformTime::Seconds() - tick); + tick = FPlatformTime::Seconds(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // NORMALS + //--------------------------------------------------------------------------------------------------------------------- + + // Extract the normals + UpdatePartNormalsIfNeeded(); + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices(SplitMeshData.VertexList, AttribInfoNormals, PartNormals, SplitMeshData.Normals); + + //--------------------------------------------------------------------------------------------------------------------- + // TANGENTS + //--------------------------------------------------------------------------------------------------------------------- + + if (bReadTangents) + { + // Extract this part's Tangents if needed + UpdatePartTangentsIfNeeded(); + + // Get the Tangents and binormals for this split + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices(SplitMeshData.VertexList, AttribInfoTangentU, PartTangentU, SplitMeshData.TangentU); + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices(SplitMeshData.VertexList, AttribInfoTangentV, PartTangentV, SplitMeshData.TangentV); + + // We need to manually generate tangents if: + // - we have normals but dont have tangentu or tangentv attributes + // - we have not specified that we wanted unreal to generate them + int32 NormalCount = SplitMeshData.Normals.Num(); + bool bGenerateTangents = (NormalCount > 0) && (SplitMeshData.TangentU.Num() <= 0 || SplitMeshData.TangentV.Num() <= 0); + // Check that the number of tangents read matches the number of normals + if (SplitMeshData.TangentU.Num() != NormalCount || SplitMeshData.TangentV.Num() != NormalCount) + bGenerateTangents = true; + + if (bGenerateTangents && (HoudiniRuntimeSettings->RecomputeTangentsFlag == EHoudiniRuntimeSettingsRecomputeFlag::HRSRF_Always)) + { + // No need to generate tangents if we want unreal to recompute them after + bGenerateTangents = false; + } + + // Generate the tangents if needed + if (bGenerateTangents) + { + SplitMeshData.TangentU.SetNumZeroed(NormalCount); + SplitMeshData.TangentV.SetNumZeroed(NormalCount); + for (int32 Idx = 0; Idx + 2 < NormalCount; Idx += 3) + { + FVector3f TangentZ; + TangentZ.X = SplitMeshData.Normals[Idx + 0]; + TangentZ.Y = SplitMeshData.Normals[Idx + 2]; + TangentZ.Z = SplitMeshData.Normals[Idx + 1]; + + FVector3f TangentX, TangentY; + TangentZ.FindBestAxisVectors(TangentX, TangentY); + + SplitMeshData.TangentU[Idx + 0] = TangentX.X; + SplitMeshData.TangentU[Idx + 2] = TangentX.Y; + SplitMeshData.TangentU[Idx + 1] = TangentX.Z; + + SplitMeshData.TangentV[Idx + 0] = TangentY.X; + SplitMeshData.TangentV[Idx + 2] = TangentY.Y; + SplitMeshData.TangentV[Idx + 1] = TangentY.Z; + } + } + } + + //--------------------------------------------------------------------------------------------------------------------- + // COLORS + //--------------------------------------------------------------------------------------------------------------------- + + UpdatePartColorsIfNeeded(); + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices(SplitMeshData.VertexList, AttribInfoColors, PartColors, SplitMeshData.Colors); + + //--------------------------------------------------------------------------------------------------------------------- + // ALPHA + //--------------------------------------------------------------------------------------------------------------------- + + UpdatePartAlphasIfNeeded(); + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices(SplitMeshData.VertexList, AttribInfoAlpha, PartAlphas, SplitMeshData.Alphas); + + //--------------------------------------------------------------------------------------------------------------------- + // UV SETS + //--------------------------------------------------------------------------------------------------------------------- + + UpdatePartUVSetsIfNeeded(true); + // See if we need to transfer uv point attributes to vertex attributes. + int32 UVSetCount = PartUVSets.Num(); + + SplitMeshData.UVSets.SetNum(UVSetCount); + for (int32 TexCoordIdx = 0; TexCoordIdx < UVSetCount; TexCoordIdx++) + { + FHoudiniMeshTranslator::TransferPartAttributesToSplit(SplitMeshData.VertexList, AttribInfoUVSets[TexCoordIdx], PartUVSets[TexCoordIdx], SplitMeshData.UVSets[TexCoordIdx]); + } + + //--------------------------------------------------------------------------------------------------------------------- + // FACE SMOOTHING + //--------------------------------------------------------------------------------------------------------------------- + + UpdatePartFaceSmoothingIfNeeded(); + FHoudiniMeshTranslator::TransferPartAttributesToSplit(SplitMeshData.VertexList, AttribInfoFaceSmoothingMasks, PartFaceSmoothingMasks, SplitMeshData.FaceSmoothingMasks); + +} + +void +FHoudiniMeshTranslator::SetPhysicsMaterialFromHGPO(UBodySetup* BodySetup) +{ + // Set physical material if present + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + + TArray AttributeValues; + + FHoudiniHapiAccessor Accessor(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_SIMPLE_PHYSICAL_MATERIAL); + Accessor.GetInfo(AttributeInfo, HAPI_ATTROWNER_PRIM); + AttributeInfo.tupleSize = 1; + bool bSuccess = Accessor.GetAttributeData(AttributeInfo, AttributeValues, 0, 1); + + if (bSuccess && AttributeValues.Num() > 0) + { + // Fetch the physics material name based off the first primitve attribute + auto& MaterialName = AttributeValues[0]; + if (!MaterialName.IsEmpty() && MaterialName != "None") + { + BodySetup->PhysMaterial = LoadObject(nullptr, *MaterialName, nullptr, LOAD_NoWarn, nullptr); + if (!BodySetup->PhysMaterial) + { + HOUDINI_LOG_HELPER(Error, TEXT("Physical Material not found: %s."), *MaterialName); + } + } + } + +}; + +bool FHoudiniMeshTranslator::ParseSplitToken(FString & Name, const FString & Token) +{ + // See if the Name matches the token exacly or token followed by an underscore. + + FString UnderscoreToken = Token + TEXT("_"); + if (Name.RemoveFromStart(UnderscoreToken)) + { + return true; + } + else if (Name.Equals(Token, ESearchCase::IgnoreCase)) + { + Name.Empty(); + return true; + } + else + { + return false; + } +} + +void FHoudiniMeshTranslator::ClassifySplitGroup(FHoudiniGroupedMeshPrimitives& SplitMeshData) +{ + // This function takes the Houdini name of the split group and classifies as renderable, + // lod, collision or a combination of all. + + FString Name = SplitMeshData.SplitGroupName; + + bool bFoundRender = false; + bool bFoundCollision = false; + + SplitMeshData.bRendered = false; + SplitMeshData.CollisionType = EHoudiniCollisionType::None; + + if (ParseSplitToken(Name, HAPI_UNREAL_GROUP_RENDERED_PREFIX)) + { + bFoundRender = true; + SplitMeshData.bIsLOD = false; + SplitMeshData.bRendered = true; + } + + if (Name.RemoveFromStart(HAPI_UNREAL_GROUP_LOD_PREFIX)) + { + bFoundRender = true; + SplitMeshData.bRendered = true; + SplitMeshData.bIsLOD = true; + + // New For 2024! Anything after the _ is a split name. + FString First, Last; + Name.Split(TEXT("_"), &First, &Last); + Name = Last; + } + + if (ParseSplitToken(Name, TEXT("collision_geo"))) + { + bFoundCollision = true; + + if (ParseSplitToken(Name, "simple")) + { + if (ParseSplitToken(Name, "sphere")) + SplitMeshData.CollisionType = EHoudiniCollisionType::SimpleSphere; + if (ParseSplitToken(Name, "box")) + SplitMeshData.CollisionType = EHoudiniCollisionType::SimpleBox; + if (ParseSplitToken(Name, "capsule")) + SplitMeshData.CollisionType = EHoudiniCollisionType::SimpleCapsule; + if (ParseSplitToken(Name, "kdop10x")) + SplitMeshData.CollisionType = EHoudiniCollisionType::Kdop10x; + if (ParseSplitToken(Name, "kdop10y")) + SplitMeshData.CollisionType = EHoudiniCollisionType::Kdop10y; + if (ParseSplitToken(Name, "xdop10z")) + SplitMeshData.CollisionType = EHoudiniCollisionType::Kdop10z; + if (ParseSplitToken(Name, "kdop18")) + SplitMeshData.CollisionType = EHoudiniCollisionType::Kdop18; + if (ParseSplitToken(Name, "xkop26")) + SplitMeshData.CollisionType = EHoudiniCollisionType::Kdop26; + else + SplitMeshData.CollisionType = EHoudiniCollisionType::Simple; + + // Look for a numeric string after the tokens, this allows us to have more than one collision per split. + FString First,Last; + Name.Split(TEXT("_"), &First, &Last); + if (First.IsNumeric()) + ParseSplitToken(Name, First); + } + else + { + if (bFoundRender) + { + // render_collision_geo_* means use the collision for the main (render) mesh. + SplitMeshData.CollisionType = EHoudiniCollisionType::MainMesh; + SplitMeshData.ComplexCollisionOwner.Empty(); + } + else + { + // collision_geo could mean two things. It could mean "create a second UStaticMesh and use it as a custom collision". + // But if the user specified "collision_geo" without specifying a rendered_collision_geo then we follow a slightly + // different path and create a static mesh with the collision geo but mark the component invisible in-game. + // + // For now, mark it was as Complex Collision and it will be post-processed. + + SplitMeshData.CollisionType = EHoudiniCollisionType::CustomComplex; + SplitMeshData.ComplexCollisionOwner = Name; + SplitMeshData.bRendered = true; + } + } + } + + if (!bFoundRender && !bFoundCollision) + { + // If neither render nor collision found then assume the name represents a split mesh. + SplitMeshData.bRendered = true; + } + + SplitMeshData.StaticMeshName = Name; +} + +void FHoudiniMeshTranslator::AddDefaultMesh(FHoudiniMeshToBuild & MeshesToBuild, const FString& Name) +{ + FHoudiniGroupedMeshPrimitives SplitGroup; + SplitGroup.CollisionType = EHoudiniCollisionType::MainMesh; + SplitGroup.bIsLOD = false; + SplitGroup.bRendered = true; + SplitGroup.SplitGroupName = Name; + + FHoudiniSplitGroupMesh Mesh; + Mesh.SplitMeshData.Add(SplitGroup); + Mesh.LODRenders.Add(0); + + MeshesToBuild.Meshes.Add(HAPI_UNREAL_GROUP_GEOMETRY_NOT_COLLISION, Mesh); +} + +FHoudiniMeshToBuild FHoudiniMeshTranslator::ScanOutputForMeshesToBuild() +{ + UpdateSplitGroups(); + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Parse all the split group names in the HGPO + //----------------------------------------------------------------------------------------------------------------------------------------------- + + TArray SplitMeshes; + + for (int Index = 0; Index < HGPO.SplitGroups.Num(); ++Index) + { + FHoudiniGroupedMeshPrimitives SplitMeshData; + SplitMeshData.SplitGroupName = HGPO.SplitGroups[Index]; + ClassifySplitGroup(SplitMeshData); + SplitMeshes.Add(SplitMeshData); + AllSplitGroups.Add(SplitMeshData.SplitGroupName); + } + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Now we have all the split groups, group them together per mesh + //----------------------------------------------------------------------------------------------------------------------------------------------- + + FHoudiniMeshToBuild MeshesToBuild; + + // Go though all split groups and create a FHoudiniSplitGroupMesh entry for each named static mesh. Ignore custom collisions as we need to + // some custom processing below. + for(auto & SplitMesh : SplitMeshes) + { + if (SplitMesh.CollisionType != EHoudiniCollisionType::CustomComplex) + MeshesToBuild.Meshes.FindOrAdd(SplitMesh.StaticMeshName); + } + + // Now go through each complex collision. There are two cases: + // 1. There is a static mesh already with this name, in which case we generate a second static mesh and link it as a custom complex collision + // or + // 2. There is no static mesh already with this name. So we just create ONE static mesh and do some processing when we create the components to make it invisible. + + for (auto& SplitMesh : SplitMeshes) + { + if (SplitMesh.CollisionType == EHoudiniCollisionType::CustomComplex) + { + if (MeshesToBuild.Meshes.Contains(SplitMesh.StaticMeshName)) + { + // case 1 above + SplitMesh.StaticMeshName += TEXT("custom_complex"); + SplitMesh.bRendered = true; + MeshesToBuild.Meshes.FindOrAdd(SplitMesh.StaticMeshName); + } + else + { + // case 2 above + auto & Mesh = MeshesToBuild.Meshes.FindOrAdd(SplitMesh.StaticMeshName); + SplitMesh.ComplexCollisionOwner.Empty(); + Mesh.bIsVisible = false; + } + } + } + + for (auto& SplitMesh : SplitMeshes) + { + auto& Mesh = MeshesToBuild.Meshes.FindOrAdd(SplitMesh.StaticMeshName); + + int Index = Mesh.SplitMeshData.Num(); + Mesh.SplitMeshData.Add(SplitMesh); + + if (SplitMesh.bRendered) + { + Mesh.LODRenders.Add(Index); + } + + bool bIsSimple = (SplitMesh.CollisionType != EHoudiniCollisionType::None) && + (SplitMesh.CollisionType != EHoudiniCollisionType::CustomComplex) && + (SplitMesh.CollisionType != EHoudiniCollisionType::MainMesh); + + if (bIsSimple) + { + Mesh.SimpleCollisions.Add(Index); + } + else if (SplitMesh.CollisionType == EHoudiniCollisionType::CustomComplex) + { + if (Mesh.CustomCollisionOwner.IsEmpty()) + { + Mesh.CustomCollisionOwner = SplitMesh.ComplexCollisionOwner; + } + else + { + HOUDINI_LOG_ERROR(TEXT("More than one custom group was found %s %d %d %s, ignoring -- skipping."), + *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *SplitMesh.SplitGroupName); + } + } + } + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Sort LODS. The order is determined alphetically, unless the top group is a "rendered_" node + //----------------------------------------------------------------------------------------------------------------------------------------------- + + for (auto & It : MeshesToBuild.Meshes) + { + FHoudiniSplitGroupMesh& Mesh = It.Value; + + Mesh.LODRenders.Sort([&](int a, int b) + { + // Sort all groups. Groups that were not classified as "LOD" groups come first. There should be only one of these. + + auto & MeshA = Mesh.SplitMeshData[a]; + auto & MeshB = Mesh.SplitMeshData[b]; + + // If both groups are LODs, sort by name. + if (MeshA.bIsLOD && MeshB.bIsLOD) + { + FString& NameA = MeshA.SplitGroupName; + FString& NameB = MeshB.SplitGroupName; + return NameA < NameB; + } + + // If A is not an LOD and B is, A comes first. + if (!MeshA.bIsLOD) + { + return MeshB.bIsLOD; + } + return false; + }); + + } + + return MeshesToBuild; +} + +FKAggregateGeom FHoudiniMeshTranslator::BuildAggregateCollision(FHoudiniSplitGroupMesh& Mesh) +{ + FKAggregateGeom AggregateCollisions; + + UpdatePartPositionIfNeeded(); + + for(int Index : Mesh.SimpleCollisions) + { + const FHoudiniGroupedMeshPrimitives& SplitMesh = Mesh.SplitMeshData[Index]; + // Create the simple colliders and add them to the aggregate + if (!AddSimpleCollisionToAggregate(SplitMesh.SplitGroupName, AggregateCollisions)) + { + // Failed to generate a convex collider + HOUDINI_LOG_WARNING(TEXT("failed to create simple collider.")); + } + } + + return AggregateCollisions; +} + +void +FHoudiniMeshTranslator::RemovePreviousOutputs() +{ + for(auto It : InputObjects) + { + FHoudiniOutputObject* FoundOutputObject = &It.Value; + for(auto Component : FoundOutputObject->OutputComponents) + { + RemoveAndDestroyComponent(Component); + } + FoundOutputObject->OutputComponents.Empty(); + + if(IsValid(FoundOutputObject->ProxyComponent)) + { + RemoveAndDestroyComponent(FoundOutputObject->ProxyComponent); + FoundOutputObject->ProxyComponent = nullptr; + } + } + InputObjects.Empty(); +} + +UStaticMesh* +FHoudiniMeshTranslator::CreateStaticMesh(const FString & MeshName, int NumLODs) +{ + UStaticMesh* StaticMesh = CreateNewUnrealStaticMesh(MeshName); + + if (!IsValid(StaticMesh)) + return nullptr; + + int NeededNumberOfLODs = 1; + + ITargetPlatform* CurrentPlatform = GetTargetPlatformManagerRef().GetRunningTargetPlatform(); + FStaticMeshLODGroup LODGroup = CurrentPlatform->GetStaticMeshLODSettings().GetLODGroup(NAME_None); + + if (StaticMesh->GetNumSourceModels() != NeededNumberOfLODs) + { + while (StaticMesh->GetNumSourceModels() < NeededNumberOfLODs) + StaticMesh->AddSourceModel(); + + // We may have to remove excessive LOD levels + if (StaticMesh->GetNumSourceModels() > NeededNumberOfLODs) + StaticMesh->SetNumSourceModels(NeededNumberOfLODs); + + // Initialize their default reduction setting + for (int32 ModelLODIndex = 0; ModelLODIndex < NeededNumberOfLODs; ModelLODIndex++) + { + StaticMesh->GetSourceModel(ModelLODIndex).ReductionSettings = LODGroup.GetDefaultSettings(ModelLODIndex); + } + StaticMesh->SetLightMapResolution(LODGroup.GetDefaultLightMapResolution()); + } + + FAssetRegistryModule::AssetCreated(StaticMesh); + + return StaticMesh; +} + +bool +FHoudiniMeshTranslator::CreateStaticMeshesFromSplitGroups() +{ + RemovePreviousOutputs(); + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Pull various settings before creating the mesh + //----------------------------------------------------------------------------------------------------------------------------------------------- + + bDoTiming = CVarHoudiniEngineMeshBuildTimer.GetValueOnAnyThread() != 0.0; + + // Update the part's material's IDS and info now + CreateNeededMaterials(); + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Get a list of meshes to build. + // Fetch all part data that is need to generated meshes. + //----------------------------------------------------------------------------------------------------------------------------------------------- + + UpdatePartVertexList(); + + // Get a list of all Static Meshes to build. + FHoudiniMeshToBuild MeshesToBuild = FHoudiniMeshTranslator::ScanOutputForMeshesToBuild(); + AllSplitGroups = HGPO.SplitGroups; + + + // Builds the corresponding faces and indices arrays. This will also add a new split group if it finds any un-assinged primitives. These + // are added to the main_geo group,. + + if (!UpdateSplitsFacesAndIndices()) + return true; + + // was the main_geo group added? + if (AllSplitGroups.Num() > HGPO.SplitGroups.Num()) + { + AddDefaultMesh(MeshesToBuild, AllSplitGroups[AllSplitGroups.Num() - 1]); + } + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Loop through and build each mesh. + //----------------------------------------------------------------------------------------------------------------------------------------------- + + for (auto & It : MeshesToBuild.Meshes) + { + CreateStaticMeshFromSplitGroups(It.Key, It.Value); + } + + // Once all meshes have been built, patch up custom collision refences + for (auto& It : MeshesToBuild.Meshes) + { + auto & Mesh = It.Value; + if (!Mesh.CustomCollisionOwner.IsEmpty()) + { + auto * Owner = MeshesToBuild.Meshes.Find(Mesh.CustomCollisionOwner); + if (Owner && Owner->UnrealStaticMesh) + { + Owner->UnrealStaticMesh->ComplexCollisionMesh = Mesh.UnrealStaticMesh; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + Owner->UnrealStaticMesh->SetCustomizedCollision(true); +#else + Owner->UnrealStaticMesh->bCustomizedCollision = true; +#endif + } + } + } + + return true; + +} + +bool +FHoudiniMeshTranslator::CreateStaticMeshFromSplitGroups(const FString& MeshName, FHoudiniSplitGroupMesh& SplitMeshData) +{ + double TimeStart = FPlatformTime::Seconds(); + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Set up data + //----------------------------------------------------------------------------------------------------------------------------------------------- +; + int NumLODs = SplitMeshData.LODRenders.Num(); + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + bool bReadTangents = HoudiniRuntimeSettings ? HoudiniRuntimeSettings->RecomputeTangentsFlag != EHoudiniRuntimeSettingsRecomputeFlag::HRSRF_Always : true; + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Create a new static mesh. Render, collision & other data will be added to this structure and then StaticMesh->Build() will be called + // at the end of the function to finalize the mesh. + //----------------------------------------------------------------------------------------------------------------------------------------------- + + SplitMeshData.UnrealStaticMesh = CreateStaticMesh(MeshName, NumLODs); + if (!IsValid(SplitMeshData.UnrealStaticMesh)) + return false; + + ITargetPlatform* CurrentPlatform = GetTargetPlatformManagerRef().GetRunningTargetPlatform(); + FStaticMeshLODGroup LODGroup = CurrentPlatform->GetStaticMeshLODSettings().GetLODGroup(NAME_None); + + // Create Output + SplitMeshData.OutputObjectIdentifier = FHoudiniOutputObjectIdentifier(HGPO.ObjectId, HGPO.GeoId, HGPO.PartId, MeshName); + + FHoudiniOutputObject* OutputObject = &OutputObjects.Add(SplitMeshData.OutputObjectIdentifier, {}); + InputObjects.Remove(SplitMeshData.OutputObjectIdentifier); + OutputObject->bProxyIsCurrent = false; + OutputObject->OutputObject = SplitMeshData.UnrealStaticMesh; + OutputObject->bIsInvisibleCollisionMesh = !SplitMeshData.bIsVisible; + + + + if (SplitMeshData.UnrealStaticMesh->GetNumSourceModels() != NumLODs) + { + while (SplitMeshData.UnrealStaticMesh->GetNumSourceModels() < NumLODs) + SplitMeshData.UnrealStaticMesh->AddSourceModel(); + + // We may have to remove excessive LOD levels + if (SplitMeshData.UnrealStaticMesh->GetNumSourceModels() > NumLODs) + SplitMeshData.UnrealStaticMesh->SetNumSourceModels(NumLODs); + + // Initialize their default reduction setting + for (int32 ModelLODIndex = 0; ModelLODIndex < NumLODs; ModelLODIndex++) + { + SplitMeshData.UnrealStaticMesh->GetSourceModel(ModelLODIndex).ReductionSettings = LODGroup.GetDefaultSettings(ModelLODIndex); + } + SplitMeshData.UnrealStaticMesh->SetLightMapResolution(LODGroup.GetDefaultLightMapResolution()); + } + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Build Description based off the Houdini data. + //----------------------------------------------------------------------------------------------------------------------------------------------- + + for(int LODIndex = 0; LODIndex < NumLODs; LODIndex++) + { + + auto & RenderGroup = SplitMeshData.SplitMeshData[SplitMeshData.LODRenders[LODIndex]]; + + RenderGroup.VertexList = AllSplitVertexLists[RenderGroup.SplitGroupName]; + PullMeshData(RenderGroup, SplitMeshData.UnrealStaticMesh, LODIndex, bReadTangents); + + FMeshDescription* MeshDescription = SplitMeshData.UnrealStaticMesh->CreateMeshDescription(LODIndex); + FStaticMeshAttributes(*MeshDescription).Register(); + BuildMeshDescription(MeshDescription, RenderGroup); + + bool bHasNormal = RenderGroup.Normals.Num() > 0; + bool bHasTangents = RenderGroup.TangentU.Num() > 0 || RenderGroup.TangentV.Num() > 0; + + // Update the Build Settings using the default setting values + FStaticMeshSourceModel* SrcModel = (SplitMeshData.UnrealStaticMesh->IsSourceModelValid(LODIndex)) ? &(SplitMeshData.UnrealStaticMesh->GetSourceModel(LODIndex)) : nullptr; + UpdateMeshBuildSettings(SrcModel->BuildSettings, bHasNormal, bHasTangents, PartUVSets.Num() > 0); + + // Store the new MeshDescription + SplitMeshData.UnrealStaticMesh->CommitMeshDescription(LODIndex); + + // Set screen size. + float ScreenSize = GetLODSCreensizeForSplit(RenderGroup.SplitGroupName); + if (ScreenSize >= 0.0f) + { + SrcModel->ScreenSize = ScreenSize; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + SplitMeshData.UnrealStaticMesh->SetAutoComputeLODScreenSize(false); +#else + SplitMeshData.UnrealStaticMesh->bAutoComputeLODScreenSize = false; +#endif + } + + CopyAttributesFromHGPOForSplit(RenderGroup.SplitGroupName, OutputObject->CachedAttributes, OutputObject->CachedTokens); + + // Update property attributes on the source model + TArray PropertyAttributes; + if (FHoudiniEngineUtils::GetGenericPropertiesAttributes( + HGPO.GeoId, + HGPO.PartId, + true, + SplitMeshData.OutputObjectIdentifier.PrimitiveIndex, + INDEX_NONE, + SplitMeshData.OutputObjectIdentifier.PointIndex, + PropertyAttributes)) + { + auto FindPropertyOnSourceModelLamba = [LODIndex](UObject* const InObject, const FString& InPropertyName, bool& bOutSkipDefaultIfPropertyNotFound, FEditPropertyChain& InPropertyChain, FProperty*& OutFoundProperty, UObject*& OutFoundPropertyObject, void*& OutContainer) + { + if (!IsValid(InObject)) + return false; + + UStaticMesh* const SM = Cast(InObject); + if (!IsValid(SM)) + return false; + + return TryToFindPropertyOnSourceModel( + SM, LODIndex, InPropertyName, InPropertyChain, bOutSkipDefaultIfPropertyNotFound, OutFoundProperty, OutFoundPropertyObject, OutContainer); + }; + + // Defer post edit change calls until after all property values have been set, since the static mesh + // build function is called from PostEditChangeProperty. + constexpr bool bDeferPostEditChangePropertyCalls = true; + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes( + SplitMeshData.UnrealStaticMesh, PropertyAttributes, 0, bDeferPostEditChangePropertyCalls, FindPropertyOnSourceModelLamba); + } + + } + + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Set various custom settings. + //----------------------------------------------------------------------------------------------------------------------------------------------- + + // Set the lightmap Coordinate Index. If we have more than one UV set, the 2nd valid set is used for lightmaps by convention + SplitMeshData.UnrealStaticMesh->SetLightMapCoordinateIndex(PartUVSets.Num() > 1 ? 1 : 0); + + // Check for a lightmapa resolution override. + if (PartLightMapResolutions.Num() > 0 && PartLightMapResolutions[0] > 0) + SplitMeshData.UnrealStaticMesh->SetLightMapResolution(PartLightMapResolutions[0]); + else + SplitMeshData.UnrealStaticMesh->SetLightMapResolution(64); + + // Nananite settings. + UpdateStaticMeshNaniteSettings(HGPO.GeoId, HGPO.PartId, SplitMeshData.OutputObjectIdentifier.PrimitiveIndex, SplitMeshData.UnrealStaticMesh); + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Build collision + //----------------------------------------------------------------------------------------------------------------------------------------------- + + UBodySetup* BodySetup = SplitMeshData.UnrealStaticMesh->GetBodySetup(); + if (!BodySetup) + { + SplitMeshData.UnrealStaticMesh->CreateBodySetup(); + BodySetup = SplitMeshData.UnrealStaticMesh->GetBodySetup(); + } + + if (IsValid(BodySetup)) + { + // Make sure rendering is done - so we are not changing data being used by collision drawing. + FlushRenderingCommands(); + + // Clean up old colliders from a previous cook + BodySetup->Modify(); + BodySetup->RemoveSimpleCollision(); + + FKAggregateGeom CollisionGeometry = BuildAggregateCollision(SplitMeshData); + + if (CollisionGeometry.GetElementCount() > 0) + { + BodySetup->AddCollisionFrom(CollisionGeometry); + BodySetup->CollisionTraceFlag = ECollisionTraceFlag::CTF_UseDefault; + } + + SetPhysicsMaterialFromHGPO(BodySetup); + + if (FHoudiniEngineUtils::HapiCheckAttributeExists(HGPO.GeoId, HGPO.PartId, + "unreal_uproperty_LODForCollision", HAPI_ATTROWNER_DETAIL)) + { + BodySetup->CollisionTraceFlag = ECollisionTraceFlag::CTF_UseComplexAsSimple; + } + } + + // If this is a custom collision object, mark it as implicit so that it doesn't get an actor created. + if (!SplitMeshData.CustomCollisionOwner.IsEmpty()) + { + OutputObject->bIsImplicit = true; + } + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Finalize mesh + //----------------------------------------------------------------------------------------------------------------------------------------------- + + double BuildTimeStart = FPlatformTime::Seconds(); + + TArray SMBuildErrors; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + SplitMeshData.UnrealStaticMesh->SetImportVersion(EImportStaticMeshVersion::LastVersion); +#else + SplitMeshData.UnrealStaticMesh->ImportVersion = EImportStaticMeshVersion::LastVersion; +#endif + SplitMeshData.UnrealStaticMesh->Build(true, &SMBuildErrors); + + for (FThreadSafeObjectIterator Iter(UStaticMeshComponent::StaticClass()); Iter; ++Iter) + { + UStaticMeshComponent* StaticMeshComponent = Cast(*Iter); + if (StaticMeshComponent->GetStaticMesh() == SplitMeshData.UnrealStaticMesh) + { + // it needs to recreate IF it already has been created + if (StaticMeshComponent->IsPhysicsStateCreated()) + { + StaticMeshComponent->RecreatePhysicsState(); + } + } + } + + FEditorSupportDelegates::RedrawAllViewports.Broadcast(); + + SplitMeshData.UnrealStaticMesh->GetOnMeshChanged().Broadcast(); + + UPackage* MeshPackage = SplitMeshData.UnrealStaticMesh->GetOutermost(); + if (IsValid(MeshPackage)) + { + MeshPackage->MarkPackageDirty(); + } + + double BuildTimeEnd = FPlatformTime::Seconds(); + if (bDoTiming) + HOUDINI_LOG_MESSAGE(TEXT("StaticMesh->Build() executed in %f seconds."), BuildTimeEnd - BuildTimeStart); + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Print results. + //----------------------------------------------------------------------------------------------------------------------------------------------- + + double TimeEnd = FPlatformTime::Seconds(); + if (bDoTiming) + HOUDINI_LOG_MESSAGE(TEXT("CreateStaticMeshFromSplitGroups() executed in %f seconds."), TimeEnd - TimeStart); + + return true; +} + +void FHoudiniMeshTranslator::UpdateSplitGroups() +{ + // The old code (per-split groups) uses slightly different conditions to fill in the HGPO.SplitGroups. This function + // fetches the groups using the new method. + + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + HAPI_Result Error = HAPI_RESULT_FAILURE; + Error = FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), HGPO.GeoId, HGPO.PartId, &PartInfo); + + TArray GroupNames; + if (!FHoudiniEngineUtils::HapiGetGroupNames(HGPO.GeoId, HGPO.PartId, HAPI_GROUPTYPE_PRIM, PartInfo.isInstanced, GroupNames)) + { + return; + } + + TArray Results; + + for (const FString& GroupName : GroupNames) + { + if (GroupName.StartsWith(HAPI_UNREAL_GROUP_LOD_PREFIX, ESearchCase::IgnoreCase) + || GroupName.StartsWith(HAPI_UNREAL_GROUP_INVISIBLE_COLLISION_PREFIX, ESearchCase::IgnoreCase) + || GroupName.StartsWith(HAPI_UNREAL_GROUP_RENDERED_COLLISION_PREFIX, ESearchCase::IgnoreCase) + || GroupName.StartsWith(HAPI_UNREAL_GROUP_RENDERED_PREFIX, ESearchCase::IgnoreCase)) + { + // Split by collisions / lods + Results.Add(GroupName); + } + } + + HGPO.SplitGroups = Results; +} + +bool +FHoudiniMeshTranslator::CreateHoudiniStaticMeshesFromSplitGroups() +{ + RemovePreviousOutputs(); + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Pull various settings before creating the mesh + //----------------------------------------------------------------------------------------------------------------------------------------------- + + bDoTiming = CVarHoudiniEngineMeshBuildTimer.GetValueOnAnyThread() != 0.0; + + // Update the part's material's IDS and info now + CreateNeededMaterials(); + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Get a list of meshes to build. + // Fetch all part data that is need to generated meshes. + //----------------------------------------------------------------------------------------------------------------------------------------------- + + UpdatePartVertexList(); + + // Get a list of all Static Meshes to build. + FHoudiniMeshToBuild MeshesToBuild = FHoudiniMeshTranslator::ScanOutputForMeshesToBuild(); + AllSplitGroups = HGPO.SplitGroups; + + + // Builds the corresponding faces and indices arrays. This will also add a new split group if it finds any un-assinged primitives. These + // are added to the main_geo group,. + + if (!UpdateSplitsFacesAndIndices()) + return true; + + // was the main_geo group added? + if (AllSplitGroups.Num() > HGPO.SplitGroups.Num()) + { + AddDefaultMesh(MeshesToBuild, AllSplitGroups[AllSplitGroups.Num() - 1]); + } + + //----------------------------------------------------------------------------------------------------------------------------------------------- + // Loop through and build each mesh. + //----------------------------------------------------------------------------------------------------------------------------------------------- + + TMap> MapHoudiniMatIdToUnrealInterface; + TMap> MapHoudiniMatAttributesToUnrealInterface; + TMap, int32>> MapUnrealMaterialInterfaceToUnrealIndexPerMesh; + + for (auto& It : MeshesToBuild.Meshes) + { + CreateHoudiniStaticMeshFromSplitGroups(It.Key, It.Value, MapHoudiniMatIdToUnrealInterface, MapHoudiniMatAttributesToUnrealInterface, MapUnrealMaterialInterfaceToUnrealIndexPerMesh); + } + + // Once all meshes have been built, patch up custom collision refences + for (auto& It : MeshesToBuild.Meshes) + { + auto& Mesh = It.Value; + if (!Mesh.CustomCollisionOwner.IsEmpty()) + { + auto* Owner = MeshesToBuild.Meshes.Find(Mesh.CustomCollisionOwner); + if (Owner && Owner->UnrealStaticMesh) + { + + Owner->UnrealStaticMesh->ComplexCollisionMesh = Mesh.UnrealStaticMesh; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + Owner->UnrealStaticMesh->SetCustomizedCollision(true); +#else + Owner->UnrealStaticMesh->bCustomizedCollision = true; +#endif + } + } + } + + return true; + +} + + +bool +FHoudiniMeshTranslator::CreateHoudiniStaticMeshFromSplitGroups(const FString& MeshName, FHoudiniSplitGroupMesh& SplitMeshData, + TMap> & MapHoudiniMatIdToUnrealInterface, + TMap> & MapHoudiniMatAttributesToUnrealInterface, + TMap, int32>> & MapUnrealMaterialInterfaceToUnrealIndexPerMesh) +{ + double tick = FPlatformTime::Seconds(); + + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMeshFromSplitGroups -- Per Split); + + // Houdini Static Meshes only create a mesh for the top LOD. + if (SplitMeshData.LODRenders.Num() == 0) + return true; + + FHoudiniGroupedMeshPrimitives & Group = SplitMeshData.SplitMeshData[SplitMeshData.LODRenders[0]]; + + FString & SplitGroupName = Group.SplitGroupName; + + // Get the vertex indices for this group + TArray& SplitVertexList = AllSplitVertexLists[SplitGroupName]; + + // Get valid count of vertex indices for this split. + const int32& SplitVertexCount = AllSplitVertexCounts[SplitGroupName]; + + // Make sure we have a valid vertex count for this split + if (SplitVertexCount % 3 != 0 || SplitVertexList.Num() % 3 != 0) + { + // Invalid vertex count, skip this split or we'd crash trying to create a mesh for it. + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%s] invalid vertex count.") + TEXT("- skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, *SplitGroupName); + + return true; + } + + // Get the output identifer for this split + FHoudiniOutputObjectIdentifier OutputObjectIdentifier = FHoudiniOutputObjectIdentifier(HGPO.ObjectId, HGPO.GeoId, HGPO.PartId, MeshName); + + // Try to find existing properties for this identifier + FHoudiniOutputObject* FoundOutputObject = InputObjects.Find(OutputObjectIdentifier); + + // If we don't yet have package params for this object identifier, fetch and resolve attributes for the split + // and update the package params + TMap TempAttributes; + TMap TempTokens; + bool bCopyAttributesAndTokens = false; + + // Get all the supported attributes from the HGPO + CopyAttributesFromHGPOForSplit(OutputObjectIdentifier, TempAttributes, TempTokens); + + // Resolve our final package params + FHoudiniAttributeResolver Resolver; + FHoudiniPackageParams FinalPackageParams; + FHoudiniEngineUtils::UpdatePackageParamsForTempOutputWithResolver( + PackageParams, + IsValid(OuterComponent) ? OuterComponent->GetWorld() : nullptr, + OuterComponent, + TempAttributes, + TempTokens, + PackageParams, + Resolver); + + bCopyAttributesAndTokens = true; + + + // Try to find an existing DM from a previous cook + UHoudiniStaticMesh* FoundStaticMesh = CreateNewHoudiniStaticMesh(OutputObjectIdentifier.SplitIdentifier); + + if (!FoundOutputObject) + { + // If we couldnt find a previous output object, create a new one + FHoudiniOutputObject NewOutputObject; + FoundOutputObject = &OutputObjects.Add(OutputObjectIdentifier, NewOutputObject); + } + FoundOutputObject->bProxyIsCurrent = true; + + // Update the attributes and tokens if this is the first split for this object identifier + if (bCopyAttributesAndTokens) + { + FoundOutputObject->CachedAttributes = MoveTemp(TempAttributes); + FoundOutputObject->CachedTokens = MoveTemp(TempTokens); + } + + if (bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateHoudiniStaticMesh() - PreBuildMesh in %f seconds."), FPlatformTime::Seconds() - tick); + tick = FPlatformTime::Seconds(); + } + + BuildHoudiniMesh(SplitGroupName, FoundStaticMesh); + + //--------------------------------------------------------------------------------------------------------------------- + // MATERIALS / FACE MATERIALS + //--------------------------------------------------------------------------------------------------------------------- + + ProcessMaterialsForHSM(SplitGroupName, FoundStaticMesh, MapHoudiniMatIdToUnrealInterface, MapHoudiniMatAttributesToUnrealInterface, MapUnrealMaterialInterfaceToUnrealIndexPerMesh); + + //// Update property attributes on the mesh + //TArray PropertyAttributes; + //if (GetGenericPropertiesAttributes( + // HGPO.GeoId, HGPO.PartId, + // AllSplitFirstValidVertexIndex[SplitGroupName], + // AllSplitFirstValidPrimIndex[SplitGroupName], + // PropertyAttributes)) + //{ + // UpdateGenericPropertiesAttributes( + // FoundStaticMesh, PropertyAttributes); + //} + + FoundStaticMesh->Optimize(); + + // Check if the mesh is valid (check all the counts (vertex, triangles, vertex instances, UVs etc) but skip + // looping over each individual triangle vertex index to check if the value is valid). + const bool bSkipVertexIndicesCheck = true; + if (!FoundStaticMesh->IsValid(bSkipVertexIndicesCheck)) + { + HOUDINI_LOG_WARNING( + TEXT("[CreateHoudiniStaticMesh]: Invalid StaticMesh data for %s in cook output! Please check the log."), + *FoundStaticMesh->GetName()); + } + + //// Try to find the outer package so we can dirty it up + //if (FoundStaticMesh->GetOuter()) + //{ + // FoundStaticMesh->GetOuter()->MarkPackageDirty(); + //} + //else + //{ + // FoundStaticMesh->MarkPackageDirty(); + //} + UPackage* MeshPackage = FoundStaticMesh->GetOutermost(); + if (IsValid(MeshPackage)) + { + MeshPackage->MarkPackageDirty(); + + /* + // DPT: deactivated auto saving mesh/material package + // only dirty for now, as we'll save them when saving the world. + // Save the created/updated package + FEditorFileUtils::PromptForCheckoutAndSave({ MeshPackage }, false, false); + */ + } + + // Add the Proxy mesh to the output maps + if (FoundOutputObject) + { + FoundOutputObject->ProxyObject = FoundStaticMesh; + FoundOutputObject->bProxyIsCurrent = true; + OutputObjects.FindOrAdd(OutputObjectIdentifier, *FoundOutputObject); + } + return true; +} + +void FHoudiniMeshTranslator::BuildHoudiniMesh(const FString& SplitGroupName, UHoudiniStaticMesh* FoundStaticMesh) +{ + // Get the vertex indices for this group + TArray& SplitVertexList = AllSplitVertexLists[SplitGroupName]; + + // Get valid count of vertex indices for this split. + const int32& SplitVertexCount = AllSplitVertexCounts[SplitGroupName]; + + // Make sure we have a valid vertex count for this split + if (SplitVertexCount % 3 != 0 || SplitVertexList.Num() % 3 != 0) + { + // Invalid vertex count, skip this split or we'd crash trying to create a mesh for it. + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%s] invalid vertex count.") + TEXT("- skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, *SplitGroupName); + + return; + } + + // WONWON + + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Build/Rebuild UHoudiniStaticMesh); + + //--------------------------------------------------------------------------------------------------------------------- + // INDICES + //--------------------------------------------------------------------------------------------------------------------- + + // + // Because of the splits, we don't need to declare all the vertices in the Part, + // but only the one that are currently used by the split's faces. + // The indicesMapper array is used to map those indices from Part Vertices to Split Vertices. + // We also keep track of the needed vertices index to declare them easily afterwards. + // + + // IndicesMapper: + // Maps index values for all vertices in the Part: + // - Vertices unused by the split will be set to -1 + // - Used vertices will have their value set to the "NewIndex" + // So that IndicesMapper[ oldIndex ] => newIndex + TArray IndicesMapper; + IndicesMapper.SetNumUninitialized(SplitVertexList.Num()); + for (int32 n = 0; n < IndicesMapper.Num(); n++) + IndicesMapper[n] = -1; + + int32 CurrentMapperIndex = 0; + + // NeededVertices: + // Array containing the old index of the needed vertices for the current split + // NeededVertices[ newIndex ] => oldIndex + TArray< int32 > NeededVertices; + NeededVertices.Reserve(SplitVertexList.Num() / 3); + TArray< int32 > TriangleIndices; + TriangleIndices.Reserve(SplitVertexList.Num()); + + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Build IndicesMapper and NeededVertices); + + bool bHasInvalidFaceIndices = false; + int32 ValidVertexId = 0; + for (int32 VertexIdx = 0; VertexIdx < SplitVertexList.Num(); VertexIdx += 3) + { + int32 WedgeCheck = SplitVertexList[VertexIdx + 0]; + if (WedgeCheck == -1) + continue; + + int32 WedgeIndices[3] = + { + SplitVertexList[VertexIdx + 0], + SplitVertexList[VertexIdx + 1], + SplitVertexList[VertexIdx + 2] + }; + + // Ensure the indices are valid + if (!IndicesMapper.IsValidIndex(WedgeIndices[0]) + || !IndicesMapper.IsValidIndex(WedgeIndices[1]) + || !IndicesMapper.IsValidIndex(WedgeIndices[2])) + { + // Invalid face index. Don't log in the loop. + bHasInvalidFaceIndices = true; + continue; + } + + // Converting Old (Part) Indices to New (Split) Indices: + for (int32 i = 0; i < 3; i++) + { + if (IndicesMapper[WedgeIndices[i]] < 0) + { + // This old index has not yet been "converted" to a new index + NeededVertices.Add(WedgeIndices[i]); + IndicesMapper[WedgeIndices[i]] = CurrentMapperIndex; + CurrentMapperIndex++; + } + + // Replace the old index with the new one + WedgeIndices[i] = IndicesMapper[WedgeIndices[i]]; + } + + // Flip wedge indices to fix the winding order. + TriangleIndices.Add(WedgeIndices[0]); + TriangleIndices.Add(WedgeIndices[2]); + TriangleIndices.Add(WedgeIndices[1]); + + ValidVertexId += 3; + } + + if (bHasInvalidFaceIndices) + { + HOUDINI_LOG_MESSAGE( + TEXT("Creating Dynamic Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%s] has some invalid face indices"), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, *SplitGroupName); + } + } + + //--------------------------------------------------------------------------------------------------------------------- + // NORMALS + //--------------------------------------------------------------------------------------------------------------------- + + // Extract this part's normal if needed + UpdatePartNormalsIfNeeded(); + + // Get the normals for this split + TArray SplitNormals; + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoNormals, PartNormals, SplitNormals); + + // Check that the number of normal we retrieved is correct + int32 NormalCount = SplitNormals.Num() / 3; + if (NormalCount < 0 || NormalCount < NeededVertices.Num()) + { + // Ignore normals + NormalCount = 0; + if (SplitNormals.Num() != 0) + HOUDINI_LOG_WARNING(TEXT("Invalid normal count detected - Skipping normals.")); + } + + //--------------------------------------------------------------------------------------------------------------------- + // TANGENTS + //--------------------------------------------------------------------------------------------------------------------- + + TArray SplitTangentU; + TArray SplitTangentV; + int32 TangentUCount = 0; + int32 TangentVCount = 0; + // No need to read the tangents if we want unreal to recompute them after + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + bool bReadTangents = HoudiniRuntimeSettings ? HoudiniRuntimeSettings->RecomputeTangentsFlag != EHoudiniRuntimeSettingsRecomputeFlag::HRSRF_Always : true; + + bool bGenerateTangentsFromNormalAttribute = false; + if (bReadTangents) + { + // Extract this part's Tangents if needed + UpdatePartTangentsIfNeeded(); + + // Get the Tangents for this split + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoTangentU, PartTangentU, SplitTangentU); + + // Get the binormals for this split + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoTangentV, PartTangentV, SplitTangentV); + + if ((SplitTangentU.Num() <= 0 || SplitTangentV.Num() <= 0)) + bReadTangents = false; + + // We need to manually generate tangents if: + // - we have normals but dont have tangentu or tangentv attributes + // - we have not specified that we wanted unreal to generate them + bGenerateTangentsFromNormalAttribute = (NormalCount > 0) && !bReadTangents; + + // Check that the number of tangents read matches the number of normals + TangentUCount = SplitTangentU.Num() / 3; + TangentVCount = SplitTangentV.Num() / 3; + if (NormalCount > 0 && (TangentUCount != NormalCount || TangentVCount != NormalCount)) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateHoudiniStaticMesh: Generate tangents due to count mismatch (# U Tangents = %d; # V Tangents = %d; # Normals = %d)"), TangentUCount, TangentVCount, NormalCount); + bGenerateTangentsFromNormalAttribute = true; + bReadTangents = false; + } + + if (bGenerateTangentsFromNormalAttribute && (HoudiniRuntimeSettings->RecomputeTangentsFlag == EHoudiniRuntimeSettingsRecomputeFlag::HRSRF_Always)) + { + // No need to generate tangents if we want unreal to recompute them after + bGenerateTangentsFromNormalAttribute = false; + } + } + else + { + bGenerateTangentsFromNormalAttribute = (NormalCount > 0); + } + + //--------------------------------------------------------------------------------------------------------------------- + // VERTEX COLORS AND ALPHAS + //--------------------------------------------------------------------------------------------------------------------- + + // Extract this part's colors if needed + UpdatePartColorsIfNeeded(); + + // Get the colors values for this split + TArray SplitColors; + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoColors, PartColors, SplitColors); + + // Extract this part's alpha values if needed + UpdatePartAlphasIfNeeded(); + + // Get the colors values for this split + TArray SplitAlphas; + FHoudiniMeshTranslator::TransferRegularPointAttributesToVertices( + SplitVertexList, AttribInfoAlpha, PartAlphas, SplitAlphas); + + const int32 ColorsCount = AttribInfoColors.exists ? SplitColors.Num() / AttribInfoColors.tupleSize : 0; + const bool bSplitColorValid = AttribInfoColors.exists && (AttribInfoColors.tupleSize >= 3) && ColorsCount > 0; + const bool bSplitAlphaValid = AttribInfoAlpha.exists && (SplitAlphas.Num() == ColorsCount); + + //--------------------------------------------------------------------------------------------------------------------- + // UVS + //--------------------------------------------------------------------------------------------------------------------- + + // Extract this part's UV sets if needed + UpdatePartUVSetsIfNeeded(); + + // See if we need to transfer uv point attributes to vertex attributes. + int32 NumUVLayers = 0; + TArray> SplitUVSets; + SplitUVSets.SetNum(MAX_STATIC_TEXCOORDS); + for (int32 TexCoordIdx = 0; TexCoordIdx < MAX_STATIC_TEXCOORDS; ++TexCoordIdx) + { + FHoudiniMeshTranslator::TransferPartAttributesToSplit( + SplitVertexList, AttribInfoUVSets[TexCoordIdx], PartUVSets[TexCoordIdx], SplitUVSets[TexCoordIdx]); + if (SplitUVSets[TexCoordIdx].Num() > 0) + { + NumUVLayers++; + } + } + + //--------------------------------------------------------------------------------------------------------------------- + // MATERIAL ATTRIBUTE OVERRIDES + //--------------------------------------------------------------------------------------------------------------------- + + // TODO: These are actually per faces, not per vertices... + // Need to update!! + UpdatePartFaceMaterialOverridesIfNeeded(); + + // + // Initialize mesh + // + const int32 NumVertexPositions = NeededVertices.Num(); + const int32 NumTriangles = TriangleIndices.Num() / 3; + const bool bHasPerFaceMaterials = PartFaceMaterialOverrides.Num() > 0 || (PartUniqueMaterialIds.Num() > 0 && !bOnlyOneFaceMaterial); + + FoundStaticMesh->Initialize( + NumVertexPositions, + NumTriangles, + NumUVLayers, // NumUVLayers + 0, // InitialNumStaticMaterials + NormalCount > 0, // HasNormals + bReadTangents || bGenerateTangentsFromNormalAttribute, // HasTangents + bSplitColorValid, // HasColors + bHasPerFaceMaterials // HasPerFaceMaterials + ); + + //--------------------------------------------------------------------------------------------------------------------- + // POSITIONS + //--------------------------------------------------------------------------------------------------------------------- + UpdatePartPositionIfNeeded(); + + // + // Transfer vertex positions: + // + // Because of the split, we're only interested in the needed vertices. + // Instead of declaring all the Positions, we'll only declare the vertices + // needed by the current split. + // + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Set Vertex Positions); + + bool bHasInvalidPositionIndexData = false; + for (int32 VertexPositionIdx = 0; VertexPositionIdx < NumVertexPositions; ++VertexPositionIdx) + //ParallelFor(NumVertexPositions, [&](uint32 VertexPositionIdx) + { + int32 NeededVertexIndex = NeededVertices[VertexPositionIdx]; + if (!PartPositions.IsValidIndex(NeededVertexIndex * 3 + 2)) + { + // Error retrieving positions. + bHasInvalidPositionIndexData = true; + continue; + } + + // We need to swap Z and Y coordinate here, and convert from m to cm. + FoundStaticMesh->SetVertexPosition(VertexPositionIdx, FVector3f( + PartPositions[NeededVertexIndex * 3 + 0] * HAPI_UNREAL_SCALE_FACTOR_POSITION, + PartPositions[NeededVertexIndex * 3 + 2] * HAPI_UNREAL_SCALE_FACTOR_POSITION, + PartPositions[NeededVertexIndex * 3 + 1] * HAPI_UNREAL_SCALE_FACTOR_POSITION + )); + }//); + + if (bHasInvalidPositionIndexData) + { + HOUDINI_LOG_WARNING( + TEXT("Creating Dynamic Static Meshes: Object [%d %s], Geo [%d], Part [%d %s], Split [%s] invalid position/index data ") + TEXT("- skipping."), + HGPO.ObjectId, *HGPO.ObjectName, HGPO.GeoId, HGPO.PartId, *HGPO.PartName, *SplitGroupName); + } + } + + //--------------------------------------------------------------------------------------------------------------------- + // FACES / TRIS + // Now set Normals, UVs and Colors on mesh points and AttributeSet + //--------------------------------------------------------------------------------------------------------------------- + + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Set Triangle Indices & Per Vertex Instance Attribute Values); + + // Now add the triangles to the mesh + for (int32 TriangleIdx = 0; TriangleIdx < NumTriangles; ++TriangleIdx) + // ParallelFor(NumTriangles, [&](uint32 TriangleIdx) + { + // TODO: add some additional intermediate consts for index calculations to make the indexing + // TODO: code a bit more readable + const int32 TriVertIdx0 = TriangleIdx * 3; + FoundStaticMesh->SetTriangleVertexIndices(TriangleIdx, FIntVector( + TriangleIndices[TriVertIdx0 + 0], + TriangleIndices[TriVertIdx0 + 1], + TriangleIndices[TriVertIdx0 + 2] + )); + + const int32 TriWindingIndex[3] = { 0, 2, 1 }; + // Normals and tangents (either getting tangents from attributes or generating tangents from the + // normals + if (NormalCount > 0 || bReadTangents) + { + for (int32 ElementIdx = 0; ElementIdx < 3; ++ElementIdx) + { + const bool bHasNormal = (NormalCount > 0 && SplitNormals.IsValidIndex(TriVertIdx0 * 3 + 3 * 3 - 1)); + FVector3f Normal = FVector3f::ZeroVector; + if (bHasNormal) + { + // Flip Z and Y coordinate for normal, but don't scale + Normal.Set( + SplitNormals[TriVertIdx0 * 3 + 3 * ElementIdx + 0], + SplitNormals[TriVertIdx0 * 3 + 3 * ElementIdx + 2], + SplitNormals[TriVertIdx0 * 3 + 3 * ElementIdx + 1] + ); + + FoundStaticMesh->SetTriangleVertexNormal(TriangleIdx, TriWindingIndex[ElementIdx], Normal); + } + + if (bReadTangents || bGenerateTangentsFromNormalAttribute) + { + FVector3f TangentU, TangentV; + if (bGenerateTangentsFromNormalAttribute) + { + if (bHasNormal) + { + // Generate the tangents if needed + Normal.FindBestAxisVectors(TangentU, TangentV); + + FoundStaticMesh->SetTriangleVertexUTangent(TriangleIdx, TriWindingIndex[ElementIdx], TangentU); + FoundStaticMesh->SetTriangleVertexVTangent(TriangleIdx, TriWindingIndex[ElementIdx], TangentV); + } + } + else + { + // Transfer the tangents from Houdini + TangentU.X = SplitTangentU[TriVertIdx0 * 3 + 3 * ElementIdx + 0]; + TangentU.Y = SplitTangentU[TriVertIdx0 * 3 + 3 * ElementIdx + 2]; + TangentU.Z = SplitTangentU[TriVertIdx0 * 3 + 3 * ElementIdx + 1]; + + TangentV.X = SplitTangentV[TriVertIdx0 * 3 + 3 * ElementIdx + 0]; + TangentV.Y = SplitTangentV[TriVertIdx0 * 3 + 3 * ElementIdx + 2]; + TangentV.Z = SplitTangentV[TriVertIdx0 * 3 + 3 * ElementIdx + 1]; + + FoundStaticMesh->SetTriangleVertexUTangent(TriangleIdx, TriWindingIndex[ElementIdx], TangentU); + FoundStaticMesh->SetTriangleVertexVTangent(TriangleIdx, TriWindingIndex[ElementIdx], TangentV); + } + } + } + } + + // Vertex Colors + if (bSplitColorValid && SplitColors.IsValidIndex(TriVertIdx0 * AttribInfoColors.tupleSize + 3 * AttribInfoColors.tupleSize - 1)) + { + FLinearColor VertexLinearColor; + for (int32 ElementIdx = 0; ElementIdx < 3; ++ElementIdx) + { + VertexLinearColor.R = FMath::Clamp( + SplitColors[TriVertIdx0 * AttribInfoColors.tupleSize + AttribInfoColors.tupleSize * ElementIdx + 0], 0.0f, 1.0f); + VertexLinearColor.G = FMath::Clamp( + SplitColors[TriVertIdx0 * AttribInfoColors.tupleSize + AttribInfoColors.tupleSize * ElementIdx + 1], 0.0f, 1.0f); + VertexLinearColor.B = FMath::Clamp( + SplitColors[TriVertIdx0 * AttribInfoColors.tupleSize + AttribInfoColors.tupleSize * ElementIdx + 2], 0.0f, 1.0f); + + if (bSplitAlphaValid) + { + VertexLinearColor.A = FMath::Clamp(SplitAlphas[TriVertIdx0 + ElementIdx], 0.0f, 1.0f); + } + else if (AttribInfoColors.tupleSize >= 4) + { + VertexLinearColor.A = FMath::Clamp( + SplitColors[TriVertIdx0 * AttribInfoColors.tupleSize + AttribInfoColors.tupleSize * ElementIdx + 3], 0.0f, 1.0f); + } + else + { + VertexLinearColor.A = 1.0f; + } + const FColor VertexColor = VertexLinearColor.ToFColor(false); + FoundStaticMesh->SetTriangleVertexColor(TriangleIdx, TriWindingIndex[ElementIdx], VertexColor); + } + } + + // UVs + if (NumUVLayers > 0) + { + // Dynamic mesh supports only 1 UV layer on the mesh it self. So we set the first layer + // on the mesh itself only, and we set all layers on the AttributeSet + for (int32 TexCoordIdx = 0; TexCoordIdx < NumUVLayers; ++TexCoordIdx) + { + const TArray& SplitUVs = SplitUVSets[TexCoordIdx]; + if (SplitUVs.IsValidIndex(TriVertIdx0 * 2 + 3 * 2 - 1)) + { + for (int32 ElementIdx = 0; ElementIdx < 3; ++ElementIdx) + { + const int32 UVIdx = TriVertIdx0 * 2 + ElementIdx * 2; + // We need to flip V coordinate when it's coming from HAPI. + const FVector2f UV(SplitUVs[UVIdx + 0], 1.0f - SplitUVs[UVIdx + 1]); + // Set the UV on the vertex instance in the UVLayer + FoundStaticMesh->SetTriangleVertexUV(TriangleIdx, TriWindingIndex[ElementIdx], TexCoordIdx, UV); + } + } + } + } + } + } + + FMeshBuildSettings BuildSettings; + UpdateMeshBuildSettings( + BuildSettings, + FoundStaticMesh->HasNormals(), + FoundStaticMesh->HasTangents(), + false); + // Compute normals if requested or needed/missing + if (BuildSettings.bRecomputeNormals) + { + FoundStaticMesh->CalculateNormals(BuildSettings.bComputeWeightedNormals); + } + + // Compute tangents if requested or needed/missing + if (BuildSettings.bRecomputeTangents) + { + FoundStaticMesh->CalculateTangents(BuildSettings.bComputeWeightedNormals); + } +} + +void +FHoudiniMeshTranslator::ProcessMaterialsForHSM( + const FString& SplitGroupName, + UHoudiniStaticMesh* FoundStaticMesh, + TMap> & MapHoudiniMatIdToUnrealInterface, + TMap> & MapHoudiniMatAttributesToUnrealInterface, + TMap, int32>> & MapUnrealMaterialInterfaceToUnrealIndexPerMesh) +{ + // Get face indices for this split. + TArray& SplitFaceIndices = AllSplitFaceIndices[SplitGroupName]; + + // Fetch the FoundMesh's Static Materials array + TArray& FoundStaticMaterials = FoundStaticMesh->GetStaticMaterials(); + + // Clear the materials array of the mesh the first time we encounter it + if (!MapUnrealMaterialInterfaceToUnrealIndexPerMesh.Contains(FoundStaticMesh)) + { + FoundStaticMaterials.Empty(); + } + TMap, int32>& MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh = MapUnrealMaterialInterfaceToUnrealIndexPerMesh.FindOrAdd(FoundStaticMesh); + + // Process material overrides first + if (PartFaceMaterialOverrides.Num() > 0) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Set Per Face Material Overrides); + + // Array used to avoid constantly attempting to load invalid materials + TArray InvalidMaterials; + + for (int32 FaceIdx = 0; FaceIdx < SplitFaceIndices.Num(); ++FaceIdx) + { + int32 SplitFaceIndex = SplitFaceIndices[FaceIdx]; + if (!PartFaceMaterialOverrides.IsValidIndex(SplitFaceIndex)) + continue; + + UMaterialInterface* MaterialInterface = nullptr; + int32 CurrentFaceMaterialIdx = 0; + const FHoudiniMaterialInfo& MatInfo = PartFaceMaterialOverrides[SplitFaceIndex]; + const FHoudiniMaterialIdentifier MaterialIdentifier = MatInfo.MakeIdentifier(); + TObjectPtr* FoundMaterialInterface = MapHoudiniMatAttributesToUnrealInterface.Find(MaterialIdentifier); + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + if (!MaterialInterface) + { + // Try to locate the corresponding material interface + + // Start by looking in our assignment map + FoundMaterialInterface = OutputAssignmentMaterials.Find(MaterialIdentifier); + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + // Only try to load a material if it has a chance to be valid! + if (!MaterialInterface && MaterialIdentifier.IsValid() && !InvalidMaterials.Contains(MaterialIdentifier)) + { + MaterialInterface = Cast( + StaticLoadObject(UMaterialInterface::StaticClass(), + nullptr, *MaterialIdentifier.MaterialObjectPath, nullptr, LOAD_NoWarn, nullptr)); + + if (!MaterialInterface) + InvalidMaterials.Add(MaterialIdentifier); + } + + if (MaterialInterface) + { + // We managed to load the UE4 material + // Make sure this material is in the assignments before replacing it. + OutputAssignmentMaterials.Add(MaterialIdentifier, MaterialInterface); + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterialInterface = ReplacementMaterials.Find(MaterialIdentifier); + if (ReplacementMaterialInterface && *ReplacementMaterialInterface) + MaterialInterface = *ReplacementMaterialInterface; + + // Add this material to the map + MapHoudiniMatAttributesToUnrealInterface.Add(MaterialIdentifier, MaterialInterface); + } + else + { + // The Attribute Material and its replacement do not exist + // See if we can fallback to the Houdini material assigned on the face + + // Get the unreal material corresponding to this houdini one + HAPI_NodeId MaterialId = PartFaceMaterialIds[SplitFaceIndex]; + + // See if we have already treated that material + FoundMaterialInterface = MapHoudiniMatIdToUnrealInterface.Find(MaterialId); + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + if (!MaterialInterface) + { + // If everything fails, we'll use the default material + MaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get()); + + // We need to add this material to the map + FString MaterialPathName = HAPI_UNREAL_DEFAULT_MATERIAL_NAME; + const bool bFoundHoudiniMaterial = FHoudiniMaterialTranslator::GetMaterialRelativePath(HGPO.AssetId, MaterialId, MaterialPathName); + const FHoudiniMaterialIdentifier DefaultMatIdentifier(MaterialPathName, bFoundHoudiniMaterial); + TObjectPtr * FoundMaterial = OutputAssignmentMaterials.Find(DefaultMatIdentifier); + if (FoundMaterial) + MaterialInterface = *FoundMaterial; + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterial = ReplacementMaterials.Find(DefaultMatIdentifier); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + // Map the Houdini ID to the unreal one + MapHoudiniMatIdToUnrealInterface.Add(MaterialId, MaterialInterface); + } + } + } + + if (MaterialInterface) + { + CurrentFaceMaterialIdx = GetFaceMaterialIndex(MaterialInterface, + MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh, + MatInfo.MaterialIndex, + FoundStaticMaterials); + // Update the Face Material on the mesh + FoundStaticMesh->SetTriangleMaterialID(FaceIdx, CurrentFaceMaterialIdx); + } + } + } + else if (PartUniqueMaterialIds.Num() > 0) + { + // The part has houdini materials + if (bOnlyOneFaceMaterial) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Set Single Material); + + // Use default Houdini material if no valid material is assigned to any of the faces. + UMaterialInterface* MaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get()); + + // Get id of this single material. + FString MaterialPathName = HAPI_UNREAL_DEFAULT_MATERIAL_NAME; + const bool bFoundHoudiniMaterial = FHoudiniMaterialTranslator::GetMaterialRelativePath(HGPO.AssetId, PartFaceMaterialIds[0], MaterialPathName); + const FHoudiniMaterialIdentifier DefaultMatIdentifier(MaterialPathName, bFoundHoudiniMaterial); + TObjectPtr* FoundMaterial = OutputAssignmentMaterials.Find(DefaultMatIdentifier); + if (FoundMaterial) + MaterialInterface = *FoundMaterial; + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterial = ReplacementMaterials.Find(DefaultMatIdentifier); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + FoundStaticMaterials.Empty(); + FoundStaticMaterials.Add(FStaticMaterial(MaterialInterface)); + } + else + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Set Per Face Materials); + + // We have multiple houdini materials + // Get default Houdini material. + UMaterial* DefaultMaterial = FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get(); + + for (int32 FaceIdx = 0; FaceIdx < SplitFaceIndices.Num(); ++FaceIdx) + { + int32 SplitFaceIndex = SplitFaceIndices[FaceIdx]; + if (!PartFaceMaterialIds.IsValidIndex(SplitFaceIndex)) + continue; + + // Get material id for this face. + HAPI_NodeId MaterialId = PartFaceMaterialIds[SplitFaceIndex]; + + // See if we have already treated that material + TObjectPtr* FoundMaterialInterface = MapHoudiniMatIdToUnrealInterface.Find(MaterialId); + UMaterialInterface* MaterialInterface = nullptr; + if (FoundMaterialInterface) + MaterialInterface = *FoundMaterialInterface; + + if (MaterialInterface) + { + int32 const* FoundUnrealMatIndex = MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh.Find(MaterialInterface); + if (FoundUnrealMatIndex) + { + // This material has been mapped already, just assign the mat index + FoundStaticMesh->SetTriangleMaterialID(FaceIdx, *FoundUnrealMatIndex); + continue; + } + } + else + { + MaterialInterface = Cast(DefaultMaterial); + + FString MaterialPathName = HAPI_UNREAL_DEFAULT_MATERIAL_NAME; + const bool bFoundHoudiniMaterial = FHoudiniMaterialTranslator::GetMaterialRelativePath(HGPO.AssetId, MaterialId, MaterialPathName); + const FHoudiniMaterialIdentifier DefaultMatIdentifier(MaterialPathName, bFoundHoudiniMaterial); + TObjectPtr* FoundMaterial = OutputAssignmentMaterials.Find(DefaultMatIdentifier); + if (FoundMaterial) + MaterialInterface = *FoundMaterial; + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterial = ReplacementMaterials.Find(DefaultMatIdentifier); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + // Map the houdini ID to the unreal one + MapHoudiniMatIdToUnrealInterface.Add(MaterialId, MaterialInterface); + } + + if (MaterialInterface) + { + // Add the material to the Static mesh + int32 UnrealMatIndex = FoundStaticMaterials.Add(FStaticMaterial(MaterialInterface)); + + // Map the houdini ID to the unreal one + MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh.Add(MaterialInterface, UnrealMatIndex); + + // Update the face index + FoundStaticMesh->SetTriangleMaterialID(FaceIdx, UnrealMatIndex); + } + } + } + } + else + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniMeshTranslator::CreateHoudiniStaticMesh -- Set Default Material); + + // No materials were found, we need to use default Houdini material. + UMaterialInterface* MaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial(HGPO.bIsTemplated).Get()); + + // See if we have a replacement material and use it on the mesh instead + TObjectPtr * ReplacementMaterial = ReplacementMaterials.Find( + FHoudiniMaterialIdentifier(HAPI_UNREAL_DEFAULT_MATERIAL_NAME, false, "")); + if (ReplacementMaterial && *ReplacementMaterial) + MaterialInterface = *ReplacementMaterial; + + FoundStaticMaterials.Empty(); + FoundStaticMaterials.Add(FStaticMaterial(MaterialInterface)); + } +} + + +bool +FHoudiniMeshTranslator::IsGammaCorrectionDisabled(HAPI_NodeId NodeId, HAPI_PartId PartId) +{ + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_DISABLE_GAMMA_CORRECTION); + TArray Values; + Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, Values); + if (Values.IsEmpty()) + return false; + + return Values[0] != 0; +} + + +void +FHoudiniMeshTranslator::GetCustomPrimitiveData( + int32 InGeoNodeId, + int32 InPartId, + int32 InPrimIndex, + TArray& OutCustomPrimData, + HAPI_AttributeOwner InAttribOwner) +{ + // Get the number of custom primitive data + TArray NumCustomPrimDataArray; + FHoudiniHapiAccessor Accessor(InGeoNodeId, InPartId, HAPI_UNREAL_ATTRIB_NUM_CUSTOM_PRIM_DATA); + Accessor.GetAttributeData(InAttribOwner, NumCustomPrimDataArray, InPrimIndex, 1); + if (NumCustomPrimDataArray.IsEmpty()) + return; + + int32 NumCustomPrimData = NumCustomPrimDataArray[0]; + OutCustomPrimData.SetNum(NumCustomPrimData); + for (int CustomPrimDataIndex = 0; CustomPrimDataIndex < NumCustomPrimData; CustomPrimDataIndex++) + { + TArray Values; + FString CurrentAttr = TEXT(HAPI_UNREAL_ATTRIB_CUSTOM_PRIMITIVE_DATA_PREFIX) + FString::FromInt(CustomPrimDataIndex); + Accessor.Init(InGeoNodeId, InPartId, TCHAR_TO_ANSI(*CurrentAttr)); + Accessor.GetAttributeData(InAttribOwner, Values, InPrimIndex, 1); + if (Values.IsEmpty()) + { + HOUDINI_LOG_ERROR(TEXT("Could not find attribute '%s' "), *CurrentAttr); + continue; + } + + OutCustomPrimData[CustomPrimDataIndex] = Values[0]; + } +} + +void +FHoudiniMeshTranslator::SetCustomPrimitiveData( + const TArray& CustomPrimData, + USceneComponent* InComponentToUpdate) +{ + if (CustomPrimData.IsEmpty()) + return; + + UPrimitiveComponent* PrimComp = Cast(InComponentToUpdate); + if (!IsValid(PrimComp)) + return; + + PrimComp->Modify(); + + // Set both the custom primitive data array AND the default (details UI) array +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 5 + PrimComp->SetCustomPrimitiveDataFloatArray(0, CustomPrimData); + PrimComp->SetDefaultCustomPrimitiveDataFloatArray(0, CustomPrimData); +#else + for (int Idx = 0; Idx < CustomPrimData.Num(); Idx++) + { + PrimComp->SetCustomPrimitiveDataFloat(Idx, CustomPrimData[Idx]); + PrimComp->SetDefaultCustomPrimitiveDataFloat(Idx, CustomPrimData[Idx]); + } +#endif + + PrimComp->MarkRenderStateDirty(); + + return; +} + +#undef LOCTEXT_NAMESPACE + + +``` + +`Source/HoudiniEngine/Private/HoudiniMeshTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniOutput.h" +#include "HoudiniPackageParams.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniMaterialTranslator.h" + +#include "CoreMinimal.h" +#include "HoudiniEngineAttributes.h" +#include "UObject/ObjectMacros.h" +#include "PhysicsEngine/AggregateGeom.h" + +#include "HoudiniMeshTranslator.generated.h" + +struct FHoudiniGroupedMeshPrimitives; +class UStaticMesh; +class UStaticMeshSocket; +class UMaterialInterface; +class UMeshComponent; +class UStaticMeshComponent; +class UHoudiniStaticMesh; +class UHoudiniStaticMeshComponent; + +struct FKAggregateGeom; +struct FHoudiniGenericAttribute; +struct FHoudiniMeshesToBuild; + +UENUM() +enum class EHoudiniSplitType : uint8 +{ + Invalid, + + Normal, + + LOD, + + RenderedComplexCollider, + InvisibleComplexCollider, + + RenderedUCXCollider, + InvisibleUCXCollider, + + RenderedSimpleCollider, + InvisibleSimpleCollider +}; + + +enum class EHoudiniCollisionType : uint8 +{ + None, + MainMesh, + CustomComplex, + Simple, + SimpleBox, + SimpleSphere, + SimpleCapsule, + Kdop10x, + Kdop10y, + Kdop10z, + Kdop18, + Kdop26 +}; + + +struct FHoudiniGroupedMeshPrimitives +{ + // Groups of primitives to be used for FHoudiniSplitGroupMesh. + + TArray VertexList; + TArray Normals; + TArray TangentU; + TArray TangentV; + TArray Indices; + TArray PartIndices; + TArray> UVSets; + TArray Colors; + TArray Alphas; + TArray FaceMaterialIndices; + TArray FaceSmoothingMasks; + TArray NeededVertices; + FString SplitGroupName; + int SplitId = 0; + bool bRendered = false; + bool bIsLOD = false; + EHoudiniCollisionType CollisionType; + FString ComplexCollisionOwner; + FString StaticMeshName; +}; + +struct FHoudiniSplitGroupMesh +{ + // This structure defines a static mesh to be built using primitives grouped by Houdini groups. + // The name of the group in Houdini determines whether the groups are used for the static mesh's + // main mesh, collisions or lods; + + // All grouped primitives used by this mesh. + TArray SplitMeshData; + + // Index into the group array for LODs. LOD 0 is the main mesh, if present. + TArray LODRenders; + + // Index into the group array for main meshes. + TArray SimpleCollisions; + + // If this mesh is to be used a custom complex collider, this is its name. + FString CustomCollisionOwner; + + bool bIsVisible = true; + + // Static Mesh generated. + UStaticMesh* UnrealStaticMesh = nullptr; + UHoudiniStaticMesh * HoudiniStaticMesh = nullptr; + // Output identifier. + FHoudiniOutputObjectIdentifier OutputObjectIdentifier; + +}; + +struct FHoudiniMeshToBuild +{ + // All meshes output from a single output node. + TMap Meshes; +}; + +struct HOUDINIENGINE_API FHoudiniMeshTranslator +{ + public: + + //----------------------------------------------------------------------------------------------------------------------------- + // HOUDINI TO UNREAL + //----------------------------------------------------------------------------------------------------------------------------- + + // + static bool CreateAllMeshesAndComponentsFromHoudiniOutput( + UHoudiniOutput* InOutput, + const FHoudiniPackageParams& InPackageParams, + EHoudiniStaticMeshMethod InStaticMeshMethod, + bool bSplitMeshSupport, + const FHoudiniStaticMeshGenerationProperties& InSMGenerationProperties, + const FMeshBuildSettings& InMeshBuildSettings, + const TMap>& InAllOutputMaterials, + UObject* InOuterComponent, + bool bInTreatExistingMaterialsAsUpToDate=false, + bool bInDestroyProxies=false); + + static bool CreateStaticMeshFromHoudiniGeoPartObject( + const FHoudiniGeoPartObject& InHGPO, + const FHoudiniPackageParams& InPackageParams, + const TMap& InOutputObjects, + TMap& OutOutputObjects, + TMap>& InAssignmentMaterialMap, + TMap>& InReplacementMaterialMap, + const TMap>& InAllOutputMaterials, + UObject* const InOuterComponent, + const bool& InForceRebuild, + EHoudiniStaticMeshMethod InStaticMeshMethod, + bool bSplitMeshSupport, + const FHoudiniStaticMeshGenerationProperties& InSMGenerationProperties, + const FMeshBuildSettings& InMeshBuildSettings, + bool bInTreatExistingMaterialsAsUpToDate = false); + + static bool CreateOrUpdateAllComponents( + UHoudiniOutput* InOutput, + UObject* InOuterComponent, + TMap& InNewOutputObjects, + bool bInDestroyProxies=false, + bool bInApplyGenericProperties=true); + + //----------------------------------------------------------------------------------------------------------------------------- + // HELPERS + //----------------------------------------------------------------------------------------------------------------------------- + static EHoudiniSplitType GetSplitTypeFromSplitName(const FString& InSplitName); + + static FString GetMeshIdentifierFromSplit(const FString& InSplitName, const EHoudiniSplitType& InSplitType); + + // TODO: Rename me! and template me! float/int/string ? + // TransferPartAttributesToSplitVertices + static int32 TransferRegularPointAttributesToVertices( + const TArray& InVertexList, + const HAPI_AttributeInfo& InAttribInfo, + const TArray& InData, + TArray& OutVertexData); + + template + static int32 TransferPartAttributesToSplit( + const TArray& InVertexList, + const HAPI_AttributeInfo& InAttribInfo, + const TArray& InData, + TArray& OutSplitData); + + // Try to find the named InPropertyName property on the source model at InSourceModelIndex on InStaticMesh. + static bool TryToFindPropertyOnSourceModel( + UStaticMesh* const InStaticMesh, + const int32 InSourceModelIndex, + const FString& InPropertyName, + FEditPropertyChain& InPropertyChain, + bool& bOutSkipDefaultIfPropertyNotFound, + FProperty*& OutFoundProperty, + UObject*& OutFoundPropertyObject, + void*& OutContainer); + + // Try to the find the named InPropertyName property on InSourceModel. + static bool TryToFindPropertyOnSourceModel( + FStaticMeshSourceModel& InSourceModel, + const FString& InPropertyName, + FEditPropertyChain& InPropertyChain, + FProperty*& OutFoundProperty, + void*& OutContainer); + + static bool ExtractMaterialIndex(FString& MaterialName, int32& MatIndex); + + // Update the MeshBuild Settings using the values from the runtime settings/overrides on the HAC + void UpdateMeshBuildSettings( + FMeshBuildSettings& OutMeshBuildSettings, + const bool& bHasNormals, + const bool& bHasTangents, + const bool& bHasLightmapUVSet); + + // Update the NaniteSettings for a given Static Mesh using attribute values + void UpdateStaticMeshNaniteSettings( + const int32& GeoId, const int32& PartId, const int32& PrimIndex, UStaticMesh* StaticMesh); + + // Copy supported (non-generic) attributes from the split by point/prim index. + static void CopyAttributesFromHGPOForSplit( + const FHoudiniGeoPartObject& InHGPO, + const int32 InPointIndex, + const int32 InPrimIndex, + TMap& OutAttributes, + TMap& OutTokens); + + // Copy supported (non-generic) attributes from the split + void CopyAttributesFromHGPOForSplit( + const FString& InSplitGroupName, TMap& OutAttributes, TMap& OutTokens); + + // Copy supported (non-generic) attributes from the split via outputobjectidentifier + void CopyAttributesFromHGPOForSplit( + const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier, TMap& OutAttributes, TMap& OutTokens); + + // Return Custom Primitive Data attributes found on the mesh + static void GetCustomPrimitiveData( + int32 InGeoNodeId, + int32 InPartId, + int32 InPrimIndex, + TArray& OutCustomPrimData, + HAPI_AttributeOwner InAttribOwner = HAPI_AttributeOwner::HAPI_ATTROWNER_INVALID); + + static void SetCustomPrimitiveData( + const TArray& CustomPrimData, + USceneComponent* InComponentToUpdate); + + //----------------------------------------------------------------------------------------------------------------------------- + // ACCESSORS + //----------------------------------------------------------------------------------------------------------------------------- + + //----------------------------------------------------------------------------------------------------------------------------- + // MUTATORS + //----------------------------------------------------------------------------------------------------------------------------- + void SetHoudiniGeoPartObject(const FHoudiniGeoPartObject& InHGPO) { HGPO = InHGPO; }; + void SetOuterComponent(UObject* InOuter) { OuterComponent = InOuter; }; + void SetPackageParams(const FHoudiniPackageParams& InPackageParams, const bool& bUpdateHGPO = false); + + void SetInputObjects(const TMap& InInputObjects) { InputObjects = InInputObjects; }; + void SetOutputObjects(TMap& InOutputObjects) { OutputObjects = InOutputObjects; }; + + void SetInputAssignmentMaterials(const TMap>& InInputMaterials) { InputAssignmentMaterials = InInputMaterials; }; + void SetReplacementMaterials(const TMap>& InReplacementMaterials) { ReplacementMaterials = InReplacementMaterials; }; + void SetAllOutputMaterials(const TMap>& InAllOutputMaterials) { AllOutputMaterials = InAllOutputMaterials; }; + + //void SetInputObjectProperties(const TMap& InInputObjectProperties) { InputObjectProperties = InInputObjectProperties; }; + //void SetOutputObjectProperties(TMap& InOutputObjectProperties) { OutputObjectProperties = InOutputObjectProperties; }; + + void SetTreatExistingMaterialsAsUpToDate(bool bInTreatExistingMaterialsAsUpToDate) { bTreatExistingMaterialsAsUpToDate = bInTreatExistingMaterialsAsUpToDate; } + + void SetStaticMeshGenerationProperties(const FHoudiniStaticMeshGenerationProperties& InStaticMeshGenerationProperties) { StaticMeshGenerationProperties = InStaticMeshGenerationProperties; }; + + void SetStaticMeshBuildSettings(const FMeshBuildSettings& InMBS) { StaticMeshBuildSettings = InMBS; }; + + // Create a StaticMesh using the MeshDescription format + bool CreateStaticMesh_MeshDescription(); + + bool CreateStaticMeshesFromSplitGroups(); + + // Indicates the update is forced + bool ForceRebuild; + int32 DefaultMeshSmoothing; + + protected: + + // Create a UHoudiniStaticMesh + bool CreateHoudiniStaticMesh(); + + bool CreateHoudiniStaticMeshesFromSplitGroups(); + + // Helper to make and populate a FHoudiniOutputObjectIdentifier from the current HGPO and the given + // InSplitGroupName and InSplitType. + FHoudiniOutputObjectIdentifier MakeOutputObjectIdentifier(const FString& InSplitGroupName, const EHoudiniSplitType InSplitType); + + static void ApplyComplexColliderHelper( + UStaticMesh* TargetStaticMesh, + UStaticMesh* ComplexStaticMesh, + const EHoudiniSplitType SplitType, + bool& AssignedCustomCollisionMesh, + FHoudiniOutputObject* OutputObject); + + void ResetPartCache(); + + bool UpdatePartVertexList(); + + void SortSplitGroups(); + + bool UpdateSplitsFacesAndIndices(); + + // Update this part's position cache if we haven't already + bool UpdatePartPositionIfNeeded(); + + // Update this part's normal cache if we haven't already + bool UpdatePartNormalsIfNeeded(); + + // Update this part's tangent and binormal caches if we haven't already + bool UpdatePartTangentsIfNeeded(); + + // Update this part's color cache if we haven't already + bool UpdatePartColorsIfNeeded(); + + // Update this part's alpha if we haven't already + bool UpdatePartAlphasIfNeeded(); + + // Update this part's face smoothing values if we haven't already + bool UpdatePartFaceSmoothingIfNeeded(); + + // Update this part's UV sets if we haven't already + bool UpdatePartUVSetsIfNeeded(const bool& bRemoveUnused = false); + + // Update this part;s lightmap resolution cache if we haven't already + bool UpdatePartLightmapResolutionsIfNeeded(); + + // Update this part's lod screensize attribute cache if we haven't already + bool UpdatePartLODScreensizeIfNeeded(); + + // Update th unique materials ids and infos needed for this part using the face materials and overrides + bool UpdatePartNeededMaterials(); + + // Update this part's face material IDs, unique material IDs and material Infos caches if we haven't already + bool UpdatePartFaceMaterialIDsIfNeeded(); + + // Update this part's material overrides cache if we haven't already + bool UpdatePartFaceMaterialOverridesIfNeeded(); + + // Updates and create the material that are needed for this part + bool CreateNeededMaterials(); + + int32 GetFaceMaterialIndex(UMaterialInterface* MaterialInterface, + TMap, int32>& MapUnrealMaterialInterfaceToUnrealMaterialIndexThisMesh, + int32 MatIndex, + TArray& FoundStaticMaterials); + + UStaticMesh* CreateNewUnrealStaticMesh(const FString& InMeshIdentifierString); + + + UHoudiniStaticMesh* CreateNewHoudiniStaticMesh(const FString& InSplitIdentifier); + + UStaticMesh* FindExistingStaticMesh(const FHoudiniOutputObjectIdentifier& InIdentifier); + + UHoudiniStaticMesh* FindExistingHoudiniStaticMesh(const FHoudiniOutputObjectIdentifier& InIdentifier); + + float GetLODSCreensizeForSplit(const FString& SplitGroupName); + + // Create convex/UCX collider for a split and add to the aggregate + bool AddConvexCollisionToAggregate(const FString& SplitGroupName, FKAggregateGeom& AggCollisions); + // Create simple colliders for a split and add to the aggregate + bool AddSimpleCollisionToAggregate(const FString& SplitGroupName, FKAggregateGeom& AggCollisions); +public: + // Helper functions to generate the simple colliders and add them to the aggregate + static int32 GenerateBoxAsSimpleCollision(const TArray& InPositionArray, FKAggregateGeom& OutAggregateCollisions); + static int32 GenerateOrientedBoxAsSimpleCollision(const TArray& InPositionArray, FKAggregateGeom& OutAggregateCollisions); + static int32 GenerateSphereAsSimpleCollision(const TArray& InPositionArray, FKAggregateGeom& OutAggregateCollisions); + static int32 GenerateSphylAsSimpleCollision(const TArray& InPositionArray, FKAggregateGeom& OutAggregateCollisions); + static int32 GenerateOrientedSphylAsSimpleCollision(const TArray& InPositionArray, FKAggregateGeom& OutAggregateCollisions); + static int32 GenerateKDopAsSimpleCollision(const TArray& InPositionArray, const TArray &Dirs, FKAggregateGeom& OutAggregateCollisions); + static TArray GetKdopDirections(const FString& SplitGroupName); + + // Helper functions to remove unused/stale components + static bool RemoveAndDestroyComponent(UObject* InComponent); + +protected: + // Helper functions for the simple colliders generation + static void CalcBoundingBox(const TArray& PositionArray, FVector& Center, FVector& Extents, FVector& LimitVec); + static void CalcBoundingSphere(const TArray& PositionArray, FSphere& sphere, FVector& LimitVec); + static void CalcBoundingSphere2(const TArray& PositionArray, FSphere& sphere, FVector& LimitVec); + static void CalcBoundingSphyl(const TArray& PositionArray, FSphere& sphere, float& length, FRotator& rotation, FVector& LimitVec); + + // Helper to create a new mesh component + static UMeshComponent* CreateMeshComponent(UObject *InOuterComponent, const TSubclassOf& InComponentType); + + // Helper to update an existing mesh component + static void UpdateMeshComponent( + UMeshComponent *InMeshComponent, + UObject *InMesh, + const FHoudiniOutputObjectIdentifier &InOutputIdentifier, + const FHoudiniOutputObject & OutputObject, + const FHoudiniGeoPartObject *InHGPO, + TArray> & HoudiniCreatedSocketActors, + TArray> & HoudiniAttachedSocketActors, + bool bInApplyGenericProperties=true); + + // Helper to create or update a mesh component for a UStaticMesh or proxy mesh output + static UMeshComponent* CreateOrUpdateMeshComponent( + const UHoudiniOutput* InOutput, + UObject* InOuterComponent, + const FHoudiniOutputObjectIdentifier& InOutputIdentifier, + const TSubclassOf& InComponentType, + FHoudiniOutputObject& OutOutputObject, + FHoudiniGeoPartObject const *& OutFoundHGPO, + bool &bCreated); + + // Helper to set or update the mesh on UStaticMeshComponent + static bool UpdateMeshOnStaticMeshComponent(UStaticMeshComponent *InComponent, UObject *InMesh); + + // Helper to set or update the mesh on UHoudiniStaticMeshComponent + static bool UpdateMeshOnHoudiniStaticMeshComponent(UHoudiniStaticMeshComponent *InComponent, UObject *InMesh); + + static bool AddActorsToMeshSocket(UStaticMeshSocket * Socket, UStaticMeshComponent * StaticMeshComponent, + TArray>& HoudiniCreatedSocketActors, TArray>& HoudiniAttachedSocketActors); + + static bool HasFracturePieceAttribute(const HAPI_NodeId& GeoId, const HAPI_NodeId& PartId); + + protected: + + // Data cache for this translator + + // The HoudiniGeoPartObject we're working on + FHoudiniGeoPartObject HGPO; + + // Outer object for attaching components to + UObject* OuterComponent = nullptr; + + // Structure that handles cooking/baking package creation parameters + FHoudiniPackageParams PackageParams; + + + // Previous output objects + TMap InputObjects; + + // New Output objects + TMap OutputObjects; + + + // Input Material Map + TMap> InputAssignmentMaterials; + // Output Material Map + TMap> OutputAssignmentMaterials; + // Input Replacement Materials maps + TMap> ReplacementMaterials; + // All the materials that have been generated by this Houdini Asset + // Used to avoid generating the same houdini material over and over again + TMap> AllOutputMaterials; + + // Input mesh properties + //TMap InputObjectProperties; + // Output mesh properties + //TMap OutputObjectProperties; + + // The generated simple/UCX colliders + TMap AllAggregateCollisions; + + // Names of the groups used for splitting the geometry + TArray AllSplitGroups; + + // Per-split lists of faces + TMap> AllSplitVertexLists; + + // Per-split number of faces + TMap AllSplitVertexCounts; + + // Per-split indices arrays + TMap> AllSplitFaceIndices; + + // Per-split first valid vertex index + TMap AllSplitFirstValidVertexIndex; + + // Per-split first valid prim index + TMap AllSplitFirstValidPrimIndex; + + // Vertex Indices for the part + TArray PartVertexList; + + // Positions + TArray PartPositions; + HAPI_AttributeInfo AttribInfoPositions; + + // Vertex Normals + TArray PartNormals; + HAPI_AttributeInfo AttribInfoNormals; + + // Vertex TangentU + TArray PartTangentU; + HAPI_AttributeInfo AttribInfoTangentU; + + // Vertex TangentV + TArray PartTangentV; + HAPI_AttributeInfo AttribInfoTangentV; + + // Vertex Colors + TArray PartColors; + HAPI_AttributeInfo AttribInfoColors; + + // Vertex Alpha values + TArray PartAlphas; + HAPI_AttributeInfo AttribInfoAlpha; + + // Face Smoothing masks + TArray PartFaceSmoothingMasks; + HAPI_AttributeInfo AttribInfoFaceSmoothingMasks; + + // UVs + TArray> PartUVSets; + TArray AttribInfoUVSets; + + // Lightmap resolution + TArray PartLightMapResolutions; + HAPI_AttributeInfo AttribInfoLightmapResolution; + + // Material IDs per face + TArray PartFaceMaterialIds; + HAPI_AttributeInfo AttribInfoFaceMaterialIds; + // Unique material IDs + TArray PartUniqueMaterialIds; + //TSet PartUniqueMaterialIds; + // Material infos for each unique Material + TArray PartUniqueMaterialInfos; + //TSet PartUniqueMaterialInfos; + // Indicates we only have a single face material + bool bOnlyOneFaceMaterial; + + // Material Overrides per face + TArray PartFaceMaterialOverrides; + // Indicates whether we have any valid material overrides via attributes + bool bHaveMaterialOverrides; + // Indicates if we have prim material attributes + bool bHavePrimMaterialOverrides; + // Indicates that material overides attributes need an instance to be created + bool bMaterialOverrideNeedsCreateInstance; + + // LOD Screensize + TArray PartLODScreensize; + + // When building a mesh, if an associated material already exists, treat + // it as up to date, regardless of the MaterialInfo.bHasChanged flag + bool bTreatExistingMaterialsAsUpToDate; + + // Default properties to be used when generating Static Meshes + FHoudiniStaticMeshGenerationProperties StaticMeshGenerationProperties; + + // Whether or not to do timing. + bool bDoTiming = false; + + // Default Mesh Build settings to be used when generating Static Meshes + FMeshBuildSettings StaticMeshBuildSettings; + + // The following functions are refactored versions of code used for generating meshes using + // mesh descriptions. They are used by the split mesh generation code. + //////////////////////////////////////////////////////////////////////////////////////// + + void BuildMeshDescription(FMeshDescription *MeshDesc, FHoudiniGroupedMeshPrimitives & SplitMeshData); + + void ProcessMaterials(UStaticMesh* FoundStaticMesh, FHoudiniGroupedMeshPrimitives& SplitMeshData); + + void PullMeshData(FHoudiniGroupedMeshPrimitives& SplitMeshData, UStaticMesh* FoundStaticMesh, int LODIndex, bool bReadTangents); + + void SetPhysicsMaterialFromHGPO(UBodySetup * BodySetup); + + FHoudiniMeshToBuild ScanOutputForMeshesToBuild(); + + void ClassifySplitGroup(FHoudiniGroupedMeshPrimitives & SplitMeshData); + + FKAggregateGeom BuildAggregateCollision(FHoudiniSplitGroupMesh& Mesh); + + void RemovePreviousOutputs(); + + UStaticMesh * CreateStaticMesh(const FString & Name, int NumLODs); + + void AddDefaultMesh(FHoudiniMeshToBuild & MeshesToBuild, const FString & Name); + + bool CreateStaticMeshFromSplitGroups(const FString & Name, FHoudiniSplitGroupMesh & Mesh); + + bool CreateHoudiniStaticMeshFromSplitGroups(const FString& Name, FHoudiniSplitGroupMesh& Mesh, + TMap> & MapHoudiniMatIdToUnrealInterface, + TMap> & MapHoudiniMatAttributesToUnrealInterface, + TMap, int32>> & MapUnrealMaterialInterfaceToUnrealIndexPerMesh); + + void UpdateSplitGroups(); + + bool ParseSplitToken(FString& Name, const FString& Token); + + void BuildHoudiniMesh(const FString & SplitGroupName, UHoudiniStaticMesh *FoundStaticMesh); + + void ProcessMaterialsForHSM( + const FString& SplitGroupName, + UHoudiniStaticMesh* FoundStaticMesh, + TMap> & MapHoudiniMatIdToUnrealInterface, + TMap> & MapHoudiniMatAttributesToUnrealInterface, + TMap, int32>> & MapUnrealMaterialInterfaceToUnrealIndexPerMesh); + + bool IsGammaCorrectionDisabled(HAPI_NodeId Node, HAPI_PartId PartId); + +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniMeshUtils.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include "HoudiniMeshUtils.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "ComponentReregisterContext.h" +#include "Components/PrimitiveComponent.h" +#include "ContentBrowserModule.h" +// #include "IContentBrowserSingleton.h" +#include "Editor.h" +#include "Engine/StaticMesh.h" +#include "Modules/ModuleManager.h" +#include "IMeshMergeUtilities.h" +#include "MeshMergeModule.h" +#include "Runtime/Launch/Resources/Version.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "MeshMergeHelpers.h" +#else + #include "MeshMergeUtilities/Private/MeshMergeHelpers.h" +#endif + +#if ENGINE_MAJOR_VERSION < 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 2) +#include "StaticMeshOperations.h" +#endif + +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniPackageParams.h" + + +bool FHoudiniMeshUtils::MergeMeshes( + const TArray& InMeshComponents, + const FHoudiniPackageParams& InPackageParams, + const FMeshMergingSettings& InSettings, + UStaticMesh*& OutStaticMesh, + FVector& OutMergedLocation, + TArray* OutAssets) +{ + if (InMeshComponents.IsEmpty()) + return false; + + // Filter out invalid components or components without valid static meshes + TArray MeshComponents = InMeshComponents.FilterByPredicate([](UPrimitiveComponent const* const InComponent) + { + if (!IsValid(InComponent)) + return false; + + UStaticMeshComponent const* const MeshComponent = Cast(InComponent); + if (IsValid(MeshComponent) && !IsValid(MeshComponent->GetStaticMesh())) + return false; + + return true; + }); + + if (MeshComponents.IsEmpty()) + return false; + + // From: MeshMergingTool in the MergeActors plugin + + const IMeshMergeUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked("MeshMergeUtilities").GetUtilities(); + UWorld* const World = MeshComponents[0]->GetWorld(); + if (!World) + { + HOUDINI_LOG_WARNING(TEXT("Invalid World retrieved from mesh component \"%s\"!"), *(MeshComponents[0]->GetPathName())); + return false; + } + TArray AssetsToSync; + static constexpr float ScreenAreaSize = TNumericLimits::Max(); + + // Determine the package name via FHoudiniPackageParams + // Pretend we are an HDA to generate a temp / cook package path + const FString PackageName = FPaths::Combine(InPackageParams.GetPackagePath(), InPackageParams.GetPackageName()); + + // If the merge destination package already exists, it is possible that the mesh is already used in a scene somewhere, or its materials or even just its textures. + // Static primitives uniform buffers could become invalid after the operation completes and lead to memory corruption. To avoid it, we force a global reregister. + if (FindObject(nullptr, *PackageName)) + { + FGlobalComponentReregisterContext GlobalReregister; + MeshUtilities.MergeComponentsToStaticMesh(MeshComponents, World, InSettings, nullptr, nullptr, PackageName, AssetsToSync, OutMergedLocation, ScreenAreaSize, true); + } + else + { + MeshUtilities.MergeComponentsToStaticMesh(MeshComponents, World, InSettings, nullptr, nullptr, PackageName, AssetsToSync, OutMergedLocation, ScreenAreaSize, true); + } + + if (AssetsToSync.IsEmpty()) + { + HOUDINI_LOG_WARNING(TEXT("Failed to generate static mesh for mesh component \"%s\""), *(MeshComponents[0]->GetPathName())); + return false; + } + + FAssetRegistryModule& AssetRegistry = FModuleManager::Get().LoadModuleChecked("AssetRegistry"); + const int32 AssetCount = AssetsToSync.Num(); + for (int32 AssetIndex = 0; AssetIndex < AssetCount; AssetIndex++) + { + AssetRegistry.AssetCreated(AssetsToSync[AssetIndex]); + GEditor->BroadcastObjectReimported(AssetsToSync[AssetIndex]); + } + + // //Also notify the content browser that the new assets exists + // FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked("ContentBrowser"); + // ContentBrowserModule.Get().SyncBrowserToAssets(AssetsToSync, true); + + // Set the new generated mesh in the input object + OutStaticMesh = Cast(AssetsToSync[0]); + if (OutAssets) + *OutAssets = AssetsToSync; + + return true; +} + +bool +FHoudiniMeshUtils::RetrieveMesh( + FMeshDescription& OutMeshDescription, + const UStaticMeshComponent* InStaticMeshComponent, + const int32 InLODIndex, + bool bInPropagateVertexColours, + bool bInApplyComponentTransform) +{ + if (!IsValid(InStaticMeshComponent)) + return false; + + // Deform mesh data according to the Spline Mesh Component's data +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2) + FMeshMergeHelpers::RetrieveMesh(InStaticMeshComponent, InLODIndex, OutMeshDescription, bInPropagateVertexColours, bInApplyComponentTransform); +#else + FMeshMergeHelpers::RetrieveMesh(InStaticMeshComponent, InLODIndex, OutMeshDescription, bInPropagateVertexColours); + if (!bInApplyComponentTransform) + { + // In UE <= 5.1 the component transform is applied by RetrieveMesh, undo it + FStaticMeshOperations::ApplyTransform(OutMeshDescription, InStaticMeshComponent->GetComponentTransform().Inverse()); + } +#endif + + return true; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniMeshUtils.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +struct FHoudiniPackageParams; + +struct FMeshDescription; +struct FMeshMergingSettings; +class UPrimitiveComponent; +class UStaticMesh; +class UStaticMeshComponent; + + +class HOUDINIENGINE_API FHoudiniMeshUtils +{ +public: + /** + * @brief Merges the meshes of InMeshComponents into one new UStaticMesh. + * @param InMeshComponents The primitive components' meshes to merge. + * @param InPackageParams The params to use for determining the package and object path of the new static mesh. + * @param InSettings The mesh merging settings. + * @param OutStaticMesh The new / output static mesh. + * @param OutMergedLocation The output transform of the merged mesh. + * @param OutAssets All assets that were created during the merge operation. + * @return True if the operation was successful. + */ + static bool MergeMeshes( + const TArray& InMeshComponents, + const FHoudiniPackageParams& InPackageParams, + const FMeshMergingSettings& InSettings, + UStaticMesh*& OutStaticMesh, + FVector& OutMergedLocation, + TArray* OutAssets=nullptr); + + static bool RetrieveMesh( + FMeshDescription& OutMeshDescription, + const UStaticMeshComponent* InStaticMeshComponent, + const int32 InLODIndex, + bool bInPropagateVertexColours, + bool bInApplyComponentTransform); + +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniOutputTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniOutputTranslator.h" + +#include "HAPI/HAPI.h" + +#include "HoudiniAnimationTranslator.h" +#include "HoudiniApi.h" +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniDataLayerUtils.h" +#include "HoudiniDataTableTranslator.h" +#include "HoudiniEngine.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineString.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniFoliageTools.h" +#include "HoudiniFoliageUtils.h" +#include "HoudiniGeometryCollectionTranslator.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniHLODLayerUtils.h" +#include "HoudiniInput.h" +#include "HoudiniInstanceTranslator.h" +#include "HoudiniLandscapeTranslator.h" +#include "HoudiniLandscapeSplineTranslator.h" +#include "HoudiniLevelInstanceUtils.h" +#include "HoudiniMeshTranslator.h" +#include "HoudiniNodeSyncComponent.h" +#include "HoudiniOutput.h" +#include "HoudiniSkeletalMeshTranslator.h" +#include "HoudiniSplineComponent.h" +#include "HoudiniSplineTranslator.h" +#include "HoudiniStaticMesh.h" +#include "HoudiniTextureTranslator.h" +#if defined(HOUDINI_USE_PCG) +#include "HoudiniPCGTranslator.h" +#endif +#include "Components/HierarchicalInstancedStaticMeshComponent.h" +#include "Editor.h" +#include "EditorSupportDelegates.h" +#include "Engine/UserDefinedStruct.h" +#include "Engine/WorldComposition.h" +#include "FileHelpers.h" +#include "HAL/PlatformFileManager.h" +#include "HAL/FileManager.h" +#include "InstancedFoliageActor.h" +#include "LandscapeInfo.h" +#include "Modules/ModuleManager.h" +#include "WorldBrowserModule.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollection/GeometryCollectionObject.h" +#else + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionObject.h" +#endif + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + + +// +bool +FHoudiniOutputTranslator::UpdateOutputs( + UHoudiniCookable* HC) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::UpdateOutputs-Cookable); + + if (!IsValid(HC)) + return false; + + if (!HC->IsOutputSupported() || !HC->OutputData) + return false; + + UObject* Outer = Cast(HC); + + // 1. Update the output objects + UpdateOutputObjects( + HC->NodeId, + HC->OutputData->Outputs, + HC->NodeIdsToCook, + HC->NodesToCookCookCounts, + Outer, + HC->OutputData->bOutputless, + HC->OutputData->bOutputTemplateGeos, + HC->OutputData->bUseOutputNodes, + HC->GetEnableCurveEditing(), + HC->OutputData->bCreateSceneComponents); + + // 2. Update tags and generic attributes on the Cookable and its component (if any) + UpdateOutputAttributesAndTags(HC); + + return true; +} + + +// +bool +FHoudiniOutputTranslator::ProcessOutputs( + UHoudiniCookable* HC, + bool& bOutHasHoudiniStaticMeshOutput) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::ProcessOutputs-Cookable); + + if (!IsValid(HC)) + return false; + + if (!HC->IsOutputSupported() || !HC->OutputData) + return false; + + // 1. Create all the outputs and their components + FHoudiniPackageParams PackageParams; + PackageParams.PackageMode = FHoudiniPackageParams::GetDefaultStaticMeshesCookMode(); + PackageParams.ReplaceMode = FHoudiniPackageParams::GetDefaultReplaceMode(); + + PackageParams.BakeFolder = HC->GetBakeFolderOrDefault(); + PackageParams.TempCookFolder = HC->GetTemporaryCookFolderOrDefault(); + + PackageParams.OuterPackage = HC->GetLevel(); + if (!PackageParams.OuterPackage) + PackageParams.OuterPackage = HC->GetPackage(); + + PackageParams.HoudiniAssetName = HC->GetHoudiniAssetName(); + PackageParams.HoudiniAssetActorName = HC->GetDisplayName(); + PackageParams.ComponentGUID = HC->CookableGUID; + PackageParams.ObjectName = FString(); + + TArray CreatedPackages; + if (!CreateAllOutputs( + HC->GetOutputs(), + HC->GetInputs(), + PackageParams, + HC->GetComponent(), // TODO COOKABLE: Use Cookable / Component here ? - need to split + HC->GetWorld(), + HC->IsProxyStaticMeshEnabled(), + HC->HasNoProxyMeshNextCookBeenRequested(), + HC->IsBakeAfterNextCookEnabled(), + HC->GetSplitMeshSupport(), + HC->GetStaticMeshGenerationProperties(), + HC->GetStaticMeshBuildSettings(), + bOutHasHoudiniStaticMeshOutput, + CreatedPackages)) + return false; + + // 2. Output cleanup + CleanOutputsPostCreate(HC->OutputData->Outputs, HC->GetWorld(), HC->HasBeenLoaded()); + + // 3. + UpdateDataLayersAndLevelInstanceOnOutput(HC->OutputData->Outputs); + + // 4. Save all created packages + if (CreatedPackages.Num() > 0) + { + // Save created packages. For example, we don't want landscape layers deleted + // along with the HDA. + FEditorFileUtils::PromptForCheckoutAndSave(CreatedPackages, true, false); + } + + return true; +} + +// +void +FHoudiniOutputTranslator::UpdateOutputObjects( + HAPI_NodeId InNodeId, + TArray>& Outputs, + const TArray& InNodeIdsToCook, + const TMap& InOutputNodeCookCounts, + UObject* InOuter, + bool bOutputless, + bool bOutputTemplateGeos, + bool bUseOutputNodes, + bool bEnableCurveEditing, + bool bCreateSceneComponents) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::UpdateOutputObjects); + + // Check if the HDA has been marked as not producing outputs + if (bOutputless) + { + ClearAndRemoveOutputs(Outputs, EHoudiniClearFlags::EHoudiniClear_Assets); + return; + } + + // + // 1. Update the output objects + // + ClearAndRemoveOutputs(Outputs, EHoudiniClearFlags::EHoudiniClear_Actors); + + TArray> NewOutputs; + if (FHoudiniOutputTranslator::BuildAllOutputs( + InNodeId, InOuter, InNodeIdsToCook, InOutputNodeCookCounts, + Outputs, NewOutputs, bOutputTemplateGeos, bUseOutputNodes, bEnableCurveEditing, bCreateSceneComponents)) + { + ClearAndRemoveOutputs(Outputs, EHoudiniClearFlags::EHoudiniClear_Assets); + + // Replace with the new parameters + Outputs = NewOutputs; + } +} + +bool +FHoudiniOutputTranslator::UpdateOutputAttributesAndTags(UHoudiniCookable* InHC) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::UpdateOutputAttributesAndTags); + + // + // Update tags and generic attributes on HAC + // + if (!InHC) + return false; + + // At the moment we don't support controlling KeepTags separately for components and actors, so if we find any + // HGPOs with KeepTags set to true, we'll keep the tags on both actors and components. In the future we may + // want to control these separately. + bool bKeepTags = false; + + // Look for detail generic property attributes on the outputs + // and see if any of them apply to the Cookable or its component/HAC + int32 NumOutputs = InHC->GetNumOutputs(); + TArray GenericAttributes; + for (int32 OutputIdx = 0; OutputIdx < NumOutputs; OutputIdx++) + { + UHoudiniOutput* CurrentOutput = InHC->GetOutputAt(OutputIdx); + if (!IsValid(CurrentOutput)) + continue; + + const TArray& CurrentOutputHGPO = CurrentOutput->GetHoudiniGeoPartObjects(); + for (auto& CurrentHGPO : CurrentOutputHGPO) + { + // We can't get generic attributes on cops - as they dont have geos + if (CurrentHGPO.Type == EHoudiniPartType::Cop) + continue; + + FHoudiniEngineUtils::GetGenericAttributeList( + CurrentHGPO.GeoId, + CurrentHGPO.PartId, + HAPI_UNREAL_ATTRIB_GENERIC_UPROP_PREFIX, + GenericAttributes, + HAPI_ATTROWNER_DETAIL); + + bKeepTags = bKeepTags || CurrentHGPO.bKeepTags; + } + } + + AActor* ActorOwner = InHC->GetOwner(); + USceneComponent* CookableComponent = InHC->GetComponent(); + if (bKeepTags == false) + { + if (ActorOwner) + ActorOwner->Tags.Empty(); + + if(CookableComponent) + CookableComponent->ComponentTags.Empty(); + } + + // Use the actor name for the log messages if possible + FString DisplayName = ActorOwner ? ActorOwner->GetActorLabel() : InHC->GetName(); + + // Attempt to apply the generic attributes to the Cookable or its component + for (const auto& CurrentPropAttribute : GenericAttributes) + { + // Get the current Property Attribute + const FString& CurrentPropertyName = CurrentPropAttribute.AttributeName; + if (CurrentPropertyName.IsEmpty()) + continue; + + // Try applying the property on the cookable first.. + if (FHoudiniGenericAttribute::UpdatePropertyAttributeOnObject(InHC, CurrentPropAttribute)) + { + // Success! + HOUDINI_LOG_MESSAGE(TEXT("Modified UProperty %s on Houdini Cookable named %s"), *CurrentPropertyName, *DisplayName); + } + + // .. then on its component if we have one + if (CookableComponent) + { + if (FHoudiniGenericAttribute::UpdatePropertyAttributeOnObject(CookableComponent, CurrentPropAttribute)) + { + // Success! + HOUDINI_LOG_MESSAGE(TEXT("Modified UProperty %s on Houdini Component named %s"), *CurrentPropertyName, *DisplayName); + } + } + } + + // TODO: Needed? + // Apply Custom prim data on the Cookable component if we have one + // This is mostly so they can be visible when selecting the HAC + UPrimitiveComponent* CookablePrimComponent = Cast(CookableComponent); + if (CookablePrimComponent) + { + TArray DetailCustomPrimData; + for (int32 OutputIdx = 0; OutputIdx < NumOutputs; OutputIdx++) + { + UHoudiniOutput* CurrentOutput = InHC->GetOutputAt(OutputIdx); + if (!IsValid(CurrentOutput)) + continue; + + const TArray& CurrentOutputHGPO = CurrentOutput->GetHoudiniGeoPartObjects(); + for (auto& CurrentHGPO : CurrentOutputHGPO) + { + // Look for detail custom prim data attributes + TArray CurrentCustomPrimData; + FHoudiniMeshTranslator::GetCustomPrimitiveData( + CurrentHGPO.GeoId, + CurrentHGPO.PartId, + 0, + CurrentCustomPrimData, + HAPI_ATTROWNER_DETAIL); + + if (!CurrentCustomPrimData.IsEmpty()) + { + if (DetailCustomPrimData.Num() < CurrentCustomPrimData.Num()) + DetailCustomPrimData.SetNum(CurrentCustomPrimData.Num()); + + for (int Idx = 0; Idx < CurrentCustomPrimData.Num(); Idx++) + DetailCustomPrimData[Idx] = CurrentCustomPrimData[Idx]; + } + } + } + + // Apply the details custom prim data found on the cookable component + if (!DetailCustomPrimData.IsEmpty()) + FHoudiniMeshTranslator::SetCustomPrimitiveData( + DetailCustomPrimData, CookablePrimComponent); + } + + return true; +} + +bool +FHoudiniOutputTranslator::CreateAllOutputs( + TArray>& Outputs, + const TArray>& Inputs, + const FHoudiniPackageParams& PackageParams, + UObject* InOuter, + UWorld* InWorld, + bool bIsProxyStaticMeshEnabled, + bool bHasNoProxyMeshNextCookBeenRequested, + bool bIsBakeAfterNextCookEnabled, + bool bSplitMeshSupport, + const FHoudiniStaticMeshGenerationProperties& InStaticMeshGenerationProperties, + const FMeshBuildSettings& InStaticMeshBuildSettings, + bool& bOutHasHoudiniStaticMeshOutput, + TArray& OutCreatedPackages) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::CreateAllOutputs); + + // + // 3. Create the actual outputs assets/components + // + + // TODO COOKABLE: Handle the case where the Out is NOT a component + // we need to split output asset creation from component creation! + UHoudiniCookable* OuterHC = Cast(InOuter); + USceneComponent* InOuterComponent = Cast(InOuter); + if (!InOuterComponent) + { + InOuterComponent = OuterHC ? OuterHC->GetComponent() : nullptr; + } + + // NOTE: The world can be NULL when, for example, when working with + // HoudiniAssetComponents in Blueprints. + if (InWorld && IsValid(InWorld->WorldComposition)) + { + // We don't want the origin to shift as we're potentially updating levels. + InWorld->WorldComposition->bTemporarilyDisableOriginTracking = true; + } + + // "Process" the mesh. + // TODO: Move this to the actual processing stage, + // And see if some of this could be threaded + + // ---------------------------------------------------- + // 3.1 Outputs prepass + // ---------------------------------------------------- + + // Collect all the landscape layers' global min/max values. + TMap LandscapeLayerGlobalMinimums; + TMap LandscapeLayerGlobalMaximums; + + // Store the instancer outputs separately so we can process them later, after all mesh output are processed. + // Determine the total number of instances, if we have more than 1 then mesh parts with instanced geo we will not create proxy meshes + // Also if we have object instancer (or oldschool attribute instancers), we won't be creating any proxy at all + TArray InstancerOutputs; + int32 NumInstances = 0; + bool bHasObjectInstancer = false; + for (auto& CurOutput : Outputs) + { + if (CurOutput->GetType() == EHoudiniOutputType::Instancer) + { + // InstancerOutputs.Add(CurOutput); + for (const FHoudiniGeoPartObject& HGPO : CurOutput->GetHoudiniGeoPartObjects()) + { + if (HGPO.Type == EHoudiniPartType::Instancer) + { + if (HGPO.InstancerType == EHoudiniInstancerType::PackedPrimitive || HGPO.InstancerType == EHoudiniInstancerType::GeometryCollection) + { + NumInstances += HGPO.PartInfo.InstanceCount; + } + else + { + NumInstances += HGPO.PartInfo.PointCount; + } + + if ((HGPO.InstancerType == EHoudiniInstancerType::ObjectInstancer) + || (HGPO.InstancerType == EHoudiniInstancerType::OldSchoolAttributeInstancer)) + { + bHasObjectInstancer = true; + } + } + } + } + else if (CurOutput->GetType() == EHoudiniOutputType::Landscape) + { + FHoudiniLandscapeTranslator::CalcHeightFieldsArrayGlobalZMinZMax(CurOutput->GetHoudiniGeoPartObjects(), LandscapeLayerGlobalMinimums, LandscapeLayerGlobalMaximums, false); + } + } + + bOutHasHoudiniStaticMeshOutput = false; + + // Get all our landscape inputs + TArray AllInputLandscapes; + FHoudiniEngineUtils::GatherLandscapeInputs(Inputs, AllInputLandscapes); + + // ---------------------------------------------------- + // 3.2 Process outputs + // ---------------------------------------------------- + // Landscape creation will cache the first tile as a reference location + // in this struct to be used by during construction of subsequent tiles. + // Landscape Size info will be cached by the first tile, similar to LandscapeReferenceLocation + FHoudiniClearedEditLayers ClearedLandscapeLayers; + TMap LandscapeMap; + + // Landscape splines track edit layers that were cleared per-landscape + TMap> ClearedLandscapeEditLayersForSplines; + + // The houdini materials that have been generated by this HDA. + // We track them to prevent recreate the same houdini material over and over if it is assigned to multiple parts. + // (this can easily happen when using packed prims) + TMap> AllOutputMaterials; + + UTexture2D* VisibleTexture = nullptr; + UMaterialInterface* VisibleMat = nullptr; + int32 NumVisibleOutputs = 0; + int32 NumTextureOutputs = 0; + int32 NumOutputs = Outputs.Num(); + bool bHasLandscape = false; + for (int32 OutputIdx = 0; OutputIdx < NumOutputs; OutputIdx++) + { + UHoudiniOutput* CurOutput = Outputs[OutputIdx]; + if (!IsValid(CurOutput)) + continue; + + FString Notification = FString::Format(TEXT("Processing output {0} / {1}..."), { FString::FromInt(OutputIdx + 1), FString::FromInt(NumOutputs) }); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Notification)); + + /* + // TODO: Cookable ? Handle this case + if (!HAC->IsOutputTypeSupported(CurOutput->GetType())) + { + continue; + } + */ + + switch (CurOutput->GetType()) + { + case EHoudiniOutputType::Mesh: + { + bool bEnableProxy = + bIsProxyStaticMeshEnabled && !bHasNoProxyMeshNextCookBeenRequested && !bIsBakeAfterNextCookEnabled; + + if (bEnableProxy && NumInstances > 1) + { + if (bHasObjectInstancer) + { + // Completely disable proxies if we have object instancers/old school attribute instancers + // as they rely on having a static mesh created (and the instanced mesh HGPO is not marked as instanced...) + bEnableProxy = false; + } + else + { + // If we dont have proxy instancer, enable proxy only for non-instanced mesh + for (const FHoudiniGeoPartObject& HGPO : CurOutput->GetHoudiniGeoPartObjects()) + { + if (HGPO.bIsInstanced && HGPO.Type == EHoudiniPartType::Mesh) + { + bEnableProxy = false; + break; + } + } + } + } + + EHoudiniStaticMeshMethod MeshMethod = EHoudiniStaticMeshMethod::FMeshDescription; + if (bEnableProxy) + MeshMethod = EHoudiniStaticMeshMethod::UHoudiniStaticMesh; + + FHoudiniMeshTranslator::CreateAllMeshesAndComponentsFromHoudiniOutput( + CurOutput, + PackageParams, + MeshMethod, + bSplitMeshSupport, + InStaticMeshGenerationProperties, + InStaticMeshBuildSettings, + AllOutputMaterials, + InOuterComponent); + + NumVisibleOutputs++; + + // Look for UHoudiniStaticMesh in the output, and set bOutHasHoudiniStaticMeshOutput accordingly + if (bEnableProxy && !bOutHasHoudiniStaticMeshOutput) + { + bOutHasHoudiniStaticMeshOutput = bOutHasHoudiniStaticMeshOutput || CurOutput->HasAnyCurrentProxy(); + } + + // Make sure to mark the mesh output as a geometry collection mesh if it is one + if (FHoudiniGeometryCollectionTranslator::IsGeometryCollectionMesh(CurOutput)) + { + for (auto& Pair : CurOutput->OutputObjects) + Pair.Value.bIsGeometryCollectionPiece = true; + } + + break; + } + + + case EHoudiniOutputType::Curve: + { + const TArray& GeoPartObjects = CurOutput->GetHoudiniGeoPartObjects(); + if (GeoPartObjects.Num() <= 0) + continue; + + const FHoudiniGeoPartObject& CurHGPO = GeoPartObjects[0]; + if (CurOutput->IsEditableNode()) + { + if (!CurOutput->HasEditableNodeBuilt()) + { + // Editable curve, only need to be built once. + UHoudiniSplineComponent* HoudiniSplineComponent = FHoudiniSplineTranslator::CreateHoudiniSplineComponentFromHoudiniEditableNode( + CurHGPO.GeoId, + CurHGPO.PartName, + InOuterComponent); + + HoudiniSplineComponent->SetIsEditableOutputCurve(true); + + FHoudiniOutputObjectIdentifier EditableSplineComponentIdentifier; + EditableSplineComponentIdentifier.ObjectId = CurHGPO.ObjectId; + EditableSplineComponentIdentifier.GeoId = CurHGPO.GeoId; + EditableSplineComponentIdentifier.PartId = CurHGPO.PartId; + EditableSplineComponentIdentifier.PartName = CurHGPO.PartName; + + TMap& OutputObjects = CurOutput->GetOutputObjects(); + FHoudiniOutputObject& FoundOutputObject = OutputObjects.FindOrAdd(EditableSplineComponentIdentifier); + check(FoundOutputObject.OutputComponents.Num() < 2); // Multiple components not supported yet. + FoundOutputObject.OutputComponents.Empty(); + FoundOutputObject.OutputComponents.Add(HoudiniSplineComponent); + CurOutput->SetHasEditableNodeBuilt(true); + } + } + else + { + // Output curve + FHoudiniSplineTranslator::CreateAllSplinesFromHoudiniOutput(CurOutput, InOuterComponent); + NumVisibleOutputs += CurOutput->GetOutputObjects().Num(); + break; + } + } + break; + + case EHoudiniOutputType::Instancer: + InstancerOutputs.Add(CurOutput); + break; + + case EHoudiniOutputType::Landscape: + { + NumVisibleOutputs++; + + // No Cooked prefixed needed when cooking an HDA, the name is derived internally. + FString CookedPrefix; + + FHoudiniLandscapeTranslator::ProcessLandscapeOutput( + CurOutput, + AllInputLandscapes, + CookedPrefix, + InWorld, + PackageParams, + LandscapeMap, + ClearedLandscapeLayers, + OutCreatedPackages); + + bHasLandscape = true; + + for (auto& Pair : CurOutput->GetOutputObjects()) + { + UHoudiniLandscapeTargetLayerOutput* LayerOutput = Cast(Pair.Value.OutputObject); + if (IsValid(LayerOutput)) + { + ALandscapeProxy* OutputLandscape = LayerOutput->Landscape; + if (OutputLandscape && !LayerOutput->PropertyAttributes.IsEmpty()) + { + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes(OutputLandscape, LayerOutput->PropertyAttributes); + OutputLandscape->GetLandscapeInfo()->FixupProxiesTransform(); + OutputLandscape->GetLandscapeInfo()->RecreateLandscapeInfo(InWorld, true); + OutputLandscape->RecreateCollisionComponents(); + FEditorDelegates::PostLandscapeLayerUpdated.Broadcast(); + } + } + break; + } + break; + } + + case EHoudiniOutputType::DataTable: + { + for (auto&& HGPO : CurOutput->HoudiniGeoPartObjects) + { + FHoudiniDataTableTranslator::BuildDataTable(HGPO, CurOutput, PackageParams); + } + break; + } + + case EHoudiniOutputType::LandscapeSpline: + { + if (!FHoudiniLandscapeSplineTranslator::ProcessLandscapeSplineOutput( + CurOutput, + AllInputLandscapes, + InWorld, + PackageParams, + ClearedLandscapeEditLayersForSplines)) + { + break; + } + + // Translation successful + NumVisibleOutputs += CurOutput->GetOutputObjects().Num(); + break; + } + + case EHoudiniOutputType::AnimSequence: + { + FHoudiniAnimationTranslator::CreateAnimSequenceFromOutput(CurOutput, PackageParams, InOuterComponent); + break; + } + + case EHoudiniOutputType::PCG: + { +#if defined(HOUDINI_USE_PCG) + FHoudiniPCGTranslator::CreatePCGFromOutput(CurOutput); +#endif + break; + } + + case EHoudiniOutputType::Skeletal: + { + FHoudiniSkeletalMeshTranslator::ProcessSkeletalMeshOutputs( + CurOutput, PackageParams, AllOutputMaterials, InOuterComponent); + + NumVisibleOutputs += CurOutput->GetOutputObjects().Num(); + break; + } + + case EHoudiniOutputType::Cop: + { + FHoudiniTextureTranslator::ProcessCopOutput(CurOutput, PackageParams); + + NumTextureOutputs += CurOutput->GetOutputObjects().Num(); + for (auto& It : CurOutput->GetOutputObjects()) + { + // If we haven;t already selected a texture to display.. + if (IsValid(VisibleTexture)) + break; + + // ... Get the first valid texture for display purpose + VisibleTexture = Cast(It.Value.OutputObject); + if (IsValid(VisibleTexture)) + break; + } + break; + } + + default: + // Do Nothing for now + break; + } + + for (auto& CurMat : CurOutput->AssignmentMaterialsById) + { + // Add the newly generated materials if any + if (!AllOutputMaterials.Contains(CurMat.Key)) + AllOutputMaterials.Add(CurMat); + } + } + + bool HasGeometryCollection = false; + + // Now that all meshes have been created, process the instancers + int InstanceCount = FHoudiniInstanceTranslator::CreateAllInstancersFromHoudiniOutputs(Outputs, InOuterComponent, PackageParams); + NumVisibleOutputs += InstanceCount; + + for (auto& CurOutput : InstancerOutputs) + { + if (!HasGeometryCollection && FHoudiniGeometryCollectionTranslator::IsGeometryCollectionInstancer(CurOutput)) + { + HasGeometryCollection = true; + break; + } + } + + if (HasGeometryCollection) + { + UObject* OutputOwner = OuterHC ? Cast(OuterHC) : Cast(InOuterComponent); + + FHoudiniGeometryCollectionTranslator::SetupGeometryCollectionComponentFromOutputs(Outputs, OutputOwner, InOuterComponent, PackageParams, InWorld); + } + + if (NumVisibleOutputs > 0) + { + // If we have valid outputs, we don't need to display the houdini logo anymore... + FHoudiniEngineUtils::RemoveHoudiniLogoFromComponent(InOuterComponent); + // .. or the default COP nesh + FHoudiniEngineUtils::RemoveTextureMeshFromComponent(InOuterComponent); + } + else if (NumTextureOutputs > 0) + { + // Create a temporary material to display the texture + if (VisibleTexture) + { + // Fully stream in the texture before drawing it. + // Not doing this would cause the texture to appear blurry in the ortho viewport + VisibleTexture->SetForceMipLevelsToBeResident(30.0f); + VisibleTexture->WaitForStreaming(); + + VisibleMat = FHoudiniTextureTranslator::CreateDefaultCopMaterialForTexture(VisibleTexture, PackageParams); + } + + // ... if we only have texture outputs, use a texture quad + FHoudiniEngineUtils::AddTextureMeshToComponent(InOuterComponent, VisibleTexture, VisibleMat); + } + else + { + // ... if we don't have any valid outputs however, display the Houdini logo + FHoudiniEngineUtils::AddHoudiniLogoToComponent(InOuterComponent); + } + + return true; +} + + +void +FHoudiniOutputTranslator::CleanOutputsPostCreate( + TArray>& Outputs, + UWorld* InWorld, + bool bHasBeenLoaded) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::CleanOutputsPostCreate); + + // + // 4. Output cleanup + // + + // We need to recreate component states for landscapes if a tile was created, moved, or resized + // otherwise the landscape will exhibit render artifacts (such as only rendering every other + // component.) + bool bHasLandscapeOutput = false; + for (UHoudiniOutput* CurrentOutput : Outputs) + { + if (CurrentOutput->GetType() != EHoudiniOutputType::Landscape) + continue; + + for (auto& Elem : CurrentOutput->GetOutputObjects()) + { + UHoudiniLandscapePtr* LandscapePtr = Cast(Elem.Value.OutputObject); + if (!IsValid(LandscapePtr)) + continue; + + ALandscapeProxy* LandscapeProxy = LandscapePtr->GetRawPtr(); + if (IsValid(LandscapeProxy)) + { + bHasLandscapeOutput = true; + LandscapeProxy->RecreateComponentsState(); + } + } + } + + if (bHasLandscapeOutput) + { + // Recreate Landscape Info calls WorldChange, so no need to do it manually. + if(InWorld) + ULandscapeInfo::RecreateLandscapeInfo(InWorld, true); + +#if WITH_EDITOR + if (GEditor) + { + // We force a viewport refresh since some actions, such as updating landscape + // edit layers will not reflect until the user moves the viewport camera. + GEditor->RedrawLevelEditingViewports(true); + } +#endif + } + + + // Disable the flag that we set before starting the import process. + if (InWorld && IsValid(InWorld->WorldComposition)) + { + InWorld->WorldComposition->bTemporarilyDisableOriginTracking = false; + } + + // If the owner component was marked as loaded, unmark all outputs + if (bHasBeenLoaded) + { + for (auto& CurrentOutput : Outputs) + { + CurrentOutput->MarkAsLoaded(false); + } + } +} + +void +FHoudiniOutputTranslator::UpdateDataLayersAndLevelInstanceOnOutput( + TArray>& InOutputs) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::UpdateDataLayersAndLevelInstanceOnOutput); + + // Update Data Layers + for (auto& CurrentOutput : InOutputs) + { + for (auto& It : CurrentOutput->OutputObjects) + { + FHoudiniOutputObjectIdentifier& Id = It.Key; + FHoudiniOutputObject& Obj = It.Value; + + if (Obj.DataLayers.IsEmpty()) + Obj.DataLayers = FHoudiniDataLayerUtils::GetDataLayers(Id.GeoId, Id.PartId); + + if (Obj.HLODLayers.IsEmpty()) + Obj.HLODLayers = FHoudiniHLODLayerUtils::GetHLODLayers(Id.GeoId, Id.PartId); + } + } + + // Update Level instances + FHoudiniLevelInstanceUtils::FetchLevelInstanceParameters(InOutputs); +} + + + +bool +FHoudiniOutputTranslator::BuildStaticMeshesOnHoudiniProxyMeshOutputs( + UHoudiniCookable* HC, + bool bInDestroyProxies) +{ + if (!IsValid(HC)) + return false; + + UObject* OuterComponent = HC->GetComponent(); + if(!OuterComponent) + OuterComponent = HC; + + FHoudiniPackageParams PackageParams; + PackageParams.PackageMode = FHoudiniPackageParams::GetDefaultStaticMeshesCookMode(); + PackageParams.ReplaceMode = FHoudiniPackageParams::GetDefaultReplaceMode(); + + PackageParams.BakeFolder = HC->GetBakeFolderOrDefault(); + PackageParams.TempCookFolder = HC->GetTemporaryCookFolderOrDefault(); + + PackageParams.OuterPackage = HC->GetPackage(); + PackageParams.HoudiniAssetName = HC->GetHoudiniAssetName(); + PackageParams.HoudiniAssetActorName = HC->GetOwner()->GetActorNameOrLabel(); + PackageParams.ComponentGUID = HC->GetCookableGUID(); + PackageParams.ObjectName = FString(); + + // Keep track of all generated houdini materials to avoid recreating them over and over + TMap> AllOutputMaterials; + + bool bFoundProxies = false; + TArray InstancerOutputs; + for(int Idx = 0; Idx < HC->GetNumOutputs(); Idx++) + { + UHoudiniOutput* CurOutput = HC->GetOutputAt(Idx); + if (!CurOutput) + continue; + + const EHoudiniOutputType OutputType = CurOutput->GetType(); + if (OutputType == EHoudiniOutputType::Mesh) + { + if (CurOutput->HasAnyCurrentProxy()) + { + bFoundProxies = true; + FHoudiniMeshTranslator::CreateAllMeshesAndComponentsFromHoudiniOutput( + CurOutput, + PackageParams, + EHoudiniStaticMeshMethod::FMeshDescription, + HC->GetSplitMeshSupport(), + HC->GetStaticMeshGenerationProperties(), + HC->GetStaticMeshBuildSettings(), + AllOutputMaterials, + OuterComponent, + true, // bInTreatExistingMaterialsAsUpToDate + bInDestroyProxies + ); + } + } + else if (OutputType == EHoudiniOutputType::Instancer) + { + for (auto& CurOutputObject : CurOutput->OutputObjects) + { + if (CurOutputObject.Value.ProxyComponent != nullptr + || CurOutputObject.Value.ProxyObject != nullptr) + { + // This is a single instance instancer (a mesh) + // that will need to be rebuilt + InstancerOutputs.Add(CurOutput); + bFoundProxies = true; + } + } + } + + for (auto& CurMat : CurOutput->AssignmentMaterialsById) + { + //Adds the generated materials if any + if (!AllOutputMaterials.Contains(CurMat.Key)) + AllOutputMaterials.Add(CurMat); + } + } + + // Return if no proxies or instancers were found + if (!bFoundProxies || InstancerOutputs.Num() <= 0) + return true; + + // We might need to also rebuild some instancer outputs (single instance instancer) + // And we might need to destroy the proxies for the instancer outputs before rebuilding the instancer + if (bInDestroyProxies) + { + for (auto& CurOutput : InstancerOutputs) + { + for (auto& CurOutputObject : CurOutput->OutputObjects) + { + if (CurOutputObject.Value.ProxyComponent) + FHoudiniMeshTranslator::RemoveAndDestroyComponent(CurOutputObject.Value.ProxyComponent); + + if (IsValid(CurOutputObject.Value.ProxyObject)) + { + CurOutputObject.Value.ProxyObject->MarkAsGarbage(); + } + } + } + } + + // Rebuild the instancers + FHoudiniInstanceTranslator::CreateAllInstancersFromHoudiniOutputs(InstancerOutputs, HC->GetOutputs(), OuterComponent, PackageParams); + + return true; +} + +// +bool +FHoudiniOutputTranslator::UpdateLoadedOutputs( + HAPI_NodeId InNodeId, + TArray>& InOutputs, + USceneComponent* InComponent, + const bool bEnableCurveEditing) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::UpdateLoadedOutputs); + + // Retrieve information about each object contained within our asset. + TArray ObjectInfos; + TArray ObjectTransforms; + if (!FHoudiniEngineUtils::HapiGetObjectInfos(InNodeId, ObjectInfos, ObjectTransforms)) + return false; + + TArray EditableCurveObjIds; + TArray EditableCurveGeoIds; + TArray EditableCurvePartIds; + TArray EditableCurvePartNames; + + // + // LEGACY WORKFLOW - Editable Curves + // + // Look for editable curve nodes in the asset if the EnableEditableCurve option is enabled + if (bEnableCurveEditing) + { + // Iterate through all objects to get all editable curve's object geo and part Ids. + for (int32 ObjectId = 0; ObjectId < ObjectInfos.Num(); ++ObjectId) + { + // Retrieve the object info + const HAPI_ObjectInfo& CurrentHapiObjectInfo = ObjectInfos[ObjectId]; + + // Cache/convert them + FHoudiniObjectInfo CurrentObjectInfo; + CacheObjectInfo(CurrentHapiObjectInfo, CurrentObjectInfo); + + // Start by getting the number of editable nodes + int32 EditableNodeCount = 0; + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::UpdateLoadedOutputs - ComposeChildNodeList - EditableNodes); + HOUDINI_CHECK_ERROR(FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), + CurrentHapiObjectInfo.nodeId, + HAPI_NODETYPE_SOP, + HAPI_NODEFLAGS_EDITABLE | HAPI_NODEFLAGS_NON_BYPASS, + true, + &EditableNodeCount)); + } + + if (EditableNodeCount > 0) + { + TArray EditableNodeIds; + EditableNodeIds.SetNumUninitialized(EditableNodeCount); + HOUDINI_CHECK_ERROR(FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), + InNodeId, EditableNodeIds.GetData(), EditableNodeCount)); + + for (int32 nEditable = 0; nEditable < EditableNodeCount; nEditable++) + { + HAPI_GeoInfo CurrentEditableGeoInfo; + FHoudiniApi::GeoInfo_Init(&CurrentEditableGeoInfo); + HOUDINI_CHECK_ERROR(FHoudiniApi::GetGeoInfo( + FHoudiniEngine::Get().GetSession(), + EditableNodeIds[nEditable], &CurrentEditableGeoInfo)); + + // Do not process the main display geo twice! + if (CurrentEditableGeoInfo.isDisplayGeo) + continue; + + // We only handle editable curves for now + if (CurrentEditableGeoInfo.type != HAPI_GeoType::HAPI_GEOTYPE_CURVE) + continue; + + // Check if the curve is closed (-1 unknown, could not find parameter on node). A closed curve will + // be returned as a mesh by HAPI instead of a curve + int32 CurveClosed = -1; + if (!FHoudiniEngineUtils::HapiGetParameterDataAsInteger( + EditableNodeIds[nEditable], HAPI_UNREAL_PARAM_CURVE_CLOSED, 0, CurveClosed)) + { + CurveClosed = -1; + } + else + { + if (CurveClosed) + CurveClosed = 1; + else + CurveClosed = 0; + } + + // Cook the editable node to get its parts + if (CurrentEditableGeoInfo.partCount <= 0) + { + //HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + //FHoudiniApi::CookNode(FHoudiniEngine::Get().GetSession(), CurrentEditableGeoInfo.nodeId, &CookOptions); + FHoudiniEngineUtils::HapiCookNode(CurrentEditableGeoInfo.nodeId, nullptr, true); + + HOUDINI_CHECK_ERROR(FHoudiniApi::GetGeoInfo( + FHoudiniEngine::Get().GetSession(), + CurrentEditableGeoInfo.nodeId, + &CurrentEditableGeoInfo)); + } + + // Iterate on this geo's parts + for (int32 PartId = 0; PartId < CurrentEditableGeoInfo.partCount; ++PartId) + { + // Get part information. + HAPI_PartInfo CurrentHapiPartInfo; + FHoudiniApi::PartInfo_Init(&CurrentHapiPartInfo); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetPartInfo( + FHoudiniEngine::Get().GetSession(), CurrentEditableGeoInfo.nodeId, PartId, &CurrentHapiPartInfo)) + continue; + + // A closed curve will be returned as a mesh in HAPI + if (CurrentHapiPartInfo.type != HAPI_PartType::HAPI_PARTTYPE_CURVE && + (CurveClosed <= 0 || CurrentHapiPartInfo.type != HAPI_PartType::HAPI_PARTTYPE_MESH)) + continue; + + // Get the editable curve's part name + FHoudiniEngineString hapiSTR(CurrentHapiPartInfo.nameSH); + FString PartName; + hapiSTR.ToFString(PartName); + + EditableCurveObjIds.Add(CurrentHapiObjectInfo.nodeId); + EditableCurveGeoIds.Add(CurrentEditableGeoInfo.nodeId); + EditableCurvePartIds.Add(CurrentHapiPartInfo.id); + EditableCurvePartNames.Add(PartName); + } + } + } + } + } + + int32 Idx = 0; + for (auto& CurrentOutput : InOutputs) + { + if (!CurrentOutput->IsEditableNode()) + { + // Output curve + FHoudiniSplineTranslator::CreateAllSplinesFromHoudiniOutput(CurrentOutput, InComponent); + } + else if (bEnableCurveEditing) + { + // + // LEGACY WORKFLOW - Editable Curves + // + // The HAC is Loaded, re-assign node id to its editable curves + if (CurrentOutput->HasEditableNodeBuilt()) + { + TMap& OutputObjects = CurrentOutput->GetOutputObjects(); + for (auto& Pair : OutputObjects) + { + if (Idx >= EditableCurvePartIds.Num()) + break; + + for(auto Component : Pair.Value.OutputComponents) + { + UHoudiniSplineComponent * HoudiniSplineComponent = Cast(Component); + if (IsValid(HoudiniSplineComponent)) + { + HoudiniSplineComponent->SetNodeId(EditableCurveGeoIds[Idx]); + + Pair.Key.ObjectId = EditableCurveObjIds[Idx]; + Pair.Key.GeoId = EditableCurveGeoIds[Idx]; + Pair.Key.PartId = EditableCurvePartIds[Idx]; + Pair.Key.PartName = EditableCurvePartNames[Idx]; + + Idx += 1; + } + } + } + } + else + { + // The HAC is a Duplication, re-construct output objects with attached duplicated editable curves, matching by part name + const TArray& Children = IsValid(InComponent) ? InComponent->GetAttachChildren() : TArray(); + for (auto & CurAttachedComp : Children) + { + if (!IsValid(CurAttachedComp)) + continue; + + if (!CurAttachedComp->IsA()) + continue; + + UHoudiniSplineComponent * CurAttachedSplineComp = Cast(CurAttachedComp); + if (!CurAttachedSplineComp) + continue; + + if (!CurAttachedSplineComp->IsEditableOutputCurve()) + continue; + + if (Idx >= EditableCurvePartIds.Num()) + break; + + // Found a match + if (CurAttachedSplineComp->GetGeoPartName().Equals(EditableCurvePartNames[Idx])) + { + FHoudiniOutputObjectIdentifier EditableSplineComponentIdentifier; + EditableSplineComponentIdentifier.ObjectId = EditableCurveObjIds[Idx]; + EditableSplineComponentIdentifier.GeoId = EditableCurveGeoIds[Idx]; + EditableSplineComponentIdentifier.PartId = EditableCurvePartIds[Idx]; + EditableSplineComponentIdentifier.PartName = EditableCurvePartNames[Idx]; + + CurAttachedSplineComp->SetNodeId(EditableSplineComponentIdentifier.GeoId); + + TMap& OutputObjects = CurrentOutput->GetOutputObjects(); + FHoudiniOutputObject& NewOutputObject = OutputObjects.FindOrAdd(EditableSplineComponentIdentifier); + check(NewOutputObject.OutputComponents.Num() < 2); // Multiple components not supported yet. + NewOutputObject.OutputComponents.Empty(); + NewOutputObject.OutputComponents.Add(CurAttachedSplineComp); + CurrentOutput->SetHasEditableNodeBuilt(true); + + // Never add additional rot/scale attributes on editable curves as this crashes HAPI + FHoudiniSplineTranslator::HapiUpdateNodeForHoudiniSplineComponent( + CurAttachedSplineComp, false); + + Idx += 1; + break; + } + } + } + } + + // Mark our outputs as loaded so they can be matched for potential reuse + // This indicates that the HGPO's ids are invalid and that HGPO should be matched using partnames instead + CurrentOutput->MarkAsLoaded(true); + } + + return true; +} + +// +bool +FHoudiniOutputTranslator::UploadChangedEditableOutput( + TArray>& InOutputs) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::UploadChangedEditableOutput); + + // Iterate through the outputs array. + for (auto& CurrentOutput : InOutputs) + { + if (!CurrentOutput) + continue; + + // Only update the editable nodes that have been built before. + if (!CurrentOutput->IsEditableNode() || !CurrentOutput->HasEditableNodeBuilt()) + continue; + + for (auto& CurrentOutputObj : CurrentOutput->GetOutputObjects()) + { + for(auto Component : CurrentOutputObj.Value.OutputComponents) + { + UHoudiniSplineComponent* HoudiniSplineComponent = Cast(Component); + if (!IsValid(HoudiniSplineComponent)) + continue; + + if (!HoudiniSplineComponent->HasChanged()) + continue; + + // Dont add rot/scale on editable curves as this crashes HAPI + if (FHoudiniSplineTranslator::HapiUpdateNodeForHoudiniSplineComponent( + HoudiniSplineComponent, false)) + HoudiniSplineComponent->MarkChanged(false); + else + HoudiniSplineComponent->SetNeedsToTriggerUpdate(false); + } + } + } + + return true; +} + + +bool +FHoudiniOutputTranslator::BuildAllOutputs( + HAPI_NodeId AssetId, + UObject* InOuterObject, + const TArray& OutputNodes, + const TMap& OutputNodeCookCounts, + TArray>& InOldOutputs, + TArray>& OutNewOutputs, + bool InOutputTemplatedGeos, + bool InUseOutputNodes, + bool bGatherEditableCurves, + bool bCreateSceneComponents) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::BuildAllOutputs); + + // NOTE: This function still gathers output nodes from the asset id. This is old behaviour. + // Output nodes are now being gathered before cooking starts and is passed in through + // the OutputNodes array. Clean up this function by only using output nodes from the + // aforementioned array. + + // Ensure the asset has a valid node ID + if (AssetId < 0) + { + return false; + } + + // See if we are a NodeSync component + bool bIsNodeSyncComponent = InOuterObject ? InOuterObject->IsA() : false; + UHoudiniCookable* OuterHC = Cast(InOuterObject); + if (IsValid(OuterHC)) + { + UHoudiniNodeSyncComponent* OuterHNSC = Cast(OuterHC->GetComponent()); + bIsNodeSyncComponent = IsValid(OuterHNSC); + } + + // Get the AssetInfo + HAPI_AssetInfo AssetInfo; + FHoudiniApi::AssetInfo_Init(&AssetInfo); + bool bAssetInfoSuccess = (HAPI_RESULT_SUCCESS == FHoudiniApi::GetAssetInfo( + FHoudiniEngine::Get().GetSession(), AssetId, &AssetInfo)); + + // Get the Asset NodeInfo + HAPI_NodeInfo AssetNodeInfo; + FHoudiniApi::NodeInfo_Init(&AssetNodeInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), AssetId, &AssetNodeInfo), false); + + // If the node is a COP node, add a Cop HoudiniOutput which only stores the node id; no geo info + if (AssetNodeInfo.type == HAPI_NODETYPE_COP || AssetNodeInfo.type == HAPI_NODETYPE_COP2) + { + // Add multiple COP outputs if needed + for (int32 OutputIdx = 0; OutputIdx < AssetNodeInfo.outputCount; OutputIdx++) + { + FHoudiniGeoPartObject currentHGPO; + currentHGPO.GeoId = AssetNodeInfo.id; + currentHGPO.PartId = OutputIdx;// 0; + currentHGPO.Type = EHoudiniPartType::Cop; + + HAPI_StringHandle NameSH; + FHoudiniApi::GetNodeOutputName( + FHoudiniEngine::Get().GetSession(), + AssetNodeInfo.id, + OutputIdx, + &NameSH); + + FString OutputName; + FHoudiniEngineString::ToFString(NameSH, OutputName); + + // Use the default name "outputX" to extract the image if we have multiple outputs + // and were unable to get the output name + if (AssetNodeInfo.outputCount > 1 && OutputName.IsEmpty()) + OutputName = TEXT("output") + FString::FromInt(OutputIdx + 1); + + // Store the output name in the HGPO + currentHGPO.PartName = OutputName; + + TObjectPtr Output = + NewObject( + InOuterObject, + UHoudiniOutput::StaticClass(), + FName(OutputName),//NAME_None, + RF_NoFlags); + + Output->AddNewHGPO(currentHGPO); + Output->UpdateOutputType(); + Output->bCreateSceneComponents = bCreateSceneComponents; + OutNewOutputs.Add(Output); + } + + return true; + } + + if (!bAssetInfoSuccess) + { + // IF we try to pull outputs from a non asset node, fix a few things + AssetInfo.nameSH = AssetNodeInfo.nameSH; + AssetInfo.nodeId = AssetNodeInfo.id; + + // TODO: improve this? + AssetInfo.objectNodeId = (AssetNodeInfo.type == HAPI_NODETYPE_OBJ) ? AssetNodeInfo.id : AssetNodeInfo.parentId; + } + + FString CurrentAssetName; + { + FHoudiniEngineString hapiSTR(AssetInfo.nameSH); + hapiSTR.ToFString(CurrentAssetName); + } + + // In certain cases, such as PDG output processing we might end up with a SOP node instead of a + // container. In that case, don't try to run child queries on this node. They will fail. + const bool bAssetHasChildren = !(AssetNodeInfo.type == HAPI_NODETYPE_SOP && AssetNodeInfo.childNodeCount == 0); + + // Retrieve information about each object contained within our asset. + TArray ObjectInfos; + TArray ObjectTransforms; + if (!FHoudiniEngineUtils::HapiGetObjectInfos(AssetId, ObjectInfos, ObjectTransforms)) + return false; + + // Mark all the previous HGPOs on the outputs as stale + // This indicates that they were from a previous cook and should then be deleted + for (auto& CurOutput : InOldOutputs) + { + if (CurOutput) + CurOutput->MarkAllHGPOsAsStale(true); + } + + // For HF / Volumes, we only create new Outputs for height volume + // Store all the other volumes (masks etc) on the side and we will + // match them with theit corresponding height volume after + TArray UnassignedVolumeParts; + + // When receiving landscape edit layers, we are no longer to split + // outputs based on 'height' volumes + TSet TileIds; + + // + // LEGACY WORKFLOW - Editable Curves + // + // Start by getting the number of editable nodes if the EnableEditableCurve option is enabled + TArray EditableGeoInfos; + if (bGatherEditableCurves) + { + int32 EditableNodeCount = 0; + if (bAssetHasChildren) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::BuildAllOutputs - ComposeChildNodeList - EditableNodes); + HOUDINI_CHECK_ERROR(FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), + AssetId, HAPI_NODETYPE_SOP, HAPI_NODEFLAGS_EDITABLE | HAPI_NODEFLAGS_NON_BYPASS, + true, &EditableNodeCount)); + } + + // All editable nodes will be output, regardless + // of whether the subnet is considered visible or not. + if (EditableNodeCount > 0) + { + TArray EditableNodeIds; + EditableNodeIds.SetNumUninitialized(EditableNodeCount); + HOUDINI_CHECK_ERROR(FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), + AssetId, EditableNodeIds.GetData(), EditableNodeCount)); + + for (int32 nEditable = 0; nEditable < EditableNodeCount; nEditable++) + { + HAPI_GeoInfo CurrentEditableGeoInfo; + FHoudiniApi::GeoInfo_Init(&CurrentEditableGeoInfo); + HOUDINI_CHECK_ERROR(FHoudiniApi::GetGeoInfo( + FHoudiniEngine::Get().GetSession(), + EditableNodeIds[nEditable], &CurrentEditableGeoInfo)); + + // TODO: Check whether this display geo is actually being output + // Just because this is a display node doesn't mean that it will be output (it + // might be in a hidden subnet) + + // Do not process the main display geo twice! + if (CurrentEditableGeoInfo.isDisplayGeo) + continue; + + // We only handle editable curves for now + if (CurrentEditableGeoInfo.type != HAPI_GEOTYPE_CURVE || !bGatherEditableCurves) + continue; + + // Add this geo to the geo info array + EditableGeoInfos.Add(CurrentEditableGeoInfo); + } + } + } + + const bool bIsSopAsset = AssetInfo.nodeId != AssetInfo.objectNodeId; + bool bUseOutputFromSubnets = true; + if (bAssetHasChildren) + { + if (FHoudiniEngineUtils::ContainsSopNodes(AssetInfo.nodeId)) + { + // This HDA contains immediate SOP nodes. Don't look for subnets to output. + bUseOutputFromSubnets = false; + } + else + { + // Assume we're using a subnet-based HDA + bUseOutputFromSubnets = true; + } + } + else + { + // This asset doesn't have any children. Don't try to find subnets. + bUseOutputFromSubnets = false; + } + + // Before we can perform visibility checks on the Object nodes, we have + // to build a set of all the Object node ids. The 'AllObjectIds' act + // as a visibility filter. If an Object node is not present in this + // list, the content of that node will not be displayed (display / output / templated nodes). + // NOTE that if the HDA contains immediate SOP nodes we will ignore + // all subnets and only use the data outputs directly from the HDA. + + TSet AllObjectIds; + if (bUseOutputFromSubnets) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::BuildAllOutputs-ComposeChildNodeList-AllSubnets); + + int NumObjSubnets; + TArray ObjectIds; + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), + AssetId, + HAPI_NODETYPE_OBJ, + HAPI_NODEFLAGS_OBJ_SUBNET | HAPI_NODEFLAGS_NON_BYPASS, + true, + &NumObjSubnets), + false); + + ObjectIds.SetNumUninitialized(NumObjSubnets); + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), + AssetId, + ObjectIds.GetData(), + NumObjSubnets), + false); + AllObjectIds.Append(ObjectIds); + } + else + { + AllObjectIds.Add(AssetInfo.objectNodeId); + } + + TMap CurrentCookCounts; + + // Iterate through all objects. + int32 OutputIdx = 1; + for (int32 ObjectIdx = 0; ObjectIdx < ObjectInfos.Num(); ObjectIdx++) + { + // Retrieve the object info + const HAPI_ObjectInfo& CurrentHapiObjectInfo = ObjectInfos[ObjectIdx]; + + // Determine whether this object node is fully visible. + bool bObjectIsVisible = false; + HAPI_NodeId GatherOutputsNodeId = -1; // Outputs will be gathered from this node. + if (!bAssetHasChildren) + { + // If the asset doesn't have children, we have to gather outputs from the asset's parent in order to output + // this asset node + bObjectIsVisible = true; + GatherOutputsNodeId = AssetNodeInfo.parentId; + } + else if (bIsSopAsset && CurrentHapiObjectInfo.nodeId == AssetInfo.objectNodeId) + { + // When dealing with a SOP asset, be sure to gather outputs from the SOP node, not the + // outer object node. + bObjectIsVisible = true; + GatherOutputsNodeId = AssetInfo.nodeId; + } + else + { + bObjectIsVisible = FHoudiniEngineUtils::IsObjNodeFullyVisible(AllObjectIds, AssetId, CurrentHapiObjectInfo.nodeId); + GatherOutputsNodeId = CurrentHapiObjectInfo.nodeId; + } + + // Cache/convert them + FHoudiniObjectInfo CurrentObjectInfo; + CacheObjectInfo(CurrentHapiObjectInfo, CurrentObjectInfo); + + // Retrieve object name. + FString CurrentObjectName = CurrentObjectInfo.Name; + + // Get transformation for this object. + FTransform TransformMatrix = FTransform::Identity; + if (ObjectTransforms.IsValidIndex(ObjectIdx)) + { + const HAPI_Transform & ObjectTransform = ObjectTransforms[ObjectIdx]; + FHoudiniEngineUtils::TranslateHapiTransform(ObjectTransform, TransformMatrix); + } + else + { + HOUDINI_LOG_WARNING( + TEXT("Creating Static Meshes: No HAPI transform for Object [%d %s] - using identity."), + CurrentHapiObjectInfo.nodeId, *CurrentObjectName); + } + + // Build an array of the geos we'll need to process + // In most case, it will only be the display geo, + // but we may also want to process editable geos as well + TArray GeoInfos; + + // These node ids may need to be cooked in order to extract part counts. + TSet ForceNodesToCook; + + // Track (heightfield) tile ids in order to determine + // when to create new tiles (used when outputting landscape edit layers). + TSet FoundTileIndices; + + // Append the initial set of editable geo infos here + // then clear the editable geo infos array since we + // only want to process them once. + GeoInfos.Append(EditableGeoInfos); + EditableGeoInfos.Empty(); + + if (bObjectIsVisible || bIsNodeSyncComponent) + { + // NOTE: The HAPI_GetDisplayGeoInfo will not always return the expected Geometry subnet's + // Display flag geometry. If the Geometry subnet contains an Object subnet somewhere, the + // GetDisplayGeoInfo will sometimes fetch the display SOP from within the subnet which is + // not what we want. + + // Resolve and gather outputs (display / output / template nodes) from the GatherOutputsNodeId. + FHoudiniEngineUtils::GatherImmediateOutputGeoInfos(GatherOutputsNodeId, + InUseOutputNodes, + InOutputTemplatedGeos, + GeoInfos, + ForceNodesToCook); + + } + + // Iterates through the geos we want to process + for (int32 GeoIdx = 0; GeoIdx < GeoInfos.Num(); GeoIdx++) + { + // Cache the geo nodes ids for this asset + const HAPI_GeoInfo& CurrentHapiGeoInfo = GeoInfos[GeoIdx]; + // We shouldn't add display nodes for cooking since the + // if (!CurrentHapiGeoInfo.isDisplayGeo) + // { + // OutNodeIdsToCook.Add(CurrentHapiGeoInfo.nodeId); + // } + + // We cannot rely on the bGeoHasChanged flag when dealing with session sync. Since the + // property will be set to false for any node that has cooked twice. Instead, we compare + // current cook counts against the last cached count that we have in order to determine + // whether geo has changed. + bool bHasChanged = false; + + if (!CurrentCookCounts.Contains(CurrentHapiGeoInfo.nodeId)) + { + CurrentCookCounts.Add(CurrentHapiGeoInfo.nodeId, FHoudiniEngineUtils::HapiGetCookCount(CurrentHapiGeoInfo.nodeId)); + } + + if (OutputNodeCookCounts.Contains(CurrentHapiGeoInfo.nodeId)) + { + // If the cook counts changed, we assume the geo has changed. + bHasChanged = OutputNodeCookCounts[CurrentHapiGeoInfo.nodeId] != CurrentCookCounts[CurrentHapiGeoInfo.nodeId]; + } + else + { + // Something is new! We don't have a cook count for this node. + bHasChanged = true; + } + + // Left in here for debugging convenience. + // if (bHasChanged) + // { + // FString NodePath; + // FHoudiniEngineUtils::HapiGetAbsNodePath(CurrentHapiGeoInfo.nodeId, NodePath); + // HOUDINI_LOG_MESSAGE(TEXT("[TaskCookAsset] We say Geo Has Changed!: %d, %s"), CurrentHapiGeoInfo.nodeId, *NodePath); + // } + + // HERE BE FUDGING! + // Change the hasGeoChanged flag on the GeoInfo to match our expectation + // of whether geo has changed. + GeoInfos[GeoIdx].hasGeoChanged = CurrentHapiGeoInfo.hasGeoChanged || bHasChanged; + + // Cook editable/templated nodes to get their parts. + if ((ForceNodesToCook.Contains(CurrentHapiGeoInfo.nodeId) && CurrentHapiGeoInfo.partCount <= 0) + || (CurrentHapiGeoInfo.isEditable && CurrentHapiGeoInfo.partCount <= 0) + || (CurrentHapiGeoInfo.isTemplated && CurrentHapiGeoInfo.partCount <= 0) + || (!CurrentHapiGeoInfo.isDisplayGeo && CurrentHapiGeoInfo.partCount <= 0)) + { + FHoudiniEngineUtils::HapiCookNode(CurrentHapiGeoInfo.nodeId, nullptr, true); + + HOUDINI_CHECK_ERROR(FHoudiniApi::GetGeoInfo( + FHoudiniEngine::Get().GetSession(), + CurrentHapiGeoInfo.nodeId, + &GeoInfos[GeoIdx])); + } + + // Cache/convert the display geo's info + FHoudiniGeoInfo CurrentGeoInfo; + CacheGeoInfo(CurrentHapiGeoInfo, CurrentGeoInfo); + + // Simply create an empty array for this geo's group names + // We might need it later for splitting + TArray GeoGroupNames; + + // Store all the sockets found for this geo's part + TArray GeoMeshSockets; + + // Flags to track whether we think this Geo respresents either a + // motion clip or a skeletal mesh + bool bIsMotionClip = false; + + // ------------------ + // Parts PrePass + // ------------------ + + // Do a pre-pass to determine whether we're dealing a motion clip of a skeletal mesh + // MotionClip: requires motion clip attrs on the packed primitive and requires bones to be present inside that same packed prim. + // Skeletal Mesh: requires a packed prim containing a mesh with capture weights. Requires a packed prim with bones. + bool bHasMotionClipTopologyFrame = false; + bool bHasMotionClipAnimFrame = false; + + //--------------------------------------------------------------------------------------------------------------------------------------- + // Fetch all part infos. In addition, cook templated geos if needed + //--------------------------------------------------------------------------------------------------------------------------------------- + + TArray HapiPartInfos; + TArray PartInfos; + HapiPartInfos.SetNum(CurrentGeoInfo.PartCount); + PartInfos.SetNum(HapiPartInfos.Num()); + + for (int32 PartId = 0; PartId < CurrentGeoInfo.PartCount; ++PartId) + { + // Get part information. + HAPI_PartInfo& CurrentHapiPartInfo = HapiPartInfos[PartId]; + FHoudiniApi::PartInfo_Init(&CurrentHapiPartInfo); + + // If the geo is templated, cook it manually + if (CurrentHapiGeoInfo.isTemplated && InOutputTemplatedGeos) + FHoudiniEngineUtils::HapiCookNode(CurrentHapiGeoInfo.nodeId, nullptr, true); + + bool bPartInfoFailed = false; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), CurrentHapiGeoInfo.nodeId, PartId, &CurrentHapiPartInfo)) + { + bPartInfoFailed = true; + + // If the geo is templated, attempt to cook it manually. + if (CurrentHapiGeoInfo.isTemplated && InOutputTemplatedGeos) + { + FHoudiniEngineUtils::HapiCookNode(CurrentHapiGeoInfo.nodeId, nullptr, true); + + HOUDINI_CHECK_ERROR(FHoudiniApi::GetGeoInfo(FHoudiniEngine::Get().GetSession(), CurrentHapiGeoInfo.nodeId, &GeoInfos[GeoIdx])); + + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), CurrentHapiGeoInfo.nodeId, PartId, &CurrentHapiPartInfo)) + { + bPartInfoFailed = false; + } + } + } + + if (bPartInfoFailed) + { + // Error retrieving part info. + HOUDINI_LOG_MESSAGE(TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d] unable to retrieve PartInfo - skipping."), + CurrentHapiObjectInfo.nodeId, *CurrentObjectName, CurrentHapiGeoInfo.nodeId, PartId); + CurrentHapiPartInfo.type = HAPI_PartType::HAPI_PARTTYPE_INVALID; + } + + // Convert/cache the part info + FHoudiniPartInfo& CurrentPartInfo = PartInfos[PartId]; + CachePartInfo(CurrentHapiPartInfo, CurrentPartInfo); + } + + //--------------------------------------------------------------------------------------------------------------------------------------- + // Motion Clip checks. Note this currently assumes one part/motion clip per geo. + //--------------------------------------------------------------------------------------------------------------------------------------- + + if (CurrentGeoInfo.PartCount >= 3) + { + HAPI_PartInfo* CurrentHapiPartInfo = &HapiPartInfos[0]; + + // Check to for motion clip topology frame + if (CurrentHapiPartInfo->type == HAPI_PARTTYPE_INSTANCER) + { + bHasMotionClipTopologyFrame = FHoudiniAnimationTranslator::IsMotionClipFrame(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo->id, false); + } + + // Check for the first motion clip anim frame + CurrentHapiPartInfo = &HapiPartInfos[2]; + if (CurrentHapiPartInfo->type == HAPI_PARTTYPE_INSTANCER) + { + bHasMotionClipAnimFrame = FHoudiniAnimationTranslator::IsMotionClipFrame(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo->id, false); + } + + if (bHasMotionClipTopologyFrame && bHasMotionClipAnimFrame) + { + // We have identified that this Geo data is likely a motion clip. + bIsMotionClip = true; + } + } + + + //--------------------------------------------------------------------------------------------------------------------------------------- + // Skeletal Mesh checks. Try to collect all the parts needed for a skeletal mesh. + //--------------------------------------------------------------------------------------------------------------------------------------- + + struct FHoudiniSkeletalMeshParts + { + int32 ShapeInstancerPartId = INDEX_NONE; + int32 ShapeMeshPartId = INDEX_NONE; + int32 PoseInstancerPartId = INDEX_NONE; + int32 PoseMeshPartId = INDEX_NONE; + int32 PhysAssetInstancerPartId = INDEX_NONE; + int32 PhysAssetMeshPartId = INDEX_NONE; + }; + + // Track shapes / skeleton parts so that we can pair them when building skeletal mesh outputs. + TMap SkelMeshParts; + + // Names of valid / complete skeletal meshes (both Rest Geometry and Packed Primitives are present). + TSet ValidSkelMeshNames; + // Track the base name for the given PartId + TMap PartIdBaseNameMap; + // Track the output associate with each skeletal mesh base name + TMap SkeletalMeshOutputs; + + + TArray SkeletalMeshPartIds; + SkeletalMeshPartIds.Init(EHoudiniPartType::Invalid, CurrentGeoInfo.PartCount); + + for (int32 PartId = 0; PartId < CurrentGeoInfo.PartCount; ++PartId) + { + HAPI_PartInfo& CurrentHapiPartInfo = HapiPartInfos[PartId]; + + if (CurrentHapiPartInfo.type == HAPI_PARTTYPE_INSTANCER) + { + // Check for skeletal mesh Rest Geometry (Shape, in Houdini terms)) + FString BaseName; + + int32 ShapeMeshPartId = INDEX_NONE; + int32 PhysAssetId = INDEX_NONE; + int32 PoseCurveId = INDEX_NONE; + + if (FHoudiniSkeletalMeshTranslator::IsRestShapeInstancer(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, BaseName, ShapeMeshPartId)) + { + PartIdBaseNameMap.Add(PartId, BaseName); + FHoudiniSkeletalMeshParts& SkelParts = SkelMeshParts.FindOrAdd(BaseName); + if (SkelParts.ShapeInstancerPartId == INDEX_NONE) + { + // Set the shape parts + SkelParts.ShapeInstancerPartId = PartId; + SkelParts.ShapeMeshPartId = ShapeMeshPartId; + PartIdBaseNameMap.Add(PartId, BaseName); + PartIdBaseNameMap.Add(ShapeMeshPartId, BaseName); + SkeletalMeshPartIds[PartId] = EHoudiniPartType::SkeletalMeshShape; + SkeletalMeshPartIds[ShapeMeshPartId] = EHoudiniPartType::SkeletalMeshShape; + ValidSkelMeshNames.Add(BaseName); + + } + } + else if (FHoudiniSkeletalMeshTranslator::IsCapturePoseInstancer(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, BaseName, PoseCurveId)) + { + PartIdBaseNameMap.Add(PartId, BaseName); + FHoudiniSkeletalMeshParts& SkelParts = SkelMeshParts.FindOrAdd(BaseName); + if (SkelParts.PoseInstancerPartId == INDEX_NONE) + { + // Set the Pose parts + SkelParts.PoseInstancerPartId = PartId; + SkelParts.PoseMeshPartId = PoseCurveId; + PartIdBaseNameMap.Add(PartId, BaseName); + PartIdBaseNameMap.Add(PoseCurveId, BaseName); + SkeletalMeshPartIds[PartId] = EHoudiniPartType::SkeletalMeshPose; + SkeletalMeshPartIds[PoseCurveId] = EHoudiniPartType::SkeletalMeshPose; + ValidSkelMeshNames.Add(BaseName); + } + } + else if (FHoudiniSkeletalMeshTranslator::IsPhysAssetInstancer(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, BaseName, PhysAssetId)) + { + PartIdBaseNameMap.Add(PartId, BaseName); + FHoudiniSkeletalMeshParts& SkelParts = SkelMeshParts.FindOrAdd(BaseName); + if (SkelParts.PhysAssetInstancerPartId == INDEX_NONE) + { + // Set the Phys Assets parts + SkelParts.PhysAssetInstancerPartId = PartId; + SkelParts.PhysAssetMeshPartId = PhysAssetId; + PartIdBaseNameMap.Add(PartId, BaseName); + PartIdBaseNameMap.Add(PhysAssetId, BaseName); + SkeletalMeshPartIds[PartId] = EHoudiniPartType::SkeletalMeshPhysAsset; + SkeletalMeshPartIds[PhysAssetId] = EHoudiniPartType::SkeletalMeshPhysAsset; + ValidSkelMeshNames.Add(BaseName); + } + } + } + } + + + //--------------------------------------------------------------------------------------------------------------------------------------- + // Parts Processing + //--------------------------------------------------------------------------------------------------------------------------------------- + + // Iterate on this geo's parts + for (int32 PartId = 0; PartId < CurrentGeoInfo.PartCount; ++PartId) + { + // Get part information. + HAPI_PartInfo CurrentHapiPartInfo = HapiPartInfos[PartId]; + FHoudiniPartInfo CurrentPartInfo = PartInfos[PartId]; + + // Unsupported/Invalid part + if (CurrentPartInfo.Type == EHoudiniPartType::Invalid) + continue; + + + // Retrieve part name. + FString CurrentPartName = CurrentPartInfo.Name; + + + // Update part/instancer type from the part infos + EHoudiniPartType CurrentPartType = EHoudiniPartType::Invalid; + EHoudiniInstancerType CurrentInstancerType = EHoudiniInstancerType::Invalid; + + bool bInstancerTypeFound = bIsMotionClip || (SkeletalMeshPartIds[PartId] != EHoudiniPartType::Invalid); + bool bIsGeometryCollection = false; + + if (CurrentHapiPartInfo.type == HAPI_PARTTYPE_INSTANCER && !bInstancerTypeFound) + { + bIsGeometryCollection = FHoudiniGeometryCollectionTranslator::IsGeometryCollectionInstancerPart(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id); + bInstancerTypeFound = true; + } + + switch (CurrentHapiPartInfo.type) + { + case HAPI_PARTTYPE_BOX: + case HAPI_PARTTYPE_SPHERE: + case HAPI_PARTTYPE_MESH: + { + if (CurrentHapiGeoInfo.type == HAPI_GEOTYPE_CURVE) + { + // Closed curve will be seen as mesh + if (FHoudiniEngineUtils::IsLandscapeSpline(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id)) + { + // This could be a landscape spline... + CurrentPartType = EHoudiniPartType::LandscapeSpline; + } + else + { + // This is actually a curve + CurrentPartType = EHoudiniPartType::Curve; + } + } + else + { + CurrentPartType = SkeletalMeshPartIds[PartId]; + + if (CurrentPartType == EHoudiniPartType::Invalid) + { + CurrentPartType = EHoudiniPartType::Mesh; + + if (bIsMotionClip) + { + // We don't care about tracking Mesh objects for motion clips. + // We just want to track the packed primitives, and extract the mesh data in the translator. + continue; + } + else if (CurrentHapiObjectInfo.isInstancer) + { + if (FHoudiniEngineUtils::IsAttributeInstancer(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, CurrentInstancerType)) + { + // That part is actually an attribute instancer + CurrentPartType = EHoudiniPartType::Instancer; + // Instancer type is set by IsAttributeInstancer + } + else + { + // That part is actually an instancer + CurrentPartType = EHoudiniPartType::Instancer; + CurrentInstancerType = EHoudiniInstancerType::ObjectInstancer; + } + + } +#if defined(HOUDINI_USE_PCG) + else if (FHoudiniPCGTranslator::IsPCGOutput(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id)) + { + CurrentPartType = EHoudiniPartType::PCG; + } +#endif + else if (CurrentHapiPartInfo.vertexCount <= 0 && CurrentHapiPartInfo.pointCount <= 0) + { + // No points, no vertices, we're likely invalid + CurrentPartType = EHoudiniPartType::Invalid; + HOUDINI_LOG_MESSAGE( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s] is a mesh with no points or vertices - skipping."), + CurrentHapiObjectInfo.nodeId, *CurrentObjectName, CurrentHapiGeoInfo.nodeId, PartId, *CurrentPartName); + } + else if (CurrentHapiPartInfo.vertexCount <= 0) + { + // This is not an instancer, we do not have vertices, but we have points + // Maybe this is a point cloud with attribute override instancing + if(FHoudiniEngineUtils::IsAttributeInstancer(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, CurrentInstancerType)) + { + // Mark it as an instancer + CurrentPartType = EHoudiniPartType::Instancer; + // Instancer type is set by IsAttributeInstancer + //CurrentInstancerType = EHoudiniInstancerType::OldSchoolAttributeInstancer; + } + else + { + // No vertices, not an instancer, just a point cloud + if (FHoudiniEngineUtils::IsValidDataTable(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id)) + { + CurrentPartType = EHoudiniPartType::DataTable; + } + else + { + CurrentPartType = EHoudiniPartType::Invalid; + } + } + } + } + } + } + break; + + case HAPI_PARTTYPE_CURVE: + { + if (SkeletalMeshPartIds[PartId] == EHoudiniPartType::SkeletalMeshPose) + { + // The Capture Pose mesh is reported by HAPI to be a Curve part type, so be sure to intercept + // it here + CurrentPartType = EHoudiniPartType::SkeletalMeshPose; + } + // Make sure that this curve is not an an attribute instancer! + else if (FHoudiniEngineUtils::IsAttributeInstancer(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, CurrentInstancerType)) + { + // Mark the part as an instancer it as an instancer + CurrentPartType = EHoudiniPartType::Instancer; + } + else if (FHoudiniEngineUtils::IsValidDataTable(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id)) + { + // the curve is actually a data table! + CurrentPartType = EHoudiniPartType::DataTable; + } + else if (FHoudiniEngineUtils::IsLandscapeSpline(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id)) + { + // the curve is actually a landscape spline! + CurrentPartType = EHoudiniPartType::LandscapeSpline; + } +#if defined(HOUDINI_USE_PCG) + else if(FHoudiniPCGTranslator::IsPCGOutput(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id)) + { + CurrentPartType = EHoudiniPartType::PCG; + } +#endif + else + { + // The curve is a curve! + CurrentPartType = EHoudiniPartType::Curve; + } + } + break; + + case HAPI_PARTTYPE_INSTANCER: + { + // This is a packed primitive instancer + CurrentPartType = EHoudiniPartType::Instancer; + if (bIsMotionClip) + { + CurrentPartType = EHoudiniPartType::MotionClip; + CurrentInstancerType = EHoudiniInstancerType::MotionClip; + } + else if (SkeletalMeshPartIds[PartId] != EHoudiniPartType::Invalid) + { + CurrentPartType = SkeletalMeshPartIds[PartId]; + CurrentInstancerType = EHoudiniInstancerType::SkeletalMesh; + } + else if (bIsGeometryCollection) + { + CurrentPartType = EHoudiniPartType::Instancer; + CurrentInstancerType = EHoudiniInstancerType::GeometryCollection; + } + else + { + CurrentPartType = EHoudiniPartType::Instancer; + CurrentInstancerType = EHoudiniInstancerType::PackedPrimitive; + } + } + break; + + case HAPI_PARTTYPE_VOLUME: + { + if (FHoudiniEngineUtils::IsValidHeightfield(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id)) + { + // Volume data, likely a Heightfield height / mask + CurrentPartType = EHoudiniPartType::Volume; + } + else + { + CurrentPartType = EHoudiniPartType::Invalid; + } + } + break; + + default: + // Unsupported Part Type + break; + } + + // There are no vertices AND no points and this part is not a packed prim instancer + if ((CurrentPartInfo.VertexCount <= 0 && CurrentPartInfo.PointCount <= 0) && + (CurrentPartType != EHoudiniPartType::Instancer || (CurrentInstancerType != EHoudiniInstancerType::PackedPrimitive && CurrentInstancerType != EHoudiniInstancerType::GeometryCollection)) && + (CurrentPartType != EHoudiniPartType::MotionClip) && + (CurrentPartType != EHoudiniPartType::SkeletalMeshPose) && + (CurrentPartType != EHoudiniPartType::SkeletalMeshShape) && + (CurrentPartType != EHoudiniPartType::SkeletalMeshPhysAsset) + ) + { + HOUDINI_LOG_MESSAGE( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s] no points or vertices found - skipping."), + CurrentHapiObjectInfo.nodeId, *CurrentObjectName, CurrentHapiGeoInfo.nodeId, PartId, *CurrentPartName); + continue; + } + + // This is an instancer with no points. + if (CurrentHapiObjectInfo.isInstancer && CurrentHapiPartInfo.pointCount <= 0) + { + HOUDINI_LOG_MESSAGE( + TEXT("Creating Static Meshes: Object [%d %s], Geo [%d], Part [%d %s] is instancer but has 0 points - skipping."), + CurrentHapiObjectInfo.nodeId, *CurrentObjectName, CurrentHapiGeoInfo.nodeId, PartId, *CurrentPartName); + continue; + } + + // Extract Mesh sockets + // Do this before ignoring invalid parts, as socket groups/attributes could be set on parts + // that don't have any mesh, just points! Those would be be considered "invalid" parts but + // could still have valid sockets! + TArray PartMeshSockets; + FHoudiniEngineUtils::AddMeshSocketsToArray_DetailAttribute(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, PartMeshSockets, CurrentHapiPartInfo.isInstanced); + FHoudiniEngineUtils::AddMeshSocketsToArray_Group(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, PartMeshSockets, CurrentHapiPartInfo.isInstanced); + + // Ignore invalid parts + if (CurrentPartType == EHoudiniPartType::Invalid) + { + if(PartMeshSockets.Num() > 0) + { + // Store these Part sockets for the Geo + // We'll copy them to the outputs produced by this Geo later + GeoMeshSockets.Append(PartMeshSockets); + } + + continue; + } + + // Build the HGPO corresponding to this part + FHoudiniGeoPartObject currentHGPO; + currentHGPO.AssetId = AssetId; + currentHGPO.AssetName = CurrentAssetName; + + currentHGPO.ObjectId = CurrentHapiObjectInfo.nodeId; + currentHGPO.ObjectName = CurrentObjectName; + + currentHGPO.GeoId = CurrentHapiGeoInfo.nodeId; + + currentHGPO.PartId = CurrentHapiPartInfo.id; + + currentHGPO.Type = CurrentPartType; + currentHGPO.InstancerType = CurrentInstancerType; + + currentHGPO.TransformMatrix = TransformMatrix; + + currentHGPO.NodePath = TEXT(""); + + currentHGPO.bIsVisible = CurrentHapiObjectInfo.isVisible && !CurrentHapiPartInfo.isInstanced; + if (bIsNodeSyncComponent) + currentHGPO.bIsVisible = true; + + currentHGPO.bIsEditable = CurrentHapiGeoInfo.isEditable; + currentHGPO.bIsInstanced = CurrentHapiPartInfo.isInstanced; + // Never consider a display geo as templated! + currentHGPO.bIsTemplated = CurrentHapiGeoInfo.isDisplayGeo ? false : CurrentHapiGeoInfo.isTemplated; + + currentHGPO.bHasGeoChanged = CurrentHapiGeoInfo.hasGeoChanged; + currentHGPO.bHasPartChanged = CurrentHapiPartInfo.hasChanged; + currentHGPO.bHasMaterialsChanged = CurrentHapiGeoInfo.hasMaterialChanged; + currentHGPO.bHasTransformChanged = CurrentHapiObjectInfo.hasTransformChanged; + + // Copy the HAPI info caches + currentHGPO.ObjectInfo = CurrentObjectInfo; + currentHGPO.GeoInfo = CurrentGeoInfo; + currentHGPO.PartInfo = CurrentPartInfo; + + currentHGPO.AllMeshSockets = PartMeshSockets; + + // If the mesh is NOT visible and is NOT instanced, skip it. + if (!currentHGPO.bIsVisible && !currentHGPO.bIsInstanced) + { + continue; + } + + // We only support meshes for templated geos + if (currentHGPO.bIsTemplated && (CurrentPartType != EHoudiniPartType::Mesh)) + continue; + + // Update the HGPO's node path + FHoudiniEngineUtils::HapiGetNodePath(currentHGPO, currentHGPO.NodePath); + + // Try to get the custom part name from attribute + FString CustomPartName; + if (FHoudiniOutputTranslator::GetCustomPartNameFromAttribute(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, CustomPartName)) + currentHGPO.SetCustomPartName(CustomPartName); + else + currentHGPO.PartName = CurrentPartName; + + FHoudiniEngineUtils::GetGenericPropertiesAttributes( + currentHGPO.GeoId, currentHGPO.PartId, + true, 0, 0, 0, + currentHGPO.GenericPropertyAttributes); + + { + currentHGPO.bKeepTags = false; + + TArray KeepTagData; + FHoudiniHapiAccessor Accessor(currentHGPO.GeoId, currentHGPO.PartId, HAPI_UNREAL_ATTRIB_TAG_KEEP); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, 1, KeepTagData); + + if (bSuccess) + { + if (KeepTagData.Num() > 0) + { + currentHGPO.bKeepTags = KeepTagData[0] == 0 ? false : true; + } + } + } + + // + // Mesh Only - Extract split groups + // + // Extract the group names used by this part to see if it will require splitting + // Only meshes can be split, via their primitive groups + TArray SplitGroupNames; + if (CurrentPartType == EHoudiniPartType::Mesh) + { + if (!CurrentHapiPartInfo.isInstanced && GeoGroupNames.Num() > 0) + { + // We are not instanced and already have extracted the geo's group names + // We can simply reuse the Geo group names / socket groups + currentHGPO.SplitGroups = GeoGroupNames; + } + else + { + // We need to get the primitive group names from HAPI + TArray GroupNames; + if (!FHoudiniEngineUtils::HapiGetGroupNames( + CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, + HAPI_GROUPTYPE_PRIM, CurrentHapiPartInfo.isInstanced, + GroupNames)) + { + GroupNames.Empty(); + } + + // Convert the string handles to FStrings + for (const FString& GroupName : GroupNames) + { + FString LodGroup = HAPI_UNREAL_GROUP_LOD_PREFIX; + FString CollisionGroup = HAPI_UNREAL_GROUP_INVISIBLE_COLLISION_PREFIX; + FString RenderedCollisionGroup = HAPI_UNREAL_GROUP_RENDERED_COLLISION_PREFIX; + if (GroupName.StartsWith(LodGroup, ESearchCase::IgnoreCase) + || GroupName.StartsWith(CollisionGroup, ESearchCase::IgnoreCase) + || GroupName.StartsWith(RenderedCollisionGroup, ESearchCase::IgnoreCase)) + //|| GroupName.StartsWith(HAPI_UNREAL_GROUP_USER_SPLIT_PREFIX, ESearchCase::IgnoreCase)) + { + // Split by collisions / lods + currentHGPO.SplitGroups.Add(GroupName); + } + } + + // Sort the Group name array by name, + // this will order the LODs and other incremental group names + currentHGPO.SplitGroups.Sort(); + + // If this part is not instanced, we can copy the geo + // group names so we can reuse them for another part + if (!CurrentHapiPartInfo.isInstanced) + { + GeoGroupNames = currentHGPO.SplitGroups; + } + } + } + + // + // Volume Only - Extract volume name/tile index + // + // Extract the volume's name, and see if a tile attribute is present + FHoudiniVolumeInfo CurrentVolumeInfo; + if (CurrentPartType == EHoudiniPartType::Volume) + { + // Get this volume's info + HAPI_VolumeInfo CurrentHapiVolumeInfo; + FHoudiniApi::VolumeInfo_Init(&CurrentHapiVolumeInfo); + + bool bVolumeValid = true; + + HAPI_Result Result = FHoudiniApi::GetVolumeInfo( + FHoudiniEngine::Get().GetSession(), + CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, + &CurrentHapiVolumeInfo); + + if (HAPI_RESULT_SUCCESS != Result) + { + HOUDINI_LOG_ERROR(TEXT("Failed to get VolumeInfo: %s"), *FHoudiniEngineUtils::GetErrorDescription(Result)); + bVolumeValid = false; + } + else if (CurrentHapiVolumeInfo.tupleSize != 1) + { + HOUDINI_LOG_ERROR(TEXT("Invalid tuple size (%d)"), CurrentHapiVolumeInfo.tupleSize); + bVolumeValid = false; + } + else if (CurrentHapiVolumeInfo.zLength != 1) + { + HOUDINI_LOG_ERROR(TEXT("Invalid zlength (%d)"), CurrentHapiVolumeInfo.zLength); + bVolumeValid = false; + } + else if (CurrentHapiVolumeInfo.storage != HAPI_STORAGETYPE_FLOAT) + { + HOUDINI_LOG_ERROR(TEXT("Invalid storage (%d)"), CurrentHapiVolumeInfo.storage); + bVolumeValid = false; + } + + // Only cache valid volumes + if (bVolumeValid) + { + // Convert/Cache the volume info + CacheVolumeInfo(CurrentHapiVolumeInfo, CurrentVolumeInfo); + + // Get the volume's name + currentHGPO.VolumeName = CurrentVolumeInfo.Name; + + // Now see if this volume has a tile attribute + TArray TileValues; + if (FHoudiniEngineUtils::GetTileAttribute(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, TileValues, HAPI_ATTROWNER_PRIM, 0, 1)) + { + if (TileValues.Num() > 0 && TileValues[0] >= 0) + currentHGPO.VolumeTileIndex = TileValues[0]; + else + currentHGPO.VolumeTileIndex = -1; + } + + currentHGPO.bHasEditLayers = FHoudiniEngineUtils::GetEditLayerName(CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, currentHGPO.VolumeLayerName, HAPI_ATTROWNER_PRIM); + } + } + currentHGPO.VolumeInfo = CurrentVolumeInfo; + + // Cache the curve info as well, and also for landscape splines + // !!! Only call GetCurveInfo if the PartType is Curve + // !!! Closed curves are actually Meshes, and calling GetCurveInfo on a Mesh will crash HAPI! + FHoudiniCurveInfo CurrentCurveInfo; + if ((CurrentPartType == EHoudiniPartType::Curve || CurrentPartType == EHoudiniPartType::LandscapeSpline) && CurrentPartInfo.Type == EHoudiniPartType::Curve) + { + HAPI_CurveInfo CurrentHapiCurveInfo; + FHoudiniApi::CurveInfo_Init(&CurrentHapiCurveInfo); + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetCurveInfo( + FHoudiniEngine::Get().GetSession(), + CurrentHapiGeoInfo.nodeId, CurrentHapiPartInfo.id, + &CurrentHapiCurveInfo)) + { + // Cache/Convert this part's curve info + CacheCurveInfo(CurrentHapiCurveInfo, CurrentCurveInfo); + } + } + currentHGPO.CurveInfo = CurrentCurveInfo; + + if (CurrentPartType == EHoudiniPartType::SkeletalMeshPose || + CurrentPartType == EHoudiniPartType::SkeletalMeshShape || + CurrentPartType == EHoudiniPartType::SkeletalMeshPhysAsset) + { + FString SkeletalMeshBaseName = PartIdBaseNameMap.FindChecked(PartId); + currentHGPO.InstancerName = SkeletalMeshBaseName; + } + + + // TODO: + // DONE? bake folders are handled out of this loop? + // See if a custom bake folder override for the mesh was assigned via the "unreal_bake_folder" attribute + //TArray BakeFolderOverrides; + + // See if we have an existing output that matches this HGPO or if we need to create a new one + // We handle volumes, motion clips and skeletal meshes differently than other outputs types. + // These are treated as a single output that has multiple HGPOs + bool IsFoundOutputValid = false; + TObjectPtr * FoundHoudiniOutput = nullptr; + if (currentHGPO.Type != EHoudiniPartType::Volume && + currentHGPO.Type != EHoudiniPartType::SkeletalMeshPose && + currentHGPO.Type != EHoudiniPartType::SkeletalMeshShape && + currentHGPO.Type != EHoudiniPartType::SkeletalMeshPhysAsset && + !bIsMotionClip) + { + // Create single output per HGPO. never reuse old outputs, as its deprecated anyway + FoundHoudiniOutput = nullptr; + } + else if (currentHGPO.Type == EHoudiniPartType::SkeletalMeshPose || + currentHGPO.Type == EHoudiniPartType::SkeletalMeshShape || + currentHGPO.Type == EHoudiniPartType::SkeletalMeshPhysAsset) + { + // Each "complete" skeletal mesh (2 packed prims) will result in a single output. + // Collect HGPOs that match the skeletal mesh BaseName into a single output. + TFunction MatchFn = [currentHGPO](UHoudiniOutput* Output) -> bool { return Output ? Output->InstancerNameMatch(currentHGPO) : false; }; + + // Look in the previous outputs if we have a match + FoundHoudiniOutput = InOldOutputs.FindByPredicate(MatchFn); + + if (FoundHoudiniOutput && IsValid(*FoundHoudiniOutput)) + IsFoundOutputValid = true; + + // If we dont have a match in the old maps, also look in the newly created outputs + if (!IsFoundOutputValid) + { + FoundHoudiniOutput = OutNewOutputs.FindByPredicate(MatchFn); + + if (FoundHoudiniOutput && IsValid(*FoundHoudiniOutput)) + IsFoundOutputValid = true; + } + } + else + { + // Collect HGPOs into a single output. + TFunction MatchFn = [currentHGPO](UHoudiniOutput* Output) -> bool { return Output ? Output->GeoMatch(currentHGPO) : false; }; + + if (currentHGPO.Type == EHoudiniPartType::Volume) + { + // Heightfields use a special match function + MatchFn = [currentHGPO](UHoudiniOutput* Output) { return Output ? Output->HeightfieldMatch(currentHGPO, true) : false; }; + } + + // Look in the previous outputs if we have a match + FoundHoudiniOutput = InOldOutputs.FindByPredicate(MatchFn); + + if (FoundHoudiniOutput && IsValid(*FoundHoudiniOutput)) + IsFoundOutputValid = true; + + // If we dont have a match in the old maps, also look in the newly created outputs + if (!IsFoundOutputValid) + { + if (currentHGPO.Type == EHoudiniPartType::Volume) + { + // Heightfields use a special match function + MatchFn = [currentHGPO](UHoudiniOutput* Output) { return Output ? Output->HeightfieldMatch(currentHGPO, false) : false; }; + } + + FoundHoudiniOutput = OutNewOutputs.FindByPredicate(MatchFn); + + if (FoundHoudiniOutput && IsValid(*FoundHoudiniOutput)) + IsFoundOutputValid = true; + } + } + + UHoudiniOutput * HoudiniOutput = nullptr; + if (IsFoundOutputValid) + { + // We can reuse the existing output + HoudiniOutput = *FoundHoudiniOutput; + HoudiniOutput->SetIsUpdating(true); + // Transfer this output from the old array to the new one + InOldOutputs.Remove(HoudiniOutput); + } + else + { + // We couldn't find a valid output object, so create a new one + if (currentHGPO.Type == EHoudiniPartType::Volume) + { + bool bBatchHGPO = false; + if(!currentHGPO.VolumeName.Equals(HAPI_UNREAL_LANDSCAPE_HEIGHT_VOLUME_NAME, ESearchCase::IgnoreCase)) + { + // This volume is not a height volume, so it will be batched into a single HGPO. + bBatchHGPO = true; + } + else if (currentHGPO.bHasEditLayers) + { + if (FoundTileIndices.Contains(currentHGPO.VolumeTileIndex)) + { + // If this volume name is height, AND we have edit layers enabled, check to see whether + // this is a new tile. If this is NOT a new tile, we assume that this is simply content + // for a new edit layer on the current tile. Batch it! + bBatchHGPO = true; + } + } + // Ensure this tile is tracked + FoundTileIndices.Add(currentHGPO.VolumeTileIndex); + + } + + // Create a new output object + //FString OutputName = TEXT("Output") + FString::FromInt(OutputIdx++); + HoudiniOutput = NewObject( + InOuterObject, + UHoudiniOutput::StaticClass(), + NAME_None,//FName(*OutputName), + RF_NoFlags); + + // Make sure the created object is valid + if (!IsValid(HoudiniOutput)) + { + //HOUDINI_LOG_WARNING("Failed to create asset output"); + continue; + } + + } + // Ensure that we always update the 'Editable' state of the output since this + // may very well change between cooks (for example, the User is editina the HDA is session sync). + HoudiniOutput->SetIsEditableNode(currentHGPO.bIsEditable && bGatherEditableCurves); + + HoudiniOutput->bCreateSceneComponents = bCreateSceneComponents; + + // Add the HGPO to the output + HoudiniOutput->AddNewHGPO(currentHGPO); + // Add this output object to the new ouput array + OutNewOutputs.AddUnique(HoudiniOutput); + } + // END: for Part + + if (GeoMeshSockets.Num() > 0) + { + // If we have any mesh socket, assign them to the HGPO for this geo + for (auto& CurNewOutput : OutNewOutputs) + { + if (!IsValid(CurNewOutput)) + continue; + + int32 FirstValidIdx = CurNewOutput->StaleCount; + if (!CurNewOutput->HoudiniGeoPartObjects.IsValidIndex(FirstValidIdx)) + FirstValidIdx = 0; + + for (int32 Idx = FirstValidIdx; Idx < CurNewOutput->HoudiniGeoPartObjects.Num(); Idx++) + { + // Only add sockets to valid/non stale HGPOs + FHoudiniGeoPartObject& CurHGPO = CurNewOutput->HoudiniGeoPartObjects[Idx]; + if (CurHGPO.ObjectId != CurrentHapiObjectInfo.nodeId) + continue; + + if (CurHGPO.GeoId != CurrentHapiGeoInfo.nodeId) + continue; + + CurHGPO.AllMeshSockets.Append(GeoMeshSockets); + } + } + } + } + // END: for GEO + } + // END: for OBJ + + // Update the output/HGPO associations from the map + // Clear the old HGPO since we don't need them anymore + for (auto& CurrentOuput : OutNewOutputs) + { + if (!IsValid(CurrentOuput)) + continue; + + CurrentOuput->DeleteAllStaleHGPOs(); + } + + // If we have unassigned volumes, + // try to find their corresponding output + if (UnassignedVolumeParts.Num() > 0) + { + for (auto& currentVolumeHGPO : UnassignedVolumeParts) + { + TObjectPtr * FoundHoudiniOutput = OutNewOutputs.FindByPredicate( + [currentVolumeHGPO](UHoudiniOutput* Output) + { + return Output ? Output->HeightfieldMatch(currentVolumeHGPO, false) : false; + }); + + if (!FoundHoudiniOutput || !IsValid(*FoundHoudiniOutput)) + { + // Skip - consider this volume as invalid + continue; + } + + // Add this HGPO to the output + (*FoundHoudiniOutput)->AddNewHGPO(currentVolumeHGPO); + } + } + + // Reuse geometry collection outputs. + TSet GCNames; + FHoudiniGeometryCollectionTranslator::GetGeometryCollectionNames(OutNewOutputs, GCNames); + for (int i = InOldOutputs.Num() - 1; i >= 0; i--) + { + UHoudiniOutput * OldOutput = InOldOutputs[i]; + if (OldOutput->Type == EHoudiniOutputType::GeometryCollection) + { + bool ReuseOutput = false; + // If a suitable output is not found in the new outputs, do not reuse the output. + for (auto & Pair : OldOutput->GetOutputObjects()) + { + if (IsValid(Pair.Value.OutputObject)) + { + if (!GCNames.Contains(Pair.Value.OutputObject->GetName())) + { + ReuseOutput = false; + AGeometryCollectionActor * Actor = Cast(Pair.Value.OutputObject); + if (IsValid(Actor)) + { + Actor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); + Actor->MarkAsGarbage(); + } + } + else + { + ReuseOutput = true; + } + } + } + + if (ReuseOutput) + { + InOldOutputs.RemoveAt(i); + OutNewOutputs.Add(OldOutput); + } + } + } + + // All our output objects now have their HGPO assigned + // We can now parse them to update the output type + for (auto& Output : OutNewOutputs) + { + Output->UpdateOutputType(); + } + + return true; +} + + +void +FHoudiniOutputTranslator::CacheObjectInfo(const HAPI_ObjectInfo& InObjInfo, FHoudiniObjectInfo& OutObjInfoCache) +{ + FHoudiniEngineString hapiSTR(InObjInfo.nameSH); + hapiSTR.ToFString(OutObjInfoCache.Name); + //OutObjInfoCache.Name = InObjInfo.nameSH; + + OutObjInfoCache.NodeId = InObjInfo.nodeId; + OutObjInfoCache.ObjectToInstanceID = InObjInfo.objectToInstanceId; + + OutObjInfoCache.bHasTransformChanged = InObjInfo.hasTransformChanged; + OutObjInfoCache.bHaveGeosChanged = InObjInfo.haveGeosChanged; + + OutObjInfoCache.bIsVisible = InObjInfo.isVisible; + OutObjInfoCache.bIsInstancer = InObjInfo.isInstancer; + OutObjInfoCache.bIsInstanced = InObjInfo.isInstanced; + + OutObjInfoCache.GeoCount = InObjInfo.geoCount; +}; + +EHoudiniGeoType +FHoudiniOutputTranslator::ConvertHapiGeoType(const HAPI_GeoType& InType) +{ + EHoudiniGeoType OutType = EHoudiniGeoType::Invalid; + switch (InType) + { + case HAPI_GEOTYPE_DEFAULT: + OutType = EHoudiniGeoType::Default; + break; + + case HAPI_GEOTYPE_INTERMEDIATE: + OutType = EHoudiniGeoType::Intermediate; + break; + + case HAPI_GEOTYPE_INPUT: + OutType = EHoudiniGeoType::Input; + break; + + case HAPI_GEOTYPE_CURVE: + OutType = EHoudiniGeoType::Curve; + break; + + default: + OutType = EHoudiniGeoType::Invalid; + break; + } + + return OutType; +} + +void +FHoudiniOutputTranslator::CacheGeoInfo(const HAPI_GeoInfo& InGeoInfo, FHoudiniGeoInfo& OutGeoInfoCache) +{ + OutGeoInfoCache.Type = ConvertHapiGeoType(InGeoInfo.type); + + FHoudiniEngineString hapiSTR(InGeoInfo.nameSH); + hapiSTR.ToFString(OutGeoInfoCache.Name); + + OutGeoInfoCache.NodeId = InGeoInfo.nodeId; + + OutGeoInfoCache.bIsEditable = InGeoInfo.isEditable; + OutGeoInfoCache.bIsTemplated = InGeoInfo.isTemplated; + OutGeoInfoCache.bIsDisplayGeo = InGeoInfo.isDisplayGeo; + OutGeoInfoCache.bHasGeoChanged = InGeoInfo.hasGeoChanged; + OutGeoInfoCache.bHasMaterialChanged = InGeoInfo.hasMaterialChanged; + + OutGeoInfoCache.PartCount = InGeoInfo.partCount; + OutGeoInfoCache.PointGroupCount = InGeoInfo.pointGroupCount; + OutGeoInfoCache.PrimitiveGroupCount = InGeoInfo.primitiveGroupCount; +}; + + +EHoudiniPartType +FHoudiniOutputTranslator::ConvertHapiPartType(const HAPI_PartType& InType) +{ + EHoudiniPartType OutType = EHoudiniPartType::Invalid; + switch (InType) + { + case HAPI_PARTTYPE_BOX: + case HAPI_PARTTYPE_SPHERE: + case HAPI_PARTTYPE_MESH: + OutType = EHoudiniPartType::Mesh; + break; + + case HAPI_PARTTYPE_CURVE: + OutType = EHoudiniPartType::Curve; + break; + + case HAPI_PARTTYPE_INSTANCER: + OutType = EHoudiniPartType::Instancer; + break; + + case HAPI_PARTTYPE_VOLUME: + OutType = EHoudiniPartType::Volume; + break; + + default: + OutType = EHoudiniPartType::Invalid; + break; + } + + return OutType; +} + +void +FHoudiniOutputTranslator::CachePartInfo(const HAPI_PartInfo& InPartInfo, FHoudiniPartInfo& OutPartInfoCache) +{ + OutPartInfoCache.PartId = InPartInfo.id; + + FHoudiniEngineString hapiSTR(InPartInfo.nameSH); + hapiSTR.ToFString(OutPartInfoCache.Name); + + OutPartInfoCache.Type = ConvertHapiPartType(InPartInfo.type); + + OutPartInfoCache.FaceCount = InPartInfo.faceCount; + OutPartInfoCache.VertexCount = InPartInfo.vertexCount; + OutPartInfoCache.PointCount = InPartInfo.pointCount; + + OutPartInfoCache.PointAttributeCounts = InPartInfo.attributeCounts[HAPI_ATTROWNER_POINT]; + OutPartInfoCache.VertexAttributeCounts = InPartInfo.attributeCounts[HAPI_ATTROWNER_VERTEX]; + OutPartInfoCache.PrimitiveAttributeCounts = InPartInfo.attributeCounts[HAPI_ATTROWNER_PRIM]; + OutPartInfoCache.DetailAttributeCounts = InPartInfo.attributeCounts[HAPI_ATTROWNER_DETAIL]; + + OutPartInfoCache.bIsInstanced = InPartInfo.isInstanced; + + OutPartInfoCache.InstancedPartCount = InPartInfo.instancedPartCount; + OutPartInfoCache.InstanceCount = InPartInfo.instanceCount; + + OutPartInfoCache.bHasChanged = InPartInfo.hasChanged; +}; + +void +FHoudiniOutputTranslator::CacheVolumeInfo(const HAPI_VolumeInfo& InVolumeInfo, FHoudiniVolumeInfo& OutVolumeInfoCache) +{ + FHoudiniEngineString hapiSTR(InVolumeInfo.nameSH); + hapiSTR.ToFString(OutVolumeInfoCache.Name); + + OutVolumeInfoCache.bIsVDB = (InVolumeInfo.type == HAPI_VOLUMETYPE_VDB); // replaces VolumeType Type; + + OutVolumeInfoCache.TupleSize = InVolumeInfo.tupleSize; + OutVolumeInfoCache.bIsFloat = (InVolumeInfo.storage == HAPI_STORAGETYPE_FLOAT); // replaces StorageType StorageType; + OutVolumeInfoCache.TileSize = InVolumeInfo.tileSize; + + FHoudiniEngineUtils::TranslateHapiTransform(InVolumeInfo.transform, OutVolumeInfoCache.Transform); + OutVolumeInfoCache.bHasTaper = InVolumeInfo.hasTaper; + + OutVolumeInfoCache.XLength = InVolumeInfo.xLength; + OutVolumeInfoCache.YLength = InVolumeInfo.yLength; + OutVolumeInfoCache.ZLength = InVolumeInfo.zLength; + + OutVolumeInfoCache.MinX = InVolumeInfo.minX; + OutVolumeInfoCache.MinY = InVolumeInfo.minY; + OutVolumeInfoCache.MinZ = InVolumeInfo.minZ; + + OutVolumeInfoCache.XTaper = InVolumeInfo.xTaper; + OutVolumeInfoCache.YTaper = InVolumeInfo.yTaper; +}; + +EHoudiniCurveType +FHoudiniOutputTranslator::ConvertHapiCurveType(const HAPI_CurveType& InType) +{ + EHoudiniCurveType OutType = EHoudiniCurveType::Invalid; + switch (InType) + { + case HAPI_CURVETYPE_LINEAR: + OutType = EHoudiniCurveType::Polygon; + break; + + case HAPI_CURVETYPE_NURBS: + OutType = EHoudiniCurveType::Nurbs; + break; + + case HAPI_CURVETYPE_BEZIER: + OutType = EHoudiniCurveType::Bezier; + break; + + case HAPI_CURVETYPE_MAX: + OutType = EHoudiniCurveType::Points; + break; + + default: + OutType = EHoudiniCurveType::Invalid; + break; + } + + return OutType; +} + +void +FHoudiniOutputTranslator::CacheCurveInfo(const HAPI_CurveInfo& InCurveInfo, FHoudiniCurveInfo& OutCurveInfoCache) +{ + OutCurveInfoCache.Type = ConvertHapiCurveType(InCurveInfo.curveType); + + OutCurveInfoCache.CurveCount = InCurveInfo.curveCount; + OutCurveInfoCache.VertexCount = InCurveInfo.vertexCount; + OutCurveInfoCache.KnotCount = InCurveInfo.knotCount; + + OutCurveInfoCache.bIsPeriodic = InCurveInfo.isPeriodic; + OutCurveInfoCache.bIsRational = InCurveInfo.isRational; + + OutCurveInfoCache.Order = InCurveInfo.order; + + OutCurveInfoCache.bHasKnots = InCurveInfo.hasKnots; +}; + + +void +FHoudiniOutputTranslator::ClearAndRemoveOutputs(TArray>& OutputsToClear, EHoudiniClearFlags ClearFlags) +{ + // DO NOT MANUALLY DESTROY THE OLD/DANGLING OUTPUTS! + // This messes up unreal's Garbage collection and would cause crashes on duplication + for (auto& OldOutput : OutputsToClear) + { + if (!OldOutput) + continue; + + OldOutput->DestroyCookedData(ClearFlags); + } + + // Simply clearing the array is enough + OutputsToClear.Empty(); +} + +void +FHoudiniOutputTranslator::ClearOutput(UHoudiniOutput* Output) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputTranslator::ClearOutput); + + switch (Output->GetType()) + { + case EHoudiniOutputType::Landscape: + { + for (auto& OutputObject : Output->GetOutputObjects()) + { + // Currently, any Landscape managed by an HDA is always present in the current level. + // Only when it gets baked will Landscapes be serialized to other levels so for now + // assume that a landscape should be available, unless explicitly deleted the user. + UHoudiniLandscapePtr* LandscapePtr = Cast(OutputObject.Value.OutputObject); + if (!LandscapePtr) + continue; + + ALandscapeProxy* Landscape = LandscapePtr->GetRawPtr(); + + if (!IsValid(Landscape) || !Landscape->IsValidLowLevel()) + continue; + + Landscape->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); + Landscape->ConditionalBeginDestroy(); + Landscape->UnregisterAllComponents(); + Landscape->Destroy(); + LandscapePtr->SetSoftPtr(nullptr); + } + } + break; + + case EHoudiniOutputType::Instancer: + { + for (auto& OutputObject : Output->GetOutputObjects()) + { + // Note: don't delete the actual components created by Unreal - they are "owned" by Unreal- instead expcility remove the foliage types. + + if (IsValid(OutputObject.Value.FoliageType)) + { + if (IsValid(OutputObject.Value.World)) + { + FHoudiniFoliageUtils::RemoveFoliageTypeFromWorld(OutputObject.Value.World, OutputObject.Value.FoliageType); + } + else + { + HOUDINI_LOG_ERROR(TEXT("Trying to delete Foliage Type, but no World was set. " + "Most likely this foliage was cooked with a previous vertion of the Houdini Plugin, try deleting the foliage manually.")); + } + } + } + } + break; + // ... Other output types ...// + + default: + break; + + } + + Output->Clear(); +} + + +bool +FHoudiniOutputTranslator::GetCustomPartNameFromAttribute( + HAPI_NodeId NodeId, + HAPI_PartId PartId, + FString& OutCustomPartName) +{ + TArray CustomNames; + + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2); + bool bHasCustomName = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, CustomNames); + + if (!bHasCustomName) + { + Accessor.Init(NodeId, PartId, HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V1); + bHasCustomName = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, CustomNames); + } + + if (!bHasCustomName) + return false; + + if (CustomNames.Num() <= 0) + return false; + + OutCustomPartName = CustomNames[0]; + + if (OutCustomPartName.IsEmpty()) + return false; + + return true; +} + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngine/Private/HoudiniOutputTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "CoreMinimal.h" +#include "HoudiniEngineRuntime.h" + +class UHoudiniOutput; +class UHoudiniInput; +class AActor; +class UActorComponent; +class USceneComponent; +class UHoudiniCookable; + +struct FHoudiniObjectInfo; +struct FHoudiniGeoInfo; +struct FHoudiniPartInfo; +struct FHoudiniVolumeInfo; +struct FHoudiniCurveInfo; +struct FHoudiniPackageParams; +struct FHoudiniStaticMeshGenerationProperties; +struct FMeshBuildSettings; + +enum class EHoudiniOutputType : uint8; +enum class EHoudiniGeoType : uint8; +enum class EHoudiniPartType : uint8; +enum class EHoudiniCurveType : int8; + +struct HOUDINIENGINE_API FHoudiniOutputTranslator +{ +public: + + // + static bool UpdateOutputs(UHoudiniCookable* HC); + + // + static bool ProcessOutputs( + UHoudiniCookable* HC, + bool& bOutHasHoudiniStaticMeshOutput); + + // + static bool BuildStaticMeshesOnHoudiniProxyMeshOutputs( + UHoudiniCookable* HC, + bool bInDestroyProxies = false); + + // + static bool UpdateLoadedOutputs( + HAPI_NodeId InNodeId, + TArray>& InOutputs, + USceneComponent* InComponent, + const bool bEnableCurveEditing); + + // + static bool UploadChangedEditableOutput( + TArray>& InOutputs); + // + static bool BuildAllOutputs( + HAPI_NodeId AssetId, + UObject* InOuterObject, + const TArray& OutputNodes, + const TMap& OutputNodeCookCounts, + TArray>& InOldOutputs, + TArray>& OutNewOutputs, + bool InOutputTemplatedGeos, + bool InUseOutputNodes, + bool bGatherEditableCurves, + bool bCreateSceneComponents); + + // Helpers functions used to convert HAPI types + static EHoudiniGeoType ConvertHapiGeoType(const HAPI_GeoType& InType); + static EHoudiniPartType ConvertHapiPartType(const HAPI_PartType& InType); + static EHoudiniCurveType ConvertHapiCurveType(const HAPI_CurveType& InType); + + // Helper functions used to cache HAPI infos + static void CacheObjectInfo(const HAPI_ObjectInfo& InObjInfo, FHoudiniObjectInfo& OutObjInfoCache); + static void CacheGeoInfo(const HAPI_GeoInfo& InGeoInfo, FHoudiniGeoInfo& OutGeoInfoCache); + static void CachePartInfo(const HAPI_PartInfo& InPartInfo, FHoudiniPartInfo& OutPartInfoCache); + static void CacheVolumeInfo(const HAPI_VolumeInfo& InVolumeInfo, FHoudiniVolumeInfo& OutVolumeInfoCache); + static void CacheCurveInfo(const HAPI_CurveInfo& InCurveInfo, FHoudiniCurveInfo& OutCurveInfoCache); + + // Helper to clear all outputs + static void ClearAndRemoveOutputs(TArray>& OutputsToClear, EHoudiniClearFlags ClearFlags); + + // Helper to clear an individual UHoudiniOutput + static void ClearOutput(UHoudiniOutput* Output); + + static bool GetCustomPartNameFromAttribute(HAPI_NodeId NodeId, HAPI_PartId PartId, FString& OutCustomPartName); + +protected: + + // 1. Update the output objects + static void UpdateOutputObjects( + HAPI_NodeId InNodeId, + TArray>& Outputs, + const TArray& InNodeIdsToCook, + const TMap& InOutputNodeCookCounts, + UObject* InOuter, + bool bOutputless, + bool bOutputTemplateGeos, + bool bUseOutputNodes, + bool bEnableCurveEditing, + bool bCreateComponents); + + // 2. Update tags and generic attributes on HAC + static bool UpdateOutputAttributesAndTags(UHoudiniCookable* InHC); + + // 3. Create the actual outputs assets/components + static bool CreateAllOutputs( + TArray>& Outputs, + const TArray>& Inputs, + const FHoudiniPackageParams& PackageParams, + UObject* InOuterComponent, + UWorld* InWorld, + bool bIsProxyStaticMeshEnabled, + bool bHasNoProxyMeshNextCookBeenRequested, + bool bIsBakeAfterNextCookEnabled, + bool bSplitMeshSupport, + const FHoudiniStaticMeshGenerationProperties& InStaticMeshGenerationProperties, + const FMeshBuildSettings& InStaticMeshBuildSettings, + bool& bOutHasHoudiniStaticMeshOutput, + TArray& OutCreatedPackages); + + // 4. Output cleanup + static void CleanOutputsPostCreate( + TArray>& Outputs, + UWorld* InWorld, + bool bHasBeenLoaded); + + // 5. Update Data Layers and level instances + static void UpdateDataLayersAndLevelInstanceOnOutput( + TArray>& InOutputs); + +}; +``` + +`Source/HoudiniEngine/Private/HoudiniPCGComponent.cpp`: + +```cpp +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "DetailCategoryBuilder.h" +#if defined(HOUDINI_USE_PCG) + +#include "HoudiniPCGComponent.h" +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "DetailLayoutBuilder.h" +#include "DetailCategoryBuilder.h" +#include "DetailWidgetRow.h" +#include "PropertyHandle.h" +#include "IDetailChildrenBuilder.h" + +// Slate UI elements +#include "Widgets/Input/SButton.h" +#include "Widgets/Text/STextBlock.h" + +// Debugging (for on-screen messages) +#include "Engine/Engine.h" + +UHoudiniPCGComponent::UHoudiniPCGComponent(class FObjectInitializer const& ObjectInitializer) +{ +} + +void UHoudiniPCGComponent::OnComponentDestroyed(bool bDestroyingHierarchy) +{ + +} + +UHoudiniPCGComponent* UHoudiniPCGComponent::CreatePCGComponent(UPCGComponent* UnrealPCComponent) +{ + const bool bUseHoudiniActor = true; + + USceneComponent* RootComponent = UnrealPCComponent->GetOwner()->GetRootComponent(); + AActor* Owner = UnrealPCComponent->GetOwner(); + + if(bUseHoudiniActor) + { + UWorld* World = UnrealPCComponent->GetWorld(); + AHoudiniPCGActor* HoudiniPCGActor = nullptr; + for(TActorIterator It(World); It; ++It) + { + HoudiniPCGActor = *It; + if(HoudiniPCGActor) + break; + } + + if(HoudiniPCGActor == nullptr) + { + FActorSpawnParameters SpawnParams; + SpawnParams.Owner = nullptr; + SpawnParams.Instigator = nullptr; + FVector SpawnLocation = UnrealPCComponent->GetOwner()->GetActorLocation(); + FRotator SpawnRotation = UnrealPCComponent->GetOwner()->GetActorRotation(); + + HoudiniPCGActor = World->SpawnActor(AHoudiniPCGActor::StaticClass(), SpawnLocation, SpawnRotation, SpawnParams); + } + + RootComponent = HoudiniPCGActor->GetRootComponent(); + if(!RootComponent) + { + RootComponent = NewObject< USceneComponent>(HoudiniPCGActor); + HoudiniPCGActor->SetRootComponent(RootComponent); + HoudiniPCGActor->AddInstanceComponent(RootComponent); + } + } + + UHoudiniPCGComponent* PCGComponent = NewObject(RootComponent); + FTransform ComponentTransform = UnrealPCComponent->GetOwner()->GetTransform(); + ComponentTransform.SetScale3D(FVector3d::One()); + PCGComponent->SetWorldTransform(ComponentTransform); + PCGComponent->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepWorldTransform); + Owner->AddInstanceComponent(PCGComponent); + PCGComponent->RegisterComponent(); + PCGComponent->PCGComponent = UnrealPCComponent; + + return PCGComponent; +} + + +TSharedRef UHoudiniPCGComponentDetails::MakeInstance() +{ + return MakeShareable(new UHoudiniPCGComponentDetails); +} + +void UHoudiniPCGComponentDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + const FName PCGCategoryName("PCG"); + IDetailCategoryBuilder& PCGCategory = DetailBuilder.EditCategory(PCGCategoryName); + + TArray> ObjectsBeingCustomized; + DetailBuilder.GetObjectsBeingCustomized(ObjectsBeingCustomized); + + TArray Components; + + for(TWeakObjectPtr Obj : ObjectsBeingCustomized) + { + if(Obj.IsValid()) + { + UHoudiniPCGComponent* Component = Cast(Obj.Get()); + if(IsValid(Component)) + Components.Add(Component); + } + } + + for (UHoudiniPCGComponent * Component : Components) + { + IDetailCategoryBuilder& MyCategory = DetailBuilder.EditCategory("Custom Category"); + + MyCategory.AddCustomRow(FText::FromString("Custom Button")) + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Action Button")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + [ + SNew(SButton) + .Text(FText::FromString("Click Me")) + .OnClicked_Lambda([Component]() + { + if(GEngine) + { + // TODO: Still needed? + } + return FReply::Handled(); + }) + ]; + } + +} + +UHoudiniPCGComponentDetails::UHoudiniPCGComponentDetails() +{ +} + + +#endif +``` + +`Source/HoudiniEngine/Private/HoudiniPCGComponent.h`: + +```h +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniPCGCookable.h" + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wshadow" +#endif + +#include "PCGComponent.h" + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#include "UObject/ObjectMacros.h" +#include "IDetailCustomization.h" +#include "HoudiniEngine.h" +#include "HoudiniPCGComponent.generated.h" + + +UCLASS(ClassGroup = (Rendering, Common), hidecategories = (Object, Activation, "Components|Activation"), ShowCategories = (Mobility), editinlinenew) +class HOUDINIENGINE_API UHoudiniPCGComponent : public USceneComponent +{ +public: + // This component is attached to an AHoudiniPCGActor which stores generated cooked data during PCG Cooks. + GENERATED_UCLASS_BODY() + + void OnComponentDestroyed(bool bDestroyingHierarchy) override; +public: + + static UHoudiniPCGComponent* CreatePCGComponent(UPCGComponent* UnrealPCGComponent); + + UPROPERTY() + TObjectPtr Cookable; + + UPROPERTY() + TObjectPtr HoudiniAsset; + + UPROPERTY() + TWeakObjectPtr PCGComponent; +}; + +class UHoudiniPCGComponentDetails : public IDetailCustomization +{ +public: + UHoudiniPCGComponentDetails(); + + // Factory method to register the customization + static TSharedRef MakeInstance(); + + // Override to customize the details panel + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; +}; + +UCLASS() +class HOUDINIENGINE_API AHoudiniPCGActor : public AActor +{ +public: + GENERATED_BODY() + +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniPCGCookable.cpp`: + +```cpp +/* +* Copyright (c) <2025> Side Effects Software Inc. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#include "HoudiniPCGNode.h" + +#include "HoudiniPCGUtils.h" +#include "PCGContext.h" +#include "PCGSettings.h" +#include "Async/PCGAsyncLoadingContext.h" +#include "PCGParamData.h" +#include "Helpers/PCGDynamicTrackingHelpers.h" +#include "UObject/FastReferenceCollector.h" +#include "HoudiniEngineRuntime.h" +#include "PCGComponent.h" + +#include "HoudiniPCGCookable.h" +#include "HoudiniParameter.h" +#include +#include +#include + +#include "HoudiniEngineManager.h" +#include "HoudiniPCGTranslator.h" +#include "Misc/StringBuilder.h" +#include "HoudiniPCGManagedResource.h" +#include "HoudiniOutputTranslator.h" +#include +#include +#include "HoudiniPCGDataObject.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniFoliageTools.h" +#include "Materials/Material.h" +#include "HoudiniLandscapeRuntimeUtils.h" +#include "Landscape.h" + +#define LOCTEXT_NAMESPACE "PCGCachedCookable" + + +UHoudiniPCGCookable::UHoudiniPCGCookable(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +UHoudiniPCGCookable::~UHoudiniPCGCookable() +{ +} + +void +UHoudiniPCGCookable::OnCookingComplete(bool bSuccess) +{ + // Ignore this callback when processing PDG. + if (bIsCookingPDG) + return; + + OnCookingCompleteInternal(bSuccess); +} + +void +UHoudiniPCGCookable::Rebuild() +{ + if(!IsValid(this->Cookable)) + return; + + this->State = EPCGCookableState::Initializing; + this->Cookable->MarkAsNeedRebuild(); +} + +void +UHoudiniPCGCookable::OnCookingCompleteInternal(bool bSuccess) +{ + switch (this->State) + { + case EPCGCookableState::Initializing: + HOUDINI_PCG_MESSAGE(TEXT("(%p) Set to EPCGCookableState::Initialized"), this); + this->State = EPCGCookableState::Initialized; + if(OnInitializedDelegate.IsBound()) + OnInitializedDelegate.Broadcast(this, bSuccess); + + break; + + default: + HOUDINI_PCG_MESSAGE(TEXT("(%p) Set to EPCGCookableState::CookingComplete"), this); + this->State = EPCGCookableState::CookingComplete; + + if(!bSuccess && !bIsCookingPDG) + AddCookError(TEXT("Houdini cook returned errors.")); + + if(OnPostOutputProcessingDelegate.IsBound()) + OnPostOutputProcessingDelegate.Broadcast(this, bSuccess); + break; + } + +} + +void +UHoudiniPCGCookable::PostLoad() +{ + Super::PostLoad(); + + State = EPCGCookableState::Loaded; + + if (this->Cookable) + { + auto OutputDelegateHandle = Cookable->GetOnPostOutputProcessingDelegate().AddLambda([this](UHoudiniCookable* _HC, bool bSuccess) + { + this->OnCookingComplete(bSuccess); + }); + } +} + +void +UHoudiniPCGCookable::PostEditImport() +{ + Super::PostEditImport(); + + State = EPCGCookableState::Loaded; + + if(this->Cookable) + { + auto OutputDelegateHandle = Cookable->GetOnPostOutputProcessingDelegate().AddLambda([this](UHoudiniCookable* _HC, bool bSuccess) + { + this->OnCookingComplete(bSuccess); + }); + } +} + + +void +UHoudiniPCGCookable::CreateHoudiniCookable(UHoudiniAsset* Asset, UHoudiniPCGSettings* Owner, UHoudiniPCGComponent* Component) +{ + HOUDINI_PCG_MESSAGE(TEXT("(%p) UHoudiniPCGCookable::CreateHoudiniCookable"), this); + + TrackedObjects.Empty(); + + Cookable = NewObject(this, NAME_None, RF_Public); + State = EPCGCookableState::Initializing; + auto OutputDelegateHandle = Cookable->GetOnPostOutputProcessingDelegate().AddLambda([this](UHoudiniCookable* _HC, bool bSuccess) + { + this->OnCookingComplete(bSuccess); + }); + + Cookable->SetDoSlateNotifications(false); + Cookable->SetAllowUpdateEditorProperties(false); + Cookable->SetParameterSupported(true); + Cookable->SetInputSupported(true); + Cookable->SetComponentSupported(Component ? true : false); + Cookable->SetEnableProxyStaticMeshOverride(false); + Cookable->SetOverrideGlobalProxyStaticMeshSettings(true); + Cookable->SetAutoCook(false); + Cookable->GetParameterData()->bCookOnParameterChange = false; + Cookable->GetInputData()->bCookOnInputChange = false; + Cookable->SetPDGSupported(true); + Cookable->SetBakingSupported(true); + Cookable->SetProxySupported(true); + + if(Component) + { + Cookable->SetComponent(Component); + } + + Cookable->SetHoudiniAssetSupported(true); + UCookableHoudiniAssetData* HAD = Cookable->GetHoudiniAssetData(); + HAD->HoudiniAsset = Asset; + this->State = EPCGCookableState::None; +} + +void +UHoudiniPCGCookable::Instantiate() +{ + HOUDINI_PCG_MESSAGE(TEXT("(%p) UHoudiniPCGCookable::Instantiate"), this); + this->State = EPCGCookableState::WaitingForSession; + FHoudiniPCGUtils::StartSessionAsync(); +} + +void +UHoudiniPCGCookable::InvalidateCookable() +{ + EHoudiniClearFlags ClearFlags = EHoudiniClearFlags::EHoudiniClear_Actors; + + if(bAutomaticallyDeleteAssets) + { + ClearFlags |= EHoudiniClearFlags::EHoudiniClear_Assets | EHoudiniClearFlags::EHoudiniClear_LandscapeLayers; + } + + FHoudiniOutputTranslator::ClearAndRemoveOutputs(this->Cookable->GetOutputs(), ClearFlags); + + if(UHoudiniPDGAssetLink * PDGAssetLink = Cookable->GetPDGAssetLink()) + { + auto* TOPNetwork = PDGAssetLink->GetSelectedTOPNetwork(); + if(IsValid(TOPNetwork)) + { + PDGAssetLink->ClearTOPNetworkWorkItemResults(TOPNetwork); + } + TOPNetwork = nullptr; + } + + if(!IsValid(this->Cookable.Get())) + return; + + this->Cookable->OnDestroy(true); + this->Cookable = nullptr; +} + +bool +UHoudiniPCGCookable::ApplyParametersToCookable(const FPCGContext* Context) +{ + const TArray Inputs = Context->InputData.GetInputsByPin(FName(FHoudiniPCGUtils::ParameterInputPinName)); + + bool bChanged = false; + for(auto& TaggedData : Inputs) + { + bChanged |= ApplyParametersToCookable(TaggedData.Data); + } + + if (bChanged) + { + // Changing the parameters will start a cook. + State = EPCGCookableState::Cooking; + } + return bChanged; +} + + +bool +UHoudiniPCGCookable::ApplyInputsToCookable(const FPCGContext* Context) +{ + int NumInputs = this->Cookable->GetNumInputs(); + + for(int Index = 0; Index < NumInputs; Index++) + { + UHoudiniInput* Input = this->Cookable->GetInputAt(Index); + + FString InputName = FHoudiniPCGUtils::GetHDAInputName(Index); + + const TArray& ContextInputData = Context->InputData.GetInputsByPin(FName(InputName)); + + if(ContextInputData.IsEmpty()) + continue; + + // See if the input contains objects, going through each tagged data. + + TArray UnrealObjectPaths; + for(const FPCGTaggedData& InputData : ContextInputData) + { + const UPCGMetadata* Metadata = InputData.Data->ConstMetadata(); + TArray UnrealObjects = GetUnrealObjectPaths(Context, Metadata); + if(!UnrealObjects.IsEmpty()) + { + UnrealObjectPaths.Append(UnrealObjects); + } + } + + switch (Input->GetInputType()) + { + case EHoudiniInputType::PCGInput: + { + if(ContextInputData.Num()) + { + UHoudiniPCGDataCollection* DataCollection = NewObject(this); + + for(const FPCGTaggedData& InputData : ContextInputData) + { + UHoudiniPCGDataObject* PCGDataObject = GetPCGDataObjects(InputData); + if(!PCGDataObject) + continue; + + DataCollection->AddObject(PCGDataObject); + } + + if(DataCollection) + bInputsChanged |= ApplyInputAsPCGData(Input, { DataCollection }); + else + bInputsChanged |= ApplyInputAsPCGData(Input, { }); + } + } + break; + case EHoudiniInputType::Geometry: + { + // Looks like we have Unreal objects, so set those on the current input. + bInputsChanged |= ApplyInputAsUnrealObjects(Context, Input, UnrealObjectPaths); + } + break; + case EHoudiniInputType::World: + { + // Looks like we have Unreal objects, so set those on the current input. + bInputsChanged |= ApplyInputAsUnrealObjects(Context, Input, UnrealObjectPaths); + } + break; + default: + break; + + } + } + + return bInputsChanged; +} + +void +UHoudiniPCGCookable::AddTrackedObjects(const FPCGContext* Context) +{ + FPCGDynamicTrackingHelper DynamicTracking; + DynamicTracking.EnableAndInitialize(Context, TrackedObjects.Num()); + for(auto& TrackedObject : TrackedObjects) + { + DynamicTracking.AddToTracking(FPCGSelectionKey::CreateFromPath(TrackedObject), false); + } + DynamicTracking.Finalize(Context); + TrackedObjects.Empty(); +} + +bool +UHoudiniPCGCookable::ApplyParametersToCookable(const UPCGData* Data) +{ + const UPCGMetadata* Metadata = Data->ConstMetadata(); + + TArray AttributeNames; + TArray AttributeTypes; + + Metadata->GetAttributes(AttributeNames, AttributeTypes); + TSet AttributeSet; + for (FName & AttrName : AttributeNames) + { + AttributeSet.Add(AttrName.ToString()); + } + + bool bChanged = false; + + for (auto & Parameter : this->Cookable->GetParameterData()->Parameters) + { + FString ParameterName = Parameter->GetParameterName(); + if (!AttributeSet.Contains(ParameterName)) + continue; + + FHoudiniPCGAttributes Attributes(Metadata, FName(ParameterName)); + if(UHoudiniParameterString* ParameterString = Cast(Parameter)) + { + TArray Values = FHoudiniPCGUtils::GetValueAsString(ParameterString->GetDefaultValues(), Attributes, 0); + bChanged |= ParameterString->SetValuesIfChanged(Values); + } + else if(UHoudiniParameterFloat* ParameterFloat = Cast(Parameter)) + { + TArray Values = FHoudiniPCGUtils::GetValueAsFloat(ParameterFloat->GetDefaultValues(), Attributes, 0); + bChanged |= ParameterFloat->SetValuesIfChanged(Values); + } + else if(UHoudiniParameterInt* ParameterInt = Cast(Parameter)) + { + TArray Values = FHoudiniPCGUtils::GetValueAsInt(ParameterInt->GetDefaultValues(), Attributes, 0); + bChanged |= ParameterInt->SetValuesIfChanged(Values); + } + else if(UHoudiniParameterToggle* ParameterToggle = Cast(Parameter)) + { + TArray Values = FHoudiniPCGUtils::GetValueAsInt(ParameterToggle->GetDefaultValues(), Attributes, 0); + bChanged |= ParameterToggle->SetValuesIfChanged(Values); + } + } + + return bChanged; +} + +void +UHoudiniPCGCookable::DestroyCookable(UWorld * World) +{ + HOUDINI_PCG_MESSAGE(TEXT("UHoudiniPCGCookable::DestroyCookable (%p)"), this); + + DeleteBakedOutput(World); + InvalidateCookable(); +} + +void +UHoudiniPCGCookable::ProcessBakedOutputs(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const FHoudiniBakedOutput* HoudiniOutput) +{ + if(FHoudiniPCGUtils::HasPCGOutputs(HoudiniOutput)) + { + CopyBakedPCGOutputDataToPinData(Context, OutputPinName, TagName, HoudiniOutput); + } + else + { + CreateOutputPinFromBakedData(Context, OutputPinName, TagName, HoudiniOutput); + } +} + +void +UHoudiniPCGCookable::ProcessCookedOutput(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const UHoudiniOutput* HoudiniOutput) +{ + if(FHoudiniPCGUtils::HasPCGOutputs(HoudiniOutput)) + { + CopyCookedPCGOutputDataToPinData(Context, OutputPinName, TagName, HoudiniOutput); + } + else + { + CreateOutputPinFromCookedData(Context, OutputPinName, TagName, HoudiniOutput); + } +} + +void +UHoudiniPCGCookable::CopyCookedPCGOutputDataToPinData(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const UHoudiniOutput* HoudiniOutput) +{ + for(auto& It : HoudiniOutput->GetOutputObjects()) + { + auto& Object = It.Value; + + if(UHoudiniPCGOutputData* PCGOutputData = Cast(Object.OutputObject)) + { + CopyPCGOutputDataToPinData(Context, OutputPinName, TagName, PCGOutputData); + } + } +} + +void +UHoudiniPCGCookable::CopyBakedPCGOutputDataToPinData(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const FHoudiniBakedOutput* HoudiniOutput) +{ + for(auto& It : HoudiniOutput->BakedOutputObjects) + { + auto& Object = It.Value; + + if(UHoudiniPCGOutputData* PCGOutputData = Cast(Object.PCGOutputData)) + { + CopyPCGOutputDataToPinData(Context, OutputPinName, TagName, PCGOutputData); + } + } +} + +void +UHoudiniPCGCookable::CopyPCGOutputDataToPinData(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const UHoudiniPCGOutputData* PCGOutputData) +{ + TArray& TaggedDataArray = Context->OutputData.TaggedData; + + if(PCGOutputData->PointParams) + { + FPCGTaggedData& TaggedOutput = TaggedDataArray.Emplace_GetRef(); + TaggedOutput.Data = PCGOutputData->PointParams; + TaggedOutput.Pin = OutputPinName; + TaggedOutput.Tags.Add(TEXT("Points")); + TaggedOutput.Tags.Add(TagName); + } + + if(PCGOutputData->VertexParams) + { + FPCGTaggedData& TaggedOutput = TaggedDataArray.Emplace_GetRef(); + TaggedOutput.Data = PCGOutputData->VertexParams; + TaggedOutput.Pin = OutputPinName; + TaggedOutput.Tags.Add(TEXT("Vertices")); + TaggedOutput.Tags.Add(TagName); + } + + if(PCGOutputData->PrimsParams) + { + FPCGTaggedData& TaggedOutput = TaggedDataArray.Emplace_GetRef(); + TaggedOutput.Data = PCGOutputData->PrimsParams; + TaggedOutput.Pin = OutputPinName; + TaggedOutput.Tags.Add(TEXT("Primitives")); + TaggedOutput.Tags.Add(TagName); + } + + if(PCGOutputData->DetailsParams) + { + FPCGTaggedData& TaggedOutput = TaggedDataArray.Emplace_GetRef(); + TaggedOutput.Data = PCGOutputData->DetailsParams; + TaggedOutput.Pin = OutputPinName; + TaggedOutput.Tags.Add(TEXT("Details")); + TaggedOutput.Tags.Add(TagName); + } + + if(!PCGOutputData->SplineParams.IsEmpty()) + { + for (auto Spline : PCGOutputData->SplineParams) + { + FPCGTaggedData& TaggedOutput = TaggedDataArray.Emplace_GetRef(); + TaggedOutput.Data = Spline; + TaggedOutput.Pin = OutputPinName; + TaggedOutput.Tags.Add(TEXT("Spline")); + } + } +} + + +void +UHoudiniPCGCookable::CreateOutputPinFromBakedData(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const FHoudiniBakedOutput* HoudiniOutput) +{ + TArray Outputs = FHoudiniPCGUtils::GetPCGOutputData(HoudiniOutput); + + CreateOutputPinData(Context, OutputPinName, TagName, Outputs); +} + +void +UHoudiniPCGCookable::CreateOutputPinFromCookedData(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const UHoudiniOutput* HoudiniOutput) +{ + TArray Outputs = FHoudiniPCGUtils::GetPCGOutputData(HoudiniOutput); + + CreateOutputPinData(Context, OutputPinName, TagName, Outputs); +} + +void +UHoudiniPCGCookable::CreateOutputPinData(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const TArray & Outputs) +{ + UPCGParamData* ParamData = FPCGContext::NewObject_AnyThread(Context); + UPCGMetadata* Metadata = ParamData->MutableMetadata(); + + constexpr bool bAllowsInterpolation = false; + constexpr bool bOverrideParent = false; + + const FName PCGOutputIndexName = FName(TEXT("OutputObjectIndex")); + const FName PCGOutputTypeName = FName(TEXT("Type")); + const FName PCGOutputComponentName = FName(TEXT("Component")); + const FName PCGOutputActorName = FName(TEXT("Actor")); + const FName PCGOutputObjectName = FName(TEXT("Object")); + + Metadata->CreateInteger32Attribute(PCGOutputIndexName, 0, bAllowsInterpolation, bOverrideParent); + Metadata->CreateStringAttribute(PCGOutputTypeName, FString(), bAllowsInterpolation, bOverrideParent); + Metadata->CreateSoftObjectPathAttribute(PCGOutputComponentName, FString(), bAllowsInterpolation, bOverrideParent); + Metadata->CreateSoftObjectPathAttribute(PCGOutputActorName, FString(), bAllowsInterpolation, bOverrideParent); + Metadata->CreateSoftObjectPathAttribute(PCGOutputObjectName, FString(), bAllowsInterpolation, bOverrideParent); + + for(int Row = 0; Row < Outputs.Num(); Row++) + { + const PCGMetadataEntryKey EntryKey = ParamData->Metadata->AddEntry(); + const auto& Output = Outputs[Row]; + + FPCGMetadataAttribute* IntAttr = Metadata->GetMutableTypedAttribute(PCGOutputIndexName); + IntAttr->SetValue(Row, Output.OutputObjectIndex); + + FPCGMetadataAttribute* StrAttr = Metadata->GetMutableTypedAttribute(PCGOutputTypeName); + StrAttr->SetValue(Row, Output.OutputType); + + FPCGMetadataAttribute* PathAttr = Metadata->GetMutableTypedAttribute(PCGOutputComponentName); + PathAttr = Metadata->GetMutableTypedAttribute(PCGOutputComponentName); + PathAttr->SetValue(Row, Output.ComponentPath); + + PathAttr = Metadata->GetMutableTypedAttribute(PCGOutputActorName); + PathAttr->SetValue(Row, Output.ActorPath); + + PathAttr = Metadata->GetMutableTypedAttribute(PCGOutputObjectName); + PathAttr->SetValue(Row, Output.ObjectPath); + + } + + TArray& TaggedDataArray = Context->OutputData.TaggedData; + FPCGTaggedData& TaggedOutput = TaggedDataArray.Emplace_GetRef(); + TaggedOutput.Data = ParamData; + TaggedOutput.Pin = OutputPinName; + TaggedOutput.Tags.Add(TagName); +} + +void +UHoudiniPCGCookable::ProcessCookedOutput(FPCGContext* Context) +{ + const UHoudiniPCGSettings* Settings = Context->GetInputSettings(); + + AddTrackedObjects(Context); + + UCookableOutputData* OutputData = this->Cookable->GetOutputData(); + if(!OutputData) + return; + + + if (UHoudiniPDGAssetLink* PDGAssetLink = this->Cookable->GetPDGAssetLink()) + { + int Index = 0; + UTOPNetwork* Network = PDGAssetLink->GetSelectedTOPNetwork(); + for(UTOPNode* Node : Network->AllTOPNodes) + { + for (FTOPWorkResult& WorkResult : Node->WorkResult) + { + for (FTOPWorkResultObject & ResultObject : WorkResult.ResultObjects) + { + TArray>& Outputs = ResultObject.GetResultOutputs(); + for (auto & Ptr : Outputs) + { + FString Tag = FString::Printf(TEXT("Output-%d"), Index++); + ProcessCookedOutput(Context, Settings->GetOutputPinName(), Tag, Ptr); + } + } + } + } + } + else + { + auto& Outputs = OutputData->Outputs; + for(int Index = 0; Index < Outputs.Num(); Index++) + { + FString Tag = FString::Printf(TEXT("Output-%d"), Index); + ProcessCookedOutput(Context, Settings->GetOutputPinName(), Tag, Outputs[Index]); + } + } +} + +void +UHoudiniPCGCookable::ProcessBakedOutput(FPCGContext* Context) +{ + const UHoudiniPCGSettings* Settings = Context->GetInputSettings(); + + if(UHoudiniPDGAssetLink* PDGAssetLink = this->Cookable->GetPDGAssetLink()) + { + auto& Outputs = this->PDGBakedOutput->BakedOutputs; + for(int Index = 0; Index < Outputs.Num(); Index++) + { + FString Tag = FString::Printf(TEXT("Output-%d"), Index); + ProcessBakedOutputs(Context, Settings->GetOutputPinName(), Tag, &Outputs[Index]); + } + } + else + { + UCookableBakingData* BakingData = this->Cookable->GetBakingData(); + if(!BakingData) + return; + + auto& Outputs = BakingData->BakedOutputs; + for(int Index = 0; Index < Outputs.Num(); Index++) + { + FString Tag = FString::Printf(TEXT("Output-%d"), Index); + ProcessBakedOutputs(Context, Settings->GetOutputPinName(), Tag, &Outputs[Index]); + } + } + + AddTrackedObjects(Context); +} + +void +UHoudiniPCGCookable::CopyParametersAndInputs(const UHoudiniPCGCookable * Other) +{ + bParamsChanged |= Cookable->SetParameterData(Other->Cookable->GetParameterData()); + bInputsChanged |= Cookable->SetInputData(Other->Cookable->GetInputData()); + + UHoudiniPDGAssetLink* ThisPDGAssetLink = Cookable->GetPDGAssetLink(); + UHoudiniPDGAssetLink* OtherPDGAssetLink = Other->Cookable->GetPDGAssetLink(); + if (OtherPDGAssetLink) + { + ThisPDGAssetLink->SelectedTOPNetworkIndex = OtherPDGAssetLink->SelectedTOPNetworkIndex; + } + +} + +bool +UHoudiniPCGCookable::UpdateParametersAndInputs(FPCGContext* Context) +{ + Cookable->SetOutputSupported(true); + + const UHoudiniPCGSettings* Settings = nullptr; + if(Context) + Settings = Context->GetInputSettings(); + + Cookable->GetOutputData()->bCreateSceneComponents = Settings ? Settings->bCreateSceneComponents : false; + + if(Context) + { + bParamsChanged |= this->ApplyParametersToCookable(Context); + bInputsChanged |= this->ApplyInputsToCookable(Context); + } + + return true; +} + +bool +UHoudiniPCGCookable::NeedsCook() const +{ + bool bHasBeenCooked = State == EPCGCookableState::CookingComplete; + + return (bInputsChanged || bParamsChanged || !bHasBeenCooked); +} + + +void +UHoudiniPCGCookable::StartCook() +{ + ensure(NeedsCook()); + + bInputsChanged = false; + bParamsChanged = false; + + Errors.Empty(); + + State = EPCGCookableState::Cooking; + + UHoudiniPDGAssetLink* PDGAssetLink = Cookable->GetPDGAssetLink(); + bIsCookingPDG = IsValid(PDGAssetLink); + + if (bIsCookingPDG) + { + HOUDINI_PCG_MESSAGE(TEXT("(%p) Starting to Cook with PDG."), this); + + HOUDINI_LOG_MESSAGE(TEXT("################>>> Cookable %p AssetLink %p"), this, PDGAssetLink); + UWorld* World= this->GetWorld(); + PDGAssetLink->SetOutputWorld(World); + PDGAssetLink->GetSelectedTOPNetwork(); + auto * TOPNetwork = PDGAssetLink->GetSelectedTOPNetwork(); + + TOPNetwork->GetOnPostCookDelegate().AddLambda([this](UTOPNetwork* Link, bool bSuccess) + { + if (State == EPCGCookableState::Cooking) + { + OnCookingCompleteInternal(bSuccess); + } + }); + if(IsValid(TOPNetwork)) + { + FHoudiniPDGManager::DirtyAll(TOPNetwork); + FHoudiniPDGManager::CookOutput(TOPNetwork); + } + } + else + { + // Non-PDG + HOUDINI_PCG_MESSAGE(TEXT("(%p) Starting to Cook."), this); + Cookable->MarkAsNeedCook(); + } +} + +void +UHoudiniPCGCookable::Bake() +{ + const bool bInRemoveHACOutputOnSuccess = false; + + if (Cookable->IsPDGSupported() && Cookable->GetPDGAssetLink()) + { + FHoudiniEngineBakeUtils::BakePDGAssetLink(Cookable->GetPDGAssetLink()); + + this->PDGBakedOutput = NewObject(this); + + auto AssetLink = Cookable->GetPDGAssetLink(); + UTOPNetwork* TopNetwork = AssetLink->GetSelectedTOPNetwork(); + for (auto Node : TopNetwork->AllTOPNodes) + { + for (auto & It : Node->GetBakedWorkResultObjectsOutputs()) + { + FHoudiniPDGWorkResultObjectBakedOutput & Result = It.Value; + this->PDGBakedOutput->BakedOutputs.Append(Result.BakedOutputs); + } + } + } + else + { + FHoudiniBakeSettings BakeSettings; + + FHoudiniEngineBakeUtils::BakeCookable(Cookable, + BakeSettings, + EHoudiniEngineBakeOption::ToActor, + bInRemoveHACOutputOnSuccess); + } +} +void +UHoudiniPCGCookable::Update(FPCGContext* Context) +{ + // This is called every tick during a PCG Cook. It updates internal state based off async operations. + // The user can cancel the PCG task if this takes too long, so there is no additional bailout mechanism. + + switch(this->State) + { + case EPCGCookableState::WaitingForSession: + if(FHoudiniPCGUtils::SessionStatus == EHoudiniPCGSessionStatus::PCGSessionStatus_Created) + { + // A session already existed or was created. Now we can register the cookable with the + // runtime. This will trigger a cook. + this->State = EPCGCookableState::Initializing; + FHoudiniEngineRuntime::Get().RegisterHoudiniCookable(Cookable.Get()); + } + else if(FHoudiniPCGUtils::SessionStatus == EHoudiniPCGSessionStatus::PCGSessionStatus_Error) + { + this->State = EPCGCookableState::Initialized; + } + break; + + case EPCGCookableState::Initializing: + // Still initializing, wait. + break; + + case EPCGCookableState::Initialized: + break; + + case EPCGCookableState::Cooking: + // Still cooking + break; + + case EPCGCookableState::CookingComplete: + // CookingComplete - process results. + HOUDINI_PCG_MESSAGE(TEXT("DONE cooking Managed Resource (%p)"), this); + CookCount = FHoudiniEngineUtils::HapiGetCookCount(Cookable->GetNodeId()); + break; + + default: + // Shouldn't get here. + HOUDINI_LOG_ERROR(TEXT("Unexpected state: default. PCG Cooking failed.")); + break; + } +} + +TArray +UHoudiniPCGCookable::GetUnrealObjectPaths(const FPCGContext* Context, const UPCGMetadata* Metadata) +{ + FHoudiniPCGAttributes Attributes(Metadata, FHoudiniPCGUtils::HDAInputObjectName); + + // Extract all soft object paths from the PCG node inputs. + + int NumRows = Attributes.NumRows; + TArray NewInputPaths; + NewInputPaths.Reserve(NumRows); + for(int Row = 0; Row < NumRows; Row++) + { + TArray DefaultPaths = {}; + TArray Paths = FHoudiniPCGUtils::GetValueAsString(DefaultPaths, Attributes, Row); + for(FString Path : Paths) + { + if(!Path.IsEmpty()) + { + UObject* FoundObject = LoadObject(nullptr, *Path); + if(FoundObject) + { + NewInputPaths.Add(Path); + TrackedObjects.Add(FSoftObjectPath(Path)); + } + else + { + FString ErrorText = FString::Printf(TEXT("Input object '%s' could not be found"), *Path); + FHoudiniPCGUtils::LogVisualError(Context, ErrorText); + return {}; + } + } + } + } + return NewInputPaths; +} + + +bool +UHoudiniPCGCookable::ApplyInputAsUnrealObjects(const FPCGContext* Context, UHoudiniInput* HoudiniInput, const TArray& InputObjects) +{ + TArray NewInputPaths = InputObjects; + NewInputPaths.Sort(); + + // First, get list of the current objects. and compare to last set of objects. if its changed, we need to uploaded. + + TArray CurrentInputObjects; + for(int Index = 0; Index < HoudiniInput->GetNumberOfInputObjects(); Index++) + { + CurrentInputObjects.Add(HoudiniInput->GetInputObjectAt(Index)->GetPathName()); + } + CurrentInputObjects.Sort(); + + // if inputs not changed, do nothing more. + bool bThisInputChanged = (CurrentInputObjects != NewInputPaths); + if(!bThisInputChanged) + return false; + + HoudiniInput->MarkChanged(true); + + TArray WorldObjects; + TArray GeometryObjects; + FString InputName = HoudiniInput->GetInputName(); + + for(int Index = 0; Index < NewInputPaths.Num(); Index++) + { + UObject* InputObject = StaticLoadObject(UObject::StaticClass(), nullptr, *NewInputPaths[Index]); + if(IsValid(InputObject) && InputObject->IsA()) + WorldObjects.Add(InputObject); + else + GeometryObjects.Add(InputObject); + } + + if (HoudiniInput->GetInputType() == EHoudiniInputType::World) + { + HoudiniInput->SetInputObjectsNumber(EHoudiniInputType::World, WorldObjects.Num()); + for(int Index = 0; Index < WorldObjects.Num(); Index++) + { + HoudiniInput->SetInputObjectAt(EHoudiniInputType::World, Index, WorldObjects[Index]); + } + + if (GeometryObjects.Num()) + { + FString ErrorString; + + if(WorldObjects.IsEmpty()) + { + ErrorString = FString::Printf(TEXT("Input %s Type is set to World, but only found Geometry object."), *InputName); + } + else + { + ErrorString = FString::Printf(TEXT("Input %s Type is set to World, but found Geometry Objects too."), *InputName); + } + + if (!ErrorString.IsEmpty()) + FHoudiniPCGUtils::LogVisualError(Context, ErrorString); + + } + } + else if(HoudiniInput->GetInputType() == EHoudiniInputType::Geometry) + { + HoudiniInput->SetInputObjectsNumber(EHoudiniInputType::Geometry, GeometryObjects.Num()); + for(int Index = 0; Index < GeometryObjects.Num(); Index++) + { + HoudiniInput->SetInputObjectAt(EHoudiniInputType::Geometry, Index, GeometryObjects[Index]); + } + + + if(WorldObjects.Num()) + { + FString ErrorString; + + if (GeometryObjects.IsEmpty()) + { + ErrorString = FString::Printf(TEXT("Input %s Type is set to Geometry, but only found World object."), *InputName); + } + else + { + ErrorString = FString::Printf(TEXT("Input %s Type is set to Geometry, but found World Objects too."), *InputName); + } + + if(!ErrorString.IsEmpty()) + FHoudiniPCGUtils::LogVisualError(Context, ErrorString); + } + } + else + { + HOUDINI_LOG_ERROR(TEXT("Did not expect to encounter input type: %s. Ensure input types are set correclty in the PCG Graph."), *HoudiniInput->GetInputTypeAsString()); + } + + return true; +} + +UHoudiniPCGDataObject* +UHoudiniPCGCookable::GetPCGDataObjects(const FPCGTaggedData& TaggedData) +{ + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(TaggedData.Data, TaggedData.Tags); + return PCGDataObject; +} + +bool +UHoudiniPCGCookable::ApplyInputAsPCGData(UHoudiniInput* HoudiniInput, const TArray& NewPCGCollections) +{ + // Set PCG data. TODO: Check CRCs from PCG to see if data is actually changed. + + if(HoudiniInput->GetInputType() != EHoudiniInputType::PCGInput) + { + HOUDINI_LOG_ERROR(TEXT("World output is set to %s when receiving PCG Data"), *HoudiniInput->GetInputTypeAsString()); + return false; + } + + HoudiniInput->SetInputObjectsNumber(EHoudiniInputType::PCGInput, NewPCGCollections.Num()); + + // Set the objects, if changed + for (int Index = 0; Index < NewPCGCollections.Num(); Index++) + { + HoudiniInput->SetInputObjectAt(EHoudiniInputType::PCGInput, 0, NewPCGCollections[Index]); + } + + HoudiniInput->MarkChanged(true); + + return true; +} + +void +UHoudiniPCGCookable::DeleteBakedActor(const FString & ActorPath) +{ + if(ActorPath.IsEmpty()) + return; + + UObject* Actor = StaticLoadObject(UObject::StaticClass(), nullptr, *ActorPath); + ; + if(AActor* SceneActor = Cast(Actor)) + { + SceneActor->GetWorld()->DestroyActor(SceneActor); + } +} + +void +UHoudiniPCGCookable::DeleteBakedComponent(const FString& ComponentPath) +{ + if(ComponentPath.IsEmpty()) + return; + + UObject* Component = StaticLoadObject(UObject::StaticClass(), nullptr, *ComponentPath); + ; + if (USceneComponent* SceneComponent = Cast(Component)) + { + SceneComponent->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + SceneComponent->UnregisterComponent(); + SceneComponent->DestroyComponent(); + } +} + +void +UHoudiniPCGCookable::DeletePackage(UPackage* Package) +{ + if(!Package) + return; + + TArray ObjectsToDelete; + ObjectsToDelete.Add(Package); + GetObjectsWithOuter(Package, ObjectsToDelete, true); + + ObjectTools::DeleteObjectsUnchecked(ObjectsToDelete); +} + + +void +UHoudiniPCGCookable::DeleteBakedObject(const FString& ObjectPath) +{ +#if WITH_EDITOR + if(ObjectPath.IsEmpty()) + return; + + UObject* Object = StaticLoadObject(UObject::StaticClass(), nullptr, *ObjectPath); + if(!IsValid(Object)) + return; + + if(!Object->IsA() && !Object->IsA()) + { + // Only delete selected types of objects that should be assets. + return; + } + + UPackage* Package = Object->GetPackage(); + + if(!Package) + return; + + FString HoudiniName; + + if (FHoudiniEngineBakeUtils::GetHoudiniGeneratedNameFromMetaInformation(Package, Object, HoudiniName)) + { + DeletePackage(Package); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Package %s is marked as a baked output, but is not tagged as generated by Houdini. Not deleting."), + *Package->GetFullName()); + } + +#endif +} + +void +UHoudiniPCGCookable::DeleteLandscapeLayer(const FString& LandscapePath, TArray & LandscapeLayers) +{ + FSoftObjectPath Path(LandscapePath); + ALandscape* Landscape = Cast(Path.ResolveObject()); + + for (auto Layer : LandscapeLayers) + FHoudiniLandscapeRuntimeUtils::DeleteEditLayer(Landscape, FName(Layer)); +} + +void +UHoudiniPCGCookable::DeleteFoliage(UWorld * World, UFoliageType * FoliageType, const TArray & FoliageInstancePositions) +{ + FHoudiniFoliageTools::RemoveFoliageInstances(World, FoliageType, FoliageInstancePositions); +} + +void +UHoudiniPCGCookable::DeleteBakedOutputObject(UWorld* World, FHoudiniBakedOutputObject& BakedOutputObject) +{ + DeleteBakedActor(BakedOutputObject.Actor); + DeleteBakedObject(BakedOutputObject.BakedObject); + DeleteBakedComponent(BakedOutputObject.Actor); + + for (FString & ActorPath : BakedOutputObject.InstancedActors) + { + DeleteBakedActor(ActorPath); + } + + for(FString& ActorPath : BakedOutputObject.LevelInstanceActors) + { + DeleteBakedActor(ActorPath); + } + for(FString& ComponentPath : BakedOutputObject.InstancedComponents) + { + DeleteBakedActor(ComponentPath); + } + + + DeleteLandscapeLayer(BakedOutputObject.Landscape, BakedOutputObject.CreatedLandscapeLayers); + + DeleteBakedActor(BakedOutputObject.Actor); + + DeleteFoliage(World, BakedOutputObject.FoliageType.Get(), BakedOutputObject.FoliageInstancePositions); + + for(FString& FoliageActor : BakedOutputObject.FoliageActors) + { + DeleteBakedActor(FoliageActor); + } + + DeleteBakedObject(BakedOutputObject.BakedSkeleton); + DeleteBakedObject(BakedOutputObject.BakedPhysicsAsset); +} + +void +UHoudiniPCGCookable::DeleteBakedOutput(UWorld* World) +{ + if(!IsValid(this->Cookable)) + return; + + TArray& BakedOutputs = this->Cookable->GetBakedOutputs(); + + for (FHoudiniBakedOutput & BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject & BakedOutputObject = It.Value; + + DeleteBakedOutputObject(World, BakedOutputObject); + } + } + BakedOutputs.Empty(); + + // Delete PDG Output + + if (this->PDGBakedOutput) + { + for (auto & BakedOutput : this->PDGBakedOutput->BakedOutputs) + { + for(auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& BakedOutputObject = It.Value; + + DeleteBakedOutputObject(World, BakedOutputObject); + } + } + + this->PDGBakedOutput->ConditionalBeginDestroy(); + this->PDGBakedOutput = nullptr; + } + +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngine/Private/HoudiniPCGCookable.h`: + +```h +/* +* Copyright (c) <2025> Side Effects Software Inc. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#pragma once + +#include "UObject/ObjectMacros.h" +#include "HoudiniCookable.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniPCGDataObject.h" + +#include "HoudiniPDGAssetLink.h" +#include "HoudiniPCGCookable.generated.h" + +class UHoudiniPCGDataObject; +class UHoudiniPCGDataCollection; +struct FHoudiniPCGObjectOutput; +class UPCGData; +class UPCGMetadata; +struct FPCGContext; +class UHoudiniPCGSettings; +class UHoudiniPCGComponent; +class UHoudiniPCGManagedResource; +class UPCGGraphInterface; + +enum class EPCGCookableState +{ + None, // Create, but nothing happening. + Loaded, // Loaded, but not loaded into Houdini. + WaitingForSession, // Waiting for Houdini Session to be created. + Initializing, // Cookable is being loaded into Houdini for the first time. + Initialized, // Cookable has been loaded into Houdini. Parameters/Inputs can be accessed. + + // Typically Setting parameters and inputs occurs between Initialized and Cooked. + + Cooking, // Cookable is cooking. + CookingComplete // Cookable is done cooking and outputs have been processed. +}; + +UCLASS() +class UHoudiniPDGBakeOutput : public UObject +{ + GENERATED_BODY() + +public: + UPROPERTY() + TArray BakedOutputs; +}; + +UCLASS() +class HOUDINIENGINE_API UHoudiniPCGCookable : public UObject +{ + // This class wraps a single UHoudiniCookable for use in PCG. It contains additional state + // and information to link it to the PCG classes. Its used in two circumstances. + // + // 1. Each UHoudiniPCGSettings contains a FHoudiniPCGCookable which is used + // to obtain parameter, input and output information about the HDA. Its results + // (cooked or baked) are never used, its just used for determining inputs and outputs in the + // PCG Graph editor. + // + // 2. A FHoudiniPCGCookable is created for each PCG node that executes. FHoudiniPCGCookables + // are re-used between executions to improve performance. Additionally one FHoudiniPCGCookable + // may be created for each execution in a loop. + // + + GENERATED_BODY() +public: + UHoudiniPCGCookable(const FObjectInitializer& ObjectInitializer); + virtual ~UHoudiniPCGCookable() override; + + // Instantiates a new HDA... instantiating is asynchronous. + void CreateHoudiniCookable(UHoudiniAsset* Asset, UHoudiniPCGSettings* PCGSettings, UHoudiniPCGComponent* Component); + + // Instantiates a new HDA... instantiating is asynchronous. + void Instantiate(); + + // UpdateParametersAndInputs() pulls the inputs and parameters from the context and cooks the Houdini Cookable. + // Returns true if succeeded, fals if failed. bParamsChanged && bInputsChanged are updated. + bool UpdateParametersAndInputs(FPCGContext* Context); + + void StartCook(); + bool NeedsCook() const; + + void CopyParametersAndInputs(const UHoudiniPCGCookable * Other); + + // DestroyCookable() releases() all data associated with the cook. + void DestroyCookable(UWorld * World); + + // Updates the current cookable state. + void Update(FPCGContext* Context); + + // Bake the Cookable + void Bake(); + + void Rebuild(); + + void PostLoad(); + void PostEditImport(); + + UPROPERTY(EditAnywhere, Category = Settings) + bool bAutomaticallyDeleteAssets = true; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Houdini Cookable") + TObjectPtr Cookable; + + UPROPERTY() + TObjectPtr PDGBakedOutput; + + EPCGCookableState State = EPCGCookableState::None; + bool bIsCookingPDG = false; + + bool bParamsChanged = false; + bool bInputsChanged = false; + + void ProcessCookedOutput(FPCGContext* Context); + void ProcessBakedOutput(FPCGContext* Context); + + void DeleteBakedOutput(UWorld* World); + + void AddCookError(const FString& Error) { Errors.Add(Error); } + const TArray& GetErrors() { return Errors; } + + DECLARE_MULTICAST_DELEGATE_TwoParams(FOnPostOutputProcessingDelegate, UHoudiniPCGCookable*, bool); + + FOnPostOutputProcessingDelegate OnInitializedDelegate; + FOnPostOutputProcessingDelegate OnPostOutputProcessingDelegate; + +private: + + TArray TrackedObjects; + int CookCount = -1; + FDelegateHandle PDGTopNetworkCookedDelegate; + TArray Errors; + + void ProcessCookedOutput(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const UHoudiniOutput* HoudiniOutput); + + void ProcessBakedOutputs(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const FHoudiniBakedOutput* HoudiniOutput); + + static void CreateOutputPinFromCookedData(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const UHoudiniOutput* HoudiniOutput); + static void CreateOutputPinFromBakedData(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const FHoudiniBakedOutput* HoudiniOutput); + static void CreateOutputPinData(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const TArray & Outputs); + static void CopyCookedPCGOutputDataToPinData(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const UHoudiniOutput* HoudiniOutput); + static void CopyBakedPCGOutputDataToPinData(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const FHoudiniBakedOutput* HoudiniOutput); + + static void CopyPCGOutputDataToPinData(FPCGContext* Context, const FName& OutputPinName, const FString& TagName, const UHoudiniPCGOutputData* PCGOutput); + + bool ApplyInputsToCookable(const FPCGContext* InContext); + + bool ApplyParametersToCookable(const FPCGContext* Context); + + bool ApplyParametersToCookable(const UPCGData* Data); + + void OnCookingComplete(bool bSuccess); + + void OnCookingCompleteInternal(bool bSuccess); + + void InvalidateCookable(); + + void AddTrackedObjects(const FPCGContext* Context); + + static bool ApplyInputAsUnrealObjects(const FPCGContext* Context, UHoudiniInput* HoudiniInput, const TArray & InputObjects); + + static UHoudiniPCGDataObject* GetPCGDataObjects(const FPCGTaggedData& TaggedData); + + TArray GetUnrealObjectPaths(const FPCGContext* Context, const UPCGMetadata* Metadata); + + static bool ApplyInputAsPCGData(UHoudiniInput* HoudiniInput, const TArray & PCGCollections); + + static void DeleteBakedActor(const FString& ActorPath); + static void DeleteBakedComponent(const FString& ActorPath); + static void DeleteBakedObject(const FString& ObjectPath); + static void DeletePackage(UPackage* Package); + static void DeleteLandscapeLayer(const FString & LandscapePath, TArray& LandscapeLayers); + static void DeleteFoliage(UWorld* World, UFoliageType* FoliageType, const TArray& FoliageInstancePositions); + void DeleteBakedOutputObject(UWorld* World, FHoudiniBakedOutputObject& BakedOutputObject); +}; + + + + +``` + +`Source/HoudiniEngine/Private/HoudiniPCGManagedResource.cpp`: + +```cpp +/* Copyright (c) <2025> Side Effects Software Inc. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#include "HoudiniPCGManagedResource.h" +#include "HoudiniPCGComponent.h" +#include "PCGGraph.h" + +void UHoudiniPCGManagedResource::PostEditImport() +{ + // In this case, the managed actors won't be copied along the actor/component, + // So we just have to "forget" the actors. + Super::PostEditImport(); +} + +void UHoudiniPCGManagedResource::PostApplyToComponent() +{ + // In this case, we want to preserve the data, so we need to do nothing +} + +void UHoudiniPCGManagedResource::DestroyCookable() +{ + if(IsValid(HoudiniPCGComponent)) + { + if(IsValid(HoudiniPCGComponent->Cookable)) + HoudiniPCGComponent->Cookable->DestroyCookable(HoudiniPCGComponent->GetWorld()); + HoudiniPCGComponent->Cookable = nullptr; + } + +} + +bool UHoudiniPCGManagedResource::Release(bool bHardRelease, TSet>& /*OutActorsToDelete*/) +{ + bIsMarkedUnused = true; + if (bHardRelease) + { + if(IsValid(HoudiniPCGComponent)) + { + DestroyCookable(); + + + AActor* Owner = HoudiniPCGComponent->GetOwner(); + HoudiniPCGComponent->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform); + HoudiniPCGComponent->DestroyComponent(); + GEditor->NoteSelectionChange(); + + HoudiniPCGComponent = nullptr; + } + } + if (PCGComponent) + { + PCGComponent->GetGraph()->OnGraphChangedDelegate.RemoveAll(this); + } + return bHardRelease; +} + +void UHoudiniPCGManagedResource::OnGraphChanged(UPCGGraphInterface* InGraph, EPCGChangeType ChangeType) +{ + // We're really look for "Force Regenerate", but there are no such flags. + const bool bIsStructural = ((ChangeType & (EPCGChangeType::Edge | EPCGChangeType::Structural)) != EPCGChangeType::None); + + if (bIsStructural) + bInvalidateResource = true; +} + +bool UHoudiniPCGManagedResource::ReleaseIfUnused(TSet>& OutActorsToDelete) +{ + if(bIsMarkedUnused) + { + Release(true, OutActorsToDelete); + return true; + } + + return false; +} + +void UHoudiniPCGManagedResource::MarkAsUsed() +{ + Super::MarkAsUsed(); +} + +void UHoudiniPCGManagedResource::MarkAsReused() +{ + Super::MarkAsReused(); +} + +void UHoudiniPCGManagedResource::PostLoad() +{ + Super::PostLoad(); + + // Create a new cookable after deserializing. + bInvalidateResource = true; +} + +bool UHoudiniPCGManagedResource::MoveResourceToNewActor(AActor* NewActor) +{ + return Super::MoveResourceToNewActor(NewActor); +} + +#if WITH_EDITOR +void UHoudiniPCGManagedResource::ChangeTransientState(EPCGEditorDirtyMode NewEditingMode) +{ + Super::ChangeTransientState(NewEditingMode); +} +#endif + + + +``` + +`Source/HoudiniEngine/Private/HoudiniPCGManagedResource.h`: + +```h +/* Copyright (c) <2025> Side Effects Software Inc. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#pragma once +#include "HoudiniOutput.h" +#include "HoudiniPCGCookable.h" +#include "PCGManagedResource.h" +#include "GameFramework/Actor.h" +#include "HoudiniPCGManagedResource.generated.h" + +class UHoudiniPCGComponent; + +UCLASS(BlueprintType) +class HOUDINIENGINE_API UHoudiniPCGManagedResource : public UPCGManagedResource +{ + // This class is derived from a built on PCG class UPCGManagedResource and is used to keep track + // of HDA output data. + + GENERATED_BODY() + +public: + //~Begin UObject interface + virtual void PostEditImport() override; + //~End UObject interface + + //~Begin UPCGManagedResource interface + virtual void PostApplyToComponent() override; + virtual bool Release(bool bHardRelease, TSet>& OutActorsToDelete) override; + virtual bool ReleaseIfUnused(TSet>& OutActorsToDelete) override; + virtual bool MoveResourceToNewActor(AActor* NewActor) override; + virtual void MarkAsUsed() override; + virtual void MarkAsReused() override; + virtual void PostLoad() override; + + void DestroyCookable(); + +#if WITH_EDITOR + virtual void ChangeTransientState(EPCGEditorDirtyMode NewEditingMode) override; +#endif + //~End UPCGManagedResource interface + + void OnGraphChanged(UPCGGraphInterface* InGraph, EPCGChangeType ChangeType); + + UPROPERTY() + TObjectPtr HoudiniPCGComponent; + + UPROPERTY() + TObjectPtr PCGComponent; + + UPROPERTY() + bool bInvalidateResource = false; + +}; + + +``` + +`Source/HoudiniEngine/Private/HoudiniPCGNode.cpp`: + +```cpp +/* +* Copyright (c) <2025> Side Effects Software Inc. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#include "PropertyPathHelpers.h" +#include "Async/Async.h" +#if defined(HOUDINI_USE_PCG) + +#include "HoudiniPCGNode.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineManager.h" +#include "HoudiniPCGUtils.h" +#include "PCGContext.h" +#include "PCGSettings.h" +#include "Async/PCGAsyncLoadingContext.h" +#include "PCGParamData.h" +#include "PCGGraph.h" +#include "HAPI/HAPI_Common.h" +#include "Helpers/PCGDynamicTrackingHelpers.h" +#include "UObject/FastReferenceCollector.h" +#include "PCGContext.h" +#include "PCGComponent.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniInput.h" +#include "HoudiniPCGCookable.h" +#include "HoudiniPCGManagedResource.h" +#include "Editor.h" +#include "IDetailsView.h" +#include "PropertyEditorModule.h" +#include "Modules/ModuleManager.h" + +#define LOCTEXT_NAMESPACE "UHoudiniPCGSettings" + + +void UHoudiniPCGSettings::SetupCookable() +{ + if(IsValid(ParameterCookable)) + { + ParameterCookable->OnPostOutputProcessingDelegate.AddLambda([this](UHoudiniPCGCookable* Cookable, bool bSuccess) + { + OnParameterCookableCooked(); + }); + ParameterCookable->OnInitializedDelegate.AddLambda([this](UHoudiniPCGCookable* Cookable, bool bSuccess) + { + OnParameterCookableInitialized(); + }); + + SetNodeLabelPrefix(); + } +} + +void UHoudiniPCGSettings::OnParameterCookableInitialized() +{ + PopulateInputsAndOutputs(); + ForceRefreshUI(); +} + + +void UHoudiniPCGSettings::PostLoad() +{ + Super::PostLoad(); + SetupCookable(); +} + +void UHoudiniPCGSettings::PostEditImport() +{ + SetupCookable(); +} + +void UHoudiniPCGSettings::BeginDestroy() +{ + Super::BeginDestroy(); +} + +void UHoudiniPCGSettings::GetStaticTrackedKeys(FPCGSelectionKeyToSettingsMap& OutKeysToSettings, TArray>& OutVisitedGraphs) const +{ + if(!HoudiniAsset) + { + // Dynamic tracking or null settings + return; + } + + FPCGSelectionKey Key = FPCGSelectionKey::CreateFromPath(HoudiniAsset.GetPath()); + + OutKeysToSettings.FindOrAdd(Key).Emplace(this, /*bCulling=*/false); +} + +void UHoudiniPCGSettings::ApplyDeprecationBeforeUpdatePins(UPCGNode* InOutNode, TArray>& InputPins, TArray>& OutputPins) +{ + Super::ApplyDeprecationBeforeUpdatePins(InOutNode, InputPins, OutputPins); +} + +FString UHoudiniPCGSettings::GetAdditionalTitleInformation() const +{ + if (!IsValid(ParameterCookable) || !IsValid(ParameterCookable->Cookable)) + { + return TEXT("No HDA set."); + } + + auto State = ParameterCookable->State; + + switch(State) + { + case EPCGCookableState::WaitingForSession: + return TEXT("Establishing Houdini Session..."); + + + case EPCGCookableState::Initialized: + return TEXT("Ready."); + + case EPCGCookableState::Initializing: + return TEXT("Initializing..."); + + case EPCGCookableState::Cooking: + return TEXT("Initializing... please wait..."); + + case EPCGCookableState::None: + case EPCGCookableState::CookingComplete: + case EPCGCookableState::Loaded: + return FString::Printf(TEXT("%s"), HoudiniAsset ? *HoudiniAsset.GetFName().ToString() : TEXT("None")); + + default: + return TEXT("* Error initializing *"); + } +} + +TArray UHoudiniPCGSettings::OutputPinProperties() const +{ + TArray PinProperties; + PinProperties.Emplace(GetOutputPinName(), EPCGDataType::Param | EPCGDataType::Point | EPCGDataType::Spline, false); + return PinProperties; +} + +FName UHoudiniPCGSettings::GetOutputPinName() const +{ + return FName(*FString::Printf(TEXT("Outputs"))); +} + +TArray UHoudiniPCGSettings::InputPinProperties() const +{ + TArray PinProperties; + + { + FString PinName = FHoudiniPCGUtils::ParameterInputPinName; + FPCGPinProperties& InputPinProperty = PinProperties.Emplace_GetRef(FName(PinName), EPCGDataType::Any, /*bAllowMultipleConnections=*/false); + InputPinProperty.SetNormalPin(); + InputPinProperty.bAllowMultipleData = true; + InputPinProperty.SetAllowMultipleConnections(true); + } + + for(int Index = 0; Index < NumInputs; Index++) + { + FString PinName = FHoudiniPCGUtils::GetHDAInputName(Index); + FPCGPinProperties& InputPinProperty = PinProperties.Emplace_GetRef(FName(PinName), EPCGDataType::Any, /*bAllowMultipleConnections=*/false); + InputPinProperty.SetNormalPin(); +#if WITH_EDITORONLY_DATA + if (ParameterCookable && ParameterCookable->Cookable) + { + auto HoudiniInput = ParameterCookable->Cookable->GetInputAt(Index); + InputPinProperty.Tooltip = FText::FromString(HoudiniInput->GetInputLabel()); + } + +#endif + } + + return PinProperties; +} + + +FPCGElementPtr UHoudiniPCGSettings::CreateElement() const +{ + return MakeShared(); +} + +void UHoudiniPCGSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + const FName PropertyName = (PropertyChangedEvent.Property != nullptr) ? PropertyChangedEvent.Property->GetFName() : NAME_None; + if(PropertyName == GET_MEMBER_NAME_CHECKED(UHoudiniPCGSettings, HoudiniAsset)) + { + if(HoudiniAsset == nullptr) + { + if(IsValid(ParameterCookable)) + { + ParameterCookable = nullptr; + } + } + + InstantiateNewParameterCookable(); + + FHoudiniEngineRuntimeUtils::ForceDetailsPanelToUpdate(); + } +} + +void RefreshDetailsForObject(UObject* TargetObject) +{ + FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + + PropertyEditorModule.UpdatePropertyViews({ TargetObject }); +} + +void UHoudiniPCGSettings::OnParameterCookableCooked() +{ + PopulateInputsAndOutputs(); + ForceRefreshUI(); +} + +void UHoudiniPCGSettings::ForceRefreshUI() +{ + // Very brute force way to refresh the details panel. + + this->Modify(); + this->MarkPackageDirty(); + this->IterationCount++; + + FProperty* Property = FindFProperty(this->GetClass(), GET_MEMBER_NAME_CHECKED(UHoudiniPCGSettings, IterationCount)); + if(Property) + { + FPropertyChangedEvent PropertyChangedEvent(Property, EPropertyChangeType::ValueSet); + this->PostEditChangeProperty(PropertyChangedEvent); + } + + FHoudiniEngineRuntimeUtils::ForceDetailsPanelToUpdate(); + +} + +void UHoudiniPCGSettings::InstantiateParameterCookable() +{ + Async(EAsyncExecution::ThreadPool, [this]() + { + ParameterCookable->Instantiate(); + + do + { + // Cook has been cancelled + if(ParameterCookable == nullptr) + break; + + if(ParameterCookable->State == EPCGCookableState::Initialized) + break; + + auto PrevState = ParameterCookable->State; + + ParameterCookable->Update(nullptr); + + if(ParameterCookable->State != PrevState) + { + // For UI update. + this->Modify(); + } + FPlatformProcess::Sleep(0.1f); + + } while(true); + + if(ParameterCookable) + { + // The parameter cookable will not be recooked once its initialized, so set to CookingComplete. + ParameterCookable->State = EPCGCookableState::CookingComplete; + AsyncTask(ENamedThreads::GameThread, [this]() + { + // Populating must be done on game thread. + this->PopulateInputsAndOutputs(); + }); + } + }); +} + +void UHoudiniPCGSettings::InstantiateNewParameterCookable() +{ + if(HoudiniAsset == nullptr) + { + this->Modify(); + + // Do not auto reset inputs and outputs. Its annoying when changing HDA for this to break. + //this->Inputs.SetNum(0); + //this->Outputs.SetNum(0); + + ParameterCookable = nullptr; + this->MarkPackageDirty(); + RefreshDetailsForObject(this); + return; + } + + ParameterCookable = nullptr; + + ParameterCookable = NewObject(this); + + ParameterCookable->CreateHoudiniCookable(HoudiniAsset, nullptr, nullptr); + ParameterCookable->Cookable->SetOutputSupported(false); + ParameterCookable->Cookable->SetPDGSupported(true); + ParameterCookable->Cookable->SetIsPCG(true); + ParameterCookable->Cookable->GetParameterData()->bCookOnParameterChange = true; + + SetupCookable(); + + InstantiateParameterCookable(); + +} + +void UHoudiniPCGSettings::SetNodeLabelPrefix() +{ + if(!IsValid(ParameterCookable)) + return; + + FString NodeLabel; + + // Use Graph name if available (should be!) + if(this->GetOuter()->GetOuter()) + { + NodeLabel += this->GetOuter()->GetOuter()->GetName(); + NodeLabel += TEXT("_"); + } + + // Use node name if available (should be!) + if(this->GetOuter()) + { + NodeLabel += this->GetOuter()->GetName(); + NodeLabel += TEXT("_"); + } + + ParameterCookable->Cookable->SetNodeLabelPrefix(NodeLabel); +} + +void UHoudiniPCGSettings::ResetFromHDA() +{ + this->Modify(); + + if(ParameterCookable) + ParameterCookable->Rebuild(); + + this->MarkPackageDirty(); +} + + + + +void UHoudiniPCGSettings::PopulateInputsAndOutputs() +{ + this->Modify(); + + UCookableInputData* InputData = ParameterCookable->Cookable->GetInputData(); + NumInputs = InputData ? ParameterCookable->Cookable->GetInputData()->Inputs.Num() : 0; + + FProperty* Prop = FindFProperty(GetClass(), GET_MEMBER_NAME_CHECKED(UHoudiniPCGSettings, IterationCount)); + if(Prop) + { + FPropertyChangedEvent PropertyChangedEvent(Prop); + PostEditChangeProperty(PropertyChangedEvent); + } + + this->MarkPackageDirty(); +} + + +FPCGCrc FHoudiniDigitalAssetPCGElement::SetCrc(FPCGContext* Context) const +{ + const UHoudiniPCGSettings* Settings = Context->GetInputSettings(); + + // Calculate the Crc. We include the Stack as this gives us a unique CRC for each loop instance. We do include the inputs CRC + // as this would force a new cookable everytime inputs change, and we don't want that for performance reasons. Instead, we check + // if the inputs to the actual HDA changed below. + + // Update CRC for Context. See comment in FPCGCreateTargetActorElement::ExecuteInternal near code similar to this: + if(!Context->DependenciesCrc.IsValid()) + { + FPCGDataCollection EmptyCollection; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + GetDependenciesCrc(FPCGGetDependenciesCrcParams(&EmptyCollection, Settings, Context->ExecutionSource.Get()), Context->DependenciesCrc); +#else + GetDependenciesCrc(EmptyCollection, Settings, FHoudiniPCGUtils::GetSourceComponent(Context), Context->DependenciesCrc); +#endif + + } + + FPCGCrc ResourceCrc = Context->DependenciesCrc; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FPCGCrc StackCRC = Context->GetStack()->GetCrc(); +#else + FPCGCrc StackCRC = Context->Stack->GetCrc(); +#endif + ResourceCrc.Combine(StackCRC); + return ResourceCrc; +} + +void FHoudiniDigitalAssetPCGElement::AbortInternal(FPCGContext* Context) const +{ + if(!Context) + return; + + FPCGCrc ResourceCrc = SetCrc(Context); + + UPCGComponent* SourceComponent = FHoudiniPCGUtils::GetSourceComponent(Context); + + SourceComponent->ForEachManagedResource([ResourceCrc, &Context](UPCGManagedResource* InResource) + { + if(!InResource->GetCrc().IsValid() + || (InResource->GetCrc() != ResourceCrc && InResource->IsA())) + return; + + UHoudiniPCGManagedResource* ManagedResource = Cast(InResource); + if (ManagedResource) + { + // Mark any managed resource as a "bInvalidateResource". The next time the node tried to execute (if the CRC is the same) + // the flag will be noted and the resource discarded. + ManagedResource->bInvalidateResource = true; + } + + }); +} + +bool FHoudiniDigitalAssetPCGElement::PrepareDataInternal(FPCGContext* Context) const +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniDigitalAssetAittributesElement::PrepareDataInternal); + + if(!Context) + return true; + + FPCHoudiniDigitalAssetAttributesContext* ThisContext = static_cast(Context); + check(ThisContext); + + const UHoudiniPCGSettings* Settings = Context->GetInputSettings(); + if(!Settings || !Settings->HoudiniAsset) + return true; + + if(!ThisContext->WasLoadRequested()) + { + ThisContext->RequestResourceLoad(ThisContext, { Settings->HoudiniAsset.GetPath() }, !Settings->bSynchronousLoad); + } + + return true; +} + +bool FHoudiniDigitalAssetPCGElement::ExecuteInternal(FPCGContext* Context) const +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniDigitalAssetPCGElement::ExecuteInternal); + + if(!Context) + return true; + + FPCHoudiniDigitalAssetAttributesContext* HDAContext = static_cast(Context); + check(HDAContext); + + // This is the main function for processing PCG nodes. It should return true when processing is complete, otherwise + // false, which means it will be called again some time in the future (eg. a frame later). + // + // When called for the first time, this function creates a UHoudiniPCGManagedResource which is used to keep + // track of a UHoudiniPCGComponent - there is one UHoudiniPCGComponent per execution of a PCG Node - note + // that in a PCG loop, this is one per loop. The UHoudiniPCGComponent keeps track of the cookable (and its outputs). + + const UHoudiniPCGSettings* Settings = Context->GetInputSettings(); + + if(!Settings || !Settings->HoudiniAsset.Get()) + { + HOUDINI_PCG_MESSAGE(TEXT("Settings or Settings->HoudiniAsset is null, not cooking.")); + return true; + } + + if (!IsValid(Settings->ParameterCookable)) + { + HOUDINI_PCG_MESSAGE(TEXT("No Parameter Cookable found. Internal error.")); + return true; + } + FPCGCrc ResourceCrc = SetCrc(Context); + + //---------------------------------------------------------------------------------------------------------------------------------------- + // See if we have an existing managed resource. + //---------------------------------------------------------------------------------------------------------------------------------------- + + UHoudiniPCGManagedResource* ManagedResource = nullptr; + + UPCGComponent * PCGComponent = FHoudiniPCGUtils::GetSourceComponent(Context); + + PCGComponent->ForEachManagedResource([&ManagedResource, ResourceCrc, &Context](UPCGManagedResource* InResource) + { + if(!InResource->GetCrc().IsValid() + || (InResource->GetCrc() != ResourceCrc && InResource->IsA())) + return; + + ManagedResource = Cast(InResource); + }); + + switch(HDAContext->ContextState) + { + case EHoudiniPCGContextState::None: + { + HOUDINI_PCG_MESSAGE(TEXT("First time called with context %p"), HDAContext); + + // For now, we must always force the Cookable to be created since the user may have edited the Parameter Cookable + // without saving the HDA, meaning the HDAs in session sync get out of sync. + ManagedResource = nullptr; + + // If the Managed Resource is invalid, don't use it. + + if(ManagedResource) + { + if(!IsValid(ManagedResource->HoudiniPCGComponent) || + !IsValid(ManagedResource->HoudiniPCGComponent->Cookable) || + ManagedResource->bInvalidateResource) + { + if (ManagedResource->HoudiniPCGComponent) + { + HOUDINI_PCG_MESSAGE(TEXT("(%p) Invalid Managed Resource Found, ignoring."), ManagedResource->HoudiniPCGComponent->Cookable.Get()); + ManagedResource->DestroyCookable(); + } + ManagedResource = nullptr; + } + } + + //---------------------------------------------------------------------------------------------------------------------------------------- + // If we didn't find a managed resource (or we ignored the old one), we need create a new one and start a cook. + //---------------------------------------------------------------------------------------------------------------------------------------- + + if(!ManagedResource) + { + // No previous resource found, so create a new one and instantiate the HDA. Note that next time Execute is called, this ManagedResource + // will be found. + // NOTE: We instantiate, then once the HDA is ready in Houdini, we set parameters and cooked. This seems to be necessary to avoid + // paramters getting overridden on the first cook. Possibly a slight rework of Houdini Engine Manager could fix this. + + UPCGComponent* SourceComponent = FHoudiniPCGUtils::GetSourceComponent(Context); + + ManagedResource = NewObject(SourceComponent); + ManagedResource->PCGComponent = SourceComponent; + if(ManagedResource->PCGComponent) + { + ManagedResource->PCGComponent->GetGraph()->OnGraphChangedDelegate.AddUObject(ManagedResource, &UHoudiniPCGManagedResource::OnGraphChanged); + } + ManagedResource->SetCrc(ResourceCrc); + ManagedResource->MarkAsUsed(); + ManagedResource->HoudiniPCGComponent = UHoudiniPCGComponent::CreatePCGComponent(SourceComponent); + SourceComponent->AddToManagedResources(ManagedResource); + + UHoudiniPCGCookable * PCGCookable = NewObject(ManagedResource->HoudiniPCGComponent); + PCGCookable->CreateHoudiniCookable(Settings->HoudiniAsset, nullptr, ManagedResource->HoudiniPCGComponent); + PCGCookable->Cookable->SetIsPCG(true); + PCGCookable->Cookable->SetLandscapeModificationEnabled(ManagedResource->PCGComponent->bIgnoreLandscapeTracking); + PCGCookable->Cookable->SetNodeLabelPrefix(TEXT("PCG_Instance_")); + PCGCookable->Instantiate(); + PCGCookable->bAutomaticallyDeleteAssets = Settings->bAutomaticallyDeleteTempAssets; + ManagedResource->HoudiniPCGComponent->Cookable = PCGCookable; + HOUDINI_PCG_MESSAGE(TEXT("(%p) Creating Managed Resource, Instantiating..."), PCGCookable); + + // Return now since instantiation is not instant. + HDAContext->ContextState = EHoudiniPCGContextState::Instantiating; + return false; + } + else + { + // We have a managed resource... update the cookable, and if that triggered a cook, we're done. If not, we can just re-use the last cook. + // Attempt to apply parameters, inputs. If a cook was started, return - we need to wait for it to complete asynchronouosly. + + ManagedResource->HoudiniPCGComponent->Cookable->CopyParametersAndInputs(Settings->ParameterCookable); + bool bSuccess = ManagedResource->HoudiniPCGComponent->Cookable->UpdateParametersAndInputs(Context); + if(!bSuccess) + { + HOUDINI_PCG_MESSAGE(TEXT("An error occured, not processing PCG node.")); + return true; + } + + ManagedResource->MarkAsReused(); + + if(ManagedResource->HoudiniPCGComponent->Cookable->NeedsCook()) + { + // Remove previous baked output before cooking. (Cooked output is already cleaned up). + UPCGComponent* SourceComponent = FHoudiniPCGUtils::GetSourceComponent(Context); + ManagedResource->HoudiniPCGComponent->Cookable->DeleteBakedOutput(SourceComponent->GetWorld()); + + // Something changed, so we must cook. + ManagedResource->HoudiniPCGComponent->Cookable->StartCook(); + HOUDINI_PCG_MESSAGE(TEXT("A cook was started.")); + HDAContext->ContextState = EHoudiniPCGContextState::Cooking; + return false; + } + else + { + // Nothing changed so we can re-use output as-is. + HOUDINI_PCG_MESSAGE(TEXT("Nothing Changed: returning Managed Resource.")); + HDAContext->ContextState = EHoudiniPCGContextState::Done; + return true; + } + } + } + case EHoudiniPCGContextState::Instantiating: + { + UHoudiniPCGCookable* Cookable = ManagedResource->HoudiniPCGComponent->Cookable.Get(); + Cookable->Update(HDAContext); + + if(Cookable->State == EPCGCookableState::Initialized) + { + Cookable->Cookable->SetOutputSupported(true); + Cookable->CopyParametersAndInputs(Settings->ParameterCookable); + Cookable->UpdateParametersAndInputs(Context); + + if(!Cookable->GetErrors().IsEmpty()) + { + HDAContext->ContextState = EHoudiniPCGContextState::Done; + FHoudiniPCGUtils::LogVisualError(Context, Cookable->GetErrors()); + return true; + } + + if (Cookable->NeedsCook()) + { + HDAContext->ContextState = EHoudiniPCGContextState::Cooking; + Cookable->StartCook(); + return false; + } + else + { + HDAContext->ContextState = EHoudiniPCGContextState::Done; + return true; + } + } + return false; + } + break; + case EHoudiniPCGContextState::Cooking: + { + // Wait for cooking to complete. + if(!IsValid(ManagedResource->HoudiniPCGComponent)) + { + // User delete component mid-cook? + HOUDINI_PCG_MESSAGE(TEXT("Houdini PCG Component lost...")); + return true; + } + UHoudiniPCGCookable* Cookable = ManagedResource->HoudiniPCGComponent->Cookable.Get(); + Cookable->Update(Context); + + if (Cookable->State == EPCGCookableState::CookingComplete) + { + if(!Cookable->GetErrors().IsEmpty()) + { + HDAContext->ContextState = EHoudiniPCGContextState::Done; + FHoudiniPCGUtils::LogVisualError(Context, Cookable->GetErrors()); + return true; + } + + if (Settings->OutputType == EHoudiniPCGOutputType::Cook) + { + Cookable->ProcessCookedOutput(Context); + } + else + { + Cookable->Bake(); + Cookable->ProcessBakedOutput(Context); + } + return true; + } + else + { + return false; + } + } + break; + default: + break; + } + return true; +} + +bool +FHoudiniDigitalAssetPCGElement::IsCacheable(const UPCGSettings* InSettings) const +{ + const UHoudiniPCGSettings* Settings = Cast(InSettings); + return Settings->bUsePCGCache; +} + +#undef LOCTEXT_NAMESPACE +#endif +``` + +`Source/HoudiniEngine/Private/HoudiniPCGNode.h`: + +```h +/* +* Copyright (c) <2025> Side Effects Software Inc. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#pragma once +#include "HoudiniAsset.h" + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wshadow" +#endif + +#include "PCGContext.h" + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#include "PCGSettings.h" +#include "Async/PCGAsyncLoadingContext.h" +#include "HoudiniCookable.h" +#include "HoudiniPCGCookable.h" +#include "HoudiniPCGNode.generated.h" + +UENUM() +enum class EHoudiniPCGOutputType : uint8 +{ + Cook = 1, + CookAndBake, +}; + +UCLASS(BlueprintType, ClassGroup = (Procedural)) +class HOUDINIENGINE_API UHoudiniPCGSettings : public UPCGSettings +{ + GENERATED_BODY() + +public: + virtual void PostLoad() override; + + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + + void PopulateInputsAndOutputs(); + + virtual bool CanCullTaskIfUnwired() const { return false; } + virtual void BeginDestroy() override; + +#if WITH_EDITOR + //~Begin UPCGSettings interface + virtual FName GetDefaultNodeName() const override { return FName(TEXT("HoudiniDigitalAsset")); } + virtual FText GetDefaultNodeTitle() const override { return NSLOCTEXT("HoudiniDigitalAsset", "NodeTitle", "Houdini Digital Asset"); } + virtual EPCGSettingsType GetType() const override { return EPCGSettingsType::InputOutput; } + virtual void GetStaticTrackedKeys(FPCGSelectionKeyToSettingsMap& OutKeysToSettings, TArray>& OutVisitedGraphs) const override; + virtual bool CanDynamicallyTrackKeys() const override { return true; } + virtual void ApplyDeprecationBeforeUpdatePins(UPCGNode* InOutNode, TArray>& InputPins, TArray>& OutputPins) override; + virtual bool HasDynamicPins() const { return true; } +#endif + + virtual FString GetAdditionalTitleInformation() const override; + + virtual TArray OutputPinProperties() const override; + virtual TArray InputPinProperties() const override; + + void ResetFromHDA(); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = HoudiniPCG) + TObjectPtr HoudiniAsset; + + /** By default, data table loading is asynchronous, can force it synchronous if needed. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings|Debug") + bool bSynchronousLoad = false; + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Properties + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + // Keep track of number of inputs... don't just used the cookable value so we don't break connections when the number + // of inputs changes. + UPROPERTY() + int NumInputs = 0; + + UPROPERTY(EditAnywhere, Category = HoudiniPCG) + EHoudiniPCGOutputType OutputType = EHoudiniPCGOutputType::CookAndBake; + UPROPERTY(EditAnywhere, Category = HoudiniPCG) + bool bCreateSceneComponents = true; + + UPROPERTY(EditAnywhere, Category = HoudiniPCG) + bool bAutomaticallyDeleteTempAssets = true; + + UPROPERTY(EditAnywhere, Category = HoudiniPCG) + bool bUsePCGCache = true; + + UPROPERTY() + int64 IterationCount = 0; // dummy value to keep track of changes. + + FName GetOutputPinName() const; + + UPROPERTY(Instanced) + TObjectPtr ParameterCookable; + +protected: +#if WITH_EDITOR + virtual EPCGChangeType GetChangeTypeForProperty(const FName& InPropertyName) const override { return Super::GetChangeTypeForProperty(InPropertyName) | EPCGChangeType::Cosmetic; } +#endif + virtual FPCGElementPtr CreateElement() const override; + // ~End UPCGSettings interface + + void InstantiateParameterCookable(); + void InstantiateNewParameterCookable(); + void OnParameterCookableCooked(); + void OnParameterCookableInitialized(); + + void PostEditImport() override; + void SetupCookable(); + void ForceRefreshUI(); + void SetNodeLabelPrefix(); +}; + +enum class EHoudiniPCGContextState +{ + None, + Instantiating, + Cooking, + Done +}; +struct FPCHoudiniDigitalAssetAttributesContext : public FPCGContext, public IPCGAsyncLoadingContext +{ +public: + EHoudiniPCGContextState ContextState = EHoudiniPCGContextState::None; + +}; + +class FHoudiniDigitalAssetPCGElement : public IPCGElementWithCustomContext +{ +public: + virtual bool IsCacheable(const UPCGSettings* InSettings) const override; + virtual bool CanExecuteOnlyOnMainThread(FPCGContext* InContext) const override { return true; } +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + virtual bool SupportsBasePointDataInputs(FPCGContext* InContext) const override { return true; } +#endif + FPCGCrc SetCrc(FPCGContext* Context) const; + +protected: + virtual bool PrepareDataInternal(FPCGContext* InContext) const override; + virtual bool ExecuteInternal(FPCGContext* InContext) const override; + virtual void AbortInternal(FPCGContext* Context) const; +}; + + +``` + +`Source/HoudiniEngine/Private/HoudiniPCGTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPCGTranslator.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniPCGUtils.h" +#include "Metadata/PCGMetadataAttributeTpl.h" +#include "PCGContext.h" +#include "PCGSettings.h" +#include "Metadata/PCGMetadata.h" +#include "PCGParamData.h" + +bool HasAttribute(HAPI_NodeId NodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner, const FString & AttrName) +{ + TArray Attributes = FHoudiniEngineUtils::GetAttributeNames(FHoudiniEngine::Get().GetSession(), NodeId, PartId, Owner); + for (FString Attr : Attributes) + { + if(Attr == AttrName) + return true; + } + return false; +} + + +bool FHoudiniPCGTranslator::IsPCGOutput(HAPI_NodeId NodeId, HAPI_PartId PartId) +{ + if(HasAttribute(NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_POINT, HOUDINI_PCG_PARAMS_OUTPUT_NAME)) + return true; + if(HasAttribute(NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_VERTEX, HOUDINI_PCG_PARAMS_OUTPUT_NAME)) + return true; + if(HasAttribute(NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_PRIM, HOUDINI_PCG_PARAMS_OUTPUT_NAME)) + return true; + if(HasAttribute(NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_DETAIL, HOUDINI_PCG_PARAMS_OUTPUT_NAME)) + return true; + return false; +} + +UHoudiniPCGOutputData* FHoudiniPCGTranslator::CreatePCGSplinesOutput(UHoudiniOutput* CurOutput) +{ + const auto& HGPO = CurOutput->GetHoudiniGeoPartObjects()[0]; + + UHoudiniPCGOutputData* Results = NewObject(CurOutput); + + HAPI_CurveInfo CurveInfo; + FHoudiniApi::CurveInfo_Init(&CurveInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetCurveInfo(FHoudiniEngine::Get().GetSession(), HGPO.GeoId, + HGPO.PartId, &CurveInfo), nullptr); + + TArray CurveCounts; + CurveCounts.SetNum(CurveInfo.curveCount); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetCurveCounts(FHoudiniEngine::Get().GetSession(), + HGPO.AssetId, HGPO.PartId, CurveCounts.GetData(), 0, CurveCounts.Num()), nullptr); + + TArray FloatPositions; + FHoudiniHapiAccessor PositionAccessor(HGPO.AssetId, HGPO.PartId, HAPI_ATTRIB_POSITION); + PositionAccessor.GetAttributeData(HAPI_AttributeOwner::HAPI_ATTROWNER_POINT, FloatPositions); + + TArray Attributes = FHoudiniEngineUtils::GetAttributeNames( + FHoudiniEngine::Get().GetSession(), + HGPO.GeoId, HGPO.PartId, + HAPI_ATTROWNER_POINT); + + TArray MetaAttributes; + for(FString Attr : Attributes) + { + bool bIgnore = false; + bIgnore |= Attr == TEXT("P"); + bIgnore |= Attr == TEXT("__vertex_id"); + + if(!bIgnore) + { + MetaAttributes.Add(Attr); + } + } + + + int CurveStart = 0; + + for (int CurveIndex = 0; CurveIndex < CurveCounts.Num(); CurveIndex++) + { + TArray SplinePoints; + SplinePoints.SetNum(CurveCounts[CurveIndex]); + + UPCGSplineData* ParamData = NewObject(); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + TArray EntryKeys; + EntryKeys.SetNum(SplinePoints.Num()); +#endif + for (int PosIndex = 0; PosIndex < SplinePoints.Num(); PosIndex++) + { + int HapiOffset = (CurveStart + PosIndex) * 3; + FVector Position; + Position.X = FloatPositions[HapiOffset + 0] * 100.0; + Position.Y = FloatPositions[HapiOffset + 2] * 100.0; + Position.Z = FloatPositions[HapiOffset + 1] * 100.0; + SplinePoints[PosIndex].Position = Position; + SplinePoints[PosIndex].InputKey = static_cast(PosIndex); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + EntryKeys[PosIndex] = PosIndex; +#endif + } + + // Set attributes. Only works on Unreal 5.6. +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + UPCGMetadata* MetaData = ParamData->Metadata; + CreatePCGMetaAttributes(MetaData, MetaAttributes, EntryKeys, HGPO.GeoId, HGPO.PartId, HAPI_ATTROWNER_POINT, CurveStart, SplinePoints.Num()); + + ParamData->Initialize(SplinePoints, CurveInfo.isClosed, FTransform::Identity, EntryKeys); +#else + ParamData->Initialize(SplinePoints, CurveInfo.isClosed, FTransform::Identity); +#endif + CurveStart += CurveCounts[CurveIndex]; + + Results->SplineParams.Add(ParamData); + } + + return Results; +} + +UHoudiniPCGOutputData* FHoudiniPCGTranslator::CreatePCGParamsOutput(UHoudiniOutput* CurOutput) +{ + const auto& HGPO = CurOutput->GetHoudiniGeoPartObjects()[0]; + + UHoudiniPCGOutputData* Results = NewObject(CurOutput); + + Results->DetailsParams = CreatePCGAttributes(HGPO.GeoId, HGPO.PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_DETAIL); + Results->PrimsParams = CreatePCGAttributes(HGPO.GeoId, HGPO.PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_PRIM); + Results->VertexParams = CreatePCGAttributes(HGPO.GeoId, HGPO.PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_VERTEX); + Results->PointParams = CreatePCGPointData(HGPO.GeoId, HGPO.PartId); + return Results; +} + +void FHoudiniPCGTranslator::CreatePCGFromOutput(UHoudiniOutput* Output) +{ + if(Output->GetHoudiniGeoPartObjects().IsEmpty()) + return; + + const auto& HGPO = Output->GetHoudiniGeoPartObjects()[0]; + + UHoudiniPCGOutputData* PCGOutput = nullptr; + + switch (HGPO.PartInfo.Type) + { + case EHoudiniPartType::Curve: + PCGOutput = CreatePCGSplinesOutput(Output); + break; + + default: + PCGOutput = CreatePCGParamsOutput(Output); + break; + } + + if (PCGOutput) + { + FHoudiniOutputObjectIdentifier OutputIdentifier; + OutputIdentifier.ObjectId = HGPO.ObjectId; + OutputIdentifier.GeoId = HGPO.GeoId; + OutputIdentifier.PartId = HGPO.PartId; + OutputIdentifier.PartName = HGPO.PartName; + FHoudiniOutputObject& NewOutputObject = Output->GetOutputObjects().FindOrAdd(OutputIdentifier); + NewOutputObject.OutputObject = PCGOutput; + } +} + +UPCGPointData* FHoudiniPCGTranslator::CreatePCGPointData(HAPI_NodeId NodeId, HAPI_PartId PartId ) +{ + UPCGPointData* PointData = NewObject(); + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), NodeId, PartId, &PartInfo); + + TArray Attributes = FHoudiniEngineUtils::GetAttributeNames(FHoudiniEngine::Get().GetSession(), NodeId, PartId, HAPI_ATTROWNER_POINT); + + TArray MetaAttributes; + + TArray Points; + Points.SetNum(PartInfo.pointCount); + + // + // Process all Houdini attributes that can be directly converted to point data. + // + + for(int AttrIndex = 0; AttrIndex < Attributes.Num(); AttrIndex++) + { + FString& Attribute = Attributes[AttrIndex]; + HAPI_AttributeInfo AttrInfo; + FHoudiniApi::AttributeInfo_Init(&AttrInfo); + FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), NodeId, PartId, H_TCHAR_TO_UTF8(*Attribute), HAPI_ATTROWNER_POINT, &AttrInfo); + FHoudiniHapiAccessor Accessor(NodeId, PartId, H_TCHAR_TO_UTF8(*Attribute)); + + if (Attribute.Equals(TEXT("P"), ESearchCase::Type::IgnoreCase)) + { + TArray Values; + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, Values); + for(int Index = 0; Index < Points.Num(); Index++) + { + FVector Position = FHoudiniPCGUtils::HoudiniToUnrealPosition(&Values[Index * 3]); + Points[Index].Transform.SetLocation(Position); + + } + } + else if(Attribute.Equals(TEXT("orient"), ESearchCase::Type::IgnoreCase)) + { + TArray Values; + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, Values); + for(int Index = 0; Index < Points.Num(); Index++) + { + FQuat Quat = FHoudiniPCGUtils::HoudiniToUnrealQuat(&Values[Index * 4]); + Points[Index].Transform.SetRotation(Quat); + + } + } + else if(Attribute.Equals(TEXT("scale"), ESearchCase::Type::IgnoreCase) && AttrInfo.tupleSize == 3) + { + TArray Values; + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, Values); + for(int Index = 0; Index < Points.Num(); Index++) + { + FVector3d Scale = FHoudiniPCGUtils::HoudiniToUnrealVector(&Values[Index * 3]); + Points[Index].Transform.SetScale3D(Scale); + } + } + else if(Attribute.Equals(TEXT("BoundsMin"), ESearchCase::Type::IgnoreCase) && AttrInfo.tupleSize == 3) + { + TArray Values; + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, Values); + for(int Index = 0; Index < Points.Num(); Index++) + { + FVector3d BoundsMin = FHoudiniPCGUtils::HoudiniToUnrealVector(&Values[Index * 3]); + Points[Index].BoundsMin = BoundsMin; + } + } + else if(Attribute.Equals(TEXT("BoundsMax"), ESearchCase::Type::IgnoreCase) && AttrInfo.tupleSize == 3) + { + TArray Values; + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, Values); + for(int Index = 0; Index < Points.Num(); Index++) + { + FVector3d BoundsMax = FHoudiniPCGUtils::HoudiniToUnrealVector(&Values[Index * 3]); + Points[Index].BoundsMax = BoundsMax; + } + } + else if(Attribute.Equals(TEXT("Cd"), ESearchCase::Type::IgnoreCase) && AttrInfo.tupleSize == 3) + { + TArray Values; + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, Values); + for(int Index = 0; Index < Points.Num(); Index++) + { + Points[Index].Color = FVector4(Values[Index * 3 + 0], Values[Index * 3 + 1], Values[Index * 3 + 2], 1.0f); + } + } + else if(Attribute.Equals(TEXT("Cd"), ESearchCase::Type::IgnoreCase) && AttrInfo.tupleSize == 4) + { + TArray Values; + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, Values); + for(int Index = 0; Index < Points.Num(); Index++) + { + Points[Index].Color = FVector4(Values[Index * 4 + 0], Values[Index * 4 + 1], Values[Index * 4 + 2], Values[Index * 4 + 3]); + } + } + else if(Attribute.Equals(TEXT("Steepness"), ESearchCase::Type::IgnoreCase) && AttrInfo.tupleSize == 1) + { + TArray Values; + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, Values); + for(int Index = 0; Index < Points.Num(); Index++) + { + Points[Index].Steepness = Values[Index]; + } + } + else if(Attribute.Equals(TEXT("Seed"), ESearchCase::Type::IgnoreCase) && AttrInfo.tupleSize == 1) + { + TArray Values; + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, Values); + for(int Index = 0; Index < Points.Num(); Index++) + { + Points[Index].Seed = Values[Index]; + } + } + else if(Attribute.Equals(TEXT("Density"), ESearchCase::Type::IgnoreCase) && AttrInfo.tupleSize == 1) + { + TArray Values; + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, Values); + for(int Index = 0; Index < Points.Num(); Index++) + { + Points[Index].Density = Values[Index]; + } + } + else + { + MetaAttributes.Add(Attribute); + } + } + + // Any unprocessed attributes can now be added as meta data. + + UPCGMetadata* MetaData = PointData->MutableMetadata(); + + TArray EntryKeys; + EntryKeys.SetNum(Points.Num()); + + for(int Index = 0; Index < EntryKeys.Num(); Index++) + { + EntryKeys[Index] = MetaData->AddEntry(); + Points[Index].MetadataEntry = EntryKeys[Index]; + } + + CreatePCGMetaAttributes(MetaData, MetaAttributes, EntryKeys, NodeId, PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_POINT); + + PointData->SetPoints(Points); + + return PointData; + +} + +UPCGParamData* FHoudiniPCGTranslator::CreatePCGAttributes(HAPI_NodeId NodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + TArray Attributes = FHoudiniEngineUtils::GetAttributeNames(FHoudiniEngine::Get().GetSession(), NodeId, PartId, Owner); + + UPCGParamData* ParamData = NewObject(); + UPCGMetadata* MetaData = ParamData->MutableMetadata(); + + TArray EntryKeys; + + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), NodeId, PartId, &PartInfo); + + switch(Owner) + { + case HAPI_ATTROWNER_POINT: + EntryKeys.SetNum(PartInfo.pointCount); + break; + case HAPI_ATTROWNER_VERTEX: + EntryKeys.SetNum(PartInfo.vertexCount); + break; + case HAPI_ATTROWNER_PRIM: + EntryKeys.SetNum(PartInfo.faceCount); + break; + case HAPI_ATTROWNER_DETAIL: + EntryKeys.SetNum(1); + break; + default: + break; + } + + for(int Index = 0; Index < EntryKeys.Num(); Index++) + EntryKeys[Index] = MetaData->AddEntry(); + + CreatePCGMetaAttributes(MetaData, Attributes, EntryKeys, NodeId, PartId, Owner); + return ParamData; +} + +void FHoudiniPCGTranslator::CreatePCGMetaAttributes( + UPCGMetadata* MetaData, + TArray & Attributes, + const TArray& EntryKeys, + HAPI_NodeId NodeId, + HAPI_PartId PartId, + HAPI_AttributeOwner Owner, + int StartIndex, + int IndexCount) +{ + + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), NodeId, PartId, &PartInfo); + + for (int AttrIndex = 0; AttrIndex < Attributes.Num(); AttrIndex++) + { + HAPI_AttributeInfo AttrInfo; + FHoudiniApi::AttributeInfo_Init(&AttrInfo); + FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), NodeId, PartId, H_TCHAR_TO_UTF8(*Attributes[AttrIndex]), Owner, &AttrInfo); + + if (Attributes[AttrIndex] == TEXT("__vertex_id") && PartInfo.vertexCount > 0) + { + TArray VertexIds; + VertexIds.SetNumZeroed(PartInfo.vertexCount); + FHoudiniApi::GetVertexList(FHoudiniEngine::Get().GetSession(), NodeId, PartId, VertexIds.GetData(), 0, VertexIds.Num()); + + MetaData->CreateInteger32Attribute(*Attributes[AttrIndex], 0, false, false); + FPCGMetadataAttribute* MetaAttr = MetaData->GetMutableTypedAttribute(*Attributes[AttrIndex]); + check(VertexIds.Num() == MetaData->GetItemCountForChild()); + MetaAttr->SetValues(EntryKeys, VertexIds); + continue; + } + + switch (AttrInfo.storage) + { + case HAPI_STORAGETYPE_UINT8: + case HAPI_STORAGETYPE_INT8: + case HAPI_STORAGETYPE_INT16: + case HAPI_STORAGETYPE_INT: + CreatePCGInt32Attribute(MetaData, EntryKeys, NodeId, PartId, Owner, FName(Attributes[AttrIndex]), StartIndex, IndexCount); + break; + case HAPI_STORAGETYPE_INT64: + CreatePCGInt64Attribute(MetaData, EntryKeys, NodeId, PartId, Owner, FName(Attributes[AttrIndex]), StartIndex, IndexCount); + break; + case HAPI_STORAGETYPE_FLOAT: + CreatePCGFloatAttribute(MetaData, EntryKeys, NodeId, PartId, Owner, FName(Attributes[AttrIndex]), StartIndex, IndexCount); + break; + case HAPI_STORAGETYPE_FLOAT64: + CreatePCGDoubleAttribute(MetaData, EntryKeys, NodeId, PartId, Owner, FName(Attributes[AttrIndex]), StartIndex, IndexCount); + break; + case HAPI_STORAGETYPE_STRING: + CreatePCGStringAttribute(MetaData, EntryKeys, NodeId, PartId, Owner, FName(Attributes[AttrIndex]), StartIndex, IndexCount); + break; + default: + break; + } + } +} + +template +TArray HoudiniPCGGetSelectedTuple(const TArray & Values, int TupleIndex, int TupleSize) +{ + int ActualCount = Values.Num() / TupleSize; + ensure(Values.Num() == (ActualCount * TupleSize)); + + TArray Results; + Results.SetNum(ActualCount); + + for (int Index = 0; Index < ActualCount; Index++) + { + Results[Index] = Values[Index * TupleSize + TupleIndex]; + } + return Results; +} + + +void +FHoudiniPCGTranslator::CreatePCGInt32Attribute(UPCGMetadata* Metadata, + const TArray& EntryKeys, + HAPI_NodeId NodeId, + HAPI_PartId PartId, + HAPI_AttributeOwner Owner, + FName AttrName, + int StartIndex, + int Count) +{ + TArray Values; + FHoudiniHapiAccessor Accessor(NodeId, PartId, H_TCHAR_TO_UTF8(*AttrName.ToString())); + Accessor.GetAttributeData(Owner, Values, StartIndex, Count); + if(Values.IsEmpty()) + return; + + HAPI_AttributeInfo AttributeInfo; + Accessor.GetInfo(AttributeInfo, Owner); + Values = HoudiniPCGGetSelectedTuple(Values, 0, AttributeInfo.tupleSize); + + Metadata->CreateInteger32Attribute(AttrName, 0, false, false); + FPCGMetadataAttribute* MetaAttr = Metadata->GetMutableTypedAttribute(AttrName); + + MetaAttr->SetValues(EntryKeys, Values); +} + +void FHoudiniPCGTranslator::CreatePCGInt64Attribute(UPCGMetadata* Metadata, + const TArray& EntryKeys, + HAPI_NodeId NodeId, + HAPI_PartId PartId, + HAPI_AttributeOwner Owner, + FName AttrName, + int StartIndex, + int IndexCount) +{ + TArray Values; + FHoudiniHapiAccessor Accessor(NodeId, PartId, H_TCHAR_TO_UTF8(*AttrName.ToString())); + Accessor.GetAttributeData(Owner, Values, StartIndex, IndexCount); + if(Values.IsEmpty()) + return; + + HAPI_AttributeInfo AttributeInfo; + Accessor.GetInfo(AttributeInfo, Owner); + Values = HoudiniPCGGetSelectedTuple(Values, 0, AttributeInfo.tupleSize); + + Metadata->CreateInteger64Attribute(AttrName, 0, false, false); + FPCGMetadataAttribute* MetaAttr = Metadata->GetMutableTypedAttribute(AttrName); + + MetaAttr->SetValues(EntryKeys, Values); +} + +void FHoudiniPCGTranslator::CreatePCGFloatAttribute(UPCGMetadata* Metadata, + const TArray& EntryKeys, + HAPI_NodeId NodeId, + HAPI_PartId PartId, + HAPI_AttributeOwner Owner, + FName AttrName, + int StartIndex, + int IndexCount) +{ + TArray Values; + FHoudiniHapiAccessor Accessor(NodeId, PartId, H_TCHAR_TO_UTF8(*AttrName.ToString())); + Accessor.GetAttributeData(Owner, Values, StartIndex, IndexCount); + if(Values.IsEmpty()) + return; + + int TupleSize = Values.Num() / EntryKeys.Num(); + + switch (TupleSize) + { + case 1: + { + Metadata->CreateFloatAttribute(AttrName, 0, false, false); + FPCGMetadataAttribute* MetaAttr = Metadata->GetMutableTypedAttribute(AttrName); + MetaAttr->SetValues(EntryKeys, Values); + } + break; + case 2: + { + TArray ConvertedValues; + ConvertedValues.SetNum(Values.Num() / TupleSize); + for (int Index = 0; Index < ConvertedValues.Num(); Index++) + { + ConvertedValues[Index][0] = Values[Index * 3 + 0]; + ConvertedValues[Index][1] = Values[Index * 3 + 1]; + } + + Metadata->CreateVector2Attribute(AttrName, FVector2d::ZeroVector, false, false); + FPCGMetadataAttribute* MetaAttr = Metadata->GetMutableTypedAttribute(AttrName); + MetaAttr->SetValues(EntryKeys, ConvertedValues); + break; + } + case 3: + { + TArray ConvertedValues; + ConvertedValues.SetNum(Values.Num() / TupleSize); + for(int Index = 0; Index < ConvertedValues.Num(); Index++) + { + ConvertedValues[Index][0] = Values[Index * 3 + 0]; + ConvertedValues[Index][1] = Values[Index * 3 + 1]; + ConvertedValues[Index][2] = Values[Index * 3 + 2]; + } + + Metadata->CreateVectorAttribute(AttrName, FVector::ZeroVector, false, false); + FPCGMetadataAttribute* MetaAttr = Metadata->GetMutableTypedAttribute(AttrName); + MetaAttr->SetValues(EntryKeys, ConvertedValues); + break; + } + case 4: + { + TArray ConvertedValues; + ConvertedValues.SetNum(Values.Num() / TupleSize); + for(int Index = 0; Index < ConvertedValues.Num(); Index++) + { + ConvertedValues[Index][0] = Values[Index * 3 + 0]; + ConvertedValues[Index][1] = Values[Index * 3 + 1]; + ConvertedValues[Index][2] = Values[Index * 3 + 2]; + } + + Metadata->CreateVector4Attribute(AttrName, FVector4d::Zero(), false, false); + FPCGMetadataAttribute* MetaAttr = Metadata->GetMutableTypedAttribute(AttrName); + MetaAttr->SetValues(EntryKeys, ConvertedValues); + break; + } + default: + break; + } +} + +void FHoudiniPCGTranslator::CreatePCGDoubleAttribute(UPCGMetadata* Metadata, + const TArray& EntryKeys, + HAPI_NodeId NodeId, + HAPI_PartId PartId, + HAPI_AttributeOwner Owner, + FName AttrName, + int StartIndex, + int IndexCount) +{ + TArray Values; + FHoudiniHapiAccessor Accessor(NodeId, PartId, H_TCHAR_TO_UTF8(*AttrName.ToString())); + Accessor.GetAttributeData(Owner, Values, StartIndex, IndexCount); + if(Values.IsEmpty()) + return; + + HAPI_AttributeInfo AttributeInfo; + Accessor.GetInfo(AttributeInfo, Owner); + Values = HoudiniPCGGetSelectedTuple(Values, 0, AttributeInfo.tupleSize); + + Metadata->CreateDoubleAttribute(AttrName, 0, false, false); + FPCGMetadataAttribute* MetaAttr = Metadata->GetMutableTypedAttribute(AttrName); + + MetaAttr->SetValues(EntryKeys, Values); +} + +void FHoudiniPCGTranslator::CreatePCGStringAttribute( + UPCGMetadata* Metadata, + const TArray& EntryKeys, + HAPI_NodeId NodeId, + HAPI_PartId PartId, + HAPI_AttributeOwner Owner, + FName AttrName, + int StartIndex, + int IndexCount) +{ + TArray Values; + FHoudiniHapiAccessor Accessor(NodeId, PartId, H_TCHAR_TO_UTF8(*AttrName.ToString())); + Accessor.GetAttributeData(Owner, Values, StartIndex, IndexCount); + if(Values.IsEmpty()) + return; + + HAPI_AttributeInfo AttributeInfo; + Accessor.GetInfo(AttributeInfo, Owner); + Values = HoudiniPCGGetSelectedTuple(Values, 0, AttributeInfo.tupleSize); + + Metadata->CreateStringAttribute(AttrName, FString(), false, false); + FPCGMetadataAttribute* MetaAttr = Metadata->GetMutableTypedAttribute(AttrName); + + MetaAttr->SetValues(EntryKeys, Values); +} + + +``` + +`Source/HoudiniEngine/Private/HoudiniPCGTranslator.h`: + +```h +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniOutput.h" +#include "Data/PCGPointData.h" +#include "Data/PCGSplineData.h" + +class UHoudiniPCGOutputData; + +struct HOUDINIENGINE_API FHoudiniPCGTranslator +{ +public: + static void CreatePCGFromOutput(UHoudiniOutput* CurOutput); + static UHoudiniPCGOutputData* CreatePCGParamsOutput(UHoudiniOutput* CurOutput); + static UHoudiniPCGOutputData* CreatePCGSplinesOutput(UHoudiniOutput* CurOutput); + + static bool IsPCGOutput(HAPI_NodeId NodeId, HAPI_PartId PartId); +private: + static UPCGParamData * CreatePCGAttributes(HAPI_NodeId NodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + static void CreatePCGMetaAttributes(UPCGMetadata* MetaData, + TArray& Attributes, + const TArray& EntryKeys, + HAPI_NodeId NodeId, + HAPI_PartId PartId, + HAPI_AttributeOwner Owner, + int StartIndex = 0, + int IndexCount = -1); + + static UPCGPointData* CreatePCGPointData(HAPI_NodeId NodeId, HAPI_PartId PartId); + + static void CreatePCGInt32Attribute(UPCGMetadata* Metadata, + const TArray& EntryKeys, + HAPI_NodeId NodeId, + HAPI_PartId PartId, + HAPI_AttributeOwner Owner, + FName AttrName, + int StartIndex, + int IndexCount); + + static void CreatePCGInt64Attribute(UPCGMetadata* Metadata, + const TArray& EntryKeys, + HAPI_NodeId NodeId, + HAPI_PartId PartId, + HAPI_AttributeOwner Owner, + FName AttrName, + int StartIndex, + int IndexCount); + + static void CreatePCGFloatAttribute(UPCGMetadata* Metadata, + const TArray& EntryKeys, + HAPI_NodeId NodeId, + HAPI_PartId PartId, + HAPI_AttributeOwner Owner, + FName AttrName, + int StartIndex, + int IndexCount); + + static void CreatePCGDoubleAttribute(UPCGMetadata* Metadata, + const TArray& EntryKeys, + HAPI_NodeId NodeId, + HAPI_PartId PartId, + HAPI_AttributeOwner Owner, + FName AttrName, + int StartIndex, + int IndexCount); + + static void CreatePCGStringAttribute( + UPCGMetadata* Metadata, + const TArray& EntryKeys, + HAPI_NodeId NodeId, + HAPI_PartId PartId, + HAPI_AttributeOwner Owner, + FName AttrName, + int StartIndex, + int IndexCount); +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniPCGUtils.cpp`: + +```cpp +/* +* Copyright (c) <2025> Side Effects Software Inc. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#include "HoudiniPCGUtils.h" + +#include "PCGContext.h" +#include "Data/PCGPointData.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniPCGNode.h" +#include "HoudiniPCGCookable.h" +#include "HoudiniInput.h" +#include "ConnectionDrawingPolicy.h" +#include "HoudiniEngine.h" +#include "HoudiniPCGTranslator.h" +#include "HoudiniPCGDataObject.h" +#include "Landscape.h" +#include "PCGParamData.h" +#include "Async/Async.h" +#include "Components/InstancedStaticMeshComponent.h" + +HOUDINI_PCG_DEFINE_LOG_CATEGORY(); + +FString FHoudiniPCGUtils::ParameterInputPinName = FString(TEXT("Parameters")); +FName FHoudiniPCGUtils::HDAInputObjectName = FName(FString(TEXT("object"))); +FCriticalSection FHoudiniPCGUtils::CriticalSection; +EHoudiniPCGSessionStatus FHoudiniPCGUtils::SessionStatus; + +void +FHoudiniPCGUtils::UnrealToHoudini(const FVector3d& UnrealVector, float HoudiniVector[3]) +{ + HoudiniVector[0] = static_cast(UnrealVector.X); + HoudiniVector[1] = static_cast(UnrealVector.Z); + HoudiniVector[2] = static_cast(UnrealVector.Y); +} + +TArray +FHoudiniPCGUtils::GetPCGOutputData(const FHoudiniBakedOutput* BakedOutput) +{ + TArray Outputs; + + int ObjectIndex = 0; + for(auto It : BakedOutput->BakedOutputObjects) + { + auto& BakedOutputObject = It.Value; + + FHoudiniPCGObjectOutput& PCGOutputObject = Outputs.Emplace_GetRef(); + PCGOutputObject.OutputObjectIndex = ObjectIndex; + PCGOutputObject.ActorPath = BakedOutputObject.Actor; + PCGOutputObject.ComponentPath = BakedOutputObject.BakedComponent; + PCGOutputObject.ObjectPath = BakedOutputObject.BakedObject; + + if (PCGOutputObject.ActorPath.IsValid()) + { + UObject* LoadedObject = StaticLoadObject(UObject::StaticClass(), nullptr, *PCGOutputObject.ActorPath.ToString()); + if(IsValid(LoadedObject)) + PCGOutputObject.OutputType = FHoudiniPCGUtils::GetTypeStringFromObject(LoadedObject); + } + + if (PCGOutputObject.OutputType.IsEmpty()) + { + if(PCGOutputObject.ObjectPath.IsValid()) + { + UObject* LoadedObject = StaticLoadObject(UObject::StaticClass(), nullptr, *PCGOutputObject.ObjectPath.ToString()); + if(IsValid(LoadedObject)) + PCGOutputObject.OutputType = FHoudiniPCGUtils::GetTypeStringFromObject(LoadedObject); + } + } + + ObjectIndex++; + } + return Outputs; +} + +FString FHoudiniPCGUtils::GetTypeStringFromObject(UObject * Object) +{ + if(Object->IsA()) + { + return TEXT("Mesh"); + } + else if(Object->IsA() || Object->IsA()) + { + return TEXT("Landscape"); + } + return TEXT(""); +} + +FString FHoudiniPCGUtils::GetTypeStringFromComponent(USceneComponent* Component) +{ + if(IsValid(Component)) + { + if(IsValid(Component)) + { + if(Component->IsA()) + { + return TEXT("InstancedStaticMesh"); + } + } + } + return TEXT(""); +} + + +FString FHoudiniPCGUtils::GetTypeStringFromOutputObject(const FHoudiniOutputObject& OutputObject) +{ + if (IsValid(OutputObject.OutputObject)) + { + FString Result = GetTypeStringFromObject(OutputObject.OutputObject); + if(!Result.IsEmpty()) + return Result; + } + + + if (!OutputObject.OutputComponents.IsEmpty()) + { + USceneComponent* SceneComponent = Cast(OutputObject.OutputComponents[0]); + if (IsValid(SceneComponent)) + { + FString Result = GetTypeStringFromComponent(SceneComponent); + if(!Result.IsEmpty()) + return Result; + } + } + + return TEXT(""); +} + + +TArray +FHoudiniPCGUtils::GetPCGOutputData(const UHoudiniOutput* HoudiniOutput) +{ + TArray Outputs; + + int ObjectIndex = 0; + for(auto It : HoudiniOutput->GetOutputObjects()) + { + FHoudiniOutputObject& OutputObj = It.Value; + + FHoudiniPCGObjectOutput& PCGOutputObject = Outputs.Emplace_GetRef(); + PCGOutputObject.OutputObjectIndex = ObjectIndex; + PCGOutputObject.OutputType = GetTypeStringFromOutputObject(OutputObj); + + if (UHoudiniLandscapeTargetLayerOutput * LandscapeOutput = Cast(OutputObj.OutputObject.Get())) + { + if (IsValid(LandscapeOutput->Landscape)) + PCGOutputObject.ActorPath = LandscapeOutput->Landscape->GetPathName(); + else if (IsValid(LandscapeOutput->LandscapeProxy)) + PCGOutputObject.ActorPath = LandscapeOutput->LandscapeProxy->GetPathName(); + } + else + { + if(OutputObj.OutputObject) + { + PCGOutputObject.ObjectPath = OutputObj.OutputObject.GetPathName(); + } + + if(OutputObj.OutputComponents.Num() > 0) + { + PCGOutputObject.ComponentPath = OutputObj.OutputComponents[0].GetPathName(); + PCGOutputObject.ActorPath = OutputObj.OutputComponents[0]->GetOuter()->GetPathName(); + } + else if(OutputObj.ProxyComponent) + { + PCGOutputObject.ComponentPath = OutputObj.ProxyComponent.GetPathName(); + if(OutputObj.ProxyObject) + PCGOutputObject.ObjectPath = OutputObj.ProxyObject.GetPackage().GetPathName(); + } + else if(OutputObj.OutputActors.Num() > 0) + { + PCGOutputObject.ActorPath = OutputObj.OutputActors[0]->GetPathName(); + } + + } + + ObjectIndex++; + } + return Outputs; +} + +bool +FHoudiniPCGUtils::HasPCGOutputs(const UHoudiniOutput* HoudiniOutput) +{ + for (auto & It : HoudiniOutput->GetOutputObjects()) + { + auto & Object = It.Value; + + if (!IsValid(Object.OutputObject)) + continue; + + if(Object.OutputObject->IsA()) + return true; + } + return false; +} + +bool +FHoudiniPCGUtils::HasPCGOutputs(const FHoudiniBakedOutput* HoudiniOutput) +{ + for(auto& It : HoudiniOutput->BakedOutputObjects) + { + auto& Object = It.Value; + + if(IsValid(Object.PCGOutputData)) + return true; + } + return false; +} + + +EHoudiniPCGInputType +FHoudiniPCGUtils::GetInputType(const UPCGData* PCGData) +{ + if(PCGData->IsA()) + return EHoudiniPCGInputType::PCGData; + else if(PCGData->IsA()) + { + const UPCGMetadata* Metadata = PCGData->ConstMetadata(); + + const FPCGMetadataAttribute* ObjectAttrs = static_cast*>(Metadata->GetConstAttribute(HDAInputObjectName)); + if(ObjectAttrs) + return EHoudiniPCGInputType::UnrealObjects; + else + return EHoudiniPCGInputType::PCGData; + } + else + { + return EHoudiniPCGInputType::None; + } +} + +TArray +FHoudiniPCGUtils::GetValueAsString(const TArray & Defaults, const FHoudiniPCGAttributes& Attributes, int Index) +{ + // Always return at least one result. + TArray Result = Defaults; + if(Result.IsEmpty()) + Result.SetNum(1); + + if(Attributes.Strings) + { + Result[0] = Attributes.Strings->GetValueFromItemKey(Index); + } + else if(Attributes.Names) + { + Result[0] = Attributes.Names->GetValueFromItemKey(Index).ToString(); + } + else if(Attributes.SoftObjectPaths) + { + Result[0] = Attributes.SoftObjectPaths->GetValueFromItemKey(Index).ToString(); + } + else if(Attributes.SoftClassPaths) + { + Result[0] = Attributes.SoftClassPaths->GetValueFromItemKey(Index).ToString(); + } + else if (Attributes.Floats) + { + Result[0] = FString::SanitizeFloat(Attributes.Floats->GetValueFromItemKey(Index)); + } + else if (Attributes.Int32s) + { + Result[0] = FString::FromInt(Attributes.Int32s->GetValueFromItemKey(Index)); + } + else if(Attributes.Int64s) + { + Result[0] = FString::FromInt(static_cast(Attributes.Int64s->GetValueFromItemKey(Index))); + } + else if(Attributes.Bools) + { + Result[0] = Attributes.Bools->GetValueFromItemKey(Index) ? FString(TEXT("1")) : FString(TEXT("0")); + } + return Result; +} + +TArray +FHoudiniPCGUtils::GetValueAsInt(const TArray & Defaults, const FHoudiniPCGAttributes& Attributes, int Index) +{ + // Always return at least one result. + TArray Result = Defaults; + if(Result.IsEmpty()) + Result.SetNum(1); + + if(Attributes.Strings) + { + FString StringValue = Attributes.Strings->GetValueFromItemKey(Index); + Result[0] = FCString::Atoi(*StringValue); + } + else if(Attributes.Floats) + { + float FloatValue = Attributes.Floats->GetValueFromItemKey(Index); + Result[0] = static_cast(FloatValue); + } + else if(Attributes.Doubles) + { + Result[0] = static_cast(Attributes.Doubles->GetValueFromItemKey(Index)); + } + else if(Attributes.Int32s) + { + Result[0] = Attributes.Int32s->GetValueFromItemKey(Index); + } + else if(Attributes.Int64s) + { + Result[0] = static_cast(Attributes.Int64s->GetValueFromItemKey(Index)); + } + return Result; +} + +TArray +FHoudiniPCGUtils::GetValueAsFloat(const TArray& DefaultValues, const FHoudiniPCGAttributes& Attributes, int RowIndex) +{ + // Always return at least one result. + TArray Result = DefaultValues; + if(Result.IsEmpty()) + Result.SetNum(1); + + if(Attributes.Strings) + { + FString StringValue = Attributes.Strings->GetValueFromItemKey(RowIndex); + Result[0] = FCString::Atof(*StringValue); + } + else if(Attributes.Floats) + { + Result[0] = Attributes.Floats->GetValueFromItemKey(RowIndex); + } + else if(Attributes.Doubles) + { + Result[0] = static_cast(Attributes.Doubles->GetValueFromItemKey(RowIndex)); + } + else if(Attributes.Int32s) + { + int IntValue = Attributes.Int32s->GetValueFromItemKey(RowIndex); + Result[0] = static_cast(IntValue); + } + else if(Attributes.Int64s) + { + int Int64Value = Attributes.Int64s->GetValueFromItemKey(RowIndex); + Result[0] = static_cast(Int64Value); + } + else if(Attributes.Bools) + { + Result[0] = Attributes.Bools->GetValueFromItemKey(RowIndex) ? 1.0f : 0.0f; + } + else if(Attributes.Vector2ds) + { + // NOTE: We do not convert UE->H space because we don't know what this represents. a point? a vector? + // something else? + FVector2d Vec = Attributes.Vector2ds->GetValueFromItemKey(RowIndex); + Result.SetNum(2); + Result[0] = Vec.X; + Result[1] = Vec.Y; + } + else if (Attributes.Vector3ds) + { + // NOTE: We do not convert UE->H space because we don't know what this represents. a point? a vector? + // something else? + FVector3d Vec = Attributes.Vector3ds->GetValueFromItemKey(RowIndex); + Result.SetNum(3); + Result[0] = Vec.X; + Result[1] = Vec.Y; + Result[2] = Vec.Z; + } + else if(Attributes.Vector4ds) + { + // NOTE: We do not convert UE->H space because we don't know what this represents. a point? a vector? + // something else? + FVector4d Vec = Attributes.Vector4ds->GetValueFromItemKey(RowIndex); + Result.SetNum(4); + Result[0] = Vec.X; + Result[1] = Vec.Y; + Result[2] = Vec.Z; + Result[3] = Vec.W; + } + else if(Attributes.Quats) + { + // NOTE: We do not convert UE->H space because we don't know what this represents. a point? a vector? + // something else? + FQuat Quat = Attributes.Quats->GetValueFromItemKey(RowIndex); + FVector4d Vec = FHoudiniPCGUtils::UnrealToHoudiniQuat(Quat); + Result.SetNum(4); + Result[0] = Vec.X; + Result[1] = Vec.Y; + Result[2] = Vec.Z; + Result[3] = Vec.W; + } + else if(Attributes.Rotators) + { + FRotator Rotator = Attributes.Rotators->GetValueFromItemKey(RowIndex); + Result.SetNum(3); + Result[0] = Rotator.Roll; + Result[1] = Rotator.Yaw; + Result[2] = Rotator.Pitch; + } + + return Result; +} + +FHoudiniPCGAttributes::FHoudiniPCGAttributes(const UPCGMetadata* Metadata, const FName& ParameterName) +{ + // Cache off all attribute types we might be interested in. + this->Int32s = Metadata->GetConstTypedAttribute(ParameterName); + this->Int64s = Metadata->GetConstTypedAttribute(ParameterName); + this->Vector2ds = Metadata->GetConstTypedAttribute(ParameterName); + this->Vector3ds = Metadata->GetConstTypedAttribute(ParameterName); + this->Vector4ds = Metadata->GetConstTypedAttribute(ParameterName); + this->Floats = Metadata->GetConstTypedAttribute(ParameterName); + this->Doubles = Metadata->GetConstTypedAttribute(ParameterName); + this->Strings = Metadata->GetConstTypedAttribute(ParameterName); + this->Bools = Metadata->GetConstTypedAttribute(ParameterName); + this->Rotators = Metadata->GetConstTypedAttribute(ParameterName); + this->Names = Metadata->GetConstTypedAttribute(ParameterName); + this->SoftObjectPaths = Metadata->GetConstTypedAttribute(ParameterName); + this->SoftClassPaths = Metadata->GetConstTypedAttribute(ParameterName); + this->Quats = Metadata->GetConstTypedAttribute(ParameterName); + this->Bools = Metadata->GetConstTypedAttribute(ParameterName); + this->NumRows = Metadata->GetItemCountForChild(); + this->Name = ParameterName.ToString(); +} + +FString +FHoudiniPCGUtils::GetHDAInputName(int Index) +{ + // Name of the pin that exposes HDA input "Index". + FString PinName = FString::Printf(TEXT("Input %d"), Index); + return PinName; +} + +FVector3d +FHoudiniPCGUtils::HoudiniToUnrealPosition(float HoudiniVector[3]) +{ + FVector3d Position; + Position.X = HoudiniVector[0] * 100.0; + Position.Y = HoudiniVector[2] * 100.0; + Position.Z = HoudiniVector[1] * 100.0; + return Position; +} + +FVector3d +FHoudiniPCGUtils::HoudiniToUnrealVector(float HoudiniVector[3]) +{ + FVector3d Position; + Position.X = HoudiniVector[0]; + Position.Y = HoudiniVector[2]; + Position.Z = HoudiniVector[1]; + return Position; +} + +FQuat +FHoudiniPCGUtils::HoudiniToUnrealQuat(float HoudiniQuat[4]) +{ + FQuat Result(HoudiniQuat[0], HoudiniQuat[2], HoudiniQuat[1], -HoudiniQuat[3]); + return Result; +} + +FVector4d +FHoudiniPCGUtils::UnrealToHoudiniQuat(const FQuat& Quat) +{ + FVector4d Result; + Result[0] = Quat.X; + Result[1] = Quat.Z; + Result[2] = Quat.Y; + Result[3] = -Quat.W; + return Result; +} + +void +FHoudiniPCGUtils::LogVisualWarning(const FPCGContext* Context, const FString& WarningMessage) +{ + HOUDINI_LOG_ERROR(TEXT("Warning: %s"), *WarningMessage); + FText Text = FText::FromString(WarningMessage); + PCGE_LOG_C(Warning, GraphAndLog, Context, Text); +} + +void +FHoudiniPCGUtils::LogVisualError(const FPCGContext* Context, const TArray& ErrorMessages) +{ + FString CombinedErrors = FString::Join(ErrorMessages, TEXT("\n")); + LogVisualError(Context, *CombinedErrors); +} + +void +FHoudiniPCGUtils::LogVisualError(const FPCGContext* Context, const FString& ErrorMessage) +{ + HOUDINI_LOG_ERROR(TEXT("Error: %s"), *ErrorMessage); + FText Text = FText::FromString(ErrorMessage); + PCGE_LOG_C(Error, GraphAndLog, Context, Text); +} + +EHoudiniPCGSessionStatus +FHoudiniPCGUtils::StartSession() +{ + if(FHoudiniEngine::Get().GetSession()) + { + SessionStatus = EHoudiniPCGSessionStatus::PCGSessionStatus_Created; + return SessionStatus; + } + bool bSuccess = FHoudiniEngine::Get().RestartSession(false); + SessionStatus = bSuccess ? EHoudiniPCGSessionStatus::PCGSessionStatus_Created : EHoudiniPCGSessionStatus::PCGSessionStatus_Error; + + if(bSuccess) + { + HOUDINI_PCG_MESSAGE(TEXT("Session Created...")); + } + else + { + + HOUDINI_PCG_ERROR(TEXT("Session Not Created...")); + } + return SessionStatus; +} + +EHoudiniPCGSessionStatus +FHoudiniPCGUtils::StartSessionAsync() +{ + FScopeLock Lock(&CriticalSection); + + if (FHoudiniEngine::Get().GetSession()) + { + SessionStatus = EHoudiniPCGSessionStatus::PCGSessionStatus_Created; + return SessionStatus; + } + + if (SessionStatus == EHoudiniPCGSessionStatus::PCGSessionStatus_Created) + { + // Make sure the session is still good. + if(!FHoudiniEngine::Get().GetSession()) + { + SessionStatus = EHoudiniPCGSessionStatus::PCGSessionStatus_None; + HOUDINI_PCG_MESSAGE(TEXT("Houdini Session Lost...")); + } + else + { + return SessionStatus; + } + } + + if (SessionStatus == EHoudiniPCGSessionStatus::PCGSessionStatus_None) + { + HOUDINI_PCG_MESSAGE(TEXT("No Unreal-Houdini Session found, will try to establish one.")); + EHoudiniPCGSessionStatus* SessionStatusPtr = &SessionStatus; + SessionStatus = EHoudiniPCGSessionStatus::PCGSessionStatus_Creating; + Async(EAsyncExecution::ThreadPool, [SessionStatusPtr]() + { + bool bConnected = FHoudiniEngine::Get().ConnectSession(false); + if(bConnected) + { + HOUDINI_PCG_MESSAGE(TEXT("Connection to existing Houdini Session.")); + SessionStatus = EHoudiniPCGSessionStatus::PCGSessionStatus_Created; + return; + } + + + bool bSuccess = FHoudiniEngine::Get().RestartSession(false); + *SessionStatusPtr = bSuccess ? EHoudiniPCGSessionStatus::PCGSessionStatus_Created : EHoudiniPCGSessionStatus::PCGSessionStatus_Error; + if (bSuccess) + { + HOUDINI_PCG_MESSAGE(TEXT("Session Created.")); + } + else + { + + HOUDINI_PCG_ERROR(TEXT("Session Not Created.")); + } + }); + } + + return SessionStatus; +} + +UPCGComponent* FHoudiniPCGUtils::GetSourceComponent(FPCGContext* Context) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + + return Cast(Context->ExecutionSource.Get()); +#else + return Context->SourceComponent.IsValid() ? Context->SourceComponent.Get() : nullptr; +#endif + +} + +``` + +`Source/HoudiniEngine/Private/HoudiniPCGUtils.h`: + +```h +/* Copyright (c) <2025> Side Effects Software Inc. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#pragma once +#include "HoudiniOutput.h" +#include "HoudiniPCGComponent.h" +#include "PCGManagedResource.h" +#include "Data/PCGPointData.h" +#include "HAPI/HAPI_Common.h" + + +class UHoudiniInput; + +struct FHoudiniPCGObjectOutput +{ + // Data to be output on the output pin of the executed node. + int OutputObjectIndex = 0; + FSoftObjectPath ComponentPath; + FSoftObjectPath ActorPath; + FSoftObjectPath ObjectPath; + FString OutputType; +}; + +enum class EHoudiniPCGInputType +{ + None, + UnrealObjects, + PCGData +}; + +enum class EHoudiniPCGSessionStatus +{ + PCGSessionStatus_None, + PCGSessionStatus_Creating, + PCGSessionStatus_Created, + PCGSessionStatus_Error +}; +struct FHoudiniPCGAttributes +{ + FHoudiniPCGAttributes(const UPCGMetadata* Metadata, const FName & ParameterNames); + + FString Name; + int NumRows; + const FPCGMetadataAttribute* Floats; + const FPCGMetadataAttribute* Doubles; + const FPCGMetadataAttribute* Int32s; + const FPCGMetadataAttribute* Int64s; + const FPCGMetadataAttribute* Vector2ds; + const FPCGMetadataAttribute* Vector3ds; + const FPCGMetadataAttribute* Vector4ds; + const FPCGMetadataAttribute* Quats; + const FPCGMetadataAttribute* Strings; + const FPCGMetadataAttribute* Bools; + const FPCGMetadataAttribute* Rotators; + const FPCGMetadataAttribute* Names; + const FPCGMetadataAttribute* SoftObjectPaths; + const FPCGMetadataAttribute* SoftClassPaths; +}; + +class UHoudiniPCGCookable; + +class HOUDINIENGINE_API FHoudiniPCGUtils +{ +public: + + static FString ParameterInputPinName;; + static FName HDAInputObjectName; + + static EHoudiniPCGSessionStatus StartSessionAsync(); + static EHoudiniPCGSessionStatus StartSession(); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Conversion functions. + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + static void UnrealToHoudini(const FVector3d& UnrealVector, float HoudiniVector[3]); + static FVector4d UnrealToHoudiniQuat(const FQuat& Quat); + static FVector3d HoudiniToUnrealPosition(float HoudiniVector[3]); + static FVector3d HoudiniToUnrealVector(float HoudiniVector[3]); + static FQuat HoudiniToUnrealQuat(float HoudiniVector[4]); + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Input Functions. + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + static EHoudiniPCGInputType GetInputType(const UPCGData* TaggedData); + + static FString GetHDAInputName(int Index); + + static bool HasPCGOutputs(const UHoudiniOutput* HoudiniOutput); + static bool HasPCGOutputs(const FHoudiniBakedOutput* HoudiniOutput); + + static TArray GetPCGOutputData(const UHoudiniOutput * HoudiniOutput); + static TArray GetPCGOutputData(const FHoudiniBakedOutput * BakedOutput); + static TArray GetPCGOutputData(const FTOPWorkResult* WorkItem); + + static TArray GetValueAsString(const TArray& DefaultValues, const FHoudiniPCGAttributes & Attributes, int RowIndex); + static TArray GetValueAsInt(const TArray& DefaultValues, const FHoudiniPCGAttributes& Attributes, int RowIndex); + static TArray GetValueAsFloat(const TArray & DefaultValues, const FHoudiniPCGAttributes& Attributes, int RowIndex); + + static void LogVisualWarning(const FPCGContext* Context, const FString& ErrorText); + static void LogVisualError(const FPCGContext* Context, const FString & ErrorText); + static void LogVisualError(const FPCGContext* Context, const TArray & Errors); + + static EHoudiniPCGSessionStatus SessionStatus; + + static FString GetTypeStringFromOutputObject(const FHoudiniOutputObject & OutputObject); + + static FString GetTypeStringFromObject(UObject* Object); + static FString GetTypeStringFromComponent(USceneComponent* Component); + + static UPCGComponent* GetSourceComponent(FPCGContext* Context); + +private: + // Synchronization primitive. + static FCriticalSection CriticalSection; + +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniPDGImporterMessages.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPDGImporterMessages.h" +#include "HoudiniEngineRuntimeUtils.h" + + +FHoudiniPDGImportBGEOMessage::FHoudiniPDGImportBGEOMessage() + : FilePath() + , Name() + , TOPNodeId(-1) + , WorkItemId(-1) +{ + StaticMeshGenerationProperties = FHoudiniEngineRuntimeUtils::GetDefaultStaticMeshGenerationProperties(); + MeshBuildSettings = FHoudiniEngineRuntimeUtils::GetDefaultMeshBuildSettings(); +} + +FHoudiniPDGImportBGEOMessage::FHoudiniPDGImportBGEOMessage( + const FString& InFilePath, + const FString& InName, + const FHoudiniPackageParams& InPackageParams +) + : FilePath(InFilePath) + , Name(InName) + , TOPNodeId(-1) + , WorkItemId(-1) +{ + SetPackageParams(InPackageParams); + StaticMeshGenerationProperties = FHoudiniEngineRuntimeUtils::GetDefaultStaticMeshGenerationProperties(); + MeshBuildSettings = FHoudiniEngineRuntimeUtils::GetDefaultMeshBuildSettings(); +} + +FHoudiniPDGImportBGEOMessage::FHoudiniPDGImportBGEOMessage( + const FString& InFilePath, + const FString& InName, + const FHoudiniPackageParams& InPackageParams, + HAPI_NodeId InTOPNodeId, + HAPI_PDG_WorkItemId InWorkItemId +) + : FilePath(InFilePath) + , Name(InName) + , TOPNodeId(InTOPNodeId) + , WorkItemId(InWorkItemId) +{ + SetPackageParams(InPackageParams); + StaticMeshGenerationProperties = FHoudiniEngineRuntimeUtils::GetDefaultStaticMeshGenerationProperties(); + MeshBuildSettings = FHoudiniEngineRuntimeUtils::GetDefaultMeshBuildSettings(); +} + +FHoudiniPDGImportBGEOMessage::FHoudiniPDGImportBGEOMessage( + const FString& InFilePath, + const FString& InName, + const FHoudiniPackageParams& InPackageParams, + HAPI_NodeId InTOPNodeId, + HAPI_PDG_WorkItemId InWorkItemId, + const FHoudiniStaticMeshGenerationProperties& InStaticMeshGenerationProperties, + const FMeshBuildSettings& InMeshBuildSettings +) + : FilePath(InFilePath) + , Name(InName) + , TOPNodeId(InTOPNodeId) + , WorkItemId(InWorkItemId) + , StaticMeshGenerationProperties(InStaticMeshGenerationProperties) + , MeshBuildSettings(InMeshBuildSettings) +{ + SetPackageParams(InPackageParams); +} + +void FHoudiniPDGImportBGEOMessage::SetPackageParams(const FHoudiniPackageParams& InPackageParams) +{ + PackageParams = InPackageParams; + PackageParams.OuterPackage = nullptr; +} + +void FHoudiniPDGImportBGEOMessage::PopulatePackageParams(FHoudiniPackageParams& OutPackageParams) const +{ + UObject* KeepOuter = OutPackageParams.OuterPackage; + OutPackageParams = PackageParams; + OutPackageParams.OuterPackage = KeepOuter; +} + +FHoudiniPDGImportBGEOResultMessage::FHoudiniPDGImportBGEOResultMessage() + : ImportResult(EHoudiniPDGImportBGEOResult::HPIBR_Failed) +{ + +} + +FHoudiniPDGImportBGEOResultMessage::FHoudiniPDGImportBGEOResultMessage( + const FString& InFilePath, + const FString& InName, + const FHoudiniPackageParams& InPackageParams, + const EHoudiniPDGImportBGEOResult& InImportResult +) + : FHoudiniPDGImportBGEOMessage(InFilePath, InName, InPackageParams) + , ImportResult(InImportResult) +{ +} + +FHoudiniPDGImportBGEODiscoverMessage::FHoudiniPDGImportBGEODiscoverMessage() + : CommandletGuid() +{ + +} + +FHoudiniPDGImportBGEODiscoverMessage::FHoudiniPDGImportBGEODiscoverMessage(const FGuid& InCommandletGuid) + : CommandletGuid(InCommandletGuid) +{ + +} + + +``` + +`Source/HoudiniEngine/Private/HoudiniPDGImporterMessages.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "Misc/Guid.h" + +#include "HAPI/HAPI_Common.h" + +#include "HoudiniPackageParams.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniOutput.h" +#include "HoudiniGenericAttribute.h" +#include "HoudiniInstanceTranslator.h" + +#include "HoudiniPDGImporterMessages.generated.h" + +struct FHoudiniStaticMeshGenerationProperties; +struct FMeshBuildSettings; + +// Message used to find/discover running commandlets +USTRUCT() +struct HOUDINIENGINE_API FHoudiniPDGImportBGEODiscoverMessage +{ +public: + GENERATED_BODY(); + + FHoudiniPDGImportBGEODiscoverMessage(); + + FHoudiniPDGImportBGEODiscoverMessage(const FGuid& InCommandletGuid); + + // The GUID of the commandlet we are looking for + UPROPERTY() + FGuid CommandletGuid; +}; + +USTRUCT() +struct HOUDINIENGINE_API FHoudiniPDGImportBGEOMessage +{ +public: + GENERATED_BODY(); + + FHoudiniPDGImportBGEOMessage(); + + FHoudiniPDGImportBGEOMessage(const FString& InFilePath, const FString& InName, const FHoudiniPackageParams& InPackageParams); + + FHoudiniPDGImportBGEOMessage( + const FString& InFilePath, + const FString& InName, + const FHoudiniPackageParams& InPackageParams, + HAPI_NodeId InTOPNodeId, + HAPI_PDG_WorkItemId InWorkItemId); + + FHoudiniPDGImportBGEOMessage( + const FString& InFilePath, + const FString& InName, + const FHoudiniPackageParams& InPackageParams, + HAPI_NodeId InTOPNodeId, + HAPI_PDG_WorkItemId InWorkItemId, + const FHoudiniStaticMeshGenerationProperties& InStaticMeshGenerationProperties, + const FMeshBuildSettings& InMeshBuildSettings); + + void SetPackageParams(const FHoudiniPackageParams& InPackageParams); + + void PopulatePackageParams(FHoudiniPackageParams &OutPackageParams) const; + + // BGEO file path + UPROPERTY() + FString FilePath; + + // PDG work item name + UPROPERTY() + FString Name; + + // TOP/PDG info + // TOP node ID + UPROPERTY() + // HAPI_NodeId TOPNodeId; + int32 TOPNodeId; + + // Work item id + UPROPERTY() + // HAPI_PDG_WorkItemId WorkItemId; + int32 WorkItemId; + + // Package params for the asset + UPROPERTY() + FHoudiniPackageParams PackageParams; + + // Settings used during static mesh generation + UPROPERTY() + FHoudiniStaticMeshGenerationProperties StaticMeshGenerationProperties; + + // Static mesh build settings used during mesh builds + UPROPERTY() + FMeshBuildSettings MeshBuildSettings; +}; + + +UENUM() +enum class EHoudiniPDGImportBGEOResult : uint8 +{ + // Create uassets from the bgeo completely failed. + HPIBR_Failed UMETA(DisplayName="Failed"), + + // Successfully created uassets for all content in the bgeo + HPIBR_Success UMETA(DisplayName = "Success"), + + // Some uassets were created, but there were unsupported objects in the bgeo as well + HPIBR_PartialSuccess UMETA(DisplayName = "Partial Success"), + + HIBPR_MAX +}; + +USTRUCT() +struct HOUDINIENGINE_API FHoudiniGenericAttributes +{ +public: + GENERATED_BODY() + + FHoudiniGenericAttributes() {}; + FHoudiniGenericAttributes(const TArray& InPropertyAttributes) : PropertyAttributes(InPropertyAttributes) {}; + FHoudiniGenericAttributes(TArray&& InPropertyAttributes) : PropertyAttributes(InPropertyAttributes) {}; + + UPROPERTY() + TArray PropertyAttributes; +}; + +USTRUCT() +struct HOUDINIENGINE_API FHoudiniPDGImportNodeOutputObject +{ +public: + GENERATED_BODY(); + + UPROPERTY() + FHoudiniOutputObjectIdentifier Identifier; + + UPROPERTY() + FString PackagePath; + + UPROPERTY() + FHoudiniGenericAttributes GenericAttributes; + + UPROPERTY() + TMap CachedAttributes; +}; + +USTRUCT() +struct HOUDINIENGINE_API FHoudiniPDGImportNodeOutput +{ +public: + GENERATED_BODY(); + + UPROPERTY() + TArray HoudiniGeoPartObjects; + + UPROPERTY() + TArray OutputObjects; + + UPROPERTY() + TArray InstancedOutputPartData; +}; + +USTRUCT() +struct HOUDINIENGINE_API FHoudiniPDGImportBGEOResultMessage : public FHoudiniPDGImportBGEOMessage +{ +public: + GENERATED_BODY(); + + FHoudiniPDGImportBGEOResultMessage(); + + FHoudiniPDGImportBGEOResultMessage(const FString& InFilePath, const FString& InName, const FHoudiniPackageParams& InPackageParams, const EHoudiniPDGImportBGEOResult& InImportResult); + + void operator=(const FHoudiniPDGImportBGEOMessage& InRHS) { (*static_cast(this)) = InRHS; } + + // Result of the bgeo import -> uassets + UPROPERTY() + EHoudiniPDGImportBGEOResult ImportResult; + + UPROPERTY() + TArray Outputs; + +}; + + +``` + +`Source/HoudiniEngine/Private/HoudiniPDGManager.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPDGManager.h" + +#include "Modules/ModuleManager.h" +#include "MessageEndpointBuilder.h" +#include "HAL/FileManager.h" + +#include "HoudiniApi.h" +#include "HoudiniAsset.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineString.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniPDGAssetLink.h" +#include "HoudiniPackageParams.h" + +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniMeshTranslator.h" +#include "HoudiniOutputTranslator.h" +#include "HoudiniPDGTranslator.h" +#include "HoudiniPDGImporterMessages.h" + +#include "HAPI/HAPI_Common.h" + +HOUDINI_PDG_DEFINE_LOG_CATEGORY(); + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +FHoudiniPDGManager::FHoudiniPDGManager() +{ +} + +FHoudiniPDGManager::~FHoudiniPDGManager() +{ +} + +bool +FHoudiniPDGManager::InitializePDGAssetLink( + const HAPI_NodeId& InNodeId, UObject* InOuter, UHoudiniPDGAssetLink*& PDGAssetLink, const bool& bHasBeenLoaded) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniPDGManager::InitializePDGAssetLink); + if (InNodeId < 0) + return false; + + if (!FHoudiniEngineUtils::IsHoudiniNodeValid((HAPI_NodeId)InNodeId)) + return false; + + // Create a new PDG Asset Link Object + bool bRegisterPDGAssetLink = false; + if (!IsValid(PDGAssetLink)) + { + PDGAssetLink = NewObject(InOuter, UHoudiniPDGAssetLink::StaticClass(), NAME_None, RF_Transactional); + bRegisterPDGAssetLink = true; + } + + if (!IsValid(PDGAssetLink)) + return false; + + PDGAssetLink->AssetID = InNodeId; + + // Get the HDA's info + HAPI_NodeInfo AssetInfo; + FHoudiniApi::NodeInfo_Init(&AssetInfo); + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), (HAPI_NodeId)PDGAssetLink->AssetID, &AssetInfo), false); + + // Get the node path + FString AssetNodePath; + FHoudiniEngineString::ToFString(AssetInfo.internalNodePathSH, PDGAssetLink->AssetNodePath); + + // Get the node name + FString AssetName; + FHoudiniEngineString::ToFString(AssetInfo.nameSH, PDGAssetLink->AssetName); + + const bool bZeroWorkItemTallys = true; + if (!FHoudiniPDGManager::PopulateTOPNetworks(PDGAssetLink, bZeroWorkItemTallys)) + { + // We couldn't find any valid TOPNet/TOPNode, this is not a PDG Asset + // Make sure the HDA doesn't have a PDGAssetLink + return false; + } + + // If the PDG asset link comes from a loaded asset, we also need to register it + if (bHasBeenLoaded) + { + bRegisterPDGAssetLink = true; + } + + // We have found valid TOPNetworks and TOPNodes, + // This is a PDG HDA, so add the AssetLink to it + PDGAssetLink->LinkState = EPDGLinkState::Linked; + + if (PDGAssetLink->SelectedTOPNetworkIndex < 0) + PDGAssetLink->SelectedTOPNetworkIndex = 0; + + if (bRegisterPDGAssetLink) + { + // Register this PDG Asset Link to the PDG Manager + TWeakObjectPtr AssetLinkPtr(PDGAssetLink); + PDGAssetLinks.Add(AssetLinkPtr); + } + + // If the commandlet is enabled, check if we have started and established communication with the commandlet yet + // if not, try to start the commandlet + bool bCommandletIsEnabled = false; + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (IsValid(HoudiniRuntimeSettings)) + { + bCommandletIsEnabled = HoudiniRuntimeSettings->bPDGAsyncCommandletImportEnabled; + } + + if (bCommandletIsEnabled) + { + const EHoudiniBGEOCommandletStatus CommandletStatus = UpdateAndGetBGEOCommandletStatus(); + if (CommandletStatus == EHoudiniBGEOCommandletStatus::NotStarted && bCommandletIsEnabled) + { + CreateBGEOCommandletAndEndpoint(); + } + } + + return true; +} + +bool +FHoudiniPDGManager::UpdatePDGAssetLink(UHoudiniPDGAssetLink* PDGAssetLink) +{ + if (!IsValid(PDGAssetLink)) + return false; + + // TODO COOKABLE: Improve! do not rely on GetOuter + UHoudiniAssetComponent* ParentHAC = Cast(PDGAssetLink->GetOuter()); + UHoudiniCookable* ParentHC = Cast(PDGAssetLink->GetOuter()); + EHoudiniAssetState ParentState = ParentHAC ? ParentHAC->GetAssetState() : ParentHC ? ParentHC->GetCurrentState() : EHoudiniAssetState::None; + HAPI_NodeId ParentNodeId = ParentHAC ? ParentHAC->GetAssetId() : ParentHC ? ParentHC->GetNodeId() : -1; + + // If the PDG Asset link is inactive, indicate that our HDA must be instantiated + if (PDGAssetLink->LinkState == EPDGLinkState::Inactive) + { + if(!ParentHAC && !ParentHC) + { + // No valid parent, error! + PDGAssetLink->LinkState = EPDGLinkState::Error_Not_Linked; + HOUDINI_LOG_ERROR(TEXT("No valid Houdini Asset Component parent for PDG Asset Link!")); + } + else if (ParentState == EHoudiniAssetState::NeedInstantiation) + { + PDGAssetLink->LinkState = EPDGLinkState::Linking; + if (ParentHAC) + ParentHAC->SetAssetState(EHoudiniAssetState::PreInstantiation); + else if (ParentHC) + ParentHC->SetCurrentState(EHoudiniAssetState::PreInstantiation); + } + else + { + PDGAssetLink->LinkState = EPDGLinkState::Error_Not_Linked; + HOUDINI_LOG_ERROR(TEXT("Unable to link the PDG Asset link! Try to rebuild the HDA.")); + } + } + + if (PDGAssetLink->LinkState == EPDGLinkState::Linking) + { + return true; + } + + if (PDGAssetLink->LinkState != EPDGLinkState::Linked) + { + if (ParentNodeId < 0) + return false; + + if (!FHoudiniEngineUtils::IsHoudiniNodeValid((HAPI_NodeId)ParentNodeId)) + return false; + + PDGAssetLink->AssetID = ParentNodeId; + } + + if(!PopulateTOPNetworks(PDGAssetLink)) + { + PDGAssetLink->LinkState = EPDGLinkState::Error_Not_Linked; + HOUDINI_LOG_ERROR(TEXT("Failed to populte the PDG Asset Link.")); + return false; + } + + return true; +} + + +bool +FHoudiniPDGManager::PopulateTOPNetworks(UHoudiniPDGAssetLink* PDGAssetLink, bool bInZeroWorkItemTallys) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniPDGManager::PopulateTOPNetworks); + + // Find all TOP networks from linked HDA, as well as the TOP nodes within, and populate internal state. + if (!IsValid(PDGAssetLink)) + return false; + + // Get all the network nodes within the asset, recursively. + // We're getting all networks because TOP network SOPs aren't considered being of TOP network type, but SOP type + int32 NetworkNodeCount = 0; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), (HAPI_NodeId)PDGAssetLink->AssetID, + HAPI_NODETYPE_SOP | HAPI_NODETYPE_TOP, HAPI_NODEFLAGS_NETWORK | HAPI_NODEFLAGS_NON_BYPASS, true, &NetworkNodeCount), false); + + if (NetworkNodeCount <= 0) + return false; + + TArray AllNetworkNodeIDs; + AllNetworkNodeIDs.SetNum(NetworkNodeCount); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), (HAPI_NodeId)PDGAssetLink->AssetID, + AllNetworkNodeIDs.GetData(), NetworkNodeCount), false); + + // For each Network we found earlier, only add those with TOP child nodes + // Therefore guaranteeing that we only add TOP networks + TArray AllTOPNetworks; + for (const HAPI_NodeId& CurrentNodeId : AllNetworkNodeIDs) + { + if (CurrentNodeId < 0) + { + continue; + } + + HAPI_NodeInfo CurrentNodeInfo; + FHoudiniApi::NodeInfo_Init(&CurrentNodeInfo); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), CurrentNodeId, &CurrentNodeInfo)) + { + continue; + } + + + // Check that this TOP Net is not nested in another TOP Net... + // This can happen with ROP Geometry TOPs for example... + bool bIsNestedInTOPNet = false; + HAPI_NodeId CurrentParentId = CurrentNodeInfo.parentId; + while (CurrentParentId > 0) + { + if (AllNetworkNodeIDs.Contains(CurrentParentId)) + { + bIsNestedInTOPNet = true; + break; + } + + HAPI_NodeInfo ParentNodeInfo; + FHoudiniApi::NodeInfo_Init(&ParentNodeInfo); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), CurrentParentId, &ParentNodeInfo)) + { + break; + } + + // Get our parent's parent + CurrentParentId = ParentNodeInfo.parentId; + } + + // Skip nested TOP nets + if (bIsNestedInTOPNet) + continue; + + // Get the list of all non bypassed TOP nodes within the current network (ignoring schedulers) + int32 TOPNodeCount = 0; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), CurrentNodeId, + HAPI_NodeType::HAPI_NODETYPE_TOP, HAPI_NODEFLAGS_TOP_NONSCHEDULER | HAPI_NODEFLAGS_NON_BYPASS, true, &TOPNodeCount)) + { + continue; + } + + TArray AllTOPNodeIDs; + AllTOPNodeIDs.SetNum(TOPNodeCount); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), CurrentNodeId, AllTOPNodeIDs.GetData(), TOPNodeCount), false); + + // Skip networks without TOP nodes + if (AllTOPNodeIDs.Num() <= 0) + { + continue; + } + + // TODO: + // Apply the show and output filter on that node + bool bShow = true; + + // Get the node path + FString CurrentNodePath; + FHoudiniEngineUtils::HapiGetNodePath(CurrentNodeInfo.id, PDGAssetLink->AssetID, CurrentNodePath); + + // Get the node name + FString CurrentNodeName; + FHoudiniEngineString::ToFString(CurrentNodeInfo.nameSH, CurrentNodeName); + + UTOPNetwork* CurrentTOPNetwork = nullptr; + int32 FoundTOPNetIndex = INDEX_NONE; + UTOPNetwork* FoundTOPNet = PDGAssetLink->GetTOPNetworkByNodePath(CurrentNodeName, PDGAssetLink->AllTOPNetworks, FoundTOPNetIndex); + if (IsValid(FoundTOPNet)) + { + // Reuse the existing corresponding TOP NET + CurrentTOPNetwork = FoundTOPNet; + PDGAssetLink->AllTOPNetworks.RemoveAt(FoundTOPNetIndex); + } + else + { + // Create a new instance for the TOP NET + CurrentTOPNetwork = NewObject(PDGAssetLink, UTOPNetwork::StaticClass(), NAME_None, RF_Transactional); + } + + // Update the TOP NET + CurrentTOPNetwork->NodeId = CurrentNodeId; + CurrentTOPNetwork->NodeName = CurrentNodeName; + CurrentTOPNetwork->NodePath = CurrentNodePath; + CurrentTOPNetwork->ParentName = PDGAssetLink->AssetName; + CurrentTOPNetwork->bShowResults = bShow; + + // Only add network that have valid TOP Nodes + if (PopulateTOPNodes(AllTOPNodeIDs, CurrentTOPNetwork, PDGAssetLink, bInZeroWorkItemTallys)) + { + // See if we need to select a new TOP node + bool bReselectValidTOP = false; + if (CurrentTOPNetwork->SelectedTOPIndex < 0) + bReselectValidTOP = true; + else if (!CurrentTOPNetwork->AllTOPNodes.IsValidIndex(CurrentTOPNetwork->SelectedTOPIndex)) + bReselectValidTOP = true; + else if (!IsValid(CurrentTOPNetwork->AllTOPNodes[CurrentTOPNetwork->SelectedTOPIndex]) || + CurrentTOPNetwork->AllTOPNodes[CurrentTOPNetwork->SelectedTOPIndex]->bHidden) + bReselectValidTOP = true; + + if (bReselectValidTOP) + { + // Select the first valid TOP node (not hidden) + for (int Idx = 0; Idx < CurrentTOPNetwork->AllTOPNodes.Num(); Idx++) + { + UTOPNode *TOPNode = CurrentTOPNetwork->AllTOPNodes[Idx]; + if (!IsValid(TOPNode) || TOPNode->bHidden) + continue; + + CurrentTOPNetwork->SelectedTOPIndex = Idx; + break; + } + } + + AllTOPNetworks.Add(CurrentTOPNetwork); + } + } + + // Clear previous TOP networks, nodes and generated data + for (UTOPNetwork* CurrentTOPNet : PDGAssetLink->AllTOPNetworks) + { + if (!IsValid(CurrentTOPNet)) + continue; + + PDGAssetLink->ClearTOPNetworkWorkItemResults(CurrentTOPNet); + } + //PDGAssetLink->ClearAllTOPData(); + PDGAssetLink->AllTOPNetworks = AllTOPNetworks; + + return (AllTOPNetworks.Num() > 0); +} + + +bool +FHoudiniPDGManager::PopulateTOPNodes( + const TArray& InTopNodeIDs, UTOPNetwork* InTOPNetwork, UHoudiniPDGAssetLink* InPDGAssetLink, bool bInZeroWorkItemTallys) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniPDGManager::PopulateTOPNodes); + if (!IsValid(InPDGAssetLink)) + return false; + + if (!IsValid(InTOPNetwork)) + return false; + + // + int32 TOPNodeCount = 0; + + // Holds list of found TOP nodes + TArray AllTOPNodes; + for(const HAPI_NodeId& CurrentTOPNodeID : InTopNodeIDs) + { + HAPI_NodeInfo CurrentNodeInfo; + FHoudiniApi::NodeInfo_Init(&CurrentNodeInfo); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), CurrentTOPNodeID, &CurrentNodeInfo)) + { + continue; + } + + // Increase the number of valid TOP Node + // (before applying the node filter) + TOPNodeCount++; + + // Get the node path + FString NodePath; + FHoudiniEngineUtils::HapiGetNodePath(CurrentNodeInfo.id, InTOPNetwork->NodeId, NodePath); + + // Get the node name + FString NodeName; + FHoudiniEngineString::ToFString(CurrentNodeInfo.nameSH, NodeName); + + // See if we can find an existing version of this TOPNOde + UTOPNode* CurrentTOPNode = nullptr; + int32 FoundNodeIndex = INDEX_NONE; + UTOPNode* FoundNode = InPDGAssetLink->GetTOPNodeByNodePath(NodePath, InTOPNetwork->AllTOPNodes, FoundNodeIndex); + if (IsValid(FoundNode)) + { + CurrentTOPNode = FoundNode; + InTOPNetwork->AllTOPNodes.RemoveAt(FoundNodeIndex); + + if (bInZeroWorkItemTallys) + { + CurrentTOPNode->ZeroWorkItemTally(); + CurrentTOPNode->NodeState = EPDGNodeState::None; + } + } + else + { + // Didn't find an existing UTOPNode for this node, create a new UTOPNode instance + CurrentTOPNode = NewObject(InPDGAssetLink, UTOPNode::StaticClass(), NAME_None, RF_Transactional); + } + + CurrentTOPNode->NodeId = CurrentTOPNodeID; + CurrentTOPNode->NodeName = NodeName; + CurrentTOPNode->NodePath = NodePath; + CurrentTOPNode->ParentName = InTOPNetwork->ParentName + TEXT("_") + InTOPNetwork->NodeName; + CurrentTOPNode->bHasChildNodes = CurrentNodeInfo.childNodeCount > 0; + + // Filter display/autoload using name + CurrentTOPNode->bHidden = false; + if (InPDGAssetLink->bUseTOPNodeFilter && !InPDGAssetLink->TOPNodeFilter.IsEmpty()) + { + // Only display nodes that matches the filter + if (!NodeName.StartsWith(InPDGAssetLink->TOPNodeFilter)) + CurrentTOPNode->bHidden = true; + } + + // Automatically load results for nodes that match the filter + if (InPDGAssetLink->bUseTOPOutputFilter) + { + bool bAutoLoad = false; + if (InPDGAssetLink->TOPOutputFilter.IsEmpty()) + bAutoLoad = true; + else if (NodeName.StartsWith(InPDGAssetLink->TOPOutputFilter)) + bAutoLoad = true; + + CurrentTOPNode->bAutoLoad = bAutoLoad; + + // Show autoloaded results by default + CurrentTOPNode->SetVisibleInLevel(bAutoLoad); + } + + AllTOPNodes.Add(CurrentTOPNode); + } + + for (UTOPNode* CurTOPNode : InTOPNetwork->AllTOPNodes) + { + if (!IsValid(CurTOPNode)) + continue; + + UHoudiniPDGAssetLink::ClearTOPNodeWorkItemResults(CurTOPNode); + } + + InTOPNetwork->AllTOPNodes = AllTOPNodes; + + return (TOPNodeCount > 0); +} + + +void +FHoudiniPDGManager::DirtyTOPNode(UTOPNode* InTOPNode) +{ + if (!IsValid(InTOPNode)) + return; + + // Dirty the specified TOP node... + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DirtyPDGNode( + FHoudiniEngine::Get().GetSession(), InTOPNode->NodeId, true)) + { + HOUDINI_LOG_ERROR(TEXT("PDG Dirty TOP Node - Failed to dirty %s!"), *(InTOPNode->NodeName)); + } + + // ... and clear its work item results. + UHoudiniPDGAssetLink::ClearTOPNodeWorkItemResults(InTOPNode); +} + +// void +// FHoudiniPDGManager::DirtyAllTasksOfTOPNode(FTOPNode& InTOPNode) +// { +// // Dirty the specified TOP node... +// if (HAPI_RESULT_SUCCESS != FHoudiniApi::DirtyPDGNode( +// FHoudiniEngine::Get().GetSession(), InTOPNode.NodeId, true)) +// { +// HOUDINI_LOG_ERROR(TEXT("PDG Dirty TOP Node - Failed to dirty %s!"), *InTOPNode.NodeName); +// } +// } + +bool +FHoudiniPDGManager::CookTOPNode(UTOPNode* InTOPNode) +{ + if (!IsValid(InTOPNode)) + return false; + + if (!FHoudiniEngine::Get().GetSession()) + return false; + + if (InTOPNode->NodeState == EPDGNodeState::Cooking || InTOPNode->AnyWorkItemsPending()) + { + HOUDINI_LOG_WARNING(TEXT("PDG Cook TOP Node - %s is already/still cooking, ignoring 'Cook TOP Node' request."), *(InTOPNode->NodePath)); + return false; + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::CookPDG( + FHoudiniEngine::Get().GetSession(), InTOPNode->NodeId, 0, 0)) + { + HOUDINI_LOG_ERROR(TEXT("PDG Cook TOP Node - Failed to cook %s!"), *(InTOPNode->NodeName)); + return false; + } + + return true; +} + + +void +FHoudiniPDGManager::DirtyAll(UTOPNetwork* InTOPNet) +{ + if (!IsValid(InTOPNet)) + return; + + // Dirty the specified TOP network... + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DirtyPDGNode( + FHoudiniEngine::Get().GetSession(), InTOPNet->NodeId, true)) + { + HOUDINI_LOG_ERROR(TEXT("PDG Dirty All - Failed to dirty all of %s's TOP nodes!"), *(InTOPNet->NodeName)); + return; + } + + // ... and clear its work item results. + UHoudiniPDGAssetLink::ClearTOPNetworkWorkItemResults(InTOPNet); +} + + +bool +FHoudiniPDGManager::CookOutput(UTOPNetwork* InTOPNet) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniPDGManager::CookOutput); + + // Cook the output TOP node of the currently selected TOP network. + //WorkItemTally.ZeroAll(); + //UHoudiniPDGAssetLink::ResetTOPNetworkWorkItemTally(InTOPNet); + + if (!IsValid(InTOPNet)) + return false; + + if (!FHoudiniEngine::Get().GetSession()) + return false; + + bool bAlreadyCooking = InTOPNet->AnyWorkItemsPending(); + + if (!bAlreadyCooking) + { + HAPI_PDG_GraphContextId GraphContextId = -1; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetPDGGraphContextId( + FHoudiniEngine::Get().GetSession(), InTOPNet->NodeId, &GraphContextId)) + { + HOUDINI_LOG_ERROR(TEXT("PDG Cook Output - Failed to get %s's graph context ID!"), *(InTOPNet->NodeName)); + return false; + } + + int32 PDGState = -1; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetPDGState( + FHoudiniEngine::Get().GetSession(), GraphContextId, &PDGState)) + { + HOUDINI_LOG_ERROR(TEXT("PDG Cook Output - Failed to get %s's PDG state."), *(InTOPNet->NodeName)); + return false; + } + bAlreadyCooking = ((HAPI_PDG_State) PDGState == HAPI_PDG_STATE_COOKING); + } + + if (bAlreadyCooking) + { + HOUDINI_LOG_WARNING(TEXT("PDG Cook Output - %s is already/still cooking, ignoring 'Cook Output' request."), *(InTOPNet->NodeName)); + return false; + } + + // TODO: ??? + // Cancel all cooks. This is required as otherwise the graph gets into an infinite cook state (bug?) + if(HAPI_RESULT_SUCCESS != FHoudiniApi::CookPDGAllOutputs( + FHoudiniEngine::Get().GetSession(), InTOPNet->NodeId, 0, 0)) + { + HOUDINI_LOG_ERROR(TEXT("PDG Cook Output - Failed to cook %s's output!"), *(InTOPNet->NodeName)); + return false; + } + + return true; +} + + +void +FHoudiniPDGManager::PauseCook(UTOPNetwork* InTOPNet) +{ + // Pause the PDG cook of the currently selected TOP network + //WorkItemTally.ZeroAll(); + //UHoudiniPDGAssetLink::ResetTOPNetworkWorkItemTally(InTOPNet); + + if (!IsValid(InTOPNet)) + return; + + if (!FHoudiniEngine::Get().GetSession()) + return; + + HAPI_PDG_GraphContextId GraphContextId = -1; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetPDGGraphContextId( + FHoudiniEngine::Get().GetSession(), InTOPNet->NodeId, &GraphContextId)) + { + HOUDINI_LOG_ERROR(TEXT("PDG Pause Cook - Failed to get %s's graph context ID!"), *(InTOPNet->NodeName)); + return; + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::PausePDGCook( + FHoudiniEngine::Get().GetSession(), GraphContextId)) + { + HOUDINI_LOG_ERROR(TEXT("PDG Pause Cook - Failed to pause %s!"), *(InTOPNet->NodeName)); + return; + } +} + + +void +FHoudiniPDGManager::CancelCook(UTOPNetwork* InTOPNet) +{ + // Cancel the PDG cook of the currently selected TOP network + //WorkItemTally.ZeroAll(); + //UHoudiniPDGAssetLink::ResetTOPNetworkWorkItemTally(InTOPNet); + + if (!IsValid(InTOPNet)) + return; + + if (!FHoudiniEngine::Get().GetSession()) + return; + + HAPI_PDG_GraphContextId GraphContextId = -1; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetPDGGraphContextId( + FHoudiniEngine::Get().GetSession(), InTOPNet->NodeId, &GraphContextId)) + { + HOUDINI_LOG_ERROR(TEXT("PDG Cancel Cook - Failed to get %s's graph context ID!"), *(InTOPNet->NodeName)); + return; + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::CancelPDGCook( + FHoudiniEngine::Get().GetSession(), GraphContextId)) + { + HOUDINI_LOG_ERROR(TEXT("PDG Cancel Cook - Failed to cancel cook for %s!"), *(InTOPNet->NodeName)); + return; + } +} + +void +FHoudiniPDGManager::Update() +{ + // Clean up registered PDG Asset Links + for(int32 Idx = PDGAssetLinks.Num() - 1; Idx >= 0; Idx--) + { + TWeakObjectPtr Ptr = PDGAssetLinks[Idx]; + if (!Ptr.IsValid() || Ptr.IsStale()) + { + PDGAssetLinks.RemoveAt(Idx); + continue; + } + + UHoudiniPDGAssetLink* CurPDGAssetLink = PDGAssetLinks[Idx].Get(); + if (!IsValid(CurPDGAssetLink)) + { + PDGAssetLinks.RemoveAt(Idx); + continue; + } + + // Remove PDG AL that dont have a valid/registered cookable + UHoudiniCookable* OuterHC = CurPDGAssetLink->GetOuterHoudiniCookable(); + if (!IsValid(OuterHC) ||!FHoudiniEngineRuntime::Get().IsCookableRegistered(OuterHC)) + { + PDGAssetLinks.RemoveAt(Idx); + continue; + } + } + + // Do nothing if we dont have any valid PDG asset Link + if (PDGAssetLinks.Num() <= 0) + return; + + // Update the PDG contexts and handle all pdg events and work item status updates + UpdatePDGContexts(); + + // Prcoess any workitem result if we have any + ProcessWorkItemResults(); +} + +// Query all the PDG graph context in the current Houdini Engine session. +// Handle PDG events, work item status updates. +// Forward relevant events to PDGAssetLink objects. +void +FHoudiniPDGManager::UpdatePDGContexts() +{ + // Get current PDG graph contexts + ReinitializePDGContext(); + + // Process next set of events for each graph context + if (PDGContextIDs.Num() > 0) + { + // Only initialize event array if not valid, or user resized max size + if(PDGEventInfos.Num() != MaxNumberOfPDGEvents) + PDGEventInfos.SetNum(MaxNumberOfPDGEvents); + + // TODO: member? + //HAPI_PDG_State PDGState; + for(const HAPI_PDG_GraphContextId& CurrentContextID : PDGContextIDs) + { + /* + // TODO: No need to reset events at each tick + int32 PDGStateInt; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetPDGState( + FHoudiniEngine::Get().GetSession(), CurrentContextID, &PDGStateInt)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to get PDG state")); + continue; + } + + PDGState = (HAPI_PDG_State)PDGStateInt; + + for (int32 Idx = 0; Idx < PDGEventInfos.Num(); Idx++) + { + ResetPDGEventInfo(PDGEventInfos[Idx]); + } + */ + + int32 PDGEventCount = 0; + int32 RemainingPDGEventCount = 0; + + HAPI_Result Result = FHoudiniApi::GetPDGEvents(FHoudiniEngine::Get().GetSession(), + CurrentContextID, PDGEventInfos.GetData(), MaxNumberOfPDGEvents, &PDGEventCount, &RemainingPDGEventCount); + + if (Result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_ERROR(TEXT("Failed to get PDG events, error code: %d"), Result); + continue; + } + + if (PDGEventCount < 1) + continue; + + for (int32 EventIdx = 0; EventIdx < PDGEventCount; EventIdx++) + { + ProcessPDGEvent(CurrentContextID, PDGEventInfos[EventIdx]); + } + + HOUDINI_LOG_MESSAGE(TEXT("PDG: Tick processed %d events, %d remaining."), PDGEventCount, RemainingPDGEventCount); + } + } + + for (auto CurAssetLink : PDGAssetLinks) + { + UHoudiniPDGAssetLink* const AssetLink = CurAssetLink.Get(); + if (!IsValid(AssetLink)) + continue; + + // Check if the post bake delegate should be fired + AssetLink->BroadcastPostAutoBakeDelegateIfRequired(); + + // Refresh UI if necessary, otherwise just make sure the work item tally is up to date + if (AssetLink->bNeedsUIRefresh) + { + FHoudiniPDGManager::RefreshPDGAssetLinkUI(AssetLink); + AssetLink->bNeedsUIRefresh = false; + } + else + { + AssetLink->UpdateWorkItemTally(); + } + } +} + +// Query the currently active PDG graph contexts in the Houdini Engine session. +// Should be done each time to get latest set of graph contexts. +void +FHoudiniPDGManager::ReinitializePDGContext() +{ + int32 NumContexts = 0; + FHoudiniApi::GetPDGGraphContextsCount(FHoudiniEngine::Get().GetSession(), &NumContexts); + + if(NumContexts > 0) + { + PDGContextNames.SetNum(NumContexts); + PDGContextIDs.SetNum(NumContexts); + + if(HAPI_RESULT_SUCCESS != FHoudiniApi::GetPDGGraphContexts( + FHoudiniEngine::Get().GetSession(), + PDGContextNames.GetData(), PDGContextIDs.GetData(), 0, NumContexts)) + { + PDGContextNames.SetNum(0); + PDGContextIDs.SetNum(0); + } + } + else + { + PDGContextNames.SetNum(0); + PDGContextIDs.SetNum(0); + } +} + +// Process a PDG event. Notify the relevant PDGAssetLink object. +void +FHoudiniPDGManager::ProcessPDGEvent(const HAPI_PDG_GraphContextId& InContextID, HAPI_PDG_EventInfo& EventInfo) +{ + UHoudiniPDGAssetLink* PDGAssetLink = nullptr; + UTOPNetwork* TOPNetwork = nullptr; + UTOPNode* TOPNode = nullptr; + + HAPI_PDG_EventType EventType = (HAPI_PDG_EventType)EventInfo.eventType; + HAPI_PDG_WorkItemState CurrentWorkItemState = (HAPI_PDG_WorkItemState)EventInfo.currentState; + HAPI_PDG_WorkItemState LastWorkItemState = (HAPI_PDG_WorkItemState)EventInfo.lastState; + + // Debug: get the HAPI_PDG_EventType as a string + const FString EventName = FHoudiniEngineUtils::HapiGetEventTypeAsString(EventType); + const FString CurrentWorkItemStateName = FHoudiniEngineUtils::HapiGetWorkItemStateAsString(CurrentWorkItemState); + const FString LastWorkItemStateName = FHoudiniEngineUtils::HapiGetWorkItemStateAsString(LastWorkItemState); + + if(!GetTOPAssetLinkNetworkAndNode(EventInfo.nodeId, PDGAssetLink, TOPNetwork, TOPNode) + || !IsValid(PDGAssetLink) || !IsValid(TOPNetwork) || !IsValid(TOPNode)) + { + // Prevent PDG warning spam + if ((EventInfo.workItemId != -1) && (EventInfo.nodeId != -1)) + HOUDINI_LOG_WARNING(TEXT("[ProcessPDGEvent]: Could not find matching TOPNode for event %s, workitem id %d, node id %d"), *EventName, EventInfo.workItemId, EventInfo.nodeId); + + return; + } + + HOUDINI_PDG_MESSAGE( + TEXT("[ProcessPDGEvent]: TOPNode: %s, WorkItem ID: %d, Event Type: %s, Current State: %s, Last State %s"), + *(TOPNode->NodePath), EventInfo.workItemId, *EventName, *CurrentWorkItemStateName, *LastWorkItemStateName); + + FLinearColor MsgColor = FLinearColor::White; + + bool bUpdatePDGNodeState = false; + switch (EventType) + { + case HAPI_PDG_EVENT_NULL: + SetTOPNodePDGState(PDGAssetLink, TOPNode, EPDGNodeState::None); + break; + + case HAPI_PDG_EVENT_NODE_CLEAR: + NotifyTOPNodePDGStateClear(PDGAssetLink, TOPNode); + break; + + case HAPI_PDG_EVENT_WORKITEM_ADD: + CreateOrRelinkWorkItem(TOPNode, InContextID, EventInfo.workItemId); + bUpdatePDGNodeState = true; + NotifyTOPNodeCreatedWorkItem(PDGAssetLink, TOPNode, EventInfo.workItemId); + break; + + case HAPI_PDG_EVENT_WORKITEM_REMOVE: + RemoveWorkItem(PDGAssetLink, EventInfo.workItemId, TOPNode); + bUpdatePDGNodeState = true; + NotifyTOPNodeRemovedWorkItem(PDGAssetLink, TOPNode, EventInfo.workItemId); + break; + + case HAPI_PDG_EVENT_COOK_WARNING: + MsgColor = FLinearColor::Yellow; + break; + + case HAPI_PDG_EVENT_COOK_ERROR: + MsgColor = FLinearColor::Red; + break; + + case HAPI_PDG_EVENT_COOK_COMPLETE: + SetTOPNodePDGState(PDGAssetLink, TOPNode, EPDGNodeState::Cook_Complete); + TOPNode->HandleOnPDGEventCookComplete(); + TOPNetwork->HandleOnPDGEventCookCompleteReceivedByChildNode(PDGAssetLink, TOPNode); + break; + + case HAPI_PDG_EVENT_DIRTY_START: + SetTOPNodePDGState(PDGAssetLink, TOPNode, EPDGNodeState::Dirtying); + break; + + case HAPI_PDG_EVENT_DIRTY_STOP: + SetTOPNodePDGState(PDGAssetLink, TOPNode, EPDGNodeState::Dirtied); + break; + + case HAPI_PDG_EVENT_WORKITEM_STATE_CHANGE: + { + // Last states + bUpdatePDGNodeState = true; + if (LastWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_WAITING && CurrentWorkItemState != HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_WAITING) + { + } + else if (LastWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_COOKING && CurrentWorkItemState != HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_COOKING) + { + } + else if (LastWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_SCHEDULED && CurrentWorkItemState != HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_SCHEDULED) + { + } + else if ( (LastWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_COOKED_CACHE || LastWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_COOKED_SUCCESS) + && CurrentWorkItemState != HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_COOKED_CACHE && CurrentWorkItemState != HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_COOKED_SUCCESS) + { + // Handled previously cooked WI + } + else if (LastWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_COOKED_FAIL && CurrentWorkItemState != HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_COOKED_FAIL) + { + } + else + { + // TODO: + // unhandled state change + HOUDINI_PDG_WARNING(TEXT("Unhandled PDG state change! Node: %s, WorkItemID %d"), IsValid(TOPNode) ? *TOPNode->NodePath : TEXT(""), EventInfo.workItemId); + } + + if (LastWorkItemState == CurrentWorkItemState) + { + // TODO: + // Not a change!! shouldnt happen! + HOUDINI_PDG_WARNING(TEXT("Last state == current state! Node: %s, WorkItemID %d, state %d"), IsValid(TOPNode) ? *TOPNode->NodePath : TEXT(""), EventInfo.workItemId, EventInfo.lastState); + } + + // New states + if (CurrentWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_WAITING) + { + NotifyTOPNodeWaitingWorkItem(PDGAssetLink, TOPNode, EventInfo.workItemId); + } + else if (CurrentWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_UNCOOKED) + { + + } + else if (CurrentWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_DIRTY) + { + // ClearWorkItemResult(InContextID, EventInfo, *TOPNode); + ClearWorkItemResult(PDGAssetLink, EventInfo.workItemId, TOPNode); + // RemoveWorkItem(PDGAssetLink, EventInfo.workitemId, *TOPNode); + } + else if (CurrentWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_SCHEDULED) + { + NotifyTOPNodeScheduledWorkItem(PDGAssetLink, TOPNode, EventInfo.workItemId); + } + else if (CurrentWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_COOKING) + { + NotifyTOPNodeCookingWorkItem(PDGAssetLink, TOPNode, EventInfo.workItemId); + } + else if (CurrentWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_COOKED_SUCCESS + || CurrentWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_COOKED_CACHE) + { + NotifyTOPNodeCookedWorkItem(PDGAssetLink, TOPNode, EventInfo.workItemId); + + // On cook success, handle results + CreateOrRelinkWorkItemResult(TOPNode, InContextID, EventInfo.workItemId, TOPNode->bAutoLoad); + } + else if (CurrentWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_COOKED_FAIL) + { + // TODO: on cook failure, get log path? + NotifyTOPNodeErrorWorkItem(PDGAssetLink, TOPNode, EventInfo.workItemId); + MsgColor = FLinearColor::Red; + } + else if (CurrentWorkItemState == HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_COOKED_CANCEL) + { + NotifyTOPNodeCookCancelledWorkItem(PDGAssetLink, TOPNode, EventInfo.workItemId); + } + } + break; + + case HAPI_PDG_EVENT_COOK_START: + TOPNode->HandleOnPDGEventCookStart(); + break; + // Unhandled events + case HAPI_PDG_EVENT_DIRTY_ALL: + case HAPI_PDG_EVENT_WORKITEM_ADD_DEP: + case HAPI_PDG_EVENT_WORKITEM_REMOVE_DEP: + case HAPI_PDG_EVENT_WORKITEM_ADD_PARENT: + case HAPI_PDG_EVENT_WORKITEM_REMOVE_PARENT: + case HAPI_PDG_EVENT_UI_SELECT: + case HAPI_PDG_EVENT_NODE_CREATE: + case HAPI_PDG_EVENT_NODE_REMOVE: + case HAPI_PDG_EVENT_NODE_RENAME: + case HAPI_PDG_EVENT_NODE_CONNECT: + case HAPI_PDG_EVENT_NODE_DISCONNECT: + case HAPI_PDG_EVENT_WORKITEM_SET_INT: // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_SET_FLOAT: // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_SET_STRING: // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_SET_FILE: // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_SET_PYOBJECT: // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_SET_GEOMETRY: // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_RESULT: + case HAPI_PDG_EVENT_WORKITEM_PRIORITY: // DEPRECATED + case HAPI_PDG_EVENT_WORKITEM_ADD_STATIC_ANCESTOR: + case HAPI_PDG_EVENT_WORKITEM_REMOVE_STATIC_ANCESTOR: + case HAPI_PDG_EVENT_NODE_PROGRESS_UPDATE: + case HAPI_PDG_EVENT_ALL: + case HAPI_PDG_EVENT_LOG: + case HAPI_PDG_CONTEXT_EVENTS: + break; + } + + if (bUpdatePDGNodeState) + { + // Work item events + EPDGNodeState CurrentTOPNodeState = TOPNode->NodeState; + if (CurrentTOPNodeState == EPDGNodeState::Cooking) + { + if (TOPNode->AreAllWorkItemsComplete()) + { + // At the end of a node/net cook, ensure that the work items are in sync with HAPI and remove any + // work items with invalid ids or that don't exist on the HAPI side anymore. + SyncAndPruneWorkItems(TOPNode); + // Check that all work items are still complete after the sync + if (TOPNode->AreAllWorkItemsComplete()) + { + if (TOPNode->AnyWorkItemsFailed()) + { + SetTOPNodePDGState(PDGAssetLink, TOPNode, EPDGNodeState::Cook_Failed); + } + else + { + SetTOPNodePDGState(PDGAssetLink, TOPNode, EPDGNodeState::Cook_Complete); + } + } + } + } + else if (TOPNode->AnyWorkItemsPending()) + { + SetTOPNodePDGState(PDGAssetLink, TOPNode, EPDGNodeState::Cooking); + } + } + + if (EventInfo.msgSH >= 0) + { + FString EventMsg; + FHoudiniEngineString::ToFString(EventInfo.msgSH, EventMsg); + if (!EventMsg.IsEmpty()) + { + // TODO: Event MSG? + // Somehow update the PDG event msg UI ?? + // Simply log for now... + if (MsgColor == FLinearColor::Red) + { + HOUDINI_LOG_ERROR(TEXT("%s"), *EventMsg); + } + else if (MsgColor == FLinearColor::Yellow) + { + HOUDINI_LOG_WARNING(TEXT("%s"), *EventMsg); + } + else + { + HOUDINI_LOG_MESSAGE(TEXT("%s"), *EventMsg); + } + } + } +} + +void +FHoudiniPDGManager::ResetPDGEventInfo(HAPI_PDG_EventInfo& InEventInfo) +{ + InEventInfo.nodeId = -1; + InEventInfo.workItemId = -1; + InEventInfo.dependencyId = -1; + InEventInfo.currentState = HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_UNDEFINED; + InEventInfo.lastState = HAPI_PDG_WorkItemState::HAPI_PDG_WORKITEM_UNDEFINED; + InEventInfo.eventType = HAPI_PDG_EventType::HAPI_PDG_EVENT_NULL; +} + + +bool +FHoudiniPDGManager::GetTOPAssetLinkNetworkAndNode( + const HAPI_NodeId& InNodeID, UHoudiniPDGAssetLink*& OutAssetLink, UTOPNetwork*& OutTOPNetwork, UTOPNode*& OutTOPNode) +{ + // Returns the PDGAssetLink and FTOPNode data associated with this TOP node ID + OutAssetLink = nullptr; + OutTOPNetwork = nullptr; + OutTOPNode = nullptr; + for (TWeakObjectPtr& CurAssetLinkPtr : PDGAssetLinks) + { + if (!CurAssetLinkPtr.IsValid() || CurAssetLinkPtr.IsStale()) + continue; + + UHoudiniPDGAssetLink* CurAssetLink = CurAssetLinkPtr.Get(); + if (!IsValid(CurAssetLink)) + continue; + + if (CurAssetLink->GetTOPNodeAndNetworkByNodeId((int32)InNodeID, OutTOPNetwork, OutTOPNode)) + { + if (OutTOPNetwork != nullptr && OutTOPNode != nullptr) + { + OutAssetLink = CurAssetLink; + return true; + } + } + } + + OutAssetLink = nullptr; + OutTOPNetwork = nullptr; + OutTOPNode = nullptr; + + return false; +} + +void +FHoudiniPDGManager::SetTOPNodePDGState(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const EPDGNodeState& InPDGState) +{ + if (!IsValid(InTOPNode)) + return; + + InTOPNode->NodeState = InPDGState; + + // InPDGAssetLink->bNeedsUIRefresh = true; + //FHoudiniPDGManager::RefreshPDGAssetLinkUI(InPDGAssetLink); +} + +void +FHoudiniPDGManager::NotifyTOPNodePDGStateClear(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode) +{ + if (!IsValid(InTOPNode)) + return; + + //Debug.LogFormat("NotifyTOPNodePDGStateClear:: {0}", topNode._nodeName); + InTOPNode->NodeState = EPDGNodeState::None; + InTOPNode->ZeroWorkItemTally(); + InTOPNode->OnDirtyNode(); + + HOUDINI_PDG_MESSAGE(TEXT("PDG: %s: WorkItemTally ZeroAll"), *(InTOPNode->NodePath)); + + // InPDGAssetLink->bNeedsUIRefresh = true; + //FHoudiniPDGManager::RefreshPDGAssetLinkUI(InPDGAssetLink); + +} + +void +FHoudiniPDGManager::NotifyTOPNodeCreatedWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID) +{ + if (!IsValid(InTOPNode)) + return; + + InTOPNode->OnWorkItemCreated(InWorkItemID); + + HOUDINI_PDG_MESSAGE(TEXT("PDG: %s: WorkItemTally Created WorkItem, Total %d"), *(InTOPNode->NodePath), InTOPNode->GetWorkItemTally().NumWorkItems()); + + // InPDGAssetLink->bNeedsUIRefresh = true; + //FHoudiniPDGManager::RefreshPDGAssetLinkUI(InPDGAssetLink); +} + +void +FHoudiniPDGManager::NotifyTOPNodeRemovedWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID) +{ + if (!IsValid(InTOPNode)) + return; + + InTOPNode->OnWorkItemRemoved(InWorkItemID); + + HOUDINI_PDG_MESSAGE(TEXT("PDG: %s: WorkItemTally Removed WorkItem, Total %d"), *(InTOPNode->NodePath), InTOPNode->GetWorkItemTally().NumWorkItems()); + + // InPDGAssetLink->bNeedsUIRefresh = true; + //FHoudiniPDGManager::RefreshPDGAssetLinkUI(InPDGAssetLink); +} + +void +FHoudiniPDGManager::NotifyTOPNodeCookedWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID) +{ + if (!IsValid(InTOPNode)) + return; + + InTOPNode->OnWorkItemCooked(InWorkItemID); + + HOUDINI_PDG_MESSAGE(TEXT("PDG: %s: WorkItemTally CookedWorkItems Total %d"), *(InTOPNode->NodePath), InTOPNode->GetWorkItemTally().NumCookedWorkItems()); + + // InPDGAssetLink->bNeedsUIRefresh = true; + //FHoudiniPDGManager::RefreshPDGAssetLinkUI(InPDGAssetLink); +} + +void +FHoudiniPDGManager::NotifyTOPNodeErrorWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID) +{ + if (!IsValid(InTOPNode)) + return; + + InTOPNode->OnWorkItemErrored(InWorkItemID); + + HOUDINI_PDG_MESSAGE(TEXT("PDG: %s: WorkItemTally ErroredWorkItems Total %d"), *(InTOPNode->NodePath), InTOPNode->GetWorkItemTally().NumErroredWorkItems()); + + // InPDGAssetLink->bNeedsUIRefresh = true; + //FHoudiniPDGManager::RefreshPDGAssetLinkUI(InPDGAssetLink); +} + +void +FHoudiniPDGManager::NotifyTOPNodeWaitingWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID) +{ + if (!IsValid(InTOPNode)) + return; + + InTOPNode->OnWorkItemWaiting(InWorkItemID); + + HOUDINI_PDG_MESSAGE(TEXT("PDG: %s: WorkItemTally WaitingWorkItems Total %d"), *(InTOPNode->NodePath), InTOPNode->GetWorkItemTally().NumErroredWorkItems()); + + // InPDGAssetLink->bNeedsUIRefresh = true; + //FHoudiniPDGManager::RefreshPDGAssetLinkUI(InPDGAssetLink); +} + +void +FHoudiniPDGManager::NotifyTOPNodeScheduledWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID) +{ + if (!IsValid(InTOPNode)) + return; + + InTOPNode->OnWorkItemScheduled(InWorkItemID); + + HOUDINI_PDG_MESSAGE(TEXT("PDG: %s: WorkItemTally ScheduledWorkItems Total %d"), *(InTOPNode->NodePath), InTOPNode->GetWorkItemTally().NumScheduledWorkItems()); + + // InPDGAssetLink->bNeedsUIRefresh = true; + //FHoudiniPDGManager::RefreshPDGAssetLinkUI(InPDGAssetLink); +} + +void +FHoudiniPDGManager::NotifyTOPNodeCookingWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID) +{ + if (!IsValid(InTOPNode)) + return; + + InTOPNode->OnWorkItemCooking(InWorkItemID); + + HOUDINI_PDG_MESSAGE(TEXT("PDG: %s: WorkItemTally CookingWorkItems Total %d"), *(InTOPNode->NodePath), InTOPNode->GetWorkItemTally().NumCookingWorkItems()); + + // InPDGAssetLink->bNeedsUIRefresh = true; + //FHoudiniPDGManager::RefreshPDGAssetLinkUI(InPDGAssetLink); +} + +void +FHoudiniPDGManager::NotifyTOPNodeCookCancelledWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID) +{ + if (!IsValid(InTOPNode)) + return; + + InTOPNode->OnWorkItemCookCancelled(InWorkItemID); + + HOUDINI_PDG_MESSAGE(TEXT("PDG: %s: WorkItemTally CookCancelledWorkItems Total %d"), *(InTOPNode->NodePath), InTOPNode->GetWorkItemTally().NumCookCancelledWorkItems()); +} + +void +FHoudiniPDGManager::ClearWorkItemResult(UHoudiniPDGAssetLink* InAssetLink, const HAPI_PDG_WorkItemId& InWorkItemID, UTOPNode* InTOPNode) +{ + if (!IsValid(InAssetLink)) + return; + + // TODO!!! + // Clear all work items' results for the specified TOP node. + // This destroys any loaded results (geometry etc). + //session.LogErrorOverride = false; + InAssetLink->ClearWorkItemResultByID(InWorkItemID, InTOPNode); + // session.LogErrorOverride = true; +} + +void +FHoudiniPDGManager::RemoveWorkItem(UHoudiniPDGAssetLink* InAssetLink, const HAPI_PDG_WorkItemId& InWorkItemID, UTOPNode* InTOPNode) +{ + if (!IsValid(InAssetLink)) + return; + + // Clear all of the work item's results for the specified TOP node and also remove the work item itself from + // the TOP node. + InAssetLink->DestroyWorkItemByID(InWorkItemID, InTOPNode); +} + +void +FHoudiniPDGManager::RefreshPDGAssetLinkUI(UHoudiniPDGAssetLink* InAssetLink) +{ + if (!IsValid(InAssetLink)) + return; + + // Only update the editor properties if the PDG asset link's Actor is selected + // else, just update the workitemtally + InAssetLink->UpdateWorkItemTally(); + + UHoudiniAssetComponent* ParentHAC = Cast(InAssetLink->GetOuter()); + UHoudiniCookable* ParentHC = Cast(InAssetLink->GetOuter()); + + if (!IsValid(ParentHAC) && !IsValid(ParentHC)) + return; + + AActor* ActorOwner = ParentHC ? ParentHC->GetOwner() : ParentHAC->GetOwner(); + if (ActorOwner != nullptr && ActorOwner->IsSelected()) + { + FHoudiniEngineUtils::UpdateEditorProperties(true); + } +} + +void +FHoudiniPDGManager::NotifyAssetCooked(UHoudiniPDGAssetLink* InAssetLink, const bool& bSuccess) +{ + if (!IsValid(InAssetLink)) + return; + + if (bSuccess) + { + if (InAssetLink->LinkState == EPDGLinkState::Linked) + { + if (InAssetLink->bAutoCook) + { + FHoudiniPDGManager::CookOutput(InAssetLink->GetSelectedTOPNetwork()); + } + } + else + { + UpdatePDGAssetLink(InAssetLink); + } + } + else + { + InAssetLink->LinkState = EPDGLinkState::Error_Not_Linked; + } +} + +int32 +FHoudiniPDGManager::CreateOrRelinkWorkItem( + UTOPNode* InTOPNode, + const HAPI_PDG_GraphContextId& InContextID, + HAPI_PDG_WorkItemId InWorkItemID) +{ + if (!IsValid(InTOPNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to get work %d info: InTOPNode is null."), InWorkItemID); + return INDEX_NONE; + } + + HAPI_PDG_WorkItemInfo WorkItemInfo; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetWorkItemInfo( + FHoudiniEngine::Get().GetSession(), InContextID, InWorkItemID, &WorkItemInfo)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to get work item %d info for %s"), InWorkItemID, *(InTOPNode->NodeName)); + // TODO? continue? + return INDEX_NONE; + } + + // Try to find the existing WorkItem by ID. + int32 Index = InTOPNode->ArrayIndexOfWorkResultByID(InWorkItemID); + if (Index == INDEX_NONE) + { + // Try to find the first entry with WorkItemID == INDEX_NONE. The WorkItemIDs are + // transient, so not saved when the map / asset link is saved. So when loading a map containing the asset + // link all the IDs are INDEX_NONE and so we re-use any stale entries in array index order (should be reliable + // if work items generate in the same order. In the future we might have to consider adding support for a + // custom ID attribute for more stable re-linking of work items). + Index = InTOPNode->ArrayIndexOfFirstInvalidWorkResult(); + if (Index == INDEX_NONE) + { + // If we couldn't find a stale entry to re-use, create a new one + FTOPWorkResult LocalWorkResult; + LocalWorkResult.WorkItemID = InWorkItemID; + LocalWorkResult.WorkItemIndex = WorkItemInfo.index; + Index = InTOPNode->WorkResult.Add(LocalWorkResult); + } + else + { + // We found a stale entry, re-use it + FTOPWorkResult& ReUsedWorkResult = InTOPNode->WorkResult[Index]; + ReUsedWorkResult.WorkItemID = InWorkItemID; + ReUsedWorkResult.WorkItemIndex = WorkItemInfo.index; + } + } + + return Index; +} + +bool +FHoudiniPDGManager::CreateOrRelinkWorkItemResult( + UTOPNode* InTOPNode, + const HAPI_PDG_GraphContextId& InContextID, + HAPI_PDG_WorkItemId InWorkItemID, + bool bInLoadResultObjects) +{ + if (!IsValid(InTOPNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to get work %d info: InTOPNode is null."), InWorkItemID); + return false; + } + + HAPI_PDG_WorkItemInfo WorkItemInfo; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetWorkItemInfo( + FHoudiniEngine::Get().GetSession(), InContextID, InWorkItemID, &WorkItemInfo)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to get work item %d info for %s"), InWorkItemID, *(InTOPNode->NodeName)); + // TODO? continue? + return false; + } + + // Try to find the existing WorkResult by ID. + int32 WorkResultArrayIndex = InTOPNode->ArrayIndexOfWorkResultByID(InWorkItemID); + FTOPWorkResult* WorkResult = nullptr; + if (WorkResultArrayIndex != INDEX_NONE) + WorkResult = InTOPNode->GetWorkResultByArrayIndex(WorkResultArrayIndex); + if (!WorkResult) + { + // TODO: This shouldn't really happen, it means a work item finished cooking and generated a result before + // we received an event that the work item was added/generated. + WorkResultArrayIndex = CreateOrRelinkWorkItem(InTOPNode, InContextID, InWorkItemID); + if (WorkResultArrayIndex != INDEX_NONE) + { + WorkResult = InTOPNode->GetWorkResultByArrayIndex(WorkResultArrayIndex); + } + } + + if (!WorkResult) + { + HOUDINI_LOG_ERROR(TEXT("Failed to get or add a FTOPWorkResult for WorkItemID %d for %s"), InWorkItemID, *(InTOPNode->NodeName)); + return false; + } + + TArray NewResultObjects; + TSet ResultIndicesThatWereReused; + if (WorkItemInfo.outputFileCount > 0) + { + TArray OutputFiles; + OutputFiles.SetNum(WorkItemInfo.outputFileCount); + const int32 resultCount = WorkItemInfo.outputFileCount; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetWorkItemOutputFiles( + FHoudiniEngine::Get().GetSession(), + InTOPNode->NodeId, InWorkItemID, OutputFiles.GetData(), resultCount)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to get work item %d result info for %s"), InWorkItemID, *(InTOPNode->NodeName)); + // TODO? continue? + return false; + } + + FString WorkItemName; + FHoudiniEngineString::ToFString(WorkItemInfo.nameSH, WorkItemName); + + // Load each result geometry + const int32 NumResults = OutputFiles.Num(); + for (int32 Idx = 0; Idx < NumResults; Idx++) + { + const HAPI_PDG_WorkItemOutputFile& OutputFile = OutputFiles[Idx]; + if (OutputFile.tagSH <= 0 || OutputFile.filePathSH <= 0) + continue; + + FString CurrentTag; + FHoudiniEngineString::ToFString(OutputFile.tagSH, CurrentTag); + if(CurrentTag.IsEmpty() || !CurrentTag.StartsWith(TEXT("file"))) + continue; + + FString CurrentPath = FString(); + FHoudiniEngineString::ToFString(OutputFile.filePathSH, CurrentPath); + + // Construct the name and look for an existing work result, re-use it if found, otherwise create a new one + const FString WorkResultName = FString::Printf( + TEXT("%s_%s_%d_%d"), + *(InTOPNode->ParentName), + *WorkItemName, + WorkResultArrayIndex, + Idx); + + // int32 ExistingObjectIndex = WorkResult->ResultObjects.IndexOfByPredicate([WorkResultName](const FTOPWorkResultObject& InResultObject) + // { + // return InResultObject.Name == WorkResultName; + // }); + int32 ExistingObjectIndex = WorkResult->ResultObjects.IndexOfByPredicate([Idx](const FTOPWorkResultObject& InResultObject) + { + return InResultObject.WorkItemResultInfoIndex == Idx; + }); + if (WorkResult->ResultObjects.IsValidIndex(ExistingObjectIndex)) + { + FTOPWorkResultObject& ExistingResultObject = WorkResult->ResultObjects[ExistingObjectIndex]; + + ExistingResultObject.Name = WorkResultName; + ExistingResultObject.FilePath = CurrentPath; + ExistingResultObject.SetAutoBakedSinceLastLoad(false); + if (ExistingResultObject.State == EPDGWorkResultState::Loaded && !bInLoadResultObjects) + { + ExistingResultObject.State = EPDGWorkResultState::ToDelete; + } + else + { + // When the commandlet is not being used, we could leave the outputs in place and have + // translators try to re-use objects/components. When the commandlet is being used, the packages + // are always saved and standalone, so if we want to automatically clean up old results then we + // need to destroy the existing outputs + if (BGEOCommandletStatus == EHoudiniBGEOCommandletStatus::Connected) + { + constexpr bool bDeleteOutputActors = false; + InTOPNode->DeleteWorkResultObjectOutputs(WorkResultArrayIndex, ExistingObjectIndex, bDeleteOutputActors); + } + + if ((ExistingResultObject.State == EPDGWorkResultState::Loaded || + ExistingResultObject.State == EPDGWorkResultState::ToDelete || + ExistingResultObject.State == EPDGWorkResultState::Deleting) && bInLoadResultObjects) + { + ExistingResultObject.State = EPDGWorkResultState::ToLoad; + } + else + { + ExistingResultObject.State = bInLoadResultObjects ? EPDGWorkResultState::ToLoad : EPDGWorkResultState::NotLoaded; + } + } + + NewResultObjects.Add(ExistingResultObject); + ResultIndicesThatWereReused.Add(ExistingObjectIndex); + } + else + { + FTOPWorkResultObject ResultObj; + ResultObj.Name = WorkResultName; + ResultObj.FilePath = CurrentPath; + ResultObj.State = bInLoadResultObjects ? EPDGWorkResultState::ToLoad : EPDGWorkResultState::NotLoaded; + ResultObj.WorkItemResultInfoIndex = Idx; + ResultObj.SetAutoBakedSinceLastLoad(false); + + NewResultObjects.Add(ResultObj); + } + } + } + // Destroy any old ResultObjects that were not re-used + const int32 NumPrevResultObjects = WorkResult->ResultObjects.Num(); + for (int32 ResultObjectIndex = 0; ResultObjectIndex < NumPrevResultObjects; ++ResultObjectIndex) + { + if (ResultIndicesThatWereReused.Contains(ResultObjectIndex)) + continue; + InTOPNode->DeleteWorkResultObjectOutputs(WorkResultArrayIndex, ResultObjectIndex); + } + WorkResult->ResultObjects = NewResultObjects; + + return true; +} + +int32 +FHoudiniPDGManager::SyncAndPruneWorkItems(UTOPNode* InTOPNode) +{ + TSet WorkItemIDSet; + TArray WorkItemIDs; + int NumWorkItems = -1; + + if (!IsValid(InTOPNode)) + return -1; + + HAPI_Session const * const HAPISession = FHoudiniEngine::Get().GetSession(); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetNumWorkItems(HAPISession, InTOPNode->NodeId, &NumWorkItems)) + { + HOUDINI_LOG_WARNING(TEXT("GetNumWorkItems call failed on TOP Node %s (%d)"), *(InTOPNode->NodeName), InTOPNode->NodeId); + return -1; + } + + WorkItemIDs.SetNum(NumWorkItems); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetWorkItems(HAPISession, InTOPNode->NodeId, WorkItemIDs.GetData(), NumWorkItems)) + { + HOUDINI_LOG_WARNING(TEXT("GetWorkItems call failed on TOP Node %s (%d)"), *(InTOPNode->NodeName), InTOPNode->NodeId); + return -1; + } + + HAPI_PDG_GraphContextId ContextId; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetPDGGraphContextId(HAPISession, InTOPNode->NodeId, &ContextId)) + { + HOUDINI_LOG_WARNING(TEXT("GetPDGGraphContextId call failed on TOP Node %s (%d)"), *(InTOPNode->NodeName), InTOPNode->NodeId); + return -1; + } + + bool bChanged = false; + for (const HAPI_PDG_WorkItemId& WorkItemID : WorkItemIDs) + { + WorkItemIDSet.Add(static_cast(WorkItemID)); + + // If the WorkItemID is not present in FTOPWorkResult array, sync from HAPI + if (InTOPNode->GetWorkResultByID(WorkItemID) == nullptr) + { + CreateOrRelinkWorkItemResult(InTOPNode, ContextId, WorkItemID); + InTOPNode->OnWorkItemCreated(WorkItemID); + bChanged = true; + } + } + + // TODO: refactor functions that access the TOPNode's array and properties directly to rather be functions on the + // UTOPNode and make access to these arrays protected/private + + // Remove any work result entries with invalid IDs or where the WorkItemID is not in the set of ids returned by + // HAPI (only if we could get the IDs from HAPI). + const FGuid CookableGUID(InTOPNode->GetHoudiniCookableGuid()); + int32 NumRemoved = 0; + const int32 NumWorkItemsInArray = InTOPNode->WorkResult.Num(); + for (int32 Index = NumWorkItemsInArray - 1; Index >= 0; --Index) + { + FTOPWorkResult& WorkResult = InTOPNode->WorkResult[Index]; + if (WorkResult.WorkItemID == INDEX_NONE || !WorkItemIDSet.Contains(WorkResult.WorkItemID)) + { + HOUDINI_PDG_WARNING( + TEXT("Pruning a FTOPWorkResult entry from TOP Node %d, WorkItemID %d, WorkItemIndex %d, Array Index %d"), + InTOPNode->NodeId, WorkResult.WorkItemID, WorkResult.WorkItemIndex, Index); + WorkResult.ClearAndDestroyResultObjects(CookableGUID); + InTOPNode->WorkResult.RemoveAt(Index); + InTOPNode->OnWorkItemRemoved(WorkResult.WorkItemID); + NumRemoved++; + bChanged = true; + } + } + + if (bChanged) + { + // Ensure that the outer level (or actor in the case of OFPA) is marked as dirty so that references to the + // output actors / objects are saved + InTOPNode->MarkPackageDirty(); + } + + return NumRemoved; +} + +void +FHoudiniPDGManager::ProcessWorkItemResults() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniPDGManager::ProcessWorkItemResults); + + const EHoudiniBGEOCommandletStatus CommandletStatus = UpdateAndGetBGEOCommandletStatus(); + for (auto& CurrentPDGAssetLink : PDGAssetLinks) + { + // Iterate through all PDG Asset Link + UHoudiniPDGAssetLink* AssetLink = CurrentPDGAssetLink.Get(); + if (!AssetLink) + continue; + + // Set up package parameters to: + // Cook to temp houdini engine directory + // and if the PDG asset link is associated with a Houdini Asset Component (HAC): + // set the outer package to the HAC + // set the HoudiniAssetName according to the HAC + // set the ComponentGUID according to the HAC + // otherwise we set the outer to the asset link's parent and leave naming and GUID blank + FHoudiniPackageParams PackageParams; + PackageParams.PackageMode = FHoudiniPackageParams::GetDefaultStaticMeshesCookMode(); + PackageParams.ReplaceMode = FHoudiniPackageParams::GetDefaultReplaceMode(); + + PackageParams.BakeFolder = FHoudiniEngineRuntime::Get().GetDefaultBakeFolder(); + PackageParams.TempCookFolder = FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder(); + + UObject* AssetLinkParent = AssetLink->GetOuter(); + UHoudiniAssetComponent* HAC = AssetLinkParent != nullptr ? Cast(AssetLinkParent) : nullptr; + UHoudiniCookable* HC = AssetLinkParent != nullptr ? Cast(AssetLinkParent) : nullptr; + if (HC) + { + AActor* Owner = HC->GetOwner(); + PackageParams.OuterPackage = Owner ? Cast(Owner->GetLevel()) : Cast(HC->GetPackage()); + PackageParams.HoudiniAssetName = HC->GetHoudiniAssetName(); + PackageParams.HoudiniAssetActorName = Owner ? Owner->GetActorNameOrLabel() : HC->GetName(); + PackageParams.ComponentGUID = HC->GetCookableGUID(); + } + else if (HAC) + { + PackageParams.OuterPackage = HAC->GetComponentLevel(); + PackageParams.HoudiniAssetName = HAC->GetHoudiniAssetName(); + PackageParams.HoudiniAssetActorName = HAC->GetOwner()->GetActorNameOrLabel(); + PackageParams.ComponentGUID = HAC->GetComponentGUID(); + } + else + { + PackageParams.OuterPackage = AssetLinkParent ? AssetLinkParent->GetOutermost() : nullptr; + PackageParams.HoudiniAssetName = FString(); + PackageParams.HoudiniAssetActorName = FString(); + // PackageParams.ComponentGUID = HAC->GetComponentGUID(); + + // // Try to find a parent actor + // UObject* Parent = AssetLinkParent; + // while (Parent && !ParentActor) + // { + // ParentActor = Cast(Parent); + // if (!ParentActor) + // Parent = ParentActor->GetOuter(); + // } + } + PackageParams.ObjectName = FString(); + + // Static mesh generation / build settings, get it from the HAC if available, otherwise from the plugin + // defaults + const FHoudiniStaticMeshGenerationProperties& StaticMeshGenerationProperties = HAC ? HAC->GetStaticMeshGenerationProperties() : FHoudiniEngineRuntimeUtils::GetDefaultStaticMeshGenerationProperties(); + const FMeshBuildSettings& MeshBuildSettings = HAC ? HAC->GetStaticMeshBuildSettings() : FHoudiniEngineRuntimeUtils::GetDefaultMeshBuildSettings(); + + // .. All TOP Nets + for (UTOPNetwork* CurrentTOPNet : AssetLink->AllTOPNetworks) + { + if (!IsValid(CurrentTOPNet)) + continue; + + // .. All TOP Nodes + for (UTOPNode* CurrentTOPNode : CurrentTOPNet->AllTOPNodes) + { + if (!IsValid(CurrentTOPNode)) + continue; + + // ... All WorkResult + CurrentTOPNode->bCachedHaveNotLoadedWorkResults = false; + CurrentTOPNode->bCachedHaveLoadedWorkResults = false; + + const int32 NumWorkResults = CurrentTOPNode->WorkResult.Num(); + for (int32 WorkResultArrayIndex = 0; WorkResultArrayIndex < NumWorkResults; ++WorkResultArrayIndex) + // for (FTOPWorkResult& CurrentWorkResult : CurrentTOPNode->WorkResult) + { + FTOPWorkResult& CurrentWorkResult = CurrentTOPNode->WorkResult[WorkResultArrayIndex]; + // ... All WorkResultObjects + const int32 NumWorkResultObjects = CurrentWorkResult.ResultObjects.Num(); + for (int32 WorkResultObjectArrayIndex = 0; WorkResultObjectArrayIndex < NumWorkResultObjects; ++WorkResultObjectArrayIndex) + // for (FTOPWorkResultObject& CurrentWorkResultObj : CurrentWorkResult.ResultObjects) + { + FTOPWorkResultObject& CurrentWorkResultObj = CurrentWorkResult.ResultObjects[WorkResultObjectArrayIndex]; + if (CurrentWorkResultObj.State == EPDGWorkResultState::ToLoad) + { + CurrentWorkResultObj.State = EPDGWorkResultState::Loading; + + // Load this WRObj + PackageParams.PDGTOPNetworkName = CurrentTOPNet->NodeName; + PackageParams.PDGTOPNodeName = CurrentTOPNode->NodeName; + PackageParams.PDGWorkItemIndex = CurrentWorkResult.WorkItemIndex; + // Use the array index to ensure uniqueness among the work items of the node ( + // CurrentWorkResult.WorkItemIndex is not necessarily unique) + PackageParams.PDGWorkResultArrayIndex = WorkResultArrayIndex; + + if (CommandletStatus == EHoudiniBGEOCommandletStatus::Connected) + { + BGEOCommandletEndpoint->Send(new FHoudiniPDGImportBGEOMessage( + CurrentWorkResultObj.FilePath, + CurrentWorkResultObj.Name, + PackageParams, + CurrentTOPNode->NodeId, + CurrentWorkResult.WorkItemID, + StaticMeshGenerationProperties, + MeshBuildSettings + ), BGEOCommandletAddress); + } + else + { + if (FHoudiniPDGTranslator::CreateAllResultObjectsForPDGWorkItem( + AssetLink, + CurrentTOPNode, + CurrentWorkResultObj, + PackageParams)) + { + CurrentWorkResultObj.State = EPDGWorkResultState::Loaded; + CurrentWorkResultObj.SetAutoBakedSinceLastLoad(false); + CurrentTOPNode->bCachedHaveLoadedWorkResults = true; + + // Broadcast that we have loaded the work result object to those interested + AssetLink->OnWorkResultObjectLoaded.Broadcast( + AssetLink, CurrentTOPNode, WorkResultArrayIndex, + CurrentWorkResultObj.WorkItemResultInfoIndex); + } + else + { + CurrentWorkResultObj.State = EPDGWorkResultState::None; + } + } + } + else if (CurrentWorkResultObj.State == EPDGWorkResultState::Loaded) + { + // If the work item result obj is in the "Loaded" state, confirm that the output actor + // is still valid (the user could have manually deleted the output + if (!IsValid(CurrentWorkResultObj.GetOutputActorOwner().GetOutputActor())) + { + // If the output actor is invalid, set the state to ToDelete to complete the + // unload/deletion process + CurrentWorkResultObj.State = EPDGWorkResultState::ToDelete; + } + else + { + CurrentTOPNode->bCachedHaveLoadedWorkResults = true; + } + } + else if (CurrentWorkResultObj.State == EPDGWorkResultState::ToDelete) + { + CurrentWorkResultObj.State = EPDGWorkResultState::Deleting; + + // Delete and clean up that WRObj + CurrentTOPNode->DeleteWorkResultObjectOutputs(WorkResultArrayIndex, WorkResultObjectArrayIndex); + CurrentTOPNode->bCachedHaveNotLoadedWorkResults = true; + } + else if (CurrentWorkResultObj.State == EPDGWorkResultState::Deleted) + { + CurrentTOPNode->bCachedHaveNotLoadedWorkResults = true; + } + else if (CurrentWorkResultObj.State == EPDGWorkResultState::NotLoaded) + { + CurrentTOPNode->bCachedHaveNotLoadedWorkResults = true; + } + } + } + } + } + } +} + +void FHoudiniPDGManager::HandleImportBGEODiscoverMessage( + const FHoudiniPDGImportBGEODiscoverMessage& InMessage, + const TSharedRef& InContext) +{ + HOUDINI_LOG_DISPLAY(TEXT("Received Discover from %s"), *InContext->GetSender().ToString()); + // Ignore any discover acks received if we already have a valid local address + // for the commandlet + if (BGEOCommandletAddress.IsValid()) + return; + + if (BGEOCommandletProcHandle.IsValid() && InMessage.CommandletGuid.IsValid() && BGEOCommandletGuid == InMessage.CommandletGuid) + { + BGEOCommandletAddress = InContext->GetSender(); + } +} + +void FHoudiniPDGManager::HandleImportBGEOResultMessage( + const FHoudiniPDGImportBGEOResultMessage& InMessage, + const TSharedRef& InContext) +{ + HOUDINI_LOG_MESSAGE(TEXT("Received BGEO import result message")); + if (InMessage.ImportResult == EHoudiniPDGImportBGEOResult::HPIBR_Success || InMessage.ImportResult == EHoudiniPDGImportBGEOResult::HPIBR_PartialSuccess) + { + FHoudiniPackageParams PackageParams; + InMessage.PopulatePackageParams(PackageParams); + + // Find asset link and work result object + UHoudiniPDGAssetLink *AssetLink = nullptr; + UTOPNetwork *TOPNetwork = nullptr; + UTOPNode *TOPNode = nullptr; + if (!GetTOPAssetLinkNetworkAndNode(InMessage.TOPNodeId, AssetLink, TOPNetwork, TOPNode) || + !IsValid(AssetLink) || !IsValid(TOPNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to find TOP node with id %d, aborting output object creation."), InMessage.TOPNodeId); + return; + } + + FTOPWorkResult* WorkResult = nullptr; + const int32 WorkResultArrayIndex = TOPNode->ArrayIndexOfWorkResultByID(InMessage.WorkItemId); + if (WorkResultArrayIndex != INDEX_NONE) + WorkResult = TOPNode->GetWorkResultByArrayIndex(WorkResultArrayIndex); + if (WorkResult == nullptr) + { + HOUDINI_LOG_WARNING(TEXT("Failed to find TOP work result with id %d, aborting output object creation."), InMessage.WorkItemId); + return; + } + const FString& WorkResultObjectName = InMessage.Name; + FTOPWorkResultObject* WorkResultObject = WorkResult->ResultObjects.FindByPredicate( + [&WorkResultObjectName](const FTOPWorkResultObject& WorkResultObject) + { + return WorkResultObject.Name == WorkResultObjectName; + } + ); + if (WorkResultObject == nullptr) + { + HOUDINI_LOG_WARNING(TEXT("Failed to find TOP work result object with name %s, aborting output object creation."), *InMessage.Name); + return; + } + + if (WorkResultObject->State != EPDGWorkResultState::Loading) + { + HOUDINI_LOG_WARNING(TEXT("TOP work result object (%s) not in Loading state, aborting output object creation."), *InMessage.Name); + return; + } + + // Set package params outer + UObject* AssetLinkParent = AssetLink->GetOuter(); + UHoudiniAssetComponent* HAC = AssetLinkParent != nullptr ? Cast(AssetLinkParent) : nullptr; + UHoudiniCookable* HC = AssetLinkParent != nullptr ? Cast(AssetLinkParent) : nullptr; + if (HC) + { + AActor* Owner = HC->GetOwner(); + PackageParams.OuterPackage = Owner ? Cast(Owner->GetLevel()) : Cast(HC->GetPackage()); + } + else if (HAC) + { + PackageParams.OuterPackage = HAC->GetComponentLevel(); + } + else + { + if (AssetLinkParent) + { + PackageParams.OuterPackage = AssetLinkParent->GetPackage(); + } + } + + // Construct UHoudiniOutputs + bool bHasUnsupportedOutputs = false; + TArray> NewOutputs; + TMap InstancedOutputPartData; + NewOutputs.Reserve(InMessage.Outputs.Num()); + for (const FHoudiniPDGImportNodeOutput& Output : InMessage.Outputs) + { + UHoudiniOutput* NewOutput = NewObject( + AssetLink, + UHoudiniOutput::StaticClass(), + NAME_None,//FName(*OutputName), + RF_NoFlags); + NewOutputs.Add(NewOutput); + const int32 NumHGPO = Output.HoudiniGeoPartObjects.Num(); + for (int32 Index = 0; Index < NumHGPO; ++Index) + { + const FHoudiniGeoPartObject& HGPO = Output.HoudiniGeoPartObjects[Index]; + NewOutput->AddNewHGPO(HGPO); + + if (Output.InstancedOutputPartData.IsValidIndex(Index)) + { + FHoudiniOutputObjectIdentifier Identifier; + Identifier.ObjectId = HGPO.ObjectId; + Identifier.GeoId = HGPO.GeoId; + Identifier.PartId = HGPO.PartId; + Identifier.PartName = HGPO.PartName; + FHoudiniInstancerPartData InstancedPartData = Output.InstancedOutputPartData[Index]; + InstancedOutputPartData.Add(Identifier, InstancedPartData); + } + } + const int32 NumObjects = Output.OutputObjects.Num(); + for (int32 Index = 0; Index < NumObjects; ++Index) + { + const FHoudiniPDGImportNodeOutputObject& ImportOutputObject = Output.OutputObjects[Index]; + FHoudiniOutputObjectIdentifier Identifier = ImportOutputObject.Identifier; + + const FString& FullPackagePath = ImportOutputObject.PackagePath; + FString PackagePath; + FString PackageName; + const bool bDidSplit = FullPackagePath.Split(TEXT("."), &PackagePath, &PackageName, ESearchCase::IgnoreCase, ESearchDir::FromEnd); + if (!bDidSplit) + PackagePath = FullPackagePath; + + FHoudiniOutputObject OutputObject; + UPackage* Package = FindPackage(nullptr, *PackagePath); + if (!IsValid(Package)) + { + // Editor might have picked up the package yet, try to load it + Package = LoadPackage(nullptr, *PackagePath, LOAD_NoWarn); + } + if (IsValid(Package)) + { + OutputObject.OutputObject = FindObject(Package, *PackageName); + } + Identifier.bLoaded = true; + NewOutput->GetOutputObjects().Add(Identifier, OutputObject); + } + NewOutput->UpdateOutputType(); + const EHoudiniOutputType OutputType = NewOutput->GetType(); + if (OutputType != EHoudiniOutputType::Mesh && OutputType != EHoudiniOutputType::Instancer) + { + bHasUnsupportedOutputs = true; + } + } + + bool bSuccess = true; + if (bHasUnsupportedOutputs) + { + HOUDINI_LOG_MESSAGE(TEXT("Processing output types not supported by commandlet for %s"), *InMessage.Name); + bSuccess = FHoudiniPDGTranslator::CreateAllResultObjectsForPDGWorkItem( + AssetLink, TOPNode, *WorkResultObject, PackageParams, + { + EHoudiniOutputType::Landscape, + EHoudiniOutputType::Curve, + EHoudiniOutputType::Skeletal, + EHoudiniOutputType::LandscapeSpline, // TODO: could we support splines and curves via the commandlet / BGEO? + } + ); + + if (bSuccess) + { + // Clear/remove the outputs on WorkResultObject that are supported by the commandlet, since we + // are going to replace them with NewOutputs now + TArray>& CurrentOutputs = WorkResultObject->GetResultOutputs(); + const int32 NumCurrentOutputs = CurrentOutputs.Num(); + for (int32 Index = 0; Index < NumCurrentOutputs; ++Index) + { + UHoudiniOutput* CurOutput = CurrentOutputs[Index]; + const EHoudiniOutputType OutputType = CurOutput->GetType(); + if (OutputType != EHoudiniOutputType::Mesh && OutputType != EHoudiniOutputType::Instancer) + { + // Was created in editor, override the dummy one in NewOutputs with CurOutput + if (NewOutputs.IsValidIndex(Index)) + { + if (OutputType == NewOutputs[Index]->GetType()) + { + UHoudiniOutput* TempOutput = NewOutputs[Index]; + FHoudiniOutputTranslator::ClearOutput(TempOutput); + NewOutputs[Index] = CurOutput; + } + else + { + HOUDINI_LOG_ERROR(TEXT("Unexpected commandlet output type at index %d!"), Index); + } + } + else + { + HOUDINI_LOG_ERROR(TEXT("Expected output index %d from commandlet to be exist!"), Index); + } + } + } + } + } + + if (bSuccess && FHoudiniPDGTranslator::LoadExistingAssetsAsResultObjectsForPDGWorkItem( + AssetLink, + TOPNode, + *WorkResultObject, + PackageParams, + NewOutputs, + {EHoudiniOutputType::Mesh, EHoudiniOutputType::Instancer}, + &InstancedOutputPartData)) + { + const int32 NumOutputs = NewOutputs.Num(); + for (int32 OutputIndex = 0; OutputIndex < NumOutputs; ++OutputIndex) + { + UHoudiniOutput *NewOutput = NewOutputs[OutputIndex]; + + if (NewOutput->GetType() != EHoudiniOutputType::Mesh) + continue; + + const FHoudiniPDGImportNodeOutput& Output = InMessage.Outputs[OutputIndex]; + int32 NumObjects = Output.OutputObjects.Num(); + for (int32 Index = 0; Index < NumObjects; ++Index) + { + const FHoudiniPDGImportNodeOutputObject& ImportOutputObject = Output.OutputObjects[Index]; + FHoudiniOutputObjectIdentifier Identifier = ImportOutputObject.Identifier; + FHoudiniOutputObject *OutputObject = NewOutput->GetOutputObjects().Find(Identifier); + if (OutputObject) + { + for(auto Component : OutputObject->OutputComponents) + { + if (UActorComponent* ActorComponent = Cast(Component)) + { + // TODO: Add support for KeepTags attribute here, if needed. + ActorComponent->ComponentTags = FHoudiniEngineUtils::GetDefaultComponentTags(ActorComponent); + } + + // Update generic property attributes + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes( + Component, + ImportOutputObject.GenericAttributes.PropertyAttributes); + } + + // Copy cached attributes + OutputObject->CachedAttributes.Append(ImportOutputObject.CachedAttributes); + } + } + } + } + else + { + bSuccess = false; + } + + if (bSuccess) + { + WorkResultObject->State = EPDGWorkResultState::Loaded; + WorkResultObject->SetAutoBakedSinceLastLoad(false); + HOUDINI_LOG_MESSAGE(TEXT("Loaded geo for %s"), *InMessage.Name); + // Broadcast that we have loaded the work result object to those interested + AssetLink->OnWorkResultObjectLoaded.Broadcast( + AssetLink, TOPNode, WorkResultArrayIndex, WorkResultObject->WorkItemResultInfoIndex); + } + else + { + WorkResultObject->State = EPDGWorkResultState::None; + HOUDINI_LOG_WARNING(TEXT("Failed to process loaded assets for %s"), *InMessage.Name); + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("Commandlet failed to import bgeo for %s"), *InMessage.Name); + } +} + +bool FHoudiniPDGManager::CreateBGEOCommandletAndEndpoint() +{ + if (!BGEOCommandletEndpoint.IsValid()) + { + BGEOCommandletAddress.Invalidate(); + BGEOCommandletEndpoint = FMessageEndpoint::Builder(TEXT("Houdini BGEO Commandlet")) + .Handling(this, &FHoudiniPDGManager::HandleImportBGEOResultMessage) + .Handling(this, &FHoudiniPDGManager::HandleImportBGEODiscoverMessage) + .ReceivingOnThread(ENamedThreads::GameThread); + + if (!BGEOCommandletEndpoint.IsValid()) + { + HOUDINI_LOG_WARNING(TEXT("Could not set up messaging end point for BGEO commandlet")); + return false; + } + + BGEOCommandletEndpoint->Subscribe(); + } + + if (!BGEOCommandletProcHandle.IsValid() || !FPlatformProcess::IsProcRunning(BGEOCommandletProcHandle)) + { + // Start the bgeo commandlet + static const FString BGEOCommandletName = TEXT("HoudiniGeoImport"); + BGEOCommandletGuid = FGuid::NewGuid(); + BGEOCommandletAddress.Invalidate(); + + // Get the absolute path to the project file, if known, otherwise get + // the project name. For the path: quote it for the command line. + IFileManager& FileManager = IFileManager::Get(); + FString ProjectPathOrName = FApp::GetProjectName(); + if (FPaths::IsProjectFilePathSet()) + { + const FString ProjectPath = FPaths::GetProjectFilePath(); + if (!ProjectPath.IsEmpty()) + { + ProjectPathOrName = FString::Printf( + TEXT("\"%s\""), + *FileManager.ConvertToAbsolutePathForExternalAppForRead(*ProjectPath) + ); + } + } + + if (ProjectPathOrName.IsEmpty()) + return false; + + // Get the executable path for the app/editor + FString ExePath = FPlatformProcess::GenerateApplicationPath(FApp::GetName(), FApp::GetBuildConfiguration()); + if (!ExePath.IsEmpty()) + ExePath = FileManager.ConvertToAbsolutePathForExternalAppForRead(*ExePath); + + if (ExePath.IsEmpty()) + return false; + + const FString CommandLineParameters = FString::Printf( + TEXT("%s -messaging -run=%s -guid=%s -listen=%s -managerpid=%d"), + *ProjectPathOrName, + *BGEOCommandletName, + *BGEOCommandletGuid.ToString(), + *BGEOCommandletEndpoint->GetAddress().ToString(), + FPlatformProcess::GetCurrentProcessId()); + + BGEOCommandletProcHandle = FPlatformProcess::CreateProc( + *ExePath, + *CommandLineParameters, + false, + true, + false, + &BGEOCommandletProcessId, + 0, + NULL, + NULL); + if (!BGEOCommandletProcHandle.IsValid()) + { + return false; + } + } + + return true; +} + +void FHoudiniPDGManager::StopBGEOCommandletAndEndpoint() +{ + BGEOCommandletEndpoint.Reset(); + BGEOCommandletAddress.Invalidate(); + BGEOCommandletGuid.Invalidate(); + + if (BGEOCommandletProcHandle.IsValid() && FPlatformProcess::IsProcRunning(BGEOCommandletProcHandle)) + { + FPlatformProcess::TerminateProc(BGEOCommandletProcHandle, true); + if (BGEOCommandletProcHandle.IsValid()) + { + FPlatformProcess::WaitForProc(BGEOCommandletProcHandle); + FPlatformProcess::CloseProc(BGEOCommandletProcHandle); + } + } +} + +EHoudiniBGEOCommandletStatus FHoudiniPDGManager::UpdateAndGetBGEOCommandletStatus() +{ + if (BGEOCommandletProcHandle.IsValid()) + { + if (!FPlatformProcess::IsProcRunning(BGEOCommandletProcHandle)) + BGEOCommandletStatus = EHoudiniBGEOCommandletStatus::Crashed; + else if (BGEOCommandletAddress.IsValid()) + BGEOCommandletStatus = EHoudiniBGEOCommandletStatus::Connected; + else + BGEOCommandletStatus = EHoudiniBGEOCommandletStatus::Running; + } + else + BGEOCommandletStatus = EHoudiniBGEOCommandletStatus::NotStarted; + + return BGEOCommandletStatus; +} + + +bool +FHoudiniPDGManager::IsPDGAsset(const HAPI_NodeId& InAssetId) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniPDGManager::IsPDGAsset); + if (InAssetId < 0) + return false; + + // First check if an unreal_pdg_asset detail attribute exists - and get its value + // This is faster than looking at all the nodes on an HDA - especially when the HDA contains a lot of nodes/assets + { + HAPI_AttributeInfo AttribInfo; + FHoudiniApi::AttributeInfo_Init(&AttribInfo); + HAPI_Result Result = FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + InAssetId, 0, HAPI_UNREAL_ATTRIB_PDG_ASSET, HAPI_ATTROWNER_DETAIL, &AttribInfo); + + if (Result == HAPI_RESULT_SUCCESS && AttribInfo.exists) + { + TArray IntData; + FHoudiniHapiAccessor Accessor(InAssetId, 0, HAPI_UNREAL_ATTRIB_PDG_ASSET); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_DETAIL, IntData, 0, 1); + if (bSuccess && !IntData.IsEmpty()) + { + return (IntData[0] != 0); + } + } + } + + // TOO LONG ON BIG HDAS! + // Get the list of all non bypassed TOP nodes within the current network (ignoring schedulers) + int32 TOPNodeCount = 0; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), InAssetId, + HAPI_NodeType::HAPI_NODETYPE_TOP, HAPI_NODEFLAGS_TOP_NONSCHEDULER | HAPI_NODEFLAGS_NON_BYPASS, true, &TOPNodeCount)) + { + return false; + } + + // We found valid TOP Nodes, this is a PDG HDA + if (TOPNodeCount > 0) + return true; + + // No valid TOP node found in SOP/TOP Networks, this is not a PDG HDA + return false; + + /* + // Get all the network nodes within the asset, recursively. + // We're getting all networks because TOP network SOPs aren't considered being of TOP network type, but SOP type + int32 NetworkNodeCount = 0; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), InAssetId, + HAPI_NODETYPE_SOP | HAPI_NODETYPE_TOP, HAPI_NODEFLAGS_NETWORK | HAPI_NODEFLAGS_NON_BYPASS, false, &NetworkNodeCount), false); + + if (NetworkNodeCount <= 0) + return false; + + TArray AllNetworkNodeIDs; + AllNetworkNodeIDs.SetNum(NetworkNodeCount); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), InAssetId, + AllNetworkNodeIDs.GetData(), NetworkNodeCount), false); + + // For each Network we found earlier, only consider those with TOP child nodes + // If we find TOP nodes in a valid network, then consider this HDA a PDG HDA + HAPI_NodeInfo CurrentNodeInfo; + FHoudiniApi::NodeInfo_Init(&CurrentNodeInfo); + for (const HAPI_NodeId& CurrentNodeId : AllNetworkNodeIDs) + { + if (CurrentNodeId < 0) + { + continue; + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), CurrentNodeId, &CurrentNodeInfo)) + { + continue; + } + + // Skip non TOP or SOP networks + if (CurrentNodeInfo.type != HAPI_NodeType::HAPI_NODETYPE_TOP + && CurrentNodeInfo.type != HAPI_NodeType::HAPI_NODETYPE_SOP) + { + continue; + } + + // Get the list of all TOP nodes within the current network (ignoring schedulers) + int32 TOPNodeCount = 0; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), CurrentNodeId, + HAPI_NodeType::HAPI_NODETYPE_TOP, HAPI_NODEFLAGS_TOP_NONSCHEDULER | HAPI_NODEFLAGS_NON_BYPASS, false, &TOPNodeCount)) + { + continue; + } + + // We found valid TOP Nodes, this is a PDG HDA + if (TOPNodeCount > 0) + return true; + } + + // No valid TOP node found in SOP/TOP Networks, this is not a PDG HDA + return false; + */ +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngine/Private/HoudiniPDGManager.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" + +#include "HAL/PlatformProcess.h" + +#include "MessageEndpoint.h" + +class UHoudiniAssetComponent; +class UHoudiniCookable; +class UHoudiniPDGAssetLink; +class UTOPNetwork; +class UTOPNode; +class FSocket; + +enum class EPDGNodeState : uint8; + +// BGEO commandlet status +enum class HOUDINIENGINE_API EHoudiniBGEOCommandletStatus : uint8 +{ + // PDG manager has not tried to start the commandlet + NotStarted, + // PDG manager has PID for the commandlet and the host OS indicates it is running, but no message has been + // received from it yet + Running, + // PDG manager has PID for the commandlet, the host OS indicates it is running, and a discover message has been + // received + Connected, + // After being in the Connected state, the commandlet stopped running (host OS indicates PID is not valid) + Crashed +}; + +struct HOUDINIENGINE_API FHoudiniPDGManager +{ + +public: + + FHoudiniPDGManager(); + + virtual ~FHoudiniPDGManager(); + + // Initialize the PDG Asset Link for a HoudiniAssetComponent + // returns true if the HAC uses a PDG asset, and a PDGAssetLink was successfully created + bool InitializePDGAssetLink( + const HAPI_NodeId& InNodeId, + UObject* InOuter, + UHoudiniPDGAssetLink*& PDGAssetLink, + const bool& bHasBeenLoaded); + + // Updates an existing PDG AssetLink + static bool UpdatePDGAssetLink(UHoudiniPDGAssetLink* PDGAssetLink); + + // Find all TOP networks from linked HDA, as well as the TOP nodes within, and populate internal state. + static bool PopulateTOPNetworks(UHoudiniPDGAssetLink* PDGAssetLink, bool bInZeroWorkItemTallys=false); + + static void RefreshPDGAssetLinkUI(UHoudiniPDGAssetLink* InAssetLink); + + // Indicates if the Asset is a PDG Asset + // This will look for TOP nodes in all SOP/TOP net in the HDA. + static bool IsPDGAsset(const HAPI_NodeId& InAssetId); + + // Given TOP nodes from a TOP network, populate internal state from each TOP node. + static bool PopulateTOPNodes( + const TArray& InTopNodeIDs, + UTOPNetwork* InTOPNetwork, + UHoudiniPDGAssetLink* InPDGAssetLink, + bool bInZeroWorkItemTallys=false); + + // Cook the specified TOP node. + static bool CookTOPNode(UTOPNode* InTOPNode); + + // Dirty the specified TOP node and clear its work item results. + static void DirtyTOPNode(UTOPNode* InTOPNode); + + // // Dirty all the tasks/work items of the specified TOP node. Does not + // // clear its work item results. + // static void DirtyAllTasksOfTOPNode(FTOPNode& InTOPNode); + + // Dirty the TOP network and clear all work item results. + static void DirtyAll(UTOPNetwork* InTOPNet); + + // Cook the output TOP node of the currently selected TOP network. + static bool CookOutput(UTOPNetwork* InTOPNet); + + // Pause the PDG cook of the currently selected TOP network + static void PauseCook(UTOPNetwork* InTOPNet); + + // Cancel the PDG cook of the currently selected TOP network + static void CancelCook(UTOPNetwork* InTOPNet); + + static void NotifyAssetCooked(UHoudiniPDGAssetLink* InAssetLink, const bool& bSuccess); + + // Update all registered PDG Asset links + void Update(); + + void ReinitializePDGContext(); + + // Clear all of the specified work item's results from the specified TOP node. This destroys any loaded results + // (geometry etc), but keeps the work item struct. + //void ClearWorkItemResult(const HAPI_PDG_GraphContextId& InContextID, const HAPI_PDG_EventInfo& InEventInfo, FTOPNode& TOPNode); + void ClearWorkItemResult(UHoudiniPDGAssetLink* InAssetLink, const HAPI_PDG_WorkItemId& InWorkItemID, UTOPNode* InTOPNode); + + // Clear the specified work item's results from the specified TOP node and remove the work item struct from the TOP + // node. This destroys any loaded results (geometry etc), and the work item struct. + void RemoveWorkItem(UHoudiniPDGAssetLink* InAssetLink, const HAPI_PDG_WorkItemId& InWorkItemID, UTOPNode* InTOPNode); + + // Create a (or re-use an existing) FTOPWorkResult for a given TOPNode and the specified work item ID, without + // creating its FTOPWorkResultObjects. + // Returns INDEX_NONE if an entry could not be created or data could not be retrieved from HAPI. + int32 CreateOrRelinkWorkItem(UTOPNode* InTOPNode, const HAPI_PDG_GraphContextId& InContextID, HAPI_PDG_WorkItemId InWorkItemID); + + // Ensure that FTOPWorkResult exists, and create its FTOPWorkResultObjects for a given TOP node and work item id, + // and optionally (via bInLoadResultObjects) create its FTOPWorkResultObjects. + // Geometry is not directly loaded by this function, the FTOPWorkResultObjects' states will be set to ToLoad and + // the ProcessWorkItemResults function will take care of loading the geo. + // Results must be tagged with 'file', and must have a file path, otherwise will not included. + bool CreateOrRelinkWorkItemResult(UTOPNode* InTOPNode, const HAPI_PDG_GraphContextId& InContextID, HAPI_PDG_WorkItemId InWorkItemID, bool bInLoadResultObjects=false); + + // First Create or re-link FTOPWorkResults based on the work items that exist on InTOPNode in HAPI. + // Then remove any FTOPWorkResults (and clean up their output) from the WorkResults of InTOPNode if: + // WorkResult.WorkItemID is INDEX_NONE + // WorkResult.WorkItemID is not in the list of work item ids that HAPI returns for this node + int32 SyncAndPruneWorkItems(UTOPNode* InTOPNode); + + // Handles replies from commandlets in response to a FHoudiniPDGImportBGEODiscoverMessage + void HandleImportBGEODiscoverMessage( + const struct FHoudiniPDGImportBGEODiscoverMessage& InMessage, + const TSharedRef& InContext); + + // Handles messages sent by the commandlet once an import of a bgeo is complete, and uassets have been created. + void HandleImportBGEOResultMessage( + const struct FHoudiniPDGImportBGEOResultMessage& InMessage, + const TSharedRef& InContext); + + // Create the bgeo commandlet endpoint and start the commandlet (if not already running). + bool CreateBGEOCommandletAndEndpoint(); + + void StopBGEOCommandletAndEndpoint(); + + // Updates and returns the BGEO commandlet status + EHoudiniBGEOCommandletStatus UpdateAndGetBGEOCommandletStatus(); + +private: + + void UpdatePDGContexts(); + + void ProcessWorkItemResults(); + + void ProcessPDGEvent(const HAPI_PDG_GraphContextId& InContextID, HAPI_PDG_EventInfo& EventInfo); + + static void ResetPDGEventInfo(HAPI_PDG_EventInfo& InEventInfo); + + // Returns the PDGAssetLink and FTOPNode associated with this TOP node ID + bool GetTOPAssetLinkNetworkAndNode(const HAPI_NodeId& InNodeID, UHoudiniPDGAssetLink*& OutAssetLink, UTOPNetwork*& OutTOPNetwork, UTOPNode*& OutTOPNode); + + void SetTOPNodePDGState(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const EPDGNodeState& InPDGState); + + void NotifyTOPNodePDGStateClear(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode); + + void NotifyTOPNodeCreatedWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID); + + void NotifyTOPNodeRemovedWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID); + + void NotifyTOPNodeCookedWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID); + + void NotifyTOPNodeErrorWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID); + + void NotifyTOPNodeWaitingWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID); + + void NotifyTOPNodeScheduledWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID); + + void NotifyTOPNodeCookingWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID); + + void NotifyTOPNodeCookCancelledWorkItem(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNode* InTOPNode, const int32& InWorkItemID); + +private: + + TArray PDGContextNames; + TArray PDGContextIDs; + TArray PDGEventInfos; + + TArray> PDGAssetLinks; + + int32 MaxNumberOfPDGEvents = 20; + + TSharedPtr BGEOCommandletEndpoint; + FMessageAddress BGEOCommandletAddress; + FProcHandle BGEOCommandletProcHandle; + FGuid BGEOCommandletGuid; + uint32 BGEOCommandletProcessId; + // Keep track of the BGEO commandlet status + EHoudiniBGEOCommandletStatus BGEOCommandletStatus; +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniPDGTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPDGTranslator.h" + + +#include "Editor.h" +#include "Containers/Array.h" +#include "FileHelpers.h" +#include "LandscapeInfo.h" + +#include "HoudiniCookable.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniGeoImporter.h" +#include "HoudiniPackageParams.h" +#include "HoudiniOutput.h" +#include "HoudiniMeshTranslator.h" +#include "HoudiniInstanceTranslator.h" +#include "HoudiniLandscapeTranslator.h" +#include "HoudiniDataTableTranslator.h" +#include "HoudiniHLODLayerUtils.h" +#include "HoudiniLandscapeSplineTranslator.h" +#include "HoudiniPDGAssetLink.h" +#include "HoudiniOutputTranslator.h" +#include "HoudiniSplineComponent.h" +#include "HoudiniSplineTranslator.h" + +#define LOCTEXT_NAMESPACE "HoudiniEngine" + +bool +FHoudiniPDGTranslator::CreateAllResultObjectsForPDGWorkItem( + UHoudiniPDGAssetLink* InAssetLink, + UTOPNode* InTOPNode, + FTOPWorkResultObject& InWorkResultObject, + const FHoudiniPackageParams& InPackageParams, + TArray InOutputTypesToProcess, + bool bInTreatExistingMaterialsAsUpToDate) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniPDGTranslator::CreateAllResultObjectsForPDGWorkItem); + + if (!IsValid(InAssetLink)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniPDGTranslator::CreateAllResultObjectsForPDGWorkItem]: InAssetLink is null.")); + return false; + } + + if (!IsValid(InTOPNode)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniPDGTranslator::CreateAllResultObjectsForPDGWorkItem]: InTOPNode is null.")); + return false; + } + + TArray> OldTOPOutputs = InWorkResultObject.GetResultOutputs(); + TArray> NewTOPOutputs; + + FHoudiniEngine::Get().CreateTaskSlateNotification(LOCTEXT("LoadPDGBGEO", "Loading PDG Output BGEO File...")); + + bool bResult = false; + // Create a new file node in HAPI for the bgeo and cook it + HAPI_NodeId FileNodeId = -1; + bResult = UHoudiniGeoImporter::OpenBGEOFile(InWorkResultObject.FilePath, FileNodeId); + if (bResult) + bResult = UHoudiniGeoImporter::CookFileNode(FileNodeId); + + // If the cook was successful, build outputs + if (bResult) + { + FHoudiniEngine::Get().UpdateTaskSlateNotification( + LOCTEXT("BuildPDGBGEOOutputs", "Building Ouputs from BGEO File...")); + + const bool bAddOutputsToRootSet = false; + bResult = UHoudiniGeoImporter::BuildAllOutputsForNode( + FileNodeId, + InAssetLink, + OldTOPOutputs, + NewTOPOutputs, + bAddOutputsToRootSet); + } + + if (bResult) + { + FHoudiniEngine::Get().UpdateTaskSlateNotification( + LOCTEXT("TranslatePDGBGEOOutputs", "Translating PDG/BGEO Outputs...")); + + // If we successfully received outputs from the BGEO file, process the outputs + FOutputActorOwner& WROOutputActorOwner = InWorkResultObject.GetOutputActorOwner(); + AActor* WorkItemOutputActor = WROOutputActorOwner.GetOutputActor(); + if (!IsValid(WorkItemOutputActor)) + { + UWorld* World = InAssetLink->GetOutputWorld(); + if (!IsValid(World)) + World = InAssetLink->GetOwnerActor() ? InAssetLink->GetOwnerActor()->GetWorld() : nullptr; + + FOutputActorOwner& NodeOutputActorOwner = InTOPNode->GetOutputActorOwner(); + AActor* TOPNodeOutputActor = NodeOutputActorOwner.GetOutputActor(); + if (!IsValid(TOPNodeOutputActor)) + { + if (NodeOutputActorOwner.CreateOutputActor(World, InAssetLink, InAssetLink->OutputParentActor, FName(InTOPNode->NodeName))) + { + TOPNodeOutputActor = NodeOutputActorOwner.GetOutputActor(); + // Ensure that the outer level (or actor in the case of OFPA) is marked as dirty so that references to the + // output actors / objects are saved + InTOPNode->MarkPackageDirty(); + } + } + if (WROOutputActorOwner.CreateOutputActor(World, InAssetLink, TOPNodeOutputActor, FName(InWorkResultObject.Name))) + { + WorkItemOutputActor = WROOutputActorOwner.GetOutputActor(); + // Ensure that the outer level (or actor in the case of OFPA) is marked as dirty so that references to the + // output actors / objects are saved + InTOPNode->MarkPackageDirty(); + } + } + + for (auto& OldOutput : OldTOPOutputs) + { + FHoudiniOutputTranslator::ClearOutput(OldOutput); + } + OldTOPOutputs.Empty(); + InWorkResultObject.GetResultOutputs().Empty(); + InWorkResultObject.SetResultOutputs(NewTOPOutputs); + + // Gather landscape actors from inputs. + // NOTE: If performance becomes a problem, cache these on the TOPNode along with all the other cached landscape + // data. + TArray AllInputLandscapes; + UHoudiniCookable* HC = InAssetLink->GetOuterHoudiniCookable(); + FHoudiniEngineUtils::GatherLandscapeInputs(HC->GetInputs(), AllInputLandscapes); + + bResult = CreateAllResultObjectsFromPDGOutputs( + NewTOPOutputs, + InPackageParams, + WorkItemOutputActor->GetRootComponent(), + InTOPNode->ClearedLayers, + AllInputLandscapes, + InAssetLink, + InOutputTypesToProcess, + bInTreatExistingMaterialsAsUpToDate); + + if (!bResult) + FHoudiniEngine::Get().FinishTaskSlateNotification( + LOCTEXT("TranslatePDGBGEOOutputsFail", "Failed to translate all PDG/BGEO Outputs...")); + else + FHoudiniEngine::Get().FinishTaskSlateNotification( + LOCTEXT("TranslatePDGBGEOOutputsDone", "Done: Translating PDG/BGEO Outputs.")); + + InTOPNode->UpdateOutputVisibilityInLevel(); + } + else + { + FHoudiniEngine::Get().FinishTaskSlateNotification( + LOCTEXT("BuildPDGBGEOOutputsFail", "Failed building outputs from BGEO file...")); + } + + // Delete the file node used to load the BGEO via HAPI + if (FileNodeId >= 0) + { + UHoudiniGeoImporter::CloseBGEOFile(FileNodeId); + FileNodeId = -1; + } + + return bResult; +} + +bool +FHoudiniPDGTranslator::LoadExistingAssetsAsResultObjectsForPDGWorkItem( + UHoudiniPDGAssetLink* InAssetLink, + UTOPNode* InTOPNode, + FTOPWorkResultObject& InWorkResultObject, + const FHoudiniPackageParams& InPackageParams, + TArray>& InOutputs, + TArray InOutputTypesToProcess, + const TMap* InPreBuiltInstancedOutputPartData) +{ + if (!IsValid(InAssetLink)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniPDGTranslator::LoadExistingAssetsAsResultObjectsForPDGWorkItem]: InAssetLink is null.")); + return false; + } + + if (!IsValid(InTOPNode)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniPDGTranslator::LoadExistingAssetsAsResultObjectsForPDGWorkItem]: InTOPNode is null.")); + return false; + } + + FHoudiniEngine::Get().CreateTaskSlateNotification( + LOCTEXT("TranslatePDGBGEOOutputs", "Translating PDG/BGEO Outputs...")); + + // If we successfully received outputs from the BGEO file, process the outputs + FOutputActorOwner& WROOutputActorOwner = InWorkResultObject.GetOutputActorOwner(); + AActor* WorkItemOutputActor = WROOutputActorOwner.GetOutputActor(); + if (!IsValid(WorkItemOutputActor)) + { + UWorld* World = InAssetLink->GetOutputWorld(); + if (!IsValid(World)) + World = InAssetLink->GetOwnerActor() ? InAssetLink->GetOwnerActor()->GetWorld() : nullptr; + + FOutputActorOwner& NodeOutputActorOwner = InTOPNode->GetOutputActorOwner(); + AActor* TOPNodeOutputActor = NodeOutputActorOwner.GetOutputActor(); + if (!IsValid(TOPNodeOutputActor)) + { + if (NodeOutputActorOwner.CreateOutputActor(World, InAssetLink, InAssetLink->OutputParentActor, FName(InTOPNode->NodeName))) + { + TOPNodeOutputActor = NodeOutputActorOwner.GetOutputActor(); + // Ensure that the outer level (or actor in the case of OFPA) is marked as dirty so that references to the + // output actors / objects are saved + InTOPNode->MarkPackageDirty(); + } + } + if (WROOutputActorOwner.CreateOutputActor(World, InAssetLink, TOPNodeOutputActor, FName(InWorkResultObject.Name))) + { + WorkItemOutputActor = WROOutputActorOwner.GetOutputActor(); + // Ensure that the outer level (or actor in the case of OFPA) is marked as dirty so that references to the + // output actors / objects are saved + InTOPNode->MarkPackageDirty(); + } + } + + InWorkResultObject.SetResultOutputs(InOutputs); + + // Gather landscape actors from inputs. + // NOTE: If performance becomes a problem, cache these on the TOPNode along with all the other cached landscape + // data. + TArray AllInputLandscapes; + UHoudiniCookable* HC = InAssetLink->GetOuterHoudiniCookable(); + FHoudiniEngineUtils::GatherLandscapeInputs(HC->GetInputs(), AllInputLandscapes); + + const bool bInTreatExistingMaterialsAsUpToDate = true; + const bool bOnlyUseExistingAssets = true; + const bool bResult = CreateAllResultObjectsFromPDGOutputs( + InOutputs, + InPackageParams, + WorkItemOutputActor->GetRootComponent(), + InTOPNode->ClearedLayers, + AllInputLandscapes, + InAssetLink, + InOutputTypesToProcess, + bInTreatExistingMaterialsAsUpToDate, + bOnlyUseExistingAssets, + InPreBuiltInstancedOutputPartData); + + if (!bResult) + FHoudiniEngine::Get().FinishTaskSlateNotification( + LOCTEXT("TranslatePDGBGEOOutputsFail", "Failed to translate all PDG/BGEO Outputs...")); + else + FHoudiniEngine::Get().FinishTaskSlateNotification( + LOCTEXT("TranslatePDGBGEOOutputsDone", "Done: Translating PDG/BGEO Outputs.")); + + InTOPNode->UpdateOutputVisibilityInLevel(); + + return bResult; +} + +bool +FHoudiniPDGTranslator::CreateAllResultObjectsFromPDGOutputs( + TArray>& InOutputs, + const FHoudiniPackageParams& InPackageParams, + UObject* InOuterComponent, + FHoudiniClearedEditLayers& ClearedLayers, + TArray AllInputLandscapes, + UHoudiniPDGAssetLink* const InAssetLink, + TArray InOutputTypesToProcess, + bool bInTreatExistingMaterialsAsUpToDate, + bool bInOnlyUseExistingAssets, + const TMap* InPreBuiltInstancedOutputPartData + ) +{ + // Process the new/updated outputs via the various translators + // We try to maintain as much parity with the existing HoudiniAssetComponent workflow + // as possible. + + TArray InstancerOutputs; + TArray LandscapeOutputs; + TArray CreatedPackages; + + // TODO: COOKABLE: + // Make sure Outer is a Component! + + //bool bCreatedNewMaps = false; + UWorld* PersistentWorld = InOuterComponent->GetTypedOuter(); + check(PersistentWorld); + + // Fetch the HAC if the asset link is associated with one + UHoudiniCookable* HC = IsValid(InAssetLink) ? InAssetLink->GetOuterHoudiniCookable() : nullptr; + const bool bIsHACValid = IsValid(HC); + + // Keep track of all generated houdini materials to avoid recreating them over and over + TMap> AllOutputMaterials; + TMap CookedLandscapes; + + // Landscape splines track edit layers that were cleared per-landscape + TMap> ClearedLandscapeEditLayersForSplines; + + for (UHoudiniOutput* CurOutput : InOutputs) + { + const EHoudiniOutputType OutputType = CurOutput->GetType(); + if (InOutputTypesToProcess.Num() > 0 && !InOutputTypesToProcess.Contains(OutputType)) + { + continue; + } + switch (OutputType) + { + case EHoudiniOutputType::Mesh: + { + const bool bInDestroyProxies = false; + if (bInOnlyUseExistingAssets) + { + const bool bInApplyGenericProperties = false; + TMap NewOutputObjects(CurOutput->GetOutputObjects()); + FHoudiniMeshTranslator::CreateOrUpdateAllComponents( + CurOutput, + InOuterComponent, + NewOutputObjects, + bInDestroyProxies, + bInApplyGenericProperties + ); + } + else + { + // If we have a valid HAC get SMGP/MBS from it, otherwise use the plugin settings + FHoudiniMeshTranslator::CreateAllMeshesAndComponentsFromHoudiniOutput( + CurOutput, + InPackageParams, + EHoudiniStaticMeshMethod::FMeshDescription, + HC->GetSplitMeshSupport(), + bIsHACValid ? HC->GetStaticMeshGenerationProperties() : FHoudiniEngineRuntimeUtils::GetDefaultStaticMeshGenerationProperties(), + bIsHACValid ? HC->GetStaticMeshBuildSettings() : FHoudiniEngineRuntimeUtils::GetDefaultMeshBuildSettings(), + AllOutputMaterials, + InOuterComponent, + bInTreatExistingMaterialsAsUpToDate, + bInDestroyProxies + ); + } + } + break; + + case EHoudiniOutputType::Curve: + { + // Output curve + FHoudiniSplineTranslator::CreateAllSplinesFromHoudiniOutput(CurOutput, InOuterComponent); + break; + } + + case EHoudiniOutputType::Instancer: + { + InstancerOutputs.Add(CurOutput); + } + break; + + case EHoudiniOutputType::Landscape: + { + // Retrieve the topnet parent to which Sharedlandscapes will be attached. + AActor* WorkItemActor = InOuterComponent->GetTypedOuter(); + USceneComponent* TopnetParent = nullptr; + if (WorkItemActor) + { + AActor* TopnetParentActor = WorkItemActor->GetAttachParentActor(); + if (TopnetParentActor) + { + TopnetParent = TopnetParentActor->GetRootComponent(); + } + } + + FString CookedPrefix = FHoudiniEngineUtils::GetOuterHoudiniCookable(CurOutput)->GetOwner()->GetActorLabel() + "_" + + InPackageParams.PDGTOPNodeName; + + FHoudiniLandscapeTranslator::ProcessLandscapeOutput( + CurOutput, + AllInputLandscapes, + CookedPrefix, + PersistentWorld, + InPackageParams, + CookedLandscapes, + ClearedLayers, + CreatedPackages); + // Attach any landscape actors to InOuterComponent + LandscapeOutputs.Add(CurOutput); + } + break; + + case EHoudiniOutputType::DataTable: + { + FHoudiniPackageParams PackageParams = InPackageParams; + + for (auto&& HGPO : CurOutput->HoudiniGeoPartObjects) + { + FHoudiniDataTableTranslator::BuildDataTable(HGPO, CurOutput, PackageParams); + } + break; + } + + case EHoudiniOutputType::LandscapeSpline: + { + FHoudiniLandscapeSplineTranslator::ProcessLandscapeSplineOutput( + CurOutput, + AllInputLandscapes, + PersistentWorld, + InPackageParams, + ClearedLandscapeEditLayersForSplines); + + break; + } + + default: + { + HOUDINI_LOG_WARNING(TEXT("[FTOPWorkResultObject::UpdateResultOutputs]: Unsupported output type: %s"), *UHoudiniOutput::OutputTypeToString(OutputType)); + } + break; + } + + for (auto& CurMat : CurOutput->GetAssignementMaterials()) + { + //Adds the generated materials if any + if (!AllOutputMaterials.Contains(CurMat.Key)) + AllOutputMaterials.Add(CurMat); + } + } + + // Process instancer outputs after all other outputs have been processed, since it + // might depend on meshes etc from other outputs + FHoudiniInstanceTranslator::CreateAllInstancersFromHoudiniOutputs( + InOutputs, + InOuterComponent, + InPackageParams, + InPreBuiltInstancedOutputPartData); + + USceneComponent* ParentComponent = Cast(InOuterComponent); + if (IsValid(ParentComponent)) + { + AActor* ParentActor = ParentComponent->GetOwner(); + for (UHoudiniOutput* LandscapeOutput : LandscapeOutputs) + { + if(!IsValid(LandscapeOutput)) + continue; + + for (auto& Pair : LandscapeOutput->GetOutputObjects()) + { + FHoudiniOutputObject& OutputObject = Pair.Value; + + // If the OutputObject has an OutputComponent, try to attach it to ParentComponent + for(auto OutputComponent : OutputObject.OutputComponents) + { + USceneComponent* Component = Cast(OutputComponent); + if (IsValid(Component) && !Component->IsAttachedTo(ParentComponent)) + { + Component->AttachToComponent(ParentComponent, FAttachmentTransformRules::KeepWorldTransform); + continue; + } + } + + // If OutputObject does not have an OutputComponent, check if OutputObject is an AActor and attach + // it to ParentComponent + if (IsValid(OutputObject.OutputObject)) + { + UHoudiniLandscapePtr* LandscapePtr = Cast(OutputObject.OutputObject); + if (IsValid(LandscapePtr)) + { + ALandscapeProxy* LandscapeProxy = LandscapePtr->GetRawPtr(); + if (IsValid(LandscapeProxy)) + { + if (!LandscapeProxy->IsAttachedTo(ParentActor)) + { + LandscapeProxy->AttachToActor(ParentActor, FAttachmentTransformRules::KeepWorldTransform); + LandscapeProxy->RecreateCollisionComponents(); + } + + if (LandscapeProxy) + { + // We need to recreate component states for landscapes if a tile was created, moved, or resized + // otherwise the landscape will exhibit render artifacts (such as only rendering every other + // component.) + LandscapeProxy->RecreateComponentsState(); + } + } + } + } + } + } + } + + /* + if (bCreatedNewMaps && PersistentWorld) + { + // Force the asset registry to update its cache of packages paths + // recursively for this world, otherwise world composition won't + // pick them up during the WorldComposition::Rescan(). + FHoudiniEngineUtils::RescanWorldPath(PersistentWorld); + + ULandscapeInfo::RecreateLandscapeInfo(PersistentWorld, true); + + if (IsValid(PersistentWorld->WorldComposition)) + { + UWorldComposition::WorldCompositionChangedEvent.Broadcast(PersistentWorld); + } + + FEditorDelegates::RefreshLevelBrowser.Broadcast(); + FEditorDelegates::RefreshAllBrowsers.Broadcast(); + } + */ + + if (CreatedPackages.Num() > 0) + { + // Save created packages. For example, we don't want landscape layers deleted + // along with the HDA. + FEditorFileUtils::PromptForCheckoutAndSave(CreatedPackages, true, false); + } + + return true; +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngine/Private/HoudiniPDGTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +class UHoudiniPDGAssetLink; +class UHoudiniOutput; +class AActor; +class UTOPNode; +class ALandscapeProxy; + +enum class EHoudiniOutputType : uint8; + +struct FHoudiniPackageParams; +struct FTOPWorkResultObject; +struct FHoudiniOutputObjectIdentifier; +struct FHoudiniInstancerPartData; +struct FHoudiniLandscapeExtent; +struct FHoudiniLandscapeReferenceLocation; +struct FHoudiniLandscapeTileSizeInfo; +struct FHoudiniLandscapeSpatialData; +struct FHoudiniClearedEditLayers; + +struct HOUDINIENGINE_API FHoudiniPDGTranslator +{ + public: + // Create/update assets/geometry for all PDG outputs of InWorkResultObject. This will use + // InWorkResultObject.FilePath to load the BGEO file and update/build outputs. + static bool CreateAllResultObjectsForPDGWorkItem( + UHoudiniPDGAssetLink* InAssetLink, + UTOPNode* InTOPNode, + FTOPWorkResultObject& InWorkResultObject, + const FHoudiniPackageParams& InPackageParams, + TArray InOutputTypesToProcess={}, + bool bInTreatExistingMaterialsAsUpToDate=false); + + static bool LoadExistingAssetsAsResultObjectsForPDGWorkItem( + UHoudiniPDGAssetLink* InAssetLink, + UTOPNode* InTOPNode, + FTOPWorkResultObject& InWorkResultObject, + const FHoudiniPackageParams& InPackageParams, + TArray>& InOutputs, + TArray InOutputTypesToProcess={}, + const TMap* InPreBuiltInstancedOutputPartData=nullptr); + + // Use the relevant translators to create assets/geometry for all PDG outputs (InOutputs). + // InOuterComponent is the component to attach the created output objects/components to. + static bool CreateAllResultObjectsFromPDGOutputs( + TArray>& InOutputs, + const FHoudiniPackageParams& InPackageParams, + UObject* InOuterComponent, + FHoudiniClearedEditLayers& ClearedLayers, + TArray AllInputLandscapes, + UHoudiniPDGAssetLink* const InAssetLink, + TArray InOutputTypesToProcess={}, + bool bInTreatExistingMaterialsAsUpToDate=false, + bool bInOnlyUseExistingAssets=false, + const TMap* InPreBuiltInstancedOutputPartData=nullptr + ); +}; + + +``` + +`Source/HoudiniEngine/Private/HoudiniPackageParams.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPackageParams.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniStaticMesh.h" +#include "Engine/DataTable.h" +#include "Engine/SkeletalMesh.h" +#include "PackageTools.h" +#include "ObjectTools.h" +#include "UObject/MetaData.h" +#include "Engine/StaticMesh.h" +#include "Animation/Skeleton.h" +#include "Animation/AnimSequence.h" +#include "FoliageType_InstancedStaticMesh.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "GeometryCollection/GeometryCollectionObject.h" +#else + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionObject.h" +#endif +#include "HoudiniDataLayerUtils.h" + +#if HOUDINI_ENABLE_DATA_LAYERS +#include "WorldPartition/DataLayer/DataLayerAsset.h" +#endif + +// +FHoudiniPackageParams::FHoudiniPackageParams() +{ + PackageMode = EPackageMode::CookToTemp; + ReplaceMode = EPackageReplaceMode::ReplaceExistingAssets; + + TempCookFolder = FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder(); + BakeFolder = FHoudiniEngineRuntime::Get().GetDefaultBakeFolder(); + + OuterPackage = nullptr; + ObjectName = FString(); + HoudiniAssetName = FString(); + HoudiniAssetActorName = FString(); + + ObjectId = 0; + GeoId = 0; + PartId = 0; + SplitStr = 0; + + ComponentGUID.Invalidate(); + + PDGTOPNetworkName.Empty(); + PDGTOPNodeName.Empty(); + PDGWorkItemIndex = INDEX_NONE; + PDGWorkResultArrayIndex = INDEX_NONE; +} + + +// +FHoudiniPackageParams::~FHoudiniPackageParams() +{ + + +} + + +// Returns the object flags corresponding to the current package mode +EObjectFlags +FHoudiniPackageParams::GetObjectFlags() const +{ + if (PackageMode == EPackageMode::CookToTemp) + return RF_Public | RF_Standalone; + else if (PackageMode == EPackageMode::Bake) + return RF_Public | RF_Standalone; + else + return RF_NoFlags; +} + +FString +FHoudiniPackageParams::GetPackageName() const +{ + if (!ObjectName.IsEmpty()) + return ObjectName; + + // If we have PDG infos, generate a name including them + if (!PDGTOPNetworkName.IsEmpty() && !PDGTOPNodeName.IsEmpty() && PDGWorkResultArrayIndex >= 0) + { + return FString::Printf( + TEXT("%s_%s_%s_%d_%d_%s"), + *HoudiniAssetName, *PDGTOPNetworkName, *PDGTOPNodeName, PDGWorkResultArrayIndex, PartId, *SplitStr); + } + else + { + // Generate an object name using the HGPO IDs and the HDA name + return FString::Printf(TEXT("%s_%d_%d_%d_%s"), *HoudiniAssetName, ObjectId, GeoId, PartId, *SplitStr); + } +} + +FString +FHoudiniPackageParams::GetPackagePath() const +{ + FString PackagePath = FString(); + switch (PackageMode) + { + case EPackageMode::CookToLevel_Invalid: + case EPackageMode::CookToTemp: + { + // Temporary Folder + PackagePath = TempCookFolder; + // Add a subdir for the HDA + if (!HoudiniAssetName.IsEmpty()) + PackagePath += TEXT("/") + HoudiniAssetName; + // Add a subdir using the owner component GUID if possible + if (ComponentGUID.IsValid()) + PackagePath += TEXT("/") + ComponentGUID.ToString().Left(PACKAGE_GUID_LENGTH); + } + break; + + case EPackageMode::Bake: + { + PackagePath = BakeFolder; + } + break; + } + + return PackagePath; +} + +bool +FHoudiniPackageParams::GetBakeCounterFromBakedAsset(const UObject* InAsset, int32& OutBakeCounter) +{ + OutBakeCounter = 0; + + if (!IsValid(InAsset)) + return false; + + UPackage* Package = InAsset->GetPackage(); + // const FString PackagePathName = Package->GetPathName(); + // FString PackagePathNamePrefix; + // FString BakeCountOrGUID; + // if (!GetPackageNameWithoutBakeCounterOrGUIDSuffix(PackagePathName, PackagePathNamePrefix, BakeCountOrGUID)) + // PackagePathNamePrefix = PackagePathName; + // + // const FString ThisPackageNameBase = UPackageTools::SanitizePackageName(GetPackagePath() + TEXT("/") + GetPackageName()); + // if (!PackagePathNamePrefix.Equals(ThisPackageNameBase)) + // return false; + // + // // Not a valid counter suffix, could be a GUID suffix. Return true since the prefixes match. + // if (BakeCountOrGUID.IsNumeric()) + // OutBakeCounter = FCString::Atoi(*BakeCountOrGUID); + // + // return true; + + if (!IsValid(Package)) + return false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FMetaData& MetaData = Package->GetMetaData(); +#else + UMetaData* MetaData = Package->GetMetaData(); + if (!IsValid(MetaData)) + return false; +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + if (MetaData.RootMetaDataMap.Contains(HAPI_UNREAL_PACKAGE_META_BAKE_COUNTER)) + { + FString BakeCounterStr = MetaData.RootMetaDataMap.FindChecked(HAPI_UNREAL_PACKAGE_META_BAKE_COUNTER); +#else + if (MetaData->RootMetaDataMap.Contains(HAPI_UNREAL_PACKAGE_META_BAKE_COUNTER)) + { + FString BakeCounterStr = MetaData->RootMetaDataMap.FindChecked(HAPI_UNREAL_PACKAGE_META_BAKE_COUNTER); +#endif + BakeCounterStr.TrimStartAndEndInline(); + if (BakeCounterStr.IsNumeric()) + { + OutBakeCounter = FCString::Atoi(*BakeCounterStr); + return true; + } + } + + return false; +} + +bool +FHoudiniPackageParams::GetGUIDFromTempAsset(const UObject* InAsset, FString& OutGUID) +{ + if (!InAsset) + return false; + + UPackage* Package = InAsset->GetPackage(); + if (!IsValid(Package)) + return false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FMetaData& MetaData = Package->GetMetaData(); +#else + UMetaData* MetaData = Package->GetMetaData(); + if (!IsValid(MetaData)) + return false; +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + if (MetaData.RootMetaDataMap.Contains(HAPI_UNREAL_PACKAGE_META_TEMP_GUID)) +#else + if (MetaData->RootMetaDataMap.Contains(HAPI_UNREAL_PACKAGE_META_TEMP_GUID)) +#endif + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + OutGUID = MetaData.RootMetaDataMap.FindChecked(HAPI_UNREAL_PACKAGE_META_TEMP_GUID); +#else + OutGUID = MetaData->RootMetaDataMap.FindChecked(HAPI_UNREAL_PACKAGE_META_TEMP_GUID); +#endif + OutGUID.TrimStartAndEndInline(); + if (!OutGUID.IsEmpty()) + return true; + } + + return false; +} + +FString +FHoudiniPackageParams::GetPackageNameExcludingBakeCounter(const UObject* InAsset) +{ + if (!IsValid(InAsset)) + return FString(); + + UPackage* Package = InAsset->GetPackage(); + if (!IsValid(Package)) + return FString(); + + FString PackageName = FPaths::GetCleanFilename(Package->GetPathName()); + int32 BakeCounter = 0; + if (GetBakeCounterFromBakedAsset(InAsset, BakeCounter)) + { + const FString BakeCounterSuffix = FString::Printf(TEXT("_%d"), BakeCounter); + if (PackageName.EndsWith(BakeCounterSuffix)) + PackageName = PackageName.Mid(0, PackageName.Len() - BakeCounterSuffix.Len()); + } + + return PackageName; +} + +bool +FHoudiniPackageParams::MatchesPackagePathNameExcludingBakeCounter(const UObject* InAsset) const +{ + if (!IsValid(InAsset)) + return false; + + UPackage* Package = InAsset->GetPackage(); + if (!IsValid(Package)) + return false; + + const FString InAssetPackagePathName = FPaths::GetPath(Package->GetPathName()) + TEXT("/") + GetPackageNameExcludingBakeCounter(InAsset); + const FString ThisPackagePathName = UPackageTools::SanitizePackageName(GetPackagePath() + TEXT("/") + GetPackageName()); + return InAssetPackagePathName.Equals(ThisPackagePathName); +} + +bool +FHoudiniPackageParams::HasMatchingPackageDirectories(UObject const* const InAsset) const +{ + if (!IsValid(InAsset)) + return false; + + UPackage const* const Package = InAsset->GetPackage(); + if (!IsValid(Package)) + return false; + + const FString InAssetPackageDirectory = FPaths::GetPath(Package->GetPathName()); + const FString ThisPackageDirectory = UPackageTools::SanitizePackageName(GetPackagePath()); + return InAssetPackageDirectory.Equals(ThisPackageDirectory); +} + +FString +FHoudiniPackageParams::GetPackageNameExcludingGUID(const UObject* InAsset) +{ + if (!IsValid(InAsset)) + return FString(); + + UPackage* Package = InAsset->GetPackage(); + if (!IsValid(Package)) + return FString(); + + FString PackageName = FPaths::GetCleanFilename(Package->GetPathName()); + FString GUIDStr; + if (GetGUIDFromTempAsset(InAsset, GUIDStr)) + { + if (PackageName.EndsWith(TEXT("_") + GUIDStr)) + PackageName = PackageName.Mid(0, PackageName.Len() - GUIDStr.Len() - 1); + } + + return PackageName; +} + +UPackage* +FHoudiniPackageParams::CreatePackageForObject(FString& OutPackageName, int32 InBakeCounterStart) const +{ + // GUID/counter used to differentiate with existing package + int32 BakeCounter = InBakeCounterStart; + FGuid CurrentGuid = FGuid::NewGuid(); + + // Get the appropriate package path/name for this object + FString PackageName = GetPackageName(); + FString PackagePath = GetPackagePath(); + + // Iterate until we find a suitable name for the package + UPackage * NewPackage = nullptr; + while (true) + { + OutPackageName = PackageName; + + // Append the Bake guid/counter to the object name if needed + if (BakeCounter > 0) + { + OutPackageName += (PackageMode == EPackageMode::Bake) + ? TEXT("_") + FString::FromInt(BakeCounter) + : TEXT("_") + CurrentGuid.ToString().Left(PACKAGE_GUID_LENGTH); + } + + // Build the final package name + FString FinalPackageName = PackagePath + TEXT("/") + OutPackageName; + + //NodeSync Name Override + if (OverideEnabled) + { + FinalPackageName = FolderOverride + TEXT("/") + NameOverride; + OutPackageName = NameOverride; + } + // Sanitize package name. + FinalPackageName = UPackageTools::SanitizePackageName(FinalPackageName); + + // If we are set to create new assets, check if a package named similarly already exists + if (ReplaceMode == EPackageReplaceMode::CreateNewAssets) + { + UPackage* FoundPackage = FindPackage(nullptr, *FinalPackageName); + if (FoundPackage == nullptr) + { + // Package might not be in memory, check if it exists on disk + FoundPackage = LoadPackage(nullptr, *FinalPackageName, LOAD_Verify | LOAD_NoWarn); + } + + if (IsValid(FoundPackage)) + { + // we need to generate a new name for it + CurrentGuid = FGuid::NewGuid(); + BakeCounter++; + continue; + } + } + + // Create actual package. + NewPackage = CreatePackage(*FinalPackageName); + if (IsValid(NewPackage)) + { + // Record bake counter / temp GUID in package metadata +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FMetaData& MetaData = NewPackage->GetMetaData(); +#else + UMetaData* MetaData = NewPackage->GetMetaData(); + if (IsValid(MetaData)) +#endif + { + if (PackageMode == EPackageMode::Bake) + { + // HOUDINI_LOG_MESSAGE(TEXT("Recording bake counter in package metadata: %d"), BakeCounter); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + MetaData.RootMetaDataMap.Add( + HAPI_UNREAL_PACKAGE_META_BAKE_COUNTER, FString::FromInt(BakeCounter)); +#else + MetaData->RootMetaDataMap.Add( + HAPI_UNREAL_PACKAGE_META_BAKE_COUNTER, FString::FromInt(BakeCounter)); +#endif + } + else if (CurrentGuid.IsValid()) + { + const FString GuidStr = CurrentGuid.ToString().Left(PACKAGE_GUID_LENGTH); + // HOUDINI_LOG_MESSAGE(TEXT("Recording temp guid in package metadata: %s"), *GuidStr); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + MetaData.RootMetaDataMap.Add(HAPI_UNREAL_PACKAGE_META_TEMP_GUID, GuidStr); +#else + MetaData->RootMetaDataMap.Add(HAPI_UNREAL_PACKAGE_META_TEMP_GUID, GuidStr); +#endif + } + + // Store the Component GUID that generated the package, as well as the package name and meta key + // indicating that the package was generated by Houdini + { + const FString ComponentGuidStr = ComponentGUID.ToString(); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + MetaData.RootMetaDataMap.Add(HAPI_UNREAL_PACKAGE_META_COMPONENT_GUID, ComponentGuidStr); + MetaData.RootMetaDataMap.Add(HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + MetaData.RootMetaDataMap.Add(HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *OutPackageName); +#else + MetaData->RootMetaDataMap.Add(HAPI_UNREAL_PACKAGE_META_COMPONENT_GUID, ComponentGuidStr); + MetaData->RootMetaDataMap.Add(HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + MetaData->RootMetaDataMap.Add(HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *OutPackageName); +#endif + } + } + } + + // If we are not in CreateNewAssets mode, we might have created a package that already exists, and saving/overwriting it will fail! + // We probably should do the FindPackage/LoadPackage logic and skip the CreatePackage if successful, + // but that might confuse existing logic relying on getting a fresh package. + // (There is some logic to deal with this in CreateObjectAndPackage(), but due to not returning existing packages it seems unused.) + // + // Instead we do something similar the save logic: + // - If it encounters a new package, but the package already exists on disk it errors + // - Otherwise it marks the new package as "FullyLoaded" and writes it to disk. + // To handle the situation in first bullet we mark it as FullyLoaded here. + if (FPackageName::DoesPackageExist(FPackagePath::FromPackageNameChecked(NewPackage->GetName()))) + { + HOUDINI_LOG_WARNING(TEXT("Package already exists on disk. MarkAsFullyLoaded() to overwrite %s"), *NewPackage->GetName()); + NewPackage->MarkAsFullyLoaded(); + } + + break; + } + + return NewPackage; +} + +UObject* FHoudiniPackageParams::CreateObjectAndPackageFromClass(UClass* Class, UObject* TemplateObject) const +{ + // Create the package for the object + FString NewObjectName; + UPackage* Package = CreatePackageForObject(NewObjectName); + if (!IsValid(Package)) + return nullptr; + + const FString SanitizedObjectName = ObjectTools::SanitizeObjectName(NewObjectName); + + UObject* ExistingTypedObject = StaticFindObject(Class, Package, *NewObjectName); + UObject* ExistingObject = FindObject(Package, *NewObjectName); + + if (IsValid(ExistingTypedObject)) + { + // An object of the appropriate type already exists, update it! + ExistingTypedObject->PreEditChange(nullptr); + } + else if (ExistingObject != nullptr) + { + // Replacing an object of a different type, Delete it first. + const bool bDeleteSucceeded = ObjectTools::DeleteSingleObject(ExistingObject); + if (bDeleteSucceeded) + { + // Force GC so we can cleanly create a new asset (and not do an 'in place' replacement) + CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); + + // Create a package for each mesh + Package = CreatePackageForObject(NewObjectName); + if (!IsValid(Package)) + return nullptr; + } + else + { + // failed to delete + return nullptr; + } + } + + UObject* CreatedObject = NewObject(Package, Class, FName(*SanitizedObjectName), GetObjectFlags(), TemplateObject); + + // Add meta information to the new object in the package. + if (IsValid(CreatedObject)) + { + FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + Package, CreatedObject, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + Package, CreatedObject, HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *NewObjectName); + + const FString ComponentGuidStr = ComponentGUID.ToString(); + FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + Package, CreatedObject, HAPI_UNREAL_PACKAGE_META_COMPONENT_GUID, ComponentGuidStr); + } + + return CreatedObject; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniPackageParams.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniAssetComponent.h" +#include "UObject/ObjectMacros.h" +#include "Engine/World.h" +#include "Misc/Paths.h" +#include "HoudiniDataLayerUtils.h" +#include "HoudiniStringResolver.h" +#include "HoudiniPackageParams.generated.h" + +class UStaticMesh; + +UENUM() +enum class EPackageMode : int8 +{ + CookToLevel_Invalid, // V1 Behaviour, saving package to the level, now invalid + CookToTemp, + Bake +}; + +UENUM() +enum class EPackageReplaceMode : int8 +{ + CreateNewAssets, + ReplaceExistingAssets +}; + +USTRUCT() +struct HOUDINIENGINE_API FHoudiniPackageParams +{ +public: + GENERATED_BODY(); + + // + FHoudiniPackageParams(); + // + ~FHoudiniPackageParams(); + + // Helper functions returning the default behavior expected when cooking mesh + static EPackageMode GetDefaultStaticMeshesCookMode() { return EPackageMode::CookToTemp; }; + // Helper functions returning the default behavior expected when cooking materials or textures + static EPackageMode GetDefaultMaterialAndTextureCookMode() { return EPackageMode::CookToTemp; }; + // Helper functions returning the default behavior for replacing existing package + static EPackageReplaceMode GetDefaultReplaceMode() { return EPackageReplaceMode::ReplaceExistingAssets; }; + + // Returns the name for the package depending on the mode + FString GetPackageName() const; + // Returns the package's path depending on the mode + FString GetPackagePath() const; + // Returns the object flags corresponding to the current package mode + EObjectFlags GetObjectFlags() const; + + // Get the bake counter for InAsset's package metadata. Return true if the counter was found, false otherwise. + static bool GetBakeCounterFromBakedAsset(const UObject* InAsset, int32& OutBakeCounter); + + // Get the GUID for a temp asset. + static bool GetGUIDFromTempAsset(const UObject* InAsset, FString& OutGUID); + + // Get package name without bake counter + static FString GetPackageNameExcludingBakeCounter(const UObject* InAsset); + + // Get package name without temp GUID suffix + static FString GetPackageNameExcludingGUID(const UObject* InAsset); + + // Returns true if these package params generate the same package path and name as InAsset's package path name (with + // any potential bake counters stripped during comparison) + bool MatchesPackagePathNameExcludingBakeCounter(const UObject* InAsset) const; + + // Returns true if the directory containing the package for InAsset matches the directory that would contain the + // package generated by these package params. + bool HasMatchingPackageDirectories(UObject const* const InAsset) const; + + // Helper function to create a Package for a given object + UPackage* CreatePackageForObject(FString& OutPackageName, int32 InBakeCounterStart=0) const; + + // Helper function to create an object and its package + + UObject* CreateObjectAndPackageFromClass(UClass* Class, UObject* TemplateObject = nullptr) const; + + template T* CreateObjectAndPackage(T* TemplateObject = nullptr) const + { + return static_cast(CreateObjectAndPackageFromClass(T::StaticClass(), TemplateObject)); + } + + // The current cook/baking mode + UPROPERTY() + EPackageMode PackageMode; + // How to handle existing assets? replace or rename? + UPROPERTY() + EPackageReplaceMode ReplaceMode; + + // When cooking in bake mode - folder to create assets in + UPROPERTY() + FString BakeFolder; + // When cooking in temp mode - folder to create assets in + UPROPERTY() + FString TempCookFolder; + + // Package to save to + UPROPERTY() + TObjectPtr OuterPackage; + + // Name of the package we want to create + // If null, we'll generate one from: + // (without PDG) ASSET_OBJ_GEO_PART_SPLIT, + // (with PDG) ASSET_TOPNET_TOPNODE_WORKITEMINDEX_PART_SPLIT + UPROPERTY() + FString ObjectName; + + // Name of the HDA + UPROPERTY() + FString HoudiniAssetName; + + // Name of actor that is managing an instance of the HDA + UPROPERTY() + FString HoudiniAssetActorName; + + // + UPROPERTY() + int32 ObjectId; + // + UPROPERTY() + int32 GeoId; + // + UPROPERTY() + int32 PartId; + // + UPROPERTY() + FString SplitStr; + + // GUID used for the owner + UPROPERTY() + FGuid ComponentGUID; + + // For PDG temporary outputs: the TOP network name + UPROPERTY() + FString PDGTOPNetworkName; + // For PDG temporary outputs: the TOP node name + UPROPERTY() + FString PDGTOPNodeName; + // For PDG temporary outputs: the work item index of the TOP node + UPROPERTY() + int32 PDGWorkItemIndex; + // For PDG temporary outputs: the work item array index in the WorkResult array of the TOP node + UPROPERTY() + int32 PDGWorkResultArrayIndex; + + //Session Sync Asset Editor Users need the ability explicitly set the name + UPROPERTY() + FString NameOverride; + + UPROPERTY() + FString FolderOverride; + + UPROPERTY() + bool OverideEnabled = false; + + ////TODO: We don't have access to Houdini attributes in HoudiniEngine/HoudiniEnginePrivatePCH. + //FString GetTempFolderArgument(ERuntimePackageMode PackageMode) const; + //FString GetBakeFolderArgument(ERuntimePackageMode PackageMode) const; + + //// Return the output path as either the temp or bake path, depending on the package mode. + //FString GetOutputFolderForPackageMode(ERuntimePackageMode PackageMode) const; + + /* + * Build a "standard" set of string formatting arguments that + * is typically used across HoudiniEngine path naming outputs. + * Note that each output type may contain additional named arguments + * that are not listed here. + * {out} - The output directory (varies depending on the package mode). + * {pkg} - The path to the destination package (varies depending on the package mode). + * {world} - Path the directory that contains the world. + * {hda_name} - Name of the HDA + * {guid} - guid of the HDA component + * @param PackageParams The output path for the current build mode (Temp / Bake). + * @param HACWorld The world in which the HDA component lives (typically Editor world). + * @param OutArgs The generated named arguments to be used for string formatting. + */ + + // Populate a map of named arguments from this FHoudiniPackageParams. + template + void UpdateTokensFromParams( + const UWorld* WorldContext, + const UObject* HACOrOuterComponent, + TMap& OutTokens) const + { + UpdateOutputPathTokens(PackageMode, OutTokens); + + if(IsValid(WorldContext)) + OutTokens.Add("world", ValueT( FPaths::GetPath(WorldContext->GetPathName()) )); + + if(HACOrOuterComponent && HACOrOuterComponent->GetOutermost()) + { + UWorld* World = HACOrOuterComponent->GetWorld(); + if(World) + { + FString LevelName; + if(World->IsPartitionedWorld()) + { + ULevel* CurrentLevel = World->GetCurrentLevel(); + if(CurrentLevel) + { + LevelName = CurrentLevel->GetOutermost()->GetPathName(); + } + } + else + { + LevelName = HACOrOuterComponent->GetOutermost()->GetPathName(); + } + OutTokens.Add("hda_level", ValueT(LevelName)); + } + } + + OutTokens.Add("object_name", ValueT( ObjectName )); + OutTokens.Add("object_id", ValueT( FString::FromInt(ObjectId) )); + OutTokens.Add("geo_id", ValueT( FString::FromInt(GeoId) )); + OutTokens.Add("part_id", ValueT( FString::FromInt(PartId) )); + OutTokens.Add("split_str", ValueT( SplitStr)); + OutTokens.Add("hda_name", ValueT( HoudiniAssetName )); + OutTokens.Add("hda_actor_name", ValueT( HoudiniAssetActorName )); + OutTokens.Add("pdg_topnet_name", ValueT( PDGTOPNetworkName )); + OutTokens.Add("pdg_topnode_name", ValueT( PDGTOPNodeName )); + OutTokens.Add("pdg_workitem_index", ValueT( FString::FromInt(PDGWorkItemIndex) )); + OutTokens.Add("pdg_workresult_array_index", ValueT( FString::FromInt(PDGWorkResultArrayIndex) )); + OutTokens.Add("guid", ValueT( ComponentGUID.ToString() )); + OutTokens.Add("guid8", ValueT( ComponentGUID.ToString().Left(8) )); + } + + template + void UpdateOutputPathTokens(EPackageMode InPackageMode, TMap& OutTokens) const + { + const FString PackagePath = GetPackagePath(); + + OutTokens.Add("temp", ValueT(TempCookFolder)); + OutTokens.Add("bake", ValueT(BakeFolder)); + + // `out_basepath` is useful if users want to organize their cook/bake assets + // different to the convention defined by GetPackagePath(). This would typically + // be combined with `unreal_level_path` during level path resolves. + switch (InPackageMode) + { + case EPackageMode::CookToTemp: + case EPackageMode::CookToLevel_Invalid: + OutTokens.Add("out_basepath", ValueT(TempCookFolder)); + break; + case EPackageMode::Bake: + OutTokens.Add("out_basepath", ValueT(BakeFolder)); + break; + } + + OutTokens.Add("out", ValueT(PackagePath)); + } + +}; + +``` + +`Source/HoudiniEngine/Private/HoudiniParameterTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterTranslator.h" + +#include "HoudiniApi.h" +#include "HoudiniEnginePrivatePCH.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineString.h" +#include "HoudiniInput.h" +#include "HoudiniNodeSyncComponent.h" +#include "HoudiniParameter.h" +#include "HoudiniParameterButton.h" +#include "HoudiniParameterButtonStrip.h" +#include "HoudiniParameterChoice.h" +#include "HoudiniParameterColor.h" +#include "HoudiniParameterFile.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterFolder.h" +#include "HoudiniParameterFolderList.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterLabel.h" +#include "HoudiniParameterMultiParm.h" +#include "HoudiniParameterOperatorPath.h" +#include "HoudiniParameterRamp.h" +#include "HoudiniParameterSeparator.h" +#include "HoudiniParameterString.h" +#include "HoudiniParameterToggle.h" + + + +// Default values for certain UI min and max parameter values +#define HAPI_UNREAL_PARAM_INT_UI_MIN 0 +#define HAPI_UNREAL_PARAM_INT_UI_MAX 10 +#define HAPI_UNREAL_PARAM_FLOAT_UI_MIN 0.0f +#define HAPI_UNREAL_PARAM_FLOAT_UI_MAX 10.0f + +// Some default parameter name +#define HAPI_UNREAL_PARAM_TRANSLATE "t" +#define HAPI_UNREAL_PARAM_ROTATE "r" +#define HAPI_UNREAL_PARAM_SCALE "s" +#define HAPI_UNREAL_PARAM_PIVOT "p" +#define HAPI_UNREAL_PARAM_UNIFORMSCALE "scale" + +bool +FHoudiniParameterTranslator::UpdateParameters( + HAPI_NodeId InNodeId, + UObject* InOuter, + TArray>& InParameters, + UHoudiniAsset* InHoudiniAsset, + const FString& InHapiAssetName, + bool bForceFullUpdate, + bool bCacheRampParms, + bool& bNeedToUpdateEditorProperties) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameters); + + TArray> NewParameters; + if (FHoudiniParameterTranslator::BuildAllParameters(InNodeId, InOuter, InParameters, NewParameters, true, bForceFullUpdate, InHoudiniAsset, InHapiAssetName, bCacheRampParms)) + { + // DO NOT MANUALLY DESTROY THE OLD/DANGLING PARAMETERS! + // This messes up unreal's Garbage collection and would cause crashes on duplication + + // Replace with the new parameters + InParameters = NewParameters; + +#if WITH_EDITORONLY_DATA + // Indicate we want to update the details panel after the parameter changes/updates + bNeedToUpdateEditorProperties = true; +#endif + } + + + return true; +} + + +bool +FHoudiniParameterTranslator::OnPreCookParameters(TArray>& InParams) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::OnPreCookParameters); + + // Call OnPreCook for all parameters. + // Parameters can use this to ensure that any cached / non-cooking state is properly + // synced before the cook starts (Looking at you, ramp parameters!) + for (UHoudiniParameter* Param : InParams) + { + if (!IsValid(Param)) + continue; + + Param->OnPreCook(); + } + + return true; +} + +// +bool +FHoudiniParameterTranslator::UpdateLoadedParameters( + HAPI_NodeId InNodeId, + TArray>& InParameters, + UObject* InOuter, + bool bForceFullUpdate, + bool bCacheRampParams, + bool& bNeedToUpdateEditorProperties) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateLoadedParameters); + + // Update all the parameters using the loaded parameter object + // We set "UpdateValues" to false because we do not want to "read" the parameter value + // from Houdini but keep the loaded value + + // Share AssetInfo if needed + bool bNeedToFetchAssetInfo = true; + HAPI_AssetInfo AssetInfo; + + // This is the first cook on loading after a save or duplication + for (int32 Idx = 0; Idx < InParameters.Num(); ++Idx) + { + UHoudiniParameter* Param = InParameters[Idx]; + if (!IsValid(Param)) + continue; + + switch(Param->GetParameterType()) + { + case EHoudiniParameterType::ColorRamp: + case EHoudiniParameterType::FloatRamp: + case EHoudiniParameterType::MultiParm: + { + // We need to sync the Ramp parameters first, so that their child parameters can be kept + if (bNeedToFetchAssetInfo) + { + FHoudiniApi::GetAssetInfo(FHoudiniEngine::Get().GetSession(), InNodeId, &AssetInfo); + bNeedToFetchAssetInfo = false; + } + + // TODO: Simplify this, should be handled in BuildAllParameters + SyncMultiParmValuesAtLoad(Param, InParameters, InNodeId, AssetInfo); + } + break; + + case EHoudiniParameterType::Button: + case EHoudiniParameterType::ButtonStrip: + { + // Do not trigger buttons upon loading + Param->MarkChanged(false); + } + break; + + default: + break; + } + } + + // This call to BuildAllParameters will keep all the loaded parameters (in the HAC's Parameters array) + // that are still present in the HDA, and keep their loaded value. + TArray> NewParameters; + // We don't need to fetch defaults from the asset definition for a loaded HAC + const UHoudiniAsset* const HoudiniAsset = nullptr; + const FString HoudiniAssetName = FString(); + if (FHoudiniParameterTranslator::BuildAllParameters( + InNodeId, + InOuter, + InParameters, + NewParameters, + false, + bForceFullUpdate, + HoudiniAsset, + HoudiniAssetName, + bCacheRampParams)) + { + // DO NOT DESTROY OLD PARAMS MANUALLY HERE + // This causes crashes upon duplication due to uncollected zombie objects... + // GC is supposed to handle this by itself + // Simply replace with the new parameters + InParameters = NewParameters; + +#if WITH_EDITORONLY_DATA + // Indicate we want to update the details panel after the parameter changes/updates + bNeedToUpdateEditorProperties = true; +#endif + } + + return true; +} + +bool +FHoudiniParameterTranslator::BuildAllParameters( + HAPI_NodeId AssetId, + class UObject* Outer, + TArray>& CurrentParameters, + TArray>& NewParameters, + bool bUpdateValues, + bool InForceFullUpdate, + const UHoudiniAsset* InHoudiniAsset, + const FString& InHoudiniAssetName, + bool bCacheRampParms) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::BuildAllParameters); + + // Ensure the asset has a valid node ID or a valid HoudiniAsset + const bool bIsAssetValid = IsValid(InHoudiniAsset); + if (AssetId < 0 && !bIsAssetValid) + { + return false; + } + + int32 ParmCount = 0; + + // Default value counts and arrays for if we need to fetch those from Houdini. + int DefaultIntValueCount = 0; + int DefaultFloatValueCount = 0; + int DefaultStringValueCount = 0; + int DefaultChoiceValueCount = 0; + TArray DefaultIntValues; + TArray DefaultFloatValues; + TArray DefaultStringValues; + TArray DefaultChoiceValues; + + HAPI_NodeId NodeId = -1; + HAPI_AssetLibraryId AssetLibraryId = -1; + FString HoudiniAssetName; + + if (AssetId >= 0) + { + // Get the asset's info + HAPI_AssetInfo AssetInfo; + FHoudiniApi::AssetInfo_Init(&AssetInfo); + HAPI_Result Result = FHoudiniApi::GetAssetInfo( + FHoudiniEngine::Get().GetSession(), AssetId, &AssetInfo); + + if (Result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_ERROR(TEXT("Hapi failed: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + return false; + } + + NodeId = AssetInfo.nodeId; + + // .. the asset's node info + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), AssetInfo.nodeId, &NodeInfo), false); + + ParmCount = NodeInfo.parmCount; + } + else + { + if (!FHoudiniEngineUtils::LoadHoudiniAsset(InHoudiniAsset, AssetLibraryId) ) + { + HOUDINI_LOG_ERROR(TEXT("Cancelling BuildAllParameters - could not load Houdini Asset.")); + return false; + } + + // Handle hda files that contain multiple assets + TArray AssetNames; + if (!FHoudiniEngineUtils::GetSubAssetNames(AssetLibraryId, AssetNames)) + { + HOUDINI_LOG_ERROR(TEXT("Cancelling BuildAllParameters - unable to retrieve asset names.")); + return false; + } + + if (AssetNames.Num() == 0) + { + HOUDINI_LOG_ERROR(TEXT("Cancelling BuildAllParameters - unable to retrieve asset names.")); + return false; + } + + // If no InHoudiniAssetName was specified, pick the first asset from the library + if (InHoudiniAssetName.IsEmpty()) + { + const FHoudiniEngineString HoudiniEngineString(AssetNames[0]); + HoudiniEngineString.ToFString(HoudiniAssetName); + } + else + { + // Ensure that the specified asset name is in the library + for (const HAPI_StringHandle& Handle : AssetNames) + { + const FHoudiniEngineString HoudiniEngineString(Handle); + FString AssetNameStr; + HoudiniEngineString.ToFString(AssetNameStr); + if (AssetNameStr == InHoudiniAssetName) + { + HoudiniAssetName = AssetNameStr; + break; + } + } + } + + if (HoudiniAssetName.IsEmpty()) + { + HOUDINI_LOG_ERROR(TEXT("Cancelling BuildAllParametersFromAssetDefinition - could not find asset in library.")); + return false; + } + + HAPI_Result Result = FHoudiniApi::GetAssetDefinitionParmCounts( + FHoudiniEngine::Get().GetSession(), AssetLibraryId, H_TCHAR_TO_UTF8(*HoudiniAssetName), &ParmCount, + &DefaultIntValueCount, &DefaultFloatValueCount, &DefaultStringValueCount, &DefaultChoiceValueCount); + + if (Result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_ERROR(TEXT("Hapi failed: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + return false; + } + + if (ParmCount > 0) + { + // Allocate space in the default value arrays + // Fetch default values from HAPI + DefaultIntValues.SetNumZeroed(DefaultIntValueCount); + DefaultFloatValues.SetNumZeroed(DefaultFloatValueCount); + DefaultStringValues.SetNumZeroed(DefaultStringValueCount); + DefaultChoiceValues.SetNumZeroed(DefaultChoiceValueCount); + + Result = FHoudiniApi::GetAssetDefinitionParmValues( + FHoudiniEngine::Get().GetSession(), AssetLibraryId, H_TCHAR_TO_UTF8(*HoudiniAssetName), + DefaultIntValues.GetData(), 0, DefaultIntValueCount, + DefaultFloatValues.GetData(), 0, DefaultFloatValueCount, + false, DefaultStringValues.GetData(), 0, DefaultStringValueCount, + DefaultChoiceValues.GetData(), 0, DefaultChoiceValueCount); + + if (Result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_ERROR(TEXT("Hapi failed: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + return false; + } + } + } + + NewParameters.Empty(); + if (ParmCount == 0) + { + // The asset doesnt have any parameter, we're done. + return true; + } + else if (ParmCount < 0) + { + // Invalid parm count + return false; + } + + // Retrieve all the parameter infos either from instantiated node or from asset definition. + TArray ParmInfos; + ParmInfos.SetNumUninitialized(ParmCount); + + if (AssetId >= 0) + { + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParameters( + FHoudiniEngine::Get().GetSession(), NodeId, &ParmInfos[0], 0, ParmCount), false); + } + else + { + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetAssetDefinitionParmInfos( + FHoudiniEngine::Get().GetSession(), AssetLibraryId, H_TCHAR_TO_UTF8(*HoudiniAssetName), &ParmInfos[0], 0, ParmCount), false); + } + + // Create a name lookup cache for the current parameters + // Use an array has in some cases, multiple parameters can have the same name! + TMap> CurrentParametersByName; + CurrentParametersByName.Reserve(CurrentParameters.Num()); + for (const auto& Parm : CurrentParameters) + { + if (!IsValid(Parm)) + continue; + + FString ParmName = Parm->GetParameterName(); + TArray* FoundParmArray = CurrentParametersByName.Find(ParmName); + if (!FoundParmArray) + { + // Create a new array + TArray ParmArray; + ParmArray.Add(Parm); + + // add the new array to the map + CurrentParametersByName.Add(ParmName, ParmArray); + } + else + { + // add this parameter to the existing array + FoundParmArray->Add(Parm); + } + } + + // Create properties for parameters. + TMap FloatRampsToIndex; + TMap ColorRampsToIndex; + TArray NewParmIds; + TArray AllMultiParams; + for (int32 ParamIdx = 0; ParamIdx < ParmCount; ++ParamIdx) + { + // Retrieve param info at this index. + const HAPI_ParmInfo & ParmInfo = ParmInfos[ParamIdx]; + + // If the parameter is corrupt, skip it + if (ParmInfo.id < 0 || ParmInfo.childIndex < 0) + { + HOUDINI_LOG_WARNING(TEXT("Corrupt parameter %d detected, skipping."), ParamIdx); + continue; + } + + // If the parameter is invisible, skip it. + //if (ParmInfo.invisible) + // continue; + + // Check if any parent folder of this parameter is invisible + bool SkipParm = false; + bool ParentFolderVisible = true; + HAPI_ParmId ParentId = ParmInfo.parentId; + while (ParentId > 0 && !SkipParm) + { + if (const HAPI_ParmInfo* ParentInfoPtr = ParmInfos.FindByPredicate([=](const HAPI_ParmInfo& Info) { + return Info.id == ParentId; + })) + { + // We now keep invisible parameters but show/hid them in UpdateParameterFromInfo(). + if (ParentInfoPtr->invisible && ParentInfoPtr->type == HAPI_PARMTYPE_FOLDER) + ParentFolderVisible = false; + + // Prevent endless loops! + if (ParentId != ParentInfoPtr->parentId) + ParentId = ParentInfoPtr->parentId; + else + ParentId = -1; + } + else + { + HOUDINI_LOG_ERROR(TEXT("Could not find parent of parameter %d"), ParmInfo.id); + SkipParm = true; + } + } + + if (SkipParm) + continue; + + // See if this parameter has already been created. + // We can't use the HAPI_ParmId because it is not unique to parameter instances, + // so instead, try to find the existing parameter by name using the lookup table + FString NewParmName; + FHoudiniEngineString(ParmInfo.nameSH).ToFString(NewParmName); + + EHoudiniParameterType ParmType = EHoudiniParameterType::Invalid; + FHoudiniParameterTranslator::GetParmTypeFromParmInfo(ParmInfo, ParmType); + + // Not using the name lookup map! + UHoudiniParameter* FoundHoudiniParameter = nullptr; + TArray* MatchingParameters = CurrentParametersByName.Find(NewParmName); + if ((ParmType != EHoudiniParameterType::Invalid) && MatchingParameters) + { + //for (auto& CurrentParm : *MatchingParameters) + for(int32 Idx = MatchingParameters->Num() - 1; Idx >= 0; Idx--) + { + UHoudiniParameter* CurrentParm = (*MatchingParameters)[Idx]; + if (!CurrentParm) + continue; + + // First Check the parameter types match + if (ParmType != CurrentParm->GetParameterType()) + { + // Types do not match + continue; + } + + // Then, make sure the tuple size hasn't changed + if (CurrentParm->GetTupleSize() != ParmInfo.size) + { + // Tuple do not match + continue; + } + + if (!CheckParameterTypeAndClassMatch(CurrentParm, ParmType)) + { + // Wrong class + continue; + } + + // We can reuse this parameter + FoundHoudiniParameter = CurrentParm; + + // Remove it from the array/map + MatchingParameters->RemoveAt(Idx); + if (MatchingParameters->Num() <= 0) + CurrentParametersByName.Remove(NewParmName); + + break; + } + } + + UHoudiniParameter * HoudiniAssetParameter = nullptr; + if (FoundHoudiniParameter) + { + // We can reuse the parameter we found + HoudiniAssetParameter = FoundHoudiniParameter; + + // Transfer param object from current map to new map + CurrentParameters.Remove(HoudiniAssetParameter); + + // Do a fast update of this parameter + if (!FHoudiniParameterTranslator::UpdateParameterFromInfo( + HoudiniAssetParameter, NodeId, ParmInfo, InForceFullUpdate, bUpdateValues, + AssetId >= 0 ? nullptr : &DefaultIntValues, + AssetId >= 0 ? nullptr : &DefaultFloatValues, + AssetId >= 0 ? nullptr : &DefaultStringValues, + AssetId >= 0 ? nullptr : &DefaultChoiceValues)) + continue; + + // Reset the states of ramp parameters. + switch (HoudiniAssetParameter->GetParameterType()) + { + + case EHoudiniParameterType::FloatRamp: + { + UHoudiniParameterRampFloat* FloatRampParam = Cast(HoudiniAssetParameter); + if (FloatRampParam) + { + // Record float and color ramps for further processing (creating their Points arrays) + FloatRampsToIndex.Add(FloatRampParam, NewParameters.Num()); + if (bCacheRampParms) + FloatRampParam->bCaching = false; + } + + break; + } + + case EHoudiniParameterType::ColorRamp: + { + UHoudiniParameterRampColor* ColorRampParam = Cast(HoudiniAssetParameter); + if (ColorRampParam) + { + // Record float and color ramps for further processing (creating their Points arrays) + ColorRampsToIndex.Add(ColorRampParam, NewParameters.Num()); + if (bCacheRampParms) + ColorRampParam->bCaching = false; + } + + break; + } + } + + } + else + { + // Create a new parameter object of the appropriate type + HoudiniAssetParameter = CreateTypedParameter(Outer, ParmType, NewParmName); + // Fully update this parameter + if (!FHoudiniParameterTranslator::UpdateParameterFromInfo( + HoudiniAssetParameter, NodeId, ParmInfo, true, true, + AssetId >= 0 ? nullptr : &DefaultIntValues, + AssetId >= 0 ? nullptr : &DefaultFloatValues, + AssetId >= 0 ? nullptr : &DefaultStringValues, + AssetId >= 0 ? nullptr : &DefaultChoiceValues)) + continue; + + // Record float and color ramps for further processing (creating their Points arrays) + const EHoudiniParameterType NewParamType = HoudiniAssetParameter->GetParameterType(); + if (NewParamType == EHoudiniParameterType::FloatRamp) + { + UHoudiniParameterRampFloat* FloatRampParam = Cast(HoudiniAssetParameter); + if (FloatRampParam) + FloatRampsToIndex.Add(FloatRampParam, NewParameters.Num()); + } + else if (NewParamType == EHoudiniParameterType::ColorRamp) + { + UHoudiniParameterRampColor* ColorRampParam = Cast(HoudiniAssetParameter); + if (ColorRampParam) + ColorRampsToIndex.Add(ColorRampParam, NewParameters.Num()); + } + } + + HoudiniAssetParameter->SetVisibleParent(ParentFolderVisible); + + // Add the new parameters + NewParameters.Add(HoudiniAssetParameter); + NewParmIds.Add(ParmInfo.id); + + // Check if the parameter is a direct child of a multiparam. + if (HoudiniAssetParameter->GetParameterType() == EHoudiniParameterType::MultiParm) + AllMultiParams.Add(HoudiniAssetParameter->GetParmId()); + + if (AllMultiParams.Contains(HoudiniAssetParameter->GetParentParmId())) + { + HoudiniAssetParameter->SetIsDirectChildOfMultiParm(true); + + // Treat the folderlist whose direct parent is a multi param as a multi param too. + if (HoudiniAssetParameter->GetParameterType() == EHoudiniParameterType::FolderList) + AllMultiParams.Add(HoudiniAssetParameter->GetParmId()); + } + } + + // Assign folder type to all folderlists, + // if the first child of the folderlist is Tab or Radio button, set the bIsTabMenu of the folderlistParam to be true, otherwise false + for (int32 Idx = 0; Idx < NewParameters.Num(); ++Idx) + { + UHoudiniParameter * CurParam = NewParameters[Idx]; + if (!IsValid(CurParam)) + continue; + + if (CurParam->GetParameterType() == EHoudiniParameterType::FolderList) + { + UHoudiniParameterFolderList* CurFolderList = Cast(CurParam); + if (!IsValid(CurFolderList)) + continue; + + int32 FirstChildIdx = Idx + 1; + if (!NewParameters.IsValidIndex(FirstChildIdx)) + continue; + + UHoudiniParameterFolder* FirstChildFolder = Cast(NewParameters[FirstChildIdx]); + if (!IsValid(FirstChildFolder)) + continue; + + if (FirstChildFolder->GetFolderType() == EHoudiniFolderParameterType::Radio || + FirstChildFolder->GetFolderType() == EHoudiniFolderParameterType::Tabs) + { + // If this is the first time build + if (!CurFolderList->IsTabMenu()) + { + // Set the folderlist to be tabs + CurFolderList->SetIsTabMenu(true); + // Select the first child tab folder by default. + FirstChildFolder->SetChosen(true); + } + } + else + CurFolderList->SetIsTabMenu(false); + } + } + + // Create / update the Points arrays for the ramp parameters + if (FloatRampsToIndex.Num() > 0) + { + for (TPair const& Entry : FloatRampsToIndex) + { + UHoudiniParameterRampFloat* const RampFloatParam = Entry.Key; + const int32 ParamIndex = Entry.Value; + if (!IsValid(RampFloatParam)) + continue; + + RampFloatParam->UpdatePointsArray(NewParameters, ParamIndex + 1); + } + } + if (ColorRampsToIndex.Num() > 0) + { + for (TPair const& Entry : ColorRampsToIndex) + { + UHoudiniParameterRampColor* const RampColorParam = Entry.Key; + const int32 ParamIndex = Entry.Value; + if (!IsValid(RampColorParam)) + continue; + + RampColorParam->UpdatePointsArray(NewParameters, ParamIndex + 1); + } + } + + return true; +} + + +void +FHoudiniParameterTranslator::GetParmTypeFromParmInfo( + const HAPI_ParmInfo& ParmInfo, + EHoudiniParameterType& ParmType) +{ + ParmType = EHoudiniParameterType::Invalid; + //ParmValueType = EHoudiniParameterValueType::Invalid; + + switch (ParmInfo.type) + { + case HAPI_PARMTYPE_BUTTON: + ParmType = EHoudiniParameterType::Button; + //ParmValueType = EHoudiniParameterValueType::Int; + break; + + case HAPI_PARMTYPE_STRING: + { + if (ParmInfo.choiceCount > 0) + { + ParmType = EHoudiniParameterType::StringChoice; + //ParmValueType = EHoudiniParameterValueType::String; + } + else + { + ParmType = EHoudiniParameterType::String; + //ParmValueType = EHoudiniParameterValueType::String; + } + break; + } + + case HAPI_PARMTYPE_INT: + { + if (ParmInfo.scriptType == HAPI_PrmScriptType::HAPI_PRM_SCRIPT_TYPE_BUTTONSTRIP) + { + ParmType = EHoudiniParameterType::ButtonStrip; + break; + } + + if (ParmInfo.choiceCount > 0) + { + ParmType = EHoudiniParameterType::IntChoice; + //ParmValueType = EHoudiniParameterValueType::Int; + } + else + { + ParmType = EHoudiniParameterType::Int; + //ParmValueType = EHoudiniParameterValueType::Int; + } + break; + } + + case HAPI_PARMTYPE_FLOAT: + { + ParmType = EHoudiniParameterType::Float; + //ParmValueType = EHoudiniParameterValueType::Float; + break; + } + + case HAPI_PARMTYPE_TOGGLE: + { + ParmType = EHoudiniParameterType::Toggle; + //ParmValueType = EHoudiniParameterValueType::Int; + break; + } + + case HAPI_PARMTYPE_COLOR: + { + ParmType = EHoudiniParameterType::Color; + //ParmValueType = EHoudiniParameterValueType::Float; + break; + } + + case HAPI_PARMTYPE_LABEL: + { + ParmType = EHoudiniParameterType::Label; + //ParmValueType = EHoudiniParameterValueType::String; + break; + } + + case HAPI_PARMTYPE_SEPARATOR: + { + ParmType = EHoudiniParameterType::Separator; + //ParmValueType = EHoudiniParameterValueType::None; + break; + } + + case HAPI_PARMTYPE_FOLDERLIST: + { + ParmType = EHoudiniParameterType::FolderList; + //ParmValueType = EHoudiniParameterValueType::None; + break; + } + + // Treat radio folders as tab folders for now + case HAPI_PARMTYPE_FOLDERLIST_RADIO: + { + ParmType = EHoudiniParameterType::FolderList; + break; + } + + case HAPI_PARMTYPE_FOLDER: + { + ParmType = EHoudiniParameterType::Folder; + //ParmValueType = EHoudiniParameterValueType::None; + break; + } + + case HAPI_PARMTYPE_MULTIPARMLIST: + { + if (HAPI_RAMPTYPE_FLOAT == ParmInfo.rampType) + { + ParmType = EHoudiniParameterType::FloatRamp; + //ParmValueType = EHoudiniParameterValueType::Float; + } + else if (HAPI_RAMPTYPE_COLOR == ParmInfo.rampType) + { + ParmType = EHoudiniParameterType::ColorRamp; + //ParmValueType = EHoudiniParameterValueType::Float; + } + else + { + ParmType = EHoudiniParameterType::MultiParm; + //ParmValueType = EHoudiniParameterValueType::Int; + } + break; + } + + case HAPI_PARMTYPE_PATH_FILE: + { + ParmType = EHoudiniParameterType::File; + //ParmValueType = EHoudiniParameterValueType::String; + break; + } + + case HAPI_PARMTYPE_PATH_FILE_DIR: + { + ParmType = EHoudiniParameterType::FileDir; + //ParmValueType = EHoudiniParameterValueType::String; + break; + } + + case HAPI_PARMTYPE_PATH_FILE_GEO: + { + ParmType = EHoudiniParameterType::FileGeo; + //ParmValueType = EHoudiniParameterValueType::String; + break; + } + + case HAPI_PARMTYPE_PATH_FILE_IMAGE: + { + ParmType = EHoudiniParameterType::FileImage; + //ParmValueType = EHoudiniParameterValueType::String; + break; + } + + case HAPI_PARMTYPE_NODE: + { + if (ParmInfo.inputNodeType == HAPI_NODETYPE_ANY || + ParmInfo.inputNodeType == HAPI_NODETYPE_SOP || + ParmInfo.inputNodeType == HAPI_NODETYPE_OBJ || + ParmInfo.inputNodeType == HAPI_NODETYPE_COP) + { + // Get the parm name to avoid considering the default object path parameters as inputs + FString ParmName = FHoudiniEngineUtils::HapiGetString(ParmInfo.nameSH); + if (ParmName == "constraints_path" + || ParmName == "lookatpath" + || ParmName == "lookupobjpath" + || ParmName == "pathobjpath") + { + ParmType = EHoudiniParameterType::String; + } + else + { + ParmType = EHoudiniParameterType::Input; + } + } + else + { + ParmType = EHoudiniParameterType::String; + } + break; + } + + default: + { + // Just ignore unsupported types for now. + HOUDINI_LOG_WARNING(TEXT("Parameter Type (%d) is unsupported"), static_cast(ParmInfo.type)); + break; + } + } +} + +UClass* +FHoudiniParameterTranslator::GetDesiredParameterClass(const HAPI_ParmInfo& ParmInfo) +{ + UClass* FoundClass = nullptr; + + switch (ParmInfo.type) + { + case HAPI_PARMTYPE_STRING: + if (!ParmInfo.choiceCount) + FoundClass = UHoudiniParameterString::StaticClass(); + else + FoundClass = UHoudiniParameterChoice ::StaticClass(); + break; + + case HAPI_PARMTYPE_INT: + if (!ParmInfo.choiceCount) + FoundClass = UHoudiniParameterInt::StaticClass(); + else + FoundClass = UHoudiniParameterChoice::StaticClass(); + break; + + case HAPI_PARMTYPE_FLOAT: + FoundClass = UHoudiniParameterFloat::StaticClass(); + break; + + case HAPI_PARMTYPE_TOGGLE: + FoundClass = UHoudiniParameterToggle::StaticClass(); + break; + + case HAPI_PARMTYPE_COLOR: + FoundClass = UHoudiniParameterColor::StaticClass(); + break; + + case HAPI_PARMTYPE_LABEL: + FoundClass = UHoudiniParameterLabel::StaticClass(); + break; + + case HAPI_PARMTYPE_BUTTON: + FoundClass = UHoudiniParameterButton::StaticClass(); + break; + + case HAPI_PARMTYPE_SEPARATOR: + FoundClass = UHoudiniParameterSeparator::StaticClass(); + break; + + case HAPI_PARMTYPE_FOLDERLIST: + FoundClass = UHoudiniParameterFolderList::StaticClass(); + break; + + case HAPI_PARMTYPE_FOLDER: + FoundClass = UHoudiniParameterFolder::StaticClass(); + break; + + case HAPI_PARMTYPE_MULTIPARMLIST: + { + if (HAPI_RAMPTYPE_FLOAT == ParmInfo.rampType || HAPI_RAMPTYPE_COLOR == ParmInfo.rampType) + FoundClass = UHoudiniParameterRampFloat::StaticClass(); + else if (HAPI_RAMPTYPE_COLOR == ParmInfo.rampType) + FoundClass = UHoudiniParameterRampColor::StaticClass(); + } + break; + + case HAPI_PARMTYPE_PATH_FILE: + FoundClass = UHoudiniParameterFile::StaticClass(); + break; + case HAPI_PARMTYPE_PATH_FILE_DIR: + FoundClass = UHoudiniParameterFile::StaticClass(); + break; + case HAPI_PARMTYPE_PATH_FILE_GEO: + FoundClass = UHoudiniParameterFile::StaticClass(); + break; + case HAPI_PARMTYPE_PATH_FILE_IMAGE: + FoundClass = UHoudiniParameterFile::StaticClass(); + break; + + case HAPI_PARMTYPE_NODE: + if (ParmInfo.inputNodeType == HAPI_NODETYPE_ANY || + ParmInfo.inputNodeType == HAPI_NODETYPE_SOP || + ParmInfo.inputNodeType == HAPI_NODETYPE_OBJ) + { + FoundClass = UHoudiniParameter::StaticClass(); + } + else + { + FoundClass = UHoudiniParameterString::StaticClass(); + } + break; + } + + if (FoundClass == nullptr) + return UHoudiniParameter::StaticClass(); + + return FoundClass; +} + +bool +FHoudiniParameterTranslator::CheckParameterTypeAndClassMatch(UHoudiniParameter* Parameter, const EHoudiniParameterType& ParmType) +{ + UClass* FoundClass = Parameter->GetClass(); + bool FailedTypeCheck = true; + + switch (ParmType) + { + case EHoudiniParameterType::Invalid: + { + FailedTypeCheck = true; + break; + } + + case EHoudiniParameterType::Button: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterButton >(); + break; + } + + case EHoudiniParameterType::Color: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterColor >(); + break; + } + + case EHoudiniParameterType::ColorRamp: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterRampColor >(); + break; + } + case EHoudiniParameterType::FloatRamp: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterRampFloat >(); + break; + } + + case EHoudiniParameterType::File: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterFile >(); + break; + } + case EHoudiniParameterType::FileDir: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterFile >(); + break; + } + case EHoudiniParameterType::FileGeo: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterFile >(); + break; + } + case EHoudiniParameterType::FileImage: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterFile >(); + break; + } + + case EHoudiniParameterType::Float: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterFloat >(); + break; + } + + case EHoudiniParameterType::Folder: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterFolder >(); + break; + } + + case EHoudiniParameterType::FolderList: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterFolderList >(); + break; + } + + case EHoudiniParameterType::Input: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterOperatorPath >(); + break; + } + + case EHoudiniParameterType::Int: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterInt >(); + break; + } + + case EHoudiniParameterType::IntChoice: + case EHoudiniParameterType::StringChoice: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterChoice >(); + break; + } + + case EHoudiniParameterType::Label: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterLabel >(); + break; + } + + case EHoudiniParameterType::MultiParm: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterMultiParm >(); + break; + } + + case EHoudiniParameterType::Separator: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterSeparator >(); + break; + } + + case EHoudiniParameterType::String: + case EHoudiniParameterType::StringAssetRef: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterString >(); + break; + } + + case EHoudiniParameterType::Toggle: + { + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterToggle >(); + break; + } + }; + + return !FailedTypeCheck; +} +/* +bool +FHoudiniParameterTranslator::CheckParameterClassAndInfoMatch(UHoudiniParameter* Parameter, const HAPI_ParmInfo& ParmInfo) +{ + if (!IsValid(Parameter)) + return false; + + UClass* FoundClass = Parameter->GetClass(); + bool FailedTypeCheck = true; + switch (ParmInfo.type) + { + case HAPI_PARMTYPE_STRING: + if (!ParmInfo.choiceCount) + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterString >(); + else + FailedTypeCheck &= !FoundClass->IsChildOf< UHoudiniParameterChoice >(); + break; + + case HAPI_PARMTYPE_INT: + if (!ParmInfo.choiceCount) + FailedTypeCheck &= !FoundClass->IsChildOf(); + else + FailedTypeCheck &= !FoundClass->IsChildOf(); + break; + + case HAPI_PARMTYPE_FLOAT: + FailedTypeCheck &= !FoundClass->IsChildOf(); + break; + + case HAPI_PARMTYPE_TOGGLE: + FailedTypeCheck &= !FoundClass->IsChildOf(); + break; + + case HAPI_PARMTYPE_COLOR: + FailedTypeCheck &= !FoundClass->IsChildOf(); + break; + + case HAPI_PARMTYPE_LABEL: + FailedTypeCheck &= !FoundClass->IsChildOf(); + break; + + case HAPI_PARMTYPE_BUTTON: + FailedTypeCheck &= !FoundClass->IsChildOf(); + break; + + case HAPI_PARMTYPE_SEPARATOR: + FailedTypeCheck &= !FoundClass->IsChildOf(); + break; + + case HAPI_PARMTYPE_FOLDERLIST: + FailedTypeCheck &= !FoundClass->IsChildOf(); + break; + + case HAPI_PARMTYPE_FOLDER: + FailedTypeCheck &= !FoundClass->IsChildOf(); + break; + + case HAPI_PARMTYPE_MULTIPARMLIST: + if (HAPI_RAMPTYPE_FLOAT == ParmInfo.rampType || HAPI_RAMPTYPE_COLOR == ParmInfo.rampType) + { + FailedTypeCheck &= !FoundClass->IsChildOf(); + } + break; + + case HAPI_PARMTYPE_PATH_FILE: + case HAPI_PARMTYPE_PATH_FILE_DIR: + case HAPI_PARMTYPE_PATH_FILE_GEO: + case HAPI_PARMTYPE_PATH_FILE_IMAGE: + FailedTypeCheck &= !FoundClass->IsChildOf(); + break; + + case HAPI_PARMTYPE_NODE: + if (ParmInfo.inputNodeType == HAPI_NODETYPE_ANY || + ParmInfo.inputNodeType == HAPI_NODETYPE_SOP || + ParmInfo.inputNodeType == HAPI_NODETYPE_OBJ) + { + FailedTypeCheck &= !FoundClass->IsChildOf(); + } + else + { + FailedTypeCheck &= !FoundClass->IsChildOf(); + } + break; + } + + return FailedTypeCheck; +} +*/ + +UHoudiniParameter * +FHoudiniParameterTranslator::CreateTypedParameter(UObject * Outer, const EHoudiniParameterType& ParmType, const FString& ParmName) +{ + UHoudiniParameter* HoudiniParameter = nullptr; + // Create a parameter of the desired type + switch (ParmType) + { + case EHoudiniParameterType::Button: + HoudiniParameter = UHoudiniParameterButton::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::ButtonStrip: + HoudiniParameter = UHoudiniParameterButtonStrip::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::Color: + HoudiniParameter = UHoudiniParameterColor::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::ColorRamp: + HoudiniParameter = UHoudiniParameterRampColor::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::FloatRamp: + HoudiniParameter = UHoudiniParameterRampFloat::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::File: + HoudiniParameter = UHoudiniParameterFile::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::FileDir: + HoudiniParameter = UHoudiniParameterFile::Create(Outer, ParmName); + HoudiniParameter->SetParameterType(EHoudiniParameterType::FileDir); + break; + + case EHoudiniParameterType::FileGeo: + HoudiniParameter = UHoudiniParameterFile::Create(Outer, ParmName); + HoudiniParameter->SetParameterType(EHoudiniParameterType::FileGeo); + break; + + case EHoudiniParameterType::FileImage: + HoudiniParameter = UHoudiniParameterFile::Create(Outer, ParmName); + HoudiniParameter->SetParameterType(EHoudiniParameterType::FileImage); + break; + + case EHoudiniParameterType::Float: + HoudiniParameter = UHoudiniParameterFloat::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::Folder: + HoudiniParameter = UHoudiniParameterFolder::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::FolderList: + HoudiniParameter = UHoudiniParameterFolderList::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::Input: + // Input parameter simply use the base class as all the processing is handled by UHoudiniInput + HoudiniParameter = UHoudiniParameterOperatorPath::Create(Outer, ParmName); + HoudiniParameter->SetParameterType(ParmType); + break; + + case EHoudiniParameterType::Int: + HoudiniParameter = UHoudiniParameterInt::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::IntChoice: + HoudiniParameter = UHoudiniParameterChoice::Create(Outer, ParmName, EHoudiniParameterType::IntChoice); + break; + + case EHoudiniParameterType::StringChoice: + HoudiniParameter = UHoudiniParameterChoice::Create(Outer, ParmName, EHoudiniParameterType::StringChoice); + break; + + case EHoudiniParameterType::Label: + HoudiniParameter = UHoudiniParameterLabel::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::MultiParm: + HoudiniParameter = UHoudiniParameterMultiParm::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::Separator: + HoudiniParameter = UHoudiniParameterSeparator::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::String: + case EHoudiniParameterType::StringAssetRef: + HoudiniParameter = UHoudiniParameterString::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::Toggle: + HoudiniParameter = UHoudiniParameterToggle::Create(Outer, ParmName); + break; + + case EHoudiniParameterType::Invalid: + // TODO handle invalid params + HoudiniParameter = UHoudiniParameter::Create(Outer, ParmName); + break; + } + + return HoudiniParameter; +} + +bool +FHoudiniParameterTranslator::UpdateParameterFromInfo( + UHoudiniParameter * HoudiniParameter, + HAPI_NodeId InNodeId, + const HAPI_ParmInfo& ParmInfo, + bool bFullUpdate, + bool bUpdateValue, + const TArray* DefaultIntValues, + const TArray* DefaultFloatValues, + const TArray* DefaultStringValues, + const TArray* DefaultChoiceValues) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo); + + if (!IsValid(HoudiniParameter)) + return false; + + // Copy values from the ParmInfos + const bool bHasValidNodeId = InNodeId >= 0; + if (bHasValidNodeId) + HoudiniParameter->SetNodeId(InNodeId); + HoudiniParameter->SetParmId(ParmInfo.id); + HoudiniParameter->SetParentParmId(ParmInfo.parentId); + + HoudiniParameter->SetChildIndex(ParmInfo.childIndex); + HoudiniParameter->SetTagCount(ParmInfo.tagCount); + HoudiniParameter->SetTupleSize(ParmInfo.size); + + HoudiniParameter->SetChoiceListType( + static_cast(ParmInfo.choiceListType)); + + HoudiniParameter->SetVisible(!ParmInfo.invisible); + HoudiniParameter->SetDisabled(ParmInfo.disabled); + HoudiniParameter->SetSpare(ParmInfo.spare); + HoudiniParameter->SetJoinNext(ParmInfo.joinNext); + HoudiniParameter->SetLabelVisible(!ParmInfo.labelNone); + + HoudiniParameter->SetTagCount(ParmInfo.tagCount); + HoudiniParameter->SetIsChildOfMultiParm(ParmInfo.isChildOfMultiParm); + + UHoudiniParameterMultiParm* MultiParm = Cast(HoudiniParameter); + if(MultiParm) + MultiParm->InstanceStartOffset = ParmInfo.instanceStartOffset; + + + // Get the parameter type + EHoudiniParameterType ParmType = HoudiniParameter->GetParameterType(); + // We need to set string values from the parmInfo + if (bFullUpdate) + { + FString Name; + { + // Name + FHoudiniEngineString HoudiniEngineStringName(ParmInfo.nameSH); + if (HoudiniEngineStringName.ToFString(Name)) + HoudiniParameter->SetParameterName(Name); + } + + { + // Label + FString Label; + FHoudiniEngineString HoudiniEngineStringLabel(ParmInfo.labelSH); + if (HoudiniEngineStringLabel.ToFString(Label)) + HoudiniParameter->SetParameterLabel(Label); + } + + { + // Help + FString Help; + FHoudiniEngineString HoudiniEngineStringHelp(ParmInfo.helpSH); + if (HoudiniEngineStringHelp.ToFString(Help)) + HoudiniParameter->SetParameterHelp(Help); + } + + if (bHasValidNodeId && + (ParmType == EHoudiniParameterType::String + || ParmType == EHoudiniParameterType::Int + || ParmType == EHoudiniParameterType::Float + || ParmType == EHoudiniParameterType::Toggle + || ParmType == EHoudiniParameterType::Color)) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__Expr); + + // See if the parm has an expression + int32 TupleIdx = ParmInfo.intValuesIndex; + bool bHasExpression = false; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::ParmHasExpression( + FHoudiniEngine::Get().GetSession(), InNodeId, + H_TCHAR_TO_UTF8(*Name), TupleIdx, &bHasExpression)) + { + // ? + } + + FString ParmExprString = TEXT(""); + if (bHasExpression) + { + // Try to get the expression's value + HAPI_StringHandle StringHandle; + if (HAPI_RESULT_SUCCESS == FHoudiniApi::GetParmExpression( + FHoudiniEngine::Get().GetSession(), InNodeId, + H_TCHAR_TO_UTF8(*Name), TupleIdx, &StringHandle)) + { + FHoudiniEngineString HoudiniEngineString(StringHandle); + HoudiniEngineString.ToFString(ParmExprString); + } + + // Check if we actually have an expression + // String parameters return true even if they do not have one + bHasExpression = ParmExprString.Len() > 0; + + } + + HoudiniParameter->SetHasExpression(bHasExpression); + HoudiniParameter->SetExpression(ParmExprString); + } + else + { + HoudiniParameter->SetHasExpression(false); + HoudiniParameter->SetExpression(FString()); + } + + // Get parameter tags. + if (bHasValidNodeId) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__Tags); + + int32 TagCount = HoudiniParameter->GetTagCount(); + for (int32 Idx = 0; Idx < TagCount; ++Idx) + { + HAPI_StringHandle TagNameSH; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetParmTagName( + FHoudiniEngine::Get().GetSession(), + InNodeId, ParmInfo.id, Idx, &TagNameSH)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to retrive parameter tag name: parmId: %d, tag index: %d"), ParmInfo.id, Idx); + continue; + } + + FString NameString = TEXT(""); + FHoudiniEngineString::ToFString(TagNameSH, NameString); + if (NameString.IsEmpty()) + { + HOUDINI_LOG_WARNING(TEXT("Failed to retrive parameter tag name: parmId: %d, tag index: %d"), ParmInfo.id, Idx); + continue; + } + + HAPI_StringHandle TagValueSH; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetParmTagValue( + FHoudiniEngine::Get().GetSession(), + InNodeId, ParmInfo.id, TCHAR_TO_ANSI(*NameString), &TagValueSH)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to retrive parameter tag value: parmId: %d, tag: %s"), ParmInfo.id, *NameString); + } + + FString ValueString = TEXT(""); + FHoudiniEngineString::ToFString(TagValueSH, ValueString); + + HoudiniParameter->GetTags().Add(NameString, ValueString); + } + } + } + + // + // Update properties specific to parameter classes + switch (ParmType) + { + case EHoudiniParameterType::Button: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__Button); + UHoudiniParameterButton* HoudiniParameterButton = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterButton)) + { + HoudiniParameterButton->SetValueIndex(ParmInfo.intValuesIndex); + } + } + break; + + case EHoudiniParameterType::ButtonStrip: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__ButtonStrip); + UHoudiniParameterButtonStrip* HoudiniParameterButtonStrip = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterButtonStrip)) + { + HoudiniParameterButtonStrip->SetValueIndex(ParmInfo.intValuesIndex); + + // Stop if we don't want to update the value + if (bUpdateValue) + { + if (bHasValidNodeId) + { + if (FHoudiniApi::GetParmIntValues( + FHoudiniEngine::Get().GetSession(), InNodeId, + HoudiniParameterButtonStrip->GetValuesPtr(), + ParmInfo.intValuesIndex, 1) != HAPI_RESULT_SUCCESS) + { + return false; + } + } + else if (DefaultIntValues && DefaultIntValues->IsValidIndex(ParmInfo.intValuesIndex)) + { + HoudiniParameterButtonStrip->SetValue( + (*DefaultIntValues)[ParmInfo.intValuesIndex]); + } + else + { + return false; + } + } + } + + if (bFullUpdate && ParmInfo.choiceCount > 0) + { + // Get the choice descriptors. + TArray< HAPI_ParmChoiceInfo > ParmChoices; + + ParmChoices.SetNum(ParmInfo.choiceCount); + for (int32 Idx = 0; Idx < ParmChoices.Num(); Idx++) + FHoudiniApi::ParmChoiceInfo_Init(&(ParmChoices[Idx])); + + if (bHasValidNodeId) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmChoiceLists( + FHoudiniEngine::Get().GetSession(), + InNodeId, &ParmChoices[0], + ParmInfo.choiceIndex, ParmInfo.choiceCount), false); + } + else if (DefaultChoiceValues && DefaultChoiceValues->IsValidIndex(ParmInfo.choiceIndex) && + DefaultChoiceValues->IsValidIndex(ParmInfo.choiceIndex + ParmInfo.choiceCount - 1)) + { + FPlatformMemory::Memcpy( + ParmChoices.GetData(), + DefaultChoiceValues->GetData() + ParmInfo.choiceIndex, + sizeof(HAPI_ParmChoiceInfo) * ParmInfo.choiceCount); + } + else + { + return false; + } + + HoudiniParameterButtonStrip->SetNumberOfValues(ParmInfo.choiceCount); + + for (int32 ChoiceIdx = 0; ChoiceIdx < ParmChoices.Num(); ++ChoiceIdx) + { + FString * ButtonLabel = HoudiniParameterButtonStrip->GetStringLabelAt(ChoiceIdx); + if (ButtonLabel) + { + FHoudiniEngineString HoudiniEngineString(ParmChoices[ChoiceIdx].labelSH); + if (!HoudiniEngineString.ToFString(*ButtonLabel)) + return false; + } + } + } + } + break; + + case EHoudiniParameterType::Color: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__Color); + UHoudiniParameterColor* HoudiniParameterColor = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterColor)) + { + // Set the valueIndex + HoudiniParameterColor->SetValueIndex(ParmInfo.floatValuesIndex); + + // Update the Parameter value if we want to + if (bUpdateValue) + { + // Get the actual value for this property. + FLinearColor Color = FLinearColor::White; + if (bHasValidNodeId) + { + if (FHoudiniApi::GetParmFloatValues( + FHoudiniEngine::Get().GetSession(), InNodeId, + (float *)&Color.R, ParmInfo.floatValuesIndex, ParmInfo.size) != HAPI_RESULT_SUCCESS) + { + return false; + } + } + else if (DefaultFloatValues && DefaultFloatValues->IsValidIndex(ParmInfo.floatValuesIndex) && + DefaultFloatValues->IsValidIndex(ParmInfo.floatValuesIndex + ParmInfo.size - 1)) + { + FPlatformMemory::Memcpy( + &Color.R, + DefaultFloatValues->GetData() + ParmInfo.floatValuesIndex, + sizeof(float) * ParmInfo.size); + } + else + { + return false; + } + + HoudiniParameterColor->SetColorValue(Color); + } + + if (bFullUpdate) + { + // Set the default value at parameter created. + HoudiniParameterColor->SetDefaultValue(); + } + } + } + break; + + case EHoudiniParameterType::ColorRamp: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__ColorRamp); + UHoudiniParameterRampColor* HoudiniParameterRampColor = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterRampColor)) + { + HoudiniParameterRampColor->SetInstanceCount(ParmInfo.instanceCount); + HoudiniParameterRampColor->MultiParmInstanceLength = ParmInfo.instanceLength; + } + } + break; + case EHoudiniParameterType::FloatRamp: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__FloatRamp); + UHoudiniParameterRampFloat* HoudiniParameterRampFloat = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterRampFloat)) + { + HoudiniParameterRampFloat->SetInstanceCount(ParmInfo.instanceCount); + HoudiniParameterRampFloat->MultiParmInstanceLength = ParmInfo.instanceLength; + } + } + break; + + case EHoudiniParameterType::File: + case EHoudiniParameterType::FileDir: + case EHoudiniParameterType::FileGeo: + case EHoudiniParameterType::FileImage: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__Files); + UHoudiniParameterFile* HoudiniParameterFile = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterFile)) + { + // Set the valueIndex + HoudiniParameterFile->SetValueIndex(ParmInfo.stringValuesIndex); + + // Update the file filter and read only tag only for full updates + if (bFullUpdate) + { + // Check if we are read-only + bool bIsReadOnly = false; + FString FileChooserTag; + if (bHasValidNodeId && FHoudiniParameterTranslator::HapiGetParameterTagValue(InNodeId, ParmInfo.id, HAPI_PARAM_TAG_FILE_READONLY, FileChooserTag)) + { + if (FileChooserTag.Equals(TEXT("read"), ESearchCase::IgnoreCase)) + bIsReadOnly = true; + } + HoudiniParameterFile->SetReadOnly(bIsReadOnly); + + // Update the file type using the typeInfo string. + if (ParmInfo.typeInfoSH > 0) + { + FString Filters; + FHoudiniEngineString HoudiniEngineString(ParmInfo.typeInfoSH); + if (HoudiniEngineString.ToFString(Filters)) + { + if (!Filters.IsEmpty()) + HoudiniParameterFile->SetFileFilters(Filters); + } + } + } + + if (bUpdateValue) + { + // Get the actual values for this property. + TArray< HAPI_StringHandle > StringHandles; + + if (bHasValidNodeId) + { + StringHandles.SetNumZeroed(ParmInfo.size); + if (FHoudiniApi::GetParmStringValues( + FHoudiniEngine::Get().GetSession(), InNodeId, false, + &StringHandles[0], ParmInfo.stringValuesIndex, ParmInfo.size) != HAPI_RESULT_SUCCESS) + { + return false; + } + } + else if (DefaultStringValues && DefaultStringValues->IsValidIndex(ParmInfo.stringValuesIndex) && + DefaultStringValues->IsValidIndex(ParmInfo.stringValuesIndex + ParmInfo.size - 1)) + { + StringHandles.SetNumZeroed(ParmInfo.size); + FPlatformMemory::Memcpy( + &StringHandles[0], + DefaultStringValues->GetData() + ParmInfo.stringValuesIndex, + sizeof(HAPI_StringHandle) * ParmInfo.size); + } + else + { + return false; + } + + // Convert HAPI string handles to Unreal strings. + HoudiniParameterFile->SetNumberOfValues(ParmInfo.size); + for (int32 Idx = 0; Idx < StringHandles.Num(); ++Idx) + { + FString ValueString = TEXT(""); + FHoudiniEngineString HoudiniEngineString(StringHandles[Idx]); + HoudiniEngineString.ToFString(ValueString); + + // Update the parameter value + HoudiniParameterFile->SetValueAt(ValueString, Idx); + } + } + + if (bFullUpdate) + { + HoudiniParameterFile->SetDefaultValues(); + } + } + } + break; + + case EHoudiniParameterType::Float: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__Float); + UHoudiniParameterFloat* HoudiniParameterFloat = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterFloat)) + { + // Set the valueIndex + HoudiniParameterFloat->SetValueIndex(ParmInfo.floatValuesIndex); + + if (bUpdateValue) + { + // Update the parameter's value + HoudiniParameterFloat->SetNumberOfValues(ParmInfo.size); + + if (bHasValidNodeId) + { + if (FHoudiniApi::GetParmFloatValues( + FHoudiniEngine::Get().GetSession(), InNodeId, + HoudiniParameterFloat->GetValuesPtr(), + ParmInfo.floatValuesIndex, ParmInfo.size) != HAPI_RESULT_SUCCESS) + { + return false; + } + } + else if (DefaultFloatValues && DefaultFloatValues->IsValidIndex(ParmInfo.floatValuesIndex) && + DefaultFloatValues->IsValidIndex(ParmInfo.floatValuesIndex + ParmInfo.size - 1)) + { + FPlatformMemory::Memcpy( + HoudiniParameterFloat->GetValuesPtr(), + DefaultFloatValues->GetData() + ParmInfo.floatValuesIndex, + sizeof(float) * ParmInfo.size); + } + else + { + return false; + } + } + + if (bFullUpdate) + { + if (ParmInfo.scriptType == HAPI_PrmScriptType::HAPI_PRM_SCRIPT_TYPE_FLOAT_LOG) + { + HoudiniParameterFloat->SetIsLogarithmic(true); + } + + // set the default float values. + HoudiniParameterFloat->SetDefaultValues(); + + // Only update Unit, no swap, and Min/Max values when doing a full update + + // Get the parameter's unit from the "unit" tag + FString ParamUnit; + if (bHasValidNodeId) + { + FHoudiniParameterTranslator::HapiGetParameterUnit(InNodeId, ParmInfo.id, ParamUnit); + HoudiniParameterFloat->SetUnit(ParamUnit); + // Get the parameter's no swap tag (hengine_noswap) + HoudiniParameterFloat->SetNoSwap(HapiGetParameterHasTag(InNodeId, ParmInfo.id, HAPI_PARAM_TAG_NOSWAP)); + } + + // Set the min and max for this parameter + if (ParmInfo.hasMin) + { + HoudiniParameterFloat->SetHasMin(true); + HoudiniParameterFloat->SetMin(ParmInfo.min); + } + else + { + HoudiniParameterFloat->SetHasMin(false); + HoudiniParameterFloat->SetMin(TNumericLimits::Lowest()); + } + + if (ParmInfo.hasMax) + { + HoudiniParameterFloat->SetHasMax(true); + HoudiniParameterFloat->SetMax(ParmInfo.max); + } + else + { + HoudiniParameterFloat->SetHasMax(false); + HoudiniParameterFloat->SetMax(TNumericLimits::Max()); + } + + // Set min and max for UI for this property. + bool bUsesDefaultMin = false; + if (ParmInfo.hasUIMin) + { + HoudiniParameterFloat->SetHasUIMin(true); + HoudiniParameterFloat->SetUIMin(ParmInfo.UIMin); + } + else if (ParmInfo.hasMin) + { + // If it is not set, use supplied min. + HoudiniParameterFloat->SetUIMin(ParmInfo.min); + } + else + { + // Min value Houdini uses by default. + HoudiniParameterFloat->SetUIMin(HAPI_UNREAL_PARAM_FLOAT_UI_MIN); + bUsesDefaultMin = true; + } + + bool bUsesDefaultMax = false; + if (ParmInfo.hasUIMax) + { + HoudiniParameterFloat->SetHasUIMax(true); + HoudiniParameterFloat->SetUIMax(ParmInfo.UIMax); + } + else if (ParmInfo.hasMax) + { + // If it is not set, use supplied max. + HoudiniParameterFloat->SetUIMax(ParmInfo.max); + } + else + { + // Max value Houdini uses by default. + HoudiniParameterFloat->SetUIMax(HAPI_UNREAL_PARAM_FLOAT_UI_MAX); + bUsesDefaultMax = true; + } + + if (bUsesDefaultMin || bUsesDefaultMax) + { + // If we are using defaults, we can detect some most common parameter names and alter defaults. + FString LocalParameterName = HoudiniParameterFloat->GetParameterName(); + FHoudiniEngineString HoudiniEngineString(ParmInfo.nameSH); + HoudiniEngineString.ToFString(LocalParameterName); + + static const FString ParameterNameTranslate(TEXT(HAPI_UNREAL_PARAM_TRANSLATE)); + static const FString ParameterNameRotate(TEXT(HAPI_UNREAL_PARAM_ROTATE)); + static const FString ParameterNameScale(TEXT(HAPI_UNREAL_PARAM_SCALE)); + static const FString ParameterNamePivot(TEXT(HAPI_UNREAL_PARAM_PIVOT)); + + if (!LocalParameterName.IsEmpty()) + { + if (LocalParameterName.Equals(ParameterNameTranslate) + || LocalParameterName.Equals(ParameterNameScale) + || LocalParameterName.Equals(ParameterNamePivot)) + { + if (bUsesDefaultMin) + { + HoudiniParameterFloat->SetUIMin(-1.0f); + } + if (bUsesDefaultMax) + { + HoudiniParameterFloat->SetUIMax(1.0f); + } + } + else if (LocalParameterName.Equals(ParameterNameRotate)) + { + if (bUsesDefaultMin) + { + HoudiniParameterFloat->SetUIMin(0.0f); + } + if (bUsesDefaultMax) + { + HoudiniParameterFloat->SetUIMax(360.0f); + } + } + } + } + } + } + } + break; + + case EHoudiniParameterType::Folder: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__Folder); + UHoudiniParameterFolder* HoudiniParameterFolder = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterFolder)) + { + // Set the valueIndex + HoudiniParameterFolder->SetValueIndex(ParmInfo.intValuesIndex); + HoudiniParameterFolder->SetFolderType(GetFolderTypeFromParamInfo(&ParmInfo)); + } + } + break; + + case EHoudiniParameterType::FolderList: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__FolderList); + UHoudiniParameterFolderList* HoudiniParameterFolderList = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterFolderList)) + { + // Set the valueIndex + HoudiniParameterFolderList->SetValueIndex(ParmInfo.intValuesIndex); + } + } + break; + + case EHoudiniParameterType::Input: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__Input); + // Inputs parameters are just stored, and handled separately by UHoudiniInputs + UHoudiniParameterOperatorPath* HoudiniParameterOperatorPath = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterOperatorPath)) + { + // DO NOT CREATE A DUPLICATE INPUT HERE! + // Inputs are created by the input translator, and will be tied to this parameter there + + // Set the valueIndex + HoudiniParameterOperatorPath->SetValueIndex(ParmInfo.stringValuesIndex); + } + } + break; + + case EHoudiniParameterType::Int: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__Int); + UHoudiniParameterInt* HoudiniParameterInt = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterInt)) + { + // Set the valueIndex + HoudiniParameterInt->SetValueIndex(ParmInfo.intValuesIndex); + + if (bUpdateValue) + { + // Get the actual values for this property. + HoudiniParameterInt->SetNumberOfValues(ParmInfo.size); + + if (bHasValidNodeId) + { + if (FHoudiniApi::GetParmIntValues( + FHoudiniEngine::Get().GetSession(), InNodeId, + HoudiniParameterInt->GetValuesPtr(), + ParmInfo.intValuesIndex, ParmInfo.size) != HAPI_RESULT_SUCCESS) + { + return false; + } + } + else if (DefaultIntValues && DefaultIntValues->IsValidIndex(ParmInfo.intValuesIndex) && + DefaultIntValues->IsValidIndex(ParmInfo.intValuesIndex + ParmInfo.size - 1)) + { + for (int32 Index = 0; Index < ParmInfo.size; ++Index) + { + // TODO: cannot use SetValueAt: Min/Max has not yet been configured and defaults to 0,0 + // so the value is clamped to 0 + // HoudiniParameterInt->SetValueAt( + // (*DefaultIntValues)[ParmInfo.intValuesIndex + Index], Index); + *(HoudiniParameterInt->GetValuesPtr() + Index) = (*DefaultIntValues)[ParmInfo.intValuesIndex + Index]; + } + } + else + { + return false; + } + } + + if (bFullUpdate) + { + if (ParmInfo.scriptType == HAPI_PrmScriptType::HAPI_PRM_SCRIPT_TYPE_INT_LOG) + { + HoudiniParameterInt->SetIsLogarithmic(true); + } + + // Set the default int values at created + HoudiniParameterInt->SetDefaultValues(); + // Only update unit and Min/Max values for a full update + + // Get the parameter's unit from the "unit" tag + FString ParamUnit; + if (bHasValidNodeId) + { + FHoudiniParameterTranslator::HapiGetParameterUnit(InNodeId, ParmInfo.id, ParamUnit); + HoudiniParameterInt->SetUnit(ParamUnit); + } + + // Set the min and max for this parameter + if (ParmInfo.hasMin) + { + HoudiniParameterInt->SetHasMin(true); + HoudiniParameterInt->SetMin((int32)ParmInfo.min); + } + else + { + HoudiniParameterInt->SetHasMin(false); + HoudiniParameterInt->SetMin(TNumericLimits::Lowest()); + } + + if (ParmInfo.hasMax) + { + HoudiniParameterInt->SetHasMax(true); + HoudiniParameterInt->SetMax((int32)ParmInfo.max); + } + else + { + HoudiniParameterInt->SetHasMax(false); + HoudiniParameterInt->SetMax(TNumericLimits::Max()); + } + + // Set min and max for UI for this property. + if (ParmInfo.hasUIMin) + { + HoudiniParameterInt->SetHasUIMin(true); + HoudiniParameterInt->SetUIMin((int32)ParmInfo.UIMin); + } + else if (ParmInfo.hasMin) + { + // If it is not set, use supplied min. + HoudiniParameterInt->SetUIMin((int32)ParmInfo.min); + } + else + { + // Min value Houdini uses by default. + HoudiniParameterInt->SetUIMin(HAPI_UNREAL_PARAM_INT_UI_MIN); + } + + if (ParmInfo.hasUIMax) + { + HoudiniParameterInt->SetHasUIMax(true); + HoudiniParameterInt->SetUIMax((int32)ParmInfo.UIMax); + } + else if (ParmInfo.hasMax) + { + // If it is not set, use supplied max. + HoudiniParameterInt->SetUIMax((int32)ParmInfo.max); + } + else + { + // Max value Houdini uses by default. + HoudiniParameterInt->SetUIMax(HAPI_UNREAL_PARAM_INT_UI_MAX); + } + } + } + } + break; + + case EHoudiniParameterType::IntChoice: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__IntChoice); + UHoudiniParameterChoice* HoudiniParameterIntChoice = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterIntChoice)) + { + // Set the valueIndex + HoudiniParameterIntChoice->SetValueIndex(ParmInfo.intValuesIndex); + + if (bUpdateValue) + { + // Get the actual values for this property. + int32 CurrentIntValue = 0; + + if (bHasValidNodeId) + { + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmIntValues( + FHoudiniEngine::Get().GetSession(), + InNodeId, &CurrentIntValue, + ParmInfo.intValuesIndex, 1/*ParmInfo.size*/), false); + } + else if (DefaultIntValues && DefaultIntValues->IsValidIndex(ParmInfo.intValuesIndex)) + { + CurrentIntValue = (*DefaultIntValues)[ParmInfo.intValuesIndex]; + } + else + { + return false; + } + + // Get the value from the index array, if applicable. + if (CurrentIntValue < HoudiniParameterIntChoice->GetNumChoices()) + CurrentIntValue = HoudiniParameterIntChoice->GetIndexFromValueArray(CurrentIntValue); + + // Check the value is valid + if (CurrentIntValue >= ParmInfo.choiceCount) + { + HOUDINI_LOG_WARNING(TEXT("parm '%s' has an invalid value %d, menu tokens are not supported for choice menus"), + *HoudiniParameterIntChoice->GetParameterName(), CurrentIntValue); + CurrentIntValue = 0; + } + + HoudiniParameterIntChoice->SetIntValue(CurrentIntValue); + } + + // Get the choice descriptors. NOTE: have to do this on every cook because they may be updated by the HDA. + bool bUpdateChoiceStrings = true; + if (bUpdateChoiceStrings) + { + // Set the default value at created + HoudiniParameterIntChoice->SetDefaultIntValue(); + // Get the choice descriptors. + TArray< HAPI_ParmChoiceInfo > ParmChoices; + + ParmChoices.SetNum(ParmInfo.choiceCount); + for (int32 Idx = 0; Idx < ParmChoices.Num(); Idx++) + FHoudiniApi::ParmChoiceInfo_Init(&(ParmChoices[Idx])); + + if (bHasValidNodeId) + { + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmChoiceLists( + FHoudiniEngine::Get().GetSession(), + InNodeId, &ParmChoices[0], + ParmInfo.choiceIndex, ParmInfo.choiceCount), false); + } + else if (DefaultChoiceValues && DefaultChoiceValues->IsValidIndex(ParmInfo.choiceIndex) && + DefaultChoiceValues->IsValidIndex(ParmInfo.choiceIndex + ParmInfo.choiceCount - 1)) + { + FPlatformMemory::Memcpy( + ParmChoices.GetData(), + DefaultChoiceValues->GetData() + ParmInfo.choiceIndex, + sizeof(HAPI_ParmChoiceInfo) * ParmInfo.choiceCount); + } + else + { + return false; + } + + // Set the array sizes + HoudiniParameterIntChoice->SetNumChoices(ParmInfo.choiceCount); + + bool bMatchedSelectionLabel = false; + int32 CurrentIntValue = HoudiniParameterIntChoice->GetIntValueIndex(); + for (int32 ChoiceIdx = 0; ChoiceIdx < ParmChoices.Num(); ++ChoiceIdx) + { + FString * ChoiceLabel = HoudiniParameterIntChoice->GetStringChoiceLabelAt(ChoiceIdx); + if (ChoiceLabel) + { + FHoudiniEngineString HoudiniEngineString(ParmChoices[ChoiceIdx].labelSH); + if (!HoudiniEngineString.ToFString(*ChoiceLabel)) + return false; + //StringChoiceLabels.Add(TSharedPtr< FString >(ChoiceLabel)); + } + + // Match our string value to the corresponding selection label. + if (ChoiceIdx == CurrentIntValue) + { + HoudiniParameterIntChoice->SetStringValue(*ChoiceLabel); + } + + int32 IntValue = ChoiceIdx; + + // If useMenuItemTokenAsValue is set, then the value is not the index. Find the value using the token, if possible. + if (ParmInfo.useMenuItemTokenAsValue) + { + if (ChoiceIdx < ParmChoices.Num()) + { + FHoudiniEngineString HoudiniEngineString(ParmChoices[ChoiceIdx].valueSH); + FString Token; + if (HoudiniEngineString.ToFString(Token)) + { + if (Token.IsNumeric()) + { + int32 Value = FCString::Atoi(*Token); + IntValue = Value; + } + } + } + } + + HoudiniParameterIntChoice->SetIntValueArray(ChoiceIdx, IntValue); + } + } + else if (bUpdateValue) + { + // We still need to match the string value to the label + HoudiniParameterIntChoice->UpdateStringValueFromInt(); + } + } + } + break; + + case EHoudiniParameterType::StringChoice: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__StringChoice); + UHoudiniParameterChoice* HoudiniParameterStringChoice = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterStringChoice)) + { + // Set the valueIndex + HoudiniParameterStringChoice->SetValueIndex(ParmInfo.stringValuesIndex); + + if (bUpdateValue) + { + // Get the actual values for this property. + HAPI_StringHandle StringHandle; + + if (bHasValidNodeId) + { + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmStringValues( + FHoudiniEngine::Get().GetSession(), + InNodeId, false, &StringHandle, + ParmInfo.stringValuesIndex, 1/*ParmInfo.size*/), false); + } + else if (DefaultStringValues && DefaultStringValues->IsValidIndex(ParmInfo.stringValuesIndex)) + { + StringHandle = (*DefaultStringValues)[ParmInfo.stringValuesIndex]; + } + else + { + return false; + } + + // If useMenuItemTokenAsValue is set, then the value is not the index. Find the value using the token, if possible. + if (ParmInfo.useMenuItemTokenAsValue) + { + // NOT HANDLED + } + + // Get the string value + FString StringValue; + FHoudiniEngineString HoudiniEngineString(StringHandle); + HoudiniEngineString.ToFString(StringValue); + + HoudiniParameterStringChoice->SetStringValue(StringValue); + } + + // Get the choice descriptors + if (bFullUpdate) + { + // Set default value at created. + HoudiniParameterStringChoice->SetDefaultStringValue(); + // Get the choice descriptors. + TArray< HAPI_ParmChoiceInfo > ParmChoices; + + ParmChoices.SetNum(ParmInfo.choiceCount); + for (int32 Idx = 0; Idx < ParmChoices.Num(); Idx++) + FHoudiniApi::ParmChoiceInfo_Init(&(ParmChoices[Idx])); + + if (bHasValidNodeId) + { + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetParmChoiceLists( + FHoudiniEngine::Get().GetSession(), + InNodeId, &ParmChoices[0], + ParmInfo.choiceIndex, ParmInfo.choiceCount), false); + } + else if (DefaultChoiceValues && DefaultChoiceValues->IsValidIndex(ParmInfo.choiceIndex) && + DefaultChoiceValues->IsValidIndex(ParmInfo.choiceIndex + ParmInfo.choiceCount - 1)) + { + FPlatformMemory::Memcpy( + ParmChoices.GetData(), + DefaultChoiceValues->GetData() + ParmInfo.choiceIndex, + sizeof(HAPI_ParmChoiceInfo) * ParmInfo.choiceCount); + } + else + { + return false; + } + + // Set the array sizes + HoudiniParameterStringChoice->SetNumChoices(ParmInfo.choiceCount); + + bool bMatchedSelectionLabel = false; + FString CurrentStringValue = HoudiniParameterStringChoice->GetStringValue(); + for (int32 ChoiceIdx = 0; ChoiceIdx < ParmChoices.Num(); ++ChoiceIdx) + { + FString * ChoiceValue = HoudiniParameterStringChoice->GetStringChoiceValueAt(ChoiceIdx); + if (ChoiceValue) + { + FHoudiniEngineString HoudiniEngineString(ParmChoices[ChoiceIdx].valueSH); + if (!HoudiniEngineString.ToFString(*ChoiceValue)) + return false; + //StringChoiceValues.Add(TSharedPtr< FString >(ChoiceValue)); + } + + FString * ChoiceLabel = HoudiniParameterStringChoice->GetStringChoiceLabelAt(ChoiceIdx); + if (ChoiceLabel) + { + FHoudiniEngineString HoudiniEngineString(ParmChoices[ChoiceIdx].labelSH); + if (!HoudiniEngineString.ToFString(*ChoiceLabel)) + return false; + //StringChoiceLabels.Add(TSharedPtr< FString >(ChoiceLabel)); + } + + // If this is a string choice list, we need to match name with corresponding selection label. + if (!bMatchedSelectionLabel && ChoiceValue->Equals(CurrentStringValue)) + { + bMatchedSelectionLabel = true; + HoudiniParameterStringChoice->SetIntValue(ChoiceIdx); + } + } + } + else if (bUpdateValue) + { + // We still need to match the string value to the label + HoudiniParameterStringChoice->UpdateIntValueFromString(); + } + } + } + break; + + case EHoudiniParameterType::Label: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__Label); + UHoudiniParameterLabel* HoudiniParameterLabel = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterLabel)) + { + if (ParmInfo.type != HAPI_PARMTYPE_LABEL) + return false; + + // Set the valueIndex + HoudiniParameterLabel->SetValueIndex(ParmInfo.stringValuesIndex); + + // Get the actual value for this property. + TArray StringHandles; + + if (bHasValidNodeId) + { + StringHandles.SetNumZeroed(ParmInfo.size); + FHoudiniApi::GetParmStringValues( + FHoudiniEngine::Get().GetSession(), + InNodeId, false, &StringHandles[0], + ParmInfo.stringValuesIndex, ParmInfo.size); + } + else if (DefaultStringValues && DefaultStringValues->IsValidIndex(ParmInfo.stringValuesIndex) && + DefaultStringValues->IsValidIndex(ParmInfo.stringValuesIndex + ParmInfo.size - 1)) + { + StringHandles.SetNumZeroed(ParmInfo.size); + FPlatformMemory::Memcpy( + StringHandles.GetData(), + DefaultStringValues->GetData() + ParmInfo.stringValuesIndex, + sizeof(HAPI_StringHandle) * ParmInfo.size); + } + else + { + return false; + } + + HoudiniParameterLabel->EmptyLabelString(); + + // Convert HAPI string handles to Unreal strings. + for (int32 Idx = 0; Idx < StringHandles.Num(); ++Idx) + { + FString ValueString = TEXT(""); + FHoudiniEngineString HoudiniEngineString(StringHandles[Idx]); + HoudiniEngineString.ToFString(ValueString); + HoudiniParameterLabel->AddLabelString(ValueString); + } + } + } + break; + + case EHoudiniParameterType::MultiParm: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__MultiParm); + UHoudiniParameterMultiParm* HoudiniParameterMulti = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterMulti)) + { + if (ParmInfo.type != HAPI_PARMTYPE_MULTIPARMLIST) + return false; + + // Set the valueIndex + HoudiniParameterMulti->SetValueIndex(ParmInfo.intValuesIndex); + + // Set the multiparm value + int32 MultiParmValue = 0; + + if (bHasValidNodeId) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmIntValues( + FHoudiniEngine::Get().GetSession(), + InNodeId, &MultiParmValue, ParmInfo.intValuesIndex, 1), false); + } + else if (DefaultIntValues && DefaultIntValues->IsValidIndex(ParmInfo.intValuesIndex)) + { + MultiParmValue = (*DefaultIntValues)[ParmInfo.intValuesIndex]; + } + else + { + return false; + } + + HoudiniParameterMulti->SetValue(MultiParmValue); + HoudiniParameterMulti->MultiParmInstanceCount = ParmInfo.instanceCount; + HoudiniParameterMulti->MultiParmInstanceLength = ParmInfo.instanceLength; + + } + + if (bFullUpdate) + { + HoudiniParameterMulti->SetDefaultInstanceCount(ParmInfo.instanceCount); + } + } + break; + + case EHoudiniParameterType::Separator: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__Sep); + UHoudiniParameterSeparator* HoudiniParameterSeparator = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterSeparator)) + { + // We can only handle separator type. + if (ParmInfo.type != HAPI_PARMTYPE_SEPARATOR) + return false; + + // Set the valueIndex + HoudiniParameterSeparator->SetValueIndex(ParmInfo.stringValuesIndex); + } + } + break; + + case EHoudiniParameterType::String: + case EHoudiniParameterType::StringAssetRef: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__String); + UHoudiniParameterString* HoudiniParameterString = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterString)) + { + // We can only handle string type. + if (ParmInfo.type != HAPI_PARMTYPE_STRING && ParmInfo.type != HAPI_PARMTYPE_NODE) + return false; + + // Set the valueIndex + HoudiniParameterString->SetValueIndex(ParmInfo.stringValuesIndex); + + // Stop if we don't want to update the value + if (bUpdateValue) + { + // Get the actual value for this property. + TArray< HAPI_StringHandle > StringHandles; + + if (bHasValidNodeId) + { + StringHandles.SetNumZeroed(ParmInfo.size); + if (FHoudiniApi::GetParmStringValues( + FHoudiniEngine::Get().GetSession(), + InNodeId, false, &StringHandles[0], + ParmInfo.stringValuesIndex, ParmInfo.size) != HAPI_RESULT_SUCCESS) + { + return false; + } + } + else if (DefaultStringValues && DefaultStringValues->IsValidIndex(ParmInfo.stringValuesIndex) && + DefaultStringValues->IsValidIndex(ParmInfo.stringValuesIndex + ParmInfo.size - 1)) + { + StringHandles.SetNumZeroed(ParmInfo.size); + FPlatformMemory::Memcpy( + StringHandles.GetData(), + DefaultStringValues->GetData() + ParmInfo.stringValuesIndex, + sizeof(HAPI_StringHandle) * ParmInfo.size); + } + else + { + return false; + } + + // Convert HAPI string handles to Unreal strings. + HoudiniParameterString->SetNumberOfValues(ParmInfo.size); + for (int32 Idx = 0; Idx < StringHandles.Num(); ++Idx) + { + FString ValueString = TEXT(""); + FHoudiniEngineString HoudiniEngineString(StringHandles[Idx]); + HoudiniEngineString.ToFString(ValueString); + HoudiniParameterString->SetValueAt(ValueString, Idx); + } + } + + if (bFullUpdate) + { + // Set default string values on created + HoudiniParameterString->SetDefaultValues(); + // Check if the parameter has the "asset_ref" tag + if (bHasValidNodeId) + { + HoudiniParameterString->SetIsAssetRef( + FHoudiniParameterTranslator::HapiGetParameterHasTag(InNodeId, ParmInfo.id, HOUDINI_PARAMETER_STRING_REF_TAG)); + } + } + } + } + break; + + case EHoudiniParameterType::Toggle: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UpdateParameterFromInfo__Toggle); + UHoudiniParameterToggle* HoudiniParameterToggle = Cast(HoudiniParameter); + if (IsValid(HoudiniParameterToggle)) + { + if (ParmInfo.type != HAPI_PARMTYPE_TOGGLE) + return false; + + // Set the valueIndex + HoudiniParameterToggle->SetValueIndex(ParmInfo.intValuesIndex); + + // Stop if we don't want to update the value + if (bUpdateValue) + { + // Get the actual values for this property. + HoudiniParameterToggle->SetNumberOfValues(ParmInfo.size); + + if (bHasValidNodeId) + { + if (FHoudiniApi::GetParmIntValues( + FHoudiniEngine::Get().GetSession(), InNodeId, + HoudiniParameterToggle->GetValuesPtr(), + ParmInfo.intValuesIndex, ParmInfo.size) != HAPI_RESULT_SUCCESS) + { + return false; + } + } + else if (DefaultIntValues && DefaultIntValues->IsValidIndex(ParmInfo.intValuesIndex) && + DefaultIntValues->IsValidIndex(ParmInfo.intValuesIndex + ParmInfo.size - 1)) + { + for (int32 Index = 0; Index < ParmInfo.size; ++Index) + { + HoudiniParameterToggle->SetValueAt( + (*DefaultIntValues)[ParmInfo.intValuesIndex + Index] != 0, Index); + } + } + else + { + return false; + } + } + } + + if (bFullUpdate) + { + HoudiniParameterToggle->SetDefaultValues(); + } + } + break; + + case EHoudiniParameterType::Invalid: + { + // TODO + } + break; + } + + return true; +} + +bool +FHoudiniParameterTranslator::HapiGetParameterTagValue( + HAPI_NodeId NodeId, + HAPI_ParmId ParmId, + const FString& Tag, + FString& TagValue) +{ + // Default + TagValue = FString(); + + // Does the parameter has the tag? + bool HasTag = false; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ParmHasTag( + FHoudiniEngine::Get().GetSession(), NodeId, ParmId, + TCHAR_TO_ANSI(*Tag), &HasTag), false); + + if (!HasTag) + return false; + + // Get the tag string value + HAPI_StringHandle StringHandle; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmTagValue( + FHoudiniEngine::Get().GetSession(), NodeId, ParmId, + TCHAR_TO_ANSI(*Tag), &StringHandle), false); + + FHoudiniEngineString HoudiniEngineString(StringHandle); + if (HoudiniEngineString.ToFString(TagValue)) + { + return true; + } + + return false; +} + + +bool +FHoudiniParameterTranslator::HapiGetParameterUnit( + HAPI_NodeId NodeId, + HAPI_ParmId ParmId, + FString& OutUnitString) +{ + // + OutUnitString = TEXT(""); + + // We're looking for the parameter unit tag + //FString UnitTag = "units"; + + // Get the actual string value. + FString UnitString = TEXT(""); + if (!FHoudiniParameterTranslator::HapiGetParameterTagValue(NodeId, ParmId, "units", UnitString)) + return false; + + // We need to do some replacement in the string here in order to be able to get the + // proper unit type when calling FUnitConversion::UnitFromString(...) after. + + // Per second and per hour are the only "per" unit that unreal recognize + UnitString.ReplaceInline(TEXT("s-1"), TEXT("/s")); + UnitString.ReplaceInline(TEXT("h-1"), TEXT("/h")); + + // Houdini likes to add '1' on all the unit, so we'll remove all of them + // except the '-1' that still needs to remain. + UnitString.ReplaceInline(TEXT("-1"), TEXT("--")); + UnitString.ReplaceInline(TEXT("1"), TEXT("")); + UnitString.ReplaceInline(TEXT("--"), TEXT("-1")); + + OutUnitString = UnitString; + + return true; +} + +bool +FHoudiniParameterTranslator::HapiGetParameterHasTag( + HAPI_NodeId NodeId, + HAPI_ParmId ParmId, + const FString& Tag) +{ + // Does the parameter has the tag we're looking for? + bool HasTag = false; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::ParmHasTag( + FHoudiniEngine::Get().GetSession(), NodeId, ParmId, + TCHAR_TO_ANSI(*Tag), &HasTag), false); + + return HasTag; +} + + +bool +FHoudiniParameterTranslator::UploadChangedParameters( + TArray>& InParameters, + HAPI_NodeId InNodeId) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadChangedParameters); + + bool bResult = true; + + TMap RampsToRevert; + // First upload all parameters, including the current child parameters/points of ramps, and then process + // the ramp parameters themselves (delete and insert operations of ramp points) + // This is so that the initial upload of parameter values use the correct parameter value/tuple array indices + // (which will change after potential insert/delete operations). Insert operations will upload their new + // parameter values after the insert. + TArray RampsToUpload; + for (int32 ParmIdx = 0; ParmIdx < InParameters.Num(); ParmIdx++) + { + TObjectPtr& CurrentParm = InParameters[ParmIdx]; + if (!IsValid(CurrentParm) || !CurrentParm->HasChanged()) + continue; + + bool bSuccess = false; + const EHoudiniParameterType CurrentParmType = CurrentParm->GetParameterType(); + if (CurrentParm->IsPendingRevertToDefault()) + { + bSuccess = RevertParameterToDefault(CurrentParm); + + if (CurrentParmType == EHoudiniParameterType::FloatRamp || + CurrentParmType == EHoudiniParameterType::ColorRamp) + { + RampsToRevert.Add(CurrentParm->GetParameterName(), CurrentParm); + } + } + else + { + if (CurrentParmType == EHoudiniParameterType::FloatRamp || + CurrentParmType == EHoudiniParameterType::ColorRamp) + { + RampsToUpload.Add(CurrentParm); + } + else + { + bSuccess = UploadParameterValue(CurrentParm); + } + } + + + if (bSuccess) + { + CurrentParm->MarkChanged(false); + //CurrentParm->SetNeedsToTriggerUpdate(false); + } + else + { + // Keep this param marked as changed but prevent it from generating updates + CurrentParm->SetNeedsToTriggerUpdate(false); + bResult = false; + } + } + + if (!FHoudiniParameterTranslator::RevertRampParameters(RampsToRevert, InNodeId)) + bResult = false; + + for (UHoudiniParameter* const RampParam : RampsToUpload) + { + if (!IsValid(RampParam)) + continue; + + if (UploadParameterValue(RampParam)) + RampParam->MarkChanged(false); + else + bResult = false; + } + + return bResult; +} + +bool +FHoudiniParameterTranslator::UploadParameterValue(UHoudiniParameter* InParam) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue); + if (!IsValid(InParam)) + return false; + + switch (InParam->GetParameterType()) + { + case EHoudiniParameterType::Float: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue-Float); + UHoudiniParameterFloat* FloatParam = Cast(InParam); + if (!IsValid(FloatParam)) + { + return false; + } + + float* DataPtr = FloatParam->GetValuesPtr(); + if (!DataPtr) + { + return false; + } + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + FloatParam->GetNodeId(), DataPtr, FloatParam->GetValueIndex(), FloatParam->GetTupleSize()), false); + } + break; + + case EHoudiniParameterType::Int: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue - Int); + UHoudiniParameterInt* IntParam = Cast(InParam); + if (!IsValid(IntParam)) + { + return false; + } + + int32* DataPtr = IntParam->GetValuesPtr(); + if (!DataPtr) + { + return false; + } + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValues( + FHoudiniEngine::Get().GetSession(), + IntParam->GetNodeId(), DataPtr, IntParam->GetValueIndex(), IntParam->GetTupleSize()), false); + } + break; + + case EHoudiniParameterType::String: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue - String); + UHoudiniParameterString* StringParam = Cast(InParam); + if (!IsValid(StringParam)) + { + return false; + } + + int32 NumValues = StringParam->GetNumberOfValues(); + if (NumValues <= 0) + { + return false; + } + + for (int32 Idx = 0; Idx < NumValues; Idx++) + { + std::string ConvertedString = H_TCHAR_TO_UTF8(*(StringParam->GetValueAt(Idx))); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue( + FHoudiniEngine::Get().GetSession(), + StringParam->GetNodeId(), ConvertedString.c_str(), StringParam->GetParmId(), Idx), false); + } + } + break; + + case EHoudiniParameterType::IntChoice: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue - IntChoice); + UHoudiniParameterChoice* ChoiceParam = Cast(InParam); + if (!IsValid(ChoiceParam)) + return false; + + // Set the parameter's int value. + const int32 IntValueIndex = ChoiceParam->GetIntValueIndex(); + const int32 IntValue = ChoiceParam->GetIntValue(IntValueIndex); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValues( + FHoudiniEngine::Get().GetSession(), + ChoiceParam->GetNodeId(), &IntValue, ChoiceParam->GetValueIndex(), ChoiceParam->GetTupleSize()), false); + } + break; + + case EHoudiniParameterType::StringChoice: + { + UHoudiniParameterChoice* ChoiceParam = Cast(InParam); + if (!IsValid(ChoiceParam)) + { + return false; + } + + if (ChoiceParam->IsStringChoice()) + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue - StringChoice); + + // Set the parameter's string value. + std::string ConvertedString = H_TCHAR_TO_UTF8(*(ChoiceParam->GetStringValue())); + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::SetParmStringValue( + FHoudiniEngine::Get().GetSession(), + ChoiceParam->GetNodeId(), ConvertedString.c_str(), ChoiceParam->GetParmId(), 0), false); + } + else + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue - StringChoice - Int); + + // Set the parameter's int value. + int32 IntValue = ChoiceParam->GetIntValueIndex(); + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::SetParmIntValues( + FHoudiniEngine::Get().GetSession(), + ChoiceParam->GetNodeId(), &IntValue, ChoiceParam->GetValueIndex(), ChoiceParam->GetTupleSize()), false); + } + } + break; + + case EHoudiniParameterType::Color: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue - Color); + UHoudiniParameterColor* ColorParam = Cast(InParam); + if (!IsValid(ColorParam)) + return false; + + bool bHasAlpha = ColorParam->GetTupleSize() == 4 ? true : false; + FLinearColor Color = ColorParam->GetColorValue(); + + // Set the color value + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + ColorParam->GetNodeId(), + (float*)(&Color.R), ColorParam->GetValueIndex(), bHasAlpha ? 4 : 3), false); + + } + break; + + case EHoudiniParameterType::Button: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue - Button); + + UHoudiniParameterButton* ButtonParam = Cast(InParam); + if (!ButtonParam) + return false; + + TArray DataArray; + DataArray.Add(1); + + // Set the button parameter value to 1, (setting button param to any value will call the callback function.) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValues( + FHoudiniEngine::Get().GetSession(), + ButtonParam->GetNodeId(), + DataArray.GetData(), + ButtonParam->GetValueIndex(), 1), false); + } + break; + + case EHoudiniParameterType::ButtonStrip: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue - Strip); + + UHoudiniParameterButtonStrip* ButtonStripParam = Cast(InParam); + if (!ButtonStripParam) + return false; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValues( + FHoudiniEngine::Get().GetSession(), + ButtonStripParam->GetNodeId(), + ButtonStripParam->GetValuesPtr(), + ButtonStripParam->GetValueIndex(), 1), false); + } + break; + + case EHoudiniParameterType::Toggle: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue - Toggle); + + UHoudiniParameterToggle* ToggleParam = Cast(InParam); + if (!ToggleParam) + return false; + + // Set the toggle parameter values. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValues( + FHoudiniEngine::Get().GetSession(), + ToggleParam->GetNodeId(), + ToggleParam->GetValuesPtr(), + ToggleParam->GetValueIndex(), ToggleParam->GetTupleSize()), false); + } + break; + + case EHoudiniParameterType::File: + case EHoudiniParameterType::FileDir: + case EHoudiniParameterType::FileGeo: + case EHoudiniParameterType::FileImage: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue - File); + + UHoudiniParameterFile* FileParam = Cast(InParam); + if (!UploadDirectoryPath(FileParam)) + return false; + } + break; + + case EHoudiniParameterType::MultiParm: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue - MultiParm); + if (!UploadMultiParmValues(InParam)) + return false; + } + + break; + + case EHoudiniParameterType::FloatRamp: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue - FloatRamp); + if (!UploadRampParameter(InParam)) + return false; + } + break; + + case EHoudiniParameterType::ColorRamp: + { + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::UploadParameterValue - ColorRamp); + if (!UploadRampParameter(InParam)) + return false; + } + break; + + default: + { + // TODO: implement other parameter types! + return true; + } + break; + } + + // The parameter is no longer considered as changed + InParam->MarkChanged(false); + + return true; +} + +bool +FHoudiniParameterTranslator::RevertParameterToDefault(UHoudiniParameter* InParam) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::RevertParameterToDefault); + + if (!IsValid(InParam)) + return false; + + if (!InParam->IsPendingRevertToDefault()) + return false; + + TArray TupleToRevert; + InParam->GetTuplePendingRevertToDefault(TupleToRevert); + if (TupleToRevert.Num() <= 0) + return false; + + FString ParameterName = InParam->GetParameterName(); + + bool bReverted = true; + for (auto CurrentIdx : TupleToRevert ) + { + if (CurrentIdx == -1) + { + // revert the whole parameter to its default value + if (HAPI_RESULT_SUCCESS != FHoudiniApi::RevertParmToDefaults( + FHoudiniEngine::Get().GetSession(), + InParam->GetNodeId(), H_TCHAR_TO_UTF8(*ParameterName))) + { + HOUDINI_LOG_WARNING(TEXT("Failed to revert parameter %s to its default value."), *ParameterName); + bReverted = false; + } + } + else + { + // revert a tuple to its default value + if (HAPI_RESULT_SUCCESS != FHoudiniApi::RevertParmToDefault( + FHoudiniEngine::Get().GetSession(), + InParam->GetNodeId(), H_TCHAR_TO_UTF8(*ParameterName), CurrentIdx)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to revert parameter %s - %d to its default value."), *ParameterName, CurrentIdx); + bReverted = false; + } + } + } + + if (!bReverted) + return false; + + // The parameter no longer needs to be reverted + InParam->MarkDefault(true); + + return true; +} + +EHoudiniFolderParameterType +FHoudiniParameterTranslator::GetFolderTypeFromParamInfo(const HAPI_ParmInfo* ParamInfo) +{ + EHoudiniFolderParameterType Type = EHoudiniFolderParameterType::Invalid; + + switch (ParamInfo->scriptType) + { + case HAPI_PrmScriptType::HAPI_PRM_SCRIPT_TYPE_GROUPSIMPLE: + Type = EHoudiniFolderParameterType::Simple; + break; + + case HAPI_PrmScriptType::HAPI_PRM_SCRIPT_TYPE_GROUPCOLLAPSIBLE: + Type = EHoudiniFolderParameterType::Collapsible; + break; + + case HAPI_PrmScriptType::HAPI_PRM_SCRIPT_TYPE_GROUP: + Type = EHoudiniFolderParameterType::Tabs; + break; + + // Treat Radio folders as tabs for now + case HAPI_PrmScriptType::HAPI_PRM_SCRIPT_TYPE_GROUPRADIO: + Type = EHoudiniFolderParameterType::Radio; + break; + + default: + Type = EHoudiniFolderParameterType::Other; + break; + + } + + return Type; + +} + +bool +FHoudiniParameterTranslator::SyncMultiParmValuesAtLoad( + UHoudiniParameter* InParam, TArray>& OldParams, const int32& InAssetId, const HAPI_AssetInfo& AssetInfo) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::SyncMultiParmValuesAtLoad); + + UHoudiniParameterMultiParm* MultiParam = Cast(InParam); + if (!IsValid(MultiParam)) + return false; + + UHoudiniParameterRampFloat* FloatRampParameter = nullptr; + UHoudiniParameterRampColor* ColorRampParameter = nullptr; + + if (MultiParam->GetParameterType() == EHoudiniParameterType::FloatRamp) + FloatRampParameter = Cast(MultiParam); + else if (MultiParam->GetParameterType() == EHoudiniParameterType::ColorRamp) + ColorRampParameter = Cast(MultiParam); + + HAPI_NodeId NodeId = AssetInfo.nodeId; + + int32 Idx = 0; + int32 InstanceCount = -1; + HAPI_ParmId ParmId = -1; + TArray ParmInfos; + if (!GetMultiParmInstanceStartIdx(AssetInfo, MultiParam->GetParameterName(), Idx, InstanceCount, ParmId, ParmInfos)) + return false; + + const int32 InstanceCountInUnreal = FMath::Max(MultiParam->GetInstanceCount(), 0); + if (InstanceCount > InstanceCountInUnreal) + { + // The multiparm has more instances on the Houdini side, remove instances from the end until it has the same + // number as in Unreal. + // NOTE: Initially this code always removed the first instance. But that causes an issue if HAPI/Houdini does + // not immediately update the parameter names (param1, param2, param3, when param1 is removed, 2 -> 1, + // 3 -> 2) so that could result in GetParameters returning parameters with unique IDs, but where the names + // are not up to date, so in the above example, the last param could still be named param3 when it should + // be named param2. + const int32 Delta = InstanceCount - InstanceCountInUnreal; + for (int32 n = 0; n < Delta; ++n) + { + FHoudiniApi::RemoveMultiparmInstance( + FHoudiniEngine::Get().GetSession(), NodeId, + ParmId, MultiParam->InstanceStartOffset + InstanceCount - 1 - n); + } + } + else if (InstanceCountInUnreal > InstanceCount) + { + // The multiparm has fewer instances on the Houdini side, add instances at the end until it has the same + // number as in Unreal. + // NOTE: Initially this code always inserted before the first instance. But that causes an issue if HAPI/Houdini + // does not immediately update the parameter names (param1, param2, param3, when a param is inserted + // before 1, then 1->2, 2->3, 3->4 so that could result in GetParameters returning parameters with unique + // IDs, but where the names are not up to date, so in the above example, the now second param could still + // be named param1 when it should be named param2. + const int32 Delta = InstanceCountInUnreal - InstanceCount; + for (int32 n = 0; n < Delta; ++n) + { + FHoudiniApi::InsertMultiparmInstance( + FHoudiniEngine::Get().GetSession(), NodeId, + ParmId, MultiParam->InstanceStartOffset + InstanceCount + n); + } + } + + // We are going to Sync nested multi-params recursively + int32 MyParmId = InParam->GetParmId(); + // First, we need to manually look for our index in the old map + // Since there is a possibility that the parameter interface has changed since our previous cook + int32 MyIndex = -1; + for (int32 ParamIdx = 0; ParamIdx < OldParams.Num(); ParamIdx++) + { + UHoudiniParameter* CurrentOldParm = OldParams[ParamIdx]; + if (!IsValid(CurrentOldParm)) + continue; + + if (CurrentOldParm->GetParmId() != MyParmId) + continue; + + // We found ourself, exit now + MyIndex = ParamIdx; + break; + } + + if (MyIndex >= 0) + { + // Now Sync nested multi-params recursively + for (int32 ParamIdx = MyIndex + 1; ParamIdx < OldParams.Num(); ParamIdx++) + { + UHoudiniParameter* NextParm = OldParams[ParamIdx]; + if (!IsValid(NextParm)) + continue; + + if (NextParm->GetParentParmId() != MyParmId) + continue; + + if (NextParm->GetParameterType() != EHoudiniParameterType::MultiParm) + continue; + + // Always make sure to NOT recurse on ourselves! + // This could happen if parms have been deleted... + if (NextParm->GetParmId() == MyParmId) + continue; + + SyncMultiParmValuesAtLoad(NextParm, OldParams, InAssetId, AssetInfo); + } + } + + // The multiparm is a ramp, Get the param infos again, since the number of param instances is changed + if (!GetMultiParmInstanceStartIdx(AssetInfo, InParam->GetParameterName(), Idx, InstanceCount, ParmId, ParmInfos)) + return false; + + // Step 3: Set values of the inserted points + if (FloatRampParameter) + { + for (auto & Point : FloatRampParameter->Points) + { + // 1: update position float at param Idx + FHoudiniApi::SetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + NodeId, &(Point->Position), ParmInfos[Idx].floatValuesIndex, 1); + + // 2: update float value at param Idx + 1 + // float value + FHoudiniApi::SetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + NodeId, &(Point->Value), ParmInfos[Idx + 1].floatValuesIndex, 1); + + // 3: update interpolation type at param Idx + 2 + int32 IntValue = (int32)(Point->Interpolation); + FHoudiniApi::SetParmIntValues( + FHoudiniEngine::Get().GetSession(), + NodeId, &IntValue, ParmInfos[Idx + 2].intValuesIndex, 1); + + Idx += 3; + } + } + else if (ColorRampParameter) + { + for (auto& Point : ColorRampParameter->Points) + { + // 1: update position float at param Idx + FHoudiniApi::SetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + NodeId, &(Point->Position), ParmInfos[Idx].floatValuesIndex, 1); + + // 2: update color value at param Idx + 1 + // float value + FHoudiniApi::SetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + NodeId, (float*)(&Point->Value.R), ParmInfos[Idx + 1].floatValuesIndex, 3); + + // 3: update interpolation type at param Idx + 2 + int32 IntValue = (int32)(Point->Interpolation); + FHoudiniApi::SetParmIntValues( + FHoudiniEngine::Get().GetSession(), + NodeId, &IntValue, ParmInfos[Idx + 2].intValuesIndex, 1); + + Idx += 3; + } + } + + return true; +} + + +bool FHoudiniParameterTranslator::UploadRampParameter(UHoudiniParameter* InParam) +{ + UHoudiniParameterMultiParm* MultiParam = Cast(InParam); + if (!IsValid(MultiParam)) + return false; + + UHoudiniCookable* Cookable = Cast(InParam->GetOuter()); + if (!Cookable) + return false; + + int32 InsertIndexStart = -1; + UHoudiniParameterRampFloat* RampFloatParam = Cast(InParam); + UHoudiniParameterRampColor* RampColorParam = Cast(InParam); + + TArray> *Events = nullptr; + if (RampFloatParam) + { + Events = &(RampFloatParam->ModificationEvents); + InsertIndexStart = RampFloatParam->GetInstanceCount(); + } + else if (RampColorParam) + { + Events = &(RampColorParam->ModificationEvents); + InsertIndexStart = RampColorParam->GetInstanceCount(); + } + else + return false; + + // Handle All Events + Events->Sort([](const UHoudiniParameterRampModificationEvent& A, const UHoudiniParameterRampModificationEvent& B) + { + return A.DeleteInstanceIndex > B.DeleteInstanceIndex; + }); + + + // Step 1: Handle all delete events first + for (auto& Event : *Events) + { + if (!Event) + continue; + + if (!Event->IsDeleteEvent()) + continue; + + FHoudiniApi::RemoveMultiparmInstance( + FHoudiniEngine::Get().GetSession(), MultiParam->GetNodeId(), + MultiParam->GetParmId(), Event->DeleteInstanceIndex + MultiParam->InstanceStartOffset); + + InsertIndexStart -= 1; + } + + int32 InsertIndex = InsertIndexStart; + + + // Step 2: Handle all insert events + for (auto& Event : *Events) + { + if (!Event) + continue; + + if (!Event->IsInsertEvent()) + continue; + + FHoudiniApi::InsertMultiparmInstance( + FHoudiniEngine::Get().GetSession(), MultiParam->GetNodeId(), + MultiParam->GetParmId(), InsertIndex + MultiParam->InstanceStartOffset); + + InsertIndex += 1; + } + + // Step 3: Set inserted parameter values (only if there are instances inserted) + if (InsertIndex > InsertIndexStart) + { + if (Cookable) + { + // Get the asset's info + HAPI_AssetInfo AssetInfo; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAssetInfo( + FHoudiniEngine::Get().GetSession(), Cookable->GetNodeId(), &AssetInfo), false); + + int32 Idx = 0; + int32 InstanceCount = -1; + HAPI_ParmId ParmId = -1; + TArray ParmInfos; + if (!FHoudiniParameterTranslator::GetMultiParmInstanceStartIdx(AssetInfo, InParam->GetParameterName(), + Idx, InstanceCount, ParmId, ParmInfos)) + return false; + + if (InstanceCount < 0) + return false; + + // Instance count doesn't match, + if (InsertIndex != InstanceCount) + return false; + + // Starting index of parameters which we just inserted + Idx += 3 * InsertIndexStart; + + if (!ParmInfos.IsValidIndex(Idx + 2)) + return false; + + for (auto & Event : *Events) + { + if (!Event) + continue; + + if (!Event->IsInsertEvent()) + continue; + + // 1: update position float at param Idx + FHoudiniApi::SetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + AssetInfo.nodeId, &(Event->InsertPosition), ParmInfos[Idx].floatValuesIndex, 1); + + // step 2: update value at param Idx + 1 + if (Event->IsFloatRampEvent()) + { + // float value + FHoudiniApi::SetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + AssetInfo.nodeId, &(Event->InsertFloat), ParmInfos[Idx + 1].floatValuesIndex, 1); + } + else + { + // color value + FHoudiniApi::SetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + AssetInfo.nodeId, (float*)(&Event->InsertColor.R), ParmInfos[Idx + 1].floatValuesIndex, 3); + } + + // step 3: update interpolation type at param Idx + 2 + int32 IntValue = (int32)(Event->InsertInterpolation); + FHoudiniApi::SetParmIntValues( + FHoudiniEngine::Get().GetSession(), + AssetInfo.nodeId, &IntValue, ParmInfos[Idx + 2].intValuesIndex, 1); + + Idx += 3; + } + } + } + + // Step 4: clear all events + Events->Empty(); + + return true; +} + +bool FHoudiniParameterTranslator::UploadMultiParmValues(UHoudiniParameter* InParam) +{ + UHoudiniParameterMultiParm* MultiParam = Cast(InParam); + if (!MultiParam) + return false; + + TArray &LastModificationArray = MultiParam->MultiParmInstanceLastModifyArray; + + int32 Size = MultiParam->MultiParmInstanceLastModifyArray.Num(); + + for (int32 Index = 0; Index < Size; ++Index) + { + if (LastModificationArray[Index] == EHoudiniMultiParmModificationType::Inserted) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::InsertMultiparmInstance( + FHoudiniEngine::Get().GetSession(), + MultiParam->GetNodeId(), + MultiParam->GetParmId(), + Index + MultiParam->InstanceStartOffset), + false); + + } + } + + for (int32 Index = Size - 1; Index >= 0; --Index) + { + if (LastModificationArray[Index] == EHoudiniMultiParmModificationType::Removed) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::RemoveMultiparmInstance( + FHoudiniEngine::Get().GetSession(), + MultiParam->GetNodeId(), + MultiParam->GetParmId(), + Index + MultiParam->InstanceStartOffset), + false); + } + } + + // Remove all removal events. + for (int32 Index = Size - 1; Index >= 0; --Index) + { + if (LastModificationArray[Index] == EHoudiniMultiParmModificationType::Removed) + LastModificationArray.RemoveAt(Index); + } + + // The last modification array is resized. + Size = LastModificationArray.Num(); + + // Reset the last modification array + for (int32 Itr =Size - 1; Itr >= 0; --Itr) + { + LastModificationArray[Itr] = EHoudiniMultiParmModificationType::None; + } + + MultiParam->MultiParmInstanceCount = Size; + + return true; +} + +bool +FHoudiniParameterTranslator::UploadDirectoryPath(UHoudiniParameterFile* InParam) +{ + if(!InParam) + return false; + + for (int32 Index = 0; Index < InParam->GetNumValues(); ++Index) + { + std::string ConvertedString = H_TCHAR_TO_UTF8(*(InParam->GetValueAt(Index))); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), + InParam->GetNodeId(), ConvertedString.c_str(), InParam->GetParmId(), Index), false); + } + + return true; +} + +bool +FHoudiniParameterTranslator::GetMultiParmInstanceStartIdx(const HAPI_AssetInfo& InAssetInfo, const FString InParmName, + int32& OutStartIdx, int32& OutInstanceCount, HAPI_ParmId& OutParmId, TArray &OutParmInfos) +{ + // TODO: FIX/IMPROVE THIS! + // This is bad, that function can be called recursively, fetches all parameters, + // iterates on them, and fetches their name!! WTF! + // TODO: Slightly better now, at least we dont fetch every parameter's name! + + // Reset outputs + OutStartIdx = -1; + OutInstanceCount = -1; + OutParmId = -1; + OutParmInfos.Empty(); + + // Try to find the parameter by its name + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), InAssetInfo.nodeId, H_TCHAR_TO_UTF8(*InParmName), &OutParmId), false); + + if (OutParmId < 0) + return false; + + // Get the asset's node info + HAPI_NodeInfo NodeInfo; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), InAssetInfo.nodeId, &NodeInfo), false); + + // Get all parameters + OutParmInfos.SetNumUninitialized(NodeInfo.parmCount); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParameters( + FHoudiniEngine::Get().GetSession(), InAssetInfo.nodeId, &OutParmInfos[0], 0, NodeInfo.parmCount), false); + + OutStartIdx = 0; + for (const auto& CurrentParmInfo : OutParmInfos) + { + if (OutParmId == CurrentParmInfo.id) + { + OutInstanceCount = OutParmInfos[OutStartIdx].instanceCount; + + // Increment, to get the Start index of the ramp children parameters + OutStartIdx++; + return true; + } + + OutStartIdx++; + } + + // We failed to find the parm + OutStartIdx = -1; + + return false; +} + +bool +FHoudiniParameterTranslator::RevertRampParameters(TMap & InRampParams, const int32 & AssetId) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniParameterTranslator::RevertRampParameters); + if (InRampParams.Num() <= 0) + return true; + + // Get the asset's info + HAPI_AssetInfo AssetInfo; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAssetInfo( + FHoudiniEngine::Get().GetSession(), AssetId, &AssetInfo), false); + + // .. the asset's node info + HAPI_NodeInfo NodeInfo; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), AssetInfo.nodeId, &NodeInfo), false); + + // Retrieve all the parameter infos. + TArray< HAPI_ParmInfo > ParmInfos; + ParmInfos.SetNumUninitialized(NodeInfo.parmCount); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParameters( + FHoudiniEngine::Get().GetSession(), AssetInfo.nodeId, &ParmInfos[0], 0, NodeInfo.parmCount), false); + + int32 ParamIdx = 0; + while (ParamIdx < ParmInfos.Num()) + { + const HAPI_ParmInfo & ParmInfo = ParmInfos[ParamIdx]; + FString ParmName; + FHoudiniEngineString(ParmInfo.nameSH).ToFString(ParmName); + + if (InRampParams.Contains(ParmName)) + { + if (!InRampParams[ParmName]) + { + ParamIdx += 1; + continue; + } + + if (InRampParams[ParmName]->GetParameterType() == EHoudiniParameterType::FloatRamp) + { + UHoudiniParameterRampFloat * FloatRamp = Cast(InRampParams[ParmName]); + if (!FloatRamp) + { + ParamIdx += 1; + continue; + } + + if (ParmInfo.instanceCount != FloatRamp->NumDefaultPoints) + { + ParamIdx += 1; + continue; + } + + for (int32 PtIdx = 0; PtIdx < FloatRamp->NumDefaultPoints; ++PtIdx) + { + const HAPI_ParmInfo & PositionParmInfo = ParmInfos[ParamIdx + 1]; + const HAPI_ParmInfo & ValueParmInfo = ParmInfos[ParamIdx + 2]; + const HAPI_ParmInfo & InterpolationParmInfo = ParmInfos[ParamIdx + 3]; + + FHoudiniApi::SetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + NodeInfo.id, FloatRamp->DefaultPositions.GetData() + PtIdx, PositionParmInfo.floatValuesIndex, 1); + + FHoudiniApi::SetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + NodeInfo.id, FloatRamp->DefaultValues.GetData() + PtIdx, ValueParmInfo.floatValuesIndex, 1); + + FHoudiniApi::SetParmIntValues( + FHoudiniEngine::Get().GetSession(), + NodeInfo.id, FloatRamp->DefaultChoices.GetData() + PtIdx, InterpolationParmInfo.intValuesIndex, 1); + + ParamIdx += 3; + } + } + + if (InRampParams[ParmName]->GetParameterType() == EHoudiniParameterType::ColorRamp) + { + UHoudiniParameterRampColor * ColorRamp = Cast(InRampParams[ParmName]); + if (!ColorRamp) + { + ParamIdx += 1; + continue; + } + + if (ParmInfo.instanceCount != ColorRamp->NumDefaultPoints) + { + ParamIdx += 1; + continue; + } + + for (int32 PtIdx = 0; PtIdx < ColorRamp->NumDefaultPoints; ++PtIdx) + { + const HAPI_ParmInfo & PositionParmInfo = ParmInfos[ParamIdx + 1]; + const HAPI_ParmInfo & ValueParmInfo = ParmInfos[ParamIdx + 2]; + const HAPI_ParmInfo & InterpolationParmInfo = ParmInfos[ParamIdx + 3]; + + FHoudiniApi::SetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + NodeInfo.id, ColorRamp->DefaultPositions.GetData() + PtIdx, PositionParmInfo.floatValuesIndex, 1); + + FHoudiniApi::SetParmFloatValues( + FHoudiniEngine::Get().GetSession(), + NodeInfo.id, (float*)(&ColorRamp->DefaultValues[PtIdx].R), ValueParmInfo.floatValuesIndex, 3); + + FHoudiniApi::SetParmIntValues( + FHoudiniEngine::Get().GetSession(), + NodeInfo.id, ColorRamp->DefaultChoices.GetData() + PtIdx, InterpolationParmInfo.intValuesIndex, 1); + + ParamIdx += 3; + } + } + } + + ParamIdx += 1; + } + + return true; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniParameterTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "CoreMinimal.h" + +#include "HoudiniEnginePrivatePCH.h" + +class UHoudiniAsset; +class UHoudiniCookable; +class UHoudiniParameter; +class UHoudiniParameterFile; + +enum class EHoudiniFolderParameterType : uint8; +enum class EHoudiniParameterType : uint8; + +struct HOUDINIENGINE_API FHoudiniParameterTranslator +{ + static bool UpdateParameters( + HAPI_NodeId InNodeId, + UObject* InOuter, + TArray>& InParameters, + UHoudiniAsset* InHoudiniAsset, + const FString& InHapiAssetName, + bool bForceFullUpdate, + bool bCacheRampParms, + bool& bNeedToUpdateEditorProperties); + + static bool OnPreCookParameters(TArray>& InParams); + + // + static bool UpdateLoadedParameters( + HAPI_NodeId InNodeId, + TArray>& InParameters, + UObject* InOuter, + bool bForceFullUpdate, + bool bCacheRampParams, + bool& bNeedToUpdateEditorProperties); + + // + static bool UploadChangedParameters( + TArray>& InParameters, + HAPI_NodeId InNodeId); + + // + static bool UploadParameterValue(UHoudiniParameter* InParam); + + // + static bool UploadMultiParmValues(UHoudiniParameter* InParam); + + // + static bool UploadRampParameter(UHoudiniParameter* InParam); + + // + static bool UploadDirectoryPath(UHoudiniParameterFile* InParam); + + // + static bool RevertParameterToDefault(UHoudiniParameter* InParam); + + // + static bool SyncMultiParmValuesAtLoad( + UHoudiniParameter* MultiParam, TArray> &OldParams, const int32& InAssetId, const HAPI_AssetInfo& AssetInfo); + + // + static bool GetMultiParmInstanceStartIdx( + const HAPI_AssetInfo& InAssetInfo, + const FString InParmName, + int32& OutStartIdx, + int32& OutInstanceCount, + HAPI_ParmId& OutParmId, + TArray &OutParmInfos); + + /** Update parameters from the asset, re-uses parameters passed into CurrentParameters. + @AssetId: Id of the digital asset + @PrimaryObject: Object to use for transactions and as Outer for new top-level parameters + @CurrentParameters: pre: current & post: invalid parameters + @NewParameters: new params added to this + + On Return: CurrentParameters are the old parameters that are no longer valid, + NewParameters are new and re-used parameters. + */ + static bool BuildAllParameters( + HAPI_NodeId AssetId, + class UObject* OuterObject, + TArray>& CurrentParameters, + TArray>& NewParameters, + bool bUpdateValues, + bool InForceFullUpdate, + const UHoudiniAsset* InHoudiniAsset, + const FString& InHoudiniAssetName, + bool bCacheRampParms); + + // Parameter creation + static UHoudiniParameter * CreateTypedParameter( + class UObject * Outer, + const EHoudiniParameterType& ParmType, + const FString& ParmName ); + + // Parameter update + // bFullUpdate should be set to false after a minor update (change/recook) of the parameter + // and set to true when creating a new parameter + // bUpdateValue should be set to false when updating loaded parameters + // as the internal parameter's value from HAPI + static bool UpdateParameterFromInfo( + UHoudiniParameter * HoudiniParameter, + HAPI_NodeId InNodeId, + const HAPI_ParmInfo& ParmInfo, + bool bFullUpdate = true, + bool bUpdateValue = true, + const TArray* DefaultIntValues = nullptr, + const TArray* DefaultFloatValues = nullptr, + const TArray* DefaultStringValues = nullptr, + const TArray* DefaultChoiceValues = nullptr); + + static UClass* GetDesiredParameterClass(const HAPI_ParmInfo& ParmInfo); + + static void GetParmTypeFromParmInfo( + const HAPI_ParmInfo& ParmInfo, + EHoudiniParameterType& ParmType); + + static bool CheckParameterTypeAndClassMatch( + UHoudiniParameter* Parameter, + const EHoudiniParameterType& ParmType); + + /* + static bool CheckParameterClassAndInfoMatch( + UHoudiniParameter* Parameter, + const HAPI_ParmInfo& ParmInfo ); + */ + + // HAPI: Get a parameter's tag value. + static bool HapiGetParameterTagValue( + HAPI_NodeId NodeId, + HAPI_ParmId ParmId, + const FString& Tag, + FString& TagValue); + + // HAPI: Get a parameter's unit. + static bool HapiGetParameterUnit( + HAPI_NodeId NodeId, + HAPI_ParmId ParmId, + FString& OutUnitString ); + + // HAPI: Indicates if a parameter has a given tag + static bool HapiGetParameterHasTag( + HAPI_NodeId NodeId, + HAPI_ParmId ParmId, + const FString& Tag); + + // Get folder parameter type from HAPI_ParmInfo struct + static EHoudiniFolderParameterType GetFolderTypeFromParamInfo( + const HAPI_ParmInfo* ParamInfo); + + static bool RevertRampParameters(TMap & InRampParams, const int32 & AssetId); +}; +``` + +`Source/HoudiniEngine/Private/HoudiniSkeletalMeshTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniSkeletalMeshTranslator.h" + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniMeshTranslator.h" +#include "HoudiniSkeletalMeshUtils.h" + +#include "Animation/Skeleton.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Components/SkeletalMeshComponent.h" +#include "Engine/SkeletalMesh.h" +#include "Factories/FbxSkeletalMeshImportData.h" +#include "IMeshBuilderModule.h" +#include "PhysicsAssetGenerationSettings.h" +#include "PhysicsAssetUtils.h" +#include "ImportUtils/SkeletalMeshImportUtils.h" +#include "Materials/Material.h" +#include "Materials/MaterialInterface.h" +#include "Math/UnrealMathUtility.h" +#include "Rendering/SkeletalMeshLODImporterData.h" +#include "Rendering/SkeletalMeshModel.h" +#include "ReferenceSkeleton.h" +#include "PhysicsEngine/PhysicsAsset.h" +#include "PhysicalMaterials/PhysicalMaterial.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "Engine/SkinnedAssetCommon.h" +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + #include "PhysicsEngine/BodySetup.h" + #include "PhysicsEngine/SkeletalBodySetup.h" +#endif + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + + + +// +// Process and fill in the mesh ref skeleton bone hierarchy using the raw binary import data +// (difference from epic - Remove any FBX Importer depenedencies) +// +// @param RefSkeleton - [out] reference skeleton hierarchy to update +// @param SkeletalDepth - [out] depth of the reference skeleton hierarchy +// @param ImportData - raw binary import data to process +// @return true if the operation completed successfully +// +bool +ProcessImportMeshSkeleton( + const USkeleton * SkeletonAsset, + FReferenceSkeleton & OutRefSkeleton, + int32 & OutSkeletalDepth, + FSkeletalMeshImportData & ImportData) +{ + // Setup skeletal hierarchy + names structure. + OutRefSkeleton.Empty(); + + FReferenceSkeletonModifier RefSkelModifier(OutRefSkeleton, SkeletonAsset); + + // Digest bones to the serializable format. + TArray& RefBonesBinary = ImportData.RefBonesBinary; + for (int32 b = 0; b < RefBonesBinary.Num(); b++) + { + const SkeletalMeshImportData::FBone& BinaryBone = RefBonesBinary[b]; + const FString BoneName = FSkeletalMeshImportData::FixupBoneName(BinaryBone.Name); + const FMeshBoneInfo BoneInfo(FName(*BoneName, FNAME_Add), BinaryBone.Name, BinaryBone.ParentIndex); + const FTransform BoneTransform(BinaryBone.BonePos.Transform); + + if (OutRefSkeleton.FindRawBoneIndex(BoneInfo.Name) != INDEX_NONE) + { + HOUDINI_LOG_MESSAGE(TEXT("SkeletonHasDuplicateBones: Skeleton has non-unique bone names.\nBone named %s encountered more than once."), *BoneName); + } + + RefSkelModifier.Add(BoneInfo, BoneTransform); + } + + if (RefBonesBinary.Num() != OutRefSkeleton.GetRawBoneNum()) + { + HOUDINI_LOG_MESSAGE(TEXT("ProcessImportMeshSkeleton : RefBonesBinary Not Equal to RefSkeleton")); + } + + // Add hierarchy index to each bone and detect max depth. + OutSkeletalDepth = 0; + TArray SkeletalDepths; + SkeletalDepths.AddZeroed(OutRefSkeleton.GetRawBoneNum()); + for (int32 b = 0; b < OutRefSkeleton.GetRawBoneNum(); b++) + { + int32 ParentIndex = OutRefSkeleton.GetRawParentIndex(b); + + int32 Depth = 1.0f; + SkeletalDepths[b] = 1.0f; + if (SkeletalDepths.IsValidIndex(ParentIndex)) + { + Depth += SkeletalDepths[ParentIndex]; + } + + if (OutSkeletalDepth < Depth) + { + OutSkeletalDepth = Depth; + } + SkeletalDepths[b] = Depth; + } + + return true; +} + + + +// Raw data bone. +struct FBoneTracker +{ + SkeletalMeshImportData::FBone Bone; + int32 OrigIndex = 0; + int32 NewIndex = 0; +}; + + + +void +AddChildren( + TArray&OutSortedBones, + int Parent, + const TArray&RefBonesBinary) +{ + //Bone.NumChildren + for (int32 i = 0; i < RefBonesBinary.Num(); i++) + { + if (RefBonesBinary[i].ParentIndex != Parent) + { + continue; + } + FBoneTracker* BoneTracker = OutSortedBones.FindByPredicate([i](FBoneTracker& BoneTracker) { + return BoneTracker.OrigIndex == i; + }); + + if (BoneTracker != nullptr) + { + continue; + } + FBoneTracker NewBone; + NewBone.Bone = RefBonesBinary[i]; + NewBone.OrigIndex = i; + OutSortedBones.Add(NewBone); + AddChildren(OutSortedBones, i, RefBonesBinary); + } +} + + + +//Resorts Bones By Their ParentIndex +void +SortBonesByParent(FSkeletalMeshImportData & SkeletalMeshImportData) +{ + TArray & RefBonesBinary = SkeletalMeshImportData.RefBonesBinary; + TArray SortedBones; + + //Add all with no parent + //AddChildren(SortedBones, -1, RefBonesBinary); + for (int32 b = 0; b < RefBonesBinary.Num(); b++) + { + SkeletalMeshImportData::FBone Bone = RefBonesBinary[b]; + //add all with parent self and their children + if ((Bone.ParentIndex == b) || (Bone.ParentIndex == -1)) + { + FBoneTracker NewBone; + NewBone.Bone = RefBonesBinary[b]; + NewBone.Bone.ParentIndex = -1; + NewBone.OrigIndex = b; + SortedBones.Add(NewBone); + AddChildren(SortedBones, b, RefBonesBinary); + } + } + + //store back in proper order + for (int32 b = 0; b < SortedBones.Num(); b++) + { + SortedBones[b].NewIndex = b; + RefBonesBinary[b] = SortedBones[b].Bone; + } + + //update Parent to new index + for (int32 i = 0; i < SkeletalMeshImportData.RefBonesBinary.Num(); i++) + { + int32 OldParentIndex = SkeletalMeshImportData.RefBonesBinary[i].ParentIndex; + //skip reparenting root + if (OldParentIndex == -1) + continue; + //Lookup incorrect oldparent + FBoneTracker* BoneTracker = SortedBones.FindByPredicate([OldParentIndex](FBoneTracker& BoneTracker) { + return BoneTracker.OrigIndex == OldParentIndex; + }); + int32 NewParentIndex = BoneTracker->NewIndex; + SkeletalMeshImportData.RefBonesBinary[i].ParentIndex = NewParentIndex; + } + + //update influence indexes + for (int32 i = 0; i < SkeletalMeshImportData.Influences.Num(); i++) + { + int32 OldIndex = SkeletalMeshImportData.Influences[i].BoneIndex; + FBoneTracker* BoneTracker = SortedBones.FindByPredicate([OldIndex](FBoneTracker& BoneTracker) { + return BoneTracker.OrigIndex == OldIndex; + }); + if (BoneTracker == nullptr) + { + continue; + } + int32 NewIndex = BoneTracker->NewIndex; + SkeletalMeshImportData.Influences[i].BoneIndex = NewIndex; + } +} + + + +//Builds Skeletal Mesh and Skeleton Assets from FSkeletalMeshImportData +void +FHoudiniSkeletalMeshTranslator::CreateUnrealData(FHoudiniSkeletalMeshBuildSettings& BuildSettings) +{ + FSkeletalMeshImportData& ImportData = BuildSettings.SkeletalMeshImportData; + USkeleton* Skeleton = BuildSettings.Skeleton; + FBox3f BoundingBox(ImportData.Points.GetData(), ImportData.Points.Num()); + + // Setup NewMesh defaults + FSkeletalMeshModel* ImportedResource = BuildSettings.SKMesh->GetImportedModel(); + check(ImportedResource->LODModels.Num() == 0); + ImportedResource->LODModels.Empty(); + ImportedResource->LODModels.Add(new FSkeletalMeshLODModel()); + const int32 ImportLODModelIndex = 0; + FSkeletalMeshLODModel& NewLODModel = ImportedResource->LODModels[ImportLODModelIndex]; + + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 4 + BuildSettings.SKMesh->SaveLODImportedData(0, ImportData); //Import the ImportData +#endif + + int32 SkeletalDepth = 0; + FReferenceSkeleton& RefSkeleton = BuildSettings.SKMesh->GetRefSkeleton(); + bool bSuccess = SkeletalMeshImportUtils::ProcessImportMeshSkeleton(Skeleton, RefSkeleton, SkeletalDepth, ImportData); + if (!bSuccess) + { + HOUDINI_LOG_ERROR(TEXT("SkeletalMeshImportUtils::ProcessImportMeshSkeleton() failed.")); + return; + } + + for (SkeletalMeshImportData::FMaterial SkeletalImportMaterial : ImportData.Materials) + { + UMaterialInterface* MaterialInterface; + MaterialInterface = Cast( + StaticLoadObject(UMaterialInterface::StaticClass(), + nullptr, *SkeletalImportMaterial.MaterialImportName, nullptr, LOAD_NoWarn, nullptr)); + + if (!IsValid(MaterialInterface)) + { + MaterialInterface = Cast(SkeletalImportMaterial.Material); + } + + FSkeletalMaterial SkeletalMaterial; + SkeletalMaterial.MaterialInterface = MaterialInterface; + BuildSettings.SKMesh->GetMaterials().Add(SkeletalMaterial); + } + + // process bone influences from import data + SkeletalMeshImportUtils::ProcessImportMeshInfluences(ImportData, BuildSettings.SKMesh->GetPathName()); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + BuildSettings.SKMesh->SetNumSourceModels(0); +#else + BuildSettings.SKMesh->ResetLODInfo(); +#endif + FSkeletalMeshLODInfo& NewLODInfo = BuildSettings.SKMesh->AddLODInfo(); + NewLODInfo.ReductionSettings.NumOfTrianglesPercentage = 1.0f; + NewLODInfo.ReductionSettings.NumOfVertPercentage = 1.0f; + NewLODInfo.ReductionSettings.MaxDeviationPercentage = 0.0f; + NewLODInfo.LODHysteresis = 0.02f; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + PRAGMA_DISABLE_DEPRECATION_WARNINGS + BuildSettings.SKMesh->SaveLODImportedData(ImportLODModelIndex, ImportData); + PRAGMA_ENABLE_DEPRECATION_WARNINGS +#endif + + FBoxSphereBounds3f bsb3f = FBoxSphereBounds3f(BoundingBox); + BuildSettings.SKMesh->SetImportedBounds(FBoxSphereBounds(bsb3f)); + // Store whether or not this mesh has vertex colors + BuildSettings.SKMesh->SetHasVertexColors(ImportData.bHasVertexColors); + //NewMesh->VertexColorGuid = Mesh->bHasVertexColors ? FGuid::NewGuid() : FGuid(); + + // Pass the number of texture coordinate sets to the LODModel. Ensure there is at least one UV coord + NewLODModel.NumTexCoords = ImportData.NumTexCoords; + + //int ImportLODModelIndex = 0; + //The imported LOD is always 0 here, the LOD custom import will import the LOD alone(in a temporary skeletalmesh) and add it to the base skeletal mesh later + check(BuildSettings.SKMesh->GetLODInfo(ImportLODModelIndex) != nullptr); + //Set the build options + FSkeletalMeshBuildSettings BuildOptions; + //Make sure the build option change in the re-import ui is reconduct + //BuildOptions.bBuildAdjacencyBuffer = true; + BuildOptions.bUseFullPrecisionUVs = false; + BuildOptions.bUseBackwardsCompatibleF16TruncUVs = false; + BuildOptions.bUseHighPrecisionTangentBasis = false; + //BuildOptions.bRecomputeNormals = !SkeletalMeshImportData.bHasNormals; + //BuildOptions.bRecomputeTangents = !SkeletalMeshImportData.bHasTangents; + BuildOptions.bRecomputeNormals = true; + BuildOptions.bRecomputeTangents = true; + //BuildOptions.bComputeWeightedNormals = true; + BuildOptions.bUseMikkTSpace = true; + //BuildOptions.bRecomputeNormals = !ImportOptions->ShouldImportNormals() || !SkelMeshImportDataPtr->bHasNormals; + //BuildOptions.bRecomputeTangents = !ImportOptions->ShouldImportTangents() || !SkelMeshImportDataPtr->bHasTangents; + //BuildOptions.bUseMikkTSpace = (ImportOptions->NormalGenerationMethod == EFBXNormalGenerationMethod::MikkTSpace) && (!ImportOptions->ShouldImportNormals() || !ImportOptions->ShouldImportTangents()); + //BuildOptions.bComputeWeightedNormals = ImportOptions->bComputeWeightedNormals; + //BuildOptions.bRemoveDegenerates = ImportOptions->bRemoveDegenerates; + //BuildOptions.ThresholdPosition = ImportOptions->OverlappingThresholds.ThresholdPosition; + //BuildOptions.ThresholdTangentNormal = ImportOptions->OverlappingThresholds.ThresholdTangentNormal; + //BuildOptions.ThresholdUV = ImportOptions->OverlappingThresholds.ThresholdUV; + //BuildOptions.MorphThresholdPosition = ImportOptions->OverlappingThresholds.MorphThresholdPosition; + BuildSettings.SKMesh->GetLODInfo(ImportLODModelIndex)->BuildSettings = BuildOptions; + //New MeshDescription build process + IMeshBuilderModule& MeshBuilderModule = IMeshBuilderModule::GetForRunningPlatform(); + //We must build the LODModel so we can restore properly the mesh, but we do not have to regenerate LODs + FSkeletalMeshBuildParameters SkeletalMeshBuildParameters = FSkeletalMeshBuildParameters(BuildSettings.SKMesh, GetTargetPlatformManagerRef().GetRunningTargetPlatform(), ImportLODModelIndex, false); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FSkeletalMeshRenderData* RenderData = BuildSettings.SKMesh->GetResourceForRendering(); + if (RenderData == nullptr) + { + BuildSettings.SKMesh->AllocateResourceForRendering(); + RenderData = BuildSettings.SKMesh->GetResourceForRendering(); + } + + bool bBuildSuccess = MeshBuilderModule.BuildSkeletalMesh(*RenderData, SkeletalMeshBuildParameters); +#else + bool bBuildSuccess = MeshBuilderModule.BuildSkeletalMesh(SkeletalMeshBuildParameters); +#endif + + //We need to have a valid render data to create physic asset + BuildSettings.SKMesh->CalculateInvRefMatrices(); + BuildSettings.SKMesh->Build(); + BuildSettings.SKMesh->MarkPackageDirty(); + FAssetRegistryModule::AssetCreated(BuildSettings.SKMesh); + + //CREATE A NEW SKELETON ASSET IF NEEDED + if (Skeleton == nullptr) + { + FString ObjectName = FString::Printf(TEXT("%s_Skeleton"), *BuildSettings.SKMesh->GetName()); + Skeleton = NewObject(BuildSettings.SKPackage, *ObjectName, RF_Public | RF_Standalone); + Skeleton->MarkPackageDirty(); + } + bSuccess = Skeleton->MergeAllBonesToBoneTree(BuildSettings.SKMesh); + if (!bSuccess) + { + HOUDINI_LOG_ERROR(TEXT("MergeAllBonesToBoneTree() failed.")); + return; + } + + BuildSettings.SKMesh->SetSkeleton(Skeleton); + UE_LOG(LogTemp, Log, TEXT("SkeletalMeshImportData: Materials %i Points %i Wedges %i Faces %i Influences %i"), + ImportData.Materials.Num(), + ImportData.Points.Num(), + ImportData.Wedges.Num(), + ImportData.Faces.Num(), + ImportData.Influences.Num()); +} + + + +//swap y and z +FVector3f +ConvertDir(FVector3f Vector) +{ + FVector3f Out; + Out[0] = Vector[0]; + Out[1] = Vector[2]; + Out[2] = Vector[1]; + return Out; +} + + + +float +FHoudiniSkeletalMeshTranslator::GetSkeletonImportScale(const FHoudiniGeoPartObject& ShapeMeshHGPO) +{ + HAPI_NodeId GeoId = ShapeMeshHGPO.GeoId; + HAPI_NodeId PartId = ShapeMeshHGPO.PartId; + + bool bFoundImportScaleAttribute = false; + + //ImportScale---------------------------------------------------------------------------------------- + HAPI_AttributeInfo UnrealSKImportScaleInfo; + FHoudiniApi::AttributeInfo_Init(&UnrealSKImportScaleInfo); + HAPI_Result UnrealSKImportScaleInfoResult = FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + GeoId, + PartId, + HAPI_UNREAL_ATTRIB_SKELETON_IMPORT_SCALE, + HAPI_AttributeOwner::HAPI_ATTROWNER_DETAIL, + &UnrealSKImportScaleInfo); + + //check result + float UnrealSKImportScale = 100.0f; + if (UnrealSKImportScaleInfo.exists) + { + TArray UnrealSKImportScaleArray; + FHoudiniHapiAccessor Accessor(GeoId, PartId, HAPI_UNREAL_ATTRIB_SKELETON_IMPORT_SCALE); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, UnrealSKImportScaleArray); + + if (bSuccess && UnrealSKImportScaleArray.Num() > 0) + { + UnrealSKImportScale = UnrealSKImportScaleArray[0]; + } + } + return UnrealSKImportScale; +} + + +bool +FHoudiniSkeletalMeshTranslator::FindAttributeOnSkeletalMeshShapeParts(const FHoudiniSkeletalMeshParts& InSKParts, + const char* AttribName, HAPI_NodeId& OutGeoId, HAPI_PartId& OutPartId) +{ + if (InSKParts.HGPOShapeInstancer && FHoudiniEngineUtils::HapiCheckAttributeExists(InSKParts.HGPOShapeInstancer->GeoId, InSKParts.HGPOShapeInstancer->PartId, AttribName)) + { + // Found unreal_skeleton on the Shape packed prim + OutGeoId = InSKParts.HGPOShapeInstancer->GeoId; + OutPartId = InSKParts.HGPOShapeInstancer->PartId; + return true; + } + + if (InSKParts.HGPOShapeMesh && FHoudiniEngineUtils::HapiCheckAttributeExists(InSKParts.HGPOShapeMesh->GeoId, InSKParts.HGPOShapeMesh->PartId, AttribName)) + { + // Found unreal_skeleton inside the Shape packed prim + OutGeoId = InSKParts.HGPOShapeMesh->GeoId; + OutPartId = InSKParts.HGPOShapeMesh->PartId; + return true; + } + + return false; +} + +FHoudiniSkeletalMesh +FHoudiniSkeletalMeshTranslator::GetSkeletalMeshMeshData(HAPI_NodeId ShapeGeoId, HAPI_NodeId ShapePartId, bool bImportNormals) +{ + FHoudiniSkeletalMesh Mesh; + + //----------------------------------------------------------------------------------- + // Shape Infos + //----------------------------------------------------------------------------------- + + HAPI_PartInfo ShapeMeshPartInfo; + FHoudiniApi::PartInfo_Init(&ShapeMeshPartInfo); + FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), ShapeGeoId, ShapePartId, &ShapeMeshPartInfo); + + //----------------------------------------------------------------------------------- + // Rest Geometry Points + //----------------------------------------------------------------------------------- + + FHoudiniHapiAccessor Accessor; + Accessor.Init(ShapeGeoId, ShapePartId, HAPI_UNREAL_ATTRIB_POSITION); + bool bSuccess = Accessor.GetAttributeData(HAPI_AttributeOwner::HAPI_ATTROWNER_POINT, Mesh.Positions); + + //----------------------------------------------------------------------------------- + // UVs + //----------------------------------------------------------------------------------- + + FHoudiniEngineUtils::UpdateMeshPartUVSets(ShapeGeoId, ShapePartId, true, Mesh.UVSets, Mesh.AttribInfoUVSets); + + //----------------------------------------------------------------------------------- + // Normals + //----------------------------------------------------------------------------------- + + bool bUseComputedNormals = !bImportNormals; + if (!bUseComputedNormals) + { + Accessor.Init(ShapeGeoId, ShapePartId, HAPI_UNREAL_ATTRIB_NORMAL); + bSuccess = Accessor.GetAttributeData(HAPI_AttributeOwner::HAPI_ATTROWNER_POINT, Mesh.Normals); + } + + //----------------------------------------------------------------------------------- + // Vertex Colors + //----------------------------------------------------------------------------------- + + Accessor.Init(ShapeGeoId, ShapePartId, HAPI_UNREAL_ATTRIB_COLOR); + Accessor.GetInfo(Mesh.ColorInfo, HAPI_ATTROWNER_INVALID); + bool bColorInfoExists = Accessor.GetAttributeData(Mesh.ColorInfo, Mesh.Colors); + + //----------------------------------------------------------------------------------- + // Tangents + //----------------------------------------------------------------------------------- + + Accessor.Init(ShapeGeoId, ShapePartId, HAPI_UNREAL_ATTRIB_TANGENTU); + Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, Mesh.Tangents); + + //----------------------------------------------------------------------------------- + // Materials + //----------------------------------------------------------------------------------- + + Mesh.Materials = GetMaterials(ShapeGeoId, ShapePartId, ShapeMeshPartInfo.faceCount); + + //----------------------------------------------------------------------------------- + // Indices + //----------------------------------------------------------------------------------- + HAPI_AttributeInfo VertexInfo; + FHoudiniApi::AttributeInfo_Init(&VertexInfo); + + HAPI_Result VertexInfoResult = FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + ShapeGeoId, + ShapePartId, + "__vertex_id", + HAPI_AttributeOwner::HAPI_ATTROWNER_VERTEX, + &VertexInfo); + + if (!VertexInfo.exists || VertexInfo.count <= 0) + { + HOUDINI_LOG_ERROR(TEXT("Error Creating Skeletal Mesh : No Vertex Info")); + return {}; + } + + + Mesh.Vertices.SetNum(VertexInfo.count); + HAPI_Result VertexDataResult = FHoudiniApi::GetVertexList( + FHoudiniEngine::Get().GetSession(), + ShapeGeoId, + ShapePartId, + &Mesh.Vertices[0], + 0, + VertexInfo.count); + + return Mesh; +} +bool +FHoudiniSkeletalMeshTranslator::SetSkeletalMeshImportDataInfluences( + FSkeletalMeshImportData& SkeletalMeshImportData, + const FHoudiniInfluences& Influences, + const FHoudiniPackageParams& PackageParams) +{ + for (int32 PointIndex = 0; PointIndex < Influences.NumVertices; PointIndex++) + { + for (int Influence = 0; Influence < Influences.NumInfluences; Influence++) + { + const FHoudiniSkinInfluence& SkinInfluence = Influences.Influences[PointIndex * Influences.NumInfluences + Influence]; + + SkeletalMeshImportData::FRawBoneInfluence UnrealInfluence; + UnrealInfluence.VertexIndex = PointIndex; + UnrealInfluence.BoneIndex = SkinInfluence.Bone ? SkinInfluence.Bone->UnrealBoneNumber : 0; + UnrealInfluence.Weight = SkinInfluence.Weight; + SkeletalMeshImportData.Influences.Add(UnrealInfluence); + } + } + return true; +} + + + +bool +FHoudiniSkeletalMeshTranslator::SetSkeletalMeshImportDataMesh( + FSkeletalMeshImportData& SkeletalMeshImportData, + const FHoudiniSkeletalMesh& Mesh, + const FHoudiniPackageParams& PackageParams) +{ + + SkeletalMeshImportData.Points.SetNum(Mesh.Positions.Num()); + int32 Index = 0; + for (FVector3f Point : Mesh.Positions) + { + //flip x and z + SkeletalMeshImportData.Points[Index] = FHoudiniEngineUtils::ConvertHoudiniPositionToUnrealVector3f(Point); + SkeletalMeshImportData.PointToRawMap.Add(Index); + Index++; + } + + bool bUseComputedNormals = Mesh.Normals.IsEmpty(); + + bool bColorInfoExists = !Mesh.Colors.IsEmpty(); + + //----------------------------------------------------------------------------------- + // Materials + //----------------------------------------------------------------------------------- + + TArray PerFaceUEMaterialIds; + TArray UniqueMaterials; + if (!FHoudiniSkeletalMeshTranslator::LoadOrCreateMaterials( + Mesh.Materials, + PackageParams, + PerFaceUEMaterialIds, + SkeletalMeshImportData)) + { + // Unable to retrieve materials, should we use default Houdini one? + HOUDINI_LOG_ERROR(TEXT("Creating Skeletal Mesh : unable to load/create materials")); + } + + //----------------------------------------------------------------------------------- + // LoadInWedgeData + // FACES AND WEDGES + //----------------------------------------------------------------------------------- + int32 face_id = 0; + int32 face_idx = 0; + int32 count = 0; + + { + int NumTexCoords = 0; + for (int32 TexCoordIndex = 0; TexCoordIndex < MAX_STATIC_TEXCOORDS; ++TexCoordIndex) + { + if (!Mesh.AttribInfoUVSets.IsValidIndex(TexCoordIndex)) + continue; + if (!Mesh.AttribInfoUVSets[TexCoordIndex].exists) + continue; + ++NumTexCoords; + } + SkeletalMeshImportData.NumTexCoords = NumTexCoords; + } + + SkeletalMeshImportData::FTriangle Triangle; + for (int VertexInstanceIndex = 0; VertexInstanceIndex < Mesh.Vertices.Num(); ++VertexInstanceIndex) + { + int VertexIndex = Mesh.Vertices[VertexInstanceIndex]; + SkeletalMeshImportData::FVertex Wedge; + Wedge.VertexIndex = VertexIndex; + + for (int32 TexCoordIndex = 0; TexCoordIndex < MAX_STATIC_TEXCOORDS; ++TexCoordIndex) + { + if (!Mesh.AttribInfoUVSets.IsValidIndex(TexCoordIndex)) + continue; + if (!Mesh.AttribInfoUVSets[TexCoordIndex].exists) + continue; + + int UVIndex = 0; + switch (Mesh.AttribInfoUVSets[TexCoordIndex].owner) + { + case HAPI_ATTROWNER_VERTEX: + UVIndex = VertexInstanceIndex; + break; + case HAPI_ATTROWNER_POINT: + UVIndex = VertexIndex; + break; + default: + // We don't support UV attributes on anything other than (houdini) points or verts. + break; + } + + int UVTupleSize = Mesh.AttribInfoUVSets[TexCoordIndex].tupleSize; + const TArray& UVData = Mesh.UVSets[TexCoordIndex]; + // ERROR: This keeps going out of bounds. Why are we getting point UVs ? Should be vertex?! + Wedge.UVs[TexCoordIndex] = FVector2f(UVData[UVIndex * UVTupleSize], 1.0f - UVData[UVIndex * UVTupleSize + 1]); + + if (bColorInfoExists) + { + + int ColorIndex = (Mesh.ColorInfo.owner == HAPI_ATTROWNER_VERTEX ? VertexInstanceIndex : VertexIndex) * Mesh.ColorInfo.tupleSize; + Wedge.Color = FLinearColor(Mesh.Colors[ColorIndex], Mesh.Colors[ColorIndex + 1], Mesh.Colors[ColorIndex + 2]).ToFColor(false); + } + } + + //Wedge.MatIndex = + SkeletalMeshImportData.Wedges.Add(Wedge); + Triangle.WedgeIndex[face_idx] = count; + Triangle.SmoothingGroups = 255; + Triangle.MatIndex = PerFaceUEMaterialIds.IsEmpty() ? 0 : PerFaceUEMaterialIds[face_id]; + + // Store normal for each vertex of face + FVector3f ConvertedNormal; + if (bUseComputedNormals) + { + ConvertedNormal = FVector3f::ZeroVector; + } + else + { + FVector3f n = Mesh.Normals[count]; + ConvertedNormal = ConvertDir(n); + ConvertedNormal.Normalize(); + } + Triangle.TangentZ[face_idx] = ConvertedNormal; + + // Compute tangent/binormal from the normal? + FVector3f TangentX, TangentY; + Triangle.TangentZ[face_idx].FindBestAxisVectors(TangentX, TangentY); + + count++; + face_idx++; + + // We're starting the next triangle so store the old one + if ((count % 3) == 0) + { + SkeletalMeshImportData::FVertex Wedge1 = SkeletalMeshImportData.Wedges[count - 3]; + SkeletalMeshImportData::FVertex Wedge2 = SkeletalMeshImportData.Wedges[count - 2]; + SkeletalMeshImportData::FVertex Wedge3 = SkeletalMeshImportData.Wedges[count - 1]; + + SkeletalMeshImportData.Wedges[count - 3] = Wedge3; + SkeletalMeshImportData.Wedges[count - 1] = Wedge1; + + //tangent winding + FVector3f Tangent0 = Triangle.TangentZ[0]; + FVector3f Tangent1 = Triangle.TangentZ[1]; + FVector3f Tangent2 = Triangle.TangentZ[2]; + + Triangle.TangentZ[0] = Tangent2; + Triangle.TangentZ[2] = Tangent0; + + SkeletalMeshImportData.Faces.Add(Triangle); + + face_id++; + face_idx = 0; + } + } + + SkeletalMeshImportData.bHasVertexColors = Mesh.ColorInfo.exists; + + SkeletalMeshImportData.bHasNormals = true; + SkeletalMeshImportData.bHasTangents = false; + + return true; +} + + +bool +FHoudiniSkeletalMeshTranslator::SetSkeletalMeshImportDataSkeleton( + FSkeletalMeshImportData& SkeletalMeshImportData, + const FHoudiniSkeleton& Skeleton, + const FHoudiniPackageParams& InPackageParams) +{ + if (Skeleton.Bones.IsEmpty()) + return false; + + SkeletalMeshImportData.RefBonesBinary.SetNum(Skeleton.Bones.Num()); + int32 BoneIndex = 0; + for (int JointIndex = 0; JointIndex < Skeleton.Bones.Num(); JointIndex++) + { + const FHoudiniSkeletonBone& Bone = Skeleton.Bones[JointIndex]; + SkeletalMeshImportData::FBone NewBone; + NewBone.Name = Bone.Name; + NewBone.Flags = 0; + NewBone.ParentIndex = Bone.Parent ? Bone.Parent->UnrealBoneNumber : -1; + NewBone.NumChildren = Bone.Children.Num(); + + SkeletalMeshImportData::FJointPos JointPos; + JointPos.Transform = FTransform3f(FTransform(Bone.UnrealLocalMatrix)); + + NewBone.BonePos = JointPos; + + if (SkeletalMeshImportData.RefBonesBinary.IsValidIndex(BoneIndex)) + SkeletalMeshImportData.RefBonesBinary[BoneIndex] = NewBone; + + BoneIndex++; + } + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 4 + SkeletalMeshImportData.bDiffPose = false; + SkeletalMeshImportData.bUseT0AsRefPose = false; +#endif + + return true; +} + +bool +FHoudiniSkeletalMeshTranslator::CreateSkeletalMeshImportData( + FSkeletalMeshImportData & SkeletalMeshImportData, + const FHoudiniSkeletalMesh & Mesh, + const FHoudiniSkeleton & Skeleton, + const FHoudiniInfluences & SkinWeights, + const FHoudiniPackageParams& PackageParams) +{ + bool bSuccess = true; + bSuccess &= SetSkeletalMeshImportDataMesh(SkeletalMeshImportData, Mesh, PackageParams); + bSuccess &= SetSkeletalMeshImportDataSkeleton(SkeletalMeshImportData, Skeleton, PackageParams); + bSuccess &= SetSkeletalMeshImportDataInfluences(SkeletalMeshImportData, SkinWeights, PackageParams); + return bSuccess; +} + +//Creates SkelatalMesh and Skeleton Assets and Packages, and adds them to OutputObjects +bool FHoudiniSkeletalMeshTranslator::ProcessSkeletalMeshParts() +{ + // If we have a mesh but no skeleton, bail. Its always required. + if (!SKParts.HasSkeleton()) + { + HOUDINI_LOG_ERROR(TEXT("No skeleton / pose found for skeletal mesh")); + return false; + } + + //---------------------------------------------------------------------------------------------------------------------------------------- + // if unreal_skeleton attribute is present, load the skeletal mesh asset. + //---------------------------------------------------------------------------------------------------------------------------------------- + + USkeleton* SkeletonAsset = nullptr; + FHoudiniSkeleton UnrealSkeleton; + bool bUseExistingSkeleton = false; + + // Look for unreal_skeleton attribute on the Shape packed prim (instancer) level, then on the mesh HGPO level. + bool bFoundUnrealSkeletonPath = false; + int SkeletonPathGeoId = INDEX_NONE; + int SkeletonPathPartId = INDEX_NONE; + bFoundUnrealSkeletonPath = FindAttributeOnSkeletalMeshShapeParts(SKParts, HAPI_UNREAL_ATTRIB_SKELETON, SkeletonPathGeoId, SkeletonPathPartId); + + if (bFoundUnrealSkeletonPath) + { + FString UnrealSkeletonPath; + + FHoudiniHapiAccessor Accessor(SkeletonPathGeoId, SkeletonPathPartId, HAPI_UNREAL_ATTRIB_SKELETON); + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, UnrealSkeletonPath); + if (!UnrealSkeletonPath.IsEmpty()) + { + SkeletonAsset = LoadObject(nullptr, *UnrealSkeletonPath); + // If the unreal_skeleton path was valid, UnrealSkeleton would now point to our desired skeleton asset. + if (SkeletonAsset) + { + GEditor->GetEditorSubsystem()->CloseAllEditorsForAsset(SkeletonAsset); + bUseExistingSkeleton = true; + UnrealSkeleton = FHoudiniSkeletalMeshUtils::UnrealToHoudiniSkeleton(SkeletonAsset); + } + else + { + HOUDINI_LOG_ERROR(TEXT("Could not find Skeleton asset at path '%s'. A new temp skeleton will be created."), *UnrealSkeletonPath); + return false; + } + } + } + + //---------------------------------------------------------------------------------------------------------------------------------------- + // Create the skeleton, if it exists. + //---------------------------------------------------------------------------------------------------------------------------------------- + + if (SKParts.HGPOPoseMesh == nullptr) + { + HOUDINI_LOG_ERROR(TEXT("No pose mesh found, cannot import skeletal mesh")); + return false; + } + + FHoudiniSkeleton SkeletonFromHoudini = FHoudiniSkeletalMeshUtils::FetchSkeleton(SKParts.HGPOPoseMesh->GeoId, SKParts.HGPOPoseMesh->PartId); + if (SkeletonFromHoudini.Bones.Num() == 0) + { + HOUDINI_LOG_ERROR(TEXT("No skeleton found on skeletal mesh export.")); + return false; + } + // If we don't have a skeleton asset yet, create one now. + if (!SkeletonAsset) + { + const FHoudiniGeoPartObject& PoseInstancerHGPO = *SKParts.HGPOPoseInstancer; + FHoudiniOutputObjectIdentifier SkeletonIdentifier(PoseInstancerHGPO.ObjectId, PoseInstancerHGPO.GeoId, PoseInstancerHGPO.PartId, ""); + + SkeletonAsset = CreateNewSkeleton(SkeletonIdentifier.SplitIdentifier); + if (!SkeletonAsset) + return false; + + // Create the output object + SkeletonIdentifier.PartName = PoseInstancerHGPO.PartName; + SkeletonIdentifier.PointIndex = 0; + SkeletonIdentifier.PrimitiveIndex = 0; + + FHoudiniOutputObject& SkeletonOutputObject = OutputObjects.FindOrAdd(SkeletonIdentifier); + SkeletonOutputObject.OutputObject = SkeletonAsset; + SkeletonOutputObject.bProxyIsCurrent = false; + + FHoudiniSkeletalMeshUtils::AddBonesToUnrealSkeleton(SkeletonAsset, &SkeletonFromHoudini); + } + + // At this point, if we do not have a skinned mesh, bail. We're only being asked to create a skeleton. + if (!SKParts.HasRestShape()) + return true; + + //---------------------------------------------------------------------------------------------------------------------------------------- + // Create a USkeletalMesh + //---------------------------------------------------------------------------------------------------------------------------------------- + + const FHoudiniGeoPartObject * ShapeInstanceGPO = SKParts.GetShapeInstancer(); + const FHoudiniGeoPartObject& ShapeMeshHGPO = *SKParts.HGPOShapeMesh; + + FHoudiniOutputObjectIdentifier ShapeIdentifier(ShapeInstanceGPO->ObjectId, ShapeInstanceGPO->GeoId, ShapeInstanceGPO->PartId, ""); + ShapeIdentifier.PartName = ShapeInstanceGPO->PartName; + ShapeIdentifier.PointIndex = 0; + ShapeIdentifier.PrimitiveIndex = 0; + + FHoudiniOutputObject& OutputObject = OutputObjects.FindOrAdd(ShapeIdentifier); + + // Get non-generic supported attributes from OutputObjectIdentifier + OutputObject.CachedAttributes.Empty(); + OutputObject.CachedTokens.Empty(); + FHoudiniMeshTranslator::CopyAttributesFromHGPOForSplit(ShapeMeshHGPO, ShapeIdentifier.PointIndex, ShapeIdentifier.PrimitiveIndex, OutputObject.CachedAttributes, OutputObject.CachedTokens); + + + USkeletalMesh* SkeletalMeshAsset = CreateNewSkeletalMesh(ShapeIdentifier.SplitIdentifier); + OutputObject.OutputObject = SkeletalMeshAsset; + OutputObject.bProxyIsCurrent = false; + + // This ensures that the render data gets built before we return, by calling PostEditChange when we fall out of scope. + FScopedSkeletalMeshPostEditChange ScopedPostEditChange( SkeletalMeshAsset ); + if (SkeletalMeshAsset) + { + SkeletalMeshAsset->PreEditChange( nullptr ); + } + + //---------------------------------------------------------------------------------------------------------------------------------------- + // Get the data from Houdini + //---------------------------------------------------------------------------------------------------------------------------------------- + + const bool bImportNormals = true; + HAPI_NodeId ShapeGeoId = SKParts.HGPOShapeMesh->GeoId; + HAPI_NodeId ShapePartId = SKParts.HGPOShapeMesh->PartId; + FHoudiniSkeletalMesh Mesh = GetSkeletalMeshMeshData(ShapeGeoId, ShapePartId, bImportNormals); + FHoudiniInfluences Influences = FHoudiniSkeletalMeshUtils::FetchInfluences(ShapeGeoId, ShapePartId, SkeletonFromHoudini); + if (bUseExistingSkeleton) + { + FHoudiniSkeletalMeshUtils::RemapInfluences(Influences, UnrealSkeleton); + } + + //---------------------------------------------------------------------------------------------------------------------------------------- + // Fill out the build settings, then build + //---------------------------------------------------------------------------------------------------------------------------------------- + + FHoudiniSkeletalMeshBuildSettings SkeletalMeshBuildSettings; + SkeletalMeshBuildSettings.ImportNormals = bImportNormals; + SkeletalMeshBuildSettings.SKMesh = SkeletalMeshAsset; + SkeletalMeshBuildSettings.Skeleton = SkeletonAsset; + SkeletalMeshBuildSettings.ImportScale = GetSkeletonImportScale(*SKParts.HGPOShapeMesh); + + bool bSuccess = CreateSkeletalMeshImportData(SkeletalMeshBuildSettings.SkeletalMeshImportData, Mesh, SkeletonFromHoudini, Influences, SkinnedMeshPackageParams); + if (!bSuccess) + return false; + + FHoudiniSkeletalMeshTranslator::CreateUnrealData(SkeletalMeshBuildSettings); + + //---------------------------------------------------------------------------------------------------------------------------------------- + // Physics Asset + //---------------------------------------------------------------------------------------------------------------------------------------- + + if (UnrealSkeleton.BoneMap.IsEmpty()) + { + USkeleton * Skeleton = SkeletalMeshAsset->GetSkeleton(); + UnrealSkeleton = FHoudiniSkeletalMeshUtils::UnrealToHoudiniSkeleton(Skeleton); + } + + UPhysicsAsset* PhysicsAsset = GetExistingPhysicsAssetFromParts(); + bool bCreateDefaultPhysicsAsset = GetCreateDefaultPhysicsAssetAttributeSet(); + + if (PhysicsAsset) + { + PhysicsAsset->PreviewSkeletalMesh = SkeletalMeshAsset; + SkeletalMeshAsset->SetPhysicsAsset(PhysicsAsset); + } + else if (SKParts.HGPOPhysAssetInstancer && SKParts.HGPOPhysAssetMesh) + { + const FHoudiniGeoPartObject* PhysAssetInstancerHGPO = SKParts.HGPOPhysAssetInstancer; + + FHoudiniOutputObjectIdentifier PhysAssetIdentifier = FHoudiniOutputObjectIdentifier(PhysAssetInstancerHGPO->ObjectId, PhysAssetInstancerHGPO->GeoId, PhysAssetInstancerHGPO->PartId, ""); + + PhysicsAsset = CreateNewPhysAsset(PhysAssetIdentifier.SplitIdentifier); + PhysicsAsset->PreviewSkeletalMesh = SkeletalMeshAsset; + + SetPhysicsAssetFromHGPO(PhysicsAsset, UnrealSkeleton, *SKParts.HGPOPhysAssetMesh); + + SkeletalMeshAsset->SetPhysicsAsset(PhysicsAsset); + + FHoudiniOutputObject& SkeletonOutputObject = OutputObjects.FindOrAdd(PhysAssetIdentifier); + SkeletonOutputObject.OutputObject = PhysicsAsset; + SkeletonOutputObject.bProxyIsCurrent = false; + + } + else if (bCreateDefaultPhysicsAsset) + { + FHoudiniOutputObjectIdentifier PhysAssetIdentifier; + + const FHoudiniGeoPartObject* PhysAssetInstancerHGPO = SKParts.HGPOPhysAssetInstancer; + if (!PhysAssetInstancerHGPO) + { + PhysAssetInstancerHGPO = SKParts.HGPOShapeMesh; + } + + if (PhysAssetInstancerHGPO) + PhysAssetIdentifier = FHoudiniOutputObjectIdentifier(PhysAssetInstancerHGPO->ObjectId, PhysAssetInstancerHGPO->GeoId, PhysAssetInstancerHGPO->PartId, ""); + + PhysicsAsset = CreateNewPhysAsset(PhysAssetIdentifier.SplitIdentifier); + + FHoudiniOutputObject& PhysicsAssetOutputObject = OutputObjects.FindOrAdd(PhysAssetIdentifier); + PhysicsAssetOutputObject.OutputObject = PhysicsAsset; + PhysicsAssetOutputObject.bProxyIsCurrent = false; + + // Do automatic asset generation. + FText ErrorMessage; + const FPhysAssetCreateParams& NewBodyData = GetDefault()->CreateParams; + bool bSetToMesh = true; + FPhysicsAssetUtils::CreateFromSkeletalMesh(PhysicsAsset, SkeletalMeshAsset, NewBodyData, ErrorMessage, bSetToMesh); + + + } + + return true; +} + +void +FHoudiniSkeletalMeshTranslator::SetPhysicsAssetFromHGPO(UPhysicsAsset* PhysicsAsset, const FHoudiniSkeleton& Skeleton, const FHoudiniGeoPartObject & HGPO) +{ + TArray BoneNames; + FHoudiniHapiAccessor Accessor(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_PHYSICS_BONE); + + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, BoneNames); + if (BoneNames.IsEmpty()) + return; + + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + HOUDINI_CHECK_ERROR(FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), HGPO.GeoId, HGPO.PartId, &PartInfo)); + TArray GroupNames; + if (!FHoudiniEngineUtils::HapiGetGroupNames(HGPO.GeoId, HGPO.PartId, HAPI_GROUPTYPE_POINT, PartInfo.isInstanced, GroupNames)) + { + return; + } + + TArray Points; + Accessor.Init(HGPO.GeoId, HGPO.PartId, HAPI_ATTRIB_POSITION); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, Points); + + for(FString & GroupName : GroupNames) + { + TMap> BoneSplitGroups = ExtractBoneGroup(BoneNames, HGPO, PartInfo, GroupName); + for (auto& Entry : BoneSplitGroups) + { + const FString& BoneName = Entry.Key; + const TArray& PointIndices = Entry.Value; + + if (BoneName.IsEmpty() || PointIndices.IsEmpty()) + continue; + + // Create or get BodySetup for this joint. Assign a Physical Material, if specified. + UBodySetup* BodySetup = GetBodySetup(PhysicsAsset, BoneName); + + FHoudiniHapiAccessor PhysicalMaterialAccessor(HGPO.GeoId, HGPO.PartId, HAPI_UNREAL_ATTRIB_PHYSICAL_MATERIAL); + TArray PhysicMaterials; + PhysicalMaterialAccessor.GetAttributeData(HAPI_ATTROWNER_POINT, PhysicMaterials, PointIndices[0], 1); + if (!PhysicMaterials.IsEmpty()) + { + UPhysicalMaterial* Material = Cast(StaticLoadObject(UPhysicalMaterial::StaticClass(), + nullptr, *(PhysicMaterials[0]), nullptr, LOAD_NoWarn, nullptr)); + BodySetup->PhysMaterial = Material; + } + + // Get Points in Unreal space. These will then be used to construct the appropriate simple primitive. + TArray UnrealPoints = GetPointForPhysicsBone(Skeleton, BoneName, PointIndices, Points); + + if (GroupName.StartsWith(TEXT("collision_geo_simple_box"))) + { + FHoudiniMeshTranslator::GenerateOrientedBoxAsSimpleCollision(UnrealPoints, BodySetup->AggGeom); + } + else if (GroupName.StartsWith(TEXT("collision_geo_simple_sphere"))) + { + FHoudiniMeshTranslator::GenerateSphereAsSimpleCollision(UnrealPoints, BodySetup->AggGeom); + } + else if (GroupName.StartsWith(TEXT("collision_geo_simple_capsule"))) + { + FHoudiniMeshTranslator::GenerateOrientedSphylAsSimpleCollision(UnrealPoints, BodySetup->AggGeom); + } + else if (GroupName.StartsWith(TEXT("collision_geo_simple_kdop"))) + { + TArray Directions = FHoudiniMeshTranslator::GetKdopDirections(GroupName); + FHoudiniMeshTranslator::GenerateKDopAsSimpleCollision(UnrealPoints, Directions, BodySetup->AggGeom); + } + } + } +} + +TMap> +FHoudiniSkeletalMeshTranslator::ExtractBoneGroup(const TArray& BoneNames, const FHoudiniGeoPartObject& HGPO, const HAPI_PartInfo& PartInfo, const FString& GroupName) +{ + // This will return an array of point indices for each bone where the point is in the group. + + TMap> Result; + + TArray PointGroupMembership; + bool AllEqual; + if (!FHoudiniEngineUtils::HapiGetGroupMembership(HGPO.GeoId, PartInfo, HAPI_GROUPTYPE_POINT, GroupName, PointGroupMembership, AllEqual)) + return Result; + + for (int Index = 0; Index < PointGroupMembership.Num(); Index++) + { + if (PointGroupMembership[Index] == 0) + continue; + + const FString& BoneName = BoneNames[Index]; + if (!Result.Contains(BoneName)) + { + Result.Add(BoneName, {}); + } + Result[BoneName].Add(Index); + } + + + return Result; +} + +UBodySetup* FHoudiniSkeletalMeshTranslator::GetBodySetup(UPhysicsAsset* PhysicsAsset, const FString& BoneName) +{ + // Get or create a new UBodySetup for this bone. Note: FPhysicsAssetUtils::CreateNewBody() will not create + // a new body setup if it already exists + + const FPhysAssetCreateParams& NewBodyData = GetDefault()->CreateParams; + int32 BodyId = FPhysicsAssetUtils::CreateNewBody(PhysicsAsset, FName(BoneName), NewBodyData); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + UBodySetup* BodySetup = Cast(PhysicsAsset->SkeletalBodySetups[BodyId].Get()); +#else + UBodySetup* BodySetup = PhysicsAsset->SkeletalBodySetups[BodyId]; +#endif + return BodySetup; + +} + +TArray FHoudiniSkeletalMeshTranslator::GetPointForPhysicsBone(const FHoudiniSkeleton& Skeleton, const FString& BoneName, const TArray PointIndices, const TArray& Points) +{ + auto Bone = Skeleton.BoneMap.Find(BoneName); + + auto BoneTransform = (*Bone)->UnrealGlobalTransform.Inverse(); + + // Convert Houdini Points to Unreal Points. + TArray UnrealPoints; + UnrealPoints.SetNum(PointIndices.Num()); + for (int Index = 0; Index < PointIndices.Num(); Index++) + { + int PointIndex = PointIndices[Index]; + UnrealPoints[Index].X = Points[PointIndex * 3 + 0]; + UnrealPoints[Index].Y = Points[PointIndex * 3 + 2]; + UnrealPoints[Index].Z = Points[PointIndex * 3 + 1]; + UnrealPoints[Index] *= HAPI_UNREAL_SCALE_FACTOR_POSITION; + + UnrealPoints[Index] = BoneTransform.TransformPosition(UnrealPoints[Index]); + } + + return UnrealPoints; +} + +UPhysicsAsset* +FHoudiniSkeletalMeshTranslator::CreateNewPhysAsset(const FString& InSplitIdentifier) +{ + + PhysAssetPackageParams.SplitStr = InSplitIdentifier; + if (PhysAssetPackageParams.ObjectName.IsEmpty()) + { + PhysAssetPackageParams.ObjectName = FString::Printf(TEXT("%s_%d_%d_%d_%sPhysicsAsset"), + *PhysAssetPackageParams.HoudiniAssetName, + PhysAssetPackageParams.ObjectId, PhysAssetPackageParams.GeoId, PhysAssetPackageParams.PartId, *PhysAssetPackageParams.SplitStr); + } + else + { + PhysAssetPackageParams.ObjectName += TEXT("Skeleton"); + } + + const FString AssetPath = PhysAssetPackageParams.GetPackagePath(); + const FString PackageName = PhysAssetPackageParams.GetPackageName(); + + const FString PackagePath = FPaths::Combine(AssetPath, PackageName); + const FSoftObjectPath PhysAssetPath(PackagePath); + + UPhysicsAsset* PhysAsset = LoadObject(nullptr, *PackagePath, nullptr, LOAD_NoWarn); + + if (IsValid(PhysAsset)) + { + PhysAsset->PreEditChange(nullptr); + } + else + { + PhysAsset = PhysAssetPackageParams.CreateObjectAndPackage(); + if (PhysAsset) + FAssetRegistryModule::AssetCreated(PhysAsset); + } + + return PhysAsset; +} + +USkeleton* +FHoudiniSkeletalMeshTranslator::CreateNewSkeleton(const FString& InSplitIdentifier) +{ + + SkeletonPackageParams.SplitStr = InSplitIdentifier; + if (SkeletonPackageParams.ObjectName.IsEmpty()) + SkeletonPackageParams.ObjectName = FString::Printf(TEXT("%s_%d_%d_%d_%sSkeleton"), *SkeletonPackageParams.HoudiniAssetName, SkeletonPackageParams.ObjectId, SkeletonPackageParams.GeoId, SkeletonPackageParams.PartId, *SkeletonPackageParams.SplitStr); + else + SkeletonPackageParams.ObjectName += TEXT("Skeleton"); + + const FString AssetPath = SkeletonPackageParams.GetPackagePath(); + const FString PackageName = SkeletonPackageParams.GetPackageName(); + + const FString PackagePath = FPaths::Combine(AssetPath, PackageName); + const FSoftObjectPath SkeletonAssetPath(PackagePath); + + if (USkeleton* ExistingSkeleton = LoadObject(nullptr, *PackagePath, nullptr, LOAD_NoWarn) ) + { + ExistingSkeleton->PreEditChange( nullptr ); + } + + USkeleton* NewSkeleton = SkeletonPackageParams.CreateObjectAndPackage(); + if (!IsValid(NewSkeleton)) + return nullptr; + + FAssetRegistryModule::AssetCreated(NewSkeleton); + + return NewSkeleton; +} +USkeletalMesh* +FHoudiniSkeletalMeshTranslator::CreateNewSkeletalMesh(const FString& InSplitIdentifier) +{ + USkeletalMesh* NewSkeletalMesh = SkinnedMeshPackageParams.CreateObjectAndPackage(); + if (!IsValid(NewSkeletalMesh)) + return nullptr; + + // Notify the asset registry of new asset + FAssetRegistryModule::AssetCreated(NewSkeletalMesh); + + return NewSkeletalMesh; +} + +bool +FHoudiniSkeletalMeshTranslator::IsRestShapeInstancer(HAPI_NodeId GeoId, HAPI_PartId PartId, FString& OutBaseName, HAPI_PartId & MeshPartId) +{ + // Rest Geometry packed prim name must end with '.shp' + TArray NameData; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, "name"); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, NameData); + + if (!bSuccess || NameData.Num() == 0) + { + return false; + } + if (!NameData[0].EndsWith(".shp")) + { + return false; + } + + // Extract the base name that we can use to identify this capture pose and pair it with its respective rest geometry. + FString Path, Filename, Extension; + FPaths::Split(NameData[0], Path, OutBaseName, Extension ); + + // Check for attributes inside this packed prim: + // point attributes: boneCapture + + // Assume that there is only one part per instance. This is always true for now but may need to be looked at later. + const int NumInstancedParts = 1; + TArray InstancedPartIds; + InstancedPartIds.SetNumZeroed(NumInstancedParts); + if ( FHoudiniApi::GetInstancedPartIds( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, + InstancedPartIds.GetData(), + 0, NumInstancedParts ) != HAPI_RESULT_SUCCESS ) + { + return false; + } + + MeshPartId = InstancedPartIds[0]; + + if (!IsRestShapeMesh(GeoId, MeshPartId)) + { + return false; + } + + return true; +} + +bool +FHoudiniSkeletalMeshTranslator::IsRestShapeMesh(HAPI_NodeId GeoId, HAPI_PartId PartId) +{ + if (!GetAttrInfo(GeoId, PartId, "boneCapture", HAPI_AttributeOwner::HAPI_ATTROWNER_POINT).exists) + { + return false; + } + + return true; +} + +bool +FHoudiniSkeletalMeshTranslator::IsPhysAssetMesh(HAPI_NodeId GeoId, HAPI_PartId PartId) +{ + if (!GetAttrInfo(GeoId, PartId, HAPI_UNREAL_ATTRIB_PHYSICS_BONE, HAPI_AttributeOwner::HAPI_ATTROWNER_POINT).exists) + { + return false; + } + + return true; +} + +bool +FHoudiniSkeletalMeshTranslator::IsCapturePoseInstancer(HAPI_NodeId GeoId, HAPI_PartId PartId, FString& OutBaseName, HAPI_PartId & PosePartId) +{ + auto GetAttrInfo = [](const HAPI_NodeId& GeoId, const HAPI_NodeId& PartId, const char* AttrName, HAPI_AttributeOwner AttrOwner) -> HAPI_AttributeInfo + { + HAPI_AttributeInfo AttrInfo; + FHoudiniApi::AttributeInfo_Init(&AttrInfo); + HAPI_Result AttrInfoResult = FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, + AttrName, AttrOwner, &AttrInfo); + return AttrInfo; + }; + + + // Capture Pose packed prim name must end with '.skel' + TArray NameData; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, "name"); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, NameData); + + if (!bSuccess || NameData.Num() == 0) + { + return false; + } + if (!NameData[0].EndsWith(".skel")) + { + return false; + } + + // Extract the base name that we can use to identify this capture pose and pair it with its respective rest geometry. + FString Path, Filename, Extension; + FPaths::Split(NameData[0], Path, OutBaseName, Extension ); + + // Check for attributes inside this packed prim: + // point attributes: transform, name + + // Assume that there is only one part per instance. This is always true for now but may need to be looked at later. + const int NumInstancedParts = 1; + TArray InstancedPartIds; + InstancedPartIds.SetNumZeroed(NumInstancedParts); + if ( FHoudiniApi::GetInstancedPartIds( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, + InstancedPartIds.GetData(), + 0, NumInstancedParts ) != HAPI_RESULT_SUCCESS ) + { + return false; + } + + PosePartId = InstancedPartIds[0]; + + if (!IsCapturePoseMesh(GeoId, PosePartId)) + { + return false; + } + + return true; +} + +bool +FHoudiniSkeletalMeshTranslator::IsPhysAssetInstancer(HAPI_NodeId GeoId, HAPI_PartId PartId, FString& OutBaseName, HAPI_PartId& PhysAssetPartId) +{ + + // Capture Pose packed prim name must end with '.skel' + TArray NameData; + + FHoudiniHapiAccessor Accessor(GeoId, PartId, "name"); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, NameData); + + if (!bSuccess || NameData.Num() == 0) + { + return false; + } + if (!NameData[0].EndsWith(".phys")) + { + return false; + } + + // Extract the base name that we can use to identify this capture pose and pair it with its respective rest geometry. + FString Path, Filename, Extension; + FPaths::Split(NameData[0], Path, OutBaseName, Extension); + + // Check for attributes inside this packed prim: + // point attributes: transform, name + + // Assume that there is only one part per instance. This is always true for now but may need to be looked at later. + const int NumInstancedParts = 1; + TArray InstancedPartIds; + InstancedPartIds.SetNumZeroed(NumInstancedParts); + if (FHoudiniApi::GetInstancedPartIds( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, + InstancedPartIds.GetData(), + 0, NumInstancedParts) != HAPI_RESULT_SUCCESS) + { + return false; + } + + PhysAssetPartId = InstancedPartIds[0]; + + if (!IsPhysAssetMesh(GeoId, PhysAssetPartId)) + { + return false; + } + + return true; +} + + + +bool +FHoudiniSkeletalMeshTranslator::IsCapturePoseMesh(HAPI_NodeId GeoId, HAPI_PartId PartId) +{ + if (!GetAttrInfo(GeoId, PartId, "transform", HAPI_AttributeOwner::HAPI_ATTROWNER_POINT).exists) + { + return false; + } + + if (!GetAttrInfo(GeoId, PartId, "name", HAPI_AttributeOwner::HAPI_ATTROWNER_POINT).exists) + { + return false; + } + + return true; +} + + + +HAPI_AttributeInfo +FHoudiniSkeletalMeshTranslator::GetAttrInfo(HAPI_NodeId GeoId, HAPI_NodeId PartId, + const char* AttrName, HAPI_AttributeOwner AttrOwner) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniApi::AttributeInfo_Init(&AttrInfo); + HAPI_Result AttrInfoResult = FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, + AttrName, AttrOwner, &AttrInfo); + + return AttrInfo; +} + + + +bool +FHoudiniSkeletalMeshTranslator::ProcessSkeletalMeshOutputs( + UHoudiniOutput* InOutput, + const FHoudiniPackageParams& InPackageParams, + TMap>& InAllOutputMaterials, + UObject* InOuterComponent) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniSkeletalMeshTranslator::ProcessSkeletalMeshOutputs); + + if (!IsValid(InOutput)) + return false; + + TMap NewOutputObjects; + TMap OldOutputObjects = InOutput->GetOutputObjects(); + TMap>& AssignementMaterials = InOutput->GetAssignementMaterials(); + TMap>& ReplacementMaterials = InOutput->GetReplacementMaterials(); + + bool InForceRebuild = false; + + FHoudiniSkeletalMeshParts SKParts; + + // Find all the correct parts that we need + for (const FHoudiniGeoPartObject& CurHGPO : InOutput->HoudiniGeoPartObjects) + { + if (CurHGPO.Type == EHoudiniPartType::SkeletalMeshShape) + { + if (CurHGPO.bIsInstanced) + SKParts.HGPOShapeMesh = &CurHGPO; + else + SKParts.HGPOShapeInstancer = &CurHGPO; + } + else if (CurHGPO.Type == EHoudiniPartType::SkeletalMeshPose) + { + if (CurHGPO.bIsInstanced) + SKParts.HGPOPoseMesh = &CurHGPO; + else + SKParts.HGPOPoseInstancer = &CurHGPO; + } + else if (CurHGPO.Type == EHoudiniPartType::SkeletalMeshPhysAsset) + { + if (CurHGPO.bIsInstanced) + SKParts.HGPOPhysAssetMesh = &CurHGPO; + else + SKParts.HGPOPhysAssetInstancer = &CurHGPO; + } + } + + // Iterate on all the output's HGPO, creating meshes as we go + for (const FHoudiniGeoPartObject& HGPO : InOutput->HoudiniGeoPartObjects) + { + // Not a skeletal mesh geo, skip + if (!(HGPO.Type == EHoudiniPartType::SkeletalMeshShape && HGPO.bIsInstanced == false)) + continue; + + // See if we have some uproperty attributes to update on + // the outer component (in most case, the HAC) + TArray PropertyAttributes; + if (FHoudiniEngineUtils::GetGenericPropertiesAttributes(HGPO.GeoId, HGPO.PartId, true, 0, 0, 0, PropertyAttributes)) + { + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes(InOuterComponent, PropertyAttributes); + } + } + + if (!ProcessSkeletalMeshParts( + SKParts, + InPackageParams, + InOuterComponent, + NewOutputObjects, + AssignementMaterials, + ReplacementMaterials, + InAllOutputMaterials)) + { + return false; + } + + for (auto& Material : AssignementMaterials) + { + // Adds the newly generated materials to the output materials array + // This is to avoid recreating those same materials again + if (!InAllOutputMaterials.Contains(Material.Key)) + InAllOutputMaterials.Add(Material); + } + + return FHoudiniMeshTranslator::CreateOrUpdateAllComponents( + InOutput, + InOuterComponent, + NewOutputObjects); +} + + + +bool +FHoudiniSkeletalMeshTranslator::ProcessSkeletalMeshParts( + const FHoudiniSkeletalMeshParts& SKParts, + const FHoudiniPackageParams& InPackageParams, + UObject* InOuterComponent, + TMap& OutOutputObjects, + TMap>& AssignmentMaterialMap, + TMap>& ReplacementMaterialMap, + const TMap>& InAllOutputMaterials) +{ + FHoudiniSkeletalMeshTranslator SKMeshTranslator; + SKMeshTranslator.SKParts = SKParts; + SKMeshTranslator.OutputObjects = OutOutputObjects; + + if (SKParts.HGPOShapeInstancer) + { + SKMeshTranslator.SkinnedMeshPackageParams = InPackageParams; + SKMeshTranslator.SkinnedMeshPackageParams.ObjectId = SKParts.HGPOShapeInstancer->ObjectId; + SKMeshTranslator.SkinnedMeshPackageParams.GeoId = SKParts.HGPOShapeInstancer->GeoId; + SKMeshTranslator.SkinnedMeshPackageParams.PartId = SKParts.HGPOShapeInstancer->PartId; + } + + if (SKParts.HGPOPoseInstancer) + { + SKMeshTranslator.SkeletonPackageParams = InPackageParams; + SKMeshTranslator.SkeletonPackageParams.ObjectId = SKParts.HGPOPoseInstancer->ObjectId; + SKMeshTranslator.SkeletonPackageParams.GeoId = SKParts.HGPOPoseInstancer->GeoId; + SKMeshTranslator.SkeletonPackageParams.PartId = SKParts.HGPOPoseInstancer->PartId; + } + + + if (SKParts.HGPOPhysAssetInstancer) + { + SKMeshTranslator.PhysAssetPackageParams = InPackageParams; + SKMeshTranslator.PhysAssetPackageParams.ObjectId = SKParts.HGPOPhysAssetInstancer->ObjectId; + SKMeshTranslator.PhysAssetPackageParams.GeoId = SKParts.HGPOPhysAssetInstancer->GeoId; + SKMeshTranslator.PhysAssetPackageParams.PartId = SKParts.HGPOPhysAssetInstancer->PartId; + } + else if (SKParts.HGPOShapeInstancer) + { + SKMeshTranslator.PhysAssetPackageParams = SKMeshTranslator.SkinnedMeshPackageParams; + } + + SKMeshTranslator.OuterComponent = InOuterComponent; + + SKMeshTranslator.InputAssignmentMaterials = AssignmentMaterialMap; + SKMeshTranslator.ReplacementMaterials = AssignmentMaterialMap; + SKMeshTranslator.AllOutputMaterials = InAllOutputMaterials; + if (SKMeshTranslator.ProcessSkeletalMeshParts()) + { + // Copy the output objects/materials + OutOutputObjects = SKMeshTranslator.OutputObjects; + AssignmentMaterialMap = SKMeshTranslator.OutputAssignmentMaterials; + + return true; + } + + return false; +} + + + +FHoudiniSkeletalMeshMaterialSettings +FHoudiniSkeletalMeshTranslator::GetMaterials(HAPI_NodeId GeoId, HAPI_PartId PartId, int NumFaces) +{ + // Get material information from unreal_material. + FHoudiniSkeletalMeshMaterialSettings MaterialSettings = FHoudiniSkeletalMeshUtils::GetMaterialOverrides(GeoId, PartId); + + // If no unreal material, try to use houdini materials. + if (MaterialSettings.Materials.IsEmpty()) + { + MaterialSettings = FHoudiniSkeletalMeshUtils::GetHoudiniMaterials(GeoId, PartId, NumFaces); + + if (!MaterialSettings.Materials.IsEmpty()) + { + MaterialSettings.bHoudiniMaterials = true; + } + } + + // If there are no materials, create one empty one, which will be assigned the default material. + if (MaterialSettings.Materials.IsEmpty()) + { + MaterialSettings.MaterialIds.SetNumZeroed(NumFaces); + MaterialSettings.Materials.SetNum(1); + + } + + return MaterialSettings; +} + +bool +FHoudiniSkeletalMeshTranslator::LoadOrCreateMaterials( + FHoudiniSkeletalMeshMaterialSettings MaterialSettings, + const FHoudiniPackageParams& InPackageParams, + TArray& OutPerFaceUEMaterialIds, + FSkeletalMeshImportData& OutImportData) +{ + if (MaterialSettings.bHoudiniMaterials) + { + MaterialSettings.bHoudiniMaterials = true; + bool bCreatedMaterials = FHoudiniSkeletalMeshUtils::CreateHoudiniMaterial(MaterialSettings, + InputAssignmentMaterials, OutputAssignmentMaterials, AllOutputMaterials, InPackageParams); + if (!bCreatedMaterials) + return false; + } + + for (auto& Material : MaterialSettings.Materials) + { + SkeletalMeshImportData::FMaterial SKMIDMaterial; + + FString MaterialAssetPath = Material.AssetPath; + SKMIDMaterial.Material = nullptr; + if (!MaterialAssetPath.IsEmpty()) + SKMIDMaterial.Material = Cast(StaticLoadObject(UMaterialInterface::StaticClass(), nullptr, *MaterialAssetPath, nullptr, LOAD_NoWarn, nullptr)); + + if (!IsValid(SKMIDMaterial.Material.Get())) + SKMIDMaterial.Material = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial().Get()); + + SKMIDMaterial.MaterialImportName = MaterialAssetPath; + OutImportData.Materials.Add(SKMIDMaterial); + } + OutPerFaceUEMaterialIds = MoveTemp(MaterialSettings.MaterialIds); + + return true; +} + +TOptional FHoudiniSkeletalMeshTranslator::GetCreateDefaultPhysicsAssetAttributeSet(const FHoudiniGeoPartObject * GeoPart) +{ + TOptional Result; + Result.Reset(); + + if (!GeoPart) + { + return Result; + } + + int Value = 0; + FHoudiniHapiAccessor Accessor(GeoPart->GeoId, GeoPart->PartId, HAPI_UNREAL_ATTRIB_CREATE_DEFAULT_PHYSICS_ASSET); + if (Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, Value)) + Result = Value != 0; + return Result; +} + +bool FHoudiniSkeletalMeshTranslator::GetCreateDefaultPhysicsAssetAttributeSet() +{ + TArray PartsToCheck; + PartsToCheck.Add(SKParts.HGPOPhysAssetInstancer); + PartsToCheck.Add(SKParts.HGPOShapeMesh); + PartsToCheck.Add(SKParts.HGPOPoseInstancer); + PartsToCheck.Add(SKParts.HGPOPoseMesh); + PartsToCheck.Add(SKParts.HGPOPhysAssetInstancer); + PartsToCheck.Add(SKParts.HGPOPhysAssetMesh); + + for(auto & PartToCheck : PartsToCheck) + { + TOptional bShouldCreate = GetCreateDefaultPhysicsAssetAttributeSet(PartToCheck); + if (bShouldCreate.IsSet()) + return bShouldCreate.Get(true); + } + return true; +} + +FString FHoudiniSkeletalMeshTranslator::GetPhysicAssetRef(const FHoudiniGeoPartObject* GeoPart) +{ + FString Result; + + if (!GeoPart) + return Result; + + FHoudiniHapiAccessor Accessor(GeoPart->GeoId, GeoPart->PartId, HAPI_UNREAL_ATTRIB_PHYSICS_ASSET); + Accessor.GetAttributeFirstValue(HAPI_ATTROWNER_INVALID, Result); + return Result; +} + +UPhysicsAsset * FHoudiniSkeletalMeshTranslator::GetExistingPhysicsAssetFromParts() +{ + FString RefPath; + RefPath = GetPhysicAssetRef(SKParts.HGPOPhysAssetInstancer); + if (RefPath.IsEmpty()) + RefPath = GetPhysicAssetRef(SKParts.HGPOShapeMesh); + if (RefPath.IsEmpty()) + RefPath = GetPhysicAssetRef(SKParts.HGPOPoseInstancer); + if (RefPath.IsEmpty()) + RefPath = GetPhysicAssetRef(SKParts.HGPOPoseMesh); + if (RefPath.IsEmpty()) + RefPath = GetPhysicAssetRef(SKParts.HGPOPhysAssetInstancer); + if (RefPath.IsEmpty()) + RefPath = GetPhysicAssetRef(SKParts.HGPOPhysAssetMesh); + + UPhysicsAsset * PhysicsAsset = Cast(StaticLoadObject(UPhysicsAsset::StaticClass(), nullptr, *RefPath, nullptr, LOAD_NoWarn, nullptr)); + + return PhysicsAsset; +} + + + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngine/Private/HoudiniSkeletalMeshTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" + +#include "HoudiniGeoPartObject.h" +#include "HoudiniOutput.h" +#include "HoudiniPackageParams.h" +#include "CoreMinimal.h" +#include "HoudiniSkeletalMeshUtils.h" +#include "ImportUtils/SkeletalMeshImportUtils.h" +#include "PhysicsEngine/BoxElem.h" +#include "Rendering/SkeletalMeshLODImporterData.h" + +class USkeletalMesh; +class USkeleton; + +struct FHoudiniSkeletalMesh +{ + TArray Positions; + TArray> UVSets; + TArray Tangents; + TArray Normals; + TArray Colors; + TArray Vertices; + TArray AttribInfoUVSets; + HAPI_AttributeInfo ColorInfo; + FHoudiniSkeletalMeshMaterialSettings Materials; + +}; + +struct FHoudiniSkeletalMeshParts +{ + const FHoudiniGeoPartObject* HGPOShapeInstancer = nullptr; + const FHoudiniGeoPartObject* HGPOShapeMesh = nullptr; + const FHoudiniGeoPartObject* HGPOPoseInstancer = nullptr; + const FHoudiniGeoPartObject* HGPOPoseMesh = nullptr; + const FHoudiniGeoPartObject* HGPOPhysAssetInstancer = nullptr; + const FHoudiniGeoPartObject* HGPOPhysAssetMesh = nullptr; + + const FHoudiniGeoPartObject* GetShapeInstancer() const { return HGPOShapeInstancer; } + bool HasRestShape() const { return HGPOShapeInstancer && HGPOShapeMesh; } + bool HasSkeleton() const { return HGPOPoseInstancer && HGPOPoseMesh; } + +}; + + +struct FHoudiniSkeletalMeshBuildSettings +{ + FSkeletalMeshImportData SkeletalMeshImportData; + float ImportScale = 1.0f; + USkeletalMesh* SKMesh = nullptr; + UPackage* SKPackage = nullptr; + USkeleton* Skeleton = nullptr; + FString CurrentObjectName; + bool ImportNormals = false; +}; + + +struct HOUDINIENGINE_API FHoudiniSkeletalMeshTranslator +{ +public: + + // Check whether the packed primitive is skeleton Rest Geometry + static bool IsRestShapeInstancer(HAPI_NodeId GeoId, HAPI_PartId PartId, FString& OutBaseName, HAPI_NodeId & MeshPartId); + static bool IsRestShapeMesh(HAPI_NodeId GeoId, HAPI_PartId PartId); + + // Check whether the packed primitive is skeleton Rest Geometry + static bool IsCapturePoseInstancer(HAPI_NodeId GeoId, HAPI_PartId PartId, FString& OutBaseName, HAPI_PartId &PoseCurveId); + static bool IsCapturePoseMesh(HAPI_NodeId GeoId, const HAPI_NodeId PartId); + + // Check whether the packed primitive is a Phys Asset + static bool IsPhysAssetInstancer(HAPI_NodeId GeoId, HAPI_PartId PartId, FString& OutBaseName, HAPI_PartId& PoseCurveId); + static bool IsPhysAssetMesh(HAPI_NodeId GeoId, const HAPI_NodeId PartId); + + // Creates all skeletal mesh assets and component for a given HoudiniOutput + static bool ProcessSkeletalMeshOutputs( + UHoudiniOutput* InOutput, + const FHoudiniPackageParams& InPackageParams, + TMap>& InAllOutputMaterials, + UObject* InOuterComponent); + +protected: + // Creates a skeletal mesh assets and component for a given HoudiniOutput + static bool ProcessSkeletalMeshParts( + const FHoudiniSkeletalMeshParts& SKParts, + const FHoudiniPackageParams& InPackageParams, + UObject* InOuterComponent, + TMap& OutOutputObjects, + TMap>& AssignmentMaterialMap, + TMap>& ReplacementMaterialMap, + const TMap>& InAllOutputMaterials); + + // Creates SkeletalMesh and Skeleton Assets and Packages, and adds them to OutputObjects + bool ProcessSkeletalMeshParts(); + + // Builds Skeletal Mesh and Skeleton Assets from FSkeletalMeshImportData + static void CreateUnrealData(FHoudiniSkeletalMeshBuildSettings& BuildSettings); + + // Creates skeletal mesh import data from intermediate structures + bool CreateSkeletalMeshImportData( + FSkeletalMeshImportData & SkeletalMeshImportData, + const FHoudiniSkeletalMesh & Mesh, + const FHoudiniSkeleton & Skeleton, + const FHoudiniInfluences & SkinWeights, + const FHoudiniPackageParams& InPackageParams); + + bool SetSkeletalMeshImportDataMesh( + FSkeletalMeshImportData& SkeletalMeshImportData, + const FHoudiniSkeletalMesh& Mesh, + const FHoudiniPackageParams& PackageParams); + + bool SetSkeletalMeshImportDataInfluences( + FSkeletalMeshImportData& SkeletalMeshImportData, + const FHoudiniInfluences& SkinWeights, + const FHoudiniPackageParams& PackageParams); + + static bool SetSkeletalMeshImportDataSkeleton( + FSkeletalMeshImportData& SkeletalMeshImportData, + const FHoudiniSkeleton& Skeleton, + const FHoudiniPackageParams& InPackageParams); + + FHoudiniSkeletalMesh GetSkeletalMeshMeshData(HAPI_NodeId ShapeGeoId , HAPI_PartId PartId, bool bImportNormals); + + static float GetSkeletonImportScale(const FHoudiniGeoPartObject& ShapeMeshHGPO); + + static bool FindAttributeOnSkeletalMeshShapeParts(const FHoudiniSkeletalMeshParts& InSKParts, const char* Attribute, HAPI_NodeId& OutGeoId, HAPI_PartId& OutPartId); + + FHoudiniSkeletalMeshMaterialSettings GetMaterials(HAPI_NodeId ShapeGeoId, HAPI_PartId PartId, int NumFaces); + + bool LoadOrCreateMaterials( + FHoudiniSkeletalMeshMaterialSettings MaterialSettings, + const FHoudiniPackageParams& InPackageParams, + TArray& OutPerFaceUEMaterialIds, + FSkeletalMeshImportData& OutImportData); + + USkeletalMesh* CreateNewSkeletalMesh(const FString& InSplitIdentifier); + USkeleton* CreateNewSkeleton(const FString& InSplitIdentifier); + UPhysicsAsset * CreateNewPhysAsset(const FString& InSplitIdentifier); + + // The HoudiniGeoPartObjects we're working on + FHoudiniSkeletalMeshParts SKParts; + // Structure that handles cooking/baking package creation parameters + FHoudiniPackageParams SkinnedMeshPackageParams; + FHoudiniPackageParams SkeletonPackageParams; + FHoudiniPackageParams PhysAssetPackageParams; + + // New Output objects + TMap OutputObjects; + // Input Material Map + TMap> InputAssignmentMaterials; + // Output Material Map + TMap> OutputAssignmentMaterials; + // Input Replacement Materials maps + TMap> ReplacementMaterials; + // All the materials that have been generated by this Houdini Asset + // Used to avoid generating the same houdini material over and over again + TMap> AllOutputMaterials; + // Outer object for attaching components to + UObject* OuterComponent = nullptr; + + // Helper to IsRestGeometry* / IsCapturePose* functions + static HAPI_AttributeInfo GetAttrInfo(HAPI_NodeId GeoId, HAPI_NodeId PartId, const char* AttrName, HAPI_AttributeOwner AttrOwner); + + // Functions to determine if we should create a default physics asset attributes. + bool GetCreateDefaultPhysicsAssetAttributeSet(); + TOptional GetCreateDefaultPhysicsAssetAttributeSet(const FHoudiniGeoPartObject* GeoPart); + + // Sets the Physics Assets collision from the HGPO. + void SetPhysicsAssetFromHGPO(UPhysicsAsset* PhysicsAsset, const FHoudiniSkeleton& Skeleton, const FHoudiniGeoPartObject& HGPO); + + // Gets all points for the given bone name. + static TArray GetPointForPhysicsBone(const FHoudiniSkeleton& Skeleton, const FString& BoneName, const TArray PointIndices, const TArray& Points); + + // Getall point indices for the given collision group. Grouped by Bone + static TMap> ExtractBoneGroup(const TArray & BoneNames, const FHoudiniGeoPartObject& HGPO, const HAPI_PartInfo & PartInfo, const FString & GroupName); + + // Gets (or creates) the UBodySetup for the named bone. + static UBodySetup* GetBodySetup(UPhysicsAsset* PhysicsAsset, const FString& BoneName); + + // Functions for getting an existing Physics Asset, if specified + UPhysicsAsset * GetExistingPhysicsAssetFromParts(); + static FString GetPhysicAssetRef(const FHoudiniGeoPartObject* GeoPart); + +}; +``` + +`Source/HoudiniEngine/Private/HoudiniSkeletalMeshUtils.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniSkeletalMeshUtils.h" + +#include "HoudiniSkeletalMeshTranslator.h" + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniMeshTranslator.h" + +#include "Animation/Skeleton.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Components/SkeletalMeshComponent.h" +#include "Engine/SkeletalMesh.h" +#include "Factories/FbxSkeletalMeshImportData.h" +#include "IMeshBuilderModule.h" +#include "ImportUtils/SkeletalMeshImportUtils.h" +#include "Materials/Material.h" +#include "Materials/MaterialInterface.h" +#include "Math/UnrealMathUtility.h" +#include "Rendering/SkeletalMeshLODImporterData.h" +#include "Rendering/SkeletalMeshModel.h" +#include "ReferenceSkeleton.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 +#include "Engine/SkinnedAssetCommon.h" +#endif +#include + +#include "Chaos/ChaosPerfTest.h" +#include "Internationalization/Regex.h" + +FMatrix +FHoudiniSkeletalMeshUtils::MakeMatrixFromHoudiniData(const float RotationData[], const float PositionData[]) +{ + // Convert Houdini matrix to data to Unreal data. Y/Z axis are swapped so we each column 1 and 2 are switched. + // In addition the Y/Z vectors need to be switched. + + FMatrix M44Pose; + M44Pose.M[0][0] = RotationData[0]; + M44Pose.M[0][1] = RotationData[2]; + M44Pose.M[0][2] = RotationData[1]; + M44Pose.M[0][3] = 0; + M44Pose.M[1][0] = RotationData[6]; + M44Pose.M[1][1] = RotationData[8]; + M44Pose.M[1][2] = RotationData[7]; + M44Pose.M[1][3] = 0; + M44Pose.M[2][0] = RotationData[3]; + M44Pose.M[2][1] = RotationData[5]; + M44Pose.M[2][2] = RotationData[4]; + M44Pose.M[2][3] = 0; + M44Pose.M[3][0] = PositionData[0] * 100.0; + M44Pose.M[3][1] = PositionData[2] * 100.0; + M44Pose.M[3][2] = PositionData[1] * 100.0; + M44Pose.M[3][3] = 1; + + return M44Pose; +} + +FMatrix FHoudiniSkeletalMeshUtils::UnrealToHoudiniMatrix(const FTransform& Transform) +{ + FMatrix UnrealMatrix = Transform.ToMatrixWithScale(); + return UnrealToHoudiniMatrix(UnrealMatrix); +} + +void FHoudiniSkeletalMeshUtils::UnrealToHoudiniMatrix(FMatrix& UnrealMatrix, float Matrix[]) +{ + FMatrix HoudiniMatrix = UnrealToHoudiniMatrix(UnrealMatrix); + + for (int Row = 0; Row < 4; Row++) + { + for (int Col = 0; Col < 4; Col++) + { + Matrix[Row * 4 + Col] = HoudiniMatrix.M[Row][Col]; + } + } +} + + +FMatrix FHoudiniSkeletalMeshUtils::UnrealToHoudiniMatrix(const FMatrix& UnrealMatrix) +{ + FMatrix Result; + Result.M[0][0] = UnrealMatrix.M[0][0]; + Result.M[0][1] = UnrealMatrix.M[0][2]; + Result.M[0][2] = UnrealMatrix.M[0][1]; + Result.M[0][3] = UnrealMatrix.M[0][3]; + + Result.M[1][0] = UnrealMatrix.M[2][0]; + Result.M[1][1] = UnrealMatrix.M[2][2]; + Result.M[1][2] = UnrealMatrix.M[2][1]; + Result.M[1][3] = UnrealMatrix.M[2][3]; + + Result.M[2][0] = UnrealMatrix.M[1][0]; + Result.M[2][1] = UnrealMatrix.M[1][2]; + Result.M[2][2] = UnrealMatrix.M[1][1]; + Result.M[2][3] = UnrealMatrix.M[1][3]; + + Result.M[3][0] = UnrealMatrix.M[3][0] * 0.01; + Result.M[3][1] = UnrealMatrix.M[3][2] * 0.01; + Result.M[3][2] = UnrealMatrix.M[3][1] * 0.01; + Result.M[3][3] = UnrealMatrix.M[3][3]; + + return Result; +} + +void FHoudiniSkeletalMeshUtils::UnrealToHoudiniMatrix(FMatrix& UnrealMatrix, float Rotation[], float Position[]) +{ + FMatrix HoudiniMatrix = UnrealToHoudiniMatrix(UnrealMatrix); + + for (int Col = 0; Col < 3; Col++) + { + for (int Row = 0; Row < 3; Row++) + { + Rotation[Row * 3 + Col] = HoudiniMatrix.M[Row][Col]; + } + Position[Col] = HoudiniMatrix.M[3][Col]; + } +} + + +FHoudiniSkeleton FHoudiniSkeletalMeshUtils::FetchSkeleton(HAPI_NodeId NodeId, HAPI_PartId PartId) +{ + FHoudiniSkeleton Result; + + // Houdini stores the bone names in the "name" attribute on the point. However, when this data is fetched HAPI + // doesn't return just the name, it expands the points so there is one per vertex. So for now, we have to assign + // an extra attribute to determine which preserves the bone number. + + TArray ParentChild; + FHoudiniHapiAccessor Accessor; + Accessor.Init(NodeId, PartId, "name"); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, ParentChild); + + if (ParentChild.IsEmpty()) + { + HOUDINI_LOG_ERROR(TEXT("No name found on skeleton")); + return {}; + } + + TArray ParentChildBoneNumbers; + Accessor.Init(NodeId, PartId, "__bone_id"); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, ParentChildBoneNumbers); + + if (ParentChildBoneNumbers.Num() != ParentChild.Num()) + { + HOUDINI_LOG_ERROR(TEXT("No __bone_id found on skeleton")); + return {}; + } + + //-------------------------------------------------------------------------------------------------------------------- + // Get all bone names and uses this to seed the skeleton + //-------------------------------------------------------------------------------------------------------------------- + + TArray BoneNames; + TMap BoneNamesToPointIndex; + for(int Index = 0; Index < ParentChild.Num(); Index++) + { + BoneNamesToPointIndex.Add(ParentChild[Index], Index); + } + + BoneNamesToPointIndex.GetKeys(BoneNames); + + if (BoneNames.IsEmpty()) + { + HOUDINI_LOG_ERROR(TEXT("No Bone names found on skeleton")); + return {}; + } + + // Fill in bone names. + TArray Bones; + Bones.SetNum(BoneNames.Num()); + TMap BoneMap; + + for(int Index = 0; Index < BoneNames.Num(); Index++) + { + FHoudiniSkeletonBone& Bone = Bones[Index]; + Bone.Name = BoneNames[Index]; + BoneMap.Add(Bone.Name, &Bone); + } + + for (int Index = 0; Index < ParentChild.Num(); Index++) + { + FHoudiniSkeletonBone* Node = BoneMap[ParentChild[Index]]; + if (!Result.HoudiniBoneMap.Contains(ParentChildBoneNumbers[Index])) + { + Node->HoudiniBoneNumber = ParentChildBoneNumbers[Index]; + Result.HoudiniBoneMap.Add(Node->HoudiniBoneNumber, Node->Name); + } + } + + //-------------------------------------------------------------------------------------------------------------------- + // Read matrices + //-------------------------------------------------------------------------------------------------------------------- + + TArray RotationData; // 9 floats per bone + TArray PositionData; // 3 floats per bone + + Accessor.Init(NodeId, PartId, "transform"); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, RotationData); + Accessor.Init(NodeId, PartId, "P"); + Accessor.GetAttributeData(HAPI_ATTROWNER_POINT, PositionData); + + for(int Index = 0; Index < Bones.Num(); Index++) + { + FHoudiniSkeletonBone* Bone = &Bones[Index]; + int PointIndex = BoneNamesToPointIndex[Bone->Name]; + + Bone->UnrealGlobalTransform = FTransform(MakeMatrixFromHoudiniData(&RotationData[PointIndex * 9], &PositionData[PointIndex * 3])); + } + + //-------------------------------------------------------------------------------------------------------------------- + // Fill in parent - child relationships + //-------------------------------------------------------------------------------------------------------------------- + + for(int Index = 0; Index < ParentChild.Num(); Index+= 2) + { + const FString & ParentName = ParentChild[Index]; + const FString& ChildName = ParentChild[Index + 1]; + + FHoudiniSkeletonBone* Parent = BoneMap[ParentName]; + FHoudiniSkeletonBone* Child = BoneMap[ChildName]; + + if (!Parent || !Child) + { + HOUDINI_LOG_ERROR(TEXT("Missing bone names: %s or %s "), *ParentName, *ChildName); + return {}; + } + Child->Parent = Parent; + Parent->Children.Add(Child); + } + + // Root is first bone with no parent + Result.Root = nullptr; + int RootIndex = INDEX_NONE; + + for (int BoneIndex = 0;BoneIndex < Bones.Num(); BoneIndex++) + { + if(!Bones[BoneIndex].Parent) + { + RootIndex = BoneIndex; + } + } + + if (RootIndex == INDEX_NONE) + { + HOUDINI_LOG_ERROR(TEXT("No root found on skeleton")); + return {}; + } + + Result.Bones = CreateSortedBoneList(Bones, RootIndex); + + Result.Root = &Result.Bones[0]; + + for(int Index = 0; Index < Result.Bones.Num(); Index++) + { + FHoudiniSkeletonBone* Bone = &Result.Bones[Index]; + Result.BoneMap.Add(Bone->Name, Bone); + } + + //-------------------------------------------------------------------------------------------------------------------- + // Convert Houdini matrices to Unreal matrix, and calculate the local matrices(which is what Unreal wants). + //-------------------------------------------------------------------------------------------------------------------- + + ConstructLocalMatricesFromGlobal(Result.Root, nullptr); + + return Result; +} + +TArray +FHoudiniSkeletalMeshUtils::CreateSortedBoneList(TArray& UnsortedBones, int RootIndex) +{ + TArray SortedBones; + SortedBones.SetNum(UnsortedBones.Num()); + int NextFreeSlot = 0; + TMap Remap; + Remap.Add(nullptr, nullptr); + + std::function AddChildren = [&](FHoudiniSkeletonBone* Parent) + { + Remap.Add(Parent, &SortedBones[NextFreeSlot]); + SortedBones[NextFreeSlot++] = *Parent; + for (auto& Child : Parent->Children) + { + AddChildren(Child); + } + }; + + AddChildren(&UnsortedBones[RootIndex]); + + for (int Index = 0; Index < SortedBones.Num(); Index++) + { + SortedBones[Index].UnrealBoneNumber = Index; + SortedBones[Index].Parent = Remap[SortedBones[Index].Parent]; + for (int ChildIndex = 0; ChildIndex < SortedBones[Index].Children.Num(); ChildIndex++) + { + SortedBones[Index].Children[ChildIndex] = Remap[SortedBones[Index].Children[ChildIndex]]; + } + } + + return SortedBones; +} + +FHoudiniInfluences FHoudiniSkeletalMeshUtils::FetchInfluences(HAPI_NodeId NodeId, HAPI_PartId PartId, FHoudiniSkeleton& Skeleton) +{ + HAPI_AttributeInfo BoneCaptureInfo; + TArray BoneCaptureData; + FHoudiniHapiAccessor Accessor; + Accessor.Init(NodeId, PartId, "boneCapture"); + Accessor.GetInfo(BoneCaptureInfo, HAPI_AttributeOwner::HAPI_ATTROWNER_POINT); + Accessor.GetAttributeData(HAPI_AttributeOwner::HAPI_ATTROWNER_POINT, BoneCaptureData); + + TArray BoneNames; + TArray BoneNameIndices; + Accessor.Init(NodeId, PartId, "boneCapture_pCaptPath"); + Accessor.GetAttributeArrayData(HAPI_AttributeOwner::HAPI_ATTROWNER_DETAIL, BoneNames, BoneNameIndices); + + int HoudiniInfluencesPerVertex = BoneCaptureInfo.tupleSize / 2; // Divide by two compensate for the 'two floats' per capture data entry. + + const int MaxInfluences = 4; // TODO: Support more than 4. + + FHoudiniInfluences SkinWeights; + SkinWeights.NumInfluences = FMath::Min(MaxInfluences, HoudiniInfluencesPerVertex); + SkinWeights.NumVertices = BoneCaptureData.Num() / (2 * HoudiniInfluencesPerVertex); + SkinWeights.Influences.SetNum(SkinWeights.NumInfluences * SkinWeights.NumVertices); + + TArray< FHoudiniSkinInfluence> InputInfluences; + InputInfluences.SetNum(HoudiniInfluencesPerVertex); + + for(int VertexIndex = 0; VertexIndex < SkinWeights.NumVertices; VertexIndex++) + { + // Read input influences + for(int InputInfluence = 0; InputInfluence < HoudiniInfluencesPerVertex; InputInfluence++) + { + int Index = VertexIndex * HoudiniInfluencesPerVertex + InputInfluence; + int BoneIndex = static_cast(BoneCaptureData[Index * 2]); + float BoneWeight = BoneCaptureData[Index * 2 + 1]; + + if (BoneWeight > 0.0f && BoneIndex != -1) + { + if(BoneIndex >= BoneNames.Num()) + { + HOUDINI_LOG_ERROR(TEXT("Invalid bone index in bone capture.")); + return {}; + } + + const FString& BoneName = BoneNames[BoneIndex]; + InputInfluences[InputInfluence].Bone = Skeleton.BoneMap[BoneName]; + InputInfluences[InputInfluence].Weight = BoneWeight; + } + else + { + InputInfluences[InputInfluence].Bone = nullptr; + InputInfluences[InputInfluence].Weight = 0.0f; + } + } + + // Sort the bone influences by weight (higher weight first) so we can get the most influencial. + InputInfluences.Sort([](const FHoudiniSkinInfluence & Bone1, const FHoudiniSkinInfluence& Bone2) { return Bone1.Weight > Bone2.Weight; }); + float TotalWeight = 0.0f; + for(int AddIndex = 0; AddIndex < SkinWeights.NumInfluences; AddIndex++) + TotalWeight += InputInfluences[AddIndex].Weight; + + float WeightScale = 1.0f / TotalWeight; + for (int AddIndex = 0; AddIndex < SkinWeights.NumInfluences; AddIndex++) + InputInfluences[AddIndex].Weight *= WeightScale; + + // Add input influence to result + for (int AddIndex = 0; AddIndex < SkinWeights.NumInfluences; AddIndex++) + SkinWeights.Influences[VertexIndex * SkinWeights.NumInfluences + AddIndex] = InputInfluences[AddIndex]; + + // If there are less influences in Houdini than required for Unreal, add dummy wights + FHoudiniSkinInfluence EmptyInfluence; + for(int Index = InputInfluences.Num(); Index < SkinWeights.NumInfluences; Index++) + { + SkinWeights.Influences[VertexIndex * SkinWeights.NumInfluences + Index] = EmptyInfluence; + } + } + return SkinWeights; +} + + +FHoudiniSkeletalMeshMaterialSettings FHoudiniSkeletalMeshUtils::GetMaterialOverrides(HAPI_NodeId NodeId, HAPI_PartId PartId) +{ + // Fetch attributes and check its valid + TArray AttributeData; + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_MATERIAL); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, AttributeData); + if (!bSuccess || AttributeData.IsEmpty()) + return {}; + + // Find all unique material overrides and create a FHoudiniSkeletalMeshMaterial per material. + TMap UniqueMaterials; + int NextFreeSlot = 0; + for(FString & Attribute : AttributeData) + { + if (!UniqueMaterials.Contains(Attribute)) + { + FHoudiniSkeletalMeshMaterial Material; + Material.OverridePath = Attribute; + Material.AssetPath = Attribute; + Material.Slot = NextFreeSlot++; + + // see if there is a slot override. The name will begin the [0], [1], etc. + if (Material.OverridePath.StartsWith("[")) + { + FRegexPattern NumberPattern(TEXT("\\[(\\d+)\\]")); + FRegexMatcher Matcher(NumberPattern, Material.OverridePath); + if (Matcher.FindNext()) + { + int32 BeginIndex = Matcher.GetCaptureGroupBeginning(1); + int32 EndIndex = Matcher.GetCaptureGroupEnding(1); + FString MatchedNumber = Material.OverridePath.Mid(BeginIndex, EndIndex - BeginIndex); + if (MatchedNumber.IsNumeric()) + { + Material.Slot = FCString::Atoi(*MatchedNumber); + Material.AssetPath = Material.OverridePath.Mid(EndIndex + 1); + Attribute = Material.AssetPath; + } + } + } + + UniqueMaterials.Add(Attribute, Material); + } + } + + // Store all unique materials in the results, sorting by slot. + FHoudiniSkeletalMeshMaterialSettings Result; + for(auto & It : UniqueMaterials) + { + Result.Materials.Add(It.Value); + } + Result.Materials.Sort([](const FHoudiniSkeletalMeshMaterial & A, const FHoudiniSkeletalMeshMaterial & B) { return A.Slot < B.Slot; } ); + + // Create per-face indexes into the material array + TMap AttributeToMaterialIndex; + for(int Index = 0; Index < Result.Materials.Num(); Index++) + { + AttributeToMaterialIndex.Add(Result.Materials[Index].AssetPath, Index); + } + + Result.MaterialIds.SetNum(AttributeData.Num()); + for (int Index = 0; Index < AttributeData.Num(); Index++) + { + Result.MaterialIds[Index] = AttributeToMaterialIndex[AttributeData[Index]]; + } + return Result; +} + +FHoudiniSkeletalMeshMaterialSettings FHoudiniSkeletalMeshUtils::GetHoudiniMaterials(HAPI_NodeId NodeId, HAPI_PartId PartId, int NumFaces) +{ + TArray MaterialNodes; + MaterialNodes.SetNum(NumFaces); + + bool bSingleMaterial = false; + + HAPI_Result HapiResult = FHoudiniApi::GetMaterialNodeIdsOnFaces( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, &bSingleMaterial, MaterialNodes.GetData(), 0, NumFaces); + + if (HapiResult != HAPI_RESULT_SUCCESS) + return {}; + + FHoudiniSkeletalMeshMaterialSettings Result; + + TMap UniqueMaterials; + + if (bSingleMaterial) + { + FHoudiniSkeletalMeshMaterial Material; + Material.NodeId = MaterialNodes[0]; + Material.Slot = 0; + UniqueMaterials.Add(MaterialNodes[0], 0); + Result.Materials.Add(Material); + } + else + { + for(int Index = 0; Index < MaterialNodes.Num(); Index++) + { + if (!UniqueMaterials.Contains(NodeId)) + { + FHoudiniSkeletalMeshMaterial Material; + Material.NodeId = MaterialNodes[Index]; + Material.Slot = Result.Materials.Num(); + UniqueMaterials.Add(NodeId, Index); + Result.Materials.Add(Material); + } + } + } + + Result.MaterialIds.SetNum(NumFaces); + for(int FaceId = 0; FaceId < NumFaces; FaceId++) + { + Result.MaterialIds[FaceId] = UniqueMaterials[MaterialNodes[FaceId]]; + } + + Result.GeoNodeId = NodeId; + return Result; + +} + +bool FHoudiniSkeletalMeshUtils::CreateHoudiniMaterial( + FHoudiniSkeletalMeshMaterialSettings& SkeletalFaceMaterials, + TMap> InputAssignmentMaterials, + TMap> AllOutputMaterials, + TMap> OutputAssignmentMaterials, + const FHoudiniPackageParams& InPackageParams) +{ + TArray UniqueHoudiniMaterialIds; + + TArray UniqueHoudiniMaterialInfos; + UniqueHoudiniMaterialInfos.SetNum(SkeletalFaceMaterials.Materials.Num()); + UniqueHoudiniMaterialIds.SetNum(SkeletalFaceMaterials.Materials.Num()); + + // Fetch all material infos. + for(int Index = 0; Index < SkeletalFaceMaterials.Materials.Num(); Index++) + { + UniqueHoudiniMaterialIds[Index] = SkeletalFaceMaterials.Materials[Index].NodeId; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetMaterialInfo(FHoudiniEngine::Get().GetSession(), + SkeletalFaceMaterials.Materials[Index].NodeId, &UniqueHoudiniMaterialInfos[Index]), false); + } + + // Create materials. + + TArray MaterialAndTexturePackages; + FHoudiniPackageParams PackageParams = InPackageParams; + TArray OutMaterialArray; + + if (!FHoudiniMaterialTranslator::CreateHoudiniMaterials( + SkeletalFaceMaterials.GeoNodeId, + InPackageParams, + UniqueHoudiniMaterialIds, + UniqueHoudiniMaterialInfos, + InputAssignmentMaterials, + AllOutputMaterials, + OutputAssignmentMaterials, + OutMaterialArray, + MaterialAndTexturePackages, + false, + true, + false)) + { + return false; + } + + // Set output materials. + + if (OutMaterialArray.Num() != SkeletalFaceMaterials.Materials.Num()) + return false; + + for(int Index = 0; Index < SkeletalFaceMaterials.Materials.Num(); Index++) + { + SkeletalFaceMaterials.Materials[Index].AssetPath = OutMaterialArray[Index]->GetPathName(); + } + return true; +} + + +void FHoudiniSkeletalMeshUtils::AddBonesToUnrealSkeleton(FReferenceSkeletonModifier & RefSkeletonModifier, const FHoudiniSkeletonBone * Bone) +{ + + int ParentId = INDEX_NONE; + if (Bone->Parent) + ParentId = RefSkeletonModifier.FindBoneIndex(FName(Bone->Parent->Name)); + + FMeshBoneInfo RootBoneInfo(FName(Bone->Name), Bone->Name, ParentId); + RefSkeletonModifier.Add(RootBoneInfo, Bone->UnrealLocalMatrix); + + for(auto & Child : Bone->Children) + AddBonesToUnrealSkeleton(RefSkeletonModifier, Child); +} + + +bool FHoudiniSkeletalMeshUtils::AddBonesToUnrealSkeleton(USkeleton* UnrealSkeleton, const FHoudiniSkeleton* HoudiniSkeleton) +{ + FReferenceSkeletonModifier RefSkeletonModifier(UnrealSkeleton); + + AddBonesToUnrealSkeleton(RefSkeletonModifier, HoudiniSkeleton->Root); + return true; +} + +FHoudiniSkeleton FHoudiniSkeletalMeshUtils::UnrealToHoudiniSkeleton(USkeleton * UnrealSkeleton) +{ + FHoudiniSkeleton HoudiniSkeleton; + + const FReferenceSkeleton& RefSkeleton = UnrealSkeleton->GetReferenceSkeleton(); + + TArray RefPose = RefSkeleton.GetRefBonePose(); + + int32 BoneCount = RefSkeleton.GetNum(); + + HoudiniSkeleton.Bones.SetNum(BoneCount); + + for (int32 BoneIndex = 0; BoneIndex < BoneCount; ++BoneIndex) + { + FString BoneName = RefSkeleton.GetBoneName(BoneIndex).ToString(); + int32 ParentIndex = RefSkeleton.GetParentIndex(BoneIndex); + FTransform & BoneTransform = RefPose[BoneIndex]; + + HoudiniSkeleton.BoneMap.Add(BoneName, &HoudiniSkeleton.Bones[BoneIndex]); + auto * ThisBone = &HoudiniSkeleton.Bones[BoneIndex]; + ThisBone->Name = BoneName; + ThisBone->UnrealGlobalTransform = BoneTransform; + ThisBone->UnrealLocalMatrix = BoneTransform; + ThisBone->UnrealBoneNumber = BoneIndex; + if (ParentIndex != INDEX_NONE) + { + auto* ParentBone = &HoudiniSkeleton.Bones[ParentIndex]; + ThisBone->Parent = &HoudiniSkeleton.Bones[ParentIndex]; + ParentBone->Children.Add(ThisBone); + } + else + { + ThisBone->Parent = nullptr; + HoudiniSkeleton.Root = ThisBone; + } + } + + return HoudiniSkeleton; +} + +void FHoudiniSkeletalMeshUtils::ConstructLocalMatricesFromGlobal(FHoudiniSkeletonBone* Node, const FHoudiniSkeletonBone* Parent) +{ + FTransform ParentUnrealMatrix = FTransform::Identity; + if (Parent != nullptr) + { + ParentUnrealMatrix = Parent->UnrealGlobalTransform; + } + Node->UnrealLocalMatrix = Node->UnrealGlobalTransform * ParentUnrealMatrix.Inverse(); + for (auto Child : Node->Children) + { + ConstructLocalMatricesFromGlobal(Child, Node); + } +} + +bool FHoudiniSkeletalMeshUtils::RemapInfluences(FHoudiniInfluences& Influences, const FHoudiniSkeleton& NewSkeleton) +{ + bool bErrors = false; + + TSet MissingBones; + + for(FHoudiniSkinInfluence& Influence : Influences.Influences) + { + if (Influence.Bone == nullptr) + continue; + + const FString & BoneName = Influence.Bone->Name; + auto Bone = NewSkeleton.BoneMap.Find(BoneName); + if (Bone) + { + Influence.Bone = *Bone; + } + else + { + // we can't find the bone, try using an ancestor. + FHoudiniSkeletonBone* Ancestor = NewSkeleton.Root; + FHoudiniSkeletonBone* Search = Influence.Bone->Parent; + while(Search) + { + auto Candidate = NewSkeleton.BoneMap.Find(Search->Name); + if (Candidate != nullptr) + { + Ancestor = *Candidate; + break; + } + else + { + Search = Search->Parent; + } + + } + + if (!MissingBones.Contains(*BoneName)) + { + MissingBones.Add(*BoneName); + HOUDINI_LOG_WARNING(TEXT("Could not find bone in unreal skeleton %s. Using %s."), *BoneName, *Ancestor->Name); + } + bErrors = true; + } + } + return bErrors; +} + +``` + +`Source/HoudiniEngine/Private/HoudiniSkeletalMeshUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" + +#include "HoudiniGeoPartObject.h" +#include "HoudiniOutput.h" +#include "HoudiniPackageParams.h" +#include "CoreMinimal.h" +#include "Math/MathFwd.h" + + +struct FReferenceSkeletonModifier; + +struct FHoudiniSkeletonBone +{ + FString Name; + FTransform UnrealGlobalTransform; + FTransform UnrealLocalMatrix; + int UnrealBoneNumber = -1; + int HoudiniBoneNumber = -1;; + TArray Children; + FHoudiniSkeletonBone * Parent = nullptr; +}; + + +struct FHoudiniSkeleton +{ + TArray Bones; + FHoudiniSkeletonBone * Root = nullptr; + TMap BoneMap; + TMap HoudiniBoneMap; +}; + +struct FHoudiniSkinInfluence +{ + // Don't be tempted to store a string here, there are tens of millions of these in an import. + // Keep this class simple. + FHoudiniSkeletonBone * Bone = nullptr; + float Weight = 0.0f; +}; +struct FHoudiniInfluences +{ + TArray Influences; + int NumInfluences = 0; + int NumVertices = 0; +}; + +struct FHoudiniSkeletalMeshMaterial +{ + FString OverridePath; // valid if using unreal_material + HAPI_NodeId NodeId; // valid if using a houdini material + FString AssetPath; // asset path of material to use. + int Slot; // material slot number. may be derived from OverridePath. +}; + + +struct FHoudiniSkeletalMeshMaterialSettings +{ + TArray< FHoudiniSkeletalMeshMaterial> Materials; + TArray MaterialIds; + HAPI_NodeId GeoNodeId = -1; + bool bHoudiniMaterials = false; +}; + +struct HOUDINIENGINE_API FHoudiniSkeletalMeshUtils +{ + static FHoudiniSkeleton UnrealToHoudiniSkeleton(USkeleton * Skeleton); + + static FHoudiniSkeleton FetchSkeleton(HAPI_NodeId PoseNodeId, HAPI_PartId PosePartId); + + static FHoudiniInfluences FetchInfluences(HAPI_NodeId NodeId, HAPI_PartId PartId, FHoudiniSkeleton& Skeleton); + + static FHoudiniSkeletalMeshMaterialSettings GetHoudiniMaterials(HAPI_NodeId NodeId, HAPI_PartId PartId, int NumFaces); + + static bool CreateHoudiniMaterial( + FHoudiniSkeletalMeshMaterialSettings& SkeletalFaceMaterials, + TMap> InputAssignmentMaterials, + TMap> AllOutputMaterials, + TMap> OutputAssignmentMaterials, + const FHoudiniPackageParams& InPackageParams); + + static FHoudiniSkeletalMeshMaterialSettings GetMaterialOverrides(HAPI_NodeId NodeId, HAPI_PartId PartId); + + static bool AddBonesToUnrealSkeleton(USkeleton * UnrealSkeleton, const FHoudiniSkeleton * HoudiniSkeleton); + + static void AddBonesToUnrealSkeleton(FReferenceSkeletonModifier& RefSkeletonModifier, const FHoudiniSkeletonBone* Bone); + + static bool RemapInfluences(FHoudiniInfluences & Influences, const FHoudiniSkeleton & NewSkeleton); + + static FMatrix MakeMatrixFromHoudiniData(const float Rotation[], const float Position[]); + + static FMatrix UnrealToHoudiniMatrix(const FTransform& Transform); + + static FMatrix UnrealToHoudiniMatrix(const FMatrix& Transform); + + static void UnrealToHoudiniMatrix(FMatrix& UnrealMatrix, float Rotation[], float Position[]); + + static void UnrealToHoudiniMatrix(FMatrix& UnrealMatrix, float Matrix[]); + + static void ConstructLocalMatricesFromGlobal(FHoudiniSkeletonBone* Node, const FHoudiniSkeletonBone* Parent); + + static TArray CreateSortedBoneList(TArray & UnsortedBones, int RootIndex); + +}; + + +``` + +`Source/HoudiniEngine/Private/HoudiniSplineTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniSplineTranslator.h" + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniInput.h" +#include "HoudiniOutput.h" +#include "HoudiniCookable.h" +#include "HoudiniSplineComponent.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineString.h" +#include "HoudiniGenericAttribute.h" +#include "HoudiniGeoPartObject.h" +#include "Components/SplineComponent.h" + +#include "EditorViewportClient.h" +#include "HoudiniEngineAttributes.h" +#include "Engine/Selection.h" + +#include "HoudiniEnginePrivatePCH.h" +#include "HAPI/HAPI.h" + +void +FHoudiniSplineTranslator::ExtractStringPositions(const FString& Positions, TArray& OutPositions) +{ + TArray PointStrings; + static const TCHAR * PositionSeparators[] = + { + TEXT(" "), + TEXT(","), + }; + + int32 NumCoords = Positions.ParseIntoArray(PointStrings, PositionSeparators, 2); + OutPositions.SetNum(NumCoords / 3); + for (int32 OutIndex = 0; OutIndex < OutPositions.Num(); OutIndex++) + { + const int32& CoordIndex = OutIndex * 3; + OutPositions[OutIndex].X = FCString::Atod(*(PointStrings[CoordIndex + 0])) * HAPI_UNREAL_SCALE_FACTOR_POSITION; + OutPositions[OutIndex].Y = FCString::Atod(*(PointStrings[CoordIndex + 2])) * HAPI_UNREAL_SCALE_FACTOR_POSITION; + OutPositions[OutIndex].Z = FCString::Atod(*(PointStrings[CoordIndex + 1])) * HAPI_UNREAL_SCALE_FACTOR_POSITION; + } +} + +void +FHoudiniSplineTranslator::ConvertPositionToVectorData(const TArray& InRawData, TArray>& OutVectorData, const TArray& CurveCounts) +{ + OutVectorData.SetNum(CurveCounts.Num()); + + int32 TotalNumPoints = 0; + for (const int32& NextCount : CurveCounts) + TotalNumPoints += NextCount; + + // Do not fill the output array, if the total number of points does not match + if (InRawData.Num() < TotalNumPoints * 3) + return; + + int32 Itr = 0; + for (int32 n = 0; n < CurveCounts.Num(); ++n) + { + TArray& NextVectorDataArray = OutVectorData[n]; + NextVectorDataArray.SetNumZeroed(CurveCounts[n]); + + for (int32 PtIdx = 0; PtIdx < CurveCounts[n]; ++PtIdx) + { + if (Itr + 2 >= InRawData.Num()) + return; + + // Swap Y/Z convert meters to centimeters + NextVectorDataArray[PtIdx].X = (double)(InRawData[Itr] * HAPI_UNREAL_SCALE_FACTOR_POSITION); + NextVectorDataArray[PtIdx].Y = (double)(InRawData[Itr + 2] * HAPI_UNREAL_SCALE_FACTOR_POSITION); + NextVectorDataArray[PtIdx].Z = (double)(InRawData[Itr + 1] * HAPI_UNREAL_SCALE_FACTOR_POSITION); + + Itr += 3; + } + } +} + +void +FHoudiniSplineTranslator::ConvertScaleToVectorData(const TArray& InRawData, TArray>& OutVectorData, const TArray& CurveCounts) +{ + OutVectorData.SetNum(CurveCounts.Num()); + + int32 TotalNumPoints = 0; + for (const int32& NextCount : CurveCounts) + TotalNumPoints += NextCount; + + // Do not fill the output array, if the total number of points does not match + if (InRawData.Num() < TotalNumPoints * 3) + return; + + int32 Itr = 0; + for (int32 n = 0; n < CurveCounts.Num(); ++n) + { + TArray& NextVectorDataArray = OutVectorData[n]; + NextVectorDataArray.SetNumZeroed(CurveCounts[n]); + + for (int32 PtIdx = 0; PtIdx < CurveCounts[n]; ++PtIdx) + { + if (Itr + 2 >= InRawData.Num()) + return; + + // Just Swap Y/Z + NextVectorDataArray[PtIdx].X = (double)InRawData[Itr]; + NextVectorDataArray[PtIdx].Y = (double)InRawData[Itr + 2]; + NextVectorDataArray[PtIdx].Z = (double)InRawData[Itr + 1]; + + Itr += 3; + } + } +} + +void +FHoudiniSplineTranslator::ConvertEulerRotationToVectorData(const TArray& InRawData, TArray>& OutVectorData, const TArray& CurveCounts) +{ + OutVectorData.SetNum(CurveCounts.Num()); + + int32 TotalNumPoints = 0; + for (const int32& NextCount : CurveCounts) + TotalNumPoints += NextCount; + + // Do not fill the output array, if the total number of points does not match + if (InRawData.Num() < TotalNumPoints * 3) + return; + + int32 Itr = 0; + for (int32 n = 0; n < CurveCounts.Num(); ++n) + { + TArray& NextVectorDataArray = OutVectorData[n]; + NextVectorDataArray.SetNumZeroed(CurveCounts[n]); + + for (int32 PtIdx = 0; PtIdx < CurveCounts[n]; ++PtIdx) + { + if (Itr + 2 >= InRawData.Num()) + return; + + // Just Swap Y/Z + NextVectorDataArray[PtIdx].X = (double)InRawData[Itr]; + NextVectorDataArray[PtIdx].Y = (double)InRawData[Itr + 2]; + NextVectorDataArray[PtIdx].Z = (double)InRawData[Itr + 1]; + + Itr += 3; + } + } +} + +void +FHoudiniSplineTranslator::ConvertQuaternionRotationToVectorData(const TArray& InRawData, TArray>& OutVectorData, const TArray& CurveCounts) +{ + OutVectorData.SetNum(CurveCounts.Num()); + + int32 TotalNumPoints = 0; + for (const int32& NextCount : CurveCounts) + TotalNumPoints += NextCount; + + // Do not fill the output array, if the total number of points does not match + if (InRawData.Num() < TotalNumPoints * 4) + return; + + int32 Itr = 0; + for (int32 n = 0; n < CurveCounts.Num(); ++n) + { + TArray& NextVectorDataArray = OutVectorData[n]; + NextVectorDataArray.SetNumZeroed(CurveCounts[n]); + + for (int32 PtIdx = 0; PtIdx < CurveCounts[n]; ++PtIdx) + { + if (Itr + 3 >= InRawData.Num()) + return; + + // Extract a quaternion: Swap Y/Z, invert W + FQuat ObjectRotation( + (double)InRawData[Itr + 0], + (double)InRawData[Itr + 2], + (double)InRawData[Itr + 1], + (double)-InRawData[Itr + 3]); + + // Get Euler angles + NextVectorDataArray[PtIdx] = ObjectRotation.Euler(); + + Itr += 4; + } + } +} + +void +FHoudiniSplineTranslator::UpdateHoudiniInputCurves(UHoudiniInput* Input) +{ + if (!Input || Input->GetInputType() != EHoudiniInputType::Curve) + return; + + TArray> *InputObjectArray = Input->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!InputObjectArray) + return; + + for (UHoudiniInputObject * NextInputObject : *InputObjectArray) + { + UHoudiniInputHoudiniSplineComponent * HoudiniSplineInput = Cast(NextInputObject); + if (!HoudiniSplineInput) + continue; + + UHoudiniSplineComponent * HoudiniSplineComponent = HoudiniSplineInput->GetCurveComponent(); + FHoudiniSplineTranslator::UpdateHoudiniCurve(HoudiniSplineComponent); + } +} + +bool +FHoudiniSplineTranslator::UpdateHoudiniCurve(UHoudiniSplineComponent* HoudiniSplineComponent) +{ + if (!IsValid(HoudiniSplineComponent)) + return false; + + if (HoudiniSplineComponent->bIsLegacyInputCurve) + { + return UpdateHoudiniCurveLegacy(HoudiniSplineComponent); + } + + int32 CurveNode_id = HoudiniSplineComponent->GetNodeId(); + if (CurveNode_id < 0) + return false; + + int32 PartId = 0; + + HAPI_InputCurveInfo InputCurveInfo; + FHoudiniApi::InputCurveInfo_Init(&InputCurveInfo); + + if (FHoudiniApi::GetInputCurveInfo( + FHoudiniEngine::Get().GetSession(), CurveNode_id, PartId, &InputCurveInfo) != HAPI_RESULT_SUCCESS ) + { + HAPI_NodeId NodeId = -1; + // Create the curve SOP Node + + if (!FHoudiniSplineTranslator::HapiCreateCurveInputNode(NodeId, -1, HoudiniSplineComponent->GetName(), false)) + return false; + + HoudiniSplineComponent->SetNodeId(NodeId); + + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetInputCurveInfo( + FHoudiniEngine::Get().GetSession(), CurveNode_id, PartId, &InputCurveInfo), false); + } + + HoudiniSplineComponent->SetCurveType((EHoudiniCurveType)InputCurveInfo.curveType); + HoudiniSplineComponent->SetCurveMethod((EHoudiniCurveMethod)InputCurveInfo.inputMethod); + HoudiniSplineComponent->SetClosedCurve(InputCurveInfo.closed); + HoudiniSplineComponent->SetReversed(InputCurveInfo.reverse); + HoudiniSplineComponent->SetCurveOrder(InputCurveInfo.order); + HoudiniSplineComponent->SetCurveBreakpointParameterization((EHoudiniCurveBreakpointParameterization)InputCurveInfo.breakpointParameterization); + + HAPI_AttributeInfo HapiCoordsInfo; + FHoudiniApi::AttributeInfo_Init(&HapiCoordsInfo); + + // Get and set the input coords positions + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), CurveNode_id, PartId, HAPI_UNREAL_ATTRIB_HAPI_INPUT_CURVE_COORDS, HAPI_ATTROWNER_DETAIL, &HapiCoordsInfo), false); + + if (HapiCoordsInfo.exists) + { + TArray CurveInputPointsFloat; + CurveInputPointsFloat.SetNum(HapiCoordsInfo.totalArrayElements); + TArray CurveSizes; + CurveSizes.SetNum(1); // Should be curve count, but only allow 1 for now. + + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetAttributeFloatArrayData( + FHoudiniEngine::Get().GetSession(), + CurveNode_id, + PartId, + HAPI_UNREAL_ATTRIB_HAPI_INPUT_CURVE_COORDS, + &HapiCoordsInfo, + CurveInputPointsFloat.GetData(), + HapiCoordsInfo.totalArrayElements, + CurveSizes.GetData(), + 0, + HapiCoordsInfo.count), false); + + TArray CurveInputPoints; + CurveInputPoints.SetNum(CurveInputPointsFloat.Num() / 3); + + // Coordinate conversion + FHoudiniEngineUtils::ConvertHoudiniPositionToUnrealVector(CurveInputPointsFloat, CurveInputPoints); + + // Build curve points for editable curves. + if (HoudiniSplineComponent->CurvePoints.Num() != CurveInputPoints.Num()) + { + HoudiniSplineComponent->CurvePoints.SetNum(CurveInputPoints.Num()); + for(int32 Idx = 0; Idx < CurveInputPoints.Num(); Idx++) + { + FTransform Transform = FTransform::Identity; + Transform.SetLocation(CurveInputPoints[Idx]); + HoudiniSplineComponent->CurvePoints[Idx] = Transform; + } + } + } + + TArray RefinedCurvePositions; + FHoudiniHapiAccessor Accessor(CurveNode_id, 0, HAPI_UNREAL_ATTRIB_POSITION); + if (!Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, RefinedCurvePositions)) + { + return false; + } + + TArray CurveDisplayPoints; + FHoudiniEngineUtils::ConvertHoudiniPositionToUnrealVector(RefinedCurvePositions, CurveDisplayPoints); + + // Update the display point on the curve + HoudiniSplineComponent->Construct(CurveDisplayPoints); + + HoudiniSplineComponent->MarkChanged(false); + + return true; +} + +bool FHoudiniSplineTranslator::UpdateHoudiniCurveLegacy(UHoudiniSplineComponent* HoudiniSplineComponent) +{ + if (!IsValid(HoudiniSplineComponent)) + return false; + + int32 CurveNode_id = HoudiniSplineComponent->GetNodeId(); + if (CurveNode_id < 0) + return false; + + FString CurvePointsString = FString(); + if (!FHoudiniEngineUtils::HapiGetParameterDataAsString( + CurveNode_id, HAPI_UNREAL_PARAM_CURVE_COORDS, TEXT(""), CurvePointsString)) + { + return false; + } + + int32 CurveTypeValue = (int32)EHoudiniCurveType::Bezier; + if (!FHoudiniEngineUtils::HapiGetParameterDataAsInteger( + CurveNode_id, HAPI_UNREAL_PARAM_CURVE_TYPE, 0, CurveTypeValue)) + { + return false; + } + HoudiniSplineComponent->SetCurveType((EHoudiniCurveType)CurveTypeValue); + + int32 CurveMethodValue = (int32)EHoudiniCurveMethod::CVs; + if (!FHoudiniEngineUtils::HapiGetParameterDataAsInteger( + CurveNode_id, HAPI_UNREAL_PARAM_CURVE_METHOD, 0, CurveMethodValue)) + { + return false; + } + HoudiniSplineComponent->SetCurveMethod((EHoudiniCurveMethod)CurveMethodValue); + + int32 CurveClosed = 0; + if (!FHoudiniEngineUtils::HapiGetParameterDataAsInteger( + CurveNode_id, HAPI_UNREAL_PARAM_CURVE_CLOSED, 0, CurveClosed)) + { + return false; + } + HoudiniSplineComponent->SetClosedCurve(CurveClosed == 1); + + int32 CurveReversed = 0; + if (!FHoudiniEngineUtils::HapiGetParameterDataAsInteger( + CurveNode_id, HAPI_UNREAL_PARAM_CURVE_REVERSED, 0, CurveReversed)) + { + return false; + } + HoudiniSplineComponent->SetReversed(CurveReversed == 1); + + // We need to get the NodeInfo to get the parent id + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetNodeInfo(FHoudiniEngine::Get().GetSession(), CurveNode_id, &NodeInfo), false); + + TArray RefinedCurvePositions; + FHoudiniHapiAccessor Accessor(CurveNode_id, 0 , HAPI_UNREAL_ATTRIB_POSITION); + if (!Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, RefinedCurvePositions)) + { + return false; + } + + // Process coords string and extract positions. + TArray CurvePoints; + FHoudiniSplineTranslator::ExtractStringPositions(CurvePointsString, CurvePoints); + + TArray CurveDisplayPoints; + FHoudiniEngineUtils::ConvertHoudiniPositionToUnrealVector(RefinedCurvePositions, CurveDisplayPoints); + + // Build curve points for editable curves. + if (HoudiniSplineComponent->CurvePoints.Num() != CurvePoints.Num()) + { + HoudiniSplineComponent->CurvePoints.SetNum(CurvePoints.Num()); + for(int32 Idx = 0; Idx < CurvePoints.Num(); Idx++) + { + FTransform Transform = FTransform::Identity; + Transform.SetLocation(CurvePoints[Idx]); + HoudiniSplineComponent->CurvePoints[Idx] = Transform; + } + } + + // Update the display point on the curve + HoudiniSplineComponent->Construct(CurveDisplayPoints); + + HoudiniSplineComponent->MarkChanged(false); + + return true; +} + + +bool +FHoudiniSplineTranslator::HapiUpdateNodeForHoudiniSplineComponent( + UHoudiniSplineComponent* HoudiniSplineComponent, + bool bInAddRotAndScaleAttributes) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniSplineTranslator::HapiUpdateNodeForHoudiniSplineComponent); + + if (!IsValid(HoudiniSplineComponent)) + return true; + + TArray PositionArray; + TArray RotationArray; + TArray Scales3dArray; + for (FTransform& CurrentTransform : HoudiniSplineComponent->CurvePoints) + { + PositionArray.Add(CurrentTransform.GetLocation()); + if (bInAddRotAndScaleAttributes) + { + RotationArray.Add(CurrentTransform.GetRotation()); + Scales3dArray.Add(CurrentTransform.GetScale3D()); + } + } + + HAPI_NodeId CurveNode_id = HoudiniSplineComponent->GetNodeId(); + FTransform ParentTransform = HoudiniSplineComponent->GetComponentTransform(); + + FString InputNodeNameString = HoudiniSplineComponent->GetName(); + UHoudiniInputHoudiniSplineComponent* InputObject = Cast(HoudiniSplineComponent->GetOuter()); + if (InputObject) + { + UHoudiniInput* Input = Cast(InputObject->GetOuter()); + if (Input) + { + InputNodeNameString = Input->GetNodeBaseName(); + } + } + InputNodeNameString += TEXT("_curve"); + + bool Success = FHoudiniSplineTranslator::HapiCreateCurveInputNodeForData( + CurveNode_id, + -1, + InputNodeNameString, + &PositionArray, + bInAddRotAndScaleAttributes ? &RotationArray : nullptr, + bInAddRotAndScaleAttributes ? &Scales3dArray : nullptr, + HoudiniSplineComponent->GetCurveType(), + HoudiniSplineComponent->GetCurveMethod(), + HoudiniSplineComponent->IsClosedCurve(), + HoudiniSplineComponent->IsReversed(), + false, + ParentTransform, + HoudiniSplineComponent->IsLegacyInputCurve(), + HoudiniSplineComponent->GetCurveOrder(), + HoudiniSplineComponent->GetCurveBreakpointParameterization() + ); + + HoudiniSplineComponent->SetNodeId(CurveNode_id); + Success &= UpdateHoudiniCurve(HoudiniSplineComponent); + + return Success; +} + +bool +FHoudiniSplineTranslator::IsCurveInputNodeValid(const HAPI_NodeId& InNodeId, const bool& bLegacyNode) +{ + // Check if connected asset id is valid, if it is not, we need to create an input asset. + if(InNodeId < 0) + return false; + + if (bLegacyNode) + { + // Legacy Curve::1.0 node + // Check if the input node has a curve "coords" params. + // If it doesn't, then the node is not a valid legacy curve1.0 node + HAPI_ParmId ParmId = -1; + if (FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), InNodeId, HAPI_UNREAL_PARAM_CURVE_COORDS, &ParmId) != HAPI_RESULT_SUCCESS + || ParmId < 0) + { + return false; + } + } + else + { + // New input curves + // First, check that the geo type is a curve + HAPI_GeoInfo InputGeoInfo; + FHoudiniApi::GeoInfo_Init(&InputGeoInfo); + if (FHoudiniApi::GetGeoInfo(FHoudiniEngine::Get().GetSession(), InNodeId, &InputGeoInfo) != HAPI_RESULT_SUCCESS) + return false; + + if (InputGeoInfo.type != HAPI_GEOTYPE_CURVE) + return false; + + // Check that the part type is a curve + HAPI_PartInfo InputPartInfo; + FHoudiniApi::PartInfo_Init(&InputPartInfo); + if (FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), InNodeId, 0, &InputPartInfo) != HAPI_RESULT_SUCCESS) + return false; + + if (InputPartInfo.type != HAPI_PARTTYPE_CURVE) + return false; + + // Check if the input node has a curve "coords" params. + // If it does, then the node a legacy curve1.0 node, so we need to recreate the input node! + HAPI_ParmId ParmId = -1; + if (FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), InNodeId, HAPI_UNREAL_PARAM_CURVE_COORDS, &ParmId) == HAPI_RESULT_SUCCESS + && ParmId >= 0) + return false; + + // Previous method. + // This caused crashes in HAPI/HARS and forced the user to restart the session manually! + // We already have an input node, make sure that it's a curve input + // if (FHoudiniApi::GetInputCurveInfo(FHoudiniEngine::Get().GetSession(), CurveNodeId, 0, &InputCurveInfo) != HAPI_RESULT_SUCCESS) + // return false; + } + + // We can reuse the node! + return true; +} + + +bool +FHoudiniSplineTranslator::HapiCreateCurveInputNodeForData( + HAPI_NodeId& CurveNodeId, + const HAPI_NodeId& ParentNodeId, + const FString& InputNodeName, + TArray* Positions, + TArray* Rotations, + TArray* Scales3d, + EHoudiniCurveType InCurveType, + EHoudiniCurveMethod InCurveMethod, + const bool& InClosed, + const bool& InReversed, + const bool& InForceClose, + const FTransform& ParentTransform, + const bool & InIsLegacyCurve, + const int32& InOrder, + const EHoudiniCurveBreakpointParameterization& InBreakpointParameterization + ) +{ + if (InIsLegacyCurve) + { + return HapiCreateCurveInputNodeForDataLegacy(CurveNodeId, ParentNodeId, InputNodeName, Positions, Rotations, Scales3d, InCurveType, InCurveMethod, InClosed, InReversed, InForceClose, ParentTransform); + } + +#if WITH_EDITOR + + // We also need a valid host asset and 2 points to make a curve + int32 NumberOfCVs = Positions->Num(); + if (NumberOfCVs < 2) + return false; + + // Check if connected asset id is valid, if it is not, we need to create an input asset. + bool bNeedToCreateNewCurveInputNode = !IsCurveInputNodeValid(CurveNodeId); + + HAPI_InputCurveInfo InputCurveInfo; + FHoudiniApi::InputCurveInfo_Init(&InputCurveInfo); + if (bNeedToCreateNewCurveInputNode) + { + // Create a new curve input Node + HAPI_NodeId NodeId = -1; + if (!FHoudiniSplineTranslator::HapiCreateCurveInputNode(NodeId, ParentNodeId, InputNodeName, InIsLegacyCurve)) + return false; + + // Check if we have a valid id for this new input asset. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(NodeId)) + return false; + + // We now have a valid id. + HAPI_NodeId PreviousInputNodeId = CurveNodeId; + CurveNodeId = NodeId; + + // We have now created a valid new input node, delete the previous one + if (PreviousInputNodeId >= 0) + { + // Get the parent OBJ node ID before deleting! + HAPI_NodeId PreviousInputOBJNode = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousInputNodeId); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input curve node")); + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputOBJNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input curve OBJ node.")); + } + } + } + + InputCurveInfo.curveType = (HAPI_CurveType)InCurveType; + InputCurveInfo.inputMethod = (HAPI_InputCurveMethod)InCurveMethod; + InputCurveInfo.closed = InClosed; + InputCurveInfo.reverse = InReversed; + InputCurveInfo.order = InOrder; + InputCurveInfo.breakpointParameterization = (HAPI_InputCurveParameterization)InBreakpointParameterization; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetInputCurveInfo( + FHoudiniEngine::Get().GetSession(), CurveNodeId, 0, + &InputCurveInfo), false); + + TArray CurvePositions; + CurvePositions.SetNum(NumberOfCVs * 3); + + for (int32 i = 0; i < Positions->Num(); i++) + { + CurvePositions[i*3 + 0] = (float)((*Positions)[i].X) / HAPI_UNREAL_SCALE_FACTOR_POSITION; + // Swap Y/Z + CurvePositions[i*3 + 1] = (float)((*Positions)[i].Z) / HAPI_UNREAL_SCALE_FACTOR_POSITION; + CurvePositions[i*3 + 2] = (float)((*Positions)[i].Y) / HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + + TArray CurveRotations; + TArray CurveScales; + + bool bAddRotations = (Rotations != nullptr && Rotations->Num() == Positions->Num()); + bool bAddScales3d = (Scales3d != nullptr && Scales3d->Num() == Positions->Num()); + + if (bAddRotations) + { + CurveRotations.SetNum(NumberOfCVs * 4); + for (int32 Idx = 0; Idx < Rotations->Num(); Idx++) + { + // Get current quaternion + const FQuat& RotationQuaternion = (*Rotations)[Idx]; + + CurveRotations[Idx * 4 + 0] = RotationQuaternion.X; + CurveRotations[Idx * 4 + 1] = RotationQuaternion.Z; + CurveRotations[Idx * 4 + 2] = RotationQuaternion.Y; + CurveRotations[Idx * 4 + 3] = -RotationQuaternion.W; + } + } + + if (bAddScales3d) + { + CurveScales.SetNum(NumberOfCVs * 3); + for (int32 Idx = 0; Idx < Scales3d->Num(); Idx++) + { + // Get current scale + FVector ScaleVector = (*Scales3d)[Idx]; + CurveScales[Idx * 3 + 0] = ScaleVector.X; + CurveScales[Idx * 3 + 1] = ScaleVector.Z; + CurveScales[Idx * 3 + 2] = ScaleVector.Y; + } + } + + if (bAddRotations || bAddScales3d) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetInputCurvePositionsRotationsScales( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + CurvePositions.GetData(), 0, CurvePositions.Num(), + CurveRotations.GetData(), 0, CurveRotations.Num(), + CurveScales.GetData(), 0, CurveScales.Num()), false); + } + else + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetInputCurvePositions( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + CurvePositions.GetData(), 0, CurvePositions.Num()), false); + } +#endif + + return true; +} + +bool +FHoudiniSplineTranslator::HapiCreateCurveInputNodeForDataLegacy( + HAPI_NodeId& CurveNodeId, + const HAPI_NodeId& ParentNodeId, + const FString& InputNodeName, + TArray* Positions, + TArray* Rotations, + TArray* Scales3d, + const EHoudiniCurveType& InCurveType, + const EHoudiniCurveMethod& InCurveMethod, + const bool& InClosed, + const bool& InReversed, + const bool& InForceClose, + const FTransform& ParentTransform ) +{ +#if WITH_EDITOR + // Positions are required + if (!Positions) + return false; + + // We also need a valid host asset and 2 points to make a curve + int32 NumberOfCVs = Positions->Num(); + if (NumberOfCVs < 2) + return false; + + // Check if connected asset id is valid, if it is not, we need to create an input asset. + if (!IsCurveInputNodeValid(CurveNodeId, true)) + { + HAPI_NodeId NodeId = -1; + // Create the curve SOP Node + if (!FHoudiniSplineTranslator::HapiCreateCurveInputNode(NodeId, ParentNodeId, InputNodeName, true)) + return false; + + // Check if we have a valid id for this new input asset. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(NodeId)) + return false; + + // We now have a valid id. + HAPI_NodeId PreviousInputNodeId = CurveNodeId; + CurveNodeId = NodeId; + + // We have now created a valid new input node, delete the previous one + if (PreviousInputNodeId >= 0) + { + // Get the parent OBJ node ID before deleting! + HAPI_NodeId PreviousInputOBJNode = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousInputNodeId); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input curve node")); + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputOBJNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input curve OBJ node.")); + } + } + } + else + { + // We have to revert the Geo to its original state so we can use the Curve SOP: + // adding parameters to the Curve SOP locked it, preventing its parameters (type, method, isClosed) from working + FHoudiniApi::RevertGeo(FHoudiniEngine::Get().GetSession(), CurveNodeId); + } + + // + // In order to be able to add rotations and scale attributes to the curve SOP, we need to cook it twice: + // + // - First, we send the positions string to it, and cook it without refinement. + // this will allow us to get the proper curve CVs, part attributes and curve info to create the desired curve. + // + // - We then need to send back all the info extracted from the curve SOP to it, and add the rotation + // and scale attributes to it. This will lock the curve SOP, and prevent the curve type and method + // parameters from functioning properly (hence why we needed the first cook to set that up) + // + + // Set the curve type and curve method parameters for the curve node + int32 CurveTypeValue = (int32)InCurveType; + FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), CurveNodeId, + HAPI_UNREAL_PARAM_CURVE_TYPE, 0, CurveTypeValue); + + int32 CurveMethodValue = (int32)InCurveMethod; + FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), CurveNodeId, + HAPI_UNREAL_PARAM_CURVE_METHOD, 0, CurveMethodValue); + + int32 CurveClosed = InClosed ? 1 : 0; + FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), CurveNodeId, + HAPI_UNREAL_PARAM_CURVE_CLOSED, 0, CurveClosed); + + int32 CurveReversed = InReversed ? 1 : 0; + FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), CurveNodeId, + HAPI_UNREAL_PARAM_CURVE_REVERSED, 0, CurveReversed); + + // Reading the curve parameters + FHoudiniEngineUtils::HapiGetParameterDataAsInteger( + CurveNodeId, HAPI_UNREAL_PARAM_CURVE_TYPE, 0, CurveTypeValue); + FHoudiniEngineUtils::HapiGetParameterDataAsInteger( + CurveNodeId, HAPI_UNREAL_PARAM_CURVE_METHOD, 0, CurveMethodValue); + FHoudiniEngineUtils::HapiGetParameterDataAsInteger( + CurveNodeId, HAPI_UNREAL_PARAM_CURVE_CLOSED, 0, CurveClosed); + FHoudiniEngineUtils::HapiGetParameterDataAsInteger( + CurveNodeId, HAPI_UNREAL_PARAM_CURVE_REVERSED, 0, CurveReversed); + + if (InForceClose) + { + // We need to update the closed parameter + FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), CurveNodeId, HAPI_UNREAL_PARAM_CURVE_CLOSED, 0, 1); + + CurveClosed = 1; + } + + // For closed NURBS (CVs and Breakpoints), we have to close the curve manually, by duplicating its last point + // in order to be able to set the rotations and scales attributes properly. + bool bCloseCurveManually = false; + if (CurveClosed && (CurveTypeValue == HAPI_CURVETYPE_NURBS) && (CurveMethodValue != 2)) + { + // The curve is not closed anymore + if (HAPI_RESULT_SUCCESS == FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), CurveNodeId, + HAPI_UNREAL_PARAM_CURVE_CLOSED, 0, 0)) + { + bCloseCurveManually = true; + + // Duplicating the first point to the end of the curve + // This needs to be done before sending the position string + FVector pos = (*Positions)[0]; + Positions->Add(pos); + + CurveClosed = false; + } + } + + // Creating the position string + FString PositionString = TEXT(""); + FHoudiniSplineTranslator::CreatePositionsString(*Positions, PositionString); + + // Get param id for the PositionString and modify it + HAPI_ParmId ParmId = -1; + if (FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), CurveNodeId, + HAPI_UNREAL_PARAM_CURVE_COORDS, &ParmId) != HAPI_RESULT_SUCCESS) + { + return false; + } + + std::string ConvertedString = H_TCHAR_TO_UTF8(*PositionString); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue( + FHoudiniEngine::Get().GetSession(), CurveNodeId, + ConvertedString.c_str(), ParmId, 0), false); + + // If we don't want to add rotations or scale attributes to the curve, + // we can just cook the node normally and stop here. + bool bAddRotations = (Rotations != nullptr); + bool bAddScales3d = (Scales3d != nullptr); + if (!bAddRotations && !bAddScales3d) + { + /* + HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::CookNode( + FHoudiniEngine::Get().GetSession(), CurveNodeId, &CookOptions), false); + */ + + // Cook the node, no need to wait for completion + return FHoudiniEngineUtils::HapiCookNode(CurveNodeId, nullptr, false); + } + + // Setting up the first cook, without the curve refinement + HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + CookOptions.maxVerticesPerPrimitive = -1; + CookOptions.refineCurveToLinear = false; + if(!FHoudiniEngineUtils::HapiCookNode(CurveNodeId, &CookOptions, true)) + return false; + + // We can now read back the Part infos from the cooked curve. + HAPI_PartInfo PartInfos; + FHoudiniApi::PartInfo_Init(&PartInfos); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetPartInfo( + FHoudiniEngine::Get().GetSession(), CurveNodeId, 0, &PartInfos), false); + + // + // Depending on the curve type and method, additionnal control points might have been created. + // We now have to interpolate the rotations and scale attributes for these. + // + + // Lambda function that interpolates rotation, scale and uniform scales values + // between two points using fCoeff as a weight, and insert the interpolated value at nInsertIndex + auto InterpolateRotScaleUScale = [&](const int32& nIndex1, const int32& nIndex2, const float& fCoeff, const int32& nInsertIndex) + { + if (Rotations && Rotations->IsValidIndex(nIndex1) && Rotations->IsValidIndex(nIndex2)) + { + FQuat interpolation = FQuat::Slerp((*Rotations)[nIndex1], (*Rotations)[nIndex2], fCoeff); + if (Rotations->IsValidIndex(nInsertIndex)) + Rotations->Insert(interpolation, nInsertIndex); + else + Rotations->Add(interpolation); + } + + if (Scales3d && Scales3d->IsValidIndex(nIndex1) && Scales3d->IsValidIndex(nIndex2)) + { + FVector interpolation = fCoeff * (*Scales3d)[nIndex1] + (1.0f - fCoeff) * (*Scales3d)[nIndex2]; + if (Scales3d->IsValidIndex(nInsertIndex)) + Scales3d->Insert(interpolation, nInsertIndex); + else + Scales3d->Add(interpolation); + } + }; + + // Lambda function that duplicates rotation and scale values + // at nIndex and insert/adds it at nInsertIndex + auto DuplicateRotScale = [&](const int32& nIndex, const int32& nInsertIndex) + { + if (Rotations && Rotations->IsValidIndex(nIndex)) + { + FQuat value = (*Rotations)[nIndex]; + if (Rotations->IsValidIndex(nInsertIndex)) + Rotations->Insert(value, nInsertIndex); + else + Rotations->Add(value); + } + + if (Scales3d && Scales3d->IsValidIndex(nIndex)) + { + FVector value = (*Scales3d)[nIndex]; + if (Scales3d->IsValidIndex(nInsertIndex)) + Scales3d->Insert(value, nInsertIndex); + else + Scales3d->Add(value); + } + }; + + // Do we want to close the curve by ourselves? + if (bCloseCurveManually) + { + // We need to duplicate the info of the first point to the last + DuplicateRotScale(0, NumberOfCVs++); + + // We need to update the closed parameter + FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), CurveNodeId, + HAPI_UNREAL_PARAM_CURVE_CLOSED, 0, 1); + } + + // INTERPOLATION + if (CurveTypeValue == HAPI_CURVETYPE_NURBS) + { + // Closed NURBS have additional points reproducing the first ones + if (InClosed) + { + // Only the first one if the method is freehand ... + DuplicateRotScale(0, NumberOfCVs++); + + if (CurveMethodValue != 2) + { + // ... but also the 2nd and 3rd if the method is CVs or Breakpoints. + DuplicateRotScale(1, NumberOfCVs++); + DuplicateRotScale(2, NumberOfCVs++); + } + } + else if (CurveMethodValue == 1) + { + // Open NURBS have 2 new points if the method is breakpoint: + // One between the 1st and 2nd ... + InterpolateRotScaleUScale(0, 1, 0.5f, 1); + + // ... and one before the last one. + InterpolateRotScaleUScale(NumberOfCVs, NumberOfCVs - 1, 0.5f, NumberOfCVs); + NumberOfCVs += 2; + } + } + else if (CurveTypeValue == HAPI_CURVETYPE_BEZIER) + { + // Bezier curves requires additional point if the method is Breakpoints + if (CurveMethodValue == 1) + { + // 2 interpolated control points are added per points (except the last one) + int32 nOffset = 0; + for (int32 n = 0; n < NumberOfCVs - 1; n++) + { + int nIndex1 = n + nOffset; + int nIndex2 = n + nOffset + 1; + + InterpolateRotScaleUScale(nIndex1, nIndex2, 0.33f, nIndex2); + nIndex2++; + InterpolateRotScaleUScale(nIndex1, nIndex2, 0.66f, nIndex2); + + nOffset += 2; + } + NumberOfCVs += nOffset; + + if (CurveClosed) + { + // If the curve is closed, we need to add 2 points after the last, + // interpolated between the last and the first one + int nIndex = NumberOfCVs - 1; + InterpolateRotScaleUScale(nIndex, 0, 0.33f, NumberOfCVs++); + InterpolateRotScaleUScale(nIndex, 0, 0.66f, NumberOfCVs++); + + // and finally, the last point is the first.. + DuplicateRotScale(0, NumberOfCVs++); + } + } + else if (CurveClosed) + { + // For the other methods, if the bezier curve is closed, the last point is the 1st + DuplicateRotScale(0, NumberOfCVs++); + } + } + + // Even after interpolation, additional points might still be missing: + // Bezier curves require a certain number of points regarding their order, + // if points are lacking then HAPI duplicates the last one. + if (NumberOfCVs < PartInfos.pointCount) + { + int nToAdd = PartInfos.pointCount - NumberOfCVs; + for (int n = 0; n < nToAdd; n++) + { + DuplicateRotScale(NumberOfCVs - 1, NumberOfCVs); + NumberOfCVs++; + } + } + + // To avoid crashes, attributes will only be added if we now have the correct number of them + bAddRotations = bAddRotations && (Rotations->Num() == PartInfos.pointCount); + bAddScales3d = bAddScales3d && (Scales3d->Num() == PartInfos.pointCount); + + // We need to increase the point attributes count for points in the Part Infos + HAPI_AttributeOwner NewAttributesOwner = HAPI_ATTROWNER_POINT; + HAPI_AttributeOwner OriginalAttributesOwner = HAPI_ATTROWNER_POINT; + + int OriginalPointParametersCount = PartInfos.attributeCounts[NewAttributesOwner]; + if (bAddRotations) + PartInfos.attributeCounts[NewAttributesOwner] += 1; + if (bAddScales3d) + PartInfos.attributeCounts[NewAttributesOwner] += 1; + + // Sending the updated PartInfos + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, &PartInfos), false); + + // We need now to reproduce ALL the curves attributes for ALL the Owners.. + for (int nOwner = 0; nOwner < HAPI_ATTROWNER_MAX; nOwner++) + { + int nOwnerAttributeCount = nOwner == NewAttributesOwner ? OriginalPointParametersCount : PartInfos.attributeCounts[nOwner]; + if (nOwnerAttributeCount == 0) + continue; + + TArray AttributeNamesSH; + AttributeNamesSH.SetNum(nOwnerAttributeCount); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAttributeNames( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, (HAPI_AttributeOwner)nOwner, + AttributeNamesSH.GetData(), AttributeNamesSH.Num()), false); + + for (int nAttribute = 0; nAttribute < AttributeNamesSH.Num(); nAttribute++) + { + const HAPI_StringHandle sh = AttributeNamesSH[nAttribute]; + if (sh == 0) + continue; + + // Get the attribute name + std::string attr_name; + FHoudiniEngineString::ToStdString(sh, attr_name); + if (strcmp(attr_name.c_str(), "__topology") == 0) + continue; + + // and the attribute infos + HAPI_AttributeInfo attr_info; + FHoudiniApi::AttributeInfo_Init(&attr_info); + //FMemory::Memzero< HAPI_AttributeInfo >( attr_info ); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAttributeInfo( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, attr_name.c_str(), + (HAPI_AttributeOwner)nOwner, &attr_info), false); + + switch (attr_info.storage) + { + case HAPI_STORAGETYPE_INT: + { + // Storing IntData + TArray< int > IntData; + IntData.SetNumUninitialized(attr_info.count * attr_info.tupleSize); + + // GET + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAttributeIntData( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + attr_name.c_str(), &attr_info, -1, + IntData.GetData(), 0, attr_info.count), false); + + // ADD + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + attr_name.c_str(), &attr_info), false); + + // SET + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeIntData( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + attr_name.c_str(), + &attr_info, IntData.GetData(), + 0, attr_info.count), false); + } + break; + + case HAPI_STORAGETYPE_INT64: + { + // Storing IntData + TArray Int64Data; + Int64Data.SetNumUninitialized(attr_info.count * attr_info.tupleSize); + + // GET + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAttributeInt64Data( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + attr_name.c_str(), &attr_info, -1, + Int64Data.GetData(), 0, attr_info.count), false); + + // ADD + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + attr_name.c_str(), &attr_info), false); + + // SET + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeInt64Data( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + attr_name.c_str(), + &attr_info, Int64Data.GetData(), + 0, attr_info.count), false); + } + break; + + case HAPI_STORAGETYPE_FLOAT: + { + // Storing Float Data + TArray< float > FloatData; + FloatData.SetNumUninitialized(attr_info.count * attr_info.tupleSize); + + // GET + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + attr_name.c_str(), &attr_info, -1, + FloatData.GetData(), + 0, attr_info.count), false); + + // ADD + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + attr_name.c_str(), + &attr_info), false); + + // SET + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + attr_name.c_str(), &attr_info, + FloatData.GetData(), + 0, attr_info.count), false); + } + break; + + case HAPI_STORAGETYPE_STRING: + { + // Storing String Data + TArray StringHandleData; + StringHandleData.SetNumUninitialized(attr_info.count * attr_info.tupleSize); + + // GET + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAttributeStringData( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + attr_name.c_str(), &attr_info, + StringHandleData.GetData(), + 0, attr_info.count), false); + + // Convert the SH to const char * + TArray StringData; + StringData.SetNumUninitialized(attr_info.count); + for (int n = 0; n < StringHandleData.Num(); n++) + { + // Converting the string + std::string strSTD; + FHoudiniEngineString::ToStdString(sh, strSTD); + + StringData[n] = strSTD.c_str(); + } + + // ADD + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + attr_name.c_str(), + &attr_info), false); + + // SET + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeStringData( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + attr_name.c_str(), &attr_info, + StringData.GetData(), + 0, attr_info.count), false); + } + break; + + default: + { + // Unhandled attribute type - warn + HOUDINI_LOG_WARNING( + TEXT("HapiCreateCurveInputNodeForData() - Unhandled attribute type - skipping") + TEXT("- consider disabling additionnal rot/scale if having issues with the HDA")); + continue; + } + + } + } + } + + // Only GET/SET curve infos if the part is a curve... + // (Closed linear curves are actually not considered as curves...) + if (PartInfos.type == HAPI_PARTTYPE_CURVE) + { + // We need to read the curve infos ... + HAPI_CurveInfo CurveInfo; + FHoudiniApi::CurveInfo_Init(&CurveInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetCurveInfo( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + &CurveInfo), false); + + // ... the curve counts + TArray< int > CurveCounts; + CurveCounts.SetNumUninitialized(CurveInfo.curveCount); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetCurveCounts( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + CurveCounts.GetData(), + 0, CurveInfo.curveCount), false); + + // .. the curve orders + TArray< int > CurveOrders; + CurveOrders.SetNumUninitialized(CurveInfo.curveCount); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetCurveOrders( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + CurveOrders.GetData(), + 0, CurveInfo.curveCount), false); + + // .. And the Knots if they exist. + TArray< float > KnotsArray; + if (CurveInfo.hasKnots) + { + KnotsArray.SetNumUninitialized(CurveInfo.knotCount); + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::GetCurveKnots( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + KnotsArray.GetData(), + 0, CurveInfo.knotCount), false); + } + + // To set them back in HAPI + // CurveInfo + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetCurveInfo( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + &CurveInfo), false); + + // CurveCounts + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetCurveCounts( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + CurveCounts.GetData(), + 0, CurveInfo.curveCount), false); + + // CurveOrders + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetCurveOrders( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + CurveOrders.GetData(), + 0, CurveInfo.curveCount), false); + + // And Knots if they exist + if (CurveInfo.hasKnots) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetCurveKnots( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + KnotsArray.GetData(), + 0, CurveInfo.knotCount), false); + } + } + + if (PartInfos.faceCount > 0) + { + // getting the face counts + TArray< int > FaceCounts; + FaceCounts.SetNumUninitialized(PartInfos.faceCount); + + if (FHoudiniApi::GetFaceCounts( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + FaceCounts.GetData(), 0, + PartInfos.faceCount) == HAPI_RESULT_SUCCESS) + { + // Set the face count + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetFaceCounts( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + FaceCounts.GetData(), + 0, PartInfos.faceCount), false); + } + } + + if (PartInfos.vertexCount > 0) + { + // the vertex list + TArray VertexList; + VertexList.SetNumUninitialized(PartInfos.vertexCount); + + if (FHoudiniApi::GetVertexList( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + VertexList.GetData(), + 0, PartInfos.vertexCount) == HAPI_RESULT_SUCCESS) + { + // setting the vertex list + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetVertexList( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + VertexList.GetData(), + 0, PartInfos.vertexCount), false); + } + } + + // We can add attributes to the curve now that all the curves attributes + // and properties have been reset. + if (bAddRotations) + { + // Create ROTATION attribute info + HAPI_AttributeInfo AttributeInfoRotation; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoRotation); + AttributeInfoRotation.count = NumberOfCVs; + AttributeInfoRotation.tupleSize = 4; + AttributeInfoRotation.exists = true; + AttributeInfoRotation.owner = NewAttributesOwner; + AttributeInfoRotation.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoRotation.originalOwner = OriginalAttributesOwner; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + HAPI_UNREAL_ATTRIB_ROTATION, + &AttributeInfoRotation), false); + + // Convert the rotation infos + TArray< float > CurveRotations; + CurveRotations.SetNumZeroed(NumberOfCVs * 4); + for (int32 Idx = 0; Idx < NumberOfCVs; ++Idx) + { + // Get current quaternion + const FQuat& RotationQuaternion = (*Rotations)[Idx]; + + CurveRotations[Idx * 4 + 0] = RotationQuaternion.X; + CurveRotations[Idx * 4 + 1] = RotationQuaternion.Z; + CurveRotations[Idx * 4 + 2] = RotationQuaternion.Y; + CurveRotations[Idx * 4 + 3] = -RotationQuaternion.W; + } + + //we can now upload them to our attribute. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + HAPI_UNREAL_ATTRIB_ROTATION, + &AttributeInfoRotation, + CurveRotations.GetData(), + 0, AttributeInfoRotation.count), false); + } + + // Create SCALE attribute info. + if (bAddScales3d) + { + HAPI_AttributeInfo AttributeInfoScale; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoScale); + AttributeInfoScale.count = NumberOfCVs; + AttributeInfoScale.tupleSize = 3; + AttributeInfoScale.exists = true; + AttributeInfoScale.owner = NewAttributesOwner; + AttributeInfoScale.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoScale.originalOwner = OriginalAttributesOwner; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + HAPI_UNREAL_ATTRIB_SCALE, + &AttributeInfoScale), false); + + // Convert the scale + TArray< float > CurveScales; + CurveScales.SetNumZeroed(NumberOfCVs * 3); + for (int32 Idx = 0; Idx < NumberOfCVs; ++Idx) + { + // Get current scale + FVector ScaleVector = (*Scales3d)[Idx]; + CurveScales[Idx * 3 + 0] = ScaleVector.X; + CurveScales[Idx * 3 + 1] = ScaleVector.Z; + CurveScales[Idx * 3 + 2] = ScaleVector.Y; + } + + // We can now upload them to our attribute. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + CurveNodeId, 0, + HAPI_UNREAL_ATTRIB_SCALE, + &AttributeInfoScale, + CurveScales.GetData(), + 0, AttributeInfoScale.count), false); + } + + // Finally, commit the geo ... + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(CurveNodeId), false); + + // And cook it with refinement enabled + CookOptions.refineCurveToLinear = true; + if(!FHoudiniEngineUtils::HapiCookNode(CurveNodeId, &CookOptions, false)) + return false; +#endif + + return true; +} + +void +FHoudiniSplineTranslator::CreatePositionsString(const TArray& InPositions, FString& OutPositionString) +{ + OutPositionString = TEXT(""); + for (int32 Idx = 0; Idx < InPositions.Num(); ++Idx) + { + FVector Position = InPositions[Idx]; + // Convert to meters + Position /= HAPI_UNREAL_SCALE_FACTOR_POSITION; + // Swap Y/Z + OutPositionString += FString::Printf(TEXT("%f, %f, %f "), Position.X, Position.Z, Position.Y); + } +} + +bool +FHoudiniSplineTranslator::HapiCreateCurveInputNode( + HAPI_NodeId& OutCurveNodeId, + const HAPI_NodeId& ParentNodeId, + const FString& InputNodeName, + const bool& InIsLegacyCurve) +{ + // Create the curve SOP Node + HAPI_NodeId NewNodeId = -1; + if (InIsLegacyCurve) + { + // Create the curve SOP Node + if (ParentNodeId < 0) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + -1, TEXT("SOP/curve::"), InputNodeName, false, &NewNodeId), false); + } + else + { + // When creating a node inside a parent node (in other words, ParentNodeId is not -1), then we cannot + // specify the node type category prefix on the node name. + HAPI_NodeId ObjectNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + ParentNodeId, TEXT("geo"), InputNodeName, true, &ObjectNodeId), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + ObjectNodeId, TEXT("curve::"), InputNodeName, false, &NewNodeId), false); + } + + OutCurveNodeId = NewNodeId; + + // Submit default points to curve. + HAPI_ParmId ParmId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), NewNodeId, + HAPI_UNREAL_PARAM_CURVE_COORDS, &ParmId), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue( + FHoudiniEngine::Get().GetSession(), NewNodeId, + HAPI_UNREAL_PARAM_INPUT_CURVE_COORDS_DEFAULT, ParmId, 0), false); + + // Cook the newly created node + return FHoudiniEngineUtils::HapiCookNode(NewNodeId, nullptr, true); + } + else + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CreateInputCurveNode( + FHoudiniEngine::Get().GetSession(), -1, &NewNodeId, H_TCHAR_TO_UTF8(*InputNodeName)), false); + + OutCurveNodeId = NewNodeId; + + TArray DefaultPositions {0,0,3,3,0,3}; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetInputCurvePositions( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, DefaultPositions.GetData(), 0, DefaultPositions.Num()), false); + + return true; + } +} + +UHoudiniSplineComponent* +FHoudiniSplineTranslator::CreateHoudiniSplineComponentFromHoudiniEditableNode(const int32 & GeoId, const FString & PartName, UObject* OuterComponent) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniSplineTranslator::CreateHoudiniSplineComponentFromHoudiniEditableNode); + if (GeoId < 0) + return nullptr; + + if (!IsValid(OuterComponent)) + return nullptr; + + USceneComponent* const SceneComponent = Cast(OuterComponent); + if (!IsValid(SceneComponent)) + return nullptr; + + // Create a HoudiniSplineComponent for the editable curve. + UHoudiniSplineComponent* HoudiniSplineComponent = NewObject( + OuterComponent, + UHoudiniSplineComponent::StaticClass(), NAME_None, RF_Public | RF_Transactional); + + HoudiniSplineComponent->SetNodeId(GeoId); + HoudiniSplineComponent->SetGeoPartName(PartName); + bool IsLegacyCurve = false; + + // Check if it is a legacy curve + int32 ParmId = -1; + + if (FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), + GeoId, HAPI_UNREAL_PARAM_CURVE_COORDS, &ParmId) == HAPI_RESULT_SUCCESS && ParmId > 0) + { + IsLegacyCurve = true; + } + + HoudiniSplineComponent->SetIsLegacyInputCurve(IsLegacyCurve); + + HoudiniSplineComponent->RegisterComponent(); + HoudiniSplineComponent->AttachToComponent(SceneComponent, FAttachmentTransformRules::KeepRelativeTransform); + + // Delete the curve points so that UpdateHoudiniCurves initializes them from HAPI + HoudiniSplineComponent->CurvePoints.Empty(); + HoudiniSplineComponent->DisplayPoints.Empty(); + UpdateHoudiniCurve(HoudiniSplineComponent); + + ReselectSelectedActors(); + + return HoudiniSplineComponent; + +} + +UHoudiniSplineComponent* +FHoudiniSplineTranslator::CreateOutputHoudiniSplineComponent( + UHoudiniCookable* OuterHC, + TArray& CurvePoints, + const TArray& CurveRotations, + const TArray& CurveScales) +{ + if (!IsValid(OuterHC)) + return nullptr; + + USceneComponent* OuterComp = OuterHC->GetComponent(); + if (!IsValid(OuterComp)) + return nullptr; + + UHoudiniSplineComponent *NewHoudiniSplineComponent = NewObject(OuterHC, UHoudiniSplineComponent::StaticClass(), NAME_None, RF_Transactional); + if (!NewHoudiniSplineComponent) + return nullptr; + + NewHoudiniSplineComponent->Construct(CurvePoints); + + bool bHasRotations = CurveRotations.Num() == CurvePoints.Num(); + bool bHasScales = CurveScales.Num() == CurvePoints.Num(); + + TArray Transforms; + for (int32 n = 0; n < CurvePoints.Num(); ++n) + { + FTransform NextTransform = FTransform::Identity; + NextTransform.SetLocation(CurvePoints[n]); + + if (bHasRotations) + NextTransform.SetRotation(CurveRotations[n].Rotation().Quaternion()); + + if (bHasScales) + NextTransform.SetScale3D(CurveScales[n]); + + Transforms.Add(NextTransform); + } + + NewHoudiniSplineComponent->CurveType = EHoudiniCurveType::Polygon; + NewHoudiniSplineComponent->bIsOutputCurve = true; + + NewHoudiniSplineComponent->AttachToComponent(OuterComp, FAttachmentTransformRules::KeepRelativeTransform); + NewHoudiniSplineComponent->RegisterComponent(); + + ReselectSelectedActors(); + + return NewHoudiniSplineComponent; +} + +USplineComponent* +FHoudiniSplineTranslator::CreateOutputUnrealSplineComponent( + UObject* OuterComponent, + const TArray& CurvePoints, + const TArray& CurveRotations, + const TArray& CurveScales, + const bool& bIsLinear, + const bool& bIsClosed) +{ + if (!IsValid(OuterComponent)) + return nullptr; + + USceneComponent* OuterSceneComponent = Cast(OuterComponent); + if (!IsValid(OuterSceneComponent)) + return nullptr; + + UObject* Outer = nullptr; + Outer = OuterSceneComponent->GetOwner() ? OuterSceneComponent->GetOwner() : OuterSceneComponent->GetOuter(); + + USplineComponent* NewSplineComponent = NewObject(Outer, USplineComponent::StaticClass(), NAME_None, RF_Transactional); + + if (!NewSplineComponent) + return nullptr; + + // Clear default USplineComponent's points + NewSplineComponent->ClearSplinePoints(); + NewSplineComponent->bEditableWhenInherited = false; + + ESplinePointType::Type SplinePointType = bIsLinear ? ESplinePointType::Linear : ESplinePointType::Curve; + for (int32 n = 0; n < CurvePoints.Num(); ++n) + { + NewSplineComponent->AddSplinePoint(CurvePoints[n], ESplineCoordinateSpace::Local); + NewSplineComponent->SetSplinePointType(n, SplinePointType); + } + + bool bHasScales = CurveScales.Num() == CurvePoints.Num(); + if (bHasScales) + { + for (int32 n = 0; n < CurvePoints.Num(); ++n) + { + NewSplineComponent->SetScaleAtSplinePoint(n, CurveScales[n], false); + } + } + + bool bHasRotations = CurveRotations.Num() == CurvePoints.Num(); + if (bHasRotations) + { + for (int32 n = 0; n < CurvePoints.Num(); ++n) + { + NewSplineComponent->SetRotationAtSplinePoint(n, FRotator::MakeFromEuler(CurveRotations[n]), ESplineCoordinateSpace::Local, false); + } + } + + NewSplineComponent->SetClosedLoop(bIsClosed); + + NewSplineComponent->UpdateSpline(); + + NewSplineComponent->AttachToComponent(OuterSceneComponent, FAttachmentTransformRules::KeepRelativeTransform); + NewSplineComponent->RegisterComponent(); + AActor *OwnerActor = Cast(Outer); + if (IsValid(OwnerActor)) + OwnerActor->AddInstanceComponent(NewSplineComponent); + + ReselectSelectedActors(); + + return NewSplineComponent; +} + +bool +FHoudiniSplineTranslator::UpdateOutputUnrealSplineComponent( + USplineComponent* EditedSplineComponent, + const TArray& CurvePoints, + const TArray& CurveRotations, + const TArray& CurveScales, + const EHoudiniCurveType& CurveType, + const bool& bClosed) +{ + if (!IsValid(EditedSplineComponent)) + return false; + + if (CurvePoints.Num() < 2) + return false; + + EditedSplineComponent->ClearSplinePoints(); + + for (int32 Idx = 0; Idx < CurvePoints.Num(); ++Idx) + { + EditedSplineComponent->AddSplinePoint(CurvePoints[Idx], ESplineCoordinateSpace::Local, false); + + if (CurveType == EHoudiniCurveType::Polygon) + EditedSplineComponent->SetSplinePointType(Idx, ESplinePointType::Linear, false); + else + EditedSplineComponent->SetSplinePointType(Idx, ESplinePointType::Curve, false); + } + + bool bHasScales = CurveScales.Num() == CurvePoints.Num(); + if (bHasScales) + { + for (int32 n = 0; n < CurvePoints.Num(); ++n) + { + EditedSplineComponent->SetScaleAtSplinePoint(n, CurveScales[n], false); + } + } + + bool bHasRotations = CurveRotations.Num() == CurvePoints.Num(); + if (bHasRotations) + { + for (int32 n = 0; n < CurvePoints.Num(); ++n) + { + EditedSplineComponent->SetRotationAtSplinePoint(n, FRotator::MakeFromEuler(CurveRotations[n]), ESplineCoordinateSpace::Local, false); + } + } + + EditedSplineComponent->SetClosedLoop(bClosed, true); + + return true; +} + +bool +FHoudiniSplineTranslator::UpdateOutputHoudiniSplineComponent(const TArray& CurvePoints, UHoudiniSplineComponent* EditedHoudiniSplineComponent) +{ + if (!IsValid(EditedHoudiniSplineComponent)) + return false; + + if (CurvePoints.Num() < 2) + return false; + + int MinCount = FMath::Min(CurvePoints.Num(), EditedHoudiniSplineComponent->CurvePoints.Num()); + + int Idx = 0; + // modify existing points + for (; Idx < MinCount; ++Idx) + { + FTransform CurTrans = EditedHoudiniSplineComponent->CurvePoints[Idx]; + if (CurTrans.GetLocation() == CurvePoints[Idx]) + continue; + + CurTrans.SetLocation(CurvePoints[Idx]); + } + + // remove extra points + if (Idx < EditedHoudiniSplineComponent->CurvePoints.Num()-1) + { + for (int32 n = EditedHoudiniSplineComponent->CurvePoints.Num() - 1; n >= Idx; --n) + { + EditedHoudiniSplineComponent->RemovePointAtIndex(n); + } + } + + + // append extra points + for (; Idx < CurvePoints.Num(); ++Idx) + { + FTransform NewPoint = FTransform::Identity; + NewPoint.SetLocation(CurvePoints[Idx]); + EditedHoudiniSplineComponent->CurvePoints.Add(NewPoint); + } + + return true; +} + + +bool +FHoudiniSplineTranslator::CreateOutputSplinesFromHoudiniGeoPartObject( + const FHoudiniGeoPartObject& InHGPO, + UObject* InOuterComponent, + TMap& InSplines, + TMap& OutSplines, + const bool& InForceRebuild, + const EHoudiniCurveOutputType& OutputCurveType, + const bool& bIsLinear, + const bool& bIsClosed) +{ + if (!IsValid(InOuterComponent)) + return false; + + int32 CurveNodeId = InHGPO.GeoId; + int32 CurvePartId = InHGPO.PartId; + if (CurveNodeId < 0 || CurvePartId < 0) + return false; + + // Extract all curve points from this HGPO + + TArray RefinedCurvePositions; + FHoudiniHapiAccessor Accessor; + Accessor.Init(CurveNodeId, CurvePartId, HAPI_UNREAL_ATTRIB_POSITION); + Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, RefinedCurvePositions); + + TArray RefinedCurveRotations; + HAPI_AttributeInfo AttributeRefinedCurveRotations; + Accessor.Init(CurveNodeId, CurvePartId, HAPI_UNREAL_ATTRIB_ROTATION); + Accessor.GetInfo(AttributeRefinedCurveRotations, HAPI_ATTROWNER_INVALID); + Accessor.GetAttributeData(AttributeRefinedCurveRotations, RefinedCurveRotations); + + TArray RefinedCurveScales; + Accessor.Init(CurveNodeId, CurvePartId, HAPI_UNREAL_ATTRIB_SCALE); + Accessor.GetAttributeData(HAPI_ATTROWNER_INVALID, RefinedCurveScales); + + HAPI_CurveInfo CurveInfo; + FHoudiniApi::CurveInfo_Init(&CurveInfo); + FHoudiniApi::GetCurveInfo(FHoudiniEngine::Get().GetSession(), CurveNodeId, CurvePartId, &CurveInfo); + + int32 NumOfCurves = CurveInfo.curveCount; + TArray CurvePointsCounts; + CurvePointsCounts.SetNumZeroed(NumOfCurves); + FHoudiniApi::GetCurveCounts(FHoudiniEngine::Get().GetSession(), CurveNodeId, CurvePartId, CurvePointsCounts.GetData(), 0, NumOfCurves); + + TArray> CurvesDisplayPoints; + FHoudiniSplineTranslator::ConvertPositionToVectorData(RefinedCurvePositions, CurvesDisplayPoints, CurvePointsCounts); + + TArray> CurvesRotations; + if (AttributeRefinedCurveRotations.exists && AttributeRefinedCurveRotations.tupleSize == 4) + { + FHoudiniSplineTranslator::ConvertQuaternionRotationToVectorData(RefinedCurveRotations, CurvesRotations, CurvePointsCounts); + } + else + { + FHoudiniSplineTranslator::ConvertEulerRotationToVectorData(RefinedCurveRotations, CurvesRotations, CurvePointsCounts); + } + + TArray> CurvesScales; + FHoudiniSplineTranslator::ConvertScaleToVectorData(RefinedCurveScales, CurvesScales, CurvePointsCounts); + + // Extract all curve points from this HGPO + FString GeoName = InHGPO.PartName; + int32 CurveIdx = 1; + + // Iterate through all curves found in this HGPO + for (int32 n = 0; n < CurvesDisplayPoints.Num(); ++n) + { + FString CurveName = FString::Printf(TEXT("%s curve %d"), *GeoName, CurveIdx); + CurveIdx += 1; + + if (CurvePointsCounts[n] < 2) + { + // Invalid vertex count, skip this curve. + HOUDINI_LOG_WARNING( + TEXT("Creating Unreal Spline: Object [%d %s], Geo [%d], Part [%d %s], Curve# [%d] invalid vertex count.") + TEXT("- skipping."), + InHGPO.ObjectId, *InHGPO.ObjectName, InHGPO.GeoId, InHGPO.PartId, *InHGPO.PartName, CurveIdx); + continue; + } + + FHoudiniOutputObjectIdentifier CurveIdentifier(InHGPO.ObjectId, InHGPO.GeoId, InHGPO.PartId, CurveName); + FHoudiniOutputObject* FoundOutputObject = InSplines.Find(CurveIdentifier); + + bool bNeedToRebuildSpline = false; + if (!FoundOutputObject) + bNeedToRebuildSpline = true; + + USceneComponent* FoundComponent = nullptr; + if (FoundOutputObject && FoundOutputObject->OutputComponents.Num() > 0) + FoundComponent = Cast(FoundOutputObject->OutputComponents[0]); + + if (IsValid(FoundComponent)) + { + // Only support output to Unreal Spline for now... + //if (FoundComponent->IsA() && FoundOutputObject->CurveOutputProperty.CurveOutputType != EHoudiniCurveOutputType::UnrealSpline) + // bNeedToRebuildSpline = true; + + //if (FoundComponent->IsA() && FoundOutputObject->CurveOutputProperty.CurveOutputType != EHoudiniCurveOutputType::HoudiniSpline) + // bNeedToRebuildSpline = true; + + if (InHGPO.bHasGeoChanged || InHGPO.PartInfo.bHasChanged || InForceRebuild) + bNeedToRebuildSpline = true; + } + else + { + bNeedToRebuildSpline = true; + } + + // The curve has not changed, no need to go through the rest + if (!bNeedToRebuildSpline) + { + OutSplines.Add(CurveIdentifier, *FoundOutputObject); + continue; + } + + bool bReusedPreviousOutput = false; + if (!FoundOutputObject) + { + HOUDINI_LOG_WARNING( + TEXT("Creating Unreal Spline (default): Object [%d %s], Geo [%d], Part [%d %s], Curve# [%d], number of points [%d]."), + InHGPO.ObjectId, *InHGPO.ObjectName, InHGPO.GeoId, InHGPO.PartId, *InHGPO.PartName, CurveIdx, CurvePointsCounts[n]); + + // If not found (at initialize), create an Unreal spline + // We only support unreal spline for now.. + // May support Houdini spline too later + USplineComponent* CreatedSplineComponent = FHoudiniSplineTranslator::CreateOutputUnrealSplineComponent( + InOuterComponent, CurvesDisplayPoints[n], CurvesRotations[n], CurvesScales[n], bIsLinear, bIsClosed); + + if (!CreatedSplineComponent) + continue; + + // Create a new output object + FHoudiniOutputObject NewOutputObject; + check(NewOutputObject.OutputComponents.Num() < 2); // Multiple components not supported yet. + NewOutputObject.OutputComponents.Empty(); + NewOutputObject.OutputComponents.Add(CreatedSplineComponent); + + NewOutputObject.CurveOutputProperty.CurveOutputType = OutputCurveType; + NewOutputObject.CurveOutputProperty.NumPoints = CurvePointsCounts[n]; + + // TODO: Need a way to access info of the output curve + NewOutputObject.CurveOutputProperty.CurveMethod = EHoudiniCurveMethod::Breakpoints; + NewOutputObject.CurveOutputProperty.CurveType = bIsLinear ? EHoudiniCurveType::Polygon : EHoudiniCurveType::Bezier; + NewOutputObject.CurveOutputProperty.bClosed = false; + // Fill in the rest of output curve properties + + OutSplines.Add(CurveIdentifier, NewOutputObject); + + // Update FOundOutputObject so we can cache attributes after + FoundOutputObject = OutSplines.Find(CurveIdentifier); + } + else + { + // If this is not a new output object we have to clear the CachedAttributes and CachedTokens before + // setting the new values (so that we do not re-use any values from the previous cook) + FoundOutputObject->CachedAttributes.Empty(); + FoundOutputObject->CachedTokens.Empty(); + + // + if (FoundOutputObject->CurveOutputProperty.CurveOutputType == EHoudiniCurveOutputType::UnrealSpline) + { + // See if we can simply update the previous Spline Component + bool bCanUpdateUnrealSpline = (!FoundOutputObject->OutputComponents.IsEmpty() && FoundOutputObject->OutputComponents[0]->IsA()); + if (bCanUpdateUnrealSpline) + { + // Update the existing unreal spline component + bReusedPreviousOutput = true; + HOUDINI_LOG_WARNING( + TEXT("Updating Unreal Spline: Object [%d %s], Geo [%d], Part [%d %s], Curve# [%d], number of points [%d]."), + InHGPO.ObjectId, *InHGPO.ObjectName, InHGPO.GeoId, InHGPO.PartId, *InHGPO.PartName, CurveIdx, CurvePointsCounts[n]); + + USplineComponent* FoundUnrealSpline = FoundOutputObject->OutputComponents.Num() > 0 ? + Cast(FoundOutputObject->OutputComponents[0]) : nullptr; + if (!FHoudiniSplineTranslator::UpdateOutputUnrealSplineComponent( + FoundUnrealSpline, CurvesDisplayPoints[n], CurvesRotations[n], CurvesScales[n], + FoundOutputObject->CurveOutputProperty.CurveType, FoundOutputObject->CurveOutputProperty.bClosed)) + continue; + + FoundOutputObject = &OutSplines.Add(CurveIdentifier, *FoundOutputObject); + } + else + { + // Create a new Unreal spline component + // We support unreal spline only for now... + bReusedPreviousOutput = false; + FoundOutputObject->CurveOutputProperty.CurveOutputType = EHoudiniCurveOutputType::UnrealSpline; + HOUDINI_LOG_WARNING( + TEXT("Creating Unreal Spline: Object [%d %s], Geo [%d], Part [%d %s], Curve# [%d], number of points [%d]."), + InHGPO.ObjectId, *InHGPO.ObjectName, InHGPO.GeoId, InHGPO.PartId, *InHGPO.PartName, CurveIdx, CurvePointsCounts[n]); + + USplineComponent* NewUnrealSpline = FHoudiniSplineTranslator::CreateOutputUnrealSplineComponent( + InOuterComponent, CurvesDisplayPoints[n], CurvesRotations[n], CurvesScales[n], bIsLinear, bIsClosed); + + if (!NewUnrealSpline) + continue; + + check(FoundOutputObject->OutputComponents.Num() < 2); // Multiple components not supported yet. + FoundOutputObject->OutputComponents.Empty(); + FoundOutputObject->OutputComponents.Add(NewUnrealSpline); + + FoundOutputObject = &OutSplines.Add(CurveIdentifier, *FoundOutputObject); + } + } + // We current support Unreal Spline output only... + /* + else + { + // We want to output a Houdini Spline Component + // See if we can simply update the previous Houdini Spline Component + bool bCanUpdateHoudiniSpline = (FoundOutputObject->OutputComponent && FoundOutputObject->OutputComponent->IsA()); + if (bCanUpdateHoudiniSpline) + { + // Update the existing houdini spline component + bReusedPreviousOutput = true; + HOUDINI_LOG_WARNING( + TEXT("Changing Houdini Spline: Object [%d %s], Geo [%d], Part [%d %s], Curve# [%d], number of points [%d]."), + InHGPO.ObjectId, *InHGPO.ObjectName, InHGPO.GeoId, InHGPO.PartId, *InHGPO.PartName, CurveIdx, CurvePointsCounts[n]); + + UHoudiniSplineComponent* FoundHoudiniSpline = Cast(FoundOutputObject->OutputComponent); + if (!FHoudiniSplineTranslator::UpdateOutputHoudiniSplineComponent(CurvesDisplayPoints[n], FoundHoudiniSpline)) + continue; + + OutSplines.Add(CurveIdentifier, *FoundOutputObject); + } + else + { + // Create a new Houdini spline component + bReusedPreviousOutput = false; + FoundOutputObject->CurveOutputProperty.CurveOutputType = EHoudiniCurveOutputType::HoudiniSpline; + HOUDINI_LOG_WARNING( + TEXT("Creating Unreal Spline: Object [%d %s], Geo [%d], Part [%d %s], Curve# [%d], number of points [%d]."), + InHGPO.ObjectId, *InHGPO.ObjectName, InHGPO.GeoId, InHGPO.PartId, *InHGPO.PartName, CurveIdx, CurvePointsCounts[n]); + + UHoudiniSplineComponent* NewHoudiniSpline = CreateOutputHoudiniSplineComponent(CurvesDisplayPoints[n], CurvesRotations[n], CurvesScales[n], InOuter); + if (!NewHoudiniSpline) + continue; + + FoundOutputObject->OutputComponent = NewHoudiniSpline; + + OutSplines.Add(CurveIdentifier, *FoundOutputObject); + } + } + */ + } + + // Cache commonly supported Houdini attributes on the OutputAttributes + TArray LevelPaths; + if (FoundOutputObject && FHoudiniEngineUtils::GetLevelPathAttribute( + InHGPO.GeoId, InHGPO.PartId, LevelPaths, HAPI_ATTROWNER_INVALID, 0, 1)) + { + if (LevelPaths.Num() > 0 && !LevelPaths[0].IsEmpty()) + { + // cache the level path attribute on the output object + FoundOutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_LEVEL_PATH, LevelPaths[0]); + } + } + + TArray OutputNames; + if (FoundOutputObject && FHoudiniEngineUtils::GetOutputNameAttribute( + InHGPO.GeoId, InHGPO.PartId, OutputNames, 0, 1)) + { + if (OutputNames.Num() > 0 && !OutputNames[0].IsEmpty()) + { + // cache the output name attribute on the output object + FoundOutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2, OutputNames[0]); + } + } + + TArray BakeNames; + if (FoundOutputObject && FHoudiniEngineUtils::GetBakeNameAttribute( + InHGPO.GeoId, InHGPO.PartId, BakeNames, HAPI_ATTROWNER_INVALID, 0, 1)) + { + if (BakeNames.Num() > 0 && !BakeNames[0].IsEmpty()) + { + // cache the output name attribute on the output object + FoundOutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_NAME, BakeNames[0]); + } + } + + TArray BakeOutputActorNames; + if (FoundOutputObject && FHoudiniEngineUtils::GetBakeActorAttribute( + InHGPO.GeoId, InHGPO.PartId, BakeOutputActorNames, HAPI_ATTROWNER_INVALID, 0, 1)) + { + if (BakeOutputActorNames.Num() > 0 && !BakeOutputActorNames[0].IsEmpty()) + { + // cache the bake actor attribute on the output object + FoundOutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_ACTOR, BakeOutputActorNames[0]); + } + } + + TArray BakeOutputActorClassNames; + if (FoundOutputObject && FHoudiniEngineUtils::GetBakeActorClassAttribute( + InHGPO.GeoId, InHGPO.PartId, BakeOutputActorClassNames, HAPI_ATTROWNER_INVALID, 0, 1)) + { + if (BakeOutputActorClassNames.Num() > 0 && !BakeOutputActorClassNames[0].IsEmpty()) + { + // cache the bake actor attribute on the output object + FoundOutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_ACTOR_CLASS, BakeOutputActorClassNames[0]); + } + } + + TArray BakeFolders; + if (FoundOutputObject && FHoudiniEngineUtils::GetBakeFolderAttribute( + InHGPO.GeoId, BakeFolders, InHGPO.PartId, 0, 1)) + { + if (BakeFolders.Num() > 0 && !BakeFolders[0].IsEmpty()) + { + // cache the unreal_bake_folder attribute on the output object + FoundOutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_FOLDER, BakeFolders[0]); + } + } + + TArray BakeOutlinerFolders; + if (FoundOutputObject && FHoudiniEngineUtils::GetBakeOutlinerFolderAttribute( + InHGPO.GeoId, InHGPO.PartId, BakeOutlinerFolders, HAPI_ATTROWNER_INVALID, 0, 1)) + { + if (BakeOutlinerFolders.Num() > 0 && !BakeOutlinerFolders[0].IsEmpty()) + { + // cache the bake actor attribute on the output object + FoundOutputObject->CachedAttributes.Add(HAPI_UNREAL_ATTRIB_BAKE_OUTLINER_FOLDER, BakeOutlinerFolders[0]); + } + } + + // Update generic properties attributes on the spline component + TArray GenericAttributes; + if (FoundOutputObject && FHoudiniEngineUtils::GetGenericPropertiesAttributes( + InHGPO.GeoId, InHGPO.PartId, true, 0, 0, 0, GenericAttributes)) + { + for(auto Component : FoundOutputObject->OutputComponents) + { + FHoudiniEngineUtils::KeepOrClearComponentTags(Cast(Component), &InHGPO); + FHoudiniEngineUtils::UpdateGenericPropertiesAttributes(Component, GenericAttributes); + } + } + + if (bReusedPreviousOutput) + { + // Remove the reused output unreal spline from the old map to avoid its deletion + InSplines.Remove(CurveIdentifier); + } + + HOUDINI_LOG_WARNING( + TEXT("Finished Generating Unreal Spline: Object [%d %s], Geo [%d], Part [%d %s], Curve# [%d], number of points [%d]."), + InHGPO.ObjectId, *InHGPO.ObjectName, InHGPO.GeoId, InHGPO.PartId, *InHGPO.PartName, CurveIdx, CurvePointsCounts[n]); + } + + return true; +} + +bool +FHoudiniSplineTranslator::CreateAllSplinesFromHoudiniOutput(UHoudiniOutput* InOutput, UObject* InOuterComponent) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniSplineTranslator::CreateAllSplinesFromHoudiniOutput); + if (!IsValid(InOutput)) + return false; + + if (!IsValid(InOuterComponent)) + return false; + + // ONLY DO THIS ON CURVES!!!! + if (InOutput->GetType() != EHoudiniOutputType::Curve) + return false; + + TMap NewOutputObjects; + TMap& OldOutputObjects = InOutput->GetOutputObjects(); + + // Iterate on all the output's HGPO + for (const FHoudiniGeoPartObject & CurHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + // not a curve, skip + if (CurHGPO.Type != EHoudiniPartType::Curve) + continue; + + // Check if we want to create a houdini output curve from corresponding attribute + TArray IntData; + IntData.Empty(); + FHoudiniHapiAccessor Accessor(CurHGPO.GeoId, CurHGPO.PartId, HAPI_UNREAL_ATTRIB_OUTPUT_UNREAL_CURVE); + bool bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, 1, IntData, 0, 1); + if (!bSuccess || IntData.Num() <= 0 || IntData[0] == 0) + continue; + + IntData.Empty(); + bool bIsLinear = false; + Accessor.Init(CurHGPO.GeoId, CurHGPO.PartId, HAPI_UNREAL_ATTRIB_OUTPUT_UNREAL_CURVE_LINEAR); + if (Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, 1, IntData, 0, 1)) + { + if (IntData.Num() > 0) + bIsLinear = IntData[0] != 0; + } + + IntData.Empty(); + Accessor.Init(CurHGPO.GeoId, CurHGPO.PartId, HAPI_UNREAL_ATTRIB_OUTPUT_UNREAL_CURVE_CLOSED); + bSuccess = Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, 1, IntData, 0, 1 ); + bool bIsClosed = false; + if (IntData.Num() > 0) + bIsClosed = IntData[0] != 0; + + // We output curve to Unreal Spline only for now + // May support output to Houdini Spline later + CreateOutputSplinesFromHoudiniGeoPartObject(CurHGPO, InOuterComponent, OldOutputObjects, NewOutputObjects, false, EHoudiniCurveOutputType::UnrealSpline, bIsLinear, bIsClosed); + } + + // TODO: FIX ME!!! This literally nukes all the output objects, even if they are not curves! + + // The old map now only contains unused/stale output curves destroy them + for (auto& OldPair : OldOutputObjects) + { + for(auto Component : OldPair.Value.OutputComponents) + { + USceneComponent* OldSplineSceneComponent = Cast(Component); + + if (!IsValid(OldSplineSceneComponent)) + continue; + + // The output object is supposed to be a spline + if (!OldSplineSceneComponent->IsA() && !OldSplineSceneComponent->IsA()) + continue; + + OldSplineSceneComponent->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + OldSplineSceneComponent->UnregisterComponent(); + OldSplineSceneComponent->DestroyComponent(); + } + OldPair.Value.OutputComponents.Empty(); + } + OldOutputObjects.Empty(); + + InOutput->SetOutputObjects(NewOutputObjects); + + // TODO: remove and delay editor propreties update + FHoudiniEngineUtils::UpdateEditorProperties(true); + + return true; +} + +void +FHoudiniSplineTranslator::ReselectSelectedActors() +{ + // TODO: Duplicate with FHoudiniEngineUtils::UpdateEditorProperties ?? + USelection* Selection = GEditor->GetSelectedActors(); + TArray SelectedActors; + SelectedActors.SetNumUninitialized(GEditor->GetSelectedActorCount()); + Selection->GetSelectedObjects(SelectedActors); + + GEditor->SelectNone(false, false, false); + + for (AActor* NextSelected : SelectedActors) + { + GEditor->SelectActor(NextSelected, true, true, true, true); + } +} + +``` + +`Source/HoudiniEngine/Private/HoudiniSplineTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "HoudiniGeoPartObject.h" + +#include "CoreMinimal.h" + +class UHoudiniInput; +class UHoudiniOutput; +class UHoudiniCookable; +class UHoudiniSplineComponent; +class USceneComponent; +class USplineComponent; + +struct FHoudiniGeoPartObject; +struct FHoudiniOutputObjectIdentifier; +struct FHoudiniOutputObject; + +enum class EHoudiniCurveOutputType : uint8; + +struct HOUDINIENGINE_API FHoudiniSplineTranslator +{ + // Get the cooked Houdini curve. + static bool UpdateHoudiniCurve(UHoudiniSplineComponent * HoudiniSplineComponent); + + // Get the cooked Houdini curve. + static bool UpdateHoudiniCurveLegacy(UHoudiniSplineComponent * HoudiniSplineComponent); + + // Get all cooked Houdini curves of an input. + static void UpdateHoudiniInputCurves(UHoudiniInput* Input); + + // Upload Houdini spline component data to the curve node, and then sync the Houdini Spline Component with the curve node. + static bool HapiUpdateNodeForHoudiniSplineComponent( + UHoudiniSplineComponent* HoudiniSplineComponent, bool bInSetRotAndScaleAttributes); + + // Indicates if the node is a valid curve input node + static bool IsCurveInputNodeValid(const HAPI_NodeId& InNodeId, const bool& bLegacyNode = false); + + // Update the curve node data, or create a new curve node if the CurveNodeId is valid. + static bool HapiCreateCurveInputNodeForData( + HAPI_NodeId& CurveNodeId, + const HAPI_NodeId& ParentNodeId, + const FString& InputNodeName, + TArray* Positions, + TArray* Rotations, + TArray* Scales3d, + EHoudiniCurveType InCurveType, + EHoudiniCurveMethod InCurveMethod, + const bool& InClosed, + const bool& InReversed, + const bool& InForceClose = false, + const FTransform& ParentTransform = FTransform::Identity, + const bool & InIsLegacyCurve = false, + // Only used if Legacy curve: + const int32& InOrder = 2, + const EHoudiniCurveBreakpointParameterization& InBreakpointParameterization = EHoudiniCurveBreakpointParameterization::Uniform + ); + + // Update curve node data using curve::1.0 + // Update the curve node data, or create a new curve node if the CurveNodeId is valid. + static bool HapiCreateCurveInputNodeForDataLegacy( + HAPI_NodeId& CurveNodeId, + const HAPI_NodeId& ParentNodeId, + const FString& InputNodeName, + TArray* Positions, + TArray* Rotations, + TArray* Scales3d, + const EHoudiniCurveType& InCurveType, + const EHoudiniCurveMethod& InCurveMethod, + const bool& InClosed, + const bool& InReversed, + const bool& InForceClose = false, + const FTransform& ParentTransform = FTransform::Identity); + + + // Create a default curve node. + static bool HapiCreateCurveInputNode( + HAPI_NodeId& OutCurveNodeId, + const HAPI_NodeId& ParentNodeId, + const FString& InputNodeName, + const bool& InIsLegacyCurve); + + // Create a Houdini spline component from a given editable node. (Only called once when first build the editable node.) + static UHoudiniSplineComponent* CreateHoudiniSplineComponentFromHoudiniEditableNode( + const int32 & GeoId, const FString & PartName, UObject* OuterComponent); + + // Helper functions. + static void ExtractStringPositions(const FString& Positions, TArray& OutPositions); + + static void ConvertPositionToVectorData( + const TArray& InRawData, TArray>& OutVectorData, const TArray& CurveCounts); + static void ConvertScaleToVectorData( + const TArray& InRawData, TArray>& OutVectorData, const TArray& CurveCounts); + static void ConvertEulerRotationToVectorData( + const TArray& InRawData, TArray>& OutVectorData, const TArray& CurveCounts); + static void ConvertQuaternionRotationToVectorData( + const TArray& InRawData, TArray>& OutVectorData, const TArray& CurveCounts); + + static void CreatePositionsString(const TArray& InPositions, FString& OutPositionString); + + static bool CreateOutputSplinesFromHoudiniGeoPartObject( + const FHoudiniGeoPartObject& InHGPO, + UObject* InOuterComponent, + TMap& InSplines, + TMap& OutSplines, + const bool& InForceRebuild, + const EHoudiniCurveOutputType& OutputCurveType, + const bool& bIsLinear, + const bool& bIsClosed); + + static bool CreateAllSplinesFromHoudiniOutput( + UHoudiniOutput* InOutput, + UObject* InOuterComponent); + + static USplineComponent* CreateOutputUnrealSplineComponent( + UObject* OuterComponent, + const TArray& CurvePoints, + const TArray& CurveRotations, + const TArray& CurveScales, + const bool& bIsLinear, + const bool& bIsClosed); + + static UHoudiniSplineComponent* CreateOutputHoudiniSplineComponent( + UHoudiniCookable* OuterHC, + TArray& CurvePoints, + const TArray& CurveRotations, + const TArray& CurveScales); + + static bool UpdateOutputUnrealSplineComponent( + USplineComponent* EditedSplineComponent, + const TArray& CurvePoints, + const TArray& CurveRotations, + const TArray& CurveScales, + const EHoudiniCurveType& CurveType, + const bool& bClosed); + + static bool UpdateOutputHoudiniSplineComponent( + const TArray& CurvePoints, + UHoudiniSplineComponent* EditedHoudiniSplineComponent); + + static void ReselectSelectedActors(); +}; +``` + +`Source/HoudiniEngine/Private/HoudiniStringResolver.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniStringResolver.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniEngineRuntime.h" + +#include "HoudiniEnginePrivatePCH.h" +#include "HAL/FileManager.h" + +void FHoudiniStringResolver::GetTokensAsStringMap(TMap& OutTokens) const +{ + for (auto& Elem : CachedTokens) + { + OutTokens.Add(Elem.Key, Elem.Value.StringValue); + } +} + +FString FHoudiniStringResolver::SanitizeTokenValue(const FString& InValue) +{ + // Replace {} characters with __ + FString OutString = InValue; + OutString.ReplaceInline(ANSI_TO_TCHAR("{"), ANSI_TO_TCHAR("__")); + OutString.ReplaceInline(ANSI_TO_TCHAR("}"), ANSI_TO_TCHAR("__")); + + return OutString; +} + +void FHoudiniStringResolver::SetToken(const FString& InName, const FString& InValue) +{ + CachedTokens.Add(InName, SanitizeTokenValue(InValue)); +} + +void FHoudiniStringResolver::SetTokensFromStringMap(const TMap& InTokens, bool bClearTokens) +{ + if (bClearTokens) + { + CachedTokens.Empty(); + } + + for (auto& Elem : InTokens) + { + CachedTokens.Add(Elem.Key, SanitizeTokenValue(Elem.Value)); + } +} + + + +FString FHoudiniStringResolver::ResolveString( + const FString& InString) const +{ + FString Result = FString::Format(*InString, CachedTokens); + return Result; +} + +//void FHoudiniStringResolver::SetCurrentWorld(UWorld* InWorld) +//{ +// SetAttribute("world", InWorld->GetPathName()); +//} + +FString FHoudiniAttributeResolver::ResolveAttribute( + const FString& InAttrName, + const FString& InDefaultValue, + const bool bInUseDefaultIfAttrValueEmpty, + bool* const bOutUsedDefaultValue) const +{ + if (!CachedAttributes.Contains(InAttrName)) + { + if (bOutUsedDefaultValue) + *bOutUsedDefaultValue = true; + return ResolveString(InDefaultValue); + } + const FString AttrStr = CachedAttributes.FindChecked(InAttrName); + const FString AttrValue = ResolveString(AttrStr); + + if (bInUseDefaultIfAttrValueEmpty && AttrValue.IsEmpty()) + { + if (bOutUsedDefaultValue) + *bOutUsedDefaultValue = true; + return ResolveString(InDefaultValue); + } + + if (bOutUsedDefaultValue) + *bOutUsedDefaultValue = false; + + return AttrValue; +} + +//FString FHoudiniStringResolver::GetTempFolderArgument() const +//{ +// // The actual temp directory should have been supplied externally +// if (Tokens.Contains(TEXT("temp"))) +// return Tokens.FindChecked(TEXT("temp")); +// +// UE_LOG(LogTemp, Warning, TEXT("[GetBakeFolderArgument] Could not find 'temp' argument. Using fallback value.")); +// return TEXT("/Game/Content/HoudiniEngine/Temp"); // Fallback value +//} +// +//FString FHoudiniStringResolver::GetBakeFolderArgument() const +//{ +// // The actual bake directory should have been supplied externally +// if (Tokens.Contains(TEXT("bake"))) +// return Tokens.FindChecked(TEXT("bake")); +// UE_LOG(LogTemp, Warning, TEXT("[GetBakeFolderArgument] Could not find 'bake' argument. Using fallback value.")); +// return TEXT("/Game/Content/HoudiniEngine/Bake"); // Fallback value +//} +// +//FString FHoudiniStringResolver::GetOutputFolderForPackageMode(EPackageMode PackageMode) const +//{ +// switch (PackageMode) +// { +// case EPackageMode::Bake: +// return GetBakeFolderArgument(); +// case EPackageMode::CookToLevel_Invalid: +// case EPackageMode::CookToTemp: +// return GetTempFolderArgument(); +// } +// return ""; +//} + + void FHoudiniAttributeResolver::SetCachedAttributes(const TMap& Attributes) + { + CachedAttributes = Attributes; + } + +void FHoudiniAttributeResolver::SetAttribute(const FString& InName, const FString& InValue) +{ + CachedAttributes.Add(InName, InValue); +} + +FString FHoudiniAttributeResolver::ResolveFullLevelPath() const +{ + FString OutputFolder = TEXT("/Game/Content/HoudiniEngine/Temp"); + + const FStringFormatArg* BaseDir = CachedTokens.Find(TEXT("out_basedir")); + if (BaseDir) + OutputFolder = BaseDir->StringValue; + + FString LevelPathAttr = ResolveAttribute(HAPI_UNREAL_ATTRIB_LEVEL_PATH, TEXT("{out}")); + if (LevelPathAttr.IsEmpty()) + return OutputFolder; + + return FHoudiniEngineRuntimeUtils::JoinPaths(OutputFolder, LevelPathAttr); +} + +FString FHoudiniAttributeResolver::ResolveOutputName(const bool bInForBake, bool* const bOutUsedDefaultValue) const +{ + FString OutputAttribName; + + if (CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2)) + { + OutputAttribName = HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V2; + } + else if (bInForBake && CachedAttributes.Contains(HAPI_UNREAL_ATTRIB_BAKE_NAME)) + { + OutputAttribName = HAPI_UNREAL_ATTRIB_BAKE_NAME; + } + else + { + OutputAttribName = HAPI_UNREAL_ATTRIB_CUSTOM_OUTPUT_NAME_V1; + } + + // When baking add the component guid to the default value for object_name + const FString DefaultValue = bInForBake ? TEXT("{object_name}_{guid8}") : TEXT("{object_name}"); + static constexpr bool bUseDefaultIfAttrValueEmpty = false; + return ResolveAttribute(OutputAttribName, DefaultValue, bUseDefaultIfAttrValueEmpty, bOutUsedDefaultValue); +} + +FString FHoudiniAttributeResolver::ResolveBakeFolder() const +{ + const FString DefaultBakeFolder = FHoudiniEngineRuntime::Get().GetDefaultBakeFolder(); + + constexpr bool bUseDefaultIfAttrValueEmpty = true; + FString BakeFolder = ResolveAttribute(HAPI_UNREAL_ATTRIB_BAKE_FOLDER, TEXT("{bake}"), bUseDefaultIfAttrValueEmpty); + if (BakeFolder.IsEmpty()) + BakeFolder = DefaultBakeFolder; + + if (!BakeFolder.StartsWith(TEXT("/"))) + { + HOUDINI_LOG_WARNING(TEXT("Bake folder %s does not start with '/'. Adding '/' to avoid an Unreal crash."), *BakeFolder); + BakeFolder = TEXT("/") + BakeFolder; + } + + //FString AbsoluteOverridePath; + //if (BakeFolder.StartsWith("/Game/")) + //{ + // const FString RelativePath = FPaths::ProjectContentDir() + BakeFolder.Mid(6, BakeFolder.Len() - 6); + // AbsoluteOverridePath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*RelativePath); + //} + //else + //{ + // if (!BakeFolder.IsEmpty()) + // AbsoluteOverridePath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*BakeFolder); + //} + + //// Check Validity of the path + //if (AbsoluteOverridePath.IsEmpty()) + //{ + // return DefaultBakeFolder; + //} + + return BakeFolder; +} + +FString FHoudiniAttributeResolver::ResolveTempFolder() const +{ + const FString DefaultTempFolder = FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder(); + + constexpr bool bUseDefaultIfAttrValueEmpty = true; + const FString TempFolder = ResolveAttribute( + HAPI_UNREAL_ATTRIB_TEMP_FOLDER, TEXT("{temp}"), bUseDefaultIfAttrValueEmpty); + if (TempFolder.IsEmpty()) + return DefaultTempFolder; + + return TempFolder; +} + +void FHoudiniAttributeResolver::LogCachedAttributesAndTokens() const +{ + TArray Lines; + Lines.Add(TEXT("==================")); + Lines.Add(TEXT("Cached Attributes:")); + Lines.Add(TEXT("==================")); + for (const auto& Entry : CachedAttributes) + { + Lines.Add(FString::Printf(TEXT("%s: %s"), *(Entry.Key), *(Entry.Value))); + } + + Lines.Add(TEXT("==============")); + Lines.Add(TEXT("Cached Tokens:")); + Lines.Add(TEXT("==============")); + for (const auto& Entry : CachedTokens) + { + Lines.Add(FString::Printf(TEXT("%s: %s"), *(Entry.Key), *(Entry.Value.StringValue))); + } + + HOUDINI_LOG_DISPLAY(TEXT("%s"), *FString::Join(Lines, TEXT("\n"))); +} + + +``` + +`Source/HoudiniEngine/Private/HoudiniStringResolver.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#include "HoudiniStringResolver.generated.h" + +USTRUCT() +struct HOUDINIENGINE_API FHoudiniStringResolver +{ + + GENERATED_USTRUCT_BODY(); + +protected: + + // Named arguments that will be substituted into attribute values upon retrieval. + TMap CachedTokens; + + +public: + // ---------------------------------- + // Named argument accessors + // ---------------------------------- + + TMap& GetCachedTokens() { return CachedTokens; } + + + // Set a named argument that will be used for argument replacement during GetAttribute calls. + void SetToken(const FString& InName, const FString& InValue); + + // Sanitize a token value. Currently only replaces { and } with __. + static FString SanitizeTokenValue(const FString& InValue); + + void GetTokensAsStringMap(TMap& OutTokens) const; + + void SetTokensFromStringMap(const TMap& InValue, bool bClearTokens=true); + + // Resolve a string by substituting `Tokens` as named arguments during string formatting. + FString ResolveString(const FString& InStr) const; + +}; + + +USTRUCT() +struct HOUDINIENGINE_API FHoudiniAttributeResolver : public FHoudiniStringResolver +{ + GENERATED_USTRUCT_BODY(); + +protected: + TMap CachedAttributes; + +public: + + void SetCachedAttributes(const TMap& Attributes); + + // Return a mutable reference to the cached attributes. + TMap& GetCachedAttributes() { return CachedAttributes; } + + // Return immutable reference to cached attributes. + const TMap& GetCachedAttributes() const { return CachedAttributes; } + + // Set an attribute with the given name and value in the attribute cache. + void SetAttribute(const FString& InName, const FString& InValue); + + // Try to resolve an attribute with the given name. If the attribute could not be + // found, use DefaultValue as a fallback. + FString ResolveAttribute( + const FString& InAttrName, + const FString& InDefaultValue, + const bool bInUseDefaultIfAttrValueEmpty=false, + bool* bOutUsedDefaultValue=nullptr) const; + + // ---------------------------------- + // Helpers + // ---------------------------------- + + // Helper for resolving the `unreal_level_path` attribute. + FString ResolveFullLevelPath() const; + + // Helper for resolver custom output name attributes. + FString ResolveOutputName(bool bInForBake=false, bool* bOutUsedDefaultValue=nullptr) const; + + // Helper for resolving the unreal_bake_folder attribute. Converts to an absolute path. + FString ResolveBakeFolder() const; + + // Helper for resolving the unreal_temp_folder attribute. + FString ResolveTempFolder() const; + + // ---------------------------------- + // Debug + // ---------------------------------- + // Logs the resolver's cached attributes and tokens + void LogCachedAttributesAndTokens() const; +}; +``` + +`Source/HoudiniEngine/Private/HoudiniTextureTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniTextureTranslator.h" + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniOutput.h" +#include "HoudiniMaterialTranslator.h" + +#include "AssetRegistry/AssetRegistryModule.h" // FAssetRegistryModule +#if WITH_EDITOR +#include "Factories/MaterialFactoryNew.h" +#include "Factories/MaterialInstanceConstantFactoryNew.h" +#endif +#include "ImageUtils.h" // FCreateTexture2DParameters +#include "Materials/Material.h" +#include "Materials/MaterialInstance.h" +#include "Materials/MaterialInstanceConstant.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 +#include "MaterialShared.h" +#endif +#include "PackageTools.h" +bool +FHoudiniTextureTranslator::HapiGetImagePlanes( + const HAPI_ParmId InNodeParmId, const HAPI_MaterialInfo& InMaterialInfo, TArray& OutImagePlanes) +{ + OutImagePlanes.Empty(); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::RenderTextureToImage( + FHoudiniEngine::Get().GetSession(), + InMaterialInfo.nodeId, InNodeParmId), false); + + int32 ImagePlaneCount = 0; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetImagePlaneCount( + FHoudiniEngine::Get().GetSession(), + InMaterialInfo.nodeId, &ImagePlaneCount), false); + + if (ImagePlaneCount <= 0) + return true; + + TArray ImagePlaneStringHandles; + ImagePlaneStringHandles.SetNumZeroed(ImagePlaneCount); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetImagePlanes( + FHoudiniEngine::Get().GetSession(), + InMaterialInfo.nodeId, &ImagePlaneStringHandles[0], ImagePlaneCount), false); + + FHoudiniEngineString::SHArrayToFStringArray(ImagePlaneStringHandles, OutImagePlanes); + + return true; +} + +bool +FHoudiniTextureTranslator::GetPlaneInfo( + const HAPI_ParmId InParmTextureId, + const HAPI_MaterialInfo& InMaterialInfo, + HAPI_ImagePacking& OutImagePacking, + const char*& OutPlaneType, + bool& bOutUseAlpha) +{ + TArray ImagePlanes; + bool bGetImagePlanesSuccess = FHoudiniTextureTranslator::HapiGetImagePlanes( + InParmTextureId, InMaterialInfo, ImagePlanes); + + if (bGetImagePlanesSuccess && ImagePlanes.Contains(TEXT(HAPI_UNREAL_MATERIAL_TEXTURE_COLOR))) + { + // We use RGBA packing even if the image planes do not contain the alpha plane. + // This is because all texture data from Houdini Engine contains the alpha plane by default. + OutImagePacking = HAPI_IMAGE_PACKING_RGBA; + OutPlaneType = HAPI_UNREAL_MATERIAL_TEXTURE_COLOR_ALPHA; + + bOutUseAlpha = ImagePlanes.Contains(TEXT(HAPI_UNREAL_MATERIAL_TEXTURE_ALPHA)); + + return true; + } + else + { + OutImagePacking = HAPI_IMAGE_PACKING_UNKNOWN; + OutPlaneType = ""; + + return false; + } +} + +bool +FHoudiniTextureTranslator::HapiRenderTexture( + const HAPI_NodeId InMaterialNodeId, + const HAPI_ParmId InTextureParmId) +{ + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::RenderTextureToImage( + FHoudiniEngine::Get().GetSession(), + InMaterialNodeId, InTextureParmId), false); + return true; +} + +bool +FHoudiniTextureTranslator::HapiRenderCOPTexture( + const HAPI_NodeId InCopNodeId) +{ + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::RenderCOPToImage( + FHoudiniEngine::Get().GetSession(), + InCopNodeId), false); + return true; +} + +bool +FHoudiniTextureTranslator::HapiExtractImage( + const HAPI_NodeId InMaterialNodeId, + const char* InPlaneType, + const HAPI_ImageDataFormat InImageDataFormat, + const HAPI_ImagePacking InImagePacking, + TArray& OutImageBuffer) +{ + // See if we have the images planes we want + int NumImagePlanes = 0; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetImagePlaneCount( + FHoudiniEngine::Get().GetSession(), InMaterialNodeId, &NumImagePlanes), false); + + TArray ImagePlanesSHArray; + ImagePlanesSHArray.SetNum(NumImagePlanes); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetImagePlanes( + FHoudiniEngine::Get().GetSession(), InMaterialNodeId, ImagePlanesSHArray.GetData(), NumImagePlanes), false); + + TArray ImagePlanesStringArray; + FHoudiniEngineString::SHArrayToFStringArray(ImagePlanesSHArray, ImagePlanesStringArray); + + bool bFound = false; + bool bCFound = false; + bool bAFound = false; + FString InPlaneTypeString(InPlaneType); + for (int32 n = 0; n < ImagePlanesStringArray.Num(); n++) + { + if (ImagePlanesStringArray[n].Equals(InPlaneTypeString, ESearchCase::IgnoreCase)) + bFound = true; + else if (InPlaneTypeString.Equals("C A")) + { + if (ImagePlanesStringArray[n].Equals("C")) + { + bCFound = true; + // If only color is found, still allow image extraction + bFound = true; + } + else if (ImagePlanesStringArray[n].Equals("A")) + { + bAFound = true; + } + + if (bCFound && bAFound) + bFound = true; + } + } + + if (!bFound) + return false; + + HAPI_ImageInfo ImageInfo; + FHoudiniApi::ImageInfo_Init(&ImageInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetImageInfo( + FHoudiniEngine::Get().GetSession(), + InMaterialNodeId, &ImageInfo), false); + + ImageInfo.dataFormat = InImageDataFormat; + ImageInfo.interleaved = true; + ImageInfo.packing = InImagePacking; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetImageInfo( + FHoudiniEngine::Get().GetSession(), + InMaterialNodeId, &ImageInfo), false); + + int32 ImageBufferSize = 0; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ExtractImageToMemory( + FHoudiniEngine::Get().GetSession(), + InMaterialNodeId, HAPI_RAW_FORMAT_NAME, + InPlaneType, &ImageBufferSize), false); + + if (ImageBufferSize <= 0) + return false; + + OutImageBuffer.SetNumUninitialized(ImageBufferSize); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetImageMemoryBuffer( + FHoudiniEngine::Get().GetSession(), + InMaterialNodeId, &OutImageBuffer[0], + ImageBufferSize), false); + + return true; +} + +UPackage* +FHoudiniTextureTranslator::CreatePackageForTexture( + const HAPI_NodeId& InMaterialNodeId, + const FString& InTextureType, + const FHoudiniPackageParams& InPackageParams, + FString& OutTextureName) +{ + FString TextureInfoDescriptor = TEXT("_texture_") + FString::FromInt(InMaterialNodeId); + if (!InTextureType.Equals("")) + TextureInfoDescriptor += TEXT("_") + InTextureType; + + FHoudiniPackageParams MyPackageParams = InPackageParams; + if (!MyPackageParams.ObjectName.IsEmpty()) + { + MyPackageParams.ObjectName += TextureInfoDescriptor; + } + else if (!MyPackageParams.HoudiniAssetName.IsEmpty()) + { + MyPackageParams.ObjectName = MyPackageParams.HoudiniAssetName + TextureInfoDescriptor; + } + else + { + MyPackageParams.ObjectName = TextureInfoDescriptor; + } + + // If we have a part name (output name) be sure to it + if(!InPackageParams.SplitStr.IsEmpty()) + MyPackageParams.ObjectName += InPackageParams.SplitStr; + + return MyPackageParams.CreatePackageForObject(OutTextureName); +} + +bool +FHoudiniTextureTranslator::CreateTexture( + // HAPI extraction parameters + const HAPI_NodeId InMaterialNodeId, + const char* InPlaneType, + HAPI_ImageDataFormat InImageDataFormat, + HAPI_ImagePacking InImagePacking, + // Texture creation parameters + UTexture2D*& OutTexture, + const FString& InNodePath, + const FString& InTextureType, + const FHoudiniPackageParams& InPackageParams, + const FCreateTexture2DParameters& InTextureParameters, + const TextureGroup InLODGroup, + TArray& OutPackages) +{ + bool bTextureCreated = false; + TArray ImageBuffer; + if (FHoudiniTextureTranslator::HapiExtractImage( + InMaterialNodeId, InPlaneType, InImageDataFormat, InImagePacking, ImageBuffer)) + { + UPackage* TexturePackage = nullptr; + if (IsValid(OutTexture)) + TexturePackage = Cast(OutTexture->GetOuter()); + + HAPI_ImageInfo ImageInfo; + FHoudiniApi::ImageInfo_Init(&ImageInfo); + HAPI_Result Result = FHoudiniApi::GetImageInfo( + FHoudiniEngine::Get().GetSession(), + InMaterialNodeId, &ImageInfo); + + if (Result == HAPI_RESULT_SUCCESS && ImageInfo.xRes > 0 && ImageInfo.yRes > 0) + { + // Create texture package, if this is a new texture. + FString TextureName; + if (!TexturePackage) + { + TexturePackage = FHoudiniTextureTranslator::CreatePackageForTexture( + InMaterialNodeId, InTextureType, InPackageParams, TextureName); + } + else if (IsValid(OutTexture)) + { + // Get the name of the texture if we are overwriting the exist asset + TextureName = OutTexture->GetName(); + } + else + { + TextureName = FPaths::GetBaseFilename(TexturePackage->GetName(), true); + } + + bool bCreatedNewTexture = !IsValid(OutTexture); + + // Reuse existing texture, or create new one. + OutTexture = FHoudiniTextureTranslator::CreateUnrealTexture( + OutTexture, + ImageInfo, + TexturePackage, + TextureName, + ImageBuffer, + InTextureParameters, + InLODGroup, + InTextureType, + InNodePath); + + //if (BakeMode == EBakeMode::CookToTemp) + OutTexture->SetFlags(RF_Public | RF_Standalone); + + // Propagate and trigger texture updates. + if (bCreatedNewTexture) + FAssetRegistryModule::AssetCreated(OutTexture); + + OutTexture->PreEditChange(nullptr); + OutTexture->PostEditChange(); + OutTexture->MarkPackageDirty(); + + bTextureCreated = true; + } + + // Cache the texture package + OutPackages.AddUnique(TexturePackage); + } + + return bTextureCreated; +} + +UTexture2D* +FHoudiniTextureTranslator::CreateUnrealTexture( + UTexture2D* ExistingTexture, + const HAPI_ImageInfo& ImageInfo, + UPackage* Package, + const FString& TextureName, + const TArray& ImageBuffer, + const FCreateTexture2DParameters& TextureParameters, + const TextureGroup LODGroup, + const FString& TextureType, + const FString& NodePath) +{ + if (!IsValid(Package)) + return nullptr; + + UTexture2D* Texture = nullptr; + if (ExistingTexture) + { + Texture = ExistingTexture; + } + else + { + // Create new texture object. + Texture = NewObject( + Package, UTexture2D::StaticClass(), *TextureName, + RF_Transactional); + + // Assign texture group. + Texture->LODGroup = LODGroup; + } + + // Add/Update meta information to package. + FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + Package, Texture, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + Package, Texture, HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *TextureName); + if (!TextureType.Equals("")) + FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + Package, Texture, HAPI_UNREAL_PACKAGE_META_GENERATED_TEXTURE_TYPE, *TextureType); + if (!NodePath.Equals("")) + FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + Package, Texture, HAPI_UNREAL_PACKAGE_META_NODE_PATH, *NodePath); + + // Initialize texture source. + Texture->Source.Init(ImageInfo.xRes, ImageInfo.yRes, 1, 1, TSF_BGRA8); + + // Lock the texture. + uint8* MipData = Texture->Source.LockMip(0); + + // Create base map. + uint8* DestPtr = nullptr; + uint32 SrcWidth = ImageInfo.xRes; + uint32 SrcHeight = ImageInfo.yRes; + const char* SrcData = &ImageBuffer[0]; + + // Handle the different packing for the source Houdini texture + uint32 PackOffset = 4; + uint32 OffsetR = 0; + uint32 OffsetG = 1; + uint32 OffsetB = 2; + uint32 OffsetA = 3; + switch (ImageInfo.packing) + { + case HAPI_IMAGE_PACKING_SINGLE: + PackOffset = 1; + OffsetR = 0; + OffsetG = 0; + OffsetB = 0; + OffsetA = 0; + break; + + case HAPI_IMAGE_PACKING_DUAL: + PackOffset = 2; + OffsetR = 0; + OffsetG = 1; + OffsetB = 1; + OffsetA = 0; + break; + + case HAPI_IMAGE_PACKING_RGB: + PackOffset = 3; + OffsetR = 0; + OffsetG = 1; + OffsetB = 2; + OffsetA = 0; + break; + + case HAPI_IMAGE_PACKING_BGR: + PackOffset = 3; + OffsetR = 2; + OffsetG = 1; + OffsetB = 0; + OffsetA = 0; + break; + + case HAPI_IMAGE_PACKING_RGBA: + PackOffset = 4; + OffsetR = 0; + OffsetG = 1; + OffsetB = 2; + OffsetA = 3; + break; + + case HAPI_IMAGE_PACKING_ABGR: + PackOffset = 4; + OffsetR = 3; + OffsetG = 2; + OffsetB = 1; + OffsetA = 0; + break; + + case HAPI_IMAGE_PACKING_UNKNOWN: + case HAPI_IMAGE_PACKING_MAX: + // invalid packing + HOUDINI_CHECK_RETURN(false, nullptr); + break; + } + + bool bHasAlphaValue = false; + for (uint32 y = 0; y < SrcHeight; y++) + { + DestPtr = &MipData[(SrcHeight - 1 - y) * SrcWidth * sizeof(FColor)]; + + for (uint32 x = 0; x < SrcWidth; x++) + { + uint32 DataOffset = y * SrcWidth * PackOffset + x * PackOffset; + + *DestPtr++ = *(uint8*)(SrcData + DataOffset + OffsetB); // B + *DestPtr++ = *(uint8*)(SrcData + DataOffset + OffsetG); // G + *DestPtr++ = *(uint8*)(SrcData + DataOffset + OffsetR); // R + + if (TextureParameters.bUseAlpha && PackOffset == 4) + { + *DestPtr++ = *(uint8*)(SrcData + DataOffset + OffsetA); // A + if (*(uint8*)(SrcData + DataOffset + OffsetA) != 0xFF) + bHasAlphaValue = true; + } + else + *DestPtr++ = 0xFF; + } + } + + // Unlock the texture. + Texture->Source.UnlockMip(0); + + // Texture creation parameters. + Texture->SRGB = TextureParameters.bSRGB; + Texture->CompressionSettings = TextureParameters.CompressionSettings; + Texture->CompressionNoAlpha = !bHasAlphaValue; + Texture->DeferCompression = TextureParameters.bDeferCompression; + + // Set the Source Guid/Hash if specified. + /* + if (TextureParameters.SourceGuidHash.IsValid()) + { + Texture->Source.SetId(TextureParameters.SourceGuidHash, true); + } + */ + + Texture->PostEditChange(); + + return Texture; +} + +bool +FHoudiniTextureTranslator::ProcessCopOutput( + UHoudiniOutput* InOutput, + const FHoudiniPackageParams& InPackageParams) +{ + if (!InOutput) + return false; + + if (InOutput->GetType() != EHoudiniOutputType::Cop) + return false; + + // TODO: Delete previous output? + TArray DummyPackages; + + FString Notification = TEXT("BGEO Importer: Creating Cop Textures..."); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Notification)); + + const TArray& GeoPartObjects = InOutput->GetHoudiniGeoPartObjects(); + if (GeoPartObjects.Num() <= 0) + return false; + + for (auto& HGPO : GeoPartObjects) + { + HAPI_NodeId CopNodeId = HGPO.GeoId; + + HAPI_Result Result; + HOUDINI_CHECK_ERROR_GET(&Result,FHoudiniApi::RenderCOPOutputToImage( + FHoudiniEngine::Get().GetSession(), + CopNodeId, + TCHAR_TO_ANSI(*HGPO.PartName))); + + if (HAPI_RESULT_SUCCESS != Result) + continue; + + FCreateTexture2DParameters CreateTexture2DParameters; + CreateTexture2DParameters.SourceGuidHash = FGuid(); + CreateTexture2DParameters.bUseAlpha = true; + CreateTexture2DParameters.CompressionSettings = TC_Default; + CreateTexture2DParameters.bDeferCompression = true; + CreateTexture2DParameters.bSRGB = true; + + // Create custom package param for this output + FHoudiniPackageParams MyPackageParams = InPackageParams; + MyPackageParams.ObjectId = HGPO.ObjectId; + MyPackageParams.GeoId = HGPO.GeoId; + MyPackageParams.PartId = HGPO.PartId; + MyPackageParams.SplitStr = HGPO.PartName; + + UTexture2D* Texture = nullptr; + FHoudiniTextureTranslator::CreateTexture( + CopNodeId, + HAPI_UNREAL_MATERIAL_TEXTURE_COLOR_ALPHA, + HAPI_IMAGE_DATA_INT8, + HAPI_IMAGE_PACKING_RGBA, + Texture, + "", + "", + MyPackageParams, + CreateTexture2DParameters, + TEXTUREGROUP_World, + DummyPackages); + + FHoudiniOutputObjectIdentifier OutputID(HGPO.ObjectId, CopNodeId, HGPO.PartId, HGPO.PartName); + FHoudiniOutputObject& FoundOutputObject = InOutput->GetOutputObjects().FindOrAdd(OutputID); + FoundOutputObject.OutputComponents.Empty(); + FoundOutputObject.OutputObject = Texture; + } + + return true; +} + +UMaterialInterface* +FHoudiniTextureTranslator::CreateDefaultCopMaterialForTexture( + UTexture2D* InTexture, + const FHoudiniPackageParams & InPackageParams) +{ + // Try to find the material we want to create an instance of + UMaterialInterface* SourceMaterial = Cast( + FHoudiniEngine::Get().GetHoudiniDefaultCOPMaterial()); + if (!SourceMaterial) + return nullptr; + + // Create/Retrieve the package for the MI + FString MaterialInstanceName; + FString MaterialInstanceNamePrefix = UPackageTools::SanitizePackageName( + SourceMaterial->GetName() + + TEXT("_instance_") + + InPackageParams.ComponentGUID.ToString()); + + // See if we can find an existing package for that instance + UPackage* MaterialInstancePackage = FHoudiniMaterialTranslator::CreatePackageForMaterial( + -1, MaterialInstanceNamePrefix, InPackageParams, MaterialInstanceName); + + // Couldn't create a package for that Material Instance + if (!MaterialInstancePackage) + return nullptr; + + bool bNewMaterialCreated = false; + UMaterialInstanceConstant* NewMaterialInstance = LoadObject( + MaterialInstancePackage, *MaterialInstanceName, nullptr, LOAD_None, nullptr); + + if (!NewMaterialInstance) + { + // Factory to create materials. + UMaterialInstanceConstantFactoryNew* MaterialInstanceFactory = NewObject(); + if (!MaterialInstanceFactory) + return nullptr; + + // Create the new material instance + MaterialInstanceFactory->AddToRoot(); + MaterialInstanceFactory->InitialParent = SourceMaterial; + NewMaterialInstance = (UMaterialInstanceConstant*)MaterialInstanceFactory->FactoryCreateNew( + UMaterialInstanceConstant::StaticClass(), + MaterialInstancePackage, + FName(*MaterialInstanceName), + RF_Public | RF_Standalone, + NULL, + GWarn); + + if (NewMaterialInstance) + bNewMaterialCreated = true; + + MaterialInstanceFactory->RemoveFromRoot(); + } + + if (!NewMaterialInstance) + return nullptr; + + // Update context for generated materials (will trigger when the object goes out of scope). + FMaterialUpdateContext MaterialUpdateContext; + + // Apply material instance parameters + FName MatParamName = FName("cop"); + NewMaterialInstance->SetTextureParameterValueEditorOnly(MatParamName, InTexture); + bool bModifiedMaterialParameters = true; + + // Schedule this material for update if needed. + if (bNewMaterialCreated || bModifiedMaterialParameters) + MaterialUpdateContext.AddMaterialInstance(NewMaterialInstance); + + if (bNewMaterialCreated) + { + // Add meta information to this package. + FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + MaterialInstancePackage, NewMaterialInstance, HAPI_UNREAL_PACKAGE_META_GENERATED_OBJECT, TEXT("true")); + FHoudiniEngineUtils::AddHoudiniMetaInformationToPackage( + MaterialInstancePackage, NewMaterialInstance, HAPI_UNREAL_PACKAGE_META_GENERATED_NAME, *MaterialInstanceName); + // Notify registry that we have created a new material. + FAssetRegistryModule::AssetCreated(NewMaterialInstance); + } + + if (bNewMaterialCreated || bModifiedMaterialParameters) + { + // Dirty the material + NewMaterialInstance->MarkPackageDirty(); + + // Update the material instance + NewMaterialInstance->InitStaticPermutation(); + NewMaterialInstance->PreEditChange(nullptr); + NewMaterialInstance->PostEditChange(); + } + + return NewMaterialInstance; +} +``` + +`Source/HoudiniEngine/Private/HoudiniTextureTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HAPI/HAPI_Common.h" +#include "Engine/TextureDefines.h" +#include "Engine/EngineTypes.h" + +class UPackage; +class UTexture2D; +class UHoudiniOutput; + +struct FHoudiniPackageParams; +struct FCreateTexture2DParameters; + +struct HOUDINIENGINE_API FHoudiniTextureTranslator +{ +public: + // Get image planes from HAPI + static bool HapiGetImagePlanes( + const HAPI_ParmId InNodeParmId, const HAPI_MaterialInfo& InMaterialInfo, TArray& OutImagePlanes); + + // Retrieve information based on the planes of the HAPI material. + static bool GetPlaneInfo( + const HAPI_ParmId InParmTextureId, + const HAPI_MaterialInfo& InMaterialInfo, + HAPI_ImagePacking& OutImagePacking, + const char*& OutPlaneType, + bool& bOutUseAlpha); + + // Render a texture off a Houdini material node. + // Note that GetPlaneInfo() renders the texture too. So if you have called GetPlaneInfo(), you don't need to call this. + static bool HapiRenderTexture( + const HAPI_NodeId InMaterialNodeId, + const HAPI_ParmId InTextureParmId); + + static bool HapiRenderCOPTexture( + const HAPI_NodeId InCopNodeId); + + // HAPI : Retrieve a list of image planes. + static bool HapiExtractImage( + const HAPI_NodeId InMaterialNodeId, + const char* InPlaneType, + const HAPI_ImageDataFormat InImageDataFormat, + const HAPI_ImagePacking InImagePacking, + TArray& OutImageBuffer); + + static UPackage* CreatePackageForTexture( + const HAPI_NodeId& InMaterialNodeId, + const FString& InTextureType, + const FHoudiniPackageParams& InPackageParams, + FString& OutTextureName); + + // Create a texture from a HAPI material. + // Returns true if the texture is successfully created, false otherwise. + static bool CreateTexture( + // HAPI extraction parameters + const HAPI_NodeId InMaterialNodeId, + const char* InPlaneType, + HAPI_ImageDataFormat InImageDataFormat, + HAPI_ImagePacking InImagePacking, + // Texture creation parameters + UTexture2D*& OutTexture, + const FString& InNodePath, + const FString& InTextureType, + const FHoudiniPackageParams& InPackageParams, + const FCreateTexture2DParameters& InTextureParameters, + const TextureGroup InLODGroup, + TArray& OutPackages); + + static UTexture2D* CreateUnrealTexture( + UTexture2D* ExistingTexture, + const HAPI_ImageInfo& ImageInfo, + UPackage* Package, + const FString& TextureName, + const TArray& ImageBuffer, + const FCreateTexture2DParameters& TextureParameters, + const TextureGroup LODGroup, + const FString& TextureType, + const FString& NodePath); + + static bool ProcessCopOutput( + UHoudiniOutput* InOutput, + const FHoudiniPackageParams& InPackageParams); + + static UMaterialInterface* CreateDefaultCopMaterialForTexture( + UTexture2D* InTexture, + const FHoudiniPackageParams& InPackageParams); +}; +``` + +`Source/HoudiniEngine/Private/SAssetSelectionWidget.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "SAssetSelectionWidget.h" + +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineString.h" + +#if WITH_EDITOR + +#include "Runtime/Launch/Resources/Version.h" + +#include "EditorStyleSet.h" +#include "Internationalization/Internationalization.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/Layout/SScrollBox.h" +#include "Widgets/Text/STextBlock.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +SAssetSelectionWidget::SAssetSelectionWidget() + : SelectedAssetName(-1) + , bIsValidWidget(false) + , bIsCancelled(false) + , bHideNameSpace(true) + , bHideVersion(false) + , bHideNodeManager(true) +{} + +bool +SAssetSelectionWidget::IsCancelled() const +{ + return bIsCancelled; +} + +bool +SAssetSelectionWidget::IsValidWidget() const +{ + return bIsValidWidget; +} + +int32 +SAssetSelectionWidget::GetSelectedAssetName() const +{ + return SelectedAssetName; +} + +void +SAssetSelectionWidget::Construct(const FArguments & InArgs) +{ + WidgetWindow = InArgs._WidgetWindow; + AvailableAssetNames = InArgs._AvailableAssetNames; + + TSharedRef VerticalBox = SNew(SVerticalBox); + this->ChildSlot + [ + SNew(SBorder) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + .BorderImage(FAppStyle::GetBrush(TEXT("Menu.Background"))) +#else + .BorderImage(FEditorStyle::GetBrush(TEXT("Menu.Background"))) +#endif + .Content() + [ + SNew(SScrollBox) + .Orientation(Orient_Vertical) + + SScrollBox::Slot() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0) + .VAlign(VAlign_Center) + [ + SAssignNew(VerticalBox, SVerticalBox) + ] + ] + ] + ]; + + + + // Add Checkboxes for the display options: + // Hide Name Space + VerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() { return bHideNameSpace ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState NewState) {bHideNameSpace = NewState == ECheckBoxState::Checked; }) + .ToolTipText(LOCTEXT("HideNamespaceTooltipText", "Hide the Asset's namespace: NAMESPACE::NODE_TYPE/ASSET_NAME::VERSION")) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("HideNamespaceText", "Hide namespace")) + ] + ]; + + // Hide NodeManager + VerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() { return bHideNodeManager ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState NewState) {bHideNodeManager = NewState == ECheckBoxState::Checked; }) + .ToolTipText(LOCTEXT("bHideNodeManagerTooltipText", "Hide the Asset's node type: NAMESPACE::NODE_TYPE/ASSET_NAME::VERSION")) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("bHideNodeManagerText", "Hide node type")) + ] + ]; + + // Hide Version + VerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() { return bHideVersion ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState NewState) {bHideVersion = NewState == ECheckBoxState::Checked; }) + .ToolTipText(LOCTEXT("HideVersionTooltipText", "Hide the Asset's version: NAMESPACE::NODE_TYPE/ASSET_NAME::VERSION")) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("HideVersionText", "Hide version")) + ] + ]; + + // Lambda for getting the asset's display name that matches the chosen options + auto GetShortNameToDisplay = [this](const FString& AssetNameString) + { + bool bDisplayShortNames = bHideNameSpace || bHideVersion || bHideNodeManager; + if (!bDisplayShortNames) + return FText::FromString(AssetNameString); + + // Extract the asset name from XX::YY/ASSET_NAME::ZZ + FString ShortAssetNameString = AssetNameString; + + int32 FirstSepIdx = ShortAssetNameString.Find("::"); + int32 LastSepIdx = ShortAssetNameString.Find("::", ESearchCase::IgnoreCase, ESearchDir::FromEnd); + if (FirstSepIdx >= 0 && LastSepIdx >= 0 && FirstSepIdx != LastSepIdx) + { + // We have two sets of :: separators - procede with the chopping + if (bHideNameSpace) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + ShortAssetNameString.RightChopInline(FirstSepIdx + 2, EAllowShrinking::Yes); +#else + ShortAssetNameString.RightChopInline(FirstSepIdx + 2, true); +#endif + LastSepIdx -= FirstSepIdx + 2; + } + + // + if (bHideVersion && LastSepIdx >= 0) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + ShortAssetNameString.LeftInline(LastSepIdx, EAllowShrinking::Yes); +#else + ShortAssetNameString.LeftInline(LastSepIdx, true); +#endif + } + } + else if(FirstSepIdx >= 0 && (bHideNameSpace || bHideVersion)) + { + // If we only have one separator + // Try to see if we can find a node type in one of the two parts + // as this will let us know where the node name is + // If unsure - don't touch the short string + FString Left = ShortAssetNameString.Left(LastSepIdx); + FString Right = ShortAssetNameString.RightChop(FirstSepIdx + 2); + + if (bHideVersion && Left.Find("/") >= 0) + { + ShortAssetNameString = Left; + } + else if (bHideNameSpace && Right.Find("/") >= 0) + { + ShortAssetNameString = Right; + } + } + + // Now remove the node type identifier YY if any + if (bHideNodeManager) + { + FirstSepIdx = ShortAssetNameString.Find("::"); + int32 ManagerSepIdx = ShortAssetNameString.Find("/"); + + if (ManagerSepIdx >= 0) + { + if (FirstSepIdx >= 0 && FirstSepIdx < ManagerSepIdx && !bHideNameSpace) + { + ShortAssetNameString = + ShortAssetNameString.Left(FirstSepIdx + 2) + + ShortAssetNameString.RightChop(ManagerSepIdx + 1); + } + else + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + ShortAssetNameString.RightChopInline(ManagerSepIdx + 1, EAllowShrinking::Yes); +#else + ShortAssetNameString.RightChopInline(ManagerSepIdx + 1, true); +#endif + } + } + } + + // Make sure we still have somnething to display + if (ShortAssetNameString.Len() <= 0) + ShortAssetNameString = AssetNameString; + + return FText::FromString(ShortAssetNameString); + }; + + for (int32 AssetNameIdx = 0, AssetNameNum = AvailableAssetNames.Num(); AssetNameIdx < AssetNameNum; ++AssetNameIdx) + { + FString AssetNameString = TEXT(""); + HAPI_StringHandle AssetName = AvailableAssetNames[AssetNameIdx]; + + FHoudiniEngineString HoudiniEngineString(AssetName); + if (!HoudiniEngineString.ToFString(AssetNameString)) + continue; + + bIsValidWidget = true; + FText AssetNameText = FText::FromString(AssetNameString); + + VerticalBox->AddSlot() + .HAlign(HAlign_Center) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .Padding(2.0f, 4.0f) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .OnClicked(this, &SAssetSelectionWidget::OnButtonAssetPick, AssetName) + .Text_Lambda([this, GetShortNameToDisplay, AssetNameString]() + { + return GetShortNameToDisplay(AssetNameString); + }) + .ToolTipText(AssetNameText) + ] + ]; + } +} + +FReply +SAssetSelectionWidget::OnButtonAssetPick(int32 AssetName) +{ + SelectedAssetName = AssetName; + + if (TSharedPtr WindowPtr = WidgetWindow.Pin()) + { + WindowPtr->HideWindow(); + WindowPtr->RequestDestroyWindow(); + } + + return FReply::Handled(); +} + +FReply +SAssetSelectionWidget::OnButtonOk() +{ + if (TSharedPtr WindowPtr = WidgetWindow.Pin()) + { + WindowPtr->HideWindow(); + WindowPtr->RequestDestroyWindow(); + } + + return FReply::Handled(); +} + +FReply +SAssetSelectionWidget::OnButtonCancel() +{ + bIsCancelled = true; + + if (TSharedPtr WindowPtr = WidgetWindow.Pin()) + { + WindowPtr->HideWindow(); + WindowPtr->RequestDestroyWindow(); + } + + return FReply::Handled(); +} + +#undef LOCTEXT_NAMESPACE + +#endif + +``` + +`Source/HoudiniEngine/Private/SAssetSelectionWidget.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#if WITH_EDITOR + +#include "CoreMinimal.h" +//#include "Misc/Attribute.h" + +#include "Widgets/SWidget.h" +#include "Widgets/SCompoundWidget.h" +#include "Widgets/SWindow.h" +#include "Widgets/DeclarativeSyntaxSupport.h" + +#include "HAPI/HAPI_Common.h" + +/** Slate widget used to pick an asset to instantiate from an HDA with multiple assets inside. **/ +class SAssetSelectionWidget : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SAssetSelectionWidget) + : _WidgetWindow(), _AvailableAssetNames() + {} + + SLATE_ARGUMENT(TSharedPtr, WidgetWindow) + SLATE_ARGUMENT(TArray< HAPI_StringHandle >, AvailableAssetNames) + SLATE_END_ARGS() + +public: + + SAssetSelectionWidget(); + +public: + + /** Widget construct. **/ + void Construct(const FArguments & InArgs); + + /** Return true if cancel button has been pressed. **/ + bool IsCancelled() const; + + /** Return true if constructed widget is valid. **/ + bool IsValidWidget() const; + + /** Return selected asset name. **/ + int32 GetSelectedAssetName() const; + +protected: + + /** Called when Ok button is pressed. **/ + FReply OnButtonOk(); + + /** Called when Cancel button is pressed. **/ + FReply OnButtonCancel(); + + /** Called when user picks an asset. **/ + FReply OnButtonAssetPick(int32 AssetName); + +protected: + + /** Parent widget window. **/ + TWeakPtr< SWindow > WidgetWindow; + + /** List of available Houdini Engine asset names. **/ + TArray< HAPI_StringHandle > AvailableAssetNames; + + /** Selected asset name. **/ + int32 SelectedAssetName; + + /** Is set to true if constructed widget is valid. **/ + bool bIsValidWidget; + + /** Is set to true if selection process has been cancelled. **/ + bool bIsCancelled; + + /** Hide name space in the asset name **/ + bool bHideNameSpace; + + /** Hide version in the asset name **/ + bool bHideVersion; + + /** Hide node manager type in the asset name **/ + bool bHideNodeManager; +}; + +#endif +``` + +`Source/HoudiniEngine/Private/Tests/HoudiniCoreTests.cpp`: + +```cpp +#include "../HoudiniEngine.h" +#include "Misc/AutomationTest.h" + +#if WITH_DEV_AUTOMATION_TESTS + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(HoudiniCoreTest, "Houdini.Core.TestAutomation", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniCoreTest::RunTest(const FString & Parameters) +{ + return true; +} + +#endif +``` + +`Source/HoudiniEngine/Private/Tests/HoudiniCoreTests.h`: + +```h +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +#endif + + +``` + +`Source/HoudiniEngine/Private/UnrealAnimationTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealAnimationTranslator.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniInputObject.h" + +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputUtils.h" +#include "UnrealObjectInputRuntimeUtils.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniSkeletalMeshUtils.h" + +#include "Animation/Skeleton.h" +#include "Animation/AnimSequence.h" +#include "ReferenceSkeleton.h" +#include "Serialization/JsonSerializer.h" + + +bool +FUnrealAnimationTranslator::SetAnimationDataOnNode( + UAnimSequence* Animation, + HAPI_NodeId& NewNodeId) +{ + if (!IsValid(Animation)) + return false; + + return AddBoneTracksToNode(NewNodeId, Animation); + +} + + +bool FUnrealAnimationTranslator::CreateInputNodeForAnimation( + UAnimSequence* AnimSequence, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + const bool& bInputNodesCanBeDeleted) +{ + FString FinalInputNodeName = InputNodeName; + + FUnrealObjectInputIdentifier Identifier; + FUnrealObjectInputHandle ParentHandle; + HAPI_NodeId ParentNodeId = -1; + + { + const FUnrealObjectInputOptions Options; + return true; + } + return false; +} + +bool +FUnrealAnimationTranslator::HapiCreateInputNodeForAnimation( + UAnimSequence* Animation, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + bool ExportAllLODs, + bool ExportSockets, + bool ExportColliders, + bool bInputNodesCanBeDeleted) +{ + // If we don't have a animation there's nothing to do. + if (!IsValid(Animation)) + return false; + + // Input node name, defaults to InputNodeName, but can be changed by the new input system + FString FinalInputNodeName = InputNodeName; + + // Find the node in new input system + // Identifier will be the identifier for the entry created in this call of the function. + FUnrealObjectInputIdentifier Identifier; + FUnrealObjectInputHandle ParentHandle; + HAPI_NodeId ParentNodeId = -1; + { + // Creates this input's identifier and input options + const FUnrealObjectInputOptions Options; + Identifier = FUnrealObjectInputIdentifier(Animation, Options, true); + + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId) && FUnrealObjectInputUtils::AreReferencedHAPINodesValid(Handle)) + { + if (!bInputNodesCanBeDeleted) + { + // Make sure to prevent deletion of the input node if needed + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + } + + OutHandle = Handle; + InputNodeId = NodeId; + return true; + } + } + + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + // Create any parent/container nodes that we would need, and get the node id of the immediate parent + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // We now need to create the nodes (since we couldn't find existing ones in the manager) + // To do that, we can simply continue this function + + // Set InputNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that InputNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (Handle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, InputNodeId)) + InputNodeId = -1; + } + else + { + InputNodeId = -1; + } + } + + // Node ID for the newly created node + HAPI_NodeId NewNodeId = -1; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateInputNode( + FinalInputNodeName, NewNodeId, ParentNodeId), false); + + if (!FHoudiniEngineUtils::HapiCookNode(NewNodeId, nullptr, true)) + return false; + + // Check if we have a valid id for this new input asset. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(NewNodeId)) + return false; + + HAPI_NodeId PreviousInputNodeId = InputNodeId; + + // Update our input NodeId + InputNodeId = NewNodeId; + // Get our parent OBJ NodeID + HAPI_NodeId InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(NewNodeId); + + // We have now created a valid new input node, delete the previous one + if (PreviousInputNodeId >= 0) + { + // Get the parent OBJ node ID before deleting! + HAPI_NodeId PreviousInputOBJNode = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousInputNodeId); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input node for %s."), *InputNodeName); + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputOBJNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input OBJ node for %s."), *InputNodeName); + } + } + + if (FUnrealAnimationTranslator::SetAnimationDataOnNode(Animation, NewNodeId)) + { + // Commit the geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(NewNodeId), false); + + //Create Output Node + HAPI_NodeId OutputNodeId; + HAPI_Result CreateResult = FHoudiniEngineUtils::CreateNode(InputObjectNodeId, TEXT("output"), + TEXT("Output"), true, &OutputNodeId); + + // Create Point Wrangle node + // This will convert matrix attributes to their proper type which HAPI doesn't seem to be translating correctly. + + HAPI_NodeId AttribWrangleNodeId; + { + CreateResult = FHoudiniEngineUtils::CreateNode(InputObjectNodeId, TEXT("attribwrangle"), + TEXT("convert_matrix"), false, &AttribWrangleNodeId); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + AttribWrangleNodeId, 0, NewNodeId, 0), false); + + // Construct a VEXpression to convert matrices. + const FString FormatString = TEXT("3@transform = matrix3(f[]@in_transform);\n4@localtransform = matrix(f[]@in_localtransform); "); + + // Set the snippet parameter to the VEXpression. + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniEngineUtils::HapiFindParameterByName(AttribWrangleNodeId, "snippet", ParmInfo); + if (ParmId != -1) + { + FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), AttribWrangleNodeId, + H_TCHAR_TO_UTF8(*FormatString), ParmId, 0); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Invalid Parameter: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + } + } + + //Create Pack Node + HAPI_NodeId PackNodeId; + HAPI_Result CreatePackResult = FHoudiniEngineUtils::CreateNode(InputObjectNodeId, TEXT("pack"), + TEXT("pack_data"), false, &PackNodeId); + + // Connect to point wrangle + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + PackNodeId, 0, AttribWrangleNodeId, 0), false); + + { + const char* PackByNameOption = HAPI_UNREAL_PARAM_PACK_BY_NAME; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), PackNodeId, + PackByNameOption, 0, 1), false); + } + + { + const char* PackedFragmentsOption = HAPI_UNREAL_PARAM_PACKED_FRAGMENTS; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), PackNodeId, + PackedFragmentsOption, 0, 0), false); + } + + HAPI_ParmInfo nameattributeParmInfo; + HAPI_ParmId nameattributeParmId = FHoudiniEngineUtils::HapiFindParameterByName(PackNodeId, "nameattribute", nameattributeParmInfo); + if (nameattributeParmId != -1) + { + FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), PackNodeId, + "frame", nameattributeParmId, 0); + } + + HAPI_ParmInfo transfer_attributesParmInfo; + HAPI_ParmId transfer_attributesParmId = FHoudiniEngineUtils::HapiFindParameterByName(PackNodeId, "transfer_attributes", transfer_attributesParmInfo); + if (transfer_attributesParmId != -1) + { + FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), PackNodeId, + "time", transfer_attributesParmId, 0); + } + + // Create Detail Wrangle node + // This will copy the clipinfo dict to detail owner after pack (Pack doesn't want to transfer detail attributes) + + HAPI_NodeId DetailWrangleNodeId; + { + CreateResult = FHoudiniEngineUtils::CreateNode(InputObjectNodeId, TEXT("attribwrangle"), + TEXT("build_clipinfo"), false, &DetailWrangleNodeId); + + // Connect to Pack + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + DetailWrangleNodeId, 0, PackNodeId, 0), false); + + // Connect to Point Wrangle + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + DetailWrangleNodeId, 1, AttribWrangleNodeId, 0), false); + + // Set the class parameter to "detail" + FHoudiniApi::SetParmIntValue(FHoudiniEngine::Get().GetSession(), DetailWrangleNodeId, "class", 0, 0); + + // Construct a VEXpression to convert matrices. + const FString FormatString = TEXT(R"(d@clipinfo = detail(1, "clipinfo");)"); + + // Set the snippet parameter to the VEXpression. + HAPI_ParmInfo SnippetParmInfo; + HAPI_ParmId SnippetParmId = FHoudiniEngineUtils::HapiFindParameterByName(DetailWrangleNodeId, "snippet", SnippetParmInfo); + if (SnippetParmId != -1) + { + FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), DetailWrangleNodeId, + H_TCHAR_TO_UTF8(*FormatString), SnippetParmId, 0); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Invalid Parameter: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + } + } + + // Connect output to detail wrangle (build_clipinfo) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + OutputNodeId, 0, DetailWrangleNodeId, 0), false); + + FHoudiniEngineUtils::HapiCookNode(AttribWrangleNodeId, nullptr, true); + FHoudiniEngineUtils::HapiCookNode(PackNodeId, nullptr, true); + } + + { + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, InputNodeId, Handle, InputObjectNodeId, nullptr, bInputNodesCanBeDeleted)) + OutHandle = Handle; + } + + return true; +} + +FTransform +FUnrealAnimationTranslator::GetCompSpaceTransformForBone(const FReferenceSkeleton& InSkel, int32 InBoneIdx) +{ + FTransform resultBoneTransform = InSkel.GetRefBonePose()[InBoneIdx]; + + auto refBoneInfo = InSkel.GetRefBoneInfo(); + + int32 Bone = InBoneIdx; + while (Bone) + { + resultBoneTransform *= InSkel.GetRefBonePose()[refBoneInfo[Bone].ParentIndex]; + Bone = refBoneInfo[Bone].ParentIndex; + } + + return resultBoneTransform; +} + +FTransform +FUnrealAnimationTranslator::GetCompSpacePoseTransformForBoneMap(const TMap& BoneMap, const FReferenceSkeleton& InSkel, int32 InBoneIdx) +{ + FTransform resultBoneTransform = FTransform::Identity; + if (BoneMap.Contains(InBoneIdx)) + { + resultBoneTransform = BoneMap.FindChecked(InBoneIdx); + } + + auto refBoneInfo = InSkel.GetRefBoneInfo(); + + int32 Bone = InBoneIdx; + while (Bone) + { + const int32 ParentIdx = refBoneInfo[Bone].ParentIndex; + FTransform BoneTransform = FTransform::Identity; + if (BoneMap.Contains(ParentIdx)) + { + BoneTransform = BoneMap.FindChecked(ParentIdx); + } + + resultBoneTransform *= BoneTransform; + Bone = ParentIdx; + } + + return resultBoneTransform; +} + + + +FTransform +FUnrealAnimationTranslator::GetCompSpacePoseTransformForBone(const TArray& Bones, const FReferenceSkeleton& InSkel, int32 InBoneIdx) +{ + FTransform resultBoneTransform = Bones[InBoneIdx]; + + auto refBoneInfo = InSkel.GetRefBoneInfo(); + + int32 Bone = InBoneIdx; + while (Bone) + { + //if root then use -90 + FTransform BoneTransform = Bones[refBoneInfo[Bone].ParentIndex]; + resultBoneTransform *= BoneTransform; + Bone = refBoneInfo[Bone].ParentIndex; + } + + return resultBoneTransform; +} + + +void +FUnrealAnimationTranslator::GetComponentSpaceTransforms(TArray& OutResult, const FReferenceSkeleton& InRefSkeleton) +{ + const int32 PoseNum = InRefSkeleton.GetRefBonePose().Num(); + OutResult.SetNum(PoseNum); + + for (int32 i = 0; i < PoseNum; i++) + { + OutResult[i] = FUnrealAnimationTranslator::GetCompSpaceTransformForBone(InRefSkeleton, i); + + } +} + +FString FUnrealAnimationTranslator::GetBonePathForBone(const FReferenceSkeleton& InSkel, int32 InBoneIdx) +{ + FString BonePath; + auto refBoneInfo = InSkel.GetRefBoneInfo(); + + int32 Bone = InBoneIdx; + TArray Indices; + while (Bone) + { + Indices.Add(Bone); + Bone = refBoneInfo[Bone].ParentIndex; + } + Indices.Add(0); + + Algo::Reverse(Indices); + for (auto Idx : Indices) + { + BonePath += FString(TEXT("\x2f")) + refBoneInfo[Idx].Name.ToString(); + } + + return BonePath; +} + + +bool +FUnrealAnimationTranslator::AddBoneTracksToNode(HAPI_NodeId& NewNodeId, UAnimSequence* Animation) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + + //read animation data + UAnimSequence* AnimSequence = CastChecked(Animation); + const USkeleton* Skeleton = Animation->GetSkeleton(); + const FReferenceSkeleton& RefSkeleton = Skeleton->GetReferenceSkeleton(); + const FString SkeletonPathName = Skeleton->GetPathName(); + // + const IAnimationDataModel* DataModel = Animation->GetDataModel(); + TArray CustomAttributes; + CustomAttributes.Reset(); + // + TArray BonesTrackNames; + DataModel->GetBoneTrackNames(BonesTrackNames); + FFrameRate FrameRate = DataModel->GetFrameRate(); + const float FrameRateInterval = DataModel->GetFrameRate().AsInterval(); + + TMap> TrackMap; //For Each Bone, Store Array of transforms (one for each key) + int32 TotalTrackKeys = 0; + //Iterate over BoneTracks + for (FName TrackName : BonesTrackNames) + { + //const FMeshBoneInfo& CurrentBone = RefSkeleton.GetRefBoneInfo()[BoneIndex]; + TArray ThisTrackTransforms; + DataModel->GetBoneTrackTransforms(TrackName, ThisTrackTransforms); + FString DetailAttributeName = FString::Printf(TEXT("%s_transforms"), *TrackName.ToString()); + TotalTrackKeys = ThisTrackTransforms.Num(); + TArray TrackFloatKeys; + TrackFloatKeys.SetNumZeroed(TotalTrackKeys * 20); + TrackMap.Add(TrackName, ThisTrackTransforms); + } + + TArray WorldSpaceBonePositions; + TArray LocalTransformData; //for pCaptData property + LocalTransformData.SetNumZeroed(4 * 4 * BonesTrackNames.Num() * (TotalTrackKeys + 1)); //4x4 matrix // adding extra key for append + + TArray WorldTransformData; + WorldTransformData.SetNumZeroed(3 * 3 * BonesTrackNames.Num() * (TotalTrackKeys + 1)); //3x3 matrix + + TArray PrimIndices; + int PrimitiveCount = 0; + int BoneCount = BonesTrackNames.Num(); + int RefBoneCount = RefSkeleton.GetRefBoneInfo().Num(); + TArray BoneNames; + TArray BonePaths; + TArray UnrealSkeletonPaths; + + TArray< TMap> Frames; + TMap BoneIndexCounterMap; + // AnimCurve data is stored in the fbx_custom_attributes dictionary which is stored on root joints for each frame + // For any joint that is not the "root" join, the dict can be empty. + // Note that we have to add an extra frame to the data for the topology frame. + TArray FbxCustomAttributes; + FbxCustomAttributes.SetNum(BonesTrackNames.Num() * (TotalTrackKeys+1)); + + // Map Bone Indexes (from skeleton) to indexes for output data + // since animated bones are typically a subset of the bones from the skeleton. + int RootBoneIndex = INDEX_NONE; + { + int BoneNameCounter = 0; + for (FName AnimBoneName : BonesTrackNames) + { + int32 BoneIndex = Skeleton->GetReferenceSkeleton().FindBoneIndex(AnimBoneName); + BoneIndexCounterMap.Add(BoneIndex, BoneNameCounter); + if (AnimBoneName == "root") + { + RootBoneIndex = BoneNameCounter; + } + BoneNameCounter++; + } + } + + for (int KeyFrame = 0; KeyFrame < TotalTrackKeys; KeyFrame++) + { + int32 BoneIndex = 0; + TArray KeyBoneTransforms; + //for each bone + TMap SingleFrame; + for (const FMeshBoneInfo& MeshBoneInfo : RefSkeleton.GetRefBoneInfo()) + { + //add that tracks keys + if (TrackMap.Contains(MeshBoneInfo.Name)) + { + TArray& TransformArray = TrackMap[MeshBoneInfo.Name]; + FTransform KeyBoneTransform = TransformArray[KeyFrame]; + KeyBoneTransforms.Add(KeyBoneTransform); + SingleFrame.Add(BoneIndex, KeyBoneTransform); + } + + BoneIndex++; + } + + if (RootBoneIndex != INDEX_NONE) + { + // Sample anim curves and store the data on the root bone for this keyframe. + // Note that we're skipping over the topology frame (hence the Keyframe+1). + const int DataIndex = (KeyFrame+1)*BoneCount + RootBoneIndex; + const float SampleTime = KeyFrame * FrameRateInterval; + + // Sample anim curve data for the root bone and store it in a JSON object for easy serialization. + TSharedPtr JSONObject = MakeShareable(new FJsonObject); + + // Sample all the curves for the current time value + const FAnimationCurveData& CurveData = DataModel->GetCurveData(); + TArray FloatCurves = CurveData.FloatCurves; + for (const FFloatCurve& Curve : FloatCurves) + { + float Sample = Curve.Evaluate(SampleTime); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + JSONObject->SetNumberField(Curve.GetName().ToString(), Sample); +#else + JSONObject->SetNumberField(Curve.Name.DisplayName.ToString(), Sample); +#endif + } + + FbxCustomAttributes[DataIndex] = FHoudiniEngineUtils::JSONToString(JSONObject); + } + + Frames.Add(SingleFrame); + } + TArray FrameIndexData; + TArray TimeData; + + + bool AppendFirstFrame = true; //topology frame + TMap BoneInfos; + + for (const FMeshBoneInfo& MeshBoneInfo : Skeleton->GetReferenceSkeleton().GetRefBoneInfo()) + { + BoneInfos.Add(MeshBoneInfo.Name, MeshBoneInfo); + } + + //For Each Key Frame ( eg 1-60) + // We'll be using the FrameOffset to inject the first frame twice for the MotionClip topology frame. + int FrameOffset = 0; + for (int FrameIndex = 0; FrameIndex < TotalTrackKeys+1; FrameIndex++) + { + if (FrameIndex > 0) + { + FrameOffset = -1; + } + + int32 BoneRefIndex = 0; + int32 BoneDataIndex = 0; + //for each bone + for (FName KeyBoneName : BonesTrackNames) + { + BoneRefIndex = Skeleton->GetReferenceSkeleton().FindBoneIndex(KeyBoneName); + const FMeshBoneInfo& MeshBoneInfo = BoneInfos.FindChecked(KeyBoneName); + //add that tracks keys + if (TrackMap.Contains(MeshBoneInfo.Name)) + { + // Fetch Component Space Bone Matrices. We'll consider this World Space for now. + FTransform BoneTransform = FUnrealAnimationTranslator::GetCompSpacePoseTransformForBoneMap(Frames[FrameIndex+FrameOffset], RefSkeleton, BoneRefIndex); + FMatrix BoneMatrix = BoneTransform.ToMatrixWithScale(); + + // Convert Unreal to Houdini Matrix + int32 WorldDataIndex = (FrameIndex)*BonesTrackNames.Num() * 3 * 3 + (BoneDataIndex * 3 * 3); + float Pos[3]; + FHoudiniSkeletalMeshUtils::UnrealToHoudiniMatrix(BoneMatrix, WorldTransformData.GetData() + WorldDataIndex, Pos); + WorldSpaceBonePositions.Add(Pos[0]); + WorldSpaceBonePositions.Add(Pos[1]); + WorldSpaceBonePositions.Add(Pos[2]); + + // Generate Local Transform Data and store it in Houdini Forat + + FMatrix FinalLocalMatrix = BoneMatrix; + if (BoneRefIndex > 0) + { + // Take into account the parent bone's transform + int32 ParentBoneIndex = RefSkeleton.GetParentIndex(BoneRefIndex); + FTransform ParentPoseTransform = GetCompSpacePoseTransformForBoneMap(Frames[FrameIndex + FrameOffset], RefSkeleton, ParentBoneIndex); + FMatrix ParentMatrix = ParentPoseTransform.ToMatrixWithScale(); + FinalLocalMatrix = BoneMatrix * ParentMatrix.Inverse(); + } + + int32 LocalDataIndex = (FrameIndex) * BonesTrackNames.Num() * 4 * 4 + (BoneDataIndex * 4 * 4); + + FHoudiniSkeletalMeshUtils::UnrealToHoudiniMatrix(FinalLocalMatrix, LocalTransformData.GetData() + LocalDataIndex); + + // WorldTransformData[i * OutNames.Num() * 3 * 3] = BoneIndex; + const float TimeValue = FrameIndex > 0 ? (FrameIndex-1) * FrameRateInterval : 0.f; + if (BoneRefIndex > 0) + { + const int32 ParentDataIndex = BoneIndexCounterMap.FindChecked(MeshBoneInfo.ParentIndex); + PrimIndices.Add((FrameIndex * BoneCount) + ParentDataIndex); + PrimIndices.Add((FrameIndex * BoneCount) + BoneDataIndex); + FrameIndexData.Add(FrameIndex); + // The topology frame (FrameIndex = 0) and the first anim frame (FrameIndex = 1) Should have time = 0. + TimeData.Add(TimeValue); + PrimitiveCount++; + } + BoneNames.Add(MeshBoneInfo.Name.ToString()); + BonePaths.Add(GetBonePathForBone(RefSkeleton, BoneRefIndex)); + UnrealSkeletonPaths.Add(SkeletonPathName); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Missing Bone")); + } + BoneDataIndex++; + } + + } + + //---------------------------------------- + // Create part. + //---------------------------------------- + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + Part.vertexCount = PrimIndices.Num(); + Part.faceCount = PrimitiveCount; + Part.pointCount = WorldSpaceBonePositions.Num() / 3; + Part.type = HAPI_PARTTYPE_MESH; + + HAPI_Result ResultPartInfo = FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, &Part); + + //---------------------------------------- + // Create point attribute info. + //---------------------------------------- + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = Part.pointCount; + AttributeInfoPoint.tupleSize = 3; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint), false); + + //Position Data + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData(FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint, + (float*)WorldSpaceBonePositions.GetData(), 0, AttributeInfoPoint.count), false); + + //vertex list. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetVertexList(FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, PrimIndices.GetData(), 0, PrimIndices.Num()), false); + + //FaceCounts + TArray FaceCounts; + FaceCounts.SetNumUninitialized(Part.faceCount); + for (int32 n = 0; n < Part.faceCount; n++) + FaceCounts[n] = 2; + + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetFaceCounts( + FaceCounts, NewNodeId, 0), false); + + + // Create point attribute info for the bone name. + HAPI_AttributeInfo BoneNameInfo; + FHoudiniApi::AttributeInfo_Init(&BoneNameInfo); + BoneNameInfo.count = Part.pointCount; + BoneNameInfo.tupleSize = 1; + BoneNameInfo.exists = true; + BoneNameInfo.owner = HAPI_ATTROWNER_POINT; + BoneNameInfo.storage = HAPI_STORAGETYPE_STRING; + BoneNameInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + FString BoneNameAttributeName = FString::Printf(TEXT("name")); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + TCHAR_TO_ANSI(*BoneNameAttributeName), &BoneNameInfo), false); + + FHoudiniHapiAccessor Accessor(NewNodeId, 0, TCHAR_TO_ANSI(*BoneNameAttributeName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(BoneNameInfo, BoneNames), false); + + // Create point attribute info for the bone path. + HAPI_AttributeInfo AttributeInfoName; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoName); + AttributeInfoName.count = Part.pointCount; + AttributeInfoName.tupleSize = 1; + AttributeInfoName.exists = true; + AttributeInfoName.owner = HAPI_ATTROWNER_POINT; + AttributeInfoName.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoName.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, "path", &AttributeInfoName), false); + + + Accessor.Init(NewNodeId, 0, "path"); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoName, BonePaths), false); + + //--------------------------------------------------------------------------------------------------------------------- + // unreal_skeleton + //--------------------------------------------------------------------------------------------------------------------- + HAPI_AttributeInfo UnrealSkeletonInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoName); + UnrealSkeletonInfo.count = Part.pointCount; + UnrealSkeletonInfo.tupleSize = 1; + UnrealSkeletonInfo.exists = true; + UnrealSkeletonInfo.owner = HAPI_ATTROWNER_POINT; + UnrealSkeletonInfo.storage = HAPI_STORAGETYPE_STRING; + UnrealSkeletonInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, HAPI_UNREAL_ATTRIB_SKELETON, &UnrealSkeletonInfo), false); + + + Accessor.Init(NewNodeId, 0, HAPI_UNREAL_ATTRIB_SKELETON); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(UnrealSkeletonInfo, UnrealSkeletonPaths), false); + + //--------------------------------------------------------------------------------------------------------------------- + // Time + //--------------------------------------------------------------------------------------------------------------------- + HAPI_AttributeInfo TimeInfo; + FHoudiniApi::AttributeInfo_Init(&TimeInfo); + TimeInfo.count = PrimitiveCount; + TimeInfo.tupleSize = 1; + TimeInfo.exists = true; + TimeInfo.owner = HAPI_ATTROWNER_PRIM; + TimeInfo.storage = HAPI_STORAGETYPE_FLOAT; + TimeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, "time", &TimeInfo), false); + + //Position Data + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData(FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, "time", &TimeInfo, (float*)TimeData.GetData(), 0, TimeInfo.count), false); + + //--------------------------------------------------------------------------------------------------------------------- + // FrameIndex + //--------------------------------------------------------------------------------------------------------------------- + HAPI_AttributeInfo FrameIndexInfo; + FHoudiniApi::AttributeInfo_Init(&FrameIndexInfo); + FrameIndexInfo.count = PrimitiveCount; + FrameIndexInfo.tupleSize = 1; + FrameIndexInfo.exists = true; + FrameIndexInfo.owner = HAPI_ATTROWNER_PRIM; + FrameIndexInfo.storage = HAPI_STORAGETYPE_INT; + FrameIndexInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + "frame", &FrameIndexInfo), false); + + //Position Data + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeIntData( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, "frame", &FrameIndexInfo, + (int32*)FrameIndexData.GetData(), 0, FrameIndexInfo.count), false); + + //--------------------------------------------------------------------------------------------------------------------- + // LocalTransform (4x4) + //--------------------------------------------------------------------------------------------------------------------- + + HAPI_AttributeInfo LocalTransformInfo; + FHoudiniApi::AttributeInfo_Init(&LocalTransformInfo); + LocalTransformInfo.count = Part.pointCount; + LocalTransformInfo.tupleSize = 1; + LocalTransformInfo.exists = true; + LocalTransformInfo.owner = HAPI_ATTROWNER_POINT; + LocalTransformInfo.storage = HAPI_STORAGETYPE_FLOAT_ARRAY; + LocalTransformInfo.originalOwner = HAPI_ATTROWNER_INVALID; + LocalTransformInfo.totalArrayElements = LocalTransformData.Num(); + LocalTransformInfo.typeInfo = HAPI_AttributeTypeInfo::HAPI_ATTRIBUTE_TYPE_MATRIX3; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + "in_localtransform", &LocalTransformInfo), false); + + TArray SizesLocalTransformArray; + for (int i = 0; i < Part.pointCount; i++) + { + SizesLocalTransformArray.Add(4 * 4); + } + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatArrayData(FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, "in_localtransform", &LocalTransformInfo, LocalTransformData.GetData(), + LocalTransformData.Num(), SizesLocalTransformArray.GetData(), 0, SizesLocalTransformArray.Num()), false); + + //--------------------------------------------------------------------------------------------------------------------- + // WorldTransform3x3 + //--------------------------------------------------------------------------------------------------------------------- + + HAPI_AttributeInfo WorldTransformInfo; + FHoudiniApi::AttributeInfo_Init(&WorldTransformInfo); + WorldTransformInfo.count = Part.pointCount; + WorldTransformInfo.tupleSize = 1; + WorldTransformInfo.exists = true; + WorldTransformInfo.owner = HAPI_ATTROWNER_POINT; + WorldTransformInfo.storage = HAPI_STORAGETYPE_FLOAT_ARRAY; + WorldTransformInfo.originalOwner = HAPI_ATTROWNER_INVALID; + WorldTransformInfo.totalArrayElements = WorldTransformData.Num(); + WorldTransformInfo.typeInfo = HAPI_AttributeTypeInfo::HAPI_ATTRIBUTE_TYPE_MATRIX3; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), NewNodeId, + 0, "in_transform", &WorldTransformInfo), false); + + TArray SizesWorldTransformArray; + for (int i = 0; i < Part.pointCount; i++) + { + SizesWorldTransformArray.Add(3 * 3); + } + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatArrayData(FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, "in_transform", &WorldTransformInfo, WorldTransformData.GetData(), + WorldTransformData.Num(), SizesWorldTransformArray.GetData(), 0, SizesWorldTransformArray.Num()), false); + + //--------------------------------------------------------------------------------------------------------------------- + // fbx_custom_attributes + //--------------------------------------------------------------------------------------------------------------------- + HAPI_AttributeInfo CustomAttrsInfo; + FHoudiniApi::AttributeInfo_Init(&CustomAttrsInfo); + CustomAttrsInfo.count = Part.pointCount; + CustomAttrsInfo.tupleSize = 1; + CustomAttrsInfo.exists = true; + CustomAttrsInfo.owner = HAPI_ATTROWNER_POINT; + CustomAttrsInfo.storage = HAPI_STORAGETYPE_DICTIONARY; + CustomAttrsInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + "fbx_custom_attributes", &CustomAttrsInfo), false); + + Accessor.Init(NewNodeId, 0, "fbx_custom_attributes"); + Accessor.SetAttributeDictionary(CustomAttrsInfo, FbxCustomAttributes); + + + //--------------------------------------------------------------------------------------------------------------------- + // clipinfo + //--------------------------------------------------------------------------------------------------------------------- + + // Output the JSON to a String + const float AnimDuration = AnimSequence->GetPlayLength(); + const float FrameRateDecimal = FrameRate.AsDecimal(); + + FString ClipInfoString = TEXT( R"({ "name":"{0}", "range":[{1}, {2}], "rate":{3}, "source_range":[{4}, {5}], "source_rate":{6} })" ); + ClipInfoString = FString::Format(*ClipInfoString, { + AnimSequence->GetName(), // name + 0.f, // range[0] + AnimDuration, // range[1] + FrameRateDecimal, // rate + 0.f, // source_range[0] + AnimDuration, // source_range[1] + FrameRateDecimal // source_rate + }); + + HAPI_AttributeInfo ClipInfo; + FHoudiniApi::AttributeInfo_Init(&ClipInfo); + ClipInfo.count = 1; + ClipInfo.tupleSize = 1; + ClipInfo.exists = true; + ClipInfo.owner = HAPI_ATTROWNER_DETAIL; + ClipInfo.storage = HAPI_STORAGETYPE_DICTIONARY; + ClipInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, "clipinfo", &ClipInfo), false); + + TArray ClipInfoData = { ClipInfoString }; + + Accessor.Init(NewNodeId, 0, "clipinfo"); + Accessor.SetAttributeDictionary(ClipInfo, ClipInfoData);; + +#endif + return true; +} + + + +``` + +`Source/HoudiniEngine/Private/UnrealAnimationTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "UObject/NameTypes.h" + +class UAnimSequence; +class FUnrealObjectInputHandle; +struct FReferenceSkeleton; + +struct HOUDINIENGINE_API FUnrealAnimationTranslator +{ + public: + static bool CreateInputNodeForAnimation( + UAnimSequence* AnimSequence, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + const bool& bInputNodesCanBeDeleted); + + static bool HapiCreateInputNodeForAnimation( + UAnimSequence* Animation, + HAPI_NodeId& InputObjectNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + bool ExportAllLODs, + bool ExportSockets, + bool ExportColliders, + bool bInputNodesCanBeDeleted); + + static bool SetAnimationDataOnNode( + UAnimSequence* Animation, + HAPI_NodeId& NewNodeId); + + static bool AddBoneTracksToNode(HAPI_NodeId& NewNodeId, UAnimSequence* Animation); + + static void GetComponentSpaceTransforms(TArray& OutResult, const FReferenceSkeleton& InRefSkeleton); + static FTransform GetCompSpaceTransformForBone(const FReferenceSkeleton& InSkel, int32 InBoneIdx); + static FString GetBonePathForBone(const FReferenceSkeleton& InSkel, int32 InBoneIdx); + static FTransform GetCompSpacePoseTransformForBone(const TArray& Bones, const FReferenceSkeleton& InSkel, int32 InBoneIdx); + static FTransform GetCompSpacePoseTransformForBoneMap(const TMap& BoneMap, const FReferenceSkeleton& InSkel, int32 InBoneIdx); + +}; + +``` + +`Source/HoudiniEngine/Private/UnrealBrushTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealBrushTranslator.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniInputObject.h" + +#include "HoudiniGeoPartObject.h" +#include "Model.h" +#include "Engine/Polys.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputUtils.h" +#include "UnrealObjectInputRuntimeUtils.h" + +#include "HoudiniEngineRuntimeUtils.h" + +// Includes for Brush building code. Remove when the code is in the correct place. +#include "HCsgUtils.h" +#include "ActorEditorUtils.h" +#include "Misc/ScopedSlowTask.h" + +#include "Engine/Level.h" + +// TODO: Fix this +// This is currently being included to get access to the CreateFaceMaterialArray / DeleteFaceMaterialArray methods. +#include "HoudiniEngineAttributes.h" +#include "UnrealMeshTranslator.h" + +DEFINE_LOG_CATEGORY_STATIC(LogBrushTranslator, Log, All); + +bool FUnrealBrushTranslator::CreateInputNodeForBrush( + UHoudiniInputBrush* InputBrushObject, + ABrush* BrushActor, + const TArray>* ExcludeActors, + HAPI_NodeId& InputNodeId, + const FString& NodeName, + bool bInExportMaterialParametersAsAttributes, + FUnrealObjectInputHandle& OutHandle, + const bool& bInputNodesCanBeDeleted) +{ + if (!IsValid(BrushActor)) + return false; + + if (!IsValid(BrushActor->Brush)) + return false; + + if (InputBrushObject->ShouldIgnoreThisInput()) + return false; + + FString FinalInputNodeName = NodeName; + + FUnrealObjectInputIdentifier Identifier; + FUnrealObjectInputHandle ParentHandle; + HAPI_NodeId ParentNodeId = -1; + + { + const FUnrealObjectInputOptions Options; + Identifier = FUnrealObjectInputIdentifier(BrushActor, Options, true); + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId)) + { + if (!bInputNodesCanBeDeleted) + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + + OutHandle = Handle; + InputNodeId = NodeId; + return true; + } + } + + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted)) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // Set InputNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that InputNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (Handle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, InputNodeId)) + InputNodeId = -1; + } + else + { + InputNodeId = -1; + } + } + + HAPI_NodeId InputObjectNodeId = -1; + + if (FHoudiniEngineUtils::IsHoudiniNodeValid(InputNodeId)) + { + InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(InputNodeId); + } + else + { + HAPI_NodeId NewNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateInputNode(FinalInputNodeName, NewNodeId, ParentNodeId), false); + + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(NewNodeId)) + return false; + + InputNodeId = NewNodeId; + InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(InputNodeId); + + HAPI_NodeId CleanNodeId; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(InputObjectNodeId, TEXT("clean"), TEXT("clean"), true, &CleanNodeId), false); + + // Connect input node to the clean node + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(FHoudiniEngine::Get().GetSession(), CleanNodeId, 0, InputNodeId, 0), false); + + // Set display flag on the clean node + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetNodeDisplay(FHoudiniEngine::Get().GetSession(), CleanNodeId, 1), false); + } + + // Transform for positions + const FTransform ActorTransform = BrushActor->GetActorTransform(); + const FTransform ActorTransformInverse = ActorTransform.Inverse(); + // Precompute matrices for Normal transformations (see FPlane::TransformBy) + const FMatrix NmlInvXform = ActorTransformInverse.ToMatrixWithScale().TransposeAdjoint(); + float NScale = ActorTransformInverse.GetDeterminant() < 0 ? -1.f : 1.f; + + //-------------------------------------------------------------------------------------------------- + // Find actors that intersect with the given brush. + //-------------------------------------------------------------------------------------------------- + TArray BrushActors; + UHoudiniInputBrush::FindIntersectingSubtractiveBrushes(InputBrushObject, BrushActors); + + UModel* BrushModel = UHCsgUtils::BuildModelFromBrushes(BrushActors); + InputBrushObject->UpdateCachedData(BrushModel, BrushActors); + + // DEBUG: Upload the level model (baked by UE) to Houdini + // ULevel* Level = BrushActor->GetTypedOuter(); + // BrushModel = Level->Model; + + int NumPoints = BrushModel->Points.Num(); + if (NumPoints == 0) + { + // The content has changed and now we don't have geo to output. + // Be sure to clean up existing nodes in Houdini. + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), InputObjectNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input OBJ node for %s."), *(BrushActor->GetActorNameOrLabel())); + } + + InputNodeId = -1; + return true; + } + + //-------------------------------------------------------------------------------------------------- + // Construct the face count buffer. Also count the vertex indices in required to define the Part. + //-------------------------------------------------------------------------------------------------- + + int NumIndices = 0; + TArray FaceCountBuffer; + + { + // Calculate the size of the vertex buffer and the base vertex index of each node. + TArray& Nodes = BrushModel->Nodes; + //TArray& Surfs = BrushModel->Surfs; + TArray& Verts = BrushModel->Verts; + + int32 NumNodes = Nodes.Num(); + + FaceCountBuffer.SetNumUninitialized(NumNodes); + // Build the face counts buffer by iterating over the BSP nodes. + for(int32 NodeIndex = 0; NodeIndex < NumNodes; NodeIndex++) + { + FBspNode& Node = Nodes[NodeIndex]; + FaceCountBuffer[NodeIndex] = Node.NumVertices; + NumIndices += Node.NumVertices; + } + } + + //-------------------------------------------------------------------------------------------------- + // Apply actor transform + //-------------------------------------------------------------------------------------------------- + + if (!ActorTransform.Equals(FTransform::Identity)) + { + // convert to HAPI_Transform + HAPI_TransformEuler HapiTransform; + FHoudiniApi::TransformEuler_Init(&HapiTransform); + FHoudiniEngineUtils::TranslateUnrealTransform(ActorTransform, HapiTransform); + + // Set the transform on the OBJ parent + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetObjectTransform( + FHoudiniEngine::Get().GetSession(), InputObjectNodeId, &HapiTransform), false); + } + + //-------------------------------------------------------------------------------------------------- + // Start processing the geo and add it to the input node + //-------------------------------------------------------------------------------------------------- + + // Create part. + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + Part.vertexCount = NumIndices; + Part.faceCount = FaceCountBuffer.Num(); + Part.pointCount = NumPoints; + Part.type = HAPI_PARTTYPE_MESH; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo(FHoudiniEngine::Get().GetSession(), InputNodeId, 0, &Part), false); + + // ----------------------------- + // Vector - Point Attribute Info + // ----------------------------- + HAPI_AttributeInfo AttributeInfoPointVector; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPointVector); + AttributeInfoPointVector.count = NumPoints; + AttributeInfoPointVector.tupleSize = 3; + AttributeInfoPointVector.exists = true; + AttributeInfoPointVector.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPointVector.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPointVector.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPointVector), false); + + // ----------------------------- + // Vector - Vertex Attribute Info + // ----------------------------- + HAPI_AttributeInfo AttributeInfoVertexVector; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertexVector); + AttributeInfoVertexVector.count = NumIndices; + AttributeInfoVertexVector.tupleSize = 3; + AttributeInfoVertexVector.exists = true; + AttributeInfoVertexVector.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertexVector.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertexVector.originalOwner = HAPI_ATTROWNER_INVALID; + + // ----------------------------- + // POSITION (P) + // ----------------------------- + + { + TArray OutPosition; + FVector3f Scale = FVector3f(1.f, 1.f, 1.f); // TODO: Extract from actor transform. + OutPosition.SetNum(NumPoints); + + for (int32 PosIndex = 0; PosIndex < NumPoints; ++PosIndex) + { + FVector3f Point = BrushModel->Points[PosIndex]; + Point = (FVector3f)ActorTransform.InverseTransformPosition((FVector3d)Point); + FVector3f Pos(Point.X, Point.Z, Point.Y); + OutPosition[PosIndex] = Pos/HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + + // Upload point positions. + FHoudiniHapiAccessor Accessor(InputNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPointVector, (const float*)OutPosition.GetData()), false); + } + + //--------------------------------------------------------------------------------------------------------------------- + // INDICES (VertexList), NORMALS, UVS + //--------------------------------------------------------------------------------------------------------------------- + // Vertex buffer processing logic based on UModel::BuildVertexBuffers(). + { + // Calculate the size of the vertex buffer and the base vertex index of each node. + TArray& Positions = BrushModel->Points; + TArray& Nodes = BrushModel->Nodes; + TArray& Surfs = BrushModel->Surfs; + TArray& Verts = BrushModel->Verts; + TArray& Vectors = BrushModel->Vectors; + + int32 NumNodes = Nodes.Num(); + TArray Indices; + TArray OutNormals; + TArray OutUV; + TArray MaterialIndices; + TMap MaterialMap; + + Indices.SetNumUninitialized(NumIndices); + OutNormals.SetNumUninitialized(NumIndices); + OutUV.SetNumUninitialized(NumIndices); + + MaterialIndices.SetNumUninitialized(NumNodes); + + // Populate the vertex index buffer. + int32 iVertex = 0; + for(int32 NodeIndex = 0; NodeIndex < NumNodes; NodeIndex++) + { + FBspNode& Node = Nodes[NodeIndex]; + FBspSurf& Surf = Surfs[Node.iSurf]; + for (int32 NodeVertexIndex = 0; NodeVertexIndex < Node.NumVertices; ++NodeVertexIndex) + { + // Vertex Index + Indices[iVertex] = Verts[Node.iVertPool + NodeVertexIndex].pVertex; + + // Normal + FVector3d N = (FVector3d)Vectors[Surf.vNormal]; + N = NmlInvXform.TransformVector(N).GetSafeNormal(); + + OutNormals[iVertex] = FVector3f(N.X, N.Z, N.Y); + + // UVs + FVector3f& vU = Vectors[Surf.vTextureU]; + FVector3f& vV = Vectors[Surf.vTextureV]; + FVector3f deltaVtx = (Positions[Indices[iVertex]] - Positions[Surf.pBase]); + float U = FVector3f::DotProduct(deltaVtx, vU) / UModel::GetGlobalBSPTexelScale(); + float V = -FVector3f::DotProduct(deltaVtx, vV) / UModel::GetGlobalBSPTexelScale(); + OutUV[iVertex] = FVector3f(U, V, 0.f); + ++iVertex; + } + + // Face Material + // Construct a material index array for the faces + int32 MaterialIndex = MaterialMap.FindOrAdd(Surf.Material, MaterialMap.Num()); + MaterialIndices[NodeIndex] = MaterialIndex; + } + + // Set the vertex index buffer + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetVertexList( + Indices, InputNodeId, 0), false); + + // Set the face counts as per the BSP nodes. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetFaceCounts( + FaceCountBuffer, InputNodeId, 0), false); + + // ----------------------------- + // Normal attribute + // ----------------------------- + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + HAPI_UNREAL_ATTRIB_NORMAL, &AttributeInfoVertexVector), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, HAPI_UNREAL_ATTRIB_NORMAL); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertexVector, (const float*)OutNormals.GetData()), false); + + // ----------------------------- + // UV attribute + // ----------------------------- + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + HAPI_UNREAL_ATTRIB_UV, &AttributeInfoVertexVector), false); + + Accessor.Init(InputNodeId, 0, HAPI_UNREAL_ATTRIB_UV); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertexVector, (const float*)OutUV.GetData()), false); + + // ----------------------------- + // Material attribute + // ----------------------------- + + TArray Materials; + + if (MaterialMap.Num() > 0) + { + // Sort the output material in the correct order (by material index) + MaterialMap.ValueSort([](int32 A, int32 B) { return A < B; }); + + // Set the value in the correct order + // Do not reduce the array of materials, this could cause crahses in some weird cases.. + /*if (MaterialMap.Num() > MaterialInterfaces.Num()) + MaterialInterfaces.SetNumZeroed(MaterialMap.Num());*/ + Materials.SetNum(MaterialMap.Num()); + + int32 MaterialIndex = 0; + for (auto Kvp : MaterialMap) + { + Materials[MaterialIndex++] = Kvp.Key; + } + } + + // List of materials, one for each face. + FHoudiniEngineIndexedStringMap OutMaterials; + + //Lists of material parameters + TMap> ScalarMaterialParameters; + TMap> VectorMaterialParameters; + TMap TextureMaterialParameters; + TMap> BoolMaterialParameters; + + bool bAttributeSuccess = false; + if (bInExportMaterialParametersAsAttributes) + { + // Create attributes for the material and all its parameters + // Get material attribute data, and all material parameters data + FUnrealMeshTranslator::CreateFaceMaterialArray( + Materials, + MaterialIndices, + OutMaterials, + ScalarMaterialParameters, + VectorMaterialParameters, + TextureMaterialParameters, + BoolMaterialParameters); + } + else + { + // Create attributes only for the materials + // Only get the material attribute data + FUnrealMeshTranslator::CreateFaceMaterialArray( + Materials, MaterialIndices, OutMaterials); + } + + // Create all the needed attributes for materials + bAttributeSuccess = FUnrealMeshTranslator::CreateHoudiniMeshAttributes( + InputNodeId, + 0, + NumNodes, + OutMaterials, + MaterialIndices, + ScalarMaterialParameters, + VectorMaterialParameters, + TextureMaterialParameters, + BoolMaterialParameters); + + if (!bAttributeSuccess) + return false; + } + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(InputNodeId), false); + + { + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, InputNodeId, Handle, InputObjectNodeId, nullptr, bInputNodesCanBeDeleted)) + OutHandle = Handle; + } + + return true; +} + +``` + +`Source/HoudiniEngine/Private/UnrealBrushTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "UObject/NameTypes.h" + +class ABrush; +class AActor; +class UModel; +class UHoudiniInputBrush; +class ABrush; +class AActor; +class FUnrealObjectInputHandle; + +struct HOUDINIENGINE_API FUnrealBrushTranslator +{ + public: + static bool CreateInputNodeForBrush( + UHoudiniInputBrush* InputBrushObject, + ABrush* BrushActor, + const TArray>* ExcludeActors, + HAPI_NodeId &CreatedNodeId, + const FString& NodeName, + bool bInExportMaterialParametersAsAttributes, + FUnrealObjectInputHandle& OutInputNodeHandle, + const bool& bInputNodesCanBeDeleted); +}; +``` + +`Source/HoudiniEngine/Private/UnrealDataTableTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealDataTableTranslator.h" + +#include "UObject/TextProperty.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniInputObject.h" + +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputUtils.h" +#include "UnrealObjectInputRuntimeUtils.h" +#include "HoudiniEngineRuntimeUtils.h" + +#include "Engine/DataTable.h" + +namespace +{ + template + TArray + PopulateTArray(TMap::TConstIterator It, + uint32 NumRows, + uint32 NumComponents, + uint32 Offset, + uint32 ComponentSize, + const TArray& Order) + { + TArray Values; + Values.Reserve(NumRows * NumComponents); + for (; It; ++It) + { + const uint8* Data = It.Value(); + for (uint32 Idx = 0; Idx < NumComponents; ++Idx) + { + Values.Add(P::GetPropertyValue(Data + Offset + Order[Idx] * ComponentSize)); + } + + } + return Values; + } + + template + TArray + PopulateTArray(TMap::TConstIterator It, + uint32 NumRows, + uint32 NumComponents, + uint32 Offset, + uint32 ComponentSize) + { + TArray Order; + Order.Reserve(NumComponents); + for (uint32 Idx = 0; Idx < NumComponents; ++Idx) + { + Order.Add(Idx); + } + return PopulateTArray(It, NumRows, NumComponents, Offset, ComponentSize, Order); + } + + TArray + PopulateBoolArray(TMap::TConstIterator It, + const FBoolProperty& Prop, + uint32 Count, + uint32 Offset) + { + TArray Values; + Values.Reserve(Count); + for (; It; ++It) + { + const uint8* Data = It.Value(); + Values.Add(Prop.GetPropertyValue(Data + Offset)); + } + return Values; + } +}; + +bool FUnrealDataTableTranslator::CreateInputNodeForDataTable( + UDataTable* DataTable, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + const bool& bInputNodesCanBeDeleted) +{ + FString FinalInputNodeName = InputNodeName; + + FUnrealObjectInputIdentifier Identifier; + FUnrealObjectInputHandle ParentHandle; + HAPI_NodeId ParentNodeId = -1; + + { + const FUnrealObjectInputOptions Options; + Identifier = FUnrealObjectInputIdentifier(DataTable, Options, true); + + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId)) + { + if (!bInputNodesCanBeDeleted) + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + + OutHandle = Handle; + InputNodeId = NodeId; + return true; + } + } + + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // Set InputNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that InputNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (Handle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, InputNodeId)) + InputNodeId = -1; + } + else + { + InputNodeId = -1; + } + } + + // Create the input node + const UScriptStruct* ScriptStruct = DataTable->GetRowStruct(); + if (!IsValid(ScriptStruct)) + return true; + + TArray RowNames = DataTable->GetRowNames(); + TArray ColTitles = DataTable->GetUniqueColumnTitles(); + TArray NiceNames = DataTable->GetColumnTitles(); + + int32 NumRows = RowNames.Num(); + int32 NumAttributes = ColTitles.Num(); + if (NumRows <= 0 || NumAttributes <= 0) + return true; + + HAPI_NodeId NewNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateInputNode(FinalInputNodeName, NewNodeId, ParentNodeId), false); + + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(NewNodeId)) + return false; + + HAPI_NodeId PreviousInputNodeId = InputNodeId; + InputNodeId = NewNodeId; + HAPI_NodeId InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(NewNodeId); + + if (PreviousInputNodeId >= 0) + { + HAPI_NodeId PreviousInputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousInputNodeId); + + if (FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), PreviousInputNodeId) != HAPI_RESULT_SUCCESS) + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input node for %s."), *FinalInputNodeName); + + if (FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), PreviousInputObjectNodeId) != HAPI_RESULT_SUCCESS) + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input object node for %s."), *FinalInputNodeName); + } + + // Create a part + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = NumAttributes; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + Part.vertexCount = 0; + Part.faceCount = 0; + Part.pointCount = NumRows; + Part.type = HAPI_PARTTYPE_MESH; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, &Part), false); + + { + // Create point attribute info for P. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = NumRows; + AttributeInfoPoint.tupleSize = 3; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint), false); + + // Set the point's position + TArray Positions; + Positions.SetNum(NumRows * 3); + for (int32 RowIdx = 0; RowIdx < NumRows; RowIdx++) + { + Positions[RowIdx * 3] = 0.0f; + Positions[RowIdx * 3 + 1] = (float)RowIdx; + Positions[RowIdx * 3 + 2] = 0.0f; + } + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPoint, Positions), false); + } + + { + // Create point attribute info for the path. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = NumRows; + AttributeInfoPoint.tupleSize = 1; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + HAPI_UNREAL_ATTRIB_OBJECT_PATH, &AttributeInfoPoint), false); + + // Get the object path + FString ObjectPathName = DataTable->GetPathName(); + + // Set the point's path attribute + FHoudiniHapiAccessor Accessor(InputNodeId, 0, HAPI_UNREAL_ATTRIB_OBJECT_PATH); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoPoint, ObjectPathName), false); + } + + { + // Create point attribute info for data table RowTable class name + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = NumRows; + AttributeInfoPoint.tupleSize = 1; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWSTRUCT, &AttributeInfoPoint), false); + + // Get the object path +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + FString RowStructName = DataTable->GetRowStructPathName().ToString(); +#else + FString RowStructName = DataTable->RowStruct->GetPathName(); +#endif + + // Set the point's path attribute + FHoudiniHapiAccessor Accessor(InputNodeId, 0, HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWSTRUCT); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoPoint, RowStructName), false); + } + + { + // Manually set the attribute for the unique RowName. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = NumRows; + AttributeInfoPoint.tupleSize = 1; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWNAME, &AttributeInfoPoint), false); + + TArray Names; + Names.Reserve(NumRows); + for (auto KV : DataTable->GetRowMap()) + { + Names.Add(KV.Key.ToString()); + } + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, HAPI_UNREAL_ATTRIB_DATA_TABLE_ROWNAME); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPoint, Names), false); + } + + // Now set the attributes values for each "point" of the data table + uint32 ColIdx = 0; + auto ColIter(ColTitles.CreateConstIterator()); + // Skip the name column. + ++ColIter; + for (; ColIter; ++ColIter) + { + ++ColIdx; + FString ColName = *ColIter; + FString NiceName = NiceNames[ColIdx]; + + // Validate struct variable names + NiceName = NiceName.Replace(TEXT(" "), TEXT("_")); + NiceName = NiceName.Replace(TEXT(":"), TEXT("_")); + + FString CurAttrName = TEXT(HAPI_UNREAL_ATTRIB_DATA_TABLE_PREFIX) + FString::FromInt(ColIdx) + TEXT("_") + NiceName; + if (FHoudiniEngineUtils::SanitizeHAPIVariableName(CurAttrName)) + { + FString OldName = TEXT(HAPI_UNREAL_ATTRIB_DATA_TABLE_PREFIX) + FString::FromInt(ColIdx) + TEXT("_") + NiceName; + HOUDINI_LOG_WARNING(TEXT("[HapiCreateInputNodeForDataTable]: Invalid row name (%s) renamed to %s."), *OldName, *CurAttrName); + } + + FProperty* ColumnProp = ScriptStruct->FindPropertyByName(FName(*ColName)); + if (!ColumnProp) + { + HOUDINI_LOG_WARNING(TEXT("[HapiCreateInputNodeForDataTable]: Could not find property %s in struct %s."), *ColName, *ScriptStruct->GetFName().ToString()); + continue; + } + // Create a point attribute info + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = NumRows; + // Set tuple size depending on prop type. + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_POINT; + // Set the storage type depending on prop type. + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + AttributeInfo.typeInfo = HAPI_ATTRIBUTE_TYPE_NONE; + + uint32 Offset = ColumnProp->GetOffset_ForInternal(); + TMap::TConstIterator It = DataTable->GetRowMap().CreateConstIterator(); + + // Identify the type of the Property + bool bIsBoolProperty = ColumnProp->IsA(); + bool bIsTextProperty = ColumnProp->IsA(); + + // For numeric properties, we want to treat enum separately to send them as string and not ints + FNumericProperty* NumProp = CastField(ColumnProp); + bool bIsNumericProperty = NumProp != nullptr ? !NumProp->IsEnum() : false; + bool bIsEnumProperty = NumProp != nullptr ? NumProp->IsEnum() : false; + + // Struct props + FStructProperty* StructProp = CastField(ColumnProp); + bool bIsStructProperty = StructProp != nullptr; + + // We need to get all values for that attribute + if (bIsBoolProperty) + { + AttributeInfo.tupleSize = 1; + AttributeInfo.storage = HAPI_STORAGETYPE_INT8; + TArray Col = PopulateBoolArray(It, *CastField(ColumnProp), NumRows, Offset); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + } + // Text treated separately because the method used for string fallback + // doesn't cleanly convert this + else if (bIsTextProperty) + { + AttributeInfo.tupleSize = 1; + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + TArray Col; + Col.Reserve(NumRows); + for (; It; ++It) + { + FText* TextValue = ColumnProp->ContainerPtrToValuePtr(It.Value()); + if (!TextValue) + { + HOUDINI_LOG_WARNING(TEXT("[HapiCreateInputNodeForDataTable]: Invalid transform value for property %s."), *ColumnProp->GetName()); + continue; + } + Col.Add(TextValue->ToString()); + } + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + } + else if (bIsNumericProperty && !bIsEnumProperty) + { + AttributeInfo.tupleSize = 1; + if (NumProp->IsInteger()) + { + if (NumProp->IsA()) + { + AttributeInfo.storage = HAPI_STORAGETYPE_UINT8; + TArray Col = PopulateTArray(It, NumRows, 1, Offset, ColumnProp->GetSize()); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + } + else if (NumProp->IsA()) + { + AttributeInfo.storage = HAPI_STORAGETYPE_INT16; + TArray Col = PopulateTArray(It, NumRows, 1, Offset, ColumnProp->GetSize()); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + + } + else if (NumProp->IsA()) + { + AttributeInfo.storage = HAPI_STORAGETYPE_INT8; + TArray Col = PopulateTArray(It, NumRows, 1, Offset, ColumnProp->GetSize()); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + } + else if (NumProp->IsA()) + { + AttributeInfo.storage = HAPI_STORAGETYPE_INT; + TArray Col = PopulateTArray(It, NumRows, 1, Offset, ColumnProp->GetSize()); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + } + // There is no uint16 datatype in Houdini, convert to int32 + else if (NumProp->IsA()) + { + AttributeInfo.storage = HAPI_STORAGETYPE_INT; + TArray Col = PopulateTArray(It, NumRows, 1, Offset, ColumnProp->GetSize()); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + + } + // There is no uint32 datatype in Houdini, convert to int64 + else if (NumProp->IsA()) + { + AttributeInfo.storage = HAPI_STORAGETYPE_INT64; + TArray Col = PopulateTArray(It, NumRows, 1, Offset, ColumnProp->GetSize()); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + } + // There is no uint64 datatype in Houdini, since there is + // no int128, just force a conversion to signed int64 + else if (NumProp->IsA()) + { + AttributeInfo.storage = HAPI_STORAGETYPE_INT64; + TArray Col = PopulateTArray(It, NumRows, 1, Offset, ColumnProp->GetSize()); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + } + // Default to signed int64. + else + { + AttributeInfo.storage = HAPI_STORAGETYPE_INT64; + TArray Col = PopulateTArray(It, NumRows, 1, Offset, ColumnProp->GetSize()); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + } + } + else if (NumProp->IsFloatingPoint()) + { + if (NumProp->IsA()) + { + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT; + TArray Col = PopulateTArray(It, NumRows, 1, Offset, ColumnProp->GetSize()); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + } + // Default to double precision floating point. + else + { + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT64; + TArray Col = PopulateTArray(It, NumRows, 1, Offset, ColumnProp->GetSize()); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + } + } + else + { + FString ext; + FString typ = ColumnProp->GetCPPMacroType(ext); + HOUDINI_LOG_WARNING(TEXT("[HapiCreateInputNodeForDataTable]: Unrecognized child of FNumericProperty: %s %s"), *typ, *ext); + continue; + } + } + else + { + if (bIsStructProperty) + { + FName StructName = StructProp->Struct->GetFName(); + + int32 NumComponents = -1; + HAPI_StorageType Storage = HAPI_STORAGETYPE_INVALID; + TArray Order; + if (StructName == NAME_Vector4f || StructName == NAME_LinearColor) + { + NumComponents = 4; + Storage = HAPI_STORAGETYPE_FLOAT; + } + else if (StructName == NAME_Vector4d || StructName == NAME_Vector4) + { + NumComponents = 4; + Storage = HAPI_STORAGETYPE_FLOAT64; + } + else if (StructName == NAME_Vector3f) + { + NumComponents = 3; + Storage = HAPI_STORAGETYPE_FLOAT; + } + else if (StructName == NAME_Rotator3f) + { + NumComponents = 3; + Storage = HAPI_STORAGETYPE_FLOAT; + // Roll, Pitch, Yaw + Order = { 1, 0, 2 }; + } + else if (StructName == NAME_Vector || StructName == NAME_Vector3d) + { + NumComponents = 3; + Storage = HAPI_STORAGETYPE_FLOAT64; + } + else if (StructName == NAME_Rotator || StructName == NAME_Rotator3d) + { + NumComponents = 3; + Storage = HAPI_STORAGETYPE_FLOAT64; + // Roll, Pitch, Yaw + Order = { 1, 0, 2 }; + } + else if (StructName == NAME_Vector2f) + { + NumComponents = 2; + Storage = HAPI_STORAGETYPE_FLOAT; + } + else if (StructName == NAME_Vector2d || StructName == NAME_Vector2D) + { + NumComponents = 2; + Storage = HAPI_STORAGETYPE_FLOAT64; + } + else if (StructName == NAME_Color) + { + NumComponents = 4; + Storage = HAPI_STORAGETYPE_UINT8; + // RGBA + Order = { 2, 1, 0, 3 }; + } + else if (StructName == NAME_Transform || StructName == NAME_Transform3d) + { + FTransform3d* Transform; + TArray RotValues; + TArray ScaleValues; + TArray TransValues; + RotValues.Reserve(4 * NumRows); + ScaleValues.Reserve(3 * NumRows); + TransValues.Reserve(3 * NumRows); + FString RotName = CurAttrName + TEXT("_rotation"); + FString ScaleName = CurAttrName + TEXT("_scale"); + FString TransName = CurAttrName + TEXT("_translate"); + for (; It; ++It) + { + Transform = ColumnProp->ContainerPtrToValuePtr(It.Value()); + if (!Transform) + { + HOUDINI_LOG_WARNING(TEXT("[HapiCreateInputNodeForDataTable]: Invalid transform value for property %s."), *ColumnProp->GetName()); + continue; + } + const FQuat4d& Rot = Transform->GetRotation(); + RotValues.Add(Rot.X); + RotValues.Add(Rot.Y); + RotValues.Add(Rot.Z); + RotValues.Add(Rot.W); + const FVector& Scale = Transform->GetScale3D(); + ScaleValues.Add(Scale.X); + ScaleValues.Add(Scale.Y); + ScaleValues.Add(Scale.Z); + const FVector& Trans = Transform->GetTranslation(); + TransValues.Add(Trans.X); + TransValues.Add(Trans.Y); + TransValues.Add(Trans.Z); + } + + AttributeInfo.tupleSize = 4; + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT64; + // Rotation + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*RotName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor; + Accessor.Init(InputNodeId, 0, TCHAR_TO_ANSI(*RotName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, RotValues), false); + + AttributeInfo.tupleSize = 3; + + // Scale + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*ScaleName), &AttributeInfo), false); + + Accessor.Init(InputNodeId, 0, TCHAR_TO_ANSI(*ScaleName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, ScaleValues), false); + + // Translate + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*TransName), &AttributeInfo), false); + + Accessor.Init(InputNodeId, 0, TCHAR_TO_ANSI(*TransName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, TransValues), false); + + continue; + } + else if (StructName == NAME_Transform3f) + { + FTransform3f* Transform; + TArray RotValues; + TArray ScaleValues; + TArray TransValues; + RotValues.Reserve(4 * NumRows); + ScaleValues.Reserve(3 * NumRows); + TransValues.Reserve(3 * NumRows); + for (; It; ++It) + { + Transform = ColumnProp->ContainerPtrToValuePtr(It.Value()); + if (!Transform) + { + HOUDINI_LOG_WARNING(TEXT("[HapiCreateInputNodeForDataTable]: Invalid transform value for property %s."), *ColumnProp->GetName()); + continue; + } + const FQuat4f& Rot = Transform->GetRotation(); + RotValues.Add(Rot.X); + RotValues.Add(Rot.Y); + RotValues.Add(Rot.Z); + RotValues.Add(Rot.W); + const FVector3f& Scale = Transform->GetScale3D(); + ScaleValues.Add(Scale.X); + ScaleValues.Add(Scale.Y); + ScaleValues.Add(Scale.Z); + const FVector3f& Trans = Transform->GetTranslation(); + TransValues.Add(Trans.X); + TransValues.Add(Trans.Y); + TransValues.Add(Trans.Z); + } + + AttributeInfo.tupleSize = 4; + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT; + // Rotation + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName + *TEXT("_rotation")), &AttributeInfo), false); + + FString Name = CurAttrName + TEXT("_rotation"); + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*Name)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, RotValues), false); + + AttributeInfo.tupleSize = 3; + // Scale + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName + *TEXT("_scale")), &AttributeInfo), false); + + Name = CurAttrName + *TEXT("_scale"); + Accessor.Init(InputNodeId, 0, TCHAR_TO_ANSI(*Name)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, ScaleValues), false); + + // Translate + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName + *TEXT("_translate")), &AttributeInfo), false); + + Name = CurAttrName + TEXT("_translate"); + Accessor.Init(InputNodeId, 0, TCHAR_TO_ANSI(*Name)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, TransValues), false); + + continue; + } + + if (NumComponents != -1 && Storage != HAPI_STORAGETYPE_INVALID) + { + AttributeInfo.tupleSize = NumComponents; + AttributeInfo.storage = Storage; + // Bytes + if (Storage == HAPI_STORAGETYPE_UINT8) + { + TArray Col = Order.Num() == 0 ? PopulateTArray(It, NumRows, NumComponents, Offset, 1) : PopulateTArray(It, NumRows, NumComponents, Offset, 1, Order); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + continue; + } + // Floats + else if (Storage == HAPI_STORAGETYPE_FLOAT) + { + TArray Col = Order.Num() == 0 ? PopulateTArray(It, NumRows, NumComponents, Offset, 4) : PopulateTArray(It, NumRows, NumComponents, Offset, 4, Order); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + continue; + } + // Doubles + else if (Storage == HAPI_STORAGETYPE_FLOAT64) + { + TArray Col = Order.Num() == 0 ? PopulateTArray(It, NumRows, NumComponents, Offset, 8) : PopulateTArray(It, NumRows, NumComponents, Offset, 8, Order); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + + continue; + } + // Something else + else + { + HOUDINI_LOG_WARNING(TEXT("[HapiCreateInputNodeForDataTable]: Invalid component size %d for property %s."), ColumnProp->GetSize(), *ColumnProp->GetName()); + continue; + } + } + } + // Fallback to string if no match + AttributeInfo.tupleSize = 1; + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + TArray Col; + Col.Reserve(NumRows); + for (; It; ++It) + { + Col.Add(DataTableUtils::GetPropertyValueAsString(ColumnProp, It.Value(), EDataTableExportFlags())); + } + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InputNodeId, 0, + TCHAR_TO_ANSI(*CurAttrName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(InputNodeId, 0, TCHAR_TO_ANSI(*CurAttrName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Col), false); + } + } + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(InputNodeId), false); + if (!FHoudiniEngineUtils::HapiCookNode(InputNodeId, nullptr, true)) + return false; + + { + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, InputNodeId, Handle, InputObjectNodeId, nullptr, bInputNodesCanBeDeleted)) + OutHandle = Handle; + } + + return true; +} + +``` + +`Source/HoudiniEngine/Private/UnrealDataTableTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "UObject/NameTypes.h" + +class UDataTable; +class FUnrealObjectInputHandle; + +struct HOUDINIENGINE_API FUnrealDataTableTranslator +{ + public: + static bool CreateInputNodeForDataTable( + UDataTable* DataTable, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + const bool& bInputNodesCanBeDeleted); +}; + +``` + +`Source/HoudiniEngine/Private/UnrealFoliageTypeTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealFoliageTypeTranslator.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "FoliageType_InstancedStaticMesh.h" +#include "HoudiniGenericAttribute.h" +#include "HoudiniInputTranslator.h" +#include "UnrealObjectInputRuntimeTypes.h" + +#include "Engine/StaticMesh.h" +#include "Components/StaticMeshComponent.h" + + +bool +FUnrealFoliageTypeTranslator::HapiCreateInputNodeForFoliageType_InstancedStaticMesh( + UFoliageType_InstancedStaticMesh* InFoliageType, + HAPI_NodeId& InputObjectNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + const bool& ExportAllLODs, + const bool& ExportSockets, + const bool& ExportColliders, + bool ExportMaterialParameters) +{ + if (!IsValid(InFoliageType)) + return false; + + UStaticMesh* const InputSM = InFoliageType->GetStaticMesh(); + if (!IsValid(InputSM)) + return false; + + UStaticMeshComponent* const StaticMeshComponent = nullptr; + FUnrealObjectInputHandle InputNodeHandle; + + FUnrealMeshExportOptions ExportOptions; + ExportOptions.bLODs = ExportAllLODs; + ExportOptions.bSockets = ExportSockets; + ExportOptions.bColliders = ExportColliders; + ExportOptions.bMainMesh = true; + ExportOptions.bMaterialParameters = ExportMaterialParameters; + ExportOptions.bPreferNaniteFallbackMesh = false; + + bool bSuccess = CreateInputNodeForStaticMesh( + InputObjectNodeId, + InputNodeHandle, + InputSM, + StaticMeshComponent, + InputNodeName, + ExportOptions, + true, + false); + + if (bSuccess) + { + OutHandle = InputNodeHandle; + const int32 PartId = 0; + CreateHoudiniFoliageTypeAttributes(InFoliageType, InputObjectNodeId, PartId, HAPI_ATTROWNER_DETAIL); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(InputObjectNodeId), false); + } + + return bSuccess; +} + +bool FUnrealFoliageTypeTranslator::CreateInputNodeForReference( + HAPI_NodeId& InInputNodeId, + UFoliageType* InFoliageType, + const FString& InInputNodeName, + const FTransform& InTransform, + const bool& bImportAsReferenceRotScaleEnabled, + FUnrealObjectInputHandle& OutHandle, + const bool& bInputNodesCanBeDeleted, + const bool& bImportAsReferenceBboxEnabled, + const FBox& InBbox, + const bool& bImportAsReferenceMaterialEnabled, + const TArray& MaterialReferences) +{ + if (!IsValid(InFoliageType)) + return true; + + UObject* ObjectToRef = nullptr; + if (UFoliageType_InstancedStaticMesh const* const SMFoliageType = Cast(InFoliageType)) + { + ObjectToRef = SMFoliageType->GetStaticMesh(); + } + else + { + return false; + } + + if (!IsValid(ObjectToRef)) + return true; + + const bool bSuccess = FHoudiniInputTranslator::CreateInputNodeForReference( + InInputNodeId, + ObjectToRef, + InInputNodeName, + InTransform, + bImportAsReferenceRotScaleEnabled, + OutHandle, + bInputNodesCanBeDeleted, + bImportAsReferenceBboxEnabled, + InBbox, + bImportAsReferenceMaterialEnabled, + MaterialReferences); + + if (!bSuccess) + return false; + + constexpr int32 PartId = 0; + if (CreateHoudiniFoliageTypeAttributes(InFoliageType, InInputNodeId, PartId, HAPI_ATTROWNER_POINT)) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(InInputNodeId), false); + return true; + } + + return false; +} + +bool +FUnrealFoliageTypeTranslator::CreateHoudiniFoliageTypeAttributes(UFoliageType* InFoliageType, const int32& InNodeId, const int32& InPartId, HAPI_AttributeOwner InAttributeOwner) +{ + if (InNodeId < 0) + return false; + + bool bSuccess = true; + + // Create attribute for unreal_foliage + HAPI_AttributeInfo AttributeInfoUnrealFoliage; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoUnrealFoliage); + AttributeInfoUnrealFoliage.tupleSize = 1; + AttributeInfoUnrealFoliage.count = 1; + AttributeInfoUnrealFoliage.exists = true; + AttributeInfoUnrealFoliage.owner = InAttributeOwner; + AttributeInfoUnrealFoliage.storage = HAPI_STORAGETYPE_INT; + AttributeInfoUnrealFoliage.originalOwner = HAPI_ATTROWNER_INVALID; + + // Create the new attribute + if (HAPI_RESULT_SUCCESS == FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, HAPI_UNREAL_ATTRIB_FOLIAGE_INSTANCER, &AttributeInfoUnrealFoliage)) + { + // The New attribute has been successfully created, set its value + int UnrealFoliage = 1; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::SetAttributeIntData( + FHoudiniEngine::Get().GetSession(), + InNodeId, InPartId, HAPI_UNREAL_ATTRIB_FOLIAGE_INSTANCER, &AttributeInfoUnrealFoliage, + &UnrealFoliage, 0, 1)) + { + bSuccess = false; + } + } + + if (!bSuccess) + return false; + + // Foliage type properties that should be sent to Houdini as unreal_uproperty_ attributes. + static TArray FoliageTypePropertyNames({ + // float + // FName("Density"), + // FName("DensityAdjustmentFactor"), + // FName("Radius"), + // FName("SingleInstanceModeRadius"), + FName("AlignMaxAngle"), + FName("RandomPitchAngle"), + FName("MinimumLayerWeight"), + FName("MinimumExclusionLayerWeight"), + FName("CollisionRadius"), + FName("ShadeRadius"), + FName("InitialSeedDensity"), + FName("AverageSpreadDistance"), + FName("SpreadVariance"), + FName("MaxInitialSeedOffset"), + FName("MaxInitialAge"), + FName("MaxAge"), + FName("OverlapPriority"), + + // bool + // FName("bSingleInstanceModeOverrideRadius"), + FName("bCanGrowInShade"), + FName("bSpawnsInShade"), + + // int32 + FName("OverriddenLightMapRes"), + FName("CustomDepthStencilValue"), + FName("TranslucencySortPriority"), + FName("NumSteps"), + FName("SeedsPerStep"), + FName("DistributionSeed"), + FName("ChangeCount"), + FName("VirtualTextureCullMips"), + + // uint32 + FName("AlignToNormal"), + FName("RandomYaw"), + FName("CollisionWithWorld"), + FName("CastShadow"), + FName("bAffectDynamicIndirectLighting"), + FName("bAffectDistanceFieldLighting"), + FName("bCastDynamicShadow"), + FName("bCastStaticShadow"), + FName("bCastShadowAsTwoSided"), + FName("bReceivesDecals"), + FName("bOverrideLightMapRes"), + FName("bUseAsOccluder"), + FName("bRenderCustomDepth"), + FName("ReapplyDensity"), + FName("ReapplyRadius"), + FName("ReapplyAlignToNormal"), + FName("ReapplyRandomYaw"), + FName("ReapplyScaling"), + FName("ReapplyScaleX"), + FName("ReapplyScaleY"), + FName("ReapplyScaleZ"), + FName("ReapplyRandomPitchAngle"), + FName("ReapplyGroundSlope"), + FName("ReapplyHeight"), + FName("ReapplyLandscapeLayers"), + FName("ReapplyZOffset"), + FName("ReapplyCollisionWithWorld"), + FName("ReapplyVertexColorMask"), + FName("bEnableDensityScaling"), + FName("bEnableDiscardOnLoad"), + + // enums + // FName("Scaling"), + FName("LightmapType"), + + // FFloatInterval + // FName("ScaleX"), + // FName("ScaleY"), + // FName("ScaleZ"), + FName("ZOffset"), + FName("GroundSlopeAngle"), + FName("Height"), + FName("ProceduralScale"), + + // FVector + FName("CollisionScale"), + FName("LowBoundOriginRadius")}); + + EAttribOwner AttribOwner; + switch (InAttributeOwner) + { + case HAPI_ATTROWNER_POINT: + AttribOwner = EAttribOwner::Point; + break; + case HAPI_ATTROWNER_VERTEX: + AttribOwner = EAttribOwner::Vertex; + break; + case HAPI_ATTROWNER_PRIM: + AttribOwner = EAttribOwner::Prim; + break; + case HAPI_ATTROWNER_DETAIL: + AttribOwner = EAttribOwner::Detail; + break; + case HAPI_ATTROWNER_INVALID: + case HAPI_ATTROWNER_MAX: + default: + HOUDINI_LOG_WARNING(TEXT("Unsupported Attribute Owner: %d"), InAttributeOwner); + return false; + } + FHoudiniGenericAttribute GenericAttribute; + GenericAttribute.AttributeCount = 1; + GenericAttribute.AttributeOwner = AttribOwner; + + // Reserve enough space in the arrays (we only have a single point (or all are detail attributes), so attribute + // count is 1, but the tuple size could be up to 10 for transforms + GenericAttribute.DoubleValues.Reserve(10); + GenericAttribute.IntValues.Reserve(10); + GenericAttribute.StringValues.Reserve(10); + + for (const FName& PropertyName : FoliageTypePropertyNames) + { + const FString PropertyNameStr = PropertyName.ToString(); + GenericAttribute.AttributeName = FString::Printf(TEXT("unreal_uproperty_%s"), *PropertyNameStr); + // Find the property on the foliage type instance + FProperty* FoundProperty = nullptr; + UObject* FoundPropertyObject = nullptr; + void* Container = nullptr; + FEditPropertyChain FoundPropertyChain; + bool bExactPropertyFound = false; + if (!FHoudiniGenericAttribute::FindPropertyOnObject( + InFoliageType, + PropertyNameStr, + FoundPropertyChain, + FoundProperty, + FoundPropertyObject, + Container, + bExactPropertyFound, + false)) + continue; + + if (!FHoudiniGenericAttribute::GetAttributeTupleSizeAndStorageFromProperty( + InFoliageType, + FoundProperty, + Container, + GenericAttribute.AttributeTupleSize, + GenericAttribute.AttributeType)) + continue; + + const int32 AtIndex = 0; + if (!FHoudiniGenericAttribute::GetPropertyValueFromObject( + InFoliageType, + FoundProperty, + Container, + GenericAttribute, + AtIndex)) + continue; + + FHoudiniEngineUtils::SetGenericPropertyAttribute(InNodeId, InPartId, GenericAttribute); + } + + return bSuccess; +} + +``` + +`Source/HoudiniEngine/Private/UnrealFoliageTypeTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" + +#include "CoreMinimal.h" +#include "UObject/ObjectMacros.h" + +#include "UnrealMeshTranslator.h" + +class UFoliageType; +class UFoliageType_InstancedStaticMesh; +class UStaticMeshComponent; + +struct HOUDINIENGINE_API FUnrealFoliageTypeTranslator : public FUnrealMeshTranslator +{ +public: + // HAPI : Marshaling, extract geometry and create input asset for it - return true on success + static bool HapiCreateInputNodeForFoliageType_InstancedStaticMesh( + UFoliageType_InstancedStaticMesh* InFoliageType, + HAPI_NodeId& InputObjectNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + const bool& ExportAllLODs = false, + const bool& ExportSockets = false, + const bool& ExportColliders = false, + bool ExportMaterialParameters = false); + + // Create an input node that references the asset via InRef (unreal_instance). + // Also calls CreateHoudiniFoliageTypeAttributes, to create the unreal_foliage attribute, as well as + // unreal_uproperty_ attributes for the foliage type settings. + static bool CreateInputNodeForReference( + HAPI_NodeId& InInputNodeId, + UFoliageType* InFoliageType, + const FString& InInputNodeName, + const FTransform& InTransform, + const bool& bImportAsReferenceRotScaleEnabled, + FUnrealObjectInputHandle& OutHandle, + const bool& bInputNodesCanBeDeleted, + const bool& bImportAsReferenceBboxEnabled = false, + const FBox& InBbox = FBox(EForceInit::ForceInit), + const bool& bImportAsReferenceMaterialEnabled = false, + const TArray& MaterialReferences = TArray()); + +protected: + // Creates the unreal_foliage and unreal_uproperty_ attributes for the foliage type. + static bool CreateHoudiniFoliageTypeAttributes( + UFoliageType* InFoliageType, const int32& InNodeId, const int32& InPartId, HAPI_AttributeOwner InAttributeOwner); +}; + +``` + +`Source/HoudiniEngine/Private/UnrealGeometryCollectionTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealGeometryCollectionTranslator.h" +#include "UnrealMeshTranslator.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEnginePrivatePCH.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputUtils.h" +#include "UnrealObjectInputRuntimeUtils.h" + +#include "Animation/AttributesContainer.h" +#include "EditorFramework/AssetImportData.h" +#include "Engine/SkeletalMesh.h" + +#include "GeometryCollection/GeometryCollection.h" +#include "GeometryCollection/GeometryCollectionClusteringUtility.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollection/GeometryCollectionComponent.h" + #include "GeometryCollection/GeometryCollectionObject.h" +#else + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionComponent.h" + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionObject.h" +#endif + +#include "HoudiniEngineAttributes.h" +#include "Materials/Material.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "MaterialDomain.h" +#endif + +bool +FUnrealGeometryCollectionTranslator::HapiCreateInputNodeForGeometryCollection( + UGeometryCollection* GeometryCollection, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + bool bExportMaterialParameters, + UGeometryCollectionComponent* GeometryCollectionComponent, + const bool& bInputNodesCanBeDeleted) +{ + // If we don't have a static mesh there's nothing to do. + if (!IsValid(GeometryCollection)) + return false; + + // Input node name, defaults to InputNodeName, but can be changed by the new input system + FString FinalInputNodeName = InputNodeName; + + // Find the node in new input system + // Identifier will be the identifier for the entry created in this call of the function. We may call this function + // recursively to create the main mesh, LODs, sockets and colliders, each getting its own identifier. + FUnrealObjectInputIdentifier Identifier; + FUnrealObjectInputHandle ParentHandle; + HAPI_NodeId ParentNodeId = -1; + { + // Creates this input's identifier and input options + FUnrealObjectInputOptions Options; + Options.bExportMaterialParameters = bExportMaterialParameters; + Identifier = FUnrealObjectInputIdentifier(GeometryCollection, Options, true); + + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId) && FHoudiniEngineUtils::IsHoudiniNodeValid(NodeId)) + { + // Make sure to prevent the destruction of the node if needed + if (!bInputNodesCanBeDeleted) + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + + OutHandle = Handle; + InputNodeId = NodeId; + return true; + } + } + + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + // Create any parent/container nodes that we would need, and get the node id of the immediate parent + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // We now need to create the nodes (since we couldn't find existing ones in the manager) + // To do that, we can simply continue this function + + // Set InputNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that InputNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (Handle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, InputNodeId)) + InputNodeId = -1; + } + else + { + InputNodeId = -1; + } + } + + // Node ID for the newly created node + HAPI_NodeId PackNodeId = -1; + FString PackName = FString::Printf(TEXT("%s_pack"), *FinalInputNodeName); + + if (ParentNodeId < 0) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + -1, TEXT("SOP/pack"), InputNodeName, true, &PackNodeId), false); + } + else + { + // When creating a node inside a parent node (in other words, ParentNodeId is not -1), then we cannot + // specify the node type category prefix on the node name. We have to create the geo Object and pack + // SOPs separately. + HAPI_NodeId ObjectNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + ParentNodeId, TEXT("geo"), PackName, true, &ObjectNodeId), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + ObjectNodeId, TEXT("pack"), PackName, true, &PackNodeId), false); + } + + // Check if we have a valid id for this new input asset. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(PackNodeId)) + return false; + + HAPI_NodeId PreviousInputNodeId = InputNodeId; + + // Update our input NodeId + // Get our parent OBJ NodeID + HAPI_NodeId InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(PackNodeId); + + // We have now created a valid new input node, delete the previous one + if (PreviousInputNodeId >= 0) + { + // Get the parent OBJ node ID before deleting! + HAPI_NodeId PreviousInputOBJNode = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousInputNodeId); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input node for %s."), *InputNodeName); + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputOBJNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input OBJ node for %s."), *InputNodeName); + } + } + + HAPI_NodeId MergeNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + InputObjectNodeId, TEXT("merge"), InputNodeName, true, &MergeNodeId), false); + + // Check if we have a valid id for this new input asset. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(MergeNodeId)) + return false; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + PackNodeId, 0, MergeNodeId, 0), false); + + UploadGeometryCollection(GeometryCollection, InputObjectNodeId, InputNodeName, MergeNodeId, bExportMaterialParameters, GeometryCollectionComponent); + + // Setup the pack node to create packed primitives. + { + const char * PackByNameOption = HAPI_UNREAL_PARAM_PACK_BY_NAME; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), PackNodeId, + PackByNameOption, 0, 1), false); + } + + { + const char * PackedFragmentsOption = HAPI_UNREAL_PARAM_PACKED_FRAGMENTS; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), PackNodeId, + PackedFragmentsOption, 0, 0), false); + } + + if (!FHoudiniEngineUtils::HapiCookNode(PackNodeId, nullptr, true)) + return false; + + InputNodeId = PackNodeId; + + { + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, InputNodeId, Handle, InputObjectNodeId, nullptr, bInputNodesCanBeDeleted)) + OutHandle = Handle; + } + + return true; +} + +bool +FUnrealGeometryCollectionTranslator::SetGeometryCollectionAttributesForPart( + UGeometryCollection* InGeometryCollection, int32 InGeometryIndex, HAPI_NodeId InNodeId, FString InName) +{ + const HAPI_PartId PartId = 0; + + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::GetPartInfo( + FHoudiniEngine::Get().GetSession(), InNodeId, PartId, &PartInfo), false); + + // Name attribute + { + // Create point attribute info. + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = PartInfo.faceCount; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InNodeId, PartId, + HAPI_ATTRIB_NAME, &AttributeInfo), false); + + // Now that we have raw positions, we can upload them for our attribute. + + FHoudiniHapiAccessor Accessor(InNodeId, 0, HAPI_ATTRIB_NAME); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfo, InName), false); + } + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(InNodeId), false); + + return true; +} + +bool +FUnrealGeometryCollectionTranslator::UploadGeometryCollection( + UGeometryCollection* GeometryCollectionObject, + HAPI_NodeId InParentNodeId, + FString InName, + HAPI_NodeId InMergeNodeId, + bool bInExportMaterialParametersAsAttributes, + UGeometryCollectionComponent * GeometryCollectionComponent) +{ + if (!IsValid(GeometryCollectionObject)) + { + return false; + } + + // Level -> ( ParentId -> ClusterLevel ) + TMap> LevelToClusterArray; + TMap LevelToNewClusterIndex; + + TSharedPtr GeometryCollectionPtr = GeometryCollectionObject->GetGeometryCollection(); + FGeometryCollection* GeometryCollection = GeometryCollectionPtr.Get(); + check(GeometryCollection); + + // vertex information + TManagedArray& Vertex = GeometryCollection->Vertex; + TManagedArray& TangentU = GeometryCollection->TangentU; + TManagedArray& TangentV = GeometryCollection->TangentV; + TManagedArray& Normal = GeometryCollection->Normal; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + // UE5.2 has removed the ability to directly access UVs +#else + TArray& UV = GeometryCollection->UVs[0]; +#endif + TManagedArray& Color = GeometryCollection->Color; + TManagedArray& BoneMap = GeometryCollection->BoneMap; + TManagedArray& BoneColor = GeometryCollection->BoneColor; + TManagedArray& BoneName = GeometryCollection->BoneName; + + TManagedArray& Indices = GeometryCollection->Indices; + TManagedArray& Visible = GeometryCollection->Visible; + TManagedArray& MaterialID = GeometryCollection->MaterialID; + TManagedArray& MaterialIndex = GeometryCollection->MaterialIndex; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + TManagedArray& Transform = GeometryCollection->Transform; +#else + TManagedArray& Transform = GeometryCollection->Transform; +#endif + TManagedArray& Parent = GeometryCollection->Parent; + TManagedArray& SimulationType = GeometryCollection->SimulationType; + + TManagedArray& TransformIndex = GeometryCollection->TransformIndex; + TManagedArray& BoundingBox = GeometryCollection->BoundingBox; + TManagedArray& InnerRadius = GeometryCollection->InnerRadius; + TManagedArray& OuterRadius = GeometryCollection->OuterRadius; + TManagedArray& VertexStartArray = GeometryCollection->VertexStart; + TManagedArray& VertexCountArray = GeometryCollection->VertexCount; + TManagedArray& FaceStartArray = GeometryCollection->FaceStart; + TManagedArray& FaceCountArray = GeometryCollection->FaceCount; + + TManagedArray& TransformToGeometryIndexArray = GeometryCollection->TransformToGeometryIndex; + + TManagedArray& Sections = GeometryCollection->Sections; + + // Need to update hierarchy level otherwise sometimes level would be out of date! + FGeometryCollectionClusteringUtility::UpdateHierarchyLevelOfChildren(GeometryCollection, -1); + + if (Sections.Num() == 0) + { + HOUDINI_LOG_ERROR(TEXT("No triangles in mesh")); + return false; + } + + for (int32 ObjectIndex = 0; ObjectIndex < GeometryCollection->TransformToGeometryIndex.Num(); ObjectIndex++) + { + int32& GeometryIndex = TransformToGeometryIndexArray[ObjectIndex]; + if (GeometryIndex == -1) + { + continue; + } + + int32& VertexStart = VertexStartArray[GeometryIndex]; + int32& VertexCount = VertexCountArray[GeometryIndex]; + int32& FaceStart = FaceStartArray[GeometryIndex]; + int32& FaceCount = FaceCountArray[GeometryIndex]; + + HAPI_NodeId GeometryNodeId = -1; + FString OutputName = FString::Printf( TEXT("gc_piece_%d"), ObjectIndex); + // Create the node in this input object's OBJ node + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + InParentNodeId, TEXT("null"), OutputName, false, &GeometryNodeId), false); + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + + Part.vertexCount = FaceCount * 3; // In GC, "FaceCount" == number of indices. + Part.faceCount = FaceCount; //1; ///FaceCountArray // FaceCount; //FaceCount / 3; // TODO GC: In GC indices count == faces count. This is not the same in Regular meshes. Double check this. + Part.pointCount = VertexCount; + Part.type = HAPI_PARTTYPE_MESH; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), GeometryNodeId, 0, &Part), false); + + //--------------------------------------------------------------------------------------------------------------------- + // POSITION (P) + //--------------------------------------------------------------------------------------------------------------------- + if (VertexCount > 3) + { + + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = VertexCount; + AttributeInfoPoint.tupleSize = 3; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), GeometryNodeId, 0, + HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint), false); + + TArray StaticMeshVertices; + StaticMeshVertices.SetNumZeroed(VertexCount * 3); + for (int32 VertexIdx = 0; VertexIdx < VertexCount; ++VertexIdx) + { + int32 IndexInArray = VertexStart + VertexIdx; + const FVector3f& PositionVector = Vertex[IndexInArray]; + + // Convert Unreal to Houdini + StaticMeshVertices[VertexIdx * 3 + 0] = PositionVector.X / HAPI_UNREAL_SCALE_FACTOR_POSITION; + StaticMeshVertices[VertexIdx * 3 + 1] = PositionVector.Z / HAPI_UNREAL_SCALE_FACTOR_POSITION; + StaticMeshVertices[VertexIdx * 3 + 2] = PositionVector.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + + // Now that we have raw positions, we can upload them for our attribute. + FHoudiniHapiAccessor Accessor(GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPoint, StaticMeshVertices), false); + + } + + TArray MaterialInterfaces; + TArray TriangleMaterialIndices; + + int32 UEDefaultMaterialIndex = INDEX_NONE; + + const int32 NumMaterials = GeometryCollectionObject->Materials.Num(); + + if (NumMaterials > 0) + { + for (int32 Index = 0; Index < NumMaterials; ++Index) + { + UMaterialInterface* CurrMaterial = GeometryCollectionObject->Materials[Index]; + + // Possible we have a null entry - replace with default + if (!IsValid(CurrMaterial)) + { + CurrMaterial = UMaterial::GetDefaultMaterial(MD_Surface); + UEDefaultMaterialIndex = Index; + } + + MaterialInterfaces.Add(CurrMaterial); + } + + TriangleMaterialIndices.Reserve(Part.faceCount); + } + + // Setup for vertex instance attributes + int32 HoudiniVertexIdx = 0; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + const bool HasUVs = GeometryCollection->NumUVLayers() > 0; +#else + const bool HasUVs = UV.Num() == Vertex.Num(); +#endif + TArray UVs; + if (HasUVs) + UVs.SetNumZeroed(Part.vertexCount * 3); + + const bool HasNormals = Normal.Num() == Vertex.Num(); + TArray Normals; + if (HasNormals) + Normals.SetNumZeroed(Part.vertexCount * 3); + + const bool HasTangentU = TangentU.Num() == Vertex.Num(); + TArray Tangents; + if (HasTangentU) + Tangents.SetNumZeroed(Part.vertexCount * 3); + + const bool HasTangentV = TangentV.Num() == Vertex.Num(); + TArray Binormals; + if (HasTangentV) + Binormals.SetNumZeroed(Part.vertexCount * 3); + + TArray RGBColors; + TArray Alphas; + const bool HasColor = Color.Num() == Vertex.Num(); + if (HasColor) + { + RGBColors.SetNumZeroed(Part.vertexCount * 3); + Alphas.SetNumZeroed(Part.vertexCount); + } + + TArray MeshTriangleVertexIndices; + MeshTriangleVertexIndices.SetNumUninitialized(Part.vertexCount); + + // For each face: + for (int32 i = 0; i < FaceCount; i++) + { + const FIntVector FaceIndices = Indices[FaceStart + i]; + + for (int32 TriangleVertexIndex = 0; TriangleVertexIndex < 3; TriangleVertexIndex++) + { + // Swap second and third vertices due to winding + const int32 WindingIdx = (3 - TriangleVertexIndex) % 3; + int32 VertexIndex = FaceIndices[WindingIdx]; + + // Calculate the index of the first component of a vertex instance's value in an inline float array + // representing vectors (3 float) per vertex instance + const int32 Float3Index = HoudiniVertexIdx * 3; + + //--------------------------------------------------------------------------------------------------------------------- + // UVS (uvX) + //--------------------------------------------------------------------------------------------------------------------- + if (HasUVs) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + FVector2f currentUV = GeometryCollection->GetUV(VertexIndex, 0); + UVs[Float3Index + 0] = currentUV.X; + UVs[Float3Index + 1] = 1 - currentUV.Y; + UVs[Float3Index + 2] = 0; +#else + UVs[Float3Index + 0] = UV[VertexIndex].X; + UVs[Float3Index + 1] = 1 - UV[VertexIndex].Y; + UVs[Float3Index + 2] = 0; +#endif + } + + //--------------------------------------------------------------------------------------------------------------------- + // NORMALS (N) + //--------------------------------------------------------------------------------------------------------------------- + + // Swapped in Z and Y due to axis conversion + if (HasNormals) + { + Normals[Float3Index + 0] = Normal[VertexIndex].X; + Normals[Float3Index + 1] = Normal[VertexIndex].Z; + Normals[Float3Index + 2] = Normal[VertexIndex].Y; + } + + //--------------------------------------------------------------------------------------------------------------------- + // TANGENT (tangentu) + //--------------------------------------------------------------------------------------------------------------------- + if (HasTangentU) + { + Tangents[Float3Index + 0] = TangentU[VertexIndex].X; + Tangents[Float3Index + 1] = TangentU[VertexIndex].Z; + Tangents[Float3Index + 2] = TangentU[VertexIndex].Y; + } + + //--------------------------------------------------------------------------------------------------------------------- + // BINORMAL (tangentv) + //--------------------------------------------------------------------------------------------------------------------- + if (HasTangentV) + { + Binormals[Float3Index + 0] = TangentV[VertexIndex].X; + Binormals[Float3Index + 1] = TangentV[VertexIndex].Z; + Binormals[Float3Index + 2] = TangentV[VertexIndex].Y; + } + + //--------------------------------------------------------------------------------------------------------------------- + // COLORS (Cd) + //--------------------------------------------------------------------------------------------------------------------- + if (HasColor) + { + RGBColors[Float3Index + 0] = Color[VertexIndex].R; + RGBColors[Float3Index + 1] = Color[VertexIndex].G; + RGBColors[Float3Index + 2] = Color[VertexIndex].B; + Alphas[HoudiniVertexIdx] = Color[VertexIndex].A; + } + + //--------------------------------------------------------------------------------------------------------------------- + // TRIANGLE/FACE VERTEX INDICES + //--------------------------------------------------------------------------------------------------------------------- + MeshTriangleVertexIndices[HoudiniVertexIdx] = VertexIndex - VertexStart; + + HoudiniVertexIdx++; + } + + //--------------------------------------------------------------------------------------------------------------------- + // TRIANGLE MATERIAL ASSIGNMENT + //--------------------------------------------------------------------------------------------------------------------- + const int32 MatIndex = MaterialID[FaceStart + i]; + if (MaterialInterfaces.IsValidIndex(MatIndex)) + { + TriangleMaterialIndices.Add(MatIndex); + } + else + { + TriangleMaterialIndices.Add(UEDefaultMaterialIndex); + } + + } + + //--------------------------------------------------------------------------------------------------------------------- + // UVS (uvX) + //--------------------------------------------------------------------------------------------------------------------- + if (HasUVs) + { + // Create attribute for uvs. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = UVs.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_UV, &AttributeInfoVertex), false); + + FHoudiniHapiAccessor Accessor(GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_UV); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, UVs), false); + } + + //--------------------------------------------------------------------------------------------------------------------- + // NORMALS (N) + //--------------------------------------------------------------------------------------------------------------------- + if (HasNormals) + { + // Create attribute for normals. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = Normals.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_NORMAL, &AttributeInfoVertex), false); + + FHoudiniHapiAccessor Accessor(GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_NORMAL); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, Normals), false); + } + + //--------------------------------------------------------------------------------------------------------------------- + // TANGENT (tangentu) + //--------------------------------------------------------------------------------------------------------------------- + if (HasTangentU) + { + // Create attribute for tangentu. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = Tangents.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_TANGENTU, &AttributeInfoVertex), false); + + FHoudiniHapiAccessor Accessor(GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_TANGENTU); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, Tangents), false); + } + + //--------------------------------------------------------------------------------------------------------------------- + // BINORMAL (tangentv) + //--------------------------------------------------------------------------------------------------------------------- + if (HasTangentV) + { + // Create attribute for normals. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = Binormals.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_TANGENTV, &AttributeInfoVertex), false); + + FHoudiniHapiAccessor Accessor(GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_TANGENTV); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, Binormals), false); + } + + //--------------------------------------------------------------------------------------------------------------------- + // COLORS (Cd) + //--------------------------------------------------------------------------------------------------------------------- + if (HasColor) + { + // Create attribute for colors. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = RGBColors.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_COLOR, &AttributeInfoVertex), false); + + FHoudiniHapiAccessor Accessor(GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_COLOR); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, RGBColors), false); + + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + AttributeInfoVertex.tupleSize = 1; + AttributeInfoVertex.count = Alphas.Num(); + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_ALPHA, &AttributeInfoVertex), false); + + Accessor.Init(GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_ALPHA); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, Alphas), false); + } + + //--------------------------------------------------------------------------------------------------------------------- + // INDICES (VertexList) + //--------------------------------------------------------------------------------------------------------------------- + if (FaceCount > 0) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetVertexList( + MeshTriangleVertexIndices, GeometryNodeId, 0), false); + + // We need to generate array of face counts. + TArray StaticMeshFaceCounts; + StaticMeshFaceCounts.SetNumUninitialized(Part.faceCount); + for (int32 n = 0; n < Part.faceCount; n++) + StaticMeshFaceCounts[n] = 3; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetFaceCounts( + StaticMeshFaceCounts, GeometryNodeId, 0), false); + } + + // Materials - Reuse code from FHoudiniMeshTranslator + if (NumMaterials > 0) + { + // List of materials, one for each face. + FHoudiniEngineIndexedStringMap TriangleMaterials; + + //Lists of material parameters + TMap> ScalarMaterialParameters; + TMap> VectorMaterialParameters; + TMap TextureMaterialParameters; + TMap> BoolMaterialParameters; + + bool bAttributeSuccess = false; + if (bInExportMaterialParametersAsAttributes) + { + // Create attributes for the material and all its parameters + // Get material attribute data, and all material parameters data + FUnrealMeshTranslator::CreateFaceMaterialArray( + MaterialInterfaces, + TriangleMaterialIndices, + TriangleMaterials, + ScalarMaterialParameters, + VectorMaterialParameters, + TextureMaterialParameters, + BoolMaterialParameters); + } + else + { + // Create attributes only for the materials + // Only get the material attribute data + FUnrealMeshTranslator::CreateFaceMaterialArray( + MaterialInterfaces, TriangleMaterialIndices, TriangleMaterials); + } + + // Create all the needed attributes for materials + bAttributeSuccess = FUnrealMeshTranslator::CreateHoudiniMeshAttributes( + GeometryNodeId, + 0, + TriangleMaterials.GetIds().Num(), + TriangleMaterials, + TriangleMaterialIndices, + ScalarMaterialParameters, + VectorMaterialParameters, + TextureMaterialParameters, + BoolMaterialParameters); + + if (!bAttributeSuccess) + { + return false; + } + } + + // Geometry collection input attributes + //--------------------------------------------------------------------------------------------------------------------- + // name (required for packing) + //--------------------------------------------------------------------------------------------------------------------- + { + // Create point attribute info. + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = Part.faceCount; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), GeometryNodeId, Part.id, + HAPI_ATTRIB_NAME, &AttributeInfo), false); + + // Upload them for our attribute. + FHoudiniHapiAccessor Accessor(GeometryNodeId, Part.id, HAPI_ATTRIB_NAME); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfo, OutputName), false); + } + + + // Geometry collection input attributes + //--------------------------------------------------------------------------------------------------------------------- + // unreal_gc_piece (required for packing) + //--------------------------------------------------------------------------------------------------------------------- + int32 Level = 1; + if (GeometryCollection->HasAttribute("Level", FGeometryCollection::TransformGroup)) + { + const TManagedArray& Levels = GeometryCollection->GetAttribute("Level", FGeometryCollection::TransformGroup); + Level = Levels[GeometryIndex]; + } + + // If simulation type is none, then disable it + if (SimulationType[GeometryIndex] == FGeometryCollection::ESimulationTypes::FST_None) + { + Level = 0; + } + + { + HAPI_AttributeInfo AttributeInfoPrim; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPrim); + + AttributeInfoPrim.count = Part.faceCount; + AttributeInfoPrim.tupleSize = 1; + AttributeInfoPrim.exists = true; + AttributeInfoPrim.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoPrim.storage = HAPI_STORAGETYPE_INT; + AttributeInfoPrim.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_GC_PIECE, &AttributeInfoPrim), false); + + FHoudiniHapiAccessor Accessor(GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_GC_PIECE); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoPrim, Level), false); + } + + // Identify the cluster level using the parent indices: + + int32 ClusterIndex = -1; + TMap & ClusterMap = LevelToClusterArray.FindOrAdd(Level); + int32 ParentIndex = Parent[GeometryIndex]; + if (ParentIndex != FGeometryCollection::Invalid) + { + if (ClusterMap.Contains(ParentIndex)) + { + ClusterIndex = ClusterMap[ParentIndex]; + } + else + { + if (LevelToNewClusterIndex.Contains(Level)) + { + LevelToNewClusterIndex[Level]++; + ClusterIndex = LevelToNewClusterIndex[Level]; + } + else + { + ClusterIndex = 0; + LevelToNewClusterIndex.Add(Level, ClusterIndex); + } + + ClusterMap.Add(ParentIndex, ClusterIndex); + } + } + + // Add the unreal_gc_cluster attribute + { + HAPI_AttributeInfo AttributeInfoPrim; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPrim); + + AttributeInfoPrim.count = Part.faceCount; + AttributeInfoPrim.tupleSize = 1; + AttributeInfoPrim.exists = true; + AttributeInfoPrim.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoPrim.storage = HAPI_STORAGETYPE_INT; + AttributeInfoPrim.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_GC_CLUSTER_PIECE, &AttributeInfoPrim), false); + + FHoudiniHapiAccessor Accessor(GeometryNodeId, 0, HAPI_UNREAL_ATTRIB_GC_CLUSTER_PIECE); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoPrim, ClusterIndex), false); + } + + + AddGeometryCollectionDetailAttributes(GeometryCollectionObject, GeometryNodeId, Part.id, Part, InName, GeometryCollectionComponent); + + // Commit the geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(GeometryNodeId), false); + + // Connect the LOD node to the merge node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + InMergeNodeId, GeometryIndex, GeometryNodeId, 0), false); + + } + + return true; +} + +bool FUnrealGeometryCollectionTranslator::AddGeometryCollectionDetailAttributes( + UGeometryCollection* GeometryCollectionObject, HAPI_NodeId GeoId, HAPI_PartId PartId, HAPI_PartInfo & Part, const FString& InName, UGeometryCollectionComponent * GeometryCollectionComponent) +{ + + // unreal_gc_name + //--------------------------------------------------------------------------------------------------------------------- + { + // Create primitive attribute with mesh asset path + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = Part.faceCount; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, 0, HAPI_UNREAL_ATTRIB_GC_NAME, &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(GeoId, 0, HAPI_UNREAL_ATTRIB_GC_NAME); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfo, InName), false); + } + + // Clustering - Damage thresholds + // Damage thresholds are not yet available in vanilla 4.26. + if (GeometryCollectionObject->DamageThreshold.Num() > 0) + { + TArray AttributeData(GeometryCollectionObject->DamageThreshold); + + // We have one array with DamageThreshold.Num() elements. + TArray AttributeDataSizes; + AttributeDataSizes.SetNumUninitialized(GeometryCollectionObject->DamageThreshold.Num()); + for (int32 n = 0; n < AttributeDataSizes.Num(); n++) + AttributeDataSizes[n] = 0; + + AttributeDataSizes[0] = GeometryCollectionObject->DamageThreshold.Num(); + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.totalArrayElements = GeometryCollectionObject->DamageThreshold.Num(); + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT_ARRAY; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const char * AttributeName = HAPI_UNREAL_ATTRIB_GC_CLUSTERING_DAMAGE_THRESHOLD; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, AttributeName, &AttributeInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatArrayData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, AttributeName, &AttributeInfo, + (const float*)AttributeData.GetData(), AttributeInfo.totalArrayElements, (const int*)AttributeDataSizes.GetData(), 0, AttributeInfo.count), false); + } + + // Clustering - Cluster connection type + { + int32 ClusterConnectionTypeValue = 0; + EClusterConnectionTypeEnum ClusterConnectionType = GeometryCollectionObject->ClusterConnectionType; + if (ClusterConnectionType == EClusterConnectionTypeEnum::Chaos_PointImplicit) + { + ClusterConnectionTypeValue = 0; + } + else if (ClusterConnectionType == EClusterConnectionTypeEnum::Chaos_DelaunayTriangulation) + { + ClusterConnectionTypeValue = 0; // Chaos_DelaunayTriangulation not supported + } + else + { + ClusterConnectionTypeValue = (int32)ClusterConnectionType - 1; + } + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_INT; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const char * AttributeName = HAPI_UNREAL_ATTRIB_GC_CLUSTERING_CLUSTER_CONNECTION_TYPE; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, AttributeName, &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(GeoId, PartId, AttributeName); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfo, ClusterConnectionTypeValue), false); + } + + // Collisions - Mass as density + { + int32 MassAsDensity = GeometryCollectionObject->bMassAsDensity == true ? 1 : 0; + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_INT; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const char * AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MASS_AS_DENSITY; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), + GeoId, PartId, AttributeName, &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(GeoId, PartId, AttributeName); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfo, MassAsDensity), false); + } + + // Collisions - Mass + { + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const char * AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MASS; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, AttributeName, &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(GeoId, PartId, AttributeName); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfo, GeometryCollectionObject->Mass), false); + } + + // Collisions - Minimum Mass Clamp + { + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const char * AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MINIMUM_MASS_CLAMP; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, AttributeName, &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(GeoId, PartId, AttributeName); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfo, GeometryCollectionObject->MinimumMassClamp), false); + + } + + const TArray & GCSizeSpecDatas = GeometryCollectionObject->SizeSpecificData; + + // Size specific data input: + + // Collisions - Size specific data - Max size + if (GCSizeSpecDatas.Num() > 0) + { + TArray AttributeData; + AttributeData.SetNum(GCSizeSpecDatas.Num()); + for (int32 i = 0; i < AttributeData.Num(); i++) + { + AttributeData[i] = GCSizeSpecDatas[i].MaxSize; + } + + // We have one array with DamageThreshold.Num() elements. + TArray< int32 > AttributeDataSizes; + AttributeDataSizes.Init(0, AttributeData.Num()); + AttributeDataSizes[0] = AttributeData.Num(); + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.totalArrayElements = AttributeData.Num(); + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT_ARRAY; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const char * AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MAX_SIZE; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, AttributeName, &AttributeInfo), false); + + FHoudiniApi::SetAttributeFloatArrayData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, AttributeName, &AttributeInfo, + (const float *)AttributeData.GetData(), AttributeInfo.totalArrayElements, (const int *)AttributeDataSizes.GetData(), 0, AttributeInfo.count); + } + + // Collisions - Size specific data - Damage threshold + if (GCSizeSpecDatas.Num() > 0) + { + TArray AttributeData; + AttributeData.SetNum(GCSizeSpecDatas.Num()); + for (int32 i = 0; i < AttributeData.Num(); i++) + { + AttributeData[i] = GCSizeSpecDatas[i].DamageThreshold; + } + + // We have one array with DamageThreshold.Num() elements. + TArray< int32 > AttributeDataSizes; + AttributeDataSizes.Init(0, AttributeData.Num()); + AttributeDataSizes[0] = AttributeData.Num(); + + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.totalArrayElements = AttributeData.Num(); + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_INT_ARRAY; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const char * AttributeName = HAPI_UNREAL_ATTRIB_GC_COLLISIONS_DAMAGE_THRESHOLD; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, AttributeName, &AttributeInfo), false); + + FHoudiniApi::SetAttributeIntArrayData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, AttributeName, &AttributeInfo, + (const int *)AttributeData.GetData(), AttributeInfo.totalArrayElements, (const int *)AttributeDataSizes.GetData(), 0, AttributeInfo.count); + } + + for (int32 GCSizeSpecIdx = 0; GCSizeSpecIdx < GCSizeSpecDatas.Num(); GCSizeSpecIdx++) + { + const FGeometryCollectionSizeSpecificData & GCSizeSpecData = GCSizeSpecDatas[GCSizeSpecIdx]; + + // Collisions - Collision Type + if(GCSizeSpecData.CollisionShapes.Num() > 0) + { + TArray AttributeData; + AttributeData.SetNum(GCSizeSpecData.CollisionShapes.Num()); + for (int32 i = 0; i < AttributeData.Num(); i++) + { + AttributeData[i] = static_cast(GCSizeSpecData.CollisionShapes[i].CollisionType); + } + + TArray< int32 > AttributeDataSizes; + AttributeDataSizes.Init(0, AttributeData.Num()); + AttributeDataSizes[0] = AttributeData.Num(); + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.totalArrayElements = AttributeData.Num(); + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_INT_ARRAY; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const FString AttributeName = FString::Printf(TEXT("%s_%d"), TEXT(HAPI_UNREAL_ATTRIB_GC_COLLISIONS_COLLISION_TYPE), GCSizeSpecIdx); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeIntArrayData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo, + (const int32 *)AttributeData.GetData(), AttributeInfo.totalArrayElements, (const int *)AttributeDataSizes.GetData(), 0, AttributeInfo.count), false); + } + + // Collisions - Size specific data - Implicit Type + if (GCSizeSpecData.CollisionShapes.Num() > 0) + { + TArray AttributeData; + AttributeData.SetNum(GCSizeSpecData.CollisionShapes.Num()); + for (int32 i = 0; i < AttributeData.Num(); i++) + { + AttributeData[i] = (int32)GCSizeSpecData.CollisionShapes[i].CollisionType; + if (GCSizeSpecData.CollisionShapes[i].ImplicitType == EImplicitTypeEnum::Chaos_Implicit_None) + { + AttributeData[i] = 0; + } + else if (GCSizeSpecData.CollisionShapes[i].ImplicitType > EImplicitTypeEnum::Chaos_Implicit_None) + { + AttributeData[i] = static_cast(GCSizeSpecData.CollisionShapes[i].ImplicitType); // If past none, keep same indexing + } + else + { + AttributeData[i] = static_cast(GCSizeSpecData.CollisionShapes[i].ImplicitType) + 1; // + 1 because 0 = None. + } + } + + TArray< int32 > AttributeDataSizes; + AttributeDataSizes.Init(0, AttributeData.Num()); + AttributeDataSizes[0] = AttributeData.Num(); + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.totalArrayElements = AttributeData.Num(); + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_INT_ARRAY; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const FString AttributeName = FString::Printf(TEXT("%s_%d"), TEXT(HAPI_UNREAL_ATTRIB_GC_COLLISIONS_IMPLICIT_TYPE), GCSizeSpecIdx); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeIntArrayData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo, + (const int32 *)AttributeData.GetData(), AttributeInfo.totalArrayElements, (const int *)AttributeDataSizes.GetData(), 0, AttributeInfo.count), false); + } + + // Collisions - Size specific data - Min Level Set Resolution + if (GCSizeSpecData.CollisionShapes.Num() > 0) + { + TArray AttributeData; + AttributeData.SetNum(GCSizeSpecData.CollisionShapes.Num()); + for (int32 i = 0; i < AttributeData.Num(); i++) + { + AttributeData[i] = GCSizeSpecData.CollisionShapes[i].LevelSet.MinLevelSetResolution; + } + + TArray< int32 > AttributeDataSizes; + AttributeDataSizes.Init(0, AttributeData.Num()); + AttributeDataSizes[0] = AttributeData.Num(); + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.totalArrayElements = AttributeData.Num(); + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_INT_ARRAY; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const FString AttributeName = FString::Printf(TEXT("%s_%d"), TEXT(HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MIN_LEVEL_SET_RESOLUTION), GCSizeSpecIdx); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeIntArrayData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo, + (const int32 *)AttributeData.GetData(), AttributeInfo.totalArrayElements, (const int *)AttributeDataSizes.GetData(), 0, AttributeInfo.count), false); + } + + // Collisions - Size specific data - Max Level Set Resolution + if (GCSizeSpecData.CollisionShapes.Num() > 0) + { + TArray AttributeData; + AttributeData.SetNum(GCSizeSpecData.CollisionShapes.Num()); + for (int32 i = 0; i < AttributeData.Num(); i++) + { + AttributeData[i] = GCSizeSpecData.CollisionShapes[i].LevelSet.MaxLevelSetResolution; + } + + TArray< int32 > AttributeDataSizes; + AttributeDataSizes.Init(0, AttributeData.Num()); + AttributeDataSizes[0] = AttributeData.Num(); + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.totalArrayElements = AttributeData.Num(); + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_INT_ARRAY; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const FString AttributeName = FString::Printf(TEXT("%s_%d"), TEXT(HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MAX_LEVEL_SET_RESOLUTION), GCSizeSpecIdx); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeIntArrayData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo, + (const int32 *)AttributeData.GetData(), AttributeInfo.totalArrayElements, (const int *)AttributeDataSizes.GetData(), 0, AttributeInfo.count), false); + } + + // Collisions - Size specific data - Min cluster level set resolution + if (GCSizeSpecData.CollisionShapes.Num() > 0) + { + TArray AttributeData; + AttributeData.SetNum(GCSizeSpecData.CollisionShapes.Num()); + for (int32 i = 0; i < AttributeData.Num(); i++) + { + AttributeData[i] = GCSizeSpecData.CollisionShapes[i].LevelSet.MinClusterLevelSetResolution; + } + + TArray< int32 > AttributeDataSizes; + AttributeDataSizes.Init(0, AttributeData.Num()); + AttributeDataSizes[0] = AttributeData.Num(); + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.totalArrayElements = AttributeData.Num(); + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_INT_ARRAY; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const FString AttributeName = FString::Printf(TEXT("%s_%d"), TEXT(HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MIN_CLUSTER_LEVEL_SET_RESOLUTION), GCSizeSpecIdx); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeIntArrayData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo, + (const int32 *)AttributeData.GetData(), AttributeInfo.totalArrayElements, (const int *)AttributeDataSizes.GetData(), 0, AttributeInfo.count), false); + } + + // Collisions - Size specific data - Max cluster level set resolution + if (GCSizeSpecData.CollisionShapes.Num() > 0) + { + TArray AttributeData; + AttributeData.SetNum(GCSizeSpecData.CollisionShapes.Num()); + for (int32 i = 0; i < AttributeData.Num(); i++) + { + AttributeData[i] = GCSizeSpecData.CollisionShapes[i].LevelSet.MaxClusterLevelSetResolution; + } + + TArray< int32 > AttributeDataSizes; + AttributeDataSizes.Init(0, AttributeData.Num()); + AttributeDataSizes[0] = AttributeData.Num(); + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.totalArrayElements = AttributeData.Num(); + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_INT_ARRAY; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const FString AttributeName = FString::Printf(TEXT("%s_%d"), TEXT(HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MAX_CLUSTER_LEVEL_SET_RESOLUTION), GCSizeSpecIdx); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeIntArrayData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo, + (const int32 *)AttributeData.GetData(), AttributeInfo.totalArrayElements, (const int *)AttributeDataSizes.GetData(), 0, AttributeInfo.count), false); + } + + // Collisions - Size specific data - Object reduction percentage + if (GCSizeSpecData.CollisionShapes.Num() > 0) + { + TArray AttributeData; + AttributeData.SetNum(GCSizeSpecData.CollisionShapes.Num()); + for (int32 i = 0; i < AttributeData.Num(); i++) + { + AttributeData[i] = GCSizeSpecData.CollisionShapes[i].CollisionObjectReductionPercentage; + } + + TArray< int32 > AttributeDataSizes; + AttributeDataSizes.Init(0, AttributeData.Num()); + AttributeDataSizes[0] = AttributeData.Num(); + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.totalArrayElements = AttributeData.Num(); + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT_ARRAY; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const FString AttributeName = FString::Printf(TEXT("%s_%d"), TEXT(HAPI_UNREAL_ATTRIB_GC_COLLISIONS_COLLISION_OBJECT_REDUCTION_PERCENTAGE), GCSizeSpecIdx); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatArrayData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo, + (const float *)AttributeData.GetData(), AttributeInfo.totalArrayElements, (const int *)AttributeDataSizes.GetData(), 0, AttributeInfo.count), false); + } + + // Collisions - Size specific data - Size specific data - Collision margin fraction + if (GCSizeSpecData.CollisionShapes.Num() > 0) + { + TArray AttributeData; + AttributeData.SetNum(GCSizeSpecData.CollisionShapes.Num()); + for (int32 i = 0; i < AttributeData.Num(); i++) + { + AttributeData[i] = GCSizeSpecData.CollisionShapes[i].CollisionMarginFraction; + } + + TArray< int32 > AttributeDataSizes; + AttributeDataSizes.Init(0, AttributeData.Num()); + AttributeDataSizes[0] = AttributeData.Num(); + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.totalArrayElements = AttributeData.Num(); + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT_ARRAY; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const FString AttributeName = FString::Printf(TEXT("%s_%d"), TEXT(HAPI_UNREAL_ATTRIB_GC_COLLISIONS_COLLISION_MARGIN_FRACTION), GCSizeSpecIdx); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatArrayData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo, + (const float *)AttributeData.GetData(), AttributeInfo.totalArrayElements, (const int *)AttributeDataSizes.GetData(), 0, AttributeInfo.count), false); + } + + // Collisions - Size specific data - Collision particles fraction + { + TArray AttributeData; + AttributeData.SetNum(GCSizeSpecData.CollisionShapes.Num()); + for (int32 i = 0; i < AttributeData.Num(); i++) + { + AttributeData[i] = GCSizeSpecData.CollisionShapes[i].CollisionParticles.CollisionParticlesFraction; + } + + TArray< int32 > AttributeDataSizes; + AttributeDataSizes.Init(0, AttributeData.Num()); + AttributeDataSizes[0] = AttributeData.Num(); + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.totalArrayElements = AttributeData.Num(); + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT_ARRAY; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const FString AttributeName = FString::Printf(TEXT("%s_%d"), TEXT(HAPI_UNREAL_ATTRIB_GC_COLLISIONS_COLLISION_PARTICLES_FRACTION), GCSizeSpecIdx); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatArrayData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo, + (const float *)AttributeData.GetData(), AttributeInfo.totalArrayElements, (const int *)AttributeDataSizes.GetData(), 0, AttributeInfo.count), false); + } + + // Collisions - Size specific data - Maximum collision particles + { + TArray AttributeData; + AttributeData.SetNum(GCSizeSpecData.CollisionShapes.Num()); + for (int32 i = 0; i < AttributeData.Num(); i++) + { + AttributeData[i] = GCSizeSpecData.CollisionShapes[i].CollisionParticles.MaximumCollisionParticles; + } + + TArray< int32 > AttributeDataSizes; + AttributeDataSizes.Init(0, AttributeData.Num()); + AttributeDataSizes[0] = AttributeData.Num(); + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.totalArrayElements = AttributeData.Num(); + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfo.storage = HAPI_STORAGETYPE_INT_ARRAY; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + const FString AttributeName = FString::Printf(TEXT("%s_%d"), TEXT(HAPI_UNREAL_ATTRIB_GC_COLLISIONS_MAXIMUM_COLLISION_PARTICLES), GCSizeSpecIdx); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeIntArrayData( + FHoudiniEngine::Get().GetSession(), + GeoId, PartId, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo, + (const int32 *)AttributeData.GetData(), AttributeInfo.totalArrayElements, (const int *)AttributeDataSizes.GetData(), 0, AttributeInfo.count), false); + } + } + + // Non-Essential attributes- Similar to Mesh input: unreal_input_mesh_name, unreal_input_source_file, component and actor tags + // INPUT GC NAME + //--------------------------------------------------------------------------------------------------------------------- + { + // Create primitive attribute with mesh asset path + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = Part.faceCount; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + GeoId, 0, HAPI_UNREAL_ATTRIB_INPUT_GC_NAME, &AttributeInfo), false); + + const FString AssetPath = GeometryCollectionObject->GetPathName(); + + FHoudiniHapiAccessor Accessor(GeoId, 0, HAPI_UNREAL_ATTRIB_INPUT_GC_NAME); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfo, AssetPath), false); + } + + //--------------------------------------------------------------------------------------------------------------------- + // COMPONENT AND ACTOR TAGS + //--------------------------------------------------------------------------------------------------------------------- + if (IsValid(GeometryCollectionComponent)) + { + // Try to create groups for the static mesh component's tags + if (GeometryCollectionComponent->ComponentTags.Num() > 0 + && !FHoudiniEngineUtils::CreateGroupsFromTags(GeoId, PartId, GeometryCollectionComponent->ComponentTags)) + HOUDINI_LOG_WARNING(TEXT("Could not create groups from the Static Mesh Component's tags!")); + + AActor* ParentActor = GeometryCollectionComponent->GetOwner(); + if (IsValid(ParentActor)) + { + // Try to create groups for the parent Actor's tags + if (ParentActor->Tags.Num() > 0 + && !FHoudiniEngineUtils::CreateGroupsFromTags(GeoId, PartId, ParentActor->Tags)) + HOUDINI_LOG_WARNING(TEXT("Could not create groups from the Static Mesh Component's parent actor tags!")); + + // Add the unreal_actor_path attribute + FHoudiniEngineUtils::AddActorPathAttribute(GeoId, PartId, ParentActor, Part.faceCount); + + // Add the unreal_level_path attribute + FHoudiniEngineUtils::AddLevelPathAttribute(GeoId, PartId, ParentActor->GetLevel(), Part.faceCount); + } + } + + return true; +} + + +``` + +`Source/HoudiniEngine/Private/UnrealGeometryCollectionTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" + +#include "CoreMinimal.h" + + +class UGeometryCollection; +class AGeometryCollectionActor; +class FUnrealObjectInputHandle; + +struct HOUDINIENGINE_API FUnrealGeometryCollectionTranslator +{ + public: + // HAPI : Marshaling, extract geometry and create input asset for it - return true on success + static bool HapiCreateInputNodeForGeometryCollection( + UGeometryCollection * GeometryCollection, + HAPI_NodeId& InputObjectNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + bool bExportMaterialParameters, + class UGeometryCollectionComponent* GeometryCollectionComponent, + const bool& bInputNodesCanBeDeleted); + + static bool SetGeometryCollectionAttributesForPart( + UGeometryCollection * InGeometryCollection, + int32 InGeometryIndex, + HAPI_NodeId InNodeId, + FString InName); + + static bool UploadGeometryCollection( + UGeometryCollection * GeometryCollectionObject, + HAPI_NodeId InParentNodeId, + FString InName, + HAPI_NodeId InMergeNodeId, + bool bInExportMaterialParametersAsAttributes, + UGeometryCollectionComponent * GeometryCollectionComponent = nullptr); + + static bool AddGeometryCollectionDetailAttributes( + UGeometryCollection* GeometryCollectionObject, + HAPI_NodeId GeoId, + HAPI_PartId PartId, + HAPI_PartInfo & Part, + const FString& InName, + UGeometryCollectionComponent * GeometryCollectionComponent = nullptr); +}; + + +``` + +`Source/HoudiniEngine/Private/UnrealInstanceTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealInstanceTranslator.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEnginePrivatePCH.h" +#include "UnrealMeshTranslator.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputTypes.h" +#include "UnrealObjectInputRuntimeUtils.h" +#include "UnrealObjectInputUtils.h" + +#include "Engine/StaticMesh.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "Materials/MaterialInterface.h" + +bool +FUnrealInstanceTranslator::HapiCreateInputNodeForInstancer( + UInstancedStaticMeshComponent* ISMC, + const FString& InNodeName, + HAPI_NodeId& OutCreatedNodeId, + FUnrealObjectInputHandle& OutHandle, + const bool& bExportLODs, + const bool& bExportSockets, + const bool& bExportColliders, + const bool& bExportAsAttributeInstancer, + const bool bPreferNaniteFallbackMesh, + bool bExportMaterialParameters, + const bool& bInputNodesCanBeDeleted) +{ + int32 InstanceCount = ISMC->GetInstanceCount(); + if (InstanceCount < 1) + return true; + + // Get the Static Mesh instanced by the component + UStaticMesh* SM = ISMC->GetStaticMesh(); + if (!IsValid(SM)) + return true; + + // Marshall the Static Mesh to Houdini + int32 SMNodeId = -1; + FString ISMCName = InNodeName + TEXT("_") + ISMC->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(ISMCName); + + FUnrealMeshExportOptions ExportOptions; + ExportOptions.bLODs = bExportLODs; + ExportOptions.bSockets = bExportSockets; + ExportOptions.bColliders = bExportColliders; + ExportOptions.bMainMesh = true; + ExportOptions.bMaterialParameters = bExportMaterialParameters; + ExportOptions.bPreferNaniteFallbackMesh = bPreferNaniteFallbackMesh; + + FUnrealObjectInputHandle SMNodeHandle; + bool bSuccess = FUnrealMeshTranslator::CreateInputNodeForStaticMesh( + SMNodeId, + SMNodeHandle, + SM, + ISMC, + InNodeName, + ExportOptions, + true, + false); + + if (!bSuccess) + return false; + + // Modifier chain name for component overrides on the static mesh (used with the ref counted input system). + const FName MeshChainName("sm_overrides"); + + FString FinalInputNodeName = InNodeName; + HAPI_NodeId ParentNodeId = -1; + FUnrealObjectInputHandle ParentHandle; + FUnrealObjectInputIdentifier Identifier; + { + // Build the identifier for the entry in the manager + constexpr bool bIsLeaf = false; + FUnrealObjectInputOptions Options = SMNodeHandle.GetIdentifier().GetOptions(); + Identifier = FUnrealObjectInputIdentifier(ISMC, Options, bIsLeaf); + + // If the entry exists in the manager, the associated HAPI nodes are valid, and it is not marked as dirty, then + // return the existing entry + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + // Check consistency: the ref node should have 0 or 1 entries. If it has more than one, then it is incompatible + // and we need to proceed with recreation + TSet ReferencedNodes; + if (FUnrealObjectInputUtils::GetReferencedNodes(Handle, ReferencedNodes)) + { + if (ReferencedNodes.Num() == 1 && ReferencedNodes.Contains(SMNodeHandle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId)) + { + // The obj merge should be along input 0 from NodeId until we get to a node that does not have + // an input + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + HAPI_NodeId CurrentNodeId = NodeId; + HAPI_NodeId CurrentInputNodeId = -1; + while (FHoudiniApi::QueryNodeInput(Session, CurrentNodeId, 0, &CurrentInputNodeId) == HAPI_RESULT_SUCCESS && CurrentInputNodeId >= 0) + { + CurrentNodeId = CurrentInputNodeId; + } + if (CurrentNodeId >= 0) + { + // Get the objpath1 node parm from the object_merge + if (FHoudiniApi::GetParmNodeValue(Session, CurrentNodeId, H_TCHAR_TO_UTF8(TEXT("objpath1")), &CurrentNodeId) != HAPI_RESULT_SUCCESS) + CurrentNodeId = -1; + } + + // If SMNodeId is not currently the source of our connected objmerge (or there is no connected objmerge) we have to rebuild + if (CurrentNodeId == SMNodeId) + { + // Make sure the node cant be deleted if needed + if (!bInputNodesCanBeDeleted) + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + + OutHandle = Handle; + OutCreatedNodeId = NodeId; + return true; + } + } + } + } + } + // If the entry does not exist, or is invalid, then we need create it + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + // Create any parent/container nodes that we would need, and get the node id of the immediate parent + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // Set OutCreatedNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that OutCreatedNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (Handle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, OutCreatedNodeId)) + OutCreatedNodeId = -1; + } + else + { + OutCreatedNodeId = -1; + } + } + + // To create the instance properly (via packed prim), we need to: + // - create a copytopoints (with pack and instance enable + // - an inputnode containing all of the instances transform as points + // - plug the input node and the static mesh node in the copytopoints + + // Create the copytopoints SOP. + HAPI_NodeId ObjectNodeId = -1; + HAPI_NodeId CopyNodeId = -1; + int32 MatNodeId = -1; + { + // If we have existing valid HAPI nodes (so we are rebuilding a dirty / old version) reuse those nodes. This + // is quite important, to keep our obj merges / node references in _Houdini_ valid + bool bCreateNodes = true; + if (FUnrealObjectInputUtils::AreHAPINodesValid(Identifier)) + { + if (FUnrealObjectInputUtils::GetHAPINodeId(Identifier, MatNodeId)) + { + ObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(MatNodeId); + if (ObjectNodeId >= 0) + { + bCreateNodes = false; + if (FHoudiniApi::QueryNodeInput(FHoudiniEngine::Get().GetSession(), MatNodeId, 0, &CopyNodeId) == HAPI_RESULT_SUCCESS) + { + // Best effort: clean up: disconnect the object merge from input 0 (HAPI should delete it + // automatically) and delete the instances null that is connected to input 1 + FHoudiniApi::DisconnectNodeInput(FHoudiniEngine::Get().GetSession(), CopyNodeId, 0); + HAPI_NodeId OldInstancesId = -1; + if (FHoudiniApi::QueryNodeInput(FHoudiniEngine::Get().GetSession(), CopyNodeId, 1, &OldInstancesId) == HAPI_RESULT_SUCCESS + && OldInstancesId >= 0) + { + FHoudiniEngineUtils::DeleteHoudiniNode(OldInstancesId); + } + } + } + } + } + + if (bCreateNodes) + { + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + ParentNodeId, ParentNodeId < 0 ? TEXT("Object/geo") : TEXT("geo"), FinalInputNodeName, true, &ObjectNodeId), false); + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + ObjectNodeId, ObjectNodeId < 0 ? TEXT("SOP/attribcreate") : TEXT("attribcreate"), FinalInputNodeName, true, &MatNodeId), false); + } + } + + if (CopyNodeId < 0) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + ObjectNodeId, TEXT("copytopoints"), "copytopoints", false, &CopyNodeId), false); + } + + // set "Pack And Instance" (pack) to true + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), CopyNodeId, "pack", 0, 1), false); + + // Now create an input node for the instance transforms + HAPI_NodeId InstancesNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + ObjectNodeId, TEXT("null"), "instances", false, &InstancesNodeId), false); + + // MARSHALL THE INSTANCE TRANSFORM + { + // Get the instance transform and convert them to Position/Rotation/Scale array + TArray Positions; + Positions.SetNumZeroed(InstanceCount * 3); + TArray Rotations; + Rotations.SetNumZeroed(InstanceCount * 4); + TArray Scales; + Scales.SetNumZeroed(InstanceCount * 3); + for (int32 InstanceIdx = 0; InstanceIdx < InstanceCount; InstanceIdx++) + { + FTransform CurTransform; + ISMC->GetInstanceTransform(InstanceIdx, CurTransform); + + // Convert Unreal Position to Houdini + FVector PositionVector = CurTransform.GetLocation(); + Positions[InstanceIdx * 3 + 0] = (float)PositionVector.X / HAPI_UNREAL_SCALE_FACTOR_POSITION; + Positions[InstanceIdx * 3 + 1] = (float)PositionVector.Z / HAPI_UNREAL_SCALE_FACTOR_POSITION; + Positions[InstanceIdx * 3 + 2] = (float)PositionVector.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION; + + // Convert Unreal Rotation to Houdini + FQuat RotationQuaternion = CurTransform.GetRotation(); + Rotations[InstanceIdx * 4 + 0] = (float)RotationQuaternion.X; + Rotations[InstanceIdx * 4 + 1] = (float)RotationQuaternion.Z; + Rotations[InstanceIdx * 4 + 2] = (float)RotationQuaternion.Y; + Rotations[InstanceIdx * 4 + 3] = (float)-RotationQuaternion.W; + + // Convert Unreal Scale to Houdini + FVector ScaleVector = CurTransform.GetScale3D(); + Scales[InstanceIdx * 3 + 0] = (float)ScaleVector.X; + Scales[InstanceIdx * 3 + 1] = (float)ScaleVector.Z; + Scales[InstanceIdx * 3 + 2] = (float)ScaleVector.Y; + } + + // Create a part for the instance points. + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + Part.vertexCount = 0; + Part.faceCount = 0; + Part.pointCount = InstanceCount; + Part.type = HAPI_PARTTYPE_MESH; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), InstancesNodeId, 0, &Part), false); + + // Create position (P) attribute + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = InstanceCount; + AttributeInfoPoint.tupleSize = 3; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + InstancesNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint), false); + + // Now that we have raw positions, we can upload them for our attribute. + + FHoudiniHapiAccessor Accessor(InstancesNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPoint, Positions), false); + + // Create Rotation (rot) attribute + HAPI_AttributeInfo AttributeInfoRotation; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoRotation); + AttributeInfoRotation.count = InstanceCount; + AttributeInfoRotation.tupleSize = 4; + AttributeInfoRotation.exists = true; + AttributeInfoRotation.owner = HAPI_ATTROWNER_POINT; + AttributeInfoRotation.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoRotation.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + InstancesNodeId, 0, HAPI_UNREAL_ATTRIB_ROTATION, &AttributeInfoRotation), false); + + Accessor.Init(InstancesNodeId, 0, HAPI_UNREAL_ATTRIB_ROTATION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoRotation, Rotations), false); + + // Create scale attribute + HAPI_AttributeInfo AttributeInfoScale; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoScale); + AttributeInfoScale.count = InstanceCount; + AttributeInfoScale.tupleSize = 3; + AttributeInfoScale.exists = true; + AttributeInfoScale.owner = HAPI_ATTROWNER_POINT; + AttributeInfoScale.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoScale.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + InstancesNodeId, 0, HAPI_UNREAL_ATTRIB_SCALE, &AttributeInfoScale), false); + + Accessor.Init(InstancesNodeId, 0, HAPI_UNREAL_ATTRIB_SCALE); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoScale, Scales), false); + + // Commit the instance point geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(InstancesNodeId), false); + } + + // Connect the mesh to the copytopoints node's second input + // Make sure to set the XFormType fopr the obejct merge created to None + FHoudiniEngineUtils::HapiConnectNodeInput(CopyNodeId, 0, SMNodeId, 0, 0); + + // Connect the instances to the copytopoints node's second input + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), CopyNodeId, 1, InstancesNodeId, 0), false); + + FHoudiniApi::SetParmIntValue(FHoudiniEngine::Get().GetSession(), MatNodeId, "numattr", 0, SM->GetStaticMaterials().Num()); + HAPI_ParmInfo ParmInfo; + HAPI_PartId ParmId; + + const TArray& MeshMaterials = SM->GetStaticMaterials(); + int32 MatIdx = 0; + for (const FStaticMaterial& Mat : MeshMaterials) + { + FString MatName = MeshMaterials.Num() == 1 ? HAPI_UNREAL_ATTRIB_MATERIAL : FString(HAPI_UNREAL_ATTRIB_MATERIAL) + FString::FromInt(MatIdx); + + // parm name is one indexed + ParmId = FHoudiniEngineUtils::HapiFindParameterByName(MatNodeId, "name" + std::to_string(++MatIdx), ParmInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), MatNodeId, TCHAR_TO_ANSI(*MatName), ParmId, 0), false); + + // set attribute type to string (index 3) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(FHoudiniEngine::Get().GetSession(), MatNodeId, TCHAR_TO_ANSI(*(FString("type") + FString::FromInt(MatIdx))), 0, 3), false); + + // set value to path of material + ParmId = FHoudiniEngineUtils::HapiFindParameterByName(MatNodeId, "string" + std::to_string(MatIdx), ParmInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), MatNodeId, TCHAR_TO_ANSI(*Mat.MaterialInterface->GetPathName(nullptr)), ParmId, 0), false); + } + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), MatNodeId, 0, CopyNodeId, 0), false); + + // Update/create the entry in the input manager + { + // Record the node in the manager + FUnrealObjectInputHandle Handle; + TSet ReferencedNodes({SMNodeHandle}); + //FHoudiniEngineUtils::AddNodeOrUpdateNode(Identifier, CopyNodeId, Handle, ObjectNodeId, &ReferencedNodes, bInputNodesCanBeDeleted); + FUnrealObjectInputHandle CopyPointsHandle; + //ReferencedNodes = { CopyPointsHandle }; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, MatNodeId, Handle, ObjectNodeId, &ReferencedNodes, bInputNodesCanBeDeleted)) + OutHandle = Handle; + + // First create the overrides on the static mesh before the copy + pack + + // Create the smc_overrides modifier chain if missing + HAPI_NodeId SMObjMergeNodeId = -1; + if (FHoudiniApi::QueryNodeInput(FHoudiniEngine::Get().GetSession(), CopyNodeId, 0, &SMObjMergeNodeId) != HAPI_RESULT_SUCCESS && SMObjMergeNodeId < 0) + { + // Destroy chain and log warning + HOUDINI_LOG_WARNING(TEXT("Could not find obj merge input node for instancer copytopoints: removing '%s' modifier chain."), *MeshChainName.ToString()); + FUnrealObjectInputUtils::RemoveModifierChain(Handle, MeshChainName); + } + else + { + if (!FUnrealObjectInputUtils::DoesModifierChainExist(Handle, MeshChainName)) + FUnrealObjectInputUtils::AddModifierChain(Handle, MeshChainName, SMObjMergeNodeId); + else + FUnrealObjectInputUtils::SetModifierChainNodeToConnectTo(Handle, MeshChainName, SMObjMergeNodeId); + + // Make sure that a static mesh component overrides modifier exists for this component's input node + if (!FUnrealObjectInputUtils::FindFirstModifierOfType(Handle, MeshChainName, EUnrealObjectInputModifierType::MaterialOverrides)) + FUnrealObjectInputUtils::CreateAndAddModifier(Handle, MeshChainName, ISMC); + + // Ensure that the physical material override modifier exists for this component's input node + if (!FUnrealObjectInputUtils::FindFirstModifierOfType(Handle, MeshChainName, EUnrealObjectInputModifierType::PhysicalMaterialOverride)) + FUnrealObjectInputUtils::CreateAndAddModifier(Handle, MeshChainName, ISMC, HAPI_ATTROWNER_PRIM); + } + + // Then create the output override chain + const FName InstancerChainName(FUnrealObjectInputNode::OutputChainName); + if (!FUnrealObjectInputUtils::DoesModifierChainExist(Handle, InstancerChainName)) + FUnrealObjectInputUtils::AddModifierChain(Handle, InstancerChainName, MatNodeId); + else + FUnrealObjectInputUtils::SetModifierChainNodeToConnectTo(Handle, InstancerChainName, MatNodeId); + + // Make sure that a static mesh component overrides modifier exists for this component's input node + if (!FUnrealObjectInputUtils::FindFirstModifierOfType(Handle, InstancerChainName, EUnrealObjectInputModifierType::MaterialOverrides)) + FUnrealObjectInputUtils::CreateAndAddModifier(Handle, InstancerChainName, ISMC, false); + + // Ensure that the physical material override modifier exists for this component's input node + if (!FUnrealObjectInputUtils::FindFirstModifierOfType(Handle, InstancerChainName, EUnrealObjectInputModifierType::PhysicalMaterialOverride)) + FUnrealObjectInputUtils::CreateAndAddModifier(Handle, InstancerChainName, ISMC, HAPI_ATTROWNER_POINT); + + // Data layer Modifier + FUnrealObjectInputModifier* DataLayerModifier = FUnrealObjectInputUtils::FindFirstModifierOfType(Handle, InstancerChainName, EUnrealObjectInputModifierType::DataLayerGroups); + if (!DataLayerModifier) + { + FUnrealObjectInputUtils::CreateAndAddModifier(Handle, InstancerChainName, ISMC->GetOwner()); + } + + // HLODs + FUnrealObjectInputModifier* HLODModifier = FUnrealObjectInputUtils::FindFirstModifierOfType(Handle, InstancerChainName, EUnrealObjectInputModifierType::HLODAttributes); + if (!HLODModifier) + { + FUnrealObjectInputUtils::CreateAndAddModifier(Handle, InstancerChainName, ISMC->GetOwner()); + } + + // Actor & Component Tags + FUnrealObjectInputModifier* TagsModifier = FUnrealObjectInputUtils::FindFirstModifierOfType(Handle, InstancerChainName, EUnrealObjectInputModifierType::ActorProperties); + if(!TagsModifier) + { + FUnrealObjectInputUtils::CreateAndAddModifier(Handle, InstancerChainName, ISMC); + } + + // Custom Primitive Data - as point attributes + if (FUnrealObjectInputModifier* Modifier = FUnrealObjectInputUtils::FindFirstModifierOfType(Handle, InstancerChainName, EUnrealObjectInputModifierType::CustomPrimitiveData)) + { + FUnrealObjectInputCustomPrimitiveData* const CustomPrimDataModifier + = static_cast(Modifier); + if (CustomPrimDataModifier) + CustomPrimDataModifier->SetUsePrimWrangle(false); + } + else + { + FUnrealObjectInputUtils::CreateAndAddModifier( + Handle, InstancerChainName, ISMC, false); + } + + // Update all modifiers + FUnrealObjectInputUtils::UpdateAllModifierChains(Handle); + + // Record the node that the static mesh obj merge is now connected to in the reference node + FUnrealObjectInputUtils::SetReferencesNodeConnectToNodeId(Handle, FUnrealObjectInputUtils::GetInputNodeOfModifierChain(Handle, MeshChainName)); + + // Ensure that the output of the sm chain is connected to the copytopoints node + HAPI_NodeId SMChainOutputNodeId = FUnrealObjectInputUtils::GetOutputNodeOfModifierChain(Handle, MeshChainName); + if (SMChainOutputNodeId >= 0 && FHoudiniEngineUtils::IsHoudiniNodeValid(SMChainOutputNodeId)) + { + if (FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), CopyNodeId, 0, SMChainOutputNodeId, 0) != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("Failed to connect the '%d' chain to the copytopoints instancer node."), (int32)SMChainOutputNodeId); + } + } + } + + // Update this input object's node IDs + OutCreatedNodeId = MatNodeId; + + return true; +} + +``` + +`Source/HoudiniEngine/Private/UnrealInstanceTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" + +#include "CoreMinimal.h" +#include "UObject/ObjectMacros.h" + +class UInstancedStaticMeshComponent; +class FUnrealObjectInputHandle; + +struct HOUDINIENGINE_API FUnrealInstanceTranslator +{ + public: + + // HAPI : Marshaling, extract geometry and create input asset for it - return true on success + static bool HapiCreateInputNodeForInstancer( + UInstancedStaticMeshComponent* ISMC, + const FString& InNodeName, + HAPI_NodeId& OutCreatedNodeId, + FUnrealObjectInputHandle& OutHandle, + const bool& bExportLODs, + const bool& bExportSockets, + const bool& bExportColliders, + const bool& bExportAsAttributeInstancer, + bool bPreferNaniteFallbackMesh, + bool bExportMaterialParameters, + const bool& bInputNodesCanBeDeleted); +}; +``` + +`Source/HoudiniEngine/Private/UnrealLandscapeSplineTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealLandscapeSplineTranslator.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniLandscapeRuntimeUtils.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputRuntimeUtils.h" +#include "UnrealObjectInputUtils.h" + +#include "Landscape.h" +#include "LandscapeSplineActor.h" +#include "LandscapeSplineControlPoint.h" +#include "Materials/MaterialInterface.h" + + +/** + * Helper struct to record segment data, such as the segment length, unresampled points (spline points generated in UE), + * and global segment index (index in the output arrays of data sent to Houdini). + */ +struct FOrderedSegmentData +{ + TObjectPtr Segment; + float SegmentLength = 0.0f; + TArray UnResampledPoints; + int32 GlobalSegmentIndex = INDEX_NONE; +}; + +/** + * Helper struct to record segments that are connected and have the same orientation. + */ +struct FConnectedSpline +{ + TArray OrderedSegments; + ULandscapeSplineControlPoint* Start; + ULandscapeSplineControlPoint* End; +}; + + +void +FHoudiniUnrealLandscapeSplineControlPointAttributes::Init(const int32 InExpectedPointCount) +{ + PointCount = InExpectedPointCount; + Rotations.Empty(PointCount * 4); + PaintLayerNames.Empty(PointCount); + RaiseTerrains.Empty(PointCount); + LowerTerrains.Empty(PointCount); + MeshRefs.Empty(PointCount); + MaterialOverrideRefs.Empty(); + MeshScales.Empty(PointCount * 3); + Ids.Empty(PointCount); + HalfWidths.Empty(PointCount); + SideFalloffs.Empty(PointCount); + EndFalloffs.Empty(PointCount); +} + +void +ConvertAndSetRotation(const FRotator& InUnrealRotation, const int32 InArrayStartIndex, TArray& OutQuatFloatArray) +{ + // Convert Unreal X-Forward to Houdini Z-Forward and Unreal Z-Up to Houdini Y-Up +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0) + static constexpr float HalfPI = UE_HALF_PI; +#else + static constexpr float HalfPI = HALF_PI; +#endif + const FQuat CPRot = InUnrealRotation.Quaternion() * FQuat(FVector::UpVector, -HalfPI); + if (InArrayStartIndex >= 0) + { + check(OutQuatFloatArray.IsValidIndex(InArrayStartIndex + 3)); + OutQuatFloatArray[InArrayStartIndex + 0] = CPRot.X; + OutQuatFloatArray[InArrayStartIndex + 1] = CPRot.Z; + OutQuatFloatArray[InArrayStartIndex + 2] = CPRot.Y; + OutQuatFloatArray[InArrayStartIndex + 3] = -CPRot.W; + } + else + { + OutQuatFloatArray.Add(CPRot.X); + OutQuatFloatArray.Add(CPRot.Z); + OutQuatFloatArray.Add(CPRot.Y); + OutQuatFloatArray.Add(-CPRot.W); + } +} + +bool +FHoudiniUnrealLandscapeSplineControlPointAttributes::AddControlPointData( +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + TObjectPtr& InControlPoint, +#else + const ULandscapeSplineControlPoint * InControlPoint, +#endif + int32 InControlPointIndex, + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId) +{ + if (!IsValid(InControlPoint)) + return false; + + ConvertAndSetRotation(InControlPoint->Rotation, -1, Rotations); + + const int32 ControlPointId = FHoudiniLandscapeRuntimeUtils::GetOrGenerateValidControlPointId( + InControlPoint, InControlPointIdMap, InNextControlPointId); + Ids.Emplace(ControlPointId); + + HalfWidths.Add(InControlPoint->Width / HAPI_UNREAL_SCALE_FACTOR_POSITION); + SideFalloffs.Add(InControlPoint->SideFalloff / HAPI_UNREAL_SCALE_FACTOR_POSITION); + EndFalloffs.Add(InControlPoint->EndFalloff / HAPI_UNREAL_SCALE_FACTOR_POSITION); + + PaintLayerNames.Emplace(InControlPoint->LayerName.ToString()); + RaiseTerrains.Add(InControlPoint->bRaiseTerrain); + LowerTerrains.Add(InControlPoint->bLowerTerrain); + + // Set the static mesh reference + MeshRefs.Emplace(IsValid(InControlPoint->Mesh) ? InControlPoint->Mesh->GetPathName() : FString()); + + const int32 NumMaterialOverrides = InControlPoint->MaterialOverrides.Num(); + if (MaterialOverrideRefs.Num() < NumMaterialOverrides) + MaterialOverrideRefs.SetNum(NumMaterialOverrides); + for (int32 MaterialOverrideIdx = 0; MaterialOverrideIdx < NumMaterialOverrides; ++MaterialOverrideIdx) + { + UMaterialInterface const* const Material = InControlPoint->MaterialOverrides[MaterialOverrideIdx]; + // Initialize the per control point array for this override index if necessary + TArray& PerCPMaterialOverrideRefs = MaterialOverrideRefs[MaterialOverrideIdx]; + if (PerCPMaterialOverrideRefs.IsEmpty()) + PerCPMaterialOverrideRefs.SetNum(PointCount); + + // Set the material ref or empty string if the material is invalid + PerCPMaterialOverrideRefs[InControlPointIndex] = IsValid(Material) ? Material->GetPathName() : FString(); + } + + MeshScales.Add(InControlPoint->MeshScale.X); + MeshScales.Add(InControlPoint->MeshScale.Z); + MeshScales.Add(InControlPoint->MeshScale.Y); + + return true; +} + + +void +FHoudiniUnrealLandscapeSplineControlPointAttributes::AddEmpty() +{ + Rotations.Add(FQuat::Identity.X); + Rotations.Add(FQuat::Identity.Z); + Rotations.Add(FQuat::Identity.Y); + Rotations.Add(-FQuat::Identity.W); + + Ids.Add(INDEX_NONE); + + HalfWidths.AddDefaulted(); + SideFalloffs.AddDefaulted(); + EndFalloffs.AddDefaulted(); + + PaintLayerNames.AddDefaulted(); + RaiseTerrains.Add(false); + LowerTerrains.Add(false); + + MeshRefs.AddDefaulted(); + + MeshScales.Add(1.0f); + MeshScales.Add(1.0f); + MeshScales.Add(1.0f); +} + + +FHoudiniUnResampledPoint::FHoudiniUnResampledPoint(EHoudiniUnrealLandscapeSplineCurve InSplineSelection) + : Alpha(0.0f) + , SplineSelection(InSplineSelection) +{ +} + +FHoudiniUnResampledPoint::FHoudiniUnResampledPoint(EHoudiniUnrealLandscapeSplineCurve InSplineSelection, const FLandscapeSplineInterpPoint& InPoint) + : Center(InPoint.Center) + , Left(InPoint.Left) + , Right(InPoint.Right) + , FalloffLeft(InPoint.FalloffLeft) + , FalloffRight(InPoint.FalloffRight) + , Rotation(FQuat::Identity) + , Alpha(0.0f) + , SplineSelection(InSplineSelection) +{ + +} + + +FVector +FHoudiniUnResampledPoint::GetSelectedPosition() const +{ + switch (SplineSelection) + { + case EHoudiniUnrealLandscapeSplineCurve::Center: + return Center; + case EHoudiniUnrealLandscapeSplineCurve::Left: + return Left; + case EHoudiniUnrealLandscapeSplineCurve::Right: + return Right; + default: + HOUDINI_LOG_WARNING(TEXT("Invalid value for SplineSelection: %d, returning Center point."), SplineSelection); + break; + } + + return Center; +} + +FQuat +FHoudiniUnResampledPoint::CalculateRotationTo(const FHoudiniUnResampledPoint& InNextPoint) +{ + FVector ForwardVector = (InNextPoint.Center - Center).GetSafeNormal(); + FVector RightVector = (Right - Center).GetSafeNormal(); + Rotation = FRotationMatrix::MakeFromXY(ForwardVector, RightVector).ToQuat(); + return Rotation; +} + +bool +FUnrealLandscapeSplineTranslator::CreateInputNode( + ULandscapeSplinesComponent* const InSplinesComponent, + bool bForceReferenceInputNodeCreation, + HAPI_NodeId& OutCreatedInputNodeId, + FUnrealObjectInputHandle& OutInputNodeHandle, + const FString& InNodeName, + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId, + float InSplineResolution, + bool bInExportCurves, + bool bInExportControlPoints, + bool bInExportLeftRightCurves, + bool bInInputNodesCanBeDeleted) +{ + if (!IsValid(InSplinesComponent)) + return false; + + // Input node name, defaults to InNodeName, but can be changed by the new input system + FString FinalInputNodeName = InNodeName; + + // Find the node in new input system + // Identifier will be the identifier for the entry created in this call of the function. + FUnrealObjectInputIdentifier Identifier; + FUnrealObjectInputHandle ParentHandle; + HAPI_NodeId ParentNodeId = -1; + { + // Check if we already have an input node for this component and its options + bool bSingleLeafNodeOnly = false; + FUnrealObjectInputIdentifier IdentReferenceNode; + TArray IdentPerOption; + if (!FUnrealObjectInputUtils::BuildLandscapeSplinesInputObjectIdentifiers( + InSplinesComponent, + bInExportCurves, + bInExportControlPoints, + bInExportLeftRightCurves, + InSplineResolution, + bForceReferenceInputNodeCreation, + bSingleLeafNodeOnly, + IdentReferenceNode, + IdentPerOption)) + { + return false; + } + + if (bSingleLeafNodeOnly) + { + // We'll create the splines input node entirely is this function call + check(!IdentPerOption.IsEmpty()); + Identifier = IdentPerOption[0]; + } + else + { + // Look for the reference node that references the per-option (curves, control points) nodes + Identifier = IdentReferenceNode; + } + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId) && (bSingleLeafNodeOnly || FUnrealObjectInputUtils::AreReferencedHAPINodesValid(Handle))) + { + if (!bInInputNodesCanBeDeleted) + { + // Make sure to prevent deletion of the input node if needed + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInInputNodesCanBeDeleted); + } + + OutInputNodeHandle = Handle; + OutCreatedInputNodeId = NodeId; + return true; + } + } + + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + // Create any parent/container nodes that we would need, and get the node id of the immediate parent + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // We now need to create the nodes (since we couldn't find existing ones in the manager) + // For the single leaf node case we can simply continue this function + // For the ref + multiple options, we call this function again for each option (as a single leaf node) and + // then create the reference node. + if (!bSingleLeafNodeOnly) + { + TSet PerOptionNodeHandles; + PerOptionNodeHandles.Reserve(IdentPerOption.Num()); + for (const FUnrealObjectInputIdentifier& OptionIdentifier : IdentPerOption) + { + FUnrealObjectInputHandle OptionHandle; + const FUnrealObjectInputOptions& Options = OptionIdentifier.GetOptions(); + HAPI_NodeId NewNodeId = -1; + FString NodeLabel; + FUnrealObjectInputUtils::GetDefaultInputNodeName(OptionIdentifier, NodeLabel); + + if (FUnrealObjectInputUtils::FindNodeViaManager(OptionIdentifier, OptionHandle)) + { + // The node already exists, but it is dirty. Fetch its HAPI node ID so that the old + // node can be deleted when creating the new HAPI node. + // TODO: maybe the new input system manager should delete the old HAPI nodes when we set the new + // HAPI node IDs on the node entries in the manager? + FUnrealObjectInputUtils::GetHAPINodeId(OptionHandle, NewNodeId); + } + + static constexpr bool bForceInputRefNodeCreation = false; + if (!CreateInputNode( + InSplinesComponent, + bForceInputRefNodeCreation, + NewNodeId, + OptionHandle, + NodeLabel, + InControlPointIdMap, + InNextControlPointId, + Options.UnrealSplineResolution, + !Options.bExportLandscapeSplineControlPoints && !Options.bExportLandscapeSplineLeftRightCurves, + Options.bExportLandscapeSplineControlPoints, + Options.bExportLandscapeSplineLeftRightCurves, + bInInputNodesCanBeDeleted)) + { + return false; + } + + PerOptionNodeHandles.Add(OptionHandle); + } + + // Create or update the HAPI node for the reference node if it does not exist + FUnrealObjectInputHandle RefNodeHandle; + if (!FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(IdentReferenceNode, PerOptionNodeHandles, RefNodeHandle, true, bInInputNodesCanBeDeleted)) + return false; + + OutInputNodeHandle = RefNodeHandle; + FUnrealObjectInputUtils::GetHAPINodeId(IdentReferenceNode, OutCreatedInputNodeId); + return true; + } + + // Set OutCreatedInputNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that OutCreatedInputNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (Handle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, OutCreatedInputNodeId)) + OutCreatedInputNodeId = -1; + } + else + { + OutCreatedInputNodeId = -1; + } + } + + HAPI_NodeId PreviousInputNodeId = OutCreatedInputNodeId; + + // Delete the previous nodes, if valid + if (PreviousInputNodeId >= 0 && FHoudiniEngineUtils::IsHoudiniNodeValid(PreviousInputNodeId)) + { + // Get the parent OBJ node ID before deleting! + HAPI_NodeId PreviousInputOBJNode = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousInputNodeId); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input node for %s."), *FinalInputNodeName); + } + + if (PreviousInputOBJNode >= 0) + { + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputOBJNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input OBJ node for %s."), *FinalInputNodeName); + } + } + } + + int32 NumNodesNeeded = 0; + if (bInExportCurves) + NumNodesNeeded++; + if (bInExportControlPoints) + NumNodesNeeded++; + if (bInExportLeftRightCurves) + NumNodesNeeded += 2; + + const bool bUseMergeNode = NumNodesNeeded > 1; + HAPI_NodeId NewNodeId = -1; + HAPI_NodeId ObjectNodeId = -1; + // Create geo node + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniEngineUtils::CreateNode(ParentNodeId, ParentNodeId < 0 ? TEXT("Object/geo") : TEXT("geo"), FinalInputNodeName, false, &ObjectNodeId), false); + // Check if we have a valid id for the new geo obj + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(ObjectNodeId)) + return false; + + if (bUseMergeNode) + { + // Create merge sop in geo obj + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniEngineUtils::CreateNode(ObjectNodeId, TEXT("merge"), FinalInputNodeName, false, &NewNodeId), false); + // Update our input NodeId + OutCreatedInputNodeId = NewNodeId; + + // Check if we have a valid id for this new input asset. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(NewNodeId)) + return false; + } + + bool bSuccess = true; + int32 MergeNodeInputIdx = 0; + if (bInExportCurves) + { + HAPI_NodeId SplinesNodeId = -1; + if (!CreateInputNode( + InSplinesComponent, ObjectNodeId, FinalInputNodeName, InControlPointIdMap, InNextControlPointId, + SplinesNodeId, EHoudiniUnrealLandscapeSplineCurve::Center, InSplineResolution)) + { + bSuccess = false; + } + else + { + if (!bUseMergeNode) + { + OutCreatedInputNodeId = SplinesNodeId; + } + else + { + // Connect to the merge node + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + OutCreatedInputNodeId, MergeNodeInputIdx, SplinesNodeId, 0), false); + MergeNodeInputIdx++; + } + } + } + + if (bInExportControlPoints) + { + HAPI_NodeId ControlPointCloudNodeId = -1; + if (!CreateInputNodeForControlPoints( + InSplinesComponent, ObjectNodeId, FinalInputNodeName, InControlPointIdMap, InNextControlPointId, ControlPointCloudNodeId)) + { + bSuccess = false; + } + else + { + if (!bUseMergeNode) + { + OutCreatedInputNodeId = ControlPointCloudNodeId; + } + else + { + // Connect to the merge node + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + OutCreatedInputNodeId, MergeNodeInputIdx, ControlPointCloudNodeId, 0), false); + MergeNodeInputIdx++; + } + } + } + + if (bInExportLeftRightCurves) + { + HAPI_NodeId SplinesNodeId = -1; + if (!CreateInputNode( + InSplinesComponent, ObjectNodeId, FinalInputNodeName, InControlPointIdMap, InNextControlPointId, + SplinesNodeId, EHoudiniUnrealLandscapeSplineCurve::Left, InSplineResolution)) + { + bSuccess = false; + } + else + { + // Connect to the merge node + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + OutCreatedInputNodeId, MergeNodeInputIdx, SplinesNodeId, 0), false); + MergeNodeInputIdx++; + } + if (!CreateInputNode( + InSplinesComponent, ObjectNodeId, FinalInputNodeName, InControlPointIdMap, InNextControlPointId, + SplinesNodeId, EHoudiniUnrealLandscapeSplineCurve::Right, InSplineResolution)) + { + bSuccess = false; + } + else + { + // Connect to the merge node + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + OutCreatedInputNodeId, MergeNodeInputIdx, SplinesNodeId, 0), false); + MergeNodeInputIdx++; + } + } + + { + // Get our parent OBJ NodeID + const HAPI_NodeId InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(OutCreatedInputNodeId); + static constexpr TSet const* ReferencedNodes = nullptr; + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, OutCreatedInputNodeId, Handle, InputObjectNodeId, ReferencedNodes, bInInputNodesCanBeDeleted)) + OutInputNodeHandle = Handle; + } + + return bSuccess; +} + + +bool +FUnrealLandscapeSplineTranslator::CreateInputNode( + ULandscapeSplinesComponent* const InSplinesComponent, + HAPI_NodeId InObjectNodeId, + const FString& InNodeName, + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId, + HAPI_NodeId& OutNodeId, + const EHoudiniUnrealLandscapeSplineCurve InExportCurve, + const float InSplineResolution) +{ + if (!IsValid(InSplinesComponent)) + return false; + + // Input node name: InNodeName + a suffix describing if this is center or left/right curves. + FString FinalInputNodeName; + switch (InExportCurve) + { + case EHoudiniUnrealLandscapeSplineCurve::Center: + FinalInputNodeName = InNodeName + TEXT("_curves"); + break; + case EHoudiniUnrealLandscapeSplineCurve::Left: + FinalInputNodeName = InNodeName + TEXT("_left_curves"); + break; + case EHoudiniUnrealLandscapeSplineCurve::Right: + FinalInputNodeName = InNodeName + TEXT("_right_curves"); + break; + } + + FHoudiniUnrealLandscapeSplinesData SplinesData; + if (!ExtractSplineData(InSplinesComponent, InControlPointIdMap, InNextControlPointId, SplinesData, InExportCurve, InSplineResolution)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to extract landscape splines data.")); + return false; + } + + // Extract linear position array and calculate number of vertices + const int32 NumSegments = SplinesData.VertexCounts.Num(); + const int32 NumVerts = SplinesData.PointPositions.Num() / 3; + + const HAPI_Session * Session = FHoudiniEngine::Get().GetSession(); + + // Create null sop in geo obj + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(InObjectNodeId, TEXT("null"), FinalInputNodeName, false, &OutNodeId), false); + + // Check if we have a valid id for this new input asset. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(OutNodeId)) + return false; + + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + PartInfo.id = 0; + PartInfo.nameSH = 0; + PartInfo.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + PartInfo.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + PartInfo.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + PartInfo.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + PartInfo.type = HAPI_PARTTYPE_CURVE; + PartInfo.pointCount = SplinesData.PointPositions.Num() / 3; + PartInfo.vertexCount = NumVerts; + PartInfo.faceCount = NumSegments; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo(Session, OutNodeId, 0, &PartInfo), false); + + HAPI_CurveInfo CurveInfo; + FHoudiniApi::CurveInfo_Init(&CurveInfo); + CurveInfo.curveType = HAPI_CURVETYPE_LINEAR; + CurveInfo.curveCount = NumSegments; + CurveInfo.vertexCount = NumVerts; + CurveInfo.knotCount = 0; + CurveInfo.isPeriodic = false; + CurveInfo.isRational = false; + CurveInfo.order = 0; + CurveInfo.hasKnots = false; + CurveInfo.isClosed = false; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetCurveInfo(Session, OutNodeId, 0, &CurveInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetCurveCounts( + Session, OutNodeId, 0, SplinesData.VertexCounts.GetData(), + 0, SplinesData.VertexCounts.Num()), false); + + // Add attributes + bool bNeedToCommit = false; + + // Point attributes + if (AddPositionAttribute(OutNodeId, SplinesData.PointPositions)) + bNeedToCommit = true; + + if (AddControlPointAttributes(OutNodeId, SplinesData.ControlPointAttributes)) + bNeedToCommit = true; + + // Segment attributes + if (AddPaintLayerNameAttribute(OutNodeId, SplinesData.SegmentPaintLayerNames, HAPI_ATTROWNER_PRIM)) + bNeedToCommit = true; + + if (AddRaiseTerrainAttribute(OutNodeId, SplinesData.SegmentRaiseTerrains, HAPI_ATTROWNER_PRIM)) + bNeedToCommit = true; + + if (AddLowerTerrainAttribute(OutNodeId, SplinesData.SegmentLowerTerrains, HAPI_ATTROWNER_PRIM)) + bNeedToCommit = true; + + if (AddSegmentMeshesAttributes(OutNodeId, SplinesData.PerMeshSegmentData)) + bNeedToCommit = true; + + // Segment connection attributes (point attributes) + if (AddTangentLengthAttribute(OutNodeId, SplinesData.PointConnectionTangentLengths)) + bNeedToCommit = true; + + if (AddConnectionSocketNameAttribute(OutNodeId, SplinesData.PointConnectionSocketNames)) + bNeedToCommit = true; + + // Add the unreal_landscape_spline_output attribute to indicate that this a landscape spline and not a normal curve + if (AddOutputAttribute(OutNodeId, 0, static_cast(InExportCurve), PartInfo.faceCount)) + bNeedToCommit = true; + + // Add landscape spline component tags if it has any + if (FHoudiniEngineUtils::CreateGroupsFromTags(OutNodeId, 0, InSplinesComponent->ComponentTags)) + bNeedToCommit = true; + + // Add the parent actor's tag if it has any + AActor* const ParentActor = InSplinesComponent->GetOwner(); + if (IsValid(ParentActor)) + { + if (FHoudiniEngineUtils::CreateGroupsFromTags(OutNodeId, 0, ParentActor->Tags)) + bNeedToCommit = true; + + // Add the unreal_actor_path attribute + if (FHoudiniEngineUtils::AddActorPathAttribute(OutNodeId, 0, ParentActor, PartInfo.faceCount)) + bNeedToCommit = true; + + // Add the unreal_level_path attribute + if(FHoudiniEngineUtils::AddLevelPathAttribute(OutNodeId, 0, ParentActor->GetLevel(), PartInfo.faceCount)) + bNeedToCommit = true; + + // Should be attached to a landscape... + ALandscapeSplineActor const* const SplinesActor = Cast(ParentActor); + if (IsValid(SplinesActor)) + { + ULandscapeInfo const* const LandscapeInfo = SplinesActor->GetLandscapeInfo(); + if (IsValid(LandscapeInfo) && LandscapeInfo->LandscapeActor.IsValid()) + { + // Add the unreal_landscape_spline_target_landscape attribute + if (AddTargetLandscapeAttribute(OutNodeId, 0, LandscapeInfo->LandscapeActor.Get(), PartInfo.faceCount)) + bNeedToCommit = true; + } + } + } + + if (bNeedToCommit) + { + // We successfully added tags to the geo, so we need to commit the changes + if (HAPI_RESULT_SUCCESS != FHoudiniEngineUtils::HapiCommitGeo(OutNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("Could not create groups for the landscape spline input's tags!")); + return false; + } + + // And cook it with refinement disabled (we want to strictly keep the control points and segments as they are) + HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + CookOptions.maxVerticesPerPrimitive = -1; + CookOptions.refineCurveToLinear = false; + static constexpr bool bWaitForCompletion = false; + if (!FHoudiniEngineUtils::HapiCookNode(OutNodeId, &CookOptions, bWaitForCompletion)) + return false; + } + + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::CreateInputNodeForControlPoints( + ULandscapeSplinesComponent* const InSplinesComponent, + const HAPI_NodeId& InObjectNodeId, + const FString& InNodeName, + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId, + HAPI_NodeId& OutNodeId) +{ + if (!IsValid(InSplinesComponent)) + return false; + + // Set the final node name with _control_points suffix + const FString FinalInputNodeName = InNodeName + TEXT("_control_points"); + + FHoudiniUnrealLandscapeSplinesControlPointData ControlPointsData; + if (!ExtractSplineControlPointsData(InSplinesComponent, InControlPointIdMap, InNextControlPointId, ControlPointsData)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to extract landscape splines control points data.")); + return false; + } + + const int32 NumPoints = ControlPointsData.ControlPointPositions.Num() / 3; + + const HAPI_Session * Session = FHoudiniEngine::Get().GetSession(); + + // Create null sop in geo obj + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniEngineUtils::CreateNode(InObjectNodeId, TEXT("null"), FinalInputNodeName, false, &OutNodeId), false); + + // Check if we have a valid id for this new input asset. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(OutNodeId)) + return false; + + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + PartInfo.id = 0; + PartInfo.nameSH = 0; + PartInfo.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + PartInfo.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + PartInfo.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + PartInfo.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + PartInfo.type = HAPI_PARTTYPE_MESH; + PartInfo.pointCount = NumPoints; + PartInfo.vertexCount = 0; + PartInfo.faceCount = 0; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo(Session, OutNodeId, 0, &PartInfo), false); + + // Add attributes + bool bNeedToCommit = false; + + if (AddPositionAttribute(OutNodeId, ControlPointsData.ControlPointPositions)) + bNeedToCommit = true; + + if (AddControlPointAttributes(OutNodeId, ControlPointsData.Attributes)) + bNeedToCommit = true; + + // Add the unreal_landscape_spline_output attribute to indicate that this a landscape spline and not a normal curve + // TODO: Should there be a special type / value for the control points + if (AddOutputAttribute(OutNodeId, 0, 1, PartInfo.pointCount, HAPI_AttributeOwner::HAPI_ATTROWNER_POINT)) + bNeedToCommit = true; + + // // Add landscape spline component tags if it has any + // if (FHoudiniEngineUtils::CreateGroupsFromTags(OutNodeId, 0, InSplinesComponent->ComponentTags)) + // bNeedToCommit = true; + + // Add the parent actor's tag if it has any + AActor* const ParentActor = InSplinesComponent->GetOwner(); + if (IsValid(ParentActor)) + { + // if (FHoudiniEngineUtils::CreateGroupsFromTags(OutNodeId, 0, ParentActor->Tags)) + // bNeedToCommit = true; + + // Add the unreal_actor_path attribute + if (FHoudiniEngineUtils::AddActorPathAttribute(OutNodeId, 0, ParentActor, PartInfo.pointCount, HAPI_ATTROWNER_POINT)) + bNeedToCommit = true; + + // Add the unreal_level_path attribute + if(FHoudiniEngineUtils::AddLevelPathAttribute(OutNodeId, 0, ParentActor->GetLevel(), PartInfo.pointCount, HAPI_ATTROWNER_POINT)) + bNeedToCommit = true; + + // Should be attached to a landscape... + ALandscapeSplineActor const* const SplinesActor = Cast(ParentActor); + if (IsValid(SplinesActor)) + { + ULandscapeInfo const* const LandscapeInfo = SplinesActor->GetLandscapeInfo(); + if (IsValid(LandscapeInfo) && LandscapeInfo->LandscapeActor.IsValid()) + { + // Add the unreal_landscape_spline_target_landscape attribute + if (AddTargetLandscapeAttribute(OutNodeId, 0, LandscapeInfo->LandscapeActor.Get(), PartInfo.pointCount, HAPI_ATTROWNER_POINT)) + bNeedToCommit = true; + } + } + } + + if (bNeedToCommit) + { + // We successfully added tags to the geo, so we need to commit the changes + if (HAPI_RESULT_SUCCESS != FHoudiniEngineUtils::HapiCommitGeo(OutNodeId)) + HOUDINI_LOG_WARNING(TEXT("Could not commit landscape spline control point geo!")); + + HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + static constexpr bool bWaitForCompletion = false; + if (!FHoudiniEngineUtils::HapiCookNode(OutNodeId, &CookOptions, bWaitForCompletion)) + return false; + } + + return true; +} + + +void FindConnectedSplines(const TArray>& InSegments, TArray& OutConnectedSplines) +{ + TArray> SegmentsToProcess = InSegments; + TSet> ProcessedSegments; + FConnectedSpline* CurrentSpline = nullptr; + while (SegmentsToProcess.Num() > 0) + { + const TObjectPtr Segment = SegmentsToProcess.Pop(); + if (ProcessedSegments.Contains(Segment)) + continue; + + if (!CurrentSpline) + { + CurrentSpline = &OutConnectedSplines.AddDefaulted_GetRef(); + FOrderedSegmentData SegmentData; + SegmentData.Segment = Segment; + CurrentSpline->OrderedSegments.Add(SegmentData); + CurrentSpline->Start = Segment->Connections[0].ControlPoint; + CurrentSpline->End = Segment->Connections[1].ControlPoint; + + ProcessedSegments.Add(Segment); + } + + // Follow the chain of connected from CurrentSpline->Start to the end + TObjectPtr LastSegment = Segment; + int32 ConnectionIdx = 0; + while (ConnectionIdx < CurrentSpline->Start->ConnectedSegments.Num()) + { + const FLandscapeSplineConnection& Connection = CurrentSpline->Start->ConnectedSegments[ConnectionIdx]; + ConnectionIdx++; + if (ProcessedSegments.Contains(Connection.Segment)) + continue; + + if (Connection.Segment->Connections[1].ControlPoint == CurrentSpline->Start) + { + CurrentSpline->Start = Connection.Segment->Connections[0].ControlPoint; + ConnectionIdx = 0; + FOrderedSegmentData SegmentData; + SegmentData.Segment = Connection.Segment; + CurrentSpline->OrderedSegments.Insert(SegmentData, 0); + ProcessedSegments.Add(Connection.Segment); + } + } + + // Follow the chain of connected from CurrentSpline->End to the end + ConnectionIdx = 0; + while (ConnectionIdx < CurrentSpline->End->ConnectedSegments.Num()) + { + const FLandscapeSplineConnection& Connection = CurrentSpline->End->ConnectedSegments[ConnectionIdx]; + ConnectionIdx++; + if (ProcessedSegments.Contains(Connection.Segment)) + continue; + + if (Connection.Segment->Connections[0].ControlPoint == CurrentSpline->End) + { + CurrentSpline->End = Connection.Segment->Connections[1].ControlPoint; + ConnectionIdx = 0; + FOrderedSegmentData SegmentData; + SegmentData.Segment = Connection.Segment; + CurrentSpline->OrderedSegments.Add(SegmentData); + ProcessedSegments.Add(Connection.Segment); + } + } + + CurrentSpline = nullptr; + } +} + + +void PopulateUnResampledPointData( + TArray& InConnectedSplines, + const EHoudiniUnrealLandscapeSplineCurve InExportCurve, + const bool bInResampleSplines, + const float InSplineResolution, + int32& OutTotalNumPoints, + TArray& OutPerSegmentVertexCount) +{ + int32 NextGlobalSegmentIndex = 0; + int32 TotalNumPoints = 0; + for (FConnectedSpline& ConnectedSpline : InConnectedSplines) + { + // Determine total number of points for all segments + // Use helper structs to get keep the Center, Left and Right positions as well as the Alpha value and rotations + // along the spline + for (FOrderedSegmentData& SegmentData : ConnectedSpline.OrderedSegments) + { + SegmentData.GlobalSegmentIndex = NextGlobalSegmentIndex; + NextGlobalSegmentIndex++; + + SegmentData.SegmentLength = 0.0f; + if (!IsValid(SegmentData.Segment)) + { + OutPerSegmentVertexCount.Add(0); + continue; + } + + // Calculate segment length and number of points per segment + const TArray& SegmentSplinePoints = SegmentData.Segment->GetPoints(); + const int32 NumPointsInSegment = SegmentSplinePoints.Num(); + + // Initialize the unresampled points array and its first element + if (NumPointsInSegment > 0) + { + SegmentData.UnResampledPoints.Reserve(NumPointsInSegment); + SegmentData.UnResampledPoints.Emplace(InExportCurve, SegmentSplinePoints[0]); + } + // Populate the rest of the unresampled point array and calculate the rotations at each unresampled point + for (int32 VertIdx = 1; VertIdx < NumPointsInSegment; ++VertIdx) + { + FHoudiniUnResampledPoint& Point0 = SegmentData.UnResampledPoints[VertIdx - 1]; + FHoudiniUnResampledPoint& Point1 = SegmentData.UnResampledPoints.Emplace_GetRef( + InExportCurve, SegmentSplinePoints[VertIdx]); + + // Set rotations (first and last points use the control point's rotation) + if (VertIdx == 1) + { + ULandscapeSplineControlPoint const* const CP = SegmentData.Segment->Connections[0].ControlPoint; + if (IsValid(CP)) + Point0.Rotation = CP->Rotation.Quaternion(); + else + Point0.Rotation = FQuat::Identity; + } + else if (VertIdx == NumPointsInSegment - 1) + { + Point0.CalculateRotationTo(Point1); + ULandscapeSplineControlPoint const* const CP = SegmentData.Segment->Connections[1].ControlPoint; + if (IsValid(CP)) + Point1.Rotation = CP->Rotation.Quaternion(); + else + Point1.Rotation = FQuat::Identity; + } + else + { + Point0.CalculateRotationTo(Point1); + } + } + + int32 NumPointsInResampledSegment = 0; + if (bInResampleSplines) + { + // Calculate the number of resampled points via SegmentLength / SplineResolution + for (int32 VertIdx = 1; VertIdx < NumPointsInSegment; ++VertIdx) + { + const FHoudiniUnResampledPoint& Point0 = SegmentData.UnResampledPoints[VertIdx - 1]; + const FHoudiniUnResampledPoint& Point1 = SegmentData.UnResampledPoints[VertIdx]; + SegmentData.SegmentLength += (Point1.GetSelectedPosition() - Point0.GetSelectedPosition()).Length(); + } + NumPointsInResampledSegment = FMath::CeilToInt32(SegmentData.SegmentLength / InSplineResolution) + 1; + } + else + { + // Not resampling, so just use the points as is + NumPointsInResampledSegment = NumPointsInSegment; + } + + // Record the number of (resampled) points we'll have in this spline/segment + TotalNumPoints += NumPointsInResampledSegment; + OutPerSegmentVertexCount.Add(NumPointsInResampledSegment); + } + } + + OutTotalNumPoints = TotalNumPoints; +} + +bool FUnrealLandscapeSplineTranslator::ExtractSplineData( + ULandscapeSplinesComponent* const InSplinesComponent, + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId, + FHoudiniUnrealLandscapeSplinesData& OutSplinesData, + const EHoudiniUnrealLandscapeSplineCurve InExportCurve, + float InSplineResolution) +{ + if (!IsValid(InSplinesComponent)) + return false; + + if (!InSplinesComponent->HasAnyControlPointsOrSegments()) + return false; + + // Use helper to fetch segments, since the Landscape Splines API differs between UE 5.0 and 5.1+ + TArray> Segments; + if (!FHoudiniEngineRuntimeUtils::GetLandscapeSplinesSegments(InSplinesComponent, Segments)) + return false; + + // We need to determine which segments are connected with the same orientation. That way we can output a more + // consistent / increasing point and vertex order per set of connected segments. + TArray ConnectedSplines; + FindConnectedSplines(Segments, ConnectedSplines); + + const int32 TotalNumSegments = Segments.Num(); + // Initialize arrays + OutSplinesData.VertexCounts.Empty(TotalNumSegments); + OutSplinesData.SegmentPaintLayerNames.Empty(TotalNumSegments); + OutSplinesData.SegmentRaiseTerrains.Empty(TotalNumSegments); + OutSplinesData.SegmentLowerTerrains.Empty(TotalNumSegments); + + // We only have to resample the splines if the spline resolution is different than the internal spline resolution + // on the landscape splines component. + const bool bResampleSplines = (InSplineResolution > 0.0f && InSplineResolution != InSplinesComponent->SplineResolution); + int32 TotalNumPoints = 0; + PopulateUnResampledPointData(ConnectedSplines, InExportCurve, bResampleSplines, InSplineResolution, TotalNumPoints, OutSplinesData.VertexCounts); + + OutSplinesData.PointPositions.Empty(TotalNumPoints); + OutSplinesData.PointConnectionSocketNames.Empty(TotalNumPoints); + OutSplinesData.PointConnectionTangentLengths.Empty(TotalNumPoints); + OutSplinesData.ControlPointAttributes.Init(TotalNumPoints); + + // OutputPointIdx: The index of the current output point (across all segments). Range: [0, TotalNumPoints). + // Incremented in the inner ResampledSegmentVertIdx loop. + int32 OutputPointIdx = 0; + for (const FConnectedSpline& ConnectedSpline : ConnectedSplines) + { + const int32 NumSegments = ConnectedSpline.OrderedSegments.Num(); + for (int32 SegmentIdx = 0; SegmentIdx < NumSegments; ++SegmentIdx) + { + const FOrderedSegmentData& SegmentData = ConnectedSpline.OrderedSegments[SegmentIdx]; + if (!IsValid(SegmentData.Segment)) + { + // Create blank entries for this invalid segment + OutSplinesData.SegmentPaintLayerNames.AddDefaulted(); + OutSplinesData.SegmentRaiseTerrains.AddDefaulted(); + OutSplinesData.SegmentLowerTerrains.AddDefaulted(); + + continue; + } + + const TArray& SegmentPoints = SegmentData.Segment->GetPoints(); + const int32 NumVertsInSegment = SegmentPoints.Num(); + if (NumVertsInSegment <= 0) + { + // Create blank entries for this invalid segment + OutSplinesData.SegmentPaintLayerNames.AddDefaulted(); + OutSplinesData.SegmentRaiseTerrains.AddDefaulted(); + OutSplinesData.SegmentLowerTerrains.AddDefaulted(); + + continue; + } + + // TODO: handle case NumVertsInSegment == 1 + int32 UnResampledPointIndex = 1; + + FHoudiniUnResampledPoint UnResampledPoint0 = SegmentData.UnResampledPoints[0]; + FHoudiniUnResampledPoint UnResampledPoint1 = SegmentData.UnResampledPoints[1]; + // If we are resampling, calculate the Alpha value [0, 1] along the segment, with Point 0 at Alpha = 0. + if (bResampleSplines) + UnResampledPoint1.Alpha = (UnResampledPoint1.GetSelectedPosition() - UnResampledPoint0.GetSelectedPosition()).Length() / SegmentData.SegmentLength; + + // Loop for the number of resampled points we'll have for this segment (which could be equal to original number + // of points in segment if we are not resampling) + const int32 NumResampledVertsInSegment = OutSplinesData.VertexCounts[SegmentData.GlobalSegmentIndex]; + for (int32 ResampledSegmentVertIdx = 0; ResampledSegmentVertIdx < NumResampledVertsInSegment; ++ResampledSegmentVertIdx, ++OutputPointIdx) + { + FVector ResampledPosition; + FRotator ResampledRotation; + + float CalculatedHalfWidth = 0; + float CalculatedSideFalloff = 0; + if (bResampleSplines) + { + // Find P0 and P1: the unresampled points before and after the resampled point on the spline + const float Alpha = static_cast(ResampledSegmentVertIdx) / (NumResampledVertsInSegment - 1.0f); + while (Alpha > UnResampledPoint1.Alpha && UnResampledPointIndex < NumVertsInSegment - 1) + { + UnResampledPoint0 = UnResampledPoint1; + UnResampledPointIndex++; + UnResampledPoint1 = SegmentData.UnResampledPoints[UnResampledPointIndex]; + UnResampledPoint1.Alpha = UnResampledPoint0.Alpha + (UnResampledPoint1.GetSelectedPosition() - UnResampledPoint0.GetSelectedPosition()).Length() / SegmentData.SegmentLength; + } + + if (ResampledSegmentVertIdx == 0) + { + // The first point is a control point and always the same as the unresampled spline's first point + ResampledPosition = UnResampledPoint0.GetSelectedPosition(); + ResampledRotation = UnResampledPoint0.Rotation.Rotator(); + } + else if (ResampledSegmentVertIdx == NumResampledVertsInSegment - 1) + { + // The last point is a control point and always the same as the unresampled spline's last point + ResampledPosition = UnResampledPoint1.GetSelectedPosition(); + ResampledRotation = UnResampledPoint1.Rotation.Rotator(); + } + else + { + // Calculate the [0, 1] value representing the position of the resampled point between P0 and P1 + const float ResampleAlpha = (Alpha - UnResampledPoint0.Alpha) / (UnResampledPoint1.Alpha - UnResampledPoint0.Alpha); + // Lerp to calculate the resampled point's position + ResampledPosition = FMath::Lerp( + UnResampledPoint0.GetSelectedPosition(), UnResampledPoint1.GetSelectedPosition(), ResampleAlpha); + + // Slerp to calculate the resampled point's rotation + ResampledRotation = FQuat::Slerp( + UnResampledPoint0.Rotation, UnResampledPoint1.Rotation, ResampleAlpha).Rotator(); + + // On points that are not control points, the half-width should be half the distance between the + // Right and Left points going through the Center point + const FVector ResampledLeft = FMath::Lerp( + UnResampledPoint0.Left, UnResampledPoint1.Left, ResampleAlpha); + const FVector ResampledRight = FMath::Lerp( + UnResampledPoint0.Right, UnResampledPoint1.Right, ResampleAlpha); + CalculatedHalfWidth = ((ResampledPosition - ResampledRight) + (ResampledLeft - ResampledPosition)).Length() / 2.0; + + const FVector ResampledLeftFalloff = FMath::Lerp( + UnResampledPoint0.FalloffLeft, UnResampledPoint1.FalloffLeft, ResampleAlpha); + const FVector ResampledRightFalloff = FMath::Lerp( + UnResampledPoint0.FalloffRight, UnResampledPoint1.FalloffRight, ResampleAlpha); + CalculatedSideFalloff = ((ResampledRightFalloff - ResampledRight).Length() + + (ResampledLeftFalloff - ResampledLeft).Length()) / 2.0; + } + } + else + { + // We are not resampling, so simply copy the unresampled position at this index + UnResampledPointIndex = ResampledSegmentVertIdx; + UnResampledPoint1 = SegmentData.UnResampledPoints[UnResampledPointIndex]; + ResampledPosition = UnResampledPoint1.GetSelectedPosition(); + ResampledRotation = UnResampledPoint1.Rotation.Rotator(); + + if (ResampledSegmentVertIdx > 0 && ResampledSegmentVertIdx < NumResampledVertsInSegment - 1) + { + const FLandscapeSplineInterpPoint& SegmentPoint = SegmentPoints[UnResampledPointIndex]; + + // On points that are not control points, the half-width should be half the distance between the + // Right and Left points going through the Center point + CalculatedHalfWidth = ((SegmentPoint.Center - SegmentPoint.Right) + (SegmentPoint.Left - SegmentPoint.Center)).Length() / 2.0; + CalculatedSideFalloff = ((SegmentPoint.FalloffRight - SegmentPoint.Right).Length() + + (SegmentPoint.FalloffLeft - SegmentPoint.Left).Length()) / 2.0; + } + } + + if (ResampledSegmentVertIdx == 0) + { + // First point is a control point, add the socket name + static constexpr int32 ConnectionIdx = 0; + OutSplinesData.PointConnectionSocketNames.Emplace(SegmentData.Segment->Connections[ConnectionIdx].SocketName.ToString()); + OutSplinesData.PointConnectionTangentLengths.Add(SegmentData.Segment->Connections[ConnectionIdx].TangentLen); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + TObjectPtr CPoint = SegmentData.Segment->Connections[ConnectionIdx].ControlPoint; +#else + ULandscapeSplineControlPoint const* const CPoint = SegmentData.Segment->Connections[ConnectionIdx].ControlPoint; +#endif + if (!IsValid(CPoint)) + { + OutSplinesData.ControlPointAttributes.AddEmpty(); + } + else + { + OutSplinesData.ControlPointAttributes.AddControlPointData( + CPoint, OutputPointIdx, InControlPointIdMap, InNextControlPointId); + } + } + else if (ResampledSegmentVertIdx == NumResampledVertsInSegment - 1) + { + // Last point is a control point, add the socket name + static constexpr int32 ConnectionIdx = 1; + OutSplinesData.PointConnectionSocketNames.Emplace(SegmentData.Segment->Connections[ConnectionIdx].SocketName.ToString()); + OutSplinesData.PointConnectionTangentLengths.Add(SegmentData.Segment->Connections[ConnectionIdx].TangentLen); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + TObjectPtr CPoint = SegmentData.Segment->Connections[ConnectionIdx].ControlPoint; +#else + ULandscapeSplineControlPoint const* const CPoint = SegmentData.Segment->Connections[ConnectionIdx].ControlPoint; +#endif + if (!IsValid(CPoint)) + { + OutSplinesData.ControlPointAttributes.AddEmpty(); + } + else + { + OutSplinesData.ControlPointAttributes.AddControlPointData( + CPoint, OutputPointIdx, InControlPointIdMap, InNextControlPointId); + } + } + else + { + // for other points the socket names, tangent lengths and control point name attributes are empty + OutSplinesData.PointConnectionSocketNames.AddDefaulted(); + OutSplinesData.PointConnectionTangentLengths.AddDefaulted(); + OutSplinesData.ControlPointAttributes.AddEmpty(); + // The control point width was calculated, set that manually + OutSplinesData.ControlPointAttributes.HalfWidths.Last() = CalculatedHalfWidth / HAPI_UNREAL_SCALE_FACTOR_POSITION; + OutSplinesData.ControlPointAttributes.SideFalloffs.Last() = CalculatedSideFalloff / HAPI_UNREAL_SCALE_FACTOR_POSITION; + // We don't have calculated end-falloff values for non-control points, set to 0 + OutSplinesData.ControlPointAttributes.EndFalloffs.Last() = 0.0f; + // Set the calculated / resampled rotation + ConvertAndSetRotation( + ResampledRotation, OutSplinesData.ControlPointAttributes.Rotations.Num() - 4, OutSplinesData.ControlPointAttributes.Rotations); + } + + // Set the final point position + OutSplinesData.PointPositions.Add(ResampledPosition.X / HAPI_UNREAL_SCALE_FACTOR_POSITION); + // Swap Y/Z + OutSplinesData.PointPositions.Add(ResampledPosition.Z / HAPI_UNREAL_SCALE_FACTOR_POSITION); + OutSplinesData.PointPositions.Add(ResampledPosition.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION); + } + + // Extract general properties from the segment + OutSplinesData.SegmentPaintLayerNames.Emplace(SegmentData.Segment->LayerName.ToString()); + OutSplinesData.SegmentRaiseTerrains.Add(SegmentData.Segment->bRaiseTerrain); + OutSplinesData.SegmentLowerTerrains.Add(SegmentData.Segment->bLowerTerrain); + + // Extract the spline mesh configuration for the segment + const int32 NumMeshes = SegmentData.Segment->SplineMeshes.Num(); + // Grow PerMeshSegmentData if needed + if (OutSplinesData.PerMeshSegmentData.Num() < NumMeshes) + { + OutSplinesData.PerMeshSegmentData.SetNum(NumMeshes); + } + for (int32 MeshIdx = 0; MeshIdx < NumMeshes; ++MeshIdx) + { + const FLandscapeSplineMeshEntry& SplineMeshEntry = SegmentData.Segment->SplineMeshes[MeshIdx]; + FHoudiniUnrealLandscapeSplineSegmentMeshData& SegmentMeshData = OutSplinesData.PerMeshSegmentData[MeshIdx]; + // Initialize mesh per segment array if needed + if (SegmentMeshData.MeshRefs.IsEmpty()) + { + SegmentMeshData.MeshRefs.SetNum(TotalNumSegments); + } + + // Set mesh reference (if there is a valid mesh for this entry) + if (IsValid(SplineMeshEntry.Mesh)) + { + SegmentMeshData.MeshRefs[SegmentData.GlobalSegmentIndex] = SplineMeshEntry.Mesh->GetPathName(); + } + + // Material overrides: initialize the array to num material overrides + const int32 NumMaterialOverrides = SplineMeshEntry.MaterialOverrides.Num(); + if (SegmentMeshData.MeshMaterialOverrideRefs.IsEmpty()) + { + SegmentMeshData.MeshMaterialOverrideRefs.SetNum(NumMaterialOverrides); + } + + // Set the material override refs + for (int32 MaterialOverrideIdx = 0; MaterialOverrideIdx < NumMaterialOverrides; ++MaterialOverrideIdx) + { + TArray& MaterialOverrideRefs = SegmentMeshData.MeshMaterialOverrideRefs[MaterialOverrideIdx]; + // Ensure there is enough space in the array for the segments + if (MaterialOverrideRefs.Num() < TotalNumSegments) + { + MaterialOverrideRefs.SetNum(TotalNumSegments); + } + + UMaterialInterface const* const MaterialOverride = SplineMeshEntry.MaterialOverrides[MaterialOverrideIdx]; + if (!IsValid(MaterialOverride)) + { + MaterialOverrideRefs[SegmentData.GlobalSegmentIndex] = TEXT(""); + continue; + } + + MaterialOverrideRefs[SegmentData.GlobalSegmentIndex] = MaterialOverride->GetPathName(); + } + + // Initialize mesh scale per segment array if needed + if (SegmentMeshData.MeshScales.IsEmpty()) + { + SegmentMeshData.MeshScales.SetNum(TotalNumSegments * 3); + } + SegmentMeshData.MeshScales[SegmentData.GlobalSegmentIndex * 3 + 0] = SplineMeshEntry.Scale.X; + SegmentMeshData.MeshScales[SegmentData.GlobalSegmentIndex * 3 + 1] = SplineMeshEntry.Scale.Z; + SegmentMeshData.MeshScales[SegmentData.GlobalSegmentIndex * 3 + 2] = SplineMeshEntry.Scale.Y; + } + } + } + + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::ExtractSplineControlPointsData( + ULandscapeSplinesComponent* const InSplinesComponent, + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId, + FHoudiniUnrealLandscapeSplinesControlPointData& OutSplinesControlPointData) +{ + if (!IsValid(InSplinesComponent)) + return false; + + if (!InSplinesComponent->HasAnyControlPointsOrSegments()) + return false; + + // Use helper to fetch control points since the landscape splines API differs between UE 5.0 and 5.1+ + TArray> ControlPoints; + if (!FHoudiniEngineRuntimeUtils::GetLandscapeSplinesControlPoints(InSplinesComponent, ControlPoints)) + return false; + + // Initialize control point arrays + const int32 NumControlPoints = ControlPoints.Num(); + OutSplinesControlPointData.ControlPointPositions.Empty(NumControlPoints * 3); + OutSplinesControlPointData.Attributes.Init(NumControlPoints); + + for (int32 ControlPointIdx = 0; ControlPointIdx < NumControlPoints; ++ControlPointIdx) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + TObjectPtr CPoint = ControlPoints[ControlPointIdx]; +#else + ULandscapeSplineControlPoint const* const CPoint = ControlPoints[ControlPointIdx]; +#endif + if (!IsValid(CPoint)) + continue; + + // Convert the position and rotation values to Houdini's coordinate system and scale + OutSplinesControlPointData.ControlPointPositions.Add(CPoint->Location.X / HAPI_UNREAL_SCALE_FACTOR_POSITION); + OutSplinesControlPointData.ControlPointPositions.Add(CPoint->Location.Z / HAPI_UNREAL_SCALE_FACTOR_POSITION); + OutSplinesControlPointData.ControlPointPositions.Add(CPoint->Location.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION); + + OutSplinesControlPointData.Attributes.AddControlPointData( + CPoint, ControlPointIdx, InControlPointIdMap, InNextControlPointId); + } + + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::AddTargetLandscapeAttribute( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + ALandscapeProxy const* const InLandscapeActor, + int InCount, + HAPI_AttributeOwner InAttribOwner) +{ + if (!IsValid(InLandscapeActor)) + return false; + + // Extract the actor path + const FString LandscapeActorPath = InLandscapeActor->GetPathName(); + + // Set the attribute's string data + HAPI_AttributeInfo AttrInfo; + FHoudiniApi::AttributeInfo_Init(&AttrInfo); + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_TARGET_LANDSCAPE); + Accessor.AddAttribute(InAttribOwner, HAPI_STORAGETYPE_STRING, 1, InCount, &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttrInfo, LandscapeActorPath), false); + + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::AddOutputAttribute( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + int InValue, + int InCount, + HAPI_AttributeOwner InAttribOwner) +{ + // Set the attribute's string data + TArray LandscapeSplineOutput; + LandscapeSplineOutput.Init(InValue, InCount); + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, InPartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE); + Accessor.AddAttribute(InAttribOwner, HAPI_STORAGETYPE_INT, 1, InCount, &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttrInfo, LandscapeSplineOutput), false); + + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::AddPositionAttribute(HAPI_NodeId InNodeId, const TArray& InPositions) +{ + if (InPositions.IsEmpty()) + return false; + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + Accessor.AddAttribute(HAPI_ATTROWNER_POINT, HAPI_STORAGETYPE_FLOAT, 3, InPositions.Num() / 3, &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttrInfo, InPositions), false); + + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::AddPaintLayerNameAttribute(HAPI_NodeId InNodeId, const TArray& InData, HAPI_AttributeOwner InAttribOwner) +{ + if (InData.IsEmpty()) + return false; + + const char * AttributeName = InAttribOwner == HAPI_ATTROWNER_POINT + ? HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_PAINT_LAYER_NAME + : HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_PAINT_LAYER_NAME; + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, AttributeName); + Accessor.AddAttribute(InAttribOwner, HAPI_STORAGETYPE_STRING, 1, InData.Num(), &AttrInfo); + bool bSuccess = Accessor.SetAttributeData(AttrInfo, InData); + + return bSuccess; +} + + +bool FUnrealLandscapeSplineTranslator::AddRaiseTerrainAttribute(HAPI_NodeId InNodeId, const TArray& InData, HAPI_AttributeOwner InAttribOwner) +{ + if (InData.IsEmpty()) + return false; + + const FString AttributeName = InAttribOwner == HAPI_ATTROWNER_POINT + ? HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_RAISE_TERRAIN + : HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_RAISE_TERRAIN; + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, TCHAR_TO_ANSI(*AttributeName)); + Accessor.AddAttribute(InAttribOwner, HAPI_STORAGETYPE_INT8, 1, InData.Num(), &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttrInfo, InData), false); + + return true; +} + + +bool FUnrealLandscapeSplineTranslator::AddLowerTerrainAttribute(HAPI_NodeId InNodeId, const TArray& InData, HAPI_AttributeOwner InAttribOwner) +{ + if (InData.IsEmpty()) + return false; + + + const FString AttributeName = InAttribOwner == HAPI_ATTROWNER_POINT + ? HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_LOWER_TERRAIN + : HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_LOWER_TERRAIN; + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, TCHAR_TO_ANSI(*AttributeName)); + Accessor.AddAttribute(InAttribOwner, HAPI_STORAGETYPE_INT8, 1, InData.Num(), &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttrInfo, InData), false); + + return true; +} + + +bool FUnrealLandscapeSplineTranslator::AddSegmentMeshesAttributes(HAPI_NodeId InNodeId, const TArray& InPerMeshSegmentData) +{ + int32 NumMeshAttrs = InPerMeshSegmentData.Num(); + if (NumMeshAttrs <= 0) + return false; + + const int32 NumSegments = InPerMeshSegmentData[0].MeshRefs.Num(); + + bool bNeedToCommit = false; + for (int32 MeshIdx = 0; MeshIdx < NumMeshAttrs; ++MeshIdx) + { + const FHoudiniUnrealLandscapeSplineSegmentMeshData& MeshSegmentData = InPerMeshSegmentData[MeshIdx]; + HAPI_AttributeInfo AttrInfo; + + // Add the mesh attribute + FString MeshAttrName = (MeshIdx == 0 ? FString(TEXT(HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_MESH)) : FString::Printf(TEXT(HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SEGMENT_MESH "%d"), MeshIdx)); + FHoudiniHapiAccessor MeshAttrAccessor(InNodeId, 0, TCHAR_TO_ANSI(*MeshAttrName)); + MeshAttrAccessor.AddAttribute(HAPI_ATTROWNER_PRIM, HAPI_STORAGETYPE_STRING, 1, NumSegments, &AttrInfo); + MeshAttrAccessor.SetAttributeData(AttrInfo, MeshSegmentData.MeshRefs); + + // Add the mesh scale attribute + FString MeshScaleAttrName = FString::Printf(TEXT("%s%s"), *MeshAttrName, TEXT(HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_SCALE_SUFFIX)); + FHoudiniHapiAccessor ScaleAttrAccessor(InNodeId, 0, TCHAR_TO_ANSI(*MeshScaleAttrName)); + ScaleAttrAccessor.AddAttribute(HAPI_ATTROWNER_PRIM, HAPI_STORAGETYPE_FLOAT, 3, NumSegments, &AttrInfo); + bool bSuccess = ScaleAttrAccessor.SetAttributeData(AttrInfo, MeshSegmentData.MeshScales); + + // Material overrides + int NumMaterialOverrides = MeshSegmentData.MeshMaterialOverrideRefs.Num(); + FString MaterialOverrideAttrNamePrefix = FString::Printf(TEXT("%s%s"), *MeshAttrName, TEXT(HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_MATERIAL_OVERRIDE_SUFFIX)); + for (int MaterialOverrideIdx = 0; MaterialOverrideIdx < NumMaterialOverrides; ++MaterialOverrideIdx) + { + FString MaterialOverrideAttrName = (MaterialOverrideIdx == 0? MaterialOverrideAttrNamePrefix : FString::Printf(TEXT("%s%d"), *MaterialOverrideAttrNamePrefix, MaterialOverrideIdx)); + FHoudiniHapiAccessor MaterialAttrAccessor(InNodeId, 0, TCHAR_TO_ANSI(*MaterialOverrideAttrName)); + MaterialAttrAccessor.AddAttribute(HAPI_ATTROWNER_PRIM, HAPI_STORAGETYPE_STRING, 1, NumSegments, &AttrInfo); + MaterialAttrAccessor.SetAttributeData(AttrInfo, MeshSegmentData.MeshMaterialOverrideRefs[MaterialOverrideIdx]); + } + } + + return bNeedToCommit; +} + + +bool +FUnrealLandscapeSplineTranslator::AddConnectionSocketNameAttribute(HAPI_NodeId InNodeId, const TArray& InData) +{ + if (InData.IsEmpty()) + return false; + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_SOCKET_NAME); + Accessor.AddAttribute(HAPI_ATTROWNER_POINT, HAPI_STORAGETYPE_STRING, 1, InData.Num(), &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttrInfo, InData), false); + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::AddRotationAttribute(HAPI_NodeId InNodeId, const TArray& InData) +{ + if (InData.IsEmpty()) + return false; + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, HAPI_UNREAL_ATTRIB_ROTATION); + Accessor.AddAttribute(HAPI_ATTROWNER_POINT, HAPI_STORAGETYPE_FLOAT, 4, InData.Num() / 4, &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttrInfo, InData), false); + return true; + +} + + +bool +FUnrealLandscapeSplineTranslator::AddMeshAttribute(HAPI_NodeId InNodeId, const TArray& InData) +{ + if (InData.IsEmpty()) + return false; + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_MESH); + Accessor.AddAttribute(HAPI_ATTROWNER_POINT, HAPI_STORAGETYPE_STRING, 1, InData.Num(), &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttrInfo, InData), false); + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::AddMaterialOverrideAttributes(HAPI_NodeId InNodeId, const TArray>& InMaterialOverrideRefs) +{ + // The InMaterialOverrideRefs[0] array contains the values for unreal_landscape_spline_mesh_material_override for all control points +// InPerMeshSegmentData[1] array contains the values for unreal_landscape_spline_mesh_material_override1 for all control points etc + + if (InNodeId < 0) + return false; + + const int NumMaterialOverrides = InMaterialOverrideRefs.Num(); + if (NumMaterialOverrides <= 0) + return false; + + int NumControlPoints = InMaterialOverrideRefs[0].Num(); + bool bNeedToCommit = false; + const FString AttrNamePrefix(TEXT(HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_MESH HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_MATERIAL_OVERRIDE_SUFFIX)); + for (int32 MaterialOverrideIdx = 0; MaterialOverrideIdx < NumMaterialOverrides; ++MaterialOverrideIdx) + { + // Add the material override attribute + const FString MaterialOverrideAttrName = (MaterialOverrideIdx == 0) + ? AttrNamePrefix + : FString::Printf(TEXT("%s%d"), *AttrNamePrefix, MaterialOverrideIdx); + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, TCHAR_TO_ANSI(*MaterialOverrideAttrName)); + Accessor.AddAttribute(HAPI_ATTROWNER_POINT, HAPI_STORAGETYPE_STRING, 1, NumControlPoints, &AttrInfo); + Accessor.SetAttributeData(AttrInfo, InMaterialOverrideRefs[MaterialOverrideIdx]); + } + + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::AddIdsAttribute(HAPI_NodeId InNodeId, const TArray& InData) +{ + if (InData.IsEmpty()) + return false; + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_ID); + Accessor.AddAttribute(HAPI_ATTROWNER_POINT, HAPI_STORAGETYPE_INT, 1, InData.Num(), &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttrInfo, InData), false); + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::AddHalfWidthAttribute(HAPI_NodeId InNodeId, const TArray& InData) +{ + if (InData.IsEmpty()) + return false; + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_HALF_WIDTH); + Accessor.AddAttribute(HAPI_ATTROWNER_POINT, HAPI_STORAGETYPE_FLOAT, 1, InData.Num(), &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttrInfo, InData), false); + return true; +} + +bool +FUnrealLandscapeSplineTranslator::AddSideFalloffAttribute(HAPI_NodeId InNodeId, const TArray& InData) +{ + if (InData.IsEmpty()) + return false; + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_SIDE_FALLOFF); + Accessor.AddAttribute(HAPI_ATTROWNER_POINT, HAPI_STORAGETYPE_FLOAT, 1, InData.Num(), &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttrInfo, InData), false); + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::AddEndFalloffAttribute(HAPI_NodeId InNodeId, const TArray& InData) +{ + if (InData.IsEmpty()) + return false; + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_END_FALLOFF); + Accessor.AddAttribute(HAPI_ATTROWNER_POINT, HAPI_STORAGETYPE_FLOAT, 1, InData.Num(), &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttrInfo, InData), false); + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::AddTangentLengthAttribute(HAPI_NodeId InNodeId, const TArray& InData) +{ + if (InData.IsEmpty()) + return false; + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_TANGENT_LENGTH); + Accessor.AddAttribute(HAPI_ATTROWNER_POINT, HAPI_STORAGETYPE_FLOAT, 1, InData.Num(), &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttrInfo, InData), false); + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::AddMeshScaleAttribute(HAPI_NodeId InNodeId, const TArray& InData) +{ + if (InData.IsEmpty()) + return false; + + const char * AttrName = HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_CONTROL_POINT_MESH HAPI_UNREAL_ATTRIB_LANDSCAPE_SPLINE_MESH_SCALE_SUFFIX; + + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InNodeId, 0, AttrName); + Accessor.AddAttribute(HAPI_ATTROWNER_POINT, HAPI_STORAGETYPE_FLOAT, 3, InData.Num() / 3, &AttrInfo); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttrInfo, InData), false); + + return true; +} + + +bool +FUnrealLandscapeSplineTranslator::AddControlPointAttributes(HAPI_NodeId InNodeId, const FHoudiniUnrealLandscapeSplineControlPointAttributes& InControlPointAttributes) +{ + bool bNeedToCommit = false; + + if (AddRotationAttribute(InNodeId, InControlPointAttributes.Rotations)) + bNeedToCommit = true; + + if (AddIdsAttribute(InNodeId, InControlPointAttributes.Ids)) + bNeedToCommit = true; + + if (AddHalfWidthAttribute(InNodeId, InControlPointAttributes.HalfWidths)) + bNeedToCommit = true; + + if (AddSideFalloffAttribute(InNodeId, InControlPointAttributes.SideFalloffs)) + bNeedToCommit = true; + + if (AddEndFalloffAttribute(InNodeId, InControlPointAttributes.EndFalloffs)) + bNeedToCommit = true; + + if (AddPaintLayerNameAttribute(InNodeId, InControlPointAttributes.PaintLayerNames, HAPI_ATTROWNER_POINT)) + bNeedToCommit = true; + + if (AddRaiseTerrainAttribute(InNodeId, InControlPointAttributes.RaiseTerrains, HAPI_ATTROWNER_POINT)) + bNeedToCommit = true; + + if (AddLowerTerrainAttribute(InNodeId, InControlPointAttributes.LowerTerrains, HAPI_ATTROWNER_POINT)) + bNeedToCommit = true; + + if (AddMeshAttribute(InNodeId, InControlPointAttributes.MeshRefs)) + bNeedToCommit = true; + + if (AddMaterialOverrideAttributes(InNodeId, InControlPointAttributes.MaterialOverrideRefs)) + bNeedToCommit = true; + + if (AddMeshScaleAttribute(InNodeId, InControlPointAttributes.MeshScales)) + bNeedToCommit = true; + return bNeedToCommit; +} + +``` + +`Source/HoudiniEngine/Private/UnrealLandscapeSplineTranslator.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "Runtime/Launch/Resources/Version.h" +#include "LandscapeSplinesComponent.h" + +#include "HAPI/HAPI_Common.h" + +struct FHoudiniUnrealLandscapeSplinesData; +class FHoudiniUnrealLandscapeSplineControlPointAttributes; +struct FLandscapeSplineInterpPoint; +class ALandscapeProxy; +class ULandscapeSplinesComponent; +class UMaterialInterface; +class UStaticMesh; + +class FUnrealObjectInputHandle; +struct FHoudiniUnrealLandscapeSplineSegmentMeshData; +struct FHoudiniUnrealLandscapeSplinesControlPointData; +struct FHoudiniUnrealLandscapeSplinesData; + + +enum class EHoudiniUnrealLandscapeSplineCurve +{ + Center = 0, + Left = 1, + Right = 2 +}; + + +class FHoudiniUnrealLandscapeSplineControlPointAttributes +{ +public: + /** Empties all arrays and reserve enough space for InExpectedPointCount entries. */ + void Init(int32 InPointCount); + + /** Add an entry to each array with the property values from InControlPoint. */ + bool AddControlPointData( +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + TObjectPtr& InControlPoint, +#else + const ULandscapeSplineControlPoint* InControlPoint, +#endif + int32 InControlPointIndex, + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId); + + /** Add an empty / default initialized entry to each array. */ + void AddEmpty(); + + TArray Rotations; + TArray PaintLayerNames; + TArray RaiseTerrains; + TArray LowerTerrains; + TArray MeshRefs; + TArray> MaterialOverrideRefs; + TArray MeshScales; + TArray Ids; + TArray HalfWidths; + TArray SideFalloffs; + TArray EndFalloffs; + +private: + int32 PointCount = 0; +}; + + +struct FHoudiniUnrealLandscapeSplineSegmentMeshData +{ + TArray MeshRefs; + TArray> MeshMaterialOverrideRefs; + TArray MeshScales; +}; + + +struct FHoudiniUnrealLandscapeSplinesData +{ + /** Point positions (xyz) for all segments. */ + TArray PointPositions; + + /** Vertex counts: the number of vertices per landscape spline. */ + TArray VertexCounts; + + /** Per-segment paint layer names */ + TArray SegmentPaintLayerNames; + + /** Per-segment bRaiseTerrain */ + TArray SegmentRaiseTerrains; + + /** Per-segment bLowerTerrain */ + TArray SegmentLowerTerrains; + + /** Static mesh attribute, the outer index is mesh 0, 1, 2 ... The struct contains the per-segment data */ + TArray PerMeshSegmentData; + + /** + * The mesh socket names on the splines' points, each index is a point index. Only the point indices that + * correspond to control points (first and last point of each segment) will have values set, the rest of the + * array will contain empty strings. + */ + TArray PointConnectionSocketNames; + + /** + * If a point corresponds with a control point on the spline, this contains the control point's tangent length + * for the segment connection. + */ + TArray PointConnectionTangentLengths; + + /** Control point specific attributes. */ + FHoudiniUnrealLandscapeSplineControlPointAttributes ControlPointAttributes; +}; + + +struct FHoudiniUnrealLandscapeSplinesControlPointData +{ + /** + * The control point positions of the splines. These are the original positions unaffected by connection mesh + * sockets. + */ + TArray ControlPointPositions; + + /** Control point attributes. */ + FHoudiniUnrealLandscapeSplineControlPointAttributes Attributes; +}; + + +/** + * Helper struct for storing unresampled points (center, left and right) and the point's normalized [0, 1] position + * along the spline. + */ +class FHoudiniUnResampledPoint +{ +public: + FHoudiniUnResampledPoint() = delete; + + FHoudiniUnResampledPoint(EHoudiniUnrealLandscapeSplineCurve InSplineSelection); + FHoudiniUnResampledPoint(EHoudiniUnrealLandscapeSplineCurve InSplineSelection, const FLandscapeSplineInterpPoint& InPoint); + + FVector GetSelectedPosition() const; + FQuat CalculateRotationTo(const FHoudiniUnResampledPoint& InNextPoint); + + FVector Center; + FVector Left; + FVector Right; + FVector FalloffLeft; + FVector FalloffRight; + FQuat Rotation; + float Alpha; + EHoudiniUnrealLandscapeSplineCurve SplineSelection; +}; + +struct HOUDINIENGINE_API FUnrealLandscapeSplineTranslator +{ +public: + // Create HAPI nodes and send the landscape splines of InLandscapeSplines to Houdini. + static bool CreateInputNode( + ULandscapeSplinesComponent* const InSplinesComponent, + bool bForceReferenceInputNodeCreation, + HAPI_NodeId& OutInputNodeId, + FUnrealObjectInputHandle& OutInputNodeHandle, + const FString& InNodeName, + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId, + float InSplineResolution=0.0f, + bool bInExportCurves=true, + bool bInExportControlPoints=false, + bool bInExportLeftRightCurves=false, + bool bInInputNodesCanBeDeleted=true); + + // Create a null SOP with a curve for each spline/segment of InSplinesComponent. + static bool CreateInputNode( + ULandscapeSplinesComponent* const InSplinesComponent, + HAPI_NodeId InObjectNodeId, + const FString& InNodeName, + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId, + HAPI_NodeId& OutNodeId, + EHoudiniUnrealLandscapeSplineCurve InExportCurve=EHoudiniUnrealLandscapeSplineCurve::Center, + float InSplineResolution=0.0f); + + // Create a null SOP with a point cloud of the control points of InSplinesComponent. + static bool CreateInputNodeForControlPoints( + ULandscapeSplinesComponent* const InSplinesComponent, + const HAPI_NodeId& InObjectNodeId, + const FString& InNodeName, + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId, + HAPI_NodeId& OutNodeId); + +private: + // Extract landscape splines data arrays: positions, and various attributes. + static bool ExtractSplineData( + ULandscapeSplinesComponent* const InSplinesComponent, + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId, + FHoudiniUnrealLandscapeSplinesData& OutSplinesData, + EHoudiniUnrealLandscapeSplineCurve InExportCurve=EHoudiniUnrealLandscapeSplineCurve::Center, + float InSplineResolution=0.0f); + + // landscape splines control points data arrays: positions, rotations, and various attributes. + static bool ExtractSplineControlPointsData( + ULandscapeSplinesComponent* const InSplinesComponent, + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId, + FHoudiniUnrealLandscapeSplinesControlPointData& OutSplinesControlPointData); + + // Adds the landscape spline target landscape prim attribute (target = InLandscapeActor). + static bool AddTargetLandscapeAttribute( + HAPI_NodeId InNodeId, + HAPI_PartId InPartId, + ALandscapeProxy const* const InLandscapeActor, + int InCount, + HAPI_AttributeOwner InAttribOwner=HAPI_ATTROWNER_PRIM); + + static bool AddOutputAttribute(HAPI_NodeId InNodeId, HAPI_PartId InPartId, int InValue, int InCount,HAPI_AttributeOwner InAttribOwner=HAPI_ATTROWNER_PRIM); + static bool AddPositionAttribute(HAPI_NodeId InNodeId, const TArray& InPositions); + static bool AddPaintLayerNameAttribute(HAPI_NodeId InNodeId, const TArray& InPaintLayerNames, HAPI_AttributeOwner InAttribOwner); + static bool AddRaiseTerrainAttribute(HAPI_NodeId InNodeId, const TArray& InRaiseTerrain, HAPI_AttributeOwner InAttribOwner); + static bool AddLowerTerrainAttribute(HAPI_NodeId InNodeId, const TArray& InLowerTerrain, HAPI_AttributeOwner InAttribOwner); + static bool AddSegmentMeshesAttributes(HAPI_NodeId InNodeId, const TArray& InPerMeshSegmentData); + static bool AddConnectionSocketNameAttribute(HAPI_NodeId InNodeId, const TArray& InPointConnectionSocketNames); + static bool AddRotationAttribute(HAPI_NodeId InNodeId, const TArray& InControlPointRotations); + static bool AddMeshAttribute(HAPI_NodeId InNodeId, const TArray& InMeshRefs); + static bool AddMaterialOverrideAttributes(HAPI_NodeId InNodeId, const TArray>& InMaterialOverrideRefs); + static bool AddIdsAttribute(HAPI_NodeId InNodeId, const TArray& InControlPointIds); + static bool AddHalfWidthAttribute(HAPI_NodeId InNodeId, const TArray& InHalfWidths); + static bool AddSideFalloffAttribute(HAPI_NodeId InNodeId, const TArray& InSideFalloffs); + static bool AddEndFalloffAttribute(HAPI_NodeId InNodeId, const TArray& InEndFalloffs); + static bool AddTangentLengthAttribute(HAPI_NodeId InNodeId, const TArray& InTangentLengths); + static bool AddMeshScaleAttribute(HAPI_NodeId InNodeId, const TArray& InTangentLengths); + static bool AddControlPointAttributes(HAPI_NodeId InNodeId, const FHoudiniUnrealLandscapeSplineControlPointAttributes& InControlPointAttributes); +}; + + +``` + +`Source/HoudiniEngine/Private/UnrealLandscapeTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniApi.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngine.h" +#include "HoudiniRuntimeSettings.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineString.h" +#include "HoudiniInput.h" + +#include "UnrealLandscapeTranslator.h" + +#include "HoudiniDataLayerUtils.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniGeoPartObject.h" + +#include "Landscape.h" +#include "LandscapeStreamingProxy.h" +#include "LandscapeDataAccess.h" +#include "LandscapeEdit.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 +#include "LandscapeEditLayer.h" +#endif +#include "LightMap.h" +#include "Engine/MapBuildDataRegistry.h" +#include "PhysicalMaterials/PhysicalMaterial.h" + +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputRuntimeUtils.h" +#include "UnrealObjectInputUtils.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniHLODLayerUtils.h" +#include "HoudiniLandscapeUtils.h" + + +bool +FUnrealLandscapeTranslator::CreateMeshOrPointsFromLandscape( + ALandscapeProxy* LandscapeProxy, + HAPI_NodeId& CreatedNodeId, + const FString& InputNodeNameString, + const bool bExportGeometryAsMesh, + const bool bExportTileUVs, + const bool bExportNormalizedUVs, + const bool bExportLighting, + const bool bExportMaterials, + const bool bApplyWorldTransform, + const HAPI_NodeId& ParentNodeId) +{ + //-------------------------------------------------------------------------------------------------- + // 1. Create an input node + //-------------------------------------------------------------------------------------------------- + HAPI_NodeId InputNodeId = -1; + // Create the curve SOP Node + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(ParentNodeId, TEXT("null"), InputNodeNameString, true, &InputNodeId), false); + + // Check if we have a valid id for this new input asset. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(InputNodeId)) + return false; + + // We now have a valid id. + CreatedNodeId = InputNodeId; + + if (!FHoudiniEngineUtils::HapiCookNode(InputNodeId, nullptr, true)) + return false; + + //-------------------------------------------------------------------------------------------------- + // 2. Set the part info + //-------------------------------------------------------------------------------------------------- + int32 ComponentSizeQuads = ((LandscapeProxy->ComponentSizeQuads + 1) >> LandscapeProxy->ExportLOD) - 1; + float ScaleFactor = (float)LandscapeProxy->ComponentSizeQuads / (float)ComponentSizeQuads; + + //int32 NumComponents = bExportOnlySelected ? SelectedComponents.Num() : LandscapeProxy->LandscapeComponents.Num(); + int32 NumComponents = LandscapeProxy->LandscapeComponents.Num(); + int32 VertexCountPerComponent = FMath::Square(ComponentSizeQuads + 1); + int32 VertexCount = NumComponents * VertexCountPerComponent; + + if (!VertexCount) + return false; + + + int32 TriangleCount = NumComponents * FMath::Square(ComponentSizeQuads) * 2; + int32 QuadCount = NumComponents * FMath::Square(ComponentSizeQuads); + int32 IndexCount = QuadCount * 4; + + // Create part info + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + //FMemory::Memzero< HAPI_PartInfo >(Part); + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + Part.vertexCount = 0; + Part.faceCount = 0; + Part.pointCount = VertexCount; + Part.type = HAPI_PARTTYPE_MESH; + + // If we are exporting to a mesh, we need vertices and faces + if (bExportGeometryAsMesh) + { + Part.vertexCount = IndexCount; + Part.faceCount = QuadCount; + } + + // Set the part infos + HAPI_GeoInfo DisplayGeoInfo; + FHoudiniApi::GeoInfo_Init(&DisplayGeoInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetDisplayGeoInfo( + FHoudiniEngine::Get().GetSession(), CreatedNodeId, &DisplayGeoInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), DisplayGeoInfo.nodeId, 0, &Part), false); + + //-------------------------------------------------------------------------------------------------- + // 3. Extract the landscape data + //-------------------------------------------------------------------------------------------------- + // Array for the position data + TArray LandscapePositionArray; + // Array for the normals + TArray LandscapeNormalArray; + // Array for the UVs + TArray LandscapeUVArray; + // Array for the vertex index of each point in its component + TArray LandscapeComponentVertexIndicesArray; + // Array for the tile names per point + TArray LandscapeComponentNameArray; + // Array for the lightmap values + TArray LandscapeLightmapValues; + // Selected components set to all components in current landscape proxy + TSet SelectedComponents; + SelectedComponents.Append(ToRawPtrTArrayUnsafe(LandscapeProxy->LandscapeComponents)); + + // Extract all the data from the landscape to the arrays + if (!ExtractLandscapeData( + LandscapeProxy, SelectedComponents, + bExportLighting, bExportTileUVs, bExportNormalizedUVs, + bApplyWorldTransform, + LandscapePositionArray, LandscapeNormalArray, + LandscapeUVArray, LandscapeComponentVertexIndicesArray, + LandscapeComponentNameArray, LandscapeLightmapValues)) + return false; + + //-------------------------------------------------------------------------------------------------- + // 3. Set the corresponding attributes in Houdini + //-------------------------------------------------------------------------------------------------- + + // Create point attribute info containing positions. + if (!AddLandscapePositionAttribute(DisplayGeoInfo.nodeId, LandscapePositionArray)) + return false; + + // Create point attribute info containing normals. + if (!AddLandscapeNormalAttribute(DisplayGeoInfo.nodeId, LandscapeNormalArray)) + return false; + + // Create point attribute info containing UVs. + if (!AddLandscapeUVAttribute(DisplayGeoInfo.nodeId, LandscapeUVArray)) + return false; + + // Create point attribute containing landscape component vertex indices (indices of vertices within the grid - x,y). + if (!AddLandscapeComponentVertexIndicesAttribute(DisplayGeoInfo.nodeId, LandscapeComponentVertexIndicesArray)) + return false; + + // Create point attribute containing landscape component name. + if (!AddLandscapeComponentNameAttribute(DisplayGeoInfo.nodeId, LandscapeComponentNameArray)) + return false; + + // Create point attribute info containing lightmap information. + if (bExportLighting) + { + if (!AddLandscapeLightmapColorAttribute(DisplayGeoInfo.nodeId, LandscapeLightmapValues)) + return false; + } + + // Set indices if we are exporting full geometry. + if (bExportGeometryAsMesh) + { + if (!AddLandscapeMeshIndicesAndMaterialsAttribute( + DisplayGeoInfo.nodeId, + bExportMaterials, + ComponentSizeQuads, + QuadCount, + LandscapeProxy, + SelectedComponents)) + return false; + } + + // If we are marshalling material information. + if (bExportMaterials) + { + if (!AddLandscapeGlobalMaterialAttribute(DisplayGeoInfo.nodeId, LandscapeProxy)) + return false; + } + + /* + // TODO: Move this to ExtractLandscapeData() + //-------------------------------------------------------------------------------------------------- + // 4. Extract and convert all the layers + //-------------------------------------------------------------------------------------------------- + ULandscapeInfo* LandscapeInfo = LandscapeProxy->GetLandscapeInfo(); + if (!LandscapeInfo) + return false; + + // Get the landscape X/Y Size + int32 MinX = MAX_int32; + int32 MinY = MAX_int32; + int32 MaxX = -MAX_int32; + int32 MaxY = -MAX_int32; + if (!LandscapeInfo->GetLandscapeExtent(MinX, MinY, MaxX, MaxY)) + return false; + + // Calc the X/Y size in points + int32 XSize = (MaxX - MinX + 1); + int32 YSize = (MaxY - MinY + 1); + if ((XSize < 2) || (YSize < 2)) + return false; + + bool MaskInitialized = false; + int32 NumLayers = LandscapeInfo->Layers.Num(); + for (int32 n = 0; n < NumLayers; n++) + { + // 1. Extract the uint8 values from the layer + TArray CurrentLayerIntData; + FLinearColor LayerUsageDebugColor; + FString LayerName; + if (!GetLandscapeTargetLayerData( + LandscapeInfo, n, + MinX, MinY, MaxX, MaxY, + CurrentLayerIntData, LayerUsageDebugColor, LayerName)) + continue; + + // 2. Convert unreal uint8 values to floats + // If the layer came from Houdini, additional info might have been stored in the DebugColor to convert the data back to float + HAPI_VolumeInfo CurrentLayerVolumeInfo; + FHoudiniApi::VolumeInfo_Init(&CurrentLayerVolumeInfo); + TArray CurrentLayerFloatData; + if (!ConvertLandscapeLayerDataToHeightfieldData( + CurrentLayerIntData, XSize, YSize, LayerUsageDebugColor, + CurrentLayerFloatData, CurrentLayerVolumeInfo)) + continue; + + if (!AddLandscapeLayerAttribute( + DisplayGeoInfo.nodeId, CurrentLayerFloatData, LayerName)) + continue; + } + */ + + // Commit the geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(DisplayGeoInfo.nodeId), false); + + return FHoudiniEngineUtils::HapiCookNode(InputNodeId, nullptr, true); +} + +bool +FUnrealLandscapeTranslator::CreateHeightfieldFromLandscape( + ALandscapeProxy* LandscapeProxy, + const FHoudiniLandscapeExportOptions& Options, + HAPI_NodeId& CreatedHeightfieldNodeId, + const FString& InputNodeNameStr, + HAPI_NodeId ParentNodeId, + const bool bSetObjectTransformToWorldTransform) +{ + if (!LandscapeProxy) + return false; + + // Export the whole landscape and its layer as a single heightfield. + FString NodeName = InputNodeNameStr; + + //-------------------------------------------------------------------------------------------------- + // Extracting the height data + //-------------------------------------------------------------------------------------------------- + TArray HeightData; + int32 XSize, YSize; + FVector Min, Max; + if (!GetLandscapeData(LandscapeProxy, HeightData, XSize, YSize, Min, Max)) + return false; + + //-------------------------------------------------------------------------------------------------- + // Convert the height uint16 data to float + //-------------------------------------------------------------------------------------------------- + TArray HeightfieldFloatValues; + HAPI_VolumeInfo HeightfieldVolumeInfo; + FHoudiniApi::VolumeInfo_Init(&HeightfieldVolumeInfo); + + // NOTE: The landscape actor's transform cannot be directly used to position the heightfield in Houdini since only a + // part of the full landscape may be loaded. We need to calculate the center of all the loaded tiles / landscape + // components. + FTransform LandscapeTransform = FHoudiniEngineRuntimeUtils::CalculateHoudiniLandscapeTransform(LandscapeProxy); + + FTransform LandscapeActorTransform = LandscapeProxy->GetLandscapeActor()->GetActorTransform(); + LandscapeTransform.SetScale3D(FVector::One()); + + FVector CenterOffset = FVector::ZeroVector; + if (!ConvertLandscapeDataToHeightFieldData( + HeightData, + XSize, YSize, + Min, Max, + LandscapeActorTransform.GetScale3D(), + HeightfieldFloatValues, + HeightfieldVolumeInfo)) + { + return false; + } + //-------------------------------------------------------------------------------------------------- + // Create the Heightfield Input Node + //-------------------------------------------------------------------------------------------------- + HAPI_NodeId HeightFieldId = -1; + HAPI_NodeId HeightId = -1; + HAPI_NodeId MaskId = -1; + HAPI_NodeId MergeId = -1; + if (!CreateHeightfieldInputNode(NodeName, XSize, YSize, HeightFieldId, HeightId, MaskId, MergeId, ParentNodeId)) + return false; + + + //-------------------------------------------------------------------------------------------------- + // Set the HeightfieldData in Houdini + //-------------------------------------------------------------------------------------------------- + // Set the Height volume's data + HAPI_PartId PartId = 0; + if (!SetHeightfieldData(HeightId, PartId, HeightfieldFloatValues, HeightfieldVolumeInfo, TEXT("height"))) + return false; + + // Apply attributes to the heightfield + ApplyAttributesToHeightfieldNode(HeightId, PartId, LandscapeProxy); + + // Commit the height volume + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(HeightId), false); + + //-------------------------------------------------------------------------------------------------- + // Add Data Layers and HLODS + //-------------------------------------------------------------------------------------------------- + + if (ParentNodeId != -1) + { + int PrevNode = FHoudiniHLODLayerUtils::AddHLODAttributes(LandscapeProxy, ParentNodeId, HeightFieldId); + FHoudiniDataLayerUtils::AddGroupsFromDataLayers(LandscapeProxy, ParentNodeId, PrevNode); + } + + //-------------------------------------------------------------------------------------------------- + // Define merge lambda, used below. + //-------------------------------------------------------------------------------------------------- + + int32 MergeInputIndex = 2; + + auto MergeInputFn = [&MergeInputIndex](const HAPI_NodeId MergeId, const HAPI_NodeId NodeId) -> HAPI_Result + { + // We had to create a new volume for this layer, so we need to connect it to the HF's merge node + HAPI_Result Result = FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + MergeId, MergeInputIndex, NodeId, 0); + + if (Result == HAPI_RESULT_SUCCESS) + { + MergeInputIndex++; + } + return Result; + }; + + //-------------------------------------------------------------------------------------------------- + // Send target layer data to Houdini. + //-------------------------------------------------------------------------------------------------- + + if (!SendTargetLayersToHoudini(LandscapeProxy, HeightFieldId, PartId, MergeId, MaskId, Options, HeightfieldVolumeInfo, XSize, YSize, MergeInputIndex)) + return false; + + //-------------------------------------------------------------------------------------------------- + // Create height field input for each editable landscape layer + //-------------------------------------------------------------------------------------------------- + + // We need a valid landscape actor to get the edit layers + ALandscape* Landscape = LandscapeProxy->GetLandscapeActor(); + if(IsValid(Landscape) && Options.bExportHeightDataPerEditLayer) + { + HAPI_VolumeInfo LayerVolumeInfo; + FHoudiniApi::VolumeInfo_Init(&HeightfieldVolumeInfo); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + for (const FLandscapeLayer& Layer : Landscape->GetLayersConst()) +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + for (const FLandscapeLayer& Layer : Landscape->GetLayers()) +#else + for(FLandscapeLayer& Layer : Landscape->LandscapeLayers) +#endif + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FString LayerName = Layer.EditLayer->GetName().ToString(); +#else + FString LayerName = Layer.Name.ToString(); +#endif + const FString LayerVolumeName = FString::Format(TEXT("landscapelayer_{0}"), { LayerName }); + + HAPI_NodeId LandscapeLayerNodeId = -1; + + HOUDINI_LANDSCAPE_MESSAGE(TEXT("[FUnrealLandscapeTranslator::CreateHeightfieldFromLandscape] Creating input node for editable landscape layer: %s"), *LayerVolumeName); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CreateHeightfieldInputVolumeNode( + FHoudiniEngine::Get().GetSession(), + HeightFieldId, + &LandscapeLayerNodeId, + H_TCHAR_TO_UTF8(*LayerVolumeName), + XSize, YSize, + 1.f + ), false); + + // Create a volume visualization node + const FString VisualizationName = FString::Format(TEXT("visualization_{0}"), { LayerName }); + HAPI_NodeId VisualizationNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CreateNode( + FHoudiniEngine::Get().GetSession(), + HeightFieldId, + "volumevisualization", + H_TCHAR_TO_UTF8(*VisualizationName), + false, + &VisualizationNodeId + ), false); + + // Set Visualization Mode to Height Field + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), + VisualizationNodeId, + "vismode", + 0, 2 + ), false); + + // Set Density Field to '*'. + HAPI_ParmId DensityFieldParmId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmIdFromName( + FHoudiniEngine::Get().GetSession(), + VisualizationNodeId, + "densityfield", + &DensityFieldParmId + ), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue( + FHoudiniEngine::Get().GetSession(), + VisualizationNodeId, + "*", + DensityFieldParmId, 0 + ), false); + + // Create a visibility node + const FString VisibilityName = FString::Format(TEXT("visibility_{0}"), {LayerName}); + HAPI_NodeId VisibilityNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CreateNode( + FHoudiniEngine::Get().GetSession(), + HeightFieldId, + "visibility", + H_TCHAR_TO_UTF8(*VisibilityName), + false, + &VisibilityNodeId + ), false); + + // Connect landscape layer to visualization + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + VisualizationNodeId, 0, LandscapeLayerNodeId, 0), false); + + // Connect visualization to visibility + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + VisibilityNodeId, 0, VisualizationNodeId, 0), false); + + // Connect the visibility node to the merge input + HOUDINI_CHECK_ERROR_RETURN(MergeInputFn(MergeId, VisibilityNodeId), false); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FGuid LayerGuid = Layer.EditLayer->GetGuid(); +#else + FGuid LayerGuid = Layer.Guid; +#endif + FScopedSetLandscapeEditingLayer Scope(Landscape, LayerGuid ); // Scope landscape access to the current layer + + //-------------------------------------------------------------------------------------------------- + // Extracting height data + //-------------------------------------------------------------------------------------------------- + + TArray LayerHeightData; + if (!GetLandscapeData(LandscapeProxy, LayerHeightData, XSize, YSize, Min, Max)) + return false; + + //-------------------------------------------------------------------------------------------------- + // Convert the height uint16 data to float + //-------------------------------------------------------------------------------------------------- + TArray LayerHeightFloatData; + if (!ConvertLandscapeDataToHeightFieldData( + LayerHeightData, XSize, YSize, Min, Max, LandscapeActorTransform.GetScale3D(), + LayerHeightFloatData, LayerVolumeInfo)) + return false; + + HAPI_PartId LayerPartId = 0; + SetHeightfieldData(LandscapeLayerNodeId, LayerPartId, LayerHeightFloatData, LayerVolumeInfo, LayerVolumeName); + + // Apply attributes to the heightfield input node + ApplyAttributesToHeightfieldNode(LandscapeLayerNodeId, 0, LandscapeProxy); + + // Commit the volume's geo + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(LandscapeLayerNodeId), false); + } + } + + HAPI_TransformEuler HAPIObjectTransform; + FHoudiniApi::TransformEuler_Init(&HAPIObjectTransform); + LandscapeTransform.SetScale3D(FVector::OneVector); + + if (bSetObjectTransformToWorldTransform) + FHoudiniEngineUtils::TranslateUnrealTransform(LandscapeTransform, HAPIObjectTransform); + else + FHoudiniEngineUtils::TranslateUnrealTransform(FTransform::Identity, HAPIObjectTransform); + // HAPIObjectTransform.position[1] = 0.0f; + + HAPI_NodeId ParentObjNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(HeightFieldId); + FHoudiniApi::SetObjectTransform(FHoudiniEngine::Get().GetSession(), ParentObjNodeId, &HAPIObjectTransform); + + // Finally, cook the Height field node + if(!FHoudiniEngineUtils::HapiCookNode(HeightFieldId, nullptr, true)) + return false; + + CreatedHeightfieldNodeId = HeightFieldId; + + return true; +} + +bool +FUnrealLandscapeTranslator::CreateHeightfieldFromLandscapeComponentArray( + ALandscapeProxy* LandscapeProxy, + const TSet& SelectedComponents, + const FHoudiniLandscapeExportOptions& Options, + HAPI_NodeId& CreatedHeightfieldNodeId, + const FString& InputNodeNameStr, + const HAPI_NodeId& ParentNodeId, + const bool bSetObjectTransformToWorldTransform) +{ + if ( SelectedComponents.Num() <= 0 ) + return false; + + ULandscapeInfo* LandscapeInfo = LandscapeProxy->GetLandscapeInfo(); + if (!IsValid(LandscapeInfo)) + return false; + + //-------------------------------------------------------------------------------------------------- + // Each selected component will be exported as tiled volumes in a single heightfield + //-------------------------------------------------------------------------------------------------- + //FTransform LandscapeTransform = FTransform::Identity; // The offset will be done in the component side + const FTransform LandscapeTM = LandscapeProxy->LandscapeActorToWorld(); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + const FTransform ProxyRelativeTM(FVector(LandscapeProxy->GetSectionBase())); +#else + const FTransform ProxyRelativeTM(FVector(LandscapeProxy->LandscapeSectionOffset)); +#endif + FTransform LandscapeTransform = ProxyRelativeTM * LandscapeTM; + + HAPI_NodeId HeightfieldNodeId = -1; + HAPI_NodeId HeightfieldeMergeId = -1; + + int32 MergeInputIndex = 0; + bool bAllComponentCreated = true; + + int32 ComponentIdx = 0; + + LandscapeInfo->ForAllLandscapeComponents([&](ULandscapeComponent* CurrentComponent) + { + if ( !CurrentComponent ) + return; + + if ( !SelectedComponents.Contains( CurrentComponent ) ) + return; + + if ( !CreateHeightfieldFromLandscapeComponent(LandscapeProxy, CurrentComponent, ComponentIdx, HeightfieldNodeId, HeightfieldeMergeId, MergeInputIndex, Options, InputNodeNameStr, LandscapeTransform, ParentNodeId) ) + bAllComponentCreated = false; + + ComponentIdx++; + }); + + // Check that we have a valid id for the input Heightfield. + if ( FHoudiniEngineUtils::IsHoudiniNodeValid( HeightfieldNodeId ) ) + CreatedHeightfieldNodeId = HeightfieldNodeId; + + // Set the HF's parent OBJ's tranform to the Landscape's transform + HAPI_TransformEuler HAPIObjectTransform; + FHoudiniApi::TransformEuler_Init(&HAPIObjectTransform); + //FMemory::Memzero< HAPI_TransformEuler >( HAPIObjectTransform ); + LandscapeTransform.SetScale3D( FVector::OneVector ); + + // In the new input system we'll apply the calculated transform later in the process on a reference node + if (bSetObjectTransformToWorldTransform) + FHoudiniEngineUtils::TranslateUnrealTransform( LandscapeTransform, HAPIObjectTransform ); + else + FHoudiniEngineUtils::TranslateUnrealTransform(FTransform::Identity, HAPIObjectTransform); + HAPIObjectTransform.position[ 1 ] = 0.0f; + + HAPI_NodeId ParentObjNodeId = FHoudiniEngineUtils::HapiGetParentNodeId( HeightfieldNodeId ); + FHoudiniApi::SetObjectTransform( FHoudiniEngine::Get().GetSession(), ParentObjNodeId, &HAPIObjectTransform ); + + return bAllComponentCreated; +} + +bool +FUnrealLandscapeTranslator::CreateHeightfieldFromLandscapeComponent( + ALandscapeProxy* LandscapeProxy, + ULandscapeComponent* LandscapeComponent, + const int32& ComponentIndex, + HAPI_NodeId& HeightFieldId, + HAPI_NodeId& MergeId, + int32& MergeInputIndex, + const FHoudiniLandscapeExportOptions& Options, + const FString& InputNodeNameStr, + const FTransform & ParentTransform, + const HAPI_NodeId& ParentNodeId) +{ + if ( !LandscapeComponent ) + return false; + + FString NodeName = InputNodeNameStr; + + //-------------------------------------------------------------------------------------------------- + // 1. Extract the height data + //-------------------------------------------------------------------------------------------------- + int32 MinX = MAX_int32; + int32 MinY = MAX_int32; + int32 MaxX = -MAX_int32; + int32 MaxY = -MAX_int32; + LandscapeComponent->GetComponentExtent( MinX, MinY, MaxX, MaxY ); + + ULandscapeInfo* LandscapeInfo = LandscapeComponent->GetLandscapeInfo(); + if ( !LandscapeInfo ) + return false; + + TArray HeightData; + int32 XSize, YSize; + if ( !GetLandscapeData( LandscapeInfo, MinX, MinY, MaxX, MaxY, HeightData, XSize, YSize ) ) + return false; + + FVector Origin = LandscapeComponent->Bounds.Origin; + FVector Extents = LandscapeComponent->Bounds.BoxExtent; + FVector Min = Origin - Extents; + FVector Max = Origin + Extents; + + //-------------------------------------------------------------------------------------------------- + // 2. Convert the landscape's combined height uint16 data to float + //-------------------------------------------------------------------------------------------------- + TArray HeightfieldFloatValues; + HAPI_VolumeInfo HeightfieldVolumeInfo; + FHoudiniApi::VolumeInfo_Init(&HeightfieldVolumeInfo); + FTransform LandscapeComponentTransform = LandscapeComponent->GetComponentTransform(); + + FVector CenterOffset = FVector::ZeroVector; + if ( !ConvertLandscapeDataToHeightFieldData( + HeightData, XSize, YSize, Min, Max, LandscapeComponentTransform.GetScale3D(), + HeightfieldFloatValues, HeightfieldVolumeInfo) ) + return false; + + // We need to modify the Volume's position to the Component's position relative to the Landscape's position + FVector RelativePosition = LandscapeComponent->GetRelativeTransform().GetLocation(); + HeightfieldVolumeInfo.transform.position[1] = RelativePosition.X; + HeightfieldVolumeInfo.transform.position[0] = RelativePosition.Y; + HeightfieldVolumeInfo.transform.position[2] = 0.0f; + + ALandscapeProxy * Proxy = LandscapeComponent->GetLandscapeProxy(); + if (Proxy) + { + FTransform LandscapeTM = Proxy->LandscapeActorToWorld(); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + FTransform ProxyRelativeTM(FVector(Proxy->GetSectionBase())); +#else + FTransform ProxyRelativeTM(FVector(Proxy->LandscapeSectionOffset)); +#endif + + // For landscapes that live in streaming proxies, need to account for both the parent transform and + // the current transform. + // For single actor landscapes, the parent proxy transform is equal to this proxy transform + // Either way, we want to multiply by the inverse of the parent transform to get the relative + FTransform LandscapeTransform = ParentTransform.Inverse() * ProxyRelativeTM * LandscapeTM; + FVector Location = LandscapeTransform.GetLocation(); + + HeightfieldVolumeInfo.transform.position[1] += Location.X / HAPI_UNREAL_SCALE_FACTOR_POSITION; + HeightfieldVolumeInfo.transform.position[0] += Location.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + + //-------------------------------------------------------------------------------------------------- + // 3. Create the Heightfield Input Node + //-------------------------------------------------------------------------------------------------- + HAPI_NodeId HeightId = -1; + HAPI_NodeId MaskId = -1; + bool CreatedHeightfieldNode = false; + if ( HeightFieldId < 0 || MergeId < 0 ) + { + if (!CreateHeightfieldInputNode(NodeName, XSize, YSize, HeightFieldId, HeightId, MaskId, MergeId, ParentNodeId)) + return false; + + MergeInputIndex = 2; + CreatedHeightfieldNode = true; + } + else + { + // Heightfield node was previously created, create additional height and a mask volumes for it + FHoudiniApi::CreateHeightfieldInputVolumeNode( + FHoudiniEngine::Get().GetSession(), + HeightFieldId, &HeightId, "height", XSize, YSize, 1.0f ); + + FHoudiniApi::CreateHeightfieldInputVolumeNode( + FHoudiniEngine::Get().GetSession(), + HeightFieldId, &MaskId, "mask", XSize, YSize, 1.0f ); + + // Connect the two newly created volumes to the HF's merge node + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + MergeId, MergeInputIndex++, HeightId, 0 ), false ); + + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + MergeId, MergeInputIndex++, MaskId, 0 ), false ); + } + + //-------------------------------------------------------------------------------------------------- + // 4. Set the Height field data in Houdini + //-------------------------------------------------------------------------------------------------- + // Set the Height volume's data + HAPI_PartId PartId = 0; + if (!SetHeightfieldData(HeightId, PartId, HeightfieldFloatValues, HeightfieldVolumeInfo, TEXT("height"))) + return false; + + // Apply tile attribute + AddLandscapeTileAttribute(HeightId, PartId, ComponentIndex); + + // Apply attributes to the heightfield + ApplyAttributesToHeightfieldNode(HeightId, PartId, LandscapeProxy); + + // Commit the height volume + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(HeightId), false); + + //-------------------------------------------------------------------------------------------------- + // 5. Extract and convert all the layers to HF masks + //-------------------------------------------------------------------------------------------------- + if (!SendTargetLayersToHoudini(LandscapeProxy, HeightFieldId, PartId, MergeId, MaskId, Options, HeightfieldVolumeInfo, XSize, YSize, MergeInputIndex)) + return false; + + // Finally, cook the Heightfield node + if (!FHoudiniEngineUtils::HapiCookNode(HeightFieldId, nullptr, true)) + return false; + + return true; +} + + +bool +FUnrealLandscapeTranslator::CreateInputNodeForLandscape( + ALandscapeProxy* LandscapeProxy, + const FString& InputNodeNameStr, + const FString& HeightFieldName, + const FTransform& LandscapeTransform, + HAPI_NodeId& HeightId, + HAPI_PartId& PartId, + HAPI_NodeId& HeightFieldId, + HAPI_NodeId& MaskId, + HAPI_NodeId& MergeId, + TArray& HeightData, + HAPI_VolumeInfo& HeightfieldVolumeInfo, + int32& XSize, int32& YSize, + const HAPI_NodeId& ParentNodeId) +{ + //-------------------------------------------------------------------------------------------------- + // 1. Extracting the height data + //-------------------------------------------------------------------------------------------------- + + FVector Min, Max; + + if (!GetLandscapeData(LandscapeProxy, HeightData, XSize, YSize, Min, Max)) + return false; + + //-------------------------------------------------------------------------------------------------- + // 2. Convert the height uint16 data to float + //-------------------------------------------------------------------------------------------------- + TArray HeightfieldFloatValues; + + if (!ConvertLandscapeDataToHeightFieldData( + HeightData, XSize, YSize, Min, Max, LandscapeTransform.GetScale3D(), + HeightfieldFloatValues, HeightfieldVolumeInfo)) + return false; + + //-------------------------------------------------------------------------------------------------- + // 3. Create the Heightfield Input Node + //-------------------------------------------------------------------------------------------------- + if (!CreateHeightfieldInputNode(InputNodeNameStr, XSize, YSize, HeightFieldId, HeightId, MaskId, MergeId, ParentNodeId)) + return false; + + //-------------------------------------------------------------------------------------------------- + // 4. Set the HeightfieldData in Houdini + //-------------------------------------------------------------------------------------------------- + // Set the Height volume's data + if (!SetHeightfieldData(HeightId, PartId, HeightfieldFloatValues, HeightfieldVolumeInfo, HeightFieldName)) + return false; + + return true; +} + + +bool +FUnrealLandscapeTranslator::CreateInputNodeForLandscapeObject( + ALandscapeProxy* InLandscape, + UHoudiniInput* InInput, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + const bool& bInputNodesCanBeDeleted) +{ + FString FinalInputNodeName = InputNodeName; + EHoudiniLandscapeExportType ExportType = InInput->GetLandscapeExportType(); + + FUnrealObjectInputIdentifier Identifier; + + const FHoudiniInputObjectSettings& InputSettings = InInput->GetInputSettings(); + + const bool bApplyWorldTransformToMeshOrPointCloudData = false; + const bool bSetObjectTransformToWorldTransform = false; + + bool bExportSelectionOnly = InputSettings.bLandscapeExportSelectionOnly; + bool bLandscapeAutoSelectComponent = InputSettings.bLandscapeAutoSelectComponent; + + // Get selected components if bLandscapeExportSelectionOnly or bLandscapeAutoSelectComponent is true + TSet> SelectedComponents; + if (bExportSelectionOnly) + { + InInput->UpdateLandscapeInputSelection(); + SelectedComponents = InInput->GetLandscapeSelectedComponents(); + } + + FUnrealObjectInputHandle ParentHandle; + HAPI_NodeId ParentNodeId = -1; + + { + const FUnrealObjectInputOptions Options = FUnrealObjectInputOptions::MakeOptionsForLandscapeData( + InputSettings, bExportSelectionOnly ? &SelectedComponents : nullptr); + Identifier = FUnrealObjectInputIdentifier(InLandscape, Options, true); + + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId)) + { + if (!bInputNodesCanBeDeleted) + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + + OutHandle = Handle; + InputNodeId = NodeId; + return true; + } + } + + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + // Create any parent/container nodes that we would need, and get the node id of the immediate parent + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // Set InputNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that InputNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (Handle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, InputNodeId)) + InputNodeId = -1; + } + else + { + InputNodeId = -1; + } + + HAPI_NodeId GeoObjNodeId = -1; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CreateNode( + FHoudiniEngine::Get().GetSession(), ParentNodeId, "geo", TCHAR_TO_ANSI(*FinalInputNodeName), true, &GeoObjNodeId), false); + + ParentNodeId = GeoObjNodeId; + + // Delete the previous nodes, if valid + if (InputNodeId >= 0 && FHoudiniEngineUtils::IsHoudiniNodeValid(InputNodeId)) + { + // Get the parent OBJ node ID before deleting! + HAPI_NodeId PreviousInputOBJNode = FHoudiniEngineUtils::HapiGetParentNodeId(InputNodeId); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), InputNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input node for %s."), *FinalInputNodeName); + } + + InputNodeId = -1; + + if (PreviousInputOBJNode >= 0) + { + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputOBJNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input OBJ node for %s."), *FinalInputNodeName); + } + } + } + + switch (ExportType) + { + case EHoudiniLandscapeExportType::Heightfield: + { + FinalInputNodeName = "heightfield"; + break; + } + + case EHoudiniLandscapeExportType::Mesh: + { + FinalInputNodeName = "mesh"; + break; + } + + case EHoudiniLandscapeExportType::Points: + { + FinalInputNodeName = "points"; + break; + } + } + } + + bool bSuccess = false; + if (ExportType == EHoudiniLandscapeExportType::Heightfield) + { + // Ensure we destroy any (Houdini) input nodes before clobbering this object with a new heightfield. + //DestroyInputNodes(InInput, InInput->GetInputType()); + + FHoudiniLandscapeExportOptions Options; + Options.bExportHeightDataPerEditLayer = InInput->IsEditLayerHeightExportEnabled(); + Options.bExportMergedPaintLayers = InInput->IsMergedPaintLayerExportEnabled(); + Options.bExportPaintLayersPerEditLayer = InInput->IsPaintLayerPerEditLayerExportEnabled(); + + int32 NumComponents = InLandscape->LandscapeComponents.Num(); + if(!bExportSelectionOnly || (SelectedComponents.Num() == NumComponents)) + { + // Export the whole landscape and its layer as a single heightfield node + bSuccess = FUnrealLandscapeTranslator::CreateHeightfieldFromLandscape( + InLandscape, + Options, + InputNodeId, + FinalInputNodeName, + ParentNodeId, + bSetObjectTransformToWorldTransform); + } + else + { + TSet SelectedLandscapeComponents = FHoudiniEngineUtils::RemoveObjectPtr(SelectedComponents); + + // Each selected landscape component will be exported as separate volumes in a single heightfield + bSuccess = FUnrealLandscapeTranslator::CreateHeightfieldFromLandscapeComponentArray( + InLandscape, + SelectedLandscapeComponents, + Options, + InputNodeId, + FinalInputNodeName, + ParentNodeId, + bSetObjectTransformToWorldTransform); + } + } + else + { + bool bExportLighting = InputSettings.bLandscapeExportLighting; + bool bExportMaterials = InputSettings.bLandscapeExportMaterials; + bool bExportNormalizedUVs = InputSettings.bLandscapeExportNormalizedUVs; + bool bExportTileUVs = InputSettings.bLandscapeExportTileUVs; + bool bExportAsMesh = InputSettings.LandscapeExportType == EHoudiniLandscapeExportType::Mesh; + + bSuccess = FUnrealLandscapeTranslator::CreateMeshOrPointsFromLandscape( + InLandscape, InputNodeId, FinalInputNodeName, + bExportAsMesh, bExportTileUVs, bExportNormalizedUVs, bExportLighting, bExportMaterials, + bApplyWorldTransformToMeshOrPointCloudData, ParentNodeId); + } + + if (!bSuccess) + return false; + + { + FUnrealObjectInputHandle Handle; + HAPI_NodeId InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(InputNodeId); + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, InputNodeId, Handle, InputObjectNodeId, nullptr, bInputNodesCanBeDeleted)) + OutHandle = Handle; + } + + return true; +} + + +// Converts Unreal uint16 values to Houdini Float +bool +FUnrealLandscapeTranslator::ConvertLandscapeLayerDataToHeightfieldData( + const TArray& IntHeightData, + int32 UnrealXSize, int32 UnrealYSize, + const FLinearColor& LayerUsageDebugColor, + TArray& LayerFloatValues) +{ + int HoudiniXSize = UnrealYSize; + int HoudiniYSize = UnrealXSize; + + LayerFloatValues.Empty(); + + int32 SizeInPoints = HoudiniXSize * HoudiniYSize; + if ((HoudiniXSize < 2) || (HoudiniYSize < 2)) + return false; + + if (IntHeightData.Num() != SizeInPoints) + return false; + + //-------------------------------------------------------------------------------------------------- + // 1. Convert values to float + //-------------------------------------------------------------------------------------------------- + + // By default, values are converted from unreal [0 255] uint8 to Houdini [0 1] float + // uint8 min/max + uint8 IntMin = 0; + uint8 IntMax = UINT8_MAX; + // The range in Digits + double DigitRange = (double)UINT8_MAX; + + // By default, the values will be converted to [0, 1] + float LayerMin = 0.0f; + float LayerMax = 1.0f; + float LayerSpacing = 1.0f / DigitRange; + + // If this layer came from Houdini, its alpha value should be PI + // This indicates that we can extract additional infos stored its debug usage color + // so we can reconstruct the original source values (float) more accurately + if (LayerUsageDebugColor.A == PI) + { + // We need the ZMin / ZMax uint8 values + IntMin = IntHeightData[0]; + IntMax = IntMin; + for (int n = 0; n < IntHeightData.Num(); n++) + { + if (IntHeightData[n] < IntMin) + IntMin = IntHeightData[n]; + if (IntHeightData[n] > IntMax) + IntMax = IntHeightData[n]; + } + + DigitRange = (double)IntMax - (double)IntMin; + + // Read the original min/max and spacing stored in the debug color + LayerMin = LayerUsageDebugColor.R; + LayerMax = LayerUsageDebugColor.G; + LayerSpacing = LayerUsageDebugColor.B; + } + + // Convert the Int data to Float + LayerFloatValues.SetNumUninitialized(SizeInPoints); + for (int32 nY = 0; nY < HoudiniYSize; nY++) + { + for (int32 nX = 0; nX < HoudiniXSize; nX++) + { + // We need to invert X/Y when reading the value from Unreal + int32 nHoudini = nX + nY * HoudiniXSize; + int32 nUnreal = nY + nX * HoudiniYSize; + + // Convert the int values to meter + // Unreal's digit value have a zero value of 32768 + double DoubleValue = ((double)IntHeightData[nUnreal] - (double)IntMin) * LayerSpacing + LayerMin; + LayerFloatValues[nHoudini] = (float)DoubleValue; + } + } + + /* + // Verifying the converted ZMin / ZMax + float FloatMin = LayerFloatValues[0]; + float FloatMax = FloatMin; + for (int32 n = 0; n < LayerFloatValues.Num(); n++) + { + if (LayerFloatValues[n] < FloatMin) + FloatMin = LayerFloatValues[n]; + if (LayerFloatValues[n] > FloatMax) + FloatMax = LayerFloatValues[n]; + } + */ + + return true; +} + +bool +FUnrealLandscapeTranslator::GetLandscapeData( + ALandscapeProxy* LandscapeProxy, + TArray& HeightData, + int32& XSize, int32& YSize, + FVector3d& Min, FVector3d& Max) +{ + if (!LandscapeProxy) + return false; + + ULandscapeInfo* LandscapeInfo = LandscapeProxy->GetLandscapeInfo(); + if (!LandscapeInfo) + return false; + + // Get the landscape extents to get its size + int32 MinX = MAX_int32; + int32 MinY = MAX_int32; + int32 MaxX = -MAX_int32; + int32 MaxY = -MAX_int32; + + ALandscape* Landscape = LandscapeProxy->GetLandscapeActor(); + if (LandscapeProxy == Landscape) + { + // The proxy is a landscape actor, so we have to use the landscape extent (landscape components + // may have been moved to proxies and may not be present on this actor). + LandscapeInfo->GetLandscapeExtent(MinX, MinY, MaxX, MaxY); + } + else + { + // We only want to get the data for this landscape proxy. + // To handle streaming proxies correctly, get the extents via all the components, + // not by calling GetLandscapeExtent or we'll end up sending ALL the streaming proxies. + for (const ULandscapeComponent* Comp : LandscapeProxy->LandscapeComponents) + { + Comp->GetComponentExtent(MinX, MinY, MaxX, MaxY); + } + } + + if (!GetLandscapeData(LandscapeInfo, MinX, MinY, MaxX, MaxY, HeightData, XSize, YSize)) + return false; + + // Get the landscape Min/Max values + // Do not use Landscape->GetActorBounds() here as instanced geo + // (due to grass layers for example) can cause it to return incorrect bounds! + FVector3d Origin, Extent; + GetLandscapeProxyBounds(LandscapeProxy, Origin, Extent); + + // Get the landscape Min/Max values + Min = Origin - Extent; + Max = Origin + Extent; + + return true; +} + +bool +FUnrealLandscapeTranslator::GetLandscapeData( + ULandscapeInfo* LandscapeInfo, + const int32& MinX, const int32& MinY, + const int32& MaxX, const int32& MaxY, + TArray& HeightData, + int32& XSize, int32& YSize) +{ + if (!LandscapeInfo) + return false; + + // Get the X/Y size in points + XSize = (MaxX - MinX + 1); + YSize = (MaxY - MinY + 1); + + if ((XSize < 2) || (YSize < 2)) + return false; + + // Extracting the uint16 values from the landscape + FLandscapeEditDataInterface LandscapeEdit(LandscapeInfo); + // Ensure we're not triggering a checkout, as we're just reading data + LandscapeEdit.SetShouldDirtyPackage(false); + + HeightData.AddZeroed(XSize * YSize); + LandscapeEdit.GetHeightDataFast(MinX, MinY, MaxX, MaxY, HeightData.GetData(), 0); + + return true; +} + + +void +FUnrealLandscapeTranslator::GetLandscapeProxyBounds( + ALandscapeProxy* LandscapeProxy, FVector3d& Origin, FVector3d& Extents) +{ + // Iterate only on the landscape components + FBox Bounds(ForceInit); + for (const UActorComponent* ActorComponent : LandscapeProxy->GetComponents()) + { + const ULandscapeComponent* LandscapeComp = Cast(ActorComponent); + if (LandscapeComp && LandscapeComp->IsRegistered()) + Bounds += LandscapeComp->Bounds.GetBox(); + } + + // Convert the bounds to origin/offset vectors + Bounds.GetCenterAndExtents(Origin, Extents); +} + +void +FUnrealLandscapeTranslator::ApplyAttributesToHeightfieldNode( + const HAPI_NodeId HeightId, + const HAPI_PartId PartId, + ALandscapeProxy* LandscapeProxy) +{ + UMaterialInterface* LandscapeMat = LandscapeProxy->GetLandscapeMaterial(); + UMaterialInterface* LandscapeHoleMat = LandscapeProxy->GetLandscapeHoleMaterial(); + UPhysicalMaterial* LandscapePhysMat = LandscapeProxy->DefaultPhysMaterial; + + AddLandscapeMaterialAttributesToVolume(HeightId, PartId, LandscapeMat, LandscapeHoleMat, LandscapePhysMat); + + // Add the landscape's actor tags as prim attributes if we have any + FHoudiniEngineUtils::CreateAttributesFromTags(HeightId, PartId, LandscapeProxy->Tags); + + // Add the unreal_actor_path attribute + FHoudiniEngineUtils::AddActorPathAttribute(HeightId, PartId, LandscapeProxy, 1); + + // Add the unreal_level_path attribute + ULevel* Level = LandscapeProxy->GetLevel(); + if (Level) + { + FHoudiniEngineUtils::AddLevelPathAttribute(HeightId, PartId, Level, 1); + /* + LevelPath = Level->GetPathName(); + + // We just want the path up to the first point + int32 DotIndex; + if (LevelPath.FindChar('.', DotIndex)) + LevelPath.LeftInline(DotIndex, false); + + AddLevelPathAttributeToVolume(HeightId, PartId, LevelPath); + */ + } + + // Add streaming poxy attribute + FHoudiniEngineUtils::AddLandscapeTypeAttribute(HeightId, PartId, LandscapeProxy, 1); +} + + +bool +FUnrealLandscapeTranslator::ConvertLandscapeDataToHeightFieldData( + const TArray& IntHeightData, + int32 XSize, + int32 YSize, + FVector Min, + FVector Max, + const FVector3d & LandscapeActorScale, + TArray& HeightfieldFloatValues, + HAPI_VolumeInfo& HeightfieldVolumeInfo) +{ + HeightfieldFloatValues.Empty(); + + int32 HoudiniXSize = YSize; + int32 HoudiniYSize = XSize; + int32 SizeInPoints = HoudiniXSize * HoudiniYSize; + if ((HoudiniXSize < 2) || (HoudiniYSize < 2)) + return false; + + if (IntHeightData.Num() != SizeInPoints) + return false; + + // Use default unreal scaling for marshalling landscapes + // A lot of precision will be lost in order to keep the same transform as the landscape input + bool bUseDefaultUE4Scaling = false; + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings && HoudiniRuntimeSettings->MarshallingLandscapesUseDefaultUnrealScaling) + bUseDefaultUE4Scaling = HoudiniRuntimeSettings->MarshallingLandscapesUseDefaultUnrealScaling; + + //-------------------------------------------------------------------------------------------------- + // Convert values to float + //-------------------------------------------------------------------------------------------------- + + // Convert the min/max values from cm to meters + Min /= 100.0; + Max /= 100.0; + + // Unreal's landscape uses 16bits precision and range from -256m to 256m with the default scale of 100.0 + // To convert the uint16 values to float "metric" values, offset the int by 32768 to center it, + // then scale it + + // Spacing used to convert from uint16 to meters + double ZSpacing = 512.0 / ((double)UINT16_MAX); + ZSpacing *= LandscapeActorScale.Z / 100.0; + + // Center value in meters (Landscape ranges from [-255:257] meters at default scale + double ZCenterOffset = 32767; + + // Convert the Int data to Float + HeightfieldFloatValues.SetNumUninitialized(SizeInPoints); + for (int32 nY = 0; nY < HoudiniYSize; nY++) + { + for (int32 nX = 0; nX < HoudiniXSize; nX++) + { + // We need to invert X/Y when reading the value from Unreal + int32 nHoudini = nX + nY * HoudiniXSize; + int32 nUnreal = nY + nX * XSize; + + // Convert the int values to meter + // Unreal's digit value have a zero value of 32768 + // Don't apply z-position offsets to the data. This offset will be applied to the + // heighfield primitive itself in Houdini. + double DoubleValue = ((double)IntHeightData[nUnreal] - ZCenterOffset) * ZSpacing; + HeightfieldFloatValues[nHoudini] = (float)DoubleValue; + } + } + + //-------------------------------------------------------------------------------------------------- + // Set the Hapi Transform. Houdini expects the scale to be set here, but we set the position + // and rotation on the Geometry nodes, so clear here. + //-------------------------------------------------------------------------------------------------- + + HAPI_Transform HapiTransform; + FHoudiniApi::Transform_Init(&HapiTransform); + + HapiTransform.rotationQuaternion[0] = 0; + HapiTransform.rotationQuaternion[1] = 0; + HapiTransform.rotationQuaternion[2] = 0; + HapiTransform.rotationQuaternion[3] = 1; + HapiTransform.position[1] = 0.0f; + HapiTransform.position[0] = 0.0f; + HapiTransform.position[2] = 0.0f; + + HapiTransform.scale[0] = LandscapeActorScale.Y * 0.5f * HoudiniXSize / 100.0f; + HapiTransform.scale[1] = LandscapeActorScale.X * 0.5f * HoudiniYSize / 100.0f; + HapiTransform.scale[2] = 0.5f; + if (bUseDefaultUE4Scaling) + HapiTransform.scale[2] *= LandscapeActorScale.Z; + + HapiTransform.shear[0] = 0.0f; + HapiTransform.shear[1] = 0.0f; + HapiTransform.shear[2] = 0.0f; + + //-------------------------------------------------------------------------------------------------- + // Fill the volume info + //-------------------------------------------------------------------------------------------------- + HeightfieldVolumeInfo.xLength = HoudiniXSize; + HeightfieldVolumeInfo.yLength = HoudiniYSize; + HeightfieldVolumeInfo.zLength = 1; + + HeightfieldVolumeInfo.minX = 0; + HeightfieldVolumeInfo.minY = 0; + HeightfieldVolumeInfo.minZ = 0; + + HeightfieldVolumeInfo.transform = HapiTransform; + + HeightfieldVolumeInfo.type = HAPI_VOLUMETYPE_HOUDINI; + HeightfieldVolumeInfo.storage = HAPI_STORAGETYPE_FLOAT; + HeightfieldVolumeInfo.tupleSize = 1; + HeightfieldVolumeInfo.tileSize = 1; + + HeightfieldVolumeInfo.hasTaper = false; + HeightfieldVolumeInfo.xTaper = 0.0; + HeightfieldVolumeInfo.yTaper = 0.0; + + return true; +} + +bool +FUnrealLandscapeTranslator::CreateHeightfieldInputNode( + const FString& NodeName, + const int32& XSize, + const int32& YSize, + HAPI_NodeId& HeightfieldNodeId, + HAPI_NodeId& HeightNodeId, + HAPI_NodeId& MaskNodeId, + HAPI_NodeId& MergeNodeId, + HAPI_NodeId ParentNodeId) +{ + // Make sure the Heightfield node doesnt already exists + if (HeightfieldNodeId != -1) + return false; + + // Convert the node's name + std::string NameStr; + FHoudiniEngineUtils::ConvertUnrealString(NodeName, NameStr); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CreateHeightFieldInput( + FHoudiniEngine::Get().GetSession(), + ParentNodeId, NameStr.c_str(), YSize, XSize, 1.0f, HAPI_HeightFieldSampling::HAPI_HEIGHTFIELD_SAMPLING_CORNER, + &HeightfieldNodeId, &HeightNodeId, &MaskNodeId, &MergeNodeId), false); + + // Cook it + return FHoudiniEngineUtils::HapiCookNode(HeightfieldNodeId, nullptr, true); +} + +bool +FUnrealLandscapeTranslator::SetHeightfieldData( + const HAPI_NodeId& VolumeNodeId, + const HAPI_PartId& PartId, + TArray& FloatValues, + const HAPI_VolumeInfo& VolumeInfo, + const FString& HeightfieldName) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FUnrealLandscapeTranslator::SetHeightfieldData); + + // Cook the node to get proper infos on it + if(!FHoudiniEngineUtils::HapiCookNode(VolumeNodeId, nullptr, true)) + return false; + + // Read the geo/part/volume info from the volume node + HAPI_GeoInfo GeoInfo; + FHoudiniApi::GeoInfo_Init(&GeoInfo); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetGeoInfo( + FHoudiniEngine::Get().GetSession(), + VolumeNodeId, &GeoInfo), false); + + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetPartInfo( + FHoudiniEngine::Get().GetSession(), + GeoInfo.nodeId, PartId, &PartInfo), false); + + // Update the volume infos + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetVolumeInfo( + FHoudiniEngine::Get().GetSession(), + VolumeNodeId, PartInfo.id, &VolumeInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetHeightFieldData( + GeoInfo.nodeId, PartInfo.id, FloatValues, HeightfieldName), false); + + return true; +} + +bool FUnrealLandscapeTranslator::AddLandscapeMaterialAttributesToVolume( + const HAPI_NodeId& VolumeNodeId, + const HAPI_PartId& PartId, + UMaterialInterface* InLandscapeMaterial, + UMaterialInterface* InLandscapeHoleMaterial, + UPhysicalMaterial* InPhysicalMaterial) +{ + if (VolumeNodeId == -1) + return false; + + // LANDSCAPE MATERIAL + if (IsValid(InLandscapeMaterial)) + { + // Extract the path name from the material interface + FString InLandscapeMaterialString = InLandscapeMaterial->GetPathName(); + + // Marshall in material names. + HAPI_AttributeInfo AttributeInfoMaterial; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoMaterial); + AttributeInfoMaterial.count = 1; + AttributeInfoMaterial.tupleSize = 1; + AttributeInfoMaterial.exists = true; + AttributeInfoMaterial.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoMaterial.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoMaterial.originalOwner = HAPI_ATTROWNER_INVALID; + + HAPI_Result Result = FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), VolumeNodeId, PartId, + HAPI_UNREAL_ATTRIB_MATERIAL, &AttributeInfoMaterial); + + if (HAPI_RESULT_SUCCESS == Result) + { + // Set the attribute's string data + FHoudiniHapiAccessor Accessor(VolumeNodeId, PartId, HAPI_UNREAL_ATTRIB_MATERIAL); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoMaterial, InLandscapeMaterialString), false); + } + + if (Result != HAPI_RESULT_SUCCESS) + { + // Failed to create the attribute + HOUDINI_LOG_WARNING( + TEXT("Failed to upload unreal_material attribute for landscape: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + } + } + + // HOLE MATERIAL + if (IsValid(InLandscapeHoleMaterial)) + { + // Extract the path name from the material interface + FString InLandscapeMaterialString = InLandscapeHoleMaterial->GetPathName(); + + // Marshall in material names. + HAPI_AttributeInfo AttributeInfoMaterial; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoMaterial); + AttributeInfoMaterial.count = 1; + AttributeInfoMaterial.tupleSize = 1; + AttributeInfoMaterial.exists = true; + AttributeInfoMaterial.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoMaterial.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoMaterial.originalOwner = HAPI_ATTROWNER_INVALID; + + HAPI_Result Result = FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), VolumeNodeId, PartId, + HAPI_UNREAL_ATTRIB_MATERIAL_HOLE, &AttributeInfoMaterial); + + if (Result == HAPI_RESULT_SUCCESS) + { + // Set the attribute's string data + FHoudiniHapiAccessor Accessor(VolumeNodeId, PartId, HAPI_UNREAL_ATTRIB_MATERIAL_HOLE); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoMaterial, InLandscapeMaterialString), false); + } + + if (Result != HAPI_RESULT_SUCCESS) + { + // Failed to create the attribute + HOUDINI_LOG_WARNING( + TEXT("Failed to upload unreal_hole_material attribute for landscape: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + } + } + + // PHYSICAL MATERIAL + if (IsValid(InPhysicalMaterial)) + { + // Extract the path name from the material interface + FString InPhysMatlString = InPhysicalMaterial->GetPathName(); + + // Marshall in material names. + HAPI_AttributeInfo AttributeInfoMaterial; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoMaterial); + AttributeInfoMaterial.count = 1; + AttributeInfoMaterial.tupleSize = 1; + AttributeInfoMaterial.exists = true; + AttributeInfoMaterial.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoMaterial.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoMaterial.originalOwner = HAPI_ATTROWNER_INVALID; + + HAPI_Result Result = FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), VolumeNodeId, PartId, + HAPI_UNREAL_ATTRIB_PHYSICAL_MATERIAL, &AttributeInfoMaterial); + + if (Result == HAPI_RESULT_SUCCESS) + { + // Set the attribute's string data + FHoudiniHapiAccessor Accessor(VolumeNodeId, PartId, HAPI_UNREAL_ATTRIB_PHYSICAL_MATERIAL); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoMaterial, InPhysMatlString), false); + } + + if (Result != HAPI_RESULT_SUCCESS) + { + // Failed to create the attribute + HOUDINI_LOG_WARNING( + TEXT("Failed to upload unreal_physical_material attribute for landscape: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + } + } + + return true; +} + + +bool +FUnrealLandscapeTranslator::GetLandscapeTargetLayerData( + ALandscapeProxy* LandscapeProxy, + ULandscapeInfo* LandscapeInfo, + int32 TargetLayerIndex, + TArray& TargetLayerData, + FLinearColor& TargetLayerDebugColor, + FString& TargetLayerName) +{ + if (!IsValid(LandscapeInfo) || !IsValid(LandscapeProxy)) + return false; + + // Get the landscape X/Y Size + int32 MinX = MAX_int32; + int32 MinY = MAX_int32; + int32 MaxX = -MAX_int32; + int32 MaxY = -MAX_int32; + + ALandscape* Landscape = LandscapeProxy->GetLandscapeActor(); + if (LandscapeProxy == Landscape) + { + // The proxy is a landscape actor, so we have to use the landscape extent (landscape components + // may have been moved to proxies and may not be present on this actor). + LandscapeInfo->GetLandscapeExtent(MinX, MinY, MaxX, MaxY); + } + else + { + // We only want to get the data for this landscape proxy. + // To handle streaming proxies correctly, get the extents via all the components, + // not by calling GetLandscapeExtent or we'll end up sending ALL the streaming proxies. + for (const ULandscapeComponent* Comp : LandscapeProxy->LandscapeComponents) + { + Comp->GetComponentExtent(MinX, MinY, MaxX, MaxY); + } + } + + if(MinX == MAX_int32 || MinY == MAX_int32 || MaxX == -MAX_int32 || MaxY == -MAX_int32) + return false; + + if (!GetLandscapeTargetLayerData( + LandscapeInfo, TargetLayerIndex, + MinX, MinY, MaxX, MaxY, + TargetLayerData, TargetLayerDebugColor, TargetLayerName)) + return false; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + const FName& VisLayerName = ALandscape::VisibilityLayer->GetLayerName(); +#else + const FName& VisLayerName = ALandscape::VisibilityLayer->LayerName; +#endif + + if (FName(TargetLayerName).Compare(VisLayerName) ==0) + { + // If we encounter the visibility layer, make sure we name + // it according to the plugin's expectations instead of using the internal `DataLayer__` name. + TargetLayerName = HAPI_UNREAL_VISIBILITY_LAYER_NAME; + } + + return true; +} + +bool +FUnrealLandscapeTranslator::GetLandscapeTargetLayerData( + ULandscapeInfo* LandscapeInfo, + int32 TargetLayerIndex, + int32 MinX, int32 MinY, + int32 MaxX, int32 MaxY, + TArray& TargetLayerData, + FLinearColor& TargetLayerUsageDebugColor, + FString& TargetLayerName) +{ + if (!LandscapeInfo) + return false; + + if (!LandscapeInfo->Layers.IsValidIndex(TargetLayerIndex)) + return false; + + FLandscapeInfoLayerSettings LayersSetting = LandscapeInfo->Layers[TargetLayerIndex]; + ULandscapeLayerInfoObject* LayerInfo = LayersSetting.LayerInfoObj; + if (!LayerInfo) + return false; + + // Calc the X/Y size in points + int32 XSize = (MaxX - MinX + 1); + int32 YSize = (MaxY - MinY + 1); + if ((XSize < 2) || (YSize < 2)) + return false; + + // extracting the uint8 values from the layer + FLandscapeEditDataInterface LandscapeEdit(LandscapeInfo); + // Ensure we're not triggering a checkout, as we're just reading data + LandscapeEdit.SetShouldDirtyPackage(false); + + TargetLayerData.AddZeroed(XSize * YSize); + LandscapeEdit.GetWeightDataFast(LayerInfo, MinX, MinY, MaxX, MaxY, TargetLayerData.GetData(), 0); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + TargetLayerUsageDebugColor = LayerInfo->GetLayerUsageDebugColor(); +#else + TargetLayerUsageDebugColor = LayerInfo->LayerUsageDebugColor; +#endif + + TargetLayerName = LayersSetting.GetLayerName().ToString(); + + return true; +} + +bool +FUnrealLandscapeTranslator::InitDefaultHeightfieldMask( + const HAPI_VolumeInfo& HeightVolumeInfo, + const HAPI_NodeId& MaskVolumeNodeId) +{ + // We need to have a mask layer as it is required for proper heightfield functionalities + + // Creating an array filled with 0.0 + TArray MaskFloatData; + MaskFloatData.SetNumUninitialized(HeightVolumeInfo.xLength * HeightVolumeInfo.yLength); + for (int32 n = 0; n < MaskFloatData.Num(); n++) + MaskFloatData[n] = 0.0f; + + // Creating the volume infos + HAPI_VolumeInfo MaskVolumeInfo = HeightVolumeInfo; + + // Set the heighfield data in Houdini + FString MaskName = TEXT("mask"); + HAPI_PartId PartId = 0; + if (!SetHeightfieldData(MaskVolumeNodeId, PartId, MaskFloatData, MaskVolumeInfo, MaskName)) + return false; + + return true; +} + +bool +FUnrealLandscapeTranslator::DestroyLandscapeAssetNode(HAPI_NodeId& ConnectedAssetId, TArray& CreatedInputAssetIds) +{ + HAPI_AssetInfo NodeAssetInfo; + FHoudiniApi::AssetInfo_Init(&NodeAssetInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetAssetInfo( + FHoudiniEngine::Get().GetSession(), ConnectedAssetId, &NodeAssetInfo), false); + + FHoudiniEngineString AssetOpName(NodeAssetInfo.fullOpNameSH); + FString OpName; + if (!AssetOpName.ToFString(OpName)) + return false; + + if (!OpName.Contains(TEXT("xform"))) + { + // Not a transform node, so not a Heightfield + // We just need to destroy the landscape asset node + return FHoudiniEngineUtils::DestroyHoudiniAsset(ConnectedAssetId); + } + + // The landscape was marshalled as a heightfield, so we need to destroy and disconnect + // the volvis nodes, all the merge node's input (each merge input is a volume for one + // of the layer/mask of the landscape ) + + // Query the volvis node id + // The volvis node is the fist input of the xform node + HAPI_NodeId VolvisNodeId = -1; + FHoudiniApi::QueryNodeInput( + FHoudiniEngine::Get().GetSession(), + ConnectedAssetId, 0, &VolvisNodeId); + + // First, destroy the merge node and its inputs + // The merge node is in the first input of the volvis node + HAPI_NodeId MergeNodeId = -1; + FHoudiniApi::QueryNodeInput( + FHoudiniEngine::Get().GetSession(), + VolvisNodeId, 0, &MergeNodeId); + + if (MergeNodeId != -1) + { + // Get the merge node info + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), MergeNodeId, &NodeInfo), false); + + for (int32 n = 0; n < NodeInfo.inputCount; n++) + { + // Get the Input node ID from the host ID + HAPI_NodeId InputNodeId = -1; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::QueryNodeInput( + FHoudiniEngine::Get().GetSession(), + MergeNodeId, n, &InputNodeId)) + break; + + if (InputNodeId == -1) + break; + + // Disconnect and Destroy that input + FHoudiniEngineUtils::HapiDisconnectAsset(MergeNodeId, n); + FHoudiniEngineUtils::DestroyHoudiniAsset(InputNodeId); + } + } + + // Second step, destroy all the volumes GEO assets + for (HAPI_NodeId AssetNodeId : CreatedInputAssetIds) + { + FHoudiniEngineUtils::DestroyHoudiniAsset(AssetNodeId); + } + CreatedInputAssetIds.Empty(); + + // Finally disconnect and destroy the xform, volvis and merge nodes, then destroy them + FHoudiniEngineUtils::HapiDisconnectAsset(ConnectedAssetId, 0); + FHoudiniEngineUtils::HapiDisconnectAsset(VolvisNodeId, 0); + FHoudiniEngineUtils::DestroyHoudiniAsset(MergeNodeId); + FHoudiniEngineUtils::DestroyHoudiniAsset(VolvisNodeId); + + return FHoudiniEngineUtils::DestroyHoudiniAsset(ConnectedAssetId); +} + + +bool +FUnrealLandscapeTranslator::ExtractLandscapeData( + ALandscapeProxy * LandscapeProxy, TSet& SelectedComponents, + const bool& bExportLighting, const bool& bExportTileUVs, const bool& bExportNormalizedUVs, + const bool bApplyWorldTransform, + TArray& LandscapePositionArray, + TArray& LandscapeNormalArray, + TArray& LandscapeUVArray, + TArray& LandscapeComponentVertexIndicesArray, + TArray& LandscapeComponentNameArray, + TArray& LandscapeLightmapValues) +{ + if (!LandscapeProxy) + return false; + + if (SelectedComponents.Num() < 1) + return false; + + // Get runtime settings. + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + + // Since we use GetWorldVertex when fetching the landscape vertices, we need to calculate the landscape world + // transform here so that we can apply its inverse to the world-space vertices if we don't want to output the + // landscape data in world space + FTransform LandscapeTransform = FTransform::Identity; + if (!bApplyWorldTransform) + { + LandscapeTransform = FHoudiniEngineRuntimeUtils::CalculateHoudiniLandscapeTransform(LandscapeProxy); + LandscapeTransform.SetScale3D(FVector::OneVector); + } + + // Calc all the needed sizes + int32 ComponentSizeQuads = ((LandscapeProxy->ComponentSizeQuads + 1) >> LandscapeProxy->ExportLOD) - 1; + float ScaleFactor = (float)LandscapeProxy->ComponentSizeQuads / (float)ComponentSizeQuads; + + int32 NumComponents = SelectedComponents.Num(); + bool bExportOnlySelected = NumComponents != LandscapeProxy->LandscapeComponents.Num(); + + int32 VertexCountPerComponent = FMath::Square(ComponentSizeQuads + 1); + int32 VertexCount = NumComponents * VertexCountPerComponent; + if (!VertexCount) + return false; + + // Initialize the data arrays + LandscapePositionArray.SetNumUninitialized(VertexCount); + LandscapeNormalArray.SetNumUninitialized(VertexCount); + LandscapeUVArray.SetNumUninitialized(VertexCount); + LandscapeComponentNameArray.SetNumUninitialized(VertexCount); + LandscapeComponentVertexIndicesArray.SetNumUninitialized(VertexCount); + if (bExportLighting) + LandscapeLightmapValues.SetNumUninitialized(VertexCount); + + //----------------------------------------------------------------------------------------------------------------- + // EXTRACT THE LANDSCAPE DATA + //----------------------------------------------------------------------------------------------------------------- + FIntPoint IntPointMax = FIntPoint::ZeroValue; + + int32 AllPositionsIdx = 0; + for (int32 ComponentIdx = 0; ComponentIdx < LandscapeProxy->LandscapeComponents.Num(); ComponentIdx++) + { + ULandscapeComponent * LandscapeComponent = LandscapeProxy->LandscapeComponents[ComponentIdx]; + if (bExportOnlySelected && !SelectedComponents.Contains(LandscapeComponent)) + continue; + + TArray64< uint8 > LightmapMipData; + int32 LightmapMipSizeX = 0; + int32 LightmapMipSizeY = 0; + + // See if we need to export lighting information. + if (bExportLighting) + { + const FMeshMapBuildData* MapBuildData = LandscapeComponent->GetMeshMapBuildData(); + FLightMap2D* LightMap2D = MapBuildData && MapBuildData->LightMap ? MapBuildData->LightMap->GetLightMap2D() : nullptr; + if (LightMap2D && LightMap2D->IsValid(0)) + { + UTexture2D * TextureLightmap = LightMap2D->GetTexture(0); + if (TextureLightmap) + { + if (TextureLightmap->Source.GetMipData(LightmapMipData, 0, 0, 0, nullptr)) + { + LightmapMipSizeX = TextureLightmap->Source.GetSizeX(); + LightmapMipSizeY = TextureLightmap->Source.GetSizeY(); + } + else + { + LightmapMipData.Empty(); + } + } + } + } + + // Construct landscape component data interface to access raw data. + FLandscapeComponentDataInterface CDI(LandscapeComponent, LandscapeProxy->ExportLOD); + + // Get name of this landscape component. + const char * LandscapeComponentNameStr = FHoudiniEngineUtils::ExtractRawString(LandscapeComponent->GetName()); + for (int32 VertexIdx = 0; VertexIdx < VertexCountPerComponent; VertexIdx++) + { + int32 VertX = 0; + int32 VertY = 0; + CDI.VertexIndexToXY(VertexIdx, VertX, VertY); + + // Get position. + FVector PositionVector = CDI.GetWorldVertex(VertX, VertY); + if (!bApplyWorldTransform) + PositionVector = LandscapeTransform.InverseTransformPosition(PositionVector); + + // Get normal / tangent / binormal. + FVector Normal = FVector::ZeroVector; + FVector TangentX = FVector::ZeroVector; + FVector TangentY = FVector::ZeroVector; + CDI.GetLocalTangentVectors(VertX, VertY, TangentX, TangentY, Normal); + + // Export UVs. + FVector TextureUV = FVector::ZeroVector; + if (bExportTileUVs) + { + // We want to export uvs per tile. + TextureUV = FVector(VertX, VertY, 0.0f); + + // If we need to normalize UV space. + if (bExportNormalizedUVs) + TextureUV /= ComponentSizeQuads; + } + else + { + // We want to export global uvs (default). + FIntPoint IntPoint = LandscapeComponent->GetSectionBase(); + TextureUV = FVector(VertX * ScaleFactor + IntPoint.X, VertY * ScaleFactor + IntPoint.Y, 0.0f); + + // Keep track of max offset. + IntPointMax = IntPointMax.ComponentMax(IntPoint); + } + + if (bExportLighting) + { + FLinearColor VertexLightmapColor(0.0f, 0.0f, 0.0f, 1.0f); + if (LightmapMipData.Num() > 0) + { + FVector2D UVCoord(VertX, VertY); + UVCoord /= (ComponentSizeQuads + 1); + + FColor LightmapColorRaw = PickVertexColorFromTextureMip( + LightmapMipData.GetData(), UVCoord, LightmapMipSizeX, LightmapMipSizeY); + + VertexLightmapColor = LightmapColorRaw.ReinterpretAsLinear(); + } + + LandscapeLightmapValues[AllPositionsIdx] = VertexLightmapColor; + } + + // Retrieve component transform. + const FTransform & ComponentTransform = LandscapeComponent->GetComponentTransform(); + + // Retrieve component scale. + const FVector & ScaleVector = ComponentTransform.GetScale3D(); + + // Perform normalization. + Normal /= ScaleVector; + Normal.Normalize(); + + TangentX /= ScaleVector; + TangentX.Normalize(); + + TangentY /= ScaleVector; + TangentY.Normalize(); + + // Perform position scaling. + FVector3f PositionTransformed = (FVector3f)PositionVector / HAPI_UNREAL_SCALE_FACTOR_POSITION; + LandscapePositionArray[AllPositionsIdx].X = PositionTransformed.X; + LandscapePositionArray[AllPositionsIdx].Y = PositionTransformed.Z; + LandscapePositionArray[AllPositionsIdx].Z = PositionTransformed.Y; + + Swap(Normal.Y, Normal.Z); + + // Store landscape component name for this point. + LandscapeComponentNameArray[AllPositionsIdx] = LandscapeComponentNameStr; + + // Store vertex index (x,y) for this point. + LandscapeComponentVertexIndicesArray[AllPositionsIdx].X = VertX; + LandscapeComponentVertexIndicesArray[AllPositionsIdx].Y = VertY; + + // Store point normal. + LandscapeNormalArray[AllPositionsIdx] = (FVector3f)Normal; + + // Store uv. + LandscapeUVArray[AllPositionsIdx] = (FVector3f)TextureUV; + + AllPositionsIdx++; + } + + // Free the memory allocated for LandscapeComponentNameStr + FHoudiniEngineUtils::FreeRawStringMemory(LandscapeComponentNameStr); + } + + // If we need to normalize UV space and we are doing global UVs. + if (!bExportTileUVs && bExportNormalizedUVs) + { + IntPointMax += FIntPoint(ComponentSizeQuads, ComponentSizeQuads); + IntPointMax = IntPointMax.ComponentMax(FIntPoint(1, 1)); + + for (int32 UVIdx = 0; UVIdx < VertexCount; ++UVIdx) + { + FVector3f& PositionUV = LandscapeUVArray[UVIdx]; + PositionUV.X /= IntPointMax.X; + PositionUV.Y /= IntPointMax.Y; + } + } + + return true; +} + +FColor +FUnrealLandscapeTranslator::PickVertexColorFromTextureMip( + const uint8 * MipBytes, FVector2D & UVCoord, int32 MipWidth, int32 MipHeight) +{ + check(MipBytes); + + FColor ResultColor(0, 0, 0, 255); + + if (UVCoord.X >= 0.0f && UVCoord.X < 1.0f && UVCoord.Y >= 0.0f && UVCoord.Y < 1.0f) + { + const int32 X = MipWidth * UVCoord.X; + const int32 Y = MipHeight * UVCoord.Y; + + const int32 Index = ((Y * MipWidth) + X) * 4; + + ResultColor.B = MipBytes[Index + 0]; + ResultColor.G = MipBytes[Index + 1]; + ResultColor.R = MipBytes[Index + 2]; + ResultColor.A = MipBytes[Index + 3]; + } + + return ResultColor; +} + +bool +FUnrealLandscapeTranslator::AddLandscapePositionAttribute(const HAPI_NodeId& NodeId, const TArray& LandscapePositionArray) +{ + int32 VertexCount = LandscapePositionArray.Num(); + if (VertexCount < 3) + return false; + + // Create point attribute info containing positions. + HAPI_AttributeInfo AttributeInfoPointPosition; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPointPosition); + //FMemory::Memzero< HAPI_AttributeInfo >( AttributeInfoPointPosition ); + AttributeInfoPointPosition.count = VertexCount; + AttributeInfoPointPosition.tupleSize = 3; + AttributeInfoPointPosition.exists = true; + AttributeInfoPointPosition.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPointPosition.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPointPosition.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NodeId, 0, + HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPointPosition), false); + + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + bool bSuccess = Accessor.SetAttributeData(AttributeInfoPointPosition, (const float*)LandscapePositionArray.GetData()); + + return bSuccess; +} + +bool +FUnrealLandscapeTranslator::AddLandscapeNormalAttribute(const HAPI_NodeId& NodeId, const TArray& LandscapeNormalArray) +{ + int32 VertexCount = LandscapeNormalArray.Num(); + if (VertexCount < 3) + return false; + + HAPI_AttributeInfo AttributeInfoPointNormal; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPointNormal); + AttributeInfoPointNormal.count = VertexCount; + AttributeInfoPointNormal.tupleSize = 3; + AttributeInfoPointNormal.exists = true; + AttributeInfoPointNormal.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPointNormal.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPointNormal.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NodeId, + 0, HAPI_UNREAL_ATTRIB_NORMAL, &AttributeInfoPointNormal), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_NORMAL); + bool bSuccess = Accessor.SetAttributeData(AttributeInfoPointNormal, (const float*)LandscapeNormalArray.GetData()); + + return bSuccess; +} + +bool +FUnrealLandscapeTranslator::AddLandscapeUVAttribute(const HAPI_NodeId& NodeId, const TArray& LandscapeUVArray) +{ + int32 VertexCount = LandscapeUVArray.Num(); + if (VertexCount < 3) + return false; + + HAPI_AttributeInfo AttributeInfoPointUV; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPointUV); + AttributeInfoPointUV.count = VertexCount; + AttributeInfoPointUV.tupleSize = 3; + AttributeInfoPointUV.exists = true; + AttributeInfoPointUV.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPointUV.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPointUV.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NodeId, + 0, HAPI_UNREAL_ATTRIB_UV, &AttributeInfoPointUV), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_UV); + bool bSuccess = Accessor.SetAttributeData(AttributeInfoPointUV, (const float*)LandscapeUVArray.GetData()); + + return bSuccess; +} + +bool +FUnrealLandscapeTranslator::AddLandscapeComponentVertexIndicesAttribute(const HAPI_NodeId& NodeId, const TArray& LandscapeComponentVertexIndicesArray) +{ + int32 VertexCount = LandscapeComponentVertexIndicesArray.Num(); + if (VertexCount < 3) + return false; + + HAPI_AttributeInfo AttributeInfoPointLandscapeComponentVertexIndices; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPointLandscapeComponentVertexIndices); + AttributeInfoPointLandscapeComponentVertexIndices.count = VertexCount; + AttributeInfoPointLandscapeComponentVertexIndices.tupleSize = 2; + AttributeInfoPointLandscapeComponentVertexIndices.exists = true; + AttributeInfoPointLandscapeComponentVertexIndices.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPointLandscapeComponentVertexIndices.storage = HAPI_STORAGETYPE_INT; + AttributeInfoPointLandscapeComponentVertexIndices.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NodeId, + 0, HAPI_UNREAL_ATTRIB_LANDSCAPE_VERTEX_INDEX, + &AttributeInfoPointLandscapeComponentVertexIndices), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_LANDSCAPE_VERTEX_INDEX); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPointLandscapeComponentVertexIndices, (const int*)LandscapeComponentVertexIndicesArray.GetData()), false); + + return true; +} + +bool +FUnrealLandscapeTranslator::AddLandscapeComponentNameAttribute(const HAPI_NodeId& NodeId, const TArray& LandscapeComponentNameArray) +{ + int32 VertexCount = LandscapeComponentNameArray.Num(); + if (VertexCount < 3) + return false; + + // Create point attribute containing landscape component name. + HAPI_AttributeInfo AttributeInfoPointLandscapeComponentNames; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPointLandscapeComponentNames); + AttributeInfoPointLandscapeComponentNames.count = VertexCount; + AttributeInfoPointLandscapeComponentNames.tupleSize = 1; + AttributeInfoPointLandscapeComponentNames.exists = true; + AttributeInfoPointLandscapeComponentNames.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPointLandscapeComponentNames.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoPointLandscapeComponentNames.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NodeId, 0, + HAPI_UNREAL_ATTRIB_LANDSCAPE_TILE_NAME, + &AttributeInfoPointLandscapeComponentNames), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeStringData( + FHoudiniEngine::Get().GetSession(), NodeId, 0, + HAPI_UNREAL_ATTRIB_LANDSCAPE_TILE_NAME, + &AttributeInfoPointLandscapeComponentNames, + (const char**)LandscapeComponentNameArray.GetData(), + 0, AttributeInfoPointLandscapeComponentNames.count), false); + + return true; +} + +bool FUnrealLandscapeTranslator::AddLandscapeTileAttribute( + const HAPI_NodeId& NodeId, const HAPI_PartId& PartId, const int32& TileIdx ) +{ + HAPI_AttributeInfo AttributeInfoTileIndex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoTileIndex); + + AttributeInfoTileIndex.count = 1; + AttributeInfoTileIndex.tupleSize = 1; + AttributeInfoTileIndex.exists = true; + AttributeInfoTileIndex.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoTileIndex.storage = HAPI_STORAGETYPE_INT; + AttributeInfoTileIndex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NodeId, + PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_TILE, &AttributeInfoTileIndex ), false ); + + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::SetAttributeIntData( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, HAPI_UNREAL_ATTRIB_LANDSCAPE_TILE, &AttributeInfoTileIndex, + (const int *)&TileIdx, 0, AttributeInfoTileIndex.count ), false ); + + return true; +} + +bool +FUnrealLandscapeTranslator::AddLandscapeLightmapColorAttribute(const HAPI_NodeId& NodeId, const TArray& LandscapeLightmapValues) +{ + int32 VertexCount = LandscapeLightmapValues.Num(); + + HAPI_AttributeInfo AttributeInfoPointLightmapColor; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPointLightmapColor); + //FMemory::Memzero< HAPI_AttributeInfo >( AttributeInfoPointLightmapColor ); + AttributeInfoPointLightmapColor.count = VertexCount; + AttributeInfoPointLightmapColor.tupleSize = 4; + AttributeInfoPointLightmapColor.exists = true; + AttributeInfoPointLightmapColor.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPointLightmapColor.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPointLightmapColor.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NodeId, + 0, HAPI_UNREAL_ATTRIB_LIGHTMAP_COLOR, &AttributeInfoPointLightmapColor), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_LIGHTMAP_COLOR); + bool bSuccess = Accessor.SetAttributeData(AttributeInfoPointLightmapColor, (const float*)LandscapeLightmapValues.GetData()); + + return true; +} + +bool +FUnrealLandscapeTranslator::AddLandscapeMeshIndicesAndMaterialsAttribute( + const HAPI_NodeId& NodeId, const bool& bExportMaterials, + const int32& ComponentSizeQuads, const int32& QuadCount, + ALandscapeProxy * LandscapeProxy, + const TSet< ULandscapeComponent * >& SelectedComponents) +{ + if (!LandscapeProxy) + return false; + + // Compute number of necessary indices. + int32 IndexCount = QuadCount * 4; + if (IndexCount < 0) + return false; + + int32 VertexCountPerComponent = FMath::Square(ComponentSizeQuads + 1); + + // Array holding indices data. + TArray LandscapeIndices; + LandscapeIndices.SetNumUninitialized(IndexCount); + + // Allocate space for face names. + // The LandscapeMaterial and HoleMaterial per point + FHoudiniEngineIndexedStringMap FaceMaterials; + FHoudiniEngineIndexedStringMap FaceHoleMaterials; + + int32 VertIdx = 0; + int32 QuadIdx = 0; + + FString MatrialName; + FString HoleMaterialName; + + const int32 QuadComponentCount = ComponentSizeQuads + 1; + for (int32 ComponentIdx = 0; ComponentIdx < LandscapeProxy->LandscapeComponents.Num(); ComponentIdx++) + { + ULandscapeComponent * LandscapeComponent = LandscapeProxy->LandscapeComponents[ComponentIdx]; + if (!SelectedComponents.Contains(LandscapeComponent)) + continue; + + if (bExportMaterials) + { + // If component has an override material, we need to get the raw name (if exporting materials). + if (LandscapeComponent->OverrideMaterial) + { + MatrialName = LandscapeComponent->OverrideMaterial->GetName(); + } + + // If component has an override hole material, we need to get the raw name (if exporting materials). + if (LandscapeComponent->OverrideHoleMaterial) + { + HoleMaterialName = LandscapeComponent->OverrideHoleMaterial->GetName(); + } + } + + int32 BaseVertIndex = ComponentIdx * VertexCountPerComponent; + for (int32 YIdx = 0; YIdx < ComponentSizeQuads; YIdx++) + { + for (int32 XIdx = 0; XIdx < ComponentSizeQuads; XIdx++) + { + LandscapeIndices[VertIdx + 0] = BaseVertIndex + (XIdx + 0) + (YIdx + 0) * QuadComponentCount; + LandscapeIndices[VertIdx + 1] = BaseVertIndex + (XIdx + 1) + (YIdx + 0) * QuadComponentCount; + LandscapeIndices[VertIdx + 2] = BaseVertIndex + (XIdx + 1) + (YIdx + 1) * QuadComponentCount; + LandscapeIndices[VertIdx + 3] = BaseVertIndex + (XIdx + 0) + (YIdx + 1) * QuadComponentCount; + + // Store override materials (if exporting materials). + if (bExportMaterials) + { + FaceMaterials.SetString(QuadIdx, MatrialName); + FaceHoleMaterials.SetString(QuadIdx, HoleMaterialName); + } + + VertIdx += 4; + QuadIdx++; + } + } + } + + // We can now set vertex list. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetVertexList( + LandscapeIndices, NodeId, 0), false); + + // We need to generate array of face counts. + TArray LandscapeFaces; + LandscapeFaces.SetNumUninitialized(QuadCount); + for (int32 n = 0; n < QuadCount; n++) + LandscapeFaces[n] = 4; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetFaceCounts( + LandscapeFaces, NodeId, 0), false); + + if (bExportMaterials) + { + if (FaceMaterials.HasEntries()) + { + // Marshall in override primitive material names. + HAPI_AttributeInfo AttributeInfoPrimitiveMaterial; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPrimitiveMaterial); + AttributeInfoPrimitiveMaterial.count = FaceMaterials.Ids.Num(); + AttributeInfoPrimitiveMaterial.tupleSize = 1; + AttributeInfoPrimitiveMaterial.exists = true; + AttributeInfoPrimitiveMaterial.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoPrimitiveMaterial.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoPrimitiveMaterial.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_MATERIAL, &AttributeInfoPrimitiveMaterial), + false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_MATERIAL); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeStringMap(AttributeInfoPrimitiveMaterial, FaceMaterials), false); + } + + if (FaceHoleMaterials.HasEntries()) + { + // Marshall in override primitive material hole names. + HAPI_AttributeInfo AttributeInfoPrimitiveMaterialHole; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPrimitiveMaterialHole); + AttributeInfoPrimitiveMaterialHole.count = FaceHoleMaterials.GetIds().Num(); + AttributeInfoPrimitiveMaterialHole.tupleSize = 1; + AttributeInfoPrimitiveMaterialHole.exists = true; + AttributeInfoPrimitiveMaterialHole.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoPrimitiveMaterialHole.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoPrimitiveMaterialHole.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_MATERIAL_HOLE, + &AttributeInfoPrimitiveMaterialHole), + false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_MATERIAL_HOLE); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeStringMap(AttributeInfoPrimitiveMaterialHole, FaceHoleMaterials), false); + } + } + + return true; +} + +bool +FUnrealLandscapeTranslator::AddLandscapeGlobalMaterialAttribute( + const HAPI_NodeId& NodeId, ALandscapeProxy * LandscapeProxy) +{ + if (!LandscapeProxy) + return false; + + // If there's a global landscape material, we marshall it as detail. + UMaterialInterface * MaterialInterface = LandscapeProxy->GetLandscapeMaterial(); + + HAPI_AttributeInfo AttributeInfoDetailMaterial; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoDetailMaterial); + AttributeInfoDetailMaterial.count = 1; + AttributeInfoDetailMaterial.tupleSize = 1; + AttributeInfoDetailMaterial.exists = true; + AttributeInfoDetailMaterial.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoDetailMaterial.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoDetailMaterial.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NodeId, 0, + HAPI_UNREAL_ATTRIB_MATERIAL, &AttributeInfoDetailMaterial), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_MATERIAL); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoDetailMaterial, MaterialInterface->GetPathName()), false); + + // If there's a global landscape hole material, we marshall it as detail. + UMaterialInterface * HoleMaterialInterface = LandscapeProxy->GetLandscapeHoleMaterial(); + + HAPI_AttributeInfo AttributeInfoDetailMaterialHole; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoDetailMaterialHole); + AttributeInfoDetailMaterialHole.count = 1; + AttributeInfoDetailMaterialHole.tupleSize = 1; + AttributeInfoDetailMaterialHole.exists = true; + AttributeInfoDetailMaterialHole.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoDetailMaterialHole.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoDetailMaterialHole.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_MATERIAL_HOLE, + &AttributeInfoDetailMaterialHole), false); + + Accessor.Init(NodeId, 0, HAPI_UNREAL_ATTRIB_MATERIAL_HOLE); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoDetailMaterialHole, HoleMaterialInterface->GetPathName()), false); + + return true; +} + + +bool +FUnrealLandscapeTranslator::AddLandscapeLayerAttribute( + const HAPI_NodeId& NodeId, const TArray& LandscapeLayerArray, const FString& LayerName) +{ + int32 VertexCount = LandscapeLayerArray.Num(); + if (VertexCount < 3) + return false; + + HAPI_AttributeInfo AttributeInfoLayer; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoLayer); + AttributeInfoLayer.count = VertexCount; + AttributeInfoLayer.tupleSize = 1; + AttributeInfoLayer.exists = true; + AttributeInfoLayer.owner = HAPI_ATTROWNER_POINT; + AttributeInfoLayer.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoLayer.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, + TCHAR_TO_ANSI(*LayerName), + &AttributeInfoLayer), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, TCHAR_TO_ANSI(*LayerName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoLayer, LandscapeLayerArray), false); + + return true; +} + +HAPI_NodeId +FUnrealLandscapeTranslator::CreateVolumeLayer(ALandscapeProxy* LandscapeProxy, + const FString & VolumeNameLayer, + const HAPI_Transform & NodeTransform, + HAPI_NodeId HeightFieldId, + HAPI_PartId PartId, + HAPI_PartId MaskId, + int XSize, + int YSize, + TArray & Data) +{ + // Create the volume + + HAPI_VolumeInfo CurrentLayerVolumeInfo; + FHoudiniApi::VolumeInfo_Init(&CurrentLayerVolumeInfo); + CurrentLayerVolumeInfo.transform = NodeTransform; + CurrentLayerVolumeInfo.xLength = XSize; + CurrentLayerVolumeInfo.yLength = YSize; + CurrentLayerVolumeInfo.zLength = 1; + CurrentLayerVolumeInfo.minX = 0; + CurrentLayerVolumeInfo.minY = 0; + CurrentLayerVolumeInfo.minZ = 0; + CurrentLayerVolumeInfo.type = HAPI_VOLUMETYPE_HOUDINI; + CurrentLayerVolumeInfo.storage = HAPI_STORAGETYPE_FLOAT; + CurrentLayerVolumeInfo.tupleSize = 1; + CurrentLayerVolumeInfo.tileSize = 1; + CurrentLayerVolumeInfo.hasTaper = false; + CurrentLayerVolumeInfo.xTaper = 0.0; + CurrentLayerVolumeInfo.yTaper = 0.0; + + // 3. See if we need to create an input volume, or can reuse the HF's default mask volume + bool IsMask = VolumeNameLayer.Equals(TEXT("mask"), ESearchCase::IgnoreCase); + + HAPI_NodeId LayerVolumeNodeId = -1; + if (!IsMask) + { + // Current layer is not mask, so we need to create a new input volume + std::string TargetLayerNameString; + FHoudiniEngineUtils::ConvertUnrealString(VolumeNameLayer, TargetLayerNameString); + FHoudiniApi::CreateHeightfieldInputVolumeNode(FHoudiniEngine::Get().GetSession(), HeightFieldId, &LayerVolumeNodeId, TargetLayerNameString.c_str(), XSize, YSize, 1.0f); + } + else + { + // Current Layer is mask, so we simply reuse the mask volume node created by default by the heightfield node + LayerVolumeNodeId = MaskId; + } + + // Check if we have a valid id for the input volume. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(LayerVolumeNodeId)) + return -1; + + // 4. Set the layer/mask height field data in Houdini + HAPI_PartId CurrentPartId = 0; + if (!SetHeightfieldData(LayerVolumeNodeId, PartId, Data, CurrentLayerVolumeInfo, VolumeNameLayer)) + return -1; + + // Apply attributes to the height field input node + ApplyAttributesToHeightfieldNode(LayerVolumeNodeId, PartId, LandscapeProxy); + + // Commit the volume's geo + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(LayerVolumeNodeId), -1); + + return LayerVolumeNodeId; + +} + +bool FUnrealLandscapeTranslator::SendTargetLayersToHoudini( + ALandscapeProxy* LandscapeProxy, + HAPI_NodeId HeightFieldId, + HAPI_PartId PartId, + HAPI_NodeId MergeId, + HAPI_NodeId MaskId, + const FHoudiniLandscapeExportOptions& Options, + const HAPI_VolumeInfo& HeightFieldVolumeInfo, + int32 XSize, + int32 YSize, + int32& OutMergeInputIndex) +{ + + bool bSuccess = true; + + // Send target layers to Houdini. Note, we always need to create and commit a Mask or the Volume Info won't function + // correctly. + if (Options.bExportMergedPaintLayers) + { + bSuccess &= SendCombinedTargetLayersToHoudini(LandscapeProxy, HeightFieldId, PartId, MergeId, MaskId, HeightFieldVolumeInfo, XSize, YSize, OutMergeInputIndex); + } + else + { + InitDefaultHeightfieldMask(HeightFieldVolumeInfo, MaskId); + ApplyAttributesToHeightfieldNode(MaskId, PartId, LandscapeProxy); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(MaskId), false); + } + + if (Options.bExportPaintLayersPerEditLayer) + bSuccess &= SendAllEditLayerTargetLayersToHoudini(LandscapeProxy, HeightFieldId, PartId, MergeId, MaskId, HeightFieldVolumeInfo, XSize, YSize, OutMergeInputIndex); + + return bSuccess; +} + +bool FUnrealLandscapeTranslator::SendCombinedTargetLayersToHoudini( + ALandscapeProxy* LandscapeProxy, + HAPI_NodeId HeightFieldId, + HAPI_PartId PartId, + HAPI_NodeId MergeId, + HAPI_NodeId MaskId, + const HAPI_VolumeInfo& HeightfieldVolumeInfo, + int32 XSize, + int32 YSize, + int32 & OutMergeInputIndex) +{ + // This function sends the combined target (paint) layers to Houdini. + // "Combined" means that all target layers in all edit layers are combined. + + ULandscapeInfo* LandscapeInfo = LandscapeProxy->GetLandscapeInfo(); + if (!LandscapeInfo) + return false; + + bool MaskInitialized = false; + + int32 NumTargetLayers = LandscapeInfo->Layers.Num(); + for (int32 TargetLayerIndex = 0; TargetLayerIndex < NumTargetLayers; TargetLayerIndex++) + { + // 1. Extract the uint8 values from the layer + TArray LayerData; + FLinearColor TargetLayerDebugColor; + FString TargetLayerName; + if (!GetLandscapeTargetLayerData(LandscapeProxy, LandscapeInfo, TargetLayerIndex, LayerData, TargetLayerDebugColor, TargetLayerName)) + { + continue; + } + + // 2. Convert unreal uint8 values to floats + // If the layer came from Houdini, additional info might have been stored in the DebugColor to convert the data back to float + + TArray CurrentLayerFloatData; + if (!ConvertLandscapeLayerDataToHeightfieldData(LayerData, XSize, YSize, TargetLayerDebugColor, CurrentLayerFloatData)) + { + continue; + } + + int LayerVolumeNodeId = CreateVolumeLayer(LandscapeProxy, + TargetLayerName, + HeightfieldVolumeInfo.transform, + HeightFieldId, + PartId, + MaskId, + YSize, + XSize, + CurrentLayerFloatData); + + if (LayerVolumeNodeId == -1) + return false; + + if (!TargetLayerName.Equals(TEXT("mask"), ESearchCase::IgnoreCase)) + { + // We had to create a new volume for this layer, so we need to connect it to the HF's merge node + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(FHoudiniEngine::Get().GetSession(), MergeId, OutMergeInputIndex, LayerVolumeNodeId, 0),false); + OutMergeInputIndex++; + } + else + { + MaskInitialized = true; + } + } + + // We need to have a mask layer as it is required for proper height field functionality + // Setting the volume info on the mask is needed for the HF to have proper transform in H! + // If we didn't create a mask volume before, send a default one now + if (!MaskInitialized) + { + InitDefaultHeightfieldMask(HeightfieldVolumeInfo, MaskId); + + ApplyAttributesToHeightfieldNode(MaskId, PartId, LandscapeProxy); + + // Commit the mask volume's geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(MaskId), false); + } + + return true; +} + +bool FUnrealLandscapeTranslator::SendAllEditLayerTargetLayersToHoudini( + ALandscapeProxy* LandscapeProxy, + HAPI_NodeId HeightFieldId, + HAPI_PartId PartId, + HAPI_NodeId MergeId, + HAPI_NodeId MaskId, + const HAPI_VolumeInfo& HeightfieldVolumeInfo, + int32 XSize, + int32 YSize, + int32& OutMergeInputIndex) +{ + // This function sends the Target (paint) layer for each Edit Layer separately to Houdini. + + ULandscapeInfo* LandscapeInfo = LandscapeProxy->GetLandscapeInfo(); + if (!LandscapeInfo) + return false; + + ALandscape * Landscape = LandscapeProxy->GetLandscapeActor(); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + for(int EditLayerIndex = 0; EditLayerIndex < Landscape->GetLayersConst().Num(); EditLayerIndex++) +#else + for (int EditLayerIndex = 0; EditLayerIndex < Landscape->GetLayerCount(); EditLayerIndex++) +#endif + { + int32 NumTargetLayers = LandscapeInfo->Layers.Num(); + for (int32 TargetLayerIndex = 0; TargetLayerIndex < NumTargetLayers; TargetLayerIndex++) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + const FName& EditLayerName = LandscapeProxy->GetLandscapeActor()->GetLayerConst(EditLayerIndex)->EditLayer->GetName(); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + const FName & EditLayerName = LandscapeProxy->GetLandscapeActor()->GetLayerConst(EditLayerIndex)->Name; +#else + const FName & EditLayerName = LandscapeProxy->GetLandscapeActor()->GetLayer(EditLayerIndex)->Name; +#endif + const FName & TargetLayerName = LandscapeInfo->Layers[TargetLayerIndex].GetLayerName(); + + FHoudiniExtents Extents = FHoudiniLandscapeUtils::GetLandscapeExtents(LandscapeProxy); + TArray LayerData = FHoudiniLandscapeUtils::GetLayerData(Landscape, Extents, EditLayerName, TargetLayerName); + + ULandscapeLayerInfoObject * LayerInfoObject = Landscape->GetLandscapeInfo()->GetLayerInfoByName(TargetLayerName); + + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + FLinearColor Color = LayerInfoObject ? LayerInfoObject->GetLayerUsageDebugColor() : FLinearColor::White; +#else + FLinearColor Color = LayerInfoObject ? LayerInfoObject->LayerUsageDebugColor : FLinearColor::White; +#endif + + TArray CurrentLayerFloatData; + if (!ConvertLandscapeLayerDataToHeightfieldData(LayerData, XSize, YSize, Color, CurrentLayerFloatData)) + { + continue; + } + + FString LayerName = FString::Format(TEXT("landscapelayer_{0}_{1}"), { EditLayerName.ToString(), TargetLayerName.ToString() }); + + int LayerVolumeNodeId = CreateVolumeLayer(LandscapeProxy, + LayerName, + HeightfieldVolumeInfo.transform, + HeightFieldId, + PartId, + MaskId, + YSize, + XSize, + CurrentLayerFloatData); + + if (LayerVolumeNodeId == -1) + return false; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(FHoudiniEngine::Get().GetSession(), MergeId, OutMergeInputIndex, LayerVolumeNodeId, 0), false); + OutMergeInputIndex++; + + } + } + + return true; +} + + +``` + +`Source/HoudiniEngine/Private/UnrealLandscapeTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "Landscape.h" +#include "HAPI/HAPI_Common.h" + +class ALandscapeProxy; +class UHoudiniInputLandscape; +class FUnrealObjectInputHandle; +class UHoudiniInput; + +struct FHoudiniLandscapeExportOptions +{ + bool bExportHeightDataPerEditLayer; + bool bExportPaintLayersPerEditLayer; + bool bExportMergedPaintLayers; +}; + +struct HOUDINIENGINE_API FUnrealLandscapeTranslator +{ + public: + + // ------------------------------------------------------------------------------------------ + // Unreal Landscape to Houdini Heightfield + // ------------------------------------------------------------------------------------------ + static bool CreateHeightfieldFromLandscape( + ALandscapeProxy* LandcapeProxy, + const FHoudiniLandscapeExportOptions & Options, + HAPI_NodeId& CreatedHeightfieldNodeId, + const FString &InputNodeNameStr, + HAPI_NodeId ParentNodeId, + bool bSetObjectTransformToWorldTransform); + + static bool CreateHeightfieldFromLandscapeComponentArray( + ALandscapeProxy* LandscapeProxy, + const TSet< ULandscapeComponent * > & SelectedComponents, + const FHoudiniLandscapeExportOptions& Options, + HAPI_NodeId& CreatedHeightfieldNodeId, + const FString &InputNodeNameStr, + const HAPI_NodeId& ParentNodeId, + bool bSetObjectTransformToWorldTransform); + + static bool CreateHeightfieldFromLandscapeComponent( + ALandscapeProxy* LandscapeProxy, + ULandscapeComponent * LandscapeComponent, + const int32& ComponentIndex, + HAPI_NodeId& HeightFieldId, + HAPI_NodeId& MergeId, + int32& MergeInputIndex, + const FHoudiniLandscapeExportOptions& Options, + const FString& InputNodeNameStr, + const FTransform & ParentTransform, + const HAPI_NodeId& ParentNodeId); + + static bool CreateInputNodeForLandscape( + ALandscapeProxy* LandscapeProxy, + const FString& InputNodeNameStr, + const FString& HeightFieldName, + const FTransform& LandscapeTransform, + HAPI_NodeId& HeightId, + HAPI_PartId& PartId, + HAPI_NodeId& HeightFieldId, + HAPI_NodeId& MaskId, + HAPI_NodeId& MergeId, + TArray& HeightData, + HAPI_VolumeInfo& HeightfieldVolumeInfo, + int32& XSize, + int32& YSize, + const HAPI_NodeId& ParentNodeId); + + static bool CreateInputNodeForLandscapeObject( + ALandscapeProxy* InLandscape, + UHoudiniInput* InInput, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + const bool& bInputNodesCanBeDeleted); + + static void ApplyAttributesToHeightfieldNode( + const HAPI_NodeId HeightId, + const HAPI_PartId PartId, + ALandscapeProxy* LandscapeProxy + ); + + // Extracts the uint16 values of a given landscape + static bool GetLandscapeData( + ALandscapeProxy* LandscapeProxy, + TArray& HeightData, + int32& XSize, int32& YSize, + FVector3d& Min, FVector3d& Max); + + static bool GetLandscapeData( + ULandscapeInfo* LandscapeInfo, + const int32& MinX, + const int32& MinY, + const int32& MaxX, + const int32& MaxY, + TArray& HeightData, + int32& XSize, int32& YSize); + + static void GetLandscapeProxyBounds( + ALandscapeProxy* LandscapeProxy, + FVector3d& Origin, FVector3d& Extents); + + // Converts Unreal uint16 values to Houdini Float + static bool ConvertLandscapeDataToHeightFieldData( + const TArray& IntHeightData, + int32 XSize, + int32 YSize, + FVector Min, + FVector Max, + const FVector3d& LandscapeActorScale, + TArray& HeightfieldFloatValues, + HAPI_VolumeInfo& HeightfieldVolumeInfo); + + // Converts Unreal uint8 values to Houdini Float + static bool ConvertLandscapeLayerDataToHeightfieldData( + const TArray& IntHeightData, + int32 XSize, int32 YSize, + const FLinearColor& LayerUsageDebugColor, + TArray& LayerFloatValues); + + // Creates an unlocked heightfield input node + static bool CreateHeightfieldInputNode( + const FString& NodeName, + const int32& XSize, + const int32& YSize, + HAPI_NodeId& HeightfieldNodeId, + HAPI_NodeId& HeightNodeId, + HAPI_NodeId& MaskNodeId, + HAPI_NodeId& MergeNodeId, + HAPI_NodeId ParentNodeId); + + // Set the volume float value for a heightfield + static bool SetHeightfieldData( + const HAPI_NodeId& AssetId, + const HAPI_PartId& PartId, + TArray< float >& FloatValues, + const HAPI_VolumeInfo& VolumeInfo, + const FString& HeightfieldName); + + static bool AddLandscapeMaterialAttributesToVolume( + const HAPI_NodeId& VolumeNodeId, + const HAPI_PartId& PartId, + UMaterialInterface* LandscapeMaterial, + UMaterialInterface* LandscapeHoleMaterial, + UPhysicalMaterial* InPhysicalMaterial); + + /* + static bool AddLevelPathAttributeToVolume( + const HAPI_NodeId& VolumeNodeId, + const HAPI_PartId& PartId, + const FString& LevelPath); + */ + + // Extracts the uint8 values of a given landscape + static bool GetLandscapeTargetLayerData( + ALandscapeProxy* LandscapeProxy, + ULandscapeInfo* LandscapeInfo, + int32 TargetLayerIndex, + TArray& TargetLayerData, + FLinearColor& TargetLayerDebugColor, + FString& TaregtLayerName); + + static bool GetLandscapeTargetLayerData( + ULandscapeInfo* LandscapeInfo, + int32 TargetLayerIndex, + int32 MinX, + int32 MinY, + int32 MaxX, + int32 MaxY, + TArray& TargetLayerData, + FLinearColor& TargetLayerUsageDebugColor, + FString& TargetLayerName); + + // Initialise the Heightfield Mask with default values + static bool InitDefaultHeightfieldMask( + const HAPI_VolumeInfo& HeightVolumeInfo, + const HAPI_NodeId& MaskVolumeNodeId); + + // Landscape nodes clean up + static bool DestroyLandscapeAssetNode( + HAPI_NodeId& ConnectedAssetId, + TArray& CreatedInputAssetIds); + + + //-------------------------------------------------------------------------------------------------- + // Unreal to Houdini - MESH / POINTS + //-------------------------------------------------------------------------------------------------- + + static bool CreateMeshOrPointsFromLandscape( + ALandscapeProxy* InLandscape, + HAPI_NodeId& InputNodeId, + const FString& InInputNodeNameString, + const bool bExportGeometryAsMesh, + const bool bExportTileUVs, + const bool bExportNormalizedUVs, + const bool bExportLighting, + const bool bExportMaterials, + const bool bApplyWorldTransform, + const HAPI_NodeId& ParentNodeId); + + // Extract data from the landscape + static bool ExtractLandscapeData( + ALandscapeProxy * LandscapeProxy, + TSet& SelectedComponents, + const bool& bExportLighting, + const bool& bExportTileUVs, + const bool& bExportNormalizedUVs, + bool bApplyWorldTransform, + TArray& LandscapePositionArray, + TArray& LandscapeNormalArray, + TArray& LandscapeUVArray, + TArray& LandscapeComponentVertexIndicesArray, + TArray& LandscapeComponentNameArray, + TArray& LandscapeLightmapValues); + + // Helper functions to extract color from a texture + static FColor PickVertexColorFromTextureMip( + const uint8 * MipBytes, + FVector2D & UVCoord, + int32 MipWidth, + int32 MipHeight); + + // Add the Position attribute extracted from a landscape + static bool AddLandscapePositionAttribute( + const HAPI_NodeId& NodeId, + const TArray& LandscapePositionArray); + + // Add the Normal attribute extracted from a landscape + static bool AddLandscapeNormalAttribute( + const HAPI_NodeId& NodeId, + const TArray& LandscapeNormalArray); + + // Add the UV attribute extracted from a landscape + static bool AddLandscapeUVAttribute( + const HAPI_NodeId& NodeId, + const TArray& LandscapeUVArray); + + // Add the Component Vertex Index attribute extracted from a landscape + static bool AddLandscapeComponentVertexIndicesAttribute( + const HAPI_NodeId& NodeId, + const TArray& LandscapeComponentVertexIndicesArray); + + // Add the Component Name attribute extracted from a landscape + static bool AddLandscapeComponentNameAttribute( + const HAPI_NodeId& NodeId, + const TArray& LandscapeComponentNameArray); + + static bool AddLandscapeTileAttribute( + const HAPI_NodeId& NodeId, const HAPI_PartId& PartId, const int32& TileIdx ); + + // Add the lightmap color attribute extracted from a landscape + static bool AddLandscapeLightmapColorAttribute( + const HAPI_NodeId& NodeId, + const TArray& LandscapeLightmapValues); + + // Creates and add the vertex indices and face materials attribute from a landscape + static bool AddLandscapeMeshIndicesAndMaterialsAttribute( + const HAPI_NodeId& NodeId, + const bool& bExportMaterials, + const int32& ComponentSizeQuads, + const int32& QuadCount, + ALandscapeProxy * LandscapeProxy, + const TSet& SelectedComponents); + + // Add the global (detail) material and hole material attribute from a landscape + static bool AddLandscapeGlobalMaterialAttribute( + const HAPI_NodeId& NodeId, + ALandscapeProxy * LandscapeProxy); + + // Add landscape layer values as point attributes + static bool AddLandscapeLayerAttribute( + const HAPI_NodeId& NodeId, + const TArray& LandscapeLayerArray, + const FString& LayerName); + + static bool SendCombinedTargetLayersToHoudini( + ALandscapeProxy* LandscapeProxy, + HAPI_NodeId HeightFieldId, + HAPI_PartId PartId, + HAPI_NodeId MergeId, + HAPI_NodeId MaskId, + const HAPI_VolumeInfo& HeightfieldVolumeInfo, + int32 XSize, + int32 YSize, + int32 & OutMergeInputIndex); + + + static bool SendAllEditLayerTargetLayersToHoudini( + ALandscapeProxy* LandscapeProxy, + HAPI_NodeId HeightFieldId, + HAPI_PartId PartId, + HAPI_NodeId MergeId, + HAPI_NodeId MaskId, + const HAPI_VolumeInfo& HeightfieldVolumeInfo, + int32 XSize, + int32 YSize, + int32& OutMergeInputIndex); + + static bool SendTargetLayersToHoudini( + ALandscapeProxy* LandscapeProxy, + HAPI_NodeId HeightFieldId, + HAPI_PartId PartId, + HAPI_NodeId MergeId, + HAPI_NodeId MaskId, + const FHoudiniLandscapeExportOptions& Options, + const HAPI_VolumeInfo& HeightfieldVolumeInfo, + int32 XSize, + int32 YSize, + int32& OutMergeInputIndex); + + static HAPI_NodeId CreateVolumeLayer(ALandscapeProxy* LandscapeProxy, + const FString& VolumeNameLayer, + const HAPI_Transform& NodeTransform, + HAPI_NodeId HeightFieldId, + HAPI_PartId PartId, + HAPI_PartId MaskId, + int XSize, + int YSize, + TArray& Data); +}; +``` + +`Source/HoudiniEngine/Private/UnrealLevelInstanceTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealLevelInstanceTranslator.h" +#include "LevelInstance/LevelInstanceActor.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineUtils.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputRuntimeUtils.h" +#include "UnrealObjectInputUtils.h" + +bool FUnrealLevelInstanceTranslator::AddLevelInstance( + ALevelInstance* LevelInstance, + UHoudiniInput* HoudiniInput, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + const bool bInputNodesCanBeDeleted) +{ + CreateNodeForLevelInstance( + LevelInstance, + HoudiniInput, + InputNodeId, + InputNodeName, + OutHandle, + bInputNodesCanBeDeleted); + + CreateAttributeData(InputNodeId, LevelInstance); + + return true; +} + +bool +FUnrealLevelInstanceTranslator::CreateNodeForLevelInstance( + ALevelInstance* LevelInstance, + UHoudiniInput* HoudiniInput, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + const bool bInputNodesCanBeDeleted) +{ + FUnrealObjectInputIdentifier Identifier; + FUnrealObjectInputIdentifier GeoNodeIdentifier; + FUnrealObjectInputHandle ParentHandle; + + FString FinalInputNodeName = InputNodeName; + HAPI_NodeId ParentNodeId = -1; + + { + FUnrealObjectInputOptions Options; + Options.bExportLevelInstanceContent = false; + Identifier = FUnrealObjectInputIdentifier(LevelInstance, Options, true); + + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId)) + { + if (!bInputNodesCanBeDeleted) + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + + OutHandle = Handle; + InputNodeId = NodeId; + return true; + } + } + + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted)) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // Set InputNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that InputNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (Handle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, InputNodeId)) + InputNodeId = -1; + } + else + { + InputNodeId = -1; + } + } + + HAPI_NodeId InputObjectNodeId = -1; + + if (FHoudiniEngineUtils::IsHoudiniNodeValid(InputNodeId)) + { + InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(InputNodeId); + } + else + { + HAPI_NodeId NewNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateInputNode(FinalInputNodeName, NewNodeId, ParentNodeId), false); + + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(NewNodeId)) + return false; + + InputNodeId = NewNodeId; + InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(InputNodeId); + } + + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 1; + Part.vertexCount = 0; + Part.faceCount = 0; + Part.pointCount = 1; + Part.type = HAPI_PARTTYPE_MESH; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo(FHoudiniEngine::Get().GetSession(), InputNodeId, 0, &Part), false); + + { + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, InputNodeId, Handle, InputObjectNodeId, nullptr, bInputNodesCanBeDeleted)) + OutHandle = Handle; + } + + return true; + +} + +bool FUnrealLevelInstanceTranslator::AddStringPointAttribute(HAPI_NodeId NodeId, const char* Name, TArray& Data) +{ + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_POINT; + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + AttributeInfo.typeInfo = HAPI_ATTRIBUTE_TYPE_NONE; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), NodeId, 0, Name, &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, Name); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfo, Data), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(NodeId), false); + + return true; +} + + +bool FUnrealLevelInstanceTranslator::AddFloatPointAttribute(HAPI_NodeId NodeId, const char * Name, const float * Data, int TupleSize, int Count) +{ + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + + AttributeInfo.count = 1; + AttributeInfo.tupleSize = 3; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_POINT; + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + AttributeInfo.typeInfo = HAPI_ATTRIBUTE_TYPE_NONE; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(),NodeId,0,Name, &AttributeInfo), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData(FHoudiniEngine::Get().GetSession(), NodeId, 0, Name, &AttributeInfo, Data, 0, Count), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(NodeId), false); + + return true; +} + +void FUnrealLevelInstanceTranslator::CreateAttributeData(HAPI_NodeId NodeId, ALevelInstance* LevelInstance) +{ + constexpr int NumInstances = 1; + TArray Positions; + TArray ActorPaths; + TArray ActorLevels; + + Positions.Reserve(NumInstances * 3); + ActorPaths.Reserve(NumInstances); + ActorLevels.Reserve(NumInstances); + + ActorPaths.Add(LevelInstance->GetPathName()); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 + FString Level = LevelInstance->GetWorldAsset().ToSoftObjectPath().GetAssetPath().ToString(); +#else + FString Level = LevelInstance->GetWorldAsset().ToSoftObjectPath().GetAssetPathName().ToString(); +#endif + ActorLevels.Add(Level); + + const FTransform& Transform = LevelInstance->GetActorTransform(); + + FVector PositionVector = Transform.GetLocation(); + Positions.Add(0.0f); + Positions.Add(0.0f); + Positions.Add(0.0f); + + AddFloatPointAttribute(NodeId, HAPI_UNREAL_ATTRIB_POSITION, Positions.GetData(), 3, 1); + AddStringPointAttribute(NodeId, HAPI_UNREAL_ATTRIB_ACTOR_PATH, ActorPaths); + AddStringPointAttribute(NodeId, HAPI_UNREAL_ATTRIB_LEVEL_INSTANCE_NAME, ActorLevels); + + FHoudiniEngineUtils::HapiCommitGeo(NodeId); +} + + + +``` + +`Source/HoudiniEngine/Private/UnrealLevelInstanceTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#include "HAPI/HAPI_Common.h" + +class ALevelInstance; +class ALandscapeProxy; +class UHoudiniInputLandscape; +class FUnrealObjectInputHandle; +class UHoudiniInput; + +class ALevelInstance; + +struct HOUDINIENGINE_API FUnrealLevelInstanceTranslator +{ +public: + static bool AddLevelInstance( + ALevelInstance* LevelInstance, + UHoudiniInput* HoudiniInput, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + const bool bInputNodesCanBeDeleted); + +private: + static bool CreateNodeForLevelInstance( + ALevelInstance* LevelInstance, + UHoudiniInput* InInput, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + const bool bInputNodesCanBeDeleted); + + static bool AddFloatPointAttribute(HAPI_NodeId NodeId, const char* Name, const float* Data, int TupleSize, int Count); + + static bool AddStringPointAttribute(HAPI_NodeId NodeId, const char* Name, TArray & Data); + + static void CreateAttributeData(HAPI_NodeId NodeId, ALevelInstance* LevelInstance); + +}; +``` + +`Source/HoudiniEngine/Private/UnrealMeshTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealMeshTranslator.h" + +#include "HoudiniDataLayerUtils.h" +#include "HoudiniEngine.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineTimers.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniMeshUtils.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputRuntimeUtils.h" +#include "UnrealObjectInputUtils.h" + +#include "Components/SplineMeshComponent.h" +#include "Components/StaticMeshComponent.h" +#include "DynamicMeshBuilder.h" +#include "Engine/StaticMesh.h" +#include "Engine/StaticMeshSocket.h" +#include "Materials/Material.h" +#include "Materials/MaterialInterface.h" +#include "MeshAttributes.h" +#include "MeshDescription.h" +#include "MeshDescriptionOperations.h" +#include "MeshUtilities.h" +#include "PhysicsEngine/BodySetup.h" +#include "PhysicalMaterials/PhysicalMaterial.h" +#include "SkeletalMeshAttributes.h" +#include "StaticMeshAttributes.h" +#include "StaticMeshResources.h" + + +#include "HoudiniEngineAttributes.h" +#include "UnrealObjectInputManager.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + #include "Engine/SkinnedAssetCommon.h" + #include "MaterialDomain.h" + #include "StaticMeshComponentLODInfo.h" +#endif + +#if WITH_EDITOR + #include "EditorFramework/AssetImportData.h" +#endif + +bool FUnrealMeshTranslator::bUseNewMeshPath = true; + +const FString FUnrealMeshTranslator::LODPrefix = TEXT("lod"); +const FString FUnrealMeshTranslator::HiResMeshName = TEXT("hires"); +const FString FUnrealMeshTranslator::MTLParams = TEXT("mtl_params"); +const FString FUnrealMeshTranslator::CombinePrefix = TEXT("combined_"); +const FString FUnrealMeshTranslator::MaterialTableName = TEXT("material_table"); + +bool +FUnrealMeshTranslator::CreateInputNodeForStaticMesh( + HAPI_NodeId& InputNodeId, + FUnrealObjectInputHandle& OutHandle, + const UStaticMesh* StaticMesh, + const UStaticMeshComponent* StaticMeshComponent, + const FString& InputNodeName, + const FUnrealMeshExportOptions& ExportOptions, + const bool bInputNodesCanBeDeleted, + const bool bForceReferenceInputNodeCreation) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FUnrealMeshTranslator::HapiCreateInputNodeForStaticMesh); + + if(bUseNewMeshPath) + { + bool bSuccess = CreateInputNodeForStaticMeshNew(InputNodeId, OutHandle, StaticMesh, StaticMeshComponent, InputNodeName, ExportOptions, bInputNodesCanBeDeleted); + return bSuccess; + } + + // If we don't have a static mesh there's nothing to do. + if (!IsValid(StaticMesh)) + return false; + + const USplineMeshComponent* SplineMeshComponent = nullptr; + bool bIsSplineMesh = false; + if (IsValid(StaticMeshComponent)) + { + SplineMeshComponent = Cast(StaticMeshComponent); + bIsSplineMesh = IsValid(SplineMeshComponent); + } + + // Only set bMainMeshIsNaniteFallback to true if this is a Nanite mesh and we are sending the fallback + // For non-Nanite meshes bMainMeshIsNaniteFallback should always be false +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + const bool bNaniteBuildEnabled = StaticMesh->IsNaniteEnabled(); +#else + const bool bNaniteBuildEnabled = StaticMesh->NaniteSettings.bEnabled; +#endif + const bool ShouldUseNaniteFallback = ExportOptions.bPreferNaniteFallbackMesh && StaticMesh->GetRenderData()->LODResources.Num(); + const bool bMainMeshIsNaniteFallback = bNaniteBuildEnabled && ShouldUseNaniteFallback && !bIsSplineMesh && (ExportOptions.bMainMesh || ExportOptions.bLODs); + + // Input node name, default to InputNodeName, but can be changed by the new input system + FString FinalInputNodeName = InputNodeName; + + // Find the node in new input system + // Identifier will be the identifier for the entry created in this call of the function. We may call this function + // recursively to create the main mesh, LODs, sockets and colliders, each getting its own identifier. + FUnrealObjectInputIdentifier Identifier; + FUnrealObjectInputHandle ParentHandle; + HAPI_NodeId ParentNodeId = -1; + const UObject* InputSystemObject = bIsSplineMesh ? static_cast(SplineMeshComponent) : static_cast(StaticMesh); + { + // Check if we already have an input node for this asset + bool bSingleLeafNodeOnly = false; + FUnrealObjectInputIdentifier ReferenceNodeIdentifier; + TArray IdentPerOption; + + if (!FUnrealObjectInputUtils::BuildMeshInputObjectIdentifiers( + InputSystemObject, + ExportOptions, + bMainMeshIsNaniteFallback, + ExportOptions.bMaterialParameters, + bForceReferenceInputNodeCreation, + bSingleLeafNodeOnly, + ReferenceNodeIdentifier, + IdentPerOption)) + { + return false; + } + + if (bSingleLeafNodeOnly) + { + // We'll create the static mesh input node entirely is this function call + check(!IdentPerOption.IsEmpty()); + Identifier = IdentPerOption[0]; + } + else + { + // Look for the reference node that references the per-option (LODs, sockets, colliders) nodes + Identifier = ReferenceNodeIdentifier; + } + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId) && (bSingleLeafNodeOnly || FUnrealObjectInputUtils::AreReferencedHAPINodesValid(Handle))) + { + if (!bInputNodesCanBeDeleted) + { + // Make sure to prevent deletion of the input node if needed + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + } + + OutHandle = Handle; + InputNodeId = NodeId; + return true; + } + } + + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + // Create any parent/container nodes that we would need, and get the node id of the immediate parent + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // We now need to create the nodes (since we couldn't find existing ones in the manager) + // For the single leaf node case we can simply continue this function + // For the ref + multiple options, we call this function again for each option (as a single leaf node) and + // then create the reference node. + if (!bSingleLeafNodeOnly) + { + TSet PerOptionNodeHandles; + PerOptionNodeHandles.Reserve(IdentPerOption.Num()); + for (const FUnrealObjectInputIdentifier& OptionIdentifier : IdentPerOption) + { + const FUnrealObjectInputOptions& Options = OptionIdentifier.GetOptions(); + + FString NodeLabel; + FUnrealObjectInputUtils::GetDefaultInputNodeName(OptionIdentifier, NodeLabel); + + HAPI_NodeId NewNodeId = -1; + FUnrealObjectInputHandle OptionHandle; + if (FUnrealObjectInputUtils::FindNodeViaManager(OptionIdentifier, OptionHandle)) + { + FUnrealObjectInputUtils::GetHAPINodeId(OptionHandle, NewNodeId); + } + + FUnrealMeshExportOptions InputExportOptions; + InputExportOptions.bLODs = Options.bExportLODs; + InputExportOptions.bSockets = Options.bExportSockets; + InputExportOptions.bColliders = Options.bExportColliders; + InputExportOptions.bMainMesh = !Options.bExportLODs && !Options.bExportSockets && !Options.bExportColliders; + InputExportOptions.bMaterialParameters = Options.bExportMaterialParameters; + InputExportOptions.bPreferNaniteFallbackMesh = Options.bMainMeshIsNaniteFallbackMesh; + + static constexpr bool bForceInputRefNodeCreation = false; + if (!CreateInputNodeForStaticMesh( + NewNodeId, + OptionHandle, + StaticMesh, + StaticMeshComponent, + NodeLabel, + InputExportOptions, + bInputNodesCanBeDeleted, + bForceInputRefNodeCreation)) + { + return false; + } + + PerOptionNodeHandles.Add(OptionHandle); + } + + // Create or update the HAPI node for the reference node if it does not exist + FUnrealObjectInputHandle RefNodeHandle; + if (!FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(ReferenceNodeIdentifier, PerOptionNodeHandles, RefNodeHandle, true, bInputNodesCanBeDeleted)) + return false; + + OutHandle = RefNodeHandle; + FUnrealObjectInputUtils::GetHAPINodeId(ReferenceNodeIdentifier, InputNodeId); + return true; + } + + // Set InputNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that InputNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (Handle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, InputNodeId)) + InputNodeId = -1; + } + else + { + InputNodeId = -1; + } + } + + // Delete previous node first + // This will avoid naming collisions due to the previous node already existing? + // + HAPI_NodeId PreviousInputNodeId = InputNodeId; + if (PreviousInputNodeId >= 0) + { + // Get the parent OBJ node ID before deleting! + HAPI_NodeId PreviousInputOBJNode = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousInputNodeId); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input node for %s."), *FinalInputNodeName); + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputOBJNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input OBJ node for %s."), *FinalInputNodeName); + } + } + + + // Node ID for the newly created node + HAPI_NodeId NewNodeId = -1; + + // Export sockets if there are some + bool DoExportSockets = ExportOptions.bSockets && (StaticMesh->Sockets.Num() > 0); + + // Export LODs if there are some + bool DoExportLODs = ExportOptions.bLODs && (StaticMesh->GetNumLODs() > 1); + + // Export colliders if there are some + bool DoExportColliders = ExportOptions.bColliders && StaticMesh->GetBodySetup() != nullptr; + if (DoExportColliders) + { + if (StaticMesh->GetBodySetup()->AggGeom.GetElementCount() <= 0) + { + DoExportColliders = false; + } + } + + // We need to use a merge node if we export lods OR sockets + bool UseMergeNode = DoExportLODs || DoExportSockets || DoExportColliders; + if (UseMergeNode) + { + // TODO: + // What if OutInputNodeId already exists? + // Delete previous merge?/input? + + // Create a merge SOP asset. This will be our "InputNodeId" + // as all the different LOD meshes and sockets will be plugged into it + if (ParentNodeId < 0) + { + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + -1, TEXT("SOP/merge"), FinalInputNodeName, true, &NewNodeId), false); + } + else + { + // When creating a node inside a parent node (in other words, ParentNodeId is not -1), then we cannot + // specify the node type category prefix on the node name. We have to create the geo Object and merge + // SOPs separately. + HAPI_NodeId ObjectNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode(ParentNodeId, TEXT("geo"), FinalInputNodeName, true, &ObjectNodeId), false); + + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode(ObjectNodeId, TEXT("merge"), FinalInputNodeName, true, &NewNodeId), false); + } + } + else + { + // No LODs/Sockets, we just need a single input node + // If InputNodeId is invalid, we need to create an input node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateInputNode(FinalInputNodeName, NewNodeId, ParentNodeId), false); + + if (!FHoudiniEngineUtils::HapiCookNode(NewNodeId, nullptr, true)) + return false; + } + + // Check if we have a valid id for this new input asset. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(NewNodeId)) + return false; + + + // Update our input NodeId + InputNodeId = NewNodeId; + // Get our parent OBJ NodeID + HAPI_NodeId InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(NewNodeId); + + // TODO: + // Setting for lightmap resolution? + + // Next Index used to connect nodes to the merge + int32 NextMergeIndex = 0; + + // Should we export the HiRes Nanite Mesh? + const bool bHaveHiResSourceModel = StaticMesh->IsHiResMeshDescriptionValid(); + bool bHiResMeshSuccess = false; + const bool bWantToExportHiResModel = bNaniteBuildEnabled && ExportOptions.bMainMesh && !ShouldUseNaniteFallback && !bIsSplineMesh; + if (bWantToExportHiResModel && bHaveHiResSourceModel) + { + // Get the HiRes Mesh description and SourceModel + FMeshDescription HiResMeshDescription = *StaticMesh->GetHiResMeshDescription(); + + const FStaticMeshSourceModel& HiResSrcModel = StaticMesh->GetHiResSourceModel(); + const FMeshBuildSettings& HiResBuildSettings = HiResSrcModel.BuildSettings; // cannot be const because FMeshDescriptionHelper modifies the LightmapIndex fields ?!? + + // If we're using a merge node, we need to create a new input null + HAPI_NodeId CurrentNodeId = -1; + if (UseMergeNode) + { + // Create a new input node for the HiRes Mesh in this input object's OBJ node + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(InputObjectNodeId, TEXT("null"), TEXT("HiRes"), false, &CurrentNodeId), false); + } + else + { + // No merge node, just use the input node we created before + CurrentNodeId = NewNodeId; + } + + // Convert the Mesh using FMeshDescription + const double StartTime = FPlatformTime::Seconds(); + bHiResMeshSuccess = FUnrealMeshTranslator::CreateInputNodeForMeshDescription( + CurrentNodeId, + HiResMeshDescription, + -1, + false, + ExportOptions.bMaterialParameters, + StaticMesh, + StaticMeshComponent); + + HOUDINI_LOG_MESSAGE(TEXT("FUnrealMeshTranslator::CreateInputNodeForMeshDescription HiRes mesh completed in %.4f seconds"), FPlatformTime::Seconds() - StartTime); + + if (UseMergeNode) + { + // Connect the HiRes mesh node to the merge node if needed + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + NewNodeId, NextMergeIndex, CurrentNodeId, 0), false); + } + + NextMergeIndex++; + } + + // Determine which LODs to export based on the ExportLODs/bMainMesh, high res mesh availability and whether + // the new input system is being used. + const int32 NumLODs = StaticMesh->GetNumLODs(); + int32 FirstLODIndex = 0; + int32 LastLODIndex = -1; + { + if (DoExportLODs) + { + // With the new system we export LODs and the main mesh in separate steps. We only want to export LOD0 with + // the LODs if this is a Nanite mesh + if (bHaveHiResSourceModel && bNaniteBuildEnabled) + { + LastLODIndex = NumLODs - 1; + FirstLODIndex = 0; + } + else + { + // Don't export LOD0 with the LODs if this is not a nanite mesh, since we have a separate "main mesh" + // input + LastLODIndex = NumLODs - 1; + FirstLODIndex = 1; + } + } + else if (ExportOptions.bMainMesh) + { + if (bHiResMeshSuccess) + { + LastLODIndex = -1; + FirstLODIndex = 0; + } + else + { + // Without nanite, the main mesh/high res mesh is LOD0 + LastLODIndex = 0; + FirstLODIndex = 0; + } + } + else + { + LastLODIndex = -1; + FirstLODIndex = 0; + } + } + + if (LastLODIndex >= 0) + { + for (int32 LODIndex = FirstLODIndex; LODIndex <= LastLODIndex; LODIndex++) + { + // Grab the LOD level. + const FStaticMeshSourceModel & SrcModel = StaticMesh->GetSourceModel(LODIndex); + + // If we're using a merge node, we need to create a new input null + HAPI_NodeId CurrentLODNodeId = -1; + if (UseMergeNode) + { + // Create a new input node for the current LOD + + FString LODName = TEXT("lod") + FString::FromInt(LODIndex); + + // Create the node in this input object's OBJ node + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode(InputObjectNodeId, TEXT("null"), LODName, false, &CurrentLODNodeId), false); + } + else + { + // No merge node, just use the input node we created before + CurrentLODNodeId = NewNodeId; + } + + // Export the current LOD Mesh by using MeshDescription + FMeshDescription* MeshDesc = nullptr; + FMeshDescription SplineMeshDesc; + + // This will either fetch the mesh description that is cached on the SrcModel + // or load it from bulk data / DDC once + if (!bIsSplineMesh) + { + if (SrcModel.GetCachedMeshDescription() != nullptr) + { + MeshDesc = SrcModel.GetCachedMeshDescription(); + } + else + { + const double StartTime = FPlatformTime::Seconds(); + MeshDesc = StaticMesh->GetMeshDescription(LODIndex); + HOUDINI_LOG_MESSAGE(TEXT("StaticMesh->GetMeshDescription completed in %.4f seconds"), FPlatformTime::Seconds() - StartTime); + } + } + else + { + // Deform mesh data according to the Spline Mesh Component's data + static constexpr bool bPropagateVertexColours = false; + static constexpr bool bApplyComponentTransform = false; + FHoudiniMeshUtils::RetrieveMesh(SplineMeshDesc, SplineMeshComponent, LODIndex, bPropagateVertexColours, bApplyComponentTransform); + MeshDesc = &SplineMeshDesc; + } + + // Should we use Mesh Description? Depends on Nanite settings, if we have a valid Mesh Description and + // if exporting LODs. (Recently discovered that Mesh Description can be null for LODs) + + bool bUseMeshDescription = (!bNaniteBuildEnabled || !ShouldUseNaniteFallback); + + if(DoExportLODs) + bUseMeshDescription = false; + else if(!MeshDesc) + bUseMeshDescription = false; + + bool bMeshSuccess = false; + if (bUseMeshDescription) + { + // Convert the Mesh using FMeshDescription + const double StartTime = FPlatformTime::Seconds(); + bMeshSuccess = FUnrealMeshTranslator::CreateInputNodeForMeshDescription( + CurrentLODNodeId, + *MeshDesc, + LODIndex, + DoExportLODs, + ExportOptions.bMaterialParameters, + StaticMesh, + StaticMeshComponent); + HOUDINI_LOG_MESSAGE(TEXT("FUnrealMeshTranslator::CreateInputNodeForMeshDescription completed in %.4f seconds"), FPlatformTime::Seconds() - StartTime); + } + else + { + // Convert the LOD Mesh using FStaticMeshLODResources + const double StartTime = FPlatformTime::Seconds(); + bMeshSuccess = FUnrealMeshTranslator::CreateInputNodeForStaticMeshLODResources( + CurrentLODNodeId, + StaticMesh->GetLODForExport(LODIndex), + LODIndex, + DoExportLODs, + ExportOptions.bMaterialParameters, + StaticMesh, + StaticMeshComponent); + HOUDINI_LOG_MESSAGE(TEXT("FUnrealMeshTranslator::CreateInputNodeForStaticMeshLODResources completed in %.4f seconds"), FPlatformTime::Seconds() - StartTime); + } + + if (!bMeshSuccess) + continue; + + if (UseMergeNode) + { + // Connect the LOD node to the merge node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(FHoudiniEngine::Get().GetSession(), + NewNodeId, NextMergeIndex, CurrentLODNodeId, 0), false); + } + + NextMergeIndex++; + } + } + + if (DoExportColliders && StaticMesh->GetBodySetup() != nullptr) + { + ExportCollisions(NextMergeIndex, StaticMesh, NewNodeId, InputObjectNodeId, StaticMesh->GetBodySetup()->AggGeom); + } + + if (DoExportSockets && StaticMesh->Sockets.Num() > 0) + { + // Create an input node for the mesh sockets + HAPI_NodeId SocketsNodeId = -1; + if (CreateInputNodeForMeshSockets(StaticMesh->Sockets, InputObjectNodeId, SocketsNodeId)) + { + // We can connect the socket node to the merge node's last input. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), NewNodeId, NextMergeIndex, SocketsNodeId, 0), false); + + NextMergeIndex++; + } + else if (SocketsNodeId != -1) + { + // If we failed to properly export the sockets, clean up the created node + FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), SocketsNodeId); + } + } + + { + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, InputNodeId, Handle, InputObjectNodeId, nullptr, bInputNodesCanBeDeleted)) + OutHandle = Handle; + } + + // + return true; +} + +bool +FUnrealMeshTranslator::CreateInputNodeForMeshSockets( + const TArray& InMeshSocket, + const HAPI_NodeId InParentNodeId, + HAPI_NodeId& OutSocketsNodeId) +{ + // Create a new input node for the sockets + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + InParentNodeId, TEXT("null"), "sockets", false, &OutSocketsNodeId), false); + + int32 NumSockets = InMeshSocket.Num(); + if (NumSockets <= 0) + return true; + + // Create part. + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + Part.pointCount = NumSockets; + Part.vertexCount = 0; + Part.faceCount = 0; + Part.type = HAPI_PARTTYPE_MESH; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, &Part), false); + + // Create POS point attribute info. + HAPI_AttributeInfo AttributeInfoPos; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPos); + AttributeInfoPos.count = NumSockets; + AttributeInfoPos.tupleSize = 3; + AttributeInfoPos.exists = true; + AttributeInfoPos.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPos.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPos.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPos), false); + + // Create Rot point attribute Info + HAPI_AttributeInfo AttributeInfoRot; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoRot); + AttributeInfoRot.count = NumSockets; + AttributeInfoRot.tupleSize = 4; + AttributeInfoRot.exists = true; + AttributeInfoRot.owner = HAPI_ATTROWNER_POINT; + AttributeInfoRot.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoRot.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_ROTATION, &AttributeInfoRot), false); + + // Create scale point attribute Info + HAPI_AttributeInfo AttributeInfoScale; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoScale); + AttributeInfoScale.count = NumSockets; + AttributeInfoScale.tupleSize = 3; + AttributeInfoScale.exists = true; + AttributeInfoScale.owner = HAPI_ATTROWNER_POINT; + AttributeInfoScale.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoScale.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_SCALE, &AttributeInfoScale), false); + + // Create the name attrib info + HAPI_AttributeInfo AttributeInfoName; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoName); + AttributeInfoName.count = NumSockets; + AttributeInfoName.tupleSize = 1; + AttributeInfoName.exists = true; + AttributeInfoName.owner = HAPI_ATTROWNER_POINT; + AttributeInfoName.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoName.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_MESH_SOCKET_NAME, &AttributeInfoName), false); + + // Create the tag attrib info + HAPI_AttributeInfo AttributeInfoTag; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoTag); + AttributeInfoTag.count = NumSockets; + AttributeInfoTag.tupleSize = 1; + AttributeInfoTag.exists = true; + AttributeInfoTag.owner = HAPI_ATTROWNER_POINT; + AttributeInfoTag.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoTag.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_MESH_SOCKET_TAG, &AttributeInfoTag), false); + + // Extract the sockets transform values + TArray SocketPos; + SocketPos.SetNumZeroed(NumSockets * 3); + TArray SocketRot; + SocketRot.SetNumZeroed(NumSockets * 4); + TArray SocketScale; + SocketScale.SetNumZeroed(NumSockets * 3); + + TArray SocketNames; + TArray SocketTags; + for (int32 Idx = 0; Idx < NumSockets; ++Idx) + { + UStaticMeshSocket* CurrentSocket = InMeshSocket[Idx]; + if (!IsValid(CurrentSocket)) + continue; + + // Get the socket's transform and convert it to HapiTransform + FTransform SocketTransform(CurrentSocket->RelativeRotation, CurrentSocket->RelativeLocation, CurrentSocket->RelativeScale); + HAPI_Transform HapiSocketTransform; + FHoudiniApi::Transform_Init(&HapiSocketTransform); + FHoudiniEngineUtils::TranslateUnrealTransform(SocketTransform, HapiSocketTransform); + + // Fill the attribute values + SocketPos[3 * Idx + 0] = HapiSocketTransform.position[0]; + SocketPos[3 * Idx + 1] = HapiSocketTransform.position[1]; + SocketPos[3 * Idx + 2] = HapiSocketTransform.position[2]; + + SocketRot[4 * Idx + 0] = HapiSocketTransform.rotationQuaternion[0]; + SocketRot[4 * Idx + 1] = HapiSocketTransform.rotationQuaternion[1]; + SocketRot[4 * Idx + 2] = HapiSocketTransform.rotationQuaternion[2]; + SocketRot[4 * Idx + 3] = HapiSocketTransform.rotationQuaternion[3]; + + SocketScale[3 * Idx + 0] = HapiSocketTransform.scale[0]; + SocketScale[3 * Idx + 1] = HapiSocketTransform.scale[1]; + SocketScale[3 * Idx + 2] = HapiSocketTransform.scale[2]; + + FString CurrentSocketName; + if (!CurrentSocket->SocketName.IsNone()) + CurrentSocketName = CurrentSocket->SocketName.ToString(); + else + CurrentSocketName = TEXT("Socket") + FString::FromInt(Idx); + SocketNames.Add(CurrentSocketName); + + if (!CurrentSocket->Tag.IsEmpty()) + SocketTags.Add(CurrentSocket->Tag); + else + SocketTags.Add(""); + } + + //we can now upload them to our attribute. + + FHoudiniHapiAccessor Accessor; + Accessor.Init(OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + Accessor.SetAttributeData(AttributeInfoPos, SocketPos); + + Accessor.Init(OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_ROTATION); + Accessor.SetAttributeData(AttributeInfoRot, SocketRot); + + Accessor.Init(OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_SCALE); + Accessor.SetAttributeData(AttributeInfoScale, SocketScale); + + Accessor.Init(OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_MESH_SOCKET_NAME); + Accessor.SetAttributeData(AttributeInfoName, SocketNames); + + Accessor.Init(OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_MESH_SOCKET_TAG); + Accessor.SetAttributeData(AttributeInfoTag, SocketTags); + + // We will also create the socket_details attributes + for (int32 Idx = 0; Idx < NumSockets; ++Idx) + { + // Build the current socket's prefix + FString SocketAttrPrefix = TEXT(HAPI_UNREAL_ATTRIB_MESH_SOCKET_PREFIX) + FString::FromInt(Idx); + + // Create mesh_socketX_pos attribute info. + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPos); + AttributeInfoPos.count = 1; + AttributeInfoPos.tupleSize = 3; + AttributeInfoPos.exists = true; + AttributeInfoPos.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoPos.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPos.originalOwner = HAPI_ATTROWNER_INVALID; + + FString PosAttr = SocketAttrPrefix + TEXT("_pos"); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*PosAttr), &AttributeInfoPos), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*PosAttr), &AttributeInfoPos, + &(SocketPos[3 * Idx]), 0, AttributeInfoPos.count), false); + + // Create mesh_socketX_rot point attribute Info + FHoudiniApi::AttributeInfo_Init(&AttributeInfoRot); + AttributeInfoRot.count = 1; + AttributeInfoRot.tupleSize = 4; + AttributeInfoRot.exists = true; + AttributeInfoRot.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoRot.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoRot.originalOwner = HAPI_ATTROWNER_INVALID; + + FString RotAttr = SocketAttrPrefix + TEXT("_rot"); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*RotAttr), &AttributeInfoRot), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*RotAttr), &AttributeInfoRot, + &(SocketRot[4 * Idx]), 0, AttributeInfoRot.count), false); + + // Create mesh_socketX_scale point attribute Info + FHoudiniApi::AttributeInfo_Init(&AttributeInfoScale); + AttributeInfoScale.count = 1; + AttributeInfoScale.tupleSize = 3; + AttributeInfoScale.exists = true; + AttributeInfoScale.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoScale.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoScale.originalOwner = HAPI_ATTROWNER_INVALID; + + FString ScaleAttr = SocketAttrPrefix + TEXT("_scale"); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*ScaleAttr), &AttributeInfoScale), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*ScaleAttr), &AttributeInfoScale, + &(SocketScale[3 * Idx]), 0, AttributeInfoScale.count), false); + + // Create the mesh_socketX_name attrib info + FHoudiniApi::AttributeInfo_Init(&AttributeInfoName); + AttributeInfoName.count = 1; + AttributeInfoName.tupleSize = 1; + AttributeInfoName.exists = true; + AttributeInfoName.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoName.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoName.originalOwner = HAPI_ATTROWNER_INVALID; + + FString NameAttr = SocketAttrPrefix + TEXT("_name"); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*NameAttr), &AttributeInfoName), false); + + Accessor.Init(OutSocketsNodeId, 0, TCHAR_TO_ANSI(*NameAttr)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoName, SocketNames[Idx]), false); + + // Create the mesh_socketX_tag attrib info + FHoudiniApi::AttributeInfo_Init(&AttributeInfoTag); + AttributeInfoTag.count = 1; + AttributeInfoTag.tupleSize = 1; + AttributeInfoTag.exists = true; + AttributeInfoTag.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoTag.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoTag.originalOwner = HAPI_ATTROWNER_INVALID; + + FString TagAttr = SocketAttrPrefix + TEXT("_tag"); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*TagAttr), &AttributeInfoTag), false); + + Accessor.Init(OutSocketsNodeId, 0, TCHAR_TO_ANSI(*TagAttr)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoTag, SocketTags[Idx]), false); + } + + // Now add the sockets group + const char * SocketGroupStr = "socket_imported"; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddGroup( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_GROUPTYPE_POINT, SocketGroupStr), false); + + // Set GroupMembership + TArray GroupArray; + GroupArray.SetNumUninitialized(NumSockets); + for (int32 n = 0; n < GroupArray.Num(); n++) + GroupArray[n] = 1; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetGroupMembership( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_GROUPTYPE_POINT, SocketGroupStr, GroupArray.GetData(), 0, NumSockets), false); + + // Commit the geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(OutSocketsNodeId), false); + + return true; +} + +bool +FUnrealMeshTranslator::CreateInputNodeForStaticMeshLODResources( + const HAPI_NodeId NodeId, + const FStaticMeshLODResources& LODResources, + const int32 InLODIndex, + const bool bAddLODGroups, + const bool bInExportMaterialParametersAsAttributes, + const UStaticMesh* StaticMesh, + const UStaticMeshComponent* StaticMeshComponent) +{ + bool bDoTiming = CVarHoudiniEngineMeshBuildTimer.GetValueOnAnyThread() != 0.0; + + // Convert the Mesh using FStaticMeshLODResources + + // Check that the mesh is not empty + if (LODResources.VertexBuffers.StaticMeshVertexBuffer.GetNumVertices() == 0) + { + HOUDINI_LOG_ERROR(TEXT("No vertices in mesh!")); + return false; + } + + if (LODResources.Sections.Num() == 0) + { + HOUDINI_LOG_ERROR(TEXT("No triangles in mesh!")); + return false; + } + + // Vertex instance and triangle counts + const uint32 OrigNumVertexInstances = LODResources.VertexBuffers.StaticMeshVertexBuffer.GetNumVertices(); + const uint32 NumTriangles = LODResources.GetNumTriangles(); + const uint32 NumVertexInstances = NumTriangles * 3; + const uint32 NumSections = LODResources.Sections.Num(); + + // Grab the build scale + const FStaticMeshSourceModel &SourceModel = StaticMesh->GetSourceModel(InLODIndex); + FVector3f BuildScaleVector = (FVector3f)SourceModel.BuildSettings.BuildScale3D; + + //--------------------------------------------------------------------------------------------------------------------- + // POSITION (P) + //--------------------------------------------------------------------------------------------------------------------- + // In FStaticMeshLODResources each vertex instances stores its position, even if the positions are not unique (in other + // words, in Houdini terminology, the number of points and vertices are the same. We'll do the same thing that Epic + // does in FBX export: we'll run through all vertex instances and use a hash to determine which instances share a + // position, so that we can a smaller number of points than vertices, and vertices share point positions + + bool bDoTimings = CVarHoudiniEngineMeshBuildTimer.GetValueOnAnyThread() != 0.0; + FHoudiniPerfTimer PositionsTimer(TEXT("Positions"), bDoTimings); + PositionsTimer.Start(); + + TArray UEVertexInstanceIdxToPointIdx; + UEVertexInstanceIdxToPointIdx.Reserve(OrigNumVertexInstances); + + TMap PositionToPointIndexMap; + PositionToPointIndexMap.Reserve(OrigNumVertexInstances); + + TArray StaticMeshVertices; + StaticMeshVertices.Reserve(OrigNumVertexInstances * 3); + for (uint32 VertexInstanceIndex = 0; VertexInstanceIndex < OrigNumVertexInstances; ++VertexInstanceIndex) + { + // Convert Unreal to Houdini + const FVector3f &PositionVector = LODResources.VertexBuffers.PositionVertexBuffer.VertexPosition(VertexInstanceIndex); + const int32 *FoundPointIndexPtr = PositionToPointIndexMap.Find(PositionVector); + if (!FoundPointIndexPtr) + { + const int32 NewPointIndex = StaticMeshVertices.Add(PositionVector.X / HAPI_UNREAL_SCALE_FACTOR_POSITION * BuildScaleVector.X) / 3; + StaticMeshVertices.Add(PositionVector.Z / HAPI_UNREAL_SCALE_FACTOR_POSITION * BuildScaleVector.Z); + StaticMeshVertices.Add(PositionVector.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION * BuildScaleVector.Y); + + PositionToPointIndexMap.Add(PositionVector, NewPointIndex); + UEVertexInstanceIdxToPointIdx.Add(NewPointIndex); + } + else + { + UEVertexInstanceIdxToPointIdx.Add(*FoundPointIndexPtr); + } + } + + StaticMeshVertices.Shrink(); + const uint32 NumVertices = StaticMeshVertices.Num() / 3; + + // Now that we know how many vertices (points), vertex instances (vertices) and triagnles we have, + // we can create the part. + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + Part.vertexCount = NumVertexInstances; + Part.faceCount = NumTriangles; + Part.pointCount = NumVertices; + Part.type = HAPI_PARTTYPE_MESH; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), NodeId, 0, &Part), false); + + // Create point attribute info. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + //FMemory::Memzero< HAPI_AttributeInfo >( AttributeInfoPoint ); + AttributeInfoPoint.count = Part.pointCount; + AttributeInfoPoint.tupleSize = 3; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NodeId, 0, + HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint), false); + + // Now that we have raw positions, we can upload them for our attribute. + FHoudiniHapiAccessor Accessor; + Accessor.Init(NodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPoint, StaticMeshVertices), false); + + PositionsTimer.Stop(); + + + // Determine which attributes we have + const bool bIsVertexInstanceNormalsValid = true; + const bool bIsVertexInstanceTangentsValid = true; + const bool bIsVertexInstanceBinormalsValid = true; + const bool bIsVertexInstanceColorsValid = LODResources.bHasColorVertexData; + const uint32 NumUVLayers = FMath::Min(LODResources.VertexBuffers.StaticMeshVertexBuffer.GetNumTexCoords(), MAX_STATIC_TEXCOORDS); + const bool bIsVertexInstanceUVsValid = NumUVLayers > 0; + + bool bUseComponentOverrideColors = false; + // Determine if have override colors on the static mesh component, if so prefer to use those + if (StaticMeshComponent && + StaticMeshComponent->LODData.IsValidIndex(InLODIndex) && + StaticMeshComponent->LODData[InLODIndex].OverrideVertexColors) + { + const FStaticMeshComponentLODInfo& ComponentLODInfo = StaticMeshComponent->LODData[InLODIndex]; + FColorVertexBuffer& ColorVertexBuffer = *ComponentLODInfo.OverrideVertexColors; + + if (ColorVertexBuffer.GetNumVertices() == LODResources.GetNumVertices()) + { + bUseComponentOverrideColors = true; + } + } + + //--------------------------------------------------------------------------------------------------------------------- + // MATERIAL INDEX -> MATERIAL INTERFACE + //--------------------------------------------------------------------------------------------------------------------- + + FHoudiniPerfTimer MaterialTimer(TEXT("Materials"), bDoTimings); + MaterialTimer.Start(); + + double MaterialsTickTime = FPlatformTime::Seconds(); + + TArray MaterialInterfaces; + TArray TriangleMaterialIndices; + + const TArray& StaticMaterials = StaticMesh->GetStaticMaterials(); + + // If the static mesh component is valid, and we are not using the ref counted input system, get the materials via + // the component to account for overrides. For the ref counted input system the component will override the + // materials in its input node. + const bool bIsStaticMeshComponentValid = (IsValid(StaticMeshComponent) && StaticMeshComponent->IsValidLowLevel()); + const int32 NumStaticMaterials = StaticMaterials.Num(); + // If we find any invalid Material (null or pending kill), or we find a section below with an out of range MaterialIndex, + // then we will set UEDefaultMaterial at the invalid index + int32 UEDefaultMaterialIndex = INDEX_NONE; + UMaterialInterface *UEDefaultMaterial = nullptr; + if (NumStaticMaterials > 0) + { + MaterialInterfaces.Reserve(NumStaticMaterials); + for (int32 MaterialIndex = 0; MaterialIndex < NumStaticMaterials; ++MaterialIndex) + { + const FStaticMaterial &MaterialInfo = StaticMaterials[MaterialIndex]; + UMaterialInterface *Material = nullptr; + { + Material = MaterialInfo.MaterialInterface; + } + // If the Material is NULL or invalid, fallback to the default material + if (!IsValid(Material)) + { + if (!UEDefaultMaterial) + { + UEDefaultMaterial = UMaterial::GetDefaultMaterial(EMaterialDomain::MD_Surface); + UEDefaultMaterialIndex = MaterialIndex; + } + Material = UEDefaultMaterial; + HOUDINI_LOG_WARNING(TEXT("Material Index %d (slot %s) has an invalid material, falling back to default: %s"), MaterialIndex, *(MaterialInfo.MaterialSlotName.ToString()), *(UEDefaultMaterial->GetPathName())); + } + // MaterialSlotToInterface.Add(MaterialInfo.ImportedMaterialSlotName, MaterialIndex); + MaterialInterfaces.Add(Material); + } + + TriangleMaterialIndices.Reserve(NumTriangles); + } + + MaterialTimer.Stop(); + + // If we haven't created UEDefaultMaterial yet, check that all the sections' MaterialIndex + // is valid, if not, create UEDefaultMaterial and add to MaterialInterfaces to get UEDefaultMaterialIndex + if (!UEDefaultMaterial || UEDefaultMaterialIndex == INDEX_NONE) + { + for (uint32 SectionIndex = 0; SectionIndex < NumSections; ++SectionIndex) + { + // If the MaterialIndex referenced by this Section is out of range, fill MaterialInterfaces with UEDefaultMaterial + // up to and including MaterialIndex and log a warning + const int32 MaterialIndex = LODResources.Sections[SectionIndex].MaterialIndex; + if (!MaterialInterfaces.IsValidIndex(MaterialIndex)) + { + if (!UEDefaultMaterial) + { + UEDefaultMaterial = UMaterial::GetDefaultMaterial(EMaterialDomain::MD_Surface); + // Add the UEDefaultMaterial to MaterialInterfaces + UEDefaultMaterialIndex = MaterialInterfaces.Add(UEDefaultMaterial); + } + HOUDINI_LOG_WARNING(TEXT("Section Index %d references an invalid Material Index %d, falling back to default material: %s"), SectionIndex, MaterialIndex, *(UEDefaultMaterial->GetPathName())); + } + } + } + + // Determine the final number of materials we have, with default for missing/invalid indices + const int32 NumMaterials = MaterialInterfaces.Num(); + + if(bDoTiming) + { + HOUDINI_LOG_MESSAGE(TEXT("CreateInputNodeForStaticMeshLODResources() - materials %f secs"), FPlatformTime::Seconds() - MaterialsTickTime); + } + + FHoudiniPerfTimer MakeUVTimer(TEXT("UV Make"), bDoTimings); + FHoudiniPerfTimer MakeNormalTimer(TEXT("Normals Make"), bDoTimings); + FHoudiniPerfTimer MakeTangentTimer(TEXT("Tangents Make"), bDoTimings); + FHoudiniPerfTimer MakeBinormalTimer(TEXT("Binormals Make"), bDoTimings); + FHoudiniPerfTimer MakeColorsTimer(TEXT("Colors Make"), bDoTimings); + FHoudiniPerfTimer MakeAlphasTimer(TEXT("Alphas Make"), bDoTimings); + + FHoudiniPerfTimer TransferUVTimer(TEXT("UV Transfer"), bDoTimings); + FHoudiniPerfTimer TransferNormalTimer(TEXT("Normals Transfer"), bDoTimings); + FHoudiniPerfTimer TransferTangentTimer(TEXT("Tangents Transfer"), bDoTimings); + FHoudiniPerfTimer TransferBinormalTimer(TEXT("Binormals Transfer"), bDoTimings); + FHoudiniPerfTimer TransferColorsTimer(TEXT("Colors Transfer"), bDoTimings); + FHoudiniPerfTimer TransferAlphasTimer(TEXT("Alphas Transfer"), bDoTimings); + + // Now we deal with vertex instance attributes. + if (NumTriangles > 0) + { + // UV layer array. Each layer has an array of floats, 3 floats per vertex instance + TArray> UVs; + // Normals: 3 floats per vertex instance + TArray Normals; + // Tangents: 3 floats per vertex instance + TArray Tangents; + // Binormals: 3 floats per vertex instance + TArray Binormals; + // RGBColors: 3 floats per vertex instance + TArray RGBColors; + // Alphas: 1 float per vertex instance + TArray Alphas; + + // Initialize the arrays for the attributes that are valid + if (bIsVertexInstanceUVsValid) + { + UVs.SetNum(NumUVLayers); + for (uint32 UVLayerIndex = 0; UVLayerIndex < NumUVLayers; ++UVLayerIndex) + { + UVs[UVLayerIndex].SetNumUninitialized(NumVertexInstances * 3); + } + } + + if (bIsVertexInstanceNormalsValid) + { + Normals.SetNumUninitialized(NumVertexInstances * 3); + } + + if (bIsVertexInstanceTangentsValid) + { + Tangents.SetNumUninitialized(NumVertexInstances * 3); + } + + if (bIsVertexInstanceBinormalsValid) + { + Binormals.SetNumUninitialized(NumVertexInstances * 3); + } + + if (bUseComponentOverrideColors || bIsVertexInstanceColorsValid) + { + RGBColors.SetNumUninitialized(NumVertexInstances * 3); + Alphas.SetNumUninitialized(NumVertexInstances); + } + + // Array of vertex (point position) indices per triangle + TArray MeshTriangleVertexIndices; + MeshTriangleVertexIndices.SetNumUninitialized(NumVertexInstances); + // Array of vertex counts per triangle/face + TArray MeshTriangleVertexCounts; + MeshTriangleVertexCounts.SetNumUninitialized(NumTriangles); + + int32 TriangleIdx = 0; + int32 HoudiniVertexIdx = 0; + FIndexArrayView TriangleVertexIndices = LODResources.IndexBuffer.GetArrayView(); + for (uint32 SectionIndex = 0; SectionIndex < NumSections; ++SectionIndex) + { + const FStaticMeshSection& Section = LODResources.Sections[SectionIndex]; + for (uint32 SectionTriangleIndex = 0; SectionTriangleIndex < Section.NumTriangles; ++SectionTriangleIndex) + { + MeshTriangleVertexCounts[TriangleIdx] = 3; + for (int32 TriangleVertexIndex = 0; TriangleVertexIndex < 3; ++TriangleVertexIndex) + { + // Reverse the winding order for Houdini (but still start at 0) + const int32 WindingIdx = (3 - TriangleVertexIndex) % 3; + const uint32 UEVertexIndex = TriangleVertexIndices[Section.FirstIndex + SectionTriangleIndex * 3 + WindingIdx]; + + // Calculate the index of the first component of a vertex instance's value in an inline float array + // representing vectors (3 float) per vertex instance + const int32 Float3Index = HoudiniVertexIdx * 3; + + //--------------------------------------------------------------------------------------------------------------------- + // UVS (uvX) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceUVsValid) + { + MakeUVTimer.Start(); + for (uint32 UVLayerIndex = 0; UVLayerIndex < NumUVLayers; ++UVLayerIndex) + { + const FVector2f &UV = LODResources.VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(UEVertexIndex, UVLayerIndex); + UVs[UVLayerIndex][Float3Index + 0] = UV.X; + UVs[UVLayerIndex][Float3Index + 1] = 1.0f - UV.Y; + UVs[UVLayerIndex][Float3Index + 2] = 0; + } + MakeUVTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // NORMALS (N) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceNormalsValid) + { + MakeNormalTimer.Start(); + const FVector4f &Normal = LODResources.VertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(UEVertexIndex); + Normals[Float3Index + 0] = Normal.X; + Normals[Float3Index + 1] = Normal.Z; + Normals[Float3Index + 2] = Normal.Y; + MakeNormalTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // TANGENT (tangentu) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceTangentsValid) + { + MakeTangentTimer.Start(); + const FVector4f &Tangent = LODResources.VertexBuffers.StaticMeshVertexBuffer.VertexTangentX(UEVertexIndex); + Tangents[Float3Index + 0] = Tangent.X; + Tangents[Float3Index + 1] = Tangent.Z; + Tangents[Float3Index + 2] = Tangent.Y; + MakeTangentTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // BINORMAL (tangentv) + //--------------------------------------------------------------------------------------------------------------------- + // In order to calculate the binormal we also need the tangent and normal + if (bIsVertexInstanceBinormalsValid) + { + MakeBinormalTimer.Start(); + FVector3f Binormal = LODResources.VertexBuffers.StaticMeshVertexBuffer.VertexTangentY(UEVertexIndex); + Binormals[Float3Index + 0] = Binormal.X; + Binormals[Float3Index + 1] = Binormal.Z; + Binormals[Float3Index + 2] = Binormal.Y; + MakeBinormalTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // COLORS (Cd) + //--------------------------------------------------------------------------------------------------------------------- + if (bUseComponentOverrideColors || bIsVertexInstanceColorsValid) + { + MakeColorsTimer.Start(); + FLinearColor Color = FLinearColor::White; + if (bUseComponentOverrideColors) + { + const FStaticMeshComponentLODInfo& ComponentLODInfo = StaticMeshComponent->LODData[InLODIndex]; + FColorVertexBuffer& ColorVertexBuffer = *ComponentLODInfo.OverrideVertexColors; + Color = ColorVertexBuffer.VertexColor(UEVertexIndex).ReinterpretAsLinear(); + } + else + { + Color = LODResources.VertexBuffers.ColorVertexBuffer.VertexColor(UEVertexIndex).ReinterpretAsLinear(); + } + RGBColors[Float3Index + 0] = Color.R; + RGBColors[Float3Index + 1] = Color.G; + RGBColors[Float3Index + 2] = Color.B; + Alphas[HoudiniVertexIdx] = Color.A; + MakeColorsTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // TRIANGLE/FACE VERTEX INDICES + //--------------------------------------------------------------------------------------------------------------------- + if (UEVertexInstanceIdxToPointIdx.IsValidIndex(UEVertexIndex)) + { + MeshTriangleVertexIndices[HoudiniVertexIdx] = UEVertexInstanceIdxToPointIdx[UEVertexIndex]; + } + + HoudiniVertexIdx++; + } + + //--------------------------------------------------------------------------------------------------------------------- + // TRIANGLE MATERIAL ASSIGNMENT + //--------------------------------------------------------------------------------------------------------------------- + if (MaterialInterfaces.IsValidIndex(Section.MaterialIndex)) + { + TriangleMaterialIndices.Add(Section.MaterialIndex); + } + else + { + TriangleMaterialIndices.Add(UEDefaultMaterialIndex); + HOUDINI_LOG_WARNING(TEXT("Section Index %d references an invalid Material Index %d, falling back to default material: %s"), SectionIndex, Section.MaterialIndex, *(UEDefaultMaterial->GetPathName())); + } + + TriangleIdx++; + } + } + + // Now transfer valid vertex instance attributes to Houdini vertex attributes + + //--------------------------------------------------------------------------------------------------------------------- + // UVS (uvX) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceUVsValid) + { + TransferUVTimer.Start(); + for (uint32 UVLayerIndex = 0; UVLayerIndex < NumUVLayers; UVLayerIndex++) + { + // Construct the attribute name for this UV index. + FString UVAttributeName = HAPI_UNREAL_ATTRIB_UV; + if (UVLayerIndex > 0) + UVAttributeName += FString::Printf(TEXT("%d"), UVLayerIndex + 1); + + // Create attribute for UVs + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.count = NumVertexInstances; + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, TCHAR_TO_ANSI(*UVAttributeName), &AttributeInfoVertex), false); + + Accessor.Init(NodeId, 0, TCHAR_TO_ANSI(*UVAttributeName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, UVs[UVLayerIndex]), false); + } + TransferUVTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // NORMALS (N) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceNormalsValid) + { + TransferNormalTimer.Start(); + // Create attribute for normals. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = Normals.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_NORMAL, &AttributeInfoVertex), false); + + Accessor.Init(NodeId, 0, HAPI_UNREAL_ATTRIB_NORMAL); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, Normals), false); + TransferNormalTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // TANGENT (tangentu) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceTangentsValid) + { + TransferTangentTimer.Start(); + // Create attribute for tangentu. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = Tangents.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_TANGENTU, &AttributeInfoVertex), false); + + Accessor.Init(NodeId, 0, HAPI_UNREAL_ATTRIB_TANGENTU); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, Tangents), false); + TransferTangentTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // BINORMAL (tangentv) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceBinormalsValid) + { + TransferBinormalTimer.Start(); + // Create attribute for normals. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = Binormals.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_TANGENTV, &AttributeInfoVertex), false); + + Accessor.Init(NodeId, 0, HAPI_UNREAL_ATTRIB_TANGENTV); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, Binormals), false); + TransferBinormalTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // COLORS (Cd) + //--------------------------------------------------------------------------------------------------------------------- + if (bUseComponentOverrideColors || bIsVertexInstanceColorsValid) + { + TransferColorsTimer.Start(); + // Create attribute for colors. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = RGBColors.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_COLOR, &AttributeInfoVertex), false); + + Accessor.Init(NodeId, 0, HAPI_UNREAL_ATTRIB_COLOR); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, RGBColors), false); + + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + AttributeInfoVertex.tupleSize = 1; + AttributeInfoVertex.count = Alphas.Num(); + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_ALPHA, &AttributeInfoVertex), false); + + Accessor.Init(NodeId, 0, HAPI_UNREAL_ATTRIB_ALPHA); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, Alphas), false); + TransferColorsTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // TRIANGLE/FACE VERTEX INDICES + //--------------------------------------------------------------------------------------------------------------------- + // We can now set vertex list. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetVertexList( + MeshTriangleVertexIndices, NodeId, 0), false); + + // Send the array of face vertex counts. + TArray StaticMeshFaceCounts; + StaticMeshFaceCounts.SetNumUninitialized(Part.faceCount); + for (int32 n = 0; n < Part.faceCount; n++) + StaticMeshFaceCounts[n] = 3; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetFaceCounts( + StaticMeshFaceCounts, NodeId, 0), false); + + // Send material assignments to Houdini + if (NumMaterials > 0) + { + // List of materials, one for each face. + FHoudiniEngineIndexedStringMap TriangleMaterials; + + //Lists of material parameters + TMap> ScalarMaterialParameters; + TMap> VectorMaterialParameters; + TMap TextureMaterialParameters; + TMap> BoolMaterialParameters; + + bool bAttributeSuccess = false; + FString PhysicalMaterialPath = GetSimplePhysicalMaterialPath(StaticMesh->GetBodySetup()); + + FHoudiniPerfTimer MaterialFaceArray(TEXT("MaterialFaceArray"), bDoTimings); + MaterialFaceArray.Start(); + + if (bInExportMaterialParametersAsAttributes) + { + // Create attributes for the material and all its parameters + FUnrealMeshTranslator::CreateFaceMaterialArray( + MaterialInterfaces, + TriangleMaterialIndices, + TriangleMaterials, + ScalarMaterialParameters, + VectorMaterialParameters, + TextureMaterialParameters, + BoolMaterialParameters); + } + else + { + // Create attributes only for the materials. Only get the material attribute data + FUnrealMeshTranslator::CreateFaceMaterialArray( + MaterialInterfaces, + TriangleMaterialIndices, + TriangleMaterials); + } + MaterialFaceArray.Stop(); + + FHoudiniPerfTimer MeshAttributes(TEXT("Mesh Attributes"), bDoTimings); + MeshAttributes.Start(); + + // Create all the needed attributes for materials + bAttributeSuccess = FUnrealMeshTranslator::CreateHoudiniMeshAttributes( + NodeId, + 0, + TriangleMaterials.GetIds().Num(), + TriangleMaterials, + TriangleMaterialIndices, + ScalarMaterialParameters, + VectorMaterialParameters, + TextureMaterialParameters, + BoolMaterialParameters, + PhysicalMaterialPath, +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + StaticMesh->GetNaniteSettings()); +#else + StaticMesh->NaniteSettings); +#endif + + MeshAttributes.Stop(); + + if (!bAttributeSuccess) + { + return false; + } + + } + + // TODO: The render mesh (LODResources) does not have face smoothing information, and the raw mesh triangle order is + // potentially different (see also line 4152 TODO_FBX in Engine\Source\Editor\UnrealEd\Private\Fbx\FbxMainExport.cpp + ////--------------------------------------------------------------------------------------------------------------------- + //// TRIANGLE SMOOTHING MASKS + ////--------------------------------------------------------------------------------------------------------------------- + //TArray TriangleSmoothingMasks; + //TriangleSmoothingMasks.SetNumZeroed(NumTriangles); + //FStaticMeshOperations::ConvertHardEdgesToSmoothGroup(MeshDescription, TriangleSmoothingMasks); + //if (TriangleSmoothingMasks.Num() > 0) + //{ + // HAPI_AttributeInfo AttributeInfoSmoothingMasks; + // FHoudiniApi::AttributeInfo_Init(&AttributeInfoSmoothingMasks); + + // AttributeInfoSmoothingMasks.tupleSize = 1; + // AttributeInfoSmoothingMasks.count = TriangleSmoothingMasks.Num(); + // AttributeInfoSmoothingMasks.exists = true; + // AttributeInfoSmoothingMasks.owner = HAPI_ATTROWNER_PRIM; + // AttributeInfoSmoothingMasks.storage = HAPI_STORAGETYPE_INT; + // AttributeInfoSmoothingMasks.originalOwner = HAPI_ATTROWNER_INVALID; + + // HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + // FHoudiniEngine::Get().GetSession(), + // NodeId, 0, HAPI_UNREAL_ATTRIB_FACE_SMOOTHING_MASK, &AttributeInfoSmoothingMasks), false); + + // HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeIntData( + // FHoudiniEngine::Get().GetSession(), + // NodeId, 0, HAPI_UNREAL_ATTRIB_FACE_SMOOTHING_MASK, &AttributeInfoSmoothingMasks, + // (const int32 *)TriangleSmoothingMasks.GetData(), 0, TriangleSmoothingMasks.Num()), false); + //} + } + + //--------------------------------------------------------------------------------------------------------------------- + // LIGHTMAP RESOLUTION + //--------------------------------------------------------------------------------------------------------------------- + { + int32 LightMapResolution = StaticMesh->GetLightMapResolution(); + + HAPI_AttributeInfo AttributeInfoLightMapResolution; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoLightMapResolution); + AttributeInfoLightMapResolution.count = 1; + AttributeInfoLightMapResolution.tupleSize = 1; + AttributeInfoLightMapResolution.exists = true; + AttributeInfoLightMapResolution.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoLightMapResolution.storage = HAPI_STORAGETYPE_INT; + AttributeInfoLightMapResolution.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_LIGHTMAP_RESOLUTION, &AttributeInfoLightMapResolution), false); + + Accessor.Init(NodeId, 0, HAPI_UNREAL_ATTRIB_LIGHTMAP_RESOLUTION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoLightMapResolution, LightMapResolution), false); + + } + + //--------------------------------------------------------------------------------------------------------------------- + // INPUT MESH NAME + //--------------------------------------------------------------------------------------------------------------------- + { + // Create primitive attribute with mesh asset path + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = Part.faceCount; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_INPUT_MESH_NAME, &AttributeInfo), false); + + Accessor.Init(NodeId, 0, HAPI_UNREAL_ATTRIB_INPUT_MESH_NAME); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfo, StaticMesh->GetPathName()), false); + } + + //--------------------------------------------------------------------------------------------------------------------- + // INPUT SOURCE FILE + //--------------------------------------------------------------------------------------------------------------------- + { + // Create primitive attribute with mesh asset path + FString Filename; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + if (UAssetImportData* ImportData = StaticMesh->GetAssetImportData()) +#else + if (UAssetImportData* ImportData = StaticMesh->AssetImportData) +#endif + { + for (const auto& SourceFile : ImportData->SourceData.SourceFiles) + { + Filename = UAssetImportData::ResolveImportFilename(SourceFile.RelativeFilename, ImportData->GetOutermost()); + break; + } + } + + if (!Filename.IsEmpty()) + { + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = Part.faceCount; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_INPUT_SOURCE_FILE, &AttributeInfo), false); + + + Accessor.Init(NodeId, 0, HAPI_UNREAL_ATTRIB_INPUT_SOURCE_FILE); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfo, Filename), false); + } + } + + //--------------------------------------------------------------------------------------------------------------------- + // LOD GROUP AND SCREENSIZE + //--------------------------------------------------------------------------------------------------------------------- + if (bAddLODGroups) + { + // LOD Group + + FString LODGroup = TEXT("lod") + FString::FromInt(InLODIndex); + + // Add a LOD group + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddGroup( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_GROUPTYPE_PRIM, H_TCHAR_TO_UTF8(*LODGroup)), false); + + // Set GroupMembership + TArray GroupArray; + GroupArray.SetNumUninitialized(Part.faceCount); + for (int32 n = 0; n < GroupArray.Num(); n++) + GroupArray[n] = 1; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetGroupMembership( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_GROUPTYPE_PRIM, H_TCHAR_TO_UTF8(*LODGroup), + GroupArray.GetData(), 0, Part.faceCount), false); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + if (!StaticMesh->GetAutoComputeLODScreenSize()) +#else + if (!StaticMesh->bAutoComputeLODScreenSize) +#endif + { + // Add the lodX_screensize attribute + FString LODAttributeName = + TEXT(HAPI_UNREAL_ATTRIB_LOD_SCREENSIZE_PREFIX) + FString::FromInt(InLODIndex) + TEXT(HAPI_UNREAL_ATTRIB_LOD_SCREENSIZE_POSTFIX); + + // Create lodX_screensize detail attribute info. + HAPI_AttributeInfo AttributeInfoLODScreenSize; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoLODScreenSize); + AttributeInfoLODScreenSize.count = 1; + AttributeInfoLODScreenSize.tupleSize = 1; + AttributeInfoLODScreenSize.exists = true; + AttributeInfoLODScreenSize.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoLODScreenSize.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoLODScreenSize.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, H_TCHAR_TO_UTF8(*LODAttributeName), &AttributeInfoLODScreenSize), false); + + float lodscreensize = SourceModel.ScreenSize.Default; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), NodeId, 0, + H_TCHAR_TO_UTF8(*LODAttributeName), &AttributeInfoLODScreenSize, + &lodscreensize, 0, 1), false); + } + } + + // Commit the geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(NodeId), false); + + return true; +} + + +FString +FUnrealMeshTranslator::GetSimplePhysicalMaterialPath(UBodySetup const* const BodySetup) +{ + if (IsValid(BodySetup) && IsValid(BodySetup->PhysMaterial)) + { + FString Path = BodySetup->PhysMaterial.GetPath(); + if (Path != "None") + return Path; + } + + return FString(); +} + +bool +FUnrealMeshTranslator::CreateInputNodeForMeshDescription( + const HAPI_NodeId& NodeId, + const FMeshDescription& MeshDescription, + const int32 InLODIndex, + const bool bAddLODGroups, + const bool bInExportMaterialParametersAsAttributes, + const UStaticMesh * StaticMesh, + const UStaticMeshComponent * StaticMeshComponent) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FUnrealMeshTranslator::CreateInputNodeForMeshDescription); + + if (!IsValid(StaticMesh)) + return false; + + // ---------------------------------------------------------------------------------------------------------------- + // Prepare the data we need for exporting the mesh via CreateAndPopulateMeshPartFromMeshDescription + // ---------------------------------------------------------------------------------------------------------------- + + // Get the physical material path + FString PhysicalMaterialPath = GetSimplePhysicalMaterialPath(StaticMesh->GetBodySetup()); + + // Grab the build scale + const FStaticMeshSourceModel &SourceModel = InLODIndex > 0 ? StaticMesh->GetSourceModel(InLODIndex) : StaticMesh->GetHiResSourceModel(); + const FVector3f BuildScaleVector = static_cast(SourceModel.BuildSettings.BuildScale3D); + + // Get the mesh attributes + FStaticMeshConstAttributes MeshConstAttributes(MeshDescription); + const int32 NumVertexInstances = MeshDescription.VertexInstances().Num(); + + FStaticMeshRenderData const* const SMRenderData = StaticMesh->GetRenderData(); + + // Determine if have override colors on the static mesh component, if so prefer to use those + bool bUseComponentOverrideColors = false; + if (StaticMeshComponent && + StaticMeshComponent->LODData.IsValidIndex(InLODIndex) && + StaticMeshComponent->LODData[InLODIndex].OverrideVertexColors && + SMRenderData && + SMRenderData->LODResources.IsValidIndex(InLODIndex)) + { + const FStaticMeshComponentLODInfo& ComponentLODInfo = StaticMeshComponent->LODData[InLODIndex]; + const FStaticMeshLODResources& RenderModel = SMRenderData->LODResources[InLODIndex]; + const FColorVertexBuffer& ColorVertexBuffer = *ComponentLODInfo.OverrideVertexColors; + + if (RenderModel.WedgeMap.Num() > 0 && ColorVertexBuffer.GetNumVertices() == RenderModel.GetNumVertices()) + { + // Use the wedge map if it is available as it is lossless. + if (RenderModel.WedgeMap.Num() == NumVertexInstances) + { + bUseComponentOverrideColors = true; + } + } + } + + // Build a material interface array (by material index) + const TArray& StaticMaterials = StaticMesh->GetStaticMaterials(); + TArray Materials; + Materials.Reserve(StaticMaterials.Num()); + for (const FStaticMaterial& StaticMaterial : StaticMaterials) + { + Materials.Add(StaticMaterial.MaterialInterface); + } + + const int32 NumSections = StaticMesh->GetNumSections(InLODIndex); + const FMeshSectionInfoMap& SectionInfoMap = StaticMesh->GetSectionInfoMap(); + TArray SectionMaterialIndices; + SectionMaterialIndices.Reserve(NumSections); + for (int32 SectionIndex = 0; SectionIndex < NumSections; ++SectionIndex) + { + SectionMaterialIndices.Add(SectionInfoMap.Get(InLODIndex, SectionIndex).MaterialIndex); + } + + TOptional LODScreenSize; +#if ENGINE_MAJOR_VERSION < 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0) + const bool bIsLODScreenSizeAutoComputed = StaticMesh->bAutoComputeLODScreenSize; +#else + const bool bIsLODScreenSizeAutoComputed = StaticMesh->IsLODScreenSizeAutoComputed(); +#endif + if (!bIsLODScreenSizeAutoComputed) + LODScreenSize = StaticMesh->GetSourceModel(InLODIndex).ScreenSize.Default; + + // ---------------------------------------------------------------------------------------------------------------- + // Export the mesh via CreateAndPopulateMeshPartFromMeshDescription + // ---------------------------------------------------------------------------------------------------------------- + + // If we are using override colors from the component, then don't export vertex colors in the + // CreateAndPopulateMeshPartFromMeshDescription function call and don't commit the geo: we'll add the override + // colors afterwards and then commit the geo. + const bool bExportVertexColors = !bUseComponentOverrideColors; + const bool bCommitGeo = !bUseComponentOverrideColors; + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + if (!CreateAndPopulateMeshPartFromMeshDescription( + NodeId, + MeshDescription, + MeshConstAttributes, + InLODIndex, + bAddLODGroups, + bInExportMaterialParametersAsAttributes, + StaticMesh, + StaticMeshComponent, + Materials, + SectionMaterialIndices, + BuildScaleVector, + PhysicalMaterialPath, + bExportVertexColors, + StaticMesh->GetLightMapResolution(), + LODScreenSize, +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + StaticMesh->GetNaniteSettings(), +#else + StaticMesh->NaniteSettings, +#endif + StaticMesh->GetAssetImportData(), + bCommitGeo, + PartInfo)) + { + return false; + } + + // ---------------------------------------------------------------------------------------------------------------- + // Handle any StaticMesh(Component) specific data that is not handled by CreateAndPopulateMeshPartFromMeshDescription + // ---------------------------------------------------------------------------------------------------------------- + + if (bUseComponentOverrideColors) + { + // RGBColors: 3 floats per vertex instance + TArray RGBColors; + // Alphas: 1 float per vertex instance + TArray Alphas; + + const FPolygonArray& MDPolygons = MeshDescription.Polygons(); + + RGBColors.SetNumUninitialized(NumVertexInstances * 3); + Alphas.SetNumUninitialized(NumVertexInstances); + + { + H_SCOPED_FUNCTION_STATIC_LABEL("Fetching Vertex Data - SM Specific"); + + int32 TriangleIdx = 0; + int32 VertexInstanceIdx = 0; + + for (const FPolygonID &PolygonID : MDPolygons.GetElementIDs()) + { + for (const FTriangleID& TriangleID : MeshDescription.GetPolygonTriangles(PolygonID)) + { + for (int32 TriangleVertexIndex = 0; TriangleVertexIndex < 3; ++TriangleVertexIndex) + { + // Reverse the winding order for Houdini (but still start at 0) + const int32 WindingIdx = (3 - TriangleVertexIndex) % 3; + const FVertexInstanceID &VertexInstanceID = MeshDescription.GetTriangleVertexInstance(TriangleID, WindingIdx); + + // Calculate the index of the first component of a vertex instance's value in an inline float array + // representing vectors (3 float) per vertex instance + const int32 Float3Index = VertexInstanceIdx * 3; + + //--------------------------------------------------------------------------------------------------------------------- + // COLORS (Cd) + //--------------------------------------------------------------------------------------------------------------------- + if (bUseComponentOverrideColors && SMRenderData) + { + FLinearColor Color = FLinearColor::White; + const FStaticMeshComponentLODInfo& ComponentLODInfo = StaticMeshComponent->LODData[InLODIndex]; + const FStaticMeshLODResources& RenderModel = SMRenderData->LODResources[InLODIndex]; + const FColorVertexBuffer& ColorVertexBuffer = *ComponentLODInfo.OverrideVertexColors; + + const int32 Index = RenderModel.WedgeMap[VertexInstanceID]; + if (Index != INDEX_NONE) + Color = ColorVertexBuffer.VertexColor(Index).ReinterpretAsLinear(); + + RGBColors[Float3Index + 0] = Color.R; + RGBColors[Float3Index + 1] = Color.G; + RGBColors[Float3Index + 2] = Color.B; + Alphas[VertexInstanceIdx] = Color.A; + } + + VertexInstanceIdx++; + } + + TriangleIdx++; + } + } + } + + { + H_SCOPED_FUNCTION_STATIC_LABEL("Transfering Data -- SM Specific"); + + //--------------------------------------------------------------------------------------------------------------------- + // COLORS (Cd) + //--------------------------------------------------------------------------------------------------------------------- + if (bUseComponentOverrideColors) + { + // Create attribute for colors. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = RGBColors.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_COLOR, &AttributeInfoVertex), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_COLOR); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, RGBColors), false); + + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + AttributeInfoVertex.tupleSize = 1; + AttributeInfoVertex.count = Alphas.Num(); + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_ALPHA, &AttributeInfoVertex), false); + + Accessor.Init(NodeId, 0, HAPI_UNREAL_ATTRIB_ALPHA); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, Alphas), false); + } + } + } + + // ---------------------------------------------------------------------------------------------------------------- + // Commit the geo + // ---------------------------------------------------------------------------------------------------------------- + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(NodeId), false); + + return true; +} + + +bool +FUnrealMeshTranslator::CreateAndPopulateMeshPartFromMeshDescription( + const HAPI_NodeId& NodeId, + const FMeshDescription& MeshDescription, + const FStaticMeshConstAttributes& MeshDescriptionAttributes, + const int32 InLODIndex, + const bool bAddLODGroups, + const bool bInExportMaterialParametersAsAttributes, + const UObject * Mesh, + const UMeshComponent * MeshComponent, + const TArray& MeshMaterials, + const TArray& SectionMaterialIndices, + const FVector3f& BuildScaleVector, + const FString& PhysicalMaterialPath, + const bool bExportVertexColors, + const TOptional LightMapResolution, + const TOptional LODScreenSize, + const TOptional NaniteSettings, + const UAssetImportData * ImportData, + const bool bCommitGeo, + HAPI_PartInfo& OutPartInfo) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FUnrealMeshTranslator::CreateAndPopulateMeshPartFromMeshDescription); + + H_SCOPED_FUNCTION_TIMER(); + + AActor* ParentActor = MeshComponent ? MeshComponent->GetOwner() : nullptr; + + // Convert the Mesh using FMeshDescription + // Get references to the attributes we are interested in + // before sending to Houdini we'll check if each attribute is valid + + TVertexAttributesConstRef VertexPositions = MeshDescriptionAttributes.GetVertexPositions(); + TVertexInstanceAttributesConstRef VertexInstanceNormals = MeshDescriptionAttributes.GetVertexInstanceNormals(); + TVertexInstanceAttributesConstRef VertexInstanceTangents = MeshDescriptionAttributes.GetVertexInstanceTangents(); + TVertexInstanceAttributesConstRef VertexInstanceBinormalSigns = MeshDescriptionAttributes.GetVertexInstanceBinormalSigns(); + TVertexInstanceAttributesConstRef VertexInstanceColors = MeshDescriptionAttributes.GetVertexInstanceColors(); + TVertexInstanceAttributesConstRef VertexInstanceUVs = MeshDescriptionAttributes.GetVertexInstanceUVs(); + //TPolygonGroupAttributesConstRef PolygonGroupMaterialSlotNames = MeshDescriptionAttributes.GetPolygonGroupMaterialSlotNames(); + + // Get the vertex and triangle arrays + const FVertexArray &MDVertices = MeshDescription.Vertices(); + const FPolygonGroupArray &MDPolygonGroups = MeshDescription.PolygonGroups(); + const FPolygonArray &MDPolygons = MeshDescription.Polygons(); + const FTriangleArray &MDTriangles = MeshDescription.Triangles(); + + // Determine point, vertex and polygon counts + const uint32 NumVertices = MDVertices.Num(); + const uint32 NumTriangles = MDTriangles.Num(); + const uint32 NumVertexInstances = NumTriangles * 3; + + // Some checks: we expect triangulated meshes + if (MeshDescription.VertexInstances().Num() != NumTriangles * 3) + { + HOUDINI_LOG_ERROR(TEXT("Expected a triangulated mesh, but # VertexInstances (%d) != # Triangles * 3 (%d)"), MeshDescription.VertexInstances().Num(), NumTriangles * 3); + return false; + } + + // Determine which attributes we have + const bool bIsVertexPositionsValid = VertexPositions.IsValid(); + const bool bIsVertexInstanceNormalsValid = VertexInstanceNormals.IsValid(); + const bool bIsVertexInstanceTangentsValid = VertexInstanceTangents.IsValid(); + const bool bIsVertexInstanceBinormalSignsValid = VertexInstanceBinormalSigns.IsValid(); + const bool bIsVertexInstanceColorsValid = VertexInstanceColors.IsValid(); + const bool bIsVertexInstanceUVsValid = VertexInstanceUVs.IsValid(); + //const bool bIsPolygonGroupImportedMaterialSlotNamesValid = PolygonGroupMaterialSlotNames.IsValid(); + + // Create part. + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + Part.vertexCount = NumVertexInstances; + Part.faceCount = NumTriangles; + Part.pointCount = NumVertices; + Part.type = HAPI_PARTTYPE_MESH; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo(FHoudiniEngine::Get().GetSession(), NodeId, 0, &Part), false); + + // Create point attribute info. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + //FMemory::Memzero< HAPI_AttributeInfo >( AttributeInfoPoint ); + AttributeInfoPoint.count = Part.pointCount; + AttributeInfoPoint.tupleSize = 3; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + bool bDoTimings = CVarHoudiniEngineMeshBuildTimer.GetValueOnAnyThread() != 0.0; + + FHoudiniPerfTimer PositionsTimer(TEXT("Positions"), bDoTimings); + PositionsTimer.Start(); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NodeId, 0, + HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint), false); + + //--------------------------------------------------------------------------------------------------------------------- + // POSITION (P) + //--------------------------------------------------------------------------------------------------------------------- + // The mesh element arrays are sparse: the max index/ID value can be larger than the number of elements - 1 + // so we have to maintain a lookup of VertexID (UE) to PointIndex (Houdini) + TArray VertexIDToHIndex; + if (bIsVertexPositionsValid && VertexPositions.GetNumElements() >= 3) + { + TArray StaticMeshVertices; + StaticMeshVertices.SetNumUninitialized(NumVertices * 3); + + VertexIDToHIndex.SetNumUninitialized(MDVertices.GetArraySize()); + for (int32 n = 0; n < VertexIDToHIndex.Num(); n++) + VertexIDToHIndex[n] = INDEX_NONE; + + int32 VertexIdx = 0; + for (const FVertexID& VertexID : MDVertices.GetElementIDs()) + { + // Convert Unreal to Houdini + const FVector3f& PositionVector = VertexPositions.Get(VertexID); + StaticMeshVertices[VertexIdx * 3 + 0] = PositionVector.X / HAPI_UNREAL_SCALE_FACTOR_POSITION * BuildScaleVector.X; + StaticMeshVertices[VertexIdx * 3 + 1] = PositionVector.Z / HAPI_UNREAL_SCALE_FACTOR_POSITION * BuildScaleVector.Z; + StaticMeshVertices[VertexIdx * 3 + 2] = PositionVector.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION * BuildScaleVector.Y; + + // Record the UE Vertex ID to Houdini Point Index lookup + VertexIDToHIndex[VertexID.GetValue()] = VertexIdx; + VertexIdx++; + } + + // Now that we have raw positions, we can upload them for our attribute. + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPoint, StaticMeshVertices), false); + } + + PositionsTimer.Stop(); + + //--------------------------------------------------------------------------------------------------------------------- + // MATERIAL SLOT -> MATERIAL INTERFACE + //--------------------------------------------------------------------------------------------------------------------- + // In theory the ImportedMaterialSlotName attribute on PolygonGroups should tell us which material slot is used by + // that group, and thus which MaterialIndex we should assign to triangles in that group. Unfortunately we have + // encountered cases where the ImportedMaterialSlotName name attribute does not match any of the MaterialSlotName or + // ImportedMaterialSlotNames in the StaticMesh->StaticMaterials array. Therefore we have no choice but to rely + // on the PolygonGroup order vs Section order to determine the MaterialIndex for a group. We do what Epic does + // when building a static mesh: Sections are created in the same order as iterating over PolygonGroups, but importantly, + // empty PolygonGroups are skipped + + // // Get material slot name to material index + // and the UMaterialInterface array + // TMap MaterialSlotToInterface; + TArray MaterialInterfaces; + TArray TriangleMaterialIndices; + + // If the mesh component is valid, and we are not using the ref counted input system, get the materials via + // the component to account for overrides. For the ref counted input system the component will override the + // materials in its input node. + const bool bIsMeshComponentValid = (IsValid(MeshComponent) && MeshComponent->IsValidLowLevel()); + const int32 NumStaticMaterials = MeshMaterials.Num(); + // If we find any invalid Material (null or pending kill), or we find a section below with an out of range MaterialIndex, + // then we will set UEDefaultMaterial at the invalid index + int32 UEDefaultMaterialIndex = INDEX_NONE; + UMaterialInterface *UEDefaultMaterial = nullptr; + if (NumStaticMaterials > 0) + { + MaterialInterfaces.Reserve(NumStaticMaterials); + for (int32 MaterialIndex = 0; MaterialIndex < NumStaticMaterials; ++MaterialIndex) + { + const FStaticMaterial &MaterialInfo = MeshMaterials[MaterialIndex]; + UMaterialInterface *Material = nullptr; + { + Material = MaterialInfo.MaterialInterface; + } + // If the Material is NULL or invalid, fallback to the default material + if (!IsValid(Material)) + { + if (!UEDefaultMaterial) + { + UEDefaultMaterial = UMaterial::GetDefaultMaterial(EMaterialDomain::MD_Surface); + UEDefaultMaterialIndex = MaterialIndex; + } + Material = UEDefaultMaterial; + HOUDINI_LOG_WARNING(TEXT("Material Index %d (slot %s) has an invalid material, falling back to default: %s"), MaterialIndex, *(MaterialInfo.MaterialSlotName.ToString()), *(UEDefaultMaterial->GetPathName())); + } + // MaterialSlotToInterface.Add(MaterialInfo.ImportedMaterialSlotName, MaterialIndex); + MaterialInterfaces.Add(Material); + } + + TriangleMaterialIndices.Reserve(NumTriangles); + } + // SectionIndex: Looking at Epic's StaticMesh build code, Sections are created in the same + // order as iterating over PolygonGroups, but skipping empty PolygonGroups + TMap PolygonGroupToMaterialIndex; + PolygonGroupToMaterialIndex.Reserve(MeshDescription.PolygonGroups().Num()); + int32 SectionIndex = 0; + for (const FPolygonGroupID &PolygonGroupID : MDPolygonGroups.GetElementIDs()) + { + // Skip empty polygon groups + if (MeshDescription.GetNumPolygonGroupPolygons(PolygonGroupID) == 0) + { + continue; + } + + //--------------------------------------------------------------------------------------------------------------------- + // TRIANGLE MATERIAL ASSIGNMENT + //--------------------------------------------------------------------------------------------------------------------- + // // Get the material index for the material slot for this polygon group + //int32 MaterialIndex = INDEX_NONE; + //if (bIsPolygonGroupImportedMaterialSlotNamesValid) + //{ + // const FName &MaterialSlotName = PolygonGroupMaterialSlotNames.Get(PolygonGroupID); + // const int32 *MaterialIndexPtr = MaterialSlotToInterface.Find(MaterialSlotName); + // if (MaterialIndexPtr != nullptr) + // { + // MaterialIndex = *MaterialIndexPtr; + // } + //} + + // Get the material for the LOD and section via the section info map + if (!SectionMaterialIndices.IsValidIndex(SectionIndex)) + { + HOUDINI_LOG_ERROR(TEXT("Found more non-empty polygon groups in the mesh description for LOD %d than sections in the mesh..."), InLODIndex); + return false; + } + + // If the MaterialIndex referenced by this Section is out of range, fill MaterialInterfaces with UEDefaultMaterial + // up to and including MaterialIndex and log a warning + int32 MaterialIndex = SectionMaterialIndices[SectionIndex]; + if (!MaterialInterfaces.IsValidIndex(MaterialIndex)) + { + if (!UEDefaultMaterial) + { + UEDefaultMaterial = UMaterial::GetDefaultMaterial(EMaterialDomain::MD_Surface); + // Add the UEDefaultMaterial to MaterialInterfaces + UEDefaultMaterialIndex = MaterialInterfaces.Add(UEDefaultMaterial); + } + HOUDINI_LOG_WARNING(TEXT("Section Index %d references an invalid Material Index %d, falling back to default material: %s"), SectionIndex, MaterialIndex, *(UEDefaultMaterial->GetPathName())); + MaterialIndex = UEDefaultMaterialIndex; + } + + PolygonGroupToMaterialIndex.Add(PolygonGroupID, MaterialIndex); + SectionIndex++; + } + + // Determine the final number of materials we have, with defaults for missing/invalid indices + const int32 NumMaterials = MaterialInterfaces.Num(); + + // Now we deal with vertex instance attributes. + // // First we must also record a UE VertexInstanceID to Houdini Vertex Index lookup, + // // and then get and convert all valid and supported vertex instance attributes from UE + // TArray VertexInstanceIDToHIndex; + + FHoudiniPerfTimer MakeUVTimer(TEXT("UV Make"), bDoTimings); + FHoudiniPerfTimer MakeNormalTimer(TEXT("Normals Make"), bDoTimings); + FHoudiniPerfTimer MakeTangentTimer(TEXT("Tangents Make"), bDoTimings); + FHoudiniPerfTimer MakeBinormalTimer(TEXT("Binormals Make"), bDoTimings); + FHoudiniPerfTimer MakeColorsTimer(TEXT("Colors Make"), bDoTimings); + FHoudiniPerfTimer MakeAlphasTimer(TEXT("Alphas Make"), bDoTimings); + + FHoudiniPerfTimer TransferUVTimer(TEXT("UV Transfer"), bDoTimings); + FHoudiniPerfTimer TransferNormalTimer(TEXT("Normals Transfer"), bDoTimings); + FHoudiniPerfTimer TransferTangentTimer(TEXT("Tangents Transfer"), bDoTimings); + FHoudiniPerfTimer TransferBinormalTimer(TEXT("Binormals Transfer"), bDoTimings); + FHoudiniPerfTimer TransferColorsTimer(TEXT("Colors Transfer"), bDoTimings); + FHoudiniPerfTimer TransferAlphasTimer(TEXT("Alphas Transfer"), bDoTimings); + + if (NumTriangles > 0) + { + // UV layer array. Each layer has an array of floats, 3 floats per vertex instance + TArray> UVs; + const int32 NumUVLayers = bIsVertexInstanceUVsValid ? FMath::Min(VertexInstanceUVs.GetNumChannels(), (int32)MAX_STATIC_TEXCOORDS) : 0; + // Normals: 3 floats per vertex instance + TArray Normals; + // Tangents: 3 floats per vertex instance + TArray Tangents; + // Binormals: 3 floats per vertex instance + TArray Binormals; + // RGBColors: 3 floats per vertex instance + TArray RGBColors; + // Alphas: 1 float per vertex instance + TArray Alphas; + + // Initialize the arrays for the attributes that are valid + if (bIsVertexInstanceUVsValid) + { + UVs.SetNum(NumUVLayers); + for (int32 UVLayerIndex = 0; UVLayerIndex < NumUVLayers; ++UVLayerIndex) + { + UVs[UVLayerIndex].SetNumUninitialized(NumVertexInstances * 3); + } + } + + if (bIsVertexInstanceNormalsValid) + { + Normals.SetNumUninitialized(NumVertexInstances * 3); + } + + if (bIsVertexInstanceTangentsValid) + { + Tangents.SetNumUninitialized(NumVertexInstances * 3); + } + + if (bIsVertexInstanceBinormalSignsValid) + { + Binormals.SetNumUninitialized(NumVertexInstances * 3); + } + + if (bExportVertexColors && bIsVertexInstanceColorsValid) + { + RGBColors.SetNumUninitialized(NumVertexInstances * 3); + Alphas.SetNumUninitialized(NumVertexInstances); + } + + // Array of material index per triangle/face + TArray MeshTriangleVertexIndices; + MeshTriangleVertexIndices.SetNumUninitialized(NumVertexInstances); + // Array of vertex counts per triangle/face + TArray MeshTriangleVertexCounts; + MeshTriangleVertexCounts.SetNumUninitialized(NumTriangles); + + int32 TriangleIdx = 0; + int32 VertexInstanceIdx = 0; + { + H_SCOPED_FUNCTION_STATIC_LABEL("Fetching Vertex Data"); + for (const FPolygonID &PolygonID : MDPolygons.GetElementIDs()) + { + for (const FTriangleID& TriangleID : MeshDescription.GetPolygonTriangles(PolygonID)) + { + + MeshTriangleVertexCounts[TriangleIdx] = 3; + for (int32 TriangleVertexIndex = 0; TriangleVertexIndex < 3; ++TriangleVertexIndex) + { + // Reverse the winding order for Houdini (but still start at 0) + const int32 WindingIdx = (3 - TriangleVertexIndex) % 3; + const FVertexInstanceID &VertexInstanceID = MeshDescription.GetTriangleVertexInstance(TriangleID, WindingIdx); + + // // UE Vertex Instance ID to Houdini Vertex Index look up + // VertexInstanceIDToHIndex[VertexInstanceID.GetValue()] = VertexInstanceIdx; + + // Calculate the index of the first component of a vertex instance's value in an inline float array + // representing vectors (3 float) per vertex instance + const int32 Float3Index = VertexInstanceIdx * 3; + + //--------------------------------------------------------------------------------------------------------------------- + // UVS (uvX) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceUVsValid) + { + MakeUVTimer.Start(); + for (int32 UVLayerIndex = 0; UVLayerIndex < NumUVLayers; ++UVLayerIndex) + { + const FVector2f &UV = VertexInstanceUVs.Get(VertexInstanceID, UVLayerIndex); + UVs[UVLayerIndex][Float3Index + 0] = UV.X; + UVs[UVLayerIndex][Float3Index + 1] = 1.0f - UV.Y; + UVs[UVLayerIndex][Float3Index + 2] = 0; + } + MakeUVTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // NORMALS (N) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceNormalsValid) + { + MakeNormalTimer.Start(); + const FVector3f &Normal = VertexInstanceNormals.Get(VertexInstanceID); + Normals[Float3Index + 0] = Normal.X; + Normals[Float3Index + 1] = Normal.Z; + Normals[Float3Index + 2] = Normal.Y; + MakeNormalTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // TANGENT (tangentu) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceTangentsValid) + { + MakeTangentTimer.Start(); + const FVector3f &Tangent = VertexInstanceTangents.Get(VertexInstanceID); + Tangents[Float3Index + 0] = Tangent.X; + Tangents[Float3Index + 1] = Tangent.Z; + Tangents[Float3Index + 2] = Tangent.Y; + MakeTangentTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // BINORMAL (tangentv) + //--------------------------------------------------------------------------------------------------------------------- + // In order to calculate the binormal we also need the tangent and normal + if (bIsVertexInstanceBinormalSignsValid && bIsVertexInstanceTangentsValid && bIsVertexInstanceNormalsValid) + { + MakeBinormalTimer.Start(); + const float &BinormalSign = VertexInstanceBinormalSigns.Get(VertexInstanceID); + FVector Binormal = FVector::CrossProduct( + FVector(Tangents[Float3Index + 0], Tangents[Float3Index + 1], Tangents[Float3Index + 2]), + FVector(Normals[Float3Index + 0], Normals[Float3Index + 1], Normals[Float3Index + 2]) + ) * BinormalSign; + Binormals[Float3Index + 0] = (float)Binormal.X; + Binormals[Float3Index + 1] = (float)Binormal.Y; + Binormals[Float3Index + 2] = (float)Binormal.Z; + MakeBinormalTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // COLORS (Cd) + //--------------------------------------------------------------------------------------------------------------------- + if (bExportVertexColors && bIsVertexInstanceColorsValid) + { + MakeColorsTimer.Start(); + FLinearColor Color = FLinearColor::White; + // Convert from SRGB to Linear. Unfortunately UE only provides this via the FColor() + // structure, so we loose precision as we have to convert to 8-bit and back to 32-bit. + FLinearColor SRGBColor = VertexInstanceColors.Get(VertexInstanceID); + Color = SRGBColor.ToFColor(true).ReinterpretAsLinear(); + RGBColors[Float3Index + 0] = Color.R; + RGBColors[Float3Index + 1] = Color.G; + RGBColors[Float3Index + 2] = Color.B; + Alphas[VertexInstanceIdx] = Color.A; + MakeColorsTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // TRIANGLE/FACE VERTEX INDICES + //--------------------------------------------------------------------------------------------------------------------- + + const FVertexID& VertexID = MeshDescription.GetVertexInstanceVertex(VertexInstanceID); + const int32 UEVertexIdx = VertexID.GetValue(); + if (VertexIDToHIndex.IsValidIndex(UEVertexIdx)) + { + MeshTriangleVertexIndices[VertexInstanceIdx] = VertexIDToHIndex[UEVertexIdx]; + } + + VertexInstanceIdx++; + } + + //--------------------------------------------------------------------------------------------------------------------- + // TRIANGLE MATERIAL ASSIGNMENT + //--------------------------------------------------------------------------------------------------------------------- + const FPolygonGroupID& PolygonGroupID = MeshDescription.GetPolygonPolygonGroup(PolygonID); + int32 MaterialIndex = PolygonGroupToMaterialIndex.FindChecked(PolygonGroupID); + TriangleMaterialIndices.Add(MaterialIndex); + + TriangleIdx++; + } + + } + } + // Now transfer valid vertex instance attributes to Houdini vertex attributes + + { + H_SCOPED_FUNCTION_STATIC_LABEL("Transfering Data"); + //--------------------------------------------------------------------------------------------------------------------- + // UVS (uvX) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceUVsValid) + { + TransferUVTimer.Start(); + for (int32 UVLayerIndex = 0; UVLayerIndex < NumUVLayers; UVLayerIndex++) + { + // Construct the attribute name for this UV index. + FString UVAttributeName = HAPI_UNREAL_ATTRIB_UV; + if (UVLayerIndex > 0) + UVAttributeName += FString::Printf(TEXT("%d"), UVLayerIndex + 1); + + // Create attribute for UVs + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.count = NumVertexInstances; + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, TCHAR_TO_ANSI(*UVAttributeName), &AttributeInfoVertex), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, TCHAR_TO_ANSI(*UVAttributeName)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, UVs[UVLayerIndex]), false); + } + TransferUVTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // NORMALS (N) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceNormalsValid) + { + TransferNormalTimer.Start(); + // Create attribute for normals. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = Normals.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_NORMAL, &AttributeInfoVertex), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_NORMAL); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, Normals), false); + + TransferNormalTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // TANGENT (tangentu) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceTangentsValid) + { + TransferTangentTimer.Start(); + // Create attribute for tangentu. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = Tangents.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_TANGENTU, &AttributeInfoVertex), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_TANGENTU); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, Tangents), false); + + TransferTangentTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // BINORMAL (tangentv) + //--------------------------------------------------------------------------------------------------------------------- + if (bIsVertexInstanceBinormalSignsValid) + { + TransferBinormalTimer.Start(); + // Create attribute for normals. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = Binormals.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_TANGENTV, &AttributeInfoVertex), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_TANGENTV); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, Binormals), false); + + TransferBinormalTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // COLORS (Cd) + //--------------------------------------------------------------------------------------------------------------------- + if (bExportVertexColors && bIsVertexInstanceColorsValid) + { + TransferColorsTimer.Start(); + // Create attribute for colors. + HAPI_AttributeInfo AttributeInfoVertex; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + + AttributeInfoVertex.tupleSize = 3; + AttributeInfoVertex.count = RGBColors.Num() / AttributeInfoVertex.tupleSize; + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_COLOR, &AttributeInfoVertex), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_COLOR); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, RGBColors), false); + + FHoudiniApi::AttributeInfo_Init(&AttributeInfoVertex); + AttributeInfoVertex.tupleSize = 1; + AttributeInfoVertex.count = Alphas.Num(); + AttributeInfoVertex.exists = true; + AttributeInfoVertex.owner = HAPI_ATTROWNER_VERTEX; + AttributeInfoVertex.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoVertex.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_ALPHA, &AttributeInfoVertex), false); + + Accessor.Init(NodeId, 0, HAPI_UNREAL_ATTRIB_ALPHA); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoVertex, Alphas), false); + + TransferColorsTimer.Stop(); + } + + //--------------------------------------------------------------------------------------------------------------------- + // TRIANGLE/FACE VERTEX INDICES + //--------------------------------------------------------------------------------------------------------------------- + // We can now set vertex list. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetVertexList( + MeshTriangleVertexIndices, NodeId, 0), false); + + // Send the array of face vertex counts. + TArray StaticMeshFaceCounts; + StaticMeshFaceCounts.SetNumUninitialized(Part.faceCount); + for (int32 n = 0; n < Part.faceCount; n++) + StaticMeshFaceCounts[n] = 3; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetFaceCounts( + StaticMeshFaceCounts, NodeId, 0), false); + + // Send material assignments to Houdini + if (NumMaterials > 0) + { + // List of materials, one for each face. + FHoudiniEngineIndexedStringMap TriangleMaterials; + + //Lists of material parameters + TMap> ScalarMaterialParameters; + TMap> VectorMaterialParameters; + TMap TextureMaterialParameters; + TMap>BoolMaterialParameters; + + FHoudiniPerfTimer TransferFaceArray(TEXT("Face Material Array"), bDoTimings); + TransferFaceArray.Start(); + + bool bAttributeSuccess = false; + if (bInExportMaterialParametersAsAttributes) + { + // Create attributes for the material and all its parameters + // Get material attribute data, and all material parameters data + FUnrealMeshTranslator::CreateFaceMaterialArray( + MaterialInterfaces, + TriangleMaterialIndices, + TriangleMaterials, + ScalarMaterialParameters, + VectorMaterialParameters, + TextureMaterialParameters, + BoolMaterialParameters); + } + else + { + // Create attributes only for the materials + // Only get the material attribute data + FUnrealMeshTranslator::CreateFaceMaterialArray( + MaterialInterfaces, + TriangleMaterialIndices, + TriangleMaterials); + } + TransferFaceArray.Stop(); + + FHoudiniPerfTimer TransferMeshAttributes(TEXT("Mesh Attributes"), bDoTimings); + TransferMeshAttributes.Start(); + + // Create all the needed attributes for materials + bAttributeSuccess = FUnrealMeshTranslator::CreateHoudiniMeshAttributes( + NodeId, + 0, + TriangleMaterials.GetIds().Num(), + TriangleMaterials, + TriangleMaterialIndices, + ScalarMaterialParameters, + VectorMaterialParameters, + TextureMaterialParameters, + BoolMaterialParameters, + PhysicalMaterialPath, + NaniteSettings); + + TransferMeshAttributes.Stop(); + if (!bAttributeSuccess) + { + HOUDINI_LOG_ERROR(TEXT("Failed to Create Mesh Attributes.")); + return false; + } + } + + //--------------------------------------------------------------------------------------------------------------------- + // TRIANGLE SMOOTHING MASKS + //--------------------------------------------------------------------------------------------------------------------- + TArray TriangleSmoothingMasks; + TriangleSmoothingMasks.SetNumZeroed(NumTriangles); + { + // Convert uint32 smoothing mask to int + TArray UnsignedSmoothingMasks; + UnsignedSmoothingMasks.SetNumZeroed(NumTriangles); + FStaticMeshOperations::ConvertHardEdgesToSmoothGroup(MeshDescription, UnsignedSmoothingMasks); + for (int32 n = 0; n < TriangleSmoothingMasks.Num(); n++) + TriangleSmoothingMasks[n] = (int32)UnsignedSmoothingMasks[n]; + } + + if (TriangleSmoothingMasks.Num() > 0) + { + HAPI_AttributeInfo AttributeInfoSmoothingMasks; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoSmoothingMasks); + + AttributeInfoSmoothingMasks.tupleSize = 1; + AttributeInfoSmoothingMasks.count = TriangleSmoothingMasks.Num(); + AttributeInfoSmoothingMasks.exists = true; + AttributeInfoSmoothingMasks.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoSmoothingMasks.storage = HAPI_STORAGETYPE_INT; + AttributeInfoSmoothingMasks.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_FACE_SMOOTHING_MASK, &AttributeInfoSmoothingMasks), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_FACE_SMOOTHING_MASK); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoSmoothingMasks, TriangleSmoothingMasks), false); + + } + } + } + + //--------------------------------------------------------------------------------------------------------------------- + // LIGHTMAP RESOLUTION + //--------------------------------------------------------------------------------------------------------------------- + if (LightMapResolution.IsSet()) + { + HAPI_AttributeInfo AttributeInfoLightMapResolution; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoLightMapResolution); + AttributeInfoLightMapResolution.count = 1; + AttributeInfoLightMapResolution.tupleSize = 1; + AttributeInfoLightMapResolution.exists = true; + AttributeInfoLightMapResolution.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoLightMapResolution.storage = HAPI_STORAGETYPE_INT; + AttributeInfoLightMapResolution.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_LIGHTMAP_RESOLUTION, &AttributeInfoLightMapResolution), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_LIGHTMAP_RESOLUTION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoLightMapResolution, LightMapResolution.GetValue()), false); + } + + //--------------------------------------------------------------------------------------------------------------------- + // INPUT MESH NAME + //--------------------------------------------------------------------------------------------------------------------- + { + H_SCOPED_FUNCTION_STATIC_LABEL(HAPI_UNREAL_ATTRIB_INPUT_MESH_NAME); + + // Create primitive attribute with mesh asset path + const FString MeshAssetPath = Mesh->GetPathName(); + + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = Part.faceCount; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_INPUT_MESH_NAME, &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_INPUT_MESH_NAME); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfo, MeshAssetPath), false); + } + + //--------------------------------------------------------------------------------------------------------------------- + // INPUT SOURCE FILE + //--------------------------------------------------------------------------------------------------------------------- + { + H_SCOPED_FUNCTION_STATIC_LABEL(HAPI_UNREAL_ATTRIB_INPUT_SOURCE_FILE); + + // Create primitive attribute with mesh asset path + FString Filename; + if (IsValid(ImportData)) + { + for (const auto& SourceFile : ImportData->SourceData.SourceFiles) + { + Filename = UAssetImportData::ResolveImportFilename(SourceFile.RelativeFilename, ImportData->GetOutermost()); + break; + } + } + + if (!Filename.IsEmpty()) + { + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = Part.faceCount; + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_UNREAL_ATTRIB_INPUT_SOURCE_FILE, &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(NodeId, 0, HAPI_UNREAL_ATTRIB_INPUT_SOURCE_FILE); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfo, Filename), false); + } + } + + /* + // Check if we have vertex attribute data to add + if (StaticMeshComponent && StaticMeshComponent->GetOwner()) + { + if (UHoudiniAttributeDataComponent* DataComponent = StaticMeshComponent->GetOwner()->FindComponentByClass()) + { + bool bSuccess = DataComponent->Upload(NodeId, StaticMeshComponent); + if (!bSuccess) + { + HOUDINI_LOG_ERROR(TEXT("Upload of attribute data for %s failed"), *StaticMeshComponent->GetOwner()->GetName()); + } + } + } + */ + + //--------------------------------------------------------------------------------------------------------------------- + // LOD GROUP AND SCREEN SIZE + //--------------------------------------------------------------------------------------------------------------------- + if (bAddLODGroups) + { + H_SCOPED_FUNCTION_STATIC_LABEL("LOD GROUP AND SCREEN SIZE"); + + // LOD Group + + FString LODGroup = TEXT("lod") + FString::FromInt(InLODIndex); + + // Add a LOD group + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddGroup( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_GROUPTYPE_PRIM, H_TCHAR_TO_UTF8(*LODGroup)), false); + + // Set GroupMembership + TArray GroupArray; + GroupArray.SetNumUninitialized(Part.faceCount); + for (int32 n = 0; n < GroupArray.Num(); n++) + GroupArray[n] = 1; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetGroupMembership( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, HAPI_GROUPTYPE_PRIM, H_TCHAR_TO_UTF8(*LODGroup), + GroupArray.GetData(), 0, Part.faceCount), false); + + if (LODScreenSize.IsSet()) + { + // Add the lodX_screensize attribute + FString LODAttributeName = + TEXT(HAPI_UNREAL_ATTRIB_LOD_SCREENSIZE_PREFIX) + FString::FromInt(InLODIndex) + TEXT(HAPI_UNREAL_ATTRIB_LOD_SCREENSIZE_POSTFIX); + + // Create lodX_screensize detail attribute info. + HAPI_AttributeInfo AttributeInfoLODScreenSize; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoLODScreenSize); + AttributeInfoLODScreenSize.count = 1; + AttributeInfoLODScreenSize.tupleSize = 1; + AttributeInfoLODScreenSize.exists = true; + AttributeInfoLODScreenSize.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoLODScreenSize.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoLODScreenSize.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, 0, H_TCHAR_TO_UTF8(*LODAttributeName), &AttributeInfoLODScreenSize), false); + + const float LODScreenSizeValue = LODScreenSize.GetValue(); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), NodeId, 0, + H_TCHAR_TO_UTF8(*LODAttributeName), &AttributeInfoLODScreenSize, + &LODScreenSizeValue, 0, 1), false); + } + } + + if (bCommitGeo) + { + // Commit the geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(NodeId), false); + } + + OutPartInfo = Part; + + return true; +} + +void +FUnrealMeshTranslator::CreateFaceMaterialArray( + const TArray& Materials, + const TArray& FaceMaterialIndices, + FHoudiniEngineIndexedStringMap& OutStaticMeshFaceMaterials) +{ + // Get the default material + UMaterialInterface * DefaultMaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial().Get()); + FString DefaultMaterialName = DefaultMaterialInterface ? DefaultMaterialInterface->GetPathName() : TEXT("default"); + + OutStaticMeshFaceMaterials.Reset(Materials.Num(), FaceMaterialIndices.Num()); + + // We need to create list of unique materials. + TArray PerSlotMaterialList; + + UMaterialInterface* MaterialInterface = nullptr; + if (Materials.Num()) + { + // We have materials. + for (int32 MaterialIdx = 0; MaterialIdx < Materials.Num(); MaterialIdx++) + { + FString ParamPrefix = Materials.Num() == 1 ? "" : FString::FromInt(MaterialIdx) + FString("_"); + MaterialInterface = Materials[MaterialIdx]; + if (!MaterialInterface) + { + // Null material interface found, add default instead. + PerSlotMaterialList.Add(DefaultMaterialName); + } + else + { + // We found a material, get its name + PerSlotMaterialList.Add(MaterialInterface->GetPathName()); + } + } + } + else + { + // We do not have any materials, just add default. + PerSlotMaterialList.Add(DefaultMaterialName); + } + + // Add the Material slot index in brackets if we have more than one material + if (PerSlotMaterialList.Num() > 1) + { + for (int32 Idx = 0; Idx < PerSlotMaterialList.Num(); ++Idx) + { + PerSlotMaterialList[Idx] = "[" + FString::FromInt(Idx) + "]" + PerSlotMaterialList[Idx]; + } + } + + OutStaticMeshFaceMaterials.Reset(PerSlotMaterialList.Num(), FaceMaterialIndices.Num()); + for (int32 FaceIdx = 0; FaceIdx < FaceMaterialIndices.Num(); ++FaceIdx) + { + int32 FaceMaterialIdx = FaceMaterialIndices[FaceIdx]; + + const FString & MaterialName = PerSlotMaterialList.IsValidIndex(FaceMaterialIdx) ? PerSlotMaterialList[FaceMaterialIdx] : DefaultMaterialName; + OutStaticMeshFaceMaterials.SetString(FaceIdx, MaterialName); + } +} + + +void +FUnrealMeshTranslator::CreateFaceMaterialArray( + const TArray& Materials, + const TArray& FaceMaterialIndices, + FHoudiniEngineIndexedStringMap& OutStaticMeshFaceMaterials, + TMap> & OutScalarMaterialParameters, + TMap> & OutVectorMaterialParameters, + TMap& OutTextureMaterialParameters, + TMap>& OutBoolMaterialParameters) +{ + H_SCOPED_FUNCTION_TIMER(); + + // Get the default material + UMaterialInterface* DefaultMaterialInterface = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial().Get()); + FString DefaultMaterialName = DefaultMaterialInterface ? DefaultMaterialInterface->GetPathName() : TEXT("default"); + + // We need to create list of unique materials. + TArray PerSlotMaterialList; + + // Initialize material parameter arrays. The key on each array is a prefix + the name of the parameter. The prefix is the + // material slot number, although omitted if there is only one material. + + TMap> ScalarParams; + TMap> VectorParams; + TMap> TextureParams; + TMap> BoolParams; + + UMaterialInterface* MaterialInterface = nullptr; + if (Materials.Num() > 0) + { + H_SCOPED_FUNCTION_STATIC_LABEL("Gather Materials"); + + // We have materials. + for (int32 MaterialIdx = 0; MaterialIdx < Materials.Num(); MaterialIdx++) + { + FString ParamPrefix = Materials.Num() == 1 ? "" : FString::FromInt(MaterialIdx) + FString("_"); + MaterialInterface = Materials[MaterialIdx]; + if (!MaterialInterface) + { + // Null material interface found, add default instead. + PerSlotMaterialList.Add(DefaultMaterialName); + + // No need to collect material parameters on the default material + continue; + } + + // We found a material, get its name and material parameters + PerSlotMaterialList.Add(MaterialInterface->GetPathName()); + + // Collect all scalar parameters in this material + { + TArray MaterialScalarParamInfos; + TArray MaterialScalarParamGuids; + MaterialInterface->GetAllScalarParameterInfo(MaterialScalarParamInfos, MaterialScalarParamGuids); + + for (auto& CurScalarParam : MaterialScalarParamInfos) + { + FString CurScalarParamName = ParamPrefix + CurScalarParam.Name.ToString(); + float CurScalarVal; + MaterialInterface->GetScalarParameterValue(CurScalarParam, CurScalarVal); + if (!ScalarParams.Contains(CurScalarParamName)) + { + TArray CurArray; + CurArray.SetNumUninitialized(Materials.Num()); + // Initialize the array with the Min float value + for (int32 ArrIdx = 0; ArrIdx < CurArray.Num(); ++ArrIdx) + CurArray[ArrIdx] = FLT_MIN; + + ScalarParams.Add(CurScalarParamName, CurArray); + OutScalarMaterialParameters.Add(CurScalarParamName); + } + + ScalarParams[CurScalarParamName][MaterialIdx] = CurScalarVal; + } + } + + // Collect all vector parameters in this material + { + TArray MaterialVectorParamInfos; + TArray MaterialVectorParamGuids; + MaterialInterface->GetAllVectorParameterInfo(MaterialVectorParamInfos, MaterialVectorParamGuids); + + for (auto& CurVectorParam : MaterialVectorParamInfos) + { + FString CurVectorParamName = ParamPrefix + CurVectorParam.Name.ToString(); + FLinearColor CurVectorValue; + MaterialInterface->GetVectorParameterValue(CurVectorParam, CurVectorValue); + if (!VectorParams.Contains(CurVectorParamName)) + { + TArray CurArray; + CurArray.SetNumUninitialized(Materials.Num()); + FLinearColor MinColor(FLT_MIN, FLT_MIN, FLT_MIN, FLT_MIN); + for (int32 ArrIdx = 0; ArrIdx < CurArray.Num(); ++ArrIdx) + CurArray[ArrIdx] = MinColor; + + VectorParams.Add(CurVectorParamName, CurArray); + OutVectorMaterialParameters.Add(CurVectorParamName); + } + + VectorParams[CurVectorParamName][MaterialIdx] = CurVectorValue; + } + } + + // Collect all texture parameters in this material + { + TArray MaterialTextureParamInfos; + TArray MaterialTextureParamGuids; + MaterialInterface->GetAllTextureParameterInfo(MaterialTextureParamInfos, MaterialTextureParamGuids); + + for (auto & CurTextureParam : MaterialTextureParamInfos) + { + FString CurTextureParamName = ParamPrefix + CurTextureParam.Name.ToString(); + UTexture * CurTexture = nullptr; + MaterialInterface->GetTextureParameterValue(CurTextureParam, CurTexture); + + if (!IsValid(CurTexture)) + continue; + + FString TexturePath = CurTexture->GetPathName(); + if (!TextureParams.Contains(CurTextureParamName)) + { + TArray CurArray; + CurArray.SetNumZeroed(Materials.Num()); + + TextureParams.Add(CurTextureParamName, CurArray); + OutTextureMaterialParameters.Add(CurTextureParamName); + } + + TextureParams[CurTextureParamName][MaterialIdx] = TexturePath; + } + } + + // Collect all bool parameters in this material + { + TArray MaterialBoolParamInfos; + TArray MaterialBoolParamGuids; + MaterialInterface->GetAllStaticSwitchParameterInfo(MaterialBoolParamInfos, MaterialBoolParamGuids); + + for (auto& CurBoolParam : MaterialBoolParamInfos) + { + FString CurBoolParamName = ParamPrefix + CurBoolParam.Name.ToString(); + bool CurBool = false; + FGuid CurExprValue; + MaterialInterface->GetStaticSwitchParameterValue(CurBoolParam, CurBool, CurExprValue); + + if (!BoolParams.Contains(CurBoolParamName)) + { + TArray CurArray; + CurArray.SetNumZeroed(Materials.Num()); + + BoolParams.Add(CurBoolParamName, CurArray); + OutBoolMaterialParameters.Add(CurBoolParamName); + } + + BoolParams[CurBoolParamName][MaterialIdx] = CurBool ? 1 : 0; + } + } + } + } + else + { + // We do not have any materials, add default. + PerSlotMaterialList.Add(DefaultMaterialName); + } + + // Add the Material slot index in brackets if we have more than one material + if (PerSlotMaterialList.Num() > 1) + { + for (int32 Idx = 0; Idx < PerSlotMaterialList.Num(); ++Idx) + { + PerSlotMaterialList[Idx] = "[" + FString::FromInt(Idx) + "]" + PerSlotMaterialList[Idx]; + } + } + + // Set all materials per face + { + H_SCOPED_FUNCTION_STATIC_LABEL("Materials"); + OutStaticMeshFaceMaterials.Reset(PerSlotMaterialList.Num(), FaceMaterialIndices.Num()); + for (int32 FaceIdx = 0; FaceIdx < FaceMaterialIndices.Num(); ++FaceIdx) + { + int32 FaceMaterialIdx = FaceMaterialIndices[FaceIdx]; + if (PerSlotMaterialList.IsValidIndex(FaceMaterialIdx)) + { + OutStaticMeshFaceMaterials.SetString(FaceIdx, PerSlotMaterialList[FaceMaterialIdx]); + } + else + { + OutStaticMeshFaceMaterials.SetString(FaceIdx, DefaultMaterialName); + } + } + } + + // Add scalar parameters + { + H_SCOPED_FUNCTION_STATIC_LABEL("ScalarParams"); + for (auto& Pair : ScalarParams) + { + auto & Entries = OutScalarMaterialParameters[Pair.Key]; + Entries.SetNum(FaceMaterialIndices.Num()); + int Index = 0; + for (int32 FaceIdx = 0; FaceIdx < FaceMaterialIndices.Num(); ++FaceIdx) + { + int32 FaceMaterialIdx = FaceMaterialIndices[FaceIdx]; + if (PerSlotMaterialList.IsValidIndex(FaceMaterialIdx)) + { + const auto& Value = Pair.Value[FaceMaterialIdx]; + Entries[Index++] = Value; + } + } + check(Index == Entries.Num()); + } + } + + // Add vector parameters. + { + H_SCOPED_FUNCTION_STATIC_LABEL("VectorParams"); + for (auto& Pair : VectorParams) + { + auto& Entries = OutVectorMaterialParameters[Pair.Key]; + Entries.SetNum(FaceMaterialIndices.Num() * 4); + int Index = 0; + for (int32 FaceIdx = 0; FaceIdx < FaceMaterialIndices.Num(); ++FaceIdx) + { + int32 FaceMaterialIdx = FaceMaterialIndices[FaceIdx]; + if (PerSlotMaterialList.IsValidIndex(FaceMaterialIdx)) + { + const auto & Value = Pair.Value[FaceMaterialIdx]; + Entries[Index++] = Value.R; + Entries[Index++] = Value.G; + Entries[Index++] = Value.B; + Entries[Index++] = Value.A; + } + } + check(Index == Entries.Num()); + } + } + + // Add texture params. + { + H_SCOPED_FUNCTION_STATIC_LABEL("TextureParams"); + + for (auto& Pair : TextureParams) + { + auto& Entries = OutTextureMaterialParameters[Pair.Key]; + Entries.Reset(PerSlotMaterialList.Num(), FaceMaterialIndices.Num()); + for (int32 FaceIdx = 0; FaceIdx < FaceMaterialIndices.Num(); ++FaceIdx) + { + int32 FaceMaterialIdx = FaceMaterialIndices[FaceIdx]; + if (PerSlotMaterialList.IsValidIndex(FaceMaterialIdx)) + { + Entries.SetString(FaceIdx, Pair.Value[FaceMaterialIdx]); + } + } + } + } + + // Add bool params. + { + H_SCOPED_FUNCTION_STATIC_LABEL("BoolParams"); + for (auto& Pair : BoolParams) + { + auto& Entries = OutBoolMaterialParameters[Pair.Key]; + Entries.SetNum(FaceMaterialIndices.Num()); + int Index = 0; + for (int32 FaceIdx = 0; FaceIdx < FaceMaterialIndices.Num(); ++FaceIdx) + { + int32 FaceMaterialIdx = FaceMaterialIndices[FaceIdx]; + if (PerSlotMaterialList.IsValidIndex(FaceMaterialIdx)) + { + const auto& Value = Pair.Value[FaceMaterialIdx]; + Entries[Index++] = Value; + } + } + check(Index == Entries.Num()); + } + } +} + + +bool +FUnrealMeshTranslator::CreateInputNodeForBox( + HAPI_NodeId& OutNodeId, + const HAPI_NodeId InParentNodeID, + const int32 ColliderIndex, + const FVector& BoxCenter, + const FVector& BoxExtent, + const FRotator& BoxRotation) +{ + // Create a new input node for the box collider + FString BoxName = TEXT("box") + FString::FromInt(ColliderIndex); + + // Create the node in this input object's OBJ node + HAPI_NodeId BoxNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + InParentNodeID, TEXT("box"), BoxName, false, &BoxNodeId), false); + + // Set the box parameters + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), BoxNodeId, "size", 0, (float)(BoxExtent.X) / HAPI_UNREAL_SCALE_FACTOR_POSITION); + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), BoxNodeId, "size", 1, (float)(BoxExtent.Z) / HAPI_UNREAL_SCALE_FACTOR_POSITION); + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), BoxNodeId, "size", 2, (float)(BoxExtent.Y) / HAPI_UNREAL_SCALE_FACTOR_POSITION); + + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), BoxNodeId, "t", 0, (float)(BoxCenter.X) / HAPI_UNREAL_SCALE_FACTOR_POSITION); + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), BoxNodeId, "t", 1, (float)(BoxCenter.Z) / HAPI_UNREAL_SCALE_FACTOR_POSITION); + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), BoxNodeId, "t", 2, (float)(BoxCenter.Y) / HAPI_UNREAL_SCALE_FACTOR_POSITION); + + // Do coordinate system conversion before sending to Houdini + FQuat RotationQuat = BoxRotation.Quaternion(); + + Swap(RotationQuat.Y, RotationQuat.Z); + RotationQuat.W = -RotationQuat.W; + const FRotator Rotator = RotationQuat.Rotator(); + + // Negate roll and pitch since they are actually RHR + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), BoxNodeId, "r", 0, (float)-Rotator.Roll); + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), BoxNodeId, "r", 1, (float)-Rotator.Pitch); + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), BoxNodeId, "r", 2, (float)Rotator.Yaw); + + if (!FHoudiniEngineUtils::HapiCookNode(BoxNodeId, nullptr, true)) + return false; + + // Create a group node + FString GroupNodeName = TEXT("group") + FString::FromInt(ColliderIndex); + + HAPI_NodeId GroupNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + InParentNodeID, TEXT("groupcreate"), GroupNodeName, false, &GroupNodeId), false); + + // Set its group name param to collision_geo_simple_box + HAPI_ParmInfo ParmInfo; + HAPI_ParmId parmId = FHoudiniEngineUtils::HapiFindParameterByName(GroupNodeId, "groupname", ParmInfo); + + FString LODGroup = TEXT("collision_geo_simple_box") + FString::FromInt(ColliderIndex); + + FHoudiniApi::SetParmStringValue( + FHoudiniEngine::Get().GetSession(), GroupNodeId, H_TCHAR_TO_UTF8(*LODGroup), parmId, 0); + + // Connect the box to the group + FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), GroupNodeId, 0, BoxNodeId, 0); + + OutNodeId = GroupNodeId; + + return true; +} + +bool +FUnrealMeshTranslator::CreateInputNodeForSphere( + HAPI_NodeId& OutNodeId, + const HAPI_NodeId InParentNodeID, + const int32 ColliderIndex, + const FVector& SphereCenter, + const float SphereRadius) +{ + // Create a new input node for the sphere collider + FString SphereName = TEXT("Sphere") + FString::FromInt(ColliderIndex); + + // Create the node in this input object's OBJ node + HAPI_NodeId SphereNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + InParentNodeID, "sphere", SphereName, false, &SphereNodeId), false); + + // Set the box parameters + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), SphereNodeId, "rad", 0, SphereRadius / HAPI_UNREAL_SCALE_FACTOR_POSITION); + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), SphereNodeId, "rad", 1, SphereRadius / HAPI_UNREAL_SCALE_FACTOR_POSITION); + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), SphereNodeId, "rad", 2, SphereRadius / HAPI_UNREAL_SCALE_FACTOR_POSITION); + + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), SphereNodeId, "t", 0, (float)(SphereCenter.X) / HAPI_UNREAL_SCALE_FACTOR_POSITION); + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), SphereNodeId, "t", 1, (float)(SphereCenter.Z) / HAPI_UNREAL_SCALE_FACTOR_POSITION); + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), SphereNodeId, "t", 2, (float)(SphereCenter.Y) / HAPI_UNREAL_SCALE_FACTOR_POSITION); + + FHoudiniApi::SetParmIntValue( + FHoudiniEngine::Get().GetSession(), SphereNodeId, "type", 0, 1); + /* + FHoudiniApi::SetParmFloatValue( + FHoudiniEngine::Get().GetSession(), SphereNodeId, "scale", 0, SphereRadius / HAPI_UNREAL_SCALE_FACTOR_POSITION); + */ + + if (!FHoudiniEngineUtils::HapiCookNode(SphereNodeId, nullptr, true)) + return false; + + // Create a group node + FString GroupNodeName = TEXT("group") + FString::FromInt(ColliderIndex); + HAPI_NodeId GroupNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + InParentNodeID, TEXT("groupcreate"), GroupNodeName, false, &GroupNodeId), false); + + // Set its group name param to collision_geo_simple_box + HAPI_ParmInfo ParmInfo; + HAPI_ParmId parmId = FHoudiniEngineUtils::HapiFindParameterByName(GroupNodeId, "groupname", ParmInfo); + + FString LODGroup = TEXT("collision_geo_simple_sphere") + FString::FromInt(ColliderIndex); + + FHoudiniApi::SetParmStringValue( + FHoudiniEngine::Get().GetSession(), GroupNodeId, H_TCHAR_TO_UTF8(*LODGroup), parmId, 0); + + // Connect the box to the group + FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), GroupNodeId, 0, SphereNodeId, 0); + + OutNodeId = GroupNodeId; + + return true; +} + +bool +FUnrealMeshTranslator::CreateInputNodeForSphyl( + HAPI_NodeId& OutNodeId, + const HAPI_NodeId InParentNodeID, + const int32 ColliderIndex, + const FVector& SphylCenter, + const FRotator& SphylRotation, + const float SphylRadius, + const float SphereLength) +{ + // + // Get the Sphyl's vertices and indices + // (code drived from FKSphylElem::GetElemSolid) + // + + // TODO: + // Rotation? + const int32 NumSides = 6; + const int32 NumRings = (NumSides / 2) + 1; + + // The first/last arc are on top of each other. + const int32 NumVerts = (NumSides + 1) * (NumRings + 1); + + // Calculate the vertices for one arc + TArray ArcVertices; + ArcVertices.SetNum(NumRings + 1); + for (int32 RingIdx = 0; RingIdx < NumRings + 1; RingIdx++) + { + float Angle; + float ZOffset; + if (RingIdx <= NumSides / 4) + { + Angle = ((float)RingIdx / (NumRings - 1)) * PI; + ZOffset = 0.5 * SphereLength; + } + else + { + Angle = ((float)(RingIdx - 1) / (NumRings - 1)) * PI; + ZOffset = -0.5 * SphereLength; + } + + // Note- unit sphere, so position always has mag of one. We can just use it for normal! + FVector SpherePos; + SpherePos.X = 0.0f; + SpherePos.Y = SphylRadius * FMath::Sin(Angle); + SpherePos.Z = SphylRadius * FMath::Cos(Angle); + + ArcVertices[RingIdx] = SpherePos + FVector(0, 0, ZOffset); + } + + // Get the transform matrix for the rotation + + // Get the Sphyl's vertices by rotating the arc NumSides+1 times. + TArray Vertices; + Vertices.SetNum(NumVerts * 3); + for (int32 SideIdx = 0; SideIdx < NumSides + 1; SideIdx++) + { + const FRotator ArcRotator(0, 360.f * ((float)SideIdx / NumSides), 0); + const FRotationMatrix ArcRot(ArcRotator); + const float XTexCoord = ((float)SideIdx / NumSides); + + for (int32 VertIdx = 0; VertIdx < NumRings + 1; VertIdx++) + { + int32 VIx = (NumRings + 1)*SideIdx + VertIdx; + + FVector ArcVertex = ArcRot.TransformPosition(ArcVertices[VertIdx]); + ArcVertex = SphylRotation.Quaternion() * ArcVertex; + + FVector CurPosition = SphylCenter + ArcVertex; + + // Convert the UE4 position to Houdini + Vertices[VIx * 3 + 0] = (float)(CurPosition.X) / HAPI_UNREAL_SCALE_FACTOR_POSITION; + Vertices[VIx * 3 + 1] = (float)(CurPosition.Z) / HAPI_UNREAL_SCALE_FACTOR_POSITION; + Vertices[VIx * 3 + 2] = (float)(CurPosition.Y) / HAPI_UNREAL_SCALE_FACTOR_POSITION; + } + } + + // Add all of the indices to the mesh. + int32 NumIndices = NumSides * NumRings * 6; + TArray Indices; + Indices.SetNum(NumIndices); + + int32 CurIndex = 0; + for (int32 SideIdx = 0; SideIdx < NumSides; SideIdx++) + { + const int32 a0start = (SideIdx + 0) * (NumRings + 1); + const int32 a1start = (SideIdx + 1) * (NumRings + 1); + for (int32 RingIdx = 0; RingIdx < NumRings; RingIdx++) + { + // First Tri (reverse winding) + Indices[CurIndex+0] = a0start + RingIdx + 0; + Indices[CurIndex+2] = a1start + RingIdx + 0; + Indices[CurIndex+1] = a0start + RingIdx + 1; + CurIndex += 3; + // Second Tri (reverse winding) + Indices[CurIndex+0] = a1start + RingIdx + 0; + Indices[CurIndex+2] = a1start + RingIdx + 1; + Indices[CurIndex+1] = a0start + RingIdx + 1; + CurIndex += 3; + } + } + + // + // Create the Sphyl Mesh in houdini + // + HAPI_NodeId SphylNodeId = -1; + FString SphylName = TEXT("Sphyl") + FString::FromInt(ColliderIndex); + if(!CreateInputNodeForCollider(SphylNodeId, InParentNodeID, ColliderIndex, SphylName, Vertices, Indices)) + return false; + + // Create a group node + FString GroupNodeName = TEXT("group") + FString::FromInt(ColliderIndex); + HAPI_NodeId GroupNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + InParentNodeID, TEXT("groupcreate"), GroupNodeName, false, &GroupNodeId), false); + + // Set its group name param to collision_geo_simple_box + HAPI_ParmInfo ParmInfo; + HAPI_ParmId parmId = FHoudiniEngineUtils::HapiFindParameterByName(GroupNodeId, "groupname", ParmInfo); + + FString LODGroup = TEXT("collision_geo_simple_capsule") + FString::FromInt(ColliderIndex); + + FHoudiniApi::SetParmStringValue( + FHoudiniEngine::Get().GetSession(), GroupNodeId, H_TCHAR_TO_UTF8(*LODGroup), parmId, 0); + + // Connect the box to the group + FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), GroupNodeId, 0, SphylNodeId, 0); + + OutNodeId = GroupNodeId; + + return true; +} + +bool +FUnrealMeshTranslator::CreateInputNodeForConvex( + HAPI_NodeId& OutNodeId, + const HAPI_NodeId InParentNodeID, + const int32 ColliderIndex, + const FKConvexElem& ConvexCollider) +{ + TArray Vertices; + TArray Indices; + + FTransform ConvexTransform = ConvexCollider.GetTransform(); + + FVector3d TransformOffset = ConvexTransform.GetLocation(); + FVector3d ScaleOffset = ConvexTransform.GetScale3D(); + FQuat4d RotationOffset = ConvexTransform.GetRotation(); + +#if ENGINE_MINOR_VERSION < 1 + //UE5.0 PhysX/Chaos +#if PHYSICS_INTERFACE_PHYSX + if (ConvexCollider.GetConvexMesh() || ConvexCollider.GetMirroredConvexMesh()) +#elif WITH_CHAOS + if (ConvexCollider.IndexData.Num() > 0 && ConvexCollider.IndexData.Num() % 3 == 0) +#else + if(false) +#endif +#else + if (ConvexCollider.IndexData.Num() > 0 && ConvexCollider.IndexData.Num() % 3 == 0) +#endif + { + // Get the convex colliders vertices and indices from the mesh + TArray VertexBuffer; + TArray IndexBuffer; + ConvexCollider.AddCachedSolidConvexGeom(VertexBuffer, IndexBuffer, FColor::White); + + for (int32 i = 0; i < VertexBuffer.Num(); i++) + { + VertexBuffer[i].Position = (FVector3f)(TransformOffset + (RotationOffset * (ScaleOffset * (FVector3d)VertexBuffer[i].Position))); + } + + Vertices.SetNum(VertexBuffer.Num() * 3); + int32 CurIndex = 0; + for (auto& CurVert : VertexBuffer) + { + Vertices[CurIndex * 3 + 0] = (float)(CurVert.Position.X) / HAPI_UNREAL_SCALE_FACTOR_POSITION; + Vertices[CurIndex * 3 + 1] = (float)(CurVert.Position.Z) / HAPI_UNREAL_SCALE_FACTOR_POSITION; + Vertices[CurIndex * 3 + 2] = (float)(CurVert.Position.Y) / HAPI_UNREAL_SCALE_FACTOR_POSITION; + CurIndex++; + } + + Indices.SetNum(IndexBuffer.Num()); + for (int Idx = 0; (Idx + 2) < IndexBuffer.Num(); Idx += 3) + { + // Reverse winding + Indices[Idx + 0] = Indices[Idx + 0]; + Indices[Idx + 2] = Indices[Idx + 1]; + Indices[Idx + 1] = Indices[Idx + 2]; + } + } + else + { + // Need to copy vertices because we plan on modifying it by Quaternion/Vector multiplication + TArray VertexBuffer; + VertexBuffer.SetNum(ConvexCollider.VertexData.Num()); + + for (int32 Idx = 0; Idx < ConvexCollider.VertexData.Num(); Idx++) + { + VertexBuffer[Idx] = TransformOffset + (RotationOffset * (ScaleOffset * ConvexCollider.VertexData[Idx])); + } + + int32 NumVert = ConvexCollider.VertexData.Num(); + Vertices.SetNum(NumVert * 3); + //Indices.SetNum(NumVert); + int32 CurIndex = 0; + for (auto& CurVert : VertexBuffer) + { + Vertices[CurIndex * 3 + 0] = (float)(CurVert.X) / HAPI_UNREAL_SCALE_FACTOR_POSITION; + Vertices[CurIndex * 3 + 1] = (float)(CurVert.Z) / HAPI_UNREAL_SCALE_FACTOR_POSITION; + Vertices[CurIndex * 3 + 2] = (float)(CurVert.Y) / HAPI_UNREAL_SCALE_FACTOR_POSITION; + + /* + // TODO: Get proper polygons... + Indices[CurIndex] = CurIndex; + */ + CurIndex++; + } + + // TODO: Get Proper polygons + for (int32 Idx = 0; Idx + 2 < NumVert; Idx++) + { + Indices.Add(Idx + 0); + Indices.Add(Idx + 1); + Indices.Add(Idx + 2); + } + } + + // + // Create the Convex Mesh in houdini + // + HAPI_NodeId ConvexNodeId = -1; + FString ConvexName = TEXT("Convex") + FString::FromInt(ColliderIndex); + if (!CreateInputNodeForCollider(ConvexNodeId, InParentNodeID, ColliderIndex, ConvexName, Vertices, Indices)) + return false; + + //HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + //FHoudiniApi::CookNode(FHoudiniEngine::Get().GetSession(), ColliderNodeId, &CookOptions); + + // Create a group node + FString GroupNodeName = TEXT("group") + FString::FromInt(ColliderIndex); + HAPI_NodeId GroupNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + InParentNodeID, "groupcreate", GroupNodeName, false, &GroupNodeId), false); + + // Set its group name param to collision_geo_simple_ucx + HAPI_ParmInfo ParmInfo; + HAPI_ParmId parmId = FHoudiniEngineUtils::HapiFindParameterByName(GroupNodeId, "groupname", ParmInfo); + + FString LODGroup = TEXT("collision_geo_simple_ucx") + FString::FromInt(ColliderIndex); + + FHoudiniApi::SetParmStringValue( + FHoudiniEngine::Get().GetSession(), GroupNodeId, H_TCHAR_TO_UTF8(*LODGroup), parmId, 0); + + // Create a convex hull (shrinkwrap::2.0) node to fix the lack of proper indices + HAPI_NodeId ConvexHullNodeId = -1; + FString ConvexHullName = TEXT("ConvexHull") + FString::FromInt(ColliderIndex); + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + InParentNodeID, "shrinkwrap::2.0", ConvexHullName, false, &ConvexHullNodeId), false); + + if (ConvexHullNodeId > 0) + { + // Connect the collider to the convex hull + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), ConvexHullNodeId, 0, ConvexNodeId, 0), false); + + // Connect the convex hull to the group + HOUDINI_CHECK_ERROR_RETURN( FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), GroupNodeId, 0, ConvexHullNodeId, 0), false); + } + else + { + // Connect the collider to the group + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), GroupNodeId, 0, ConvexNodeId, 0), false); + + } + + OutNodeId = GroupNodeId; + + return true; +} + +bool +FUnrealMeshTranslator::CreateInputNodeForCollider( + HAPI_NodeId& OutNodeId, + const HAPI_NodeId InParentNodeID, + const int32 ColliderIndex, + const FString& ColliderName, + const TArray& ColliderVertices, + const TArray& ColliderIndices) +{ + // Create a new input node for the collider in this input object's OBJ node + HAPI_NodeId ColliderNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN( FHoudiniEngineUtils::CreateNode( + InParentNodeID, "null", H_TCHAR_TO_UTF8(*ColliderName), false, &ColliderNodeId), false); + + // Create a part + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + Part.vertexCount = ColliderIndices.Num(); + Part.faceCount = ColliderIndices.Num() / 3; + Part.pointCount = ColliderVertices.Num() / 3; + Part.type = HAPI_PARTTYPE_MESH; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), ColliderNodeId, 0, &Part), false); + + // Create point attribute info. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = ColliderVertices.Num() / 3; + AttributeInfoPoint.tupleSize = 3; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + ColliderNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint), false); + + // Upload the positions + FHoudiniHapiAccessor Accessor(ColliderNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPoint, ColliderVertices), false); + + // Upload the indices + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetVertexList( + ColliderIndices, ColliderNodeId, 0), false); + + // Generate the array of face counts. + TArray ColliderFaceCounts; + ColliderFaceCounts.SetNumUninitialized(Part.faceCount); + for (int32 n = 0; n < Part.faceCount; n++) + ColliderFaceCounts[n] = 3; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetFaceCounts( + ColliderFaceCounts, ColliderNodeId, 0), false); + + // Commit the geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(ColliderNodeId), false); + + OutNodeId = ColliderNodeId; + + return true; +} + +bool +FUnrealMeshTranslator::CreateHoudiniMeshAttributes( + const int32 NodeId, + const int32 PartId, + const int32 Count, + const FHoudiniEngineIndexedStringMap& TriangleMaterials, + const TArray & MaterialSlotIndices, + const TMap>& ScalarMaterialParameters, + const TMap>& VectorMaterialParameters, + const TMap& TextureMaterialParameters, + const TMap>& BoolMaterialParameters, + const TOptional PhysicalMaterial, + const TOptional InNaniteSettings) +{ + H_SCOPED_FUNCTION_TIMER(); + + if (NodeId < 0) + return false; + + bool bSuccess = true; + + // Create attribute for material slot + HAPI_AttributeInfo AttributeInfoMaterialSlot; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoMaterialSlot); + AttributeInfoMaterialSlot.tupleSize = 1; + AttributeInfoMaterialSlot.count = Count; + AttributeInfoMaterialSlot.exists = true; + AttributeInfoMaterialSlot.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoMaterialSlot.storage = HAPI_STORAGETYPE_INT; + AttributeInfoMaterialSlot.originalOwner = HAPI_ATTROWNER_INVALID; + + // Create the new attribute + if(HAPI_RESULT_SUCCESS == FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, HAPI_UNREAL_ATTRIB_MATERIAL_SLOT, &AttributeInfoMaterialSlot)) + { + // The New attribute has been successfully created, set its value + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_MATERIAL_SLOT); + bSuccess &= Accessor.SetAttributeData(AttributeInfoMaterialSlot, MaterialSlotIndices); + } + + + // Create attribute for materials. + HAPI_AttributeInfo AttributeInfoMaterial; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoMaterial); + AttributeInfoMaterial.tupleSize = 1; + AttributeInfoMaterial.count = Count; + AttributeInfoMaterial.exists = true; + AttributeInfoMaterial.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoMaterial.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoMaterial.originalOwner = HAPI_ATTROWNER_INVALID; + + // Create the new attribute + if (HAPI_RESULT_SUCCESS == FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, HAPI_UNREAL_ATTRIB_MATERIAL, &AttributeInfoMaterial)) + { + // The New attribute has been successfully created, set its value + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_MATERIAL); + bSuccess &= Accessor.SetAttributeStringMap(AttributeInfoMaterial, TriangleMaterials); + } + + // Add scalar material parameter attributes + for (auto& Pair : ScalarMaterialParameters) + { + FString CurMaterialParamAttribName = FString(HAPI_UNREAL_ATTRIB_MATERIAL) + "_parameter_" + Pair.Key; + FHoudiniEngineUtils::SanitizeHAPIVariableName(CurMaterialParamAttribName); + + // Create attribute for material parameter. + HAPI_AttributeInfo AttributeInfoMaterialParameter; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoMaterialParameter); + AttributeInfoMaterialParameter.tupleSize = 1; + AttributeInfoMaterialParameter.count = Count; + AttributeInfoMaterialParameter.exists = true; + AttributeInfoMaterialParameter.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoMaterialParameter.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoMaterialParameter.originalOwner = HAPI_ATTROWNER_INVALID; + + // Create the new attribute + if (HAPI_RESULT_SUCCESS == FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, TCHAR_TO_ANSI(*CurMaterialParamAttribName), &AttributeInfoMaterialParameter)) + { + // The New attribute has been successfully created, set its value + + FHoudiniHapiAccessor Accessor(NodeId, PartId, TCHAR_TO_ANSI(*CurMaterialParamAttribName)); + + bSuccess &= Accessor.SetAttributeData(AttributeInfoMaterialParameter, Pair.Value); + } + } + + // Add vector material parameters + for (auto& Pair : VectorMaterialParameters) + { + FString CurMaterialParamAttribName = FString(HAPI_UNREAL_ATTRIB_MATERIAL) + "_parameter_" + Pair.Key; + FHoudiniEngineUtils::SanitizeHAPIVariableName(CurMaterialParamAttribName); + + // Create attribute for material parameter. + HAPI_AttributeInfo AttributeInfoMaterialParameter; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoMaterialParameter); + AttributeInfoMaterialParameter.tupleSize = 4; + AttributeInfoMaterialParameter.count = Count; + AttributeInfoMaterialParameter.exists = true; + AttributeInfoMaterialParameter.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoMaterialParameter.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoMaterialParameter.originalOwner = HAPI_ATTROWNER_INVALID; + + // Create the new attribute + if (HAPI_RESULT_SUCCESS == FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), + NodeId, PartId, TCHAR_TO_ANSI(*CurMaterialParamAttribName), &AttributeInfoMaterialParameter)) + { + // The New attribute has been successfully created, set its value + FHoudiniHapiAccessor Accessor(NodeId, PartId, TCHAR_TO_ANSI(*CurMaterialParamAttribName)); + bSuccess &= Accessor.SetAttributeData(AttributeInfoMaterialParameter, Pair.Value); + } + } + + // Add texture material parameter attributes + for (auto& Pair : TextureMaterialParameters) + { + FString CurMaterialParamAttribName = FString(HAPI_UNREAL_ATTRIB_MATERIAL) + "_parameter_" + Pair.Key; + FHoudiniEngineUtils::SanitizeHAPIVariableName(CurMaterialParamAttribName); + + // Create attribute for material parameter. + HAPI_AttributeInfo AttributeInfoMaterialParameter; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoMaterialParameter); + AttributeInfoMaterialParameter.tupleSize = 1; + AttributeInfoMaterialParameter.count = Count; + AttributeInfoMaterialParameter.exists = true; + AttributeInfoMaterialParameter.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoMaterialParameter.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoMaterialParameter.originalOwner = HAPI_ATTROWNER_INVALID; + + const FHoudiniEngineIndexedStringMap & StringMap = Pair.Value; + // Create the new attribute + if (HAPI_RESULT_SUCCESS == FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), + NodeId, PartId, TCHAR_TO_ANSI(*CurMaterialParamAttribName), &AttributeInfoMaterialParameter)) + { + // The New attribute has been successfully created, set its value + FHoudiniHapiAccessor Accessor(NodeId, PartId, TCHAR_TO_ANSI(*CurMaterialParamAttribName)); + bSuccess = Accessor.SetAttributeStringMap(AttributeInfoMaterialParameter, StringMap); + } + } + + // Add bool material parameter attributes + for (auto& Pair : BoolMaterialParameters) + { + FString CurMaterialParamAttribName = FString(HAPI_UNREAL_ATTRIB_MATERIAL) + "_parameter_" + Pair.Key; + FHoudiniEngineUtils::SanitizeHAPIVariableName(CurMaterialParamAttribName); + + // Create attribute for material parameter. + HAPI_AttributeInfo AttributeInfoMaterialParameter; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoMaterialParameter); + AttributeInfoMaterialParameter.tupleSize = 1; + AttributeInfoMaterialParameter.count = Count; + AttributeInfoMaterialParameter.exists = true; + AttributeInfoMaterialParameter.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoMaterialParameter.storage = HAPI_STORAGETYPE_INT8; + AttributeInfoMaterialParameter.originalOwner = HAPI_ATTROWNER_INVALID; + + // Create the new attribute + if (HAPI_RESULT_SUCCESS == FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, TCHAR_TO_ANSI(*CurMaterialParamAttribName), &AttributeInfoMaterialParameter)) + { + FHoudiniHapiAccessor Accessor(NodeId, 0, TCHAR_TO_ANSI(*CurMaterialParamAttribName)); + bSuccess &= Accessor.SetAttributeData(AttributeInfoMaterialParameter, Pair.Value); + } + } + + if (PhysicalMaterial.IsSet() && !PhysicalMaterial->IsEmpty()) + { + // Create attribute for physical materials. + HAPI_AttributeInfo AttributeInfoPhysicalMaterial; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPhysicalMaterial); + AttributeInfoPhysicalMaterial.tupleSize = 1; + AttributeInfoPhysicalMaterial.count = Count; + AttributeInfoPhysicalMaterial.exists = true; + AttributeInfoPhysicalMaterial.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoPhysicalMaterial.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoPhysicalMaterial.originalOwner = HAPI_ATTROWNER_INVALID; + + // Create the new attribute + if (HAPI_RESULT_SUCCESS == FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, HAPI_UNREAL_ATTRIB_SIMPLE_PHYSICAL_MATERIAL, &AttributeInfoPhysicalMaterial)) + { + // The New attribute has been successfully created, set its value + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_SIMPLE_PHYSICAL_MATERIAL); + bSuccess &= Accessor.SetAttributeUniqueData(AttributeInfoPhysicalMaterial, PhysicalMaterial.GetValue()); + } + } + + // Add the nanite attributes if needed + bool bNaniteEnabled = false; + if (InNaniteSettings.IsSet() && InNaniteSettings->bEnabled) + { + // Create an attribute for nanite enabled + HAPI_AttributeInfo AttributeInfoNanite; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoNanite); + AttributeInfoNanite.tupleSize = 1; + AttributeInfoNanite.count = Count; + AttributeInfoNanite.exists = true; + AttributeInfoNanite.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoNanite.storage = HAPI_STORAGETYPE_INT; + AttributeInfoNanite.originalOwner = HAPI_ATTROWNER_INVALID; + + // Create the new attribute + if (HAPI_RESULT_SUCCESS == FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, HAPI_UNREAL_ATTRIB_NANITE_ENABLED, &AttributeInfoNanite)) + { + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_NANITE_ENABLED); + bSuccess &= Accessor.SetAttributeUniqueData(AttributeInfoNanite, 1); + } + + // Create an attribute for nanite position precision + //HAPI_AttributeInfo AttributeInfoNanite; + + FHoudiniApi::AttributeInfo_Init(&AttributeInfoNanite); + AttributeInfoNanite.tupleSize = 1; + AttributeInfoNanite.count = Count; + AttributeInfoNanite.exists = true; + AttributeInfoNanite.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoNanite.storage = HAPI_STORAGETYPE_INT; + AttributeInfoNanite.originalOwner = HAPI_ATTROWNER_INVALID; + + // Create the new attribute + if (HAPI_RESULT_SUCCESS == FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, HAPI_UNREAL_ATTRIB_NANITE_POSITION_PRECISION, &AttributeInfoNanite)) + { + // The New attribute has been successfully created, set its value + int32 PositionPrecision = InNaniteSettings->PositionPrecision; + + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_NANITE_POSITION_PRECISION); + bSuccess &= Accessor.SetAttributeUniqueData(AttributeInfoNanite, InNaniteSettings->PositionPrecision); + } + + // Create an attribute for nanite percent triangle + //HAPI_AttributeInfo AttributeInfoNanite; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoNanite); + AttributeInfoNanite.tupleSize = 1; + AttributeInfoNanite.count = Count; + AttributeInfoNanite.exists = true; + AttributeInfoNanite.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoNanite.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoNanite.originalOwner = HAPI_ATTROWNER_INVALID; + + // Create the new attribute + if (HAPI_RESULT_SUCCESS == FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, HAPI_UNREAL_ATTRIB_NANITE_PERCENT_TRIANGLES, &AttributeInfoNanite)) + { + // The New attribute has been successfully created, set its value + float KeepPercentTriangles = InNaniteSettings->KeepPercentTriangles; + + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_NANITE_PERCENT_TRIANGLES); + bSuccess &= Accessor.SetAttributeUniqueData(AttributeInfoNanite, KeepPercentTriangles); + } + + // Create an attribute for nanite fb relative error + //HAPI_AttributeInfo AttributeInfoNanite; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoNanite); + AttributeInfoNanite.tupleSize = 1; + AttributeInfoNanite.count = Count; + AttributeInfoNanite.exists = true; + AttributeInfoNanite.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoNanite.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoNanite.originalOwner = HAPI_ATTROWNER_INVALID; + + // Create the new attribute + if (HAPI_RESULT_SUCCESS == FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, HAPI_UNREAL_ATTRIB_NANITE_FB_RELATIVE_ERROR, &AttributeInfoNanite)) + { + // The New attribute has been successfully created, set its value + float FallbackRelativeError = InNaniteSettings->FallbackRelativeError; + + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_NANITE_FB_RELATIVE_ERROR); + bSuccess &= Accessor.SetAttributeUniqueData(AttributeInfoNanite, FallbackRelativeError); + } + + // Create an attribute for nanite trim relative error + //HAPI_AttributeInfo AttributeInfoNanite; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoNanite); + AttributeInfoNanite.tupleSize = 1; + AttributeInfoNanite.count = Count; + AttributeInfoNanite.exists = true; + AttributeInfoNanite.owner = HAPI_ATTROWNER_PRIM; + AttributeInfoNanite.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoNanite.originalOwner = HAPI_ATTROWNER_INVALID; + + // Create the new attribute + if (HAPI_RESULT_SUCCESS == FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NodeId, PartId, HAPI_UNREAL_ATTRIB_NANITE_TRIM_RELATIVE_ERROR, &AttributeInfoNanite)) + { + // The New attribute has been successfully created, set its value + float TrimRelativeError = InNaniteSettings->TrimRelativeError; + + FHoudiniHapiAccessor Accessor(NodeId, PartId, HAPI_UNREAL_ATTRIB_NANITE_TRIM_RELATIVE_ERROR); + bSuccess &= Accessor.SetAttributeUniqueData(AttributeInfoNanite, TrimRelativeError); + } + } + + return bSuccess; +} + +bool FUnrealMeshTranslator::ExportCollisions( + int32& NextMergeIndex, + const UStaticMesh* StaticMesh, + const HAPI_NodeId MergeNodeId, + const HAPI_NodeId InputObjectNodeId, + const FKAggregateGeom& SimpleColliders) +{ + // If there are no simple colliders to create then skip this bodysetup + if(SimpleColliders.BoxElems.Num() + SimpleColliders.SphereElems.Num() + SimpleColliders.SphylElems.Num() + + SimpleColliders.ConvexElems.Num() > 0) + { + HAPI_NodeId CollisionMergeNodeId = -1; + int32 NextCollisionMergeIndex = 0; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + InputObjectNodeId, TEXT("merge"), TEXT("simple_colliders_merge") + FString::FromInt(NextMergeIndex), false, &CollisionMergeNodeId), false); + + // Export BOX colliders + for(auto& CurBox : SimpleColliders.BoxElems) + { + FVector BoxCenter = CurBox.Center; + FVector BoxExtent = FVector(CurBox.X, CurBox.Y, CurBox.Z); + FRotator BoxRotation = CurBox.Rotation; + + HAPI_NodeId BoxNodeId = -1; + if(!CreateInputNodeForBox( + BoxNodeId, InputObjectNodeId, NextCollisionMergeIndex, + BoxCenter, BoxExtent, BoxRotation)) + continue; + + if(BoxNodeId < 0) + continue; + + // Connect the Box node to the merge node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + CollisionMergeNodeId, NextCollisionMergeIndex, BoxNodeId, 0), false); + + NextCollisionMergeIndex++; + } + + // Export SPHERE colliders + for(auto& CurSphere : SimpleColliders.SphereElems) + { + HAPI_NodeId SphereNodeId = -1; + if(!CreateInputNodeForSphere( + SphereNodeId, InputObjectNodeId, NextCollisionMergeIndex, + CurSphere.Center, CurSphere.Radius)) + continue; + + if(SphereNodeId < 0) + continue; + + // Connect the Sphere node to the merge node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + CollisionMergeNodeId, NextCollisionMergeIndex, SphereNodeId, 0), false); + + NextCollisionMergeIndex++; + } + + // Export CAPSULE colliders + for(auto& CurSphyl : SimpleColliders.SphylElems) + { + HAPI_NodeId SphylNodeId = -1; + if(!CreateInputNodeForSphyl( + SphylNodeId, InputObjectNodeId, NextCollisionMergeIndex, + CurSphyl.Center, CurSphyl.Rotation, CurSphyl.Radius, CurSphyl.Length)) + continue; + + if(SphylNodeId < 0) + continue; + + // Connect the capsule node to the merge node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + CollisionMergeNodeId, NextCollisionMergeIndex, SphylNodeId, 0), false); + + NextCollisionMergeIndex++; + } + + // Export CONVEX colliders + for(auto& CurConvex : SimpleColliders.ConvexElems) + { + HAPI_NodeId ConvexNodeId = -1; + if(!CreateInputNodeForConvex( + ConvexNodeId, InputObjectNodeId, NextCollisionMergeIndex, CurConvex)) + continue; + + if(ConvexNodeId < 0) + continue; + + // Connect the capsule node to the merge node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + CollisionMergeNodeId, NextCollisionMergeIndex, ConvexNodeId, 0), false); + + NextCollisionMergeIndex++; + } + + // Create a new Attribute Wrangler node which will be used to create the new attributes. + HAPI_NodeId AttribWrangleNodeId; + if(FHoudiniEngineUtils::CreateNode( + InputObjectNodeId, TEXT("attribwrangle"), + TEXT("physical_material"), + true, &AttribWrangleNodeId) != HAPI_RESULT_SUCCESS) + { + // Failed to create the node. + HOUDINI_LOG_WARNING( + TEXT("Failed to create Physical Material attribute for mesh: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + return true; + } + + // Connect the new node to the previous node. Set CollisionMergeNodeId to the attrib node + // as is this the final output of the chain. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + AttribWrangleNodeId, 0, CollisionMergeNodeId, 0), false); + CollisionMergeNodeId = AttribWrangleNodeId; + + // Set the wrangle's class to primitives + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(FHoudiniEngine::Get().GetSession(), AttribWrangleNodeId, "class", 0, 1), false); + + // Create a Vex expression, add the mesh input name. + + const FString FormatString = TEXT("s@{0} = '{1}';\n"); + FString PathName = StaticMesh->GetPathName(); + FString AttrName = TEXT(HAPI_UNREAL_ATTRIB_INPUT_MESH_NAME); + std::string VEXpression = H_TCHAR_TO_UTF8(*FString::Format(*FormatString, { AttrName, PathName })); + + // Create a new primitive attribute where each value contains the Physical Material + // mae in Unreal. + UPhysicalMaterial* PhysicalMaterial = StaticMesh->GetBodySetup()->PhysMaterial; + if(PhysicalMaterial) + { + + // Construct a VEXpression to set create and set a Physical Material Attribute. + // eg. s@unreal_physical_material = 'MyPath/PhysicalMaterial'; + PathName = PhysicalMaterial->GetPathName(); + AttrName = TEXT(HAPI_UNREAL_ATTRIB_SIMPLE_PHYSICAL_MATERIAL); + VEXpression += H_TCHAR_TO_UTF8(*FString::Format(*FormatString, { AttrName, PathName })); + } + + // Set the snippet parameter to the VEXpression. + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniEngineUtils::HapiFindParameterByName(AttribWrangleNodeId, "snippet", ParmInfo); + if(ParmId != -1) + { + FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), AttribWrangleNodeId, + VEXpression.c_str(), ParmId, 0); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Invalid Parameter: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + } + + // Connect our collision merge node (or the phys mat attrib wrangle) to the main merge node + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + MergeNodeId, NextMergeIndex, CollisionMergeNodeId, 0), false); + NextMergeIndex++; + } + return true; +} + + +bool FUnrealMeshTranslator::GetMaterialInfo( + const TArray& Materials, + TArray& OutMaterialInfos) +{ + H_SCOPED_FUNCTION_TIMER(); + + OutMaterialInfos.SetNum(Materials.Num()); + + // We have materials. + for(int32 MaterialIdx = 0; MaterialIdx < Materials.Num(); MaterialIdx++) + { + FString ParamPrefix = HAPI_UNREAL_ATTRIB_GENERIC_MAT_PARAM_PREFIX; + ParamPrefix += Materials.Num() == 1 ? "" : FString::FromInt(MaterialIdx) + FString("_"); + + UMaterialInterface* MaterialInterface = Materials[MaterialIdx]; + if(!MaterialInterface) + continue; + + FUnrealMaterialInfo& MaterialInfo = OutMaterialInfos[MaterialIdx]; + + MaterialInfo.MaterialPath = MaterialInterface->GetPathName(); + + // Collect all scalar parameters in this material + { + TArray MaterialScalarParamInfos; + TArray MaterialScalarParamGuids; + MaterialInterface->GetAllScalarParameterInfo(MaterialScalarParamInfos, MaterialScalarParamGuids); + + for(auto& CurScalarParam : MaterialScalarParamInfos) + { + FString CurScalarParamName = ParamPrefix + CurScalarParam.Name.ToString(); + float CurScalarVal; + MaterialInterface->GetScalarParameterValue(CurScalarParam, CurScalarVal); + MaterialInfo.ScalarParameters.Add(CurScalarParamName, CurScalarVal); + } + } + + // Collect all vector parameters in this material + { + TArray MaterialVectorParamInfos; + TArray MaterialVectorParamGuids; + MaterialInterface->GetAllVectorParameterInfo(MaterialVectorParamInfos, MaterialVectorParamGuids); + + for(auto& CurVectorParam : MaterialVectorParamInfos) + { + FString CurVectorParamName = ParamPrefix + CurVectorParam.Name.ToString(); + FLinearColor CurVectorValue; + MaterialInterface->GetVectorParameterValue(CurVectorParam, CurVectorValue); + MaterialInfo.VectorParameters.Add(CurVectorParamName, CurVectorValue); + } + } + + // Collect all texture parameters in this material + { + TArray MaterialTextureParamInfos; + TArray MaterialTextureParamGuids; + MaterialInterface->GetAllTextureParameterInfo(MaterialTextureParamInfos, MaterialTextureParamGuids); + + for(auto& CurTextureParam : MaterialTextureParamInfos) + { + FString CurTextureParamName = ParamPrefix + CurTextureParam.Name.ToString(); + UTexture* CurTexture = nullptr; + MaterialInterface->GetTextureParameterValue(CurTextureParam, CurTexture); + + FString TexturePath = IsValid(CurTexture) ? CurTexture->GetPathName() : TEXT(""); + + MaterialInfo.TextureParameters.Add(CurTextureParamName, TexturePath); + } + } + + // Collect all bool parameters in this material + { + TArray MaterialBoolParamInfos; + TArray MaterialBoolParamGuids; + MaterialInterface->GetAllStaticSwitchParameterInfo(MaterialBoolParamInfos, MaterialBoolParamGuids); + + for(auto& CurBoolParam : MaterialBoolParamInfos) + { + FString CurBoolParamName = ParamPrefix + CurBoolParam.Name.ToString(); + bool CurBool = false; + FGuid CurExprValue; + MaterialInterface->GetStaticSwitchParameterValue(CurBoolParam, CurBool, CurExprValue); + + MaterialInfo.BoolParameters.Add(CurBoolParamName, CurBool); + } + } + } + + return true; +} + + +bool FUnrealMeshTranslator::GetOrCreateMaterialTableNode( + FUnrealMeshExportData& ExportData, + const TArray& MaterialInfos) +{ + // Get or create the GeoNode. + bool bCreated = false; + HAPI_NodeId GeoNodeId = ExportData.GetOrCreateConstructionGeoNode(bCreated, MaterialTableName, EUnrealObjectInputNodeType::Leaf); + if(GeoNodeId == INDEX_NONE) + return false; + + // If we already created the geo node, we don't have to recreate the internal nodes. + if(!bCreated) + return true; + + HAPI_NodeId MaterialNodeId; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(GeoNodeId, TEXT("null"), TEXT("material_node"), false, &MaterialNodeId), false); + + ExportData.RegisterConstructionNode(MaterialTableName, MaterialNodeId); + + // Create part. + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + Part.pointCount = MaterialInfos.Num(); + Part.vertexCount = 3 * MaterialInfos.Num(); + Part.faceCount = MaterialInfos.Num(); + Part.type = HAPI_PARTTYPE_MESH; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo(FHoudiniEngine::Get().GetSession(), MaterialNodeId, 0, &Part), false); + + { + // Create POS point attribute info. We won't use it. + HAPI_AttributeInfo AttributeInfoPos; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPos); + AttributeInfoPos.count = 3; + AttributeInfoPos.tupleSize = 3; + AttributeInfoPos.exists = true; + AttributeInfoPos.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPos.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPos.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), MaterialNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPos), false); + + TArray Positions; + Positions.SetNumZeroed(AttributeInfoPos.count * AttributeInfoPos.tupleSize); + + FHoudiniHapiAccessor Accessor(MaterialNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + Accessor.SetAttributeData(AttributeInfoPos, Positions); + } + + + TArray VertexListData; + VertexListData.SetNumUninitialized(Part.vertexCount); + for (int Index = 0; Index < VertexListData.Num(); Index++) + { + VertexListData[Index] = Index % 3; + } + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetVertexList(VertexListData, MaterialNodeId, 0), false); + + TArray StaticMeshFaceCounts; + StaticMeshFaceCounts.SetNumUninitialized(Part.faceCount); + + for(int32 n = 0; n < Part.faceCount; n++) + StaticMeshFaceCounts[n] = 3; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetFaceCounts(StaticMeshFaceCounts, MaterialNodeId, 0), false); + + { + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = MaterialInfos.Num(); + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_INT; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + TArray MaterialSlots; + MaterialSlots.SetNum(MaterialInfos.Num()); + for(int Index = 0; Index < MaterialSlots.Num(); Index++) + MaterialSlots[Index] = Index; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), MaterialNodeId, 0, HAPI_UNREAL_ATTRIB_MATERIAL_SLOT, &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(MaterialNodeId, 0, HAPI_UNREAL_ATTRIB_MATERIAL_SLOT); + Accessor.SetAttributeData(AttributeInfo, MaterialSlots); + } + + for (int MaterialIndex = 0; MaterialIndex < MaterialInfos.Num(); MaterialIndex++) + { + const FUnrealMaterialInfo& MaterialInfo = MaterialInfos[MaterialIndex]; + + for (auto It : MaterialInfo.ScalarParameters) + { + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = MaterialInfos.Num(); + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + FString AttributeName = *It.Key; + + FHoudiniEngineUtils::SanitizeHAPIVariableName(AttributeName); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), MaterialNodeId, 0, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(MaterialNodeId, 0, *AttributeName); + Accessor.SetAttributeData(AttributeInfo,&It.Value, MaterialIndex, 1); + } + + for(auto It : MaterialInfo.VectorParameters) + { + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = MaterialInfos.Num(); + AttributeInfo.tupleSize = 4; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + FString AttributeName = *It.Key; + FHoudiniEngineUtils::SanitizeHAPIVariableName(AttributeName); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), MaterialNodeId, 0, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + float Values[4]; + Values[0] = It.Value.R; + Values[1] = It.Value.G; + Values[2] = It.Value.B; + Values[3] = It.Value.A; + + FHoudiniHapiAccessor Accessor(MaterialNodeId, 0, *AttributeName); + Accessor.SetAttributeData(AttributeInfo, Values, MaterialIndex, 1); + } + + for(auto It : MaterialInfo.BoolParameters) + { + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = MaterialInfos.Num(); + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_INT8; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + FString AttributeName = *It.Key; + FHoudiniEngineUtils::SanitizeHAPIVariableName(AttributeName); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), MaterialNodeId, 0, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + int8 Value = It.Value ? 1 : 0; + FHoudiniHapiAccessor Accessor(MaterialNodeId, 0, *AttributeName); + Accessor.SetAttributeData(AttributeInfo, &Value, MaterialIndex, 1); + } + + for(auto It : MaterialInfo.TextureParameters) + { + HAPI_AttributeInfo AttributeInfo; + FHoudiniApi::AttributeInfo_Init(&AttributeInfo); + AttributeInfo.count = MaterialInfos.Num(); + AttributeInfo.tupleSize = 1; + AttributeInfo.exists = true; + AttributeInfo.owner = HAPI_ATTROWNER_PRIM; + AttributeInfo.storage = HAPI_STORAGETYPE_STRING; + AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + FString AttributeName = *It.Key; + FHoudiniEngineUtils::SanitizeHAPIVariableName(AttributeName); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), MaterialNodeId, 0, H_TCHAR_TO_UTF8(*AttributeName), &AttributeInfo), false); + + FHoudiniHapiAccessor Accessor(MaterialNodeId, 0, *AttributeName); + Accessor.SetAttributeData(AttributeInfo, &It.Value, MaterialIndex, 1); + } + } + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(MaterialNodeId), {}); + + HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + if(!FHoudiniEngineUtils::HapiCookNode(MaterialNodeId, &CookOptions, true)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to cook node!")); + } + + return true; +} + +bool FUnrealMeshTranslator::GetOrCreateMaterialZipNode( + HAPI_NodeId& AttribCopyNodeId, + const HAPI_NodeId ParentNodeId, + const HAPI_NodeId MeshNode, + const HAPI_NodeId MaterialTableNode, + const TArray& MaterialInfos) +{ + FStringBuilderBase AttributesToCopy; + + for (const FUnrealMaterialInfo & Info : MaterialInfos) + { + for(auto ScalarParam : Info.ScalarParameters) + { + AttributesToCopy.Append(ScalarParam.Key); + AttributesToCopy.Append(TEXT(" ")); + } + + for(auto VectorParam : Info.VectorParameters) + { + AttributesToCopy.Append(VectorParam.Key); + AttributesToCopy.Append(TEXT(" ")); + } + + for(auto TextureParam : Info.TextureParameters) + { + AttributesToCopy.Append(TextureParam.Key); + AttributesToCopy.Append(TEXT(" ")); + } + + for(auto BoolParam : Info.BoolParameters) + { + AttributesToCopy.Append(BoolParam.Key); + AttributesToCopy.Append(TEXT(" ")); + } + } + TArray Attribs = HoudiniTCHARToUTF(AttributesToCopy.ToString()); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(ParentNodeId, TEXT("attribcopy"), TEXT("attrib_copy"), false, &AttribCopyNodeId), false); + + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + HAPI_ParmId ParmId = -1; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(Session, AttribCopyNodeId, "srcgrouptype", 0, 2), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(Session, AttribCopyNodeId, "destgrouptype", 0, 2), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(Session, AttribCopyNodeId, "matchbyattribute", 0, 1), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(Session, AttribCopyNodeId, "matchbyattributemethod", 0, 1), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(Session, AttribCopyNodeId, "attrib", 0, 2), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmIdFromName(Session, AttribCopyNodeId, "attributetomatch", &ParmId), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue(Session, AttribCopyNodeId, HAPI_UNREAL_ATTRIB_MATERIAL_SLOT, ParmId, 0), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmIdFromName(Session, AttribCopyNodeId, "attribname", &ParmId), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue(Session, AttribCopyNodeId, Attribs.GetData(), ParmId, 0), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(Session, AttribCopyNodeId, 0, MeshNode, 0), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(Session, AttribCopyNodeId, 1, MaterialTableNode, 0), false); + + return true; +} + +bool FUnrealMeshExportData::ScanForExistingNodesInHoudini() +{ + // This function looks in the top level Geo node in Houdini to see which nodes already exist. + // We currently do this on each Unreal->Houdini export, possibly we could keep track of this + // per Mesh, but this also allows us to cache data from existing sessions? + + int ChildCount = 0; + + HAPI_NodeId ParentNodeId = GetConstructionSubnetNodeId(); + if(ParentNodeId == INDEX_NONE) + return true; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), + ParentNodeId, + HAPI_NODETYPE_ANY, + HAPI_NODEFLAGS_ANY, + false, + &ChildCount), false); + + if(ChildCount == 0) + return true; + + // Retrieve all the display node ids + TArray ChildNodeIds; + ChildNodeIds.SetNum(ChildCount); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), + ParentNodeId, + ChildNodeIds.GetData(), + ChildCount), false); + + // See what we have + + for(int ChildNodeId : ChildNodeIds) + { + FString NodeLabel; + FHoudiniEngineUtils::GetHoudiniAssetName(ChildNodeId, NodeLabel); + ExistingUnassignedHAPINodes.Add(NodeLabel, ChildNodeId); + } + return true; +} + +bool FUnrealMeshTranslator::GetOrCreateStaticMeshLODGeometries( + FUnrealMeshExportData& ExportData, + const UStaticMesh* StaticMesh, + const FUnrealMeshExportOptions& ExportOptions, + EHoudiniMeshSource MeshSource) +{ + if(ExportOptions.bMainMesh) + { + FString Label = MakeLODName(0, MeshSource); + + if(!ExportData.Contains(Label)) + { + const bool bAddLODGroups = ExportOptions.bLODs; + GetOrCreateExportStaticMeshLOD(ExportData, 0, bAddLODGroups, StaticMesh, MeshSource); + } + } + + if(ExportOptions.bLODs) + { + int NumLODs = StaticMesh->GetNumLODs(); + + for(int LODIndex = 0; LODIndex < NumLODs; LODIndex++) + { + FString NodeLabel = MakeLODName(LODIndex, MeshSource); + if(!ExportData.Contains(NodeLabel)) + { + const bool bAddLODGroups = true; + GetOrCreateExportStaticMeshLOD(ExportData, LODIndex, bAddLODGroups, StaticMesh, MeshSource); + } + } + } + + return true; +} + + +bool FUnrealMeshTranslator::CreateInputNodeForStaticMeshNew( + HAPI_NodeId& InputObjectNodeId, + FUnrealObjectInputHandle& OutHandle, + const UStaticMesh* StaticMesh, + const UStaticMeshComponent* StaticMeshComponent, + const FString& InputNodeName, + const FUnrealMeshExportOptions& ExportOptions, + const bool bInputNodesCanBeDeleted) +{ + FUnrealObjectInputHandle StaticMeshHandle; + + FHoudiniPerfTimer PerfTimer(TEXT("Create Static Mesh Input Nodes"), true); + PerfTimer.Start(); + + if (IsValid(StaticMeshComponent) && StaticMeshComponent->IsA()) + { + // Spline Mesh requires special handling, since its geometry is per-component. + + FUnrealObjectInputHandle ComponentHandle; + + bool bSuccess = CreateInputNodeForSplineMeshComponentNew( + InputObjectNodeId, + ComponentHandle, + Cast(StaticMeshComponent), + ExportOptions, + bInputNodesCanBeDeleted); + + if(bSuccess) + { + OutHandle = std::move(ComponentHandle); + } + } + else + { + // Static Mesh with optional component. + + bool bSuccess = CreateInputNodeForStaticMeshNew( + InputObjectNodeId, + StaticMeshHandle, + StaticMesh, + InputNodeName, + ExportOptions, + bInputNodesCanBeDeleted); + + if(!bSuccess) + return false; + + if(StaticMeshComponent) + { + FUnrealObjectInputHandle ComponentHandle; + + bSuccess = CreateInputNodeForStaticMeshComponentNew( + InputObjectNodeId, + ComponentHandle, + StaticMeshHandle, + StaticMeshComponent, + InputNodeName, + ExportOptions, + bInputNodesCanBeDeleted); + + if(bSuccess) + { + OutHandle = ComponentHandle; + } + } + else + { + OutHandle = StaticMeshHandle; + } + + } + + return true; +} + +bool FUnrealMeshTranslator::CreateInputNodeForStaticMeshNew( + HAPI_NodeId& InputObjectNodeId, + FUnrealObjectInputHandle& OutHandle, + const UStaticMesh* StaticMesh, + const FString& InputNodeName, + const FUnrealMeshExportOptions& ExportOptions, + const bool bInputNodesCanBeDeleted) +{ + // ExportData contains information about the mesh being constructed. + FUnrealMeshExportData ExportData(StaticMesh, bInputNodesCanBeDeleted); + + FString MeshLabel; + bool bSuccess = GetOrConstructStaticMesh(MeshLabel, ExportData, ExportOptions, StaticMesh); + if(!bSuccess || !ExportData.Contains(MeshLabel)) + return false; + + // Fetch the construction results. + InputObjectNodeId = ExportData.GetHapiNodeId(MeshLabel); + OutHandle = ExportData.GetNodeHandle(MeshLabel); + + return true; +} + +FString FUnrealMeshExportData::CleanInputPath(const FString & ObjectPath) +{ + FString Path = ObjectPath.Replace(TEXT(":"), TEXT("/")).Replace(TEXT("."), TEXT("/")); + return Path; +} + +void FUnrealMeshExportData::EnsureConstructionSubnetExists() +{ + // Just add a dummy node to make sure parent exists. + FString Path = ConstructionSubnetPath + TEXT("/Dummy"); + + FUnrealObjectInputIdentifier TopLevelIdentifier = FUnrealObjectInputIdentifier(Path); + FUnrealObjectInputUtils::EnsureParentsExist(TopLevelIdentifier, ConstructionSubnetHandle, bCanDelete); + ConstructionSubnetNodeId = FUnrealObjectInputUtils::GetHAPINodeId(ConstructionSubnetHandle); +} + +bool FUnrealMeshTranslator::GetOrCreateExportStaticMeshLOD( + FUnrealMeshExportData& ExportData, + const int LODIndex, + const bool bAddLODGroups, + const UStaticMesh* StaticMesh, + const EHoudiniMeshSource RequestedMeshSource) + +{ + FString LODName = MakeLODName(LODIndex, RequestedMeshSource); + + bool bCreated = false; + HAPI_NodeId GeoNodeId = ExportData.GetOrCreateConstructionGeoNode(bCreated, LODName, EUnrealObjectInputNodeType::Leaf); + if(GeoNodeId == INDEX_NONE) + return false; + + // If the geo node already existed, don't recreate it. + if(!bCreated) + return true; + + HAPI_NodeId NodeId; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CreateNode(FHoudiniEngine::Get().GetSession(), GeoNodeId, "null", H_TCHAR_TO_UTF8(*LODName), true, &NodeId), false); + + ExportData.RegisterConstructionNode(LODName, NodeId); + + // Try to use the prefered mesh source. Not all options are available on every mesh, so provide fallbacks. + EHoudiniMeshSource MeshSource = RequestedMeshSource; + + if (MeshSource == EHoudiniMeshSource::HiResMeshDescription) + { + if (StaticMesh->GetHiResMeshDescription() == nullptr) + { + // Something has gone wrong! + HOUDINI_LOG_ERROR(TEXT("Bad Mesh Descriptor")); + MeshSource = EHoudiniMeshSource::MeshDescription; + } + } + + if(MeshSource == EHoudiniMeshSource::MeshDescription) + { + if(StaticMesh->GetMeshDescription(LODIndex) == nullptr) + { + HOUDINI_LOG_MESSAGE(TEXT("No MeshDescription, falling back to LOD Resource. %s "), *StaticMesh->GetPathName()); + MeshSource = EHoudiniMeshSource::LODResource; + } + } + + bool bSuccess = false; + + switch(MeshSource) + { + case EHoudiniMeshSource::LODResource: + bSuccess = FUnrealMeshTranslator::CreateInputNodeForStaticMeshLODResources( + NodeId, + StaticMesh->GetLODForExport(LODIndex), + LODIndex, + bAddLODGroups, + false, + StaticMesh, + nullptr); + break; + case EHoudiniMeshSource::MeshDescription: + bSuccess = FUnrealMeshTranslator::CreateInputNodeForMeshDescription( + NodeId, + *StaticMesh->GetMeshDescription(LODIndex), + LODIndex, + bAddLODGroups, + false, + StaticMesh, + nullptr); + break; + + case EHoudiniMeshSource::HiResMeshDescription: + bSuccess = FUnrealMeshTranslator::CreateInputNodeForMeshDescription( + NodeId, + *StaticMesh->GetHiResMeshDescription(), + LODIndex, + bAddLODGroups, + false, + StaticMesh, + nullptr); + break; + + default: + break; + } + + return bSuccess; +} + +//TODO: Fix spline meshes + + +TArray FUnrealMeshTranslator::GetMaterials(const UStaticMesh* StaticMesh) +{ + const TArray& StaticMaterials = StaticMesh->GetStaticMaterials(); + + TArray Results; + + UMaterialInterface* UEDefaultMaterial = UMaterial::GetDefaultMaterial(EMaterialDomain::MD_Surface); + + for(int32 MaterialIndex = 0; MaterialIndex < StaticMaterials.Num(); ++MaterialIndex) + { + const FStaticMaterial& MaterialInfo = StaticMaterials[MaterialIndex]; + UMaterialInterface* Material = MaterialInfo.MaterialInterface; + + // If the Material is NULL or invalid, fallback to the default material + if(!IsValid(Material)) + { + Material = UEDefaultMaterial; + HOUDINI_LOG_WARNING(TEXT("Material Index %d (slot %s) has an invalid material, falling back to default: %s"), MaterialIndex, *(MaterialInfo.MaterialSlotName.ToString()), *(UEDefaultMaterial->GetPathName())); + } + // MaterialSlotToInterface.Add(MaterialInfo.ImportedMaterialSlotName, MaterialIndex); + Results.Add(Material); + } + + return Results; +} +bool FUnrealMeshTranslator::GetOrConstructStaticMeshGeometryNode( + FString& GeometryLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const UStaticMesh* StaticMesh) +{ + EHoudiniMeshSource MeshSource = DetermineMeshSource(ExportOptions, StaticMesh); + + // Create all low-level geometry nodes required by these export options. For example, lod0, lod1 + bool bSuccess = GetOrCreateStaticMeshLODGeometries(ExportData, StaticMesh, ExportOptions, MeshSource); + if(!bSuccess) + return false; + + // Constructs the geometry nodes for the current mesh, if needed. Usuall the main mesh and/or lods and/or + // hires mesh. + if (ExportOptions.bMainMesh && !ExportOptions.bLODs) + { + // If we are just constructing the main mesh and no LODs, we don't need to create an extra + // merge node, just return the current node. + + GeometryLabel = MakeLODName(0, MeshSource); + + return true; + } + else if(ExportOptions.bLODs) + { + // Combine all LODs and return that node. + + GeometryLabel = TEXT("all_lods_") + MakeMeshSourceStr(MeshSource); + + if(ExportData.Contains(GeometryLabel)) + return true; + + TSet NodeIds; + + // Add each LOD... ignore LOD0, if its needed it will already have been added. + auto & Handles = ExportData.GetConstructionHandles(); + for (int LODIndex = 0; LODIndex < StaticMesh->GetNumLODs(); LODIndex++) + { + FString LODName = MakeLODName(LODIndex, MeshSource); + if (Handles.Contains(LODName)) + { + NodeIds.Add(Handles[LODName]); + } + } + + // Create the geo node, but if it already exists just re-use it. + + bool bCreated = false; + HAPI_NodeId GeoNode = ExportData.GetOrCreateConstructionGeoNode(bCreated, GeometryLabel, EUnrealObjectInputNodeType::Reference); + if(GeoNode == INDEX_NONE) + return false; + + if(!bCreated) + return true; + + HAPI_NodeId NodeId; + bSuccess = CreateMergeNode(NodeId, GeometryLabel, GeoNode, GetHapiNodeIds(NodeIds.Array())); + + ExportData.RegisterConstructionNode(GeometryLabel, NodeId, &NodeIds); + return bSuccess; + } + return false; +} + +bool FUnrealMeshTranslator::GetOrConstructStaticMeshRenderNode( + FString& RenderMeshLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const UStaticMesh* StaticMesh) +{ + + // Get or create the geometry node for this set of export options. + FString GeometryLabel; + bool bSuccess = GetOrConstructStaticMeshGeometryNode( + GeometryLabel, + ExportData, + ExportOptions, + StaticMesh); + + if(!bSuccess) + return false; + + if (ExportOptions.bMaterialParameters) + { + // Fetch Materials + TArray MaterialInterfaces = FUnrealMeshTranslator::GetMaterials(StaticMesh); + TArray MaterialInfos; + GetMaterialInfo(MaterialInterfaces, MaterialInfos); + + + // Material Table. + if(!ExportData.Contains(MaterialTableName)) + { + GetOrCreateMaterialTableNode(ExportData, MaterialInfos); + } + + // if we need material parameters create a new node and zip the geometry and materials + FStringBuilderBase StringBuilder; + StringBuilder.Append(GeometryLabel); + StringBuilder.Append(TEXT("_mparams")); + + RenderMeshLabel = StringBuilder.ToString(); + + TSet References; + References.Add(ExportData.GetNodeHandle(GeometryLabel)); + References.Add(ExportData.GetNodeHandle(MaterialTableName)); + + // Get or create the geo node. If it already exists, don't recreate it. + bool bCreated = false; + HAPI_NodeId GeoNodeId = ExportData.GetOrCreateConstructionGeoNode(bCreated, RenderMeshLabel, EUnrealObjectInputNodeType::Reference); + if(GeoNodeId == INDEX_NONE) + return false; + + if(!bCreated) + return true; + + HAPI_NodeId ZipNodeId; + + bSuccess = GetOrCreateMaterialZipNode( + ZipNodeId, + GeoNodeId, + ExportData.GetHapiNodeId(GeometryLabel), + ExportData.GetHapiNodeId(MaterialTableName), + MaterialInfos); + + ExportData.RegisterConstructionNode(RenderMeshLabel, ZipNodeId, &References); + + return bSuccess; + } + else + { + RenderMeshLabel = GeometryLabel; + } + + return true; +} + +FString FUnrealMeshTranslator::MakeUniqueExportName(const FUnrealMeshExportOptions& ExportOptions) +{ + FStringBuilderBase LabelBuilder; + LabelBuilder.Append("final"); + + if(ExportOptions.bMainMesh) + LabelBuilder.Append(TEXT("_main")); + + if(ExportOptions.bLODs) + LabelBuilder.Append(TEXT("_lods")); + + if(ExportOptions.bColliders) + LabelBuilder.Append(TEXT("_colliders")); + + if(ExportOptions.bSockets) + LabelBuilder.Append(TEXT("_sockets")); + + if(ExportOptions.bPreferNaniteFallbackMesh) + LabelBuilder.Append(TEXT("_nanite")); + + if(ExportOptions.bMaterialParameters) + LabelBuilder.Append(TEXT("_materialparams")); + + return LabelBuilder.ToString(); +} + +bool FUnrealMeshTranslator::GetOrConstructStaticMesh( + FString& MeshLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const UStaticMesh* StaticMesh) +{ + MeshLabel = MakeUniqueExportName(ExportOptions); + + // Get or create the geo node. Don't construct internal nodes if it already exists. + bool bCreated = false; + HAPI_NodeId GeoNode = ExportData.GetOrCreateConstructionGeoNode(bCreated, MeshLabel, EUnrealObjectInputNodeType::Reference); + if(GeoNode == INDEX_NONE) + return false; + + if(!bCreated) + return true; + + TSet ReferencedNodes; + if(ExportOptions.bLODs || ExportOptions.bMainMesh) + { + FString RenderMesh; + bool bSuccess = GetOrConstructStaticMeshRenderNode(RenderMesh, ExportData, ExportOptions, StaticMesh); + if(!bSuccess) + return false; + + ReferencedNodes.Add(ExportData.GetNodeHandle(RenderMesh)); + } + + if (ExportOptions.bColliders) + { + FString CollisionLabel; + bool bSuccess = GetOrConstructCollisions(CollisionLabel, ExportData, ExportOptions, StaticMesh); + if(!bSuccess) + return false; + + ReferencedNodes.Add(ExportData.GetNodeHandle(CollisionLabel)); + } + + if(ExportOptions.bSockets) + { + FString SocketsLabel; + bool bSuccess = GetOrConstructSockets(SocketsLabel, ExportData, ExportOptions, StaticMesh); + if(!bSuccess) + return false; + + ReferencedNodes.Add(ExportData.GetNodeHandle(SocketsLabel)); + } + + HAPI_NodeId NodeId; + bool bSuccess = CreateMergeNode(NodeId, MeshLabel, GeoNode, GetHapiNodeIds(ReferencedNodes.Array())); + + ExportData.RegisterConstructionNode(MeshLabel, NodeId, &ReferencedNodes); + + if(!bSuccess || GeoNode == INDEX_NONE) + return false; + + return bSuccess; +} + +bool FUnrealMeshTranslator::GetOrConstructSplineMeshComponent( + FString& MeshLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const USplineMeshComponent* SplineMeshComponent) +{ + UStaticMesh* StaticMesh = SplineMeshComponent->GetStaticMesh(); + if(!IsValid(StaticMesh)) + return true; + + MeshLabel = MakeUniqueExportName(ExportOptions); + + // Get or create the geo node. Don't construct internal nodes if it already exists. + bool bCreated = false; + HAPI_NodeId GeoNode = ExportData.GetOrCreateConstructionGeoNode(bCreated, MeshLabel, EUnrealObjectInputNodeType::Reference); + if(GeoNode == INDEX_NONE) + return false; + + if(!bCreated) + return true; + + if(ExportOptions.bMaterialParameters) + { + // Fetch Materials + TArray MaterialInterfaces = FUnrealMeshTranslator::GetMaterials(StaticMesh); + TArray MaterialInfos; + GetMaterialInfo(MaterialInterfaces, MaterialInfos); + + // Material Table. + if(!ExportData.Contains(MaterialTableName)) + { + GetOrCreateMaterialTableNode(ExportData, MaterialInfos); + } + } + + TSet ReferencedNodes; + if(ExportOptions.bLODs || ExportOptions.bMainMesh) + { + FString RenderMesh; + bool bSuccess = GetOrConstructSplineMeshRenderNode(RenderMesh, ExportData, ExportOptions, SplineMeshComponent); + if(!bSuccess) + return false; + + ReferencedNodes.Add(ExportData.GetNodeHandle(RenderMesh)); + } + + if(ExportOptions.bColliders) + { + FString CollisionLabel; + bool bSuccess = GetOrConstructCollisions(CollisionLabel, ExportData, ExportOptions, StaticMesh); + if(!bSuccess) + return false; + + ReferencedNodes.Add(ExportData.GetNodeHandle(CollisionLabel)); + } + + if(ExportOptions.bSockets) + { + FString SocketsLabel; + bool bSuccess = GetOrConstructSockets(SocketsLabel, ExportData, ExportOptions, StaticMesh); + if(!bSuccess) + return false; + + ReferencedNodes.Add(ExportData.GetNodeHandle(SocketsLabel)); + } + + HAPI_NodeId NodeId; + bool bSuccess = CreateMergeNode(NodeId, MeshLabel, GeoNode, GetHapiNodeIds(ReferencedNodes.Array())); + + ExportData.RegisterConstructionNode(MeshLabel, NodeId, &ReferencedNodes); + + if(!bSuccess || GeoNode == INDEX_NONE) + return false; + + return bSuccess; + return true; +} + +bool FUnrealMeshTranslator::CreateMergeNode( + HAPI_NodeId& NodeId, + const FString& NodeLabel, + const HAPI_NodeId ParentNodeId, + const TArray& Inputs) +{ + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(ParentNodeId, TEXT("merge"), NodeLabel, true, &NodeId), false); + + for(int Index = 0; Index < Inputs.Num(); Index++) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(FHoudiniEngine::Get().GetSession(), NodeId, Index, Inputs[Index], 0), false); + } + + return true; + +} + +FString FUnrealMeshTranslator::MakeMeshSourceStr(EHoudiniMeshSource Source) +{ + FString SourceString = TEXT(""); + switch(Source) + { + case EHoudiniMeshSource::LODResource: + SourceString = TEXT("_lodresource"); + break; + case EHoudiniMeshSource::MeshDescription: + SourceString = TEXT("_meshdesc"); + break; + case EHoudiniMeshSource::HiResMeshDescription: + SourceString = TEXT("_hiresmeshdesc"); + break; + default: + SourceString = TEXT(""); + break; + } + return SourceString; +} + +FString FUnrealMeshTranslator::MakeLODName(int LODIndex, EHoudiniMeshSource Source) +{ + FString SourceString = MakeMeshSourceStr(Source); + FString Result = FString::Printf(TEXT("%s%d_%s"), *LODPrefix, LODIndex, *SourceString); + return Result; +} + + +bool FUnrealMeshTranslator::GetOrConstructCollisions( + FString& CollisionsLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const UStaticMesh* Mesh) +{ + CollisionsLabel = TEXT("collisions"); + + bool bCreated = false; + HAPI_NodeId GeoNode = ExportData.GetOrCreateConstructionGeoNode(bCreated, CollisionsLabel, EUnrealObjectInputNodeType::Leaf); + if(GeoNode == INDEX_NONE) + return false; + + if(!bCreated) + return false; + + HAPI_NodeId MergeNodeId = INDEX_NONE; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(GeoNode, TEXT("merge"), *CollisionsLabel, true, &MergeNodeId), false); + + ExportData.RegisterConstructionNode(CollisionsLabel, MergeNodeId); + + int NextMergeIndex = 0; + bool bSuccess = ExportCollisions(NextMergeIndex, Mesh, MergeNodeId, GeoNode, Mesh->GetBodySetup()->AggGeom); + if(!bSuccess) + return bSuccess; + + return bSuccess; +} + +bool FUnrealMeshTranslator::GetOrConstructSockets( + FString& SocketsLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const UStaticMesh* Mesh) +{ + SocketsLabel = TEXT("sockets"); + + bool bCreated = false; + HAPI_NodeId GeoNode = ExportData.GetOrCreateConstructionGeoNode(bCreated, SocketsLabel, EUnrealObjectInputNodeType::Leaf); + if(GeoNode == INDEX_NONE) + return false; + + if(!bCreated) + return true; + + // Create an input node for the mesh sockets + HAPI_NodeId SocketsNodeId = -1; + bool bSuccess = CreateInputNodeForMeshSockets(Mesh->Sockets, GeoNode, SocketsNodeId); + if(!bSuccess) + return bSuccess; + + ExportData.RegisterConstructionNode(SocketsLabel, SocketsNodeId); + + return bSuccess; +} + +bool FUnrealMeshExportData::Contains(const FString& Label) +{ + return RegisteredHandles.Contains(Label); +} + + +HAPI_NodeId FUnrealMeshExportData::GetHapiNodeId(const FString& Label) +{ + HAPI_NodeId NodeId = INDEX_NONE; + + FUnrealObjectInputUtils::GetHAPINodeId(RegisteredHandles[Label], NodeId); + return NodeId; +} + +const TMap& FUnrealMeshExportData::GetConstructionHandles() +{ + return RegisteredHandles; +} + +FUnrealMeshExportData::FUnrealMeshExportData(const UObject* Object, bool bInCanDoDelete) +{ + FString ObjectPath = Object->GetPathName(); + bCanDelete = bInCanDoDelete; + + ConstructionSubnetPath = CleanInputPath(ObjectPath); + + EnsureConstructionSubnetExists(); + + ScanForExistingNodesInHoudini(); +} + + +HAPI_NodeId FUnrealMeshExportData::GetOrCreateConstructionGeoNode( + bool& bCreated, + const FString& Label, + EUnrealObjectInputNodeType NodeType) +{ + bCreated = false; + + // Have we already seen this identifier and registered it? If so, just return it. + if (RegisteredIdentifiers.Contains(Label)) + { + FUnrealObjectInputIdentifier Identifier = RegisteredIdentifiers[Label]; + ensure(RegisteredHandles.Contains(Label)); + ensure(RegisteredGeoNodes.Contains(Label)); + + HAPI_NodeId NodeId = INDEX_NONE; + FUnrealObjectInputUtils::GetHAPINodeId(Identifier, NodeId); + return NodeId; + + } + + FUnrealObjectInputIdentifier Identifier = MakeNodeIdentifier(Label, NodeType); + RegisteredIdentifiers.Add(Label, Identifier); + + // Is there a HAPI node for this label which isn't registered? Is so, fetch the Handle and register it. + if (ExistingUnassignedHAPINodes.Contains(Label)) + { + HAPI_NodeId GeoNodeId = ExistingUnassignedHAPINodes[Label]; + ExistingUnassignedHAPINodes.Remove(Label); + + FUnrealObjectInputHandle Handle; + bool bSuccess = FUnrealObjectInputUtils::FindNodeViaManager(Identifier, Handle); + if(bSuccess) + { + RegisteredHandles.Add(Label, Handle); + RegisteredGeoNodes.Add(Label, GeoNodeId); + return GeoNodeId; + } + else + { + // This means we found a node that the reference input system knows nothing about. We need to overwrite it, + // so delete it. + FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), GeoNodeId); + } + } + + // IF we get her, we'll create a new node. + + bCreated = true; + HAPI_NodeId GeoNodeId = INDEX_NONE; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(GetConstructionSubnetNodeId(), TEXT("geo"), Label, true, &GeoNodeId), INDEX_NONE); + + RegisteredGeoNodes.Add(Label, GeoNodeId); + + return GeoNodeId; +} + +HAPI_NodeId FUnrealMeshExportData::RegisterConstructionNode( + const FString& Label, + const HAPI_NodeId NodeId, + const TSet* ReferencedNodes) +{ + // This function must be called if GetOrCreateConstructionGeoNode() returned with bCreated == true. + // For reasons which are not entirely clear to me FUnrealObjectInputUtils::AddNodeOrUpdateNode() + // requires both an Geo (Object) node and an internal SOP. Since we cannot create the internal SOP + // until after we create the GEO we must call GetOrCreateConstructionGeoNode() first, then call RegisterConstructionNode() + // later. + + // If this call crashes, you didn't call GetOrCreateConstructionGeoNode() :^) + + FUnrealObjectInputIdentifier* FoundId = RegisteredIdentifiers.Find(Label); + ensure(FoundId); + HAPI_NodeId GeoNodeId = RegisteredGeoNodes[Label]; + + FUnrealObjectInputHandle Handle; + + if (FoundId) + { + FUnrealObjectInputUtils::AddNodeOrUpdateNode(*FoundId, NodeId, Handle, GeoNodeId, ReferencedNodes, bCanDelete); + } + + RegisteredHandles.Add(Label, Handle); + + return GeoNodeId; +} + +FUnrealObjectInputIdentifier FUnrealMeshExportData::MakeNodeIdentifier(const FString& Label, EUnrealObjectInputNodeType NodeType) +{ + FString FullPath = FString::Printf(TEXT("%s/%s"), *ConstructionSubnetPath, *Label); + FullPath = FullPath.Replace(TEXT("."), TEXT("/")); + + // Why does the Id need a type? Sure its a property of the Handle or internal node? I am perplexed. + FUnrealObjectInputIdentifier Id = FUnrealObjectInputIdentifier(FullPath, NodeType); + RegisteredIdentifiers.Add(Label, Id); + return Id; +} + +FUnrealObjectInputHandle FUnrealMeshExportData::GetNodeHandle(const FString& Label) +{ + FUnrealObjectInputHandle* Found = RegisteredHandles.Find(Label); + if(Found) + return *Found; + else + return FUnrealObjectInputHandle(); +} + +TArray GetHapiNodeIds(const TArray& Identifiers) +{ + TArray Results; + for (auto Id : Identifiers) + { + Results.Add(GetHapiNodeId(Id)); + } + return Results; +} + +HAPI_NodeId GetHapiNodeId(FUnrealObjectInputIdentifier Identifier) +{ + HAPI_NodeId NodeId = INDEX_NONE; + FUnrealObjectInputUtils::GetHAPINodeId(Identifier, NodeId); + return NodeId; +} + +TArray GetHapiNodeIds(const TArray& Handles) +{ + TArray Results; + for(auto Handle : Handles) + { + Results.Add(GetHapiNodeId(Handle)); + } + return Results; +} + +HAPI_NodeId GetHapiNodeId(FUnrealObjectInputHandle Handle) +{ + HAPI_NodeId NodeId = INDEX_NONE; + FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId); + return NodeId; +} + + +bool FUnrealMeshTranslator::CreateInputNodeForStaticMeshComponentNew( + HAPI_NodeId& InputObjectNodeId, + FUnrealObjectInputHandle& OutHandle, + const FUnrealObjectInputHandle& StaticMeshHandle, + const UStaticMeshComponent* StaticMeshComponent, + const FString& InputNodeName, + const FUnrealMeshExportOptions& ExportOptions, + const bool bInputNodesCanBeDeleted) +{ + FString TopLevelNodePath = *StaticMeshComponent->GetPathName(); + FUnrealObjectInputHandle ParentHandle; + + FUnrealObjectInputIdentifier TopLevelIdentifier = FUnrealObjectInputIdentifier(TopLevelNodePath); + FUnrealObjectInputUtils::EnsureParentsExist(TopLevelIdentifier, ParentHandle, bInputNodesCanBeDeleted); + HAPI_NodeId ParentNodeId = FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle); + + FString GeoNodeLabel = TEXT("component"); + + HAPI_NodeId GeoNode; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(ParentNodeId, TEXT("geo"), GeoNodeLabel, true, &GeoNode), false); + + TSet References; + References.Add(StaticMeshHandle); + + HAPI_NodeId NodeId; + bool bSuccess = CreateMergeNode(NodeId, TEXT("static_mesh"), GeoNode, GetHapiNodeIds(References.Array())); + + FString FullPath = FString::Printf(TEXT("%s/%s"), *TopLevelNodePath, *GeoNodeLabel); + FUnrealObjectInputIdentifier Id = FUnrealObjectInputIdentifier(FullPath, EUnrealObjectInputNodeType::Reference); + + FUnrealObjectInputUtils::AddNodeOrUpdateNode(Id, NodeId, OutHandle, GeoNode, &References, true); + + return true; +} + +EHoudiniMeshSource FUnrealMeshTranslator::DetermineMeshSource(const FUnrealMeshExportOptions& ExportOptions, const UStaticMesh* StaticMesh) +{ + bool bAllMeshDescriptionValid = true; + for (int LODIndex = 0; LODIndex < StaticMesh->GetNumLODs(); LODIndex++) + { + if (StaticMesh->GetMeshDescription(LODIndex) == nullptr) + { + bAllMeshDescriptionValid = false; + break; + } + } + + // If any LOD is missing a mesh description, use the LOD Resources instead. + // Missing Mesh Descriptions can happen for automatically generated LODs. + // But we should make the LODResource and Mesh Description export data the same, then we can mix and match. + + if(!bAllMeshDescriptionValid) + return EHoudiniMeshSource::LODResource; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + if (StaticMesh->IsNaniteEnabled()) +#else + if (StaticMesh->NaniteSettings.bEnabled) +#endif + { + if (ExportOptions.bPreferNaniteFallbackMesh) + { + if (StaticMesh->GetRenderData()->LODResources.Num()) + { + return EHoudiniMeshSource::LODResource; + } + else + { + return EHoudiniMeshSource::MeshDescription; + } + } + else + { + if (StaticMesh->GetHiResMeshDescription() != nullptr) + { + return EHoudiniMeshSource::HiResMeshDescription; + } + else + { + return EHoudiniMeshSource::MeshDescription; + } + } + } + else + { + return EHoudiniMeshSource::MeshDescription; + } +} + +bool FUnrealMeshTranslator::CreateInputNodeForSplineMeshComponentNew( + HAPI_NodeId& InputObjectNodeId, + FUnrealObjectInputHandle& OutHandle, + const USplineMeshComponent* StaticMeshComponent, + const FUnrealMeshExportOptions& ExportOptions, + const bool bInputNodesCanBeDeleted) +{ + // ExportData contains information about the mesh being constructed. + FUnrealMeshExportData ExportData(StaticMeshComponent, bInputNodesCanBeDeleted); + + FString ComponentLabel; + bool bSuccess = GetOrConstructSplineMeshComponent(ComponentLabel, ExportData, ExportOptions, StaticMeshComponent); + if(!bSuccess || !ExportData.Contains(ComponentLabel)) + return false; + + // Fetch the construction results. + InputObjectNodeId = ExportData.GetHapiNodeId(ComponentLabel); + OutHandle = ExportData.GetNodeHandle(ComponentLabel); + + return true; +} + +bool FUnrealMeshTranslator::GetOrConstructSplineMeshRenderNode( + FString& RenderMeshLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const USplineMeshComponent* SplineMeshComponent) +{ + + // Get or create the geometry node for this set of export options. + FString GeometryLabel; + bool bSuccess = GetOrConstructSplineMeshGeometryNode( + GeometryLabel, + ExportData, + ExportOptions, + SplineMeshComponent); + + if(!bSuccess) + return false; + + if(ExportOptions.bMaterialParameters) + { + UStaticMesh* StaticMesh = SplineMeshComponent->GetStaticMesh(); + if(!StaticMesh) + return true; + + // Fetch Materials + TArray MaterialInterfaces = FUnrealMeshTranslator::GetMaterials(StaticMesh); + TArray MaterialInfos; + GetMaterialInfo(MaterialInterfaces, MaterialInfos); + + + // Material Table. + if(!ExportData.Contains(MaterialTableName)) + { + GetOrCreateMaterialTableNode(ExportData, MaterialInfos); + } + + // if we need material parameters create a new node and zip the geometry and materials + FStringBuilderBase StringBuilder; + StringBuilder.Append(GeometryLabel); + StringBuilder.Append(TEXT("_mparams")); + + RenderMeshLabel = StringBuilder.ToString(); + + TSet References; + References.Add(ExportData.GetNodeHandle(GeometryLabel)); + References.Add(ExportData.GetNodeHandle(MaterialTableName)); + + // Get or create the geo node. If it already exists, don't recreate it. + bool bCreated = false; + HAPI_NodeId GeoNodeId = ExportData.GetOrCreateConstructionGeoNode(bCreated, RenderMeshLabel, EUnrealObjectInputNodeType::Reference); + if(GeoNodeId == INDEX_NONE) + return false; + + if(!bCreated) + return true; + + HAPI_NodeId ZipNodeId; + + bSuccess = GetOrCreateMaterialZipNode( + ZipNodeId, + GeoNodeId, + ExportData.GetHapiNodeId(GeometryLabel), + ExportData.GetHapiNodeId(MaterialTableName), + MaterialInfos); + + ExportData.RegisterConstructionNode(RenderMeshLabel, ZipNodeId, &References); + + return bSuccess; + } + else + { + RenderMeshLabel = GeometryLabel; + } + + return true; +} + +bool FUnrealMeshTranslator::GetOrConstructSplineMeshGeometryNode( + FString& GeometryLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const USplineMeshComponent* MeshComponent) + +{ + // Create all low-level geometry nodes required by these export options. For example, lod0, lod1 + bool bSuccess = GetOrCreateSplineMeshLODGeometries(ExportData, MeshComponent, ExportOptions); + if(!bSuccess) + return false; + + // Constructs the geometry nodes for the current mesh, if needed. Usuall the main mesh and/or lods and/or + // hires mesh. + + FString LOD0Name = MakeLODName(0, EHoudiniMeshSource::MeshDescription); + + if(ExportOptions.bMainMesh && !ExportOptions.bLODs) + { + GeometryLabel = LOD0Name; + + return true; + } + else if(ExportOptions.bLODs) + { + // Combine all LODs and return that node. + + GeometryLabel = TEXT("all_lods"); + if(ExportData.Contains(GeometryLabel)) + return true; + + TSet NodeIds; + + // Add either the hires mesh or LOD0 + + FString MainMeshName = LOD0Name; + + NodeIds.Add(ExportData.GetNodeHandle(MainMeshName)); + + // Add each LOD... ignore LOD0, if its needed it will already have been added. + + for(auto It : ExportData.GetConstructionHandles()) + { + if(It.Key == LOD0Name) + continue; + + if(It.Key.StartsWith(LODPrefix)) + NodeIds.Add(It.Value); + } + + // Create the geo node, but if it already exists just re-use it. + + bool bCreated = false; + HAPI_NodeId GeoNode = ExportData.GetOrCreateConstructionGeoNode(bCreated, GeometryLabel, EUnrealObjectInputNodeType::Reference); + if(GeoNode == INDEX_NONE) + return false; + + if(!bCreated) + return true; + + HAPI_NodeId NodeId; + bSuccess = CreateMergeNode(NodeId, GeometryLabel, GeoNode, GetHapiNodeIds(NodeIds.Array())); + + ExportData.RegisterConstructionNode(GeometryLabel, NodeId, &NodeIds); + return bSuccess; + } + return false; +} + +bool FUnrealMeshTranslator::GetOrCreateSplineMeshLODGeometries( + FUnrealMeshExportData& ExportData, + const USplineMeshComponent* SplineMeshComponent, + const FUnrealMeshExportOptions& ExportOptions) +{ + UStaticMesh* StaticMesh = SplineMeshComponent->GetStaticMesh(); + if(!IsValid(StaticMesh)) + return true; + + if(ExportOptions.bMainMesh) + { + FString Label = MakeLODName(0, EHoudiniMeshSource::MeshDescription); + + if(!ExportData.Contains(Label)) + { + GetOrCreateExportSplineMeshLOD(ExportData, 0, SplineMeshComponent); + } + } + + if(ExportOptions.bLODs) + { + int NumLODs = StaticMesh->GetNumLODs(); + + for(int LODIndex = 0; LODIndex < NumLODs; LODIndex++) + { + FString NodeLabel = MakeLODName(LODIndex, EHoudiniMeshSource::MeshDescription); + if(!ExportData.Contains(NodeLabel)) + { + GetOrCreateExportSplineMeshLOD(ExportData, LODIndex, SplineMeshComponent); + } + } + } + + return true; +} + +bool FUnrealMeshTranslator::GetOrCreateExportSplineMeshLOD( + FUnrealMeshExportData& ExportData, + const int LODIndex, + const USplineMeshComponent* SplineMeshComponent) + +{ + FString LODName = MakeLODName(LODIndex, EHoudiniMeshSource::MeshDescription); + + bool bCreated = false; + HAPI_NodeId GeoNodeId = ExportData.GetOrCreateConstructionGeoNode(bCreated, LODName, EUnrealObjectInputNodeType::Leaf); + if(GeoNodeId == INDEX_NONE) + return false; + + // If the geo node already existed, don't recreate it. + if(!bCreated) + return true; + + HAPI_NodeId NodeId = INDEX_NONE; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::CreateNode(FHoudiniEngine::Get().GetSession(), GeoNodeId, "null", H_TCHAR_TO_UTF8(*LODName), true, &NodeId), false); + + ExportData.RegisterConstructionNode(LODName, NodeId); + + FMeshDescription MeshDesc; + static constexpr bool bPropagateVertexColours = false; + static constexpr bool bApplyComponentTransform = false; + FHoudiniMeshUtils::RetrieveMesh(MeshDesc, SplineMeshComponent, LODIndex, bPropagateVertexColours, bApplyComponentTransform); + + bool bSuccess = FUnrealMeshTranslator::CreateInputNodeForMeshDescription( + NodeId, + MeshDesc, + LODIndex, + true, + false, + SplineMeshComponent->GetStaticMesh(), + nullptr); + + return bSuccess; +} + +``` + +`Source/HoudiniEngine/Private/UnrealMeshTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "HoudiniEngineRuntimeUtils.h" + +#include "CoreMinimal.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "Components/MeshComponent.h" +#include "Components/SplineMeshComponent.h" +#include "Engine/EngineTypes.h" +#include "Misc/Optional.h" +#include "UObject/ObjectMacros.h" +#include "PhysicalMaterials/PhysicalMaterial.h" + +class UStaticMesh; +class UAnimSequence; +class UStaticMeshComponent; +class UStaticMeshSocket; + +class UMaterialInterface; +class FUnrealObjectInputHandle; +class FHoudiniEngineIndexedStringMap; +class FStaticMeshConstAttributes; +struct FStaticMeshSourceModel; +struct FStaticMeshLODResources; +struct FMeshDescription; +struct FKConvexElem; + + +struct HOUDINIENGINE_API FUnrealMeshExportOptions +{ + bool bLODs = true; + bool bSockets = true; + bool bColliders = true; + bool bMainMesh = true; + bool bMaterialParameters = false; + bool bPreferNaniteFallbackMesh = false; +}; + + +struct HOUDINIENGINE_API FUnrealMaterialInfo +{ + FString MaterialPath; + TMap ScalarParameters; + TMap VectorParameters; + TMap TextureParameters; + TMap BoolParameters; +}; + + +TArray GetHapiNodeIds(const TArray & Identifiers); +HAPI_NodeId GetHapiNodeId(FUnrealObjectInputIdentifier Identifier); +TArray GetHapiNodeIds(const TArray& Handles); +HAPI_NodeId GetHapiNodeId(FUnrealObjectInputHandle Handle); + +struct HOUDINIENGINE_API FUnrealMeshExportData +{ + // This struct is created on each invocation of the Unreal Mesh Translator. The Unreal Mesh Translator + // keeps track of multiple Geo (Object) Nodes which keep track of different parts of the mesh, eg. + // lod0 and lod1 would contain the geometry for the lods, and all_lods contains all lods merged together. + // + + FUnrealMeshExportData(const UObject* Parent, bool bCanDelete); + + // This function creates a Geo Construction node if it doesn't exist. If the Geo node does exist then + // the function will ensure handles and identifiers exist. + HAPI_NodeId GetOrCreateConstructionGeoNode( + bool& bCreated, + const FString& Label, + const EUnrealObjectInputNodeType NodeType); + + // This function must be called if GetOrCreateConstructionGeoNode() returned with bCreated == true. + // See implementation for more details. NodeId must be a Hapi Node internal to the created Geo. + HAPI_NodeId RegisterConstructionNode( + const FString& Label, + const HAPI_NodeId NodeId, + const TSet* ReferencedNodes = nullptr); + + // Accessors to constructor nodes. Really all you need is the first function, GetConstructionHandles(), + // but the others just provide convenient shortcuts to reduce code verbosity. + const TMap& GetConstructionHandles(); + bool Contains(const FString& Label); + HAPI_NodeId GetHapiNodeId(const FString& Label); + FUnrealObjectInputHandle GetNodeHandle(const FString& Label); + +private: + HAPI_NodeId GetConstructionSubnetNodeId() const { return ConstructionSubnetNodeId; } + void EnsureConstructionSubnetExists(); + bool ScanForExistingNodesInHoudini(); + FUnrealObjectInputIdentifier MakeNodeIdentifier(const FString& Label, EUnrealObjectInputNodeType NodeType); + static FString CleanInputPath(const FString& ObjectPath); + + // For each Label keep track of the data associated with it. + TMap RegisteredHandles; + TMap RegisteredIdentifiers; + TMap RegisteredGeoNodes; + TMap ExistingUnassignedHAPINodes; + + // ConstructionSubnetHandle is a handle to the node where all the construction happens. Keep a cached copy of the + // Hapi node, since we access it a lot. + FUnrealObjectInputHandle ConstructionSubnetHandle; + HAPI_NodeId ConstructionSubnetNodeId = INDEX_NONE; + FString ConstructionSubnetPath; + + bool bCanDelete = true; +}; + +enum HOUDINIENGINE_API EHoudiniMeshSource +{ + MeshDescription, + LODResource, + HiResMeshDescription +}; + +struct HOUDINIENGINE_API FUnrealMeshTranslator +{ +public: + // A Word To The Wise: + // + // This code has been significantly refactored to fix issues with the reference input system. + // The old code is still largely in place in case we come across any major problems. + // + // The major problem we had before was that if a Mesh was shared between two inputs with different + // settings, you'd most likely get the wrong settings. This wasn't very common, but still an issue. + // There were also performance issues. + // + // The new system will use merge nodes to combine different geo nodes to get the desired result. + // For example, if the user specifies an input node as a mesh, and requires 2 lods + colliders, + // the plugin now: + // + // 1. creates two nodes, one for each lod (eg. lod0, lod1) + // 2. create merge node (a "geometry") that merges these lods + // 3. creates a merge node with the geometry node from step 2, the "render mesh" + // 4. creates the collision geo node + // 5. creates the final static mesh by merging the collision and render mesh nodes. + // + // lod0 --- + // |- geometry --- render_mesh --- + // lod1 --- |- output + // collision --- + // + // Now if a second input uses the same static mesh as an input, but without LODs or collision, + // we can just re-use the existing nodes: + // + // lod0 --- geometry --- render_mesh --- output + // + // The naming of the intermediate nodes is actually constructed from the export options, so is + // unique to each combination. Leaf nodes always use the same name (eg. lod0, collisions). + // In the same way, if a user is using an input and then enables "lods", only missing LODs will + // need to be uploaded, providing a performance improvement. + // + // The render mesh may often be a merge node with single inputs, but it exists because it + // is used to add material parameters. This uses a "material table" to upload each material + // parameter only once and then performs an attrib_copy to apply the material parameters to + // each face in the geometry. + // + // Also the old code had a lot of logic to avoid empty merge nodes, but this has been intentionally + // avoided code complication vs a very very minor memory overhead in Houdini. + // + // The best way to see all this in action is to look in SessionSync. + // + + + // HAPI : Marshaling, extract geometry and create input asset for it - return true on success + static bool CreateInputNodeForStaticMesh( + HAPI_NodeId& InputObjectNodeId, + FUnrealObjectInputHandle& OutHandle, + const UStaticMesh* Mesh, + const UStaticMeshComponent* StaticMeshComponent, + const FString& InputNodeName, + const FUnrealMeshExportOptions& ExportOptions, + const bool bInputNodesCanBeDeleted, + const bool bForceReferenceInputNodeCreation); + + static bool CreateInputNodeForStaticMeshNew( + HAPI_NodeId& InputObjectNodeId, + FUnrealObjectInputHandle& OutHandle, + const UStaticMesh* Mesh, + const FString& InputNodeName, + const FUnrealMeshExportOptions& ExportOptions, + const bool bInputNodesCanBeDeleted); + + static bool CreateInputNodeForStaticMeshNew( + HAPI_NodeId& InputObjectNodeId, + FUnrealObjectInputHandle& OutHandle, + const UStaticMesh* Mesh, + const UStaticMeshComponent* StaticMeshComponent, + const FString& InputNodeName, + const FUnrealMeshExportOptions& ExportOptions, + const bool bInputNodesCanBeDeleted); + + static bool CreateInputNodeForStaticMeshComponentNew( + HAPI_NodeId& InputObjectNodeId, + FUnrealObjectInputHandle& OutHandle, + const FUnrealObjectInputHandle& StaticMeshHandle, + const UStaticMeshComponent* StaticMeshComponent, + const FString& InputNodeName, + const FUnrealMeshExportOptions& ExportOptions, + const bool bInputNodesCanBeDeleted); + + static bool CreateInputNodeForSplineMeshComponentNew( + HAPI_NodeId& InputObjectNodeId, + FUnrealObjectInputHandle& OutHandle, + const USplineMeshComponent* StaticMeshComponent, + const FUnrealMeshExportOptions& ExportOptions, + const bool bInputNodesCanBeDeleted); + + // Convert the Mesh using FStaticMeshLODResources + static bool CreateInputNodeForStaticMeshLODResources( + const HAPI_NodeId NodeId, + const FStaticMeshLODResources& LODResources, + const int32 LODIndex, + const bool DoExportLODs, + const bool bInExportMaterialParametersAsAttributes, + const UStaticMesh* StaticMesh, + const UStaticMeshComponent* StaticMeshComponent); + + // Helper for converting mesh assets using FMeshDescription + static bool CreateAndPopulateMeshPartFromMeshDescription( + const HAPI_NodeId& NodeId, + const FMeshDescription& MeshDescription, + const FStaticMeshConstAttributes& MeshDescriptionAttributes, + const int32 InLODIndex, + const bool bAddLODGroups, + const bool bInExportMaterialParametersAsAttributes, + const UObject* Mesh, + const UMeshComponent* MeshComponent, + const TArray& MeshMaterials, + const TArray& SectionMaterialIndices, + const FVector3f& BuildScaleVector, + const FString& PhysicalMaterialPath, + bool bExportVertexColors, + const TOptional LightMapResolution, + const TOptional LODScreenSize, + const TOptional NaniteSettings, + const UAssetImportData* ImportData, + bool bCommitGeo, + HAPI_PartInfo& OutPartInfo); + + // Convert the Mesh using FMeshDescription + static bool CreateInputNodeForMeshDescription( + const HAPI_NodeId& NodeId, + const FMeshDescription& MeshDescription, + const int32 InLODIndex, + bool bAddLODGroups, + bool bInExportMaterialParametersAsAttributes, + const UStaticMesh* StaticMesh, + const UStaticMeshComponent* StaticMeshComponent); + + static bool CreateInputNodeForBox( + HAPI_NodeId& OutBoxNodeId, + const HAPI_NodeId InParentNodeID, + const int32 ColliderIndex, + const FVector& BoxCenter, + const FVector& BoxExtent, + const FRotator& BoxRotation); + + static bool CreateInputNodeForSphere( + HAPI_NodeId& OutSphereNodeId, + const HAPI_NodeId InParentNodeID, + const int32 ColliderIndex, + const FVector& SphereCenter, + const float SphereRadius); + + static bool CreateInputNodeForSphyl( + HAPI_NodeId& OutNodeId, + const HAPI_NodeId InParentNodeID, + const int32 ColliderIndex, + const FVector& SphylCenter, + const FRotator& SphylRotation, + const float SphylRadius, + const float SphereLength); + + static bool CreateInputNodeForConvex( + HAPI_NodeId& OutNodeId, + const HAPI_NodeId InParentNodeID, + const int32 ColliderIndex, + const FKConvexElem& ConvexCollider); + + static bool CreateInputNodeForCollider( + HAPI_NodeId& OutNodeId, + const HAPI_NodeId InParentNodeID, + const int32 ColliderIndex, + const FString& ColliderName, + const TArray& ColliderVertices, + const TArray& ColliderIndices); + + static bool CreateInputNodeForMeshSockets( + const TArray& InMeshSocket, + const HAPI_NodeId InParentNodeId, + HAPI_NodeId& OutSocketsNodeId); + + // Helper function to extract the array of material names used by a given mesh + // This is used for marshalling static mesh's materials. + // Memory allocated by this function needs to be cleared by DeleteFaceMaterialArray() + static void CreateFaceMaterialArray( + const TArray& Materials, + const TArray& FaceMaterialIndices, + FHoudiniEngineIndexedStringMap& OutStaticMeshFaceMaterials); + + // Helper function to extract the array of material names used by a given mesh + // Also extracts all scalar/vector/texture parameter in the materials + // This is used for marshalling static mesh's materials. + // Memory allocated by this function needs to be cleared by DeleteFaceMaterialArray() + // The texture parameter array also needs to be cleared. + static void CreateFaceMaterialArray( + const TArray& Materials, + const TArray& FaceMaterialIndices, + FHoudiniEngineIndexedStringMap& OutStaticMeshFaceMaterials, + TMap>& OutScalarMaterialParameters, + TMap>& OutVectorMaterialParameters, + TMap& OutTextureMaterialParameters, + TMap>& OutBoolMaterialParameters); + + // Create and set mesh material attribute and material (scalar, vector and texture) parameters attributes + static bool CreateHoudiniMeshAttributes( + const int32 NodeId, + const int32 PartId, + const int32 Count, + const FHoudiniEngineIndexedStringMap& TriangleMaterials, + const TArray& MaterialSlotIndices, + const TMap>& ScalarMaterialParameters, + const TMap>& VectorMaterialParameters, + const TMap& TextureMaterialParameters, + const TMap>& BoolMaterialParameters, + const TOptional PhysicalMaterial = TOptional(), + const TOptional InNaniteSettings = TOptional()); + + // Gets the simple physical Material path for the mesh component overrides or, + // if not set, from the body setup + static FString GetSimplePhysicalMaterialPath(UBodySetup const* BodySetup); + + static bool GetOrCreateExportStaticMeshLOD( + FUnrealMeshExportData& ExportData, + const int LODIndex, + const bool bAddLODGroups, + const UStaticMesh* Mesh, + const EHoudiniMeshSource MeshSource); + + static bool GetOrCreateStaticMeshLODGeometries( + FUnrealMeshExportData& ExportData, + const UStaticMesh* StaticMesh, + const FUnrealMeshExportOptions& ExportOptions, + EHoudiniMeshSource MeshSource); + + static bool GetOrConstructStaticMeshGeometryNode( + FString& GeometryLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const UStaticMesh* Mesh); + + static bool GetOrConstructStaticMeshRenderNode( + FString & RenderMeshLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const UStaticMesh* Mesh); + + static bool GetOrConstructCollisions( + FString& CollisionsLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const UStaticMesh* Mesh); + + static bool GetOrConstructStaticMesh( + FString& MeshLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const UStaticMesh* StaticMesh); + + static FString MakeUniqueExportName(const FUnrealMeshExportOptions& ExportOptions); + + static TArray GetMaterials(const UStaticMesh* Mesh); + + static bool CreateMergeNode( + HAPI_NodeId & NodeId, + const FString & NodeLabel, + const HAPI_NodeId ParentNodeId, + const TArray & Inputs); + + static bool GetOrConstructSplineMeshRenderNode( + FString& RenderMeshLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const USplineMeshComponent* Mesh); + + static bool GetOrConstructSplineMeshGeometryNode( + FString& GeometryLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const USplineMeshComponent* Mesh); + + + static bool GetOrConstructSplineMeshComponent( + FString& MeshLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const USplineMeshComponent* SplineMeshComponent); + + static bool GetOrCreateSplineMeshLODGeometries( + FUnrealMeshExportData& ExportData, + const USplineMeshComponent* SplineMeshComponent, + const FUnrealMeshExportOptions& ExportOptions); + + static bool GetOrCreateExportSplineMeshLOD( + FUnrealMeshExportData& ExportData, + const int LODIndex, + const USplineMeshComponent* Mesh); + + static FString MakeLODName(int LODIndex, EHoudiniMeshSource Source); + + static FString MakeMeshSourceStr(EHoudiniMeshSource Source); + + static EHoudiniMeshSource DetermineMeshSource(const FUnrealMeshExportOptions& ExportOptions, const UStaticMesh * StaticMesh); + + static bool ExportCollisions( + int32& NextMergeIndex, + const UStaticMesh* StaticMesh, + const HAPI_NodeId MergeNodeId, + const HAPI_NodeId InputObjectNodeId, + const FKAggregateGeom& SimpleColliders); + + + static bool GetOrConstructSockets( + FString& SocketsLabel, + FUnrealMeshExportData& ExportData, + const FUnrealMeshExportOptions& ExportOptions, + const UStaticMesh* Mesh); + + static bool GetMaterialInfo( + const TArray& Materials, + TArray& OutMaterialInfos); + + static bool GetOrCreateMaterialTableNode( + FUnrealMeshExportData& MeshNodes, + const TArray& MaterialInfos); + + static bool GetOrCreateMaterialZipNode( + HAPI_NodeId& NodeId, + const HAPI_NodeId ParentNodeId, + const HAPI_NodeId MeshNode, + const HAPI_NodeId MaterialTableNode, + const TArray& MaterialInfos); + + static const FString LODPrefix; + static const FString HiResMeshName; + static const FString MTLParams; + static const FString CombinePrefix; + static const FString MaterialTableName; + + static bool bUseNewMeshPath; + +}; + +``` + +`Source/HoudiniEngine/Private/UnrealObjectInputManagerImpl.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealObjectInputManagerImpl.h" + +#include "HAPI/HAPI_Common.h" + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputUtils.h" + +FUnrealObjectInputManagerImpl::FUnrealObjectInputManagerImpl() + : WorldOriginNodeId() +{ +} + +FUnrealObjectInputManagerImpl::~FUnrealObjectInputManagerImpl() +{ + Clear(); +} + +bool +FUnrealObjectInputManagerImpl::FindNode( + const FUnrealObjectInputIdentifier& InIdentifier, + FUnrealObjectInputHandle& OutHandle) const +{ + if (!InIdentifier.IsValid()) + return false; + + FUnrealObjectInputNode* const* const NodeEntry = FindNode(InIdentifier); + if (!NodeEntry) + return false; + + FUnrealObjectInputNode const* const Node = *NodeEntry; + if (!Node) + return false; + + OutHandle = FUnrealObjectInputHandle(InIdentifier); + return true; +} + +bool +FUnrealObjectInputManagerImpl::GetNodeByIdentifier(const FUnrealObjectInputIdentifier& InputIdentifier, const FUnrealObjectInputNode*& OutNode) const +{ + if (!InputIdentifier.IsValid()) + return false; + + FUnrealObjectInputNode* const* NodeEntry = FindNode(InputIdentifier); + if (!NodeEntry) + return false; + + OutNode = *NodeEntry; + return true; +} + +bool +FUnrealObjectInputManagerImpl::GetNodeByIdentifier(const FUnrealObjectInputIdentifier& InputIdentifier, FUnrealObjectInputNode*& OutNode) const +{ + if (!InputIdentifier.IsValid()) + return false; + + FUnrealObjectInputNode* const* NodeEntry = FindNode(InputIdentifier); + if (!NodeEntry) + return false; + + OutNode = *NodeEntry; + return true; +} + +bool +FUnrealObjectInputManagerImpl::AddContainer(const FUnrealObjectInputIdentifier& InIdentifier, const int32 InNodeId, FUnrealObjectInputHandle& OutHandle) +{ + if (!InIdentifier.IsValid()) + return false; + + if (InIdentifier.GetNodeType() != EUnrealObjectInputNodeType::Container) + return false; + + if (Contains(InIdentifier)) + return false; + + FUnrealObjectInputHandle ParentHandle; + if (!EnsureParentsExist(InIdentifier, ParentHandle, true)) + return false; + + FUnrealObjectInputContainerNode* const Node = new FUnrealObjectInputContainerNode(InIdentifier, ParentHandle, InNodeId); + AddNode(InIdentifier, Node); + + OutHandle = FUnrealObjectInputHandle(InIdentifier); + + if (OnNodeAddedDelegate.IsBound()) + OnNodeAddedDelegate.Broadcast(InIdentifier); + + return true; +} + +bool +FUnrealObjectInputManagerImpl::AddReferenceNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InObjectNodeId, + const int32 InNodeId, + FUnrealObjectInputHandle& OutHandle, + TSet const* const InReferencedNodes, + const int32 InReferencesConnectToNodeId) +{ + if (!InIdentifier.IsValid()) + return false; + + if (InIdentifier.GetNodeType() != EUnrealObjectInputNodeType::Reference) + return false; + + if (Contains(InIdentifier)) + return false; + + FUnrealObjectInputHandle ParentHandle; + if (!EnsureParentsExist(InIdentifier, ParentHandle, true)) + return false; + + FUnrealObjectInputReferenceNode* Node = InReferencedNodes + ? new FUnrealObjectInputReferenceNode(InIdentifier, ParentHandle, InObjectNodeId, InNodeId, *InReferencedNodes, InReferencesConnectToNodeId) + : new FUnrealObjectInputReferenceNode(InIdentifier, ParentHandle, InObjectNodeId, InNodeId, InReferencesConnectToNodeId); + + AddNode(InIdentifier, Node); + + OutHandle = FUnrealObjectInputHandle(InIdentifier); + + if (OnNodeAddedDelegate.IsBound()) + OnNodeAddedDelegate.Broadcast(InIdentifier); + + return true; +} + +bool +FUnrealObjectInputManagerImpl::AddLeaf( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InObjectNodeId, + const int32 InNodeId, + FUnrealObjectInputHandle& OutHandle) +{ + if (!InIdentifier.IsValid()) + return false; + + if (InIdentifier.GetNodeType() != EUnrealObjectInputNodeType::Leaf) + return false; + + if (Contains(InIdentifier)) + return false; + + FUnrealObjectInputHandle ParentHandle; + if (!EnsureParentsExist(InIdentifier, ParentHandle, true)) + return false; + + FUnrealObjectInputLeafNode* Node = new FUnrealObjectInputLeafNode(InIdentifier, ParentHandle, InObjectNodeId, InNodeId); + AddNode(InIdentifier, Node); + + OutHandle = FUnrealObjectInputHandle(InIdentifier); + + if (OnNodeAddedDelegate.IsBound()) + OnNodeAddedDelegate.Broadcast(InIdentifier); + + return true; +} + +bool +FUnrealObjectInputManagerImpl::UpdateContainer( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InNodeId, + const bool bInClearDirtyFlag) +{ + if (!InIdentifier.IsValid() || InIdentifier.GetNodeType() != EUnrealObjectInputNodeType::Container) + return false; + + FUnrealObjectInputNode* Node = nullptr; + if (!GetNodeByIdentifier(InIdentifier, Node) || !Node) + return false; + FUnrealObjectInputContainerNode* ContainerNode = static_cast(Node); + if (!ContainerNode) + return false; + + // As part of updating the node, ensure that its parents exist and make sure it is pointing to its parent. + FUnrealObjectInputHandle ParentHandle; + if (EnsureParentsExist(InIdentifier, ParentHandle, true)) + ContainerNode->SetParent(ParentHandle); + + ContainerNode->SetHAPINodeId(InNodeId); + + // Clear the dirty flag if set (based on bInClearDirtyFlag) + if (bInClearDirtyFlag) + ContainerNode->ClearDirtyFlag(); + + if (OnNodeUpdatedDelegate.IsBound()) + OnNodeUpdatedDelegate.Broadcast(InIdentifier); + + return true; +} + +bool +FUnrealObjectInputManagerImpl::UpdateReferenceNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const TOptional InObjectNodeId, + const TOptional InNodeId, + TSet const* const InReferencedNodes, + const TOptional InReferencesConnectToNodeId, + const bool bInClearDirtyFlag) +{ + if (!InIdentifier.IsValid() || InIdentifier.GetNodeType() != EUnrealObjectInputNodeType::Reference) + return false; + + FUnrealObjectInputNode* Node = nullptr; + if (!GetNodeByIdentifier(InIdentifier, Node) || !Node) + return false; + FUnrealObjectInputReferenceNode* ReferenceNode = static_cast(Node); + if (!ReferenceNode) + return false; + + // As part of updating the node, ensure that its parents exist and make sure it is pointing to its parent. + FUnrealObjectInputHandle ParentHandle; + if (EnsureParentsExist(InIdentifier, ParentHandle, true)) + ReferenceNode->SetParent(ParentHandle); + + bool bSuccess = true; + if (InObjectNodeId.IsSet() && InObjectNodeId.GetValue() >= 0) + { + if (!ReferenceNode->SetObjectHAPINodeId(InObjectNodeId.GetValue())) + bSuccess = false; + } + if (InNodeId.IsSet() && InNodeId.GetValue() >= 0) + { + if (!ReferenceNode->SetHAPINodeId(InNodeId.GetValue())) + bSuccess = false; + } + if (InReferencesConnectToNodeId.IsSet()) + { + const int32 ReferencesConnectToNodeId = InReferencesConnectToNodeId.GetValue(); + if (ReferencesConnectToNodeId < 0) + ReferenceNode->ResetReferencesConnectToNodeId(); + else + ReferenceNode->SetReferencesConnectToNodeId(ReferencesConnectToNodeId); + } + if (InReferencedNodes) + ReferenceNode->SetReferencedNodes(*InReferencedNodes); + + // Clear the dirty flag if set (based on bInClearDirtyFlag) + if (bInClearDirtyFlag) + ReferenceNode->ClearDirtyFlag(); + + if (OnNodeUpdatedDelegate.IsBound()) + OnNodeUpdatedDelegate.Broadcast(InIdentifier); + + return bSuccess; +} + +bool +FUnrealObjectInputManagerImpl::UpdateLeaf( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InObjectNodeId, + const int32 InNodeId, + const bool bInClearDirtyFlag) +{ + if (!InIdentifier.IsValid() || InIdentifier.GetNodeType() != EUnrealObjectInputNodeType::Leaf) + return false; + + FUnrealObjectInputNode* Node = nullptr; + if (!GetNodeByIdentifier(InIdentifier, Node) || !Node) + return false; + FUnrealObjectInputLeafNode* LeafNode = static_cast(Node); + if (!LeafNode) + return false; + + // As part of updating the node, ensure that its parents exist and make sure it is pointing to its parent. + FUnrealObjectInputHandle ParentHandle; + if (EnsureParentsExist(InIdentifier, ParentHandle, true)) + LeafNode->SetParent(ParentHandle); + + bool bSuccess = true; + if (!LeafNode->SetObjectHAPINodeId(InObjectNodeId)) + bSuccess = false; + if (!LeafNode->SetHAPINodeId(InNodeId)) + bSuccess = false; + + // Clear the dirty flag if set (based on bInClearDirtyFlag) + if (bInClearDirtyFlag) + LeafNode->ClearDirtyFlag(); + + if (OnNodeUpdatedDelegate.IsBound()) + OnNodeUpdatedDelegate.Broadcast(InIdentifier); + + return bSuccess; +} + +bool +FUnrealObjectInputManagerImpl::EnsureParentsExist( + const FUnrealObjectInputIdentifier& InIdentifier, + FUnrealObjectInputHandle& OutParentHandle, + const bool& bInputNodesCanBeDeleted) +{ + if(!InIdentifier.IsValid()) + { + HOUDINI_LOG_ERROR(TEXT("Invalid FUnrealObjectInputIdentifier")); + return false; + } + + FUnrealObjectInputIdentifier ParentIdentifier; + if (!InIdentifier.MakeParentIdentifier(ParentIdentifier)) + { + // If InIdentifier is valid, but does not have a valid parent identifier, then it cannot have a parent + OutParentHandle = FUnrealObjectInputHandle(); + return true; + } + + FUnrealObjectInputHandle ParentHandle; + const bool bParentEntryExists = FindNode(ParentIdentifier, ParentHandle); + if (bParentEntryExists && AreHAPINodesValid(ParentIdentifier)) + { + // Make sure we prevent node destruction if needed + if (!bInputNodesCanBeDeleted) + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(ParentHandle, bInputNodesCanBeDeleted); + + OutParentHandle = std::move(ParentHandle); + return true; + } + + FUnrealObjectInputHandle GrandParentHandle; + if (!EnsureParentsExist(ParentIdentifier, GrandParentHandle, bInputNodesCanBeDeleted)) + return false; + + int32 GrandParentNodeId = -1; + if (GrandParentHandle.IsValid()) + { + FUnrealObjectInputNode* Node = nullptr; + if (GetNode(GrandParentHandle, Node)) + GrandParentNodeId = Node->GetHAPINodeId(); + } + + // Create an obj subnet + const FString NodeLabel = GetDefaultNodeName(ParentIdentifier); + static constexpr bool bCookOnCreation = true; + const bool bIsTopLevelNode = GrandParentNodeId < 0; + const FString OperatorName = bIsTopLevelNode ? TEXT("Object/subnet") : TEXT("subnet"); + int32 ParentNodeId = -1; + const HAPI_Result ResultVariable = FHoudiniEngineUtils::CreateNode( + GrandParentNodeId, OperatorName, NodeLabel, bCookOnCreation, &ParentNodeId); + if (ResultVariable != HAPI_RESULT_SUCCESS) + { + const FString ErrorMessage = FHoudiniEngineUtils::GetErrorDescription(); + HOUDINI_LOG_WARNING(TEXT( "Failed to create node via HAPI: %s" ), *ErrorMessage); + return false; + } + + // In session sync, if the subnet's display flag is enabled, it can cause slow deletion / creation of nodes inside + // nested subnets due to display flag propagation checks. So we attempt to disable the display flag of top level + // subnets here. + if (bIsTopLevelNode) + { + if (HAPI_RESULT_SUCCESS != FHoudiniApi::SetNodeDisplay(FHoudiniEngine::Get().GetSession(), ParentNodeId, 0)) + { + const FString ErrorMessage = FHoudiniEngineUtils::GetErrorDescription(); + HOUDINI_LOG_WARNING(TEXT("Failed to disable the display flag of an input subnet via HAPI: %s"), *ErrorMessage); + } + } + + if (!bParentEntryExists || !ParentHandle.IsValid()) + AddContainer(ParentIdentifier, ParentNodeId, ParentHandle); + else + UpdateContainer(ParentIdentifier, ParentNodeId); + + // Make sure we prevent node destruction if needed + if (!bInputNodesCanBeDeleted) + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(ParentHandle, bInputNodesCanBeDeleted); + else + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(ParentHandle, bInputNodesCanBeDeleted); + + OutParentHandle = std::move(ParentHandle); + + return true; +} + +bool +FUnrealObjectInputManagerImpl::IsDirty(const FUnrealObjectInputIdentifier& InIdentifier) const +{ + FUnrealObjectInputNode const* Node = nullptr; + if (!GetNodeByIdentifier(InIdentifier, Node)) + return false; + if (!Node) + return false; + + return Node->IsDirty(); +} + +bool +FUnrealObjectInputManagerImpl::MarkAsDirty(const FUnrealObjectInputIdentifier& InIdentifier, const bool bInAlsoDirtyReferencedNodes) +{ + FUnrealObjectInputNode* Node = nullptr; + if (!GetNodeByIdentifier(InIdentifier, Node)) + return false; + if (!Node) + return false; + + if (bInAlsoDirtyReferencedNodes && InIdentifier.GetNodeType() == EUnrealObjectInputNodeType::Reference) + { + FUnrealObjectInputReferenceNode* const RefNode = static_cast(Node); + if (!RefNode) + return false; + RefNode->MarkAsDirty(bInAlsoDirtyReferencedNodes); + } + else + { + Node->MarkAsDirty(); + } + + return true; +} + +bool +FUnrealObjectInputManagerImpl::ClearDirtyFlag(const FUnrealObjectInputIdentifier& InIdentifier) +{ + FUnrealObjectInputNode* Node = nullptr; + if (!GetNodeByIdentifier(InIdentifier, Node)) + return false; + if (!Node) + return false; + + Node->ClearDirtyFlag(); + + return true; +} + +bool +FUnrealObjectInputManagerImpl::GetHAPINodeIds(const FUnrealObjectInputIdentifier& InIdentifier, TArray& OutNodeIds) const +{ + FUnrealObjectInputNode* Node = nullptr; + if (!GetNodeByIdentifier(InIdentifier, Node)) + return false; + if (!Node) + return false; + + Node->GetHAPINodeIds(OutNodeIds); + + return true; +} + +bool +FUnrealObjectInputManagerImpl::GetHAPINodeIds(const FUnrealObjectInputIdentifier& InIdentifier, TArray& OutNodeIds) const +{ + FUnrealObjectInputNode* Node = nullptr; + if (!GetNodeByIdentifier(InIdentifier, Node)) + return false; + if (!Node) + return false; + + Node->GetHAPINodeIds(OutNodeIds); + + return true; +} + +bool +FUnrealObjectInputManagerImpl::GetAllHAPINodeIds(TArray& OutNodeIds) const +{ + const int32 NumEntries = InputNodes.Num(); + // Estimate 2 node ids per entry + OutNodeIds.Reserve(2 * NumEntries); + TArray NodeIds; + for (const auto& Entry : InputNodes) + { + FUnrealObjectInputNode const* const Node = Entry.Value; + if (!Node) + continue; + if (!NodeIds.IsEmpty()) + NodeIds.Reset(); + Node->GetHAPINodeIds(NodeIds); + OutNodeIds.Append(NodeIds); + } + + return true; +} + +bool +FUnrealObjectInputManagerImpl::GetAllHAPINodeIds(TArray& OutNodeIds) const +{ + const int32 NumEntries = InputNodes.Num(); + // Estimate 2 node ids per entry + OutNodeIds.Reserve(2 * NumEntries); + TArray NodeIds; + for (const auto& Entry : InputNodes) + { + FUnrealObjectInputNode const* const Node = Entry.Value; + if (!Node) + continue; + if (!NodeIds.IsEmpty()) + NodeIds.Reset(); + Node->GetHAPINodeIds(NodeIds); + OutNodeIds.Append(NodeIds); + } + + return true; +} + +bool +FUnrealObjectInputManagerImpl::Clear() +{ + // Destroy the Node structs + for (auto& Entry : InputNodes) + { + FUnrealObjectInputNode* Node = Entry.Value; + if (!Node) + continue; + + delete Node; + Entry.Value = nullptr; + } + InputNodes.Empty(); + return true; +} + +FUnrealObjectInputHAPINodeId +FUnrealObjectInputManagerImpl::GetWorldOriginNodeId(const bool bInCreateIfMissingOrInvalid) +{ + static const FUnrealObjectInputHAPINodeId InvalidNode; + + if (WorldOriginNodeId.IsValid()) + return WorldOriginNodeId; + + if (!bInCreateIfMissingOrInvalid) + return InvalidNode; + + // Create a OBJ/null with default/identity transform + constexpr HAPI_NodeId ParentNodeId = -1; + constexpr bool bCookOnCreation = true; + HAPI_NodeId NodeId = -1; + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniEngineUtils::CreateNode(ParentNodeId, TEXT("Object/null"), TEXT("WorldOrigin"), bCookOnCreation, &NodeId), InvalidNode); + + WorldOriginNodeId.Set(NodeId); + return WorldOriginNodeId; +} + +int32 +FUnrealObjectInputManagerImpl::GetWorldOriginHAPINodeId(const bool bInCreateIfMissingOrInvalid) +{ + const FUnrealObjectInputHAPINodeId NewWorldOriginNodeId = GetWorldOriginNodeId(bInCreateIfMissingOrInvalid); + return NewWorldOriginNodeId.GetHAPINodeId(); +} + +FString +FUnrealObjectInputManagerImpl::GetDefaultNodeName(const FUnrealObjectInputIdentifier& InIdentifier) const +{ + if (!InIdentifier.IsValid()) + return FString(); + + const AActor* Actor = Cast(InIdentifier.GetObject()); + + const EUnrealObjectInputNodeType Type = InIdentifier.GetNodeType(); + if (Type == EUnrealObjectInputNodeType::Leaf || Type == EUnrealObjectInputNodeType::Reference) + { + TArray NameParts; + NameParts.Reserve(4); + + // Get the object name, or for actors, get their label + /*FString ObjectName = IsValid(Actor) ? Actor->GetActorNameOrLabel() : InIdentifier.GetObject()->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(ObjectName);*/ + + FString ObjectName = InIdentifier.GetObject()->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(ObjectName); + + NameParts.Add(ObjectName); + + const FUnrealObjectInputOptions& Options = InIdentifier.GetOptions(); + if (Type == EUnrealObjectInputNodeType::Reference) + NameParts.Add(TEXT("merge")); + + // Add part generated from Options + const FString OptionsSuffix = Options.GenerateNodeNameSuffix(); + if (!OptionsSuffix.IsEmpty()) + NameParts.Add(OptionsSuffix); + + // Add empty part so that name ends with _ so that numeric suffixes on name clashes are easier to see + NameParts.Add(TEXT("")); + return FString::Join(NameParts, TEXT("_")); + } + + // Get the object name, or for actors, get their label + FString ObjectName; + if (IsValid(Actor)) + ObjectName = Actor->GetActorNameOrLabel(); + else + ObjectName = FPaths::GetBaseFilename(InIdentifier.GetNormalizedObjectPath()); + + FHoudiniEngineUtils::SanitizeHAPIVariableName(ObjectName); + return ObjectName; +} + +bool +FUnrealObjectInputManagerImpl::GetUniqueHoudiniNodeId(const int32 InHAPINodeId, int32& OutUniqueHoudiniNodeId) const +{ + if (InHAPINodeId < 0) + return false; + + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + if (!Session) + return false; + + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo(Session, InHAPINodeId, &NodeInfo), false); + + OutUniqueHoudiniNodeId = NodeInfo.uniqueHoudiniNodeId; + return true; +} + +bool +FUnrealObjectInputManagerImpl::AreHAPINodesValid(const FUnrealObjectInputIdentifier& InIdentifier) const +{ + if (!InIdentifier.IsValid()) + return false; + FUnrealObjectInputNode const* Node = nullptr; + if (!GetNodeByIdentifier(InIdentifier, Node) || !Node) + return false; + return Node->AreHAPINodesValid(); +} + +bool +FUnrealObjectInputManagerImpl::IsHAPINodeValid(const FUnrealObjectInputHAPINodeId& InNodeId) const +{ + if (!InNodeId.IsSet()) + return false; + + const HAPI_NodeId NodeId = InNodeId.GetHAPINodeId(); + + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + + auto Success = FHoudiniApi::GetNodeInfo(Session, NodeId, &NodeInfo); + if(Success != HAPI_RESULT_SUCCESS) + return false; + + if (InNodeId.GetUniqueHoudiniNodeId() != NodeInfo.uniqueHoudiniNodeId) + return false; + + bool ValidationAnswer = false; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::IsNodeValid(Session, NodeId, NodeInfo.uniqueHoudiniNodeId, &ValidationAnswer)) + return false; + + return ValidationAnswer; +} + +bool +FUnrealObjectInputManagerImpl::DeleteHAPINode( + FUnrealObjectInputHAPINodeId& InNodeId) const +{ + if (!InNodeId.IsValid()) + return false; + + bool bResult = FHoudiniEngineUtils::DeleteHoudiniNode(InNodeId.GetHAPINodeId()); + InNodeId.Set(INDEX_NONE, INDEX_NONE); + return bResult; +} + +bool +FUnrealObjectInputManagerImpl::SetHAPINodeDisplay(const FUnrealObjectInputHAPINodeId& InNodeId, const bool bInOnOff) const +{ + if (!InNodeId.IsValid()) + return false; + + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetNodeDisplay(FHoudiniEngine::Get().GetSession(), InNodeId.GetHAPINodeId(), bInOnOff), false); + return true; +} + +bool +FUnrealObjectInputManagerImpl::SetHAPINodeDisplay(const int32 InNodeId, const bool bInOnOff) const +{ + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetNodeDisplay(FHoudiniEngine::Get().GetSession(), InNodeId, bInOnOff), false); + return true; +} + +bool +FUnrealObjectInputManagerImpl::AddRef(const FUnrealObjectInputIdentifier& InIdentifier) +{ + if (!InIdentifier.IsValid()) + return false; + + FUnrealObjectInputNode** NodeEntry = InputNodes.Find(InIdentifier); + if (!NodeEntry) + return false; + + FUnrealObjectInputNode* Node = *NodeEntry; + if (!Node) + return false; + + bool bSuccess = Node->AddRef(); + + //HOUDINI_LOG_MESSAGE(TEXT("added ref %s %d"), *InIdentifier.ToString(), Node->GetRefCount()); + + return bSuccess; + +} + +bool +FUnrealObjectInputManagerImpl::RemoveRef(const FUnrealObjectInputIdentifier& InIdentifier) +{ + if (!InIdentifier.IsValid()) + return false; + + FUnrealObjectInputNode** NodeEntry = InputNodes.Find(InIdentifier); + if (!NodeEntry) + return false; + + FUnrealObjectInputNode* Node = *NodeEntry; + if (!Node) + return false; + + if (!Node->RemoveRef()) + return false; + + + //HOUDINI_LOG_MESSAGE(TEXT("removed ref %s %d"), *InIdentifier.ToString(), Node->GetRefCount()); + + if (Node->IsRefCounted() && Node->GetRefCount() == 0 && Node->CanBeDeleted()) + { + // Destroy HAPI nodes + if (Node->AreHAPINodesValid()) + { + // if (FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), Node->GetNodeId()) != HAPI_RESULT_SUCCESS) + if (!Node->DeleteHAPINodes()) + { + // Log if we could not delete the node + HOUDINI_LOG_WARNING(TEXT("Could not delete HAPI node: %d"), Node->GetHAPINodeId()); + } + } + + RemoveNode(InIdentifier); + delete Node; + Node = nullptr; + + if (OnNodeDeletedDelegate.IsBound()) + OnNodeDeletedDelegate.Broadcast(InIdentifier); + } + + return true; +} + +bool +FUnrealObjectInputManagerImpl::AddBackLink(const FUnrealObjectInputIdentifier& InReferencedIdentifier, const FUnrealObjectInputIdentifier& InReferencedBy) +{ + if (!InReferencedIdentifier.IsValid() || !InReferencedBy.IsValid()) + return false; + + FUnrealObjectInputBackLinkReferences& ReferencesData = BackLinks.FindOrAdd(InReferencedIdentifier); + int32& NumReferences = ReferencesData.NumReferencesBy.FindOrAdd(InReferencedBy); + NumReferences++; + + return true; +} + +bool +FUnrealObjectInputManagerImpl::RemoveBackLink(const FUnrealObjectInputIdentifier& InReferencedIdentifier, const FUnrealObjectInputIdentifier& InReferencedBy) +{ + if (!InReferencedIdentifier.IsValid()) + return false; + + FUnrealObjectInputBackLinkReferences* const ReferencesData = BackLinks.Find(InReferencedIdentifier); + if (!ReferencesData) + return false; + + int32* NumReferences = ReferencesData->NumReferencesBy.Find(InReferencedBy); + if (!NumReferences) + return false; + + (*NumReferences) -= 1; + if (*NumReferences <= 0) + { + NumReferences = nullptr; + ReferencesData->NumReferencesBy.Remove(InReferencedBy); + } + + return true; +} + +bool +FUnrealObjectInputManagerImpl::GetReferencedBy(const FUnrealObjectInputIdentifier& InReferencedIdentifier, TSet& OutReferencedBy) const +{ + if (!InReferencedIdentifier.IsValid()) + return false; + + FUnrealObjectInputBackLinkReferences const* const ReferencesData = BackLinks.Find(InReferencedIdentifier); + if (!ReferencesData) + return false; + + if (ReferencesData->NumReferencesBy.Num() <= 0) + { + OutReferencedBy.Reset(); + return true; + } + + OutReferencedBy.Empty(ReferencesData->NumReferencesBy.Num()); + for (const auto& Pair : ReferencesData->NumReferencesBy) + { + OutReferencedBy.Add(Pair.Key); + } + + return true; +} + + +void FUnrealObjectInputManagerImpl::Dump() +{ + for (auto It : InputNodes) + { + FString IdentifierString = It.Key.ToString(); + int RefCount = It.Value->GetRefCount(); + + FString Str = FString::Printf(TEXT("%s ref count %d"), *IdentifierString, RefCount); + + FUnrealObjectInputNode* Node = nullptr; + this->GetNodeByIdentifier(It.Key, Node); + if(Node) + { + HOUDINI_LOG_MESSAGE(TEXT(" parent %s"), *Node->GetParent().GetIdentifier().ToString()); + } + + HOUDINI_LOG_MESSAGE(TEXT("%s"), *Str); + TSet ReferencedBy; + + FUnrealObjectInputManagerImpl::GetReferencedBy(It.Key, ReferencedBy); + + for (auto Ref : ReferencedBy) + { + Str = FString::Printf(TEXT("%s ref"), *IdentifierString); + HOUDINI_LOG_MESSAGE(TEXT("%s"), *Str); + } + } +} + + +FUnrealObjectInputNode* const * FUnrealObjectInputManagerImpl::FindNode(const FUnrealObjectInputIdentifier& Identifier) const +{ + return InputNodes.Find(Identifier); +} + +void FUnrealObjectInputManagerImpl::AddNode(const FUnrealObjectInputIdentifier& Identifier, FUnrealObjectInputNode* Node) +{ + InputNodes.Add(Identifier, Node); +} +void FUnrealObjectInputManagerImpl::RemoveNode(const FUnrealObjectInputIdentifier& Identifier) +{ + InputNodes.Remove(Identifier); +} + +bool FUnrealObjectInputManagerImpl::Contains(const FUnrealObjectInputHandle& InHandle) const +{ + return InputNodes.Contains(InHandle.GetIdentifier()); +} + +bool FUnrealObjectInputManagerImpl::Contains(const FUnrealObjectInputIdentifier& InIdentifier) const +{ + return InputNodes.Contains(InIdentifier); +} + + +``` + +`Source/HoudiniEngine/Private/UnrealObjectInputManagerImpl.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#include "HAPI/HAPI_Common.h" + +#include "UnrealObjectInputManager.h" +#include "UnrealObjectInputRuntimeTypes.h" + + +struct FUnrealObjectInputBackLinkReferences +{ + TMap NumReferencesBy; +}; + + +/** + * Implementation of FUnrealObjectInputManager. + * + * Input node entries are stored in the InputNodes TMap by FUnrealObjectInputIdentifier. + */ +class HOUDINIENGINE_API FUnrealObjectInputManagerImpl : public FUnrealObjectInputManager +{ +public: + + FUnrealObjectInputManagerImpl(); + + virtual ~FUnrealObjectInputManagerImpl(); + + virtual bool FindNode(const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutHandle) const override; + + virtual bool Contains(const FUnrealObjectInputHandle& InHandle) const override; + virtual bool Contains(const FUnrealObjectInputIdentifier& InIdentifier) const override; + + virtual bool GetNode(const FUnrealObjectInputHandle& InHandle, const FUnrealObjectInputNode*& OutNode) const override { return GetNodeByIdentifier(InHandle.GetIdentifier(), OutNode); } + virtual bool GetNode(const FUnrealObjectInputHandle& InHandle, FUnrealObjectInputNode*& OutNode) const override { return GetNodeByIdentifier(InHandle.GetIdentifier(), OutNode); } + + virtual bool AddContainer( + const FUnrealObjectInputIdentifier& InIdentifier, const int32 InNodeId, FUnrealObjectInputHandle& OutHandle) override; + + virtual bool AddReferenceNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InObjectNodeId, + const int32 InNodeId, + FUnrealObjectInputHandle& OutHandle, + TSet const* const InReferencedNodes=nullptr, + const int32 InReferencesConnectToNodeId=INDEX_NONE) override; + + virtual bool AddLeaf( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InObjectNodeId, + const int32 InNodeId, + FUnrealObjectInputHandle& OutHandle) override; + + virtual bool UpdateContainer( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InNodeId, + const bool bInClearDirtyFlag=true) override; + + virtual bool UpdateReferenceNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const TOptional InObjectNodeId=TOptional(), + const TOptional InNodeId=TOptional(), + TSet const* const InReferencedNodes=nullptr, + const TOptional InReferencesConnectToNodeId=TOptional(), + const bool bInClearDirtyFlag=true) override; + + virtual bool UpdateLeaf( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InObjectNodeId, + const int32 InNodeId, + const bool bInClearDirtyFlag=true) override; + + virtual FString GetDefaultNodeName(const FUnrealObjectInputIdentifier& InIdentifier) const override; + + virtual bool GetUniqueHoudiniNodeId(const int32 InHAPINodeId, int32& OutUniqueHoudiniNodeId) const override; + + virtual bool AreHAPINodesValid(const FUnrealObjectInputIdentifier& InIdentifier) const override; + virtual bool IsHAPINodeValid(const FUnrealObjectInputHAPINodeId& InNodeId) const override; + + virtual bool DeleteHAPINode(FUnrealObjectInputHAPINodeId& InNodeId) const override; + virtual bool SetHAPINodeDisplay(const FUnrealObjectInputHAPINodeId& InNodeId, const bool bInOnOff) const override; + virtual bool SetHAPINodeDisplay(const int32 InNodeId, const bool bInOnOff) const override; + virtual bool GetHAPINodeIds(const FUnrealObjectInputIdentifier& InIdentifier, TArray& OutNodeIds) const override; + virtual bool GetHAPINodeIds(const FUnrealObjectInputIdentifier& InIdentifier, TArray& OutNodeIds) const override; + virtual bool GetAllHAPINodeIds(TArray& OutNodeIds) const override; + virtual bool GetAllHAPINodeIds(TArray& OutNodeIds) const override; + + virtual bool EnsureParentsExist( + const FUnrealObjectInputIdentifier& InIdentifier, + FUnrealObjectInputHandle& OutParentHandle, + const bool& bInputNodesCanBeDeleted) override; + + virtual bool IsDirty(const FUnrealObjectInputIdentifier& InIdentifier) const override; + virtual bool MarkAsDirty(const FUnrealObjectInputIdentifier& InIdentifier, bool bInAlsoDirtyReferencedNodes) override; + virtual bool ClearDirtyFlag(const FUnrealObjectInputIdentifier& InIdentifier) override; + + virtual bool Clear() override; + + virtual FUnrealObjectInputHAPINodeId GetWorldOriginNodeId(const bool bInCreateIfMissingOrInvalid=true) override; + virtual int32 GetWorldOriginHAPINodeId(const bool bInCreateIfMissingOrInvalid=true) override; + + virtual bool AddRef(const FUnrealObjectInputIdentifier& InIdentifier) override; + virtual bool RemoveRef(const FUnrealObjectInputIdentifier& InIdentifier) override; + + virtual bool AddBackLink(const FUnrealObjectInputIdentifier& InReferencedIdentifier, const FUnrealObjectInputIdentifier& InReferencedBy) override; + virtual bool RemoveBackLink(const FUnrealObjectInputIdentifier& InReferencedIdentifier, const FUnrealObjectInputIdentifier& InReferencedBy) override; + virtual bool GetReferencedBy(const FUnrealObjectInputIdentifier& InReferencedIdentifier, TSet& OutReferencedBy) const override; + + virtual FOnNodeAddUpdateDelete& GetOnNodeAddedDelegate() override { return OnNodeAddedDelegate; } + virtual FOnNodeAddUpdateDelete& GetOnNodeUpdatedDelegate() override { return OnNodeUpdatedDelegate; } + virtual FOnNodeAddUpdateDelete& GetOnNodeDeletedDelegate() override { return OnNodeDeletedDelegate; } + + virtual void Dump() override; + +protected: + /** Helper function to get FUnrealObjectInputNode entries by identifier (FUnrealObjectInputIdentifier). */ + virtual bool GetNodeByIdentifier(const FUnrealObjectInputIdentifier& InputIdentifier, const FUnrealObjectInputNode*& OutNode) const; + + /** Helper function to get FUnrealObjectInputNode entries by identifier (FUnrealObjectInputIdentifier). */ + virtual bool GetNodeByIdentifier(const FUnrealObjectInputIdentifier& InputIdentifier, FUnrealObjectInputNode*& OutNode) const; + +private: + /** The input node entries by identifier. */ + + FUnrealObjectInputNode* const * FindNode(const FUnrealObjectInputIdentifier& Identifier) const; + void AddNode(const FUnrealObjectInputIdentifier& Identifier, FUnrealObjectInputNode* Node); + void RemoveNode(const FUnrealObjectInputIdentifier& Identifier); + + TMap InputNodes; + + /** The HAPI Node id of the world origin null, use for transforms when object merging. */ + FUnrealObjectInputHAPINodeId WorldOriginNodeId; + + /** Map of a node identifier to data about nodes that reference it. */ + TMap BackLinks; + + FOnNodeAddUpdateDelete OnNodeAddedDelegate; + FOnNodeAddUpdateDelete OnNodeUpdatedDelegate; + FOnNodeAddUpdateDelete OnNodeDeletedDelegate; +}; + +``` + +`Source/HoudiniEngine/Private/UnrealObjectInputTypes.cpp`: + +```cpp +#include "UnrealObjectInputTypes.h" + +#include "HoudiniDataLayerUtils.h" +#include "UnrealObjectInputManager.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniHLODLayerUtils.h" +#include "StaticMeshResources.h" + +#include "GameFramework/Actor.h" +#include "Components/MeshComponent.h" +#include "Materials/Material.h" +#include "Materials/MaterialInterface.h" +#include "PhysicalMaterials/PhysicalMaterial.h" +#include "WorldPartition/HLOD/HLODLayer.h" + +HAPI_NodeId +EnsureHAPINodeExistsInternal( + const HAPI_NodeId InParentNetworkNodeId, + const FString& InOpTypeName, + const FString& InNodeName, + TArray& InHAPINodeIds, + const int32 InIndex) +{ + // Check that InNodeIdToConnectTo is valid + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(InParentNetworkNodeId)) + return false; + + // Check if we already have a valid node, if not create it + FUnrealObjectInputHAPINodeId ExistingNodeId; + if (InHAPINodeIds.IsValidIndex(InIndex)) + ExistingNodeId = InHAPINodeIds[InIndex]; + if (!ExistingNodeId.IsValid()) + { + static constexpr bool bCookOnCreation = false; + HAPI_NodeId NewNodeId = -1; + if (FHoudiniEngineUtils::CreateNode( + InParentNetworkNodeId, InOpTypeName, InNodeName, bCookOnCreation, &NewNodeId) != HAPI_RESULT_SUCCESS) + { + // Failed to create the node. + HOUDINI_LOG_WARNING( + TEXT("Failed to create %s node: %s"), *InNodeName, *FHoudiniEngineUtils::GetErrorDescription()); + return INDEX_NONE; + } + ExistingNodeId.Set(NewNodeId); + while (InHAPINodeIds.Num() <= InIndex) + InHAPINodeIds.AddDefaulted(); + InHAPINodeIds[InIndex] = ExistingNodeId; + } + + return ExistingNodeId.GetHAPINodeId(); +} + + +HAPI_NodeId +FUnrealObjectInputMaterialOverrides::EnsureHAPINodeExists(const HAPI_NodeId InParentNetworkNodeId) +{ + const FString OpTypeName(bUsePrimWrangle ? TEXT("attribwrangle") : TEXT("attribcreate")); + const FString NodeName(TEXT("material_overrides")); + return EnsureHAPINodeExistsInternal(InParentNetworkNodeId, OpTypeName, NodeName, HAPINodeIds, 0); +} + +bool +FUnrealObjectInputMaterialOverrides::UpdateAsPrimWrangle(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) +{ + // If we don't have a valid mesh component destroy the nodes and return false + if (!IsValid(MeshComponent)) + { + DestroyHAPINodes(); + return false; + } + + // Check that InNodeIdToConnectTo is valid + if (!InNodeIdToConnectTo.IsValid()) + return false; + + const HAPI_NodeId HAPINodeIdtoConnectTo = InNodeIdToConnectTo.GetHAPINodeId(); + + // Check if we already have a valid node, if not create it + const HAPI_NodeId MaterialOverridesNodeId = EnsureHAPINodeExists( + FHoudiniEngineUtils::HapiGetParentNodeId(HAPINodeIdtoConnectTo)); + if (MaterialOverridesNodeId < 0) + return false; + + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + + // Connect our input to InNodeIdToConnectTo's output + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + Session, + MaterialOverridesNodeId, 0, HAPINodeIdtoConnectTo, 0), false); + + // Set group to exclude applying the material overrides to collision geo + HAPI_ParmInfo GroupParmInfo; + HAPI_ParmId GroupParmId = FHoudiniEngineUtils::HapiFindParameterByName(MaterialOverridesNodeId, "group", GroupParmInfo); + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmStringValue(Session, MaterialOverridesNodeId, "* ^collision_*", GroupParmId, 0), false); + // Set grouptype to primitive + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue(Session, MaterialOverridesNodeId, "grouptype", 0, 4), false); + + // Construct a VEXpression to set create and set material override attributes. + // eg. s@unreal_material1 = 'MyPath/MyMaterial'; + const int32 NumMaterials = MeshComponent->GetNumMaterials(); + TArray MaterialPaths; + MaterialPaths.Reserve(NumMaterials); + UMaterialInterface const* const DefaultMaterial = FHoudiniEngine::Get().GetHoudiniDefaultMaterial().Get(); + const FString DefaultMaterialPathName = IsValid(DefaultMaterial) ? DefaultMaterial->GetPathName() : TEXT("default"); + for (int32 MaterialIndex = 0; MaterialIndex < NumMaterials; ++MaterialIndex) + { + UMaterialInterface const* const Material = MeshComponent->GetMaterial(MaterialIndex); + MaterialPaths.Add(FString::Format(TEXT("\"{0}\""), { IsValid(Material) ? Material->GetPathName() : DefaultMaterialPathName })); + } + + const FString MaterialPathsString = FString::Join(MaterialPaths, TEXT(",\n ")); + FString VEXpression = \ +R"(// Material overrides from component by slot index +string material_overrides[] = { + {0} +}; +// Number of overrides +int num_slots = len(material_overrides); + +// Don't change any material assignments if we have no overrides +if (num_slots <= 0) + return; + +// If there is only one slot we can set all primitives to the override +if (num_slots == 1) { + s@unreal_material = material_overrides[0]; + return; +} + +// Each material attribute value is prefixed with the slot number +// get the slot number for the current primitive +string material = s@unreal_material; +string slot_str = re_find(r"\[\d+\]", material); +if (strlen(slot_str) == 0) + return; +int material_slot = atoi(slot_str[1:-1]); +// Check that material_slot is in range +if (material_slot < 0 || material_slot >= num_slots) + return; +// Set the material to the override from the component for slot # "material_slot" +// and keep the slot prefix +s@unreal_material = "[" + itoa(material_slot) + "]" + material_overrides[material_slot];)"; + VEXpression = FString::Format(*VEXpression, { MaterialPathsString }); + + // aet the wrangle's class to primitives + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue(Session, MaterialOverridesNodeId, "class", 0, 1), false); + + // Set the snippet parameter to the VEXpression. + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniEngineUtils::HapiFindParameterByName(MaterialOverridesNodeId, "snippet", ParmInfo); + if (ParmId != -1) + { + FHoudiniApi::SetParmStringValue(Session, MaterialOverridesNodeId, + H_TCHAR_TO_UTF8(*VEXpression), ParmId, 0); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Invalid Parameter: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + return false; + } + + return true; +} + + +bool +FUnrealObjectInputMaterialOverrides::UpdateAsPointAttribCreate(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) +{ + // If we don't have a valid mesh component destroy the nodes and return false + if (!IsValid(MeshComponent)) + { + DestroyHAPINodes(); + return false; + } + + // Check that InNodeIdToConnectTo is valid + if (!InNodeIdToConnectTo.IsValid()) + return false; + + const HAPI_NodeId HAPINodeIdToConnectTo = InNodeIdToConnectTo.GetHAPINodeId(); + + // Check if we already have a valid node, if not create it + const HAPI_NodeId MaterialOverridesNodeId = EnsureHAPINodeExists( + FHoudiniEngineUtils::HapiGetParentNodeId(HAPINodeIdToConnectTo)); + if (MaterialOverridesNodeId < 0) + return false; + + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + + // Connect our input to InNodeIdToConnectTo's output + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + Session, + MaterialOverridesNodeId, 0, HAPINodeIdToConnectTo, 0), false); + + // Get the default material for in case we encounter invalid materials + UMaterialInterface const* const DefaultMaterial = FHoudiniEngine::Get().GetHoudiniDefaultMaterial().Get(); + const FString DefaultMaterialPathName = IsValid(DefaultMaterial) ? DefaultMaterial->GetPathName() : TEXT("default"); + + const int32 NumMaterials = MeshComponent->GetNumMaterials(); + FHoudiniApi::SetParmIntValue( + Session, MaterialOverridesNodeId, "numattr", 0, NumMaterials); + HAPI_ParmInfo ParmInfo; + HAPI_PartId ParmId; + + for (int32 MatNum = 1; MatNum <= NumMaterials; ++MatNum) + { + UMaterialInterface const* const Material = MeshComponent->GetMaterial(MatNum - 1); + FString MatName(TEXT(HAPI_UNREAL_ATTRIB_MATERIAL)); + if (NumMaterials > 1) + MatName += FString::FromInt(MatNum - 1); + + // Get material path name + const FString MaterialPathName = IsValid(Material) ? Material->GetPathName() : DefaultMaterialPathName; + + // parm name is one indexed + ParmId = FHoudiniEngineUtils::HapiFindParameterByName(MaterialOverridesNodeId, TCHAR_TO_ANSI(*FString::Printf(TEXT("name%d"), MatNum)), ParmInfo); + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmStringValue(Session, MaterialOverridesNodeId, TCHAR_TO_ANSI(*MatName), ParmId, 0), false); + + // set attribute type to string (index 3) + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue(Session, MaterialOverridesNodeId, TCHAR_TO_ANSI(*FString::Printf(TEXT("type%d"), MatNum)), 0, 3), false); + + // set value to path of material + ParmId = FHoudiniEngineUtils::HapiFindParameterByName(MaterialOverridesNodeId, TCHAR_TO_ANSI(*FString::Printf(TEXT("string%d"), MatNum)), ParmInfo); + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmStringValue(Session, MaterialOverridesNodeId, TCHAR_TO_ANSI(*MaterialPathName), ParmId, 0), false); + } + + return true; +} + + +void +FUnrealObjectInputMaterialOverrides::SetMeshComponent(UMeshComponent* const InMeshComponent) +{ + if (InMeshComponent == MeshComponent) + return; + + MeshComponent = InMeshComponent; + MarkAsNeedsRebuild(); +} + + +void +FUnrealObjectInputMaterialOverrides::SetUsePrimWrangle(const bool bInUsePrimWrangle) +{ + if (bInUsePrimWrangle == bUsePrimWrangle) + return; + bUsePrimWrangle = bInUsePrimWrangle; + MarkAsNeedsRebuild(); +} + + +bool +FUnrealObjectInputMaterialOverrides::Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) +{ + // If we don't have a valid mesh component, destroy all nodes and return false + if (!IsValid(MeshComponent)) + { + DestroyHAPINodes(); + return false; + } + + // Remove existing nodes if rebuilding + if (bNeedsRebuild) + DestroyHAPINodes(); + + bool bSuccess = false; + if (!bUsePrimWrangle) + bSuccess = UpdateAsPointAttribCreate(InNodeIdToConnectTo); + else + bSuccess = UpdateAsPrimWrangle(InNodeIdToConnectTo); + + if (bSuccess) + bNeedsRebuild = false; + + return bSuccess; +} + + +void +FUnrealObjectInputPhysicalMaterialOverride::SetPrimitiveComponent(UPrimitiveComponent* const InPrimitiveComponent) +{ + if (InPrimitiveComponent == PrimitiveComponent) + return; + PrimitiveComponent = InPrimitiveComponent; + MarkAsNeedsRebuild(); +} + + +void +FUnrealObjectInputPhysicalMaterialOverride::SetAttributeOwner(const HAPI_AttributeOwner InAttributeOwner) +{ + if (InAttributeOwner == AttributeOwner) + return; + AttributeOwner = InAttributeOwner; + MarkAsNeedsRebuild(); +} + + +HAPI_NodeId +FUnrealObjectInputPhysicalMaterialOverride::EnsureHAPINodeExists(const HAPI_NodeId InParentNetworkNodeId) +{ + const FString OpTypeName(TEXT("attribcreate")); + const FString NodeName(TEXT("physical_material_override")); + return EnsureHAPINodeExistsInternal(InParentNetworkNodeId, OpTypeName, NodeName, HAPINodeIds, 0); +} + + +bool +FUnrealObjectInputPhysicalMaterialOverride::Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) +{ + // Check that InNodeIdToConnectTo is valid + if (!InNodeIdToConnectTo.IsValid()) + return false; + + if (!IsValid(PrimitiveComponent)) + { + DestroyHAPINodes(); + return false; + } + + // Remove existing nodes if rebuilding + if (bNeedsRebuild) + DestroyHAPINodes(); + + const HAPI_NodeId HAPINodeIdToConnectTo = InNodeIdToConnectTo.GetHAPINodeId(); + + // Check if we already have a valid node, if not create it + const HAPI_NodeId PhysMatOverrideNodeId = EnsureHAPINodeExists( + FHoudiniEngineUtils::HapiGetParentNodeId(HAPINodeIdToConnectTo)); + if (PhysMatOverrideNodeId < 0) + return false; + + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + + // Connect our input to InNodeIdToConnectTo's output + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + Session, PhysMatOverrideNodeId, 0, HAPINodeIdToConnectTo, 0), false); + + // Set the number of attributes: 1 + FHoudiniApi::SetParmIntValue(Session, PhysMatOverrideNodeId, "numattr", 0, 1); + + // Get the attribute class + int AttrClass = 1; + switch (AttributeOwner) + { + case HAPI_ATTROWNER_VERTEX: + AttrClass = 3; + break; + case HAPI_ATTROWNER_POINT: + AttrClass = 2; + break; + case HAPI_ATTROWNER_PRIM: + AttrClass = 1; + break; + case HAPI_ATTROWNER_DETAIL: + AttrClass = 0; + break; + default: + HOUDINI_LOG_WARNING(TEXT("Unsupported value for attribute class: %d"), AttributeOwner); + return false; + } + + UPhysicalMaterial const* PhysMat = nullptr; + PhysMat = PrimitiveComponent->BodyInstance.GetSimplePhysicalMaterial(); + + // If the material is invalid then the path is empty string and we disable the attribute on the node + FString MaterialPath(""); + bool bEnable = false; + if (IsValid(PhysMat) && PhysMat != GEngine->DefaultPhysMaterial) + { + // If the material is valid get its path + MaterialPath = PhysMat->GetPathName(); + bEnable = true; + } + + // Set enable + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue(Session, PhysMatOverrideNodeId, "enable1", 0, bEnable), false); + + // Set attribute class + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue(Session, PhysMatOverrideNodeId, "class1", 0, AttrClass), false); + + // Set the attribcreate attribute name + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniEngineUtils::HapiFindParameterByName(PhysMatOverrideNodeId, "name1", ParmInfo); + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmStringValue(Session, PhysMatOverrideNodeId, HAPI_UNREAL_ATTRIB_SIMPLE_PHYSICAL_MATERIAL, ParmId, 0), false); + + // set attribute type to string (index 3) + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue(Session, PhysMatOverrideNodeId, "type1", 0, 3), false); + + // set value to path of material + ParmId = FHoudiniEngineUtils::HapiFindParameterByName(PhysMatOverrideNodeId, "string1", ParmInfo); + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmStringValue(Session, PhysMatOverrideNodeId, TCHAR_TO_ANSI(*MaterialPath), ParmId, 0), false); + + bNeedsRebuild = false; + + return true; +} + + +void +FUnrealObjectInputActorAsReference::SetActor(AActor* const InActor) +{ + if (InActor == Actor) + return; + + Actor = InActor; + MarkAsNeedsRebuild(); +} + + +HAPI_NodeId +FUnrealObjectInputActorAsReference::EnsureHAPINodeExists(const HAPI_NodeId InParentNetworkNodeId) +{ + const FString OpTypeName(TEXT("attribwrangle")); + const FString NodeName(TEXT("actor_reference_attributes")); + return EnsureHAPINodeExistsInternal(InParentNetworkNodeId, OpTypeName, NodeName, HAPINodeIds, 0); +} + + +bool +FUnrealObjectInputActorAsReference::Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) +{ + // If we don't have a valid mesh component destroy the nodes and return false + if (!IsValid(Actor)) + { + DestroyHAPINodes(); + return false; + } + + // Check that InNodeIdToConnectTo is valid + if (!InNodeIdToConnectTo.IsValid()) + return false; + + const HAPI_NodeId HAPINodeIdToConnectTo = InNodeIdToConnectTo.GetHAPINodeId(); + + // Check if we already have a valid node, if not create it + const HAPI_NodeId MaterialOverridesNodeId = EnsureHAPINodeExists( + FHoudiniEngineUtils::HapiGetParentNodeId(HAPINodeIdToConnectTo)); + if (MaterialOverridesNodeId < 0) + return false; + + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + + // Connect our input to InNodeIdToConnectTo's output + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + Session, MaterialOverridesNodeId, 0, HAPINodeIdToConnectTo, 0), false); + + // Extract the level path from the level + FString LevelPath(""); + if (IsValid(Actor->GetLevel())) + { + LevelPath = Actor->GetLevel()->GetPathName(); + // We just want the path up to the first point + int32 DotIndex; + if (LevelPath.FindChar('.', DotIndex)) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + LevelPath.LeftInline(DotIndex, EAllowShrinking::No); +#else + LevelPath.LeftInline(DotIndex, false); +#endif + } + + // Construct a VEXpression to set create and set level path and actor path attributes + const FString VEXpressionFormat = \ +R"(s@{0} = "{1}"; +s@{2} = "{3}";)"; + const FString VEXpression = FString::Format(*VEXpressionFormat, { + HAPI_UNREAL_ATTRIB_LEVEL_PATH, LevelPath, + HAPI_UNREAL_ATTRIB_ACTOR_PATH, Actor->GetPathName() + }); + + // Set the wrangle's class to points + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue(Session, MaterialOverridesNodeId, "class", 0, 2), false); + + // Set the snippet parameter to the VEXpression. + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniEngineUtils::HapiFindParameterByName(MaterialOverridesNodeId, "snippet", ParmInfo); + if (ParmId != -1) + { + FHoudiniApi::SetParmStringValue(Session, MaterialOverridesNodeId, H_TCHAR_TO_UTF8(*VEXpression), ParmId, 0); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Invalid Parameter: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + return false; + } + + return true; +} + +void +FUnrealObjectInputDataLayer::SetActor(AActor* const InActor) +{ + if (InActor == Actor) + return; + + Actor = InActor; + MarkAsNeedsRebuild(); +} + + +HAPI_NodeId +FUnrealObjectInputDataLayer::EnsureHAPINodeExists(const HAPI_NodeId InParentNetworkNodeId) +{ + const FString OpTypeName(TEXT("attribwrangle")); + const FString NodeName(TEXT("unreal_data_layers")); + return EnsureHAPINodeExistsInternal(InParentNetworkNodeId, OpTypeName, NodeName, HAPINodeIds, 0); +} + + +bool +FUnrealObjectInputDataLayer::Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) +{ + // If we don't have a valid mesh component destroy the nodes and return false + if (!IsValid(Actor)) + { + DestroyHAPINodes(); + return false; + } + + // Check that InNodeIdToConnectTo is valid + if (!InNodeIdToConnectTo.IsValid()) + return false; + + const HAPI_NodeId HAPINodeIdToConnectTo = InNodeIdToConnectTo.GetHAPINodeId(); + + // Check if we already have a valid node, if not create it + const HAPI_NodeId VexNodeId = EnsureHAPINodeExists( + FHoudiniEngineUtils::HapiGetParentNodeId(HAPINodeIdToConnectTo)); + if (VexNodeId < 0) + return false; + + const HAPI_Session * Session = FHoudiniEngine::Get().GetSession(); + + // Connect our input to InNodeIdToConnectTo's output + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + Session, VexNodeId, 0, HAPINodeIdToConnectTo, 0), false); + + FHoudiniDataLayerUtils::SetVexCode(VexNodeId, Actor); + + return true; +} + + +void +FUnrealObjectInputHLODAttributes::SetActor(AActor* const InActor) +{ + if (InActor == Actor) + return; + + Actor = InActor; + MarkAsNeedsRebuild(); +} + + +HAPI_NodeId +FUnrealObjectInputHLODAttributes::EnsureHAPINodeExists(const HAPI_NodeId InParentNetworkNodeId) +{ + const FString OpTypeName(TEXT("attribwrangle")); + const FString NodeName(TEXT("unreal_hlod_attributes")); + return EnsureHAPINodeExistsInternal(InParentNetworkNodeId, OpTypeName, NodeName, HAPINodeIds, 0); +} + + +bool +FUnrealObjectInputHLODAttributes::Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) +{ + // If we don't have a valid mesh component destroy the nodes and return false + if (!IsValid(Actor)) + { + DestroyHAPINodes(); + return false; + } + + // Check that InNodeIdToConnectTo is valid + if (!InNodeIdToConnectTo.IsValid()) + return false; + + const HAPI_NodeId HAPINodeIdToConnectTo = InNodeIdToConnectTo.GetHAPINodeId(); + + // Check if we already have a valid node, if not create it + const HAPI_NodeId VexNodeId = EnsureHAPINodeExists(FHoudiniEngineUtils::HapiGetParentNodeId(HAPINodeIdToConnectTo)); + + if (VexNodeId < 0) + return false; + + const HAPI_Session* Session = FHoudiniEngine::Get().GetSession(); + + // Connect our input to InNodeIdToConnectTo's output + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(Session, VexNodeId, 0, HAPINodeIdToConnectTo, 0), false); + + // Set the wrangle's class to prims + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(Session, VexNodeId, "class", 0, 1), false); + + FHoudiniHLODLayerUtils::SetVexCode(VexNodeId, Actor); + + return true; +} + +HAPI_NodeId +FUnrealObjectInputActorProperties::EnsureHAPINodeExists(HAPI_NodeId InParentNetworkNodeId) +{ + const FString OpTypeName(TEXT("attribwrangle")); + const FString NodeName(TEXT("actor_properties")); + return EnsureHAPINodeExistsInternal(InParentNetworkNodeId, OpTypeName, NodeName, HAPINodeIds, 0); +} + +TArray FUnrealObjectInputActorProperties::GetMaterialOverrides(UStaticMeshComponent* MeshComponent) +{ + TArray MaterialInfos; + MaterialInfos.SetNum(MeshComponent->GetNumMaterials()); + + for(int Index = 0; Index < MeshComponent->GetNumMaterials(); Index++) + { + UMaterialInterface* Material = MeshComponent->OverrideMaterials.IsValidIndex(Index) ? MeshComponent->OverrideMaterials[Index] : nullptr; + if(!IsValid(Material)) + continue; + + TArray MateriaParamInfos; + TArray MaterialParamGuids; + + Material->GetAllScalarParameterInfo(MateriaParamInfos, MaterialParamGuids); + + for(auto& CurScalarParam : MateriaParamInfos) + { + FHoudiniMaterialParameter& Entry = MaterialInfos[Index].Scalars.Emplace_GetRef(); + Entry.Name = CurScalarParam.Name.ToString(); + Material->GetScalarParameterValue(CurScalarParam, Entry.Value); + } + + Material->GetAllVectorParameterInfo(MateriaParamInfos, MaterialParamGuids); + + for(auto& CurVectorParam : MateriaParamInfos) + { + FHoudiniMaterialParameter& Entry = MaterialInfos[Index].Vectors.Emplace_GetRef(); + Entry.Name = CurVectorParam.Name.ToString(); + Material->GetVectorParameterValue(CurVectorParam, Entry.Value); + } + + Material->GetAllTextureParameterInfo(MateriaParamInfos, MaterialParamGuids); + + for(auto& CurTextureParam : MateriaParamInfos) + { + FHoudiniMaterialParameter& Entry = MaterialInfos[Index].Textures.Emplace_GetRef(); + Entry.Name = CurTextureParam.Name.ToString(); + + UTexture* Texture = nullptr; + Material->GetTextureParameterValue(CurTextureParam, Texture); + + Entry.Value = IsValid(Texture) ? Texture->GetPathName() : TEXT(""); + } + + } + + return MaterialInfos; +} +bool +FUnrealObjectInputActorProperties::Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) +{ + // This modifier uses Vex code to set actor properties. This potentially includes material + // parameters which may be set on a per-actor basis. + + // If we don't have a valid mesh component destroy the nodes and return false + // Check we have a valid owner as ISMC from a Packed Level Actor BP won't have a valid owner + if(!IsValid(MeshComponent) || !MeshComponent->GetOwner()) + { + DestroyHAPINodes(); + return false; + } + + TArray Tags = MeshComponent->GetOwner()->Tags; + TArray Components; + + // Check that InNodeIdToConnectTo is valid + if(!InNodeIdToConnectTo.IsValid()) + return false; + + HAPI_NodeId HAPINodeIdToConnectTo = InNodeIdToConnectTo.GetHAPINodeId(); + + HAPI_NodeId ParentId = FHoudiniEngineUtils::HapiGetParentNodeId(HAPINodeIdToConnectTo); + + // Check if we already have a valid node, if not create it + HAPI_NodeId VexNodeId = EnsureHAPINodeExists(ParentId); + + if(VexNodeId < 0) + return false; + + const HAPI_Session* Session = FHoudiniEngine::Get().GetSession(); + + // Connect our input to InNodeIdToConnectTo's output + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(Session, VexNodeId, 0, HAPINodeIdToConnectTo, 0), false); + + // Set the wrangle's class to prims + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(Session, VexNodeId, "class", 0, 1), false); + + // Build up Vex code for actor tags + + FStringBuilderBase Builder; + + for(auto Tag : Tags) + { + + Builder.Appendf(TEXT("setprimgroup(0, \"%s\", @primnum, 1, \"set\");\n"), *Tag.ToString()); + } + + // Various paths we want to export... + + FString ActorPath = MeshComponent->GetOwner()->GetPathName(); + Builder.Appendf(TEXT("s@%s=\"%s\";\n"), TEXT(HAPI_UNREAL_ATTRIB_ACTOR_PATH), *ActorPath); + + FString LevelPath = MeshComponent->GetOwner()->GetLevel()->GetPathName(); + Builder.Appendf(TEXT("s@%s=\"%s\";\n"), TEXT(HAPI_UNREAL_ATTRIB_LEVEL_PATH), *LevelPath); + + + // Material parameters found on SMCs. + + UStaticMeshComponent* SMC = Cast(MeshComponent); + + if (SMC && IsValid(SMC->GetStaticMesh())) + { + // If we have a valid Static Mesh Component, get material overrides. + + TArray MaterialInfos = GetMaterialOverrides(SMC); + + int LODIndex = 0; + UStaticMesh* StaticMesh = SMC->GetStaticMesh(); + + const FStaticMeshLODResources& LODResource = StaticMesh->GetRenderData()->LODResources[LODIndex]; + const int32 NumSections = LODResource.Sections.Num(); + + auto MakeHoudiniParamName = [&](int Index, const FString& Name) + { + // Make sure the attribute name will be valid + FString SanitizedName = Name; + FHoudiniEngineUtils::SanitizeHAPIVariableName(SanitizedName); + + FString ParamPrefix = MaterialInfos.Num() == 1 ? "" : FString::FromInt(Index) + FString("_"); + return FString::Printf(TEXT("unreal_material_parameter_%s%s"), *ParamPrefix, *SanitizedName); + }; + + for(int32 SectionIndex = 0; SectionIndex < NumSections; ++SectionIndex) + { + const FStaticMeshSection& Section = LODResource.Sections[SectionIndex]; + + FHoudiniMeshMaterialInfo* MaterialInfo = (MaterialInfos.IsValidIndex(Section.MaterialIndex)) ? &MaterialInfos[Section.MaterialIndex] : nullptr; + if(!MaterialInfo) + continue; + + int FirstTriangle = Section.FirstIndex / 3; + int LastTriangle = FirstTriangle + Section.NumTriangles; + + bool bDoSnippet = !MaterialInfo->Scalars.IsEmpty() || !MaterialInfo->Vectors.IsEmpty() || !MaterialInfo->Textures.IsEmpty(); + + if(bDoSnippet) + { + Builder.Append(FString::Printf(TEXT("if (@primnum >= %d && @primnum < %d)\n"), FirstTriangle, LastTriangle)); + Builder.Append(TEXT("{\n")); + + for(auto Scalar : MaterialInfo->Scalars) + { + FString AttributeName = MakeHoudiniParamName(Section.MaterialIndex, Scalar.Name); + Builder.Append(FString::Printf(TEXT(" f@%s = %.17g;\n"), *AttributeName, Scalar.Value)); + } + + for(auto Vector : MaterialInfo->Vectors) + { + FString AttributeName = MakeHoudiniParamName(Section.MaterialIndex, Vector.Name); + Builder.Append(FString::Printf(TEXT(" p@%s = { "), *AttributeName)); + Builder.Append(FString::Printf(TEXT(" %.17g, "), Vector.Value.R)); + Builder.Append(FString::Printf(TEXT(" %.17g, "), Vector.Value.G)); + Builder.Append(FString::Printf(TEXT(" %.17g, "), Vector.Value.B)); + Builder.Append(FString::Printf(TEXT(" %.17g };\n"), Vector.Value.A)); + } + + for(auto Texture : MaterialInfo->Textures) + { + FString AttributeName = MakeHoudiniParamName(Section.MaterialIndex, Texture.Name); + Builder.Append(FString::Printf(TEXT(" s@%s = \"%s\";\n"), *AttributeName, *Texture.Value)); + } + + Builder.Append(TEXT("}\n")); + } + } + } + + FString VexCode = Builder.ToString(); + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniEngineUtils::HapiFindParameterByName(VexNodeId, "snippet", ParmInfo); + FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), VexNodeId, H_TCHAR_TO_UTF8(*VexCode), ParmId, 0); + + return true; +} + + +HAPI_NodeId +FUnrealObjectInputCustomPrimitiveData::EnsureHAPINodeExists( + const HAPI_NodeId InParentNetworkNodeId) +{ + const FString OpTypeName(TEXT("attribwrangle")); + const FString NodeName(TEXT("custom_primitive_data")); + return EnsureHAPINodeExistsInternal(InParentNetworkNodeId, OpTypeName, NodeName, HAPINodeIds, 0); +} + +void +FUnrealObjectInputCustomPrimitiveData::SetPrimComponent(UPrimitiveComponent* const InPrimComponent) +{ + if (InPrimComponent == PrimComponent) + return; + + PrimComponent = InPrimComponent; + MarkAsNeedsRebuild(); +} + + +void +FUnrealObjectInputCustomPrimitiveData::SetUsePrimWrangle(const bool bInUsePrimWrangle) +{ + if (bInUsePrimWrangle == bUsePrimWrangle) + return; + bUsePrimWrangle = bInUsePrimWrangle; + MarkAsNeedsRebuild(); +} + + +bool +FUnrealObjectInputCustomPrimitiveData::Update( + const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) +{ + // If we don't have a valid prim component, destroy all nodes and return false + if (!IsValid(PrimComponent)) + { + DestroyHAPINodes(); + return false; + } + + // Remove existing nodes if rebuilding + if (bNeedsRebuild) + DestroyHAPINodes(); + + // Check that InNodeIdToConnectTo is valid + if (!InNodeIdToConnectTo.IsValid()) + return false; + + const HAPI_NodeId HAPINodeIdtoConnectTo = InNodeIdToConnectTo.GetHAPINodeId(); + + // Check if we already have a valid node, if not create it + const HAPI_NodeId CustomPrimDataNodeId = EnsureHAPINodeExists( + FHoudiniEngineUtils::HapiGetParentNodeId(HAPINodeIdtoConnectTo)); + if (CustomPrimDataNodeId < 0) + return false; + + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + + // Connect our input to InNodeIdToConnectTo's output + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + Session, + CustomPrimDataNodeId, 0, HAPINodeIdtoConnectTo, 0), false); + + /* + // Set group to exclude applying the CPD attributes to collision geo + HAPI_ParmInfo GroupParmInfo; + HAPI_ParmId GroupParmId = FHoudiniEngineUtils::HapiFindParameterByName(CustomPrimDataNodeId, "group", GroupParmInfo); + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmStringValue(Session, CustomPrimDataNodeId, "* ^collision_*", GroupParmId, 0), false); + // Set grouptype to primitive + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue(Session, CustomPrimDataNodeId, "grouptype", 0, 4), false); + */ + + // Construct a VEXpression to create and set custom prim data attributes. + // eg. i@unreal_num_custom_primitive_data = 2; + // f@unreal_custom_primitive_data0 = 1.0; + // f@unreal_custom_primitive_data1 = 2.0; + // Create a Vex expression string that will contain the whole code snippet + FString VEXpressionString; + + // First add a line to set the number of CPD + FString FormatString = TEXT("i@{0} = {1};\n"); + FString AttributeString = TEXT(HAPI_UNREAL_ATTRIB_NUM_CUSTOM_PRIM_DATA); + FString ValueString = FString::FromInt(PrimComponent->GetCustomPrimitiveData().Data.Num()); + VEXpressionString += FString::Format(*FormatString, { AttributeString, ValueString }); + + // Now add a line for each of the CPD + FormatString = TEXT("f@{0} = {1};\n"); + for (int32 Idx = 0; Idx < PrimComponent->GetCustomPrimitiveData().Data.Num(); Idx++) + { + // Construct the indexed attribute name + AttributeString = FString(HAPI_UNREAL_ATTRIB_CUSTOM_PRIMITIVE_DATA_PREFIX) + FString::FromInt(Idx); + // And convert the value to string + ValueString = FString::SanitizeFloat(PrimComponent->GetCustomPrimitiveData().Data[Idx]); + + // Add the new line to the current VEXpression + VEXpressionString += FString::Format(*FormatString, { AttributeString, ValueString }); + } + + if (bUsePrimWrangle) + { + // set the wrangle's class to primitives + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue( + Session, CustomPrimDataNodeId, "class", 0, 1), false); + } + else + { + // set the wrangle's class to points + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue( + Session, CustomPrimDataNodeId, "class", 0, 2), false); + } + + // Set the snippet parameter to the VEXpression. + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniEngineUtils::HapiFindParameterByName(CustomPrimDataNodeId, "snippet", ParmInfo); + if (ParmId != -1) + { + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmStringValue( + Session, CustomPrimDataNodeId, H_TCHAR_TO_UTF8(*VEXpressionString), ParmId, 0), false); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Invalid Parameter: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + return false; + } + + bNeedsRebuild = false; + + return true; +} +``` + +`Source/HoudiniEngine/Private/UnrealObjectInputTypes.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + + +#include "UnrealObjectInputRuntimeTypes.h" + +#include "HAPI/HAPI_Common.h" + +#include "UObject/ObjectPtr.h" + +// Houdini Engine forward declarations + +// Unreal Engine forward declarations +class AActor; +class UMeshComponent; +class UPrimitiveComponent; + +template +struct FHoudiniMaterialParameter +{ + FString Name; + Type Value; +}; + +struct FHoudiniMeshMaterialInfo +{ + TArray> Scalars; + TArray> Vectors; + TArray> Textures; +}; + +/** + * A modifier that adds: + * - an attribwrangle for setting unreal_material prim attribute to the overrides from the given + * MeshComponent by slots, or + * - an attribcreate for setting unreal_materialX point attributes to the overrides from the given + * MeshComponent by slots + */ +class HOUDINIENGINE_API FUnrealObjectInputMaterialOverrides : public FUnrealObjectInputModifier +{ +public: + FUnrealObjectInputMaterialOverrides(FUnrealObjectInputNode& InOwner, UMeshComponent* InMC, bool bInUsePrimWrangle=true) + : FUnrealObjectInputModifier(InOwner), MeshComponent(InMC), bUsePrimWrangle(bInUsePrimWrangle) {} + + static EUnrealObjectInputModifierType StaticGetType() { return EUnrealObjectInputModifierType::MaterialOverrides; } + virtual EUnrealObjectInputModifierType GetType() const override { return StaticGetType(); } + + /** Set the MeshComponent. This set the modifier as requiring a rebuild. */ + void SetMeshComponent(UMeshComponent* InMeshComponent); + UMeshComponent* GetMeshComponent() const { return MeshComponent; } + + /** Set whether to use a primitive wrangle or point attribcreate. */ + void SetUsePrimWrangle(bool bInUsePrimWrangle); + /** Get whether to use a primitive wrangle or point attribcreate. */ + bool UsePrimWrangle() const { return bUsePrimWrangle; } + + virtual bool Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) override; + +protected: + virtual bool UpdateAsPrimWrangle(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo); + + virtual bool UpdateAsPointAttribCreate(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo); + +private: + HAPI_NodeId EnsureHAPINodeExists(const HAPI_NodeId InParentNetworkNodeId); + + /** The mesh component to get the material overrides from. */ + TObjectPtr MeshComponent; + + /** Whether to use a primitive wrangle or to create point attributes per slot via attribcreate. */ + bool bUsePrimWrangle; +}; + + +/** A modifier that adds an attribcreate for setting simple physical material override from a PrimitiveComponent. */ +class HOUDINIENGINE_API FUnrealObjectInputPhysicalMaterialOverride : public FUnrealObjectInputModifier +{ +public: + FUnrealObjectInputPhysicalMaterialOverride( + FUnrealObjectInputNode& InOwner, UPrimitiveComponent* InPrimitiveComponent, HAPI_AttributeOwner InAttrOwner) + : FUnrealObjectInputModifier(InOwner), PrimitiveComponent(InPrimitiveComponent), AttributeOwner(InAttrOwner) {} + + static EUnrealObjectInputModifierType StaticGetType() { return EUnrealObjectInputModifierType::PhysicalMaterialOverride; } + virtual EUnrealObjectInputModifierType GetType() const override { return StaticGetType(); } + + /** + * Setter for the primitive component from which to get the physical material override. Changes will mark the + * modifier as needing a rebuild. + */ + void SetPrimitiveComponent(UPrimitiveComponent* InPrimitiveComponent); + /** Getter for the primitive component from which to get the physical material override. */ + UPrimitiveComponent* GetPrimitiveComponent() const { return PrimitiveComponent; } + + /** Setter for which class of attribute to create. Changes will mark the modifier as needing a rebuild.*/ + void SetAttributeOwner(HAPI_AttributeOwner InAttributeOwner); + /** Getter for which class of attribute to create. */ + HAPI_AttributeOwner GetAttributeOwner() const { return AttributeOwner; } + + virtual bool Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) override; + +private: + HAPI_NodeId EnsureHAPINodeExists(const HAPI_NodeId InParentNetworkNodeId); + + /** The primitive component to get the physical material override from. */ + TObjectPtr PrimitiveComponent; + + /** The class of attribute to create. */ + HAPI_AttributeOwner AttributeOwner; +}; + + +/** Modifier for adding level path and actor path attributes to actors that are imported by reference. */ +class HOUDINIENGINE_API FUnrealObjectInputActorAsReference : public FUnrealObjectInputModifier +{ +public: + FUnrealObjectInputActorAsReference(FUnrealObjectInputNode& InOwner, AActor* InActor) + : FUnrealObjectInputModifier(InOwner), Actor(InActor) {} + + static EUnrealObjectInputModifierType StaticGetType() { return EUnrealObjectInputModifierType::ActorAsReference; } + virtual EUnrealObjectInputModifierType GetType() const override { return StaticGetType(); } + + /** Set the actor to get the actor and level path from. Changes will mark the modifier as requiring a rebuild. */ + void SetActor(AActor* InActor); + /** Get the actor to get the actor and level path from. */ + AActor* GetActor() const { return Actor; } + + virtual bool Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) override; + +private: + HAPI_NodeId EnsureHAPINodeExists(const HAPI_NodeId InParentNetworkNodeId); + + /** The actor to get the actor and level path from. */ + TObjectPtr Actor; +}; + + +/** Modifier for data layer groups */ +class HOUDINIENGINE_API FUnrealObjectInputDataLayer: public FUnrealObjectInputModifier +{ +public: + FUnrealObjectInputDataLayer(FUnrealObjectInputNode& InOwner, AActor* InActor) + : FUnrealObjectInputModifier(InOwner), Actor(InActor) {} + + static EUnrealObjectInputModifierType StaticGetType() { return EUnrealObjectInputModifierType::DataLayerGroups; } + virtual EUnrealObjectInputModifierType GetType() const override { return StaticGetType(); } + + void SetActor(AActor* InActor); + AActor* GetActor() const { return Actor; } + + virtual bool Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) override; + +private: + HAPI_NodeId EnsureHAPINodeExists(const HAPI_NodeId InParentNetworkNodeId); + + TObjectPtr Actor; +}; + +/** Modifier for HLODs */ +class HOUDINIENGINE_API FUnrealObjectInputHLODAttributes : public FUnrealObjectInputModifier +{ +public: + FUnrealObjectInputHLODAttributes(FUnrealObjectInputNode& InOwner, AActor* InActor) + : FUnrealObjectInputModifier(InOwner), Actor(InActor) {} + + static EUnrealObjectInputModifierType StaticGetType() { return EUnrealObjectInputModifierType::HLODAttributes; } + virtual EUnrealObjectInputModifierType GetType() const override { return StaticGetType(); } + + void SetActor(AActor* InActor); + AActor* GetActor() const { return Actor; } + + virtual bool Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) override; + +private: + HAPI_NodeId EnsureHAPINodeExists(const HAPI_NodeId InParentNetworkNodeId); + + TObjectPtr Actor; +}; + + +/** Modifier for actor and components */ +class HOUDINIENGINE_API FUnrealObjectInputActorProperties : public FUnrealObjectInputModifier +{ +public: + FUnrealObjectInputActorProperties(FUnrealObjectInputNode& InOwner, UMeshComponent* InComponent) + : FUnrealObjectInputModifier(InOwner), MeshComponent(InComponent) { + } + + static EUnrealObjectInputModifierType StaticGetType() { return EUnrealObjectInputModifierType::ActorProperties; } + virtual EUnrealObjectInputModifierType GetType() const override { return StaticGetType(); } + + virtual bool Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) override; + + HAPI_NodeId EnsureHAPINodeExists(HAPI_NodeId InParentNetworkNodeId); + + static TArray GetMaterialOverrides(UStaticMeshComponent* MeshComponent); + +private: + + TObjectPtr MeshComponent; +}; + + +/** + * A modifier that adds an attribute wrangle either on prim/points that sets + * - unreal_num_custom_primitive_data to indicate the number of CPD + * - unreal_custom_primitive_dataX for each Custom Primitive Data value (X being the zero based index) + */ +class HOUDINIENGINE_API FUnrealObjectInputCustomPrimitiveData : public FUnrealObjectInputModifier +{ +public: + FUnrealObjectInputCustomPrimitiveData(FUnrealObjectInputNode& InOwner, UPrimitiveComponent* InPC, bool bInUsePrimWrangle = true) + : FUnrealObjectInputModifier(InOwner), PrimComponent(InPC), bUsePrimWrangle(bInUsePrimWrangle) { + } + + static EUnrealObjectInputModifierType StaticGetType() { return EUnrealObjectInputModifierType::CustomPrimitiveData; } + virtual EUnrealObjectInputModifierType GetType() const override { return StaticGetType(); } + + /** Set the PrimComponent. This set the modifier as requiring a rebuild. */ + void SetPrimComponent(UPrimitiveComponent* InPrimComponent); + UPrimitiveComponent* GetPrimComponent() const { return PrimComponent; } + + /** Set whether to use a primitive wrangle or point wrangle. */ + void SetUsePrimWrangle(bool bInUsePrimWrangle); + /** Get whether to use a primitive wrangle or point wrangle. */ + bool UsePrimWrangle() const { return bUsePrimWrangle; } + + virtual bool Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) override; + +private: + HAPI_NodeId EnsureHAPINodeExists(const HAPI_NodeId InParentNetworkNodeId); + + /** The primitive component to get the custom primitive data from. */ + TObjectPtr PrimComponent; + + /** Whether to use a primitive oe point wrangle to create the attributes. */ + bool bUsePrimWrangle; +}; +``` + +`Source/HoudiniEngine/Private/UnrealObjectInputUtils.cpp`: + +```cpp +#include "UnrealObjectInputUtils.h" + +#include "UObject/Object.h" +#include "LandscapeSplinesComponent.h" + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "UnrealMeshTranslator.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputManager.h" +#include "UnrealObjectInputRuntimeUtils.h" + + +bool +FUnrealObjectInputUtils::FindNodeViaManager( + const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutHandle) +{ + if (!InIdentifier.IsValid()) + return false; + + // Check with the manager if there already is a node for this object + FUnrealObjectInputManager* Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + FUnrealObjectInputHandle Handle; + if (!Manager->FindNode(InIdentifier, Handle)) + return false; + + OutHandle = std::move(Handle); + return true; +} + +FUnrealObjectInputNode* +FUnrealObjectInputUtils::GetNodeViaManager(const FUnrealObjectInputHandle& InHandle) +{ + if (!InHandle.IsValid()) + return nullptr; + + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return nullptr; + + FUnrealObjectInputNode* Node = nullptr; + if (!Manager->GetNode(InHandle, Node)) + return nullptr; + + return Node; +} + +FUnrealObjectInputNode* +FUnrealObjectInputUtils::GetNodeViaManager(const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutHandle) +{ + if (!InIdentifier.IsValid()) + return nullptr; + + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return nullptr; + + FUnrealObjectInputHandle Handle; + if (!Manager->FindNode(InIdentifier, Handle)) + return nullptr; + + FUnrealObjectInputNode* Node = nullptr; + if (!Manager->GetNode(Handle, Node)) + return nullptr; + + OutHandle = std::move(Handle); + return Node; +} + +bool +FUnrealObjectInputUtils::FindParentNodeViaManager( + const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutParentHandle) +{ + if (!InIdentifier.IsValid()) + return false; + + // Check with the manager if there already is a node for this object + FUnrealObjectInputManager* Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + FUnrealObjectInputNode* Node = nullptr; + if (!Manager->GetNode(InIdentifier, Node) || !Node) + return false; + + OutParentHandle = Node->GetParent(); + return true; +} + +bool +FUnrealObjectInputUtils::AreHAPINodesValid(const FUnrealObjectInputHandle& InHandle) +{ + if (!InHandle.IsValid()) + return false; + + // Check with the manager if there already is a node for this object + FUnrealObjectInputManager* Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + return Manager->AreHAPINodesValid(InHandle.GetIdentifier()); +} + +bool +FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutHandle) +{ + if (!FindNodeViaManager(InIdentifier, OutHandle) || !AreHAPINodesValid(OutHandle) || FUnrealObjectInputRuntimeUtils::IsInputNodeDirty(InIdentifier)) + return false; + + return true; +} + +bool +FUnrealObjectInputUtils::AreReferencedHAPINodesValid(const FUnrealObjectInputHandle& InHandle) +{ + if (!InHandle.IsValid() || InHandle.GetIdentifier().GetNodeType() != EUnrealObjectInputNodeType::Reference) + return false; + + // Check with the manager if there already is a node for this object + FUnrealObjectInputManager* Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + FUnrealObjectInputNode const* Node = nullptr; + if (!Manager->GetNode(InHandle, Node)) + return false; + + if (!Node) + return false; + + FUnrealObjectInputReferenceNode const* const RefNode = static_cast(Node); + return RefNode->AreReferencedHAPINodesValid(); +} + +bool +FUnrealObjectInputUtils::AddNodeOrUpdateNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InNodeId, + FUnrealObjectInputHandle& OutHandle, + const int32 InObjectNodeId, + TSet const* const InReferencedNodes, + const bool& bInputNodesCanBeDeleted, + const TOptional InReferencesConnectToNodeId) +{ + if (!InIdentifier.IsValid()) + return false; + + FUnrealObjectInputManager* Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + FUnrealObjectInputHandle Handle; + const bool bNodeExists = FindNodeViaManager(InIdentifier, Handle) && Handle.IsValid(); + + bool bSuccess = false; + switch (InIdentifier.GetNodeType()) + { + case EUnrealObjectInputNodeType::Container: + if (bNodeExists) + { + bSuccess = Manager->UpdateContainer(InIdentifier, InNodeId); + } + else + { + bSuccess = Manager->AddContainer(InIdentifier, InNodeId, Handle); + } + break; + case EUnrealObjectInputNodeType::Reference: + if (bNodeExists) + { + bSuccess = Manager->UpdateReferenceNode(InIdentifier, InObjectNodeId, InNodeId, InReferencedNodes, InReferencesConnectToNodeId); + } + else + { + bSuccess = Manager->AddReferenceNode(InIdentifier, InObjectNodeId, InNodeId, Handle, InReferencedNodes, InReferencesConnectToNodeId.Get(INDEX_NONE)); + } + break; + case EUnrealObjectInputNodeType::Leaf: + if (bNodeExists) + { + bSuccess = Manager->UpdateLeaf(InIdentifier, InObjectNodeId, InNodeId); + } + else + { + bSuccess = Manager->AddLeaf(InIdentifier, InObjectNodeId, InNodeId, Handle); + } + break; + case EUnrealObjectInputNodeType::Invalid: + HOUDINI_LOG_WARNING(TEXT("[FUnrealObjectInputUtils::AddNodeOrUpdateNode] Invalid NodeType in identifier.")); + bSuccess = false; + break; + } + + if (!bSuccess) + return false; + + // Make sure to prevent deletion of the node and its parents if needed + if (!bInputNodesCanBeDeleted) + { + //Manager->EnsureParentsExist(InIdentifier, Handle, bInputNodesCanBeDeleted); + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + } + + OutHandle = std::move(Handle); + return bSuccess; +} + +FUnrealObjectInputHandle FUnrealObjectInputUtils::GetHandle(const FUnrealObjectInputIdentifier& Identifier) +{ + FUnrealObjectInputHandle Handle; + FUnrealObjectInputUtils::FindNodeViaManager(Identifier, Handle); + return Handle; +} + +HAPI_NodeId FUnrealObjectInputUtils::GetHAPINodeId(const FUnrealObjectInputHandle& Handle) +{ + HAPI_NodeId Result; + bool bSuccess = GetHAPINodeId(Handle, Result); + return bSuccess ? Result : INDEX_NONE; +} + +bool +FUnrealObjectInputUtils::GetHAPINodeId(const FUnrealObjectInputHandle& InHandle, int32& OutNodeId) +{ + if (!InHandle.IsValid()) + return false; + + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + FUnrealObjectInputNode* Node; + Manager->GetNode(InHandle, Node); + OutNodeId = Node->GetHAPINodeId(); + + return true; +} + +bool +FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(const FUnrealObjectInputHandle& InHandle, const bool& bCanBeDeleted) +{ + if (!InHandle.IsValid()) + return false; + + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + FUnrealObjectInputNode* Node = nullptr; + Manager->GetNode(InHandle, Node); + + if (!Node) + return false; + + // Only set the value to false if it isnt already + if(!bCanBeDeleted) + Node->SetCanBeDeleted(bCanBeDeleted); + + return true; +} + +bool +FUnrealObjectInputUtils::GetDefaultInputNodeName(const FUnrealObjectInputIdentifier& InIdentifier, FString& OutNodeName) +{ + if (!InIdentifier.IsValid()) + return false; + + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + OutNodeName = Manager->GetDefaultNodeName(InIdentifier); + return true; +} + +bool +FUnrealObjectInputUtils::EnsureParentsExist(const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutParentHandle, const bool& bInputNodesCanBeDeleted) +{ + if (!InIdentifier.IsValid()) + return false; + + FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + return Manager->EnsureParentsExist(InIdentifier, OutParentHandle, bInputNodesCanBeDeleted); +} + +bool +FUnrealObjectInputUtils::SetReferencedNodes(const FUnrealObjectInputHandle& InRefNodeHandle, const TSet& InReferencedNodes) +{ + if (!InRefNodeHandle.IsValid()) + return false; + + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + FUnrealObjectInputNode* Node = nullptr; + if (!Manager->GetNode(InRefNodeHandle, Node)) + return false; + + FUnrealObjectInputReferenceNode* RefNode = static_cast(Node); + if (!RefNode) + return false; + + RefNode->SetReferencedNodes(InReferencedNodes); + return true; +} + +bool +FUnrealObjectInputUtils::GetReferencedNodes(const FUnrealObjectInputHandle& InRefNodeHandle, TSet& OutReferencedNodes) +{ + if (!InRefNodeHandle.IsValid()) + return false; + + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + FUnrealObjectInputNode* Node = nullptr; + if (!Manager->GetNode(InRefNodeHandle, Node)) + return false; + + FUnrealObjectInputReferenceNode* RefNode = static_cast(Node); + if (!RefNode) + return false; + + RefNode->GetReferencedNodes(OutReferencedNodes); + return true; +} + +bool +FUnrealObjectInputUtils::BuildMeshInputObjectIdentifiers( + UObject const* const InInputObject, + const FUnrealMeshExportOptions ExportOptions, + const bool bInMainMeshIsNaniteFallbackMesh, + const bool bExportMaterialParameters, + const bool bForceCreateReferenceNode, + bool &bOutSingleLeafNodeOnly, + FUnrealObjectInputIdentifier& OutInputIdentifier, + TArray& OutPerOptionIdentifiers) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FUnrealObjectInputUtils::BuildMeshInputObjectIdentifiers); + + FUnrealObjectInputOptions DefaultOptions; + DefaultOptions.bImportAsReference = false; + DefaultOptions.bImportAsReferenceRotScaleEnabled = false; + DefaultOptions.bExportLODs = false; + DefaultOptions.bExportSockets = false; + DefaultOptions.bExportColliders = false; + DefaultOptions.bMainMeshIsNaniteFallbackMesh = false; + DefaultOptions.bExportMaterialParameters = bExportMaterialParameters; + + // The Nanite fallback mesh is only applicable if we are exporting the main mesh and/or LODs + const bool bEffectiveMainMeshIsNaniteFallbackMesh = bInMainMeshIsNaniteFallbackMesh && (ExportOptions.bMainMesh || ExportOptions.bLODs); + + // Determine number of leaves + uint32 NumLeaves = 0; + if (ExportOptions.bMainMesh) + NumLeaves++; + if (ExportOptions.bLODs) + NumLeaves++; + if (ExportOptions.bSockets) + NumLeaves++; + if (ExportOptions.bColliders) + NumLeaves++; + if (NumLeaves <= 1 && !bForceCreateReferenceNode) + { + // Only one active option, we can create a leaf node and not a reference node + FUnrealObjectInputOptions Options = DefaultOptions; + Options.bExportLODs = ExportOptions.bLODs; + Options.bExportSockets = ExportOptions.bSockets; + Options.bExportColliders = ExportOptions.bColliders; + Options.bMainMeshIsNaniteFallbackMesh = bEffectiveMainMeshIsNaniteFallbackMesh; + + constexpr bool bIsLeaf = true; + bOutSingleLeafNodeOnly = true; + OutInputIdentifier = FUnrealObjectInputIdentifier(); + OutPerOptionIdentifiers = {FUnrealObjectInputIdentifier(InInputObject, Options, bIsLeaf)}; + return true; + } + + bOutSingleLeafNodeOnly = false; + // Construct the reference node's identifier + { + FUnrealObjectInputOptions Options = DefaultOptions; + Options.bExportLODs = ExportOptions.bLODs; + Options.bExportSockets = ExportOptions.bSockets; + Options.bExportColliders = ExportOptions.bColliders; + Options.bMainMeshIsNaniteFallbackMesh = bEffectiveMainMeshIsNaniteFallbackMesh; + + constexpr bool bIsLeaf = false; + OutInputIdentifier = FUnrealObjectInputIdentifier(InInputObject, Options, bIsLeaf); + } + + // Construct per-option identifiers + TArray PerOptionIdentifiers; + // First one is the main mesh only, so all options false + if (ExportOptions.bMainMesh) + { + constexpr bool bIsLeaf = true; + FUnrealObjectInputOptions Options = DefaultOptions; + Options.bMainMeshIsNaniteFallbackMesh = bEffectiveMainMeshIsNaniteFallbackMesh; + // TODO: add a specific main mesh option? + PerOptionIdentifiers.Add(FUnrealObjectInputIdentifier(InInputObject, Options, bIsLeaf)); + } + + if (ExportOptions.bLODs) + { + constexpr bool bIsLeaf = true; + FUnrealObjectInputOptions Options = DefaultOptions; + Options.bExportLODs = true; + Options.bMainMeshIsNaniteFallbackMesh = bEffectiveMainMeshIsNaniteFallbackMesh; + PerOptionIdentifiers.Add(FUnrealObjectInputIdentifier(InInputObject, Options, bIsLeaf)); + } + + if (ExportOptions.bSockets) + { + constexpr bool bIsLeaf = true; + FUnrealObjectInputOptions Options = DefaultOptions; + Options.bExportSockets = true; + PerOptionIdentifiers.Add(FUnrealObjectInputIdentifier(InInputObject, Options, bIsLeaf)); + } + + if (ExportOptions.bColliders) + { + constexpr bool bIsLeaf = true; + FUnrealObjectInputOptions Options = DefaultOptions; + Options.bExportColliders = true; + PerOptionIdentifiers.Add(FUnrealObjectInputIdentifier(InInputObject, Options, bIsLeaf)); + } + + OutPerOptionIdentifiers = MoveTemp(PerOptionIdentifiers); + return true; +} + +bool +FUnrealObjectInputUtils::BuildLandscapeSplinesInputObjectIdentifiers( + ULandscapeSplinesComponent const* const InSplinesComponent, + const bool bInExportSplineCurves, + const bool bInExportControlPoints, + const bool bInExportLeftRightCurves, + const float InUnrealSplineResolution, + const bool bForceCreateReferenceNode, + bool &bOutSingleLeafNodeOnly, + FUnrealObjectInputIdentifier& OutReferenceNode, + TArray& OutPerOptionIdentifiers) +{ + const FUnrealObjectInputOptions DefaultOptions; + + // Determine number of leaves + uint32 NumLeaves = 0; + if (bInExportSplineCurves) + NumLeaves++; + if (bInExportControlPoints) + NumLeaves++; + if (bInExportLeftRightCurves) + NumLeaves++; + if (NumLeaves <= 1 && !bForceCreateReferenceNode) + { + // Only one active option, we can create a leaf node and not a reference node + FUnrealObjectInputOptions Options = DefaultOptions; + Options.bExportLandscapeSplineControlPoints = bInExportControlPoints; + Options.bExportLandscapeSplineLeftRightCurves = bInExportLeftRightCurves; + // Spline resolution only affects the curves, not the control point cloud + if (!bInExportControlPoints) + Options.UnrealSplineResolution = InUnrealSplineResolution; + + constexpr bool bIsLeaf = true; + bOutSingleLeafNodeOnly = true; + OutReferenceNode = FUnrealObjectInputIdentifier(); + OutPerOptionIdentifiers = {FUnrealObjectInputIdentifier(InSplinesComponent, Options, bIsLeaf)}; + return true; + } + + bOutSingleLeafNodeOnly = false; + // Construct the reference node's identifier + { + FUnrealObjectInputOptions Options = DefaultOptions; + Options.bExportLandscapeSplineControlPoints = bInExportControlPoints; + Options.bExportLandscapeSplineLeftRightCurves = bInExportLeftRightCurves; + Options.UnrealSplineResolution = InUnrealSplineResolution; + + constexpr bool bIsLeaf = false; + OutReferenceNode = FUnrealObjectInputIdentifier(InSplinesComponent, Options, bIsLeaf); + } + + // Construct per-option identifiers + TArray PerOptionIdentifiers; + // First one is the spline curves only, so all options false + if (bInExportSplineCurves) + { + constexpr bool bIsLeaf = true; + FUnrealObjectInputOptions Options = DefaultOptions; + // TODO: add a specific spline curves option? + Options.UnrealSplineResolution = InUnrealSplineResolution; + PerOptionIdentifiers.Add(FUnrealObjectInputIdentifier(InSplinesComponent, Options, bIsLeaf)); + } + + if (bInExportControlPoints) + { + constexpr bool bIsLeaf = true; + FUnrealObjectInputOptions Options = DefaultOptions; + Options.bExportLandscapeSplineControlPoints = true; + PerOptionIdentifiers.Add(FUnrealObjectInputIdentifier(InSplinesComponent, Options, bIsLeaf)); + } + + if (bInExportLeftRightCurves) + { + constexpr bool bIsLeaf = true; + FUnrealObjectInputOptions Options = DefaultOptions; + Options.bExportLandscapeSplineLeftRightCurves = true; + Options.UnrealSplineResolution = InUnrealSplineResolution; + PerOptionIdentifiers.Add(FUnrealObjectInputIdentifier(InSplinesComponent, Options, bIsLeaf)); + } + + OutPerOptionIdentifiers = MoveTemp(PerOptionIdentifiers); + return true; +} + +bool +FUnrealObjectInputUtils::SetObjectMergeXFormTypeToWorldOrigin(const HAPI_NodeId& InObjMergeNodeId) +{ + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + + FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + const HAPI_NodeId WorldOriginNodeId = Manager->GetWorldOriginHAPINodeId(); + if (WorldOriginNodeId < 0) + { + HOUDINI_LOG_WARNING(TEXT("[FUnrealObjectInputUtils::SetObjectMergeXFormTypeToWorldOrigin] Could not find/create world origin null.")); + return false; + } + + // Set the transform value to "Into Specified Object" + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue(Session, InObjMergeNodeId, H_TCHAR_TO_UTF8(TEXT("xformtype")), 0, 2), false); + // Set the transform object to the world origin null from the manager + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmNodeValue(Session, InObjMergeNodeId, H_TCHAR_TO_UTF8(TEXT("xformpath")), WorldOriginNodeId), false); + + return true; +} + +bool +FUnrealObjectInputUtils::SetReferencesNodeConnectToNodeId(const FUnrealObjectInputHandle& InHandle, const HAPI_NodeId InNodeId) +{ + auto & Identifier = InHandle.GetIdentifier(); + + // Identifier must be valid and for a reference node + if (!Identifier.IsValid() || Identifier.GetNodeType() != EUnrealObjectInputNodeType::Reference) + return false; + + FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + FUnrealObjectInputNode* Node = nullptr; + if (!Manager->GetNode(Identifier, Node)) + return false; + + FUnrealObjectInputReferenceNode* const RefNode = static_cast(Node); + if (!RefNode) + return false; + + RefNode->SetReferencesConnectToNodeId(InNodeId); + + return true; +} + +bool +FUnrealObjectInputUtils::ConnectReferencedNodesToMerge(const FUnrealObjectInputIdentifier& InRefNodeIdentifier) +{ + // Identifier must be valid and for a reference node + if (!InRefNodeIdentifier.IsValid() || InRefNodeIdentifier.GetNodeType() != EUnrealObjectInputNodeType::Reference) + return false; + + if (!AreHAPINodesValid(InRefNodeIdentifier)) + return false; + + FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + FUnrealObjectInputNode* Node = nullptr; + if (!Manager->GetNode(InRefNodeIdentifier, Node)) + return false; + + FUnrealObjectInputReferenceNode* const RefNode = static_cast(Node); + if (!RefNode) + return false; + + const HAPI_Session* const Session = FHoudiniEngine::Get().GetSession(); + const FUnrealObjectInputHAPINodeId RefNodeId = RefNode->GetReferencesConnectToNodeId(); + if (!RefNodeId.IsValid()) + return false; + + const HAPI_NodeId RefHAPINodeId = RefNodeId.GetHAPINodeId(); + + // Get currently connected inputs + TArray PrevConnectedNodes; + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + if (FHoudiniApi::GetNodeInfo(Session, RefHAPINodeId, &NodeInfo) == HAPI_RESULT_SUCCESS) + { + // There is no function in HAPI currently to directly get the number of _connected_ input nodes or to + // compose a list of the connected input nodes. + // Nodes with "infinite" inputs, such as the Merge SOP, always have NodeInfo.inputCount == 9999. So we + // stop iteration at the first disconnected input instead of visiting all 9999 possible indices. + for (int32 InputIndex = 0; InputIndex < NodeInfo.inputCount; ++InputIndex) + { + HAPI_NodeId ConnectedInputNodeId = -1; + if (FHoudiniApi::QueryNodeInput(Session, RefHAPINodeId, InputIndex, &ConnectedInputNodeId) != HAPI_RESULT_SUCCESS || ConnectedInputNodeId < 0) + break; + PrevConnectedNodes.Add(ConnectedInputNodeId); + } + } + + // Connect referenced nodes + const TSet& ReferencedNodes = RefNode->GetReferencedNodes(); + TSet ConnectedNodeSet; + ConnectedNodeSet.Reserve(ReferencedNodes.Num()); + int32 InputIndex = 0; + for (const FUnrealObjectInputHandle& Handle : ReferencedNodes) + { + if (!Handle.IsValid()) + continue; + + HAPI_NodeId NodeId = -1; + if (!GetHAPINodeId(Handle, NodeId)) + continue; + + // Connect the current input object to the merge node + if (FHoudiniApi::ConnectNodeInput(Session, RefHAPINodeId, InputIndex, NodeId, 0) != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FUnrealObjectInputUtils::ConnectReferencedNodes] Failed to connected node input: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + continue; + } + // HAPI will automatically create a object_merge node (we are expecting that NodeId and RefNodeId are never in + // the same network), we need to set the xformtype to "Into specified object" + HAPI_NodeId ConnectedNodeId = -1; + if (FHoudiniApi::QueryNodeInput(Session, RefHAPINodeId, InputIndex, &ConnectedNodeId) != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FUnrealObjectInputUtils::ConnectReferencedNodes] Failed to query connected node input: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + continue; + + } + InputIndex++; + + if (ConnectedNodeId < 0) + { + // No connection was made even though the previous functions were successful!? + continue; + } + + ConnectedNodeSet.Add(ConnectedNodeId); + + // Set the transform value to "Into Specified Object" + // Set the transform object to the world origin null from the manager + SetObjectMergeXFormTypeToWorldOrigin(ConnectedNodeId); + } + + // Disconnect previously connected nodes at indices >= FirstUnusedInputIndex + // Disconnect in reverse: inputs are consolidated on disconnect on "infinite" input nodes like the Merge SOP + const int32 FirstUnusedInputIndex = InputIndex; + for (int32 InputIndexToDelete = PrevConnectedNodes.Num(); InputIndexToDelete >= FirstUnusedInputIndex; --InputIndexToDelete) + { + HOUDINI_CHECK_ERROR(FHoudiniApi::DisconnectNodeInput(Session, RefHAPINodeId, InputIndexToDelete)); + } + + // Delete nodes from previous connections that are no longer used (the object merge SOPs automatically created + // by HAPI) + for (const HAPI_NodeId& NodeToDeleteId : PrevConnectedNodes) + { + if (ConnectedNodeSet.Contains(NodeToDeleteId)) + continue; + // Check that the node is valid / still exists before attempting to delete the node + HAPI_NodeInfo NodeToDeleteInfo; + FHoudiniApi::NodeInfo_Init(&NodeToDeleteInfo); + if (FHoudiniApi::GetNodeInfo(Session, NodeToDeleteId, &NodeInfo) != HAPI_RESULT_SUCCESS) + continue; + bool bNodeIsValid = false; + if (FHoudiniApi::IsNodeValid(Session, NodeToDeleteId, NodeInfo.uniqueHoudiniNodeId, &bNodeIsValid) != HAPI_RESULT_SUCCESS || !bNodeIsValid) + continue; + HOUDINI_CHECK_ERROR(FHoudiniApi::DeleteNode(Session, NodeToDeleteId)); + } + + return true; +} + +bool +FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const TSet& InReferencedNodes, + FUnrealObjectInputHandle& OutHandle, + bool bInConnectReferencedNodes, + bool bInputNodesCanBeDeleted) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode); + + // Identifier must be valid and for a reference node + if (!InIdentifier.IsValid() || InIdentifier.GetNodeType() != EUnrealObjectInputNodeType::Reference) + return false; + + FUnrealObjectInputHandle Handle; + if (!FindNodeViaManager(InIdentifier, Handle) || !AreHAPINodesValid(Handle)) + { + // Add the node without an node id first + // Then get its parent id and create the HAPI node inside the parent's network + int32 ObjectNodeId = -1; + int32 NodeId = -1; + if (!AddNodeOrUpdateNode(InIdentifier, NodeId, Handle, ObjectNodeId, &InReferencedNodes, bInputNodesCanBeDeleted)) + return false; + + // Get the parent node id + int32 ParentNodeId = -1; + FUnrealObjectInputHandle ParentHandle; + if (EnsureParentsExist(InIdentifier, ParentHandle, bInputNodesCanBeDeleted)) + GetHAPINodeId(ParentHandle, ParentNodeId); + + FString NodeName; + GetDefaultInputNodeName(InIdentifier, NodeName); + // Create the object node + if (FHoudiniEngineUtils::CreateNode(ParentNodeId, TEXT("geo"), NodeName, true, &ObjectNodeId) != HAPI_RESULT_SUCCESS) + return false; + // Create the merge node + if (FHoudiniEngineUtils::CreateNode(ObjectNodeId, TEXT("merge"), NodeName, true, &NodeId) != HAPI_RESULT_SUCCESS) + return false; + + if (!AddNodeOrUpdateNode(InIdentifier, NodeId, Handle, ObjectNodeId, &InReferencedNodes, bInputNodesCanBeDeleted)) + return false; + + if (bInConnectReferencedNodes) + { + if (!ConnectReferencedNodesToMerge(InIdentifier)) + return false; + } + + FUnrealObjectInputRuntimeUtils::ClearInputNodeDirtyFlag(InIdentifier); + + OutHandle = std::move(Handle); + return true; + } + + SetReferencedNodes(Handle, InReferencedNodes); + if (bInConnectReferencedNodes) + { + if (!ConnectReferencedNodesToMerge(InIdentifier)) + return false; + } + + FUnrealObjectInputRuntimeUtils::ClearInputNodeDirtyFlag(InIdentifier); + + OutHandle = std::move(Handle); + return true; +} + +bool +FUnrealObjectInputUtils::AddModifierChain(const FUnrealObjectInputHandle& InHandle, const FName InChainName, const int32 InNodeIdToConnectTo) +{ + FUnrealObjectInputNode* Node = GetNodeViaManager(InHandle); + if (!Node) + return false; + + return Node->AddModifierChain(InChainName, InNodeIdToConnectTo); +} + +bool +FUnrealObjectInputUtils::SetModifierChainNodeToConnectTo(const FUnrealObjectInputHandle& InHandle, const FName InChainName, const int32 InNodeToConnectTo) +{ + FUnrealObjectInputNode* Node = GetNodeViaManager(InHandle); + if(!Node) + return false; + return Node->SetModifierChainNodeToConnectTo(InChainName, InNodeToConnectTo); +} + +bool +FUnrealObjectInputUtils::DoesModifierChainExist(const FUnrealObjectInputHandle& InHandle, const FName InChainName) +{ + FUnrealObjectInputNode* Node = GetNodeViaManager(InHandle); + if(!Node) + return false; + + return Node->GetModifierChain(InChainName) != nullptr; +} + +HAPI_NodeId +FUnrealObjectInputUtils::GetInputNodeOfModifierChain(const FUnrealObjectInputHandle& InHandle, const FName InChainName) +{ + FUnrealObjectInputNode* Node = GetNodeViaManager(InHandle); + if(!Node) + return INDEX_NONE; + + return Node->GetInputHAPINodeIdOfModifierChain(InChainName); +} + +HAPI_NodeId +FUnrealObjectInputUtils::GetOutputNodeOfModifierChain(const FUnrealObjectInputHandle& InHandle, const FName InChainName) +{ + FUnrealObjectInputNode* Node = GetNodeViaManager(InHandle); + if(!Node) + return INDEX_NONE; + + return Node->GetOutputHAPINodeIdOfModifierChain(InChainName); +} + +bool +FUnrealObjectInputUtils::RemoveModifierChain(const FUnrealObjectInputHandle& InHandle, const FName InChainName) +{ + FUnrealObjectInputNode* Node = GetNodeViaManager(InHandle); + if(!Node) + return false; + + return Node->RemoveModifierChain(InChainName); +} + +bool +FUnrealObjectInputUtils::DestroyModifier(const FUnrealObjectInputHandle& InHandle, const FName InChainName, FUnrealObjectInputModifier* const InModifierToDestroy) +{ + FUnrealObjectInputNode* Node = GetNodeViaManager(InHandle); + if(!Node) + return false; + + return Node->DestroyModifier(InChainName, InModifierToDestroy); +} + +FUnrealObjectInputModifier* +FUnrealObjectInputUtils::FindFirstModifierOfType(const FUnrealObjectInputHandle& InHandle, FName InChainName, EUnrealObjectInputModifierType InModifierType) +{ + FUnrealObjectInputNode* Node = GetNodeViaManager(InHandle); + if(!Node) + return nullptr; + + return Node->FindFirstModifierOfType(InChainName, InModifierType); +} + +bool +FUnrealObjectInputUtils::UpdateModifiers(const FUnrealObjectInputHandle& InHandle, const FName InChainName) +{ + FUnrealObjectInputNode* Node = GetNodeViaManager(InHandle); + if(!Node) + return false; + + return Node->UpdateModifiers(InChainName); +} + +bool +FUnrealObjectInputUtils::UpdateAllModifierChains(const FUnrealObjectInputHandle& InHandle) +{ + FUnrealObjectInputNode* Node = GetNodeViaManager(InHandle); + if(!Node) + return false; + + return Node->UpdateAllModifierChains(); +} + + +``` + +`Source/HoudiniEngine/Private/UnrealObjectInputUtils.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreTypes.h" +#include "Containers/Array.h" +#include "Containers/Set.h" +#include "Misc/Optional.h" +#include "UObject/NameTypes.h" + +#include "HAPI/HAPI_Common.h" + +#include "UnrealObjectInputRuntimeTypes.h" + + +struct FUnrealMeshExportOptions; +// UE forward declarations +class UObject; +class ULandscapeSplinesComponent; + + +struct HOUDINIENGINE_API FUnrealObjectInputUtils +{ + public: + // Find the node in the input manager that corresponds to Identifier. If it could not be found return false. + // If found, return true and set Handle to reference the node. + static bool FindNodeViaManager(const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutHandle); + + // Returns the node associated with InHandle. Returns nullptr if the handle is invalid or the node does not exist. + static FUnrealObjectInputNode* GetNodeViaManager(const FUnrealObjectInputHandle& InHandle); + + // Returns the node associated with InIdentifier. Returns nullptr if the handle is invalid or the node does not exist. + static FUnrealObjectInputNode* GetNodeViaManager(const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutHandle); + + // Helper to get a handle to the parent of node of InHandle. + static bool FindParentNodeViaManager(const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutParentHandle); + + // Returns true if the HAPI nodes for input referenced by InHandle are valid (exist). + static bool AreHAPINodesValid(const FUnrealObjectInputHandle& InHandle); + + // Helper that checks that the input entry associated with InIdentifier exists, the HAPI nodes for are valid and + // the entry is not marked as dirty. + static bool NodeExistsAndIsNotDirty(const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutHandle); + + // Returns true if the HAPI nodes referenced by the input reference node of InHandle are valid (exist). + static bool AreReferencedHAPINodesValid(const FUnrealObjectInputHandle& InHandle); + + // Add an entry for an input to track and reference count in the manager. + // Returns true on success and sets OutHandle to point to the entry. + static bool AddNodeOrUpdateNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 NodeId, + FUnrealObjectInputHandle& OutHandle, + const int32 InObjectNodeId, + TSet const* const InReferencedNodes, + const bool& bInputNodesCanBeDeleted, + const TOptional InReferencesConnectToNodeId=TOptional()); + + // Helper to get the HAPI NodeId associated with InHandle. Recommend using the 2nd overload below, as its easier to use. + static bool GetHAPINodeId(const FUnrealObjectInputHandle& InHandle, int32& OutNodeId); + + static HAPI_NodeId GetHAPINodeId(const FUnrealObjectInputHandle& Handle); + + static FUnrealObjectInputHandle GetHandle(const FUnrealObjectInputIdentifier &Identifier); + + // Helper to set the CanBeDeleted property on the input object associated with InHandle + static bool UpdateInputNodeCanBeDeleted(const FUnrealObjectInputHandle& InHandle, const bool& bCanBeDeleted); + + // Helper to get the default input node name to use via the new input system. + static bool GetDefaultInputNodeName(const FUnrealObjectInputIdentifier& InIdentifier, FString& OutNodeName); + + // Helper to ensure that the parent/container nodes of a given identifier exist + static bool EnsureParentsExist(const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutParentHandle, const bool& bInputNodesCanBeDeleted); + + // Helper to set the references on a reference node + static bool SetReferencedNodes(const FUnrealObjectInputHandle& InRefNodeHandle, const TSet& InReferencedNodes); + + // Helper to get the references on a reference node + static bool GetReferencedNodes(const FUnrealObjectInputHandle& InRefNodeHandle, TSet& OutReferencedNodes); + + // Helper to build identifiers for static/skeletal mesh inputs based on options (such as LODs, Colliders, Sockets) + static bool BuildMeshInputObjectIdentifiers( + UObject const* const InInputObject, + const FUnrealMeshExportOptions ExportOptions, + const bool bInMainMeshIsNaniteFallbackMesh, + const bool bExportMaterialParameters, + const bool bForceCreateReferenceNode, + bool &bOutSingleLeafNodeOnly, + FUnrealObjectInputIdentifier& OutInputIdentifier, + TArray& OutPerOptionIdentifiers); + + // Helper to build identifiers for landscape spline inputs based on options (such as send control points) + static bool BuildLandscapeSplinesInputObjectIdentifiers( + ULandscapeSplinesComponent const* const InSplinesComponent, + const bool bInExportSplineCurves, + const bool bInExportControlPoints, + const bool bInExportLeftRightCurves, + const float InUnrealSplineResolution, + const bool bForceCreateReferenceNode, + bool &bOutSingleLeafNodeOnly, + FUnrealObjectInputIdentifier& OutReferenceNode, + TArray& OutPerOptionIdentifiers); + + // Helper to set an object_merge SOP's xformtype to "Into Specified Object" and the xformpath to the manager's + // WorldOrigin null. + static bool SetObjectMergeXFormTypeToWorldOrigin(const HAPI_NodeId& InObjMergeNodeId); + + // Helper to set the node that references connect to (such as a Merge SOP) for a reference node in the input system + static bool SetReferencesNodeConnectToNodeId(const FUnrealObjectInputHandle& InHandle, HAPI_NodeId InNodeId); + + // Helper to connect a reference node's referenced nodes to its merge SOP + static bool ConnectReferencedNodesToMerge(const FUnrealObjectInputIdentifier& InRefNodeIdentifier); + + // Helper to create a SOP/merge for merging InReferencedNodes. + static bool CreateOrUpdateReferenceInputMergeNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const TSet& InReferencedNodes, + FUnrealObjectInputHandle& OutHandle, + bool bInConnectReferencedNode, + bool bInputNodesCanBeDeleted); + + // Add a named modifier chain to the input node. Returns false if the chain was not added (if it existed already, for example) + static bool AddModifierChain(const FUnrealObjectInputHandle& InHandle, FName InChainName, int32 InNodeIdToConnectTo); + + // Set the node that the first node of the chain connects to. + static bool SetModifierChainNodeToConnectTo(const FUnrealObjectInputHandle& InHandle, FName InChainName, int32 InNodeToConnectTo); + + // Returns true if the named modifier chain exists on the node. + static bool DoesModifierChainExist(const FUnrealObjectInputHandle& InHandle, FName InChainName); + + // Returns the input node id of the named modifier chain on the node. Returns < 0 if the chain does not exist. + static HAPI_NodeId GetInputNodeOfModifierChain(const FUnrealObjectInputHandle& InHandle, FName InChainName); + + // Returns the output node id of the named modifier chain on the node. Returns < 0 if the chain does not exist. + static HAPI_NodeId GetOutputNodeOfModifierChain(const FUnrealObjectInputHandle& InHandle, FName InChainName); + + // Removed the named modifier chain. Returns true if the chain existed and was removed. + static bool RemoveModifierChain(const FUnrealObjectInputHandle& InHandle, FName InChainName); + + // Destroy the given modifier in the named chain. This deletes the modifier, so InModifierToDestroy will be invalid after a successful call to this function. + static bool DestroyModifier(const FUnrealObjectInputHandle& InHandle, FName InChainName, FUnrealObjectInputModifier* InModifierToDestroy); + + // Find and return the first modifier of class T on the node identified by InIdentifier. Returns null if it has + // no such modifier. + static FUnrealObjectInputModifier* FindFirstModifierOfType(const FUnrealObjectInputHandle& InHandle, FName InChainName, EUnrealObjectInputModifierType InModifierType); + + // Add a modifier of class T to the node identified by InIdentifier if it does not already have a modifier + // of that class. Return the newly created and added (or existing) modifier. + template + static T* CreateAndAddModifier(const FUnrealObjectInputHandle& InHandle, FName InChainName, Args... ConstructorArguments); + + // Call FUnrealInputObjectModifier::Update() on all modifiers in the chain. + static bool UpdateModifiers(const FUnrealObjectInputHandle& InHandle, FName InChainName); + + // Call FUnrealInputObjectModifier::Update() on all modifiers on the node identified by InIdentifier. + static bool UpdateAllModifierChains(const FUnrealObjectInputHandle& InHandle); +}; + + +template +T* FUnrealObjectInputUtils::CreateAndAddModifier(const FUnrealObjectInputHandle& InHandle, const FName InChainName, Args... ConstructorArguments) +{ + FUnrealObjectInputNode* Node = GetNodeViaManager(InHandle); + if (!Node || !InHandle.IsValid()) + return nullptr; + + return Node->CreateAndAddModifier(InChainName, ConstructorArguments...); +} + +``` + +`Source/HoudiniEngine/Private/UnrealPCGDataTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealPCGDataTranslator.h" + +#include + +#include "UObject/TextProperty.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniInputObject.h" + +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputUtils.h" +#include "UnrealObjectInputRuntimeUtils.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniPCGUtils.h" +#include "HoudiniPCGDataObject.h" +#include + + +namespace +{ + template + TArray + PopulateTArray(TMap::TConstIterator It, + uint32 NumRows, + uint32 NumComponents, + uint32 Offset, + uint32 ComponentSize, + const TArray& Order) + { + TArray Values; + Values.Reserve(NumRows * NumComponents); + for(; It; ++It) + { + const uint8* Data = It.Value(); + for(uint32 Idx = 0; Idx < NumComponents; ++Idx) + { + Values.Add(P::GetPropertyValue(Data + Offset + Order[Idx] * ComponentSize)); + } + + } + return Values; + } + + template + TArray + PopulateTArray(TMap::TConstIterator It, + uint32 NumRows, + uint32 NumComponents, + uint32 Offset, + uint32 ComponentSize) + { + TArray Order; + Order.Reserve(NumComponents); + for(uint32 Idx = 0; Idx < NumComponents; ++Idx) + { + Order.Add(Idx); + } + return PopulateTArray(It, NumRows, NumComponents, Offset, ComponentSize, Order); + } + + TArray + PopulateBoolArray(TMap::TConstIterator It, + const FBoolProperty& Prop, + uint32 Count, + uint32 Offset) + { + TArray Values; + Values.Reserve(Count); + for(; It; ++It) + { + const uint8* Data = It.Value(); + Values.Add(Prop.GetPropertyValue(Data + Offset)); + } + return Values; + } +}; + +bool FUnrealPCGDataTranslator::CreateInputNodeForPCGData( + UHoudiniPCGDataCollection* PCGDataCollection, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + bool bInputNodesCanBeDeleted) +{ + + // Create handles for each input node that will be merged together. + + TSet Handles; + switch (PCGDataCollection->Type) + { + case EHoudiniPCGDataType::InputPCGGeometry: + { + FUnrealObjectInputHandle Handle = CreateInputNodeForPCGAttrData(InputNodeName, PCGDataCollection, bInputNodesCanBeDeleted); + Handles.Add(Handle); + } + break; + + case EHoudiniPCGDataType::InputPCGSplines: + { + TArray SplineHandles = CreateInputNodeForPCGSplineData(InputNodeName, PCGDataCollection, bInputNodesCanBeDeleted); + Handles.Append(SplineHandles); + } + break; + + default: + break; + } + + // Merge all nodes into the input. + + const FUnrealObjectInputIdentifier MergeNodeIdentifier(PCGDataCollection, {}, false); + FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(MergeNodeIdentifier, Handles, OutHandle, true, bInputNodesCanBeDeleted); + + HAPI_NodeId MergeNodeId = FUnrealObjectInputUtils::GetHAPINodeId(MergeNodeIdentifier); + + HAPI_NodeId InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(MergeNodeId); + FUnrealObjectInputUtils::AddNodeOrUpdateNode(MergeNodeIdentifier, MergeNodeId, OutHandle, InputObjectNodeId, nullptr, bInputNodesCanBeDeleted); + + return OutHandle.IsValid(); +} + +void +FUnrealPCGDataTranslator::SetAttributes(UHoudiniPCGDataObject* PCGDataObject, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + if(!PCGDataObject) + return; + + for(auto& It : PCGDataObject->Attributes) + { + UHoudiniPCGDataAttributeBase* Attr = It.Get(); + + if (Attr->GetAttrName() == TEXT("__vertex_id") && Owner == HAPI_ATTROWNER_VERTEX) + { + auto* AttributeInt = Cast(Attr); + if (!AttributeInt) + { + HOUDINI_LOG_ERROR(TEXT("__vertex_id must be an integer")); + return; + } + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetVertexList(AttributeInt->Values, InputNodeId, PartId), ); + } + else if(auto* AttributeFloat = Cast(Attr)) + SendToHoudini(AttributeFloat, InputNodeId, PartId, Owner); + else if(auto* AttributeDouble = Cast(Attr)) + SendToHoudini(AttributeDouble, InputNodeId, PartId, Owner); + else if(auto* AttributeInt = Cast(Attr)) + SendToHoudini(AttributeInt, InputNodeId, PartId, Owner); + else if(auto* AttributeInt64 = Cast(Attr)) + SendToHoudini(AttributeInt64, InputNodeId, PartId, Owner); + else if(auto* AttributeVector2d = Cast(Attr)) + SendToHoudini(AttributeVector2d, InputNodeId, PartId, Owner); + else if(auto* AttributeVector3d = Cast(Attr)) + SendToHoudini(AttributeVector3d, InputNodeId, PartId, Owner); + else if(auto* AttributeVector4d = Cast(Attr)) + SendToHoudini(AttributeVector4d, InputNodeId, PartId, Owner); + else if(auto* AttributeString = Cast(Attr)) + SendToHoudini(AttributeString, InputNodeId, PartId, Owner); + else if(auto* AttributeSoftObjectPath = Cast(Attr)) + SendToHoudini(AttributeSoftObjectPath, InputNodeId, PartId, Owner); + else if(auto* AttributeSoftClassPath = Cast(Attr)) + SendToHoudini(AttributeSoftClassPath, InputNodeId, PartId, Owner); + else + check(false); + } +} + +FUnrealObjectInputHandle +FUnrealPCGDataTranslator::CreateInputNode(const FString & Name, UObject * Object, bool bInputNodesCanBeDeleted) +{ + // Create Identifier for this object and handle + FUnrealObjectInputOptions Options; + FUnrealObjectInputIdentifier Identifier = FUnrealObjectInputIdentifier(Object, Options, true); + FUnrealObjectInputHandle Handle; + + if(FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + return Identifier; + } + + + // Make sure we have a parent node. + FUnrealObjectInputHandle ParentHandle; + + FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted); + HAPI_NodeId ParentNodeId = FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle); + + // Create the input node + FString FinalInputNodeName = Name; + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + HAPI_NodeId NewNodeId = FHoudiniEngineUtils::CreateInputHapiNode(FinalInputNodeName, ParentNodeId); + if(!FHoudiniEngineUtils::IsHoudiniNodeValid(NewNodeId)) + return {}; + + // Remove previous node and its parent. + HAPI_NodeId PreviousInputNodeId = FUnrealObjectInputUtils::GetHAPINodeId(Handle); + if(PreviousInputNodeId != INDEX_NONE) + { + HAPI_NodeId PreviousInputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousInputNodeId); + + if(FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), PreviousInputNodeId) != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input node for %s."), *FinalInputNodeName); + } + + if(FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), PreviousInputObjectNodeId) != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input object node for %s."), *FinalInputNodeName); + } + } + + HAPI_NodeId InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(NewNodeId); + FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, NewNodeId, Handle, InputObjectNodeId, nullptr, bInputNodesCanBeDeleted); + + return Handle; +} + +TArray +FUnrealPCGDataTranslator::CreateInputNodeForPCGSplineData(const FString& InputNodeName, UHoudiniPCGDataCollection* PCGDataCollection, bool bInputNodesCanBeDeleted) +{ + TArray Results; + + for(auto& SplineObject : PCGDataCollection->Splines) + { + FUnrealObjectInputHandle Handle = CreateInputNodeForPCGSplineData(InputNodeName, SplineObject, bInputNodesCanBeDeleted); + Results.Add(Handle); + } + return Results; +} + +FUnrealObjectInputHandle +FUnrealPCGDataTranslator::CreateInputNodeForPCGSplineData(const FString& InputNodeName, UHoudiniPCGDataObject* PCGDataObject, bool bInputNodesCanBeDeleted) +{ + FString InputName = InputNodeName + PCGDataObject->GetName(); + FUnrealObjectInputHandle Handle = FUnrealPCGDataTranslator::CreateInputNode( + InputName, + PCGDataObject, + bInputNodesCanBeDeleted); + + if(!Handle.IsValid()) + return Handle; + + HAPI_NodeId NodeId = FUnrealObjectInputUtils::GetHAPINodeId(Handle); + + UHoudiniPCGDataAttributeVector3d * PosAttr = Cast(PCGDataObject->FindAttribute(TEXT("P"))); + if(!PosAttr) + return {}; + + int NumSegments = 1; + int NumPositions = PosAttr->Values.Num(); + int CurveCounts = NumPositions; + + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + Part.type = HAPI_PARTTYPE_CURVE; + Part.pointCount = NumPositions; + Part.vertexCount = NumPositions; + Part.faceCount = NumSegments; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo(FHoudiniEngine::Get().GetSession(), NodeId, 0, &Part), {}); + + HAPI_CurveInfo CurveInfo; + FHoudiniApi::CurveInfo_Init(&CurveInfo); + CurveInfo.curveType = HAPI_CURVETYPE_LINEAR; + CurveInfo.curveCount = NumSegments; + CurveInfo.vertexCount = NumPositions; + CurveInfo.knotCount = 0; + CurveInfo.isPeriodic = false; + CurveInfo.isRational = false; + CurveInfo.order = 0; + CurveInfo.hasKnots = false; + CurveInfo.isClosed = PCGDataObject->bIsClosed; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetCurveInfo(FHoudiniEngine::Get().GetSession(), NodeId, 0, &CurveInfo), {}); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetCurveCounts(FHoudiniEngine::Get().GetSession(), NodeId, Part.id, &CurveCounts, 0, 1), {}); + + SendToHoudini(PosAttr, NodeId, Part.id, HAPI_ATTROWNER_POINT); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(NodeId), {}); + + HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + CookOptions.maxVerticesPerPrimitive = -1; + CookOptions.refineCurveToLinear = false; + static constexpr bool bWaitForCompletion = false; + FHoudiniEngineUtils::HapiCookNode(NodeId, &CookOptions, bWaitForCompletion); + + return Handle; +} + + +FUnrealObjectInputHandle +FUnrealPCGDataTranslator::CreateInputNodeForPCGAttrData(const FString& InputNodeName, UHoudiniPCGDataCollection* PCGCollection, bool bInputNodesCanBeDeleted) +{ + if(!IsValid(PCGCollection->Points)) + { + HOUDINI_PCG_ERROR(TEXT("Not able to process a PCG Data without points")); + return {}; + } + + FUnrealObjectInputHandle Handle = FUnrealPCGDataTranslator::CreateInputNode( + InputNodeName, + PCGCollection, + bInputNodesCanBeDeleted); + + if(!Handle.IsValid()) + return Handle; + + int NumPoints = PCGCollection->Points ? PCGCollection->Points->GetNumRows() : 0; + int NumPrims = PCGCollection->Primitives ? PCGCollection->Primitives->GetNumRows() : 0; + int NumVertices = PCGCollection->Vertices ? PCGCollection->Vertices->GetNumRows() : 0; + int NumDetails = PCGCollection->Details ? PCGCollection->Details->GetNumRows() : 0; + + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = PCGCollection->Points->Attributes.Num(); + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = PCGCollection->Primitives ? PCGCollection->Primitives->Attributes.Num() : 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = PCGCollection->Vertices ? PCGCollection->Vertices->Attributes.Num() : 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = PCGCollection->Details ? PCGCollection->Details->Attributes.Num() : 0; + Part.vertexCount = NumVertices; + Part.faceCount = NumPrims; + Part.pointCount = NumPoints; + Part.type = HAPI_PARTTYPE_MESH; + + HAPI_NodeId NodeId = FUnrealObjectInputUtils::GetHAPINodeId(Handle); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo(FHoudiniEngine::Get().GetSession(), NodeId, 0, &Part), {}); + + SetAttributes(PCGCollection->Points, NodeId, Part.id, HAPI_ATTROWNER_POINT); + SetAttributes(PCGCollection->Vertices, NodeId, Part.id, HAPI_ATTROWNER_VERTEX); + SetAttributes(PCGCollection->Primitives, NodeId, Part.id, HAPI_ATTROWNER_PRIM); + SetAttributes(PCGCollection->Details, NodeId, Part.id, HAPI_ATTROWNER_DETAIL); + + // We need to generate array of face counts. + if (Part.faceCount) + { + TArray StaticMeshFaceCounts; + StaticMeshFaceCounts.SetNumUninitialized(Part.faceCount); + for(int32 n = 0; n < Part.faceCount; n++) + StaticMeshFaceCounts[n] = 3; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetFaceCounts( + StaticMeshFaceCounts, NodeId, 0), {}); + } + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(NodeId), {}); + if(!FHoudiniEngineUtils::HapiCookNode(NodeId, nullptr, true)) + return {}; + + return Handle; +} + +FString GetHapiName(FName AttrName) +{ + FString HapiName = AttrName.ToString(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(HapiName); + return HapiName; +} + +void FUnrealPCGDataTranslator::SendToHoudini(UHoudiniPCGDataAttributeFloat * Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InputNodeId, PartId, H_TCHAR_TO_UTF8((*GetHapiName(Data->GetAttrName())))); + Accessor.AddAttribute(Owner, HAPI_StorageType::HAPI_STORAGETYPE_FLOAT, 1,Data->Values.Num(), &AttrInfo); + Accessor.SetAttributeData(AttrInfo,Data->Values); +} + +void FUnrealPCGDataTranslator::SendToHoudini(UHoudiniPCGDataAttributeDouble * Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InputNodeId, PartId, H_TCHAR_TO_UTF8((*GetHapiName(Data->GetAttrName())))); + Accessor.AddAttribute(Owner, HAPI_StorageType::HAPI_STORAGETYPE_FLOAT64, 1,Data->Values.Num(), &AttrInfo); + Accessor.SetAttributeData(AttrInfo,Data->Values); +} + +void FUnrealPCGDataTranslator::SendToHoudini(UHoudiniPCGDataAttributeInt * Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InputNodeId, PartId, H_TCHAR_TO_UTF8((*GetHapiName(Data->GetAttrName())))); + Accessor.AddAttribute(Owner, HAPI_StorageType::HAPI_STORAGETYPE_INT, 1,Data->Values.Num(), &AttrInfo); + Accessor.SetAttributeData(AttrInfo,Data->Values); +} + +void FUnrealPCGDataTranslator::SendToHoudini(UHoudiniPCGDataAttributeInt64 * Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InputNodeId, PartId, H_TCHAR_TO_UTF8((*GetHapiName(Data->GetAttrName())))); + Accessor.AddAttribute(Owner, HAPI_StorageType::HAPI_STORAGETYPE_INT64, 1,Data->Values.Num(), &AttrInfo); + Accessor.SetAttributeData(AttrInfo,Data->Values); +} + +void FUnrealPCGDataTranslator::SendToHoudini(UHoudiniPCGDataAttributeString* Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InputNodeId, PartId, H_TCHAR_TO_UTF8((*GetHapiName(Data->GetAttrName())))); + Accessor.AddAttribute(Owner, HAPI_StorageType::HAPI_STORAGETYPE_STRING, 1, Data->Values.Num(), &AttrInfo); + Accessor.SetAttributeData(AttrInfo, Data->Values); +} + +void FUnrealPCGDataTranslator::SendToHoudini(UHoudiniPCGDataAttributeSoftObjectPath * Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InputNodeId, PartId, H_TCHAR_TO_UTF8((*GetHapiName(Data->GetAttrName())))); + Accessor.AddAttribute(Owner, HAPI_StorageType::HAPI_STORAGETYPE_STRING, 1, Data->Values.Num(), &AttrInfo); + + TArray StringValues; + StringValues.Reserve(Data->Values.Num()); + for(auto& Path : Data->Values) + StringValues.Add(Path.ToString()); + + Accessor.SetAttributeData(AttrInfo, StringValues); +} + +void FUnrealPCGDataTranslator::SendToHoudini(UHoudiniPCGDataAttributeSoftClassPath* Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InputNodeId, PartId, H_TCHAR_TO_UTF8((*GetHapiName(Data->GetAttrName())))); + Accessor.AddAttribute(Owner, HAPI_StorageType::HAPI_STORAGETYPE_STRING, 1, Data->Values.Num(), &AttrInfo); + + TArray StringValues; + StringValues.Reserve(Data->Values.Num()); + for(auto& Path : Data->Values) + StringValues.Add(Path.ToString()); + + + Accessor.SetAttributeData(AttrInfo, StringValues); +} + +void FUnrealPCGDataTranslator::SendToHoudini(UHoudiniPCGDataAttributeVector2d * Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InputNodeId, PartId, H_TCHAR_TO_UTF8((*GetHapiName(Data->GetAttrName())))); + Accessor.AddAttribute(Owner, HAPI_StorageType::HAPI_STORAGETYPE_FLOAT, 2, Data->Values.Num(), &AttrInfo); + + TArray FloatValues; + FloatValues.SetNum(Data->Values.Num() * 2); + for(int Index = 0; Index Values.Num(); Index++) + { + FloatValues[Index * 2 + 0] = Data->Values[Index].X; + FloatValues[Index * 2 + 1] = Data->Values[Index].Y; + } + Accessor.SetAttributeData(AttrInfo, FloatValues); +} + + +void FUnrealPCGDataTranslator::SendToHoudini(UHoudiniPCGDataAttributeVector3d * Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InputNodeId, PartId, H_TCHAR_TO_UTF8((*GetHapiName(Data->GetAttrName())))); + Accessor.AddAttribute(Owner, HAPI_StorageType::HAPI_STORAGETYPE_FLOAT, 3, Data->Values.Num(), &AttrInfo); + + TArray FloatValues; + FloatValues.SetNum(Data->Values.Num() * 3); + for(int Index = 0; Index Values.Num(); Index++) + { + FloatValues[Index * 3 + 0] = Data->Values[Index].X; + FloatValues[Index * 3 + 1] = Data->Values[Index].Y; + FloatValues[Index * 3 + 2] = Data->Values[Index].Z; + } + Accessor.SetAttributeData(AttrInfo, FloatValues); +} + +void FUnrealPCGDataTranslator::SendToHoudini(UHoudiniPCGDataAttributeVector4d * Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InputNodeId, PartId, H_TCHAR_TO_UTF8((*GetHapiName(Data->GetAttrName())))); + Accessor.AddAttribute(Owner, HAPI_StorageType::HAPI_STORAGETYPE_FLOAT, 4, Data->Values.Num(), &AttrInfo); + TArray FloatValues; + FloatValues.SetNum(Data->Values.Num() * 4); + for(int Index = 0; Index Values.Num(); Index++) + { + FloatValues[Index * 4 + 0] = Data->Values[Index].X; + FloatValues[Index * 4 + 1] = Data->Values[Index].Y; + FloatValues[Index * 4 + 2] = Data->Values[Index].Z; + FloatValues[Index * 4 + 3] = Data->Values[Index].W; + } + Accessor.SetAttributeData(AttrInfo, FloatValues); +} + +void FUnrealPCGDataTranslator::SendToHoudini(UHoudiniPCGDataAttributeQuat * Data,HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InputNodeId, PartId, H_TCHAR_TO_UTF8((*GetHapiName(Data->GetAttrName())))); + Accessor.AddAttribute(Owner, HAPI_StorageType::HAPI_STORAGETYPE_FLOAT, 4,Data->Values.Num(), &AttrInfo); + + TArray FloatValues; + FloatValues.SetNum(Data->Values.Num() * 4); + for(int Index = 0; Index Values.Num(); Index++) + { + // Note, no intentional Unreal swizzling or scaling here, we don't know the type. + FVector4d Vector = FHoudiniPCGUtils::UnrealToHoudiniQuat(Data->Values[Index]); + FloatValues[Index * 4 + 0] = Vector.X; + FloatValues[Index * 4 + 1] = Vector.Y; + FloatValues[Index * 4 + 2] = Vector.Z; + FloatValues[Index * 4 + 3] = Vector.W; + } + Accessor.SetAttributeData(AttrInfo, FloatValues); +} + +void SendToHoudini(UHoudiniPCGDataAttributeString * Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner) +{ + HAPI_AttributeInfo AttrInfo; + FHoudiniHapiAccessor Accessor(InputNodeId, PartId, H_TCHAR_TO_UTF8((*GetHapiName(Data->GetAttrName())))); + Accessor.AddAttribute(Owner, HAPI_StorageType::HAPI_STORAGETYPE_STRING, 1,Data->Values.Num(), &AttrInfo); + Accessor.SetAttributeData(AttrInfo,Data->Values); +} + +``` + +`Source/HoudiniEngine/Private/UnrealPCGDataTranslator.h`: + +```h +/* + * Copyright (c) <2021> Side Effects Software Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. The name of Side Effects Software may not be used to endorse or + * promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include + +#include "HoudiniPCGDataObject.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "HAPI/HAPI_Common.h" +#include "UObject/NameTypes.h" + +class UPCGPointData; +class FUnrealObjectInputHandle; +class UHoudiniPCGDataObject; + +struct HOUDINIENGINE_API FUnrealPCGDataTranslator +{ +public: + static bool CreateInputNodeForPCGData( + UHoudiniPCGDataCollection* PCGData, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + bool bInputNodesCanBeDeleted); + +protected: + static FUnrealObjectInputHandle CreateInputNodeForPCGAttrData(const FString& InputNodeName, UHoudiniPCGDataCollection* InputData, bool bInputNodesCanBeDeleted); + static TArray CreateInputNodeForPCGSplineData(const FString& InputNodeName, UHoudiniPCGDataCollection* InputData, bool bInputNodesCanBeDeleted); + static FUnrealObjectInputHandle CreateInputNodeForPCGSplineData(const FString& InputNodeName, UHoudiniPCGDataObject* InputData, bool bInputNodesCanBeDeleted); + + static void SetAttributes(UHoudiniPCGDataObject* PCGDataObject, HAPI_NodeId Node, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + + static void SendToHoudini(UHoudiniPCGDataAttributeFloat* Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + static void SendToHoudini(UHoudiniPCGDataAttributeInt* Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + static void SendToHoudini(UHoudiniPCGDataAttributeDouble* Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + static void SendToHoudini(UHoudiniPCGDataAttributeString* Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + static void SendToHoudini(UHoudiniPCGDataAttributeInt64* Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + static void SendToHoudini(UHoudiniPCGDataAttributeVector2d* Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + static void SendToHoudini(UHoudiniPCGDataAttributeVector3d* Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + static void SendToHoudini(UHoudiniPCGDataAttributeVector4d* Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + static void SendToHoudini(UHoudiniPCGDataAttributeQuat* Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + static void SendToHoudini(UHoudiniPCGDataAttributeSoftObjectPath* Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + static void SendToHoudini(UHoudiniPCGDataAttributeSoftClassPath* Data, HAPI_NodeId InputNodeId, HAPI_PartId PartId, HAPI_AttributeOwner Owner); + + static FUnrealObjectInputHandle CreateInputNode(const FString& Name, UObject* Object, bool bInputNodesCanBeDeleted); + +}; + +``` + +`Source/HoudiniEngine/Private/UnrealSkeletalMeshTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealSkeletalMeshTranslator.h" + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineString.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniSkeletalMeshUtils.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputRuntimeUtils.h" +#include "UnrealObjectInputUtils.h" +#include "UnrealMeshTranslator.h" + +#include "Animation/Skeleton.h" +#include "Components/SkeletalMeshComponent.h" +#include "Engine/SkeletalMesh.h" +#include "Engine/SkeletalMeshSocket.h" +#include "PhysicsEngine/PhysicsAsset.h" +#include "PhysicalMaterials/PhysicalMaterial.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + #include "PhysicsEngine/AggregateGeom.h" + #include "PhysicsEngine/SkeletalBodySetup.h" +#endif +#include "SkeletalMeshAttributes.h" +#include "Rendering/SkeletalMeshModel.h" + + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + #include "Engine/SkinnedAssetCommon.h" +#endif + + +FTransform +GetCompSpaceTransformForBone(const FReferenceSkeleton& InSkel, const int32& InBoneIdx) +{ + FTransform resultBoneTransform = InSkel.GetRefBonePose()[InBoneIdx]; + + auto refBoneInfo = InSkel.GetRefBoneInfo(); + + int32 Bone = InBoneIdx; + while (Bone) + { + resultBoneTransform *= InSkel.GetRefBonePose()[refBoneInfo[Bone].ParentIndex]; + Bone = refBoneInfo[Bone].ParentIndex; + } + + return resultBoneTransform; +} + +void +GetComponentSpaceTransforms(TArray& OutResult, const FReferenceSkeleton& InRefSkeleton) +{ + const int32 PoseNum = InRefSkeleton.GetRefBonePose().Num(); + OutResult.SetNum(PoseNum); + + for (int32 i = 0; i < PoseNum; i++) + { + OutResult[i] = GetCompSpaceTransformForBone(InRefSkeleton, i); + + } +} + +bool +FUnrealSkeletalMeshTranslator::HapiCreateInputNodeForSkeletalMesh( + USkeletalMesh* SkeletalMesh, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + USkeletalMeshComponent* SkeletalMeshComponent /*=nullptr*/, + const FUnrealMeshExportOptions& ExportOptions, + const bool& bInputNodesCanBeDeleted /*=true*/, + const bool& bExportMaterialParameters /*= false*/) +{ + // Create nodes for the mesh data + FUnrealObjectInputHandle SKMeshHandle; + if (!CreateInputNodesForSkeletalMesh( + SkeletalMesh, + InputNodeId, + InputNodeName, + SKMeshHandle, + SkeletalMeshComponent /*=nullptr*/, + ExportOptions, + bInputNodesCanBeDeleted /*=true*/, + bExportMaterialParameters /*= false*/)) + { + return false; + } + + // Now create the capture pose node + HAPI_NodeId CapturePoseNodeId = -1; + HAPI_NodeId PackFolderNodeId = -1; + + { + // Create input node for the capture pose only + FUnrealObjectInputHandle CapturePoseHandle; + if (!CreateInputNodeForCapturePose( + SkeletalMesh, + -1, + InputNodeName + TEXT("_capture_pose"), + CapturePoseNodeId, + CapturePoseHandle, + bInputNodesCanBeDeleted)) + { + return false; + } + + // Build the identifier for the reference node that represents the full SKMesh with capture pose + FUnrealObjectInputOptions Options = SKMeshHandle.GetIdentifier().GetOptions(); + Options.AddBoolOption(TEXT("bCapturePose"), true); + FUnrealObjectInputIdentifier Identifier(SkeletalMesh, Options, false); + FUnrealObjectInputHandle RefNodeHandle; + // Check if the exists in the manager and get the HAPI NodeId for it (if valid) + if (FUnrealObjectInputUtils::FindNodeViaManager(Identifier, RefNodeHandle)) + FUnrealObjectInputUtils::GetHAPINodeId(RefNodeHandle, PackFolderNodeId); + + FUnrealObjectInputHandle ParentHandle; + HAPI_NodeId ParentNodeId = -1; + HAPI_NodeId GeoObjectNodeId = -1; + // If the HAPI Node Id < 0 it means there was no entry in the manager, or HAPI Node Id for it is invalid. + // Create the packfolder node + if (PackFolderNodeId < 0) + { + FString FinalInputNodeName = InputNodeName + TEXT("_packed"); + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + // Create any parent/container nodes that we would need, and get the node id of the immediate parent + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // Create geo object node + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniEngineUtils::CreateNode(ParentNodeId, TEXT("geo"), FinalInputNodeName, true, &GeoObjectNodeId), false); + // Create packfolder node + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniEngineUtils::CreateNode(GeoObjectNodeId, TEXT("packfolder"), FinalInputNodeName, true, &PackFolderNodeId), false); + } + else + { + GeoObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(PackFolderNodeId); + } + + // Update the entry for the reference node in the manager: the node reference the SKMesh nodes (mesh, colliders, + // LOD, sockets) and the capture pose node + TSet RefNodes { SKMeshHandle, CapturePoseHandle }; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, PackFolderNodeId, RefNodeHandle, GeoObjectNodeId, &RefNodes, bInputNodesCanBeDeleted)) + OutHandle = RefNodeHandle; + } + + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + + // Connect input 0 to skeletalmesh data + if (!FHoudiniEngineUtils::HapiConnectNodeInput(PackFolderNodeId, 1, InputNodeId, 0, -1)) + return false; + + // Connect input 1 to the capture pose + if (!FHoudiniEngineUtils::HapiConnectNodeInput(PackFolderNodeId, 2, CapturePoseNodeId, 0, -1)) + return false; + + // Cook the packfolder node + if (!FHoudiniEngineUtils::HapiCookNode(PackFolderNodeId, nullptr, true)) + return false; + + // Set the name# and type# attributes + HAPI_ParmInfo ParmInfo; + FHoudiniApi::ParmInfo_Init(&ParmInfo); + int32 ParmId = FHoudiniEngineUtils::HapiFindParameterByName(PackFolderNodeId, "name1", ParmInfo); + if (ParmId >= 0) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue(Session, PackFolderNodeId, TCHAR_TO_ANSI(TEXT("Base")), ParmId, 0), false); + ParmId = FHoudiniEngineUtils::HapiFindParameterByName(PackFolderNodeId, "type1", ParmInfo); + if (ParmId >= 0) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue(Session, PackFolderNodeId, TCHAR_TO_ANSI(TEXT("shp")), ParmId, 0), false); + + ParmId = FHoudiniEngineUtils::HapiFindParameterByName(PackFolderNodeId, "name2", ParmInfo); + if (ParmId >= 0) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue(Session, PackFolderNodeId, TCHAR_TO_ANSI(TEXT("Base")), ParmId, 0), false); + ParmId = FHoudiniEngineUtils::HapiFindParameterByName(PackFolderNodeId, "type2", ParmInfo); + if (ParmId >= 0) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue(Session, PackFolderNodeId, TCHAR_TO_ANSI(TEXT("skel")), ParmId, 0), false); + + InputNodeId = PackFolderNodeId; + + return true; +} + +bool +FUnrealSkeletalMeshTranslator::CreateInputNodesForSkeletalMesh( + USkeletalMesh* SkeletalMesh, + HAPI_NodeId& InputNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + USkeletalMeshComponent* SkeletalMeshComponent /*=nullptr*/, + const FUnrealMeshExportOptions& ExportOptions, + const bool& bInputNodesCanBeDeleted /*=true*/, + const bool& bExportMaterialParameters /*= false*/) +{ + // If we don't have a skeletal mesh there's nothing to do. + if (!IsValid(SkeletalMesh)) + return false; + + // Input node name, defaults to InputNodeName, but can be changed by the new input system + FString FinalInputNodeName = InputNodeName; + + // Find the node in new input system + // Identifier will be the identifier for the entry created in this call of the function. + // We may call this function recursively to create the main mesh, LODs, sockets and colliders, each getting its own identifier. + FUnrealObjectInputIdentifier Identifier; + FUnrealObjectInputHandle ParentHandle; + HAPI_NodeId ParentNodeId = -1; + + { + // Check if we already have an input node for this asset + static constexpr bool bForceCreateInputRefNode = false; + bool bSingleLeafNodeOnly = false; + FUnrealObjectInputIdentifier IdentReferenceNode; + TArray IdentPerOption; + static constexpr bool bMainMeshIsNaniteFallbackMesh = false; + + + if (!FUnrealObjectInputUtils::BuildMeshInputObjectIdentifiers( + SkeletalMesh, + ExportOptions, + bMainMeshIsNaniteFallbackMesh, + bExportMaterialParameters, + bForceCreateInputRefNode, + bSingleLeafNodeOnly, + IdentReferenceNode, + IdentPerOption)) + { + return false; + } + + if (bSingleLeafNodeOnly) + { + // We'll create the skeletal mesh input node entirely is this function call + check(!IdentPerOption.IsEmpty()); + Identifier = IdentPerOption[0]; + } + else + { + // Look for the reference node that references the per-option (LODs, colliders) nodes + Identifier = IdentReferenceNode; + } + + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId) && (bSingleLeafNodeOnly || FUnrealObjectInputUtils::AreReferencedHAPINodesValid(Handle))) + { + if (!bInputNodesCanBeDeleted) + { + // Make sure to prevent deletion of the input node if needed + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + } + + OutHandle = Handle; + InputNodeId = NodeId; + return true; + } + } + + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + // Create any parent/container nodes that we would need, and get the node id of the immediate parent + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // We now need to create the nodes (since we couldn't find existing ones in the manager) + // For the single leaf node case we can simply continue this function + // For the ref + multiple options, we call this function again for each option (as a single leaf node) and + // then create the reference node. + if (!bSingleLeafNodeOnly) + { + TSet PerOptionNodeHandles; + PerOptionNodeHandles.Reserve(IdentPerOption.Num()); + for (const FUnrealObjectInputIdentifier& OptionIdentifier : IdentPerOption) + { + const FUnrealObjectInputOptions& Options = OptionIdentifier.GetOptions(); + + FString NodeLabel; + FUnrealObjectInputUtils::GetDefaultInputNodeName(OptionIdentifier, NodeLabel); + + HAPI_NodeId NewNodeId = -1; + FUnrealObjectInputHandle OptionHandle; + if (FUnrealObjectInputUtils::FindNodeViaManager(OptionIdentifier, OptionHandle)) + { + FUnrealObjectInputUtils::GetHAPINodeId(OptionHandle, NewNodeId); + } + + FUnrealMeshExportOptions ExportInputOptions; + ExportInputOptions.bLODs = Options.bExportLODs; + ExportInputOptions.bSockets = Options.bExportSockets; + ExportInputOptions.bColliders = Options.bExportColliders; + ExportInputOptions.bMainMesh = !Options.bExportLODs && !Options.bExportSockets && !Options.bExportColliders; + // Recursive call + if (!FUnrealSkeletalMeshTranslator::CreateInputNodesForSkeletalMesh( + SkeletalMesh, + NewNodeId, + NodeLabel, + OptionHandle, + SkeletalMeshComponent, + ExportInputOptions, + bInputNodesCanBeDeleted, + Options.bExportMaterialParameters)) + { + return false; + } + + PerOptionNodeHandles.Add(OptionHandle); + } + + // Create or update the HAPI node for the reference node if it does not exist + FUnrealObjectInputHandle RefNodeHandle; + if (!FUnrealObjectInputUtils::CreateOrUpdateReferenceInputMergeNode(IdentReferenceNode, PerOptionNodeHandles, RefNodeHandle, true, bInputNodesCanBeDeleted)) + return false; + + OutHandle = RefNodeHandle; + FUnrealObjectInputUtils::GetHAPINodeId(IdentReferenceNode, InputNodeId); + return true; + } + + // Set InputNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that InputNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (Handle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, InputNodeId)) + InputNodeId = -1; + } + else + { + InputNodeId = -1; + } + } + + // Node ID for the newly created node + HAPI_NodeId NewNodeId = -1; + + bool DoExportSockets = ExportOptions.bSockets && (SkeletalMesh->NumSockets() > 0); + bool DoExportLODs = ExportOptions.bLODs && (SkeletalMesh->GetLODNum() > 1); + + // Export colliders if there are some + // For Skeletal mesh, we need to look at all the SKBodySetups + bool DoExportColliders = false; + TArray> BodySetups; + if (ExportOptions.bColliders && SkeletalMesh->GetPhysicsAsset()) + { + BodySetups = SkeletalMesh->GetPhysicsAsset()->SkeletalBodySetups; + for (auto& CurBS : BodySetups) + { + if (CurBS->AggGeom.GetElementCount() <= 0) + continue; + + // We found at least one collider, we'll need to export them! + DoExportColliders = true; + break; + } + } + + // We need to use a merge node if we export lods OR sockets + bool UseMergeNode = DoExportLODs || DoExportSockets || DoExportColliders; + if (UseMergeNode) + { + // TODO: + // What if OutInputNodeId already exists? + // Delete previous merge?/input? + + // Create a merge SOP asset. This will be our "InputNodeId" + // as all the different LOD meshes and sockets will be plugged into it + if (ParentNodeId < 0) + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + -1, TEXT("SOP/merge"), FinalInputNodeName, true, &NewNodeId), false); + } + else + { + // When creating a node inside a parent node (in other words, ParentNodeId is not -1), then we cannot + // specify the node type category prefix on the node name. We have to create the geo Object and merge + // SOPs separately. + HAPI_NodeId ObjectNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + ParentNodeId, TEXT("geo"), FinalInputNodeName, true, &ObjectNodeId), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + ObjectNodeId, TEXT("merge"), FinalInputNodeName, true, &NewNodeId), false); + } + } + else + { + // No LODs/Sockets, we just need a single input node + // If InputNodeId is invalid, we need to create an input node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateInputNode(FinalInputNodeName, NewNodeId, ParentNodeId), false); + if (!FHoudiniEngineUtils::HapiCookNode(NewNodeId, nullptr, true)) + return false; + } + + // Check if we have a valid id for this new input asset. + if (!FHoudiniEngineUtils::IsHoudiniNodeValid(NewNodeId)) + return false; + + HAPI_NodeId PreviousInputNodeId = InputNodeId; + + // Update our input NodeId + InputNodeId = NewNodeId; + // Get our parent OBJ NodeID + HAPI_NodeId InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(NewNodeId); + + // We have now created a valid new input node, delete the previous one + if (PreviousInputNodeId >= 0) + { + // Get the parent OBJ node ID before deleting! + HAPI_NodeId PreviousInputOBJNode = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousInputNodeId); + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input node for %s."), *FinalInputNodeName); + } + + if (HAPI_RESULT_SUCCESS != FHoudiniApi::DeleteNode( + FHoudiniEngine::Get().GetSession(), PreviousInputOBJNode)) + { + HOUDINI_LOG_WARNING(TEXT("Failed to cleanup the previous input OBJ node for %s."), *FinalInputNodeName); + } + } + + // Next Index used to connect nodes to the merge + int32 NextMergeIndex = 0; + + // Determine which LODs to export based on the ExportLODs/bMainMesh, high res mesh availability and whether + // the new input system is being used. + const int32 NumLODs = SkeletalMesh->GetLODNum(); + int32 FirstLODIndex = 0; + int32 LastLODIndex = -1; + + if (DoExportLODs) + { + LastLODIndex = NumLODs - 1; + + // When using the new input system + // Don't export LOD0 with the LODs since we have a separate "main mesh" input + FirstLODIndex = 1; + } + else if (ExportOptions.bMainMesh) + { + // Just export the main mesh (LOD0) + LastLODIndex = 0; + FirstLODIndex = 0; + } + else + { + // Dont export any LOD + LastLODIndex = -1; + FirstLODIndex = 0; + } + + if (LastLODIndex >= 0) + { + for (int32 LODIndex = FirstLODIndex; LODIndex <= LastLODIndex; LODIndex++) + { + // If we're using a merge node, we need to create a new input null + HAPI_NodeId CurrentLODNodeId = -1; + if (UseMergeNode) + { + // Create a new input node for the current LOD + FString LODName = TEXT("lod") + FString::FromInt(LODIndex); + + // Create the node in this input object's OBJ node + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + InputObjectNodeId, TEXT("null"), H_TCHAR_TO_UTF8(*LODName), false, &CurrentLODNodeId), false); + } + else + { + // No merge node, just use the input node we created before + CurrentLODNodeId = NewNodeId; + } + + // Set the skeletal mesh data for this lod on the input node + static constexpr bool bUseMeshDescription = true; // SkeletalMesh->IsLODImportedDataBuildAvailable(LODIndex) && !SkeletalMesh->IsLODImportedDataEmpty(LODIndex); + if (bUseMeshDescription) + { + // Use mesh description + if (!SetSkeletalMeshDataOnNodeFromMeshDescription( + SkeletalMesh, SkeletalMeshComponent, CurrentLODNodeId, LODIndex, DoExportLODs, bExportMaterialParameters)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to set the skeletal mesh data on the input node for %s LOD %d."), *InputNodeName, LODIndex); + continue; + } + } + else if (!FUnrealSkeletalMeshTranslator::SetSkeletalMeshDataOnNodeFromSourceModel( + SkeletalMesh, SkeletalMeshComponent, CurrentLODNodeId, LODIndex, DoExportLODs, bExportMaterialParameters)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to set the skeletal mesh data on the input node for %s LOD %d."), *InputNodeName, LODIndex); + continue; + } + + // Create captureattribpack + HAPI_NodeId CaptureAttribPackNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + InputObjectNodeId, TEXT("captureattribpack"), FString::Printf(TEXT("captureattribpack%d"), LODIndex), false, &CaptureAttribPackNodeId), false); + + // Connect LOD node + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + CaptureAttribPackNodeId, 0, CurrentLODNodeId, 0), false); + + if (UseMergeNode) + { + // Connect the capture attrib pack to the merge node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + NewNodeId, NextMergeIndex, CaptureAttribPackNodeId, 0), false); + } + else + { + // If we are not merging, then our output node becomes CaptureAttribPackNodeId + NewNodeId = CaptureAttribPackNodeId; + // Ensure that the display flag is set + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetNodeDisplay( + FHoudiniEngine::Get().GetSession(), NewNodeId, 1), false); + } + + NextMergeIndex++; + } + } + + // TODO: duplicate with Static Mesh + // Refactor this and the static mesh to a exportcolliders function taking a body setup? + if (DoExportColliders && BodySetups.Num() > 0) + { + FReferenceSkeleton RefSK = SkeletalMesh->GetRefSkeleton(); + TArray AllBonePos = RefSK.GetRawRefBonePose(); + + for (auto& BodySetup : BodySetups) + { + FKAggregateGeom SimpleColliders = BodySetup->AggGeom; + + // If there are no simple colliders to create then skip this bodysetup + if (SimpleColliders.BoxElems.Num() + SimpleColliders.SphereElems.Num() + SimpleColliders.SphylElems.Num() + + SimpleColliders.ConvexElems.Num() <= 0) + continue; + + HAPI_NodeId CollisionMergeNodeId = -1; + int32 NextCollisionMergeIndex = 0; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + InputObjectNodeId, TEXT("merge"), TEXT("simple_colliders_merge") + FString::FromInt(NextMergeIndex), false, &CollisionMergeNodeId), false); + + // Calculate the colliders transform + // They are stored relative to a bone, so we first need to get the corresponding bone's transform + // by going up the chains of bones until we reach the root bone + int32 BoneIndex = RefSK.FindBoneIndex(BodySetup->BoneName); + FTransform BoneTransform = AllBonePos.IsValidIndex(BoneIndex) ? AllBonePos[BoneIndex] : FTransform::Identity; + do + { + int32 ParentIndex = RefSK.GetParentIndex(BoneIndex); + if (ParentIndex != INDEX_NONE) + { + BoneTransform = BoneTransform * AllBonePos[ParentIndex]; + } + + BoneIndex = ParentIndex; + } while (RefSK.IsValidIndex(BoneIndex)); + + + // Export BOX colliders + for (auto& CurBox : SimpleColliders.BoxElems) + { + FTransform BoxTransform = CurBox.GetTransform() * BoneTransform; + + FVector BoxCenter = BoxTransform.GetLocation();//CurBox.Center; + FVector BoxExtent = FVector(CurBox.X, CurBox.Y, CurBox.Z); + FRotator BoxRotation = BoxTransform.GetRotation().Rotator();//CurBox.Rotation; + + HAPI_NodeId BoxNodeId = -1; + if (!FUnrealMeshTranslator::CreateInputNodeForBox( + BoxNodeId, InputObjectNodeId, NextCollisionMergeIndex, + BoxCenter, BoxExtent, BoxRotation)) + continue; + + if (BoxNodeId < 0) + continue; + + // Connect the Box node to the merge node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + CollisionMergeNodeId, NextCollisionMergeIndex, BoxNodeId, 0), false); + + NextCollisionMergeIndex++; + } + + // Export SPHERE colliders + for (auto& CurSphere : SimpleColliders.SphereElems) + { + FTransform SphereTransform = CurSphere.GetTransform() * BoneTransform; + FVector SphereCenter = SphereTransform.GetLocation();// CurSphere.Center; + + HAPI_NodeId SphereNodeId = -1; + if (!FUnrealMeshTranslator::CreateInputNodeForSphere( + SphereNodeId, InputObjectNodeId, NextCollisionMergeIndex, + SphereCenter, CurSphere.Radius)) + continue; + + if (SphereNodeId < 0) + continue; + + // Connect the Sphere node to the merge node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + CollisionMergeNodeId, NextCollisionMergeIndex, SphereNodeId, 0), false); + + NextCollisionMergeIndex++; + } + + // Export CAPSULE colliders + for (auto& CurSphyl : SimpleColliders.SphylElems) + { + FTransform SphylTransform = CurSphyl.GetTransform() * BoneTransform; + + FVector SphylCenter = SphylTransform.GetLocation(); // CurSphyl.Center + FRotator SphylRotation = SphylTransform.GetRotation().Rotator();//CurSphyl.Rotation; + + HAPI_NodeId SphylNodeId = -1; + if (!FUnrealMeshTranslator::CreateInputNodeForSphyl( + SphylNodeId, InputObjectNodeId, NextCollisionMergeIndex, + SphylCenter, SphylRotation, CurSphyl.Radius, CurSphyl.Length)) + continue; + + if (SphylNodeId < 0) + continue; + + // Connect the capsule node to the merge node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + CollisionMergeNodeId, NextCollisionMergeIndex, SphylNodeId, 0), false); + + NextCollisionMergeIndex++; + } + + // TODO!! Insert bone transform here!! + // Export CONVEX colliders + for (auto& CurConvex : SimpleColliders.ConvexElems) + { + HAPI_NodeId ConvexNodeId = -1; + if (!FUnrealMeshTranslator::CreateInputNodeForConvex( + ConvexNodeId, InputObjectNodeId, NextCollisionMergeIndex, CurConvex)) + continue; + + if (ConvexNodeId < 0) + continue; + + // Connect the capsule node to the merge node. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + CollisionMergeNodeId, NextCollisionMergeIndex, ConvexNodeId, 0), false); + + NextCollisionMergeIndex++; + } + + // Create a new primitive attribute where each value contains the Physical Material name in Unreal. + UPhysicalMaterial* PhysicalMaterial = BodySetup->PhysMaterial; + if (PhysicalMaterial) + { + // Create a new Attribute Wrangler node which will be used to create the new attributes. + HAPI_NodeId AttribWrangleNodeId; + if (FHoudiniEngineUtils::CreateNode( + InputObjectNodeId, TEXT("attribwrangle"), + TEXT("physical_material"), + true, &AttribWrangleNodeId) != HAPI_RESULT_SUCCESS) + { + // Failed to create the node. + HOUDINI_LOG_WARNING( + TEXT("Failed to create Physical Material attribute for mesh: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + return false; + } + + // Connect the new node to the previous node. Set NewNodeId to the attrib node + // as is this the final output of the chain. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + AttribWrangleNodeId, 0, CollisionMergeNodeId, 0), false); + CollisionMergeNodeId = AttribWrangleNodeId; + + // Set the wrangle's class to primitives + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::SetParmIntValue(FHoudiniEngine::Get().GetSession(), AttribWrangleNodeId, "class", 0, 1), false); + + // Construct a VEXpression to set create and set a Physical Material Attribute. + // eg. s@unreal_physical_material = 'MyPath/PhysicalMaterial'; + const FString FormatString = TEXT("s@{0} = '{1}';"); + FString PathName = PhysicalMaterial->GetPathName(); + FString AttrName = TEXT(HAPI_UNREAL_ATTRIB_SIMPLE_PHYSICAL_MATERIAL); + std::string VEXpression = H_TCHAR_TO_UTF8(*FString::Format(*FormatString, + { AttrName, PathName })); + + // Set the snippet parameter to the VEXpression. + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniEngineUtils::HapiFindParameterByName(AttribWrangleNodeId, "snippet", ParmInfo); + if (ParmId != -1) + { + FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), AttribWrangleNodeId, + VEXpression.c_str(), ParmId, 0); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Invalid Parameter: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + } + } + + // Connect our collision merge node (or the phys mat attrib wrangle) to the main merge node + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + NewNodeId, NextMergeIndex, CollisionMergeNodeId, 0), false); + NextMergeIndex++; + } + } + + if (DoExportSockets && SkeletalMesh->NumSockets() > 0) + { + // Create an input node for the skeletal mesh sockets + HAPI_NodeId SocketsNodeId = -1; + + if (CreateInputNodeForSkeletalMeshSockets(SkeletalMesh, InputObjectNodeId, SocketsNodeId)) + { + // We can connect the socket node to the merge node's last input. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), NewNodeId, NextMergeIndex, SocketsNodeId, 0), false); + + NextMergeIndex++; + } + else if (SocketsNodeId != -1) + { + // If we failed to properly export the sockets, clean up the created node + FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), SocketsNodeId); + } + } + + { + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, InputNodeId, Handle, InputObjectNodeId, nullptr, bInputNodesCanBeDeleted)) + OutHandle = Handle; + } + + // + return true; +} + +bool +FUnrealSkeletalMeshTranslator::CreateSkeletalMeshBoneCaptureAttributes( + const HAPI_NodeId InNodeId, + USkeletalMesh const* const InSkeletalMesh, + const HAPI_PartInfo& PartInfo, + const TArray& BoneCaptureIndexArray, + const TArray& BoneCaptureDataArray, + const TArray& SizesBoneCaptureIndexArray) +{ + //--------------------------------------------------------------------------------------------------------------------- + // Capt_Names + // Bone Names + //--------------------------------------------------------------------------------------------------------------------- + const FReferenceSkeleton& RefSkeleton = InSkeletalMesh->GetRefSkeleton(); + + TArray ComponentSpaceTransforms; + GetComponentSpaceTransforms(ComponentSpaceTransforms, RefSkeleton); + + TArray CaptNamesData; + TArray CaptParentsData; + TArray XFormsData; + + const int32 TotalBones = RefSkeleton.GetRawBoneNum(); + TArray CaptData; //for pCaptData property + CaptData.SetNumZeroed(TotalBones * 20); + //CaptData.Init(0.0f, TotalBones * 20); + + XFormsData.AddZeroed(16 * TotalBones); + CaptParentsData.AddUninitialized(TotalBones); + CaptNamesData.SetNum(TotalBones); + + for (int32 BoneIndex = 0; BoneIndex < TotalBones; ++BoneIndex) + { + const FMeshBoneInfo& CurrentBone = RefSkeleton.GetRefBoneInfo()[BoneIndex]; + const FTransform& LocalBoneTransform = RefSkeleton.GetRefBonePose()[BoneIndex]; + FTransform& BoneTransform = ComponentSpaceTransforms[BoneIndex]; + + FMatrix M44 = FHoudiniSkeletalMeshUtils::UnrealToHoudiniMatrix(BoneTransform); + FMatrix M44Inverse = M44.Inverse(); //see pCaptData property + + int32 row = 0; + int32 col = 0; + for (int32 i = 0; i < 16; i++) + { + XFormsData[16 * BoneIndex + i] = M44.M[row][col]; + CaptData[20 * BoneIndex + i] = M44Inverse.M[row][col]; + col++; + if (col > 3) + { + row++; + col = 0; + } + } + CaptData[20 * BoneIndex + 16] = 1.0f;//Top height + CaptData[20 * BoneIndex + 17] = 1.0f;//Bottom Height + CaptData[20 * BoneIndex + 18] = 1.0f;//Ratio of (top x radius of tube)/(bottom x radius of tube) adjusted for orientation + CaptData[20 * BoneIndex + 19] = 1.0f;//Ratio of (top z radius of tube)/(bottom z radius of tube) adjusted for orientation + + + CaptNamesData[BoneIndex] = CurrentBone.ExportName; + CaptParentsData[BoneIndex] = CurrentBone.ParentIndex; + } + + HAPI_AttributeInfo CaptNamesInfo; + FHoudiniApi::AttributeInfo_Init(&CaptNamesInfo); + CaptNamesInfo.count = 1; + CaptNamesInfo.tupleSize = 1; + CaptNamesInfo.exists = true; + CaptNamesInfo.owner = HAPI_ATTROWNER_DETAIL; + CaptNamesInfo.storage = HAPI_STORAGETYPE_STRING_ARRAY; + CaptNamesInfo.originalOwner = HAPI_ATTROWNER_DETAIL; + CaptNamesInfo.totalArrayElements = CaptNamesData.Num(); + CaptNamesInfo.typeInfo = HAPI_AttributeTypeInfo::HAPI_ATTRIBUTE_TYPE_NONE; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InNodeId, 0, + "capt_names", &CaptNamesInfo), false); + + TArray SizesFixedArray; + SizesFixedArray.Add(CaptNamesData.Num()); + + + FHoudiniHapiAccessor Accessor(InNodeId, 0, "capt_names"); + Accessor.SetAttributeArrayData(CaptNamesInfo, CaptNamesData, SizesFixedArray); + + //boneCapture_pCaptPath------------------------------------------------------------------- + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InNodeId, 0, + "boneCapture_pCaptPath", &CaptNamesInfo), false); + + Accessor.Init(InNodeId, 0, "boneCapture_pCaptPath"); + Accessor.SetAttributeArrayData(CaptNamesInfo, CaptNamesData, SizesFixedArray); + + //--------------------------------------------------------------------------------------------------------------------- + // Capt_Parents + //--------------------------------------------------------------------------------------------------------------------- + HAPI_AttributeInfo CaptParentsInfo; + FHoudiniApi::AttributeInfo_Init(&CaptParentsInfo); + CaptParentsInfo.count = 1; + CaptParentsInfo.tupleSize = 1; + CaptParentsInfo.exists = true; + CaptParentsInfo.owner = HAPI_ATTROWNER_DETAIL; + CaptParentsInfo.storage = HAPI_STORAGETYPE_INT_ARRAY; + CaptParentsInfo.originalOwner = HAPI_ATTROWNER_DETAIL; + CaptParentsInfo.totalArrayElements = CaptParentsData.Num(); + CaptParentsInfo.typeInfo = HAPI_AttributeTypeInfo::HAPI_ATTRIBUTE_TYPE_NONE; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InNodeId, 0, + "capt_parents", &CaptParentsInfo), false); + + TArray SizesParentsArray; + SizesParentsArray.Add(CaptParentsData.Num()); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeIntArrayData( + FHoudiniEngine::Get().GetSession(), InNodeId, + 0, "capt_parents", &CaptParentsInfo, CaptParentsData.GetData(), + CaptParentsData.Num(), SizesParentsArray.GetData(), 0, SizesParentsArray.Num()), false); + + //--------------------------------------------------------------------------------------------------------------------- + // Capt_Xforms + //--------------------------------------------------------------------------------------------------------------------- + HAPI_AttributeInfo CaptXFormsInfo; + FHoudiniApi::AttributeInfo_Init(&CaptXFormsInfo); + CaptXFormsInfo.count = 1; + CaptXFormsInfo.tupleSize = 16; + CaptXFormsInfo.exists = true; + CaptXFormsInfo.owner = HAPI_ATTROWNER_DETAIL; + CaptXFormsInfo.storage = HAPI_STORAGETYPE_FLOAT_ARRAY; + CaptXFormsInfo.originalOwner = HAPI_ATTROWNER_DETAIL; + CaptXFormsInfo.totalArrayElements = XFormsData.Num(); + CaptXFormsInfo.typeInfo = HAPI_AttributeTypeInfo::HAPI_ATTRIBUTE_TYPE_NONE; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InNodeId, 0, + "capt_xforms", &CaptXFormsInfo), false); + + TArray SizesXFormsArray; + SizesXFormsArray.Add(TotalBones); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatArrayData( + FHoudiniEngine::Get().GetSession(), InNodeId, + 0, "capt_xforms", &CaptXFormsInfo, XFormsData.GetData(), + CaptXFormsInfo.totalArrayElements, SizesXFormsArray.GetData(), 0, CaptXFormsInfo.count), false); + + //--------------------------------------------------------------------------------------------------------------------- + // boneCapture_pCaptData + //--------------------------------------------------------------------------------------------------------------------- + HAPI_AttributeInfo CaptDataInfo; + FHoudiniApi::AttributeInfo_Init(&CaptDataInfo); + CaptDataInfo.count = 1; + CaptDataInfo.tupleSize = 20; //The pCaptData property property contains exactly 20 floats + CaptDataInfo.exists = true; + CaptDataInfo.owner = HAPI_ATTROWNER_DETAIL; + CaptDataInfo.storage = HAPI_STORAGETYPE_FLOAT_ARRAY; + CaptDataInfo.originalOwner = HAPI_ATTROWNER_DETAIL; + CaptDataInfo.totalArrayElements = CaptData.Num(); //(bones * 20) + CaptDataInfo.typeInfo = HAPI_AttributeTypeInfo::HAPI_ATTRIBUTE_TYPE_NONE; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InNodeId, 0, + "boneCapture_pCaptData", &CaptDataInfo), false); + + TArray SizesCaptDataArray; + SizesCaptDataArray.Add(TotalBones); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatArrayData( + FHoudiniEngine::Get().GetSession(), InNodeId, + 0, "boneCapture_pCaptData", &CaptDataInfo, CaptData.GetData(), + CaptDataInfo.totalArrayElements, SizesCaptDataArray.GetData(), 0, CaptDataInfo.count), false); + + //--------------------------------------------------------------------------------------------------------------------- + // boneCapture_data + //--------------------------------------------------------------------------------------------------------------------- + HAPI_AttributeInfo BoneCaptureDataInfo; + FHoudiniApi::AttributeInfo_Init(&BoneCaptureDataInfo); + BoneCaptureDataInfo.count = PartInfo.pointCount; + BoneCaptureDataInfo.tupleSize = 1; + BoneCaptureDataInfo.exists = true; + BoneCaptureDataInfo.storage = HAPI_STORAGETYPE_FLOAT_ARRAY; + BoneCaptureDataInfo.owner = HAPI_ATTROWNER_POINT; + BoneCaptureDataInfo.originalOwner = HAPI_ATTROWNER_POINT; + BoneCaptureDataInfo.totalArrayElements = BoneCaptureDataArray.Num();// Part.pointCount* InfluenceCount; + BoneCaptureDataInfo.typeInfo = HAPI_AttributeTypeInfo::HAPI_ATTRIBUTE_TYPE_NONE; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InNodeId, 0, + "boneCapture_data", &BoneCaptureDataInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatArrayData( + FHoudiniEngine::Get().GetSession(), InNodeId, + 0, "boneCapture_data", &BoneCaptureDataInfo, BoneCaptureDataArray.GetData(), + BoneCaptureDataInfo.totalArrayElements, SizesBoneCaptureIndexArray.GetData(), 0, SizesBoneCaptureIndexArray.Num()), false); + + //--------------------------------------------------------------------------------------------------------------------- + // bonecapture_index + //--------------------------------------------------------------------------------------------------------------------- + HAPI_AttributeInfo BoneCaptureIndexInfo; + FHoudiniApi::AttributeInfo_Init(&BoneCaptureIndexInfo); + BoneCaptureIndexInfo.count = PartInfo.pointCount; + BoneCaptureIndexInfo.tupleSize = 1; + BoneCaptureIndexInfo.exists = true; + BoneCaptureIndexInfo.storage = HAPI_STORAGETYPE_INT_ARRAY; + BoneCaptureIndexInfo.owner = HAPI_ATTROWNER_POINT; + BoneCaptureIndexInfo.originalOwner = HAPI_ATTROWNER_POINT; + BoneCaptureIndexInfo.totalArrayElements = BoneCaptureIndexArray.Num();// Part.pointCount* InfluenceCount; + BoneCaptureIndexInfo.typeInfo = HAPI_AttributeTypeInfo::HAPI_ATTRIBUTE_TYPE_NONE; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), InNodeId, 0, + "boneCapture_index", &BoneCaptureIndexInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeIntArrayData( + FHoudiniEngine::Get().GetSession(), InNodeId, + 0, "boneCapture_index", &BoneCaptureDataInfo, BoneCaptureIndexArray.GetData(), + BoneCaptureIndexInfo.totalArrayElements, SizesBoneCaptureIndexArray.GetData(), 0, SizesBoneCaptureIndexArray.Num()), false); + + return true; +} + + +bool +FUnrealSkeletalMeshTranslator::SetSkeletalMeshDataOnNodeFromMeshDescription( + USkeletalMesh const* const SkeletalMesh, + USkeletalMeshComponent const* const SkeletalMeshComponent, + const HAPI_NodeId& NewNodeId, + const int32 LODIndex, + const bool bAddLODGroup, + const bool bInExportMaterialParametersAsAttributes) +{ + if (!IsValid(SkeletalMesh)) + return false; + + // ---------------------------------------------------------------------------------------------------------------- + // Get the mesh description and prepare data for CreateAndPopulateMeshPartFromMeshDescription + // ---------------------------------------------------------------------------------------------------------------- + + // Get the mesh description for the LOD + FMeshDescription MeshDescription; +#if ENGINE_MAJOR_VERSION < 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 3) + // Check first if we have bulk data available and non-empty. + if (SkeletalMesh->IsLODImportedDataBuildAvailable(LODIndex) && !SkeletalMesh->IsLODImportedDataEmpty(LODIndex)) + { + FSkeletalMeshImportData SkeletalMeshImportData; + SkeletalMesh->LoadLODImportedData(LODIndex, SkeletalMeshImportData); + SkeletalMeshImportData.GetMeshDescription(MeshDescription); + } + else + { + // Fall back on the LOD model directly if no bulk data exists. When we commit + // the mesh description, we override using the bulk data. This can happen for older + // skeletal meshes, from UE 4.24 and earlier. + const FSkeletalMeshModel* SkeletalMeshModel = SkeletalMesh->GetImportedModel(); + if (SkeletalMeshModel && SkeletalMeshModel->LODModels.IsValidIndex(LODIndex)) + { + SkeletalMeshModel->LODModels[LODIndex].GetMeshDescription(MeshDescription, SkeletalMesh); + } + } +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + MeshDescription = SkeletalMesh->GetMeshDescription(LODIndex) ? *SkeletalMesh->GetMeshDescription(LODIndex) : FMeshDescription(); +#else + SkeletalMesh->GetMeshDescription(LODIndex, MeshDescription); +#endif + + const FSkeletalMeshConstAttributes MeshConstAttributes(MeshDescription); + + // We don't have LightMapResolution or NaniteSettings for SKMesh + const TOptional LightMapResolution; + const TOptional NaniteSettings; + + // Get the LOD screen size + FSkeletalMeshLODInfo const* const LODInfo = SkeletalMesh->GetLODInfo(LODIndex); + TOptional LODScreenSize; + if (LODInfo) + LODScreenSize = LODInfo->ScreenSize.Default; + + // Default build scale + const FVector3f BuildScaleVector = FVector3f::OneVector; + + // Get the physical material path override if configured + const FString PhysicalMaterialPath = FUnrealMeshTranslator::GetSimplePhysicalMaterialPath(SkeletalMesh->GetBodySetup()); + + // Build an array of MaterialInterfaces for the skeletal mesh's materials + const TArray SkeletalMaterials = SkeletalMesh->GetMaterials(); + TArray Materials; + Materials.Reserve(SkeletalMaterials.Num()); + for (const FSkeletalMaterial& SkeletalMaterial : SkeletalMaterials) + { + Materials.Add(SkeletalMaterial.MaterialInterface); + } + + // Build an array of section index to material index + const FSkeletalMeshLODModel& LodModel = SkeletalMesh->GetImportedModel()->LODModels[LODIndex]; + const int32 NumSections = LodModel.Sections.Num(); + TArray SectionMaterialIndices; + SectionMaterialIndices.Reserve(NumSections); + for (const FSkelMeshSection& Section : LodModel.Sections) + { + SectionMaterialIndices.Add(Section.MaterialIndex); + } + + static constexpr bool bUseComponentOverrideColors = false; + + // ---------------------------------------------------------------------------------------------------------------- + // Export the mesh via CreateAndPopulateMeshPartFromMeshDescription + // ---------------------------------------------------------------------------------------------------------------- + + // Do not commit the geo, we need to add some attributes after CreateAndPopulateMeshPartFromMeshDescription + const bool bExportVertexColors = !bUseComponentOverrideColors; + static constexpr bool bCommitGeo = false; + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + if (!FUnrealMeshTranslator::CreateAndPopulateMeshPartFromMeshDescription( + NewNodeId, + MeshDescription, + MeshConstAttributes, + LODIndex, + bAddLODGroup, + bInExportMaterialParametersAsAttributes, + SkeletalMesh, + SkeletalMeshComponent, + Materials, + SectionMaterialIndices, + BuildScaleVector, + PhysicalMaterialPath, + bExportVertexColors, + LightMapResolution, + LODScreenSize, + NaniteSettings, + SkeletalMesh->GetAssetImportData(), + bCommitGeo, + PartInfo)) + { + return false; + } + + // Get the bone capture / weight data + const FVertexArray& Vertices = MeshDescription.Vertices(); + const int32 NumVertices = Vertices.Num(); + + const FSkinWeightsVertexAttributesConstRef VertexSkinWeights = MeshConstAttributes.GetVertexSkinWeights(); + + TArray BoneCaptureIndexArray; + BoneCaptureIndexArray.Reserve(NumVertices * MAX_TOTAL_INFLUENCES); + TArray BoneCaptureDataArray; + BoneCaptureDataArray.Reserve(NumVertices * MAX_TOTAL_INFLUENCES); + TArray SizesBoneCaptureIndexArray; + SizesBoneCaptureIndexArray.Reserve(NumVertices); + + for (const FVertexID& VertexID : Vertices.GetElementIDs()) + { + FVertexBoneWeightsConst VertexBoneWeights = VertexSkinWeights.Get(VertexID); + uint32 WeightCount = 0; + for (const UE::AnimationCore::FBoneWeight& BoneWeight : VertexBoneWeights) + { + // Get normalized weight + const float Weight = BoneWeight.GetWeight(); + if (Weight > 0.0f) + { + BoneCaptureDataArray.Add(Weight); + const FBoneIndexType BoneIndex = BoneWeight.GetBoneIndex(); + BoneCaptureIndexArray.Add(BoneIndex); + WeightCount++; + } + } + SizesBoneCaptureIndexArray.Add(WeightCount); + } + + BoneCaptureIndexArray.Shrink(); + BoneCaptureDataArray.Shrink(); + SizesBoneCaptureIndexArray.Shrink(); + + if (!CreateSkeletalMeshBoneCaptureAttributes( + NewNodeId, SkeletalMesh, PartInfo, BoneCaptureIndexArray, BoneCaptureDataArray, SizesBoneCaptureIndexArray)) + { + return false; + } + + // Commit the geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(NewNodeId), false); + + return true; +} + + +bool +FUnrealSkeletalMeshTranslator::SetSkeletalMeshDataOnNodeFromSourceModel( + USkeletalMesh* SkeletalMesh, + USkeletalMeshComponent* SkeletalMeshComponent, + HAPI_NodeId& NewNodeId, + int32 LODIndex, + const bool& bAddLODGroups, + const bool bInExportMaterialParametersAsAttributes) +{ + if (!IsValid(SkeletalMesh)) + return false; + + const FSkeletalMeshModel* SkelMeshResource = SkeletalMesh->GetImportedModel(); + if (!SkelMeshResource) + return false; + + if (!SkelMeshResource->LODModels.IsValidIndex(LODIndex)) + return false; + + const FSkeletalMeshLODModel& SourceModel = SkelMeshResource->LODModels[LODIndex]; + + // Copy all the vertex data from the various chunks to a single buffer. + // Makes the rest of the code in this function cleaner and easier to maintain. + TArray Vertices; + SourceModel.GetVertices(Vertices); + + // Verify the integrity of the mesh. + const int32 VertexCount = Vertices.Num(); + if (VertexCount == 0) + return false; + + if (Vertices.Num() != VertexCount) + return false; + + TArray Points; + Points.SetNum(VertexCount); + + TArray Normals; + Normals.SetNum(VertexCount); + + TArray UV0; + UV0.SetNum(VertexCount); + + for (int32 VertIndex = 0; VertIndex < VertexCount; VertIndex++) + { + Points[VertIndex] = Vertices[VertIndex].Position; + Normals[VertIndex] = FVector3f(Vertices[VertIndex].TangentZ.X, Vertices[VertIndex].TangentZ.Y, Vertices[VertIndex].TangentZ.Z); + Swap(Normals[VertIndex].Y, Normals[VertIndex].Z); + UV0[VertIndex] = FVector3f(Vertices[VertIndex].UVs[0].X, 1.0f - Vertices[VertIndex].UVs[0].Y, 0.0f); + } + + //--------------------------------------------------------------------------------------------------------------------- + // POSITION (P) + //--------------------------------------------------------------------------------------------------------------------- + // In FStaticMeshLODResources each vertex instances stores its position, even if the positions are not unique (in other + // words, in Houdini terminology, the number of points and vertices are the same. We'll do the same thing that Epic + // does in FBX export: we'll run through all vertex instances and use a hash to determine which instances share a + // position, so that we can a smaller number of points than vertices, and vertices share point positions + TArray UEVertexInstanceIdxToPointIdx; + UEVertexInstanceIdxToPointIdx.Reserve(VertexCount); + + TMap PositionToPointIndexMap; + PositionToPointIndexMap.Reserve(VertexCount); + FVector3f BuildScaleVector = FVector3f::OneVector; + + TArray SkeletalMeshPoints; + SkeletalMeshPoints.Reserve(VertexCount * 3); + + TArray SkeletalMeshNormals; + SkeletalMeshNormals.Reserve(VertexCount * 3); + + TArray PointUVs; + PointUVs.Reserve(VertexCount * 3); + + TArray BoneCaptureData; + + int32 InfluenceCount = 4; + TArray BoneCaptureIndexArray; + BoneCaptureIndexArray.Reserve(InfluenceCount * VertexCount); + + TArray BoneCaptureDataArray; + BoneCaptureDataArray.Reserve(InfluenceCount * VertexCount); + + TArray SizesBoneCaptureIndexArray; + SizesBoneCaptureIndexArray.Reserve(InfluenceCount * VertexCount); + + // - Switching to iterate over sections + for (FSkelMeshSection section : SourceModel.Sections) + { + for (int32 VertexInstanceIndex = 0; VertexInstanceIndex < section.SoftVertices.Num(); ++VertexInstanceIndex) + { + // Convert Unreal to Houdini + const FVector3f& PositionVector = section.SoftVertices[VertexInstanceIndex].Position; + const FVector3f& NormalVector = FVector3f(section.SoftVertices[VertexInstanceIndex].TangentZ.X, section.SoftVertices[VertexInstanceIndex].TangentZ.Y, section.SoftVertices[VertexInstanceIndex].TangentZ.Z); + const FVector2D& UV0Vector2d = FVector2D(section.SoftVertices[VertexInstanceIndex].UVs[0].X, section.SoftVertices[VertexInstanceIndex].UVs[0].Y); + const int32* FoundPointIndexPtr = PositionToPointIndexMap.Find(PositionVector); + + if (!FoundPointIndexPtr) + { + const int32 NewPointIndex = SkeletalMeshPoints.Add(PositionVector.X / HAPI_UNREAL_SCALE_FACTOR_POSITION * BuildScaleVector.X) / 3; + SkeletalMeshPoints.Add(PositionVector.Z / HAPI_UNREAL_SCALE_FACTOR_POSITION * BuildScaleVector.Z); + SkeletalMeshPoints.Add(PositionVector.Y / HAPI_UNREAL_SCALE_FACTOR_POSITION * BuildScaleVector.Y); + SkeletalMeshNormals.Add(NormalVector.X); + //SkeletalMeshNormals.Add(-NormalVector.Z); + SkeletalMeshNormals.Add(NormalVector.Z); + SkeletalMeshNormals.Add(NormalVector.Y); + PointUVs.Add((float)UV0Vector2d.X); + PointUVs.Add(1.0f - (float)UV0Vector2d.Y); + PointUVs.Add(0); + + PositionToPointIndexMap.Add(PositionVector, NewPointIndex); + UEVertexInstanceIdxToPointIdx.Add(NewPointIndex); + + int weightcounts = 0; + for (int idx = 0; idx < InfluenceCount; idx++) + { + float weight = (float)section.SoftVertices[VertexInstanceIndex].InfluenceWeights[idx] / 255.0f; + if (weight > 0.0f) + { + BoneCaptureData.Add(weight); + BoneCaptureDataArray.Add(weight); + int BoneIndex = section.SoftVertices[VertexInstanceIndex].InfluenceBones[idx]; + int AltIndex = section.BoneMap[BoneIndex]; + BoneCaptureData.Add(AltIndex); + BoneCaptureIndexArray.Add(AltIndex); + weightcounts++; + } + } + SizesBoneCaptureIndexArray.Add(weightcounts); + } + else + { + UEVertexInstanceIdxToPointIdx.Add(*FoundPointIndexPtr); + } + } + } + + SkeletalMeshPoints.Shrink(); + SkeletalMeshNormals.Shrink(); + SizesBoneCaptureIndexArray.Shrink(); + BoneCaptureIndexArray.Shrink(); + BoneCaptureDataArray.Shrink(); + + + //--------------------------------------------------------------------------------------------------------------------- + // VERTICES (Vertex Indices) + //--------------------------------------------------------------------------------------------------------------------- + TArray StaticMeshIndices; + + // Create the per-material polygons sets. + int32 SectionCount = SourceModel.Sections.Num(); + TArray> VertexIndexOffsetPairArray{ TPair(0,0) }; + + int TotalTriangleCount = 0; + TArray FaceNormals; + for (int32 SectionIndex = 0; SectionIndex < SectionCount; ++SectionIndex) + { + const FSkelMeshSection& Section = SourceModel.Sections[SectionIndex]; + + int32 MatIndex = Section.MaterialIndex; + + // Static meshes contain one triangle list per element. + int32 TriangleCount = Section.NumTriangles; + TotalTriangleCount += TriangleCount; + + // Copy over the index buffer into the FBX polygons set. + for (int32 TriangleIndex = 0; TriangleIndex < TriangleCount; ++TriangleIndex) + { + for (int32 PointIndex = 0; PointIndex < 3; PointIndex++) + { + int32 VertexPositionIndex = SourceModel.IndexBuffer[Section.BaseIndex + ((TriangleIndex * 3) + PointIndex)]; + StaticMeshIndices.Add(UEVertexInstanceIdxToPointIdx[VertexPositionIndex]); + + FVector3f fixed = Normals[VertexPositionIndex]; + FaceNormals.Add(fixed); + } + + //fix winding + int32 last = StaticMeshIndices.Num(); + int32 temp = StaticMeshIndices[last - 1]; + + StaticMeshIndices[last - 1] = StaticMeshIndices[last - 2]; + StaticMeshIndices[last - 2] = temp; + } + } + + // Create part. + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + Part.vertexCount = StaticMeshIndices.Num(); + Part.faceCount = TotalTriangleCount; + Part.pointCount = SkeletalMeshPoints.Num() / 3; + Part.type = HAPI_PARTTYPE_MESH; + + HAPI_Result ResultPartInfo = FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, &Part); + + //--------------------------------------------------------------------------------------------------------------------- + // POINTS (P) + //--------------------------------------------------------------------------------------------------------------------- + // Create point attribute info. + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = Part.pointCount; + AttributeInfoPoint.tupleSize = 3; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint), false); + + // Now that we have raw positions, we can upload them for our attribute. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint, + (float*)SkeletalMeshPoints.GetData(), 0, AttributeInfoPoint.count), false); + + //--------------------------------------------------------------------------------------------------------------------- + // INDICES (VertexList) + //--------------------------------------------------------------------------------------------------------------------- + + // We can now set vertex list. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetVertexList( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, StaticMeshIndices.GetData(), 0, StaticMeshIndices.Num()), false); + + // // We need to generate array of face counts. + // TArray< int32 > StaticMeshFaceCounts; + //BoneCaptureIndexArray.Reserve(Part.faceCount); + // StaticMeshFaceCounts.Init(3, Part.faceCount); + //HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetFaceCounts( + // FHoudiniEngine::Get().GetSession(), + // NewNodeId, 0, StaticMeshFaceCounts.GetData(), 0, StaticMeshFaceCounts.Num()), false); + + + // We need to generate array of face counts. + TArray StaticMeshFaceCounts; + StaticMeshFaceCounts.SetNumUninitialized(Part.faceCount); + for (int32 n = 0; n < Part.faceCount; n++) + StaticMeshFaceCounts[n] = 3; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetFaceCounts( + StaticMeshFaceCounts, NewNodeId, 0), false); + + + + //--------------------------------------------------------------------------------------------------------------------- + // NORMALS (N) + //--------------------------------------------------------------------------------------------------------------------- + // Create attribute for normals. + HAPI_AttributeInfo AttributeInfoNormal; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoNormal); + AttributeInfoNormal.tupleSize = 3; + AttributeInfoNormal.count = Part.pointCount; //Normals is array of FVector3f + AttributeInfoNormal.exists = true; + AttributeInfoNormal.owner = HAPI_ATTROWNER_POINT; + AttributeInfoNormal.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoNormal.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, HAPI_UNREAL_ATTRIB_NORMAL, &AttributeInfoNormal), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, HAPI_UNREAL_ATTRIB_NORMAL, + &AttributeInfoNormal, (float*)SkeletalMeshNormals.GetData(), + 0, AttributeInfoNormal.count), false); + + //--------------------------------------------------------------------------------------------------------------------- + // POINT UVS (UV) + //--------------------------------------------------------------------------------------------------------------------- + + HAPI_AttributeInfo AttributeInfoUV; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoUV); + AttributeInfoUV.tupleSize = 3; + AttributeInfoUV.count = PointUVs.Num() / AttributeInfoUV.tupleSize; + AttributeInfoUV.exists = true; + AttributeInfoUV.owner = HAPI_ATTROWNER_POINT; + AttributeInfoUV.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoUV.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, HAPI_UNREAL_ATTRIB_UV, &AttributeInfoUV), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, HAPI_UNREAL_ATTRIB_UV, + &AttributeInfoUV, (float*)PointUVs.GetData(), + 0, AttributeInfoUV.count), false); + + //--------------------------------------------------------------------------------------------------------------------- + // Materials + //--------------------------------------------------------------------------------------------------------------------- + + // Create attribute for materials. + TArray MaterialInterfaces; + for (FSkeletalMaterial SkeletalMaterial : SkeletalMesh->GetMaterials()) + { + MaterialInterfaces.Add(SkeletalMaterial.MaterialInterface); + } + + // + // Build a triangle material indices array: material index per triangle + // + TArray TriangleMaterialIndices; + TriangleMaterialIndices.Reserve(TotalTriangleCount); + for (int32 SectionIndex = 0; SectionIndex < SectionCount; ++SectionIndex) + { + const int32 MaterialIndex = SourceModel.Sections[SectionIndex].MaterialIndex; + const int32 NumSectionTriangles = SourceModel.Sections[SectionIndex].NumTriangles; + for (int32 SectionTriangleIndex = 0; SectionTriangleIndex < NumSectionTriangles; ++SectionTriangleIndex) + { + TriangleMaterialIndices.Add(MaterialIndex); + } + } + + // List of materials, one for each face. + FHoudiniEngineIndexedStringMap StaticMeshFaceMaterials; + + //Lists of material parameters + TMap> ScalarMaterialParameters; + TMap> VectorMaterialParameters; + TMap TextureMaterialParameters; + TMap> BoolMaterialParameters; + + bool bAttributeSuccess = false; + FString PhysicalMaterialPath = FUnrealMeshTranslator::GetSimplePhysicalMaterialPath(SkeletalMesh->GetBodySetup()); + if (bInExportMaterialParametersAsAttributes) + { + // Create attributes for the material and all its parameters + // Get material attribute data, and all material parameters data + FUnrealMeshTranslator::CreateFaceMaterialArray( + MaterialInterfaces, + TriangleMaterialIndices, + StaticMeshFaceMaterials, + ScalarMaterialParameters, + VectorMaterialParameters, + TextureMaterialParameters, + BoolMaterialParameters); + } + else + { + // Create attributes only for the materials + // Only get the material attribute data + FUnrealMeshTranslator::CreateFaceMaterialArray( + MaterialInterfaces, TriangleMaterialIndices, StaticMeshFaceMaterials); + } + + // Create all the needed attributes for materials + bAttributeSuccess = FUnrealMeshTranslator::CreateHoudiniMeshAttributes( + NewNodeId, + 0, + TriangleMaterialIndices.Num(), + StaticMeshFaceMaterials, + TriangleMaterialIndices, + ScalarMaterialParameters, + VectorMaterialParameters, + TextureMaterialParameters, + BoolMaterialParameters, + PhysicalMaterialPath); + + if (!bAttributeSuccess) + return false; + + if (!CreateSkeletalMeshBoneCaptureAttributes( + NewNodeId, SkeletalMesh, Part, BoneCaptureIndexArray, BoneCaptureDataArray, SizesBoneCaptureIndexArray)) + { + return false; + } + + //--------------------------------------------------------------------------------------------------------------------- + // LOD GROUP AND SCREENSIZE + //--------------------------------------------------------------------------------------------------------------------- + if (bAddLODGroups) + { + // LOD Group + FString LODGroup = TEXT("lod") + FString::FromInt(LODIndex); + + // Add a LOD group + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddGroup( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, HAPI_GROUPTYPE_PRIM, H_TCHAR_TO_UTF8(*LODGroup)), false); + + // Set GroupMembership + TArray GroupArray; + GroupArray.SetNumUninitialized(Part.faceCount); + for (int32 n = 0; n < GroupArray.Num(); n++) + GroupArray[n] = 1; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetGroupMembership( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, HAPI_GROUPTYPE_PRIM, H_TCHAR_TO_UTF8(*LODGroup), + GroupArray.GetData(), 0, Part.faceCount), false); + + FSkeletalMeshLODInfo* LODInfo = SkeletalMesh->GetLODInfo(LODIndex); + if (LODInfo) + { + // Add the lodX_screensize attribute + FString LODAttributeName = + TEXT(HAPI_UNREAL_ATTRIB_LOD_SCREENSIZE_PREFIX) + FString::FromInt(LODIndex) + TEXT(HAPI_UNREAL_ATTRIB_LOD_SCREENSIZE_POSTFIX); + + // Create lodX_screensize detail attribute info. + HAPI_AttributeInfo AttributeInfoLODScreenSize; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoLODScreenSize); + AttributeInfoLODScreenSize.count = 1; + AttributeInfoLODScreenSize.tupleSize = 1; + AttributeInfoLODScreenSize.exists = true; + AttributeInfoLODScreenSize.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoLODScreenSize.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoLODScreenSize.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, H_TCHAR_TO_UTF8(*LODAttributeName), &AttributeInfoLODScreenSize), false); + + float lodscreensize = LODInfo->ScreenSize.Default; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + H_TCHAR_TO_UTF8(*LODAttributeName), &AttributeInfoLODScreenSize, + &lodscreensize, 0, 1), false); + } + } + + // Commit the geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(NewNodeId), false); + + return true; +} + + +bool +FUnrealSkeletalMeshTranslator::CreateInputNodeForSkeletalMeshSockets( + USkeletalMesh* InSkeletalMesh, + const HAPI_NodeId& InParentNodeId, + HAPI_NodeId& OutSocketsNodeId) +{ + if (!InSkeletalMesh) + return false; + + TArray InMeshSocket = InSkeletalMesh->GetActiveSocketList(); + int32 NumSockets = InMeshSocket.Num(); + if (NumSockets <= 0) + return false; + + FReferenceSkeleton RefSK = InSkeletalMesh->GetRefSkeleton(); + TArray AllBonePos = RefSK.GetRawRefBonePose(); + + // Create a new input node for the sockets + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode( + InParentNodeId, TEXT("null"), "sockets", false, &OutSocketsNodeId), false); + + // Create part. + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + Part.id = 0; + Part.nameSH = 0; + Part.attributeCounts[HAPI_ATTROWNER_POINT] = 0; + Part.attributeCounts[HAPI_ATTROWNER_PRIM] = 0; + Part.attributeCounts[HAPI_ATTROWNER_VERTEX] = 0; + Part.attributeCounts[HAPI_ATTROWNER_DETAIL] = 0; + Part.pointCount = NumSockets; + Part.vertexCount = 0; + Part.faceCount = 0; + Part.type = HAPI_PARTTYPE_MESH; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, &Part), false); + + // Create POS point attribute info. + HAPI_AttributeInfo AttributeInfoPos; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPos); + AttributeInfoPos.count = NumSockets; + AttributeInfoPos.tupleSize = 3; + AttributeInfoPos.exists = true; + AttributeInfoPos.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPos.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPos.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPos), false); + + // Create Rot point attribute Info + HAPI_AttributeInfo AttributeInfoRot; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoRot); + AttributeInfoRot.count = NumSockets; + AttributeInfoRot.tupleSize = 4; + AttributeInfoRot.exists = true; + AttributeInfoRot.owner = HAPI_ATTROWNER_POINT; + AttributeInfoRot.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoRot.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_ROTATION, &AttributeInfoRot), false); + + // Create scale point attribute Info + HAPI_AttributeInfo AttributeInfoScale; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoScale); + AttributeInfoScale.count = NumSockets; + AttributeInfoScale.tupleSize = 3; + AttributeInfoScale.exists = true; + AttributeInfoScale.owner = HAPI_ATTROWNER_POINT; + AttributeInfoScale.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoScale.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_SCALE, &AttributeInfoScale), false); + + // Create the name attrib info + HAPI_AttributeInfo AttributeInfoName; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoName); + AttributeInfoName.count = NumSockets; + AttributeInfoName.tupleSize = 1; + AttributeInfoName.exists = true; + AttributeInfoName.owner = HAPI_ATTROWNER_POINT; + AttributeInfoName.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoName.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_MESH_SOCKET_NAME, &AttributeInfoName), false); + + // Create the bone name attrib info + HAPI_AttributeInfo AttributeInfoBoneName; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoBoneName); + AttributeInfoBoneName.count = NumSockets; + AttributeInfoBoneName.tupleSize = 1; + AttributeInfoBoneName.exists = true; + AttributeInfoBoneName.owner = HAPI_ATTROWNER_POINT; + AttributeInfoBoneName.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoBoneName.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_MESH_SOCKET_BONE_NAME, &AttributeInfoBoneName), false); + + // Extract the sockets transform values + TArray SocketPos; + SocketPos.SetNumZeroed(NumSockets * 3); + TArray SocketRot; + SocketRot.SetNumZeroed(NumSockets * 4); + TArray SocketScale; + SocketScale.SetNumZeroed(NumSockets * 3); + + TArray SocketNames; + TArray SocketBoneNames; + for (int32 Idx = 0; Idx < NumSockets; ++Idx) + { + USkeletalMeshSocket* CurrentSocket = InMeshSocket[Idx]; + if (!IsValid(CurrentSocket)) + continue; + + // Calculate the sockets transform + // They are stored relative to a bone, so we first need to get the corresponding bone's transform + int32 BoneIndex = RefSK.FindBoneIndex(CurrentSocket->BoneName); + FTransform BoneTransform = AllBonePos.IsValidIndex(BoneIndex) ? AllBonePos[BoneIndex] : FTransform::Identity; + do + { + int32 ParentIndex = RefSK.GetParentIndex(BoneIndex); + if (ParentIndex != INDEX_NONE) + { + BoneTransform = BoneTransform * AllBonePos[ParentIndex]; + } + + BoneIndex = ParentIndex; + } while (RefSK.IsValidIndex(BoneIndex)); + + + //FTransform BoneTransform = AllBonePos.IsValidIndex(BoneIndex) ? AllBonePos[BoneIndex] : FTransform::Identity; + FTransform RelSocketTransform(CurrentSocket->RelativeRotation, CurrentSocket->RelativeLocation, CurrentSocket->RelativeScale); + FTransform SocketTransform = RelSocketTransform * BoneTransform; + + // Convert the socket transform to a HapiTransform + HAPI_Transform HapiSocketTransform; + FHoudiniApi::Transform_Init(&HapiSocketTransform); + FHoudiniEngineUtils::TranslateUnrealTransform(SocketTransform, HapiSocketTransform); + + // Fill the attribute values + SocketPos[3 * Idx + 0] = HapiSocketTransform.position[0]; + SocketPos[3 * Idx + 1] = HapiSocketTransform.position[1]; + SocketPos[3 * Idx + 2] = HapiSocketTransform.position[2]; + + SocketRot[4 * Idx + 0] = HapiSocketTransform.rotationQuaternion[0]; + SocketRot[4 * Idx + 1] = HapiSocketTransform.rotationQuaternion[1]; + SocketRot[4 * Idx + 2] = HapiSocketTransform.rotationQuaternion[2]; + SocketRot[4 * Idx + 3] = HapiSocketTransform.rotationQuaternion[3]; + + SocketScale[3 * Idx + 0] = HapiSocketTransform.scale[0]; + SocketScale[3 * Idx + 1] = HapiSocketTransform.scale[1]; + SocketScale[3 * Idx + 2] = HapiSocketTransform.scale[2]; + + FString CurrentSocketName; + if (!CurrentSocket->SocketName.IsNone()) + CurrentSocketName = CurrentSocket->SocketName.ToString(); + else + CurrentSocketName = TEXT("Socket") + FString::FromInt(Idx); + SocketNames.Add(CurrentSocketName); + + FString BoneName = CurrentSocket->BoneName.ToString(); + if (!BoneName.IsEmpty()) + SocketBoneNames.Add(BoneName); + else + SocketBoneNames.Add(""); + } + + //we can now upload them to our attribute. + FHoudiniHapiAccessor Accessor; + Accessor.Init(OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoPos, SocketPos), false); + + Accessor.Init(OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_ROTATION); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoRot, SocketRot), false); + + Accessor.Init(OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_SCALE); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoScale, SocketScale), false); + + Accessor.Init(OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_MESH_SOCKET_NAME); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoName, SocketNames), false); + + Accessor.Init(OutSocketsNodeId, 0, HAPI_UNREAL_ATTRIB_MESH_SOCKET_BONE_NAME); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(AttributeInfoBoneName, SocketBoneNames), false); + + // We will also create the socket_details attributes + for (int32 Idx = 0; Idx < NumSockets; ++Idx) + { + // Build the current socket's prefix + FString SocketAttrPrefix = TEXT(HAPI_UNREAL_ATTRIB_MESH_SOCKET_PREFIX) + FString::FromInt(Idx); + + // Create mesh_socketX_pos attribute info. + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPos); + AttributeInfoPos.count = 1; + AttributeInfoPos.tupleSize = 3; + AttributeInfoPos.exists = true; + AttributeInfoPos.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoPos.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPos.originalOwner = HAPI_ATTROWNER_INVALID; + + FString PosAttr = SocketAttrPrefix + TEXT("_pos"); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*PosAttr), &AttributeInfoPos), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*PosAttr), &AttributeInfoPos, + &(SocketPos[3 * Idx]), 0, AttributeInfoPos.count), false); + + // Create mesh_socketX_rot point attribute Info + FHoudiniApi::AttributeInfo_Init(&AttributeInfoRot); + AttributeInfoRot.count = 1; + AttributeInfoRot.tupleSize = 4; + AttributeInfoRot.exists = true; + AttributeInfoRot.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoRot.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoRot.originalOwner = HAPI_ATTROWNER_INVALID; + + FString RotAttr = SocketAttrPrefix + TEXT("_rot"); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*RotAttr), &AttributeInfoRot), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*RotAttr), &AttributeInfoRot, + &(SocketRot[4 * Idx]), 0, AttributeInfoRot.count), false); + + // Create mesh_socketX_scale point attribute Info + FHoudiniApi::AttributeInfo_Init(&AttributeInfoScale); + AttributeInfoScale.count = 1; + AttributeInfoScale.tupleSize = 3; + AttributeInfoScale.exists = true; + AttributeInfoScale.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoScale.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoScale.originalOwner = HAPI_ATTROWNER_INVALID; + + FString ScaleAttr = SocketAttrPrefix + TEXT("_scale"); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*ScaleAttr), &AttributeInfoScale), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*ScaleAttr), &AttributeInfoScale, + &(SocketScale[3 * Idx]), 0, AttributeInfoScale.count), false); + + // Create the mesh_socketX_name attrib info + FHoudiniApi::AttributeInfo_Init(&AttributeInfoName); + AttributeInfoName.count = 1; + AttributeInfoName.tupleSize = 1; + AttributeInfoName.exists = true; + AttributeInfoName.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoName.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoName.originalOwner = HAPI_ATTROWNER_INVALID; + + FString NameAttr = SocketAttrPrefix + TEXT("_name"); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*NameAttr), &AttributeInfoName), false); + + Accessor.Init(OutSocketsNodeId, 0, TCHAR_TO_ANSI(*NameAttr)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoName, SocketNames[Idx]), false); + + // Create the mesh_socketX_bone attrib info + FHoudiniApi::AttributeInfo_Init(&AttributeInfoBoneName); + AttributeInfoBoneName.count = 1; + AttributeInfoBoneName.tupleSize = 1; + AttributeInfoBoneName.exists = true; + AttributeInfoBoneName.owner = HAPI_ATTROWNER_DETAIL; + AttributeInfoBoneName.storage = HAPI_STORAGETYPE_STRING; + AttributeInfoBoneName.originalOwner = HAPI_ATTROWNER_INVALID; + + FString BoneNameAttr = SocketAttrPrefix + TEXT("_bone_name"); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, TCHAR_TO_ANSI(*BoneNameAttr), &AttributeInfoBoneName), false); + + Accessor.Init(OutSocketsNodeId, 0, TCHAR_TO_ANSI(*BoneNameAttr)); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeUniqueData(AttributeInfoBoneName, SocketBoneNames[Idx]), false); + } + + // Now add the sockets group + const char* SocketGroupStr = "socket_imported"; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddGroup( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_GROUPTYPE_POINT, SocketGroupStr), false); + + // Set GroupMembership + TArray GroupArray; + GroupArray.SetNumUninitialized(NumSockets); + for (int32 n = 0; n < GroupArray.Num(); n++) + GroupArray[n] = 1; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetGroupMembership( + FHoudiniEngine::Get().GetSession(), + OutSocketsNodeId, 0, HAPI_GROUPTYPE_POINT, SocketGroupStr, GroupArray.GetData(), 0, NumSockets), false); + + // Commit the geo. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiCommitGeo(OutSocketsNodeId), false); + + return true; +} + + +bool +FUnrealSkeletalMeshTranslator::CreateInputNodeForCapturePose( + USkeletalMesh* InSkeletalMesh, + const HAPI_NodeId InParentNodeId, + const FString& InInputNodeName, + HAPI_NodeId& InOutSkeletonNodeId, + FUnrealObjectInputHandle& OutHandle, + const bool bInputNodesCanBeDeleted) +{ + if (!IsValid(InSkeletalMesh)) + return false; + + const USkeleton* Skeleton = InSkeletalMesh->GetSkeleton(); + if (!IsValid(Skeleton)) + return false; + + // Input node name, defaults to InputNodeName, but can be changed by the new input system + FString FinalInputNodeName = InInputNodeName; + + HAPI_NodeId PreviousNodeId = InOutSkeletonNodeId; + + // Build an identifier for the capture pose node + FUnrealObjectInputOptions Options; + Options.AddBoolOption(TEXT("bCapturePose"), true); + FUnrealObjectInputIdentifier Identifier(InSkeletalMesh, Options, true); + FUnrealObjectInputHandle ParentHandle; + HAPI_NodeId ParentNodeId = InParentNodeId; + + { + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId)) + { + if (!bInputNodesCanBeDeleted) + { + // Make sure to prevent deletion of the input node if needed + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + } + + OutHandle = Handle; + InOutSkeletonNodeId = NodeId; + return true; + } + } + + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + // Create any parent/container nodes that we would need, and get the node id of the immediate parent + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // Set InputNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that InputNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (Handle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, PreviousNodeId)) + PreviousNodeId = -1; + } + else + { + PreviousNodeId = -1; + } + } + + HAPI_NodeId NewNodeId = -1; + + { + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateInputNode( + FinalInputNodeName, NewNodeId, ParentNodeId), false); + + + + // After we have created the new node, delete the old node + if (PreviousNodeId >= 0) + { + const HAPI_NodeId ObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(PreviousNodeId); + FHoudiniEngineUtils::DeleteHoudiniNode(PreviousNodeId); + FHoudiniEngineUtils::DeleteHoudiniNode(ObjectNodeId); + PreviousNodeId = -1; + } + } + + // The ObjectNodeId inside which we'll be creating some more input processing nodes. + const HAPI_NodeId ObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(NewNodeId); + + //---------------------------------------- + // Create nodes to perform additional input processing + //---------------------------------------- + { + // Create Point Wrangle node + // This will convert matrix attributes to their proper type which HAPI doesn't seem to be translating correctly. + HAPI_NodeId AttribWrangleNodeId = -1; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(ObjectNodeId, TEXT("attribwrangle"), + TEXT("convert_matrix"), false, &AttribWrangleNodeId), false); + + // Connect Wrangle to Null + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + AttribWrangleNodeId, 0, NewNodeId, 0), false); + + // Construct a VEXpression to convert matrices. + const FString FormatString = TEXT("3@transform = matrix3(f[]@in_transform);"); + + // Set the snippet parameter to the VEXpression. + { + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniEngineUtils::HapiFindParameterByName(AttribWrangleNodeId, "snippet", ParmInfo); + if (ParmId != -1) + { + FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), AttribWrangleNodeId, + H_TCHAR_TO_UTF8(*FormatString), ParmId, 0); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Invalid Parameter: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + } + } + + + // Create primitive node + HAPI_NodeId PrimitiveNodeId; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(ObjectNodeId, TEXT("primitive"), + TEXT("open_primitive_u"), true, &PrimitiveNodeId), false); + + // Connect Wrangle to Primitive + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + PrimitiveNodeId, 0, AttribWrangleNodeId, 0), false); + + { + // Set the primitive "closeu" parameter to open. This will prevent Houdini from auto-closing primitives + // since our primitives are all edges. + HAPI_ParmInfo ParmInfo; + HAPI_ParmId ParmId = FHoudiniEngineUtils::HapiFindParameterByName(PrimitiveNodeId, "closeu", ParmInfo); + if (ParmId != -1) + { + // FHoudiniApi::SetParmStringValue(FHoudiniEngine::Get().GetSession(), PrimitiveNodeId, + // "open", ParmId, 0); + FHoudiniApi::SetParmIntValue(FHoudiniEngine::Get().GetSession(), PrimitiveNodeId, + "closeu", 0, 1); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Invalid Parameter: %s"), + *FHoudiniEngineUtils::GetErrorDescription()); + } + } + + // Create output node + HAPI_NodeId OutputNodeId; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(ObjectNodeId, TEXT("output"), + TEXT("output"), true, &OutputNodeId), false); + + // Connect Primitive to Output + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput( + FHoudiniEngine::Get().GetSession(), + OutputNodeId, 0, PrimitiveNodeId, 0), false); + } + + // Import note on Skeleton vs SkeletalMesh's RefSkeleton: The USkeleton->RefSkeleton will contain all the bones in the + // (potentially shared) Skeleton asset. The SkeletalMesh->RefSkeleton may contain either all the same bones as the + // USkeleton asset or it may only contain a subset of bones (this happens when the Skeleton asset is shared among + // different skeletal meshes that contains different bone sets). + + // Create the skeletal mesh Capture Pose on NewNodeId + + const FReferenceSkeleton& MeshRefSkel = InSkeletalMesh->GetRefSkeleton(); + + // We just want to export raw bones, not virtual bones. + const int32 NumRawBones = MeshRefSkel.GetRawBoneNum(); + + const TArray& BoneInfoArray = MeshRefSkel.GetRawRefBoneInfo(); + // Stores the component-space bone transforms in Houdini space + TMap HouBoneTransform; + + // For the Capture Pose, we only need "P", "name" and "transform" attributes. + TArray PosData; + TArray WorldTransformData; + TArray WorldTransformSizeData; + TArray BoneNameData; + TArray PrimIndexData; + TArray PrimSizeData; + + constexpr int32 TransformDataStride = 9; + + PosData.SetNum(NumRawBones); + WorldTransformData.SetNum(NumRawBones * TransformDataStride); + WorldTransformSizeData.Init(9, NumRawBones); + BoneNameData.SetNum(NumRawBones); + PrimSizeData.Reserve(NumRawBones); + PrimIndexData.Reserve(NumRawBones*2); + + + int32 BoneDataIndex = 0; + for (const FMeshBoneInfo& BoneInfo : BoneInfoArray) + { + + FTransform UnrealBoneTransform(InSkeletalMesh->GetComposedRefPoseMatrix(BoneInfo.Name)); + FMatrix HoudiniMatrix = FHoudiniSkeletalMeshUtils::UnrealToHoudiniMatrix(UnrealBoneTransform); + + // Bone position + FVector3f Location = FVector3f(FVector3d(HoudiniMatrix.GetOrigin())); + PosData[BoneDataIndex] = Location; + + int32 TransformDataIndex = BoneDataIndex * TransformDataStride; + WorldTransformData[TransformDataIndex + 0] = HoudiniMatrix.M[0][0]; + WorldTransformData[TransformDataIndex + 1] = HoudiniMatrix.M[0][1]; + WorldTransformData[TransformDataIndex + 2] = HoudiniMatrix.M[0][2]; + WorldTransformData[TransformDataIndex + 3] = HoudiniMatrix.M[1][0]; + WorldTransformData[TransformDataIndex + 4] = HoudiniMatrix.M[1][1]; + WorldTransformData[TransformDataIndex + 5] = HoudiniMatrix.M[1][2]; + WorldTransformData[TransformDataIndex + 6] = HoudiniMatrix.M[2][0]; + WorldTransformData[TransformDataIndex + 7] = HoudiniMatrix.M[2][1]; + WorldTransformData[TransformDataIndex + 8] = HoudiniMatrix.M[2][2]; + + // Primitives (Edges) between joints + if (BoneInfo.ParentIndex != INDEX_NONE) + { + // We have an edge between the current bone and its parent. + PrimIndexData.Add(BoneInfo.ParentIndex); + PrimIndexData.Add(BoneDataIndex); + PrimSizeData.Add(2); + } + + // Bone names + BoneNameData[BoneDataIndex] = BoneInfo.Name.ToString(); + + BoneDataIndex++; + } + + //---------------------------------------- + // Create part. + //---------------------------------------- + HAPI_PartInfo Part; + FHoudiniApi::PartInfo_Init(&Part); + Part.vertexCount = PrimIndexData.Num(); + Part.faceCount = PrimSizeData.Num(); + Part.pointCount = NumRawBones; + Part.type = HAPI_PARTTYPE_MESH; + + HAPI_Result ResultPartInfo = FHoudiniApi::SetPartInfo( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, &Part); + + //---------------------------------------- + // Create point attribute info. + //---------------------------------------- + HAPI_AttributeInfo AttributeInfoPoint; + FHoudiniApi::AttributeInfo_Init(&AttributeInfoPoint); + AttributeInfoPoint.count = Part.pointCount; + AttributeInfoPoint.tupleSize = 3; + AttributeInfoPoint.exists = true; + AttributeInfoPoint.owner = HAPI_ATTROWNER_POINT; + AttributeInfoPoint.storage = HAPI_STORAGETYPE_FLOAT; + AttributeInfoPoint.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint), false); + + // Position Data + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatData( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, HAPI_UNREAL_ATTRIB_POSITION, &AttributeInfoPoint, + reinterpret_cast(PosData.GetData()), 0, AttributeInfoPoint.count), false); + + // Vertex list. + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetVertexList( + FHoudiniEngine::Get().GetSession(), + NewNodeId, 0, PrimIndexData.GetData(), 0, PrimIndexData.Num()), false); + + // FaceCounts + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::HapiSetFaceCounts( + PrimSizeData, NewNodeId, 0), false); + + //---------------------------------------- + // name: name of the joint + //---------------------------------------- + HAPI_AttributeInfo BoneNameInfo; + FHoudiniApi::AttributeInfo_Init(&BoneNameInfo); + BoneNameInfo.count = Part.pointCount; + BoneNameInfo.tupleSize = 1; + BoneNameInfo.exists = true; + BoneNameInfo.owner = HAPI_ATTROWNER_POINT; + BoneNameInfo.storage = HAPI_STORAGETYPE_STRING; + BoneNameInfo.originalOwner = HAPI_ATTROWNER_INVALID; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + "name", &BoneNameInfo), false); + + FHoudiniHapiAccessor Accessor(NewNodeId, 0, "name"); + HOUDINI_CHECK_RETURN(Accessor.SetAttributeData(BoneNameInfo, BoneNameData), false); + + //---------------------------------------- + // in_transform: 3x3 component space transform for each bone + //---------------------------------------- + + HAPI_AttributeInfo WorldTransformInfo; + FHoudiniApi::AttributeInfo_Init(&WorldTransformInfo); + WorldTransformInfo.count = Part.pointCount; + WorldTransformInfo.tupleSize = 1; + WorldTransformInfo.exists = true; + WorldTransformInfo.owner = HAPI_ATTROWNER_POINT; + WorldTransformInfo.storage = HAPI_STORAGETYPE_FLOAT_ARRAY; + WorldTransformInfo.originalOwner = HAPI_ATTROWNER_INVALID; + WorldTransformInfo.totalArrayElements = WorldTransformData.Num(); + WorldTransformInfo.typeInfo = HAPI_AttributeTypeInfo::HAPI_ATTRIBUTE_TYPE_MATRIX3; + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::AddAttribute( + FHoudiniEngine::Get().GetSession(), NewNodeId, 0, + "in_transform", &WorldTransformInfo), false); + + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetAttributeFloatArrayData( + FHoudiniEngine::Get().GetSession(), NewNodeId, + 0, "in_transform", + &WorldTransformInfo, WorldTransformData.GetData(), WorldTransformData.Num(), + WorldTransformSizeData.GetData(), 0, WorldTransformSizeData.Num()), false); + + //---------------------------------------- + // End of capture pose translation + //---------------------------------------- + FHoudiniEngineUtils::HapiCommitGeo(NewNodeId); + + { + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, NewNodeId, Handle, ObjectNodeId, nullptr, bInputNodesCanBeDeleted)) + OutHandle = Handle; + } + + InOutSkeletonNodeId = NewNodeId; + + return true; +} + +``` + +`Source/HoudiniEngine/Private/UnrealSkeletalMeshTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" + +#include "CoreMinimal.h" +#include "Engine/SkeletalMesh.h" + +struct FUnrealMeshExportOptions; +class USkeletalMesh; +class USkeletalMeshComponent; +class USkeletalMeshSocket; +class FUnrealObjectInputHandle; + +struct HOUDINIENGINE_API FUnrealSkeletalMeshTranslator +{ + public: + + // HAPI : Marshaling, extract geometry and skeletons and create input nodes for it - return true on success + static bool HapiCreateInputNodeForSkeletalMesh( + USkeletalMesh* Mesh, + HAPI_NodeId& InputObjectNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + class USkeletalMeshComponent* SkeletalMeshComponent, + const FUnrealMeshExportOptions& FUnrealMeshExportOptions, + const bool& bInputNodesCanBeDeleted, + const bool& bExportMaterialParameters); + + // Create nodes for the mesh data only: mesh, LODs, colliders, sockets. + static bool CreateInputNodesForSkeletalMesh( + USkeletalMesh* Mesh, + HAPI_NodeId& InputObjectNodeId, + const FString& InputNodeName, + FUnrealObjectInputHandle& OutHandle, + class USkeletalMeshComponent* SkeletalMeshComponent, + const FUnrealMeshExportOptions& FUnrealMeshExportOptions, + const bool& bInputNodesCanBeDeleted, + const bool& bExportMaterialParameters); + + // Actually exports the skeletal mesh data (mesh, skeleton ... ) using LOD's SourceModel to the newly created input node - returns true on success + static bool SetSkeletalMeshDataOnNodeFromSourceModel( + USkeletalMesh* SkeletalMesh, + USkeletalMeshComponent* SkeletalMeshComponent, + HAPI_NodeId& NewNodeId, + int32 LODIndex, + const bool& bAddLODGroup, + const bool bInExportMaterialParametersAsAttributes); + + static bool CreateSkeletalMeshBoneCaptureAttributes( + HAPI_NodeId InNodeId, + USkeletalMesh const* InSkeletalMesh, + const HAPI_PartInfo& PartInfo, + const TArray& BoneCaptureIndexArray, + const TArray& BoneCaptureDataArray, + const TArray& SizesBoneCaptureIndexArray); + + // Actually exports the skeletal mesh data (mesh, skeleton ... ) using the LOD's import data MeshDescription to the newly created input node - returns true on success + static bool SetSkeletalMeshDataOnNodeFromMeshDescription( + USkeletalMesh const* SkeletalMesh, + USkeletalMeshComponent const* SkeletalMeshComponent, + const HAPI_NodeId& NewNodeId, + int32 LODIndex, + bool bAddLODGroup, + bool bInExportMaterialParametersAsAttributes); + + static bool CreateInputNodeForSkeletalMeshSockets( + USkeletalMesh* InSkeletalMesh, + const HAPI_NodeId& InParentNodeId, + HAPI_NodeId& OutSocketsNodeId); + + static bool CreateInputNodeForCapturePose( + USkeletalMesh* InSkeletalMesh, + const HAPI_NodeId InParentNodeId, + const FString& InInputNodeName, + HAPI_NodeId& InOutSkeletonNodeId, + FUnrealObjectInputHandle& OutHandle, + const bool bInputNodesCanBeDeleted); + +}; +``` + +`Source/HoudiniEngine/Private/UnrealSplineTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealSplineTranslator.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniSplineTranslator.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputUtils.h" +#include "UnrealObjectInputRuntimeUtils.h" + +#include "Components/SplineComponent.h" + + +bool +FUnrealSplineTranslator::CreateInputNodeForSplineComponent( + USplineComponent* SplineComponent, + HAPI_NodeId& CreatedInputNodeId, + FUnrealObjectInputHandle& OutHandle, + const float& SplineResolution, + const FString& InputNodeName, + const bool& bInUseLegacyInputCurves, + const bool& bInputNodesCanBeDeleted) +{ + if (!IsValid(SplineComponent)) + return false; + + // Input node name, defaults to InputNodeName, but can be changed by the new input system + FString FinalInputNodeName = InputNodeName; + + // Find the node in new input system + // Identifier will be the identifier for the entry created in this call of the function. + FUnrealObjectInputIdentifier Identifier; + FUnrealObjectInputHandle ParentHandle; + HAPI_NodeId ParentNodeId = -1; + + { + // Creates this input's identifier and input options + FUnrealObjectInputOptions Options; + Options.UnrealSplineResolution = SplineResolution; + Options.bUseLegacyInputCurves = bInUseLegacyInputCurves; + Identifier = FUnrealObjectInputIdentifier(SplineComponent, Options, true); + + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::NodeExistsAndIsNotDirty(Identifier, Handle)) + { + HAPI_NodeId NodeId = -1; + if (FUnrealObjectInputUtils::GetHAPINodeId(Handle, NodeId) && FUnrealObjectInputUtils::AreReferencedHAPINodesValid(Handle)) + { + if (!bInputNodesCanBeDeleted) + { + // Make sure to prevent deletion of the input node if needed + FUnrealObjectInputUtils::UpdateInputNodeCanBeDeleted(Handle, bInputNodesCanBeDeleted); + } + + OutHandle = Handle; + CreatedInputNodeId = NodeId; + return true; + } + } + + FUnrealObjectInputUtils::GetDefaultInputNodeName(Identifier, FinalInputNodeName); + // Create any parent/container nodes that we would need, and get the node id of the immediate parent + if (FUnrealObjectInputUtils::EnsureParentsExist(Identifier, ParentHandle, bInputNodesCanBeDeleted) && ParentHandle.IsValid()) + FUnrealObjectInputUtils::GetHAPINodeId(ParentHandle, ParentNodeId); + + // Set CreatedInputNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that CreatedInputNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (Handle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, CreatedInputNodeId)) + CreatedInputNodeId = -1; + } + else + { + CreatedInputNodeId = -1; + } + + // We now need to create the nodes (since we couldn't find existing ones in the manager) + // To do that, we can simply continue this function + + // Set CreatedInputNodeId to the current NodeId associated with Handle, since that is what we are replacing. + // (Option changes could mean that CreatedInputNodeId is associated with a completely different entry, albeit for + // the same asset, in the manager) + if (Handle.IsValid()) + { + if (!FUnrealObjectInputUtils::GetHAPINodeId(Handle, CreatedInputNodeId)) + CreatedInputNodeId = -1; + } + else + { + CreatedInputNodeId = -1; + } + } + + int32 NumberOfControlPoints = SplineComponent->GetNumberOfSplinePoints(); + float SplineLength = SplineComponent->GetSplineLength(); + + // Calculate the number of refined point we want + int32 NumberOfRefinedSplinePoints = SplineResolution > 0.0f ? ceil(SplineLength / SplineResolution) + 1 : NumberOfControlPoints; + + TArray RefinedSplinePositions; + TArray RefinedSplineRotations; + TArray RefinedSplineScales; + if (NumberOfRefinedSplinePoints <= NumberOfControlPoints) + { + // There's not enough refined points, so we'll use the control points instead + RefinedSplinePositions.SetNumZeroed(NumberOfControlPoints); + RefinedSplineRotations.SetNumZeroed(NumberOfControlPoints); + RefinedSplineScales.SetNumZeroed(NumberOfControlPoints); + + for (int32 n = 0; n < NumberOfControlPoints; ++n) + { + RefinedSplinePositions[n] = SplineComponent->GetLocationAtSplinePoint(n, ESplineCoordinateSpace::Local); + RefinedSplineRotations[n] = SplineComponent->GetQuaternionAtSplinePoint(n, ESplineCoordinateSpace::World); + RefinedSplineScales[n] = SplineComponent->GetScaleAtSplinePoint(n); + } + } + else + { + // Calculate the refined spline component + RefinedSplinePositions.SetNumZeroed(NumberOfRefinedSplinePoints); + RefinedSplineRotations.SetNumZeroed(NumberOfRefinedSplinePoints); + RefinedSplineScales.SetNumZeroed(NumberOfRefinedSplinePoints); + + float CurrentDistance = 0.0f; + for (int32 n = 0; n < NumberOfRefinedSplinePoints; ++n) + { + RefinedSplinePositions[n] = SplineComponent->GetLocationAtDistanceAlongSpline(CurrentDistance, ESplineCoordinateSpace::Local); + RefinedSplineRotations[n] = SplineComponent->GetQuaternionAtDistanceAlongSpline(CurrentDistance, ESplineCoordinateSpace::World); + RefinedSplineScales[n] = SplineComponent->GetScaleAtDistanceAlongSpline(CurrentDistance); + + CurrentDistance += SplineResolution; + } + } + + // Currently, we must create legacy curves when using the new input system + // as the new HAPI_CreateInputCurveNode function does not support choosing a parent node! + bool bUseLegacy = bInUseLegacyInputCurves; + bUseLegacy = true; + + if (!FHoudiniSplineTranslator::HapiCreateCurveInputNodeForData( + CreatedInputNodeId, + ParentNodeId, + FinalInputNodeName, + &RefinedSplinePositions, + &RefinedSplineRotations, + &RefinedSplineScales, + EHoudiniCurveType::Polygon, + EHoudiniCurveMethod::Breakpoints, + SplineComponent->IsClosedLoop(), + false, + false, + FTransform::Identity, + bUseLegacy)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to create the input curve data!")); + return false; + } + + // Add spline component tags if it has any + bool NeedToCommit = FHoudiniEngineUtils::CreateGroupsFromTags(CreatedInputNodeId, 0, SplineComponent->ComponentTags); + + // Add the parent actor's tag if it has any + AActor* ParentActor = SplineComponent->GetOwner(); + if (IsValid(ParentActor)) + { + if (FHoudiniEngineUtils::CreateGroupsFromTags(CreatedInputNodeId, 0, ParentActor->Tags)) + NeedToCommit = true; + + HAPI_PartInfo PartInfo; + FHoudiniApi::PartInfo_Init(&PartInfo); + FHoudiniApi::GetPartInfo(FHoudiniEngine::Get().GetSession(), CreatedInputNodeId, 0, &PartInfo); + + // Add the unreal_actor_path attribute + if(FHoudiniEngineUtils::AddActorPathAttribute(CreatedInputNodeId, 0, ParentActor, PartInfo.faceCount)) + NeedToCommit = true; + + // Add the unreal_level_path attribute + if(FHoudiniEngineUtils::AddLevelPathAttribute(CreatedInputNodeId, 0, ParentActor->GetLevel(), PartInfo.faceCount)) + NeedToCommit = true; + } + + if (NeedToCommit) + { + // We successfully added tags to the geo, so we need to commit the changes + if (HAPI_RESULT_SUCCESS != FHoudiniEngineUtils::HapiCommitGeo(CreatedInputNodeId)) + HOUDINI_LOG_WARNING(TEXT("Could not create groups for the spline input's tags!")); + + // And cook it with refinement enabled + // This is mandatory as it fixes issues down the line on the next update! + // (session was lost upon trying to access the data afterwards) + HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + CookOptions.maxVerticesPerPrimitive = -1; + CookOptions.refineCurveToLinear = true; + if (!FHoudiniEngineUtils::HapiCookNode(CreatedInputNodeId, &CookOptions, false)) + return false; + } + + // Get our parent OBJ NodeID + HAPI_NodeId InputObjectNodeId = FHoudiniEngineUtils::HapiGetParentNodeId(CreatedInputNodeId); + { + FUnrealObjectInputHandle Handle; + if (FUnrealObjectInputUtils::AddNodeOrUpdateNode(Identifier, CreatedInputNodeId, Handle, InputObjectNodeId, nullptr, bInputNodesCanBeDeleted)) + OutHandle = Handle; + } + + return true; +} + +``` + +`Source/HoudiniEngine/Private/UnrealSplineTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HAPI/HAPI_Common.h" +#include "UObject/NameTypes.h" + +class USplineComponent; +class FUnrealObjectInputHandle; + +struct HOUDINIENGINE_API FUnrealSplineTranslator +{ +public: + static bool CreateInputNodeForSplineComponent( + USplineComponent* SplineComponent, + HAPI_NodeId& OutCreatedInputNodeId, + FUnrealObjectInputHandle& OutInputNodeHandle, + const float& SplineResolution, + const FString& NodeName, + const bool& bInUseLegacyInputCurves, + const bool& bInputNodesCanBeDeleted); + +}; +``` + +`Source/HoudiniEngine/Private/UnrealTextureTranslator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealTextureTranslator.h" +#include "HoudiniEngineUtils.h" +#include "TextureResource.h" + + +bool +FUnrealTextureTranslator::HapiCreateCOPTexture( + UTexture2D* Texture, + const HAPI_NodeId ParentNode) +{ + // The texture needs certain settings, otherwise RawData->Lock() will fail. + // So we save the old settings so we can restore them later. + TextureCompressionSettings OldCompressionSettings = Texture->CompressionSettings; + TextureMipGenSettings OldMipGenSettings = Texture->MipGenSettings; + bool OldSRGB = Texture->SRGB; + auto RestoreTexture = [&]() + { + Texture->CompressionSettings = OldCompressionSettings; + Texture->MipGenSettings = OldMipGenSettings; + Texture->SRGB = OldSRGB; + Texture->UpdateResource(); + }; + Texture->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap; + Texture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps; + Texture->SRGB = false; + Texture->UpdateResource(); + + // Get the texture's first mipmap (i.e. the full resolution texture). + if (Texture->GetNumMips() == 0) + { + RestoreTexture(); + return false; + } + + FTexturePlatformData* PlatformData = Texture->GetPlatformData(); + if(!PlatformData) + { + RestoreTexture(); + return false; + } + + if(PlatformData->Mips.Num() <= 0) + { + RestoreTexture(); + return false; + } + + FTexture2DMipMap* MipMap = &Texture->GetPlatformData()->Mips[0]; + if (!MipMap) + { + RestoreTexture(); + return false; + } + + // Get a pointer to the mipmap's raw pixel data. + FByteBulkData* RawData = &MipMap->BulkData; + if (!RawData || RawData->IsLocked()) + { + RestoreTexture(); + return false; + } + FColor* ImageColors = static_cast(RawData->Lock(LOCK_READ_ONLY)); + if (!ImageColors) + { + // Unlocks the raw data if the image data wasn't casted successfully. + if (RawData->IsLocked()) + RawData->Unlock(); + RestoreTexture(); + return false; + } + + // Convert the data to linear color space, and save it to a new array. + float* ImageData = new float[MipMap->SizeX * MipMap->SizeY * 4]; + for (size_t i = 0; i < MipMap->SizeX * MipMap->SizeY; i++) + { + FLinearColor LinearImageColor = FLinearColor(ImageColors[i]); + ImageData[i*4] = LinearImageColor.R; + ImageData[i*4+1] = LinearImageColor.G; + ImageData[i*4+2] = LinearImageColor.B; + ImageData[i*4+3] = LinearImageColor.A; + } + + // Create the COP in Houdini. + bool bSuccess = HAPI_RESULT_SUCCESS == FHoudiniApi::CreateCOPImage( + FHoudiniEngine::Get().GetSession(), + ParentNode, + MipMap->SizeX, + MipMap->SizeY, + HAPI_IMAGE_PACKING_RGBA, + false, + true, + ImageData, + 0, + MipMap->SizeX*MipMap->SizeY*4); + + // Clean up. + RawData->Unlock(); + RestoreTexture(); + delete[] ImageData; + + return bSuccess; +} + +bool +FUnrealTextureTranslator::CreateGeometryForTexture( + HAPI_NodeId ParentNodeId, + HAPI_NodeId& CreatedOutNodeId) +{ + HAPI_Session const* const Session = FHoudiniEngine::Get().GetSession(); + + // Check ParentNodeId is a Geo ? + CreatedOutNodeId = -1; + + // Output Node + HAPI_NodeId OutNodeId = -1; + HAPI_Result Result = FHoudiniEngineUtils::CreateNode(ParentNodeId, TEXT("output"), TEXT("OUT"), true, &OutNodeId); + if (Result != HAPI_RESULT_SUCCESS) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineUtils::CreateInputNode]: CreateNode failed: %s"), *FHoudiniEngineUtils::GetErrorDescription()); + return false; + } + + // Grid Node + HAPI_NodeId GridNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(ParentNodeId, TEXT("grid"), TEXT("Grid"), true, &GridNodeId), false); + // Size 1 1 - sizex sizey + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmFloatValue(Session, GridNodeId, "size", 0, 1.0), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmFloatValue(Session, GridNodeId, "size", 1, 1.0), false); + // XY Plane - orient 0 + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(Session, GridNodeId, "orient", 0, 0), false); + // rows 2 cols 2 + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(Session, GridNodeId, "rows", 0, 2), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(Session, GridNodeId, "cols", 0, 2), false); + + // uv project Node + HAPI_NodeId UVNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(ParentNodeId, TEXT("uvproject"), TEXT("UV"), true, &UVNodeId), false); + + // cop preview mat Node + HAPI_NodeId MatNodeId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniEngineUtils::CreateNode(ParentNodeId, TEXT("coppreviewmaterial"), TEXT("COP Preview Material"), true, &MatNodeId), false); + // basecolorsource 1 (COP) + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmIntValue(Session, MatNodeId, "basecolorsource", 0, 1), false); + // basecolorpath ../copmemoryimport1 + HAPI_ParmId ParmId = -1; + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetParmIdFromName(Session, MatNodeId, "basecolorpath", &ParmId), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::SetParmStringValue(Session, MatNodeId, "../copmemoryimport1/texture1", ParmId, 0), false); + + // Connect the nodes together + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(Session, OutNodeId, 0, MatNodeId, 0), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(Session, MatNodeId, 0, UVNodeId, 0), false); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::ConnectNodeInput(Session, UVNodeId, 0, GridNodeId, 0), false); + + // Return the output node as the created node ID + CreatedOutNodeId = OutNodeId; + + return true; +} + +``` + +`Source/HoudiniEngine/Private/UnrealTextureTranslator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniEngine.h" + +class UTexture2D; + +struct HOUDINIENGINE_API FUnrealTextureTranslator +{ + public: + + // HAPI : Marshal texture into a Houdini COP node. + // If ParentNode is -1, the texture will be created in a copnet in /img. + static bool HapiCreateCOPTexture( + UTexture2D* Texture, + const HAPI_NodeId ParentNode); + + // Creates a Grid node with uvs and material to old the COP texture + static bool CreateGeometryForTexture( + HAPI_NodeId ParentNodeId, + HAPI_NodeId& CreatedOutNodeId); +}; + +``` + +`Source/HoudiniEngine/Public/HAPI/HAPI.h`: + +```h +/* + * PROPRIETARY INFORMATION. This software is proprietary to + * Side Effects Software Inc., and is not to be reproduced, + * transmitted, or disclosed in any way without written permission. + * + * Produced by: + * Side Effects Software Inc + * 123 Front Street West, Suite 1401 + * Toronto, Ontario + * Canada M5J 2M2 + * 416-504-9876 + * + * COMMENTS: + * For parsing help, there is a variable naming convention we maintain: + * strings: char * and does not end in "buffer" + * binary: char * and is either exactly "buffer" or ends + * with "_buffer" + * single values: don't end with "_array" or "_buffer" + * arrays: * and is either "array" or ends + * with "_array". Use "_fixed_array" to skip resize using + * tupleSize for the thrift generator. + * array length: is either "length", "count", or ends with + * "_length" or "_count". Use "_fixed_array" to skip resize + * using tupleSize for the thrift generator. + */ + +#ifndef __HAPI_h__ +#define __HAPI_h__ + +#include "HAPI_API.h" +#include "HAPI_Common.h" +#include "HAPI_Helpers.h" + +/// @defgroup Sessions +/// Functions for creating and inspecting HAPI session state. + +/// @brief Creates a new in-process session. There can only be +/// one such session per host process. +/// +/// @ingroup Sessions +/// +/// @param[out] session +/// A ::HAPI_Session struct to receive the session id, +/// in this case always 0. +/// +/// @param[in] session_info +/// A ::HAPI_SessionInfo struct to specify session configurations. +/// +HAPI_DECL HAPI_CreateInProcessSession( HAPI_Session * session, + const HAPI_SessionInfo * session_info ); + +/// @brief Starts a Thrift RPC server process on the local host serving +/// clients on a TCP socket and waits for it to start serving. +/// It is safe to create an RPC session on local host using the +/// specified port after this call succeeds. +/// +/// @ingroup Sessions +/// @param[in] options +/// Options to configure the server being started. +/// +/// @param[in] port +/// The TCP socket to create on the server. +/// +/// @param[out] process_id +/// The process id of the server, if started successfully. +/// +/// @param[in] log_file +/// When a filepath is provided for this argument, all logs will +/// be appended to the specified file. The specfied path must be +/// an absolute path. The server will create any intermediate +/// directories in the filepath that do not already exist. When +/// this argument is NULL/nullptr, logging will be directed to +/// the standard streams. +/// +HAPI_DECL HAPI_StartThriftSocketServer( + const HAPI_ThriftServerOptions * options, + int port, + HAPI_ProcessId * process_id, + const char * log_file ); + +/// @brief Creates a Thrift RPC session using a TCP socket as transport. +/// +/// @ingroup Sessions +/// @param[out] session +/// A ::HAPI_Session struct to receive the unique session id +/// of the new session. +/// +/// @param[in] host_name +/// The name of the server host. +/// +/// @param[in] port +/// The server port to connect to. +/// +/// @param[in] session_info +/// A ::HAPI_SessionInfo struct to specify session configurations. +/// +HAPI_DECL HAPI_CreateThriftSocketSession( HAPI_Session * session, + const char * host_name, + int port, + const HAPI_SessionInfo * session_info ); + +/// @brief Starts a Thrift RPC server process on the local host serving +/// clients on a Windows named pipe or a Unix domain socket and +/// waits for it to start serving. It is safe to create an RPC +/// session using the specified pipe or socket after this call +/// succeeds. +/// +/// @ingroup Sessions +/// +/// @param[in] options +/// Options to configure the server being started. +/// +/// @param[in] pipe_name +/// The name of the pipe or socket. +/// +/// @param[out] process_id +/// The process id of the server, if started successfully. +/// +/// @param[in] log_file +/// When a filepath is provided for this argument, all logs will +/// be appended to the specified file. The specfied path must be +/// an absolute path. The server will create any intermediate +/// directories in the filepath that do not already exist. When +/// this argument is NULL/nullptr, logging will be directed to +/// the standard streams. +/// +HAPI_DECL HAPI_StartThriftNamedPipeServer( + const HAPI_ThriftServerOptions * options, + const char * pipe_name, + HAPI_ProcessId * process_id, + const char * log_file ); + +/// @brief Creates a Thrift RPC session using a Windows named pipe +/// or a Unix domain socket as transport. +/// +/// @ingroup Sessions +/// +/// @param[out] session +/// A ::HAPI_Session struct to receive the unique session id +/// of the new session. +/// +/// @param[in] pipe_name +/// The name of the pipe or socket. +/// +/// @param[in] session_info +/// A ::HAPI_SessionInfo struct to specify session configurations. +/// +HAPI_DECL HAPI_CreateThriftNamedPipeSession( HAPI_Session * session, + const char * pipe_name, + const HAPI_SessionInfo * session_info ); + +/// @brief Starts a Thrift RPC server process on the localhost serving clients +/// by utilizing shared memory to transfer data between the client and +/// server and waits for it to start serving. +/// +/// @ingroup Sessions +/// +/// @param[in] options +/// Options to configure the server being started. +/// +/// @param[in] shared_mem_name +/// The name of the memory buffer. This must be unique to the +/// server in order to avoid any conflicts. +/// +/// @param[out] process_id +/// The process id of the server, if started successfully. +/// +/// @param[in] log_file +/// When a filepath is provided for this argument, all logs will +/// be appended to the specified file. The specified path must +/// be an absolute path. The server will create any intermediate +/// directories in the filepath that do not already exist. When +/// this argument is NULL/nullptr, logging will be directed to +/// the standard streams. +HAPI_DECL HAPI_StartThriftSharedMemoryServer( + const HAPI_ThriftServerOptions * options, + const char * shared_mem_name, + HAPI_ProcessId * process_id, + const char * log_file); + +/// @brief Creates a Thrift RPC session using a shared memory buffer as the +/// transport mechanism. +/// +/// @ingroup Sessions +/// +/// @param[out] session +/// A ::HAPI_Session struct to receive the unique session id +/// of the new session. +/// +/// @param[in] shared_mem_name +/// The name of the memory buffer. This must match the name of +/// the shared memory buffer of the server that you are wishing +/// to connect to. +/// +/// @param[in] session_info +/// A ::HAPI_SessionInfo struct to specify session configurations. +/// +HAPI_DECL HAPI_CreateThriftSharedMemorySession( HAPI_Session * session, + const char * shared_mem_name, + const HAPI_SessionInfo * session_info ); + +/// @brief Binds a new implementation DLL to one of the custom session +/// slots. +/// +/// @ingroup Sessions +/// +/// @param[in] session_type +/// Which custom implementation slot to bind the +/// DLL to. Must be one of ::HAPI_SESSION_CUSTOM1, +/// ::HAPI_SESSION_CUSTOM2, or ::HAPI_SESSION_CUSTOM3. +/// +/// @param[in] dll_path +/// The path to the custom implementation DLL. +/// +HAPI_DECL HAPI_BindCustomImplementation( HAPI_SessionType session_type, + const char * dll_path ); + +/// @brief Creates a new session using a custom implementation. +/// Note that the implementation DLL must already have +/// been bound to the session via calling +/// ::HAPI_BindCustomImplementation(). +/// +/// @ingroup Sessions +/// +/// @param[in] session_type +/// session_type indicates which custom session +/// slot to create the session on. +/// +/// @param[in,out] session_info +/// Any data required by the custom implementation to +/// create its session. +/// +/// @param[out] session +/// A ::HAPI_Session struct to receive the session id, +/// The sessionType parameter of the struct should +/// also match the session_type parameter passed in. +/// +HAPI_DECL HAPI_CreateCustomSession( HAPI_SessionType session_type, + void * session_info, + HAPI_Session * session ); + +/// @brief Checks whether the session identified by ::HAPI_Session::id is +/// a valid session opened in the implementation identified by +/// ::HAPI_Session::type. +/// +/// @ingroup Sessions +/// +/// @param[in] session +/// The ::HAPI_Session to check. +/// +/// @return ::HAPI_RESULT_SUCCESS if the session is valid. +/// Otherwise, the session is invalid and passing it to +/// other HAPI calls may result in undefined behavior. +/// +HAPI_DECL HAPI_IsSessionValid( const HAPI_Session * session ); + +/// @brief Closes a session. If the session has been established using +/// RPC, then the RPC connection is closed. +/// +/// @ingroup Sessions +/// +/// @param[in] session +/// The HAPI_Session to close. After this call, this +/// session is invalid and passing it to HAPI calls other +/// than ::HAPI_IsSessionValid() may result in undefined +/// behavior. +/// +HAPI_DECL HAPI_CloseSession( const HAPI_Session * session ); + +// INITIALIZATION / CLEANUP ------------------------------------------------- + +/// @brief Check whether the runtime has been initialized yet using +/// ::HAPI_Initialize(). Function will return ::HAPI_RESULT_SUCCESS +/// if the runtime has been initialized and ::HAPI_RESULT_NOT_INITIALIZED +/// otherwise. +/// +/// @ingroup Sessions +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +HAPI_DECL HAPI_IsInitialized( const HAPI_Session * session ); + +/// @brief Create the asset manager, set up environment variables, and +/// initialize the main Houdini scene. No license check is done +/// during this step. Only when you try to load an asset library +/// (OTL) do we actually check for licenses. +/// +/// @ingroup Sessions +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] cook_options +/// Global cook options used by subsequent default cooks. +/// This can be overwritten by individual cooks but if +/// you choose to instantiate assets with cook_on_load +/// set to true then these cook options will be used. +/// +/// @param[in] use_cooking_thread +/// Use a separate thread for cooking of assets. This +/// allows for asynchronous cooking and larger stack size. +/// +/// +/// @param[in] cooking_thread_stack_size +/// Set the stack size of the cooking thread. Use -1 to +/// set the stack size to the Houdini default. This +/// value is in bytes. +/// +/// +/// @param[in] houdini_environment_files +/// A list of paths, separated by a ";" on Windows and a ":" +/// on Linux and Mac, to .env files that follow the same +/// syntax as the houdini.env file in Houdini's user prefs +/// folder. These will be applied after the default +/// houdini.env file and will overwrite the process' +/// environment variable values. You an use this to enforce +/// a stricter environment when running engine. +/// For more info, see: +/// http://www.sidefx.com/docs/houdini/basics/config_env +/// +/// +/// @param[in] otl_search_path +/// The directory where OTLs are searched for. You can +/// pass NULL here which will only use the default +/// Houdini OTL search paths. You can also pass in +/// multiple paths separated by a ";" on Windows and a ":" +/// on Linux and Mac. If something other than NULL is +/// passed the default Houdini search paths will be +/// appended to the end of the path string. +/// +/// +/// @param[in] dso_search_path +/// The directory where generic DSOs (custom plugins) are +/// searched for. You can pass NULL here which will +/// only use the default Houdini DSO search paths. You +/// can also pass in multiple paths separated by a ";" +/// on Windows and a ":" on Linux and Mac. If something +/// other than NULL is passed the default Houdini search +/// paths will be appended to the end of the path string. +/// +/// +/// @param[in] image_dso_search_path +/// The directory where image DSOs (custom plugins) are +/// searched for. You can pass NULL here which will +/// only use the default Houdini DSO search paths. You +/// can also pass in multiple paths separated by a ";" +/// on Windows and a ":" on Linux and Mac. If something +/// other than NULL is passed the default Houdini search +/// paths will be appended to the end of the path string. +/// +/// +/// @param[in] audio_dso_search_path +/// The directory where audio DSOs (custom plugins) are +/// searched for. You can pass NULL here which will +/// only use the default Houdini DSO search paths. You +/// can also pass in multiple paths separated by a ";" +/// on Windows and a ":" on Linux and Mac. If something +/// other than NULL is passed the default Houdini search +/// paths will be appended to the end of the path string. +/// +/// +HAPI_DECL HAPI_Initialize( const HAPI_Session * session, + const HAPI_CookOptions * cook_options, + HAPI_Bool use_cooking_thread, + int cooking_thread_stack_size, + const char * houdini_environment_files, + const char * otl_search_path, + const char * dso_search_path, + const char * image_dso_search_path, + const char * audio_dso_search_path ); + +/// @brief Clean up memory. This will unload all assets and you will +/// need to call ::HAPI_Initialize() again to be able to use any +/// HAPI methods again. +/// +/// @note This does not release any licenses. The license will be returned when +/// the process terminates. +/// +/// @ingroup Sessions +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +HAPI_DECL HAPI_Cleanup( const HAPI_Session * session ); + +/// @brief When using an in-process session, this method **must** be called in +/// order for the host process to shutdown cleanly. This method should +/// be called before ::HAPI_CloseSession(). +/// +/// @note This method should only be called before exiting the program, +/// because HAPI can no longer be used by the process once this method +/// has been called. +/// +/// @ingroup Sessions +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +HAPI_DECL HAPI_Shutdown( const HAPI_Session * session ); + +/// @brief Start a Houdini Performance Monitor profile. +/// A profile records time and memory statistics from events +/// that occur in the Houdini session. A profile records node cooks +/// for example; how long it takes to cook a node and how many times +/// a node is cooked. Return ::HAPI_RESULT_INVALID_ARGUMENT if NULL +/// is passed in for the `profile_id` parameter. +/// +/// @ingroup Sessions +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] title +/// The title of the profile. If NULL is passed into this +/// parameter, then a default title will be chosen for the +/// profile. +/// +/// @param[out] profile_id +/// The id of the profile. You can pass the id to +/// ::HAPI_StopPerformanceMonitorProfile to stop the profile. +/// +/// +HAPI_DECL HAPI_StartPerformanceMonitorProfile( const HAPI_Session * session, + const char * title, + int * profile_id ); + +/// @brief Stop the Performance Monitor profile that matches the given +/// profile id and save out the profile's statistics to the specified +/// file path on disk. The profile is cleared from memory after its +/// statistics are saved to disk. Return ::HAPI_RESULT_INVALID_ARGUMENT +/// if no profile exists for the given id. Return ::HAPI_RESULT_FAILURE +/// if the profile statistics could not be saved out to the specified +/// file path. In this case, the profile is stopped but is not cleared +/// from memory. You can call +/// ::HAPI_StopPerformanceMonitorProfile to attempt saving the +/// profile to disk again. +/// +/// @ingroup Sessions +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] profile_id +/// The id of the profile to stop. +/// +/// @param[in] file_path +/// The path to the file where the profile statistics should be +/// written to. Use the Performance Monitor file extension, +/// .hperf, in the file name (i.e. /path/to/myProfile.hperf). +/// +HAPI_DECL HAPI_StopPerformanceMonitorProfile( const HAPI_Session * session, + int profile_id, + const char * file_path ); + +/// @defgroup Environment +/// Functions for reading and writing to the session environment + +/// @brief Gives back a certain environment integers like version number. +/// Note that you do not need a session for this. These constants +/// are hard-coded in all HAPI implementations, including HARC and +/// HAPIL. This should be the first API you call to determine if +/// any future API calls will mismatch implementation. +/// +/// @ingroup Environment +/// +/// @param[in] int_type +/// One of ::HAPI_EnvIntType. +/// +/// @param[out] value +/// Int value. +/// +HAPI_DECL HAPI_GetEnvInt( HAPI_EnvIntType int_type, int * value ); + +/// @brief Gives back a certain session-specific environment integers +/// like current license type being used. +/// +/// @ingroup Environment +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] int_type +/// One of ::HAPI_SessionEnvIntType. +/// +/// @param[out] value +/// Int value. +/// +HAPI_DECL HAPI_GetSessionEnvInt( const HAPI_Session * session, + HAPI_SessionEnvIntType int_type, + int * value ); + +/// @brief Get environment variable from the server process as an integer. +/// +/// @ingroup Environment +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] variable_name +/// Name of the environmnet variable. +/// +/// @param[out] value +/// The int pointer to return the value in. +/// +HAPI_DECL HAPI_GetServerEnvInt( const HAPI_Session * session, + const char * variable_name, + int * value ); + +/// @brief Get environment variable from the server process as a string. +/// +/// @ingroup Environment +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] variable_name +/// Name of the environmnet variable. +/// +/// @param[out] value +/// The HAPI_StringHandle pointer to return the value in. +/// +HAPI_DECL HAPI_GetServerEnvString( const HAPI_Session * session, + const char * variable_name, + HAPI_StringHandle * value ); + +/// @brief Provides the number of environment variables that are in +/// the server environment's process +/// +/// Note that ::HAPI_GetServerEnvVarList() should be called directly after +/// this method, otherwise there is the possibility that the environment +/// variable count of the server will have changed by the time that +/// ::HAPI_GetServerEnvVarList() is called. +/// +/// @ingroup Environment +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] env_count +/// A pointer to an int to return the value in +HAPI_DECL HAPI_GetServerEnvVarCount( const HAPI_Session * session, + int * env_count ); + +/// @brief Provides a list of all of the environment variables +/// in the server's process +/// +/// @ingroup Environment +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] values_array +/// An ::HAPI_StringHandle array at least the size of length +/// +/// @param[in] start +/// First index of range. Must be at least @c 0 and at most +/// @c env_count - 1 where @c env_count is the count returned by +/// ::HAPI_GetServerEnvVarCount() +/// +/// +/// +/// +/// @param[in] length +/// Given @c env_count returned by ::HAPI_GetServerEnvVarCount(), +/// length should be at least @c 0 and at most env_count - start. +/// +HAPI_DECL HAPI_GetServerEnvVarList( const HAPI_Session * session, + HAPI_StringHandle * values_array, + int start, + int length ); + +/// @brief Set environment variable for the server process as an integer. +/// +/// Note that this may affect other sessions on the same server +/// process. The session parameter is mainly there to identify the +/// server process, not the specific session. +/// +/// For in-process sessions, this will affect the current process's +/// environment. +/// +/// @ingroup Environment +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] variable_name +/// Name of the environment variable. +/// +/// @param[in] value +/// The integer value. +/// +HAPI_DECL HAPI_SetServerEnvInt( const HAPI_Session * session, + const char * variable_name, + int value ); + +/// @brief Set environment variable for the server process as a string. +/// +/// Note that this may affect other sessions on the same server +/// process. The session parameter is mainly there to identify the +/// server process, not the specific session. +/// +/// For in-process sessions, this will affect the current process's +/// environment. +/// +/// @ingroup Environment +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] variable_name +/// Name of the environmnet variable. +/// +/// @param[in] value +/// The string value. +/// +HAPI_DECL HAPI_SetServerEnvString( const HAPI_Session * session, + const char * variable_name, + const char * value ); + +/// @defgroup Status +/// Functions for reading session connection and cook status. + +/// @brief Gives back the status code for a specific status type. +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] status_type +/// One of ::HAPI_StatusType. +/// +/// @param[out] status +/// Actual status code for the status type given. That is, +/// if you pass in ::HAPI_STATUS_CALL_RESULT as +/// status_type, you'll get back a ::HAPI_Result for this +/// argument. If you pass in ::HAPI_STATUS_COOK_STATE +/// as status_type, you'll get back a ::HAPI_State enum +/// for this argument. +/// +HAPI_DECL HAPI_GetStatus( const HAPI_Session * session, + HAPI_StatusType status_type, + int * status ); + +/// @brief Return length of string buffer storing status string message. +/// +/// If called with ::HAPI_STATUS_COOK_RESULT this will actually +/// parse the node networks for the previously cooked asset(s) +/// and aggregate all node errors, warnings, and messages +/// (depending on the @c verbosity level set). Usually this is done +/// just for the last cooked single asset but if you load a whole +/// Houdini scene using ::HAPI_LoadHIPFile() then you'll have +/// multiple "previously cooked assets". +/// +/// You MUST call ::HAPI_GetStatusStringBufLength() before calling +/// ::HAPI_GetStatusString() because ::HAPI_GetStatusString() will +/// not return the real status string and instead return a +/// cached version of the string that was created inside +/// ::HAPI_GetStatusStringBufLength(). The reason for this is that +/// the length of the real status string may change between +/// the call to ::HAPI_GetStatusStringBufLength() and the call to +/// ::HAPI_GetStatusString(). +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] status_type +/// One of ::HAPI_StatusType. +/// +/// @param[in] verbosity +/// Preferred verbosity level. +/// +/// @param[out] buffer_length +/// Length of buffer char array ready to be filled. +/// +HAPI_DECL HAPI_GetStatusStringBufLength( const HAPI_Session * session, + HAPI_StatusType status_type, + HAPI_StatusVerbosity verbosity, + int * buffer_length ); + +/// @brief Return status string message. +/// +/// You MUST call ::HAPI_GetStatusStringBufLength() before calling +/// ::HAPI_GetStatusString() because ::HAPI_GetStatusString() will +/// not return the real status string and instead return a +/// cached version of the string that was created inside +/// ::HAPI_GetStatusStringBufLength(). The reason for this is that +/// the length of the real status string may change between +/// the call to ::HAPI_GetStatusStringBufLength() and the call to +/// ::HAPI_GetStatusString(). +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] status_type +/// One of ::HAPI_StatusType. +/// +/// @param[out] string_value +/// Buffer char array ready to be filled. +/// +/// @param[in] length +/// Length of the string buffer (must match size of +/// @p string_value - so including NULL terminator). +/// +/// +HAPI_DECL HAPI_GetStatusString( const HAPI_Session * session, + HAPI_StatusType status_type, + char * string_value, + int length ); + +/// @brief Compose the cook result string (errors and warnings) of a +/// specific node. +/// +/// This will actually parse the node network inside the given +/// node and return ALL errors/warnings/messages of all child nodes, +/// combined into a single string. If you'd like a more narrowed +/// search, call this function on one of the child nodes. +/// +/// You MUST call ::HAPI_ComposeNodeCookResult() before calling +/// ::HAPI_GetComposedNodeCookResult() because +/// ::HAPI_GetComposedNodeCookResult() will +/// not return the real result string and instead return a +/// cached version of the string that was created inside +/// ::HAPI_ComposeNodeCookResult(). The reason for this is that +/// the length of the real status string may change between +/// the call to ::HAPI_ComposeNodeCookResult() and the call to +/// ::HAPI_GetComposedNodeCookResult(). +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] verbosity +/// Preferred verbosity level. +/// +/// @param[out] buffer_length +/// Length of buffer char array ready to be filled. +/// +HAPI_DECL HAPI_ComposeNodeCookResult( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_StatusVerbosity verbosity, + int * buffer_length ); + +/// @brief Return cook result string message on a single node. +/// +/// You MUST call ::HAPI_ComposeNodeCookResult() before calling +/// ::HAPI_GetComposedNodeCookResult() because +/// ::HAPI_GetComposedNodeCookResult() will +/// not return the real result string and instead return a +/// cached version of the string that was created inside +/// ::HAPI_ComposeNodeCookResult(). The reason for this is that +/// the length of the real status string may change between +/// the call to ::HAPI_ComposeNodeCookResult() and the call to +/// ::HAPI_GetComposedNodeCookResult(). +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] string_value +/// Buffer char array ready to be filled. +/// +/// @param[in] length +/// Length of the string buffer (must match size of +/// @p string_value - so including NULL terminator). +/// +/// +HAPI_DECL HAPI_GetComposedNodeCookResult( const HAPI_Session * session, + char * string_value, + int length ); + +/// @brief Gets the length of the cook result string (errors and warnings) of +/// a specific node. +/// +/// Unlike ::HAPI_ComposeNodeCookResult(), this node does not parse +/// inside the node network. Only errors, warnings, and messages that +/// appear on the specified node will be a part of the cook result +/// string. +/// +/// You MUST call ::HAPI_GetNodeCookResultLength() before calling +/// ::HAPI_GetNodeCookResult() because ::HAPI_GetNodeCookResult() will +/// not return the real result and instead returns a cached version of +/// the string that was created during the call to +/// ::HAPI_GetNodeCookResultLength(). The reason for this is that the +/// length of the real status string may change between the call to +/// ::HAPI_GetNodeCookResultLength() and ::HAPI_GetNodeCookResult(). +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] verbosity +/// Preferred verbosity level. +/// +/// @param[out] buffer_length +/// Lenght of buffer char array ready to be filled. +/// +HAPI_DECL HAPI_GetNodeCookResultLength( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_StatusVerbosity verbosity, + int * buffer_length ); + +/// @brief Return the cook result string that was composed during a call to +/// ::HAPI_GetNodeCookResultLength(). +/// +/// You MUST call ::HAPI_GetNodeCookResultLength() before calling +/// ::HAPI_GetNodeCookResult() because ::HAPI_GetNodeCookResult() will +/// not return the real result and instead returns a cached version of +/// the string that was created during the call to +/// ::HAPI_GetNodeCookResultLength(). The reason for this is that the +/// length of the real status string may change between the call to +/// ::HAPI_GetNodeCookResultLength() and ::HAPI_GetNodeCookResult(). +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] string_value +/// Buffer char array that will be filled with the cook result +/// string. +/// +/// @param[in] length +/// Length of the char buffer (must match size of +/// @p string_value - so include NULL terminator). +/// +/// +HAPI_DECL HAPI_GetNodeCookResult( const HAPI_Session * session, + char * string_value, + int length ); + +/// @brief Get the number of message nodes set in "Type Properties". +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] count +/// The number of message nodes. +/// +HAPI_DECL HAPI_GetMessageNodeCount( const HAPI_Session * session, + HAPI_NodeId node_id, + int * count ); + +/// @brief Get the ids of message nodes set in the "Type Properties". +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] message_node_ids_array +/// The array of node IDs to be filled. +/// +/// @param[in] count +/// The number of message nodes. +/// +HAPI_DECL HAPI_GetMessageNodeIds( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_NodeId * message_node_ids_array, + int count ); + +/// @brief Recursively check for specific errors by error code on a node. +/// +/// Note that checking for errors can be expensive because it checks +/// ALL child nodes within a node and then tries to do a string match +/// for the errors being looked for. This is why such error checking +/// is part of a standalone function and not done during the cooking +/// step. +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] errors_to_look_for +/// The HAPI_ErrorCode error codes (as a bitfield) to look for. +/// +/// @param[out] errors_found +/// Returned HAPI_ErrorCode bitfield indicating which of the +/// looked for errors have been found. +/// +HAPI_DECL HAPI_CheckForSpecificErrors( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_ErrorCodeBits errors_to_look_for, + HAPI_ErrorCodeBits * errors_found ); + +/// @brief Clears the connection error. Should be used before starting +/// or creating Thrift server. +/// +/// Only available when using Thrift connections. +/// +/// @ingroup Status +/// +HAPI_DECL HAPI_ClearConnectionError( ); + +/// @brief Return the length of string buffer storing connection error +/// message. +/// +/// @ingroup Status +/// +/// Only available when using Thrift connections. +/// +/// @param[out] buffer_length +/// Length of buffer char array ready to be filled. +/// +HAPI_DECL HAPI_GetConnectionErrorLength( int * buffer_length ); + +/// @brief Return the connection error message. +/// +/// You MUST call ::HAPI_GetConnectionErrorLength() before calling +/// this to get the correct string length. +/// +/// Only available when using Thrift connections. +/// +/// @ingroup Status +/// +/// @param[out] string_value +/// Buffer char array ready to be filled. +/// +/// @param[in] length +/// Length of the string buffer (must match size of +/// string_value - so including NULL terminator). +/// Use ::HAPI_GetConnectionErrorLength to get this length. +/// +/// @param[in] clear +/// If true, will clear the error when HAPI_RESULT_SUCCESS +/// is returned. +/// +HAPI_DECL HAPI_GetConnectionError( char * string_value, + int length, + HAPI_Bool clear ); + +/// @brief Get total number of nodes that need to cook in the current +/// session. +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] count +/// Total cook count. +/// +HAPI_DECL HAPI_GetCookingTotalCount( const HAPI_Session * session, + int * count ); + +/// @brief Get current number of nodes that have already cooked in the +/// current session. Note that this is a very crude approximation +/// of the cooking progress - it may never make it to 100% or it +/// might spend another hour at 100%. Use ::HAPI_GetStatusString +/// to get a better idea of progress if this number gets stuck. +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] count +/// Current cook count. +/// +HAPI_DECL HAPI_GetCookingCurrentCount( const HAPI_Session * session, + int * count ); + +/// @brief Interrupt a cook or load operation. +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +HAPI_DECL HAPI_Interrupt( const HAPI_Session * session ); + +/// @defgroup Utility +/// Utility math and other functions + +/// @brief Converts the transform described by a ::HAPI_TransformEuler +/// struct into a different transform and rotation order. +/// +/// @ingroup Utility +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] transform_in +/// The transform to be converted. +/// +/// @param[in] rst_order +/// The desired transform order of the output. +/// +/// @param[in] rot_order +/// The desired rotation order of the output. +/// +/// @param[out] transform_out +/// The converted transform. +/// +HAPI_DECL HAPI_ConvertTransform( const HAPI_Session * session, + const HAPI_TransformEuler * transform_in, + HAPI_RSTOrder rst_order, + HAPI_XYZOrder rot_order, + HAPI_TransformEuler * transform_out ); + +/// @brief Converts a 4x4 matrix into its TRS form. +/// +/// @ingroup Utility +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] matrix +/// A 4x4 matrix expressed in a 16 element float array. +/// +/// @param[in] rst_order +/// The desired transform order of the output. +/// +/// @param[out] transform_out +/// Used for the output. +/// +HAPI_DECL HAPI_ConvertMatrixToQuat( const HAPI_Session * session, + const float * matrix, + HAPI_RSTOrder rst_order, + HAPI_Transform * transform_out ); + +/// @brief Converts a 4x4 matrix into its TRS form. +/// +/// @ingroup Utility +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] matrix +/// A 4x4 matrix expressed in a 16 element float array. +/// +/// @param[in] rst_order +/// The desired transform order of the output. +/// +/// @param[in] rot_order +/// The desired rotation order of the output. +/// +/// @param[out] transform_out +/// Used for the output. +/// +HAPI_DECL HAPI_ConvertMatrixToEuler( const HAPI_Session * session, + const float * matrix, + HAPI_RSTOrder rst_order, + HAPI_XYZOrder rot_order, + HAPI_TransformEuler * transform_out ); + +/// @brief Converts ::HAPI_Transform into a 4x4 transform matrix. +/// +/// @ingroup Utility +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] transform +/// The ::HAPI_Transform you wish to convert. +/// +/// @param[out] matrix +/// A 16 element float array that will contain the result. +/// +HAPI_DECL HAPI_ConvertTransformQuatToMatrix( const HAPI_Session * session, + const HAPI_Transform * transform, + float * matrix ); + +/// @brief Converts ::HAPI_TransformEuler into a 4x4 transform matrix. +/// +/// @ingroup Utility +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] transform +/// The ::HAPI_TransformEuler you wish to convert. +/// +/// @param[out] matrix +/// A 16 element float array that will contain the result. +/// +HAPI_DECL HAPI_ConvertTransformEulerToMatrix( + const HAPI_Session * session, + const HAPI_TransformEuler * transform, + float * matrix ); + +/// @brief Acquires or releases the Python interpreter lock. This is +/// needed if HAPI is called from Python and HAPI is in threaded +/// mode (see ::HAPI_Initialize()). +/// +/// The problem arises when async functions like +/// ::HAPI_CreateNode() may start a cooking thread that +/// may try to run Python code. That is, we would now have +/// Python running on two different threads - something not +/// allowed by Python by default. +/// +/// We need to tell Python to explicitly "pause" the Python state +/// on the client thread while we run Python in our cooking thread. +/// +/// You must call this function first with locked == true before +/// any async HAPI call. Then, after the async call finished, +/// detected via calls to ::HAPI_GetStatus(), call this method +/// again to release the lock with locked == false. +/// +/// @ingroup Utility +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] locked +/// True will acquire the interpreter lock to use it for +/// the HAPI cooking thread. False will release the lock +/// back to the client thread. +/// +HAPI_DECL HAPI_PythonThreadInterpreterLock( const HAPI_Session * session, + HAPI_Bool locked ); + +/// @defgroup Strings +/// Functions for handling strings. + +/// @brief Gives back the string length of the string with the +/// given handle. +/// +/// @ingroup Strings +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] string_handle +/// Handle of the string to query. +/// +/// @param[out] buffer_length +/// Buffer length of the queried string (including NULL +/// terminator). +/// +HAPI_DECL HAPI_GetStringBufLength( const HAPI_Session * session, + HAPI_StringHandle string_handle, + int * buffer_length ); + +/// @brief Gives back the string value of the string with the +/// given handle. +/// +/// @ingroup Strings +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] string_handle +/// Handle of the string to query. +/// +/// @param[out] string_value +/// Actual string value (character array). +/// +/// @param[in] length +/// Length of the string buffer (must match size of +/// @p string_value - so including NULL terminator). +/// +HAPI_DECL HAPI_GetString( const HAPI_Session * session, + HAPI_StringHandle string_handle, + char * string_value, + int length ); + +/// @brief Adds the given string to the string table and returns +/// the handle. It is the responsibility of the caller to +/// manage access to the string. The intended use for custom strings +/// is to allow structs that reference strings to be passed in to HAPI +/// +/// @ingroup Strings +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] string_value +/// Actual string value (character array). +/// +/// @param[out] handle_value +/// Handle of the string that was added +/// +HAPI_DECL HAPI_SetCustomString( const HAPI_Session * session, + const char * string_value, + HAPI_StringHandle * handle_value ); + +/// @brief Removes the specified string from the server +/// and invalidates the handle +/// +/// @ingroup Strings +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] string_handle +/// Handle of the string that was added +/// +HAPI_DECL HAPI_RemoveCustomString( const HAPI_Session * session, + const HAPI_StringHandle string_handle ); + +/// @brief Gives back the length of the buffer needed to hold +/// all the values null-separated for the given string +/// handles. Used with ::HAPI_GetStringBatch(). +/// +/// @ingroup Strings +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] string_handle_array +/// Array of string handles to be read. +/// +/// @param[in] string_handle_count +/// Length of @p string_handle_array +/// +/// @param[out] string_buffer_size +/// Buffer length required for subsequent call to +/// HAPI_GetStringBatch to hold all the given +/// string values null-terminated +/// +HAPI_DECL HAPI_GetStringBatchSize( const HAPI_Session * session, + const int * string_handle_array, + int string_handle_count, + int * string_buffer_size ); + +/// @brief Gives back the values of the given string handles. +/// The given char array is filled with null-separated +/// values, and the final value is null-terminated. +/// Used with ::HAPI_GetStringBatchSize(). Using this function +/// instead of repeated calls to ::HAPI_GetString() can be more +/// more efficient for a large number of strings. +/// +/// @ingroup Strings +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] char_buffer +/// Array of characters to hold string values. +/// +/// @param[in] char_array_length +/// Length of @p char_array. Must be large enough to hold +/// all the string values including null separators. +/// +/// +/// +HAPI_DECL HAPI_GetStringBatch( const HAPI_Session * session, + char * char_buffer, + int char_array_length ); + + +/// @defgroup Time +/// Time related functions + +/// @brief Gets the global time of the scene. All API calls deal with +/// this time to cook. +/// +/// @ingroup Time +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] time +/// Time as a double in seconds. +/// +HAPI_DECL HAPI_GetTime( const HAPI_Session * session, double * time ); + +/// @brief Sets the global time of the scene. All API calls will deal +/// with this time to cook. +/// +/// @ingroup Time +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] time +/// Time as a double in seconds. +/// +HAPI_DECL HAPI_SetTime( const HAPI_Session * session, double time ); + +/// @brief Returns whether the Houdini session will use the current time in +/// Houdini when cooking and retrieving data. By default this is +/// disabled and the Houdini session uses time 0 (i.e. frame 1). +/// In SessionSync, it is enabled by default, but can be overridden. +/// Note that this function will ALWAYS return +/// ::HAPI_RESULT_SUCCESS. +/// +/// @ingroup Time +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] enabled +/// Whether use Houdini time is enabled or not. +/// +HAPI_DECL HAPI_GetUseHoudiniTime( const HAPI_Session * session, + HAPI_Bool * enabled ); + +/// @brief Sets whether the Houdini session should use the current time in +/// Houdini when cooking and retrieving data. By default this is +/// disabled and the Houdini session uses time 0 (i.e. frame 1). +/// In SessionSync, it is enabled by default, but can be overridden. +/// Note that this function will ALWAYS return +/// ::HAPI_RESULT_SUCCESS. +/// +/// @ingroup Time +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] enabled +/// Set to true to use Houdini time. +/// +HAPI_DECL HAPI_SetUseHoudiniTime( const HAPI_Session * session, + HAPI_Bool enabled ); + +/// @brief Gets the current global timeline options. +/// +/// @ingroup Time +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] timeline_options +/// The global timeline options struct. +/// +HAPI_DECL HAPI_GetTimelineOptions( const HAPI_Session * session, + HAPI_TimelineOptions * timeline_options ); + +/// @brief Sets the global timeline options. +/// +/// @ingroup Time +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] timeline_options +/// The global timeline options struct. +/// +HAPI_DECL HAPI_SetTimelineOptions( + const HAPI_Session * session, + const HAPI_TimelineOptions * timeline_options ); + +/// @brief Gets the global compositor options. +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] compositor_options +/// The compositor options struct. +/// +HAPI_DECL HAPI_GetCompositorOptions( + const HAPI_Session * session, + HAPI_CompositorOptions * compositor_options); + +/// @brief Sets the global compositor options. +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] compositor_options +/// The compositor options. +/// +HAPI_DECL HAPI_SetCompositorOptions( + const HAPI_Session * session, + const HAPI_CompositorOptions * compositor_options); + +/// @defgroup Assets +/// Functions for managing asset libraries + +/// @brief Loads a Houdini asset library (OTL) from a .otl file. +/// It does NOT create anything inside the Houdini scene. +/// +/// @note This is when we actually check for valid licenses. +/// +/// The next step is to call ::HAPI_GetAvailableAssetCount() +/// to get the number of assets contained in the library using the +/// returned library_id. Then call ::HAPI_GetAvailableAssets() +/// to get the list of available assets by name. Use the asset +/// names with ::HAPI_CreateNode() to actually create +/// one of these nodes in the Houdini scene and get back +/// an asset_id. +/// +/// @note The HIP file saved using ::HAPI_SaveHIPFile() will only +/// have an absolute path reference to the loaded OTL meaning +/// that if the OTL is moved or renamed the HIP file won't +/// load properly. It also means that if you change the OTL +/// using the saved HIP scene the same OTL file will change +/// as the one used with Houdini Engine. +/// See @ref HAPI_Fundamentals_SavingHIPFile. +/// +/// @ingroup Assets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] file_path +/// Absolute path to the .otl file. +/// +/// @param[in] allow_overwrite +/// With this true, if the library file being loaded +/// contains asset definitions that have already been +/// loaded they will overwrite the existing definitions. +/// Otherwise, a library containing asset definitions that +/// already exist will fail to load, returning a +/// ::HAPI_Result of +/// ::HAPI_RESULT_ASSET_DEF_ALREADY_LOADED. +/// +/// @param[out] library_id +/// Newly loaded otl id to be used with +/// ::HAPI_GetAvailableAssetCount() and +/// ::HAPI_GetAvailableAssets(). +/// +HAPI_DECL HAPI_LoadAssetLibraryFromFile( const HAPI_Session * session, + const char * file_path, + HAPI_Bool allow_overwrite, + HAPI_AssetLibraryId * library_id ); + +/// @brief Loads a Houdini asset library (OTL) from memory. +/// It does NOT create anything inside the Houdini scene. +/// +/// @note This is when we actually check for valid licenses. +/// +/// Please note that the performance benefit of loading a library +/// from memory are negligible at best. Due to limitations of +/// Houdini's library manager, there is still some disk access +/// and file writes because every asset library needs to be +/// saved to a real file. Use this function only as a convenience +/// if you already have the library file in memory and don't wish +/// to have to create your own temporary library file and then +/// call ::HAPI_LoadAssetLibraryFromFile(). +/// +/// The next step is to call ::HAPI_GetAvailableAssetCount() +/// to get the number of assets contained in the library using the +/// returned library_id. Then call ::HAPI_GetAvailableAssets() +/// to get the list of available assets by name. Use the asset +/// names with ::HAPI_CreateNode() to actually create +/// one of these nodes in the Houdini scene and get back +/// an asset_id. +/// +/// @note The saved HIP file using ::HAPI_SaveHIPFile() will +/// @a contain the OTL loaded as part of its @b Embedded OTLs. +/// This means that you can safely move or rename the original +/// OTL file and the HIP will continue to work but if you make +/// changes to the OTL while using the saved HIP the changes +/// won't be saved to the original OTL. +/// See @ref HAPI_Fundamentals_SavingHIPFile. +/// +/// @ingroup Assets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] library_buffer +/// The memory buffer containing the asset definitions +/// in the same format as a standard Houdini .otl file. +/// +/// @param[in] library_buffer_length +/// The size of the OTL memory buffer. +/// +/// @param[in] allow_overwrite +/// With this true, if the library file being loaded +/// contains asset definitions that have already been +/// loaded they will overwrite the existing definitions. +/// Otherwise, a library containing asset definitions that +/// already exist will fail to load, returning a +/// ::HAPI_Result of +/// ::HAPI_RESULT_ASSET_DEF_ALREADY_LOADED. +/// +/// @param[out] library_id +/// Newly loaded otl id to be used with +/// ::HAPI_GetAvailableAssetCount() and +/// ::HAPI_GetAvailableAssets(). +/// +HAPI_DECL HAPI_LoadAssetLibraryFromMemory( const HAPI_Session * session, + const char * library_buffer, + int library_buffer_length, + HAPI_Bool allow_overwrite, + HAPI_AssetLibraryId * library_id ); + +/// @brief Get the number of assets contained in an asset library. +/// You should call ::HAPI_LoadAssetLibraryFromFile() prior to +/// get a library_id. +/// +/// @ingroup Assets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] library_id +/// Returned by ::HAPI_LoadAssetLibraryFromFile(). +/// +/// +/// @param[out] asset_count +/// The number of assets contained in this asset library. +/// +HAPI_DECL HAPI_GetAvailableAssetCount( const HAPI_Session * session, + HAPI_AssetLibraryId library_id, + int * asset_count ); + +/// @brief Get the names of the assets contained in an asset library. +/// +/// The asset names will contain additional information about +/// the type of asset, namespace, and version, along with the +/// actual asset name. For example, if you have an Object type +/// asset, in the "hapi" namespace, of version 2.0, named +/// "foo", the asset name returned here will be: +/// hapi::Object/foo::2.0 +/// +/// You should call ::HAPI_LoadAssetLibraryFromFile() prior to +/// get a library_id. Then, you should call +/// ::HAPI_GetAvailableAssetCount() to get the number of assets to +/// know how large of a string handles array you need to allocate. +/// +/// @ingroup Assets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] library_id +/// Returned by ::HAPI_LoadAssetLibraryFromFile(). +/// +/// +/// @param[out] asset_names_array +/// Array of string handles (integers) that should be +/// at least the size of asset_count. +/// +/// @param[in] asset_count +/// Should be the same or less than the value returned by +/// ::HAPI_GetAvailableAssetCount(). +/// +/// +/// +HAPI_DECL HAPI_GetAvailableAssets( const HAPI_Session * session, + HAPI_AssetLibraryId library_id, + HAPI_StringHandle * asset_names_array, + int asset_count ); + +/// @brief Fill an asset_info struct from a node. +/// +/// @ingroup Assets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] asset_info +/// Returned ::HAPI_AssetInfo struct. +/// +HAPI_DECL HAPI_GetAssetInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_AssetInfo * asset_info ); + +/// @brief Get the number of asset parameters contained in an asset +/// library, as well as the number of parameter int, float, +/// string, and choice values. +/// +/// This does not create the asset in the session. +/// Use this for faster querying of asset parameters compared to +/// creating the asset node and querying the node's parameters. +/// +/// This does require ::HAPI_LoadAssetLibraryFromFile() to be +/// called prior, in order to load the asset library and +/// acquire library_id. Then ::HAPI_GetAvailableAssetCount and +/// ::HAPI_GetAvailableAssets should be called to get the +/// asset_name. +/// +/// @ingroup Assets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] library_id +/// Returned by ::HAPI_LoadAssetLibraryFromFile(). +/// +/// +/// @param[in] asset_name +/// Name of the asset to get the parm counts for. +/// +/// @param[out] parm_count +/// The number of parameters in the asset library. +/// +/// @param[out] int_value_count +/// The number of int values for parameters in the asset +/// library. +/// +/// @param[out] float_value_count +/// The number of float values for parameters in the asset +/// library. +/// +/// @param[out] string_value_count +/// The number of string values for parameters in the asset +/// library. +/// +/// @param[out] choice_value_count +/// The number of choice values for parameters in the asset +/// library. +/// +HAPI_DECL HAPI_GetAssetDefinitionParmCounts( const HAPI_Session * session, + HAPI_AssetLibraryId library_id, + const char * asset_name, + int * parm_count, + int * int_value_count, + int * float_value_count, + int * string_value_count, + int * choice_value_count ); + +/// @brief Fill an array of ::HAPI_ParmInfo structs with parameter +/// information for the specified asset in the specified asset +/// library. +/// +/// This does not create the asset in the session. +/// Use this for faster querying of asset parameters compared to +/// creating the asset node and querying the node's parameters. +/// +/// This does require ::HAPI_LoadAssetLibraryFromFile() to be +/// called prior, in order to load the asset library and +/// acquire library_id. ::HAPI_GetAssetDefinitionParmCounts should +/// be called prior to acquire the count for the size of +/// parm_infos_array. +/// +/// @ingroup Assets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] library_id +/// Returned by ::HAPI_LoadAssetLibraryFromFile(). +/// +/// +/// @param[in] asset_name +/// Name of the asset to get the parm counts for. +/// +/// @param[out] parm_infos_array +/// Array of ::HAPI_ParmInfo at least the size of +/// length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most parm_count - 1 acquired from +/// ::HAPI_GetAssetInfo. +/// +/// +/// +/// +/// @param[in] length +/// Must be at least 1 and at most parm_count - start acquired +/// from ::HAPI_GetAssetInfo +/// +/// +/// +/// +HAPI_DECL HAPI_GetAssetDefinitionParmInfos( const HAPI_Session * session, + HAPI_AssetLibraryId library_id, + const char * asset_name, + HAPI_ParmInfo * parm_infos_array, + int start, + int length ); + +/// @brief Fill arrays of parameter int values, float values, string values, +/// and choice values for parameters in the specified asset in the +/// specified asset library. +/// +/// This does not create the asset in the session. +/// Use this for faster querying of asset parameters compared to +/// creating the asset node and querying the node's parameters. +/// Note that only default values are retrieved. +/// +/// This does require ::HAPI_LoadAssetLibraryFromFile() to be +/// called prior, in order to load the asset library and +/// acquire library_id. ::HAPI_GetAssetDefinitionParmCounts should +/// be called prior to acquire the counts for the sizes of +/// the values arrays. +/// +/// @ingroup Assets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] library_id +/// Returned by ::HAPI_LoadAssetLibraryFromFile(). +/// +/// +/// @param[in] asset_name +/// Name of the asset to get the parm counts for. +/// +/// @param[out] int_values_array +/// Array of ints at least the size of int_length. +/// +/// @param[in] int_start +/// First index of range for int_values_array. Must be at +/// least 0 and at most int_value_count - 1 acquired from +/// ::HAPI_GetAssetDefinitionParmCounts. +/// +/// +/// +/// +/// @param[in] int_length +/// Must be at least 0 and at most int_value_count - int_start +/// acquired from ::HAPI_GetAssetDefinitionParmCounts. +/// +/// +/// +/// +/// @param[out] float_values_array +/// Array of floats at least the size of float_length. +/// +/// @param[in] float_start +/// First index of range for float_values_array. Must be at +/// least 0 and at most float_value_count - 1 acquired from +/// ::HAPI_GetAssetDefinitionParmCounts. +/// +/// +/// +/// +/// @param[in] float_length +/// Must be at least 0 and at most float_value_count - +/// float_start acquired from +/// ::HAPI_GetAssetDefinitionParmCounts. +/// +/// +/// +/// +/// @param[in] string_evaluate +/// Whether or not to evaluate the string expressions. +/// For example, the string "$F" would evaluate to the +/// current frame number. So, passing in evaluate = false +/// would give you back the string "$F" and passing +/// in evaluate = true would give you back "1" (assuming +/// the current frame is 1). +/// +/// +/// @param[out] string_values_array +/// Array of HAPI_StringHandle at least the size of +/// string_length. +/// +/// @param[in] string_start +/// First index of range for string_values_array. Must be at +/// least 0 and at most string_value_count - 1 acquired from +/// ::HAPI_GetAssetDefinitionParmCounts. +/// +/// +/// +/// +/// @param[in] string_length +/// Must be at least 0 and at most string_value_count - +/// string_start acquired from +/// ::HAPI_GetAssetDefinitionParmCounts. +/// +/// +/// +/// +/// @param[out] choice_values_array +/// Array of ::HAPI_ParmChoiceInfo at least the size of +/// choice_length. +/// +/// @param[in] choice_start +/// First index of range for choice_values_array. Must be at +/// least 0 and at most choice_value_count - 1 acquired from +/// ::HAPI_GetAssetDefinitionParmCounts. +/// +/// +/// +/// +/// @param[in] choice_length +/// Must be at least 0 and at most choice_value_count - +/// choice_start acquired from +/// ::HAPI_GetAssetDefinitionParmCounts. +/// +/// +/// +/// +HAPI_DECL HAPI_GetAssetDefinitionParmValues( + const HAPI_Session * session, + HAPI_AssetLibraryId library_id, + const char * asset_name, + int * int_values_array, + int int_start, + int int_length, + float * float_values_array, + int float_start, + int float_length, + HAPI_Bool string_evaluate, + HAPI_StringHandle * string_values_array, + int string_start, + int string_length, + HAPI_ParmChoiceInfo * choice_values_array, + int choice_start, + int choice_length ); + +/// @brief +/// +/// @ingroup Assets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] library_id +/// Returned by ::HAPI_LoadAssetLibraryFromFile(). +/// +/// +/// @param[in] asset_name +/// Name of the asset that the parm tag is being retrieved from. +/// +/// @param[in] parm_id +/// Id of the parm that the tag belongs to. +/// +/// @param[in] tag_index +/// The index of the parm tag to retrieve the name of. +/// +/// @param[out] tag_name +/// The string handle for the specified parm tag's name. +/// +HAPI_DECL HAPI_GetAssetDefinitionParmTagName( + const HAPI_Session * session, + HAPI_AssetLibraryId library_id, + const char * asset_name, + HAPI_ParmId parm_id, + int tag_index, + HAPI_StringHandle * tag_name ); + +/// @brief +/// +/// @ingroup Assets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] library_id +/// Returned by ::HAPI_LoadAssetLibraryFromFile(). +/// +/// +/// @param[in] asset_name +/// Name of the asset that the parm tag is being retrieved from. +/// +/// @param[in] parm_id +/// Id of the parm that the tag belongs to. +/// +/// @param[in] tag_name +/// The name of the parm tag to retrieve the value of. +/// +/// @param[out] tag_value +/// The string handle for the specified parm tag's value. +/// +HAPI_DECL HAPI_GetAssetDefinitionParmTagValue( + const HAPI_Session * session, + HAPI_AssetLibraryId library_id, + const char * asset_name, + HAPI_ParmId parm_id, + const char * tag_name, + HAPI_StringHandle * tag_value ); + +/// @brief Gets the number of HDAs that have been loaded by Houdini. +/// +/// @note This only includes HDAs that have been loaded from disk. +/// Embedded HDAs will be excluded. +/// +/// @ingroup Assets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] count +/// The returned number of loaded HDAs. +/// +HAPI_DECL HAPI_GetLoadedAssetLibraryCount( + const HAPI_Session * session, + int * count); + +/// @brief Gets the HAPI_AssetLibraryId's for HDAs that are loaded in Houdini. +/// +/// @ingroup Assets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] asset_library_ids_array +/// Array of HAPI_AssetLibraryId's at least the size of length. +/// +/// @param[in] start +/// First index from the list of HAPI_AssetLibraryId's to +/// return. Must be at least 0 and at most count - 1 where count +/// is the value returned by ::HAPI_GetLoadedAssetLibraryCount. +/// +/// @param[in] length +/// The number of HAPI_AssetLibraryId's to return. Must be at +/// least 0 and at most count - start where count is the value +/// returned by HAPI_GetLoadedAssetLibraryCount. +/// +HAPI_DECL HAPI_GetAssetLibraryIds( + const HAPI_Session * session, + HAPI_AssetLibraryId * asset_library_ids_array, + int start, + int length); + +/// @brief Gets the HAPI_StringHandle for the file path of a loaded asset +/// library. +/// +/// @ingroup Assets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// @param[in] asset_library_id +/// The HAPI_AssetLibraryId of the asset library. +/// +/// @param[out] file_path_sh +/// The returned HAPI_StringHandle of the asset's file path on +/// disk. +/// +HAPI_DECL HAPI_GetAssetLibraryFilePath( + const HAPI_Session * session, + HAPI_AssetLibraryId asset_library_id, + HAPI_StringHandle * file_path_sh); + +/// @defgroup HipFiles Hip Files +/// Functions for managing hip files + +/// @brief Loads a .hip file into the main Houdini scene. +/// +/// @note In threaded mode, this is an _async call_! +/// +/// @note This method will load the HIP file into the scene. This means +/// that any registered `hou.hipFile` event callbacks will be triggered +/// with the `hou.hipFileEventType.BeforeMerge` and +/// `hou.hipFileEventType.AfterMerge` events. +/// +/// @note This method loads a HIP file, completely overwriting +/// everything that already exists in the scene. Therefore, any HAPI ids +/// (node ids, part ids, etc.) that were obtained before calling this +/// method will be invalidated. +/// +/// @ingroup HipFiles +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] file_name +/// Absolute path to the .hip file to load. +/// +/// @param[in] cook_on_load +/// Set to true if you wish the nodes to cook as soon +/// as they are created. Otherwise, you will have to +/// call ::HAPI_CookNode() explicitly for each after you +/// call this function. +/// +/// +HAPI_DECL HAPI_LoadHIPFile( const HAPI_Session * session, + const char * file_name, + HAPI_Bool cook_on_load ); + +/// @brief Loads a .hip file into the main Houdini scene. +/// +/// @note In threaded mode, this is an _async call_! +/// +/// @note This method will merge the HIP file into the scene. This means +/// that any registered `hou.hipFile` event callbacks will be triggered +/// with the `hou.hipFileEventType.BeforeMerge` and +/// `hou.hipFileEventType.AfterMerge` events. +/// +/// @ingroup HipFiles +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// @param[in] file_name +/// Absolute path to the .hip file to load. +/// +/// @param[in] cook_on_load +/// Set to true if you wish the nodes to cook as soon +/// as they are created. Otherwise, you will have to +/// call ::HAPI_CookNode() explicitly for each after you +/// call this function. +/// +/// @param[out] file_id +/// This parameter will be set to the HAPI_HIPFileId of the +/// loaded HIP file. This can be used to lookup nodes that were +/// created as a result of loading this HIP file. +/// +HAPI_DECL HAPI_MergeHIPFile(const HAPI_Session * session, + const char * file_name, + HAPI_Bool cook_on_load, + HAPI_HIPFileId * file_id); + +/// @brief Saves a .hip file of the current Houdini scene. +/// +/// @ingroup HipFiles +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] file_path +/// Absolute path to the .hip file to save to. +/// +/// @param[in] lock_nodes +/// Specify whether to lock all SOP nodes before saving +/// the scene file. This way, when you load the scene +/// file you can see exactly the state of each SOP at +/// the time it was saved instead of relying on the +/// re-cook to accurately reproduce the state. It does, +/// however, take a lot more space and time locking all +/// nodes like this. +/// +/// +HAPI_DECL HAPI_SaveHIPFile( const HAPI_Session * session, + const char * file_path, + HAPI_Bool lock_nodes ); + +/// @brief Gets the number of nodes that were created as a result of loading a +/// .hip file +/// +/// @ingroup HipFiles +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// @param[in] id +/// The HIP file id. +/// +/// @param[out] count +/// Pointer to an int where the HIP file node count will be +/// stored. +HAPI_DECL HAPI_GetHIPFileNodeCount(const HAPI_Session * session, + HAPI_HIPFileId id, + int * count); + +/// @brief Fills an array of ::HAPI_NodeId of nodes that were created as a +/// result of loading the HIP file specified by the ::HAPI_HIPFileId +/// +/// @ingroup HipFiles +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// @param[in] id +/// The HIP file id. +/// +/// @param[out] node_ids +/// Array of ::HAPI_NodeId at least the size of length. +/// +/// @param[in] length +/// The number of ::HAPI_NodeId to be stored. This should be at +/// least 0 and at most the count provided by +/// HAPI_GetHIPFileNodeCount +HAPI_DECL HAPI_GetHIPFileNodeIds(const HAPI_Session * session, + HAPI_HIPFileId id, + HAPI_NodeId * node_ids, + int length); + +/// @defgroup Nodes +/// Functions for working with nodes + +/// @brief Determine if your instance of the node actually still exists +/// inside the Houdini scene. This is what can be used to +/// determine when the Houdini scene needs to be re-populated +/// using the host application's instances of the nodes. +/// Note that this function will ALWAYS return +/// ::HAPI_RESULT_SUCCESS. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] unique_node_id +/// The unique node id from +/// ::HAPI_NodeInfo::uniqueHoudiniNodeId. +/// +/// +/// @param[out] answer +/// Answer to the question. +/// +HAPI_DECL HAPI_IsNodeValid( const HAPI_Session * session, + HAPI_NodeId node_id, + int unique_node_id, + HAPI_Bool * answer ); + +/// @brief Fill an ::HAPI_NodeInfo struct. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] node_info +/// Return value - contains things like asset id. +/// +HAPI_DECL HAPI_GetNodeInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_NodeInfo * node_info ); + +/// @brief Get the node absolute path in the Houdini node network or a +/// relative path any other node. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] relative_to_node_id +/// Set this to -1 to get the absolute path of the node_id. +/// Otherwise, the path will be relative to this node id. +/// +/// @param[out] path +/// The returned path string, valid until the next call to +/// this function. +/// +HAPI_DECL HAPI_GetNodePath( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_NodeId relative_to_node_id, + HAPI_StringHandle * path ); + +/// @brief Get the root node of a particular network type (ie. OBJ). +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_type +/// The node network type. +/// +/// @param[out] node_id +/// The node id of the root node network. +/// +HAPI_DECL HAPI_GetManagerNodeId( const HAPI_Session * session, + HAPI_NodeType node_type, + HAPI_NodeId * node_id ); + +/// @brief Compose a list of child nodes based on given filters. +/// +/// This function will only compose the list of child nodes. It will +/// not return this list. After your call to this function, call +/// HAPI_GetComposedChildNodeList() to get the list of child node ids. +/// +/// Note: When looking for all Display SOP nodes using this function, +/// and using recursive mode, the recursion will stop as soon as a +/// display SOP is found within each OBJ geometry network. It is +/// almost never useful to get a list of ALL display SOP nodes +/// recursively as they would all containt the same geometry. Even so, +/// this special case only comes up if the display SOP itself is a +/// subnet. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] parent_node_id +/// The node id of the parent node. +/// +/// @param[in] node_type_filter +/// The node type by which to filter the children. +/// +/// @param[in] node_flags_filter +/// The node flags by which to filter the children. +/// +/// @param[in] recursive +/// Whether or not to compose the list recursively. +/// +/// @param[out] count +/// The number of child nodes composed. Use this as the +/// argument to ::HAPI_GetComposedChildNodeList(). +/// +HAPI_DECL HAPI_ComposeChildNodeList( const HAPI_Session * session, + HAPI_NodeId parent_node_id, + HAPI_NodeTypeBits node_type_filter, + HAPI_NodeFlagsBits node_flags_filter, + HAPI_Bool recursive, + int * count ); + +/// @brief Get the composed list of child node ids from the previous call +/// to HAPI_ComposeChildNodeList(). +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] parent_node_id +/// The node id of the parent node. +/// +/// @param[out] child_node_ids_array +/// The array of ::HAPI_NodeId for the child nodes. +/// +/// @param[in] count +/// The number of children in the composed list. MUST match +/// the count returned by HAPI_ComposeChildNodeList(). +/// +/// +/// +/// +HAPI_DECL HAPI_GetComposedChildNodeList( const HAPI_Session * session, + HAPI_NodeId parent_node_id, + HAPI_NodeId * child_node_ids_array, + int count ); + +/// @brief Create a node inside a node network. Nodes created this way +/// will have their ::HAPI_NodeInfo::createdPostAssetLoad set +/// to true. +/// +/// @note In threaded mode, this is an _async call_! +/// +/// @note This is also when we actually check for valid licenses. +/// +/// This API will invoke the cooking thread if threading is +/// enabled. This means it will return immediately with a call +/// result of ::HAPI_RESULT_SUCCESS, even if fed garbage. Use +/// the status and cooking count APIs under DIAGNOSTICS to get +/// a sense of the progress. All other API calls will block +/// until the creation (and, optionally, the first cook) +/// of the node has finished. +/// +/// Also note that the cook result won't be of type +/// ::HAPI_STATUS_CALL_RESULT like all calls (including this one). +/// Whenever the threading cook is done it will fill the +/// @a cook result which is queried using +/// ::HAPI_STATUS_COOK_RESULT. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] parent_node_id +/// The parent node network's node id or -1 if the parent +/// network is the manager (top-level) node. In that case, +/// the manager must be identified by the table name in the +/// operator_name. +/// +/// +/// +/// @param[in] operator_name +/// The name of the node operator type. +/// +/// If you are creating an Object or SOP node, you can pass +/// parent_node_id == -1 as long as you include the table name +/// (ie. Object/ or Sop/) as a prefix to the operator_name. +/// This covenience is only available for Object or SOP nodes. +/// This is the common case for when creating asset nodes +/// from a loaded asset library. In that case, just pass +/// whatever ::HAPI_GetAvailableAssets() returns. +/// +/// If you have a parent_node_id then you should +/// include only the namespace, name, and version. +/// +/// For example, lets say you have an Object type asset, in +/// the "hapi" namespace, of version 2.0, named "foo". If +/// you pass parent_node_id == -1, then set the operator_name +/// as "Object/hapi::foo::2.0". Otherwise, if you have a valid +/// parent_node_id, then just pass operator_name as +/// "hapi::foo::2.0". +/// +/// @param[in] node_label +/// (Optional) The label of the newly created node. +/// +/// +/// @param[in] cook_on_creation +/// Set whether the node should cook once created or not. +/// +/// +/// @param[out] new_node_id +/// The returned node id of the just-created node. +/// +HAPI_DECL HAPI_CreateNode( const HAPI_Session * session, + HAPI_NodeId parent_node_id, + const char * operator_name, + const char * node_label, + HAPI_Bool cook_on_creation, + HAPI_NodeId * new_node_id ); + +/// @brief Creates a simple geometry SOP node that can accept geometry input. +/// Inside the specified parent node, this will create a Null SOP +/// you can set the geometry of using the geometry SET APIs. +/// You can then connect this node to any other node as a geometry +/// input. +/// +/// Note that when saving the Houdini scene using +/// ::HAPI_SaveHIPFile() the nodes created with this +/// method will be green and will start with the name "input". +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] parent_node_id +/// The node id of the parent OBJ node or SOP subnetwork node in +/// which the input node should be created, or -1 to create a +/// new dummy parent OBJ node for this input node. +/// +/// +/// +/// @param[out] node_id +/// Newly created node's id. Use ::HAPI_GetNodeInfo() +/// to get more information about the node. +/// +/// @param[in] name +/// Give this input node a name for easy debugging. +/// The node's parent OBJ node and the Null SOP node will both +/// get this given name with "input_" prepended. +/// You can also pass NULL in which case the name will +/// be "input#" where # is some number. +/// +/// +HAPI_DECL HAPI_CreateInputNode( const HAPI_Session * session, + HAPI_NodeId parent_node_id, + HAPI_NodeId * node_id, + const char * name ); + +/// @brief Helper for creating specifically creating a curve input geometry SOP. +/// Inside the specified parent node, this will create a Null SOP that +/// contains the the HAPI_ATTRIB_INPUT_CURVE_COORDS attribute. +/// It will setup the node as a curve part with no points. +/// In addition to creating the input node, it will also commit and cook +/// the geometry. +/// +/// Note that when saving the Houdini scene using +/// ::HAPI_SaveHIPFile() the nodes created with this +/// method will be green and will start with the name "input". +/// +/// @ingroup InputCurves +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] parent_node_id +/// The node id of the parent OBJ node or SOP subnetwork node in +/// which the input node should be created, or -1 to create a +/// new dummy parent OBJ node for this input node. +/// +/// +/// +/// @param[out] node_id +/// Newly created node's id. Use ::HAPI_GetNodeInfo() +/// to get more information about the node. +/// +/// @param[in] name +/// Give this input node a name for easy debugging. +/// The node's parent OBJ node and the Null SOP node will both +/// get this given name with "input_" prepended. +/// You can also pass NULL in which case the name will +/// be "input#" where # is some number. +/// +/// +HAPI_DECL HAPI_CreateInputCurveNode( const HAPI_Session * session, + HAPI_NodeId parent_node_id, + HAPI_NodeId * node_id, + const char * name ); + + +/// @defgroup HeightFields Height Fields +/// Functions for creating and inspecting HAPI session state. + +/// @brief Creates the required node hierarchy needed for heightfield inputs. +/// +/// Note that when saving the Houdini scene using +/// ::HAPI_SaveHIPFile() the nodes created with this +/// method will be green and will start with the name "input". +/// +/// @ingroup HeightFields +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] parent_node_id +/// The parent node network's node id or -1 if the parent +/// network is the manager (top-level) node. In that case, +/// the manager must be identified by the table name in the +/// operator_name. +/// +/// +/// +/// @param[in] name +/// Give this input node a name for easy debugging. +/// The node's parent OBJ node and the Null SOP node will both +/// get this given name with "input_" prepended. +/// You can also pass NULL in which case the name will +/// be "input#" where # is some number. +/// +/// +/// @param[in] xsize +/// size of the heightfield in X +/// +/// @param[in] ysize +/// size of the heightfield in y +/// +/// @param[in] voxelsize +/// Size of the voxel +/// +/// @param[in] sampling +/// Type of sampling which should be either center or corner. +/// +/// @param[out] heightfield_node_id +/// Newly created node id for the heightfield node. +/// Use ::HAPI_GetNodeInfo() to get more information about +/// the node. +/// +/// @param[out] height_node_id +/// Newly created node id for the height volume. +/// Use ::HAPI_GetNodeInfo() to get more information about the node. +/// +/// @param[out] mask_node_id +/// Newly created node id for the mask volume. +/// Use ::HAPI_GetNodeInfo() to get more information about the +/// node. +/// +/// @param[out] merge_node_id +/// Newly created merge node id. +/// The merge node can be used to connect additional input masks. +/// Use ::HAPI_GetNodeInfo() to get more information about the node. +/// +HAPI_DECL HAPI_CreateHeightFieldInput( const HAPI_Session * session, + HAPI_NodeId parent_node_id, + const char * name, + int xsize, + int ysize, + float voxelsize, + HAPI_HeightFieldSampling sampling, + HAPI_NodeId * heightfield_node_id, + HAPI_NodeId * height_node_id, + HAPI_NodeId * mask_node_id, + HAPI_NodeId * merge_node_id ); + +/// @brief Creates a volume input node that can be used with Heightfields +/// +/// Note that when saving the Houdini scene using +/// ::HAPI_SaveHIPFile() the nodes created with this +/// method will be green and will start with the name "input". +/// +/// @ingroup HeightFields +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] parent_node_id +/// The parent node network's node id or -1 if the parent +/// network is the manager (top-level) node. In that case, +/// the manager must be identified by the table name in the +/// operator_name. +/// +/// +/// +/// @param[out] new_node_id +/// Newly created node id for the volume. +/// Use ::HAPI_GetNodeInfo() to get more information about the +/// node. +/// +/// @param[in] name +/// The name of the volume to create. +/// You can also pass NULL in which case the name will +/// be "input#" where # is some number. +/// +/// +/// @param[in] xsize +/// size of the heightfield in X +/// +/// @param[in] ysize +/// size of the heightfield in y +/// +/// @param[in] voxelsize +/// Size of the voxel +/// +HAPI_DECL HAPI_CreateHeightfieldInputVolumeNode( const HAPI_Session * session, + HAPI_NodeId parent_node_id, + HAPI_NodeId * new_node_id, + const char * name, + int xsize, + int ysize, + float voxelsize ); + +/// @brief Initiate a cook on this node. Note that this may trigger +/// cooks on other nodes if they are connected. +/// +/// @note In threaded mode, this is an _async call_! +/// +/// This API will invoke the cooking thread if threading is +/// enabled. This means it will return immediately. Use +/// the status and cooking count APIs under DIAGNOSTICS to get +/// a sense of the progress. All other API calls will block +/// until the cook operation has finished. +/// +/// Also note that the cook result won't be of type +/// ::HAPI_STATUS_CALL_RESULT like all calls (including this one). +/// Whenever the threading cook is done it will fill the +/// @a cook result which is queried using +/// ::HAPI_STATUS_COOK_RESULT. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] cook_options +/// The cook options. Pass in NULL to use the global +/// cook options that you specified when calling +/// ::HAPI_Initialize(). +/// +/// +HAPI_DECL HAPI_CookNode( const HAPI_Session * session, + HAPI_NodeId node_id, + const HAPI_CookOptions * cook_options ); + +/// @brief Delete a node from a node network. Only nodes with their +/// ::HAPI_NodeInfo::createdPostAssetLoad set to true can be +/// deleted this way. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node to delete. +/// +HAPI_DECL HAPI_DeleteNode( const HAPI_Session * session, + HAPI_NodeId node_id ); + +/// @brief Rename a node that you created. Only nodes with their +/// ::HAPI_NodeInfo::createdPostAssetLoad set to true can be +/// renamed this way. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node to rename. +/// +/// @param[in] new_name +/// The new node name. +/// +HAPI_DECL HAPI_RenameNode( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * new_name ); + +/// @brief Connect two nodes together. +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @ingroup Nodes +/// +/// @param[in] node_id +/// The node whom's input to connect to. +/// +/// @param[in] input_index +/// The input index. Should be between 0 and the +/// to_node's ::HAPI_NodeInfo::inputCount - 1. +/// +/// +/// @param[in] node_id_to_connect +/// The node to connect to node_id's input. +/// +/// @param[in] output_index +/// The output index. Should be between 0 and the +/// to_node's ::HAPI_NodeInfo::outputCount - 1. +/// +/// +HAPI_DECL HAPI_ConnectNodeInput( const HAPI_Session * session, + HAPI_NodeId node_id, + int input_index, + HAPI_NodeId node_id_to_connect, + int output_index ); + +/// @brief Disconnect a node input. +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @ingroup Nodes +/// +/// @param[in] node_id +/// The node whom's input to disconnect. +/// +/// @param[in] input_index +/// The input index. Should be between 0 and the +/// to_node's ::HAPI_NodeInfo::inputCount - 1. +/// +/// +HAPI_DECL HAPI_DisconnectNodeInput( const HAPI_Session * session, + HAPI_NodeId node_id, + int input_index ); + +/// @brief Query which node is connected to another node's input. +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @ingroup Nodes +/// +/// @param[in] node_to_query +/// The node to query. +/// +/// @param[in] input_index +/// The input index. Should be between 0 and the +/// to_node's ::HAPI_NodeInfo::inputCount - 1. +/// +/// +/// @param[out] connected_node_id +/// The node id of the connected node to this input. If +/// nothing is connected then -1 will be returned. +/// +HAPI_DECL HAPI_QueryNodeInput( const HAPI_Session * session, + HAPI_NodeId node_to_query, + int input_index, + HAPI_NodeId * connected_node_id ); + +/// @brief Get the name of an node's input. This function will return +/// a string handle for the name which will be valid (persist) +/// until the next call to this function. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] input_idx +/// The input index. Should be between 0 and the +/// node_to_query's ::HAPI_NodeInfo::inputCount - 1. +/// +/// +/// @param[out] name +/// Input name string handle return value - valid until +/// the next call to this function. +/// +HAPI_DECL HAPI_GetNodeInputName( const HAPI_Session * session, + HAPI_NodeId node_id, + int input_idx, + HAPI_StringHandle * name ); + +/// @brief Disconnect all of the node's output connections at the output index. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node whom's outputs to disconnect. +/// +/// @param[in] output_index +/// The output index. Should be between 0 and the +/// to_node's ::HAPI_NodeInfo::outputCount. +/// +/// +HAPI_DECL HAPI_DisconnectNodeOutputsAt( const HAPI_Session * session, + HAPI_NodeId node_id, + int output_index ); + +/// @brief Get the number of nodes currently connected to the given node at +/// the output index. +/// +/// @ingroup Nodes +/// +/// Use the @c count returned by this function to get the +/// ::HAPI_NodeId of connected nodes using +/// ::HAPI_QueryNodeOutputConnectedNodes(). +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] output_idx +/// The output index. Should be between 0 and the +/// to_node's ::HAPI_NodeInfo::outputCount - 1. +/// +/// +/// +/// @param[in] into_subnets +/// Whether to search by diving into subnets. +/// +/// +/// @param[in] through_dots +/// Whether to search through dots. +/// +/// +/// @param[out] connected_count +/// The number of nodes currently connected to this node at +/// given output index. Use this count with a call to +/// ::HAPI_QueryNodeOutputConnectedNodes() to get list of +/// connected nodes. +/// +HAPI_DECL HAPI_QueryNodeOutputConnectedCount( const HAPI_Session * session, + HAPI_NodeId node_id, + int output_idx, + HAPI_Bool into_subnets, + HAPI_Bool through_dots, + int * connected_count ); + +/// @brief Get the ids of nodes currently connected to the given node +/// at the output index. +/// +/// Use the @c connected_count returned by +/// ::HAPI_QueryNodeOutputConnectedCount(). +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] output_idx +/// The output index. Should be between 0 and the +/// to_node's ::HAPI_NodeInfo::outputCount - 1. +/// +/// +/// +/// @param[in] into_subnets +/// Whether to search by diving into subnets. +/// +/// +/// @param[in] through_dots +/// Whether to search through dots. +/// +/// +/// @param[out] connected_node_ids_array +/// Array of ::HAPI_NodeId at least the size of @c length. +/// +/// @param[in] start +/// At least @c 0 and at most @c connected_count returned by +/// ::HAPI_QueryNodeOutputConnectedCount(). +/// +/// +/// +/// +/// @param[in] length +/// Given @c connected_count returned by +/// ::HAPI_QueryNodeOutputConnectedCount(), @c length should +/// be at least @c 1 and at most connected_count - start. +/// +/// +/// +/// +HAPI_DECL HAPI_QueryNodeOutputConnectedNodes( const HAPI_Session * session, + HAPI_NodeId node_id, + int output_idx, + HAPI_Bool into_subnets, + HAPI_Bool through_dots, + HAPI_NodeId * connected_node_ids_array, + int start, int length ); + +/// @brief Get the name of an node's output. This function will return +/// a string handle for the name which will be valid (persist) +/// until the next call to this function. +/// Only COPs and VOPs can have output names - other node types +/// will return their node label when this is called +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] output_idx +/// The output index. Should be between 0 and the +/// to_node's ::HAPI_NodeInfo::outputCount - 1. +/// +/// +/// +/// @param[out] name +/// Output name string handle return value - valid until +/// the next call to this function. +/// +HAPI_DECL HAPI_GetNodeOutputName( const HAPI_Session * session, + HAPI_NodeId node_id, + int output_idx, + HAPI_StringHandle * name ); + +/// @brief Get the id of the node with the specified path. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] parent_node_id +/// If @c path does not start with "/", search for the path +/// relative to this node. Provide -1 if @c path is an absolute +/// path. +/// +/// @param[in] path +/// The path of the node. If the path does not start with "/", +/// it is treated as a relative path from the node specified in +/// @c parent_node_id. +/// +/// @param[out] node_id +/// The id of the found node. +/// +HAPI_DECL HAPI_GetNodeFromPath( const HAPI_Session * session, + const HAPI_NodeId parent_node_id, + const char * path, + HAPI_NodeId * node_id ); + +/// @brief Gets the node id of an output node in a SOP network. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id of a SOP node with at least one output node. The +/// total number of node outputs can be found from the node's +/// ::HAPI_NodeInfo::outputCount +/// +/// @param[in] output +/// The output index. Should be between 0 and the node's +/// ::HAPI_NodeInfo::outputCount - 1. +/// +/// +/// +/// @param[out] output_node_id +/// Pointer to a HAPI_NodeId where the node id of the output +/// node will be stored. +HAPI_DECL HAPI_GetOutputNodeId( const HAPI_Session * session, + HAPI_NodeId node_id, + int output, + HAPI_NodeId * output_node_id ); + +/// @defgroup Parms Parms +/// Functions for wroking with Node parameters (parms) + +/// @brief Fill an array of ::HAPI_ParmInfo structs with parameter +/// information from the asset instance node. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] parm_infos_array +/// Array of ::HAPI_ParmInfo at least the size of +/// length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_NodeInfo::parmCount - 1. +/// +/// +/// +/// +/// @param[in] length +/// Must be at least 1 and at most +/// ::HAPI_NodeInfo::parmCount - start. +/// +/// +/// +/// +HAPI_DECL HAPI_GetParameters( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_ParmInfo * parm_infos_array, + int start, int length ); + +/// @brief Get the parm info of a parameter by parm id. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_id +/// The parm id. +/// +/// @param[out] parm_info +/// The returned parm info. +/// +HAPI_DECL HAPI_GetParmInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_ParmId parm_id, + HAPI_ParmInfo * parm_info ); + +/// @brief All parameter APIs require a ::HAPI_ParmId but if you know the +/// parameter you wish to operate on by name than you can use +/// this function to get its ::HAPI_ParmId. If the parameter with +/// the given name is not found the parameter id returned +/// will be -1. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The parm name. +/// +/// @param[out] parm_id +/// The return value. The parameter's ::HAPI_ParmId. If +/// the parameter with the given name is not found the +/// parameter id returned will be -1. +/// +HAPI_DECL HAPI_GetParmIdFromName( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name, + HAPI_ParmId * parm_id ); + +/// @brief Get the parm info of a parameter by name. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The parm name. +/// +/// @param[out] parm_info +/// The returned parm info. +/// +HAPI_DECL HAPI_GetParmInfoFromName( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name, + HAPI_ParmInfo * parm_info ); + +/// @brief Get the tag name on a parameter given an index. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_id +/// The parm id. +/// +/// @param[in] tag_index +/// The tag index, which should be between 0 and +/// ::HAPI_ParmInfo::tagCount - 1. +/// @note These indices are invalidated whenever tags are added +/// to parameters. Do not store these or expect them to be the +/// same if the scene is modified. +/// +/// +/// +/// @param[out] tag_name +/// The returned tag name. This string handle will be valid +/// until another call to ::HAPI_GetParmTagName(). +/// +HAPI_DECL HAPI_GetParmTagName( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_ParmId parm_id, + int tag_index, + HAPI_StringHandle * tag_name ); + +/// @brief Get the tag value on a parameter given the tag name. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_id +/// The parm id. +/// +/// @param[in] tag_name +/// The tag name, either known or returned by +/// ::HAPI_GetParmTagName(). +/// +/// @param[out] tag_value +/// The returned tag value. This string handle will be valid +/// until another call to ::HAPI_GetParmTagValue(). +/// +HAPI_DECL HAPI_GetParmTagValue( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_ParmId parm_id, + const char * tag_name, + HAPI_StringHandle * tag_value ); + +/// @brief See if a parameter has a specific tag. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_id +/// The parm id. +/// +/// @param[in] tag_name +/// The tag name to look for. +/// +/// @param[out] has_tag +/// True if the tag exists on the parameter, false otherwise. +/// +HAPI_DECL HAPI_ParmHasTag( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_ParmId parm_id, + const char * tag_name, + HAPI_Bool * has_tag ); + +/// @brief See if a parameter has an expression +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The parm name. +/// +/// @param[in] index +/// The parm index. +/// +/// @param[out] has_expression +/// True if an expression exists on the parameter, false otherwise. +/// +HAPI_DECL HAPI_ParmHasExpression( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name, + int index, + HAPI_Bool * has_expression ); + +/// @brief Get the first parm with a specific, ideally unique, tag on it. +/// This is particularly useful for getting the ogl parameters on a +/// material node. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] tag_name +/// The tag name to look for. +/// +/// @param[out] parm_id +/// The returned parm id. This will be -1 if no parm was found +/// with this tag. +/// +HAPI_DECL HAPI_GetParmWithTag( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * tag_name, + HAPI_ParmId * parm_id ); + +/// @brief Get single integer or float parm expression by name +/// or Null string if no expression is present +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The parm name. +/// +/// @param[in] index +/// Index within the parameter's values tuple. +/// +/// @param[out] value +/// The returned string value. +/// +HAPI_DECL HAPI_GetParmExpression( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name, + int index, + HAPI_StringHandle * value ); + +/// @brief Revert single parm by name to default +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The parm name. +/// +/// @param[in] index +/// Index within the parameter's values tuple. +/// +HAPI_DECL HAPI_RevertParmToDefault( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name, + int index ); + +/// @brief Revert all instances of the parm by name to defaults +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The parm name. +/// +HAPI_DECL HAPI_RevertParmToDefaults( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name ); + +/// @brief Set (push) an expression string. We can only set a single value at +/// a time because we want to avoid fixed size string buffers. +/// +/// @note Regardless of the value, when calling this function +/// on a parameter, if that parameter has a callback function +/// attached to it, that callback function will be called. For +/// example, if the parameter is a button the button will be +/// pressed. +/// +/// @note In threaded mode, this is an _async call_! +/// +/// This API will invoke the cooking thread if threading is +/// enabled. This means it will return immediately. Use +/// the status and cooking count APIs under DIAGNOSTICS to get +/// a sense of the progress. All other API calls will block +/// until the cook operation has finished. +/// +/// Also note that the cook result won't be of type +/// ::HAPI_STATUS_CALL_RESULT like all calls (including this one). +/// Whenever the threading cook is done it will fill the +/// @a cook result which is queried using +/// ::HAPI_STATUS_COOK_RESULT. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] value +/// The expression string. +/// +/// @param[in] parm_id +/// Parameter id of the parameter being updated. +/// +/// @param[in] index +/// Index within the parameter's values tuple. +/// +HAPI_DECL HAPI_SetParmExpression( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * value, + HAPI_ParmId parm_id, int index ); + +/// @brief Remove the expression string, leaving the value of the +/// parm at the current value of the expression +/// +/// @note Regardless of the value, when calling this function +/// on a parameter, if that parameter has a callback function +/// attached to it, that callback function will be called. For +/// example, if the parameter is a button the button will be +/// pressed. +/// +/// @note In threaded mode, this is an _async call_! +/// +/// This API will invoke the cooking thread if threading is +/// enabled. This means it will return immediately. Use +/// the status and cooking count APIs under DIAGNOSTICS to get +/// a sense of the progress. All other API calls will block +/// until the cook operation has finished. +/// +/// Also note that the cook result won't be of type +/// ::HAPI_STATUS_CALL_RESULT like all calls (including this one). +/// Whenever the threading cook is done it will fill the +/// @a cook result which is queried using +/// ::HAPI_STATUS_COOK_RESULT. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_id +/// Parameter id of the parameter being updated. +/// +/// @param[in] index +/// Index within the parameter's values tuple. +/// +HAPI_DECL HAPI_RemoveParmExpression( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_ParmId parm_id, int index ); + +/// @brief Get single parm int value by name. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The parm name. +/// +/// @param[in] index +/// Index within the parameter's values tuple. +/// +/// @param[out] value +/// The returned int value. +/// +HAPI_DECL HAPI_GetParmIntValue( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name, + int index, + int * value ); + +/// @brief Fill an array of parameter int values. This is more efficient +/// than calling ::HAPI_GetParmIntValue() individually for each +/// parameter value. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] values_array +/// Array of ints at least the size of length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_NodeInfo::parmIntValueCount - 1. +/// +/// +/// +/// +/// @param[in] length +/// Must be at least 1 and at most +/// ::HAPI_NodeInfo::parmIntValueCount - start. +/// +/// +/// +/// +HAPI_DECL HAPI_GetParmIntValues( const HAPI_Session * session, + HAPI_NodeId node_id, + int * values_array, + int start, int length ); + +/// @brief Get single parm float value by name. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The parm name. +/// +/// @param[in] index +/// Index within the parameter's values tuple. +/// +/// @param[out] value +/// The returned float value. +/// +HAPI_DECL HAPI_GetParmFloatValue( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name, + int index, + float * value ); + +/// @brief Fill an array of parameter float values. This is more efficient +/// than calling ::HAPI_GetParmFloatValue() individually for each +/// parameter value. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] values_array +/// Array of floats at least the size of length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_NodeInfo::parmFloatValueCount - 1. +/// +/// +/// +/// +/// @param[in] length +/// Must be at least 1 and at most +/// ::HAPI_NodeInfo::parmFloatValueCount - start. +/// +/// +/// +/// +HAPI_DECL HAPI_GetParmFloatValues( const HAPI_Session * session, + HAPI_NodeId node_id, + float * values_array, + int start, int length ); + +/// @brief Get single parm string value by name. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The name of the parameter. +/// +/// @param[in] index +/// Index within the parameter's values tuple. +/// +/// @param[in] evaluate +/// Whether or not to evaluate the string expression. +/// For example, the string "$F" would evaluate to the +/// current frame number. So, passing in evaluate = false +/// would give you back the string "$F" and passing +/// in evaluate = true would give you back "1" (assuming +/// the current frame is 1). +/// +/// +/// @param[out] value +/// The returned string value. +/// +HAPI_DECL HAPI_GetParmStringValue( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name, + int index, + HAPI_Bool evaluate, + HAPI_StringHandle * value ); + +/// @brief Fill an array of parameter string handles. These handles must +/// be used in conjunction with ::HAPI_GetString() to get the +/// actual string values. This is more efficient than calling +/// ::HAPI_GetParmStringValue() individually for each +/// parameter value. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] evaluate +/// Whether or not to evaluate the string expression. +/// For example, the string "$F" would evaluate to the +/// current frame number. So, passing in evaluate = false +/// would give you back the string "$F" and passing +/// in evaluate = true would give you back "1" (assuming +/// the current frame is 1). +/// +/// @param[out] values_array +/// Array of integers at least the size of length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_NodeInfo::parmStringValueCount - 1. +/// +/// +/// +/// +/// @param[in] length +/// Must be at least 1 and at most +/// ::HAPI_NodeInfo::parmStringValueCount - start. +/// +/// +/// +/// +HAPI_DECL HAPI_GetParmStringValues( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_Bool evaluate, + HAPI_StringHandle * values_array, + int start, int length ); + +/// @brief Get a single node id parm value of an Op Path parameter. This is +/// how you see which node is connected as an input for the current +/// node (via parameter). +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The name of the parameter. +/// +/// @param[out] value +/// The node id of the node being pointed to by the parm. +/// If there is no node found, -1 will be returned. +/// +HAPI_DECL HAPI_GetParmNodeValue( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name, + HAPI_NodeId * value ); + +/// @brief Extract a file specified by path on a parameter. This will copy +/// the file to the destination directory from wherever it might be, +/// inlcuding inside the asset definition or online. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The name of the parameter. +/// +/// @param[in] destination_directory +/// The destination directory to copy the file to. +/// +/// @param[in] destination_file_name +/// The destination file name. +/// +HAPI_DECL HAPI_GetParmFile( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name, + const char * destination_directory, + const char * destination_file_name ); + +/// @brief Fill an array of ::HAPI_ParmChoiceInfo structs with parameter +/// choice list information from the asset instance node. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] parm_choices_array +/// Array of ::HAPI_ParmChoiceInfo exactly the size of +/// length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_NodeInfo::parmChoiceCount - 1. +/// +/// +/// +/// +/// @param[in] length +/// Must be at least 1 and at most +/// ::HAPI_NodeInfo::parmChoiceCount - start. +/// +/// +/// +/// +HAPI_DECL HAPI_GetParmChoiceLists( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_ParmChoiceInfo * parm_choices_array, + int start, int length ); + +/// @brief Set single parm int value by name. +/// +/// @note Regardless of the value, when calling this function +/// on a parameter, if that parameter has a callback function +/// attached to it, that callback function will be called. For +/// example, if the parameter is a button the button will be +/// pressed. +/// +/// @note In threaded mode, this is an _async call_! +/// +/// This API will invoke the cooking thread if threading is +/// enabled. This means it will return immediately. Use +/// the status and cooking count APIs under DIAGNOSTICS to get +/// a sense of the progress. All other API calls will block +/// until the cook operation has finished. +/// +/// Also note that the cook result won't be of type +/// ::HAPI_STATUS_CALL_RESULT like all calls (including this one). +/// Whenever the threading cook is done it will fill the +/// @a cook result which is queried using +/// ::HAPI_STATUS_COOK_RESULT. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The parm name. +/// +/// @param[in] index +/// Index within the parameter's values tuple. +/// +/// @param[in] value +/// The int value. +/// +HAPI_DECL HAPI_SetParmIntValue( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name, + int index, + int value ); + +/// @brief Set (push) an array of parameter int values. +/// +/// @note Regardless of the values, when calling this function +/// on a set of parameters, if any parameter has a callback +/// function attached to it, that callback function will be called. +/// For example, if the parameter is a button the button will be +/// pressed. +/// +/// @note In threaded mode, this is an _async call_! +/// +/// This API will invoke the cooking thread if threading is +/// enabled. This means it will return immediately. Use +/// the status and cooking count APIs under DIAGNOSTICS to get +/// a sense of the progress. All other API calls will block +/// until the cook operation has finished. +/// +/// Also note that the cook result won't be of type +/// ::HAPI_STATUS_CALL_RESULT like all calls (including this one). +/// Whenever the threading cook is done it will fill the +/// @a cook result which is queried using +/// ::HAPI_STATUS_COOK_RESULT. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] values_array +/// Array of integers at least the size of length. +/// +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_NodeInfo::parmIntValueCount - 1. +/// +/// +/// +/// +/// @param[in] length +/// Must be at least 1 and at most +/// ::HAPI_NodeInfo::parmIntValueCount - start. +/// +/// +/// +/// +HAPI_DECL HAPI_SetParmIntValues( const HAPI_Session * session, + HAPI_NodeId node_id, + const int * values_array, + int start, int length ); + +/// @brief Set single parm float value by name. +/// +/// @note Regardless of the value, when calling this function +/// on a parameter, if that parameter has a callback function +/// attached to it, that callback function will be called. For +/// example, if the parameter is a button the button will be +/// pressed. +/// +/// @note In threaded mode, this is an _async call_! +/// +/// This API will invoke the cooking thread if threading is +/// enabled. This means it will return immediately. Use +/// the status and cooking count APIs under DIAGNOSTICS to get +/// a sense of the progress. All other API calls will block +/// until the cook operation has finished. +/// +/// Also note that the cook result won't be of type +/// ::HAPI_STATUS_CALL_RESULT like all calls (including this one). +/// Whenever the threading cook is done it will fill the +/// @a cook result which is queried using +/// ::HAPI_STATUS_COOK_RESULT. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The parm name. +/// +/// @param[in] index +/// Index within the parameter's values tuple. +/// +/// @param[in] value +/// The float value. +/// +HAPI_DECL HAPI_SetParmFloatValue( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name, + int index, + float value ); + +/// @brief Set (push) an array of parameter float values. +/// +/// @note Regardless of the values, when calling this function +/// on a set of parameters, if any parameter has a callback +/// function attached to it, that callback function will be called. +/// For example, if the parameter is a button the button will be +/// pressed. +/// +/// @note In threaded mode, this is an _async call_! +/// +/// This API will invoke the cooking thread if threading is +/// enabled. This means it will return immediately. Use +/// the status and cooking count APIs under DIAGNOSTICS to get +/// a sense of the progress. All other API calls will block +/// until the cook operation has finished. +/// +/// Also note that the cook result won't be of type +/// ::HAPI_STATUS_CALL_RESULT like all calls (including this one). +/// Whenever the threading cook is done it will fill the +/// @a cook result which is queried using +/// ::HAPI_STATUS_COOK_RESULT. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] values_array +/// Array of floats at least the size of length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_NodeInfo::parmFloatValueCount - 1. +/// +/// @param[in] length +/// Must be at least 1 and at most +/// ::HAPI_NodeInfo::parmFloatValueCount - start. +/// +HAPI_DECL HAPI_SetParmFloatValues( const HAPI_Session * session, + HAPI_NodeId node_id, + const float * values_array, + int start, int length ); + +/// @brief Set (push) a string value. We can only set a single value at +/// a time because we want to avoid fixed size string buffers. +/// +/// @note Regardless of the value, when calling this function +/// on a parameter, if that parameter has a callback function +/// attached to it, that callback function will be called. For +/// example, if the parameter is a button the button will be +/// pressed. +/// +/// @note In threaded mode, this is an _async call_! +/// +/// This API will invoke the cooking thread if threading is +/// enabled. This means it will return immediately. Use +/// the status and cooking count APIs under DIAGNOSTICS to get +/// a sense of the progress. All other API calls will block +/// until the cook operation has finished. +/// +/// Also note that the cook result won't be of type +/// ::HAPI_STATUS_CALL_RESULT like all calls (including this one). +/// Whenever the threading cook is done it will fill the +/// @a cook result which is queried using +/// ::HAPI_STATUS_COOK_RESULT. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] value +/// The string value. +/// +/// @param[in] parm_id +/// Parameter id of the parameter being updated. +/// +/// @param[in] index +/// Index within the parameter's values tuple. +/// +HAPI_DECL HAPI_SetParmStringValue( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * value, + HAPI_ParmId parm_id, int index ); + +/// @brief Set a node id parm value of an Op Path parameter. For example, +/// This is how you connect the geometry output of an asset to the +/// geometry input of another asset - whether the input is a parameter +/// or a node input (the top of the node). Node inputs get converted +/// top parameters in HAPI. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_name +/// The name of the parameter. +/// +/// @param[in] value +/// The node id of the node being connected. Pass -1 to +/// disconnect. +/// +HAPI_DECL HAPI_SetParmNodeValue( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * parm_name, + HAPI_NodeId value ); + +/// @brief Insert an instance of a multiparm before instance_position. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_id +/// A parm id given by a ::HAPI_ParmInfo struct that +/// has type ::HAPI_PARMTYPE_MULTIPARMLIST. +/// +/// @param[in] instance_position +/// The new instance will be inserted at this position +/// index. Do note the multiparms can start at position +/// 1 or 0. Use ::HAPI_ParmInfo::instanceStartOffset to +/// distinguish. +/// +HAPI_DECL HAPI_InsertMultiparmInstance( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_ParmId parm_id, + int instance_position ); + +/// @brief Remove the instance of a multiparm given by instance_position. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] parm_id +/// A parm id given by a ::HAPI_ParmInfo struct that +/// has type ::HAPI_PARMTYPE_MULTIPARMLIST. +/// +/// @param[in] instance_position +/// The instance at instance_position will removed. +/// +HAPI_DECL HAPI_RemoveMultiparmInstance( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_ParmId parm_id, + int instance_position ); + +// HANDLES ------------------------------------------------------------------ + +/// @brief Fill an array of ::HAPI_HandleInfo structs with information +/// about every exposed user manipulation handle on the node. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] handle_infos_array +/// Array of ::HAPI_HandleInfo at least the size of length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AssetInfo::handleCount - 1. +/// +/// +/// @param[in] length +/// Must be at least 1 and at most +/// ::HAPI_AssetInfo::handleCount - start. +/// +/// +HAPI_DECL HAPI_GetHandleInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_HandleInfo * handle_infos_array, + int start, int length ); + +/// @brief Fill an array of ::HAPI_HandleBindingInfo structs with information +/// about the binding of a particular handle on the given node. +/// +/// @ingroup Parms +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] handle_index +/// The index of the handle, from 0 to handleCount - 1 +/// from the call to ::HAPI_GetAssetInfo(). +/// +/// @param[out] handle_binding_infos_array +/// Array of ::HAPI_HandleBindingInfo at least the size +/// of length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_HandleInfo::bindingsCount - 1. +/// +/// +/// @param[in] length +/// Must be at least 1 and at most +/// ::HAPI_HandleInfo::bindingsCount - start. +/// +/// +HAPI_DECL HAPI_GetHandleBindingInfo( + const HAPI_Session * session, + HAPI_NodeId node_id, + int handle_index, + HAPI_HandleBindingInfo * handle_binding_infos_array, + int start, int length ); + +/// @defgroup Presets Presets +/// Functions for working with Node presets + +/// @brief Generate a preset blob of the current state of all the +/// parameter values, cache it, and return its size in bytes. +/// +/// @ingroup Presets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The exposed node id. +/// +/// @param[in] preset_type +/// The preset type. +/// +/// @param[in] preset_name +/// Optional. This is only used if the @p preset_type is +/// ::HAPI_PRESETTYPE_IDX. If NULL is given, the preset +/// name will be the same as the name of the node with +/// the given @p node_id. +/// +/// @param[out] buffer_length +/// Size of the buffer. +/// +HAPI_DECL HAPI_GetPresetBufLength( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PresetType preset_type, + const char * preset_name, + int * buffer_length ); + +/// @brief Generates a preset for the given asset. +/// +/// @ingroup Presets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The exposed node id. +/// +/// @param[out] buffer +/// Buffer to hold the preset data. +/// +/// @param[in] buffer_length +/// Size of the buffer. Should be the same as the length +/// returned by ::HAPI_GetPresetBufLength(). +/// +HAPI_DECL HAPI_GetPreset( const HAPI_Session * session, + HAPI_NodeId node_id, + char * buffer, + int buffer_length ); + +/// @brief Sets a particular asset to a given preset. +/// +/// @ingroup Presets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The exposed node id. +/// +/// @param[in] preset_type +/// The preset type. +/// +/// @param[in] preset_name +/// Optional. This is only used if the @p preset_type is +/// ::HAPI_PRESETTYPE_IDX. If NULL is give, the first +/// preset in the IDX file will be chosen. +/// +/// +/// @param[in] buffer +/// Buffer to hold the preset data. +/// +/// @param[in] buffer_length +/// Size of the buffer. +/// +HAPI_DECL HAPI_SetPreset( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PresetType preset_type, + const char * preset_name, + const char * buffer, + int buffer_length ); + +/// @brief Gets the number of presets in an IDX file. When this method is +/// called, the names of the presets are stored in a single internal +/// buffer from which they can be retrieved by calling +/// HAPI_GetPresetNames(). +/// +/// Note that calling HAPI_GetPresetCount() will overwrite the preset +/// names that were previously stored in the internal buffer. Therefore, +/// ensure that you have called HAPI_GetPresetNames() before calling +/// HAPI_GetPresetCount() again. +/// +/// @ingroup Presets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] buffer +/// A buffer containing the raw binary data of the .idx file. +/// +/// @param[in] buffer_length +/// Size of the buffer. +/// +/// @param[out] count +/// Number of presets in the file. +/// +HAPI_DECL HAPI_GetPresetCount( const HAPI_Session * session, + const char * buffer, + int buffer_length, + int * count ); + +/// @brief Gets the names of presets in an IDX file. HAPI_GetPresetCount() must +/// be called before calling this method. See the HAPI_GetPresetCount() +/// documentation for more information. +/// +/// @ingroup Presets +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] buffer +/// A buffer containing the raw binary data of the .idx file. +/// This should be the same buffer that was passed into +/// HAPI_GetPresetCount(). +/// +/// @param[in] buffer_length +/// Size of the buffer. +/// +/// @param[out] preset_names_array +/// Array of preset names to be filled +/// +/// @param[in] preset_names_count +/// Number of presets in the file. Should be the same as +/// the count returned by ::HAPI_GetPresetCount() +/// +HAPI_DECL HAPI_GetPresetNames( const HAPI_Session * session, + const char * buffer, + int buffer_length, + HAPI_StringHandle * preset_names_array, + int preset_names_count ); + +/// @defgroup Objects +/// Functions for working with OBJ Nodes + +/// @brief Get the object info on an OBJ node. +/// +/// @ingroup Objects +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] object_info +/// The output ::HAPI_ObjectInfo. +/// +HAPI_DECL HAPI_GetObjectInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_ObjectInfo * object_info ); + +/// @brief Get the tranform of an OBJ node. +/// +/// @ingroup Objects +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The object node id. +/// +/// @param[in] relative_to_node_id +/// The object node id for the object to which the returned +/// transform will be relative to. Pass -1 or the node_id +/// to just get the object's local transform. +/// +/// @param[in] rst_order +/// The order of application of translation, rotation and +/// scale. +/// +/// @param[out] transform +/// The output ::HAPI_Transform transform. +/// +HAPI_DECL HAPI_GetObjectTransform( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_NodeId relative_to_node_id, + HAPI_RSTOrder rst_order, + HAPI_Transform * transform ); + +/// @brief Compose a list of child object nodes given a parent node id. +/// +/// Use the @c object_count returned by this function to get the +/// ::HAPI_ObjectInfo structs for each child object using +/// ::HAPI_GetComposedObjectList(). +/// +/// Note, if not using the @c categories arg, this is equivalent to: +/// @code +/// HAPI_ComposeChildNodeList( +/// session, parent_node_id, +/// HAPI_NODETYPE_OBJ, +/// HAPI_NODEFLAGS_OBJ_GEOMETRY, +/// true, &object_count ); +/// @endcode +/// +/// @ingroup Objects +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] parent_node_id +/// The parent node id. +/// +/// @param[in] categories +/// (Optional) Lets you filter object nodes by their render +/// categories. This is a standard OBJ parameter, usually +/// under the Render > Shading tab. If an OBJ node does not +/// have this parameter, one can always add it as a spare. +/// +/// The value of this string argument should be NULL if not +/// used or a space-separated list of category names. +/// Multiple category names will be treated as an AND op. +/// +/// +/// @param[out] object_count +/// The number of object nodes currently under the parent. +/// Use this count with a call to +/// ::HAPI_GetComposedObjectList() to get the object infos. +/// +HAPI_DECL HAPI_ComposeObjectList( const HAPI_Session * session, + HAPI_NodeId parent_node_id, + const char * categories, + int * object_count ); + +/// @brief Fill an array of ::HAPI_ObjectInfo structs. +/// +/// This is best used with ::HAPI_ComposeObjectList() with. +/// +/// @ingroup Objects +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] parent_node_id +/// The parent node id. +/// +/// @param[out] object_infos_array +/// Array of ::HAPI_ObjectInfo at least the size of +/// @c length. +/// +/// @param[in] start +/// At least @c 0 and at most @c object_count returned by +/// ::HAPI_ComposeObjectList(). +/// +/// +/// @param[in] length +/// Given @c object_count returned by +/// ::HAPI_ComposeObjectList(), @c length should be at least +/// @c 0 and at most object_count - start. +/// +/// +HAPI_DECL HAPI_GetComposedObjectList( const HAPI_Session * session, + HAPI_NodeId parent_node_id, + HAPI_ObjectInfo * object_infos_array, + int start, int length ); + +/// @brief Fill an array of ::HAPI_Transform structs. +/// +/// This is best used with ::HAPI_ComposeObjectList() with. +/// +/// Note that these transforms will be relative to the +/// @c parent_node_id originally given to ::HAPI_ComposeObjectList() +/// and expected to be the same with this call. If @c parent_node_id +/// is not an OBJ node, the transforms will be given as they are on +/// the object node itself. +/// +/// @ingroup Objects +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] parent_node_id +/// The parent node id. The object transforms will be +/// relative to this node unless this node is not an OBJ. +/// +/// @param[in] rst_order +/// The order of application of translation, rotation and +/// scale. +/// +/// @param[out] transform_array +/// Array of ::HAPI_Transform at least the size of +/// length. +/// +/// @param[in] start +/// At least @c 0 and at most @c object_count returned by +/// ::HAPI_ComposeObjectList(). +/// +/// +/// @param[in] length +/// Given @c object_count returned by +/// ::HAPI_ComposeObjectList(), @c length should be at least +/// @c 0 and at most object_count - start. +/// +/// +HAPI_DECL HAPI_GetComposedObjectTransforms( const HAPI_Session * session, + HAPI_NodeId parent_node_id, + HAPI_RSTOrder rst_order, + HAPI_Transform * transform_array, + int start, int length ); + +/// @brief Get the node ids for the objects being instanced by an +/// Instance OBJ node. +/// +/// @ingroup Objects +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] object_node_id +/// The object node id. +/// +/// @param[out] instanced_node_id_array +/// Array of ::HAPI_NodeId at least the size of length. +/// +/// @param[in] start +/// At least @c 0 and at most @c object_count returned by +/// ::HAPI_ComposeObjectList(). +/// +/// +/// @param[in] length +/// Given @c object_count returned by +/// ::HAPI_ComposeObjectList(), @c length should be at least +/// @c 0 and at most object_count - start. +/// +/// +HAPI_DECL HAPI_GetInstancedObjectIds( const HAPI_Session * session, + HAPI_NodeId object_node_id, + HAPI_NodeId * instanced_node_id_array, + int start, int length ); + +/// @brief Fill an array of ::HAPI_Transform structs with the transforms +/// of each instance of this instancer object for a given part. +/// +/// @ingroup Objects +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The object node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] rst_order +/// The order of application of translation, rotation and +/// scale. +/// +/// @param[out] transforms_array +/// Array of ::HAPI_Transform at least the size of length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_PartInfo::pointCount - 1. This is the 0th +/// part of the display geo of the instancer object node. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_PartInfo::pointCount - @p start. This is the 0th +/// part of the display geo of the instancer object node. +/// +/// +HAPI_DECL HAPI_GetInstanceTransformsOnPart( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_RSTOrder rst_order, + HAPI_Transform * transforms_array, + int start, int length ); + +/// @brief Set the transform of an individual object. Note that the object +/// nodes have to either be editable or have their transform +/// parameters exposed at the asset level. This won't work otherwise. +/// +/// @ingroup Objects +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The object node id. +/// +/// @param[in] trans +/// A ::HAPI_TransformEuler that stores the transform. +/// +HAPI_DECL HAPI_SetObjectTransform( const HAPI_Session * session, + HAPI_NodeId node_id, + const HAPI_TransformEuler * trans ); + +/// @defgroup GeometryGetters Geometry Getters +/// Functions for reading Geometry (SOP) data + +/// @brief Get the display geo (SOP) node inside an Object node. If there +/// there are multiple display SOP nodes, only the first one is +/// returned. If the node is a display SOP itself, even if a network, +/// it will return its own geo info. If the node is a SOP but not +/// a network and not the display SOP, this function will fail. +/// +/// The above implies that you can safely call this function on both +/// OBJ and SOP asset nodes and get the same (equivalent) geometry +/// display node back. SOP asset nodes will simply return themselves. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] object_node_id +/// The object node id. +/// +/// @param[out] geo_info +/// ::HAPI_GeoInfo return value. +/// +HAPI_DECL HAPI_GetDisplayGeoInfo( const HAPI_Session * session, + HAPI_NodeId object_node_id, + HAPI_GeoInfo * geo_info ); + +/// @brief A helper method that gets the number of main geometry outputs inside +/// an Object node or SOP node. If the node is an Object node, this +/// method will return the cumulative number of geometry outputs for all +/// geometry nodes that it contains. When searching for output geometry, +/// this method will only consider subnetworks that have their display +/// flag enabled. +/// +/// This method must be called before HAPI_GetOutputGeoInfos() is +/// called. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id of the Object or SOP node to get the geometry +/// output count of. +/// +/// @param[out] count +/// The number of geometry (SOP) outputs. +HAPI_DECL HAPI_GetOutputGeoCount( const HAPI_Session* session, + HAPI_NodeId node_id, + int* count); + +/// @brief Gets the geometry info structs (::HAPI_GeoInfo) for a node's +/// main geometry outputs. This method can only be called after +/// HAPI_GetOutputGeoCount() has been called with the same node id. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id of the Object or SOP node to get the output +/// geometry info structs (::HAPI_GeoInfo) for. +/// +/// @param[out] geo_infos_array +/// Output array where the output geometry info structs will be +/// stored. The size of the array must match the count argument +/// returned by the HAPI_GetOutputGeoCount() method. +/// +/// @param[in] count +/// Sanity check count. The count must be equal to the count +/// returned by the HAPI_GetOutputGeoCount() method. +HAPI_DECL HAPI_GetOutputGeoInfos( const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_GeoInfo* geo_infos_array, + int count ); + +/// @brief Get the geometry info struct (::HAPI_GeoInfo) on a SOP node. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] geo_info +/// ::HAPI_GeoInfo return value. +/// +HAPI_DECL HAPI_GetGeoInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_GeoInfo * geo_info ); + +/// @brief Get a particular part info struct. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] part_info +/// ::HAPI_PartInfo return value. +/// +HAPI_DECL HAPI_GetPartInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_PartInfo * part_info ); + + +/// @brief Gets the number of edges that belong to an edge group on a geometry +/// part. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] group_name +/// The name of the edge group. +/// +/// @param[out] edge_count +/// The number of edges that belong to the group. +/// +HAPI_DECL HAPI_GetEdgeCountOfEdgeGroup( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * group_name, + int * edge_count ); + +/// @brief Get the array of faces where the nth integer in the array is +/// the number of vertices the nth face has. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] face_counts_array +/// An integer array at least the size of length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_PartInfo::faceCount - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_PartInfo::faceCount - @p start. +/// +/// +HAPI_DECL HAPI_GetFaceCounts( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + int * face_counts_array, + int start, int length ); + +/// @brief Get array containing the vertex-point associations where the +/// ith element in the array is the point index the ith vertex +/// associates with. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] vertex_list_array +/// An integer array at least the size of length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_PartInfo::vertexCount - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_PartInfo::vertexCount - @p start. +/// +/// +HAPI_DECL HAPI_GetVertexList( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + int * vertex_list_array, + int start, int length ); + +/// @defgroup Attributes +/// Functions for working with attributes. + +/// @brief Get the attribute info struct for the attribute specified by name. +/// +/// @ingroup Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] owner +/// Attribute owner. +/// +/// @param[out] attr_info +/// ::HAPI_AttributeInfo to be filled. Check +/// ::HAPI_AttributeInfo::exists to see if this attribute +/// exists. +/// +HAPI_DECL HAPI_GetAttributeInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeOwner owner, + HAPI_AttributeInfo * attr_info ); + +/// @brief Get list of attribute names by attribute owner. Note that the +/// name string handles are only valid until the next time this +/// function is called. +/// +/// @ingroup Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] owner +/// The ::HAPI_AttributeOwner enum value specifying the +/// owner of the attribute. +/// +/// @param[out] attribute_names_array +/// Array of ints (string handles) to house the +/// attribute names. Should be exactly the size of the +/// appropriate attribute owner type count +/// in ::HAPI_PartInfo. +/// +/// @param[in] count +/// Sanity check count. Must be equal to the appropriate +/// attribute owner type count in ::HAPI_PartInfo. +/// +HAPI_DECL HAPI_GetAttributeNames( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_AttributeOwner owner, + HAPI_StringHandle * attribute_names_array, + int count ); + +/// @brief Get attribute integer data. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeIntData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + int * data_array, + int start, int length ); + +/// @brief Get array attribute integer data. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeIntArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length ); + +/// @brief Get attribute unsigned 8-bit integer data. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An unsigned 8-bit integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeUInt8Data( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + HAPI_UInt8 * data_array, + int start, int length ); + +/// @brief Get array attribute unsigned 8-bit integer data. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An unsigned 8-bit integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeUInt8ArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + HAPI_UInt8 * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length ); + +/// @brief Get attribute 8-bit integer data. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An 8-bit integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeInt8Data( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + HAPI_Int8 * data_array, + int start, int length ); + +/// @brief Get array attribute 8-bit integer data. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An 8-bit integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeInt8ArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + HAPI_Int8 * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length ); + +/// @brief Get attribute 16-bit integer data. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An 16-bit integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeInt16Data( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + HAPI_Int16 * data_array, + int start, int length ); + +/// @brief Get array attribute 16-bit integer data. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An 16-bit integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeInt16ArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + HAPI_Int16 * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length ); + +/// @brief Get attribute 64-bit integer data. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An 64-bit integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeInt64Data( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + HAPI_Int64 * data_array, + int start, int length ); + +/// @brief Get array attribute 64-bit integer data. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An 64-bit integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeInt64ArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + HAPI_Int64 * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length ); + +/// @brief Get attribute float data. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An float array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeFloatData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + float * data_array, + int start, int length ); + +/// @brief Get array attribute float data. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An float array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeFloatArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + float * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length ); + +/// @brief Get 64-bit attribute float data. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An 64-bit float array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeFloat64Data( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + double * data_array, + int start, int length ); + +/// @brief Get array attribute 64-bit float data. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for the. +/// totalArrayElements. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An 64-bit float array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +HAPI_DECL HAPI_GetAttributeFloat64ArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + double * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length ); + +/// @brief Get attribute string data. Note that the string handles +/// returned are only valid until the next time this function +/// is called. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_array +/// An ::HAPI_StringHandle array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @warning The string handles should be used to retrieve the strings +/// immediately and are invalidated when another call to get +/// this attribute's data is made. +/// +HAPI_DECL HAPI_GetAttributeStringData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + HAPI_StringHandle * data_array, + int start, int length ); + +/// @brief Get array attribute string data. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// Note that the string handles returned are only valid until +/// the next time this function is called. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for the. +/// totalArrayElements. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An ::HAPI_StringHandle array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @warning The string handles should be used to retrieve the strings +/// immediately and are invalidated when another call to get +/// this attribute's data is made. +/// +HAPI_DECL HAPI_GetAttributeStringArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + HAPI_StringHandle * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length ); + +/// @brief Get attribute dictionary data. +/// +/// Dictionary data is serialized as JSON-encoded strings. +/// Note that the string handles returned are only valid until the next +/// time this function is called. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_array +/// An ::HAPI_StringHandle array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @warning The string handles should be used to retrieve the strings +/// immediately and are invalidated when another call to get +/// this attribute's data is made. +/// +HAPI_DECL HAPI_GetAttributeDictionaryData( const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + HAPI_AttributeInfo* attr_info, + HAPI_StringHandle* data_array, + int start, + int length ); + +/// @brief Get array attribute dictionary data. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// Dictionary data is serialized as JSON-encoded strings. +/// Note that the string handles returned are only valid until +/// the next time this function is called. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for the. +/// totalArrayElements. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An ::HAPI_StringHandle array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @warning The string handles should be used to retrieve the strings +/// immediately and are invalidated when another call to get +/// this attribute's data is made. +/// +HAPI_DECL HAPI_GetAttributeDictionaryArrayData( const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + HAPI_AttributeInfo* attr_info, + HAPI_StringHandle* data_fixed_array, + int data_fixed_length, + int* sizes_fixed_array, + int start, + int sizes_fixed_length ); + + +/// @brief Get attribute integer data asynchronously. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeIntDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + int * data_array, + int start, int length, + int * job_id ); + + +/// @brief Get attribute unsigned 8-bit integer data asynchronously. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeUInt8DataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + HAPI_UInt8 * data_array, + int start, int length, + int * job_id ); + + +/// @brief Get attribute 8-bit integer data asynchronously. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeInt8DataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + HAPI_Int8 * data_array, + int start, int length, + int * job_id ); + +/// @brief Get attribute 16-bit integer data asynchronously. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeInt16DataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + HAPI_Int16 * data_array, + int start, int length, + int * job_id ); + +/// @brief Get attribute 64-bit integer data asynchronously. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeInt64DataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + HAPI_Int64 * data_array, + int start, int length, + int * job_id ); + + +/// @brief Get attribute float data asynchronously. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeFloatDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + float * data_array, + int start, int length, + int * job_id ); + +/// @brief Get attribute 64-bit float data asynchronously. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] stride +/// Specifies how many items to skip over for each element. +/// With a stride of -1, the stride will be set to +/// @c attr_info->tuple_size. Otherwise, the stride will be +/// set to the maximum of @c attr_info->tuple_size and +/// @c stride. +/// +/// @param[out] data_array +/// An integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeFloat64DataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + int stride, + double * data_array, + int start, int length, + int * job_id ); + +/// @brief Get attribute string data asynchronously. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_array +/// An integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +/// @warning The string handles should be used to retrieve the strings +/// immediately and are invalidated when another call to get +/// this attribute's data is made. +/// +HAPI_DECL HAPI_GetAttributeStringDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + HAPI_StringHandle * data_array, + int start, int length, + int * job_id ); + +/// @brief Get attribute dictionary data asynchronously. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_array +/// An integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +/// @warning The string handles should be used to retrieve the strings +/// immediately and are invalidated when another call to get +/// this attribute's data is made. +/// +HAPI_DECL HAPI_GetAttributeDictionaryDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + HAPI_AttributeInfo * attr_info, + HAPI_StringHandle * data_array, + int start, int length, + int * job_id ); + +/// @brief Get array attribute integer data asynchronously. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] attr_name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeIntArrayDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * attr_name, + HAPI_AttributeInfo * attr_info, + int * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length, + int * job_id ); + +/// @brief Get array attribute unsigned 8-bit integer data asynchronously. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] attr_name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeUInt8ArrayDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * attr_name, + HAPI_AttributeInfo * attr_info, + HAPI_UInt8 * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length, + int * job_id ); + +/// @brief Get array attribute 8-bit integer data asynchronously. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] attr_name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeInt8ArrayDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * attr_name, + HAPI_AttributeInfo * attr_info, + HAPI_Int8 * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length, + int * job_id ); + +/// @brief Get array attribute 16-bit integer data asynchronously. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] attr_name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeInt16ArrayDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * attr_name, + HAPI_AttributeInfo * attr_info, + HAPI_Int16 * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length, + int * job_id ); + +/// @brief Get array attribute 64-bit integer data asynchronously. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] attr_name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeInt64ArrayDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * attr_name, + HAPI_AttributeInfo * attr_info, + HAPI_Int64 * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length, + int * job_id ); + +/// @brief Get array attribute float data asynchronously. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] attr_name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeFloatArrayDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * attr_name, + HAPI_AttributeInfo * attr_info, + float * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length, + int * job_id ); + +/// @brief Get array attribute 64-bit float data asynchronously. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] attr_name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_GetAttributeFloat64ArrayDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * attr_name, + HAPI_AttributeInfo * attr_info, + double * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length, + int * job_id ); + +/// @brief Get array attribute string data asynchronously. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] attr_name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +/// @warning The string handles should be used to retrieve the strings +/// immediately and are invalidated when another call to get +/// this attribute's data is made. +/// +HAPI_DECL HAPI_GetAttributeStringArrayDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * attr_name, + HAPI_AttributeInfo * attr_info, + HAPI_StringHandle * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length, + int * job_id ); + +/// @brief Get array attribute dictionary data asynchronously. +/// Each entry in an array attribute can have varying array lengths. +/// Therefore the array values are returned as a flat array, with +/// another sizes array containing the lengths of each array entry. +/// +/// @ingroup GeometryGetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] attr_name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[out] data_fixed_array +/// An integer array at least the size of +/// ::HAPI_AttributeInfo::totalArrayElements. +/// +/// @param[in] data_fixed_length +/// Must be ::HAPI_AttributeInfo::totalArrayElements. +/// +/// +/// @param[out] sizes_fixed_array +/// An integer array at least the size of +/// sizes_fixed_length to hold the size of each entry. +/// +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// Note, if 0 is passed for length, the function will just +/// do nothing and return ::HAPI_RESULT_SUCCESS. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +/// @warning The string handles should be used to retrieve the strings +/// immediately and are invalidated when another call to get +/// this attribute's data is made. +/// +HAPI_DECL HAPI_GetAttributeDictionaryArrayDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * attr_name, + HAPI_AttributeInfo * attr_info, + HAPI_StringHandle * data_fixed_array, + int data_fixed_length, + int * sizes_fixed_array, + int start, int sizes_fixed_length, + int * job_id ); + +/// @brief Get status of a job. +/// +/// @ingroup Status +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] job_id +/// The id assigned to the job. It must equal to the index +/// assigned through a previous call to an asynchronous HAPI +/// method. +/// +/// @param[out] job_status +/// The current status of the job. One of ::HAPI_JobStatus. +/// +HAPI_DECL HAPI_GetJobStatus( const HAPI_Session * session, + int job_id, + HAPI_JobStatus * job_status ); + +/// @brief Get group names for an entire geo. Please note that this +/// function is NOT per-part, but it is per-geo. The companion +/// function ::HAPI_GetGroupMembership() IS per-part. Also keep +/// in mind that the name string handles are only +/// valid until the next time this function is called. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] group_type +/// The group type. +/// +/// @param[out] group_names_array +/// The array of names to be filled. Should be the size +/// given by ::HAPI_GeoInfo_GetGroupCountByType() with +/// @p group_type and the ::HAPI_GeoInfo of @p geo_id. +/// @note These string handles are only valid until the +/// next call to ::HAPI_GetGroupNames(). +/// +/// @param[in] group_count +/// Sanity check. Should be less than or equal to the size +/// of @p group_names. +/// +HAPI_DECL HAPI_GetGroupNames( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_GroupType group_type, + HAPI_StringHandle * group_names_array, + int group_count ); + +/// @brief Get group membership. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] group_type +/// The group type. +/// +/// @param[in] group_name +/// The group name. +/// +/// @param[out] membership_array_all_equal +/// (optional) Quick way to determine if all items are in +/// the given group or all items our not in the group. +/// If you do not need this information or you are getting edge +/// group information, you can just pass NULL for this argument. +/// +/// @param[out] membership_array +/// Array of ints that represent the membership of this +/// group. When getting membership information for a point or +/// primitive group, the size of the array should be the size +/// given by ::HAPI_PartInfo_GetElementCountByGroupType() with +/// @p group_type and the ::HAPI_PartInfo of @p part_id. When +/// retrieving the edges belonging to an edge group, the +/// membership array will be filled with point numbers that +/// comprise the edges of the edge group. Each edge is specified +/// by two points, which means that the size of the array should +/// be the size given by ::HAPI_GetEdgeCountOfEdgeGroup() * 2. +/// +/// @param[in] start +/// Start offset into the membership array. Must be +/// less than ::HAPI_PartInfo_GetElementCountByGroupType() when +/// it is a point or primitive group. When getting the +/// membership information for an edge group, this argument must +/// be less than the size returned by +/// ::HAPI_GetEdgeCountOfEdgeGroup() * 2 - 1. +/// +/// +/// @param[in] length +/// Should be less than or equal to the size +/// of @p membership_array. +/// +HAPI_DECL HAPI_GetGroupMembership( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_GroupType group_type, + const char * group_name, + HAPI_Bool * membership_array_all_equal, + int * membership_array, + int start, int length ); + +/// @brief Get group counts for a specific packed instanced part. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. (should be a packed primitive) +/// +/// @param[out] pointGroupCount +/// Number of point groups on the packed instance part. +/// Will be set to -1 if the part is not a valid packed part. +/// +/// @param[out] primitiveGroupCount +/// Number of primitive groups on the instanced part. +/// Will be set to -1 if the part is not a valid instancer +/// +HAPI_DECL HAPI_GetGroupCountOnPackedInstancePart( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + int * pointGroupCount, + int * primitiveGroupCount ); + +/// @brief Get the group names for a packed instance part +/// This functions allows you to get the group name for a specific +/// packed primitive part. +/// Keep in mind that the name string handles are only +/// valid until the next time this function is called. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. (should be a packed primitive) +/// +/// @param[in] group_type +/// The group type. +/// +/// @param[out] group_names_array +/// The array of names to be filled. Should be the size +/// given by ::HAPI_GetGroupCountOnPackedInstancePart() with +/// @p group_type and the ::HAPI_PartInfo of @p part_id. +/// @note These string handles are only valid until the +/// next call to ::HAPI_GetGroupNamesOnPackedInstancePart(). +/// +/// @param[in] group_count +/// Sanity check. Should be less than or equal to the size +/// of @p group_names. +/// +HAPI_DECL HAPI_GetGroupNamesOnPackedInstancePart( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_GroupType group_type, + HAPI_StringHandle * group_names_array, + int group_count ); + +/// @brief Get group membership for a packed instance part +/// This functions allows you to get the group membership for a specific +/// packed primitive part. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. (should be a packed primitive) +/// +/// @param[in] group_type +/// The group type. +/// +/// @param[in] group_name +/// The group name. +/// +/// @param[out] membership_array_all_equal +/// (optional) Quick way to determine if all items are in +/// the given group or all items our not in the group. +/// You can just pass NULL here if not interested. +/// +/// @param[out] membership_array +/// Array of ints that represent the membership of this +/// group. Should be the size given by +/// ::HAPI_PartInfo_GetElementCountByGroupType() with +/// @p group_type and the ::HAPI_PartInfo of @p part_id. +/// +/// @param[in] start +/// Start offset into the membership array. Must be +/// less than ::HAPI_PartInfo_GetElementCountByGroupType(). +/// +/// +/// @param[in] length +/// Should be less than or equal to the size +/// of @p membership_array. +/// +/// +HAPI_DECL HAPI_GetGroupMembershipOnPackedInstancePart( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_GroupType group_type, + const char * group_name, + HAPI_Bool * membership_array_all_equal, + int * membership_array, + int start, int length ); + +/// @brief Get the part ids that this instancer part is instancing. +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The instancer part id. +/// +/// @param[out] instanced_parts_array +/// Array of ::HAPI_PartId's to instance. +/// +/// @param[in] start +/// Should be less than @p part_id's +/// ::HAPI_PartInfo::instancedPartCount but more than or +/// equal to 0. +/// +/// +/// @param[in] length +/// Should be less than @p part_id's +/// ::HAPI_PartInfo::instancedPartCount - @p start. +/// +/// +HAPI_DECL HAPI_GetInstancedPartIds( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_PartId * instanced_parts_array, + int start, int length ); + +/// @brief Get the instancer part's list of transforms on which to +/// instance the instanced parts you got from +/// ::HAPI_GetInstancedPartIds(). +/// +/// @ingroup GeometryGetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The instancer part id. +/// +/// @param[in] rst_order +/// The order of application of translation, rotation and +/// scale. +/// +/// @param[out] transforms_array +/// Array of ::HAPI_PartId's to instance. +/// +/// @param[in] start +/// Should be less than @p part_id's +/// ::HAPI_PartInfo::instanceCount but more than or +/// equal to 0. +/// +/// +/// @param[in] length +/// Should be less than @p part_id's +/// ::HAPI_PartInfo::instanceCount - @p start. +/// +/// +HAPI_DECL HAPI_GetInstancerPartTransforms( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_RSTOrder rst_order, + HAPI_Transform * transforms_array, + int start, int length ); + +/// @defgroup GeometrySetters Geometry Setters +/// Functions for setting geometry (SOP) data + +/// @brief Set the main part info struct (::HAPI_PartInfo). +/// +/// @ingroup GeometrySetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// +/// @param[in] part_info +/// ::HAPI_PartInfo value that describes the input +/// geometry. +/// +HAPI_DECL HAPI_SetPartInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const HAPI_PartInfo * part_info ); + +/// @brief Set the array of faces where the nth integer in the array is +/// the number of vertices the nth face has. +/// +/// @ingroup GeometrySetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// +/// @param[in] face_counts_array +/// An integer array at least the size of @p length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_PartInfo::faceCount - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_PartInfo::faceCount - @p start. +/// +/// +HAPI_DECL HAPI_SetFaceCounts( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const int * face_counts_array, + int start, int length ); + +/// @brief Set array containing the vertex-point associations where the +/// ith element in the array is the point index the ith vertex +/// associates with. +/// +/// @ingroup GeometrySetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] vertex_list_array +/// An integer array at least the size of length. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_PartInfo::vertexCount - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_PartInfo::vertexCount - @p start. +/// +/// +HAPI_DECL HAPI_SetVertexList( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const int * vertex_list_array, + int start, int length ); + +/// @brief Add an attribute. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo stores attribute properties. +/// +HAPI_DECL HAPI_AddAttribute( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info ); + +/// @brief Delete an attribute from an input geo +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo stores attribute properties. +/// +HAPI_DECL HAPI_DeleteAttribute( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info ); + +/// @brief Set attribute integer data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +HAPI_DECL HAPI_SetAttributeIntData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const int * data_array, + int start, int length ); + +/// @brief Set unsigned 8-bit attribute integer data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An unsigned 8-bit integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +HAPI_DECL HAPI_SetAttributeUInt8Data( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_UInt8 * data_array, + int start, int length ); + +/// @brief Set 8-bit attribute integer data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An 8-bit integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +HAPI_DECL HAPI_SetAttributeInt8Data( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_Int8 * data_array, + int start, int length ); + +/// @brief Set 16-bit attribute integer data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An 16-bit integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +HAPI_DECL HAPI_SetAttributeInt16Data( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_Int16 * data_array, + int start, int length ); + +/// @brief Set 64-bit attribute integer data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An 64-bit integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +HAPI_DECL HAPI_SetAttributeInt64Data( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_Int64 * data_array, + int start, int length ); + +/// @brief Set attribute float data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An float array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +HAPI_DECL HAPI_SetAttributeFloatData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const float * data_array, + int start, int length ); + +/// @brief Set 64-bit attribute float data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An 64-bit float array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +HAPI_DECL HAPI_SetAttributeFloat64Data( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const double * data_array, + int start, int length ); + +/// @brief Set attribute string data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An ::HAPI_StringHandle array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +HAPI_DECL HAPI_SetAttributeStringData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const char ** data_array, + int start, int length ); + +/// @brief Set attribute string data by index. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] string_array +/// An array of strings at least the size of +/// string_count/tt>. +/// +/// @param[in] string_count +/// Number of strings that are indexed. +/// +/// @param[in] indices_array +/// integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. Each +/// entry indexes string_array. +/// +/// @param[in] indices_start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] indices_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +HAPI_DECL HAPI_SetAttributeIndexedStringData( const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const char** string_array, + int string_count, + const int* indices_array, + int indices_start, + int indices_length); + +/// @brief Set multiple attribute string data to the same unique value. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A string +/// +/// @param[in] data_length +/// Must be the length of string data. +/// +/// @param[in] start_index +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +HAPI_DECL HAPI_SetAttributeStringUniqueData( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const char* data_array, + int data_length, + int start_index, + int num_indices); + +/// @brief Set multiple attribute int data to the same unique value. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A integer array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +HAPI_DECL HAPI_SetAttributeIntUniqueData( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const int* data_array, + int data_length, + int start_index, + int num_indices); + +/// @brief Set multiple attribute unsigned 8-bit int data to the same unique value. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A integer array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +HAPI_DECL HAPI_SetAttributeUInt8UniqueData( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const HAPI_UInt8* data_array, + int data_length, + int start_index, + int num_indices); + +/// @brief Set multiple attribute 8-bit int data to the same unique value. +/// +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A integer array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +HAPI_DECL HAPI_SetAttributeInt8UniqueData( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const HAPI_Int8* data_array, + int data_length, + int start_index, + int num_indices); + +/// @brief Set multiple attribute 16-bit int data to the same unique value. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A integer array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +HAPI_DECL HAPI_SetAttributeInt16UniqueData( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const HAPI_Int16* data_array, + int data_length, + int start_index, + int num_indices); + +/// @brief Set multiple attribute 64-bit int data to the same unique value. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A integer array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +HAPI_DECL HAPI_SetAttributeInt64UniqueData( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const HAPI_Int64* data_array, + int data_length, + int start_index, + int num_indices); + +/// @brief Set multiple attribute float data to the same unique value. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A floating point array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +HAPI_DECL HAPI_SetAttributeFloatUniqueData( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const float* data_array, + int data_length, + int start_index, + int num_indices); + +/// @brief Set multiple attribute 64-bit float data to the same unique value. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A floating point array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +HAPI_DECL HAPI_SetAttributeFloat64UniqueData( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const double* data_array, + int data_length, + int start_index, + int num_indices); + +/// @brief Set attribute dictionary data. The dictionary data should +/// be provided as JSON-encoded strings. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An ::HAPI_StringHandle array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +HAPI_DECL HAPI_SetAttributeDictionaryData( const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const char** data_array, + int start, int length ); + +/// @brief Set integer array attribute data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the int values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +HAPI_DECL HAPI_SetAttributeIntArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const int * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length ); + +/// @brief Set unsigned 8-bit integer array attribute data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the HAPI_UInt8 values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +HAPI_DECL HAPI_SetAttributeUInt8ArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_UInt8 * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length ); + +/// @brief Set 8-bit integer array attribute data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the HAPI_Int8 values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +HAPI_DECL HAPI_SetAttributeInt8ArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_Int8 * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length); + +/// @brief Set 16-bit integer array attribute data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the HAPI_Int16 values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +HAPI_DECL HAPI_SetAttributeInt16ArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_Int16 * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length ); + +/// @brief Set 64-bit integer array attribute data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the HAPI_Int64 values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +HAPI_DECL HAPI_SetAttributeInt64ArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_Int64 * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length ); + +/// @brief Set float array attribute data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the float values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +HAPI_DECL HAPI_SetAttributeFloatArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const float * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length ); + +/// @brief Set 64-bit float array attribute data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the double values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +HAPI_DECL HAPI_SetAttributeFloat64ArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const double * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length ); + +/// @brief Set string array attribute data. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the string values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +HAPI_DECL HAPI_SetAttributeStringArrayData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const char ** data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length ); + +/// @brief Set attribute dictionary array data. The dictionary data should +/// be provided as JSON-encoded strings. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the dictionary values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +HAPI_DECL HAPI_SetAttributeDictionaryArrayData( const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const char** data_fixed_array, + int data_fixed_length, + const int* sizes_fixed_array, + int start, + int sizes_fixed_length); + +/// @brief Set attribute integer data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeIntDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const int * data_array, + int start, int length, + int * job_id ); + +/// @brief Set unsigned 8-bit attribute integer data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An unsigned 8-bit integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeUInt8DataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_UInt8 * data_array, + int start, int length, + int * job_id ); + +/// @brief Set 8-bit attribute integer data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An 8-bit integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeInt8DataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_Int8 * data_array, + int start, int length, + int * job_id ); + +/// @brief Set 16-bit attribute integer data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An 16-bit integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeInt16DataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_Int16 * data_array, + int start, int length, + int * job_id ); + +/// @brief Set 64-bit attribute integer data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An 64-bit integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeInt64DataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_Int64 * data_array, + int start, int length, + int * job_id ); + +/// @brief Set attribute float data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An float array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeFloatDataAsync( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const float * data_array, + int start, int length, + int * job_id ); + +/// @brief Set 64-bit attribute float data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An 64-bit float array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeFloat64DataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const double * data_array, + int start, int length, + int * job_id ); + +/// @brief Set attribute string data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An ::HAPI_StringHandle array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeStringDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const char ** data_array, + int start, int length, + int * job_id ); + +/// @brief Set attribute string data by index asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] string_array +/// An array of strings at least the size of +/// string_count/tt>. +/// +/// @param[in] string_count +/// Number of strings that are indexed. +/// +/// @param[in] indices_array +/// integer array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. Each +/// entry indexes string_array. +/// +/// @param[in] indices_start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] indices_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeIndexedStringDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const char ** string_array, + int string_count, + const int * indices_array, + int indices_start, + int indices_length, + int * job_id ); + +/// @brief Set multiple attribute string data to the same unique value +/// asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A string +/// +/// @param[in] data_length +/// Must be the length of string data. +/// +/// @param[in] start_index +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeStringUniqueDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const char * data_array, + int data_length, + int start_index, + int num_indices, + int * job_id ); + +/// @brief Set multiple attribute int data to the same unique value +/// asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A integer array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeIntUniqueDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const int * data_array, + int data_length, + int start_index, + int num_indices, + int * job_id ); + +/// @brief Set multiple attribute unsigned 8-bit int data to the same unique +/// value asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A integer array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeUInt8UniqueDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_UInt8 * data_array, + int data_length, + int start_index, + int num_indices, + int * job_id ); + +/// @brief Set multiple attribute 8-bit int data to the same unique value +/// asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A integer array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeInt8UniqueDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_Int8* data_array, + int data_length, + int start_index, + int num_indices, + int * job_id ); + +/// @brief Set multiple attribute 16-bit int data to the same unique value +/// asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A integer array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeInt16UniqueDataAsync( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const HAPI_Int16* data_array, + int data_length, + int start_index, + int num_indices, + int * job_id ); + +/// @brief Set multiple attribute 64-bit int data to the same unique value +/// asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A integer array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeInt64UniqueDataAsync( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const HAPI_Int64* data_array, + int data_length, + int start_index, + int num_indices, + int * job_id ); + +/// @brief Set multiple attribute float data to the same unique value +/// asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A floating point array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeFloatUniqueDataAsync( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const float* data_array, + int data_length, + int start_index, + int num_indices, + int * job_id ); + +/// @brief Set multiple attribute 64-bit float data to the same unique +/// asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size. +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// A floating point array at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] data_length +/// An integer of at least the size of +/// ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start_index +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] num_indices +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start_index. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeFloat64UniqueDataAsync( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const double* data_array, + int data_length, + int start_index, + int num_indices, + int * job_id ); + +/// @brief Set attribute dictionary data asynchronously. The dictionary data +/// should be provided as JSON-encoded strings. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo used as input for what tuple size +/// you want. Also contains some sanity checks like +/// data type. Generally should be the same struct +/// returned by ::HAPI_GetAttributeInfo(). +/// +/// @param[in] data_array +/// An ::HAPI_StringHandle array at least the size of +/// length * ::HAPI_AttributeInfo::tupleSize. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at +/// most ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeDictionaryDataAsync( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char* name, + const HAPI_AttributeInfo* attr_info, + const char** data_array, + int start, int length, + int * job_id ); + +/// @brief Set integer array attribute data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the int values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeIntArrayDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const int * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length, + int * job_id ); + +/// @brief Set unsigned 8-bit integer array attribute data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the HAPI_UInt8 values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeUInt8ArrayDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_UInt8 * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length, + int * job_id ); + +/// @brief Set 8-bit integer array attribute data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the HAPI_Int8 values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeInt8ArrayDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_Int8 * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length, + int * job_id ); + +/// @brief Set 16-bit integer array attribute data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the HAPI_Int16 values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeInt16ArrayDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_Int16 * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length, + int * job_id ); + +/// @brief Set 64-bit integer array attribute data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the HAPI_Int64 values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeInt64ArrayDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const HAPI_Int64 * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length, + int * job_id ); + +/// @brief Set float array attribute data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the float values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeFloatArrayDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const float * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length, + int * job_id ); + +/// @brief Set 64-bit float array attribute data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the double values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeFloat64ArrayDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const double * data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length, + int * job_id ); + +/// @brief Set string array attribute data asynchronously. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the string values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeStringArrayDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const char ** data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length, + int * job_id ); + +/// @brief Set attribute dictionary array data asynchronously. The dictionary +/// should be provided as JSON-encoded strings. +/// +/// @ingroup GeometrySetters Attributes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] name +/// Attribute name. +/// +/// @param[in] attr_info +/// ::HAPI_AttributeInfo that contains the description for the +/// attribute that is being set. +/// +/// @param[in] data_fixed_array +/// An array containing the dictionary values of the attribute. +/// +/// @param[in] data_fixed_length +/// The total size of the data array. The size can be no greater +/// than the ::HAPI_AttributeInfo::totalArrayElements +/// of the attribute. +/// +/// +/// @param[in] sizes_fixed_array +/// An array of integers that contains the sizes of each +/// attribute array. This is required because the attribute +/// array for each geometry component can be of variable size. +/// +/// @param[in] start +/// First index of range. Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - 1. +/// +/// +/// @param[in] sizes_fixed_length +/// Must be at least 0 and at most +/// ::HAPI_AttributeInfo::count - @p start. +/// +/// +/// @param[out] job_id +/// The id assigned to the job. +/// +HAPI_DECL HAPI_SetAttributeDictionaryArrayDataAsync( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const HAPI_AttributeInfo * attr_info, + const char ** data_fixed_array, + int data_fixed_length, + const int * sizes_fixed_array, + int start, + int sizes_fixed_length, + int * job_id ); + +/// @brief Add a group to the input geo with the given type and name. +/// +/// @ingroup GeometrySetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] group_type +/// The group type. +/// +/// @param[in] group_name +/// Name of new group to be added. +/// +HAPI_DECL HAPI_AddGroup( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_GroupType group_type, + const char * group_name ); + +/// @brief Remove a group from the input geo with the given type and name. +/// +/// @ingroup GeometrySetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// @param[in] group_type +/// The group type. +/// +/// @param[in] group_name +/// Name of the group to be removed +/// +HAPI_DECL HAPI_DeleteGroup( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_GroupType group_type, + const char * group_name ); + +/// @brief Set group membership. +/// +/// @ingroup GeometrySetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +/// @param[in] part_id +/// Currently not used. Just pass 0. +/// +/// +/// @param[in] group_type +/// The group type. +/// +/// @param[in] group_name +/// The group name. +/// +/// @param[in] membership_array +/// Array of ints that represent the membership of this +/// group. Should be the size given by +/// ::HAPI_PartInfo_GetElementCountByGroupType() with +/// @p group_type and the ::HAPI_PartInfo of @p part_id. +/// +/// @param[in] start +/// Start offset into the membership array. Must be +/// less than ::HAPI_PartInfo_GetElementCountByGroupType(). +/// +/// +/// @param[in] length +/// Should be less than or equal to the size +/// of @p membership_array. When setting edge group membership, +/// this parameter should be set to the number of points (which +/// are used to implictly define the edges), not to the number +/// edges in the group. +/// +/// +HAPI_DECL HAPI_SetGroupMembership( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_GroupType group_type, + const char * group_name, + const int * membership_array, + int start, int length ); + +/// @brief Commit the current input geometry to the cook engine. Nodes +/// that use this geometry node will re-cook using the input +/// geometry given through the geometry setter API calls. +/// +/// @ingroup GeometrySetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +HAPI_DECL HAPI_CommitGeo( const HAPI_Session * session, + HAPI_NodeId node_id ); + +/// @brief Remove all changes that have been committed to this +/// geometry. If this is an intermediate result node (Edit SOP), all +/// deltas will be removed. If it's any other type of node, the node +/// will be unlocked if it is locked. +/// +/// @ingroup GeometrySetters +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The SOP node id. +/// +HAPI_DECL HAPI_RevertGeo( const HAPI_Session * session, + HAPI_NodeId node_id ); + +/// @defgroup Materials +/// Functions for working with materials + +/// @brief Get material ids by face/primitive. The material ids returned +/// will be valid as long as the asset is alive. You should query +/// this list after every cook to see if the material assignments +/// have changed. You should also query each material individually +/// using ::HAPI_GetMaterialInfo() to see if it is dirty and needs +/// to be re-imported. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] geometry_node_id +/// The geometry node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] are_all_the_same +/// (optional) If true, all faces on this part have the +/// same material assignment. You can pass NULL here. +/// +/// @param[out] material_ids_array +/// An array of ::HAPI_NodeId at least the size of +/// @p length and at most the size of +/// ::HAPI_PartInfo::faceCount. +/// +/// @param[in] start +/// The starting index into the list of faces from which +/// you wish to get the material ids from. Note that +/// this should be less than ::HAPI_PartInfo::faceCount. +/// +/// +/// @param[in] length +/// The number of material ids you wish to get. Note that +/// this should be at most: +/// ::HAPI_PartInfo::faceCount - @p start. +/// +/// +HAPI_DECL HAPI_GetMaterialNodeIdsOnFaces( const HAPI_Session * session, + HAPI_NodeId geometry_node_id, + HAPI_PartId part_id, + HAPI_Bool * are_all_the_same, + HAPI_NodeId * material_ids_array, + int start, int length ); + +/// @brief Get the material info. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] material_node_id +/// The material node id. +/// +/// @param[out] material_info +/// The returned material info. +/// +HAPI_DECL HAPI_GetMaterialInfo( const HAPI_Session * session, + HAPI_NodeId material_node_id, + HAPI_MaterialInfo * material_info ); + +/// @brief Render a single texture from a COP to an image for +/// later extraction. +/// +/// @ingroup Materials +/// +/// Note that you must call this method, +/// ::HAPI_RenderCOPOutputToImage(), or ::HAPI_RenderTextureToImage() +/// before any of the other material APIs. +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] cop_node_id +/// The COP node id. +/// +HAPI_DECL HAPI_RenderCOPToImage( const HAPI_Session * session, + HAPI_NodeId cop_node_id ); + +/// @brief Render a single texture from a COP to an image for +/// later extraction. COPs may have multiple outputs, +/// so this method lets you select which output to use. +/// +/// @ingroup Materials +/// +/// Note that you must call this method, +/// ::HAPI_RenderCOPToImage(), or ::HAPI_RenderTextureToImage() +/// before any of the other material APIs. +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] cop_node_id +/// The COP node id. +/// +/// @param[in] cop_output_name +/// The name of the output to extract. Passing in +/// an empty string will default to the COP's first output. +/// +HAPI_DECL HAPI_RenderCOPOutputToImage( const HAPI_Session * session, + HAPI_NodeId cop_node_id, + const char * cop_output_name ); + +/// @brief Render only a single texture to an image for later extraction. +/// An example use of this method might be to render the diffuse, +/// normal, and bump texture maps of a material to individual +/// texture files for use within the client application. +/// +/// Note that you must call this method, +/// ::HAPI_RenderCOPToImage(), or ::HAPI_RenderCOPOutputToImage() +/// before any of the other material APIs. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] material_node_id +/// The material node id. +/// +/// @param[in] parm_id +/// This is the index in the parameter list of the +/// material_id's node of the parameter containing the +/// texture map file path. +/// +HAPI_DECL HAPI_RenderTextureToImage( const HAPI_Session * session, + HAPI_NodeId material_node_id, + HAPI_ParmId parm_id ); + +/// @brief Get information about the image that was just rendered, like +/// resolution and default file format. This information will be +/// used when extracting planes to an image. +/// +/// Note that you must call one of ::HAPI_RenderTextureToImage(), +/// ::HAPI_RenderCOPToImage(), or ::HAPI_RenderCOPOutputToImage() +/// first for this method call to make sense. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] material_node_id +/// The material node id. +/// +/// @param[out] image_info +/// The struct containing the image information. +/// +HAPI_DECL HAPI_GetImageInfo( const HAPI_Session * session, + HAPI_NodeId material_node_id, + HAPI_ImageInfo * image_info ); + +/// @brief Set image information like resolution and file format. +/// This information will be used when extracting planes to +/// an image. +/// +/// Note that you must call one of ::HAPI_RenderTextureToImage(), +/// ::HAPI_RenderCOPToImage(), or ::HAPI_RenderCOPOutputToImage() +/// first for this method call to make sense. +/// +/// You should also first call ::HAPI_GetImageInfo() to get the +/// current Image Info and change only the properties +/// you don't like. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] material_node_id +/// The material node id. +/// +/// @param[in] image_info +/// The struct containing the new image information. +/// +HAPI_DECL HAPI_SetImageInfo( const HAPI_Session * session, + HAPI_NodeId material_node_id, + const HAPI_ImageInfo * image_info ); + +/// @brief Get the number of image planes for the just rendered image. +/// +/// Note that you must call one of ::HAPI_RenderTextureToImage(), +/// ::HAPI_RenderCOPToImage(), or ::HAPI_RenderCOPOutputToImage() +/// first for this method call to make sense. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] material_node_id +/// The material node id. +/// +/// @param[out] image_plane_count +/// The number of image planes. +/// +HAPI_DECL HAPI_GetImagePlaneCount( const HAPI_Session * session, + HAPI_NodeId material_node_id, + int * image_plane_count ); + +/// @brief Get the names of the image planes of the just rendered image. +/// +/// Note that you must call one of ::HAPI_RenderTextureToImage(), +/// ::HAPI_RenderCOPToImage(), or ::HAPI_RenderCOPOutputToImage() +/// first for this method call to make sense. +/// +/// You should also call ::HAPI_GetImagePlaneCount() first to get +/// the total number of image planes so you know how large the +/// image_planes string handle array should be. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] material_node_id +/// The material node id. +/// +/// @param[out] image_planes_array +/// The image plane names. +/// +/// @param[in] image_plane_count +/// The number of image planes to get names for. This +/// must be less than or equal to the count returned +/// by ::HAPI_GetImagePlaneCount(). +/// +/// +HAPI_DECL HAPI_GetImagePlanes( const HAPI_Session * session, + HAPI_NodeId material_node_id, + HAPI_StringHandle * image_planes_array, + int image_plane_count ); + +/// @brief Extract a rendered image to a file. +/// +/// Note that you must call one of ::HAPI_RenderTextureToImage(), +/// ::HAPI_RenderCOPToImage(), or ::HAPI_RenderCOPOutputToImage() +/// first for this method call to make sense. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] material_node_id +/// The material node id. +/// +/// @param[in] image_file_format_name +/// The image file format name you wish the image to be +/// extracted as. You can leave this parameter NULL to +/// get the image in the original format if it comes from +/// another texture file or in the default HAPI format, +/// which is ::HAPI_DEFAULT_IMAGE_FORMAT_NAME, if the image +/// is generated. +/// +/// You can get some of the very common standard image +/// file format names from HAPI_Common.h under the +/// "Defines" section. +/// +/// You can also get a list of all supported file formats +/// (and the exact names this parameter expects) +/// by using ::HAPI_GetSupportedImageFileFormats(). This +/// list will include custom file formats you created via +/// custom DSOs (see HDK docs about IMG_Format). You will +/// get back a list of ::HAPI_ImageFileFormat. This +/// parameter expects the ::HAPI_ImageFileFormat::nameSH +/// of a given image file format. +/// +/// @param[in] image_planes +/// The image planes you wish to extract into the file. +/// Multiple image planes should be separated by spaces. +/// +/// @param[in] destination_folder_path +/// The folder where the image file should be created. +/// +/// @param[in] destination_file_name +/// Optional parameter to overwrite the name of the +/// extracted texture file. This should NOT include +/// the extension as the file type will be decided +/// by the ::HAPI_ImageInfo you can set using +/// ::HAPI_SetImageInfo(). You still have to use +/// destination_file_path to get the final file path. +/// +/// Pass in NULL to have the file name be automatically +/// generated from the name of the material SHOP node, +/// the name of the texture map parameter if the +/// image was rendered from a texture, and the image +/// plane names specified. +/// +/// @param[out] destination_file_path +/// The full path string handle, including the +/// destination_folder_path and the texture file name, +/// to the extracted file. Note that this string handle +/// will only be valid until the next call to +/// this function. +/// +HAPI_DECL HAPI_ExtractImageToFile( const HAPI_Session * session, + HAPI_NodeId material_node_id, + const char * image_file_format_name, + const char * image_planes, + const char * destination_folder_path, + const char * destination_file_name, + int * destination_file_path ); +/// @brief Get the file name that this image would be saved to +/// +/// Check to see what file path HAPI_ExtractImageToFile would have +/// saved to given the same parms. Perhaps you might wish to see +/// if it already exists before extracting. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] material_node_id +/// The material node id. +/// +/// @param[in] image_file_format_name +/// The image file format name you wish the image to be +/// extracted as. See HAPI_ExtractImageToFile for more information. +/// +/// @param[in] image_planes +/// The image planes you wish to extract into the file. +/// Multiple image planes should be separated by spaces. +/// +/// @param[in] destination_folder_path +/// The folder where the image file sould be created. +/// +/// @param[in] destination_file_name +/// Optional parameter to overwrite the name of the +/// extracted texture file. See HAPI_ExtractImageToFile for more information. +/// +/// @param[in] texture_parm_id +/// The index in the parameter list of the material node. +/// of the parameter containing the texture map file path +/// +/// @param[out] destination_file_path +/// The full path string handle, including the +/// destination_folder_path and the texture file name, +/// to the extracted file. Note that this string handle +/// will only be valid until the next call to +/// this function. +/// +HAPI_DECL HAPI_GetImageFilePath( const HAPI_Session * session, + HAPI_NodeId material_node_id, + const char * image_file_format_name, + const char * image_planes, + const char * destination_folder_path, + const char * destination_file_name, + HAPI_ParmId texture_parm_id, + int * destination_file_path ); + +/// @brief Extract a rendered image to memory. +/// +/// Note that you must call one of ::HAPI_RenderTextureToImage(), +/// ::HAPI_RenderCOPToImage(), or ::HAPI_RenderCOPOutputToImage() +/// first for this method call to make sense. +/// +/// Also note that this function will do all the work of +/// extracting and compositing the image into a memory buffer +/// but will not return to you that buffer, only its size. Use +/// the returned size to allocated a sufficiently large buffer +/// and call ::HAPI_GetImageMemoryBuffer() to fill your buffer +/// with the just extracted image. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] material_node_id +/// The material node id. +/// +/// @param[in] image_file_format_name +/// The image file format name you wish the image to be +/// extracted as. You can leave this parameter NULL to +/// get the image in the original format if it comes from +/// another texture file or in the default HAPI format, +/// which is ::HAPI_DEFAULT_IMAGE_FORMAT_NAME, if the image +/// is generated. +/// +/// You can get some of the very common standard image +/// file format names from HAPI_Common.h under the +/// "Defines" section. +/// +/// You can also get a list of all supported file formats +/// (and the exact names this parameter expects) +/// by using ::HAPI_GetSupportedImageFileFormats(). This +/// list will include custom file formats you created via +/// custom DSOs (see HDK docs about IMG_Format). You will +/// get back a list of ::HAPI_ImageFileFormat. This +/// parameter expects the ::HAPI_ImageFileFormat::nameSH +/// of a given image file format. +/// +/// @param[in] image_planes +/// The image planes you wish to extract into the file. +/// Multiple image planes should be separated by spaces. +/// +/// @param[out] buffer_size +/// The extraction will be done to an internal buffer +/// who's size you get via this parameter. Use the +/// returned buffer_size when calling +/// ::HAPI_GetImageMemoryBuffer() to get the image +/// buffer you just extracted. +/// +HAPI_DECL HAPI_ExtractImageToMemory( const HAPI_Session * session, + HAPI_NodeId material_node_id, + const char * image_file_format_name, + const char * image_planes, + int * buffer_size ); + +/// @brief Fill your allocated buffer with the just extracted +/// image buffer. +/// +/// Note that you must call one of ::HAPI_RenderTextureToImage(), +/// ::HAPI_RenderCOPToImage(), or ::HAPI_RenderCOPOutputToImage() +/// first for this method call to make sense. +/// +/// Also note that you must call ::HAPI_ExtractImageToMemory() +/// first in order to perform the extraction and get the +/// extracted image buffer size that you need to know how much +/// memory to allocated to fit your extracted image. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] material_node_id +/// The material node id. +/// +/// @param[out] buffer +/// The buffer passed in here will be filled with the +/// image buffer created during the call to +/// ::HAPI_ExtractImageToMemory(). +/// +/// @param[in] length +/// Sanity check. This size should be the same as the +/// size allocated for the buffer passed in and should +/// be at least as large as the buffer_size returned by +/// the call to ::HAPI_ExtractImageToMemory(). +/// +/// +HAPI_DECL HAPI_GetImageMemoryBuffer( const HAPI_Session * session, + HAPI_NodeId material_node_id, + char * buffer, int length ); + +/// @brief Get the number of supported texture file formats. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] file_format_count +/// The number of supported texture file formats. +/// +HAPI_DECL HAPI_GetSupportedImageFileFormatCount( const HAPI_Session * session, + int * file_format_count ); + +/// @brief Get a list of support image file formats - their names, +/// descriptions and a list of recognized extensions. +/// +/// Note that you MUST call +/// ::HAPI_GetSupportedImageFileFormatCount() +/// before calling this function for the first time. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] formats_array +/// The list of ::HAPI_ImageFileFormat structs to +/// be filled. +/// +/// @param[in] file_format_count +/// The number of supported texture file formats. This +/// should be at least as large as the count returned +/// by ::HAPI_GetSupportedImageFileFormatCount(). +/// +/// +HAPI_DECL HAPI_GetSupportedImageFileFormats( + const HAPI_Session * session, + HAPI_ImageFileFormat * formats_array, + int file_format_count ); + +/// @brief Loads some raw image data into a COP node. +/// +/// This method expects the image data to be in linear color space. +/// It will not do any color space conversions for you; the data +/// must be converted beforehand. +/// +/// @ingroup Materials +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] parent_node_id +/// The node that the copnet containing the texture will be +/// created in, or -1 if the parent is the image manager. +/// +/// @param[in] width +/// The width of the image in pixels. @p width * @p height * +/// channel count (implied by @p packing) should equal the size +/// of @p data_array. +/// +/// @param[in] height +/// The height of the image in pixels. @p width * @p height * +/// channel count (implied by @p packing) should equal the size +/// of @p data_array. +/// +/// @param[in] packing +/// How the image data is packed (single-channel, RBGA, etc). +/// +/// @param[in] flip_x +/// Whether to flip the image horizontally. +/// +/// @param[in] flip_y +/// Whether to flip the image vertically. +/// +/// @param[in] data_array +/// A float array representing the image's pixel data. Values +/// should be between 0.0 and 1.0. The data should be packed +/// according to @p packing. +/// +/// @param[in] start +/// The index of the first value of @p data_array to use. +/// Must be at least 0 and at most (size of @p data_array - 1). +/// +/// +/// +/// @param[in] length +/// How much of @p data_array to use. Must be at least 0 +/// and at most (size of @p data_array - start). +/// +/// +HAPI_DECL HAPI_CreateCOPImage( const HAPI_Session * session, + HAPI_NodeId parent_node_id, + const int width, + const int height, + const HAPI_ImagePacking packing, + HAPI_Bool flip_x, + HAPI_Bool flip_y, + const float * data_array, + int start, int length ); + +/// @defgroup Animation +/// Functions for working with animation. + +/// @brief Set an animation curve on a parameter of an exposed node. +/// +/// @ingroup Animation +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The exposed node id. +/// +/// @param[in] parm_id +/// The id of an exposed parameter within the node. +/// @param[in] parm_index +/// The index of the parameter, if it is for example +/// a 3 tuple +/// +/// @param[in] curve_keyframes_array +/// An array of ::HAPI_Keyframe structs that describes +/// the keys on this curve. +/// +/// @param[in] keyframe_count +/// The number of keys on the curve. +/// +HAPI_DECL HAPI_SetAnimCurve( const HAPI_Session * session, + HAPI_NodeId node_id, HAPI_ParmId parm_id, + int parm_index, + const HAPI_Keyframe * curve_keyframes_array, + int keyframe_count ); + +/// @brief A specialized convenience function to set the T,R,S values +/// on an exposed node. +/// +/// @ingroup Animation +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The exposed node id. +/// +/// @param[in] trans_comp +/// A value of ::HAPI_TransformComponent that +/// identifies the particular component of the +/// transform to attach the curve to, for example +/// ::HAPI_TRANSFORM_TX. +/// +/// @param[in] curve_keyframes_array +/// An array of ::HAPI_Keyframe structs that describes +/// the keys on this curve. +/// +/// @param[in] keyframe_count +/// The number of keys on the curve. +/// +HAPI_DECL HAPI_SetTransformAnimCurve( + const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_TransformComponent trans_comp, + const HAPI_Keyframe * curve_keyframes_array, + int keyframe_count ); + +/// @brief Resets the simulation cache of the asset. This is very useful +/// for assets that use dynamics, to be called after some +/// setup has changed for the asset - for example, asset inputs +/// +/// @ingroup Time +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The asset node id. +/// +HAPI_DECL HAPI_ResetSimulation( const HAPI_Session * session, + HAPI_NodeId node_id ); + +/// @defgroup Volumes +/// Functions for working with Volume data + +/// @brief Retrieve any meta-data about the volume primitive, including +/// its transform, location, scale, taper, resolution. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] volume_info +/// The meta-data associated with the volume on the +/// part specified by the previous parameters. +/// +HAPI_DECL HAPI_GetVolumeInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_VolumeInfo * volume_info ); + +/// @brief Iterate through a volume based on 8x8x8 sections of the volume +/// Start iterating through the value of the volume at part_id. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] tile +/// The tile info referring to the first tile in the +/// volume at part_id. +/// +HAPI_DECL HAPI_GetFirstVolumeTile( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_VolumeTileInfo * tile ); + +/// @brief Iterate through a volume based on 8x8x8 sections of the volume +/// Continue iterating through the value of the volume at part_id. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] tile +/// The tile info referring to the next tile in the +/// set of tiles associated with the volume at this part. +/// +HAPI_DECL HAPI_GetNextVolumeTile( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_VolumeTileInfo * tile ); + +/// @brief Retrieve floating point values of the voxel at a specific +/// index. Note that you must call ::HAPI_GetVolumeInfo() prior +/// to this call. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] x_index +/// The x index/coordinate of the voxel. +/// +/// @param[in] y_index +/// The y index/coordinate of the voxel. +/// +/// @param[in] z_index +/// The z index/coordinate of the voxel. +/// +/// @param[out] values_array +/// The values of the voxel. +/// +/// @param[in] value_count +/// Should be equal to the volume's +/// ::HAPI_VolumeInfo::tupleSize. +/// +/// +HAPI_DECL HAPI_GetVolumeVoxelFloatData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + int x_index, + int y_index, + int z_index, + float * values_array, + int value_count ); + +/// @brief Retrieve floating point values of the voxels pointed to +/// by a tile. Note that a tile may extend beyond the limits +/// of the volume so not all values in the given buffer will +/// be written to. Voxels outside the volume will be initialized +/// to the given fill value. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] fill_value +/// Value that will be used to fill the @p values_array. +/// This is useful so that you can see what values +/// have actually been written to. +/// +/// @param[in] tile +/// The tile to retrieve. +/// +/// @param[out] values_array +/// The values of the tile. +/// +/// @param[in] length +/// The length should be ( 8 ^ 3 ) * tupleSize. +/// +HAPI_DECL HAPI_GetVolumeTileFloatData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + float fill_value, + const HAPI_VolumeTileInfo * tile, + float * values_array, + int length ); + +/// @brief Retrieve integer point values of the voxel at a specific +/// index. Note that you must call ::HAPI_GetVolumeInfo() prior +/// to this call. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] x_index +/// The x index/coordinate of the voxel. +/// +/// @param[in] y_index +/// The y index/coordinate of the voxel. +/// +/// @param[in] z_index +/// The z index/coordinate of the voxel. +/// +/// @param[out] values_array +/// The values of the voxel. +/// +/// @param[in] value_count +/// Should be equal to the volume's +/// ::HAPI_VolumeInfo::tupleSize. +/// +/// +HAPI_DECL HAPI_GetVolumeVoxelIntData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + int x_index, + int y_index, + int z_index, + int * values_array, + int value_count ); + +/// @brief Retrieve integer point values of the voxels pointed to +/// by a tile. Note that a tile may extend beyond the limits +/// of the volume so not all values in the given buffer will +/// be written to. Voxels outside the volume will be initialized +/// to the given fill value. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] fill_value +/// Value that will be used to fill the @p values_array. +/// This is useful so that you can see what values +/// have actually been written to. +/// +/// @param[in] tile +/// The tile to retrieve. +/// +/// @param[out] values_array +/// The values of the tile. +/// +/// @param[in] length +/// The length should be ( 8 ^ 3 ) * tupleSize. +/// +HAPI_DECL HAPI_GetVolumeTileIntData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + int fill_value, + const HAPI_VolumeTileInfo * tile, + int * values_array, + int length ); + +/// @brief Get the height field data for a terrain volume as a flattened +/// 2D array of float heights. Should call ::HAPI_GetVolumeInfo() +/// first to make sure the volume info is initialized. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] values_array +/// Heightfield flattened array. Should be at least the size of +/// @p start + @p length. +/// +/// @param[in] start +/// The start at least 0 and at most +/// ( ::HAPI_VolumeInfo::xLength * ::HAPI_VolumeInfo::yLength ) +/// - @p length. +/// +/// @param[in] length +/// The length should be at least 1 or at most +/// ( ::HAPI_VolumeInfo::xLength * ::HAPI_VolumeInfo::yLength ) +/// - @p start. +/// +HAPI_DECL HAPI_GetHeightFieldData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + float * values_array, + int start, int length ); + +/// @brief Set the volume info of a geo on a geo input. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] volume_info +/// All volume information that can be specified per +/// volume. This includes the position, orientation, scale, +/// data format, tuple size, and taper. The tile size is +/// always 8x8x8. +/// +HAPI_DECL HAPI_SetVolumeInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const HAPI_VolumeInfo * volume_info ); + +/// @brief Set the values of a float tile: this is an 8x8x8 subsection of +/// the volume. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] tile +/// The tile that the volume will be input into. +/// +/// @param[in] values_array +/// The values of the individual voxel tiles in the +/// volume. The length of this array should +/// be ( 8 ^ 3 ) * tupleSize. +/// +/// @param[in] length +/// The length should be ( 8 ^ 3 ) * tupleSize. +/// +HAPI_DECL HAPI_SetVolumeTileFloatData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const HAPI_VolumeTileInfo * tile, + const float * values_array, + int length ); + +/// @brief Set the values of an int tile: this is an 8x8x8 subsection of +/// the volume. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] tile +/// The tile that the volume will be input into. +/// +/// @param[in] values_array +/// The values of the individual voxel tiles in the +/// volume. The length of this array should +/// be ( 8 ^ 3 ) * tupleSize. +/// +/// @param[in] length +/// The length should be ( 8 ^ 3 ) * tupleSize. +/// +HAPI_DECL HAPI_SetVolumeTileIntData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const HAPI_VolumeTileInfo * tile, + const int * values_array, + int length ); + +/// @brief Set the values of a float voxel in the volume. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] x_index +/// The x index/coordinate of the voxel. +/// +/// @param[in] y_index +/// The y index/coordinate of the voxel. +/// +/// @param[in] z_index +/// The z index/coordinate of the voxel. +/// +/// @param[in] values_array +/// The values of the voxel. +/// +/// @param[in] value_count +/// Should be equal to the volume's +/// ::HAPI_VolumeInfo::tupleSize. +/// +/// +HAPI_DECL HAPI_SetVolumeVoxelFloatData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + int x_index, + int y_index, + int z_index, + const float * values_array, + int value_count ); + +/// @brief Set the values of a integer voxel in the volume. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] x_index +/// The x index/coordinate of the voxel. +/// +/// @param[in] y_index +/// The y index/coordinate of the voxel. +/// +/// @param[in] z_index +/// The z index/coordinate of the voxel. +/// +/// @param[in] values_array +/// The values of the voxel. +/// +/// @param[in] value_count +/// Should be equal to the volume's +/// ::HAPI_VolumeInfo::tupleSize. +/// +/// +HAPI_DECL HAPI_SetVolumeVoxelIntData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + int x_index, + int y_index, + int z_index, + const int * values_array, + int value_count ); + +/// @brief Get the bounding values of a volume. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] x_min +/// The minimum x value of the volume's bounding box. +/// Can be null if you do not want this value. +/// +/// @param[out] y_min +/// The minimum y value of the volume's bounding box. +/// Can be null if you do not want this value. +/// +/// @param[out] z_min +/// The minimum z value of the volume's bounding box. +/// Can be null if you do not want this value. +/// +/// @param[out] x_max +/// The maximum x value of the volume's bounding box. +/// Can be null if you do not want this value. +/// +/// @param[out] y_max +/// The maximum y value of the volume's bounding box. +/// Can be null if you do not want this value. +/// +/// @param[out] z_max +/// The maximum z value of the volume's bounding box. +/// Can be null if you do not want this value. +/// +/// @param[out] x_center +/// The x value of the volume's bounding box center. +/// Can be null if you do not want this value. +/// +/// @param[out] y_center +/// The y value of the volume's bounding box center. +/// Can be null if you do not want this value. +/// +/// @param[out] z_center +/// The z value of the volume's bounding box center. +/// Can be null if you do not want this value. +/// +HAPI_DECL HAPI_GetVolumeBounds( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + float * x_min, float * y_min, float * z_min, + float * x_max, float * y_max, float * z_max, + float * x_center, float * y_center, float * z_center ); + +/// @brief Set the height field data for a terrain volume with the values from +/// a flattened 2D array of float. +/// ::HAPI_SetVolumeInfo() should be called first to make sure that the +/// volume and its info are initialized. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[in] values_array +/// Heightfield flattened array. Should be at least the size of +/// @p start + @p length. +/// +/// @param[in] start +/// The start at least 0 and at most +/// ( ::HAPI_VolumeInfo::xLength * ::HAPI_VolumeInfo::yLength ) - @p length. +/// +/// @param[in] length +/// The length should be at least 1 or at most +/// ( ::HAPI_VolumeInfo::xLength * ::HAPI_VolumeInfo::yLength ) - @p start. +/// +/// @param[in] name +/// The name of the volume used for the heightfield. +/// If set to "height" the values will be used for height information, +/// if not, the data will used as a mask. +/// +HAPI_DECL HAPI_SetHeightFieldData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const char * name, + const float * values_array, + int start, int length ); + +/// @brief Retrieve the visualization meta-data of the volume. +/// +/// @ingroup Volumes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] visual_info +/// The meta-data associated with the visualization +/// settings of the part specified by the previous +/// parameters. +/// +HAPI_DECL HAPI_GetVolumeVisualInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_VolumeVisualInfo * visual_info ); + +/// @defgroup Curves +/// Functions for working with curves + +/// @brief Retrieve any meta-data about the curves, including the +/// curve's type, order, and periodicity. +/// +/// @ingroup Curves +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] info +/// The curve info represents the meta-data about +/// the curves, including the type, order, +/// and periodicity. +/// +HAPI_DECL HAPI_GetCurveInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_CurveInfo * info ); + +/// @brief Retrieve the number of vertices for each curve in the part. +/// +/// @ingroup Curves +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] counts_array +/// The number of cvs each curve contains +/// +/// @param[in] start +/// The index of the first curve. +/// +/// +/// @param[in] length +/// The number of curves' counts to retrieve. +/// +HAPI_DECL HAPI_GetCurveCounts( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + int * counts_array, + int start, int length ); + +/// @brief Retrieve the orders for each curve in the part if the +/// curve has varying order. +/// +/// @ingroup Curves +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] orders_array +/// The order of each curve will be returned in this +/// array. +/// +/// @param[in] start +/// The index of the first curve. +/// +/// +/// @param[in] length +/// The number of curves' orders to retrieve. +/// +HAPI_DECL HAPI_GetCurveOrders( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + int * orders_array, + int start, int length ); + +/// @brief Retrieve the knots of the curves in this part. +/// +/// @ingroup Curves +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] knots_array +/// The knots of each curve will be returned in this +/// array. +/// +/// @param[in] start +/// The index of the first curve. +/// +/// +/// @param[in] length +/// The number of curves' knots to retrieve. The +/// length of all the knots on a single curve is +/// the order of that curve plus the number of +/// vertices (see ::HAPI_GetCurveOrders(), +/// and ::HAPI_GetCurveCounts()). +/// +HAPI_DECL HAPI_GetCurveKnots( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + float * knots_array, + int start, int length ); + +/// @brief Set meta-data for the curve mesh, including the +/// curve type, order, and periodicity. +/// +/// @ingroup Curves +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// Currently unused. Input asset geos are assumed +/// to have only one part. +/// +/// @param[in] info +/// The curve info represents the meta-data about +/// the curves, including the type, order, +/// and periodicity. +/// +HAPI_DECL HAPI_SetCurveInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const HAPI_CurveInfo * info ); + +/// @brief Set the number of vertices for each curve in the part. +/// +/// @ingroup Curves +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// Currently unused. Input asset geos are assumed +/// to have only one part. +/// +/// @param[in] counts_array +/// The number of cvs each curve contains. +/// +/// @param[in] start +/// The index of the first curve. +/// +/// +/// @param[in] length +/// The number of curves' counts to set. +/// +/// +HAPI_DECL HAPI_SetCurveCounts( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const int * counts_array, + int start, int length ); + +/// @brief Set the orders for each curve in the part if the +/// curve has varying order. +/// +/// @ingroup Curves +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// Currently unused. Input asset geos are assumed +/// to have only one part. +/// +/// @param[in] orders_array +/// The orders of each curve. +/// +/// @param[in] start +/// The index of the first curve. +/// +/// +/// @param[in] length +/// The number of curves' orders to retrieve. +/// +/// +HAPI_DECL HAPI_SetCurveOrders( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const int * orders_array, + int start, int length ); + +/// @brief Set the knots of the curves in this part. +/// +/// @ingroup Curves +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// Currently unused. Input asset geos are assumed +/// to have only one part. +/// +/// @param[in] knots_array +/// The knots of each curve. +/// +/// @param[in] start +/// The index of the first curve. +/// +/// +/// @param[in] length +/// The number of curves' knots to set. The +/// length of all the knots on a single curve is +/// the order of that curve plus the number of +/// vertices (see ::HAPI_SetCurveOrders(), +/// and ::HAPI_SetCurveCounts()). +/// +HAPI_DECL HAPI_SetCurveKnots( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const float * knots_array, + int start, int length ); + +// INPUT CURVE INFO --------------------------------------------------------- + +/// @defgroup InputCurves +/// Functions for working with curves + +/// @brief Retrieve meta-data about the input curves, including the +/// curve's type, order, and whether or not the curve is closed and reversed. +/// +/// @ingroup InputCurves +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// The part id. +/// +/// @param[out] info +/// The curve info represents the meta-data about +/// the curves, including the type, order, +/// and closed and reversed values. +/// +HAPI_DECL HAPI_GetInputCurveInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + HAPI_InputCurveInfo * info ); + +/// @brief Set meta-data for the input curves, including the +/// curve type, order, reverse and closed properties. +/// +/// @ingroup InputCurves +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// Currently unused. Input asset geos are assumed +/// to have only one part. +/// +/// @param[in] info +/// The curve info represents the meta-data about +/// the curves, including the type, order, +/// and closed and reverse properties. +/// +HAPI_DECL HAPI_SetInputCurveInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const HAPI_InputCurveInfo * info ); + +/// @brief Sets the positions for input curves, doing checks for +/// curve validity, and adjusting the curve settings accordingly. +/// Will also cook the node. +/// +/// @ingroup InputCurves +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// Currently unused. Input asset geos are assumed +/// to have only one part. +/// +/// @param[in] positions_array +/// A float array representing the positions attribute. +/// It will read the array assuming a tuple size of 3. +/// Note that this function does not do any coordinate axes +/// conversion. +/// +/// @param[in] start +/// The index of the first position in positions_array. +/// +/// +/// @param[in] length +/// The size of the positions array. +/// +/// +HAPI_DECL HAPI_SetInputCurvePositions( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const float* positions_array, + int start, + int length); + +/// @brief Sets the positions for input curves, doing checks for +/// curve validity, and adjusting the curve settings accordingly. +/// Will also cook the node. Additionally, adds rotation and scale +/// attributes to the curve. +/// +/// @ingroup InputCurves +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] part_id +/// Currently unused. Input asset geos are assumed +/// to have only one part. +/// +/// @param[in] positions_array +/// A float array representing the positions attribute. +/// It will read the array assuming a tuple size of 3. +/// Note that this function does not do any coordinate axes +/// conversion. +/// +/// @param[in] positions_array +/// A float array representing the positions attribute. +/// It will read the array assuming a tuple size of 3. +/// Note that this function does not do any coordinate axes +/// conversion. +/// +/// @param[in] positions_start +/// The index of the first position in positions_array. +/// +/// +/// @param[in] positions_length +/// The size of the positions array. +/// +/// +/// @param[in] rotations_array +/// A float array representing the rotation (rot) attribute. +/// It will read the array assuming a tuple size of 4 +/// representing quaternion values +/// +/// @param[in] rotations_start +/// The index of the first rotation in rotations_array. +/// +/// +/// @param[in] rotations_length +/// The size of the rotations array. +/// +/// +/// @param[in] scales_array +/// A float array representing the scale attribute. +/// It will read the array assuming a tuple size of 3 +/// +/// @param[in] scales_start +/// The index of the first scale in scales_array. +/// +/// +/// @param[in] scales_length +/// The size of the scales array. +/// +/// +HAPI_DECL HAPI_SetInputCurvePositionsRotationsScales( + const HAPI_Session* session, + HAPI_NodeId node_id, + HAPI_PartId part_id, + const float* positions_array, + int positions_start, + int positions_length, + const float* rotations_array, + int rotations_start, + int rotations_length, + const float * scales_array, + int scales_start, + int scales_length); + +// BASIC PRIMITIVES --------------------------------------------------------- + +/// @brief Get the box info on a geo part (if the part is a box). +/// +/// @ingroup Geometry +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] geo_node_id +/// The geo node id. +/// +/// @param[in] part_id +/// The part id of the +/// +/// @param[out] box_info +/// The returned box info. +/// +HAPI_DECL HAPI_GetBoxInfo( const HAPI_Session * session, + HAPI_NodeId geo_node_id, + HAPI_PartId part_id, + HAPI_BoxInfo * box_info ); + +/// @brief Get the sphere info on a geo part (if the part is a sphere). +/// +/// @ingroup Geometry +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] geo_node_id +/// The geo node id. +/// +/// @param[in] part_id +/// The part id of the +/// +/// @param[out] sphere_info +/// The returned sphere info. +/// +HAPI_DECL HAPI_GetSphereInfo( const HAPI_Session * session, + HAPI_NodeId geo_node_id, + HAPI_PartId part_id, + HAPI_SphereInfo * sphere_info ); + +/// @defgroup Caching +/// Functions for working with memory and file caches + +/// @brief Get the number of currently active caches. +/// +/// @ingroup Caching +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] active_cache_count +/// The number of currently active caches. +/// +HAPI_DECL HAPI_GetActiveCacheCount( const HAPI_Session * session, + int * active_cache_count ); + +/// @brief Get the names of the currently active caches. +/// +/// Requires a valid active cache count which you get from: +/// ::HAPI_GetActiveCacheCount(). +/// +/// @ingroup Caching +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] cache_names_array +/// String array with the returned cache names. Must be +/// at least the size of @a active_cache_count. +/// +/// @param[in] active_cache_count +/// The count returned by ::HAPI_GetActiveCacheCount(). +/// +/// +HAPI_DECL HAPI_GetActiveCacheNames( const HAPI_Session * session, + HAPI_StringHandle * cache_names_array, + int active_cache_count ); + +/// @brief Lets you inspect specific properties of the different memory +/// caches in the current Houdini context. +/// +/// @ingroup Caching +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] cache_name +/// Cache name from ::HAPI_GetActiveCacheNames(). +/// +/// @param[in] cache_property +/// The specific property of the cache to get the value for. +/// +/// @param[out] property_value +/// Returned property value. +/// +HAPI_DECL HAPI_GetCacheProperty( const HAPI_Session * session, + const char * cache_name, + HAPI_CacheProperty cache_property, + int * property_value ); + +/// @brief Lets you modify specific properties of the different memory +/// caches in the current Houdini context. This includes clearing +/// caches, reducing their memory use, or changing how memory limits +/// are respected by a cache. +/// +/// @ingroup Caching +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] cache_name +/// Cache name from ::HAPI_GetActiveCacheNames(). +/// +/// @param[in] cache_property +/// The specific property of the cache to modify. +/// +/// @param[in] property_value +/// The new property value. +/// +HAPI_DECL HAPI_SetCacheProperty( const HAPI_Session * session, + const char * cache_name, + HAPI_CacheProperty cache_property, + int property_value ); + +/// @brief Saves a geometry to file. The type of file to save is +/// to be determined by the extension ie. .bgeo, .obj +/// +/// @ingroup Caching +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] file_name +/// The name of the file to be saved. The extension +/// of the file determines its type. +/// +HAPI_DECL HAPI_SaveGeoToFile( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * file_name ); + +/// @brief Loads a geometry file and put its contents onto a SOP +/// node. +/// +/// @ingroup Caching +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] file_name +/// The name of the file to be loaded +/// +HAPI_DECL HAPI_LoadGeoFromFile( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * file_name ); + +/// @brief Saves the node and all its contents to file. +/// The saved file can be loaded by calling ::HAPI_LoadNodeFromFile. +/// +/// @ingroup Caching +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] file_name +/// The name of the file to be saved. The extension +/// of the file determines its type. +/// +HAPI_DECL HAPI_SaveNodeToFile( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * file_name ); + +/// @brief Loads and creates a previously saved node and all +/// its contents from given file. +/// The saved file must have been created by calling +/// ::HAPI_SaveNodeToFile. +/// +/// @ingroup Caching +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] file_name +/// The name of the file to be loaded +/// +/// @param[in] parent_node_id +/// The parent node id of the Geometry object. +/// +/// @param[in] node_label +/// The name of the new Geometry object. +/// +/// @param[in] cook_on_load +/// Set to true if you wish the nodes to cook as soon +/// as they are created. Otherwise, you will have to +/// call ::HAPI_CookNode() explicitly for each after you +/// call this function. +/// +/// @param[out] new_node_id +/// The newly created node id. +/// +HAPI_DECL HAPI_LoadNodeFromFile( const HAPI_Session * session, + const char * file_name, + HAPI_NodeId parent_node_id, + const char * node_label, + HAPI_Bool cook_on_load, + HAPI_NodeId * new_node_id ); + +/// @brief Cache the current state of the geo to memory, given the +/// format, and return the size. Use this size with your call +/// to ::HAPI_SaveGeoToMemory() to copy the cached geo to your +/// buffer. It is guaranteed that the size will not change between +/// your call to ::HAPI_GetGeoSize() and ::HAPI_SaveGeoToMemory(). +/// +/// @ingroup Caching +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] format +/// The file format, ie. ".obj", ".bgeo.sc" etc. +/// +/// @param[out] size +/// The size of the buffer required to hold the output. +/// +HAPI_DECL HAPI_GetGeoSize( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * format, + int * size ); + +/// @brief Saves the cached geometry to your buffer in memory, +/// whose format and required size is identified by the call to +/// ::HAPI_GetGeoSize(). The call to ::HAPI_GetGeoSize() is +/// required as ::HAPI_GetGeoSize() does the actual saving work. +/// +/// Also note that this call to ::HAPI_SaveGeoToMemory will delete +/// the internal geo buffer that was cached in the previous call +/// to ::HAPI_GetGeoSize(). This means that you will need to call +/// ::HAPI_GetGeoSize() again before you can call this function. +/// +/// @ingroup Caching +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] buffer +/// The buffer we will write into. +/// +/// @param[in] length +/// The size of the buffer passed in. +/// +/// +HAPI_DECL HAPI_SaveGeoToMemory( const HAPI_Session * session, + HAPI_NodeId node_id, + char * buffer, + int length ); + +/// @brief Loads a geometry from memory and put its +/// contents onto a SOP node. +/// +/// @ingroup Caching +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] format +/// The file format, ie. "obj", "bgeo" etc. +/// +/// @param[in] buffer +/// The buffer we will read the geometry from. +/// +/// @param[in] length +/// The size of the buffer passed in. +/// +/// +HAPI_DECL HAPI_LoadGeoFromMemory( const HAPI_Session * session, + HAPI_NodeId node_id, + const char * format, + const char * buffer, + int length ); + +/// @brief Set the specified node's display flag. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] onOff +/// Display flag. +/// +HAPI_DECL HAPI_SetNodeDisplay( const HAPI_Session * session, + HAPI_NodeId node_id, + int onOff ); + +/// @brief Get the specified node's total cook count, including +/// its children, if specified. +/// +/// @ingroup Nodes +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] node_type_filter +/// The node type by which to filter the children. +/// +/// @param[in] node_flags_filter +/// The node flags by which to filter the children. +/// +/// @param[in] recursive +/// Whether or not to include the specified node's +/// children cook count in the tally. +/// +/// @param[out] count +/// The number of cooks in total for this session. +/// +HAPI_DECL HAPI_GetTotalCookCount( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_NodeTypeBits node_type_filter, + HAPI_NodeFlagsBits node_flags_filter, + HAPI_Bool recursive, + int * count ); + +/// @defgroup SessionSync +/// Functions for working with SessionSync + +/// @brief Enable or disable SessionSync mode. +/// +/// @ingroup SessionSync +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] enable +/// Enable or disable SessionSync mode. +/// +HAPI_DECL HAPI_SetSessionSync( const HAPI_Session * session, + HAPI_Bool enable ); + +/// @brief Get the ::HAPI_Viewport info for synchronizing viewport in +/// SessionSync. When SessionSync is running this will +/// return Houdini's current viewport information. +/// +/// @ingroup SessionSync +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// @param[out] viewport +/// The output ::HAPI_Viewport. +/// +HAPI_DECL HAPI_GetViewport( const HAPI_Session * session, + HAPI_Viewport * viewport ); + +/// @brief Set the ::HAPI_Viewport info for synchronizing viewport in +/// SessionSync. When SessionSync is running, this can be +/// used to set the viewport information which Houdini +/// will then synchronizse with for its viewport. +/// +/// @ingroup SessionSync +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// @param[in] viewport +/// A ::HAPI_Viewport that stores the viewport. +/// +HAPI_DECL HAPI_SetViewport( const HAPI_Session * session, + const HAPI_Viewport * viewport ); + +/// @brief Get the ::HAPI_SessionSyncInfo for synchronizing SessionSync +/// state between Houdini and Houdini Engine integrations. +/// +/// @ingroup SessionSync +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// @param[out] session_sync_info +/// The output ::HAPI_SessionSyncInfo. +/// +HAPI_DECL HAPI_GetSessionSyncInfo( + const HAPI_Session * session, + HAPI_SessionSyncInfo * session_sync_info ); + +/// @brief Set the ::HAPI_SessionSyncInfo for synchronizing SessionSync +/// state between Houdini and Houdini Engine integrations. +/// +/// @ingroup SessionSync +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// @param[in] session_sync_info +/// A ::HAPI_SessionSyncInfo that stores the state. +/// +HAPI_DECL HAPI_SetSessionSyncInfo( + const HAPI_Session * session, + const HAPI_SessionSyncInfo * session_sync_info ); + +/// @defgroup PDG PDG/TOPs +/// Functions for working with PDG/TOPs + +/// @brief Return an array of PDG graph context names and ids, the first +/// count names will be returned. These ids can be used +/// with ::HAPI_GetPDGEvents and ::HAPI_GetPDGState. The values +/// of the names can be retrieved with ::HAPI_GetString. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] context_names_array +/// Array of context names stored as ::HAPI_StringHandle +/// at least the size of length. These can be used +/// with ::HAPI_GetString() and are valid until the +/// next call to this function. +/// +/// @param[out] context_id_array +/// Array of graph context ids at least the size of length. +/// +/// @param[in] start +/// First index of range. Must be at least @c 0 and at most +/// @c context_count - 1 where @c context_count is the count +/// returned by ::HAPI_GetPDGGraphContextsCount() +/// +/// +/// +/// +/// @param[in] length +/// Given @c num_contexts returned by ::HAPI_GetPDGGraphContextsCount(), +/// length should be at least @c 0 and at most num_contexts - start. +/// +HAPI_DECL HAPI_GetPDGGraphContexts( const HAPI_Session * session, + HAPI_StringHandle * context_names_array, + HAPI_PDG_GraphContextId * context_id_array, + int start, + int length ); + +/// @brief Return the total number of PDG graph contexts found. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[out] num_contexts +/// Total PDG graph contexts count. +/// +HAPI_DECL HAPI_GetPDGGraphContextsCount( + const HAPI_Session* session, + int* num_contexts ); + +/// @brief Get the PDG graph context for the specified TOP node. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] top_node_id +/// The id of the TOP node to query its graph context. +/// +/// @param[out] context_id +/// The PDG graph context id. +/// +HAPI_DECL HAPI_GetPDGGraphContextId( const HAPI_Session * session, + HAPI_NodeId top_node_id, + HAPI_PDG_GraphContextId * context_id ); + +/// @brief Starts a PDG cooking operation. This can be asynchronous. +/// Progress can be checked with ::HAPI_GetPDGState() and +/// ::HAPI_GetPDGState(). Events generated during this cook can be +/// collected with ::HAPI_GetPDGEvents(). Any uncollected events will be +/// discarded at the start of the cook. +/// +/// If there are any $HIPFILE file dependencies on nodes involved in the cook +/// a hip file will be automatically saved to $HOUDINI_TEMP_DIR directory so +/// that it can be copied to the working directory by the scheduler. This means +/// $HIP will be equal to $HOUDINI_TEMP_DIR. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// @param[in] cook_node_id +/// The node id of a TOP node for the cook operation. +/// +/// @param[in] generate_only +/// 1 means only static graph generation will done. 0 means +/// a full graph cook. Generation is always blocking. +/// +/// @param[in] blocking +/// 0 means return immediately and cooking will be done +/// asynchronously. 1 means return when cooking completes. +/// +HAPI_DECL HAPI_CookPDG( const HAPI_Session * session, + HAPI_NodeId cook_node_id, + int generate_only, + int blocking ); + +/// @brief Starts a PDG cooking operation. This can be asynchronous. +/// Progress can be checked with ::HAPI_GetPDGState() and +/// ::HAPI_GetPDGState(). Events generated during this cook can be +/// collected with ::HAPI_GetPDGEvents(). Any uncollected events will be +/// discarded at the start of the cook. +/// +/// If there are any $HIPFILE file dependencies on nodes involved in the +/// cook a hip file will be automatically saved to $HOUDINI_TEMP_DIR +/// directory so that it can be copied to the working directory by the +/// scheduler. This means $HIP will be equal to $HOUDINI_TEMP_DIR. +/// +/// If cook_node_id is a network / subnet, then if it has output nodes +/// it cooks all of its output nodes and not just output 0. If it does +/// not have output nodes it cooks the node with the output flag. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// @param[in] cook_node_id +/// The node id of a TOP node for the cook operation. +/// +/// @param[in] generate_only +/// 1 means only static graph generation will done. 0 means +/// a full graph cook. Generation is always blocking. +/// +/// @param[in] blocking +/// 0 means return immediately and cooking will be done +/// asynchronously. 1 means return when cooking completes. +/// +HAPI_DECL HAPI_CookPDGAllOutputs( + const HAPI_Session* session, + HAPI_NodeId cook_node_id, + int generate_only, + int blocking); + +/// @brief Returns PDG events that have been collected. Calling this function +/// will remove those events from the queue. Events collection is restarted +/// by calls to ::HAPI_CookPDG(). +/// +/// @ingroup PDG +/// +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] graph_context_id +/// The id of the graph context +/// +/// @param[out] event_array +/// buffer of ::HAPI_PDG_EventInfo of size at least length. +/// +/// @param[in] length +/// The size of the buffer passed in. +/// +/// @param[out] event_count +/// Number of events removed from queue and copied to buffer. +/// +/// @param[out] remaining_events +/// Number of queued events remaining after this operation. +/// +HAPI_DECL HAPI_GetPDGEvents( const HAPI_Session * session, + HAPI_PDG_GraphContextId graph_context_id, + HAPI_PDG_EventInfo * event_array, + int length, + int * event_count, + int * remaining_events ); + +/// @brief Gets the state of a PDG graph +/// +/// @ingroup PDG +/// +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] graph_context_id +/// The graph context id +/// +/// @param[out] pdg_state +/// One of ::HAPI_PDG_State. +/// +HAPI_DECL HAPI_GetPDGState( const HAPI_Session * session, + HAPI_PDG_GraphContextId graph_context_id, + int * pdg_state ); + +/// @brief Creates a new pending workitem for the given node. The workitem +/// will not be submitted to the graph until it is committed with +/// ::HAPI_CommitWorkitems(). The node is expected to be a generator type. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] workitem_id +/// The id of the pending workitem. +/// +/// @param[in] name +/// The null-terminated name of the workitem. The name will +/// be automatically suffixed to make it unique. +/// +/// @param[in] index +/// The index of the workitem. The semantics of the index +/// are user defined. +/// +HAPI_DECL_DEPRECATED_REPLACE(5.0.0, 19.5.161, HAPI_CreateWorkItem) +HAPI_CreateWorkitem( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId * workitem_id, + const char * name, + int index ); + +/// @brief Retrieves the info of a given workitem by id. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] graph_context_id +/// The graph context that the workitem is in. +/// +/// @param[in] workitem_id +/// The id of the workitem. +/// +/// @param[out] workitem_info +/// The returned ::HAPI_PDG_WorkItemInfo for the workitem. Note +/// that the enclosed string handle is only valid until the next +/// call to this function. +/// +HAPI_DECL_DEPRECATED_REPLACE(5.0.0, 19.5.161, HAPI_GetWorkItemInfo) +HAPI_GetWorkitemInfo( const HAPI_Session * session, + HAPI_PDG_GraphContextId graph_context_id, + HAPI_PDG_WorkItemId workitem_id, + HAPI_PDG_WorkItemInfo * workitem_info ); + +/// @brief Adds integer data to a pending PDG workitem data member for the given node. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] workitem_id +/// The id of the pending workitem returned by ::HAPI_CreateWorkitem() +/// +/// @param[in] data_name +/// null-terminated name of the data member +/// +/// @param[in] values_array +/// array of integer values +/// +/// @param[in] length +/// number of values to copy from values_array to the parameter +/// +HAPI_DECL_DEPRECATED_REPLACE(5.0.0, 19.5.161, HAPI_SetWorkItemIntAttribute) +HAPI_SetWorkitemIntData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId workitem_id, + const char * data_name, + const int * values_array, + int length ); + +/// @brief Adds float data to a pending PDG workitem data member for the given node. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] workitem_id +/// The id of the pending workitem returned by ::HAPI_CreateWorkitem() +/// +/// @param[in] data_name +/// null-terminated name of the workitem data member +/// +/// @param[in] values_array +/// array of float values +/// +/// @param[in] length +/// number of values to copy from values_array to the parameter +/// +HAPI_DECL_DEPRECATED_REPLACE(5.0.0, 19.5.161, HAPI_SetWorkItemFloatAttribute) +HAPI_SetWorkitemFloatData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId workitem_id, + const char * data_name, + const float * values_array, + int length ); + +/// @brief Adds integer data to a pending PDG workitem data member for the given node. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] workitem_id +/// The id of the created workitem returned by HAPI_CreateWorkitem() +/// +/// @param[in] data_name +/// null-terminated name of the data member +/// +/// @param[in] data_index +/// index of the string data member +/// +/// @param[in] value +/// null-terminated string to copy to the workitem data member +/// +HAPI_DECL_DEPRECATED_REPLACE(5.0.0, 19.5.161, HAPI_SetWorkItemStringAttribute) +HAPI_SetWorkitemStringData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId workitem_id, + const char * data_name, + int data_index, + const char * value ); + +/// @brief Commits any pending workitems. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id for which the pending workitems have been +/// created but not yet injected. +/// +HAPI_DECL_DEPRECATED_REPLACE(5.0.0, 19.5.161, HAPI_CommitWorkItems) +HAPI_CommitWorkitems( const HAPI_Session * session, + HAPI_NodeId node_id ); + +/// @brief Gets the number of workitems that are available on the given node. +/// Should be used with ::HAPI_GetWorkitems. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] num +/// The number of workitems. +/// +HAPI_DECL_DEPRECATED_REPLACE(5.0.0, 19.5.161, HAPI_GetNumWorkItems) +HAPI_GetNumWorkitems( const HAPI_Session * session, + HAPI_NodeId node_id, + int * num ); + +/// @brief Gets the list of work item ids for the given node +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] workitem_ids_array +/// buffer for resulting array of ::HAPI_PDG_WorkItemId +/// +/// @param[in] length +/// The length of the @p workitem_ids buffer +/// +HAPI_DECL_DEPRECATED_REPLACE(5.0.0, 19.5.161, HAPI_GetWorkItems) +HAPI_GetWorkitems( const HAPI_Session * session, + HAPI_NodeId node_id, + int * workitem_ids_array, + int length ); + +/// @brief Gets the length of the workitem data member. +/// It is the length of the array of data. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] workitem_id +/// The id of the workitem +/// +/// @param[in] data_name +/// null-terminated name of the data member +/// +/// @param[out] length +/// The length of the data member array +/// +HAPI_DECL_DEPRECATED_REPLACE(5.0.0, 19.5.161, HAPI_GetWorkItemDataSize) +HAPI_GetWorkitemDataLength( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId workitem_id, + const char * data_name, + int * length ); + +/// @brief Gets int data from a work item member. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] workitem_id +/// The id of the workitem +/// +/// @param[in] data_name +/// null-terminated name of the data member +/// +/// @param[out] data_array +/// buffer of at least size length to copy the data into. The required +/// length should be determined by ::HAPI_GetWorkitemDataLength(). +/// +/// @param[in] length +/// The length of @p data_array +/// +HAPI_DECL_DEPRECATED_REPLACE(5.0.0, 19.5.161, HAPI_GetWorkItemIntAttribute) +HAPI_GetWorkitemIntData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId workitem_id, + const char * data_name, + int * data_array, + int length ); + +/// @brief Gets float data from a work item member. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] workitem_id +/// The id of the workitem +/// +/// @param[in] data_name +/// null-terminated name of the data member +/// +/// @param[out] data_array +/// buffer of at least size length to copy the data into. The required +/// length should be determined by ::HAPI_GetWorkitemDataLength(). +/// +/// @param[in] length +/// The length of the @p data_array +/// +HAPI_DECL_DEPRECATED_REPLACE(5.0.0, 19.5.161, HAPI_GetWorkItemFloatAttribute) +HAPI_GetWorkitemFloatData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId workitem_id, + const char * data_name, + float * data_array, + int length ); + +/// @brief Gets string ids from a work item member. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] workitem_id +/// The id of the workitem +/// +/// @param[in] data_name +/// null-terminated name of the data member +/// +/// @param[out] data_array +/// buffer of at least size length to copy the data into. The required +/// length should be determined by ::HAPI_GetWorkitemDataLength(). +/// The data is an array of ::HAPI_StringHandle which can be used with +/// ::HAPI_GetString(). The string handles are valid until the +/// next call to this function. +/// +/// @param[in] length +/// The length of @p data_array +/// +HAPI_DECL_DEPRECATED_REPLACE(5.0.0, 19.5.161, HAPI_GetWorkItemStringAttribute) +HAPI_GetWorkitemStringData( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId workitem_id, + const char * data_name, + HAPI_StringHandle * data_array, + int length ); + +/// @brief Gets the info for workitem results. +/// The number of workitem results is found on the ::HAPI_PDG_WorkItemInfo +/// returned by ::HAPI_GetWorkitemInfo() +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] workitem_id +/// The id of the workitem +/// +/// @param[out] resultinfo_array +/// Buffer to fill with info structs. String handles are valid +/// until the next call of this function. +/// +/// @param[in] resultinfo_count +/// The length of @p resultinfo_array +/// +HAPI_DECL_DEPRECATED_REPLACE(5.0.0, 19.5.161, HAPI_GetWorkItemOutputFiles) +HAPI_GetWorkitemResultInfo( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId workitem_id, + HAPI_PDG_WorkItemOutputFile * resultinfo_array, + int resultinfo_count ); + +/// @brief Creates a new pending work item for the given node. The work item +/// will not be submitted to the graph until it is committed with +/// ::HAPI_CommitWorkItems(). The node is expected to be a generator type. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] work_item_id +/// The id of the pending workitem. +/// +/// @param[in] name +/// The null-terminated name of the work item. The name will +/// be automatically suffixed to make it unique. +/// +/// @param[in] index +/// The index of the work item. The semantics of the index +/// are user defined. +/// +HAPI_DECL HAPI_CreateWorkItem( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId * work_item_id, + const char * name, + int index ); + +/// @brief Retrieves the info of a given work item by id. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] graph_context_id +/// The graph context that the work item is in. +/// +/// @param[in] work_item_id +/// The id of the work item. +/// +/// @param[out] work_item_info +/// The returned ::HAPI_PDG_WorkItemInfo for the work item. Note +/// that the enclosed string handle is only valid until the next +/// call to this function. +/// +HAPI_DECL HAPI_GetWorkItemInfo( const HAPI_Session * session, + HAPI_PDG_GraphContextId graph_context_id, + HAPI_PDG_WorkItemId work_item_id, + HAPI_PDG_WorkItemInfo * work_item_info ); + +/// @brief Adds integer data to a pending PDG work item attribute for the given node. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] work_item_id +/// The id of the pending work item returned by ::HAPI_CreateWorkItem() +/// +/// @param[in] attribute_name +/// null-terminated name of the work item attribute +/// +/// @param[in] values_array +/// array of integer values +/// +/// @param[in] length +/// number of values to copy from values_array to the parameter +/// +HAPI_DECL HAPI_SetWorkItemIntAttribute( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId work_item_id, + const char * attribute_name, + const int * values_array, + int length ); + +/// @brief Adds float data to a pending PDG work item attribute for the given node. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] work_item_id +/// The id of the pending work item returned by ::HAPI_CreateWorkItem() +/// +/// @param[in] attribute_name +/// null-terminated name of the work item attribute +/// +/// @param[in] values_array +/// array of float values +/// +/// @param[in] length +/// number of values to copy from values_array to the parameter +/// +HAPI_DECL HAPI_SetWorkItemFloatAttribute( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId work_item_id, + const char * attribute_name, + const float * values_array, + int length ); + +/// @brief Adds integer data to a pending PDG work item attribute for the given node. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] work_item_id +/// The id of the created work item returned by HAPI_CreateWorkItem() +/// +/// @param[in] attribute_name +/// null-terminated name of the work item attribute +/// +/// @param[in] data_index +/// index of the string data member +/// +/// @param[in] value +/// null-terminated string to copy to the work item data member +/// +HAPI_DECL HAPI_SetWorkItemStringAttribute( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId work_item_id, + const char * attribute_name, + int data_index, + const char * value ); + +/// @brief Commits any pending work items. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id for which the pending work items have been +/// created but not yet injected. +/// +HAPI_DECL HAPI_CommitWorkItems( const HAPI_Session * session, + HAPI_NodeId node_id ); + +/// @brief Gets the number of work items that are available on the given node. +/// Should be used with ::HAPI_GetWorkItems. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] num +/// The number of work items. +/// +HAPI_DECL HAPI_GetNumWorkItems( const HAPI_Session * session, + HAPI_NodeId node_id, + int * num ); + +/// @brief Gets the list of work item ids for the given node +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[out] work_item_ids_array +/// buffer for resulting array of ::HAPI_PDG_WorkItemId +/// +/// @param[in] length +/// The length of the @p work_item_ids buffer +/// +HAPI_DECL HAPI_GetWorkItems( const HAPI_Session * session, + HAPI_NodeId node_id, + int * work_item_ids_array, + int length ); + +/// @brief Gets the size of the work item attribute. +/// It is the length of the array of data. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] work_item_id +/// The id of the work item +/// +/// @param[in] attribute_name +/// null-terminated name of the work item attribute +/// +/// @param[out] length +/// The length of the data member array +/// +HAPI_DECL HAPI_GetWorkItemAttributeSize( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId work_item_id, + const char * attribute_name, + int * length ); + +/// @brief Gets int data from a work item attribute. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] work_item_id +/// The id of the work_item +/// +/// @param[in] attribute_name +/// null-terminated name of the work item attribute +/// +/// @param[out] data_array +/// buffer of at least size length to copy the data into. The required +/// length should be determined by ::HAPI_GetWorkItemDataLength(). +/// +/// @param[in] length +/// The length of @p data_array +/// +HAPI_DECL HAPI_GetWorkItemIntAttribute( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId work_item_id, + const char * attribute_name, + int * data_array, + int length ); + +/// @brief Gets float data from a work item attribute. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] work_item_id +/// The id of the work_item +/// +/// @param[in] attribute_name +/// null-terminated name of the work item attribute +/// +/// @param[out] data_array +/// buffer of at least size length to copy the data into. The required +/// length should be determined by ::HAPI_GetWorkItemDataLength(). +/// +/// @param[in] length +/// The length of the @p data_array +/// +HAPI_DECL HAPI_GetWorkItemFloatAttribute( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId work_item_id, + const char * attribute_name, + float * data_array, + int length ); + +/// @brief Gets string ids from a work item attribute. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] work_item_id +/// The id of the work item +/// +/// @param[in] attribute_name +/// null-terminated name of the work item attribute +/// +/// @param[out] data_array +/// buffer of at least size length to copy the data into. The required +/// length should be determined by ::HAPI_GetWorkItemDataLength(). +/// The data is an array of ::HAPI_StringHandle which can be used with +/// ::HAPI_GetString(). The string handles are valid until the +/// next call to this function. +/// +/// @param[in] length +/// The length of @p data_array +/// +HAPI_DECL HAPI_GetWorkItemStringAttribute( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId work_item_id, + const char * attribute_name, + HAPI_StringHandle * data_array, + int length ); + +/// @brief Gets the info for work item output files. +/// The number of work item results is found on the ::HAPI_PDG_WorkItemInfo +/// returned by ::HAPI_GetWorkItemInfo() +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] work_item_id +/// The id of the work item +/// +/// @param[out] resultinfo_array +/// Buffer to fill with info structs. String handles are valid +/// until the next call of this function. +/// +/// @param[in] resultinfo_count +/// The length of @p resultinfo_array +/// +HAPI_DECL HAPI_GetWorkItemOutputFiles( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_PDG_WorkItemId work_item_id, + HAPI_PDG_WorkItemOutputFile * resultinfo_array, + int resultinfo_count ); + +/// @brief Dirties the given node. Cancels the cook if necessary and then +/// deletes all workitems on the node. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] node_id +/// The node id. +/// +/// @param[in] clean_results +/// Remove the results generated by the node. +/// +/// +HAPI_DECL HAPI_DirtyPDGNode( const HAPI_Session * session, + HAPI_NodeId node_id, + HAPI_Bool clean_results ); + +/// @brief Pause the PDG cooking operation. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] graph_context_id +/// The id of the graph context +/// +HAPI_DECL HAPI_PausePDGCook( const HAPI_Session * session, + HAPI_PDG_GraphContextId graph_context_id ); + +/// @brief Cancel the PDG cooking operation. +/// +/// @ingroup PDG +/// +/// @param[in] session +/// The session of Houdini you are interacting with. +/// See @ref HAPI_Sessions for more on sessions. +/// Pass NULL to just use the default in-process session. +/// +/// +/// @param[in] graph_context_id +/// The id of the graph context +/// +HAPI_DECL HAPI_CancelPDGCook( const HAPI_Session * session, + HAPI_PDG_GraphContextId graph_context_id ); + +#endif // __HAPI_h__ + +``` + +`Source/HoudiniEngine/Public/HAPI/HAPI_API.h`: + +```h +/* + * PROPRIETARY INFORMATION. This software is proprietary to + * Side Effects Software Inc., and is not to be reproduced, + * transmitted, or disclosed in any way without written permission. + * + * Produced by: + * Side Effects Software Inc + * 123 Front Street West, Suite 1401 + * Toronto, Ontario + * Canada M5J 2M2 + * 416-504-9876 + * + * COMMENTS: + */ + +#ifndef __HAPI_API_h__ +#define __HAPI_API_h__ + +// Helper macros. +#if defined( __GNUC__ ) + #define HAPI_IS_GCC_GE( MAJOR, MINOR ) \ + ( __GNUC__ > MAJOR || (__GNUC__ == MAJOR && __GNUC_MINOR__ >= MINOR) ) +#else + #define HAPI_IS_GCC_GE( MAJOR, MINOR ) 0 +#endif // defined( __GNUC__ ) +#define HAPI_TO_STRING_( a ) # a +#define HAPI_TO_STRING( a ) HAPI_TO_STRING_( a ) + +// Mark function as deprecated and may be removed in the future. +// @note This qualifier can only appear in function declarations. +#ifdef HAPI_SUPPRESS_DEPRECATIONS + #define HAPI_DEPRECATED( hapi_ver, houdini_ver ) + #define HAPI_DEPRECATED_REPLACE( hapi_ver, houdini_ver, replacement ) +#elif defined( GCC3 ) + #if HAPI_IS_GCC_GE( 4, 6 ) || defined( __clang__ ) + #define HAPI_DEPRECATED( hapi_ver, houdini_ver ) \ + __attribute__( ( deprecated( \ + "Deprecated since version HAPI " HAPI_TO_STRING( hapi_ver ) \ + ", Houdini " HAPI_TO_STRING( houdini_ver ) ) ) ) + #define HAPI_DEPRECATED_REPLACE( hapi_ver, houdini_ver, replacement ) \ + __attribute__( ( deprecated( \ + "Deprecated since version HAPI " HAPI_TO_STRING( hapi_ver ) \ + ", Houdini " HAPI_TO_STRING( houdini_ver ) ". Use " \ + HAPI_TO_STRING( replacement ) " instead." ) ) ) + #else + #define HAPI_DEPRECATED( hapi_ver, houdini_ver ) \ + __attribute__( ( deprecated ) ) + #define HAPI_DEPRECATED_REPLACE( hapi_ver, houdini_ver, replacement ) \ + __attribute__( ( deprecated ) ) + #endif +#elif defined( _MSC_VER ) + #define HAPI_DEPRECATED( hapi_ver, houdini_ver ) \ + __declspec( deprecated( \ + "Deprecated since version HAPI " HAPI_TO_STRING( hapi_ver ) \ + ", Houdini " HAPI_TO_STRING( houdini_ver ) ) ) + #define HAPI_DEPRECATED_REPLACE( hapi_ver, houdini_ver, replacement ) \ + __declspec( deprecated( \ + "Deprecated since version HAPI " HAPI_TO_STRING( hapi_ver ) \ + ", Houdini " HAPI_TO_STRING( houdini_ver ) ". Use " \ + HAPI_TO_STRING( replacement ) " instead." ) ) +#else + #define HAPI_DEPRECATED( hapi_ver, houdini_ver ) + #define HAPI_DEPRECATED_REPLACE( hapi_ver, houdini_ver, replacement ) +#endif + +#if defined( WIN32 ) && !defined( MAKING_STATIC ) + #define HAPI_VISIBILITY_EXPORT __declspec( dllexport ) + #define HAPI_VISIBILITY_IMPORT __declspec( dllimport ) + #define HAPI_VISIBILITY_EXPORT_TINST HAPI_VISIBILITY_EXPORT + #define HAPI_VISIBILITY_IMPORT_TINST HAPI_VISIBILITY_IMPORT +#elif defined(__GNUC__) && HAPI_IS_GCC_GE( 4, 2 ) && !defined( MAKING_STATIC ) + #define HAPI_VISIBILITY_EXPORT __attribute__( ( visibility( "default" ) ) ) + #define HAPI_VISIBILITY_IMPORT __attribute__( ( visibility( "default" ) ) ) + #define HAPI_VISIBILITY_EXPORT_TINST + #define HAPI_VISIBILITY_IMPORT_TINST +#else + #define HAPI_VISIBILITY_EXPORT + #define HAPI_VISIBILITY_IMPORT + #define HAPI_VISIBILITY_EXPORT_TINST + #define HAPI_VISIBILITY_IMPORT_TINST +#endif + +#ifdef HAPI_EXPORTS + #define HAPI_API HAPI_VISIBILITY_EXPORT +#else + #define HAPI_API HAPI_VISIBILITY_IMPORT +#endif // HAPI_EXPORTS + +#if defined( WIN32 ) + #define HAPI_CALL __cdecl +#else + #define HAPI_CALL +#endif // defined( WIN32 ) + +#ifdef __cplusplus + #define HAPI_EXTERN_C extern "C" +#else + #define HAPI_EXTERN_C +#endif // __cplusplus + +#define HAPI_RETURN HAPI_Result HAPI_CALL + +#define HAPI_DECL_RETURN( r ) HAPI_EXTERN_C HAPI_API r HAPI_CALL +#define HAPI_DECL_DEPRECATED( hapi_ver, houdini_ver ) \ + HAPI_EXTERN_C \ + HAPI_DEPRECATED( hapi_ver, houdini_ver ) \ + HAPI_API \ + HAPI_RETURN +#define HAPI_DECL_DEPRECATED_REPLACE( hapi_ver, houdini_ver, replacement ) \ + HAPI_EXTERN_C \ + HAPI_DEPRECATED_REPLACE( hapi_ver, houdini_ver, replacement ) \ + HAPI_API \ + HAPI_RETURN + +#define HAPI_DECL HAPI_EXTERN_C HAPI_API HAPI_RETURN +#define HAPI_DEF HAPI_EXTERN_C HAPI_RETURN + +// Static asserts +#ifdef __cplusplus +#define HAPI_STATIC_ASSERT( cond, msg ) \ + template< bool b > \ + struct HAPI_StaticAssert_##msg {}; \ + template<> \ + struct HAPI_StaticAssert_##msg< true > \ + { \ + typedef int static_assert_##msg; \ + }; \ + typedef HAPI_StaticAssert_##msg< ( cond ) >::static_assert_##msg _sa +#else + #define HAPI_STATIC_ASSERT( cond, msg ) HAPI_STATIC_ASSERT_I( cond, msg ) + #define HAPI_STATIC_ASSERT_I( cond, msg ) \ + typedef char static_assert_##msg[ 2 * ( cond ) - 1 ] +#endif // __cplusplus + +#endif // __HAPI_API_h__ + +``` + +`Source/HoudiniEngine/Public/HAPI/HAPI_Common.h`: + +```h +/* + * PROPRIETARY INFORMATION. This software is proprietary to + * Side Effects Software Inc., and is not to be reproduced, + * transmitted, or disclosed in any way without written permission. + * + */ + +#ifndef __HAPI_COMMON_h__ +#define __HAPI_COMMON_h__ + +#include "HAPI_API.h" + +///////////////////////////////////////////////////////////////////////////// +// Defines + +#define HAPI_POSITION_VECTOR_SIZE 3 +#define HAPI_SCALE_VECTOR_SIZE 3 +#define HAPI_SHEAR_VECTOR_SIZE 3 +#define HAPI_NORMAL_VECTOR_SIZE 3 +#define HAPI_QUATERNION_VECTOR_SIZE 4 +#define HAPI_EULER_VECTOR_SIZE 3 +#define HAPI_UV_VECTOR_SIZE 2 +#define HAPI_COLOR_VECTOR_SIZE 4 +#define HAPI_CV_VECTOR_SIZE 4 + +#define HAPI_PRIM_MIN_VERTEX_COUNT 1 +#define HAPI_PRIM_MAX_VERTEX_COUNT 16 + +#define HAPI_INVALID_PARM_ID -1 + +#define HAPI_MAX_NUM_CONNECTIONS 128 + +/// Common Default Attributes' Names +/// @{ +#define HAPI_ATTRIB_POSITION "P" +#define HAPI_ATTRIB_UV "uv" +#define HAPI_ATTRIB_UV2 "uv2" +#define HAPI_ATTRIB_NORMAL "N" +#define HAPI_ATTRIB_TANGENT "tangentu" +#define HAPI_ATTRIB_TANGENT2 "tangentv" +#define HAPI_ATTRIB_COLOR "Cd" +#define HAPI_ATTRIB_NAME "name" +#define HAPI_ATTRIB_INSTANCE "instance" +#define HAPI_ATTRIB_ROT "rot" +#define HAPI_ATTRIB_SCALE "scale" +/// @} + +/// This is the name of the primitive group created from all the primitives +/// that are not in any user-defined group. This way, when you put all the +/// groups together you cover the entire mesh. This is important for some +/// clients where the mesh has to be defined in terms of submeshes that cover +/// the entire original mesh. +#define HAPI_UNGROUPED_GROUP_NAME "__ungrouped_group" + +/// Common image file format names (to use with the material extract APIs). +/// Note that you may still want to check if they are supported via +/// HAPI_GetSupportedImageFileFormats() since all formats are loaded +/// dynamically by Houdini on-demand so just because these formats are defined +/// here doesn't mean they are supported in your instance. +/// @{ +#define HAPI_RAW_FORMAT_NAME "HAPI_RAW" // HAPI-only Raw Format +#define HAPI_PNG_FORMAT_NAME "PNG" +#define HAPI_JPEG_FORMAT_NAME "JPEG" +#define HAPI_BMP_FORMAT_NAME "Bitmap" +#define HAPI_TIFF_FORMAT_NAME "TIFF" +#define HAPI_TGA_FORMAT_NAME "Targa" +/// @} + +/// Default image file format's name - used when the image generated and has +/// no "original" file format and the user does not specify a format to +/// convert to. +#define HAPI_DEFAULT_IMAGE_FORMAT_NAME HAPI_PNG_FORMAT_NAME + +/// Name of subnet OBJ node containing the global nodes. +#define HAPI_GLOBAL_NODES_NODE_NAME "GlobalNodes" + +/// Environment variables. +#define HAPI_ENV_HIP "HIP" +#define HAPI_ENV_JOB "JOB" +#define HAPI_ENV_CLIENT_NAME "HAPI_CLIENT_NAME" + +/// [HAPI_CACHE] +/// Common cache names. You can see these same cache names in the +/// Cache Manager window in Houdini (Windows > Cache Manager). +#define HAPI_CACHE_COP2_COOK "Old COP Cook Cache" +#define HAPI_CACHE_COP2_FLIPBOOK "Composite View Cache" +#define HAPI_CACHE_IMAGE "Image Cache" +#define HAPI_CACHE_OBJ "Object Transform Cache" +#define HAPI_CACHE_GL_TEXTURE "OpenGL Texture Cache" +#define HAPI_CACHE_GL_VERTEX "OpenGL Vertex Cache" +#define HAPI_CACHE_SOP "SOP Cache" +#define HAPI_CACHE_VEX "VEX File Cache" +/// [HAPI_CACHE] + +/// [HAPI_InputCurve] +/// HAPI input curve attribute +#define HAPI_ATTRIB_INPUT_CURVE_COORDS "hapi_input_curve_coords" +/// [HAPI_InputCurve] + +// Make sure our enums and structs are usable without those keywords, as-is, +// in C. +#ifdef __cplusplus + #define HAPI_C_ENUM_TYPEDEF( enum_name ) + #define HAPI_C_STRUCT_TYPEDEF( struct_name ) +#else + #define HAPI_C_ENUM_TYPEDEF( enum_name ) \ + typedef enum enum_name enum_name; + #define HAPI_C_STRUCT_TYPEDEF( struct_name ) \ + typedef struct struct_name struct_name; +#endif // __cplusplus + +///////////////////////////////////////////////////////////////////////////// +// Typedefs + +// C has no bool. +#ifdef __cplusplus + typedef bool HAPI_Bool; +#else + typedef char HAPI_Bool; +#endif // __cplusplus + +// x-bit Integers +// Thrift doesn't support unsigned integers, so we cast it as a signed 8-bit int, but only +// for automated code generation and thrift +#ifdef HAPI_AUTOGEN + typedef signed char int8_t; + typedef short int16_t; + typedef long long int64_t; + typedef signed char HAPI_UInt8; // signed 8-bit int for thrift to avoid overflow +#else + #include + #ifdef HAPI_THRIFT_ABI + typedef int8_t HAPI_UInt8; // signed 8-bit int for thrift to avoid overflow + #else + typedef uint8_t HAPI_UInt8; + HAPI_STATIC_ASSERT(sizeof(HAPI_UInt8) == 1, unsupported_size_of_uint8); + #endif +#endif + +typedef int8_t HAPI_Int8; +HAPI_STATIC_ASSERT(sizeof(HAPI_Int8) == 1, unsupported_size_of_int8); +typedef int16_t HAPI_Int16; +HAPI_STATIC_ASSERT(sizeof(HAPI_Int16) == 2, unsupported_size_of_int16); +typedef int64_t HAPI_Int64; +HAPI_STATIC_ASSERT(sizeof(HAPI_Int64) == 8, unsupported_size_of_long); + + +// The process id has to be uint on Windows and int on any other platform. +#if ( defined _WIN32 || defined WIN32 ) + typedef unsigned int HAPI_ProcessId; +#else + typedef int HAPI_ProcessId; +#endif + +/// Has to be 64-bit. +typedef HAPI_Int64 HAPI_SessionId; + +/// Use this with HAPI_GetString() to get the value. +/// See @ref HAPI_Fundamentals_Strings. +typedef int HAPI_StringHandle; + +typedef int HAPI_AssetLibraryId; + +/// See @ref HAPI_Nodes_Basics. +typedef int HAPI_NodeId; + +/// Either get this from the ::HAPI_ParmInfo or ::HAPI_GetParmIdFromName(). +/// See @ref HAPI_Parameters. +typedef int HAPI_ParmId; + +/// Use this with ::HAPI_GetPartInfo(). +/// See @ref HAPI_Parts. +typedef int HAPI_PartId; + +/// Use this with PDG functions +typedef int HAPI_PDG_WorkItemId; + +/// Backwards compatibility for HAPI_PDG_WorkitemId +typedef HAPI_PDG_WorkItemId HAPI_PDG_WorkitemId; + +/// Use this with PDG functions +typedef int HAPI_PDG_GraphContextId; + +/// When we load a HIP file, we associate a HIP file ID with the created nodes +/// so that they can be looked up later +typedef int HAPI_HIPFileId; + +///////////////////////////////////////////////////////////////////////////// +// Enums + +enum HAPI_License +{ + HAPI_LICENSE_NONE, + HAPI_LICENSE_HOUDINI_ENGINE, + HAPI_LICENSE_HOUDINI, + HAPI_LICENSE_HOUDINI_FX, + HAPI_LICENSE_HOUDINI_ENGINE_INDIE, + HAPI_LICENSE_HOUDINI_INDIE, + HAPI_LICENSE_HOUDINI_ENGINE_UNITY_UNREAL, + HAPI_LICENSE_HOUDINI_EDUCATION, + HAPI_LICENSE_HOUDINI_ENGINE_EDUCATION, + HAPI_LICENSE_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_License ) + +enum HAPI_StatusType +{ + HAPI_STATUS_CALL_RESULT, + HAPI_STATUS_COOK_RESULT, + HAPI_STATUS_COOK_STATE, + HAPI_STATUS_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_StatusType ) + +enum HAPI_StatusVerbosity +{ + HAPI_STATUSVERBOSITY_0, + HAPI_STATUSVERBOSITY_1, + HAPI_STATUSVERBOSITY_2, + + /// Used for Results. Equivalent to ::HAPI_STATUSVERBOSITY_2 + HAPI_STATUSVERBOSITY_ALL = HAPI_STATUSVERBOSITY_2, + /// Used for Results. Equivalent to ::HAPI_STATUSVERBOSITY_0 + HAPI_STATUSVERBOSITY_ERRORS = HAPI_STATUSVERBOSITY_0, + /// Used for Results. Equivalent to ::HAPI_STATUSVERBOSITY_1 + HAPI_STATUSVERBOSITY_WARNINGS = HAPI_STATUSVERBOSITY_1, + /// Used for Results. Equivalent to ::HAPI_STATUSVERBOSITY_2 + HAPI_STATUSVERBOSITY_MESSAGES = HAPI_STATUSVERBOSITY_2, +}; +HAPI_C_ENUM_TYPEDEF( HAPI_StatusVerbosity ) + +enum HAPI_JobStatus +{ + HAPI_JOB_STATUS_RUNNING, + HAPI_JOB_STATUS_IDLE, + HAPI_JOB_STATUS_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_JobStatus ) + +enum HAPI_Result +{ + HAPI_RESULT_SUCCESS = 0, + HAPI_RESULT_FAILURE = 1, + HAPI_RESULT_ALREADY_INITIALIZED = 2, + HAPI_RESULT_NOT_INITIALIZED = 3, + HAPI_RESULT_CANT_LOADFILE = 4, + HAPI_RESULT_PARM_SET_FAILED = 5, + HAPI_RESULT_INVALID_ARGUMENT = 6, + HAPI_RESULT_CANT_LOAD_GEO = 7, + HAPI_RESULT_CANT_GENERATE_PRESET = 8, + HAPI_RESULT_CANT_LOAD_PRESET = 9, + HAPI_RESULT_ASSET_DEF_ALREADY_LOADED = 10, + + HAPI_RESULT_NO_LICENSE_FOUND = 110, + HAPI_RESULT_DISALLOWED_NC_LICENSE_FOUND = 120, + HAPI_RESULT_DISALLOWED_NC_ASSET_WITH_C_LICENSE = 130, + HAPI_RESULT_DISALLOWED_NC_ASSET_WITH_LC_LICENSE = 140, + HAPI_RESULT_DISALLOWED_LC_ASSET_WITH_C_LICENSE = 150, + HAPI_RESULT_DISALLOWED_HENGINEINDIE_W_3PARTY_PLUGIN = 160, + HAPI_RESULT_SHARED_MEMORY_BUFFER_OVERFLOW = 170, + HAPI_RESULT_INVALID_SHARED_MEMORY_BUFFER = 180, + + HAPI_RESULT_ASSET_INVALID = 200, + HAPI_RESULT_NODE_INVALID = 210, + + HAPI_RESULT_USER_INTERRUPTED = 300, + + HAPI_RESULT_INVALID_SESSION = 400 +}; +HAPI_C_ENUM_TYPEDEF( HAPI_Result ) + +enum HAPI_ErrorCode +{ + HAPI_ERRORCODE_ASSET_DEF_NOT_FOUND = 1 << 0, + HAPI_ERRORCODE_PYTHON_NODE_ERROR = 1 << 1 +}; +HAPI_C_ENUM_TYPEDEF( HAPI_ErrorCode ) +typedef int HAPI_ErrorCodeBits; + +enum HAPI_SessionType +{ + HAPI_SESSION_INPROCESS, + HAPI_SESSION_THRIFT, + HAPI_SESSION_CUSTOM1, + HAPI_SESSION_CUSTOM2, + HAPI_SESSION_CUSTOM3, + HAPI_SESSION_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_SessionType ) + +enum HAPI_State +{ + /// Everything cook successfully without errors + HAPI_STATE_READY, + /// You should abort the cook. + HAPI_STATE_READY_WITH_FATAL_ERRORS, + /// Only some objects failed. + HAPI_STATE_READY_WITH_COOK_ERRORS, + HAPI_STATE_STARTING_COOK, + HAPI_STATE_COOKING, + HAPI_STATE_STARTING_LOAD, + HAPI_STATE_LOADING, + HAPI_STATE_MAX, + + HAPI_STATE_MAX_READY_STATE = HAPI_STATE_READY_WITH_COOK_ERRORS +}; +HAPI_C_ENUM_TYPEDEF( HAPI_State ) + +enum HAPI_PackedPrimInstancingMode +{ + HAPI_PACKEDPRIM_INSTANCING_MODE_INVALID = -1, + HAPI_PACKEDPRIM_INSTANCING_MODE_DISABLED, + HAPI_PACKEDPRIM_INSTANCING_MODE_HIERARCHY, + HAPI_PACKEDPRIM_INSTANCING_MODE_FLAT, + HAPI_PACKEDPRIM_INSTANCING_MODE_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_PackedPrimInstancingMode ) + +enum HAPI_Permissions +{ + HAPI_PERMISSIONS_NON_APPLICABLE, + HAPI_PERMISSIONS_READ_WRITE, + HAPI_PERMISSIONS_READ_ONLY, + HAPI_PERMISSIONS_WRITE_ONLY, + HAPI_PERMISSIONS_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_Permissions ) + +enum HAPI_RampType +{ + HAPI_RAMPTYPE_INVALID = -1, + HAPI_RAMPTYPE_FLOAT, + HAPI_RAMPTYPE_COLOR, + HAPI_RAMPTYPE_MAX, +}; +HAPI_C_ENUM_TYPEDEF( HAPI_RampType ) + +/// Represents the data type of a parm. +/// As you can see, some of these high level types share the same underlying +/// raw data type. For instance, both string and file parameter types can be +/// represented with strings, yet semantically they are different. We will +/// group high level parameter types that share an underlying raw data type +/// together, so you can always check the raw data type of a parameter based +/// on its high level data type by checking a range of values. +enum HAPI_ParmType +{ + /// @{ + HAPI_PARMTYPE_INT = 0, + HAPI_PARMTYPE_MULTIPARMLIST, + HAPI_PARMTYPE_TOGGLE, + HAPI_PARMTYPE_BUTTON, + /// }@ + + /// @{ + HAPI_PARMTYPE_FLOAT, + HAPI_PARMTYPE_COLOR, + /// @} + + /// @{ + HAPI_PARMTYPE_STRING, + HAPI_PARMTYPE_PATH_FILE, + HAPI_PARMTYPE_PATH_FILE_GEO, + HAPI_PARMTYPE_PATH_FILE_IMAGE, + /// @} + + HAPI_PARMTYPE_NODE, + + /// @{ + HAPI_PARMTYPE_FOLDERLIST, + HAPI_PARMTYPE_FOLDERLIST_RADIO, + /// @} + + /// @{ + HAPI_PARMTYPE_FOLDER, + HAPI_PARMTYPE_LABEL, + HAPI_PARMTYPE_SEPARATOR, + HAPI_PARMTYPE_PATH_FILE_DIR, + /// @} + + // Helpers + + /// Total number of supported parameter types. + HAPI_PARMTYPE_MAX, + + HAPI_PARMTYPE_INT_START = HAPI_PARMTYPE_INT, + HAPI_PARMTYPE_INT_END = HAPI_PARMTYPE_BUTTON, + + HAPI_PARMTYPE_FLOAT_START = HAPI_PARMTYPE_FLOAT, + HAPI_PARMTYPE_FLOAT_END = HAPI_PARMTYPE_COLOR, + + HAPI_PARMTYPE_STRING_START = HAPI_PARMTYPE_STRING, + HAPI_PARMTYPE_STRING_END = HAPI_PARMTYPE_NODE, + + HAPI_PARMTYPE_PATH_START = HAPI_PARMTYPE_PATH_FILE, + HAPI_PARMTYPE_PATH_END = HAPI_PARMTYPE_PATH_FILE_IMAGE, + + HAPI_PARMTYPE_NODE_START = HAPI_PARMTYPE_NODE, + HAPI_PARMTYPE_NODE_END = HAPI_PARMTYPE_NODE, + + HAPI_PARMTYPE_CONTAINER_START = HAPI_PARMTYPE_FOLDERLIST, + HAPI_PARMTYPE_CONTAINER_END = HAPI_PARMTYPE_FOLDERLIST_RADIO, + + HAPI_PARMTYPE_NONVALUE_START = HAPI_PARMTYPE_FOLDER, + HAPI_PARMTYPE_NONVALUE_END = HAPI_PARMTYPE_SEPARATOR +}; +HAPI_C_ENUM_TYPEDEF( HAPI_ParmType ) + +/// Corresponds to the types as shown in the Houdini Type Properties +/// window and in DialogScript files. Available on HAPI_ParmInfo +/// See: Parameter types +/// +enum HAPI_PrmScriptType +{ + /// "int", "integer" + HAPI_PRM_SCRIPT_TYPE_INT = 0, + HAPI_PRM_SCRIPT_TYPE_FLOAT, + HAPI_PRM_SCRIPT_TYPE_ANGLE, + HAPI_PRM_SCRIPT_TYPE_STRING, + HAPI_PRM_SCRIPT_TYPE_FILE, + HAPI_PRM_SCRIPT_TYPE_DIRECTORY, + HAPI_PRM_SCRIPT_TYPE_IMAGE, + HAPI_PRM_SCRIPT_TYPE_GEOMETRY, + /// "toggle", "embed" + HAPI_PRM_SCRIPT_TYPE_TOGGLE, + HAPI_PRM_SCRIPT_TYPE_BUTTON, + HAPI_PRM_SCRIPT_TYPE_VECTOR2, + /// "vector", "vector3" + HAPI_PRM_SCRIPT_TYPE_VECTOR3, + HAPI_PRM_SCRIPT_TYPE_VECTOR4, + HAPI_PRM_SCRIPT_TYPE_INTVECTOR2, + /// "intvector", "intvector3" + HAPI_PRM_SCRIPT_TYPE_INTVECTOR3, + HAPI_PRM_SCRIPT_TYPE_INTVECTOR4, + HAPI_PRM_SCRIPT_TYPE_UV, + HAPI_PRM_SCRIPT_TYPE_UVW, + /// "dir", "direction" + HAPI_PRM_SCRIPT_TYPE_DIR, + /// "color", "rgb" + HAPI_PRM_SCRIPT_TYPE_COLOR, + /// "color4", "rgba" + HAPI_PRM_SCRIPT_TYPE_COLOR4, + HAPI_PRM_SCRIPT_TYPE_HUECIRCLE, + HAPI_PRM_SCRIPT_TYPE_OPPATH, + HAPI_PRM_SCRIPT_TYPE_OPLIST, + HAPI_PRM_SCRIPT_TYPE_OBJECT, + HAPI_PRM_SCRIPT_TYPE_OBJECTLIST, + HAPI_PRM_SCRIPT_TYPE_RENDER, + HAPI_PRM_SCRIPT_TYPE_SEPARATOR, + HAPI_PRM_SCRIPT_TYPE_GEOMETRY_DATA, + HAPI_PRM_SCRIPT_TYPE_KEY_VALUE_DICT, + HAPI_PRM_SCRIPT_TYPE_LABEL, + HAPI_PRM_SCRIPT_TYPE_RGBAMASK, + HAPI_PRM_SCRIPT_TYPE_ORDINAL, + HAPI_PRM_SCRIPT_TYPE_RAMP_FLT, + HAPI_PRM_SCRIPT_TYPE_RAMP_RGB, + HAPI_PRM_SCRIPT_TYPE_FLOAT_LOG, + HAPI_PRM_SCRIPT_TYPE_INT_LOG, + HAPI_PRM_SCRIPT_TYPE_DATA, + HAPI_PRM_SCRIPT_TYPE_FLOAT_MINMAX, + HAPI_PRM_SCRIPT_TYPE_INT_MINMAX, + HAPI_PRM_SCRIPT_TYPE_INT_STARTEND, + HAPI_PRM_SCRIPT_TYPE_BUTTONSTRIP, + HAPI_PRM_SCRIPT_TYPE_ICONSTRIP, + + /// The following apply to HAPI_PARMTYPE_FOLDER type parms. + /// Radio buttons Folder + HAPI_PRM_SCRIPT_TYPE_GROUPRADIO = 1000, + /// Collapsible Folder + HAPI_PRM_SCRIPT_TYPE_GROUPCOLLAPSIBLE, + /// Simple Folder + HAPI_PRM_SCRIPT_TYPE_GROUPSIMPLE, + /// Tabs Folder + HAPI_PRM_SCRIPT_TYPE_GROUP +}; +HAPI_C_ENUM_TYPEDEF( HAPI_PrmScriptType ) + +enum HAPI_ChoiceListType +{ + /// Parameter is not a menu + HAPI_CHOICELISTTYPE_NONE, + /// Menu Only, Single Selection + HAPI_CHOICELISTTYPE_NORMAL, + /// Mini Menu Only, Single Selection + HAPI_CHOICELISTTYPE_MINI, + /// Field + Single Selection Menu + HAPI_CHOICELISTTYPE_REPLACE, + /// Field + Multiple Selection Menu + HAPI_CHOICELISTTYPE_TOGGLE +}; +HAPI_C_ENUM_TYPEDEF( HAPI_ChoiceListType ) + +enum HAPI_PresetType +{ + HAPI_PRESETTYPE_INVALID = -1, + /// Just the presets binary blob + HAPI_PRESETTYPE_BINARY = 0, + /// Presets blob within an .idx file format + HAPI_PRESETTYPE_IDX, + HAPI_PRESETTYPE_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_PresetType ) + +enum HAPI_NodeType +{ + HAPI_NODETYPE_ANY = -1, + HAPI_NODETYPE_NONE = 0, + HAPI_NODETYPE_OBJ = 1 << 0, + HAPI_NODETYPE_SOP = 1 << 1, + HAPI_NODETYPE_CHOP = 1 << 2, + HAPI_NODETYPE_ROP = 1 << 3, + HAPI_NODETYPE_SHOP = 1 << 4, + HAPI_NODETYPE_COP2 = 1 << 5, + HAPI_NODETYPE_VOP = 1 << 6, + HAPI_NODETYPE_DOP = 1 << 7, + HAPI_NODETYPE_TOP = 1 << 8, + HAPI_NODETYPE_COP = 1 << 9, + HAPI_NODETYPE_LOP = 1 << 10 +}; +HAPI_C_ENUM_TYPEDEF( HAPI_NodeType ) +typedef int HAPI_NodeTypeBits; + +/// Flags used to filter compositions of node lists. +/// Flags marked 'Recursive Flag' will exclude children whos parent does not +/// satisfy the flag, even if the children themselves satisfy the flag. +enum HAPI_NodeFlags +{ + HAPI_NODEFLAGS_ANY = -1, + HAPI_NODEFLAGS_NONE = 0, + /// Recursive Flag + HAPI_NODEFLAGS_DISPLAY = 1 << 0, + /// Recursive Flag + HAPI_NODEFLAGS_RENDER = 1 << 1, + HAPI_NODEFLAGS_TEMPLATED = 1 << 2, + HAPI_NODEFLAGS_LOCKED = 1 << 3, + HAPI_NODEFLAGS_EDITABLE = 1 << 4, + HAPI_NODEFLAGS_BYPASS = 1 << 5, + HAPI_NODEFLAGS_NETWORK = 1 << 6, + + /// OBJ Node Specific Flags + HAPI_NODEFLAGS_OBJ_GEOMETRY = 1 << 7, + HAPI_NODEFLAGS_OBJ_CAMERA = 1 << 8, + HAPI_NODEFLAGS_OBJ_LIGHT = 1 << 9, + HAPI_NODEFLAGS_OBJ_SUBNET = 1 << 10, + + /// SOP Node Specific Flags + /// Looks for "curve" + HAPI_NODEFLAGS_SOP_CURVE = 1 << 11, + /// Looks for Guide Geometry + HAPI_NODEFLAGS_SOP_GUIDE = 1 << 12, + + /// TOP Node Specific Flags + /// All TOP nodes except schedulers + HAPI_NODEFLAGS_TOP_NONSCHEDULER = 1 << 13, + + /// Recursive Flag + /// Nodes that are not bypassed + HAPI_NODEFLAGS_NON_BYPASS = 1 << 14 +}; +HAPI_C_ENUM_TYPEDEF( HAPI_NodeFlags ) +typedef int HAPI_NodeFlagsBits; + +enum HAPI_GroupType +{ + HAPI_GROUPTYPE_INVALID = -1, + HAPI_GROUPTYPE_POINT, + HAPI_GROUPTYPE_PRIM, + HAPI_GROUPTYPE_EDGE, + HAPI_GROUPTYPE_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_GroupType ) + +enum HAPI_AttributeOwner +{ + HAPI_ATTROWNER_INVALID = -1, + HAPI_ATTROWNER_VERTEX, + HAPI_ATTROWNER_POINT, + HAPI_ATTROWNER_PRIM, + HAPI_ATTROWNER_DETAIL, + HAPI_ATTROWNER_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_AttributeOwner ) + +enum HAPI_CurveType +{ + HAPI_CURVETYPE_INVALID = -1, + HAPI_CURVETYPE_LINEAR, + HAPI_CURVETYPE_NURBS, + HAPI_CURVETYPE_BEZIER, + HAPI_CURVETYPE_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_CurveType ) + +enum HAPI_InputCurveMethod +{ + HAPI_CURVEMETHOD_INVALID = -1, + HAPI_CURVEMETHOD_CVS, + HAPI_CURVEMETHOD_BREAKPOINTS, + HAPI_CURVEMETHOD_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_InputCurveMethod ) + +enum HAPI_InputCurveParameterization +{ + HAPI_CURVEPARAMETERIZATION_INVALID = -1, + HAPI_CURVEPARAMETERIZATION_UNIFORM, + HAPI_CURVEPARAMETERIZATION_CHORD, + HAPI_CURVEPARAMETERIZATION_CENTRIPETAL, + HAPI_CURVEPARAMETERIZATION_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_InputCurveParameterization ) + +enum HAPI_VolumeType +{ + HAPI_VOLUMETYPE_INVALID = -1, + HAPI_VOLUMETYPE_HOUDINI, + HAPI_VOLUMETYPE_VDB, + HAPI_VOLUMETYPE_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_VolumeType ) + +enum HAPI_VolumeVisualType +{ + HAPI_VOLUMEVISTYPE_INVALID = -1, + HAPI_VOLUMEVISTYPE_SMOKE, + HAPI_VOLUMEVISTYPE_RAINBOW, + HAPI_VOLUMEVISTYPE_ISO, + HAPI_VOLUMEVISTYPE_INVISIBLE, + HAPI_VOLUMEVISTYPE_HEIGHTFIELD, + HAPI_VOLUMEVISTYPE_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_VolumeVisualType ) + +enum HAPI_StorageType +{ + HAPI_STORAGETYPE_INVALID = -1, + + HAPI_STORAGETYPE_INT, + HAPI_STORAGETYPE_INT64, + HAPI_STORAGETYPE_FLOAT, + HAPI_STORAGETYPE_FLOAT64, + HAPI_STORAGETYPE_STRING, + HAPI_STORAGETYPE_UINT8, + HAPI_STORAGETYPE_INT8, + HAPI_STORAGETYPE_INT16, + HAPI_STORAGETYPE_DICTIONARY, + + HAPI_STORAGETYPE_INT_ARRAY, + HAPI_STORAGETYPE_INT64_ARRAY, + HAPI_STORAGETYPE_FLOAT_ARRAY, + HAPI_STORAGETYPE_FLOAT64_ARRAY, + HAPI_STORAGETYPE_STRING_ARRAY, + HAPI_STORAGETYPE_UINT8_ARRAY, + HAPI_STORAGETYPE_INT8_ARRAY, + HAPI_STORAGETYPE_INT16_ARRAY, + HAPI_STORAGETYPE_DICTIONARY_ARRAY, + + HAPI_STORAGETYPE_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_StorageType ) + +enum HAPI_AttributeTypeInfo +{ + HAPI_ATTRIBUTE_TYPE_INVALID = -1, + /// Implicit type based on data + HAPI_ATTRIBUTE_TYPE_NONE, + /// Position + HAPI_ATTRIBUTE_TYPE_POINT, + /// Homogeneous position + HAPI_ATTRIBUTE_TYPE_HPOINT, + /// Direction vector + HAPI_ATTRIBUTE_TYPE_VECTOR, + /// Normal + HAPI_ATTRIBUTE_TYPE_NORMAL, + /// Color + HAPI_ATTRIBUTE_TYPE_COLOR, + /// Quaternion + HAPI_ATTRIBUTE_TYPE_QUATERNION, + /// 3x3 Matrix + HAPI_ATTRIBUTE_TYPE_MATRIX3, + /// Matrix + HAPI_ATTRIBUTE_TYPE_MATRIX, + /// Parametric interval + HAPI_ATTRIBUTE_TYPE_ST, + /// "Private" (hidden) + HAPI_ATTRIBUTE_TYPE_HIDDEN, + /// 2x2 Bounding box + HAPI_ATTRIBUTE_TYPE_BOX2, + /// 3x3 Bounding box + HAPI_ATTRIBUTE_TYPE_BOX, + /// Texture coordinate + HAPI_ATTRIBUTE_TYPE_TEXTURE, + HAPI_ATTRIBUTE_TYPE_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_AttributeTypeInfo ) + +enum HAPI_GeoType +{ + HAPI_GEOTYPE_INVALID = -1, + + /// Most geos will be of this type which essentially means a geo + /// not of the other types. + HAPI_GEOTYPE_DEFAULT, + + /// An exposed edit node. + /// See @ref HAPI_IntermediateAssetsResults. + HAPI_GEOTYPE_INTERMEDIATE, + + /// An input geo that can accept geometry from the host. + /// See @ref HAPI_AssetInputs_MarshallingGeometryIntoHoudini. + HAPI_GEOTYPE_INPUT, + + /// A curve. + /// See @ref HAPI_Curves. + HAPI_GEOTYPE_CURVE, + + HAPI_GEOTYPE_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_GeoType ) + +enum HAPI_PartType +{ + HAPI_PARTTYPE_INVALID = -1, + HAPI_PARTTYPE_MESH, + HAPI_PARTTYPE_CURVE, + HAPI_PARTTYPE_VOLUME, + HAPI_PARTTYPE_INSTANCER, + HAPI_PARTTYPE_BOX, + HAPI_PARTTYPE_SPHERE, + HAPI_PARTTYPE_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_PartType ) + +enum HAPI_InputType +{ + HAPI_INPUT_INVALID = -1, + HAPI_INPUT_TRANSFORM, + HAPI_INPUT_GEOMETRY, + HAPI_INPUT_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_InputType ) + +enum HAPI_CurveOrders +{ + HAPI_CURVE_ORDER_VARYING = 0, + HAPI_CURVE_ORDER_INVALID = 1, + HAPI_CURVE_ORDER_LINEAR = 2, + HAPI_CURVE_ORDER_QUADRATIC = 3, + HAPI_CURVE_ORDER_CUBIC = 4, +}; +HAPI_C_ENUM_TYPEDEF( HAPI_CurveOrders ) + +enum HAPI_TransformComponent +{ + HAPI_TRANSFORM_TX = 0, + HAPI_TRANSFORM_TY, + HAPI_TRANSFORM_TZ, + HAPI_TRANSFORM_RX, + HAPI_TRANSFORM_RY, + HAPI_TRANSFORM_RZ, + HAPI_TRANSFORM_QX, + HAPI_TRANSFORM_QY, + HAPI_TRANSFORM_QZ, + HAPI_TRANSFORM_QW, + HAPI_TRANSFORM_SX, + HAPI_TRANSFORM_SY, + HAPI_TRANSFORM_SZ +}; +HAPI_C_ENUM_TYPEDEF( HAPI_TransformComponent ) + +enum HAPI_RSTOrder +{ + HAPI_TRS = 0, + HAPI_TSR, + HAPI_RTS, + HAPI_RST, + HAPI_STR, + HAPI_SRT, + + HAPI_RSTORDER_DEFAULT = HAPI_SRT +}; +HAPI_C_ENUM_TYPEDEF( HAPI_RSTOrder ) + +enum HAPI_XYZOrder +{ + HAPI_XYZ = 0, + HAPI_XZY, + HAPI_YXZ, + HAPI_YZX, + HAPI_ZXY, + HAPI_ZYX, + + HAPI_XYZORDER_DEFAULT = HAPI_XYZ +}; +HAPI_C_ENUM_TYPEDEF( HAPI_XYZOrder ) + +enum HAPI_ImageDataFormat +{ + HAPI_IMAGE_DATA_UNKNOWN = -1, + HAPI_IMAGE_DATA_INT8, + HAPI_IMAGE_DATA_INT16, + HAPI_IMAGE_DATA_INT32, + HAPI_IMAGE_DATA_FLOAT16, + HAPI_IMAGE_DATA_FLOAT32, + HAPI_IMAGE_DATA_MAX, + + HAPI_IMAGE_DATA_DEFAULT = HAPI_IMAGE_DATA_INT8 +}; +HAPI_C_ENUM_TYPEDEF( HAPI_ImageDataFormat ) + +enum HAPI_ImagePacking +{ + HAPI_IMAGE_PACKING_UNKNOWN = -1, + HAPI_IMAGE_PACKING_SINGLE, /// Single Channel + HAPI_IMAGE_PACKING_DUAL, /// Dual Channel + HAPI_IMAGE_PACKING_RGB, /// RGB + HAPI_IMAGE_PACKING_BGR, /// RGB Reversed + HAPI_IMAGE_PACKING_RGBA, /// RGBA + HAPI_IMAGE_PACKING_ABGR, /// RGBA Reversed + HAPI_IMAGE_PACKING_MAX, + + HAPI_IMAGE_PACKING_DEFAULT3 = HAPI_IMAGE_PACKING_RGB, + HAPI_IMAGE_PACKING_DEFAULT4 = HAPI_IMAGE_PACKING_RGBA +}; +HAPI_C_ENUM_TYPEDEF( HAPI_ImagePacking ) + +/// Used with ::HAPI_GetEnvInt() to retrieve basic information +/// about the HAPI implementation currently being linked +/// against. Note that as of HAPI version 2.0, these enum values are +/// guaranteed never to change so you can reliably get this information from +/// any post-2.0 version of HAPI. The same goes for the actual +/// ::HAPI_GetEnvInt() API call. +enum HAPI_EnvIntType +{ + HAPI_ENVINT_INVALID = -1, + + /// The three components of the Houdini version that HAPI is + /// expecting to link against. + /// @{ + HAPI_ENVINT_VERSION_HOUDINI_MAJOR = 100, + HAPI_ENVINT_VERSION_HOUDINI_MINOR = 110, + HAPI_ENVINT_VERSION_HOUDINI_BUILD = 120, + HAPI_ENVINT_VERSION_HOUDINI_PATCH = 130, + /// @} + + /// The two components of the Houdini Engine (marketed) version. + /// @{ + HAPI_ENVINT_VERSION_HOUDINI_ENGINE_MAJOR = 200, + HAPI_ENVINT_VERSION_HOUDINI_ENGINE_MINOR = 210, + /// @} + + /// This is a monotonously increasing API version number that can be used + /// to lock against a certain API for compatibility purposes. Basically, + /// when this number changes code compiled against the HAPI.h methods + /// might no longer compile. Semantic changes to the methods will also + /// cause this version to increase. This number will be reset to 0 + /// every time the Houdini Engine version is bumped. + HAPI_ENVINT_VERSION_HOUDINI_ENGINE_API = 220, + + HAPI_ENVINT_MAX, +}; +HAPI_C_ENUM_TYPEDEF( HAPI_EnvIntType ) + +/// This enum is to be used with ::HAPI_GetSessionEnvInt() to retrieve basic +/// session-specific information. +enum HAPI_SessionEnvIntType +{ + HAPI_SESSIONENVINT_INVALID = -1, + + /// License Type. See ::HAPI_License. + HAPI_SESSIONENVINT_LICENSE = 100, + + HAPI_SESSIONENVINT_MAX +}; +HAPI_C_ENUM_TYPEDEF( HAPI_SessionEnvIntType ) + +/// Identifies a memory cache +enum HAPI_CacheProperty +{ + /// Current memory usage in MB. Setting this to 0 invokes + /// a cache clear. + HAPI_CACHEPROP_CURRENT, + + HAPI_CACHEPROP_HAS_MIN, /// True if it actually has a minimum size. + HAPI_CACHEPROP_MIN, /// Min cache memory limit in MB. + HAPI_CACHEPROP_HAS_MAX, /// True if it actually has a maximum size. + HAPI_CACHEPROP_MAX, /// Max cache memory limit in MB. + + /// How aggressive to cull memory. This only works for: + /// - ::HAPI_CACHE_COP2_COOK where: + /// 0 -> Never reduce inactive cache. + /// 1 -> Always reduce inactive cache. + /// - ::HAPI_CACHE_OBJ where: + /// 0 -> Never enforce the max memory limit. + /// 1 -> Always enforce the max memory limit. + /// - ::HAPI_CACHE_SOP where: + /// 0 -> When to Unload = Never + /// When to Limit Max Memory = Never + /// 1-2 -> When to Unload = Based on Flag + /// When to Limit Max Memory = Never + /// 3-4 -> When to Unload = Based on Flag + /// When to Limit Max Memory = Always + /// 5 -> When to Unload = Always + /// When to Limit Max Memory = Always + HAPI_CACHEPROP_CULL_LEVEL, +}; + +HAPI_C_ENUM_TYPEDEF( HAPI_CacheProperty ) + +/// Type of sampling for heightfield +enum HAPI_HeightFieldSampling +{ + HAPI_HEIGHTFIELD_SAMPLING_CENTER, + HAPI_HEIGHTFIELD_SAMPLING_CORNER +}; +HAPI_C_ENUM_TYPEDEF( HAPI_HeightFieldSampling ) + +/// Used with PDG functions +enum HAPI_PDG_State +{ + HAPI_PDG_STATE_READY, + HAPI_PDG_STATE_COOKING, + HAPI_PDG_STATE_MAX, + + HAPI_PDG_STATE_MAX_READY_STATE = HAPI_PDG_STATE_READY +}; +HAPI_C_ENUM_TYPEDEF( HAPI_PDG_State ) + +/// Used with PDG functions +enum HAPI_PDG_EventType +{ + /// An empty, undefined event. Should be ignored. + HAPI_PDG_EVENT_NULL, + + /// Sent when a new work item is added by a node + HAPI_PDG_EVENT_WORKITEM_ADD, + /// Sent when a work item is deleted from a node + HAPI_PDG_EVENT_WORKITEM_REMOVE, + /// Sent when a work item's state changes + HAPI_PDG_EVENT_WORKITEM_STATE_CHANGE, + + /// Sent when a work item has a dependency added + HAPI_PDG_EVENT_WORKITEM_ADD_DEP, + /// Sent when a dependency is removed from a work item + HAPI_PDG_EVENT_WORKITEM_REMOVE_DEP, + + /// Sent from dynamic work items that generate from a cooked item + HAPI_PDG_EVENT_WORKITEM_ADD_PARENT, + /// Sent when the parent item for a work item is deleted + HAPI_PDG_EVENT_WORKITEM_REMOVE_PARENT, + + /// A node event that indicates that node is about to have all its work items cleared + HAPI_PDG_EVENT_NODE_CLEAR, + + /// Sent when an error is issued by the node + HAPI_PDG_EVENT_COOK_ERROR, + /// Sent when an warning is issued by the node + HAPI_PDG_EVENT_COOK_WARNING, + + /// Sent for each node in the graph, when a cook completes + HAPI_PDG_EVENT_COOK_COMPLETE, + + /// A node event indicating that one more items in the node will be dirtied + HAPI_PDG_EVENT_DIRTY_START, + /// A node event indicating that the node has finished dirtying items + HAPI_PDG_EVENT_DIRTY_STOP, + + /// A event indicating that the entire graph is about to be dirtied + HAPI_PDG_EVENT_DIRTY_ALL, + + /// A work item event that indicates the item has been selected in the TOPs UI + HAPI_PDG_EVENT_UI_SELECT, + + /// Sent when a new node is created + HAPI_PDG_EVENT_NODE_CREATE, + /// Sent when a node was removed from the graph + HAPI_PDG_EVENT_NODE_REMOVE, + /// Sent when a node was renamed + HAPI_PDG_EVENT_NODE_RENAME, + /// Sent when a node was connected to another node + HAPI_PDG_EVENT_NODE_CONNECT, + /// Sent when a node is disconnected from another node + HAPI_PDG_EVENT_NODE_DISCONNECT, + /// Sent when a node cooks for the first time + HAPI_PDG_EVENT_NODE_FIRST_COOK, + + /// Deprecated + HAPI_PDG_EVENT_WORKITEM_SET_INT, + /// Deprecated + HAPI_PDG_EVENT_WORKITEM_SET_FLOAT, + /// Deprecated + HAPI_PDG_EVENT_WORKITEM_SET_STRING, + /// Deprecated + HAPI_PDG_EVENT_WORKITEM_SET_FILE, + /// Deprecated + HAPI_PDG_EVENT_WORKITEM_SET_DICT, + /// Deprecated + HAPI_PDG_EVENT_WORKITEM_SET_PYOBJECT, + /// Deprecated + HAPI_PDG_EVENT_WORKITEM_SET_GEOMETRY, + /// Deprecated + HAPI_PDG_EVENT_WORKITEM_MERGE, + /// Sent when an output file is added to a work item + HAPI_PDG_EVENT_WORKITEM_RESULT, + + /// Deprecated + HAPI_PDG_EVENT_WORKITEM_PRIORITY, + /// Sent for each node in the graph, when a cook starts + HAPI_PDG_EVENT_COOK_START, + /// Deprecated + HAPI_PDG_EVENT_WORKITEM_ADD_STATIC_ANCESTOR, + /// Deprecated + HAPI_PDG_EVENT_WORKITEM_REMOVE_STATIC_ANCESTOR, + + /// Deprecated + HAPI_PDG_EVENT_NODE_PROGRESS_UPDATE, + /// Deprecated + HAPI_PDG_EVENT_BATCH_ITEM_INITIALIZED, + /// A special enum that represents the OR of all event types + HAPI_PDG_EVENT_ALL, + /// A special enum that represents the OR of both the `CookError` and `CookWarning` events + HAPI_PDG_EVENT_LOG, + + /// Sent when a new scheduler is added to the graph + HAPI_PDG_EVENT_SCHEDULER_ADDED, + /// Sent when a scheduler is removed from the graph + HAPI_PDG_EVENT_SCHEDULER_REMOVED, + /// Deprecated + HAPI_PDG_EVENT_SET_SCHEDULER, + /// Deprecated + HAPI_PDG_EVENT_SERVICE_MANAGER_ALL, + + /// Sent when a node finishes cooking + HAPI_PDG_EVENT_NODE_COOKED, + /// Sent when a node finished generating + HAPI_PDG_EVENT_NODE_GENERATED, + + /// Sent when a work item's frame changes + HAPI_PDG_EVENT_WORKITEM_FRAME, + + HAPI_PDG_CONTEXT_EVENTS, +}; +HAPI_C_ENUM_TYPEDEF( HAPI_PDG_EventType ) + +/// Used with PDG functions +enum HAPI_PDG_WorkItemState +{ + HAPI_PDG_WORKITEM_UNDEFINED, + HAPI_PDG_WORKITEM_UNCOOKED, + HAPI_PDG_WORKITEM_WAITING, + HAPI_PDG_WORKITEM_SCHEDULED, + HAPI_PDG_WORKITEM_COOKING, + HAPI_PDG_WORKITEM_COOKED_SUCCESS, + HAPI_PDG_WORKITEM_COOKED_CACHE, + HAPI_PDG_WORKITEM_COOKED_FAIL, + HAPI_PDG_WORKITEM_COOKED_CANCEL, + HAPI_PDG_WORKITEM_DIRTY +}; +HAPI_C_ENUM_TYPEDEF( HAPI_PDG_WorkItemState ) + +/// Backwards compatibility for HAPI_PDG_WorkitemState +typedef HAPI_PDG_WorkItemState HAPI_PDG_WorkitemState; + +enum HAPI_TCP_PortType +{ + HAPI_TCP_PORT_ANY, + HAPI_TCP_PORT_RANGE, + HAPI_TCP_PORT_LIST +}; +HAPI_C_ENUM_TYPEDEF( HAPI_TCP_PortType ) + +enum HAPI_ThriftSharedMemoryBufferType +{ + HAPI_THRIFT_SHARED_MEMORY_FIXED_LENGTH_BUFFER, + HAPI_THRIFT_SHARED_MEMORY_RING_BUFFER +}; +HAPI_C_ENUM_TYPEDEF( HAPI_ThriftSharedMemoryBufferType ) + +///////////////////////////////////////////////////////////////////////////// +// Main API Structs + +// GENERICS ----------------------------------------------------------------- + +/// A Transform with Quaternion rotation +struct HAPI_API HAPI_Transform +{ + float position[ HAPI_POSITION_VECTOR_SIZE ]; + float rotationQuaternion[ HAPI_QUATERNION_VECTOR_SIZE ]; + float scale[ HAPI_SCALE_VECTOR_SIZE ]; + float shear[ HAPI_SHEAR_VECTOR_SIZE ]; + + HAPI_RSTOrder rstOrder; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_Transform ) + +/// A Transform with Euler rotation +struct HAPI_API HAPI_TransformEuler +{ + float position[ HAPI_POSITION_VECTOR_SIZE ]; + float rotationEuler[ HAPI_EULER_VECTOR_SIZE ]; + float scale[ HAPI_SCALE_VECTOR_SIZE ]; + float shear[ HAPI_SHEAR_VECTOR_SIZE ]; + + HAPI_XYZOrder rotationOrder; + HAPI_RSTOrder rstOrder; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_TransformEuler ) + +// SESSIONS ----------------------------------------------------------------- + +/// Identifies a session +struct HAPI_API HAPI_Session +{ + /// The type of session determines the which implementation will be + /// used to communicate with the Houdini Engine library. + HAPI_SessionType type; + + /// Some session types support multiple simultaneous sessions. This means + /// that each session needs to have a unique identifier. + HAPI_SessionId id; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_Session ) + +/// Configurations for sessions +struct HAPI_API HAPI_SessionInfo +{ + /// The number of subconnections in this session + int connectionCount; + + /// Specification for the port numbers + HAPI_TCP_PortType portType; + + /// Specifies a range of port numbers, [minPort, maxPort] + int minPort; + int maxPort; + + /// Specifies a list of port numbers + int ports[ HAPI_MAX_NUM_CONNECTIONS ]; + + // Must match the buffer type passed to the HARS executable through the + // command line or ::HAPI_StartThriftSharedMemoryServer + HAPI_ThriftSharedMemoryBufferType sharedMemoryBufferType; + + // Must match the buffer size passed to the HARS executable through the + // command line or ::HAPI_StartThriftSharedMemoryServer. This is the size of + // the shared memory buffer in megabytes (MB). + HAPI_Int64 sharedMemoryBufferSize; + + HAPI_Bool enableSharedMemoryDataTransfer; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_SessionInfo ) + +/// Options to configure a Thrift server being started from HARC. +struct HAPI_API HAPI_ThriftServerOptions +{ + /// Close the server automatically when all clients disconnect from it. + HAPI_Bool autoClose; + + /// Timeout in milliseconds for waiting on the server to + /// signal that it's ready to serve. If the server fails + /// to signal within this time interval, the start server call fails + /// and the server process is terminated. + float timeoutMs; + + // Specifies the maximum status verbosity that will be logged. + HAPI_StatusVerbosity verbosity; + + // Only used when starting a Thrift shared memory server. This controls the + // type of buffer that is used in the underlying communication protocol. A + // fixed length buffer is faster but the data passed to any single HAPI API + // call cannot exceed the total length of the buffer. A ring buffer is + // slower but has no limitations on the size of the data. + HAPI_ThriftSharedMemoryBufferType sharedMemoryBufferType; + + // Only used when starting a Thrift shared memory server. This variable + // specifies the size in megabytes (MB) of the allocated shared memory + // buffer. + HAPI_Int64 sharedMemoryBufferSize; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_ThriftServerOptions ) + +// TIME --------------------------------------------------------------------- + +/// Data for global timeline, used with ::HAPI_SetTimelineOptions() +struct HAPI_API HAPI_TimelineOptions +{ + double fps; + + double startTime; + double endTime; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_TimelineOptions ) + +// ASSETS ------------------------------------------------------------------- + +/// Meta-data about an HDA, returned by ::HAPI_GetAssetInfo() +struct HAPI_API HAPI_AssetInfo +{ + /// Use the node id to get the asset's parameters. + /// See @ref HAPI_Nodes_Basics. + HAPI_NodeId nodeId; + + /// The objectNodeId differs from the regular nodeId in that for + /// geometry based assets (SOPs) it will be the node id of the dummy + /// object (OBJ) node instead of the asset node. For object based assets + /// the objectNodeId will equal the nodeId. The reason the distinction + /// exists is because transforms are always stored on the object node + /// but the asset parameters may not be on the asset node if the asset + /// is a geometry asset so we need both. + HAPI_NodeId objectNodeId; + + /// It's possible to instantiate an asset without cooking it. + /// See @ref HAPI_Assets_Cooking. + HAPI_Bool hasEverCooked; + + HAPI_StringHandle nameSH; /// Instance name (the label + a number). + HAPI_StringHandle labelSH; /// This is what any end user should be shown. + HAPI_StringHandle filePathSH; /// Path to the .otl library file. + HAPI_StringHandle versionSH; /// User-defined asset version. + HAPI_StringHandle fullOpNameSH; /// Full asset name and namespace. + HAPI_StringHandle helpTextSH; /// Asset help marked-up text. + HAPI_StringHandle helpURLSH; /// Asset help URL. + + int objectCount; /// See @ref HAPI_Objects. + int handleCount; /// See @ref HAPI_Handles. + + /// Transform inputs exposed by the asset. For OBJ assets this is the + /// number of transform inputs on the OBJ node. For SOP assets, this is + /// the singular transform input on the dummy wrapper OBJ node. + /// See @ref HAPI_AssetInputs. + int transformInputCount; + + /// Geometry inputs exposed by the asset. For SOP assets this is + /// the number of geometry inputs on the SOP node itself. OBJ assets + /// will always have zero geometry inputs. + /// See @ref HAPI_AssetInputs. + int geoInputCount; + + /// Geometry outputs exposed by the asset. For SOP assets this is + /// the number of geometry outputs on the SOP node itself. OBJ assets + /// will always have zero geometry outputs. + /// See @ref HAPI_AssetInputs. + int geoOutputCount; + + /// For incremental updates. Indicates whether any of the assets's + /// objects have changed. Refreshed only during an asset cook. + HAPI_Bool haveObjectsChanged; + + /// For incremental updates. Indicates whether any of the asset's + /// materials have changed. Refreshed only during an asset cook. + HAPI_Bool haveMaterialsChanged; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_AssetInfo ) + +/// Options which affect how nodes are cooked. +struct HAPI_API HAPI_CookOptions +{ + /// Normally, geos are split into parts in two different ways. First it + /// is split by group and within each group it is split by primitive type. + /// + /// For example, if you have a geo with group1 covering half of the mesh + /// and volume1 and group2 covering the other half of the mesh, all of + /// curve1, and volume2 you will end up with 5 parts. First two parts + /// will be for the half-mesh of group1 and volume1, and the last three + /// will cover group2. + /// + /// This toggle lets you disable the splitting by group and just have + /// the geo be split by primitive type alone. By default, this is true + /// and therefore geos will be split by group and primitive type. If + /// set to false, geos will only be split by primitive type. + HAPI_Bool splitGeosByGroup; + HAPI_StringHandle splitGroupSH; + + /// This toggle lets you enable the splitting by unique values + /// of a specified attribute. By default, this is false and + /// the geo be split as described above. + /// as described above. If this is set to true, and splitGeosByGroup + /// set to false, mesh geos will be split on attribute values + /// The attribute name to split on must be created with HAPI_SetCustomString + /// and then the splitAttrSH handle set on the struct. + HAPI_Bool splitGeosByAttribute; + HAPI_StringHandle splitAttrSH; + + /// For meshes only, this is enforced by convexing the mesh. Use -1 + /// to avoid convexing at all and get some performance boost. + int maxVerticesPerPrimitive; + + /// For curves only. + /// If this is set to true, then all curves will be refined to a linear + /// curve and you can no longer access the original CVs. You can control + /// the refinement detail via ::HAPI_CookOptions::curveRefineLOD. + /// If it's false, the curve type (NURBS, Bezier etc) will be left as is. + HAPI_Bool refineCurveToLinear; + + /// Controls the number of divisions per unit distance when refining + /// a curve to linear. The default in Houdini is 8.0. + float curveRefineLOD; + + /// If this option is turned on, then we will recursively clear the + /// errors and warnings (and messages) of all nodes before performing + /// the cook. + HAPI_Bool clearErrorsAndWarnings; + + /// Decide whether to recursively cook all templated geos or not. + HAPI_Bool cookTemplatedGeos; + + /// Decide whether to split points by vertex attributes. This takes + /// all vertex attributes and tries to copy them to their respective + /// points. If two vertices have any difference in their attribute values, + /// the corresponding point is split into two points. This is repeated + /// until all the vertex attributes have been copied to the points. + /// + /// With this option enabled, you can reduce the total number of vertices + /// on a game engine side as sharing of attributes (like UVs) is optimized. + /// To make full use of this feature, you have to think of Houdini points + /// as game engine vertices (sharable). With this option OFF (or before + /// this feature existed) you had to map Houdini vertices to game engine + /// vertices, to make sure all attribute values are accounted for. + HAPI_Bool splitPointsByVertexAttributes; + + /// Choose how you want the cook to handle packed primitives. + /// The default is: ::HAPI_PACKEDPRIM_INSTANCING_MODE_DISABLED + HAPI_PackedPrimInstancingMode packedPrimInstancingMode; + + /// Choose which special part types should be handled. Unhandled special + /// part types will just be refined to ::HAPI_PARTTYPE_MESH. + HAPI_Bool handleBoxPartTypes; + HAPI_Bool handleSpherePartTypes; + + /// If enabled, sets the ::HAPI_PartInfo::hasChanged member during the + /// cook. If disabled, the member will always be true. Checking for + /// part changes can be expensive, so there is a potential performance + /// gain when disabled. + HAPI_Bool checkPartChanges; + + /// This toggle lets you enable the caching of the mesh topology. + /// By default, this is false. If this is set to true, cooking a mesh + /// geometry will update only the topology if the number of points changed. + /// Use this to get better performance on deforming meshes. + HAPI_Bool cacheMeshTopology; + + /// If enabled, calls to ::HAPI_CookNode() on an OBJ node will cook the output + /// nodes of any nested SOP nodes. If none exist or the option is disabled, + /// HAPI will instead cook the display nodes of any nested SOP nodes. + HAPI_Bool preferOutputNodes; + + /// For internal use only. :) + int extraFlags; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_CookOptions ) + +// NODES -------------------------------------------------------------------- + +/// Meta-data for a Houdini Node +struct HAPI_API HAPI_NodeInfo +{ + HAPI_NodeId id; + HAPI_NodeId parentId; + HAPI_StringHandle nameSH; + HAPI_NodeType type; + + /// Always true unless the asset's definition has changed due to loading + /// a duplicate asset definition and from another OTL asset library + /// file OR deleting the OTL asset library file used by this node's asset. + HAPI_Bool isValid; + + /// Total number of cooks of this node. + int totalCookCount; + + /// Use this unique id to grab the OP_Node pointer for this node. + /// If you're linking against the C++ HDK, include the OP_Node.h header + /// and call OP_Node::lookupNode(). + int uniqueHoudiniNodeId; + + /// This is the internal node path in the Houdini scene graph. This path + /// is meant to be abstracted away for most client purposes but for + /// advanced uses it can come in handy. + HAPI_StringHandle internalNodePathSH; + + /// Total number of parameters this asset has exposed. Includes hidden + /// parameters. + /// See @ref HAPI_Parameters. + int parmCount; + + /// Number of values. A single parameter may have more than one value so + /// this number is more than or equal to ::HAPI_NodeInfo::parmCount. + /// @{ + int parmIntValueCount; + int parmFloatValueCount; + int parmStringValueCount; + /// @} + + /// The total number of choices among all the combo box parameters. + /// See @ref HAPI_Parameters_ChoiceLists. + int parmChoiceCount; + + /// The number of child nodes. This is 0 for all nodes that are not + /// node networks. + int childNodeCount; + + /// The number of inputs this specific node has. + int inputCount; + + /// The number of outputs this specific node has. + int outputCount; + + /// Nodes created via scripts or via ::HAPI_CreateNode() will be have + /// this set to true. Only such nodes can be deleted using + /// ::HAPI_DeleteNode(). + HAPI_Bool createdPostAssetLoad; + + /// Indicates if this node will change over time + HAPI_Bool isTimeDependent; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_NodeInfo ) + +// PARAMETERS --------------------------------------------------------------- + +/// +/// Contains parameter information like name, label, type, and size. +/// +struct HAPI_API HAPI_ParmInfo +{ + /// The parent id points to the id of the parent parm + /// of this parm. The parent parm is something like a folder. + HAPI_ParmId id; + + /// Parameter id of the parent of this parameter. + HAPI_ParmId parentId; + + /// Child index within its immediate parent parameter. + int childIndex; + + /// The HAPI type of the parm + HAPI_ParmType type; + + /// The Houdini script-type of the parm + HAPI_PrmScriptType scriptType; + + /// Some parameter types require additional type information. + /// - File path parameters will indicate what file extensions they + /// expect in a space-separated list of wild-cards. This is set + /// in the Operator Type Properties using the File Pattern + /// parameter property. + /// For example, for filtering by PNG and JPG only: "*.png *.jpg" + HAPI_StringHandle typeInfoSH; + + /// For the majority of parameter types permission will not be applicable. + /// For file path parameters these permissions will indicate how the + /// asset plans to use the file: whether it will only read it, only write + /// to it, or both. This is set in the Operator Type Properties using + /// the Browse Mode parameter property. + HAPI_Permissions permissions; + + /// Number of tags on this paramter. + int tagCount; + + /// Tuple size. For scalar parameters this value is 1, but for vector + /// parameters this value can be greater. For example, a 3 vector would + /// have a size of 3. For folders and folder lists, this value is the + /// number of children they own. + int size; + + /// Any ::HAPI_ParmType can be a choice list. If this is set to + /// ::HAPI_CHOICELISTTYPE_NONE, than this parameter is NOT a choice list. + /// Otherwise, the parameter is a choice list of the indicated type. + /// See @ref HAPI_Parameters_ChoiceLists. + HAPI_ChoiceListType choiceListType; + + /// Any ::HAPI_ParmType can be a choice list. If the parameter is a + /// choice list, this tells you how many choices it currently has. + /// Note that some menu parameters can have a dynamic number of choices + /// so it is important that this count is re-checked after every cook. + /// See @ref HAPI_Parameters_ChoiceLists. + int choiceCount; + + /// Note that folders are not real parameters in Houdini so they do not + /// have names. The folder names given here are generated from the name + /// of the folderlist (or switcher) parameter which is a parameter. The + /// folderlist parameter simply defines how many of the "next" parameters + /// belong to the first folder, how many of the parameters after that + /// belong to the next folder, and so on. This means that folder names + /// can change just by reordering the folders around so don't rely on + /// them too much. The only guarantee here is that the folder names will + /// be unique among all other parameter names. + HAPI_StringHandle nameSH; + + /// The label string for the parameter + HAPI_StringHandle labelSH; + + /// If this parameter is a multiparm instance than the + /// ::HAPI_ParmInfo::templateNameSH will be the hash-templated parm name, + /// containing #'s for the parts of the name that use the instance number. + /// Compared to the ::HAPI_ParmInfo::nameSH, the ::HAPI_ParmInfo::nameSH + /// will be the ::HAPI_ParmInfo::templateNameSH but with the #'s + /// replaced by the instance number. For regular parms, the + /// ::HAPI_ParmInfo::templateNameSH is identical to the + /// ::HAPI_ParmInfo::nameSH. + HAPI_StringHandle templateNameSH; + + /// The help string for this parameter + HAPI_StringHandle helpSH; + + /// Whether min/max exists for the parameter values. + /// @{ + HAPI_Bool hasMin; + HAPI_Bool hasMax; + HAPI_Bool hasUIMin; + HAPI_Bool hasUIMax; + /// @} + + /// Parameter value range, shared between int and float parameters. + /// @{ + float min; + float max; + float UIMin; + float UIMax; + /// @} + + /// Whether this parm should be hidden from the user entirely. This is + /// mostly used to expose parameters as asset meta-data but not allow the + /// user to directly modify them. + HAPI_Bool invisible; + + /// Whether this parm should appear enabled or disabled. + HAPI_Bool disabled; + + /// If true, it means this parameter doesn't actually exist on the node + /// in Houdini but was added by Houdini Engine as a spare parameter. + /// This is just for your information. The behaviour of this parameter + /// is not any different than a non-spare parameter. + HAPI_Bool spare; + + HAPI_Bool joinNext; /// Whether this parm should be on the same line as + /// the next parm. + HAPI_Bool labelNone; /// Whether the label should be displayed. + + /// The index to use to look into the values array in order to retrieve + /// the actual value(s) of this parameter. + /// @{ + int intValuesIndex; + int floatValuesIndex; + int stringValuesIndex; + int choiceIndex; + /// @} + + /// If this is a ::HAPI_PARMTYPE_NODE, this tells you what node types + /// this parameter accepts. + HAPI_NodeType inputNodeType; + + /// The node input parameter could have another subtype filter specified, + /// like "Object: Geometry Only". In this case, this value will specify + /// that extra filter. If the filter demands a node that HAPI does not + /// support, both this and ::HAPI_ParmInfo::inputNodeType will be set to + /// NONE as such a node is not settable through HAPI. + HAPI_NodeFlags inputNodeFlag; + + /// See @ref HAPI_Parameters_MultiParms. + /// @{ + HAPI_Bool isChildOfMultiParm; + + int instanceNum; /// The index of the instance in the multiparm. + int instanceLength; /// The number of parms in a multiparm instance. + int instanceCount; /// The number of instances in a multiparm. + + /// First instance's ::HAPI_ParmInfo::instanceNum. Either 0 or 1. + int instanceStartOffset; + + HAPI_RampType rampType; + /// @} + + /// Provides the raw condition string which is used to evaluate the + /// the visibility of a parm + HAPI_StringHandle visibilityConditionSH; + + /// Provides the raw condition string which is used to evalute whether + /// a parm is enabled or disabled + HAPI_StringHandle disabledConditionSH; + + /// Whether or not the "Use Menu Item Token As Value" checkbox was checked in a integer menu item. + HAPI_Bool useMenuItemTokenAsValue; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_ParmInfo ) + +/// Meta-data for a combo-box / choice parm +struct HAPI_API HAPI_ParmChoiceInfo +{ + HAPI_ParmId parentParmId; + HAPI_StringHandle labelSH; + + /// This evaluates to the value of the token associated with the label + /// applies to string menus only. + HAPI_StringHandle valueSH; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_ParmChoiceInfo ) + +// HANDLES ------------------------------------------------------------------ + +/// +/// Contains handle information such as the type of handle +/// (translate, rotate, scale, softxform ...etc) and the number of +/// parameters the current handle is bound to. +/// +struct HAPI_API HAPI_HandleInfo +{ + HAPI_StringHandle nameSH; + HAPI_StringHandle typeNameSH; + + int bindingsCount; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_HandleInfo ) + +/// +/// Contains binding information that maps the handle parameter to +/// the asset parameter. The index is only used for int and float vector +/// and colour parms. +/// +struct HAPI_API HAPI_HandleBindingInfo +{ + HAPI_StringHandle handleParmNameSH; + HAPI_StringHandle assetParmNameSH; + + HAPI_ParmId assetParmId; + int assetParmIndex; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_HandleBindingInfo ) + +// OBJECTS ------------------------------------------------------------------ + +/// Meta-data for an OBJ Node +struct HAPI_API HAPI_ObjectInfo +{ + HAPI_StringHandle nameSH; + + /// @deprecated This member is no longer used + HAPI_StringHandle objectInstancePathSH; + + /// For incremental updates. Indicates whether the object's transform + /// has changed. Refreshed only during an asset cook. + HAPI_Bool hasTransformChanged; + + /// For incremental updates. Indicates whether any of the object's + /// geometry nodes have changed. Refreshed only during an asset cook. + HAPI_Bool haveGeosChanged; + + /// Whether the object is hidden and should not be shown. Some objects + /// should be hidden but still brought into the host environment, for + /// example those used only for instancing. + /// See @ref HAPI_Instancing. + HAPI_Bool isVisible; + + /// See @ref HAPI_Instancing. + HAPI_Bool isInstancer; + + /// Determine if this object is being instanced. Normally, this implies + /// that while this object may not be visible, it should still be + /// brought into the host application because it is needed by an instancer. + /// See @ref HAPI_Instancing. + HAPI_Bool isInstanced; + + /// @deprecated No longer used. See @ref HAPI_Geos + int geoCount; + + /// Use the node id to get the node's parameters. + /// Using the HDK, you can also get the raw node C++ pointer for this + /// object's internal node. + /// See @ref HAPI_Nodes_Basics. + HAPI_NodeId nodeId; + + /// If the object is an instancer, this variable gives the object id of + /// the object that should be instanced. + /// See @ref HAPI_Instancing. + HAPI_NodeId objectToInstanceId; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_ObjectInfo ) + +// GEOMETRY ----------------------------------------------------------------- + +/// Meta-data for a SOP Node +struct HAPI_API HAPI_GeoInfo +{ + HAPI_GeoType type; + HAPI_StringHandle nameSH; + + /// Use the node id to get the node's parameters. + /// Using the HDK, you can also get the raw node C++ pointer for this + /// object's internal node. + HAPI_NodeId nodeId; + + /// Whether the SOP node has been exposed by dragging it into the + /// editable nodes section of the asset definition. + HAPI_Bool isEditable; + + /// Has the templated flag turned on which means "expose as read-only". + HAPI_Bool isTemplated; + + /// Final Result (Display SOP). + HAPI_Bool isDisplayGeo; + + /// For incremental updates. + HAPI_Bool hasGeoChanged; + + /// @deprecated This variable is deprecated and should no longer be used. + /// Materials are now separate from parts. They are maintained at the + /// asset level so you only need to check if the material itself has + /// changed via ::HAPI_MaterialInfo::hasChanged instead of the material + /// on the part. + HAPI_Bool hasMaterialChanged; + + /// Groups. + /// @{ + int pointGroupCount; + int primitiveGroupCount; + int edgeGroupCount; + /// @} + + /// Total number of parts this geometry contains. + /// See @ref HAPI_Parts. + int partCount; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_GeoInfo ) + +/// Meta-data describing a Geo Part +struct HAPI_API HAPI_PartInfo +{ + /// Id to identify this part relative to it's Geo + HAPI_PartId id; + /// String handle for the name of the part + HAPI_StringHandle nameSH; + HAPI_PartType type; + + int faceCount; + int vertexCount; + /// Number of points. Note that this is NOT the number + /// of "positions" as "points" may imply. If your + /// geometry has 3 points then set this to 3 and not 3*3. + int pointCount; + + int attributeCounts[ HAPI_ATTROWNER_MAX ]; + + /// If this is true, don't display this part. Load its data but then + /// instance it where the corresponding instancer part tells you to + /// instance it. + HAPI_Bool isInstanced; + + /// The number of parts that this instancer part is instancing. + /// For example, if we're instancing a curve and a box, they would come + /// across as two parts, hence this count would be two. + /// Call ::HAPI_GetInstancedPartIds() to get the list of ::HAPI_PartId. + int instancedPartCount; + + /// The number of instances that this instancer part is instancing. + /// Using the same example as with ::HAPI_PartInfo::instancedPartCount, + /// if I'm instancing the merge of a curve and a box 5 times, this count + /// would be 5. To be clear, all instanced parts are instanced the same + /// number of times and with the same transform for each instance. + /// Call ::HAPI_GetInstancerPartTransforms() to get the transform of + /// each instance. + int instanceCount; + + /// If this is false, the underlying attribute data appear to match that of + /// the previous cook. In this case you may be able to re-used marshaled + /// data from the previous cook. + HAPI_Bool hasChanged; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_PartInfo ) + +/// Meta-data describing an attribute +/// See @ref HAPI_Attributes. +struct HAPI_API HAPI_AttributeInfo +{ + HAPI_Bool exists; + + HAPI_AttributeOwner owner; + HAPI_StorageType storage; + + /// When converting from the Houdini native GA geometry format to the + /// GT geometry format HAPI uses, some attributes might change owners. + /// For example, in Houdini GA curves can have points shared by + /// vertices but the GT format only supports curve vertices + /// (no points). This means that if you had point attributes on a curve + /// in Houdini, when it comes out of HAPI those point attributes will now + /// be vertex attributes. In this case, the ::HAPI_AttributeInfo::owner + /// will be set to ::HAPI_ATTROWNER_VERTEX but the + /// ::HAPI_AttributeInfo::originalOwner will be ::HAPI_ATTROWNER_POINT. + HAPI_AttributeOwner originalOwner; + + /// Number of attributes. This count will match the number of values + /// given the owner. For example, if the owner is ::HAPI_ATTROWNER_VERTEX + /// this count will be the same as the ::HAPI_PartInfo::vertexCount. + /// To be clear, this is not the number of values in the attribute, rather + /// it is the number of attributes. If your geometry has three 3D points + /// then this count will be 3 (not 3*3) while the + /// ::HAPI_AttributeInfo::tupleSize will be 3. + int count; + + /// Number of values per attribute. + /// Note that this is NOT the memory size of the attribute. It is the + /// number of values per attributes. Multiplying this by the + /// size of the ::HAPI_AttributeInfo::storage will give you the memory + /// size per attribute. + int tupleSize; + + /// Total number of elements for an array attribute. + /// An array attribute can be thought of as a 2 dimensional array where + /// the 2nd dimension can vary in size for each element in the 1st + /// dimension. Therefore this returns the total number of values in + /// the entire array. + /// This should be used to determine the total storage + /// size needed by multiplying with ::HAPI_AttributeInfo::storage. + /// Note that this will be 0 for a non-array attribute. + HAPI_Int64 totalArrayElements; + + /// Attribute type info + /// This is used to help identify the type of data stored in an attribute. + /// Using the type is recommended over using just an attribute's name to identify + /// its purpose. + HAPI_AttributeTypeInfo typeInfo; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_AttributeInfo ) + +// MATERIALS ---------------------------------------------------------------- + +struct HAPI_API HAPI_MaterialInfo +{ + /// This is the HAPI node id for the SHOP node this material is attached + /// to. Use it to get access to the parameters (which contain the + /// texture paths). + /// IMPORTANT: When the ::HAPI_MaterialInfo::hasChanged is true this + /// @p nodeId could have changed. Do not assume ::HAPI_MaterialInfo::nodeId + /// will never change for a specific material. + HAPI_NodeId nodeId; + + HAPI_Bool exists; + + HAPI_Bool hasChanged; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_MaterialInfo ) + +/// Describes an image format, used with ::HAPI_GetSupportedImageFileFormats() +struct HAPI_API HAPI_ImageFileFormat +{ + HAPI_StringHandle nameSH; + HAPI_StringHandle descriptionSH; + HAPI_StringHandle defaultExtensionSH; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_ImageFileFormat ) + +/// Data for an image, used with ::HAPI_GetImageInfo() and ::HAPI_SetImageInfo() +struct HAPI_API HAPI_ImageInfo +{ + /// Unlike the other members of this struct changing imageFileFormatNameSH + /// and giving this struct back to ::HAPI_SetImageInfo() nothing will happen. + /// Use this member variable to derive which image file format will be used + /// by the ::HAPI_ExtractImageToFile() and ::HAPI_ExtractImageToMemory() + /// functions if called with image_file_format_name set to NULL. This way, + /// you can decide whether to ask for a file format conversion (slower) or + /// not (faster). + /// (Read-only) + HAPI_StringHandle imageFileFormatNameSH; + + int xRes; + int yRes; + + HAPI_ImageDataFormat dataFormat; + + HAPI_Bool interleaved; /// ex: true = RGBRGBRGB, false = RRRGGGBBB + HAPI_ImagePacking packing; + + /// Adjust the gamma of the image. For anything less than + /// ::HAPI_IMAGE_DATA_INT16, you probably want to leave this as 2.2. + double gamma; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_ImageInfo ) + +// ANIMATION ---------------------------------------------------------------- + +/// Data for a single Key Frame +struct HAPI_API HAPI_Keyframe +{ + float time; + float value; + float inTangent; + float outTangent; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_Keyframe ) + +// VOLUMES ------------------------------------------------------------------ + +/// +/// This represents a volume primitive but does not contain the actual voxel +/// values, which can be retrieved on a per-tile basis. +/// +/// See @ref HAPI_Volumes. +/// +struct HAPI_API HAPI_VolumeInfo +{ + HAPI_StringHandle nameSH; + + HAPI_VolumeType type; + + /// Each voxel is identified with an index. The indices will range + /// between: + /// [ ( minX, minY, minZ ), ( minX+xLength, minY+yLength, minZ+zLength ) ) + /// @{ + int xLength; + int yLength; + int zLength; + int minX; + int minY; + int minZ; + /// @} + + /// Number of values per voxel. + /// The tuple size field is 1 for scalars and 3 for vector data. + int tupleSize; + + /// Can be either ::HAPI_STORAGETYPE_INT or ::HAPI_STORAGETYPE_FLOAT. + HAPI_StorageType storage; + + /// The dimensions of each tile. + /// This can be 8 or 16, denoting an 8x8x8 or 16x16x16 tiles. + int tileSize; + + /// The transform of the volume with respect to the lengths. + /// The volume may be positioned anywhere in space. + HAPI_Transform transform; + + /// Denotes special situations where the volume tiles are not perfect + /// cubes, but are tapered instead. + HAPI_Bool hasTaper; + + /// If there is taper involved, denotes the amount of taper involved. + /// @{ + float xTaper; + float yTaper; + /// @} +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_VolumeInfo ) + +/// +/// A HAPI_VolumeTileInfo represents an cube subarray of the volume. +/// The size of each dimension is ::HAPI_VolumeInfo::tileSize +/// bbox [(minX, minY, minZ), (minX+tileSize, minY+tileSize, minZ+tileSize)) +/// +struct HAPI_API HAPI_VolumeTileInfo +{ + int minX; + int minY; + int minZ; + HAPI_Bool isValid; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_VolumeTileInfo ) + +/// +/// Describes the visual settings of a volume. +/// +struct HAPI_API HAPI_VolumeVisualInfo +{ + HAPI_VolumeVisualType type; + float iso; + float density; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_VolumeVisualInfo ) + +// CURVES ------------------------------------------------------------------- + +/// +/// Represents the meta-data associated with a curve mesh (a number +/// of curves of the same type). +struct HAPI_API HAPI_CurveInfo +{ + HAPI_CurveType curveType; + + /// The number of curves contained in this curve mesh. + int curveCount; + + /// The number of control vertices (CVs) for all curves. + int vertexCount; + + /// The number of knots for all curves. + int knotCount; + + /// Whether the curves in this curve mesh are periodic (closed by appending a new point) + HAPI_Bool isPeriodic; + + /// Whether the curves in this curve mesh are rational. + HAPI_Bool isRational; + + /// Order of 1 is invalid. 0 means there is a varying order. + int order; + + /// Whether the curve has knots. + HAPI_Bool hasKnots; + + /// Similar to isPeriodic, but creates a polygon instead of a separate point + HAPI_Bool isClosed; +}; +HAPI_C_STRUCT_TYPEDEF(HAPI_CurveInfo) + +// Curve info dealing specifically with input curves +struct HAPI_API HAPI_InputCurveInfo +{ + /// The desired curve type of the curve. + /// Note that this is NOT necessarily equal to the value in HAPI_CurveInfo + /// in the case of curve refinement. + HAPI_CurveType curveType; + + /// The desired order for your input curve. + /// This is your desired order, which may differ from HAPI_CurveInfo + /// as it will do range checks and adjust the actual order accordingly. + int order; + + /// Whether or not the curve is closed. + /// May differ from HAPI_CurveInfo::isPeriodic depending on the curveType + /// (e.g. A NURBs curve is never technically closed according to HAPI_CurveInfo). + HAPI_Bool closed; + + /// Whether or not to reverse the curve input. + HAPI_Bool reverse; + + // Input method type (CVs or Brekapoints). + HAPI_InputCurveMethod inputMethod; + + // Parameterization - Only used when inputMethod is BREAKPOINTS + HAPI_InputCurveParameterization breakpointParameterization; + +}; +HAPI_C_STRUCT_TYPEDEF(HAPI_InputCurveInfo) + +// BASIC PRIMITIVES --------------------------------------------------------- + +/// Data for a Box Part +struct HAPI_API HAPI_BoxInfo +{ + float center[ HAPI_POSITION_VECTOR_SIZE ]; + float size[ HAPI_SCALE_VECTOR_SIZE ]; + float rotation[ HAPI_EULER_VECTOR_SIZE ]; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_BoxInfo ) + +/// Data for a Sphere Part +struct HAPI_API HAPI_SphereInfo +{ + float center[ HAPI_POSITION_VECTOR_SIZE ]; + float radius; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_SphereInfo ) + +// PDG Structs -------------------------------------------------------------- + +/// Data associated with a PDG Event +struct HAPI_API HAPI_PDG_EventInfo +{ + /// id of related node. + HAPI_NodeId nodeId; + + /// id of related work item. + HAPI_PDG_WorkItemId workItemId; + + /// id of related work item dependency. + HAPI_PDG_WorkItemId dependencyId; + + /// ::HAPI_PDG_WorkItemState value of current state for state change. + int currentState; + + /// ::HAPI_PDG_WorkItemState value of last state for state change. + int lastState; + + /// ::HAPI_PDG_EventType event type. + int eventType; + + /// String handle of the event message (> 0 if there is a message). + HAPI_StringHandle msgSH; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_PDG_EventInfo ) + +/// Info for a PDG work item +struct HAPI_API HAPI_PDG_WorkItemInfo +{ + /// The work item's index + int index; + + /// The number of output files reported by the work item. + int outputFileCount; + + /// Name of the work item + HAPI_StringHandle nameSH; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_PDG_WorkItemInfo ) + +/// Backwards compatibility for HAPI_PDG_WorkitemInfo +typedef HAPI_PDG_WorkItemInfo HAPI_PDG_WorkitemInfo; + +/// Data for a PDG output file +struct HAPI_API HAPI_PDG_WorkItemOutputFile +{ + /// The file path of the output. + int filePathSH; + + /// The file tag of the output. + int tagSH; + + /// Hash of the output file. Properties such as the file path and + /// modification time of the file are typical inputs to the hash function, + /// but this will vary based on the node that is producing the output file. + HAPI_Int64 hash; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_PDG_WorkItemOutputFile ) + +/// Backwards compatibility for HAPI_PDG_WorkitemResultInfo +typedef HAPI_PDG_WorkItemOutputFile HAPI_PDG_WorkitemResultInfo; + +// SESSIONSYNC -------------------------------------------------------------- + +/// +/// Contains the information for synchronizing viewport between Houdini +/// and other applications. When SessionSync is enabled, Houdini will +/// update this struct with its viewport state. It will also update +/// its own viewport if this struct has changed. +/// The data stored is in Houdini's right-handed Y-up coordinate system. +/// +struct HAPI_API HAPI_Viewport +{ + /// The world position of the viewport camera's pivot. + float position[ HAPI_POSITION_VECTOR_SIZE ]; + + /// The direction of the viewport camera stored as a quaternion. + float rotationQuaternion[ HAPI_QUATERNION_VECTOR_SIZE ]; + + /// The offset from the pivot to the viewport camera's + /// actual world position. + float offset; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_Viewport ) + +/// +/// Contains the information for synchronizing general SessionSync +/// state between Houdini and other applications. When SessionSync +/// is enabled, Houdini will update this struct with its state. +/// It will also update its internal state if this struct has +/// changed. +/// +struct HAPI_API HAPI_SessionSyncInfo +{ + /// Specifies whether Houdini's current time is used for Houdini Engine + /// cooks. This is automatically enabled in SessionSync where + /// Houdini's viewport forces cooks to use Houdini's current time. + /// This is disabled in non-SessionSync mode, but can be toggled to + /// override default behaviour. + HAPI_Bool cookUsingHoudiniTime; + + /// Specifies whether viewport synchronization is enabled. If enabled, + /// in SessionSync, Houdini will update its own viewport using + /// ::HAPI_Viewport. + HAPI_Bool syncViewport; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_SessionSyncInfo ) + +/// Configuration options for Houdini's compositing context +struct HAPI_API HAPI_CompositorOptions +{ + /// Specifies the maximum allowed width of an image in the compositor + int maximumResolutionX; + + /// Specifies the maximum allowed height of an image in the compositor + int maximumResolutionY; +}; +HAPI_C_STRUCT_TYPEDEF( HAPI_CompositorOptions ) + +#endif // __HAPI_COMMON_h__ + +``` + +`Source/HoudiniEngine/Public/HAPI/HAPI_Helpers.h`: + +```h +/* + * PROPRIETARY INFORMATION. This software is proprietary to + * Side Effects Software Inc., and is not to be reproduced, + * transmitted, or disclosed in any way without written permission. + * + * Produced by: + * Side Effects Software Inc + * 123 Front Street West, Suite 1401 + * Toronto, Ontario + * Canada M5J 2M2 + * 416-504-9876 + * + * COMMENTS: + */ + +#ifndef __HAPI_HELPERS_h__ +#define __HAPI_HELPERS_h__ + +#include "HAPI_API.h" +#include "HAPI_Common.h" + +// Session ------------------------------------------------------------------ + +HAPI_DECL_RETURN( void ) + HAPI_SessionInfo_Init( HAPI_SessionInfo * in ); +HAPI_DECL_RETURN( HAPI_SessionInfo ) + HAPI_SessionInfo_Create(); + +// Server ------------------------------------------------------------------- + +HAPI_DECL_RETURN( void ) + HAPI_ThriftServerOptions_Init( HAPI_ThriftServerOptions * in ); +HAPI_DECL_RETURN( HAPI_ThriftServerOptions ) + HAPI_ThriftServerOptions_Create(); + +// TIME --------------------------------------------------------------------- + +HAPI_DECL_RETURN( void ) + HAPI_TimelineOptions_Init( HAPI_TimelineOptions * in ); +HAPI_DECL_RETURN( HAPI_TimelineOptions ) + HAPI_TimelineOptions_Create(); + +// COMPOSITOR SETTINGS ------------------------------------------------------ + +HAPI_DECL_RETURN( void) + HAPI_CompositorOptions_Init( HAPI_CompositorOptions * in ); +HAPI_DECL_RETURN( HAPI_CompositorOptions ) + HAPI_CompositorOptions_Create(); + +// ASSETS ------------------------------------------------------------------- + +HAPI_DECL_RETURN( void ) + HAPI_AssetInfo_Init( HAPI_AssetInfo * in ); +HAPI_DECL_RETURN( HAPI_AssetInfo ) + HAPI_AssetInfo_Create(); + +HAPI_DECL_RETURN( void ) + HAPI_CookOptions_Init( HAPI_CookOptions * in ); +HAPI_DECL_RETURN( HAPI_CookOptions ) + HAPI_CookOptions_Create(); +HAPI_DECL_RETURN( HAPI_Bool ) + HAPI_CookOptions_AreEqual( + const HAPI_CookOptions * left, + const HAPI_CookOptions * right ); + +// NODES -------------------------------------------------------------------- + +HAPI_DECL_RETURN( void ) + HAPI_NodeInfo_Init( HAPI_NodeInfo * in ); +HAPI_DECL_RETURN( HAPI_NodeInfo ) + HAPI_NodeInfo_Create(); + +// PARAMETERS --------------------------------------------------------------- + +/// Clears the struct to default values. +HAPI_DECL_RETURN( void ) + HAPI_ParmInfo_Init( HAPI_ParmInfo * in ); + +/// Creates a struct with default values and returns it. +HAPI_DECL_RETURN( HAPI_ParmInfo ) + HAPI_ParmInfo_Create(); + +/// Convenience function that checks on the value of the ::HAPI_ParmInfo::type +/// field to tell you the underlying data type. +/// @{ +HAPI_DECL_RETURN( HAPI_Bool ) + HAPI_ParmInfo_IsInt( const HAPI_ParmInfo * in ); +HAPI_DECL_RETURN( HAPI_Bool ) + HAPI_ParmInfo_IsFloat( const HAPI_ParmInfo * in ); +HAPI_DECL_RETURN( HAPI_Bool ) + HAPI_ParmInfo_IsString( const HAPI_ParmInfo * in ); +HAPI_DECL_RETURN( HAPI_Bool ) + HAPI_ParmInfo_IsPath( const HAPI_ParmInfo * in ); +HAPI_DECL_RETURN( HAPI_Bool ) + HAPI_ParmInfo_IsNode( const HAPI_ParmInfo * in ); +/// @} + +/// Parameter has no underlying No data type. Examples of this are UI items +/// such as folder lists and separators. +HAPI_DECL_RETURN( HAPI_Bool ) + HAPI_ParmInfo_IsNonValue( const HAPI_ParmInfo * in ); + +/// Convenience function. If the parameter can be represented by this data +/// type, it returns ::HAPI_ParmInfo::size, and zero otherwise. +/// @{ +HAPI_DECL_RETURN( int ) + HAPI_ParmInfo_GetIntValueCount( const HAPI_ParmInfo * in ); +HAPI_DECL_RETURN( int ) + HAPI_ParmInfo_GetFloatValueCount( const HAPI_ParmInfo * in ); +HAPI_DECL_RETURN( int ) + HAPI_ParmInfo_GetStringValueCount( const HAPI_ParmInfo* in ); +/// @} + +HAPI_DECL_RETURN( void ) + HAPI_ParmChoiceInfo_Init( HAPI_ParmChoiceInfo * in ); +HAPI_DECL_RETURN( HAPI_ParmChoiceInfo ) + HAPI_ParmChoiceInfo_Create(); + +// HANDLES ------------------------------------------------------------------ + +HAPI_DECL_RETURN( void ) + HAPI_HandleInfo_Init( HAPI_HandleInfo * in ); +HAPI_DECL_RETURN( HAPI_HandleInfo ) + HAPI_HandleInfo_Create(); + +HAPI_DECL_RETURN( void ) + HAPI_HandleBindingInfo_Init( HAPI_HandleBindingInfo * in ); +HAPI_DECL_RETURN( HAPI_HandleBindingInfo ) + HAPI_HandleBindingInfo_Create(); + +// OBJECTS ------------------------------------------------------------------ + +HAPI_DECL_RETURN( void ) + HAPI_ObjectInfo_Init( HAPI_ObjectInfo * in ); +HAPI_DECL_RETURN( HAPI_ObjectInfo ) + HAPI_ObjectInfo_Create(); + +// GEOMETRY ----------------------------------------------------------------- + +HAPI_DECL_RETURN( void ) + HAPI_GeoInfo_Init( HAPI_GeoInfo * in ); +HAPI_DECL_RETURN( HAPI_GeoInfo ) + HAPI_GeoInfo_Create(); +HAPI_DECL_RETURN( int ) + HAPI_GeoInfo_GetGroupCountByType( + HAPI_GeoInfo * in, HAPI_GroupType type ); + +HAPI_DECL_RETURN( void ) + HAPI_PartInfo_Init( HAPI_PartInfo * in ); +HAPI_DECL_RETURN( HAPI_PartInfo ) + HAPI_PartInfo_Create(); +HAPI_DECL_RETURN( int ) + HAPI_PartInfo_GetElementCountByAttributeOwner( + HAPI_PartInfo * in, HAPI_AttributeOwner owner ); +HAPI_DECL_RETURN( int ) + HAPI_PartInfo_GetElementCountByGroupType( + HAPI_PartInfo * in, HAPI_GroupType type ); +HAPI_DECL_RETURN( int ) + HAPI_PartInfo_GetAttributeCountByOwner( + HAPI_PartInfo * in, HAPI_AttributeOwner owner ); + +HAPI_DECL_RETURN( void ) + HAPI_AttributeInfo_Init( HAPI_AttributeInfo * in ); +HAPI_DECL_RETURN( HAPI_AttributeInfo ) + HAPI_AttributeInfo_Create(); + +// MATERIALS ---------------------------------------------------------------- + +HAPI_DECL_RETURN( void ) + HAPI_MaterialInfo_Init( HAPI_MaterialInfo * in ); +HAPI_DECL_RETURN( HAPI_MaterialInfo ) + HAPI_MaterialInfo_Create(); + +HAPI_DECL_RETURN( void ) + HAPI_ImageFileFormat_Init( HAPI_ImageFileFormat *in ); +HAPI_DECL_RETURN( HAPI_ImageFileFormat ) + HAPI_ImageFileFormat_Create(); + +HAPI_DECL_RETURN( void ) + HAPI_ImageInfo_Init( HAPI_ImageInfo * in ); +HAPI_DECL_RETURN( HAPI_ImageInfo ) + HAPI_ImageInfo_Create(); + +// ANIMATION ---------------------------------------------------------------- + +HAPI_DECL_RETURN( void ) + HAPI_Keyframe_Init( HAPI_Keyframe * in ); +HAPI_DECL_RETURN( HAPI_Keyframe ) + HAPI_Keyframe_Create(); + +// VOLUMES ------------------------------------------------------------------ + +HAPI_DECL_RETURN( void ) + HAPI_VolumeInfo_Init( HAPI_VolumeInfo * in ); +HAPI_DECL_RETURN( HAPI_VolumeInfo ) + HAPI_VolumeInfo_Create(); + +HAPI_DECL_RETURN( void ) + HAPI_VolumeTileInfo_Init( HAPI_VolumeTileInfo * in ); +HAPI_DECL_RETURN( HAPI_VolumeTileInfo ) + HAPI_VolumeTileInfo_Create(); + +// CURVES ------------------------------------------------------------------- + +HAPI_DECL_RETURN( void ) + HAPI_CurveInfo_Init( HAPI_CurveInfo * in ); +HAPI_DECL_RETURN( HAPI_CurveInfo ) + HAPI_CurveInfo_Create(); + +// CURVE INPUTS ------------------------------------------------------------ + +HAPI_DECL_RETURN( void ) + HAPI_InputCurveInfo_Init( HAPI_InputCurveInfo * in ); +HAPI_DECL_RETURN( HAPI_InputCurveInfo ) + HAPI_InputCurveInfo_Create(); + +// TRANSFORMS --------------------------------------------------------------- + +HAPI_DECL_RETURN( void ) + HAPI_Transform_Init( HAPI_Transform * in ); + +HAPI_DECL_RETURN( HAPI_Transform ) + HAPI_Transform_Create(); + +HAPI_DECL_RETURN( void ) + HAPI_TransformEuler_Init( HAPI_TransformEuler * in ); + +HAPI_DECL_RETURN( HAPI_TransformEuler ) + HAPI_TransformEuler_Create(); + +// SESSIONSYNC -------------------------------------------------------------- + +HAPI_DECL_RETURN (void ) + HAPI_Viewport_Init( HAPI_Viewport * in ); + +HAPI_DECL_RETURN( HAPI_Viewport ) + HAPI_Viewport_Create(); + +HAPI_DECL_RETURN (void ) + HAPI_SessionSyncInfo_Init( HAPI_SessionSyncInfo * in ); + +HAPI_DECL_RETURN( HAPI_SessionSyncInfo ) + HAPI_SessionSyncInfo_Create(); + +#endif // __HAPI_HELPERS_h__ + +``` + +`Source/HoudiniEngine/Public/HAPI/HAPI_Version.h`: + +```h +/* +* Copyright (c) <2026> Side Effects Software Inc.* +* Permission is hereby granted, free of charge, to any person obtaining a copy* of this software and associated documentation files (the "Software"), to deal* in the Software without restriction, including without limitation the rights* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell* copies of the Software, and to permit persons to whom the Software is* furnished to do so, subject to the following conditions:** The above copyright notice and this permission notice shall be included in all* copies or substantial portions of the Software.** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE* SOFTWARE. * + * Produced by: + * Side Effects Software Inc + * 123 Front Street West, Suite 1401 + * Toronto, Ontario + * Canada M5J 2M2 + * 416-504-9876 + * + * COMMENTS: + * Generated version information to be used when linking for + * sanity checks. + */ + +///////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////// +// WARNING! This file is GENERATED by UNREAL_Version.py script. +// DO NOT modify manually or commit to the repository! +///////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////// + +#ifndef __HAPI_VERSION_h__ +#define __HAPI_VERSION_h__ + +// The three components of the Houdini version that HAPI is +// expecting to compile against. +#define HAPI_VERSION_HOUDINI_MAJOR 21 + +#define HAPI_VERSION_HOUDINI_MINOR 0 + +#define HAPI_VERSION_HOUDINI_BUILD 631 +#define HAPI_VERSION_HOUDINI_PATCH 0 + +// The two components of the Houdini Engine (marketed) version. +#define HAPI_VERSION_HOUDINI_ENGINE_MAJOR 8 +#define HAPI_VERSION_HOUDINI_ENGINE_MINOR 0 + +// This is a monotonously increasing API version number that can be used +// to lock against a certain API for compatibility purposes. Basically, +// when this number changes code compiled against the HAPI.h methods +// might no longer compile. Semantic changes to the methods will also +// cause this version to increase. This number will be reset to 0 +// every time the Houdini Engine version is bumped. +#define HAPI_VERSION_HOUDINI_ENGINE_API 3 + +#endif // __HAPI_VERSION_h__ + +``` + +`Source/HoudiniEngine/Public/HoudiniApi.h`: + +```h +/* + * Copyright (c) <2025> Side Effects Software Inc. * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * + * COMMENTS: + * This file is generated. Do not modify directly. + */ + +#pragma once +#include "HAPI/HAPI.h" +#include "HAL/PlatformProcess.h" + + +struct HOUDINIENGINE_API FHoudiniApi +{ +public: + + static void InitializeHAPI(void* LibraryHandle); + static void FinalizeHAPI(); + static bool IsHAPIInitialized(); + +public: + + typedef HAPI_Result (*AddAttributeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info); + typedef HAPI_Result (*AddGroupFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name); + typedef HAPI_AssetInfo (*AssetInfo_CreateFuncPtr)(); + typedef void (*AssetInfo_InitFuncPtr)(HAPI_AssetInfo * in); + typedef HAPI_AttributeInfo (*AttributeInfo_CreateFuncPtr)(); + typedef void (*AttributeInfo_InitFuncPtr)(HAPI_AttributeInfo * in); + typedef HAPI_Result (*BindCustomImplementationFuncPtr)(HAPI_SessionType session_type, const char * dll_path); + typedef HAPI_Result (*CancelPDGCookFuncPtr)(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id); + typedef HAPI_Result (*CheckForSpecificErrorsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ErrorCodeBits errors_to_look_for, HAPI_ErrorCodeBits * errors_found); + typedef HAPI_Result (*CleanupFuncPtr)(const HAPI_Session * session); + typedef HAPI_Result (*ClearConnectionErrorFuncPtr)(); + typedef HAPI_Result (*CloseSessionFuncPtr)(const HAPI_Session * session); + typedef HAPI_Result (*CommitGeoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id); + typedef HAPI_Result (*CommitWorkItemsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id); + typedef HAPI_Result (*CommitWorkitemsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id); + typedef HAPI_Result (*ComposeChildNodeListFuncPtr)(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeTypeBits node_type_filter, HAPI_NodeFlagsBits node_flags_filter, HAPI_Bool recursive, int * count); + typedef HAPI_Result (*ComposeNodeCookResultFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_StatusVerbosity verbosity, int * buffer_length); + typedef HAPI_Result (*ComposeObjectListFuncPtr)(const HAPI_Session * session, HAPI_NodeId parent_node_id, const char * categories, int * object_count); + typedef HAPI_CompositorOptions (*CompositorOptions_CreateFuncPtr)(); + typedef void (*CompositorOptions_InitFuncPtr)(HAPI_CompositorOptions * in); + typedef HAPI_Result (*ConnectNodeInputFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int input_index, HAPI_NodeId node_id_to_connect, int output_index); + typedef HAPI_Result (*ConvertMatrixToEulerFuncPtr)(const HAPI_Session * session, const float * matrix, HAPI_RSTOrder rst_order, HAPI_XYZOrder rot_order, HAPI_TransformEuler * transform_out); + typedef HAPI_Result (*ConvertMatrixToQuatFuncPtr)(const HAPI_Session * session, const float * matrix, HAPI_RSTOrder rst_order, HAPI_Transform * transform_out); + typedef HAPI_Result (*ConvertTransformFuncPtr)(const HAPI_Session * session, const HAPI_TransformEuler * transform_in, HAPI_RSTOrder rst_order, HAPI_XYZOrder rot_order, HAPI_TransformEuler * transform_out); + typedef HAPI_Result (*ConvertTransformEulerToMatrixFuncPtr)(const HAPI_Session * session, const HAPI_TransformEuler * transform, float * matrix); + typedef HAPI_Result (*ConvertTransformQuatToMatrixFuncPtr)(const HAPI_Session * session, const HAPI_Transform * transform, float * matrix); + typedef HAPI_Result (*CookNodeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const HAPI_CookOptions * cook_options); + typedef HAPI_Bool (*CookOptions_AreEqualFuncPtr)(const HAPI_CookOptions * left, const HAPI_CookOptions * right); + typedef HAPI_CookOptions (*CookOptions_CreateFuncPtr)(); + typedef void (*CookOptions_InitFuncPtr)(HAPI_CookOptions * in); + typedef HAPI_Result (*CookPDGFuncPtr)(const HAPI_Session * session, HAPI_NodeId cook_node_id, int generate_only, int blocking); + typedef HAPI_Result (*CookPDGAllOutputsFuncPtr)(const HAPI_Session* session, HAPI_NodeId cook_node_id, int generate_only, int blocking); + typedef HAPI_Result (*CreateCOPImageFuncPtr)(const HAPI_Session * session, HAPI_NodeId parent_node_id, const int width, const int height, const HAPI_ImagePacking packing, HAPI_Bool flip_x, HAPI_Bool flip_y, const float * data_array, int start, int length); + typedef HAPI_Result (*CreateCustomSessionFuncPtr)(HAPI_SessionType session_type, void * session_info, HAPI_Session * session); + typedef HAPI_Result (*CreateHeightFieldInputFuncPtr)(const HAPI_Session * session, HAPI_NodeId parent_node_id, const char * name, int xsize, int ysize, float voxelsize, HAPI_HeightFieldSampling sampling, HAPI_NodeId * heightfield_node_id, HAPI_NodeId * height_node_id, HAPI_NodeId * mask_node_id, HAPI_NodeId * merge_node_id); + typedef HAPI_Result (*CreateHeightfieldInputVolumeNodeFuncPtr)(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeId * new_node_id, const char * name, int xsize, int ysize, float voxelsize); + typedef HAPI_Result (*CreateInProcessSessionFuncPtr)(HAPI_Session * session, const HAPI_SessionInfo * session_info); + typedef HAPI_Result (*CreateInputCurveNodeFuncPtr)(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeId * node_id, const char * name); + typedef HAPI_Result (*CreateInputNodeFuncPtr)(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeId * node_id, const char * name); + typedef HAPI_Result (*CreateNodeFuncPtr)(const HAPI_Session * session, HAPI_NodeId parent_node_id, const char * operator_name, const char * node_label, HAPI_Bool cook_on_creation, HAPI_NodeId * new_node_id); + typedef HAPI_Result (*CreateThriftNamedPipeSessionFuncPtr)(HAPI_Session * session, const char * pipe_name, const HAPI_SessionInfo * session_info); + typedef HAPI_Result (*CreateThriftSharedMemorySessionFuncPtr)(HAPI_Session * session, const char * shared_mem_name, const HAPI_SessionInfo * session_info); + typedef HAPI_Result (*CreateThriftSocketSessionFuncPtr)(HAPI_Session * session, const char * host_name, int port, const HAPI_SessionInfo * session_info); + typedef HAPI_Result (*CreateWorkItemFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId * work_item_id, const char * name, int index); + typedef HAPI_Result (*CreateWorkitemFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId * workitem_id, const char * name, int index); + typedef HAPI_CurveInfo (*CurveInfo_CreateFuncPtr)(); + typedef void (*CurveInfo_InitFuncPtr)(HAPI_CurveInfo * in); + typedef HAPI_Result (*DeleteAttributeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info); + typedef HAPI_Result (*DeleteGroupFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name); + typedef HAPI_Result (*DeleteNodeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id); + typedef HAPI_Result (*DirtyPDGNodeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_Bool clean_results); + typedef HAPI_Result (*DisconnectNodeInputFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int input_index); + typedef HAPI_Result (*DisconnectNodeOutputsAtFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int output_index); + typedef HAPI_Result (*ExtractImageToFileFuncPtr)(const HAPI_Session * session, HAPI_NodeId material_node_id, const char * image_file_format_name, const char * image_planes, const char * destination_folder_path, const char * destination_file_name, int * destination_file_path); + typedef HAPI_Result (*ExtractImageToMemoryFuncPtr)(const HAPI_Session * session, HAPI_NodeId material_node_id, const char * image_file_format_name, const char * image_planes, int * buffer_size); + typedef HAPI_GeoInfo (*GeoInfo_CreateFuncPtr)(); + typedef int (*GeoInfo_GetGroupCountByTypeFuncPtr)(HAPI_GeoInfo * in, HAPI_GroupType type); + typedef void (*GeoInfo_InitFuncPtr)(HAPI_GeoInfo * in); + typedef HAPI_Result (*GetActiveCacheCountFuncPtr)(const HAPI_Session * session, int * active_cache_count); + typedef HAPI_Result (*GetActiveCacheNamesFuncPtr)(const HAPI_Session * session, HAPI_StringHandle * cache_names_array, int active_cache_count); + typedef HAPI_Result (*GetAssetDefinitionParmCountsFuncPtr)(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, int * parm_count, int * int_value_count, int * float_value_count, int * string_value_count, int * choice_value_count); + typedef HAPI_Result (*GetAssetDefinitionParmInfosFuncPtr)(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, HAPI_ParmInfo * parm_infos_array, int start, int length); + typedef HAPI_Result (*GetAssetDefinitionParmTagNameFuncPtr)(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, HAPI_ParmId parm_id, int tag_index, HAPI_StringHandle * tag_name); + typedef HAPI_Result (*GetAssetDefinitionParmTagValueFuncPtr)(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, HAPI_ParmId parm_id, const char * tag_name, HAPI_StringHandle * tag_value); + typedef HAPI_Result (*GetAssetDefinitionParmValuesFuncPtr)(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, int * int_values_array, int int_start, int int_length, float * float_values_array, int float_start, int float_length, HAPI_Bool string_evaluate, HAPI_StringHandle * string_values_array, int string_start, int string_length, HAPI_ParmChoiceInfo * choice_values_array, int choice_start, int choice_length); + typedef HAPI_Result (*GetAssetInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_AssetInfo * asset_info); + typedef HAPI_Result (*GetAssetLibraryFilePathFuncPtr)(const HAPI_Session * session, HAPI_AssetLibraryId asset_library_id, HAPI_StringHandle * file_path_sh); + typedef HAPI_Result (*GetAssetLibraryIdsFuncPtr)(const HAPI_Session * session, HAPI_AssetLibraryId * asset_library_ids_array, int start, int length); + typedef HAPI_Result (*GetAttributeDictionaryArrayDataFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, HAPI_AttributeInfo* attr_info, HAPI_StringHandle* data_fixed_array, int data_fixed_length, int* sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*GetAttributeDictionaryArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*GetAttributeDictionaryDataFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, HAPI_AttributeInfo* attr_info, HAPI_StringHandle* data_array, int start, int length); + typedef HAPI_Result (*GetAttributeDictionaryDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*GetAttributeFloat64ArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, double * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*GetAttributeFloat64ArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, double * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*GetAttributeFloat64DataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, double * data_array, int start, int length); + typedef HAPI_Result (*GetAttributeFloat64DataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, double * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*GetAttributeFloatArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, float * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*GetAttributeFloatArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, float * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*GetAttributeFloatDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, float * data_array, int start, int length); + typedef HAPI_Result (*GetAttributeFloatDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, float * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*GetAttributeInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeOwner owner, HAPI_AttributeInfo * attr_info); + typedef HAPI_Result (*GetAttributeInt16ArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_Int16 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*GetAttributeInt16ArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_Int16 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*GetAttributeInt16DataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int16 * data_array, int start, int length); + typedef HAPI_Result (*GetAttributeInt16DataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int16 * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*GetAttributeInt64ArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_Int64 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*GetAttributeInt64ArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_Int64 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*GetAttributeInt64DataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int64 * data_array, int start, int length); + typedef HAPI_Result (*GetAttributeInt64DataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int64 * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*GetAttributeInt8ArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_Int8 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*GetAttributeInt8ArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_Int8 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*GetAttributeInt8DataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int8 * data_array, int start, int length); + typedef HAPI_Result (*GetAttributeInt8DataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int8 * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*GetAttributeIntArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*GetAttributeIntArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, int * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*GetAttributeIntDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, int * data_array, int start, int length); + typedef HAPI_Result (*GetAttributeIntDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, int * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*GetAttributeNamesFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_AttributeOwner owner, HAPI_StringHandle * attribute_names_array, int count); + typedef HAPI_Result (*GetAttributeStringArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*GetAttributeStringArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*GetAttributeStringDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_array, int start, int length); + typedef HAPI_Result (*GetAttributeStringDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*GetAttributeUInt8ArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_UInt8 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*GetAttributeUInt8ArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_UInt8 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*GetAttributeUInt8DataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_UInt8 * data_array, int start, int length); + typedef HAPI_Result (*GetAttributeUInt8DataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_UInt8 * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*GetAvailableAssetCountFuncPtr)(const HAPI_Session * session, HAPI_AssetLibraryId library_id, int * asset_count); + typedef HAPI_Result (*GetAvailableAssetsFuncPtr)(const HAPI_Session * session, HAPI_AssetLibraryId library_id, HAPI_StringHandle * asset_names_array, int asset_count); + typedef HAPI_Result (*GetBoxInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId geo_node_id, HAPI_PartId part_id, HAPI_BoxInfo * box_info); + typedef HAPI_Result (*GetCachePropertyFuncPtr)(const HAPI_Session * session, const char * cache_name, HAPI_CacheProperty cache_property, int * property_value); + typedef HAPI_Result (*GetComposedChildNodeListFuncPtr)(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeId * child_node_ids_array, int count); + typedef HAPI_Result (*GetComposedNodeCookResultFuncPtr)(const HAPI_Session * session, char * string_value, int length); + typedef HAPI_Result (*GetComposedObjectListFuncPtr)(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_ObjectInfo * object_infos_array, int start, int length); + typedef HAPI_Result (*GetComposedObjectTransformsFuncPtr)(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_RSTOrder rst_order, HAPI_Transform * transform_array, int start, int length); + typedef HAPI_Result (*GetCompositorOptionsFuncPtr)(const HAPI_Session * session, HAPI_CompositorOptions * compositor_options); + typedef HAPI_Result (*GetConnectionErrorFuncPtr)(char * string_value, int length, HAPI_Bool clear); + typedef HAPI_Result (*GetConnectionErrorLengthFuncPtr)(int * buffer_length); + typedef HAPI_Result (*GetCookingCurrentCountFuncPtr)(const HAPI_Session * session, int * count); + typedef HAPI_Result (*GetCookingTotalCountFuncPtr)(const HAPI_Session * session, int * count); + typedef HAPI_Result (*GetCurveCountsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * counts_array, int start, int length); + typedef HAPI_Result (*GetCurveInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_CurveInfo * info); + typedef HAPI_Result (*GetCurveKnotsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, float * knots_array, int start, int length); + typedef HAPI_Result (*GetCurveOrdersFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * orders_array, int start, int length); + typedef HAPI_Result (*GetDisplayGeoInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId object_node_id, HAPI_GeoInfo * geo_info); + typedef HAPI_Result (*GetEdgeCountOfEdgeGroupFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * group_name, int * edge_count); + typedef HAPI_Result (*GetEnvIntFuncPtr)(HAPI_EnvIntType int_type, int * value); + typedef HAPI_Result (*GetFaceCountsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * face_counts_array, int start, int length); + typedef HAPI_Result (*GetFirstVolumeTileFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_VolumeTileInfo * tile); + typedef HAPI_Result (*GetGeoInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_GeoInfo * geo_info); + typedef HAPI_Result (*GetGeoSizeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * format, int * size); + typedef HAPI_Result (*GetGroupCountOnPackedInstancePartFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * pointGroupCount, int * primitiveGroupCount); + typedef HAPI_Result (*GetGroupMembershipFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name, HAPI_Bool * membership_array_all_equal, int * membership_array, int start, int length); + typedef HAPI_Result (*GetGroupMembershipOnPackedInstancePartFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name, HAPI_Bool * membership_array_all_equal, int * membership_array, int start, int length); + typedef HAPI_Result (*GetGroupNamesFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_GroupType group_type, HAPI_StringHandle * group_names_array, int group_count); + typedef HAPI_Result (*GetGroupNamesOnPackedInstancePartFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, HAPI_StringHandle * group_names_array, int group_count); + typedef HAPI_Result (*GetHIPFileNodeCountFuncPtr)(const HAPI_Session * session, HAPI_HIPFileId id, int * count); + typedef HAPI_Result (*GetHIPFileNodeIdsFuncPtr)(const HAPI_Session * session, HAPI_HIPFileId id, HAPI_NodeId * node_ids, int length); + typedef HAPI_Result (*GetHandleBindingInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int handle_index, HAPI_HandleBindingInfo * handle_binding_infos_array, int start, int length); + typedef HAPI_Result (*GetHandleInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_HandleInfo * handle_infos_array, int start, int length); + typedef HAPI_Result (*GetHeightFieldDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, float * values_array, int start, int length); + typedef HAPI_Result (*GetImageFilePathFuncPtr)(const HAPI_Session * session, HAPI_NodeId material_node_id, const char * image_file_format_name, const char * image_planes, const char * destination_folder_path, const char * destination_file_name, HAPI_ParmId texture_parm_id, int * destination_file_path); + typedef HAPI_Result (*GetImageInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId material_node_id, HAPI_ImageInfo * image_info); + typedef HAPI_Result (*GetImageMemoryBufferFuncPtr)(const HAPI_Session * session, HAPI_NodeId material_node_id, char * buffer, int length); + typedef HAPI_Result (*GetImagePlaneCountFuncPtr)(const HAPI_Session * session, HAPI_NodeId material_node_id, int * image_plane_count); + typedef HAPI_Result (*GetImagePlanesFuncPtr)(const HAPI_Session * session, HAPI_NodeId material_node_id, HAPI_StringHandle * image_planes_array, int image_plane_count); + typedef HAPI_Result (*GetInputCurveInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_InputCurveInfo * info); + typedef HAPI_Result (*GetInstanceTransformsOnPartFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_RSTOrder rst_order, HAPI_Transform * transforms_array, int start, int length); + typedef HAPI_Result (*GetInstancedObjectIdsFuncPtr)(const HAPI_Session * session, HAPI_NodeId object_node_id, HAPI_NodeId * instanced_node_id_array, int start, int length); + typedef HAPI_Result (*GetInstancedPartIdsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_PartId * instanced_parts_array, int start, int length); + typedef HAPI_Result (*GetInstancerPartTransformsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_RSTOrder rst_order, HAPI_Transform * transforms_array, int start, int length); + typedef HAPI_Result (*GetJobStatusFuncPtr)(const HAPI_Session * session, int job_id, HAPI_JobStatus * job_status); + typedef HAPI_Result (*GetLoadedAssetLibraryCountFuncPtr)(const HAPI_Session * session, int * count); + typedef HAPI_Result (*GetManagerNodeIdFuncPtr)(const HAPI_Session * session, HAPI_NodeType node_type, HAPI_NodeId * node_id); + typedef HAPI_Result (*GetMaterialInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId material_node_id, HAPI_MaterialInfo * material_info); + typedef HAPI_Result (*GetMaterialNodeIdsOnFacesFuncPtr)(const HAPI_Session * session, HAPI_NodeId geometry_node_id, HAPI_PartId part_id, HAPI_Bool * are_all_the_same, HAPI_NodeId * material_ids_array, int start, int length); + typedef HAPI_Result (*GetMessageNodeCountFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int * count); + typedef HAPI_Result (*GetMessageNodeIdsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeId * message_node_ids_array, int count); + typedef HAPI_Result (*GetNextVolumeTileFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_VolumeTileInfo * tile); + typedef HAPI_Result (*GetNodeCookResultFuncPtr)(const HAPI_Session * session, char * string_value, int length); + typedef HAPI_Result (*GetNodeCookResultLengthFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_StatusVerbosity verbosity, int * buffer_length); + typedef HAPI_Result (*GetNodeFromPathFuncPtr)(const HAPI_Session * session, const HAPI_NodeId parent_node_id, const char * path, HAPI_NodeId * node_id); + typedef HAPI_Result (*GetNodeInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeInfo * node_info); + typedef HAPI_Result (*GetNodeInputNameFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int input_idx, HAPI_StringHandle * name); + typedef HAPI_Result (*GetNodeOutputNameFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int output_idx, HAPI_StringHandle * name); + typedef HAPI_Result (*GetNodePathFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeId relative_to_node_id, HAPI_StringHandle * path); + typedef HAPI_Result (*GetNumWorkItemsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int * num); + typedef HAPI_Result (*GetNumWorkitemsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int * num); + typedef HAPI_Result (*GetObjectInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ObjectInfo * object_info); + typedef HAPI_Result (*GetObjectTransformFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeId relative_to_node_id, HAPI_RSTOrder rst_order, HAPI_Transform * transform); + typedef HAPI_Result (*GetOutputGeoCountFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, int* count); + typedef HAPI_Result (*GetOutputGeoInfosFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_GeoInfo* geo_infos_array, int count); + typedef HAPI_Result (*GetOutputNodeIdFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int output, HAPI_NodeId * output_node_id); + typedef HAPI_Result (*GetPDGEventsFuncPtr)(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id, HAPI_PDG_EventInfo * event_array, int length, int * event_count, int * remaining_events); + typedef HAPI_Result (*GetPDGGraphContextIdFuncPtr)(const HAPI_Session * session, HAPI_NodeId top_node_id, HAPI_PDG_GraphContextId * context_id); + typedef HAPI_Result (*GetPDGGraphContextsFuncPtr)(const HAPI_Session * session, HAPI_StringHandle * context_names_array, HAPI_PDG_GraphContextId * context_id_array, int start, int length); + typedef HAPI_Result (*GetPDGGraphContextsCountFuncPtr)(const HAPI_Session* session, int* num_contexts); + typedef HAPI_Result (*GetPDGStateFuncPtr)(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id, int * pdg_state); + typedef HAPI_Result (*GetParametersFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmInfo * parm_infos_array, int start, int length); + typedef HAPI_Result (*GetParmChoiceListsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmChoiceInfo * parm_choices_array, int start, int length); + typedef HAPI_Result (*GetParmExpressionFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, HAPI_StringHandle * value); + typedef HAPI_Result (*GetParmFileFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, const char * destination_directory, const char * destination_file_name); + typedef HAPI_Result (*GetParmFloatValueFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, float * value); + typedef HAPI_Result (*GetParmFloatValuesFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, float * values_array, int start, int length); + typedef HAPI_Result (*GetParmIdFromNameFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, HAPI_ParmId * parm_id); + typedef HAPI_Result (*GetParmInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, HAPI_ParmInfo * parm_info); + typedef HAPI_Result (*GetParmInfoFromNameFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, HAPI_ParmInfo * parm_info); + typedef HAPI_Result (*GetParmIntValueFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, int * value); + typedef HAPI_Result (*GetParmIntValuesFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int * values_array, int start, int length); + typedef HAPI_Result (*GetParmNodeValueFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, HAPI_NodeId * value); + typedef HAPI_Result (*GetParmStringValueFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, HAPI_Bool evaluate, HAPI_StringHandle * value); + typedef HAPI_Result (*GetParmStringValuesFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_Bool evaluate, HAPI_StringHandle * values_array, int start, int length); + typedef HAPI_Result (*GetParmTagNameFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int tag_index, HAPI_StringHandle * tag_name); + typedef HAPI_Result (*GetParmTagValueFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, const char * tag_name, HAPI_StringHandle * tag_value); + typedef HAPI_Result (*GetParmWithTagFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * tag_name, HAPI_ParmId * parm_id); + typedef HAPI_Result (*GetPartInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_PartInfo * part_info); + typedef HAPI_Result (*GetPresetFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, char * buffer, int buffer_length); + typedef HAPI_Result (*GetPresetBufLengthFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PresetType preset_type, const char * preset_name, int * buffer_length); + typedef HAPI_Result (*GetPresetCountFuncPtr)(const HAPI_Session * session, const char * buffer, int buffer_length, int * count); + typedef HAPI_Result (*GetPresetNamesFuncPtr)(const HAPI_Session * session, const char * buffer, int buffer_length, HAPI_StringHandle * preset_names_array, int preset_names_count); + typedef HAPI_Result (*GetServerEnvIntFuncPtr)(const HAPI_Session * session, const char * variable_name, int * value); + typedef HAPI_Result (*GetServerEnvStringFuncPtr)(const HAPI_Session * session, const char * variable_name, HAPI_StringHandle * value); + typedef HAPI_Result (*GetServerEnvVarCountFuncPtr)(const HAPI_Session * session, int * env_count); + typedef HAPI_Result (*GetServerEnvVarListFuncPtr)(const HAPI_Session * session, HAPI_StringHandle * values_array, int start, int length); + typedef HAPI_Result (*GetSessionEnvIntFuncPtr)(const HAPI_Session * session, HAPI_SessionEnvIntType int_type, int * value); + typedef HAPI_Result (*GetSessionSyncInfoFuncPtr)(const HAPI_Session * session, HAPI_SessionSyncInfo * session_sync_info); + typedef HAPI_Result (*GetSphereInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId geo_node_id, HAPI_PartId part_id, HAPI_SphereInfo * sphere_info); + typedef HAPI_Result (*GetStatusFuncPtr)(const HAPI_Session * session, HAPI_StatusType status_type, int * status); + typedef HAPI_Result (*GetStatusStringFuncPtr)(const HAPI_Session * session, HAPI_StatusType status_type, char * string_value, int length); + typedef HAPI_Result (*GetStatusStringBufLengthFuncPtr)(const HAPI_Session * session, HAPI_StatusType status_type, HAPI_StatusVerbosity verbosity, int * buffer_length); + typedef HAPI_Result (*GetStringFuncPtr)(const HAPI_Session * session, HAPI_StringHandle string_handle, char * string_value, int length); + typedef HAPI_Result (*GetStringBatchFuncPtr)(const HAPI_Session * session, char * char_buffer, int char_array_length); + typedef HAPI_Result (*GetStringBatchSizeFuncPtr)(const HAPI_Session * session, const int * string_handle_array, int string_handle_count, int * string_buffer_size); + typedef HAPI_Result (*GetStringBufLengthFuncPtr)(const HAPI_Session * session, HAPI_StringHandle string_handle, int * buffer_length); + typedef HAPI_Result (*GetSupportedImageFileFormatCountFuncPtr)(const HAPI_Session * session, int * file_format_count); + typedef HAPI_Result (*GetSupportedImageFileFormatsFuncPtr)(const HAPI_Session * session, HAPI_ImageFileFormat * formats_array, int file_format_count); + typedef HAPI_Result (*GetTimeFuncPtr)(const HAPI_Session * session, double * time); + typedef HAPI_Result (*GetTimelineOptionsFuncPtr)(const HAPI_Session * session, HAPI_TimelineOptions * timeline_options); + typedef HAPI_Result (*GetTotalCookCountFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeTypeBits node_type_filter, HAPI_NodeFlagsBits node_flags_filter, HAPI_Bool recursive, int * count); + typedef HAPI_Result (*GetUseHoudiniTimeFuncPtr)(const HAPI_Session * session, HAPI_Bool * enabled); + typedef HAPI_Result (*GetVertexListFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * vertex_list_array, int start, int length); + typedef HAPI_Result (*GetViewportFuncPtr)(const HAPI_Session * session, HAPI_Viewport * viewport); + typedef HAPI_Result (*GetVolumeBoundsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, float * x_min, float * y_min, float * z_min, float * x_max, float * y_max, float * z_max, float * x_center, float * y_center, float * z_center); + typedef HAPI_Result (*GetVolumeInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_VolumeInfo * volume_info); + typedef HAPI_Result (*GetVolumeTileFloatDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, float fill_value, const HAPI_VolumeTileInfo * tile, float * values_array, int length); + typedef HAPI_Result (*GetVolumeTileIntDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int fill_value, const HAPI_VolumeTileInfo * tile, int * values_array, int length); + typedef HAPI_Result (*GetVolumeVisualInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_VolumeVisualInfo * visual_info); + typedef HAPI_Result (*GetVolumeVoxelFloatDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int x_index, int y_index, int z_index, float * values_array, int value_count); + typedef HAPI_Result (*GetVolumeVoxelIntDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int x_index, int y_index, int z_index, int * values_array, int value_count); + typedef HAPI_Result (*GetWorkItemAttributeSizeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, int * length); + typedef HAPI_Result (*GetWorkItemFloatAttributeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, float * data_array, int length); + typedef HAPI_Result (*GetWorkItemInfoFuncPtr)(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id, HAPI_PDG_WorkItemId work_item_id, HAPI_PDG_WorkItemInfo * work_item_info); + typedef HAPI_Result (*GetWorkItemIntAttributeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, int * data_array, int length); + typedef HAPI_Result (*GetWorkItemOutputFilesFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, HAPI_PDG_WorkItemOutputFile * resultinfo_array, int resultinfo_count); + typedef HAPI_Result (*GetWorkItemStringAttributeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, HAPI_StringHandle * data_array, int length); + typedef HAPI_Result (*GetWorkItemsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int * work_item_ids_array, int length); + typedef HAPI_Result (*GetWorkitemDataLengthFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, int * length); + typedef HAPI_Result (*GetWorkitemFloatDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, float * data_array, int length); + typedef HAPI_Result (*GetWorkitemInfoFuncPtr)(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id, HAPI_PDG_WorkItemId workitem_id, HAPI_PDG_WorkItemInfo * workitem_info); + typedef HAPI_Result (*GetWorkitemIntDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, int * data_array, int length); + typedef HAPI_Result (*GetWorkitemResultInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, HAPI_PDG_WorkItemOutputFile * resultinfo_array, int resultinfo_count); + typedef HAPI_Result (*GetWorkitemStringDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, HAPI_StringHandle * data_array, int length); + typedef HAPI_Result (*GetWorkitemsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int * workitem_ids_array, int length); + typedef HAPI_HandleBindingInfo (*HandleBindingInfo_CreateFuncPtr)(); + typedef void (*HandleBindingInfo_InitFuncPtr)(HAPI_HandleBindingInfo * in); + typedef HAPI_HandleInfo (*HandleInfo_CreateFuncPtr)(); + typedef void (*HandleInfo_InitFuncPtr)(HAPI_HandleInfo * in); + typedef HAPI_ImageFileFormat (*ImageFileFormat_CreateFuncPtr)(); + typedef void (*ImageFileFormat_InitFuncPtr)(HAPI_ImageFileFormat *in); + typedef HAPI_ImageInfo (*ImageInfo_CreateFuncPtr)(); + typedef void (*ImageInfo_InitFuncPtr)(HAPI_ImageInfo * in); + typedef HAPI_Result (*InitializeFuncPtr)(const HAPI_Session * session, const HAPI_CookOptions * cook_options, HAPI_Bool use_cooking_thread, int cooking_thread_stack_size, const char * houdini_environment_files, const char * otl_search_path, const char * dso_search_path, const char * image_dso_search_path, const char * audio_dso_search_path); + typedef HAPI_InputCurveInfo (*InputCurveInfo_CreateFuncPtr)(); + typedef void (*InputCurveInfo_InitFuncPtr)(HAPI_InputCurveInfo * in); + typedef HAPI_Result (*InsertMultiparmInstanceFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int instance_position); + typedef HAPI_Result (*InterruptFuncPtr)(const HAPI_Session * session); + typedef HAPI_Result (*IsInitializedFuncPtr)(const HAPI_Session * session); + typedef HAPI_Result (*IsNodeValidFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int unique_node_id, HAPI_Bool * answer); + typedef HAPI_Result (*IsSessionValidFuncPtr)(const HAPI_Session * session); + typedef HAPI_Keyframe (*Keyframe_CreateFuncPtr)(); + typedef void (*Keyframe_InitFuncPtr)(HAPI_Keyframe * in); + typedef HAPI_Result (*LoadAssetLibraryFromFileFuncPtr)(const HAPI_Session * session, const char * file_path, HAPI_Bool allow_overwrite, HAPI_AssetLibraryId * library_id); + typedef HAPI_Result (*LoadAssetLibraryFromMemoryFuncPtr)(const HAPI_Session * session, const char * library_buffer, int library_buffer_length, HAPI_Bool allow_overwrite, HAPI_AssetLibraryId * library_id); + typedef HAPI_Result (*LoadGeoFromFileFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * file_name); + typedef HAPI_Result (*LoadGeoFromMemoryFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * format, const char * buffer, int length); + typedef HAPI_Result (*LoadHIPFileFuncPtr)(const HAPI_Session * session, const char * file_name, HAPI_Bool cook_on_load); + typedef HAPI_Result (*LoadNodeFromFileFuncPtr)(const HAPI_Session * session, const char * file_name, HAPI_NodeId parent_node_id, const char * node_label, HAPI_Bool cook_on_load, HAPI_NodeId * new_node_id); + typedef HAPI_MaterialInfo (*MaterialInfo_CreateFuncPtr)(); + typedef void (*MaterialInfo_InitFuncPtr)(HAPI_MaterialInfo * in); + typedef HAPI_Result (*MergeHIPFileFuncPtr)(const HAPI_Session * session, const char * file_name, HAPI_Bool cook_on_load, HAPI_HIPFileId * file_id); + typedef HAPI_NodeInfo (*NodeInfo_CreateFuncPtr)(); + typedef void (*NodeInfo_InitFuncPtr)(HAPI_NodeInfo * in); + typedef HAPI_ObjectInfo (*ObjectInfo_CreateFuncPtr)(); + typedef void (*ObjectInfo_InitFuncPtr)(HAPI_ObjectInfo * in); + typedef HAPI_ParmChoiceInfo (*ParmChoiceInfo_CreateFuncPtr)(); + typedef void (*ParmChoiceInfo_InitFuncPtr)(HAPI_ParmChoiceInfo * in); + typedef HAPI_Result (*ParmHasExpressionFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, HAPI_Bool * has_expression); + typedef HAPI_Result (*ParmHasTagFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, const char * tag_name, HAPI_Bool * has_tag); + typedef HAPI_ParmInfo (*ParmInfo_CreateFuncPtr)(); + typedef int (*ParmInfo_GetFloatValueCountFuncPtr)(const HAPI_ParmInfo * in); + typedef int (*ParmInfo_GetIntValueCountFuncPtr)(const HAPI_ParmInfo * in); + typedef int (*ParmInfo_GetStringValueCountFuncPtr)(const HAPI_ParmInfo* in); + typedef void (*ParmInfo_InitFuncPtr)(HAPI_ParmInfo * in); + typedef HAPI_Bool (*ParmInfo_IsFloatFuncPtr)(const HAPI_ParmInfo * in); + typedef HAPI_Bool (*ParmInfo_IsIntFuncPtr)(const HAPI_ParmInfo * in); + typedef HAPI_Bool (*ParmInfo_IsNodeFuncPtr)(const HAPI_ParmInfo * in); + typedef HAPI_Bool (*ParmInfo_IsNonValueFuncPtr)(const HAPI_ParmInfo * in); + typedef HAPI_Bool (*ParmInfo_IsPathFuncPtr)(const HAPI_ParmInfo * in); + typedef HAPI_Bool (*ParmInfo_IsStringFuncPtr)(const HAPI_ParmInfo * in); + typedef HAPI_PartInfo (*PartInfo_CreateFuncPtr)(); + typedef int (*PartInfo_GetAttributeCountByOwnerFuncPtr)(HAPI_PartInfo * in, HAPI_AttributeOwner owner); + typedef int (*PartInfo_GetElementCountByAttributeOwnerFuncPtr)(HAPI_PartInfo * in, HAPI_AttributeOwner owner); + typedef int (*PartInfo_GetElementCountByGroupTypeFuncPtr)(HAPI_PartInfo * in, HAPI_GroupType type); + typedef void (*PartInfo_InitFuncPtr)(HAPI_PartInfo * in); + typedef HAPI_Result (*PausePDGCookFuncPtr)(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id); + typedef HAPI_Result (*PythonThreadInterpreterLockFuncPtr)(const HAPI_Session * session, HAPI_Bool locked); + typedef HAPI_Result (*QueryNodeInputFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_to_query, int input_index, HAPI_NodeId * connected_node_id); + typedef HAPI_Result (*QueryNodeOutputConnectedCountFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int output_idx, HAPI_Bool into_subnets, HAPI_Bool through_dots, int * connected_count); + typedef HAPI_Result (*QueryNodeOutputConnectedNodesFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int output_idx, HAPI_Bool into_subnets, HAPI_Bool through_dots, HAPI_NodeId * connected_node_ids_array, int start, int length); + typedef HAPI_Result (*RemoveCustomStringFuncPtr)(const HAPI_Session * session, const HAPI_StringHandle string_handle); + typedef HAPI_Result (*RemoveMultiparmInstanceFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int instance_position); + typedef HAPI_Result (*RemoveParmExpressionFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int index); + typedef HAPI_Result (*RenameNodeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * new_name); + typedef HAPI_Result (*RenderCOPOutputToImageFuncPtr)(const HAPI_Session * session, HAPI_NodeId cop_node_id, const char * cop_output_name); + typedef HAPI_Result (*RenderCOPToImageFuncPtr)(const HAPI_Session * session, HAPI_NodeId cop_node_id); + typedef HAPI_Result (*RenderTextureToImageFuncPtr)(const HAPI_Session * session, HAPI_NodeId material_node_id, HAPI_ParmId parm_id); + typedef HAPI_Result (*ResetSimulationFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id); + typedef HAPI_Result (*RevertGeoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id); + typedef HAPI_Result (*RevertParmToDefaultFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index); + typedef HAPI_Result (*RevertParmToDefaultsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name); + typedef HAPI_Result (*SaveGeoToFileFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * file_name); + typedef HAPI_Result (*SaveGeoToMemoryFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, char * buffer, int length); + typedef HAPI_Result (*SaveHIPFileFuncPtr)(const HAPI_Session * session, const char * file_path, HAPI_Bool lock_nodes); + typedef HAPI_Result (*SaveNodeToFileFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * file_name); + typedef HAPI_SessionInfo (*SessionInfo_CreateFuncPtr)(); + typedef void (*SessionInfo_InitFuncPtr)(HAPI_SessionInfo * in); + typedef HAPI_SessionSyncInfo (*SessionSyncInfo_CreateFuncPtr)(); + typedef HAPI_Result (*SetAnimCurveFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int parm_index, const HAPI_Keyframe * curve_keyframes_array, int keyframe_count); + typedef HAPI_Result (*SetAttributeDictionaryArrayDataFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char** data_fixed_array, int data_fixed_length, const int* sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*SetAttributeDictionaryArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*SetAttributeDictionaryDataFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char** data_array, int start, int length); + typedef HAPI_Result (*SetAttributeDictionaryDataAsyncFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char** data_array, int start, int length, int * job_id); + typedef HAPI_Result (*SetAttributeFloat64ArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const double * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*SetAttributeFloat64ArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const double * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*SetAttributeFloat64DataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const double * data_array, int start, int length); + typedef HAPI_Result (*SetAttributeFloat64DataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const double * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*SetAttributeFloat64UniqueDataFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const double* data_array, int data_length, int start_index, int num_indices); + typedef HAPI_Result (*SetAttributeFloat64UniqueDataAsyncFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const double* data_array, int data_length, int start_index, int num_indices, int * job_id); + typedef HAPI_Result (*SetAttributeFloatArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const float * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*SetAttributeFloatArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const float * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*SetAttributeFloatDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const float * data_array, int start, int length); + typedef HAPI_Result (*SetAttributeFloatDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const float * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*SetAttributeFloatUniqueDataFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const float* data_array, int data_length, int start_index, int num_indices); + typedef HAPI_Result (*SetAttributeFloatUniqueDataAsyncFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const float* data_array, int data_length, int start_index, int num_indices, int * job_id); + typedef HAPI_Result (*SetAttributeIndexedStringDataFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char** string_array, int string_count, const int* indices_array, int indices_start, int indices_length); + typedef HAPI_Result (*SetAttributeIndexedStringDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** string_array, int string_count, const int * indices_array, int indices_start, int indices_length, int * job_id); + typedef HAPI_Result (*SetAttributeInt16ArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int16 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*SetAttributeInt16ArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int16 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*SetAttributeInt16DataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int16 * data_array, int start, int length); + typedef HAPI_Result (*SetAttributeInt16DataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int16 * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*SetAttributeInt16UniqueDataFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int16* data_array, int data_length, int start_index, int num_indices); + typedef HAPI_Result (*SetAttributeInt16UniqueDataAsyncFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int16* data_array, int data_length, int start_index, int num_indices, int * job_id); + typedef HAPI_Result (*SetAttributeInt64ArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int64 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*SetAttributeInt64ArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int64 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*SetAttributeInt64DataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int64 * data_array, int start, int length); + typedef HAPI_Result (*SetAttributeInt64DataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int64 * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*SetAttributeInt64UniqueDataFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int64* data_array, int data_length, int start_index, int num_indices); + typedef HAPI_Result (*SetAttributeInt64UniqueDataAsyncFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int64* data_array, int data_length, int start_index, int num_indices, int * job_id); + typedef HAPI_Result (*SetAttributeInt8ArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*SetAttributeInt8ArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*SetAttributeInt8DataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8 * data_array, int start, int length); + typedef HAPI_Result (*SetAttributeInt8DataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8 * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*SetAttributeInt8UniqueDataFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int8* data_array, int data_length, int start_index, int num_indices); + typedef HAPI_Result (*SetAttributeInt8UniqueDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8* data_array, int data_length, int start_index, int num_indices, int * job_id); + typedef HAPI_Result (*SetAttributeIntArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*SetAttributeIntArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*SetAttributeIntDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_array, int start, int length); + typedef HAPI_Result (*SetAttributeIntDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*SetAttributeIntUniqueDataFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const int* data_array, int data_length, int start_index, int num_indices); + typedef HAPI_Result (*SetAttributeIntUniqueDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_array, int data_length, int start_index, int num_indices, int * job_id); + typedef HAPI_Result (*SetAttributeStringArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*SetAttributeStringArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*SetAttributeStringDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_array, int start, int length); + typedef HAPI_Result (*SetAttributeStringDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_array, int start, int length, int * job_id); + typedef HAPI_Result (*SetAttributeStringUniqueDataFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char* data_array, int data_length, int start_index, int num_indices); + typedef HAPI_Result (*SetAttributeStringUniqueDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char * data_array, int data_length, int start_index, int num_indices, int * job_id); + typedef HAPI_Result (*SetAttributeUInt8ArrayDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + typedef HAPI_Result (*SetAttributeUInt8ArrayDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + typedef HAPI_Result (*SetAttributeUInt8DataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_array, int start, int length); + typedef HAPI_Result (*SetAttributeUInt8DataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_array, int start, int length, int * job_id); + typedef HAPI_Result (*SetAttributeUInt8UniqueDataFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_UInt8* data_array, int data_length, int start_index, int num_indices); + typedef HAPI_Result (*SetAttributeUInt8UniqueDataAsyncFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_array, int data_length, int start_index, int num_indices, int * job_id); + typedef HAPI_Result (*SetCachePropertyFuncPtr)(const HAPI_Session * session, const char * cache_name, HAPI_CacheProperty cache_property, int property_value); + typedef HAPI_Result (*SetCompositorOptionsFuncPtr)(const HAPI_Session * session, const HAPI_CompositorOptions * compositor_options); + typedef HAPI_Result (*SetCurveCountsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const int * counts_array, int start, int length); + typedef HAPI_Result (*SetCurveInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_CurveInfo * info); + typedef HAPI_Result (*SetCurveKnotsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const float * knots_array, int start, int length); + typedef HAPI_Result (*SetCurveOrdersFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const int * orders_array, int start, int length); + typedef HAPI_Result (*SetCustomStringFuncPtr)(const HAPI_Session * session, const char * string_value, HAPI_StringHandle * handle_value); + typedef HAPI_Result (*SetFaceCountsFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const int * face_counts_array, int start, int length); + typedef HAPI_Result (*SetGroupMembershipFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name, const int * membership_array, int start, int length); + typedef HAPI_Result (*SetHeightFieldDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const float * values_array, int start, int length); + typedef HAPI_Result (*SetImageInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId material_node_id, const HAPI_ImageInfo * image_info); + typedef HAPI_Result (*SetInputCurveInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_InputCurveInfo * info); + typedef HAPI_Result (*SetInputCurvePositionsFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const float* positions_array, int start, int length); + typedef HAPI_Result (*SetInputCurvePositionsRotationsScalesFuncPtr)(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const float* positions_array, int positions_start, int positions_length, const float* rotations_array, int rotations_start, int rotations_length, const float * scales_array, int scales_start, int scales_length); + typedef HAPI_Result (*SetNodeDisplayFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, int onOff); + typedef HAPI_Result (*SetObjectTransformFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const HAPI_TransformEuler * trans); + typedef HAPI_Result (*SetParmExpressionFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * value, HAPI_ParmId parm_id, int index); + typedef HAPI_Result (*SetParmFloatValueFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, float value); + typedef HAPI_Result (*SetParmFloatValuesFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const float * values_array, int start, int length); + typedef HAPI_Result (*SetParmIntValueFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, int value); + typedef HAPI_Result (*SetParmIntValuesFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const int * values_array, int start, int length); + typedef HAPI_Result (*SetParmNodeValueFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, HAPI_NodeId value); + typedef HAPI_Result (*SetParmStringValueFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, const char * value, HAPI_ParmId parm_id, int index); + typedef HAPI_Result (*SetPartInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_PartInfo * part_info); + typedef HAPI_Result (*SetPresetFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PresetType preset_type, const char * preset_name, const char * buffer, int buffer_length); + typedef HAPI_Result (*SetServerEnvIntFuncPtr)(const HAPI_Session * session, const char * variable_name, int value); + typedef HAPI_Result (*SetServerEnvStringFuncPtr)(const HAPI_Session * session, const char * variable_name, const char * value); + typedef HAPI_Result (*SetSessionSyncFuncPtr)(const HAPI_Session * session, HAPI_Bool enable); + typedef HAPI_Result (*SetSessionSyncInfoFuncPtr)(const HAPI_Session * session, const HAPI_SessionSyncInfo * session_sync_info); + typedef HAPI_Result (*SetTimeFuncPtr)(const HAPI_Session * session, double time); + typedef HAPI_Result (*SetTimelineOptionsFuncPtr)(const HAPI_Session * session, const HAPI_TimelineOptions * timeline_options); + typedef HAPI_Result (*SetTransformAnimCurveFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_TransformComponent trans_comp, const HAPI_Keyframe * curve_keyframes_array, int keyframe_count); + typedef HAPI_Result (*SetUseHoudiniTimeFuncPtr)(const HAPI_Session * session, HAPI_Bool enabled); + typedef HAPI_Result (*SetVertexListFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const int * vertex_list_array, int start, int length); + typedef HAPI_Result (*SetViewportFuncPtr)(const HAPI_Session * session, const HAPI_Viewport * viewport); + typedef HAPI_Result (*SetVolumeInfoFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_VolumeInfo * volume_info); + typedef HAPI_Result (*SetVolumeTileFloatDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_VolumeTileInfo * tile, const float * values_array, int length); + typedef HAPI_Result (*SetVolumeTileIntDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_VolumeTileInfo * tile, const int * values_array, int length); + typedef HAPI_Result (*SetVolumeVoxelFloatDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int x_index, int y_index, int z_index, const float * values_array, int value_count); + typedef HAPI_Result (*SetVolumeVoxelIntDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int x_index, int y_index, int z_index, const int * values_array, int value_count); + typedef HAPI_Result (*SetWorkItemFloatAttributeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, const float * values_array, int length); + typedef HAPI_Result (*SetWorkItemIntAttributeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, const int * values_array, int length); + typedef HAPI_Result (*SetWorkItemStringAttributeFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, int data_index, const char * value); + typedef HAPI_Result (*SetWorkitemFloatDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, const float * values_array, int length); + typedef HAPI_Result (*SetWorkitemIntDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, const int * values_array, int length); + typedef HAPI_Result (*SetWorkitemStringDataFuncPtr)(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, int data_index, const char * value); + typedef HAPI_Result (*ShutdownFuncPtr)(const HAPI_Session * session); + typedef HAPI_Result (*StartPerformanceMonitorProfileFuncPtr)(const HAPI_Session * session, const char * title, int * profile_id); + typedef HAPI_Result (*StartThriftNamedPipeServerFuncPtr)(const HAPI_ThriftServerOptions * options, const char * pipe_name, HAPI_ProcessId * process_id, const char * log_file); + typedef HAPI_Result (*StartThriftSharedMemoryServerFuncPtr)(const HAPI_ThriftServerOptions * options, const char * shared_mem_name, HAPI_ProcessId * process_id, const char * log_file); + typedef HAPI_Result (*StartThriftSocketServerFuncPtr)(const HAPI_ThriftServerOptions * options, int port, HAPI_ProcessId * process_id, const char * log_file); + typedef HAPI_Result (*StopPerformanceMonitorProfileFuncPtr)(const HAPI_Session * session, int profile_id, const char * file_path); + typedef HAPI_ThriftServerOptions (*ThriftServerOptions_CreateFuncPtr)(); + typedef void (*ThriftServerOptions_InitFuncPtr)(HAPI_ThriftServerOptions * in); + typedef HAPI_TimelineOptions (*TimelineOptions_CreateFuncPtr)(); + typedef void (*TimelineOptions_InitFuncPtr)(HAPI_TimelineOptions * in); + typedef HAPI_TransformEuler (*TransformEuler_CreateFuncPtr)(); + typedef void (*TransformEuler_InitFuncPtr)(HAPI_TransformEuler * in); + typedef HAPI_Transform (*Transform_CreateFuncPtr)(); + typedef void (*Transform_InitFuncPtr)(HAPI_Transform * in); + typedef HAPI_Viewport (*Viewport_CreateFuncPtr)(); + typedef HAPI_VolumeInfo (*VolumeInfo_CreateFuncPtr)(); + typedef void (*VolumeInfo_InitFuncPtr)(HAPI_VolumeInfo * in); + typedef HAPI_VolumeTileInfo (*VolumeTileInfo_CreateFuncPtr)(); + typedef void (*VolumeTileInfo_InitFuncPtr)(HAPI_VolumeTileInfo * in); + +public: + + static AddAttributeFuncPtr AddAttribute; + static AddGroupFuncPtr AddGroup; + static AssetInfo_CreateFuncPtr AssetInfo_Create; + static AssetInfo_InitFuncPtr AssetInfo_Init; + static AttributeInfo_CreateFuncPtr AttributeInfo_Create; + static AttributeInfo_InitFuncPtr AttributeInfo_Init; + static BindCustomImplementationFuncPtr BindCustomImplementation; + static CancelPDGCookFuncPtr CancelPDGCook; + static CheckForSpecificErrorsFuncPtr CheckForSpecificErrors; + static CleanupFuncPtr Cleanup; + static ClearConnectionErrorFuncPtr ClearConnectionError; + static CloseSessionFuncPtr CloseSession; + static CommitGeoFuncPtr CommitGeo; + static CommitWorkItemsFuncPtr CommitWorkItems; + static CommitWorkitemsFuncPtr CommitWorkitems; + static ComposeChildNodeListFuncPtr ComposeChildNodeList; + static ComposeNodeCookResultFuncPtr ComposeNodeCookResult; + static ComposeObjectListFuncPtr ComposeObjectList; + static CompositorOptions_CreateFuncPtr CompositorOptions_Create; + static CompositorOptions_InitFuncPtr CompositorOptions_Init; + static ConnectNodeInputFuncPtr ConnectNodeInput; + static ConvertMatrixToEulerFuncPtr ConvertMatrixToEuler; + static ConvertMatrixToQuatFuncPtr ConvertMatrixToQuat; + static ConvertTransformFuncPtr ConvertTransform; + static ConvertTransformEulerToMatrixFuncPtr ConvertTransformEulerToMatrix; + static ConvertTransformQuatToMatrixFuncPtr ConvertTransformQuatToMatrix; + static CookNodeFuncPtr CookNode; + static CookOptions_AreEqualFuncPtr CookOptions_AreEqual; + static CookOptions_CreateFuncPtr CookOptions_Create; + static CookOptions_InitFuncPtr CookOptions_Init; + static CookPDGFuncPtr CookPDG; + static CookPDGAllOutputsFuncPtr CookPDGAllOutputs; + static CreateCOPImageFuncPtr CreateCOPImage; + static CreateCustomSessionFuncPtr CreateCustomSession; + static CreateHeightFieldInputFuncPtr CreateHeightFieldInput; + static CreateHeightfieldInputVolumeNodeFuncPtr CreateHeightfieldInputVolumeNode; + static CreateInProcessSessionFuncPtr CreateInProcessSession; + static CreateInputCurveNodeFuncPtr CreateInputCurveNode; + static CreateInputNodeFuncPtr CreateInputNode; + static CreateNodeFuncPtr CreateNode; + static CreateThriftNamedPipeSessionFuncPtr CreateThriftNamedPipeSession; + static CreateThriftSharedMemorySessionFuncPtr CreateThriftSharedMemorySession; + static CreateThriftSocketSessionFuncPtr CreateThriftSocketSession; + static CreateWorkItemFuncPtr CreateWorkItem; + static CreateWorkitemFuncPtr CreateWorkitem; + static CurveInfo_CreateFuncPtr CurveInfo_Create; + static CurveInfo_InitFuncPtr CurveInfo_Init; + static DeleteAttributeFuncPtr DeleteAttribute; + static DeleteGroupFuncPtr DeleteGroup; + static DeleteNodeFuncPtr DeleteNode; + static DirtyPDGNodeFuncPtr DirtyPDGNode; + static DisconnectNodeInputFuncPtr DisconnectNodeInput; + static DisconnectNodeOutputsAtFuncPtr DisconnectNodeOutputsAt; + static ExtractImageToFileFuncPtr ExtractImageToFile; + static ExtractImageToMemoryFuncPtr ExtractImageToMemory; + static GeoInfo_CreateFuncPtr GeoInfo_Create; + static GeoInfo_GetGroupCountByTypeFuncPtr GeoInfo_GetGroupCountByType; + static GeoInfo_InitFuncPtr GeoInfo_Init; + static GetActiveCacheCountFuncPtr GetActiveCacheCount; + static GetActiveCacheNamesFuncPtr GetActiveCacheNames; + static GetAssetDefinitionParmCountsFuncPtr GetAssetDefinitionParmCounts; + static GetAssetDefinitionParmInfosFuncPtr GetAssetDefinitionParmInfos; + static GetAssetDefinitionParmTagNameFuncPtr GetAssetDefinitionParmTagName; + static GetAssetDefinitionParmTagValueFuncPtr GetAssetDefinitionParmTagValue; + static GetAssetDefinitionParmValuesFuncPtr GetAssetDefinitionParmValues; + static GetAssetInfoFuncPtr GetAssetInfo; + static GetAssetLibraryFilePathFuncPtr GetAssetLibraryFilePath; + static GetAssetLibraryIdsFuncPtr GetAssetLibraryIds; + static GetAttributeDictionaryArrayDataFuncPtr GetAttributeDictionaryArrayData; + static GetAttributeDictionaryArrayDataAsyncFuncPtr GetAttributeDictionaryArrayDataAsync; + static GetAttributeDictionaryDataFuncPtr GetAttributeDictionaryData; + static GetAttributeDictionaryDataAsyncFuncPtr GetAttributeDictionaryDataAsync; + static GetAttributeFloat64ArrayDataFuncPtr GetAttributeFloat64ArrayData; + static GetAttributeFloat64ArrayDataAsyncFuncPtr GetAttributeFloat64ArrayDataAsync; + static GetAttributeFloat64DataFuncPtr GetAttributeFloat64Data; + static GetAttributeFloat64DataAsyncFuncPtr GetAttributeFloat64DataAsync; + static GetAttributeFloatArrayDataFuncPtr GetAttributeFloatArrayData; + static GetAttributeFloatArrayDataAsyncFuncPtr GetAttributeFloatArrayDataAsync; + static GetAttributeFloatDataFuncPtr GetAttributeFloatData; + static GetAttributeFloatDataAsyncFuncPtr GetAttributeFloatDataAsync; + static GetAttributeInfoFuncPtr GetAttributeInfo; + static GetAttributeInt16ArrayDataFuncPtr GetAttributeInt16ArrayData; + static GetAttributeInt16ArrayDataAsyncFuncPtr GetAttributeInt16ArrayDataAsync; + static GetAttributeInt16DataFuncPtr GetAttributeInt16Data; + static GetAttributeInt16DataAsyncFuncPtr GetAttributeInt16DataAsync; + static GetAttributeInt64ArrayDataFuncPtr GetAttributeInt64ArrayData; + static GetAttributeInt64ArrayDataAsyncFuncPtr GetAttributeInt64ArrayDataAsync; + static GetAttributeInt64DataFuncPtr GetAttributeInt64Data; + static GetAttributeInt64DataAsyncFuncPtr GetAttributeInt64DataAsync; + static GetAttributeInt8ArrayDataFuncPtr GetAttributeInt8ArrayData; + static GetAttributeInt8ArrayDataAsyncFuncPtr GetAttributeInt8ArrayDataAsync; + static GetAttributeInt8DataFuncPtr GetAttributeInt8Data; + static GetAttributeInt8DataAsyncFuncPtr GetAttributeInt8DataAsync; + static GetAttributeIntArrayDataFuncPtr GetAttributeIntArrayData; + static GetAttributeIntArrayDataAsyncFuncPtr GetAttributeIntArrayDataAsync; + static GetAttributeIntDataFuncPtr GetAttributeIntData; + static GetAttributeIntDataAsyncFuncPtr GetAttributeIntDataAsync; + static GetAttributeNamesFuncPtr GetAttributeNames; + static GetAttributeStringArrayDataFuncPtr GetAttributeStringArrayData; + static GetAttributeStringArrayDataAsyncFuncPtr GetAttributeStringArrayDataAsync; + static GetAttributeStringDataFuncPtr GetAttributeStringData; + static GetAttributeStringDataAsyncFuncPtr GetAttributeStringDataAsync; + static GetAttributeUInt8ArrayDataFuncPtr GetAttributeUInt8ArrayData; + static GetAttributeUInt8ArrayDataAsyncFuncPtr GetAttributeUInt8ArrayDataAsync; + static GetAttributeUInt8DataFuncPtr GetAttributeUInt8Data; + static GetAttributeUInt8DataAsyncFuncPtr GetAttributeUInt8DataAsync; + static GetAvailableAssetCountFuncPtr GetAvailableAssetCount; + static GetAvailableAssetsFuncPtr GetAvailableAssets; + static GetBoxInfoFuncPtr GetBoxInfo; + static GetCachePropertyFuncPtr GetCacheProperty; + static GetComposedChildNodeListFuncPtr GetComposedChildNodeList; + static GetComposedNodeCookResultFuncPtr GetComposedNodeCookResult; + static GetComposedObjectListFuncPtr GetComposedObjectList; + static GetComposedObjectTransformsFuncPtr GetComposedObjectTransforms; + static GetCompositorOptionsFuncPtr GetCompositorOptions; + static GetConnectionErrorFuncPtr GetConnectionError; + static GetConnectionErrorLengthFuncPtr GetConnectionErrorLength; + static GetCookingCurrentCountFuncPtr GetCookingCurrentCount; + static GetCookingTotalCountFuncPtr GetCookingTotalCount; + static GetCurveCountsFuncPtr GetCurveCounts; + static GetCurveInfoFuncPtr GetCurveInfo; + static GetCurveKnotsFuncPtr GetCurveKnots; + static GetCurveOrdersFuncPtr GetCurveOrders; + static GetDisplayGeoInfoFuncPtr GetDisplayGeoInfo; + static GetEdgeCountOfEdgeGroupFuncPtr GetEdgeCountOfEdgeGroup; + static GetEnvIntFuncPtr GetEnvInt; + static GetFaceCountsFuncPtr GetFaceCounts; + static GetFirstVolumeTileFuncPtr GetFirstVolumeTile; + static GetGeoInfoFuncPtr GetGeoInfo; + static GetGeoSizeFuncPtr GetGeoSize; + static GetGroupCountOnPackedInstancePartFuncPtr GetGroupCountOnPackedInstancePart; + static GetGroupMembershipFuncPtr GetGroupMembership; + static GetGroupMembershipOnPackedInstancePartFuncPtr GetGroupMembershipOnPackedInstancePart; + static GetGroupNamesFuncPtr GetGroupNames; + static GetGroupNamesOnPackedInstancePartFuncPtr GetGroupNamesOnPackedInstancePart; + static GetHIPFileNodeCountFuncPtr GetHIPFileNodeCount; + static GetHIPFileNodeIdsFuncPtr GetHIPFileNodeIds; + static GetHandleBindingInfoFuncPtr GetHandleBindingInfo; + static GetHandleInfoFuncPtr GetHandleInfo; + static GetHeightFieldDataFuncPtr GetHeightFieldData; + static GetImageFilePathFuncPtr GetImageFilePath; + static GetImageInfoFuncPtr GetImageInfo; + static GetImageMemoryBufferFuncPtr GetImageMemoryBuffer; + static GetImagePlaneCountFuncPtr GetImagePlaneCount; + static GetImagePlanesFuncPtr GetImagePlanes; + static GetInputCurveInfoFuncPtr GetInputCurveInfo; + static GetInstanceTransformsOnPartFuncPtr GetInstanceTransformsOnPart; + static GetInstancedObjectIdsFuncPtr GetInstancedObjectIds; + static GetInstancedPartIdsFuncPtr GetInstancedPartIds; + static GetInstancerPartTransformsFuncPtr GetInstancerPartTransforms; + static GetJobStatusFuncPtr GetJobStatus; + static GetLoadedAssetLibraryCountFuncPtr GetLoadedAssetLibraryCount; + static GetManagerNodeIdFuncPtr GetManagerNodeId; + static GetMaterialInfoFuncPtr GetMaterialInfo; + static GetMaterialNodeIdsOnFacesFuncPtr GetMaterialNodeIdsOnFaces; + static GetMessageNodeCountFuncPtr GetMessageNodeCount; + static GetMessageNodeIdsFuncPtr GetMessageNodeIds; + static GetNextVolumeTileFuncPtr GetNextVolumeTile; + static GetNodeCookResultFuncPtr GetNodeCookResult; + static GetNodeCookResultLengthFuncPtr GetNodeCookResultLength; + static GetNodeFromPathFuncPtr GetNodeFromPath; + static GetNodeInfoFuncPtr GetNodeInfo; + static GetNodeInputNameFuncPtr GetNodeInputName; + static GetNodeOutputNameFuncPtr GetNodeOutputName; + static GetNodePathFuncPtr GetNodePath; + static GetNumWorkItemsFuncPtr GetNumWorkItems; + static GetNumWorkitemsFuncPtr GetNumWorkitems; + static GetObjectInfoFuncPtr GetObjectInfo; + static GetObjectTransformFuncPtr GetObjectTransform; + static GetOutputGeoCountFuncPtr GetOutputGeoCount; + static GetOutputGeoInfosFuncPtr GetOutputGeoInfos; + static GetOutputNodeIdFuncPtr GetOutputNodeId; + static GetPDGEventsFuncPtr GetPDGEvents; + static GetPDGGraphContextIdFuncPtr GetPDGGraphContextId; + static GetPDGGraphContextsFuncPtr GetPDGGraphContexts; + static GetPDGGraphContextsCountFuncPtr GetPDGGraphContextsCount; + static GetPDGStateFuncPtr GetPDGState; + static GetParametersFuncPtr GetParameters; + static GetParmChoiceListsFuncPtr GetParmChoiceLists; + static GetParmExpressionFuncPtr GetParmExpression; + static GetParmFileFuncPtr GetParmFile; + static GetParmFloatValueFuncPtr GetParmFloatValue; + static GetParmFloatValuesFuncPtr GetParmFloatValues; + static GetParmIdFromNameFuncPtr GetParmIdFromName; + static GetParmInfoFuncPtr GetParmInfo; + static GetParmInfoFromNameFuncPtr GetParmInfoFromName; + static GetParmIntValueFuncPtr GetParmIntValue; + static GetParmIntValuesFuncPtr GetParmIntValues; + static GetParmNodeValueFuncPtr GetParmNodeValue; + static GetParmStringValueFuncPtr GetParmStringValue; + static GetParmStringValuesFuncPtr GetParmStringValues; + static GetParmTagNameFuncPtr GetParmTagName; + static GetParmTagValueFuncPtr GetParmTagValue; + static GetParmWithTagFuncPtr GetParmWithTag; + static GetPartInfoFuncPtr GetPartInfo; + static GetPresetFuncPtr GetPreset; + static GetPresetBufLengthFuncPtr GetPresetBufLength; + static GetPresetCountFuncPtr GetPresetCount; + static GetPresetNamesFuncPtr GetPresetNames; + static GetServerEnvIntFuncPtr GetServerEnvInt; + static GetServerEnvStringFuncPtr GetServerEnvString; + static GetServerEnvVarCountFuncPtr GetServerEnvVarCount; + static GetServerEnvVarListFuncPtr GetServerEnvVarList; + static GetSessionEnvIntFuncPtr GetSessionEnvInt; + static GetSessionSyncInfoFuncPtr GetSessionSyncInfo; + static GetSphereInfoFuncPtr GetSphereInfo; + static GetStatusFuncPtr GetStatus; + static GetStatusStringFuncPtr GetStatusString; + static GetStatusStringBufLengthFuncPtr GetStatusStringBufLength; + static GetStringFuncPtr GetString; + static GetStringBatchFuncPtr GetStringBatch; + static GetStringBatchSizeFuncPtr GetStringBatchSize; + static GetStringBufLengthFuncPtr GetStringBufLength; + static GetSupportedImageFileFormatCountFuncPtr GetSupportedImageFileFormatCount; + static GetSupportedImageFileFormatsFuncPtr GetSupportedImageFileFormats; + static GetTimeFuncPtr GetTime; + static GetTimelineOptionsFuncPtr GetTimelineOptions; + static GetTotalCookCountFuncPtr GetTotalCookCount; + static GetUseHoudiniTimeFuncPtr GetUseHoudiniTime; + static GetVertexListFuncPtr GetVertexList; + static GetViewportFuncPtr GetViewport; + static GetVolumeBoundsFuncPtr GetVolumeBounds; + static GetVolumeInfoFuncPtr GetVolumeInfo; + static GetVolumeTileFloatDataFuncPtr GetVolumeTileFloatData; + static GetVolumeTileIntDataFuncPtr GetVolumeTileIntData; + static GetVolumeVisualInfoFuncPtr GetVolumeVisualInfo; + static GetVolumeVoxelFloatDataFuncPtr GetVolumeVoxelFloatData; + static GetVolumeVoxelIntDataFuncPtr GetVolumeVoxelIntData; + static GetWorkItemAttributeSizeFuncPtr GetWorkItemAttributeSize; + static GetWorkItemFloatAttributeFuncPtr GetWorkItemFloatAttribute; + static GetWorkItemInfoFuncPtr GetWorkItemInfo; + static GetWorkItemIntAttributeFuncPtr GetWorkItemIntAttribute; + static GetWorkItemOutputFilesFuncPtr GetWorkItemOutputFiles; + static GetWorkItemStringAttributeFuncPtr GetWorkItemStringAttribute; + static GetWorkItemsFuncPtr GetWorkItems; + static GetWorkitemDataLengthFuncPtr GetWorkitemDataLength; + static GetWorkitemFloatDataFuncPtr GetWorkitemFloatData; + static GetWorkitemInfoFuncPtr GetWorkitemInfo; + static GetWorkitemIntDataFuncPtr GetWorkitemIntData; + static GetWorkitemResultInfoFuncPtr GetWorkitemResultInfo; + static GetWorkitemStringDataFuncPtr GetWorkitemStringData; + static GetWorkitemsFuncPtr GetWorkitems; + static HandleBindingInfo_CreateFuncPtr HandleBindingInfo_Create; + static HandleBindingInfo_InitFuncPtr HandleBindingInfo_Init; + static HandleInfo_CreateFuncPtr HandleInfo_Create; + static HandleInfo_InitFuncPtr HandleInfo_Init; + static ImageFileFormat_CreateFuncPtr ImageFileFormat_Create; + static ImageFileFormat_InitFuncPtr ImageFileFormat_Init; + static ImageInfo_CreateFuncPtr ImageInfo_Create; + static ImageInfo_InitFuncPtr ImageInfo_Init; + static InitializeFuncPtr Initialize; + static InputCurveInfo_CreateFuncPtr InputCurveInfo_Create; + static InputCurveInfo_InitFuncPtr InputCurveInfo_Init; + static InsertMultiparmInstanceFuncPtr InsertMultiparmInstance; + static InterruptFuncPtr Interrupt; + static IsInitializedFuncPtr IsInitialized; + static IsNodeValidFuncPtr IsNodeValid; + static IsSessionValidFuncPtr IsSessionValid; + static Keyframe_CreateFuncPtr Keyframe_Create; + static Keyframe_InitFuncPtr Keyframe_Init; + static LoadAssetLibraryFromFileFuncPtr LoadAssetLibraryFromFile; + static LoadAssetLibraryFromMemoryFuncPtr LoadAssetLibraryFromMemory; + static LoadGeoFromFileFuncPtr LoadGeoFromFile; + static LoadGeoFromMemoryFuncPtr LoadGeoFromMemory; + static LoadHIPFileFuncPtr LoadHIPFile; + static LoadNodeFromFileFuncPtr LoadNodeFromFile; + static MaterialInfo_CreateFuncPtr MaterialInfo_Create; + static MaterialInfo_InitFuncPtr MaterialInfo_Init; + static MergeHIPFileFuncPtr MergeHIPFile; + static NodeInfo_CreateFuncPtr NodeInfo_Create; + static NodeInfo_InitFuncPtr NodeInfo_Init; + static ObjectInfo_CreateFuncPtr ObjectInfo_Create; + static ObjectInfo_InitFuncPtr ObjectInfo_Init; + static ParmChoiceInfo_CreateFuncPtr ParmChoiceInfo_Create; + static ParmChoiceInfo_InitFuncPtr ParmChoiceInfo_Init; + static ParmHasExpressionFuncPtr ParmHasExpression; + static ParmHasTagFuncPtr ParmHasTag; + static ParmInfo_CreateFuncPtr ParmInfo_Create; + static ParmInfo_GetFloatValueCountFuncPtr ParmInfo_GetFloatValueCount; + static ParmInfo_GetIntValueCountFuncPtr ParmInfo_GetIntValueCount; + static ParmInfo_GetStringValueCountFuncPtr ParmInfo_GetStringValueCount; + static ParmInfo_InitFuncPtr ParmInfo_Init; + static ParmInfo_IsFloatFuncPtr ParmInfo_IsFloat; + static ParmInfo_IsIntFuncPtr ParmInfo_IsInt; + static ParmInfo_IsNodeFuncPtr ParmInfo_IsNode; + static ParmInfo_IsNonValueFuncPtr ParmInfo_IsNonValue; + static ParmInfo_IsPathFuncPtr ParmInfo_IsPath; + static ParmInfo_IsStringFuncPtr ParmInfo_IsString; + static PartInfo_CreateFuncPtr PartInfo_Create; + static PartInfo_GetAttributeCountByOwnerFuncPtr PartInfo_GetAttributeCountByOwner; + static PartInfo_GetElementCountByAttributeOwnerFuncPtr PartInfo_GetElementCountByAttributeOwner; + static PartInfo_GetElementCountByGroupTypeFuncPtr PartInfo_GetElementCountByGroupType; + static PartInfo_InitFuncPtr PartInfo_Init; + static PausePDGCookFuncPtr PausePDGCook; + static PythonThreadInterpreterLockFuncPtr PythonThreadInterpreterLock; + static QueryNodeInputFuncPtr QueryNodeInput; + static QueryNodeOutputConnectedCountFuncPtr QueryNodeOutputConnectedCount; + static QueryNodeOutputConnectedNodesFuncPtr QueryNodeOutputConnectedNodes; + static RemoveCustomStringFuncPtr RemoveCustomString; + static RemoveMultiparmInstanceFuncPtr RemoveMultiparmInstance; + static RemoveParmExpressionFuncPtr RemoveParmExpression; + static RenameNodeFuncPtr RenameNode; + static RenderCOPOutputToImageFuncPtr RenderCOPOutputToImage; + static RenderCOPToImageFuncPtr RenderCOPToImage; + static RenderTextureToImageFuncPtr RenderTextureToImage; + static ResetSimulationFuncPtr ResetSimulation; + static RevertGeoFuncPtr RevertGeo; + static RevertParmToDefaultFuncPtr RevertParmToDefault; + static RevertParmToDefaultsFuncPtr RevertParmToDefaults; + static SaveGeoToFileFuncPtr SaveGeoToFile; + static SaveGeoToMemoryFuncPtr SaveGeoToMemory; + static SaveHIPFileFuncPtr SaveHIPFile; + static SaveNodeToFileFuncPtr SaveNodeToFile; + static SessionInfo_CreateFuncPtr SessionInfo_Create; + static SessionInfo_InitFuncPtr SessionInfo_Init; + static SessionSyncInfo_CreateFuncPtr SessionSyncInfo_Create; + static SetAnimCurveFuncPtr SetAnimCurve; + static SetAttributeDictionaryArrayDataFuncPtr SetAttributeDictionaryArrayData; + static SetAttributeDictionaryArrayDataAsyncFuncPtr SetAttributeDictionaryArrayDataAsync; + static SetAttributeDictionaryDataFuncPtr SetAttributeDictionaryData; + static SetAttributeDictionaryDataAsyncFuncPtr SetAttributeDictionaryDataAsync; + static SetAttributeFloat64ArrayDataFuncPtr SetAttributeFloat64ArrayData; + static SetAttributeFloat64ArrayDataAsyncFuncPtr SetAttributeFloat64ArrayDataAsync; + static SetAttributeFloat64DataFuncPtr SetAttributeFloat64Data; + static SetAttributeFloat64DataAsyncFuncPtr SetAttributeFloat64DataAsync; + static SetAttributeFloat64UniqueDataFuncPtr SetAttributeFloat64UniqueData; + static SetAttributeFloat64UniqueDataAsyncFuncPtr SetAttributeFloat64UniqueDataAsync; + static SetAttributeFloatArrayDataFuncPtr SetAttributeFloatArrayData; + static SetAttributeFloatArrayDataAsyncFuncPtr SetAttributeFloatArrayDataAsync; + static SetAttributeFloatDataFuncPtr SetAttributeFloatData; + static SetAttributeFloatDataAsyncFuncPtr SetAttributeFloatDataAsync; + static SetAttributeFloatUniqueDataFuncPtr SetAttributeFloatUniqueData; + static SetAttributeFloatUniqueDataAsyncFuncPtr SetAttributeFloatUniqueDataAsync; + static SetAttributeIndexedStringDataFuncPtr SetAttributeIndexedStringData; + static SetAttributeIndexedStringDataAsyncFuncPtr SetAttributeIndexedStringDataAsync; + static SetAttributeInt16ArrayDataFuncPtr SetAttributeInt16ArrayData; + static SetAttributeInt16ArrayDataAsyncFuncPtr SetAttributeInt16ArrayDataAsync; + static SetAttributeInt16DataFuncPtr SetAttributeInt16Data; + static SetAttributeInt16DataAsyncFuncPtr SetAttributeInt16DataAsync; + static SetAttributeInt16UniqueDataFuncPtr SetAttributeInt16UniqueData; + static SetAttributeInt16UniqueDataAsyncFuncPtr SetAttributeInt16UniqueDataAsync; + static SetAttributeInt64ArrayDataFuncPtr SetAttributeInt64ArrayData; + static SetAttributeInt64ArrayDataAsyncFuncPtr SetAttributeInt64ArrayDataAsync; + static SetAttributeInt64DataFuncPtr SetAttributeInt64Data; + static SetAttributeInt64DataAsyncFuncPtr SetAttributeInt64DataAsync; + static SetAttributeInt64UniqueDataFuncPtr SetAttributeInt64UniqueData; + static SetAttributeInt64UniqueDataAsyncFuncPtr SetAttributeInt64UniqueDataAsync; + static SetAttributeInt8ArrayDataFuncPtr SetAttributeInt8ArrayData; + static SetAttributeInt8ArrayDataAsyncFuncPtr SetAttributeInt8ArrayDataAsync; + static SetAttributeInt8DataFuncPtr SetAttributeInt8Data; + static SetAttributeInt8DataAsyncFuncPtr SetAttributeInt8DataAsync; + static SetAttributeInt8UniqueDataFuncPtr SetAttributeInt8UniqueData; + static SetAttributeInt8UniqueDataAsyncFuncPtr SetAttributeInt8UniqueDataAsync; + static SetAttributeIntArrayDataFuncPtr SetAttributeIntArrayData; + static SetAttributeIntArrayDataAsyncFuncPtr SetAttributeIntArrayDataAsync; + static SetAttributeIntDataFuncPtr SetAttributeIntData; + static SetAttributeIntDataAsyncFuncPtr SetAttributeIntDataAsync; + static SetAttributeIntUniqueDataFuncPtr SetAttributeIntUniqueData; + static SetAttributeIntUniqueDataAsyncFuncPtr SetAttributeIntUniqueDataAsync; + static SetAttributeStringArrayDataFuncPtr SetAttributeStringArrayData; + static SetAttributeStringArrayDataAsyncFuncPtr SetAttributeStringArrayDataAsync; + static SetAttributeStringDataFuncPtr SetAttributeStringData; + static SetAttributeStringDataAsyncFuncPtr SetAttributeStringDataAsync; + static SetAttributeStringUniqueDataFuncPtr SetAttributeStringUniqueData; + static SetAttributeStringUniqueDataAsyncFuncPtr SetAttributeStringUniqueDataAsync; + static SetAttributeUInt8ArrayDataFuncPtr SetAttributeUInt8ArrayData; + static SetAttributeUInt8ArrayDataAsyncFuncPtr SetAttributeUInt8ArrayDataAsync; + static SetAttributeUInt8DataFuncPtr SetAttributeUInt8Data; + static SetAttributeUInt8DataAsyncFuncPtr SetAttributeUInt8DataAsync; + static SetAttributeUInt8UniqueDataFuncPtr SetAttributeUInt8UniqueData; + static SetAttributeUInt8UniqueDataAsyncFuncPtr SetAttributeUInt8UniqueDataAsync; + static SetCachePropertyFuncPtr SetCacheProperty; + static SetCompositorOptionsFuncPtr SetCompositorOptions; + static SetCurveCountsFuncPtr SetCurveCounts; + static SetCurveInfoFuncPtr SetCurveInfo; + static SetCurveKnotsFuncPtr SetCurveKnots; + static SetCurveOrdersFuncPtr SetCurveOrders; + static SetCustomStringFuncPtr SetCustomString; + static SetFaceCountsFuncPtr SetFaceCounts; + static SetGroupMembershipFuncPtr SetGroupMembership; + static SetHeightFieldDataFuncPtr SetHeightFieldData; + static SetImageInfoFuncPtr SetImageInfo; + static SetInputCurveInfoFuncPtr SetInputCurveInfo; + static SetInputCurvePositionsFuncPtr SetInputCurvePositions; + static SetInputCurvePositionsRotationsScalesFuncPtr SetInputCurvePositionsRotationsScales; + static SetNodeDisplayFuncPtr SetNodeDisplay; + static SetObjectTransformFuncPtr SetObjectTransform; + static SetParmExpressionFuncPtr SetParmExpression; + static SetParmFloatValueFuncPtr SetParmFloatValue; + static SetParmFloatValuesFuncPtr SetParmFloatValues; + static SetParmIntValueFuncPtr SetParmIntValue; + static SetParmIntValuesFuncPtr SetParmIntValues; + static SetParmNodeValueFuncPtr SetParmNodeValue; + static SetParmStringValueFuncPtr SetParmStringValue; + static SetPartInfoFuncPtr SetPartInfo; + static SetPresetFuncPtr SetPreset; + static SetServerEnvIntFuncPtr SetServerEnvInt; + static SetServerEnvStringFuncPtr SetServerEnvString; + static SetSessionSyncFuncPtr SetSessionSync; + static SetSessionSyncInfoFuncPtr SetSessionSyncInfo; + static SetTimeFuncPtr SetTime; + static SetTimelineOptionsFuncPtr SetTimelineOptions; + static SetTransformAnimCurveFuncPtr SetTransformAnimCurve; + static SetUseHoudiniTimeFuncPtr SetUseHoudiniTime; + static SetVertexListFuncPtr SetVertexList; + static SetViewportFuncPtr SetViewport; + static SetVolumeInfoFuncPtr SetVolumeInfo; + static SetVolumeTileFloatDataFuncPtr SetVolumeTileFloatData; + static SetVolumeTileIntDataFuncPtr SetVolumeTileIntData; + static SetVolumeVoxelFloatDataFuncPtr SetVolumeVoxelFloatData; + static SetVolumeVoxelIntDataFuncPtr SetVolumeVoxelIntData; + static SetWorkItemFloatAttributeFuncPtr SetWorkItemFloatAttribute; + static SetWorkItemIntAttributeFuncPtr SetWorkItemIntAttribute; + static SetWorkItemStringAttributeFuncPtr SetWorkItemStringAttribute; + static SetWorkitemFloatDataFuncPtr SetWorkitemFloatData; + static SetWorkitemIntDataFuncPtr SetWorkitemIntData; + static SetWorkitemStringDataFuncPtr SetWorkitemStringData; + static ShutdownFuncPtr Shutdown; + static StartPerformanceMonitorProfileFuncPtr StartPerformanceMonitorProfile; + static StartThriftNamedPipeServerFuncPtr StartThriftNamedPipeServer; + static StartThriftSharedMemoryServerFuncPtr StartThriftSharedMemoryServer; + static StartThriftSocketServerFuncPtr StartThriftSocketServer; + static StopPerformanceMonitorProfileFuncPtr StopPerformanceMonitorProfile; + static ThriftServerOptions_CreateFuncPtr ThriftServerOptions_Create; + static ThriftServerOptions_InitFuncPtr ThriftServerOptions_Init; + static TimelineOptions_CreateFuncPtr TimelineOptions_Create; + static TimelineOptions_InitFuncPtr TimelineOptions_Init; + static TransformEuler_CreateFuncPtr TransformEuler_Create; + static TransformEuler_InitFuncPtr TransformEuler_Init; + static Transform_CreateFuncPtr Transform_Create; + static Transform_InitFuncPtr Transform_Init; + static Viewport_CreateFuncPtr Viewport_Create; + static VolumeInfo_CreateFuncPtr VolumeInfo_Create; + static VolumeInfo_InitFuncPtr VolumeInfo_Init; + static VolumeTileInfo_CreateFuncPtr VolumeTileInfo_Create; + static VolumeTileInfo_InitFuncPtr VolumeTileInfo_Init; + +public: + + static HAPI_Result AddAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info); + static HAPI_Result AddGroupEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name); + static HAPI_AssetInfo AssetInfo_CreateEmptyStub(); + static void AssetInfo_InitEmptyStub(HAPI_AssetInfo * in); + static HAPI_AttributeInfo AttributeInfo_CreateEmptyStub(); + static void AttributeInfo_InitEmptyStub(HAPI_AttributeInfo * in); + static HAPI_Result BindCustomImplementationEmptyStub(HAPI_SessionType session_type, const char * dll_path); + static HAPI_Result CancelPDGCookEmptyStub(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id); + static HAPI_Result CheckForSpecificErrorsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ErrorCodeBits errors_to_look_for, HAPI_ErrorCodeBits * errors_found); + static HAPI_Result CleanupEmptyStub(const HAPI_Session * session); + static HAPI_Result ClearConnectionErrorEmptyStub(); + static HAPI_Result CloseSessionEmptyStub(const HAPI_Session * session); + static HAPI_Result CommitGeoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id); + static HAPI_Result CommitWorkItemsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id); + static HAPI_Result CommitWorkitemsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id); + static HAPI_Result ComposeChildNodeListEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeTypeBits node_type_filter, HAPI_NodeFlagsBits node_flags_filter, HAPI_Bool recursive, int * count); + static HAPI_Result ComposeNodeCookResultEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_StatusVerbosity verbosity, int * buffer_length); + static HAPI_Result ComposeObjectListEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, const char * categories, int * object_count); + static HAPI_CompositorOptions CompositorOptions_CreateEmptyStub(); + static void CompositorOptions_InitEmptyStub(HAPI_CompositorOptions * in); + static HAPI_Result ConnectNodeInputEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int input_index, HAPI_NodeId node_id_to_connect, int output_index); + static HAPI_Result ConvertMatrixToEulerEmptyStub(const HAPI_Session * session, const float * matrix, HAPI_RSTOrder rst_order, HAPI_XYZOrder rot_order, HAPI_TransformEuler * transform_out); + static HAPI_Result ConvertMatrixToQuatEmptyStub(const HAPI_Session * session, const float * matrix, HAPI_RSTOrder rst_order, HAPI_Transform * transform_out); + static HAPI_Result ConvertTransformEmptyStub(const HAPI_Session * session, const HAPI_TransformEuler * transform_in, HAPI_RSTOrder rst_order, HAPI_XYZOrder rot_order, HAPI_TransformEuler * transform_out); + static HAPI_Result ConvertTransformEulerToMatrixEmptyStub(const HAPI_Session * session, const HAPI_TransformEuler * transform, float * matrix); + static HAPI_Result ConvertTransformQuatToMatrixEmptyStub(const HAPI_Session * session, const HAPI_Transform * transform, float * matrix); + static HAPI_Result CookNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const HAPI_CookOptions * cook_options); + static HAPI_Bool CookOptions_AreEqualEmptyStub(const HAPI_CookOptions * left, const HAPI_CookOptions * right); + static HAPI_CookOptions CookOptions_CreateEmptyStub(); + static void CookOptions_InitEmptyStub(HAPI_CookOptions * in); + static HAPI_Result CookPDGEmptyStub(const HAPI_Session * session, HAPI_NodeId cook_node_id, int generate_only, int blocking); + static HAPI_Result CookPDGAllOutputsEmptyStub(const HAPI_Session* session, HAPI_NodeId cook_node_id, int generate_only, int blocking); + static HAPI_Result CreateCOPImageEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, const int width, const int height, const HAPI_ImagePacking packing, HAPI_Bool flip_x, HAPI_Bool flip_y, const float * data_array, int start, int length); + static HAPI_Result CreateCustomSessionEmptyStub(HAPI_SessionType session_type, void * session_info, HAPI_Session * session); + static HAPI_Result CreateHeightFieldInputEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, const char * name, int xsize, int ysize, float voxelsize, HAPI_HeightFieldSampling sampling, HAPI_NodeId * heightfield_node_id, HAPI_NodeId * height_node_id, HAPI_NodeId * mask_node_id, HAPI_NodeId * merge_node_id); + static HAPI_Result CreateHeightfieldInputVolumeNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeId * new_node_id, const char * name, int xsize, int ysize, float voxelsize); + static HAPI_Result CreateInProcessSessionEmptyStub(HAPI_Session * session, const HAPI_SessionInfo * session_info); + static HAPI_Result CreateInputCurveNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeId * node_id, const char * name); + static HAPI_Result CreateInputNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeId * node_id, const char * name); + static HAPI_Result CreateNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, const char * operator_name, const char * node_label, HAPI_Bool cook_on_creation, HAPI_NodeId * new_node_id); + static HAPI_Result CreateThriftNamedPipeSessionEmptyStub(HAPI_Session * session, const char * pipe_name, const HAPI_SessionInfo * session_info); + static HAPI_Result CreateThriftSharedMemorySessionEmptyStub(HAPI_Session * session, const char * shared_mem_name, const HAPI_SessionInfo * session_info); + static HAPI_Result CreateThriftSocketSessionEmptyStub(HAPI_Session * session, const char * host_name, int port, const HAPI_SessionInfo * session_info); + static HAPI_Result CreateWorkItemEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId * work_item_id, const char * name, int index); + static HAPI_Result CreateWorkitemEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId * workitem_id, const char * name, int index); + static HAPI_CurveInfo CurveInfo_CreateEmptyStub(); + static void CurveInfo_InitEmptyStub(HAPI_CurveInfo * in); + static HAPI_Result DeleteAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info); + static HAPI_Result DeleteGroupEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name); + static HAPI_Result DeleteNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id); + static HAPI_Result DirtyPDGNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_Bool clean_results); + static HAPI_Result DisconnectNodeInputEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int input_index); + static HAPI_Result DisconnectNodeOutputsAtEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int output_index); + static HAPI_Result ExtractImageToFileEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, const char * image_file_format_name, const char * image_planes, const char * destination_folder_path, const char * destination_file_name, int * destination_file_path); + static HAPI_Result ExtractImageToMemoryEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, const char * image_file_format_name, const char * image_planes, int * buffer_size); + static HAPI_GeoInfo GeoInfo_CreateEmptyStub(); + static int GeoInfo_GetGroupCountByTypeEmptyStub(HAPI_GeoInfo * in, HAPI_GroupType type); + static void GeoInfo_InitEmptyStub(HAPI_GeoInfo * in); + static HAPI_Result GetActiveCacheCountEmptyStub(const HAPI_Session * session, int * active_cache_count); + static HAPI_Result GetActiveCacheNamesEmptyStub(const HAPI_Session * session, HAPI_StringHandle * cache_names_array, int active_cache_count); + static HAPI_Result GetAssetDefinitionParmCountsEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, int * parm_count, int * int_value_count, int * float_value_count, int * string_value_count, int * choice_value_count); + static HAPI_Result GetAssetDefinitionParmInfosEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, HAPI_ParmInfo * parm_infos_array, int start, int length); + static HAPI_Result GetAssetDefinitionParmTagNameEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, HAPI_ParmId parm_id, int tag_index, HAPI_StringHandle * tag_name); + static HAPI_Result GetAssetDefinitionParmTagValueEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, HAPI_ParmId parm_id, const char * tag_name, HAPI_StringHandle * tag_value); + static HAPI_Result GetAssetDefinitionParmValuesEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, const char * asset_name, int * int_values_array, int int_start, int int_length, float * float_values_array, int float_start, int float_length, HAPI_Bool string_evaluate, HAPI_StringHandle * string_values_array, int string_start, int string_length, HAPI_ParmChoiceInfo * choice_values_array, int choice_start, int choice_length); + static HAPI_Result GetAssetInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_AssetInfo * asset_info); + static HAPI_Result GetAssetLibraryFilePathEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId asset_library_id, HAPI_StringHandle * file_path_sh); + static HAPI_Result GetAssetLibraryIdsEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId * asset_library_ids_array, int start, int length); + static HAPI_Result GetAttributeDictionaryArrayDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, HAPI_AttributeInfo* attr_info, HAPI_StringHandle* data_fixed_array, int data_fixed_length, int* sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result GetAttributeDictionaryArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result GetAttributeDictionaryDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, HAPI_AttributeInfo* attr_info, HAPI_StringHandle* data_array, int start, int length); + static HAPI_Result GetAttributeDictionaryDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_array, int start, int length, int * job_id); + static HAPI_Result GetAttributeFloat64ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, double * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result GetAttributeFloat64ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, double * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result GetAttributeFloat64DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, double * data_array, int start, int length); + static HAPI_Result GetAttributeFloat64DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, double * data_array, int start, int length, int * job_id); + static HAPI_Result GetAttributeFloatArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, float * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result GetAttributeFloatArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, float * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result GetAttributeFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, float * data_array, int start, int length); + static HAPI_Result GetAttributeFloatDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, float * data_array, int start, int length, int * job_id); + static HAPI_Result GetAttributeInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeOwner owner, HAPI_AttributeInfo * attr_info); + static HAPI_Result GetAttributeInt16ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_Int16 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result GetAttributeInt16ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_Int16 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result GetAttributeInt16DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int16 * data_array, int start, int length); + static HAPI_Result GetAttributeInt16DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int16 * data_array, int start, int length, int * job_id); + static HAPI_Result GetAttributeInt64ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_Int64 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result GetAttributeInt64ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_Int64 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result GetAttributeInt64DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int64 * data_array, int start, int length); + static HAPI_Result GetAttributeInt64DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int64 * data_array, int start, int length, int * job_id); + static HAPI_Result GetAttributeInt8ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_Int8 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result GetAttributeInt8ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_Int8 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result GetAttributeInt8DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int8 * data_array, int start, int length); + static HAPI_Result GetAttributeInt8DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_Int8 * data_array, int start, int length, int * job_id); + static HAPI_Result GetAttributeIntArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result GetAttributeIntArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, int * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result GetAttributeIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, int * data_array, int start, int length); + static HAPI_Result GetAttributeIntDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, int * data_array, int start, int length, int * job_id); + static HAPI_Result GetAttributeNamesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_AttributeOwner owner, HAPI_StringHandle * attribute_names_array, int count); + static HAPI_Result GetAttributeStringArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result GetAttributeStringArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result GetAttributeStringDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_array, int start, int length); + static HAPI_Result GetAttributeStringDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_StringHandle * data_array, int start, int length, int * job_id); + static HAPI_Result GetAttributeUInt8ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, HAPI_UInt8 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result GetAttributeUInt8ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * attr_name, HAPI_AttributeInfo * attr_info, HAPI_UInt8 * data_fixed_array, int data_fixed_length, int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result GetAttributeUInt8DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_UInt8 * data_array, int start, int length); + static HAPI_Result GetAttributeUInt8DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, HAPI_AttributeInfo * attr_info, int stride, HAPI_UInt8 * data_array, int start, int length, int * job_id); + static HAPI_Result GetAvailableAssetCountEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, int * asset_count); + static HAPI_Result GetAvailableAssetsEmptyStub(const HAPI_Session * session, HAPI_AssetLibraryId library_id, HAPI_StringHandle * asset_names_array, int asset_count); + static HAPI_Result GetBoxInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId geo_node_id, HAPI_PartId part_id, HAPI_BoxInfo * box_info); + static HAPI_Result GetCachePropertyEmptyStub(const HAPI_Session * session, const char * cache_name, HAPI_CacheProperty cache_property, int * property_value); + static HAPI_Result GetComposedChildNodeListEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_NodeId * child_node_ids_array, int count); + static HAPI_Result GetComposedNodeCookResultEmptyStub(const HAPI_Session * session, char * string_value, int length); + static HAPI_Result GetComposedObjectListEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_ObjectInfo * object_infos_array, int start, int length); + static HAPI_Result GetComposedObjectTransformsEmptyStub(const HAPI_Session * session, HAPI_NodeId parent_node_id, HAPI_RSTOrder rst_order, HAPI_Transform * transform_array, int start, int length); + static HAPI_Result GetCompositorOptionsEmptyStub(const HAPI_Session * session, HAPI_CompositorOptions * compositor_options); + static HAPI_Result GetConnectionErrorEmptyStub(char * string_value, int length, HAPI_Bool clear); + static HAPI_Result GetConnectionErrorLengthEmptyStub(int * buffer_length); + static HAPI_Result GetCookingCurrentCountEmptyStub(const HAPI_Session * session, int * count); + static HAPI_Result GetCookingTotalCountEmptyStub(const HAPI_Session * session, int * count); + static HAPI_Result GetCurveCountsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * counts_array, int start, int length); + static HAPI_Result GetCurveInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_CurveInfo * info); + static HAPI_Result GetCurveKnotsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, float * knots_array, int start, int length); + static HAPI_Result GetCurveOrdersEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * orders_array, int start, int length); + static HAPI_Result GetDisplayGeoInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId object_node_id, HAPI_GeoInfo * geo_info); + static HAPI_Result GetEdgeCountOfEdgeGroupEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * group_name, int * edge_count); + static HAPI_Result GetEnvIntEmptyStub(HAPI_EnvIntType int_type, int * value); + static HAPI_Result GetFaceCountsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * face_counts_array, int start, int length); + static HAPI_Result GetFirstVolumeTileEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_VolumeTileInfo * tile); + static HAPI_Result GetGeoInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_GeoInfo * geo_info); + static HAPI_Result GetGeoSizeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * format, int * size); + static HAPI_Result GetGroupCountOnPackedInstancePartEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * pointGroupCount, int * primitiveGroupCount); + static HAPI_Result GetGroupMembershipEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name, HAPI_Bool * membership_array_all_equal, int * membership_array, int start, int length); + static HAPI_Result GetGroupMembershipOnPackedInstancePartEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name, HAPI_Bool * membership_array_all_equal, int * membership_array, int start, int length); + static HAPI_Result GetGroupNamesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_GroupType group_type, HAPI_StringHandle * group_names_array, int group_count); + static HAPI_Result GetGroupNamesOnPackedInstancePartEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, HAPI_StringHandle * group_names_array, int group_count); + static HAPI_Result GetHIPFileNodeCountEmptyStub(const HAPI_Session * session, HAPI_HIPFileId id, int * count); + static HAPI_Result GetHIPFileNodeIdsEmptyStub(const HAPI_Session * session, HAPI_HIPFileId id, HAPI_NodeId * node_ids, int length); + static HAPI_Result GetHandleBindingInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int handle_index, HAPI_HandleBindingInfo * handle_binding_infos_array, int start, int length); + static HAPI_Result GetHandleInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_HandleInfo * handle_infos_array, int start, int length); + static HAPI_Result GetHeightFieldDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, float * values_array, int start, int length); + static HAPI_Result GetImageFilePathEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, const char * image_file_format_name, const char * image_planes, const char * destination_folder_path, const char * destination_file_name, HAPI_ParmId texture_parm_id, int * destination_file_path); + static HAPI_Result GetImageInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, HAPI_ImageInfo * image_info); + static HAPI_Result GetImageMemoryBufferEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, char * buffer, int length); + static HAPI_Result GetImagePlaneCountEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, int * image_plane_count); + static HAPI_Result GetImagePlanesEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, HAPI_StringHandle * image_planes_array, int image_plane_count); + static HAPI_Result GetInputCurveInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_InputCurveInfo * info); + static HAPI_Result GetInstanceTransformsOnPartEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_RSTOrder rst_order, HAPI_Transform * transforms_array, int start, int length); + static HAPI_Result GetInstancedObjectIdsEmptyStub(const HAPI_Session * session, HAPI_NodeId object_node_id, HAPI_NodeId * instanced_node_id_array, int start, int length); + static HAPI_Result GetInstancedPartIdsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_PartId * instanced_parts_array, int start, int length); + static HAPI_Result GetInstancerPartTransformsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_RSTOrder rst_order, HAPI_Transform * transforms_array, int start, int length); + static HAPI_Result GetJobStatusEmptyStub(const HAPI_Session * session, int job_id, HAPI_JobStatus * job_status); + static HAPI_Result GetLoadedAssetLibraryCountEmptyStub(const HAPI_Session * session, int * count); + static HAPI_Result GetManagerNodeIdEmptyStub(const HAPI_Session * session, HAPI_NodeType node_type, HAPI_NodeId * node_id); + static HAPI_Result GetMaterialInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, HAPI_MaterialInfo * material_info); + static HAPI_Result GetMaterialNodeIdsOnFacesEmptyStub(const HAPI_Session * session, HAPI_NodeId geometry_node_id, HAPI_PartId part_id, HAPI_Bool * are_all_the_same, HAPI_NodeId * material_ids_array, int start, int length); + static HAPI_Result GetMessageNodeCountEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int * count); + static HAPI_Result GetMessageNodeIdsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeId * message_node_ids_array, int count); + static HAPI_Result GetNextVolumeTileEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_VolumeTileInfo * tile); + static HAPI_Result GetNodeCookResultEmptyStub(const HAPI_Session * session, char * string_value, int length); + static HAPI_Result GetNodeCookResultLengthEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_StatusVerbosity verbosity, int * buffer_length); + static HAPI_Result GetNodeFromPathEmptyStub(const HAPI_Session * session, const HAPI_NodeId parent_node_id, const char * path, HAPI_NodeId * node_id); + static HAPI_Result GetNodeInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeInfo * node_info); + static HAPI_Result GetNodeInputNameEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int input_idx, HAPI_StringHandle * name); + static HAPI_Result GetNodeOutputNameEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int output_idx, HAPI_StringHandle * name); + static HAPI_Result GetNodePathEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeId relative_to_node_id, HAPI_StringHandle * path); + static HAPI_Result GetNumWorkItemsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int * num); + static HAPI_Result GetNumWorkitemsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int * num); + static HAPI_Result GetObjectInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ObjectInfo * object_info); + static HAPI_Result GetObjectTransformEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeId relative_to_node_id, HAPI_RSTOrder rst_order, HAPI_Transform * transform); + static HAPI_Result GetOutputGeoCountEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, int* count); + static HAPI_Result GetOutputGeoInfosEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_GeoInfo* geo_infos_array, int count); + static HAPI_Result GetOutputNodeIdEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int output, HAPI_NodeId * output_node_id); + static HAPI_Result GetPDGEventsEmptyStub(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id, HAPI_PDG_EventInfo * event_array, int length, int * event_count, int * remaining_events); + static HAPI_Result GetPDGGraphContextIdEmptyStub(const HAPI_Session * session, HAPI_NodeId top_node_id, HAPI_PDG_GraphContextId * context_id); + static HAPI_Result GetPDGGraphContextsEmptyStub(const HAPI_Session * session, HAPI_StringHandle * context_names_array, HAPI_PDG_GraphContextId * context_id_array, int start, int length); + static HAPI_Result GetPDGGraphContextsCountEmptyStub(const HAPI_Session* session, int* num_contexts); + static HAPI_Result GetPDGStateEmptyStub(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id, int * pdg_state); + static HAPI_Result GetParametersEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmInfo * parm_infos_array, int start, int length); + static HAPI_Result GetParmChoiceListsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmChoiceInfo * parm_choices_array, int start, int length); + static HAPI_Result GetParmExpressionEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, HAPI_StringHandle * value); + static HAPI_Result GetParmFileEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, const char * destination_directory, const char * destination_file_name); + static HAPI_Result GetParmFloatValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, float * value); + static HAPI_Result GetParmFloatValuesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, float * values_array, int start, int length); + static HAPI_Result GetParmIdFromNameEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, HAPI_ParmId * parm_id); + static HAPI_Result GetParmInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, HAPI_ParmInfo * parm_info); + static HAPI_Result GetParmInfoFromNameEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, HAPI_ParmInfo * parm_info); + static HAPI_Result GetParmIntValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, int * value); + static HAPI_Result GetParmIntValuesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int * values_array, int start, int length); + static HAPI_Result GetParmNodeValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, HAPI_NodeId * value); + static HAPI_Result GetParmStringValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, HAPI_Bool evaluate, HAPI_StringHandle * value); + static HAPI_Result GetParmStringValuesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_Bool evaluate, HAPI_StringHandle * values_array, int start, int length); + static HAPI_Result GetParmTagNameEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int tag_index, HAPI_StringHandle * tag_name); + static HAPI_Result GetParmTagValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, const char * tag_name, HAPI_StringHandle * tag_value); + static HAPI_Result GetParmWithTagEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * tag_name, HAPI_ParmId * parm_id); + static HAPI_Result GetPartInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_PartInfo * part_info); + static HAPI_Result GetPresetEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, char * buffer, int buffer_length); + static HAPI_Result GetPresetBufLengthEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PresetType preset_type, const char * preset_name, int * buffer_length); + static HAPI_Result GetPresetCountEmptyStub(const HAPI_Session * session, const char * buffer, int buffer_length, int * count); + static HAPI_Result GetPresetNamesEmptyStub(const HAPI_Session * session, const char * buffer, int buffer_length, HAPI_StringHandle * preset_names_array, int preset_names_count); + static HAPI_Result GetServerEnvIntEmptyStub(const HAPI_Session * session, const char * variable_name, int * value); + static HAPI_Result GetServerEnvStringEmptyStub(const HAPI_Session * session, const char * variable_name, HAPI_StringHandle * value); + static HAPI_Result GetServerEnvVarCountEmptyStub(const HAPI_Session * session, int * env_count); + static HAPI_Result GetServerEnvVarListEmptyStub(const HAPI_Session * session, HAPI_StringHandle * values_array, int start, int length); + static HAPI_Result GetSessionEnvIntEmptyStub(const HAPI_Session * session, HAPI_SessionEnvIntType int_type, int * value); + static HAPI_Result GetSessionSyncInfoEmptyStub(const HAPI_Session * session, HAPI_SessionSyncInfo * session_sync_info); + static HAPI_Result GetSphereInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId geo_node_id, HAPI_PartId part_id, HAPI_SphereInfo * sphere_info); + static HAPI_Result GetStatusEmptyStub(const HAPI_Session * session, HAPI_StatusType status_type, int * status); + static HAPI_Result GetStatusStringEmptyStub(const HAPI_Session * session, HAPI_StatusType status_type, char * string_value, int length); + static HAPI_Result GetStatusStringBufLengthEmptyStub(const HAPI_Session * session, HAPI_StatusType status_type, HAPI_StatusVerbosity verbosity, int * buffer_length); + static HAPI_Result GetStringEmptyStub(const HAPI_Session * session, HAPI_StringHandle string_handle, char * string_value, int length); + static HAPI_Result GetStringBatchEmptyStub(const HAPI_Session * session, char * char_buffer, int char_array_length); + static HAPI_Result GetStringBatchSizeEmptyStub(const HAPI_Session * session, const int * string_handle_array, int string_handle_count, int * string_buffer_size); + static HAPI_Result GetStringBufLengthEmptyStub(const HAPI_Session * session, HAPI_StringHandle string_handle, int * buffer_length); + static HAPI_Result GetSupportedImageFileFormatCountEmptyStub(const HAPI_Session * session, int * file_format_count); + static HAPI_Result GetSupportedImageFileFormatsEmptyStub(const HAPI_Session * session, HAPI_ImageFileFormat * formats_array, int file_format_count); + static HAPI_Result GetTimeEmptyStub(const HAPI_Session * session, double * time); + static HAPI_Result GetTimelineOptionsEmptyStub(const HAPI_Session * session, HAPI_TimelineOptions * timeline_options); + static HAPI_Result GetTotalCookCountEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_NodeTypeBits node_type_filter, HAPI_NodeFlagsBits node_flags_filter, HAPI_Bool recursive, int * count); + static HAPI_Result GetUseHoudiniTimeEmptyStub(const HAPI_Session * session, HAPI_Bool * enabled); + static HAPI_Result GetVertexListEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int * vertex_list_array, int start, int length); + static HAPI_Result GetViewportEmptyStub(const HAPI_Session * session, HAPI_Viewport * viewport); + static HAPI_Result GetVolumeBoundsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, float * x_min, float * y_min, float * z_min, float * x_max, float * y_max, float * z_max, float * x_center, float * y_center, float * z_center); + static HAPI_Result GetVolumeInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_VolumeInfo * volume_info); + static HAPI_Result GetVolumeTileFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, float fill_value, const HAPI_VolumeTileInfo * tile, float * values_array, int length); + static HAPI_Result GetVolumeTileIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int fill_value, const HAPI_VolumeTileInfo * tile, int * values_array, int length); + static HAPI_Result GetVolumeVisualInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_VolumeVisualInfo * visual_info); + static HAPI_Result GetVolumeVoxelFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int x_index, int y_index, int z_index, float * values_array, int value_count); + static HAPI_Result GetVolumeVoxelIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int x_index, int y_index, int z_index, int * values_array, int value_count); + static HAPI_Result GetWorkItemAttributeSizeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, int * length); + static HAPI_Result GetWorkItemFloatAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, float * data_array, int length); + static HAPI_Result GetWorkItemInfoEmptyStub(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id, HAPI_PDG_WorkItemId work_item_id, HAPI_PDG_WorkItemInfo * work_item_info); + static HAPI_Result GetWorkItemIntAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, int * data_array, int length); + static HAPI_Result GetWorkItemOutputFilesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, HAPI_PDG_WorkItemOutputFile * resultinfo_array, int resultinfo_count); + static HAPI_Result GetWorkItemStringAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, HAPI_StringHandle * data_array, int length); + static HAPI_Result GetWorkItemsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int * work_item_ids_array, int length); + static HAPI_Result GetWorkitemDataLengthEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, int * length); + static HAPI_Result GetWorkitemFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, float * data_array, int length); + static HAPI_Result GetWorkitemInfoEmptyStub(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id, HAPI_PDG_WorkItemId workitem_id, HAPI_PDG_WorkItemInfo * workitem_info); + static HAPI_Result GetWorkitemIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, int * data_array, int length); + static HAPI_Result GetWorkitemResultInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, HAPI_PDG_WorkItemOutputFile * resultinfo_array, int resultinfo_count); + static HAPI_Result GetWorkitemStringDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, HAPI_StringHandle * data_array, int length); + static HAPI_Result GetWorkitemsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int * workitem_ids_array, int length); + static HAPI_HandleBindingInfo HandleBindingInfo_CreateEmptyStub(); + static void HandleBindingInfo_InitEmptyStub(HAPI_HandleBindingInfo * in); + static HAPI_HandleInfo HandleInfo_CreateEmptyStub(); + static void HandleInfo_InitEmptyStub(HAPI_HandleInfo * in); + static HAPI_ImageFileFormat ImageFileFormat_CreateEmptyStub(); + static void ImageFileFormat_InitEmptyStub(HAPI_ImageFileFormat *in); + static HAPI_ImageInfo ImageInfo_CreateEmptyStub(); + static void ImageInfo_InitEmptyStub(HAPI_ImageInfo * in); + static HAPI_Result InitializeEmptyStub(const HAPI_Session * session, const HAPI_CookOptions * cook_options, HAPI_Bool use_cooking_thread, int cooking_thread_stack_size, const char * houdini_environment_files, const char * otl_search_path, const char * dso_search_path, const char * image_dso_search_path, const char * audio_dso_search_path); + static HAPI_InputCurveInfo InputCurveInfo_CreateEmptyStub(); + static void InputCurveInfo_InitEmptyStub(HAPI_InputCurveInfo * in); + static HAPI_Result InsertMultiparmInstanceEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int instance_position); + static HAPI_Result InterruptEmptyStub(const HAPI_Session * session); + static HAPI_Result IsInitializedEmptyStub(const HAPI_Session * session); + static HAPI_Result IsNodeValidEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int unique_node_id, HAPI_Bool * answer); + static HAPI_Result IsSessionValidEmptyStub(const HAPI_Session * session); + static HAPI_Keyframe Keyframe_CreateEmptyStub(); + static void Keyframe_InitEmptyStub(HAPI_Keyframe * in); + static HAPI_Result LoadAssetLibraryFromFileEmptyStub(const HAPI_Session * session, const char * file_path, HAPI_Bool allow_overwrite, HAPI_AssetLibraryId * library_id); + static HAPI_Result LoadAssetLibraryFromMemoryEmptyStub(const HAPI_Session * session, const char * library_buffer, int library_buffer_length, HAPI_Bool allow_overwrite, HAPI_AssetLibraryId * library_id); + static HAPI_Result LoadGeoFromFileEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * file_name); + static HAPI_Result LoadGeoFromMemoryEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * format, const char * buffer, int length); + static HAPI_Result LoadHIPFileEmptyStub(const HAPI_Session * session, const char * file_name, HAPI_Bool cook_on_load); + static HAPI_Result LoadNodeFromFileEmptyStub(const HAPI_Session * session, const char * file_name, HAPI_NodeId parent_node_id, const char * node_label, HAPI_Bool cook_on_load, HAPI_NodeId * new_node_id); + static HAPI_MaterialInfo MaterialInfo_CreateEmptyStub(); + static void MaterialInfo_InitEmptyStub(HAPI_MaterialInfo * in); + static HAPI_Result MergeHIPFileEmptyStub(const HAPI_Session * session, const char * file_name, HAPI_Bool cook_on_load, HAPI_HIPFileId * file_id); + static HAPI_NodeInfo NodeInfo_CreateEmptyStub(); + static void NodeInfo_InitEmptyStub(HAPI_NodeInfo * in); + static HAPI_ObjectInfo ObjectInfo_CreateEmptyStub(); + static void ObjectInfo_InitEmptyStub(HAPI_ObjectInfo * in); + static HAPI_ParmChoiceInfo ParmChoiceInfo_CreateEmptyStub(); + static void ParmChoiceInfo_InitEmptyStub(HAPI_ParmChoiceInfo * in); + static HAPI_Result ParmHasExpressionEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, HAPI_Bool * has_expression); + static HAPI_Result ParmHasTagEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, const char * tag_name, HAPI_Bool * has_tag); + static HAPI_ParmInfo ParmInfo_CreateEmptyStub(); + static int ParmInfo_GetFloatValueCountEmptyStub(const HAPI_ParmInfo * in); + static int ParmInfo_GetIntValueCountEmptyStub(const HAPI_ParmInfo * in); + static int ParmInfo_GetStringValueCountEmptyStub(const HAPI_ParmInfo* in); + static void ParmInfo_InitEmptyStub(HAPI_ParmInfo * in); + static HAPI_Bool ParmInfo_IsFloatEmptyStub(const HAPI_ParmInfo * in); + static HAPI_Bool ParmInfo_IsIntEmptyStub(const HAPI_ParmInfo * in); + static HAPI_Bool ParmInfo_IsNodeEmptyStub(const HAPI_ParmInfo * in); + static HAPI_Bool ParmInfo_IsNonValueEmptyStub(const HAPI_ParmInfo * in); + static HAPI_Bool ParmInfo_IsPathEmptyStub(const HAPI_ParmInfo * in); + static HAPI_Bool ParmInfo_IsStringEmptyStub(const HAPI_ParmInfo * in); + static HAPI_PartInfo PartInfo_CreateEmptyStub(); + static int PartInfo_GetAttributeCountByOwnerEmptyStub(HAPI_PartInfo * in, HAPI_AttributeOwner owner); + static int PartInfo_GetElementCountByAttributeOwnerEmptyStub(HAPI_PartInfo * in, HAPI_AttributeOwner owner); + static int PartInfo_GetElementCountByGroupTypeEmptyStub(HAPI_PartInfo * in, HAPI_GroupType type); + static void PartInfo_InitEmptyStub(HAPI_PartInfo * in); + static HAPI_Result PausePDGCookEmptyStub(const HAPI_Session * session, HAPI_PDG_GraphContextId graph_context_id); + static HAPI_Result PythonThreadInterpreterLockEmptyStub(const HAPI_Session * session, HAPI_Bool locked); + static HAPI_Result QueryNodeInputEmptyStub(const HAPI_Session * session, HAPI_NodeId node_to_query, int input_index, HAPI_NodeId * connected_node_id); + static HAPI_Result QueryNodeOutputConnectedCountEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int output_idx, HAPI_Bool into_subnets, HAPI_Bool through_dots, int * connected_count); + static HAPI_Result QueryNodeOutputConnectedNodesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int output_idx, HAPI_Bool into_subnets, HAPI_Bool through_dots, HAPI_NodeId * connected_node_ids_array, int start, int length); + static HAPI_Result RemoveCustomStringEmptyStub(const HAPI_Session * session, const HAPI_StringHandle string_handle); + static HAPI_Result RemoveMultiparmInstanceEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int instance_position); + static HAPI_Result RemoveParmExpressionEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int index); + static HAPI_Result RenameNodeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * new_name); + static HAPI_Result RenderCOPOutputToImageEmptyStub(const HAPI_Session * session, HAPI_NodeId cop_node_id, const char * cop_output_name); + static HAPI_Result RenderCOPToImageEmptyStub(const HAPI_Session * session, HAPI_NodeId cop_node_id); + static HAPI_Result RenderTextureToImageEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, HAPI_ParmId parm_id); + static HAPI_Result ResetSimulationEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id); + static HAPI_Result RevertGeoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id); + static HAPI_Result RevertParmToDefaultEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index); + static HAPI_Result RevertParmToDefaultsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name); + static HAPI_Result SaveGeoToFileEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * file_name); + static HAPI_Result SaveGeoToMemoryEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, char * buffer, int length); + static HAPI_Result SaveHIPFileEmptyStub(const HAPI_Session * session, const char * file_path, HAPI_Bool lock_nodes); + static HAPI_Result SaveNodeToFileEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * file_name); + static HAPI_SessionInfo SessionInfo_CreateEmptyStub(); + static void SessionInfo_InitEmptyStub(HAPI_SessionInfo * in); + static HAPI_SessionSyncInfo SessionSyncInfo_CreateEmptyStub(); + static HAPI_Result SetAnimCurveEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_ParmId parm_id, int parm_index, const HAPI_Keyframe * curve_keyframes_array, int keyframe_count); + static HAPI_Result SetAttributeDictionaryArrayDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char** data_fixed_array, int data_fixed_length, const int* sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result SetAttributeDictionaryArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result SetAttributeDictionaryDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char** data_array, int start, int length); + static HAPI_Result SetAttributeDictionaryDataAsyncEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char** data_array, int start, int length, int * job_id); + static HAPI_Result SetAttributeFloat64ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const double * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result SetAttributeFloat64ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const double * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result SetAttributeFloat64DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const double * data_array, int start, int length); + static HAPI_Result SetAttributeFloat64DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const double * data_array, int start, int length, int * job_id); + static HAPI_Result SetAttributeFloat64UniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const double* data_array, int data_length, int start_index, int num_indices); + static HAPI_Result SetAttributeFloat64UniqueDataAsyncEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const double* data_array, int data_length, int start_index, int num_indices, int * job_id); + static HAPI_Result SetAttributeFloatArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const float * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result SetAttributeFloatArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const float * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result SetAttributeFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const float * data_array, int start, int length); + static HAPI_Result SetAttributeFloatDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const float * data_array, int start, int length, int * job_id); + static HAPI_Result SetAttributeFloatUniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const float* data_array, int data_length, int start_index, int num_indices); + static HAPI_Result SetAttributeFloatUniqueDataAsyncEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const float* data_array, int data_length, int start_index, int num_indices, int * job_id); + static HAPI_Result SetAttributeIndexedStringDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char** string_array, int string_count, const int* indices_array, int indices_start, int indices_length); + static HAPI_Result SetAttributeIndexedStringDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** string_array, int string_count, const int * indices_array, int indices_start, int indices_length, int * job_id); + static HAPI_Result SetAttributeInt16ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int16 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result SetAttributeInt16ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int16 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result SetAttributeInt16DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int16 * data_array, int start, int length); + static HAPI_Result SetAttributeInt16DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int16 * data_array, int start, int length, int * job_id); + static HAPI_Result SetAttributeInt16UniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int16* data_array, int data_length, int start_index, int num_indices); + static HAPI_Result SetAttributeInt16UniqueDataAsyncEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int16* data_array, int data_length, int start_index, int num_indices, int * job_id); + static HAPI_Result SetAttributeInt64ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int64 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result SetAttributeInt64ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int64 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result SetAttributeInt64DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int64 * data_array, int start, int length); + static HAPI_Result SetAttributeInt64DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int64 * data_array, int start, int length, int * job_id); + static HAPI_Result SetAttributeInt64UniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int64* data_array, int data_length, int start_index, int num_indices); + static HAPI_Result SetAttributeInt64UniqueDataAsyncEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int64* data_array, int data_length, int start_index, int num_indices, int * job_id); + static HAPI_Result SetAttributeInt8ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result SetAttributeInt8ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result SetAttributeInt8DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8 * data_array, int start, int length); + static HAPI_Result SetAttributeInt8DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8 * data_array, int start, int length, int * job_id); + static HAPI_Result SetAttributeInt8UniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_Int8* data_array, int data_length, int start_index, int num_indices); + static HAPI_Result SetAttributeInt8UniqueDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_Int8* data_array, int data_length, int start_index, int num_indices, int * job_id); + static HAPI_Result SetAttributeIntArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result SetAttributeIntArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result SetAttributeIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_array, int start, int length); + static HAPI_Result SetAttributeIntDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_array, int start, int length, int * job_id); + static HAPI_Result SetAttributeIntUniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const int* data_array, int data_length, int start_index, int num_indices); + static HAPI_Result SetAttributeIntUniqueDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const int * data_array, int data_length, int start_index, int num_indices, int * job_id); + static HAPI_Result SetAttributeStringArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result SetAttributeStringArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result SetAttributeStringDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_array, int start, int length); + static HAPI_Result SetAttributeStringDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char ** data_array, int start, int length, int * job_id); + static HAPI_Result SetAttributeStringUniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const char* data_array, int data_length, int start_index, int num_indices); + static HAPI_Result SetAttributeStringUniqueDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const char * data_array, int data_length, int start_index, int num_indices, int * job_id); + static HAPI_Result SetAttributeUInt8ArrayDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length); + static HAPI_Result SetAttributeUInt8ArrayDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_fixed_array, int data_fixed_length, const int * sizes_fixed_array, int start, int sizes_fixed_length, int * job_id); + static HAPI_Result SetAttributeUInt8DataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_array, int start, int length); + static HAPI_Result SetAttributeUInt8DataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_array, int start, int length, int * job_id); + static HAPI_Result SetAttributeUInt8UniqueDataEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const char* name, const HAPI_AttributeInfo* attr_info, const HAPI_UInt8* data_array, int data_length, int start_index, int num_indices); + static HAPI_Result SetAttributeUInt8UniqueDataAsyncEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const HAPI_AttributeInfo * attr_info, const HAPI_UInt8 * data_array, int data_length, int start_index, int num_indices, int * job_id); + static HAPI_Result SetCachePropertyEmptyStub(const HAPI_Session * session, const char * cache_name, HAPI_CacheProperty cache_property, int property_value); + static HAPI_Result SetCompositorOptionsEmptyStub(const HAPI_Session * session, const HAPI_CompositorOptions * compositor_options); + static HAPI_Result SetCurveCountsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const int * counts_array, int start, int length); + static HAPI_Result SetCurveInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_CurveInfo * info); + static HAPI_Result SetCurveKnotsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const float * knots_array, int start, int length); + static HAPI_Result SetCurveOrdersEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const int * orders_array, int start, int length); + static HAPI_Result SetCustomStringEmptyStub(const HAPI_Session * session, const char * string_value, HAPI_StringHandle * handle_value); + static HAPI_Result SetFaceCountsEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const int * face_counts_array, int start, int length); + static HAPI_Result SetGroupMembershipEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, HAPI_GroupType group_type, const char * group_name, const int * membership_array, int start, int length); + static HAPI_Result SetHeightFieldDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const char * name, const float * values_array, int start, int length); + static HAPI_Result SetImageInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId material_node_id, const HAPI_ImageInfo * image_info); + static HAPI_Result SetInputCurveInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_InputCurveInfo * info); + static HAPI_Result SetInputCurvePositionsEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const float* positions_array, int start, int length); + static HAPI_Result SetInputCurvePositionsRotationsScalesEmptyStub(const HAPI_Session* session, HAPI_NodeId node_id, HAPI_PartId part_id, const float* positions_array, int positions_start, int positions_length, const float* rotations_array, int rotations_start, int rotations_length, const float * scales_array, int scales_start, int scales_length); + static HAPI_Result SetNodeDisplayEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, int onOff); + static HAPI_Result SetObjectTransformEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const HAPI_TransformEuler * trans); + static HAPI_Result SetParmExpressionEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * value, HAPI_ParmId parm_id, int index); + static HAPI_Result SetParmFloatValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, float value); + static HAPI_Result SetParmFloatValuesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const float * values_array, int start, int length); + static HAPI_Result SetParmIntValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, int index, int value); + static HAPI_Result SetParmIntValuesEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const int * values_array, int start, int length); + static HAPI_Result SetParmNodeValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * parm_name, HAPI_NodeId value); + static HAPI_Result SetParmStringValueEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, const char * value, HAPI_ParmId parm_id, int index); + static HAPI_Result SetPartInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_PartInfo * part_info); + static HAPI_Result SetPresetEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PresetType preset_type, const char * preset_name, const char * buffer, int buffer_length); + static HAPI_Result SetServerEnvIntEmptyStub(const HAPI_Session * session, const char * variable_name, int value); + static HAPI_Result SetServerEnvStringEmptyStub(const HAPI_Session * session, const char * variable_name, const char * value); + static HAPI_Result SetSessionSyncEmptyStub(const HAPI_Session * session, HAPI_Bool enable); + static HAPI_Result SetSessionSyncInfoEmptyStub(const HAPI_Session * session, const HAPI_SessionSyncInfo * session_sync_info); + static HAPI_Result SetTimeEmptyStub(const HAPI_Session * session, double time); + static HAPI_Result SetTimelineOptionsEmptyStub(const HAPI_Session * session, const HAPI_TimelineOptions * timeline_options); + static HAPI_Result SetTransformAnimCurveEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_TransformComponent trans_comp, const HAPI_Keyframe * curve_keyframes_array, int keyframe_count); + static HAPI_Result SetUseHoudiniTimeEmptyStub(const HAPI_Session * session, HAPI_Bool enabled); + static HAPI_Result SetVertexListEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const int * vertex_list_array, int start, int length); + static HAPI_Result SetViewportEmptyStub(const HAPI_Session * session, const HAPI_Viewport * viewport); + static HAPI_Result SetVolumeInfoEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_VolumeInfo * volume_info); + static HAPI_Result SetVolumeTileFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_VolumeTileInfo * tile, const float * values_array, int length); + static HAPI_Result SetVolumeTileIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, const HAPI_VolumeTileInfo * tile, const int * values_array, int length); + static HAPI_Result SetVolumeVoxelFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int x_index, int y_index, int z_index, const float * values_array, int value_count); + static HAPI_Result SetVolumeVoxelIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PartId part_id, int x_index, int y_index, int z_index, const int * values_array, int value_count); + static HAPI_Result SetWorkItemFloatAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, const float * values_array, int length); + static HAPI_Result SetWorkItemIntAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, const int * values_array, int length); + static HAPI_Result SetWorkItemStringAttributeEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId work_item_id, const char * attribute_name, int data_index, const char * value); + static HAPI_Result SetWorkitemFloatDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, const float * values_array, int length); + static HAPI_Result SetWorkitemIntDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, const int * values_array, int length); + static HAPI_Result SetWorkitemStringDataEmptyStub(const HAPI_Session * session, HAPI_NodeId node_id, HAPI_PDG_WorkItemId workitem_id, const char * data_name, int data_index, const char * value); + static HAPI_Result ShutdownEmptyStub(const HAPI_Session * session); + static HAPI_Result StartPerformanceMonitorProfileEmptyStub(const HAPI_Session * session, const char * title, int * profile_id); + static HAPI_Result StartThriftNamedPipeServerEmptyStub(const HAPI_ThriftServerOptions * options, const char * pipe_name, HAPI_ProcessId * process_id, const char * log_file); + static HAPI_Result StartThriftSharedMemoryServerEmptyStub(const HAPI_ThriftServerOptions * options, const char * shared_mem_name, HAPI_ProcessId * process_id, const char * log_file); + static HAPI_Result StartThriftSocketServerEmptyStub(const HAPI_ThriftServerOptions * options, int port, HAPI_ProcessId * process_id, const char * log_file); + static HAPI_Result StopPerformanceMonitorProfileEmptyStub(const HAPI_Session * session, int profile_id, const char * file_path); + static HAPI_ThriftServerOptions ThriftServerOptions_CreateEmptyStub(); + static void ThriftServerOptions_InitEmptyStub(HAPI_ThriftServerOptions * in); + static HAPI_TimelineOptions TimelineOptions_CreateEmptyStub(); + static void TimelineOptions_InitEmptyStub(HAPI_TimelineOptions * in); + static HAPI_TransformEuler TransformEuler_CreateEmptyStub(); + static void TransformEuler_InitEmptyStub(HAPI_TransformEuler * in); + static HAPI_Transform Transform_CreateEmptyStub(); + static void Transform_InitEmptyStub(HAPI_Transform * in); + static HAPI_Viewport Viewport_CreateEmptyStub(); + static HAPI_VolumeInfo VolumeInfo_CreateEmptyStub(); + static void VolumeInfo_InitEmptyStub(HAPI_VolumeInfo * in); + static HAPI_VolumeTileInfo VolumeTileInfo_CreateEmptyStub(); + static void VolumeTileInfo_InitEmptyStub(HAPI_VolumeTileInfo * in); +}; + +``` + +`Source/HoudiniEngineEditor/HoudiniEngineEditor.Build.cs`: + +```cs +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +using UnrealBuildTool; +using System; +using System.IO; + +public class HoudiniEngineEditor : ModuleRules +{ + private int HOUDINI_USE_PCG = 1; + + public HoudiniEngineEditor( ReadOnlyTargetRules Target ) : base( Target ) + { + bPrecompile = true; + PCHUsage = PCHUsageMode.NoSharedPCHs; + PrivatePCHHeaderFile = "Private/HoudiniEngineEditorPrivatePCH.h"; + + // Check if we are compiling on unsupported platforms. + if ( Target.Platform != UnrealTargetPlatform.Win64 && + Target.Platform != UnrealTargetPlatform.Mac && + Target.Platform != UnrealTargetPlatform.Linux ) + { + string Err = string.Format( "Houdini Engine Editor: Compiling for unsupported platform." ); + System.Console.WriteLine( Err ); + throw new BuildException( Err ); + } + + PublicIncludePaths.AddRange( + new string[] { + Path.Combine(ModuleDirectory, "Public") + } + ); + + PrivateIncludePaths.AddRange( + new string[] { + "HoudiniEngine/Private", + "HoudiniEngineRuntime/Private" + } + ); + + PrivateIncludePathModuleNames.AddRange( + new string[] { + "PlacementMode" + } + ); + + // Add common dependencies. + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "HoudiniEngine", + "HoudiniEngineRuntime", + "ImageCore", + "Slate", + "SlateCore", + "Landscape", + "Foliage", + "FoliageEdit", + "Chaos", + "GeometryCollectionEngine", + "MaterialEditor", + "ToolMenus" + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "AppFramework", + "AssetTools", + "ContentBrowser", + "DesktopWidgets", + "EditorStyle", + "EditorWidgets", + "Engine", + "InputCore", + "LevelEditor", + "MainFrame", + "Projects", + "PropertyEditor", + "RHI", + "RawMesh", + "RenderCore", + "TargetPlatform", + "UnrealEd", + "ApplicationCore", + "CurveEditor", + "Json", + "SceneOutliner", + "PropertyPath", + "MaterialEditor", + "SourceControl", + "EditorSubsystem", + "GameProjectGeneration", + "ToolWidgets", + "EditorFramework", + "DataLayerEditor", + "TraceAnalysis", + "PhysicsCore", + "KismetWidgets", + "AdvancedPreviewScene" + } + ); + + if (HOUDINI_USE_PCG == 1) + { + PrivateDependencyModuleNames.AddRange( + new string[] + { + "PCG" + } + ); + PrivateDefinitions.Add("HOUDINI_USE_PCG=1"); + } + + if (Target.Version.MajorVersion == 5 && (Target.Version.MinorVersion == 0 || Target.Version.MinorVersion == 1)) + { + PrivateDependencyModuleNames.AddRange( + new string[] + { + "EditorScriptingUtilities" + } + ); + } + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + "PlacementMode", + } + ); + } +} + +``` + +`Source/HoudiniEngineEditor/Private/AssetDefinition_HoudiniToolsPackageAsset.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +// #include "CoreMinimal.h" +// #include "AssetDefinitionDefault.h" +// #include "HoudiniToolsPackageAsset.h" +// +// #include "AssetDefinition_HoudiniToolsPackageAsset.generated.h" +// +// // -------------------------------------------- +// // HoudiniToolsPackage asset definition +// // -------------------------------------------- +// +// UCLASS() +// class UAssetDefinition_HoudiniToolsPackageAsset : public UAssetDefinitionDefault +// { +// GENERATED_BODY() +// +// public: +// // UAssetDefinition Begin +// virtual FText GetAssetDisplayName() const override { return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_HoudiniToolsPackageAsset", "Houdini Tools Package Asset"); } +// virtual FLinearColor GetAssetColor() const override { return FLinearColor(FColor(175, 0, 128)); } +// virtual TSoftClassPtr GetAssetClass() const override { return UHoudiniToolsPackageAsset::StaticClass(); } +// virtual TConstArrayView GetAssetCategories() const override +// { +// return TConstArrayView(); +// } +// virtual FText GetObjectDisplayNameText(UObject* Object) const override { return FText::FromString(TEXT("UHoudiniToolsPackageAsset")); } +// // UAssetDefinition End +// }; +``` + +`Source/HoudiniEngineEditor/Private/AssetTypeActions_HoudiniAsset.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "AssetTypeActions_HoudiniAsset.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetEditor.h" +#include "HoudiniCookable.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniEngineStyle.h" +#include "HoudiniToolsEditor.h" +#include "HoudiniToolTypes.h" + +#include "EditorFramework/AssetImportData.h" +#include "EditorReimportHandler.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "HAL/FileManager.h" +#include "Internationalization/Internationalization.h" +#include "LevelEditor.h" +#include "Modules/ModuleManager.h" +#include "UObject/UObjectIterator.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +FText +FAssetTypeActions_HoudiniAsset::GetName() const +{ + return LOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset"); +} + +FColor +FAssetTypeActions_HoudiniAsset::GetTypeColor() const +{ + return FColor(255, 165, 0); +} + +UClass * +FAssetTypeActions_HoudiniAsset::GetSupportedClass() const +{ + return UHoudiniAsset::StaticClass(); +} + +uint32 +FAssetTypeActions_HoudiniAsset::GetCategories() +{ + return EAssetTypeCategories::Misc; +} + +bool +FAssetTypeActions_HoudiniAsset::HasActions(const TArray< UObject * > & InObjects) const +{ + return true; +} + +void +FAssetTypeActions_HoudiniAsset::GetActions(const TArray & InObjects, class FMenuBuilder & MenuBuilder) +{ + bool ValidObjects = false; + TArray> HoudiniAssets; + if (InObjects.Num() > 0) + { + HoudiniAssets = GetTypedWeakObjectPtrs(InObjects); + ValidObjects = true; + } + + FName StyleSetName = FHoudiniEngineStyle::GetStyleSetName(); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniPresetTypeActions", "HoudiniPreset_EditToolProperties", "Edit Tool Properties"), + NSLOCTEXT( + "HoudiniPresetTypeActions", "HoudiniPreset_EditToolProperties_Tooltip", + "Edit the HoudiniPreset asset properties."), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateStatic(&FAssetTypeActions_HoudiniAsset::ExecuteEditToolProperties, HoudiniAssets), + FCanExecuteAction::CreateLambda([=] { return ValidObjects; }) + ) + ); + + MenuBuilder.AddMenuSeparator(); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_Reimport", "Reimport"), + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_ReimportTooltip", "Reimport selected Houdini Assets."), + FSlateIcon(StyleSetName, "HoudiniEngine._Reset"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniAsset::ExecuteReimport, HoudiniAssets), + FCanExecuteAction::CreateLambda([=] { return ValidObjects; }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_RebuildAll", "Rebuild All Instances"), + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_RebuildAllTooltip", "Reimports and rebuild all instances of the selected Houdini Assets."), + FSlateIcon(StyleSetName, "HoudiniEngine._RebuildAll"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniAsset::ExecuteRebuildAllInstances, HoudiniAssets), + FCanExecuteAction::CreateLambda([=] { return ValidObjects; }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_FindInExplorer", "Find Source HDA"), + NSLOCTEXT( + "HoudiniAssetTypeActions", "HoudiniAsset_FindInExplorerTooltip", + "Opens explorer at the location of this asset's source HDA."), + FSlateIcon(_GetEditorStyle().GetStyleSetName(), "ContentBrowser.ShowInExplorer"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniAsset::ExecuteFindInExplorer, HoudiniAssets), + FCanExecuteAction::CreateLambda([=] { return ValidObjects; }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_OpenInHoudini", "Open in Houdini"), + NSLOCTEXT( + "HoudiniAssetTypeActions", "HoudiniAsset_OpenInHoudiniTooltip", + "Opens the selected asset in Houdini."), + FSlateIcon(StyleSetName, "HoudiniEngine._OpenInHoudini"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniAsset::ExecuteOpenInHoudini, HoudiniAssets), + FCanExecuteAction::CreateLambda([=] { return (HoudiniAssets.Num() == 1); }) + ) + ); + + MenuBuilder.AddMenuSeparator(); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_Instantiate", "Instantiate"), + NSLOCTEXT( + "HoudiniAssetTypeActions", "HoudiniAsset_InstantiateTooltip", + "Instantiate the selected asset in the current world."), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniAsset::ExecuteInstantiate, HoudiniAssets), + FCanExecuteAction::CreateLambda([=] { return (HoudiniAssets.Num() == 1); }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_InstantiateOrigin", "Instantiate at the origin"), + NSLOCTEXT( + "HoudiniAssetTypeActions", "HoudiniAsset_InstantiateOriginTooltip", + "Instantiate the selected asset in the current world. The Houdini Asset Actor will be created at the origin of the level (0,0,0)."), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniAsset::ExecuteInstantiateOrigin, HoudiniAssets), + FCanExecuteAction::CreateLambda([=] { return (HoudiniAssets.Num() == 1); }) + ) + ); + + MenuBuilder.AddMenuSeparator(); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_ApplyOpSingle", "Apply to the current selection (single input)"), + NSLOCTEXT( + "HoudiniAssetTypeActions", "HoudiniAsset_ApplyOpSingleTooltip", + "Applies the selected asset to the current world selection. All the selected object will be assigned to the first input."), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniAsset::ExecuteApplyOpSingle, HoudiniAssets), + FCanExecuteAction::CreateLambda([=] { return (HoudiniAssets.Num() == 1); }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_ApplyOpMulti", "Apply to the current selection (multiple inputs )"), + NSLOCTEXT( + "HoudiniAssetTypeActions", "HoudiniAsset_ApplyOpMultiTooltip", + "Applies the selected asset to the current world selection. Each selected object will be assigned to its own input (one object per input)."), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniAsset::ExecuteApplyOpMulti, HoudiniAssets), + FCanExecuteAction::CreateLambda([=] { return (HoudiniAssets.Num() == 1); }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_ApplyBatch", "Batch Apply to the current selection"), + NSLOCTEXT( + "HoudiniAssetTypeActions", "HoudiniAsset_ApplyBatchTooltip", + "Batch apply the selected asset to the current world selection. An instance of the selected Houdini asset will be created for each selected object."), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniAsset::ExecuteApplyBatch, HoudiniAssets), + FCanExecuteAction::CreateLambda([=] { return (HoudiniAssets.Num() == 1); }) + ) + ); +} + + +bool +FAssetTypeActions_HoudiniAsset::AssetsActivatedOverride(const TArray& InObjects, EAssetTypeActivationMethod::Type ActivationType) +{ + if (ActivationType == EAssetTypeActivationMethod::DoubleClicked) + { + bool ValidObjects = false; + TArray> HoudiniAssets; + if (InObjects.Num() > 0) + { + HoudiniAssets = GetTypedWeakObjectPtrs(InObjects); + ValidObjects = true; + } + + if (ValidObjects) + { + ExecuteInstantiate(HoudiniAssets); + return true; + } + } + + return false; +} + + +TSharedRef +FAssetTypeActions_HoudiniAsset::AddLevelEditorMenuExtenders(TArray> HoudiniAssets) +{ + FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked(TEXT("LevelEditor")); + TSharedRef LevelEditorCommandBindings = LevelEditor.GetGlobalLevelEditorActions(); + + FName StyleSetName = FHoudiniEngineStyle::GetStyleSetName(); + + TSharedRef Extender = MakeShareable(new FExtender); + Extender->AddMenuExtension( + "ActorAsset", + EExtensionHook::After, + LevelEditorCommandBindings, + FMenuExtensionDelegate::CreateLambda([this, HoudiniAssets, StyleSetName](FMenuBuilder& MenuBuilder) + { + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_FindInExplorer", "Find Source HDA"), + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_FindInExplorerTooltip", "Opens an explorer at the location of this actor's source HDA file."), + FSlateIcon(StyleSetName, "HoudiniEngine.DigitalAsset"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniAsset::ExecuteFindInExplorer, HoudiniAssets), + FCanExecuteAction::CreateLambda([=] { return (HoudiniAssets.Num() > 0); }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_OpenInHoudini", "Open HDA in Houdini"), + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_OpenInHoudiniTooltip", "Opens the selected asset's source HDA file in Houdini."), + FSlateIcon(StyleSetName, "HoudiniEngine._OpenInHoudini"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniAsset::ExecuteOpenInHoudini, HoudiniAssets), + FCanExecuteAction::CreateLambda([=] { return (HoudiniAssets.Num() == 1); }) + ) + ); + }) + ); + + return Extender; +} + +void +FAssetTypeActions_HoudiniAsset::ExecuteReimport(TArray> InHoudiniAssetPtrs) +{ + for (auto ObjIt = InHoudiniAssetPtrs.CreateConstIterator(); ObjIt; ++ObjIt) + { + UHoudiniAsset* HoudiniAsset = (*ObjIt).Get(); + if (HoudiniAsset) + { + FReimportManager::Instance()->Reimport(HoudiniAsset, true); + } + } +} + +void +FAssetTypeActions_HoudiniAsset::ExecuteFindInExplorer(TArray> InHoudiniAssetPtrs) +{ + for (auto ObjIt = InHoudiniAssetPtrs.CreateConstIterator(); ObjIt; ++ObjIt) + { + UHoudiniAsset * HoudiniAsset = (*ObjIt).Get(); + if (HoudiniAsset && HoudiniAsset->AssetImportData) + { + const FString SourceFilePath = HoudiniAsset->AssetImportData->GetFirstFilename(); + if (SourceFilePath.Len() && IFileManager::Get().FileSize(*SourceFilePath) != INDEX_NONE) + return FPlatformProcess::ExploreFolder(*SourceFilePath); + } + } +} + +void +FAssetTypeActions_HoudiniAsset::ExecuteOpenInHoudini(TArray> InHoudiniAssetPtrs) +{ + if (!FHoudiniEngine::IsInitialized()) + return; + + if (InHoudiniAssetPtrs.Num() != 1) + return; + + UHoudiniAsset * HoudiniAsset = InHoudiniAssetPtrs[0].Get(); + if (!HoudiniAsset || !(HoudiniAsset->AssetImportData)) + return; + + FString SourceFilePath = HoudiniAsset->AssetImportData->GetFirstFilename(); + if (!SourceFilePath.Len() || IFileManager::Get().FileSize(*SourceFilePath) == INDEX_NONE) + return; + + if (!FPaths::FileExists(SourceFilePath)) + return; + + // We'll need to modify the file name for expanded .hda + FString FileExtension = FPaths::GetExtension(SourceFilePath); + if (FileExtension.Compare(TEXT("hdalibrary"), ESearchCase::IgnoreCase) == 0) + { + // the .hda directory is what we're actually interested in loading + SourceFilePath = FPaths::GetPath(SourceFilePath); + } + + if (FPaths::IsRelative(SourceFilePath)) + FPaths::ConvertRelativePathToFull(SourceFilePath); + + // Set custom $HOME env var if it's been specified + FHoudiniEngineRuntimeUtils::SetHoudiniHomeEnvironmentVariable(); + + // Then open the HDA file in Houdini + FString LibHAPILocation = FHoudiniEngine::Get().GetLibHAPILocation(); + FString HoudiniLocation = LibHAPILocation + "/hview"; + + FPlatformProcess::CreateProc( + HoudiniLocation.GetCharArray().GetData(), + SourceFilePath.GetCharArray().GetData(), + true, false, false, + nullptr, 0, + FPlatformProcess::UserTempDir(), + nullptr, nullptr); +} + + +void +FAssetTypeActions_HoudiniAsset::ExecuteRebuildAllInstances(TArray> InHoudiniAssetPtrs) +{ + // Reimports and then rebuild all instances of the asset + for (auto ObjIt = InHoudiniAssetPtrs.CreateConstIterator(); ObjIt; ++ObjIt) + { + UHoudiniAsset * HoudiniAsset = (*ObjIt).Get(); + if (!HoudiniAsset) + continue; + + // Reimports the asset + FReimportManager::Instance()->Reimport(HoudiniAsset, true); + + // Rebuilds all instances of that asset in the scene + for (TObjectIterator Itr; Itr; ++Itr) + { + UHoudiniCookable* Cookable = *Itr; + if (IsValid(Cookable) && (Cookable->GetHoudiniAsset() == HoudiniAsset)) + { + Cookable->MarkAsNeedRebuild(); + } + } + } +} + + +void +FAssetTypeActions_HoudiniAsset::ExecuteApplyOpSingle(TArray> InHoudiniAssetPtrs) +{ + return ExecuteApplyAssetToSelection(InHoudiniAssetPtrs, EHoudiniToolType::HTOOLTYPE_OPERATOR_SINGLE); +} + +void +FAssetTypeActions_HoudiniAsset::ExecuteApplyOpMulti(TArray> InHoudiniAssetPtrs) +{ + return ExecuteApplyAssetToSelection(InHoudiniAssetPtrs, EHoudiniToolType::HTOOLTYPE_OPERATOR_MULTI); +} +void +FAssetTypeActions_HoudiniAsset::ExecuteApplyBatch(TArray> InHoudiniAssetPtrs) +{ + return ExecuteApplyAssetToSelection(InHoudiniAssetPtrs, EHoudiniToolType::HTOOLTYPE_OPERATOR_BATCH); +} + +void FAssetTypeActions_HoudiniAsset::ExecuteEditToolProperties( + TArray> InHoudiniAssetPtrs) +{ + for(TWeakObjectPtr AssetPtr : InHoudiniAssetPtrs) + { + if (!AssetPtr.IsValid()) + { + continue; + } + + const UHoudiniAsset* HoudiniAsset = AssetPtr.Get(); + const UHoudiniToolsPackageAsset* ToolsPackage = FHoudiniToolsEditor::FindOwningToolsPackage(HoudiniAsset); + + // Create, and populate, a new FHoudiniTool for this asset which is needed to launch the ToolPropertyEditor. + TSharedPtr HoudiniTool; + HoudiniTool = MakeShareable( new FHoudiniTool() ); + FHoudiniToolsEditor& Tools = FHoudiniEngineEditor::Get().GetHoudiniTools(); + Tools.PopulateHoudiniTool(HoudiniTool, HoudiniAsset, nullptr, ToolsPackage, false); + FHoudiniToolsEditor::LaunchHoudiniToolPropertyEditor(HoudiniTool); + } +} + +void +FAssetTypeActions_HoudiniAsset::ExecuteApplyAssetToSelection(TArray> InHoudiniAssetPtrs, const EHoudiniToolType& InType) +{ + if (InHoudiniAssetPtrs.Num() != 1) + return; + + UHoudiniAsset * HoudiniAsset = InHoudiniAssetPtrs[0].Get(); + if (!HoudiniAsset || !(HoudiniAsset->AssetImportData)) + return; + + FHoudiniEngineEditorUtils::InstantiateHoudiniAsset(HoudiniAsset, InType, EHoudiniToolSelectionType::HTOOL_SELECTION_WORLD_ONLY); + + /* + // Creating a temporary tool for the selected asset + TSoftObjectPtr HoudiniAssetPtr(HoudiniAsset); + FHoudiniTool HoudiniTool( + HoudiniAssetPtr, + FText::FromString(HoudiniAsset->GetName()), + Type, + EHoudiniToolSelectionType::HTOOL_SELECTION_WORLD_ONLY, + FText(), + NULL, + FString(), + false, + FFilePath(), + FHoudiniToolDirectory(), + FString()); + + SHoudiniToolPalette::InstantiateHoudiniTool(&HoudiniTool); + */ +} + +void +FAssetTypeActions_HoudiniAsset::ExecuteInstantiateOrigin(TArray> InHoudiniAssetPtrs) +{ + for (auto HoudiniAssetPtr : InHoudiniAssetPtrs) + { + UHoudiniAsset * HoudiniAsset = HoudiniAssetPtr.Get(); + if (!HoudiniAsset || !(HoudiniAsset->AssetImportData)) + continue; + + FHoudiniEngineEditorUtils::InstantiateHoudiniAssetAt(HoudiniAsset, FTransform::Identity); + } +} + +void +FAssetTypeActions_HoudiniAsset::ExecuteInstantiate(TArray> InHoudiniAssetPtrs) +{ + FTransform DefaultTransform = FHoudiniEngineEditorUtils::GetDefaulAssetSpawnTransform(); + for (auto HoudiniAssetPtr : InHoudiniAssetPtrs) + { + UHoudiniAsset * HoudiniAsset = HoudiniAssetPtr.Get(); + if (!HoudiniAsset || !(HoudiniAsset->AssetImportData)) + continue; + + FHoudiniEngineEditorUtils::InstantiateHoudiniAssetAt(HoudiniAsset, DefaultTransform); + } +} + +void +FAssetTypeActions_HoudiniAsset::OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor) +{ + const EToolkitMode::Type Mode = EditWithinLevelEditor.IsValid() ? EToolkitMode::WorldCentric : EToolkitMode::Standalone; + + for (auto ObjIt = InObjects.CreateConstIterator(); ObjIt; ++ObjIt) + { + if (UHoudiniAsset* HDA = Cast(*ObjIt)) + { + TSharedRef NewHDAEditor(new FHoudiniAssetEditor()); + NewHDAEditor->InitHoudiniAssetEditor(Mode, EditWithinLevelEditor, HDA); + } + } +} + + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngineEditor/Private/AssetTypeActions_HoudiniAsset.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "AssetTypeActions_Base.h" + +class UClass; +class UObject; +class UHoudiniAsset; + +enum class EHoudiniToolType : uint8; + +// TODO: UE5.2 uses Asset Definitions instead of Asset Actions. See AssetDefinitions.h for a conversion guide. + +class FAssetTypeActions_HoudiniAsset : public FAssetTypeActions_Base +{ + public: + + // FAssetTypeActions_Base methods. + virtual FText GetName() const override; + virtual FColor GetTypeColor() const override; + virtual UClass* GetSupportedClass() const override; + virtual uint32 GetCategories() override; + + virtual bool HasActions(const TArray< UObject * > & InObjects) const override; + virtual void GetActions(const TArray< UObject * > & InObjects, class FMenuBuilder & MenuBuilder) override; + + virtual bool AssetsActivatedOverride(const TArray& InObjects, EAssetTypeActivationMethod::Type ActivationType) override; + + virtual void OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor = TSharedPtr()) override; + + TSharedRef AddLevelEditorMenuExtenders(TArray> InHoudiniAssetPtrs); + + protected: + + // Handler for reimport option. + void ExecuteReimport(TArray> InHoudiniAssetPtrs); + + // Handler for rebuild all option + void ExecuteRebuildAllInstances(TArray> InHoudiniAssetPtrs); + + // Handler for find in explorer option + void ExecuteFindInExplorer(TArray> InHoudiniAssetPtrs); + + // Handler for the open in Houdini option + void ExecuteOpenInHoudini(TArray> InHoudiniAssetPtrs); + + // Handler to apply the current hda to the current world selection (single input) + void ExecuteApplyOpSingle(TArray> InHoudiniAssetPtrs); + + // Handler to apply the current hda to the current world selection (multi input) + void ExecuteApplyOpMulti(TArray> InHoudiniAssetPtrs); + + // Handler to batch apply the current hda to the current world selection + void ExecuteApplyBatch(TArray> InHoudiniAssetPtrs ); + + // Handler to launch edit tool properties + static void ExecuteEditToolProperties(TArray> InHoudiniAssetPtrs); + + // Handler to instantiate the HDA in the world + void ExecuteInstantiate(TArray> InHoudiniAssetPtrs); + + // Handler to instantiate the HDA in the world, actor is placed at the origin + void ExecuteInstantiateOrigin(TArray> InHoudiniAssetPtrs); + + void ExecuteApplyAssetToSelection(TArray> InHoudiniAssetPtrs, const EHoudiniToolType& InType); +}; + +``` + +`Source/HoudiniEngineEditor/Private/AssetTypeActions_HoudiniPreset.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "AssetTypeActions_HoudiniPreset.h" + +#include "HoudiniAsset.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEngineStyle.h" +#include "HoudiniPreset.h" +#include "HoudiniToolsEditor.h" +#include "EditorFramework/AssetImportData.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +FText +FAssetTypeActions_HoudiniPreset::GetName() const +{ + return LOCTEXT("HoudiniPresetTypeActions", "HoudiniPreset"); +} + +FColor +FAssetTypeActions_HoudiniPreset::GetTypeColor() const +{ + return FHoudiniEngineStyle::GetHoudiniPresetColor().ToFColor(true); +} + +UClass * +FAssetTypeActions_HoudiniPreset::GetSupportedClass() const +{ + return UHoudiniPreset::StaticClass(); +} + +uint32 +FAssetTypeActions_HoudiniPreset::GetCategories() +{ + return EAssetTypeCategories::Misc; +} + +bool +FAssetTypeActions_HoudiniPreset::HasActions(const TArray< UObject * > & InObjects) const +{ + return true; +} + +void FAssetTypeActions_HoudiniPreset::GetActions(const TArray& InObjects, FMenuBuilder& MenuBuilder) +{ + bool ValidObjects = false; + TArray> PresetAssets; + if (InObjects.Num() > 0) + { + PresetAssets = GetTypedWeakObjectPtrs(InObjects); + ValidObjects = true; + } + + FName StyleSetName = FHoudiniEngineStyle::GetStyleSetName(); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniPresetTypeActions", "HoudiniPreset_EditToolProperties", "Edit Tool Properties"), + NSLOCTEXT( + "HoudiniPresetTypeActions", "HoudiniPreset_EditToolProperties_Tooltip", + "Edit the HoudiniPreset asset properties."), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateStatic(&FAssetTypeActions_HoudiniPreset::ExecuteEditToolProperties, PresetAssets), + FCanExecuteAction::CreateLambda([=] { return ValidObjects; }) + ) + ); + + + MenuBuilder.AddMenuSeparator(); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniPresetTypeActions", "HoudiniPreset_Instantiate", "Instantiate"), + NSLOCTEXT( + "HoudiniPresetTypeActions", "HoudiniPreset_InstantiateTooltip", + "Instantiate the selected asset in the current world."), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateStatic(&FAssetTypeActions_HoudiniPreset::ExecuteInstantiate, PresetAssets), + FCanExecuteAction::CreateLambda([=] { return (PresetAssets.Num() == 1); }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniPresetTypeActions", "HoudiniPreset_InstantiateOrigin", "Instantiate at the origin"), + NSLOCTEXT( + "HoudiniPresetTypeActions", "HoudiniPreset_InstantiateOriginTooltip", + "Instantiate the selected asset in the current world. The Houdini Asset Actor will be created at the origin of the level (0,0,0)."), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateStatic(&FAssetTypeActions_HoudiniPreset::ExecuteInstantiateOrigin, PresetAssets), + FCanExecuteAction::CreateLambda([=] { return (PresetAssets.Num() == 1); }) + ) + ); + + MenuBuilder.AddMenuSeparator(); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniPresetTypeActions", "HoudiniPreset_ApplyPresetToSelection", "Apply preset to HoudiniAsset actors"), + NSLOCTEXT( + "HoudiniPresetTypeActions", "HoudiniPreset_ApplyPresetToSelectionTooltip", + "Applies the preset to the parameters of all the selected HoudiniAsset actors, if allowed by the preset."), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateStatic(&FAssetTypeActions_HoudiniPreset::ExecuteApplyPreset, PresetAssets), + FCanExecuteAction::CreateLambda([=] { return (PresetAssets.Num() == 1); }) + ) + ); + + MenuBuilder.AddMenuSeparator(); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniPresetTypeActions", "HoudiniPreset_ApplyOpSingle", "Apply to the current selection (single input)"), + NSLOCTEXT( + "HoudiniPresetTypeActions", "HoudiniPreset_ApplyOpSingleTooltip", + "Applies the selected asset to the current world selection. All the selected object will be assigned to the first input."), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateStatic(&FAssetTypeActions_HoudiniPreset::ExecuteApplyOpSingle, PresetAssets), + FCanExecuteAction::CreateLambda([=] { return (PresetAssets.Num() == 1); }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniPresetTypeActions", "HoudiniPreset_ApplyOpMulti", "Apply to the current selection (multiple inputs )"), + NSLOCTEXT( + "HoudiniPresetTypeActions", "HoudiniPreset_ApplyOpMultiTooltip", + "Applies the selected asset to the current world selection. Each selected object will be assigned to its own input (one object per input)."), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateStatic(&FAssetTypeActions_HoudiniPreset::ExecuteApplyOpMulti, PresetAssets), + FCanExecuteAction::CreateLambda([=] { return (PresetAssets.Num() == 1); }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniPresetTypeActions", "HoudiniPreset_ApplyBatch", "Batch Apply to the current selection"), + NSLOCTEXT( + "HoudiniPresetTypeActions", "HoudiniPreset_ApplyBatchTooltip", + "Batch apply the selected asset to the current world selection. An instance of the selected Houdini asset will be created for each selected object."), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateStatic(&FAssetTypeActions_HoudiniPreset::ExecuteApplyBatch, PresetAssets), + FCanExecuteAction::CreateLambda([=] { return (PresetAssets.Num() == 1); }) + ) + ); +} + +void FAssetTypeActions_HoudiniPreset::ExecuteApplyPreset(TArray> InHoudiniAssetPtrs) +{ + for(TWeakObjectPtr PresetPtr : InHoudiniAssetPtrs) + { + if (!PresetPtr.IsValid()) + { + continue; + } + + const UHoudiniPreset* HoudiniPreset = PresetPtr.Get(); + FHoudiniToolsEditor::ApplyPresetToSelectedHoudiniAssetActors(HoudiniPreset); + } +} + +void +FAssetTypeActions_HoudiniPreset::ExecuteApplyOpSingle(TArray> InHoudiniAssetPtrs) +{ + return ExecuteApplyAssetToSelection(InHoudiniAssetPtrs, EHoudiniToolType::HTOOLTYPE_OPERATOR_SINGLE); +} + +void +FAssetTypeActions_HoudiniPreset::ExecuteApplyOpMulti(TArray> InHoudiniAssetPtrs) +{ + return ExecuteApplyAssetToSelection(InHoudiniAssetPtrs, EHoudiniToolType::HTOOLTYPE_OPERATOR_MULTI); +} +void +FAssetTypeActions_HoudiniPreset::ExecuteApplyBatch(TArray> InHoudiniAssetPtrs) +{ + return ExecuteApplyAssetToSelection(InHoudiniAssetPtrs, EHoudiniToolType::HTOOLTYPE_OPERATOR_BATCH); +} + +void FAssetTypeActions_HoudiniPreset::ExecuteEditToolProperties( + TArray> InHoudiniAssetPtrs) +{ + for(TWeakObjectPtr PresetPtr : InHoudiniAssetPtrs) + { + if (!PresetPtr.IsValid()) + { + continue; + } + + const UHoudiniPreset* HoudiniPreset = PresetPtr.Get(); + const UHoudiniAsset* HoudiniAsset = PresetPtr->SourceHoudiniAsset; + const UHoudiniToolsPackageAsset* ToolsPackage = FHoudiniToolsEditor::FindOwningToolsPackage(HoudiniPreset); + + // Create, and populate, a new FHoudiniTool for this asset which is needed to launch the ToolPropertyEditor. + TSharedPtr HoudiniTool; + HoudiniTool = MakeShareable( new FHoudiniTool() ); + FHoudiniToolsEditor& Tools = FHoudiniEngineEditor::Get().GetHoudiniTools(); + Tools.PopulateHoudiniTool(HoudiniTool, HoudiniAsset, HoudiniPreset, ToolsPackage, false); + FHoudiniToolsEditor::LaunchHoudiniToolPropertyEditor(HoudiniTool); + } +} + +void +FAssetTypeActions_HoudiniPreset::ExecuteApplyAssetToSelection(TArray> InHoudiniAssetPtrs, const EHoudiniToolType& InType) +{ + if (InHoudiniAssetPtrs.Num() != 1) + return; + + UHoudiniPreset* HoudiniPreset = InHoudiniAssetPtrs[0].Get(); + if (!IsValid(HoudiniPreset)) + { + return; + } + + UHoudiniAsset* HoudiniAsset = HoudiniPreset->SourceHoudiniAsset; + if (!IsValid(HoudiniAsset) || !(IsValid(HoudiniAsset->AssetImportData))) + return; + + FHoudiniEngineEditorUtils::InstantiateHoudiniAsset(HoudiniAsset, InType, EHoudiniToolSelectionType::HTOOL_SELECTION_WORLD_ONLY, HoudiniPreset); +} + +void +FAssetTypeActions_HoudiniPreset::ExecuteInstantiateOrigin(TArray> InHoudiniAssetPtrs) +{ + for (auto HoudiniAssetPtr : InHoudiniAssetPtrs) + { + UHoudiniPreset* HoudiniPreset = HoudiniAssetPtr.Get(); + if (!IsValid(HoudiniPreset)) + { + continue; + } + UHoudiniAsset* HoudiniAsset = HoudiniPreset->SourceHoudiniAsset; + if (!IsValid(HoudiniAsset) || !IsValid(HoudiniAsset->AssetImportData)) + continue; + + FHoudiniEngineEditorUtils::InstantiateHoudiniAssetAt(HoudiniAsset, FTransform::Identity, nullptr, nullptr, HoudiniPreset); + } +} + +void +FAssetTypeActions_HoudiniPreset::ExecuteInstantiate(TArray> InHoudiniAssetPtrs) +{ + FTransform DefaultTransform = FHoudiniEngineEditorUtils::GetDefaulAssetSpawnTransform(); + for (auto HoudiniAssetPtr : InHoudiniAssetPtrs) + { + UHoudiniPreset* HoudiniPreset = HoudiniAssetPtr.Get(); + if (!IsValid(HoudiniPreset)) + { + continue; + } + + UHoudiniAsset* HoudiniAsset = HoudiniPreset->SourceHoudiniAsset; + if (!IsValid(HoudiniAsset) || !IsValid(HoudiniAsset->AssetImportData)) + { + continue; + } + + FHoudiniEngineEditorUtils::InstantiateHoudiniAssetAt(HoudiniAsset, DefaultTransform, nullptr, nullptr, HoudiniPreset); + } +} + + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/AssetTypeActions_HoudiniPreset.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "AssetTypeActions_Base.h" +#include "HoudiniToolsPackageAsset.h" + +class UHoudiniPreset; +class UClass; +class UObject; +class UHoudiniAsset; + +enum class EHoudiniToolType : uint8; + +// TODO: UE5.2 uses Asset Definitions instead of Asset Actions. See AssetDefinitions.h for a conversion guide. + +class FAssetTypeActions_HoudiniPreset : public FAssetTypeActions_Base +{ + public: + + // FAssetTypeActions_Base methods. + virtual FText GetName() const override; + virtual FColor GetTypeColor() const override; + virtual UClass* GetSupportedClass() const override; + virtual uint32 GetCategories() override; + + virtual bool HasActions(const TArray< UObject * > & InObjects) const override; + virtual void GetActions(const TArray< UObject * > & InObjects, class FMenuBuilder & MenuBuilder) override; + + protected: + + // Handler to apply the preset to the selected HoudiniAsset actors + static void ExecuteApplyPreset(TArray> InHoudiniAssetPtrs); + + // Handler to apply the current hda to the current world selection (single input) + static void ExecuteApplyOpSingle(TArray> InHoudiniAssetPtrs); + + // Handler to apply the current hda to the current world selection (multi input) + static void ExecuteApplyOpMulti(TArray> InHoudiniAssetPtrs); + + // Handler to batch apply the current hda to the current world selection + static void ExecuteApplyBatch(TArray> InHoudiniAssetPtrs ); + + // Handler to launch edit tool properties + static void ExecuteEditToolProperties(TArray> InHoudiniAssetPtrs); + + // Handler to instantiate the HDA in the world + static void ExecuteInstantiate(TArray> InHoudiniAssetPtrs); + + // Handler to instantiate the HDA in the world, actor is placed at the origin + static void ExecuteInstantiateOrigin(TArray> InHoudiniAssetPtrs); + + static void ExecuteApplyAssetToSelection(TArray> InHoudiniAssetPtrs, const EHoudiniToolType& InType); +}; + +``` + +`Source/HoudiniEngineEditor/Private/AssetTypeActions_HoudiniToolsPackageAsset.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "AssetTypeActions_HoudiniToolsPackageAsset.h" +#include "HoudiniToolsPackageAsset.h" + +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniEngineStyle.h" +#include "HoudiniEngine.h" +#include "HoudiniAsset.h" + +#include "HoudiniEngineRuntimeUtils.h" + +#include "HoudiniToolsEditor.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "HAL/FileManager.h" +#include "LevelEditor.h" +#include "Modules/ModuleManager.h" + +#include "Internationalization/Internationalization.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +FText +FAssetTypeActions_HoudiniToolsPackageAsset::GetName() const +{ + return LOCTEXT("HoudiniAssetTypeActions", "HoudiniLibrary"); +} + +FColor +FAssetTypeActions_HoudiniToolsPackageAsset::GetTypeColor() const +{ + return FColor(255, 165, 0); +} + +UClass * +FAssetTypeActions_HoudiniToolsPackageAsset::GetSupportedClass() const +{ + return UHoudiniToolsPackageAsset::StaticClass(); +} + +uint32 +FAssetTypeActions_HoudiniToolsPackageAsset::GetCategories() +{ + return EAssetTypeCategories::Misc; +} + +bool +FAssetTypeActions_HoudiniToolsPackageAsset::HasActions(const TArray< UObject * > & InObjects) const +{ + return true; +} + +void +FAssetTypeActions_HoudiniToolsPackageAsset::GetActions(const TArray & InObjects, class FMenuBuilder & MenuBuilder) +{ + bool ValidObjects = false; + TArray> HoudiniPackages; + if (InObjects.Num() > 0) + { + HoudiniPackages = GetTypedWeakObjectPtrs(InObjects); + ValidObjects = true; + } + + FName StyleSetName = FHoudiniEngineStyle::GetStyleSetName(); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_Reimport", "Reimport"), + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_ReimportTooltip", "Reimport package description and HDAs, if permitted by package settings."), + FSlateIcon(StyleSetName, "HoudiniEngine._Reset"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniToolsPackageAsset::ExecuteReimport, HoudiniPackages), + FCanExecuteAction::CreateLambda([=] { return ValidObjects; }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_Reimport", "Reimport package description"), + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_ReimportTooltip", "Reimport package description only, if permitted by package settings."), + FSlateIcon(StyleSetName, "HoudiniEngine._Reset"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniToolsPackageAsset::ExecuteReimportDescriptionOnly, HoudiniPackages), + FCanExecuteAction::CreateLambda([=] { return ValidObjects; }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_Reimport", "Find new HDAs"), + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_ReimportTooltip", "Find and import new HDAs into this package."), + FSlateIcon(StyleSetName, "HoudiniEngine._Reset"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniToolsPackageAsset::ExecuteImportNewHDAs, HoudiniPackages), + FCanExecuteAction::CreateLambda([=] { return ValidObjects; }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetTypeActions", "HoudiniAsset_FindInExplorer", "Find Source Package"), + NSLOCTEXT( + "HoudiniAssetTypeActions", "HoudiniAsset_FindInExplorerTooltip", + "Opens explorer at the location of this asset."), + FSlateIcon(StyleSetName, "HoudiniEngine.DigitalAsset"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniToolsPackageAsset::ExecuteFindInExplorer, HoudiniPackages), + FCanExecuteAction::CreateLambda([=] { return ValidObjects; }) + ) + ); + + +} + + +// bool +// FAssetTypeActions_HoudiniToolsPackageAsset::AssetsActivatedOverride(const TArray& InObjects, EAssetTypeActivationMethod::Type ActivationType) +// { +// if (ActivationType == EAssetTypeActivationMethod::DoubleClicked) +// { +// bool ValidObjects = false; +// TArray> HoudiniAssets; +// if (InObjects.Num() > 0) +// { +// HoudiniAssets = GetTypedWeakObjectPtrs(InObjects); +// ValidObjects = true; +// } +// +// if (ValidObjects) +// { +// ExecuteInstantiate(HoudiniAssets); +// return true; +// } +// } +// +// return false; +// } + + +// TSharedRef +// FAssetTypeActions_HoudiniToolsPackageAsset::AddLevelEditorMenuExtenders(TArray> PackageAssets) +// { +// FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked(TEXT("LevelEditor")); +// TSharedRef LevelEditorCommandBindings = LevelEditor.GetGlobalLevelEditorActions(); +// +// FName StyleSetName = FHoudiniEngineStyle::GetStyleSetName(); +// +// TSharedRef Extender = MakeShareable(new FExtender); +// Extender->AddMenuExtension( +// "ActorAsset", +// EExtensionHook::After, +// LevelEditorCommandBindings, +// FMenuExtensionDelegate::CreateLambda([this, PackageAssets, StyleSetName](FMenuBuilder& MenuBuilder) +// { +// MenuBuilder.AddMenuEntry( +// NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_FindInExplorer", "Find Source Package"), +// NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_FindInExplorerTooltip", "Opens explorer at the source location for this package."), +// FSlateIcon(StyleSetName, "HoudiniEngine.DigitalAsset"), +// FUIAction( +// FExecuteAction::CreateSP(this, &FAssetTypeActions_HoudiniToolsPackageAsset::ExecuteFindInExplorer, PackageAssets), +// FCanExecuteAction::CreateLambda([=] { return (PackageAssets.Num() > 0); }) +// ) +// ); +// }) +// ); +// +// return Extender; +// } + +void +FAssetTypeActions_HoudiniToolsPackageAsset::ExecuteReimport(TArray> InPackagePtrs) +{ + for (auto ObjIt = InPackagePtrs.CreateConstIterator(); ObjIt; ++ObjIt) + { + UHoudiniToolsPackageAsset* ToolsPackage = (*ObjIt).Get(); + if (!IsValid(ToolsPackage)) + { + continue; + } + + // Attempt to import the package description + FHoudiniToolsEditor::ReimportExternalToolsPackageDescription(ToolsPackage); + FHoudiniToolsEditor::ReimportPackageHDAs(ToolsPackage); + + if (GEditor) + { + // Since we're doing a manual reimport of external data, we're manually triggering reimport events here + // to allow the Tools panel to refresh. + GEditor->GetEditorSubsystem()->BroadcastAssetReimport(ToolsPackage); + } + } +} + +void +FAssetTypeActions_HoudiniToolsPackageAsset::ExecuteReimportDescriptionOnly(TArray> InPackagePtrs) +{ + for (auto ObjIt = InPackagePtrs.CreateConstIterator(); ObjIt; ++ObjIt) + { + UHoudiniToolsPackageAsset* ToolsPackage = (*ObjIt).Get(); + if (!IsValid(ToolsPackage)) + { + continue; + } + + // Attempt to import the package description + FHoudiniToolsEditor::ReimportExternalToolsPackageDescription(ToolsPackage); + if (GEditor) + { + // Since we're doing a manual reimport of external data, we're manually triggering reimport events here + // to allow the Tools panel to refresh. + GEditor->GetEditorSubsystem()->BroadcastAssetReimport(ToolsPackage); + } + } +} + +void +FAssetTypeActions_HoudiniToolsPackageAsset::ExecuteImportNewHDAs(TArray> InPackagePtrs) +{ + for (auto ObjIt = InPackagePtrs.CreateConstIterator(); ObjIt; ++ObjIt) + { + UHoudiniToolsPackageAsset* ToolsPackage = (*ObjIt).Get(); + if (!IsValid(ToolsPackage)) + { + continue; + } + + // Only import missing HDAs into this package. + FHoudiniToolsEditor::ReimportPackageHDAs(ToolsPackage, true); + } +} + +void +FAssetTypeActions_HoudiniToolsPackageAsset::ExecuteFindInExplorer(TArray> InPackagePtrs) +{ + for (auto ObjIt = InPackagePtrs.CreateConstIterator(); ObjIt; ++ObjIt) + { + const UHoudiniToolsPackageAsset* ToolsPackage = (*ObjIt).Get(); + if (!IsValid(ToolsPackage)) + { + continue; + } + + const FString PackageDir = FHoudiniToolsEditor::GetAbsoluteToolsPackagePath(ToolsPackage); + if (PackageDir.Len() == 0) + { + UE_LOG(LogHoudiniTools, Warning, TEXT("Tools Package (%s) does not have a valid external path."), *ToolsPackage->GetPathName()); + continue; + } + + if (!FPaths::DirectoryExists(PackageDir)) + { + UE_LOG(LogHoudiniTools, Warning, TEXT("Tools Package (%s) external path '%s' does not exist."), *ToolsPackage->GetPathName(), *PackageDir); + continue; + } + + return FPlatformProcess::ExploreFolder(*PackageDir); + } +} + + + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngineEditor/Private/AssetTypeActions_HoudiniToolsPackageAsset.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "AssetTypeActions_Base.h" +#include "HoudiniToolsPackageAsset.h" + +class UClass; +class UObject; +class UHoudiniAsset; + +enum class EHoudiniToolType : uint8; + +// TODO: UE5.2 uses Asset Definitions instead of Asset Actions. See AssetDefinitions.h for a conversion guide. + +class FAssetTypeActions_HoudiniToolsPackageAsset : public FAssetTypeActions_Base +{ + public: + + // FAssetTypeActions_Base methods. + virtual FText GetName() const override; + virtual FColor GetTypeColor() const override; + virtual UClass* GetSupportedClass() const override; + virtual uint32 GetCategories() override; + + virtual bool HasActions(const TArray< UObject * > & InObjects) const override; + virtual void GetActions(const TArray< UObject * > & InObjects, class FMenuBuilder & MenuBuilder) override; + + protected: + + // Handler for reimport option. + void ExecuteReimport(TArray> PackageAssets); + void ExecuteReimportDescriptionOnly(TArray> PackageAssets); + void ExecuteImportNewHDAs(TArray> PackageAssets); + + // Handler for find in explorer option + void ExecuteFindInExplorer(TArray> PackageAssets); +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniAssetActorFactory.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniAssetActorFactory.h" + +#include "HoudiniEngineEditorPrivatePCH.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniEngineUtils.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +UHoudiniAssetActorFactory::UHoudiniAssetActorFactory(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) +{ + DisplayName = LOCTEXT("HoudiniAssetDisplayName", "Houdini Engine Asset"); + NewActorClass = AHoudiniAssetActor::StaticClass(); +} + +bool +UHoudiniAssetActorFactory::CanCreateActorFrom(const FAssetData & AssetData, FText & OutErrorMsg) +{ + if (AssetData.IsValid() && AssetData.IsInstanceOf(UHoudiniAsset::StaticClass())) + { + return true; + } + else + { + OutErrorMsg = NSLOCTEXT("HoudiniEngine", "NoHoudiniAsset", "A valid Houdini Engine asset must be specified."); + return false; + } +} + +UObject * +UHoudiniAssetActorFactory::GetAssetFromActorInstance(AActor * Instance) +{ + check(Instance->IsA(NewActorClass)); + AHoudiniAssetActor * HoudiniAssetActor = CastChecked(Instance); + + if (HoudiniAssetActor->GetHoudiniCookable()) + { + // Get the HDA via the Cookable + if (HoudiniAssetActor->HoudiniCookable->IsHoudiniAssetSupported()) + { + return HoudiniAssetActor->HoudiniCookable->GetHoudiniAsset(); + } + } + + return nullptr; +} + +void +UHoudiniAssetActorFactory::PostSpawnActor(UObject * Asset, AActor * NewActor) +{ + HOUDINI_LOG_MESSAGE(TEXT("PostSpawnActor %s, supplied Asset = 0x%0.8p"), *NewActor->GetActorNameOrLabel(), Asset); + + UHoudiniAsset* HoudiniAsset = Cast(Asset); + AHoudiniAssetActor * HoudiniAssetActor = CastChecked(NewActor); + + UHoudiniCookable* HC = HoudiniAssetActor->GetHoudiniCookable(); + check(HC); + + USceneComponent* CookableComponent = HC->GetComponent(); + check(CookableComponent); + + FHoudiniEngineUtils::AddHoudiniLogoToComponent(CookableComponent); + + if (!HoudiniAssetActor->IsUsedForPreview()) + { + if (IsValid(HoudiniAsset)) + { + HC->SetHoudiniAsset(HoudiniAsset); + } + + FHoudiniEngineRuntime::Get().RegisterHoudiniCookable(HoudiniAssetActor->HoudiniCookable); + } +} + +void +UHoudiniAssetActorFactory::PostCreateBlueprint(UObject * Asset, AActor * CDO) +{ + HOUDINI_LOG_MESSAGE(TEXT("PostCreateBlueprint, supplied Asset = 0x%0.8p"), Asset); + + // TODO COOKABLE: HANDLE ME! + + UHoudiniAsset * HoudiniAsset = CastChecked(Asset); + if (HoudiniAsset) + { + AHoudiniAssetActor * HoudiniAssetActor = CastChecked< AHoudiniAssetActor >(CDO); + UHoudiniAssetComponent * HoudiniAssetComponent = HoudiniAssetActor->GetHoudiniAssetComponent(); + check(HoudiniAssetComponent); + + FHoudiniEngineUtils::AddHoudiniLogoToComponent(HoudiniAssetComponent); + + if (!HoudiniAssetActor->IsUsedForPreview()) + { + UHoudiniCookable* Cookable = HoudiniAssetActor->GetHoudiniCookable(); + check(Cookable); + + Cookable->SetHoudiniAsset(HoudiniAsset); + FHoudiniEngineRuntime::Get().RegisterHoudiniCookable(Cookable); + } + } +} + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngineEditor/Private/HoudiniAssetActorFactory.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "ActorFactories/ActorFactory.h" +#include "Runtime/Launch/Resources/Version.h" +#include "HoudiniAssetActorFactory.generated.h" + +class FText; +class AActor; +class UObject; +class UHoudiniAssetComponent; + +struct FAssetData; + +UCLASS(config = Editor) +class UHoudiniAssetActorFactory : public UActorFactory +{ + GENERATED_UCLASS_BODY() + +public: + // UActorFactory methods: + // Return true if Actor can be created from a given asset. + virtual bool CanCreateActorFrom(const FAssetData & AssetData, FText & OutErrorMsg) override; + // Given an instance of an actor pertaining to this factory, find the asset that should be used to create a new actor. + virtual UObject * GetAssetFromActorInstance(AActor * Instance) override; + // Modify the actor after it has been spawned. + virtual void PostSpawnActor(UObject * Asset, AActor * NewActor) override; + // Called after a blueprint is created by this factory to update the blueprint's CDO properties + // with state from the asset for this factory. +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + //TODO:5.4 ?? FIX ME ?? + virtual void PostCreateBlueprint(UObject* Asset, AActor* CDO); +#else + virtual void PostCreateBlueprint(UObject * Asset, AActor * CDO) override; +#endif + + +protected: + bool AddHoudiniLogoToComponent(UHoudiniAssetComponent* HAC); +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniAssetBroker.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniAssetBroker.h" + +#include "HoudiniAssetComponent.h" +#include "HoudiniAsset.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +FHoudiniAssetBroker::~FHoudiniAssetBroker() +{ + +} + +UClass * +FHoudiniAssetBroker::GetSupportedAssetClass() +{ + return UHoudiniAsset::StaticClass(); +} + +bool +FHoudiniAssetBroker::AssignAssetToComponent(UActorComponent * InComponent, UObject * InAsset) +{ + if (UHoudiniAssetComponent* HoudiniAssetComponent = Cast(InComponent)) + { + UHoudiniAsset* HoudiniAsset = Cast(InAsset); + if (HoudiniAsset || !InAsset) + { + HoudiniAssetComponent->SetHoudiniAsset(HoudiniAsset); + return true; + } + } + + return false; +} + +UObject * +FHoudiniAssetBroker::GetAssetFromComponent(UActorComponent * InComponent) +{ + if (UHoudiniAssetComponent* HoudiniAssetComponent = Cast(InComponent)) + { + return HoudiniAssetComponent->GetHoudiniAsset(); + } + + return nullptr; +} + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngineEditor/Private/HoudiniAssetBroker.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "ComponentAssetBroker.h" + +class UObject; +class UActorComponent; + +class FHoudiniAssetBroker : public IComponentAssetBroker +{ +public: + + virtual ~FHoudiniAssetBroker(); + + // IComponentAssetBroker methods. + // Reports the asset class this broker knows how to handle. + UClass * GetSupportedAssetClass() override; + + // Assign the assigned asset to the supplied component. + bool AssignAssetToComponent(UActorComponent * InComponent, UObject * InAsset) override; + + // Get the currently assigned asset from the component. + UObject * GetAssetFromComponent(UActorComponent * InComponent) override; +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniAssetComponentDetails.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniAssetComponentDetails.h" + +#include "HoudiniAssetComponent.h" +#include "HoudiniAsset.h" +#include "HoudiniCookable.h" +#include "HoudiniCookableDetails.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniParameter.h" +#include "HoudiniHandleComponent.h" +#include "HoudiniParameterDetails.h" +#include "HoudiniInput.h" +#include "HoudiniInputDetails.h" +#include "HoudiniHandleDetails.h" +#include "HoudiniNodeSyncComponent.h" +#include "HoudiniOutput.h" +#include "HoudiniOutputDetails.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Layout/SSeparator.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/Images/SImage.h" +#include "HoudiniEngineEditorPrivatePCH.h" + +#include "PropertyCustomizationHelpers.h" +#include "DetailLayoutBuilder.h" +#include "DetailCategoryBuilder.h" +#include "SHoudiniPresets.h" +#include "Chaos/AABB.h" +#include "Chaos/AABB.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Widgets/Images/SLayeredImage.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +TSharedRef< IDetailCustomization > +FHoudiniAssetComponentDetails::MakeInstance() +{ + return MakeShareable(new FHoudiniAssetComponentDetails); +} + +FHoudiniAssetComponentDetails::FHoudiniAssetComponentDetails() +{ + CookableDetails = MakeShared(); +} + +void +FHoudiniAssetComponentDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + // Get all components which are being customized. + TArray> ObjectsCustomized; + DetailBuilder.GetObjectsBeingCustomized(ObjectsCustomized); + + bool bHasCookable = false; + // Extract the Houdini Asset Component to detail + for (int32 i = 0; i < ObjectsCustomized.Num(); ++i) + { + if (!IsValidWeakPointer(ObjectsCustomized[i])) + continue; + + UObject * Object = ObjectsCustomized[i].Get(); + if (Object) + { + UHoudiniAssetComponent * HAC = Cast(Object); + if (!IsValid(HAC)) + continue; + + HoudiniAssetComponents.Add(HAC); + if (HAC->GetCookable()) + bHasCookable = true; + } + } + + // Make sure the HAC has a cookable to show the UI + if (!bHasCookable) + return; + + CookableDetails->CustomizeDetails(DetailBuilder); +} + + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniAssetComponentDetails.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "IDetailCustomization.h" +#include "HoudiniCookableDetails.h" +#include "HoudiniPDGDetails.h" +#include "HoudiniOutputDetails.h" +#include "HoudiniParameterDetails.h" +#include "HoudiniEngineDetails.h" + +class UHoudiniAssetComponent; +class UStaticMesh; + +class FHoudiniAssetComponentDetails : public IDetailCustomization +{ +public: + + // Constructor. + FHoudiniAssetComponentDetails(); + + // IDetailCustomization methods. + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; + + // Create an instance of this detail layout class. + static TSharedRef MakeInstance(); + +private: + + // Handler for double clicking the static mesh thumbnail, opens the editor. + FReply OnThumbnailDoubleClick( + const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent, UObject* Object); + + // Components which are being customized. + TArray> HoudiniAssetComponents; + + + // Structure holding the Cookable details + TSharedPtr CookableDetails; +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniAssetEditor.cpp`: + +```cpp +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniAssetEditor.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetEditorViewportClient.h" +#include "HoudiniCookable.h" +#include "HoudiniCookableDetails.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineCommands.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineStyle.h" +#include "SHoudiniAssetEditorViewport.h" +#include "SHoudiniNodeSyncPanel.h" + + +#include "DetailCategoryBuilder.h" +#include "DetailLayoutBuilder.h" +#include "Framework/Commands/UICommandList.h" +#include "Materials/MaterialInstanceConstant.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 +#include "MaterialShared.h" +#endif +#include "PropertyEditorDelegates.h" +#include "SCommonEditorViewportToolbarBase.h" +#include "UObject/UObjectGlobals.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/Docking/SDockTab.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Text/STextBlock.h" + +#define LOCTEXT_NAMESPACE "HoudiniAssetEditor" + +const FName HoudiniAssetEditorAppName = FName(TEXT("HoudiniAssetEditorApp")); + +struct FHoudiniAssetEditorTabs +{ + // Tab identifiers + static const FName DetailsID; + static const FName ViewportID; + static const FName NodeSyncID; + static const FName PreviewSceneSettingsID; +}; + +const FName FHoudiniAssetEditorTabs::DetailsID(TEXT("Details")); +const FName FHoudiniAssetEditorTabs::ViewportID(TEXT("Viewport")); +const FName FHoudiniAssetEditorTabs::NodeSyncID(TEXT("NodeSync")); +const FName FHoudiniAssetEditorTabs::PreviewSceneSettingsID(TEXT("PreviewSceneSettings")); + +//----------------------------------------------------------------------------- +// SHoudiniAssetEditorDetailsPanel +//----------------------------------------------------------------------------- +void +SHoudiniAssetEditorDetailsPanel::Construct( + const FArguments& InArgs, TSharedPtr InHoudiniAssetEditor) +{ + HoudiniAssetEditorPtr = InHoudiniAssetEditor; + + // Create a property view + FPropertyEditorModule& EditModule = FModuleManager::Get().GetModuleChecked("PropertyEditor"); + + // Get the ViewIdentifier for this details view from the AssetEditor + FString DetailsIdentifier = InHoudiniAssetEditor->GetHoudiniAssetEditorIdentifier(); + + FDetailsViewArgs DetailsViewArgs; + DetailsViewArgs.bAllowSearch = true; + DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; + DetailsViewArgs.bHideSelectionTip = true; + DetailsViewArgs.HostCommandList = InHoudiniAssetEditor->GetToolkitCommands(); + DetailsViewArgs.HostTabManager = InHoudiniAssetEditor->GetTabManager(); + DetailsViewArgs.ViewIdentifier = FName(*DetailsIdentifier); + + PropertyView = EditModule.CreateDetailView(DetailsViewArgs); + + // Create the box that will contain all our content + ChildSlot + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .FillHeight(1.0f) + .Padding( 3.0f, 2.0f ) + [ + PopulateSlot(PropertyView.ToSharedRef()) + ] + ]; + + // For Cookable details customization + FOnGetDetailCustomizationInstance CustomizeHoudiniAssetForEditor = FOnGetDetailCustomizationInstance::CreateStatic(&FHoudiniCookableDetails::MakeInstance); + PropertyView->RegisterInstancedCustomPropertyLayout(UHoudiniAsset::StaticClass(), CustomizeHoudiniAssetForEditor); + + /* + // TODO: Offer modes for HDA editor?? + TAttribute HoudiniAssetEditorMode = TAttribute::Create( + TAttribute::FGetter::CreateSP(InHoudiniAssetEditor.ToSharedRef(), &FHoudiniAssetEditor::GetCurrentMode)); + */ + +} + +UObject* +SHoudiniAssetEditorDetailsPanel::GetObjectToObserve() const +{ + return HoudiniAssetEditorPtr.Pin()->GetHoudiniCookableBeingEdited(); +} + +TSharedRef +SHoudiniAssetEditorDetailsPanel::PopulateSlot(TSharedRef PropertyEditorWidget) +{ + return SNew(SVerticalBox) + + SVerticalBox::Slot() + .FillHeight(1) + [ + PropertyEditorWidget + ]; +} + +void +SHoudiniAssetEditorDetailsPanel::Tick( + const FGeometry& AllottedGeometry, + const double InCurrentTime, + const float InDeltaTime) +{ + // In order to be able to specify the identifier for this editor manually, + // we had to directly use the FPropertyEditorModule's create function instead of + // using the SSingleObjectDetailsPanel function. + // This prevents us from setting bAutoObserveObject on the SSingleObjectDetailsPanel... + // ... so reproduce its behavior here... + // see SSingleObjectDetailsPanel::Tick() + + UObject* CurrentObject = GetObjectToObserve(); + if (MyLastObservedObject.Get() != CurrentObject) + { + MyLastObservedObject = CurrentObject; + + TArray SelectedObjects; + if (CurrentObject != NULL) + { + SelectedObjects.Add(CurrentObject); + } + + SetPropertyWindowContents(SelectedObjects); + } +} + + +//----------------------------------------------------------------------------- +// FHoudiniAssetEditor +//----------------------------------------------------------------------------- +FHoudiniAssetEditor::FHoudiniAssetEditor() + : HoudiniAssetBeingEdited(nullptr), + HoudiniCookableBeingEdited(nullptr), + HoudiniAssetEditorIdentifier() +{ +} + +TSharedRef +FHoudiniAssetEditor::SpawnViewportTab(const FSpawnTabArgs& Args) +{ + return SNew(SDockTab) + .Label(LOCTEXT("ViewportTabTitle", "Viewport")) + [ + SNew(SOverlay) + + // The editor viewport + + SOverlay::Slot() + [ + ViewportPtr.ToSharedRef() + ] + + // Bottom-right corner text indicating the mode of the editor + + SOverlay::Slot() + .Padding(10) + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Right) + [ + SNew(STextBlock) + .Visibility(EVisibility::HitTestInvisible) + .TextStyle(FAppStyle::Get(), "Graph.CornerText") + .Text(this, &FHoudiniAssetEditor::GetViewportCornerText) + ] + ]; +} + +TSharedRef +FHoudiniAssetEditor::SpawnDetailsTab(const FSpawnTabArgs& Args) +{ + TSharedPtr HoudiniAssetEditorPtr = SharedThis(this); + + // Spawn the tab + return SNew(SDockTab) + .Label(LOCTEXT("DetailsTabTitle", "Details")) + [ + DetailsTabPtr.ToSharedRef() + ]; +} + +TSharedRef +FHoudiniAssetEditor::SpawnNodeSyncTab(const FSpawnTabArgs& Args) +{ + // Set the Node Sync panel to the AssetEditor mode + //NodeSyncPanel->SetIsAssetEditorPanel(true); + + TSharedRef SpawnedTab = SNew(SDockTab) + .TabRole(ETabRole::NomadTab) + .Label(LOCTEXT("NodeSyncTabTitle", "Node Sync")) + [ + SAssignNew(NodeSyncPanel, SHoudiniNodeSyncPanel) + .IsAssetEditor(true) + ]; + + SpawnedTab->SetTabIcon(FHoudiniEngineStyle::Get()->GetBrush("HoudiniEngine.HoudiniEngineLogo")); + + return SpawnedTab; +} + +TSharedRef +FHoudiniAssetEditor::SpawnPreviewSceneSettingsTab(const FSpawnTabArgs& Args) +{ + FAdvancedPreviewSceneModule& AdvancedPreviewSceneModule = FModuleManager::LoadModuleChecked("AdvancedPreviewScene"); + + TArray Delegates; + Delegates.Add({ OnPreviewSceneChangedDelegate }); + AdvancedPreviewSettingsWidget = AdvancedPreviewSceneModule.CreateAdvancedPreviewSceneSettingsWidget( + ViewportPtr->GetPreviewScene(), + nullptr, + TArray(), + TArray(), + Delegates); + + //check(Args.GetTabId() == PreviewSceneSettingsTabId); + return SAssignNew(PreviewSceneDockTab, SDockTab) + .Label(LOCTEXT("PReviewSceneSettingsTabTitle", "Preview Scene Settings")) + [ + AdvancedPreviewSettingsWidget.IsValid() ? AdvancedPreviewSettingsWidget.ToSharedRef() : SNullWidget::NullWidget + ]; +} + +void +FHoudiniAssetEditor::RegisterTabSpawners(const TSharedRef& InTabManager) +{ + WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_HoudiniAssetEditor", "Houdini Asset Editor")); + auto WorkspaceMenuCategoryRef = WorkspaceMenuCategory.ToSharedRef(); + + FAssetEditorToolkit::RegisterTabSpawners(InTabManager); + + // VIEWPORT + InTabManager->RegisterTabSpawner(FHoudiniAssetEditorTabs::ViewportID, FOnSpawnTab::CreateSP(this, &FHoudiniAssetEditor::SpawnViewportTab)) + .SetDisplayName(LOCTEXT("ViewportTab", "Viewport")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Viewports")); + + // DETAILS + InTabManager->RegisterTabSpawner(FHoudiniAssetEditorTabs::DetailsID, FOnSpawnTab::CreateSP(this, &FHoudiniAssetEditor::SpawnDetailsTab)) + .SetDisplayName(LOCTEXT("DetailsTabLabel", "Details")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Details")); + + /* + // NODE SYNC + InTabManager->RegisterTabSpawner(FHoudiniAssetEditorTabs::NodeSyncID, FOnSpawnTab::CreateSP(this, &FHoudiniAssetEditor::SpawnNodeSyncTab)) + .SetDisplayName(LOCTEXT("NodeSyncTabLabel", "Node Sync")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.ContentBrowser")); + */ + + // PREVIEW SCENE SETTINGS + InTabManager->RegisterTabSpawner(FHoudiniAssetEditorTabs::PreviewSceneSettingsID, FOnSpawnTab::CreateSP(this, &FHoudiniAssetEditor::SpawnPreviewSceneSettingsTab)) + .SetDisplayName(LOCTEXT("PreviewSceneTab", "Preview Scene Settings")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Details")) +#if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 3) + .SetReadOnlyBehavior(ETabReadOnlyBehavior::Custom); +#else + ; +#endif +} + +void +FHoudiniAssetEditor::UnregisterTabSpawners(const TSharedRef& InTabManager) +{ + FAssetEditorToolkit::UnregisterTabSpawners(InTabManager); + + InTabManager->UnregisterTabSpawner(FHoudiniAssetEditorTabs::ViewportID); + InTabManager->UnregisterTabSpawner(FHoudiniAssetEditorTabs::DetailsID); + //InTabManager->UnregisterTabSpawner(FHoudiniAssetEditorTabs::NodeSyncID); + InTabManager->UnregisterTabSpawner(FHoudiniAssetEditorTabs::PreviewSceneSettingsID); +} + +void +FHoudiniAssetEditor::InitHoudiniAssetEditor( + const EToolkitMode::Type Mode, + const TSharedPtr& InitToolkitHost, + class UHoudiniAsset* InitHDA) +{ + HoudiniAssetBeingEdited = InitHDA; + + bIsViewingCopHDA = false; + + bShowRedChannel = true; + bShowGreenChannel = true; + bShowBlueChannel = true; + bShowAlphaChannel = true; + + SelectedTextureOutput = 0; + NumTextureOutputs = 0; + + // Get the next available Identifier for our details + if(HoudiniAssetEditorIdentifier.IsEmpty()) + HoudiniAssetEditorIdentifier = FHoudiniEngine::Get().RegisterNewHoudiniAssetEditor(); + + BindCommands(); + + TSharedPtr HoudiniAssetEditorPtr = SharedThis(this); + ViewportPtr = SNew(SHoudiniAssetEditorViewport, HoudiniAssetEditorPtr); + DetailsTabPtr = SNew(SHoudiniAssetEditorDetailsPanel, HoudiniAssetEditorPtr); + + // Default layout + const TSharedRef StandaloneDefaultLayout = FTabManager::NewLayout("Standalone_HoudiniAssetEditor_Layout_v1") + ->AddArea + ( + FTabManager::NewPrimaryArea() + ->SetOrientation(Orient_Vertical) + ->Split + ( + FTabManager::NewSplitter() + ->SetOrientation(Orient_Horizontal) + ->SetSizeCoefficient(0.9f) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.8f) + ->SetHideTabWell(true) + ->AddTab(FHoudiniAssetEditorTabs::ViewportID, ETabState::OpenedTab) + ) + ->Split + ( + FTabManager::NewSplitter() + ->SetOrientation(Orient_Vertical) + ->SetSizeCoefficient(0.2f) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.75f) + ->SetHideTabWell(true) + ->AddTab(FHoudiniAssetEditorTabs::DetailsID, ETabState::OpenedTab) + ) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.25f) + ->AddTab(FHoudiniAssetEditorTabs::NodeSyncID, ETabState::OpenedTab) + ) + ) + ) + ); + + // Initialize the asset editor + InitAssetEditor( + Mode, + InitToolkitHost, + HoudiniAssetEditorAppName, + StandaloneDefaultLayout, + /*bCreateDefaultStandaloneMenu=*/ true, + /*bCreateDefaultToolbar=*/ true, + InitHDA); + + // Set the Houdini Asset on the viewport + // This will instantiate the HDA in the editor world, creating the Actor, Component and Cookable + ViewportPtr->SetHoudiniAsset(HoudiniAssetBeingEdited); + + // Get the Houdini Asset Actor created in the viewport + AHoudiniAssetActor* HAA = ViewportPtr->GetViewportClient()->GetHoudiniAssetActor(); + if (HAA) + { + // Initialize the Cookable + HoudiniCookableBeingEdited = HAA->GetHoudiniCookable(); + + if (HoudiniCookableBeingEdited) + { + // Set supported features in the asset editor + HoudiniCookableBeingEdited->SetHoudiniAssetSupported(true); + HoudiniCookableBeingEdited->SetParameterSupported(true); + HoudiniCookableBeingEdited->SetInputSupported(true); + HoudiniCookableBeingEdited->SetOutputSupported(true); + HoudiniCookableBeingEdited->SetComponentSupported(true); + HoudiniCookableBeingEdited->SetBakingSupported(true); + + // NO PDG - NO PROXIES + HoudiniCookableBeingEdited->SetPDGSupported(false); + HoudiniCookableBeingEdited->SetProxySupported(false); + + // Change the default bake type + HoudiniCookableBeingEdited->SetHoudiniEngineBakeOption(EHoudiniEngineBakeOption::ToAsset); + + // Let the cookable know its used in an Houdini Asset Editor + HoudiniCookableBeingEdited->AssetEditorId = FName(*HoudiniAssetEditorIdentifier); + + + HoudiniCookableBeingEdited->GetOnPostOutputProcessingDelegate().AddRaw(this, &FHoudiniAssetEditor::OnPostOutputProcess); + + // Register the Cookable with the Manager + FHoudiniEngineRuntime::Get().RegisterHoudiniCookable(HoudiniCookableBeingEdited); + } + } + + // Extend things + ExtendMenu(); + ExtendToolbar(); + RegenerateMenusAndToolbars(); +} + +void +FHoudiniAssetEditor::OnPostOutputProcess(UHoudiniCookable* _HC, bool bSuccess) +{ + // See if this Cookable is texture only + bool bTextureOnly = true; + NumTextureOutputs = 0; + for (int32 OutputIdx = 0; OutputIdx < _HC->GetNumOutputs(); OutputIdx++) + { + UHoudiniOutput* CurOutput = _HC->GetOutputAt(OutputIdx); + if (!IsValid(CurOutput)) + continue; + + // Make sure we have a COP output ... + if (CurOutput->GetType() != EHoudiniOutputType::Cop) + { + bTextureOnly = false; + continue; + } + + // ... that is valid + if (CurOutput->GetOutputObjects().Num() <= 0) + continue; + + NumTextureOutputs++; + } + + // Don't switch to a 2D Viewport if we dont output anything! + if (NumTextureOutputs <= 0) + bTextureOnly = false; + + if (bTextureOnly) + { + if (!bIsViewingCopHDA) + { + // Switch viewport to texture + ViewportPtr->GetViewportClient()->SetViewportTo2D(); + bIsViewingCopHDA = true; + } + } + else + { + if (bIsViewingCopHDA) + { + // Switch viewport to 3D + ViewportPtr->GetViewportClient()->SetViewportTo3D(); + bIsViewingCopHDA = false; + } + } + + // Update available 2d outputs + UpdateOutputList(); + + UpdateTextureOutputOnPreviewMesh(); + + ViewportPtr->Invalidate(); + +} + +void +FHoudiniAssetEditor::OnClose() +{ + // Unregister our Cookable + FHoudiniEngineRuntime::Get().UnRegisterHoudiniCookable(HoudiniCookableBeingEdited); + + // Unregister our Details Identifier + FHoudiniEngine::Get().UnRegisterHoudiniAssetEditor(HoudiniAssetEditorIdentifier); + + // TODO: + // Check if we need to manually clean up the scene / delete HAA +} + +void +FHoudiniAssetEditor::BindCommands() +{ +} + +FName +FHoudiniAssetEditor::GetToolkitFName() const +{ + return FName("HoudiniAssetEditor"); +} + +FText +FHoudiniAssetEditor::GetBaseToolkitName() const +{ + return LOCTEXT("HoudiniAssetEditorAppLabel", "Houdini Asset Editor"); +} + +FText +FHoudiniAssetEditor::GetToolkitName() const +{ + return FText::FromString(HoudiniAssetBeingEdited->GetName()); +} + +FText +FHoudiniAssetEditor::GetToolkitToolTipText() const +{ + return FAssetEditorToolkit::GetToolTipTextForObject(HoudiniAssetBeingEdited); +} + +FString +FHoudiniAssetEditor::GetWorldCentricTabPrefix() const +{ + return TEXT("HoudiniAssetEditor"); +} + +FString FHoudiniAssetEditor::GetDocumentationLink() const +{ + return TEXT("https://www.sidefx.com/docs/houdini/unreal/"); +} + +void +FHoudiniAssetEditor::OnToolkitHostingStarted(const TSharedRef& Toolkit) +{ + // @TODO: MODETOOLS: Need to be able to register the widget in the toolbox panel with ToolkitHost, + // so it can instance the ed mode widgets into it + // TSharedPtr InlineContent = Toolkit->GetInlineContent(); + // if (InlineContent.IsValid()) + // { + // ToolboxPtr->SetContent(InlineContent.ToSharedRef()); + // } +} + +void +FHoudiniAssetEditor::OnToolkitHostingFinished(const TSharedRef& Toolkit) +{ + //ToolboxPtr->SetContent(SNullWidget::NullWidget); + //@TODO: MODETOOLS: How to handle multiple ed modes at once in a standalone asset editor? +} + +FLinearColor +FHoudiniAssetEditor::GetWorldCentricTabColorScale() const +{ + return FLinearColor(255.0f, 165.0f/255.0f, 0.0f); +} + +void +FHoudiniAssetEditor::AddReferencedObjects(FReferenceCollector& Collector) +{ + Collector.AddReferencedObject(HoudiniAssetBeingEdited); + Collector.AddReferencedObject(HoudiniCookableBeingEdited); +} + +void +FHoudiniAssetEditor::ExtendMenu() +{ + MainMenuExtender = MakeShareable(new FExtender); + + // Extend File menu, we will add Houdini section. + MainMenuExtender->AddMenuExtension( + "FileLoadAndSave", + EExtensionHook::After, + FHoudiniEngineEditor::Get().GetHoudiniEngineCommands(), + FMenuExtensionDelegate::CreateStatic(&FHoudiniEngineEditor::AddHoudiniFileMenuExtension)); + + MainMenuExtender->AddMenuBarExtension( + "Edit", + EExtensionHook::After, + FHoudiniEngineEditor::Get().GetHoudiniEngineCommands(), + FMenuBarExtensionDelegate::CreateStatic(&FHoudiniEngineEditor::AddHoudiniEditorMenu)); + + AddMenuExtender(MainMenuExtender); +} + + +void +FHoudiniAssetEditor::ExtendToolbar() +{ + TSharedPtr ToolbarExtender = MakeShareable(new FExtender); + + ToolbarExtender->AddToolBarExtension( + "Asset", + EExtensionHook::After, + ViewportPtr->GetCommandList(), + FToolBarExtensionDelegate::CreateSP(this, &FHoudiniAssetEditor::FillToolbar) + ); + + AddToolbarExtender(ToolbarExtender); +} + +void +FHoudiniAssetEditor::FillToolbar(FToolBarBuilder& ToolbarBuilder) +{ + TSharedRef ChannelControl = MakeChannelControlWidget(); + TSharedRef TextureOutput = MakeTextureOutputWidget(); + + ToolbarBuilder.BeginSection("Channels"); + { + ToolbarBuilder.AddWidget(ChannelControl); + ToolbarBuilder.AddWidget(TextureOutput); + } + ToolbarBuilder.EndSection(); +} + + +FSlateColor +FHoudiniAssetEditor::GetChannelButtonBackgroundColor(ETextureChannelButton Button) const +{ + FSlateColor Dropdown = FAppStyle::Get().GetSlateColor("Colors.Dropdown"); + + switch (Button) + { + case ETextureChannelButton::Red: + return bShowRedChannel ? FLinearColor::Red : FLinearColor::White; + case ETextureChannelButton::Green: + return bShowGreenChannel ? FLinearColor::Green : FLinearColor::White; + case ETextureChannelButton::Blue: + return bShowBlueChannel ? FLinearColor::Blue : FLinearColor::White; + case ETextureChannelButton::Alpha: + return FLinearColor::White; + default: + check(false); + return FSlateColor(); + } +} + +FSlateColor +FHoudiniAssetEditor::GetChannelButtonForegroundColor(ETextureChannelButton Button) const +{ + FSlateColor DefaultForeground = FAppStyle::Get().GetSlateColor("Colors.Foreground"); + + switch (Button) + { + case ETextureChannelButton::Red: + return bShowRedChannel ? FLinearColor::Black : DefaultForeground; + case ETextureChannelButton::Green: + return bShowGreenChannel ? FLinearColor::Black : DefaultForeground; + case ETextureChannelButton::Blue: + return bShowBlueChannel ? FLinearColor::Black : DefaultForeground; + case ETextureChannelButton::Alpha: + return bShowAlphaChannel ? FLinearColor::Black : DefaultForeground; + default: + check(false); + return FSlateColor::UseForeground(); + } +} + +ECheckBoxState +FHoudiniAssetEditor::OnGetChannelButtonCheckState(ETextureChannelButton Button) const +{ + switch (Button) + { + case ETextureChannelButton::Red: + return bShowRedChannel ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + case ETextureChannelButton::Green: + return bShowGreenChannel ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + case ETextureChannelButton::Blue: + return bShowBlueChannel ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + case ETextureChannelButton::Alpha: + return bShowAlphaChannel ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + default: + check(false); + return ECheckBoxState::Unchecked; + } +} + +TSharedRef +FHoudiniAssetEditor::MakeChannelControlWidget() +{ + auto OnChannelCheckStateChanged = [this](ECheckBoxState NewState, ETextureChannelButton Button) + { + switch (Button) + { + case ETextureChannelButton::Red: + bShowRedChannel = !bShowRedChannel; + break; + case ETextureChannelButton::Green: + bShowGreenChannel = !bShowGreenChannel; + break; + case ETextureChannelButton::Blue: + bShowBlueChannel = !bShowBlueChannel; + break; + case ETextureChannelButton::Alpha: + bShowAlphaChannel = !bShowAlphaChannel; + break; + default: + check(false); + break; + } + + UpdateColorChannelsOnPreviewMesh(); + }; + + auto GetChannelVisibilty = [this]() + { + return bIsViewingCopHDA ? EVisibility::Visible : EVisibility::Hidden; + }; + + TSharedRef ChannelControl = + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .Padding(2.0f) + .AutoWidth() + [ + SNew(SCheckBox) + .Style(FAppStyle::Get(), "TextureEditor.ChannelButtonStyle") + .BorderBackgroundColor(this, &FHoudiniAssetEditor::GetChannelButtonBackgroundColor, ETextureChannelButton::Red) + .ForegroundColor(this, &FHoudiniAssetEditor::GetChannelButtonForegroundColor, ETextureChannelButton::Red) + .OnCheckStateChanged_Lambda(OnChannelCheckStateChanged, ETextureChannelButton::Red) + .IsChecked(this, &FHoudiniAssetEditor::OnGetChannelButtonCheckState, ETextureChannelButton::Red) + //.IsEnabled(this, &FTextureEditorToolkit::IsChannelButtonEnabled, ETextureChannelButton::Red) + .Visibility_Lambda(GetChannelVisibilty) + [ + SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("TextureEditor.ChannelButtonFont")) + .Text(FText::FromString("R")) + ] + ] + +SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .Padding(2.0f) + .AutoWidth() + [ + SNew(SCheckBox) + .Style(FAppStyle::Get(), "TextureEditor.ChannelButtonStyle") + .BorderBackgroundColor(this, &FHoudiniAssetEditor::GetChannelButtonBackgroundColor, ETextureChannelButton::Green) + .ForegroundColor(this, &FHoudiniAssetEditor::GetChannelButtonForegroundColor, ETextureChannelButton::Green) + .OnCheckStateChanged_Lambda(OnChannelCheckStateChanged, ETextureChannelButton::Green) + .IsChecked(this, &FHoudiniAssetEditor::OnGetChannelButtonCheckState, ETextureChannelButton::Green) + //.IsEnabled(this, &FTextureEditorToolkit::IsChannelButtonEnabled, ETextureChannelButton::Green) + .Visibility_Lambda(GetChannelVisibilty) + [ + SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("TextureEditor.ChannelButtonFont")) + .Text(FText::FromString("G")) + ] + ] + + +SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .Padding(2.0f) + .AutoWidth() + [ + SNew(SCheckBox) + .Style(FAppStyle::Get(), "TextureEditor.ChannelButtonStyle") + .BorderBackgroundColor(this, &FHoudiniAssetEditor::GetChannelButtonBackgroundColor, ETextureChannelButton::Blue) + .ForegroundColor(this, &FHoudiniAssetEditor::GetChannelButtonForegroundColor, ETextureChannelButton::Blue) + .OnCheckStateChanged_Lambda(OnChannelCheckStateChanged, ETextureChannelButton::Blue) + .IsChecked(this, &FHoudiniAssetEditor::OnGetChannelButtonCheckState, ETextureChannelButton::Blue) + //.IsEnabled(this, &FTextureEditorToolkit::IsChannelButtonEnabled, ETextureChannelButton::Blue) + .Visibility_Lambda(GetChannelVisibilty) + [ + SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("TextureEditor.ChannelButtonFont")) + .Text(FText::FromString("B")) + ] + ] + +SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .Padding(2.0f) + .AutoWidth() + [ + SNew(SCheckBox) + .Style(FAppStyle::Get(), "TextureEditor.ChannelButtonStyle") + .BorderBackgroundColor(this, &FHoudiniAssetEditor::GetChannelButtonBackgroundColor, ETextureChannelButton::Alpha) + .ForegroundColor(this, &FHoudiniAssetEditor::GetChannelButtonForegroundColor, ETextureChannelButton::Alpha) + .OnCheckStateChanged_Lambda(OnChannelCheckStateChanged, ETextureChannelButton::Alpha) + .IsChecked(this, &FHoudiniAssetEditor::OnGetChannelButtonCheckState, ETextureChannelButton::Alpha) + //.IsEnabled(this, &FTextureEditorToolkit::IsChannelButtonEnabled, ETextureChannelButton::Alpha) + .Visibility_Lambda(GetChannelVisibilty) + [ + SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("TextureEditor.ChannelButtonFont")) + .Text(FText::FromString("A")) + ] + ]; + + return ChannelControl; +} + + +void +FHoudiniAssetEditor::UpdateColorChannelsOnPreviewMesh() +{ + // No need to do anything if we aren't viewing a COP + if (!bIsViewingCopHDA) + return; + + // Get our cookable's scene component + USceneComponent* CookableComponent = + HoudiniCookableBeingEdited ? HoudiniCookableBeingEdited->GetComponent() : nullptr; + if (!CookableComponent) + return; + + // Get the COP SM + UStaticMesh* HoudiniCOPMesh = FHoudiniEngine::Get().GetHoudiniCOPStaticMesh().Get(); + if (!HoudiniCOPMesh) + return; + + // Update context for generated materials (will trigger when the object goes out of scope). + FMaterialUpdateContext MaterialUpdateContext; + + // Iterate on the HAC's component + for (USceneComponent* CurrentSceneComp : CookableComponent->GetAttachChildren()) + { + if (!IsValid(CurrentSceneComp) || !CurrentSceneComp->IsA()) + continue; + + // Get the static mesh component + UStaticMeshComponent* SMC = Cast(CurrentSceneComp); + if (!IsValid(SMC)) + continue; + + // Check if the SMC is the Houdini Logo + if (SMC->GetStaticMesh() != HoudiniCOPMesh) + continue; + + UMaterialInstanceConstant* MaterialInstance = + Cast(SMC->GetMaterial(0)); + if (!MaterialInstance) + continue; + + // Apply material instance parameters + MaterialInstance->SetStaticSwitchParameterValueEditorOnly(FName("R"), bShowRedChannel); + MaterialInstance->SetStaticSwitchParameterValueEditorOnly(FName("G"), bShowGreenChannel); + MaterialInstance->SetStaticSwitchParameterValueEditorOnly(FName("B"), bShowBlueChannel); + MaterialInstance->SetStaticSwitchParameterValueEditorOnly(FName("A"), bShowAlphaChannel); + + MaterialUpdateContext.AddMaterialInstance(MaterialInstance); + } +} + +void +FHoudiniAssetEditor::UpdateTextureOutputOnPreviewMesh() +{ + // No need to do anything if we aren't viewing a COP + if (!bIsViewingCopHDA) + return; + + // Get our cookable's scene component + USceneComponent* CookableComponent = + HoudiniCookableBeingEdited ? HoudiniCookableBeingEdited->GetComponent() : nullptr; + if (!CookableComponent) + return; + + // Get the COP SM + UStaticMesh* HoudiniCOPMesh = FHoudiniEngine::Get().GetHoudiniCOPStaticMesh().Get(); + if (!HoudiniCOPMesh) + return; + + // Update context for generated materials (will trigger when the object goes out of scope). + FMaterialUpdateContext MaterialUpdateContext; + + // Get the texture for the selected output + UTexture2D* SelectedTexture = nullptr; + UHoudiniOutput* CurOutput = HoudiniCookableBeingEdited->GetOutputAt(SelectedTextureOutput); + if (IsValid(CurOutput) && CurOutput->GetType() == EHoudiniOutputType::Cop) + { + for (auto& It : CurOutput->GetOutputObjects()) + { + // ... Get the first valid texture for display purpose + SelectedTexture = Cast(It.Value.OutputObject); + if (IsValid(CurOutput)) + break; + } + } + + // Fully stream in the texture before drawing it. + // Not doing this would cause the texture to appear blurry in the ortho viewport + if (SelectedTexture) + { + SelectedTexture->SetForceMipLevelsToBeResident(30.0f); + SelectedTexture->WaitForStreaming(); + } + + // Iterate on the HAC's component + for (USceneComponent* CurrentSceneComp : CookableComponent->GetAttachChildren()) + { + if (!IsValid(CurrentSceneComp) || !CurrentSceneComp->IsA()) + continue; + + // Get the static mesh component + UStaticMeshComponent* SMC = Cast(CurrentSceneComp); + if (!IsValid(SMC)) + continue; + + // Check if the SMC is the Houdini Logo + if (SMC->GetStaticMesh() != HoudiniCOPMesh) + continue; + + UMaterialInstanceConstant* MaterialInstance = + Cast(SMC->GetMaterial(0)); + if (!MaterialInstance) + continue; + + // Apply material instance parameters + FName MatParamName = FName("cop"); + MaterialInstance->SetTextureParameterValueEditorOnly(MatParamName, SelectedTexture); + + MaterialUpdateContext.AddMaterialInstance(MaterialInstance); + } +} + +TSharedRef +FHoudiniAssetEditor::MakeTextureOutputWidget() +{ + auto GetOutputVisibility = [this]() + { + if(!bIsViewingCopHDA) + return EVisibility::Hidden; + + // Only show output selector if we have more than one texture output + if(NumTextureOutputs <= 1) + return EVisibility::Hidden; + + return EVisibility::Visible; + }; + + // Lambda for changing output + auto OnSelChanged = [this](TSharedPtr InNewChoice) + { + if (!InNewChoice.IsValid()) + return; + + FString NewChoiceStr = *InNewChoice.Get(); + for (int32 OutputIdx = 0; OutputIdx < HoudiniCookableBeingEdited->GetNumOutputs(); OutputIdx++) + { + UHoudiniOutput* CurOutput = HoudiniCookableBeingEdited->GetOutputAt(OutputIdx); + if (!IsValid(CurOutput)) + continue; + + if (CurOutput->GetType() != EHoudiniOutputType::Cop) + continue; + + for (auto& HGPO : CurOutput->GetHoudiniGeoPartObjects()) + { + if (HGPO.PartName != NewChoiceStr) + continue; + + SelectedTextureOutput = OutputIdx; + } + + // Update the selected texture output + UpdateTextureOutputOnPreviewMesh(); + } + }; + + TSharedPtr InitiallySelectedOutput = OutputList.Num() > 0 ? OutputList[0] : nullptr; + TSharedRef OutputControl = + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .Padding(2.0f) + .AutoWidth() + [ + SNew(SComboBox>) + .OptionsSource(&OutputList) + .InitiallySelectedItem(InitiallySelectedOutput) + .Visibility_Lambda(GetOutputVisibility) + .OnGenerateWidget_Lambda([](TSharedPtr ChoiceEntry) + { + FText ChoiceEntryText = FText::FromString(*ChoiceEntry); + return SNew(STextBlock) + .Text(ChoiceEntryText) + .ToolTipText(ChoiceEntryText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + }) + .OnSelectionChanged_Lambda([=](TSharedPtr NewChoice, ESelectInfo::Type SelectType) + { + return OnSelChanged(NewChoice); + }) + [ + SNew(STextBlock) + .Text_Lambda([this]() + { + if (OutputList.IsValidIndex(SelectedTextureOutput)) + return FText::FromString(*OutputList[SelectedTextureOutput].Get()); + else + return FText::FromString(FString::FromInt(SelectedTextureOutput)); + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ]; + + + return OutputControl; +} + +void +FHoudiniAssetEditor::UpdateOutputList() +{ + OutputList.Reset(); + for (int32 OutputIdx = 0; OutputIdx < HoudiniCookableBeingEdited->GetNumOutputs(); OutputIdx++) + { + UHoudiniOutput* CurOutput = HoudiniCookableBeingEdited->GetOutputAt(OutputIdx); + if (!IsValid(CurOutput)) + continue; + + // ensure the output is a texture + if (CurOutput->GetType() != EHoudiniOutputType::Cop) + continue; + + // make sure that cop output is valid + if (CurOutput->GetOutputObjects().Num() <= 0) + continue; + + for (auto& HGPO : CurOutput->GetHoudiniGeoPartObjects()) + OutputList.Add(MakeShareable(new FString(HGPO.PartName))); + } + + // Prevent selecting an output that is no longer valid + if (SelectedTextureOutput >= OutputList.Num()) + SelectedTextureOutput = 0; +} +/* +void +FHoudiniAssetEditor::CreateEditorModeManager() +{ + check(ViewportPtr.IsValid()); + TSharedPtr ViewportClient = ViewportPtr->GetViewportClient(); + check(ViewportClient.IsValid()); + PRAGMA_DISABLE_DEPRECATION_WARNINGS + ViewportClient->TakeOwnershipOfModeManager(EditorModeManager); + PRAGMA_ENABLE_DEPRECATION_WARNINGS +} +*/ + +/* +EHoudiniAssetEditorMode::Type +FHoudiniAssetEditor::GetCurrentMode() const +{ + return EHoudiniAssetEditorMode::ViewMode; +} +*/ + +void +FHoudiniAssetEditor::CreateModeToolbarWidgets(FToolBarBuilder& IgnoredBuilder) +{ + FSlimHorizontalToolBarBuilder ToolbarBuilder(ViewportPtr->GetCommandList(), FMultiBoxCustomization::None); + AddToolbarWidget(ToolbarBuilder.MakeWidget()); +} + +FText +FHoudiniAssetEditor::GetViewportCornerText() const +{ + return LOCTEXT("HDA_CornerText", "HDA"); + + /* + switch (GetCurrentMode()) + { + case EHoudiniAssetEditorMode::ViewMode: + return LOCTEXT("ViewMode_CornerText", "View"); + case EHoudiniAssetEditorMode::HDAMode: + return LOCTEXT("HDAMode_CornerText", "Edit HDA"); + case EHoudiniAssetEditorMode::SessionSyncMode: + return LOCTEXT("SessionSyncMode_CornerText", "Session Sync"); + default: + return FText::GetEmpty(); + } + */ +} + + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniAssetEditor.h`: + +```h +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "IDetailsView.h" +#include "SEditorViewport.h" +#include "SSingleObjectDetailsPanel.h" +#include "Toolkits/AssetEditorToolkit.h" +#include "AdvancedPreviewSceneModule.h" + +class FSpawnTabArgs; +class FToolBarBuilder; + +class FHoudiniAssetEditor; +class SHoudiniAssetEditorViewport; +class SHoudiniNodeSyncPanel; +class UHoudiniAsset; +class UHoudiniCookable; + +/* +namespace EHoudiniAssetEditorMode +{ + enum Type + { + ViewMode, + HDAMode, + SessionSyncMode + }; +} +*/ + +enum class ETextureChannelButton : uint8 +{ + Red, + Green, + Blue, + Alpha +}; + +//----------------------------------------------------------------------------- +// SHoudiniAssetEditorDetailsPanel +//----------------------------------------------------------------------------- +class SHoudiniAssetEditorDetailsPanel : public SSingleObjectDetailsPanel +{ +public: + SLATE_BEGIN_ARGS(SHoudiniAssetEditorDetailsPanel) {} + SLATE_END_ARGS() + +public: + + void Construct(const FArguments& InArgs, TSharedPtr InHoudiniAssetEditor); + + // SSingleObjectDetailsPanel interface + virtual UObject* GetObjectToObserve() const override; + virtual TSharedRef PopulateSlot(TSharedRef PropertyEditorWidget) override; + void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override; + +private: + + // Pointer back to our owning Houdini Asset editor instance + TWeakPtr HoudiniAssetEditorPtr; + + // Cached object view + TWeakObjectPtr MyLastObservedObject; +}; + + +//----------------------------------------------------------------------------- +// FHoudiniAssetEditor +//----------------------------------------------------------------------------- +class FHoudiniAssetEditor : public FAssetEditorToolkit, public FGCObject +{ +public: + FHoudiniAssetEditor(); + + // IToolkit interface + virtual void RegisterTabSpawners(const TSharedRef& TabManager) override; + virtual void UnregisterTabSpawners(const TSharedRef& TabManager) override; + + + // FAssetEditorToolkit + virtual FName GetToolkitFName() const override; + virtual FText GetBaseToolkitName() const override; + virtual FText GetToolkitName() const override; + virtual FText GetToolkitToolTipText() const override; + virtual FLinearColor GetWorldCentricTabColorScale() const override; + virtual FString GetWorldCentricTabPrefix() const override; + virtual FString GetDocumentationLink() const override; + virtual void OnToolkitHostingStarted(const TSharedRef& Toolkit) override; + virtual void OnToolkitHostingFinished(const TSharedRef& Toolkit) override; + + // FSerializableObject interface + virtual void AddReferencedObjects(FReferenceCollector& Collector) override; + virtual FString GetReferencerName() const override { return TEXT("FHoudiniAssetEditor"); } + + + // Init the editor + void InitHoudiniAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, class UHoudiniAsset* InitHDA); + + // On Editor Close + virtual void OnClose() override; + + // HDA / Cookable accessors + UHoudiniAsset* GetHoudiniAssetBeingEdited() const { return HoudiniAssetBeingEdited; } + UHoudiniCookable* GetHoudiniCookableBeingEdited() { return HoudiniCookableBeingEdited; } + + // Return the editor's identifer used to update details + FString GetHoudiniAssetEditorIdentifier() const { return HoudiniAssetEditorIdentifier; }; + + //EHoudiniAssetEditorMode::Type GetCurrentMode() const; + +protected: + + // UI extensions + void BindCommands(); + void ExtendMenu(); + void ExtendToolbar(); + + void FillToolbar(FToolBarBuilder& ToolbarBuilder); + + void CreateModeToolbarWidgets(FToolBarBuilder& ToolbarBuilder); + + TSharedRef SpawnViewportTab(const FSpawnTabArgs& Args); + TSharedRef SpawnDetailsTab(const FSpawnTabArgs& Args); + TSharedRef SpawnNodeSyncTab(const FSpawnTabArgs& Args); + TSharedRef SpawnPreviewSceneSettingsTab(const FSpawnTabArgs& Args); + + FText GetViewportCornerText() const; + + // 2D viewport RGBA channel control + TSharedRef MakeChannelControlWidget(); + FSlateColor GetChannelButtonBackgroundColor(ETextureChannelButton Button) const; + FSlateColor GetChannelButtonForegroundColor(ETextureChannelButton Button) const; + ECheckBoxState OnGetChannelButtonCheckState(ETextureChannelButton Button) const; + + void UpdateColorChannelsOnPreviewMesh(); + + // 2D Viewport output selector + TSharedRef MakeTextureOutputWidget(); + + void UpdateOutputList(); + void UpdateTextureOutputOnPreviewMesh(); + + void OnPostOutputProcess(UHoudiniCookable* _HC, bool bSuccess); + + //virtual void CreateEditorModeManager() override; + +protected: + + // The current HDA being edited + TObjectPtr HoudiniAssetBeingEdited; + + // The cookable editing the above HDA + TObjectPtr HoudiniCookableBeingEdited; + + // Main UI elements pointers + + // Viewport + TSharedPtr ViewportPtr; + // Details panel + TSharedPtr DetailsTabPtr; + // Node Sync panel + TSharedPtr NodeSyncPanel; + // Scene preview settings widget + TSharedPtr AdvancedPreviewSettingsWidget; + // The tab that the preview scene settings widget goes in + TWeakPtr PreviewSceneDockTab; + // The extender to pass to the level editor to extend it's File menu. + TSharedPtr MainMenuExtender; + + // The editor's identifer used to update its details panel + // This needs to be set on the Cookable, and registered with FHoudiniEngine + FString HoudiniAssetEditorIdentifier; + + FAdvancedPreviewSceneModule::FOnPreviewSceneChanged OnPreviewSceneChangedDelegate; + + // Are we editing a COP HDA (texture outputs only?) + bool bIsViewingCopHDA; + + // Channel filters for textures + bool bShowRedChannel; + bool bShowGreenChannel; + bool bShowBlueChannel; + bool bShowAlphaChannel; + + int32 SelectedTextureOutput; + int32 NumTextureOutputs; + TArray> OutputList; +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniAssetEditorViewportClient.cpp`: + +```cpp +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniAssetEditorViewportClient.h" + +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAsset.h" +#include "SHoudiniAssetEditorViewport.h" +#include "HoudiniHandleComponent.h" +#include "HoudiniSplineComponent.h" + +#include "ActorFactories/ActorFactory.h" +#if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 3) + #include "Editor/ActorPositioning.h" +#endif +#include "ComponentVisualizer.h" +#include "Editor/AdvancedPreviewScene/Public/AdvancedPreviewSceneModule.h" +#include "Editor/UnrealEd/Public/UnrealWidget.h" +#include "Editor/UnrealEdEngine.h" +#include "Runtime/Engine/Classes/Components/PostProcessComponent.h" +#include "Runtime/Engine/Classes/Engine/PostProcessVolume.h" +#include "Runtime/Engine/Public/SceneView.h" +#include "SnappingUtils.h" +#include "UnrealEdGlobals.h" +#include "UObject/UObjectIterator.h" + + + +FHoudiniAssetEditorViewportClient::FHoudiniAssetEditorViewportClient( + const TSharedRef& InHoudiniAssetEditorViewport, + const TSharedRef& InPreviewScene) + : FEditorViewportClient( + nullptr, + &InPreviewScene.Get(), + StaticCastSharedRef(InHoudiniAssetEditorViewport)) + , ViewportPtr(InHoudiniAssetEditorViewport) +{ + AdvancedPreviewScene = static_cast(PreviewScene); + + // Enable RealTime + SetRealtime(true); + + // Hide grid, we don't need this. + DrawHelper.bDrawGrid = false; + DrawHelper.bDrawPivot = false; + DrawHelper.AxesLineThickness = 5; + DrawHelper.PivotSize = 5; + + //Initiate view + SetViewLocation(FVector(400, 400, 400)); + SetViewRotation(FVector(-75, -75, -75).Rotation()); + + EngineShowFlags.SetScreenPercentage(true); + + // Set the Default type to Ortho and the XZ Plane + ELevelViewportType NewViewportType = LVT_Perspective; + SetViewportType(NewViewportType); + + // View Modes in Persp and Ortho + SetViewModes(VMI_Lit, VMI_Lit); + + // Add a PostProcess Component to the scene that will be controlled + // by the scene settings + PostProcessComponent = NewObject(); + //PostProcessComponent->Settings = Profile.PostProcessingSettings; + PostProcessComponent->bUnbound = true; + PreviewScene->AddComponent(PostProcessComponent, FTransform(), false); + + //Allow post process materials... + EngineShowFlags.SetPostProcessMaterial(true); + EngineShowFlags.SetPostProcessing(true); + + ToggleOrbitCamera(true); + + bIs2DViewport = false; +} + +void +FHoudiniAssetEditorViewportClient::Tick(float DeltaSeconds) +{ + FEditorViewportClient::Tick(DeltaSeconds); + + // Tick the preview scene world. + if (!GIntraFrameDebuggingGameThread) + { + if(AdvancedPreviewScene) + AdvancedPreviewScene->GetWorld()->Tick(LEVELTICK_All, DeltaSeconds); + } +} + +void +FHoudiniAssetEditorViewportClient::ResetScene() +{ + // TODO ? + // Reset scene settings, view transform etc... ? +} + +void +FHoudiniAssetEditorViewportClient::SetHoudiniAsset(UHoudiniAsset* InAsset) +{ + // Get the HAA asset Factory + UActorFactory* Factory = GEditor->FindActorFactoryForActorClass(AHoudiniAssetActor::StaticClass()); + if (!Factory) + return; + + // Load the asset + UObject* AssetObj = Cast(InAsset); + if (!AssetObj) + return; + + // Set the Actor transform so assets properly face forward in UE + FTransform FaceForward = FTransform::Identity; + FaceForward.SetRotation(FQuat(FVector(0.0f, 0.0f, 1.0f), -UE_HALF_PI)); + + // Create the actor for the HDA + AActor* CreatedActor = Factory->CreateActor(AssetObj, GetWorld()->GetCurrentLevel(), FaceForward); + if (!CreatedActor) + return; + + HoudiniAssetActor = Cast(CreatedActor); + if (!HoudiniAssetActor) + return; + +} + + +void +FHoudiniAssetEditorViewportClient::Draw(const FSceneView* View, FPrimitiveDrawInterface* PDI) +{ + FEditorViewportClient::Draw(View, PDI); + if (GUnrealEd == nullptr) + return; + /* + if (GUnrealEd != NULL && !IsInGameView()) + { + GUnrealEd->DrawComponentVisualizers(View, PDI); + } + */ + + // Visualize Houdini splines + TSharedPtr SplineVisualizer = GUnrealEd->FindComponentVisualizer(UHoudiniSplineComponent::StaticClass()); + if (SplineVisualizer.IsValid()) + { + for (TObjectIterator Itr; Itr; ++Itr) + { + if (Itr->GetOwner() && Itr->GetOwner() == HoudiniAssetActor) + { + const UActorComponent* Comp = Cast(*Itr); + if (Comp != nullptr && Comp->IsRegistered()) + { + SplineVisualizer->DrawVisualization(Comp, View, PDI); + } + } + } + } + + // Visualize Houdini Handles + TSharedPtr HandleVisualizer = GUnrealEd->FindComponentVisualizer(UHoudiniHandleComponent::StaticClass()); + if (HandleVisualizer.IsValid()) + { + for (TObjectIterator Itr; Itr; ++Itr) + { + if (Itr->GetOwner() && Itr->GetOwner() == HoudiniAssetActor) + { + const UActorComponent* Comp = Cast(*Itr); + if (Comp != nullptr && Comp->IsRegistered()) + { + HandleVisualizer->DrawVisualization(Comp, View, PDI); + } + } + } + } +} + + +void +FHoudiniAssetEditorViewportClient::ProcessClick(FSceneView& View, HHitProxy* HitProxy, FKey Key, EInputEvent Event, uint32 HitX, uint32 HitY) +{ + bool bHandled = false; + + const FViewportClick Click(&View, this, Key, Event, HitX, HitY); + bHandled = GUnrealEd->ComponentVisManager.HandleClick(this, HitProxy, Click); + + /* + HComponentVisProxy* ComponentVisProxy = HitProxyCast(HitProxy); + + TSharedPtr SplineVisualizer = GUnrealEd->FindComponentVisualizer(UHoudiniSplineComponent::StaticClass()); + TSharedPtr HandleVisualizer = GUnrealEd->FindComponentVisualizer(UHoudiniHandleComponent::StaticClass()); + if (HitProxy == nullptr) + { + // If our HitProxy is null - make sure we end editing of current splines/handles + SplineVisualizer->EndEditing(); + HandleVisualizer->EndEditing(); + } + else + { + + + // Forward to our visualizers + bHandled = SplineVisualizer->VisProxyHandleClick(this, ComponentVisProxy, Click); + + if(!bHandled) + bHandled = SplineVisualizer->VisProxyHandleClick(this, ComponentVisProxy, Click); + } + */ + + if (!bHandled) + { + FEditorViewportClient::ProcessClick(View, HitProxy, Key, Event, HitX, HitY); + } +} + +bool +FHoudiniAssetEditorViewportClient::InputWidgetDelta(FViewport* InViewport, EAxisList::Type CurrentAxis, FVector& Drag, FRotator& Rot, FVector& Scale) +{ + bool bHandled = false; + if (GUnrealEd->ComponentVisManager.HandleInputDelta(this, InViewport, Drag, Rot, Scale)) + { + return true; + } + + // Give the current editor mode a chance to use the input first. If it does, don't apply it to anything else. + if (FEditorViewportClient::InputWidgetDelta(InViewport, CurrentAxis, Drag, Rot, Scale)) + { + bHandled = true; + } + + /* + // Forward to our visualizers + TSharedPtr SplineVisualizer = GUnrealEd->FindComponentVisualizer(UHoudiniSplineComponent::StaticClass()); + bHandled = SplineVisualizer->HandleInputDelta(this, Viewport, Drag, Rot, Scale); + + TSharedPtr HandleVisualizer = GUnrealEd->FindComponentVisualizer(UHoudiniHandleComponent::StaticClass()); + if (!bHandled) + bHandled = HandleVisualizer->HandleInputDelta(this, Viewport, Drag, Rot, Scale); + + if (!bHandled) + { + return FEditorViewportClient::InputWidgetDelta(InViewport, CurrentAxis, Drag, Rot, Scale); + } + */ + return bHandled; +} + + +bool +FHoudiniAssetEditorViewportClient::InputKey(const FInputKeyEventArgs& InEventArgs) +{ + if (bDisableInput) + return true; + + const int32 HitX = InEventArgs.Viewport->GetMouseX(); + const int32 HitY = InEventArgs.Viewport->GetMouseY(); + + FInputEventState InputState(InEventArgs.Viewport, InEventArgs.Key, InEventArgs.Event); + + // Compute a view. + FSceneViewFamilyContext ViewFamily(FSceneViewFamily::ConstructionValues( + InEventArgs.Viewport, + GetScene(), + EngineShowFlags) + .SetRealtimeUpdate(IsRealtime())); + + FSceneView* View = CalcSceneView(&ViewFamily); + +#if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 3) + // Compute the click location. + if (InputState.IsMouseButtonEvent() && InputState.IsAnyMouseButtonDown()) + { + const FViewportCursorLocation Cursor(View, this, HitX, HitY); + const FActorPositionTraceResult TraceResult = FActorPositioning::TraceWorldForPositionWithDefault(Cursor, *View); + GEditor->UnsnappedClickLocation = TraceResult.Location; + GEditor->ClickLocation = TraceResult.Location; + GEditor->ClickPlane = FPlane(TraceResult.Location, TraceResult.SurfaceNormal); + + // Snap the new location if snapping is enabled + FSnappingUtils::SnapPointToGrid(GEditor->ClickLocation, FVector::ZeroVector); + } +#endif + + if (GUnrealEd->ComponentVisManager.HandleInputKey(this, InEventArgs.Viewport, InEventArgs.Key, InEventArgs.Event)) + { + return true; + } + /* + bool bHandled = false; + const FKey& Key = InEventArgs.Key; + const EInputEvent& Event = InEventArgs.Event; + const FViewport* InViewport = InEventArgs.Viewport; + + // Forward to our visualizers + TSharedPtr SplineVisualizer = GUnrealEd->FindComponentVisualizer(UHoudiniSplineComponent::StaticClass()); + bHandled = SplineVisualizer->HandleInputKey(this, Viewport, Key, Event); + + TSharedPtr HandleVisualizer = GUnrealEd->FindComponentVisualizer(UHoudiniHandleComponent::StaticClass()); + if (!bHandled) + bHandled = HandleVisualizer->HandleInputKey(this, Viewport, Key, Event); + */ + //if (!bHandled) + return FEditorViewportClient::InputKey(InEventArgs); +} + + +void +FHoudiniAssetEditorViewportClient::SetViewportTo2D() +{ + // Switch viewport to texture + EngineShowFlags.SetScreenPercentage(false); + EngineShowFlags.DisableAdvancedFeatures(); + + // Front view - unlit +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + SetViewportType(LVT_OrthoFront); +#else + SetViewportType(LVT_OrthoXZ); +#endif + SetViewModes(VMI_Unlit, VMI_Unlit); + + ViewTransformOrthographic.SetOrthoZoom(1000.0f); + + // Set the perspective view to a "front" one, this ensures the + // image will be properly visible if the user switches the view + // from front to perspective. + ViewTransformPerspective.SetLocation(FVector(120.0, 0.0, 0.0)); + ViewTransformPerspective.SetRotation(FRotator(0.0, 180.0, 0.0)); + ViewTransformPerspective.SetLookAt(FVector(0.0, 0.0, 0.0)); + + bIs2DViewport = true; + + // Lower the floor to avoid intersection with the image in 3d view + AdvancedPreviewScene->SetFloorOffset(100.0f); +} + +void +FHoudiniAssetEditorViewportClient::SetViewportTo3D() +{ + // Re enable engine showflags + EngineShowFlags.SetScreenPercentage(true); + EngineShowFlags.EnableAdvancedFeatures(); + + // Switch viewport to 3D - lit + SetViewportType(LVT_Perspective); + SetViewModes(VMI_Lit, VMI_Lit); + + // Return to default transform + SetViewLocation(FVector(400, 400, 400)); + SetViewRotation(FVector(-75, -75, -75).Rotation()); + ViewTransformPerspective.SetLookAt(FVector(0, 0, 0)); + ViewTransformOrthographic.SetOrthoZoom(2000.0f); + + bIs2DViewport = false; + + // reset the floor offset + AdvancedPreviewScene->SetFloorOffset(0.0f); +} +``` + +`Source/HoudiniEngineEditor/Private/HoudiniAssetEditorViewportClient.h`: + +```h +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "SHoudiniAssetEditorViewport.h" + +#include "CoreMinimal.h" +#include "Editor/AdvancedPreviewScene/Public/AdvancedPreviewScene.h" +#include "Editor/UnrealEd/Public/EditorViewportClient.h" + + +class FHoudiniAssetEditorViewportClient : public FEditorViewportClient +{ +public: + + //Constructor + FHoudiniAssetEditorViewportClient( + const TSharedRef& InHDAEditorViewport, + const TSharedRef& InPreviewScene); + + // Destructor + + + // Tick + virtual void Tick(float DeltaSeconds) override; + + void ResetScene(); + + void SetHoudiniAsset(UHoudiniAsset* InAsset); + + AHoudiniAssetActor* GetHoudiniAssetActor() { return HoudiniAssetActor; }; + + virtual void Draw(const FSceneView* View, FPrimitiveDrawInterface* PDI) override; + + virtual void ProcessClick(FSceneView& View, HHitProxy* HitProxy, FKey Key, EInputEvent Event, uint32 HitX, uint32 HitY) override; + + virtual bool InputWidgetDelta(FViewport * InViewport, EAxisList::Type CurrentAxis, FVector & Drag, FRotator & Rot, FVector & Scale) override; + + virtual bool InputKey(const FInputKeyEventArgs& EventArgs) override; + + void SetViewportTo2D(); + + void SetViewportTo3D(); + +protected: + + // Pointer back to our Houdini Asset Editor Viewport owner + TWeakPtr ViewportPtr; + + // The preview scene used to display our HDA instead of the "real" editor world scene + FAdvancedPreviewScene* AdvancedPreviewScene; + + // HAA in the scene + UPROPERTY() + AHoudiniAssetActor* HoudiniAssetActor; + + //All registered actor components + UPROPERTY() + TArray ActorComponents; + + UPROPERTY() + TObjectPtr PostProcessComponent; + + bool bIs2DViewport; +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniAssetFactory.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniAssetFactory.h" + +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniAsset.h" +#include "HoudiniToolsPackageAsset.h" +#include "HoudiniToolsEditor.h" +#include "HoudiniEngineUtils.h" +#include "EditorFramework/AssetImportData.h" +#include "Misc/FileHelper.h" +#include "Internationalization/Internationalization.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +UHoudiniAssetFactory::UHoudiniAssetFactory(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) +{ + // This factory is responsible for manufacturing HoudiniEngine assets. + SupportedClass = UHoudiniAsset::StaticClass(); + + // This factory does not manufacture new objects from scratch. + bCreateNew = false; + + // This factory will not open the editor for each new object. + bEditAfterNew = false; + + // This factory will import objects from files. + bEditorImport = true; + + // Factory does not import objects from text. + bText = false; + + // Add supported formats. + Formats.Add(TEXT("otl;Houdini Engine Asset")); + Formats.Add(TEXT("otllc;Houdini Engine Limited Commercial Asset")); + Formats.Add(TEXT("otlnc;Houdini Engine Non-Commercial Asset")); + Formats.Add(TEXT("hda;Houdini Engine Asset")); + Formats.Add(TEXT("hdalc;Houdini Engine Limited Commercial Asset")); + Formats.Add(TEXT("hdanc;Houdini Engine Non-Commercial Asset")); + Formats.Add(TEXT("hdalibrary;Houdini Engine Expanded Asset")); +} + +bool +UHoudiniAssetFactory::DoesSupportClass(UClass * Class) +{ + return Class == SupportedClass; +} + +FText +UHoudiniAssetFactory::GetDisplayName() const +{ + return LOCTEXT("HoudiniAssetFactoryDescription", "Houdini Engine Asset"); +} + +UObject * +UHoudiniAssetFactory::FactoryCreateBinary( + UClass * InClass, UObject* InParent, FName InName, EObjectFlags Flags, + UObject * Context, const TCHAR * Type, const uint8 *& Buffer, + const uint8 * BufferEnd, FFeedbackContext * Warn ) +{ + // Broadcast notification that a new asset is being imported. + GEditor->GetEditorSubsystem()->BroadcastAssetPreImport(this, InClass, InParent, InName, Type); + + // Create a new asset. + UHoudiniAsset * HoudiniAsset = NewObject(InParent, InName, Flags); + + // Create reimport information. + UAssetImportData * AssetImportData = HoudiniAsset->AssetImportData; + if (!AssetImportData) + { + AssetImportData = NewObject(HoudiniAsset, UAssetImportData::StaticClass()); + HoudiniAsset->AssetImportData = AssetImportData; + } + AssetImportData->Update(UFactory::GetCurrentFilename()); + + // Import with the sanitized filename from the AssetImportData + FString SanitizedFileName = AssetImportData->GetSourceData().SourceFiles.Num() > 0 ? AssetImportData->GetSourceData().SourceFiles[0].RelativeFilename : UFactory::GetCurrentFilename(); + HoudiniAsset->CreateAsset(Buffer, BufferEnd, SanitizedFileName); + + // Import optional external data for the HoudiniAsset. + + // We always import external JSON / Image data if it is available. + // The Reimport action has to determine whether it wants to preserve existing data, or use the new data. We won't + // be implementing that policy here. + FHoudiniToolsEditor::ImportExternalHoudiniAssetData(HoudiniAsset); + + // Broadcast notification that the new asset has been imported. + GEditor->GetEditorSubsystem()->BroadcastAssetPostImport(this, HoudiniAsset); + + return HoudiniAsset; +} + +UObject* +UHoudiniAssetFactory::FactoryCreateFile(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, const FString& Filename, const TCHAR* Parms, FFeedbackContext* Warn, bool& bOutOperationCanceled) +{ + // "houdini.hdalibrary" files (expanded hda / hda folder) need a special treatment, + // but ".hda" files can be loaded normally + FString FileExtension = FPaths::GetExtension(Filename); + if (FileExtension.Compare(TEXT("hdalibrary"), ESearchCase::IgnoreCase) != 0) + { + return Super::FactoryCreateFile(InClass, InParent, InName, Flags, Filename, Parms, Warn, bOutOperationCanceled); + } + + // Make sure the file name is sections.list + FString NameOfFile = FPaths::GetBaseFilename(Filename); + if (NameOfFile.Compare(TEXT("houdini"), ESearchCase::IgnoreCase) != 0) + { + HOUDINI_LOG_ERROR(TEXT("Failed to load file '%s'. File is not a valid extended HDA."), *Filename); + return nullptr; + } + + // Make sure that the proper .list file is loaded + FString PathToFile = FPaths::GetPath(Filename); + if (PathToFile.Find(TEXT(".hda")) != (PathToFile.Len() - 4)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to load file '%s'. File is not a valid extended HDA."), *Filename); + return nullptr; + } + + FString NewFilename = PathToFile; + FString NewFileNameNoHDA = FPaths::GetBaseFilename(PathToFile); + FName NewIname = FName(*NewFileNameNoHDA); + FString NewFileExtension = FPaths::GetExtension(NewFilename); + + // load as binary + TArray Data; + if (!FFileHelper::LoadFileToArray(Data, *Filename)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to load file '%s' to array"), *Filename); + return nullptr; + } + + Data.Add(0); + ParseParms(Parms); + const uint8* Ptr = &Data[0]; + + return FactoryCreateBinary(InClass, InParent, NewIname, Flags, nullptr, *NewFileExtension, Ptr, Ptr + Data.Num() - 1, Warn); +} + +bool +UHoudiniAssetFactory::CanReimport(UObject * Obj, TArray< FString > & OutFilenames) +{ + UHoudiniAsset * HoudiniAsset = Cast(Obj); + if (HoudiniAsset) + { + UAssetImportData * AssetImportData = HoudiniAsset->AssetImportData; + if (AssetImportData) + OutFilenames.Add(AssetImportData->GetFirstFilename()); + else + OutFilenames.Add(TEXT("")); + + return true; + } + + return false; +} + +void +UHoudiniAssetFactory::SetReimportPaths(UObject * Obj, const TArray< FString > & NewReimportPaths) +{ + UHoudiniAsset * HoudiniAsset = Cast< UHoudiniAsset >(Obj); + if (HoudiniAsset && (1 == NewReimportPaths.Num())) + HoudiniAsset->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]); +} + +EReimportResult::Type +UHoudiniAssetFactory::Reimport(UObject * Obj) +{ + + UHoudiniAsset * HoudiniAsset = Cast< UHoudiniAsset >(Obj); + if (HoudiniAsset && HoudiniAsset->AssetImportData) + { + // Make sure file is valid and exists. + const FString & Filename = HoudiniAsset->AssetImportData->GetFirstFilename(); + + if (!Filename.Len() || IFileManager::Get().FileSize(*Filename) == INDEX_NONE) + return EReimportResult::Failed; + + // TODO: Preserve existing ToolData after import, if ToolPackage->bImportToolDescription == true. + UHoudiniToolData* PrevToolData = HoudiniAsset->HoudiniToolData; + + if (UFactory::StaticImportObject( + HoudiniAsset->GetClass(), HoudiniAsset->GetOuter(), *HoudiniAsset->GetName(), + RF_Public | RF_Standalone, *Filename, NULL, this)) + { + UHoudiniToolsPackageAsset* ToolPackage = FHoudiniToolsEditor::FindOwningToolsPackage(HoudiniAsset); + + if (IsValid(ToolPackage) && !ToolPackage->bReimportToolsDescription && IsValid(PrevToolData)) + { + // We need to preserve the old data. + UHoudiniToolData* NewToolData = FHoudiniToolsEditor::GetOrCreateHoudiniToolData(HoudiniAsset); + NewToolData->CopyFrom(*PrevToolData); + + NewToolData->UpdateOwningAssetThumbnail(); + + HOUDINI_LOG_WARNING(TEXT("Reimported HoudiniAsset (%s) but ignored external data due to package settings."), *HoudiniAsset->GetName()); + const FString Msg = FString::Format(TEXT("Reimported HoudiniAsset ({0}) but ignored external data (JSON/Icon) due to package settings."), { HoudiniAsset->GetName() }); + FHoudiniEngineUtils::CreateSlateNotification(Msg); + } + + if (HoudiniAsset->GetOuter()) + HoudiniAsset->GetOuter()->MarkPackageDirty(); + else + HoudiniAsset->MarkPackageDirty(); + + return EReimportResult::Succeeded; + } + } + + HOUDINI_LOG_MESSAGE(TEXT("Houdini Asset reimport has failed.")); + return EReimportResult::Failed; +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniAssetFactory.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "EditorReimportHandler.h" +#include "Factories/Factory.h" +#include "HoudiniAssetFactory.generated.h" + +class UClass; +class UObject; +class FFeedbackContext; + +UCLASS(config = Editor) +class UHoudiniAssetFactory : public UFactory, public FReimportHandler +{ + GENERATED_UCLASS_BODY() + + public: + + // UFactory methods. + // return true if it supports this class + virtual bool DoesSupportClass(UClass * Class) override; + + // Returns the name of the factory for menus + virtual FText GetDisplayName() const override; + + // Create a new object by importing it from a binary buffer. + virtual UObject * FactoryCreateBinary( + UClass * InClass, UObject * InParent, FName InName, EObjectFlags Flags, + UObject * Context, const TCHAR * Type, const uint8 *& Buffer, const uint8 * BufferEnd, + FFeedbackContext * Warn) override; + + // Create a new object by importing it from a file name. + virtual UObject* FactoryCreateFile(UClass* InClass, UObject* InParent, FName InName, + EObjectFlags Flags, const FString& Filename, const TCHAR* Parms, + FFeedbackContext* Warn, bool& bOutOperationCanceled) override; + + // FReimportHandler methods. + // Check to see if we have a handler to manage the reimporting of the object + virtual bool CanReimport(UObject * Obj, TArray< FString > & OutFilenames) override; + + // Sets the reimport path(s) for the specified object + virtual void SetReimportPaths(UObject * Obj, const TArray< FString > & NewReimportPaths) override; + + // Attempt to reimport the specified object from its source + virtual EReimportResult::Type Reimport(UObject * Obj) override; +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniCookableDetails.cpp`: + +```cpp +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniCookableDetails.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniParameter.h" +#include "HoudiniHandleComponent.h" +#include "HoudiniParameterDetails.h" +#include "HoudiniInput.h" +#include "HoudiniInputDetails.h" +#include "HoudiniHandleDetails.h" +#include "HoudiniNodeSyncComponent.h" +#include "HoudiniOutput.h" +#include "HoudiniOutputDetails.h" +#include "SHoudiniPresets.h" + +#include "Chaos/AABB.h" +#include "DetailCategoryBuilder.h" +#include "DetailLayoutBuilder.h" +#include "Engine/StaticMesh.h" +#include "FoliageType_InstancedStaticMesh.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "IDetailGroup.h" +#include "PhysicalMaterials/PhysicalMaterial.h" +#include "PropertyCustomizationHelpers.h" +#include "SAssetDropTarget.h" +#include "Templates/SharedPointer.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Input/SNumericEntryBox.h" +#include "Widgets/Input/SSpinBox.h" +#include "Widgets/Input/SVectorInputBox.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Images/SLayeredImage.h" +#include "Widgets/Layout/SSeparator.h" +#include "Widgets/Layout/SBox.h" +#include "UObject/ObjectMacros.h" + +#include "HoudiniEngineEditorPrivatePCH.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + + +TSharedRef +FHoudiniCookableDetails::MakeInstance() +{ + return MakeShareable(new FHoudiniCookableDetails); +} + +FHoudiniCookableDetails::FHoudiniCookableDetails() +{ + OutputDetails = MakeShared(); + ParameterDetails = MakeShared(); + PDGDetails = MakeShared(); + HoudiniEngineDetails = MakeShared(); +} + +void +FHoudiniCookableDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + // Get all components which are being customized. + TArray> ObjectsCustomized; + DetailBuilder.GetObjectsBeingCustomized(ObjectsCustomized); + + // Components which are being customized. + TArray> HoudiniCookable; + + // Extract the Houdini Asset Component to detail + for (int32 i = 0; i < ObjectsCustomized.Num(); ++i) + { + if (!IsValidWeakPointer(ObjectsCustomized[i])) + continue; + + UObject * Object = ObjectsCustomized[i].Get(); + if (Object) + { + UHoudiniCookable* HC = Cast(Object); + if (IsValid(HC)) + { + HoudiniCookable.Add(HC); + continue; + } + + UHoudiniAssetComponent * HAC = Cast(Object); + if (IsValid(HAC)) + { + HC = HAC->GetCookable(); + if (IsValid(HC)) + HoudiniCookable.Add(HC); + + continue; + } + + AHoudiniAssetActor* HAA = Cast(Object); + if (IsValid(HAA)) + { + HC = HAA->GetHoudiniCookable(); + if(IsValid(HC)) + HoudiniCookable.Add(HC); + + continue; + } + } + } + + // Check if we'll need to add indie license labels + bool bIsIndieLicense = FHoudiniEngine::Get().IsLicenseIndie(); + bool bIsEduLicense = FHoudiniEngine::Get().IsLicenseEducation(); + + // To handle multiselection parameter edit, we try to group the selected components by their houdini assets + // TODO? ignore multiselection if all are not the same HDA? + TMap, TArray>> HoudiniAssetToCookables; + for (auto currentHC : HoudiniCookable) + { + // Add cookable with no assets + if (!currentHC->IsHoudiniAssetSupported()) + { + TArray>& ValueRef = HoudiniAssetToCookables.FindOrAdd(nullptr); + ValueRef.Add(currentHC); + continue; + } + + TWeakObjectPtr HoudiniAsset = currentHC->GetHoudiniAsset(); + TArray>& ValueRef = HoudiniAssetToCookables.FindOrAdd(HoudiniAsset); + ValueRef.Add(currentHC); + } + + for (auto Iter : HoudiniAssetToCookables) + { + TArray> HCs = Iter.Value; + if (HCs.Num() < 1) + continue; + + TWeakObjectPtr MainCookable = HCs[0]; + if (!IsValidWeakPointer(MainCookable)) + continue; + + // If we have selected more than one component that have different HDAs, + // we'll want to separate the param/input/output category for each HDA + FString MultiSelectionIdentifier = FString(); + if (HoudiniAssetToCookables.Num() > 1) + { + MultiSelectionIdentifier = TEXT("("); + if (MainCookable->GetHoudiniAsset()) + MultiSelectionIdentifier += MainCookable->GetHoudiniAssetName(); + MultiSelectionIdentifier += TEXT(")"); + } + + // + // HOUDINI ENGINE DETAILS + // + bool bIsAssetEditor = false; + if (!MainCookable->AssetEditorId.IsNone()) + bIsAssetEditor = true; + + EHoudiniDetailsFlags Flags = EHoudiniDetailsFlags::Defaults; + if (bIsAssetEditor) + { + Flags.bRemoveHDAOutputAfterBake = false; + Flags.bAutoBake = false; + Flags.bTemporaryCookFolderRow = false; + Flags.bCookTriggers = false; + Flags.bDoNotGenerateOutputs = false; + Flags.bPushTransformToHoudini = false; + } + + CreateHoudiniEngineDetails(DetailBuilder, HCs, MultiSelectionIdentifier, Flags); + + + // + // HOUDINI ASSET DETAILS + // + if (MainCookable->IsHoudiniAssetSupported()) + { + CreateHoudiniAssetDetails(DetailBuilder, HCs); + } + + + // + // NODE SYNC DETAILS + // + bool bIsNodeSyncComponent = MainCookable->GetComponent() ? MainCookable->GetComponent()->IsA() : false; + if (bIsNodeSyncComponent) + { + CreateNodeSyncDetails(DetailBuilder, HCs, MultiSelectionIdentifier); + } + + // + // PDG ASSET LINK (if available) + // + if (MainCookable->IsPDGSupported()) + { + CreatePDGDetails(DetailBuilder, HCs, MultiSelectionIdentifier); + } + + // + // PARAMETER DETAILS + // + if (MainCookable->IsParameterSupported()) + { + CreateParameterDetails(DetailBuilder, HCs, MultiSelectionIdentifier); + } + + // + // HANDLE DETAILS + // + if (MainCookable->IsComponentSupported()) + { + CreateHandleDetails(DetailBuilder, HCs, MultiSelectionIdentifier); + } + + // + // INPUT DETAILS + // + if (MainCookable->IsInputSupported()) + { + CreateInputDetails(DetailBuilder, HCs, MultiSelectionIdentifier); + } + + // + // OUTPUT DETAILS + // + if (MainCookable->IsOutputSupported()) + { + CreateOutputDetails(DetailBuilder, HCs, MultiSelectionIdentifier); + } + + // + // MESH CONVERSION OPTIONS + // + if(MainCookable->IsOutputSupported()) + { + CreateMeshConversonSettings(DetailBuilder, HCs); + } + + // + // PROXY SETTINGS + // + if (MainCookable->IsProxySupported()) + { + CreateProxyDetails(DetailBuilder, HCs); + } + + // + // MESH BUILD SETTINGS + // MESH GENERATION PROPERTIES + // + if (MainCookable->IsOutputSupported()) + { + CreateMeshBuildSettingsDetails(DetailBuilder, HCs); + CreateMeshGenerationDetails(DetailBuilder, HCs); + } + } +} + +void +FHoudiniCookableDetails::CreateHoudiniEngineDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier, + const EHoudiniDetailsFlags& DetailsFlags) +{ + FString HoudiniEngineCategoryName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_MAIN); + HoudiniEngineCategoryName += MultiSelectionIdentifier; + + TSharedPtr OptionsImage = SNew(SLayeredImage) + .Image(FAppStyle::Get().GetBrush("DetailsView.ViewOptions")) + .ColorAndOpacity(FSlateColor::UseForeground()); + + // Create Houdini Engine details category + IDetailCategoryBuilder& HouEngineCategory = + DetailBuilder.EditCategory(*HoudiniEngineCategoryName, FText::FromString("Houdini Engine"), ECategoryPriority::Important); + + // If we are running Houdini Engine Indie license, we need to display a special label. + // Check if we'll need to add indie license labels + bool bIsIndieLicense = FHoudiniEngine::Get().IsLicenseIndie(); + bool bIsEduLicense = FHoudiniEngine::Get().IsLicenseEducation(); + if (bIsIndieLicense) + FHoudiniEngineDetails::AddIndieLicenseRow(HouEngineCategory); + else if (bIsEduLicense) + FHoudiniEngineDetails::AddEducationLicenseRow(HouEngineCategory); + + // Houdini Engine Icon + HoudiniEngineDetails->CreateHoudiniEngineIconWidget(HouEngineCategory); + + // Widget for HoudiniAsset related actions. Currently only contains things for Presets. + //HoudiniEngineDetails->CreateHoudiniEngineActionWidget(HouEngineCategory, InCookables); + + // Houdini Engine Session Status + HoudiniEngineDetails->AddSessionStatusRow(HouEngineCategory); + + // Create Generate Category. If not, just so "Reset Parameters" button. + if(DetailsFlags.bGenerateBar) + { + HoudiniEngineDetails->CreateGenerateWidgets(HouEngineCategory, InCookables, DetailsFlags); + } + else + { + HoudiniEngineDetails->CreateResetParametersOnlyWidgets(HouEngineCategory, InCookables); + } + + // Create Bake Category + HoudiniEngineDetails->CreateBakeWidgets(HouEngineCategory, InCookables, DetailsFlags); + + // Create Asset Options Category + if (DetailsFlags.bAssetOptions) + HoudiniEngineDetails->CreateAssetOptionsWidgets(HouEngineCategory, InCookables, DetailsFlags); + + // Create Help and Debug Category + HoudiniEngineDetails->CreateHelpAndDebugWidgets(HouEngineCategory, InCookables); +} + +void +FHoudiniCookableDetails::CreateHoudiniAssetDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables) +{ + if (InCookables.IsEmpty()) + return; + + // Create the HDA details category + FString AssetCatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_HDA); + IDetailCategoryBuilder& HouAssetCategory = + DetailBuilder.EditCategory(*AssetCatName, FText::GetEmpty(), ECategoryPriority::Important); + + HoudiniEngineDetails->CreateHoudiniAssetDetails(HouAssetCategory, InCookables); +} + + +void +FHoudiniCookableDetails::CreateNodeSyncDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier) +{ + // If we are working on a node sync component, display its specific options + FString HoudiniNodeSyncCategoryName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_NODESYNC); + HoudiniNodeSyncCategoryName += MultiSelectionIdentifier; + + // Create Houdini Engine details category + IDetailCategoryBuilder& HouNodeSyncCategory = + DetailBuilder.EditCategory(*HoudiniNodeSyncCategoryName, FText::FromString("Houdini - Node Sync"), ECategoryPriority::Important); + HoudiniEngineDetails->CreateNodeSyncWidgets(HouNodeSyncCategory, InCookables); +} + +void +FHoudiniCookableDetails::CreatePDGDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier) +{ + TWeakObjectPtr MainCookable = InCookables[0]; + if (!IsValidWeakPointer(MainCookable)) + return; + + UHoudiniPDGAssetLink* HPDGAL = MainCookable->GetPDGAssetLink(); + if (!HPDGAL) + return; + + FString PDGCatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_PDG); + PDGCatName += MultiSelectionIdentifier; + + // Create the PDG Asset Link details category + IDetailCategoryBuilder& HouPDGCategory = + DetailBuilder.EditCategory(*PDGCatName, FText::FromString("Houdini - PDG Asset Link"), ECategoryPriority::Important); + + // If we are running Houdini Engine Indie license, we need to display a special label. + // Check if we'll need to add indie license labels + bool bIsIndieLicense = FHoudiniEngine::Get().IsLicenseIndie(); + bool bIsEduLicense = FHoudiniEngine::Get().IsLicenseEducation(); + if (bIsIndieLicense) + FHoudiniEngineDetails::AddIndieLicenseRow(HouPDGCategory); + else if (bIsEduLicense) + FHoudiniEngineDetails::AddEducationLicenseRow(HouPDGCategory); + + // TODO: Handle multi selection ? + PDGDetails->CreateWidget(HouPDGCategory, HPDGAL, MainCookable->GetIsPCG()); +} + +void +FHoudiniCookableDetails::CreateParameterDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier) +{ + if(InCookables.IsEmpty()) + return; + + TWeakObjectPtr MainCookable = InCookables[0]; + if (!IsValidWeakPointer(MainCookable)) + return; + + if (MainCookable->GetNumParameters() <= 0) + return; + + // If we have selected more than one component that have different HDAs, + // we need to create multiple categories one for each different HDA + FString ParamCatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_PARAMS); + ParamCatName += MultiSelectionIdentifier; + + // Create the Parameters details category + IDetailCategoryBuilder& HouParameterCategory = + DetailBuilder.EditCategory(*ParamCatName, FText::GetEmpty(), ECategoryPriority::Important); + + // If we are running Houdini Engine Indie license, we need to display a special label. + bool bIsIndieLicense = FHoudiniEngine::Get().IsLicenseIndie(); + bool bIsEduLicense = FHoudiniEngine::Get().IsLicenseEducation(); + if (bIsIndieLicense) + FHoudiniEngineDetails::AddIndieLicenseRow(HouParameterCategory); + else if (bIsEduLicense) + FHoudiniEngineDetails::AddEducationLicenseRow(HouParameterCategory); + + // Iterate through the component's parameters. JoinedParams is used to build an array of + // horizontally joined parameters. For example, with two joined parameters JoinedParams + // will look like { { cookable.param1}, { cookable.param2 } } and then call CreateWidget(). + // + // If the parameters are not joined, CreateWidget() will be called twice with two arrays + // + // { { cookable.param1} } and { {cookable.param2 } } + // + // In addition, each linked parameter will be stored in the inner array. eg. + // + // { { cookable.param1, linked.param1 }, { cookable.param2, linked.param2 } } + // + + TArray>> JoinedParams; + for (int32 ParamIdx = 0; ParamIdx < MainCookable->GetNumParameters(); ParamIdx++) + { + // We only want to create root parameters here, they will recursively create child parameters. + UHoudiniParameter* CurrentParam = MainCookable->GetParameterAt(ParamIdx); + if (!IsValid(CurrentParam)) + continue; + + // Build an array of edited parameter for multi edit + JoinedParams.Emplace(); + auto& EditedParams = JoinedParams.Last(); + EditedParams.Add(CurrentParam); + + // Add the corresponding params in the other HAC. Note that the parameters must be in the same + // order for this to work. + + for (int LinkedIdx = 1; LinkedIdx < InCookables.Num(); LinkedIdx++) + { + UHoudiniParameter* LinkedParam = InCookables[LinkedIdx]->GetParameterAt(ParamIdx); + if (!IsValid(LinkedParam)) + continue; + + // Linked params should match the main param! If not try to find one that matches + if (!LinkedParam->Matches(*CurrentParam)) + { + LinkedParam = MainCookable->FindMatchingParameter(CurrentParam); + if (!IsValid(LinkedParam) || LinkedParam->IsChildParameter()) + continue; + } + + EditedParams.Add(LinkedParam); + } + + if (!ParameterDetails->ShouldJoinNext(*CurrentParam)) + { + // If we are not joining the parameter to the next parameter, create the widget now + // using the contents of JointedParams and then reset the array for the next loop. + // Note that the last parameter never has the "Joined to Next" flag set. + ParameterDetails->CreateWidget(HouParameterCategory, JoinedParams); + JoinedParams.Empty(); + } + } +} + +void +FHoudiniCookableDetails::CreateHandleDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier) +{ + TWeakObjectPtr MainCookable = InCookables[0]; + if (!IsValidWeakPointer(MainCookable)) + return; + + if (MainCookable->GetNumHandles() <= 0) + return; + + // If we have selected more than one component that have different HDAs, + // we need to create multiple categories one for each different HDA + FString HandleCatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_HANDLES); + HandleCatName += MultiSelectionIdentifier; + + // Create the Parameters details category + IDetailCategoryBuilder& HouHandleCategory = + DetailBuilder.EditCategory(*HandleCatName, FText::GetEmpty(), ECategoryPriority::Important); + + // If we are running Houdini Engine Indie license, we need to display a special label. + bool bIsIndieLicense = FHoudiniEngine::Get().IsLicenseIndie(); + bool bIsEduLicense = FHoudiniEngine::Get().IsLicenseEducation(); + if (bIsIndieLicense) + FHoudiniEngineDetails::AddIndieLicenseRow(HouHandleCategory); + else if (bIsEduLicense) + FHoudiniEngineDetails::AddEducationLicenseRow(HouHandleCategory); + + // Iterate through the component's Houdini handles + for (int32 HandleIdx = 0; HandleIdx < MainCookable->GetNumHandles(); ++HandleIdx) + { + UHoudiniHandleComponent* CurrentHandleComponent = MainCookable->GetHandleComponentAt(HandleIdx); + if (!IsValid(CurrentHandleComponent)) + continue; + + TArray> EditedHandles; + EditedHandles.Add(CurrentHandleComponent); + + // Add the corresponding params in the other HAC + for (int LinkedIdx = 1; LinkedIdx < InCookables.Num(); ++LinkedIdx) + { + UHoudiniHandleComponent* LinkedHandle = InCookables[LinkedIdx]->GetHandleComponentAt(HandleIdx); + if (!IsValid(LinkedHandle)) + continue; + + // Linked handles should match the main param, if not try to find one that matches + if (!LinkedHandle->Matches(*CurrentHandleComponent)) + { + LinkedHandle = MainCookable->FindMatchingHandle(CurrentHandleComponent); + if (!IsValid(LinkedHandle)) + continue; + } + + EditedHandles.Add(LinkedHandle); + } + + FHoudiniHandleDetails::CreateWidget(HouHandleCategory, EditedHandles); + } +} + +void +FHoudiniCookableDetails::CreateInputDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier) +{ + if(InCookables.IsEmpty()) + return; + + TWeakObjectPtr MainCookable = InCookables[0]; + if (!IsValidWeakPointer(MainCookable)) + return; + + if (MainCookable->GetNumInputs() <= 0) + return; + + // If we have selected more than one component that have different HDAs, + // we need to create multiple categories one for each different HDA + FString InputCatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_INPUTS); + InputCatName += MultiSelectionIdentifier; + + // Create the input details category + IDetailCategoryBuilder& HouInputCategory = + DetailBuilder.EditCategory(*InputCatName, FText::GetEmpty(), ECategoryPriority::Important); + + // If we are running Houdini Engine Indie license, we need to display a special label. + bool bIsIndieLicense = FHoudiniEngine::Get().IsLicenseIndie(); + bool bIsEduLicense = FHoudiniEngine::Get().IsLicenseEducation(); + if (bIsIndieLicense) + FHoudiniEngineDetails::AddIndieLicenseRow(HouInputCategory); + else if (bIsEduLicense) + FHoudiniEngineDetails::AddEducationLicenseRow(HouInputCategory); + + // Iterate through the component's inputs + for (int32 InputIdx = 0; InputIdx < MainCookable->GetNumInputs(); InputIdx++) + { + UHoudiniInput* CurrentInput = MainCookable->GetInputAt(InputIdx); + if (!IsValid(CurrentInput)) + continue; + + // TODO COOKABLE: ?? handle needed ? this is mostly for BP + if (!MainCookable->IsInputTypeSupported(CurrentInput->GetInputType())) + continue; + + // Object path parameter inputs are displayed by the ParameterDetails - skip them + if (CurrentInput->IsObjectPathParameter()) + continue; + + // Build an array of edited inputs for multi edit + TArray> EditedInputs; + EditedInputs.Add(CurrentInput); + + // Add the corresponding inputs in the other HAC + for (int LinkedIdx = 1; LinkedIdx < InCookables.Num(); LinkedIdx++) + { + UHoudiniInput* LinkedInput = InCookables[LinkedIdx]->GetInputAt(InputIdx); + if (!IsValid(LinkedInput)) + continue; + + // Linked params should match the main param! If not try to find one that matches + if (!LinkedInput->Matches(*CurrentInput)) + { + LinkedInput = MainCookable->FindMatchingInput(CurrentInput); + if (!IsValid(LinkedInput)) + continue; + } + + EditedInputs.Add(LinkedInput); + } + + FHoudiniInputDetails::CreateWidget(HouInputCategory, EditedInputs); + } +} + +void +FHoudiniCookableDetails::CreateOutputDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier) +{ + TWeakObjectPtr MainCookable = InCookables[0]; + if (!IsValidWeakPointer(MainCookable)) + return; + /* + // Still display the output section even if we havent produce any output + if (MainCookable->GetNumOutputs() <= 0) + return; + */ + + // If we have selected more than one component that have different HDAs, + // we need to create multiple categories one for each different HDA + FString OutputCatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_OUTPUTS); + OutputCatName += MultiSelectionIdentifier; + + // Create the output details category + IDetailCategoryBuilder& HouOutputCategory = + DetailBuilder.EditCategory(*OutputCatName, FText::GetEmpty(), ECategoryPriority::Important); + + // Iterate through the component's outputs + for (int32 OutputIdx = 0; OutputIdx < MainCookable->GetNumOutputs(); OutputIdx++) + { + UHoudiniOutput* CurrentOutput = MainCookable->GetOutputAt(OutputIdx); + if (!IsValid(CurrentOutput)) + continue; + + // Build an array of edited outputs for multi edit + TArray> EditedOutputs; + EditedOutputs.Add(CurrentOutput); + + // Add the corresponding outputs in the other HAC + for (int LinkedIdx = 1; LinkedIdx < InCookables.Num(); LinkedIdx++) + { + UHoudiniOutput* LinkedOutput = InCookables[LinkedIdx]->GetOutputAt(OutputIdx); + if (!IsValid(LinkedOutput)) + continue; + + EditedOutputs.Add(LinkedOutput); + } + + // TODO: Handle multi selection of outputs like params/inputs? + OutputDetails->CreateWidget(HouOutputCategory, EditedOutputs); + } +} + + +void +FHoudiniCookableDetails::CreateProxyDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables) +{ + if (InCookables.Num() <= 0) + return; + + TWeakObjectPtr MainCookable = InCookables[0]; + if (!IsValidWeakPointer(MainCookable)) + return; + + if (!MainCookable->IsProxySupported()) + return; + + // Create the Proxy details category + FString ProxyCatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_MESHGEN); + + // If we have selected more than one component that have different HDAs, + // we need to create multiple categories one for each different HDA + // OutputCatName += MultiSelectionIdentifier; + + IDetailCategoryBuilder& HouProxyCategory = + DetailBuilder.EditCategory(*ProxyCatName, FText::GetEmpty(), ECategoryPriority::Important); + + FString Label = TEXT("Houdini Proxy Mesh Settings"); + IDetailGroup& ProxyGrp = HouProxyCategory.AddGroup(FName(*Label), FText::FromString(Label)); + + // Lambda used to trigger a refine of the cookables if necessary + auto RefineCookablesIfNeeded = [InCookables]() + { + TArray ActorsToRefine; + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + AHoudiniAssetActor* CurActor = Cast(CurCookable->GetOwner()); + if (!IsValid(CurActor)) + continue; + + if(!CurCookable->IsProxyStaticMeshEnabled()) + ActorsToRefine.Add(CurActor); + } + + FHoudiniEngineUtils::RefineHoudiniProxyMeshActorArrayToStaticMeshes(ActorsToRefine); + }; + + // + // Override Global Proxy Mesh Setting + // + //HouProxyCategory.AddCustomRow() + ProxyGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Override Global Proxy Mesh Setting")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + if (!IsValidWeakPointer(MainCookable)) + return ECheckBoxState::Unchecked; + + return MainCookable->IsOverrideGlobalProxyStaticMeshSettings() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([MainCookable, InCookables, RefineCookablesIfNeeded](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainCookable)) + return; + + const bool bNewState = (NewState == ECheckBoxState::Checked); + if (MainCookable->IsOverrideGlobalProxyStaticMeshSettings() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniOverrideProxyChange", "Houdini Input: Override Global Proxy Mesh Settings"), + MainCookable->GetOuter()); + + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + if (CurCookable->IsOverrideGlobalProxyStaticMeshSettings() == bNewState) + continue; + + CurCookable->Modify(); + CurCookable->SetOverrideGlobalProxyStaticMeshSettings(bNewState); + // Reset the timer + CurCookable->ClearRefineMeshesTimer(); + // SetRefineMeshesTimer will check the relevant settings and only set the timer if enabled via settings + CurCookable->SetRefineMeshesTimer(); + } + + // Refine if needed + RefineCookablesIfNeeded(); + }) + ] + ]; + + + // Enable Proxy Mesh + ProxyGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Enable Proxy Mesh")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + if (!IsValidWeakPointer(MainCookable)) + return ECheckBoxState::Unchecked; + + return MainCookable->IsProxyStaticMeshEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .IsEnabled_Lambda([MainCookable]() {return MainCookable->IsOverrideGlobalProxyStaticMeshSettings(); }) + .OnCheckStateChanged_Lambda([MainCookable, InCookables, RefineCookablesIfNeeded](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainCookable)) + return; + + const bool bNewState = (NewState == ECheckBoxState::Checked); + if (MainCookable->IsProxyStaticMeshEnabled() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniOverrideProxyEnable", "Houdini: Override Proxy Mesh Enabled"), + MainCookable->GetOuter()); + + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + if (CurCookable->IsProxyStaticMeshEnabled() == bNewState) + continue; + + CurCookable->Modify(); + CurCookable->SetEnableProxyStaticMeshOverride(bNewState); + // Reset the timer + CurCookable->ClearRefineMeshesTimer(); + // SetRefineMeshesTimer will check the relevant settings and only set the timer if enabled via settings + CurCookable->SetRefineMeshesTimer(); + } + + // Refine if needed + RefineCookablesIfNeeded(); + }) + ] + ]; + + // Refine Proxy Meshes after a timeout + ProxyGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Refine Proxy Meshes after a timeout")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + if (!IsValidWeakPointer(MainCookable)) + return ECheckBoxState::Unchecked; + + return MainCookable->GetProxyData()->bEnableProxyStaticMeshRefinementByTimerOverride ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .IsEnabled_Lambda([MainCookable]() {return MainCookable->IsOverrideGlobalProxyStaticMeshSettings(); }) + .OnCheckStateChanged_Lambda([MainCookable, InCookables](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainCookable)) + return; + + const bool bNewState = (NewState == ECheckBoxState::Checked); + if (MainCookable->GetProxyData()->bEnableProxyStaticMeshRefinementByTimerOverride == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniOverrideProxyByTimerEnable", "Houdini: Override Proxy Mesh Refine by Timer"), + MainCookable->GetOuter()); + + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + if (CurCookable->GetProxyData()->bEnableProxyStaticMeshRefinementByTimerOverride == bNewState) + continue; + + CurCookable->Modify(); + CurCookable->SetEnableProxyStaticMeshRefinementByTimerOverride(bNewState); + // Reset the timer + CurCookable->ClearRefineMeshesTimer(); + // SetRefineMeshesTimer will check the relevant settings and only set the timer if enabled via settings + CurCookable->SetRefineMeshesTimer(); + } + }) + ] + ]; + + // + // Proxy Mesh Auto Refine Timeout Seconds + // + + // Lambdas for slider begin + auto SliderBegin = [](const TArray>& Cookables) + { + if (Cookables.Num() == 0) + return; + + if (!IsValidWeakPointer(Cookables[0])) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniProxyMeshTimerChange", "Houdini: Changing Proxy Mesh refinement Timer value"), + Cookables[0]->GetOuter()); + + for (int Idx = 0; Idx < Cookables.Num(); Idx++) + { + if (!IsValidWeakPointer(Cookables[Idx])) + continue; + + Cookables[Idx]->GetProxyData()->Modify(); + } + }; + + // Lambdas for slider end + auto SliderEnd = [](const TArray>& Cookables) + { + // Mark the value as changed to trigger an update + for (int Idx = 0; Idx < Cookables.Num(); Idx++) + { + if (!IsValidWeakPointer(Cookables[Idx])) + continue; + + // TODO: Mark changed or equivalent? + } + }; + + // Lambdas for changing the parameter value + auto ChangeFloatValueAt = [](const float& Value, const bool& DoChange, const TArray>& Cookables) + { + if (Cookables.Num() == 0) + return; + + if (!IsValidWeakPointer(Cookables[0])) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniProxyMeshTimerChange", "Houdini: Changing Proxy Mesh refinement Timer value"), + Cookables[0]->GetOuter() ); + + for (int Idx = 0; Idx < Cookables.Num(); Idx++) + { + if (!IsValidWeakPointer(Cookables[Idx])) + continue; + + Cookables[Idx]->SetProxyMeshAutoRefineTimeoutSecondsOverride(Value); + + if (DoChange) + { + Cookables[Idx]->GetProxyData()->Modify(); + + // Reset the timer + Cookables[Idx]->ClearRefineMeshesTimer(); + // SetRefineMeshesTimer will check the relevant settings and only set the timer if enabled via settings + Cookables[Idx]->SetRefineMeshesTimer(); + } + } + }; + + ProxyGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Proxy Mesh Auto-refine Timeout Seconds")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SNumericEntryBox) + .AllowSpin(true) + + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + + .MinValue(0) + .MaxValue(3600) + + .MinSliderValue(0) + .MaxSliderValue(60) + + .Value_Lambda([MainCookable]() { return MainCookable->GetProxyMeshAutoRefineTimeoutSeconds(); }) + .OnValueChanged_Lambda([InCookables, ChangeFloatValueAt](float Val) + { + ChangeFloatValueAt(Val, false, InCookables); + }) + + .OnValueCommitted_Lambda([InCookables, ChangeFloatValueAt](float Val, ETextCommit::Type TextCommitType) + { + ChangeFloatValueAt(Val, true, InCookables); + }) + .OnBeginSliderMovement_Lambda([InCookables, SliderBegin]() + { + SliderBegin(InCookables); + }) + .OnEndSliderMovement_Lambda([InCookables, SliderEnd](const float NewValue) + { + SliderEnd(InCookables); + }) + .SliderExponent(1.0f) + ] + ] + ]; + + // Refine Proxy Static Mesh when saving a Map + ProxyGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Auto-refine Proxy Meshes when saving a Map")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + if (!IsValidWeakPointer(MainCookable)) + return ECheckBoxState::Unchecked; + + return MainCookable->GetProxyData()->bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .IsEnabled_Lambda([MainCookable]() {return MainCookable->IsOverrideGlobalProxyStaticMeshSettings(); }) + .OnCheckStateChanged_Lambda([MainCookable, InCookables](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainCookable)) + return; + + const bool bNewState = (NewState == ECheckBoxState::Checked); + if (MainCookable->GetProxyData()->bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniOverrideProxyRefineOnMapSave", "Houdini: Override Proxy Mesh Refine on Map Save"), + MainCookable->GetOuter()); + + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + if (CurCookable->GetProxyData()->bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride == bNewState) + continue; + + CurCookable->Modify(); + CurCookable->SetEnableProxyStaticMeshRefinementOnPreSaveWorldOverride(bNewState); + // Reset the timer + CurCookable->ClearRefineMeshesTimer(); + // SetRefineMeshesTimer will check the relevant settings and only set the timer if enabled via settings + CurCookable->SetRefineMeshesTimer(); + } + }) + ] + ]; + + // Refine Proxy Meshes on PIE + ProxyGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Auto-refine Proxy Meshes when Playing-In-Editor.")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + if (!IsValidWeakPointer(MainCookable)) + return ECheckBoxState::Unchecked; + + return MainCookable->GetProxyData()->bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .IsEnabled_Lambda([MainCookable]() {return MainCookable->IsOverrideGlobalProxyStaticMeshSettings(); }) + .OnCheckStateChanged_Lambda([MainCookable, InCookables](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainCookable)) + return; + + const bool bNewState = (NewState == ECheckBoxState::Checked); + if (MainCookable->GetProxyData()->bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniOverrideProxyRefineOnPIE", "Houdini: Override Proxy Mesh Refine on PIE"), + MainCookable->GetOuter()); + + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + if (CurCookable->GetProxyData()->bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride == bNewState) + continue; + + CurCookable->Modify(); + CurCookable->SetEnableProxyStaticMeshRefinementOnPreBeginPIEOverride(bNewState); + // Reset the timer + CurCookable->ClearRefineMeshesTimer(); + // SetRefineMeshesTimer will check the relevant settings and only set the timer if enabled via settings + CurCookable->SetRefineMeshesTimer(); + } + }) + ] + ]; +} + + +void +FHoudiniCookableDetails::CreateMeshConversonSettings( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables) +{ + if(InCookables.Num() <= 0) + return; + + TWeakObjectPtr MainCookable = InCookables[0]; + if(!IsValidWeakPointer(MainCookable)) + return; + + if(!MainCookable->IsOutputSupported()) + return; + + // Create the SM Build Settings category + FString BuildSettingsCatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_MESHGEN); + + // If we have selected more than one component that have different HDAs, + // we need to create multiple categories one for each different HDA + // OutputCatName += MultiSelectionIdentifier; + + IDetailCategoryBuilder& Category = + DetailBuilder.EditCategory(*BuildSettingsCatName, FText::GetEmpty(), ECategoryPriority::Important); + + FString Label = TEXT("Mesh Conversion Options"); +// IDetailGroup& ProxyGrp = Category.AddGroup(FName(*Label), FText::FromString(Label)); + + // Lambda to mark the cookable as changed + auto MarkCookableOutputsNeedUpdate = [](TWeakObjectPtr& InCookable) + { + if(!IsValidWeakPointer(InCookable)) + return; + + // TODO: actually trigger an output update + // Mark all outputs as changed? + //InCookable->NeedUpdateOutputs(); + }; + + + Category.AddCustomRow(FText::FromString("SplitMeshRow")) + .RowTag("SplitMeshSupport") + .NameContent() + [ + SNew(STextBlock) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Text(LOCTEXT("SplitMeshSupport", "Split Mesh Support")) + ] + .ValueContent() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + return MainCookable->GetOutputData()->bSplitMeshSupport ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](ECheckBoxState NewState) + { + bool bNewState = (NewState == ECheckBoxState::Checked) ? true : false; + for(auto CurCookable : InCookables) + { + if(!IsValidWeakPointer(CurCookable) && IsValid(CurCookable->GetOutputData())) + continue; + + bool bIsSet = CurCookable->GetSplitMeshSupport(); + if(bIsSet == bNewState) + continue; + + CurCookable->Modify(); + CurCookable->GetOutputData()->bSplitMeshSupport = bNewState; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; +} + +void +FHoudiniCookableDetails::CreateMeshBuildSettingsDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables) +{ + if (InCookables.Num() <= 0) + return; + + TWeakObjectPtr MainCookable = InCookables[0]; + if (!IsValidWeakPointer(MainCookable)) + return; + + if (!MainCookable->IsOutputSupported()) + return; + + // Create the SM Build Settings category + FString BuildSettingsCatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_MESHGEN); + + // If we have selected more than one component that have different HDAs, + // we need to create multiple categories one for each different HDA + // OutputCatName += MultiSelectionIdentifier; + + IDetailCategoryBuilder& HouMeshGenCategory = + DetailBuilder.EditCategory(*BuildSettingsCatName, FText::GetEmpty(), ECategoryPriority::Important); + + FString Label = TEXT("Static Mesh Build Settings"); + IDetailGroup& ProxyGrp = HouMeshGenCategory.AddGroup(FName(*Label), FText::FromString(Label)); + + + // Lambda to mark the cookable as changed + auto MarkCookableOutputsNeedUpdate = [](TWeakObjectPtr& InCookable) + { + if (!IsValidWeakPointer(InCookable)) + return; + + // TODO: actually trigger an output update + // Mark all outputs as changed? + //InCookable->NeedUpdateOutputs(); + }; + + // COPIED FROM FMeshBuildSettingsLayout + ProxyGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("MeshBuildSettings", "Build Settings")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH); + + + + { + ProxyGrp.AddWidgetRow() + .RowTag("RecomputeNormals") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("RecomputeNormals", "Recompute Normals")) + ] + .ValueContent() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().bRecomputeNormals ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](ECheckBoxState NewState) + { + const uint8 bNewState = (NewState == ECheckBoxState::Checked) ? 1 : 0; + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.bRecomputeNormals == bNewState) + continue; + + CurCookable->Modify(); + SMBS.bRecomputeNormals = bNewState; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + ProxyGrp.AddWidgetRow() + .RowTag("RecomputeTangents") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("RecomputeTangents", "Recompute Tangents")) + ] + .ValueContent() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().bRecomputeTangents ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](ECheckBoxState NewState) + { + const uint8 bNewState = (NewState == ECheckBoxState::Checked) ? 1 : 0; + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.bRecomputeTangents == bNewState) + continue; + CurCookable->Modify(); + SMBS.bRecomputeTangents = bNewState; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + ProxyGrp.AddWidgetRow() + .RowTag("UseMikkTSpace") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("UseMikkTSpace", "Use MikkTSpace Tangent Space")) + ] + .ValueContent() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().bUseMikkTSpace ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](ECheckBoxState NewState) + { + const uint8 bNewState = (NewState == ECheckBoxState::Checked) ? 1 : 0; + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.bUseMikkTSpace == bNewState) + continue; + + CurCookable->Modify(); + SMBS.bUseMikkTSpace = bNewState; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + ProxyGrp.AddWidgetRow() + .RowTag("ComputeWeightedNormals") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("ComputeWeightedNormals", "Compute Weighted Normals")) + ] + .ValueContent() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().bComputeWeightedNormals ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](ECheckBoxState NewState) + { + const uint8 bNewState = (NewState == ECheckBoxState::Checked) ? 1 : 0; + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.bComputeWeightedNormals == bNewState) + continue; + + CurCookable->Modify(); + SMBS.bComputeWeightedNormals = bNewState; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + ProxyGrp.AddWidgetRow() + .RowTag("RemoveDegenerates") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("RemoveDegenerates", "Remove Degenerates")) + ] + .ValueContent() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().bRemoveDegenerates ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](ECheckBoxState NewState) + { + const uint8 bNewState = (NewState == ECheckBoxState::Checked) ? 1 : 0; + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.bRemoveDegenerates == bNewState) + continue; + + CurCookable->Modify(); + SMBS.bRemoveDegenerates = bNewState; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + ProxyGrp.AddWidgetRow() + .RowTag("BuildReversedIndexBuffer") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("BuildReversedIndexBuffer", "Build Reversed Index Buffer")) + ] + .ValueContent() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().bBuildReversedIndexBuffer ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](ECheckBoxState NewState) + { + const uint8 bNewState = (NewState == ECheckBoxState::Checked) ? 1 : 0; + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.bBuildReversedIndexBuffer == bNewState) + continue; + + CurCookable->Modify(); + SMBS.bBuildReversedIndexBuffer = bNewState; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + ProxyGrp.AddWidgetRow() + .RowTag("UseHighPrecisionTangentBasis") + .NameContent() + [ + SNew(STextBlock) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Text(LOCTEXT("UseHighPrecisionTangentBasis", "Use High Precision Tangent Basis")) + ] + .ValueContent() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().bUseHighPrecisionTangentBasis ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](ECheckBoxState NewState) + { + const uint8 bNewState = (NewState == ECheckBoxState::Checked) ? 1 : 0; + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.bUseHighPrecisionTangentBasis == bNewState) + continue; + CurCookable->Modify(); + SMBS.bUseHighPrecisionTangentBasis = bNewState; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + ProxyGrp.AddWidgetRow() + .RowTag("UseFullPrecisionUVs") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("UseFullPrecisionUVs", "Use Full Precision UVs")) + ] + .ValueContent() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().bUseFullPrecisionUVs ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](ECheckBoxState NewState) + { + const uint8 bNewState = (NewState == ECheckBoxState::Checked) ? 1 : 0; + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.bUseFullPrecisionUVs == bNewState) + continue; + + CurCookable->Modify(); + SMBS.bUseFullPrecisionUVs = bNewState; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + ProxyGrp.AddWidgetRow() + .RowTag("UseBackwardsCompatibleF16TruncUVs") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("UseBackwardsCompatibleF16TruncUVs", "UE4 Compatible UVs")) + ] + .ValueContent() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().bUseBackwardsCompatibleF16TruncUVs ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](ECheckBoxState NewState) + { + const uint8 bNewState = (NewState == ECheckBoxState::Checked) ? 1 : 0; + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.bUseBackwardsCompatibleF16TruncUVs == bNewState) + continue; + + CurCookable->Modify(); + SMBS.bUseBackwardsCompatibleF16TruncUVs = bNewState; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + ProxyGrp.AddWidgetRow() + .RowTag("GenerateLightmapUVs") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("GenerateLightmapUVs", "Generate Lightmap UVs")) + ] + .ValueContent() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().bGenerateLightmapUVs ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](ECheckBoxState NewState) + { + const uint8 bNewState = (NewState == ECheckBoxState::Checked) ? 1 : 0; + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.bGenerateLightmapUVs == bNewState) + continue; + + CurCookable->Modify(); + SMBS.bGenerateLightmapUVs = bNewState; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + ProxyGrp.AddWidgetRow() + .RowTag("MinLightmapResolution") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("MinLightmapResolution", "Min Lightmap Resolution")) + ] + .ValueContent() + [ + SNew(SSpinBox) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .MinValue(1) + .MaxValue(2048) + .Value_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().MinLightmapResolution; + }) + .OnValueChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](int32 NewValue) + { + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.MinLightmapResolution == NewValue) + continue; + + CurCookable->Modify(); + SMBS.MinLightmapResolution = NewValue; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + ProxyGrp.AddWidgetRow() + .RowTag("SourceLightmapIndex") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("SourceLightmapIndex", "Source Lightmap Index")) + ] + .ValueContent() + [ + SNew(SSpinBox) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .MinValue(0) + .MaxValue(7) + .Value_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().SrcLightmapIndex; + }) + .OnValueChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](int32 NewValue) + { + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.SrcLightmapIndex == NewValue) + continue; + + CurCookable->Modify(); + SMBS.SrcLightmapIndex = NewValue; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + ProxyGrp.AddWidgetRow() + .RowTag("DestinationLightmapIndex") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("DestinationLightmapIndex", "Destination Lightmap Index")) + ] + .ValueContent() + [ + SNew(SSpinBox) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .MinValue(0) + .MaxValue(7) + .Value_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().DstLightmapIndex; + }) + .OnValueChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](int32 NewValue) + { + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.DstLightmapIndex == NewValue) + continue; + + CurCookable->Modify(); + SMBS.DstLightmapIndex = NewValue; + + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + // Lambda update BuildScale + auto OnBuildScaleChange = [InCookables, MarkCookableOutputsNeedUpdate](float NewValue, int8 XYZ) + { + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + switch (XYZ) + { + case 0: + SMBS.BuildScale3D.X = NewValue; + break; + case 1: + SMBS.BuildScale3D.Y = NewValue; + break; + case 2: + SMBS.BuildScale3D.Z = NewValue; + break; + default: + break; + } + CurCookable->Modify(); + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + }; + + ProxyGrp.AddWidgetRow() + .RowTag("BuildScale") + .NameContent() + [ + SNew(STextBlock) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Text(LOCTEXT("BuildScale", "Build Scale")) + .ToolTipText( LOCTEXT("BuildScale_ToolTip", "The local scale applied when building the mesh") ) + ] + .ValueContent() + .MinDesiredWidth(125.0f * 3.0f) + .MaxDesiredWidth(125.0f * 3.0f) + [ + SNew(SVectorInputBox) + .X_Lambda([MainCookable]() { return MainCookable->GetStaticMeshBuildSettings().BuildScale3D.X; }) + .Y_Lambda([MainCookable]() { return MainCookable->GetStaticMeshBuildSettings().BuildScale3D.Y; }) + .Z_Lambda([MainCookable]() { return MainCookable->GetStaticMeshBuildSettings().BuildScale3D.Z; }) + .bColorAxisLabels(false) + .AllowSpin(false) + .OnXCommitted_Lambda([InCookables, OnBuildScaleChange, MarkCookableOutputsNeedUpdate](float NewValue, ETextCommit::Type TextCommitType) + { OnBuildScaleChange(NewValue, 0); }) + .OnYCommitted_Lambda([InCookables, OnBuildScaleChange, MarkCookableOutputsNeedUpdate](float NewValue, ETextCommit::Type TextCommitType) + { OnBuildScaleChange(NewValue, 1); }) + .OnZCommitted_Lambda([InCookables, OnBuildScaleChange, MarkCookableOutputsNeedUpdate](float NewValue, ETextCommit::Type TextCommitType) + { OnBuildScaleChange(NewValue, 2); }) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ]; + } + + { + auto OnDistanceFieldResChanged = [InCookables, MarkCookableOutputsNeedUpdate](float NewValue, bool bIsCommit) + { + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.DistanceFieldResolutionScale == NewValue) + continue; + SMBS.DistanceFieldResolutionScale = NewValue; + if (bIsCommit) + { + CurCookable->Modify(); + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + } + }; + + ProxyGrp.AddWidgetRow() + .RowTag("DistanceFieldResolutionScale") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("DistanceFieldResolutionScale", "Distance Field Resolution Scale")) + ] + .ValueContent() + [ + SNew(SSpinBox) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .MinValue(0.0f) + .MaxValue(100.0f) + .Value_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().DistanceFieldResolutionScale; + }) + .OnValueChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate, OnDistanceFieldResChanged](float NewValue) + { OnDistanceFieldResChanged(NewValue, false); }) + .OnValueCommitted_Lambda([InCookables, MarkCookableOutputsNeedUpdate, OnDistanceFieldResChanged](float NewValue, ETextCommit::Type TextCommitType) + { OnDistanceFieldResChanged(NewValue, true); }) + ]; + } + + { + ProxyGrp.AddWidgetRow() + .RowTag("GenerateDistanceFieldAsIfTwoSided") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("GenerateDistanceFieldAsIfTwoSided", "Two-Sided Distance Field Generation")) + ] + .ValueContent() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().bGenerateDistanceFieldAsIfTwoSided ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](ECheckBoxState NewState) + { + const uint8 bNewState = (NewState == ECheckBoxState::Checked) ? 1 : 0; + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.bGenerateDistanceFieldAsIfTwoSided == bNewState) + continue; + + CurCookable->Modify(); + SMBS.bGenerateDistanceFieldAsIfTwoSided = bNewState; + MarkCookableOutputsNeedUpdate(CurCookable); + } + }) + ]; + } + + { + TSharedRef PropWidget = SNew(SObjectPropertyEntryBox) + .AllowedClass(UStaticMesh::StaticClass()) + .AllowClear(true) + .ObjectPath_Lambda([MainCookable]() + { + if (MainCookable->GetStaticMeshBuildSettings().DistanceFieldReplacementMesh) + return MainCookable->GetStaticMeshBuildSettings().DistanceFieldReplacementMesh->GetPathName(); + else + return FString(""); + }) + .OnObjectChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate](const FAssetData& InAssetData) + { + UStaticMesh* NewObject = Cast(InAssetData.GetAsset()); + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.DistanceFieldReplacementMesh == NewObject) + continue; + + CurCookable->Modify(); + SMBS.DistanceFieldReplacementMesh = NewObject; + MarkCookableOutputsNeedUpdate(CurCookable); + } + }); + + ProxyGrp.AddWidgetRow() + .RowTag("DistanceFieldReplacementMesh") + .NameContent() + [ + SNew(STextBlock) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + .Text(LOCTEXT("DistanceFieldReplacementMesh", "Distance Field Replacement Mesh")) + ] + .ValueContent() + [ + PropWidget + ]; + } + + { + auto OnMaxLumenMeshCardsChanged = [InCookables, MarkCookableOutputsNeedUpdate](int32 NewValue, bool bIsCommit) + { + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FMeshBuildSettings& SMBS = CurCookable->GetStaticMeshBuildSettings(); + if (SMBS.MaxLumenMeshCards == NewValue) + continue; + SMBS.MaxLumenMeshCards = NewValue; + if (bIsCommit) + { + CurCookable->Modify(); + // Mark our outputs as needing an update + MarkCookableOutputsNeedUpdate(CurCookable); + } + } + }; + + ProxyGrp.AddWidgetRow() + .RowTag("MaxLumenMeshCards") + .NameContent() + [ + SNew(STextBlock) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Text(LOCTEXT("MaxLumenMeshCards", "Max Lumen Mesh Cards")) + ] + .ValueContent() + [ + SNew(SSpinBox) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .MinValue(0) + .MaxValue(32) + .Value_Lambda([MainCookable]() + { + return MainCookable->GetStaticMeshBuildSettings().MaxLumenMeshCards; + }) + .OnValueChanged_Lambda([InCookables, MarkCookableOutputsNeedUpdate, OnMaxLumenMeshCardsChanged](int32 NewValue) + { OnMaxLumenMeshCardsChanged(NewValue, false); }) + .OnValueCommitted_Lambda([InCookables, MarkCookableOutputsNeedUpdate, OnMaxLumenMeshCardsChanged](int32 NewValue, ETextCommit::Type TextCommitType) + { OnMaxLumenMeshCardsChanged(NewValue, true); }) + ]; + } +} + + +void +FHoudiniCookableDetails::CreateMeshGenerationDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables) +{ + if (InCookables.Num() <= 0) + return; + + TWeakObjectPtr MainCookable = InCookables[0]; + if (!IsValidWeakPointer(MainCookable)) + return; + + if (!MainCookable->IsOutputSupported()) + return; + + // Create the Mesh Generation category + FString BuildSettingsCatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_MESHGEN); + + // If we have selected more than one component that have different HDAs, + // we need to create multiple categories one for each different HDA + // OutputCatName += MultiSelectionIdentifier; + + IDetailCategoryBuilder& HouMeshGenCategory = + DetailBuilder.EditCategory(*BuildSettingsCatName, FText::GetEmpty(), ECategoryPriority::Important); + + FString Label = TEXT("Static Mesh Generation Properties"); + IDetailGroup& ProxyGrp = HouMeshGenCategory.AddGroup(FName(*Label), FText::FromString(Label)); + + // Lambda used to trigger a n output update if necessary + auto MarkOutputUpdateNeeded = [InCookables]() + { + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + } + }; + + // + // bDoubleSidedGeometry + // + { + ProxyGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Double Sided Geometry")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .IsChecked_Lambda([MainCookable]() + { + if (!IsValidWeakPointer(MainCookable)) + return ECheckBoxState::Unchecked; + return MainCookable->GetStaticMeshGenerationProperties().bGeneratedDoubleSidedGeometry ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([MainCookable, InCookables, MarkOutputUpdateNeeded](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainCookable)) + return; + + const uint32 bNewState = (NewState == ECheckBoxState::Checked) ? 1 : 0; + if (MainCookable->GetStaticMeshGenerationProperties().bGeneratedDoubleSidedGeometry == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniSMGPFullPrecUVs", "Houdini Static Mesh Generation Properties: Changed bUseFullPrecisionUVs"), + MainCookable->GetOuter()); + + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + FHoudiniStaticMeshGenerationProperties& SMGP = CurCookable->GetStaticMeshGenerationProperties(); + if (SMGP.bGeneratedDoubleSidedGeometry == bNewState) + continue; + + CurCookable->Modify(); + SMGP.bGeneratedDoubleSidedGeometry = bNewState; + } + + // Mark our outputs as needing an update + MarkOutputUpdateNeeded(); + }) + ] + ]; + } + + + // + // PhysMaterial + // + + // Create thumbnail for this HDA. + TSharedPtr ThumbnailPool = HouMeshGenCategory.GetParentLayout().GetThumbnailPool(); + { + // Lambda for updating the Physical Material + auto UpdatePhysMat = [MainCookable](const TArray>& InCookables, UObject* InObject) + { + if (!IsValidWeakPointer(MainCookable)) + return; + + if (!InObject->IsA()) + return; + + UPhysicalMaterial* NewPhysMat = Cast(InObject); + if (!IsValid(NewPhysMat)) + return; + + // TODO: Transaction on all cookable? + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniSMGPPhysMaterial", "Houdini Static Mesh Generation Properties: Changed PhysMaterial"), + MainCookable->GetOuter()); + + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + if (!CurCookable->IsOutputSupported()) + continue; + + FHoudiniStaticMeshGenerationProperties& SMGP = CurCookable->GetStaticMeshGenerationProperties(); + if (SMGP.GeneratedPhysMaterial == NewPhysMat) + continue; + + CurCookable->Modify(); + SMGP.GeneratedPhysMaterial = NewPhysMat; + } + }; + + ProxyGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Simple Collision Physical Material")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .Padding(2, 2, 5, 2) + .AutoHeight() + .Padding(0, 5, 0, 0) + .AutoHeight() + [ + SNew(SObjectPropertyEntryBox) + .ObjectPath_Lambda([MainCookable]() + { + if (!IsValidWeakPointer(MainCookable)) + return FString(); + + UPhysicalMaterial* PhysMat = MainCookable->GetStaticMeshGenerationProperties().GeneratedPhysMaterial; + if (!IsValid(PhysMat)) + return FString(); + + return PhysMat->GetPathName(); + }) + .AllowedClass(UPhysicalMaterial::StaticClass()) + .OnObjectChanged_Lambda([InCookables, UpdatePhysMat](const FAssetData& InAssetData) + { + UPhysicalMaterial* PhysMat = Cast(InAssetData.GetAsset()); + if (IsValid(PhysMat)) + UpdatePhysMat(InCookables, PhysMat); + }) + .AllowCreate(false) + .AllowClear(true) + .DisplayUseSelected(true) + .DisplayBrowse(true) + .DisplayThumbnail(true) + .ThumbnailPool(ThumbnailPool) + .NewAssetFactories(TArray()) + ] + ]; + } + + // + // DefaultBodyInstance + // + + /* + // Default properties of the body instance + //UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeneratedStaticMeshSettings", meta = (FullyExpand = "true")) + //struct FBodyInstance DefaultBodyInstance; + + TArray> ObjectsCustomized; + { + for (int Idx = 0; Idx < InCookables.Num(); Idx++) + { + if (!IsValidWeakPointer(InCookables[Idx])) + continue; + + FHoudiniStaticMeshGenerationProperties& SMGP = InCookables[Idx]->GetStaticMeshGenerationProperties(); + SMGP.DefaultBodyInstance. + InCookables[Idx]->GetOutputData()->Modify(); + } + } + TSharedPtr MaterialCategory; + TSharedPtr BodyInstanceCustomizationHelper; + + BodyInstanceCustomizationHelper = MakeShareable(new FBodyInstanceCustomizationHelper(ObjectsCustomized)); + BodyInstanceCustomizationHelper->CustomizeDetails(DetailBuilder, DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UPrimitiveComponent, BodyInstance))); + + */ + + + // + // CollisionTraceFlag - TODO + // + { + CollisionTraceFlagsAsString.Add(MakeShared(TEXT("Project Default"))); // CTF_UseDefault + CollisionTraceFlagsAsString.Add(MakeShared(TEXT("Simple And Complex"))); // CTF_UseSimpleAndComplex + CollisionTraceFlagsAsString.Add(MakeShared(TEXT("Use Simple Collision As Complex"))); // CTF_UseSimpleAsComplex + CollisionTraceFlagsAsString.Add(MakeShared(TEXT("Use Complex Collision As Simple"))); // CTF_UseComplexAsSimple + + TSharedPtr InitialSel = MakeShareable(new FString(*CollisionTraceFlagsAsString[MainCookable->GetStaticMeshGenerationProperties().GeneratedCollisionTraceFlag.GetIntValue()])); + ProxyGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Collision Complexity")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SComboBox>) + .OptionsSource(&CollisionTraceFlagsAsString) + .InitiallySelectedItem(InitialSel) + .OnGenerateWidget_Lambda([](TSharedPtr InItem) + { + return SNew(STextBlock).Text(FText::FromString(*InItem)); + }) + .OnSelectionChanged_Lambda([InCookables, this](TSharedPtr NewChoice, ESelectInfo::Type SelectType) + { + if (!NewChoice.IsValid()) + return; + + int32 FoundIdx = CollisionTraceFlagsAsString.Find(NewChoice); + if (FoundIdx < 0) + return; + + ECollisionTraceFlag NewVal = (ECollisionTraceFlag)FoundIdx; + for (int Idx = 0; Idx < InCookables.Num(); Idx++) + { + if (!IsValidWeakPointer(InCookables[Idx])) + continue; + + FHoudiniStaticMeshGenerationProperties& SMGP = InCookables[Idx]->GetStaticMeshGenerationProperties(); + if (SMGP.GeneratedCollisionTraceFlag == NewVal) + continue; + + InCookables[Idx]->Modify(); + SMGP.GeneratedCollisionTraceFlag = NewVal; + InCookables[Idx]->GetOutputData()->Modify(); + + // TODO: Update outputs? + } + }) + [ + SNew(STextBlock) + .Text_Lambda([this, MainCookable]() + { + return FText::FromString(*CollisionTraceFlagsAsString[ + (int)MainCookable->GetStaticMeshGenerationProperties().GeneratedCollisionTraceFlag]); + }) + //.Text(FText::FromString("Light Map Resolution")) + .Font(_GetEditorStyle().GetFontStyle( TEXT( "PropertyWindow.NormalFont" ) ) ) + ] + ] + ] + ]; + } + + + // + // LightMapResolution + // + + { + // Resolution of lightmap for baked lighting. + //UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Light Map Resolution", FixedIncrement = "4.0")) + //int32 LightMapResolution; + + // Lambdas for slider begin + auto SliderBeginLightMapResolution = [](const TArray>& Cookables) + { + if (Cookables.Num() == 0) + return; + + if (!IsValidWeakPointer(Cookables[0])) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniChangeLightMapResolution", "Houdini Static Mesh Generation Properties: Changed Light Map Resolution"), + Cookables[0]->GetOuter()); + + for (int Idx = 0; Idx < Cookables.Num(); Idx++) + { + if (!IsValidWeakPointer(Cookables[Idx])) + continue; + + Cookables[Idx]->GetOutputData()->Modify(); + } + }; + + // Lambdas for slider end + auto SliderEndLightMapResolution = [](const TArray>& Cookables) + { + // Mark the value as changed to trigger an update + for (int Idx = 0; Idx < Cookables.Num(); Idx++) + { + if (!IsValidWeakPointer(Cookables[Idx])) + continue; + + // TODO: Mark changed or equivalent? + } + }; + + // Lambdas for changing the value + auto ChangeLightMapResolution = [](const int32& Value, const bool& DoChange, const TArray>& Cookables) + { + if (Cookables.Num() == 0) + return; + + if (!IsValidWeakPointer(Cookables[0])) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniChangeLightMapResolution", "Houdini Static Mesh Generation Properties: Changed LightMapResolution"), + Cookables[0]->GetOuter()); + + for (int Idx = 0; Idx < Cookables.Num(); Idx++) + { + if (!IsValidWeakPointer(Cookables[Idx])) + continue; + + FHoudiniStaticMeshGenerationProperties& SMGP = Cookables[Idx]->GetStaticMeshGenerationProperties(); + if (SMGP.GeneratedLightMapResolution == Value) + continue; + + Cookables[Idx]->Modify(); + SMGP.GeneratedLightMapResolution = Value; + + if (DoChange) + { + Cookables[Idx]->GetProxyData()->Modify(); + } + } + }; + + ProxyGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Light Map Resolution")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SNumericEntryBox) + .AllowSpin(true) + + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + + .MinValue(0) + .MaxValue(3600) + + .MinSliderValue(0) + .MaxSliderValue(60) + + .Value_Lambda([MainCookable]() { return MainCookable->GetStaticMeshGenerationProperties().GeneratedLightMapResolution; }) + .OnValueChanged_Lambda([InCookables, ChangeLightMapResolution](int32 Val) + { + ChangeLightMapResolution(Val, false, InCookables); + }) + .OnValueCommitted_Lambda([InCookables, ChangeLightMapResolution](int32 Val, ETextCommit::Type TextCommitType) + { + ChangeLightMapResolution(Val, true, InCookables); + }) + .OnBeginSliderMovement_Lambda([InCookables, SliderBeginLightMapResolution]() + { + SliderBeginLightMapResolution(InCookables); + }) + .OnEndSliderMovement_Lambda([InCookables, SliderEndLightMapResolution](const int32 NewValue) + { + SliderEndLightMapResolution(InCookables); + }) + .SliderExponent(4.0f) + ] + ] + ]; + } + + + + // + // LightMapCoordinateIndex + // + + { + /// The UV coordinate index of lightmap + //UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Light map coordinate index")) + //int32 LightMapCoordinateIndex; + + // Lambdas for slider begin + auto SliderBeginLightMapCoordinateIndex = [](const TArray>& Cookables) + { + if (Cookables.Num() == 0) + return; + + if (!IsValidWeakPointer(Cookables[0])) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniChangeLightMapCoordinateIndex", "Houdini Static Mesh Generation Properties: Changed LightMapCoordinateIndex"), + Cookables[0]->GetOuter()); + + for (int Idx = 0; Idx < Cookables.Num(); Idx++) + { + if (!IsValidWeakPointer(Cookables[Idx])) + continue; + + Cookables[Idx]->GetProxyData()->Modify(); + } + }; + + // Lambdas for slider end + auto SliderEndLightMapCoordinateIndex = [](const TArray>& Cookables) + { + // Mark the value as changed to trigger an update + for (int Idx = 0; Idx < Cookables.Num(); Idx++) + { + if (!IsValidWeakPointer(Cookables[Idx])) + continue; + + // TODO: Mark changed or equivalent? + } + }; + + // Lambdas for changing the value + auto ChangeLightMapCoordinateIndex = [](const int32& Value, const bool& DoChange, const TArray>& Cookables) + { + if (Cookables.Num() == 0) + return; + + if (!IsValidWeakPointer(Cookables[0])) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniChangeLightMapCoordinateIndex", "Houdini Static Mesh Generation Properties: Changed LightMapCoordinateIndex"), + Cookables[0]->GetOuter() ); + + for (int Idx = 0; Idx < Cookables.Num(); Idx++) + { + if (!IsValidWeakPointer(Cookables[Idx])) + continue; + + FHoudiniStaticMeshGenerationProperties& SMGP = Cookables[Idx]->GetStaticMeshGenerationProperties(); + if (SMGP.GeneratedLightMapCoordinateIndex == Value) + continue; + + Cookables[Idx]->Modify(); + SMGP.GeneratedLightMapCoordinateIndex = Value; + + if (DoChange) + { + Cookables[Idx]->GetProxyData()->Modify(); + } + } + }; + + ProxyGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Light map coordinate index")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SNumericEntryBox) + .AllowSpin(true) + + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + + .MinValue(0) + .MaxValue(3600) + + .MinSliderValue(0) + .MaxSliderValue(60) + + .Value_Lambda([MainCookable]() { return MainCookable->GetStaticMeshGenerationProperties().GeneratedLightMapCoordinateIndex; }) + .OnValueChanged_Lambda([InCookables, ChangeLightMapCoordinateIndex](int32 Val) + { + ChangeLightMapCoordinateIndex(Val, false, InCookables); + }) + .OnValueCommitted_Lambda([InCookables, ChangeLightMapCoordinateIndex](int32 Val, ETextCommit::Type TextCommitType) + { + ChangeLightMapCoordinateIndex(Val, true, InCookables); + }) + .OnBeginSliderMovement_Lambda([InCookables, SliderBeginLightMapCoordinateIndex]() + { + SliderBeginLightMapCoordinateIndex(InCookables); + }) + .OnEndSliderMovement_Lambda([InCookables, SliderEndLightMapCoordinateIndex](const int32 NewValue) + { + SliderEndLightMapCoordinateIndex(InCookables); + }) + .SliderExponent(4.0f) + ] + ] + ]; + } +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniCookableDetails.h`: + +```h +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniEngineDetails.h" +#include "HoudiniOutputDetails.h" +#include "HoudiniPDGDetails.h" +#include "HoudiniParameterDetails.h" +#include "HoudiniRuntimeSettings.h" + +#include "CoreMinimal.h" +#include "Engine/EngineTypes.h" +#include "IDetailCustomization.h" + + +class UHoudiniCookable; +class UHoudiniAssetComponent; +class UStaticMesh; + +class FHoudiniCookableDetails : public IDetailCustomization +{ +public: + + // Constructor. + FHoudiniCookableDetails(); + + // IDetailCustomization methods. + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; + + // Create an instance of this detail layout class. + static TSharedRef MakeInstance(); + + void CreateHoudiniEngineDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier = FString(), + const EHoudiniDetailsFlags& DetailsFlags = EHoudiniDetailsFlags::Defaults); + + void CreateHoudiniAssetDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables); + + void CreateNodeSyncDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier = FString()); + + void CreatePDGDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier = FString()); + + void CreateParameterDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier = FString()); + + void CreateHandleDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier = FString()); + + void CreateInputDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier = FString()); + + void CreateOutputDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables, + const FString& MultiSelectionIdentifier = FString()); + + void CreateProxyDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables); + + void CreateMeshBuildSettingsDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables); + + void CreateMeshGenerationDetails( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables); + + void CreateMeshConversonSettings( + IDetailLayoutBuilder& DetailBuilder, + TArray>& InCookables); + +private: + + // Handler for double clicking the static mesh thumbnail, opens the editor. + FReply OnThumbnailDoubleClick( + const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent, UObject* Object); + + // Structure holding the output's details + TSharedPtr OutputDetails; + + // Structure holding the parameter's details + TSharedPtr ParameterDetails; + + // Structure holding the PDG Asset Link's details + TSharedPtr PDGDetails; + + // Structure holding the HoudiniAsset details + TSharedPtr HoudiniEngineDetails; + + TArray> CollisionTraceFlagsAsString; +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEditorAssetStateSubsystem.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include "HoudiniEditorAssetStateSubsystem.h" +#include "HoudiniAssetStateTypes.h" +#include "HoudiniCookable.h" +#include "HoudiniEngineBakeUtils.h" + +#include "UObject/Object.h" + +void +UHoudiniEditorAssetStateSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + IHoudiniEditorAssetStateSubsystemInterface::SetInstance(this); +} + +void +UHoudiniEditorAssetStateSubsystem::Deinitialize() +{ + IHoudiniEditorAssetStateSubsystemInterface::SetInstance(nullptr); +} + +void +UHoudiniEditorAssetStateSubsystem::NotifyOfHoudiniAssetStateChange(UObject* InHoudiniAssetContext, const EHoudiniAssetState InFromState, const EHoudiniAssetState InToState) +{ + if (!IsValid(InHoudiniAssetContext)) + return; + + UHoudiniCookable* const HC = Cast(InHoudiniAssetContext); + if (!IsValid(HC)) + return; + + // Auto Bake needs to happen after output processing has already been handled, + // otherwise we won't have the OutputObjects required to handle baking properly. + // If we went from Processing -> None, then cook was successful, outputs have been processed, + // and if auto bake is enabled, then auto bake! + if (InFromState == EHoudiniAssetState::Processing + && InToState == EHoudiniAssetState::None + && HC->WasLastCookSuccessful() + && HC->IsBakeAfterNextCookEnabled()) + { + FHoudiniBakeSettings BakeSettings; + BakeSettings.SetFromCookable(HC); + FHoudiniEngineBakeUtils::BakeCookable( + HC, + BakeSettings, + HC->GetBakingData()->HoudiniEngineBakeOption, + HC->GetBakingData()->bRemoveOutputAfterBake); + + if (HC->GetBakeAfterNextCook() == EHoudiniBakeAfterNextCook::Once) + HC->SetBakeAfterNextCook(EHoudiniBakeAfterNextCook::Disabled); + } + + // Done, no need to bake for the HC + return; +} + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEditorAssetStateSubsystem.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniEditorAssetStateSubsystemInterface.h" + +#include "CoreTypes.h" +#include "EditorSubsystem.h" + +#include "HoudiniEditorAssetStateSubsystem.generated.h" + + +// UE forward declarations +class UObject; + +// Houdini Engine forward declarations +enum class EHoudiniAssetState : uint8; + +/** + * An editor subsystem that gets notified about HoudiniAssetComponent state changes. Currently this subsystem is used + * for auto-baking functionality (receiving the state update from the HoudiniEngineRuntime module and then baking + * the HDA via this subsystem (HoudiniEngineEditor module)). + */ +UCLASS() +class HOUDINIENGINEEDITOR_API UHoudiniEditorAssetStateSubsystem : public UEditorSubsystem, public IHoudiniEditorAssetStateSubsystemInterface +{ + GENERATED_BODY() + +public: + UHoudiniEditorAssetStateSubsystem() {} + + //~ Begin UEditorSubsystem interface + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + //~ End UEditorSubsystem interface + +protected: + //~ Begin IHoudiniEditorAssetStateSubsystemInterface interface + + // This should be called by the HoudiniEngineManager to notify the subsystem that a Houdini Asset object, such + // as a HoudiniAssetComponent, has changed state. + virtual void NotifyOfHoudiniAssetStateChange(UObject* InHoudiniAssetContext, const EHoudiniAssetState InFromState, const EHoudiniAssetState InToState) override; + + //~ End IHoudiniEditorAssetStateSubsystemInterface interface +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEditorNodeSyncSubsystem.cpp`: + +```cpp +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "HoudiniEditorNodeSyncSubsystem.h" + +#include "HoudiniAssetActor.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineCommands.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEngineString.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniGeoImporter.h" +#include "HoudiniInput.h" +#include "HoudiniInputObject.h" +#include "HoudiniInputTranslator.h" +#include "HoudiniInputTypes.h" +#include "HoudiniMeshTranslator.h" +#include "HoudiniNodeSyncComponent.h" +#include "HoudiniSkeletalMeshTranslator.h" +#include "HoudiniOutputTranslator.h" +#include "SHoudiniNodeSyncPanel.h" +#include "UnrealMeshTranslator.h" +#include "UnrealSkeletalMeshTranslator.h" + +#include "ActorFactories/ActorFactory.h" +#include "Engine/SkeletalMesh.h" +#include "LevelEditor.h" + + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "Engine/SkinnedAssetCommon.h" +#endif + +void +UHoudiniEditorNodeSyncSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + // Register our extensions + FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked("LevelEditor"); + LevelEditorModule.OnRegisterLayoutExtensions().AddUObject(this, &UHoudiniEditorNodeSyncSubsystem::RegisterLayoutExtensions); + + // Initialize our input objects + InitNodeSyncInputsIfNeeded(); +} + +void +UHoudiniEditorNodeSyncSubsystem::Deinitialize() +{ + // Allow the inputs to delete their node + NodeSyncWorldInput->SetCanDeleteHoudiniNodes(true); + NodeSyncCBInput->SetCanDeleteHoudiniNodes(true); + + // Clean the world input + FHoudiniInputTranslator::DisconnectAndDestroyInput(NodeSyncWorldInput, EHoudiniInputType::World); + TArray>* InputObjects = NodeSyncWorldInput->GetHoudiniInputObjectArray(EHoudiniInputType::World); + if (InputObjects) + InputObjects->Empty(); + + // Clean the CB input + FHoudiniInputTranslator::DisconnectAndDestroyInput(NodeSyncCBInput, EHoudiniInputType::World); + InputObjects = NodeSyncCBInput->GetHoudiniInputObjectArray(EHoudiniInputType::Geometry); + if (InputObjects) + InputObjects->Empty(); + + // Unregister our extensions + FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked("LevelEditor"); + LevelEditorModule.OnRegisterLayoutExtensions().RemoveAll(this); +} + +void +UHoudiniEditorNodeSyncSubsystem::RegisterLayoutExtensions(FLayoutExtender& Extender) +{ + //Extender.ExtendLayout(LevelEditorTabIds::PlacementBrowser, ELayoutExtensionPosition::After, FTabManager::FTab(HoudiniNodeSyncTabId, ETabState::ClosedTab)); + Extender.ExtendLayout(_GetPlacementBrowserName(), ELayoutExtensionPosition::After, FTabManager::FTab(NodeSyncTabName, ETabState::ClosedTab)); +} + +bool +UHoudiniEditorNodeSyncSubsystem::CreateSessionIfNeeded() +{ + // Do nothing if we have a valid session + if (HAPI_RESULT_SUCCESS == FHoudiniApi::IsSessionValid(FHoudiniEngine::Get().GetSession())) + return FHoudiniEngine::Get().IsSessionSyncEnabled(); + + // Attempt to open session sync + if (!FHoudiniEngine::Get().IsSessionSyncEnabled()) + { + FHoudiniEngineCommands::OpenSessionSync(true); + } + + // Make sure we have a valid session + if (HAPI_RESULT_SUCCESS != FHoudiniApi::IsSessionValid(FHoudiniEngine::Get().GetSession())) + return false; + + // returns true if we have a session sync-enabled session + return FHoudiniEngine::Get().IsSessionSyncEnabled(); +} + + +bool +UHoudiniEditorNodeSyncSubsystem::GetNodeSyncWorldInput(UHoudiniInput*& OutInput) +{ + if (!InitNodeSyncInputsIfNeeded()) + return false; + + if (!IsValid(NodeSyncWorldInput)) + return false; + + OutInput = NodeSyncWorldInput; + + return true; +} + +bool +UHoudiniEditorNodeSyncSubsystem::GetNodeSyncCBInput(UHoudiniInput*& OutInput) +{ + if (!InitNodeSyncInputsIfNeeded()) + return false; + + if (!IsValid(NodeSyncCBInput)) + return false; + + OutInput = NodeSyncCBInput; + + return true; +} + +bool +UHoudiniEditorNodeSyncSubsystem::InitNodeSyncInputsIfNeeded() +{ + if (IsValid(NodeSyncWorldInput) && IsValid(NodeSyncCBInput)) + return true; + + // Lambda used for initializing both NodeSyncInputs + auto InitInputOptions = [&](UHoudiniInput* Input, const FString& InName, const bool& bIsWorld) + { + if (!IsValid(Input)) + return false; + + // Set the input type and name + bool bOutBPModif = false; + Input->SetInputType(bIsWorld ? EHoudiniInputType::World : EHoudiniInputType::Geometry, bOutBPModif); + Input->SetName(InName); + + // Set the default input options + Input->SetExportColliders(false); + Input->SetExportLODs(false); + Input->SetExportSockets(false); + Input->SetLandscapeExportType(EHoudiniLandscapeExportType::Heightfield); + Input->SetAddRotAndScaleAttributes(false); + Input->SetImportAsReference(false); + Input->SetImportAsReferenceRotScaleEnabled(false); + Input->SetKeepWorldTransform(true); + Input->SetPackBeforeMerge(false); + Input->SetUnrealSplineResolution(50.0f); + Input->SetExportLevelInstanceContent(true); + Input->SetCanDeleteHoudiniNodes(false); + Input->SetUseLegacyInputCurve(true); + + Input->SetAssetNodeId(-1); + + return true; + }; + + if (!IsValid(NodeSyncWorldInput)) + { + // Create a fake HoudiniInput/HoudiniInputObject so we can use the input Translator to send the data to H + FString InputObjectName = TEXT("NodeSyncWorldInput"); + NodeSyncWorldInput = NewObject( + this, UHoudiniInput::StaticClass(), FName(*InputObjectName), RF_Transactional); + + if (!InitInputOptions(NodeSyncWorldInput, InputObjectName, true)) + return false; + } + + if (!IsValid(NodeSyncCBInput)) + { + // Create a fake HoudiniInput/HoudiniInputObject so we can use the input Translator to send the data to H + FString InputObjectName = TEXT("NodeSyncCBInput"); + NodeSyncCBInput = NewObject( + this, UHoudiniInput::StaticClass(), FName(*InputObjectName), RF_Transactional); + + if (!InitInputOptions(NodeSyncCBInput, InputObjectName, false)) + return false; + } + + return true; +} + + +void +UHoudiniEditorNodeSyncSubsystem::SendContentBrowserSelection(const TArray& CurrentCBSelection) +{ + LastSendStatus = EHoudiniNodeSyncStatus::Running; + SendStatusMessage = "Sending..."; + + if (CurrentCBSelection.Num() <= 0) + { + HOUDINI_LOG_MESSAGE(TEXT("Houdini Node Sync: No selection in the content browser")); + + LastSendStatus = EHoudiniNodeSyncStatus::Failed; + SendStatusMessage = "Send Failed: No selection in the content browser."; + SendStatusDetails = "Houdini Node Sync - Send Failed: No selection in the content browser\nPlease select Assets in the content browser and try again."; + + return; + } + + // No need to upload something we've already sent ... + TArray ObjectsToSend = CurrentCBSelection; + TArray>* InputObjects = NodeSyncCBInput->GetHoudiniInputObjectArray(EHoudiniInputType::Geometry); + if (InputObjects) + { + // ... so remove all selected objects that were previously sent + for (const auto& CurInputObject : *InputObjects) + { + UObject* CurrentObject = CurInputObject ? CurInputObject->GetObject() : nullptr; + if (!CurrentObject) + continue; + + int32 FoundIdx = ObjectsToSend.Find(CurrentObject); + if (FoundIdx != INDEX_NONE) + ObjectsToSend.RemoveAt(FoundIdx); + } + } + + // Keep track of the index where we add new things + // New objects are appended at the end, and we don't want to resend the whole array + int32 AddedObjectIndex = NodeSyncCBInput->GetNumberOfInputObjects(EHoudiniInputType::Geometry); + + SendToHoudini(ObjectsToSend, AddedObjectIndex, false); +} + + +void +UHoudiniEditorNodeSyncSubsystem::SendWorldSelection() +{ + LastSendStatus = EHoudiniNodeSyncStatus::Running; + SendStatusMessage = "Sending..."; + + // Get current world selection + TArray CurrentWorldSelection; + int32 SelectedHoudiniAssets = FHoudiniEngineEditorUtils::GetWorldSelection(CurrentWorldSelection, false); + if (SelectedHoudiniAssets <= 0) + { + HOUDINI_LOG_MESSAGE(TEXT("Houdini Node Sync: No selection in the world outliner")); + + LastSendStatus = EHoudiniNodeSyncStatus::Failed; + SendStatusMessage = "Send Failed: No selection in the world outliner."; + SendStatusDetails = "Houdini Node Sync - Send Failed: No selection in the world outliner\nPlease select Actors in the World and try again."; + + return; + } + + // Ensure that the NodeSync inputs are valid + InitNodeSyncInputsIfNeeded(); + + // No need to upload something we've already sent + TArray>* InputObjects = NodeSyncWorldInput->GetHoudiniInputObjectArray(EHoudiniInputType::World); + if (InputObjects) + { + // Remove all selected objects that were previously sent + for (const auto& CurInputObject : *InputObjects) + { + UObject* CurrentObject = CurInputObject ? CurInputObject->GetObject() : nullptr; + if (!CurrentObject) + continue; + + int32 FoundIdx = CurrentWorldSelection.Find(CurrentObject); + if (FoundIdx != INDEX_NONE) + CurrentWorldSelection.RemoveAt(FoundIdx); + } + } + + // Keep track of the index where we add new things + // New objects are appended at the end, and we don't want to resend the whole array + int32 AddedObjectIndex = NodeSyncWorldInput->GetNumberOfInputObjects(EHoudiniInputType::World); + + // See if our previously sent nodes are still valid + if (CheckNodeSyncInputNodesValid()) + UpdateNodeSyncInputs(); + + // Send the selected data to Houdini + SendToHoudini(CurrentWorldSelection, AddedObjectIndex, true); + + // Rebuild the NodeSync selection view + TSharedPtr NSPanel = FHoudiniEngineEditor::Get().GetNodeSyncPanel(); + if(NSPanel && NSPanel.IsValid()) + NSPanel->RebuildSelectionView(); +} + + +void +UHoudiniEditorNodeSyncSubsystem::SendToHoudini(const TArray& SelectedAssets, int32 ObjectIndex, const bool& bSendWorld) +{ + if (SelectedAssets.Num() <= 0) + { + LastSendStatus = EHoudiniNodeSyncStatus::Success; + SendStatusMessage = "Send Success"; + SendStatusDetails = "Houdini Node Sync - Send success - No new data to be sent was found!"; + return; + } + + // Add a slate notification + FString Notification = TEXT("Sending selected assets to Houdini..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + //GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Yellow, TEXT("Sending To Houdini!")); + + // Update status + LastSendStatus = EHoudiniNodeSyncStatus::Running; + SendStatusMessage = "Sending..."; + + if (!CreateSessionIfNeeded()) + { + // For now, just warn the session is not session sync + HOUDINI_LOG_WARNING(TEXT("HoudiniNodeSync: the current session is not session-sync one!")); + + LastSendStatus = EHoudiniNodeSyncStatus::Warning; + SendStatusMessage = "Warning: the current session is not Session-sync one!"; + SendStatusDetails = SendStatusMessage + "\nYou can start a Session-sync session by using the Open Session Sync entry in the Houdini Engine menu."; + } + + // Create the content node + // As a subnet, so it's able to contain multiple geos + FString SendNodePath = NodeSyncOptions.SendNodePath; + HAPI_NodeId UnrealContentNodeId = -1; + HAPI_Result result = FHoudiniApi::GetNodeFromPath( + FHoudiniEngine::Get().GetSession(), -1, TCHAR_TO_ANSI(*SendNodePath), &UnrealContentNodeId); + + if ((result != HAPI_RESULT_SUCCESS) || (UnrealContentNodeId < 0)) + { + FString Name = SendNodePath; + Name.RemoveFromStart("/obj/"); + result = FHoudiniEngineUtils::CreateNode( - 1, "Object/subnet", TCHAR_TO_ANSI(*Name), true, &UnrealContentNodeId); + } + + // Decide wether we want to use to World or CB Input + UHoudiniInput* NodeSyncInput = bSendWorld ? NodeSyncWorldInput : NodeSyncCBInput; + + // Make sure the NodeSync input have been initialized + if (!InitNodeSyncInputsIfNeeded() || !IsValid(NodeSyncInput)) + { + // For now, just warn the session is not session sync + HOUDINI_LOG_WARNING(TEXT("HoudiniNodeSync: the current session is not session-sync one!")); + + LastSendStatus = EHoudiniNodeSyncStatus::Failed; + SendStatusMessage = "Error: Unable to initialize/access the Node Sync Input!"; + SendStatusDetails = SendStatusMessage; + + return; + } + + // Default input options + NodeSyncInput->SetCanDeleteHoudiniNodes(false); + NodeSyncInput->SetUseLegacyInputCurve(true); + + // TODO: Check? + NodeSyncInput->SetAssetNodeId(-1); + NodeSyncInput->SetInputNodeId(UnrealContentNodeId); + + const FHoudiniInputObjectSettings InputSettings(NodeSyncInput); + + // For each selected Asset, create a HoudiniInputObject and send it to H + for (int32 Idx = 0; Idx < SelectedAssets.Num(); Idx++) + { + TArray CreatedNodeIds; + TSet Handles; + UObject* CurrentObject = SelectedAssets[Idx]; + if (!IsValid(CurrentObject)) + continue; + + /* + // Create an input object wrapper for the current object + UHoudiniInputObject * CurrentInputObject = UHoudiniInputObject::CreateTypedInputObject(CurrentObject, NodeSyncInput, FString::FromInt(Idx + 1), InputSettings); + if (!IsValid(CurrentInputObject)) + continue; + */ + + NodeSyncInput->SetInputObjectAt(ObjectIndex + Idx, CurrentObject); + + UHoudiniInputObject* CurrentInputObject = NodeSyncInput->GetHoudiniInputObjectAt(ObjectIndex + Idx); + if (!IsValid(CurrentInputObject)) + continue; + + // Create a geo node for this object in the content node + FString ObjectName = CurrentObject->GetName(); + FHoudiniEngineUtils::SanitizeHAPIVariableName(ObjectName); + + // If the object is an Actor, prefer its label over the object name + AActor* CurrentActor = Cast(CurrentObject); + if (IsValid(CurrentActor)) + { + ObjectName = CurrentActor->GetActorNameOrLabel(); + } + + // See first if the node already exists + HAPI_NodeId CurrentObjectNodeId = -1; + result = FHoudiniApi::GetNodeFromPath( + FHoudiniEngine::Get().GetSession(), UnrealContentNodeId, TCHAR_TO_ANSI(*ObjectName), &CurrentObjectNodeId); + + if ((result != HAPI_RESULT_SUCCESS) || (CurrentObjectNodeId < 0)) + { + // No existing node found - create a new one + if (HAPI_RESULT_SUCCESS != FHoudiniEngineUtils::CreateNode( + UnrealContentNodeId, "geo", TCHAR_TO_ANSI(*ObjectName), true, &CurrentObjectNodeId)) + { + HOUDINI_LOG_WARNING(TEXT("HoudiniNodeSync: Failed to create input object geo node for %s."), *CurrentInputObject->GetName()); + + LastSendStatus = EHoudiniNodeSyncStatus::SuccessWithErrors; + SendStatusMessage = "Send Success with errors"; + SendStatusDetails = "Houdini Node Sync - Send success with errors - Not all selected objects were created."; + } + } + + // Preset the existing Object Node ID to the unreal content node + CurrentInputObject->SetInputNodeId(-1); + CurrentInputObject->SetInputObjectNodeId(CurrentObjectNodeId); + + // TODO: Transform for actors? + FTransform CurrentActorTransform = FTransform::Identity; + + // For Textures. no need to create extra geometry like HDA inputs + if (CurrentObject->IsA()) + NodeSyncInput->SetExportMainGeometry(false); + else + NodeSyncInput->SetExportMainGeometry(true); + + // Send the HoudiniInputObject to H + if (!FHoudiniInputTranslator::UploadHoudiniInputObject( + NodeSyncInput, CurrentInputObject, CurrentActorTransform, CreatedNodeIds, Handles, false)) + { + HOUDINI_LOG_WARNING(TEXT("HoudiniNodeSync: Failed to send %s to %s."), *CurrentInputObject->GetName(), *SendNodePath); + + LastSendStatus = EHoudiniNodeSyncStatus::SuccessWithErrors; + SendStatusMessage = "Send Success with errors"; + SendStatusDetails = "Houdini Node Sync - Send success with errors - Not all selected objects were created."; + + continue; + } + + // Mark that input object as non dirty + CurrentInputObject->MarkChanged(false); + CurrentInputObject->MarkTransformChanged(false); + + // We've created the input nodes for this object, now, we need to object merge them into the content node in the path specified by the user + bool bObjMergeSuccess = false; + for (int32 CreatedNodeIdx = 0; CreatedNodeIdx < CreatedNodeIds.Num(); CreatedNodeIdx++) + { + // todo: if we've created more than one node, merge them together? + if (CreatedNodeIds.Num() > 1) + ObjectName += TEXT("_") + FString::FromInt(CreatedNodeIdx + 1); + + HAPI_NodeId ObjectMergeNodeId = -1; + HAPI_NodeId GeoObjectMergeNodeId = -1; + bObjMergeSuccess &= FHoudiniInputTranslator::HapiCreateOrUpdateGeoObjectMergeAndSetTransform( + CurrentObjectNodeId, CreatedNodeIds[CreatedNodeIdx], ObjectName, ObjectMergeNodeId, CurrentObjectNodeId, true, FTransform::Identity, 1); + } + } + + // Update status + if (LastSendStatus != EHoudiniNodeSyncStatus::SuccessWithErrors) + { + LastSendStatus = EHoudiniNodeSyncStatus::Success; + SendStatusMessage = "Send Success"; + SendStatusDetails = "Houdini Node Sync - Send success"; + } + + // TODO: Improve me! handle failures! + Notification = TEXT("Houdini Node Sync success!"); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // Start ticking if needed once we send something + if(NodeSyncOptions.bSyncWorldInput) + StartTicking(); +} + + +void +UHoudiniEditorNodeSyncSubsystem::UpdateAllSelection() +{ + LastSendStatus = EHoudiniNodeSyncStatus::Running; + SendStatusMessage = "Updating..."; + + // Get current world selection + TArray CurrentWorldSelection; + + // Make sure the node sync inputs are valid + InitNodeSyncInputsIfNeeded(); + + // Build an array of all previously sent actors + TArray>* InputObjects = NodeSyncWorldInput->GetHoudiniInputObjectArray(EHoudiniInputType::World); + if (InputObjects) + { + for (const auto& CurInputObject : *InputObjects) + { + CurrentWorldSelection.Add(CurInputObject->GetObject()); + } + } + + // Resend all the WorldSelection + SendToHoudini(CurrentWorldSelection, 0, true); + + // Rebuild the NodeSync selection view + TSharedPtr NSPanel = FHoudiniEngineEditor::Get().GetNodeSyncPanel(); + if (NSPanel && NSPanel.IsValid()) + NSPanel->RebuildSelectionView(); +} + +void +UHoudiniEditorNodeSyncSubsystem::DeleteAllSelection() +{ + LastSendStatus = EHoudiniNodeSyncStatus::Running; + SendStatusMessage = "Deleting..."; + + // Make sure the node sync input is valid + InitNodeSyncInputsIfNeeded(); + + // Shortly authorize the input to delete its node + NodeSyncWorldInput->SetCanDeleteHoudiniNodes(true); + + // Clean the world input + bool bReturn = FHoudiniInputTranslator::DisconnectAndDestroyInput(NodeSyncWorldInput, EHoudiniInputType::World); + TArray>* InputObjects = NodeSyncWorldInput->GetHoudiniInputObjectArray(EHoudiniInputType::World); + if (InputObjects) + InputObjects->Empty(); + + NodeSyncWorldInput->SetCanDeleteHoudiniNodes(false); + + if (bReturn) + { + LastSendStatus = EHoudiniNodeSyncStatus::Success; + SendStatusMessage = "Delete Success."; + SendStatusDetails = "Houdini Node Sync - Delete Success: Successfully deleted all sent data!"; + } + else + { + LastSendStatus = EHoudiniNodeSyncStatus::Failed; + SendStatusMessage = "Delete Failed."; + SendStatusDetails = "Houdini Node Sync - Delete Failed: Unable to delete all sent data!"; + } + + // Rebuild the NodeSync selection view + TSharedPtr NSPanel = FHoudiniEngineEditor::Get().GetNodeSyncPanel(); + if (NSPanel && NSPanel.IsValid()) + NSPanel->RebuildSelectionView(); +} + + + +void +UHoudiniEditorNodeSyncSubsystem::FetchFromHoudini() +{ + // Add a slate notification + FString Notification = TEXT("Fetching data from Houdini..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + LastFetchStatus = EHoudiniNodeSyncStatus::Running; + FetchStatusMessage = "Fetching..."; + + if (!CreateSessionIfNeeded()) + { + // For now, just warn the session is not session sync + HOUDINI_LOG_WARNING(TEXT("HoudiniNodeSync: the current session is not session-sync one!")); + + LastFetchStatus = EHoudiniNodeSyncStatus::Warning; + FetchStatusMessage = "warning: the current session is not Session-sync one!"; + FetchStatusDetails = FetchStatusMessage + "\nYou can start a Session-sync session by using the Open Session Sync entry in the Houdini Engine menu."; + } + + // We use the BGEO importer when Fetching to the content browser + bool bUseBGEOImport = !NodeSyncOptions.bFetchToWorld; + bool bSuccess = false; + + // Parse the fetch node path into a string array + // Multiple paths are separated by ; + TArray FetchNodePaths; + NodeSyncOptions.FetchNodePath.ParseIntoArray(FetchNodePaths, TEXT(";"), true); + + for (int32 PathIdx = 0; PathIdx < FetchNodePaths.Num(); PathIdx++) + { + // Only use the first one for now + FString CurrentFetchNodePath = FetchNodePaths[PathIdx]; + + // Make sure that the FetchNodePath is a valid Houdini node path pointing to a valid Node + HAPI_NodeId FetchNodeId = -1; + if (!ValidateFetchedNodePath(CurrentFetchNodePath, FetchNodeId)) + return; + + if (bUseBGEOImport) + { + // We need to gather all the required nodes + TArray FetchNodeIds; + if (!GatherAllFetchedNodeIds(FetchNodeId, NodeSyncOptions.bUseOutputNodes, FetchNodeIds)) + { + HOUDINI_LOG_ERROR(TEXT("Houdini Node Sync: Failed to gather fetch nodes.")); + LastFetchStatus = EHoudiniNodeSyncStatus::Failed; + FetchStatusMessage = "Failed: Unable to gather fetch node outputs!"; + FetchStatusDetails = "Houdini Node Sync - Fetch Failed - Unable to gather fetch node outputs."; + + return; + } + + // Make sure that all the required output nodes have been cooked + // This ensure that we'll be able to get the proper number of parts for them + HAPI_CookOptions CookOptions = FHoudiniEngine::GetDefaultCookOptions(); + for (auto CurrentNodeId : FetchNodeIds) + { + if (!FHoudiniEngineUtils::HapiCookNode(CurrentNodeId, &CookOptions, true)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to cook NodeSyncFetch node!")); + // Only log? still try to continue with the output processing? + // return; + } + } + + // Parent obj node that will contain all the merge nodes used for the import + // This will make cleaning up the fetch node easier + TArray CreatedNodeIds; + + // Create a new Geo importer + TArray> DummyOldOutputs; + TArray> NewOutputs; + UHoudiniGeoImporter* HoudiniGeoImporter = NewObject(this); + HoudiniGeoImporter->AddToRoot(); + + // Clean up lambda + auto CleanUp = [&NewOutputs, &HoudiniGeoImporter, &CreatedNodeIds]() + { + // Remove the importer and output objects from the root set + HoudiniGeoImporter->RemoveFromRoot(); + for (auto Out : NewOutputs) + Out->RemoveFromRoot(); + + // Delete the nodes created for the import + for(auto CurrentNodeId : CreatedNodeIds) + { + // Delete the parent node of the created nodes + FHoudiniEngineUtils::DeleteHoudiniNode( + FHoudiniEngineUtils::HapiGetParentNodeId(CurrentNodeId) + ); + } + }; + + // Failure lambda + auto FailImportAndReturn = [this, &CleanUp, &NewOutputs, &HoudiniGeoImporter]() + { + CleanUp(); + + this->LastFetchStatus = EHoudiniNodeSyncStatus::Failed; + this->FetchStatusMessage = "Failed"; + this->FetchStatusDetails = "Houdini Node Sync - Fetch Failed."; + + // TODO: Improve me! + FString Notification = TEXT("Houdini Node Sync - Fetch failed!"); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + return; + }; + + // Process each fetch node with the GeoImporter + for (auto CurrentFetchId : FetchNodeIds) + { + FString CurrentFetchPath; + if (!FHoudiniEngineUtils::HapiGetAbsNodePath(CurrentFetchId, CurrentFetchPath)) + continue; + + // Check if the node is a COP or COP2 node + HAPI_NodeType NodeType; + bool bGetNodeTypeSuccess = FHoudiniEngineUtils::HapiGetNodeType(CurrentFetchId, NodeType); + bool bIsCopNode = bGetNodeTypeSuccess && (NodeType == HAPI_NODETYPE_COP || NodeType == HAPI_NODETYPE_COP2); + + HAPI_NodeId CurrentOutputNodeId = -1; + if (bIsCopNode) + { + // Simply use the COP node as the output node + CurrentOutputNodeId = CurrentFetchId; + } + else + { + // Create an object merge node for the geo importer + if (!HoudiniGeoImporter->MergeGeoFromNode(CurrentFetchPath, CurrentOutputNodeId)) + return FailImportAndReturn(); + + // Keep track of the created merge node so we can delete it later on + CreatedNodeIds.Add(CurrentOutputNodeId); + } + + // 4. Get the output from the Fetch node + //TArray CurrentOutputs; + if (!HoudiniGeoImporter->BuildOutputsForNode(CurrentOutputNodeId, DummyOldOutputs, NewOutputs, NodeSyncOptions.bUseOutputNodes)) + return FailImportAndReturn(); + } + + // Prepare the package used for creating the mesh, landscape and instancer pacakges + FHoudiniPackageParams PackageParams; + PackageParams.PackageMode = EPackageMode::Bake; + PackageParams.TempCookFolder = FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder(); + PackageParams.HoudiniAssetName = NodeSyncOptions.GetFetchNodeNameAt(PathIdx); + PackageParams.BakeFolder = NodeSyncOptions.UnrealAssetFolder; + + PackageParams.ObjectName = NodeSyncOptions.GetUnrealAssetName(PathIdx); + PackageParams.HoudiniAssetActorName = NodeSyncOptions.GetUnrealActorLabel(PathIdx); + PackageParams.NameOverride = NodeSyncOptions.GetUnrealAssetName(PathIdx); + + if (NodeSyncOptions.bReplaceExisting) + { + PackageParams.ReplaceMode = EPackageReplaceMode::ReplaceExistingAssets; + } + else + { + PackageParams.ReplaceMode = EPackageReplaceMode::CreateNewAssets; + } + + // 5. Create all the objects using the outputs + const FHoudiniStaticMeshGenerationProperties& StaticMeshGenerationProperties = FHoudiniEngineRuntimeUtils::GetDefaultStaticMeshGenerationProperties(); + const FMeshBuildSettings& MeshBuildSettings = FHoudiniEngineRuntimeUtils::GetDefaultMeshBuildSettings(); + if (!HoudiniGeoImporter->CreateObjectsFromOutputs(NewOutputs, PackageParams, StaticMeshGenerationProperties, MeshBuildSettings)) + return FailImportAndReturn(); + + // Get our result object and "finalize" them + TArray Results = HoudiniGeoImporter->GetOutputObjects(); + for (UObject* Object : Results) + { + if (!IsValid(Object)) + continue; + + //GEditor->GetEditorSubsystem()->BroadcastAssetPostImport(this, Object); + Object->MarkPackageDirty(); + Object->PostEditChange(); + } + + CleanUp(); + + // Sync the content browser to the newly created assets + if (GEditor) + GEditor->SyncBrowserToObjects(Results); + + bSuccess = Results.Num() > 0; + } + else + { + // Spawn a new HoudiniActor with a HoudiniNodeSyncComponent + AActor* CreatedActor = nullptr; + + // Clean up lambda + auto CleanUp = [&CreatedActor]() + { + if (IsValid(CreatedActor)) + { + CreatedActor->Destroy(); + } + }; + + // Failure lambda + auto FailImportAndReturn = [this, &CleanUp, &CreatedActor]() + { + CleanUp(); + + this->LastFetchStatus = EHoudiniNodeSyncStatus::Failed; + this->FetchStatusMessage = "Failed"; + this->FetchStatusDetails = "Houdini Node Sync - Fetch Failed."; + + // TODO: Improve me! + FString Notification = TEXT("Houdini Node Sync - Fetch failed!"); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + return; + }; + + UWorld* World = GEditor->GetEditorWorldContext().World(); + if (NodeSyncOptions.bReplaceExisting) + { + // See if an Actor already exist for this node path + AHoudiniAssetActor* FoundActor = nullptr; + for (TActorIterator ActorIt(World, AHoudiniAssetActor::StaticClass(), EActorIteratorFlags::OnlyActiveLevels); ActorIt; ++ActorIt) + { + FoundActor = *ActorIt; + if (!FoundActor) + continue; + + UHoudiniNodeSyncComponent* FoundHNSC = Cast(FoundActor->GetHoudiniAssetComponent()); + if (!IsValid(FoundHNSC)) + continue; + + if (FoundHNSC->GetFetchNodePath() != CurrentFetchNodePath) + continue; + + // Re-use the found actor + CreatedActor = FoundActor; + } + } + + if (!IsValid(CreatedActor)) + { + // We need to create a new HoudiniAssetActor + // Get the asset Factory + UActorFactory* Factory = GEditor->FindActorFactoryForActorClass(AHoudiniAssetActor::StaticClass()); + if (!Factory) + return FailImportAndReturn(); + + // Spawn in the current world/level + ULevel* LevelToSpawnIn = World->GetCurrentLevel(); + CreatedActor = Factory->CreateActor(nullptr, LevelToSpawnIn, FHoudiniEngineEditorUtils::GetDefaulAssetSpawnTransform()); + } + + if (!IsValid(CreatedActor)) + return FailImportAndReturn(); + + // Ensure spawn was successful + AHoudiniAssetActor* HACActor = Cast(CreatedActor); + if (!IsValid(HACActor)) + return FailImportAndReturn(); + + UHoudiniAssetComponent* HAC = Cast(HACActor->GetRootComponent()); + if (!IsValid(HAC)) + return FailImportAndReturn(); + + // Remove the H logo here + FHoudiniEngineUtils::RemoveHoudiniLogoFromComponent(HAC); + + // This will convert the HoudiniAssetActor to a NodeSync one + HACActor->SetNodeSyncActor(true); + + // Check that we have a valid NodeSync component + UHoudiniNodeSyncComponent* HNSC = Cast(HACActor->GetHoudiniAssetComponent()); + if(!IsValid(HNSC)) + return FailImportAndReturn(); + + // Add the Houdini logo back to the NodeSync component + FHoudiniEngineUtils::AddHoudiniLogoToComponent(HNSC); + + // Set the Fetch node path on the component + HNSC->SetFetchNodePath(CurrentFetchNodePath); + + // Set the Node Sync options + UHoudiniCookable* HC = HNSC->GetCookable(); + if (!IsValid(HC)) + return FailImportAndReturn(); + + HC->SetCurrentState(EHoudiniAssetState::NewHDA); + + // Disable proxies + HC->SetOverrideGlobalProxyStaticMeshSettings(true); + HC->SetEnableProxyStaticMeshOverride(false); + + // AutoBake + HC->SetBakeAfterNextCook(NodeSyncOptions.bAutoBake ? EHoudiniBakeAfterNextCook::Always : EHoudiniBakeAfterNextCook::Disabled); + HC->SetRemoveOutputAfterBake(true); + + // Other options + HC->SetUseOutputNodes(NodeSyncOptions.bUseOutputNodes); + HC->SetReplacePreviousBake(NodeSyncOptions.bReplaceExisting); + HC->SetBakeFolderPath(NodeSyncOptions.UnrealAssetFolder); + + // Make sure we the actor has a unique name/label + FString ActorNameAndLabel = NodeSyncOptions.GetUnrealActorLabel(PathIdx); + // Try to find an existing actor of the desired name - make our name unique if we find one + AActor* NamedActor = FHoudiniEngineUtils::FindActorInWorldByLabelOrName(World, ActorNameAndLabel); + if(NamedActor == nullptr) + HACActor->SetActorLabel(NodeSyncOptions.GetUnrealActorLabel(PathIdx)); + else if(NamedActor != HACActor) + FHoudiniEngineUtils::RenameToUniqueActor(HACActor, NodeSyncOptions.GetUnrealActorLabel(PathIdx)); + + // Get its transform + FTransform FetchTransform; + if (FHoudiniEngineUtils::HapiGetAssetTransform(FetchNodeId, FetchTransform)) + { + // Assign the transform to the actor + HACActor->SetActorTransform(FetchTransform); + } + + // Select the Actor we just created + if (GEditor->CanSelectActor(CreatedActor, true, true)) + { + GEditor->SelectNone(true, true, false); + GEditor->SelectActor(CreatedActor, true, true, true); + } + + // Update the status message to fetching + this->LastFetchStatus = EHoudiniNodeSyncStatus::Running; + this->FetchStatusMessage = "Fetching"; + this->FetchStatusDetails = "Houdini Node Sync - Fetching data from Houdini Node \"" + CurrentFetchNodePath + "\"."; + + bSuccess = true; + } + } + + if (bSuccess) + { + // TODO: Improve me! + Notification = TEXT("Houdini Node Sync success!"); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + LastFetchStatus = EHoudiniNodeSyncStatus::Success; + FetchStatusMessage = "Fetch Success"; + FetchStatusDetails = "Houdini Node Sync - Successfully fetched data from Houdini"; + } + else + { + // TODO: Improve me! + Notification = TEXT("Houdini Node Sync failed!"); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + LastFetchStatus = EHoudiniNodeSyncStatus::Failed; + FetchStatusMessage = "Fetch Failed"; + FetchStatusDetails = "Houdini Node Sync - Failed fetching data from Houdini"; + } +} + + +bool +UHoudiniEditorNodeSyncSubsystem::GatherAllFetchedNodeIds( + HAPI_NodeId InFetchNodeId, + const bool bUseOutputNodes, + TArray& OutOutputNodes) +{ + // This function behaves similarly to FHoudiniEngineUtils::GatherAllAssetOutputs() + // With a few NodeSync specific twists: + // - does not require an asset/asset info + // - does not care about editable/templated nodes + // - ignore object visibility + + // Get the NodeInfo for the fetch node + HAPI_NodeInfo FetchNodeInfo; + FHoudiniApi::NodeInfo_Init(&FetchNodeInfo); + HOUDINI_CHECK_ERROR_RETURN(FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), InFetchNodeId, &FetchNodeInfo), false); + + // If the node is neither SOP nor OBJ nor COP nor COP2, stop there + if (FetchNodeInfo.type != HAPI_NODETYPE_SOP + && FetchNodeInfo.type != HAPI_NODETYPE_OBJ + && FetchNodeInfo.type != HAPI_NODETYPE_COP + && FetchNodeInfo.type != HAPI_NODETYPE_COP2) + { + HOUDINI_LOG_ERROR(TEXT("Houdini Node Sync: Invalid fetch node type - the node should be either a SOP or OBJ node.")); + return false; + } + + // For non-container/non-subnet SOP nodes, or any COP nodes, no need to look further, just use the node itself + const bool bAssetHasChildren = !(FetchNodeInfo.type == HAPI_NODETYPE_SOP && FetchNodeInfo.childNodeCount == 0); + const bool bAssetIsCop = FetchNodeInfo.type == HAPI_NODETYPE_COP || FetchNodeInfo.type == HAPI_NODETYPE_COP2; + if (!bAssetHasChildren || bAssetIsCop) + { + OutOutputNodes.AddUnique(InFetchNodeId); + return true; + } + + // Retrieve information about each object contained within our asset. + TArray ObjectInfos; + TArray ObjectTransforms; + if (!FHoudiniEngineUtils::HapiGetObjectInfos(InFetchNodeId, ObjectInfos, ObjectTransforms)) + { + HOUDINI_LOG_ERROR(TEXT("Houdini Node Sync: Fetch failed - Unable to get object infos for the node.")); + return false; + } + + bool bUseOutputFromSubnets = false; + if (bAssetHasChildren && !FHoudiniEngineUtils::ContainsSopNodes(InFetchNodeId)) + { + // Assume we're using a subnet-based HDA + bUseOutputFromSubnets = true; + } + + // Before we can perform visibility checks on the Object nodes, we have + // to build a set of all the Object node ids. The 'AllObjectIds' act + // as a visibility filter. If an Object node is not present in this + // list, the content of that node will not be displayed (display / output / templated nodes). + // NOTE that if the HDA contains immediate SOP nodes we will ignore + // all subnets and only use the data outputs directly from the HDA. + TSet AllObjectIds; + if (bUseOutputFromSubnets) + { + int NumObjSubnets; + TArray ObjectIds; + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), + InFetchNodeId, + HAPI_NODETYPE_OBJ, + HAPI_NODEFLAGS_OBJ_SUBNET | HAPI_NODEFLAGS_NON_BYPASS, + true, + &NumObjSubnets + ), + false); + + ObjectIds.SetNumUninitialized(NumObjSubnets); + HOUDINI_CHECK_ERROR_RETURN( + FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), + InFetchNodeId, + ObjectIds.GetData(), + NumObjSubnets + ), + false); + AllObjectIds.Append(ObjectIds); + } + else + { + AllObjectIds.Add(InFetchNodeId); + } + + // Iterate through all objects to determine visibility and + // gather output nodes that needs to be cooked. + int32 OutputIdx = 1; + const bool bIsSopAsset = FetchNodeInfo.type == HAPI_NODETYPE_SOP; + for (int32 ObjectIdx = 0; ObjectIdx < ObjectInfos.Num(); ObjectIdx++) + { + // Retrieve the object info + const HAPI_ObjectInfo& CurrentHapiObjectInfo = ObjectInfos[ObjectIdx]; + + // Determine whether this object node is fully visible. + HAPI_NodeId GatherOutputsNodeId = -1; // Outputs will be gathered from this node. + if (!bAssetHasChildren) + { + // If the asset doesn't have children, we have to gather outputs from the asset's parent in order to output + // this asset node + GatherOutputsNodeId = FetchNodeInfo.parentId; + } + else if (bIsSopAsset) + { + // When dealing with a SOP asset, be sure to gather outputs from the SOP node, not the + // outer object node. + GatherOutputsNodeId = InFetchNodeId; + } + else + { + GatherOutputsNodeId = CurrentHapiObjectInfo.nodeId; + } + + // Build an array of the geos we'll need to process + // In most case, it will only be the display geo + TArray GeoInfos; + + // These node ids may need to be cooked in order to extract part counts. + TSet CurrentOutGeoNodeIds; + // NOTE: The HAPI_GetDisplayGeoInfo will not always return the expected Geometry subnet's + // Display flag geometry. If the Geometry subnet contains an Object subnet somewhere, the + // GetDisplayGeoInfo will sometimes fetch the display SOP from within the subnet which is + // not what we want. + + // Resolve and gather outputs (display / output / template nodes) from the GatherOutputsNodeId. + FHoudiniEngineUtils::GatherImmediateOutputGeoInfos(GatherOutputsNodeId, + bUseOutputNodes, + false, + GeoInfos, + CurrentOutGeoNodeIds); + + // Add them to our global output node list + for (const HAPI_NodeId& NodeId : CurrentOutGeoNodeIds) + { + OutOutputNodes.AddUnique(NodeId); + } + } + + return true; +} + + +FLinearColor +UHoudiniEditorNodeSyncSubsystem::GetStatusColor(const EHoudiniNodeSyncStatus& Status) +{ + FLinearColor OutStatusColor = FLinearColor::White; + + switch (Status) + { + case EHoudiniNodeSyncStatus::None: + // Nothing done yet + OutStatusColor = FLinearColor::White; + break; + + case EHoudiniNodeSyncStatus::Failed: + // Last operation failed + OutStatusColor = FLinearColor::Red; + break; + + case EHoudiniNodeSyncStatus::Success: + // Last operation was successfull + OutStatusColor = FLinearColor::Green; + break; + + case EHoudiniNodeSyncStatus::SuccessWithErrors: + // Last operation was succesfull, but reported errors + OutStatusColor = FLinearColor(1.0f, 0.647f, 0.0f); + break; + + case EHoudiniNodeSyncStatus::Running: + // Fetching/Sending + OutStatusColor = FLinearColor(0.0f, 0.749f, 1.0f); + break; + + case EHoudiniNodeSyncStatus::Warning: + // Display a warning + OutStatusColor = FLinearColor(1.0f, 0.647f, 0.0f); + break; + } + + return OutStatusColor; +} + + +bool +UHoudiniEditorNodeSyncSubsystem::ValidateFetchedNodePath(const FString& InFetchNodePath, HAPI_NodeId& OutFetchedNodeId) +{ + // Make sure that the FetchNodePath is a valid Houdini node path pointing to a valid Node + OutFetchedNodeId = -1; + + // Make sure we're not trying to fetch /obj, as this seems to crash HE + if (InFetchNodePath.Equals("/obj", ESearchCase::IgnoreCase) + || InFetchNodePath.Equals("/obj/", ESearchCase::IgnoreCase)) + { + HOUDINI_LOG_ERROR(TEXT("Invalid FetchNodePath")); + LastFetchStatus = EHoudiniNodeSyncStatus::Failed; + FetchStatusMessage = "Failed: Invalid Fetch node path!"; + FetchStatusDetails = "Houdini Node Sync - Fetch Failed - Unable to fetch /obj/."; + + return false; + } + + // Get the node ID for the given path + HAPI_Result Result = FHoudiniApi::GetNodeFromPath( + FHoudiniEngine::Get().GetSession(), -1, TCHAR_TO_ANSI(*InFetchNodePath), &OutFetchedNodeId); + if ((Result != HAPI_RESULT_SUCCESS) || (OutFetchedNodeId < 0)) + { + HOUDINI_LOG_ERROR(TEXT("Invalid FetchNodePath")); + LastFetchStatus = EHoudiniNodeSyncStatus::Failed; + FetchStatusMessage = "Failed: Invalid Fetch node path!"; + FetchStatusDetails = "Houdini Node Sync - Fetch Failed - The Fetch node path is invalid."; + + return false; + } + + return true; +} + + +bool +UHoudiniEditorNodeSyncSubsystem::CheckNodeSyncInputNodesValid() +{ + if (!IsValid(NodeSyncWorldInput)) + return false; + + // No need to tick if we dont have any input objects + if (NodeSyncWorldInput->GetNumberOfInputObjects(EHoudiniInputType::World) <= 0) + { + StopTicking(); + return false; + } + + const TArray>* InputObjects = NodeSyncWorldInput->GetHoudiniInputObjectArray(EHoudiniInputType::World); + if (!InputObjects) + return false; + + bool bReturn = false; + for (const auto& CurInputObject : *InputObjects) + { + if (FHoudiniEngineUtils::IsHoudiniNodeValid(CurInputObject->GetInputObjectNodeId())) + continue; + + CurInputObject->MarkChanged(true); + bReturn = true; + } + + return bReturn; +} + +bool +UHoudiniEditorNodeSyncSubsystem::UpdateNodeSyncInputs() +{ + if (!IsValid(NodeSyncWorldInput)) + return false; + + // No need to tick if we dont have any input objects + if (NodeSyncWorldInput->GetNumberOfInputObjects(EHoudiniInputType::World) <= 0) + { + StopTicking(); + return true; + } + + // See if we need to update some of the node sync inputs + if (!FHoudiniInputTranslator::UpdateWorldInput(NodeSyncWorldInput)) + return false; + + if (!NodeSyncWorldInput->NeedsToTriggerUpdate()) + return false; + + bool bSuccess = true; + if (NodeSyncWorldInput->IsDataUploadNeeded()) + { + TArray>* InputObjectsArray = NodeSyncWorldInput->GetHoudiniInputObjectArray(EHoudiniInputType::World); + + // Iterate on all the input objects and see if they need to be uploaded + TArray CreatedNodeIds; + TSet Handles; + TArray ValidNodeIds; + TArray ChangedInputObjects; + for (int32 ObjIdx = 0; ObjIdx < InputObjectsArray->Num(); ObjIdx++) + { + UHoudiniInputObject* CurrentInputObject = (*InputObjectsArray)[ObjIdx]; + if (!IsValid(CurrentInputObject)) + continue; + + ValidNodeIds.Reset(); + ChangedInputObjects.Reset(); + // The input object could have child objects: GetChangedObjectsAndValidNodes finds if the object itself or + // any its children has changed, and also returns the NodeIds of those objects that are still valid and + // unchanged + CurrentInputObject->GetChangedObjectsAndValidNodes(ChangedInputObjects, ValidNodeIds); + + // Keep track of the node ids for unchanged objects that already exist + if (ValidNodeIds.Num() > 0) + CreatedNodeIds.Append(ValidNodeIds); + + // Upload the changed input objects + for (UHoudiniInputObject* ChangedInputObject : ChangedInputObjects) + { + // Upload the current input object to Houdini + if (!FHoudiniInputTranslator::UploadHoudiniInputObject( + NodeSyncWorldInput, ChangedInputObject, FTransform::Identity, CreatedNodeIds, Handles, ChangedInputObject->CanDeleteHoudiniNodes())) + bSuccess = false; + } + } + + /* + // We've created the input nodes for this object, now, we need to object merge them into the content node in the path specified by the user + bool bObjMergeSuccess = false; + for (int32 CreatedNodeIdx = 0; CreatedNodeIdx < CreatedNodeIds.Num(); CreatedNodeIdx++) + { + // todo: if we've created more than one node, merge them together? + if (CreatedNodeIds.Num() > 1) + ObjectName += TEXT("_") + FString::FromInt(CreatedNodeIdx + 1); + + HAPI_NodeId ObjectMergeNodeId = -1; + HAPI_NodeId GeoObjectMergeNodeId = -1; + bObjMergeSuccess &= FHoudiniInputTranslator::HapiCreateOrUpdateGeoObjectMergeAndSetTransform( + CurrentObjectNodeId, CreatedNodeIds[CreatedNodeIdx], ObjectName, ObjectMergeNodeId, CurrentObjectNodeId, true, FTransform::Identity, 1); + } + */ + + NodeSyncWorldInput->MarkDataUploadNeeded(!bSuccess); + } + + if (NodeSyncWorldInput->IsTransformUploadNeeded()) + { + bSuccess &= FHoudiniInputTranslator::UploadInputTransform(NodeSyncWorldInput); + } + + // Update the input properties AFTER eventually uploading it + bSuccess = FHoudiniInputTranslator::UpdateInputProperties(NodeSyncWorldInput); + + if (bSuccess) + { + NodeSyncWorldInput->MarkChanged(false); + NodeSyncWorldInput->MarkAllInputObjectsChanged(false); + } + + if (NodeSyncWorldInput->HasInputTypeChanged()) + NodeSyncWorldInput->SetPreviousInputType(EHoudiniInputType::Invalid); + + // Even if we failed, no need to try updating again. + NodeSyncWorldInput->SetNeedsToTriggerUpdate(false); + + return true; +} + +void +UHoudiniEditorNodeSyncSubsystem::StartTicking() +{ + // If we have no timer delegate spawned, spawn one. + if (!TickerHandle.IsValid() && GEditor) + { + // We use the ticker manager so we get ticked once per frame, no more. + //TickerHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this, &UHoudiniEditorNodeSyncSubsystem::Tick)); + + TickerHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([this](float DeltaTime) { return this->Tick(DeltaTime); })); + + // Grab current time for delayed notification. + //FHoudiniEngine::Get().SetHapiNotificationStartedTime(FPlatformTime::Seconds()); + } + + dLastTick = 0.0; +} + +void +UHoudiniEditorNodeSyncSubsystem::StopTicking() +{ + if (TickerHandle.IsValid() && GEditor) + { + if (IsInGameThread()) + { + FTSTicker::GetCoreTicker().RemoveTicker(TickerHandle); + TickerHandle.Reset(); + + bMustStopTicking = false; + dLastTick = 0.0; + } + else + { + // We can't stop ticking now as we're not in the game Thread, + // and accessing the timer would crash, indicate that we want to stop ticking asap + // This can happen when loosing a session due to a Houdini crash + bMustStopTicking = true; + } + } +} + +bool +UHoudiniEditorNodeSyncSubsystem::IsTicking() const +{ + return TickerHandle.IsValid(); +} + +bool +UHoudiniEditorNodeSyncSubsystem::Tick(float DeltaTime) +{ + if (bMustStopTicking) + { + // Ticking should be stopped immediately + StopTicking(); + return true; + } + + double dNow = FPlatformTime::Seconds(); + if ((dNow - dLastTick) < 1.0) + return true; + + UpdateNodeSyncInputs(); + + dLastTick = dNow; + + return true; +} +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineCommands.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineCommands.h" + +#include "HoudiniEngineEditorPrivatePCH.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniAssetActor.h" +#include "HoudiniCookable.h" +#include "HoudiniOutputTranslator.h" +#include "HoudiniStaticMesh.h" +#include "HoudiniOutput.h" +#include "HoudiniEngineStyle.h" +#include "HoudiniEngineDetails.h" +#include "UnrealObjectInputManager.h" + +#include "DesktopPlatformModule.h" +#include "Interfaces/IMainFrameModule.h" +#include "EditorDirectories.h" +#include "Misc/ScopedSlowTask.h" +#include "Async/Async.h" +#include "FileHelpers.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Engine/ObjectLibrary.h" +#include "ObjectTools.h" +#include "CoreGlobals.h" +#include "HoudiniEngineOutputStats.h" +#include "Misc/FeedbackContext.h" +#include "HAL/FileManager.h" +#include "Modules/ModuleManager.h" +#include "Interfaces/IPluginManager.h" +#include "ISettingsModule.h" +#include "UObject/ObjectSaveContext.h" +//#include "UObject/ObjectSaveContext.h" +#include "LevelEditor.h" +#include "UObject/UObjectIterator.h" +#if defined(HOUDINI_USE_PCG) +#include "HoudiniPCGUtils.h" +#endif +#include "Trace/StoreClient.h" + +#include "IContentBrowserSingleton.h" +#include "ContentBrowserModule.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +FHoudiniEngineCommands::FHoudiniEngineCommands() + : TCommands (TEXT("HoudiniEngine"), NSLOCTEXT("Contexts", "HoudiniEngine", "Houdini Engine Plugin"), NAME_None, FHoudiniEngineStyle::GetStyleSetName()) +{ +} + +void +FHoudiniEngineCommands::RegisterCommands() +{ + UI_COMMAND(_CreateSession, "Create Session", "Creates a new Houdini Engine session.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_ConnectSession, "Connect Session", "Connects to an existing Houdini Engine session.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_StopSession, "Stop Session", "Stops the current Houdini Engine session.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_RestartSession, "Restart Session", "Restarts the current Houdini Engine session.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_OpenSessionSync, "Open Houdini Session Sync...", "Opens Houdini with Session Sync and connect to it.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_CloseSessionSync, "Close Houdini Session Sync", "Close the Session Sync Houdini.", EUserInterfaceActionType::Button, FInputChord()); + + // Viewport Sync + UI_COMMAND(_ViewportSyncNone, "Disabled", "Do not sync viewports.", EUserInterfaceActionType::Check, FInputChord()); + UI_COMMAND(_ViewportSyncUnreal, "Sync Unreal to Houdini.", "Sync the Unreal viewport to Houdini's.", EUserInterfaceActionType::Check, FInputChord()); + UI_COMMAND(_ViewportSyncHoudini, "Sync Houdini to Unreal", "Sync the Houdini viewport to Unreal's.", EUserInterfaceActionType::Check, FInputChord()); + UI_COMMAND(_ViewportSyncBoth, "Both", "Sync both Unreal and Houdini's viewport.", EUserInterfaceActionType::Check, FInputChord()); + + //NodeSync + UI_COMMAND(_OpenNodeSync, "Houdini Node Sync...", "Opens the Houdini Node Sync Panel.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_OpenHoudiniTools, "Houdini Tools...", "Opens the Houdini Tools Panel.", EUserInterfaceActionType::Button, FInputChord()); + + // PDG Import Commandlet + UI_COMMAND(_StartPDGCommandlet, "Start Async Importer", "Start the commandlet that imports PDG BGEO results in the background.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_StopPDGCommandlet, "Stop Async Importer", "Stops the commandlet that imports PDG BGEO results in the background.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_IsPDGCommandletEnabled, "Enable Async Importer", "Enables the commandlet that imports PDG BGEO results in the background.", EUserInterfaceActionType::Check, FInputChord()); + + UI_COMMAND(_InstallInfo, "Installation Info...", "Display information on the current Houdini Engine installation", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_PluginSettings, "Plugin Settings...", "Displays the Houdini Engine plugin project settings", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_PluginEditorSettings, "Plugin Editor Preferences...", "Displays the Houdini Engine plugin editor preferences", EUserInterfaceActionType::Button, FInputChord()); + + UI_COMMAND(_OpenInHoudini, "Open scene in Houdini...", "Opens the current Houdini scene in Houdini.", EUserInterfaceActionType::Button, FInputChord(EKeys::O, EModifierKey::Control | EModifierKey::Alt)); + UI_COMMAND(_SaveHIPFile, "Save Houdini scene (HIP)", "Saves a .hip file of the current Houdini scene.", EUserInterfaceActionType::Button, FInputChord()); + + UI_COMMAND(_ContentExampleGit, "Content Example...", "Opens the GitHub repository that contains the plugin's content examples.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_ContentExampleBrowseTo, "Browse Content Examples...", "Browse to the installed content example folder in the current project (if installed).", EUserInterfaceActionType::Button, FInputChord()); + + UI_COMMAND(_OnlineDoc, "Online Documentation...", "Go to the plugin's online documentation.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_OnlineForum, "Online Forum...", "Go to the plugin's online forum.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_ReportBug, "Report a bug...", "Report a bug for Houdini Engine for Unreal plugin.", EUserInterfaceActionType::Button, FInputChord()); + + UI_COMMAND(_CookAll, "Recook All", "Recooks all Houdini Assets Actors in the current level.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_CookSelected, "Recook Selection", "Recooks selected Houdini Asset Actors in the current level.", EUserInterfaceActionType::Button, FInputChord(EKeys::C, EModifierKey::Control | EModifierKey::Alt)); + + UI_COMMAND(_RebuildAll, "Rebuild All", "Rebuilds all Houdini Assets Actors in the current level.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_RebuildSelected, "Rebuild Selection", "Rebuilds selected Houdini Asset Actors in the current level.", EUserInterfaceActionType::Button, FInputChord(EKeys::R, EModifierKey::Control | EModifierKey::Alt)); + + UI_COMMAND(_BakeAll, "Bake And Replace All Houdini Assets", "Bakes and replaces with blueprints all Houdini Assets in the scene.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_BakeSelected, "Bake And Replace Selection", "Bakes and replaces with blueprints selected Houdini Asset Actors in the current level.", EUserInterfaceActionType::Button, FInputChord(EKeys::B, EModifierKey::Control | EModifierKey::Alt)); + + UI_COMMAND(_RefineAll, "Refine all Houdini Proxy Meshes To Static Meshes", "Builds and replaces all Houdini proxy meshes with UStaticMesh instances.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_RefineSelected, "Refine selected Houdini Proxy Meshes To Static Meshes", "Builds and replaces selected Houdini proxy meshes with UStaticMesh instances.", EUserInterfaceActionType::Button, FInputChord()); + + UI_COMMAND(_CleanUpTempFolder, "Clean Houdini Engine Temp Folder", "Deletes the unused temporary files in the Temporary Cook Folder.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(_PauseAssetCooking, "Pause Houdini Engine Cooking", "When activated, prevents Houdini Engine from cooking assets until unpaused.", EUserInterfaceActionType::Check, FInputChord(EKeys::P, EModifierKey::Control | EModifierKey::Alt)); +} + +void +FHoudiniEngineCommands::SaveHIPFile() +{ + if (!FHoudiniEngine::IsInitialized() || FHoudiniEngine::Get().GetSession() == nullptr) + { + HOUDINI_LOG_ERROR(TEXT("Cannot save the Houdini scene, the Houdini Engine session hasn't been started.")); + return; + } + + IDesktopPlatform * DesktopPlatform = FDesktopPlatformModule::Get(); + if (!DesktopPlatform || !FHoudiniEngineUtils::IsInitialized()) + return; + + TArray SaveFilenames; + bool bSaved = false; + void * ParentWindowWindowHandle = NULL; + + IMainFrameModule & MainFrameModule = FModuleManager::LoadModuleChecked< IMainFrameModule >(TEXT("MainFrame")); + const TSharedPtr< SWindow > & MainFrameParentWindow = MainFrameModule.GetParentWindow(); + if (MainFrameParentWindow.IsValid() && MainFrameParentWindow->GetNativeWindow().IsValid()) + ParentWindowWindowHandle = MainFrameParentWindow->GetNativeWindow()->GetOSWindowHandle(); + + FString FileType = TEXT("Houdini HIP file|*.hip"); + if(FHoudiniEngine::Get().IsLicenseIndie()) + FileType = TEXT("Houdini HIP file (Limited Commerical)|*.hiplc"); + else if(FHoudiniEngine::Get().IsLicenseEducation()) + FileType = TEXT("Houdini HIP file (Non Commercial)|*.hipnc"); + + bSaved = DesktopPlatform->SaveFileDialog( + ParentWindowWindowHandle, + NSLOCTEXT("SaveHIPFile", "SaveHIPFile", "Saves a .hip file of the current Houdini scene.").ToString(), + *(FEditorDirectories::Get().GetLastDirectory(ELastDirectory::GENERIC_EXPORT)), + TEXT(""), + FileType, + EFileDialogFlags::None, + SaveFilenames); + + if(bSaved && SaveFilenames.Num()) + { + // Add a slate notification + FString Notification = TEXT("Saving internal Houdini scene..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // ... and a log message + HOUDINI_LOG_MESSAGE(TEXT("Saved Houdini scene to %s"), *SaveFilenames[0]); + + // Get first path. + std::string HIPPathConverted(H_TCHAR_TO_UTF8(*SaveFilenames[0])); + + // Save HIP file through Engine. + FHoudiniApi::SaveHIPFile(FHoudiniEngine::Get().GetSession(), HIPPathConverted.c_str(), false); + } +} + +void +FHoudiniEngineCommands::OpenInHoudini() +{ + if(!FHoudiniEngine::IsInitialized() || FHoudiniEngine::Get().GetSession() == nullptr) + { + HOUDINI_LOG_ERROR(TEXT("Cannot open the scene in Houdini, the Houdini Engine session hasn't been started.")); + return; + } + + FString FileExtension = TEXT(".hip"); + if (FHoudiniEngine::Get().IsLicenseIndie()) + FileExtension = TEXT(".hiplc"); + else if (FHoudiniEngine::Get().IsLicenseEducation()) + FileExtension = TEXT(".hipnc"); + + // First, saves the current scene as a hip file + // Creates a proper temporary file name + FString UserTempPath = FPaths::CreateTempFilename( + FPlatformProcess::UserTempDir(), + TEXT("HoudiniEngine"), *FileExtension); + + // Save HIP file through Engine. + std::string TempPathConverted(H_TCHAR_TO_UTF8(*UserTempPath)); + FHoudiniApi::SaveHIPFile( + FHoudiniEngine::Get().GetSession(), + TempPathConverted.c_str(), false); + + if (!FPaths::FileExists(UserTempPath)) + return; + + // Add a slate notification + FString Notification = TEXT("Opening scene in Houdini..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // Add quotes to the path to avoid issues with spaces + UserTempPath = TEXT("\"") + UserTempPath + TEXT("\""); + + // Set custom $HOME env var if it's been specified + FHoudiniEngineRuntimeUtils::SetHoudiniHomeEnvironmentVariable(); + + // Then open the hip file in Houdini + FString LibHAPILocation = FHoudiniEngine::Get().GetLibHAPILocation(); + FString HoudiniExecutable = FHoudiniEngine::Get().GetHoudiniExecutable(); + FString HoudiniLocation = LibHAPILocation + TEXT("//") + HoudiniExecutable; + + FProcHandle ProcHandle = FPlatformProcess::CreateProc( + *HoudiniLocation, + *UserTempPath, + true, false, false, + nullptr, 0, + *FPlatformProcess::GetCurrentWorkingDirectory(), + nullptr, nullptr); + + if (!ProcHandle.IsValid()) + { + // Try with the steam version executable instead + HoudiniLocation = LibHAPILocation + TEXT("//hindie.steam"); + + ProcHandle = FPlatformProcess::CreateProc( + *HoudiniLocation, + *UserTempPath, + true, false, false, + nullptr, 0, + *FPlatformProcess::GetCurrentWorkingDirectory(), + nullptr, nullptr); + + if (!ProcHandle.IsValid()) + { + HOUDINI_LOG_ERROR(TEXT("Failed to open scene in Houdini.")); + } + } + + // ... and a log message + HOUDINI_LOG_MESSAGE(TEXT("Opened scene in Houdini.")); +} + +void +FHoudiniEngineCommands::ReportBug() +{ + FPlatformProcess::LaunchURL(HAPI_UNREAL_BUG_REPORT_URL, nullptr, nullptr); +} + +void +FHoudiniEngineCommands::ShowInstallInfo() +{ + FHoudiniEngineDetails::CreateInstallInfoWindow(); +} + +void +FHoudiniEngineCommands::ShowPluginSettings() +{ + FModuleManager::LoadModuleChecked("Settings").ShowViewer(FName("Project"), FName("Plugins"), FName("HoudiniEngine")); +} + +void +FHoudiniEngineCommands::ShowPluginEditorSettings() +{ + FModuleManager::LoadModuleChecked("Settings").ShowViewer(FName("Editor"), FName("Plugins"), FName("HoudiniEngine")); +} + +void +FHoudiniEngineCommands::OpenContentExampleGit() +{ + FPlatformProcess::LaunchURL(HAPI_UNREAL_CONTENT_EXAMPLES_URL, nullptr, nullptr); +} + +void +FHoudiniEngineCommands::BrowseToContentExamples() +{ + TSharedPtr Plugin = IPluginManager::Get().FindPlugin(TEXT("HoudiniEngineExamples")); + if (!Plugin.IsValid())// || !Plugin->IsEnabled()) + return; + + // Get the ContentExample's folder + //FString CEFolder = Plugin->GetContentDir() + "/ContentExamples/Maps"; + FString CEFolder = "/HoudiniEngineExamples/ContentExamples/Maps"; + + TArray FolderList; + FolderList.Push(CEFolder); + + FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked("ContentBrowser"); + ContentBrowserModule.Get().FocusPrimaryContentBrowser(false); + ContentBrowserModule.Get().ForceShowPluginContent(true); + //ContentBrowserModule.Get().SetSelectedPaths(FolderList, true); + ContentBrowserModule.Get().SyncBrowserToFolders(FolderList, true, true); +} + +bool +FHoudiniEngineCommands::HasContentExamples() +{ + TSharedPtr Plugin = IPluginManager::Get().FindPlugin(TEXT("HoudiniEngineExamples")); + if (!Plugin.IsValid()) + return false; + + if (!Plugin->IsEnabled()) + return false; + + return true; +} + +void +FHoudiniEngineCommands::OnlineDocumentation() +{ + FPlatformProcess::LaunchURL(HAPI_UNREAL_ONLINE_DOC_URL, nullptr, nullptr); +} + +void +FHoudiniEngineCommands::OnlineForum() +{ + FPlatformProcess::LaunchURL(HAPI_UNREAL_ONLINE_FORUM_URL, nullptr, nullptr); +} + +void +FHoudiniEngineCommands::CleanUpTempFolder() +{ + // TODO: Improve me! slow now that we also have SM saved in the temp directory + // Due to the ref, we probably iterate a little too much, and should maybe do passes following the order of refs: + // mesh first, then materials, then textures. + // have a look at UWrangleContentCommandlet as well + + // Add a slate notification + FString Notification = TEXT("Cleaning up Houdini Engine temporary folder..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + GWarn->BeginSlowTask(LOCTEXT("CleanUpTemp", "Cleaning up the Houdini Engine Temp Folder"), false, false); + + // Get the default temp cook folder + FString TempCookFolder = FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder(); + + TArray TempCookFolders; + TempCookFolders.Add(FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder()); + for (TObjectIterator It; It; ++It) + { + FString CookFolder = It->GetTemporaryCookFolder().Path; + if (CookFolder.IsEmpty()) + continue; + + TempCookFolders.AddUnique(CookFolder); + } + + // The Asset registry will help us finding if the content of the asset is referenced + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + + int32 DeletedCount = 0; + bool bDidDeleteAsset = true; + while (bDidDeleteAsset) + { + // To correctly clean the temp folder, we need to iterate multiple times, because some of the temp assets + // might be referenced by other temp assets.. (ie Textures are referenced by Materials) + // We'll stop looking for assets to delete when no deletion occured. + bDidDeleteAsset = false; + + TArray AssetDataList; + for (auto& TempFolder : TempCookFolders) + { + // The Object library will list all UObjects found in the TempFolder + auto ObjectLibrary = UObjectLibrary::CreateLibrary(UObject::StaticClass(), false, true); + ObjectLibrary->LoadAssetDataFromPath(TempFolder); + + // Get all the assets found in the TEMP folder + TArray CurrentAssetDataList; + ObjectLibrary->GetAssetDataList(CurrentAssetDataList); + + AssetDataList.Append(CurrentAssetDataList); + } + + // All the assets we're going to delete + TArray AssetDataToDelete; + for (FAssetData Data : AssetDataList) + { + UPackage* CurrentPackage = Data.GetPackage(); + if (!IsValid(CurrentPackage)) + continue; + + // Do not try to delete the package if it's referenced anywhere + TArray ReferenceNames; + AssetRegistryModule.Get().GetReferencers(CurrentPackage->GetFName(), ReferenceNames, UE::AssetRegistry::EDependencyCategory::All); + + if (ReferenceNames.Num() > 0) + continue; + + bool bAssetDataSafeToDelete = true; + TArray AssetsInPackage; + AssetRegistryModule.Get().GetAssetsByPackageName(CurrentPackage->GetFName(), AssetsInPackage); + for (const auto& AssetInfo : AssetsInPackage) + { + // Check if the objects contained in the package are referenced by something that won't be garbage collected (*including* the undo buffer) + UObject* AssetInPackage = AssetInfo.GetAsset(); + if (!IsValid(AssetInPackage)) + continue; + + FReferencerInformationList ReferencesIncludingUndo; + bool bReferencedInMemoryOrUndoStack = IsReferenced( + AssetInPackage, + GARBAGE_COLLECTION_KEEPFLAGS, +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + EInternalObjectFlags_GarbageCollectionKeepFlags, +#else + EInternalObjectFlags::GarbageCollectionKeepFlags, +#endif + true, + &ReferencesIncludingUndo); + + if (!bReferencedInMemoryOrUndoStack) + continue; + + // We do have external references, check if the external references are in our ObjectToDelete list + // If they are, we can delete the asset because its references are going to be deleted as well. + for (auto ExtRef : ReferencesIncludingUndo.ExternalReferences) + { + UObject* Outer = ExtRef.Referencer->GetOuter(); + if (!IsValid(Outer)) + continue; + + bool bOuterFound = false; + for (auto DataToDelete : AssetDataToDelete) + { + if (DataToDelete.GetPackage() == Outer) + { + bOuterFound = true; + break; + } + else if (DataToDelete.GetAsset() == Outer) + { + bOuterFound = true; + break; + } + } + + // We have at least one reference that's not going to be deleted, we have to keep the asset + if (!bOuterFound) + { + bAssetDataSafeToDelete = false; + break; + } + } + } + + if (bAssetDataSafeToDelete) + AssetDataToDelete.Add(Data); + } + + // Nothing to delete + if (AssetDataToDelete.Num() <= 0) + break; + + int32 CurrentDeleted = ObjectTools::DeleteAssets(AssetDataToDelete, false); + + if (CurrentDeleted > 0) + { + DeletedCount += CurrentDeleted; + bDidDeleteAsset = true; + } + } + + + // Now, go through all the directories in the temp directories and delete all the empty ones + IFileManager& FM = IFileManager::Get(); + // Lambda that parses a directory recursively and returns true if it is empty + auto IsEmptyFolder = [&FM](FString PathToDeleteOnDisk) + { + struct FEmptyFolderVisitor : public IPlatformFile::FDirectoryVisitor + { + bool bIsEmpty; + FEmptyFolderVisitor() + : bIsEmpty(true) + { + } + + virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override + { + if (!bIsDirectory) + { + bIsEmpty = false; + return false; // abort searching + } + + return true; // continue searching + } + }; + + // Look for files on disk in case the folder contains things not tracked by the asset registry + FEmptyFolderVisitor EmptyFolderVisitor; + IFileManager::Get().IterateDirectoryRecursively(*PathToDeleteOnDisk, EmptyFolderVisitor); + return EmptyFolderVisitor.bIsEmpty; + }; + + // Iterates on all the temporary cook directories recursively, + // And keep not of all the empty directories + FString TempCookPathOnDisk; + TArray FoldersToDelete; + if (FPackageName::TryConvertLongPackageNameToFilename(TempCookFolder, TempCookPathOnDisk)) + { + FM.IterateDirectoryRecursively(*TempCookPathOnDisk, [&FM, &FoldersToDelete, &IsEmptyFolder](const TCHAR* InFilenameOrDirectory, const bool InIsDirectory) -> bool + { + // Skip Files + if (!InIsDirectory) + return true; + + FString CurrentDirectoryPath = FString(InFilenameOrDirectory); + if (IsEmptyFolder(CurrentDirectoryPath)) + FoldersToDelete.Add(CurrentDirectoryPath); + + // keep iterating + return true; + }); + } + + int32 DeletedDirectories = 0; + for (auto& FolderPath : FoldersToDelete) + { + FString PathToDelete; + if (!FPackageName::TryConvertFilenameToLongPackageName(FolderPath, PathToDelete)) + continue; + + if (IFileManager::Get().DeleteDirectory(*FolderPath, false, true)) + { + AssetRegistryModule.Get().RemovePath(PathToDelete); + DeletedDirectories++; + } + } + + GWarn->EndSlowTask(); + + // Add a slate notification + Notification = TEXT("Deleted ") + FString::FromInt(DeletedCount) + TEXT(" temporary files and ") + FString::FromInt(DeletedDirectories) + TEXT(" directories."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // ... and a log message + HOUDINI_LOG_MESSAGE(TEXT("Deleted %d temporary files and %d directories."), DeletedCount, DeletedDirectories); +} + +void +FHoudiniEngineCommands::BakeAllAssets() +{ + // Add a slate notification + FString Notification = TEXT("Baking all assets in the current level..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // Bakes and replaces with blueprints all Houdini Assets in the current level + int32 BakedCount = 0; + for (TObjectIterator Itr; Itr; ++Itr) + { + UHoudiniCookable * CurrentHC = *Itr; + if (!IsValid(CurrentHC)) + { + FString AssetName = CurrentHC->GetOuter() ? CurrentHC->GetOuter()->GetName() : CurrentHC->GetName(); + if (AssetName != "Default__HoudiniAssetActor") + HOUDINI_LOG_ERROR(TEXT("Failed to bake a Houdini Asset in the scene! - %s is invalid"), *AssetName); + continue; + } + + // If component is not cooking or instancing, we can bake blueprint. + if (CurrentHC->IsInstantiatingOrCooking()) + { + FString AssetName = CurrentHC->GetOuter() ? CurrentHC->GetOuter()->GetName() : CurrentHC->GetName(); + HOUDINI_LOG_ERROR(TEXT("Failed to bake a Houdini Asset in the scene! - %s is actively instantiating or cooking"), *AssetName); + continue; + } + + bool bSuccess = false; + bool BakeToBlueprints = true; + if (BakeToBlueprints) + { + FHoudiniBakedObjectData BakeOutputs; + FHoudiniBakeSettings BakeOptions; + BakeOptions.bReplaceActors = true; + BakeOptions.bReplaceAssets = true; + BakeOptions.bRecenterBakedActors = CurrentHC->GetRecenterBakedActors(); + + bSuccess = FHoudiniEngineBakeUtils::BakeBlueprints(CurrentHC, BakeOptions, BakeOutputs); + FHoudiniEngineBakeUtils::SaveBakedPackages(BakeOutputs.PackagesToSave); + + if (bSuccess) + { + // Instantiate blueprints in component's level, then remove houdini asset actor + bSuccess = false; + ULevel* Level = CurrentHC->GetLevel(); + if (IsValid(Level)) + { + UWorld* World = Level->GetWorld(); + if (IsValid(World)) + { + FActorSpawnParameters SpawnParams; + SpawnParams.OverrideLevel = Level; + FTransform Transform = CurrentHC->GetComponentTransform(); + for (UBlueprint* Blueprint : BakeOutputs.Blueprints) + { + if (!IsValid(Blueprint)) + continue; + World->SpawnActor(Blueprint->GetBlueprintClass(), &Transform, SpawnParams); + } + + FHoudiniEngineBakeUtils::DeleteBakedHoudiniAssetActor(CurrentHC); + bSuccess = true; + } + } + } + } + else + { + FHoudiniBakeSettings BakeOptions; + BakeOptions.bReplaceActors = true; + BakeOptions.bReplaceAssets = true; + BakeOptions.bRecenterBakedActors = CurrentHC->GetRecenterBakedActors(); + if (FHoudiniEngineBakeUtils::BakeCookableToActors(CurrentHC, BakeOptions)) + { + bSuccess = true; + FHoudiniEngineBakeUtils::DeleteBakedHoudiniAssetActor(CurrentHC); + } + } + + if (bSuccess) + BakedCount++; + } + + // Add a slate notification + Notification = TEXT("Baked ") + FString::FromInt(BakedCount) + TEXT(" Houdini assets."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // ... and a log message + HOUDINI_LOG_MESSAGE(TEXT("Baked all %d Houdini assets in the current level."), BakedCount); +} + +void +FHoudiniEngineCommands::PauseAssetCooking() +{ + // Revert the global flag + bool bCurrentCookingEnabled = !FHoudiniEngine::Get().IsCookingEnabled(); + FHoudiniEngine::Get().SetCookingEnabled(bCurrentCookingEnabled); + + // We need to refresh UI when pause cooking. Set refresh UI counter to be the number of current registered HCs. + if (!bCurrentCookingEnabled) + FHoudiniEngine::Get().SetUIRefreshCountWhenPauseCooking( FHoudiniEngineRuntime::Get().GetRegisteredHoudiniCookableCount() ); + + // Add a slate notification + FString Notification = TEXT("Houdini Engine cooking paused"); + if (bCurrentCookingEnabled) + Notification = TEXT("Houdini Engine cooking resumed"); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // ... and a log message + if (bCurrentCookingEnabled) + HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine cooking resumed.")); + else + HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine cooking paused.")); +} + +bool +FHoudiniEngineCommands::IsAssetCookingPaused() +{ + return !FHoudiniEngine::Get().IsCookingEnabled(); +} + +void +FHoudiniEngineCommands::RecookSelection() +{ + // Get current world selection + TArray WorldSelection; + int32 SelectedHoudiniAssets = FHoudiniEngineEditorUtils::GetWorldSelection(WorldSelection, true); + if (SelectedHoudiniAssets <= 0) + { + HOUDINI_LOG_MESSAGE(TEXT("No Houdini Assets selected in the world outliner")); + return; + } + + // Add a slate notification + FString Notification = TEXT("Cooking selected Houdini Assets..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // Iterates over the selection and cook the assets if they're in a valid state + int32 CookedCount = 0; + for (int32 Idx = 0; Idx < SelectedHoudiniAssets; Idx++) + { + AHoudiniAssetActor * HoudiniAssetActor = Cast(WorldSelection[Idx]); + if (!IsValid(HoudiniAssetActor)) + continue; + + UHoudiniCookable* HoudiniCookable = HoudiniAssetActor->GetHoudiniCookable(); + if (!IsValid(HoudiniCookable)) + continue; + + HoudiniCookable->MarkAsNeedCook(); + CookedCount++; + } + + // Add a slate notification + Notification = TEXT("Re-cooking ") + FString::FromInt(CookedCount) + TEXT(" Houdini assets."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // ... and a log message + HOUDINI_LOG_MESSAGE(TEXT("Re-cooking %d selected Houdini assets."), CookedCount); +} + +void +FHoudiniEngineCommands::RecookAllAssets() +{ + // Add a slate notification + FString Notification = TEXT("Cooking all assets in the current level..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // Bakes and replaces with blueprints all Houdini Assets in the current level + int32 CookedCount = 0; + for (TObjectIterator Itr; Itr; ++Itr) + { + UHoudiniCookable* HoudiniCookable = *Itr; + if (!IsValid(HoudiniCookable)) + continue; + + HoudiniCookable->MarkAsNeedCook(); + CookedCount++; + } + + // Add a slate notification + Notification = TEXT("Re-cooked ") + FString::FromInt(CookedCount) + TEXT(" Houdini assets."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // ... and a log message + HOUDINI_LOG_MESSAGE(TEXT("Re-cooked %d Houdini assets in the current level."), CookedCount); +} + +void +FHoudiniEngineCommands::RebuildAllAssets() +{ + // Add a slate notification + FString Notification = TEXT("Re-building all assets in the current level..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // Bakes and replaces with blueprints all Houdini Assets in the current level + int32 RebuiltCount = 0; + for (TObjectIterator Itr; Itr; ++Itr) + { + UHoudiniCookable* HoudiniCookable = *Itr; + if (!IsValid(HoudiniCookable)) + continue; + + HoudiniCookable->MarkAsNeedRebuild(); + RebuiltCount++; + } + + // Add a slate notification + Notification = TEXT("Rebuilt ") + FString::FromInt(RebuiltCount) + TEXT(" Houdini assets."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // ... and a log message + HOUDINI_LOG_MESSAGE(TEXT("Rebuilt %d Houdini assets in the current level."), RebuiltCount); +} + +void +FHoudiniEngineCommands::RebuildSelection() +{ + // Get current world selection + TArray WorldSelection; + int32 SelectedHoudiniAssets = FHoudiniEngineEditorUtils::GetWorldSelection(WorldSelection, true); + if (SelectedHoudiniAssets <= 0) + { + HOUDINI_LOG_MESSAGE(TEXT("No Houdini Assets selected in the world outliner")); + return; + } + + // Add a slate notification + FString Notification = TEXT("Rebuilding selected Houdini Assets..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // Iterates over the selection and rebuilds the assets if they're in a valid state + int32 RebuiltCount = 0; + for (int32 Idx = 0; Idx < SelectedHoudiniAssets; Idx++) + { + AHoudiniAssetActor * HoudiniAssetActor = Cast(WorldSelection[Idx]); + if (!IsValid(HoudiniAssetActor)) + continue; + + UHoudiniCookable* HoudiniCookable = HoudiniAssetActor->GetHoudiniCookable(); + if (!IsValid(HoudiniCookable)) + continue; + + HoudiniCookable->MarkAsNeedRebuild(); + RebuiltCount++; + } + + // Add a slate notification + Notification = TEXT("Rebuilt ") + FString::FromInt(RebuiltCount) + TEXT(" Houdini assets."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // ... and a log message + HOUDINI_LOG_MESSAGE(TEXT("Rebuilt %d selected Houdini assets."), RebuiltCount); +} + +void +FHoudiniEngineCommands::BakeSelection() +{ + // Get current world selection + TArray WorldSelection; + int32 SelectedHoudiniAssets = FHoudiniEngineEditorUtils::GetWorldSelection(WorldSelection, true); + if (SelectedHoudiniAssets <= 0) + { + HOUDINI_LOG_MESSAGE(TEXT("No Houdini Assets selected in the world outliner")); + return; + } + + // Add a slate notification + FString Notification = TEXT("Baking selected Houdini Asset Actors in the current level..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // Iterates over the selection and rebuilds the assets if they're in a valid state + int32 BakedCount = 0; + for (int32 Idx = 0; Idx < SelectedHoudiniAssets; Idx++) + { + AHoudiniAssetActor * HoudiniAssetActor = Cast(WorldSelection[Idx]); + if (!IsValid(HoudiniAssetActor)) + continue; + + UHoudiniCookable* HC = HoudiniAssetActor->GetHoudiniCookable(); + if (!IsValid(HC)) + { + HOUDINI_LOG_ERROR(TEXT("Failed to export a Houdini Asset in the scene! - Invalid Houdini Cookable")); + continue; + } + + // If component is not cooking or instancing, we can bake blueprint. + if (HC->IsInstantiatingOrCooking()) + continue; + + FHoudiniBakedObjectData BakeOutputs; + const bool bReplaceAssets = true; + + FHoudiniBakeSettings BakeOptions; + BakeOptions.bReplaceActors = true; + BakeOptions.bReplaceAssets = true; + BakeOptions.bRecenterBakedActors = HC->GetRecenterBakedActors(); + + const bool bSuccess = FHoudiniEngineBakeUtils::BakeBlueprints(HC, BakeOptions, BakeOutputs); + FHoudiniEngineBakeUtils::SaveBakedPackages(BakeOutputs.PackagesToSave); + + if (!bSuccess) + continue; + + // Instantiate blueprints in component's level, then remove houdini asset actor + ULevel* Level = HC->GetLevel(); + if (!IsValid(Level)) + continue; + UWorld* World = Level->GetWorld(); + if (!IsValid(World)) + continue; + + FActorSpawnParameters SpawnParams; + SpawnParams.OverrideLevel = Level; + FTransform Transform = HC->GetComponentTransform(); + for (UBlueprint* Blueprint : BakeOutputs.Blueprints) + { + if (!IsValid(Blueprint)) + continue; + World->SpawnActor(Blueprint->GetBlueprintClass(), &Transform, SpawnParams); + } + + FHoudiniEngineBakeUtils::DeleteBakedHoudiniAssetActor(HC); + BakedCount++; + } + + // Add a slate notification + Notification = TEXT("Baked ") + FString::FromInt(BakedCount) + TEXT(" Houdini assets."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // ... and a log message + HOUDINI_LOG_MESSAGE(TEXT("Baked all %d Houdini assets in the current level."), BakedCount); +} + +// Recentre HoudiniAsset actors' pivots to their input / cooked static-mesh average centre. +void +FHoudiniEngineCommands::RecentreSelection() +{ + // TODO: Finish me! This has been deactivated for all v2 + /* + //Get current world selection + TArray WorldSelection; + int32 SelectedHoudiniAssets = FHoudiniEngineEditorUtils::GetWorldSelection(WorldSelection, true); + if (SelectedHoudiniAssets <= 0) + { + HOUDINI_LOG_MESSAGE(TEXT("No Houdini Assets selected in the world outliner")); + return; + } + + // Add a slate notification + FString Notification = TEXT("Recentering selected Houdini Assets..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // Iterates over the selection and cook the assets if they're in a valid state + int32 RecentreCount = 0; + for (int32 Idx = 0; Idx < SelectedHoudiniAssets; Idx++) + { + AHoudiniAssetActor * HoudiniAssetActor = Cast(WorldSelection[Idx]); + if (!IsValid(HoudiniAssetActor)) + continue; + + UHoudiniCookable * HoudiniCookable = HoudiniAssetActor->GetHoudiniCookable(); + if (!HoudiniCookable || !HoudiniCookable->IsComponentValid()) + continue; + + // Get the average centre of all the created Static Meshes + FVector AverageBoundsCentre = FVector::ZeroVector; + int32 NumBounds = 0; + const FVector CurrentLocation = HoudiniCookable->GetComponentLocation(); + { + //Check Static Meshes + TArray StaticMeshes; + StaticMeshes.Reserve(16); + + for (auto& CurOutput : HoudiniCookable->GetOutputs()) + { + for (auto& CurOutputObject : CurOutput->GetOutputObjects()) + { + CurOutputObject.Value.OutputObject + } + } + HoudiniCookable->GetAllUsedStaticMeshes(StaticMeshes); + + //Get average centre of all the static meshes. + for (const UStaticMesh* pMesh : StaticMeshes) + { + if (!pMesh) + continue; + + //to world space + AverageBoundsCentre += (pMesh->GetBounds().Origin + CurrentLocation); + NumBounds++; + } + } + + //Check Inputs + if (0 == NumBounds) + { + const TArray< UHoudiniInput* >& AssetInputs = HoudiniCookable->Inputs; + for (const UHoudiniInput* pInput : AssetInputs) + { + if (!IsValid(pInput)) + continue; + + // to world space + FBox Bounds = pInput->GetInputBounds(CurrentLocation); + if (Bounds.IsValid) + { + AverageBoundsCentre += Bounds.GetCenter(); + NumBounds++; + } + } + } + + //if we have more than one, get the average centre + if (NumBounds > 1) + { + AverageBoundsCentre /= (float)NumBounds; + } + + //if we need to move... + float fDist = FVector::DistSquared(CurrentLocation, AverageBoundsCentre); + if (NumBounds && fDist > 1.0f) + { + // Move actor to average centre and recook + // This will refresh the static mesh under the HoudiniAssestComponent ( undoing the translation ). + HoudiniAssetActor->SetActorLocation(AverageBoundsCentre, false, nullptr, ETeleportType::TeleportPhysics); + + // Recook now the houdini-static-mesh has a new origin + HoudiniCookable->StartTaskAssetCookingManual(); + RecentreCount++; + } + } + + if (RecentreCount) + { + // UE4 Editor doesn't refresh the translation-handles until they are re-selected, confusing the user, deselect the objects. + GEditor->SelectNone(true, false); + } + + // Add a slate notification + Notification = TEXT("Re-centred ") + FString::FromInt(RecentreCount) + TEXT(" Houdini assets."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // ... and a log message + HOUDINI_LOG_MESSAGE(TEXT("Re-centred %d selected Houdini assets."), RecentreCount); + */ +} + +void +FHoudiniEngineCommands::OpenSessionSync(bool bWaitForCompletion) +{ + FHoudiniEngine::Get().OpenSessionSync(bWaitForCompletion); +} + +void +FHoudiniEngineCommands::CloseSessionSync() +{ + if (!FHoudiniEngine::Get().StopSession()) + { + // StopSession returns false only if Houdini is not initialized + HOUDINI_LOG_ERROR(TEXT("Failed to stop Session Sync - HAPI Not initialized")); + return; + } + + // Add a slate notification + FString Notification = TEXT("Stopping Houdini Session Sync..."); + FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // ... and a log message + HOUDINI_LOG_MESSAGE(TEXT("Stopping Houdini Session Sync.")); + + // Stop Houdini Session sync if it is still running! + FProcHandle PreviousHESS = FHoudiniEngine::Get().GetHESSProcHandle(); + if (FPlatformProcess::IsProcRunning(PreviousHESS)) + { + FPlatformProcess::TerminateProc(PreviousHESS, true); + } +} + +void +FHoudiniEngineCommands::OpenNodeSync() +{ + //FGlobalTabmanager::Get()->TryInvokeTab(NodeSyncTabName); + FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked(TEXT("LevelEditor")); + LevelEditorModule.GetLevelEditorTabManager()->TryInvokeTab(NodeSyncTabName); +} + +void FHoudiniEngineCommands::OpenHoudiniToolsTab() +{ + // FGlobalTabmanager::Get()->TryInvokeTab(HoudiniToolsTabName); + FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked( TEXT("LevelEditor") ); + LevelEditorModule.GetLevelEditorTabManager()->TryInvokeTab(HoudiniToolsTabName); +} + + +void +FHoudiniEngineCommands::SetViewportSync(const int32& ViewportSync) +{ + if (ViewportSync == 1) + { + FHoudiniEngine::Get().SetSyncViewportEnabled(true); + FHoudiniEngine::Get().SetSyncHoudiniViewportEnabled(true); + FHoudiniEngine::Get().SetSyncUnrealViewportEnabled(false); + } + else if (ViewportSync == 2) + { + FHoudiniEngine::Get().SetSyncViewportEnabled(true); + FHoudiniEngine::Get().SetSyncHoudiniViewportEnabled(false); + FHoudiniEngine::Get().SetSyncUnrealViewportEnabled(true); + } + else if (ViewportSync == 3) + { + FHoudiniEngine::Get().SetSyncViewportEnabled(true); + FHoudiniEngine::Get().SetSyncHoudiniViewportEnabled(true); + FHoudiniEngine::Get().SetSyncUnrealViewportEnabled(true); + } + else + { + FHoudiniEngine::Get().SetSyncViewportEnabled(false); + FHoudiniEngine::Get().SetSyncHoudiniViewportEnabled(false); + FHoudiniEngine::Get().SetSyncUnrealViewportEnabled(false); + } +} + +int32 +FHoudiniEngineCommands::GetViewportSync() +{ + if(!FHoudiniEngine::Get().IsSyncViewportEnabled()) + return 0; + + bool bSyncH = FHoudiniEngine::Get().IsSyncHoudiniViewportEnabled(); + bool bSyncU = FHoudiniEngine::Get().IsSyncUnrealViewportEnabled(); + if (bSyncH && !bSyncU) + return 1; + else if (!bSyncH && bSyncU) + return 2; + else if (bSyncH && bSyncU) + return 3; + else + return 0; +} + +void +FHoudiniEngineCommands::RestartSession() +{ + // Restart the current Houdini Engine Session + if (!FHoudiniEngine::Get().RestartSession()) + return; + + // We've successfully restarted the Houdini Engine session, + // We now need to notify all the Cookable that they need to re instantiate + // themselves in the new Houdini engine session. + FHoudiniEngineUtils::MarkAllCookablesAsNeedInstantiation(); +} + +void +FHoudiniEngineCommands::CreateSession() +{ + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + + // Restart the current Houdini Engine Session + if (!FHoudiniEngine::Get().CreateSession(HoudiniRuntimeSettings->SessionType)) + return; + + // We've successfully created the Houdini Engine session, + // We now need to notify all the Cookables that they need to re instantiate + // themselves in the new Houdini engine session. + FHoudiniEngineUtils::MarkAllCookablesAsNeedInstantiation(); +} + +void +FHoudiniEngineCommands::ConnectSession() +{ + // Restart the current Houdini Engine Session + if (!FHoudiniEngine::Get().ConnectSession(true)) + return; + + // We've successfully connected to a Houdini Engine session, + // We now need to notify all the cookables that they need to re instantiate + // themselves in the new Houdini engine session. + FHoudiniEngineUtils::MarkAllCookablesAsNeedInstantiation(); +} + +bool +FHoudiniEngineCommands::IsSessionValid() +{ + return FHoudiniEngine::IsInitialized(); +} + +bool +FHoudiniEngineCommands::IsSessionSyncProcessValid() +{ + // Only launch Houdini in Session sync if we havent started it already! + FProcHandle PreviousHESS = FHoudiniEngine::Get().GetHESSProcHandle(); + return FPlatformProcess::IsProcRunning(PreviousHESS); +} + +void +FHoudiniEngineCommands::StopSession() +{ + // Restart the current Houdini Engine Session + if (!FHoudiniEngine::Get().StopSession()) + { + // StopSession returns false only if Houdini is not initialized + HOUDINI_LOG_ERROR(TEXT("Failed to restart the Houdini Engine session - HAPI Not initialized")); + } + else + { + HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine session stopped.")); + } +} + +EHoudiniProxyRefineRequestResult +FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshes(bool bOnlySelectedActors, bool bSilent, bool bRefineAll, bool bOnPreSaveWorld, UWorld *OnPreSaveWorld, bool bOnPreBeginPIE) +{ + // Get current world selection + TArray WorldSelection; + int32 NumSelectedHoudiniAssets = 0; + if (bOnlySelectedActors) + { + NumSelectedHoudiniAssets = FHoudiniEngineEditorUtils::GetWorldSelection(WorldSelection, true); + if (NumSelectedHoudiniAssets <= 0) + { + HOUDINI_LOG_MESSAGE(TEXT("No Houdini Assets selected in the world outliner")); + return EHoudiniProxyRefineRequestResult::Invalid; + } + } + + // Add a slate notification + FString Notification = TEXT("Refining Houdini proxy meshes to static meshes..."); + // FHoudiniEngineUtils::CreateSlateNotification(Notification); + + // First find the cookables that have meshes that we must refine + TArray CookablesToRefine; + TArray CookablesToCook; + // cookables that would be candidates for refinement/cooking, but have errors + TArray SkippedCookables; + + if (bOnlySelectedActors) + { + for (int32 Index = 0; Index < NumSelectedHoudiniAssets; ++Index) + { + AHoudiniAssetActor* HoudiniAssetActor = Cast(WorldSelection[Index]); + if (!IsValid(HoudiniAssetActor)) + continue; + + UHoudiniCookable* Cookable = HoudiniAssetActor->GetHoudiniCookable(); + if (!IsValid(Cookable)) + continue; + + // Check if we should consider this component for proxy mesh refinement or cooking, based on its settings and + // flags passed to the function. + TriageHoudiniCookablesForProxyMeshRefinement( + Cookable, bRefineAll, bOnPreSaveWorld, OnPreSaveWorld, bOnPreBeginPIE, + CookablesToRefine, CookablesToCook, SkippedCookables); + } + } + else + { + for (TObjectIterator Itr; Itr; ++Itr) + { + UHoudiniCookable* HoudiniCookable = *Itr; + if (!IsValid(HoudiniCookable)) + continue; + + if (bOnPreSaveWorld && OnPreSaveWorld && OnPreSaveWorld != HoudiniCookable->GetWorld()) + continue; + + // Check if we should consider this component for proxy mesh refinement or cooking, based on its settings and + // flags passed to the function. + TriageHoudiniCookablesForProxyMeshRefinement(HoudiniCookable, bRefineAll, bOnPreSaveWorld, OnPreSaveWorld, bOnPreBeginPIE, CookablesToRefine, CookablesToCook, SkippedCookables); + } + } + + return RefineTriagedHoudiniProxyMeshesToStaticMeshes( + CookablesToRefine, + CookablesToCook, + SkippedCookables, + bSilent, + bRefineAll, + bOnPreSaveWorld, + OnPreSaveWorld, + bOnPreBeginPIE + ); +} + +EHoudiniProxyRefineRequestResult +FHoudiniEngineCommands::RefineHoudiniProxyMeshActorArrayToStaticMeshes(const TArray& InActorsToRefine, bool bSilent) +{ + // For H21 the code for this function was moved out of the Editor module. This function is kept around for now to ease backporting. + return FHoudiniEngineUtils::RefineHoudiniProxyMeshActorArrayToStaticMeshes(InActorsToRefine, bSilent); +} + +void +FHoudiniEngineCommands::StartPDGCommandlet() +{ + FHoudiniEngine::Get().StartPDGCommandlet(); +} + +void +FHoudiniEngineCommands::StopPDGCommandlet() +{ + FHoudiniEngine::Get().StopPDGCommandlet(); +} + +bool +FHoudiniEngineCommands::IsPDGCommandletRunningOrConnected() +{ + return FHoudiniEngine::Get().IsPDGCommandletRunningOrConnected(); +} + +bool +FHoudiniEngineCommands::IsPDGCommandletEnabled() +{ + const UHoudiniRuntimeSettings* const Settings = GetDefault(); + if (IsValid(Settings)) + { + return Settings->bPDGAsyncCommandletImportEnabled; + } + + return false; +} + +bool +FHoudiniEngineCommands::SetPDGCommandletEnabled(bool InEnabled) +{ + UHoudiniRuntimeSettings* const Settings = GetMutableDefault(); + if (IsValid(Settings)) + { + Settings->bPDGAsyncCommandletImportEnabled = InEnabled; + return true; + } + + return false; +} + +void +FHoudiniEngineCommands::ClearInputManager() +{ + FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + { + HOUDINI_LOG_WARNING(TEXT("[ClearInputManager]: Manager is null!")); + return; + } + + Manager->Clear(); +} + +void +FHoudiniEngineCommands::TriageHoudiniCookablesForProxyMeshRefinement( + UHoudiniCookable* InHC, + bool bRefineAll, + bool bOnPreSaveWorld, + UWorld *OnPreSaveWorld, + bool bOnPreBeginPIE, + TArray &OutToRefine, + TArray &OutToCook, + TArray &OutSkipped) +{ + // For H21 the code for this function was moved out of the Editor module. This function is kept around for now to ease backporting. + FHoudiniEngineUtils::TriageHoudiniCookablesForProxyMeshRefinement(InHC, + bRefineAll, + bOnPreSaveWorld, + OnPreSaveWorld, + bOnPreBeginPIE, + OutToRefine, + OutToCook, + OutSkipped); +} + +EHoudiniProxyRefineRequestResult +FHoudiniEngineCommands::RefineTriagedHoudiniProxyMeshesToStaticMeshes( + const TArray& InCookablesToRefine, + const TArray& InCookablesToCook, + const TArray& InSkippedCookables, + bool bInSilent, + bool bInRefineAll, + bool bInOnPreSaveWorld, + UWorld* InOnPreSaveWorld, + bool bInOnPrePIEBeginPlay) +{ + // For H21 the code for this function was moved out of the Editor module. This function is kept around for now to ease backporting. + return FHoudiniEngineUtils::RefineTriagedHoudiniProxyMeshesToStaticMeshes( + InCookablesToRefine, + InCookablesToCook, + InSkippedCookables, + bInSilent, + bInRefineAll, + bInOnPreSaveWorld, + InOnPreSaveWorld, + bInOnPrePIEBeginPlay); +} + +void +FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshesWithCookInBackgroundThread( + const TArray& InCookablesToCook, + TSharedPtr InTaskProgress, + const uint32 InNumCookablesToProcess, + bool bInOnPreSaveWorld, + UWorld *InOnPreSaveWorld, + const TArray &InSuccessfulCookables, + const TArray &InFailedCookables, + const TArray &InSkippedCookables) +{ + // For H21 the code for this function was moved out of the Editor module. This function is kept around for now to ease backporting. + FHoudiniEngineUtils::RefineHoudiniProxyMeshesToStaticMeshesWithCookInBackgroundThread( + InCookablesToCook, + InTaskProgress, + InNumCookablesToProcess, + bInOnPreSaveWorld, + InOnPreSaveWorld, + InSuccessfulCookables, + InFailedCookables, + InSkippedCookables); +} + +void +FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshesNotifyDone( + const uint32 InNumTotalCookables, + FSlowTask* const InTaskProgress, + const bool bCancelled, + const bool bOnPreSaveWorld, + UWorld* const InOnPreSaveWorld, + const TArray &InSuccessfulCookables, + const TArray &InFailedCookables, + const TArray &InSkippedCookables) +{ + // For H21 the code for this function was moved out of the Editor module. This function is kept around for now to ease backporting. + FHoudiniEngineUtils::RefineHoudiniProxyMeshesToStaticMeshesNotifyDone( + InNumTotalCookables, + InTaskProgress, + bCancelled, + bOnPreSaveWorld, + InOnPreSaveWorld, + InSuccessfulCookables, + InFailedCookables, + InSkippedCookables); +} + +void +FHoudiniEngineCommands::RefineProxyMeshesHandleOnPostSaveWorld(const TArray &InSuccessfulCookables, uint32 InSaveFlags, UWorld* InWorld, bool bInSuccess) +{ + // For H21 the code for this function was moved out of the Editor module. This function is kept around for now to ease backporting. + FHoudiniEngineUtils::RefineProxyMeshesHandleOnPostSaveWorld(InSuccessfulCookables, InSaveFlags, InWorld, bInSuccess); +} + +void +FHoudiniEngineCommands::SetAllowPlayInEditorRefinement( + const TArray& InCookables, + bool bEnabled) +{ + // For H21 the code for this function was moved out of the Editor module. This function is kept around for now to ease backporting. + FHoudiniEngineUtils::SetAllowPlayInEditorRefinement(InCookables, bEnabled); +} + +void +FHoudiniEngineCommands::DumpGenericAttribute(const TArray& Args) +{ + if (Args.Num() < 1) + { + HOUDINI_LOG_ERROR(TEXT(" ")); + HOUDINI_LOG_ERROR(TEXT("DumpGenericAttribute takes a class name as argument! ie: DumpGenericAttribute StaticMesh")); + HOUDINI_LOG_ERROR(TEXT(" ")); + return; + } + + for (int32 Idx = 0; Idx < Args.Num(); Idx++) + { + // Get the class name + FString ClassName = Args[Idx]; + + HOUDINI_LOG_MESSAGE(TEXT("------------------------------------------------------------------------------------------------------------")); + HOUDINI_LOG_MESSAGE(TEXT(" Dumping GenericAttribute for Class %s"), *ClassName); + HOUDINI_LOG_MESSAGE(TEXT("------------------------------------------------------------------------------------------------------------")); + + HOUDINI_LOG_MESSAGE(TEXT(" ")); + HOUDINI_LOG_MESSAGE(TEXT("Format: ")); + HOUDINI_LOG_MESSAGE(TEXT("unreal_uproperty_XXXX : NAME (DISPLAY_NAME) - UE TYPE: UETYPE - H TYPE: HTYPE TUPLE.")); + HOUDINI_LOG_MESSAGE(TEXT(" ")); + HOUDINI_LOG_MESSAGE(TEXT(" ")); + + // Make sure we can find the class + UClass* FoundClass = FHoudiniEngineRuntimeUtils::GetClassByName(ClassName); + if (!IsValid(FoundClass) && (ClassName.StartsWith("U") || ClassName.StartsWith("F"))) + { + // Try again after removing the starting U/F character + FString ChoppedName = ClassName.RightChop(1); + FoundClass = FHoudiniEngineRuntimeUtils::GetClassByName(ChoppedName); + } + + if (!IsValid(FoundClass)) + { + HOUDINI_LOG_ERROR(TEXT("DumpGenericAttribute wasn't able to find a UClass that matches %s!"), *ClassName); + HOUDINI_LOG_MESSAGE(TEXT("------------------------------------------------------------------------------------------------------------")); + return; + } + + UObject* ObjectToParse = FoundClass->GetDefaultObject(); + if (!IsValid(ObjectToParse)) + { + // Use the class directly if we failed to get a DCO + ObjectToParse = FoundClass; + } + + // Reuse the find property function used by the generic attribute system + FProperty* FoundProperty = nullptr; + UObject* FoundPropertyObject = nullptr; + void* Container = nullptr; + FEditPropertyChain FoundPropertyChain; + bool bExactPropertyFound = false; + FHoudiniGenericAttribute::FindPropertyOnObject(ObjectToParse, FString(), FoundPropertyChain, FoundProperty, FoundPropertyObject, Container, bExactPropertyFound, true); + + HOUDINI_LOG_MESSAGE(TEXT("------------------------------------------------------------------------------------------------------------")); + HOUDINI_LOG_MESSAGE(TEXT(" ")); + } +} + + +void +FHoudiniEngineCommands::CleanHoudiniEngineSession() +{ + // HAPI needs to be initialized + if (!FHoudiniApi::IsHAPIInitialized()) + { + HOUDINI_LOG_ERROR(TEXT("Unable to clean the current Houdini Engine Session - HAPI is not initialized.")); + return; + } + + // We need a current session + const HAPI_Session* CurrentSession = FHoudiniEngine::Get().GetSession(); + if (!CurrentSession) + { + HOUDINI_LOG_ERROR(TEXT("Unable to clean the current Houdini Engine Session - no current session.")); + return; + } + + // We need the current session to be valid + if (HAPI_RESULT_SUCCESS != FHoudiniApi::IsSessionValid(CurrentSession)) + { + HOUDINI_LOG_ERROR(TEXT("Unable to clean the current Houdini Engine Session - the current session is invalid.")); + return; + } + + HAPI_Result Result; + HOUDINI_CHECK_ERROR_GET(&Result, FHoudiniApi::Cleanup(CurrentSession)); + if (HAPI_RESULT_SUCCESS != Result) + { + HOUDINI_LOG_ERROR(TEXT("Failed to clean up the current Houdini Engine Session.")); + } + else + { + HOUDINI_LOG_MESSAGE(TEXT("Succesfully cleaned up the current Houdini Engine Session.")); + + // We need to reinitialize the session after the clean up + FHoudiniEngine::Get().InitializeHAPISession(); + } +} + + +void +FHoudiniEngineCommands::StartPerformanceMonitoring() +{ + // HAPI needs to be initialized + if (!FHoudiniApi::IsHAPIInitialized()) + { + HOUDINI_LOG_ERROR(TEXT("Unable to start HAPI performance monitoring - HAPI is not initialized.")); + return; + } + + // We need a current session + const HAPI_Session* CurrentSession = FHoudiniEngine::Get().GetSession(); + if (!CurrentSession) + { + HOUDINI_LOG_ERROR(TEXT("Unable to start HAPI performance monitoring - no current session.")); + return; + } + + // We need the current session to be valid + if (HAPI_RESULT_SUCCESS != FHoudiniApi::IsSessionValid(CurrentSession)) + { + HOUDINI_LOG_ERROR(TEXT("Unable to start HAPI performance monitoring - the current session is invalid.")); + return; + } + + FHoudiniEngine::Get().StartHAPIPerformanceMonitoring(); +} + +void +FHoudiniEngineCommands::StopPerformanceMonitoring() +{ + // HAPI needs to be initialized + if (!FHoudiniApi::IsHAPIInitialized()) + { + HOUDINI_LOG_ERROR(TEXT("Unable to start HAPI performance monitoring - HAPI is not initialized.")); + return; + } + + // We need a current session + const HAPI_Session* CurrentSession = FHoudiniEngine::Get().GetSession(); + if (!CurrentSession) + { + HOUDINI_LOG_ERROR(TEXT("Unable to start HAPI performance monitoring - no current session.")); + return; + } + + // We need the current session to be valid + if (HAPI_RESULT_SUCCESS != FHoudiniApi::IsSessionValid(CurrentSession)) + { + HOUDINI_LOG_ERROR(TEXT("Unable to start HAPI performance monitoring - the current session is invalid.")); + return; + } + + FString TraceStorePath; + if (TraceStorePath.IsEmpty()) + { + using UE::Trace::FStoreClient; + FStoreClient* StoreClientPtr = FStoreClient::Connect(TEXT("localhost")); + TUniquePtr StoreClient = TUniquePtr(StoreClientPtr); + + if (StoreClient) + { + const FStoreClient::FStatus* Status = StoreClient->GetStatus(); + if (Status) + { + TraceStorePath = FString(Status->GetStoreDir()); + } + } + } + + FHoudiniEngine::Get().StopHAPIPerformanceMonitoring(TraceStorePath); +} + +void +FHoudiniEngineCommands::DumpNode(const TArray& Args) +{ + // HAPI needs to be initialized + if(!FHoudiniApi::IsHAPIInitialized()) + { + HOUDINI_LOG_ERROR(TEXT("HAPI is not initialized.")); + return; + } + + // We need a current session + const HAPI_Session* CurrentSession = FHoudiniEngine::Get().GetSession(); + if(!CurrentSession) + { + HOUDINI_LOG_ERROR(TEXT("No current session.")); + return; + } + + // We need the current session to be valid + if(HAPI_RESULT_SUCCESS != FHoudiniApi::IsSessionValid(CurrentSession)) + { + HOUDINI_LOG_ERROR(TEXT("The current session is invalid.")); + return; + } + + if(Args.Num() < 1) + { + HOUDINI_LOG_ERROR(TEXT("DumpNode takes a node id as argument! ie: DumpNode /obj/node")); + return; + } + + FHoudiniEngineUtils::DumpNode(Args[0]); +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineCommands.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "Framework/Commands/Commands.h" +#include "Misc/SlowTask.h" +#include "Delegates/IDelegateInstance.h" +#include "HoudiniEngineRuntimeCommon.h" +#include "HoudiniRuntimeSettings.h" + +class AHoudiniAssetActor; +class UHoudiniCookable; +struct FSlowTask; + +static const FName NodeSyncTabName("HoudiniNodeSync"); +static const FName HoudiniToolsTabName("HoudiniTools"); +static const FName ExamplesTabName("Examples"); + +// Class containing commands for Houdini Engine actions +class FHoudiniEngineCommands : public TCommands +{ +public: + + + FHoudiniEngineCommands(); + + // TCommand<> interface + virtual void RegisterCommands() override; + +public: + + // Menu action called to save a HIP file. + static void SaveHIPFile(); + + // Menu action called to report a bug. + static void ReportBug(); + + // Menu action called to open the current scene in Houdini. + static void OpenInHoudini(); + + // Menu action called to clean up all unused files in the cook temp folder + static void CleanUpTempFolder(); + + // Menu action to bake/replace all current Houdini Assets with blueprints + static void BakeAllAssets(); + + // Helper function for baking/replacing the current select Houdini Assets with blueprints + static void BakeSelection(); + + // Helper function for restarting the current Houdini Engine session. + static void RestartSession(); + + // Menu action to pause cooking for all Houdini Assets + static void PauseAssetCooking(); + + // Helper delegate used to get the current state of PauseAssetCooking. + static bool IsAssetCookingPaused(); + + // Helper function for recooking all assets in the current level + static void RecookAllAssets(); + + // Helper function for rebuilding all assets in the current level + static void RebuildAllAssets(); + + // Helper function for recooking selected assets + static void RecookSelection(); + + // Helper function for rebuilding selected assets + static void RebuildSelection(); + + // Helper function for rebuilding selected assets + static void RecentreSelection(); + + // Helper function for starting Houdini in Sesion Sync mode + static void OpenSessionSync(bool bWaitForCompletion = false); + + // Helper function for closing the current Houdini Sesion Sync + static void CloseSessionSync(); + + // Helper function for Showing Node Sync + static void OpenNodeSync(); + + // Helper function for summoning Houdini Tools Tab + static void OpenHoudiniToolsTab(); + + // returns true if the current HE session is valid + static bool IsSessionValid(); + + // Returns true if the current Session Sync process is still running + static bool IsSessionSyncProcessValid(); + + static int32 GetViewportSync(); + + static void SetViewportSync(const int32& ViewportSync); + + static void CreateSession(); + + static void ConnectSession(); + + static void StopSession(); + + static void ShowInstallInfo(); + + static void ShowPluginSettings(); + + static void ShowPluginEditorSettings(); + + static void OpenContentExampleGit(); + + static void BrowseToContentExamples(); + + static bool HasContentExamples(); + + static void OnlineDocumentation(); + + static void OnlineForum(); + + static void DumpGenericAttribute(const TArray& Args); + + static void CleanHoudiniEngineSession(); + + static void StartPerformanceMonitoring(); + + static void StopPerformanceMonitoring(); + + static void DumpNode(const TArray& Args); + + // Helper function for building static meshes for all assets using HoudiniStaticMesh + // If bSilent is false, show a progress dialog. + // If bRefineAll is true, then all Cookables with HoudiniStaticMesh meshes will be + // refined to UStaticMesh. Otherwise, bOnPreSaveWorld and bOnPrePIEBeginPlay is checked + // against the settings of the component to determine if refinement should take place. + // If bOnPreSaveWorld is true, then OnPreSaveWorld should be the World that is being saved. In + // that case, only proxy meshes attached to Cookables from that world will be refined. + static EHoudiniProxyRefineRequestResult RefineHoudiniProxyMeshesToStaticMeshes(bool bOnlySelectedActors, bool bSilent=false, bool bRefineAll=true, bool bOnPreSaveWorld=false, UWorld *PreSaveWorld=nullptr, bool bOnPrePIEBeginPlay=false); + + // Refine all proxy meshes on UHoudiniAssetCompoments of InActorsToRefine. + static EHoudiniProxyRefineRequestResult RefineHoudiniProxyMeshActorArrayToStaticMeshes(const TArray& InActorsToRefine, bool bSilent=false); + + static void StartPDGCommandlet(); + + static void StopPDGCommandlet(); + + static bool IsPDGCommandletRunningOrConnected(); + + // Returns true if the commandlet is enabled in the settings + static bool IsPDGCommandletEnabled(); + + // Set the bPDGAsyncCommandletImportEnabled in the settings + static bool SetPDGCommandletEnabled(bool InEnabled); + + // Calls the FUnrealObjectInputManager::Clear() function on the input manager singleton. + static void ClearInputManager(); + +public: + + // UI Action to create a Houdini Engine Session + TSharedPtr _CreateSession; + // UI Action to connect to a Houdini Engine Session + TSharedPtr _ConnectSession; + // UI Action to stop the current Houdini Engine Session + TSharedPtr _StopSession; + // UI Action to restart the current Houdini Engine Session + TSharedPtr _RestartSession; + // UI Action to open Houdini Session Sync + TSharedPtr _OpenSessionSync; + // UI Action to open Houdini Session Sync + TSharedPtr _CloseSessionSync; + + // UI Action to disable viewport sync + TSharedPtr _ViewportSyncNone; + // UI Action to enable unreal viewport sync + TSharedPtr _ViewportSyncUnreal; + // UI Action to enable houdini viewport sync + TSharedPtr _ViewportSyncHoudini; + // UI Action to enable bidirectionnal viewport sync + TSharedPtr _ViewportSyncBoth; + + // UI Action to open Houdini Node Sync Panel + TSharedPtr _OpenNodeSync; + // UI Action to open Houdini Tools Panel + TSharedPtr _OpenHoudiniTools; + + // + TSharedPtr _InstallInfo; + // + TSharedPtr _PluginSettings; + // + TSharedPtr _PluginEditorSettings; + + // Menu action called to open the current scene in Houdini. + TSharedPtr _OpenInHoudini; + // Menu action called to save a HIP file. + TSharedPtr _SaveHIPFile; + // Menu action called to clean up all unused files in the cook temp folder + TSharedPtr _CleanUpTempFolder; + + + // Opens the content examples Git Repo + TSharedPtr _ContentExampleGit; + // Browse to the installed content example folder + TSharedPtr _ContentExampleBrowseTo; + + // Opens the online doc + TSharedPtr _OnlineDoc; + // Opens the plugin forum + TSharedPtr _OnlineForum; + // Menu action called to report a bug. + TSharedPtr _ReportBug; + + // UI Action to recook all HDA + TSharedPtr _CookAll; + // UI Action to recook the current world selection + TSharedPtr _CookSelected; + // Menu action to bake/replace all current Houdini Assets with blueprints + TSharedPtr _BakeAll; + // UI Action to bake and replace the current world selection + TSharedPtr _BakeSelected; + // UI Action to rebuild all HDA + TSharedPtr _RebuildAll; + // UI Action to rebuild the current world selection + TSharedPtr _RebuildSelected; + // UI Action for building static meshes for all assets using HoudiniStaticMesh + TSharedPtr _RefineAll; + // UI Action for building static meshes for selected assets using HoudiniStaticMesh + TSharedPtr _RefineSelected; + // Menu action to pause cooking for all Houdini Assets + TSharedPtr _PauseAssetCooking; + + // UI Action to recentre the current selection + TSharedPtr _RecentreSelected; + + // Start PDG/BGEO commandlet + TSharedPtr _StartPDGCommandlet; + // Stop PDG/BGEO commandlet + TSharedPtr _StopPDGCommandlet; + // Is PDG/BGEO commandlet enabled + TSharedPtr _IsPDGCommandletEnabled; + +protected: + + // Triage a cookable with UHoudiniStaticMesh as needing cooking or if a UStaticMesh can be immediately built + static void TriageHoudiniCookablesForProxyMeshRefinement( + UHoudiniCookable* InHC, + bool bRefineAll, + bool bOnPreSaveWorld, + UWorld *OnPreSaveWorld, + bool bOnPreBeginPIE, + TArray &OutToRefine, + TArray &OutToCook, + TArray &OutSkipped); + + static EHoudiniProxyRefineRequestResult RefineTriagedHoudiniProxyMeshesToStaticMeshes( + const TArray& InCookablesToRefine, + const TArray& InCookablesToCook, + const TArray& InSkippedCookables, + bool bInSilent=false, + bool bInRefineAll=true, + bool bInOnPreSaveWorld=false, + UWorld* InOnPreSaveWorld=nullptr, + bool bInOnPrePIEBeginPlay=false); + + // Called in a background thread by RefineHoudiniProxyMeshesToStaticMeshes when some Cookables need to be cooked to generate UStaticMeshes. Checks and waits for + // cooking of each component to complete, and then calls RefineHoudiniProxyMeshesToStaticMeshesNotifyDone on the main thread. + static void RefineHoudiniProxyMeshesToStaticMeshesWithCookInBackgroundThread( + const TArray &InCookablesToCook, + TSharedPtr InTaskProgress, + const uint32 InNumSkippedCookables, + bool bInOnPreSaveWorld, + UWorld *InOnPreSaveWorld, + const TArray &InSuccessfulCookables, + const TArray &InFailedCookables, + const TArray &InSkippedCookables); + + // Display a notification / end/close progress dialog, when refining mesh proxies to static meshes is complete + static void RefineHoudiniProxyMeshesToStaticMeshesNotifyDone( + const uint32 InNumTotalCookables, + FSlowTask* const InTaskProgress, + const bool bCancelled, + const bool bOnPreSaveWorld, + UWorld* const InOnPreSaveWorld, + const TArray &InSuccessfulCookables, + const TArray &InFailedCookables, + const TArray &InSkippedCookables); + + // Handle OnPostSaveWorld for refining proxy meshes: this saves all the dirty UPackages of the UStaticMeshes + // that were created during RefineHoudiniProxyMeshesToStaticMeshes if it was called as a result of a PreSaveWorld. + static void RefineProxyMeshesHandleOnPostSaveWorld( + const TArray &InSuccessfulCookables, + uint32 InSaveFlags, + UWorld* InWorld, + bool bInSuccess); + + static void SetAllowPlayInEditorRefinement( + const TArray& InCookables, bool bEnabled); + +}; + + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineDetails.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineEditorPrivatePCH.h" + +#include "HoudiniEngineDetails.h" + +#include "HAPI/HAPI_Version.h" +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniAssetComponentDetails.h" +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEditorNodeSyncSubsystem.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniEngineStyle.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniLandscapeTranslator.h" +#include "HoudiniNodeSyncComponent.h" +#include "HoudiniPackageParams.h" +#include "HoudiniParameter.h" +#include "HoudiniPresetFactory.h" +#include "HoudiniToolsEditor.h" +#include "SHoudiniPresets.h" +#include "SSelectFolderPathDialog.h" +#include "SSelectHoudiniPathDialog.h" + +#include "ActorPickerMode.h" +#include "ActorTreeItem.h" +#include "AssetSelection.h" +#include "AssetThumbnail.h" +#include "Brushes/SlateImageBrush.h" +#include "CoreMinimal.h" +//#include "ContentBrowserModule.h" +#include "DetailCategoryBuilder.h" +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "HAL/FileManager.h" +#include "HAL/PlatformApplicationMisc.h" +//#include "IContentBrowserSingleton.h" +#include "IDetailGroup.h" +#include "Interfaces/IMainFrameModule.h" +#include "Modules/ModuleManager.h" +#include "PropertyCustomizationHelpers.h" +#include "SAssetDropTarget.h" +#include "SceneOutlinerModule.h" +#include "ScopedTransaction.h" +#include "SEnumCombo.h" +#include "ToolMenuEntry.h" + +#include "Widgets/Images/SImage.h" +#include "Widgets/Images/SLayeredImage.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Input/SMultiLineEditableTextBox.h" +#include "Widgets/Input/SNumericEntryBox.h" +#include "Widgets/Layout/SScrollBox.h" +#include "Widgets/Layout/SSeparator.h" +#include "Widgets/Layout/SUniformGridPanel.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Text/SRichTextBlock.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +#define HOUDINI_ENGINE_UI_SECTION_GENERATE 1 +#define HOUDINI_ENGINE_UI_SECTION_BAKE 2 +#define HOUDINI_ENGINE_UI_SECTION_ASSET_OPTIONS 3 +#define HOUDINI_ENGINE_UI_SECTION_HELP_AND_DEBUG 4 +#define HOUDINI_ENGINE_UI_SECTION_PARAMETER_RESET 5 + +#define HOUDINI_ENGINE_UI_BUTTON_WIDTH 150.0f + +#define HOUDINI_ENGINE_UI_SECTION_GENERATE_HEADER_TEXT "Generate" +#define HOUDINI_ENGINE_UI_SECTION_BAKE_HEADER_TEXT "Bake" +#define HOUDINI_ENGINE_UI_SECTION_ASSET_OPTIONS_HEADER_TEXT "Asset Options" +#define HOUDINI_ENGINE_UI_SECTION_HELP_AND_DEBUG_HEADER_TEXT "Help and Debug" +#define HOUDINI_ENGINE_UI_SECTION_PARAMETER_RESET_TEXT "Reset Parameters" + +EHoudiniDetailsFlags EHoudiniDetailsFlags::Defaults; + +void +SHoudiniAssetLogWidget::Construct(const FArguments & InArgs) +{ + this->ChildSlot + [ + SNew(SBorder) + .BorderImage(_GetEditorStyle().GetBrush(TEXT("Menu.Background"))) + .Content() + [ + SNew(SScrollBox) + + SScrollBox::Slot() + [ + SNew(SMultiLineEditableTextBox) + .Text(FText::FromString(InArgs._LogText)) + .AutoWrapText(true) + .IsReadOnly(true) + ] + ] + ]; +} + + +void +FHoudiniEngineDetails::CreateHoudiniEngineIconWidget( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder) +{ + IDetailLayoutBuilder* SavedLayoutBuilder = &HoudiniEngineCategoryBuilder.GetParentLayout(); + + // Skip drawing the icon if the icon image is not loaded correctly. + TSharedPtr HoudiniEngineUIIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIIconBrush(); + if (!HoudiniEngineUIIconBrush.IsValid()) + return; + + FDetailWidgetRow & Row = HoudiniEngineCategoryBuilder.AddCustomRow(FText::GetEmpty()); + TSharedRef Box = SNew(SHorizontalBox); + TSharedPtr Image; + + TSharedPtr OptionsImage = SNew(SLayeredImage) + .Image(FAppStyle::Get().GetBrush("DetailsView.ViewOptions")) + .ColorAndOpacity(FSlateColor::UseForeground()); + + Box->AddSlot() + .AutoWidth() + .Padding(0.0f, 5.0f, 5.0f, 10.0f) + .HAlign(HAlign_Left) + [ + SNew(SBox) + .IsEnabled(false) + .HeightOverride(30) + .WidthOverride(208) + [ + SAssignNew(Image, SImage) + .ColorAndOpacity(FSlateColor::UseForeground()) + ] + ]; + + Image->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([HoudiniEngineUIIconBrush]() { + return HoudiniEngineUIIconBrush.Get(); + }))); + + Row.WholeRowWidget.Widget = Box; + Row.IsEnabled(false); +} + + + +void +FHoudiniEngineDetails::CreateHoudiniAssetDetails( + IDetailCategoryBuilder& HouAssetCategory, + TArray>& InCookables) +{ + if (InCookables.IsEmpty()) + return; + + TWeakObjectPtr MainCookable = InCookables[0]; + if (!IsValidWeakPointer(MainCookable)) + return; + + if (!MainCookable->IsHoudiniAssetSupported()) + return; + + FText AssetNameText = FText::GetEmpty(); + UHoudiniAsset* MainHDA = MainCookable->GetHoudiniAsset(); + if (MainHDA) + AssetNameText = FText::FromString(MainCookable->GetHapiAssetName()); + + // Create thumbnail for this HDA. + TSharedPtr AssetThumbnailPool = HouAssetCategory.GetParentLayout().GetThumbnailPool(); + TSharedPtr< FAssetThumbnail > HDAThumbnail = + MakeShareable(new FAssetThumbnail(MainHDA, 64, 64, AssetThumbnailPool)); + + // Create a widget row, or get the given row. + FDetailWidgetRow* Row = &(HouAssetCategory.AddCustomRow(AssetNameText)); + if (!Row) + return; + + // Add a name for the HDA row + Row->NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniAssetName", "Houdini Asset")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .ToolTipText_Lambda([MainCookable]() + { + // Display the full name of the node for tooltip + return FText::FromString(MainCookable->GetHoudiniAssetData()->HapiAssetName); + }) + ]; + + // Lambda for updating the Houdini asset + auto UpdateHoudiniAsset = [MainHDA](const TArray>& InCookables, UObject* InObject) + { + if (!IsValid(MainHDA)) + return; + + if (!InObject->IsA()) + return; + + UHoudiniAsset* NewHDA = Cast(InObject); + if (!IsValid(NewHDA)) + return; + + // TODO: Transaction on all cookable? + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniAssetChange", "Houdini Engine: Changed the Houdini Asset."), + MainHDA->GetOuter()); + + for (auto CurCookable : InCookables) + { + if (!IsValidWeakPointer(CurCookable)) + continue; + + if (!CurCookable->IsHoudiniAssetSupported()) + continue; + + // Update the HDA then notify the change - which will force a reinstantiate of the cookable + CurCookable->SetHoudiniAsset(NewHDA); + CurCookable->OnHoudiniAssetChanged(); + } + }; + + // Create a vertical Box for storing the UI + TSharedRef VerticalBox = SNew(SVerticalBox); + + // Add the Preset menu + TSharedPtr Image; + TSharedPtr OptionsImage = SNew(SLayeredImage) + .Image(FAppStyle::Get().GetBrush("DetailsView.ViewOptions")) + .ColorAndOpacity(FSlateColor::UseForeground()); + + IDetailLayoutBuilder* SavedLayoutBuilder = &HouAssetCategory.GetParentLayout(); + + // Add the Houdini Asset Picker + VerticalBox->AddSlot() + .Padding(0, 5, 0, 0) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(0.9f) + [ + SNew(SObjectPropertyEntryBox) + .ObjectPath_Lambda([MainHDA]() + { + if (IsValid(MainHDA)) + return MainHDA->GetPathName(); + + return FString(); + }) + .AllowedClass(UHoudiniAsset::StaticClass()) + .OnObjectChanged_Lambda([InCookables, UpdateHoudiniAsset](const FAssetData& InAssetData) + { + UHoudiniAsset* HDA = Cast(InAssetData.GetAsset()); + if (IsValid(HDA)) + UpdateHoudiniAsset(InCookables, HDA); + }) + .AllowCreate(false) + .AllowClear(true) + .DisplayUseSelected(true) + .DisplayBrowse(true) + .DisplayThumbnail(true) + .ThumbnailPool(AssetThumbnailPool/*UThumbnailManager::Get().GetSharedThumbnailPool()*/) + .NewAssetFactories(TArray()) + .ToolTipText_Lambda([MainCookable]() + { + // Display the full name of the node for tooltip + return FText::FromString(MainCookable->GetHoudiniAssetData()->HapiAssetName); + }) + ] + + SHorizontalBox::Slot() + .FillWidth(0.1f) + .HAlign(HAlign_Right) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .FillHeight(0.5f) + [ + SNew(SComboButton) + .HasDownArrow(false) + .ContentPadding(0) + .ForegroundColor(FSlateColor::UseForeground()) + .ButtonStyle(FAppStyle::Get(), "SimpleButton") + .AddMetaData(FTagMetaData(TEXT("ViewOptions"))) + .ToolTipText(LOCTEXT("HoudiniAssetPresetButton", "Houdini Asset Presets.")) + .OnGetMenuContent_Lambda([InCookables, SavedLayoutBuilder]() -> TSharedRef + { + return FHoudiniEngineDetails::ConstructActionMenu(InCookables, SavedLayoutBuilder).ToSharedRef(); + }) + .ButtonContent() + [ + OptionsImage.ToSharedRef() + ] + ] + + SVerticalBox::Slot() + .FillHeight(0.5f) + ] + ]; + + + // Set the widget in the row we created + Row->ValueWidget.Widget = VerticalBox; + + Row->ValueWidget.MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH); +} + +void +FHoudiniEngineDetails::CreateHoudiniEngineActionWidget( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InCookables) +{ + if (InCookables.Num() <= 0) + return; + + const TWeakObjectPtr& MainHC = InCookables[0]; + IDetailLayoutBuilder* SavedLayoutBuilder = &HoudiniEngineCategoryBuilder.GetParentLayout(); + + if (!IsValidWeakPointer(MainHC)) + return; + + // Skip drawing the icon if the icon image is not loaded correctly. + TSharedPtr HoudiniEngineUIIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIIconBrush(); + if (!HoudiniEngineUIIconBrush.IsValid()) + return; + + FDetailWidgetRow & Row = HoudiniEngineCategoryBuilder.AddCustomRow(FText::GetEmpty()); + TSharedRef Box = SNew(SHorizontalBox); + TSharedPtr Image; + + TSharedPtr OptionsImage = SNew(SLayeredImage) + .Image(FAppStyle::Get().GetBrush("DetailsView.ViewOptions")) + .ColorAndOpacity(FSlateColor::UseForeground()); + + Box->AddSlot() + .FillWidth(1.0f) + .HAlign(HAlign_Right) + [ + SNew(SComboButton) + .HasDownArrow(false) + .ContentPadding(0) + .ForegroundColor( FSlateColor::UseForeground() ) + .ButtonStyle( FAppStyle::Get(), "SimpleButton" ) + .AddMetaData(FTagMetaData(TEXT("ViewOptions"))) + .OnGetMenuContent_Lambda([InCookables, SavedLayoutBuilder]() -> TSharedRef + { + return FHoudiniEngineDetails::ConstructActionMenu(InCookables, SavedLayoutBuilder).ToSharedRef(); + }) + .ButtonContent() + [ + OptionsImage.ToSharedRef() + ] + ]; + + Row.WholeRowWidget.Widget = Box; +} + +bool +ShouldEnableParametersButton(const TArray>& InHCs) +{ + for(auto& NextHC : InHCs) + { + if(!IsValidWeakPointer(NextHC)) + continue; + + // Reset parameters to default values? + for(int32 n = 0; n < NextHC->GetNumParameters(); ++n) + { + UHoudiniParameter* NextParm = NextHC->GetParameterAt(n); + if(IsValid(NextParm) && !NextParm->IsDefault()) + return true; + } + } + + return false; +} + +void ResetParameters(const TArray>& InHCs) +{ + for(auto& NextHC : InHCs) + { + if(!IsValidWeakPointer(NextHC)) + continue; + + // Reset parameters to default values? + for(int32 n = 0; n < NextHC->GetNumParameters(); ++n) + { + UHoudiniParameter* NextParm = NextHC->GetParameterAt(n); + if(IsValid(NextParm) && !NextParm->IsDefault()) + { + NextParm->RevertToDefault(); + } + } + } +} + +void FHoudiniEngineDetails::CreateResetParametersButton(const TArray>& InHCs, TSharedRef ButtonHorizontalBox) +{ + auto ShouldEnableResetParametersButtonLambda = [InHCs]() + { + return ShouldEnableParametersButton(InHCs); + }; + + auto OnResetParametersClickedLambda = [InHCs]() + { + ResetParameters(InHCs); + return FReply::Handled(); + }; + + TSharedPtr HoudiniEngineUIResetParametersIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIResetParametersIconBrush(); + + TSharedPtr ResetParametersButton; + TSharedPtr ResetParametersButtonHorizontalBox; + ButtonHorizontalBox->AddSlot() + .MaxWidth(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + //.Padding(2.0f, 0.0f, 0.0f, 2.0f) + [ + SNew(SBox) + .WidthOverride(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SAssignNew(ResetParametersButton, SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("HoudiniAssetDetailsResetParametersAssetButton", "Reset the selected Houdini Asset's parameters to their default values.")) + //.Text(FText::FromString("Reset Parameters")) + .IsEnabled_Lambda(ShouldEnableResetParametersButtonLambda) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda(OnResetParametersClickedLambda) + .Content() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + [ + SAssignNew(ResetParametersButtonHorizontalBox, SHorizontalBox) + ] + ] + ] + ]; + + if(HoudiniEngineUIResetParametersIconBrush.IsValid()) + { + TSharedPtr ResetParametersImage; + ResetParametersButtonHorizontalBox->AddSlot() + .MaxWidth(16.0f) + //.Padding(0.0f, 0.0f, 3.0f, 0.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(ResetParametersImage, SImage) + //.ColorAndOpacity(FSlateColor::UseForeground()) + ] + ]; + + ResetParametersImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([HoudiniEngineUIResetParametersIconBrush]() + { + return HoudiniEngineUIResetParametersIconBrush.Get(); + }) + ) + ); + } + + ResetParametersButtonHorizontalBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + //.FillWidth(4.2f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + //.MinDesiredWidth(160.f) + .Text(FText::FromString("Reset Parameters")) + ]; +} + +void FHoudiniEngineDetails::CreateResetParametersOnlyWidgets( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InHCs) +{ + if(InHCs.Num() <= 0) + return; + + const TWeakObjectPtr& MainHC = InHCs[0]; + if(!IsValidWeakPointer(MainHC)) + return; + + FHoudiniEngineDetails::AddHeaderRowForCookable(HoudiniEngineCategoryBuilder, MainHC, HOUDINI_ENGINE_UI_SECTION_PARAMETER_RESET); + + // Button Row (draw only if expanded) + if(!MainHC->bGenerateMenuExpanded) + return; + + FDetailWidgetRow& ButtonRow = HoudiniEngineCategoryBuilder.AddCustomRow(FText::GetEmpty()); + TSharedRef ButtonHorizontalBox = SNew(SHorizontalBox); + ButtonRow.WholeRowWidget.Widget = ButtonHorizontalBox; + ButtonRow.IsEnabled(true); + + //---------------------------------------------------------------- + // Reset Parameters button + //---------------------------------------------------------------- + + CreateResetParametersButton(InHCs, ButtonHorizontalBox); +} + +void +FHoudiniEngineDetails::CreateGenerateWidgets( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InHCs, + const EHoudiniDetailsFlags& Flags) +{ + if (InHCs.Num() <= 0) + return; + + const TWeakObjectPtr& MainHC = InHCs[0]; + if (!IsValidWeakPointer(MainHC)) + return; + + auto OnReBuildClickedLambda = [InHCs]() + { + for (auto& NextHC : InHCs) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + NextHC->MarkAsNeedRebuild(); + } + + return FReply::Handled(); + }; + + auto OnRecookClickedLambda = [InHCs]() + { + for(auto& NextHC : InHCs) + { + if(!IsValidWeakPointer(NextHC)) + continue; + + NextHC->MarkAsNeedCook(); + } + return FReply::Handled(); + }; + + auto OnCookFolderTextCommittedLambda = [InHCs, MainHC](const FText& Val, ETextCommit::Type TextCommitType) + { + SetFolderPath(Val, false, MainHC, InHCs); + }; + + auto OnCookFolderBrowseButtonClickedLambda = [InHCs, MainHC]() + { + TSharedRef Dialog = + SNew(SSelectFolderPathDialog) + .InitialPath(FText::FromString(MainHC->GetTemporaryCookFolderOrDefault())) + .TitleText(LOCTEXT("CookFolderDialogTitle", "Select Temporary Cook Folder")); + + if (Dialog->ShowModal() != EAppReturnType::Cancel) + { + SetFolderPath(Dialog->GetFolderPath(), false, MainHC, InHCs); + } + + return FReply::Handled(); + }; + + auto OnCookFolderResetButtonClickedLambda = [InHCs, MainHC]() + { + FText EmptyText; + SetFolderPath(EmptyText, false, MainHC, InHCs); + + return FReply::Handled(); + }; + + FHoudiniEngineDetails::AddHeaderRowForCookable(HoudiniEngineCategoryBuilder, MainHC, HOUDINI_ENGINE_UI_SECTION_GENERATE); + + // Button Row (draw only if expanded) + if (!MainHC->bGenerateMenuExpanded) + return; + + TSharedPtr HoudiniEngineUIRebuildIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIRebuildIconBrush(); + TSharedPtr HoudiniEngineUIRecookIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIRecookIconBrush(); + + FDetailWidgetRow& ButtonRow = HoudiniEngineCategoryBuilder.AddCustomRow(FText::GetEmpty()); + TSharedRef ButtonHorizontalBox = SNew(SHorizontalBox); + + // We will need to hide some UI elements in the asset editor + bool bIsAssetEditor = !MainHC->AssetEditorId.IsNone(); + + //---------------------------------------------------------------- + // Recook button + //---------------------------------------------------------------- + TSharedPtr RecookButton; + TSharedPtr RecookButtonHorizontalBox; + ButtonHorizontalBox->AddSlot() + .MaxWidth(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + //.Padding(2.0f, 0.0f, 0.0f, 2.0f) + [ + SNew(SBox) + .WidthOverride(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SAssignNew(RecookButton, SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("HoudiniAssetDetailsRecookAssetButton", "Recook the selected Houdini Asset: all parameters and inputs are re-upload to Houdini and the asset is then forced to recook.")) + //.Text(FText::FromString("Recook")) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda(OnRecookClickedLambda) + .Content() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + [ + SAssignNew(RecookButtonHorizontalBox, SHorizontalBox) + ] + ] + ] + ]; + + if (HoudiniEngineUIRecookIconBrush.IsValid()) + { + TSharedPtr RecookImage; + RecookButtonHorizontalBox->AddSlot() + .MaxWidth(16.0f) + //.Padding(23.0f, 0.0f, 3.0f, 0.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(RecookImage, SImage) + //.ColorAndOpacity(FSlateColor::UseForeground()) + ] + ]; + + RecookImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([HoudiniEngineUIRecookIconBrush]() + { + return HoudiniEngineUIRecookIconBrush.Get(); + }) + ) + ); + } + + RecookButtonHorizontalBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + //.MaxWidth(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(FText::FromString("Recook")) + ]; + + + + //---------------------------------------------------------------- + // Rebuild button + //---------------------------------------------------------------- + TSharedPtr RebuildButton; + TSharedPtr RebuildButtonHorizontalBox; + ButtonHorizontalBox->AddSlot() + .MaxWidth(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + //.Padding(15.0f, 0.0f, 0.0f, 2.0f) + //.Padding(2.0f, 0.0f, 0.0f, 2.0f) + [ + SNew(SBox) + .WidthOverride(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SAssignNew(RebuildButton, SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("HoudiniAssetDetailsRebuildAssetButton", "Rebuild the selected Houdini Asset: its source .HDA file is reimported and updated, the asset's nodes in Houdini are destroyed and recreated, and the asset is then forced to recook.")) + //.Text(FText::FromString("Rebuild")) + .Visibility(EVisibility::Visible) + .Content() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + [ + SAssignNew(RebuildButtonHorizontalBox, SHorizontalBox) + ] + ] + .OnClicked_Lambda(OnReBuildClickedLambda) + ] + ]; + + if (HoudiniEngineUIRebuildIconBrush.IsValid()) + { + TSharedPtr RebuildImage; + RebuildButtonHorizontalBox->AddSlot() + //.Padding(25.0f, 0.0f, 3.0f, 0.0f) + //.Padding(2.0f, 0.0f, 0.0f, 2.0f) + .MaxWidth(16.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(RebuildImage, SImage) + //.ColorAndOpacity(FSlateColor::UseForeground()) + ] + ]; + + RebuildImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([HoudiniEngineUIRebuildIconBrush]() + { + return HoudiniEngineUIRebuildIconBrush.Get(); + }) + ) + ); + } + + RebuildButtonHorizontalBox->AddSlot() + .VAlign(VAlign_Center) + .AutoWidth() + .Padding(5.0, 0.0, 0.0, 0.0) + [ + SNew(STextBlock) + .Text(FText::FromString("Rebuild")) + ]; + + ButtonRow.WholeRowWidget.Widget = ButtonHorizontalBox; + ButtonRow.IsEnabled(true); + + //---------------------------------------------------------------- + // Reset Parameters button + //---------------------------------------------------------------- + bool bParameterSupported = MainHC->IsParameterSupported(); + if (bParameterSupported) + { + CreateResetParametersButton(InHCs, ButtonHorizontalBox); + } + + if(Flags.bTemporaryCookFolderRow) + { + //---------------------------------------------------------------- + // Temp Cook Folder Row + //---------------------------------------------------------------- + FDetailWidgetRow& TempCookFolderRow = HoudiniEngineCategoryBuilder.AddCustomRow(FText::FromString("Temporary Cook Folder")); + + TSharedRef TempCookFolderRowHorizontalBox = SNew(SHorizontalBox); + + TempCookFolderRowHorizontalBox->AddSlot() + .MaxWidth(155.0f) + .VAlign(VAlign_Center) + [ + SNew(SBox) + .WidthOverride(155.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniEngineTemporaryCookFolderLabel", "Temporary Cook Folder")) + .ToolTipText(LOCTEXT( + "HoudiniEngineTemporaryCookFolderTooltip", + "Default folder used to store the temporary files (Static Meshes, Materials, Textures..) that are " + "generated by Houdini Assets when they cook. If this value is blank, the default from the plugin " + "settings is used.")) + ] + ]; + + TempCookFolderRowHorizontalBox->AddSlot() + //.MaxWidth(235.0f) + [ + SNew(SBox) + //.WidthOverride(235.0f) + .WidthOverride(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SNew(SEditableTextBox) + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + .ToolTipText(LOCTEXT( + "HoudiniEngineTemporaryCookFolderTooltip", + "Default folder used to store the temporary files (Static Meshes, Materials, Textures..) that are " + "generated by Houdini Assets when they cook. If this value is blank, the default from the plugin " + "settings is used.")) + .HintText(LOCTEXT("HoudiniEngineTempCookFolderHintText", "Input to set temporary cook folder")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text_Lambda([MainHC]() + { + if (!IsValidWeakPointer(MainHC)) + return FText(); + return FText::FromString(MainHC->GetTemporaryCookFolderOrDefault()); + }) + .OnTextCommitted_Lambda(OnCookFolderTextCommittedLambda) + ] + ]; + + TempCookFolderRowHorizontalBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SButton) + //.ContentPadding(FMargin(6.0, 2.0)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled(true) + .Text(LOCTEXT("BrowseButtonText", "Browse")) + .ToolTipText(LOCTEXT("CookFolderBrowseButtonToolTip", "Browse to select temporary cook folder")) + .OnClicked_Lambda(OnCookFolderBrowseButtonClickedLambda) + ]; + + TempCookFolderRowHorizontalBox->AddSlot() + .AutoWidth() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + [ + SNew(SButton) + //.ContentPadding(FMargin(6.0, 2.0)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled(true) + .Text(LOCTEXT("ResetButtonText", "Reset")) + .ToolTipText(LOCTEXT("CookFolderResetButtonToolTip", "Reset the cook folder to default setting")) + .OnClicked_Lambda(OnCookFolderResetButtonClickedLambda) + ]; + + TempCookFolderRow.WholeRowWidget.Widget = TempCookFolderRowHorizontalBox; + } +} + +void +FHoudiniEngineDetails::AddRemovedHDAOutputAfterBakeCheckBox( + const TWeakObjectPtr& MainHC, + const TArray>& InHCs, + TSharedPtr& LeftColumnVerticalBox) +{ + TSharedPtr CheckBoxRemoveOutput; + LeftColumnVerticalBox->AddSlot() + .AutoHeight() + .Padding(0.0f, 0.0f, 0.0f, 3.5f) + [ + SNew(SBox) + .WidthOverride(160.f) + [ + SAssignNew(CheckBoxRemoveOutput, SCheckBox) + .Content() + [ + SNew(STextBlock).Text(LOCTEXT("HoudiniEngineUIRemoveOutputCheckBox", "Remove HDA Output After Bake")) + .ToolTipText(LOCTEXT("HoudiniEngineUIRemoveOutputCheckBoxToolTip", "After baking the existing output of this Houdini Asset Actor will be removed.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([MainHC]() + { + if(!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->GetRemoveOutputAfterBake() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InHCs](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked); + + for(auto& NextHC : InHCs) + { + if(!IsValidWeakPointer(NextHC)) + continue; + + if(NextHC->GetRemoveOutputAfterBake() == bNewState) + continue; + + NextHC->SetRemoveOutputAfterBake(bNewState); + NextHC->MarkPackageDirty(); + } + }) + ] + ]; +} + +void +FHoudiniEngineDetails::AddRenterBakedActorsCheckbox( + const TWeakObjectPtr& MainHC, + const TArray>& InHCs, + TSharedPtr& LeftColumnVerticalBox) +{ + TSharedPtr CheckBoxRecenterBakedActors; + + LeftColumnVerticalBox->AddSlot() + .AutoHeight() + .Padding(0.0f, 0.0f, 0.0f, 3.5f) + [ + SNew(SBox) + .WidthOverride(160.f) + [ + SAssignNew(CheckBoxRecenterBakedActors, SCheckBox) + .Content() + [ + SNew(STextBlock).Text(LOCTEXT("HoudiniEngineUIRecenterBakedActorsCheckBox", "Recenter Baked Actors")) + .ToolTipText(LOCTEXT("HoudiniEngineUIRecenterBakedActorsCheckBoxToolTip", "After baking recenter the baked actors to their bounding box center.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([MainHC]() + { + if(!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->GetRecenterBakedActors() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InHCs](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked); + + for(auto& NextHC : InHCs) + { + if(!IsValidWeakPointer(NextHC)) + continue; + + if(NextHC->GetRecenterBakedActors() == bNewState) + continue; + + NextHC->SetRecenterBakedActors(bNewState); + NextHC->MarkPackageDirty(); + } + + // FHoudiniEngineUtils::UpdateEditorProperties(MainHC, true); + }) + ] + ]; +} + +void FHoudiniEngineDetails::AddAutoBakeCheckbox( + const TWeakObjectPtr& MainHC, + const TArray>& InHCs, + TSharedPtr& RightColumnVerticalBox) +{ + TSharedPtr CheckBoxAutoBake; + + RightColumnVerticalBox->AddSlot() + .AutoHeight() + .Padding(0.0f, 0.0f, 0.0f, 3.5f) + [ + SNew(SBox) + .WidthOverride(160.f) + [ + SAssignNew(CheckBoxAutoBake, SCheckBox) + .Content() + [ + SNew(STextBlock).Text(LOCTEXT("HoudiniEngineUIAutoBakeCheckBox", "Auto Bake")) + .ToolTipText(LOCTEXT("HoudiniEngineUIAutoBakeCheckBoxToolTip", "Automatically bake the next cook.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([MainHC]() + { + if(!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->IsBakeAfterNextCookEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InHCs](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked); + + for(auto& NextHC : InHCs) + { + if(!IsValidWeakPointer(NextHC)) + continue; + + if(NextHC->IsBakeAfterNextCookEnabled() == bNewState) + continue; + + NextHC->SetBakeAfterNextCook(bNewState ? EHoudiniBakeAfterNextCook::Always : EHoudiniBakeAfterNextCook::Disabled); + NextHC->MarkPackageDirty(); + } + + // FHoudiniEngineUtils::UpdateEditorProperties(MainHC, true); + }) + ] + ]; + +} + + +void FHoudiniEngineDetails::AddReplaceCheckbox( + const TWeakObjectPtr& MainHC, + const TArray>& InHCs, + TSharedPtr& RightColumnVerticalBox) +{ + TSharedPtr CheckBoxReplacePreviousBake; + + // Replace Checkbox + RightColumnVerticalBox->AddSlot() + .AutoHeight() + .Padding(0.0f, 0.0f, 0.0f, 3.5f) + [ + SNew(SBox) + .WidthOverride(160.f) + [ + SAssignNew(CheckBoxReplacePreviousBake, SCheckBox) + .Content() + [ + SNew(STextBlock).Text(LOCTEXT("HoudiniEngineUIBakeReplaceWithPreviousCheckBox", "Replace Previous Bake")) + .ToolTipText(LOCTEXT("HoudiniEngineUIBakeReplaceWithPreviousCheckBoxToolTip", "When baking replace the previous bake's output instead of creating additional output actors/components/objects.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([MainHC]() + { + if(!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->GetReplacePreviousBake() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([MainHC, InHCs](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked); + + for(auto& NextHC : InHCs) + { + if(!IsValidWeakPointer(NextHC)) + continue; + + if(NextHC->GetReplacePreviousBake() == bNewState) + continue; + + NextHC->SetReplacePreviousBake(bNewState); + NextHC->MarkPackageDirty(); + } + + if(MainHC.IsValid()) + FHoudiniEngineUtils::UpdateEditorProperties(true); + }) + ] + ]; +} + +void +FHoudiniEngineDetails::AddBakeFolderSelector(IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, const TWeakObjectPtr& MainHC, const TArray>& InHCs) +{ + auto OnBakeFolderTextCommittedLambda = [InHCs, MainHC](const FText& Val, ETextCommit::Type TextCommitType) + { + SetFolderPath(Val, true, MainHC, InHCs); + }; + + // Bake Folder Row + FDetailWidgetRow& BakeFolderRow = HoudiniEngineCategoryBuilder.AddCustomRow(FText::FromString("Bake Folder")); + + TSharedRef BakeFolderRowHorizontalBox = SNew(SHorizontalBox); + + BakeFolderRowHorizontalBox->AddSlot() + .MaxWidth(155.0f) + .VAlign(VAlign_Center) + [ + SNew(SBox) + .WidthOverride(155.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniEngineBakeFolderLabel", "Bake Folder")) + .ToolTipText(LOCTEXT( + "HoudiniEngineBakeFolderTooltip", + "The folder used to store the objects that are generated by this Houdini Asset when baking, if the " + "unreal_bake_folder attribute is not set on the geometry. If this value is blank, the default from the " + "plugin settings is used.")) + ] + ]; + + BakeFolderRowHorizontalBox->AddSlot() + .MaxWidth(235.0) + [ + SNew(SBox) + .WidthOverride(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SNew(SEditableTextBox) + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + .ToolTipText(LOCTEXT( + "HoudiniEngineBakeFolderTooltip", + "The folder used to store the objects that are generated by this Houdini Asset when baking, if the " + "unreal_bake_folder attribute is not set on the geometry. If this value is blank, the default from the " + "plugin settings is used.")) + .HintText(LOCTEXT("HoudiniEngineBakeFolderHintText", "Input to set bake folder")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text_Lambda([MainHC]() + { + if(!IsValidWeakPointer(MainHC)) + return FText(); + return FText::FromString(MainHC->GetBakeFolderOrDefault()); + }) + .OnTextCommitted_Lambda(OnBakeFolderTextCommittedLambda) + ] + ]; + + TArray>* ActorBakeOptionSources = FHoudiniEngineEditor::Get().GetHoudiniEngineBakeActorOptionsLabels(); + + auto OnBakeFolderBrowseButtonClickedLambda = [BakeFolderRowHorizontalBox, MainHC, InHCs]() + { + TSharedRef Dialog = + SNew(SSelectFolderPathDialog) + .InitialPath(FText::FromString(MainHC->GetBakeFolderOrDefault())) + .TitleText(LOCTEXT("BakeFolderDialogTitle", "Select Bake Folder")); + + if(Dialog->ShowModal() != EAppReturnType::Cancel) + { + SetFolderPath(Dialog->GetFolderPath(), true, MainHC, InHCs); + } + + return FReply::Handled(); + }; + + auto OnBakeFolderResetButtonClickedLambda = [MainHC, InHCs]() + { + FText EmptyText; + SetFolderPath(EmptyText, true, MainHC, InHCs); + + return FReply::Handled(); + }; + + BakeFolderRowHorizontalBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SButton) + //.ContentPadding(FMargin(6.0, 2.0)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled(true) + .Text(LOCTEXT("BrowseButtonText", "Browse")) + .ToolTipText(LOCTEXT("BakeFolderBrowseButtonToolTip", "Browse to select bake folder")) + .OnClicked_Lambda(OnBakeFolderBrowseButtonClickedLambda) + ]; + + BakeFolderRowHorizontalBox->AddSlot() + .AutoWidth() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + [ + SNew(SButton) + //.ContentPadding(FMargin(6.0, 2.0)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled(true) + .Text(LOCTEXT("ResetButtonText", "Reset")) + .ToolTipText(LOCTEXT("BrowseButtonToolTip", "Reset the bake folder to default setting")) + .OnClicked_Lambda(OnBakeFolderResetButtonClickedLambda) + ]; + + + BakeFolderRow.WholeRowWidget.Widget = BakeFolderRowHorizontalBox; +} + +void +FHoudiniEngineDetails::AddBakeControlBar( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TWeakObjectPtr& MainHC, + const TArray>& InHCs, + EHoudiniDetailsFlags DetailsFlags) +{ + // Button Row + FDetailWidgetRow& ButtonRow = HoudiniEngineCategoryBuilder.AddCustomRow(FText::FromString("Bake")); + + TSharedRef ButtonRowHorizontalBox = SNew(SHorizontalBox); + + auto OnBakeButtonClickedLambda = [InHCs, MainHC]() + { + FHoudiniBakeSettings BakeSettings; + EHoudiniEngineBakeOption BakeOption; + bool bRemoveOutputAfterBake; + + BakeSettings.SetFromCookable(MainHC.Get()); + BakeOption = MainHC->GetBakingData()->HoudiniEngineBakeOption; + bRemoveOutputAfterBake = MainHC->GetBakingData()->bRemoveOutputAfterBake; + + for(auto& CurrentHC : InHCs) + { + if(!IsValidWeakPointer(CurrentHC)) + continue; + + FHoudiniEngineBakeUtils::BakeCookable( + CurrentHC.Get(), + BakeSettings, + BakeOption, + bRemoveOutputAfterBake); + } + + return FReply::Handled(); + }; + + // Bake Button + if (DetailsFlags.bBakeButton) + { + TSharedPtr BakeButton; + TSharedPtr BakeButtonHorizontalBox; + + ButtonRowHorizontalBox->AddSlot() + .MaxWidth(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SNew(SBox) + .WidthOverride(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SAssignNew(BakeButton, SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("HoudiniAssetDetailsBakeButton", "Bake the Houdini Asset Component(s).")) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda(OnBakeButtonClickedLambda) + .Content() + [ + SAssignNew(BakeButtonHorizontalBox, SHorizontalBox) + ] + ] + ]; + + TSharedPtr BakeIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIBakeIconBrush(); + if(BakeIconBrush.IsValid()) + { + TSharedPtr BakeImage; + BakeButtonHorizontalBox->AddSlot() + .MaxWidth(16.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(BakeImage, SImage) + ] + ]; + + BakeImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([BakeIconBrush]() { + return BakeIconBrush.Get(); + }))); + } + + BakeButtonHorizontalBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(FText::FromString("Bake")) + ]; + + switch(MainHC->GetHoudiniEngineBakeOption()) + { + case EHoudiniEngineBakeOption::ToActor: + { + if(MainHC->GetReplacePreviousBake()) + { + BakeButton->SetToolTipText(LOCTEXT("HoudiniEngineBakeButtonBakeWithReplaceToActorToolTip", + "Bake this Houdini Asset Actor and its components to native unreal actors and components, replacing the previous baked result.")); + } + else + { + BakeButton->SetToolTipText(LOCTEXT("HoudiniEngineBakeButtonBakeToActorToolTip", + "Bake this Houdini Asset Actor and its components to native unreal actors and components.")); + } + } + break; + + case EHoudiniEngineBakeOption::ToBlueprint: + { + BakeButton->SetToolTipText(LOCTEXT("HoudiniEngineBakeButtonBakeToBlueprintToolTip", + "Bake this Houdini Asset Actor to a blueprint.")); + } + break; + + case EHoudiniEngineBakeOption::ToAsset: + { + BakeButton->SetToolTipText(LOCTEXT("HoudiniEngineBakeButtonBakeToAssetToolTip", + "Bake this Houdini Asset to native Unreal assets in the content browser.")); + } + break; + } + } + + // Bake Type ComboBox + TSharedPtr>> TypeComboBox; + TArray>* BakeOptionSources = FHoudiniEngineEditor::Get().GetHoudiniEngineBakeTypeOptionsLabels(); + TSharedPtr IntialSelec = MakeShareable(new FString(FHoudiniEngineEditor::Get().GetStringFromHoudiniEngineBakeOption(MainHC->GetHoudiniEngineBakeOption()))); + ButtonRowHorizontalBox->AddSlot() + /*.AutoWidth()*/ + .Padding(3.0, 0.0, 4.0f, 0.0f) + .MaxWidth(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SNew(SBox) + .WidthOverride(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SAssignNew(TypeComboBox, SComboBox>) + .OptionsSource(BakeOptionSources) + .InitiallySelectedItem(IntialSelec) + .OnGenerateWidget_Lambda( + [](TSharedPtr< FString > InItem) + { + FText ChoiceEntryText = FText::FromString(*InItem); + return SNew(STextBlock) + .Text(ChoiceEntryText) + .ToolTipText(ChoiceEntryText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + }) + .OnSelectionChanged_Lambda( + [MainHC, InHCs](TSharedPtr NewChoice, ESelectInfo::Type SelectType) + { + if(!NewChoice.IsValid()) + return; + + const EHoudiniEngineBakeOption NewOption = + FHoudiniEngineEditor::Get().StringToHoudiniEngineBakeOption(*NewChoice.Get()); + + for(auto& NextHC : InHCs) + { + if(!IsValidWeakPointer(NextHC)) + continue; + + NextHC->SetHoudiniEngineBakeOption(NewOption); + NextHC->MarkPackageDirty(); + } + + if(MainHC.IsValid()) + FHoudiniEngineUtils::UpdateEditorProperties(true); + }) + [ + SNew(STextBlock) + .Text_Lambda([MainHC]() + { + if(!IsValidWeakPointer(MainHC)) + return FText(); + + return FText::FromString( + FHoudiniEngineEditor::Get().GetStringFromHoudiniEngineBakeOption(MainHC->GetHoudiniEngineBakeOption())); + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + ]; + + TArray>* ActorBakeOptionSources = FHoudiniEngineEditor::Get().GetHoudiniEngineBakeActorOptionsLabels(); + ButtonRowHorizontalBox->AddSlot() + /*.AutoWidth()*/ + .Padding(3.0, 0.0, 4.0f, 0.0f) + //.MaxWidth(103.f) + .MaxWidth(HOUDINI_ENGINE_UI_BUTTON_WIDTH * 1.5f) + [ + SNew(SBox) + //.WidthOverride(103.f) + .WidthOverride(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SAssignNew(TypeComboBox, SComboBox>) + .OptionsSource(ActorBakeOptionSources) + .InitiallySelectedItem(IntialSelec) + .IsEnabled_Lambda([MainHC]() + { + // Only enabled when in "Bake To Actor" mode + return (MainHC->GetHoudiniEngineBakeOption() == EHoudiniEngineBakeOption::ToActor); + }) + .OnGenerateWidget_Lambda( + [](TSharedPtr< FString > InItem) + { + FText ChoiceEntryText = FText::FromString(*InItem); + return SNew(STextBlock) + .Text(ChoiceEntryText) + .ToolTipText(ChoiceEntryText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + }) + .OnSelectionChanged_Lambda( + [MainHC, InHCs](TSharedPtr< FString > NewChoice, ESelectInfo::Type SelectType) + { + if(!NewChoice.IsValid()) + return; + + const EHoudiniEngineActorBakeOption NewOption = + FHoudiniEngineEditor::Get().StringToHoudiniEngineActorBakeOption(*NewChoice.Get()); + + for(auto& NextHC : InHCs) + { + if(!IsValidWeakPointer(NextHC)) + continue; + + if(NextHC->GetActorBakeOption() == NewOption) + continue; + + NextHC->SetActorBakeOption(NewOption); + NextHC->MarkPackageDirty(); + } + + if(MainHC.IsValid()) + FHoudiniEngineUtils::UpdateEditorProperties(true); + }) + [ + SNew(STextBlock) + .Text_Lambda([MainHC]() + { + if(!IsValidWeakPointer(MainHC)) + return FText(); + + return FText::FromString( + FHoudiniEngineEditor::GetStringfromActorBakeOption(MainHC->GetActorBakeOption())); + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + ]; + + ButtonRow.WholeRowWidget.Widget = ButtonRowHorizontalBox; + +} + + + +void +FHoudiniEngineDetails::CreateBakeWidgets( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InHCs, + const EHoudiniDetailsFlags& DetailsFlags) +{ + if (InHCs.Num() <= 0) + return; + + const TWeakObjectPtr& MainHC = InHCs[0]; + if (!IsValidWeakPointer(MainHC)) + return; + + if (!MainHC->IsBakingSupported() && !DetailsFlags.bDisplayOnOutputLess) + return; + + FHoudiniEngineDetails::AddHeaderRowForCookable(HoudiniEngineCategoryBuilder, MainHC, HOUDINI_ENGINE_UI_SECTION_BAKE); + + if (!MainHC->bBakeMenuExpanded) + return; + + // Button Row + AddBakeControlBar(HoudiniEngineCategoryBuilder, MainHC, InHCs, DetailsFlags); + + TSharedRef AdditionalBakeSettingsRowHorizontalBox = SNew(SHorizontalBox); + + TSharedPtr LeftColumnVerticalBox; + TSharedPtr RightColumnVerticalBox; + + AdditionalBakeSettingsRowHorizontalBox->AddSlot() + .Padding(30.0f, 5.0f, 0.0f, 0.0f) + .MaxWidth(200.f) + [ + SNew(SBox) + .WidthOverride(200.f) + [ + SAssignNew(LeftColumnVerticalBox, SVerticalBox) + ] + ]; + + AdditionalBakeSettingsRowHorizontalBox->AddSlot() + .Padding(20.0f, 5.0f, 0.0f, 0.0f) + .MaxWidth(200.f) + [ + SNew(SBox) + [ + SAssignNew(RightColumnVerticalBox, SVerticalBox) + ] + ]; + + if(DetailsFlags.bRemoveHDAOutputAfterBake) + AddRemovedHDAOutputAfterBakeCheckBox(MainHC, InHCs, LeftColumnVerticalBox); + + AddRenterBakedActorsCheckbox(MainHC, InHCs, LeftColumnVerticalBox); + + if (DetailsFlags.bAutoBake) + AddAutoBakeCheckbox(MainHC, InHCs, RightColumnVerticalBox); + + if (DetailsFlags.bReplacePreviousBake) + AddReplaceCheckbox(MainHC, InHCs, RightColumnVerticalBox); + + // Clear Output After Baking Row + FDetailWidgetRow& ClearOutputAfterBakingRow = HoudiniEngineCategoryBuilder.AddCustomRow(FText::FromString("Bake Options")); + ClearOutputAfterBakingRow.WholeRowWidget.Widget = AdditionalBakeSettingsRowHorizontalBox; + + AddBakeFolderSelector(HoudiniEngineCategoryBuilder, MainHC, InHCs); +} + +void +FHoudiniEngineDetails::CreateAssetOptionsWidgets( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InHCs, + const EHoudiniDetailsFlags& DetailsFlags) +{ + if (InHCs.Num() <= 0) + return; + + const TWeakObjectPtr& MainHC = InHCs[0]; + if (!IsValidWeakPointer(MainHC)) + return; + + // Header Row + FHoudiniEngineDetails::AddHeaderRowForCookable(HoudiniEngineCategoryBuilder, MainHC, HOUDINI_ENGINE_UI_SECTION_ASSET_OPTIONS); + if (!MainHC->bAssetOptionMenuExpanded) + return; + + auto IsCheckedParameterChangedLambda = [MainHC]() + { + if (!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->GetCookOnParameterChange() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto OnCheckStateParameterChangedLambda = [InHCs](ECheckBoxState NewState) + { + bool bChecked = (NewState == ECheckBoxState::Checked); + for (auto& NextHC : InHCs) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + if (NextHC->GetCookOnParameterChange() == bChecked) + continue; + + NextHC->SetCookOnParameterChange(bChecked); + NextHC->MarkPackageDirty(); + } + }; + + auto IsCheckedInputChangedLambda = [MainHC]() + { + if (!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->GetCookOnInputChange() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto OnCheckStateInputChangedLambda = [InHCs](ECheckBoxState NewState) + { + bool bChecked = (NewState == ECheckBoxState::Checked); + for (auto& NextHC : InHCs) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + if (NextHC->GetCookOnInputChange() == bChecked) + continue; + + NextHC->SetCookOnInputChange(bChecked); + NextHC->MarkPackageDirty(); + } + }; + + auto IsCheckedTransformChangeLambda = [MainHC]() + { + if (!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->GetCookOnTransformChange() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto OnCheckStateChangedTransformChangeLambda = [InHCs](ECheckBoxState NewState) + { + const bool bChecked = (NewState == ECheckBoxState::Checked); + for (auto& NextHC : InHCs) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + if (NextHC->GetCookOnTransformChange() == bChecked) + continue; + + NextHC->SetCookOnTransformChange(bChecked); + NextHC->MarkPackageDirty(); + NextHC->MarkAsNeedCook(); + } + }; + + auto IsCheckedAssetInputCookLambda = [MainHC]() + { + if (!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->GetCookOnCookableInputCook() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto OnCheckStateChangedAssetInputCookLambda = [InHCs](ECheckBoxState NewState) + { + const bool bChecked = (NewState == ECheckBoxState::Checked); + for (auto& NextHC : InHCs) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + if (NextHC->GetCookOnCookableInputCook() == bChecked) + continue; + + NextHC->SetCookOnCookableInputCook(bChecked); + NextHC->MarkPackageDirty(); + } + }; + + auto IsCheckedPushTransformToHoudiniLambda = [MainHC]() + { + if (!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->GetUploadTransformsToHoudiniEngine() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto OnCheckStateChangedPushTransformToHoudiniLambda = [InHCs](ECheckBoxState NewState) + { + const bool bChecked = (NewState == ECheckBoxState::Checked); + for (auto& NextHC : InHCs) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + if (NextHC->GetUploadTransformsToHoudiniEngine() == bChecked) + continue; + + NextHC->SetUploadTransformsToHoudiniEngine(bChecked); + NextHC->MarkPackageDirty(); + NextHC->MarkAsNeedCook(); + } + }; + + auto IsCheckedUseTempLandscapesLayersToHoudiniLambda = [MainHC]() + { + if (!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->GetLandscapeUseTempLayers() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto OnCheckStateChangedUseTempLandscapeLayersLambda = [InHCs](ECheckBoxState NewState) + { + const bool bChecked = (NewState == ECheckBoxState::Checked); + for (auto& NextHC : InHCs) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + if (NextHC->GetLandscapeUseTempLayers() == bChecked) + continue; + + NextHC->SetLandscapeUseTempLayers(bChecked); + NextHC->MarkPackageDirty(); + } + }; + + auto IsCheckedEnableCurveEditingLambda = [MainHC]() + { + if (!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->GetEnableCurveEditing() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto OnCheckStateChangedEnableCurveEditingLambda = [InHCs](ECheckBoxState NewState) + { + const bool bChecked = (NewState == ECheckBoxState::Checked); + for (auto& NextHC : InHCs) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + if (NextHC->GetEnableCurveEditing() == bChecked) + continue; + + NextHC->SetEnableCurveEditing(bChecked); + NextHC->MarkPackageDirty(); + } + }; + + auto IsCheckedDoNotGenerateOutputsLambda = [MainHC]() + { + if (!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->IsOutputless() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto OnCheckStateChangedDoNotGenerateOutputsLambda = [InHCs](ECheckBoxState NewState) + { + const bool bChecked = (NewState == ECheckBoxState::Checked); + for (auto& NextHC : InHCs) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + if (NextHC->IsOutputless() == bChecked) + continue; + + NextHC->SetOutputless(bChecked); + NextHC->MarkPackageDirty(); + NextHC->MarkAsNeedCook(); + } + }; + + auto IsCheckedOutputTemplatedGeosLambda = [MainHC]() + { + if (!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->GetOutputTemplateGeos() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto OnCheckStateChangedOutputTemplatedGeosLambda = [InHCs](ECheckBoxState NewState) + { + const bool bChecked = (NewState == ECheckBoxState::Checked); + for (auto& NextHC : InHCs) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + if (NextHC->GetOutputTemplateGeos() == bChecked) + continue; + + NextHC->SetOutputTemplateGeos(bChecked); + NextHC->MarkPackageDirty(); + NextHC->MarkAsNeedCook(); + } + }; + + auto IsCheckedUseOutputNodesLambda = [MainHC]() + { + if (!IsValidWeakPointer(MainHC)) + return ECheckBoxState::Unchecked; + + return MainHC->GetUseOutputNodes() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto OnCheckStateChangedUseOutputNodesLambda = [InHCs](ECheckBoxState NewState) + { + const bool bChecked = (NewState == ECheckBoxState::Checked); + for (auto& NextHC : InHCs) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + if (NextHC->GetUseOutputNodes() == bChecked) + continue; + + NextHC->SetUseOutputNodes(bChecked); + NextHC->MarkPackageDirty(); + NextHC->MarkAsNeedCook(); + } + }; + + // Checkboxes row + FDetailWidgetRow & CheckBoxesRow = HoudiniEngineCategoryBuilder.AddCustomRow(FText::FromString("Asset Cook Options")); + TSharedPtr FirstLeftColumnVerticalBox; + TSharedPtr FirstRightColumnVerticalBox; + TSharedPtr SecondLeftColumnVerticalBox; + TSharedPtr SecondRightColumnVerticalBox; + TSharedRef WidgetBox = SNew(SHorizontalBox); + WidgetBox->AddSlot() + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + //First Line + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(30.0f, 5.0f, 0.0f, 0.0f) + .MaxWidth(200.f) + [ + // First Left + SNew(SBox) + .WidthOverride(200.f) + [ + SAssignNew(FirstLeftColumnVerticalBox, SVerticalBox) + ] + ] + + SHorizontalBox::Slot() + .Padding(20.0f, 5.0f, 0.0f, 0.0f) + .MaxWidth(200.f) + [ + // First Right + SNew(SBox) + [ + SAssignNew(FirstRightColumnVerticalBox, SVerticalBox) + ] + ] + ] + + SVerticalBox::Slot() + .AutoHeight() + [ + //Second Line + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(30.0f, 5.0f, 0.0f, 0.0f) + .MaxWidth(200.f) + [ + // Second Left + SNew(SBox) + .WidthOverride(200.f) + [ + SAssignNew(SecondLeftColumnVerticalBox, SVerticalBox) + ] + ] + + SHorizontalBox::Slot() + .Padding(20.0f, 5.0f, 0.0f, 0.0f) + .MaxWidth(200.f) + [ + // Second Right + SNew(SBox) + [ + SAssignNew(SecondRightColumnVerticalBox, SVerticalBox) + ] + ] + ] + ]; + + FText TooltipText; + + //---------------------------------------------------------------------------- + // First line - left - Cook Triggers + //---------------------------------------------------------------------------- + if (DetailsFlags.bCookTriggers) + { + FirstLeftColumnVerticalBox->AddSlot() + .AutoHeight() + .Padding(0.0f, 0.0f, 0.0f, 3.5f) + [ + SNew(SBox) + .WidthOverride(160.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniEngineCookTriggersLabel", "Cook Triggers")) + ] + ]; + + if (MainHC->IsParameterSupported()) + { + // Cook on Parameter change check box + TooltipText = LOCTEXT("HoudiniEngineParameterChangeTooltip", + "If enabled, modifying a parameter on this Houdini Asset will automatically trigger a cook of the HDA in Houdini."); + FirstLeftColumnVerticalBox->AddSlot() + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(4.0f) + [ + SNew(STextBlock) + .MinDesiredWidth(160.f) + .Text(LOCTEXT("HoudiniEngineParameterChangeCheckBoxLabel", "On Parameter Change")) + .ToolTipText(TooltipText) + ] + + SHorizontalBox::Slot() + [ + SNew(SCheckBox) + .OnCheckStateChanged_Lambda(OnCheckStateParameterChangedLambda) + .IsChecked_Lambda(IsCheckedParameterChangedLambda) + .ToolTipText(TooltipText) + ] + ]; + } + + if (MainHC->IsInputSupported()) + { + // Cook on Input change check box + TooltipText = LOCTEXT("HoudiniEngineInputChangeTooltip", + "If enabled, any change on an input of this Houdini Asset will automatically trigger a cook of the HDA in Houdini."); + FirstLeftColumnVerticalBox->AddSlot() + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(4.0f) + [ + SNew(STextBlock) + .MinDesiredWidth(160.f) + .Text(LOCTEXT("HoudiniEngineInputChangeCheckBoxLabel", "On Input Change")) + .ToolTipText(TooltipText) + ] + + SHorizontalBox::Slot() + [ + SNew(SCheckBox) + .OnCheckStateChanged_Lambda(OnCheckStateInputChangedLambda) + .IsChecked_Lambda(IsCheckedInputChangedLambda) + .ToolTipText(TooltipText) + ] + ]; + } + + if (MainHC->IsComponentSupported()) + { + // Transform change check box + TooltipText = LOCTEXT("HoudiniEngineTransformChangeTooltip", "If enabled, changing the Houdini Asset Actor's transform in Unreal will also update its HDA's node transform in Houdini, and trigger a recook of the HDA with the updated transform."); + FirstLeftColumnVerticalBox->AddSlot() + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(4.0f) + [ + SNew(STextBlock) + .MinDesiredWidth(160.f) + .Text(LOCTEXT("HoudiniEngineTransformChangeCheckBoxLabel", "On Transform Change")) + .ToolTipText(TooltipText) + ] + + SHorizontalBox::Slot() + [ + SNew(SCheckBox) + .OnCheckStateChanged_Lambda(OnCheckStateChangedTransformChangeLambda) + .IsChecked_Lambda(IsCheckedTransformChangeLambda) + .ToolTipText(TooltipText) + ] + ]; + } + + if (MainHC->IsInputSupported()) + { + // Triggers Downstream cook checkbox + TooltipText = LOCTEXT("HoudiniEngineAssetInputCookTooltip", "When enabled, this asset will automatically re-cook after one its Houdini asset input has finished cooking."); + FirstLeftColumnVerticalBox->AddSlot() + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(4.0f) + [ + SNew(STextBlock) + .MinDesiredWidth(160.f) + .Text(LOCTEXT("HoudiniEngineAssetInputCheckBoxLabel", "On HDA Input Cook")) + .ToolTipText(TooltipText) + ] + + SHorizontalBox::Slot() + [ + SNew(SCheckBox) + .OnCheckStateChanged_Lambda(OnCheckStateChangedAssetInputCookLambda) + .IsChecked_Lambda(IsCheckedAssetInputCookLambda) + .ToolTipText(TooltipText) + ] + ]; + } + } + + //---------------------------------------------------------------------------- + // First line - right - Outputs + //---------------------------------------------------------------------------- + if (MainHC->IsOutputSupported()) + { + FirstRightColumnVerticalBox->AddSlot() + .AutoHeight() + .Padding(0.0f, 0.0f, 0.0f, 3.5f) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniEngineOutputLabel", "Outputs")) + ]; + + //------------------------------------------------------------------------ + // Do not generate output check box + //------------------------------------------------------------------------ + if (DetailsFlags.bDoNotGenerateOutputs) + { + TooltipText = LOCTEXT("HoudiniEnginOutputlessTooltip", "If enabled, this Houdini Asset will cook normally but will not generate any output in Unreal. This is especially usefull when chaining multiple assets together via Asset Inputs."); + FirstRightColumnVerticalBox->AddSlot() + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(4.0f) + [ + SNew(STextBlock) + .MinDesiredWidth(160.f) + .Text(LOCTEXT("HoudiniEngineDoNotGenerateOutputsCheckBoxLabel", "Do Not Generate Outputs")) + .ToolTipText(TooltipText) + ] + + SHorizontalBox::Slot() + [ + SNew(SCheckBox) + .OnCheckStateChanged_Lambda(OnCheckStateChangedDoNotGenerateOutputsLambda) + .IsChecked_Lambda(IsCheckedDoNotGenerateOutputsLambda) + .ToolTipText(TooltipText) + ] + ]; + } + + //------------------------------------------------------------------------ + // Use Output Nodes check box + //------------------------------------------------------------------------ + TooltipText = LOCTEXT("HoudiniEnginUseOutputNodesTooltip", "If enabled, Output nodes found in this Houdini asset will be used alongside the Display node to create outputs."); + FirstRightColumnVerticalBox->AddSlot() + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(4.0f) + [ + SNew(STextBlock) + .MinDesiredWidth(160.f) + .Text(LOCTEXT("HoudiniEnginUseOutputNodesCheckBoxLabel", "Use Output Nodes")) + .ToolTipText(TooltipText) + ] + + SHorizontalBox::Slot() + [ + SNew(SCheckBox) + .OnCheckStateChanged_Lambda(OnCheckStateChangedUseOutputNodesLambda) + .IsChecked_Lambda(IsCheckedUseOutputNodesLambda) + .ToolTipText(TooltipText) + ] + ]; + + //------------------------------------------------------------------------ + // Output templated geos check box + //------------------------------------------------------------------------ + TooltipText = LOCTEXT("HoudiniEnginOutputTemplatesTooltip", "If enabled, Geometry nodes in the asset that have the template flag will be outputed."); + FirstRightColumnVerticalBox->AddSlot() + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(4.0f) + [ + SNew(STextBlock) + .MinDesiredWidth(160.f) + .Text(LOCTEXT("HoudiniEnginOutputTemplatesCheckBoxLabel", "Use Templated Geos")) + .ToolTipText(TooltipText) + ] + + SHorizontalBox::Slot() + [ + SNew(SCheckBox) + .OnCheckStateChanged_Lambda(OnCheckStateChangedOutputTemplatedGeosLambda) + .IsChecked_Lambda(IsCheckedOutputTemplatedGeosLambda) + .ToolTipText(TooltipText) + ] + ]; + } + + + //---------------------------------------------------------------------------- + // Second line - Left - Misc + //---------------------------------------------------------------------------- + TSharedPtr MiscVerticalBox; + + // If we don't display the CookTriggers box - show misc on the first left box + if (DetailsFlags.bCookTriggers) + MiscVerticalBox = SecondLeftColumnVerticalBox; + else + MiscVerticalBox = FirstLeftColumnVerticalBox; + + MiscVerticalBox->AddSlot() + .AutoHeight() + .Padding(0.0f, 0.0f, 0.0f, 3.5f) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniEngineMiscLabel", "Miscellaneous")) + ]; + + if (MainHC->IsComponentSupported() && DetailsFlags.bPushTransformToHoudini) + { + // Push Transform to Houdini check box + TooltipText = LOCTEXT("HoudiniEnginePushTransformTooltip", "If enabled, modifying this Houdini Asset Actor's transform will automatically update the HDA's node transform in Houdini."); + MiscVerticalBox->AddSlot() + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(4.0f) + [ + SNew(STextBlock) + .MinDesiredWidth(160.f) + .Text(LOCTEXT("HoudiniEnginePushTransformToHoudiniCheckBoxLabel", "Push Transform to Houdini")) + .ToolTipText(TooltipText) + ] + + SHorizontalBox::Slot() + [ + SNew(SCheckBox) + .OnCheckStateChanged_Lambda(OnCheckStateChangedPushTransformToHoudiniLambda) + .IsChecked_Lambda(IsCheckedPushTransformToHoudiniLambda) + .ToolTipText(TooltipText) + ] + ]; + } + + if (MainHC->IsComponentSupported()) + { + // Landscape Temp Layers + TooltipText = LOCTEXT("HoudiniEngineTempLandscapeLayersTooltip", "Cooking use temporary landscape layers."); + MiscVerticalBox->AddSlot() + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(4.0f) + [ + SNew(STextBlock) + .MinDesiredWidth(160.f) + .Text(LOCTEXT("HoudiniEngineTempLandscapeCheckBoxLabel", "Temp Landscape Layers")) + .ToolTipText(TooltipText) + ] + + SHorizontalBox::Slot() + [ + SNew(SCheckBox) + .OnCheckStateChanged_Lambda(OnCheckStateChangedUseTempLandscapeLayersLambda) + .IsChecked_Lambda(IsCheckedUseTempLandscapesLayersToHoudiniLambda) + .ToolTipText(TooltipText) + ] + ]; + } + + + // Curve Editing + if (MainHC->IsOutputSupported()) + { + TooltipText = LOCTEXT("HoudiniEngineEnableEditableCurvesTooltip", + "Enables a legacy/deprecated workflow that uses editable curve::1.0 nodes to create curve inputs."); + MiscVerticalBox->AddSlot() + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(4.0f) + [ + SNew(STextBlock) + .MinDesiredWidth(160.f) + .Text(LOCTEXT("HoudiniEngineEnableEditableCurvesLabel", "Enable Editable Curves")) + .ToolTipText(TooltipText) + ] + + SHorizontalBox::Slot() + [ + SNew(SCheckBox) + .OnCheckStateChanged_Lambda(OnCheckStateChangedEnableCurveEditingLambda) + .IsChecked_Lambda(IsCheckedEnableCurveEditingLambda) + .ToolTipText(TooltipText) + ] + ]; + } + + // Use whole widget + CheckBoxesRow.WholeRowWidget.Widget = WidgetBox; +} + +void +FHoudiniEngineDetails::CreateHelpAndDebugWidgets( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InHCs) +{ + if (InHCs.Num() <= 0) + return; + + const TWeakObjectPtr& MainHC = InHCs[0]; + if (!IsValidWeakPointer(MainHC)) + return; + + // Header Row + FHoudiniEngineDetails::AddHeaderRowForCookable(HoudiniEngineCategoryBuilder, MainHC, HOUDINI_ENGINE_UI_SECTION_HELP_AND_DEBUG); + if (!MainHC->bHelpAndDebugMenuExpanded) + return; + + HAPI_NodeId MainNodeId = MainHC->GetNodeId(); + TArray InNodeIds; + InNodeIds.SetNum(InHCs.Num()); + for (int32 Idx = 0; Idx < InHCs.Num(); Idx++) + InNodeIds[Idx] = InHCs[Idx].Get() ? InHCs[Idx].Get()->GetNodeId() : -1; + + auto OnFetchCookLogButtonClickedLambda = [InNodeIds]() + { + return ShowCookLog(InNodeIds); + }; + + auto OnHelpButtonClickedLambda = [MainNodeId]() + { + return ShowAssetHelp(MainNodeId); + }; + + // Button Row + FDetailWidgetRow & ButtonRow = HoudiniEngineCategoryBuilder.AddCustomRow(FText::FromString("Help Cook Logs")); + TSharedRef ButtonRowHorizontalBox = SNew(SHorizontalBox); + TSharedPtr CookLogButtonHorizontalBox = SNew(SHorizontalBox); + + // Fetch Cook Log button + ButtonRowHorizontalBox->AddSlot() + //.Padding(15.0f, 0.0f, 0.0f, 0.0f) + .MaxWidth(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SNew(SBox) + .WidthOverride(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(FText::FromString("Fetch and display all available Houdini cook logs for this.")) + //.Text(FText::FromString("Fetch Cook Log")) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda(OnFetchCookLogButtonClickedLambda) + .Content() + [ + SAssignNew(CookLogButtonHorizontalBox, SHorizontalBox) + ] + ] + ]; + + TSharedPtr CookLogIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUICookLogIconBrush(); + if (CookLogIconBrush.IsValid()) + { + TSharedPtr CookImage; + CookLogButtonHorizontalBox->AddSlot() + .MaxWidth(16.0f) + //.Padding(23.0f, 0.0f, 3.0f, 0.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(CookImage, SImage) + ] + ]; + + CookImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([CookLogIconBrush]() { + return CookLogIconBrush.Get(); + }))); + } + + CookLogButtonHorizontalBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(FText::FromString("Show Cook Logs")) + ]; + + if (MainHC->IsHoudiniAssetSupported()) + { + // Asset Help Button + TSharedPtr AssetHelpButtonHorizontalBox; + ButtonRowHorizontalBox->AddSlot() + //.Padding(4.0, 0.0f, 0.0f, 0.0f) + .MaxWidth(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SNew(SBox) + .WidthOverride(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(FText::FromString("Display this Houdini Asset Actor's HDA help.")) + //.Text(FText::FromString("Asset Help")) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda(OnHelpButtonClickedLambda) + .Content() + [ + SAssignNew(AssetHelpButtonHorizontalBox, SHorizontalBox) + ] + ] + ]; + + TSharedPtr AssetHelpIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIAssetHelpIconBrush(); + if (AssetHelpIconBrush.IsValid()) + { + TSharedPtr AssetHelpImage; + AssetHelpButtonHorizontalBox->AddSlot() + .MaxWidth(16.0f) + //.Padding(23.0f, 0.0f, 3.0f, 0.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(AssetHelpImage, SImage) + ] + ]; + + AssetHelpImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([AssetHelpIconBrush]() { + return AssetHelpIconBrush.Get(); + }))); + } + + AssetHelpButtonHorizontalBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(FText::FromString("Asset Help")) + ]; + } + + ButtonRow.WholeRowWidget.Widget = ButtonRowHorizontalBox; +} + +FString +FormatHoudiniVersionString(int32 VersionMajor, int32 VersionMinor, int32 VersionBuild, int32 VersionPatch) +{ + return FString::Printf(TEXT("%d.%d.%d.%d"), VersionMajor, VersionMinor, VersionBuild, VersionPatch); +} + +FString +FormatEngineVersionString(int32 VersionMajor, int32 VersionMinor, int32 VersionApi) +{ + return FString::Printf(TEXT("%d.%d.%d"), VersionMajor, VersionMinor, VersionApi); +} + +void +CreateInstallInfoStrings(FString &InstallInfo, FString &InstallInfoStyled) +{ + FString VersionHoudiniBuilt = FormatHoudiniVersionString( + HAPI_VERSION_HOUDINI_MAJOR, HAPI_VERSION_HOUDINI_MINOR, HAPI_VERSION_HOUDINI_BUILD, HAPI_VERSION_HOUDINI_PATCH); + FString VersionEngineBuilt = FormatEngineVersionString( + HAPI_VERSION_HOUDINI_ENGINE_MAJOR, HAPI_VERSION_HOUDINI_ENGINE_MINOR, HAPI_VERSION_HOUDINI_ENGINE_API); + FString VersionHoudiniRunning; + FString VersionEngineRunning; + + // Add running against Houdini version. + { + int32 RunningMajor = 0; + int32 RunningMinor = 0; + int32 RunningBuild = 0; + int32 RunningPatch = 0; + + if (FHoudiniApi::IsHAPIInitialized()) + { + const HAPI_Session* Session = FHoudiniEngine::Get().GetSession(); + // Retrieve version numbers for running Houdini. + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_MAJOR, &RunningMajor); + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_MINOR, &RunningMinor); + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_BUILD, &RunningBuild); + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_PATCH, &RunningPatch); + + VersionHoudiniRunning = FormatHoudiniVersionString( + RunningMajor, RunningMinor, RunningBuild, RunningPatch); + } + else + VersionHoudiniRunning = TEXT("Unknown"); + } + + // Add running against Houdini Engine version. + { + int32 RunningEngineMajor = 0; + int32 RunningEngineMinor = 0; + int32 RunningEngineApi = 0; + + if (FHoudiniApi::IsHAPIInitialized()) + { + const HAPI_Session* Session = FHoudiniEngine::Get().GetSession(); + // Retrieve version numbers for running Houdini Engine. + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_ENGINE_MAJOR, &RunningEngineMajor); + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_ENGINE_MINOR, &RunningEngineMinor); + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_ENGINE_API, &RunningEngineApi); + + VersionEngineRunning = FormatEngineVersionString( + RunningEngineMajor, RunningEngineMinor, RunningEngineApi); + } + else + VersionHoudiniRunning = "Unknown"; + } + + // Add path of libHAPI. + FString LibHAPILocation = FHoudiniEngine::Get().GetLibHAPILocation(); + if (LibHAPILocation.IsEmpty()) + LibHAPILocation = TEXT("Not Found"); + + FString HoudiniExecutable = FHoudiniEngine::Get().GetHoudiniExecutable(); + + // Add licensing info. + FString HAPILicenseType = TEXT(""); + if (!FHoudiniEngineUtils::GetLicenseType(HAPILicenseType)) + HAPILicenseType = TEXT("Unknown"); + + FString HoudiniSessionStatus; + switch (FHoudiniEngine::Get().GetSessionStatus()) { + case EHoudiniSessionStatus::Invalid: + HoudiniSessionStatus = TEXT("Invalid"); + break; + case EHoudiniSessionStatus::NotStarted: + HoudiniSessionStatus = TEXT("NotStarted"); + break; + case EHoudiniSessionStatus::Connected: + HoudiniSessionStatus = TEXT("Connected"); + break; + case EHoudiniSessionStatus::None: + HoudiniSessionStatus = TEXT("None"); + break; + case EHoudiniSessionStatus::Stopped: + HoudiniSessionStatus = TEXT("Stopped"); + break; + case EHoudiniSessionStatus::Failed: + HoudiniSessionStatus = TEXT("Failed"); + break; + case EHoudiniSessionStatus::Lost: + HoudiniSessionStatus = TEXT("Lost"); + break; + case EHoudiniSessionStatus::NoLicense: + HoudiniSessionStatus = TEXT("NoLicense"); + break; + } + + const FString InstallInfoFormat = TEXT( +R"""(Plugin was built with: + Houdini: {0} + HoudiniEngine: {1} + +Plugin is running with: + Houdini: {2} + HoudiniEngine: {3} + +Houdini Executable Type: {4} +HoudiniEngine Library Location: {5} + +License Type Acquired: {6} +Current Session Status: {7})"""); + + const FString InstallInfoFormatStyled = TEXT( +R"""(Plugin was built with: + Houdini: {0} + HoudiniEngine: {1} + +Plugin is running with: + Houdini: {2} + HoudiniEngine: {3} + +Houdini Executable Type: {4} +HoudiniEngine Library Location: {5} + +License Type Acquired: {6} +Current Session Status: {7})"""); + + TArray Args + { + VersionHoudiniBuilt, + VersionEngineBuilt, + VersionHoudiniRunning, + VersionEngineRunning, + HoudiniExecutable, + LibHAPILocation, + HAPILicenseType, + HoudiniSessionStatus + }; + + InstallInfo = FString::Format(*InstallInfoFormat, Args); + InstallInfoStyled = FString::Format(*InstallInfoFormatStyled, Args); +} + +void +FHoudiniEngineDetails::CreateInstallInfoWindow() +{ + FString InstallInfo; + FString InstallInfoStyled; + CreateInstallInfoStrings(InstallInfo, InstallInfoStyled); + + auto CopyInstallInfo = [InstallInfo]() + { + FPlatformApplicationMisc::ClipboardCopy(*InstallInfo); + return FReply::Handled(); + }; + + TSharedPtr Image; + TSharedPtr CloseButton; + float InstallInfoButtonWidth = 70.0f; + + TSharedRef InstallInfoWindow = + SNew(SWindow) + .Title(LOCTEXT("InstallInfoTitle", "Houdini Engine Installation Info")) + .SizingRule(ESizingRule::Autosized) + .SupportsMaximize(false) + .SupportsMinimize(false) + [ + SNew(SVerticalBox) + // Houdini Engine Logo + +SVerticalBox::Slot() + .HAlign(HAlign_Fill) + .VAlign(VAlign_Fill) + .AutoHeight() + .Padding(20.0f, 20.0f, 20.0f, 0.0f) + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Left) + [ + SNew(SBox) + .HeightOverride(30) + .WidthOverride(208) + [ + SAssignNew(Image, SImage) + .ColorAndOpacity(FSlateColor::UseForeground()) + ] + ] + ] + // Install Info + +SVerticalBox::Slot() + .HAlign(HAlign_Fill) + .VAlign(VAlign_Fill) + .AutoHeight() + .Padding(20.0f, 20.0f, 20.0f, 0.0f) + [ + SNew(SRichTextBlock) + .Text(FText::FromString(InstallInfoStyled)) + .DecoratorStyleSet(FHoudiniEngineStyle::Get().Get()) + .Justification(ETextJustify::Left) + .LineHeightPercentage(1.25f) + ] + +SVerticalBox::Slot() + .Padding(20.0f, 20.0f, 20.0f, 0.0f) + .AutoHeight() + .HAlign(HAlign_Right) + .VAlign(VAlign_Fill) + [ + SNew(SHorizontalBox) + // Copy Button + +SHorizontalBox::Slot() + .MaxWidth(InstallInfoButtonWidth) + [ + SNew(SBox) + .WidthOverride(InstallInfoButtonWidth) + [ + SNew(SButton) + .Content() + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("Copy", "Copy")) + ] + ] + .OnClicked_Lambda(CopyInstallInfo) + ] + ] + // Close Button + +SHorizontalBox::Slot() + .MaxWidth(InstallInfoButtonWidth) + [ + SNew(SBox) + .WidthOverride(InstallInfoButtonWidth) + [ + SAssignNew(CloseButton, SButton) + .Content() + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("Close", "Close")) + ] + ] + ] + ] + ] + ]; + + CloseButton->SetOnClicked( + FOnClicked::CreateLambda( + [InstallInfoWindow]() + { + TSharedRef Window = FSlateApplication::Get().FindWidgetWindow(InstallInfoWindow).ToSharedRef(); + FSlateApplication::Get().RequestDestroyWindow(InstallInfoWindow); + return FReply::Handled(); + })); + + TSharedPtr HoudiniEngineUIIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIIconBrush(); + + // Skip drawing the icon if the icon image is not loaded correctly. + if (HoudiniEngineUIIconBrush.IsValid()) + { + Image->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda( + [HoudiniEngineUIIconBrush]() { return HoudiniEngineUIIconBrush.Get(); } + ))); + } + + IMainFrameModule &MainFrameModule = FModuleManager::LoadModuleChecked(TEXT("MainFrame")); + + if (MainFrameModule.GetParentWindow().IsValid()) + { + FSlateApplication::Get().AddWindowAsNativeChild(InstallInfoWindow, MainFrameModule.GetParentWindow().ToSharedRef()); + } + else + { + FSlateApplication::Get().AddWindow(InstallInfoWindow); + } +} + +FMenuBuilder +FHoudiniEngineDetails::Helper_CreateHoudiniAssetPicker() +{ + auto OnShouldFilterHoudiniAssetLambda = [](const AActor* const Actor) + { + if (!Actor) + return false; + + // Only return HoudiniAssetActors, but not our HAA + if (!Actor->IsA()) + return false; + + return true; + }; + + auto OnActorSelected = [](AActor* Actor) + { + UE_LOG(LogTemp, Warning, TEXT("Actor Selected")); + + return; + }; + + FMenuBuilder MenuBuilder(true, nullptr); + FOnShouldFilterActor ActorFilter = FActorTreeItem::FFilterPredicate::CreateLambda(OnShouldFilterHoudiniAssetLambda); + + MenuBuilder.BeginSection(NAME_None, LOCTEXT("HoudiniEngineDetailsAssetPicker", "Asset")); + { + FSceneOutlinerModule & SceneOutlinerModule = + FModuleManager::Get().LoadModuleChecked< FSceneOutlinerModule >(TEXT("SceneOutliner")); + FSceneOutlinerInitializationOptions InitOptions; + { + InitOptions.Filters->AddFilterPredicate(ActorFilter); + InitOptions.bFocusSearchBoxWhenOpened = false; + InitOptions.bShowCreateNewFolder = false; + + // Add the gutter so we can change the selection's visibility + InitOptions.ColumnMap.Add(FSceneOutlinerBuiltInColumnTypes::Gutter(), FSceneOutlinerColumnInfo(ESceneOutlinerColumnVisibility::Visible, 0, FCreateSceneOutlinerColumn(), true, TOptional(), FSceneOutlinerBuiltInColumnTypes::Gutter_Localized())); + InitOptions.ColumnMap.Add(FSceneOutlinerBuiltInColumnTypes::Label(), FSceneOutlinerColumnInfo(ESceneOutlinerColumnVisibility::Visible, 10, FCreateSceneOutlinerColumn(), true, TOptional(), FSceneOutlinerBuiltInColumnTypes::Label_Localized())); + InitOptions.ColumnMap.Add(FSceneOutlinerBuiltInColumnTypes::ActorInfo(), FSceneOutlinerColumnInfo(ESceneOutlinerColumnVisibility::Visible, 20, FCreateSceneOutlinerColumn(), true, TOptional(), FSceneOutlinerBuiltInColumnTypes::ActorInfo_Localized())); + } + + static const FVector2D SceneOutlinerWindowSize(350.0f, 200.0f); + TSharedRef MenuWidget = + SNew(SBox) + .WidthOverride(SceneOutlinerWindowSize.X) + .HeightOverride(SceneOutlinerWindowSize.Y) + [ + SNew(SBorder) + .BorderImage(_GetEditorStyle().GetBrush("Menu.Background")) + [ + SceneOutlinerModule.CreateActorPicker(InitOptions, FOnActorPicked::CreateLambda(OnActorSelected)) + ] + ]; + + MenuBuilder.AddWidget(MenuWidget, FText::GetEmpty(), true); + } + MenuBuilder.EndSection(); + + return MenuBuilder; +} + +const FSlateBrush * +FHoudiniEngineDetails::GetHoudiniAssetThumbnailBorder(TSharedPtr< SBorder > HoudiniAssetThumbnailBorder) const +{ + if (HoudiniAssetThumbnailBorder.IsValid() && HoudiniAssetThumbnailBorder->IsHovered()) + return _GetEditorStyle().GetBrush("PropertyEditor.AssetThumbnailLight"); + else + return _GetEditorStyle().GetBrush("PropertyEditor.AssetThumbnailShadow"); +} + +TSharedPtr +FHoudiniEngineDetails::ConstructActionMenu( + const TArray>& InCookables, + class IDetailLayoutBuilder* LayoutBuilder) +{ + FMenuBuilder MenuBuilder(true, NULL); + + const int32 NumHCs = InCookables.Num(); + if (NumHCs == 0) + return MenuBuilder.MakeWidget(); + + TWeakObjectPtr HC = InCookables[0]; + if (!HC.IsValid()) + { + return MenuBuilder.MakeWidget(); + } + + MenuBuilder.BeginSection("AssetCreate", LOCTEXT("HDAActionMenu_SectionCreate", "Create")); + + // Create Preset + MenuBuilder.AddMenuEntry( + FText::FromString("Create Preset"), + FText::FromString("Create a new preset from the current HoudiniAssetComponent parameters."), + FSlateIcon(), + FUIAction( + FExecuteAction::CreateLambda([HC]() -> void + { + SHoudiniCreatePresetFromHDA::CreateDialog(HC); + }), + FCanExecuteAction() + ) + ); + + MenuBuilder.EndSection(); + + MenuBuilder.BeginSection("Modify", LOCTEXT("HDAActionMenu_SectionModify", "Modify")); + + // Update Selected Preset (if a preset asset is selected) + MenuBuilder.AddMenuEntry( + FText::FromString("Update Selected Preset"), + FText::FromString("Update the Houdini Preset that is currently selected in the content browser."), + FSlateIcon(), + FUIAction( + FExecuteAction::CreateLambda([HC]() -> void + { + SHoudiniUpdatePresetFromHDA::CreateDialog(HC); + }), + FCanExecuteAction::CreateLambda([NumHCs]() -> bool + { + if (NumHCs != 1) + { + return false; + } + + TArray SelectedAssets; + AssetSelectionUtils::GetSelectedAssets(SelectedAssets); + if (SelectedAssets.Num() != 1) + { + return false; + } + + const FAssetData& AssetData = SelectedAssets[0]; + const UHoudiniPreset* SelectedPreset = Cast(AssetData.GetAsset()); + return IsValid(SelectedPreset); + }) + ) + ); + + MenuBuilder.EndSection(); + + TArray Presets; + FHoudiniToolsEditor::FindPresetsForHoudiniAsset(HC->GetHoudiniAsset(), Presets); + + Algo::Sort(Presets, [](const UHoudiniPreset* LHS, const UHoudiniPreset* RHS) { return LHS->Name < RHS->Name; }); + + TSharedPtr SearchImage = SNew(SImage) + .Image(FAppStyle::Get().GetBrush("Symbols.SearchGlass")) + .ColorAndOpacity(FSlateColor::UseForeground()); + + // Presets + // TODO: store presets in a searchable submenu + MenuBuilder.BeginSection("Presets", LOCTEXT("HDAActionMenu_SectionPresets", "Presets")); + for (UHoudiniPreset* Preset : Presets) + { + if (!IsValid(Preset)) + { + continue; + } + + if (Preset->bHidePreset) + { + continue; + } + + TSharedRef PresetItem = + SNew(SHorizontalBox) + + // Preset Name + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text( FText::FromString(Preset->Name) ) + ] + + // Browse to HoudiniPreset button + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .HAlign(HAlign_Right) + [ + SNew(SButton) + .ContentPadding(0) + .ForegroundColor( FSlateColor::UseForeground() ) + .ButtonStyle( FAppStyle::Get(), "SimpleButton" ) + .ToolTipText( LOCTEXT("HDAActionMenu_SectionPresets_FindInCB", "Find in Content Browser") ) + .OnClicked_Lambda([Preset]() -> FReply + { + FHoudiniToolsEditor::BrowseToObjectInContentBrowser(Preset); + return FReply::Handled(); + }) + .Content() + [ + SearchImage.ToSharedRef() + ] + ]; + + // Menu entry for preset + MenuBuilder.AddMenuEntry( + FUIAction( + FExecuteAction::CreateLambda([Preset, InCookables, LayoutBuilder]() -> void + { + bool bPresetApplied = false; + for (TWeakObjectPtr HC : InCookables) + { + // Apply preset on Houdini Asset Component + if (!HC.IsValid()) + { + HOUDINI_LOG_WARNING(TEXT("Could not apply preset. Cookable reference is no longer valid.")); + continue; + } + + FHoudiniToolsEditor::ApplyPresetToHoudiniCookable(Preset, HC.Get(), true); + bPresetApplied = true; + } + + if (bPresetApplied && LayoutBuilder) + { + LayoutBuilder->ForceRefreshDetails(); + } + }), + FCanExecuteAction()), + PresetItem, + NAME_None, + FText::FromString(Preset->Description) + ); + } + MenuBuilder.EndSection(); + + return MenuBuilder.MakeWidget(); +} + +/* +TSharedRef< SWidget > +FHoudiniEngineDetails::OnGetHoudiniAssetMenuContent(TArray InHACs) +{ + TArray< const UClass * > AllowedClasses; + AllowedClasses.Add(UHoudiniAsset::StaticClass()); + + TArray< UFactory * > NewAssetFactories; + + UHoudiniAsset * HoudiniAsset = nullptr; + if (InHACs.Num() > 0) + { + UHoudiniAssetComponent * HoudiniAssetComponent = InHACs[0]; + HoudiniAsset = HoudiniAssetComponent->HoudiniAsset; + } + + auto OnShouldFilterHoudiniAssetLambda = [](const AActor* const Actor) + { + if (!Actor) + return false; + + // Only return HoudiniAssetActors, but not our HAA + if (!Actor->IsA()) + return false; + + return true; + }; + + // Delegate for filtering Houdini assets. + FOnShouldFilterAsset OnShouldFilterHoudiniAsset = FOnShouldFilterAsset::CreateLambda(OnShouldFilterHoudiniAssetLambda); + + return PropertyCustomizationHelpers::MakeAssetPickerWithMenu( + FAssetData(HoudiniAsset), true, + AllowedClasses, NewAssetFactories, OnShouldFilterHoudiniAsset, + FOnAssetSelected::CreateLambda([](const FAssetData & AssetData) {}), + FSimpleDelegate::CreateLambda([]() { }) + ); +} +*/ + +FReply +FHoudiniEngineDetails::ShowCookLog(const TArray& InNodeIds) +{ + TSharedPtr< SWindow > ParentWindow; + const FString CookLog = FHoudiniEngineUtils::GetCookLog(InNodeIds); + + // Check if the main frame is loaded. When using the old main frame it may not be. + if (FModuleManager::Get().IsModuleLoaded("MainFrame")) + { + IMainFrameModule & MainFrame = FModuleManager::LoadModuleChecked("MainFrame"); + ParentWindow = MainFrame.GetParentWindow(); + } + + if (ParentWindow.IsValid()) + { + TSharedPtr HoudiniAssetCookLog; + + TSharedRef Window = + SNew(SWindow) + .Title(LOCTEXT("WindowTitle", "Houdini Cook Log")) + .ClientSize(FVector2D(640, 480)); + + Window->SetContent( + SAssignNew(HoudiniAssetCookLog, SHoudiniAssetLogWidget) + .LogText(CookLog)); + + if (FSlateApplication::IsInitialized()) + FSlateApplication::Get().AddModalWindow(Window, ParentWindow, false); + } + + return FReply::Handled(); +} + +FReply +FHoudiniEngineDetails::ShowAssetHelp(HAPI_NodeId InNodeId) +{ + FString AssetHelp; + if (InNodeId < 0) + { + AssetHelp = TEXT(" --- This Houdini asset has not cooked yet - please recook it or rebuild it first --- "); + } + else + { + // If we have a help URL, then open it + const FString AssetHelpURL = FHoudiniEngineUtils::GetAssetHelpURL(InNodeId); + if (AssetHelpURL.StartsWith(TEXT("http://")) || AssetHelpURL.StartsWith(TEXT("https://")) || AssetHelpURL.StartsWith(TEXT("file://"))) + { + FPlatformProcess::LaunchURL(*AssetHelpURL, nullptr, nullptr); + return FReply::Handled(); + } + + // If not, get the help string + AssetHelp = FHoudiniEngineUtils::GetAssetHelp(InNodeId); + if (AssetHelp.IsEmpty()) + AssetHelp = TEXT(" --- No help found for this Houdini Asset --- "); + + } + + // Check if the main frame is loaded. When using the old main frame it may not be. + TSharedPtr ParentWindow; + if (FModuleManager::Get().IsModuleLoaded("MainFrame")) + { + IMainFrameModule& MainFrame = FModuleManager::LoadModuleChecked< IMainFrameModule >("MainFrame"); + ParentWindow = MainFrame.GetParentWindow(); + } + + if (ParentWindow.IsValid()) + { + TSharedPtr HoudiniAssetHelpLog; + + TSharedRef Window = + SNew(SWindow) + .Title(LOCTEXT("WindowTitle", "Houdini Asset Help")) + .ClientSize(FVector2D(640, 480)); + + Window->SetContent( + SAssignNew(HoudiniAssetHelpLog, SHoudiniAssetLogWidget) + .LogText(AssetHelp)); + + if (FSlateApplication::IsInitialized()) + FSlateApplication::Get().AddModalWindow(Window, ParentWindow, false); + } + + return FReply::Handled(); +} + +void +FHoudiniEngineDetails::AddHeaderRowForCookable( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TWeakObjectPtr& HoudiniCookable, + int32 MenuSection) +{ + if (!IsValidWeakPointer(HoudiniCookable)) + return; + + FOnClicked OnExpanderClick = FOnClicked::CreateLambda([HoudiniCookable, MenuSection, &HoudiniEngineCategoryBuilder]() + { + if (!IsValidWeakPointer(HoudiniCookable)) + return FReply::Handled(); + + switch (MenuSection) + { + case HOUDINI_ENGINE_UI_SECTION_GENERATE: + HoudiniCookable->bGenerateMenuExpanded = !HoudiniCookable->bGenerateMenuExpanded; + break; + + case HOUDINI_ENGINE_UI_SECTION_PARAMETER_RESET: + // Note, just use Generate flag, since its a simplified version of Generate. + HoudiniCookable->bGenerateMenuExpanded = !HoudiniCookable->bGenerateMenuExpanded; + break; + + case HOUDINI_ENGINE_UI_SECTION_BAKE: + HoudiniCookable->bBakeMenuExpanded = !HoudiniCookable->bBakeMenuExpanded; + break; + + case HOUDINI_ENGINE_UI_SECTION_ASSET_OPTIONS: + HoudiniCookable->bAssetOptionMenuExpanded = !HoudiniCookable->bAssetOptionMenuExpanded; + break; + + case HOUDINI_ENGINE_UI_SECTION_HELP_AND_DEBUG: + HoudiniCookable->bHelpAndDebugMenuExpanded = !HoudiniCookable->bHelpAndDebugMenuExpanded; + break; + default: + break; + } + + FHoudiniEngineUtils::UpdateEditorProperties(true); + + // TODO: This is a quick fix for 130742. However, its not a complete solution since clicking the expansion does not + // always update all details panels correctly. The correct solution here is to move all the above expansion bools, like + // bAssetOptionMenuExpanded, out of the HAC into per- IDetailLayoutBuilder values. ie. store them per Details panel, + // not per-component. https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/Slate/DetailsCustomization/ + + HoudiniEngineCategoryBuilder.GetParentLayout().ForceRefreshDetails(); + + return FReply::Handled(); + }); + + TFunction GetText = [MenuSection]() + { + switch (MenuSection) + { + case HOUDINI_ENGINE_UI_SECTION_GENERATE: + return FText::FromString(HOUDINI_ENGINE_UI_SECTION_GENERATE_HEADER_TEXT); + break; + + case HOUDINI_ENGINE_UI_SECTION_BAKE: + return FText::FromString(HOUDINI_ENGINE_UI_SECTION_BAKE_HEADER_TEXT); + break; + + case HOUDINI_ENGINE_UI_SECTION_ASSET_OPTIONS: + return FText::FromString(HOUDINI_ENGINE_UI_SECTION_ASSET_OPTIONS_HEADER_TEXT); + break; + + case HOUDINI_ENGINE_UI_SECTION_HELP_AND_DEBUG: + return FText::FromString(HOUDINI_ENGINE_UI_SECTION_HELP_AND_DEBUG_HEADER_TEXT); + + case HOUDINI_ENGINE_UI_SECTION_PARAMETER_RESET: + return FText::FromString(HOUDINI_ENGINE_UI_SECTION_PARAMETER_RESET_TEXT); + + default: + break; + } + return FText::FromString(""); + }; + + TFunction GetExpanderBrush = [HoudiniCookable, MenuSection](SButton* InExpanderArrow) + { + FName ResourceName; + bool bMenuExpanded = false; + + if (IsValidWeakPointer(HoudiniCookable)) + { + switch (MenuSection) + { + case HOUDINI_ENGINE_UI_SECTION_GENERATE: + bMenuExpanded = HoudiniCookable->bGenerateMenuExpanded; + break; + + case HOUDINI_ENGINE_UI_SECTION_BAKE: + bMenuExpanded = HoudiniCookable->bBakeMenuExpanded; + break; + + case HOUDINI_ENGINE_UI_SECTION_ASSET_OPTIONS: + bMenuExpanded = HoudiniCookable->bAssetOptionMenuExpanded; + break; + + case HOUDINI_ENGINE_UI_SECTION_HELP_AND_DEBUG: + bMenuExpanded = HoudiniCookable->bHelpAndDebugMenuExpanded; + } + } + + if (bMenuExpanded) + { + ResourceName = InExpanderArrow->IsHovered() ? "TreeArrow_Expanded_Hovered" : "TreeArrow_Expanded"; + } + else + { + ResourceName = InExpanderArrow->IsHovered() ? "TreeArrow_Collapsed_Hovered" : "TreeArrow_Collapsed"; + } + + return _GetEditorStyle().GetBrush(ResourceName); + }; + + return AddHeaderRow(HoudiniEngineCategoryBuilder, OnExpanderClick, GetText, GetExpanderBrush); +} + +void +FHoudiniEngineDetails::AddHeaderRowForHoudiniPDGAssetLink(IDetailCategoryBuilder& PDGCategoryBuilder, const TWeakObjectPtr& InPDGAssetLink, int32 MenuSection) +{ + if (!IsValidWeakPointer(InPDGAssetLink)) + return; + + FOnClicked OnExpanderClick = FOnClicked::CreateLambda([InPDGAssetLink, MenuSection]() + { + if (!IsValidWeakPointer(InPDGAssetLink)) + return FReply::Handled(); + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + switch (MenuSection) + { + case HOUDINI_ENGINE_UI_SECTION_BAKE: + InPDGAssetLink->Modify(); + InPDGAssetLink->bBakeMenuExpanded = !InPDGAssetLink->bBakeMenuExpanded; + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, bBakeMenuExpanded), InPDGAssetLink.Get()); + break; + } + + //FHoudiniEngineUtils::UpdateEditorProperties(InPDGAssetLink, true); + + return FReply::Handled(); + }); + + TFunction GetText = [MenuSection]() + { + switch (MenuSection) + { + case HOUDINI_ENGINE_UI_SECTION_BAKE: + return FText::FromString(HOUDINI_ENGINE_UI_SECTION_BAKE_HEADER_TEXT); + break; + } + return FText::FromString(""); + }; + + TFunction GetExpanderBrush = [InPDGAssetLink, MenuSection](SButton* InExpanderArrow) + { + FName ResourceName; + bool bMenuExpanded = false; + + if (IsValidWeakPointer(InPDGAssetLink)) + { + switch (MenuSection) + { + case HOUDINI_ENGINE_UI_SECTION_BAKE: + bMenuExpanded = InPDGAssetLink->bBakeMenuExpanded; + break; + } + } + + if (bMenuExpanded) + { + ResourceName = InExpanderArrow->IsHovered() ? "TreeArrow_Expanded_Hovered" : "TreeArrow_Expanded"; + } + else + { + ResourceName = InExpanderArrow->IsHovered() ? "TreeArrow_Collapsed_Hovered" : "TreeArrow_Collapsed"; + } + + return _GetEditorStyle().GetBrush(ResourceName); + }; + + return AddHeaderRow(PDGCategoryBuilder, OnExpanderClick, GetText, GetExpanderBrush); +} + +void +FHoudiniEngineDetails::AddHeaderRow( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + FOnClicked& InOnExpanderClick, + TFunction& InGetText, + TFunction& InGetExpanderBrush) +{ + // Header Row + FDetailWidgetRow & HeaderRow = HoudiniEngineCategoryBuilder.AddCustomRow(FText::GetEmpty()); + TSharedPtr HeaderHorizontalBox; + HeaderRow.WholeRowWidget.Widget = SAssignNew(HeaderHorizontalBox, SHorizontalBox); + + TSharedPtr ExpanderImage; + TSharedPtr ExpanderArrow; + HeaderHorizontalBox->AddSlot().VAlign(VAlign_Center).HAlign(HAlign_Left).AutoWidth() + [ + SAssignNew(ExpanderArrow, SButton) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ClickMethod(EButtonClickMethod::MouseDown) + .Visibility(EVisibility::Visible) + .OnClicked(InOnExpanderClick) + [ + SAssignNew(ExpanderImage, SImage) + .ColorAndOpacity(FSlateColor::UseForeground()) + ] + ]; + + HeaderHorizontalBox->AddSlot().Padding(1.0f).VAlign(VAlign_Center).AutoWidth() + [ + SNew(STextBlock) + .Text_Lambda([InGetText](){return InGetText(); }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ]; + + ExpanderImage->SetImage( + TAttribute::Create( + [ExpanderArrow, InGetExpanderBrush]() + { + return InGetExpanderBrush(ExpanderArrow.Get()); + })); +} + + +void +FHoudiniEngineDetails::AddIndieLicenseRow(IDetailCategoryBuilder& InCategory) +{ + FText IndieText = + FText::FromString(TEXT("Houdini Engine Indie - For Limited Commercial Use Only")); + + FSlateFontInfo LargeDetailsFont = IDetailLayoutBuilder::GetDetailFontBold(); + LargeDetailsFont.Size += 2; + + FSlateColor LabelColor = FLinearColor(1.0f, 1.0f, 0.0f, 1.0f); + + InCategory.AddCustomRow(FText::GetEmpty()) + [ + SNew(STextBlock) + .Text(IndieText) + .ToolTipText(IndieText) + .Font(LargeDetailsFont) + .Justification(ETextJustify::Center) + .ColorAndOpacity(LabelColor) + ]; + + InCategory.AddCustomRow(FText::GetEmpty()) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .Padding(0, 0, 5, 0) + [ + SNew(SSeparator) + .Thickness(2.0f) + ] + ]; +} + +void +FHoudiniEngineDetails::AddEducationLicenseRow(IDetailCategoryBuilder& InCategory) +{ + FText EduText = + FText::FromString(TEXT("Houdini Engine Education - For Educationnal Use Only")); + + FSlateFontInfo LargeDetailsFont = IDetailLayoutBuilder::GetDetailFontBold(); + LargeDetailsFont.Size += 2; + + FSlateColor LabelColor = FLinearColor(1.0f, 1.0f, 0.0f, 1.0f); + + InCategory.AddCustomRow(FText::GetEmpty()) + [ + SNew(STextBlock) + .Text(EduText) + .ToolTipText(EduText) + .Font(LargeDetailsFont) + .Justification(ETextJustify::Center) + .ColorAndOpacity(LabelColor) + ]; + + InCategory.AddCustomRow(FText::GetEmpty()) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .Padding(0, 0, 5, 0) + [ + SNew(SSeparator) + .Thickness(2.0f) + ] + ]; +} + + +void +FHoudiniEngineDetails::AddSessionStatusRow(IDetailCategoryBuilder& InCategory) +{ + FDetailWidgetRow& SessionStatusRow = InCategory.AddCustomRow(FText::FromString("Session Status")) + .WholeRowContent() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text_Lambda([]() + { + FString StatusString; + FLinearColor StatusColor; + GetSessionStatusAndColor(StatusString, StatusColor); + return FText::FromString(StatusString); + }) + .ColorAndOpacity_Lambda([]() + { + FString StatusString; + FLinearColor StatusColor; + GetSessionStatusAndColor(StatusString, StatusColor); + return FSlateColor(StatusColor); + }) + ] + ]; +} + +bool +FHoudiniEngineDetails::GetSessionStatusAndColor( + FString& OutStatusString, FLinearColor& OutStatusColor) +{ + OutStatusString = FString(); + OutStatusColor = FLinearColor::White; + + bool result = FHoudiniEngine::Get().GetSessionStatusAndColor(OutStatusString, OutStatusColor); + return result; +} + +void +FHoudiniEngineDetails::CreateNodeSyncWidgets( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InCookables) +{ + if (InCookables.Num() <= 0) + return; + + const TWeakObjectPtr& MainHC = InCookables[0]; + if (!IsValidWeakPointer(MainHC)) + return; + + const TWeakObjectPtr& MainHNSC = Cast(MainHC->GetComponent()); + if (!IsValidWeakPointer(MainHNSC)) + return; + + //FHoudiniEngineDetails::AddHeaderRowForHoudiniAssetComponent(HoudiniEngineCategoryBuilder, MainHAC, HOUDINI_ENGINE_UI_SECTION_GENERATE); + + auto IsCheckedLiveSyncLambda = [MainHNSC]() + { + if (!IsValidWeakPointer(MainHNSC)) + return ECheckBoxState::Unchecked; + + return MainHNSC->GetLiveSyncEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto OnCheckStateLiveSyncLambda = [InCookables](ECheckBoxState NewState) + { + bool bChecked = (NewState == ECheckBoxState::Checked); + for (auto& NextHC : InCookables) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + const TWeakObjectPtr& NextHNSC = Cast(NextHC->GetComponent()); + if (!IsValidWeakPointer(NextHNSC)) + continue; + + if (NextHNSC->GetLiveSyncEnabled() == bChecked) + continue; + + NextHNSC->SetLiveSyncEnabled(bChecked); + NextHNSC->MarkPackageDirty(); + } + }; + + auto UpdateNodePath = [InCookables](const FString& NewPath) + { + UHoudiniEditorNodeSyncSubsystem* HoudiniSubsystem = GEditor->GetEditorSubsystem(); + if (!IsValid(HoudiniSubsystem)) + return; + + HAPI_NodeId FetchedNodeId = -1; + if (!HoudiniSubsystem->ValidateFetchedNodePath(NewPath, FetchedNodeId)) + { + // Node path invalid! + HOUDINI_LOG_WARNING(TEXT("Houdini Node Sync - Fetch Failed - The Fetch node path is invalid.")); + FHoudiniEngineUtils::UpdateEditorProperties(true); + return; + } + + // Change the node path + for (auto& NextHC : InCookables) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + const TWeakObjectPtr& NextHNSC = Cast(NextHC->GetComponent()); + if (!IsValidWeakPointer(NextHNSC)) + continue; + + if (NextHNSC->GetFetchNodePath().Equals(NewPath)) + continue; + + NextHNSC->SetFetchNodePath(NewPath); + NextHNSC->MarkPackageDirty(); + NextHNSC->SetHoudiniAssetState(EHoudiniAssetState::NewHDA); + } + }; + + auto OnFetchPathTextCommittedLambda = [InCookables, MainHNSC, UpdateNodePath](const FText& Val, ETextCommit::Type TextCommitType) + { + if (!IsValidWeakPointer(MainHNSC)) + return; + + FString NewPathStr = Val.ToString(); + UpdateNodePath(NewPathStr); + }; + + auto OnFetchFolderBrowseButtonClickedLambda = [InCookables, MainHNSC, UpdateNodePath]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + if (!HoudiniEditorNodeSyncSubsystem) + return FReply::Handled(); + + TSharedRef Dialog = + SNew(SSelectHoudiniPathDialog) + .InitialPath(FText::FromString(MainHNSC->GetFetchNodePath())) + .TitleText(LOCTEXT("FetchPathDialogTitle", "Select a Houdini node to fetch")) + .SingleSelection(true); + + if (Dialog->ShowModal() != EAppReturnType::Ok) + return FReply::Handled(); + + // Get the new path and update it + FString NewPath = Dialog->GetFolderPath().ToString(); + UpdateNodePath(NewPath); + + return FReply::Handled(); + }; + + + // + // Fetch node Path Row + // + FDetailWidgetRow & FetchNodeRow = HoudiniEngineCategoryBuilder.AddCustomRow(FText::FromString("Fetch Node Path")); + + FString FetchPathTooltipString = + "The path of a node in Houdini that you want to fetch.\ne.g /obj/MyNetwork/Mynode \nThe paths can easily be obtained by using the browse button and selecting them in the dialog.\ + \nAlternatively, you can copy/paste a node to this text box to get its path.\nOnly a single path can be used with per NodeSyncComponent."; + + TSharedRef FetchNodeRowHorizontalBox = SNew(SHorizontalBox); + FetchNodeRowHorizontalBox->AddSlot() + .HAlign(HAlign_Left) + .VAlign(VAlign_Center) + [ + SNew(SBox) + .WidthOverride(335.0f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("FetchNodePathLabel", "Houdini Node Path To Fetch (single)")) + .ToolTipText(FText::FromString(FetchPathTooltipString)) + ] + ]; + + FetchNodeRowHorizontalBox->AddSlot() + .HAlign(HAlign_Left) + .VAlign(VAlign_Center) + [ + SNew(SEditableTextBox) + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + .ToolTipText_Lambda([MainHNSC, FetchPathTooltipString]() + { + FString TooltipString = FetchPathTooltipString; + + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + if (!HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.FetchNodePath.IsEmpty()) + { + TooltipString += "\n\nCurrent value:\n"; + TooltipString += MainHNSC->GetFetchNodePath().Replace(TEXT(";"), TEXT("\n")); + } + + return FText::FromString(TooltipString); + }) + .HintText(LOCTEXT("NodePathLabel", "Houdini Node Path To Fetch")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(FText::FromString(MainHNSC->GetFetchNodePath())) + .OnTextCommitted_Lambda(OnFetchPathTextCommittedLambda) + ]; + + FetchNodeRowHorizontalBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled(true) + .Text(LOCTEXT("BrowseButtonText", "...")) + .ToolTipText(LOCTEXT("FetchBrowseButtonToolTip", "Browse to select a node to fetch...")) + .OnClicked_Lambda(OnFetchFolderBrowseButtonClickedLambda) + ]; + + FetchNodeRow.WholeRowWidget.Widget = FetchNodeRowHorizontalBox; + + // + // Enable LiveSync Row + // + FDetailWidgetRow& LiveSyncRow = HoudiniEngineCategoryBuilder.AddCustomRow(FText::FromString("Live Sync")); + + TSharedRef LiveSyncRowHorizontalBox = SNew(SHorizontalBox); + TSharedPtr CheckBoxLiveSync; + + LiveSyncRowHorizontalBox->AddSlot() + [ + SNew(SBox) + .WidthOverride(160.f) + [ + SAssignNew(CheckBoxLiveSync, SCheckBox) + .Content() + [ + SNew(STextBlock).Text(LOCTEXT("LiveSyncCheckBox", "Enable Live Sync")) + .ToolTipText(LOCTEXT("LiveSyncCheckBoxToolTip", "When enabled, changes made to the feched node in Houdini will automatically updates this component's outputs.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda(IsCheckedLiveSyncLambda) + .OnCheckStateChanged_Lambda(OnCheckStateLiveSyncLambda) + ] + ]; + + LiveSyncRow.WholeRowWidget.Widget = LiveSyncRowHorizontalBox; + + // + // FETCH Button + // + FDetailWidgetRow& NodeSyncFetchRow = HoudiniEngineCategoryBuilder.AddCustomRow(FText::FromString("Node Sync Fetch")); + TSharedRef NodeSyncFetchRowHorizontalBox = SNew(SHorizontalBox); + + NodeSyncFetchRowHorizontalBox->AddSlot() + .FillWidth(1.0f) + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(SBox) + .WidthOverride(135.0f) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("FetchFromHoudiniLabel", "Fetch the data from Houdini")) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([InCookables]() + { + // Change the node path + for (auto& NextHC : InCookables) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + const TWeakObjectPtr& NextHNSC = Cast(NextHC->GetComponent()); + if (!IsValidWeakPointer(NextHNSC)) + continue; + + NextHNSC->MarkPackageDirty(); + NextHNSC->SetHoudiniAssetState(EHoudiniAssetState::NewHDA); + } + + return FReply::Handled(); + }) + .Content() + [ + SNew(STextBlock) + .Text(FText::FromString("Fetch")) + ] + ] + ]; + NodeSyncFetchRow.WholeRowWidget.Widget = NodeSyncFetchRowHorizontalBox; + + // + // FETCH status + // + FDetailWidgetRow& NodeSyncStatusRow = HoudiniEngineCategoryBuilder.AddCustomRow(FText::FromString("Node Sync Status")); + TSharedRef NodeSyncStatusRowHorizontalBox = SNew(SHorizontalBox); + + NodeSyncStatusRowHorizontalBox->AddSlot() + .FillWidth(1.0f) + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Top) + [ + SNew(STextBlock) + .Justification(ETextJustify::Left) + .Text_Lambda([MainHNSC]() + { + return FText::FromString(MainHNSC->FetchMessage); + }) + .ColorAndOpacity_Lambda([MainHNSC]() + { + FLinearColor StatusColor = UHoudiniEditorNodeSyncSubsystem::GetStatusColor(MainHNSC->FetchStatus); + return FSlateColor(StatusColor); + }) + .ToolTipText_Lambda([MainHNSC]() + { + return FText::FromString(MainHNSC->FetchMessage); + }) + ]; + NodeSyncStatusRow.WholeRowWidget.Widget = NodeSyncStatusRowHorizontalBox; +} + +void +FHoudiniEngineDetails::SetFolderPath( + const FText& InPathText, + const bool& bIsBakePath, + const TWeakObjectPtr& InMainHC, + const TArray>& InHCs) +{ + if (!IsValidWeakPointer(InMainHC)) + return; + + FString NewPathStr = InPathText.ToString(); + if (NewPathStr.StartsWith("Game/")) + { + NewPathStr = "/" + NewPathStr; + } + + FText InvalidPathReason; + if (!FHoudiniEngineUtils::ValidatePath(NewPathStr, &InvalidPathReason)) + { + HOUDINI_LOG_WARNING(TEXT("Invalid path: %s"), *InvalidPathReason.ToString()); + FHoudiniEngineUtils::UpdateEditorProperties(true); + return; + } + + for (auto& NextHC : InHCs) + { + if (!IsValidWeakPointer(NextHC)) + continue; + + if (bIsBakePath) + NextHC->SetBakeFolderPath(NewPathStr); + else + NextHC->SetTemporaryCookFolderPath(NewPathStr); + + // why ? + NextHC->MarkPackageDirty(); + } +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineDetails.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + + +#include "HAPI/HAPI_Common.h" +#include "HoudiniEngineDetails.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineRuntimePrivatePCH.h" + +#include "Framework/SlateDelegates.h" +#include "Framework/SlateDelegates.h" +#include "Styling/SlateBrush.h" +#include "Templates/SharedPointer.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/SCompoundWidget.h" +#include "Widgets/SWidget.h" + +class IDetailCategoryBuilder; +class UHoudiniAssetComponent; +class UHoudiniCookable; +class UHoudiniPDGAssetLink; +class FMenuBuilder; +class SBorder; +class SButton; +class SHorizontalBox; +class SVerticalBox; + +#define IsValidWeakPointer(InWeakObjectPointer) \ + FHoudiniEngineDetails::IsValidWeakObjectPointer(InWeakObjectPointer, true, TEXT(__FILE__), __LINE__) + + +class SHoudiniAssetLogWidget : public SCompoundWidget +{ +public: + + SLATE_BEGIN_ARGS(SHoudiniAssetLogWidget) + : _LogText(TEXT("")) + {} + + SLATE_ARGUMENT(FString, LogText) + SLATE_END_ARGS() + + /** Widget construct. **/ + void Construct(const FArguments & InArgs); +}; + +struct EHoudiniDetailsFlags +{ + // Controls the UI for the Houdini Details Planel. The defaults are for for HACs, but its customized + // for PCG, where some settings should not be used. + + bool bAutoBake = true; + bool bBakeButton = true; + bool bDisplayOnOutputLess = false; + bool bAssetOptions = true; + bool bGenerateBar = true; + bool bReplacePreviousBake = true; + bool bRemoveHDAOutputAfterBake = true; + bool bTemporaryCookFolderRow = true; + bool bCookTriggers = true; + bool bDoNotGenerateOutputs = true; + bool bPushTransformToHoudini = true; + + static EHoudiniDetailsFlags Defaults; +}; + +class FHoudiniEngineDetails : public TSharedFromThis +{ +public: + + // HE ICON + static void CreateHoudiniEngineIconWidget( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder); + + // HOUDINI ASSET + PRESET MENU + static void CreateHoudiniAssetDetails( + IDetailCategoryBuilder& HoudiniAssetCategory, + TArray>& InCookables); + + // PRESET MENU (FULL ROW - unused) + static void CreateHoudiniEngineActionWidget( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InCookables); + + // GENERATE + static void CreateGenerateWidgets( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InHCs, + const EHoudiniDetailsFlags& DetailsFlags); + + // RESET PARAMETERS - used by PCG, this is like GENERATE, but no Rebuild/Recook buttons. + static void CreateResetParametersOnlyWidgets( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InHCs); + + // BAKE + static void CreateBakeWidgets( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InHCs, + const EHoudiniDetailsFlags& DetailsFlags); + + // PDG + static void CreatePDGBakeWidgets( + IDetailCategoryBuilder& InPDGCategory, + UHoudiniPDGAssetLink* InPDGAssetLink); + + // ASSET OPTIONS + static void CreateAssetOptionsWidgets( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InHCs, + const EHoudiniDetailsFlags& DetailsFlags); + + // HELP DEBUG + static void CreateHelpAndDebugWidgets( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InHCs); + + // NODE SYNC + static void CreateNodeSyncWidgets( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TArray>& InCookables); + + static void CreateInstallInfoWindow(); + + static void AddRemovedHDAOutputAfterBakeCheckBox(const TWeakObjectPtr& MainHC, + const TArray>& InHCs, + TSharedPtr& LeftColumnVerticalBox); + + static void AddRenterBakedActorsCheckbox(const TWeakObjectPtr& MainHC, + const TArray>& InHCs, + TSharedPtr& LeftColumnVerticalBox); + + static void AddAutoBakeCheckbox(const TWeakObjectPtr& MainHC, + const TArray>& InHCs, + TSharedPtr& RightColumnVerticalBox); + + static void AddReplaceCheckbox(const TWeakObjectPtr& MainHC, + const TArray>& InHCs, + TSharedPtr& RightColumnVerticalBox); + + static void AddBakeFolderSelector( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TWeakObjectPtr& MainHC, + const TArray>& InHCs); + + static void AddBakeControlBar( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TWeakObjectPtr& MainHC, + const TArray>& InHCs, + EHoudiniDetailsFlags DetailsFlags); + + static FReply ShowCookLog(const TArray& InNodeIds); + + static FReply ShowAssetHelp(HAPI_NodeId InNodeId); + + static FMenuBuilder Helper_CreateHoudiniAssetPicker(); + + const FSlateBrush * GetHoudiniAssetThumbnailBorder(TSharedPtr HoudiniAssetThumbnailBorder) const; + + /** Construct drop down menu content for Houdini asset. **/ + //static TSharedRef< SWidget > OnGetHoudiniAssetMenuContent(TArray InHACs); + + static TSharedPtr ConstructActionMenu( + const TArray>& InCookables, + class IDetailLayoutBuilder*); + + static void AddHeaderRowForCookable( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + const TWeakObjectPtr& HoudiniCookable, + int32 MenuSection); + + static void AddHeaderRowForHoudiniPDGAssetLink( + IDetailCategoryBuilder& PDGCategoryBuilder, + const TWeakObjectPtr& InPDGAssetLink, + int32 MenuSection); + + static void AddHeaderRow( + IDetailCategoryBuilder& HoudiniEngineCategoryBuilder, + FOnClicked& InOnExpanderClick, + TFunction& InGetText, + TFunction& InGetExpanderBrush); + + // Adds a text row that indicate the status of the Houdini Session + static void AddSessionStatusRow(IDetailCategoryBuilder& InCategory); + + static bool GetSessionStatusAndColor(FString& OutStatusString, FLinearColor& OutStatusColor); + + // Adds a text row indicate we're using a Houdini indie license + static void AddIndieLicenseRow(IDetailCategoryBuilder& InCategory); + + // Adds a text row indicate we're using a Houdini Edu license + static void AddEducationLicenseRow(IDetailCategoryBuilder& InCategory); + + // Helper to check if InWeakObjectPointer is valid or not. If not valid, the filepath and line number where the check + // occurred is logged. + template + static bool IsValidWeakObjectPointer( + const TWeakObjectPtr& InWeakObjectPointer, + const bool bInLogInvalid=false, + const FString& InFilePath=FString(), + const int32 InLineNumber=INDEX_NONE); + +private: + + // Helper function that can be used to set either the bake/tempcook folder path on cookables + static void SetFolderPath( + const FText& InPathText, + const bool& bIsBakePath, + const TWeakObjectPtr& InMainHC, + const TArray>& InHCs); + + + static void CreateResetParametersButton(const TArray>& InHCs, TSharedRef ButtonHorizontalBox); + +}; + + +template +bool +FHoudiniEngineDetails::IsValidWeakObjectPointer(const TWeakObjectPtr& InWeakObjectPointer, const bool bInLogInvalid, const FString& InFilePath, const int32 InLineNumber) +{ + const bool bIsValid = InWeakObjectPointer.IsValid(); + if (!bIsValid && bInLogInvalid) + HOUDINI_LOG_WARNING(TEXT("Invalid TWeakObjectPtr in Details: %s:%d"), *InFilePath, InLineNumber); + return bIsValid; +} + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineEditor.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineEditor.h" + +#include "HoudiniEngineEditorPrivatePCH.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetActorFactory.h" +#include "HoudiniAssetBroker.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniAssetComponentDetails.h" +#include "HoudiniCookableDetails.h" +#include "HoudiniEditorNodeSyncSubsystem.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineCommands.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEngineStyle.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniHandleComponentVisualizer.h" +#include "HoudiniInput.h" +#include "HoudiniOutput.h" +#include "HoudiniPackageParams.h" +#include "HoudiniParameter.h" +#include "HoudiniPDGAssetLink.h" +#include "HoudiniRuntimeSettings.h" +#include "HoudiniRuntimeSettingsDetails.h" +#include "HoudiniSplineComponentVisualizer.h" +#include "HoudiniToolsEditor.h" +#include "HoudiniToolsPackageAsset.h" +#include "HoudiniToolsRuntimeUtils.h" +#include "SHoudiniToolsPanel.h" +#include "SHoudiniNodeSyncPanel.h" +#include "UnrealMeshTranslator.h" + +#include "AssetTypeActions_HoudiniAsset.h" +#include "AssetTypeActions_HoudiniPreset.h" +#include "AssetTypeActions_HoudiniToolsPackageAsset.h" + +#include "Animation/AnimSequence.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "ContentBrowserModule.h" +#include "Editor.h" +#include "Editor/UnrealEdEngine.h" +#include "EditorScriptingHelpers.h" +#include "HoudiniAssetFactory.h" +#include "HoudiniPresetActorFactory.h" +#include "Engine/Selection.h" +#include "Engine/SkeletalMesh.h" +#include "Framework/Application/SlateApplication.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "HAL/ConsoleManager.h" +#include "HAL/FileManagerGeneric.h" +#include "HAL/PlatformFileManager.h" +#include "Interfaces/IPluginManager.h" +#include "ISettingsModule.h" +#include "LevelEditor.h" +#include "Logging/LogMacros.h" +#include "Misc/FileHelper.h" +#include "Misc/MessageDialog.h" +#include "Misc/Paths.h" +#include "Modules/ModuleManager.h" +#include "PropertyEditorModule.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonSerializer.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "Subsystems/PlacementSubsystem.h" +#endif +#if defined(HOUDINI_USE_PCG) +#include "HoudiniPCGDetails.h" +#endif +#include "Templates/SharedPointer.h" +#include "UnrealEdGlobals.h" +#include "Toolkits/AssetEditorModeUILayer.h" +#include "Widgets/Docking/SDockTab.h" +#if defined(HOUDINI_USE_PCG) +#include "HoudiniPCGComponent.h" +#endif +#if WITH_EDITOR + #include "Editor/WorkspaceMenuStructure/Public/WorkspaceMenuStructure.h" + #include "Editor/WorkspaceMenuStructure/Public/WorkspaceMenuStructureModule.h" +#endif + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FHoudiniEngineEditor, HoudiniEngineEditor); + +// This is never declared on Horde for some reason. Compiled locally, it is declared in HoudiniEnginePrivateRuntimePCH.h +#ifndef HOUDINI_ENGINE_EDITOR_LOG_CATEGORY_DECLARED + HOUDINIENGINEEDITOR_API DECLARE_LOG_CATEGORY_EXTERN(LogHoudiniEngineEditor, Log, All); +#endif + +DEFINE_LOG_CATEGORY(LogHoudiniEngineEditor); + +FHoudiniEngineEditor * +FHoudiniEngineEditor::HoudiniEngineEditorInstance = nullptr; + +FHoudiniEngineEditor & +FHoudiniEngineEditor::Get() +{ + return *HoudiniEngineEditorInstance; +} + +bool +FHoudiniEngineEditor::IsInitialized() +{ + return FHoudiniEngineEditor::HoudiniEngineEditorInstance != nullptr; +} + +FHoudiniEngineEditor::FHoudiniEngineEditor() +{ +} + +void FHoudiniEngineEditor::StartupModule() +{ + HOUDINI_LOG_MESSAGE(TEXT("Starting the Houdini Engine Editor module.")); + + // Create style set. + FHoudiniEngineStyle::Initialize(); + + // Create HoudiniTools + + HoudiniToolsPtr = MakeShareable(new FHoudiniToolsEditor); + + // Initilizes various resources used by our editor UI widgets + InitializeWidgetResource(); + + // Register asset type actions. + RegisterAssetTypeActions(); + + // Register asset brokers. + RegisterAssetBrokers(); + + // Register component visualizers. + RegisterComponentVisualizers(); + + // Register detail presenters. + RegisterDetails(); + + // Register actor factories. + RegisterActorFactories(); + + // Extends the file menu. + ExtendMenu(); + + // Extend the World Outliner Menu + AddLevelViewportMenuExtender(); + + //Extend the right click context menu + ExtendContextMenu(); + + // Adds the custom console commands + RegisterConsoleCommands(); + + // Register global undo / redo callbacks. + //RegisterForUndo(); + + RegisterEditorTabs(); + + // Register sections (filters) for the details category + RegisterSectionMappings(); + + /* + if (FModuleManager::Get().IsModuleLoaded(TEXT("LevelEditor"))) + { + RegisterEditorTabs(); + } + else + { + ModulesChangedHandle = FModuleManager::Get().OnModulesChanged().AddRaw(this, &FHoudiniEngineEditor::ModulesChangedCallback); + } + */ + + // RegisterPlacementModeExtensions(); + + // Register for any FEditorDelegates that we are interested in, such as + // PreSaveWorld and PreBeginPIE, for HoudiniStaticMesh -> UStaticMesh builds + RegisterEditorDelegates(); + + // Store the instance. + FHoudiniEngineEditor::HoudiniEngineEditorInstance = this; + + HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine Editor module startup complete.")); +} + +/* +//------------------------------------------------------------------------------ +void FHoudiniEngineEditor::ModulesChangedCallback(FName ModuleName, EModuleChangeReason ReasonForChange) +{ + if (ReasonForChange == EModuleChangeReason::ModuleLoaded && ModuleName == TEXT("LevelEditor")) + { + RegisterEditorTabs(); + } +} +*/ + +void FHoudiniEngineEditor::ShutdownModule() +{ + HOUDINI_LOG_MESSAGE(TEXT("Shutting down the Houdini Engine Editor module.")); + + if (HoudiniToolsPtr.IsValid()) + { + HoudiniToolsPtr->Shutdown(); + } + + // Unregister the sections (filters) for the details category + UnregisterSectionMappings(); + + // Deregister editor delegates + UnregisterEditorDelegates(); + + // Deregister console commands + UnregisterConsoleCommands(); + + // Remove the level viewport Menu extender + RemoveLevelViewportMenuExtender(); + + // Unregister asset type actions. + UnregisterAssetTypeActions(); + + // Unregister asset brokers. + //UnregisterAssetBrokers(); + + // Unregister detail presenters. + UnregisterDetails(); + + UnRegisterEditorTabs(); + + // Unregister our component visualizers. + //UnregisterComponentVisualizers(); + + // Unregister global undo / redo callbacks. + //UnregisterForUndo(); + + //UnregisterPlacementModeExtensions(); + + // Unregister settings. + ISettingsModule * SettingsModule = FModuleManager::GetModulePtr("Settings"); + if (SettingsModule) + SettingsModule->UnregisterSettings("Editor", "Plugins", "HoudiniEngine"); + + HoudiniToolsPtr.Reset(); + + // Unregister the styleset + FHoudiniEngineStyle::Shutdown(); + + HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine Editor module shutdown complete.")); +} + +FString +FHoudiniEngineEditor::GetHoudiniEnginePluginDir() +{ + FString EnginePluginDir = FPaths::EnginePluginsDir() / TEXT("Runtime/HoudiniEngine"); + if (FPaths::DirectoryExists(EnginePluginDir)) + return EnginePluginDir; + + FString ProjectPluginDir = FPaths::ProjectPluginsDir() / TEXT("Runtime/HoudiniEngine"); + if (FPaths::DirectoryExists(ProjectPluginDir)) + return ProjectPluginDir; + + TSharedPtr HoudiniPlugin = IPluginManager::Get().FindPlugin(TEXT("HoudiniEngine")); + FString PluginBaseDir = HoudiniPlugin.IsValid() ? HoudiniPlugin->GetBaseDir() : EnginePluginDir; + if (FPaths::DirectoryExists(PluginBaseDir)) + return PluginBaseDir; + + HOUDINI_LOG_WARNING(TEXT("Could not find the Houdini Engine plugin's directory")); + + return EnginePluginDir; +} + +void +FHoudiniEngineEditor::RegisterDetails() +{ + FPropertyEditorModule & PropertyModule = FModuleManager::LoadModuleChecked< FPropertyEditorModule >("PropertyEditor"); + + // Register details presenter for our component type and runtime settings. + PropertyModule.RegisterCustomClassLayout( + TEXT("HoudiniAssetComponent"), + FOnGetDetailCustomizationInstance::CreateStatic(&FHoudiniCookableDetails::MakeInstance)); + + PropertyModule.RegisterCustomClassLayout( + TEXT("HoudiniAssetActor"), + FOnGetDetailCustomizationInstance::CreateStatic(&FHoudiniCookableDetails::MakeInstance)); + + PropertyModule.RegisterCustomClassLayout( + TEXT("HoudiniCookable"), + FOnGetDetailCustomizationInstance::CreateStatic(&FHoudiniCookableDetails::MakeInstance)); + + PropertyModule.RegisterCustomClassLayout( + TEXT("HoudiniRuntimeSettings"), + FOnGetDetailCustomizationInstance::CreateStatic(&FHoudiniRuntimeSettingsDetails::MakeInstance)); +#if defined(HOUDINI_USE_PCG) + PropertyModule.RegisterCustomClassLayout( + TEXT("HoudiniPCGSettings"), + FOnGetDetailCustomizationInstance::CreateStatic(&UHoudiniPCGSettingsCustomization::MakeInstance)); +#endif +} + +void +FHoudiniEngineEditor::UnregisterDetails() +{ + if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) + { + FPropertyEditorModule & PropertyModule = + FModuleManager::LoadModuleChecked("PropertyEditor"); + + PropertyModule.UnregisterCustomClassLayout(TEXT("HoudiniAssetComponent")); + PropertyModule.UnregisterCustomClassLayout(TEXT("HoudiniRuntimeSettings")); +#if defined(HOUDINI_USE_PCG) + PropertyModule.UnregisterCustomClassLayout(TEXT("HoudiniPCGSettings")); +#endif + } +} + +void +FHoudiniEngineEditor::RegisterComponentVisualizers() +{ + if (GUnrealEd) + { + // Register Houdini spline visualizer + SplineComponentVisualizer = MakeShareable(new FHoudiniSplineComponentVisualizer); + if (SplineComponentVisualizer.IsValid()) + { + GUnrealEd->RegisterComponentVisualizer(UHoudiniSplineComponent::StaticClass()->GetFName(), SplineComponentVisualizer); + SplineComponentVisualizer->OnRegister(); + } + + // Register Houdini handle visualizer + HandleComponentVisualizer = MakeShareable(new FHoudiniHandleComponentVisualizer); + if (HandleComponentVisualizer.IsValid()) + { + GUnrealEd->RegisterComponentVisualizer(UHoudiniHandleComponent::StaticClass()->GetFName(), HandleComponentVisualizer); + HandleComponentVisualizer->OnRegister(); + } + } +} + +void +FHoudiniEngineEditor::UnregisterComponentVisualizers() +{ + if (GUnrealEd) + { + // Unregister Houdini spline visualizer + if(SplineComponentVisualizer.IsValid()) + GUnrealEd->UnregisterComponentVisualizer(UHoudiniSplineComponent::StaticClass()->GetFName()); + + // Unregister Houdini handle visualizer + if (HandleComponentVisualizer.IsValid()) + GUnrealEd->UnregisterComponentVisualizer(UHoudiniHandleComponent::StaticClass()->GetFName()); + } +} + +void +FHoudiniEngineEditor::RegisterAssetTypeAction(IAssetTools & AssetTools, TSharedRef< IAssetTypeActions > Action) +{ + AssetTools.RegisterAssetTypeActions(Action); + AssetTypeActions.Add(Action); +} + +void +FHoudiniEngineEditor::RegisterAssetTypeActions() +{ + // Create and register asset type actions for Houdini asset. + IAssetTools& AssetTools = FModuleManager::LoadModuleChecked< FAssetToolsModule >("AssetTools").Get(); + RegisterAssetTypeAction(AssetTools, MakeShareable(new FAssetTypeActions_HoudiniAsset())); + RegisterAssetTypeAction(AssetTools, MakeShareable(new FAssetTypeActions_HoudiniToolsPackageAsset())); + RegisterAssetTypeAction(AssetTools, MakeShareable(new FAssetTypeActions_HoudiniPreset())); +} + +void +FHoudiniEngineEditor::UnregisterAssetTypeActions() +{ + // Unregister asset type actions we have previously registered. + if (FModuleManager::Get().IsModuleLoaded("AssetTools")) + { + IAssetTools & AssetTools = FModuleManager::GetModuleChecked< FAssetToolsModule >("AssetTools").Get(); + + for (int32 Index = 0; Index < AssetTypeActions.Num(); ++Index) + AssetTools.UnregisterAssetTypeActions(AssetTypeActions[Index].ToSharedRef()); + + AssetTypeActions.Empty(); + } +} + +void +FHoudiniEngineEditor::RegisterAssetBrokers() +{ + // Create and register broker for Houdini asset. + HoudiniAssetBroker = MakeShareable(new FHoudiniAssetBroker()); + FComponentAssetBrokerage::RegisterBroker( HoudiniAssetBroker, UHoudiniAssetComponent::StaticClass(), true, true ); +} + +void +FHoudiniEngineEditor::UnregisterAssetBrokers() +{ + if (UObjectInitialized()) + { + // Unregister broker. + FComponentAssetBrokerage::UnregisterBroker( HoudiniAssetBroker ); + } +} + +void +FHoudiniEngineEditor::RegisterActorFactories() +{ + if (GEditor) + { + UHoudiniAssetActorFactory* HoudiniAssetActorFactory = + NewObject< UHoudiniAssetActorFactory >(GetTransientPackage(), UHoudiniAssetActorFactory::StaticClass()); + UHoudiniPresetActorFactory* HoudiniPresetActorFactory = + NewObject< UHoudiniPresetActorFactory >(GetTransientPackage(), UHoudiniPresetActorFactory::StaticClass()); + + GEditor->ActorFactories.Add(HoudiniAssetActorFactory); + GEditor->ActorFactories.Add(HoudiniPresetActorFactory); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + if (UPlacementSubsystem* PlacementSubsystem = GEditor->GetEditorSubsystem()) + { + PlacementSubsystem->RegisterAssetFactory(HoudiniAssetActorFactory); + PlacementSubsystem->RegisterAssetFactory(HoudiniPresetActorFactory); + } +#endif + } +} + + +void +FHoudiniEngineEditor::RegisterEditorTabs() +{ + const IWorkspaceMenuStructure& MenuStructure = WorkspaceMenu::GetMenuStructure(); + + FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked( TEXT("LevelEditor") ); + + // If we have a valid LevelEditor tab manager, register now, just in case the tab manager is already active. + // Not sure whether this case ever occurs, but if it does it may cause issues with RegisterLayoutExtension events. + const TSharedPtr LevelEditorTabManager = LevelEditorModule.GetLevelEditorTabManager(); + if (LevelEditorTabManager.IsValid()) + { + RegisterLevelEditorTabs(LevelEditorTabManager); + } + + // Be sure to also register during OnRegisterTabs() events, since it will be called whenever the LevelEditor tab manager changes. + OnLevelEditorRegisterTabsHandle = LevelEditorModule.OnRegisterTabs().AddRaw(this, &FHoudiniEngineEditor::RegisterLevelEditorTabs); + + //FGlobalTabmanager::Get()->RegisterTabSpawner(NodeSyncTabName, FOnSpawnTab::CreateRaw(this, &FHoudiniEngineEditor::OnSpawnNodeSyncTab)) + // .SetDisplayName(LOCTEXT("FNodeSyncTitleTitle", "Houdini Node Sync")) + // .SetTooltipText(LOCTEXT("FNodeSyncTitleTitleTooltip", "Houdini Node Sync")) + // .SetMenuType(ETabSpawnerMenuType::Hidden) + // .SetGroup(MenuStructure.GetLevelEditorCategory()); + + // FGlobalTabmanager::Get()->RegisterTabSpawner(HoudiniToolsTabName, FOnSpawnTab::CreateRaw(this, &FHoudiniEngineEditor::OnSpawnHoudiniToolsTab)) + // .SetDisplayName(LOCTEXT("FHoudiniToolsTitle", "Houdini Tools")) + // .SetTooltipText(LOCTEXT("FHoudiniToolsTitleTooltip", "A shelf containing Houdini Digital Assets")) + // .SetMenuType(ETabSpawnerMenuType::Hidden) + // .SetGroup(MenuStructure.GetLevelEditorCategory()); +} + +void +FHoudiniEngineEditor::UnRegisterEditorTabs() +{ + //FGlobalTabmanager::Get()->UnregisterTabSpawner(NodeSyncTabName); + + FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked( TEXT("LevelEditor") ); + const TSharedPtr LevelEditorTabManager = LevelEditorModule.GetLevelEditorTabManager(); + if (LevelEditorTabManager.IsValid()) + { + LevelEditorTabManager->UnregisterTabSpawner(HoudiniToolsTabName); + LevelEditorTabManager->UnregisterTabSpawner(NodeSyncTabName); + } + LevelEditorModule.OnRegisterTabs().Remove(OnLevelEditorRegisterTabsHandle); +} + +void +FHoudiniEngineEditor::RegisterLevelEditorTabs(TSharedPtr LevelTabManager) +{ + if (!LevelTabManager.IsValid()) + { + return; + } + + const IWorkspaceMenuStructure& MenuStructure = WorkspaceMenu::GetMenuStructure(); + LevelTabManager->RegisterTabSpawner(HoudiniToolsTabName, FOnSpawnTab::CreateRaw(this, &FHoudiniEngineEditor::OnSpawnHoudiniToolsTab)) + .SetDisplayName(LOCTEXT("FHoudiniToolsTitle", "Houdini Tools")) + .SetTooltipText(LOCTEXT("FHoudiniToolsTitleTooltip", "A shelf containing Houdini Digital Assets")) + .SetMenuType(ETabSpawnerMenuType::Hidden) + .SetGroup(MenuStructure.GetLevelEditorCategory()); + + LevelTabManager->RegisterTabSpawner(NodeSyncTabName, FOnSpawnTab::CreateRaw(this, &FHoudiniEngineEditor::OnSpawnNodeSyncTab)) + .SetDisplayName(LOCTEXT("FNodeSyncTitleTitle", "Houdini Node Sync")) + .SetTooltipText(LOCTEXT("FNodeSyncTitleTitleTooltip", "Houdini Node Sync")) + .SetMenuType(ETabSpawnerMenuType::Hidden) + .SetGroup(MenuStructure.GetLevelEditorCategory()); +} + +void +FHoudiniEngineEditor::BindMenuCommands() +{ + HEngineCommands = MakeShareable(new FUICommandList); + + FHoudiniEngineCommands::Register(); + const FHoudiniEngineCommands& Commands = FHoudiniEngineCommands::Get(); + + // Session + + HEngineCommands->MapAction( + Commands._CreateSession, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::CreateSession(); }), + FCanExecuteAction::CreateLambda([]() { return !FHoudiniEngineCommands::IsSessionValid(); })); + + HEngineCommands->MapAction( + Commands._ConnectSession, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::ConnectSession(); }), + FCanExecuteAction::CreateLambda([]() { return !FHoudiniEngineCommands::IsSessionValid(); })); + + HEngineCommands->MapAction( + Commands._StopSession, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::StopSession(); }), + FCanExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::IsSessionValid(); })); + + HEngineCommands->MapAction( + Commands._RestartSession, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::RestartSession(); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + HEngineCommands->MapAction( + Commands._OpenSessionSync, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::OpenSessionSync(); }), + FCanExecuteAction::CreateLambda([]() { return !FHoudiniEngineCommands::IsSessionSyncProcessValid(); })); + + HEngineCommands->MapAction( + Commands._CloseSessionSync, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::CloseSessionSync(); }), + FCanExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::IsSessionSyncProcessValid(); })); + + HEngineCommands->MapAction( + Commands._ViewportSyncNone, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::SetViewportSync(0); }), + FCanExecuteAction::CreateLambda([]() { return true; }), + FIsActionChecked::CreateLambda([]() { return (FHoudiniEngineCommands::GetViewportSync() == 0); }) + ); + + HEngineCommands->MapAction( + Commands._ViewportSyncHoudini, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::SetViewportSync(1); }), + FCanExecuteAction::CreateLambda([]() { return true; }), + FIsActionChecked::CreateLambda([]() { return (FHoudiniEngineCommands::GetViewportSync() == 1); }) + ); + + HEngineCommands->MapAction( + Commands._ViewportSyncUnreal, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::SetViewportSync(2); }), + FCanExecuteAction::CreateLambda([]() { return true; }), + FIsActionChecked::CreateLambda([]() { return (FHoudiniEngineCommands::GetViewportSync() == 2); }) + ); + + HEngineCommands->MapAction( + Commands._ViewportSyncBoth, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::SetViewportSync(3); }), + FCanExecuteAction::CreateLambda([]() { return true; }), + FIsActionChecked::CreateLambda([]() { return (FHoudiniEngineCommands::GetViewportSync() == 3); }) + ); + + HEngineCommands->MapAction( + Commands._OpenNodeSync, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::OpenNodeSync(); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + HEngineCommands->MapAction( + Commands._OpenHoudiniTools, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::OpenHoudiniToolsTab(); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + // PDG commandlet + HEngineCommands->MapAction( + Commands._IsPDGCommandletEnabled, + FExecuteAction::CreateLambda([]() { FHoudiniEngineCommands::SetPDGCommandletEnabled(!FHoudiniEngineCommands::IsPDGCommandletEnabled()); }), + FCanExecuteAction::CreateLambda([]() { return true; }), + FIsActionChecked::CreateLambda([]() { return FHoudiniEngineCommands::IsPDGCommandletEnabled(); }) + ); + + HEngineCommands->MapAction( + Commands._StartPDGCommandlet, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::StartPDGCommandlet(); }), + FCanExecuteAction::CreateLambda([]() + { + return FHoudiniEngineCommands::IsPDGCommandletEnabled() && !FHoudiniEngineCommands::IsPDGCommandletRunningOrConnected(); + }) + ); + + HEngineCommands->MapAction( + Commands._StopPDGCommandlet, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::StopPDGCommandlet(); }), + FCanExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::IsPDGCommandletRunningOrConnected(); })); + + // Plugin + + HEngineCommands->MapAction( + Commands._InstallInfo, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::ShowInstallInfo(); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + HEngineCommands->MapAction( + Commands._PluginSettings, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::ShowPluginSettings(); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + HEngineCommands->MapAction( + Commands._PluginEditorSettings, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::ShowPluginEditorSettings(); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + // Files + + HEngineCommands->MapAction( + Commands._OpenInHoudini, + FExecuteAction::CreateLambda([](){ return FHoudiniEngineCommands::OpenInHoudini(); }), + FCanExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::IsSessionValid(); })); + + HEngineCommands->MapAction( + Commands._SaveHIPFile, + FExecuteAction::CreateLambda([](){ return FHoudiniEngineCommands::SaveHIPFile(); }), + FCanExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::IsSessionValid(); })); + + HEngineCommands->MapAction( + Commands._CleanUpTempFolder, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::CleanUpTempFolder(); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + // Help and support + HEngineCommands->MapAction( + Commands._ContentExampleGit, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::OpenContentExampleGit(); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + HEngineCommands->MapAction( + Commands._ContentExampleBrowseTo, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::BrowseToContentExamples(); }), + FCanExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::HasContentExamples(); })); + + HEngineCommands->MapAction( + Commands._ReportBug, + FExecuteAction::CreateLambda([](){ return FHoudiniEngineCommands::ReportBug(); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + HEngineCommands->MapAction( + Commands._OnlineDoc, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::OnlineDocumentation(); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + HEngineCommands->MapAction( + Commands._OnlineForum, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::OnlineForum(); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + // Actions + + HEngineCommands->MapAction( + Commands._CookAll, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::RecookAllAssets(); }), + FCanExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::IsSessionValid(); })); + + HEngineCommands->MapAction( + Commands._CookSelected, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::RecookSelection(); }), + FCanExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::IsSessionValid(); })); + + HEngineCommands->MapAction( + Commands._RebuildAll, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::RebuildAllAssets(); }), + FCanExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::IsSessionValid(); })); + + HEngineCommands->MapAction( + Commands._RebuildSelected, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::RebuildSelection(); }), + FCanExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::IsSessionValid(); })); + + HEngineCommands->MapAction( + Commands._BakeAll, + FExecuteAction::CreateLambda([](){ return FHoudiniEngineCommands::BakeAllAssets(); }), + FCanExecuteAction::CreateLambda([](){ return true; })); + + HEngineCommands->MapAction( + Commands._BakeSelected, + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::BakeSelection(); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + HEngineCommands->MapAction( + Commands._RefineAll, + FExecuteAction::CreateLambda([]() { FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshes(false); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + HEngineCommands->MapAction( + Commands._RefineSelected, + FExecuteAction::CreateLambda([]() { FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshes(true); }), + FCanExecuteAction::CreateLambda([]() { return true; })); + + HEngineCommands->MapAction( + Commands._PauseAssetCooking, + FExecuteAction::CreateLambda([](){ return FHoudiniEngineCommands::PauseAssetCooking(); }), + FCanExecuteAction::CreateLambda([](){ return FHoudiniEngineCommands::IsSessionValid(); }), + FIsActionChecked::CreateLambda([](){ return FHoudiniEngineCommands::IsAssetCookingPaused(); })); + + // Non menu command (used for shortcuts only) + + // Append the command to the editor module + FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked< FLevelEditorModule >("LevelEditor"); + LevelEditorModule.GetGlobalLevelEditorActions()->Append(HEngineCommands.ToSharedRef()); +} + +void +FHoudiniEngineEditor::ExtendMenu() +{ + if (IsRunningCommandlet()) + return; + + // We need to add/bind the UI Commands to their functions first + BindMenuCommands(); + + MainMenuExtender = MakeShareable(new FExtender); + + // Extend File menu, we will add Houdini section. + MainMenuExtender->AddMenuExtension( + "FileLoadAndSave", + EExtensionHook::After, + HEngineCommands, + FMenuExtensionDelegate::CreateStatic(&FHoudiniEngineEditor::AddHoudiniFileMenuExtension)); + + MainMenuExtender->AddMenuBarExtension( + "Edit", + EExtensionHook::After, + HEngineCommands, + FMenuBarExtensionDelegate::CreateStatic(&FHoudiniEngineEditor::AddHoudiniEditorMenu)); + + // Add our menu extender + FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); + LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MainMenuExtender); +} + +void +FHoudiniEngineEditor::AddHoudiniFileMenuExtension(FMenuBuilder & MenuBuilder) +{ + MenuBuilder.BeginSection("Houdini", LOCTEXT("HoudiniLabel", "Houdini Engine")); + + // Icons used by the commands are defined in the HoudiniEngineStyle + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._OpenInHoudini); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._SaveHIPFile); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._CleanUpTempFolder); + + MenuBuilder.EndSection(); +} + +void +FHoudiniEngineEditor::AddHoudiniEditorMenu(FMenuBarBuilder& MenuBarBuilder) +{ + // View + MenuBarBuilder.AddPullDownMenu( + LOCTEXT("HoudiniLabel", "Houdini Engine"), + LOCTEXT("HoudiniMenu_ToolTip", "Open the Houdini Engine menu"), + FNewMenuDelegate::CreateStatic(&FHoudiniEngineEditor::AddHoudiniMainMenuExtension), + "View"); +} + +void +FHoudiniEngineEditor::AddHoudiniMainMenuExtension(FMenuBuilder & MenuBuilder) +{ + MenuBuilder.BeginSection("Session", LOCTEXT("SessionLabel", "Session")); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._CreateSession); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._ConnectSession); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._StopSession); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._RestartSession); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._OpenSessionSync); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._CloseSessionSync); + + // Viewport sync menu + struct FLocalMenuBuilder + { + static void FillViewportSyncMenu(FMenuBuilder& InSubMenuBuilder) + { + InSubMenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._ViewportSyncNone); + InSubMenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._ViewportSyncHoudini); + InSubMenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._ViewportSyncUnreal); + InSubMenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._ViewportSyncBoth); + } + }; + + MenuBuilder.AddSubMenu( + LOCTEXT("SyncViewport", "Sync Viewport"), + LOCTEXT("SyncViewport_ToolTip", "Sync Viewport"), + FNewMenuDelegate::CreateStatic(&FLocalMenuBuilder::FillViewportSyncMenu), + false, + FSlateIcon(FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine._SyncViewport")); + + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._OpenNodeSync); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._OpenHoudiniTools); + + MenuBuilder.EndSection(); + + MenuBuilder.BeginSection("PDG", LOCTEXT("PDGLabel", "PDG")); + struct FLocalPDGMenuBuilder + { + static void FillPDGMenu(FMenuBuilder& InSubMenuBuilder) + { + InSubMenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._IsPDGCommandletEnabled); + InSubMenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._StartPDGCommandlet); + InSubMenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._StopPDGCommandlet); + } + }; + MenuBuilder.AddSubMenu( + LOCTEXT("PDGSubMenu", "Work Item Import Settings"), + LOCTEXT("PDGSubmenu_ToolTip", "PDG Work Item Import Settings"), + FNewMenuDelegate::CreateStatic(&FLocalPDGMenuBuilder::FillPDGMenu), + false, + FSlateIcon(FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.PDGLink")); + MenuBuilder.EndSection(); + + MenuBuilder.BeginSection("Plugin", LOCTEXT("PluginLabel", "Plugin")); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._InstallInfo); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._PluginSettings); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._PluginEditorSettings); + MenuBuilder.EndSection(); + + MenuBuilder.BeginSection("File", LOCTEXT("FileLabel", "File")); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._OpenInHoudini); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._SaveHIPFile); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._CleanUpTempFolder); + MenuBuilder.EndSection(); + + MenuBuilder.BeginSection("Help", LOCTEXT("HelpLabel", "Help And Support")); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._OnlineDoc); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._OnlineForum); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._ReportBug); + + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._ContentExampleGit); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._ContentExampleBrowseTo); + MenuBuilder.EndSection(); + + MenuBuilder.BeginSection("Actions", LOCTEXT("ActionsLabel", "Actions")); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._CookAll); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._CookSelected); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._RebuildAll); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._RebuildSelected); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._BakeAll); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._BakeSelected); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._RefineAll); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._RefineSelected); + MenuBuilder.AddMenuEntry(FHoudiniEngineCommands::Get()._PauseAssetCooking); + + MenuBuilder.EndSection(); +} + +void +FHoudiniEngineEditor::RegisterForUndo() +{ + /* + if (GUnrealEd) + GUnrealEd->RegisterForUndo(this); + */ +} + +void +FHoudiniEngineEditor::UnregisterForUndo() +{ + /* + if (GUnrealEd) + GUnrealEd->UnregisterForUndo(this); + */ +} + +void +FHoudiniEngineEditor::RegisterPlacementModeExtensions() +{ + +} + +void +FHoudiniEngineEditor::UnregisterPlacementModeExtensions() +{ + +} + +void +FHoudiniEngineEditor::RegisterSectionMappings() +{ + FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + + // Houdini Category Section + FName ClassName = UHoudiniAssetComponent::StaticClass()->GetFName(); + TSharedRef Section = PropertyModule.FindOrCreateSection(ClassName, "Houdini", LOCTEXT("Houdini", "Houdini")); + + // The Section (more or less details filters) will contain the following categories + // Houdini Engine + FString CatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_MAIN); + Section->AddCategory(*CatName); + + // HoudiniPDGAssetLink + CatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_PDG); + Section->AddCategory(*CatName); + + // HoudiniParameters + CatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_PARAMS); + Section->AddCategory(*CatName); + + // HoudiniHandles + CatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_HANDLES); + Section->AddCategory(*CatName); + + // HoudiniInputs + CatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_INPUTS); + Section->AddCategory(*CatName); + + // HoudiniOutputs + CatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_OUTPUTS); + Section->AddCategory(*CatName); + + // HoudiniNodeSync + CatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_NODESYNC); + Section->AddCategory(*CatName); + + // HoudiniAsset + CatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_HDA); + Section->AddCategory(*CatName); + + // HoudiniMeshGeneration + CatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_MESHGEN); + Section->AddCategory(*CatName); + + // HoudiniProxyMeshGeneration + //CatName = TEXT(HOUDINI_ENGINE_EDITOR_CATEGORY_PROXY); + //Section->AddCategory(*CatName); +} + +void +FHoudiniEngineEditor::UnregisterSectionMappings() +{ + if (FModuleManager::Get().IsModuleLoaded("PropertyEditor") && FSlateApplication::IsInitialized()) + { + FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); + FName ClassName = UHoudiniAssetComponent::StaticClass()->GetFName(); + + PropertyModule.RemoveSection(ClassName, "Houdini"); + } +} + + +void +FHoudiniEngineEditor::InitializeWidgetResource() +{ + // Choice labels for all the input types + //TArray> InputTypeChoiceLabels; + InputTypeChoiceLabels.Reset(); + InputTypeChoiceLabels.Add(MakeShareable(new FString(UHoudiniInput::InputTypeToString(EHoudiniInputType::Geometry)))); + InputTypeChoiceLabels.Add(MakeShareable(new FString(UHoudiniInput::InputTypeToString(EHoudiniInputType::World)))); + InputTypeChoiceLabels.Add(MakeShareable(new FString(UHoudiniInput::InputTypeToString(EHoudiniInputType::Curve)))); + + PCGInputTypeChoiceLabels.Reset(); + PCGInputTypeChoiceLabels.Add(MakeShareable(new FString(UHoudiniInput::InputTypeToString(EHoudiniInputType::Geometry)))); + PCGInputTypeChoiceLabels.Add(MakeShareable(new FString(UHoudiniInput::InputTypeToString(EHoudiniInputType::World)))); + PCGInputTypeChoiceLabels.Add(MakeShareable(new FString(UHoudiniInput::InputTypeToString(EHoudiniInputType::PCGInput)))); + + BlueprintInputTypeChoiceLabels.Reset(); + BlueprintInputTypeChoiceLabels.Add(MakeShareable(new FString(UHoudiniInput::InputTypeToString(EHoudiniInputType::Geometry)))); + BlueprintInputTypeChoiceLabels.Add(MakeShareable(new FString(UHoudiniInput::InputTypeToString(EHoudiniInputType::Curve)))); + + AssetEditorInputTypeChoiceLabels.Reset(); + AssetEditorInputTypeChoiceLabels.Add(MakeShareable(new FString(UHoudiniInput::InputTypeToString(EHoudiniInputType::Geometry)))); + AssetEditorInputTypeChoiceLabels.Add(MakeShareable(new FString(UHoudiniInput::InputTypeToString(EHoudiniInputType::World)))); + + // Choice labels for all Houdini curve types + HoudiniCurveTypeChoiceLabels.Reset(); + HoudiniCurveTypeChoiceLabels.Add(MakeShareable(new FString(FHoudiniEngineEditorUtils::HoudiniCurveTypeToString(EHoudiniCurveType::Polygon)))); + HoudiniCurveTypeChoiceLabels.Add(MakeShareable(new FString(FHoudiniEngineEditorUtils::HoudiniCurveTypeToString(EHoudiniCurveType::Nurbs)))); + HoudiniCurveTypeChoiceLabels.Add(MakeShareable(new FString(FHoudiniEngineEditorUtils::HoudiniCurveTypeToString(EHoudiniCurveType::Bezier)))); + HoudiniCurveTypeChoiceLabels.Add(MakeShareable(new FString(FHoudiniEngineEditorUtils::HoudiniCurveTypeToString(EHoudiniCurveType::Points)))); + + // Choice labels for all Houdini curve methods + HoudiniCurveMethodChoiceLabels.Reset(); + HoudiniCurveMethodChoiceLabels.Add(MakeShareable(new FString(FHoudiniEngineEditorUtils::HoudiniCurveMethodToString(EHoudiniCurveMethod::CVs)))); + HoudiniCurveMethodChoiceLabels.Add(MakeShareable(new FString(FHoudiniEngineEditorUtils::HoudiniCurveMethodToString(EHoudiniCurveMethod::Breakpoints)))); + HoudiniCurveMethodChoiceLabels.Add(MakeShareable(new FString(FHoudiniEngineEditorUtils::HoudiniCurveMethodToString(EHoudiniCurveMethod::Freehand)))); + + // Choice labels for all Houdini breakpoint parameterization + HoudiniCurveBreakpointParameterizationChoiceLabels.Reset(); + HoudiniCurveBreakpointParameterizationChoiceLabels.Add(MakeShareable(new FString(FHoudiniEngineEditorUtils::HoudiniCurveBreakpointParameterizationToString(EHoudiniCurveBreakpointParameterization::Uniform)))); + HoudiniCurveBreakpointParameterizationChoiceLabels.Add(MakeShareable(new FString(FHoudiniEngineEditorUtils::HoudiniCurveBreakpointParameterizationToString(EHoudiniCurveBreakpointParameterization::Chord)))); + HoudiniCurveBreakpointParameterizationChoiceLabels.Add(MakeShareable(new FString(FHoudiniEngineEditorUtils::HoudiniCurveBreakpointParameterizationToString(EHoudiniCurveBreakpointParameterization::Centripetal)))); + + // Choice labels for all Houdini ramp parameter interpolation methods + HoudiniParameterRampInterpolationLabels.Reset(); + HoudiniParameterRampInterpolationLabels.Add(MakeShareable(new FString(UHoudiniParameter::GetStringFromHoudiniInterpMethod(EHoudiniRampInterpolationType::CONSTANT)))); + HoudiniParameterRampInterpolationLabels.Add(MakeShareable(new FString(UHoudiniParameter::GetStringFromHoudiniInterpMethod(EHoudiniRampInterpolationType::LINEAR)))); + HoudiniParameterRampInterpolationLabels.Add(MakeShareable(new FString(UHoudiniParameter::GetStringFromHoudiniInterpMethod(EHoudiniRampInterpolationType::CATMULL_ROM)))); + HoudiniParameterRampInterpolationLabels.Add(MakeShareable(new FString(UHoudiniParameter::GetStringFromHoudiniInterpMethod(EHoudiniRampInterpolationType::MONOTONE_CUBIC)))); + HoudiniParameterRampInterpolationLabels.Add(MakeShareable(new FString(UHoudiniParameter::GetStringFromHoudiniInterpMethod(EHoudiniRampInterpolationType::BEZIER)))); + HoudiniParameterRampInterpolationLabels.Add(MakeShareable(new FString(UHoudiniParameter::GetStringFromHoudiniInterpMethod(EHoudiniRampInterpolationType::BSPLINE)))); + HoudiniParameterRampInterpolationLabels.Add(MakeShareable(new FString(UHoudiniParameter::GetStringFromHoudiniInterpMethod(EHoudiniRampInterpolationType::HERMITE)))); + + // Choice labels for all Houdini curve output export types + HoudiniCurveOutputExportTypeLabels.Reset(); + HoudiniCurveOutputExportTypeLabels.Add(MakeShareable(new FString(TEXT("Unreal Spline")))); + HoudiniCurveOutputExportTypeLabels.Add(MakeShareable(new FString(TEXT("Houdini Spline")))); + + // Choice labels for all Unreal curve output curve types + //(for temporary, we need to figure out a way to access the output curve's info later) + UnrealCurveOutputCurveTypeLabels.Reset(); + UnrealCurveOutputCurveTypeLabels.Add(MakeShareable(new FString(TEXT("Linear")))); + UnrealCurveOutputCurveTypeLabels.Add(MakeShareable(new FString(TEXT("Curve")))); + + // Option labels for all landscape outputs bake options + HoudiniLandscapeOutputBakeOptionLabels.Reset(); + HoudiniLandscapeOutputBakeOptionLabels.Add(MakeShareable(new FString(TEXT("To Current Level")))); + HoudiniLandscapeOutputBakeOptionLabels.Add(MakeShareable(new FString(TEXT("To Image")))); + HoudiniLandscapeOutputBakeOptionLabels.Add(MakeShareable(new FString(TEXT("To New World")))); + + // Option labels for Houdini Engine PDG bake options + HoudiniEnginePDGBakeTypeOptionLabels.Reset(); + HoudiniEnginePDGBakeTypeOptionLabels.Add(MakeShareable(new FString(FHoudiniEngineEditor::GetStringFromHoudiniEngineBakeOption(EHoudiniEngineBakeOption::ToActor)))); + HoudiniEnginePDGBakeTypeOptionLabels.Add(MakeShareable(new FString(FHoudiniEngineEditor::GetStringFromHoudiniEngineBakeOption(EHoudiniEngineBakeOption::ToBlueprint)))); + + // Option labels for Houdini Engine bake options + HoudiniEngineBakeTypeOptionLabels.Reset(); + HoudiniEngineBakeTypeOptionLabels.Add(MakeShareable(new FString(FHoudiniEngineEditor::GetStringFromHoudiniEngineBakeOption(EHoudiniEngineBakeOption::ToActor)))); + HoudiniEngineBakeTypeOptionLabels.Add(MakeShareable(new FString(FHoudiniEngineEditor::GetStringFromHoudiniEngineBakeOption(EHoudiniEngineBakeOption::ToBlueprint)))); + HoudiniEngineBakeTypeOptionLabels.Add(MakeShareable(new FString(FHoudiniEngineEditor::GetStringFromHoudiniEngineBakeOption(EHoudiniEngineBakeOption::ToAsset)))); + + // Option labels for Houdini Engine PDG bake options + HoudiniEnginePDGBakeSelectionOptionLabels.Reset(); + HoudiniEnginePDGBakeSelectionOptionLabels.Add(MakeShareable(new FString(FHoudiniEngineEditor::GetStringFromPDGBakeTargetOption(EPDGBakeSelectionOption::All)))); + HoudiniEnginePDGBakeSelectionOptionLabels.Add(MakeShareable(new FString(FHoudiniEngineEditor::GetStringFromPDGBakeTargetOption(EPDGBakeSelectionOption::SelectedNetwork)))); + HoudiniEnginePDGBakeSelectionOptionLabels.Add(MakeShareable(new FString(FHoudiniEngineEditor::GetStringFromPDGBakeTargetOption(EPDGBakeSelectionOption::SelectedNode)))); + + HoudiniEnginePDGBakePackageReplaceModeOptionLabels.Reset(); + HoudiniEnginePDGBakePackageReplaceModeOptionLabels.Add(MakeShareable(new FString(FHoudiniEngineEditor::GetStringFromPDGBakePackageReplaceModeOption(EPDGBakePackageReplaceModeOption::ReplaceExistingAssets)))); + HoudiniEnginePDGBakePackageReplaceModeOptionLabels.Add(MakeShareable(new FString(FHoudiniEngineEditor::GetStringFromPDGBakePackageReplaceModeOption(EPDGBakePackageReplaceModeOption::CreateNewAssets)))); + + HoudiniEngineBakeActorOptionsLabels.Reset(); + HoudiniEngineBakeActorOptionsLabels.Add(MakeShareable(new FString(FHoudiniEngineEditor::GetStringfromActorBakeOption(EHoudiniEngineActorBakeOption::OneActorPerComponent)))); + HoudiniEngineBakeActorOptionsLabels.Add(MakeShareable(new FString(FHoudiniEngineEditor::GetStringfromActorBakeOption(EHoudiniEngineActorBakeOption::OneActorPerHDA)))); + + static FString IconsDir = FHoudiniEngineUtils::GetHoudiniEnginePluginDir() / TEXT("Resources/Icons/"); + + // Houdini Logo Brush + FString Icon128FilePath = IconsDir + TEXT("icon_houdini_logo_128"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*Icon128FilePath)) + { + const FName BrushName(*Icon128FilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniLogoBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // Houdini Engine Logo Brush + FString HEIcon128FilePath = IconsDir + TEXT("icon_hengine_logo_128"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*HEIcon128FilePath)) + { + const FName BrushName(*HEIcon128FilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineLogoBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // Houdini Engine Banner + FString HoudiniEngineUIIconFilePath = GetHoudiniEnginePluginDir() / TEXT("Resources/hengine_banner_d.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*HoudiniEngineUIIconFilePath)) + { + const FName BrushName(*HoudiniEngineUIIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUIIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // Rebuild Icon Brush + FString HoudiniEngineUIRebuildIconFilePath = IconsDir + TEXT("rebuild_all16x16.png"); + //FString HoudiniEngineUIRebuildIconFilePath = GetHoudiniEnginePluginDir() / TEXT("Resources/hengine_reload_icon.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*HoudiniEngineUIRebuildIconFilePath)) + { + const FName BrushName(*HoudiniEngineUIRebuildIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUIRebuildIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // Recook Icon Brush + //FString HoudiniEngineUIRecookIconFilePath = GetHoudiniEnginePluginDir() / TEXT("Resources/hengine_recook_icon.png"); + FString HoudiniEngineUIRecookIconFilePath = IconsDir + TEXT("cook_all16x16.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*HoudiniEngineUIRecookIconFilePath)) + { + const FName BrushName(*HoudiniEngineUIRecookIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUIRecookIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // Reset Parameters Icon Brush + //FString HoudiniEngineUIResetParametersIconFilePath = GetHoudiniEnginePluginDir() / TEXT("Resources/hengine_resetparameters_icon.png"); + FString HoudiniEngineUIResetParametersIconFilePath = IconsDir + TEXT("reset_parameters16x16.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*HoudiniEngineUIResetParametersIconFilePath)) + { + const FName BrushName(*HoudiniEngineUIResetParametersIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUIResetParametersIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // Bake + FString BakeIconFilePath = IconsDir + TEXT("bake_all16x16.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*BakeIconFilePath)) + { + const FName BrushName(*BakeIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUIBakeIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // CookLog + FString CookLogIconFilePath = IconsDir + TEXT("cook_log16x16.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*CookLogIconFilePath)) + { + const FName BrushName(*CookLogIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUICookLogIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // AssetHelp + FString AssetHelpIconFilePath = IconsDir + TEXT("asset_help16x16.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*AssetHelpIconFilePath)) + { + const FName BrushName(*AssetHelpIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUIAssetHelpIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + + // PDG Asset Link + // PDG + FString PDGIconFilePath = IconsDir + TEXT("pdg_link16x16.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*PDGIconFilePath)) + { + const FName BrushName(*PDGIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUIPDGIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // PDG Cancel + // PDGCancel + FString PDGCancelIconFilePath = IconsDir + TEXT("pdg_cancel16x16.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*PDGCancelIconFilePath)) + { + const FName BrushName(*PDGCancelIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUIPDGCancelIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // PDG Dirty All + // PDGDirtyAll + FString PDGDirtyAllIconFilePath = IconsDir + TEXT("pdg_dirty_all16x16.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*PDGDirtyAllIconFilePath)) + { + const FName BrushName(*PDGDirtyAllIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUIPDGDirtyAllIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // PDG Dirty Node + // PDGDirtyNode + FString PDGDirtyNodeIconFilePath = IconsDir + TEXT("pdg_dirty_node16x16.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*PDGDirtyNodeIconFilePath)) + { + const FName BrushName(*PDGDirtyNodeIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUIPDGDirtyNodeIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // PDG Pause + // PDGReset + FString PDGPauseIconFilePath = IconsDir + TEXT("pdg_pause16x16.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*PDGPauseIconFilePath)) + { + const FName BrushName(*PDGPauseIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUIPDGPauseIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // PDG Reset + // PDGReset + FString PDGResetIconFilePath = IconsDir + TEXT("pdg_reset16x16.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*PDGResetIconFilePath)) + { + const FName BrushName(*PDGResetIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUIPDGResetIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } + + // PDG Refresh + // PDGRefresh + FString PDGRefreshIconFilePath = IconsDir + TEXT("pdg_refresh16x16.png"); + if (FSlateApplication::IsInitialized() && FPlatformFileManager::Get().GetPlatformFile().FileExists(*PDGRefreshIconFilePath)) + { + const FName BrushName(*PDGRefreshIconFilePath); + const FIntPoint Size = FSlateApplication::Get().GetRenderer()->GenerateDynamicImageResource(BrushName); + if (Size.X > 0 && Size.Y > 0) + { + static const int32 ProgressIconSize = 32; + HoudiniEngineUIPDGRefreshIconBrush = MakeShareable(new FSlateDynamicImageBrush( + BrushName, FVector2D(ProgressIconSize, ProgressIconSize))); + } + } +} + +void +FHoudiniEngineEditor::SendToHoudini_CB(TArray SelectedAssets) +{ + UHoudiniEditorNodeSyncSubsystem* HoudiniSubsystem = GEditor->GetEditorSubsystem(); + if (!IsValid(HoudiniSubsystem)) + return; + + TArray SelectedObjects; + for (auto& CurrentAsset : SelectedAssets) + { + UObject* CurrentObject = CurrentAsset.GetAsset(); + if (!IsValid(CurrentObject)) + continue; + + SelectedObjects.Add(CurrentObject); + } + + HoudiniSubsystem->SendContentBrowserSelection(SelectedObjects); +} + +void +FHoudiniEngineEditor::SendToHoudini_World() +{ + UHoudiniEditorNodeSyncSubsystem* HoudiniSubsystem = GEditor->GetEditorSubsystem(); + if (!IsValid(HoudiniSubsystem)) + return; + + HoudiniSubsystem->SendWorldSelection(); +} + +void +FHoudiniEngineEditor::ExtendContextMenu() +{ + { + FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + TArray< FContentBrowserMenuExtender_SelectedAssets >& CBMenuExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders(); + + CBMenuExtenderDelegates.Add(FContentBrowserMenuExtender_SelectedAssets::CreateLambda([this](const TArray& SelectedAssets) + { + TSharedRef Extender(new FExtender()); + + bool bShouldExtendAssetActions = true; + for (const FAssetData& Asset : SelectedAssets) + { + // TODO: Foliage Types? BP ? +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + if ((Asset.AssetClassPath != USkeletalMesh::StaticClass()->GetClassPathName()) + && (Asset.AssetClassPath != UStaticMesh::StaticClass()->GetClassPathName()) + && (Asset.AssetClassPath != UAnimSequence::StaticClass()->GetClassPathName()) + && (Asset.AssetClassPath != UTexture2D::StaticClass()->GetClassPathName())) +#else + if ((Asset.AssetClass != USkeletalMesh::StaticClass()->GetFName()) + && (Asset.AssetClass != UStaticMesh::StaticClass()->GetFName()) + && (Asset.AssetClass != UAnimSequence::StaticClass()->GetFName()) + && (Asset.AssetClass != UTexture2D::StaticClass()->GetFName())) +#endif + { + bShouldExtendAssetActions = false; + break; + } + } + + if (bShouldExtendAssetActions) + { + Extender->AddMenuExtension( + "GetAssetActions", + EExtensionHook::After, + nullptr, + FMenuExtensionDelegate::CreateLambda( + [SelectedAssets, this](FMenuBuilder& MenuBuilder) + { + MenuBuilder.AddMenuEntry( + LOCTEXT("CB_Extension_SendToHoudini", "Send To Houdini"), + LOCTEXT("CB_Extension_SendToHoudini_Tooltip", "Send this asset to houdini"), + FSlateIcon(FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateLambda([SelectedAssets, this]() { SendToHoudini_CB(SelectedAssets); }), + FCanExecuteAction::CreateLambda([=] { return (SelectedAssets.Num() > 0); }) + ) + ); + }) + ); + } + + return Extender; + })); + ContentBrowserExtenderDelegateHandle = CBMenuExtenderDelegates.Last().GetHandle(); + + } + +} + + +void +FHoudiniEngineEditor::AddLevelViewportMenuExtender() +{ + FLevelEditorModule& LevelEditorModule = FModuleManager::Get().LoadModuleChecked("LevelEditor"); + auto& MenuExtenders = LevelEditorModule.GetAllLevelViewportContextMenuExtenders(); + + MenuExtenders.Add(FLevelEditorModule::FLevelViewportMenuExtender_SelectedActors::CreateRaw(this, &FHoudiniEngineEditor::GetLevelViewportContextMenuExtender)); + LevelViewportExtenderHandle = MenuExtenders.Last().GetHandle(); +} + +void +FHoudiniEngineEditor::RemoveLevelViewportMenuExtender() +{ + if (LevelViewportExtenderHandle.IsValid()) + { + FLevelEditorModule* LevelEditorModule = FModuleManager::Get().GetModulePtr("LevelEditor"); + if (LevelEditorModule) + { + typedef FLevelEditorModule::FLevelViewportMenuExtender_SelectedActors DelegateType; + LevelEditorModule->GetAllLevelViewportContextMenuExtenders().RemoveAll( + [this](const DelegateType& In) { return In.GetHandle() == LevelViewportExtenderHandle; }); + } + } +} + +TSharedRef +FHoudiniEngineEditor::GetLevelViewportContextMenuExtender(const TSharedRef CommandList, const TArray InActors) +{ + TSharedRef Extender = MakeShareable(new FExtender); + + // Build an array of the HoudiniAssets corresponding to the selected actors + TArray> Actors; + TArray> HoudiniAssets; + TArray> HoudiniAssetActors; + for (auto CurrentActor : InActors) + { + if (!IsValid(CurrentActor)) + continue; + + Actors.Add(CurrentActor); + + AHoudiniAssetActor * HoudiniAssetActor = Cast(CurrentActor); + if (!IsValid(HoudiniAssetActor)) + continue; + + HoudiniAssetActors.Add(HoudiniAssetActor); + + UHoudiniAssetComponent* HoudiniAssetComponent = HoudiniAssetActor->GetHoudiniAssetComponent(); + if (!IsValid(HoudiniAssetComponent)) + continue; + + UHoudiniAsset* HoudiniAsset = HoudiniAssetComponent->GetHoudiniAsset(); + if (!IsValid(HoudiniAsset)) + continue; + + HoudiniAssets.AddUnique(HoudiniAsset); + } + + if (HoudiniAssets.Num() > 0) + { + // Add the Asset menu extension + if (AssetTypeActions.Num() > 0) + { + // Add the menu extensions via our HoudiniAssetTypeActions + FAssetTypeActions_HoudiniAsset * HATA = static_cast(AssetTypeActions[0].Get()); + if (HATA) + Extender = HATA->AddLevelEditorMenuExtenders(HoudiniAssets); + } + } + + if (HoudiniAssetActors.Num() > 0) + { + // Add some actor menu extensions + FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked(TEXT("LevelEditor")); + TSharedRef LevelEditorCommandBindings = LevelEditor.GetGlobalLevelEditorActions(); + Extender->AddMenuExtension( + "ActorControl", + EExtensionHook::After, + LevelEditorCommandBindings, + FMenuExtensionDelegate::CreateLambda([this, HoudiniAssetActors](FMenuBuilder& MenuBuilder) + { + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_Recentre", "Recentre selected"), + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_RecentreTooltip", "Recentres the selected Houdini Asset Actors pivots to their input/cooked static mesh average centre."), + FSlateIcon(FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::RecentreSelection(); }), + FCanExecuteAction::CreateLambda([=] { return (HoudiniAssetActors.Num() > 0); }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_Recook", "Recook selected"), + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_RecookTooltip", "Forces a recook on the selected Houdini Asset Actors."), + FSlateIcon(FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine._CookSelected"), + FUIAction( + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::RecookSelection(); }), + FCanExecuteAction::CreateLambda([=] { return (HoudiniAssetActors.Num() > 0); }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_Rebuild", "Rebuild selected"), + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_RebuildTooltip", "Rebuilds selected Houdini Asset Actors in the current level."), + FSlateIcon(FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine._RebuildSelected"), + FUIAction( + FExecuteAction::CreateLambda([]() { return FHoudiniEngineCommands::RebuildSelection(); }), + FCanExecuteAction::CreateLambda([=] { return (HoudiniAssetActors.Num() > 0); }) + ) + ); + + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_Refine_ProxyMeshes", "Refine Houdini Proxy Meshes"), + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "HoudiniActor_Refine_ProxyMeshesTooltip", "Build and replace Houdini Proxy Meshes with Static Meshes."), + FSlateIcon(FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine._RefineSelected"), + FUIAction( + FExecuteAction::CreateLambda([]() { FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshes(true); }), + FCanExecuteAction::CreateLambda([=] { return (HoudiniAssetActors.Num() > 0); }) + ) + ); + }) + ); + } + + // Now add the node sync extender if we have any actor + if (Actors.Num() > 0) + { + // Add some actor menu extensions + FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked(TEXT("LevelEditor")); + TSharedRef LevelEditorCommandBindings = LevelEditor.GetGlobalLevelEditorActions(); + Extender->AddMenuExtension( + "ActorControl", + EExtensionHook::After, + LevelEditorCommandBindings, + FMenuExtensionDelegate::CreateLambda([this, Actors](FMenuBuilder& MenuBuilder) + { + MenuBuilder.AddMenuEntry( + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "Houdini_NodeSync_SendToHoudini", "Send to Houdini"), + NSLOCTEXT("HoudiniAssetLevelViewportContextActions", "Houdini_NodeSync_SendToHoudiniTooltip", "Sends the current selection to Houdini via Node Sync."), + FSlateIcon(FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo"), + FUIAction( + FExecuteAction::CreateLambda([this]() { return SendToHoudini_World(); }), + // Prevent a crash if the NodeSyncPanel hasn't been created yet... + FCanExecuteAction::CreateLambda([this, &Actors] { return Actors.Num() > 0 && NodeSyncPanel.IsValid(); }) + ) + ); + }) + ); + } + + return Extender; +} + +void +FHoudiniEngineEditor::RegisterConsoleCommands() +{ + // Register corresponding console commands + static FAutoConsoleCommand CCmdOpen = FAutoConsoleCommand( + TEXT("Houdini.Open"), + TEXT("Open the scene in Houdini."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::OpenInHoudini)); + + static FAutoConsoleCommand CCmdSave = FAutoConsoleCommand( + TEXT("Houdini.Save"), + TEXT("Save the current Houdini scene to a hip file."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::SaveHIPFile)); + + static FAutoConsoleCommand CCmdBake = FAutoConsoleCommand( + TEXT("Houdini.BakeAll"), + TEXT("Bakes and replaces with blueprints all Houdini Asset Actors in the current level."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::BakeAllAssets)); + + static FAutoConsoleCommand CCmdClean = FAutoConsoleCommand( + TEXT("Houdini.CleanTemp"), + TEXT("Cleans up unused/unreferenced Houdini Engine temporary files."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::CleanUpTempFolder)); + + static FAutoConsoleCommand CCmdPause = FAutoConsoleCommand( + TEXT("Houdini.Pause"), + TEXT("Pauses Houdini Engine Asset cooking."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::PauseAssetCooking)); + + // Additional console only commands + static FAutoConsoleCommand CCmdCookAll = FAutoConsoleCommand( + TEXT("Houdini.CookAll"), + TEXT("Re-cooks all Houdini Engine Asset Actors in the current level."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::RecookAllAssets)); + + static FAutoConsoleCommand CCmdRebuildAll = FAutoConsoleCommand( + TEXT("Houdini.RebuildAll"), + TEXT("Rebuilds all Houdini Engine Asset Actors in the current level."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::RebuildAllAssets)); + + static FAutoConsoleCommand CCmdCookSelec = FAutoConsoleCommand( + TEXT("Houdini.Cook"), + TEXT("Re-cooks selected Houdini Asset Actors in the current level."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::RecookSelection)); + + static FAutoConsoleCommand CCmdRebuildSelec = FAutoConsoleCommand( + TEXT("Houdini.Rebuild"), + TEXT("Rebuilds selected Houdini Asset Actors in the current level."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::RebuildSelection)); + + static FAutoConsoleCommand CCmdBakeSelec = FAutoConsoleCommand( + TEXT("Houdini.Bake"), + TEXT("Bakes and replaces with blueprints selected Houdini Asset Actors in the current level."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::BakeSelection)); + + static FAutoConsoleCommand CCmdRestartSession = FAutoConsoleCommand( + TEXT("Houdini.RestartSession"), + TEXT("Restart the current Houdini Session."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::RestartSession)); + + /* + IConsoleManager &ConsoleManager = IConsoleManager::Get(); + const TCHAR *CommandName = TEXT("HoudiniEngine.RefineHoudiniProxyMeshesToStaticMeshes"); + IConsoleCommand *Command = ConsoleManager.RegisterConsoleCommand( + CommandName, + TEXT("Builds and replaces all Houdini proxy meshes with UStaticMeshes."), + FConsoleCommandDelegate::CreateLambda([]() { FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshes(false); })); + if (Command) + { + ConsoleCommands.Add(Command); + } + else + { + HOUDINI_LOG_ERROR(TEXT("Failed to register the '%s' console command."), CommandName); + } + */ + + static FAutoConsoleCommand CCmdRefine = FAutoConsoleCommand( + TEXT("Houdini.RefineAll"), + TEXT("Builds and replaces all Houdini proxy meshes with UStaticMeshes."), + FConsoleCommandDelegate::CreateLambda([]() { FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshes(false); })); + + static FAutoConsoleCommand CCmdOpenSessionSync = FAutoConsoleCommand( + TEXT("Houdini.OpenSessionSync"), + TEXT("Stops the current session, opens Houdini and automatically start and connect a Session Sync."), + FConsoleCommandDelegate::CreateLambda([]() { FHoudiniEngineCommands::OpenSessionSync(false); })); + +#if !UE_BUILD_SHIPPING + static FAutoConsoleCommand CCmdClearInputManager = FAutoConsoleCommand( + TEXT("Houdini.Debug.ClearInputManager"), + TEXT("Clears all entries from the input manager."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::ClearInputManager)); +#endif + + static FAutoConsoleCommand CCmdDumpGenericAttribute = FAutoConsoleCommand( + TEXT("Houdini.DumpGenericAttribute"), + TEXT("Outputs a list of all the generic property attribute for a given class."), + FConsoleCommandWithArgsDelegate::CreateStatic(&FHoudiniEngineCommands::DumpGenericAttribute)); + + static FAutoConsoleCommand CCmdCleanSession = FAutoConsoleCommand( + TEXT("Houdini.CleanSession"), + TEXT("Cleans the current Houdini Engine Session - this will delete every node in the current Houdini Session."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::CleanHoudiniEngineSession)); + + static FAutoConsoleCommand CCmdStartPerfMon = FAutoConsoleCommand( + TEXT("Houdini.StartHAPIPerformanceMonitor"), + TEXT("Starts a HAPI Performance Monitoring Session."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::StartPerformanceMonitoring)); + + static FAutoConsoleCommand CCmdStopPerfMon = FAutoConsoleCommand( + TEXT("Houdini.StopHAPIPerformanceMonitor"), + TEXT("Stops and save to file the current HAPI Performance Monitoring Session."), + FConsoleCommandDelegate::CreateStatic(&FHoudiniEngineCommands::StopPerformanceMonitoring)); + + static FAutoConsoleCommand CCmdDumpNode = FAutoConsoleCommand( + TEXT("Houdini.DumpNode"), + TEXT("Prints out Houdini Engine information about a given node."), + FConsoleCommandWithArgsDelegate::CreateStatic(&FHoudiniEngineCommands::DumpNode)); + +} + +void +FHoudiniEngineEditor::UnregisterConsoleCommands() +{ + IConsoleManager &ConsoleManager = IConsoleManager::Get(); + for (IConsoleCommand *Command : ConsoleCommands) + { + if (Command) + { + ConsoleManager.UnregisterConsoleObject(Command); + } + } + ConsoleCommands.Empty(); +} + +void +FHoudiniEngineEditor::RegisterEditorDelegates() +{ + // This runs when the world has been modified and saved + // (in non-WP world, this is called when saving the level) + PreSaveWorldEditorDelegateHandle = FEditorDelegates::PreSaveWorldWithContext.AddLambda([this](UWorld* World, FObjectPreSaveContext InContext) + { + // Skip if this is a game world or an autosave, only refine meshes when the user manually saves + if (World->IsGameWorld() || InContext.GetSaveFlags() & ESaveFlags::SAVE_FromAutosave || InContext.IsProceduralSave()) + return; + + // Do the refinement + HandleOnPreSave(World); + + // Set a PostSaveWorld delegate for saving all dirty temp packages + FDelegateHandle& OnPostSaveWorldHandle = FHoudiniEngineEditor::Get().GetOnPostSaveWorldOnceHandle(); + if (OnPostSaveWorldHandle.IsValid()) + { + if (FEditorDelegates::PostSaveWorldWithContext.Remove(OnPostSaveWorldHandle)) + OnPostSaveWorldHandle.Reset(); + } + + // Save all dirty temporary cook package OnPostSaveWorld + OnPostSaveWorldHandle = FEditorDelegates::PostSaveWorldWithContext.AddLambda( + [World](UWorld* PreSaveWorld, FObjectPostSaveContext InContext) + { + if (World && World != PreSaveWorld) + return; + + FHoudiniEngineEditorUtils::SaveAllHoudiniTemporaryCookData(PreSaveWorld); + + FDelegateHandle& OnPostSaveWorldHandle = FHoudiniEngineEditor::Get().GetOnPostSaveWorldOnceHandle(); + if (OnPostSaveWorldHandle.IsValid()) + { + if (FEditorDelegates::PostSaveWorldWithContext.Remove(OnPostSaveWorldHandle)) + OnPostSaveWorldHandle.Reset(); + } + }); + }); + + // WP worlds do not call the PreSaveWorld callback when saving the current level + // This prevented the refinement when saving from being executed properly + // We can instead rely on PreSavePackage, when called on HoudiniAssetActors. + // This means that the refinement is called multiple times when multiple HDAs are in the level, + // but the actual refinement process happens only once. + PreSavePackageEditorDelegateHandle = UPackage::PreSavePackageWithContextEvent.AddLambda([this](UPackage* Package, FObjectPreSaveContext InContext) + { + // Detect if we should actually do anything (check for autosaves, cooking, etc.) + if (InContext.GetSaveFlags() & ESaveFlags::SAVE_FromAutosave || InContext.IsProceduralSave()) + return; + + // Only runs the refinement when Houdini Asset Actors are being saved + UObject* Asset = Package->FindAssetInPackage(); + if (!IsValid(Asset) || !Asset->IsA()) + return; + + AHoudiniAssetActor* HAA = Cast(Asset); + if (!IsValid(HAA)) + return; + + UWorld* World = HAA->GetWorld(); + if (World->IsGameWorld()) + return; + + bool bPostSaveNeeded = HandleOnPreSave(World); + + // Only add a PostSave delegate call if refinement happened + if(!bPostSaveNeeded) + return; + + // Set a PostSavePackage delegate for saving all dirty temp packages + FDelegateHandle& OnPostSavePackageOnceHandle = FHoudiniEngineEditor::Get().GetOnPostSavePackageOnceHandle(); + if (OnPostSavePackageOnceHandle.IsValid()) + { + if (UPackage::PackageSavedWithContextEvent.Remove(OnPostSavePackageOnceHandle)) + OnPostSavePackageOnceHandle.Reset(); + } + + // Save all dirty temporary cook package on PostSavePackage + OnPostSavePackageOnceHandle = UPackage::PackageSavedWithContextEvent.AddLambda( + [World](const FString & PackageFilename, UPackage * Package, FObjectPostSaveContext ObjectSaveContext) + { + FHoudiniEngineEditorUtils::SaveAllHoudiniTemporaryCookData(World); + + FDelegateHandle& OnPostSavePackageOnceHandle = FHoudiniEngineEditor::Get().GetOnPostSavePackageOnceHandle(); + if (OnPostSavePackageOnceHandle.IsValid()) + { + if (UPackage::PackageSavedWithContextEvent.Remove(OnPostSavePackageOnceHandle)) + OnPostSavePackageOnceHandle.Reset(); + } + }); + }); + + PreBeginPIEEditorDelegateHandle = FEditorDelegates::PreBeginPIE.AddLambda([this](const bool bIsSimulating){ this->HandleOnBeginPIE(); }); + + OnDeleteActorsBegin = FEditorDelegates::OnDeleteActorsBegin.AddLambda([this](){ this->HandleOnDeleteActorsBegin(); }); + OnDeleteActorsEnd = FEditorDelegates::OnDeleteActorsEnd.AddLambda([this](){ this-> HandleOnDeleteActorsEnd(); }); + + /* + // + // COMMENTED OUT! Unreal actually calls the delegate for all class, not just the specified owner + // so this actually prevents renaming for everything... + // + // Add a rename prevention filter to HodiniToolsPackage + OnIsNameAllowed.BindLambda([](const FString& Name, FText* OutErrorMessage) -> bool + { + if (Name != FHoudiniToolsRuntimeUtils::GetPackageUAssetName()) + { + *OutErrorMessage = FText::FromString("Renaming a HoudiniToolsPackage to anything but \"HoudiniToolsPackage\" is not supported."); + return false; + } + + return true; + }); + FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + AssetToolsModule.Get().RegisterIsNameAllowedDelegate("HoudiniToolsPackage", OnIsNameAllowed); + */ +} + +void +FHoudiniEngineEditor::UnregisterEditorDelegates() +{ + if (PreSaveWorldEditorDelegateHandle.IsValid()) + FEditorDelegates::PreSaveWorldWithContext.Remove(PreSaveWorldEditorDelegateHandle); + + if (PreSavePackageEditorDelegateHandle.IsValid()) + UPackage::PreSavePackageWithContextEvent.Remove(PreSavePackageEditorDelegateHandle); + + if (PreBeginPIEEditorDelegateHandle.IsValid()) + FEditorDelegates::PreBeginPIE.Remove(PreBeginPIEEditorDelegateHandle); + + if (EndPIEEditorDelegateHandle.IsValid()) + FEditorDelegates::EndPIE.Remove(EndPIEEditorDelegateHandle); + + if (OnDeleteActorsBegin.IsValid()) + FEditorDelegates::OnDeleteActorsBegin.Remove(OnDeleteActorsBegin); + + if (OnDeleteActorsEnd.IsValid()) + FEditorDelegates::OnDeleteActorsEnd.Remove(OnDeleteActorsEnd); + + /* + // Unregister the ToolsPackage rename delegate + if (OnIsNameAllowed.IsBound()) + { + FAssetToolsModule* AssetToolsModule = FModuleManager::GetModulePtr("AssetTools"); + if (AssetToolsModule) + AssetToolsModule->Get().UnregisterIsNameAllowedDelegate("HoudiniToolsPackage"); + + OnIsNameAllowed.Unbind(); + } + */ +} + +FString +FHoudiniEngineEditor::GetStringFromHoudiniEngineBakeOption(EHoudiniEngineBakeOption BakeOption) +{ + FString Str; + switch (BakeOption) + { + case EHoudiniEngineBakeOption::ToActor: + Str = "Actor"; + break; + + case EHoudiniEngineBakeOption::ToBlueprint: + Str = "Blueprint"; + break; + + case EHoudiniEngineBakeOption::ToAsset: + Str = "Asset"; + break; + } + + return Str; +} + +FString +FHoudiniEngineEditor::GetStringFromPDGBakeTargetOption(EPDGBakeSelectionOption BakeOption) +{ + FString Str; + switch (BakeOption) + { + case EPDGBakeSelectionOption::All: + Str = "All Outputs"; + break; + + case EPDGBakeSelectionOption::SelectedNetwork: + Str = "Selected Network (All Outputs)"; + break; + + case EPDGBakeSelectionOption::SelectedNode: + Str = "Selected Node (All Outputs)"; + break; + } + + return Str; +} + +FString +FHoudiniEngineEditor::GetStringfromActorBakeOption(EHoudiniEngineActorBakeOption ActorBakeOption) +{ + FString Str; + switch(ActorBakeOption) + { + case EHoudiniEngineActorBakeOption::OneActorPerHDA: + Str = "One Actor Per HDA"; + break; + + case EHoudiniEngineActorBakeOption::OneActorPerComponent: + Str = "One Actor Per Component"; + break; + } + return Str; +} + + +EHoudiniEngineActorBakeOption +FHoudiniEngineEditor::StringToHoudiniEngineActorBakeOption(const FString& InString) +{ + if (InString == "One Actor Per HDA") + return EHoudiniEngineActorBakeOption::OneActorPerHDA; + if (InString == "One Actor Per Component") + return EHoudiniEngineActorBakeOption::OneActorPerComponent; + return EHoudiniEngineActorBakeOption::OneActorPerComponent;; +} + +FString +FHoudiniEngineEditor::GetStringFromPDGBakePackageReplaceModeOption(EPDGBakePackageReplaceModeOption InOption) +{ + FString Str; + switch (InOption) + { + case EPDGBakePackageReplaceModeOption::CreateNewAssets: + Str = "Create New Assets"; + break; + case EPDGBakePackageReplaceModeOption::ReplaceExistingAssets: + Str = "Replace Existing Assets"; + break; + } + + return Str; +} + +EHoudiniEngineBakeOption +FHoudiniEngineEditor::StringToHoudiniEngineBakeOption(const FString & InString) +{ + if (InString == "Actor") + return EHoudiniEngineBakeOption::ToActor; + + if (InString == "Blueprint") + return EHoudiniEngineBakeOption::ToBlueprint; + + if (InString == "Asset") + return EHoudiniEngineBakeOption::ToAsset; + + return EHoudiniEngineBakeOption::ToActor; +} + +EPDGBakeSelectionOption +FHoudiniEngineEditor::StringToPDGBakeSelectionOption(const FString& InString) +{ + if (InString == "All Outputs") + return EPDGBakeSelectionOption::All; + + if (InString == "Selected Network (All Outputs)") + return EPDGBakeSelectionOption::SelectedNetwork; + + if (InString == "Selected Node (All Outputs)") + return EPDGBakeSelectionOption::SelectedNode; + + return EPDGBakeSelectionOption::All; +} + +EPDGBakePackageReplaceModeOption +FHoudiniEngineEditor::StringToPDGBakePackageReplaceModeOption(const FString & InString) +{ + if (InString == "Create New Assets") + return EPDGBakePackageReplaceModeOption::CreateNewAssets; + + if (InString == "Replace Existing Assets") + return EPDGBakePackageReplaceModeOption::ReplaceExistingAssets; + + return EPDGBakePackageReplaceModeOption::ReplaceExistingAssets; +} + +EPackageReplaceMode +FHoudiniEngineEditor::PDGBakePackageReplaceModeToPackageReplaceMode(const EPDGBakePackageReplaceModeOption& InReplaceMode) +{ + EPackageReplaceMode Mode; + switch (InReplaceMode) + { + case EPDGBakePackageReplaceModeOption::CreateNewAssets: + Mode = EPackageReplaceMode::CreateNewAssets; + break; + case EPDGBakePackageReplaceModeOption::ReplaceExistingAssets: + Mode = EPackageReplaceMode::ReplaceExistingAssets; + break; + default: + { + Mode = FHoudiniPackageParams::GetDefaultReplaceMode(); + HOUDINI_LOG_WARNING(TEXT("Unsupported value for EPDGBakePackageReplaceModeOption %d, using " + "FHoudiniPackageParams::GetDefaultReplaceMode() for resulting EPackageReplaceMode %d"), + InReplaceMode, Mode); + } + } + + return Mode; +} + +bool +FHoudiniEngineEditor::HandleOnPreSave(UWorld* InWorld) +{ + // Refine current ProxyMeshes to Static Meshes + const bool bSelectedOnly = false; + const bool bSilent = false; + const bool bRefineAll = false; + const bool bOnPreSaveWorld = true; + //UWorld* const OnPreSaveWorld = InWorld; + const bool bOnPreBeginPIE = false; + + // Do the refinement + EHoudiniProxyRefineRequestResult RefineResult = FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshes(bSelectedOnly, bSilent, bRefineAll, bOnPreSaveWorld, InWorld, bOnPreBeginPIE); + + //Return true only if a refinement happened + if (RefineResult == EHoudiniProxyRefineRequestResult::Invalid || RefineResult == EHoudiniProxyRefineRequestResult::None) + return false; + + return true; +} + +void +FHoudiniEngineEditor::HandleOnBeginPIE() +{ + // If the Houdini Engine Session was connected and valid before PIE, + // we'll need to reconnect the Houdini session after PIE. + // Setup a delegate for that + if (FHoudiniEngine::Get().IsTicking()) + { + const bool bWasConnected = FHoudiniEngine::Get().GetSessionStatus() == EHoudiniSessionStatus::Connected; + if (bWasConnected) + { + EndPIEEditorDelegateHandle = FEditorDelegates::EndPIE.AddLambda([&, bWasConnected](const bool bEndPIEIsSimulating) + { + // If the Houdini session was previously connected, we need to reestablish the connection after PIE. + // We need to restart the current Houdini Engine Session if needed. + // This will reuse the previous session if it didnt shutdown, or start a new one if needed. + // (HARS shuts down when stopping the session, so we cant just reconnect when not using Session Sync) + if(FHoudiniEngine::Get().GetSessionStatus() != EHoudiniSessionStatus::Connected) + FHoudiniEngineCommands::RestartSession(); + + // If the session wasn't stopped - we just need to start ticking again + if (!FHoudiniEngine::Get().IsTicking()) + FHoudiniEngine::Get().StartTicking(true); + + FEditorDelegates::EndPIE.Remove(EndPIEEditorDelegateHandle); + }); + } + } + + // Refine ProxyMeshes to StaticMeshes for PIE + const bool bSelectedOnly = false; + const bool bSilent = false; + const bool bRefineAll = false; + const bool bOnPreSaveWorld = false; + UWorld* const OnPreSaveWorld = nullptr; + const bool bOnPreBeginPIE = true; + FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshes(bSelectedOnly, bSilent, bRefineAll, bOnPreSaveWorld, OnPreSaveWorld, bOnPreBeginPIE); +} + +void +FHoudiniEngineEditor::HandleOnDeleteActorsBegin() +{ + if (!GEditor) + return; + + TArray AssetActorsWithTempPDGOutput; + // Iterate over all selected actors + for(FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It) + { + AActor* SelectedActor = Cast(*It); + if (IsValid(SelectedActor)) + { + // If the class is a AHoudiniAssetActor check if it has temporary PDG outputs + AHoudiniAssetActor* AssetActor = Cast(SelectedActor); + if (IsValid(AssetActor)) + { + UHoudiniPDGAssetLink* AssetLink = AssetActor->GetPDGAssetLink(); + if (IsValid(AssetLink) && AssetLink->HasTemporaryOutputs()) + { + AssetActorsWithTempPDGOutput.Add(AssetActor); + } + } + } + } + + if (AssetActorsWithTempPDGOutput.Num() > 0) + { + const FText DialogTitle = LOCTEXT( + "PDGAssetLink_DeleteWithTemporaryOutputs_Title", + "Warning: PDG Asset Link(s) With Temporary Outputs"); + const EAppReturnType::Type Choice = FMessageDialog::Open( + EAppMsgType::YesNo, + EAppReturnType::No, + LOCTEXT( + "PDGAssetLink_DeleteWithTemporaryOutputs", + "One or more PDG Asset Links in the selection still have temporary outputs. Are you sure you want to " + "delete these PDG Asset Links and their actors?"), +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + DialogTitle); +#else + &DialogTitle); +#endif + + const bool bKeepAssetLinkActors = (Choice == EAppReturnType::No); + for (AHoudiniAssetActor* AssetActor : AssetActorsWithTempPDGOutput) + { + if (bKeepAssetLinkActors) + { + GEditor->SelectActor(AssetActor, false, false); + ActorsToReselectOnDeleteActorsEnd.Add(AssetActor); + } + } + } +} + +void +FHoudiniEngineEditor::HandleOnDeleteActorsEnd() +{ + if (!GEditor) + return; + + for (AActor* Actor : ActorsToReselectOnDeleteActorsEnd) + { + if (IsValid(Actor)) + GEditor->SelectActor(Actor, true, false); + } + GEditor->NoteSelectionChange(); + ActorsToReselectOnDeleteActorsEnd.Empty(); +} + + +TSharedRef +FHoudiniEngineEditor::OnSpawnNodeSyncTab(const FSpawnTabArgs& SpawnTabArgs) +{ + TSharedRef SpawnedTab = SNew(SDockTab) + .TabRole(ETabRole::NomadTab) + //.Icon(FHoudiniEngineStyle::Get()->GetBrush("HoudiniEngine.HoudiniEngineLogo")) + [ + SAssignNew(NodeSyncPanel, SHoudiniNodeSyncPanel) + .IsAssetEditor(false) + ]; + + SpawnedTab->SetTabIcon(FHoudiniEngineStyle::Get()->GetBrush("HoudiniEngine.HoudiniEngineLogo")); + + return SpawnedTab; +} + +TSharedRef FHoudiniEngineEditor::OnSpawnHoudiniToolsTab(const FSpawnTabArgs& SpawnTabArgs) +{ + TSharedRef SpawnedTab = SNew(SDockTab) + .TabRole(ETabRole::NomadTab) + //.Icon(FHoudiniEngineStyle::Get()->GetBrush("HoudiniEngine.HoudiniEngineLogo")) + [ + SNew(SHoudiniToolsPanel) + ]; + + SpawnedTab->SetTabIcon(FHoudiniEngineStyle::Get()->GetBrush("HoudiniEngine.HoudiniEngineLogo")); + + return SpawnedTab; +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineEditor.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "IHoudiniEngineEditor.h" +#include "HoudiniInputTypes.h" +#include "HoudiniEngineToolTypes.h" + +#include "CoreTypes.h" +#include "Templates/SharedPointer.h" +#include "Framework/Commands/UICommandList.h" +#include "Brushes/SlateDynamicImageBrush.h" +#include "IAssetTools.h" + + +class FExtender; +class IAssetTools; +class IAssetTypeActions; +class IComponentAssetBroker; +class FComponentVisualizer; +class FMenuBuilder; +class FMenuBarBuilder; +class FUICommandList; +class AActor; +class FHoudiniToolsEditor; + +struct IConsoleCommand; +struct FSlateDynamicImageBrush; + +enum class EHoudiniCurveType : int8; +enum class EHoudiniCurveMethod: int8; +enum class EHoudiniLandscapeOutputBakeType: uint8; +enum class EHoudiniEngineBakeOption : uint8; +enum class EPDGBakeSelectionOption : uint8; +enum class EPDGBakePackageReplaceModeOption : uint8; +enum class EPackageReplaceMode : int8; + + + +class HOUDINIENGINEEDITOR_API FHoudiniEngineEditor : public IHoudiniEngineEditor +{ + public: + FHoudiniEngineEditor(); + + // IModuleInterface methods. + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + // IHoudiniEngineEditor methods + virtual void RegisterComponentVisualizers() override; + virtual void UnregisterComponentVisualizers() override; + virtual void RegisterDetails() override; + virtual void UnregisterDetails() override; + virtual void RegisterAssetTypeActions() override; + virtual void UnregisterAssetTypeActions() override; + virtual void RegisterAssetBrokers() override; + virtual void UnregisterAssetBrokers() override; + virtual void RegisterActorFactories() override; + virtual void ExtendMenu() override; + virtual void RegisterForUndo() override; + virtual void UnregisterForUndo() override; + virtual void RegisterPlacementModeExtensions() override; + virtual void UnregisterPlacementModeExtensions() override; + virtual void RegisterEditorTabs() override; + virtual void UnRegisterEditorTabs() override; + + void RegisterLevelEditorTabs(TSharedPtr LevelTabManager); + void RegisterLevelEditorLayoutExtensions(FLayoutExtender& Extender); + + // Return singleton instance of Houdini Engine Editor, used internally. + static FHoudiniEngineEditor & Get(); + + // Return true if singleton instance has been created. + static bool IsInitialized(); + + // Returns the plugin's directory + static FString GetHoudiniEnginePluginDir(); + + // Initializes Widget resources + void InitializeWidgetResource(); + + // Menu action to pause cooking for all Houdini Assets + void PauseAssetCooking(); + + // Helper delegate used to determine if PauseAssetCooking can be executed. + bool CanPauseAssetCooking(); + + // Helper delegate used to get the current state of PauseAssetCooking. + bool IsAssetCookingPaused(); + + // Returns a pointer to the input choice types + TArray>* GetInputTypeChoiceLabels() { return &InputTypeChoiceLabels; }; + TArray>* GetPCGInputTypeChoiceLabels() { return &PCGInputTypeChoiceLabels; } + TArray>* GetBlueprintInputTypeChoiceLabels() { return &BlueprintInputTypeChoiceLabels; }; + TArray>* GetAssetEditorInputTypeChoiceLabels() { return &AssetEditorInputTypeChoiceLabels; } + + // Returns a pointer to the Houdini curve types + TArray>* GetHoudiniCurveTypeChoiceLabels() { return &HoudiniCurveTypeChoiceLabels; }; + + // Returns a pointer to the Houdini curve methods + TArray>* GetHoudiniCurveMethodChoiceLabels() { return &HoudiniCurveMethodChoiceLabels; }; + + // Returns a pointer to the Houdini curve methods + TArray>* GetHoudiniCurveBreakpointParameterizationChoiceLabels() { return &HoudiniCurveBreakpointParameterizationChoiceLabels; }; + + // Returns a pointer to the Houdini ramp parameter interpolation methods + TArray>* GetHoudiniParameterRampInterpolationMethodLabels() {return &HoudiniParameterRampInterpolationLabels;} + + // Returns a pointer to the Houdini curve output export types + TArray>* GetHoudiniCurveOutputExportTypeLabels() { return &HoudiniCurveOutputExportTypeLabels; }; + + TArray>* GetHoudiniLandscapeOutputBakeOptionsLabels() { return &HoudiniLandscapeOutputBakeOptionLabels; }; + + // Returns a pointer to the Houdini Engine PDG Bake Type labels + TArray>* GetHoudiniEnginePDGBakeTypeOptionsLabels() { return &HoudiniEnginePDGBakeTypeOptionLabels; }; + + // Returns a pointer to the Houdini Engine Bake Type labels + TArray>* GetHoudiniEngineBakeTypeOptionsLabels() { return &HoudiniEngineBakeTypeOptionLabels; }; + + // Returns a pointer to the Houdini Engine PDG Bake Target labels + TArray>* GetHoudiniEnginePDGBakeSelectionOptionsLabels() { return &HoudiniEnginePDGBakeSelectionOptionLabels; }; + + // Returns a pointer to the Houdini Engine PDG Bake Package Replace Mode labels + TArray>* GetHoudiniEnginePDGBakePackageReplaceModeOptionsLabels() { return &HoudiniEnginePDGBakePackageReplaceModeOptionLabels; }; + + // Returns a pointer to the bake actor labels + TArray>* GetHoudiniEngineBakeActorOptionsLabels() { return &HoudiniEngineBakeActorOptionsLabels; }; + + // Returns a shared Ptr to the Houdini logo + TSharedPtr GetHoudiniLogoBrush() const { return HoudiniLogoBrush; }; + TSharedPtr GetHoudiniEngineLogoBrush() const { return HoudiniEngineLogoBrush; }; + + // Functions Return a shared Ptr to the Houdini Engine UI Icon + TSharedPtr GetHoudiniEngineUIIconBrush() const { return HoudiniEngineUIIconBrush; } + TSharedPtr GetHoudiniEngineUIRebuildIconBrush() const { return HoudiniEngineUIRebuildIconBrush; } + TSharedPtr GetHoudiniEngineUIRecookIconBrush() const { return HoudiniEngineUIRecookIconBrush; } + TSharedPtr GetHoudiniEngineUIResetParametersIconBrush() const { return HoudiniEngineUIResetParametersIconBrush; } + + TSharedPtr GetHoudiniEngineUIBakeIconBrush() const { return HoudiniEngineUIBakeIconBrush; } + TSharedPtr GetHoudiniEngineUICookLogIconBrush() const { return HoudiniEngineUICookLogIconBrush; } + TSharedPtr GetHoudiniEngineUIAssetHelpIconBrush() const { return HoudiniEngineUIAssetHelpIconBrush; } + TSharedPtr GetHoudiniEngineUIPDGIconBrush() const { return HoudiniEngineUIPDGIconBrush; } + TSharedPtr GetHoudiniEngineUIPDGCancelIconBrush() const { return HoudiniEngineUIPDGCancelIconBrush; } + TSharedPtr GetHoudiniEngineUIPDGDirtyAllIconBrush() const { return HoudiniEngineUIPDGDirtyAllIconBrush; } + TSharedPtr GetHoudiniEngineUIPDGDirtyNodeIconBrush() const { return HoudiniEngineUIPDGDirtyNodeIconBrush; } + TSharedPtr GetHoudiniEngineUIPDGPauseIconBrush() const { return HoudiniEngineUIPDGPauseIconBrush; } + TSharedPtr GetHoudiniEngineUIPDGResetIconBrush() const { return HoudiniEngineUIPDGResetIconBrush; } + TSharedPtr GetHoudiniEngineUIPDGRefreshIconBrush() const { return HoudiniEngineUIPDGRefreshIconBrush; } + + // Returns a pointer to Unreal output curve types (for temporary) + TArray>* GetUnrealOutputCurveTypeLabels() { return &UnrealCurveOutputCurveTypeLabels; }; + + // returns string from Houdini Engine Bake Option + static FString GetStringFromHoudiniEngineBakeOption(EHoudiniEngineBakeOption BakeOption); + + // returns string from Houdini Engine PDG Bake Target Option + static FString GetStringFromPDGBakeTargetOption(EPDGBakeSelectionOption BakeOption); + + // returns string from PDG package replace mode option + static FString GetStringFromPDGBakePackageReplaceModeOption(EPDGBakePackageReplaceModeOption Option); + + // return the string for the actor bake option + static FString GetStringfromActorBakeOption(EHoudiniEngineActorBakeOption ActorBakeOption); + + + // Return HoudiniEngineBakeOption from FString + static EHoudiniEngineBakeOption StringToHoudiniEngineBakeOption(const FString & InString); + + // Return EPDGBakeSelectionOption from FString + static EPDGBakeSelectionOption StringToPDGBakeSelectionOption(const FString& InString); + + // Return EPDGBakePackageReplaceModeOption from FString + static EPDGBakePackageReplaceModeOption StringToPDGBakePackageReplaceModeOption(const FString & InString); + + // Return EHoudiniEngineActorBakeOption from FString + static EHoudiniEngineActorBakeOption StringToHoudiniEngineActorBakeOption(const FString& InString); + + // Convert EPDGBakePackageReplaceModeOption to EPackageReplaceMode + // TODO: perhaps EPackageReplaceMode can be moved to HoudiniEngineRuntime to avoid having both + // TODO: EPDGBakePackageReplaceModeOption and EPackageReplaceMode? + EPackageReplaceMode PDGBakePackageReplaceModeToPackageReplaceMode(const EPDGBakePackageReplaceModeOption& InReplaceMode); + + // Get the reference of the radio button folder circle point arrays reference + TArray & GetHoudiniParameterRadioButtonPointsOuter() { return HoudiniParameterRadioButtonPointsOuter; }; + TArray & GetHoudiniParameterRadioButtonPointsInner() { return HoudiniParameterRadioButtonPointsInner; }; + + // Gets the PostSaveWorldOnceHandle + FDelegateHandle& GetOnPostSaveWorldOnceHandle() { return PostSaveWorldOnceHandle; } + + // Gets the PostSavePackageOnceHandle + FDelegateHandle& GetOnPostSavePackageOnceHandle() { return PostSavePackageOnceHandle; } + + //void ModulesChangedCallback(FName ModuleName, EModuleChangeReason ReasonForChange); + //FDelegateHandle ModulesChangedHandle; + + // TODO: FHoudiniToolsEditor should be converted to an editor subsystem + FHoudiniToolsEditor& GetHoudiniTools() const { return *HoudiniToolsPtr; } + + TSharedPtr GetNodeSyncPanel() { return NodeSyncPanel; } + + // Get the List of UI commands used by the various menus + TSharedPtr GetHoudiniEngineCommands() { return HEngineCommands; }; + + // Add menu extension for our module. + static void AddHoudiniFileMenuExtension(FMenuBuilder& MenuBuilder); + + // Add the Houdini Engine editor menu + static void AddHoudiniEditorMenu(FMenuBarBuilder& MenuBarBuilder); + + // Add menu extension for our module. + static void AddHoudiniMainMenuExtension(FMenuBuilder& MenuBuilder); + + protected: + + // Binds the commands used by the menus + void BindMenuCommands(); + + // Register AssetType action. + void RegisterAssetTypeAction(IAssetTools& AssetTools, TSharedRef< IAssetTypeActions > Action); + + // Adds the custom Houdini Engine commands to the world outliner context menu + void AddLevelViewportMenuExtender(); + + //Extend the content browser Context Menu + void ExtendContextMenu(); + + // Send CB selection to Houdini + void SendToHoudini_CB(TArray SelectedAssets); + + // Send World selection to Houdini + void SendToHoudini_World(); + + FDelegateHandle ContentBrowserExtenderDelegateHandle; + + // Removes the custom Houdini Engine commands to the world outliner context menu + void RemoveLevelViewportMenuExtender(); + + // Returns all the custom Houdini Engine commands for the world outliner context menu + TSharedRef GetLevelViewportContextMenuExtender( + const TSharedRef CommandList, const TArray InActors); + + // Register all console commands provided by this module + void RegisterConsoleCommands(); + + // Unregister all registered console commands provided by this module + void UnregisterConsoleCommands(); + + // Register for any FEditorDelegates that we are interested in, such as + // PreSaveWorld and PreBeginPIE, for HoudiniStaticMesh -> UStaticMesh builds + void RegisterEditorDelegates(); + + // Deregister editor delegates + void UnregisterEditorDelegates(); + + // Process the OnDeleteActorsBegin call received from FEditorDelegates. + // Check if any AHoudiniAssetActors with PDG links are selected for deletion. If so, + // check if these still have temporary outputs and give the user to option to skip + // deleting the ones with temporary output. + void HandleOnDeleteActorsBegin(); + + // Re-select AHoudiniAssetActors that were deselected (to avoid deletion) by HandleOnDeleteActorsBegin + void HandleOnDeleteActorsEnd(); + + // Handle pre-save events + // Either PreSaveWorld/PreSavePackage + // This allows proper refinement of Proxies to Static Mesh when saving + bool HandleOnPreSave(UWorld* InWorld); + + // Handle Begin PlayInEditor event + // This allows proper refinement of Proxies to Static Mesh + void HandleOnBeginPIE(); + + // For the Houdini category sections in the UI + void RegisterSectionMappings(); + void UnregisterSectionMappings(); + + private: + + // Singleton instance of Houdini Engine Editor. + static FHoudiniEngineEditor * HoudiniEngineEditorInstance; + + // AssetType actions associated with Houdini asset. + TArray> AssetTypeActions; + + // Broker associated with Houdini asset. + TSharedPtr HoudiniAssetBroker; + + // Widget resources: Input Type combo box labels + TArray> InputTypeChoiceLabels; + TArray> BlueprintInputTypeChoiceLabels; + TArray> PCGInputTypeChoiceLabels; + TArray> AssetEditorInputTypeChoiceLabels; + + // Widget resources: Houdini Curve Type combo box labels + TArray> HoudiniCurveTypeChoiceLabels; + + // Widget resources: Houdini Curve Method combo box labels + TArray> HoudiniCurveMethodChoiceLabels; + + // Widget resources: Houdini Curve Method combo box labels + TArray> HoudiniCurveBreakpointParameterizationChoiceLabels; + + // Widget resources: Houdini Ramp Interpolation method combo box labels + TArray> HoudiniParameterRampInterpolationLabels; + + // Widget resources: Houdini Curve Output type labels + TArray> HoudiniCurveOutputExportTypeLabels; + + // Widget resources: Unreal Curve type labels (for temporary, we need to figure out a way to access the output curve's info later) + TArray> UnrealCurveOutputCurveTypeLabels; + + // Widget resources: Landscape output Bake type labels + TArray> HoudiniLandscapeOutputBakeOptionLabels; + + // Widget resources: PDG Bake type labels + TArray> HoudiniEnginePDGBakeTypeOptionLabels; + + // Widget resources: Bake type labels + TArray> HoudiniEngineBakeTypeOptionLabels; + + // Widget resources: PDG Bake target labels + TArray> HoudiniEnginePDGBakeSelectionOptionLabels; + + // Widget resources: PDG Bake package replace mode labels + TArray> HoudiniEnginePDGBakePackageReplaceModeOptionLabels; + + // Bake Actor options labels + TArray> HoudiniEngineBakeActorOptionsLabels; + + // List of UI commands used by the various menus + TSharedPtr HEngineCommands; + + // Houdini logo brush. + TSharedPtr HoudiniLogoBrush; + // Houdini Engine logo brush + TSharedPtr HoudiniEngineLogoBrush; + + // houdini Engine UI Brushes + TSharedPtr HoudiniEngineUIIconBrush; + TSharedPtr HoudiniEngineUIRebuildIconBrush; + TSharedPtr HoudiniEngineUIRecookIconBrush; + TSharedPtr HoudiniEngineUIResetParametersIconBrush; + + TSharedPtr HoudiniEngineUIBakeIconBrush; + TSharedPtr HoudiniEngineUICookLogIconBrush; + TSharedPtr HoudiniEngineUIAssetHelpIconBrush; + TSharedPtr HoudiniEngineUIPDGIconBrush; + TSharedPtr HoudiniEngineUIPDGCancelIconBrush; + TSharedPtr HoudiniEngineUIPDGDirtyAllIconBrush; + TSharedPtr HoudiniEngineUIPDGDirtyNodeIconBrush; + TSharedPtr HoudiniEngineUIPDGPauseIconBrush; + TSharedPtr HoudiniEngineUIPDGResetIconBrush; + TSharedPtr HoudiniEngineUIPDGRefreshIconBrush; + + // The extender to pass to the level editor to extend it's File menu. + TSharedPtr MainMenuExtender; + + // The extender to pass to the level editor to extend it's Main menu. + //TSharedPtr FileMenuExtender; + + // DelegateHandle for the viewport's context menu extender + FDelegateHandle LevelViewportExtenderHandle; + + // SplineComponentVisualizer + TSharedPtr SplineComponentVisualizer; + + TSharedPtr HandleComponentVisualizer; + + // Array of HoudiniEngine console commands + TArray ConsoleCommands; + + // Delegate handle for the PreSaveWorld editor delegate + FDelegateHandle PreSaveWorldEditorDelegateHandle; + + // Delegate handle for the PreSavePackage editor delegate + FDelegateHandle PreSavePackageEditorDelegateHandle; + + // Delegate handle for the PostSaveWorld editor delegate: this + // is bound on PreSaveWorld with specific captures and then unbound + // by itself + FDelegateHandle PostSaveWorldOnceHandle; + + // Delegate handle for the PostSavePackage delegate: + // this is bound on PreSavePackage with specific captures and then unbound by itself + FDelegateHandle PostSavePackageOnceHandle; + + // Delegate handle for the PreBeginPIE editor delegate + FDelegateHandle PreBeginPIEEditorDelegateHandle; + + // Delegate handle for the EndPIE editor delegate + FDelegateHandle EndPIEEditorDelegateHandle; + + // Delegate handle for OnDeleteActorsBegin + FDelegateHandle OnDeleteActorsBegin; + + // Delegate handle for OnDeleteActorsEnd + FDelegateHandle OnDeleteActorsEnd; + + // Delegate handle for LevelEditorModule::OnRegisterTabs + FDelegateHandle OnLevelEditorRegisterTabsHandle; + + /* + * + // Delegate handle for AssetTools::IsNameAllowed + FIsNameAllowed OnIsNameAllowed; + */ + + // List of actors that HandleOnDeleteActorsBegin marked to _not_ be deleted. This + // is used to re-select these actors in HandleOnDeleteActorsEnd. + TArray ActorsToReselectOnDeleteActorsEnd; + + // Cache the points of radio button folder circle points to avoid huge amount of repeat computation. + // (Computing points are time consuming since it uses trigonometric functions) + TArray HoudiniParameterRadioButtonPointsOuter; + TArray HoudiniParameterRadioButtonPointsInner; + + //NodeSync Tab + TSharedRef OnSpawnNodeSyncTab(const class FSpawnTabArgs& SpawnTabArgs); + + //HoudiniTools Tab + TSharedRef OnSpawnHoudiniToolsTab(const class FSpawnTabArgs& SpawnTabArgs); + + TSharedPtr NodeSyncPanel; + + // Houdini Tools utility. + TSharedPtr HoudiniToolsPtr; + + +}; +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineEditorLocalization.cpp`: + +```cpp +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineEditorLocalization.h`: + +```h +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineEditorPrivatePCH.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#define HOUDINI_ENGINE_EDITOR + +#include "HoudiniEngineRuntimePrivatePCH.h" + +#include "Editor.h" +#include "LevelEditor.h" +#include "Runtime/Launch/Resources/Version.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 + #include "Subsystems/EditorAssetSubsystem.h" +#else + #include "EditorAssetLibrary.h" +#endif + +// Details panel desired sizes. +#define HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH 270 +#define HAPI_UNREAL_DESIRED_ROW_FULL_WIDGET_WIDTH 310 +#define HAPI_UNREAL_DESIRED_SETTINGS_ROW_VALUE_WIDGET_WIDTH 350 +#define HAPI_UNREAL_DESIRED_SETTINGS_ROW_FULL_WIDGET_WIDTH 400 +#define HAPI_UNREAL_PADDING_HORIZONTAL_JOIN 10 + + // URL used for bug reporting. +#define HAPI_UNREAL_BUG_REPORT_URL TEXT("https://www.sidefx.com/bugs/submit/") +#define HAPI_UNREAL_ONLINE_DOC_URL TEXT("https://www.sidefx.com/docs/unreal/") +#define HAPI_UNREAL_ONLINE_FORUM_URL TEXT("https://www.sidefx.com/forum/51/") + +// URL used for the content examples +#define HAPI_UNREAL_CONTENT_EXAMPLES_URL TEXT("https://github.com/sideeffects/HoudiniEngineForUnreal-ContentExamples") +#define HAPI_UNREAL_CONTENT_EXAMPLES__RELEASE_URL TEXT("https://github.com/sideeffects/HoudiniEngineForUnreal-ContentExamples/releases") + + +// UI Category Names + +#define HOUDINI_ENGINE_EDITOR_CATEGORY_MAIN "HoudiniEngine"; +#define HOUDINI_ENGINE_EDITOR_CATEGORY_PDG "HoudiniPDGAssetLink"; +#define HOUDINI_ENGINE_EDITOR_CATEGORY_PARAMS "HoudiniParameters"; +#define HOUDINI_ENGINE_EDITOR_CATEGORY_HANDLES "HoudiniHandles"; +#define HOUDINI_ENGINE_EDITOR_CATEGORY_INPUTS "HoudiniInputs"; +#define HOUDINI_ENGINE_EDITOR_CATEGORY_OUTPUTS "HoudiniOutputs"; +#define HOUDINI_ENGINE_EDITOR_CATEGORY_NODESYNC "HoudiniNodeSync"; +#define HOUDINI_ENGINE_EDITOR_CATEGORY_HDA "HoudiniAsset"; +//#define HOUDINI_ENGINE_EDITOR_CATEGORY_PROXY "HoudiniProxyMeshGeneration"; +#define HOUDINI_ENGINE_EDITOR_CATEGORY_MESHGEN "HoudiniMeshGeneration"; + +// +// Parameter UI constants +// + +// Constants for parameter UI indentation + +// Change this constant to change the overall indentation width +#define INDENTATION_UNIT_WIDTH 20.0f +// Do not change this width unless the folder triangle arrow is customized. +#define NON_FOLDER_OFFSET_WIDTH 22.0f + + +// Houdini parameter UI row margin heights +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_BUTTON 8.80f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_BUTTONSTRIP 2.15f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_COLOR 5.50f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_COLORRAMP 57.20f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_COLORRAMP_INSTANCE 12.90f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILE 6.90f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILEDIR 6.90f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILEGEO 6.90f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILEIMAGE 6.90f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOAT 6.00f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOAT_VEC3 7.55f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOAT_INSTANCE 10.40f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOATRAMP 51.70f +#define Houdini_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOATRAMP_INSTANCE 12.90f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FOLDER 2.00f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FOLDERLIST 2.05f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT 0.0f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_GEOMETRY 62.50f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_GEOMETRY_INSTANCE 49.45f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_GEOMETRY_INSTANCE_TRANSFORM 36.00f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_CURVE 41.45f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_CURVE_INSTANCE 68.40f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_ASSET 177.55f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_LANDSCAPE 235.00f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_LANDSCAPE_MESH 275.00f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_WORLD 219.35f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_SKELETAL 18.00f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_GEOMETRYCOLLECTION 18.00f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INT 6.00f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INT_VEC3 7.55f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INT_INSTANCE 10.40f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INTCHOICE 7.80f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_LABEL 4.40f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_MULTIPARM 6.00f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_SEPARATOR 1.95f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRING 6.15f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRINGASSETREF 6.15f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRINGCHOICE 7.80f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRING_INSTANCE 10.30f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_TOGGLE 5.60f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INVALID 0.0f + +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_BUTTON_MULTIPARMHEADER 4.55f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_BUTTONSTRIP_MULTIPARMHEADER 2.45f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_COLOR_MULTIPARMHEADER 2.15f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_COLORRAMP_MULTIPARMHEADER 57.20f +#define Houdini_PARAMETER_UI_ROW_MARGIN_HEIGHT_COLORRAMP_INSTANCE_MULTIPARMHEADER 12.90f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILE_MULTIPARMHEADER 2.68f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILEDIR_MULTIPARMHEADER 2.68f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILEGEO_MULTIPARMHEADER 2.68f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILEIMAGE_MULTIPARMHEADER 2.68f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOAT_MULTIPARMHEADER 2.60f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOAT_VEC3_MULTIPARMHEADER 3.75f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOAT_INSTANCE_MULTIPARMHEADER 10.40f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOATRAMP_MULTIPARMHEADER 51.70f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOATRAMP_INSTANCE_MULTIPARMHEADER 12.90f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FOLDER_MULTIPARMHEADER 1.00f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FOLDERLIST_MULTIPARMHEADER 0.0f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_MULTIPARMHEADER 0.0f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_GEOMETRY_MULTIPARMHEADER 58.50f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_GEOMETRY_INSTANCE_MULTIPARMHEADER 49.45f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_GEOMETRY_INSTANCE_TRANSFORM_MULTIPARMHEADER 40.00f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_CURVE_MULTIPARMHEADER 37.45f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_CURVE_INSTANCE_MULTIPARMHEADER 68.40f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_ASSET_MULTIPARMHEADER 173.55f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_LANDSCAPE_MULTIPARMHEADER 231.00f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_LANDSCAPE_MESH_MULTIPARMHEADER 266.95f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_WORLD_MULTIPARMHEADER 215.05f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_SKELETAL_MULTIPARMHEADER 14. +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_GEOMETRYCOLLECTION_MULTIPARMHEADER 14.00f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INT_MULTIPARMHEADER 2.57f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INT_VEC3_MULTIPARMHEADER 4.12f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INT_INSTANCE_MULTIPARMHEADER 10.40f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INTCHOICE_MULTIPARMHEADER 4.30f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_LABEL_MULTIPARMHEADER 0.75f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_MULTIPARM_MULTIPARMHEADER 0.0f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_SEPARATOR_MULTIPARMHEADER 1.95f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRING_MULTIPARMHEADER 2.50f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRINGASSETREF_MULTIPARMHEADER 2.50f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRINGCHOICE_MULTIPARMHEADER 4.30f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRING_INSTANCE_MULTIPARMHEADER 10.30f +#define HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_TOGGLE_MULTIPARMHEADER 2.30f + +// Radio button UI constants +#define HOUDINI_RADIO_BUTTON_CIRCLE_SAMPLES_NUM_OUTER 18 +#define HOUDINI_RADIO_BUTTON_CIRCLE_SAMPLES_NUM_INNER 8 +#define HOUDINI_RADIO_BUTTON_CIRCLE_RADIUS_OUTER 4.5f +#define HOUDINI_RADIO_BUTTON_CIRCLE_RADIUS_INNER 1.0f +#define HOUDINI_RADIO_BUTTON_CIRCLE_CENTER_X 7.0f +#define HOUDINI_RADIO_BUTTON_CIRCLE_CENTER_Y 13.2f + + +// Backward compatibility with 5.0 +static const ISlateStyle& _GetEditorStyle() +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + return FAppStyle::Get(); +#else + return FEditorStyle::Get(); +#endif +}; + +// Wrapper to manage code compatibility across multiple UE versions +static bool _DoesAssetExist(const FString& AssetPath) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 + UEditorAssetSubsystem* EditorAssetSubsystem = GEditor->GetEditorSubsystem(); + return EditorAssetSubsystem->DoesAssetExist(AssetPath); +#else + return UEditorAssetLibrary::DoesAssetExist(AssetPath); +#endif +}; + +// Wrapper to manage code compatibility across multiple UE versions +static const FSlateBrush* _GetBrush(FName PropertyName) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 + return FAppStyle::GetBrush(PropertyName); +#else + return FEditorStyle::GetBrush(PropertyName); +#endif +} + +// Wrapper to manage code compatibility across multiple UE versions +static const FMargin _GetMargin(FName PropertyName) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 + return FAppStyle::GetMargin(PropertyName); +#else + return FEditorStyle::GetMargin(PropertyName); +#endif +} + +// Wrapper to manage code compatibility across multiple UE versions +static const float _GetFloat(FName PropertyName) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 + return FAppStyle::GetFloat(PropertyName); +#else + return FEditorStyle::GetFloat(PropertyName); +#endif +} + +static FName _GetPlacementBrowserName() +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >=2 + return LevelEditorTabIds::PlacementBrowser; +#else + return FName(TEXT("PlacementBrowser")); +#endif +} + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineEditorUtils.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineEditorUtils.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniOutput.h" +#include "HoudiniRuntimeSettings.h" +#include "HoudiniToolsEditor.h" + +#include "ContentBrowserModule.h" +#include "IContentBrowserSingleton.h" +#include "Engine/Selection.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "EditorViewportClient.h" +#include "ActorFactories/ActorFactory.h" +#include "FileHelpers.h" +#include "PropertyPathHelpers.h" +#include "Components/SceneComponent.h" +#include "UObject/UObjectIterator.h" +#include "Materials/MaterialInterface.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +int32 +FHoudiniEngineEditorUtils::GetContentBrowserSelection(TArray< UObject* >& ContentBrowserSelection) +{ + ContentBrowserSelection.Empty(); + + // Get the current Content browser selection + FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked< FContentBrowserModule >("ContentBrowser"); + TArray SelectedAssets; + ContentBrowserModule.Get().GetSelectedAssets(SelectedAssets); + + for (int32 n = 0; n < SelectedAssets.Num(); n++) + { + // Get the current object + UObject * Object = SelectedAssets[n].GetAsset(); + if (!IsValid(Object)) + continue; + + bool bIncludeObject = false; + + if (Object->GetClass() == UStaticMesh::StaticClass()) + { + // Include if this is a static mesh + ContentBrowserSelection.Add(Object); + continue; + } + + // Only static meshes are supported + if (Object->GetClass() == UBlueprint::StaticClass()) + { + const UBlueprint* BlueprintObj = Cast(Object); + if (IsValid(BlueprintObj) && BlueprintObj->BlueprintType == EBlueprintType::BPTYPE_Normal) + { + // Include if this is a "normal" blueprint. + ContentBrowserSelection.Add(Object); + continue; + } + } + } + + return ContentBrowserSelection.Num(); +} + +int32 +FHoudiniEngineEditorUtils::GetWorldSelection(TArray& WorldSelection, bool bHoudiniAssetActorsOnly) +{ + WorldSelection.Empty(); + + // Get the current editor selection + if (GEditor) + { + USelection* SelectedActors = GEditor->GetSelectedActors(); + if (IsValid(SelectedActors)) + { + for (FSelectionIterator It(*SelectedActors); It; ++It) + { + AActor * Actor = Cast(*It); + if (!IsValid(Actor)) + continue; + + // Ignore the SkySphere? + FString ClassName = Actor->GetClass() ? Actor->GetClass()->GetName() : FString(); + if (ClassName == TEXT("BP_Sky_Sphere_C")) + continue; + + // We're normally only selecting actors with StaticMeshComponents and SplineComponents + // Heightfields? Filter here or later? also allow HoudiniAssets? + WorldSelection.Add(Actor); + } + } + } + + // If we only want Houdini Actors... + if (bHoudiniAssetActorsOnly) + { + // ... remove all but them + for (int32 Idx = WorldSelection.Num() - 1; Idx >= 0; Idx--) + { + AHoudiniAssetActor * HoudiniAssetActor = Cast(WorldSelection[Idx]); + if (!IsValid(HoudiniAssetActor)) + WorldSelection.RemoveAt(Idx); + } + } + + return WorldSelection.Num(); +} + + +FString +FHoudiniEngineEditorUtils::HoudiniCurveTypeToString(const EHoudiniCurveType& HoudiniCurveType) +{ + FString HoudiniCurveTypeStr; + switch (HoudiniCurveType) + { + case EHoudiniCurveType::Invalid: + { + HoudiniCurveTypeStr = TEXT("Invalid"); + } + break; + + case EHoudiniCurveType::Polygon: + { + HoudiniCurveTypeStr = TEXT("Polygon"); + } + break; + + case EHoudiniCurveType::Nurbs: + { + HoudiniCurveTypeStr = TEXT("Nurbs"); + } + break; + + case EHoudiniCurveType::Bezier: + { + HoudiniCurveTypeStr = TEXT("Bezier"); + } + break; + + case EHoudiniCurveType::Points: + { + HoudiniCurveTypeStr = TEXT("Points"); + } + break; + } + + return HoudiniCurveTypeStr; +} + +FString +FHoudiniEngineEditorUtils::HoudiniCurveMethodToString(const EHoudiniCurveMethod& CurveMethod) +{ + FString HoudiniCurveMethodStr; + switch (CurveMethod) + { + case EHoudiniCurveMethod::Invalid: + { + HoudiniCurveMethodStr = TEXT("Invalid"); + } + break; + case EHoudiniCurveMethod::CVs: + { + HoudiniCurveMethodStr = TEXT("CVs"); + } + break; + case EHoudiniCurveMethod::Breakpoints: + { + HoudiniCurveMethodStr = TEXT("Breakpoints"); + } + break; + case EHoudiniCurveMethod::Freehand: + { + HoudiniCurveMethodStr = TEXT("Freehand"); + } + break; + } + + return HoudiniCurveMethodStr; +} + +FString FHoudiniEngineEditorUtils::HoudiniCurveBreakpointParameterizationToString( + const EHoudiniCurveBreakpointParameterization& HoudiniCurveBreakpointParameterization) +{ + FString HoudiniCurveBreakpointParameterizationStr; + switch (HoudiniCurveBreakpointParameterization) + { + case EHoudiniCurveBreakpointParameterization::Invalid: + { + HoudiniCurveBreakpointParameterizationStr = TEXT("Invalid"); + } + break; + case EHoudiniCurveBreakpointParameterization::Uniform: + { + HoudiniCurveBreakpointParameterizationStr = TEXT("Uniform"); + } + break; + case EHoudiniCurveBreakpointParameterization::Chord: + { + HoudiniCurveBreakpointParameterizationStr = TEXT("Chord"); + } + break; + case EHoudiniCurveBreakpointParameterization::Centripetal: + { + HoudiniCurveBreakpointParameterizationStr = TEXT("Centripetal"); + } + break; + } + + return HoudiniCurveBreakpointParameterizationStr; +} + + +FString +FHoudiniEngineEditorUtils::HoudiniCurveMethodToUnrealCurveTypeString(const EHoudiniCurveType& HoudiniCurveType) +{ + // Temporary, we need to figure out a way to access the output curve's info later + FString UnrealCurveType; + switch (HoudiniCurveType) + { + case EHoudiniCurveType::Polygon: + case EHoudiniCurveType::Points: + { + UnrealCurveType = TEXT("Linear"); + } + break; + + case EHoudiniCurveType::Nurbs: + case EHoudiniCurveType::Bezier: + { + UnrealCurveType = TEXT("Curve"); + } + break; + } + + return UnrealCurveType; +} + +FString +FHoudiniEngineEditorUtils::HoudiniLandscapeOutputBakeTypeToString(const EHoudiniLandscapeOutputBakeType& LandscapeBakeType) +{ + FString LandscapeBakeTypeString; + switch (LandscapeBakeType) + { + case EHoudiniLandscapeOutputBakeType::Detachment: + LandscapeBakeTypeString = "To Current Level"; + break; + + case EHoudiniLandscapeOutputBakeType::BakeToImage: + LandscapeBakeTypeString = "To Image"; + break; + + case EHoudiniLandscapeOutputBakeType::BakeToWorld: + LandscapeBakeTypeString = "To New World"; + break; + + } + + return LandscapeBakeTypeString; +} + + +FTransform +FHoudiniEngineEditorUtils::GetDefaulAssetSpawnTransform() +{ + FTransform SpawnTransform = FTransform::Identity; + + // Get the editor viewport LookAt position to spawn the new objects + if (GEditor && GEditor->GetActiveViewport()) + { + FEditorViewportClient* ViewportClient = (FEditorViewportClient*)GEditor->GetActiveViewport()->GetClient(); + if (ViewportClient) + { + // We need to toggle the orbit camera on to get the proper LookAtLocation to spawn our asset + ViewportClient->ToggleOrbitCamera(true); + SpawnTransform.SetLocation(ViewportClient->GetLookAtLocation()); + ViewportClient->ToggleOrbitCamera(false); + } + } + + return SpawnTransform; +} + +FTransform +FHoudiniEngineEditorUtils::GetMeanWorldSelectionTransform() +{ + FTransform SpawnTransform = GetDefaulAssetSpawnTransform(); + + if (GEditor && (GEditor->GetSelectedActorCount() > 0)) + { + // Get the current Level Editor Selection + USelection* SelectedActors = GEditor->GetSelectedActors(); + + int NumAppliedTransform = 0; + for (FSelectionIterator It(*SelectedActors); It; ++It) + { + AActor * Actor = Cast< AActor >(*It); + if (!Actor) + continue; + + // Just Ignore the SkySphere... + FString ClassName = Actor->GetClass() ? Actor->GetClass()->GetName() : FString(); + if (ClassName == TEXT("BP_Sky_Sphere_C")) + continue; + + FTransform CurrentTransform = Actor->GetTransform(); + + ALandscapeProxy* Landscape = Cast< ALandscapeProxy >(Actor); + if (Landscape) + { + // We need to offset Landscape's transform in X/Y to center them properly + FVector Origin, Extent; + Actor->GetActorBounds(false, Origin, Extent); + + // Use the origin's XY Position + FVector Location = CurrentTransform.GetLocation(); + Location.X = Origin.X; + Location.Y = Origin.Y; + CurrentTransform.SetLocation(Location); + } + + // Accumulate all the actor transforms... + if (NumAppliedTransform == 0) + SpawnTransform = CurrentTransform; + else + SpawnTransform.Accumulate(CurrentTransform); + + NumAppliedTransform++; + } + + if (NumAppliedTransform > 0) + { + // "Mean" all the accumulated Transform + SpawnTransform.SetScale3D(FVector::OneVector); + SpawnTransform.NormalizeRotation(); + + if (NumAppliedTransform > 1) + SpawnTransform.SetLocation(SpawnTransform.GetLocation() / (float)NumAppliedTransform); + } + } + + return SpawnTransform; +} + +void +FHoudiniEngineEditorUtils::InstantiateHoudiniAsset( + UHoudiniAsset* InHoudiniAsset, + const EHoudiniToolType& InType, + const EHoudiniToolSelectionType& InSelectionType, + UHoudiniPreset* InPreset) +{ + if (!InHoudiniAsset) + return; + + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + + // Load the asset + UObject* AssetObj = Cast(InHoudiniAsset); + if (!AssetObj) + return; + + // Get the asset Factory + UActorFactory* Factory = GEditor->FindActorFactoryForActorClass(AHoudiniAssetActor::StaticClass()); + if (!Factory) + return; + + // Get the current Level Editor Selection + TArray WorldSelection; + int32 WorldSelectionCount = FHoudiniEngineEditorUtils::GetWorldSelection(WorldSelection); + + // Get the current Content browser selection + TArray ContentBrowserSelection; + int32 ContentBrowserSelectionCount = FHoudiniEngineEditorUtils::GetContentBrowserSelection(ContentBrowserSelection); + + // By default, Content browser selection has a priority over the world selection + bool UseCBSelection = ContentBrowserSelectionCount > 0; + if (InSelectionType == EHoudiniToolSelectionType::HTOOL_SELECTION_CB_ONLY) + UseCBSelection = true; + else if (InSelectionType == EHoudiniToolSelectionType::HTOOL_SELECTION_WORLD_ONLY) + UseCBSelection = false; + + // Modify the created actor's position from the current editor world selection + FTransform SpawnTransform = GetDefaulAssetSpawnTransform(); + if (WorldSelectionCount > 0) + { + // Get the "mean" transform of all the selected actors + SpawnTransform = GetMeanWorldSelectionTransform(); + } + + // If the current tool is a batch one, we'll need to create multiple instances of the HDA + if (InType == EHoudiniToolType::HTOOLTYPE_OPERATOR_BATCH) + { + // Unselect the current selection to select the created actor after + if (GEditor) + GEditor->SelectNone(true, true, false); + + // An instance of the asset will be created for each selected object + for (int32 SelecIndex = 0; SelecIndex < (UseCBSelection ? ContentBrowserSelectionCount : WorldSelectionCount); SelecIndex++) + { + // Get the current object + UObject* CurrentSelectedObject = nullptr; + if (UseCBSelection && ContentBrowserSelection.IsValidIndex(SelecIndex)) + CurrentSelectedObject = ContentBrowserSelection[SelecIndex]; + + if (!UseCBSelection && WorldSelection.IsValidIndex(SelecIndex)) + CurrentSelectedObject = WorldSelection[SelecIndex]; + + if (!CurrentSelectedObject) + continue; + + // If it's an actor, use its Transform to spawn the HDA + AActor* CurrentSelectedActor = Cast(CurrentSelectedObject); + if (CurrentSelectedActor) + SpawnTransform = CurrentSelectedActor->GetTransform(); + else + SpawnTransform = GetDefaulAssetSpawnTransform(); + + // Create the actor for the HDA + AActor* CreatedActor = Factory->CreateActor(AssetObj, GEditor->GetEditorWorldContext().World()->GetCurrentLevel(), SpawnTransform); + if (!CreatedActor) + continue; + + // Get the HoudiniAssetActor / HoudiniAssetComponent we just created + AHoudiniAssetActor* HoudiniAssetActor = (AHoudiniAssetActor*)CreatedActor; + if (!HoudiniAssetActor) + continue; + + UHoudiniCookable* HoudiniCookable = HoudiniAssetActor->GetHoudiniCookable(); + if (!HoudiniCookable) + continue; + + if (IsValid(InPreset)) + { + HoudiniCookable->QueuePreCookCallback([InPreset](UHoudiniCookable* HC) + { + // First apply the preset when we reach the PreCook phase. + if (IsValid(InPreset)) + { + if(HC) + FHoudiniToolsEditor::ApplyPresetToHoudiniCookable(InPreset, HC); + } + }); + } + + // Create and set the input preset for this HDA and selected Object + TMap InputPreset; + InputPreset.Add(CurrentSelectedObject, 0); + HoudiniCookable->QueuePreCookCallback([InputPreset](UHoudiniCookable* HC) + { + // Apply the inputs once the HDA has reached its PreCookCallback + FHoudiniToolsEditor::ApplyObjectsAsHoudiniAssetInputs(InputPreset, HC); + }); + + // Select the Actor we just created + if (GEditor && GEditor->CanSelectActor(CreatedActor, true, false)) + GEditor->SelectActor(CreatedActor, true, true, true); + } + } + else + { + // We only need to create a single instance of the asset, regarding of the selection + AActor* CreatedActor = Factory->CreateActor(AssetObj, GEditor->GetEditorWorldContext().World()->GetCurrentLevel(), SpawnTransform); + if (!CreatedActor) + return; + + // Generator tools don't need to preset their input + if (InType != EHoudiniToolType::HTOOLTYPE_GENERATOR) + { + TMap InputPresets; + AHoudiniAssetActor* HoudiniAssetActor = (AHoudiniAssetActor*)CreatedActor; + UHoudiniCookable* HoudiniCookable = HoudiniAssetActor ? HoudiniAssetActor->GetHoudiniCookable() : nullptr; + if (HoudiniCookable) + { + // Build the preset map + int InputIndex = 0; + for (auto CurrentObject : (UseCBSelection ? ContentBrowserSelection : WorldSelection)) + { + if (!CurrentObject) + continue; + + if (InType == EHoudiniToolType::HTOOLTYPE_OPERATOR_MULTI) + { + // The selection will be applied individually to multiple inputs + // (first object to first input, second object to second input etc...) + InputPresets.Add(CurrentObject, InputIndex++); + } + else + { + // All the selection will be applied to the asset's first input + InputPresets.Add(CurrentObject, 0); + } + } + + if (IsValid(InPreset)) + { + HoudiniCookable->QueuePreCookCallback([InPreset](UHoudiniCookable* HC) + { + // First apply the preset when we reach the PreCook phase. + if (IsValid(InPreset) && IsValid(HC)) + { + FHoudiniToolsEditor::ApplyPresetToHoudiniCookable(InPreset, HC); + } + }); + } + + // Set the input preset on the HoudiniAssetComponent + if (InputPresets.Num() > 0) + { + HoudiniCookable->QueuePreCookCallback([InputPresets](UHoudiniCookable* HC) + { + // Apply the inputs once the HDA has reached its PreCookCallback + FHoudiniToolsEditor::ApplyObjectsAsHoudiniAssetInputs(InputPresets, HC); + }); + } + } + } + + // Select the Actor we just created + if (GEditor->CanSelectActor(CreatedActor, true, true)) + { + GEditor->SelectNone(true, true, false); + GEditor->SelectActor(CreatedActor, true, true, true); + } + } +} + +AActor* +FHoudiniEngineEditorUtils::InstantiateHoudiniAssetAt( + UHoudiniAsset* InHoudiniAsset, + const FTransform& InTransform, + UWorld* InSpawnInWorld, + ULevel* InSpawnInLevelOverride, + UHoudiniPreset* InPreset) +{ + if (!InHoudiniAsset) + return nullptr; + + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + + // Load the asset + UObject* AssetObj = Cast(InHoudiniAsset); + if (!AssetObj) + return nullptr; + + // Get the asset Factory + UActorFactory* Factory = GEditor->FindActorFactoryForActorClass(AHoudiniAssetActor::StaticClass()); + if (!Factory) + return nullptr; + + // Determine the level to spawn in from the supplied parameters + // InSpawnInLevelOverride if valid, else if InSpawnInWorld is valid its current level + // lastly, get the editor world's current level + ULevel* LevelToSpawnIn = InSpawnInLevelOverride; + if (!IsValid(LevelToSpawnIn)) + { + if (IsValid(InSpawnInWorld)) + LevelToSpawnIn = InSpawnInWorld->GetCurrentLevel(); + else + LevelToSpawnIn = GEditor->GetEditorWorldContext().World()->GetCurrentLevel(); + } + + // We only need to create a single instance of the asset, regarding of the selection + AActor* CreatedActor = Factory->CreateActor(AssetObj, LevelToSpawnIn, InTransform); + if (!CreatedActor) + return nullptr; + + if (IsValid(InPreset)) + { + // If we have a preset, we have to apply it during PreCook + AHoudiniAssetActor* HACActor = Cast(CreatedActor); + if (IsValid(HACActor)) + { + UHoudiniCookable* HoudiniCookable = HACActor->GetHoudiniCookable(); + if (HoudiniCookable) + { + HoudiniCookable->QueuePreCookCallback([InPreset](UHoudiniCookable* HC) + { + if (IsValid(InPreset)) + { + FHoudiniToolsEditor::ApplyPresetToHoudiniCookable(InPreset, HC); + } + }); + } + } + } + + // Select the Actor we just created + if (GEditor->CanSelectActor(CreatedActor, true, true)) + { + GEditor->SelectNone(true, true, false); + GEditor->SelectActor(CreatedActor, true, true, true); + } + + return CreatedActor; +} + + +void +FHoudiniEngineEditorUtils::SaveAllHoudiniTemporaryCookData(UWorld *InSaveWorld) +{ + // Add a slate notification + FString Notification = TEXT("Saving all Houdini temporary cook data..."); + // FHoudiniEngineUtils::CreateSlateNotification(Notification); + + TArray PackagesToSave; + for (TObjectIterator Itr; Itr; ++Itr) + { + UHoudiniCookable* HC = *Itr; + if (!IsValid(HC)) + continue; + + if (InSaveWorld && InSaveWorld != HC->GetWorld()) + continue; + + const int32 NumOutputs = HC->GetNumOutputs(); + for (int32 Index = 0; Index < NumOutputs; ++Index) + { + UHoudiniOutput *Output = HC->GetOutputAt(Index); + if (!IsValid(Output)) + continue; + + // TODO: Also save landscape layer info objects? + if (Output->GetType() != EHoudiniOutputType::Mesh) + continue; + + for (auto &OutputObjectPair : Output->GetOutputObjects()) + { + UObject *Obj = OutputObjectPair.Value.OutputObject; + if (!IsValid(Obj)) + continue; + + UStaticMesh *SM = Cast(Obj); + if (!SM) + continue; + + UPackage *Package = SM->GetOutermost(); + if (!IsValid(Package)) + continue; + + if (Package->IsDirty() && Package->IsFullyLoaded() && Package != GetTransientPackage()) + { + PackagesToSave.Add(Package); + } + } + + for (auto& MaterialAssignementPair : Output->GetAssignementMaterials()) + { + UMaterialInterface* MatInterface = MaterialAssignementPair.Value; + if (!IsValid(MatInterface)) + continue; + + UPackage *Package = MatInterface->GetOutermost(); + if (!IsValid(Package)) + continue; + + if (Package->IsDirty() && Package->IsFullyLoaded() && Package != GetTransientPackage()) + { + PackagesToSave.Add(Package); + } + } + } + } + + UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, true); +} + +void +FHoudiniEngineEditorUtils::ReselectSelectedActors() +{ + // TODO: Duplicate with FHoudiniEngineUtils::UpdateEditorProperties ?? + USelection* Selection = GEditor->GetSelectedActors(); + TArray SelectedActors; + SelectedActors.SetNum(GEditor->GetSelectedActorCount()); + Selection->GetSelectedObjects(SelectedActors); + + GEditor->SelectNone(false, false, false); + + for (AActor* NextSelected : SelectedActors) + { + GEditor->SelectActor(NextSelected, true, true, true, true); + } +} + +void +FHoudiniEngineEditorUtils::ReselectActorIfSelected(AActor* const InActor) +{ + if (IsValid(InActor) && InActor->IsSelected()) + { + GEditor->SelectActor(InActor, false, false, false, false); + GEditor->SelectActor(InActor, true, true, true, true); + } +} + +void +FHoudiniEngineEditorUtils::ReselectComponentOwnerIfSelected(USceneComponent* const InComponent) +{ + if (!IsValid(InComponent)) + return; + AActor* const Actor = InComponent->GetOwner(); + if (!IsValid(Actor)) + return; + ReselectActorIfSelected(Actor); +} + +FString +FHoudiniEngineEditorUtils::GetNodeNamePaddedByPathDepth(const FString& InNodeName, const FString& InNodePath, const uint8 Padding, const TCHAR PathSep) +{ + int32 Depth = 0; + for (const TCHAR Char : InNodePath) + { + if (Char == PathSep) + Depth++; + } + FString Trimmed = InNodeName; + Trimmed.TrimStartInline(); + return Trimmed.LeftPad(Trimmed.Len() + (Depth * Padding)); +} + +void +FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty(FName InPropertyPath, UObject* InRootObject) +{ + if (!IsValid(InRootObject)) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty]: InRootObject is null.")); + return; + } + + const FCachedPropertyPath CachedPath(InPropertyPath.ToString()); + if (CachedPath.Resolve(InRootObject)) + { + // Notify that we have changed the property + // FPropertyChangedEvent Evt = CachedPath.ToPropertyChangedEvent(EPropertyChangeType::Unspecified); + // Construct FPropertyChangedEvent from the cached property path + const int32 NumSegments = CachedPath.GetNumSegments(); + FPropertyChangedEvent Evt( + CastFieldChecked(CachedPath.GetLastSegment().GetField().ToField()), + EPropertyChangeType::Unspecified, + { InRootObject }); + + if(NumSegments > 1) + { + Evt.SetActiveMemberProperty(CastFieldChecked(CachedPath.GetSegment(NumSegments - 2).GetField().ToField())); + } + + // Set the array of indices to the changed property + TArray> ArrayIndicesPerObject; + ArrayIndicesPerObject.AddDefaulted(1); + for (int32 SegmentIdx = 0; SegmentIdx < NumSegments; ++SegmentIdx) + { + const FPropertyPathSegment& Segment = CachedPath.GetSegment(SegmentIdx); + const int32 ArrayIndex = Segment.GetArrayIndex(); + if (ArrayIndex != INDEX_NONE) + { + ArrayIndicesPerObject[0].Add(Segment.GetName().ToString(), ArrayIndex); + } + } + Evt.SetArrayIndexPerObject(ArrayIndicesPerObject); + + FEditPropertyChain Chain; + CachedPath.ToEditPropertyChain(Chain); + FPropertyChangedChainEvent ChainEvent(Chain, Evt); + ChainEvent.ObjectIteratorIndex = 0; + InRootObject->PostEditChangeChainProperty(ChainEvent); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Could not resolve property path '%s' on %s."), *InPropertyPath.ToString(), *(InRootObject->GetFullName())); + } +} + +FString FHoudiniEngineEditorUtils::HoudiniEngineSavedDir() +{ + return FPaths::ProjectSavedDir() / "HoudiniEngine"; +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineEditorUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniPreset.h" + +class FString; +class UObject; +class UHoudiniAsset; +class AActor; +class ULevel; +class USceneComponent; + +enum class EHoudiniCurveType : int8; +enum class EHoudiniCurveMethod : int8; +enum class EHoudiniCurveBreakpointParameterization : int8; +enum class EHoudiniLandscapeOutputBakeType : uint8; +enum class EHoudiniToolType : uint8; +enum class EHoudiniToolSelectionType : uint8; + +struct FHoudiniEngineEditorUtils +{ +public: + + // Triggers an update the details panel + //static void UpdateEditorProperties(UObject* ObjectToUpdate); + + // Helper function for accessing the current CB selection + static int32 GetContentBrowserSelection(TArray< UObject* >& ContentBrowserSelection); + + // Helper function for accessing the current world selection + static int32 GetWorldSelection(TArray< UObject* >& WorldSelection, bool bHoudiniAssetActorsOnly = false); + + static FString HoudiniCurveTypeToString(const EHoudiniCurveType& HoudiniCurveType); + + static FString HoudiniCurveMethodToString(const EHoudiniCurveMethod& HoudinCurveMethod); + + static FString HoudiniCurveBreakpointParameterizationToString(const EHoudiniCurveBreakpointParameterization& HoudiniCurveBreakpointParameterization); + + static FString HoudiniLandscapeOutputBakeTypeToString(const EHoudiniLandscapeOutputBakeType& LandscapeBakeType); + + // (for temporary, we need to figure out a way to access the output curve's info later) + static FString HoudiniCurveMethodToUnrealCurveTypeString(const EHoudiniCurveType& HoudiniCurveType); + + static FTransform GetDefaulAssetSpawnTransform(); + + static FTransform GetMeanWorldSelectionTransform(); + + // Instantiate a HoudiniAsset at a given position. If InSpawnInLevelOverride is non-null, spawns in that level. + // Otherwise if InSpawnInWorld, spawns in the current level of InSpawnInWorld. Lastly, if both are null, spawns + // in the current level of the editor context world. + static AActor* InstantiateHoudiniAssetAt( + UHoudiniAsset* InHoudiniAsset, + const FTransform& InTransform, + UWorld* InSpawnInWorld=nullptr, + ULevel* InSpawnInLevelOverride=nullptr, + UHoudiniPreset* InPreset=nullptr + ); + + // Instantiate the given HDA, and handles the current CB/World selection + static void InstantiateHoudiniAsset( + UHoudiniAsset* InHoudiniAsset, + const EHoudiniToolType& InType, + const EHoudiniToolSelectionType& InSelectionType, + UHoudiniPreset* InPreset=nullptr); + + // Helper function used to save all temporary packages when the level is saved + static void SaveAllHoudiniTemporaryCookData(UWorld *InSaveWorld); + + // Deselect and reselect all selected actors to get rid of component not showing bug after create. + static void ReselectSelectedActors(); + + // Deselect and reselect InActor, but only if it is currently selected. Related to ReselectSelectedActors and + // the component not showing bug after create. + static void ReselectActorIfSelected(AActor* const InActor); + + // Deselect and reselect the owning actor of InComopnent, but only if the actor is currently selected. Related to + // ReselectSelectedActors and the component not showing bug after create. + static void ReselectComponentOwnerIfSelected(USceneComponent* const InComponent); + + // Gets the node name indent from the left with the specified number of spaces based on the path depth. + static FString GetNodeNamePaddedByPathDepth(const FString& InNodeName, const FString& InNodePath, const uint8 Padding=4, const TCHAR PathSep='/'); + + // Property change notifications + // Call PostEditChangeChainProperty on InRootObject for the property at InPropertyPath relative to + // InRootObject. + static void NotifyPostEditChangeProperty(FName InPropertyPath, UObject* InRootObject); + + // Get a ProjectSavedDir()/HoudiniEngine directory for saving editor state + static FString HoudiniEngineSavedDir(); +}; +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineStyle.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineStyle.h" + +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineUtils.h" + +#include "EditorStyleSet.h" +#include "SlateOptMacros.h" +#include "Styling/StyleColors.h" +#include "Styling/SlateStyle.h" +#include "Styling/SlateStyleRegistry.h" +#include "Styling/SlateTypes.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +#define IMAGE_BRUSH( RelativePath, ... ) FSlateImageBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define BOX_BRUSH( RelativePath, ... ) FSlateBoxBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define BORDER_BRUSH( RelativePath, ... ) FSlateBorderBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define TTF_FONT( RelativePath, ... ) FSlateFontInfo( StyleSet->RootToContentDir( RelativePath, TEXT(".ttf") ), __VA_ARGS__ ) +#define TTF_CORE_FONT( RelativePath, ... ) FSlateFontInfo( StyleSet->RootToCoreContentDir( RelativePath, TEXT(".ttf") ), __VA_ARGS__ ) +#define OTF_FONT( RelativePath, ... ) FSlateFontInfo( StyleSet->RootToContentDir( RelativePath, TEXT(".otf") ), __VA_ARGS__ ) +#define OTF_CORE_FONT( RelativePath, ... ) FSlateFontInfo( StyleSet->RootToCoreContentDir( RelativePath, TEXT(".otf") ), __VA_ARGS__ ) +#define DEFAULT_FONT(...) FCoreStyle::GetDefaultFontStyle(__VA_ARGS__) + +TSharedPtr FHoudiniEngineStyle::StyleSet = nullptr; + +TSharedPtr +FHoudiniEngineStyle::Get() +{ + return StyleSet; +} + +FName +FHoudiniEngineStyle::GetStyleSetName() +{ + static FName HoudiniStyleName(TEXT("HoudiniEngineStyle")); + return HoudiniStyleName; +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION + +void +FHoudiniEngineStyle::Initialize() +{ + // Only register the StyleSet once + if (StyleSet.IsValid()) + return; + + StyleSet = MakeShareable(new FSlateStyleSet(GetStyleSetName())); + StyleSet->SetContentRoot(FPaths::EngineContentDir() / TEXT("Editor/Slate")); + StyleSet->SetCoreContentRoot(FPaths::EngineContentDir() / TEXT("Slate")); + + // Note, these sizes are in Slate Units. + // Slate Units do NOT have to map to pixels. + const FVector2D Icon5x16(5.0f, 16.0f); + const FVector2D Icon8x4(8.0f, 4.0f); + const FVector2D Icon8x8(8.0f, 8.0f); + const FVector2D Icon10x10(10.0f, 10.0f); + const FVector2D Icon12x12(12.0f, 12.0f); + const FVector2D Icon12x16(12.0f, 16.0f); + const FVector2D Icon14x14(14.0f, 14.0f); + const FVector2D Icon16x16(16.0f, 16.0f); + const FVector2D Icon20x20(20.0f, 20.0f); + const FVector2D Icon22x22(22.0f, 22.0f); + const FVector2D Icon24x24(24.0f, 24.0f); + const FVector2D Icon25x25(25.0f, 25.0f); + const FVector2D Icon32x32(32.0f, 32.0f); + const FVector2D Icon40x40(40.0f, 40.0f); + const FVector2D Icon64x64(64.0f, 64.0f); + const FVector2D Icon36x24(36.0f, 24.0f); + const FVector2D Icon128x128(128.0f, 128.0f); + + static FString IconsDir = FHoudiniEngineUtils::GetHoudiniEnginePluginDir() / TEXT("Resources/Icons/"); + StyleSet->Set( + "HoudiniEngine.HoudiniEngineLogo", + new FSlateImageBrush(IconsDir + TEXT("icon_houdini_logo_16.png"), Icon16x16)); + + StyleSet->Set( + "ClassIcon.HoudiniAssetActor", + new FSlateImageBrush(IconsDir + TEXT("icon_houdini_logo_16.png"), Icon16x16)); + + StyleSet->Set( + "ClassThumbnail.HoudiniAssetActor", + new FSlateImageBrush(IconsDir + TEXT("icon_houdini_logo_128.png"), Icon64x64)); + + StyleSet->Set( + "HoudiniEngine.HoudiniEngineLogo40", + new FSlateImageBrush(IconsDir + TEXT("icon_houdini_logo_40.png"), Icon40x40)); + + StyleSet->Set( + "ClassIcon.HoudiniAsset", + new FSlateImageBrush(IconsDir + TEXT("houdini_digital_asset.png"), Icon16x16)); + + StyleSet->Set( + "ClassThumbnail.HoudiniAsset", + new FSlateImageBrush(IconsDir + TEXT("houdini_digital_asset_128.png"), Icon64x64)); + + static FString ResourcesDir = FHoudiniEngineUtils::GetHoudiniEnginePluginDir() / TEXT("Resources/"); + + FString AssetHelpIcon = IconsDir + TEXT("asset_help16x16.png"); + FString BakeAllIcon = IconsDir + TEXT("bake_all16x16.png"); + FString BakeSelIcon = IconsDir + TEXT("bake_selected16x16.png"); + FString CleanTempIcon = IconsDir + TEXT("clean_temp16x16.png"); + FString CookAllIcon = IconsDir + TEXT("cook_all16x16.png"); + FString CookLogIcon = IconsDir + TEXT("cook_log16x16.png"); + FString CookSelIcon = IconsDir + TEXT("cook_selected16x16.png"); + FString DigitalAssetIcon = IconsDir + TEXT("digital_asset16x16.png"); + FString OnlineForumIcon = IconsDir + TEXT("online_forum16x16.png"); + FString OnlineHelpIcon = IconsDir + TEXT("online_help16x16.png"); + FString OpenInHIcon = IconsDir + TEXT("open_in_houdini16x16.png"); + FString PauseIcon = IconsDir + TEXT("pause16x16.png"); + FString PDGCancelIcon = IconsDir + TEXT("pdg_cancel16x16.png"); + FString PDGDirtyAllIcon = IconsDir + TEXT("pdg_dirty_all16x16.png"); + FString PDGDirtyNodeIcon = IconsDir + TEXT("pdg_dirty_node16x16.png"); + FString PDGLinkIcon = IconsDir + TEXT("pdg_link16x16.png"); + FString PDGPauseIcon = IconsDir + TEXT("pdg_pause16x16.png"); + FString PDGRefreshIcon = IconsDir + TEXT("pdg_refresh16x16.png"); + FString PDGResetIcon = IconsDir + TEXT("pdg_reset16x16.png"); + FString RebuildAllIcon = IconsDir + TEXT("rebuild_all16x16.png"); + FString RebuildSelIcon = IconsDir + TEXT("rebuild_selected16x16.png"); + FString RefineAllIcon = IconsDir + TEXT("refine_all16x16.png"); + FString RefineSelIcon = IconsDir + TEXT("refine_selected16x16.png"); + FString ReportBugIcon = IconsDir + TEXT("report_bug16x16.png"); + FString ResetIcon = IconsDir + TEXT("reset16x16.png"); + FString ResetParamIcon = IconsDir + TEXT("reset_parameters16x16.png"); + FString SaveToHipIcon = IconsDir + TEXT("save_to_hip16x16.png"); + FString SessionConnectIcon = IconsDir + TEXT("session_connect16x16.png"); + FString SessionCreateIcon = IconsDir + TEXT("session_create16x16.png"); + FString SessionRestartIcon = IconsDir + TEXT("session_restart16x16.png"); + FString SessionStopIcon = IconsDir + TEXT("session_stop16x16.png"); + FString SessionSyncIcon = IconsDir + TEXT("session_sync16x16.png"); + FString SessionSyncStartIcon = IconsDir + TEXT("session_sync_start16x16.png"); + FString SessionSyncStopIcon = IconsDir + TEXT("session_sync_stop16x16.png"); + FString ViewportSyncIcon = IconsDir + TEXT("viewport_sync16x16.png"); + FString ViewportSyncBothIcon = IconsDir + TEXT("viewport_sync_both16x16.png"); + FString ViewportSyncHoudiniIcon = IconsDir + TEXT("viewport_sync_houdini16x16.png"); + FString ViewportSyncOffIcon = IconsDir + TEXT("viewport_sync_off16x16.png"); + FString ViewportSyncUnrealIcon = IconsDir + TEXT("viewport_sync_unreal16x16.png"); + FString NodeSyncIcon = IconsDir + TEXT("icon_houdini_logo_16.png"); + FString HoudiniToolsIcon = IconsDir + TEXT("icon_houdini_logo_16.png"); + FString CurveClosedIcon = IconsDir + TEXT("curve_closed16x16.png"); + FString CurveNotClosedIcon = IconsDir + TEXT("curve_not_closed16x16.png"); + FString CurveReversedIcon = IconsDir + TEXT("curve_reversed16x16.png"); + FString CurveNotReversedIcon = IconsDir + TEXT("curve_not_reversed16x16.png"); + + FString ContentExampleGitIcon = IconsDir + TEXT("icon_houdini_logo_16.png"); + FString ContentExampleBrowseToIcon = IconsDir + TEXT("icon_houdini_logo_16.png"); + //_GetEditorStyle().GetBrush("SystemWideCommands.FindInContentBrowser.Small")->GetResourceName().ToString(); + + FString InfoIcon = IconsDir + TEXT("icon_hengine_logo_16.png"); + FString SettingsIcon = _GetEditorStyle().GetBrush("Launcher.EditSettings")->GetResourceName().ToString(); + + StyleSet->Set("HoudiniEngine._CreateSession", new FSlateImageBrush(SessionCreateIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._ConnectSession", new FSlateImageBrush(SessionConnectIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._StopSession", new FSlateImageBrush(SessionStopIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._RestartSession", new FSlateImageBrush(SessionRestartIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._SessionSync", new FSlateImageBrush(SessionSyncIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._OpenSessionSync", new FSlateImageBrush(SessionSyncStartIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._CloseSessionSync", new FSlateImageBrush(SessionSyncStopIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._ResetPCGSession", new FSlateImageBrush(SettingsIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._SyncViewport", new FSlateImageBrush(ViewportSyncIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._ViewportSyncNone", new FSlateImageBrush(ViewportSyncOffIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._ViewportSyncBoth", new FSlateImageBrush(ViewportSyncBothIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._ViewportSyncUnreal", new FSlateImageBrush(ViewportSyncUnrealIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._ViewportSyncHoudini", new FSlateImageBrush(ViewportSyncHoudiniIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._OpenNodeSync", new FSlateImageBrush(NodeSyncIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._OpenHoudiniTools", new FSlateImageBrush(HoudiniToolsIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._InstallInfo", new FSlateImageBrush(InfoIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._PluginSettings", new FSlateImageBrush(SettingsIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._PluginEditorSettings", new FSlateImageBrush(SettingsIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._OpenInHoudini", new FSlateImageBrush(OpenInHIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._SaveHIPFile", new FSlateImageBrush(SaveToHipIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._CleanUpTempFolder", new FSlateImageBrush(CleanTempIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._OnlineDoc", new FSlateImageBrush(OnlineHelpIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._OnlineForum", new FSlateImageBrush(OnlineForumIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._ReportBug", new FSlateImageBrush(ReportBugIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._ContentExampleGit", new FSlateImageBrush(ContentExampleGitIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._ContentExampleBrowseTo", new FSlateImageBrush(ContentExampleBrowseToIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._CookAll", new FSlateImageBrush(CookAllIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._CookSelected", new FSlateImageBrush(CookSelIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._BakeSelected", new FSlateImageBrush(BakeSelIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._BakeAll", new FSlateImageBrush(BakeAllIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._RebuildAll", new FSlateImageBrush(RebuildAllIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._RebuildSelected", new FSlateImageBrush(RebuildSelIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._RefineAll", new FSlateImageBrush(RefineAllIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._RefineSelected", new FSlateImageBrush(RefineSelIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._PauseAssetCooking", new FSlateImageBrush(PauseIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._Reset", new FSlateImageBrush(ResetIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine.DigitalAsset", new FSlateImageBrush(DigitalAssetIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine.AssetHelp", new FSlateImageBrush(AssetHelpIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine.PDGLink", new FSlateImageBrush(PDGLinkIcon, Icon16x16)); + + StyleSet->Set("HoudiniEngine._CurveClosed", new FSlateImageBrush(CurveClosedIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._CurveNotClosed", new FSlateImageBrush(CurveNotClosedIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._CurveReversed", new FSlateImageBrush(CurveReversedIcon, Icon16x16)); + StyleSet->Set("HoudiniEngine._CurveNotReversed", new FSlateImageBrush(CurveNotReversedIcon, Icon16x16)); + + // We need some colors from Editor Style & this is the only way to do this at the moment + const FSlateColor DefaultForeground = _GetEditorStyle().GetSlateColor("DefaultForeground"); + const FSlateColor InvertedForeground = _GetEditorStyle().GetSlateColor("InvertedForeground"); + const FSlateColor SelectorColor = _GetEditorStyle().GetSlateColor("SelectorColor"); + const FSlateColor SelectionColor = _GetEditorStyle().GetSlateColor("SelectionColor"); + const FSlateColor SelectionColor_Inactive = _GetEditorStyle().GetSlateColor("SelectionColor_Inactive"); + + const FTableRowStyle &NormalTableRowStyle = _GetEditorStyle().GetWidgetStyle("TableView.Row"); + StyleSet->Set( + "HoudiniEngine.TableRow", FTableRowStyle(NormalTableRowStyle) + .SetEvenRowBackgroundBrush(FSlateNoResource()) + .SetEvenRowBackgroundHoveredBrush(IMAGE_BRUSH("Common/Selection", Icon8x8, FLinearColor(1.0f, 1.0f, 1.0f, 0.1f))) + .SetOddRowBackgroundBrush(FSlateNoResource()) + .SetOddRowBackgroundHoveredBrush(IMAGE_BRUSH("Common/Selection", Icon8x8, FLinearColor(1.0f, 1.0f, 1.0f, 0.1f))) + .SetSelectorFocusedBrush(BORDER_BRUSH("Common/Selector", FMargin(4.f / 16.f), SelectorColor)) + .SetActiveBrush(IMAGE_BRUSH("Common/Selection", Icon8x8, SelectionColor)) + .SetActiveHoveredBrush(IMAGE_BRUSH("Common/Selection", Icon8x8, SelectionColor)) + .SetInactiveBrush(IMAGE_BRUSH("Common/Selection", Icon8x8, SelectionColor_Inactive)) + .SetInactiveHoveredBrush(IMAGE_BRUSH("Common/Selection", Icon8x8, SelectionColor_Inactive)) + .SetTextColor(DefaultForeground) + .SetSelectedTextColor(InvertedForeground) + ); + + // Normal Text + const FTextBlockStyle& NormalText = _GetEditorStyle().GetWidgetStyle("NormalText"); + StyleSet->Set( + "HoudiniEngine.ThumbnailText", FTextBlockStyle(NormalText) + //.SetFont(TTF_CORE_FONT("Fonts/Roboto-Regular", 9)) + .SetFont(DEFAULT_FONT("Regular", 9)) + .SetColorAndOpacity(FSlateColor::UseForeground()) + .SetShadowOffset(FVector2D::ZeroVector) + .SetShadowColorAndOpacity(FLinearColor::Black) + .SetHighlightColor(FLinearColor(0.02f, 0.3f, 0.0f)) + .SetHighlightShape(BOX_BRUSH("Common/TextBlockHighlightShape", FMargin(3.f / 8.f))) + ); + + // InstallInfo Text + const FTextBlockStyle RegularText = FTextBlockStyle() + .SetFont(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + .SetColorAndOpacity(FSlateColor::UseForeground()); + + StyleSet->Set("InstallInfo.Regular", + FTextBlockStyle(NormalText) + ); + StyleSet->Set("InstallInfo.Bold", + FTextBlockStyle(NormalText).SetFont(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + ); + + StyleSet->Set("InstallInfo.Italic", + FTextBlockStyle(NormalText).SetFont(FCoreStyle::GetDefaultFontStyle("Italic", 10)) + ); + + StyleSet->Set("HoudiniEngine.ThumbnailShadow", new BOX_BRUSH("ContentBrowser/ThumbnailShadow", FMargin(4.0f / 64.0f))); + StyleSet->Set("HoudiniEngine.ThumbnailBackground", new IMAGE_BRUSH("Common/ClassBackground_64x", FVector2D(64.f, 64.f), FLinearColor(0.75f, 0.75f, 0.75f, 1.0f))); + + const FButtonStyle SimpleButton = FButtonStyle() + .SetNormal(FSlateNoResource()) + .SetHovered(FSlateNoResource()) + .SetPressed(FSlateNoResource()) + // .SetHovered(FSlateRoundedBoxBrush(FStyleColors::Dropdown, 4.0f)) + // .SetPressed(FSlateRoundedBoxBrush(FStyleColors::Dropdown, 4.0f)) + .SetDisabled(FSlateNoResource()) + .SetNormalForeground(FStyleColors::Foreground) + // .SetHoveredForeground(FStyleColors::ForegroundHover) + // .SetPressedForeground(FStyleColors::ForegroundHover) + .SetDisabledForeground(FStyleColors::Foreground) + .SetNormalPadding(CoreStyleConstants::ButtonMargins) + .SetPressedPadding(CoreStyleConstants::PressedButtonMargins); + + StyleSet->Set("HoudiniEngine.HelpButton", SimpleButton); + + + + // Register Slate style. + FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get()); +}; + +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +#undef IMAGE_BRUSH +#undef BOX_BRUSH +#undef BORDER_BRUSH +#undef TTF_FONT +#undef TTF_CORE_FONT +#undef OTF_FONT +#undef OTF_CORE_FONT + +void +FHoudiniEngineStyle::Shutdown() +{ + if (StyleSet.IsValid()) + { + FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet.Get()); + ensure(StyleSet.IsUnique()); + StyleSet.Reset(); + } +} + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineStyle.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "Styling/SlateStyle.h" + +class FHoudiniEngineStyle +{ + public: + static void Initialize(); + static void Shutdown(); + static TSharedPtr Get(); + static FName GetStyleSetName(); + + static FColor GetHoudiniAssetFColor() { return FColor(255, 165, 0); } + static FLinearColor GetHoudiniAssetColor() { return FLinearColor(GetHoudiniAssetFColor()); } + static FLinearColor GetHoudiniPresetColor() { return FLinearColor(0.0f, 0.3f, 1.0f, 1.0f); } + + private: + //static FString InContent(const FString &RelativePath, const ANSICHAR *Extension); + static TSharedPtr StyleSet; +}; +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineTool.h`: + +```h +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniEngineToolTypes.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniAsset.h" +#include "HoudiniPreset.h" +#include "HoudiniToolTypes.h" + +#include "HoudiniEngineToolTypes.generated.h" + + +class IAssetTools; +class IAssetTypeActions; +class IComponentAssetBroker; +class UHoudiniAssetComponent; +class FMenuBuilder; + +struct FSlateBrush; +struct FHoudiniToolDescription; + +USTRUCT(BlueprintType) +struct FHoudiniToolDirectory +{ + GENERATED_BODY() + + /** Name of the tool directory */ + UPROPERTY(GlobalConfig, Category = Tool, EditAnywhere) + FString Name; + + /** Path of the tool directory */ + UPROPERTY(GlobalConfig, Category = Tool, EditAnywhere) + FDirectoryPath Path; + + /** Unique generated ID used to store the imported uasset for the tools */ + UPROPERTY(GlobalConfig, Category = Tool, VisibleDefaultsOnly) + FString ContentDirID; + + FORCEINLINE bool operator==(const FHoudiniToolDirectory& Other)const + { + return Name == Other.Name && Path.Path == Other.Path.Path; + } + + FORCEINLINE bool operator!=(const FHoudiniToolDirectory& Other)const + { + return !(*this == Other); + } +}; + + +UENUM() +enum class EHoudiniToolCategoryType : uint8 +{ + Package, + User +}; + + +UENUM() +enum class EHoudiniPackageToolType : uint8 +{ + HoudiniAsset, + Preset +}; + + +USTRUCT(BlueprintType) +struct FHoudiniToolCategory +{ + GENERATED_BODY() + + FHoudiniToolCategory() + : Name() + , CategoryType(EHoudiniToolCategoryType::Package) + {} + + FHoudiniToolCategory(const FString& InCategoryName, const EHoudiniToolCategoryType& InCategoryType) + : Name(InCategoryName) + , CategoryType(InCategoryType) + {} + + bool operator<(const FHoudiniToolCategory& Other) const + { + if (CategoryType != Other.CategoryType) + { + // if we have different categories, the User category + // should always be first in the list. + if (CategoryType == EHoudiniToolCategoryType::User) + { + return true; // A < B + } + else + { + return false; // A > B + } + } + + // For now, we manually place the SideFX category at the top until + // we have a more generic category prioritization mechanism. + if (Name.ToUpper() == TEXT("SIDEFX")) + return true; + if (Other.Name.ToUpper() == TEXT("SIDEFX")) + return false; + + // Category Types are the same. Resolve priority by name. + return Name < Other.Name; + } + + bool operator==(const FHoudiniToolCategory& Other) const + { + return (Name == Other.Name) && (CategoryType == Other.CategoryType); + } + + + FHoudiniToolCategory& operator=(const FHoudiniToolCategory& Other) = default; + + FString Name; + EHoudiniToolCategoryType CategoryType; +}; + +inline uint32 GetTypeHash(const FHoudiniToolCategory& Category) +{ + uint32 Hash = GetTypeHash(Category.Name); + Hash = HashCombine(Hash, FCrc::TypeCrc32(Category.CategoryType)); + return Hash; +} + + +struct FHoudiniTool +{ + + FHoudiniTool() + : HoudiniAsset( nullptr) + , Name() + , ToolTipText() + , Icon() + , IconTexture(nullptr) + , HelpURL() + , Type(EHoudiniToolType::HTOOLTYPE_OPERATOR_SINGLE) + , CategoryType(EHoudiniToolCategoryType::Package) + , SelectionType(EHoudiniToolSelectionType::HTOOL_SELECTION_ALL) + { + } + + FHoudiniTool( + const TSoftObjectPtr < class UHoudiniAsset >& InHoudiniAsset, const FText& InName, + const EHoudiniToolType& InType, const EHoudiniToolSelectionType& InSelType, + const FText& InToolTipText, TSharedPtr InIcon, + UTexture2D* InIconTexture, + const FString& InHelpURL, + const EHoudiniToolCategoryType& InCategoryType, const FHoudiniToolDirectory& InToolDirectory, + const FString& InJSONFile ) + : HoudiniAsset( InHoudiniAsset ) + , Name( InName ) + , ToolTipText( InToolTipText ) + , Icon( InIcon ) + , IconTexture( InIconTexture ) + , HelpURL( InHelpURL ) + , Type( InType ) + , CategoryType( InCategoryType ) + , SelectionType( InSelType ) + { + } + + // Return the asset object based on the PackageToolType. This will load the object if needed. + UObject* GetAssetObject() const + { + switch (PackageToolType) + { + case EHoudiniPackageToolType::HoudiniAsset: + return HoudiniAsset.LoadSynchronous(); + break; + case EHoudiniPackageToolType::Preset: + return HoudiniPreset.LoadSynchronous(); + break; + default: ; + } + return nullptr; + } + + UClass* GetAssetObjectClass() const + { + switch (PackageToolType) + { + case EHoudiniPackageToolType::HoudiniAsset: + return UHoudiniAsset::StaticClass(); + break; + case EHoudiniPackageToolType::Preset: + return UHoudiniPreset::StaticClass(); + break; + default: ; + } + return nullptr; + } + + bool IsHiddenInCategory(const FString& CategoryName) const { return ExcludedFromCategories.Contains(CategoryName); } + + /** The Houdini Asset used by the tool **/ + TSoftObjectPtr < UHoudiniAsset > HoudiniAsset; + /** The (optional) Houdini Preset represent by this tool, if it is a present **/ + TSoftObjectPtr < UHoudiniPreset > HoudiniPreset; + /** The owning tools package **/ + TSoftObjectPtr < class UHoudiniToolsPackageAsset > ToolsPackage; + + // Type for this tool inside a package + EHoudiniPackageToolType PackageToolType; + + /** The name to be displayed */ + FText Name; + + /** The name to be displayed */ + FText ToolTipText; + + /** The icon to be displayed */ + TSharedPtr Icon; + + UTexture2D* IconTexture; + + /** The help URL for this tool */ + FString HelpURL; + + /** The type of tool, this will change how the asset handles the current selection **/ + EHoudiniToolType Type; + + // Type of category to which FHoudiniTool belongs. + EHoudiniToolCategoryType CategoryType; + + /** Indicate what the tool should consider for selection **/ + EHoudiniToolSelectionType SelectionType; + + // Categories from which this HoudiniTool is explicitly excluded / hidden + TSet ExcludedFromCategories; +}; + +struct FHoudiniToolList +{ + // List of HoudiniTools + TArray> Tools; + // Set of unique packages collected from the tools in this list. + TSet> Packages; +}; + + + + + + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniGeoFactory.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniGeoFactory.h" + +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniGeoImporter.h" +#include "HoudiniPackageParams.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniOutput.h" +#include "HoudiniEngine.h" + +#include "Engine/StaticMesh.h" +//#include "Engine/SkeletalMesh.h" + +#include "EditorFramework/AssetImportData.h" +#include "Misc/FileHelper.h" +#include "Internationalization/Internationalization.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +UHoudiniGeoFactory::UHoudiniGeoFactory(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) +{ + // This factory is responsible for manufacturing HoudiniEngine assets. + SupportedClass = nullptr;// UHoudiniAsset::StaticClass(); + + // This factory does not manufacture new objects from scratch. + bCreateNew = false; + + // This factory will not open the editor for each new object. + bEditAfterNew = false; + + // This factory will import objects from files. + bEditorImport = true; + + // Factory does not import objects from text. + bText = false; + + // Add supported formats. + Formats.Add(TEXT("bgeo;Houdini Geometry")); + Formats.Add(TEXT("bgeo.sc;Houdini Geometry (compressed)")); + Formats.Add(TEXT("sc;Houdini Geometry (compressed)")); +} + +bool +UHoudiniGeoFactory::FactoryCanImport(const FString& Filename) +{ + const FString Extension = FPaths::GetExtension(Filename); + if(FPaths::GetExtension(Filename) == TEXT("bgeo")) + return true; + if (FPaths::GetExtension(Filename) == TEXT("bgeo.sc")) + return true; + if (FPaths::GetExtension(Filename) == TEXT("sc")) + return true; + + return false; +} + +bool +UHoudiniGeoFactory::DoesSupportClass(UClass * Class) +{ + return Class == UStaticMesh::StaticClass(); //|| Class == USkeletalMesh::StaticClass()); +} + +UClass* +UHoudiniGeoFactory::ResolveSupportedClass() +{ + return UStaticMesh::StaticClass(); +} + +FText +UHoudiniGeoFactory::GetDisplayName() const +{ + return LOCTEXT("HoudiniGeoFactoryDescription", "Houdini Engine Geo"); +} + +UObject* +UHoudiniGeoFactory::FactoryCreateFile(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, const FString& Filename, const TCHAR* Parms, FFeedbackContext* Warn, bool& bOutOperationCanceled) +{ + // Make sure we're loading bgeo / bgeo.sc files + FString FileExtension = FPaths::GetExtension(Filename); + if (!FileExtension.Contains(TEXT("bgeo"), ESearchCase::IgnoreCase) && !FileExtension.Contains(TEXT("sc"), ESearchCase::IgnoreCase)) + return Super::FactoryCreateFile(InClass, InParent, InName, Flags, Filename, Parms, Warn, bOutOperationCanceled); + + // + // TODO: Handle import settings here? + // + UObject* Success = Import(InClass, Cast(InParent), InName.ToString(), Filename, Flags, false); + if (!Success) + { + FString Notification = TEXT("BGEO Importer: Failed to load the BGEO file."); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Notification)); + return nullptr; + } + + // Notifiy we're done loading the bgeo + FString Notification = TEXT("BGEO Importer: BGEO file imported succesfully."); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Notification)); + + return Success; +} + +bool +UHoudiniGeoFactory::CanReimport(UObject * Obj, TArray& OutFilenames) +{ + UAssetImportData* ImportData = nullptr; + if (Obj->GetClass() == UStaticMesh::StaticClass()) + { + UStaticMesh* Mesh = Cast(Obj); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + ImportData = Mesh->GetAssetImportData(); +#else + ImportData = Mesh->AssetImportData; +#endif + } + /* + else if (Obj->GetClass() == USkeletalMesh::StaticClass()) + { + USkeletalMesh* Cache = Cast(Obj); + ImportData = Cache->AssetImportData; + } + */ + + if (ImportData) + { + FString FileExtension = FPaths::GetExtension(ImportData->GetFirstFilename()); + if (FileExtension.Contains(TEXT("bgeo")) + || FileExtension.Contains(TEXT("sc"))) + { + ImportData->ExtractFilenames(OutFilenames); + return true; + } + } + return false; +} + +void +UHoudiniGeoFactory::SetReimportPaths(UObject * Obj, const TArray& NewReimportPaths) +{ + UStaticMesh* Mesh = Cast(Obj); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + if (Mesh && Mesh->GetAssetImportData() && ensure(NewReimportPaths.Num() == 1)) + { + Mesh->GetAssetImportData()->UpdateFilenameOnly(NewReimportPaths[0]); + } +#else + if (Mesh && Mesh->AssetImportData && ensure(NewReimportPaths.Num() == 1)) + { + Mesh->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]); + } +#endif + /* + USkeletalMesh* SkeletalMesh = Cast(Obj); + if (SkeletalMesh && SkeletalMesh->AssetImportData && ensure(NewReimportPaths.Num() == 1)) + { + SkeletalMesh->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]); + } + */ +} + + +UObject* +UHoudiniGeoFactory::Import(UClass* InClass, UPackage* InParent, const FString & FileName, const FString & AbsoluteFilePath, EObjectFlags Flags, const bool& bReimport) +{ + // Broadcast PreImport + GEditor->GetEditorSubsystem()->BroadcastAssetPreImport(this, InClass, InParent, FName(FileName), TEXT("Houdini GEO")); + + // Create a new Geo importer + TArray> DummyOldOutputs; + TArray> NewOutputs; + UHoudiniGeoImporter* BGEOImporter = NewObject(this); + BGEOImporter->AddToRoot(); + + // Clean up lambda + auto CleanUp = [&NewOutputs, &BGEOImporter]() + { + // Remove the importer and output objects from the root set + BGEOImporter->RemoveFromRoot(); + for (auto Out : NewOutputs) + Out->RemoveFromRoot(); + }; + + // Failure lambda + auto FailImportAndReturnNull = [this, &CleanUp, &NewOutputs, &BGEOImporter]() + { + CleanUp(); + + // Failed to read the file info, fail the import + GEditor->GetEditorSubsystem()->BroadcastAssetPostImport(this, nullptr); + + return nullptr; + }; + + // 1. Houdini Engine Session + // See if we should/can start the default "first" HE session + if (!BGEOImporter->AutoStartHoudiniEngineSessionIfNeeded()) + return FailImportAndReturnNull(); + + // 2. Update the file paths + if (!BGEOImporter->SetFilePath(AbsoluteFilePath)) + return FailImportAndReturnNull(); + + // 3. Load the BGEO file in HAPI + HAPI_NodeId NodeId; + if (!BGEOImporter->LoadBGEOFileInHAPI(NodeId)) + return FailImportAndReturnNull(); + + // Prepare the package used for creating the mesh, landscape and instancer pacakges + FHoudiniPackageParams PackageParams; + PackageParams.PackageMode = EPackageMode::Bake; + PackageParams.TempCookFolder = FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder(); + PackageParams.HoudiniAssetName = FString(); + PackageParams.BakeFolder = FPackageName::GetLongPackagePath(InParent->GetOutermost()->GetName()); + PackageParams.ObjectName = FPaths::GetBaseFilename(InParent->GetName()); + + if (bReimport) + { + PackageParams.ReplaceMode = EPackageReplaceMode::ReplaceExistingAssets; + } + else + { + PackageParams.ReplaceMode = EPackageReplaceMode::CreateNewAssets; + } + + // 4. Get the output from the file node + if (!BGEOImporter->BuildOutputsForNode(NodeId, DummyOldOutputs, NewOutputs)) + return FailImportAndReturnNull(); + + // 5. Create all the objects using the outputs + const FHoudiniStaticMeshGenerationProperties& StaticMeshGenerationProperties = FHoudiniEngineRuntimeUtils::GetDefaultStaticMeshGenerationProperties(); + const FMeshBuildSettings& MeshBuildSettings = FHoudiniEngineRuntimeUtils::GetDefaultMeshBuildSettings(); + if (!BGEOImporter->CreateObjectsFromOutputs(NewOutputs, PackageParams, StaticMeshGenerationProperties, MeshBuildSettings)) + return FailImportAndReturnNull(); + + // 6. Delete the created node in Houdini + if (!BGEOImporter->DeleteCreatedNode(NodeId)) + { + // Not good, but not fatal.. + //return false; + } + + // Get our result object and "finalize" them + TArray Results = BGEOImporter->GetOutputObjects(); + for (UObject* Object : Results) + { + if (!IsValid(Object)) + continue; + + Object->SetFlags(Flags); + + UAssetImportData * AssetImportData = nullptr; + if (Object->IsA()) + { + UStaticMesh* SM = Cast(Object); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + AssetImportData = SM->GetAssetImportData(); +#else + AssetImportData = SM->AssetImportData; +#endif + // Create reimport information. + if (!AssetImportData) + { + AssetImportData = NewObject(SM, UAssetImportData::StaticClass()); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + SM->SetAssetImportData(AssetImportData); +#else + SM->AssetImportData = AssetImportData; +#endif + } + } + /* + else if (Object->IsA()) + { + USkeletalMesh * SkeletalMesh = Cast(Object); + AssetImportData = SkeletalMesh->AssetImportData; + // Create reimport information. + if (!AssetImportData) + { + AssetImportData = NewObject(SkeletalMesh, USkeletalMesh::StaticClass()); + SkeletalMesh->AssetImportData = AssetImportData; + } + } + */ + + if (AssetImportData) + AssetImportData->Update(AbsoluteFilePath); + + GEditor->GetEditorSubsystem()->BroadcastAssetPostImport(this, Object); + Object->MarkPackageDirty(); + Object->PostEditChange(); + } + + CleanUp(); + + // Determine out parent according to the generated assets outer + UObject* OutParent = (Results.Num() > 0 && InParent != Results[0]->GetOutermost()) ? Results[0]->GetOutermost() : InParent; + return (Results.Num() > 0) ? OutParent : nullptr; +} + +EReimportResult::Type +UHoudiniGeoFactory::Reimport(UObject * Obj) +{ + auto FailReimport = []() + { + // Notifiy we failed to load the bgeo + HOUDINI_LOG_MESSAGE(TEXT("Houdini Asset reimport has failed.")); + return EReimportResult::Failed; + }; + + if (!IsValid(Obj)) + return FailReimport(); + + UPackage* Package = Cast(Obj->GetOuter()); + if (!IsValid(Package)) + return FailReimport(); + + UAssetImportData* ImportData = nullptr; + if (Obj->GetClass() == UStaticMesh::StaticClass()) + { + UStaticMesh* StaticMesh = Cast(Obj); + if (!IsValid(StaticMesh)) + return FailReimport(); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + ImportData = StaticMesh->GetAssetImportData(); +#else + ImportData = StaticMesh->AssetImportData; +#endif + } + /* + else if(Obj->GetClass() == USkeletalMesh::StaticClass()) + { + USkeletalMesh* SkeletalMesh = Cast(Obj); + if (!IsValid(SkeletalMesh)) + return FailReimport(); + + ImportData = SkeletalMesh->AssetImportData; + } + */ + if (!ImportData || !IsValid(ImportData)) + return FailReimport(); + + if (ImportData->GetSourceFileCount() <= 0) + return FailReimport(); + + const FString RelativeFileName = ImportData->SourceData.SourceFiles[0].RelativeFilename; + const FString FileExtension = FPaths::GetExtension(RelativeFileName); + FString FileName = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*RelativeFileName); + if (!FileExtension.Contains(TEXT("bgeo"), ESearchCase::IgnoreCase) && !FileExtension.Contains(TEXT("sc"), ESearchCase::IgnoreCase)) + return FailReimport(); + + if (!Import(Obj->GetClass(), Package, Obj->GetName(), FileName, Obj->GetFlags(), true)) + return FailReimport(); + + HOUDINI_LOG_MESSAGE(TEXT("Houdini Asset reimport has Succeed.")); + + // Notifiy we're done loading the bgeo + FString Notification = TEXT("BGEO Importer: BGEO file re-imported succesfully."); + FHoudiniEngine::Get().UpdateTaskSlateNotification(FText::FromString(Notification)); + + return EReimportResult::Succeeded; +} + +int32 +UHoudiniGeoFactory::GetPriority() const +{ + return ImportPriority; +} + + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniGeoFactory.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "EditorReimportHandler.h" +#include "Factories/Factory.h" +#include "HoudiniGeoFactory.generated.h" + +class UClass; +class UObject; +class FFeedbackContext; + +UCLASS(config = Editor) +class UHoudiniGeoFactory : public UFactory, public FReimportHandler +{ + GENERATED_UCLASS_BODY() + + public: + + // + // UFactory Interface + // + // Returns the name of the factory for menus + virtual FText GetDisplayName() const override; + // return true if it supports this class + virtual bool DoesSupportClass(UClass * Class) override; + // + virtual UClass* ResolveSupportedClass() override; + // + //virtual UClass* ResolveSupportedClass() override; + // Return true if we can import the file + virtual bool FactoryCanImport(const FString& Filename) override; + + // Create a new object by importing it from a file name. + virtual UObject* FactoryCreateFile(UClass* InClass, UObject* InParent, FName InName, + EObjectFlags Flags, const FString& Filename, const TCHAR* Parms, + FFeedbackContext* Warn, bool& bOutOperationCanceled) override; + + // + // FReimportHandler Interface + // + UObject* Import(UClass* InClass, UPackage* OuterObject, const FString & FileName, const FString & AbsoluteFilePath, EObjectFlags Flags, const bool& bReimport); + + // Check to see if we have a handler to manage the reimporting of the object + virtual bool CanReimport(UObject * Obj, TArray< FString > & OutFilenames) override; + + // Sets the reimport path(s) for the specified object + virtual void SetReimportPaths(UObject * Obj, const TArray< FString > & NewReimportPaths) override; + // Attempt to reimport the specified object from its source + virtual EReimportResult::Type Reimport(UObject * Obj) override; + + //void ShowImportOptionsWindow(TSharedPtr& Options, FString FilePath, const UHoudiniGeoImporter& Importer); + + virtual int32 GetPriority() const override; + + // + // + // +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniHandleComponentVisualizer.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniHandleComponentVisualizer.h" + +#include "EditorViewportClient.h" +#include "CanvasTypes.h" +#include "SceneView.h" + +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniHandleTranslator.h" +#include "HoudiniAssetComponent.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +IMPLEMENT_HIT_PROXY(HHoudiniHandleVisProxy, HComponentVisProxy); + +HHoudiniHandleVisProxy::HHoudiniHandleVisProxy(const UActorComponent * InComponent) + : HComponentVisProxy(InComponent, HPP_Wireframe) +{} + + +FHoudiniHandleComponentVisualizerCommands::FHoudiniHandleComponentVisualizerCommands() + : TCommands< FHoudiniHandleComponentVisualizerCommands >( + "HoudiniHandleComponentVisualizer", + FText::FromString("Houdini handle Component Visualizer"), + NAME_None, + _GetEditorStyle().GetStyleSetName()) +{} + +void +FHoudiniHandleComponentVisualizerCommands::RegisterCommands() +{} + + +FHoudiniHandleComponentVisualizer::FHoudiniHandleComponentVisualizer() + : FComponentVisualizer() + , EditedComponent(nullptr) + , bEditing(false) +{ + FHoudiniHandleComponentVisualizerCommands::Register(); + VisualizerActions = MakeShareable(new FUICommandList); +} + +FHoudiniHandleComponentVisualizer::~FHoudiniHandleComponentVisualizer() +{ + FHoudiniHandleComponentVisualizerCommands::Unregister(); +} + +void +FHoudiniHandleComponentVisualizer::DrawVisualization( + const UActorComponent * Component, + const FSceneView * View, + FPrimitiveDrawInterface * PDI) +{ + const UHoudiniHandleComponent* HandleComponent = Cast(Component); + if (!HandleComponent) + return; + + UHoudiniAssetComponent* HAC = Cast(HandleComponent->GetOuter()); + if (!HAC) + return; + + static TMap ColorMapActive; + static TMap ColorMapInactive; + + int32 AssetId = HAC->GetAssetId(); + if (!ColorMapActive.Contains(AssetId) || !ColorMapInactive.Contains(AssetId)) + { + FLinearColor NewActiveColor = FLinearColor::MakeRandomColor(); + FLinearColor NewInactiveColor = NewActiveColor.CopyWithNewOpacity(0.1)/2.5f; + + ColorMapActive.Add(AssetId, NewActiveColor); + ColorMapInactive.Add(AssetId, NewInactiveColor); + } + + const FLinearColor& ActiveColor = ColorMapActive[AssetId]; + const FLinearColor& InactiveColor = ColorMapInactive[AssetId]; + + bool IsSelected = (EditedComponent == Component) || (Component->IsSelected()); + + // Draw point and set hit box for it. + PDI->SetHitProxy(new HHoudiniHandleVisProxy(HandleComponent)); + { + static const float GrabHandleSizeActive = 24.0f; + static const float GrabHandleSizeInactive = 18.0f; + + PDI->DrawPoint(HandleComponent->GetComponentTransform().GetLocation(), IsSelected ? ActiveColor : InactiveColor, IsSelected ? GrabHandleSizeActive : GrabHandleSizeInactive, SDPG_Foreground); + } + + if (HandleComponent->HandleType == EHoudiniHandleType::Bounder) + { + // Also draw the scale box for bounder handle + FTransform BoxTransform = HandleComponent->GetComponentTransform(); + const double BoxRad = 50.0; + const FBox Box(FVector3d(-BoxRad, -BoxRad, -BoxRad), FVector3d(BoxRad, BoxRad, BoxRad)); + DrawWireBox(PDI, BoxTransform.ToMatrixWithScale(), Box, IsSelected ? ActiveColor : InactiveColor, SDPG_Foreground); + } + + PDI->SetHitProxy(nullptr); +} + +void +FHoudiniHandleComponentVisualizer::DrawVisualizationHUD( + const UActorComponent* Component, + const FViewport* Viewport, + const FSceneView* View, + FCanvas* Canvas) +{ + FComponentVisualizer::DrawVisualizationHUD(Component, Viewport, View, Canvas); + if (Canvas == nullptr || View == nullptr) + { + return; + } + + const UHoudiniHandleComponent* HandleComponent = Cast(Component); + if (!HandleComponent) + return; + + bool IsSelected = (EditedComponent == Component) || (Component->IsSelected()); + + // Get the Handle location in screen space + FVector Location = HandleComponent->GetComponentTransform().GetLocation(); + FPlane Plane(0, 0, 0, 0); + Plane = View->Project(Location); + const FVector Position(Plane); + + // Calculate the draw position + const FIntRect CanvasRect = Canvas->GetViewRect(); + const float HalfX = CanvasRect.Width() / 2.f; + const float HalfY = CanvasRect.Height() / 2.f; + float DrawPositionX = FMath::FloorToFloat(HalfX + Position.X * HalfX); + float DrawPositionY = FMath::FloorToFloat(HalfY + -1.f * Position.Y * HalfY); + + // Add a slight offset to avoid overlaping the text to the handle itself + DrawPositionX += 24.0f; + + // Get the active/inactive color for that handle + const FLinearColor& Color = IsSelected ? FLinearColor::White : FLinearColor::Gray; + + // Get the handle name and type + FString Text = HandleComponent->GetHandleName(); + switch (HandleComponent->GetHandleType()) + { + case EHoudiniHandleType::Xform: + Text += "\n(Transform Handle)"; + break; + + case EHoudiniHandleType::Bounder: + Text += "\n(Bounder Handle)"; + break; + + default: + break; + } + + // Draw the handle text + Canvas->DrawShadowedString(DrawPositionX, DrawPositionY, *Text, GEngine->GetLargeFont(), Color, FLinearColor::Black); +} + +bool +FHoudiniHandleComponentVisualizer::VisProxyHandleClick( + FEditorViewportClient* InViewportClient, + HComponentVisProxy* VisProxy, + const FViewportClick& Click) +{ + bEditing = false; + + bAllowTranslate = false; + bAllowRotation = false; + bAllowScale = false; + + if (VisProxy && VisProxy->Component.IsValid()) + { + const UHoudiniHandleComponent * Component = + CastChecked(VisProxy->Component.Get()); + + const TArray &XformParms = Component->XformParms; + + if (!Component->CheckHandleValid()) + return bEditing; + + EditedComponent = const_cast(Component); + if (Component) + { + if (VisProxy->IsA(HHoudiniHandleVisProxy::StaticGetType())) + bEditing = true; + + bAllowTranslate = + XformParms[int32(EXformParameter::TX)]->AssetParameter || + XformParms[int32(EXformParameter::TY)]->AssetParameter || + XformParms[int32(EXformParameter::TZ)]->AssetParameter; + + bAllowRotation = + XformParms[int32(EXformParameter::RX)]->AssetParameter || + XformParms[int32(EXformParameter::RY)]->AssetParameter || + XformParms[int32(EXformParameter::RZ)]->AssetParameter; + + bAllowScale = + XformParms[int32(EXformParameter::SX)]->AssetParameter || + XformParms[int32(EXformParameter::SY)]->AssetParameter || + XformParms[int32(EXformParameter::SZ)]->AssetParameter; + } + } + + return bEditing; +} + +void +FHoudiniHandleComponentVisualizer::EndEditing() +{ + EditedComponent = nullptr; +} + +bool +FHoudiniHandleComponentVisualizer::GetWidgetLocation( + const FEditorViewportClient * ViewportClient, + FVector & OutLocation) const +{ + if (EditedComponent) + { + OutLocation = EditedComponent->GetComponentTransform().GetLocation(); + return true; + } + + return false; +} + +bool +FHoudiniHandleComponentVisualizer::GetCustomInputCoordinateSystem( + const FEditorViewportClient * ViewportClient, + FMatrix & OutMatrix) const +{ + if (EditedComponent && ViewportClient->GetWidgetMode() == UE::Widget::EWidgetMode::WM_Scale) + { + OutMatrix = FRotationMatrix::Make(EditedComponent->GetComponentTransform().GetRotation()); + return true; + } + else + { + return false; + } +} + +bool +FHoudiniHandleComponentVisualizer::HandleInputDelta( + FEditorViewportClient* ViewportClient, + FViewport* Viewport, + FVector& DeltaTranslate, + FRotator& DeltaRotate, + FVector& DeltaScale) +{ + if (!EditedComponent) + return false; + + if (!DeltaTranslate.IsZero() && bAllowTranslate) + { + EditedComponent->SetRelativeLocation(EditedComponent->GetRelativeTransform().GetLocation() + DeltaTranslate); + } + + if (!DeltaRotate.IsZero() && bAllowRotation) + { + EditedComponent->SetRelativeRotation(DeltaRotate.Quaternion() * EditedComponent->GetRelativeTransform().GetRotation()); + } + + if (!DeltaScale.IsZero() && bAllowScale) + { + EditedComponent->SetRelativeScale3D(EditedComponent->GetRelativeTransform().GetScale3D() + DeltaScale); + } + + return true; +} + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngineEditor/Private/HoudiniHandleComponentVisualizer.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "ComponentVisualizer.h" +#include "Framework/Commands/Commands.h" +#include "Framework/Commands/UICommandList.h" + +#include "Components/ActorComponent.h" +#include "HoudiniHandleComponent.h" + +// Base class for clickable editing proxies. +struct HHoudiniHandleVisProxy : public HComponentVisProxy +{ + DECLARE_HIT_PROXY(); + HHoudiniHandleVisProxy(const UActorComponent * InComponent); + + virtual EMouseCursor::Type GetMouseCursor() override + { + return EMouseCursor::CardinalCross; + } +}; + +// Define commands for our component visualizer +class FHoudiniHandleComponentVisualizerCommands : public TCommands< FHoudiniHandleComponentVisualizerCommands > +{ +public: + + // Constructor. + FHoudiniHandleComponentVisualizerCommands(); + + // Register commands. + virtual void RegisterCommands() override; + +public: + + // Command for adding a control point. + TSharedPtr< FUICommandInfo > CommandAddControlPoint; + + // Command for deleting a control point. + TSharedPtr< FUICommandInfo > CommandDeleteControlPoint; +}; + + +// Our handle visualizer. +class FHoudiniHandleComponentVisualizer : public FComponentVisualizer +{ +public: + FHoudiniHandleComponentVisualizer(); + + virtual ~FHoudiniHandleComponentVisualizer(); + + // FComponentVisualizer methods. + + // Draw visualization for the given component. + virtual void DrawVisualization( + const UActorComponent * Component, + const FSceneView * View, + FPrimitiveDrawInterface * PDI) override; + + // Draw visualization HUD for the given component (on screen text). + virtual void DrawVisualizationHUD( + const UActorComponent* Component, + const FViewport* Viewport, + const FSceneView* View, + FCanvas* Canvas) override; + + // Handle a click on a registered hit box. + virtual bool VisProxyHandleClick( + FEditorViewportClient* InViewportClient, + HComponentVisProxy* VisProxy, + const FViewportClick& Click) override; + + virtual void EndEditing() override; + + // Returns location of a gizmo widget. + virtual bool GetWidgetLocation( + const FEditorViewportClient* ViewportClient, FVector& OutLocation) const override; + + // + virtual bool GetCustomInputCoordinateSystem( + const FEditorViewportClient * ViewportClient, FMatrix & OutMatrix) const override; + + // Handle input change. + virtual bool HandleInputDelta( + FEditorViewportClient* ViewportClient, + FViewport* Viewport, + FVector& DeltaTranslate, + FRotator& DeltaRotate, + FVector& DeltaScale) override; + + // + void SetEditedComponent(UHoudiniHandleComponent* InComponent) { EditedComponent = InComponent; }; + + // + void ClearEditedComponent() { EditedComponent = nullptr; }; + + +protected: + + // Visualizer actions. + TSharedPtr VisualizerActions; + + // Houdini component which is being edited. + UHoudiniHandleComponent* EditedComponent; + + // Is set to true if we are editing. + uint32 bEditing : 1; + uint32 bAllowTranslate : 1; + uint32 bAllowRotation : 1; + uint32 bAllowScale : 1; +}; +``` + +`Source/HoudiniEngineEditor/Private/HoudiniHandleDetails.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniHandleDetails.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniHandleComponent.h" +#include "HoudiniHandleTranslator.h" +#include "HoudiniHandleComponentVisualizer.h" +#include "HoudiniEngineDetails.h" + +#include "DetailCategoryBuilder.h" +#include "DetailWidgetRow.h" +#include "IDetailGroup.h" +#include "Widgets/Input/SVectorInputBox.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Images/SImage.h" +#include "UnrealEdGlobals.h" +#include "Editor/UnrealEdEngine.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +void +FHoudiniHandleDetails::CreateWidget(IDetailCategoryBuilder & HouHandleCategory, const TArray> &InHandles) +{ + if (InHandles.Num() <= 0) + return; + + const TWeakObjectPtr& MainHandle = InHandles[0]; + if (!IsValidWeakPointer(MainHandle)) + return; + + + FString HandleNameStr = MainHandle->GetHandleName() + TEXT(" (") + GetHandleTypeString(MainHandle->GetHandleType()) + TEXT(" )"); + FName HandleName = FName(*HandleNameStr); + IDetailGroup& Group = HouHandleCategory.AddGroup(HandleName, FText::FromString(HandleNameStr), false, false); + + // Create a widget row for this handle + FDetailWidgetRow& Row = Group.AddWidgetRow(); + + CreateNameWidget(Row); + + // Create value widget + TSharedRef ValueWidgetVerticalBox = SNew(SVerticalBox); + + // Translate + auto OnLocationChangedLambda = [MainHandle](float Val, int32 Axis) + { + if (!IsValidWeakPointer(MainHandle)) + return; + + FVector3d Location = MainHandle->GetRelativeTransform().GetLocation(); + + if (Axis == 0) + Location.X = Val; + else if (Axis == 1) + Location.Y = Val; + else + Location.Z = Val; + + MainHandle->SetRelativeLocation(Location); + FHoudiniHandleTranslator::UpdateTransformParameters(MainHandle.Get()); + }; + + auto RevertLocationToDefault = [MainHandle]() + { + if (!IsValidWeakPointer(MainHandle)) + return FReply::Handled(); + + FVector3d DefaultLocation = FVector3d::ZeroVector; + MainHandle->SetRelativeLocation(DefaultLocation); + FHoudiniHandleTranslator::UpdateTransformParameters(MainHandle.Get()); + + return FReply::Handled(); + }; + + ValueWidgetVerticalBox->AddSlot().Padding(2.0f, 2.0f, 5.0f, 2.0f).VAlign(VAlign_Top) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [ + SNew(SVectorInputBox) + .bColorAxisLabels(true) + .X_Lambda([MainHandle]() {return MainHandle->GetRelativeTransform().GetLocation().X; }) + .Y_Lambda([MainHandle]() {return MainHandle->GetRelativeTransform().GetLocation().Y; }) + .Z_Lambda([MainHandle]() {return MainHandle->GetRelativeTransform().GetLocation().Z; }) + .OnXCommitted_Lambda([OnLocationChangedLambda](float Val, ETextCommit::Type TextCommitType) + { + OnLocationChangedLambda(Val, 0); + }) + .OnYCommitted_Lambda([OnLocationChangedLambda](float Val, ETextCommit::Type TextCommitType) + { + OnLocationChangedLambda(Val, 1); + }) + .OnZCommitted_Lambda([OnLocationChangedLambda](float Val, ETextCommit::Type TextCommitType) + { + OnLocationChangedLambda(Val, 2); + }) + ] + + + SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f).VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertToDefault", "Revert to default")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility_Lambda([MainHandle]() + { + if (!IsValidWeakPointer(MainHandle)) + return EVisibility::Hidden; + + if (MainHandle->GetRelativeLocation() == FVector::ZeroVector) + return EVisibility::Hidden; + + return EVisibility::Visible; + }) + .OnClicked_Lambda(RevertLocationToDefault) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ]; + + + // Rotation + auto OnRotationChangedLambda = [MainHandle](float Val, int32 Axis) + { + if (!IsValidWeakPointer(MainHandle)) + return; + + FRotator Rotation = MainHandle->GetRelativeTransform().Rotator(); + if (Axis == 0) + Rotation.Roll = Val; + else if (Axis == 1) + Rotation.Pitch = Val; + else + Rotation.Yaw = Val; + MainHandle->SetRelativeRotation(Rotation.Quaternion()); + FHoudiniHandleTranslator::UpdateTransformParameters(MainHandle.Get()); + }; + + auto RevertRotationToDefault = [MainHandle]() + { + if (!IsValidWeakPointer(MainHandle)) + return FReply::Handled(); + + MainHandle->SetRelativeRotation(FQuat::Identity); + FHoudiniHandleTranslator::UpdateTransformParameters(MainHandle.Get()); + + return FReply::Handled(); + }; + + ValueWidgetVerticalBox->AddSlot().Padding(2.0f, 2.0f, 5.0f, 2.0f).VAlign(VAlign_Top) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [ + SNew(SVectorInputBox) + .bColorAxisLabels(true) + .X_Lambda([MainHandle]() {return MainHandle->GetRelativeTransform().Rotator().Roll; }) + .Y_Lambda([MainHandle]() {return MainHandle->GetRelativeTransform().Rotator().Pitch; }) + .Z_Lambda([MainHandle]() {return MainHandle->GetRelativeTransform().Rotator().Yaw; }) + .OnXCommitted_Lambda([OnRotationChangedLambda](float Val, ETextCommit::Type TextCommitType) + { + OnRotationChangedLambda(Val, 0); + }) + .OnYCommitted_Lambda([OnRotationChangedLambda](float Val, ETextCommit::Type TextCommitType) + { + OnRotationChangedLambda(Val, 1); + }) + .OnZCommitted_Lambda([OnRotationChangedLambda](float Val, ETextCommit::Type TextCommitType) + { + OnRotationChangedLambda(Val, 2); + }) + ] + + + SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f).VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertToDefault", "Revert to default")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility_Lambda([MainHandle]() + { + if (!IsValidWeakPointer(MainHandle)) + return EVisibility::Hidden; + + if (MainHandle->GetRelativeTransform().GetRotation() == FQuat::Identity) + return EVisibility::Hidden; + + return EVisibility::Visible; + }) + .OnClicked_Lambda(RevertRotationToDefault) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ]; + + + // Scale + auto OnScaleChangedLambda = [MainHandle](float Val, int32 Axis) + { + if (!IsValidWeakPointer(MainHandle)) + return; + + FVector3d Scale = MainHandle->GetRelativeTransform().GetScale3D(); + + if (Axis == 0) + Scale.X = Val; + else if (Axis == 1) + Scale.Y = Val; + else + Scale.Z = Val; + + MainHandle->SetRelativeScale3D(Scale); + FHoudiniHandleTranslator::UpdateTransformParameters(MainHandle.Get()); + }; + + auto RevertScaleToDefault = [MainHandle]() + { + if (!IsValidWeakPointer(MainHandle)) + return FReply::Handled(); + + MainHandle->SetRelativeScale3D(FVector::OneVector); + + return FReply::Handled(); + }; + + ValueWidgetVerticalBox->AddSlot().Padding(2.0f, 2.0f, 5.0f, 2.0f).VAlign(VAlign_Top) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [ + SNew(SVectorInputBox) + .bColorAxisLabels(true) + .X_Lambda([MainHandle]() {return MainHandle->GetRelativeTransform().GetScale3D().X; }) + .Y_Lambda([MainHandle]() {return MainHandle->GetRelativeTransform().GetScale3D().Y; }) + .Z_Lambda([MainHandle]() {return MainHandle->GetRelativeTransform().GetScale3D().Z; }) + .OnXCommitted_Lambda([OnScaleChangedLambda](float Val, ETextCommit::Type TextCommitType) + { + OnScaleChangedLambda(Val, 0); + }) + .OnYCommitted_Lambda([OnScaleChangedLambda](float Val, ETextCommit::Type TextCommitType) + { + OnScaleChangedLambda(Val, 1); + }) + .OnZCommitted_Lambda([OnScaleChangedLambda](float Val, ETextCommit::Type TextCommitType) + { + OnScaleChangedLambda(Val, 2); + }) + ] + + + SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f).VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertToDefault", "Revert to default")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility_Lambda([MainHandle]() + { + if (!IsValidWeakPointer(MainHandle)) + return EVisibility::Hidden; + + if (MainHandle->GetRelativeTransform().GetScale3D() == FVector3d::OneVector) + return EVisibility::Hidden; + + return EVisibility::Visible; + }) + .OnClicked_Lambda(RevertScaleToDefault) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ]; + + + Row.ValueWidget.Widget = ValueWidgetVerticalBox; + Row.ValueWidget.MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH); + + + auto OnMouseEnterLambda = [MainHandle](const FGeometry& Geometry, const FPointerEvent& Event) + { + if (!IsValidWeakPointer(MainHandle)) + return; + + TSharedPtr Visualizer = GUnrealEd->FindComponentVisualizer(UHoudiniHandleComponent::StaticClass()->GetFName()); + TSharedPtr HandleVisualizer = StaticCastSharedPtr(Visualizer); + + if (HandleVisualizer.IsValid()) + { + HandleVisualizer->SetEditedComponent(MainHandle.Get()); + } + }; + + auto OnMouseLeaveLambda = [MainHandle](const FPointerEvent& Event) + { + if (!IsValidWeakPointer(MainHandle)) + return; + + TSharedPtr Visualizer = GUnrealEd->FindComponentVisualizer(UHoudiniHandleComponent::StaticClass()->GetFName()); + TSharedPtr HandleVisualizer = StaticCastSharedPtr(Visualizer); + + if (HandleVisualizer.IsValid()) + { + HandleVisualizer->SetEditedComponent(nullptr); + } + }; + + // Set on mouse leave UI widget callback functions + Row.NameWidget.Widget->SetOnMouseEnter(FNoReplyPointerEventHandler::CreateLambda(OnMouseEnterLambda)); + Row.ValueWidget.Widget->SetOnMouseEnter(FNoReplyPointerEventHandler::CreateLambda(OnMouseEnterLambda)); + + Row.NameWidget.Widget->SetOnMouseLeave(FSimpleNoReplyPointerEventHandler::CreateLambda(OnMouseLeaveLambda)); + Row.ValueWidget.Widget->SetOnMouseLeave(FSimpleNoReplyPointerEventHandler::CreateLambda(OnMouseLeaveLambda)); +} + +void +FHoudiniHandleDetails::CreateNameWidget(FDetailWidgetRow& Row) +{ + TSharedRef VerticalBox = SNew(SVerticalBox); + + VerticalBox->AddSlot().AutoHeight().Padding(2.0f, 5.0f, 5.0f, 5.0f).VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString("Translate")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ]; + + VerticalBox->AddSlot().AutoHeight().Padding(2.0f, 5.0f, 5.0f, 5.0f).VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString("Rotation")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ]; + + VerticalBox->AddSlot().AutoHeight().Padding(2.0f, 5.0f, 5.0f, 5.0f).VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString("Scale")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ]; + + + Row.NameWidget.Widget = VerticalBox; +} + +FString +FHoudiniHandleDetails::GetHandleTypeString(const EHoudiniHandleType& HandleType) +{ + switch (HandleType) + { + case EHoudiniHandleType::Bounder: + return FString("Bounder"); + case EHoudiniHandleType::Xform: + return FString("Xform"); + case EHoudiniHandleType::Unsupported: + return FString("Unsupported"); + + default: + break; + } + + return FString(""); +} + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngineEditor/Private/HoudiniHandleDetails.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "DetailWidgetRow.h" + +class UHoudiniHandleComponent; +class IDetailCategoryBuilder; +enum class EHoudiniHandleType : uint8; + +class FHoudiniHandleDetails : public TSharedFromThis +{ +public: + static void CreateWidget(IDetailCategoryBuilder & HouHandleCategory, const TArray> &InHandles); + + static void CreateNameWidget(FDetailWidgetRow& Row); + + static FString GetHandleTypeString(const EHoudiniHandleType& HandleType); +}; +``` + +`Source/HoudiniEngineEditor/Private/HoudiniInputDetails.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniInputDetails.h" + +#include "HoudiniEngineEditorPrivatePCH.h" + +#include "HoudiniAssetActor.h" +#include "HoudiniAssetBlueprintComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniInput.h" +#include "HoudiniInputWidgets.h" +#include "HoudiniLandscapeTranslator.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniEngineDetails.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniEngineStyle.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniInputTranslator.h" +#include "HoudiniPackageParams.h" +#if defined(HOUDINI_USE_PCG) + #include "HoudiniPCGCookable.h" +#endif +#include "HoudiniSplineComponentVisualizer.h" +#include "UnrealObjectInputRuntimeUtils.h" + +#include "ActorTreeItem.h" +#include "AssetRegistry/AssetData.h" +#include "DetailLayoutBuilder.h" +#include "DetailCategoryBuilder.h" +#include "DetailWidgetRow.h" +#include "Editor.h" +#include "Editor/PropertyEditor/Private/SDetailsViewBase.h" +#include "Editor/PropertyEditor/Public/PropertyCustomizationHelpers.h" +#include "Editor/UnrealEd/Public/AssetThumbnail.h" +#include "Editor/UnrealEdEngine.h" +#include "Engine/Selection.h" +#include "Engine/SkeletalMesh.h" +#include "EngineUtils.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Framework/SlateDelegates.h" +#include "IDetailCustomization.h" +#include "Modules/ModuleManager.h" +#include "SAssetDropTarget.h" +#include "SceneOutlinerModule.h" +#include "ScopedTransaction.h" +#include "UnrealEdGlobals.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Input/SEditableText.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Input/SNumericEntryBox.h" +#include "Widgets/Input/SRotatorInputBox.h" +#include "Widgets/Input/SVectorInputBox.h" +#include "Widgets/Layout/SExpandableArea.h" +#include "Widgets/Layout/SUniformGridPanel.h" +#include "Widgets/SWidget.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "GeometryCollection/GeometryCollectionObject.h" +#else + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionObject.h" +#endif + + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +// Customized TextBlock to show 'editing...' text if this Houdini Spline Component is being edited +class SCurveEditingTextBlock : public STextBlock +{ +public: + UHoudiniSplineComponent* HoudiniSplineComponent; + TSharedPtr HoudiniSplineComponentVisualizer; +public: + virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, + FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override + { + if (!HoudiniSplineComponentVisualizer.IsValid() || !HoudiniSplineComponent) + return LayerId; + + if (HoudiniSplineComponentVisualizer->GetEditedHoudiniSplineComponent() != HoudiniSplineComponent) + return LayerId; + + return STextBlock::OnPaint(Args, AllottedGeometry, MyClippingRect, + OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); + } +}; + +// Customized SVerticalBox used by for creating the spline point selection +// and control UI. When a spline is changed, or the selected points for a spline +// are changed, then this refreshes the UI so that the transform controls for +// the points are updated accordingly. +class SCurveListenerVerticalBox : public SVerticalBox +{ +public: + TSharedPtr HoudiniSplineComponentVisualizer; + IDetailCategoryBuilder* CategoryBuilder; + mutable UHoudiniSplineComponent* PrevSplineComponent; + mutable FTransform PrevSplineTransform; + mutable TArray PrevEditedControlPointsIndexes; + +public: + virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, + FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override + { + if (!HoudiniSplineComponentVisualizer.IsValid()) + return LayerId; + + bool bRefreshUI = false; + + if (HoudiniSplineComponentVisualizer->GetEditedHoudiniSplineComponent() != PrevSplineComponent) + { + bRefreshUI = true; + PrevSplineComponent = HoudiniSplineComponentVisualizer->GetEditedHoudiniSplineComponent(); + if (PrevSplineComponent) + { + PrevEditedControlPointsIndexes = PrevSplineComponent->EditedControlPointsIndexes; + PrevSplineTransform = PrevSplineComponent->GetComponentTransform(); + } + } + else if (PrevSplineComponent) + { + if (PrevSplineComponent->HasChanged()) + { + bRefreshUI = true; + } + else if (PrevSplineTransform.GetLocation() != PrevSplineComponent->GetComponentTransform().GetLocation() || + PrevSplineTransform.GetRotation() != PrevSplineComponent->GetComponentTransform().GetRotation()) + { + bRefreshUI = true; + PrevSplineTransform = PrevSplineComponent->GetComponentTransform(); + } + else { + TArray& CurEditedControlPointIndexes = PrevSplineComponent->EditedControlPointsIndexes; + + if (CurEditedControlPointIndexes.Num() == PrevEditedControlPointsIndexes.Num()) + { + for (int32 Idx = 0; Idx < CurEditedControlPointIndexes.Num(); ++Idx) + { + if (CurEditedControlPointIndexes[Idx] != PrevEditedControlPointsIndexes[Idx]) { + bRefreshUI = true; + break; + } + } + } + else + bRefreshUI = true; + + if (bRefreshUI) + PrevEditedControlPointsIndexes = CurEditedControlPointIndexes; + } + } + + if (bRefreshUI) + { + if (GEditor) + GEditor->RedrawAllViewports(); + + if (CategoryBuilder->IsParentLayoutValid()) + CategoryBuilder->GetParentLayout().ForceRefreshDetails(); + } + + return LayerId; + } +}; + +void +FHoudiniInputDetails::CreateWidget( + IDetailCategoryBuilder& HouInputCategory, + const TArray>& InInputs, + FDetailWidgetRow* InputRow) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + if (!IsValidWeakPointer(MainInput)) + return; + + // Get thumbnail pool for this builder. + TSharedPtr< FAssetThumbnailPool > AssetThumbnailPool = HouInputCategory.GetParentLayout().GetThumbnailPool(); + + EHoudiniInputType MainInputType = MainInput->GetInputType(); + UHoudiniAssetComponent* HAC = MainInput->GetTypedOuter(); + + // Create a widget row, or get the given row. + FDetailWidgetRow* Row = InputRow; + FString InputRowString = MainInput->GetInputLabel() + " " + MainInput->GetInputName(); + Row = InputRow == nullptr ? &(HouInputCategory.AddCustomRow(FText::FromString(InputRowString))) : InputRow; + if (!Row) + return; + + // Create the standard input name widget if this is not a operator path parameter. + // Operator path parameter's name widget is handled by HoudiniParameterDetails. + if (!InputRow) + CreateNameWidget(MainInput, *Row, true, InInputs.Num()); + + // Create a vertical Box for storing the UI + TSharedRef VerticalBox = SNew(SVerticalBox); + + // ComboBox : Input Type +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + TSharedPtr DetailsViewShared = HouInputCategory.GetParentLayout().GetDetailsViewSharedPtr(); + const IDetailsView* DetailsView = DetailsViewShared.Get(); +#else + const IDetailsView* DetailsView = HouInputCategory.GetParentLayout().GetDetailsView(); +#endif + + AddInputTypeComboBox(HouInputCategory, VerticalBox, InInputs, DetailsView); + + switch (MainInput->GetInputType()) + { + case EHoudiniInputType::Geometry: + { + AddGeometryInputUI(HouInputCategory, VerticalBox, InInputs, AssetThumbnailPool); + } + break; + + case EHoudiniInputType::Curve: + { + AddCurveInputUI(HouInputCategory, VerticalBox, InInputs, AssetThumbnailPool); + } + break; + + case EHoudiniInputType::World: + { + AddWorldInputUI(HouInputCategory, VerticalBox, InInputs, DetailsView); + } + break; + } + + + Row->ValueWidget.Widget = VerticalBox; + + Row->ValueWidget.MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH); + //Row.ValueWidget.Widget->SetEnabled(!MainParam->IsDisabled()); + +} + +void +FHoudiniInputDetails::CreateNameWidget( + const TWeakObjectPtr& InInput, FDetailWidgetRow & Row, bool bLabel, int32 InInputCount) +{ + if (!IsValidWeakPointer(InInput)) + return; + + FString InputLabelStr = InInput->GetInputLabel(); + if (InInputCount > 1) + { + InputLabelStr += TEXT(" (") + FString::FromInt(InInputCount) + TEXT(")"); + } + + const FText & FinalInputLabelText = bLabel ? FText::FromString(InputLabelStr) : FText::GetEmpty(); + FText InputTooltip = GetInputTooltip(InInput); + { + Row.NameWidget.Widget = + SNew(STextBlock) + .Text(FinalInputLabelText) + .ToolTipText(InputTooltip) + .Font(_GetEditorStyle().GetFontStyle(!InInput->HasChanged() ? TEXT("PropertyWindow.NormalFont") : TEXT("PropertyWindow.BoldFont"))); + } +} + +FText +FHoudiniInputDetails::GetInputTooltip(const TWeakObjectPtr& InParam) +{ + // TODO: Add input tooltip + return FText(); +} + +void +FHoudiniInputDetails::AddInputTypeComboBox(IDetailCategoryBuilder& CategoryBuilder, TSharedRef VerticalBox, const TArray>& InInputs, const IDetailsView* DetailsView) +{ + // Get the details view name and locked status + bool bDetailsLocked = false; + FName DetailsPanelName = "LevelEditorSelectionDetails"; + if (DetailsView) + { + DetailsPanelName = DetailsView->GetIdentifier(); + if (DetailsView->IsLocked()) + bDetailsLocked = true; + } + + // Lambda return a FText correpsonding to an input's current type + auto GetInputText = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return FText(); + return FText::FromString(InInput->GetInputTypeAsString()); + }; + + // Lambda for changing inputs type + auto OnSelChanged = [DetailsPanelName](const TArray>& InInputsToUpdate, TSharedPtr InNewChoice) + { + if (!InNewChoice.IsValid()) + return; + + EHoudiniInputType NewInputType = UHoudiniInput::StringToInputType(*InNewChoice.Get()); + if (NewInputType != EHoudiniInputType::World) + { + Helper_CancelWorldSelection(InInputsToUpdate, DetailsPanelName); + } + + if (InInputsToUpdate.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputsToUpdate[0]; + if (!IsValidWeakPointer(MainInput)) + return; + + UHoudiniAssetBlueprintComponent* HAB = MainInput->GetTypedOuter(); + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Input Type"), + MainInput->GetOuter()); + + bool bBlueprintStructureModified = false; + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetInputType() == NewInputType) + continue; + + /* This causes multiple issues. It does not set reset the previous type variable to Invalid sometimes + and it causes re-cook infinitely after few undo changing type. + { + CurInput->SetInputType(NewInputType); + CurInput->Modify(); + } + */ + + { + // Cache the current input type for undo type changing (since new type becomes previous type after undo) + EHoudiniInputType PrevType = CurInput->GetPreviousInputType(); + CurInput->SetPreviousInputType(NewInputType); + + CurInput->Modify(); + CurInput->SetPreviousInputType(PrevType); + CurInput->SetInputType(NewInputType, bBlueprintStructureModified); // pass in false for 2nd parameter in order to avoid creating default curve if empty + } + CurInput->MarkChanged(true); + + FHoudiniEngineEditorUtils::ReselectSelectedActors(); + + } + + if (HAB) + { + if (bBlueprintStructureModified) + HAB->MarkAsBlueprintStructureModified(); + } + + }; + + const TWeakObjectPtr& MainInput = InInputs[0]; + TArray>* SupportedChoices = nullptr; + UHoudiniAssetBlueprintComponent* HAC = MainInput->GetTypedOuter(); + if (HAC) + { + SupportedChoices = FHoudiniEngineEditor::Get().GetBlueprintInputTypeChoiceLabels(); + } +#if defined(HOUDINI_USE_PCG) + else if (UHoudiniPCGCookable* HPCGC = MainInput->GetTypedOuter()) + { + SupportedChoices = FHoudiniEngineEditor::Get().GetPCGInputTypeChoiceLabels(); + } +#endif + else if (UHoudiniCookable* Cookable = MainInput->GetTypedOuter()) + { + if(!Cookable->AssetEditorId.IsNone()) + SupportedChoices = FHoudiniEngineEditor::Get().GetAssetEditorInputTypeChoiceLabels(); + else + SupportedChoices = FHoudiniEngineEditor::Get().GetInputTypeChoiceLabels(); + } + else + { + SupportedChoices = FHoudiniEngineEditor::Get().GetInputTypeChoiceLabels(); + } + + TSharedPtr InitiallySelectedType = (*SupportedChoices)[0]; + for (int Idx = 0; Idx < SupportedChoices->Num(); ++Idx) + { + EHoudiniInputType CurType = UHoudiniInput::StringToInputType(*(*SupportedChoices)[Idx]); + if (CurType == MainInput->GetInputType()) + { + InitiallySelectedType = (*SupportedChoices)[Idx]; + break; + } + } + + // ComboBox : Input Type + TSharedPtr>> ComboBoxInputType; + TSharedPtr RebuildImage; + VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .FillWidth(1.0f) + //.FillWidth(0.75f) + //.AutoWidth() + [ + SAssignNew(ComboBoxInputType, SComboBox>) + .OptionsSource(SupportedChoices) + .InitiallySelectedItem(InitiallySelectedType) + .OnGenerateWidget_Lambda( + [](TSharedPtr< FString > ChoiceEntry) + { + FText ChoiceEntryText = FText::FromString(*ChoiceEntry); + return SNew(STextBlock) + .Text(ChoiceEntryText) + .ToolTipText(ChoiceEntryText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + }) + .OnSelectionChanged_Lambda([=](TSharedPtr NewChoice, ESelectInfo::Type SelectType) + { + return OnSelChanged(InInputs, NewChoice); + }) + [ + SNew( STextBlock ) + .Text_Lambda([=]() + { + return GetInputText(MainInput); + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .MaxWidth(16.0f) + .AutoWidth() + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RecookInput", "Recook this input only.")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([InInputs]() + { + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + // Force the input to reupload its data + CurInput->MarkChanged(true); + //CurInput->MarkDataUploadNeeded(true); + CurInput->MarkAllInputObjectsChanged(true); + } + + return FReply::Handled(); + }) + [ + SNew(SImage) + .Image(FHoudiniEngineStyle::Get()->GetBrush("HoudiniEngine._RebuildAll")) + ] + ] + ]; +} + +void +FHoudiniInputDetails::AddKeepWorldTransformCheckBox(TSharedRef< SVerticalBox > VerticalBox, const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + if (!IsValidWeakPointer(MainInput)) + return; + + // Lambda returning a CheckState from the input's current KeepWorldTransform state + auto IsCheckedKeepWorldTransform = [](const TWeakObjectPtr& InInput) + { + return InInput.IsValid() && InInput->GetKeepWorldTransform() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + // Lambda for changing KeepWorldTransform state + auto CheckStateChangedKeepWorldTransform = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + if (MainInput->GetKeepWorldTransform() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Keep World Transform"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetKeepWorldTransform() == bNewState) + continue; + + CurInput->Modify(); + + CurInput->SetKeepWorldTransform(bNewState); + CurInput->MarkChanged(true); + } + }; + + + // Checkbox : Keep World Transform + TSharedPtr CheckBoxTranformType; + VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SAssignNew(CheckBoxTranformType, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("KeepWorldTransformCheckbox", "Keep World Transform")) + .ToolTipText(LOCTEXT("KeepWorldTransformCheckboxTip", "Set this Input's object_merge Transform Type to INTO_THIS_OBJECT. If unchecked, it will be set to NONE.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([=]() + { + return IsCheckedKeepWorldTransform(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedKeepWorldTransform(InInputs, NewState); + }) + ]; + + // the checkbox is read only for geo inputs + if (MainInput->GetInputType() == EHoudiniInputType::Geometry) + CheckBoxTranformType->SetEnabled(false); + + // Checkbox is read only if the input is an object-path parameter + //if (MainInput->IsObjectPathParameter() ) + // CheckBoxTranformType->SetEnabled(false); +} + +void +FHoudiniInputDetails::AddPackBeforeMergeCheckbox(TSharedRef< SVerticalBox > VerticalBox, const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + if (!IsValidWeakPointer(MainInput)) + return; + + // Lambda returning a CheckState from the input's current PackBeforeMerge state + auto IsCheckedPackBeforeMerge = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->GetPackBeforeMerge() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + // Lambda for changing PackBeforeMerge state + auto CheckStateChangedPackBeforeMerge = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->GetPackBeforeMerge() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Pack before merge"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetPackBeforeMerge() == bNewState) + continue; + + CurInput->Modify(); + + CurInput->SetPackBeforeMerge(bNewState); + CurInput->MarkChanged(true); + } + }; + + TSharedPtr CheckBoxPackBeforeMerge; + VerticalBox->AddSlot() + .Padding( 2, 2, 5, 2 ) + .AutoHeight() + [ + SAssignNew(CheckBoxPackBeforeMerge, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("PackBeforeMergeCheckbox", "Pack Geometry Before Merging")) + .ToolTipText(LOCTEXT( "PackBeforeMergeCheckboxTip", "Pack each separate piece of geometry before merging them into the input.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([=]() + { + return IsCheckedPackBeforeMerge(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedPackBeforeMerge(InInputs, NewState); + }) + ]; +} + +void +FHoudiniInputDetails::AddExportCheckboxes(TSharedRef< SVerticalBox > VerticalBox, const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + if (!IsValidWeakPointer(MainInput)) + return; + + // Lambda returning a CheckState from the input's current ExportMainGeoemtry state + auto IsCheckedExportMainGeo = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->GetExportMainGeometry() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + // Lambda returning a CheckState from the input's current ExportLODs state + auto IsCheckedExportLODs = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->GetExportLODs() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + // Lambda returning a CheckState from the input's current ExportSockets state + auto IsCheckedExportSockets = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->GetExportSockets() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + // Lambda returning a CheckState from the input's current ExportColliders state + auto IsCheckedExportColliders = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->GetExportColliders() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto IsCheckedMergeSplineMeshComponents = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->IsMergeSplineMeshComponentsEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto IsCheckedPreferNanite = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->GetPreferNaniteFallbackMesh() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto IsCheckedExportMaterialParameters = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->GetExportMaterialParameters() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + // Lambda for changing ExportMainGeometry state + auto CheckStateChangedExportMainGeo = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->GetExportMainGeometry() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Main Geometry"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetExportMainGeometry() == bNewState) + continue; + + CurInput->Modify(); + + CurInput->SetExportMainGeometry(bNewState); + CurInput->MarkChanged(true); + CurInput->MarkAllInputObjectsChanged(true); + } + }; + + // Lambda for changing ExportLODs state + auto CheckStateChangedExportLODs = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->GetExportLODs() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export LODs"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetExportLODs() == bNewState) + continue; + + CurInput->Modify(); + + CurInput->SetExportLODs(bNewState); + CurInput->MarkChanged(true); + CurInput->MarkAllInputObjectsChanged(true); + } + }; + + // Lambda for changing ExportSockets state + auto CheckStateChangedExportSockets = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->GetExportSockets() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Sockets"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetExportSockets() == bNewState) + continue; + + CurInput->Modify(); + + CurInput->SetExportSockets(bNewState); + CurInput->MarkChanged(true); + CurInput->MarkAllInputObjectsChanged(true); + } + }; + + // Lambda for changing ExportColliders state + auto CheckStateChangedExportColliders = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->GetExportColliders() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Colliders"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetExportColliders() == bNewState) + continue; + + CurInput->Modify(); + + CurInput->SetExportColliders(bNewState); + CurInput->MarkChanged(true); + CurInput->MarkAllInputObjectsChanged(true); + } + }; + + auto CheckStateChangedMergeSplineMeshComponents = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->IsMergeSplineMeshComponentsEnabled() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Merge Spline Mesh Components"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->IsMergeSplineMeshComponentsEnabled() == bNewState) + continue; + + CurInput->Modify(); + + CurInput->SetMergeSplineMeshComponents(bNewState); + CurInput->MarkChanged(true); + CurInput->MarkAllInputObjectsChanged(true); + } + }; + + auto CheckStateChangedPreferNanite = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->GetPreferNaniteFallbackMesh() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Use Nanite Fallback Preference"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetPreferNaniteFallbackMesh() == bNewState) + continue; + + CurInput->Modify(); + + CurInput->SetPreferNaniteFallbackMesh(bNewState); + CurInput->MarkChanged(true); + CurInput->MarkAllInputObjectsChanged(true); + } + }; + + // Lambda for changing ExportMaterialParameters state + auto CheckStateChangedExportMaterialParameters = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->GetExportMaterialParameters() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Material Parameters"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetExportMaterialParameters() == bNewState) + continue; + + CurInput->Modify(); + + CurInput->SetExportMaterialParameters(bNewState); + CurInput->MarkChanged(true); + CurInput->MarkAllInputObjectsChanged(true); + } + }; + + + + TSharedPtr CheckBoxExportMainGeo; + VerticalBox->AddSlot() + .Padding( 2, 2, 5, 2 ) + .AutoHeight() + [ + SAssignNew(CheckBoxExportMainGeo, SCheckBox ) + .Content() + [ + SNew( STextBlock ) + .Text( LOCTEXT( "ExportMainGeo", "Export Main Geometry" ) ) + .ToolTipText( LOCTEXT( "ExportMainGeoCheckboxTip", "If enabled, the Main Geometry will be sent to Houdini. Turn this off if you only want to send Collider data, or LOD data to Houdini." ) ) + .Font(_GetEditorStyle().GetFontStyle( TEXT( "PropertyWindow.NormalFont" ) ) ) + ] + .IsChecked_Lambda([=]() + { + return IsCheckedExportMainGeo(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedExportMainGeo(InInputs, NewState); + }) + ]; + + TSharedPtr CheckBoxExportLODs; + VerticalBox->AddSlot() + .Padding( 2, 2, 5, 2 ) + .AutoHeight() + [ + SAssignNew(CheckBoxExportLODs, SCheckBox ) + .Content() + [ + SNew( STextBlock ) + .Text( LOCTEXT( "ExportAllLOD", "Export LODs" ) ) + .ToolTipText( LOCTEXT( "ExportAllLODCheckboxTip", "If enabled, all LOD Meshes in this static mesh will be sent to Houdini." ) ) + .Font(_GetEditorStyle().GetFontStyle( TEXT( "PropertyWindow.NormalFont" ) ) ) + ] + .IsChecked_Lambda([=]() + { + return IsCheckedExportLODs(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedExportLODs(InInputs, NewState); + }) + ]; + + TSharedPtr CheckBoxExportSockets; + VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SAssignNew( CheckBoxExportSockets, SCheckBox ) + .Content() + [ + SNew( STextBlock ) + .Text( LOCTEXT( "ExportSockets", "Export Sockets" ) ) + .ToolTipText( LOCTEXT( "ExportSocketsTip", "If enabled, all Mesh Sockets in this static mesh will be sent to Houdini." ) ) + .Font(_GetEditorStyle().GetFontStyle( TEXT( "PropertyWindow.NormalFont" ) ) ) + ] + .IsChecked_Lambda([=]() + { + return IsCheckedExportSockets(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedExportSockets(InInputs, NewState); + }) + ]; + + + TSharedPtr CheckBoxExportColliders; + VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SAssignNew( CheckBoxExportColliders, SCheckBox ) + .Content() + [ + SNew( STextBlock ) + .Text( LOCTEXT( "ExportColliders", "Export Colliders" ) ) + .ToolTipText( LOCTEXT( "ExportCollidersTip", "If enabled, collision geometry for this static mesh will be sent to Houdini." ) ) + .Font(_GetEditorStyle().GetFontStyle( TEXT( "PropertyWindow.NormalFont" ) ) ) + ] + .IsChecked_Lambda([=]() + { + return IsCheckedExportColliders(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedExportColliders(InInputs, NewState); + }) + ]; + + + TSharedPtr CheckBoxExportMaterialParameters; + VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SAssignNew(CheckBoxExportMaterialParameters, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("ExportMaterialParameters", "Export Material Parameters")) + .ToolTipText(LOCTEXT("ExportMaterialParametersTip", "If enabled, materials parameters on the input objects will be sent to Houdini as primitive attributes.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([=]() + { + return IsCheckedExportMaterialParameters(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedExportMaterialParameters(InInputs, NewState); + }) + ] + ]; + + + TSharedPtr CheckBoxPreferNaniteFallback; + VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SAssignNew(CheckBoxPreferNaniteFallback, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("PreferNaniteFallbackMesh", "Prefer Nanite Fallback Mesh")) + .ToolTipText(LOCTEXT("PreferNaniteFallbackMeshTip", "If enabled, when a Nanite asset is used as input, Houdini will use the fallback mesh if available.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([=]() + { + return IsCheckedPreferNanite(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedPreferNanite(InInputs, NewState); + }) + ] + ]; + + + TSharedPtr CheckBoxMergeSplineMeshComponents; + VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SAssignNew(CheckBoxMergeSplineMeshComponents, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("MergeSplineMeshComponents", "Merge Spline Mesh Components")) + .ToolTipText(LOCTEXT("MergeSplineMeshComponentsTip", "If enabled, spline mesh components from world input Actors are merged into a single static mesh per actor.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .Visibility(EVisibility::Visible) + .IsChecked_Lambda([=]() + { + return IsCheckedMergeSplineMeshComponents(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedMergeSplineMeshComponents(InInputs, NewState); + }) + ] + ]; +} + +void +FHoudiniInputDetails::AddLevelInstanceExportCheckboxes(TSharedRef< SVerticalBox > VerticalBox, const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + if (!IsValidWeakPointer(MainInput)) + return; + + // Lambda returning a CheckState from the input's current bExportLevelInstanceContent state + auto IsExportLevelInstanceContentEnabled = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->IsExportLevelInstanceContentEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + // Lambda for changing bIsExportLevelInstanceContent state + auto CheckStateChangedExportLevelInstanceContent = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + const bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->IsExportLevelInstanceContentEnabled() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Level Instance Content"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->IsExportLevelInstanceContentEnabled() == bNewState) + continue; + + CurInput->Modify(); + + CurInput->SetExportLevelInstanceContent(bNewState); + CurInput->MarkChanged(true); + CurInput->MarkAllInputObjectsChanged(true); + } + }; + + + TSharedPtr CheckBoxExportLevelInstanceContent; + VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SAssignNew(CheckBoxExportLevelInstanceContent, SCheckBox) + .Content() + [ + SNew( STextBlock ) + .Text( LOCTEXT( "ExportLevelInstanceContent", "Export Level Instance Content" ) ) + .ToolTipText( LOCTEXT( "ExportLevelInstanceContentCheckboxTip", "If enabled, level instance content will be sent to Houdini." ) ) + .Font(_GetEditorStyle().GetFontStyle( TEXT( "PropertyWindow.NormalFont" ) ) ) + ] + .IsChecked_Lambda([=]() + { + return IsExportLevelInstanceContentEnabled(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedExportLevelInstanceContent(InInputs, NewState); + }) + ] + ]; +} + +void +FHoudiniInputDetails::AddCurveRotScaleAttributesCheckBox( + TSharedRef InVerticalBox, + const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + if (!IsValidWeakPointer(MainInput) || MainInput->GetInputType() != EHoudiniInputType::Curve) + return; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("CurveAddRotScaleAttributes", "Add Rotation/Scale Attributes")) + .ToolTipText(LOCTEXT("CurveAddRotScaleAttributesTip", "If enabled, rotation and scale attributes will be added per to the input curve for each control point.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return ECheckBoxState::Unchecked; + + return MainInput->IsAddRotAndScaleAttributesEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([MainInput, InInputs](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + const bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->IsAddRotAndScaleAttributesEnabled() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Add Rotation/Scale Attributes to Curve Checkbox"), + MainInput->GetOuter()); + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->IsAddRotAndScaleAttributesEnabled() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetAddRotAndScaleAttributes(bNewState); + CurInput->MarkChanged(true); + } + }) + ]; +} + +void +FHoudiniInputDetails::AddCurveAutoUpdateCheckBox( + TSharedRef InVerticalBox, + const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + if (!IsValidWeakPointer(MainInput) || MainInput->GetInputType() != EHoudiniInputType::Curve) + return; + + auto IsAutoUpdate = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->GetCookOnCurveChange() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto CheckStateChangedAutoUpdate = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->GetCookOnCurveChange() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Curve Auto Update CheckBox"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetCookOnCurveChange() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetCookOnCurveChange(bNewState); + CurInput->MarkChanged(true); + } + }; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("CurveAutoUpdateCheckBox", "Auto Update")) + .ToolTipText(LOCTEXT("CurveAutoUpdateCheckBoxTip", "When checked, cook is triggered automatically when the curve is modified.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([=]() + { + return IsAutoUpdate(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedAutoUpdate(InInputs, NewState); + }) + ]; +} + +void +FHoudiniInputDetails::AddExportAsReferenceCheckBoxes( + TSharedRef InVerticalBox, + const TArray>& InInputs) +{ + // NOTE: The "Export*" checkboxes are represented in + // UHoudiniInput as "Import*". This function is used for the + // New Geometry and New World input types. It differs from + // AddImportAsReferenceCheckboxes in the appearance of the + // checkboxes. + + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + if (!IsValidWeakPointer(MainInput)) + return; + + // Lambda returning a CheckState from the input's current + // ImportAsReference state + auto IsCheckedImportAsReference = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->GetImportAsReference() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + // Add export input as references checkbox + { + auto CheckStateChangeImportAsReference = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + const bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->GetImportAsReference() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Input as References CheckBox"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetImportAsReference() == bNewState) + continue; + + TArray>* InputObjs = CurInput->GetHoudiniInputObjectArray(CurInput->GetInputType()); + + if (InputObjs) + { + for (auto CurInputObj : *InputObjs) + { + if (!IsValid(CurInputObj)) + continue; + + if (CurInputObj->GetImportAsReference() != bNewState) + { + CurInputObj->MarkChanged(true); + } + } + } + + CurInput->Modify(); + CurInput->SetImportAsReference(bNewState); + CurInput->MarkChanged(true); + } + }; + + TSharedPtr CheckBoxImportAsReference; + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SAssignNew(CheckBoxImportAsReference, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("ExportInputAsRefCheckbox", "Export Input as References")) + .ToolTipText(LOCTEXT("ExportInputAsRefCheckboxTip", "Export input objects as references to Houdini.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([=]() + { + return IsCheckedImportAsReference(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangeImportAsReference(InInputs, NewState); + }) + ]; + } + + // 1 - Add rot/scale checkbox + { + auto IsCheckedImportAsReferenceRotScale = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->GetImportAsReferenceRotScaleEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto CheckStateChangedImportAsReferenceRotScale = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->GetImportAsReferenceRotScaleEnabled() == bNewState) + return; + + FScopedTransaction Transcation( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniExportAsRefRotScale", "Houdini Input: Changed Add Rotation/Scale to References CheckBox"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetImportAsReferenceRotScaleEnabled() == bNewState) + continue; + + TArray>* InputObjs = CurInput->GetHoudiniInputObjectArray(CurInput->GetInputType()); + if (InputObjs) + { + for (auto CurInputObj : *InputObjs) + { + if (!IsValid(CurInputObj)) + continue; + + if (CurInputObj->GetImportAsReferenceRotScaleEnabled() != bNewState) + { + CurInputObj->MarkChanged(true); + } + } + } + + CurInput->Modify(); + CurInput->SetImportAsReferenceRotScaleEnabled(bNewState); + CurInput->MarkChanged(true); + } + }; + + if (IsCheckedImportAsReference(MainInput) == ECheckBoxState::Checked) { + TSharedPtr CheckBoxImportAsReferenceRotScale; + + InVerticalBox->AddSlot() + .Padding(10, 2, 5, 2) + .AutoHeight() + [ + SAssignNew(CheckBoxImportAsReferenceRotScale, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("ExportInputAsRefRotScaleCheckbox", "Add Rotation/Scale to References")) + .ToolTipText(LOCTEXT("ExportInputAsRefRotScaleTip", "Add rotation/scale attributes to the input references when Export Input as References is enabled.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([=]() + { + return IsCheckedImportAsReferenceRotScale(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedImportAsReferenceRotScale(InInputs, NewState); + }) + ]; + } + } + + // 2 - Add bounding box min/max checkbox + { + auto IsCheckedImportAsReferenceBbox = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->GetImportAsReferenceBboxEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto CheckStateChangedImportAsReferenceBbox = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->GetImportAsReferenceBboxEnabled() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputAsReferenceBbox", "Houdini Input: Changed Add Bbox Min/Max to References CheckBox"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetImportAsReferenceBboxEnabled() == bNewState) + continue; + + TArray>* InputObjs = CurInput->GetHoudiniInputObjectArray(CurInput->GetInputType()); + if (InputObjs) + { + for (auto CurInputObj : *InputObjs) + { + if (!IsValid(CurInputObj)) + continue; + + if (CurInputObj->GetImportAsReferenceBboxEnabled() != bNewState) + { + CurInputObj->MarkChanged(true); + } + } + } + + CurInput->Modify(); + CurInput->SetImportAsReferenceBboxEnabled(bNewState); + CurInput->MarkChanged(true); + } + }; + + if (IsCheckedImportAsReference(MainInput) == ECheckBoxState::Checked) + { + TSharedPtr CheckBoxImportAsReferenceBbox; + + InVerticalBox->AddSlot() + .Padding(10, 2, 5, 2) + .AutoHeight() + [ + SAssignNew(CheckBoxImportAsReferenceBbox, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("ExportInputAsRefBboxCheckBox", "Add Bounding Box Min/Max to References")) + .ToolTipText(LOCTEXT("ExportInputAsRefBboxTip", "Add bounding box min/max attributes to the input references when Export Input as References is enabled.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([=]() + { + return IsCheckedImportAsReferenceBbox(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedImportAsReferenceBbox(InInputs, NewState); + }) + ]; + } + } + + // 3 - Add material checkbox + { + auto IsCheckedImportAsReferenceMaterial = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->GetImportAsReferenceMaterialEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto CheckStateChangedImportAsReferenceMaterial = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->GetImportAsReferenceMaterialEnabled() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputAsReferenceMaterial", "Houdini Input: Changed Add Material to References CheckBox"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetImportAsReferenceMaterialEnabled() == bNewState) + continue; + + TArray>* InputObjs = CurInput->GetHoudiniInputObjectArray(CurInput->GetInputType()); + if (InputObjs) + { + for (auto CurInputObj : *InputObjs) + { + if (!IsValid(CurInputObj)) + continue; + + if (CurInputObj->GetImportAsReferenceMaterialEnabled() != bNewState) + { + CurInputObj->MarkChanged(true); + } + } + } + + CurInput->Modify(); + CurInput->SetImportAsReferenceMaterialEnabled(bNewState); + CurInput->MarkChanged(true); + } + }; + + if (IsCheckedImportAsReference(MainInput) == ECheckBoxState::Checked) + { + TSharedPtr CheckBoxImportAsReferenceMaterial; + + InVerticalBox->AddSlot() + .Padding(10, 2, 5, 2) + .AutoHeight() + [ + SAssignNew(CheckBoxImportAsReferenceMaterial, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("ExportInputAsRefMaterialCheckBox", "Add Material to References")) + .ToolTipText(LOCTEXT("ExportInputAsRefMaterialTip", "Add material attributes to the input references when Export Input as References is enabled.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([=]() + { + return IsCheckedImportAsReferenceMaterial(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedImportAsReferenceMaterial(InInputs, NewState); + }) + ]; + } + } +} + +void +FHoudiniInputDetails::AddDirectlyConnectHdasCheckBox( + TSharedRef InVerticalBox, + const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + if (!IsValidWeakPointer(MainInput)) + return; + + auto DirectlyConnectHdas = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->GetDirectlyConnectHdas() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto CheckStateChangedDirectlyConnectHdas = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->GetDirectlyConnectHdas() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Directly Connect HDAs CheckBox"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetDirectlyConnectHdas() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetDirectlyConnectHdas(bNewState); + CurInput->MarkChanged(true); + } + }; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("DirectlyConnectHdasCheckBox", "Directly Connect HDAs in Houdini")) + .ToolTipText(LOCTEXT("DirectlyConnectHdasCheckBoxTip", "Directly connect HDAs in Houdini instead of processing input HDAs separately.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([=]() + { + return DirectlyConnectHdas(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedDirectlyConnectHdas(InInputs, NewState); + }) + ]; +} + +void +FHoudiniInputDetails::AddUnrealSplineResolutionInput( + TSharedRef InVerticalBox, + const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + if (!IsValidWeakPointer(MainInput)) + return; + + auto UnrealSplineResolutionChanged = [MainInput, InInputs](float NewVal) + { + if (!IsValidWeakPointer(MainInput)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Unreal Spline Resolution"), + MainInput->GetOuter()); + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetUnrealSplineResolution() == NewVal) + continue; + + CurInput->Modify(); + CurInput->SetUnrealSplineResolution(NewVal); + CurInput->MarkChanged(true); + } + }; + + auto ResetUnrealSplineResolution = [MainInput, InInputs]() + { + if (!IsValidWeakPointer(MainInput)) + return FReply::Handled(); + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Reset Unreal Spline Resolution"), + MainInput->GetOuter()); + + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + float DefaultSplineResolution = HoudiniRuntimeSettings ? HoudiniRuntimeSettings->MarshallingSplineResolution : 50; + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetUnrealSplineResolution() == DefaultSplineResolution) + continue; + + CurInput->Modify(); + CurInput->SetUnrealSplineResolution(DefaultSplineResolution); + CurInput->MarkChanged(true); + } + + return FReply::Handled(); + }; + + auto GetUnrealSplineResolutionValue = [MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return TOptional(); + + return TOptional(MainInput->GetUnrealSplineResolution()); + }; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + [ + SNew(STextBlock) + .Text(LOCTEXT("UnrealSplineResolution", "Unreal Spline Resolution")) + .ToolTipText(LOCTEXT("UnrealSplineResolutionTip", "Resolution used when marshalling the Unreal Splines to HoudiniEngine.\n(step in cm between control points)\nSet this to 0 to only export the control points.")) + .Font(_GetEditorStyle().GetFontStyle("PropertyWindow.NormalFont")) + ] + + SHorizontalBox::Slot() + .Padding(2, 0) + .VAlign(VAlign_Center) + [ + SNew(SNumericEntryBox) + .AllowSpin(true) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .MinValue(0) + .MaxValue(1000) + .MinSliderValue(0) + .MaxSliderValue(1000) + .Value_Lambda([GetUnrealSplineResolutionValue]() + { + return GetUnrealSplineResolutionValue(); + }) + .SliderExponent(1) + .OnValueChanged_Lambda([UnrealSplineResolutionChanged](float NewVal) + { + UnrealSplineResolutionChanged(NewVal); + }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2, 0) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("UnrealSpineResolutionDefault", "Result to default value.")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([ResetUnrealSplineResolution]() + { + return ResetUnrealSplineResolution(); + }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ]; +} + +void +FHoudiniInputDetails::AddUseLegacyInputCurvesCheckBox( + TSharedRef InVerticalBox, + const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + if (!IsValidWeakPointer(MainInput)) + return; + + auto UseLegacyInputCurves = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->IsUseLegacyInputCurvesEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto CheckStateChangedUseLegacyInputCurves = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->IsUseLegacyInputCurvesEnabled() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Use Legacy Input Curves CheckBox"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->IsUseLegacyInputCurvesEnabled() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetUseLegacyInputCurve(bNewState); + CurInput->MarkChanged(true); + } + }; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("UseLegacyInputCurvesCheckBox", "Use Legacy Input Curves")) + .ToolTipText(LOCTEXT("UseLegacyInputCurvesCheckBoxTip", "If enabled, the deprecated curve::1.0 node will be used for spline inputs.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([=]() + { + return UseLegacyInputCurves(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedUseLegacyInputCurves(InInputs, NewState); + }) + ]; +} + +void +FHoudiniInputDetails::AddExportSelectedLandscapesOnlyCheckBox( + TSharedRef InVerticalBox, + const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + if (!IsValidWeakPointer(MainInput)) + return; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("ExportEditLayersDataCheckBox", "Export Height Data Per Edit Layer")) + .ToolTipText(LOCTEXT("ExportEditLayersDataCheckBoxTip", "Exports Edit Layer data. If Edit Layers are not needed disabling this will improve performance.")) + .Font(_GetEditorStyle().GetFontStyle("PropertyWindow.NormalFont")) + ] + .IsChecked_Lambda([MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return ECheckBoxState::Unchecked; + + return MainInput->IsEditLayerHeightExportEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InInputs, MainInput](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Selected Landscape Components Only CheckBox"), + MainInput->GetOuter()); + + for (auto CurrentInput : InInputs) + { + if (!IsValidWeakPointer(CurrentInput)) + continue; + + bool bNewState = (NewState == ECheckBoxState::Checked); + if (bNewState == CurrentInput->IsEditLayerHeightExportEnabled()) + continue; + + CurrentInput->Modify(); + CurrentInput->SetExportHeightDataPerEditLayer(bNewState); + CurrentInput->UpdateLandscapeInputSelection(); + CurrentInput->MarkChanged(true); + CurrentInput->MarkAllInputObjectsChanged(true); + } + }) + ]; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("ExportPaintLayersPerEditLauyerCheckBox", "Export Paint Layers Per Edit Layer")) + .ToolTipText(LOCTEXT("ExportPaintLayersPerEditLauyerCheckBoxTip", "Exports Paint Layer data per Edit Layer. If Edit Layers are not needed disabling this will improve performance.")) + .Font(_GetEditorStyle().GetFontStyle("PropertyWindow.NormalFont")) + ] + .IsChecked_Lambda([MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return ECheckBoxState::Unchecked; + + return MainInput->IsPaintLayerPerEditLayerExportEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InInputs, MainInput](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Selected Landscape Components Only CheckBox"), + MainInput->GetOuter()); + + for (auto CurrentInput : InInputs) + { + if (!IsValidWeakPointer(CurrentInput)) + continue; + + bool bNewState = (NewState == ECheckBoxState::Checked); + if (bNewState == CurrentInput->IsPaintLayerPerEditLayerExportEnabled()) + continue; + + CurrentInput->Modify(); + CurrentInput->SetExportPaintLayerPerEditLayer(bNewState); + CurrentInput->UpdateLandscapeInputSelection(); + CurrentInput->MarkChanged(true); + CurrentInput->MarkAllInputObjectsChanged(true); + } + }) + ]; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("ExportMergedPaintLayersCheckBox", "Export Merged Paint Layers Per Landscape")) + .ToolTipText(LOCTEXT("ExportMergedPaintLayersCheckBoxTip", "Exports merged Paint Layers for the landscape.")) + .Font(_GetEditorStyle().GetFontStyle("PropertyWindow.NormalFont")) + ] + .IsChecked_Lambda([MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return ECheckBoxState::Unchecked; + + return MainInput->IsMergedPaintLayerExportEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InInputs, MainInput](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Selected Landscape Components Only CheckBox"), + MainInput->GetOuter()); + + for (auto CurrentInput : InInputs) + { + if (!IsValidWeakPointer(CurrentInput)) + continue; + + bool bNewState = (NewState == ECheckBoxState::Checked); + if (bNewState == CurrentInput->IsMergedPaintLayerExportEnabled()) + continue; + + CurrentInput->Modify(); + CurrentInput->SetExportMergedPaintLayers(bNewState); + CurrentInput->UpdateLandscapeInputSelection(); + CurrentInput->MarkChanged(true); + CurrentInput->MarkAllInputObjectsChanged(true); + } + }) + ]; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("ExportSelectedLandscapeCheckBox", "Export Selected Landscape Components Only")) + .ToolTipText(LOCTEXT("ExportSelectedLandscapeCheckBoxTip", "If enabled, only the selected landscape components will be exported.")) + .Font(_GetEditorStyle().GetFontStyle("PropertyWindow.NormalFont")) + ] + .IsChecked_Lambda([MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return ECheckBoxState::Unchecked; + + return MainInput->IsLandscapeExportSelectionOnlyEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InInputs, MainInput](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Selected Landscape Components Only CheckBox"), + MainInput->GetOuter()); + + for (auto CurrentInput : InInputs) + { + if (!IsValidWeakPointer(CurrentInput)) + continue; + + bool bNewState = (NewState == ECheckBoxState::Checked); + if (bNewState == CurrentInput->IsLandscapeExportSelectionOnlyEnabled()) + continue; + + CurrentInput->Modify(); + CurrentInput->SetLandscapeExportSelectionOnlyEnabled(bNewState); + CurrentInput->UpdateLandscapeInputSelection(); + CurrentInput->MarkChanged(true); + CurrentInput->MarkAllInputObjectsChanged(true); + } + }) + ]; + + if (MainInput->IsLandscapeExportSelectionOnlyEnabled()) + { + InVerticalBox->AddSlot() + .Padding(10, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("AutoSelectComponentCheckbox", "Auto Select Components in Asset Bounds")) + .ToolTipText(LOCTEXT("AutoSelectComponentCheckboxTooltip", "If enabled, when no Landscape components are currently selected, the one within the asset's bounding box will be exported.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return ECheckBoxState::Unchecked; + + return MainInput->IsLandscapeAutoSelectComponentEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InInputs, MainInput](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Landscape Auto Select Components in Asset Bounds CheckBox"), + MainInput->GetOuter()); + + for (auto CurrentInput : InInputs) + { + if (!IsValidWeakPointer(CurrentInput)) + continue; + + bool bNewState = (NewState == ECheckBoxState::Checked); + if (bNewState == CurrentInput->IsLandscapeAutoSelectComponentEnabled()) + continue; + + CurrentInput->Modify(); + CurrentInput->SetLandscapeAutoSelectComponentEnabled(bNewState); + CurrentInput->UpdateLandscapeInputSelection(); + CurrentInput->MarkChanged(true); + } + }) + ]; + } +} + +void +FHoudiniInputDetails::AddExportLandscapeAsOptions( + TSharedRef InVerticalBox, + const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + if (!IsValidWeakPointer(MainInput)) + return; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeExportAs", "Export Landscape as")) + .ToolTipText(LOCTEXT("LandscapeExportAsTip", "Choose the type of data you want the ladscape to be exported to:\n * Heightfield\n * Mesh\n * Points")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ]; + + TSharedPtr ButtonOptionsPanel; + InVerticalBox->AddSlot() + .Padding(2, 4, 5, 4) + .AutoHeight() + [ + SAssignNew(ButtonOptionsPanel, SUniformGridPanel) + ]; + + auto IsCheckedExportAs = [](const TWeakObjectPtr& Input, const EHoudiniLandscapeExportType& LandscapeExportType) + { + if (!IsValidWeakPointer(Input)) + return ECheckBoxState::Unchecked; + + return Input->GetLandscapeExportType() == LandscapeExportType ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + auto CheckStateChangedExportAs = [](const TWeakObjectPtr& Input, const EHoudiniLandscapeExportType& LandscapeExportType) + { + if (!IsValidWeakPointer(Input)) + return false; + + if (Input->GetLandscapeExportType() == LandscapeExportType) + return false; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Landscape Export Type"), + Input->GetOuter()); + Input->Modify(); + + Input->SetLandscapeExportType(LandscapeExportType); + Input->SetHasLandscapeExportTypeChanged(true); + Input->MarkChanged(true); + + TArray>* LandscapeInputObjectsArray = Input->GetHoudiniInputObjectArray(Input->GetInputType()); + if (!LandscapeInputObjectsArray) + return true; + + for (UHoudiniInputObject* NextInputObj : *LandscapeInputObjectsArray) + { + if (!NextInputObj) + continue; + NextInputObj->MarkChanged(true); + } + + return true; + }; + + // Heightfield + FText HeightfieldTooltip = LOCTEXT("LandscapeExportAsHeightfieldTip", "If enabled, the landscape will be exported to Houdini as a heightfield."); + ButtonOptionsPanel->AddSlot(0, 0) + [ + SNew(SCheckBox) + .Style(_GetEditorStyle(), "Property.ToggleButton.Start") + .IsChecked_Lambda([IsCheckedExportAs, MainInput]() + { + return IsCheckedExportAs(MainInput, EHoudiniLandscapeExportType::Heightfield); + }) + .OnCheckStateChanged_Lambda([CheckStateChangedExportAs, InInputs](ECheckBoxState NewState) + { + for (auto CurrentInput : InInputs) + CheckStateChangedExportAs(CurrentInput, EHoudiniLandscapeExportType::Heightfield); + }) + .ToolTipText(HeightfieldTooltip) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(2, 2) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("ClassIcon.LandscapeComponent")) + ] + + SHorizontalBox::Slot() + .FillWidth(1.f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Padding(2, 2) + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeExportAsHeightfieldCheckbox", "Heightfield")) + .ToolTipText(HeightfieldTooltip) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + ] + ]; + + // Mesh + FText MeshTooltip = LOCTEXT("LandscapeExportAsHeightfieldTip", "If enabled, the landscape will be exported to Houdini as a mesh."); + ButtonOptionsPanel->AddSlot(1, 0) + [ + SNew(SCheckBox) + .Style(_GetEditorStyle(), "Property.ToggleButton.Middle") + .IsChecked_Lambda([IsCheckedExportAs, MainInput]() + { + return IsCheckedExportAs(MainInput, EHoudiniLandscapeExportType::Mesh); + }) + .OnCheckStateChanged_Lambda([CheckStateChangedExportAs, InInputs](ECheckBoxState NewState) + { + for (auto CurrentInput : InInputs) + CheckStateChangedExportAs(CurrentInput, EHoudiniLandscapeExportType::Mesh); + }) + .ToolTipText(MeshTooltip) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(2, 2) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("ClassIcon.StaticMeshComponent")) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Padding(2, 2) + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeExportAsMeshCheckbox", "Mesh")) + .ToolTipText(MeshTooltip) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + ] + ]; + + // Points + FText PointsTooltip = LOCTEXT("LandscapeExportAsPointsTip", "If enabled, the landscape will be exported to Houdini as points."); + ButtonOptionsPanel->AddSlot(2, 0) + [ + SNew(SCheckBox) + .Style(_GetEditorStyle(), "Property.ToggleButton.End") + .IsChecked_Lambda([IsCheckedExportAs, MainInput]() + { + return IsCheckedExportAs(MainInput, EHoudiniLandscapeExportType::Points); + }) + .OnCheckStateChanged_Lambda([CheckStateChangedExportAs, InInputs](ECheckBoxState NewState) + { + for (auto CurrentInput : InInputs) + CheckStateChangedExportAs(CurrentInput, EHoudiniLandscapeExportType::Points); + }) + .ToolTipText(PointsTooltip) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(2, 2) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Mobility.Static")) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Padding(2, 2) + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeExportAsPointsCheckbox", "Points")) + .ToolTipText(PointsTooltip) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + ] + ]; + + // The following checkbox are only added when not in heightfield mode + if (MainInput->GetLandscapeExportType() != EHoudiniLandscapeExportType::Heightfield) + { + TSharedPtr CheckBoxExportMaterials; + InVerticalBox->AddSlot() + .Padding(10, 2, 5, 2) + .AutoHeight() + [ + SAssignNew(CheckBoxExportMaterials, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeMaterialsCheckbox", "Export Landscape Materials")) + .ToolTipText(LOCTEXT("LandscapeMaterialsTooltip", "If enabled, the landscape materials will be exported with it.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return ECheckBoxState::Unchecked; + + return MainInput->IsLandscapeExportMaterialsEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InInputs, MainInput](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Landscape Materials CheckBox"), + MainInput->GetOuter()); + + for (auto CurrentInput : InInputs) + { + if (!IsValidWeakPointer(CurrentInput)) + continue; + + bool bNewState = (NewState == ECheckBoxState::Checked); + if (bNewState == CurrentInput->IsLandscapeExportMaterialsEnabled()) + continue; + + CurrentInput->Modify(); + + CurrentInput->SetLandscapeExportMaterialsEnabled(bNewState); + CurrentInput->MarkChanged(true); + } + }) + ]; + + TSharedPtr CheckBoxExportTileUVs; + InVerticalBox->AddSlot() + .Padding(10, 2, 5, 2) + .AutoHeight() + [ + SAssignNew(CheckBoxExportTileUVs, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeTileUVsCheckbox", "Export Landscape Tile UVs")) + .ToolTipText(LOCTEXT("LandscapeTileUVsTooltip", "If enabled, UVs will be exported separately for each Landscape tile.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return ECheckBoxState::Unchecked; + + return MainInput->IsLandscapeExportTileUVsEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InInputs, MainInput](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Landscape Tile UVs CheckBox"), + MainInput->GetOuter()); + + for (auto CurrentInput : InInputs) + { + if (!IsValidWeakPointer(CurrentInput)) + continue; + + bool bNewState = (NewState == ECheckBoxState::Checked); + if (bNewState == CurrentInput->IsLandscapeExportTileUVsEnabled()) + continue; + + CurrentInput->Modify(); + + CurrentInput->SetLandscapeExportTileUVsEnabled(bNewState); + CurrentInput->MarkChanged(true); + } + }) + ]; + + TSharedPtr CheckBoxExportNormalizedUVs; + InVerticalBox->AddSlot() + .Padding(10, 2, 5, 2) + .AutoHeight() + [ + SAssignNew(CheckBoxExportNormalizedUVs, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeNormalizedUVsCheckbox", "Export Landscape Normalized UVs")) + .ToolTipText(LOCTEXT("LandscapeNormalizedUVsTooltip", "If enabled, landscape UVs will be exported in [0, 1].")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return ECheckBoxState::Unchecked; + + return MainInput->IsLandscapeExportNormalizedUVsEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InInputs, MainInput](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Landscape Normalized UVs"), + MainInput->GetOuter()); + + for (auto CurrentInput : InInputs) + { + if (!IsValidWeakPointer(CurrentInput)) + continue; + + bool bNewState = (NewState == ECheckBoxState::Checked); + if (bNewState == CurrentInput->IsLandscapeExportNormalizedUVsEnabled()) + continue; + + CurrentInput->Modify(); + + CurrentInput->SetLandscapeExportNormalizedUVsEnabled(bNewState); + CurrentInput->MarkChanged(true); + } + }) + ]; + + TSharedPtr CheckBoxExportLighting; + InVerticalBox->AddSlot() + .Padding(10, 2, 5, 2) + .AutoHeight() + [ + SAssignNew(CheckBoxExportLighting, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeLightingCheckbox", "Export Landscape Lighting")) + .ToolTipText(LOCTEXT("LandscapeLightingTooltip", "If enabled, lightmap information will be exported with the landscape.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return ECheckBoxState::Unchecked; + + return MainInput->IsLandscapeExportLightingEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InInputs, MainInput](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Export Landscape Lighting"), + MainInput->GetOuter()); + + for (auto CurrentInput : InInputs) + { + if (!IsValidWeakPointer(CurrentInput)) + continue; + + bool bNewState = (NewState == ECheckBoxState::Checked); + if (bNewState == CurrentInput->IsLandscapeExportLightingEnabled()) + continue; + + CurrentInput->Modify(); + + CurrentInput->SetLandscapeExportLightingEnabled(bNewState); + CurrentInput->MarkChanged(true); + } + }) + ]; + + } +} + +void +FHoudiniInputDetails::AddLandscapeAutoSelectSplinesCheckBox( + TSharedRef InVerticalBox, + const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + if (!IsValidWeakPointer(MainInput)) + return; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeAutoSelectSplinesCheckBox", "Auto Select Landscape Splines For Export")) + .ToolTipText(LOCTEXT("LandscapeAutoSelectSplinesCheckBoxTip", "If enabled, then for any landscape that is selected to be sent to Houdini, its splines are also selected for export.")) + .Font(_GetEditorStyle().GetFontStyle("PropertyWindow.NormalFont")) + ] + .IsChecked_Lambda([MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return ECheckBoxState::Unchecked; + + return MainInput->IsLandscapeAutoSelectSplinesEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InInputs, MainInput](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Auto Select Landscape Splines For Export CheckBox"), + MainInput->GetOuter()); + + for (auto CurrentInput : InInputs) + { + if (!IsValidWeakPointer(CurrentInput)) + continue; + + const bool bNewState = (NewState == ECheckBoxState::Checked); + if (bNewState == CurrentInput->IsLandscapeAutoSelectSplinesEnabled()) + continue; + + CurrentInput->Modify(); + CurrentInput->SetLandscapeAutoSelectSplines(bNewState); + if (CurrentInput->GetInputType() == EHoudiniInputType::World) + { + if (bNewState) + { + CurrentInput->AddAllLandscapeSplineActorsForInputLandscapes(); + } + else + { + CurrentInput->RemoveAllLandscapeSplineActorsForInputLandscapes(); + } + } + CurrentInput->MarkChanged(true); + } + }) + ]; + + if (MainInput->IsLandscapeAutoSelectSplinesEnabled()) + { + // TODO: additional options? + } +} + +void +FHoudiniInputDetails::AddExportOptions( + TSharedRef InVerticalBox, + const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + if (!IsValidWeakPointer(MainInput)) + return; + + auto ExportOptionsMenuStateChanged = [MainInput, InInputs](bool bIsExpanded) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = bIsExpanded; + if (MainInput->GetExportOptionsMenuExpanded() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniExportOptionsMenu", "Houdini Input: Changed Export Options Menu State"), + MainInput->GetOuter()); + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetExportOptionsMenuExpanded() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetExportOptionsMenuExpanded(bNewState); + } + }; + + TSharedRef ExportOptions_VerticalBox = SNew(SVerticalBox); + + TSharedRef ExportOptions_Expandable = SNew(SExpandableArea) + .AreaTitle(LOCTEXT("NewInputsExportOptionsMenu", "Export Options")) + .InitiallyCollapsed(!MainInput->GetExportOptionsMenuExpanded()) + .MinWidth(350.0f) + .OnAreaExpansionChanged(FOnBooleanValueChanged::CreateLambda(ExportOptionsMenuStateChanged)) + .BodyContent() + [ + ExportOptions_VerticalBox + ]; + + switch (MainInput->GetInputType()) + { + case EHoudiniInputType::Geometry: + { + AddKeepWorldTransformCheckBox(ExportOptions_VerticalBox, InInputs); + AddPackBeforeMergeCheckbox(ExportOptions_VerticalBox, InInputs); + AddExportAsReferenceCheckBoxes(ExportOptions_VerticalBox, InInputs); + AddExportCheckboxes(ExportOptions_VerticalBox, InInputs); + } + break; + + case EHoudiniInputType::World: + { + AddKeepWorldTransformCheckBox(ExportOptions_VerticalBox, InInputs); + AddPackBeforeMergeCheckbox(ExportOptions_VerticalBox, InInputs); + AddExportAsReferenceCheckBoxes(ExportOptions_VerticalBox, InInputs); + AddExportCheckboxes(ExportOptions_VerticalBox, InInputs); + AddLevelInstanceExportCheckboxes(ExportOptions_VerticalBox, InInputs); + AddDirectlyConnectHdasCheckBox(ExportOptions_VerticalBox, InInputs); + AddUseLegacyInputCurvesCheckBox(ExportOptions_VerticalBox, InInputs); + AddUnrealSplineResolutionInput(ExportOptions_VerticalBox, InInputs); + } + break; + + case EHoudiniInputType::Curve: + { + AddKeepWorldTransformCheckBox(ExportOptions_VerticalBox, InInputs); + AddCurveAutoUpdateCheckBox(ExportOptions_VerticalBox, InInputs); + AddCurveRotScaleAttributesCheckBox(ExportOptions_VerticalBox, InInputs); + AddUseLegacyInputCurvesCheckBox(ExportOptions_VerticalBox, InInputs); + } + break; + + default: + break; + } + + EHoudiniInputType InputType = MainInput->GetInputType(); + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .FillWidth(350.0f) + [ + ExportOptions_Expandable + ] + ]; +} + +void +FHoudiniInputDetails::AddLandscapeOptions( + TSharedRef InVerticalBox, + const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + auto LandscapeOptionsMenuStateChanged = [MainInput, InInputs](bool bIsExpanded) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = bIsExpanded; + + if (MainInput->GetLandscapeOptionsMenuExpanded() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniLandscapeOptionsMenu", "Houdini Input: Changed Landscape Options Menu State"), + MainInput->GetOuter()); + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetLandscapeOptionsMenuExpanded() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetLandscapeOptionsMenuExpanded(bNewState); + } + }; + + TSharedRef LandscapeOptions_VerticalBox = SNew(SVerticalBox); + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SExpandableArea) + .AreaTitle(LOCTEXT("LandscapeOptionsMenu", "Landscape Options")) + .InitiallyCollapsed(!MainInput->GetLandscapeOptionsMenuExpanded()) + .OnAreaExpansionChanged(FOnBooleanValueChanged::CreateLambda(LandscapeOptionsMenuStateChanged)) + .BodyContent() + [ + LandscapeOptions_VerticalBox + ] + ]; + + AddExportSelectedLandscapesOnlyCheckBox(LandscapeOptions_VerticalBox, InInputs); + AddExportLandscapeAsOptions(LandscapeOptions_VerticalBox, InInputs); +} + +void +FHoudiniInputDetails::AddLandscapeSplinesOptions( + TSharedRef InVerticalBox, + const TArray>& InInputs) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + auto LandscapeOptionsMenuStateChanged = [MainInput, InInputs](bool bIsExpanded) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = bIsExpanded; + + if (MainInput->IsLandscapeSplinesExportOptionsMenuExpanded() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniLandscapeSplinesOptionsMenu", "Houdini Input: Changed Landscape Splines Options Menu State"), + MainInput->GetOuter()); + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->IsLandscapeSplinesExportOptionsMenuExpanded() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetLandscapeSplinesExportOptionsMenuExpanded(bNewState); + } + }; + + const TSharedRef LandscapeSplinesOptions_VerticalBox = SNew(SVerticalBox); + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SExpandableArea) + .AreaTitle(LOCTEXT("LandscapeSplinesOptionsMenu", "Landscape Splines Options")) + .InitiallyCollapsed(!MainInput->IsLandscapeSplinesExportOptionsMenuExpanded()) + .OnAreaExpansionChanged(FOnBooleanValueChanged::CreateLambda(LandscapeOptionsMenuStateChanged)) + .BodyContent() + [ + LandscapeSplinesOptions_VerticalBox + ] + ]; + + // Lambda returning a CheckState from the input's current bLandscapeSplinesExportControlPoints state + auto IsLandscapeSplinesExportControlPointsEnabled = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->IsLandscapeSplinesExportControlPointsEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + // Lambda for changing bLandscapeSplinesExportControlPoints state + auto CheckStateChangedLandscapeSplinesExportControlPoints = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->IsLandscapeSplinesExportControlPointsEnabled() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Export Landscape Spline Control Points"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->IsLandscapeSplinesExportControlPointsEnabled() == bNewState) + continue; + + CurInput->Modify(); + + CurInput->SetLandscapeSplinesExportControlPoints(bNewState); + CurInput->MarkChanged(true); + CurInput->MarkAllInputObjectsChanged(true); + } + }; + + // Lambda returning a CheckState from the input's current bLandscapeSplinesExportLeftRightCurves state + auto IsLandscapeSplinesExportLeftRightCurvesEnabled = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->IsLandscapeSplinesExportLeftRightCurvesEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + // Lambda for changing bLandscapeSplinesExportLeftRightCurves state + auto CheckStateChangedLandscapeSplinesExportLeftRightCurves = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->IsLandscapeSplinesExportLeftRightCurvesEnabled() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChangeLandscapeSplinesExportLeftRightCurves", "Houdini Input: Changed Export Landscape Spline Left/Right Curves"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->IsLandscapeSplinesExportLeftRightCurvesEnabled() == bNewState) + continue; + + CurInput->Modify(); + + CurInput->SetLandscapeSplinesExportLeftRightCurves(bNewState); + CurInput->MarkChanged(true); + CurInput->MarkAllInputObjectsChanged(true); + } + }; + + // Lambda returning a CheckState from the input's current bLandscapeSplinesExportSplineMeshComponents state + auto IsLandscapeSplinesExportSplineMeshComponentsEnabled = [](const TWeakObjectPtr& InInput) + { + if (!IsValidWeakPointer(InInput)) + return ECheckBoxState::Unchecked; + + return InInput->IsLandscapeSplinesExportSplineMeshComponentsEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + // Lambda for changing bLandscapeSplinesExportSplineMeshComponents state + auto CheckStateChangedLandscapeSplinesExportSplineMeshComponents = [MainInput](const TArray>& InInputsToUpdate, ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + if (MainInput->IsLandscapeSplinesExportSplineMeshComponentsEnabled() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChangeLandscapeSplinesExportSplineMeshComponents", "Houdini Input: Changed Export Landscape Spline Mesh Components"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->IsLandscapeSplinesExportSplineMeshComponentsEnabled() == bNewState) + continue; + + CurInput->Modify(); + + CurInput->SetLandscapeSplinesExportSplineMeshComponents(bNewState); + CurInput->MarkChanged(true); + CurInput->MarkAllInputObjectsChanged(true); + } + }; + + TSharedPtr CheckBoxLandscapeSplinesExportControlPoints; + LandscapeSplinesOptions_VerticalBox->AddSlot() + .Padding( 2, 2, 5, 2 ) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SAssignNew(CheckBoxLandscapeSplinesExportControlPoints, SCheckBox ) + .Content() + [ + SNew( STextBlock ) + .Text( LOCTEXT( "ExportLandscapeSplineControlPoints", "Export Landscape Spline Control Points" ) ) + .ToolTipText( LOCTEXT( "ExportLandscapeSplineControlPointsCheckboxTip", "If enabled, the control points of landscape splines are exported as a point cloud." ) ) + .Font(_GetEditorStyle().GetFontStyle( TEXT( "PropertyWindow.NormalFont" ) ) ) + ] + .IsChecked_Lambda([=]() + { + return IsLandscapeSplinesExportControlPointsEnabled(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedLandscapeSplinesExportControlPoints(InInputs, NewState); + }) + ] + ]; + + TSharedPtr CheckBoxLandscapeSplinesExportLeftRightCurves; + LandscapeSplinesOptions_VerticalBox->AddSlot() + .Padding( 2, 2, 5, 2 ) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SAssignNew(CheckBoxLandscapeSplinesExportLeftRightCurves, SCheckBox ) + .Content() + [ + SNew( STextBlock ) + .Text( LOCTEXT( "ExportLandscapeSplineLeftRightCurves", "Export Landscape Spline Left/Right Curves" ) ) + .ToolTipText( LOCTEXT( "ExportLandscapeSplineLeftRightCurvesCheckboxTip", "If enabled, the left and right curves of landscape splines are also exported." ) ) + .Font(_GetEditorStyle().GetFontStyle( TEXT( "PropertyWindow.NormalFont" ) ) ) + ] + .IsChecked_Lambda([=]() + { + return IsLandscapeSplinesExportLeftRightCurvesEnabled(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedLandscapeSplinesExportLeftRightCurves(InInputs, NewState); + }) + ] + ]; + + TSharedPtr CheckBoxLandscapeSplinesExportSplineMeshComponents; + LandscapeSplinesOptions_VerticalBox->AddSlot() + .Padding( 2, 2, 5, 2 ) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SAssignNew(CheckBoxLandscapeSplinesExportSplineMeshComponents, SCheckBox ) + .Content() + [ + SNew( STextBlock ) + .Text( LOCTEXT( "ExportLandscapeSplineMeshComponents", "Export Spline Mesh Components" ) ) + .ToolTipText( LOCTEXT( "ExportLandscapeSplineMeshComponentsCheckboxTip", "If enabled, the spline mesh components of landscape splines are also exported." ) ) + .Font(_GetEditorStyle().GetFontStyle( TEXT( "PropertyWindow.NormalFont" ) ) ) + ] + .Visibility(EVisibility::Visible) + .IsChecked_Lambda([=]() + { + return IsLandscapeSplinesExportSplineMeshComponentsEnabled(MainInput); + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return CheckStateChangedLandscapeSplinesExportSplineMeshComponents(InInputs, NewState); + }) + ] + ]; + + AddLandscapeAutoSelectSplinesCheckBox(LandscapeSplinesOptions_VerticalBox, InInputs); +} + +void +FHoudiniInputDetails::AddCurveInputUI( + IDetailCategoryBuilder& CategoryBuilder, + TSharedRef InVerticalBox, + const TArray>& InInputs, + TSharedPtr AssetThumbnailPool) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + if (!IsValidWeakPointer(MainInput)) + return; + + const int32 NumInputObjects = MainInput->GetNumberOfInputObjects(EHoudiniInputType::Curve); + + auto SetInputObjectsCount = [MainInput, &CategoryBuilder](const int32& NewInputCount) + { + if (!IsValidWeakPointer(MainInput)) + return; + + UHoudiniCookable* OuterHC = Cast(MainInput->GetOuter()); + if (!IsValid(OuterHC)) + return; + + // Do not insert input object when the Cookable does not finish cooking + EHoudiniAssetState CurrentHCState = OuterHC->GetCurrentState(); + if (CurrentHCState >= EHoudiniAssetState::PreCook && CurrentHCState <= EHoudiniAssetState::Processing) + return; + + // Clear the to be inserted object array, which records the pointers of the input objects to be inserted. + MainInput->LastInsertedInputs.Empty(); + + // Record the pointer of the object to be inserted before transaction for undo the insert action. + bool bBlueprintStructureModified = false; + UHoudiniInputHoudiniSplineComponent* NewInput = + MainInput->CreateHoudiniSplineInput(nullptr, true, false, bBlueprintStructureModified); + MainInput->LastInsertedInputs.Add(NewInput); + + FHoudiniEngineEditorUtils::ReselectSelectedActors(); + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Added Curve Input"), + MainInput->GetOuter()); + + MainInput->Modify(); + MainInput->GetHoudiniInputObjectArray(MainInput->GetInputType())->Add(NewInput); + MainInput->SetInputObjectsNumber(EHoudiniInputType::Curve, NewInputCount); + + if (bBlueprintStructureModified) + FHoudiniEngineRuntimeUtils::MarkBlueprintAsStructurallyModified(OuterHC->GetComponent()); + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + }; + + auto CurveInputsStateChanged = [MainInput, InInputs, &CategoryBuilder](bool bIsExpanded) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = bIsExpanded; + + if (MainInput->GetCurveInputsMenuExpanded() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Curve Inputs Menu State"), + MainInput->GetOuter()); + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetCurveInputsMenuExpanded() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetCurveInputsMenuExpanded(bNewState); + + if (GEditor) + GEditor->RedrawAllViewports(); + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + } + }; + + AddExportOptions(InVerticalBox, InInputs); + + FText InputsMenuTitle = FText::Format( + NumInputObjects == 1 ? + LOCTEXT("NumArrayItemsFmt", "{0} Curve") : + LOCTEXT("NumArrayItemsFmt", "{0} Curves"), + FText::AsNumber(NumInputObjects)); + + TSharedRef Inputs_Expandable = SNew(SExpandableArea) + .AreaTitle(InputsMenuTitle) + .InitiallyCollapsed(!MainInput->GetCurveInputsMenuExpanded()) + .OnAreaExpansionChanged(FOnBooleanValueChanged::CreateLambda(CurveInputsStateChanged)); + + TSharedRef InputsCollapsed_VerticalBox = SNew(SVerticalBox).Visibility_Lambda([MainInput]() + { + return MainInput->GetCurveInputsMenuExpanded() ? EVisibility::Collapsed : EVisibility::Visible; + }); + + TSharedRef InputsExpanded_VerticalBox = SNew(SVerticalBox).Visibility_Lambda([MainInput]() + { + return MainInput->GetCurveInputsMenuExpanded() ? EVisibility::Visible : EVisibility::Collapsed; + }); + + InVerticalBox->AddSlot().Padding(2, 2, 5, 2).AutoHeight()[Inputs_Expandable]; + InVerticalBox->AddSlot().Padding(5, 5, 5, 5).AutoHeight()[InputsCollapsed_VerticalBox]; + InVerticalBox->AddSlot().Padding(3, 1, 5, 2).AutoHeight()[InputsExpanded_VerticalBox]; + + InputsExpanded_VerticalBox->AddSlot() + .Padding(2, 0, 5, 15) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SButton) + .OnClicked_Lambda([SetInputObjectsCount, InInputs, NumInputObjects]() + { + SetInputObjectsCount(NumInputObjects + 1); + return FReply::Handled(); + }) + .Content() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Icons.Plus")) + .ColorAndOpacity(FStyleColors::AccentGreen) + ] + + SHorizontalBox::Slot() + .Padding(FMargin(3, 0, 0, 0)) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("AddButtonText", "Add")) + .TextStyle(_GetEditorStyle(), "SmallButtonText") + .ToolTipText(LOCTEXT("AddButtonTip", "Adds a new Curve input.")) + ] + ] + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .Padding(FMargin(3, 0, 0, 0)) + .AutoWidth() + [ + SNew(SButton) + .OnClicked_Lambda([InInputs, MainInput, &CategoryBuilder]() + { + TArray>* CurveInputComponentArray = MainInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + + // Detach all curves before deleting. + for (int n = CurveInputComponentArray->Num() - 1; n >= 0; n--) + { + UHoudiniInputHoudiniSplineComponent* HoudiniInput = + Cast ((*CurveInputComponentArray)[n]); + if (!IsValid(HoudiniInput)) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = HoudiniInput->GetCurveComponent(); + if (!IsValid(HoudiniSplineComponent)) + continue; + + FDetachmentTransformRules DetachTransRules(EDetachmentRule::KeepRelative, EDetachmentRule::KeepRelative, EDetachmentRule::KeepRelative, false); + HoudiniSplineComponent->DetachFromComponent(DetachTransRules); + } + + // Clear the insert objects buffer before transaction. + MainInput->LastInsertedInputs.Empty(); + + // Record a transaction for undo/redo + FScopedTransaction Transaction(FText::FromString("Modifying Houdini Input: Delete curve inputs.")); + MainInput->Modify(); + + bool bBlueprintStructureModified = false; + + // actual delete. + for (int n = CurveInputComponentArray->Num() - 1; n >= 0; n--) + { + UHoudiniInputHoudiniSplineComponent* HoudiniInput = + Cast ((*CurveInputComponentArray)[n]); + if (!IsValid(HoudiniInput)) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = HoudiniInput->GetCurveComponent(); + if (!IsValid(HoudiniSplineComponent)) + continue; + + MainInput->RemoveSplineFromInputObject(HoudiniInput, bBlueprintStructureModified); + + MainInput->DeleteInputObjectAt(EHoudiniInputType::Curve, n); + } + + MainInput->SetInputObjectsNumber(EHoudiniInputType::Curve, 0); + + if (bBlueprintStructureModified) + { + UActorComponent* OuterComponent = Cast(MainInput->GetOuter()); + FHoudiniEngineRuntimeUtils::MarkBlueprintAsStructurallyModified(OuterComponent); + } + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + return FReply::Handled(); + }) + .Content() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Icons.Delete")) + .ColorAndOpacity(FStyleColors::AccentRed) + ] + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("ClearAllButtonText", "Clear All")) + .TextStyle(_GetEditorStyle(), "SmallButtonText") + .ToolTipText(LOCTEXT("ClearAllButtonTip", "Clears all inputs.")) + ] + ] + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .Padding(FMargin(3, 0, 0, 0)) + .AutoWidth() + [ + SNew(SButton) + .OnClicked_Lambda([MainInput]() + { + MainInput->ResetDefaultCurveOffset(); + return FReply::Handled(); + }) + .Content() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + .ColorAndOpacity(FStyleColors::AccentYellow) + ] + + SHorizontalBox::Slot() + .Padding(FMargin(3, 0, 0, 0)) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("ResetButtonText", "Reset Offset")) + .TextStyle(_GetEditorStyle(), "SmallButtonText") + .ToolTipText(LOCTEXT("ResetButtonTip", "Reset offset for all inputs.")) + ] + ] + ] + ]; + + TSharedPtr SplineComponentVisualizer; + if (GUnrealEd) + { + TSharedPtr Visualizer = + GUnrealEd->FindComponentVisualizer(UHoudiniSplineComponent::StaticClass()->GetFName()); + SplineComponentVisualizer = StaticCastSharedPtr(Visualizer); + } + + for (int32 Idx = 0; Idx < NumInputObjects; Idx++) + { + Helper_CreateCurveWidgetCollapsed(CategoryBuilder, InInputs, Idx, AssetThumbnailPool, InputsCollapsed_VerticalBox, SplineComponentVisualizer); + Helper_CreateCurveWidgetExpanded(CategoryBuilder, InInputs, Idx, AssetThumbnailPool, InputsExpanded_VerticalBox, SplineComponentVisualizer); + } + + auto PointSelectionMenuChanged = [MainInput, InInputs](bool bIsExpanded) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = bIsExpanded; + + if (MainInput->GetCurvePointSelectionMenuExpanded() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniCurvePointSelectionMenu", "Houdini Input: Changed Curve Point Selection Menu State"), + MainInput->GetOuter()); + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetCurvePointSelectionMenuExpanded() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetCurvePointSelectionMenuExpanded(bNewState); + } + }; + + TSharedRef PointSelection_VerticalBox = SNew(SVerticalBox); + + TSharedRef PointSelection_Expandable = SNew(SExpandableArea) + .AreaTitle(LOCTEXT("HoudiniCurvePointSelectionMenuTitle", "Spline Point Editor")) + .InitiallyCollapsed(!MainInput->GetCurvePointSelectionMenuExpanded()) + .OnAreaExpansionChanged(FOnBooleanValueChanged::CreateLambda(PointSelectionMenuChanged)) + .BodyContent() + [ + PointSelection_VerticalBox + ]; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + [ + PointSelection_Expandable + ] + ]; + + Helper_AddCurvePointSelectionUI(CategoryBuilder, InInputs, AssetThumbnailPool, PointSelection_VerticalBox, SplineComponentVisualizer); +} + +void +FHoudiniInputDetails::Helper_CreateCurveWidgetExpanded( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const int32& InObjIdx, + TSharedPtr AssetThumbnailPool, + TSharedRef InVerticalBox, + TSharedPtr HouSplineComponentVisualizer) +{ + const TWeakObjectPtr& MainInput = InInputs[0]; + if (!IsValidWeakPointer(MainInput)) + return; + + UHoudiniCookable* OuterHC = Cast(MainInput->GetOuter()); + if (!IsValid(OuterHC)) + return; + + TArray>* CurveInputs = MainInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurveInputs) + return; + + if (!CurveInputs->IsValidIndex(InObjIdx)) + return; + + UHoudiniInputObject* HoudiniInputObject = (*CurveInputs)[InObjIdx]; + UHoudiniInputHoudiniSplineComponent* HoudiniSplineInputObject = Cast(HoudiniInputObject); + UHoudiniSplineComponent* HoudiniSplineComponent = HoudiniSplineInputObject->GetCurveComponent(); + if (!HoudiniSplineComponent) + return; + + bool bUseLegacyCurve = HoudiniSplineComponent->IsLegacyInputCurve(); + FString CurveName = HoudiniSplineComponent->GetHoudiniSplineName(); + int32 NumCurvePoints = HoudiniSplineComponent->GetCurvePointCount(); + bool bIsClosed = HoudiniSplineComponent->IsClosedCurve(); + bool bIsReversed = HoudiniSplineComponent->IsReversed(); + bool bIsVisible = HoudiniSplineComponent->IsHoudiniSplineVisible(); + + auto GetHoudiniSplineComponentAtIdx = [](const TWeakObjectPtr& InInput, int32 Idx) + { + UHoudiniSplineComponent* FoundHoudiniSplineComponent = nullptr; + if (!IsValidWeakPointer(InInput)) + return FoundHoudiniSplineComponent; + + // Get the TArray ptr to the curve objects in this input + TArray>* CurveInputComponentArray = InInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurveInputComponentArray) + return FoundHoudiniSplineComponent; + + if (!CurveInputComponentArray->IsValidIndex(Idx)) + return FoundHoudiniSplineComponent; + + UHoudiniInputObject* HoudiniInputObject = (*CurveInputComponentArray)[Idx]; + UHoudiniInputHoudiniSplineComponent* HoudiniSplineInputObject = + Cast(HoudiniInputObject); + + FoundHoudiniSplineComponent = HoudiniSplineInputObject->GetCurveComponent(); + + return FoundHoudiniSplineComponent; + }; + + auto DeleteCurveAtIdx = [InInputs, InObjIdx, OuterHC, CurveInputs, &CategoryBuilder]() + { + if (!IsValid(OuterHC)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Deleted a curve input."), + OuterHC); + + int CurveInputsNum = CurveInputs->Num(); + for (auto& CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + CurInput->Modify(); + + TArray>* CurCurveInputs = CurInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurCurveInputs || + CurveInputsNum != CurCurveInputs->Num() || + !CurCurveInputs->IsValidIndex(InObjIdx)) + continue; + + UHoudiniInputHoudiniSplineComponent* HoudiniInput = Cast((*CurCurveInputs)[InObjIdx]); + if (!IsValid(HoudiniInput)) + return; + + UHoudiniSplineComponent* HoudiniSplineComponent = HoudiniInput->GetCurveComponent(); + if (!HoudiniSplineComponent) + return; + + FDetachmentTransformRules DetachTransRules(EDetachmentRule::KeepRelative, EDetachmentRule::KeepRelative, EDetachmentRule::KeepRelative, false); + HoudiniSplineComponent->DetachFromComponent(DetachTransRules); + + CurInput->DeleteInputObjectAt(EHoudiniInputType::Curve, InObjIdx); + } + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + }; + + auto ChangedClosedCurve = [GetHoudiniSplineComponentAtIdx, InInputs, InObjIdx, OuterHC, &CategoryBuilder](ECheckBoxState NewState) + { + if (!IsValid(OuterHC)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniCurveInputChangeClosed", "Houdini Input: Changed Curve Closed"), + OuterHC); + + for (auto& Input : InInputs) + { + if (!IsValidWeakPointer(Input)) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = GetHoudiniSplineComponentAtIdx(Input, InObjIdx); + + if (!IsValid(HoudiniSplineComponent)) + continue; + + if (HoudiniSplineComponent->IsClosedCurve() == bNewState) + continue; + + HoudiniSplineComponent->Modify(); + HoudiniSplineComponent->SetClosedCurve(bNewState); + HoudiniSplineComponent->MarkChanged(true); + } + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + }; + + auto ChangedReversedCurve = [GetHoudiniSplineComponentAtIdx, InInputs, InObjIdx, OuterHC, &CategoryBuilder](ECheckBoxState NewState) + { + if (!IsValid(OuterHC)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniCurveInputChangeReversed", "Houdini Input: Changed Curve Reversed"), + OuterHC); + + for (auto& Input : InInputs) + { + if (!IsValidWeakPointer(Input)) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = GetHoudiniSplineComponentAtIdx(Input, InObjIdx); + if (!IsValid(HoudiniSplineComponent)) + continue; + + if (HoudiniSplineComponent->IsReversed() == bNewState) + continue; + + HoudiniSplineComponent->Modify(); + HoudiniSplineComponent->SetReversed(bNewState); + HoudiniSplineComponent->MarkChanged(true); + } + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + }; + + auto ChangedVisibleCurve = [GetHoudiniSplineComponentAtIdx, InInputs, OuterHC, InObjIdx, &CategoryBuilder](ECheckBoxState NewState) + { + if (!IsValid(OuterHC)) + return; + + bool bNewState = (NewState == ECheckBoxState::Checked); + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniCurveInputChangeVisible", "Houdini Input: Changed Curve Visible"), + OuterHC); + + for (auto& Input : InInputs) + { + if (!IsValidWeakPointer(Input)) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = GetHoudiniSplineComponentAtIdx(Input, InObjIdx); + if (!HoudiniSplineComponent) + continue; + + if (HoudiniSplineComponent->IsHoudiniSplineVisible() == bNewState) + continue; + + HoudiniSplineComponent->SetHoudiniSplineVisible(bNewState); + } + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + + if (GEditor) + GEditor->RedrawAllViewports(); + }; + + TSharedPtr EditingTextBlock; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SEditableText) + .Text(FText::FromString(CurveName)) + .OnTextCommitted_Lambda([HoudiniSplineComponent](FText NewText, ETextCommit::Type CommitType) + { + if (CommitType == ETextCommit::Type::OnEnter) + HoudiniSplineComponent->SetHoudiniSplineName(NewText.ToString()); + }) + ] + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SAssignNew(EditingTextBlock, SCurveEditingTextBlock) + .Text(LOCTEXT("CurveEdited", "*")) + .ToolTipText(LOCTEXT("CurveEditedTip", "This curve is being edited.")) + ] + ] + + SHorizontalBox::Slot() + .Padding(8, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::Format( + NumCurvePoints == 1 ? + LOCTEXT("NumCurvePointsFmt", "({0} point)") : + LOCTEXT("NumCurvePointsFmt", "({0} points)"), + FText::AsNumber(NumCurvePoints))) + .ToolTipText(LOCTEXT("NumCurvePointsTip", "Number of points on this curve.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + + SHorizontalBox::Slot() + .Padding(0) + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + + SHorizontalBox::Slot() + .Padding(20, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + .HAlign(HAlign_Right) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeDeleteButton( + FSimpleDelegate::CreateLambda([DeleteCurveAtIdx]() + { + return DeleteCurveAtIdx(); + }), + TAttribute(LOCTEXT("CurveInputDeleteTip", "Delete this curve input object."))) + ] + ] + ]; + + EditingTextBlock->HoudiniSplineComponent = HoudiniSplineComponent; + EditingTextBlock->HoudiniSplineComponentVisualizer = HouSplineComponentVisualizer; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("CurveClosedCheckBox", "Closed")) + .ToolTipText(LOCTEXT("CurveClosedCheckBoxTip", "Toggle whether this curve is closed.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([bIsClosed]() { + return bIsClosed ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return ChangedClosedCurve(NewState); + }) + ] + + SHorizontalBox::Slot() + .Padding(5, 0, 0, 0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("CurveReversedCheckBox", "Reversed")) + .ToolTipText(LOCTEXT("CurveReversedCheckBoxTip", "Toggle whether this curve is reversed.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([bIsReversed]() { + return bIsReversed ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return ChangedReversedCurve(NewState); + }) + ] + + SHorizontalBox::Slot() + .Padding(5, 0, 0, 0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("CurveVisibleCheckBox", "Visible")) + .ToolTipText(LOCTEXT("CurveVisibleCheckBoxTip", "Toggle whether this curve is visible.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([bIsVisible]() { + return bIsVisible ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([=](ECheckBoxState NewState) + { + return ChangedVisibleCurve(NewState); + }) + ] + ]; + + auto GetCurveTypeText = [HoudiniSplineComponent]() + { + return FText::FromString(FHoudiniEngineEditorUtils::HoudiniCurveTypeToString(HoudiniSplineComponent->GetCurveType())); + }; + + auto OnCurveTypeChanged = [GetHoudiniSplineComponentAtIdx, InInputs, InObjIdx, OuterHC](TSharedPtr InNewChoice) + { + if (!IsValid(OuterHC)) + return; + + if (!InNewChoice.IsValid()) + return; + + EHoudiniCurveType NewInputType = UHoudiniInput::StringToHoudiniCurveType(*InNewChoice.Get()); + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniCurveInputChangeType", "Houdini Input: Changed Curve Type"), + OuterHC); + + for (auto& Input : InInputs) + { + if (!IsValidWeakPointer(Input)) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = GetHoudiniSplineComponentAtIdx(Input, InObjIdx); + if (!IsValid(HoudiniSplineComponent)) + continue; + + if (HoudiniSplineComponent->GetCurveType() == NewInputType) + continue; + + HoudiniSplineComponent->Modify(); + HoudiniSplineComponent->SetCurveType(NewInputType); + + int32 CurveOrder = HoudiniSplineComponent->GetCurveOrder(); + if (CurveOrder < 4 && (NewInputType == EHoudiniCurveType::Nurbs || NewInputType == EHoudiniCurveType::Bezier)) + HoudiniSplineComponent->SetCurveOrder(4); + else if (NewInputType == EHoudiniCurveType::Polygon) + HoudiniSplineComponent->SetCurveOrder(2); + + HoudiniSplineComponent->MarkChanged(true); + } + }; + + auto GetCurveMethodText = [HoudiniSplineComponent]() + { + return FText::FromString(FHoudiniEngineEditorUtils::HoudiniCurveMethodToString(HoudiniSplineComponent->GetCurveMethod())); + }; + + auto OnCurveMethodChanged = [GetHoudiniSplineComponentAtIdx, InInputs, InObjIdx, OuterHC](TSharedPtr InNewChoice) + { + if (!IsValid(OuterHC)) + return; + + if (!InNewChoice.IsValid()) + return; + + EHoudiniCurveMethod NewInputMethod = UHoudiniInput::StringToHoudiniCurveMethod(*InNewChoice.Get()); + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniCurveInputChangeMethod", "Houdini Input: Changed Curve Method"), + OuterHC); + + for (auto& Input : InInputs) + { + if (!IsValidWeakPointer(Input)) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = GetHoudiniSplineComponentAtIdx(Input, InObjIdx); + if (!IsValid(HoudiniSplineComponent)) + continue; + + if (HoudiniSplineComponent->GetCurveMethod() == NewInputMethod) + return; + + HoudiniSplineComponent->Modify(); + HoudiniSplineComponent->SetCurveMethod(NewInputMethod); + HoudiniSplineComponent->MarkChanged(true); + } + }; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + .FillWidth(75) + .MaxWidth(75) + [ + SNew(STextBlock) + .Text(LOCTEXT("CurveTypeLabel", "Curve Type")) + .ToolTipText(LOCTEXT("CurveTypeLabelTip", "Change the curve type for this curve.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + + SHorizontalBox::Slot() + .Padding(5, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + .FillWidth(100) + .MaxWidth(100) + [ + SNew(SComboBox>) + .OptionsSource(FHoudiniEngineEditor::Get().GetHoudiniCurveTypeChoiceLabels()) + .InitiallySelectedItem((*FHoudiniEngineEditor::Get().GetHoudiniCurveTypeChoiceLabels())[(int)HoudiniSplineComponent->GetCurveType()]) + .OnGenerateWidget_Lambda([](TSharedPtr ChoiceEntry) + { + FText ChoiceEntryText = FText::FromString(*ChoiceEntry); + return SNew(STextBlock) + .Text(ChoiceEntryText) + .ToolTipText(ChoiceEntryText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + }) + .OnSelectionChanged_Lambda([OnCurveTypeChanged](TSharedPtr NewChoice, ESelectInfo::Type SelectType) + { + return OnCurveTypeChanged(NewChoice); + }) + [ + SNew(STextBlock) + .Text_Lambda([GetCurveTypeText]() + { + return GetCurveTypeText(); + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + ]; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + .FillWidth(75) + .MaxWidth(75) + + [ + SNew(STextBlock) + .Text(LOCTEXT("CurveMethodLabel", "Curve Method")) + .ToolTipText(LOCTEXT("CurveTypeLabelTip", "Change the curve method for this curve.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + + SHorizontalBox::Slot() + .Padding(5, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + .FillWidth(100) + .MaxWidth(100) + [ + SNew(SComboBox>) + .OptionsSource(FHoudiniEngineEditor::Get().GetHoudiniCurveMethodChoiceLabels()) + .InitiallySelectedItem((*FHoudiniEngineEditor::Get().GetHoudiniCurveMethodChoiceLabels())[(int)HoudiniSplineComponent->GetCurveMethod()]) + .OnGenerateWidget_Lambda([](TSharedPtr ChoiceEntry) + { + FText ChoiceEntryText = FText::FromString(*ChoiceEntry); + return SNew(STextBlock) + .Text(ChoiceEntryText) + .ToolTipText(ChoiceEntryText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + }) + .OnSelectionChanged_Lambda([OnCurveMethodChanged](TSharedPtr NewChoice, ESelectInfo::Type SelectType) + { + return OnCurveMethodChanged(NewChoice); + }) + [ + SNew(STextBlock) + .Text_Lambda([GetCurveMethodText]() + { + return GetCurveMethodText(); + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + ]; + + if (!bUseLegacyCurve) + { + TSharedPtr> NumericEntryBox; + int32 Idx = 0; + + auto OnBreakpointParameterizationChanged = [GetHoudiniSplineComponentAtIdx, InInputs, InObjIdx, OuterHC](TSharedPtr InNewChoice) + { + if (!IsValid(OuterHC)) + return; + + if (!InNewChoice.IsValid()) + return; + + EHoudiniCurveBreakpointParameterization NewInputMethod = UHoudiniInput::StringToHoudiniCurveBreakpointParameterization(*InNewChoice.Get()); + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniCurveInputChangeBreakpointParameterization", "Houdini Input: Changed Curve Breakpoint Parameterization"), + OuterHC); + + for (auto& Input : InInputs) + { + if (!IsValidWeakPointer(Input)) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = GetHoudiniSplineComponentAtIdx(Input, InObjIdx); + if (!IsValid(HoudiniSplineComponent)) + continue; + + if (HoudiniSplineComponent->GetCurveBreakpointParameterization() == NewInputMethod) + return; + + HoudiniSplineComponent->Modify(); + HoudiniSplineComponent->SetCurveBreakpointParameterization(NewInputMethod); + HoudiniSplineComponent->MarkChanged(true); + } + }; + + auto GetCurveBreakpointParameterizationText = [HoudiniSplineComponent]() + { + return FText::FromString(FHoudiniEngineEditorUtils::HoudiniCurveBreakpointParameterizationToString(HoudiniSplineComponent->GetCurveBreakpointParameterization())); + }; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + .FillWidth(75) + .MaxWidth(75) + [ + SNew(STextBlock) + .Text(LOCTEXT("CurveOrder", "Curve Order")) + .ToolTipText(LOCTEXT("CurveOrderTooltip", "Curve order of the curve. Only used for Bezier or NURBs curves. Must be a value between 2 and 11.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + + SHorizontalBox::Slot() + .Padding(5, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + .FillWidth(100) + .MaxWidth(100) + [ + SNew(SNumericEntryBox) + .AllowSpin(true) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .MinValue(HAPI_UNREAL_ATTRIB_HAPI_INPUT_CURVE_ORDER_MIN) + .MaxValue(HAPI_UNREAL_ATTRIB_HAPI_INPUT_CURVE_ORDER_MAX) + .MinSliderValue(HAPI_UNREAL_ATTRIB_HAPI_INPUT_CURVE_ORDER_MIN) + .MaxSliderValue(HAPI_UNREAL_ATTRIB_HAPI_INPUT_CURVE_ORDER_MAX) + .Value(HoudiniSplineComponent->GetCurveOrder()) + .OnValueChanged_Lambda([GetHoudiniSplineComponentAtIdx, InInputs, InObjIdx, OuterHC](int Val) + { + if (!IsValid(OuterHC)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniChangeCurveOrder", "Houdini Input: Changed curve order"), + OuterHC); + + for (auto& Input : InInputs) + { + if (!IsValidWeakPointer(Input)) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = GetHoudiniSplineComponentAtIdx(Input, InObjIdx); + if (!IsValid(HoudiniSplineComponent)) + continue; + + HoudiniSplineComponent->Modify(); + HoudiniSplineComponent->SetCurveOrder(Val); + HoudiniSplineComponent->MarkChanged(true); + } + }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2, 0, 0, 0) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("OrderToDefault", "Reset to default value.")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility(HoudiniSplineComponent->GetCurveOrder() != HAPI_UNREAL_ATTRIB_HAPI_INPUT_CURVE_ORDER_MIN ? EVisibility::Visible : EVisibility::Hidden) + .OnClicked_Lambda([GetHoudiniSplineComponentAtIdx, InInputs, InObjIdx, OuterHC]() + { + if (!IsValid(OuterHC)) + return FReply::Handled(); + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniCurveInputChangeMethod", "Houdini Input: Changed Curve Method"), + OuterHC); + + for (auto& Input : InInputs) + { + if (!IsValidWeakPointer(Input)) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = GetHoudiniSplineComponentAtIdx(Input, InObjIdx); + if (!IsValid(HoudiniSplineComponent)) + continue; + + if (HoudiniSplineComponent->GetCurveOrder() == HAPI_UNREAL_ATTRIB_HAPI_INPUT_CURVE_ORDER_MIN) + continue; + + HoudiniSplineComponent->Modify(); + HoudiniSplineComponent->SetCurveOrder(HAPI_UNREAL_ATTRIB_HAPI_INPUT_CURVE_ORDER_MIN); + HoudiniSplineComponent->MarkChanged(true); + } + + return FReply::Handled(); + }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ]; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("CurveBreakpointParametrizationLabel", "Breakpoint Parametrization")) + .ToolTipText(LOCTEXT("CurveBreakpointParametrizationLabelTip", "Change the breakpoint parametrization for this curve if the curve's method is 'Breakpoints'")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + + SHorizontalBox::Slot() + .Padding(5, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + .FillWidth(100) + .MaxWidth(100) + [ + SNew(SComboBox>) + .IsEnabled(HoudiniSplineComponent->GetCurveMethod() == EHoudiniCurveMethod::Breakpoints) + .OptionsSource(FHoudiniEngineEditor::Get().GetHoudiniCurveBreakpointParameterizationChoiceLabels()) + .InitiallySelectedItem((*FHoudiniEngineEditor::Get().GetHoudiniCurveBreakpointParameterizationChoiceLabels())[(int32)HoudiniSplineComponent->GetCurveBreakpointParameterization()]) + .OnGenerateWidget_Lambda([](TSharedPtr ChoiceEntry) + { + FText ChoiceEntryText = FText::FromString(*ChoiceEntry); + return SNew(STextBlock) + .Text(ChoiceEntryText) + .ToolTipText(ChoiceEntryText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + }) + .OnSelectionChanged_Lambda([OnBreakpointParameterizationChanged](TSharedPtr NewChoice, ESelectInfo::Type SelectType) + { + return OnBreakpointParameterizationChanged(NewChoice); + }) + [ + SNew(STextBlock) + .Text_Lambda([GetCurveBreakpointParameterizationText]() + { + return GetCurveBreakpointParameterizationText(); + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + ]; + } + + auto BakeInputCurveLambda = [](const TArray>& Inputs, int32 Index, bool bBakeToBlueprint) + { + for (auto& NextInput : Inputs) + { + if (!IsValidWeakPointer(NextInput)) + continue; + + UHoudiniCookable* OuterHC = Cast(NextInput->GetOuter()); + if (!IsValid(OuterHC)) + continue; + + AActor* OwnerActor = OuterHC->GetOwner(); + if (!IsValid(OwnerActor)) + continue; + + TArray>* CurveInputComponentArray = NextInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurveInputComponentArray) + continue; + + if (!CurveInputComponentArray->IsValidIndex(Index)) + continue; + + UHoudiniInputObject* HoudiniInputObject = (*CurveInputComponentArray)[Index]; + UHoudiniInputHoudiniSplineComponent* HoudiniSplineInputObject = + Cast(HoudiniInputObject); + + if (!IsValid(HoudiniSplineInputObject)) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = HoudiniSplineInputObject->GetCurveComponent(); + if (!IsValid(HoudiniSplineComponent)) + continue; + + FHoudiniPackageParams PackageParams; + PackageParams.BakeFolder = OuterHC->GetBakeFolderOrDefault(); + PackageParams.HoudiniAssetName = OuterHC->GetName(); + PackageParams.GeoId = NextInput->GetAssetNodeId(); + PackageParams.PackageMode = EPackageMode::Bake; + PackageParams.ObjectId = Index; + PackageParams.ObjectName = OwnerActor->GetActorNameOrLabel() + "InputHoudiniSpline" + FString::FromInt(Index); + + FHoudiniBakeSettings BakeSettings; + + if (bBakeToBlueprint) + { + FHoudiniEngineBakeUtils::BakeInputHoudiniCurveToBlueprint( + OuterHC, + HoudiniSplineComponent, + PackageParams, + BakeSettings, + OwnerActor->GetWorld(), OwnerActor->GetActorTransform()); + } + else + { + FHoudiniEngineBakeUtils::BakeInputHoudiniCurveToActor( + OuterHC, + HoudiniSplineComponent, + PackageParams, + BakeSettings, + OwnerActor->GetWorld(), OwnerActor->GetActorTransform()); + } + } + + return FReply::Handled(); + }; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 15) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Text(LOCTEXT("CurveBakeToActorButton", "Bake to Actor")) + .ToolTipText(LOCTEXT("CurveBakeToActorButtonTip", "Bake this input curve to Actor.")) + .OnClicked_Lambda([InInputs, InObjIdx, BakeInputCurveLambda]() + { + return BakeInputCurveLambda(InInputs, InObjIdx, false); + }) + ] + + SHorizontalBox::Slot() + .Padding(10, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Text(LOCTEXT("CurveBakeToBlueprintButton", "Bake to Blueprint")) + .ToolTipText(LOCTEXT("CurveBakeToBlueprintButtonTip", "Back this input curve to Blueprint.")) + .OnClicked_Lambda([InInputs, InObjIdx, BakeInputCurveLambda]() + { + return BakeInputCurveLambda(InInputs, InObjIdx, true); + }) + ] + ]; +} + +void +FHoudiniInputDetails::Helper_CreateCurveWidgetCollapsed( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const int32& InObjIdx, + TSharedPtr AssetThumbnailPool, + TSharedRef InVerticalBox, + TSharedPtr HouSplineComponentVisualizer) +{ + const TWeakObjectPtr& MainInput = InInputs[0]; + if (!IsValidWeakPointer(MainInput)) + return; + /* + UHoudiniCookable* OuterHC = Cast(MainInput->GetOuter()); + if (!IsValid(OuterHC)) + return; + */ + + TArray>* CurveInputs = MainInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurveInputs) + return; + + if (!CurveInputs->IsValidIndex(InObjIdx)) + return; + + UHoudiniInputObject* HoudiniInputObject = (*CurveInputs)[InObjIdx]; + UHoudiniInputHoudiniSplineComponent* HoudiniSplineInputObject = Cast(HoudiniInputObject); + UHoudiniSplineComponent* HoudiniSplineComponent = HoudiniSplineInputObject->GetCurveComponent(); + if (!HoudiniSplineComponent) + return; + + bool bIsLegacyCurve = HoudiniSplineComponent->IsLegacyInputCurve(); + FString CurveName = HoudiniSplineComponent->GetHoudiniSplineName(); + int32 NumCurvePoints = HoudiniSplineComponent->GetCurvePointCount(); + FText CurveType = FText::FromString(FHoudiniEngineEditorUtils::HoudiniCurveTypeToString(HoudiniSplineComponent->GetCurveType())); + FText CurveMethod = FText::FromString(FHoudiniEngineEditorUtils::HoudiniCurveMethodToString(HoudiniSplineComponent->GetCurveMethod())); + + bool bIsClosed = HoudiniSplineComponent->IsClosedCurve(); + bool bIsReversed = HoudiniSplineComponent->IsReversed(); + bool bIsVisible = HoudiniSplineComponent->IsHoudiniSplineVisible(); + + TSharedPtr EditingTextBlock; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(CurveName)) + ] + + SHorizontalBox::Slot() + .Padding(2, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SAssignNew(EditingTextBlock, SCurveEditingTextBlock) + .Text(LOCTEXT("CurveEdited", "*")) + .ToolTipText(LOCTEXT("CurveEditedTip", "This curve is being edited.")) + ] + ] + + SHorizontalBox::Slot() + .Padding(8, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::Format( + NumCurvePoints == 1 ? + LOCTEXT("NumCurvePointsFmt", "({0} point)") : + LOCTEXT("NumCurvePointsFmt", "({0} points)"), + FText::AsNumber(NumCurvePoints))) + .ToolTipText(LOCTEXT("SplineNumCurvePointsTip", "Number of points on this curve.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + + SHorizontalBox::Slot() + .Padding(0) + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + + SHorizontalBox::Slot() + .Padding(20, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + .HAlign(HAlign_Right) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(CurveType) + .ToolTipText(LOCTEXT("CurveTypeTip", "Curve Type")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + + SHorizontalBox::Slot() + .Padding(8, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(CurveMethod) + .ToolTipText(LOCTEXT("CurveMethodTip", "Curve Method")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + + SHorizontalBox::Slot() + .Padding(20, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(bIsClosed ? + LOCTEXT("CurveClosedTip", "Closed") : + LOCTEXT("CurveClosedTip", "Not closed")) + .ButtonStyle(_GetEditorStyle(), TEXT("NoBorder")) + .ContentPadding(0) + .Visibility(EVisibility::Visible) + [ + SNew(SImage) + .Image(FSlateIcon( + FHoudiniEngineStyle::GetStyleSetName(), + bIsClosed ? + TEXT("HoudiniEngine._CurveClosed") : + TEXT("HoudiniEngine._CurveNotClosed")) + .GetSmallIcon()) + .ColorAndOpacity(FSlateColor(FColor(255, 255, 255, 168))) + ] + ] + + SHorizontalBox::Slot() + .Padding(5, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(bIsReversed ? + LOCTEXT("CurveReversedTip", "Reversed") : + LOCTEXT("CurveReversedTip", "Not reversed")) + .ButtonStyle(_GetEditorStyle(), TEXT("NoBorder")) + .ContentPadding(0) + .Visibility(EVisibility::Visible) + [ + SNew(SImage) + .Image(FSlateIcon( + FHoudiniEngineStyle::GetStyleSetName(), + bIsClosed ? + TEXT("HoudiniEngine._CurveReversed") : + TEXT("HoudiniEngine._CurveNotReversed")) + .GetSmallIcon()) + .ColorAndOpacity(FSlateColor(FColor(255, 255, 255, 168))) + ] + ] + + SHorizontalBox::Slot() + .Padding(5, 0, 0, 0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(bIsVisible ? + LOCTEXT("CurveVisibleTip", "Visible") : + LOCTEXT("CurveVisibleTip", "Hidden")) + .ButtonStyle(_GetEditorStyle(), TEXT("NoBorder")) + .ContentPadding(0) + .Visibility(EVisibility::Visible) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush(bIsVisible ? "Icons.Visible" : "Icons.Hidden")) + .ColorAndOpacity(FSlateColor(FColor(255, 255, 255, 168))) + ] + ] + ] + ] + ]; + + EditingTextBlock->HoudiniSplineComponent = HoudiniSplineComponent; + EditingTextBlock->HoudiniSplineComponentVisualizer = HouSplineComponentVisualizer; +} + +void +FHoudiniInputDetails::Helper_AddCurvePointSelectionUI( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + TSharedPtr AssetThumbnailPool, + TSharedRef InVerticalBox, + TSharedPtr HoudiniSplineComponentVisualizer) +{ + const TWeakObjectPtr& MainInput = InInputs[0]; + if (!IsValidWeakPointer(MainInput)) + return; + + UHoudiniSplineComponent* HoudiniSplineComponent = HoudiniSplineComponentVisualizer->GetEditedHoudiniSplineComponent(); + + TArray>* CurveInputs = MainInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurveInputs || CurveInputs->Num() <= 0) + return; + + UHoudiniInputObject* HoudiniInputObject = nullptr; + int32 HoudiniInputObjectIdx = -1; + if (!HoudiniSplineComponent) + { + // We're not currently editing spline, get the first one by default + HoudiniInputObject = (*CurveInputs)[0]; + HoudiniInputObjectIdx = 0; + } + else + { + for (int32 Idx = 0; Idx < CurveInputs->Num(); ++Idx) + { + UHoudiniInputHoudiniSplineComponent* CurSplineInput = Cast((*CurveInputs)[Idx]); + UHoudiniSplineComponent* CurSpline = CurSplineInput->GetCurveComponent(); + if (CurSpline == HoudiniSplineComponent) + { + HoudiniInputObject = (*CurveInputs)[Idx]; + HoudiniInputObjectIdx = Idx; + break; + } + } + } + + if (!HoudiniInputObject || HoudiniInputObjectIdx < 0) + return; + + TSharedPtr CurveListener; + + // The curve listener vertical box is just an empty box that is added so that the + // corresponding SCurveListenerVerticalBox can be used to trigger updates to the + // UI and to redraw the viewports when: + // - The currently selected spline is changed + // - The spline is moved + // - The selected control points are changed + // - The spline's control points have moved or changed + InVerticalBox->AddSlot() + .Padding(0) + .MaxHeight(1) + [ + SAssignNew(CurveListener, SCurveListenerVerticalBox) + ]; + + CurveListener->CategoryBuilder = &CategoryBuilder; + CurveListener->HoudiniSplineComponentVisualizer = HoudiniSplineComponentVisualizer; + CurveListener->PrevSplineComponent = HoudiniSplineComponent; + if (HoudiniSplineComponent) + { + CurveListener->PrevEditedControlPointsIndexes = HoudiniSplineComponent->EditedControlPointsIndexes; + CurveListener->PrevSplineTransform = HoudiniSplineComponent->GetComponentTransform(); + } + + if (!HoudiniSplineComponent) + { + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(STextBlock) + .Text(LOCTEXT("PointSelectionEmpty", "No spline is being currently edited.")) + .Font(_GetEditorStyle().GetFontStyle("PropertyWindow.NormalFont")) + ]; + return; + } + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("PointSelectionSelected", "Currently Editing ")) + .Font(_GetEditorStyle().GetFontStyle("PropertyWindow.NormalFont")) + ] + + SHorizontalBox::Slot() + .Padding(0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(FText::FromString(HoudiniSplineComponent->GetHoudiniSplineName())) + ] + ]; + + FMargin ButtonPadding(2, 0); + InVerticalBox->AddSlot() + .Padding(2, 6, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .AutoWidth() + .Padding(2, 0, 10, 0) + [ + SNew(STextBlock) + .Text(LOCTEXT("PointSelectionControls", "Point Selection")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .AutoWidth() + .Padding(2, 0, 2, 0) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .AutoWidth() + .Padding(ButtonPadding) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "SplineComponentDetails.SelectFirst") + .ContentPadding(2) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("SelectFirstSplinePointToolTip", "Select first spline point.")) + .OnClicked_Lambda([=]() + { + HoudiniSplineComponentVisualizer->SelectSplinePoint(0, false); + return FReply::Handled(); + }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(ButtonPadding) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "SplineComponentDetails.AddPrev") + .ContentPadding(2) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("SelectAddPrevSplinePointToolTip", "Add previous spline point to current selection.")) + .OnClicked_Lambda([=]() + { + HoudiniSplineComponentVisualizer->ExtendSplinePointSelection(1, true, true); + return FReply::Handled(); + }) + .IsEnabled(HoudiniSplineComponentVisualizer->AreSplinePointsSelected()) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(ButtonPadding) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "SplineComponentDetails.SelectPrev") + .ContentPadding(2) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("SelectPrevSplinePointToolTip", "Select previous spline point.")) + .OnClicked_Lambda([=]() + { + HoudiniSplineComponentVisualizer->ExtendSplinePointSelection(1, true, false); + return FReply::Handled(); + }) + .IsEnabled(HoudiniSplineComponentVisualizer->AreSplinePointsSelected()) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(ButtonPadding) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "SplineComponentDetails.SelectAll") + .ContentPadding(2) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("SelectAllSplinePointToolTip", "Select all spline points.")) + .OnClicked_Lambda([=]() + { + if (!HoudiniSplineComponent) + return FReply::Handled(); + + int32 NumTotalPoints = HoudiniSplineComponent->GetCurvePointCount(); + for (int32 Idx = 0; Idx < NumTotalPoints; ++Idx) + HoudiniSplineComponentVisualizer->SelectSplinePoint(Idx, true); + return FReply::Handled(); + }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(ButtonPadding) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "SplineComponentDetails.SelectNext") + .ContentPadding(2) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("SelectNextSplinePointToolTip", "Select next spline point.")) + .OnClicked_Lambda([=]() + { + HoudiniSplineComponentVisualizer->ExtendSplinePointSelection(1, false, false); + return FReply::Handled(); + }) + .IsEnabled(HoudiniSplineComponentVisualizer->AreSplinePointsSelected()) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(ButtonPadding) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "SplineComponentDetails.AddNext") + .ContentPadding(2) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("SelectAddNextSplinePointToolTip", "Add next spline point to current selection.")) + .OnClicked_Lambda([=]() + { + HoudiniSplineComponentVisualizer->ExtendSplinePointSelection(1, false, true); + return FReply::Handled(); + }) + .IsEnabled(HoudiniSplineComponentVisualizer->AreSplinePointsSelected()) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(ButtonPadding) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "SplineComponentDetails.SelectLast") + .ContentPadding(2) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("SelectLastSplinePointToolTip", "Select last spline point.")) + .OnClicked_Lambda([=]() + { + if (!HoudiniSplineComponent) + return FReply::Handled(); + int32 NumTotalPoints = HoudiniSplineComponent->GetCurvePointCount(); + HoudiniSplineComponentVisualizer->SelectSplinePoint(NumTotalPoints - 1, false); + return FReply::Handled(); + }) + ] + ] + ]; + + TArray& EditedControlPointsIndexes = HoudiniSplineComponent->EditedControlPointsIndexes; + + auto GetSelectedPointsAsString = [](const TArray& SelectedPointIndexes) + { + FString FormattedString; + for (int32 i = 0; i < SelectedPointIndexes.Num(); ++i) + { + FormattedString += FText::AsNumber(SelectedPointIndexes[i]).ToString(); + if (i + 1 < SelectedPointIndexes.Num()) + FormattedString += TEXT(", "); + } + return FormattedString; + }; + + auto UpdateSelectedPointsFromString = [MainInput, InInputs, HoudiniSplineComponentVisualizer, &CategoryBuilder](FString InputString) + { + if (!IsValidWeakPointer(MainInput)) + return; + + TArray>* CurveInputs = MainInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurveInputs) + return; + + int32 SplineIdx = -1; + UHoudiniSplineComponent* TargetSpline = HoudiniSplineComponentVisualizer->GetEditedHoudiniSplineComponent(); + for (int32 Idx = 0; Idx < CurveInputs->Num(); ++Idx) + { + UHoudiniInputHoudiniSplineComponent* CurSplineInput = Cast((*CurveInputs)[Idx]); + UHoudiniSplineComponent* CurSpline = CurSplineInput->GetCurveComponent(); + if (CurSpline == TargetSpline) + { + SplineIdx = Idx; + break; + } + } + + if (SplineIdx == -1) + return; + + // Parse the selected point indexes + TArray SelectedPointIndexes; + int32 MaxSelectedPointIdx = -1; + { + const int32 MaxPointIdx = TargetSpline->GetCurvePointCount(); + int32 Idx = 0; + int32 InputLen = InputString.Len(); + bool bIsPrevComma = false; + while (Idx < InputLen) + { + // Only allow whitespace and commas. If something else is + // present, then the current input string is invalid + // and the selection update is cancelled. + if (InputString[Idx] == ',') + { + if (bIsPrevComma) + return; + bIsPrevComma = true; + ++Idx; + continue; + } + + // Skip whitespaces + if (InputString[Idx] == ' ') + { + ++Idx; + continue; + } + + // Require the current character to be a digit. + if ('0' > InputString[Idx] || '9' < InputString[Idx]) + return; + + int32 CurPointIdx = 0; + while (Idx < InputLen && '0' <= InputString[Idx] && InputString[Idx] <= '9') + { + CurPointIdx = (CurPointIdx * 10) + (InputString[Idx] - '0'); + ++Idx; + if (CurPointIdx >= MaxPointIdx) + return; + } + if (CurPointIdx > MaxSelectedPointIdx) + MaxSelectedPointIdx = CurPointIdx; + if (!SelectedPointIndexes.Contains(CurPointIdx)) + SelectedPointIndexes.Add(CurPointIdx); + bIsPrevComma = false; + } + } + + // If we have got this far, then we can perform the + // update transaction. + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Updating the selected spline points from key selection."), + MainInput->GetOuter()); + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + TArray>* CurCurveInputs = CurInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurCurveInputs || !CurCurveInputs->IsValidIndex(SplineIdx)) + continue; + + UHoudiniInputHoudiniSplineComponent* CurSplineInput = Cast((*CurCurveInputs)[SplineIdx]); + UHoudiniSplineComponent* CurSpline = CurSplineInput->GetCurveComponent(); + if (!CurSpline) + continue; + + if (MaxSelectedPointIdx >= CurSpline->GetCurvePointCount()) + continue; + + CurSpline->EditedControlPointsIndexes = SelectedPointIndexes; + } + + if (GEditor) + GEditor->RedrawAllViewports(); + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + }; + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .VAlign(VAlign_Center) + .AutoWidth() + .FillWidth(85) + .MaxWidth(85) + [ + SNew(STextBlock) + .Text(LOCTEXT("SelectedKeys", "Selected Points")) + .ToolTipText(LOCTEXT("SelectedKeysTip", "0-indexed collection of comma-separated points currently selected.")) + .Font(_GetEditorStyle().GetFontStyle("PropertyWindow.NormalFont")) + ] + + SHorizontalBox::Slot() + .Padding(5, 0, 0, 0) + .VAlign(VAlign_Center) + .AutoWidth() + .FillWidth(160) + .MaxWidth(160) + [ + SNew(SEditableTextBox) + .MinDesiredWidth(160) + .Text_Lambda([&EditedControlPointsIndexes, GetSelectedPointsAsString]() { + return FText::FromString(GetSelectedPointsAsString(EditedControlPointsIndexes)); + }) + .ToolTipText(LOCTEXT("SelectedKeysInputTip", "Enter the list of points you want to selected as a 0-indexed comma-separated list.")) + .Font(_GetEditorStyle().GetFontStyle("PropertyWindow.NormalFont")) + .OnTextCommitted_Lambda([=](const FText& InputText, ETextCommit::Type CommitType) + { + if (CommitType != ETextCommit::Type::OnEnter && CommitType != ETextCommit::Type::OnUserMovedFocus) + return; + UpdateSelectedPointsFromString(InputText.ToString()); + }) + ] + ]; + + if (!HoudiniSplineComponentVisualizer->AreSplinePointsSelected()) + { + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(STextBlock) + .Text(LOCTEXT("NoPointsSelected", "No control points are selected.")) + .ToolTipText(LOCTEXT("NoPointsSelectedTip", "Select at least one control point to open the transform menu.")) + .Font(_GetEditorStyle().GetFontStyle("PropertyWindow.NormalFont")) + ]; + return; + } + + // From here on forward, we are guaranteed to have at least one point selected. + + // Find the input fields which have multiple values. + TArray& CurvePoints = HoudiniSplineComponent->CurvePoints; + + bool IsSingleValue[3][3]; + FTransform Transform = CurvePoints[EditedControlPointsIndexes[0]]; + for (int32 i = 0; i < 3; ++i) + { + for (int32 j = 0; j < 3; ++j) + IsSingleValue[i][j] = 1; + } + + for (int32 i = 1; i < EditedControlPointsIndexes.Num(); ++i) + { + const FTransform& CurTransform = CurvePoints[EditedControlPointsIndexes[i]]; + + // Equal to UE_KINDA_SMALL_NUMBER used by UE + const float Threshold = 1.e-4f; + + // Translate + IsSingleValue[0][0] &= FMath::Abs(CurTransform.GetLocation().X - Transform.GetLocation().X) < Threshold; + IsSingleValue[0][1] &= FMath::Abs(CurTransform.GetLocation().Y - Transform.GetLocation().Y) < Threshold; + IsSingleValue[0][2] &= FMath::Abs(CurTransform.GetLocation().Z - Transform.GetLocation().Z) < Threshold; + + // Rotate + IsSingleValue[1][0] &= FMath::Abs(CurTransform.GetRotation().Rotator().Roll - Transform.GetRotation().Rotator().Roll) < Threshold; + IsSingleValue[1][1] &= FMath::Abs(CurTransform.GetRotation().Rotator().Pitch - Transform.GetRotation().Rotator().Pitch) < Threshold; + IsSingleValue[1][2] &= FMath::Abs(CurTransform.GetRotation().Rotator().Yaw - Transform.GetRotation().Rotator().Yaw) < Threshold; + + // Scale + IsSingleValue[2][0] &= FMath::Abs(CurTransform.GetScale3D().X - Transform.GetScale3D().X) < Threshold; + IsSingleValue[2][1] &= FMath::Abs(CurTransform.GetScale3D().Y - Transform.GetScale3D().Y) < Threshold; + IsSingleValue[2][2] &= FMath::Abs(CurTransform.GetScale3D().Z - Transform.GetScale3D().Z) < Threshold; + } + + const FTransform& HoudiniSplineComponentTransform = HoudiniSplineComponent->GetComponentTransform(); + + const bool bUseAbsLocation = MainInput->GetCurvePointSelectionUseAbsLocation(); + if (bUseAbsLocation) + { + FVector AbsoluteLocation = HoudiniSplineComponentTransform.TransformPosition(Transform.GetLocation()); + Transform.SetLocation(AbsoluteLocation); + } + + const bool bUseAbsRotation = MainInput->GetCurvePointSelectionUseAbsRotation(); + if (bUseAbsRotation) + { + FQuat AbsoluteRotation = HoudiniSplineComponentTransform.GetRotation() * Transform.GetRotation(); + Transform.SetRotation(AbsoluteRotation); + } + + auto UpdateTransformLocation = [MainInput, InInputs, HoudiniSplineComponentVisualizer](FVector InNewLocation, bool bUpdateX, bool bUpdateY, bool bUpdateZ, bool bUseAbsLocation) + { + if (!IsValidWeakPointer(MainInput)) + return; + + TArray>* CurveInputs = MainInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurveInputs) + return; + + int32 SplineIdx = -1; + UHoudiniSplineComponent* TargetSpline = HoudiniSplineComponentVisualizer->GetEditedHoudiniSplineComponent(); + for (int32 Idx = 0; Idx < CurveInputs->Num(); ++Idx) + { + UHoudiniInputHoudiniSplineComponent* CurSplineInput = Cast((*CurveInputs)[Idx]); + UHoudiniSplineComponent* CurSpline = CurSplineInput->GetCurveComponent(); + if (CurSpline == TargetSpline) + { + SplineIdx = Idx; + break; + } + } + + if (SplineIdx == -1) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Modifying transform for selected spline points."), + MainInput->GetOuter()); + + const TArray& SplinePointsToUpdate = TargetSpline->EditedControlPointsIndexes; + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + TArray>* CurCurveInputs = CurInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurCurveInputs || !CurCurveInputs->IsValidIndex(SplineIdx)) + continue; + + UHoudiniInputHoudiniSplineComponent* CurSplineInput = Cast((*CurCurveInputs)[SplineIdx]); + UHoudiniSplineComponent* CurSpline = CurSplineInput->GetCurveComponent(); + if (!CurSpline) + continue; + + CurInput->Modify(); + + // If we are passing an absolute location, then convert it back to relative + // before storing it back into the spline. + FVector CurInNewLocation = InNewLocation; + if (bUseAbsLocation) + { + const FTransform& CurSplineTransform = CurSpline->GetComponentTransform(); + CurInNewLocation = CurSplineTransform.InverseTransformPosition(CurInNewLocation); + } + + const TArray& CurCurvePoints = CurSpline->CurvePoints; + for (const int32& Idx : SplinePointsToUpdate) + { + if (!CurCurvePoints.IsValidIndex(Idx)) + continue; + FTransform CurPoint = CurCurvePoints[Idx]; + FVector NewLocation = CurPoint.GetLocation(); + if (bUpdateX) + NewLocation.X = CurInNewLocation.X; + if (bUpdateY) + NewLocation.Y = CurInNewLocation.Y; + if (bUpdateZ) + NewLocation.Z = CurInNewLocation.Z; + CurPoint.SetLocation(NewLocation); + CurSpline->EditPointAtindex(CurPoint, Idx); + } + + CurInput->MarkChanged(true); + } + }; + + auto UpdateTransformRotation = [MainInput, InInputs, &CategoryBuilder, HoudiniSplineComponentVisualizer](FRotator InNewRotator, bool bUpdateRoll, bool bUpdatePitch, bool bUpdateYaw, bool bUseAbsRotation) + { + if (!IsValidWeakPointer(MainInput)) + return; + + TArray>* CurveInputs = MainInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurveInputs) + return; + + int32 SplineIdx = -1; + UHoudiniSplineComponent* TargetSpline = HoudiniSplineComponentVisualizer->GetEditedHoudiniSplineComponent(); + for (int32 Idx = 0; Idx < CurveInputs->Num(); ++Idx) + { + UHoudiniInputHoudiniSplineComponent* CurSplineInput = Cast((*CurveInputs)[Idx]); + UHoudiniSplineComponent* CurSpline = CurSplineInput->GetCurveComponent(); + if (CurSpline == TargetSpline) + { + SplineIdx = Idx; + break; + } + } + + if (SplineIdx == -1) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Modifying rotation for selected spline points."), + MainInput->GetOuter()); + + const TArray& SplinePointsToUpdate = TargetSpline->EditedControlPointsIndexes; + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + TArray>* CurCurveInputs = CurInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurCurveInputs || !CurCurveInputs->IsValidIndex(SplineIdx)) + continue; + + UHoudiniInputHoudiniSplineComponent* CurSplineInput = Cast((*CurCurveInputs)[SplineIdx]); + UHoudiniSplineComponent* CurSpline = CurSplineInput->GetCurveComponent(); + if (!CurSpline) + continue; + + CurInput->Modify(); + + FRotator CurInNewRotator = InNewRotator; + if (bUseAbsRotation) + { + const FTransform& CurSplineTransform = CurSpline->GetComponentTransform(); + CurInNewRotator = (CurSplineTransform.GetRotation().Inverse() * FQuat::MakeFromRotator(InNewRotator)).Rotator(); + } + + const TArray& CurCurvePoints = CurSpline->CurvePoints; + for (const int32& Idx : SplinePointsToUpdate) + { + if (!CurCurvePoints.IsValidIndex(Idx)) + continue; + FTransform CurPoint = CurCurvePoints[Idx]; + FQuat NewRotation = CurPoint.GetRotation(); + FRotator NewRotator = NewRotation.Rotator(); + if (bUpdateRoll) + NewRotator.Roll = CurInNewRotator.Roll; + if (bUpdatePitch) + NewRotator.Pitch = CurInNewRotator.Pitch; + if (bUpdateYaw) + NewRotator.Yaw = CurInNewRotator.Yaw; + CurPoint.SetRotation(FQuat::MakeFromRotator(NewRotator)); + CurSpline->EditPointAtindex(CurPoint, Idx); + } + + CurInput->MarkChanged(true); + } + }; + + auto UpdateTransformScale = [MainInput, InInputs, HoudiniSplineComponentVisualizer](FVector InNewScale, bool bUpdateX, bool bUpdateY, bool bUpdateZ) + { + if (!IsValidWeakPointer(MainInput)) + return; + + TArray>* CurveInputs = MainInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurveInputs) + return; + + int32 SplineIdx = -1; + UHoudiniSplineComponent* TargetSpline = HoudiniSplineComponentVisualizer->GetEditedHoudiniSplineComponent(); + for (int32 Idx = 0; Idx < CurveInputs->Num(); ++Idx) + { + UHoudiniInputHoudiniSplineComponent* CurSplineInput = Cast((*CurveInputs)[Idx]); + UHoudiniSplineComponent* CurSpline = CurSplineInput->GetCurveComponent(); + if (CurSpline == TargetSpline) + { + SplineIdx = Idx; + break; + } + } + + if (SplineIdx == -1) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Modifying scale for selected spline points."), + MainInput->GetOuter()); + + const TArray& SplinePointsToUpdate = TargetSpline->EditedControlPointsIndexes; + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + TArray>* CurCurveInputs = CurInput->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!CurCurveInputs || !CurCurveInputs->IsValidIndex(SplineIdx)) + continue; + + UHoudiniInputHoudiniSplineComponent* CurSplineInput = Cast((*CurCurveInputs)[SplineIdx]); + UHoudiniSplineComponent* CurSpline = CurSplineInput->GetCurveComponent(); + if (!CurSpline) + continue; + + CurInput->Modify(); + + const TArray& CurCurvePoints = CurSpline->CurvePoints; + for (const int32& Idx : SplinePointsToUpdate) + { + if (!CurCurvePoints.IsValidIndex(Idx)) + continue; + FTransform CurPoint = CurCurvePoints[Idx]; + FVector NewScale = CurPoint.GetScale3D(); + if (bUpdateX) + NewScale.X = InNewScale.X; + if (bUpdateY) + NewScale.Y = InNewScale.Y; + if (bUpdateZ) + NewScale.Z = InNewScale.Z; + CurPoint.SetScale3D(NewScale); + CurSpline->EditPointAtindex(CurPoint, Idx); + } + + CurInput->MarkChanged(true); + } + }; + + // Add Transform UI + TSharedPtr TransformOffset_VerticalBox; + + InVerticalBox->AddSlot() + .Padding(0) + .AutoHeight() + [ + SAssignNew(TransformOffset_VerticalBox, SVerticalBox) + ]; + + auto GetUseAbsoluteLocation = [MainInput](const bool& bExpectedState) + { + if (!IsValidWeakPointer(MainInput)) + return false; + return MainInput->GetCurvePointSelectionUseAbsLocation() == bExpectedState; + }; + + auto SetUseAbsoluteLocation = [MainInput, &CategoryBuilder](const TArray>& InInputsToUpdate, const bool &bNewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + if (MainInput->GetCurvePointSelectionUseAbsLocation() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Change curve point use absolute location."), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetCurvePointSelectionUseAbsLocation() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetCurvePointSelectionUseAbsLocation(bNewState); + } + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + }; + + FMenuBuilder LocationMenu(true, NULL, NULL); + + FUIAction SetRelativeLocation + ( + FExecuteAction::CreateLambda([=]() + { + SetUseAbsoluteLocation(InInputs, false); + }), + FCanExecuteAction(), + FIsActionChecked::CreateLambda([=]() + { + return GetUseAbsoluteLocation(false); + }) + ); + + FUIAction SetAbsoluteLocation + ( + FExecuteAction::CreateLambda([=]() + { + SetUseAbsoluteLocation(InInputs, true); + }), + FCanExecuteAction(), + FIsActionChecked::CreateLambda([=]() + { + return GetUseAbsoluteLocation(true); + }) + ); + + LocationMenu.BeginSection(TEXT("TransformType"), FText(LOCTEXT("TransformType", "Location Type"))); + + LocationMenu.AddMenuEntry + ( + FText(LOCTEXT("LocationRelativeLabel", "Relative")), + FText(LOCTEXT("LocationRelativeLabelTip", "Location is relative to its parents.")), + FSlateIcon(), + SetRelativeLocation, + NAME_None, + EUserInterfaceActionType::RadioButton + ); + + + LocationMenu.AddMenuEntry + ( + FText(LOCTEXT("LocationRelativeLabel", "Absolute")), + FText(LOCTEXT("LocationRelativeLabelTip", "Location is absolute to the world.")), + FSlateIcon(), + SetAbsoluteLocation, + NAME_None, + EUserInterfaceActionType::RadioButton + ); + + LocationMenu.EndSection(); + + // Translate + TransformOffset_VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .VAlign(VAlign_Center) + .FillWidth(80) + .MaxWidth(80) + [ + SNew(SComboButton) + .ContentPadding(0) + .MenuContent() + [ + LocationMenu.MakeWidget() + ] + .ButtonContent() + [ + SNew(STextBlock) + .Text(bUseAbsLocation ? LOCTEXT("LocationType", "Absolute Location") : LOCTEXT("LocationType", "Location")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .FillWidth(240) + .MaxWidth(240) + [ + SNew(SVectorInputBox) + .bColorAxisLabels(true) + .AllowSpin(true) + .X(IsSingleValue[0][0] ? TOptional(Transform.GetLocation().X) : TOptional()) + .Y(IsSingleValue[0][1] ? TOptional(Transform.GetLocation().Y) : TOptional()) + .Z(IsSingleValue[0][2] ? TOptional(Transform.GetLocation().Z) : TOptional()) + .OnXCommitted_Lambda([=](float InNewX, ETextCommit::Type CommitType) + { + if (CommitType != ETextCommit::Type::OnEnter && CommitType != ETextCommit::Type::OnUserMovedFocus) + return; + UpdateTransformLocation(FVector(InNewX, 0, 0), true, false, false, bUseAbsLocation); + }) + .OnYCommitted_Lambda([=](float InNewY, ETextCommit::Type CommitType) + { + if (CommitType != ETextCommit::Type::OnEnter && CommitType != ETextCommit::Type::OnUserMovedFocus) + return; + UpdateTransformLocation(FVector(0, InNewY, 0), false, true, false, bUseAbsLocation); + }) + .OnZCommitted_Lambda([=](float InNewZ, ETextCommit::Type CommitType) + { + if (CommitType != ETextCommit::Type::OnEnter && CommitType != ETextCommit::Type::OnUserMovedFocus) + return; + UpdateTransformLocation(FVector(0, 0, InNewZ), false, false, true, bUseAbsLocation); + }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Right) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + .Padding(0) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ClickMethod(EButtonClickMethod::MouseDown) + .Visibility(EVisibility::Hidden) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Icons.Lock")) + ] + ] + ] + ]; + + // Rotation + auto GetUseAbsoluteRotation = [MainInput](const bool& bExpectedState) + { + if (!IsValidWeakPointer(MainInput)) + return false; + return MainInput->GetCurvePointSelectionUseAbsRotation() == bExpectedState; + }; + + auto SetUseAbsoluteRotation = [MainInput, &CategoryBuilder](const TArray>& InInputsToUpdate, const bool &bNewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + if (MainInput->GetCurvePointSelectionUseAbsRotation() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Change curve point use absolute Rotation."), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetCurvePointSelectionUseAbsRotation() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetCurvePointSelectionUseAbsRotation(bNewState); + } + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + }; + + FMenuBuilder RotationMenu(true, NULL, NULL); + + FUIAction SetRelativeRotation + ( + FExecuteAction::CreateLambda([=]() + { + SetUseAbsoluteRotation(InInputs, false); + }), + FCanExecuteAction(), + FIsActionChecked::CreateLambda([=]() + { + return GetUseAbsoluteRotation(false); + }) + ); + + FUIAction SetAbsoluteRotation + ( + FExecuteAction::CreateLambda([=]() + { + SetUseAbsoluteRotation(InInputs, true); + }), + FCanExecuteAction(), + FIsActionChecked::CreateLambda([=]() + { + return GetUseAbsoluteRotation(true); + }) + ); + + RotationMenu.BeginSection(TEXT("TransformType"), FText(LOCTEXT("TransformType", "Rotation Type"))); + + RotationMenu.AddMenuEntry + ( + FText(LOCTEXT("RotationRelativeLabel", "Relative")), + FText(LOCTEXT("RotationRelativeLabelTip", "Rotation is relative to its parents.")), + FSlateIcon(), + SetRelativeRotation, + NAME_None, + EUserInterfaceActionType::RadioButton + ); + + + RotationMenu.AddMenuEntry + ( + FText(LOCTEXT("RotationRelativeLabel", "Absolute")), + FText(LOCTEXT("RotationRelativeLabelTip", "Rotation is absolute to the world.")), + FSlateIcon(), + SetAbsoluteRotation, + NAME_None, + EUserInterfaceActionType::RadioButton + ); + + RotationMenu.EndSection(); + + TransformOffset_VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .VAlign(VAlign_Center) + .FillWidth(80) + .MaxWidth(80) + [ + SNew(SComboButton) + .ContentPadding(0) + .MenuContent() + [ + RotationMenu.MakeWidget() + ] + .ButtonContent() + [ + SNew(STextBlock) + .Text(bUseAbsRotation ? LOCTEXT("RotationType", "Absolute Rotation") : LOCTEXT("RotationType", "Rotation")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .FillWidth(240) + .MaxWidth(240) + [ + SNew(SRotatorInputBox) + .AllowSpin(true) + .bColorAxisLabels(true) + .Roll(IsSingleValue[1][0] ? TOptional(Transform.GetRotation().Rotator().Roll) : TOptional()) + .Pitch(IsSingleValue[1][1] ? TOptional(Transform.GetRotation().Rotator().Pitch) : TOptional()) + .Yaw(IsSingleValue[1][2] ? TOptional(Transform.GetRotation().Rotator().Yaw) : TOptional()) + .OnRollCommitted_Lambda([=](float InNewRoll, ETextCommit::Type CommitType) + { + if (CommitType != ETextCommit::Type::OnEnter && CommitType != ETextCommit::Type::OnUserMovedFocus) + return; + UpdateTransformRotation(FRotator(InNewRoll, 0, 0), true, false, false, bUseAbsRotation); + }) + .OnPitchCommitted_Lambda([=](float InNewPitch, ETextCommit::Type CommitType) + { + if (CommitType != ETextCommit::Type::OnEnter && CommitType != ETextCommit::Type::OnUserMovedFocus) + return; + UpdateTransformRotation(FRotator(0, InNewPitch, 0), false, true, false, bUseAbsRotation); + }) + .OnYawCommitted_Lambda([=](float InNewYaw, ETextCommit::Type CommitType) + { + if (CommitType != ETextCommit::Type::OnEnter && CommitType != ETextCommit::Type::OnUserMovedFocus) + return; + UpdateTransformRotation(FRotator(0, 0, InNewYaw), false, false, true, bUseAbsRotation); + }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Right) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + .Padding(0.0f) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ClickMethod(EButtonClickMethod::MouseDown) + .Visibility(EVisibility::Hidden) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Icons.Lock")) + ] + ] + ] + ]; + + const bool &bScaleLocked = HoudiniInputObject->IsUniformScaleLocked(); + + TransformOffset_VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0) + .VAlign(VAlign_Center) + .FillWidth(80) + .MaxWidth(80) + [ + SNew(STextBlock) + .Text(LOCTEXT("CurvePointInputScale", "Scale")) + .ToolTipText(LOCTEXT("CurvePointInputScaleTooltip", "Scale")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .FillWidth(240) + .MaxWidth(240) + [ + SNew(SVectorInputBox) + .bColorAxisLabels(true) + .X(IsSingleValue[2][0] ? TOptional(Transform.GetScale3D().X) : TOptional()) + .Y(IsSingleValue[2][1] ? TOptional(Transform.GetScale3D().Y) : TOptional()) + .Z(IsSingleValue[2][2] ? TOptional(Transform.GetScale3D().Z) : TOptional()) + .OnXCommitted_Lambda([=](float InNewX, ETextCommit::Type CommitType) + { + if (CommitType != ETextCommit::Type::OnEnter && CommitType != ETextCommit::Type::OnUserMovedFocus) + return; + if (bScaleLocked) + UpdateTransformScale(FVector(InNewX, InNewX, InNewX), true, true, true); + else + UpdateTransformScale(FVector(InNewX, 0, 0), true, false, false); + }) + .OnYCommitted_Lambda([=](float InNewY, ETextCommit::Type CommitType) + { + if (CommitType != ETextCommit::Type::OnEnter && CommitType != ETextCommit::Type::OnUserMovedFocus) + return; + if (bScaleLocked) + UpdateTransformScale(FVector(InNewY, InNewY, InNewY), true, true, true); + else + UpdateTransformScale(FVector(0, InNewY, 0), false, true, false); + }) + .OnZCommitted_Lambda([=](float InNewZ, ETextCommit::Type CommitType) + { + if (CommitType != ETextCommit::Type::OnEnter && CommitType != ETextCommit::Type::OnUserMovedFocus) + return; + if (bScaleLocked) + UpdateTransformScale(FVector(InNewZ, InNewZ, InNewZ), true, true, true); + else + UpdateTransformScale(FVector(0, 0, InNewZ), false, false, true); + }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Right) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + .Padding(0) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ClickMethod(EButtonClickMethod::MouseDown) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([=, &CategoryBuilder]() + { + for (auto& CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + UHoudiniInputObject* CurInputObject = CurInput->GetHoudiniInputObjectAt(EHoudiniInputType::Curve, HoudiniInputObjectIdx); + + if (!IsValid(CurInputObject)) + continue; + + CurInputObject->SwitchUniformScaleLock(); + } + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + + return FReply::Handled(); + }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush(bScaleLocked ? TEXT("Icons.Lock") : TEXT("Icons.Unlock"))) + ] + + ] + ] + ]; +} + +FMenuBuilder +FHoudiniInputDetails::Helper_CreateWorldActorPickerWidget(const TArray>& InInputs) +{ + const TWeakObjectPtr& MainInput = InInputs.Num() > 0 ? InInputs[0] : nullptr; + + auto OnShouldFilterWorld = [MainInput](const AActor* const Actor) + { + if (!IsValidWeakPointer(MainInput)) + return true; + + const TArray>* InputObjects = MainInput->GetHoudiniInputObjectArray(EHoudiniInputType::World); + if (!InputObjects) + return false; + + // Only return actors that are currently selected by our input + for (const auto& CurInputObject : *InputObjects) + { + if (!IsValid(CurInputObject)) + continue; + + AActor* CurActor = Cast(CurInputObject->GetObject()); + if (!IsValid(CurActor)) + { + // See if the input object is a Cookable, if it is, get its parent actor + UHoudiniCookable* CurHC = Cast(CurInputObject->GetObject()); + if (IsValid(CurHC)) + CurActor = CurHC->GetOwner(); + } + + if (!IsValid(CurActor)) + continue; + + if (CurActor == Actor) + return true; + } + + return false; + }; + + auto OnWorldSelected = [](AActor* Actor) + { + // Do Nothing + }; + + FMenuBuilder MenuBuilder(true, nullptr); + FOnShouldFilterActor ActorFilter = FActorTreeItem::FFilterPredicate::CreateLambda(OnShouldFilterWorld); + + MenuBuilder.BeginSection(NAME_None, LOCTEXT("WorldInputSelectedActors", "Currently Selected Actors")); + { + FSceneOutlinerModule & SceneOutlinerModule = + FModuleManager::Get().LoadModuleChecked< FSceneOutlinerModule >(TEXT("SceneOutliner")); + FSceneOutlinerInitializationOptions InitOptions; + { + InitOptions.Filters->AddFilterPredicate(ActorFilter); + InitOptions.bFocusSearchBoxWhenOpened = false; + InitOptions.bShowCreateNewFolder = false; + + // Add the gutter so we can change the selection's visibility + InitOptions.ColumnMap.Add(FSceneOutlinerBuiltInColumnTypes::Gutter(), FSceneOutlinerColumnInfo(ESceneOutlinerColumnVisibility::Visible, 0, FCreateSceneOutlinerColumn(), true, TOptional(), FSceneOutlinerBuiltInColumnTypes::Gutter_Localized())); + InitOptions.ColumnMap.Add(FSceneOutlinerBuiltInColumnTypes::Label(), FSceneOutlinerColumnInfo(ESceneOutlinerColumnVisibility::Visible, 10, FCreateSceneOutlinerColumn(), true, TOptional(), FSceneOutlinerBuiltInColumnTypes::Label_Localized())); + InitOptions.ColumnMap.Add(FSceneOutlinerBuiltInColumnTypes::ActorInfo(), FSceneOutlinerColumnInfo(ESceneOutlinerColumnVisibility::Visible, 20, FCreateSceneOutlinerColumn(), true, TOptional(), FSceneOutlinerBuiltInColumnTypes::ActorInfo_Localized())); + } + + static const FVector2D SceneOutlinerWindowSize(350.0f, 200.0f); + TSharedRef< SWidget > MenuWidget = + SNew(SBox) + .WidthOverride(SceneOutlinerWindowSize.X) + .HeightOverride(SceneOutlinerWindowSize.Y) + [ + SNew(SBorder) + .BorderImage(_GetEditorStyle().GetBrush("Menu.Background")) + [ + SceneOutlinerModule.CreateActorPicker( + InitOptions, + FOnActorPicked::CreateLambda(OnWorldSelected)) + ] + ]; + + MenuBuilder.AddWidget(MenuWidget, FText::GetEmpty(), true); + } + MenuBuilder.EndSection(); + + return MenuBuilder; +} + +FMenuBuilder +FHoudiniInputDetails::Helper_CreateBoundSelectorPickerWidget(const TArray>& InInputs) +{ + const TWeakObjectPtr& MainInput = InInputs.Num() > 0 ? InInputs[0] : nullptr; + + auto OnShouldFilter = [MainInput](const AActor* const Actor) + { + if (!IsValid(Actor)) + return false; + + const TArray>* BoundObjects = MainInput->GetBoundSelectorObjectArray(); + if (!BoundObjects) + return false; + + // Only return actors that are currently selected by our input + for (const auto& CurActor : *BoundObjects) + { + if (!IsValid(CurActor)) + continue; + + if (CurActor == Actor) + return true; + } + + return false; + }; + + + auto OnSelected = [](AActor* Actor) + { + // Do Nothing + }; + + FMenuBuilder MenuBuilder(true, nullptr); + MenuBuilder.BeginSection(NAME_None, LOCTEXT("WorldInputBoundSelectors", "Bound Selectors")); + { + FSceneOutlinerModule & SceneOutlinerModule = + FModuleManager::Get().LoadModuleChecked< FSceneOutlinerModule >(TEXT("SceneOutliner")); + FSceneOutlinerInitializationOptions InitOptions; + { + InitOptions.Filters->AddFilterPredicate(FActorTreeItem::FFilterPredicate::CreateLambda(OnShouldFilter)); + InitOptions.bFocusSearchBoxWhenOpened = false; + InitOptions.bShowCreateNewFolder = false; + + // Add the gutter so we can change the selection's visibility + InitOptions.ColumnMap.Add(FSceneOutlinerBuiltInColumnTypes::Gutter(), FSceneOutlinerColumnInfo(ESceneOutlinerColumnVisibility::Visible, 0, FCreateSceneOutlinerColumn(), true, TOptional(), FSceneOutlinerBuiltInColumnTypes::Gutter_Localized())); + InitOptions.ColumnMap.Add(FSceneOutlinerBuiltInColumnTypes::Label(), FSceneOutlinerColumnInfo(ESceneOutlinerColumnVisibility::Visible, 10, FCreateSceneOutlinerColumn(), true, TOptional(), FSceneOutlinerBuiltInColumnTypes::Label_Localized())); + InitOptions.ColumnMap.Add(FSceneOutlinerBuiltInColumnTypes::ActorInfo(), FSceneOutlinerColumnInfo(ESceneOutlinerColumnVisibility::Visible, 20, FCreateSceneOutlinerColumn(), true, TOptional(), FSceneOutlinerBuiltInColumnTypes::ActorInfo_Localized())); + } + + static const FVector2D SceneOutlinerWindowSize(350.0f, 200.0f); + TSharedRef< SWidget > MenuWidget = + SNew(SBox) + .WidthOverride(SceneOutlinerWindowSize.X) + .HeightOverride(SceneOutlinerWindowSize.Y) + [ + SNew(SBorder) + .BorderImage(_GetEditorStyle().GetBrush("Menu.Background")) + [ + SceneOutlinerModule.CreateActorPicker( + InitOptions, + FOnActorPicked::CreateLambda(OnSelected)) + ] + ]; + + MenuBuilder.AddWidget(MenuWidget, FText::GetEmpty(), true); + } + MenuBuilder.EndSection(); + + return MenuBuilder; +} + +void +FHoudiniInputDetails::AddGeometryInputUI( + IDetailCategoryBuilder& CategoryBuilder, + TSharedRef InVerticalBox, + const TArray>& InInputs, + TSharedPtr AssetThumbnailPool) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + if (!IsValidWeakPointer(MainInput)) + return; + + const int32 NumInputObjects = MainInput->GetNumberOfInputObjects(EHoudiniInputType::Geometry); + + auto SetInputObjectsCount = [MainInput, &CategoryBuilder](const TArray>& InInputsToUpdate, const int32& NewInputCount) + { + if (!IsValidWeakPointer(MainInput)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Number of New Geometry Input Objects"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetNumberOfInputObjects(EHoudiniInputType::Geometry) == NewInputCount) + continue; + + CurInput->Modify(); + CurInput->SetInputObjectsNumber(EHoudiniInputType::Geometry, NewInputCount); + CurInput->MarkChanged(true); + + if (GEditor) + GEditor->RedrawAllViewports(); + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + } + }; + + auto GeometryInputsStateChanged = [MainInput, InInputs, &CategoryBuilder](bool bIsExpanded) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = bIsExpanded; + + if (MainInput->GetGeometryInputsMenuExpanded() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed New Geometry Inputs Menu State"), + MainInput->GetOuter()); + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetGeometryInputsMenuExpanded() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetGeometryInputsMenuExpanded(bNewState); + + if (GEditor) + GEditor->RedrawAllViewports(); + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + } + }; + + AddExportOptions(InVerticalBox, InInputs); + + FText InputsMenuTitle = FText::Format( + NumInputObjects == 1 ? + LOCTEXT("NumArrayItemsFmt", "{0} Input Selected") : + LOCTEXT("NumArrayItemsFmt", "{0} Inputs Selected"), + FText::AsNumber(NumInputObjects)); + + TSharedRef Inputs_Expandable = SNew(SExpandableArea) + .AreaTitle(InputsMenuTitle) + .InitiallyCollapsed(!MainInput->GetGeometryInputsMenuExpanded()) + .OnAreaExpansionChanged(FOnBooleanValueChanged::CreateLambda(GeometryInputsStateChanged)); + + TSharedRef InputsCollapsed_VerticalBox = SNew(SVerticalBox).Visibility_Lambda([MainInput]() + { + return MainInput->GetGeometryInputsMenuExpanded() ? EVisibility::Collapsed : EVisibility::Visible; + }); + + TSharedRef InputsExpanded_VerticalBox = SNew(SVerticalBox).Visibility_Lambda([MainInput]() + { + return MainInput->GetGeometryInputsMenuExpanded() ? EVisibility::Visible : EVisibility::Collapsed; + }); + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + Inputs_Expandable + ]; + + InVerticalBox->AddSlot() + .Padding(5, 5, 5, 5) + .AutoHeight() + [ + InputsCollapsed_VerticalBox + ]; + + InVerticalBox->AddSlot() + .Padding(3, 1, 5, 2) + .AutoHeight() + [ + InputsExpanded_VerticalBox + ]; + + + InputsExpanded_VerticalBox->AddSlot() + .Padding(2, 0, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SButton) + .OnClicked_Lambda([SetInputObjectsCount, InInputs, NumInputObjects]() + { + SetInputObjectsCount(InInputs, NumInputObjects + 1); + return FReply::Handled(); + }) + .Content() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Icons.Plus")) + .ColorAndOpacity(FStyleColors::AccentGreen) + ] + + SHorizontalBox::Slot() + .Padding(FMargin(3, 0, 0, 0)) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("AddButtonText", "Add")) + .TextStyle(_GetEditorStyle(), "SmallButtonText") + .ToolTipText(LOCTEXT("AddButtonTip", "Adds a new Geometry input.")) + ] + ] + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .Padding(FMargin(3, 0, 0, 0)) + .AutoWidth() + [ + SNew(SButton) + .OnClicked_Lambda([SetInputObjectsCount, InInputs, NumInputObjects]() + { + // Delete all inputs + SetInputObjectsCount(InInputs, 0); + // Add a default, empty one + SetInputObjectsCount(InInputs, 1); + return FReply::Handled(); + }) + .Content() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Icons.Delete")) + .ColorAndOpacity(FStyleColors::AccentRed) + ] + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("ClearAllButtonText", "Clear All")) + .TextStyle(_GetEditorStyle(), "SmallButtonText") + .ToolTipText(LOCTEXT("ClearAllButtonTip", "Clears all inputs.")) + ] + ] + ] + ]; + + if (NumInputObjects <= 0) + { + InputsExpanded_VerticalBox->AddSlot().Padding(2, 2, 5, 2).AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("NoInputsSelectedText", "No inputs are selected.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ]; + } + + for (int32 CurObjectIdx = 0; CurObjectIdx < NumInputObjects; CurObjectIdx++) + { + Helper_CreateGeometryInputObjectCollapsed(CategoryBuilder, InInputs, CurObjectIdx, InputsCollapsed_VerticalBox, AssetThumbnailPool); + Helper_CreateGeometryInputObjectExpanded(CategoryBuilder, InInputs, CurObjectIdx, InputsExpanded_VerticalBox, AssetThumbnailPool); + } +} + +void +FHoudiniInputDetails::AddWorldInputUI( + IDetailCategoryBuilder& CategoryBuilder, + TSharedRef InVerticalBox, + const TArray>& InInputs, + const IDetailsView* DetailsView) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + if (!IsValidWeakPointer(MainInput)) + return; + + AddExportOptions(InVerticalBox, InInputs); + AddLandscapeOptions(InVerticalBox, InInputs); + AddLandscapeSplinesOptions(InVerticalBox, InInputs); + + bool bIsBoundSelector = MainInput->IsWorldInputBoundSelector(); + + const int32 NumInputObjects = + bIsBoundSelector ? + MainInput->GetNumberOfBoundSelectorObjects() : + MainInput->GetNumberOfInputObjects(EHoudiniInputType::World); + + bool bDetailsLocked = false; + FName DetailsPanelName = "LevelEditorSelectionDetails"; + if (DetailsView) + { + DetailsPanelName = DetailsView->GetIdentifier(); + if (DetailsView->IsLocked()) + bDetailsLocked = true; + } + + auto WorldInputsStateChanged = [MainInput, InInputs, &CategoryBuilder](bool bIsExpanded) + { + if (!IsValidWeakPointer(MainInput)) + return; + + bool bNewState = bIsExpanded; + if (MainInput->GetWorldInputsMenuExpanded() == bNewState) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed New World Inputs Menu State"), + MainInput->GetOuter()); + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetWorldInputsMenuExpanded() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetWorldInputsMenuExpanded(bNewState); + } + + if (GEditor) + GEditor->RedrawAllViewports(); + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + }; + + TSharedRef Inputs_VerticalBox = SNew(SVerticalBox); + + FText InputSelectionType = + bIsBoundSelector ? + LOCTEXT("BoundSelection", "Bound") : + LOCTEXT("InputSelection", "Input"); + + FText InputsMenuTitle = FText::Format( + NumInputObjects == 1 ? + LOCTEXT("NumBoundsSelected", "{0} {1} Selected") : + LOCTEXT("NumInputsSelected", "{0} {1}s Selected"), + FText::AsNumber(NumInputObjects), InputSelectionType); + + InVerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SExpandableArea) + .AreaTitle(InputsMenuTitle) + .InitiallyCollapsed(!MainInput->GetWorldInputsMenuExpanded()) + .OnAreaExpansionChanged(FOnBooleanValueChanged::CreateLambda(WorldInputsStateChanged)) + .BodyContent() + [ + Inputs_VerticalBox + ] + ]; + + // Use Bound Selector CheckBox + Inputs_VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("UseBoundSelector", "Use Bound Selector")) + .ToolTipText(LOCTEXT("UseBoundSelectorTip", "When enabled, this world input works as a bound selector, sending all the objects contained in the bound selector bounding boxes.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return ECheckBoxState::Unchecked; + + return MainInput->IsWorldInputBoundSelector() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([MainInput, &CategoryBuilder, InInputs](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed World Input Use Bound Selector"), + MainInput->GetOuter()); + + bool bNewState = (NewState == ECheckBoxState::Checked); + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->IsWorldInputBoundSelector() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetWorldInputBoundSelector(bNewState); + CurInput->MarkChanged(true); + } + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + }) + ]; + + // Update Selection Automatically CheckBox + Inputs_VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("AutoUpdate", "Update Automatically")) + .ToolTipText(LOCTEXT("AutoUpdateTip", "If enabled, this input will automatically update if its selected objects/actors are changed.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([MainInput]() + { + if (!IsValidWeakPointer(MainInput)) + return ECheckBoxState::Unchecked; + + return MainInput->GetWorldInputAutoUpdates() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([MainInput, InInputs](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(MainInput)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniAutoUpdateChange", "Houdini Input: Changed Update Automatically"), + MainInput->GetOuter()); + + bool bNewState = (NewState == ECheckBoxState::Checked); + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->GetWorldInputAutoUpdates() == bNewState) + continue; + + CurInput->Modify(); + CurInput->SetWorldInputAutoUpdates(bNewState); + CurInput->MarkChanged(true); + } + }) + ]; + + // Start Selection / Use Current Selection Button + FPropertyEditorModule& PropertyModule = + FModuleManager::Get().GetModuleChecked("PropertyEditor"); + + FText SelectButtonText = bDetailsLocked ? + LOCTEXT("WorldInputUseCurrentSelection", "Use Current") : + LOCTEXT("WorldInputStartSelection", "Start Selection"); + + FText SelectButtonTip = bDetailsLocked ? + LOCTEXT("WorldInputUseCurrentSelectionTip", "Unlock details panel and use currently selected objects.") : + LOCTEXT("WorldInputStartSelectionTip", "Lock details panel and select world objects to use as input."); + + FName SelectButtonImage = bDetailsLocked ? + FName("Icons.ArrowDown") : + FName("Icons.Plus"); + + Inputs_VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SButton) + .ToolTipText(SelectButtonTip) + .OnClicked_Lambda([MainInput, InInputs, DetailsPanelName, &CategoryBuilder]() + { + return MainInput->IsWorldInputBoundSelector() ? + Helper_OnButtonClickUseSelectionAsBoundSelector(CategoryBuilder, InInputs, DetailsPanelName) : + Helper_OnButtonClickSelectActors(CategoryBuilder, InInputs, DetailsPanelName); + }) + .Content() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush(SelectButtonImage)) + .ColorAndOpacity(FStyleColors::AccentGreen) + ] + + SHorizontalBox::Slot() + .Padding(FMargin(3, 0, 0, 0)) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(SelectButtonText) + .TextStyle(_GetEditorStyle(), "SmallButtonText") + ] + ] + ] + + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .Padding(FMargin(3, 0, 0, 0)) + .AutoWidth() + [ + SNew(SButton) + .ToolTipText(LOCTEXT("ClearSelectionButtonTip", "Clear Selection - All inputs will be removed from the current selection.")) + .OnClicked_Lambda([InInputs, MainInput, &CategoryBuilder]() + { + if (!IsValidWeakPointer(MainInput)) + return FReply::Handled(); + + const bool bMainInputBoundSelection = MainInput->IsWorldInputBoundSelector(); + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Cleared World Input Selection"), + MainInput->GetOuter()); + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + if (CurInput->IsWorldInputBoundSelector() != bMainInputBoundSelection) + continue; + + CurInput->Modify(); + + if (bMainInputBoundSelection) + { + CurInput->SetBoundSelectorObjectsNumber(0); + CurInput->UpdateWorldSelectionFromBoundSelectors(); + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + } + else + { + TArray EmptySelection; + CurInput->UpdateWorldSelection(EmptySelection); + } + } + + return FReply::Handled(); + }) + .Content() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Icons.Delete")) + .ColorAndOpacity(FStyleColors::AccentRed) + ] + /* + SHorizontalBox::Slot() + .Padding(FMargin(3, 0, 0, 0)) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("ClearSelectionButtonText", "Clear Selection")) + .TextStyle(_GetEditorStyle(), "SmallButtonText") + .ToolTipText(LOCTEXT("ClearSelectionButtonTip", "Clears all inputs.")) + ]*/ + ] + ] + + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .Padding(FMargin(3, 0, 0, 0)) + .AutoWidth() + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RefreshSelectionButtonTextTip", "Refresh Selection - Refresh the current selection and updates them in Houdini if needed.")) + .OnClicked_Lambda([MainInput, InInputs, DetailsPanelName, &CategoryBuilder]() + { + return Helper_OnButtonClickSelectActors( + CategoryBuilder, + InInputs, + DetailsPanelName, + MainInput->IsWorldInputBoundSelector(), + true); + }) + .Content() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Icons.Refresh")) + .ColorAndOpacity(FStyleColors::AccentBlue) + ]/* + + SHorizontalBox::Slot() + .Padding(FMargin(3, 0, 0, 0)) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("RefreshSelectionButtonText", "Refresh Selection")) + .TextStyle(_GetEditorStyle(), "SmallButtonText") + .ToolTipText(LOCTEXT("RefreshSelectionButtonTextTip", "Refresh the current selection and updates them in Houdini if needed.")) + ]*/ + ] + ] + ]; + + if (bIsBoundSelector) + { + FMenuBuilder MenuBuilder = Helper_CreateBoundSelectorPickerWidget(InInputs); + Inputs_VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + MenuBuilder.MakeWidget() + ]; + } + { + FMenuBuilder MenuBuilder = Helper_CreateWorldActorPickerWidget(InInputs); + Inputs_VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + MenuBuilder.MakeWidget() + ]; + } +} + +void +FHoudiniInputDetails::Helper_CreateGeometryInputObjectCollapsed( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const FPlatformTypes::int32& InObjectIdx, + TSharedRef InVerticalBox, + TSharedPtr AssetThumbnailPool) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + if (!IsValidWeakPointer(MainInput)) + return; + + UHoudiniInputObject* HoudiniInputObject = MainInput->GetHoudiniInputObjectAt(EHoudiniInputType::Geometry, InObjectIdx); + UObject* InputObject = HoudiniInputObject ? HoudiniInputObject->GetObject() : nullptr; + + TSharedPtr ObjThumbnail = MakeShareable(new FAssetThumbnail(InputObject, 64, 64, AssetThumbnailPool)); + + // Create thumbnail for this static mesh. + constexpr int32 ThumbnailSize = 30; + + TSharedPtr StaticMeshThumbnail = MakeShareable( + new FAssetThumbnail(InputObject, ThumbnailSize, ThumbnailSize, AssetThumbnailPool)); + + // Lambda for adding new geometry input objects + auto UpdateGeometryObjectAt = [MainInput, &CategoryBuilder](const TArray>& InInputsToUpdate, const int32& AtIndex, UObject* InObject, const bool& bAutoInserMissingObjects) + { + if (!IsValidWeakPointer(MainInput)) + return; + + if (!IsValid(InObject)) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed a Geometry Input Object"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + UObject* InputObject = nullptr; + int32 NumInputObjects = CurInput->GetNumberOfInputObjects(EHoudiniInputType::Geometry); + + if (AtIndex < NumInputObjects) + { + InputObject = CurInput->GetInputObjectAt(EHoudiniInputType::Geometry, AtIndex); + if (InObject == InputObject) + continue; + } + else if (bAutoInserMissingObjects) + { + CurInput->InsertInputObjectAt(EHoudiniInputType::Geometry, AtIndex); + } + else + { + continue; + } + + UHoudiniInputObject* CurrentInputObjectWrapper = CurInput->GetHoudiniInputObjectAt(AtIndex); + if (CurrentInputObjectWrapper) + CurrentInputObjectWrapper->Modify(); + + CurInput->Modify(); + CurInput->SetInputObjectAt(EHoudiniInputType::Geometry, AtIndex, InObject); + CurInput->MarkChanged(true); + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + } + }; + + // Drop Target: Static/Skeletal Mesh + TSharedPtr HorizontalBox = NULL; + InVerticalBox->AddSlot() + .Padding(0.0f) + .AutoHeight() + [ + SNew(SAssetDropTarget) + .bSupportsMultiDrop(true) + .OnAreAssetsAcceptableForDrop_Lambda([](TArrayView InAssets) + { + for (auto& CurAssetData : InAssets) + { + if (UHoudiniInput::IsObjectAcceptable(EHoudiniInputType::Geometry, CurAssetData.GetAsset())) + return true; + } + + return false; + }) + .OnAssetsDropped_Lambda([InInputs, InObjectIdx, UpdateGeometryObjectAt](const FDragDropEvent&, TArrayView InAssets) + { + int32 CurrentObjectIdx = InObjectIdx; + + for (auto& CurAssetData : InAssets) + { + UObject* Object = CurAssetData.GetAsset(); + if (!IsValid(Object)) + continue; + if (!UHoudiniInput::IsObjectAcceptable(EHoudiniInputType::Geometry, Object)) + continue; + // Update the object, inserting new one if necessary + UpdateGeometryObjectAt(InInputs, CurrentObjectIdx++, Object, true); + } + }) + [ + SAssignNew(HorizontalBox, SHorizontalBox) + ] + ]; + + // Thumbnail : Static Mesh + HorizontalBox->AddSlot() + .Padding(0.0f) + .AutoWidth() + [ + SNew(SBox) + .WidthOverride(ThumbnailSize) + .HeightOverride(ThumbnailSize) + [ + StaticMeshThumbnail->MakeThumbnailWidget() + ] + ]; + + FText MeshNameText = FText::GetEmpty(); + if (InputObject) + { + MeshNameText = FText::FromString(InputObject->GetName()); + } + + HorizontalBox->AddSlot() + .FillWidth(1.0f) + .Padding(3, 0, 5, 0) + .VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(MeshNameText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ]; +} + +void +FHoudiniInputDetails::Helper_CreateGeometryInputObjectExpanded( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const FPlatformTypes::int32& InObjectIdx, + TSharedRef InVerticalBox, + TSharedPtr AssetThumbnailPool) +{ + if (InInputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainInput = InInputs[0]; + + if (!IsValidWeakPointer(MainInput)) + return; + + UHoudiniInputObject* HoudiniInputObject = MainInput->GetHoudiniInputObjectAt(EHoudiniInputType::Geometry, InObjectIdx); + UObject* InputObject = HoudiniInputObject ? HoudiniInputObject->GetObject() : nullptr; + + + // Create thumbnail for this static mesh. + constexpr int32 ThumbnailSize = 46; + + TSharedPtr StaticMeshThumbnail = MakeShareable( + new FAssetThumbnail(InputObject, ThumbnailSize, ThumbnailSize, AssetThumbnailPool)); + + // Lambda for adding new geometry input objects + auto UpdateGeometryObjectAt = [MainInput, &CategoryBuilder](const TArray>& InInputsToUpdate, const int32& AtIndex, UObject* InObject, const bool& bAutoInserMissingObjects) + { + if (!IsValidWeakPointer(MainInput)) + return; + + // TODO: Commented out to make reset button functional. Check + // that removing this doesn't create undesired results. + //if (!IsValid(InObject)) + // return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed a Geometry Input Object"), + MainInput->GetOuter()); + + for (auto CurInput : InInputsToUpdate) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + UObject* InputObject = nullptr; + int32 NumInputObjects = CurInput->GetNumberOfInputObjects(EHoudiniInputType::Geometry); + if (AtIndex < NumInputObjects) + { + InputObject = CurInput->GetInputObjectAt(EHoudiniInputType::Geometry, AtIndex); + if (InObject == InputObject) + continue; + } + else if (bAutoInserMissingObjects) + { + CurInput->InsertInputObjectAt(EHoudiniInputType::Geometry, AtIndex); + } + else + { + continue; + } + + UHoudiniInputObject* CurrentInputObjectWrapper = CurInput->GetHoudiniInputObjectAt(AtIndex); + if (CurrentInputObjectWrapper) + CurrentInputObjectWrapper->Modify(); + + CurInput->Modify(); + CurInput->SetInputObjectAt(EHoudiniInputType::Geometry, AtIndex, InObject); + CurInput->MarkChanged(true); + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + } + }; + + TSharedPtr HorizontalBox; + InVerticalBox->AddSlot() + .Padding(0, 5, 0, 0) + .AutoHeight() + [ + SNew(SAssetDropTarget) + .bSupportsMultiDrop(true) + .OnAreAssetsAcceptableForDrop_Lambda([](TArrayView InAssets) + { + for (auto& CurAssetData : InAssets) + { + if (UHoudiniInput::IsObjectAcceptable(EHoudiniInputType::Geometry, CurAssetData.GetAsset())) + return true; + } + + return false; + }) + .OnAssetsDropped_Lambda([InInputs, InObjectIdx, UpdateGeometryObjectAt](const FDragDropEvent&, TArrayView InAssets) + { + int32 CurrentObjectIdx = InObjectIdx; + for (auto& CurAssetData : InAssets) + { + UObject* Object = CurAssetData.GetAsset(); + if (!IsValid(Object)) + continue; + + if (!UHoudiniInput::IsObjectAcceptable(EHoudiniInputType::Geometry, Object)) + continue; + + // Update the object, inserting new one if necessary + UpdateGeometryObjectAt(InInputs, CurrentObjectIdx++, Object, true); + } + }) + [ + SAssignNew(HorizontalBox, SHorizontalBox) + ] + ]; + + + FText ThumbnailTip = FText::GetEmpty(); + if (InputObject) + { + ThumbnailTip = FText::FromString(InputObject->GetDesc()); + } + + HorizontalBox->AddSlot() + .Padding(0) + .AutoWidth() + [ + SNew(SBorder) + .BorderImage(_GetEditorStyle().GetBrush(TEXT("AssetThumbnail.AssetBackground"))) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .OnMouseDoubleClick_Lambda([MainInput, InObjectIdx](const FGeometry&, const FPointerEvent&) + { + UObject* InputObject = MainInput->GetInputObjectAt(EHoudiniInputType::Geometry, InObjectIdx); + if (GEditor && InputObject) + GEditor->EditObject(InputObject); + + return FReply::Handled(); + }) + [ + SNew(SBox) + .WidthOverride(ThumbnailSize) + .HeightOverride(ThumbnailSize) + .ToolTipText(ThumbnailTip) + [ + StaticMeshThumbnail->MakeThumbnailWidget() + ] + ] + ]; + + FText MeshNameText = FText::GetEmpty(); + if (InputObject) + MeshNameText = FText::FromString(InputObject->GetName()); + + TSharedPtr ComboAndButtonBox; + HorizontalBox->AddSlot() + .FillWidth(1) + .Padding(4, 0, 5, 0) + .VAlign(VAlign_Center) + [ + SAssignNew(ComboAndButtonBox, SVerticalBox) + ]; + + // Add Combo box : Static Mesh + TSharedPtr StaticMeshComboButton; + ComboAndButtonBox->AddSlot() + .FillHeight(1) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .Padding(0) + .VAlign(VAlign_Center) + [ + SAssignNew(StaticMeshComboButton, SComboButton) + .ButtonContent() + [ + SNew(STextBlock) + .TextStyle(_GetEditorStyle(), TEXT("PropertyEditor.AssetClass")) + .Font(_GetEditorStyle().GetFontStyle(FName(TEXT("PropertyWindow.NormalFont")))) + .ColorAndOpacity(_GetEditorStyle().GetColor(TEXT("AssetThumbnail"), ".ColorAndOpacity")) + .Text(MeshNameText) + ] + ] + ]; + + + TWeakPtr WeakStaticMeshComboButton(StaticMeshComboButton); + StaticMeshComboButton->SetOnGetMenuContent(FOnGetContent::CreateLambda( + [MainInput, InInputs, InObjectIdx, WeakStaticMeshComboButton, UpdateGeometryObjectAt]() + { + TArray AllowedClasses = UHoudiniInput::GetAllowedClasses(EHoudiniInputType::Geometry); + UObject* DefaultObj = MainInput->GetInputObjectAt(InObjectIdx); + + TArray NewAssetFactories; + return PropertyCustomizationHelpers::MakeAssetPickerWithMenu( + FAssetData(DefaultObj), + true, + AllowedClasses, + NewAssetFactories, + FOnShouldFilterAsset(), + FOnAssetSelected::CreateLambda( + [InInputs, InObjectIdx, WeakStaticMeshComboButton, UpdateGeometryObjectAt](const FAssetData& AssetData) + { + TSharedPtr ComboButton = WeakStaticMeshComboButton.Pin(); + if (ComboButton.IsValid()) + { + ComboButton->SetIsOpen(false); + UObject* Object = AssetData.GetAsset(); + UpdateGeometryObjectAt(InInputs, InObjectIdx, Object, false); + } + } + ), + FSimpleDelegate::CreateLambda([]() {})); + })); + + // Add buttons + TSharedPtr ButtonHorizontalBox; + ComboAndButtonBox->AddSlot() + .FillHeight(1) + .Padding(0) + .VAlign(VAlign_Center) + [ + SAssignNew(ButtonHorizontalBox, SHorizontalBox) + ]; + + // Create tooltip. + FFormatNamedArguments Args; + Args.Add(TEXT("Asset"), MeshNameText); + FText StaticMeshTooltip = FText::Format( + LOCTEXT("BrowseToSpecificAssetInContentBrowser", + "Browse to '{Asset}' in the content browser."), Args); + + // Button : Use selected in content browser + ButtonHorizontalBox->AddSlot() + .AutoWidth() + .Padding(1, 0, 3, 0) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeUseSelectedButton( + FSimpleDelegate::CreateLambda([InInputs, InObjectIdx, UpdateGeometryObjectAt]() + { + if (GEditor) + { + TArray CBSelections; + GEditor->GetContentBrowserSelections(CBSelections); + + TArray AllowedClasses = UHoudiniInput::GetAllowedClasses(EHoudiniInputType::Geometry); + int32 CurrentObjectIdx = InObjectIdx; + for (auto& CurAssetData : CBSelections) + { + UObject* Object = CurAssetData.GetAsset(); + if (!IsValid(Object)) + continue; + + if (!UHoudiniInput::IsObjectAcceptable(EHoudiniInputType::Geometry, Object)) + continue; + + UpdateGeometryObjectAt(InInputs, CurrentObjectIdx++, Object, true); + } + } + }), + TAttribute(LOCTEXT("GeometryInputUseSelectedAssetFromCB", "Use the currently selected asset from the content browser."))) + ]; + + // Button : Browse Static Mesh + ButtonHorizontalBox->AddSlot() + .AutoWidth() + .Padding(1, 0, 3, 0) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateLambda([MainInput, InObjectIdx]() + { + UObject* InputObject = MainInput->GetInputObjectAt(InObjectIdx); + if (GEditor && InputObject) + { + TArray Objects; + Objects.Add(InputObject); + GEditor->SyncBrowserToObjects(Objects); + } + }), + TAttribute(StaticMeshTooltip)) + ]; + + // ButtonBox: Reset + ButtonHorizontalBox->AddSlot() + .AutoWidth() + .Padding(1, 0, 3, 0) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeResetButton( + FSimpleDelegate::CreateLambda([UpdateGeometryObjectAt, InInputs, InObjectIdx]() + { + UpdateGeometryObjectAt(InInputs, InObjectIdx, nullptr, false); + }), + TAttribute(LOCTEXT("GeometryInputReset", "Reset this geometry input object."))) + ]; + + ButtonHorizontalBox->AddSlot() + .AutoWidth() + .Padding(1, 0, 3, 0) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("TransformOffset", "Open transform offset menu below.")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([&CategoryBuilder, MainInput, InInputs, InObjectIdx]() + { + if (!IsValidWeakPointer(MainInput)) + return FReply::Handled(); + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Toggling Transform Offset Menu for Geometry Input Object"), + MainInput->GetOuter()); + + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + CurInput->OnTransformUIExpand(InObjectIdx); + CurInput->Modify(); + } + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + + return FReply::Handled(); + }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Icons.Transform")) + .ColorAndOpacity(FSlateColor(FColor(255, 255, 255, 168))) + ] + ]; + + ButtonHorizontalBox->AddSlot() + .AutoWidth() + .Padding(1, 0, 3, 0) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeAddButton( + FSimpleDelegate::CreateLambda([&CategoryBuilder, MainInput, InInputs, InObjectIdx]() + { + if (!IsValidWeakPointer(MainInput)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Insert a Geometry Input Object"), + MainInput->GetOuter()); + // Insert + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + CurInput->Modify(); + CurInput->InsertInputObjectAt(EHoudiniInputType::Geometry, InObjectIdx); + } + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + }), + TAttribute(LOCTEXT("GeometryInputAdd", "Add a new geometry input object above."))) + ]; + + ButtonHorizontalBox->AddSlot() + .AutoWidth() + .Padding(1, 0, 3, 0) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("DuplicateGeometryInputObject", "Duplicate this geometry input object.")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([&CategoryBuilder, MainInput, InInputs, InObjectIdx]() + { + if (!IsValidWeakPointer(MainInput)) + return FReply::Handled(); + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Duplicate a Geometry Input Object"), + MainInput->GetOuter()); + + // Duplicate + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + CurInput->Modify(); + CurInput->DuplicateInputObjectAt(EHoudiniInputType::Geometry, InObjectIdx); + } + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + + return FReply::Handled(); + }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Icons.Duplicate")) + .ColorAndOpacity(FSlateColor(FColor(255, 255, 255, 168))) + ] + ]; + + ButtonHorizontalBox->AddSlot() + .AutoWidth() + .Padding(1, 0, 3, 0) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeDeleteButton( + FSimpleDelegate::CreateLambda([&CategoryBuilder, MainInput, InInputs, InObjectIdx]() + { + if (!IsValidWeakPointer(MainInput)) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Delete a Geometry Input Object"), + MainInput->GetOuter()); + + // Delete + for (auto CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + CurInput->Modify(); + CurInput->DeleteInputObjectAt(EHoudiniInputType::Geometry, InObjectIdx); + + if (GEditor) + GEditor->RedrawAllViewports(); + } + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + }), + TAttribute(LOCTEXT("GeometryInputDelete", "Delete this geometry input object."))) + ]; + + + TSharedPtr TransformOffset_VerticalBox; + + InVerticalBox->AddSlot() + .Padding(5, 0, 0, 0) + .AutoHeight() + [ + SAssignNew(TransformOffset_VerticalBox, SVerticalBox) + .Visibility(MainInput->IsTransformUIExpanded(InObjectIdx) ? EVisibility::Visible : EVisibility::Collapsed) + ]; + + TransformOffset_VerticalBox->AddSlot() + .Padding(0, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("GeoInputTransform", "Transform Offset")) + .ToolTipText(LOCTEXT("GeoInputTransformTooltip", "Transform offset used for correction before sending the asset to Houdini.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ]; + + // Lambda for changing the transform values + auto ChangeTransformOffsetAt = [&](float Value, int32 AtIndex, int32 PosRotScaleIndex, int32 XYZIndex, bool DoChange, const TArray>& InInputs) + { + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniInputChange", "Houdini Input: Changed Transform Offset"), + InInputs[0]->GetOuter()); + + bool bChanged = true; + for (int Idx = 0; Idx < InInputs.Num(); Idx++) + { + if (!InInputs[Idx].IsValid()) + continue; + + UHoudiniInputObject* InputObject = InInputs[Idx]->GetHoudiniInputObjectAt(AtIndex); + if (InputObject) + InputObject->Modify(); + + bChanged &= InInputs[Idx]->SetTransformOffsetAt(Value, AtIndex, PosRotScaleIndex, XYZIndex); + } + + if (bChanged && DoChange) + { + // Mark the values as changed to trigger an update + for (int Idx = 0; Idx < InInputs.Num(); Idx++) + { + InInputs[Idx]->MarkChanged(true); + } + } + else + { + // Cancel the transaction + Transaction.Cancel(); + } + }; + + // Get Visibility of reset buttons + bool bResetButtonVisiblePosition = false; + bool bResetButtonVisibleRotation = false; + bool bResetButtonVisibleScale = false; + + for (auto& CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + FTransform* CurTransform = CurInput->GetTransformOffset(InObjectIdx); + if (!CurTransform) + continue; + + if (CurTransform->GetLocation() != FVector3d::ZeroVector) + bResetButtonVisiblePosition = true; + + FRotator Rotator = CurTransform->Rotator(); + if (Rotator.Roll != 0 || Rotator.Pitch != 0 || Rotator.Yaw != 0) + bResetButtonVisibleRotation = true; + + if (CurTransform->GetScale3D() != FVector3d::OneVector) + bResetButtonVisibleScale = true; + } + + auto ChangeTransformOffsetUniformlyAt = [InObjectIdx, InInputs, ChangeTransformOffsetAt](const float& Val, const int32& PosRotScaleIndex) + { + ChangeTransformOffsetAt(Val, InObjectIdx, PosRotScaleIndex, 0, true, InInputs); + ChangeTransformOffsetAt(Val, InObjectIdx, PosRotScaleIndex, 1, true, InInputs); + ChangeTransformOffsetAt(Val, InObjectIdx, PosRotScaleIndex, 2, true, InInputs); + }; + + TransformOffset_VerticalBox->AddSlot() + .Padding(0, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("GeoInputTranslate", "T")) + .ToolTipText(LOCTEXT("GeoInputTranslateTooltip", "Translate")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SVectorInputBox) + .bColorAxisLabels(true) + .AllowSpin(true) + .X(TAttribute>::Create( + TAttribute>::FGetter::CreateUObject( + MainInput.Get(), &UHoudiniInput::GetPositionOffsetX, InObjectIdx))) + .Y(TAttribute>::Create( + TAttribute>::FGetter::CreateUObject( + MainInput.Get(), &UHoudiniInput::GetPositionOffsetY, InObjectIdx))) + .Z(TAttribute>::Create( + TAttribute>::FGetter::CreateUObject( + MainInput.Get(), &UHoudiniInput::GetPositionOffsetZ, InObjectIdx))) + .OnXCommitted_Lambda([=](float Val, ETextCommit::Type TextCommitType) + { ChangeTransformOffsetAt(Val, InObjectIdx, 0, 0, true, InInputs); }) + .OnYCommitted_Lambda([=](float Val, ETextCommit::Type TextCommitType) + { ChangeTransformOffsetAt(Val, InObjectIdx, 0, 1, true, InInputs); }) + .OnZCommitted_Lambda([=](float Val, ETextCommit::Type TextCommitType) + { ChangeTransformOffsetAt(Val, InObjectIdx, 0, 2, true, InInputs); }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Right) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + .Padding(0) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ClickMethod(EButtonClickMethod::MouseDown) + .Visibility(EVisibility::Hidden) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Icons.Lock")) + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Left) + .VAlign(VAlign_Center) + .Padding(0) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ClickMethod(EButtonClickMethod::MouseDown) + .ToolTipText(LOCTEXT("GeoInputResetButtonToolTip", "Reset To Default")) + .Visibility(bResetButtonVisiblePosition ? EVisibility::Visible : EVisibility::Hidden) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + .OnClicked_Lambda([MainInput, ChangeTransformOffsetUniformlyAt, &CategoryBuilder]() + { + ChangeTransformOffsetUniformlyAt(0.0f, 0); + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + + return FReply::Handled(); + }) + ] + ] + ]; + + // Rotation + TransformOffset_VerticalBox->AddSlot() + .Padding(0, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("GeoInputRotate", "R")) + .ToolTipText(LOCTEXT("GeoInputRotateTooltip", "Rotate")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SRotatorInputBox) + .AllowSpin(true) + .bColorAxisLabels(true) + .Roll(TAttribute>::Create( + TAttribute>::FGetter::CreateUObject( + MainInput.Get(), &UHoudiniInput::GetUserInputRoll, InObjectIdx))) + .Pitch(TAttribute>::Create( + TAttribute>::FGetter::CreateUObject( + MainInput.Get(), &UHoudiniInput::GetUserInputPitch, InObjectIdx))) + .Yaw(TAttribute>::Create( + TAttribute>::FGetter::CreateUObject( + MainInput.Get(), &UHoudiniInput::GetUserInputYaw, InObjectIdx))) + .OnRollCommitted_Lambda([=](float Val, ETextCommit::Type TextCommitType) + { ChangeTransformOffsetAt(Val, InObjectIdx, 1, 0, true, InInputs); }) + .OnPitchCommitted_Lambda([=](float Val, ETextCommit::Type TextCommitType) + { ChangeTransformOffsetAt(Val, InObjectIdx, 1, 1, true, InInputs); }) + .OnYawCommitted_Lambda([=](float Val, ETextCommit::Type TextCommitType) + { ChangeTransformOffsetAt(Val, InObjectIdx, 1, 2, true, InInputs); }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Right) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().HAlign(HAlign_Right).VAlign(VAlign_Center).Padding(0.0f) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ClickMethod(EButtonClickMethod::MouseDown) + .Visibility(EVisibility::Hidden) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Icons.Lock")) + ] + ] + + SHorizontalBox::Slot().AutoWidth().HAlign(HAlign_Left).VAlign(VAlign_Center).Padding(0.0f) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ClickMethod(EButtonClickMethod::MouseDown) + .ToolTipText(LOCTEXT("GeoInputResetButtonToolTip", "Reset To Default")) + .Visibility(bResetButtonVisibleRotation ? EVisibility::Visible : EVisibility::Hidden) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + .OnClicked_Lambda([ChangeTransformOffsetUniformlyAt, MainInput, &CategoryBuilder]() + { + ChangeTransformOffsetUniformlyAt(0.0f, 1); + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + + return FReply::Handled(); + }) + ] + ] + ]; + + bool bLocked = false; + if (HoudiniInputObject) + bLocked = HoudiniInputObject->IsUniformScaleLocked(); + + TransformOffset_VerticalBox->AddSlot() + .Padding(0, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(1.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("GeoInputScale", "S")) + .ToolTipText(LOCTEXT("GeoInputScaleTooltip", "Scale")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SVectorInputBox) + .bColorAxisLabels(true) + .X(TAttribute>::Create( + TAttribute>::FGetter::CreateUObject( + MainInput.Get(), &UHoudiniInput::GetScaleOffsetX, InObjectIdx))) + .Y(TAttribute>::Create( + TAttribute>::FGetter::CreateUObject( + MainInput.Get(), &UHoudiniInput::GetScaleOffsetY, InObjectIdx))) + .Z(TAttribute>::Create( + TAttribute>::FGetter::CreateUObject( + MainInput.Get(), &UHoudiniInput::GetScaleOffsetZ, InObjectIdx))) + .OnXCommitted_Lambda([=](float Val, ETextCommit::Type TextCommitType) + { + if (bLocked) + ChangeTransformOffsetUniformlyAt(Val, 2); + else + ChangeTransformOffsetAt(Val, InObjectIdx, 2, 0, true, InInputs); + }) + .OnYCommitted_Lambda([=](float Val, ETextCommit::Type TextCommitType) + { + if (bLocked) + ChangeTransformOffsetUniformlyAt(Val, 2); + else + ChangeTransformOffsetAt(Val, InObjectIdx, 2, 1, true, InInputs); + }) + .OnZCommitted_Lambda([=](float Val, ETextCommit::Type TextCommitType) + { + if (bLocked) + ChangeTransformOffsetUniformlyAt(Val, 2); + else + ChangeTransformOffsetAt(Val, InObjectIdx, 2, 2, true, InInputs); + }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Right) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + .Padding(0) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ToolTipText(HoudiniInputObject ? + LOCTEXT("GeoInputLockButtonToolTip", "When locked, scales uniformly based on the current xyz scale values so the input object maintains its shape in each direction when scaled") : + LOCTEXT("GeoInputLockButtonToolTipNoObject", "No input object selected")) + .ClickMethod(EButtonClickMethod::MouseDown) + .Visibility(EVisibility::Visible) + [ + SNew(SImage) + .Image(bLocked ? _GetEditorStyle().GetBrush("Icons.Lock") : _GetEditorStyle().GetBrush("Icons.Unlock")) + ] + .OnClicked_Lambda([InInputs, MainInput, InObjectIdx, HoudiniInputObject, &CategoryBuilder]() + { + for (auto& CurInput : InInputs) + { + if (!IsValidWeakPointer(CurInput)) + continue; + + UHoudiniInputObject* CurInputObject = CurInput->GetHoudiniInputObjectAt(EHoudiniInputType::Geometry, InObjectIdx); + if (!IsValid(CurInputObject)) + continue; + + CurInputObject->SwitchUniformScaleLock(); + } + + if (HoudiniInputObject) + { + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + } + + return FReply::Handled(); + }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Left) + .VAlign(VAlign_Center) + .Padding(0) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ClickMethod(EButtonClickMethod::MouseDown) + .ToolTipText(LOCTEXT("GeoInputResetButtonToolTip", "Reset To Default")) + .Visibility(bResetButtonVisibleScale ? EVisibility::Visible : EVisibility::Hidden) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + .OnClicked_Lambda([ChangeTransformOffsetUniformlyAt, MainInput, &CategoryBuilder]() + { + ChangeTransformOffsetUniformlyAt(1.0f, 2); + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + + return FReply::Handled(); + }) + ] + ] + ]; +} + +FReply +FHoudiniInputDetails::Helper_OnButtonClickSelectActors( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const FName& DetailsPanelName) +{ + return Helper_OnButtonClickSelectActors(CategoryBuilder, InInputs, DetailsPanelName, false, false); +} + +FReply +FHoudiniInputDetails::Helper_OnButtonClickUseSelectionAsBoundSelector( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const FName& DetailsPanelName) +{ + return Helper_OnButtonClickSelectActors(CategoryBuilder, InInputs, DetailsPanelName, true, false); +} + +FReply +FHoudiniInputDetails::Helper_OnButtonClickSelectActors( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const FName& DetailsPanelName, + bool bUseWorldInAsWorldSelector, + bool bForceUpdate) +{ + if (InInputs.Num() <= 0) + return FReply::Handled(); + + const TWeakObjectPtr& MainInput = InInputs[0]; + + if (!IsValidWeakPointer(MainInput)) + return FReply::Handled(); + + // There's no undo operation for button. + FPropertyEditorModule& PropertyModule = + FModuleManager::Get().GetModuleChecked("PropertyEditor"); + + // Locate the details panel. + TSharedPtr DetailsView = PropertyModule.FindDetailView(DetailsPanelName); + + if (!DetailsView.IsValid()) + return FReply::Handled(); + + class SLocalDetailsView : public SDetailsViewBase + { + public: + void LockDetailsView() { SDetailsViewBase::bIsLocked = true; } + void UnlockDetailsView() { SDetailsViewBase::bIsLocked = false; } + }; + auto* LocalDetailsView = static_cast(DetailsView.Get()); + + // When forcing the update (refresh) + // Restart the selection AND update it immediately. + // This ensures that everything is properly taken care of + bool bDoStartSelection = !DetailsView->IsLocked(); + bool bDoUpdateSelection = DetailsView->IsLocked(); + if (bForceUpdate) + { + bDoStartSelection = true; + bDoUpdateSelection = true; + } + + if (bDoStartSelection) + { + // + // START SELECTION + // Locks the details view and select our currently selected actors + // + LocalDetailsView->LockDetailsView(); + check(DetailsView->IsLocked()); + + // Force refresh of details view. + TArray InputOuters; + + for (auto CurIn : InInputs) + InputOuters.Add(CurIn->GetOuter()); + + if (CategoryBuilder.IsParentLayoutValid()) + CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + //ReselectSelectedActors(); + + if (bUseWorldInAsWorldSelector) + { + // Bound Selection + // Select back the previously chosen bound selectors + GEditor->SelectNone(false, true); + int32 NumBoundSelectors = MainInput->GetNumberOfBoundSelectorObjects(); + for (int32 Idx = 0; Idx < NumBoundSelectors; Idx++) + { + AActor* Actor = MainInput->GetBoundSelectorObjectAt(Idx); + if (!IsValid(Actor)) + continue; + + GEditor->SelectActor(Actor, true, true); + } + } + else + { + // Regular selection + // Select the already chosen input Actors from the World Outliner. + GEditor->SelectNone(false, true); + int32 NumInputObjects = MainInput->GetNumberOfInputObjects(EHoudiniInputType::World); + for (int32 Idx = 0; Idx < NumInputObjects; Idx++) + { + UHoudiniInputObject* CurInputObject = MainInput->GetHoudiniInputObjectAt(Idx); + if (!CurInputObject) + continue; + + AActor* Actor = nullptr; + UHoudiniInputActor* InputActor = Cast(CurInputObject); + if (IsValid(InputActor)) + { + // Get the input actor + Actor = InputActor->GetActor(); + } + else + { + // See if the input object is a HAC + UHoudiniInputHoudiniAsset* InputHAC = Cast(CurInputObject); + if (IsValid(InputHAC)) + { + Actor = InputHAC->GetHoudiniCookable() ? InputHAC->GetHoudiniCookable()->GetOwner() : nullptr; + } + } + + if (!IsValid(Actor)) + continue; + + GEditor->SelectActor(Actor, true, true); + } + } + } + + if(bDoUpdateSelection) + { + // + // UPDATE SELECTION + // Unlocks the input's selection and select the HDA back. + // + + if (!GEditor || !GEditor->GetSelectedObjects()) + return FReply::Handled(); + + USelection* SelectedActors = GEditor->GetSelectedActors(); + USelection* SelectedComponents = GEditor->GetSelectedComponents(); + if (!SelectedActors && !SelectedComponents) + return FReply::Handled(); + + TArray AllSelectedActors; + + // Add all actors from the actor selection + for (FSelectionIterator It(*SelectedActors); It; ++It) + { + AActor* CurrentActor = Cast(*It); + if (!CurrentActor) + continue; + + AllSelectedActors.Add(CurrentActor); + } + + // Create a transaction + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniInputChange", "Changed Houdini World Outliner Input Objects"), + MainInput->GetOuter()); + + TArray AllActors; + for (auto CurrentInput : InInputs) + { + CurrentInput->Modify(); + + // Get our parent component/actor + UHoudiniCookable* OuterCookable = Cast(CurrentInput->GetOuter()); + USceneComponent* ParentComponent = OuterCookable ? OuterCookable->GetComponent() : Cast(CurrentInput->GetOuter()); + AActor* ParentActor = OuterCookable ? OuterCookable->GetOwner() : nullptr; + + AllActors.Add(ParentActor); + + bool bHasChanged = true; + if (bUseWorldInAsWorldSelector) + { + // + // Update bound selectors + + // Clean up the selected actors + TArray ValidBoundSelectedActors; + for (auto& CurrentBoundActor : AllSelectedActors) + { + if (!CurrentBoundActor) + continue; + + // Don't allow selection of ourselves. Bad things happen if we do. + if (ParentActor && (CurrentBoundActor == ParentActor)) + continue; + + ValidBoundSelectedActors.Add(CurrentBoundActor); + } + + // See if the bound selector have changed + int32 PreviousBoundSelectorCount = CurrentInput->GetNumberOfBoundSelectorObjects(); + if (PreviousBoundSelectorCount == ValidBoundSelectedActors.Num()) + { + // Same number of BoundSelectors, see if they have changed + bHasChanged = false; + for (int32 BoundIdx = 0; BoundIdx < PreviousBoundSelectorCount; BoundIdx++) + { + AActor* PreviousBound = CurrentInput->GetBoundSelectorObjectAt(BoundIdx); + if (!PreviousBound) + continue; + + if (!ValidBoundSelectedActors.Contains(PreviousBound)) + { + bHasChanged = true; + break; + } + } + } + + if (bHasChanged) + { + // Only update the bound selector objects on the input if they have changed + CurrentInput->SetBoundSelectorObjectsNumber(ValidBoundSelectedActors.Num()); + int32 InputObjectIdx = 0; + for (auto CurActor : ValidBoundSelectedActors) + { + CurrentInput->SetBoundSelectorObjectAt(InputObjectIdx++, CurActor); + } + + // Update the current selection from the BoundSelectors + CurrentInput->UpdateWorldSelectionFromBoundSelectors(); + } + } + else + { + // + // Update our selection directly with the currently selected actors + // + + TArray ValidSelectedActors; + for (auto& CurrentActor : AllSelectedActors) + { + if (!CurrentActor) + continue; + + // Don't allow selection of ourselves. Bad things happen if we do. + if (ParentActor && (CurrentActor == ParentActor)) + continue; + + ValidSelectedActors.Add(CurrentActor); + } + + // Update the input objects from the valid selected actors array + // Only new/remove input objects will be marked as changed + bHasChanged = CurrentInput->UpdateWorldSelection(ValidSelectedActors); + } + + // If we didnt change the selection, cancel the transaction + if (!bHasChanged) + Transaction.Cancel(); + + // This will allow a one time update of the inputs if auto-update is disabled + if (!CurrentInput->GetWorldInputAutoUpdates()) + bForceUpdate = true; + } + + // We can now unlock the details view... + LocalDetailsView->UnlockDetailsView(); + check(!DetailsView->IsLocked()); + + // .. reset the selected actors, force refresh and override the lock. + DetailsView->SetObjects(AllActors, true, true); + + // We now need to reselect all our Asset Actors. + // If we don't do this, our Asset parameters will stop refreshing and the user will be very confused. + // It is also resetting the state of the selection before the input actor selection process was started. + GEditor->SelectNone(false, true); + for (auto CurrentActor : AllActors) + { + AActor* ParentActor = Cast(CurrentActor); + if (!ParentActor) + continue; + + GEditor->SelectActor(ParentActor, true, true); + } + + // Update the input details layout. + // if (CategoryBuilder.IsParentLayoutValid()) + // CategoryBuilder.GetParentLayout().ForceRefreshDetails(); + } + + if (bForceUpdate) + { + for (auto CurrentInput : InInputs) + { + // Manually update the world input if auto update is disabled + bool bPrevious = CurrentInput->GetWorldInputAutoUpdates(); + CurrentInput->SetWorldInputAutoUpdates(true); + FHoudiniInputTranslator::UpdateWorldInput(CurrentInput.Get()); + CurrentInput->SetWorldInputAutoUpdates(bPrevious); + } + } + + return FReply::Handled(); +} + +bool +FHoudiniInputDetails::Helper_CancelWorldSelection(const TArray>& InInputs, const FName& DetailsPanelName) +{ + if (InInputs.Num() <= 0) + return false; + + // Get the property module to access the details view + FPropertyEditorModule & PropertyModule = + FModuleManager::Get().GetModuleChecked("PropertyEditor"); + + // Locate the details panel. + TSharedPtr DetailsView = PropertyModule.FindDetailView(DetailsPanelName); + + if (!DetailsView.IsValid()) + return false; + + if (!DetailsView->IsLocked()) + return false; + + class SLocalDetailsView : public SDetailsViewBase + { + public: + void LockDetailsView() { SDetailsViewBase::bIsLocked = true; } + void UnlockDetailsView() { SDetailsViewBase::bIsLocked = false; } + }; + auto * LocalDetailsView = static_cast(DetailsView.Get()); + + // Get all our parent components / actors + TArray AllComponents; + TArray AllActors; + for (auto CurrentInput : InInputs) + { + // Get our parent component/actor + UHoudiniCookable* OuterCookable = Cast(CurrentInput->GetOuter()); + USceneComponent* ParentComponent = OuterCookable ? OuterCookable->GetComponent() : Cast(CurrentInput->GetOuter()); + if (!ParentComponent) + continue; + + AllComponents.Add(ParentComponent); + + AActor* ParentActor = ParentComponent ? ParentComponent->GetOwner() : nullptr; + if (!ParentActor) + continue; + + AllActors.Add(ParentActor); + } + + // Unlock the detail view and re-select our parent actors + { + LocalDetailsView->UnlockDetailsView(); + check(!DetailsView->IsLocked()); + + // Reset selected actor to itself, force refresh and override the lock. + DetailsView->SetObjects(AllActors, true, true); + } + + // Reselect the Asset Actor. If we don't do this, our Asset parameters will stop + // refreshing and the user will be very confused. It is also resetting the state + // of the selection before the input actor selection process was started. + GEditor->SelectNone(false, true); + for (auto ParentActorObj : AllActors) + { + AActor* ParentActor = Cast(ParentActorObj); + if (!ParentActor) + continue; + + GEditor->SelectActor(ParentActor, true, true); + } + + return true; +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniInputDetails.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#include "Widgets/SBoxPanel.h" +#include "IDetailsView.h" + +class UHoudiniInput; +class UHoudiniSplineComponent; + +class IDetailCategoryBuilder; +class FDetailWidgetRow; +class FMenuBuilder; +class SVerticalBox; +class IDetailsView; +class FReply; +class FAssetThumbnailPool; + +class FHoudiniInputDetails : public TSharedFromThis +{ + public: + static void CreateWidget( + IDetailCategoryBuilder& HouInputCategoryBuilder, + const TArray>& InInputs, FDetailWidgetRow* InputRow = nullptr); + + static void CreateNameWidget( + const TWeakObjectPtr& InParam, + FDetailWidgetRow & Row, + bool bLabel, + int32 InInputCount); + + static FText GetInputTooltip( const TWeakObjectPtr& InInput ); + + // ComboBox : Input Type + static void AddInputTypeComboBox( + IDetailCategoryBuilder& CategoryBuilder, + TSharedRef InVerticalBox, + const TArray>& InInputs, + const IDetailsView* InDetailsView); + + // Checkbox : Keep World Transform + static void AddKeepWorldTransformCheckBox( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + // Checkbox : Pack before merging + static void AddPackBeforeMergeCheckbox( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + // Checkboxes : Export LODs / Sockets / Collisions + static void AddExportCheckboxes( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + // Level Instance Export Options + static void AddLevelInstanceExportCheckboxes( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + static void AddCurveRotScaleAttributesCheckBox( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + static void AddCurveAutoUpdateCheckBox( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + // Export options: common checkboxes for the new + // Geometry and new World inputs + static void AddExportAsReferenceCheckBoxes( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + // Landscape Options + static void AddExportSelectedLandscapesOnlyCheckBox( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + static void AddExportLandscapeAsOptions( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + static void AddLandscapeAutoSelectSplinesCheckBox( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + static void AddExportOptions( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + static void AddLandscapeOptions( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + static void AddLandscapeSplinesOptions( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + static void AddDirectlyConnectHdasCheckBox( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + static void AddUnrealSplineResolutionInput( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + static void AddUseLegacyInputCurvesCheckBox( + TSharedRef InVerticalBox, + const TArray>& InInputs); + + static void Helper_CreateCurveWidgetExpanded( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const int32& InCurveObjectIdx, + TSharedPtr AssetThumbnailPool, + TSharedRef InVerticalBox, + TSharedPtr HouSplineComponentVisualizer); + + static void Helper_CreateCurveWidgetCollapsed( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const int32& InCurveObjectIdx, + TSharedPtr AssetThumbnailPool, + TSharedRef InVerticalBox, + TSharedPtr HouSplineComponentVisualizer); + + static void Helper_AddCurvePointSelectionUI( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + TSharedPtr AssetThumbnailPool, + TSharedRef InVerticalBox, + TSharedPtr HouSplineComponentVisualizer); + + // Add Curve Inputs UI Widgets + static void AddCurveInputUI( + IDetailCategoryBuilder& CategoryBuilder, + TSharedRef InVerticalBox, + const TArray>& InInputs, + TSharedPtr AssetThumbnailPool); + + // Add new Geometry UI widget + static void AddGeometryInputUI( + IDetailCategoryBuilder& CategoryBuilder, + TSharedRef InVerticalBox, + const TArray>& InInputs, + TSharedPtr AssetThumbnailPool); + + // Add new World UI widget + static void AddWorldInputUI( + IDetailCategoryBuilder& CategoryBuilder, + TSharedRef InVerticalBox, + const TArray>& InInputs, + const IDetailsView* DetailsView); + + static void Helper_CreateGeometryInputObjectCollapsed( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const FPlatformTypes::int32& InObjectIdx, + TSharedRef InVerticalBox, + TSharedPtr AssetThumbnailPool); + + static void Helper_CreateGeometryInputObjectExpanded( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const FPlatformTypes::int32& InObjectIdx, + TSharedRef InVerticalBox, + TSharedPtr AssetThumbnailPool); + + static FMenuBuilder Helper_CreateWorldActorPickerWidget(const TArray>& InInputs); + + static FMenuBuilder Helper_CreateBoundSelectorPickerWidget(const TArray>& InInputs); + + static FReply Helper_OnButtonClickSelectActors( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const FName& DetailsPanelName); + + static FReply Helper_OnButtonClickUseSelectionAsBoundSelector( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const FName& DetailsPanelName); + + static FReply Helper_OnButtonClickSelectActors( + IDetailCategoryBuilder& CategoryBuilder, + const TArray>& InInputs, + const FName& InDetailsPanelName, + bool bUseWorldInAsWorldSelector, + bool bForceUpdate); + + static bool Helper_CancelWorldSelection( + const TArray>& InInputs, const FName& DetailsPanelName); +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniInputWidgets.h`: + +```h +#pragma once + +#include "CoreMinimal.h" + +#include "GameFramework/Actor.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Input/SComboBox.h" +#include "HoudiniEngineEditorPrivatePCH.h" + +DECLARE_DELEGATE_TwoParams( FOnLandscapeSelectionChanged, const FString&, AActor* ) + +class SLandscapeComboBox : public SComboBox> +{ + typedef TMap LandscapeOptionsType; + + SLATE_BEGIN_ARGS(SLandscapeComboBox) + : _Content() + {} + + /** Slot for this button's content (optional) */ + SLATE_DEFAULT_SLOT( FArguments, Content ) + + // SLATE_ARGUMENT( const TArray< TSharedPtr >&, LandscapeOptions ) + // Customized slate argument to deal with conversions of landscape options into combobox source options. + SLATE_PRIVATE_ARGUMENT_VARIABLE( LandscapeOptionsType, LandscapeOptions ); + + // SLATE_PRIVATE_ARGUMENT_FUNCTION ( const TArray< TSharedPtr >&, LandscapeOptions ) + WidgetArgsType& LandscapeOptions( const LandscapeOptionsType& LandscapeOptions) + { + // Cache the strings locally + _LandscapeOptions = LandscapeOptions; + return static_cast(this)->Me(); + } + + /** The option that should be selected when the combo box is first created */ + SLATE_ARGUMENT( FString, CurrentSelection ) + + SLATE_EVENT( FOnLandscapeSelectionChanged, OnLandscapeSelectionChanged ) + SLATE_EVENT( FOnGenerateWidget, OnGenerateWidget ) + + // TODO: implement any additional slate args here that needs to be forwarded to the + // underlying ComboBox slate widget. + + SLATE_END_ARGS() + + /** + * Construct the widget from a declaration + * + * @param InArgs Declaration from which to construct the combo box + */ + void Construct( const FArguments& InArgs ) + { + Options = InArgs._LandscapeOptions; + Labels.Empty(); + + // Create labels from the landscape names + TArray Keys; + TSharedPtr CurrentSelection; + + Options.GetKeys(Keys); + for( const FString& Name : Keys) + { + Labels.Add(MakeShareable(new FString(Name))); + if (InArgs._CurrentSelection == Name) + { + CurrentSelection = Labels.Last(); + } + } + + // If we don't have labels or we don't have a current selection, initialize the string + // with a custom message. + if (Labels.Num() == 0 ) + { + CurrentSelection = MakeShareable(new FString("No landscapes found")); + } + else if (!CurrentSelection.IsValid()) + { + CurrentSelection = MakeShareable(new FString("No landscape selected")); + } + + SelectedItemText = SNew( STextBlock ) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text_Lambda([=]() -> FText + { + return FText::FromString(*CurrentSelection.Get()); + }) + ; + + this->OnGenerateWidget = InArgs._OnGenerateWidget; + this->OnLandscapeSelectionChanged = InArgs._OnLandscapeSelectionChanged; + + SComboBox>::Construct( + SComboBox>::FArguments() + .OptionsSource(&Labels) + .InitiallySelectedItem(CurrentSelection) + .OnGenerateWidget(OnGenerateWidget) + .Content() + [ + SelectedItemText.ToSharedRef() + ] + .OnSelectionChanged_Lambda([this](TSharedPtr NewChoice, ESelectInfo::Type SelectType) + { + if (NewChoice.IsValid()) + { + SelectedItemText->SetText( FText::FromString(*NewChoice) ); + // Find the actor that corresponds to the choice + AActor* NewProxy = nullptr; + if (Options.Contains(*NewChoice)) + { + NewProxy = Options.FindChecked(*NewChoice); + } + OnLandscapeSelectionChanged.ExecuteIfBound(*NewChoice, NewProxy); + } + }) + ); + } + +protected: + LandscapeOptionsType Options; + TArray> Labels; + + // Delegate that is invoked when the selected (landscape) item in the combo box changes + FOnLandscapeSelectionChanged OnLandscapeSelectionChanged; + + // Delegate to invoke when we need to visualize an option as a widget. + FOnGenerateWidget OnGenerateWidget; + + // Widget for the currently selected item + TSharedPtr SelectedItemText; +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniOutputDetails.cpp`: + +```cpp + +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniOutputDetails.h" + +#include "HoudiniEngineEditorPrivatePCH.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniAssetComponentDetails.h" +#include "HoudiniCookable.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniEngineCommands.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEngineOutputStats.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniInstanceTranslator.h" +#include "HoudiniMeshTranslator.h" +#include "HoudiniSplineComponent.h" +#include "HoudiniStaticMesh.h" + +#include "Animation/AnimSequence.h" +#include "Animation/Skeleton.h" +#include "Components/SplineComponent.h" +#include "DetailCategoryBuilder.h" +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "Editor/PropertyEditor/Public/PropertyCustomizationHelpers.h" +#include "Editor/UnrealEd/Public/AssetThumbnail.h" +#include "Editor/UnrealEdEngine.h" +#include "Engine/DataTable.h" +#include "Engine/StaticMesh.h" +#include "Engine/SkeletalMesh.h" +#include "IDetailGroup.h" +#include "Landscape.h" +#include "LandscapeProxy.h" +#include "LandscapeSplineActor.h" +#include "LandscapeSplinesComponent.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstance.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Particles/ParticleSystem.h" +#include "PhysicsEngine/BodySetup.h" +#include "SAssetDropTarget.h" +#include "ScopedTransaction.h" +#include "Sound/SoundBase.h" +#include "UnrealEdGlobals.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SVectorInputBox.h" +#include "Widgets/Input/SRotatorInputBox.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/Text/STextBlock.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "Engine/SkinnedAssetCommon.h" +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollection/GeometryCollectionComponent.h" + #include "GeometryCollection/GeometryCollectionObject.h" +#else + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionComponent.h" + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionObject.h" +#endif + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +void +FHoudiniOutputDetails::CreateWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TArray>& InOutputs) +{ + if (InOutputs.Num() <= 0) + return; + + const TWeakObjectPtr& MainOutput = InOutputs[0]; + if (!IsValidWeakPointer(MainOutput)) + return; + + // Don't create UI for editable curve. + if (MainOutput->IsEditableNode() && MainOutput->GetType() == EHoudiniOutputType::Curve) + return; + + // Get thumbnail pool for this builder. + TSharedPtr AssetThumbnailPool = HouOutputCategory.GetParentLayout().GetThumbnailPool(); + + switch (MainOutput->GetType()) + { + case EHoudiniOutputType::Mesh: + { + FHoudiniOutputDetails::CreateMeshOutputWidget(HouOutputCategory, MainOutput); + break; + } + + case EHoudiniOutputType::Landscape: + { + FHoudiniOutputDetails::CreateLandscapeOutputWidget(HouOutputCategory, MainOutput); + break; + } + + case EHoudiniOutputType::Instancer: + { + FHoudiniOutputDetails::CreateInstancerOutputWidget(HouOutputCategory, MainOutput); + break; + } + + case EHoudiniOutputType::Curve: + { + FHoudiniOutputDetails::CreateCurveOutputWidget(HouOutputCategory, MainOutput); + break; + } + + case EHoudiniOutputType::GeometryCollection: + { + FHoudiniOutputDetails::CreateGeometryCollectionOutputWidget(HouOutputCategory, MainOutput); + break; + } + case EHoudiniOutputType::DataTable: + { + FHoudiniOutputDetails::CreateDataTableOutputWidget(HouOutputCategory, MainOutput); + break; + } + case EHoudiniOutputType::LandscapeSpline: + { + FHoudiniOutputDetails::CreateLandscapeSplineOutputWidget(HouOutputCategory, MainOutput); + break; + } + case EHoudiniOutputType::AnimSequence: + { + FHoudiniOutputDetails::CreateAnimSequenceOutputWidget(HouOutputCategory, MainOutput); + break; + } + + case EHoudiniOutputType::Skeletal: + { + FHoudiniOutputDetails::CreateSkeletalOutputWidget(HouOutputCategory, MainOutput); + break; + } + + case EHoudiniOutputType::Cop: + { + FHoudiniOutputDetails::CreateTextureOutputWidget(HouOutputCategory, MainOutput); + break; + } + + default: + { + FHoudiniOutputDetails::CreateDefaultOutputWidget(HouOutputCategory, MainOutput); + break; + } + } +} + + +void +FHoudiniOutputDetails::CreateLandscapeOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + // Go through this output's objects + TMap& OutputObjects = InOutput->GetOutputObjects(); + for (auto& CurrentOutputObj : OutputObjects) + { + FHoudiniOutputObjectIdentifier& Identifier = CurrentOutputObj.Key; + const FHoudiniGeoPartObject *HGPO = nullptr; + for (const auto& CurHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + if (!Identifier.Matches(CurHGPO)) + continue; + + HGPO = &CurHGPO; + break; + } + + if (!HGPO) + continue; + + + if (UHoudiniLandscapePtr* LandscapePointer = Cast(CurrentOutputObj.Value.OutputObject)) + { + CreateLandscapeOutputWidget_Helper(HouOutputCategory, InOutput, *HGPO, LandscapePointer, Identifier); + } + else if (UHoudiniLandscapeTargetLayerOutput* LayerOutput = Cast(CurrentOutputObj.Value.OutputObject)) + { + CreateLandscapeEditLayerOutputWidget_Helper(HouOutputCategory, InOutput, *HGPO, LayerOutput, Identifier); + } + } +} + +void +FHoudiniOutputDetails::CreateLandscapeOutputWidget_Helper( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const FHoudiniGeoPartObject& HGPO, + const TWeakObjectPtr& LandscapePointer, + const FHoudiniOutputObjectIdentifier& OutputIdentifier) +{ + if (!LandscapePointer.IsValid() || !LandscapePointer->LandscapeSoftPtr.IsValid()) + return; + + if (!IsValidWeakPointer(InOutput)) + return; + + const TWeakObjectPtr& HAC = Cast(InOutput->GetOuter()); + const TWeakObjectPtr& HC = Cast(InOutput->GetOuter()); + if (!IsValidWeakPointer(HAC) && !IsValidWeakPointer(HC)) + return; + + const TWeakObjectPtr& OwnerActor = HC.IsValid() ? HC->GetOwner() : HAC->GetOwner(); + if (!IsValidWeakPointer(OwnerActor)) + return; + + const TWeakObjectPtr& Landscape = LandscapePointer->LandscapeSoftPtr.Get(); + if (!IsValidWeakPointer(Landscape)) + return; + + // TODO: Get bake base name + FString Label = Landscape->GetName(); + if (!LandscapePointer->EditLayerName.IsNone()) + { + Label = FString::Format(TEXT("{0} ({1})"), {Label, (LandscapePointer->EditLayerName.ToString())}); + } + + EHoudiniLandscapeOutputBakeType & LandscapeOutputBakeType = LandscapePointer->BakeType; + + // Create a group for this output object + IDetailGroup& LandscapeGrp = HouOutputCategory.AddGroup(FName(*Label), FText::FromString(Label)); + + // -------------------------------- + // Modified edit layers, if there are any. + // -------------------------------- + + if (!LandscapePointer->EditLayerName.IsNone()) + { + // Create label to display the edit layer name. + + LandscapeGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeOutputEditLayers", "Edit Layer")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(STextBlock) + .Text(FText::AsCultureInvariant(FText::AsCultureInvariant(LandscapePointer->EditLayerName.ToString())) ) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ]; + } + + + + // -------------------------------- + // Bake options for landscape tile + // -------------------------------- + + // Get thumbnail pool for this builder + IDetailLayoutBuilder & DetailLayoutBuilder = HouOutputCategory.GetParentLayout(); + TSharedPtr AssetThumbnailPool = DetailLayoutBuilder.GetThumbnailPool(); + TArray>* BakeOptionString = FHoudiniEngineEditor::Get().GetHoudiniLandscapeOutputBakeOptionsLabels(); + + // Create bake mesh name textfield. + LandscapeGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("BakeBaseName", "Bake Name")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .FillWidth(1) + [ + SNew(SEditableTextBox) + .Text(FText::FromString(Label)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .ToolTipText(LOCTEXT("BakeNameTip", "The base name of the baked asset")) + .HintText(LOCTEXT("BakeNameHintText", "Input bake name to override default")) + .OnTextCommitted_Lambda([InOutput, OutputIdentifier](const FText& Val, ETextCommit::Type TextCommitType) + { + if (InOutput.IsValid()) + { + FHoudiniOutputDetails::OnBakeNameCommitted(Val, TextCommitType, InOutput, OutputIdentifier); + FHoudiniEngineUtils::UpdateEditorProperties(true); + } + }) + ] + + SHorizontalBox::Slot() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertNameOverride", "Revert bake name override")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([InOutput, OutputIdentifier]() + { + FHoudiniOutputDetails::OnRevertBakeNameToDefault(InOutput, OutputIdentifier); + return FReply::Handled(); + }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ]; + + // Create the thumbnail for the landscape output object. + TSharedPtr LandscapeThumbnail = + MakeShareable(new FAssetThumbnail(Landscape.Get(), 64, 64, AssetThumbnailPool)); + + TSharedPtr LandscapeThumbnailBorder; + TSharedRef VerticalBox = SNew(SVerticalBox); + LandscapeGrp.AddWidgetRow() + .NameContent() + [ + SNew(SSpacer) + .Size(FVector2D(250, 64)) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + VerticalBox + ]; + + VerticalBox->AddSlot() + .Padding(0, 2) + .AutoHeight() + [ + SNew(SBox).WidthOverride(175) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0.0f, 0.0f, 2.0f, 0.0f) + .AutoWidth() + [ + SAssignNew(LandscapeThumbnailBorder, SBorder) + .Padding(5.0f) + .BorderImage(this, &FHoudiniOutputDetails::GetThumbnailBorder, (const TWeakObjectPtr&) Landscape) + .OnMouseDoubleClick(this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) Landscape) + [ + SNew(SBox) + .WidthOverride(64) + .HeightOverride(64) + .ToolTipText(FText::FromString(Landscape->GetPathName())) + [ + LandscapeThumbnail->MakeThumbnailWidget() + ] + ] + ] + + + SHorizontalBox::Slot() + .Padding(0.0f, 4.0f, 4.0f, 4.0f) + .VAlign(VAlign_Center) + [ + SNew(SBox) + //.WidthOverride(40.0f) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Text(LOCTEXT("BakeOutputLandscape", "Bake Output")) + .IsEnabled(true) + .OnClicked_Lambda([InOutput, OutputIdentifier, HC, HGPO, Landscape, LandscapeOutputBakeType]() + { + if (!InOutput.IsValid() || !Landscape.IsValid()) + return FReply::Handled(); + + if (!HC.IsValid()) + return FReply::Handled(); + + FHoudiniBakeSettings BakeSettings; + BakeSettings.SetFromCookable(HC.Get()); + + FHoudiniOutputObject const* const FoundOutputObject = InOutput->GetOutputObjects().Find(OutputIdentifier); + if (FoundOutputObject) + { + TArray AllOutputs; + AllOutputs.Reserve(HC->GetNumOutputs()); + HC->GetOutputs(AllOutputs); + FHoudiniOutputDetails::OnBakeOutputObject( + FoundOutputObject->BakeName, + Landscape.Get(), + OutputIdentifier, + *FoundOutputObject, + HGPO, + (UObject*)HC.Get(), + InOutput.Get(), + HC->GetBakeFolderOrDefault(), + BakeSettings, + HC->GetTemporaryCookFolderOrDefault(), + LandscapeOutputBakeType, + AllOutputs); + } + + // TODO: Remove the output landscape if the landscape bake type is Detachment? + return FReply::Handled(); + }) + .ToolTipText(LOCTEXT("HoudiniLandscapeBakeButton", "Bake this output Landscape to a new Landscape Actor.")) + ] + ] + + SHorizontalBox::Slot() + .Padding(0.0f, 4.0f, 4.0f, 4.0f) + .VAlign(VAlign_Center) + [ + SNew(SBox).WidthOverride(120.f) + [ + SNew(SComboBox>) + .OptionsSource(FHoudiniEngineEditor::Get().GetHoudiniLandscapeOutputBakeOptionsLabels()) + .InitiallySelectedItem((*FHoudiniEngineEditor::Get().GetHoudiniLandscapeOutputBakeOptionsLabels())[(uint8)LandscapeOutputBakeType]) + .OnGenerateWidget_Lambda( + [](TSharedPtr< FString > InItem) + { + return SNew(STextBlock).Text(FText::FromString(*InItem)); + }) + .OnSelectionChanged_Lambda( + [LandscapePointer, InOutput](TSharedPtr NewChoice, ESelectInfo::Type SelectType) + { + if (SelectType != ESelectInfo::Type::OnMouseClick) + return; + + FString *NewChoiceStr = NewChoice.Get(); + if (!NewChoiceStr) + return; + + if (LandscapePointer.IsValid()) + { + if (*NewChoiceStr == FHoudiniEngineEditorUtils::HoudiniLandscapeOutputBakeTypeToString(EHoudiniLandscapeOutputBakeType::Detachment)) + { + LandscapePointer->SetLandscapeOutputBakeType(EHoudiniLandscapeOutputBakeType::Detachment); + } + else if (*NewChoiceStr == FHoudiniEngineEditorUtils::HoudiniLandscapeOutputBakeTypeToString(EHoudiniLandscapeOutputBakeType::BakeToImage)) + { + LandscapePointer->SetLandscapeOutputBakeType(EHoudiniLandscapeOutputBakeType::BakeToImage); + } + else + { + LandscapePointer->SetLandscapeOutputBakeType(EHoudiniLandscapeOutputBakeType::BakeToWorld); + } + } + + if (InOutput.IsValid()) + FHoudiniEngineUtils::UpdateEditorProperties(true); + }) + [ + SNew(STextBlock) + .Text_Lambda([LandscapePointer]() + { + if (!IsValidWeakPointer(LandscapePointer)) + return FText(); + + const FString BakeTypeString = FHoudiniEngineEditorUtils::HoudiniLandscapeOutputBakeTypeToString(LandscapePointer->GetLandscapeOutputBakeType()); + return FText::FromString(BakeTypeString); + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + ] + ] + ]; + + // Store thumbnail for this landscape. + OutputObjectThumbnailBorders.Add(Landscape, LandscapeThumbnailBorder); + + // We need to add material box for each the landscape and landscape hole materials + for (int32 MaterialIdx = 0; MaterialIdx < 2; ++MaterialIdx) + { + UMaterialInterface * MaterialInterface = MaterialIdx == 0 ? Landscape->GetLandscapeMaterial() : Landscape->GetLandscapeHoleMaterial(); + TSharedPtr MaterialThumbnailBorder; + TSharedPtr HorizontalBox = NULL; + + FString MaterialName, MaterialPathName; + if (MaterialInterface) + { + MaterialName = MaterialInterface->GetName(); + MaterialPathName = MaterialInterface->GetPathName(); + } + + // Create thumbnail for this material. + TSharedPtr< FAssetThumbnail > MaterialInterfaceThumbnail = + MakeShareable(new FAssetThumbnail(MaterialInterface, 64, 64, AssetThumbnailPool)); + + VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(STextBlock) + .Text(MaterialIdx == 0 ? LOCTEXT("LandscapeMaterial", "Landscape Material") : LOCTEXT("LandscapeHoleMaterial", "Landscape Hole Material")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ]; + + VerticalBox->AddSlot() + .Padding(0, 2) + [ + SNew(SAssetDropTarget) + .OnAreAssetsAcceptableForDrop(this, &FHoudiniOutputDetails::OnMaterialInterfaceDraggedOver) + .OnAssetsDropped(this, &FHoudiniOutputDetails::OnMaterialInterfaceDropped, Landscape, InOutput, MaterialIdx) + [ + SAssignNew(HorizontalBox, SHorizontalBox) + ] + ]; + + HorizontalBox->AddSlot() + .Padding(0.0f, 0.0f, 2.0f, 0.0f) + .AutoWidth() + [ + SAssignNew(MaterialThumbnailBorder, SBorder) + .Padding(5.0f) + .BorderImage(this, &FHoudiniOutputDetails::GetMaterialInterfaceThumbnailBorder, (const TWeakObjectPtr&) Landscape, MaterialIdx) + .OnMouseDoubleClick(this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) MaterialInterface) + [ + SNew(SBox) + .WidthOverride(64) + .HeightOverride(64) + .ToolTipText(FText::FromString(MaterialPathName)) + [ + MaterialInterfaceThumbnail->MakeThumbnailWidget() + ] + ] + ]; + + // Store thumbnail for this landscape and material index. + { + TPairInitializer, int32> Pair(Landscape, MaterialIdx); + MaterialInterfaceThumbnailBorders.Add(Pair, MaterialThumbnailBorder); + } + + // Combox Box and Button Box + TSharedPtr ComboAndButtonBox; + HorizontalBox->AddSlot() + .FillWidth(1.0f) + .Padding(0.0f, 4.0f, 4.0f, 4.0f) + .VAlign(VAlign_Center) + [ + SAssignNew(ComboAndButtonBox, SVerticalBox) + ]; + + // Combo row + TSharedPtr AssetComboButton; + ComboAndButtonBox->AddSlot() + .FillHeight(1.0f) + [ + SNew(SVerticalBox) + SVerticalBox::Slot().FillHeight(1.0f) + [ + SAssignNew(AssetComboButton, SComboButton) + //.ToolTipText( this, &FHoudiniAssetComponentDetails::OnGetToolTip ) + .ButtonStyle(_GetEditorStyle(), "PropertyEditor.AssetComboStyle") + .ForegroundColor(_GetEditorStyle().GetColor("PropertyEditor.AssetName.ColorAndOpacity")) + .OnGetMenuContent(this, &FHoudiniOutputDetails::OnGetMaterialInterfaceMenuContent, TWeakObjectPtr(MaterialInterface), (TWeakObjectPtr)Landscape, InOutput, MaterialIdx) + .ContentPadding(2.0f) + .ButtonContent() + [ + SNew(STextBlock) + .TextStyle(_GetEditorStyle(), "PropertyEditor.AssetClass") + .Font(_GetEditorStyle().GetFontStyle(FName(TEXT("PropertyWindow.NormalFont")))) + .Text(FText::FromString(MaterialName)) + ] + ] + ]; + + // Buttons row + TSharedPtr ButtonBox; + ComboAndButtonBox->AddSlot() + .FillHeight(1.0f) + [ + SAssignNew(ButtonBox, SHorizontalBox) + ]; + + // Add use Content Browser selection arrow + ButtonBox->AddSlot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeUseSelectedButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnUseContentBrowserSelectedMaterialInterface, + (const TWeakObjectPtr&)Landscape, InOutput, MaterialIdx), + TAttribute< FText >(LOCTEXT("UseSelectedAssetFromContentBrowser", "Use Selected Asset from Content Browser"))) + ]; + + // Create tooltip. + FFormatNamedArguments Args; + Args.Add(TEXT("Asset"), FText::FromString(MaterialName)); + FText MaterialTooltip = FText::Format( + LOCTEXT("BrowseToSpecificAssetInContentBrowser", "Browse to '{Asset}' in Content Browser"), Args); + + ButtonBox->AddSlot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&)MaterialInterface), + TAttribute< FText >(MaterialTooltip)) + ]; + + ButtonBox->AddSlot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("ResetToBaseMaterial", "Reset to base material")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility(EVisibility::Visible) + .OnClicked( this, &FHoudiniOutputDetails::OnResetMaterialInterfaceClicked, Landscape, InOutput, MaterialIdx) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ]; + + // Store combo button for this mesh and index. + { + TPairInitializer, int32> Pair(Landscape, MaterialIdx); + MaterialInterfaceComboButtons.Add(Pair, AssetComboButton); + } + } +} + +void FHoudiniOutputDetails::CreateLandscapeEditLayerOutputWidget_Helper( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const FHoudiniGeoPartObject& HGPO, + const TWeakObjectPtr& LandscapeEditLayer, + const FHoudiniOutputObjectIdentifier& OutputIdentifier) +{ + const UHoudiniAssetComponent* HAC = Cast(InOutput->GetOuter()); + const UHoudiniCookable* HC = Cast(InOutput->GetOuter()); + if (!IsValid(HAC) && !IsValid(HC)) + return; + + // Check everything is valid first. + if (!LandscapeEditLayer.IsValid() + || !IsValid(LandscapeEditLayer->Landscape) + || !IsValidWeakPointer(InOutput)) + return; + + ALandscape* Landscape = LandscapeEditLayer->Landscape; + + const FString Label = Landscape->GetName(); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + const FString EditLayerName = LandscapeEditLayer->BakedEditLayer; +#else + const FString EditLayerName = Landscape->bHasLayersContent ? LandscapeEditLayer->BakedEditLayer : + "Target landscape does not support edit layers."; +#endif + + const FString TargetLayerName = LandscapeEditLayer->TargetLayer; + + // Get thumbnail pool for this builder + IDetailLayoutBuilder & DetailLayoutBuilder = HouOutputCategory.GetParentLayout(); + TSharedPtr< FAssetThumbnailPool > AssetThumbnailPool = DetailLayoutBuilder.GetThumbnailPool(); + IDetailGroup& LandscapeGrp = HouOutputCategory.AddGroup(FName(*Label), FText::FromString(Label)); + + //------------------------------------------------------------------------------------------------------------------------------ + // Landscape information + //------------------------------------------------------------------------------------------------------------------------------ + + FString TargetLandscape = Landscape->GetActorLabel(); + FString OutputMode = LandscapeEditLayer->bCreatedLandscape ? "Create Landscape" : "Modify Landscape"; + + LandscapeGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeEditLayerName", "Output Mode")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(STextBlock) + .Text(FText::AsCultureInvariant(OutputMode)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ]; + + LandscapeGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeEditLayerName", "Target Landscape")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(STextBlock) + .Text(FText::AsCultureInvariant(TargetLandscape)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ]; + + //------------------------------------------------------------------------------------------------------------------------------ + // Create labels to display the edit layer name. + //------------------------------------------------------------------------------------------------------------------------------ + + LandscapeGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeEditLayerName", "Edit Layer Name")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(STextBlock) + .Text(FText::AsCultureInvariant(EditLayerName)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ]; + + LandscapeGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeEditLayerName", "Target Layer Name")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(STextBlock) + .Text(FText::AsCultureInvariant(TargetLayerName)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ]; + + //------------------------------------------------------------------------------------------------------------------------------ + // Size Information + //------------------------------------------------------------------------------------------------------------------------------ + + const FHoudiniExtents & Extents = LandscapeEditLayer->Extents; + + FString SizeInfo = FString::Printf(TEXT("(%d,%d) -> (%d,%d)"), Extents.Min.X, Extents.Min.Y, Extents.Max.X, Extents.Max.Y); + + LandscapeGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("LandscapeEditLayerName", "Modified Extents")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(STextBlock) + .Text(FText::AsCultureInvariant(SizeInfo)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ]; +} + +void +FHoudiniOutputDetails::CreateMeshOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + // Go through this output's object + int32 OutputObjIdx = 0; + TMap& OutputObjects = InOutput->GetOutputObjects(); + for (auto& IterObject : OutputObjects) + { + UStaticMesh* StaticMesh = Cast(IterObject.Value.OutputObject); + UHoudiniStaticMesh* ProxyMesh = Cast(IterObject.Value.ProxyObject); + + if ((!IsValid(StaticMesh)) + && (!IsValid(ProxyMesh))) + continue; + + // Do not display geometry collection static meshes. + if (IterObject.Value.bIsGeometryCollectionPiece) + continue; + + FHoudiniOutputObjectIdentifier & OutputIdentifier = IterObject.Key; + + // Find the corresponding HGPO in the output + FHoudiniGeoPartObject HoudiniGeoPartObject; + for (const auto& curHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + if (!OutputIdentifier.Matches(curHGPO)) + continue; + + HoudiniGeoPartObject = curHGPO; + break; + } + + bool bIsProxyMeshCurrent = IterObject.Value.bProxyIsCurrent; + if (IsValid(StaticMesh) && !bIsProxyMeshCurrent) + { + // If we have a static mesh, display its widget unless the proxy is more recent + CreateStaticMeshAndMaterialWidgets( + HouOutputCategory, InOutput, StaticMesh, OutputIdentifier, HoudiniGeoPartObject, bIsProxyMeshCurrent); + } + else + { + // If we only have a proxy mesh, then show the proxy widget + CreateProxyMeshAndMaterialWidgets( + HouOutputCategory, InOutput, ProxyMesh, OutputIdentifier, HoudiniGeoPartObject); + } + } +} + + +void +FHoudiniOutputDetails::CreateSkeletalOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + const TWeakObjectPtr& HAC = Cast(InOutput->GetOuter()); + const TWeakObjectPtr& HC = Cast(InOutput->GetOuter()); + if (!IsValidWeakPointer(HAC) && !IsValidWeakPointer(HC)) + return; + + // See if we have a Skeletal Mesh and/or skeleton. + USkeletalMesh* SkelMesh = nullptr; + USkeleton* Skeleton = nullptr; + UPhysicsAsset * PhysicsAsset = nullptr; + FHoudiniOutputObjectIdentifier OutputIdentifier; + + TMap& OutputObjects = InOutput->GetOutputObjects(); + for (auto& IterObject : OutputObjects) + { + if (!SkelMesh) + { + SkelMesh = Cast(IterObject.Value.OutputObject); + OutputIdentifier = IterObject.Key; + } + if (!Skeleton) + { + Skeleton = Cast(IterObject.Value.OutputObject); + + if (SkelMesh == nullptr) + OutputIdentifier = IterObject.Key; + } + if (!PhysicsAsset) + { + PhysicsAsset = Cast(IterObject.Value.OutputObject); + + if (SkelMesh == nullptr) + OutputIdentifier = IterObject.Key; + } + } + + if ((!IsValid(SkelMesh)) && (!IsValid(Skeleton))) + return; + + // Find the corresponding HGPO in the output + FHoudiniGeoPartObject HoudiniGeoPartObject; + for (const auto& curHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + if (OutputIdentifier.Matches(curHGPO)) + { + HoudiniGeoPartObject = curHGPO; + break; + } + } + + // If we have a Skeletal mesh, display its widget unless the proxy is more recent + CreateSkeletalMeshAndMaterialWidgets(HouOutputCategory, InOutput, SkelMesh, Skeleton, PhysicsAsset, OutputIdentifier, HoudiniGeoPartObject); +} + +void FHoudiniOutputDetails::CreateAnimSequenceOutputWidget(IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + if (!IsValidWeakPointer(InOutput)) + return; + + TMap& OutputObjects = InOutput->GetOutputObjects(); + int32 Idx = 1; + for (auto& IterObject : OutputObjects) + { + FHoudiniOutputObject& CurrentOutputObject = IterObject.Value; + + if (UAnimSequence* AnimSequence = Cast(CurrentOutputObject.OutputObject)) + { + if (!IsValid(AnimSequence)) + { + continue; + } + FString Label = AnimSequence->GetName(); + IDetailGroup& OutputGrp = HouOutputCategory.AddGroup(FName(Label), FText::FromString(Label), false, false); + TSharedRef VerticalBox = SNew(SVerticalBox); + TSharedPtr ThumbnailBorder; + IDetailLayoutBuilder& DetailLayoutBuilder = HouOutputCategory.GetParentLayout(); + TSharedPtr AssetThumbnailPool = DetailLayoutBuilder.GetThumbnailPool(); + TSharedPtr< FAssetThumbnail > AnimThumbnail = + MakeShareable(new FAssetThumbnail(AnimSequence, 64, 64, AssetThumbnailPool)); + + OutputGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Anim Sequence")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + VerticalBox + ]; + + VerticalBox->AddSlot() + .Padding(0, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0.0f, 0.0f, 2.0f, 0.0f) + .AutoWidth() + [ + SAssignNew(ThumbnailBorder, SBorder) + .Padding(5.0f) + .BorderImage(this, &FHoudiniOutputDetails::GetThumbnailBorder, (const TWeakObjectPtr&) AnimSequence) + .OnMouseDoubleClick(this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) AnimSequence) + [ + SNew(SBox) + .WidthOverride(64) + .HeightOverride(64) + .ToolTipText(FText::FromString(AnimSequence->GetPathName())) + [ + AnimThumbnail->MakeThumbnailWidget() + ] + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromName(AnimSequence->GetFName())) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2, 0) + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&) AnimSequence), + TAttribute(LOCTEXT("HoudiniAnimSequenceBrowseButton", "Browse to this asset in the content browser"))) + ] + ] + ]; + + // OutputGrp.AddWidgetRow() + // .NameContent() + // [ + // SNew(STextBlock) + // .Text(FText::FromString("Row Struct")) + // .Font(IDetailLayoutBuilder::GetDetailFont()) + // ] + // .ValueContent() + // .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + // [ + // RSVerticalBox + // ]; + // + // RSVerticalBox->AddSlot() + // .Padding(0, 2) + // .AutoHeight() + // [ + // SNew(SHorizontalBox) + // + SHorizontalBox::Slot() + // .Padding(0.0f, 0.0f, 2.0f, 0.0f) + // .AutoWidth() + // [ + // SAssignNew(RSThumbnailBorder, SBorder) + // .Padding(5.0f) + // .BorderImage(this, &FHoudiniOutputDetails::GetThumbnailBorder, (const TWeakObjectPtr&) DT->RowStruct) + // .OnMouseDoubleClick(this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) DT->RowStruct) + // [ + // SNew(SBox) + // .WidthOverride(64) + // .HeightOverride(64) + // .ToolTipText(FText::FromName(DT->RowStruct->GetFName())) + // [ + // RSThumbnail->MakeThumbnailWidget() + // ] + // ] + // ] + // + SHorizontalBox::Slot() + // .AutoWidth() + // .Padding(2.0f, 0.0f) + // .VAlign(VAlign_Center) + // [ + // SNew(SVerticalBox) + // + SVerticalBox::Slot() + // .AutoHeight() + // .Padding(2.0f, 0.0f) + // .VAlign(VAlign_Center) + // [ + // SNew(STextBlock) + // .Text(FText::FromName(DT->RowStruct->GetFName())) + // .Font(IDetailLayoutBuilder::GetDetailFont()) + // ] + // + SVerticalBox::Slot() + // .AutoHeight() + // .Padding(2.0f, 0.0f) + // .VAlign(VAlign_Center) + // .HAlign(HAlign_Left) + // [ + // PropertyCustomizationHelpers::MakeBrowseButton( + // FSimpleDelegate::CreateSP( + // this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&) DT->RowStruct), + // TAttribute(LOCTEXT("HoudiniDataTableRowStructBrowseButton", "Browse to the generated Data Table's row struct in the content browser"))) + // ] + // ] + // + // ]; + + OutputObjectThumbnailBorders.Add((UObject*) AnimSequence, ThumbnailBorder); + } + } +} + +void +FHoudiniOutputDetails::CreateCurveOutputWidget(IDetailCategoryBuilder& HouOutputCategory, const TWeakObjectPtr& InOutput) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + int32 OutputObjIdx = 0; + TMap& OutputObjects = InOutput->GetOutputObjects(); + for (auto& IterObject : OutputObjects) + { + FHoudiniOutputObject& CurrentOutputObject = IterObject.Value; + for(auto Component : IterObject.Value.OutputComponents) + { + USceneComponent* SplineComponent = Cast(Component); + if (!IsValid(SplineComponent)) + continue; + + FHoudiniOutputObjectIdentifier& OutputIdentifier = IterObject.Key; + FHoudiniGeoPartObject HoudiniGeoPartObject; + for (const auto& curHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + if (!OutputIdentifier.Matches(curHGPO)) + continue; + + HoudiniGeoPartObject = curHGPO; + break; + } + + CreateCurveWidgets(HouOutputCategory, InOutput, SplineComponent, CurrentOutputObject, OutputIdentifier, HoudiniGeoPartObject); + } + } +} + +void FHoudiniOutputDetails::CreateGeometryCollectionOutputWidget(IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + TMap& OutputObjects = InOutput->GetOutputObjects(); + + + for (auto& IterObject : OutputObjects) + { + FHoudiniOutputObject& CurrentOutputObject = IterObject.Value; + + UGeometryCollection* GeometryCollection = Cast(IterObject.Value.OutputObject); + if (!IsValid(GeometryCollection)) + continue; + + FHoudiniOutputObjectIdentifier& OutputIdentifier = IterObject.Key; + FHoudiniGeoPartObject HoudiniGeoPartObject; + for (const auto& curHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + if (!OutputIdentifier.Matches(curHGPO)) + continue; + + HoudiniGeoPartObject = curHGPO; + break; + } + + CreateGeometryCollectionWidgets(HouOutputCategory, InOutput, GeometryCollection, CurrentOutputObject, OutputIdentifier, HoudiniGeoPartObject); + } +} + +void +FHoudiniOutputDetails::CreateDataTableOutputWidget(IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + TMap& OutputObjects = InOutput->GetOutputObjects(); + int32 Idx = 1; + for (auto& IterObject : OutputObjects) + { + FHoudiniOutputObject& CurrentOutputObject = IterObject.Value; + + if (UDataTable* DT = Cast(CurrentOutputObject.OutputObject)) + { + FString DTIndex = "Data Table Output " + FString::FromInt(Idx++); + IDetailGroup& DTOutputGrp = HouOutputCategory.AddGroup(FName(DTIndex), FText::FromString(DTIndex), false, false); + TSharedRef DTVerticalBox = SNew(SVerticalBox); + TSharedRef RSVerticalBox = SNew(SVerticalBox); + TSharedPtr DTThumbnailBorder; + TSharedPtr RSThumbnailBorder; + IDetailLayoutBuilder& DetailLayoutBuilder = HouOutputCategory.GetParentLayout(); + TSharedPtr AssetThumbnailPool = DetailLayoutBuilder.GetThumbnailPool(); + TSharedPtr< FAssetThumbnail > DTThumbnail = + MakeShareable(new FAssetThumbnail(DT, 64, 64, AssetThumbnailPool)); + TSharedPtr< FAssetThumbnail > RSThumbnail = + MakeShareable(new FAssetThumbnail(DT->RowStruct, 64, 64, AssetThumbnailPool)); + + DTOutputGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Data Table")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + DTVerticalBox + ]; + + DTVerticalBox->AddSlot() + .Padding(0, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0.0f, 0.0f, 2.0f, 0.0f) + .AutoWidth() + [ + SAssignNew(DTThumbnailBorder, SBorder) + .Padding(5.0f) + .BorderImage(this, &FHoudiniOutputDetails::GetThumbnailBorder, (const TWeakObjectPtr&) DT) + .OnMouseDoubleClick(this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) DT) + [ + SNew(SBox) + .WidthOverride(64) + .HeightOverride(64) + .ToolTipText(FText::FromString(DT->GetPathName())) + [ + DTThumbnail->MakeThumbnailWidget() + ] + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromName(DT->GetFName())) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2, 0) + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&) DT), + TAttribute(LOCTEXT("HoudiniDataTableBrowseButton", "Browse to this generated Data Table in the content browser"))) + ] + ] + ]; + + DTOutputGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Row Struct")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + RSVerticalBox + ]; + + RSVerticalBox->AddSlot() + .Padding(0, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0.0f, 0.0f, 2.0f, 0.0f) + .AutoWidth() + [ + SAssignNew(RSThumbnailBorder, SBorder) + .Padding(5.0f) + .BorderImage(this, &FHoudiniOutputDetails::GetThumbnailBorder, (const TWeakObjectPtr&) DT->RowStruct) + .OnMouseDoubleClick(this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) DT->RowStruct) + [ + SNew(SBox) + .WidthOverride(64) + .HeightOverride(64) + .ToolTipText(FText::FromName(DT->RowStruct->GetFName())) + [ + RSThumbnail->MakeThumbnailWidget() + ] + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromName(DT->RowStruct->GetFName())) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&) DT->RowStruct), + TAttribute(LOCTEXT("HoudiniDataTableRowStructBrowseButton", "Browse to the generated Data Table's row struct in the content browser"))) + ] + ] + + ]; + + OutputObjectThumbnailBorders.Add((UObject*) DT, DTThumbnailBorder); + OutputObjectThumbnailBorders.Add((UObject*) DT->RowStruct, RSThumbnailBorder); + } + } +} + +void FHoudiniOutputDetails::CreateLandscapeSplineOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, const TWeakObjectPtr& InOutput) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + const TMap& OutputObjects = InOutput->GetOutputObjects(); + + for (const auto& IterObject : OutputObjects) + { + const FHoudiniOutputObject& CurrentOutputObject = IterObject.Value; + + ALandscapeSplineActor* const LandscapeSplineActor = Cast(IterObject.Value.OutputObject); + + if (!IsValid(LandscapeSplineActor)) + continue; + + const FHoudiniOutputObjectIdentifier& OutputIdentifier = IterObject.Key; + FHoudiniGeoPartObject HoudiniGeoPartObject; + for (const auto& curHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + if (!OutputIdentifier.Matches(curHGPO)) + continue; + + HoudiniGeoPartObject = curHGPO; + break; + } + + CreateLandscapeSplineWidgets( + HouOutputCategory, + InOutput, + LandscapeSplineActor, + CurrentOutputObject, + OutputIdentifier, + HoudiniGeoPartObject); + } +} + +void FHoudiniOutputDetails::CreateLandscapeSplineWidgets( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const TWeakObjectPtr& LandscapeSplineActor, + const FHoudiniOutputObject& OutputObject, + const FHoudiniOutputObjectIdentifier& OutputIdentifier, + const FHoudiniGeoPartObject& HoudiniGeoPartObject) +{ + if (!IsValidWeakPointer(LandscapeSplineActor)) + return; + + ULandscapeSplinesComponent* const SplinesComponent = LandscapeSplineActor->GetSplinesComponent(); + if (!IsValid(SplinesComponent)) + return; + + // FHoudiniOutputObject const* const FoundOutputObject = InOutput->GetOutputObjects().Find(OutputIdentifier); + + // Get thumbnail pool for this builder. + const IDetailLayoutBuilder& DetailLayoutBuilder = HouOutputCategory.GetParentLayout(); + const TSharedPtr AssetThumbnailPool = DetailLayoutBuilder.GetThumbnailPool(); + + const FString Label = HoudiniGeoPartObject.bHasCustomPartName ? HoudiniGeoPartObject.PartName : LandscapeSplineActor->GetName(); + + // Create thumbnail for this mesh. + const TSharedPtr StaticMeshThumbnail = + MakeShareable(new FAssetThumbnail(LandscapeSplineActor.Get(), 64, 64, AssetThumbnailPool)); + TSharedPtr StaticMeshThumbnailBorder; + + const TSharedRef VerticalBox = SNew(SVerticalBox); + + IDetailGroup& StaticMeshGrp = HouOutputCategory.AddGroup(FName(*Label), FText::FromString(Label)); + + const FString LandscapeSplineLabel = TEXT("Landscape Spline"); + + StaticMeshGrp.AddWidgetRow() + .NameContent() + [ + SNew( STextBlock ) + .Text( FText::FromString(LandscapeSplineLabel) ) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + VerticalBox + ]; + + VerticalBox->AddSlot() + .Padding( 0, 2 ) + .AutoHeight() + [ + SNew( SHorizontalBox ) + +SHorizontalBox::Slot() + .Padding( 0.0f, 0.0f, 2.0f, 0.0f ) + .AutoWidth() + [ + SAssignNew( StaticMeshThumbnailBorder, SBorder ) + .Padding( 5.0f ) + .BorderImage( this, &FHoudiniOutputDetails::GetThumbnailBorder, (const TWeakObjectPtr&) LandscapeSplineActor ) + .OnMouseDoubleClick( this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) LandscapeSplineActor ) + [ + SNew( SBox ) + .WidthOverride( 64 ) + .HeightOverride( 64 ) + .ToolTipText( FText::FromString( SplinesComponent->GetPathName() ) ) + [ + StaticMeshThumbnail->MakeThumbnailWidget() + ] + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&) LandscapeSplineActor), + TAttribute(LOCTEXT("HoudiniLandscapeSplineActorBrowseButton", "Select this Landscape Spline Actor in the viewport"))) + ] + ]; + + OutputObjectThumbnailBorders.Add(LandscapeSplineActor, StaticMeshThumbnailBorder); +} + + +void +FHoudiniOutputDetails::CreateCurveWidgets( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const TWeakObjectPtr& SplineComponent, + FHoudiniOutputObject& OutputObject, + FHoudiniOutputObjectIdentifier& OutputIdentifier, + FHoudiniGeoPartObject& HoudiniGeoPartObject) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + // We support Unreal Spline out only for now + const TWeakObjectPtr& SplineOutput = Cast(SplineComponent); + if (!IsValidWeakPointer(SplineOutput)) + return; + + const TWeakObjectPtr& HAC = Cast(InOutput->GetOuter()); + const TWeakObjectPtr& HC = Cast(InOutput->GetOuter()); + if (!IsValidWeakPointer(HAC) && !IsValidWeakPointer(HC)) + return; + + const TWeakObjectPtr& OwnerActor = HC.IsValid() ? HC->GetOwner() : HAC->GetOwner(); + if (!IsValidWeakPointer(OwnerActor)) + return; + + FHoudiniCurveOutputProperties* OutputProperty = &(OutputObject.CurveOutputProperty); + EHoudiniCurveType OutputCurveType = OutputObject.CurveOutputProperty.CurveType; + + FString Label = SplineComponent->GetName(); + if (HoudiniGeoPartObject.bHasCustomPartName) + Label = HoudiniGeoPartObject.PartName; + + //Label += FString("_") + OutputIdentifier.SplitIdentifier; + + FString OutputCurveName = OutputObject.BakeName; + if(OutputCurveName.IsEmpty()) + OutputCurveName = OwnerActor->GetActorNameOrLabel() + "_" + Label; + + const FText& LabelText = FText::FromString("Unreal Spline"); + + IDetailGroup& CurveOutputGrp = HouOutputCategory.AddGroup(FName(*Label), FText::FromString(Label), false, false); + + // Bake name row UI + CurveOutputGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("BakeBaseName", "Bake Name")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .FillWidth(1) + [ + SNew(SEditableTextBox) + .Text(FText::FromString(OutputObject.BakeName)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .ToolTipText(LOCTEXT("BakeNameTip", "The base name of the baked asset")) + .HintText(LOCTEXT("BakeNameHintText", "Input bake name to override default")) + .OnTextCommitted_Lambda([InOutput, OutputIdentifier](const FText& Val, ETextCommit::Type TextCommitType) + { + if (InOutput.IsValid()) + { + FHoudiniOutputDetails::OnBakeNameCommitted(Val, TextCommitType, InOutput, OutputIdentifier); + FHoudiniEngineUtils::UpdateEditorProperties(true); + } + }) + ] + + + SHorizontalBox::Slot() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertNameOverride", "Revert bake name override")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([InOutput, OutputIdentifier]() + { + FHoudiniOutputDetails::OnRevertBakeNameToDefault(InOutput, OutputIdentifier); + return FReply::Handled(); + }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ]; + + CurveOutputGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("OutputCurveSplineType", "Spline Type")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .ToolTipText_Lambda([SplineOutput, Label, OutputCurveType]() + { + FString ToolTipStr = FString::Printf(TEXT(" curve: %s\n Export type: Unreal Spline\n num points: %d\n curve type: %s\n closed: %s"), + *Label, + SplineOutput->GetNumberOfSplinePoints(), + *FHoudiniEngineEditorUtils::HoudiniCurveTypeToString(OutputCurveType), + SplineOutput->IsClosedLoop() ? *(FString("yes")) : *(FString("no"))); + + return FText::FromString(ToolTipStr); + }) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(STextBlock) + // We support Unreal Spline output only for now... + .Text(LOCTEXT("OutputCurveSplineTypeUnreal", "Unreal Spline")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ]; + + //if (bIsUnrealSpline) + //{ + const TWeakObjectPtr& UnrealSpline = Cast(SplineComponent); + + // Curve type combo box UI + auto InitialSelectionLambda = [OutputProperty]() + { + if (OutputProperty->CurveType == EHoudiniCurveType::Polygon) + { + return (*FHoudiniEngineEditor::Get().GetUnrealOutputCurveTypeLabels())[0]; + } + else + { + return (*FHoudiniEngineEditor::Get().GetUnrealOutputCurveTypeLabels())[1]; + } + }; + + TSharedPtr>> UnrealCurveTypeComboBox; + + CurveOutputGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Text(LOCTEXT("OutputCurveUnrealSplinePointType", "Spline Point Type")) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SAssignNew(UnrealCurveTypeComboBox, SComboBox>) + .OptionsSource(FHoudiniEngineEditor::Get().GetUnrealOutputCurveTypeLabels()) + .InitiallySelectedItem(InitialSelectionLambda()) + .OnGenerateWidget_Lambda( + [](TSharedPtr< FString > InItem) + { + return SNew(STextBlock).Text(FText::FromString(*InItem)); + }) + .OnSelectionChanged_Lambda( + [OutputIdentifier, InOutput, SplineComponent](TSharedPtr< FString > NewChoice, ESelectInfo::Type SelectType) + { + if (!IsValidWeakPointer(SplineComponent)) + return; + + // Set the curve point type locally + USplineComponent* Spline = Cast(SplineComponent); + if (!IsValid(Spline)) + return; + + FString *NewChoiceStr = NewChoice.Get(); + if (!NewChoiceStr) + return; + + if (!IsValidWeakPointer(InOutput)) + return; + + FHoudiniOutputObject* const OutputObject = InOutput->GetOutputObjects().Find(OutputIdentifier); + if (!OutputObject) + return; + + FHoudiniCurveOutputProperties& OutputProperty = OutputObject->CurveOutputProperty; + + if (*NewChoiceStr == "Linear") + { + if (OutputProperty.CurveType == EHoudiniCurveType::Polygon) + return; + + OutputProperty.CurveType = EHoudiniCurveType::Polygon; + + for (int32 PtIdx = 0; PtIdx < Spline->GetNumberOfSplinePoints(); ++PtIdx) + { + Spline->SetSplinePointType(PtIdx, ESplinePointType::Linear); + } + + FHoudiniEngineEditorUtils::ReselectSelectedActors(); + FHoudiniEngineUtils::UpdateEditorProperties(true); + } + else if (*NewChoiceStr == "Curve") + { + if (OutputProperty.CurveType != EHoudiniCurveType::Polygon) + return; + + OutputProperty.CurveType = EHoudiniCurveType::Bezier; + + for (int32 PtIdx = 0; PtIdx < Spline->GetNumberOfSplinePoints(); ++PtIdx) + { + Spline->SetSplinePointType(PtIdx, ESplinePointType::Curve); + } + + FHoudiniEngineEditorUtils::ReselectSelectedActors(); + FHoudiniEngineUtils::UpdateEditorProperties(true); + } + }) + [ + SNew(STextBlock) + .Text_Lambda([OutputProperty]() + { + if (OutputProperty->CurveType == EHoudiniCurveType::Polygon) + return FText::FromString(TEXT("Linear")); + else + return FText::FromString(TEXT("Curve")); + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ]; + + // Add closed curve checkbox UI + TSharedPtr ClosedCheckBox; + CurveOutputGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Text(LOCTEXT("OutputCurveUnrealSplineClosed", "Closed")) + ] + .ValueContent() + [ + SAssignNew(ClosedCheckBox, SCheckBox) + .OnCheckStateChanged_Lambda([UnrealSpline, InOutput](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(UnrealSpline)) + return; + + UnrealSpline->SetClosedLoop(NewState == ECheckBoxState::Checked); + FHoudiniEngineEditorUtils::ReselectSelectedActors(); + + if (InOutput.IsValid()) + FHoudiniEngineUtils::UpdateEditorProperties(true); + }) + .IsChecked_Lambda([UnrealSpline]() + { + if (!IsValidWeakPointer(UnrealSpline)) + return ECheckBoxState::Unchecked; + + return UnrealSpline->IsClosedLoop() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + ]; + //} + + // Add Bake Button UI + TSharedPtr BakeButton; + CurveOutputGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + ] + .ValueContent() + [ + SAssignNew(BakeButton, SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Text(LOCTEXT("OutputCurveBakeButtonText", "Bake Output")) + .IsEnabled(true) + .ToolTipText(LOCTEXT("OutputCurveBakeButtonUnrealSplineTooltipText", "Bake this output curve to an Actor with a Spline Component.")) + .OnClicked_Lambda([InOutput, SplineComponent, OutputIdentifier, HC, OutputCurveName]() + { + if (!SplineComponent.IsValid() || !InOutput.IsValid()) + return FReply::Handled(); + + if(!HC.IsValid()) + return FReply::Handled(); + + FHoudiniOutputObject* const OutputObject = InOutput->GetOutputObjects().Find(OutputIdentifier); + if (OutputObject) + return FReply::Handled(); + + TArray AllOutputs; + AllOutputs.Reserve(HC->GetNumOutputs()); + HC->GetOutputs(AllOutputs); + + FHoudiniGeoPartObject HoudiniGeoPartObject; + for (const auto& curHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + if (!OutputIdentifier.Matches(curHGPO)) + continue; + + HoudiniGeoPartObject = curHGPO; + break; + } + + FHoudiniBakeSettings BakeSettings; + BakeSettings.SetFromCookable(HC.Get()); + + if (OutputObject) + { + FHoudiniOutputDetails::OnBakeOutputObject( + OutputCurveName, + SplineComponent.Get(), + OutputIdentifier, + *OutputObject, + HoudiniGeoPartObject, + (UObject*)HC.Get(), + InOutput.Get(), + HC->GetBakeFolderOrDefault(), + BakeSettings, + HC->GetTemporaryCookFolderOrDefault(), + EHoudiniLandscapeOutputBakeType::InValid, + AllOutputs); + } + + return FReply::Handled(); + }) + ]; +} + + +void +FHoudiniOutputDetails::CreateGeometryCollectionWidgets( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const TWeakObjectPtr& GeometryCollection, + FHoudiniOutputObject& OutputObject, + FHoudiniOutputObjectIdentifier& OutputIdentifier, + FHoudiniGeoPartObject& HoudiniGeoPartObject) +{ + if (!IsValidWeakPointer(GeometryCollection)) + return; + + FHoudiniOutputObject* FoundOutputObject = InOutput->GetOutputObjects().Find(OutputIdentifier); + FString BakeName = FoundOutputObject ? FoundOutputObject->BakeName : FString(); + + // Get thumbnail pool for this builder. + IDetailLayoutBuilder & DetailLayoutBuilder = HouOutputCategory.GetParentLayout(); + TSharedPtr AssetThumbnailPool = DetailLayoutBuilder.GetThumbnailPool(); + + // TODO: GetBakingBaseName! + FString Label = GeometryCollection->GetName(); + if (HoudiniGeoPartObject.bHasCustomPartName) + Label = HoudiniGeoPartObject.PartName; + + int32 NumPieces = GeometryCollection->GeometrySource.Num(); + FString PiecesString = NumPieces > 1 ? FString::FromInt(NumPieces) + TEXT(" pieces.") : TEXT("Geometry Collection"); + + // Create thumbnail for this mesh. + TSharedPtr< FAssetThumbnail > StaticMeshThumbnail = + MakeShareable(new FAssetThumbnail(GeometryCollection.Get(), 64, 64, AssetThumbnailPool)); + TSharedPtr StaticMeshThumbnailBorder; + + TSharedRef VerticalBox = SNew(SVerticalBox); + + IDetailGroup& StaticMeshGrp = HouOutputCategory.AddGroup(FName(*Label), FText::FromString(Label)); + + FString GeometryCollectionLabel = TEXT( "Geometry Collection" ); + + StaticMeshGrp.AddWidgetRow() + .NameContent() + [ + SNew( STextBlock ) + .Text( FText::FromString(GeometryCollectionLabel) ) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + VerticalBox + ]; + + VerticalBox->AddSlot() + .Padding( 0, 2 ) + .AutoHeight() + [ + SNew( SHorizontalBox ) + +SHorizontalBox::Slot() + .Padding( 0.0f, 0.0f, 2.0f, 0.0f ) + .AutoWidth() + [ + SAssignNew( StaticMeshThumbnailBorder, SBorder ) + .Padding( 5.0f ) + .BorderImage( this, &FHoudiniOutputDetails::GetThumbnailBorder, (const TWeakObjectPtr&)GeometryCollection ) + .OnMouseDoubleClick( this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) GeometryCollection ) + [ + SNew( SBox ) + .WidthOverride( 64 ) + .HeightOverride( 64 ) + .ToolTipText( FText::FromString( GeometryCollection->GetPathName() ) ) + [ + StaticMeshThumbnail->MakeThumbnailWidget() + ] + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(PiecesString)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&)GeometryCollection), + TAttribute(LOCTEXT("HoudiniGeometryCollectionBrowseButton", "Browse to this generated Geometry Collection in the content browser"))) + ] + ]; + + OutputObjectThumbnailBorders.Add((UObject*)GeometryCollection.Get(), StaticMeshThumbnailBorder); + +} + +void +FHoudiniOutputDetails::CreateStaticMeshAndMaterialWidgets( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const TWeakObjectPtr& StaticMesh, + FHoudiniOutputObjectIdentifier& OutputIdentifier, + FHoudiniGeoPartObject& HoudiniGeoPartObject, + const bool& bIsProxyMeshCurrent) +{ + if (!IsValidWeakPointer(StaticMesh)) + return; + + const TWeakObjectPtr& HC = Cast(InOutput->GetOuter()); + if (!IsValidWeakPointer(HC)) + return; + + FHoudiniOutputObject* FoundOutputObject = InOutput->GetOutputObjects().Find(OutputIdentifier); + FString BakeName = FoundOutputObject ? FoundOutputObject->BakeName : FString(); + + // Get thumbnail pool for this builder. + IDetailLayoutBuilder & DetailLayoutBuilder = HouOutputCategory.GetParentLayout(); + TSharedPtr AssetThumbnailPool = DetailLayoutBuilder.GetThumbnailPool(); + + // TODO: GetBakingBaseName! + FString Label = StaticMesh->GetName(); + if (HoudiniGeoPartObject.bHasCustomPartName) + Label = HoudiniGeoPartObject.PartName; + + // Create thumbnail for this mesh. + TSharedPtr< FAssetThumbnail > StaticMeshThumbnail = + MakeShareable(new FAssetThumbnail(StaticMesh.Get(), 64, 64, AssetThumbnailPool)); + TSharedPtr StaticMeshThumbnailBorder; + + TSharedRef VerticalBox = SNew(SVerticalBox); + + IDetailGroup& StaticMeshGrp = HouOutputCategory.AddGroup(FName(*Label), FText::FromString(Label)); + StaticMeshGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("BakeBaseName", "Bake Name")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .FillWidth(1) + [ + SNew(SEditableTextBox) + .Text(FText::FromString(BakeName)) + .HintText(LOCTEXT("BakeNameHintText", "Input bake name to override default")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .OnTextCommitted_Lambda([OutputIdentifier, InOutput](const FText& Val, ETextCommit::Type TextCommitType) + { + if (!IsValidWeakPointer(InOutput)) + return; + + FHoudiniOutputDetails::OnBakeNameCommitted(Val, TextCommitType, InOutput, OutputIdentifier); + FHoudiniEngineUtils::UpdateEditorProperties(true); + }) + .ToolTipText( LOCTEXT( "BakeNameTip", "The base name of the baked asset") ) + ] + + +SHorizontalBox::Slot() + .Padding( 2.0f, 0.0f ) + .VAlign( VAlign_Center ) + .AutoWidth() + [ + SNew( SButton ) + .ToolTipText( LOCTEXT( "RevertNameOverride", "Revert bake name override" ) ) + .ButtonStyle(_GetEditorStyle(), "NoBorder" ) + .ContentPadding( 0 ) + .Visibility( EVisibility::Visible ) + .OnClicked_Lambda([InOutput, OutputIdentifier]() + { + FHoudiniOutputDetails::OnRevertBakeNameToDefault(InOutput, OutputIdentifier); + FHoudiniEngineUtils::UpdateEditorProperties(true); + return FReply::Handled(); + }) + [ + SNew( SImage ) + .Image(_GetEditorStyle().GetBrush( "PropertyWindow.DiffersFromDefault" ) ) + ] + ] + ]; + + // Add details on the SM colliders + EHoudiniSplitType SplitType = FHoudiniMeshTranslator::GetSplitTypeFromSplitName(OutputIdentifier.SplitIdentifier); + FString MeshLabel = TEXT( "Static Mesh" ); + + // If the Proxy mesh is more recent, indicate it in the details + if (bIsProxyMeshCurrent) + { + MeshLabel += TEXT("\n(unrefined)"); + } + + // Indicate that this mesh is instanced + if (HoudiniGeoPartObject.bIsInstanced) + { + MeshLabel += TEXT("\n(instanced)"); + } + + if (HoudiniGeoPartObject.bIsTemplated) + { + MeshLabel += TEXT("\n(templated)"); + } + + int32 NumSimpleColliders = 0; + if (IsValid(StaticMesh->GetBodySetup())) + NumSimpleColliders = StaticMesh->GetBodySetup()->AggGeom.GetElementCount(); + + if(NumSimpleColliders > 0) + { + MeshLabel += TEXT( "\n(") + FString::FromInt(NumSimpleColliders) + TEXT(" Simple Collider" ); + if (NumSimpleColliders > 1 ) + MeshLabel += TEXT("s"); + MeshLabel += TEXT(")"); + } + else if (SplitType == EHoudiniSplitType::RenderedComplexCollider) + { + MeshLabel += TEXT( "\n(Rendered Complex Collider)" ); + } + else if(SplitType == EHoudiniSplitType::InvisibleComplexCollider ) + { + MeshLabel += TEXT( "\n(Invisible Complex Collider)" ); + } + + if ( StaticMesh->GetNumLODs() > 1 ) + MeshLabel += TEXT("\n(") + FString::FromInt( StaticMesh->GetNumLODs() ) + TEXT(" LODs)"); + + if (HoudiniGeoPartObject.AllMeshSockets.Num() > 0) + { + if (bIsProxyMeshCurrent) + { + // Proxy is current, show the number of sockets on the HGPO + MeshLabel += TEXT("\n(") + FString::FromInt(HoudiniGeoPartObject.AllMeshSockets.Num()) + TEXT(" sockets)"); + } + else + { + // Show the number of sockets on the SM + MeshLabel += TEXT("\n(") + FString::FromInt(StaticMesh->Sockets.Num()) + TEXT(" sockets)"); + } + } + + UHoudiniAssetComponent* HoudiniAssetComponent = Cast(InOutput->GetOuter()); + StaticMeshGrp.AddWidgetRow() + .NameContent() + [ + SNew( STextBlock ) + .Text( FText::FromString(MeshLabel) ) + .Font( IDetailLayoutBuilder::GetDetailFont() ) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + VerticalBox + ]; + + VerticalBox->AddSlot() + .Padding( 0, 2 ) + .AutoHeight() + [ + SNew( SHorizontalBox ) + +SHorizontalBox::Slot() + .Padding( 0.0f, 0.0f, 2.0f, 0.0f ) + .AutoWidth() + [ + SAssignNew( StaticMeshThumbnailBorder, SBorder ) + .Padding( 5.0f ) + .BorderImage( this, &FHoudiniOutputDetails::GetThumbnailBorder, (const TWeakObjectPtr&)StaticMesh ) + .OnMouseDoubleClick( this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) StaticMesh ) + [ + SNew( SBox ) + .WidthOverride( 64 ) + .HeightOverride( 64 ) + .ToolTipText( FText::FromString( StaticMesh->GetPathName() ) ) + [ + StaticMeshThumbnail->MakeThumbnailWidget() + ] + ] + ] + + +SHorizontalBox::Slot() + .FillWidth( 1.0f ) + .Padding( 0.0f, 4.0f, 4.0f, 4.0f ) + .VAlign( VAlign_Center ) + [ + SNew( SVerticalBox ) + +SVerticalBox::Slot() + [ + SNew( SHorizontalBox ) + +SHorizontalBox::Slot() + .MaxWidth( 120.0f ) + [ + SNew( SButton ) + .VAlign( VAlign_Center ) + .HAlign( HAlign_Center ) + .Text( LOCTEXT( "BakeOutputMesh", "Bake Output" ) ) + .IsEnabled(true) + .OnClicked_Lambda([BakeName, StaticMesh, OutputIdentifier, InOutput, HC]() + { + if (!StaticMesh.IsValid() || !InOutput.IsValid() || !HC.IsValid()) + return FReply::Handled(); + + FHoudiniOutputObject* const FoundOutputObject = InOutput->GetOutputObjects().Find(OutputIdentifier); + if (!FoundOutputObject) + return FReply::Handled(); + + TArray AllOutputs; + FString TempCookFolder; + FString BakeFolder; + FHoudiniBakeSettings BakeSettings; + AllOutputs.Reserve(HC->GetNumOutputs()); + HC->GetOutputs(AllOutputs); + TempCookFolder = HC->GetTemporaryCookFolderOrDefault(); + BakeFolder = HC->GetBakeFolderOrDefault(); + BakeSettings.SetFromCookable(HC.Get()); + + FHoudiniGeoPartObject HoudiniGeoPartObject; + for (const auto& curHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + if (!OutputIdentifier.Matches(curHGPO)) + continue; + + HoudiniGeoPartObject = curHGPO; + break; + } + + FHoudiniOutputDetails::OnBakeOutputObject( + BakeName, + StaticMesh.Get(), + OutputIdentifier, + *FoundOutputObject, + HoudiniGeoPartObject, + (UObject*)HC.Get(), + InOutput.Get(), + BakeFolder, + BakeSettings, + TempCookFolder, + EHoudiniLandscapeOutputBakeType::InValid, + AllOutputs); + + return FReply::Handled(); + }) + .ToolTipText( LOCTEXT( "HoudiniStaticMeshBakeButton", "Bake this output object to a Static Mesh in the Bake folder." ) ) + ] + +SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&)StaticMesh), + TAttribute(LOCTEXT("HoudiniStaticMeshBrowseButton", "Browse to this generated static mesh in the content browser"))) + ] + ] + ] + ]; + + // Store thumbnail for this mesh. + OutputObjectThumbnailBorders.Add(StaticMesh, StaticMeshThumbnailBorder); + + // We need to add material box for each material present in this static mesh. + auto & StaticMeshMaterials = StaticMesh->GetStaticMaterials(); + for ( int32 MaterialIdx = 0; MaterialIdx < StaticMeshMaterials.Num(); ++MaterialIdx ) + { + UMaterialInterface * MaterialInterface = StaticMeshMaterials[ MaterialIdx ].MaterialInterface; + TSharedPtr< SBorder > MaterialThumbnailBorder; + TSharedPtr< SHorizontalBox > HorizontalBox = NULL; + + FString MaterialName, MaterialPathName; + if ( IsValid(MaterialInterface) + && IsValid(MaterialInterface->GetOuter()) ) + { + MaterialName = MaterialInterface->GetName(); + MaterialPathName = MaterialInterface->GetPathName(); + } + else + { + MaterialInterface = nullptr; + MaterialName = TEXT("Material (invalid)") + FString::FromInt( MaterialIdx ) ; + MaterialPathName = TEXT("Material (invalid)") + FString::FromInt(MaterialIdx); + } + + // Create thumbnail for this material. + TSharedPtr< FAssetThumbnail > MaterialInterfaceThumbnail = + MakeShareable( new FAssetThumbnail( MaterialInterface, 64, 64, AssetThumbnailPool ) ); + + VerticalBox->AddSlot().Padding( 0, 2 ) + [ + SNew( SAssetDropTarget ) + .OnAreAssetsAcceptableForDrop( this, &FHoudiniOutputDetails::OnMaterialInterfaceDraggedOver ) + .OnAssetsDropped( + this, &FHoudiniOutputDetails::OnMaterialInterfaceDropped, StaticMesh, InOutput, MaterialIdx ) + [ + SAssignNew( HorizontalBox, SHorizontalBox ) + ] + ]; + + HorizontalBox->AddSlot().Padding( 0.0f, 0.0f, 2.0f, 0.0f ).AutoWidth() + [ + SAssignNew( MaterialThumbnailBorder, SBorder ) + .Padding( 5.0f ) + .BorderImage( + this, &FHoudiniOutputDetails::GetMaterialInterfaceThumbnailBorder, (const TWeakObjectPtr&)StaticMesh, MaterialIdx ) + .OnMouseDoubleClick( + this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&)MaterialInterface ) + [ + SNew( SBox ) + .WidthOverride( 64 ) + .HeightOverride( 64 ) + .ToolTipText( FText::FromString( MaterialPathName ) ) + [ + MaterialInterfaceThumbnail->MakeThumbnailWidget() + ] + ] + ]; + + // Store thumbnail for this mesh and material index. + { + TPairInitializer&, int32> Pair( StaticMesh, MaterialIdx ); + MaterialInterfaceThumbnailBorders.Add( Pair, MaterialThumbnailBorder ); + } + + // ComboBox and buttons + TSharedPtr ComboAndButtonBox; + HorizontalBox->AddSlot() + .FillWidth(1.0f) + .Padding(0.0f, 4.0f, 4.0f, 4.0f) + [ + SAssignNew(ComboAndButtonBox, SVerticalBox) + ]; + + // Add Combo box + TSharedPtr< SComboButton > AssetComboButton; + ComboAndButtonBox->AddSlot().VAlign(VAlign_Center).FillHeight(1.0f) + [ + SNew(SVerticalBox) + SVerticalBox::Slot().VAlign(VAlign_Center).FillHeight(1.0f) + [ + SAssignNew(AssetComboButton, SComboButton) + .ButtonStyle(_GetEditorStyle(), "PropertyEditor.AssetComboStyle") + .ForegroundColor(_GetEditorStyle().GetColor("PropertyEditor.AssetName.ColorAndOpacity")) + .OnGetMenuContent(this, &FHoudiniOutputDetails::OnGetMaterialInterfaceMenuContent, + TWeakObjectPtr(MaterialInterface), (const TWeakObjectPtr&)StaticMesh, InOutput, MaterialIdx) + .ContentPadding(2.0f) + .ButtonContent() + [ + SNew(STextBlock) + .TextStyle(_GetEditorStyle(), "PropertyEditor.AssetClass") + .Font(_GetEditorStyle().GetFontStyle(FName(TEXT("PropertyWindow.NormalFont")))) + .Text(FText::FromString(MaterialName)) + ] + ] + ]; + + + // Create tooltip. + FFormatNamedArguments Args; + Args.Add(TEXT("Asset"), FText::FromString(MaterialName)); + FText MaterialTooltip = FText::Format( + LOCTEXT("BrowseToSpecificAssetInContentBrowser", "Browse to '{Asset}' in Content Browser"), Args); + + + // Add buttons + TSharedPtr< SHorizontalBox > ButtonBox; + ComboAndButtonBox->AddSlot().FillHeight(1.0f) + [ + SAssignNew(ButtonBox, SHorizontalBox) + ]; + + // Use CB selection arrow button + ButtonBox->AddSlot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeUseSelectedButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnUseContentBrowserSelectedMaterialInterface, + (const TWeakObjectPtr&)StaticMesh, InOutput, MaterialIdx), + TAttribute< FText >(LOCTEXT("UseSelectedAssetFromContentBrowser", "Use Selected Asset from Content Browser"))) + ]; + + // Browse CB button + ButtonBox->AddSlot() + .AutoWidth() + .Padding( 2.0f, 0.0f ) + .VAlign( VAlign_Center ) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&)MaterialInterface ), TAttribute< FText >( MaterialTooltip ) ) + ]; + + // Reset button + ButtonBox->AddSlot() + .AutoWidth() + .Padding( 2.0f, 0.0f ) + .VAlign( VAlign_Center ) + [ + SNew( SButton ) + .ToolTipText( LOCTEXT( "ResetToBaseMaterial", "Reset to base material" ) ) + .ButtonStyle(_GetEditorStyle(), "NoBorder" ) + .ContentPadding( 0 ) + .Visibility( EVisibility::Visible ) + .OnClicked( + this, &FHoudiniOutputDetails::OnResetMaterialInterfaceClicked, StaticMesh, InOutput, MaterialIdx) + [ + SNew( SImage ) + .Image(_GetEditorStyle().GetBrush( "PropertyWindow.DiffersFromDefault" ) ) + ] + ]; + + // Store combo button for this mesh and index. + { + TPairInitializer&, int32> Pair( StaticMesh, MaterialIdx ); + MaterialInterfaceComboButtons.Add( Pair, AssetComboButton ); + } + } +} + +void +FHoudiniOutputDetails::CreateProxyMeshAndMaterialWidgets( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const TWeakObjectPtr& ProxyMesh, + FHoudiniOutputObjectIdentifier& OutputIdentifier, + FHoudiniGeoPartObject& HoudiniGeoPartObject) +{ + if (!IsValidWeakPointer(ProxyMesh)) + return; + + FHoudiniOutputObject* FoundOutputObject = InOutput->GetOutputObjects().Find(OutputIdentifier); + FString BakeName = FoundOutputObject ? FoundOutputObject->BakeName : FString(); + + // Get thumbnail pool for this builder. + IDetailLayoutBuilder & DetailLayoutBuilder = HouOutputCategory.GetParentLayout(); + TSharedPtr AssetThumbnailPool = DetailLayoutBuilder.GetThumbnailPool(); + + // TODO: GetBakingBaseName! + FString Label = ProxyMesh->GetName(); + if (HoudiniGeoPartObject.bHasCustomPartName) + Label = HoudiniGeoPartObject.PartName; + + // Create thumbnail for this mesh. + TSharedPtr MeshThumbnail = MakeShareable(new FAssetThumbnail(ProxyMesh.Get(), 64, 64, AssetThumbnailPool)); + TSharedPtr MeshThumbnailBorder; + + TSharedRef< SVerticalBox > VerticalBox = SNew(SVerticalBox); + + IDetailGroup& StaticMeshGrp = HouOutputCategory.AddGroup(FName(*Label), FText::FromString(Label)); + + StaticMeshGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("BakeBaseName", "Bake Name")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .FillWidth(1) + [ + SNew(SEditableTextBox) + .Text(FText::FromString(BakeName)) + .HintText(LOCTEXT("BakeNameHintText", "Input bake name to override default")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .OnTextCommitted_Lambda([OutputIdentifier, InOutput](const FText& Val, ETextCommit::Type TextCommitType) + { + FHoudiniOutputDetails::OnBakeNameCommitted(Val, TextCommitType, InOutput, OutputIdentifier); + FHoudiniEngineUtils::UpdateEditorProperties(true); + }) + .ToolTipText(LOCTEXT("BakeNameTip", "The base name of the baked asset")) + ] + + SHorizontalBox::Slot() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertNameOverride", "Revert bake name override")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([InOutput, OutputIdentifier]() + { + FHoudiniOutputDetails::OnRevertBakeNameToDefault(InOutput, OutputIdentifier); + FHoudiniEngineUtils::UpdateEditorProperties(true); + return FReply::Handled(); + }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ]; + + // Add details on the Proxy Mesh + EHoudiniSplitType SplitType = FHoudiniMeshTranslator::GetSplitTypeFromSplitName(OutputIdentifier.SplitIdentifier); + FString MeshLabel = TEXT("Proxy Mesh"); + + // Indicate that this mesh is instanced + if (HoudiniGeoPartObject.bIsInstanced) + { + MeshLabel += TEXT("\n(instanced)"); + } + + if (HoudiniGeoPartObject.bIsTemplated) + { + MeshLabel += TEXT("\n(templated)"); + } + + // Mention complex colliders even if they are not used for proxies + if (SplitType == EHoudiniSplitType::RenderedComplexCollider) + { + MeshLabel += TEXT("\n(Rendered Complex Collider)"); + } + else if (SplitType == EHoudiniSplitType::InvisibleComplexCollider) + { + MeshLabel += TEXT("\n(Invisible Complex Collider)"); + } + + if (HoudiniGeoPartObject.AllMeshSockets.Num() > 0) + { + MeshLabel += TEXT("\n(") + FString::FromInt(HoudiniGeoPartObject.AllMeshSockets.Num()) + TEXT(" sockets)"); + } + + UHoudiniAssetComponent* HoudiniAssetComponent = Cast(InOutput->GetOuter()); + StaticMeshGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString(MeshLabel)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + VerticalBox + ]; + + VerticalBox->AddSlot() + .Padding(0, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0.0f, 0.0f, 2.0f, 0.0f) + .AutoWidth() + [ + SAssignNew(MeshThumbnailBorder, SBorder) + .Padding(5.0f) + .BorderImage(this, &FHoudiniOutputDetails::GetThumbnailBorder, (const TWeakObjectPtr&) ProxyMesh) + .OnMouseDoubleClick(this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) ProxyMesh) + [ + SNew(SBox) + .WidthOverride(64) + .HeightOverride(64) + .ToolTipText(FText::FromString(ProxyMesh->GetPathName())) + [ + MeshThumbnail->MakeThumbnailWidget() + ] + ] + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(0.0f, 4.0f, 4.0f, 4.0f) + .VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + //.MaxWidth(80.0f) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Text(LOCTEXT("RefineOutputProxy", "Refine Proxy")) + .IsEnabled(true) + .OnClicked(this, &FHoudiniOutputDetails::OnRefineClicked, (const TWeakObjectPtr&)ProxyMesh, InOutput) + .ToolTipText(LOCTEXT("RefineTooltip", "Refine this Proxy Mesh Component to a Static Mesh Component.")) + ] + ] + ] + ]; + + // Store thumbnail for this mesh. + OutputObjectThumbnailBorders.Add(ProxyMesh, MeshThumbnailBorder); + + // We need to add material box for each material present in this static mesh. + auto & ProxyMeshMaterials = ProxyMesh->GetStaticMaterials(); + for (int32 MaterialIdx = 0; MaterialIdx < ProxyMeshMaterials.Num(); ++MaterialIdx) + { + UMaterialInterface * MaterialInterface = ProxyMeshMaterials[MaterialIdx].MaterialInterface; + TSharedPtr< SBorder > MaterialThumbnailBorder; + TSharedPtr< SHorizontalBox > HorizontalBox = NULL; + + FString MaterialName, MaterialPathName; + if (IsValid(MaterialInterface) + && IsValid(MaterialInterface->GetOuter())) + { + MaterialName = MaterialInterface->GetName(); + MaterialPathName = MaterialInterface->GetPathName(); + } + else + { + MaterialInterface = nullptr; + MaterialName = TEXT("Material (invalid)") + FString::FromInt(MaterialIdx); + MaterialPathName = TEXT("Material (invalid)") + FString::FromInt(MaterialIdx); + } + + // Create thumbnail for this material. + TSharedPtr MaterialInterfaceThumbnail = + MakeShareable(new FAssetThumbnail(MaterialInterface, 64, 64, AssetThumbnailPool)); + + // No drop target + VerticalBox->AddSlot() + .Padding(0, 2) + [ + SNew(SAssetDropTarget) + //.OnIsAssetAcceptableForDrop(false) + //.OnAssetDropped( + // this, &FHoudiniOutputDetails::OnMaterialInterfaceDropped, StaticMesh, InOutput, MaterialIdx) + [ + SAssignNew(HorizontalBox, SHorizontalBox) + ] + ]; + + HorizontalBox->AddSlot() + .Padding(0.0f, 0.0f, 2.0f, 0.0f) + .AutoWidth() + [ + SAssignNew(MaterialThumbnailBorder, SBorder) + .Padding(5.0f) + .BorderImage( + this, &FHoudiniOutputDetails::GetMaterialInterfaceThumbnailBorder, (const TWeakObjectPtr&) ProxyMesh, MaterialIdx) + .OnMouseDoubleClick( + this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) MaterialInterface) + [ + SNew(SBox) + .WidthOverride(64) + .HeightOverride(64) + .ToolTipText(FText::FromString(MaterialPathName)) + [ + MaterialInterfaceThumbnail->MakeThumbnailWidget() + ] + ] + ]; + + // Store thumbnail for this mesh and material index. + { + TPairInitializer&, int32> Pair(ProxyMesh, MaterialIdx); + MaterialInterfaceThumbnailBorders.Add(Pair, MaterialThumbnailBorder); + } + + // Combo box and buttons + TSharedPtr ComboAndButtonBox; + HorizontalBox->AddSlot() + .FillWidth(1.0f) + .Padding(0.0f, 4.0f, 4.0f, 4.0f) + .VAlign(VAlign_Center) + [ + SAssignNew(ComboAndButtonBox, SVerticalBox) + ]; + + // Add combo box + TSharedPtr AssetComboButton; + ComboAndButtonBox->AddSlot().FillHeight(1.0f).VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + SVerticalBox::Slot().FillHeight(1.0f).VAlign(VAlign_Center) + [ + SAssignNew(AssetComboButton, SComboButton) + .ButtonStyle(_GetEditorStyle(), "PropertyEditor.AssetComboStyle") + .ForegroundColor(_GetEditorStyle().GetColor("PropertyEditor.AssetName.ColorAndOpacity")) + /*.OnGetMenuContent(this, &FHoudiniOutputDetails::OnGetMaterialInterfaceMenuContent, + MaterialInterface, StaticMesh, InOutput, MaterialIdx)*/ + .ContentPadding(2.0f) + .ButtonContent() + [ + SNew(STextBlock) + .TextStyle(_GetEditorStyle(), "PropertyEditor.AssetClass") + .Font(_GetEditorStyle().GetFontStyle(FName(TEXT("PropertyWindow.NormalFont")))) + .Text(FText::FromString(MaterialName)) + ] + ] + ]; + + + TSharedPtr ButtonBox; + ComboAndButtonBox->AddSlot().FillHeight(1.0f) + [ + SAssignNew(ButtonBox, SHorizontalBox) + ]; + + // Disable the combobutton for proxies + AssetComboButton->SetEnabled(false); + + // Add use selection form content browser array + ButtonBox->AddSlot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeUseSelectedButton( + /*FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnUseContentBrowserSelectedMaterialInterface, + (UObject*)ProxyMesh, InOutput, MaterialIdx),*/ + FSimpleDelegate::CreateLambda([]() {}), // Do nothing for proxies + TAttribute< FText >(LOCTEXT("UseSelectedAssetFromContentBrowser", "Use Selected Asset from Content Browser")), false) + // Disable the use CB selection button for proxies + ]; + + + // Create tooltip. + FFormatNamedArguments Args; + Args.Add(TEXT("Asset"), FText::FromString(MaterialName)); + FText MaterialTooltip = FText::Format( + LOCTEXT("BrowseToSpecificAssetInContentBrowser", "Browse to '{Asset}' in Content Browser"), Args); + + ButtonBox->AddSlot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP(this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&) MaterialInterface), TAttribute(MaterialTooltip)) + ]; + + /* + ButtonBox->AddSlot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("ResetToBaseMaterial", "Reset to base material")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility(EVisibility::Visible) + .OnClicked( + this, &FHoudiniOutputDetails::OnResetMaterialInterfaceClicked, StaticMesh, InOutput, MaterialIdx) + [ + SNew(SImage) + .Image(FEditorStyle::GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ]; + */ + + // Store combo button for this mesh and index. + { + TPairInitializer&, int32> Pair(ProxyMesh, MaterialIdx); + MaterialInterfaceComboButtons.Add(Pair, AssetComboButton); + } + } +} + +FText +FHoudiniOutputDetails::GetOutputDebugName(const TWeakObjectPtr& InOutput) +{ + if (!InOutput.IsValid()) + return FText::FromString(TEXT("Invalid")); + + // Get the name and type + FString OutputNameStr = InOutput->GetName() + TEXT(" ") + UHoudiniOutput::OutputTypeToString(InOutput->GetType()); + + // Then add the number of parts + OutputNameStr += TEXT(" (") + FString::FromInt(InOutput->GetHoudiniGeoPartObjects().Num()) + TEXT(" Part(s))\n"); + + return FText::FromString(OutputNameStr); +} +FText +FHoudiniOutputDetails::GetOutputDebugDescription(const TWeakObjectPtr& InOutput) +{ + if (!InOutput.IsValid()) + return FText::FromString(TEXT("Invalid")); + + const TArray& HGPOs = InOutput->GetHoudiniGeoPartObjects(); + + FString OutputValStr; + OutputValStr += TEXT("HGPOs:\n"); + for (auto& HGPO : HGPOs) + { + OutputValStr += TEXT(" - ") + HGPO.PartName + TEXT(" (") + FHoudiniGeoPartObject::HoudiniPartTypeToString(HGPO.Type) + TEXT(")"); + + if (HGPO.SplitGroups.Num() > 0) + { + OutputValStr += TEXT("( ") + FString::FromInt(HGPO.SplitGroups.Num()) + TEXT(" splits:"); + for (auto& split : HGPO.SplitGroups) + { + OutputValStr += TEXT(" ") + split; + } + OutputValStr += TEXT(")"); + } + + if (!HGPO.VolumeName.IsEmpty()) + { + OutputValStr += TEXT("( ") + HGPO.VolumeName; + if (HGPO.VolumeTileIndex >= 0) + OutputValStr += TEXT(" tile ") + FString::FromInt(HGPO.VolumeTileIndex); + OutputValStr += TEXT(" )"); + } + + OutputValStr += TEXT("\n"); + } + + // Add output objects if any + TMap AllOutputObj = InOutput->GetOutputObjects(); + if (AllOutputObj.Num() > 0) + { + bool TitleAdded = false; + for (const auto& Iter : AllOutputObj) + { + UObject* OutObject = Iter.Value.OutputObject; + if (OutObject) + { + OutputValStr += OutObject->GetFullName() + TEXT(" (obj)\n"); + } + + for(auto Component : Iter.Value.OutputComponents) + { + UObject* OutComp = Component; + if (OutComp) + { + OutputValStr += OutComp->GetFullName() + TEXT(" (comp)\n"); + } + } + } + } + + return FText::FromString(OutputValStr); +} + +FText +FHoudiniOutputDetails::GetOutputTooltip(const TWeakObjectPtr& InOutput) +{ + // TODO + return FText(); +} + + +const FSlateBrush * +FHoudiniOutputDetails::GetThumbnailBorder(const TWeakObjectPtr Mesh) const +{ + TSharedPtr ThumbnailBorder = OutputObjectThumbnailBorders[Mesh]; + if (ThumbnailBorder.IsValid() && ThumbnailBorder->IsHovered()) + return _GetEditorStyle().GetBrush("PropertyEditor.AssetThumbnailLight"); + else + return _GetEditorStyle().GetBrush("PropertyEditor.AssetThumbnailShadow"); +} + + +const FSlateBrush * +FHoudiniOutputDetails::GetMaterialInterfaceThumbnailBorder(const TWeakObjectPtr OutputObject, int32 MaterialIdx) const +{ + if (!IsValidWeakPointer(OutputObject)) + return nullptr; + + TPairInitializer&, int32> Pair(OutputObject, MaterialIdx); + TSharedPtr ThumbnailBorder = MaterialInterfaceThumbnailBorders[Pair]; + + if (ThumbnailBorder.IsValid() && ThumbnailBorder->IsHovered()) + return _GetEditorStyle().GetBrush("PropertyEditor.AssetThumbnailLight"); + else + return _GetEditorStyle().GetBrush("PropertyEditor.AssetThumbnailShadow"); +} + +/* +const FSlateBrush * +FHoudiniOutputDetails::GetMaterialInterfaceThumbnailBorder(ALandscapeProxy * Landscape, int32 MaterialIdx) const +{ + if (!Landscape) + return nullptr; + + TPairInitializer< ALandscapeProxy *, int32 > Pair(Landscape, MaterialIdx); + TSharedPtr< SBorder > ThumbnailBorder = LandscapeMaterialInterfaceThumbnailBorders[Pair]; + + if (ThumbnailBorder.IsValid() && ThumbnailBorder->IsHovered()) + return FEditorStyle::GetBrush("PropertyEditor.AssetThumbnailLight"); + else + return FEditorStyle::GetBrush("PropertyEditor.AssetThumbnailShadow"); +} +*/ + +FReply +FHoudiniOutputDetails::OnThumbnailDoubleClick( + const FGeometry & InMyGeometry, + const FPointerEvent & InMouseEvent, const TWeakObjectPtr Object) +{ + if (Object.IsValid() && GEditor) + GEditor->EditObject(Object.Get()); + + return FReply::Handled(); +} + +bool +FHoudiniOutputDetails::OnMaterialInterfaceDraggedOver(TArrayView InAssets) const +{ + UObject* InObject = InAssets[0].GetAsset(); + return (InObject && InObject->IsA(UMaterialInterface::StaticClass())); +} + + +FReply +FHoudiniOutputDetails::OnResetMaterialInterfaceClicked( + const TWeakObjectPtr StaticMesh, + const TWeakObjectPtr HoudiniOutput, + int32 MaterialIdx) +{ + FReply RetValue = FReply::Handled(); + if (!StaticMesh.IsValid() || !HoudiniOutput.IsValid()) + return RetValue; + + TArray& StaticMaterials = StaticMesh->GetStaticMaterials(); + + if (!StaticMaterials.IsValidIndex(MaterialIdx)) + return RetValue; + + // Retrieve material interface which is being replaced. + UMaterialInterface * MaterialInterface = StaticMaterials[MaterialIdx].MaterialInterface; + if (!MaterialInterface) + return RetValue; + + // Find the string corresponding to the material that is being replaced + const FHoudiniMaterialIdentifier* MaterialIdentifierPtr = HoudiniOutput->GetReplacementMaterials().FindKey(MaterialInterface); + if (!MaterialIdentifierPtr ) + { + // This material was not replaced, no need to reset it + return RetValue; + } + + // This material has been replaced previously. + const FHoudiniMaterialIdentifier MaterialIdentifier = *MaterialIdentifierPtr; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniMaterialReplacement", "Houdini Material Reset"), HoudiniOutput.Get()); + + // Remove the replacement + HoudiniOutput->Modify(); + HoudiniOutput->GetReplacementMaterials().Remove(MaterialIdentifier); + + bool bViewportNeedsUpdate = true; + + // Try to find the original assignment, if not, we'll use the default material + UMaterialInterface * AssignMaterial = FHoudiniEngine::Get().GetHoudiniDefaultMaterial().Get(); + TObjectPtr * FoundMat = HoudiniOutput->GetAssignementMaterials().Find(MaterialIdentifier); + if (FoundMat && (*FoundMat)) + AssignMaterial = *FoundMat; + + // Replace material on static mesh. + StaticMesh->Modify(); + StaticMaterials[MaterialIdx].MaterialInterface = AssignMaterial; + + // Replace the material on any component (SMC/ISMC) that uses the above SM + // TODO: ?? Replace for all? + for (auto& OutputObject : HoudiniOutput->GetOutputObjects()) + { + for(auto Component : OutputObject.Value.OutputComponents) + { + // Only look at MeshComponents + UStaticMeshComponent * SMC = Cast(Component); + if (!SMC) + continue; + + if (SMC->GetStaticMesh() != StaticMesh.Get()) + continue; + + SMC->Modify(); + SMC->SetMaterial(MaterialIdx, AssignMaterial); + } + } + + FHoudiniEngineUtils::UpdateEditorProperties(true); + + if (GEditor) + GEditor->RedrawAllViewports(); + + return RetValue; +} + +FReply +FHoudiniOutputDetails::OnResetMaterialInterfaceClicked( + const TWeakObjectPtr InLandscape, + const TWeakObjectPtr InHoudiniOutput, + int32 InMaterialIdx) +{ + FReply RetValue = FReply::Handled(); + if (!InLandscape.IsValid() || !InHoudiniOutput.IsValid()) + return RetValue; + + // Retrieve the material interface which is being replaced. + UMaterialInterface * MaterialInterface = InMaterialIdx == 0 ? InLandscape->GetLandscapeMaterial() : InLandscape->GetLandscapeHoleMaterial(); + UMaterialInterface * MaterialInterfaceReplacement = Cast(FHoudiniEngine::Get().GetHoudiniDefaultMaterial().Get()); + + // Find the string corresponding to the material that is being replaced + const FHoudiniMaterialIdentifier* MaterialIdentifierPtr = InHoudiniOutput->GetReplacementMaterials().FindKey(MaterialInterface); + if (!MaterialIdentifierPtr) + { + // This material was not replaced, no need to reset it + return RetValue; + } + + // This material has been replaced previously. + const FHoudiniMaterialIdentifier MaterialIdentifier = *MaterialIdentifierPtr; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniMaterialReplacement", "Houdini Material Reset"), InHoudiniOutput.Get()); + + // Remove the replacement + InHoudiniOutput->Modify(); + InHoudiniOutput->GetReplacementMaterials().Remove(MaterialIdentifier); + + bool bViewportNeedsUpdate = true; + + // Try to find the original assignment, if not, we'll use the default material + UMaterialInterface * AssignMaterial = FHoudiniEngine::Get().GetHoudiniDefaultMaterial().Get(); + TObjectPtr * FoundMat = InHoudiniOutput->GetAssignementMaterials().Find(MaterialIdentifier); + if (FoundMat && (*FoundMat)) + AssignMaterial = *FoundMat; + + // Replace material on Landscape + InLandscape->Modify(); + if (InMaterialIdx == 0) + InLandscape->LandscapeMaterial = AssignMaterial; + else + InLandscape->LandscapeHoleMaterial = AssignMaterial; + + InLandscape->UpdateAllComponentMaterialInstances(); + + /* + // As UpdateAllComponentMaterialInstances() is not accessible to us, we'll try to access the Material's UProperty + // to trigger a fake Property change event that will call the Update function... + UProperty* FoundProperty = FindField< UProperty >(Landscape->GetClass(), (MaterialIdx == 0) ? TEXT("LandscapeMaterial") : TEXT("LandscapeHoleMaterial")); + if (FoundProperty) + { + FPropertyChangedEvent PropChanged(FoundProperty, EPropertyChangeType::ValueSet); + Landscape->PostEditChangeProperty(PropChanged); + } + else + { + // The only way to update the material for now is to recook/recreate the landscape... + HoudiniAssetComponent->StartTaskAssetCookingManual(); + } + */ + + FHoudiniEngineUtils::UpdateEditorProperties(true); + + if (GEditor) + GEditor->RedrawAllViewports(); + + return RetValue; +} + +void +FHoudiniOutputDetails::OnBrowseTo(const TWeakObjectPtr InObject) +{ + if (!IsValidWeakPointer(InObject)) + return; + + if (GEditor) + { + TArray Objects; + Objects.Add(InObject.Get()); + GEditor->SyncBrowserToObjects(Objects); + } +} + +TSharedRef +FHoudiniOutputDetails::OnGetMaterialInterfaceMenuContent( + const TWeakObjectPtr MaterialInterface, + const TWeakObjectPtr OutputObject, + const TWeakObjectPtr InOutput, + int32 MaterialIdx) +{ + TArray AllowedClasses; + AllowedClasses.Add(UMaterialInterface::StaticClass()); + + TArray NewAssetFactories; + + return PropertyCustomizationHelpers::MakeAssetPickerWithMenu( + FAssetData(MaterialInterface.Get()), + true, + AllowedClasses, + NewAssetFactories, + OnShouldFilterMaterialInterface, + FOnAssetSelected::CreateSP( + this, &FHoudiniOutputDetails::OnMaterialInterfaceSelected, OutputObject, InOutput, MaterialIdx), + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::CloseMaterialInterfaceComboButton)); +} + + +void +FHoudiniOutputDetails::CloseMaterialInterfaceComboButton() +{ + +} + +void +FHoudiniOutputDetails::OnMaterialInterfaceDropped( + const FDragDropEvent& InDragDropEvent, + TArrayView InAssets, + const TWeakObjectPtr StaticMesh, + const TWeakObjectPtr HoudiniOutput, + int32 MaterialIdx) +{ + UObject* InObject = InAssets[0].GetAsset(); + if (!InObject) + return; + + return OnMaterialInterfaceDropped(InObject, StaticMesh, HoudiniOutput, MaterialIdx); +} + +void +FHoudiniOutputDetails::OnMaterialInterfaceDropped( + const TWeakObjectPtr InObject, + const TWeakObjectPtr StaticMesh, + const TWeakObjectPtr HoudiniOutput, + int32 MaterialIdx) +{ + if (!InObject.IsValid() || !HoudiniOutput.IsValid()) + return; + + UMaterialInterface * MaterialInterface = Cast(InObject); + if (!IsValid(MaterialInterface)) + return; + + if (!IsValidWeakPointer(StaticMesh)) + return; + + TArray& StaticMaterials = StaticMesh->GetStaticMaterials(); + if (!StaticMaterials.IsValidIndex(MaterialIdx)) + return; + + bool bViewportNeedsUpdate = false; + + // Retrieve material interface which is being replaced. + UMaterialInterface * OldMaterialInterface = StaticMaterials[MaterialIdx].MaterialInterface; + if (OldMaterialInterface == MaterialInterface) + return; + + // Find the string corresponding to the material that is being replaced + FHoudiniMaterialIdentifier MaterialIdentifier; + const FHoudiniMaterialIdentifier* MaterialIdentifierPtr = HoudiniOutput->GetReplacementMaterials().FindKey(OldMaterialInterface); + if (MaterialIdentifierPtr) + { + // This material has been replaced previously. + MaterialIdentifier = *MaterialIdentifierPtr; + } + else + { + // We have no previous replacement for this material, + // see if we can find it the material assignment list. + MaterialIdentifierPtr = HoudiniOutput->GetAssignementMaterials().FindKey(OldMaterialInterface); + if (MaterialIdentifierPtr) + { + // This material has been assigned previously. + MaterialIdentifier = *MaterialIdentifierPtr; + } + else + { + UMaterialInterface * DefaultMaterial = FHoudiniEngine::Get().GetHoudiniDefaultMaterial().Get(); + if (OldMaterialInterface == DefaultMaterial) + { + // This is replacement for default material. + MaterialIdentifier = FHoudiniMaterialIdentifier(HAPI_UNREAL_DEFAULT_MATERIAL_NAME, false, ""); + } + else + { + // External Material? + // TODO: can we somehow map the material instance parameters here? + if (IsValid(OldMaterialInterface)) + { + MaterialIdentifier = FHoudiniMaterialIdentifier(OldMaterialInterface->GetPathName(), false, ""); + } + } + } + } + + if (!MaterialIdentifier.IsValid()) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniMaterialReplacement", "Houdini Material Replacement"), HoudiniOutput.Get()); + + // Add a new material replacement entry. + HoudiniOutput->Modify(); + HoudiniOutput->GetReplacementMaterials().Add(MaterialIdentifier, MaterialInterface); + + // Replace material on static mesh. + StaticMesh->Modify(); + StaticMaterials[MaterialIdx].MaterialInterface = MaterialInterface; + + // Replace the material on any component (SMC/ISMC) that uses the above SM + for (auto& OutputObject : HoudiniOutput->GetOutputObjects()) + { + for(auto Component : OutputObject.Value.OutputComponents) + { + // Only look at MeshComponents + UStaticMeshComponent * SMC = Cast(Component); + if (IsValid(SMC)) + { + if (SMC->GetStaticMesh() == StaticMesh.Get()) + { + SMC->Modify(); + SMC->SetMaterial(MaterialIdx, MaterialInterface); + } + } + else + { + UStaticMesh* SM = Cast(OutputObject.Value.OutputObject); + if (IsValid(SM)) + { + SM->Modify(); + SM->SetMaterial(MaterialIdx, MaterialInterface); + } + } + } + } + + FHoudiniEngineUtils::UpdateEditorProperties(true); + + /* + if(GUnrealEd) + GUnrealEd->UpdateFloatingPropertyWindows(); +*/ + if (GEditor) + GEditor->RedrawAllViewports(); +} + +// Delegate used when a valid material has been drag and dropped on a landscape. +void +FHoudiniOutputDetails::OnMaterialInterfaceDropped( + const FDragDropEvent& InDragDropEvent, + TArrayView InAssets, + const TWeakObjectPtr InLandscape, + const TWeakObjectPtr InOutput, + int32 MaterialIdx) +{ + UObject* InDroppedObject = InAssets[0].GetAsset(); + if (!InDroppedObject) + return; + + return OnMaterialInterfaceDropped(InDroppedObject, InLandscape, InOutput, MaterialIdx); +} + +// Delegate used when a valid material has been drag and dropped on a landscape. +void +FHoudiniOutputDetails::OnMaterialInterfaceDropped( + UObject* InDroppedObject, + const TWeakObjectPtr InLandscape, + const TWeakObjectPtr InOutput, + int32 MaterialIdx) +{ + UMaterialInterface * MaterialInterface = Cast< UMaterialInterface >(InDroppedObject); + if (!IsValid(MaterialInterface)) + return; + + if (!InLandscape.IsValid() || !InOutput.IsValid()) + return; + + bool bViewportNeedsUpdate = false; + + // Retrieve the material interface which is being replaced. + UMaterialInterface * OldMaterialInterface = MaterialIdx == 0 ? InLandscape->GetLandscapeMaterial() : InLandscape->GetLandscapeHoleMaterial(); + if (OldMaterialInterface == MaterialInterface) + return; + + // Find the string corresponding to the material that is being replaced + FHoudiniMaterialIdentifier MaterialIdentifier; + const FHoudiniMaterialIdentifier* MaterialIdentifierPtr = InOutput->GetReplacementMaterials().FindKey(OldMaterialInterface); + if (MaterialIdentifierPtr) + { + // This material has been replaced previously. + MaterialIdentifier = *MaterialIdentifierPtr; + } + else + { + // We have no previous replacement for this material, + // see if we can find it the material assignment list. + MaterialIdentifierPtr = InOutput->GetAssignementMaterials().FindKey(OldMaterialInterface); + if (MaterialIdentifierPtr) + { + // This material has been assigned previously. + MaterialIdentifier = *MaterialIdentifierPtr; + } + else + { + UMaterialInterface * DefaultMaterial = FHoudiniEngine::Get().GetHoudiniDefaultMaterial().Get(); + if (OldMaterialInterface == DefaultMaterial) + { + // This is replacement for default material. + MaterialIdentifier = FHoudiniMaterialIdentifier(HAPI_UNREAL_DEFAULT_MATERIAL_NAME, false, ""); + } + else + { + // External Material? + if (IsValid(OldMaterialInterface)) + { + MaterialIdentifier = FHoudiniMaterialIdentifier( + OldMaterialInterface->GetPathName(), false, ""); + } + } + } + } + + if (!MaterialIdentifier.IsValid()) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniMaterialReplacement", "Houdini Material Replacement"), InOutput.Get()); + + // Add a new material replacement entry. + InOutput->Modify(); + InOutput->GetReplacementMaterials().Add(MaterialIdentifier, MaterialInterface); + + // Replace material on the landscape + InLandscape->Modify(); + + if (MaterialIdx == 0) + InLandscape->LandscapeMaterial = MaterialInterface; + else + InLandscape->LandscapeHoleMaterial = MaterialInterface; + + // Update the landscape components Material instances + InLandscape->UpdateAllComponentMaterialInstances(); + + /* + // As UpdateAllComponentMaterialInstances() is not accessible to us, we'll try to access the Material's UProperty + // to trigger a fake Property change event that will call the Update function... + UProperty* FoundProperty = FindField< UProperty >(InLandscape->GetClass(), (MaterialIdx == 0) ? TEXT("LandscapeMaterial") : TEXT("LandscapeHoleMaterial")); + if (FoundProperty) + { + FPropertyChangedEvent PropChanged(FoundProperty, EPropertyChangeType::ValueSet); + InLandscape->PostEditChangeProperty(PropChanged); + } + else + { + // The only way to update the material for now is to recook/recreate the landscape... + HoudiniAssetComponent->StartTaskAssetCookingManual(); + } + */ + + FHoudiniEngineUtils::UpdateEditorProperties(true); + + if (GEditor) + GEditor->RedrawAllViewports(); +} + +void +FHoudiniOutputDetails::OnMaterialInterfaceSelected( + const FAssetData & AssetData, + const TWeakObjectPtr OutputObject, + const TWeakObjectPtr InOutput, + int32 MaterialIdx) +{ + TPairInitializer&, int32> Pair(OutputObject, MaterialIdx); + TSharedPtr AssetComboButton = MaterialInterfaceComboButtons[Pair]; + if (AssetComboButton.IsValid()) + { + AssetComboButton->SetIsOpen(false); + + UObject * Object = AssetData.GetAsset(); + + UStaticMesh* SM = Cast(OutputObject); + if (IsValid(SM)) + { + return OnMaterialInterfaceDropped(Object, SM, InOutput, MaterialIdx); + } + + ALandscapeProxy* Landscape = Cast(OutputObject); + if (IsValid(Landscape)) + { + return OnMaterialInterfaceDropped(Object, Landscape, InOutput, MaterialIdx); + } + } +} + +void +FHoudiniOutputDetails::OnUseContentBrowserSelectedMaterialInterface( + const TWeakObjectPtr OutputObject, + const TWeakObjectPtr InOutput, + int32 MaterialIdx) +{ + if (!IsValidWeakPointer(OutputObject)) + return; + + if (!IsValidWeakPointer(InOutput)) + return; + + if (GEditor) + { + TArray CBSelections; + GEditor->GetContentBrowserSelections(CBSelections); + + // Get the first selected material object + UObject* Object = nullptr; + for (auto & CurAssetData : CBSelections) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + if (CurAssetData.AssetClassPath != UMaterial::StaticClass()->GetClassPathName() && + CurAssetData.AssetClassPath != UMaterialInstance::StaticClass()->GetClassPathName() && + CurAssetData.AssetClassPath != UMaterialInstanceConstant::StaticClass()->GetClassPathName()) +#else + if (CurAssetData.AssetClass != UMaterial::StaticClass()->GetFName() && + CurAssetData.AssetClass != UMaterialInstance::StaticClass()->GetFName() && + CurAssetData.AssetClass != UMaterialInstanceConstant::StaticClass()->GetFName()) +#endif + continue; + + Object = CurAssetData.GetAsset(); + break; + } + + if (IsValid(Object)) + { + UStaticMesh* SM = Cast(OutputObject); + if (IsValid(SM)) + { + return OnMaterialInterfaceDropped(Object, SM, InOutput, MaterialIdx); + } + + ALandscapeProxy* Landscape = Cast(OutputObject); + if (IsValid(Landscape)) + { + return OnMaterialInterfaceDropped(Object, Landscape, InOutput, MaterialIdx); + } + } + } +} + +FHoudiniInstancedOutput* GetInstancedOutput(const TWeakObjectPtr& InOutput, const FHoudiniOutputObjectIdentifier& InIdentifier) +{ + if (!IsValidWeakPointer(InOutput)) + return nullptr; + + return InOutput->GetInstancedOutputs().Find(InIdentifier); +} + +void +FHoudiniOutputDetails::CreateInstancerOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + // Do not display instancer UI for one-instance instancers + bool OnlyOneInstanceInstancers = true; + for (auto& Iter : InOutput->GetInstancedOutputs()) + { + FHoudiniInstancedOutput& CurInstanceOutput = (Iter.Value); + if (CurInstanceOutput.NumInstances <= 1) + continue; + + OnlyOneInstanceInstancers = false; + break; + } + + // This output only has one-instance instancers (SMC), no need to display the instancer UI. + if (OnlyOneInstanceInstancers) + return; + + IDetailLayoutBuilder & DetailLayoutBuilder = HouOutputCategory.GetParentLayout(); + TSharedPtr AssetThumbnailPool = DetailLayoutBuilder.GetThumbnailPool(); + + // Get this output's OutputObject + const TMap& OutputObjects = InOutput->GetOutputObjects(); + + // Iterate on all of the output's HGPO + for (const FHoudiniGeoPartObject& CurHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + // Not an instancer, skip + if (CurHGPO.Type != EHoudiniPartType::Instancer) + continue; + + // Get the label for that instancer + FString InstancerLabel = InOutput->GetName() + TEXT(" ") + UHoudiniOutput::OutputTypeToString(InOutput->GetType()); + if (CurHGPO.bHasCustomPartName) + InstancerLabel = CurHGPO.PartName; + + TSharedRef InstancerVerticalBox = SNew(SVerticalBox); + TSharedPtr InstancerHorizontalBox = nullptr; + + // Create a new Group for that instancer + IDetailGroup& InstancerGroup = HouOutputCategory.AddGroup(FName(*InstancerLabel), FText::FromString(InstancerLabel)); + + // Now iterate and display the instance outputs that matches this HGPO + for (auto& Iter : InOutput->GetInstancedOutputs()) + { + FHoudiniOutputObjectIdentifier& CurOutputObjectIdentifier = Iter.Key; + if (!CurOutputObjectIdentifier.Matches(CurHGPO)) + continue; + + FHoudiniInstancedOutput& CurInstanceOutput = (Iter.Value); + + // Dont display instancer UI for one-instance instancers (SMC) + if (CurInstanceOutput.NumInstances <= 1) + continue; + + { + UObject* InstancedObject = CurInstanceOutput.InstancedObject.LoadSynchronous(); + if (!IsValid(InstancedObject)) + { + HOUDINI_LOG_WARNING(TEXT("Null Object found for instance ")); + continue; + } + + // Create thumbnail for this object. + TSharedPtr Thumbnail = + MakeShareable(new FAssetThumbnail(InstancedObject, 64, 64, AssetThumbnailPool)); + TSharedRef PickerVerticalBox = SNew(SVerticalBox); + TSharedPtr PickerHorizontalBox = nullptr; + TSharedPtr ThumbnailBorder; + + // For the variation name, reuse the instancer label and append the variation index if we have more than one variation + FString InstanceOutputLabel = FString::Printf(TEXT("%d Instances"), CurInstanceOutput.NumInstances); + + IDetailGroup* DetailGroup = &InstancerGroup; + + // See if we can find the corresponding component to get its type + FString InstancerType = TEXT("(Instancer)"); + const FHoudiniOutputObject* OutputObject = OutputObjects.Find(CurOutputObjectIdentifier); + if (OutputObject && OutputObject->OutputComponents.Num() > 0) + InstancerType = FHoudiniInstanceTranslator::GetInstancerTypeFromComponent(OutputObject->OutputComponents[0]); + + DetailGroup->AddWidgetRow() + .NameContent() + [ + //SNew(SSpacer) + SNew(STextBlock) + .TextStyle(_GetEditorStyle(), "PropertyEditor.AssetClass") + .Font(_GetEditorStyle().GetFontStyle(FName(TEXT("PropertyWindow.NormalFont")))) + .Text(FText::FromString(InstanceOutputLabel)) + + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + PickerVerticalBox + ]; + + PickerVerticalBox->AddSlot() + .Padding(0, 2) + .AutoHeight() + [ + SAssignNew(PickerHorizontalBox, SHorizontalBox) + ]; + + PickerHorizontalBox->AddSlot().Padding(0.0f, 0.0f, 2.0f, 0.0f).AutoWidth() + [ + SAssignNew(ThumbnailBorder, SBorder) + .Padding(5.0f) + .OnMouseDoubleClick(this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, TWeakObjectPtr(InstancedObject)) + [ + SNew(SBox) + .WidthOverride(64) + .HeightOverride(64) + .ToolTipText(FText::FromString(InstancedObject->GetPathName())) + [ + Thumbnail->MakeThumbnailWidget() + ] + ] + ]; + + // Create tooltip. + FFormatNamedArguments Args; + Args.Add(TEXT("Asset"), FText::FromString(InstancedObject->GetName())); + FText Tooltip = + FText::Format(LOCTEXT("BrowseToSpecificAssetInContentBrowser", "Browse to '{Asset}' in Content Browser"), Args); + + PickerHorizontalBox->AddSlot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateLambda([InOutput, CurOutputObjectIdentifier]() + { + FHoudiniInstancedOutput* InstanceOutput = GetInstancedOutput(InOutput, CurOutputObjectIdentifier); + if (!InstanceOutput) + return; + + UObject* InputObject = InstanceOutput->InstancedObject.LoadSynchronous(); + + if (GEditor && InputObject) + { + TArray Objects; + Objects.Add(InputObject); + GEditor->SyncBrowserToObjects(Objects); + } + }), + TAttribute< FText >(Tooltip)) + ]; + + } + } + } +} + +/* +void +FHoudiniOutputDetails::OnMaterialInterfaceSelected( + const FAssetData & AssetData, + ALandscapeProxy* Landscape, + UHoudiniOutput * InOutput, + int32 MaterialIdx) +{ + TPairInitializer< ALandscapeProxy *, int32 > Pair(Landscape, MaterialIdx); + TSharedPtr< SComboButton > AssetComboButton = LandscapeMaterialInterfaceComboButtons[Pair]; + if (AssetComboButton.IsValid()) + { + AssetComboButton->SetIsOpen(false); + + UObject * Object = AssetData.GetAsset(); + OnMaterialInterfaceDropped(Object, Landscape, InOutput, MaterialIdx); + } +} +*/ + +void +FHoudiniOutputDetails::CreateDefaultOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + // Get thumbnail pool for this builder. + TSharedPtr< FAssetThumbnailPool > AssetThumbnailPool = HouOutputCategory.GetParentLayout().GetThumbnailPool(); + + // Default widget displaying name/output type + FString OutputNameStr = InOutput->GetName(); + FText OutputTooltip = GetOutputTooltip(InOutput); + + // Create a new detail row + // Name + FText OutputNameTxt = GetOutputDebugName(InOutput); + FString OutputRowString = TEXT("Output ") + OutputNameStr; + FDetailWidgetRow & Row = HouOutputCategory.AddCustomRow(FText::FromString(OutputRowString)); + Row.NameWidget.Widget = + SNew(STextBlock) + .Text(OutputNameTxt) + .ToolTipText(OutputTooltip) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + + // Value + FText OutputTypeTxt = GetOutputDebugDescription(InOutput); + Row.ValueWidget.Widget = + SNew(STextBlock) + .Text(OutputTypeTxt) + .ToolTipText(OutputTooltip) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + + Row.ValueWidget.MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH); +} + +void +FHoudiniOutputDetails::OnBakeOutputObject( + const FString& InBakeName, + UObject * BakedOutputObject, + const FHoudiniOutputObjectIdentifier & OutputIdentifier, + const FHoudiniOutputObject& InOutputObject, + const FHoudiniGeoPartObject & HGPO, + const UObject* OutputOwner, + UHoudiniOutput* InOutput, + const FString & BakeFolder, + const FHoudiniBakeSettings& BakeSettings, + const FString & TempCookFolder, + const EHoudiniLandscapeOutputBakeType & LandscapeBakeType, + const TArray& InAllOutputs) +{ + if (!IsValid(BakedOutputObject) || !IsValid(InOutput)) + return; + + // Fill in the package params + FHoudiniPackageParams PackageParams; + // Configure FHoudiniAttributeResolver and fill the package params with resolved object name and bake folder. + // The resolver is then also configured with the package params for subsequent resolving (level_path etc) + FHoudiniAttributeResolver Resolver; + // Determine the relevant WorldContext based on the output owner + UWorld* WorldContext = OutputOwner ? OutputOwner->GetWorld() : GWorld; + + UHoudiniCookable* const HC = FHoudiniEngineUtils::GetOuterHoudiniCookable(OutputOwner); + check(IsValid(HC)); + + TArray& AllBakedOutputs = HC->GetBakedOutputs(); + + // Check if we have previously baked this object on this Cookable + bool bHasPreviousBakeData = false; + FHoudiniBakedOutputObject BakedObjectEntry; + const int32 OutputIndex = InAllOutputs.IndexOfByKey(InOutput); + if (OutputIndex >= 0 && AllBakedOutputs.IsValidIndex(OutputIndex)) + { + FHoudiniBakedOutputObject const* const PrevBakedOutputObject = AllBakedOutputs[OutputIndex].BakedOutputObjects.Find(OutputIdentifier); + if (PrevBakedOutputObject) + { + bHasPreviousBakeData = true; + BakedObjectEntry = *PrevBakedOutputObject; + } + } + + const FString HoudiniAssetName = HC->GetHoudiniAssetName(); + const FString HoudiniAssetActorName = IsValid(HC->GetOwner()) ? HC->GetOwner()->GetActorNameOrLabel() : TEXT(""); + const bool bAutomaticallySetAttemptToLoadMissingPackages = true; + const bool bSkipObjectNameResolutionAndUseDefault = !InBakeName.IsEmpty(); // If InBakeName is set use it as is for the object name + const bool bSkipBakeFolderResolutionAndUseDefault = false; + + /* + FString DefaultObjectName; + if (InBakeName.IsEmpty()) + { + // First get names from previous bake entry + if (bHasPreviousBakeData) + { + UObject const* const PrevObject = BakedObjectEntry.GetBakedObjectIfValid(); + if (PrevObject) + { + DefaultObjectName = PrevObject->GetName(); + } + else + { + AActor const* const PrevActor = BakedObjectEntry.GetActorIfValid(); + if (PrevActor) + DefaultObjectName = PrevActor->GetName(); + } + } + + // fallback to name from BakedOutputObject + if (DefaultObjectName.IsEmpty()) + DefaultObjectName = BakedOutputObject->GetName(); + } + else + { + DefaultObjectName = InBakeName; + } + */ + + FHoudiniEngineUtils::FillInPackageParamsForBakingOutputWithResolver( + WorldContext, HC, OutputIdentifier, InOutputObject, bHasPreviousBakeData, BakedOutputObject->GetName(), //DefaultObjectName, + PackageParams, Resolver, BakeFolder, EPackageReplaceMode::ReplaceExistingAssets, + HoudiniAssetName, HoudiniAssetActorName, + bAutomaticallySetAttemptToLoadMissingPackages, bSkipObjectNameResolutionAndUseDefault, + bSkipBakeFolderResolutionAndUseDefault); + + FHoudiniBakedObjectData NewBakeOutput; + const EHoudiniOutputType Type = InOutput->GetType(); + switch (Type) + { + case EHoudiniOutputType::Mesh: + { + UStaticMesh* StaticMesh = Cast(BakedOutputObject); + if (StaticMesh) + { + FDirectoryPath TempCookFolderPath; + TempCookFolderPath.Path = TempCookFolder; + TMap AlreadyBakedStaticMeshMap; + TMap AlreadyBakedMaterialsMap; + UStaticMesh* DuplicatedMesh = FHoudiniEngineBakeUtils::BakeStaticMesh( + StaticMesh, PackageParams, InAllOutputs, TempCookFolderPath, AlreadyBakedStaticMeshMap, AlreadyBakedMaterialsMap); + + BakedObjectEntry.Actor.Empty(); + BakedObjectEntry.BakedComponent.Empty(); + BakedObjectEntry.BakedObject = FSoftObjectPath(DuplicatedMesh).ToString(); + } + } + break; + + case EHoudiniOutputType::Curve: + { + USplineComponent* SplineComponent = Cast(BakedOutputObject); + if (SplineComponent) + { + AActor* BakedActor; + USplineComponent* BakedSplineComponent; + FHoudiniEngineBakeUtils::BakeCurve( + HC, SplineComponent, GWorld->GetCurrentLevel(), PackageParams, BakeSettings, FName(PackageParams.ObjectName), BakedActor, BakedSplineComponent, NewBakeOutput); + + BakedObjectEntry.Actor = FSoftObjectPath(BakedActor).ToString(); + BakedObjectEntry.BakedComponent = FSoftObjectPath(BakedSplineComponent).ToString(); + BakedObjectEntry.BakedObject.Empty(); + } + } + break; + + case EHoudiniOutputType::Landscape: + { + ALandscapeProxy* Landscape = Cast(BakedOutputObject); + if (Landscape) + { + FHoudiniEngineBakeUtils::BakeHeightfield(Landscape, PackageParams, LandscapeBakeType, NewBakeOutput); + BakedObjectEntry.Actor.Empty(); + BakedObjectEntry.BakedComponent.Empty(); + BakedObjectEntry.BakedObject.Empty(); + } + } + break; + + case EHoudiniOutputType::Cop: + { + UTexture2D* Texture = Cast(BakedOutputObject); + if (Texture) + { + UTexture2D* BakedTexture = FHoudiniEngineBakeUtils::BakeTextureToPackage(Texture, PackageParams, nullptr); + + BakedObjectEntry.Actor.Empty(); + BakedObjectEntry.BakedComponent.Empty(); + BakedObjectEntry.BakedObject = FSoftObjectPath(BakedTexture).ToString(); + } + } + break; + } + + if (OutputIndex >= 0) + { + if (!AllBakedOutputs.IsValidIndex(OutputIndex)) + { + AllBakedOutputs.SetNum(OutputIndex + 1); + } + AllBakedOutputs[OutputIndex].BakedOutputObjects.Emplace(OutputIdentifier, BakedObjectEntry); + } + + { + const FString FinishedTemplate = TEXT("Baking finished. Created {0} packages. Updated {1} packages."); + FString Msg = FString::Format(*FinishedTemplate, { NewBakeOutput.BakeStats.NumPackagesCreated, NewBakeOutput.BakeStats.NumPackagesUpdated } ); + FHoudiniEngine::Get().FinishTaskSlateNotification( FText::FromString(Msg) ); + } +} + +FReply +FHoudiniOutputDetails::OnRefineClicked( + const TWeakObjectPtr ObjectToRefine, + const TWeakObjectPtr InOutput) +{ + // TODO: Actually refine only the selected ProxyMesh + // For now, refine all the selection + FHoudiniEngineCommands::RefineHoudiniProxyMeshesToStaticMeshes(true, true); + + return FReply::Handled(); +} + +void +FHoudiniOutputDetails::OnBakeNameCommitted( + const FText& Val, ETextCommit::Type TextCommitType, + const TWeakObjectPtr& InOutput, const FHoudiniOutputObjectIdentifier& InIdentifier) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + TMap& OutputObjects = InOutput->GetOutputObjects(); + FHoudiniOutputObject* FoundOutputObject = OutputObjects.Find(InIdentifier); + + if (!FoundOutputObject) + return; + + FoundOutputObject->BakeName = Val.ToString(); +} + +void +FHoudiniOutputDetails::OnRevertBakeNameToDefault(const TWeakObjectPtr& InOutput, const FHoudiniOutputObjectIdentifier & InIdentifier) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + TMap& OutputObjects = InOutput->GetOutputObjects(); + FHoudiniOutputObject* FoundOutputObject = OutputObjects.Find(InIdentifier); + + if (!FoundOutputObject) + return; + + FoundOutputObject->BakeName = FString(); +} + + +void +FHoudiniOutputDetails::CreateSkeletalMeshAndMaterialWidgets( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const TWeakObjectPtr& SkelMesh, + const TWeakObjectPtr& Skeleton, + const TWeakObjectPtr& PhysicsAsset, + FHoudiniOutputObjectIdentifier& OutputIdentifier, + FHoudiniGeoPartObject& HoudiniGeoPartObject) +{ + if (!IsValidWeakPointer(SkelMesh) && !IsValidWeakPointer(Skeleton) && !IsValidWeakPointer(PhysicsAsset)) + return; + + const TWeakObjectPtr& HAC = Cast(InOutput->GetOuter()); + const TWeakObjectPtr& HC = Cast(InOutput->GetOuter()); + if (!IsValidWeakPointer(HAC) && !IsValidWeakPointer(HC)) + return; + + FHoudiniOutputObject* FoundOutputObject = InOutput->GetOutputObjects().Find(OutputIdentifier); + FString BakeName = FoundOutputObject ? FoundOutputObject->BakeName : FString(); + + // Get thumbnail pool for this builder. + IDetailLayoutBuilder& DetailLayoutBuilder = HouOutputCategory.GetParentLayout(); + TSharedPtr AssetThumbnailPool = DetailLayoutBuilder.GetThumbnailPool(); + + // TODO: GetBakingBaseName! + FString SkelLabel; + if (Skeleton.IsValid()) + SkelLabel = Skeleton->GetName(); + + if (SkelMesh.IsValid()) + SkelLabel = SkelMesh->GetName(); + + if (HoudiniGeoPartObject.bHasCustomPartName) + SkelLabel = HoudiniGeoPartObject.PartName; + + // Create thumbnail for this mesh. + TSharedPtr SkeletonThumbnail = MakeShareable(new FAssetThumbnail(Skeleton.Get(), 64, 64, AssetThumbnailPool)); + TSharedPtr SkeletonThumbnailBorder; + + TSharedPtr PhysicsAssetThumbnail = MakeShareable(new FAssetThumbnail(PhysicsAsset.Get(), 64, 64, AssetThumbnailPool)); + TSharedPtr PhysicsAssetThumbnailBorder; + + TSharedRef SkeletonVerticalBox = SNew(SVerticalBox); + TSharedRef PhysicsAssetVerticalBox = SNew(SVerticalBox); + + IDetailGroup& SkeletonGrp = HouOutputCategory.AddGroup(FName(*SkelLabel), FText::FromString(SkelLabel)); + + // Add details on the Skeleton. + + if (IsValid(Skeleton.Get())) + { + FString SkeletonLabel = TEXT("Skeleton"); + + SkeletonGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString(SkeletonLabel)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SkeletonVerticalBox + ]; + + SkeletonVerticalBox->AddSlot() + .Padding(0, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0.0f, 0.0f, 2.0f, 0.0f) + .AutoWidth() + [ + SAssignNew(SkeletonThumbnailBorder, SBorder) + .Padding(5.0f) + .BorderImage(this, &FHoudiniOutputDetails::GetThumbnailBorder, (const TWeakObjectPtr&)Skeleton) + .OnMouseDoubleClick(this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) Skeleton) + [ + SNew(SBox) + .WidthOverride(64) + .HeightOverride(64) + .ToolTipText(FText::FromString(Skeleton->GetPathName())) + [ + SkeletonThumbnail->MakeThumbnailWidget() + ] + ] + ] + + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(0.0f, 4.0f, 4.0f, 4.0f) + .VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&)Skeleton), + TAttribute(LOCTEXT("HoudiniSkelMeshBrowseButton", "Browse to this generated Skeleton in the content browser"))) + ] + ] + ] + ]; + + // Store thumbnail for this mesh. + OutputObjectThumbnailBorders.Add(Skeleton, SkeletonThumbnailBorder); + } + + if (IsValid(PhysicsAsset.Get())) + { + FString PhysicsAssetLabel = TEXT("Physics Asset"); + + SkeletonGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString(PhysicsAssetLabel)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + PhysicsAssetVerticalBox + ]; + + PhysicsAssetVerticalBox->AddSlot() + .Padding(0, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0.0f, 0.0f, 2.0f, 0.0f) + .AutoWidth() + [ + SAssignNew(PhysicsAssetThumbnailBorder, SBorder) + .Padding(5.0f) + .BorderImage(this, &FHoudiniOutputDetails::GetThumbnailBorder, (const TWeakObjectPtr&)PhysicsAsset) + .OnMouseDoubleClick(this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) PhysicsAsset) + [ + SNew(SBox) + .WidthOverride(64) + .HeightOverride(64) + .ToolTipText(FText::FromString(PhysicsAsset->GetPathName())) + [ + PhysicsAssetThumbnail->MakeThumbnailWidget() + ] + ] + ] + + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(0.0f, 4.0f, 4.0f, 4.0f) + .VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&)PhysicsAsset), + TAttribute(LOCTEXT("HoudiniSkelMeshBrowseButton", "Browse to this Physics Asset in the content browser"))) + ] + ] + ] + ]; + + // Store thumbnail for this mesh. + OutputObjectThumbnailBorders.Add(PhysicsAsset, PhysicsAssetThumbnailBorder); + } + + // SKEL MESH + if (!IsValidWeakPointer(SkelMesh)) + return; + + // TODO: GetBakingBaseName! + FString SkelMeshLabel = SkelMesh->GetName(); + if (HoudiniGeoPartObject.bHasCustomPartName) + SkelMeshLabel = HoudiniGeoPartObject.PartName; + + // Create thumbnail for this mesh. + TSharedPtr SkelMeshThumbnail = + MakeShareable(new FAssetThumbnail(SkelMesh.Get(), 64, 64, AssetThumbnailPool)); + TSharedPtr SkelMeshThumbnailBorder; + + TSharedRef SKMVerticalBox = SNew(SVerticalBox); + + /* + SkeletonGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("BakeBaseName", "Bake Name")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .FillWidth(1) + [ + SNew(SEditableTextBox) + .Text(FText::FromString(BakeName)) + .HintText(LOCTEXT("BakeNameHintText", "Input bake name to override default")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .OnTextCommitted_Lambda([OutputIdentifier, InOutput](const FText& Val, ETextCommit::Type TextCommitType) + { + if (!IsValidWeakPointer(InOutput)) + return; + + FHoudiniOutputDetails::OnBakeNameCommitted(Val, TextCommitType, InOutput, OutputIdentifier); + FHoudiniEngineUtils::UpdateEditorProperties(true); + }) + .ToolTipText(LOCTEXT("BakeNameTip", "The base name of the baked asset")) + ] + + + SHorizontalBox::Slot() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertNameOverride", "Revert bake name override")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([InOutput, OutputIdentifier]() + { + FHoudiniOutputDetails::OnRevertBakeNameToDefault(InOutput, OutputIdentifier); + FHoudiniEngineUtils::UpdateEditorProperties(true); + return FReply::Handled(); + }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ]; + */ + + // Add details on the SkelMesh colliders + FString SKMLabel = TEXT("Skeletal Mesh"); + + /* + // Indicate that this mesh is instanced + if (HoudiniGeoPartObject.bIsInstanced) + { + MeshLabel += TEXT("\n(instanced)"); + } + + if (HoudiniGeoPartObject.bIsTemplated) + { + MeshLabel += TEXT("\n(templated)"); + } + */ + SkeletonGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString(SKMLabel)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SKMVerticalBox + ]; + + SKMVerticalBox->AddSlot() + .Padding(0, 2) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0.0f, 0.0f, 2.0f, 0.0f) + .AutoWidth() + [ + SAssignNew(SkelMeshThumbnailBorder, SBorder) + .Padding(5.0f) + .BorderImage(this, &FHoudiniOutputDetails::GetThumbnailBorder, (const TWeakObjectPtr&)SkelMesh) + .OnMouseDoubleClick(this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&) SkelMesh) + [ + SNew(SBox) + .WidthOverride(64) + .HeightOverride(64) + .ToolTipText(FText::FromString(SkelMesh->GetPathName())) + [ + SkelMeshThumbnail->MakeThumbnailWidget() + ] + ] + ] + + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(0.0f, 4.0f, 4.0f, 4.0f) + .VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + [ + SNew(SHorizontalBox) + /* + + SHorizontalBox::Slot() + .MaxWidth(120.0f) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Text(LOCTEXT("BakeOutputMesh", "Bake Output")) + .IsEnabled(true) + .OnClicked_Lambda([BakeName, SkelMesh, OutputIdentifier, InOutput, OwningHAC]() + { + if (!SkelMesh.IsValid() || !InOutput.IsValid()) + return FReply::Handled(); + + FHoudiniOutputObject* const FoundOutputObject = InOutput->GetOutputObjects().Find(OutputIdentifier); + if (!FoundOutputObject) + return FReply::Handled(); + + TArray AllOutputs; + FString TempCookFolder; + FString BakeFolder; + if (OwningHAC.IsValid()) + { + AllOutputs.Reserve(OwningHAC->GetNumOutputs()); + OwningHAC->GetOutputs(AllOutputs); + + TempCookFolder = OwningHAC->GetTemporaryCookFolder().Path; + BakeFolder = OwningHAC->GetBakeFolder().Path; + } + + FHoudiniGeoPartObject HoudiniGeoPartObject; + for (const auto& curHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + if (!OutputIdentifier.Matches(curHGPO)) + continue; + + HoudiniGeoPartObject = curHGPO; + break; + } + + FHoudiniOutputDetails::OnBakeOutputObject( + BakeName, + SkelMesh.Get(), + OutputIdentifier, + *FoundOutputObject, + HoudiniGeoPartObject, + OwningHAC.Get(), + InOutput.Get(), + BakeFolder, + TempCookFolder, + EHoudiniLandscapeOutputBakeType::InValid, + AllOutputs); + + return FReply::Handled(); + }) + .ToolTipText(LOCTEXT("HoudiniSkelMeshBakeButton", "Bake this output object to a Skeletal Mesh in the Bake folder.")) + ] + */ + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&)SkelMesh), + TAttribute(LOCTEXT("HoudiniSkelMeshBrowseButton", "Browse to this generated Skeletal Mesh in the content browser"))) + ] + ] + ] + ]; + + // Store thumbnail for this mesh. + OutputObjectThumbnailBorders.Add(SkelMesh, SkelMeshThumbnailBorder); + + // We need to add material box for each material present in this static mesh. + auto& SkelMeshMaterials = SkelMesh->GetMaterials(); + for (int32 MaterialIdx = 0; MaterialIdx < SkelMeshMaterials.Num(); ++MaterialIdx) + { + UMaterialInterface* MaterialInterface = SkelMeshMaterials[MaterialIdx].MaterialInterface; + TSharedPtr< SBorder > MaterialThumbnailBorder; + TSharedPtr< SHorizontalBox > HorizontalBox = NULL; + + FString MaterialName, MaterialPathName; + if (IsValid(MaterialInterface) + && IsValid(MaterialInterface->GetOuter())) + { + MaterialName = MaterialInterface->GetName(); + MaterialPathName = MaterialInterface->GetPathName(); + } + else + { + MaterialInterface = nullptr; + MaterialName = TEXT("Material (invalid)") + FString::FromInt(MaterialIdx); + MaterialPathName = TEXT("Material (invalid)") + FString::FromInt(MaterialIdx); + } + + // Create thumbnail for this material. + TSharedPtr< FAssetThumbnail > MaterialInterfaceThumbnail = + MakeShareable(new FAssetThumbnail(MaterialInterface, 64, 64, AssetThumbnailPool)); + + SKMVerticalBox->AddSlot() + .Padding(0, 2) + [ + /*SNew(SAssetDropTarget) + .OnAreAssetsAcceptableForDrop(this, false) + .OnAssetsDropped( + this, &FHoudiniOutputDetails::OnMaterialInterfaceDropped, SkelMesh, InOutput, MaterialIdx)*/ + SNew(SBox) + [ + SAssignNew(HorizontalBox, SHorizontalBox) + ] + ]; + + HorizontalBox->AddSlot() + .Padding(0.0f, 0.0f, 2.0f, 0.0f) + .AutoWidth() + [ + SAssignNew(MaterialThumbnailBorder, SBorder) + .Padding(5.0f) + .BorderImage( + this, &FHoudiniOutputDetails::GetMaterialInterfaceThumbnailBorder, (const TWeakObjectPtr&)SkelMesh, MaterialIdx) + .OnMouseDoubleClick( + this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&)MaterialInterface) + [ + SNew(SBox) + .WidthOverride(64) + .HeightOverride(64) + .ToolTipText(FText::FromString(MaterialPathName)) + [ + MaterialInterfaceThumbnail->MakeThumbnailWidget() + ] + ] + ]; + + // Store thumbnail for this mesh and material index. + { + TPairInitializer&, int32> Pair(SkelMesh, MaterialIdx); + MaterialInterfaceThumbnailBorders.Add(Pair, MaterialThumbnailBorder); + } + + // ComboBox and buttons + TSharedPtr ComboAndButtonBox; + HorizontalBox->AddSlot() + .FillWidth(1.0f) + .Padding(0.0f, 4.0f, 4.0f, 4.0f) + [ + SAssignNew(ComboAndButtonBox, SVerticalBox) + ]; + + // Add Combo box + TSharedPtr AssetComboButton; + ComboAndButtonBox->AddSlot() + .VAlign(VAlign_Center) + .FillHeight(1.0f) + [ + SNew(SVerticalBox) + SVerticalBox::Slot().VAlign(VAlign_Center).FillHeight(1.0f) + [ + SAssignNew(AssetComboButton, SComboButton) + .ButtonStyle(_GetEditorStyle(), "PropertyEditor.AssetComboStyle") + .ForegroundColor(_GetEditorStyle().GetColor("PropertyEditor.AssetName.ColorAndOpacity")) + .OnGetMenuContent(this, &FHoudiniOutputDetails::OnGetMaterialInterfaceMenuContent, + TWeakObjectPtr(MaterialInterface), (const TWeakObjectPtr&)SkelMesh, InOutput, MaterialIdx) + .ContentPadding(2.0f) + .ButtonContent() + [ + SNew(STextBlock) + .TextStyle(_GetEditorStyle(), "PropertyEditor.AssetClass") + .Font(_GetEditorStyle().GetFontStyle(FName(TEXT("PropertyWindow.NormalFont")))) + .Text(FText::FromString(MaterialName)) + ] + ] + ]; + + // Create tooltip. + FFormatNamedArguments Args; + Args.Add(TEXT("Asset"), FText::FromString(MaterialName)); + FText MaterialTooltip = FText::Format( + LOCTEXT("BrowseToSpecificAssetInContentBrowser", "Browse to '{Asset}' in Content Browser"), Args); + + // Add buttons + TSharedPtr ButtonBox; + ComboAndButtonBox->AddSlot() + .FillHeight(1.0f) + [ + SAssignNew(ButtonBox, SHorizontalBox) + ]; + + // Use CB selection arrow button + ButtonBox->AddSlot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeUseSelectedButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnUseContentBrowserSelectedMaterialInterface, + (const TWeakObjectPtr&)SkelMesh, InOutput, MaterialIdx), + TAttribute< FText >(LOCTEXT("UseSelectedAssetFromContentBrowser", "Use Selected Asset from Content Browser"))) + ]; + + // Browse CB button + ButtonBox->AddSlot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&)MaterialInterface), TAttribute< FText >(MaterialTooltip)) + ]; + + // Store combo button for this mesh and index. + { + TPairInitializer&, int32> Pair(SkelMesh, MaterialIdx); + MaterialInterfaceComboButtons.Add(Pair, AssetComboButton); + } + } +} + +void +FHoudiniOutputDetails::CreateTextureOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput) +{ + if (!IsValidWeakPointer(InOutput)) + return; + + const TWeakObjectPtr& HC = Cast(InOutput->GetOuter()); + if (!IsValidWeakPointer(HC)) + return; + + // Go through this output's object + int32 OutputObjIdx = 0; + TMap& OutputObjects = InOutput->GetOutputObjects(); + for (auto& IterObject : OutputObjects) + { + UTexture2D* CurrentTexture = Cast(IterObject.Value.OutputObject); + if (!IsValid(CurrentTexture)) + continue; + + FHoudiniOutputObjectIdentifier& OutputIdentifier = IterObject.Key; + + // Find the corresponding HGPO in the output + FHoudiniGeoPartObject HoudiniGeoPartObject; + for (const auto& curHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + if (!OutputIdentifier.Matches(curHGPO)) + continue; + + HoudiniGeoPartObject = curHGPO; + break; + } + + FString TextureName = CurrentTexture->GetName(); + FString TexturePathName = CurrentTexture->GetPathName(); + if (!IsValid(CurrentTexture)) + { + CurrentTexture = nullptr; + TextureName = TEXT("Texture (invalid)") + FString::FromInt(OutputObjIdx); + TexturePathName = TEXT("Texture (invalid)") + FString::FromInt(OutputObjIdx); + } + + TSharedPtr ThumbnailBorder; + TSharedPtr HorizontalBox = NULL; + // Get thumbnail pool for this builder. + IDetailLayoutBuilder& DetailLayoutBuilder = HouOutputCategory.GetParentLayout(); + TSharedPtr AssetThumbnailPool = DetailLayoutBuilder.GetThumbnailPool(); + // Create thumbnail for this texture. + TSharedPtr TextureThumbnail = + MakeShareable(new FAssetThumbnail(CurrentTexture, 64, 64, AssetThumbnailPool)); + + TSharedRef VerticalBox = SNew(SVerticalBox); + FString TextureLabel = TEXT("Texture"); + IDetailGroup& TextureGrp = HouOutputCategory.AddGroup(FName(*TextureName), FText::FromString(TextureName)); + TextureGrp.AddWidgetRow() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString(TextureLabel)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + VerticalBox + ]; + + VerticalBox->AddSlot() + .Padding(0, 2) + [ + SAssignNew(HorizontalBox, SHorizontalBox) + ]; + + HorizontalBox->AddSlot() + .Padding(0.0f, 0.0f, 2.0f, 0.0f) + .AutoWidth() + [ + SAssignNew(ThumbnailBorder, SBorder) + .Padding(5.0f) + .BorderImage( + this, &FHoudiniOutputDetails::GetMaterialInterfaceThumbnailBorder, (const TWeakObjectPtr&)CurrentTexture, OutputObjIdx) + .OnMouseDoubleClick( + this, &FHoudiniOutputDetails::OnThumbnailDoubleClick, (const TWeakObjectPtr&)CurrentTexture) + [ + SNew(SBox) + .WidthOverride(64) + .HeightOverride(64) + .ToolTipText(FText::FromString(TexturePathName)) + [ + TextureThumbnail->MakeThumbnailWidget() + ] + ] + ]; + + // Store thumbnail for this texture and index. + { + TPairInitializer&, int32> Pair(CurrentTexture, OutputObjIdx); + MaterialInterfaceThumbnailBorders.Add(Pair, ThumbnailBorder); + } + + // ComboBox and buttons + TSharedPtr ComboAndButtonBox; + HorizontalBox->AddSlot() + .FillWidth(1.0f) + .Padding(0.0f, 4.0f, 4.0f, 4.0f) + [ + SAssignNew(ComboAndButtonBox, SVerticalBox) + ]; + + // Add Combo box + TSharedPtr< SComboButton > AssetComboButton; + ComboAndButtonBox->AddSlot() + .VAlign(VAlign_Center) + .FillHeight(1.0f) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + .FillHeight(1.0f) + [ + SAssignNew(AssetComboButton, SComboButton) + .ButtonStyle(_GetEditorStyle(), "PropertyEditor.AssetComboStyle") + .ForegroundColor(_GetEditorStyle().GetColor("PropertyEditor.AssetName.ColorAndOpacity")) + /* + .OnGetMenuContent(this, &FHoudiniOutputDetails::OnGetMaterialInterfaceMenuContent, + TWeakObjectPtr(CurrentTexture), (const TWeakObjectPtr&)CurrentTexture, InOutput, OutputObjIdx) + */ + .ContentPadding(2.0f) + .ButtonContent() + [ + SNew(STextBlock) + .TextStyle(_GetEditorStyle(), "PropertyEditor.AssetClass") + .Font(_GetEditorStyle().GetFontStyle(FName(TEXT("PropertyWindow.NormalFont")))) + .Text(FText::FromString(TextureName)) + ] + ] + ]; + + // Create tooltip. + FFormatNamedArguments Args; + Args.Add(TEXT("Asset"), FText::FromString(TextureName)); + FText Tooltip = FText::Format( + LOCTEXT("BrowseToSpecificAssetInContentBrowser", "Browse to '{Asset}' in Content Browser"), Args); + + + // Add buttons + TSharedPtr ButtonBox; + ComboAndButtonBox->AddSlot() + .FillHeight(1.0f) + [ + SAssignNew(ButtonBox, SHorizontalBox) + ]; + + + // Bake button + FString BakeName = IterObject.Value.BakeName; + ButtonBox->AddSlot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Text(LOCTEXT("BakeTexture", "Bake Texture")) + .IsEnabled(true) + .OnClicked_Lambda([BakeName, CurrentTexture, OutputIdentifier, InOutput, HC]() + { + if (!CurrentTexture || !InOutput.IsValid() || !HC.IsValid()) + return FReply::Handled(); + + FHoudiniOutputObject* const FoundOutputObject = InOutput->GetOutputObjects().Find(OutputIdentifier); + if (!FoundOutputObject) + return FReply::Handled(); + + TArray AllOutputs; + FString TempCookFolder; + FString BakeFolder; + FHoudiniBakeSettings BakeSettings; + AllOutputs.Reserve(HC->GetNumOutputs()); + HC->GetOutputs(AllOutputs); + TempCookFolder = HC->GetTemporaryCookFolderOrDefault(); + BakeFolder = HC->GetBakeFolderOrDefault(); + BakeSettings.SetFromCookable(HC.Get()); + + FHoudiniGeoPartObject HoudiniGeoPartObject; + for (const auto& curHGPO : InOutput->GetHoudiniGeoPartObjects()) + { + if (!OutputIdentifier.Matches(curHGPO)) + continue; + + HoudiniGeoPartObject = curHGPO; + break; + } + + FHoudiniOutputDetails::OnBakeOutputObject( + BakeName, + CurrentTexture, + OutputIdentifier, + *FoundOutputObject, + HoudiniGeoPartObject, + (UObject*)HC.Get(), + InOutput.Get(), + BakeFolder, + BakeSettings, + TempCookFolder, + EHoudiniLandscapeOutputBakeType::InValid, + AllOutputs); + + return FReply::Handled(); + }) + .ToolTipText(LOCTEXT("HoudiniTextureBakeButton", "Bake this Texture to the Bake folder.")) + ]; + + // Browse CB button + ButtonBox->AddSlot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateSP( + this, &FHoudiniOutputDetails::OnBrowseTo, (const TWeakObjectPtr&)CurrentTexture), TAttribute(Tooltip)) + ]; + } +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniOutputDetails.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "ContentBrowserDelegates.h" +#include "Materials/MaterialInterface.h" +#include "Components/Border.h" +#include "Components/ComboBox.h" +#include "PhysicsEngine/PhysicsAsset.h" + + +struct FHoudiniBakeSettings; + +class AGeometryCollectionActor; +class ALandscapeProxy; +class ALandscapeSplineActor; +class FAssetThumbnailPool; +class FDetailWidgetRow; +class IDetailCategoryBuilder; +class SBorder; +class SComboButton; +class UGeometryCollection; +class UHoudiniLandscapePtr; +class UHoudiniLandscapeTargetLayerOutput; +class UHoudiniOutput; +class UHoudiniAssetComponent; +class UHoudiniStaticMesh; +class UMaterialInterface; +class USplineComponent; +class USkeletalMesh; +class USkeleton; + +struct FHoudiniGeoPartObject; +struct FHoudiniOutputObjectIdentifier; +struct FHoudiniOutputObject; + +enum class EHoudiniOutputType : uint8; +enum class EHoudiniLandscapeOutputBakeType : uint8; + +class FHoudiniOutputDetails : public TSharedFromThis +{ +public: + void CreateWidget( + IDetailCategoryBuilder& HouInputCategoryBuilder, + const TArray>& InOutputs); + + void CreateMeshOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput); + + void CreateSkeletalOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput); + + void CreateAnimSequenceOutputWidget(IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput); + + void CreateCurveOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput); + + void CreateGeometryCollectionOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput); + + void CreateDataTableOutputWidget(IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput); + + void CreateLandscapeSplineOutputWidget(IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput); + + void CreateLandscapeSplineWidgets( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const TWeakObjectPtr& LandscapeSplineActor, + const FHoudiniOutputObject& OutputObject, + const FHoudiniOutputObjectIdentifier& OutputIdentifier, + const FHoudiniGeoPartObject& HoudiniGeoPartObject); + + void CreateStaticMeshAndMaterialWidgets( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const TWeakObjectPtr& StaticMesh, + FHoudiniOutputObjectIdentifier& OutputIdentifier, + FHoudiniGeoPartObject& HoudiniGeoPartObject, + const bool& bIsProxyMeshCurrent); + + void CreateProxyMeshAndMaterialWidgets( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const TWeakObjectPtr& ProxyMesh, + FHoudiniOutputObjectIdentifier& OutputIdentifier, + FHoudiniGeoPartObject& HoudiniGeoPartObject); + + void CreateSkeletalMeshAndMaterialWidgets( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const TWeakObjectPtr& SkelMesh, + const TWeakObjectPtr& Skeleton, + const TWeakObjectPtr& PhysicsAsset, + FHoudiniOutputObjectIdentifier& OutputIdentifier, + FHoudiniGeoPartObject& HoudiniGeoPartObject); + + void CreateCurveWidgets( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const TWeakObjectPtr& SplineComponent, + FHoudiniOutputObject& OutputObject, + FHoudiniOutputObjectIdentifier& OutputIdentifier, + FHoudiniGeoPartObject& HoudiniGeoPartObject); + + void CreateGeometryCollectionWidgets( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput, + const TWeakObjectPtr& GeometryCollection, + FHoudiniOutputObject& OutputObject, + FHoudiniOutputObjectIdentifier& OutputIdentifier, + FHoudiniGeoPartObject& HoudiniGeoPartObject); + + void CreateLandscapeOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput); + + void CreateLandscapeOutputWidget_Helper( + IDetailCategoryBuilder & HouOutputCategory, + const TWeakObjectPtr& InOutput, + const FHoudiniGeoPartObject & HGPO, + const TWeakObjectPtr& LandscapePointer, + const FHoudiniOutputObjectIdentifier & OutputIdentifier); + + void CreateLandscapeEditLayerOutputWidget_Helper( + IDetailCategoryBuilder & HouOutputCategory, + const TWeakObjectPtr& InOutput, + const FHoudiniGeoPartObject & HGPO, + const TWeakObjectPtr& LandscapeEditLayer, + const FHoudiniOutputObjectIdentifier & OutputIdentifier); + + void CreateInstancerOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput); + + void CreateDefaultOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput); + + void CreateTextureOutputWidget( + IDetailCategoryBuilder& HouOutputCategory, + const TWeakObjectPtr& InOutput); + + static FText GetOutputTooltip(const TWeakObjectPtr& MainOutput); + static FText GetOutputDebugName(const TWeakObjectPtr& InOutput); + static FText GetOutputDebugDescription(const TWeakObjectPtr& InOutput); + + static void OnBakeNameCommitted( + const FText& Val, ETextCommit::Type TextCommitType, + const TWeakObjectPtr& InOutput, + const FHoudiniOutputObjectIdentifier & InIdentifier); + + static void OnRevertBakeNameToDefault( + const TWeakObjectPtr& InOutput, + const FHoudiniOutputObjectIdentifier & InIdentifier); + + static void OnBakeOutputObject( + const FString& InBakeName, + UObject * BakedOutputObject, + const FHoudiniOutputObjectIdentifier & OutputIdentifier, + const FHoudiniOutputObject& InOutputObject, + const FHoudiniGeoPartObject & HGPO, + const UObject* OutputOwner, + UHoudiniOutput* InOutput, + const FString & BakeFolder, + const FHoudiniBakeSettings& BakeSettings, + const FString & TempCookFolder, + const EHoudiniLandscapeOutputBakeType & LandscapeBakeType, + const TArray& InAllOutputs); + + FReply OnRefineClicked(const TWeakObjectPtr ObjectToRefine, const TWeakObjectPtr InOutput); + + // Gets the border brush to show around thumbnails, changes when the user hovers on it. + const FSlateBrush * GetThumbnailBorder(const TWeakObjectPtr Mesh) const; + const FSlateBrush * GetMaterialInterfaceThumbnailBorder(const TWeakObjectPtr Mesh, int32 MaterialIdx) const; + + // Delegate used to detect if valid object has been dragged and dropped. + bool OnMaterialInterfaceDraggedOver(TArrayView InAssets) const; + + // Delegate used when a valid material has been drag and dropped on a mesh. + void OnMaterialInterfaceDropped( + const FDragDropEvent& InDragDropEvent, + TArrayView InAssets, + const TWeakObjectPtr InStaticMesh, + const TWeakObjectPtr InOutput, + int32 MaterialIdx); + + void OnMaterialInterfaceDropped( + const TWeakObjectPtr InObject, + const TWeakObjectPtr InStaticMesh, + const TWeakObjectPtr InOutput, + int32 MaterialIdx); + + // Delegate used when a valid material has been drag and dropped on a landscape. + void OnMaterialInterfaceDropped( + const FDragDropEvent& InDragDropEvent, + TArrayView InAssets, + const TWeakObjectPtr InLandscape, + const TWeakObjectPtr InOutput, + int32 MaterialIdx); + + void OnMaterialInterfaceDropped( + UObject* InObject, + const TWeakObjectPtr InLandscape, + const TWeakObjectPtr InOutput, + int32 MaterialIdx); + + // Construct drop down menu content for material. + TSharedRef OnGetMaterialInterfaceMenuContent( + const TWeakObjectPtr MaterialInterface, + const TWeakObjectPtr OutputObject, + const TWeakObjectPtr InOutput, + int32 MaterialIdx); + + // Delegate for handling selection in content browser. + void OnMaterialInterfaceSelected( + const FAssetData & AssetData, + const TWeakObjectPtr OutputObject, + const TWeakObjectPtr InOutput, + int32 MaterialIdx); + + // Delegate for handling Use CB selection arrow button clicked. + void OnUseContentBrowserSelectedMaterialInterface( + const TWeakObjectPtr OutputObject, + const TWeakObjectPtr InOutput, + int32 MaterialIdx); + + // Closes the combo button. + void CloseMaterialInterfaceComboButton(); + + // Browse to material interface. + void OnBrowseTo(const TWeakObjectPtr InObject); + + // Handler for reset material interface button. + FReply OnResetMaterialInterfaceClicked( + const TWeakObjectPtr StaticMesh, const TWeakObjectPtr InOutput, int32 MaterialIdx); + + FReply OnResetMaterialInterfaceClicked( + const TWeakObjectPtr InLandscape, const TWeakObjectPtr InHoudiniOutput, int32 InMaterialIdx); + + // Handler for when static mesh thumbnail is double clicked. We open editor in this case. + FReply OnThumbnailDoubleClick( + const FGeometry & InMyGeometry, const FPointerEvent & InMouseEvent, const TWeakObjectPtr Object); + + // Handler for bake individual static mesh action. + // static FReply OnBakeStaticMesh(UStaticMesh * StaticMesh, UHoudiniAssetComponent * HoudiniAssetComponent, FHoudiniGeoPartObject& GeoPartObject); + +private: + + // Map of meshes and corresponding thumbnail borders. + TMap, TSharedPtr> OutputObjectThumbnailBorders; + // Map of meshes / material indices to thumbnail borders. + TMap, int32>, TSharedPtr> MaterialInterfaceThumbnailBorders; + // Map of meshes / material indices to combo elements. + TMap, int32>, TSharedPtr> MaterialInterfaceComboButtons; + + /** Delegate for filtering material interfaces. **/ + FOnShouldFilterAsset OnShouldFilterMaterialInterface; +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPCGComponentDetails.cpp`: + +```cpp +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "DetailCategoryBuilder.h" +#if defined(HOUDINI_USE_PCG) + +#include "HoudiniPCGComponent.h" +#include "HoudiniEngineRuntimePrivatePCH.h" + +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "DetailLayoutBuilder.h" +#include "DetailCategoryBuilder.h" +#include "DetailWidgetRow.h" +#include "PropertyHandle.h" +#include "IDetailChildrenBuilder.h" + +// Slate UI elements +#include "Widgets/Input/SButton.h" +#include "Widgets/Text/STextBlock.h" + +// Debugging (for on-screen messages) +#include "Engine/Engine.h" + +#include "Internationalization/Internationalization.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +TSharedRef UHoudiniPCGComponentDetails::MakeInstance() +{ + return MakeShareable(new UHoudiniPCGComponentDetails); +} + +void UHoudiniPCGComponentDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + const FName PCGCategoryName("PCG"); + IDetailCategoryBuilder& PCGCategory = DetailBuilder.EditCategory(PCGCategoryName); + + TArray> ObjectsBeingCustomized; + DetailBuilder.GetObjectsBeingCustomized(ObjectsBeingCustomized); + + TArray Components; + + for(TWeakObjectPtr Obj : ObjectsBeingCustomized) + { + if(Obj.IsValid()) + { + UHoudiniPCGComponent* Component = Cast(Obj.Get()); + if(IsValid(Component)) + Components.Add(Component); + } + } + + + + for(UHoudiniPCGComponent* Component : Components) + { + IDetailCategoryBuilder& MyCategory = DetailBuilder.EditCategory("Houdini"); + + FDetailWidgetRow& ButtonRow = MyCategory.AddCustomRow(FText::FromString("Custom Button")); + TSharedRef ButtonHorizontalBox = SNew(SHorizontalBox); + TSharedPtr ClearCacheButton; + TSharedPtr ClearCookBox; + + ButtonHorizontalBox->AddSlot() + // .MaxWidth(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + //.Padding(2.0f, 0.0f, 0.0f, 2.0f) + [ + SNew(SBox) + // .WidthOverride(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SAssignNew(ClearCacheButton, SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("HoudiniPCGComponentDetails", "Clears internal Cookables data.")) + .Text(FText::FromString("Clear HDA Cache Data")) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([Component]() + { + //Component->Cookable->Invalidate(); + return FReply::Handled(); + }) + ] + ]; + + ButtonRow.WholeRowWidget.Widget = ButtonHorizontalBox; + ButtonRow.IsEnabled(true); + } + +} + +UHoudiniPCGComponentDetails::UHoudiniPCGComponentDetails() +{ +} + +#undef LOCTEXT_NAMESPACE +#endif +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPCGComponentDetails.h`: + +```h +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniPCGCookable.h" +#include "PCGComponent.h" +#include "UObject/ObjectMacros.h" +#include "IDetailCustomization.h" +#include "HoudiniPCGComponent.generated.h" + +class UHoudiniPCGComponentDetails : public IDetailCustomization +{ +public: + UHoudiniPCGComponentDetails(); + + // Factory method to register the customization + static TSharedRef MakeInstance(); + + // Override to customize the details panel + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; +}; + + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPCGDetails.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPCGDetails.h" +#include "HoudiniPCGNode.h" +#include "DetailLayoutBuilder.h" + + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + + +TSharedRef UHoudiniPCGSettingsCustomization::MakeInstance() +{ + return MakeShareable(new UHoudiniPCGSettingsCustomization); +} + +void UHoudiniPCGSettingsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + TArray> ObjectsBeingCustomized; + DetailBuilder.GetObjectsBeingCustomized(ObjectsBeingCustomized); + + TArray> SettingsBeingCustomized; + TArray> CookablesBeingCustomized; + + + for (auto Obj : ObjectsBeingCustomized) + { + UHoudiniPCGSettings* Settings = Cast(Obj.Get()); + if (IsValid(Settings)) + { + SettingsBeingCustomized.Add(Settings); + if(IsValid(Settings->ParameterCookable)) + { + UHoudiniCookable* Cookable = Settings->ParameterCookable->Cookable; + + if(Cookable) + { + CookablesBeingCustomized.Add(Cookable); + } + } + + } + } + + for (auto Settings : SettingsBeingCustomized) + { + if(!Settings.IsValid()) + continue; + + + IDetailCategoryBuilder& SettingsCategory = DetailBuilder.EditCategory(TEXT("HoudiniPCG")); + + SettingsCategory.AddCustomRow(FText::GetEmpty()).ValueContent() + .MaxDesiredWidth(120.f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Fill) + [ + SNew(SButton) + .OnClicked_Lambda([Settings]() -> FReply + { + if (Settings.IsValid()) + { + Settings.Get()->ResetFromHDA(); + } + return FReply::Handled(); + }) + .ToolTipText(FText::FromString("Resets the node from the HDA, resetting parameters, inputs and outputs.")) + [ + SNew(STextBlock) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Text(LOCTEXT("UHoudiniPCGSettingsCustomizationRebuildHDA", "Reset From HDA")) + ] + ] + ]; + + if(!IsValid(Settings->ParameterCookable) || !IsValid(Settings->ParameterCookable->Cookable)) + continue; + + UHoudiniCookable* Cookable = Settings->ParameterCookable->Cookable; + + if (!Settings->ParameterCookable->Cookable->GetIsPCG()) + { + Settings->ParameterCookable->Cookable->SetIsPCG(true); + } + + TArray> Cookables; + Cookables.Add(Cookable); + + EHoudiniDetailsFlags Flags; + Flags.bAutoBake = false; + Flags.bBakeButton = false; + Flags.bDisplayOnOutputLess = true; + Flags.bAssetOptions = false; + Flags.bGenerateBar = false; + Flags.bReplacePreviousBake = false; + + CookableDetails->CreateHoudiniEngineDetails(DetailBuilder, Cookables, FString(), Flags); + + if(Cookable->IsPDGSupported()) + { + CookableDetails->CreatePDGDetails(DetailBuilder, Cookables); + } + + CookableDetails->CreateInputDetails(DetailBuilder, Cookables); + CookableDetails->CreateParameterDetails(DetailBuilder, Cookables); + } +} + +UHoudiniPCGSettingsCustomization::UHoudiniPCGSettingsCustomization() +{ + CookableDetails = MakeShareable(new FHoudiniCookableDetails()); +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPCGDetails.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "DetailCategoryBuilder.h" +#include "HoudiniCookableDetails.h" +#include "HoudiniOutputDetails.h" +#include "IDetailCustomization.h" +#include "Async/PCGAsyncLoadingContext.h" + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wshadow" +#endif + +#include "PCGContext.h" + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#include "PCGGraph.h" + +class UHoudiniPCGSettingsCustomization : public IDetailCustomization +{ +public: + UHoudiniPCGSettingsCustomization(); + + static TSharedRef MakeInstance(); + + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; + + TSharedPtr CookableDetails; + +}; + + + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPCGSettingsDetails.cpp`: + +```cpp +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPCGSettingsDetails.h" + +#include "DetailCategoryBuilder.h" +#if defined(HOUDINI_USE_PCG) + +#include "HoudiniPCGComponent.h" +#include "HoudiniEngineRuntimePrivatePCH.h" + +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "DetailLayoutBuilder.h" +#include "DetailCategoryBuilder.h" +#include "DetailWidgetRow.h" +#include "PropertyHandle.h" +#include "IDetailChildrenBuilder.h" + +// Slate UI elements +#include "Widgets/Input/SButton.h" +#include "Widgets/Text/STextBlock.h" + +// Debugging (for on-screen messages) +#include "Engine/Engine.h" + +#include "Internationalization/Internationalization.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +TSharedRef UHoudiniPCGSettingsDetails::MakeInstance() +{ + return MakeShareable(new UHoudiniPCGComponentDetails); +} + +void UHoudiniPCGSettingsDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + const FName PCGCategoryName("PCG"); + IDetailCategoryBuilder& PCGCategory = DetailBuilder.EditCategory(PCGCategoryName); + + TArray> ObjectsBeingCustomized; + DetailBuilder.GetObjectsBeingCustomized(ObjectsBeingCustomized); + + TArray Components; + + for(TWeakObjectPtr Obj : ObjectsBeingCustomized) + { + if(Obj.IsValid()) + { + UHoudiniPCGComponent* Component = Cast(Obj.Get()); + if(IsValid(Component)) + Components.Add(Component); + } + } + + for(UHoudiniPCGComponent* Component : Components) + { + IDetailCategoryBuilder& MyCategory = DetailBuilder.EditCategory("Houdini"); + + FDetailWidgetRow& ButtonRow = MyCategory.AddCustomRow(FText::FromString("Custom Button")); + TSharedRef ButtonHorizontalBox = SNew(SHorizontalBox); + TSharedPtr ClearCacheButton; + TSharedPtr ClearCookBox; + + ButtonHorizontalBox->AddSlot() + // .MaxWidth(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + //.Padding(2.0f, 0.0f, 0.0f, 2.0f) + [ + SNew(SBox) + // .WidthOverride(HOUDINI_ENGINE_UI_BUTTON_WIDTH) + [ + SAssignNew(ClearCacheButton, SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("HoudiniPCGComponentDetails", "Clears internal Cookables data.")) + .Text(FText::FromString("Clear HDA Cache Data")) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([Component]() + { + //Component->CookableCache->Invalidate(); + return FReply::Handled(); + }) + ] + ]; + + ButtonRow.WholeRowWidget.Widget = ButtonHorizontalBox; + ButtonRow.IsEnabled(true); + } + +} + +UHoudiniPCGSettingsDetails::UHoudiniPCGSettingsDetails() +{ +} + +#undef LOCTEXT_NAMESPACE +#endif +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPCGSettingsDetails.h`: + +```h +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniPCGCookable.h" + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wshadow" +#endif + +#include "PCGComponent.h" + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#include "UObject/ObjectMacros.h" +#include "IDetailCustomization.h" + +class UHoudiniPCGSettingsDetails : public IDetailCustomization +{ +public: + UHoudiniPCGSettingsDetails(); + + // Factory method to register the customization + static TSharedRef MakeInstance(); + + // Override to customize the details panel + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; +}; + + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPDGDetails.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPDGDetails.h" + +#include "HoudiniEngineEditorPrivatePCH.h" + +#include "HoudiniPDGAssetLink.h" +#include "HoudiniPDGManager.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniAssetActor.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniEngineCommands.h" +#include "HoudiniEngineDetails.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineEditorUtils.h" + +#include "DetailCategoryBuilder.h" +#include "DetailLayoutBuilder.h" +#include "IDetailGroup.h" +#include "IDetailCustomization.h" +#include "PropertyCustomizationHelpers.h" +#include "DetailWidgetRow.h" +#include "ScopedTransaction.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Layout/SSpacer.h" +#include "Framework/SlateDelegates.h" +#include "Templates/SharedPointer.h" + +#include "Internationalization/Internationalization.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +#define HOUDINI_ENGINE_UI_SECTION_PDG_BAKE 2 + +namespace +{ +bool +IsTextAndTooltipLexicographicallyBefore( + const TSharedPtr& Left, + const TSharedPtr& Right) +{ + // If we don't trim the whitespace, then all strings with + // padded spaces will end up at the start. + return Left->Text.TrimStart() < Right->Text.TrimStart(); +} + +FString +FormatTOPNodeName( + const TWeakObjectPtr& InPDGAssetLink, + FString TOPNodeName) +{ + if (InPDGAssetLink->bUseTOPOutputFilter && + TOPNodeName.StartsWith(InPDGAssetLink->TOPOutputFilter)) + { + TOPNodeName.RemoveFromStart(InPDGAssetLink->TOPOutputFilter); + TOPNodeName.Append(" (Output)"); + } + else if (InPDGAssetLink->bUseTOPNodeFilter && + TOPNodeName.StartsWith(InPDGAssetLink->TOPNodeFilter)) + { + TOPNodeName.RemoveFromStart(InPDGAssetLink->TOPNodeFilter); + } + return TOPNodeName; +} +} + +void +FHoudiniPDGDetails::CreateWidget( + IDetailCategoryBuilder& HouPDGCategory, + const TWeakObjectPtr&& InPDGAssetLink, + bool bIsPCG) +{ + if (!IsValidWeakPointer(InPDGAssetLink)) + return; + + // PDG ASSET + FHoudiniPDGDetails::AddPDGAssetWidget(HouPDGCategory, InPDGAssetLink, bIsPCG); + + // TOP NETWORKS + FHoudiniPDGDetails::AddTOPNetworkWidget(HouPDGCategory, InPDGAssetLink, bIsPCG); + + // PDG EVENT MESSAGES +} + + +void +FHoudiniPDGDetails::AddPDGAssetWidget( + IDetailCategoryBuilder& InPDGCategory, const TWeakObjectPtr& InPDGAssetLink, + bool bIsPCG) +{ + // PDG STATUS ROW + AddPDGAssetStatus(InPDGCategory, InPDGAssetLink); + + // Commandlet Status row + AddPDGCommandletStatus(InPDGCategory, FHoudiniEngine::Get().GetPDGCommandletStatus()); + + // REFRESH / RESET Buttons + { + TSharedRef RefreshHBox = SNew(SHorizontalBox); + TSharedPtr ResetHBox = SNew(SHorizontalBox); + + FDetailWidgetRow& PDGRefreshResetRow = InPDGCategory.AddCustomRow(FText::FromString("PDG Refresh")) + .WholeRowContent() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(SBox) + .WidthOverride(200.0f) + [ + SNew(SButton) + //.Text(LOCTEXT("Refresh", "Refresh")) + .ToolTipText(LOCTEXT("RefreshTooltip", "Refreshes infos displayed by the the PDG Asset Link")) + .ContentPadding(FMargin(5.0f, 5.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .OnClicked_Lambda([InPDGAssetLink]() + { + FHoudiniPDGDetails::RefreshPDGAssetLink(InPDGAssetLink); + return FReply::Handled(); + }) + .Content() + [ + SAssignNew(RefreshHBox, SHorizontalBox) + ] + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SBox) + .WidthOverride(200.0f) + [ + SNew(SButton) + //.Text(LOCTEXT("Reset", "Reset")) + .ToolTipText(LOCTEXT("ResetTooltip", "Resets the PDG Asset Link")) + .ContentPadding(FMargin(5.0f, 5.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .OnClicked_Lambda([InPDGAssetLink]() + { + // TODO: RESET USELESS? this is just a UI refresh - change name ? + FHoudiniPDGDetails::RefreshUI(InPDGAssetLink); + return FReply::Handled(); + }) + .Content() + [ + SAssignNew(ResetHBox, SHorizontalBox) + ] + ] + ] + ]; + + TSharedPtr RefreshIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIPDGRefreshIconBrush(); + if (RefreshIconBrush.IsValid()) + { + TSharedPtr RefreshImage; + RefreshHBox->AddSlot() + .MaxWidth(16.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(RefreshImage, SImage) + ] + ]; + + RefreshImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([RefreshIconBrush]() { return RefreshIconBrush.Get(); }))); + } + + RefreshHBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("Refresh", "Refresh")) + ]; + + TSharedPtr ResetIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIPDGResetIconBrush(); + if (ResetIconBrush.IsValid()) + { + TSharedPtr ResetImage; + ResetHBox->AddSlot() + .MaxWidth(16.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(ResetImage, SImage) + ] + ]; + + ResetImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([ResetIconBrush]() { return ResetIconBrush.Get(); }))); + } + + ResetHBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("Reset", "Reset")) + ]; + } + + // TOP NODE FILTER + { + FText Tooltip = FText::FromString(TEXT("When enabled, the TOP Node Filter will only display the TOP Nodes found in the current network that start with the filter prefix. Disabling the Filter will display all of the TOP Network's TOP Nodes.")); + // Lambda for changing the filter value + auto ChangeTOPNodeFilter = [InPDGAssetLink](const FString& NewValue) + { + if (!IsValidWeakPointer(InPDGAssetLink)) + return; + + if (InPDGAssetLink->TOPNodeFilter.Equals(NewValue)) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + InPDGAssetLink->Modify(); + InPDGAssetLink->TOPNodeFilter = NewValue; + // Notify that we have changed the property + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, TOPNodeFilter), InPDGAssetLink.Get()); + }; + + FDetailWidgetRow& PDGFilterRow = InPDGCategory.AddCustomRow(FText::FromString("PDG Filter")); + // Disable if PDG is not linked + DisableIfPDGNotLinked(PDGFilterRow, InPDGAssetLink); + PDGFilterRow.NameWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + [ + // Checkbox enable filter + SNew(SCheckBox) + .IsChecked_Lambda([InPDGAssetLink]() + { + return InPDGAssetLink->bUseTOPNodeFilter ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;; + }) + .OnCheckStateChanged_Lambda([InPDGAssetLink](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(InPDGAssetLink)) + return; + + const bool bNewState = (NewState == ECheckBoxState::Checked) ? true : false; + if (InPDGAssetLink->bUseTOPNodeFilter == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + InPDGAssetLink->Modify(); + InPDGAssetLink->bUseTOPNodeFilter = bNewState; + // Notify that we have changed the property + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, bUseTOPNodeFilter), InPDGAssetLink.Get()); + }) + .ToolTipText(Tooltip) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("TOP Node Filter"))) + .ToolTipText(Tooltip) + ]; + + PDGFilterRow.ValueWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [ + SNew(SEditableTextBox) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .ToolTipText(Tooltip) + .Text_Lambda([InPDGAssetLink]() + { + if (!IsValidWeakPointer(InPDGAssetLink)) + return FText(); + return FText::FromString(InPDGAssetLink->TOPNodeFilter); + }) + .OnTextCommitted_Lambda([ChangeTOPNodeFilter](const FText& Val, ETextCommit::Type TextCommitType) + { + ChangeTOPNodeFilter(Val.ToString()); + }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertToDefault", "Revert to default")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([=]() + { + FString DefaultFilter = TEXT(HAPI_UNREAL_PDG_DEFAULT_TOP_FILTER); + ChangeTOPNodeFilter(DefaultFilter); + return FReply::Handled(); + }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ]; + } + + // TOP OUTPUT FILTER + { + // Lambda for changing the filter value + FText Tooltip = FText::FromString(TEXT("When enabled, the Work Item Output Files created for the TOP Nodes found in the current network that start with the filter prefix will be automatically loaded int the world after being cooked.")); + auto ChangeTOPOutputFilter = [InPDGAssetLink](const FString& NewValue) + { + if (!IsValidWeakPointer(InPDGAssetLink)) + return; + + if (InPDGAssetLink->TOPOutputFilter.Equals(NewValue)) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + InPDGAssetLink->Modify(); + InPDGAssetLink->TOPOutputFilter = NewValue; + // Notify that we have changed the property + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, TOPOutputFilter), InPDGAssetLink.Get()); + }; + + FDetailWidgetRow& PDGOutputFilterRow = InPDGCategory.AddCustomRow(FText::FromString("PDG Output")); + // Disable if PDG is not linked + DisableIfPDGNotLinked(PDGOutputFilterRow, InPDGAssetLink); + + PDGOutputFilterRow.NameWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + [ + // Checkbox enable filter + SNew(SCheckBox) + .IsChecked_Lambda([InPDGAssetLink]() + { + return InPDGAssetLink->bUseTOPOutputFilter ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InPDGAssetLink](ECheckBoxState NewState) + { + if (IsValidWeakPointer(InPDGAssetLink)) + return; + + const bool bNewState = (NewState == ECheckBoxState::Checked) ? true : false; + if (InPDGAssetLink->bUseTOPOutputFilter == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + InPDGAssetLink->Modify(); + InPDGAssetLink->bUseTOPOutputFilter = bNewState; + // Notify that we have changed the property + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, bUseTOPOutputFilter), InPDGAssetLink.Get()); + }) + .ToolTipText(Tooltip) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("TOP Output Filter"))) + .ToolTipText(Tooltip) + ]; + + PDGOutputFilterRow.ValueWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [ + SNew(SEditableTextBox) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text_Lambda([InPDGAssetLink]() + { + if (!IsValidWeakPointer(InPDGAssetLink)) + return FText(); + return FText::FromString(InPDGAssetLink->TOPOutputFilter); + }) + .OnTextCommitted_Lambda([ChangeTOPOutputFilter](const FText& Val, ETextCommit::Type TextCommitType) + { + ChangeTOPOutputFilter(Val.ToString()); + }) + .ToolTipText(Tooltip) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertToDefault", "Revert to default")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([ChangeTOPOutputFilter]() + { + FString DefaultFilter = TEXT(HAPI_UNREAL_PDG_DEFAULT_TOP_OUTPUT_FILTER); + ChangeTOPOutputFilter(DefaultFilter); + return FReply::Handled(); + }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ]; + } + + // Checkbox: Autocook + if(!bIsPCG) + { + FText Tooltip = FText::FromString(TEXT("When enabled, the selected TOP Network's output will automatically cook after succesfully cooking the PDG Asset Link HDA.")); + FDetailWidgetRow& PDGAutocookRow = InPDGCategory.AddCustomRow(FText::FromString("PDG Autocook")); + // Disable if PDG is not linked + DisableIfPDGNotLinked(PDGAutocookRow, InPDGAssetLink); + PDGAutocookRow.NameWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Auto-cook"))) + .ToolTipText(Tooltip) + ]; + + TSharedPtr AutoCookCheckBox; + PDGAutocookRow.ValueWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + [ + // Checkbox + SAssignNew(AutoCookCheckBox, SCheckBox) + .IsChecked_Lambda([InPDGAssetLink]() + { + return InPDGAssetLink->bAutoCook ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InPDGAssetLink](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked) ? true : false; + if (!IsValidWeakPointer(InPDGAssetLink) || InPDGAssetLink->bAutoCook == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + InPDGAssetLink->Modify(); + InPDGAssetLink->bAutoCook = bNewState; + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, bAutoCook), InPDGAssetLink.Get()); + }) + .ToolTipText(Tooltip) + ]; + } + // Output parent actor selector + if(!bIsPCG) + { + IDetailPropertyRow* PDGOutputParentActorRow = InPDGCategory.AddExternalObjectProperty({ InPDGAssetLink.Get() }, "OutputParentActor"); + if (PDGOutputParentActorRow) + { + TAttribute PDGOutputParentActorRowEnabled; + BindDisableIfPDGNotLinked(PDGOutputParentActorRowEnabled, InPDGAssetLink); + PDGOutputParentActorRow->IsEnabled(PDGOutputParentActorRowEnabled); + TSharedPtr NameWidget; + TSharedPtr ValueWidget; + PDGOutputParentActorRow->GetDefaultWidgets(NameWidget, ValueWidget); + PDGOutputParentActorRow->DisplayName(FText::FromString(TEXT("Output Parent Actor"))); + PDGOutputParentActorRow->ToolTip(FText::FromString( + TEXT("The PDG Output Actors will be created under this parent actor. If not set, then the PDG Output Actors will be created under a new folder."))); + } + } + + // Add bake widgets for PDG output + CreatePDGBakeWidgets(InPDGCategory, InPDGAssetLink, bIsPCG); + + // TODO: move this to a better place: the baking code is in HoudiniEngineEditor, the PDG manager (that knows about + // when work object results are loaded is in HoudiniEngine and the PDGAssetLink is in HoudiniEngineRuntime). So + // we bind an auto-bake helper function here. Maybe the baking code can move to HoudiniEngine? + if (InPDGAssetLink->AutoBakeDelegateHandle.IsValid()) + InPDGAssetLink->OnWorkResultObjectLoaded.Remove(InPDGAssetLink->AutoBakeDelegateHandle); + InPDGAssetLink->AutoBakeDelegateHandle = InPDGAssetLink->OnWorkResultObjectLoaded.AddStatic(FHoudiniEngineBakeUtils::CheckPDGAutoBakeAfterResultObjectLoaded); + + // WORK ITEM STATUS + if (!bIsPCG) + { + FDetailWidgetRow& PDGStatusRow = InPDGCategory.AddCustomRow(FText::FromString("PDG work item status")); + // Disable if PDG is not linked + DisableIfPDGNotLinked(PDGStatusRow, InPDGAssetLink); + FHoudiniPDGDetails::AddWorkItemStatusWidget( + PDGStatusRow, TEXT("Asset Work Item Status"), InPDGAssetLink, false); + } +} + +bool +FHoudiniPDGDetails::GetPDGStatusAndColor( + const TWeakObjectPtr& InPDGAssetLink, FString& OutPDGStatusString, FLinearColor& OutPDGStatusColor) +{ + OutPDGStatusString = FString(); + OutPDGStatusColor = FLinearColor::White; + + if (!IsValidWeakPointer(InPDGAssetLink)) + return false; + + switch (InPDGAssetLink->LinkState) + { + case EPDGLinkState::Linked: + OutPDGStatusString = TEXT("PDG is READY"); + OutPDGStatusColor = FLinearColor::Green; + break; + case EPDGLinkState::Linking: + OutPDGStatusString = TEXT("PDG is Linking"); + OutPDGStatusColor = FLinearColor::Yellow; + break; + case EPDGLinkState::Error_Not_Linked: + OutPDGStatusString = TEXT("PDG is ERRORED"); + OutPDGStatusColor = FLinearColor::Red; + break; + case EPDGLinkState::Inactive: + OutPDGStatusString = TEXT("PDG is INACTIVE"); + OutPDGStatusColor = FLinearColor::White; + break; + default: + return false; + } + + return true; +} + +void +FHoudiniPDGDetails::AddPDGAssetStatus( + IDetailCategoryBuilder& InPDGCategory, const TWeakObjectPtr& InPDGAssetLink) +{ + FDetailWidgetRow& PDGStatusRow = InPDGCategory.AddCustomRow(FText::FromString("PDG Asset Status")) + .WholeRowContent() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text_Lambda([InPDGAssetLink]() + { + FString PDGStatusString; + FLinearColor PDGStatusColor; + GetPDGStatusAndColor(InPDGAssetLink, PDGStatusString, PDGStatusColor); + return FText::FromString(PDGStatusString); + }) + .ColorAndOpacity_Lambda([InPDGAssetLink]() + { + FString PDGStatusString; + FLinearColor PDGStatusColor; + GetPDGStatusAndColor(InPDGAssetLink, PDGStatusString, PDGStatusColor); + return FSlateColor(PDGStatusColor); + }) + ] + ]; +} + +void +FHoudiniPDGDetails::GetPDGCommandletStatus(FString& OutStatusString, FLinearColor& OutStatusColor) +{ + OutStatusString = FString(); + OutStatusColor = FLinearColor::White; + switch (FHoudiniEngine::Get().GetPDGCommandletStatus()) + { + case EHoudiniBGEOCommandletStatus::Connected: + OutStatusString = TEXT("Async importer is CONNECTED"); + OutStatusColor = FLinearColor::Green; + break; + case EHoudiniBGEOCommandletStatus::Running: + OutStatusString = TEXT("Async importer is Running"); + OutStatusColor = FLinearColor::Yellow; + break; + case EHoudiniBGEOCommandletStatus::Crashed: + OutStatusString = TEXT("Async importer has CRASHED"); + OutStatusColor = FLinearColor::Red; + break; + case EHoudiniBGEOCommandletStatus::NotStarted: + OutStatusString = TEXT("Async importer is NOT STARTED"); + OutStatusColor = FLinearColor::White; + break; + } +} + +void +FHoudiniPDGDetails::AddPDGCommandletStatus( + IDetailCategoryBuilder& InPDGCategory, const EHoudiniBGEOCommandletStatus& InCommandletStatus) +{ + FDetailWidgetRow& PDGStatusRow = InPDGCategory.AddCustomRow(FText::FromString("PDG Commandlet Status")) + .WholeRowContent() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Visibility_Lambda([]() + { + const UHoudiniRuntimeSettings* Settings = GetDefault(); + if (IsValid(Settings)) + { + return FHoudiniEngineCommands::IsPDGCommandletEnabled() ? EVisibility::Visible : EVisibility::Collapsed; + } + + return EVisibility::Visible; + }) + .Text_Lambda([]() + { + FString StatusString; + FLinearColor StatusColor; + GetPDGCommandletStatus(StatusString, StatusColor); + return FText::FromString(StatusString); + }) + .ColorAndOpacity_Lambda([]() + { + FString StatusString; + FLinearColor StatusColor; + GetPDGCommandletStatus(StatusString, StatusColor); + return FSlateColor(StatusColor); + }) + ] + ]; +} + +bool +FHoudiniPDGDetails::GetWorkItemTallyValueAndColor( + const TWeakObjectPtr& InAssetLink, + bool bInForSelectedNode, + const FString& InTallyItemString, + int32& OutValue, + FLinearColor& OutColor) +{ + OutValue = 0; + OutColor = FLinearColor::White; + + if (!IsValidWeakPointer(InAssetLink)) + return false; + + bool bFound = false; + const FWorkItemTallyBase* TallyPtr = nullptr; + if (bInForSelectedNode) + { + UTOPNode* const TOPNode = InAssetLink->GetSelectedTOPNode(); + if (TOPNode && !TOPNode->bHidden) + TallyPtr = &(TOPNode->GetWorkItemTally()); + } + else + TallyPtr = &(InAssetLink->WorkItemTally); + + if (TallyPtr) + { + if (InTallyItemString == TEXT("WAITING")) + { + // For now we add waiting and scheduled together, since there is no separate column for scheduled on the UI + OutValue = TallyPtr->NumWaitingWorkItems() + TallyPtr->NumScheduledWorkItems(); + OutColor = OutValue > 0 ? FLinearColor(0.0f, 1.0f, 1.0f) : FLinearColor::White; + bFound = true; + } + else if (InTallyItemString == TEXT("COOKING")) + { + OutValue = TallyPtr->NumCookingWorkItems(); + OutColor = OutValue > 0 ? FLinearColor::Yellow : FLinearColor::White; + bFound = true; + } + else if (InTallyItemString == TEXT("COOKED")) + { + OutValue = TallyPtr->NumCookedWorkItems(); + OutColor = OutValue > 0 ? FLinearColor::Green : FLinearColor::White; + bFound = true; + } + else if (InTallyItemString == TEXT("FAILED")) + { + OutValue = TallyPtr->NumErroredWorkItems(); + OutColor = OutValue > 0 ? FLinearColor::Red : FLinearColor::White; + bFound = true; + } + } + + return bFound; +} + +void +FHoudiniPDGDetails::AddWorkItemStatusWidget( + FDetailWidgetRow& InRow, const FString& InTitleString, const TWeakObjectPtr& InAssetLink, bool bInForSelectedNode) +{ + auto AddGridBox = [InAssetLink, bInForSelectedNode](const FString& Title) -> SHorizontalBox::FSlot::FSlotArguments + { + SHorizontalBox::FSlot::FSlotArguments Slot = SHorizontalBox::Slot(); + + Slot + .MaxWidth(500.0f) + .Padding(0.0f, 0.0f, 2.0f, 0.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .AutoHeight() + .Padding(FMargin(1.0f, 2.0f)) + [ + SNew(SBorder) + .IsEnabled_Lambda([InAssetLink]() { return IsPDGLinked(InAssetLink); }) + .BorderImage(_GetEditorStyle().GetBrush("ToolPanel.GroupBorder")) + .BorderBackgroundColor(FSlateColor(FLinearColor(0.6, 0.6, 0.6))) + .Padding(FMargin(1.0f, 5.0f)) + [ + SNew(SBox) + .WidthOverride(95.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(Title)) + .ColorAndOpacity_Lambda([InAssetLink, bInForSelectedNode, Title]() + { + int32 Value; + FLinearColor Color; + GetWorkItemTallyValueAndColor(InAssetLink, bInForSelectedNode, Title, Value, Color); + return FSlateColor(Color); + }) + ] + ] + ] + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .AutoHeight() + .Padding(FMargin(1.0f, 2.0f)) + [ + SNew(SBorder) + .IsEnabled_Lambda([InAssetLink]() { return IsPDGLinked(InAssetLink); }) + .BorderImage(_GetEditorStyle().GetBrush("ToolPanel.GroupBorder")) + .BorderBackgroundColor(FSlateColor(FLinearColor(0.8, 0.8, 0.8))) + .Padding(FMargin(1.0f, 5.0f)) + [ + SNew(SBox) + .WidthOverride(95.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text_Lambda([InAssetLink, bInForSelectedNode, Title]() + { + int32 Value; + FLinearColor Color; + GetWorkItemTallyValueAndColor(InAssetLink, bInForSelectedNode, Title, Value, Color); + return FText::AsNumber(Value); + }) + .ColorAndOpacity_Lambda([InAssetLink, bInForSelectedNode, Title]() + { + int32 Value; + FLinearColor Color; + GetWorkItemTallyValueAndColor(InAssetLink, bInForSelectedNode, Title, Value, Color); + return FSlateColor(Color); + }) + ] + ] + ] + ]; + + return Slot; + }; + + InRow.WholeRowContent() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(0.0f, 0.0f) + .AutoWidth() + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + [ + SNew(SSpacer) + ] + + SVerticalBox::Slot() + .AutoHeight() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Padding(FMargin(0.0f, 2.0f)) + [ + SNew(STextBlock) + .IsEnabled_Lambda([InAssetLink]() { return IsPDGLinked(InAssetLink); }) + .Text(FText::FromString(InTitleString)) + + ] + + SVerticalBox::Slot() + .AutoHeight() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Padding(FMargin(0.0f, 2.0f)) + [ + SNew(SHorizontalBox) + + AddGridBox(TEXT("WAITING")) + + AddGridBox(TEXT("COOKING")) + + AddGridBox(TEXT("COOKED")) + + AddGridBox(TEXT("FAILED")) + ] + + SVerticalBox::Slot() + [ + SNew(SSpacer) + ] + ] + ]; +} + + +void +FHoudiniPDGDetails::AddTOPNetworkWidget( + IDetailCategoryBuilder& InPDGCategory, const TWeakObjectPtr& InPDGAssetLink, bool bIsPCG ) +{ + auto DirtyAll = [this](const TWeakObjectPtr& InPDGAssetLink) + { + if (IsValidWeakPointer(InPDGAssetLink)) + { + UTOPNetwork* const TOPNetwork = InPDGAssetLink->GetSelectedTOPNetwork(); + if (IsValid(TOPNetwork)) + { + if (IsPDGLinked(InPDGAssetLink)) + { + FHoudiniPDGManager::DirtyAll(TOPNetwork); + // FHoudiniPDGDetails::RefreshUI(InPDGAssetLink); + } + else + { + UHoudiniPDGAssetLink::ClearTOPNetworkWorkItemResults(TOPNetwork); + } + } + } + }; + + if (!InPDGAssetLink->GetSelectedTOPNetwork()) + return; + + if (InPDGAssetLink->AllTOPNetworks.Num() <= 0) + return; + + TOPNetworksPtr.Reset(); + + FString GroupLabel = TEXT("TOP Networks"); + IDetailGroup& TOPNetWorkGrp = InPDGCategory.AddGroup(FName(*GroupLabel), FText::FromString(GroupLabel), false, true); + + // Combobox: TOP Network + { + FDetailWidgetRow& PDGTOPNetRow = TOPNetWorkGrp.AddWidgetRow(); + // DisableIfPDGNotLinked(PDGTOPNetRow, InPDGAssetLink); + PDGTOPNetRow.NameWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("TOP Network"))) + ]; + + // Fill the TOP Networks SharedString array + TOPNetworksPtr.SetNum(InPDGAssetLink->AllTOPNetworks.Num()); + for(int32 Idx = 0; Idx < InPDGAssetLink->AllTOPNetworks.Num(); Idx++) + { + const UTOPNetwork* Network = InPDGAssetLink->AllTOPNetworks[Idx]; + if (!IsValid(Network)) + { + TOPNetworksPtr[Idx] = MakeShareable(new FTextAndTooltip( + Idx, + TEXT("Invalid"), + TEXT("Invalid") + )); + } + else + { + TOPNetworksPtr[Idx] = MakeShareable(new FTextAndTooltip( + Idx, + FHoudiniEngineEditorUtils::GetNodeNamePaddedByPathDepth(Network->NodeName, Network->NodePath), + Network->NodePath + )); + } + } + + Algo::Sort(TOPNetworksPtr, &IsTextAndTooltipLexicographicallyBefore); + + if(TOPNetworksPtr.Num() <= 0) + TOPNetworksPtr.Add(MakeShareable(new FTextAndTooltip(INDEX_NONE, "----"))); + + // Lambda for selecting another TOPNet + auto OnTOPNetChanged = [InPDGAssetLink](TSharedPtr InNewChoice) + { + if (!InNewChoice.IsValid() || !IsValidWeakPointer(InPDGAssetLink)) + return; + + const int32 NewChoice = InNewChoice->Value; + int32 NewSelectedIndex = -1; + if (InPDGAssetLink->AllTOPNetworks.IsValidIndex(NewChoice)) + NewSelectedIndex = NewChoice; + + if (InPDGAssetLink->SelectedTOPNetworkIndex == NewSelectedIndex) + return; + + if (NewSelectedIndex < 0) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + InPDGAssetLink->Modify(); + InPDGAssetLink->SelectedTOPNetworkIndex = NewSelectedIndex; + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, SelectedTOPNetworkIndex), InPDGAssetLink.Get()); + }; + + TSharedPtr HorizontalBoxTOPNet; + TSharedPtr>> ComboBoxTOPNet; + int32 SelectedIndex = TOPNetworksPtr.IndexOfByPredicate([InPDGAssetLink](const TSharedPtr& InEntry) + { + return InEntry.IsValid() && InEntry->Value == InPDGAssetLink->SelectedTOPNetworkIndex; + }); + if (SelectedIndex < 0) + SelectedIndex = 0; + + PDGTOPNetRow.ValueWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(2, 2, 5, 2) + .FillWidth(300.f) + .MaxWidth(300.f) + [ + SAssignNew(ComboBoxTOPNet, SComboBox>) + .OptionsSource(&TOPNetworksPtr) + .InitiallySelectedItem(TOPNetworksPtr[SelectedIndex]) + .OnGenerateWidget_Lambda([](TSharedPtr ChoiceEntry) + { + const FText ChoiceEntryText = FText::FromString(ChoiceEntry->Text); + const FText ChoiceEntryToolTip = FText::FromString(ChoiceEntry->ToolTip); + return SNew(STextBlock) + .Text(ChoiceEntryText) + .ToolTipText(ChoiceEntryToolTip) + .Margin(2.0f) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + }) + .OnSelectionChanged_Lambda([OnTOPNetChanged](TSharedPtr NewChoice, ESelectInfo::Type SelectType) + { + return OnTOPNetChanged(NewChoice); + }) + [ + SNew(STextBlock) + .Text_Lambda([InPDGAssetLink]() + { + return FText::FromString(InPDGAssetLink->GetSelectedTOPNetworkName()); + }) + .ToolTipText_Lambda([InPDGAssetLink]() + { + UTOPNetwork const * const Network = InPDGAssetLink->GetSelectedTOPNetwork(); + if (IsValid(Network)) + { + if (!Network->NodePath.IsEmpty()) + return FText::FromString(Network->NodePath); + else + return FText::FromString(Network->NodeName); + } + else + { + return FText(); + } + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ]; + } + + // Buttons: DIRTY ALL / COOK OUTPUT + if (!bIsPCG) + { + TSharedRef DirtyAllHBox = SNew(SHorizontalBox); + TSharedPtr CookOutHBox = SNew(SHorizontalBox); + + FDetailWidgetRow& PDGDirtyCookRow = TOPNetWorkGrp.AddWidgetRow() + .WholeRowContent() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + [ + SNew(SBox) + .WidthOverride(200.0f) + [ + SNew(SButton) + //.Text(LOCTEXT("DirtyAll", "Dirty All")) + .ToolTipText(LOCTEXT("DirtyAllTooltip", "Dirty all TOP nodes in the selected TOP network and clears all of its work item results.")) + .ContentPadding(FMargin(5.0f, 5.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled_Lambda([InPDGAssetLink]() { return IsPDGLinked(InPDGAssetLink) || (IsValidWeakPointer(InPDGAssetLink) && InPDGAssetLink->GetSelectedTOPNetwork()); }) + .OnClicked_Lambda([InPDGAssetLink, DirtyAll]() + { + DirtyAll(InPDGAssetLink); + return FReply::Handled(); + }) + .Content() + [ + SAssignNew(DirtyAllHBox, SHorizontalBox) + ] + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SBox) + .WidthOverride(200.0f) + [ + SNew(SButton) + //.Text(LOCTEXT("CookOut", "Cook Output")) + .ToolTipText(LOCTEXT("CookOutTooltip", "Cooks the output nodes of the selected TOP network")) + .ContentPadding(FMargin(5.0f, 5.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled_Lambda([InPDGAssetLink]() + { + if (!IsPDGLinked(InPDGAssetLink)) + return false; + const UTOPNetwork* const SelectedTOPNet = InPDGAssetLink->GetSelectedTOPNetwork(); + if (!IsValid(SelectedTOPNet)) + return false; + + // Disable if there any nodes in the network that are already cooking + return !SelectedTOPNet->AnyWorkItemsPending(); + }) + .OnClicked_Lambda([InPDGAssetLink, DirtyAll]() + { + if (IsValid(InPDGAssetLink->GetSelectedTOPNetwork())) + { + //InPDGAssetLink->WorkItemTally.ZeroAll(); + FHoudiniPDGManager::CookOutput(InPDGAssetLink->GetSelectedTOPNetwork()); + // FHoudiniPDGDetails::RefreshUI(InPDGAssetLink); + } + return FReply::Handled(); + }) + .Content() + [ + SAssignNew(CookOutHBox, SHorizontalBox) + ] + ] + ] + ]; + + TSharedPtr DirtyAllIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIPDGDirtyAllIconBrush(); + if (DirtyAllIconBrush.IsValid()) + { + TSharedPtr DirtyAllImage; + DirtyAllHBox->AddSlot() + .MaxWidth(16.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(DirtyAllImage, SImage) + ] + ]; + + DirtyAllImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([DirtyAllIconBrush]() { return DirtyAllIconBrush.Get(); }))); + } + + DirtyAllHBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("DirtyAll", "Dirty All")) + ]; + + TSharedPtr CookOutIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIRecookIconBrush(); + if (CookOutIconBrush.IsValid()) + { + TSharedPtr CookOutImage; + CookOutHBox->AddSlot() + .MaxWidth(16.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(CookOutImage, SImage) + ] + ]; + + CookOutImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([CookOutIconBrush]() { return CookOutIconBrush.Get(); }))); + } + + CookOutHBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text_Lambda([InPDGAssetLink]() { return InPDGAssetLink->bBakeAfterAllWorkResultObjectsLoaded ? LOCTEXT("CookOutAndBake", "Cook Output & Bake") : LOCTEXT("CookOut", "Cook Output"); }) + ]; + + DisableIfPDGNotLinked(PDGDirtyCookRow, InPDGAssetLink); + } + + // Buttons: PAUSE COOK / CANCEL COOK + if (!bIsPCG) + { + TSharedRef PauseHBox = SNew(SHorizontalBox); + TSharedPtr CancelHBox = SNew(SHorizontalBox); + + FDetailWidgetRow& PDGDirtyCookRow = TOPNetWorkGrp.AddWidgetRow() + .WholeRowContent() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SBox) + .WidthOverride(200.0f) + [ + SNew(SButton) + //.Text(LOCTEXT("Pause", "Pause Cook")) + .ToolTipText(LOCTEXT("PauseTooltip", "Pauses cooking for the selected TOP Network")) + .ContentPadding(FMargin(5.0f, 2.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled_Lambda([InPDGAssetLink]() { return IsPDGLinked(InPDGAssetLink); }) + .OnClicked_Lambda([InPDGAssetLink]() + { + if (IsValid(InPDGAssetLink->GetSelectedTOPNetwork())) + { + //InPDGAssetLink->WorkItemTally.ZeroAll(); + FHoudiniPDGManager::PauseCook(InPDGAssetLink->GetSelectedTOPNetwork()); + // FHoudiniPDGDetails::RefreshUI(InPDGAssetLink); + } + return FReply::Handled(); + }) + .Content() + [ + SAssignNew(PauseHBox, SHorizontalBox) + ] + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SBox) + .WidthOverride(200.0f) + [ + SNew(SButton) + //.Text(LOCTEXT("Cancel", "Cancel Cook")) + .ToolTipText(LOCTEXT("CancelTooltip", "Cancels cooking the selected TOP network")) + .ContentPadding(FMargin(5.0f, 2.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled_Lambda([InPDGAssetLink]() { return IsPDGLinked(InPDGAssetLink); }) + .OnClicked_Lambda([InPDGAssetLink]() + { + if (IsValid(InPDGAssetLink->GetSelectedTOPNetwork())) + { + //InPDGAssetLink->WorkItemTally.ZeroAll(); + FHoudiniPDGManager::CancelCook(InPDGAssetLink->GetSelectedTOPNetwork()); + // FHoudiniPDGDetails::RefreshUI(InPDGAssetLink); + } + return FReply::Handled(); + }) + .Content() + [ + SAssignNew(CancelHBox, SHorizontalBox) + ] + ] + ] + ]; + + TSharedPtr PauseIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIPDGPauseIconBrush(); + if (PauseIconBrush.IsValid()) + { + TSharedPtr PauseImage; + PauseHBox->AddSlot() + .MaxWidth(16.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(PauseImage, SImage) + ] + ]; + + PauseImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([PauseIconBrush]() { return PauseIconBrush.Get(); }))); + } + + PauseHBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("Pause", "Pause Cook")) + ]; + + TSharedPtr CancelIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIPDGCancelIconBrush(); + if (CancelIconBrush.IsValid()) + { + TSharedPtr CancelImage; + CancelHBox->AddSlot() + .MaxWidth(16.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(CancelImage, SImage) + ] + ]; + + CancelImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([CancelIconBrush]() { return CancelIconBrush.Get(); }))); + } + + CancelHBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("Cancel", "Cancel Cook")) + ]; + + DisableIfPDGNotLinked(PDGDirtyCookRow, InPDGAssetLink); + } + + // Buttons: Unload Work Item Objects + if(!bIsPCG) + { + FDetailWidgetRow& PDGUnloadLoadWorkItemsRow = TOPNetWorkGrp.AddWidgetRow() + .WholeRowContent() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SBox) + .IsEnabled_Lambda([InPDGAssetLink]() { return IsValidWeakPointer(InPDGAssetLink) && InPDGAssetLink->GetSelectedTOPNetwork(); }) + .WidthOverride(200.0f) + [ + SNew(SButton) + .Text(LOCTEXT("UnloadWorkItemsForNetwork", "Unload All Work Item Objects")) + .ToolTipText(LOCTEXT("UnloadWorkItemsForNetworkTooltip", "Unloads / removes loaded work item results from level for all nodes in this network. Not undoable: use the \"Load Work Item Objects\" button on the individual TOP nodes to reload work item results.")) + .ContentPadding(FMargin(5.0f, 2.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled_Lambda([InPDGAssetLink]() + { + if (!IsValidWeakPointer(InPDGAssetLink)) + return false; + + UTOPNetwork* const SelectedNet = InPDGAssetLink->GetSelectedTOPNetwork(); + if (!IsValid(SelectedNet) || + INDEX_NONE == SelectedNet->AllTOPNodes.IndexOfByPredicate([](const UTOPNode* InNode) { return IsValid(InNode) && InNode->bCachedHaveLoadedWorkResults; })) + return false; + + return true; + }) + .OnClicked_Lambda([InPDGAssetLink]() + { + if (IsValidWeakPointer(InPDGAssetLink)) + { + UTOPNetwork* const TOPNet = InPDGAssetLink->GetSelectedTOPNetwork(); + if (IsValid(TOPNet)) + { + if (IsPDGLinked(InPDGAssetLink)) + { + // Set the state to ToDelete, PDGManager will delete it when processing work items + TOPNet->SetLoadedWorkResultsToDelete(); + } + else + { + // Delete and unload the result objects and actors now + TOPNet->DeleteAllWorkResultObjectOutputs(); + } + } + } + + return FReply::Handled(); + }) + ] + ] + ]; + } + + // TOP NODE WIDGETS + FHoudiniPDGDetails::AddTOPNodeWidget(TOPNetWorkGrp, InPDGAssetLink, bIsPCG); +} + +bool +FHoudiniPDGDetails::GetSelectedTOPNodeStatusAndColor(const TWeakObjectPtr& InPDGAssetLink, FString& OutTOPNodeStatus, FLinearColor &OutTOPNodeStatusColor) +{ + OutTOPNodeStatus = FString(); + OutTOPNodeStatusColor = FLinearColor::White; + if (IsValidWeakPointer(InPDGAssetLink)) + { + UTOPNode* const TOPNode = InPDGAssetLink->GetSelectedTOPNode(); + if (IsValid(TOPNode) && !TOPNode->bHidden) + { + OutTOPNodeStatus = UHoudiniPDGAssetLink::GetTOPNodeStatus(TOPNode); + OutTOPNodeStatusColor = UHoudiniPDGAssetLink::GetTOPNodeStatusColor(TOPNode); + + return true; + } + } + + return false; +} + +void +FHoudiniPDGDetails::AddTOPNodeWidget( + IDetailGroup& InGroup, const TWeakObjectPtr& InPDGAssetLink, bool bIsPCG ) +{ + if (!InPDGAssetLink->GetSelectedTOPNetwork()) + return; + + if(bIsPCG) + return; + + FString GroupLabel = TEXT("TOP Nodes"); + IDetailGroup& TOPNodesGrp = InGroup.AddGroup(FName(*GroupLabel), FText::FromString(GroupLabel), true); + + // Combobox: TOP Node + { + FDetailWidgetRow& PDGTOPNodeRow = TOPNodesGrp.AddWidgetRow(); + // DisableIfPDGNotLinked(PDGTOPNodeRow, InPDGAssetLink); + PDGTOPNodeRow.NameWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("TOP Node"))) + ]; + + // Update the TOP Node SharedString + TOPNodesPtr.Reset(); + TOPNodesPtr.Add(MakeShareable(new FTextAndTooltip(INDEX_NONE, LOCTEXT("ComboBoxEntryNoSelectedTOPNode", "- Select -").ToString()))); + const UTOPNetwork* const SelectedTOPNet = InPDGAssetLink->GetSelectedTOPNetwork(); + if (IsValid(SelectedTOPNet)) + { + const int32 NumTOPNodes = SelectedTOPNet->AllTOPNodes.Num(); + for (int32 Idx = 0; Idx < NumTOPNodes; Idx++) + { + const UTOPNode* const Node = SelectedTOPNet->AllTOPNodes[Idx]; + if (!IsValid(Node) || Node->bHidden) + continue; + + TOPNodesPtr.Add(MakeShareable(new FTextAndTooltip( + Idx, + FHoudiniEngineEditorUtils::GetNodeNamePaddedByPathDepth( + FormatTOPNodeName(InPDGAssetLink, Node->NodeName), + Node->NodePath), + Node->NodePath + ))); + } + + // We only want to sort the range after the empty selection. + // That is, we always want "- Select -" at the top of the combobox. + { + const auto TOPNodesView = + TArrayView>(TOPNodesPtr).RightChop(1); + Algo::Sort(TOPNodesView, &IsTextAndTooltipLexicographicallyBefore); + } + } + + FString NodeErrorText = FString(); + FString NodeErrorTooltip = FString(); + FLinearColor NodeErrorColor = FLinearColor::White; + if (!IsValid(SelectedTOPNet) || SelectedTOPNet->AllTOPNodes.Num() <= 0) + { + NodeErrorText = TEXT("No valid TOP Node found!"); + NodeErrorTooltip = TEXT("There is no valid TOP Node found in the selected TOP Network!"); + NodeErrorColor = FLinearColor::Red; + } + else if(TOPNodesPtr.Num() <= 0) + { + NodeErrorText = TEXT("No visible TOP Node found!"); + NodeErrorTooltip = TEXT("No visible TOP Node found, all nodes in this network are hidden. Please update your TOP Node Filter."); + NodeErrorColor = FLinearColor::Yellow; + } + + // Lambda for selecting a TOPNode + auto OnTOPNodeChanged = [InPDGAssetLink](TSharedPtr InNewChoice) + { + UTOPNetwork* const TOPNetwork = InPDGAssetLink->GetSelectedTOPNetwork(); + if (!InNewChoice.IsValid() || !IsValid(TOPNetwork)) + return; + + const int32 NewChoice = InNewChoice->Value; + int32 NewSelectedIndex = INDEX_NONE; + if (TOPNetwork->AllTOPNodes.IsValidIndex(NewChoice)) + NewSelectedIndex = NewChoice; + + if (TOPNetwork->SelectedTOPIndex != NewSelectedIndex) + { + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + TOPNetwork); + + TOPNetwork->Modify(); + TOPNetwork->SelectedTOPIndex = NewSelectedIndex; + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UTOPNetwork, SelectedTOPIndex), TOPNetwork); + } + }; + + TSharedPtr HorizontalBoxTOPNode; + TSharedPtr>> ComboBoxTOPNode; + int32 SelectedIndex = 0; + UTOPNetwork* const SelectedTOPNetwork = InPDGAssetLink->GetSelectedTOPNetwork(); + if (IsValid(SelectedTOPNetwork) && SelectedTOPNetwork->SelectedTOPIndex >= 0) + { + //SelectedIndex = InPDGAssetLink->GetSelectedTOPNetwork()->SelectedTOPIndex; + + // We need to match the selection by the index in the AllTopNodes array + // Because of the nodefilter, it is possible that the selected index does not match the index in TOPNodesPtr + const int32 SelectedTOPNodeIndex = SelectedTOPNetwork->SelectedTOPIndex; + // Find the matching UI index + for (int32 UIIndex = 0; UIIndex < TOPNodesPtr.Num(); UIIndex++) + { + if (TOPNodesPtr[UIIndex] && TOPNodesPtr[UIIndex]->Value == SelectedTOPNodeIndex) + { + // We found the UI Index that matches the current TOP Node! + SelectedIndex = UIIndex; + break; + } + } + } + + TSharedPtr ErrorText; + + PDGTOPNodeRow.ValueWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(2, 2, 5, 2) + .FillWidth(300.f) + .MaxWidth(300.f) + [ + SAssignNew(ComboBoxTOPNode, SComboBox>) + .OptionsSource(&TOPNodesPtr) + .InitiallySelectedItem(TOPNodesPtr[SelectedIndex]) + .OnGenerateWidget_Lambda([](TSharedPtr ChoiceEntry) + { + const FText ChoiceEntryText = FText::FromString(ChoiceEntry->Text); + const FText ChoiceEntryToolTip = FText::FromString(ChoiceEntry->ToolTip); + return SNew(STextBlock) + .Text(ChoiceEntryText) + .ToolTipText(ChoiceEntryToolTip) + .Margin(2.0f) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + }) + .OnSelectionChanged_Lambda([OnTOPNodeChanged](TSharedPtr NewChoice, ESelectInfo::Type SelectType) + { + return OnTOPNodeChanged(NewChoice); + }) + [ + SNew(STextBlock) + .Text_Lambda([InPDGAssetLink, ComboBoxTOPNode, Options = TOPNodesPtr]() + { + if (IsValidWeakPointer(InPDGAssetLink)) + return FText::FromString(FormatTOPNodeName( + InPDGAssetLink, InPDGAssetLink->GetSelectedTOPNodeName())); + else + return FText(); + }) + .ToolTipText_Lambda([InPDGAssetLink]() + { + UTOPNode const * const TOPNode = InPDGAssetLink->GetSelectedTOPNode(); + if (IsValid(TOPNode)) + { + if (!TOPNode->NodePath.IsEmpty()) + return FText::FromString(TOPNode->NodePath); + else + return FText::FromString(TOPNode->NodeName); + } + else + { + return FText(); + } + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + + SHorizontalBox::Slot() + .Padding(2, 2, 5, 2) + .AutoWidth() + [ + SAssignNew(ErrorText, STextBlock) + .Text(FText::FromString(NodeErrorText)) + .ToolTipText(FText::FromString(NodeErrorText)) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .ColorAndOpacity(FLinearColor::Red) + //.ShadowColorAndOpacity(FLinearColor::Black) + ]; + + // Update the error text if needed + ErrorText->SetText(FText::FromString(NodeErrorText)); + ErrorText->SetToolTipText(FText::FromString(NodeErrorTooltip)); + ErrorText->SetColorAndOpacity(NodeErrorColor); + + // Hide the combobox if we have an error + ComboBoxTOPNode->SetVisibility(NodeErrorText.IsEmpty() ? EVisibility::Visible : EVisibility::Hidden); + } + + // TOP Node State + if(!bIsPCG) + { + FDetailWidgetRow& PDGNodeStateResultRow = TOPNodesGrp.AddWidgetRow(); + DisableIfPDGNotLinked(PDGNodeStateResultRow, InPDGAssetLink); + PDGNodeStateResultRow.NameWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("TOP Node State"))) + ]; + + PDGNodeStateResultRow.ValueWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + [ + SNew(STextBlock) + .Text_Lambda([InPDGAssetLink]() + { + FString TOPNodeStatus = FString(); + FLinearColor TOPNodeStatusColor = FLinearColor::White; + GetSelectedTOPNodeStatusAndColor(InPDGAssetLink, TOPNodeStatus, TOPNodeStatusColor); + return FText::FromString(TOPNodeStatus); + }) + .ColorAndOpacity_Lambda([InPDGAssetLink]() + { + FString TOPNodeStatus = FString(); + FLinearColor TOPNodeStatusColor = FLinearColor::White; + GetSelectedTOPNodeStatusAndColor(InPDGAssetLink, TOPNodeStatus, TOPNodeStatusColor); + return FSlateColor(TOPNodeStatusColor); + }) + ]; + } + + // Checkbox: Load Work Item Output Files + if(!bIsPCG) + { + auto ToolTipLambda = [InPDGAssetLink]() + { + bool bDisabled = false; + if (IsValidWeakPointer(InPDGAssetLink) && InPDGAssetLink->GetSelectedTOPNode()) + { + bDisabled = InPDGAssetLink->GetSelectedTOPNode()->bHasChildNodes; + } + + return bDisabled + ? FText::FromString(TEXT("This node has child nodes, the auto-load setting must be set on the child nodes individually.")) + : FText::FromString(TEXT("When enabled, Output files produced by this TOP Node's Work Items will automatically be loaded when cooked.")); + }; + FDetailWidgetRow& PDGNodeAutoLoadRow = TOPNodesGrp.AddWidgetRow(); + + DisableIfPDGNotLinked(PDGNodeAutoLoadRow, InPDGAssetLink); + PDGNodeAutoLoadRow.IsEnabledAttr.Bind(TAttribute::FGetter::CreateLambda([InPDGAssetLink]() + { + if (!IsPDGLinked(InPDGAssetLink)) + return false; + UTOPNode* const Node = InPDGAssetLink->GetSelectedTOPNode(); + if (IsValid(Node) && !Node->bHidden && !Node->bHasChildNodes) + return true; + return false; + })); + + PDGNodeAutoLoadRow.NameWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Auto-Load Work Item Output Files"))) + .ToolTipText_Lambda(ToolTipLambda) + ]; + + TSharedPtr AutoLoadCheckBox; + + PDGNodeAutoLoadRow.ValueWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + [ + // Checkbox + SAssignNew(AutoLoadCheckBox, SCheckBox) + .IsChecked_Lambda([InPDGAssetLink]() + { + return InPDGAssetLink->GetSelectedTOPNode() + ? (InPDGAssetLink->GetSelectedTOPNode()->bAutoLoad ? ECheckBoxState::Checked : ECheckBoxState::Unchecked) + : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InPDGAssetLink](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked) ? true : false; + UTOPNode* TOPNode = InPDGAssetLink->GetSelectedTOPNode(); + if (!IsValid(TOPNode) || TOPNode->bAutoLoad == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + TOPNode); + + TOPNode->Modify(); + TOPNode->bAutoLoad = bNewState; + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UTOPNode, bAutoLoad), TOPNode); + + // FHoudiniPDGDetails::RefreshUI(InPDGAssetLink); + }) + .ToolTipText_Lambda(ToolTipLambda) + ]; + } + + // Checkbox: Work Item Output Files Visible + { + auto ToolTipLambda = [InPDGAssetLink]() + { + bool bDisabled = false; + if (IsValidWeakPointer(InPDGAssetLink) && InPDGAssetLink->GetSelectedTOPNode()) + { + bDisabled = InPDGAssetLink->GetSelectedTOPNode()->bHasChildNodes; + } + + return bDisabled + ? FText::FromString(TEXT("This node has child nodes, visibility of work item outputs must be set on the child nodes individually.")) + : FText::FromString(TEXT("Toggles the visibility of the actors created from this TOP Node's Work Item File Outputs.")); + }; + + FDetailWidgetRow& PDGNodeShowResultRow = TOPNodesGrp.AddWidgetRow(); + // DisableIfPDGNotLinked(PDGNodeShowResultRow, InPDGAssetLink); + PDGNodeShowResultRow.IsEnabledAttr.Bind(TAttribute::FGetter::CreateLambda([InPDGAssetLink]() + { + if (!IsValidWeakPointer(InPDGAssetLink)) + return false; + + UTOPNode* const Node = InPDGAssetLink->GetSelectedTOPNode(); + if (IsValid(Node) && !Node->bHidden && !Node->bHasChildNodes) + return true; + + return false; + })); + PDGNodeShowResultRow.NameWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Work Item Output Files Visible"))) + .ToolTipText_Lambda(ToolTipLambda) + ]; + + TSharedPtr ShowResCheckBox; + PDGNodeShowResultRow.ValueWidget.Widget = + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + [ + // Checkbox + SAssignNew(ShowResCheckBox, SCheckBox) + .IsChecked_Lambda([InPDGAssetLink]() + { + return InPDGAssetLink->GetSelectedTOPNode() + ? (InPDGAssetLink->GetSelectedTOPNode()->IsVisibleInLevel() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked) + : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InPDGAssetLink](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked) ? true : false; + UTOPNode* const TOPNode = InPDGAssetLink->GetSelectedTOPNode(); + if (!IsValid(TOPNode) || TOPNode->IsVisibleInLevel() == bNewState) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + TOPNode); + + TOPNode->Modify(); + TOPNode->SetVisibleInLevel(bNewState); + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty(TEXT("bShow"), TOPNode); + // FHoudiniPDGDetails::RefreshUI(InPDGAssetLink); + }) + .ToolTipText_Lambda(ToolTipLambda) + ]; + } + + // Buttons: DIRTY NODE / COOK NODE + if (!bIsPCG) + { + TSharedRef DirtyHBox = SNew(SHorizontalBox); + TSharedPtr CookHBox = SNew(SHorizontalBox); + + TSharedPtr DirtyButton; + TSharedPtr CookButton; + + FDetailWidgetRow& PDGDirtyCookRow = TOPNodesGrp.AddWidgetRow() + .WholeRowContent() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SBox) + .IsEnabled_Lambda([InPDGAssetLink]() + { + return IsPDGLinked(InPDGAssetLink) || (IsValidWeakPointer(InPDGAssetLink) && IsValid(InPDGAssetLink->GetSelectedTOPNode())); + }) + .WidthOverride(200.0f) + [ + SAssignNew(DirtyButton, SButton) + //.Text(LOCTEXT("DirtyNode", "Dirty Node")) + .ToolTipText(LOCTEXT("DirtyNodeTooltip", "Dirties the selected TOP node and clears its work item results.")) + .ContentPadding(FMargin(5.0f, 2.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled_Lambda([InPDGAssetLink]() + { + return IsPDGLinked(InPDGAssetLink) || (IsValidWeakPointer(InPDGAssetLink) && IsValid(InPDGAssetLink->GetSelectedTOPNode())); + }) + .OnClicked_Lambda([InPDGAssetLink]() + { + if (IsValidWeakPointer(InPDGAssetLink)) + { + UTOPNode* const TOPNode = InPDGAssetLink->GetSelectedTOPNode(); + if (IsValid(TOPNode)) + { + if (IsPDGLinked(InPDGAssetLink)) + { + FHoudiniPDGManager::DirtyTOPNode(TOPNode); + // FHoudiniPDGDetails::RefreshUI(InPDGAssetLink); + } + else + { + UHoudiniPDGAssetLink::ClearTOPNodeWorkItemResults(TOPNode); + } + } + } + + return FReply::Handled(); + }) + .IsEnabled_Lambda([InPDGAssetLink]() + { + if (IsValid(InPDGAssetLink->GetSelectedTOPNode()) && !InPDGAssetLink->GetSelectedTOPNode()->bHidden) + return true; + return false; + }) + .Content() + [ + SAssignNew(DirtyHBox, SHorizontalBox) + ] + ] + ] + // + SHorizontalBox::Slot() + // .AutoWidth() + // [ + // SNew(SBox) + // .WidthOverride(200.0f) + // [ + // SAssignNew(DirtyButton, SButton) + // .Text(LOCTEXT("DirtyAllTasks", "Dirty All Tasks")) + // .ToolTipText(LOCTEXT("DirtyAllTasksTooltip", "Dirties all tasks/work items on the selected TOP node.")) + // .ContentPadding(FMargin(5.0f, 2.0f)) + // .VAlign(VAlign_Center) + // .HAlign(HAlign_Center) + // .OnClicked_Lambda([InPDGAssetLink]() + // { + // if(InPDGAssetLink->GetSelectedTOPNode()) + // { + // FHoudiniPDGManager::DirtyAllTasksOfTOPNode(*(InPDGAssetLink->GetSelectedTOPNode())); + // FHoudiniPDGDetails::RefreshUI(InPDGAssetLink); + // } + // + // return FReply::Handled(); + // }) + // ] + // ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SBox) + .IsEnabled_Lambda([InPDGAssetLink]() { return IsPDGLinked(InPDGAssetLink); }) + .WidthOverride(200.0f) + [ + SAssignNew(CookButton, SButton) + //.Text(LOCTEXT("CookNode", "Cook Node")) + .ToolTipText(LOCTEXT("CookNodeTooltip", "Cooks the selected TOP Node.")) + .ContentPadding(FMargin(5.0f, 2.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled_Lambda([InPDGAssetLink]() + { + if (!IsPDGLinked(InPDGAssetLink)) + return false; + UTOPNode* const SelectedNode = InPDGAssetLink->GetSelectedTOPNode(); + if (!IsValid(SelectedNode)) + return false; + // Disable Cook Node button if the node is already cooking + return !SelectedNode->bHidden && SelectedNode->NodeState != EPDGNodeState::Cooking && !SelectedNode->AnyWorkItemsPending(); + }) + .OnClicked_Lambda([InPDGAssetLink]() + { + UTOPNode* const Node = InPDGAssetLink->GetSelectedTOPNode(); + if (IsValid(Node)) + { + FHoudiniPDGManager::CookTOPNode(Node); + // FHoudiniPDGDetails::RefreshUI(InPDGAssetLink); + } + return FReply::Handled(); + }) + .Content() + [ + SAssignNew(CookHBox, SHorizontalBox) + ] + ] + ] + ]; + + TSharedPtr DirtyIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIPDGDirtyNodeIconBrush(); + if (DirtyIconBrush.IsValid()) + { + TSharedPtr DirtyImage; + DirtyHBox->AddSlot() + .MaxWidth(16.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(DirtyImage, SImage) + ] + ]; + + DirtyImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([DirtyIconBrush]() { return DirtyIconBrush.Get(); }))); + } + + DirtyHBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("DirtyNode", "Dirty Node")) + ]; + + TSharedPtr CookIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIRecookIconBrush(); + if (CookIconBrush.IsValid()) + { + TSharedPtr CookImage; + CookHBox->AddSlot() + .MaxWidth(16.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(CookImage, SImage) + ] + ]; + + CookImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([CookIconBrush]() { return CookIconBrush.Get(); }))); + } + + CookHBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(LOCTEXT("CookNode", "Cook Node")) + ]; + + DisableIfPDGNotLinked(PDGDirtyCookRow, InPDGAssetLink); + } + + // Buttons: Load Work Item Objects / Unload Work Item Objects + if (!bIsPCG) + { + TSharedPtr UnloadWorkItemsButton; + TSharedPtr LoadWorkItemsButton; + + FDetailWidgetRow& PDGUnloadLoadWorkItemsRow = TOPNodesGrp.AddWidgetRow() + .WholeRowContent() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SBox) + .IsEnabled_Lambda([InPDGAssetLink]() + { + return IsValidWeakPointer(InPDGAssetLink) && IsValid(InPDGAssetLink->GetSelectedTOPNode()); + }) + .WidthOverride(200.0f) + [ + SAssignNew(UnloadWorkItemsButton, SButton) + .Text(LOCTEXT("UnloadWorkItemsForNode", "Unload Work Item Objects")) + .ToolTipText(LOCTEXT("UnloadWorkItemsForNodeTooltip", "Unloads / removes loaded work item results from level. Not undoable: use the \"Load Work Item Objects\" button to reload the results.")) + .ContentPadding(FMargin(5.0f, 2.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled_Lambda([InPDGAssetLink]() + { + if (!IsValidWeakPointer(InPDGAssetLink)) + return false; + + UTOPNode* const SelectedNode = InPDGAssetLink->GetSelectedTOPNode(); + if (!IsValid(SelectedNode) || SelectedNode->bHidden || !SelectedNode->bCachedHaveLoadedWorkResults) + return false; + + return true; + }) + .OnClicked_Lambda([InPDGAssetLink]() + { + if (IsValidWeakPointer(InPDGAssetLink)) + { + UTOPNode* const TOPNode = InPDGAssetLink->GetSelectedTOPNode(); + if (IsValid(TOPNode)) + { + if (IsPDGLinked(InPDGAssetLink)) + { + // Set the state to ToDelete, PDGManager will delete it when processing work items + TOPNode->SetLoadedWorkResultsToDelete(); + } + else + { + // Delete and unload the result objects and actors now + TOPNode->DeleteAllWorkResultObjectOutputs(); + } + } + } + + return FReply::Handled(); + }) + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SBox) + .IsEnabled_Lambda([InPDGAssetLink]() { return IsPDGLinked(InPDGAssetLink); }) + .WidthOverride(200.0f) + [ + SAssignNew(LoadWorkItemsButton, SButton) + .Text(LOCTEXT("LoadWorkItems", "Load Work Item Objects")) + .ToolTipText(LOCTEXT("LoadWorkItemsForNodeTooltip", "Loads any available but not loaded work items objects (this could include items from a previous cook). Creates output actors. Not undoable: use the \"Unload Work Item Objects\" button to unload/remove loaded work item results.")) + .ContentPadding(FMargin(5.0f, 2.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled_Lambda([InPDGAssetLink]() + { + if (!IsValidWeakPointer(InPDGAssetLink)) + return false; + + UTOPNode* const SelectedNode = InPDGAssetLink->GetSelectedTOPNode(); + if (!IsValid(SelectedNode) || SelectedNode->bHidden || !SelectedNode->bCachedHaveNotLoadedWorkResults) + return false; + + return true; + }) + .OnClicked_Lambda([InPDGAssetLink]() + { + if (IsValidWeakPointer(InPDGAssetLink)) + { + UTOPNode* const SelectedNode = InPDGAssetLink->GetSelectedTOPNode(); + if (IsValid(SelectedNode)) + { + SelectedNode->SetNotLoadedWorkResultsToLoad(true); + } + } + return FReply::Handled(); + }) + ] + ] + ]; + } + + // TOP Node WorkItem Status + if(!bIsPCG) + { + if (InPDGAssetLink->GetSelectedTOPNode()) + { + FDetailWidgetRow& PDGNodeWorkItemStatsRow = TOPNodesGrp.AddWidgetRow(); + DisableIfPDGNotLinked(PDGNodeWorkItemStatsRow, InPDGAssetLink); + FHoudiniPDGDetails::AddWorkItemStatusWidget( + PDGNodeWorkItemStatsRow, TEXT("TOP Node Work Item Status"), InPDGAssetLink, true); + } + } +} + +void +FHoudiniPDGDetails::RefreshPDGAssetLink(const TWeakObjectPtr& InPDGAssetLink) +{ + // Repopulate the network and nodes for the assetlink + if (!IsValidWeakPointer(InPDGAssetLink) || !FHoudiniPDGManager::UpdatePDGAssetLink(InPDGAssetLink.Get())) + return; + + FHoudiniPDGDetails::RefreshUI(InPDGAssetLink, true); +} + +void +FHoudiniPDGDetails::RefreshUI(const TWeakObjectPtr& InPDGAssetLink, const bool& InFullUpdate) +{ + if (!IsValidWeakPointer(InPDGAssetLink)) + return; + + // Update the workitem stats + InPDGAssetLink->UpdateWorkItemTally(); + + // Update the editor properties + FHoudiniEngineUtils::UpdateEditorProperties(InFullUpdate); +} + +void +FHoudiniPDGDetails::CreatePDGBakeWidgets(IDetailCategoryBuilder& InPDGCategory, const TWeakObjectPtr& InPDGAssetLink, bool bIsPCG) +{ + if (!IsValidWeakPointer(InPDGAssetLink)) + return; + + FHoudiniEngineDetails::AddHeaderRowForHoudiniPDGAssetLink(InPDGCategory, InPDGAssetLink, HOUDINI_ENGINE_UI_SECTION_PDG_BAKE); + + if (!InPDGAssetLink->bBakeMenuExpanded) + return; + + auto OnBakeButtonClickedLambda = [InPDGAssetLink]() + { + switch (InPDGAssetLink->HoudiniEngineBakeOption) + { + case EHoudiniEngineBakeOption::ToActor: + { + // if (InPDGAssetLink->bIsReplace) + // FHoudiniEngineBakeUtils::ReplaceHoudiniActorWithActors(InPDGAssetLink); + // else + FHoudiniEngineBakeUtils::BakePDGAssetLinkOutputsKeepActors(InPDGAssetLink.Get(), InPDGAssetLink->PDGBakeSelectionOption, InPDGAssetLink->PDGBakePackageReplaceMode, InPDGAssetLink->bRecenterBakedActors); + } + break; + + case EHoudiniEngineBakeOption::ToBlueprint: + { + // if (InPDGAssetLink->bIsReplace) + // FHoudiniEngineBakeUtils::ReplaceWithBlueprint(InPDGAssetLink); + // else + FHoudiniEngineBakeUtils::BakePDGAssetLinkBlueprints(InPDGAssetLink.Get(), InPDGAssetLink->PDGBakeSelectionOption, InPDGAssetLink->PDGBakePackageReplaceMode, InPDGAssetLink->bRecenterBakedActors); + } + break; + + case EHoudiniEngineBakeOption::ToAsset: + { + // TODO + // This should not happen! + } + break; + } + + return FReply::Handled(); + }; + + auto OnBakeFolderTextCommittedLambda = [InPDGAssetLink](const FText& Val, ETextCommit::Type TextCommitType) + { + if (!IsValidWeakPointer(InPDGAssetLink)) + return; + + FString NewPathStr = Val.ToString(); + if (NewPathStr.IsEmpty()) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + //Todo? Check if the new Bake folder path is valid + InPDGAssetLink->Modify(); + InPDGAssetLink->BakeFolder.Path = NewPathStr; + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, BakeFolder), InPDGAssetLink.Get()); + }; + + // Button Row + FDetailWidgetRow & ButtonRow = InPDGCategory.AddCustomRow(FText::FromString("PDG Bake")); + DisableIfPDGNotLinked(ButtonRow, InPDGAssetLink); + + TSharedRef ButtonRowHorizontalBox = SNew(SHorizontalBox); + + // Bake Button + TSharedRef BakeHBox = SNew(SHorizontalBox); + TSharedPtr BakeButton; + ButtonRowHorizontalBox->AddSlot() + /*.AutoWidth()*/ + .Padding(15.f, 0.0f, 0.0f, 0.0f) + .MaxWidth(75.0f) + [ + SNew(SBox) + .WidthOverride(75.0f) + [ + SAssignNew(BakeButton, SButton) + //.Text(FText::FromString("Bake")) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + //.ToolTipText(LOCTEXT("HoudiniPDGDetailsBakeButton", "Bake the Houdini PDG TOP Node(s)")) + .ToolTipText_Lambda([InPDGAssetLink]() + { + switch (InPDGAssetLink->HoudiniEngineBakeOption) + { + case EHoudiniEngineBakeOption::ToActor: + { + return LOCTEXT( + "HoudiniEnginePDGBakeButtonBakeToActorToolTip", + "Bake this Houdini PDG Asset's output assets and seperate the output actors from the PDG asset link."); + } + break; + + case EHoudiniEngineBakeOption::ToBlueprint: + { + return LOCTEXT( + "HoudiniEnginePDGBakeButtonBakeToBlueprintToolTip", + "Bake this Houdini PDG Asset's output assets to blueprints and remove temporary output actors that no " + "longer has output components from the PDG asset link."); + } + break; + + case EHoudiniEngineBakeOption::ToAsset: + default: + { + return FText(); + } + } + }) + .Visibility(EVisibility::Visible) + .IsEnabled_Lambda([InPDGAssetLink]() { return IsPDGLinked(InPDGAssetLink); }) + .OnClicked_Lambda(OnBakeButtonClickedLambda) + .Content() + [ + SAssignNew(BakeHBox, SHorizontalBox) + ] + ] + ]; + + TSharedPtr BakeIconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIBakeIconBrush(); + if (BakeIconBrush.IsValid()) + { + TSharedPtr BakeImage; + BakeHBox->AddSlot() + .MaxWidth(16.0f) + [ + SNew(SBox) + .WidthOverride(16.0f) + .HeightOverride(16.0f) + [ + SAssignNew(BakeImage, SImage) + ] + ]; + + BakeImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([BakeIconBrush]() { return BakeIconBrush.Get(); }))); + } + + BakeHBox->AddSlot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(STextBlock) + .Text(FText::FromString("Bake")) + ]; + + // bake Type ComboBox + TSharedPtr>> TypeComboBox; + + TArray>* OptionSource = FHoudiniEngineEditor::Get().GetHoudiniEnginePDGBakeTypeOptionsLabels(); + TSharedPtr IntialSelec; + if (OptionSource) + { + // IntialSelec = (*OptionSource)[(int)InPDGAssetLink->HoudiniEngineBakeOption]; + const FString DefaultStr = FHoudiniEngineEditor::Get().GetStringFromHoudiniEngineBakeOption(InPDGAssetLink->HoudiniEngineBakeOption); + const TSharedPtr* DefaultOption = OptionSource->FindByPredicate( + [DefaultStr](TSharedPtr InStringPtr) + { + return InStringPtr.IsValid() && *InStringPtr == DefaultStr; + } + ); + if (DefaultOption) + IntialSelec = *DefaultOption; + } + + ButtonRowHorizontalBox->AddSlot() + /*.AutoWidth()*/ + .Padding(3.0, 0.0, 4.0f, 0.0f) + .MaxWidth(93.f) + [ + SNew(SBox) + .IsEnabled_Lambda([InPDGAssetLink]() { return IsPDGLinked(InPDGAssetLink); }) + .WidthOverride(93.f) + [ + SAssignNew(TypeComboBox, SComboBox>) + .OptionsSource(OptionSource) + .InitiallySelectedItem(IntialSelec) + .OnGenerateWidget_Lambda( + [](TSharedPtr< FString > InItem) + { + FText ChoiceEntryText = FText::FromString(*InItem); + return SNew(STextBlock) + .Text(ChoiceEntryText) + .ToolTipText(ChoiceEntryText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + }) + .OnSelectionChanged_Lambda( + [InPDGAssetLink](TSharedPtr< FString > NewChoice, ESelectInfo::Type SelectType) + { + if (!IsValidWeakPointer(InPDGAssetLink)) + return; + + if (!NewChoice.IsValid() || SelectType == ESelectInfo::Type::Direct) + return; + + const EHoudiniEngineBakeOption NewOption = + FHoudiniEngineEditor::Get().StringToHoudiniEngineBakeOption(*NewChoice.Get()); + + if (NewOption != InPDGAssetLink->HoudiniEngineBakeOption) + { + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + InPDGAssetLink->Modify(); + InPDGAssetLink->HoudiniEngineBakeOption = NewOption; + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, HoudiniEngineBakeOption), InPDGAssetLink.Get()); + } + }) + [ + SNew(STextBlock) + .Text_Lambda([InPDGAssetLink, TypeComboBox, OptionSource]() + { + return FText::FromString(FHoudiniEngineEditor::Get().GetStringFromHoudiniEngineBakeOption(InPDGAssetLink->HoudiniEngineBakeOption)); + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + ]; + + // bake selection ComboBox + TSharedPtr>> BakeSelectionComboBox; + + TArray>* PDGBakeSelectionOptionSource = FHoudiniEngineEditor::Get().GetHoudiniEnginePDGBakeSelectionOptionsLabels(); + TSharedPtr PDGBakeSelectionIntialSelec; + if (PDGBakeSelectionOptionSource) + { + PDGBakeSelectionIntialSelec = (*PDGBakeSelectionOptionSource)[(int)InPDGAssetLink->PDGBakeSelectionOption]; + } + + ButtonRowHorizontalBox->AddSlot() + /*.AutoWidth()*/ + .Padding(3.0, 0.0, 4.0f, 0.0f) + .MaxWidth(163.f) + [ + SNew(SBox) + .IsEnabled_Lambda([InPDGAssetLink]() { return IsPDGLinked(InPDGAssetLink); }) + .WidthOverride(163.f) + [ + SAssignNew(TypeComboBox, SComboBox>) + .OptionsSource(PDGBakeSelectionOptionSource) + .InitiallySelectedItem(PDGBakeSelectionIntialSelec) + .OnGenerateWidget_Lambda( + [](TSharedPtr< FString > InItem) + { + FText ChoiceEntryText = FText::FromString(*InItem); + return SNew(STextBlock) + .Text(ChoiceEntryText) + .ToolTipText(ChoiceEntryText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + }) + .OnSelectionChanged_Lambda( + [InPDGAssetLink](TSharedPtr< FString > NewChoice, ESelectInfo::Type SelectType) + { + if (!IsValidWeakPointer(InPDGAssetLink) || !NewChoice.IsValid()) + return; + + const EPDGBakeSelectionOption NewOption = + FHoudiniEngineEditor::Get().StringToPDGBakeSelectionOption(*NewChoice.Get()); + + if (NewOption != InPDGAssetLink->PDGBakeSelectionOption) + { + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + InPDGAssetLink->Modify(); + InPDGAssetLink->PDGBakeSelectionOption = NewOption; + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, PDGBakeSelectionOption), InPDGAssetLink.Get()); + } + }) + [ + SNew(STextBlock) + .Text_Lambda([InPDGAssetLink]() + { + return FText::FromString( + FHoudiniEngineEditor::Get().GetStringFromPDGBakeTargetOption(InPDGAssetLink->PDGBakeSelectionOption)); + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + ]; + + ButtonRow.WholeRowWidget.Widget = ButtonRowHorizontalBox; + + // Bake package replacement mode row + FDetailWidgetRow & BakePackageReplaceRow = InPDGCategory.AddCustomRow(FText::FromString("PDG Bake Replace Options")); + DisableIfPDGNotLinked(BakePackageReplaceRow, InPDGAssetLink); + + TSharedRef BakePackageReplaceRowHorizontalBox = SNew(SHorizontalBox); + + BakePackageReplaceRowHorizontalBox->AddSlot() + /*.AutoWidth()*/ + .Padding(30.0f, 0.0f, 6.0f, 0.0f) + .MaxWidth(155.0f) + [ + SNew(SBox) + .IsEnabled_Lambda([InPDGAssetLink]() { return IsPDGLinked(InPDGAssetLink); }) + .WidthOverride(155.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniEnginePDGBakePackageReplacementModeLabel", "Replace Mode")) + .ToolTipText( + LOCTEXT("HoudiniEnginePDGBakePackageReplacementModeTooltip", "Replacement mode " + "during baking. Create new assets, using numerical suffixes in package names when necessary, or " + "replace existing assets with matching names. Also replaces the previous bake's output actors in " + "replacement mode vs creating incremental ones.")) + ] + ]; + + // bake package replace mode ComboBox + TSharedPtr>> BakePackageReplaceModeComboBox; + + TArray>* PDGBakePackageReplaceModeOptionSource = FHoudiniEngineEditor::Get().GetHoudiniEnginePDGBakePackageReplaceModeOptionsLabels(); + TSharedPtr PDGBakePackageReplaceModeInitialSelec; + if (PDGBakePackageReplaceModeOptionSource) + { + const FString DefaultStr = FHoudiniEngineEditor::Get().GetStringFromPDGBakePackageReplaceModeOption(InPDGAssetLink->PDGBakePackageReplaceMode); + const TSharedPtr* DefaultOption = PDGBakePackageReplaceModeOptionSource->FindByPredicate( + [DefaultStr](TSharedPtr InStringPtr) + { + return InStringPtr.IsValid() && *InStringPtr == DefaultStr; + } + ); + if (DefaultOption) + PDGBakePackageReplaceModeInitialSelec = *DefaultOption; + } + + BakePackageReplaceRowHorizontalBox->AddSlot() + /*.AutoWidth()*/ + .Padding(3.0, 0.0, 4.0f, 0.0f) + .MaxWidth(163.f) + [ + SNew(SBox) + .IsEnabled_Lambda([InPDGAssetLink]() { return IsPDGLinked(InPDGAssetLink); }) + .WidthOverride(163.f) + [ + SAssignNew(TypeComboBox, SComboBox>) + .OptionsSource(PDGBakePackageReplaceModeOptionSource) + .InitiallySelectedItem(PDGBakePackageReplaceModeInitialSelec) + .OnGenerateWidget_Lambda( + [](TSharedPtr< FString > InItem) + { + const FText ChoiceEntryText = FText::FromString(*InItem); + return SNew(STextBlock) + .Text(ChoiceEntryText) + .ToolTipText(ChoiceEntryText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))); + }) + .OnSelectionChanged_Lambda( + [InPDGAssetLink](TSharedPtr< FString > NewChoice, ESelectInfo::Type SelectType) + { + if (!IsValidWeakPointer(InPDGAssetLink) || !NewChoice.IsValid()) + return; + + const EPDGBakePackageReplaceModeOption NewOption = + FHoudiniEngineEditor::Get().StringToPDGBakePackageReplaceModeOption(*NewChoice.Get()); + + if (NewOption != InPDGAssetLink->PDGBakePackageReplaceMode) + { + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + InPDGAssetLink->Modify(); + InPDGAssetLink->PDGBakePackageReplaceMode = NewOption; + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, PDGBakePackageReplaceMode), InPDGAssetLink.Get()); + } + }) + [ + SNew(STextBlock) + .Text_Lambda([InPDGAssetLink]() + { + return FText::FromString( + FHoudiniEngineEditor::Get().GetStringFromPDGBakePackageReplaceModeOption(InPDGAssetLink->PDGBakePackageReplaceMode)); + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + ]; + + BakePackageReplaceRow.WholeRowWidget.Widget = BakePackageReplaceRowHorizontalBox; + + // Bake Folder Row + FDetailWidgetRow & BakeFolderRow = InPDGCategory.AddCustomRow(FText::FromString("PDG Bake Folder")); + DisableIfPDGNotLinked(BakeFolderRow, InPDGAssetLink); + + TSharedRef BakeFolderRowHorizontalBox = SNew(SHorizontalBox); + + BakeFolderRowHorizontalBox->AddSlot() + /*.AutoWidth()*/ + .Padding(30.0f, 0.0f, 6.0f, 0.0f) + .MaxWidth(155.0f) + [ + SNew(SBox) + .IsEnabled_Lambda([InPDGAssetLink]() { return IsPDGLinked(InPDGAssetLink); }) + .WidthOverride(155.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniEngineBakeFolderLabel", "Bake Folder")) + .ToolTipText(LOCTEXT( + "HoudiniEnginePDGBakeFolderTooltip", + "The folder used to store the objects that are generated by this Houdini PDG Asset when baking, if the " + "unreal_bake_folder attribute is not set on the geometry. If this value is blank, the default from the " + "plugin settings is used.")) + ] + ]; + + BakeFolderRowHorizontalBox->AddSlot() + /*.AutoWidth()*/ + .MaxWidth(235.0) + [ + SNew(SBox) + .IsEnabled_Lambda([InPDGAssetLink]() { return IsPDGLinked(InPDGAssetLink); }) + .WidthOverride(235.0f) + [ + SNew(SEditableTextBox) + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + .ToolTipText(LOCTEXT( + "HoudiniEnginePDGBakeFolderTooltip", + "The folder used to store the objects that are generated by this Houdini PDG Asset when baking, if the " + "unreal_bake_folder attribute is not set on the geometry. If this value is blank, the default from the " + "plugin settings is used.")) + .HintText(LOCTEXT("HoudiniEngineBakeFolderHintText", "Input to set bake folder")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text_Lambda([InPDGAssetLink](){ return FText::FromString(InPDGAssetLink->BakeFolder.Path); }) + .OnTextCommitted_Lambda(OnBakeFolderTextCommittedLambda) + ] + ]; + + BakeFolderRow.WholeRowWidget.Widget = BakeFolderRowHorizontalBox; + + // Add additional bake options + FDetailWidgetRow & AdditionalBakeSettingsRow = InPDGCategory.AddCustomRow(FText::FromString("PDG Bake Options")); + TSharedRef AdditionalBakeSettingsRowHorizontalBox = SNew(SHorizontalBox); + + TSharedPtr CheckBoxAutoBake; + TSharedPtr CheckBoxAutoBakeWithFailedWorkItems; + TSharedPtr CheckBoxRecenterBakedActors; + + TSharedPtr LeftColumnVerticalBox; + TSharedPtr RightColumnVerticalBox; + + AdditionalBakeSettingsRowHorizontalBox->AddSlot() + .Padding(30.0f, 5.0f, 0.0f, 0.0f) + .MaxWidth(200.f) + [ + SNew(SBox) + .WidthOverride(200.f) + [ + SAssignNew(LeftColumnVerticalBox, SVerticalBox) + ] + ]; + + AdditionalBakeSettingsRowHorizontalBox->AddSlot() + .Padding(20.0f, 5.0f, 0.0f, 0.0f) + .MaxWidth(200.f) + [ + SNew(SBox) + [ + SAssignNew(RightColumnVerticalBox, SVerticalBox) + ] + ]; + + LeftColumnVerticalBox->AddSlot() + .AutoHeight() + .Padding(0.0f, 0.0f, 0.0f, 3.5f) + [ + SNew(SBox) + .WidthOverride(160.f) + [ + SAssignNew(CheckBoxRecenterBakedActors, SCheckBox) + .Content() + [ + SNew(STextBlock).Text(LOCTEXT("HoudiniEngineUIRecenterBakedActorsCheckBox", "Recenter Baked Actors")) + .ToolTipText(LOCTEXT("HoudiniEngineUIRecenterBakedActorsCheckBoxToolTip", "After baking recenter the baked actors to their bounding box center.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([InPDGAssetLink]() + { + return InPDGAssetLink->bRecenterBakedActors ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InPDGAssetLink](ECheckBoxState NewState) + { + if (!IsValidWeakPointer(InPDGAssetLink)) + return; + + const bool bNewState = (NewState == ECheckBoxState::Checked); + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + InPDGAssetLink->Modify(); + InPDGAssetLink->bRecenterBakedActors = bNewState; + + // Notify that we have changed the property + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, bRecenterBakedActors), InPDGAssetLink.Get()); + }) + ] + ]; + + if(!bIsPCG) + { + RightColumnVerticalBox->AddSlot() + .AutoHeight() + .Padding(0.0f, 0.0f, 0.0f, 3.5f) + [ + SNew(SBox) + .WidthOverride(160.f) + [ + SAssignNew(CheckBoxAutoBake, SCheckBox) + .Content() + [ + SNew(STextBlock).Text(LOCTEXT("HoudiniEngineUIAutoBakeCheckBox", "Auto Bake")) + .ToolTipText(LOCTEXT("HoudiniEngineUIAutoBakeCheckBoxToolTip", "Automatically bake work result objects as they are loaded.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([InPDGAssetLink]() + { + return InPDGAssetLink->bBakeAfterAllWorkResultObjectsLoaded ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InPDGAssetLink](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked); + + if(!IsValidWeakPointer(InPDGAssetLink)) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + InPDGAssetLink->Modify(); + InPDGAssetLink->bBakeAfterAllWorkResultObjectsLoaded = bNewState; + + // Notify that we have changed the property + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, bBakeAfterAllWorkResultObjectsLoaded), InPDGAssetLink.Get()); + }) + ] + ]; + + RightColumnVerticalBox->AddSlot() + .AutoHeight() + .Padding(0.0f, 0.0f, 0.0f, 3.5f) + [ + SNew(SBox) + .WidthOverride(160.f) + [ + SAssignNew(CheckBoxAutoBakeWithFailedWorkItems, SCheckBox) + .Content() + [ + SNew(STextBlock).Text(LOCTEXT("HoudiniEngineUIAutoBakeCheckBoxWithFailedWorkItems", "Auto Bake With Failed Work Items")) + .ToolTipText(LOCTEXT("HoudiniEngineUIAutoBakeCheckBoxWithFailedWorkItemsToolTip", "Automatically bake work result objects as they are loaded even for nodes with failed work items.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsEnabled_Lambda([InPDGAssetLink]() + { + if(!IsValidWeakPointer(InPDGAssetLink)) + return false; + return InPDGAssetLink->bBakeAfterAllWorkResultObjectsLoaded; + }) + .IsChecked_Lambda([InPDGAssetLink]() + { + return InPDGAssetLink->IsAutoBakeNodesWithFailedWorkItemsEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([InPDGAssetLink](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked); + + if(!IsValidWeakPointer(InPDGAssetLink)) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniPDGAssetLinkParameterChange", "Houdini PDG Asset Link Parameter: Changing a value"), + InPDGAssetLink.Get()); + + InPDGAssetLink->Modify(); + InPDGAssetLink->SetAutoBakeNodesWithFailedWorkItemsEnabled(bNewState); + + // Notify that we have changed the property + FHoudiniEngineEditorUtils::NotifyPostEditChangeProperty( + UHoudiniPDGAssetLink::GetbAutoBakeNodesWithFailedWorkItemsPropertyName(), InPDGAssetLink.Get()); + }) + ] + ]; + } + + + AdditionalBakeSettingsRow.WholeRowWidget.Widget = AdditionalBakeSettingsRowHorizontalBox; +} + +FTextAndTooltip::FTextAndTooltip(int32 InValue, const FString& InText) + : Text(InText) + , Value(InValue) +{ +} + +FTextAndTooltip::FTextAndTooltip(int32 InValue, const FString& InText, const FString &InToolTip) + : Text(InText) + , ToolTip(InToolTip) + , Value(InValue) +{ +} + +FTextAndTooltip::FTextAndTooltip(int32 InValue, FString&& InText) + : Text(InText) + , Value(InValue) +{ +} + +FTextAndTooltip::FTextAndTooltip(int32 InValue, FString&& InText, FString&& InToolTip) + : Text(InText) + , ToolTip(InToolTip) + , Value(InValue) +{ +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPDGDetails.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "Templates/SharedPointer.h" +#include "DetailWidgetRow.h" + +#include "HoudiniPDGAssetLink.h" +#include "HoudiniEngineDetails.h" + +class IDetailGroup; +class IDetailCategoryBuilder; + +struct FWorkItemTally; +enum class EPDGLinkState : uint8; +enum class EHoudiniBGEOCommandletStatus : uint8; + +// Convenience struct to hold a label and tooltip for widgets. +struct FTextAndTooltip +{ +public: + FTextAndTooltip(int32 InValue, const FString& InText); + FTextAndTooltip(int32 InValue, const FString& InText, const FString &InTooltip); + FTextAndTooltip(int32 InValue, FString&& InText); + FTextAndTooltip(int32 InValue, FString&& InText, FString&& InTooltip); + + FString Text; + + FString ToolTip; + + int32 Value; +}; + +class FHoudiniPDGDetails : public TSharedFromThis +{ + public: + + void CreateWidget( + IDetailCategoryBuilder & HouPDGCategory, + const TWeakObjectPtr&& InPDGAssetLink, + bool bIsPDG); + //UHoudiniAssetComponent* InHAC); + + void AddPDGAssetWidget( + IDetailCategoryBuilder& InPDGCategory, + const TWeakObjectPtr& InPDGAssetLink, + bool bIsPDG); + + void AddWorkItemStatusWidget( + FDetailWidgetRow& InRow, const FString& TitleString, const TWeakObjectPtr& InAssetLink, bool bInForSelectedNode); + + void AddPDGAssetStatus( + IDetailCategoryBuilder& InPDGCategory, const TWeakObjectPtr& InPDGAssetLink); + + void AddPDGCommandletStatus( + IDetailCategoryBuilder& InPDGCategory, const EHoudiniBGEOCommandletStatus& InCommandletStatus); + + void AddTOPNetworkWidget( + IDetailCategoryBuilder& InPDGCategory, const TWeakObjectPtr& InPDGAssetLink, bool bISPCG); + + void AddTOPNodeWidget( + IDetailGroup& InGroup, const TWeakObjectPtr& InPDGAssetLink, bool bIsPCG); + + static void RefreshPDGAssetLink( + const TWeakObjectPtr& InPDGAssetLink); + + static void RefreshUI( + const TWeakObjectPtr& InPDGAssetLink, const bool& InFullUpdate = true); + + static void + CreatePDGBakeWidgets(IDetailCategoryBuilder& InPDGCategory, const TWeakObjectPtr& InPDGAssetLink, bool bIsPCG); + protected: + // Helper function for getting the work item tally and color + static bool GetWorkItemTallyValueAndColor( + const TWeakObjectPtr& InAssetLink, bool bInForSelectedNode, const FString& InTallyItemString, + int32& OutValue, FLinearColor& OutColor); + + // Helper to get the status text for the selected TOP node, and the color with which to display it on the UI. + // Returns false if the InPDGAssetLink is invalid, or there is no selected TOP node. + static bool GetSelectedTOPNodeStatusAndColor( + const TWeakObjectPtr& InPDGAssetLink, FString& OutTOPNodeStatus, FLinearColor &OutTOPNodeStatusColor); + + // Helper to get asset link status and status color for UI + static bool GetPDGStatusAndColor( + const TWeakObjectPtr& InPDGAssetLink, FString& OutPDGStatusString, FLinearColor& OutPDGStatusColor); + + // Helper for getting the commandlet status text and color for the UI + static void GetPDGCommandletStatus(FString& OutStatusString, FLinearColor& OutStatusColor); + + // Helper to check if the asset link state is Linked + static FORCEINLINE bool IsPDGLinked(const TWeakObjectPtr& InPDGAssetLink) + { + return IsValidWeakPointer(InPDGAssetLink) && InPDGAssetLink->LinkState == EPDGLinkState::Linked; + } + + // Helper for binding IsPDGLinked to a TAttribute + static FORCEINLINE void BindDisableIfPDGNotLinked(TAttribute &InAttrToBind, const TWeakObjectPtr& InPDGAssetLink) + { + InAttrToBind.Bind( + TAttribute::FGetter::CreateLambda([InPDGAssetLink]() + { + return IsPDGLinked(InPDGAssetLink); + }) + ); + } + + // Helper to disable a UI row if InPDGAssetLink is not linked + static FORCEINLINE void DisableIfPDGNotLinked(FDetailWidgetRow& InRow, const TWeakObjectPtr& InPDGAssetLink) + { + BindDisableIfPDGNotLinked(InRow.IsEnabledAttr, InPDGAssetLink); + } + + private: + + TArray> TOPNetworksPtr; + + TArray> TOPNodesPtr; + +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniParameterDetails.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterDetails.h" + +#include "HoudiniAssetComponent.h" +#include "HoudiniParameter.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterString.h" +#include "HoudiniParameterColor.h" +#include "HoudiniParameterButton.h" +#include "HoudiniParameterButtonStrip.h" +#include "HoudiniParameterLabel.h" +#include "HoudiniParameterToggle.h" +#include "HoudiniParameterFile.h" +#include "HoudiniParameterChoice.h" +#include "HoudiniParameterFolder.h" +#include "HoudiniParameterFolderList.h" +#include "HoudiniParameterMultiParm.h" +#include "HoudiniParameterSeparator.h" +#include "HoudiniParameterRamp.h" +#include "HoudiniParameterOperatorPath.h" +#include "HoudiniInput.h" +#include "HoudiniAsset.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEnginePrivatePCH.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniEngineDetails.h" +#include "SNewFilePathPicker.h" + +#include "DetailCategoryBuilder.h" +#include "DetailLayoutBuilder.h" +#include "IDetailGroup.h" +#include "IDetailCustomization.h" +#include "PropertyCustomizationHelpers.h" +#include "DetailWidgetRow.h" +#include "Math/UnitConversion.h" +#include "ScopedTransaction.h" +#include "EditorDirectories.h" + +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Input/SVectorInputBox.h" +#include "Widgets/Input/SNumericEntryBox.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Input/SMultiLineEditableTextBox.h" +#include "Widgets/Colors/SColorPicker.h" +#include "Widgets/Views/SExpanderArrow.h" +#include "Widgets/Layout/SExpandableArea.h" +#include "Widgets/Views/STableRow.h" +#include "Widgets/Input/NumericUnitTypeInterface.inl" +#include "Widgets/Images/SImage.h" +#include "Widgets/Layout/SSeparator.h" +#include "Widgets/Layout/SSplitter.h" +#include "SCurveEditorView.h" +#include "SAssetDropTarget.h" +#include "AssetThumbnail.h" + +#include "SHoudiniColorRamp.h" +#include "SHoudiniFloatRamp.h" + +#include "Sound/SoundBase.h" +#include "Engine/SkeletalMesh.h" +#include "Particles/ParticleSystem.h" +#include "FoliageType.h" + +#include "HoudiniInputDetails.h" + +#include "Framework/SlateDelegates.h" +#include "Templates/SharedPointer.h" +#include "DetailCategoryBuilder.h" + + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +// Copied from SDetailSingleItemRow.cpp +namespace DetailWidgetConstants +{ +const FMargin LeftRowPadding(20.0f, 0.0f, 10.0f, 0.0f); +const FMargin RightRowPadding(12.0f, 0.0f, 2.0f, 0.0f); +} + + +int32 +SCustomizedButton::OnPaint( + const FPaintArgs& Args, + const FGeometry& AllottedGeometry, + const FSlateRect& MyClippingRect, + FSlateWindowElementList& OutDrawElements, + int32 LayerId, + const FWidgetStyle& InWidgetStyle, + bool bParentEnabled) const +{ + TSharedPtr Content = GetContent(); + + // 0. Initialize Line Buffer. + TArray Line; + Line.SetNum(2); + + // Initialize Color buffer. + FLinearColor Color = FLinearColor::White; + + // 1. Draw the radio button. + if (bIsRadioButton) + { + // Construct the radio button circles exactly once, + // All radio buttons share the same circles then + if (FHoudiniEngineEditor::Get().GetHoudiniParameterRadioButtonPointsOuter().Num() != HOUDINI_RADIO_BUTTON_CIRCLE_SAMPLES_NUM_OUTER || + FHoudiniEngineEditor::Get().GetHoudiniParameterRadioButtonPointsInner().Num() != HOUDINI_RADIO_BUTTON_CIRCLE_SAMPLES_NUM_INNER) + { + ConstructRadioButtonCircles(); + } + + DrawRadioButton(AllottedGeometry, OutDrawElements, LayerId, bChosen); + } + + // 2. Draw background color (if selected) + if (bChosen) + { + Line[0].X = AllottedGeometry.Size.X - AllottedGeometry.Size.Y / 2.0f + 10.0f; + Line[0].Y = Content->GetDesiredSize().Y / 2.0f; + Line[1].X = AllottedGeometry.Size.Y / 2.0f - 10.0f; + Line[1].Y = Content->GetDesiredSize().Y / 2.0f; + + Color = FLinearColor::White; + Color.A = bIsRadioButton ? 0.05 : 0.1; + + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Line, + ESlateDrawEffect::None, Color, true, AllottedGeometry.Size.Y - 10.0f); + } + + // 3. Drawing square around the text + { + // Switch the point order for each line to save few value assignment cycles + Line[0].X = 0.0f; + Line[0].Y = 0.0f; + Line[1].X = 0.0f; + Line[1].Y = Content->GetDesiredSize().Y; + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Line, + ESlateDrawEffect::None, FLinearColor::Black, true, 1.0f); + + //Line[0].X = 0.0f; + //Line[0].Y = Content->GetDesiredSize().Y; + Line[0].X = AllottedGeometry.Size.X; + Line[0].Y = Content->GetDesiredSize().Y; + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Line, + ESlateDrawEffect::None, bChosen ? FLinearColor::Gray : FLinearColor::Black, true, 1.0f); + + //Line[0].X = AllottedGeometry.Size.X; + //Line[0].Y = Content->GetDesiredSize().Y; + Line[1].X = AllottedGeometry.Size.X; + Line[1].Y = 0.0f; + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Line, + ESlateDrawEffect::None, FLinearColor::Black, true, 1.0f); /* draw gray bottom line if this tab is selected, black otherwise*/ + + //Line[0].X = AllottedGeometry.Size.X; + //Line[0].Y = 0.0f; + Line[0].X = 0.0f; + Line[0].Y = 0.0f; + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Line, + ESlateDrawEffect::None, FLinearColor::Black, true, 1.0f); + } + + // 4. Draw child widget + Content->Paint(Args, AllottedGeometry, MyClippingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); + + return LayerId; +}; + +void +SCustomizedButton::ConstructRadioButtonCircles() const +{ + TArray& OuterPoints = FHoudiniEngineEditor::Get().GetHoudiniParameterRadioButtonPointsOuter(); + TArray& InnerPoints = FHoudiniEngineEditor::Get().GetHoudiniParameterRadioButtonPointsInner(); + OuterPoints.Empty(); + InnerPoints.Empty(); + + OuterPoints.SetNumZeroed(HOUDINI_RADIO_BUTTON_CIRCLE_SAMPLES_NUM_OUTER); + InnerPoints.SetNumZeroed(8); + + // Construct outer circle + int32 CurDegree = 0; + int32 DegStep = 360 / HOUDINI_RADIO_BUTTON_CIRCLE_SAMPLES_NUM_OUTER; + + for (int32 Idx = 0; Idx < HOUDINI_RADIO_BUTTON_CIRCLE_SAMPLES_NUM_OUTER; ++Idx) + { + OuterPoints[Idx].X = HOUDINI_RADIO_BUTTON_CIRCLE_CENTER_X + + HOUDINI_RADIO_BUTTON_CIRCLE_RADIUS_OUTER * FMath::Sin(FMath::DegreesToRadians(CurDegree)); + OuterPoints[Idx].Y = HOUDINI_RADIO_BUTTON_CIRCLE_CENTER_X + + HOUDINI_RADIO_BUTTON_CIRCLE_RADIUS_OUTER * FMath::Cos(FMath::DegreesToRadians(CurDegree)); + + CurDegree += DegStep; + } + + // Construct inner circle + CurDegree = 0; + DegStep = 360 / HOUDINI_RADIO_BUTTON_CIRCLE_SAMPLES_NUM_INNER; + for (int32 Idx = 0; Idx < 8; ++Idx) + { + InnerPoints[Idx].X = HOUDINI_RADIO_BUTTON_CIRCLE_CENTER_X + + HOUDINI_RADIO_BUTTON_CIRCLE_RADIUS_INNER * FMath::Sin(FMath::DegreesToRadians(CurDegree)); + InnerPoints[Idx].Y = HOUDINI_RADIO_BUTTON_CIRCLE_CENTER_X + + HOUDINI_RADIO_BUTTON_CIRCLE_RADIUS_INNER * FMath::Cos(FMath::DegreesToRadians(CurDegree)); + + CurDegree += DegStep; + } +} + +void +SCustomizedButton::DrawRadioButton( + const FGeometry& AllottedGeometry, + FSlateWindowElementList& OutDrawElements, + int32 LayerId, + const bool& bSelected) const +{ + TArray& OuterPoints = FHoudiniEngineEditor::Get().GetHoudiniParameterRadioButtonPointsOuter(); + TArray& InnerPoints = FHoudiniEngineEditor::Get().GetHoudiniParameterRadioButtonPointsInner(); + if (OuterPoints.Num() <= 1 || InnerPoints.Num() <= 1) + return; + + FLinearColor ColorNonSelected = FLinearColor::White; + FLinearColor ColorSelected = FLinearColor::Yellow; + + // initialize line buffer + TArray Line; + Line.SetNumZeroed(2); + bool alternator = false; + + // Draw outer circle + Line[0] = OuterPoints.Last(); + for (int32 Idx = 0; Idx < OuterPoints.Num(); ++Idx) + { + // alternate the points order each time to some some assignment cycles + if (alternator) + { + Line[0].X = OuterPoints[Idx].X; + Line[0].Y = OuterPoints[Idx].Y; + } + else + { + Line[1].X = OuterPoints[Idx].X; + Line[1].Y = OuterPoints[Idx].Y; + } + + alternator = !alternator; + + // Draw a line segment + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Line, + ESlateDrawEffect::None, ColorNonSelected, true, 1.0f); + } + + // Draw inner circle + alternator = false; + Line[0] = InnerPoints.Last(); + for (int32 Idx = 0; Idx < InnerPoints.Num(); ++Idx) + { + // alternate the points order each time to some some assignment cycles + if (alternator) + { + Line[0].X = InnerPoints[Idx].X; + Line[0].Y = InnerPoints[Idx].Y; + } + else + { + Line[1].X = InnerPoints[Idx].X; + Line[1].Y = InnerPoints[Idx].Y; + } + + alternator = !alternator; + + // Draw a line segment + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Line, + ESlateDrawEffect::None, bSelected ? ColorSelected : ColorNonSelected, true, 3.0f); + } +} + +void +SCustomizedBox::SetHoudiniParameter( + const TArray>& InParams) +{ + if (InParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = InParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + + const bool bIsMultiparmInstanceHeader = MainParam->IsDirectChildOfMultiParm() && MainParam->GetChildIndex() == 0; + + switch (MainParam->GetParameterType()) + { + case EHoudiniParameterType::Button: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_BUTTON_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_BUTTON; + } + break; + + case EHoudiniParameterType::ButtonStrip: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_BUTTONSTRIP_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_BUTTONSTRIP; + } + break; + + case EHoudiniParameterType::Color: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_COLOR_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_COLOR; + } + break; + + case EHoudiniParameterType::ColorRamp: + { + UHoudiniParameterRampColor const* const ColorRampParameter = Cast(MainParam.Get()); + if (!IsValid(ColorRampParameter)) + return; + + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_COLORRAMP; + if (ColorRampParameter->CachedPoints.Num() > 0) + MarginHeight = MarginHeight + HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_COLORRAMP_INSTANCE * (float)(ColorRampParameter->CachedPoints.Num() - 1); + } + break; + + case EHoudiniParameterType::File: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILE_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILE; + } + break; + + case EHoudiniParameterType::FileDir: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILEDIR_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILEDIR; + } + break; + + case EHoudiniParameterType::FileGeo: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILEGEO_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILEGEO; + } + break; + + case EHoudiniParameterType::FileImage: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILEIMAGE_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FILEIMAGE; + } + break; + + case EHoudiniParameterType::Float: + { + if (MainParam->GetTupleSize() == 3) + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOAT_VEC3_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOAT_VEC3; + } + else + { + if (bIsMultiparmInstanceHeader) + { + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOAT_MULTIPARMHEADER + + (MainParam->GetTupleSize() - 1) * HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOAT_INSTANCE_MULTIPARMHEADER; + } + else + { + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOAT + + (MainParam->GetTupleSize() - 1)* HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOAT_INSTANCE; + } + } + } + break; + + case EHoudiniParameterType::FloatRamp: + { + UHoudiniParameterRampFloat * FloatRampParameter = Cast(MainParam); + if (!IsValid(FloatRampParameter)) + return; + + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOATRAMP; + + if (FloatRampParameter->CachedPoints.Num() > 0) + MarginHeight = MarginHeight + Houdini_PARAMETER_UI_ROW_MARGIN_HEIGHT_FLOATRAMP_INSTANCE * (float)(FloatRampParameter->CachedPoints.Num() - 1); + } + break; + + case EHoudiniParameterType::Folder: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FOLDER_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FOLDER; + } + break; + + case EHoudiniParameterType::FolderList: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FOLDERLIST_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_FOLDERLIST; + } + break; + + case EHoudiniParameterType::Input: + { + UHoudiniParameterOperatorPath* InputParam = Cast(MainParam); + if (!IsValid(InputParam) || !InputParam->HoudiniInput.IsValid()) + break; + + UHoudiniInput* Input = InputParam->HoudiniInput.Get(); + if (!IsValid(Input)) + break; + + if (bIsMultiparmInstanceHeader) + { + switch (Input->GetInputType()) + { + case EHoudiniInputType::Curve: + { + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_CURVE_MULTIPARMHEADER + + Input->GetNumberOfInputObjects() * HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_CURVE_INSTANCE_MULTIPARMHEADER; + } + break; + + case EHoudiniInputType::Geometry: + case EHoudiniInputType::World: + default: + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_MULTIPARMHEADER; + break; + } + } + else + { + switch (Input->GetInputType()) + { + case EHoudiniInputType::Curve: + { + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_CURVE + + Input->GetNumberOfInputObjects() * HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT_CURVE_INSTANCE; + } + break; + + case EHoudiniInputType::Geometry: + case EHoudiniInputType::World: + default: + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INPUT; + break; + + } + } + } + break; + + case EHoudiniParameterType::Int: + { + if (MainParam->GetTupleSize() == 3) + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INT_VEC3_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INT_VEC3; + } + else + { + if (bIsMultiparmInstanceHeader) + { + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INT_MULTIPARMHEADER + + (MainParam->GetTupleSize() - 1) * HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INT_INSTANCE_MULTIPARMHEADER; + } + else + { + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INT + + (MainParam->GetTupleSize() - 1) * HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INT_INSTANCE; + } + } + } + break; + + case EHoudiniParameterType::IntChoice: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INTCHOICE_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INTCHOICE; + } + break; + + case EHoudiniParameterType::Label: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_LABEL_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_LABEL; + } + break; + + case EHoudiniParameterType::MultiParm: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_MULTIPARM_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_MULTIPARM; + } + break; + + case EHoudiniParameterType::Separator: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_SEPARATOR_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_SEPARATOR; + bIsSeparator = true; + } + break; + + case EHoudiniParameterType::String: + { + if (bIsMultiparmInstanceHeader) + { + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRING_MULTIPARMHEADER + + (MainParam->GetTupleSize() - 1) * HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRING_INSTANCE_MULTIPARMHEADER; + } + else + { + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRING + + (MainParam->GetTupleSize() - 1) * HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRING_INSTANCE; + } + } + break; + + case EHoudiniParameterType::StringAssetRef: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRINGASSETREF_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRINGASSETREF; + } + break; + + case EHoudiniParameterType::StringChoice: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRINGCHOICE_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_STRINGCHOICE; + } + break; + + case EHoudiniParameterType::Toggle: + { + if (bIsMultiparmInstanceHeader) + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_TOGGLE_MULTIPARMHEADER; + else + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_TOGGLE; + } + break; + + case EHoudiniParameterType::Invalid: + { + MarginHeight = HOUDINI_PARAMETER_UI_ROW_MARGIN_HEIGHT_INVALID; + } + break; + + default: + MarginHeight = 0.0f; + break; + } +} + +float +SCustomizedBox::AddIndentation( + const TWeakObjectPtr& InParam, + const TMap>& InAllMultiParms, + const TMap>& InAllFoldersAndFolderLists) +{ + if (!InParam.IsValid()) + return 0.0f; + + bool bIsMainParmSimpleFolder = false; + // Get if this Parameter is a simple / collapsible folder + if (InParam->GetParameterType() == EHoudiniParameterType::Folder) + { + UHoudiniParameterFolder* FolderParm = Cast(InParam); + if (FolderParm) + bIsMainParmSimpleFolder = !FolderParm->IsTab(); + } + + int32 ParentId = InParam->GetParentParmId(); + TWeakObjectPtr CurParm = InParam; + float Indentation = 0.0f; + + while (ParentId >= 0) + { + TWeakObjectPtr ParentFolder; + TWeakObjectPtr ParentMultiParm; + + if (InAllFoldersAndFolderLists.Contains(ParentId)) + ParentFolder = InAllFoldersAndFolderLists[ParentId]; + + if (InAllMultiParms.Contains(ParentId)) + ParentMultiParm = InAllMultiParms[ParentId]; + + // The parent is a folder, add one unit of indentation + if (ParentFolder.IsValid()) + { + // Update the parent parm id + ParentId = ParentFolder->GetParentParmId(); + + if (ParentFolder->GetParameterType() == EHoudiniParameterType::FolderList) + continue; + + TWeakObjectPtr Folder = Cast(ParentFolder); + + if (!IsValidWeakPointer(Folder)) + continue; + + // update the current parm, find the parent of new cur param in the next round + CurParm = Folder; + Indentation += 1.0f; + } + // The parent is a multiparm + else if (ParentMultiParm.IsValid()) + { + // Update the parent parm id + ParentId = ParentMultiParm->GetParentParmId(); + + if (CurParm->GetChildIndex() == 0) + { + Indentation += 0.0f; + } + else + { + Indentation += 2.0f; + } + + // update the current parm, find the parent of new cur param in the next round + CurParm = ParentMultiParm; + } + else + { + // no folder/multiparm parent, end the loop + ParentId = -1; + } + } + + + float IndentationWidth = INDENTATION_UNIT_WIDTH * Indentation; + + // Add a base indentation to non simple/collapsible param + // Since it needs more space to offset the arrow width + if (!bIsMainParmSimpleFolder) + IndentationWidth += NON_FOLDER_OFFSET_WIDTH; + + this->AddSlot().AutoWidth() + [ + SNew(SBox).WidthOverride(IndentationWidth) + ]; + + + return IndentationWidth; +}; + +int32 +SCustomizedBox::OnPaint( + const FPaintArgs& Args, + const FGeometry& AllottedGeometry, + const FSlateRect& MyClippingRect, + FSlateWindowElementList& OutDrawElements, + int32 LayerId, + const FWidgetStyle& InWidgetStyle, + bool bParentEnabled) const +{ + SHorizontalBox::OnPaint(Args, AllottedGeometry, MyClippingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); + + // Initialize line buffer + TArray Line; + Line.SetNumZeroed(2); + // Initialize color buffer + FLinearColor Color = FLinearColor::White; + Color.A = 0.3; + + // draw the bottom line if this row is the tab folder list + if (bIsTabFolderListRow) + { + // Get the start position of the tabs bottom line (right bottom pt of the right most child widget) + float VerticalLineStartPosX = 0.0f; + float VerticalLineStartPosY = 0.0f; + float BottomLineStartPosX = 0.0f; + float BottomLineStartPosY = -1.0f; + + for (int32 Idx = 0; Idx < Children.Num(); ++Idx) + { + TSharedPtr CurChild = Children.GetChildAt(Idx); + if (!CurChild.IsValid()) + continue; + + if (Idx == 0) + { + VerticalLineStartPosX = CurChild->GetDesiredSize().X; + VerticalLineStartPosY = CurChild->GetDesiredSize().Y; + } + + BottomLineStartPosX += CurChild->GetDesiredSize().X; + + if (BottomLineStartPosY < 0.0f) + BottomLineStartPosY= CurChild->GetDesiredSize().Y; + } + + // Draw bottom line + Line[0].X = BottomLineStartPosX; + Line[0].Y = BottomLineStartPosY; + Line[1].X = AllottedGeometry.Size.X; + Line[1].Y = BottomLineStartPosY; + + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Line, + ESlateDrawEffect::None, Color, true, 1.0f); + } + + // Draw divider lines + { + Line[0].Y = -MarginHeight; + Line[1].Y = AllottedGeometry.Size.Y + MarginHeight; + + int32 NumOfLinesToDraw = bIsTabFolderListRow ? DividerLinePositions.Num() - 1 : DividerLinePositions.Num(); + for (int32 Idx = 0; Idx < NumOfLinesToDraw; ++Idx) + { + const float& CurDivider = DividerLinePositions[Idx]; + Line[0].X = CurDivider; + Line[1].X = CurDivider; + + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Line, + ESlateDrawEffect::None, Color, true, 1.0f); + } + + // Draw the last inner most divider line differently when this the tabs' row. + if (bIsTabFolderListRow && DividerLinePositions.Num() > 0) + { + const float& TabDivider = DividerLinePositions.Last(); + Line[0].X = TabDivider; + Line[1].X = TabDivider; + Line[0].Y = 0.f; + + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Line, + ESlateDrawEffect::None, Color, true, 1.0f); + } + } + + // Draw tab ending lines + { + float YPos = 0.0f; + + for (const float & CurEndingDivider : EndingDividerLinePositions) + { + // Draw cur ending line (vertical) + + Line[0].X = CurEndingDivider; + Line[0].Y = -2.3f; + Line[1].X = CurEndingDivider; + Line[1].Y = YPos; + + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Line, + ESlateDrawEffect::None, Color, true, 1.0f); + + // Draw cur ending line (horizontal) + + // Line[0].X = CurEndingDivider; + Line[0].Y = YPos; + Line[1].X = AllottedGeometry.Size.X; + // Line[1].Y = YPos; + + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Line, + ESlateDrawEffect::None, Color, true, 1.0f); + + YPos += 2.0f; + } + } + + // Draw the separator line if this is the row of a separator parameter + { + if (bIsSeparator) + { + Line[0].X = 25.f; + if (DividerLinePositions.Num() > 0) + Line[0].X += DividerLinePositions.Last(); + + Line[0].Y = AllottedGeometry.Size.Y / 2.f; + Line[1].X = AllottedGeometry.Size.X - 20.f; + Line[1].Y = Line[0].Y; + + Color.A = 0.7; + + FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Line, + ESlateDrawEffect::None, Color, true, 1.5f); + } + } + + return LayerId; +}; + +template< class T > +bool FHoudiniParameterDetails::CastParameters( + const TArray& InParams, + TArray& OutCastedParams ) +{ + for (auto CurrentParam : InParams) + { + T* CastedParam = Cast(CurrentParam); + if (IsValid(CastedParam)) + OutCastedParams.Add(CastedParam); + } + + return (OutCastedParams.Num() == InParams.Num()); +} + +template< class T > +bool FHoudiniParameterDetails::CastParameters( + const TArray>& InParams, + TArray>& OutCastedParams ) +{ + for (const auto& CurrentParam : InParams) + { + if (!IsValidWeakPointer(CurrentParam)) + continue; + + T* CastedParam = Cast(CurrentParam.Get()); + if (IsValid(CastedParam)) + OutCastedParams.Add(CastedParam); + } + + return (OutCastedParams.Num() == InParams.Num()); +} + +void +FHoudiniParameterDetails::Debug() +{ + int Entry = 0; + for(auto & StackEntry : FolderStack) + { + FString Output = FString::Format(TEXT("{0} "), { Entry }); + for(int Index = 0; Index < StackEntry.Num(); Index++) + { + Output += StackEntry[Index]->GetParameterLabel(); + Output += TEXT(" "); + } + + HOUDINI_LOG_MESSAGE(TEXT("%s\n"), *Output); + Entry++; + + } +} + +void +FHoudiniParameterDetails::CreateWidget( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams) +{ + // Uncomment this to debug printf the state of the Stack. + //Debug(); + + if (InJoinedParams.IsEmpty()) + return; + + const auto& FirstLinkedParams = InJoinedParams[0]; + + if (FirstLinkedParams.IsEmpty()) + return; + + const TWeakObjectPtr& InParam = FirstLinkedParams[0]; + if (!IsValidWeakPointer(InParam)) + return; + + // The directory won't parse if parameter ids are -1 + // simply return + if (InParam->GetParmId() < 0) + return; + + if (CurrentRampFloat) + { + // CreateWidgetFloatRamp(HouParameterCategory, InParams); + // If this parameter is a part of the last float ramp, skip it + if (InParam->GetIsChildOfMultiParm() && InParam->GetParentParmId() == CurrentRampFloat->GetParmId()) + return; + + // This parameter is not part of the last float ramp (we've passed all of its points/instances), reset + // CurrentRampFloat in order to continue normal processing of parameters + CurrentRampFloat = nullptr; + } + if (CurrentRampColor) + { + // CreateWidgetColorRamp(HouParameterCategory, InParams); + // if this parameter is a part of the last color ramp, skip it + if (InParam->GetIsChildOfMultiParm() && InParam->GetParentParmId() == CurrentRampColor->GetParmId()) + return; + + // This parameter is not part of the last color ramp (we've passed all of its points/instances), reset + // CurrentRampColor in order to continue normal processing of parameters + CurrentRampColor = nullptr; + } + + TArray Rows; + + switch (InParam->GetParameterType()) + { + case EHoudiniParameterType::Float: + case EHoudiniParameterType::Int: + case EHoudiniParameterType::String: + case EHoudiniParameterType::IntChoice: + case EHoudiniParameterType::StringChoice: + case EHoudiniParameterType::Separator: + case EHoudiniParameterType::Color: + case EHoudiniParameterType::Button: + case EHoudiniParameterType::ButtonStrip: + case EHoudiniParameterType::Label: + case EHoudiniParameterType::Toggle: + case EHoudiniParameterType::File: + case EHoudiniParameterType::FileDir: + case EHoudiniParameterType::FileGeo: + case EHoudiniParameterType::FileImage: + CreateJoinableWidget(HouParameterCategory, InJoinedParams, Rows); + break; + + case EHoudiniParameterType::FolderList: + CreateWidgetFolderList(HouParameterCategory, InJoinedParams, Rows); + break; + + case EHoudiniParameterType::Folder: + CreateWidgetFolder(HouParameterCategory, InJoinedParams, Rows); + break; + + case EHoudiniParameterType::MultiParm: + CreateWidgetMultiParm(HouParameterCategory, InJoinedParams, Rows); + break; + + case EHoudiniParameterType::FloatRamp: + CreateWidgetFloatRamp(HouParameterCategory, InJoinedParams, Rows); + break; + + case EHoudiniParameterType::ColorRamp: + CreateWidgetColorRamp(HouParameterCategory, InJoinedParams, Rows); + break; + + case EHoudiniParameterType::Input: + CreateWidgetOperatorPath(HouParameterCategory, InJoinedParams, Rows); + break; + + case EHoudiniParameterType::Invalid: + HandleUnsupportedParmType(HouParameterCategory, InJoinedParams); + break; + + default: + HandleUnsupportedParmType(HouParameterCategory, InJoinedParams); + break; + } + + uint32 MetaDataIndex = 0; + for (FDetailWidgetRow* const Row : Rows) + { + if (!Row) + { + continue; + } + + // Add meta data to all possible slots in the row + const TSharedRef Widgets[] = { + Row->ExtensionWidget.Widget, + Row->NameWidget.Widget, + Row->WholeRowWidget.Widget, + Row->ValueWidget.Widget }; + + for (auto Widget : Widgets) + { + AddMetaDataToAllDescendants(Widget, InParam->GetParameterName(), MetaDataIndex); + } + } + + // Remove a divider lines recursively if last joined parameter hits the end of a tab + { + const auto& LastLinkedParams = InJoinedParams.Last(); + if (LastLinkedParams.IsEmpty()) + return; + + const TWeakObjectPtr& LastParam = LastLinkedParams[0]; + if (!IsValidWeakPointer(InParam)) + return; + + RemoveTabDividers(HouParameterCategory, LastParam); + } +} + +void FHoudiniParameterDetails::CreateJoinableWidget( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows) +{ + if (InJoinedParams.IsEmpty()) + return; + + FDetailWidgetRow* Row = CreateNestedRow(HouParameterCategory, InJoinedParams); + if (!Row) + return; + + OutRows.Add(Row); + + const bool bIsJoined = InJoinedParams.Num() > 1; + const bool bUseWholeRow = bIsJoined || UsesWholeRow(InJoinedParams[0]); + FDetailWidgetDecl& Slot = (bUseWholeRow ? Row->WholeRowContent() : Row->ValueContent()); + + // For the sake of simplicity we always have a container to hold joinable parameters, even when + // not horizontally joining. We only want to use the customized box (for indentation of nested + // widgets) when we are horizontally joining. + TSharedPtr HorizontalJoinBox; + if (bIsJoined) + { + HorizontalJoinBox = CreateCustomizedBox(InJoinedParams[0]); + } + else + { + SAssignNew(HorizontalJoinBox, SHorizontalBox); + } + + if (!HorizontalJoinBox.IsValid()) + { + return; + } + + Slot.Widget = HorizontalJoinBox.ToSharedRef(); + + for (const auto& LinkedParams : InJoinedParams) + { + if (LinkedParams.IsEmpty()) + { + continue; + } + + const auto& Param = LinkedParams[0]; + + if (!IsValidWeakPointer(Param)) + { + continue; + } + + // The directory won't parse if parameter ids are -1 + if (Param->GetParmId() < 0) + { + continue; + } + + TSharedRef LabelledParameter = SNew(SHoudiniLabelledParameter); + LabelledParameter->SetEnabled(!Param->IsDisabled()); + + const bool bUseLabel = IsLabelVisible(LinkedParams); + + // We only need a custom solution for displaying the name text block when horizontally + // joining multiple parameters. Otherwise, we can use Unreal's provided columns in the + // details panel. + TSharedPtr TextBlock = CreateNameTextBlock(LinkedParams); + if (bIsJoined) + { + if (TextBlock.IsValid() && bUseLabel) + { + LabelledParameter->SetNameContent(TextBlock.ToSharedRef()); + } + + if (!Param->ShouldDisplay()) + { + LabelledParameter->SetVisibility(EVisibility::Hidden); + } + } + else if (bUseLabel) + { + TSharedPtr CustomizedBox = CreateCustomizedBox(LinkedParams); + + if (CustomizedBox.IsValid() && TextBlock.IsValid()) + { + TextBlock->SetEnabled(!Param->IsDisabled()); + CustomizedBox->AddSlot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + .AttachWidget(TextBlock.ToSharedRef()); + Row->NameContent().Widget = CustomizedBox.ToSharedRef(); + } + } + + // Attach our labelled parameter widget to the horizontal box containg all joined parameters + // on this row. + { + SHorizontalBox::FScopedWidgetSlotArguments SlotArguments = HorizontalJoinBox->AddSlot(); + if (!ShouldWidgetFill(Param->GetParameterType())) + { + // Make widget occupy minimum required space + SlotArguments.AutoWidth(); + } + if (Param->GetJoinNext()) + { + // If there is a parameter to the right, add some padding in between + SlotArguments.Padding(0.f, 0.f, HAPI_UNREAL_PADDING_HORIZONTAL_JOIN, 0.f); + } + SlotArguments.AttachWidget(LabelledParameter); + } + + switch (Param->GetParameterType()) + { + case EHoudiniParameterType::Int: + CreateWidgetInt(LabelledParameter, LinkedParams); + break; + + case EHoudiniParameterType::Float: + CreateWidgetFloat(LabelledParameter, LinkedParams); + break; + + case EHoudiniParameterType::String: + CreateWidgetString(HouParameterCategory, LabelledParameter, LinkedParams); + break; + + case EHoudiniParameterType::IntChoice: + case EHoudiniParameterType::StringChoice: + CreateWidgetChoice(LabelledParameter, LinkedParams); + break; + + case EHoudiniParameterType::Separator: + CreateWidgetSeparator(LabelledParameter, LinkedParams); + break; + + case EHoudiniParameterType::Color: + CreateWidgetColor(LabelledParameter, LinkedParams); + break; + + case EHoudiniParameterType::Button: + CreateWidgetButton(LabelledParameter, LinkedParams); + break; + + case EHoudiniParameterType::ButtonStrip: + CreateWidgetButtonStrip(LabelledParameter, LinkedParams); + break; + + case EHoudiniParameterType::Label: + CreateWidgetLabel(LabelledParameter, LinkedParams); + break; + + case EHoudiniParameterType::Toggle: + CreateWidgetToggle(LabelledParameter, LinkedParams); + break; + + case EHoudiniParameterType::File: + case EHoudiniParameterType::FileDir: + case EHoudiniParameterType::FileGeo: + case EHoudiniParameterType::FileImage: + CreateWidgetFile(LabelledParameter, LinkedParams); + break; + + default: + break; + } + } +} + +void +FHoudiniParameterDetails::CreateTabEndingRow(IDetailCategoryBuilder& HouParameterCategory) +{ + FDetailWidgetRow& Row = HouParameterCategory.AddCustomRow(FText::GetEmpty()); + TSharedPtr TabEndingRow = SNew(SCustomizedBox); + + TabEndingRow->DividerLinePositions = DividerLinePositions; + + if (TabEndingRow.IsValid()) + CurrentTabEndingRow = TabEndingRow.Get(); + + Row.WholeRowWidget.Widget = TabEndingRow.ToSharedRef(); + Row.WholeRowWidget.MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH); +} + +TSharedPtr +FHoudiniParameterDetails::CreateCustomizedBox( + const TArray>& InParams) +{ + if (InParams.Num() <= 0) + return nullptr; + + const TWeakObjectPtr& MainParam = InParams[0]; + if (!IsValidWeakPointer(MainParam)) + return nullptr; + + TSharedPtr CustomizedBox = SNew(SCustomizedBox); + + CustomizedBox->DividerLinePositions = DividerLinePositions; + CustomizedBox->SetHoudiniParameter(InParams); + CustomizedBox->AddIndentation(MainParam.Get(), AllMultiParms, AllFoldersAndFolderLists); + + if (MainParam->IsDirectChildOfMultiParm()) + { + // If it is head of an multiparm instance + if (MainParam->GetChildIndex() == 0) + { + CreateWidgetMultiParmObjectButtons(CustomizedBox, InParams); + } + } + + return CustomizedBox; +} + +TSharedPtr +FHoudiniParameterDetails::CreateNameTextBlock( + const TArray>& InParams) +{ + if (InParams.Num() <= 0) + return nullptr; + + const TWeakObjectPtr& MainParam = InParams[0]; + if (!IsValidWeakPointer(MainParam)) + return nullptr; + + FString ParameterLabelString = MainParam->GetParameterLabel(); + + if (MainParam->IsDirectChildOfMultiParm()) + { + // If it is head of an multiparm instance + if (MainParam->GetChildIndex() == 0) + { + int32 CurrentMultiParmInstanceIndex = 0; + if (MultiParmInstanceIndices.Contains(MainParam->GetParentParmId())) + { + MultiParmInstanceIndices[MainParam->GetParentParmId()] += 1; + CurrentMultiParmInstanceIndex = MultiParmInstanceIndices[MainParam->GetParentParmId()]; + } + ParameterLabelString += TEXT(" (") + FString::FromInt(CurrentMultiParmInstanceIndex + 1) + TEXT(")"); + } + } + + return SNew(STextBlock) + .Text(FText::FromString(ParameterLabelString)) + .ToolTipText(GetParameterTooltip(MainParam)) + .Font(_GetEditorStyle().GetFontStyle( + MainParam->IsDefault() + ? TEXT("PropertyWindow.NormalFont") + : TEXT("PropertyWindow.BoldFont"))); +} + +void +FHoudiniParameterDetails::CreateNameWidget( + FDetailWidgetRow* Row, + const TArray>& InParams, + bool WithLabel) +{ + if (!Row) + return; + + TSharedPtr HorizontalBox = CreateCustomizedBox(InParams); + TSharedPtr TextBlock = CreateNameTextBlock(InParams); + + if (!HorizontalBox.IsValid()) + return; + + if (TextBlock.IsValid()) + { + HorizontalBox->AddSlot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + TextBlock.ToSharedRef() + ]; + } + + Row->NameWidget.Widget = HorizontalBox.ToSharedRef(); +} + +void +FHoudiniParameterDetails::CreateNameWidgetWithAutoUpdate( + FDetailWidgetRow* Row, + const TArray> &InParams, + bool WithLabel) +{ + if (!Row) + return; + + if (InParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = InParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + FString ParameterLabelStr = MainParam->GetParameterLabel(); + TSharedRef HorizontalBox = SNew(SCustomizedBox); + HorizontalBox->DividerLinePositions = DividerLinePositions; + HorizontalBox->SetHoudiniParameter(InParams); + HorizontalBox->AddIndentation(MainParam, AllMultiParms, AllFoldersAndFolderLists); + + TSharedPtr VerticalBox; + HorizontalBox->AddSlot() + [ + SAssignNew(VerticalBox, SVerticalBox) + ]; + + if (MainParam->IsDirectChildOfMultiParm()) + { + // If it is head of an multiparm instance + if (MainParam->GetChildIndex() == 0) + { + int32 CurrentMultiParmInstanceIndex = 0; + if (MultiParmInstanceIndices.Contains(MainParam->GetParentParmId())) + { + MultiParmInstanceIndices[MainParam->GetParentParmId()] += 1; + CurrentMultiParmInstanceIndex = MultiParmInstanceIndices[MainParam->GetParentParmId()]; + } + + ParameterLabelStr += TEXT(" (") + FString::FromInt(CurrentMultiParmInstanceIndex + 1) + TEXT(")"); + + CreateWidgetMultiParmObjectButtons(HorizontalBox, InParams); + } + + if (MainParam->GetParameterType() == EHoudiniParameterType::ColorRamp) + { + UHoudiniParameterRampColor* RampParameter = Cast(MainParam); + if (RampParameter) + { + if (RampParameter->bCaching) + ParameterLabelStr += "*"; + } + } + + const FText & FinalParameterLabelText = WithLabel ? FText::FromString(ParameterLabelStr) : FText::GetEmpty(); + + VerticalBox->AddSlot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + SNew(STextBlock) + .Text(FinalParameterLabelText) + .ToolTipText(GetParameterTooltip(MainParam)) + .Font(_GetEditorStyle().GetFontStyle(MainParam->IsDefault() ? TEXT("PropertyWindow.NormalFont") : TEXT("PropertyWindow.BoldFont"))) + ]; + } + else + { + // TODO: Refactor me...extend 'auto/manual update' to all parameters? (It only applies to color and float ramps for now.) + bool bParamNeedUpdate = false; + if (MainParam->GetParameterType() == EHoudiniParameterType::ColorRamp) + { + UHoudiniParameterRampColor* RampParameter = Cast(MainParam); + if (RampParameter) + bParamNeedUpdate = RampParameter->bCaching; + } + else if (MainParam->GetParameterType() == EHoudiniParameterType::FloatRamp) + { + UHoudiniParameterRampFloat* RampParameter = Cast(MainParam); + if (RampParameter) + bParamNeedUpdate = RampParameter->bCaching; + } + + if (bParamNeedUpdate) + ParameterLabelStr += "*"; + + const FText & FinalParameterLabelText = WithLabel ? FText::FromString(ParameterLabelStr) : FText::GetEmpty(); + + VerticalBox->AddSlot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + SNew(STextBlock) + .Text(FinalParameterLabelText) + .ToolTipText(GetParameterTooltip(MainParam)) + .Font(_GetEditorStyle().GetFontStyle(MainParam->IsDefault() ? TEXT("PropertyWindow.NormalFont") : TEXT("PropertyWindow.BoldFont"))) + ]; + } + + auto IsAutoUpdateChecked = [MainParam]() + { + if (!IsValidWeakPointer(MainParam)) + return ECheckBoxState::Unchecked; + + return MainParam->IsAutoUpdate() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }; + + const bool bCookingEnabled = FHoudiniEngine::Get().IsCookingEnabled(); + + auto OnAutoUpdateCheckBoxStateChanged = [MainParam, InParams, bCookingEnabled](ECheckBoxState NewState) + { + if (NewState == ECheckBoxState::Checked) + { + for (auto & NextSelectedParam : InParams) + { + if (!IsValidWeakPointer(NextSelectedParam)) + continue; + + if (NextSelectedParam->IsAutoUpdate() && bCookingEnabled) + continue; + + // Do not allow mode change when the Houdini asset component is cooking + if (FHoudiniEngineUtils::IsHoudiniCookableCooking(NextSelectedParam.Get())) + continue; + + switch (MainParam->GetParameterType()) + { + case EHoudiniParameterType::ColorRamp: + { + UHoudiniParameterRampColor* ColorRampParameter = Cast(NextSelectedParam); + + if (!ColorRampParameter) + continue; + + // Do not sync the selected color ramp parameter if its parent HDA is being cooked + if (FHoudiniEngineUtils::IsHoudiniCookableCooking(ColorRampParameter)) + continue; + + // Sync the Cached curve points at update mode switch. + ColorRampParameter->SyncCachedPoints(); + ColorRampParameter->SetCaching(false); + } + break; + + case EHoudiniParameterType::FloatRamp: + { + UHoudiniParameterRampFloat* FloatRampParameter = Cast(NextSelectedParam); + + if (!FloatRampParameter) + continue; + + // Do not sync the selected float ramp parameter if its parent HDA is being cooked + if (FHoudiniEngineUtils::IsHoudiniCookableCooking(FloatRampParameter)) + continue; + + // Sync the Cached curve points at update mode switch. + FloatRampParameter->SyncCachedPoints(); + FloatRampParameter->SetCaching(false); + } + break; + + default: + break; + } + + NextSelectedParam->SetAutoUpdate(true); + } + } + else + { + for (auto & NextSelectedParam : InParams) + { + if (!IsValidWeakPointer(NextSelectedParam)) + continue; + + if (!(NextSelectedParam->IsAutoUpdate() && bCookingEnabled)) + continue; + + // Do not allow mode change when the Houdini asset component is cooking + if (FHoudiniEngineUtils::IsHoudiniCookableCooking(NextSelectedParam.Get())) + continue; + + NextSelectedParam->SetAutoUpdate(false); + } + } + }; + + // Auto update check box + TSharedPtr CheckBox; + + VerticalBox->AddSlot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + SNew(SHorizontalBox) + + + SHorizontalBox::Slot() + [ + SAssignNew(CheckBox, SCheckBox) + .OnCheckStateChanged_Lambda([OnAutoUpdateCheckBoxStateChanged](ECheckBoxState NewState) + { + OnAutoUpdateCheckBoxStateChanged(NewState); + }) + .IsChecked_Lambda([IsAutoUpdateChecked]() + { + return IsAutoUpdateChecked(); + }) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("AutoUpdate", "Auto-update")) + .ToolTipText(LOCTEXT("AutoUpdateTip", "When enabled, this parameter will automatically update its value while editing. Turning this off will allow you to more easily update it, and the update can be pushed by checking the toggle again.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ] + ]; + + if ((MainParam->GetParameterType() != EHoudiniParameterType::FloatRamp) && (MainParam->GetParameterType() != EHoudiniParameterType::ColorRamp)) + CheckBox->SetVisibility(EVisibility::Hidden); + + Row->NameWidget.Widget = HorizontalBox; +} + +FDetailWidgetRow* +FHoudiniParameterDetails::CreateNestedRow( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + bool bDecreaseChildCount) +{ + if (InJoinedParams.IsEmpty()) + return nullptr; + + const auto& InParams = InJoinedParams[0]; + + if (InParams.Num() <= 0) + return nullptr; + + const TWeakObjectPtr& MainParam = InParams[0]; + if (!IsValidWeakPointer(MainParam)) + return nullptr; + + bool bShouldDisplayRow = false; + + // Only display the row if, among all joined params, at least one is visible. + for (const auto& LinkedParams : InJoinedParams) + { + if (LinkedParams.Num() <= 0) + continue; + + const TWeakObjectPtr& Param = LinkedParams[0]; + if (!IsValidWeakPointer(Param)) + continue; + + if (Param->ShouldDisplay()) + { + bShouldDisplayRow = true; + break; + } + } + + // Created row for the current parameter (if there is not a row created, do not show the parameter). + FDetailWidgetRow* Row = nullptr; + + // Current parameter is in a multiparm instance (directly) + if (MainParam->IsDirectChildOfMultiParm()) + { + int32 ParentMultiParmId = MainParam->GetParentParmId(); + + // If this is a folder param, its folder list parent parm is the multiparm + if (MainParam->GetParameterType() == EHoudiniParameterType::Folder) + { + if (!AllFoldersAndFolderLists.Contains(MainParam->GetParentParmId())) // This should not happen + return nullptr; + + UHoudiniParameterFolderList* ParentFolderList = Cast(AllFoldersAndFolderLists[MainParam->GetParentParmId()]); + if (!IsValid(ParentFolderList)) + return nullptr; // This should not happen + + ParentMultiParmId = ParentFolderList->GetParentParmId(); + } + + if (!AllMultiParms.Contains(ParentMultiParmId)) // This should not happen normally + return nullptr; + + // Get the parent multiparm + const TWeakObjectPtr& ParentMultiParm = AllMultiParms[ParentMultiParmId]; + + // The parent multiparm is visible. + if (ParentMultiParm.IsValid() && ParentMultiParm->IsShown() && bShouldDisplayRow) + { + if (MainParam->GetParameterType() != EHoudiniParameterType::FolderList) + { + FString ParameterRowString = MainParam->GetParameterLabel() + " " + MainParam->GetParameterName(); + Row = &(HouParameterCategory.AddCustomRow(FText::FromString(ParameterRowString))); + } + } + + } + // This item is not a direct child of a multiparm. + else + { + bool bIsFolder = MainParam->GetParameterType() == EHoudiniParameterType::Folder; + + // If this parameter is a folder, its parent folder should be the second top of the stack + int32 NestedMinStackDepth = bIsFolder ? 1 : 0; + + // Current parameter is inside a folder. + if (FolderStack.Num() > NestedMinStackDepth) + { + // If the current parameter is a folder, we take the top second queue on the stack, since the top one represents itself. + // Otherwise take the top queue on the stack. + TArray & CurrentLayerFolderQueue = bIsFolder ? + FolderStack[FolderStack.Num() - 2] : FolderStack.Last(); + + if (CurrentLayerFolderQueue.Num() <= 0) // Error state + return nullptr; + + bool bParentFolderVisible = CurrentLayerFolderQueue[0]->IsContentShown(); + + bool bIsSelectedTabVisible = false; + + // If its parent folder is visible, display current parameter, + // Otherwise, just prune the stacks. + if (bParentFolderVisible) + { + int32 ParentFolderId = MainParam->GetParentParmId(); + + // If the current parameter is a folder, its parent is a folderlist. + // So we need to continue to get the parent of the folderlist. + if (MainParam->GetParameterType() == EHoudiniParameterType::Folder) + { + if (AllFoldersAndFolderLists.Contains(ParentFolderId)) + ParentFolderId = AllFoldersAndFolderLists[ParentFolderId]->GetParentParmId(); + else + return nullptr; // error state + } + + UHoudiniParameterFolder* ParentFolder = nullptr; + + if (AllFoldersAndFolderLists.Contains(ParentFolderId)) + ParentFolder = Cast(AllFoldersAndFolderLists[ParentFolderId]); + + // This row should be shown if its parent folder is shown. + if (ParentFolder) + bShouldDisplayRow &= (ParentFolder->IsTab() && ParentFolder->IsChosen()) || (!ParentFolder->IsTab() && ParentFolder->IsExpanded()); + + if (bShouldDisplayRow) + { + if (MainParam->GetParameterType() != EHoudiniParameterType::FolderList) + { + FString ParameterRowString = MainParam->GetParameterLabel() + " " + MainParam->GetParameterName(); + Row = &(HouParameterCategory.AddCustomRow(FText::FromString(ParameterRowString))); + } + } + } + + // prune the stack finally + if (bDecreaseChildCount) + { + CurrentLayerFolderQueue[0]->GetChildCounter() -= InJoinedParams.Num(); + + if (CurrentLayerFolderQueue[0]->GetChildCounter() < 1) + PruneStack(); + } + } + // If this parameter is in the root dir, just create a row. + else + { + if (bShouldDisplayRow) + { + if (MainParam->GetParameterType() != EHoudiniParameterType::FolderList) + { + FString ParameterRowString = MainParam->GetParameterLabel() + " " + MainParam->GetParameterName(); + Row = &(HouParameterCategory.AddCustomRow(FText::FromString(ParameterRowString))); + } + + } + } + } + + if (Row) + CurrentTabEndingRow = nullptr; + + if (Row) + { + Row->RowTag(*(MainParam->GetParameterName())); + } + + return Row; +} + +void +FHoudiniParameterDetails::HandleUnsupportedParmType( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams) +{ + if (InJoinedParams.IsEmpty()) + return; + + const auto& InParams = InJoinedParams[0]; + + if (InParams.Num() < 1) + return; + + const TWeakObjectPtr& MainParam = InParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + CreateNestedRow(HouParameterCategory, InJoinedParams); +} + +void +FHoudiniParameterDetails::CreateWidgetFloat( + const TSharedRef LabelledParameter, + const TArray>& InParams) +{ + TArray> FloatParams; + if (!CastParameters(InParams, FloatParams)) + return; + + if (FloatParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = FloatParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + // Helper function to find a unit from a string (name or abbreviation) + auto ParmUnit = FUnitConversion::UnitFromString(*(MainParam->GetUnit())); + EUnit Unit = EUnit::Unspecified; + if (FUnitConversion::Settings().ShouldDisplayUnits() && ParmUnit.IsSet()) + Unit = ParmUnit.GetValue(); + + TSharedPtr> paramTypeInterface; + paramTypeInterface = MakeShareable(new TNumericUnitTypeInterface(Unit)); + + // Lambdas for slider begin + auto SliderBegin = [](const TArray>& FloatParams) + { + if (FloatParams.Num() == 0) + return; + + if (!IsValidWeakPointer(FloatParams[0])) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterFloatChange", "Houdini Parameter Float: Changing a value"), + FloatParams[0]->GetOuter()); + + for (int Idx = 0; Idx < FloatParams.Num(); Idx++) + { + if (!IsValidWeakPointer(FloatParams[Idx])) + continue; + + FloatParams[Idx]->Modify(); + } + }; + + // Lambdas for slider end + auto SliderEnd = [](const TArray>& FloatParams) + { + // Mark the value as changed to trigger an update + for (int Idx = 0; Idx < FloatParams.Num(); Idx++) + { + if (!IsValidWeakPointer(FloatParams[Idx])) + continue; + + FloatParams[Idx]->MarkChanged(true); + } + }; + + // Lambdas for changing the parameter value + auto ChangeFloatValueAt = [](const float& Value, const int32& Index, const bool& DoChange, const TArray>& FloatParams) + { + if (FloatParams.Num() == 0) + return; + + if (!IsValidWeakPointer(FloatParams[0])) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterFloatChange", "Houdini Parameter Float: Changing a value"), + FloatParams[0]->GetOuter() ); + + bool bChanged = false; + for (int Idx = 0; Idx < FloatParams.Num(); Idx++) + { + if (!IsValidWeakPointer(FloatParams[Idx])) + continue; + + FloatParams[Idx]->Modify(); + if (FloatParams[Idx]->SetValueAt(Value, Index)) + { + // Only mark the param has changed if DoChange is true!!! + if(DoChange) + FloatParams[Idx]->MarkChanged(true); + bChanged = true; + } + } + + if (!bChanged || !DoChange) + { + // Cancel the transaction if no parameter's value has actually been changed + Transaction.Cancel(); + } + }; + + auto RevertToDefault = [](const int32& TupleIndex, const TArray>& FloatParams) + { + if (FloatParams.Num() == 0) + return FReply::Handled(); + + if (!IsValidWeakPointer(FloatParams[0])) + return FReply::Handled(); + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterFloatChange", "Houdini Parameter Float: Revert to default value"), + FloatParams[0]->GetOuter()); + + if (TupleIndex < 0) + { + for (int32 Idx = 0; Idx < FloatParams.Num(); Idx++) + { + if (!IsValidWeakPointer(FloatParams[Idx])) + continue; + + if (FloatParams[Idx]->IsDefault()) + continue; + + FloatParams[Idx]->RevertToDefault(-1); + } + } + else + { + for (int32 Idx = 0; Idx < FloatParams.Num(); Idx++) + { + if (!IsValidWeakPointer(FloatParams[Idx])) + continue; + + if (FloatParams[Idx]->IsDefaultValueAtIndex(TupleIndex)) + continue; + + FloatParams[Idx]->RevertToDefault(TupleIndex); + } + } + return FReply::Handled(); + }; + + + TSharedRef< SVerticalBox > VerticalBox = SNew(SVerticalBox); + + if (MainParam->GetTupleSize() == 3) + { + // Should we swap Y and Z fields (only relevant for Vector3) + // Ignore the swapping if that parameter has the noswap tag + bool SwapVector3 = !MainParam->GetNoSwap(); + + auto ChangeFloatValueUniformly = [FloatParams, ChangeFloatValueAt](const float& Val, const bool& bDoChange) + { + ChangeFloatValueAt(Val, 0, bDoChange, FloatParams); + ChangeFloatValueAt(Val, 1, bDoChange, FloatParams); + ChangeFloatValueAt(Val, 2, bDoChange, FloatParams); + }; + + VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SVectorInputBox) + .bColorAxisLabels(true) + .AllowSpin(true) + .X(TAttribute>::Create(TAttribute>::FGetter::CreateUObject(MainParam.Get(), &UHoudiniParameterFloat::GetValue, 0))) + .Y(TAttribute>::Create(TAttribute>::FGetter::CreateUObject(MainParam.Get(), &UHoudiniParameterFloat::GetValue, SwapVector3 ? 2 : 1))) + .Z(TAttribute>::Create(TAttribute>::FGetter::CreateUObject(MainParam.Get(), &UHoudiniParameterFloat::GetValue, SwapVector3 ? 1 : 2))) + .OnXCommitted_Lambda( [ChangeFloatValueAt, ChangeFloatValueUniformly, FloatParams, MainParam, SwapVector3](float Val, ETextCommit::Type TextCommitType) + { + if (MainParam->IsUniformLocked()) + ChangeFloatValueUniformly(Val, true); + else + ChangeFloatValueAt( Val, 0, true, FloatParams); + }) + .OnYCommitted_Lambda( [ChangeFloatValueAt, ChangeFloatValueUniformly, FloatParams, MainParam, SwapVector3](float Val, ETextCommit::Type TextCommitType) + { + if (MainParam->IsUniformLocked()) + ChangeFloatValueUniformly(Val, true); + else + ChangeFloatValueAt( Val, SwapVector3 ? 2 : 1, true, FloatParams); + }) + .OnZCommitted_Lambda([ChangeFloatValueAt, ChangeFloatValueUniformly, FloatParams, MainParam, SwapVector3](float Val, ETextCommit::Type TextCommitType) + { + if (MainParam->IsUniformLocked()) + ChangeFloatValueUniformly(Val, true); + else + ChangeFloatValueAt( Val, SwapVector3 ? 1 : 2, true, FloatParams); + }) + .OnXChanged_Lambda([ChangeFloatValueAt, ChangeFloatValueUniformly, FloatParams, MainParam, SwapVector3](float Val) + { + if (MainParam->IsUniformLocked()) + ChangeFloatValueUniformly(Val, false); + else + ChangeFloatValueAt(Val, 0, false, FloatParams); + }) + .OnYChanged_Lambda([ChangeFloatValueAt, ChangeFloatValueUniformly, FloatParams, MainParam, SwapVector3](float Val) + { + if (MainParam->IsUniformLocked()) + ChangeFloatValueUniformly(Val, false); + else + ChangeFloatValueAt(Val, SwapVector3 ? 2 : 1, false, FloatParams); + }) + .OnZChanged_Lambda([ChangeFloatValueAt, ChangeFloatValueUniformly, FloatParams, MainParam, SwapVector3](float Val) + { + if (MainParam->IsUniformLocked()) + ChangeFloatValueUniformly(Val, false); + else + ChangeFloatValueAt(Val, SwapVector3 ? 1 : 2, false, FloatParams); + }) + .OnBeginSliderMovement_Lambda([SliderBegin, FloatParams]() { SliderBegin(FloatParams); }) + .OnEndSliderMovement_Lambda([SliderEnd, FloatParams](const float NewValue) { SliderEnd(FloatParams); }) + .TypeInterface(paramTypeInterface) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ClickMethod(EButtonClickMethod::MouseDown) + .ToolTipText(LOCTEXT("FloatParameterLockButtonToolTip", "When locked, change the vector value uniformly.")) + .Visibility(EVisibility::Visible) + [ + SNew(SImage) + .Image(MainParam->IsUniformLocked() ? _GetEditorStyle().GetBrush("Icons.Lock") : _GetEditorStyle().GetBrush("Icons.Unlock")) + ] + .OnClicked_Lambda([FloatParams, MainParam]() + { + if (!IsValidWeakPointer(MainParam)) + return FReply::Handled(); + + for (auto & CurParam : FloatParams) + { + if (!IsValidWeakPointer(CurParam)) + continue; + + CurParam->SwitchUniformLock(); + } + + FHoudiniEngineUtils::UpdateEditorProperties(true); + + return FReply::Handled(); + }) + ] + + + SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Left) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertToDefault", "Revert to default")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility_Lambda([FloatParams]() + { + for (auto & SelectedParam : FloatParams) + { + if (!IsValidWeakPointer(SelectedParam)) + continue; + + if (!SelectedParam->IsDefault()) + return EVisibility::Visible; + } + + return EVisibility::Hidden; + }) + .OnClicked_Lambda([FloatParams, RevertToDefault]() { return RevertToDefault(-1, FloatParams); }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ] + ]; + } + else + { + for (int32 Idx = 0; Idx < MainParam->GetTupleSize(); ++Idx) + { + TSharedPtr> NumericEntryBox; + VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SAssignNew(NumericEntryBox, SNumericEntryBox< float >) + .AllowSpin(true) + + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + + .MinValue(MainParam->GetMin()) + .MaxValue(MainParam->GetMax()) + + .MinSliderValue(MainParam->GetUIMin()) + .MaxSliderValue(MainParam->GetUIMax()) + + .Value(TAttribute>::Create(TAttribute>::FGetter::CreateUObject(MainParam.Get(), &UHoudiniParameterFloat::GetValue, Idx))) + .OnValueChanged_Lambda([ChangeFloatValueAt, Idx, FloatParams](float Val) { ChangeFloatValueAt(Val, Idx, false, FloatParams); }) + .OnValueCommitted_Lambda([ChangeFloatValueAt, Idx, FloatParams](float Val, ETextCommit::Type TextCommitType) { ChangeFloatValueAt(Val, Idx, true, FloatParams); }) + .OnBeginSliderMovement_Lambda([SliderBegin, FloatParams]() { SliderBegin(FloatParams); }) + .OnEndSliderMovement_Lambda([SliderEnd, FloatParams](const float NewValue) { SliderEnd(FloatParams); }) + .SliderExponent(MainParam->IsLogarithmic() ?8.0f : 1.0f) + .TypeInterface(paramTypeInterface) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertToDefault", "Revert to default")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .OnClicked_Lambda([Idx, FloatParams, RevertToDefault]() { return RevertToDefault(Idx, FloatParams); }) + .Visibility_Lambda([Idx, FloatParams]() + { + for (auto & SelectedParam :FloatParams) + { + if (!IsValidWeakPointer(SelectedParam)) + continue; + + if (!SelectedParam->IsDefaultValueAtIndex(Idx)) + return EVisibility::Visible; + } + + return EVisibility::Hidden; + }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ]; + } + } + + LabelledParameter->SetContent(VerticalBox); +} + +void +FHoudiniParameterDetails::CreateWidgetInt( + const TSharedRef LabelledParameter, + const TArray>& InParams) +{ + TArray> IntParams; + if (!CastParameters(InParams, IntParams)) + return; + + if (IntParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = IntParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + TSharedRef VerticalBox = SNew(SVerticalBox); + + // Helper function to find a unit from a string (name or abbreviation) + auto ParmUnit = FUnitConversion::UnitFromString(*(MainParam->GetUnit())); + EUnit Unit = EUnit::Unspecified; + if (FUnitConversion::Settings().ShouldDisplayUnits() && ParmUnit.IsSet()) + Unit = ParmUnit.GetValue(); + + TSharedPtr> paramTypeInterface; + paramTypeInterface = MakeShareable(new TNumericUnitTypeInterface(Unit)); + + // Lambda for slider begin + auto SliderBegin = [](const TArray>& IntParams) + { + if (IntParams.Num() == 0) + return; + + if (!IsValidWeakPointer(IntParams[0])) + return; + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterIntChange", "Houdini Parameter Int: Changing a value"), + IntParams[0]->GetOuter()); + + for (int Idx = 0; Idx < IntParams.Num(); Idx++) + { + if (!IsValidWeakPointer(IntParams[Idx])) + continue; + + IntParams[Idx]->Modify(); + } + }; + + // Lambda for slider end + auto SliderEnd = [](const TArray>& IntParams) + { + // Mark the value as changed to trigger an update + for (int Idx = 0; Idx < IntParams.Num(); Idx++) + { + if (!IsValidWeakPointer(IntParams[Idx])) + continue; + + IntParams[Idx]->MarkChanged(true); + } + }; + + // Lambda for changing the parameter value + auto ChangeIntValueAt = [](const int32& Value, const int32& Index, const bool& DoChange, const TArray>& IntParams) + { + if (IntParams.Num() == 0) + return; + + if (!IsValidWeakPointer(IntParams[0])) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterIntChange", "Houdini Parameter Int: Changing a value"), + IntParams[0]->GetOuter()); + + bool bChanged = false; + for (int Idx = 0; Idx < IntParams.Num(); Idx++) + { + if (!IsValidWeakPointer(IntParams[Idx])) + continue; + + IntParams[Idx]->Modify(); + if (IntParams[Idx]->SetValueAt(Value, Index)) + { + // Only mark the param has changed if DoChange is true!!! + if (DoChange) + IntParams[Idx]->MarkChanged(true); + bChanged = true; + } + } + + if (!bChanged || !DoChange) + { + // Cancel the transaction if there is no param has actually been changed + Transaction.Cancel(); + } + }; + + auto RevertToDefault = [](const int32& TupleIndex, const TArray>& IntParams) + { + for (int32 Idx = 0; Idx < IntParams.Num(); Idx++) + { + if (!IsValidWeakPointer(IntParams[Idx])) + continue; + + if (IntParams[Idx]->IsDefaultValueAtIndex(TupleIndex)) + continue; + + IntParams[Idx]->RevertToDefault(TupleIndex); + } + + return FReply::Handled(); + }; + + for (int32 Idx = 0; Idx < MainParam->GetTupleSize(); ++Idx) + { + TSharedPtr< SNumericEntryBox< int32 > > NumericEntryBox; + VerticalBox->AddSlot().Padding(2, 2, 5, 2) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SAssignNew(NumericEntryBox, SNumericEntryBox< int32 >) + .AllowSpin(true) + + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + + .MinValue(MainParam->GetMin()) + .MaxValue(MainParam->GetMax()) + + .MinSliderValue(MainParam->GetUIMin()) + .MaxSliderValue(MainParam->GetUIMax()) + + .Value( TAttribute>::Create(TAttribute>::FGetter::CreateUObject(MainParam.Get(), &UHoudiniParameterInt::GetValue, Idx))) + .OnValueChanged_Lambda( [=](int32 Val) { ChangeIntValueAt(Val, Idx, false, IntParams); } ) + .OnValueCommitted_Lambda([=](float Val, ETextCommit::Type TextCommitType) { ChangeIntValueAt(Val, Idx, true, IntParams); }) + .OnBeginSliderMovement_Lambda( [=]() { SliderBegin(IntParams); }) + .OnEndSliderMovement_Lambda([=](const float NewValue) { SliderEnd(IntParams); }) + .SliderExponent(MainParam->IsLogarithmic() ? 8.0f : 1.0f) + .TypeInterface(paramTypeInterface) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertToDefault", "Revert to default")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility_Lambda([Idx, IntParams]() + { + for (auto & NextSelectedParam : IntParams) + { + if (!IsValidWeakPointer(NextSelectedParam)) + continue; + + if (!NextSelectedParam->IsDefaultValueAtIndex(Idx)) + return EVisibility::Visible; + } + + return EVisibility::Hidden; + }) + .OnClicked_Lambda([Idx, IntParams, RevertToDefault]() { return RevertToDefault(Idx, IntParams); }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ]; + /* + if (NumericEntryBox.IsValid()) + NumericEntryBox->SetEnabled(!MainParam->IsDisabled()); + */ + } + + LabelledParameter->SetContent(VerticalBox); +} + +void +FHoudiniParameterDetails::CreateWidgetString( + IDetailCategoryBuilder& HouParameterCategory, + const TSharedRef LabelledParameter, + const TArray>& InParams) +{ + TArray> StringParams; + if (!CastParameters(InParams, StringParams)) + return; + + if (StringParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = StringParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + bool bIsMultiLine = false; + bool bIsUnrealRef = false; + UClass* UnrealRefClass = UObject::StaticClass(); + + TSharedRef< SVerticalBox > VerticalBox = SNew(SVerticalBox); + + TMap& Tags = MainParam->GetTags(); + if (Tags.Contains(HOUDINI_PARAMETER_STRING_REF_TAG) && FCString::Atoi(*Tags[HOUDINI_PARAMETER_STRING_REF_TAG]) == 1) + { + bIsUnrealRef = true; + + if (Tags.Contains(HOUDINI_PARAMETER_STRING_REF_CLASS_TAG)) + { + UClass* FoundClass = FHoudiniEngineRuntimeUtils::GetClassByName(Tags[HOUDINI_PARAMETER_STRING_REF_CLASS_TAG]); + if (FoundClass != nullptr) + { + UnrealRefClass = FoundClass; + } + } + } + + if (Tags.Contains(HOUDINI_PARAMETER_STRING_MULTILINE_TAG)) + { + bIsMultiLine = true; + } + + for (int32 Idx = 0; Idx < MainParam->GetTupleSize(); ++Idx) + { + // Lambda for changing the parameter value + auto ChangeStringValueAt = [](const FString& Value, UObject* ChosenObj, const int32& Index, const bool& DoChange, const TArray>& StringParams) + { + if (StringParams.Num() == 0) + return; + + if (!IsValidWeakPointer(StringParams[0])) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterSrtingChange", "Houdini Parameter String: Changing a value"), + StringParams[0]->GetOuter()); + + bool bChanged = false; + for (int Idx = 0; Idx < StringParams.Num(); Idx++) + { + if (!IsValidWeakPointer(StringParams[Idx])) + continue; + + StringParams[Idx]->Modify(); + if (StringParams[Idx]->SetValueAt(Value, Index)) + { + StringParams[Idx]->MarkChanged(true); + bChanged = true; + } + + StringParams[Idx]->SetAssetAt(ChosenObj, Index); + } + + if (!bChanged || !DoChange) + { + // Cancel the transaction if there is no param actually has been changed + Transaction.Cancel(); + } + + FHoudiniEngineUtils::UpdateEditorProperties(false); + }; + + auto RevertToDefault = [](const int32& TupleIndex, const TArray>& StringParams) + { + for (int32 Idx = 0; Idx < StringParams.Num(); Idx++) + { + if (!IsValidWeakPointer(StringParams[Idx])) + continue; + + if (StringParams[Idx]->IsDefaultValueAtIndex(TupleIndex)) + continue; + + StringParams[Idx]->RevertToDefault(TupleIndex); + } + + return FReply::Handled(); + }; + + if (bIsUnrealRef) + { + TArray AllowedClasses; + if (UnrealRefClass != UObject::StaticClass()) + { + // Use the class specified by the user + AllowedClasses.Add(UnrealRefClass); + } + else + { + // Using UObject would list way too many assets, and take a long time to open the menu, + // so we need to reestrict the classes a bit + AllowedClasses.Add(UStaticMesh::StaticClass()); + AllowedClasses.Add(UHoudiniAsset::StaticClass()); + AllowedClasses.Add(USkeletalMesh::StaticClass()); + AllowedClasses.Add(UBlueprint::StaticClass()); + AllowedClasses.Add(UMaterialInterface::StaticClass()); + AllowedClasses.Add(UTexture::StaticClass()); + AllowedClasses.Add(ULevel::StaticClass()); + AllowedClasses.Add(UStreamableRenderAsset::StaticClass()); + AllowedClasses.Add(USoundBase::StaticClass()); + AllowedClasses.Add(UParticleSystem::StaticClass()); + AllowedClasses.Add(UFoliageType::StaticClass()); + } + + TSharedPtr EditableTextBox; + TSharedPtr HorizontalBox; + VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + SNew(SAssetDropTarget) + .OnAreAssetsAcceptableForDrop_Lambda([UnrealRefClass](TArrayView InAssets) + { + return InAssets[0].GetAsset()->IsA(UnrealRefClass); + }) + .OnAssetsDropped_Lambda([=](const FDragDropEvent&, TArrayView InAssets) + { + // Get the asset reference string for this object + UObject* InObject = InAssets[0].GetAsset(); + FString ReferenceStr = UHoudiniParameterString::GetAssetReference(InObject); + ChangeStringValueAt(ReferenceStr, InObject, Idx, true, StringParams); + }) + [ + SAssignNew(HorizontalBox, SHorizontalBox) + ] + ]; + + // Thumbnail + // Get thumbnail pool for this builder. + TSharedPtr< FAssetThumbnailPool > AssetThumbnailPool = HouParameterCategory.GetParentLayout().GetThumbnailPool(); + + // Create a thumbnail for the selected object / class + UObject* EditObject = nullptr; + const FString AssetPath = MainParam->GetValueAt(Idx); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + EditObject = StaticFindObject(nullptr, nullptr, *AssetPath, EFindObjectFlags::ExactClass); +#else + EditObject = StaticFindObject(nullptr, nullptr, *AssetPath, true); +#endif + + FAssetData AssetData; + if (IsValid(EditObject)) + { + AssetData = FAssetData(EditObject); + } + else + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + AssetData.AssetClassPath = UnrealRefClass->GetClassPathName(); +#else + AssetData.AssetClass = UnrealRefClass->GetFName(); +#endif + } + + constexpr int32 ThumbnailSize = 46; + + TSharedPtr< FAssetThumbnail > StaticMeshThumbnail = MakeShareable( + new FAssetThumbnail(AssetData, ThumbnailSize, ThumbnailSize, AssetThumbnailPool)); + + TSharedPtr ThumbnailBorder; + HorizontalBox->AddSlot() + .Padding(0, 3, 5, 0) + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SBorder) + .Visibility(EVisibility::SelfHitTestInvisible) + .Padding(FMargin(0, 0, 4, 4)) + .BorderImage(FAppStyle::Get().GetBrush("PropertyEditor.AssetTileItem.DropShadow")) + [ + SNew(SOverlay) + + SOverlay::Slot() + .Padding(1) + [ + SAssignNew(ThumbnailBorder, SBorder) + .Padding(0) + .OnMouseDoubleClick_Lambda( + [EditObject, Idx](const FGeometry&, const FPointerEvent&) + { + if (EditObject && GEditor) + GEditor->EditObject(EditObject); + + return FReply::Handled(); + }) + [ + SNew(SBox) + .WidthOverride(ThumbnailSize) + .HeightOverride(ThumbnailSize) + [ + StaticMeshThumbnail->MakeThumbnailWidget() + ] + ] + ] + + SOverlay::Slot() + [ + SNew(SImage) + .Image(TAttribute::Create( + TAttribute::FGetter::CreateLambda( + [WeakThumbnailBorder = TWeakPtr(ThumbnailBorder)]() + { + TSharedPtr ThumbnailBorderPtr = WeakThumbnailBorder.Pin(); + if (ThumbnailBorderPtr.IsValid() && ThumbnailBorderPtr->IsHovered()) + return _GetEditorStyle().GetBrush( + "PropertyEditor.AssetThumbnailBorderHovered"); + else + return _GetEditorStyle().GetBrush( + "PropertyEditor.AssetThumbnailBorder"); + } + ) + )) + .Visibility(EVisibility::SelfHitTestInvisible) + ] + ] + ]; + + FText MeshNameText = FText::GetEmpty(); + if (EditObject) + MeshNameText = FText::FromString(EditObject->GetName()); + + TSharedPtr StaticMeshComboButton; + + TSharedPtr ButtonBox; + HorizontalBox->AddSlot() + .Padding(0.0f, 4.0f, 4.0f, 4.0f) + .VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + .AutoHeight() + [ + SAssignNew(StaticMeshComboButton, SComboButton) + .ButtonContent() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(FName(TEXT("PropertyWindow.NormalFont")))) + .Text(FText::FromName(AssetData.AssetName)) + .ToolTipText(FText::FromString(MainParam->GetValueAt(Idx))) + ] + ] + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + .AutoHeight() + [ + SAssignNew(ButtonBox, SHorizontalBox) + ] + ]; + + // Create tooltip. + FFormatNamedArguments Args; + Args.Add(TEXT("Asset"), MeshNameText); + FText StaticMeshTooltip = FText::Format( + LOCTEXT( + "BrowseToSpecificAssetInContentBrowser", + "Browse to '{Asset}' in the content browser."), + Args); + + // Button : Use selected in content browser + ButtonBox->AddSlot() + .AutoWidth() + .Padding(1, 0, 3, 0) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeUseSelectedButton( + FSimpleDelegate::CreateLambda( + [AllowedClasses, ChangeStringValueAt, Idx, StringParams]() + { + if (GEditor) + { + TArray CBSelections; + GEditor->GetContentBrowserSelections(CBSelections); + + if (CBSelections.IsEmpty()) + { + return; + } + + UObject* Object = CBSelections[0].GetAsset(); + + if (!IsValid(Object)) + { + return; + } + + FString ReferenceStr = UHoudiniParameterString::GetAssetReference(Object); + + ChangeStringValueAt(ReferenceStr, Object, Idx, true, StringParams); + } + }), + TAttribute(LOCTEXT( + "GeometryInputUseSelectedAssetFromCB", + "Use the currently selected asset from the content browser."))) + ]; + + // Button : Browse Static Mesh + ButtonBox->AddSlot() + .AutoWidth() + .Padding(1, 0, 3, 0) + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateLambda([EditObject]() + { + if (GEditor && EditObject) + { + TArray Objects; + Objects.Add(EditObject); + GEditor->SyncBrowserToObjects(Objects); + } + }), + TAttribute(StaticMeshTooltip)) + ]; + TWeakPtr WeakStaticMeshComboButton(StaticMeshComboButton); + StaticMeshComboButton->SetOnGetMenuContent(FOnGetContent::CreateLambda( + [AllowedClasses, WeakStaticMeshComboButton, ChangeStringValueAt, Idx, StringParams]() + { + TArray NewAssetFactories; + return PropertyCustomizationHelpers::MakeAssetPickerWithMenu( + FAssetData(nullptr), + true, + AllowedClasses, + NewAssetFactories, + FOnShouldFilterAsset(), + FOnAssetSelected::CreateLambda( + [WeakStaticMeshComboButton, ChangeStringValueAt, Idx, StringParams](const FAssetData & AssetData) + { + TSharedPtr StaticMeshComboButtonPtr = WeakStaticMeshComboButton.Pin(); + if (StaticMeshComboButtonPtr.IsValid()) + { + StaticMeshComboButtonPtr->SetIsOpen(false); + + UObject * Object = AssetData.GetAsset(); + // Get the asset reference string for this object + // !! Accept null objects to allow clearing the asset picker !! + FString ReferenceStr = UHoudiniParameterString::GetAssetReference(Object); + + ChangeStringValueAt(ReferenceStr, Object, Idx, true, StringParams); + } + } + ), + FSimpleDelegate::CreateLambda([]() {})); + }) + ); + } + else if (bIsMultiLine) + { + TSharedPtr< SMultiLineEditableTextBox > MultiLineEditableTextBox; + VerticalBox->AddSlot().Padding(2, 2, 5, 2).AutoHeight() + [ + SNew(SAssetDropTarget) + .OnAreAssetsAcceptableForDrop_Lambda([](TArrayView InAssets) + {return true;}) + .OnAssetsDropped_Lambda([=](const FDragDropEvent&, TArrayView InAssets) + { + // Get the asset reference string for this object + FString ReferenceStr = UHoudiniParameterString::GetAssetReference(InAssets[0].GetAsset()); + + FString NewString = ReferenceStr; + if (StringParams[0]->GetValueAt(Idx).Len() > 0) + NewString = StringParams[0]->GetValueAt(Idx) + "\n" + NewString; + + ChangeStringValueAt(NewString, nullptr, Idx, true, StringParams); + }) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Top).MaxWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SAssignNew(MultiLineEditableTextBox, SMultiLineEditableTextBox) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(FText::FromString(MainParam->GetValueAt(Idx))) + .OnTextCommitted_Lambda([=](const FText& Val, ETextCommit::Type TextCommitType) { ChangeStringValueAt(Val.ToString(), nullptr, Idx, true, StringParams); }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertToDefault", "Revert to default")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility_Lambda([Idx, StringParams]() + { + for (auto & NextSelectedParam : StringParams) + { + if (!IsValidWeakPointer(NextSelectedParam)) + continue; + + if (!NextSelectedParam->IsDefaultValueAtIndex(Idx)) + return EVisibility::Visible; + } + + return EVisibility::Hidden; + }) + .OnClicked_Lambda([Idx, StringParams, RevertToDefault]() { return RevertToDefault(Idx, StringParams); }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ] + ]; + } + else + { + TSharedPtr< SEditableTextBox > EditableTextBox; + VerticalBox->AddSlot().Padding(2, 2, 5, 2) + [ + SNew(SAssetDropTarget) + .OnAreAssetsAcceptableForDrop_Lambda([](TArrayView InAssets) + {return true;}) + .OnAssetsDropped_Lambda([=](const FDragDropEvent&, TArrayView InAssets) + { + // Get the asset reference string for this object + FString ReferenceStr = UHoudiniParameterString::GetAssetReference(InAssets[0].GetAsset()); + + ChangeStringValueAt(ReferenceStr, nullptr, Idx, true, StringParams); + }) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f).MaxWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SAssignNew(EditableTextBox, SEditableTextBox) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(FText::FromString(MainParam->GetValueAt(Idx))) + .OnTextCommitted_Lambda([=](const FText& Val, ETextCommit::Type TextCommitType) + { ChangeStringValueAt(Val.ToString(), nullptr, Idx, true, StringParams); }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(LOCTEXT("RevertToDefault", "Revert to default")) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ContentPadding(0) + .Visibility_Lambda([Idx, StringParams]() + { + for (auto & NextSelectedParam : StringParams) + { + if (!IsValidWeakPointer(NextSelectedParam)) + continue; + + if (!NextSelectedParam->IsDefaultValueAtIndex(Idx)) + return EVisibility::Visible; + } + + return EVisibility::Hidden; + }) + .OnClicked_Lambda([Idx, StringParams, RevertToDefault]() + { return RevertToDefault(Idx, StringParams); }) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("PropertyWindow.DiffersFromDefault")) + ] + ] + ] + ]; + } + + } + + LabelledParameter->SetContent(VerticalBox); +} + +void +FHoudiniParameterDetails::CreateWidgetColor( + const TSharedRef LabelledParameter, + const TArray>& InParams) +{ + TArray> ColorParams; + if (!CastParameters(InParams, ColorParams)) + return; + + if (ColorParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = ColorParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + bool bHasAlpha = (MainParam->GetTupleSize() == 4); + + // Add color picker UI. + TSharedPtr ColorBlock; + TSharedRef VerticalBox = SNew(SVerticalBox); + VerticalBox->AddSlot().Padding(2, 2, 5, 2) + [ + SAssignNew(ColorBlock, SColorBlock) + .Color(MainParam->GetColorValue()) + .ShowBackgroundForAlpha(bHasAlpha) + .OnMouseButtonDown_Lambda([this, ColorParams, MainParam, bHasAlpha](const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) + { + FColorPickerArgs PickerArgs; + PickerArgs.ParentWidget = FSlateApplication::Get().GetActiveTopLevelWindow(); + PickerArgs.bUseAlpha = bHasAlpha; + PickerArgs.DisplayGamma = TAttribute< float >::Create( + TAttribute< float >::FGetter::CreateUObject(GEngine, &UEngine::GetDisplayGamma)); + PickerArgs.OnColorCommitted = FOnLinearColorValueChanged::CreateLambda([MainParam, ColorParams](FLinearColor InColor) + { + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterColorChange", "Houdini Parameter Color: Changing value"), + MainParam->GetOuter(), true); + + bool bChanged = false; + for (auto & Param : ColorParams) + { + if (!IsValidWeakPointer(Param)) + continue; + + Param->Modify(); + if (Param->SetColorValue(InColor)) + { + Param->MarkChanged(true); + bChanged = true; + } + } + + // cancel the transaction if there is actually no value changed + if (!bChanged) + { + Transaction.Cancel(); + } + }); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + PickerArgs.InitialColor = MainParam->GetColorValue(); +#else + PickerArgs.InitialColorOverride = MainParam->GetColorValue(); +#endif + PickerArgs.bOnlyRefreshOnOk = true; + OpenColorPicker(PickerArgs); + return FReply::Handled(); + }) + ]; + + LabelledParameter->SetContent(VerticalBox); +} + +void +FHoudiniParameterDetails::CreateWidgetButton( + const TSharedRef LabelledParameter, + const TArray>& InParams) +{ + TArray> ButtonParams; + if (!CastParameters(InParams, ButtonParams)) + return; + + if (ButtonParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = ButtonParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + FText ParameterLabelText = FText::FromString(MainParam->GetParameterLabel()); + FText ParameterTooltip = GetParameterTooltip(MainParam); + + TSharedRef HorizontalBox = SNew(SHorizontalBox); + TSharedPtr Button; + + // Add button UI. + HorizontalBox->AddSlot().Padding(1, 2, 4, 2) + [ + SAssignNew(Button, SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Text(ParameterLabelText) + .ToolTipText(ParameterTooltip) + .OnClicked(FOnClicked::CreateLambda( [MainParam, ButtonParams]() + { + for (auto & Param : ButtonParams) + { + if (!IsValidWeakPointer(Param)) + continue; + + // There is no undo redo operation for button + Param->MarkChanged(true); + } + + return FReply::Handled(); + })) + ]; + + LabelledParameter->SetContent(HorizontalBox); +} + +void +FHoudiniParameterDetails::CreateWidgetButtonStrip( + const TSharedRef LabelledParameter, + const TArray>& InParams) +{ + TArray> ButtonStripParams; + if (!CastParameters(InParams, ButtonStripParams)) + return; + + if (ButtonStripParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = ButtonStripParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + auto OnButtonStateChanged = [MainParam, ButtonStripParams](ECheckBoxState NewState, int32 Idx) + { + + /* + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterButtonStripChange", "Houdini Parameter Button Strip: Changing value"), + MainParam->GetOuter(), true); + */ + bool bChanged = false; + + for (auto & NextParam : ButtonStripParams) + { + if (!IsValidWeakPointer(NextParam)) + continue; + + if (NextParam->SetValueAt(NewState == ECheckBoxState::Checked, Idx)) + { + NextParam->MarkChanged(true); + bChanged = true; + } + } + + //if (!bChanged) + // Transaction.Cancel(); + + }; + + + FText ParameterLabelText = FText::FromString(MainParam->GetParameterLabel()); + FText ParameterTooltip = GetParameterTooltip(MainParam); + + TSharedRef HorizontalBox = SNew(SHorizontalBox); + FLinearColor BgColor(0.53f, 0.81f, 0.82f, 1.0f); // Sky Blue Backgroud color + + for (uint32 Idx = 0; Idx < MainParam->GetNumValues(); ++Idx) + { + const FString* LabelString = MainParam->GetStringLabelAt(Idx); + FText LabelText = LabelString ? FText::FromString(*LabelString) : FText(); + + TSharedPtr Button; + + HorizontalBox->AddSlot().Padding(0).FillWidth(1.0f) + [ + SAssignNew(Button, SCheckBox) + .Style(_GetEditorStyle(), "Property.ToggleButton.Middle") + .IsChecked(TAttribute::CreateLambda( + [MainParam, Idx]() -> ECheckBoxState + { + if (!IsValidWeakPointer(MainParam)) + { + return ECheckBoxState::Undetermined; + } + + return MainParam->GetValueAt(Idx) + ? ECheckBoxState::Checked + : ECheckBoxState::Unchecked; + })) + .OnCheckStateChanged_Lambda([OnButtonStateChanged, Idx](ECheckBoxState NewState) + { + OnButtonStateChanged(NewState, Idx); + }) + .Content() + [ + SNew(STextBlock) + .Text(LabelText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ]; + + Button->SetColorAndOpacity(BgColor); + } + + LabelledParameter->SetContent(HorizontalBox); +} + +void +FHoudiniParameterDetails::CreateWidgetLabel( + const TSharedRef LabelledParameter, + const TArray>& InParams) +{ + TArray> LabelParams; + if (!CastParameters(InParams, LabelParams)) + return; + + if (LabelParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = LabelParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + TSharedRef HorizontalBox = SNew(SHorizontalBox); + + for (int32 Index = 0; Index < MainParam->GetTupleSize(); ++Index) + { + FString NextLabelString = MainParam->GetStringAtIndex(Index); + FText ParameterLabelText = FText::FromString(NextLabelString); + + FText ParamTooltipText = FText::FromString("Column " + FString::FromInt(Index) + ": " + NextLabelString); + + TSharedPtr TextBlock; + // Add Label UI. + HorizontalBox->AddSlot() + .Padding(1, 2, 16, 2) + .AutoWidth() + [ + SAssignNew(TextBlock, STextBlock) + .Text(ParameterLabelText) + .ToolTipText(ParamTooltipText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ]; + } + + LabelledParameter->SetContent(HorizontalBox); +} + +void +FHoudiniParameterDetails::CreateWidgetToggle( + const TSharedRef LabelledParameter, + const TArray>& InParams) +{ + TArray> ToggleParams; + if (!CastParameters(InParams, ToggleParams)) + return; + + if (ToggleParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = ToggleParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + FText ParameterLabelText = FText::FromString(MainParam->GetParameterLabel()); + + TSharedRef VerticalBox = SNew(SVerticalBox); + auto IsToggleCheckedLambda = [MainParam](int32 Index) + { + if(!IsValidWeakPointer(MainParam)) + return ECheckBoxState::Unchecked; + + if (Index >= MainParam->GetNumValues()) + return ECheckBoxState::Unchecked; + + if (MainParam->GetValueAt(Index)) + return ECheckBoxState::Checked; + + return ECheckBoxState::Unchecked; + }; + + auto OnToggleCheckStateChanged = [MainParam, ToggleParams](ECheckBoxState NewState, int32 Index) + { + if (!IsValidWeakPointer(MainParam)) + return; + + if (Index >= MainParam->GetNumValues()) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterToggleChange", "Houdini Parameter Toggle: Changing value"), + MainParam->GetOuter(), true); + + bool bState = (NewState == ECheckBoxState::Checked); + + bool bChanged = false; + for (auto & Param : ToggleParams) + { + if (!IsValidWeakPointer(Param)) + continue; + + Param->Modify(); + if (Param->SetValueAt(bState, Index)) + { + bChanged = true; + Param->MarkChanged(true); + } + } + + // Cancel the transaction if no parameter has actually been changed + if (!bChanged) + { + Transaction.Cancel(); + } + }; + + for (int32 Index = 0; Index < MainParam->GetTupleSize(); ++Index) + { + TSharedPtr< SCheckBox > CheckBox; + VerticalBox->AddSlot() + .Padding(2, 2, 5, 2) + [ + SAssignNew(CheckBox, SCheckBox) + .OnCheckStateChanged_Lambda([OnToggleCheckStateChanged, Index](ECheckBoxState NewState) { + OnToggleCheckStateChanged(NewState, Index); + + }) + .IsChecked_Lambda([IsToggleCheckedLambda, Index]() { + return IsToggleCheckedLambda(Index); + }) + ]; + } + + LabelledParameter->SetContent(VerticalBox); +} + +void +FHoudiniParameterDetails::CreateWidgetFile( + const TSharedRef LabelledParameter, + const TArray>& InParams) +{ + // TODO: Cookable me! + TArray> FileParams; + if (!CastParameters(InParams, FileParams)) + return; + + if (FileParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = FileParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + TSharedRef VerticalBox = SNew(SVerticalBox); + + FString FileTypeWidgetFilter = TEXT("All files (*.*)|*.*"); + if (!MainParam->GetFileFilters().IsEmpty()) + FileTypeWidgetFilter = FString::Printf(TEXT("%s files (%s)|%s"), *MainParam->GetFileFilters(), *MainParam->GetFileFilters(), *MainParam->GetFileFilters()); + + FString BrowseWidgetDirectory = FEditorDirectories::Get().GetLastDirectory(ELastDirectory::GENERIC_OPEN); + + TMap& Tags = MainParam->GetTags(); + if (Tags.Contains(HAPI_PARAM_TAG_DEFAULT_DIR)) + { + if(!Tags[HAPI_PARAM_TAG_DEFAULT_DIR].IsEmpty()) + { + FString DefaultDir = Tags[HAPI_PARAM_TAG_DEFAULT_DIR]; + if(FPaths::DirectoryExists(DefaultDir)) + BrowseWidgetDirectory = DefaultDir; + } + } + + auto UpdateCheckRelativePath = [MainParam](const FString & PickedPath) + { + UHoudiniAssetComponent* HoudiniAssetComponent = Cast(MainParam->GetOuter()); + if (MainParam->GetOuter() && !PickedPath.IsEmpty() && FPaths::IsRelative(PickedPath)) + { + // Check if the path is relative to the UE4 project + FString AbsolutePath = FPaths::ConvertRelativePathToFull(PickedPath); + if (FPaths::FileExists(AbsolutePath)) + { + return AbsolutePath; + } + + // Check if the path is relative to the asset + if (IsValid(HoudiniAssetComponent)) + { + if (IsValid(HoudiniAssetComponent->GetHoudiniAsset())) + { + FString AssetFilePath = FPaths::GetPath(HoudiniAssetComponent->GetHoudiniAsset()->AssetFileName); + if (FPaths::FileExists(AssetFilePath)) + { + FString UpdatedFileWidgetPath = FPaths::Combine(*AssetFilePath, *PickedPath); + if (FPaths::FileExists(UpdatedFileWidgetPath)) + { + return UpdatedFileWidgetPath; + } + } + } + } + } + + return PickedPath; + }; + + for (int32 Idx = 0; Idx < MainParam->GetTupleSize(); ++Idx) + { + FString FileWidgetPath = MainParam->GetValueAt(Idx); + FString FileWidgetBrowsePath = BrowseWidgetDirectory; + + if (!FileWidgetPath.IsEmpty()) + { + FString FileWidgetDirPath = FPaths::GetPath(FileWidgetPath); + if (!FileWidgetDirPath.IsEmpty()) + FileWidgetBrowsePath = FileWidgetDirPath; + } + + bool IsDirectoryPicker = MainParam->GetParameterType() == EHoudiniParameterType::FileDir; + bool bIsNewFile = !MainParam->IsReadOnly(); + + FText BrowseTooltip = LOCTEXT("FileButtonToolTipText", "Choose a file from this computer"); + if (IsDirectoryPicker) + BrowseTooltip = LOCTEXT("DirButtonToolTipText", "Choose a directory from this computer"); + + VerticalBox->AddSlot().Padding(2, 2, 5, 2) + [ + SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.0f).MaxWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + [ + SNew(SNewFilePathPicker) + .BrowseButtonImage(_GetEditorStyle().GetBrush("PropertyWindow.Button_Ellipsis")) + .BrowseButtonStyle(_GetEditorStyle(), "HoverHintOnly") + .BrowseButtonToolTip(BrowseTooltip) + .BrowseDirectory(FileWidgetBrowsePath) + .BrowseTitle(LOCTEXT("PropertyEditorTitle", "File picker...")) + .FilePath(FileWidgetPath) + .FileTypeFilter(FileTypeWidgetFilter) + .IsNewFile(bIsNewFile) + .IsDirectoryPicker(IsDirectoryPicker) + .ToolTipText_Lambda([MainParam]() + { + // return the current param value as a tooltip + FString FileValue = MainParam.IsValid() ? MainParam->GetValueAt(0) : FString(); + return FText::FromString(FileValue); + }) + .OnPathPicked(FOnPathPicked::CreateLambda([MainParam, FileParams, UpdateCheckRelativePath, Idx](const FString & PickedPath) + { + if (MainParam->GetNumValues() <= Idx) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterFileChange", "Houdini Parameter File: Changing a file path"), + MainParam->GetOuter(), true); + + bool bChanged = false; + + for (auto & Param : FileParams) + { + if (!IsValidWeakPointer(Param)) + continue; + + Param->Modify(); + if (Param->SetValueAt(UpdateCheckRelativePath(PickedPath), Idx)) + { + bChanged = true; + Param->MarkChanged(true); + } + } + + // Cancel the transaction if no value has actually been changed + if (!bChanged) + { + Transaction.Cancel(); + } + })) + ] + ]; + + } + + LabelledParameter->SetContent(VerticalBox); +} + + +void +FHoudiniParameterDetails::CreateWidgetChoice( + const TSharedRef LabelledParameter, + const TArray>& InParams) +{ + TArray> ChoiceParams; + if (!CastParameters(InParams, ChoiceParams)) + return; + + if (ChoiceParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = ChoiceParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + // Lambda for changing the parameter value + auto ChangeSelectionLambda = [ChoiceParams](TSharedPtr< FString > NewChoice, ESelectInfo::Type SelectType) + { + if (!NewChoice.IsValid()) + return; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterChoiceChange", "Houdini Parameter Choice: Changing selection"), + ChoiceParams[0]->GetOuter()); + + const int32 NewIntValue = ChoiceParams[0]->GetIntValueFromLabel(*NewChoice); + + bool bChanged = false; + for (int Idx = 0; Idx < ChoiceParams.Num(); Idx++) + { + if (!IsValidWeakPointer(ChoiceParams[Idx])) + continue; + + ChoiceParams[Idx]->Modify(); + if (ChoiceParams[Idx]->SetIntValue(NewIntValue)) + { + bChanged = true; + ChoiceParams[Idx]->MarkChanged(true); + ChoiceParams[Idx]->UpdateStringValueFromInt(); + } + } + + if (!bChanged) + { + // Cancel the transaction if no parameter was changed + Transaction.Cancel(); + } + }; + + // + MainParam->UpdateChoiceLabelsPtr(); + TArray>* OptionSource = MainParam->GetChoiceLabelsPtr(); + TSharedPtr IntialSelec; + if (OptionSource && OptionSource->IsValidIndex(MainParam->GetIntValueIndex())) + { + IntialSelec = (*OptionSource)[MainParam->GetIntValueIndex()]; + } + + TSharedRef< SHorizontalBox > HorizontalBox = SNew(SHorizontalBox); + TSharedPtr< SComboBox< TSharedPtr< FString > > > ComboBox; + HorizontalBox->AddSlot().Padding( 2, 2, 5, 2 ) + [ + SAssignNew( ComboBox, SComboBox< TSharedPtr< FString > > ) + .OptionsSource(OptionSource) + .InitiallySelectedItem(IntialSelec) + .OnGenerateWidget_Lambda( + []( TSharedPtr< FString > InItem ) + { + return SNew(STextBlock).Text(FText::FromString(*InItem)); + }) + .OnSelectionChanged_Lambda( + [ChangeSelectionLambda](TSharedPtr< FString > NewChoice, ESelectInfo::Type SelectType) + { + ChangeSelectionLambda(NewChoice, SelectType); + }) + [ + SNew(STextBlock) + .Text_Lambda([MainParam]() { return FText::FromString(MainParam->GetLabel()); }) + .Font(_GetEditorStyle().GetFontStyle( TEXT( "PropertyWindow.NormalFont" ) ) ) + ] + ]; + + LabelledParameter->SetContent(HorizontalBox); +} + +void +FHoudiniParameterDetails::CreateWidgetSeparator( + const TSharedRef LabelledParameter, + const TArray>& InParams) +{ + if (InParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = InParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + TSharedRef HorizontalBox = SNew(SCustomizedBox); + + HorizontalBox->DividerLinePositions = DividerLinePositions; + HorizontalBox->SetHoudiniParameter(InParams); + + LabelledParameter->SetContent(HorizontalBox); +} + +void +FHoudiniParameterDetails::CreateWidgetOperatorPath( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows) +{ + if (InJoinedParams.IsEmpty()) + return; + + const auto& InParams = InJoinedParams[0]; + + TArray> OperatorPathParams; + if (!CastParameters(InParams, OperatorPathParams)) + return; + + if (OperatorPathParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = OperatorPathParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + const TWeakObjectPtr& MainInput = MainParam->HoudiniInput; + if (!MainInput.IsValid()) + return; + + // Build an array of edited inputs for multi edition + TArray> EditedInputs; + EditedInputs.Add(MainInput); + + // Add the corresponding inputs found in the other HAC + for (int LinkedIdx = 1; LinkedIdx < OperatorPathParams.Num(); LinkedIdx++) + { + UHoudiniInput* LinkedInput = OperatorPathParams[LinkedIdx]->HoudiniInput.Get(); + if (!IsValid(LinkedInput)) + continue; + + // Linked params should match the main param! If not try to find one that matches + if (!LinkedInput->Matches(*MainInput)) + continue; + + EditedInputs.Add(LinkedInput); + } + + // Create a new detail row + FDetailWidgetRow* Row = CreateNestedRow(HouParameterCategory, InJoinedParams); + if (!Row) + return; + + // Create the standard parameter name widget. + CreateNameWidget(Row, InParams, true); + + FHoudiniInputDetails::CreateWidget(HouParameterCategory, EditedInputs, Row); + + Row->ValueWidget.Widget->SetEnabled(!MainParam->IsDisabled()); + + OutRows.Add(Row); +} + +void +FHoudiniParameterDetails::CreateWidgetFloatRamp( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows) +{ + if (InJoinedParams.IsEmpty()) + return; + + const auto& InParams = InJoinedParams[0]; + + if (InParams.Num() < 1) + return; + + const TWeakObjectPtr& MainParam = InParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + if (MainParam->GetParameterType() == EHoudiniParameterType::FloatRamp) + { + UHoudiniParameterRampFloat* FloatRampParameter = Cast(MainParam); + if (FloatRampParameter) + { + CurrentRampFloat = FloatRampParameter; + FDetailWidgetRow* Row = CreateWidgetRamp(HouParameterCategory, InJoinedParams); + + OutRows.Add(Row); + } + } +} + +void +FHoudiniParameterDetails::CreateWidgetColorRamp( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows) +{ + if (InJoinedParams.IsEmpty()) + return; + + const auto& InParams = InJoinedParams[0]; + + if (InParams.Num() < 1) + return; + + const TWeakObjectPtr& MainParam = InParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + if (MainParam->GetParameterType() == EHoudiniParameterType::ColorRamp) + { + UHoudiniParameterRampColor* RampColor = Cast(MainParam); + if (RampColor) + { + CurrentRampColor = RampColor; + FDetailWidgetRow* Row = CreateWidgetRamp(HouParameterCategory, InJoinedParams); + OutRows.Add(Row); + } + } + +} + + +FDetailWidgetRow* +FHoudiniParameterDetails::CreateWidgetRamp( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams) +{ + if (InJoinedParams.IsEmpty()) + return nullptr; + + const auto& InParams = InJoinedParams[0]; + + if (InParams.Num() <= 0) + return nullptr; + + const TWeakObjectPtr& MainParam = InParams[0]; + if (!IsValidWeakPointer(MainParam)) + return nullptr; + + // Create a new detail row + FDetailWidgetRow* Row = CreateNestedRow(HouParameterCategory, InJoinedParams); + if (!Row) + return nullptr; + + // Create the standard parameter name widget with an added autoupdate checkbox. + CreateNameWidgetWithAutoUpdate(Row, InParams, true); + + TSharedRef VerticalBox = SNew(SVerticalBox); + if (MainParam->GetParameterType() == EHoudiniParameterType::ColorRamp) + { + UHoudiniParameterRampColor *RampColorParam = Cast(MainParam); + if (!RampColorParam) + return nullptr; + + TArray> ColorRampParameters; + CastParameters(InParams, ColorRampParameters); + + Row->ValueContent() + .HAlign(HAlign_Fill) + [ + SNew(SHoudiniColorRamp) + .RampParameters(ColorRampParameters) + .OnValueCommitted_Lambda( + []() { FHoudiniEngineUtils::UpdateEditorProperties(true); }) + ]; + } + else if(MainParam->GetParameterType() == EHoudiniParameterType::FloatRamp) + { + UHoudiniParameterRampFloat *RampFloatParam = Cast(MainParam); + if (!RampFloatParam) + return nullptr; + + TArray> FloatRampParameters; + CastParameters(InParams, FloatRampParameters); + + Row->ValueContent() + .HAlign(HAlign_Fill) + [ + SNew(SHoudiniFloatRamp) + .RampParameters(FloatRampParameters) + .OnValueCommitted_Lambda( + []() { FHoudiniEngineUtils::UpdateEditorProperties(true); }) + ]; + } + + return Row; +} + +void +FHoudiniParameterDetails::CreateWidgetFolderList( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows) +{ + if (InJoinedParams.IsEmpty()) + return; + + const auto& InParams = InJoinedParams[0]; + + TArray> FolderListParams; + if (!CastParameters(InParams, FolderListParams)) + return; + + if (FolderListParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = FolderListParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + // Add this folder list to the folder map + AllFoldersAndFolderLists.Add(MainParam->GetParmId(), MainParam); + + MainParam->GetTabs().Empty(); + + // A folder list will be followed by all its child folders, + // so set the CurrentFolderListSize to the tuple size, we'll process such many folder parameters right after + CurrentFolderListSize = MainParam->GetTupleSize(); + + if (MainParam->IsDirectChildOfMultiParm()) + MultiParmInstanceIndices.Add(MainParam->GetParmId(), -1); + + if (CurrentFolderListSize <= 0) // There should not be empty folder list, this will not happen normally + return; + + // The following folders belong to current folder list + CurrentFolderList = MainParam.Get(); + + // If the tab is either a tabs or radio button and the parameter is visible + if (MainParam->IsTabMenu() && MainParam->ShouldDisplay()) + { + // Set the current tabs to be not shown by default now. CreateWidgetTab will decide if the tabs is shown. + CurrentFolderList->SetTabsShown(false); + + // Create a row to hold tab buttons if the folder list is a tabs or radio button + + // CreateNestedRow does not actually create a row for tabs, it is responsible to prune the folder stack. + // ( CreateWidgetTab will be responsible to create a row according to the visibility of its outer level folders ) + FDetailWidgetRow* TabRow = CreateNestedRow(HouParameterCategory, InJoinedParams, false); + + OutRows.Add(TabRow); + } + + // When see a folder list, go depth first search at this step. + // Push an empty queue to the stack. + FolderStack.Add(TArray()); +} + + +void +FHoudiniParameterDetails::CreateWidgetFolder( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows) +{ + if (InJoinedParams.IsEmpty()) + return; + + const auto& InParams = InJoinedParams[0]; + + TArray> FolderParams; + if (!CastParameters(InParams, FolderParams)) + return; + + if (FolderParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = FolderParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + if (!IsValid(CurrentFolderList)) // This should not happen + return; + + // If a folder is invisible, its children won't be listed by HAPI. + // So just reduce FolderListSize by 1, reduce the child counter of its parent folder by 1 if necessary, + // and prune the stack in such case. + + // NOTE: Andy: I'm not sure the above comment is correct anymore. However, we do need to special work if processing tabs. + if (!MainParam->IsVisible() && MainParam->IsTab()) + { + CurrentFolderListSize -= 1; + + if (CurrentFolderListSize == 0) + { + if (FolderStack.Num() > 1) + { + TArray &ParentFolderQueue = FolderStack[FolderStack.Num() - 2]; + if (ParentFolderQueue.Num() > 0 && IsValid(ParentFolderQueue[0])) + ParentFolderQueue[0]->GetChildCounter() -= 1; + } + + CreateWidgetTabUIElements(HouParameterCategory, MainParam, OutRows); + + PruneStack(); + + CurrentFolderList = nullptr; + } + + + return; + } + // We expect 'TupleSize' children param of this folder after finish processing all the child folders of cur folderlist + MainParam->ResetChildCounter(); + + // Add this folder to the folder map + AllFoldersAndFolderLists.Add(MainParam->GetParmId(), MainParam); + + // Set the parent param to current folderList. + // it was parent multiparm's id if this folder is a child of a multiparms. + // This will cause problem if the folder is inside of a multiparm + MainParam->SetParentParmId(CurrentFolderList->GetParmId()); + + + // Case 1: The folder is a direct child of a multiparm. + if (MainParam->IsDirectChildOfMultiParm()) + { + if (FolderStack.Num() <= 0) // This should not happen + return; + + // Get its parent multiparm first + TWeakObjectPtr ParentMultiParm; + { + UHoudiniParameterFolderList * ParentFolderList = nullptr; + if (!AllFoldersAndFolderLists.Contains(MainParam->GetParentParmId())) + return; + + ParentFolderList = Cast(AllFoldersAndFolderLists[MainParam->GetParentParmId()]); + + if (!ParentFolderList) + return; + + if (AllMultiParms.Contains(ParentFolderList->GetParentParmId())) + ParentMultiParm = AllMultiParms[ParentFolderList->GetParentParmId()]; + + if (!ParentMultiParm.IsValid()) // This should not happen + return; + } + + bool bShown = ParentMultiParm->IsShown(); + + // Case 1-1: The folder is NOT tabs + if (!MainParam->IsTab()) + { + bShown = MainParam->IsExpanded() && bShown; + + // If the parent multiparm is shown. + if (ParentMultiParm->IsShown()) + { + FDetailWidgetRow* FolderHeaderRow = CreateNestedRow(HouParameterCategory, InJoinedParams, false); + CreateFolderHeaderUI(HouParameterCategory, FolderHeaderRow, InParams); + + OutRows.Add(FolderHeaderRow); + } + } + // Case 1-2: The folder IS tabs. + else + { + CreateWidgetTab(HouParameterCategory, MainParam, ParentMultiParm->IsShown(), OutRows); + } + + // Push the folder to the queue if it is not a tab folder + // This step is handled by CreateWidgetTab() if it is tabs + if ((!MainParam->IsTab() || !ParentMultiParm->IsShown()) && MainParam->GetTupleSize() > 0) + { + TArray & MyQueue = FolderStack.Last(); + MainParam->SetIsContentShown(bShown); + MyQueue.Add(MainParam.Get()); + } + } + + // Case 2: The folder is NOT a direct child of a multiparm. + else + { + // Case 2-1: The folder is in another folder. + if (FolderStack.Num() > 1 && CurrentFolderListSize > 0) + { + TArray & MyFolderQueue = FolderStack.Last(); + TArray & ParentFolderQueue = FolderStack[FolderStack.Num() - 2]; + + if (ParentFolderQueue.Num() <= 0) //This should happen + return; + + // Peek the folder queue of the last layer to get its parent folder parm. + bool ParentFolderVisible = ParentFolderQueue[0]->IsContentShown(); + + // If this folder is expanded (selected if is tabs) + bool bExpanded = ParentFolderVisible; + + // Case 2-1-1: The folder is NOT in a tab menu. + if (!MainParam->IsTab()) + { + bExpanded &= MainParam->IsExpanded(); + + // The parent folder is visible. + if (ParentFolderVisible) + { + // Add the folder header UI. + FDetailWidgetRow* FolderHeaderRow = CreateNestedRow(HouParameterCategory, InJoinedParams, false); + CreateFolderHeaderUI(HouParameterCategory, FolderHeaderRow, InParams); + + OutRows.Add(FolderHeaderRow); + } + + MainParam->SetIsContentShown(bExpanded); + MyFolderQueue.Add(MainParam.Get()); + } + // Case 2-1-2: The folder IS in a tab menu. + else + { + bExpanded &= MainParam->IsChosen(); + + CreateWidgetTab(HouParameterCategory, MainParam, ParentFolderVisible, OutRows); + } + } + // Case 2-2: The folder is in the root. + else + { + bool bExpanded = true; + + // Case 2-2-1: The folder is NOT under a tab menu. + if (!MainParam->IsTab()) + { + if (FolderStack.Num() <= 0) // This will not happen + return; + + // Create Folder header under root. + FDetailWidgetRow* FolderRow = CreateNestedRow(HouParameterCategory, InJoinedParams, false); + CreateFolderHeaderUI(HouParameterCategory, FolderRow, InParams); + OutRows.Add(FolderRow); + + if (FolderStack.Num() == 0) // This should not happen + return; + + TArray& MyFolderQueue = FolderStack[0]; + bExpanded &= MainParam->IsExpanded(); + MainParam->SetIsContentShown(bExpanded); + MyFolderQueue.Add(MainParam.Get()); + } + // Case 2-2-2: The folder IS under a tab menu. + else + { + // Tabs in root is always visible + CreateWidgetTab(HouParameterCategory, MainParam, true, OutRows); + } + } + } + + CurrentFolderListSize -= 1; + + // Prune the stack if finished parsing current folderlist + if (CurrentFolderListSize == 0) + { + if (FolderStack.Num() > 1 && !MainParam->IsDirectChildOfMultiParm()) + { + TArray & ParentFolderQueue = FolderStack[FolderStack.Num() - 2]; + if (ParentFolderQueue.Num() > 0 && IsValid(ParentFolderQueue[0])) + ParentFolderQueue[0]->GetChildCounter() -= 1; + } + + PruneStack(); + + CurrentFolderList = nullptr; + } +} + +void +FHoudiniParameterDetails::CreateFolderHeaderUI( + IDetailCategoryBuilder& HouParameterCategory, + FDetailWidgetRow* HeaderRow, + const TArray> &InParams) +{ + if (!HeaderRow) // The folder is invisible. + return; + + TArray> FolderParams; + if (!CastParameters(InParams, FolderParams)) + return; + + if (FolderParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = FolderParams[0]; + + if (!IsValidWeakPointer(MainParam)) + return; + + TSharedPtr VerticalBox; + + FString LabelStr = MainParam->GetParameterLabel(); + + TSharedPtr HorizontalBox; + TSharedPtr ExpanderArrow; + TSharedPtr ExpanderImage; + + HeaderRow->NameWidget.Widget = SAssignNew(HorizontalBox, SCustomizedBox); + + HorizontalBox->AddIndentation(MainParam, AllMultiParms, AllFoldersAndFolderLists); + HorizontalBox->DividerLinePositions = DividerLinePositions; + HorizontalBox->SetHoudiniParameter(InParams); + + if (MainParam->IsDirectChildOfMultiParm() && MainParam->GetChildIndex() == 1) + { + int32 CurrentMultiParmInstanceIndex = 0; + if (MultiParmInstanceIndices.Contains(MainParam->GetParentParmId())) + { + MultiParmInstanceIndices[MainParam->GetParentParmId()] += 1; + CurrentMultiParmInstanceIndex = MultiParmInstanceIndices[MainParam->GetParentParmId()]; + LabelStr = LabelStr + TEXT(" (") + FString::FromInt(CurrentMultiParmInstanceIndex) + TEXT(")"); + } + + CreateWidgetMultiParmObjectButtons(HorizontalBox, InParams); + } + + HorizontalBox->AddSlot().Padding(1.0f).VAlign(VAlign_Center).AutoWidth() + [ + SAssignNew(ExpanderArrow, SButton) + .ButtonStyle(_GetEditorStyle(), "NoBorder") + .ClickMethod(EButtonClickMethod::MouseDown) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([=, &HouParameterCategory]() + { + if (!IsValidWeakPointer(MainParam)) + return FReply::Handled(); + + MainParam->ExpandButtonClicked(); + + FHoudiniEngineUtils::UpdateEditorProperties(true); + + return FReply::Handled(); + }) + [ + SAssignNew(ExpanderImage, SImage) + .ColorAndOpacity(FSlateColor::UseForeground()) + ] + ]; + + + FText LabelText = FText::FromString(LabelStr); + + HorizontalBox->AddSlot().Padding(1.0f).VAlign(VAlign_Center).AutoWidth() + [ + SNew(STextBlock) + .Text(LabelText) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ]; + + TWeakPtr WeakExpanderArrow(ExpanderArrow); + ExpanderImage->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda([MainParam, WeakExpanderArrow]() + { + FName ResourceName; + TSharedPtr ExpanderArrowPtr = WeakExpanderArrow.Pin(); + + bool bIsExpanded = false; + if (IsValidWeakPointer(MainParam)) + { + bIsExpanded = MainParam->IsExpanded(); + } + + if (bIsExpanded) + { + ResourceName = ExpanderArrowPtr.IsValid() && ExpanderArrowPtr->IsHovered() ? "TreeArrow_Expanded_Hovered" : "TreeArrow_Expanded"; + } + else + { + ResourceName = ExpanderArrowPtr.IsValid() && ExpanderArrowPtr->IsHovered() ? "TreeArrow_Collapsed_Hovered" : "TreeArrow_Collapsed"; + } + + return _GetEditorStyle().GetBrush(ResourceName); + } + ) + )); + + if(MainParam->GetFolderType() == EHoudiniFolderParameterType::Simple) + ExpanderArrow->SetEnabled(false); + +} + +void +FHoudiniParameterDetails::CreateWidgetTabUIElements( + IDetailCategoryBuilder& HouParameterCategory, + const TWeakObjectPtr& InFolder, + TArray& OutRows) +{ + UHoudiniParameterFolder* const Folder = InFolder.Get(); + TArray& FolderQueue = FolderStack.Last(); + + if (CurrentFolderListSize > 1) + return; + + // Do not draw anything for empty Tabs! + // This would create extra lines in the Param UI, and adds extra dividers to the following parameters. + if (CurrentTabs.IsEmpty()) + return; + + // The tabs belong to current folder list + UHoudiniParameterFolderList* CurrentTabMenuFolderList = CurrentFolderList; + + // Create a row (UI) for current tabs + TSharedPtr HorizontalBox; + FDetailWidgetRow& Row = HouParameterCategory.AddCustomRow(FText::GetEmpty()) + [ + SAssignNew(HorizontalBox, SCustomizedBox) + ]; + + // Put current tab folder list param into an array + TArray> CurrentTabMenuFolderListArr; + CurrentTabMenuFolderListArr.Add(CurrentTabMenuFolderList); + + HorizontalBox->SetHoudiniParameter(CurrentTabMenuFolderListArr); + DividerLinePositions.Add(HorizontalBox->AddIndentation(InFolder, AllMultiParms, AllFoldersAndFolderLists)); + HorizontalBox->DividerLinePositions = DividerLinePositions; + + float DesiredHeight = 0.0f; + float DesiredWidth = 0.0f; + + // Process all tabs of current folder list at once when done. + + for (auto& CurTab : CurrentTabs) + { + if (!IsValid(CurTab)) + continue; + + CurTab->SetIsContentShown(CurTab->IsChosen()); + FolderQueue.Add(CurTab); + + auto OnTabClickedLambda = [CurrentTabMenuFolderList, CurTab, &HouParameterCategory]() + { + if (CurrentTabMenuFolderList) + { + if (!CurrentTabMenuFolderList->bIsTabMenu || CurrentTabMenuFolderList->TabFolders.Num() <= 0) + return FReply::Handled(); + + if (CurTab->IsChosen()) + return FReply::Handled(); + + CurTab->SetChosen(true); + + for (UHoudiniParameterFolder* NextFolder : CurrentTabMenuFolderList->TabFolders) + { + if (CurTab->GetParmId() != NextFolder->GetParmId() && NextFolder->IsChosen()) + NextFolder->SetChosen(false); + } + + FHoudiniEngineUtils::UpdateEditorProperties(true); + } + + return FReply::Handled(); + }; + + FString FolderLabelString = TEXT(" ") + CurTab->GetParameterLabel(); + if (CurTab->GetFolderType() == EHoudiniFolderParameterType::Radio) + FolderLabelString = TEXT(" ") + FolderLabelString; + + bool bChosen = CurTab->IsTab() && CurTab->IsChosen(); + + TSharedPtr CurCustomizedButton; + + HorizontalBox->AddSlot().VAlign(VAlign_Bottom) + .AutoWidth() + .Padding(0.f) + .HAlign(HAlign_Left) + [ + SAssignNew(CurCustomizedButton, SCustomizedButton) + .OnClicked_Lambda(OnTabClickedLambda) + .Content() + [ + SNew(STextBlock) + .Text(FText::FromString(FolderLabelString)) + ] + ]; + + CurCustomizedButton->bChosen = bChosen; + CurCustomizedButton->bIsRadioButton = CurTab->GetFolderType() == EHoudiniFolderParameterType::Radio; + + DesiredHeight = CurCustomizedButton->GetDesiredSize().Y; + DesiredWidth += CurCustomizedButton->GetDesiredSize().X; + } + + HorizontalBox->bIsTabFolderListRow = true; + + Row.WholeRowWidget.MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH); + + // Set the current tabs to be shown, since slate widgets have been created + CurrentTabMenuFolderList->SetTabsShown(true); + + // Clear the temporary tabs + CurrentTabs.Empty(); + + OutRows.Add(&Row); +} + +void FHoudiniParameterDetails::CreateWidgetTab( + IDetailCategoryBuilder& HouParameterCategory, + const TWeakObjectPtr& InFolder, + const bool bIsShown, + TArray& OutRows) +{ + if (!InFolder.IsValid() || !CurrentFolderList) + return; + + if (FolderStack.Num() <= 0) // error state + return; + + UHoudiniParameterFolder* const Folder = InFolder.Get(); + TArray & FolderQueue = FolderStack.Last(); + + // Cache all tabs of current tab folder list. + CurrentFolderList->AddTabFolder(Folder); + + // If the tabs is not shown, just push the folder param into the queue. + if (!bIsShown) + { + InFolder->SetIsContentShown(bIsShown); + FolderQueue.Add(Folder); + return; + } + + // tabs currently being processed + CurrentTabs.Add(Folder); + + if (CurrentFolderListSize > 1) + return; + + CreateWidgetTabUIElements(HouParameterCategory, InFolder, OutRows); +} + +void +FHoudiniParameterDetails::CreateWidgetMultiParm( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows) +{ + if (InJoinedParams.IsEmpty()) + return; + + const auto& InParams = InJoinedParams[0]; + + TArray> MultiParmParams; + if (!CastParameters(InParams, MultiParmParams)) + return; + + if (MultiParmParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = MultiParmParams[0]; + if (!IsValidWeakPointer(MainParam)) + return; + + // Add current multiparm parameter to AllmultiParms map + AllMultiParms.Add(MainParam->GetParmId(), MainParam); + + // Create a new detail row + FDetailWidgetRow* Row = CreateNestedRow(HouParameterCategory, InJoinedParams); + + if (!Row) + { + MainParam->SetIsShown(false); + return; + } + + MainParam->SetIsShown(true); + + MultiParmInstanceIndices.Add(MainParam->GetParmId(), -1); + + CreateNameWidget(Row, InParams, true); + + auto OnInstanceValueChangedLambda = [MainParam](int32 InValue, ETextCommit::Type CommitType) + { + if (CommitType != ETextCommit::Type::OnEnter && CommitType != ETextCommit::Type::OnUserMovedFocus) + return; + + if (InValue < 0) + return; + + if (MainParam->SetNumElements(InValue)) + MainParam->MarkChanged(true); + }; + + // Add multiparm UI. + TSharedRef HorizontalBox = SNew(SHorizontalBox); + TSharedPtr< SNumericEntryBox< int32 > > NumericEntryBox; + + HorizontalBox->AddSlot().Padding(2, 2, 5, 2) + [ + SAssignNew(NumericEntryBox, SNumericEntryBox< int32 >) + .AllowSpin(true) + + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .OnValueCommitted(SNumericEntryBox::FOnValueCommitted::CreateLambda([OnInstanceValueChangedLambda](int32 InValue, ETextCommit::Type CommitType) { + OnInstanceValueChangedLambda(InValue, CommitType); + })) + .Value(MainParam->MultiParmInstanceCount) + ]; + + HorizontalBox->AddSlot().AutoWidth().Padding(2.0f, 0.0f) + [ + PropertyCustomizationHelpers::MakeAddButton(FSimpleDelegate::CreateLambda([MainParam, MultiParmParams]() + { + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterMultiParamAddInstance", "Houdini Parameter Multi Parameter: Adding an instance"), + MainParam->GetOuter(), true); + + for (auto& Param : MultiParmParams) + { + if (!IsValidWeakPointer(Param)) + continue; + + // Add a reverse step for redo/undo + Param->MultiParmInstanceLastModifyArray.Add(EHoudiniMultiParmModificationType::Removed); + + Param->MarkChanged(true); + Param->Modify(); + + if (Param->MultiParmInstanceLastModifyArray.Num() > 0) + Param->MultiParmInstanceLastModifyArray.RemoveAt(Param->MultiParmInstanceLastModifyArray.Num() - 1); + + Param->InsertElement(); + + } + }), + LOCTEXT("AddMultiparmInstanceToolTipAddLastInstance", "Add an Instance"), true) + ]; + + HorizontalBox->AddSlot().AutoWidth().Padding(2.0f, 0.0f) + [ + // Remove the last multiparm instance + PropertyCustomizationHelpers::MakeRemoveButton(FSimpleDelegate::CreateLambda([MainParam, MultiParmParams]() + { + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterMultiParamDeleteInstance", "Houdini Parameter Multi Parameter: Deleting an instance"), + MainParam->GetOuter(), true); + + for (auto & Param : MultiParmParams) + { + TArray& LastModifiedArray = Param->MultiParmInstanceLastModifyArray; + int32 RemovedIndex = LastModifiedArray.Num() - 1; + while (LastModifiedArray.IsValidIndex(RemovedIndex) && LastModifiedArray[RemovedIndex] == EHoudiniMultiParmModificationType::Removed) + RemovedIndex -= 1; + + // Add a reverse step for redo/undo + EHoudiniMultiParmModificationType PreviousModType = EHoudiniMultiParmModificationType::None; + if (LastModifiedArray.IsValidIndex(RemovedIndex)) + { + PreviousModType = LastModifiedArray[RemovedIndex]; + LastModifiedArray[RemovedIndex] = EHoudiniMultiParmModificationType::Inserted; + } + + Param->MarkChanged(true); + + Param->Modify(); + + if (LastModifiedArray.IsValidIndex(RemovedIndex)) + { + LastModifiedArray[RemovedIndex] = PreviousModType; + } + + Param->RemoveElement(RemovedIndex); + } + + }), + LOCTEXT("RemoveLastMultiParamLastToolTipRemoveLastInstance", "Remove the last instance"), true) + ]; + + HorizontalBox->AddSlot().AutoWidth().Padding(2.0f, 0.0f) + [ + PropertyCustomizationHelpers::MakeEmptyButton(FSimpleDelegate::CreateLambda([MainParam, MultiParmParams]() + { + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterMultiParamDeleteAllInstances", "Houdini Parameter Multi Parameter: Deleting all instances"), + MainParam->GetOuter(), true); + + for (auto& Param : MultiParmParams) + { + TArray& LastModifiedArray = Param->MultiParmInstanceLastModifyArray; + TArray IndicesToReverse; + + for (int32 Index = 0; Index < LastModifiedArray.Num(); ++Index) + { + if (LastModifiedArray[Index] == EHoudiniMultiParmModificationType::None) + { + LastModifiedArray[Index] = EHoudiniMultiParmModificationType::Inserted; + IndicesToReverse.Add(Index); + } + } + + Param->MarkChanged(true); + + Param->Modify(); + + for (int32& Index : IndicesToReverse) + { + if (LastModifiedArray.IsValidIndex(Index)) + LastModifiedArray[Index] = EHoudiniMultiParmModificationType::None; + } + + + Param->EmptyElements(); + } + + }), + LOCTEXT("HoudiniParameterRemoveAllMultiparmInstancesToolTip", "Remove all instances"), true) + ]; + + Row->ValueWidget.Widget = HorizontalBox; + Row->ValueWidget.MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH); + + OutRows.Add(Row); +} + +void +FHoudiniParameterDetails::CreateWidgetMultiParmObjectButtons( + TSharedPtr HorizontalBox, + const TArray>& InParams) +{ + if (InParams.Num() <= 0) + return; + + const TWeakObjectPtr& MainParam = InParams[0]; + + if (!IsValidWeakPointer(MainParam)) + return; + + if (!HorizontalBox || !AllMultiParms.Contains(MainParam->GetParentParmId()) || !MultiParmInstanceIndices.Contains(MainParam->GetParentParmId())) + return; + + const TWeakObjectPtr& MainParentMultiParm = AllMultiParms[MainParam->GetParentParmId()]; + + if (!IsValidWeakPointer(MainParentMultiParm)) + return; + + if (!MainParentMultiParm->IsShown()) + return; + + // push all parent multiparm of the InParams to the array + TArray> ParentMultiParams; + for (auto & InParam : InParams) + { + if (!IsValidWeakPointer(InParam)) + continue; + + if (!MultiParmInstanceIndices.Contains(InParam->GetParentParmId())) + continue; + + if (InParam->GetChildIndex() == 0) + { + const TWeakObjectPtr& ParentMultiParm = AllMultiParms[InParam->GetParentParmId()]; + + if (ParentMultiParm.IsValid()) + ParentMultiParams.Add(ParentMultiParm); + } + } + + + int32 InstanceIndex = MultiParmInstanceIndices[MainParam->GetParentParmId()]; + + TSharedRef AddButton = PropertyCustomizationHelpers::MakeAddButton(FSimpleDelegate::CreateLambda([ParentMultiParams, InstanceIndex]() + { + for (auto & ParentParam : ParentMultiParams) + { + // Add button call back + if (!IsValidWeakPointer(ParentParam)) + continue; + + TArray& LastModifiedArray = ParentParam->MultiParmInstanceLastModifyArray; + + if (!LastModifiedArray.IsValidIndex(InstanceIndex)) + continue; + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterMultiParmAddBeforeCurInstance", "Houdini Parameter Multi Parm: Adding an instance"), + ParentParam->GetOuter(), true); + + + int32 Index = InstanceIndex; + + // Add a reverse step for undo/redo + if (Index >= LastModifiedArray.Num()) + LastModifiedArray.Add(EHoudiniMultiParmModificationType::Removed); + else + LastModifiedArray.Insert(EHoudiniMultiParmModificationType::Removed, Index); + + ParentParam->MarkChanged(true); + ParentParam->Modify(); + + if (Index >= LastModifiedArray.Num() - 1 && LastModifiedArray.Num()) + LastModifiedArray.RemoveAt(LastModifiedArray.Num() - 1); + else + LastModifiedArray.RemoveAt(Index); + + ParentParam->InsertElementAt(InstanceIndex); + + } + }), + LOCTEXT("HoudiniParameterMultiParamAddBeforeCurrentInstanceToolTip", "Insert an instance before this instance")); + + + TSharedRef RemoveButton = PropertyCustomizationHelpers::MakeRemoveButton(FSimpleDelegate::CreateLambda([ParentMultiParams, InstanceIndex]() + { + for (auto & ParentParam : ParentMultiParams) + { + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniParameterMultiParmDeleteCurInstance", "Houdini Parameter Multi Parm: Deleting an instance"), + ParentParam->GetOuter(), true); + + + TArray& LastModifiedArray = ParentParam->MultiParmInstanceLastModifyArray; + + int32 Index = InstanceIndex; + EHoudiniMultiParmModificationType PreviousModType = EHoudiniMultiParmModificationType::None; + while (LastModifiedArray.IsValidIndex(Index) && LastModifiedArray[Index] == EHoudiniMultiParmModificationType::Removed) + { + Index -= 1; + } + + if (LastModifiedArray.IsValidIndex(Index)) + { + PreviousModType = LastModifiedArray[Index]; + LastModifiedArray[Index] = EHoudiniMultiParmModificationType::Inserted; + } + + ParentParam->MarkChanged(true); + + ParentParam->Modify(); + + if (LastModifiedArray.IsValidIndex(Index)) + { + LastModifiedArray[Index] = PreviousModType; + } + + ParentParam->RemoveElement(InstanceIndex); + } + + }), + LOCTEXT("HoudiniParameterMultiParamDeleteCurrentInstanceToolTip", "Remove an instance"), true); + + + HorizontalBox->AddSlot().AutoWidth().Padding(0.0f, 0.0f)[AddButton]; + HorizontalBox->AddSlot().AutoWidth().Padding(0.0f, 0.0f)[RemoveButton]; + + int32 StartIdx = MainParam->GetParameterType() == EHoudiniParameterType::Folder ? 1 : 0; + if (MainParam->GetChildIndex() != StartIdx) + { + AddButton.Get().SetVisibility(EVisibility::Hidden); + RemoveButton.Get().SetVisibility(EVisibility::Hidden); + } + +} + +void +FHoudiniParameterDetails::PruneStack() +{ + for (int32 StackItr = FolderStack.Num() - 1; StackItr >= 0; --StackItr) + { + TArray &CurrentQueue = FolderStack[StackItr]; + + for (int32 QueueItr = CurrentQueue.Num() - 1; QueueItr >= 0; --QueueItr) + { + UHoudiniParameterFolder * CurrentFolder = CurrentQueue[QueueItr]; + if (!IsValid(CurrentFolder)) + continue; + + if (CurrentFolder->GetChildCounter() == 0) + { + CurrentQueue.RemoveAt(QueueItr); + } + } + + if (CurrentQueue.Num() == 0) + { + FolderStack.RemoveAt(StackItr); + } + } +} + +FText +FHoudiniParameterDetails::GetParameterTooltip(const TWeakObjectPtr& InParam) +{ + if (!IsValidWeakPointer(InParam)) + return FText(); + + // Tooltip starts with Label (name) + FString Tooltip = InParam->GetParameterLabel() + TEXT(" (") + InParam->GetParameterName() + TEXT(")"); + + // Append the parameter type + FString ParmTypeStr = GetParameterTypeString(InParam->GetParameterType(), InParam->GetTupleSize()); + if (!ParmTypeStr.IsEmpty()) + Tooltip += TEXT("\n") + ParmTypeStr; + + // If the parameter has some help, append it + FString Help = InParam->GetParameterHelp(); + if (!Help.IsEmpty()) + Tooltip += TEXT("\n") + Help; + + // If the parameter has an expression, append it + if (InParam->HasExpression()) + { + FString Expr = InParam->GetExpression(); + if (!Expr.IsEmpty()) + Tooltip += TEXT("\nExpression: ") + Expr; + } + + return FText::FromString(Tooltip); +} + +FString +FHoudiniParameterDetails::GetParameterTypeString( + const EHoudiniParameterType& InType, + const int32& InTupleSize) +{ + FString ParamStr; + + switch (InType) + { + case EHoudiniParameterType::Button: + ParamStr = TEXT("Button"); + break; + + case EHoudiniParameterType::ButtonStrip: + ParamStr = TEXT("Button Strip"); + break; + + case EHoudiniParameterType::Color: + { + if (InTupleSize == 4) + ParamStr = TEXT("Color with Alpha"); + else + ParamStr = TEXT("Color"); + } + break; + + case EHoudiniParameterType::ColorRamp: + ParamStr = TEXT("Color Ramp"); + break; + + case EHoudiniParameterType::File: + ParamStr = TEXT("File (") + FString::FromInt(InTupleSize) + TEXT(" tuple)"); + break; + + case EHoudiniParameterType::FileDir: + ParamStr = TEXT("File Dir (") + FString::FromInt(InTupleSize) + TEXT(" tuple)"); + break; + + case EHoudiniParameterType::FileGeo: + ParamStr = TEXT("File Geo (") + FString::FromInt(InTupleSize) + TEXT(" tuple)"); + break; + + case EHoudiniParameterType::FileImage: + ParamStr = TEXT("File Image (") + FString::FromInt(InTupleSize) + TEXT(" tuple)"); + break; + + case EHoudiniParameterType::Float: + ParamStr = TEXT("Float (VEC") + FString::FromInt(InTupleSize) + TEXT(")"); + break; + + case EHoudiniParameterType::FloatRamp: + ParamStr = TEXT("Float Ramp"); + break; + + case EHoudiniParameterType::Folder: + case EHoudiniParameterType::FolderList: + break; + + case EHoudiniParameterType::Input: + ParamStr = TEXT("Opearator Path"); + break; + + case EHoudiniParameterType::Int: + ParamStr = TEXT("Integer (VEC") + FString::FromInt(InTupleSize) + TEXT(")"); + break; + + case EHoudiniParameterType::IntChoice: + ParamStr = TEXT("Int Choice"); + break; + + case EHoudiniParameterType::Label: + ParamStr = TEXT("Label (") + FString::FromInt(InTupleSize) + TEXT(" tuple)"); + break; + + case EHoudiniParameterType::MultiParm: + ParamStr = TEXT("MultiParm"); + break; + + case EHoudiniParameterType::Separator: + break; + + case EHoudiniParameterType::String: + ParamStr = TEXT("String (") + FString::FromInt(InTupleSize) + TEXT(" tuple)"); + break; + + case EHoudiniParameterType::StringAssetRef: + ParamStr = TEXT("String Asset Ref (") + FString::FromInt(InTupleSize) + TEXT(" tuple)"); + break; + + case EHoudiniParameterType::StringChoice: + ParamStr = TEXT("String Choice"); + break; + + case EHoudiniParameterType::Toggle: + ParamStr = TEXT("Toggle (") + FString::FromInt(InTupleSize) + TEXT(" tuple)"); + break; + + default: + ParamStr = TEXT("invalid parameter type"); + break; + } + + + return ParamStr; +} + +bool +FHoudiniParameterDetails::ShouldJoinNext(const UHoudiniParameter& InParam) +{ + switch (InParam.GetParameterType()) + { + // Special case where we do not want to support joining a widget that is joinable in Houdini. + // For example, input parameter widget is a much larger widget in Unreal than in Houdini. + case EHoudiniParameterType::Input: + return false; + default: + return InParam.GetJoinNext(); + } +} + +// Check recursively if a parameter hits the end of a visible tabs +void +FHoudiniParameterDetails::RemoveTabDividers( + IDetailCategoryBuilder& HouParameterCategory, + const TWeakObjectPtr& InParam) +{ + if (!IsValidWeakPointer(InParam)) + return; + + // When the paramId is invalid, the directory won't parse. + // So simply return the function + if (InParam->GetParmId() < 0) + return; + + // Do not end the tab if this param is a non empty parent type, leave it to its children + EHoudiniParameterType ParmType = InParam->GetParameterType(); + if ((ParmType == EHoudiniParameterType::FolderList || + ParmType == EHoudiniParameterType::Folder) && InParam->GetTupleSize() > 0) + return; + + if (ParmType == EHoudiniParameterType::MultiParm) + { + UHoudiniParameterMultiParm* InMultiParm = Cast(InParam); + if (!InMultiParm) + return; + + if (InMultiParm->MultiParmInstanceCount * InMultiParm->MultiParmInstanceLength > 0) + return; + } + + int32 ParentParamId = InParam->GetParentParmId(); + TWeakObjectPtr CurParam = InParam; + + while (AllFoldersAndFolderLists.Contains(ParentParamId) || AllMultiParms.Contains(ParentParamId)) + { + if (AllMultiParms.Contains(ParentParamId)) + { + // The parent is a multiparm + const TWeakObjectPtr& ParentMultiParm = AllMultiParms[ParentParamId]; + if (!IsValidWeakPointer(ParentMultiParm)) + return; + + if (ParentMultiParm->MultiParmInstanceCount * ParentMultiParm->MultiParmInstanceLength - 1 == CurParam->GetChildIndex()) + { + ParentParamId = ParentMultiParm->GetParentParmId(); + CurParam = ParentMultiParm; + + continue; + } + else + { + // return directly if the parameter is not the last child param of the multiparm + return; + } + } + else + { + // The parent is a folder or folderlist + TWeakObjectPtr ParentFolderParam = AllFoldersAndFolderLists[ParentParamId]; + CurParam = ParentFolderParam; + + if (!IsValidWeakPointer(ParentFolderParam)) + return; + + if (ParentFolderParam->GetParameterType() == EHoudiniParameterType::Folder) + { + // The parent is a folder + ParentParamId = ParentFolderParam->GetParentParmId(); + continue; + } + else + { + // The parent is a folderlist + UHoudiniParameterFolderList const* const ParentFolderList = Cast(ParentFolderParam); + if (!IsValid(ParentFolderList)) + return; + + if (ParentFolderList->IsTabMenu() && ParentFolderList->IsTabsShown() && ParentFolderList->IsTabParseFinished() && DividerLinePositions.Num() > 0) + { + if (!CurrentTabEndingRow) + CreateTabEndingRow(HouParameterCategory); + + if (CurrentTabEndingRow && CurrentTabEndingRow->DividerLinePositions.Num() > 0) + { + CurrentTabEndingRow->EndingDividerLinePositions.Add(DividerLinePositions.Top()); + CurrentTabEndingRow->DividerLinePositions.Pop(); + } + + DividerLinePositions.Pop(); + + ParentParamId = ParentFolderList->GetParentParmId(); + } + else + { + return; + } + } + } + } +} + +bool FHoudiniParameterDetails::IsLabelVisible( + const TArray>& InParams) +{ + if (InParams.Num() <= 0) + return false; + + const TWeakObjectPtr& MainParam = InParams[0]; + if (!IsValidWeakPointer(MainParam)) + return false; + + switch (MainParam->GetParameterType()) + { + case EHoudiniParameterType::Separator: + case EHoudiniParameterType::Button: + return false; + default: + return MainParam->IsLabelVisible(); + } +} + +bool FHoudiniParameterDetails::UsesWholeRow(const TArray>& InParams) +{ + if (InParams.Num() <= 0) + return false; + + const TWeakObjectPtr& MainParam = InParams[0]; + if (!IsValidWeakPointer(MainParam)) + return false; + + switch (MainParam->GetParameterType()) + { + case EHoudiniParameterType::Separator: + return true; + default: + return false; + } +} + +bool FHoudiniParameterDetails::ShouldWidgetFill(EHoudiniParameterType ParameterType) +{ + switch (ParameterType) + { + case EHoudiniParameterType::IntChoice: + case EHoudiniParameterType::StringChoice: + case EHoudiniParameterType::Color: + case EHoudiniParameterType::Button: + case EHoudiniParameterType::ButtonStrip: + case EHoudiniParameterType::Label: + case EHoudiniParameterType::Toggle: + return false; + + case EHoudiniParameterType::Int: + case EHoudiniParameterType::Float: + case EHoudiniParameterType::String: + case EHoudiniParameterType::Separator: + case EHoudiniParameterType::File: + case EHoudiniParameterType::FileDir: + case EHoudiniParameterType::FileGeo: + case EHoudiniParameterType::FileImage: + default: + return true; + } +} + +void +FHoudiniParameterDetails::AddMetaDataToAllDescendants( + const TSharedRef AncestorWidget, + const FString& UniqueName, + uint32& Index) +{ + // Important: We use GetAllChildren and not GetChildren. + // Widgets might choose to not expose some of their children via GetChildren. + FChildren* const Children = AncestorWidget->GetAllChildren(); + + if (!Children) + { + return; + } + + for (int32 i = 0; i < Children->Num(); ++i) + { + const TSharedRef Child = Children->GetChildAt(i); + AddMetaDataToAllDescendants(Child, UniqueName, Index); + + // Append the type of widget to the unique name, + // this will be helpful in identifying sub-widgets for compound widget when reselecting.. + // example: SNumericEntryBox are a composed SSpinBox and sEditText + // We don't want to be selecting the text if we were using the slider... + FString Name = UniqueName + TEXT("_") + Child->GetTypeAsString(); + Child->AddMetadata(MakeShared(Name, Index++)); + } +} + +void +SHoudiniLabelledParameter::Construct(const FArguments& InArgs) +{ + bEnableContentPadding = InArgs._NameContent.Widget != SNullWidget::NullWidget; + + ContentPadding = TAttribute::CreateLambda( + [this]() + { + return bEnableContentPadding + ? DetailWidgetConstants::RightRowPadding + : FMargin(); + }); + + SHorizontalBox::Construct( + SHorizontalBox::FArguments() + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + InArgs._NameContent.Widget + ] + + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .Padding(ContentPadding) + [ + InArgs._Content.Widget + ]); +} + +void SHoudiniLabelledParameter::SetContent(TSharedRef InContent) +{ + GetSlot(1) + [ + InContent + ]; +} + +void SHoudiniLabelledParameter::SetNameContent(TSharedRef InNameContent) +{ + bEnableContentPadding = InNameContent != SNullWidget::NullWidget; + GetSlot(0) + [ + InNameContent + ]; +} + + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniParameterDetails.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniAssetComponent.h" + +#include "CoreMinimal.h" + +#include "HoudiniParameterTranslator.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Input/SButton.h" + +class UHoudiniAssetComponent; +class UHoudiniParameter; +class UHoudiniParameterFloat; +class UHoudiniParameterInt; +class UHoudiniParameterString; +class UHoudiniParameterColor; +class UHoudiniParameterButton; +class UHoudiniParameterButtonStrip; +class UHoudiniParameterLabel; +class UHoudiniParameterToggle; +class UHoudiniParameterFile; +class UHoudiniParameterChoice; +class UHoudiniParameterFolder; +class UHoudiniParameterFolderList; +class UHoudiniParameterMultiParm; +class UHoudiniParameterRampFloat; +class UHoudiniParameterRampColor; +class UHoudiniParameterOperatorPath; + +class IDetailCategoryBuilder; +class FDetailWidgetRow; +class SHorizontalBox; +class SHoudiniAssetParameterRampCurveEditor; + +enum class EHoudiniRampInterpolationType : int8; + +class SCustomizedButton : public SButton +{ +public: + bool bChosen; + + bool bIsRadioButton; + +public: + virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, + FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; + + // Construct the circles for all radio buttons. Initialize at first use + void ConstructRadioButtonCircles() const; + + void DrawRadioButton(const FGeometry& AllottedGeometry, FSlateWindowElementList& OutDrawElements, int32 LayerId, const bool& bSelected) const; +}; + +class SCustomizedBox : public SHorizontalBox +{ +public: + bool bIsTabFolderListRow; + + bool bIsSeparator; + + TArray DividerLinePositions; + + TArray EndingDividerLinePositions; + + float MarginHeight; + +public: + virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, + FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; + + // Add indentation to current row, computed by tracing the directory hierarchy, + // return the indentation width of this parameter row. + float AddIndentation(const TWeakObjectPtr& InParam, const TMap>& InAllMultiParms, const TMap>& InAllFoldersAndFolderLists); + + void SetHoudiniParameter(const TArray>& InParams); +}; + +/** + * Widget used to wrap parameter controls, optionally with content attached to the name slot. + * + * This is used to support displaying a label next to a parameter widget when horizontally joining. + * We use it because using the name content slot provided by Unreal only allows us to place one name + * widget and one content widget in the details view. + */ +class SHoudiniLabelledParameter : public SHorizontalBox +{ +public: + + SLATE_BEGIN_ARGS(SHoudiniLabelledParameter) + : _Content() + {} + + SLATE_DEFAULT_SLOT(FArguments, Content) + SLATE_NAMED_SLOT(FArguments, NameContent) + + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + void SetContent(TSharedRef InContent); + void SetNameContent(TSharedRef InNameContent); + +private: + // Controls the padding on the content slot. + // We only want padding if there is a non-null widget is attached to the Name slot. + bool bEnableContentPadding; + + // Current padding used on the content slot. + TAttribute ContentPadding; +}; + +//class FHoudiniParameterDetails : public TSharedFromThis, public TNumericUnitTypeInterface, public TNumericUnitTypeInterface +class FHoudiniParameterDetails : public TSharedFromThis +{ + public: + /** + * @param InJoinedParams Array of horizontally joined parameters, where each element is an + * array of linked parameters. Not all widgets support being joined + * horizontally. Use @ref ShouldJoinNext to determine if a widget can + * be joined. + */ + void CreateWidget( + IDetailCategoryBuilder & HouParameterCategory, + const TArray>>& InJoinedParams); + + void CreateJoinableWidget( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows); + + void CreateWidgetInt( + const TSharedRef LabelledParameter, + const TArray>& InParams); + + void CreateWidgetFloat( + const TSharedRef LabelledParameter, + const TArray>& InParams); + + void CreateWidgetString( + IDetailCategoryBuilder& HouParameterCategory, + const TSharedRef LabelledParameter, + const TArray>& InParams); + + void CreateWidgetColor( + const TSharedRef LabelledParameter, + const TArray>& InParams); + + void CreateWidgetButton( + const TSharedRef LabelledParameter, + const TArray>& InParams); + + void CreateWidgetButtonStrip( + const TSharedRef LabelledParameter, + const TArray>& InParams); + + void CreateWidgetLabel( + const TSharedRef LabelledParameter, + const TArray>& InParams); + + void CreateWidgetToggle( + const TSharedRef LabelledParameter, + const TArray>& InParams); + + void CreateWidgetFile( + const TSharedRef LabelledParameter, + const TArray>& InParams); + + void CreateWidgetChoice( + const TSharedRef LabelledParameter, + const TArray>& InParams); + + void CreateWidgetSeparator( + const TSharedRef LabelledParameter, + const TArray>& InParams); + + void CreateWidgetFolderList( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows); + + void CreateWidgetFolder( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows); + + void CreateWidgetMultiParm( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows); + + void CreateWidgetOperatorPath( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows); + + void CreateWidgetFloatRamp( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows); + + void CreateWidgetColorRamp( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + TArray& OutRows); + + void CreateTabEndingRow(IDetailCategoryBuilder & HouParameterCategory); + + + void HandleUnsupportedParmType( + IDetailCategoryBuilder & HouParameterCategory, + const TArray>>& InJoinedParams); + + static FText GetParameterTooltip(const TWeakObjectPtr& InParam); + + static FString GetParameterTypeString(const EHoudiniParameterType& InType, const int32& InTupleSize); + + /** Determines if @ref CreateWidget expects this parameter to be joined. */ + static bool ShouldJoinNext(const UHoudiniParameter& InParam); + + private: + + void Debug(); + + template< class T > + static bool CastParameters( + const TArray& InParams, TArray& OutCastedParams); + + template< class T > + static bool CastParameters( + const TArray>& InParams, TArray>& OutCastedParams); + + // + // Private helper functions for widget creation + // + + + TSharedPtr CreateCustomizedBox( + const TArray>& InParams); + + TSharedPtr CreateNameTextBlock( + const TArray>& InParams); + + // Creates the default name widget, the parameter will then fill the value after + void CreateNameWidget(FDetailWidgetRow* Row, const TArray> &InParams, bool WithLabel); + + // Creates the default name widget, with an extra checkbox for disabling the the parameter update + void CreateNameWidgetWithAutoUpdate(FDetailWidgetRow* Row, const TArray> &InParams, bool WithLabel); + + // Needs to be called for all parameters, not just when we need a row. + // This is because we adjust folder stack here. + // In the future, folder structure should really be seperated from details customization. + FDetailWidgetRow* CreateNestedRow( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams, + bool bDecreaseChildCount = true); + + void CreateFolderHeaderUI(IDetailCategoryBuilder& HouParameterCategory, FDetailWidgetRow* HeaderRow, const TArray>& InParams); // + + void CreateWidgetTab( + IDetailCategoryBuilder& HouParameterCategory, + const TWeakObjectPtr& InParam, + const bool bIsShown, + TArray& OutRows); + void CreateWidgetTabUIElements( + IDetailCategoryBuilder& HouParameterCategory, + const TWeakObjectPtr& InParam, + TArray& OutRows); + + void CreateWidgetMultiParmObjectButtons(TSharedPtr HorizontalBox, const TArray>& InParams); // + + FDetailWidgetRow* CreateWidgetRamp( + IDetailCategoryBuilder& HouParameterCategory, + const TArray>>& InJoinedParams); + + void PruneStack(); + + void RemoveTabDividers(IDetailCategoryBuilder& HouParameterCategory, const TWeakObjectPtr& InParam); + + static bool IsLabelVisible(const TArray>& InParams); + static bool UsesWholeRow(const TArray>& InParams); + + /** + * In Houdini, some widgets try to occupy all available space on a row (i.e. separators) + * while other types (i.e. toggles) use only the minimum space they require. + * @returns true if the widget should occupy all available space, false otherwise. + */ + static bool ShouldWidgetFill(EHoudiniParameterType ParameterType); + + static void AddMetaDataToAllDescendants( + const TSharedRef AncestorWidget, + const FString& UniqueName, + uint32& Index); + + private: + // The parameter directory is flattened with BFS inside of DFS. + // When a folderlist is encountered, it goes 'one step' of DFS, otherwise BFS. + // So that use a Stack structure to reconstruct the tree. + TArray> FolderStack; + + // Float Ramp currently being processed + UHoudiniParameterRampFloat* CurrentRampFloat; + + // Color Ramp currently being processed + UHoudiniParameterRampColor* CurrentRampColor; + + /* Variables for keeping expansion state after adding multiparm instance*/ + TMap> AllMultiParms; + + // Cached the map of parameter id and folders/folder lists + TMap> AllFoldersAndFolderLists; + + /* Variables for keeping expansion state after adding multiparm instance*/ + + TMap MultiParmInstanceIndices; + + // Number of remaining folders for current folder list + int32 CurrentFolderListSize = 0; + + // The folder list currently being processed + UHoudiniParameterFolderList* CurrentFolderList; + + // Cached child folders of current tabs + TArray CurrentTabs; + + TArray DividerLinePositions; + + SCustomizedBox* CurrentTabEndingRow; + +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPreset.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPreset.h" + +#include "HoudiniParameterChoice.h" +#include "HoudiniParameterColor.h" +#include "HoudiniParameterFile.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterRamp.h" +#include "HoudiniParameterString.h" +#include "HoudiniParameterToggle.h" +#include "HoudiniEngineRuntime.h" + + +FHoudiniPresetCurveInputObject::FHoudiniPresetCurveInputObject() + : bClosed(false) + , bReversed(false) + , CurveOrder(0) + , bIsHoudiniSplineVisible(false) + , CurveType(EHoudiniCurveType::Invalid) + , CurveMethod(EHoudiniCurveMethod::Invalid) + , CurveBreakpointParameterization(EHoudiniCurveBreakpointParameterization::Invalid) + , bIsOutputCurve(false) + , bCookOnCurveChanged(false) + , bIsLegacyInputCurve(false) + , bIsInputCurve(false) + , bIsEditableOutputCurve(false) +{ +} + +bool +FHoudiniPresetHelpers::IsSupportedInputType(const EHoudiniInputType InputType) +{ + if (InputType == EHoudiniInputType::Geometry || + InputType == EHoudiniInputType::Curve) + { + return true; + } + + return false; +} + +bool +FHoudiniPresetHelpers::GetParameterValues(const UHoudiniParameterInt* Param, TMap& OutValues, FString& OutValueStr) +{ + if (!IsValid(Param)) + { + return false; + } + + const int32 NumValues = Param->GetNumberOfValues(); + FHoudiniPresetIntValues Values; + Values.Values.SetNum(NumValues); + for (int i = 0; i < NumValues; i++) + { + Param->GetValueAt(i, Values.Values[i]); + } + + OutValues.Add(Param->GetParameterName(), Values); + OutValueStr = Values.ToString(); + return true; +} + + +bool +FHoudiniPresetHelpers::GetParameterValues(const UHoudiniParameterChoice* Param, TMap& OutValues, FString& OutValueStr) +{ + if (!IsValid(Param)) + { + return false; + } + + FHoudiniPresetIntValues Values; + Values.Values.Add(Param->GetIntValueIndex() ); + + OutValues.Add(Param->GetParameterName(), Values); + OutValueStr = Values.ToString(); + return true; +} + + +bool +FHoudiniPresetHelpers::GetParameterValues(const UHoudiniParameterToggle* Param, TMap& OutValues, FString& OutValueStr) +{ + if (!IsValid(Param)) + { + return false; + } + + const int32 NumValues = Param->GetNumValues(); + FHoudiniPresetIntValues Values; + Values.Values.SetNum(NumValues); + for (int i = 0; i < NumValues; i++) + { + Values.Values[i] = static_cast(Param->GetValueAt(i)); + } + + OutValues.Add(Param->GetParameterName(), Values); + OutValueStr = Values.ToString(); + return true; +} + + +bool +FHoudiniPresetHelpers::GetParameterValues(const UHoudiniParameterFloat* Param, TMap& OutValues, FString& OutValueStr) +{ + if (!IsValid(Param)) + { + return false; + } + + const int32 NumValues = Param->GetNumberOfValues(); + FHoudiniPresetFloatValues Values; + Values.Values.SetNum(NumValues); + for (int i = 0; i < NumValues; i++) + { + Param->GetValueAt(i, Values.Values[i]); + } + + OutValues.Add(Param->GetParameterName(), Values); + OutValueStr = Values.ToString(); + return true; +} + + +bool +FHoudiniPresetHelpers::GetParameterValues(const UHoudiniParameterColor* Param, TMap& OutValues, FString& OutValueStr) +{ + if (!IsValid(Param)) + { + return false; + } + + FHoudiniPresetFloatValues Values; + Values.Values.SetNum(4); + + const FLinearColor Color = Param->GetColorValue(); + Values.Values[0] = Color.R; + Values.Values[1] = Color.G; + Values.Values[2] = Color.B; + Values.Values[3] = Color.A; + + OutValues.Add(Param->GetParameterName(), Values); + OutValueStr = Values.ToString(); + return true; +} + + +bool +FHoudiniPresetHelpers::GetParameterValues(const UHoudiniParameterString* Param, TMap& OutValues, FString& OutValueStr) +{ + if (!IsValid(Param)) + { + return false; + } + + const int32 NumValues = Param->GetNumberOfValues(); + FHoudiniPresetStringValues Values; + Values.Values.SetNum(NumValues); + for (int i = 0; i < NumValues; i++) + { + Values.Values[i] = Param->GetValueAt(i); + } + + OutValues.Add(Param->GetParameterName(), Values); + OutValueStr = Values.ToString(); + return true; +} + + +bool +FHoudiniPresetHelpers::GetParameterValues(const UHoudiniParameterFile* Param, TMap& OutValues, FString& OutValueStr) +{ + if (!IsValid(Param)) + { + return false; + } + + const int32 NumValues = Param->GetNumValues(); + FHoudiniPresetStringValues Values; + Values.Values.SetNum(NumValues); + for (int i = 0; i < NumValues; i++) + { + Values.Values[i] = Param->GetValueAt(i); + } + + OutValues.Add(Param->GetParameterName(), Values); + OutValueStr = Values.ToString(); + + return true; +} + + +bool +FHoudiniPresetHelpers::GetParameterValues(const UHoudiniParameterChoice* Param, TMap& OutValues, FString& OutValueStr) +{ + if (!IsValid(Param)) + { + return false; + } + + FHoudiniPresetStringValues Values; + Values.Values.Add(Param->GetStringValue() ); + + OutValues.Add(Param->GetParameterName(), Values); + OutValueStr = Values.ToString(); + return true; +} + + +bool +FHoudiniPresetHelpers::GetParameterValues(const UHoudiniParameterRampFloat* Param, + TMap& OutValues, FString& OutValueStr) +{ + if (!IsValid(Param)) + { + return false; + } + + FHoudiniPresetRampFloatValues Values; + for (const UHoudiniParameterRampFloatPoint* ParmPoint : Param->Points) + { + if (!IsValid(ParmPoint)) + { + continue; + } + FHoudiniPresetRampFloatPoint Point; + Point.Position = ParmPoint->Position; + Point.Value = ParmPoint->Value; + Point.Interpolation = ParmPoint->Interpolation; + Values.Points.Add(Point); + } + + OutValues.Add(Param->GetParameterName(), Values); + OutValueStr = Values.ToString(); + + return true; +} + + +bool +FHoudiniPresetHelpers::GetParameterValues(const UHoudiniParameterRampColor* Parm, + TMap& OutValues, FString& OutValueStr) +{ + if (!IsValid(Parm)) + { + return false; + } + + FHoudiniPresetRampColorValues Values; + for (const UHoudiniParameterRampColorPoint* ParmPoint : Parm->Points) + { + if (!IsValid(ParmPoint)) + { + continue; + } + FHoudiniPresetRampColorPoint Point; + Point.Position = ParmPoint->Position; + Point.Value = ParmPoint->Value; + Point.Interpolation = ParmPoint->Interpolation; + Values.Points.Add(Point); + } + + OutValues.Add(Parm->GetParameterName(), Values); + OutValueStr = Values.ToString(); + + return true; +} + +bool +FHoudiniPresetHelpers::GetParameterValues(const UHoudiniParameterMultiParm* Parm, TMap& OutValues, FString& OutValueStr) +{ + if (!IsValid(Parm)) + return false; + + FHoudiniPresetMultiParmValues Values; + Values.Count = Parm->GetInstanceCount(); + + OutValues.Add(Parm->GetParameterName(), Values); + OutValueStr = Values.ToString(); + + return true; +} + +void +FHoudiniPresetHelpers::GetGenericInput(UHoudiniInput* Input, + bool bIsParameterInput, + const FString& ParameterName, + TArray& OutValues) +{ + FHoudiniPresetInputValue Value; + Value.ParameterName = ParameterName; + Value.bIsParameterInput = bIsParameterInput; + Value.InputIndex = Input->GetInputIndex(); + + switch( Input->GetInputType() ) + { + case EHoudiniInputType::Geometry: + UpdateFromGeometryInput(Value, Input); + break; + case EHoudiniInputType::Curve: + UpdateFromCurveInput(Value, Input); + break; + default: + break; + } + + OutValues.Add(Value); +} + +void +FHoudiniPresetHelpers::UpdateGenericInputSettings(FHoudiniPresetInputValue& Value, const UHoudiniInput* Input) +{ + Value.InputType = Input->GetInputType(); + + // Copy export settings + Value.bKeepWorldTransform = Input->GetKeepWorldTransform(); + Value.bPackGeometryBeforeMerging = Input->GetPackBeforeMerge(); + Value.bExportInputAsReference = Input->GetImportAsReference(); + Value.bExportLODs = Input->GetExportLODs(); + Value.bExportSockets = Input->GetExportSockets(); + Value.bExportColliders = Input->GetExportColliders(); + Value.bExportMaterialParameters = Input->GetExportMaterialParameters(); + Value.bMergeSplineMeshComponents = Input->IsMergeSplineMeshComponentsEnabled(); + Value.bPreferNaniteFallbackMesh = Input->GetPreferNaniteFallbackMesh(); +} + +void +FHoudiniPresetHelpers::UpdateFromGeometryInput(FHoudiniPresetInputValue& Value, const UHoudiniInput* Input) +{ + UpdateGenericInputSettings(Value, Input); + + const TArray>* InputObjects = Input->GetHoudiniInputObjectArray(EHoudiniInputType::Geometry); + if (!InputObjects) + { + return; + } + + const int32 NumObjects = InputObjects->Num(); + for (int i = 0; i < NumObjects; i++) + { + const UHoudiniInputObject* InputObj = (*InputObjects)[i]; + FHoudiniPresetGeometryInputObject PresetObject; + + if (IsValid(InputObj)) + { + PresetObject.InputObject = InputObj->GetObject(); + PresetObject.Transform = InputObj->GetTransform(); + } + + Value.GeometryInputObjects.Add(PresetObject); + } +} + +void +FHoudiniPresetHelpers::UpdateFromCurveInput(FHoudiniPresetInputValue& Value, const UHoudiniInput* Input) +{ + UpdateGenericInputSettings(Value, Input); + + const TArray>* InputObjects = Input->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!InputObjects) + { + return; + } + + + const int32 NumObjects = InputObjects->Num(); + Value.CurveInputObjects.SetNum(NumObjects); + + for (int i = 0; i < NumObjects; i++) + { + const UHoudiniInputHoudiniSplineComponent* InputObj = Cast((*InputObjects)[i]); + FHoudiniPresetCurveInputObject PresetObject; + if (IsValid(InputObj)) + { + PresetObject.Transform = InputObj->GetTransform(); + + const UHoudiniSplineComponent* CurveComponent = InputObj->GetCurveComponent(); + if (IsValid(CurveComponent)) + { + PresetObject.CurvePoints = CurveComponent->CurvePoints; + PresetObject.bClosed = CurveComponent->IsClosedCurve(); + PresetObject.bReversed = CurveComponent->IsReversed(); + PresetObject.CurveOrder = CurveComponent->GetCurveOrder(); + PresetObject.bIsHoudiniSplineVisible = CurveComponent->IsHoudiniSplineVisible(); + PresetObject.CurveType = CurveComponent->GetCurveType(); + PresetObject.CurveMethod = CurveComponent->GetCurveMethod(); + PresetObject.CurveBreakpointParameterization = CurveComponent->GetCurveBreakpointParameterization(); + PresetObject.bIsOutputCurve = CurveComponent->bIsOutputCurve; + PresetObject.bCookOnCurveChanged = CurveComponent->bCookOnCurveChanged; + PresetObject.bIsLegacyInputCurve = CurveComponent->IsLegacyInputCurve(); + PresetObject.bIsInputCurve = CurveComponent->IsInputCurve(); + PresetObject.bIsEditableOutputCurve = CurveComponent->IsEditableOutputCurve(); + } + } + + Value.CurveInputObjects[i] = PresetObject; + } +} + +void +FHoudiniPresetHelpers::ApplyPresetParameterValues(const FHoudiniPresetIntValues& Values, UHoudiniParameterInt* Parm) +{ + if (!IsValid(Parm)) + { + return; + } + const int32 NumValues = FMath::Min(Parm->GetNumberOfValues(), Values.Values.Num()); + for (int i = 0; i < NumValues; i++) + { + Parm->SetValueAt(Values.Values[i], i); + Parm->MarkChanged(true); + } +} + + +void +FHoudiniPresetHelpers::ApplyPresetParameterValues(const FHoudiniPresetIntValues& Values, UHoudiniParameterChoice* Parm) +{ + if (!IsValid(Parm)) + { + return; + } + + if (Values.Values.Num() > 0) + { + Parm->SetIntValue( Values.Values[0] ); + Parm->MarkChanged(true); + } +} + + +void +FHoudiniPresetHelpers::ApplyPresetParameterValues(const FHoudiniPresetIntValues& Values, UHoudiniParameterToggle* Parm) +{ + if (!IsValid(Parm)) + { + return; + } + if (Values.Values.Num() > 0) + { + Parm->SetValueAt( static_cast(Values.Values[0]), 0); + Parm->MarkChanged(true); + } +} + + +void +FHoudiniPresetHelpers::ApplyPresetParameterValues(const FHoudiniPresetFloatValues& Values, UHoudiniParameterFloat* Parm) +{ + if (!IsValid(Parm)) + { + return; + } + const int32 NumValues = FMath::Min(Parm->GetNumberOfValues(), Values.Values.Num()); + for (int i = 0; i < NumValues; i++) + { + Parm->SetValueAt(Values.Values[i], i); + Parm->MarkChanged(true); + } +} + + +void +FHoudiniPresetHelpers::ApplyPresetParameterValues(const FHoudiniPresetFloatValues& Values, UHoudiniParameterColor* Parm) +{ + if (!IsValid(Parm)) + { + return; + } + + FLinearColor Color(1.f, 1.f, 1.f, 1.f); + const int32 NumValues = FMath::Min(Values.Values.Num(), 4); + for (int i = 0; i < NumValues; i++) + { + switch(i) + { + case 0: + Color.R = Values.Values[i]; + break; + case 1: + Color.G = Values.Values[i]; + break; + case 2: + Color.B = Values.Values[i]; + break; + case 3: + Color.A = Values.Values[i]; + break; + default: ; + } + } + + Parm->SetColorValue(Color); + Parm->MarkChanged(true); +} + + +void +FHoudiniPresetHelpers::ApplyPresetParameterValues(const FHoudiniPresetStringValues& Values, UHoudiniParameterString* Parm) +{ + if (!IsValid(Parm)) + { + return; + } + const int32 NumValues = FMath::Min(Parm->GetNumberOfValues(), Values.Values.Num()); + for (int i = 0; i < NumValues; i++) + { + Parm->SetValueAt(Values.Values[i], i); + Parm->MarkChanged(true); + } +} + + +void +FHoudiniPresetHelpers::ApplyPresetParameterValues(const FHoudiniPresetStringValues& Values, UHoudiniParameterFile* Parm) +{ + if (!IsValid(Parm)) + { + return; + } + const int32 NumValues = FMath::Min(Parm->GetNumValues(), Values.Values.Num()); + for (int i = 0; i < NumValues; i++) + { + Parm->SetValueAt(Values.Values[i], i); + Parm->MarkChanged(true); + } +} + + +void +FHoudiniPresetHelpers::ApplyPresetParameterValues(const FHoudiniPresetStringValues& Values, UHoudiniParameterChoice* Parm) +{ + if (!IsValid(Parm)) + { + return; + } + + if (Values.Values.Num() > 0) + { + Parm->SetStringValue( Values.Values[0] ); + Parm->MarkChanged(true); + } +} + +void +FHoudiniPresetHelpers::ApplyPresetParameterValues(const FHoudiniPresetRampFloatValues& Values, + UHoudiniParameterRampFloat* Param) +{ + if (!IsValid(Param)) + { + return; + } + + // When applying a preset, we ignore the auto-update setting on the ramp parameter since we're likely going to be + // updating a bunch of other parameters too. The HDA / Houdini Engine's cook settings will determine whether it's + // going to cook our changes or not. + + const int32 NumPoints = Values.Points.Num(); + + // Copy our preset points into the cached points array of the Ramp parameter. + Param->SetNumCachedPoints(NumPoints); + for (int i = 0; i < NumPoints; i++) + { + const FHoudiniPresetRampFloatPoint& PresetPoint = Values.Points[i]; + Param->SetCachedPointAtIndex(i, PresetPoint.Position, PresetPoint.Value, PresetPoint.Interpolation ); + } + + Param->MarkChanged(true); + + // Clear the events queue, then force a sync to ensure that we have the correct events in the queue to match our + // cached points state. + // The reason we do this is because the HDA cooking might be paused, or the HDA is cooking already, so the user may + // start performing additional ramp manipulations which will start conflicting with our cached state. + Param->ModificationEvents.Empty(); + Param->SyncCachedPoints(); + // We have already synced the cached points, so we don't need to process them on the next PreCook event. + Param->bCaching = false; +} + +void +FHoudiniPresetHelpers::ApplyPresetParameterValues(const FHoudiniPresetMultiParmValues& Values, UHoudiniParameterMultiParm* Param) +{ + if (!IsValid(Param)) + { + return; + } + Param->SetNumElements(Values.Count); + + Param->MarkChanged(true); +} + + +void +FHoudiniPresetHelpers::ApplyPresetParameterValues(const FHoudiniPresetRampColorValues& Values, UHoudiniParameterRampColor* Param) +{ + if (!IsValid(Param)) + { + return; + } + + // When applying a preset, we ignore the auto-update setting on the ramp parameter since we're likely going to be + // updating a bunch of other parameters too. The HDA / Houdini Engine's cook settings will determine whether it's + // going to cook our changes or not. + + const int32 NumPoints = Values.Points.Num(); + + // Copy our preset points into the cached points array of the Ramp parameter. + Param->SetNumCachedPoints(NumPoints); + for (int i = 0; i < NumPoints; i++) + { + const FHoudiniPresetRampColorPoint& PresetPoint = Values.Points[i]; + Param->SetCachedPointAtIndex(i, PresetPoint.Position, PresetPoint.Value, PresetPoint.Interpolation ); + } + + Param->MarkChanged(true); + + // Clear the events queue, then force a sync to ensure that we have the correct events in the queue to match our + // cached points state. + // The reason we do this is because the HDA cooking might be paused, or the HDA is cooking already, so the user may + // start performing additional ramp manipulations which will start conflicting with our cached state. + Param->ModificationEvents.Empty(); + Param->SyncCachedPoints(); + // We have already synced the cached points, so we don't need to process them on the next PreCook event. + Param->bCaching = false; +} + + +void +FHoudiniPresetHelpers::ApplyPresetParameterValues(const FHoudiniPresetInputValue& PresetInput, UHoudiniInput* Input) +{ + if (!IsValid(Input)) + return; + + Input->Modify(); + + // Apply Export settings + Input->SetKeepWorldTransform( PresetInput.bKeepWorldTransform ); + Input->SetPackBeforeMerge( PresetInput.bPackGeometryBeforeMerging ); + Input->SetImportAsReference( PresetInput.bExportInputAsReference ); + Input->SetExportLODs( PresetInput.bExportLODs ); + Input->SetExportSockets( PresetInput.bExportSockets ); + Input->SetExportColliders( PresetInput.bExportColliders ); + Input->SetExportMaterialParameters( PresetInput.bExportMaterialParameters ); + Input->SetMergeSplineMeshComponents( PresetInput.bMergeSplineMeshComponents ); + Input->SetPreferNaniteFallbackMesh( PresetInput.bPreferNaniteFallbackMesh ); + + // Overwrite this input with the type and input objects that were captured by the preset. + bool bBlueprintStructureModified = false; + Input->SetInputType( PresetInput.InputType, bBlueprintStructureModified ); + + switch(PresetInput.InputType) + { + case EHoudiniInputType::Geometry: + ApplyPresetGeometryInput(PresetInput, Input); + break; + case EHoudiniInputType::Curve: + ApplyPresetCurveInput(PresetInput, Input); + break; + default: + break; + } + + Input->MarkChanged(true); +} + +void +FHoudiniPresetHelpers::ApplyPresetGeometryInput(const FHoudiniPresetInputValue& PresetInput, UHoudiniInput* Input) +{ + // TODO: Correctly resize geometry array. Delete excess input objects. + + Input->SetInputObjectsNumber( PresetInput.InputType, PresetInput.GeometryInputObjects.Num() ); + + // Apply Geometry Input Objects + for (int32 i = 0; i < PresetInput.GeometryInputObjects.Num(); i++) + { + const FHoudiniPresetGeometryInputObject& PresetObject = PresetInput.GeometryInputObjects[i]; + UObject* PresetInputObject = PresetObject.InputObject.LoadSynchronous(); + if (IsValid(PresetInputObject)) + { + // Set the valid input object at this index. + Input->SetInputObjectAt(PresetInput.InputType, i, PresetInputObject); + // Apply the input object transform + UHoudiniInputObject* InputObject = Input->GetHoudiniInputObjectAt(PresetInput.InputType, i); + + if (InputObject) + { + if (!InputObject->GetTransform().Equals(PresetObject.Transform)) + { + InputObject->SetTransform(PresetObject.Transform); + InputObject->MarkTransformChanged(true); + } + } + } + else + { + // We don't have a preset input at this index. Ensure this input object index is empty. + const UHoudiniInputObject* InputObject = Input->GetHoudiniInputObjectAt(PresetInput.InputType, i); + if (IsValid(InputObject)) + { + // Delete the existing input object. + Input->DeleteInputObjectAt(PresetInput.InputType, i, false); + } + } + } +} + +void +FHoudiniPresetHelpers::ApplyPresetCurveInput(const FHoudiniPresetInputValue& PresetInput, UHoudiniInput* Input) +{ + TArray>* InputObjects = Input->GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + if (!InputObjects) + { + return; + } + + Input->Modify(); + + const int32 NumCurves = PresetInput.CurveInputObjects.Num(); + if (InputObjects->Num() > NumCurves) + { + // Delete excess input objects + for (int i = InputObjects->Num()-1; i < NumCurves; i++ ) + { + Input->DeleteInputObjectAt(i, false); + } + Input->MarkChanged(true); + } + + Input->SetInputObjectsNumber(EHoudiniInputType::Curve, NumCurves); + + // Apply Curve Input Objects + for (int32 i = 0; i < PresetInput.CurveInputObjects.Num(); i++) + { + const FHoudiniPresetCurveInputObject& PresetObject = PresetInput.CurveInputObjects[i]; + + UHoudiniInputHoudiniSplineComponent* InputObj = nullptr; + + if (InputObjects->IsValidIndex(i)) + { + InputObj = Cast((*InputObjects)[i]); + } + + if (!IsValid(InputObj)) + { + bool bBlueprintStructModified = false; + InputObj = Input->GetOrCreateCurveInputObjectAt(i ,true, bBlueprintStructModified); + } + + if (!InputObj || !IsValid(InputObj)) + { + continue; + } + + InputObj->Modify(); + + InputObj->SetTransform(PresetObject.Transform); + InputObj->MarkTransformChanged(true); + + UHoudiniSplineComponent* SplineComponent = InputObj->GetCurveComponent(); + + if (!IsValid(SplineComponent)) + { + continue; + } + + SplineComponent->Modify(); + + SplineComponent->CurvePoints = PresetObject.CurvePoints; + SplineComponent->ResetDisplayPoints(); + SplineComponent->SetClosedCurve( PresetObject.bClosed ); + SplineComponent->SetReversed( PresetObject.bReversed ); + SplineComponent->SetCurveOrder( PresetObject.CurveOrder ); + SplineComponent->SetHoudiniSplineVisible( PresetObject.bIsHoudiniSplineVisible ); + SplineComponent->SetCurveType( PresetObject.GetValidCurveType() ); + SplineComponent->SetCurveMethod( PresetObject.GetValidCurveMethod() ); + SplineComponent->SetCurveBreakpointParameterization( PresetObject.GetValidCurveBreakpointParameterization() ); + SplineComponent->bIsOutputCurve = PresetObject.bIsOutputCurve; + SplineComponent->bCookOnCurveChanged = PresetObject.bCookOnCurveChanged; + SplineComponent->bIsLegacyInputCurve = PresetObject.bIsLegacyInputCurve; + SplineComponent->SetIsInputCurve( PresetObject.bIsInputCurve ); + SplineComponent->SetIsEditableOutputCurve( PresetObject.bIsEditableOutputCurve ); + + InputObj->MarkChanged(true); + SplineComponent->MarkChanged(true); + + } + +} + + +UHoudiniPreset::UHoudiniPreset() + : SourceHoudiniAsset(nullptr) + , bRevertHDAParameters(false) + , bHidePreset(false) + , bApplyOnlyToSource(false) + , bCanInstantiate(false) +{ + +} + + +#if WITH_EDITOR +void +UHoudiniPreset::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + UObject::PostEditChangeProperty(PropertyChangedEvent); + if (FModuleManager::Get().IsModuleLoaded("HoudiniEngineRuntime")) + { + const FHoudiniEngineRuntime& HoudiniEngineRuntime = FModuleManager::GetModuleChecked("HoudiniEngineRuntime"); + HoudiniEngineRuntime.BroadcastToolOrPackageChanged(); + } +} +#endif + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPreset.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniEngineRuntimeCommon.h" +#include "HoudiniInput.h" +#include "HoudiniRuntimeSettings.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniParameterMultiParm.h" +#include "HoudiniToolData.h" + +#include "HoudiniPreset.generated.h" + +/** + * This is a Houdini Tools package descriptor inside of UE, typically created + * after a HoudiniToolsPackage has been imported into the UE project. + */ + +class UHoudiniInput; +class UHoudiniAsset; +class UHoudiniParameterChoice; +class UHoudiniParameterColor; +class UHoudiniParameterToggle; +class UHoudiniParameterInt; +class UHoudiniParameterFloat; +class UHoudiniParameterString; +class UHoudiniParameterFile; +class UHoudiniParameterRampFloat; +class UHoudiniParameterRampColor; +class UHoudiniParameterOperatorPath; + + +UENUM() +enum class EHoudiniPresetValueType +{ + Invalid, + Float, + Int, + String +}; + + +USTRUCT() +struct FHoudiniPresetBase +{ + GENERATED_BODY() + virtual ~FHoudiniPresetBase() {} + + virtual FString ToString() { return FString(); } + + virtual EHoudiniPresetValueType GetValueType() { return EHoudiniPresetValueType::Invalid; } +}; + + +USTRUCT(BlueprintType) +struct FHoudiniPresetFloatValues : public FHoudiniPresetBase +{ + GENERATED_BODY() + + virtual EHoudiniPresetValueType GetValueType() override { return EHoudiniPresetValueType::Float; } + + virtual FString ToString() override { return FString::JoinBy(Values, TEXT(", "), [](const float& Value) { return FString::SanitizeFloat(Value); }); } + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TArray Values; +}; + + +USTRUCT(BlueprintType) +struct FHoudiniPresetIntValues : public FHoudiniPresetBase +{ + GENERATED_BODY() + + virtual EHoudiniPresetValueType GetValueType() override { return EHoudiniPresetValueType::Int; } + + virtual FString ToString() override { return FString::JoinBy(Values, TEXT(", "), [](const float& Value) { return FString::FromInt(Value); }); } + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TArray Values; +}; + + +USTRUCT(BlueprintType) +struct FHoudiniPresetStringValues : public FHoudiniPresetBase +{ + GENERATED_BODY() + + virtual EHoudiniPresetValueType GetValueType() override { return EHoudiniPresetValueType::String; } + + virtual FString ToString() override { return FString::Join(Values, TEXT(", ")); } + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TArray Values; +}; + +USTRUCT(BlueprintType) +struct FHoudiniPresetRampFloatPoint +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + float Position = 0.0f; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + float Value = 0.0f; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + EHoudiniRampInterpolationType Interpolation = EHoudiniRampInterpolationType::LINEAR; +}; + +USTRUCT(BlueprintType) +struct FHoudiniPresetRampColorPoint +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + float Position = 0.0f; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + FLinearColor Value = FLinearColor::White; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + EHoudiniRampInterpolationType Interpolation = EHoudiniRampInterpolationType::LINEAR; +}; + +USTRUCT(BlueprintType) +struct FHoudiniPresetRampFloatValues : public FHoudiniPresetBase +{ + GENERATED_BODY() + + virtual EHoudiniPresetValueType GetValueType() override { return EHoudiniPresetValueType::String; } + + virtual FString ToString() override { return FString::Format(TEXT("%d float points."), { Points.Num() }); } + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TArray Points; +}; + +USTRUCT(BlueprintType) +struct FHoudiniPresetRampColorValues : public FHoudiniPresetBase +{ + GENERATED_BODY() + + virtual EHoudiniPresetValueType GetValueType() override { return EHoudiniPresetValueType::String; } + + virtual FString ToString() override { return FString::Format(TEXT("{0} color points."), { Points.Num() }); } + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TArray Points; +}; + +USTRUCT(BlueprintType) +struct FHoudiniPresetMultiParmValues : public FHoudiniPresetBase +{ + GENERATED_BODY() + + virtual EHoudiniPresetValueType GetValueType() override { return EHoudiniPresetValueType::Int; } + + virtual FString ToString() override { return FString::Format(TEXT("Number of Elements: {0}"), { Count }); } + + UPROPERTY(EditAnywhere, Category = "Houdini Preset") + int Count = 0;; +}; + +USTRUCT(BlueprintType) +struct FHoudiniPresetGeometryInputObject : public FHoudiniPresetBase +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TSoftObjectPtr InputObject; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + FTransform Transform = FTransform::Identity; +}; + +USTRUCT(BlueprintType) +struct FHoudiniPresetCurveInputObject : public FHoudiniPresetBase +{ + GENERATED_BODY() + + FHoudiniPresetCurveInputObject(); + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + FTransform Transform = FTransform::Identity; + + // Curve Spline Component + + EHoudiniCurveType GetValidCurveType() const + { + if (CurveType == EHoudiniCurveType::Invalid) + { + return EHoudiniCurveType::Polygon; + } + return CurveType; + } + + EHoudiniCurveMethod GetValidCurveMethod() const + { + if (CurveMethod == EHoudiniCurveMethod::Invalid) + { + return EHoudiniCurveMethod::CVs; + } + return CurveMethod; + } + + EHoudiniCurveBreakpointParameterization GetValidCurveBreakpointParameterization() const + { + if (CurveBreakpointParameterization == EHoudiniCurveBreakpointParameterization::Invalid) + { + return EHoudiniCurveBreakpointParameterization::Uniform; + } + return CurveBreakpointParameterization; + } + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TArray CurvePoints; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bClosed = false; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bReversed = false; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + int32 CurveOrder = 2; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bIsHoudiniSplineVisible = true; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + EHoudiniCurveType CurveType = EHoudiniCurveType::Polygon; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + EHoudiniCurveMethod CurveMethod = EHoudiniCurveMethod::CVs; + + // Only used for new HAPI curve / breakpoints + UPROPERTY(EditAnywhere, Category="Houdini Preset") + EHoudiniCurveBreakpointParameterization CurveBreakpointParameterization = EHoudiniCurveBreakpointParameterization::Uniform; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bIsOutputCurve = false; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bCookOnCurveChanged = true; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bIsLegacyInputCurve = false; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bIsInputCurve = false; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bIsEditableOutputCurve = false; +}; + + +USTRUCT(BlueprintType) +struct FHoudiniPresetInputValue : public FHoudiniPresetBase +{ + GENERATED_BODY() + + virtual EHoudiniPresetValueType GetValueType() override { return EHoudiniPresetValueType::String; } + + virtual FString ToString() override { return FString(); } + + // Export Options + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bKeepWorldTransform = true; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bPackGeometryBeforeMerging = false; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bExportInputAsReference = false; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bExportLODs = false; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bExportSockets = false; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bExportColliders = false; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bExportMaterialParameters = false; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bMergeSplineMeshComponents = false; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bPreferNaniteFallbackMesh = false; + + // Input properties + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + EHoudiniInputType InputType = EHoudiniInputType::Geometry; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bIsParameterInput = false; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + FString ParameterName; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + int32 InputIndex = -1; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TArray GeometryInputObjects; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TArray CurveInputObjects; +}; + + +struct FHoudiniPresetHelpers +{ + static bool IsSupportedInputType(const EHoudiniInputType InputType); + + // GetParameterValues to return arrays of the type and a string version of the values. + static bool GetParameterValues(const UHoudiniParameterInt* Param, TMap& OutValues, FString& OutValueStr); + static bool GetParameterValues(const UHoudiniParameterChoice* Param, TMap& OutValues, FString& OutValueStr); + static bool GetParameterValues(const UHoudiniParameterToggle* Param, TMap& OutValues, FString& OutValueStr); + static bool GetParameterValues(const UHoudiniParameterFloat* Param, TMap& OutValues, FString& OutValueStr); + static bool GetParameterValues(const UHoudiniParameterColor* Param, TMap& OutValues, FString& OutValueStr); + static bool GetParameterValues(const UHoudiniParameterString* Param, TMap& OutValues, FString& OutValueStr); + static bool GetParameterValues(const UHoudiniParameterFile* Param, TMap& OutValues, FString& OutValueStr); + static bool GetParameterValues(const UHoudiniParameterChoice* Param, TMap& OutValues, FString& OutValueStr); + static bool GetParameterValues(const UHoudiniParameterMultiParm* Param, TMap& OutValues, FString& OutValueStr); + + + // Ingest ramp parameters + static bool GetParameterValues(const UHoudiniParameterRampFloat* Param, TMap& OutValues, FString& OutValueStr); + static bool GetParameterValues(const UHoudiniParameterRampColor* Param, TMap& OutValues, FString& OutValueStr); + + // Ingest input parameters + static void GetGenericInput(UHoudiniInput* Input, bool bIsParameterInput, const FString& ParameterName, TArray& OutValues); + static void UpdateGenericInputSettings(FHoudiniPresetInputValue& Value, const UHoudiniInput* Input); + static void UpdateFromGeometryInput(FHoudiniPresetInputValue& Value, const UHoudiniInput* Input); + static void UpdateFromCurveInput(FHoudiniPresetInputValue& Value, const UHoudiniInput* Input); + + + // Preset Helpers + static void ApplyPresetParameterValues(const FHoudiniPresetIntValues& Values, UHoudiniParameterInt* Param); + static void ApplyPresetParameterValues(const FHoudiniPresetIntValues& Values, UHoudiniParameterChoice* Param); + static void ApplyPresetParameterValues(const FHoudiniPresetIntValues& Values, UHoudiniParameterToggle* Param); + static void ApplyPresetParameterValues(const FHoudiniPresetFloatValues& Values, UHoudiniParameterFloat* Param); + static void ApplyPresetParameterValues(const FHoudiniPresetFloatValues& Values, UHoudiniParameterColor* Param); + static void ApplyPresetParameterValues(const FHoudiniPresetStringValues& Values, UHoudiniParameterString* Param); + static void ApplyPresetParameterValues(const FHoudiniPresetStringValues& Values, UHoudiniParameterFile* Param); + static void ApplyPresetParameterValues(const FHoudiniPresetStringValues& Values, UHoudiniParameterChoice* Param); + static void ApplyPresetParameterValues(const FHoudiniPresetRampFloatValues& Values, UHoudiniParameterRampFloat* Param); + static void ApplyPresetParameterValues(const FHoudiniPresetRampColorValues& Values, UHoudiniParameterRampColor* Param); + static void ApplyPresetParameterValues(const FHoudiniPresetMultiParmValues& Values, UHoudiniParameterMultiParm* Param); + static void ApplyPresetParameterValues(const FHoudiniPresetInputValue& PresetInput, UHoudiniInput* Input); + +protected: + static void ApplyPresetGeometryInput(const FHoudiniPresetInputValue& PresetInput, UHoudiniInput* Input); + static void ApplyPresetCurveInput(const FHoudiniPresetInputValue& PresetInput, UHoudiniInput* Input); +}; + + +UCLASS(BlueprintType, hidecategories=(Object)) +class HOUDINIENGINEEDITOR_API UHoudiniPreset : public UObject +{ + GENERATED_BODY() +public: + + UHoudiniPreset(); + + #if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + #endif + + // The label for this preset + UPROPERTY(EditAnywhere, Category="Houdini Preset") + FString Name; + + // A description for this preset + UPROPERTY(EditAnywhere, Category="Houdini Preset", meta=(MultiLine="true")) + FString Description; + + // The HoudiniAsset linked to this preset. + // Should this be a soft object pointer instead? + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TObjectPtr SourceHoudiniAsset; + + // Whether the revert all parameters on the HDA to their default values before applying this preset + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bRevertHDAParameters; + + // Whether to treat this preset as hidden (hide from preset menus and will be not be visible in HoudiniTools Panel). + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bHidePreset; + + // Whether this preset can be applied to any HDA, or only the SourceHoudiniAsset. + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bApplyOnlyToSource; + + // Whether this preset be instantiated (using the SourceHoudiniAsset). + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bCanInstantiate; + + // Cook and Bake Folders + // We add toggles specifically for temp/bake folders since we might want to + // control them separately respective options groups. + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bApplyTemporaryCookFolder; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + FString TemporaryCookFolder; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bApplyBakeFolder; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + FString BakeFolder; + + // Bake Options + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bApplyBakeOptions; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + EHoudiniEngineBakeOption HoudiniEngineBakeOption; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bRemoveOutputAfterBake; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bRecenterBakedActors; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bAutoBake; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bReplacePreviousBake; + + // Asset Options + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bApplyAssetOptions; + + // Asset Options - Cook Triggers + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bCookOnParameterChange; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bCookOnTransformChange; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bCookOnAssetInputCook; + + // Asset Options - Outputs + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bDoNotGenerateOutputs; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bUseOutputNodes; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bOutputTemplateGeos; + + // Asset Options - Misc + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bUploadTransformsToHoudiniEngine; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bLandscapeUseTempLayers; + + + // Parameters + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TMap FloatParameters; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TMap IntParameters; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TMap StringParameters; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TMap RampFloatParameters; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TMap RampColorParameters; + + UPROPERTY(EditAnywhere, Category = "Houdini Preset") + TMap MultiParmParameters; + + // Inputs + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + TArray InputParameters; + + // Static Mesh Generation Settings + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bApplyStaticMeshGenSettings; + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + FHoudiniStaticMeshGenerationProperties StaticMeshGenerationProperties; + + UPROPERTY(Category = "HoudiniMeshGeneration", EditAnywhere) + FMeshBuildSettings StaticMeshBuildSettings; + + // Proxy Mesh Gen Settings + + UPROPERTY(EditAnywhere, Category="Houdini Preset") + bool bApplyProxyMeshGenSettings; + + UPROPERTY(Category = "HoudiniProxyMeshGeneration", EditAnywhere) + bool bOverrideGlobalProxyStaticMeshSettings; + + UPROPERTY(Category = "HoudiniProxyMeshGeneration", EditAnywhere, meta = (DisplayName="Enable Proxy Static Mesh")) + bool bEnableProxyStaticMeshOverride; + + UPROPERTY(Category = "HoudiniProxyMeshGeneration", EditAnywhere, meta = (DisplayName="Refine Proxy Static Meshes After a Timeout")) + bool bEnableProxyStaticMeshRefinementByTimerOverride; + + UPROPERTY(Category = "HoudiniProxyMeshGeneration", EditAnywhere, meta = (DisplayName="Proxy Mesh Auto Refine Timeout Seconds")) + float ProxyMeshAutoRefineTimeoutSecondsOverride; + + UPROPERTY(Category = "HoudiniProxyMeshGeneration", EditAnywhere, meta = (DisplayName="Refine Proxy Static Meshes When Saving a Map")) + bool bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride; + + UPROPERTY(Category = "HoudiniProxyMeshGeneration", EditAnywhere, meta = (DisplayName="Refine Proxy Static Meshes On PIE")) + bool bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride; + + // Raw image data of the icon to be displayed + UPROPERTY() + FHImageData IconImageData; + + // Callback for when this preset has been instantiated. + // Used for testing. + TArray< TFunction > PostInstantiationCallbacks; + + +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPresetActorFactory.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPresetActorFactory.h" + +#include "HoudiniEngineEditorPrivatePCH.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniPreset.h" +#include "HoudiniToolsEditor.h" + + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +UHoudiniPresetActorFactory::UHoudiniPresetActorFactory(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) +{ + DisplayName = LOCTEXT("HoudiniAssetDisplayName", "Houdini Engine Asset"); + NewActorClass = AHoudiniAssetActor::StaticClass(); +} + +bool +UHoudiniPresetActorFactory::CanCreateActorFrom(const FAssetData & AssetData, FText & OutErrorMsg) +{ + UHoudiniPreset* Preset = Cast(AssetData.GetAsset() ); + if (!AssetData.IsValid() || !AssetData.GetClass()->IsChildOf(UHoudiniPreset::StaticClass()) || !IsValid(Preset)) + { + OutErrorMsg = NSLOCTEXT("CanCreateActor", "NoHoudiniPreset", "A valid Houdini Engine Preset asset must be specified."); + return false; + } + + + if (!IsValid(Preset->SourceHoudiniAsset)) + { + OutErrorMsg = NSLOCTEXT("CanCreateActor", "NoHoudiniPresetSourceAsset", "The preset requires a source asset."); + return false; + } + + return true; +} + +UObject * +UHoudiniPresetActorFactory::GetAssetFromActorInstance(AActor * Instance) +{ + check(Instance->IsA(NewActorClass)); + AHoudiniAssetActor * HoudiniAssetActor = CastChecked(Instance); + + check(HoudiniAssetActor->GetHoudiniAssetComponent()); + return HoudiniAssetActor->GetHoudiniAssetComponent()->HoudiniAsset_DEPRECATED; +} + +void +UHoudiniPresetActorFactory::PostSpawnActor(UObject* InObject, AActor* NewActor) +{ + HOUDINI_LOG_MESSAGE(TEXT("PostSpawnActor (Preset) %s, supplied Preset = 0x%0.8p"), *NewActor->GetActorNameOrLabel(), InObject); + UHoudiniPreset* Preset = Cast(InObject); + if (!IsValid(Preset)) + { + return; + } + + UHoudiniAsset* HoudiniAsset = Preset->SourceHoudiniAsset; + if (!HoudiniAsset) + return; + + AHoudiniAssetActor* HoudiniAssetActor = CastChecked(NewActor); + UHoudiniCookable* HoudiniCookable = HoudiniAssetActor->GetHoudiniCookable(); + UHoudiniAssetComponent* HoudiniAssetComponent = HoudiniAssetActor->GetHoudiniAssetComponent(); + + check(HoudiniAssetComponent); + + FHoudiniEngineUtils::AddHoudiniLogoToComponent(HoudiniAssetComponent); + + if (HoudiniAssetActor->IsUsedForPreview()) + return; + + if (HoudiniCookable) + { + HoudiniCookable->SetHoudiniAsset(HoudiniAsset); + FHoudiniEngineRuntime::Get().RegisterHoudiniCookable(HoudiniCookable); + + // Apply the preset once the Cookable has reached the PreCookCallback + // (which is when both the inputs and parameters have been initialized). + HoudiniCookable->QueuePreCookCallback([Preset](UHoudiniCookable* InHC) + { + if (IsValid(InHC) && IsValid(Preset)) + { + FHoudiniToolsEditor::ApplyPresetToHoudiniCookable(Preset, InHC); + } + }); + } +} + +void +UHoudiniPresetActorFactory::PostCreateBlueprint(UObject* InObject, AActor * CDO) +{ + HOUDINI_LOG_MESSAGE(TEXT("PostCreateBlueprint, supplied Asset = 0x%0.8p"), InObject); + + UHoudiniPreset* Preset = Cast(InObject); + if (!IsValid(Preset)) + { + return; + } + + UHoudiniAsset* HoudiniAsset = Preset->SourceHoudiniAsset; + if (HoudiniAsset) + { + AHoudiniAssetActor* HoudiniAssetActor = CastChecked< AHoudiniAssetActor >(CDO); + UHoudiniAssetComponent* HoudiniAssetComponent = HoudiniAssetActor->GetHoudiniAssetComponent(); + check(HoudiniAssetComponent); + + UHoudiniCookable* Cookable = HoudiniAssetActor->GetHoudiniCookable(); + check(Cookable); + + FHoudiniEngineUtils::AddHoudiniLogoToComponent(HoudiniAssetComponent); + if (!HoudiniAssetActor->IsUsedForPreview()) + { + Cookable->SetHoudiniAsset(HoudiniAsset); + FHoudiniEngineRuntime::Get().RegisterHoudiniCookable(Cookable); + + // We probably need to wait for a cook before we can apply the preset? + FHoudiniToolsEditor::ApplyPresetToHoudiniCookable(Preset, Cookable); + } + } +} + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPresetActorFactory.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "ActorFactories/ActorFactory.h" +#include "Runtime/Launch/Resources/Version.h" +#include "HoudiniPresetActorFactory.generated.h" + +class FText; +class AActor; +class UObject; +class UHoudiniAssetComponent; + +struct FAssetData; + +UCLASS(config = Editor) +class UHoudiniPresetActorFactory : public UActorFactory +{ + GENERATED_UCLASS_BODY() + +public: + // UActorFactory methods: + // Return true if Actor can be created from a given asset. + virtual bool CanCreateActorFrom(const FAssetData & AssetData, FText & OutErrorMsg) override; + // Given an instance of an actor pertaining to this factory, find the asset that should be used to create a new actor. + virtual UObject * GetAssetFromActorInstance(AActor * Instance) override; + // Modify the actor after it has been spawned. + virtual void PostSpawnActor(UObject * Asset, AActor * NewActor) override; + // Called after a blueprint is created by this factory to update the blueprint's CDO properties + // with state from the asset for this factory. +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + // TODO 54: FIX ME ?? !! + virtual void PostCreateBlueprint(UObject* Asset, AActor* CDO); +#else + virtual void PostCreateBlueprint(UObject * Asset, AActor * CDO) override; +#endif + +protected: + bool AddHoudiniLogoToComponent(UHoudiniAssetComponent* HAC); +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPresetFactory.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "AssetToolsModule.h" +#include "HoudiniPreset.h" +#include "Factories/Factory.h" +#include "Modules/ModuleManager.h" +#include "UObject/Object.h" + +#include "HoudiniPresetFactory.generated.h" + +enum class EAssetCommandResult : uint8; +struct FAssetCategoryPath; +struct FAssetOpenArgs; + + +// -------------------------------------------- +// HoudiniToolsPackageAsset factory +// -------------------------------------------- + +UCLASS(hidecategories = Object) +class UHoudiniPresetFactory : public UFactory +{ + GENERATED_BODY() + +public: + + UHoudiniPresetFactory() + { + SupportedClass = UHoudiniPreset::StaticClass(); + + bCreateNew = true; + bEditAfterNew = true; + } + + FText GetDisplayName() const override + { + return NSLOCTEXT("DisplayName", "AssetDefinition_HoudiniPreset", "Houdini Preset"); + } + + FText GetToolTip() const override + { + return NSLOCTEXT("Tooltip", "AssetDefinition_HoudiniPresetTooltip", "Contains a preset that can be applied to HDAs."); + } + + UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override + { + UHoudiniPreset* Preset = nullptr; + if (ensure(SupportedClass == Class)) + { + ensure(0 != (RF_Public & Flags)); + Preset = NewObject(InParent, Class, Name, Flags); + } + return Preset; + } + + uint32 GetMenuCategories() const override + { + IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); + return AssetTools.RegisterAdvancedAssetCategory("HoudiniEngine", NSLOCTEXT("AssetCategoryName", "HoudiniEngineCategory", "Houdini Engine")); + } + + FString GetDefaultNewAssetName() const override + { + return TEXT("HoudiniPreset"); + } + + bool ShouldShowInNewMenu() const override { return true; } +}; + + + + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPublicAPI.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include "HoudiniPublicAPI.h" + +#include "HoudiniAsset.h" +#include "HoudiniPreset.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniPublicAPIAssetWrapper.h" +#include "HoudiniPublicAPIInputTypes.h" +#include "HoudiniEngineCommands.h" + +#include "Engine/World.h" +#include "Engine/Level.h" + +UHoudiniPublicAPI::UHoudiniPublicAPI() +{ +} + +bool +UHoudiniPublicAPI::IsSessionValid_Implementation() const +{ + return FHoudiniEngineCommands::IsSessionValid(); +} + +void +UHoudiniPublicAPI::CreateSession_Implementation() +{ + if (!IsSessionValid()) + FHoudiniEngineCommands::CreateSession(); +} + +void +UHoudiniPublicAPI::StopSession_Implementation() +{ + if (IsSessionValid()) + FHoudiniEngineCommands::StopSession(); +} + +void +UHoudiniPublicAPI::RestartSession_Implementation() +{ + if (IsSessionValid()) + FHoudiniEngineCommands::RestartSession(); + else + FHoudiniEngineCommands::CreateSession(); +} + +UHoudiniPublicAPIAssetWrapper* +UHoudiniPublicAPI::InstantiateAsset_Implementation( + UHoudiniAsset* InHoudiniAsset, + const FTransform& InInstantiateAt, + UObject* InWorldContextObject, + ULevel* InSpawnInLevelOverride, + const bool bInEnableAutoCook, + const bool bInEnableAutoBake, + const FString& InBakeDirectoryPath, + const EHoudiniEngineBakeOption InBakeMethod, + const bool bInRemoveOutputAfterBake, + const bool bInRecenterBakedActors, + const bool bInReplacePreviousBake) +{ + if (!IsValid(InHoudiniAsset) || !(InHoudiniAsset->AssetImportData)) + { + SetErrorMessage(TEXT("InHoudiniAsset is invalid or does not have AssetImportData.")); + return nullptr; + } + + // Create wrapper for asset instance + UHoudiniPublicAPIAssetWrapper* Wrapper = UHoudiniPublicAPIAssetWrapper::CreateEmptyWrapper(this); + + if (Wrapper) + { + // Enable/disable error logging based on the API setting + Wrapper->SetLoggingErrorsEnabled(IsLoggingErrors()); + + if (!InstantiateAssetWithExistingWrapper( + Wrapper, + InHoudiniAsset, + InInstantiateAt, + InWorldContextObject, + InSpawnInLevelOverride, + bInEnableAutoCook, + bInEnableAutoBake, + InBakeDirectoryPath, + InBakeMethod, + bInRemoveOutputAfterBake, + bInRecenterBakedActors, + bInReplacePreviousBake)) + { + // failed to instantiate asset, return null + return nullptr; + } + } + + return Wrapper; +} + +bool +UHoudiniPublicAPI::InstantiateAssetWithExistingWrapper_Implementation( + UHoudiniPublicAPIAssetWrapper* InWrapper, + UHoudiniAsset* InHoudiniAsset, + const FTransform& InInstantiateAt, + UObject* InWorldContextObject, + ULevel* InSpawnInLevelOverride, + const bool bInEnableAutoCook, + const bool bInEnableAutoBake, + const FString& InBakeDirectoryPath, + const EHoudiniEngineBakeOption InBakeMethod, + const bool bInRemoveOutputAfterBake, + const bool bInRecenterBakedActors, + const bool bInReplacePreviousBake) +{ + if (!IsValid(InWrapper)) + { + SetErrorMessage(TEXT("InWrapper is not valid.")); + return false; + } + + if (!IsValid(InHoudiniAsset) || !(InHoudiniAsset->AssetImportData)) + { + SetErrorMessage(TEXT("InHoudiniAsset is invalid or does not have AssetImportData.")); + return false; + } + + UWorld* OverrideWorldToSpawnIn = IsValid(InWorldContextObject) ? InWorldContextObject->GetWorld() : nullptr; + AActor* HoudiniAssetActor = FHoudiniEngineEditorUtils::InstantiateHoudiniAssetAt(InHoudiniAsset, InInstantiateAt, OverrideWorldToSpawnIn, InSpawnInLevelOverride); + if (!IsValid(HoudiniAssetActor)) + { + // Determine the path of what would have been the owning level/world of the actor for error logging purposes + const FString LevelPath = IsValid(InSpawnInLevelOverride) ? InSpawnInLevelOverride->GetPathName() : FString(); + const FString WorldPath = (OverrideWorldToSpawnIn && IsValid(OverrideWorldToSpawnIn)) ? OverrideWorldToSpawnIn->GetPathName() : FString(); + const FString OwnerPath = LevelPath.IsEmpty() ? WorldPath : LevelPath; + SetErrorMessage(FString::Printf(TEXT("Failed to spawn a AHoudiniAssetActor in %s."), *OwnerPath)); + return false; + } + + // Wrap the instantiated asset + if (!InWrapper->WrapHoudiniAssetObject(HoudiniAssetActor)) + { + FString WrapperError; + InWrapper->GetLastErrorMessage(WrapperError); + SetErrorMessage(FString::Printf( + TEXT("Failed to wrap '%s': %s."), *(HoudiniAssetActor->GetActorNameOrLabel()), *WrapperError)); + return false; + } + + InWrapper->SetAutoCookingEnabled(bInEnableAutoCook); + + FDirectoryPath BakeDirectoryPath; + BakeDirectoryPath.Path = InBakeDirectoryPath; + InWrapper->SetBakeFolder(BakeDirectoryPath); + InWrapper->SetBakeMethod(InBakeMethod); + InWrapper->SetRemoveOutputAfterBake(bInRemoveOutputAfterBake); + InWrapper->SetRecenterBakedActors(bInRecenterBakedActors); + InWrapper->SetReplacePreviousBake(bInReplacePreviousBake); + InWrapper->SetAutoBakeEnabled(bInEnableAutoBake); + + return true; +} + +UHoudiniPublicAPIAssetWrapper* +UHoudiniPublicAPI::InstantiatePreset_Implementation( + UHoudiniPreset* InHoudiniPreset, + const FTransform& InInstantiateAt, + UObject* InWorldContextObject, + ULevel* InSpawnInLevelOverride, + const bool bInEnableAutoCook, + const bool bInEnableAutoBake, + const FString& InBakeDirectoryPath, + const EHoudiniEngineBakeOption InBakeMethod, + const bool bInRemoveOutputAfterBake, + const bool bInRecenterBakedActors, + const bool bInReplacePreviousBake) +{ + if (!IsValid(InHoudiniPreset)) + { + SetErrorMessage(TEXT("InHoudiniPreset is invalid.")); + return nullptr; + } + + // Get the preset's HDA + UHoudiniAsset* SourceHDA = InHoudiniPreset->SourceHoudiniAsset; + if (!IsValid(SourceHDA) || !(SourceHDA->AssetImportData)) + { + SetErrorMessage(TEXT("The preset's Houdini Asset is invalid or does not have AssetImportData.")); + return nullptr; + } + + // Create wrapper for asset instance + UHoudiniPublicAPIAssetWrapper* Wrapper = UHoudiniPublicAPIAssetWrapper::CreateEmptyWrapper(this); + + if (Wrapper) + { + // Enable/disable error logging based on the API setting + Wrapper->SetLoggingErrorsEnabled(IsLoggingErrors()); + + if (!InstantiatePresetWithExistingWrapper( + Wrapper, + InHoudiniPreset, + InInstantiateAt, + InWorldContextObject, + InSpawnInLevelOverride, + bInEnableAutoCook, + bInEnableAutoBake, + InBakeDirectoryPath, + InBakeMethod, + bInRemoveOutputAfterBake, + bInRecenterBakedActors, + bInReplacePreviousBake)) + { + // failed to instantiate preset, return null + return nullptr; + } + } + + return Wrapper; +} + +bool +UHoudiniPublicAPI::InstantiatePresetWithExistingWrapper_Implementation( + UHoudiniPublicAPIAssetWrapper* InWrapper, + UHoudiniPreset* InHoudiniPreset, + const FTransform& InInstantiateAt, + UObject* InWorldContextObject, + ULevel* InSpawnInLevelOverride, + const bool bInEnableAutoCook, + const bool bInEnableAutoBake, + const FString& InBakeDirectoryPath, + const EHoudiniEngineBakeOption InBakeMethod, + const bool bInRemoveOutputAfterBake, + const bool bInRecenterBakedActors, + const bool bInReplacePreviousBake) +{ + if (!IsValid(InWrapper)) + { + SetErrorMessage(TEXT("InWrapper is not valid.")); + return false; + } + + if (!IsValid(InHoudiniPreset)) + { + SetErrorMessage(TEXT("InHoudiniPreset is invalid.")); + return false; + } + + // Get the preset's HDA + UHoudiniAsset* SourceHDA = InHoudiniPreset->SourceHoudiniAsset; + if (!IsValid(SourceHDA) || !(SourceHDA->AssetImportData)) + { + SetErrorMessage(TEXT("The preset's Houdini Asset is invalid or does not have AssetImportData.")); + return false; + } + + UWorld* OverrideWorldToSpawnIn = IsValid(InWorldContextObject) ? InWorldContextObject->GetWorld() : nullptr; + AActor* HoudiniAssetActor = FHoudiniEngineEditorUtils::InstantiateHoudiniAssetAt(SourceHDA, InInstantiateAt, OverrideWorldToSpawnIn, InSpawnInLevelOverride, InHoudiniPreset); + if (!IsValid(HoudiniAssetActor)) + { + // Determine the path of what would have been the owning level/world of the actor for error logging purposes + const FString LevelPath = IsValid(InSpawnInLevelOverride) ? InSpawnInLevelOverride->GetPathName() : FString(); + const FString WorldPath = IsValid(OverrideWorldToSpawnIn) ? OverrideWorldToSpawnIn->GetPathName() : FString(); + const FString OwnerPath = LevelPath.IsEmpty() ? WorldPath : LevelPath; + SetErrorMessage(FString::Printf(TEXT("Failed to spawn a AHoudiniAssetActor in %s."), *OwnerPath)); + return false; + } + + // Wrap the instantiated asset + if (!InWrapper->WrapHoudiniAssetObject(HoudiniAssetActor)) + { + FString WrapperError; + InWrapper->GetLastErrorMessage(WrapperError); + SetErrorMessage(FString::Printf( + TEXT("Failed to wrap '%s': %s."), *(HoudiniAssetActor->GetActorNameOrLabel()), *WrapperError)); + return false; + } + + InWrapper->SetAutoCookingEnabled(bInEnableAutoCook); + + FDirectoryPath BakeDirectoryPath; + BakeDirectoryPath.Path = InBakeDirectoryPath; + InWrapper->SetBakeFolder(BakeDirectoryPath); + InWrapper->SetBakeMethod(InBakeMethod); + InWrapper->SetRemoveOutputAfterBake(bInRemoveOutputAfterBake); + InWrapper->SetRecenterBakedActors(bInRecenterBakedActors); + InWrapper->SetReplacePreviousBake(bInReplacePreviousBake); + InWrapper->SetAutoBakeEnabled(bInEnableAutoBake); + + return true; +} + + +bool +UHoudiniPublicAPI::IsAssetCookingPaused_Implementation() const +{ + return FHoudiniEngineCommands::IsAssetCookingPaused(); +} + +void +UHoudiniPublicAPI::PauseAssetCooking_Implementation() +{ + if (!IsAssetCookingPaused()) + FHoudiniEngineCommands::PauseAssetCooking(); +} + +void +UHoudiniPublicAPI::ResumeAssetCooking_Implementation() +{ + if (IsAssetCookingPaused()) + FHoudiniEngineCommands::PauseAssetCooking(); +} + +UHoudiniPublicAPIInput* +UHoudiniPublicAPI::CreateEmptyInput_Implementation(TSubclassOf InInputClass, UObject* InOuter) +{ + UObject* Outer = InOuter; + if (!IsValid(Outer)) + Outer = this; + UHoudiniPublicAPIInput* const NewInput = NewObject(Outer, InInputClass.Get()); + if (!IsValid(NewInput)) + { + SetErrorMessage(TEXT("Could not create a new valid UHoudiniPublicAPIInput instance.")); + } + else if (Outer) + { + // Enable/disable error logging based on the outer's setting (if it is a sub-class of UHoudiniPublicAPIObjectBase) + const bool bShouldLogErrors = Outer->IsA() ? + Cast(Outer)->IsLoggingErrors() : IsLoggingErrors(); + NewInput->SetLoggingErrorsEnabled(bShouldLogErrors); + } + + return NewInput; +} + +EHoudiniPublicAPIRampInterpolationType +UHoudiniPublicAPI::ToHoudiniPublicAPIRampInterpolationType(const EHoudiniRampInterpolationType InInterpolationType) +{ + switch (InInterpolationType) + { + case EHoudiniRampInterpolationType::InValid: + return EHoudiniPublicAPIRampInterpolationType::InValid; + case EHoudiniRampInterpolationType::BEZIER: + return EHoudiniPublicAPIRampInterpolationType::BEZIER; + case EHoudiniRampInterpolationType::LINEAR: + return EHoudiniPublicAPIRampInterpolationType::LINEAR; + case EHoudiniRampInterpolationType::BSPLINE: + return EHoudiniPublicAPIRampInterpolationType::BSPLINE; + case EHoudiniRampInterpolationType::HERMITE: + return EHoudiniPublicAPIRampInterpolationType::HERMITE; + case EHoudiniRampInterpolationType::CONSTANT: + return EHoudiniPublicAPIRampInterpolationType::CONSTANT; + case EHoudiniRampInterpolationType::CATMULL_ROM: + return EHoudiniPublicAPIRampInterpolationType::CATMULL_ROM; + case EHoudiniRampInterpolationType::MONOTONE_CUBIC: + return EHoudiniPublicAPIRampInterpolationType::MONOTONE_CUBIC; + } + + return EHoudiniPublicAPIRampInterpolationType::InValid; +} + +EHoudiniRampInterpolationType +UHoudiniPublicAPI::ToHoudiniRampInterpolationType(const EHoudiniPublicAPIRampInterpolationType InInterpolationType) +{ + switch (InInterpolationType) + { + case EHoudiniPublicAPIRampInterpolationType::InValid: + return EHoudiniRampInterpolationType::InValid; + case EHoudiniPublicAPIRampInterpolationType::BEZIER: + return EHoudiniRampInterpolationType::BEZIER; + case EHoudiniPublicAPIRampInterpolationType::LINEAR: + return EHoudiniRampInterpolationType::LINEAR; + case EHoudiniPublicAPIRampInterpolationType::BSPLINE: + return EHoudiniRampInterpolationType::BSPLINE; + case EHoudiniPublicAPIRampInterpolationType::HERMITE: + return EHoudiniRampInterpolationType::HERMITE; + case EHoudiniPublicAPIRampInterpolationType::CONSTANT: + return EHoudiniRampInterpolationType::CONSTANT; + case EHoudiniPublicAPIRampInterpolationType::CATMULL_ROM: + return EHoudiniRampInterpolationType::CATMULL_ROM; + case EHoudiniPublicAPIRampInterpolationType::MONOTONE_CUBIC: + return EHoudiniRampInterpolationType::MONOTONE_CUBIC; + } + + return EHoudiniRampInterpolationType::InValid; +} + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPublicAPIAssetWrapper.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include "HoudiniPublicAPIAssetWrapper.h" + +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniEngineCommands.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEngineManager.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniNodeSyncComponent.h" +#include "HoudiniOutputDetails.h" +#include "HoudiniParameter.h" +#include "HoudiniParameterButton.h" +#include "HoudiniParameterChoice.h" +#include "HoudiniParameterColor.h" +#include "HoudiniParameterFile.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterFolder.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterMultiParm.h" +#include "HoudiniParameterRamp.h" +#include "HoudiniParameterString.h" +#include "HoudiniParameterToggle.h" +#include "HoudiniPDGManager.h" +#include "HoudiniPublicAPI.h" +#include "HoudiniPublicAPIBlueprintLib.h" +#include "HoudiniPublicAPIInputTypes.h" +#include +#include "Landscape.h" +#include "LandscapeInfo.h" +#include "LandscapeStreamingProxy.h" + +FHoudiniPublicAPIRampPoint::FHoudiniPublicAPIRampPoint() + : Position(0) + , Interpolation(EHoudiniPublicAPIRampInterpolationType::LINEAR) +{ +} + +FHoudiniPublicAPIRampPoint::FHoudiniPublicAPIRampPoint( + const float InPosition, + const EHoudiniPublicAPIRampInterpolationType InInterpolation) + : Position(InPosition) + , Interpolation(InInterpolation) +{ + +} + +FHoudiniPublicAPIFloatRampPoint::FHoudiniPublicAPIFloatRampPoint() + : Value(0) +{ +} + +FHoudiniPublicAPIFloatRampPoint::FHoudiniPublicAPIFloatRampPoint( + const float InPosition, + const float InValue, + const EHoudiniPublicAPIRampInterpolationType InInterpolation) + : FHoudiniPublicAPIRampPoint(InPosition, InInterpolation) + , Value(InValue) +{ + +} + +FHoudiniPublicAPIColorRampPoint::FHoudiniPublicAPIColorRampPoint() + : Value(FLinearColor::Black) +{ +} + +FHoudiniPublicAPIColorRampPoint::FHoudiniPublicAPIColorRampPoint( + const float InPosition, + const FLinearColor& InValue, + const EHoudiniPublicAPIRampInterpolationType InInterpolation) + : FHoudiniPublicAPIRampPoint(InPosition, InInterpolation) + , Value(InValue) +{ + +} + +FHoudiniParameterTuple::FHoudiniParameterTuple() + : BoolValues() + , Int32Values() + , FloatValues() + , StringValues() +{ +} + +FHoudiniParameterTuple::FHoudiniParameterTuple(const bool& InValue) + : FHoudiniParameterTuple() +{ + BoolValues.Add(InValue); +} + +FHoudiniParameterTuple::FHoudiniParameterTuple(const TArray& InValues) + : BoolValues(InValues) +{ +} + +FHoudiniParameterTuple::FHoudiniParameterTuple(const int32& InValue) + : FHoudiniParameterTuple() +{ + Int32Values.Add(InValue); +} + +FHoudiniParameterTuple::FHoudiniParameterTuple(const TArray& InValues) + : Int32Values(InValues) +{ +} + +FHoudiniParameterTuple::FHoudiniParameterTuple(const float& InValue) + : FHoudiniParameterTuple() +{ + FloatValues.Add(InValue); +} + +FHoudiniParameterTuple::FHoudiniParameterTuple(const TArray& InValues) + : FloatValues(InValues) +{ +} + +FHoudiniParameterTuple::FHoudiniParameterTuple(const FString& InValue) + : FHoudiniParameterTuple() +{ + StringValues.Add(InValue); +} + +FHoudiniParameterTuple::FHoudiniParameterTuple(const TArray& InValues) + : StringValues(InValues) +{ +} + +FHoudiniParameterTuple::FHoudiniParameterTuple(const TArray& InRampPoints) + : FloatRampPoints(InRampPoints) +{ +} + +FHoudiniParameterTuple::FHoudiniParameterTuple(const TArray& InRampPoints) + : ColorRampPoints(InRampPoints) +{ +} + +// +// UHoudiniPublicAPIAssetWrapper +// + + +UHoudiniPublicAPIAssetWrapper::UHoudiniPublicAPIAssetWrapper() + : HoudiniAssetObject(nullptr) + , bAssetLinkSetupAttemptComplete(false) +{ + +} + +UHoudiniPublicAPIAssetWrapper* +UHoudiniPublicAPIAssetWrapper::CreateWrapper(UObject* InOuter, UObject* InHoudiniAssetActorOrComponent) +{ + if (!IsValid(InHoudiniAssetActorOrComponent)) + return nullptr; + + // Check if InHoudiniAssetActorOrComponent is supported + if (!CanWrapHoudiniObject(InHoudiniAssetActorOrComponent)) + return nullptr; + + UHoudiniPublicAPIAssetWrapper* NewWrapper = CreateEmptyWrapper(InOuter); + if (!IsValid(NewWrapper)) + return nullptr; + + // If we cannot wrap the specified actor, return nullptr. + if (!NewWrapper->WrapHoudiniAssetObject(InHoudiniAssetActorOrComponent)) + { + NewWrapper->MarkAsGarbage(); + return nullptr; + } + + return NewWrapper; +} + +UHoudiniPublicAPIAssetWrapper* +UHoudiniPublicAPIAssetWrapper::CreateEmptyWrapper(UObject* InOuter) +{ + UObject* const Outer = InOuter ? InOuter : GetTransientPackage(); + UClass* const Class = StaticClass(); + UHoudiniPublicAPIAssetWrapper* NewWrapper = NewObject( + Outer, Class, + MakeUniqueObjectName(Outer, Class)); + if (!IsValid(NewWrapper)) + return nullptr; + + return NewWrapper; +} + +bool +UHoudiniPublicAPIAssetWrapper::CanWrapHoudiniObject(UObject* InObject) +{ + if (!IsValid(InObject)) + return false; + + return InObject->IsA() || InObject->IsA() || InObject->IsA(); +} + +bool +UHoudiniPublicAPIAssetWrapper::GetTemporaryCookFolder_Implementation(FDirectoryPath& OutDirectoryPath) const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + OutDirectoryPath = HC->GetTemporaryCookFolder(); + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetTemporaryCookFolder_Implementation(const FDirectoryPath& InDirectoryPath) const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + if (HC->GetTemporaryCookFolder().Path != InDirectoryPath.Path) + { + HC->SetTemporaryCookFolder(InDirectoryPath); + HC->Modify(); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetBakeFolder_Implementation(FDirectoryPath& OutDirectoryPath) const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + OutDirectoryPath = HC->GetBakeFolder(); + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetBakeFolder_Implementation(const FDirectoryPath& InDirectoryPath) const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + if(HC->SetBakeFolder(InDirectoryPath)) + { + HC->Modify(); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::BakeAllOutputs_Implementation() +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + FHoudiniBakeSettings BakeSettings; + BakeSettings.SetFromCookable(HC); + + return FHoudiniEngineBakeUtils::BakeCookable( + HC, + BakeSettings, + HC->GetHoudiniEngineBakeOption(), + HC->GetRemoveOutputAfterBake()); +} + +bool +UHoudiniPublicAPIAssetWrapper::BakeAllOutputsWithSettings_Implementation( + EHoudiniEngineBakeOption InBakeOption, + bool bInReplacePreviousBake, + bool bInRemoveTempOutputsOnSuccess, + bool bInRecenterBakedActors) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + FHoudiniBakeSettings BakeSettings; + BakeSettings.bReplaceActors = bInReplacePreviousBake; + BakeSettings.bReplaceAssets = bInReplacePreviousBake; + BakeSettings.bRecenterBakedActors = bInRecenterBakedActors; + + return FHoudiniEngineBakeUtils::BakeCookable(HC, BakeSettings, InBakeOption, bInRemoveTempOutputsOnSuccess); +} + +bool +UHoudiniPublicAPIAssetWrapper::SetAutoBakeEnabled_Implementation(const bool bInAutoBakeEnabled) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + EHoudiniBakeAfterNextCook bAutoBake = bInAutoBakeEnabled ? EHoudiniBakeAfterNextCook::Always : EHoudiniBakeAfterNextCook::Disabled; + if (HC->GetBakeAfterNextCook() != bAutoBake) + { + HC->SetBakeAfterNextCook(bAutoBake); + HC->Modify(); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::IsAutoBakeEnabled_Implementation() const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + return HC->IsBakeAfterNextCookEnabled(); +} + +bool +UHoudiniPublicAPIAssetWrapper::SetBakeMethod_Implementation(const EHoudiniEngineBakeOption InBakeMethod) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + if (HC->GetHoudiniEngineBakeOption() != InBakeMethod) + { + HC->SetHoudiniEngineBakeOption(InBakeMethod); + HC->Modify(); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetBakeMethod_Implementation(EHoudiniEngineBakeOption& OutBakeMethod) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + OutBakeMethod = HC->GetHoudiniEngineBakeOption(); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetRemoveOutputAfterBake_Implementation(const bool bInRemoveOutputAfterBake) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + HC->SetRemoveOutputAfterBake(bInRemoveOutputAfterBake); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetRemoveOutputAfterBake_Implementation() const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + return HC->GetRemoveOutputAfterBake(); +} + +bool +UHoudiniPublicAPIAssetWrapper::SetRecenterBakedActors_Implementation(const bool bInRecenterBakedActors) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + if (HC->GetRecenterBakedActors() != bInRecenterBakedActors) + { + HC->SetRecenterBakedActors(bInRecenterBakedActors); + HC->Modify(); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetRecenterBakedActors_Implementation() const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + return HC->GetRecenterBakedActors(); +} + +bool +UHoudiniPublicAPIAssetWrapper::SetReplacePreviousBake_Implementation(const bool bInReplacePreviousBake) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + if (HC->GetReplacePreviousBake() != bInReplacePreviousBake) + { + HC->SetReplacePreviousBake(bInReplacePreviousBake); + HC->Modify(); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetReplacePreviousBake_Implementation() const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + return HC->GetReplacePreviousBake(); +} + +TArray +UHoudiniPublicAPIAssetWrapper::GetBakedOutputActors_Implementation() +{ + TArray OutputActors; + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return OutputActors; + + const TArray& BakedOutputs = HC->GetBakedOutputs(); + for (const FHoudiniBakedOutput& BakedOutput : BakedOutputs) + { + for (const auto& BakedPair : BakedOutput.BakedOutputObjects) + { + AActor* Actor = BakedPair.Value.GetActorIfValid(true); + if (Actor) + OutputActors.Add(Actor); + + // Get valid Foliage Actors + OutputActors.Append(BakedPair.Value.GetFoliageActorsIfValid(true)); + + // Get valid instanced actors + TArray InstancedActors = BakedPair.Value.GetInstancedActorsIfValid(true); + OutputActors.Append(InstancedActors); + + // Get valid Landscape and Proxies + ALandscape* BakedLandscape = BakedPair.Value.GetLandscapeIfValid(true); + if (BakedLandscape) + OutputActors.Add(BakedLandscape); + } + } + + return OutputActors; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetValidHoudiniAssetActorWithError(AHoudiniAssetActor*& OutActor) const +{ + AHoudiniAssetActor* const Actor = GetHoudiniAssetActor(); + if (!IsValid(Actor)) + { + SetErrorMessage( + TEXT("Could not find a valid AHoudiniAssetActor for the wrapped asset, or no asset has been wrapped.")); + return false; + } + + OutActor = Actor; + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetValidHoudiniAssetComponentWithError(UHoudiniAssetComponent*& OutHAC) const +{ + UHoudiniAssetComponent* const HAC = GetHoudiniAssetComponent(); + if (!IsValid(HAC)) + { + SetErrorMessage( + TEXT("Could not find a valid HoudiniAssetComponent for the wrapped asset, or no asset has been wrapped.")); + return false; + } + + OutHAC = HAC; + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetValidHoudiniCookableWithError(UHoudiniCookable*& OutHC) const +{ + UHoudiniCookable* const HC = GetHoudiniCookable(); + if (!IsValid(HC)) + { + SetErrorMessage( + TEXT("Could not find a valid HoudiniCookable for the wrapped asset, or no asset has been wrapped.")); + return false; + } + + OutHC = HC; + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetValidOutputAtWithError(const int32 InOutputIndex, UHoudiniOutput*& OutOutput) const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + // Check if InOutputIndex is a valid/in-range index + const int32 NumOutputs = HC->GetNumOutputs(); + if (InOutputIndex < 0 || InOutputIndex >= NumOutputs) + { + SetErrorMessage(FString::Printf( + TEXT("Output index %d is out of range [0, %d]"), InOutputIndex, NumOutputs)); + return false; + } + + UHoudiniOutput* const Output= HC->GetOutputAt(InOutputIndex); + if (!IsValid(Output)) + { + SetErrorMessage(FString::Printf(TEXT("Output at index %d is invalid."), InOutputIndex)); + return false; + } + + OutOutput = Output; + return true; +} + + +UHoudiniPDGAssetLink* +UHoudiniPublicAPIAssetWrapper::GetHoudiniPDGAssetLink() const +{ + UHoudiniCookable* const HC = GetHoudiniCookable(); + if (!IsValid(HC)) + return nullptr; + + return HC->GetPDGAssetLink(); +} + +bool +UHoudiniPublicAPIAssetWrapper::GetValidHoudiniPDGAssetLinkWithError(UHoudiniPDGAssetLink*& OutAssetLink) const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + UHoudiniPDGAssetLink* const AssetLink = HC->GetPDGAssetLink(); + if (!IsValid(AssetLink)) + { + SetErrorMessage( + TEXT("Could not find a valid HoudiniPDGAssetLink for the wrapped asset. Does it contain a TOP network?")); + return false; + } + + OutAssetLink = AssetLink; + return true; +} + +void +UHoudiniPublicAPIAssetWrapper::ClearHoudiniAssetObject_Implementation() +{ + UHoudiniPDGAssetLink* const AssetLink = GetHoudiniPDGAssetLink(); + if (IsValid(AssetLink)) + { + if (OnPDGPostTOPNetworkCookDelegateHandle.IsValid()) + AssetLink->GetOnPostTOPNetworkCookDelegate().Remove(OnPDGPostTOPNetworkCookDelegateHandle); + if (OnPDGPostBakeDelegateHandle.IsValid()) + AssetLink->GetOnPostBakeDelegate().Remove(OnPDGPostBakeDelegateHandle); + } + + bAssetLinkSetupAttemptComplete = false; + + FHoudiniEngineUtils::GetOnHoudiniProxyMeshesRefinedDelegate().Remove(OnHoudiniProxyMeshesRefinedDelegateHandle); + + UHoudiniCookable* const HC = GetHoudiniCookable(); + if (IsValid(HC)) + { + if (OnAssetStateChangeDelegateHandle.IsValid()) + HC->GetOnAssetStateChangeDelegate().Remove(OnAssetStateChangeDelegateHandle); + if (OnPostCookDelegateHandle.IsValid()) + HC->GetOnPostCookDelegate().Remove(OnPostCookDelegateHandle); + if (OnPostBakeDelegateHandle.IsValid()) + HC->GetBakingData()->GetOnPostBakeDelegate().Remove(OnPostBakeDelegateHandle); + } + + OnPDGPostTOPNetworkCookDelegateHandle.Reset(); + OnPDGPostBakeDelegateHandle.Reset(); + OnAssetStateChangeDelegateHandle.Reset(); + OnPostCookDelegateHandle.Reset(); + OnPostBakeDelegateHandle.Reset(); + OnHoudiniProxyMeshesRefinedDelegateHandle.Reset(); + + HoudiniAssetObject = nullptr; + CachedHoudiniAssetActor = nullptr; + CachedHoudiniCookable = nullptr; + CachedHoudiniAssetComponent = nullptr; +} + +bool +UHoudiniPublicAPIAssetWrapper::WrapHoudiniAssetObject_Implementation(UObject* InHoudiniAssetObjectToWrap) +{ + // If InHoudiniAssetObjectToWrap is null, just unwrap any currently wrapped asset + if (!InHoudiniAssetObjectToWrap) + { + ClearHoudiniAssetObject(); + return true; + } + + // Check if InHoudiniAssetObjectToWrap is supported + if (!CanWrapHoudiniObject(InHoudiniAssetObjectToWrap)) + { + UClass* const ObjectClass = IsValid(InHoudiniAssetObjectToWrap) ? InHoudiniAssetObjectToWrap->GetClass() : nullptr; + SetErrorMessage(FString::Printf( + TEXT("Cannot wrap objects of class '%s'."), ObjectClass ? *(ObjectClass->GetName()) : TEXT("Unknown"))); + return false; + } + + // First unwrap/unbind if we are currently wrapping an instantiated asset + ClearHoudiniAssetObject(); + + HoudiniAssetObject = InHoudiniAssetObjectToWrap; + + // Cache the HoudiniAssetActor and HoudiniAssetComponent + if (HoudiniAssetObject->IsA()) + { + CachedHoudiniAssetActor = Cast(InHoudiniAssetObjectToWrap); + CachedHoudiniCookable = CachedHoudiniAssetActor->GetHoudiniCookable(); + if (CachedHoudiniCookable.IsValid()) + { + CachedHoudiniAssetComponent = Cast(CachedHoudiniCookable->GetComponent()); + } + else + { + CachedHoudiniAssetComponent = CachedHoudiniAssetActor->GetHoudiniAssetComponent(); + } + } + else if (HoudiniAssetObject->IsA()) + { + CachedHoudiniAssetComponent = Cast(InHoudiniAssetObjectToWrap); + CachedHoudiniAssetActor = Cast(CachedHoudiniAssetComponent->GetOwner()); + CachedHoudiniCookable = CachedHoudiniAssetComponent->GetCookable(); + } + else if (HoudiniAssetObject->IsA()) + { + CachedHoudiniCookable = Cast(InHoudiniAssetObjectToWrap); + CachedHoudiniAssetActor = Cast(CachedHoudiniCookable->GetOwner()); + CachedHoudiniAssetComponent = Cast(CachedHoudiniCookable->GetComponent()); + } + + UHoudiniCookable* const HC = GetHoudiniCookable(); + if (IsValid(HC)) + { + // Bind to HandleOnHoudiniAssetStateChange from the HC: we also implement IHoudiniAssetStateEvents, and + // in the default implementation HandleOnHoudiniAssetStateChange will call the appropriate Handle functions + // for PostInstantiate, PostCook etc + OnAssetStateChangeDelegateHandle = HC->GetOnAssetStateChangeDelegate().AddUFunction(this, TEXT("HandleOnHoudiniCookableStateChange")); + OnPostCookDelegateHandle = HC->GetOnPostCookDelegate().AddUFunction(this, TEXT("HandleOnHoudiniCookablePostCook")); + OnPostBakeDelegateHandle = HC->GetBakingData()->GetOnPostBakeDelegate().AddUFunction(this, TEXT("HandleOnHoudiniCookablePostBake")); + } + + OnHoudiniProxyMeshesRefinedDelegateHandle = FHoudiniEngineUtils::GetOnHoudiniProxyMeshesRefinedDelegate().AddUFunction(this, TEXT("HandleOnHoudiniProxyMeshesRefinedGlobal")); + + // PDG asset link bindings: We attempt to bind to PDG here, but it likely is not available yet. + // We have to wait until post instantiation in order to know if there is a PDG asset link + // for this HDA. This is checked again in HandleOnHoudiniCookableStateChange and sets + // bAssetLinkSetupAttemptComplete. + BindToPDGAssetLink(); + + return true; +} + +AHoudiniAssetActor* +UHoudiniPublicAPIAssetWrapper::GetHoudiniAssetActor_Implementation() const +{ + return CachedHoudiniAssetActor.Get(); +} + +UHoudiniAssetComponent* +UHoudiniPublicAPIAssetWrapper::GetHoudiniAssetComponent_Implementation() const +{ + return CachedHoudiniAssetComponent.Get(); +} + +UHoudiniCookable* +UHoudiniPublicAPIAssetWrapper::GetHoudiniCookable_Implementation() const +{ + return CachedHoudiniCookable.Get(); +} + +bool +UHoudiniPublicAPIAssetWrapper::DeleteInstantiatedAsset_Implementation() +{ + AHoudiniAssetActor* AssetActor = nullptr; + if (!GetValidHoudiniAssetActorWithError(AssetActor)) + return false; + + // If the Houdini Asset Actor, is selected in the Editor, Unreal will crash when saving a level. + USelection* Selection = GEditor->GetSelectedActors(); + for (FSelectionIterator It(*Selection); It; ++It) + { + AActor* CurrentActor = Cast(*It); + if (CurrentActor == AssetActor) + { + GEditor->SelectNone(true, true); + break; + } + } + + // Unbind / unwrap the HDA actor + ClearHoudiniAssetObject(); + AssetActor->Destroy(); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::Rebuild_Implementation() +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + HC->MarkAsNeedRebuild(); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::Recook_Implementation() +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + HC->MarkAsNeedCook(); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetAutoCookingEnabled_Implementation(const bool bInSetEnabled) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + bool bModified = false; + + // Enable cooking... + if (HC->IsCookingEnabled() != bInSetEnabled) + { + HC->SetCookingEnabled(bInSetEnabled); + bModified = true; + } + + // ... and the cook triggers + if (HC->GetCookOnTransformChange() != bInSetEnabled) + { + HC->SetCookOnTransformChange(bInSetEnabled); + bModified = true; + } + + if (HC->GetCookOnInputChange() != bInSetEnabled) + { + HC->SetCookOnInputChange(bInSetEnabled); + bModified = true; + } + + if (HC->GetCookOnParameterChange() != bInSetEnabled) + { + HC->SetCookOnParameterChange(bInSetEnabled); + bModified = true; + } + + if(bModified) + HC->Modify(); + + return bModified; +} + +bool +UHoudiniPublicAPIAssetWrapper::IsAutoCookingEnabled_Implementation() const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + return HC->IsCookingEnabled(); +} + +bool +UHoudiniPublicAPIAssetWrapper::SetDoNotGenerateOutputs_Implementation(const bool bInSetEnabled) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + if (HC->IsOutputless() == bInSetEnabled) + return false; + + HC->SetOutputless(bInSetEnabled); + HC->Modify(); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::IsDoNotGenerateOutputsEnabled_Implementation() const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + return HC->IsOutputless(); +} + + +bool +UHoudiniPublicAPIAssetWrapper::SetCookOnParameterOrInputChanges_Implementation(const bool bInSetEnabled) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + bool bModified = false; + if (HC->GetCookOnParameterChange() != bInSetEnabled) + { + HC->SetCookOnParameterChange(bInSetEnabled); + bModified = true; + } + + if (HC->GetCookOnInputChange() != bInSetEnabled) + { + HC->SetCookOnInputChange(bInSetEnabled); + bModified = true; + } + + if(bModified) + HC->Modify(); + + return bModified; +} + +bool +UHoudiniPublicAPIAssetWrapper::IsCookOnParameterOrInputChangesEnabled_Implementation() const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + return HC->GetCookOnParameterChange(); +} + +bool +UHoudiniPublicAPIAssetWrapper::SetCookOnTransformChange_Implementation(const bool bInSetEnabled) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + if (HC->GetCookOnTransformChange() == bInSetEnabled) + return false; + + HC->SetCookOnTransformChange(bInSetEnabled); + HC->Modify(); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::IsCookOnTransformChangeEnabled_Implementation() const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + return HC->GetCookOnTransformChange(); +} + +bool +UHoudiniPublicAPIAssetWrapper::SetCookOnAssetInputCook_Implementation(const bool bInSetEnabled) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + if (HC->GetCookOnCookableInputCook() == bInSetEnabled) + return false; + + HC->SetCookOnCookableInputCook(bInSetEnabled); + HC->Modify(); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::IsCookOnAssetInputCookEnabled_Implementation() const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + return HC->GetCookOnCookableInputCook(); +} + + +bool +UHoudiniPublicAPIAssetWrapper::SetFloatParameterValue_Implementation(FName InParameterTupleName, float InValue, int32 InAtIndex, bool bInMarkChanged) +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // Handle all the cases where the underlying parameter value is a float + const EHoudiniParameterType ParamType = Param->GetParameterType(); + bool bDidChangeValue = false; + if (ParamType == EHoudiniParameterType::Float) + { + UHoudiniParameterFloat* FloatParam = Cast(Param); + if (!IsValid(FloatParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + if (InAtIndex >= FloatParam->GetNumberOfValues()) + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple index %d is out of range (tuple size == %d)."), InAtIndex, FloatParam->GetNumberOfValues())); + return false; + } + + bDidChangeValue = FloatParam->SetValueAt(InValue, InAtIndex); + } + else if (ParamType == EHoudiniParameterType::Color) + { + UHoudiniParameterColor* ColorParam = Cast(Param); + if (!IsValid(ColorParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + static const int32 NumColorChannels = 4; + if (InAtIndex >= NumColorChannels) + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple index %d is out of range (tuple size == %d)."), InAtIndex, NumColorChannels)); + return false; + } + + FLinearColor CurrentColorValue = ColorParam->GetColorValue(); + if (CurrentColorValue.Component(InAtIndex) != InValue) + { + CurrentColorValue.Component(InAtIndex) = InValue; + ColorParam->SetColorValue(CurrentColorValue); + bDidChangeValue = true; + } + } + else + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple '%s' is of a type that cannot be set via SetFloatParamterValue."), *InParameterTupleName.ToString())); + return false; + } + + if (bDidChangeValue && bInMarkChanged) + Param->MarkChanged(true); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetFloatParameterValue_Implementation(FName InParameterTupleName, float& OutValue, int32 InAtIndex) const +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // Handle all the cases where the underlying parameter value is a float + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::Float) + { + UHoudiniParameterFloat* FloatParam = Cast(Param); + if (!IsValid(FloatParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + if (InAtIndex >= FloatParam->GetNumberOfValues()) + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple index %d is out of range (tuple size == %d)."), InAtIndex, FloatParam->GetNumberOfValues())); + return false; + } + + return FloatParam->GetValueAt(InAtIndex, OutValue); + } + else if (ParamType == EHoudiniParameterType::Color) + { + UHoudiniParameterColor* ColorParam = Cast(Param); + if (!IsValid(ColorParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + static const int32 NumColorChannels = 4; + if (InAtIndex >= NumColorChannels) + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple index %d is out of range (tuple size == %d)."), InAtIndex, NumColorChannels)); + return false; + } + + OutValue = ColorParam->GetColorValue().Component(InAtIndex); + return true; + } + + return false; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetColorParameterValue_Implementation(FName InParameterTupleName, const FLinearColor& InValue, bool bInMarkChanged) +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // Handle all the cases where the underlying parameter value is a float + const EHoudiniParameterType ParamType = Param->GetParameterType(); + bool bDidChangeValue = false; + if (ParamType == EHoudiniParameterType::Color) + { + UHoudiniParameterColor* ColorParam = Cast(Param); + if (!IsValid(ColorParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + bDidChangeValue = ColorParam->SetColorValue(InValue); + } + else + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple '%s' is of a type that cannot be set via SetColorParamterValue."), *InParameterTupleName.ToString())); + return false; + } + + if (bDidChangeValue && bInMarkChanged) + Param->MarkChanged(true); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetColorParameterValue_Implementation(FName InParameterTupleName, FLinearColor& OutValue) const +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // Handle all the cases where the underlying parameter value is a float + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::Color) + { + UHoudiniParameterColor* ColorParam = Cast(Param); + if (!IsValid(ColorParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + OutValue = ColorParam->GetColorValue(); + return true; + } + + return false; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetIntParameterValue_Implementation(FName InParameterTupleName, int32 InValue, int32 InAtIndex, bool bInMarkChanged) +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // Handle all the cases where the underlying parameter value is an int or bool + const EHoudiniParameterType ParamType = Param->GetParameterType(); + bool bDidChangeValue = false; + if (ParamType == EHoudiniParameterType::Int) + { + UHoudiniParameterInt* IntParam = Cast(Param); + if (!IsValid(IntParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + if (InAtIndex >= IntParam->GetNumberOfValues()) + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple index %d is out of range (tuple size == %d)."), InAtIndex, IntParam->GetNumberOfValues())); + return false; + } + + bDidChangeValue = IntParam->SetValueAt(InValue, InAtIndex); + } + else if (ParamType == EHoudiniParameterType::IntChoice) + { + UHoudiniParameterChoice* ChoiceParam = Cast(Param); + if (!IsValid(ChoiceParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + bDidChangeValue = ChoiceParam->SetIntValue(InValue); + } + else if (ParamType == EHoudiniParameterType::MultiParm) + { + UHoudiniParameterMultiParm* MultiParam = Cast(Param); + if (!IsValid(MultiParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + bDidChangeValue = MultiParam->SetNumElements(InValue); + } + else if (ParamType == EHoudiniParameterType::Toggle) + { + UHoudiniParameterToggle* ToggleParam = Cast(Param); + if (!IsValid(ToggleParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + bDidChangeValue = ToggleParam->SetValueAt(InValue != 0, InAtIndex); + } + else if (ParamType == EHoudiniParameterType::Folder) + { + UHoudiniParameterFolder* FolderParam = Cast(Param); + if (!IsValid(FolderParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + const bool NewValue = InValue != 0; + if (FolderParam->IsChosen() != NewValue) + { + FolderParam->SetChosen(NewValue); + bDidChangeValue = true; + } + } + else if (ParamType == EHoudiniParameterType::FloatRamp || ParamType == EHoudiniParameterType::ColorRamp) + { + // For ramps we have to use the appropriate function so that delete/insert operations are managed correctly + bDidChangeValue = SetRampParameterNumPoints(InParameterTupleName, InValue); + } + else + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple '%s' is of a type that cannot be set via SetIntParameterValue."), *InParameterTupleName.ToString())); + return false; + } + + if (bDidChangeValue && bInMarkChanged) + Param->MarkChanged(true); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetIntParameterValue_Implementation(FName InParameterTupleName, int32& OutValue, int32 InAtIndex) const +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // Handle all the cases where the underlying parameter value is an int or bool + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::Int) + { + UHoudiniParameterInt* IntParam = Cast(Param); + if (!IsValid(IntParam)) + return false; + + if (InAtIndex >= IntParam->GetNumberOfValues()) + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple index %d is out of range (tuple size == %d)."), InAtIndex, IntParam->GetNumberOfValues())); + return false; + } + + return IntParam->GetValueAt(InAtIndex, OutValue); + } + else if (ParamType == EHoudiniParameterType::IntChoice) + { + UHoudiniParameterChoice* ChoiceParam = Cast(Param); + if (!IsValid(ChoiceParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + OutValue = ChoiceParam->GetIntValue(ChoiceParam->GetIntValueIndex()); + return true; + } + else if (ParamType == EHoudiniParameterType::MultiParm) + { + UHoudiniParameterMultiParm* MultiParam = Cast(Param); + if (!IsValid(MultiParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + OutValue = MultiParam->GetNextInstanceCount(); + return true; + } + else if (ParamType == EHoudiniParameterType::Toggle) + { + UHoudiniParameterToggle* ToggleParam = Cast(Param); + if (!IsValid(ToggleParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + OutValue = ToggleParam->GetValueAt(InAtIndex); + return true; + } + else if (ParamType == EHoudiniParameterType::Folder) + { + UHoudiniParameterFolder* FolderParam = Cast(Param); + if (!IsValid(FolderParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + OutValue = FolderParam->IsChosen(); + return true; + } + + return false; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetBoolParameterValue_Implementation(FName InParameterTupleName, bool InValue, int32 InAtIndex, bool bInMarkChanged) +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // Handle all the cases where the underlying parameter value is an int or bool + const EHoudiniParameterType ParamType = Param->GetParameterType(); + bool bDidChangeValue = false; + if (ParamType == EHoudiniParameterType::Toggle) + { + UHoudiniParameterToggle* ToggleParam = Cast(Param); + if (!IsValid(ToggleParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + bDidChangeValue = ToggleParam->SetValueAt(InValue, InAtIndex); + } + else if (ParamType == EHoudiniParameterType::Folder) + { + UHoudiniParameterFolder* FolderParam = Cast(Param); + if (!IsValid(FolderParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + if (FolderParam->IsChosen() != InValue) + { + FolderParam->SetChosen(InValue); + bDidChangeValue = true; + } + } + else + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple '%s' is of a type that cannot be set via SetBoolParameterValue."), *InParameterTupleName.ToString())); + return false; + } + + if (bDidChangeValue && bInMarkChanged) + Param->MarkChanged(true); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetBoolParameterValue_Implementation(FName InParameterTupleName, bool& OutValue, int32 InAtIndex) const +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // Handle all the cases where the underlying parameter value is an int or bool + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::Toggle) + { + UHoudiniParameterToggle* ToggleParam = Cast(Param); + if (!IsValid(ToggleParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + OutValue = ToggleParam->GetValueAt(InAtIndex); + return true; + } + else if (ParamType == EHoudiniParameterType::Folder) + { + UHoudiniParameterFolder* FolderParam = Cast(Param); + if (!IsValid(FolderParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + OutValue = FolderParam->IsChosen(); + return true; + } + + return false; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetStringParameterValue_Implementation(FName InParameterTupleName, const FString& InValue, int32 InAtIndex, bool bInMarkChanged) +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // Handle all the cases where the underlying parameter value is an int or bool + const EHoudiniParameterType ParamType = Param->GetParameterType(); + bool bDidChangeValue = false; + if (ParamType == EHoudiniParameterType::String || ParamType == EHoudiniParameterType::StringAssetRef) + { + UHoudiniParameterString* StringParam = Cast(Param); + if (!IsValid(StringParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + if (InAtIndex >= StringParam->GetNumberOfValues()) + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple index %d is out of range (tuple size == %d)."), InAtIndex, StringParam->GetNumberOfValues())); + return false; + } + + // We have to handle asset references differently + if (ParamType == EHoudiniParameterType::StringAssetRef) + { + // Find/load the asset, make sure it is a valid reference/object + const FSoftObjectPath AssetRef(InValue); + UObject* const Asset = AssetRef.TryLoad(); + if (IsValid(Asset)) + { + UObject* const CurrentAsset = StringParam->GetAssetAt(InAtIndex); + if (CurrentAsset != Asset) + { + StringParam->SetAssetAt(Asset, InAtIndex); + bDidChangeValue = true; + } + } + else + { + SetErrorMessage(FString::Printf( + TEXT("Asset reference '%s' is invalid. Not setting parameter value."), *InValue)); + return false; + } + } + else + { + bDidChangeValue = StringParam->SetValueAt(InValue, InAtIndex); + } + } + else if (ParamType == EHoudiniParameterType::StringChoice) + { + UHoudiniParameterChoice* ChoiceParam = Cast(Param); + if (!IsValid(ChoiceParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + bDidChangeValue = ChoiceParam->SetStringValue(InValue); + } + else if (ParamType == EHoudiniParameterType::File || ParamType == EHoudiniParameterType::FileDir || + ParamType == EHoudiniParameterType::FileGeo || ParamType == EHoudiniParameterType::FileImage) + { + UHoudiniParameterFile* FileParam = Cast(Param); + if (!IsValid(FileParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + if (InAtIndex >= FileParam->GetNumValues()) + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple index %d is out of range (tuple size == %d)."), InAtIndex, FileParam->GetNumValues())); + return false; + } + + bDidChangeValue = FileParam->SetValueAt(InValue, InAtIndex); + } + else + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple '%s' is of a type that cannot be set via SetStringParameterValue."), *InParameterTupleName.ToString())); + return false; + } + + if (bDidChangeValue && bInMarkChanged) + Param->MarkChanged(true); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetStringParameterValue_Implementation(FName InParameterTupleName, FString& OutValue, int32 InAtIndex) const +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // Handle all the cases where the underlying parameter value is an int or bool + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::String || ParamType == EHoudiniParameterType::StringAssetRef) + { + UHoudiniParameterString* StringParam = Cast(Param); + if (!IsValid(StringParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + if (InAtIndex >= StringParam->GetNumberOfValues()) + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple index %d is out of range (tuple size == %d)."), InAtIndex, StringParam->GetNumberOfValues())); + return false; + } + + // For asset references: get the asset, and then get the string reference from it and return that. + // If the asset is null, return the empty string. + if (ParamType == EHoudiniParameterType::StringAssetRef) + { + UObject* const Asset = StringParam->GetAssetAt(InAtIndex); + if (IsValid(Asset)) + { + OutValue = UHoudiniParameterString::GetAssetReference(Asset); + } + else + { + OutValue.Empty(); + } + } + else + { + OutValue = StringParam->GetValueAt(InAtIndex); + } + return true; + } + else if (ParamType == EHoudiniParameterType::StringChoice) + { + UHoudiniParameterChoice* ChoiceParam = Cast(Param); + if (!IsValid(ChoiceParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + OutValue = ChoiceParam->GetStringValue(); + return true; + } + else if (ParamType == EHoudiniParameterType::File || ParamType == EHoudiniParameterType::FileDir || + ParamType == EHoudiniParameterType::FileGeo || ParamType == EHoudiniParameterType::FileImage) + { + UHoudiniParameterFile* FileParam = Cast(Param); + if (!IsValid(FileParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + if (InAtIndex >= FileParam->GetNumValues()) + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple index %d is out of range (tuple size == %d)."), InAtIndex, FileParam->GetNumValues())); + return false; + } + + OutValue = FileParam->GetValueAt(InAtIndex); + return true; + } + + return false; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetAssetRefParameterValue_Implementation(FName InParameterTupleName, UObject* InValue, int32 InAtIndex, bool bInMarkChanged) +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // Handle all the cases where the underlying parameter value is an int or bool + const EHoudiniParameterType ParamType = Param->GetParameterType(); + bool bDidChangeValue = false; + if (ParamType == EHoudiniParameterType::StringAssetRef) + { + UHoudiniParameterString* StringParam = Cast(Param); + if (!IsValid(StringParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + if (InAtIndex >= StringParam->GetNumberOfValues()) + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple index %d is out of range (tuple size == %d)."), InAtIndex, StringParam->GetNumberOfValues())); + return false; + } + + // Find/load the asset, make sure it is a valid reference/object + UObject* const CurrentAsset = StringParam->GetAssetAt(InAtIndex); + if (CurrentAsset != InValue) + { + StringParam->SetAssetAt(InValue, InAtIndex); + bDidChangeValue = true; + } + } + else + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple '%s' is of a type that cannot be set via SetAssetRefParamter."), *InParameterTupleName.ToString())); + return false; + } + + if (bDidChangeValue && bInMarkChanged) + Param->MarkChanged(true); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetAssetRefParameterValue_Implementation(FName InParameterTupleName, UObject*& OutValue, int32 InAtIndex) const +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // Handle all the cases where the underlying parameter value is an int or bool + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::StringAssetRef) + { + UHoudiniParameterString* StringParam = Cast(Param); + if (!IsValid(StringParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + if (InAtIndex >= StringParam->GetNumberOfValues()) + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple index %d is out of range (tuple size == %d)."), InAtIndex, StringParam->GetNumberOfValues())); + return false; + } + + OutValue = StringParam->GetAssetAt(InAtIndex); + return true; + } + + return false; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetRampParameterNumPoints_Implementation(FName InParameterTupleName, const int32 InNumPoints) const +{ + if (InNumPoints < 1) + { + SetErrorMessage(TEXT("InNumPoints must be >= 1.")); + return false; + } + + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // If the parameter is not set to auto update, or if cooking is paused, we have to set the cached points and + // not the main points. + // const bool bCookingEnabled = !FHoudiniEngineCommands::IsAssetCookingPaused(); + // const bool bUseCachedPoints = (!Param->IsAutoUpdate() || !bCookingEnabled); + const bool bUseCachedPoints = !Param->IsAutoUpdate(); + + UHoudiniParameterRampFloat* FloatRampParam = nullptr; + UHoudiniParameterRampColor* ColorRampParam = nullptr; + + // Handle all the cases where the underlying parameter value is an int or bool + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::FloatRamp) + { + FloatRampParam = Cast(Param); + if (!IsValid(FloatRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + } + else if (ParamType == EHoudiniParameterType::ColorRamp) + { + ColorRampParam = Cast(Param); + if (!IsValid(ColorRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + } + else + { + SetErrorMessage(FString::Printf( + TEXT("Parameter '%s' is not a float or color ramp parameter."), *(InParameterTupleName.ToString()))); + return false; + } + + if (!FloatRampParam && !ColorRampParam) + { + return false; + } + + if (bUseCachedPoints) + { + // When using the cached points we only have to resize the array + if (FloatRampParam) + { + const int32 CurrentNumPoints = FloatRampParam->CachedPoints.Num(); + if (CurrentNumPoints != InNumPoints) + { + FloatRampParam->CachedPoints.SetNum(InNumPoints); + // FloatRampParam->MarkChanged(true); + } + } + else + { + const int32 CurrentNumPoints = ColorRampParam->CachedPoints.Num(); + if (CurrentNumPoints != InNumPoints) + { + ColorRampParam->CachedPoints.SetNum(InNumPoints); + // ColorRampParam->MarkChanged(true); + } + } + + // Update the ramp's widget if it is currently visible/selected + const bool bForceFullUpdate = true; + FHoudiniEngineUtils::UpdateEditorProperties(bForceFullUpdate); + } + else + { + int32 NumPendingInsertOperations = 0; + int32 NumPendingDeleteOperations = 0; + TSet InstanceIndexesPendingDelete; + + TArray>& ModificationEvents = FloatRampParam ? FloatRampParam->ModificationEvents : ColorRampParam->ModificationEvents; + for (UHoudiniParameterRampModificationEvent const* const Event : ModificationEvents) + { + if (!IsValid(Event)) + continue; + + if (Event->IsInsertEvent()) + NumPendingInsertOperations++; + else if (Event->IsDeleteEvent()) + { + InstanceIndexesPendingDelete.Add(Event->DeleteInstanceIndex); + NumPendingDeleteOperations++; + } + } + + const int32 PointsArraySize = FloatRampParam ? FloatRampParam->Points.Num() : ColorRampParam->Points.Num(); + int32 CurrentNumPoints = PointsArraySize + NumPendingInsertOperations - NumPendingDeleteOperations; + + if (InNumPoints < CurrentNumPoints) + { + // When deleting points, first remove pending insert operations from the end + if (NumPendingInsertOperations > 0) + { + const int32 NumEvents = ModificationEvents.Num(); + TArray TempModificationArray; + TempModificationArray.Reserve(NumEvents); + + for (int32 Index = NumEvents - 1; Index >= 0; --Index) + { + UHoudiniParameterRampModificationEvent* const Event = ModificationEvents[Index]; + if (InNumPoints < CurrentNumPoints && IsValid(Event) && Event->IsInsertEvent()) + { + CurrentNumPoints--; + NumPendingInsertOperations--; + continue; + } + + TempModificationArray.Add(Event); + } + + Algo::Reverse(TempModificationArray); + ModificationEvents = MoveTemp(TempModificationArray); + } + + // If we still have points to delete... + if (InNumPoints < CurrentNumPoints) + { + // Deleting points, add delete operations, deleting from the end of Points (points that are not yet + // pending delete) + for (int32 Index = PointsArraySize - 1; (InNumPoints < CurrentNumPoints && Index >= 0); --Index) + { + int32 InstanceIndex = INDEX_NONE; + + if (FloatRampParam) + { + UHoudiniParameterRampFloatPoint const* const PointData = FloatRampParam->Points[Index]; + if (!IsValid(PointData)) + continue; + + InstanceIndex = PointData->InstanceIndex; + } + else + { + UHoudiniParameterRampColorPoint const* const PointData = ColorRampParam->Points[Index]; + if (!IsValid(PointData)) + continue; + + InstanceIndex = PointData->InstanceIndex; + } + + if (!InstanceIndexesPendingDelete.Contains(InstanceIndex)) + { + InstanceIndexesPendingDelete.Add(InstanceIndex); + CurrentNumPoints--; + + UHoudiniParameterRampModificationEvent* DeleteEvent = NewObject( + FloatRampParam, UHoudiniParameterRampModificationEvent::StaticClass()); + + if (DeleteEvent) + { + if (FloatRampParam) + { + DeleteEvent->SetFloatRampEvent(); + } + else + { + DeleteEvent->SetColorRampEvent(); + } + DeleteEvent->SetDeleteEvent(); + DeleteEvent->DeleteInstanceIndex = InstanceIndex; + + ModificationEvents.Add(DeleteEvent); + } + } + } + } + + Param->MarkChanged(true); + } + else if (InNumPoints > CurrentNumPoints) + { + // Adding points, add insert operations + while (InNumPoints > CurrentNumPoints) + { + CurrentNumPoints++; + UHoudiniParameterRampModificationEvent* InsertEvent = NewObject( + Param, UHoudiniParameterRampModificationEvent::StaticClass()); + + if (InsertEvent) + { + if (FloatRampParam) + { + InsertEvent->SetFloatRampEvent(); + } + else + { + InsertEvent->SetColorRampEvent(); + } + InsertEvent->SetInsertEvent(); + // Leave point position, value and interpolation at default + + ModificationEvents.Add(InsertEvent); + } + } + + Param->MarkChanged(true); + } + + // If at this point InNumPoints != CurrentNumPoints then something went wrong, we couldn't delete all the + // desired points + if (InNumPoints != CurrentNumPoints) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected error: could not delete the required number of ramp points " + "(target # points = %d; have # points %d)."), InNumPoints, CurrentNumPoints)); + return false; + } + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetRampParameterNumPoints_Implementation(FName InParameterTupleName, int32& OutNumPoints) const +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + // If the parameter is not set to auto update, or if cooking is paused, we have to set the cached points and + // not the main points. + // const bool bCookingEnabled = !FHoudiniEngineCommands::IsAssetCookingPaused(); + // const bool bUseCachedPoints = (!Param->IsAutoUpdate() || !bCookingEnabled); + const bool bUseCachedPoints = !Param->IsAutoUpdate(); + + UHoudiniParameterRampFloat* FloatRampParam = nullptr; + UHoudiniParameterRampColor* ColorRampParam = nullptr; + + // Handle all the cases where the underlying parameter value is an int or bool + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::FloatRamp) + { + FloatRampParam = Cast(Param); + if (!IsValid(FloatRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + } + else if (ParamType == EHoudiniParameterType::ColorRamp) + { + ColorRampParam = Cast(Param); + if (!IsValid(ColorRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + } + else + { + SetErrorMessage(FString::Printf( + TEXT("Parameter '%s' is not a float or color ramp parameter."), *(InParameterTupleName.ToString()))); + return false; + } + + if (bUseCachedPoints) + { + // When using the cached points we only have to resize the array + if (FloatRampParam) + { + OutNumPoints = FloatRampParam->CachedPoints.Num(); + } + else if (ColorRampParam) + { + OutNumPoints = ColorRampParam->CachedPoints.Num(); + } + } + else + { + int32 NumPendingInsertOperations = 0; + int32 NumPendingDeleteOperations = 0; + + if (!FloatRampParam && !ColorRampParam) + return false; + + TArray>& ModificationEvents = FloatRampParam ? FloatRampParam->ModificationEvents : ColorRampParam->ModificationEvents; + for (UHoudiniParameterRampModificationEvent const* const Event : ModificationEvents) + { + if (!IsValid(Event)) + continue; + + if (Event->IsInsertEvent()) + NumPendingInsertOperations++; + else if (Event->IsDeleteEvent()) + NumPendingDeleteOperations++; + } + + const int32 PointsArraySize = FloatRampParam ? FloatRampParam->Points.Num() : ColorRampParam->Points.Num(); + OutNumPoints = PointsArraySize + NumPendingInsertOperations - NumPendingDeleteOperations; + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetFloatRampParameterPointValue_Implementation( + FName InParameterTupleName, + const int32 InPointIndex, + const float InPointPosition, + const float InPointValue, + const EHoudiniPublicAPIRampInterpolationType InInterpolationType, + const bool bInMarkChanged) +{ + return SetRampParameterPointValue( + InParameterTupleName, InPointIndex, InPointPosition, InPointValue, FLinearColor::Black, InInterpolationType, bInMarkChanged); +} + +bool +UHoudiniPublicAPIAssetWrapper::GetFloatRampParameterPointValue_Implementation( + FName InParameterTupleName, + const int32 InPointIndex, + float& OutPointPosition, + float& OutPointValue, + EHoudiniPublicAPIRampInterpolationType& OutInterpolationType) const +{ + FLinearColor ColorValue; + return GetRampParameterPointValue( + InParameterTupleName, InPointIndex, OutPointPosition, OutPointValue, ColorValue, OutInterpolationType); +} + +bool +UHoudiniPublicAPIAssetWrapper::SetFloatRampParameterPoints_Implementation( + FName InParameterTupleName, + const TArray& InRampPoints, + const bool bInMarkChanged) +{ + const int32 TargetNumPoints = InRampPoints.Num(); + if (TargetNumPoints == 0) + { + SetErrorMessage(TEXT("InRampPoints must have at least one entry.")); + return false; + } + + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + UHoudiniParameterRampFloat* FloatRampParam = nullptr; + + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::FloatRamp) + { + FloatRampParam = Cast(Param); + if (!IsValid(FloatRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + } + else + { + SetErrorMessage(FString::Printf(TEXT("Parameter '%s' is not a float ramp or color ramp parameter."), *(Param->GetName()))); + return false; + } + + // If the parameter is not set to auto update, or if cooking is paused, we have to set the cached points and + // not the main points. + // const bool bCookingEnabled = !FHoudiniEngineCommands::IsAssetCookingPaused(); + // const bool bUseCachedPoints = (!Param->IsAutoUpdate() || !bCookingEnabled); + const bool bUseCachedPoints = !Param->IsAutoUpdate(); + + // Set the ramp point count to match the size of InRampPoints + if (!SetRampParameterNumPoints(InParameterTupleName, TargetNumPoints)) + return false; + + // Get all ramp points (INDEX_NONE == get all points) + TArray> RampPointData; + if (!FindRampPointData(Param, INDEX_NONE, RampPointData) || RampPointData.Num() <= 0) + return false; + + // Check that we fetched the correct number of point data objects + if (RampPointData.Num() != TargetNumPoints) + { + SetErrorMessage(FString::Printf(TEXT("Failed to set the number of ramp points to %d."), TargetNumPoints)); + return false; + } + + for (int32 Index = 0; Index < TargetNumPoints; ++Index) + { + TPair const& Entry = RampPointData[Index]; + UObject* const PointData = Entry.Key; + const bool bIsPointData = Entry.Value; + if (!IsValid(PointData)) + return false; + + const FHoudiniPublicAPIFloatRampPoint& NewRampPoint = InRampPoints[Index]; + const EHoudiniRampInterpolationType NewInterpolation = UHoudiniPublicAPI::ToHoudiniRampInterpolationType( + NewRampPoint.Interpolation); + + if (bIsPointData) + { + UHoudiniParameterRampFloatPoint* FloatPointData = Cast(PointData); + if (!IsValid(FloatPointData)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampFloatPoint instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + + if (bUseCachedPoints) + { + // When setting the cached points, we set the values directly instead of using the setters, but we set + // the bCaching flag on the parameter and mark the position/value/interpolation parent parameters as changed + if (FloatPointData->Position != NewRampPoint.Position) + { + FloatPointData->Position = NewRampPoint.Position; + FloatRampParam->bCaching = true; + } + + if (FloatPointData->Value != NewRampPoint.Value) + { + FloatPointData->Value = NewRampPoint.Value; + FloatRampParam->bCaching = true; + } + + if (FloatPointData->Interpolation != NewInterpolation) + { + FloatPointData->Interpolation = NewInterpolation; + FloatRampParam->bCaching = true; + } + } + else + { + // When setting the main points, we set the values using the setters on the point data but still manually + // mark the position/value/interpolation parent parameters as changed + if (FloatPointData->Position != NewRampPoint.Position && FloatPointData->PositionParentParm) + { + FloatPointData->SetPosition(NewRampPoint.Position); + if (bInMarkChanged) + FloatPointData->PositionParentParm->MarkChanged(bInMarkChanged); + } + + if (FloatPointData->Value != NewRampPoint.Value && FloatPointData->ValueParentParm) + { + FloatPointData->SetValue(NewRampPoint.Value); + if (bInMarkChanged) + FloatPointData->ValueParentParm->MarkChanged(bInMarkChanged); + } + + if (FloatPointData->Interpolation != NewInterpolation && FloatPointData->InterpolationParentParm) + { + FloatPointData->SetInterpolation(NewInterpolation); + if (bInMarkChanged) + FloatPointData->InterpolationParentParm->MarkChanged(bInMarkChanged); + } + } + } + else + { + UHoudiniParameterRampModificationEvent* const Event = Cast(PointData); + if (!IsValid(Event)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampModificationEvent instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + + Event->InsertPosition = NewRampPoint.Position; + Event->InsertFloat = NewRampPoint.Value; + Event->InsertInterpolation = NewInterpolation; + } + } + + if (bUseCachedPoints) + { + // Update the ramp's widget if it is currently visible/selected + const bool bForceFullUpdate = true; + FHoudiniEngineUtils::UpdateEditorProperties(bForceFullUpdate); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetFloatRampParameterPoints_Implementation( + FName InParameterTupleName, + TArray& OutRampPoints) const +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + UHoudiniParameterRampFloat* FloatRampParam = nullptr; + + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::FloatRamp) + { + FloatRampParam = Cast(Param); + if (!IsValid(FloatRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + } + else + { + SetErrorMessage(FString::Printf(TEXT("Parameter '%s' is not a float ramp parameter."), *(Param->GetName()))); + return false; + } + + // Get all ramp points (INDEX_NONE == get all points) + TArray> RampPointData; + if (!FindRampPointData(Param, INDEX_NONE, RampPointData) || RampPointData.Num() <= 0) + return false; + + OutRampPoints.Reserve(RampPointData.Num()); + const bool bAllowShrinking = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + OutRampPoints.SetNum(0, bAllowShrinking ? EAllowShrinking::Yes : EAllowShrinking::No); +#else + OutRampPoints.SetNum(0, bAllowShrinking); +#endif + for (TPair const& Entry : RampPointData) + { + UObject* const PointData = Entry.Key; + const bool bIsPointData = Entry.Value; + if (!IsValid(PointData)) + return false; + + FHoudiniPublicAPIFloatRampPoint TempPointData; + + if (bIsPointData) + { + UHoudiniParameterRampFloatPoint* const FloatPointData = Cast(PointData); + if (!IsValid(FloatPointData)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampFloatPoint instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + + TempPointData.Position = FloatPointData->Position; + TempPointData.Value = FloatPointData->Value; + TempPointData.Interpolation = UHoudiniPublicAPI::ToHoudiniPublicAPIRampInterpolationType( + FloatPointData->Interpolation); + } + else + { + UHoudiniParameterRampModificationEvent* const Event = Cast(PointData); + if (!IsValid(Event)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampModificationEvent instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + + TempPointData.Position = Event->InsertPosition; + TempPointData.Value = Event->InsertFloat; + TempPointData.Interpolation = UHoudiniPublicAPI::ToHoudiniPublicAPIRampInterpolationType(Event->InsertInterpolation); + } + + OutRampPoints.Add(TempPointData); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetColorRampParameterPointValue_Implementation( + FName InParameterTupleName, + const int32 InPointIndex, + const float InPointPosition, + const FLinearColor& InPointValue, + const EHoudiniPublicAPIRampInterpolationType InInterpolationType, + const bool bInMarkChanged) +{ + const float FloatValue = 0; + return SetRampParameterPointValue( + InParameterTupleName, InPointIndex, InPointPosition, FloatValue, InPointValue, InInterpolationType, bInMarkChanged); +} + +bool +UHoudiniPublicAPIAssetWrapper::GetColorRampParameterPointValue_Implementation( + FName InParameterTupleName, + const int32 InPointIndex, + float& OutPointPosition, + FLinearColor& OutPointValue, + EHoudiniPublicAPIRampInterpolationType& OutInterpolationType) const +{ + float FloatValue = 0; + return GetRampParameterPointValue( + InParameterTupleName, InPointIndex, OutPointPosition, FloatValue, OutPointValue, OutInterpolationType); +} + +bool +UHoudiniPublicAPIAssetWrapper::SetColorRampParameterPoints_Implementation( + FName InParameterTupleName, + const TArray& InRampPoints, + const bool bInMarkChanged) +{ + const int32 TargetNumPoints = InRampPoints.Num(); + if (TargetNumPoints == 0) + { + SetErrorMessage(TEXT("InRampPoints must have at least one entry.")); + return false; + } + + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + UHoudiniParameterRampColor* ColorRampParam = nullptr; + + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::ColorRamp) + { + ColorRampParam = Cast(Param); + if (!IsValid(ColorRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + } + else + { + SetErrorMessage(FString::Printf(TEXT("Parameter '%s' is not a color ramp parameter."), *(Param->GetName()))); + return false; + } + + // If the parameter is not set to auto update, or if cooking is paused, we have to set the cached points and + // not the main points. + // const bool bCookingEnabled = !FHoudiniEngineCommands::IsAssetCookingPaused(); + // const bool bUseCachedPoints = (!Param->IsAutoUpdate() || !bCookingEnabled); + const bool bUseCachedPoints = !Param->IsAutoUpdate(); + + // Set the ramp point count to match the size of InRampPoints + if (!SetRampParameterNumPoints(InParameterTupleName, TargetNumPoints)) + return false; + + // Get all ramp points (INDEX_NONE == get all points) + TArray> RampPointData; + if (!FindRampPointData(Param, INDEX_NONE, RampPointData) || RampPointData.Num() <= 0) + return false; + + // Check that we fetched the correct number of point data objects + if (RampPointData.Num() != TargetNumPoints) + { + SetErrorMessage(FString::Printf(TEXT("Failed to set the number of ramp points to %d."), TargetNumPoints)); + return false; + } + + for (int32 Index = 0; Index < TargetNumPoints; ++Index) + { + TPair const& Entry = RampPointData[Index]; + UObject* const PointData = Entry.Key; + const bool bIsPointData = Entry.Value; + if (!IsValid(PointData)) + return false; + + const FHoudiniPublicAPIColorRampPoint& NewRampPoint = InRampPoints[Index]; + const EHoudiniRampInterpolationType NewInterpolation = UHoudiniPublicAPI::ToHoudiniRampInterpolationType( + NewRampPoint.Interpolation); + + if (bIsPointData) + { + UHoudiniParameterRampColorPoint* ColorPointData = Cast(PointData); + if (!IsValid(ColorPointData)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampColorPoint instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + + if (bUseCachedPoints) + { + // When setting the cached points, we set the values directly instead of using the setters, but we set + // the bCaching flag on the parameter and mark the position/value/interpolation parent parameters as changed + if (ColorPointData->Position != NewRampPoint.Position) + { + ColorPointData->Position = NewRampPoint.Position; + ColorRampParam->bCaching = true; + } + + if (ColorPointData->Value != NewRampPoint.Value) + { + ColorPointData->Value = NewRampPoint.Value; + ColorRampParam->bCaching = true; + } + + if (ColorPointData->Interpolation != NewInterpolation) + { + ColorPointData->Interpolation = NewInterpolation; + ColorRampParam->bCaching = true; + } + } + else + { + // When setting the main points, we set the values using the setters on the point data but still manually + // mark the position/value/interpolation parent parameters as changed + if (ColorPointData->Position != NewRampPoint.Position && ColorPointData->PositionParentParm) + { + ColorPointData->SetPosition(NewRampPoint.Position); + if (bInMarkChanged) + ColorPointData->PositionParentParm->MarkChanged(bInMarkChanged); + } + + if (ColorPointData->Value != NewRampPoint.Value && ColorPointData->ValueParentParm) + { + ColorPointData->SetValue(NewRampPoint.Value); + if (bInMarkChanged) + ColorPointData->ValueParentParm->MarkChanged(bInMarkChanged); + } + + if (ColorPointData->Interpolation != NewInterpolation && ColorPointData->InterpolationParentParm) + { + ColorPointData->SetInterpolation(NewInterpolation); + if (bInMarkChanged) + ColorPointData->InterpolationParentParm->MarkChanged(bInMarkChanged); + } + } + } + else + { + UHoudiniParameterRampModificationEvent* const Event = Cast(PointData); + if (!IsValid(Event)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampModificationEvent instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + + Event->InsertPosition = NewRampPoint.Position; + Event->InsertColor = NewRampPoint.Value; + Event->InsertInterpolation = NewInterpolation; + } + } + + if (bUseCachedPoints) + { + // Update the ramp's widget if it is currently visible/selected + const bool bForceFullUpdate = true; + FHoudiniEngineUtils::UpdateEditorProperties(bForceFullUpdate); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetColorRampParameterPoints_Implementation( + FName InParameterTupleName, + TArray& OutRampPoints) const +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + UHoudiniParameterRampColor* ColorRampParam = nullptr; + + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::ColorRamp) + { + ColorRampParam = Cast(Param); + if (!IsValid(ColorRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + } + else + { + SetErrorMessage(FString::Printf(TEXT("Parameter '%s' is not a float ramp parameter."), *(Param->GetName()))); + return false; + } + + // Get all ramp points (INDEX_NONE == get all points) + TArray> RampPointData; + if (!FindRampPointData(Param, INDEX_NONE, RampPointData) || RampPointData.Num() <= 0) + return false; + + OutRampPoints.Reserve(RampPointData.Num()); + const bool bAllowShrinking = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + OutRampPoints.SetNum(0, bAllowShrinking ? EAllowShrinking::Yes : EAllowShrinking::No); +#else + OutRampPoints.SetNum(0, bAllowShrinking); +#endif + for (TPair const& Entry : RampPointData) + { + UObject* const PointData = Entry.Key; + const bool bIsPointData = Entry.Value; + if (!IsValid(PointData)) + return false; + + FHoudiniPublicAPIColorRampPoint TempPointData; + + if (bIsPointData) + { + UHoudiniParameterRampColorPoint* const ColorPointData = Cast(PointData); + if (!IsValid(ColorPointData)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampColorPoint instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + + TempPointData.Position = ColorPointData->Position; + TempPointData.Value = ColorPointData->Value; + TempPointData.Interpolation = UHoudiniPublicAPI::ToHoudiniPublicAPIRampInterpolationType( + ColorPointData->Interpolation); + } + else + { + UHoudiniParameterRampModificationEvent* const Event = Cast(PointData); + if (!IsValid(Event)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampModificationEvent instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + + TempPointData.Position = Event->InsertPosition; + TempPointData.Value = Event->InsertColor; + TempPointData.Interpolation = UHoudiniPublicAPI::ToHoudiniPublicAPIRampInterpolationType(Event->InsertInterpolation); + } + + OutRampPoints.Add(TempPointData); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::TriggerButtonParameter_Implementation(FName InButtonParameterName) +{ + UHoudiniParameter* Param = FindValidParameterByName(InButtonParameterName); + if (!Param) + return false; + + // Handle all the cases where the underlying parameter value is an int or bool + const EHoudiniParameterType ParamType = Param->GetParameterType(); + // bool bDidTrigger = false; + if (ParamType == EHoudiniParameterType::Button) + { + UHoudiniParameterButton* ButtonParam = Cast(Param); + if (!IsValid(ButtonParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + + // Marking the button as changed will result in it being triggered/clicked via HAPI + if (!ButtonParam->HasChanged() || !ButtonParam->NeedsToTriggerUpdate()) + { + ButtonParam->MarkChanged(true); + // bDidTrigger = true; + } + } + else + { + SetErrorMessage(FString::Printf( + TEXT("Parameter tuple '%s' is not a button."), *InButtonParameterName.ToString())); + return false; + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetParameterTuples_Implementation(TMap& OutParameterTuples) const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + const int32 NumParameters = HC->GetNumParameters(); + OutParameterTuples.Empty(NumParameters); + OutParameterTuples.Reserve(NumParameters); + for (int32 Index = 0; Index < NumParameters; ++Index) + { + const UHoudiniParameter* const Param = HC->GetParameterAt(Index); + const EHoudiniParameterType ParameterType = Param->GetParameterType(); + const int32 TupleSize = Param->GetTupleSize(); + const FName PTName(Param->GetParameterName()); + + FHoudiniParameterTuple ParameterTuple; + + bool bSkipped = false; + switch (ParameterType) + { + case EHoudiniParameterType::Color: + case EHoudiniParameterType::Float: + { + // Output as float + ParameterTuple.FloatValues.SetNumZeroed(TupleSize); + for (int32 TupleIndex = 0; TupleIndex < TupleSize; ++TupleIndex) + { + GetFloatParameterValue(PTName, ParameterTuple.FloatValues[TupleIndex], TupleIndex); + } + break; + } + + case EHoudiniParameterType::Int: + case EHoudiniParameterType::IntChoice: + case EHoudiniParameterType::MultiParm: + { + // Output as int + ParameterTuple.Int32Values.SetNumZeroed(TupleSize); + for (int32 TupleIndex = 0; TupleIndex < TupleSize; ++TupleIndex) + { + GetIntParameterValue(PTName, ParameterTuple.Int32Values[TupleIndex], TupleIndex); + } + break; + } + + case EHoudiniParameterType::String: + case EHoudiniParameterType::StringChoice: + case EHoudiniParameterType::StringAssetRef: + case EHoudiniParameterType::File: + case EHoudiniParameterType::FileDir: + case EHoudiniParameterType::FileGeo: + case EHoudiniParameterType::FileImage: + { + // Output as string + ParameterTuple.StringValues.SetNumZeroed(TupleSize); + for (int32 TupleIndex = 0; TupleIndex < TupleSize; ++TupleIndex) + { + GetStringParameterValue(PTName, ParameterTuple.StringValues[TupleIndex], TupleIndex); + } + break; + } + + case EHoudiniParameterType::Toggle: + { + // Output as bool + ParameterTuple.BoolValues.SetNumZeroed(TupleSize); + for (int32 TupleIndex = 0; TupleIndex < TupleSize; ++TupleIndex) + { + GetBoolParameterValue(PTName, ParameterTuple.BoolValues[TupleIndex], TupleIndex); + } + break; + } + + case EHoudiniParameterType::ColorRamp: + { + GetColorRampParameterPoints(PTName, ParameterTuple.ColorRampPoints); + break; + } + case EHoudiniParameterType::FloatRamp: + { + GetFloatRampParameterPoints(PTName, ParameterTuple.FloatRampPoints); + break; + } + + case EHoudiniParameterType::Button: + case EHoudiniParameterType::ButtonStrip: + case EHoudiniParameterType::Input: + case EHoudiniParameterType::Invalid: + case EHoudiniParameterType::Folder: + case EHoudiniParameterType::FolderList: + case EHoudiniParameterType::Label: + case EHoudiniParameterType::Separator: + default: + // Skipped + bSkipped = true; + break; + } + + if (!bSkipped) + OutParameterTuples.Add(PTName, ParameterTuple); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetParameterTuples_Implementation(const TMap& InParameterTuples) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + bool bSuccess = true; + for (const TPair& Entry : InParameterTuples) + { + const FName& ParameterTupleName = Entry.Key; + const FHoudiniParameterTuple& ParameterTuple = Entry.Value; + if (ParameterTuple.BoolValues.Num() > 0) + { + // Set as bool + const int32 TupleSize = ParameterTuple.BoolValues.Num(); + for (int32 TupleIndex = 0; TupleIndex < TupleSize; ++TupleIndex) + { + if (!SetBoolParameterValue(ParameterTupleName, ParameterTuple.BoolValues[TupleIndex], TupleIndex)) + { + SetErrorMessage(FString::Printf( + TEXT("SetParameterTuples: Failed to set %s as a bool at tuple index %d."), *ParameterTupleName.ToString(), TupleIndex)); + bSuccess = false; + break; + } + } + } + else if (ParameterTuple.FloatValues.Num() > 0) + { + // Set as float + const int32 TupleSize = ParameterTuple.FloatValues.Num(); + for (int32 TupleIndex = 0; TupleIndex < TupleSize; ++TupleIndex) + { + if (!SetFloatParameterValue(ParameterTupleName, ParameterTuple.FloatValues[TupleIndex], TupleIndex)) + { + SetErrorMessage(FString::Printf( + TEXT("SetParameterTuples: Failed to set %s as a float at tuple index %d."), + *ParameterTupleName.ToString(), TupleIndex)); + bSuccess = false; + break; + } + } + } + else if (ParameterTuple.Int32Values.Num() > 0) + { + // Set as int + const int32 TupleSize = ParameterTuple.Int32Values.Num(); + for (int32 TupleIndex = 0; TupleIndex < TupleSize; ++TupleIndex) + { + if (!SetIntParameterValue(ParameterTupleName, ParameterTuple.Int32Values[TupleIndex], TupleIndex)) + { + SetErrorMessage(FString::Printf( + TEXT("SetParameterTuples: Failed to set %s as a int at tuple index %d."), + *ParameterTupleName.ToString(), TupleIndex)); + bSuccess = false; + break; + } + } + } + else if (ParameterTuple.StringValues.Num() > 0) + { + // Set as string + const int32 TupleSize = ParameterTuple.StringValues.Num(); + for (int32 TupleIndex = 0; TupleIndex < TupleSize; ++TupleIndex) + { + if (!SetStringParameterValue(ParameterTupleName, ParameterTuple.StringValues[TupleIndex], TupleIndex)) + { + SetErrorMessage(FString::Printf( + TEXT("SetParameterTuples: Failed to set %s as a string at tuple index %d."), + *ParameterTupleName.ToString(), TupleIndex)); + bSuccess = false; + break; + } + } + } + else if (ParameterTuple.FloatRampPoints.Num() > 0) + { + // Set as a float ramp + if (!SetFloatRampParameterPoints(ParameterTupleName, ParameterTuple.FloatRampPoints)) + bSuccess = false; + } + else if (ParameterTuple.ColorRampPoints.Num() > 0) + { + // Set as a color ramp + if (!SetColorRampParameterPoints(ParameterTupleName, ParameterTuple.ColorRampPoints)) + bSuccess = false; + } + } + + return bSuccess; +} + +UHoudiniPublicAPIInput* +UHoudiniPublicAPIAssetWrapper::CreateEmptyInput_Implementation(TSubclassOf InInputClass) +{ + UHoudiniPublicAPI* API = UHoudiniPublicAPIBlueprintLib::GetAPI(); + if (!IsValid(API)) + return nullptr; + + UHoudiniPublicAPIInput* const NewInput = API->CreateEmptyInput(InInputClass, this); + if (!IsValid(NewInput)) + { + SetErrorMessage(FString::Printf( + TEXT("Failed to create a new input of class '%s'."), + *(IsValid(InInputClass.Get()) ? InInputClass->GetName() : FString()))); + + return nullptr; + } + + return NewInput; +} + +int32 +UHoudiniPublicAPIAssetWrapper::GetNumNodeInputs_Implementation() const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return -1; + + int32 NumNodeInputs = 0; + const int32 NumInputs = HC->GetNumInputs(); + for (int32 Index = 0; Index < NumInputs; ++Index) + { + UHoudiniInput const* const Input = HC->GetInputAt(Index); + if (!IsValid(Input)) + continue; + + if (!Input->IsObjectPathParameter()) + NumNodeInputs++; + } + + return NumNodeInputs; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetInputAtIndex_Implementation(const int32 InNodeInputIndex, const UHoudiniPublicAPIInput* InInput) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + UHoudiniInput* HoudiniInput = GetHoudiniNodeInputByIndex(InNodeInputIndex); + if (!IsValid(HoudiniInput)) + { + SetErrorMessage(FString::Printf( + TEXT("SetInputAtIndex: Could not find a HoudiniInput for InNodeInputIndex %d. Has the HDA been instantiated?"), + InNodeInputIndex)); + return false; + } + + const bool bSuccess = PopulateHoudiniInput(InInput, HoudiniInput); + + // Update the details panel (mostly for when new curves/components are created where visualizers are driven + // through the details panel) + FHoudiniEngineEditorUtils::ReselectComponentOwnerIfSelected(HC->GetComponent()); + + return bSuccess; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetInputAtIndex_Implementation(const int32 InNodeInputIndex, UHoudiniPublicAPIInput*& OutInput) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + const UHoudiniInput* HoudiniInput = GetHoudiniNodeInputByIndex(InNodeInputIndex); + if (!IsValid(HoudiniInput)) + { + SetErrorMessage(FString::Printf( + TEXT("GetInputAtIndex: Could not find a HoudiniInput for InNodeInputIndex %d. Has the HDA been instantiated?"), + InNodeInputIndex)); + return false; + } + + UHoudiniPublicAPIInput* APIInput = nullptr; + const bool bSuccessfullyCopied = CreateAndPopulateAPIInput(HoudiniInput, APIInput); + if (!IsValid(APIInput)) + return false; + + OutInput = APIInput; + return bSuccessfullyCopied; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetInputsAtIndices_Implementation(const TMap& InInputs) +{ + bool bAnyFailures = false; + for (const TPair& Entry : InInputs) + { + if (!SetInputAtIndex(Entry.Key, Entry.Value)) + { + SetErrorMessage(FString::Printf( + TEXT("SetInputsAtIndices: Failed to set node input at index %d"), Entry.Key)); + bAnyFailures = true; + } + } + + return !bAnyFailures; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetInputsAtIndices_Implementation(TMap& OutInputs) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + bool bAnyFailures = false; + const int32 NumInputs = HC->GetNumInputs(); + for (int32 Index = 0; Index < NumInputs; ++Index) + { + UHoudiniInput const* const HoudiniInput = HC->GetInputAt(Index); + if (!IsValid(HoudiniInput) || HoudiniInput->IsObjectPathParameter()) + continue; + + UHoudiniPublicAPIInput* APIInput = nullptr; + const bool bSuccessfullyCopied = CreateAndPopulateAPIInput(HoudiniInput, APIInput); + if (!IsValid(APIInput)) + { + bAnyFailures = true; + continue; + } + if (!bSuccessfullyCopied) + bAnyFailures = true; + + OutInputs.Add(HoudiniInput->GetInputIndex(), APIInput); + } + + return !bAnyFailures; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetInputParameter_Implementation(const FName& InParameterName, const UHoudiniPublicAPIInput* InInput) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + UHoudiniInput* HoudiniInput = FindValidHoudiniNodeInputParameter(InParameterName); + if (!IsValid(HoudiniInput)) + { + SetErrorMessage(FString::Printf( + TEXT("SetInputParameter: Could not find a parameter-based HoudiniInput with name %s. Has the HDA been instantiated?"), + *(InParameterName.ToString()))); + return false; + } + + const bool bSuccess = PopulateHoudiniInput(InInput, HoudiniInput); + + // Update the details panel (mostly for when new curves/components are created where visualizers are driven + // through the details panel) + FHoudiniEngineEditorUtils::ReselectComponentOwnerIfSelected(HC->GetComponent()); + + return bSuccess; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetInputParameter_Implementation(const FName& InParameterName, UHoudiniPublicAPIInput*& OutInput) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + const UHoudiniInput* HoudiniInput = FindValidHoudiniNodeInputParameter(InParameterName); + if (!IsValid(HoudiniInput)) + { + SetErrorMessage(FString::Printf( + TEXT("GetInputParameter: Could not find a parameter-based HoudiniInput with name %s. Has the HDA been instantiated?"), + *(InParameterName.ToString()))); + return false; + } + + UHoudiniPublicAPIInput* APIInput = nullptr; + const bool bSuccessfullyCopied = CreateAndPopulateAPIInput(HoudiniInput, APIInput); + if (!IsValid(APIInput)) + return false; + + OutInput = APIInput; + return bSuccessfullyCopied; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetInputParameters_Implementation(const TMap& InInputs) +{ + bool bAnyFailures = false; + for (const TPair& Entry : InInputs) + { + if (!SetInputParameter(Entry.Key, Entry.Value)) + { + SetErrorMessage(FString::Printf( + TEXT("SetInputParameters: Failed to set input parameter %s"), *(Entry.Key.ToString()))); + bAnyFailures = true; + } + } + + return !bAnyFailures; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetInputParameters_Implementation(TMap& OutInputs) +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + bool bAnyFailures = false; + const int32 NumInputs = HC->GetNumInputs(); + for (int32 Index = 0; Index < NumInputs; ++Index) + { + UHoudiniInput const* const HoudiniInput = HC->GetInputAt(Index); + if (!IsValid(HoudiniInput) || !HoudiniInput->IsObjectPathParameter()) + continue; + + UHoudiniPublicAPIInput* APIInput = nullptr; + const bool bSuccessfullyCopied = CreateAndPopulateAPIInput(HoudiniInput, APIInput); + if (!IsValid(APIInput)) + { + bAnyFailures = true; + continue; + } + if (!bSuccessfullyCopied) + bAnyFailures = true; + + OutInputs.Add(FName(HoudiniInput->GetInputName()), APIInput); + } + + return !bAnyFailures; +} + +int32 +UHoudiniPublicAPIAssetWrapper::GetNumOutputs_Implementation() const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return -1; + + return HC->GetNumOutputs(); +} + +EHoudiniOutputType +UHoudiniPublicAPIAssetWrapper::GetOutputTypeAt_Implementation(const int32 InIndex) const +{ + UHoudiniOutput* Output = nullptr; + if (!GetValidOutputAtWithError(InIndex, Output)) + return EHoudiniOutputType::Invalid; + + return Output->GetType(); +} + +bool +UHoudiniPublicAPIAssetWrapper::GetOutputIdentifiersAt_Implementation(const int32 InIndex, TArray& OutIdentifiers) const +{ + UHoudiniOutput* Output = nullptr; + if (!GetValidOutputAtWithError(InIndex, Output)) + return false; + + const TMap& OutputObjects = Output->GetOutputObjects(); + OutIdentifiers.Empty(); + OutIdentifiers.Reserve(OutputObjects.Num()); + for (const TPair& Entry : OutputObjects) + { + OutIdentifiers.Add(FHoudiniPublicAPIOutputObjectIdentifier(Entry.Key)); + } + + return true; +} + +UObject* +UHoudiniPublicAPIAssetWrapper::GetOutputObjectAt_Implementation(const int32 InIndex, const FHoudiniPublicAPIOutputObjectIdentifier& InIdentifier) const +{ + UHoudiniOutput* Output = nullptr; + if (!GetValidOutputAtWithError(InIndex, Output)) + return nullptr; + + const TMap& OutputObjects = Output->GetOutputObjects(); + FHoudiniOutputObject const* const OutputObject = OutputObjects.Find(InIdentifier.GetIdentifier()); + if (!OutputObject) + return nullptr; + + return OutputObject->bProxyIsCurrent ? OutputObject->ProxyObject : OutputObject->OutputObject; +} + +UObject* +UHoudiniPublicAPIAssetWrapper::GetOutputComponentAt_Implementation(const int32 InIndex, const FHoudiniPublicAPIOutputObjectIdentifier& InIdentifier) const +{ + UHoudiniOutput* Output = nullptr; + if (!GetValidOutputAtWithError(InIndex, Output)) + return nullptr; + + const TMap& OutputObjects = Output->GetOutputObjects(); + FHoudiniOutputObject const* const OutputObject = OutputObjects.Find(InIdentifier.GetIdentifier()); + if (!OutputObject) + return nullptr; + + if (OutputObject->bProxyIsCurrent) { + return OutputObject->ProxyComponent; + } else { + return OutputObject->OutputComponents.Num() > 0 ? OutputObject->OutputComponents[0] : nullptr; + } +} + +bool +UHoudiniPublicAPIAssetWrapper::GetOutputBakeNameFallbackAt_Implementation(const int32 InIndex, const FHoudiniPublicAPIOutputObjectIdentifier& InIdentifier, FString& OutBakeName) const +{ + UHoudiniOutput* Output = nullptr; + if (!GetValidOutputAtWithError(InIndex, Output)) + return false; + + const TMap& OutputObjects = Output->GetOutputObjects(); + FHoudiniOutputObject const* const OutputObject =OutputObjects.Find(InIdentifier.GetIdentifier()); + if (!OutputObject) + return false; + + OutBakeName = OutputObject->BakeName; + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetOutputBakeNameFallbackAt_Implementation(const int32 InIndex, const FHoudiniPublicAPIOutputObjectIdentifier& InIdentifier, const FString& InBakeName) +{ + UHoudiniOutput* Output = nullptr; + if (!GetValidOutputAtWithError(InIndex, Output)) + return false; + + TMap& OutputObjects = Output->GetOutputObjects(); + FHoudiniOutputObject* const OutputObject = OutputObjects.Find(InIdentifier.GetIdentifier()); + if (!OutputObject) + return false; + + OutputObject->BakeName = InBakeName; + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::BakeOutputObjectAt_Implementation( + const int32 InIndex, + const FHoudiniPublicAPIOutputObjectIdentifier& InIdentifier, + const FName InBakeName, + const EHoudiniLandscapeOutputBakeType InLandscapeBakeType) +{ + UHoudiniOutput* Output = nullptr; + if (!GetValidOutputAtWithError(InIndex, Output)) + return false; + + TMap& OutputObjects = Output->GetOutputObjects(); + const FHoudiniOutputObjectIdentifier& Identifier = InIdentifier.GetIdentifier(); + FHoudiniOutputObject* const OutputObject = OutputObjects.Find(Identifier); + if (!OutputObject) + { + SetErrorMessage(FString::Printf( + TEXT("BakeOutputObjectAt: Could not find an output object using the specified identifier."))); + return false; + } + + // Determine the object to bake (this is different depending on landscape, curve or mesh + const EHoudiniOutputType OutputType = Output->GetType(); + + if (OutputType == EHoudiniOutputType::Mesh && OutputObject->bProxyIsCurrent) + { + // Output is currently a proxy, this cannot be baked without cooking first. + SetErrorMessage(FString::Printf( + TEXT("BakeOutputObjectAt: Object is a proxy mesh, please refine it before baking to CB."))); + + return false; + } + + UObject* ObjectToBake = nullptr; + switch (OutputType) + { + case EHoudiniOutputType::Landscape: + { + UHoudiniLandscapePtr* const LandscapePtr = Cast(OutputObject->OutputObject); + if (IsValid(LandscapePtr)) + { + ObjectToBake = LandscapePtr->LandscapeSoftPtr.IsValid() ? LandscapePtr->LandscapeSoftPtr.Get() : nullptr; + } + break; + } + case EHoudiniOutputType::Curve: + ObjectToBake = OutputObject->OutputComponents.Num() > 0 ? OutputObject->OutputComponents[0] : nullptr; + break; + case EHoudiniOutputType::Mesh: + ObjectToBake = OutputObject->OutputObject; + break; + case EHoudiniOutputType::Instancer: + case EHoudiniOutputType::Skeletal: + case EHoudiniOutputType::LandscapeSpline: + case EHoudiniOutputType::Invalid: + default: + SetErrorMessage(FString::Printf( + TEXT("BakeOutputObjectAt: unsupported output type (%d) for baking to CB."), OutputType)); + return false; + } + + if (!IsValid(ObjectToBake)) + { + SetErrorMessage(FString::Printf(TEXT("BakeOutputObjectAt: Could not find a valid object to bake to CB."))); + return false; + } + + // Find the corresponding HGPO in the output + FHoudiniGeoPartObject HoudiniGeoPartObject; + for (const auto& HGPO : Output->GetHoudiniGeoPartObjects()) + { + if (!Identifier.Matches(HGPO)) + continue; + + HoudiniGeoPartObject = HGPO; + break; + } + + if (!HoudiniGeoPartObject.IsValid()) + { + SetErrorMessage(TEXT("BakeOutputObjectAt: Could not find a valid HGPO for the output object. Please recook.")); + return false; + } + + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + TArray AllOutputs; + HC->GetOutputs(AllOutputs); + + FHoudiniBakeSettings BakeSettings; + BakeSettings.SetFromCookable(HC); + + FHoudiniOutputDetails::OnBakeOutputObject( + InBakeName.IsNone() ? OutputObject->BakeName : InBakeName.ToString(), + ObjectToBake, + Identifier, + *OutputObject, + HoudiniGeoPartObject, + HC, + Output, + HC->GetBakeFolderOrDefault(), + BakeSettings, + HC->GetTemporaryCookFolderOrDefault(), + InLandscapeBakeType, + AllOutputs); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::HasAnyCurrentProxyOutput_Implementation() const +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return false; + + return HC->HasAnyCurrentProxyOutput(); +} + +bool +UHoudiniPublicAPIAssetWrapper::HasAnyCurrentProxyOutputAt_Implementation(const int32 InIndex) const +{ + UHoudiniOutput* Output = nullptr; + if (!GetValidOutputAtWithError(InIndex, Output)) + return false; + + return Output->HasAnyCurrentProxy(); +} + +bool +UHoudiniPublicAPIAssetWrapper::IsOutputCurrentProxyAt_Implementation(const int32 InIndex, const FHoudiniPublicAPIOutputObjectIdentifier& InIdentifier) const +{ + UHoudiniOutput* Output = nullptr; + if (!GetValidOutputAtWithError(InIndex, Output)) + return false; + + return Output->IsProxyCurrent(InIdentifier.GetIdentifier()); +} + +EHoudiniProxyRefineRequestResult +UHoudiniPublicAPIAssetWrapper::RefineAllCurrentProxyOutputs_Implementation(const bool bInSilent) +{ + AHoudiniAssetActor* AssetActor = nullptr; + if (!GetValidHoudiniAssetActorWithError(AssetActor)) + return EHoudiniProxyRefineRequestResult::Invalid; + + return FHoudiniEngineCommands::RefineHoudiniProxyMeshActorArrayToStaticMeshes({ AssetActor }, bInSilent); +} + +bool +UHoudiniPublicAPIAssetWrapper::HasPDGAssetLink_Implementation() const +{ + return IsValid(GetHoudiniPDGAssetLink()); +} + +bool +UHoudiniPublicAPIAssetWrapper::GetPDGTOPNetworkPaths_Implementation(TArray& OutTOPNetworkPaths) const +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + const uint32 NumNetworks = AssetLink->AllTOPNetworks.Num(); + OutTOPNetworkPaths.Empty(NumNetworks); + for (UTOPNetwork const* const TOPNet : AssetLink->AllTOPNetworks) + { + OutTOPNetworkPaths.Add(TOPNet->NodePath); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetPDGTOPNodePaths_Implementation(const FString& InNetworkRelativePath, TArray& OutTOPNodePaths) const +{ + int32 NetworkIndex = INDEX_NONE; + UTOPNetwork* TOPNet = nullptr; + if (!GetValidTOPNetworkByPathWithError(InNetworkRelativePath, NetworkIndex, TOPNet)) + return false; + + const uint32 NumNodes = TOPNet->AllTOPNodes.Num(); + OutTOPNodePaths.Empty(NumNodes); + for (UTOPNode const* const TOPNode : TOPNet->AllTOPNodes) + { + OutTOPNodePaths.Add(TOPNode->NodePath); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::PDGDirtyAllNetworks_Implementation() +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + for (UTOPNetwork* const TOPNetwork : AssetLink->AllTOPNetworks) + { + if (!IsValid(TOPNetwork)) + continue; + + FHoudiniPDGManager::DirtyAll(TOPNetwork); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::PDGDirtyNetwork_Implementation(const FString& InNetworkRelativePath) +{ + int32 NetworkIndex = INDEX_NONE; + UTOPNetwork* TOPNet = nullptr; + if (!GetValidTOPNetworkByPathWithError(InNetworkRelativePath, NetworkIndex, TOPNet)) + return false; + + FHoudiniPDGManager::DirtyAll(TOPNet); + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::PDGDirtyNode_Implementation(const FString& InNetworkRelativePath, const FString& InNodeRelativePath) +{ + int32 NetworkIndex = INDEX_NONE; + int32 NodeIndex = INDEX_NONE; + UTOPNode* TOPNode = nullptr; + if (!GetValidTOPNodeByPathWithError(InNetworkRelativePath, InNodeRelativePath, NetworkIndex, NodeIndex, TOPNode)) + return false; + + FHoudiniPDGManager::DirtyTOPNode(TOPNode); + + return true; +} + +// bool +// UHoudiniPublicAPIAssetWrapper::PDGCookOutputsForAllNetworks_Implementation() +// { +// UHoudiniPDGAssetLink* const AssetLink = GetHoudiniPDGAssetLink(); +// if (!IsValid(AssetLink)) +// return false; +// +// for (UTOPNetwork* const TOPNetwork : AssetLink->AllTOPNetworks) +// { +// if (!IsValid(TOPNetwork)) +// continue; +// +// FHoudiniPDGManager::CookOutput(TOPNetwork); +// } +// +// return true; +// } + +bool +UHoudiniPublicAPIAssetWrapper::PDGCookOutputsForNetwork_Implementation(const FString& InNetworkRelativePath) +{ + int32 NetworkIndex = INDEX_NONE; + UTOPNetwork* TOPNet = nullptr; + if (!GetValidTOPNetworkByPathWithError(InNetworkRelativePath, NetworkIndex, TOPNet)) + return false; + + return FHoudiniPDGManager::CookOutput(TOPNet); +} + +bool +UHoudiniPublicAPIAssetWrapper::PDGCookNode_Implementation(const FString& InNetworkRelativePath, const FString& InNodeRelativePath) +{ + int32 NetworkIndex = INDEX_NONE; + int32 NodeIndex = INDEX_NONE; + UTOPNode* TOPNode = nullptr; + if (!GetValidTOPNodeByPathWithError(InNetworkRelativePath, InNodeRelativePath, NetworkIndex, NodeIndex, TOPNode)) + return false; + + return FHoudiniPDGManager::CookTOPNode(TOPNode); +} + +bool +UHoudiniPublicAPIAssetWrapper::PDGBakeAllOutputs_Implementation() +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + return PDGBakeAllOutputsWithSettings( + AssetLink->HoudiniEngineBakeOption, + AssetLink->PDGBakeSelectionOption, + AssetLink->PDGBakePackageReplaceMode, + AssetLink->bRecenterBakedActors); +} + +bool +UHoudiniPublicAPIAssetWrapper::PDGBakeAllOutputsWithSettings_Implementation( + const EHoudiniEngineBakeOption InBakeOption, + const EPDGBakeSelectionOption InBakeSelection, + const EPDGBakePackageReplaceModeOption InBakeReplacementMode, + const bool bInRecenterBakedActors) +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + bool bBakeSuccess = false; + switch (InBakeOption) + { + case EHoudiniEngineBakeOption::ToActor: + bBakeSuccess = FHoudiniEngineBakeUtils::BakePDGAssetLinkOutputsKeepActors( + AssetLink, + InBakeSelection, + InBakeReplacementMode, + bInRecenterBakedActors); + break; + case EHoudiniEngineBakeOption::ToBlueprint: + bBakeSuccess = FHoudiniEngineBakeUtils::BakePDGAssetLinkBlueprints( + AssetLink, + InBakeSelection, + InBakeReplacementMode, + bInRecenterBakedActors); + break; + default: + bBakeSuccess = false; + break; + } + + return bBakeSuccess; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetPDGAutoBakeEnabled_Implementation(const bool bInAutoBakeEnabled) +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + if (AssetLink->bBakeAfterAllWorkResultObjectsLoaded != bInAutoBakeEnabled) + { + AssetLink->bBakeAfterAllWorkResultObjectsLoaded = bInAutoBakeEnabled; + if(AssetLink->GetOuter()) + AssetLink->GetOuter()->Modify(); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::IsPDGAutoBakeEnabled_Implementation() const +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + return AssetLink->bBakeAfterAllWorkResultObjectsLoaded; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetPDGAutoBakeNodesWithFailedWorkItemsEnabled_Implementation(const bool bInEnabled) +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + if (AssetLink->IsAutoBakeNodesWithFailedWorkItemsEnabled() != bInEnabled) + { + AssetLink->SetAutoBakeNodesWithFailedWorkItemsEnabled(bInEnabled); + if(AssetLink->GetOuter()) + AssetLink->GetOuter()->Modify(); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::IsPDGAutoBakeNodesWithFailedWorkItemsEnabled_Implementation() const +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + return AssetLink->IsAutoBakeNodesWithFailedWorkItemsEnabled(); +} + +bool +UHoudiniPublicAPIAssetWrapper::SetPDGBakeMethod_Implementation(const EHoudiniEngineBakeOption InBakeMethod) +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + if (AssetLink->HoudiniEngineBakeOption != InBakeMethod) + { + AssetLink->HoudiniEngineBakeOption = InBakeMethod; + if(AssetLink->GetOuter()) + AssetLink->GetOuter()->Modify(); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetPDGBakeMethod_Implementation(EHoudiniEngineBakeOption& OutBakeMethod) +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + OutBakeMethod = AssetLink->HoudiniEngineBakeOption; + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetPDGBakeSelection_Implementation(const EPDGBakeSelectionOption InBakeSelection) +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + if (AssetLink->PDGBakeSelectionOption != InBakeSelection) + { + AssetLink->PDGBakeSelectionOption = InBakeSelection; + if(AssetLink->GetOuter()) + AssetLink->GetOuter()->Modify(); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetPDGBakeSelection_Implementation(EPDGBakeSelectionOption& OutBakeSelection) +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + OutBakeSelection = AssetLink->PDGBakeSelectionOption; + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetPDGRecenterBakedActors_Implementation(const bool bInRecenterBakedActors) +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + if (AssetLink->bRecenterBakedActors != bInRecenterBakedActors) + { + AssetLink->bRecenterBakedActors = bInRecenterBakedActors; + if(AssetLink->GetOuter()) + AssetLink->GetOuter()->Modify(); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetPDGRecenterBakedActors_Implementation() const +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + return AssetLink->bRecenterBakedActors; +} + +bool +UHoudiniPublicAPIAssetWrapper::SetPDGBakingReplacementMode_Implementation(const EPDGBakePackageReplaceModeOption InBakingReplacementMode) +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + if (AssetLink->PDGBakePackageReplaceMode != InBakingReplacementMode) + { + AssetLink->PDGBakePackageReplaceMode = InBakingReplacementMode; + if(AssetLink->GetOuter()) + AssetLink->GetOuter()->Modify(); + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetPDGBakingReplacementMode_Implementation(EPDGBakePackageReplaceModeOption& OutBakingReplacementMode) const +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + OutBakingReplacementMode = AssetLink->PDGBakePackageReplaceMode; + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::BindToPDGAssetLink() +{ + if (bAssetLinkSetupAttemptComplete) + return true; + + UHoudiniPDGAssetLink* const PDGAssetLink = GetHoudiniPDGAssetLink(); + if (IsValid(PDGAssetLink)) + { + OnPDGPostTOPNetworkCookDelegateHandle = PDGAssetLink->GetOnPostTOPNetworkCookDelegate().AddUFunction(this, TEXT("HandleOnHoudiniPDGAssetLinkTOPNetPostCook")); + OnPDGPostBakeDelegateHandle = PDGAssetLink->GetOnPostBakeDelegate().AddUFunction(this, TEXT("HandleOnHoudiniPDGAssetLinkPostBake")); + bAssetLinkSetupAttemptComplete = true; + + return true; + } + + return false; +} + +void +UHoudiniPublicAPIAssetWrapper::HandleOnHoudiniCookableStateChange(UHoudiniCookable* InHC, const EHoudiniAssetState InFromState, const EHoudiniAssetState InToState) +{ + if (!IsValid(InHC)) + return; + + // PDG link setup / bindings. Check if its bound, if not do it now, + if (!bAssetLinkSetupAttemptComplete) + BindToPDGAssetLink(); + + if (InHC != GetHoudiniCookable()) + { + SetErrorMessage(FString::Printf( + TEXT("HandleOnHoudiniCookableStateChange: unexpected InHC: %s, expected the wrapper's HC."), + IsValid(InHC) ? *InHC->GetName() : TEXT(""))); + return; + } + + if (InToState == EHoudiniAssetState::PreInstantiation) + { + if (OnPreInstantiationDelegate.IsBound()) + OnPreInstantiationDelegate.Broadcast(this); + } + + if (InFromState == EHoudiniAssetState::Instantiating && InToState == EHoudiniAssetState::PreCook) + { + if (OnPostInstantiationDelegate.IsBound()) + OnPostInstantiationDelegate.Broadcast(this); + } + + if (InFromState == EHoudiniAssetState::PreProcess) + { + if (OnPreProcessStateExitedDelegate.IsBound()) + OnPreProcessStateExitedDelegate.Broadcast(this); + } + + if (InFromState == EHoudiniAssetState::Processing && InToState == EHoudiniAssetState::None) + { + if (OnPostProcessingDelegate.IsBound()) + OnPostProcessingDelegate.Broadcast(this); + } +} + +void +UHoudiniPublicAPIAssetWrapper::HandleOnHoudiniCookablePostCook(UHoudiniCookable* InHC, const bool bInCookSuccess) +{ + if (!IsValid(InHC)) + return; + + if (InHC != GetHoudiniCookable()) + { + SetErrorMessage(FString::Printf( + TEXT("HandleOnHoudiniCookableStateChange: unexpected InHC: %s, expected the wrapper's HC."), + IsValid(InHC) ? *InHC->GetName() : TEXT(""))); + return; + } + + if (OnPostCookDelegate.IsBound()) + OnPostCookDelegate.Broadcast(this, bInCookSuccess); +} + +void +UHoudiniPublicAPIAssetWrapper::HandleOnHoudiniCookablePostBake(UHoudiniCookable* InHC, const bool bInBakeSuccess) +{ + if (!IsValid(InHC)) + return; + + if (InHC != GetHoudiniCookable()) + { + SetErrorMessage(FString::Printf( + TEXT("HandleOnHoudiniCookableStateChange: unexpected InHC: %s, expected the wrapper's HC."), + IsValid(InHC) ? *InHC->GetName() : TEXT(""))); + return; + } + + if (OnPostBakeDelegate.IsBound()) + OnPostBakeDelegate.Broadcast(this, bInBakeSuccess); +} + +void +UHoudiniPublicAPIAssetWrapper::HandleOnHoudiniPDGAssetLinkTOPNetPostCook(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNetwork* InTOPNet, const bool bInAnyWorkItemsFailed) +{ + if (!IsValid(InPDGAssetLink)) + return; + + if (InPDGAssetLink != GetHoudiniPDGAssetLink()) + { + SetErrorMessage(FString::Printf( + TEXT("HandleOnHoudiniPDGAssetLinkTOPNetPostCook: unexpected InPDGAssetLink: %s, expected the wrapper's PDGAssetLink."), + IsValid(InPDGAssetLink) ? *InPDGAssetLink->GetName() : TEXT(""))); + return; + } + + if (OnPostPDGTOPNetworkCookDelegate.IsBound()) + OnPostPDGTOPNetworkCookDelegate.Broadcast(this, !bInAnyWorkItemsFailed); +} + +void +UHoudiniPublicAPIAssetWrapper::HandleOnHoudiniPDGAssetLinkPostBake(UHoudiniPDGAssetLink* InPDGAssetLink, const bool bInBakeSuccess) +{ + if (!IsValid(InPDGAssetLink)) + return; + + if (InPDGAssetLink != GetHoudiniPDGAssetLink()) + { + SetErrorMessage(FString::Printf( + TEXT("HandleOnHoudiniPDGAssetLinkPostBake: unexpected InPDGAssetLink: %s, expected the wrapper's PDGAssetLink."), + IsValid(InPDGAssetLink) ? *InPDGAssetLink->GetName() : TEXT(""))); + return; + } + + if (OnPostPDGBakeDelegate.IsBound()) + OnPostPDGBakeDelegate.Broadcast(this, bInBakeSuccess); +} + +void +UHoudiniPublicAPIAssetWrapper::HandleOnHoudiniProxyMeshesRefinedGlobal(UHoudiniCookable* InHC, const EHoudiniProxyRefineResult InResult) +{ + if (!IsValid(InHC)) + return; + + if (InHC != GetHoudiniCookable()) + return; + + if (OnProxyMeshesRefinedDelegate.IsBound()) + OnProxyMeshesRefinedDelegate.Broadcast(this, InResult); +} + +UHoudiniParameter* +UHoudiniPublicAPIAssetWrapper::FindValidParameterByName(const FName& InParameterTupleName) const +{ + AActor* const Actor = GetHoudiniAssetActor(); + const FString ActorName = IsValid(Actor) ? Actor->GetActorNameOrLabel() : FString(); + + UHoudiniCookable* const HC = GetHoudiniCookable(); + if (!IsValid(HC)) + { + SetErrorMessage(FString::Printf(TEXT("Could not find Cookable on Actor '%s'"), *ActorName)); + return nullptr; + } + + UHoudiniParameter* const Param = HC->FindParameterByName(InParameterTupleName.ToString()); + if (!IsValid(Param)) + { + SetErrorMessage(FString::Printf( + TEXT("Could not find valid parameter tuple '%s' on '%s'."), + *InParameterTupleName.ToString(), *ActorName)); + return nullptr; + } + + return Param; +} + +bool +UHoudiniPublicAPIAssetWrapper::FindRampPointData( + UHoudiniParameter* const InParam, + const int32 InIndex, + TArray>& OutPointData) const +{ + if (!IsValid(InParam)) + return false; + + // If the parameter is not set to auto update, or if cooking is paused, we have to set the cached points and + // not the main points. + // const bool bCookingEnabled = !FHoudiniEngineCommands::IsAssetCookingPaused(); + // const bool bUseCachedPoints = (!InParam->IsAutoUpdate() || !bCookingEnabled); + const bool bUseCachedPoints = !InParam->IsAutoUpdate(); + + const bool bFetchAllPoints = (InIndex == INDEX_NONE); + + UHoudiniParameterRampFloat* FloatRampParam = nullptr; + UHoudiniParameterRampColor* ColorRampParam = nullptr; + + // Handle all the cases where the underlying parameter value is an int or bool + const EHoudiniParameterType ParamType = InParam->GetParameterType(); + if (ParamType == EHoudiniParameterType::FloatRamp) + { + FloatRampParam = Cast(InParam); + if (!IsValid(FloatRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *InParam->GetClass()->GetName(), ParamType)); + return false; + } + } + else if (ParamType == EHoudiniParameterType::ColorRamp) + { + ColorRampParam = Cast(InParam); + if (!IsValid(ColorRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *InParam->GetClass()->GetName(), ParamType)); + return false; + } + } + else + { + SetErrorMessage(FString::Printf( + TEXT("Parameter '%s' is not a float or color ramp parameter."), *(InParam->GetName()))); + return false; + } + + if (bUseCachedPoints) + { + // When using the cached points we only have to resize the array + if (FloatRampParam) + { + if (!bFetchAllPoints && !FloatRampParam->CachedPoints.IsValidIndex(InIndex)) + { + SetErrorMessage(FString::Printf( + TEXT("Ramp point index %d is out of range [0, %d]."), InIndex, FloatRampParam->CachedPoints.Num() - 1)); + return false; + } + + if (bFetchAllPoints) + { + // Get all points + OutPointData.Reserve(FloatRampParam->CachedPoints.Num()); + const bool bAllowShrinking = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + OutPointData.SetNum(0, bAllowShrinking ? EAllowShrinking::Yes : EAllowShrinking::No); +#else + OutPointData.SetNum(0, bAllowShrinking); +#endif + for (UHoudiniParameterRampFloatPoint* const RampPoint : FloatRampParam->CachedPoints) + { + const bool bIsPointData = true; + OutPointData.Add(TPair(RampPoint, bIsPointData)); + } + } + else + { + OutPointData.Reserve(1); + const bool bAllowShrinking = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + OutPointData.SetNum(0, bAllowShrinking ? EAllowShrinking::Yes : EAllowShrinking::No); +#else + OutPointData.SetNum(0, bAllowShrinking); +#endif + const bool bIsPointData = true; + OutPointData.Add(TPair(FloatRampParam->CachedPoints[InIndex], bIsPointData)); + } + + return true; + } + else + { + if (!bFetchAllPoints && !ColorRampParam->CachedPoints.IsValidIndex(InIndex)) + { + SetErrorMessage(FString::Printf( + TEXT("Ramp point index %d is out of range [0, %d]."), InIndex, ColorRampParam->CachedPoints.Num() - 1)); + return false; + } + + if (bFetchAllPoints) + { + // Get all points + OutPointData.Reserve(ColorRampParam->CachedPoints.Num()); + const bool bAllowShrinking = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + OutPointData.SetNum(0, bAllowShrinking ? EAllowShrinking::Yes : EAllowShrinking::No); +#else + OutPointData.SetNum(0, bAllowShrinking); +#endif + for (UHoudiniParameterRampColorPoint* const RampPoint : ColorRampParam->CachedPoints) + { + const bool bIsPointData = true; + OutPointData.Add(TPair(RampPoint, bIsPointData)); + } + } + else + { + OutPointData.Reserve(1); + const bool bAllowShrinking = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + OutPointData.SetNum(0, bAllowShrinking ? EAllowShrinking::Yes : EAllowShrinking::No); +#else + OutPointData.SetNum(0, bAllowShrinking); +#endif + + const bool bIsPointData = true; + OutPointData.Add(TPair(ColorRampParam->CachedPoints[InIndex], bIsPointData)); + } + + return true; + } + } + else + { + TSet InstanceIndexesPendingDelete; + int32 NumInsertOps = 0; + TArray>& ModificationEvents = FloatRampParam ? FloatRampParam->ModificationEvents : ColorRampParam->ModificationEvents; + for (UHoudiniParameterRampModificationEvent const* const Event : ModificationEvents) + { + if (!IsValid(Event)) + continue; + + if (Event->IsInsertEvent()) + NumInsertOps++; + else if (Event->IsDeleteEvent()) + InstanceIndexesPendingDelete.Add(Event->DeleteInstanceIndex); + } + + const int32 PointsArraySize = FloatRampParam ? FloatRampParam->Points.Num() : ColorRampParam->Points.Num(); + const int32 NumActivePointsInArray = PointsArraySize - InstanceIndexesPendingDelete.Num(); + const int32 TotalNumPoints = NumActivePointsInArray + NumInsertOps; + + // Reserve the expected amount of space needed in the array and reset / destruct existing items so we can + // add from index 0 + if (bFetchAllPoints) + OutPointData.Reserve(TotalNumPoints); + else + OutPointData.Reserve(1); + const bool bAllowShrinking = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + OutPointData.SetNum(0, bAllowShrinking ? EAllowShrinking::Yes : EAllowShrinking::No); +#else + OutPointData.SetNum(0, bAllowShrinking); +#endif + + if (bFetchAllPoints || InIndex < NumActivePointsInArray) + { + // Getting all points or point is in the points array + if (FloatRampParam) + { + const int32 ArraySize = FloatRampParam->Points.Num(); + int32 PointIndex = 0; + for (int32 Index = 0; Index < ArraySize && (bFetchAllPoints || PointIndex <= InIndex); ++Index) + { + UHoudiniParameterRampFloatPoint* const PointData = FloatRampParam->Points[Index]; + const bool bIsDeletedPoint = (PointData && InstanceIndexesPendingDelete.Contains(PointData->InstanceIndex)); + if (!bIsDeletedPoint) + { + if (PointIndex == InIndex || bFetchAllPoints) + { + const bool bIsPointData = true; + OutPointData.Add(TPair(PointData, bIsPointData)); + } + + // If we are fetching only the point at InIndex, then we are done here + if (PointIndex == InIndex) + return true; + + PointIndex++; + } + } + } + else + { + const int32 ArraySize = ColorRampParam->Points.Num(); + int32 PointIndex = 0; + for (int32 Index = 0; Index < ArraySize && (bFetchAllPoints || PointIndex <= InIndex); ++Index) + { + UHoudiniParameterRampColorPoint* const PointData = ColorRampParam->Points[Index]; + const bool bIsDeletedPoint = (PointData && InstanceIndexesPendingDelete.Contains(PointData->InstanceIndex)); + if (!bIsDeletedPoint) + { + if (PointIndex == InIndex || bFetchAllPoints) + { + const bool bIsPointData = true; + OutPointData.Add(TPair(PointData, bIsPointData)); + } + + // If we are fetching only the point at InIndex, then we are done here + if (PointIndex == InIndex) + return true; + + PointIndex++; + } + } + } + } + + if (bFetchAllPoints || InIndex < TotalNumPoints) + { + // Point is an insert operation + const int32 NumEvents = ModificationEvents.Num(); + int32 PointIndex = NumActivePointsInArray; + for (int32 Index = 0; Index < NumEvents && (bFetchAllPoints || PointIndex <= InIndex); ++Index) + { + UHoudiniParameterRampModificationEvent* const Event = ModificationEvents[Index]; + if (!IsValid(Event)) + continue; + + if (!Event->IsInsertEvent()) + continue; + + if (PointIndex == InIndex || bFetchAllPoints) + { + const bool bIsPointData = false; + OutPointData.Add(TPair(Event, bIsPointData)); + } + + if (PointIndex == InIndex) + return true; + + PointIndex++; + } + } + else + { + // Point is out of range + SetErrorMessage(FString::Printf( + TEXT("Ramp point index %d is out of range [0, %d]."), InIndex, TotalNumPoints)); + return false; + } + + if (bFetchAllPoints) + { + if (TotalNumPoints != OutPointData.Num()) + { + SetErrorMessage(FString::Printf( + TEXT("Failed to fetch all ramp points. Got %d, expected %d."), OutPointData.Num(), TotalNumPoints)); + return false; + } + + return true; + } + } + + // If we reach this point we didn't find the point + SetErrorMessage(FString::Printf(TEXT("Could not find valid ramp point at index %d."), InIndex)); + return false; +} + +bool UHoudiniPublicAPIAssetWrapper::SetRampParameterPointValue( + FName InParameterTupleName, + const int32 InPointIndex, + const float InPosition, + const float InFloatValue, + const FLinearColor& InColorValue, + const EHoudiniPublicAPIRampInterpolationType InInterpolation, + const bool bInMarkChanged) +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + UHoudiniParameterRampFloat* FloatRampParam = nullptr; + UHoudiniParameterRampColor* ColorRampParam = nullptr; + + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::FloatRamp) + { + FloatRampParam = Cast(Param); + if (!IsValid(FloatRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + } + else if (ParamType == EHoudiniParameterType::ColorRamp) + { + ColorRampParam = Cast(Param); + if (!IsValid(ColorRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + } + else + { + SetErrorMessage(FString::Printf(TEXT("Parameter '%s' is not a float ramp or color ramp parameter."), *(Param->GetName()))); + return false; + } + + const EHoudiniRampInterpolationType NewInterpolation = UHoudiniPublicAPI::ToHoudiniRampInterpolationType( + InInterpolation); + + // If the parameter is not set to auto update, or if cooking is paused, we have to set the cached points and + // not the main points. + // const bool bCookingEnabled = !FHoudiniEngineCommands::IsAssetCookingPaused(); + // const bool bUseCachedPoints = (!Param->IsAutoUpdate() || !bCookingEnabled); + const bool bUseCachedPoints = !Param->IsAutoUpdate(); + + // Get the point at InPointIndex's data + TArray> RampPointData; + if (!FindRampPointData(Param, InPointIndex, RampPointData) || RampPointData.Num() < 1) + return false; + + UObject* const PointData = RampPointData[0].Key; + const bool bIsPointData = RampPointData[0].Value; + if (!IsValid(PointData)) + return false; + + if (bIsPointData) + { + UHoudiniParameterRampFloatPoint* FloatPointData = nullptr; + UHoudiniParameterRampColorPoint* ColorPointData = nullptr; + + if (FloatRampParam) + { + FloatPointData = Cast(PointData); + if (!IsValid(FloatPointData)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampFloatPoint instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + } + else + { + ColorPointData = Cast(PointData); + if (!IsValid(ColorPointData)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampColorPoint instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + } + + if (bUseCachedPoints) + { + // When setting the cached points, we set the values directly instead of using the setters, but we set + // the bCaching flag on the parameter and mark the position/value/interpolation parent parameters as changed + if (FloatPointData) + { + if (FloatPointData->Position != InPosition) + { + FloatPointData->Position = InPosition; + FloatRampParam->bCaching = true; + } + + if (FloatPointData->Value != InFloatValue) + { + FloatPointData->Value = InFloatValue; + FloatRampParam->bCaching = true; + } + + if (FloatPointData->Interpolation != NewInterpolation) + { + FloatPointData->Interpolation = NewInterpolation; + FloatRampParam->bCaching = true; + } + } + else if (ColorPointData && ColorRampParam) + { + if (ColorPointData->Position != InPosition) + { + ColorPointData->Position = InPosition; + ColorRampParam->bCaching = true; + } + + if (ColorPointData->Value != InColorValue) + { + ColorPointData->Value = InColorValue; + ColorRampParam->bCaching = true; + } + + if (ColorPointData->Interpolation != NewInterpolation) + { + ColorPointData->Interpolation = NewInterpolation; + ColorRampParam->bCaching = true; + } + } + + // Update the ramp's widget if it is currently visible/selected + const bool bForceFullUpdate = true; + FHoudiniEngineUtils::UpdateEditorProperties(bForceFullUpdate); + } + else + { + // When setting the main points, we set the values using the setters on the point data but still manually + // mark the position/value/interpolation parent parameters as changed + if (FloatPointData) + { + if (FloatPointData->Position != InPosition && FloatPointData->PositionParentParm) + { + FloatPointData->SetPosition(InPosition); + if (bInMarkChanged) + FloatPointData->PositionParentParm->MarkChanged(bInMarkChanged); + } + + if (FloatPointData->Value != InFloatValue && FloatPointData->ValueParentParm) + { + FloatPointData->SetValue(InFloatValue); + if (bInMarkChanged) + FloatPointData->ValueParentParm->MarkChanged(bInMarkChanged); + } + + if (FloatPointData->Interpolation != NewInterpolation && FloatPointData->InterpolationParentParm) + { + FloatPointData->SetInterpolation(NewInterpolation); + if (bInMarkChanged) + FloatPointData->InterpolationParentParm->MarkChanged(bInMarkChanged); + } + } + else if (ColorPointData) + { + if (ColorPointData->Position != InPosition && ColorPointData->PositionParentParm) + { + ColorPointData->SetPosition(InPosition); + if (bInMarkChanged) + ColorPointData->PositionParentParm->MarkChanged(bInMarkChanged); + } + + if (ColorPointData->Value != InColorValue && ColorPointData->ValueParentParm) + { + ColorPointData->SetValue(InColorValue); + if (bInMarkChanged) + ColorPointData->ValueParentParm->MarkChanged(bInMarkChanged); + } + + if (ColorPointData->Interpolation != NewInterpolation && ColorPointData->InterpolationParentParm) + { + ColorPointData->SetInterpolation(NewInterpolation); + if (bInMarkChanged) + ColorPointData->InterpolationParentParm->MarkChanged(bInMarkChanged); + } + } + } + } + else + { + UHoudiniParameterRampModificationEvent* const Event = Cast(PointData); + if (!IsValid(Event)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampModificationEvent instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + + Event->InsertPosition = InPosition; + if (FloatRampParam) + { + Event->InsertFloat = InFloatValue; + } + else if (ColorRampParam) + { + Event->InsertColor = InColorValue; + } + Event->InsertInterpolation = NewInterpolation; + } + + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetRampParameterPointValue( + FName InParameterTupleName, + const int32 InPointIndex, + float& OutPosition, + float& OutFloatValue, + FLinearColor& OutColorValue, + EHoudiniPublicAPIRampInterpolationType& OutInterpolation) const +{ + UHoudiniParameter* Param = FindValidParameterByName(InParameterTupleName); + if (!Param) + return false; + + UHoudiniParameterRampFloat* FloatRampParam = nullptr; + UHoudiniParameterRampColor* ColorRampParam = nullptr; + + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType == EHoudiniParameterType::FloatRamp) + { + FloatRampParam = Cast(Param); + if (!IsValid(FloatRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + } + else if (ParamType == EHoudiniParameterType::ColorRamp) + { + ColorRampParam = Cast(Param); + if (!IsValid(ColorRampParam)) + { + SetErrorMessage(FString::Printf( + TEXT("Unexpected parameter class (%s) vs type (%d)"), *Param->GetClass()->GetName(), ParamType)); + return false; + } + } + else + { + SetErrorMessage(FString::Printf(TEXT("Parameter '%s' is not a float ramp or color ramp parameter."), *(Param->GetName()))); + return false; + } + + TArray> RampPointData; + if (!FindRampPointData(Param, InPointIndex, RampPointData) || RampPointData.Num() < 1) + return false; + + UObject* const PointData = RampPointData[0].Key; + const bool bIsPointData = RampPointData[0].Value; + if (!IsValid(PointData)) + return false; + + if (bIsPointData) + { + UHoudiniParameterRampFloatPoint* FloatPointData = nullptr; + UHoudiniParameterRampColorPoint* ColorPointData = nullptr; + + if (FloatRampParam) + { + FloatPointData = Cast(PointData); + if (!IsValid(FloatPointData)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampFloatPoint instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + } + else + { + ColorPointData = Cast(PointData); + if (!IsValid(ColorPointData)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampColorPoint instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + } + + // When setting the cached points, we set the values directly instead of using the setters, but we set + // the bCaching flag on the parameter and mark the position/value/interpolation parent parameters as changed + if (FloatPointData) + { + OutPosition = FloatPointData->Position; + OutFloatValue = FloatPointData->Value; + OutInterpolation = UHoudiniPublicAPI::ToHoudiniPublicAPIRampInterpolationType( + FloatPointData->Interpolation); + } + else if (ColorPointData) + { + OutPosition = ColorPointData->Position; + OutColorValue = ColorPointData->Value; + OutInterpolation = UHoudiniPublicAPI::ToHoudiniPublicAPIRampInterpolationType( + ColorPointData->Interpolation); + } + } + else + { + UHoudiniParameterRampModificationEvent* const Event = Cast(PointData); + if (!IsValid(Event)) + { + SetErrorMessage(FString::Printf( + TEXT("Expected UHoudiniParameterRampModificationEvent instance, but received incompatible class '%s'."), + *(PointData->GetClass()->GetName()))); + return false; + } + + OutPosition = Event->InsertPosition; + if (FloatRampParam) + { + OutFloatValue = Event->InsertFloat; + } + else if (ColorRampParam) + { + OutColorValue = Event->InsertColor; + } + OutInterpolation = UHoudiniPublicAPI::ToHoudiniPublicAPIRampInterpolationType(Event->InsertInterpolation); + } + + return true; +} + +UHoudiniInput* +UHoudiniPublicAPIAssetWrapper::GetHoudiniNodeInputByIndex(const int32 InNodeInputIndex) +{ + if (InNodeInputIndex < 0) + return nullptr; + + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return nullptr; + + const int32 NumInputs = HC->GetNumInputs(); + for (int32 Index = 0; Index < NumInputs; ++Index) + { + UHoudiniInput* const Input = HC->GetInputAt(Index); + if (!IsValid(Input)) + continue; + if (Input->GetInputIndex() == InNodeInputIndex) + return Input; + } + + return nullptr; +} + +const UHoudiniInput* +UHoudiniPublicAPIAssetWrapper::GetHoudiniNodeInputByIndex(const int32 InNodeInputIndex) const +{ + if (InNodeInputIndex < 0) + return nullptr; + + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return nullptr; + + const int32 NumInputs = HC->GetNumInputs(); + for (int32 Index = 0; Index < NumInputs; ++Index) + { + UHoudiniInput const* const Input = HC->GetInputAt(Index); + if (!IsValid(Input)) + continue; + if (Input->GetInputIndex() == InNodeInputIndex) + return Input; + } + + return nullptr; +} + +UHoudiniInput* +UHoudiniPublicAPIAssetWrapper::FindValidHoudiniNodeInputParameter(const FName& InInputParameterName) +{ + if (InInputParameterName == NAME_None) + return nullptr; + + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return nullptr; + + const FString InputParameterName = InInputParameterName.ToString(); + const int32 NumInputs = HC->GetNumInputs(); + for (int32 Index = 0; Index < NumInputs; ++Index) + { + UHoudiniInput* const Input = HC->GetInputAt(Index); + if (!IsValid(Input)) + continue; + if (Input->IsObjectPathParameter() && Input->GetInputName() == InputParameterName) + return Input; + } + + return nullptr; +} + +const UHoudiniInput* +UHoudiniPublicAPIAssetWrapper::FindValidHoudiniNodeInputParameter(const FName& InInputParameterName) const +{ + if (InInputParameterName == NAME_None) + return nullptr; + + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return nullptr; + + const FString InputParameterName = InInputParameterName.ToString(); + const int32 NumInputs = HC->GetNumInputs(); + for (int32 Index = 0; Index < NumInputs; ++Index) + { + UHoudiniInput const* const Input = HC->GetInputAt(Index); + if (!IsValid(Input)) + continue; + if (Input->IsObjectPathParameter() && Input->GetInputName() == InputParameterName) + return Input; + } + + return nullptr; +} + +bool +UHoudiniPublicAPIAssetWrapper::CreateAndPopulateAPIInput(const UHoudiniInput* InHoudiniInput, UHoudiniPublicAPIInput*& OutAPIInput) +{ + if (!IsValid(InHoudiniInput)) + return false; + + TSubclassOf APIInputClass; + const EHoudiniInputType InputType = InHoudiniInput->GetInputType(); + switch (InputType) + { + case EHoudiniInputType::Geometry: + APIInputClass = UHoudiniPublicAPIGeoInput::StaticClass(); + break; + case EHoudiniInputType::Curve: + APIInputClass = UHoudiniPublicAPICurveInput::StaticClass(); + break; + + case EHoudiniInputType::World: + APIInputClass = UHoudiniPublicAPIWorldInput::StaticClass(); + break; + + // Deprecated input types + case EHoudiniInputType::Invalid: + default: + SetErrorMessage(FString::Printf(TEXT("GetInputAtIndex: Invalid input type %d"), InputType)); + return false; + } + + UHoudiniPublicAPIInput* APIInput = CreateEmptyInput(APIInputClass); + if (!IsValid(APIInput)) + { + return false; + } + + const bool bSuccessfullyCopied = APIInput->PopulateFromHoudiniInput(InHoudiniInput); + OutAPIInput = APIInput; + return bSuccessfullyCopied; +} + +bool +UHoudiniPublicAPIAssetWrapper::PopulateHoudiniInput(const UHoudiniPublicAPIInput* InAPIInput, UHoudiniInput* InHoudiniInput) const +{ + if (!IsValid(InHoudiniInput)) + return false; + + return InAPIInput->UpdateHoudiniInput(InHoudiniInput); +} + +bool +UHoudiniPublicAPIAssetWrapper::GetValidTOPNetworkByPathWithError(const FString& InNetworkRelativePath, int32& OutNetworkIndex, UTOPNetwork*& OutNetwork) const +{ + UHoudiniPDGAssetLink* AssetLink = nullptr; + if (!GetValidHoudiniPDGAssetLinkWithError(AssetLink)) + return false; + + int32 NetworkIndex = INDEX_NONE; + UTOPNetwork* const Network = UHoudiniPDGAssetLink::GetTOPNetworkByNodePath( + InNetworkRelativePath, AssetLink->AllTOPNetworks, NetworkIndex); + if (!IsValid(Network)) + { + SetErrorMessage(FString::Printf( + TEXT("Could not find valid TOP network at relative path '%s'."), *InNetworkRelativePath)); + return false; + } + + OutNetworkIndex = NetworkIndex; + OutNetwork = Network; + return true; +} + +bool +UHoudiniPublicAPIAssetWrapper::GetValidTOPNodeByPathWithError( + const FString& InNetworkRelativePath, + const FString& InNodeRelativePath, + int32& OutNetworkIndex, + int32& OutNodeIndex, + UTOPNode*& OutNode) const +{ + int32 NetworkIndex = INDEX_NONE; + UTOPNetwork* Network = nullptr; + if (!GetValidTOPNetworkByPathWithError(InNetworkRelativePath, NetworkIndex, Network)) + return false; + + int32 NodeIndex = INDEX_NONE; + UTOPNode* const Node = UHoudiniPDGAssetLink::GetTOPNodeByNodePath( + InNodeRelativePath, Network->AllTOPNodes, NodeIndex); + if (!IsValid(Node)) + { + SetErrorMessage(FString::Printf( + TEXT("Could not find valid TOP node at relative path '%s'."), *InNodeRelativePath)); + return false; + } + + OutNetworkIndex = NetworkIndex; + OutNodeIndex = NodeIndex; + OutNode = Node; + return true; +} + +void +UHoudiniPublicAPIAssetWrapper::ProcessComponentSynchronous_Implementation() +{ + UHoudiniCookable* HC = nullptr; + if (!GetValidHoudiniCookableWithError(HC)) + return; + + if (!FHoudiniEngine::Get().IsCookingEnabled()) + return; + + // Node Sync component cant be processed + USceneComponent* CookableComponent = HC->GetComponent(); + if (CookableComponent && CookableComponent->IsA()) + return; + + FHoudiniEngineManager* HEM = FHoudiniEngine::Get().GetHoudiniEngineManager(); + if (!HEM) + return; + + bool bIsStillProcessing = true; + while (bIsStillProcessing) + { + EHoudiniAssetState CurrentState = HC->GetCurrentState(); + if (CurrentState == EHoudiniAssetState::NeedInstantiation) + { + // We can exit here. + bIsStillProcessing = false; + } + else if (CurrentState == EHoudiniAssetState::None) + { + // When reaching the none state - we want to process the component + // one last time in case some changes trigger an update/cook + HEM->ProcessCookable(HC); + if (HC->GetCurrentState() == EHoudiniAssetState::None) + { + // The component is not active anymore - we can return + bIsStillProcessing = false; + } + } + else + { + // Keep processing the component until we reach an inactive state + HEM->ProcessCookable(HC); + } + } +} + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPublicAPIBlueprintLib.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPublicAPIBlueprintLib.h" +#include "HoudiniPublicAPI.h" + + +UHoudiniPublicAPIBlueprintLib::UHoudiniPublicAPIBlueprintLib(class FObjectInitializer const & ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +UHoudiniPublicAPI* UHoudiniPublicAPIBlueprintLib::GetAPI() +{ + static UHoudiniPublicAPI* Obj = NewObject(GetTransientPackage(), NAME_None, RF_MarkAsRootSet); + return Obj; +} + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPublicAPIInputTypes.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPublicAPIInputTypes.h" + +#include "HoudiniPublicAPIAssetWrapper.h" + +#include "HoudiniInput.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" + +UHoudiniPublicAPIInput::UHoudiniPublicAPIInput() +{ + bKeepWorldTransform = false; + bImportAsReference = false; + bImportAsReferenceRotScaleEnabled = true; + bImportAsReferenceBboxEnabled = true; + bImportAsReferenceMaterialEnabled = true; + bExportMaterialParameters = false; +} + +bool +UHoudiniPublicAPIInput::IsAcceptableObjectForInput_Implementation(UObject* InObject) const +{ + return UHoudiniInput::IsObjectAcceptable(GetInputType(), InObject); +} + +bool +UHoudiniPublicAPIInput::SetInputObjects_Implementation(const TArray& InObjects) +{ + bool bHasFailures = false; + InputObjects.Empty(InObjects.Num()); + for (UObject* const Object : InObjects) + { + if (!IsValid(Object)) + { + SetErrorMessage(FString::Printf(TEXT("An input object is null or invalid."))); + bHasFailures = true; + continue; + } + else if (!IsAcceptableObjectForInput(Object)) + { + SetErrorMessage(FString::Printf( + TEXT("Object '%s' is not of an acceptable type for inputs of class %s."), + *(Object->GetName()), *(GetClass()->GetName()))); + bHasFailures = true; + continue; + } + + InputObjects.Add(Object); + } + + return !bHasFailures; +} + +bool +UHoudiniPublicAPIInput::GetInputObjects_Implementation(TArray& OutObjects) +{ + OutObjects = InputObjects; + + return true; +} + +bool +UHoudiniPublicAPIInput::PopulateFromHoudiniInput(UHoudiniInput const* const InInput) +{ + const EHoudiniInputType InputType = GetInputType(); + if (!IsValid(InInput)) + { + SetErrorMessage(TEXT("InInput is invalid.")); + return false; + } + + if (InInput->GetInputType() != InputType) + { + SetErrorMessage(FString::Printf( + TEXT("Incompatible input types %d vs %d"), InInput->GetInputType(), InputType)); + return false; + } + + bKeepWorldTransform = InInput->GetKeepWorldTransform(); + bImportAsReference = InInput->GetImportAsReference(); + bImportAsReferenceRotScaleEnabled = InInput->GetImportAsReferenceRotScaleEnabled(); + bImportAsReferenceBboxEnabled = InInput->GetImportAsReferenceBboxEnabled(); + bImportAsReferenceMaterialEnabled = InInput->GetImportAsReferenceMaterialEnabled(); + bExportMaterialParameters = InInput->GetExportMaterialParameters(); + + const TArray>* SrcInputObjectsPtr = InInput->GetHoudiniInputObjectArray(InputType); + if (SrcInputObjectsPtr && SrcInputObjectsPtr->Num() > 0) + { + InputObjects.Empty(SrcInputObjectsPtr->Num()); + for (UHoudiniInputObject const* const SrcInputObject : *SrcInputObjectsPtr) + { + if (!IsValid(SrcInputObject)) + continue; + + UObject* NewInputObject = ConvertInternalInputObject(SrcInputObject->GetObject()); + + if (NewInputObject && !IsValid(NewInputObject)) + { + SetErrorMessage(FString::Printf( + TEXT("One of the input objects is non-null but pending kill/invalid."))); + return false; + } + + InputObjects.Add(NewInputObject); + + CopyHoudiniInputObjectPropertiesToInputObject(SrcInputObject, InputObjects.Num() - 1); + } + } + + return true; +} + +bool +UHoudiniPublicAPIInput::UpdateHoudiniInput(UHoudiniInput* const InInput) const +{ + if (!IsValid(InInput)) + { + SetErrorMessage(TEXT("InInput is invalid.")); + return false; + } + + bool bAnyChanges = false; + + // If the input type didn't change, but the new/incoming InputObjects array is now smaller than the current input + // objects array on the input, delete the surplus objects + const EHoudiniInputType InputType = GetInputType(); + const int32 NumInputObjects = InputObjects.Num(); + if (InputType == InInput->GetInputType()) + { + const int32 OldNumInputObjects = InInput->GetNumberOfInputObjects(); + if (NumInputObjects < OldNumInputObjects) + { + for (int32 Index = OldNumInputObjects - 1; Index >= NumInputObjects; --Index) + { + InInput->DeleteInputObjectAt(Index); + } + bAnyChanges = true; + } + } + else + { + // Set / change the input type + bool bBlueprintStructureModified = false; + InInput->SetInputType(InputType, bBlueprintStructureModified); + } + + // Set any general settings + if (InInput->GetKeepWorldTransform() != bKeepWorldTransform) + { + InInput->SetKeepWorldTransform(bKeepWorldTransform); + bAnyChanges = true; + } + if (InInput->GetImportAsReference() != bImportAsReference) + { + InInput->SetImportAsReference(bImportAsReference); + bAnyChanges = true; + } + if (InInput->GetImportAsReferenceRotScaleEnabled() != bImportAsReferenceRotScaleEnabled) + { + InInput->SetImportAsReferenceRotScaleEnabled(bImportAsReferenceRotScaleEnabled); + bAnyChanges = true; + } + if (InInput->GetImportAsReferenceBboxEnabled() != bImportAsReferenceBboxEnabled) + { + InInput->SetImportAsReferenceBboxEnabled(bImportAsReferenceBboxEnabled); + bAnyChanges = true; + } + if (InInput->GetImportAsReferenceMaterialEnabled() != bImportAsReferenceMaterialEnabled) + { + InInput->SetImportAsReferenceMaterialEnabled(bImportAsReferenceMaterialEnabled); + bAnyChanges = true; + } + if (InInput->GetExportMaterialParameters() != bExportMaterialParameters) + { + InInput->SetExportMaterialParameters(bExportMaterialParameters); + bAnyChanges = true; + } + + // Copy / set the input objects on the Houdini Input + InInput->SetInputObjectsNumber(InputType, NumInputObjects); + for (int32 Index = 0; Index < NumInputObjects; ++Index) + { + UObject* const InputObject = InputObjects[Index]; + UObject const* CurrentInputObject = InInput->GetInputObjectAt(Index); + + if (!IsValid(InputObject)) + { + // Delete existing input object, but leave its space in the array, we'll set that to nullptr + if (CurrentInputObject) + { + const bool bRemoveIndexFromArray = false; + InInput->DeleteInputObjectAt(Index, bRemoveIndexFromArray); + } + InInput->SetInputObjectAt(Index, nullptr); + + if (!bAnyChanges && CurrentInputObject) + bAnyChanges = true; + } + else + { + UObject const* const NewInputObject = ConvertAPIInputObjectAndAssignToInput(InputObject, InInput, Index); + UHoudiniInputObject *DstHoudiniInputObject = InInput->GetHoudiniInputObjectAt(Index); + if (DstHoudiniInputObject) + CopyInputObjectPropertiesToHoudiniInputObject(Index, DstHoudiniInputObject); + + if (!bAnyChanges && NewInputObject != CurrentInputObject) + bAnyChanges = true; + } + } + + if (bAnyChanges) + { + InInput->MarkChanged(true); + } + + return true; +} + +bool +UHoudiniPublicAPIInput::CopyHoudiniInputObjectPropertiesToInputObject(UHoudiniInputObject const* const InHoudiniInputObject, const int32 InInputObjectIndex) +{ + if (!IsValid(InHoudiniInputObject) || !InputObjects.IsValidIndex(InInputObjectIndex)) + return false; + + return true; +} + +bool +UHoudiniPublicAPIInput::CopyInputObjectPropertiesToHoudiniInputObject(const int32 InInputObjectIndex, UHoudiniInputObject* const InHoudiniInputObject) const +{ + if (!InputObjects.IsValidIndex(InInputObjectIndex) || !IsValid(InHoudiniInputObject)) + return false; + + if (InHoudiniInputObject->GetImportAsReference() != bImportAsReference) + { + // InHoudiniInputObject->SetImportAsReference(bImportAsReference); + InHoudiniInputObject->MarkChanged(true); + } + + return true; +} + +UObject* +UHoudiniPublicAPIInput::ConvertAPIInputObjectAndAssignToInput(UObject* InAPIInputObject, UHoudiniInput* InHoudiniInput, const int32 InInputIndex) const +{ + if (!IsValid(InHoudiniInput)) + return nullptr; + + UObject const* const CurrentInputObject = InHoudiniInput->GetInputObjectAt(InInputIndex); + + UObject* const ObjectToSet = (IsValid(InAPIInputObject)) ? InAPIInputObject : nullptr; + + // Delete the existing input object if it is invalid or differs from ObjectToSet + if (CurrentInputObject && (!IsValid(CurrentInputObject) || CurrentInputObject != ObjectToSet)) + { + // Keep the space/index in the array, we're going to set the new input object at the same index + const bool bRemoveIndexFromArray = false; + InHoudiniInput->DeleteInputObjectAt(InInputIndex, bRemoveIndexFromArray); + InHoudiniInput->MarkChanged(true); + } + + InHoudiniInput->SetInputObjectAt(InInputIndex, ObjectToSet); + + return ObjectToSet; +} + +UHoudiniPublicAPIGeoInput::UHoudiniPublicAPIGeoInput() +{ + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + + bKeepWorldTransform = false; + bPackBeforeMerge = false; + bExportMainGeometry = true; + bExportLODs = false; + bExportSockets = false; + bExportColliders = false; + bPreferNaniteFallbackMesh = HoudiniRuntimeSettings ? HoudiniRuntimeSettings->bPreferNaniteFallbackMesh : false; +} + +bool +UHoudiniPublicAPIGeoInput::SetInputObjects_Implementation(const TArray& InObjects) +{ + const bool bSuccess = Super::SetInputObjects_Implementation(InObjects); + + // Keep the transforms at the valid indices, resize the array to match InputObjects length. Set identity transform + // in new slots. + const int32 NumInputObjects = InputObjects.Num(); + const int32 NumTransforms = InputObjectTransformOffsetArray.Num(); + if (NumTransforms > NumInputObjects) + { + InputObjectTransformOffsetArray.SetNum(NumInputObjects); + } + else if (NumTransforms < NumInputObjects) + { + InputObjectTransformOffsetArray.Reserve(NumInputObjects); + for (int32 Index = NumTransforms; Index < NumInputObjects; ++Index) + { + InputObjectTransformOffsetArray.Emplace(FTransform::Identity); + } + } + + return bSuccess; +} + +bool +UHoudiniPublicAPIGeoInput::PopulateFromHoudiniInput(UHoudiniInput const* const InInput) +{ + if (!Super::PopulateFromHoudiniInput(InInput)) + return false; + + bPackBeforeMerge = InInput->GetPackBeforeMerge(); + bExportMainGeometry = InInput->GetExportMainGeometry(); + bExportLODs = InInput->GetExportLODs(); + bExportSockets = InInput->GetExportSockets(); + bExportColliders = InInput->GetExportColliders(); + + return true; +} + +bool +UHoudiniPublicAPIGeoInput::UpdateHoudiniInput(UHoudiniInput* const InInput) const +{ + if (!Super::UpdateHoudiniInput(InInput)) + return false; + + bool bAnyChanges = false; + if (InInput->GetPackBeforeMerge() != bPackBeforeMerge) + { + InInput->SetPackBeforeMerge(bPackBeforeMerge); + bAnyChanges = true; + } + if (InInput->GetExportMainGeometry() != bExportMainGeometry) + { + InInput->SetExportMainGeometry(bExportMainGeometry); + bAnyChanges = true; + } + if (InInput->GetExportLODs() != bExportLODs) + { + InInput->SetExportLODs(bExportLODs); + bAnyChanges = true; + } + if (InInput->GetExportSockets() != bExportSockets) + { + InInput->SetExportSockets(bExportSockets); + bAnyChanges = true; + } + if (InInput->GetPreferNaniteFallbackMesh() != bPreferNaniteFallbackMesh) + { + InInput->SetPreferNaniteFallbackMesh(bPreferNaniteFallbackMesh); + bAnyChanges = true; + } + if (InInput->GetExportColliders() != bExportColliders) + { + InInput->SetExportColliders(bExportColliders); + bAnyChanges = true; + } + + if (bAnyChanges) + { + InInput->MarkChanged(true); + } + + return true; +} + +void +UHoudiniPublicAPIGeoInput::PostLoad() +{ + Super::PostLoad(); +} + +bool +UHoudiniPublicAPIGeoInput::CopyHoudiniInputObjectPropertiesToInputObject(UHoudiniInputObject const* const InHoudiniInputObject, const int32 InInputObjectIndex) +{ + if (!Super::CopyHoudiniInputObjectPropertiesToInputObject(InHoudiniInputObject, InInputObjectIndex)) + return false; + + if (!IsValid(InHoudiniInputObject) || !InputObjects.IsValidIndex(InInputObjectIndex)) + return false; + + // Copy the transform offset + if (SupportsTransformOffset()) + { + SetInputObjectTransformOffset(InInputObjectIndex, InHoudiniInputObject->GetTransform()); + } + + return true; +} + +bool +UHoudiniPublicAPIGeoInput::CopyInputObjectPropertiesToHoudiniInputObject(const int32 InInputObjectIndex, UHoudiniInputObject* const InHoudiniInputObject) const +{ + if (!Super::CopyInputObjectPropertiesToHoudiniInputObject(InInputObjectIndex, InHoudiniInputObject)) + return false; + + if (!InputObjects.IsValidIndex(InInputObjectIndex) || !IsValid(InHoudiniInputObject)) + return false; + + // Copy the transform offset + if (SupportsTransformOffset()) + { + FTransform Transform; + if (!GetInputObjectTransformOffset(InInputObjectIndex, Transform)) + Transform = FTransform::Identity; + + if (!InHoudiniInputObject->GetTransform().Equals(Transform)) + { + InHoudiniInputObject->SetTransform(Transform); + InHoudiniInputObject->MarkChanged(true); + } + } + + return true; +} + + +bool +UHoudiniPublicAPIGeoInput::SetInputObjectTransformOffset_Implementation( + const int32 InInputObjectIndex, const FTransform& InTransform) +{ + if (!SupportsTransformOffset()) + { + SetErrorMessage(FString::Printf( + TEXT("%s inputs do not support transform offsets."), *UEnum::GetValueAsString(GetInputType()))); + return false; + } + + if (!InputObjects.IsValidIndex(InInputObjectIndex)) + { + SetErrorMessage(TEXT("InInputObjectIndex is out of range.")); + return false; + } + + if (!InputObjectTransformOffsetArray.IsValidIndex(InInputObjectIndex)) + { + const int32 NumTransforms = InputObjectTransformOffsetArray.Num(); + InputObjectTransformOffsetArray.SetNum(InInputObjectIndex + 1); + for (int32 TransformIndex = NumTransforms; TransformIndex < InInputObjectIndex; ++TransformIndex) + { + InputObjectTransformOffsetArray[TransformIndex] = FTransform::Identity; + } + } + InputObjectTransformOffsetArray[InInputObjectIndex] = InTransform; + + return true; +} + +bool +UHoudiniPublicAPIGeoInput::GetInputObjectTransformOffset_Implementation( + const int32 InInputObjectIndex, FTransform& OutTransform) const +{ + if (!SupportsTransformOffset()) + { + SetErrorMessage(FString::Printf( + TEXT("%s inputs do not support transform offsets."), *UEnum::GetValueAsString(GetInputType()))); + return false; + } + + if (!InputObjects.IsValidIndex(InInputObjectIndex)) + { + SetErrorMessage(TEXT("InInputObjectIndex is out of range.")); + return false; + } + + if (!InputObjectTransformOffsetArray.IsValidIndex(InInputObjectIndex)) + { + SetErrorMessage(FString::Printf( + TEXT("Input object at index '%d' does not have a transform offset set."), InInputObjectIndex)); + return false; + } + + OutTransform = InputObjectTransformOffsetArray[InInputObjectIndex]; + return true; +} + +bool +UHoudiniPublicAPIGeoInput::GetInputObjectTransformOffsetArray_Implementation(TArray& OutInputObjectTransformOffsetArray) const +{ + if (!SupportsTransformOffset()) + { + SetErrorMessage(FString::Printf( + TEXT("%s inputs do not support transform offsets."), *UEnum::GetValueAsString(GetInputType()))); + return false; + } + + OutInputObjectTransformOffsetArray = InputObjectTransformOffsetArray; + + return true; +} + + +UHoudiniPublicAPICurveInputObject::UHoudiniPublicAPICurveInputObject() + : bClosed(false) + , bReversed(false) + , CurveType(EHoudiniPublicAPICurveType::Polygon) + , CurveMethod(EHoudiniPublicAPICurveMethod::CVs) + , CurveBreakpointParameterization(EHoudiniPublicAPICurveBreakpointParameterization::Uniform) +{ + +} + + +void +UHoudiniPublicAPICurveInputObject::PopulateFromHoudiniSplineComponent(UHoudiniSplineComponent const* const InSpline) +{ + if (!IsValid(InSpline)) + return; + + bClosed = InSpline->IsClosedCurve(); + bReversed = InSpline->IsReversed(); + CurveType = ToHoudiniPublicAPICurveType(InSpline->GetCurveType()); + CurveMethod = ToHoudiniPublicAPICurveMethod(InSpline->GetCurveMethod()); + CurveBreakpointParameterization = ToHoudiniPublicAPICurveBreakpointParameterization(InSpline->GetCurveBreakpointParameterization()); + CurvePoints = InSpline->CurvePoints; +} + +void +UHoudiniPublicAPICurveInputObject::CopyToHoudiniSplineComponent(UHoudiniSplineComponent* const InSpline) const +{ + if (!IsValid(InSpline)) + return; + + bool bAnyChanges = false; + if (bClosed != InSpline->IsClosedCurve()) + { + InSpline->SetClosedCurve(bClosed); + bAnyChanges = true; + } + if (bReversed != InSpline->IsReversed()) + { + InSpline->SetReversed(bReversed); + bAnyChanges = true; + } + const EHoudiniCurveType HoudiniCurveType = ToHoudiniCurveType(CurveType); + if (HoudiniCurveType != InSpline->GetCurveType()) + { + InSpline->SetCurveType(HoudiniCurveType); + bAnyChanges = true; + } + const EHoudiniCurveMethod HoudiniCurveMethod = ToHoudiniCurveMethod(CurveMethod); + if (HoudiniCurveMethod != InSpline->GetCurveMethod()) + { + InSpline->SetCurveMethod(HoudiniCurveMethod); + bAnyChanges = true; + } + const EHoudiniCurveBreakpointParameterization HoudiniCurveBreakpointParameterization = ToHoudiniCurveBreakpointParamterization(CurveBreakpointParameterization); + if (HoudiniCurveBreakpointParameterization != InSpline->GetCurveBreakpointParameterization()) + { + InSpline->SetCurveBreakpointParameterization(HoudiniCurveBreakpointParameterization); + bAnyChanges = true; + } + + // Check if there are curve point differences + bool bUpdatePoints = false; + if (CurvePoints.Num() == InSpline->CurvePoints.Num()) + { + const int32 NumPoints = CurvePoints.Num(); + for (int32 Index = 0; Index < NumPoints; ++Index) + { + const FTransform& A = CurvePoints[Index]; + const FTransform& B = InSpline->CurvePoints[Index]; + + if (!A.Equals(B, 0.0f)) + { + bUpdatePoints = true; + break; + } + } + } + else + { + bUpdatePoints = true; + } + + // If there are curve point differences, update the points + if (bUpdatePoints) + { + InSpline->ResetCurvePoints(); + InSpline->ResetDisplayPoints(); + InSpline->CurvePoints = CurvePoints; + bAnyChanges = true; + } + + if (bAnyChanges) + InSpline->MarkChanged(true); +} + +EHoudiniCurveType +UHoudiniPublicAPICurveInputObject::ToHoudiniCurveType(const EHoudiniPublicAPICurveType InCurveType) +{ + switch (InCurveType) + { + case EHoudiniPublicAPICurveType::Invalid: + return EHoudiniCurveType::Invalid; + case EHoudiniPublicAPICurveType::Polygon: + return EHoudiniCurveType::Polygon; + case EHoudiniPublicAPICurveType::Nurbs: + return EHoudiniCurveType::Nurbs; + case EHoudiniPublicAPICurveType::Bezier: + return EHoudiniCurveType::Bezier; + case EHoudiniPublicAPICurveType::Points: + return EHoudiniCurveType::Points; + } + + return EHoudiniCurveType::Invalid; +} + +EHoudiniCurveMethod +UHoudiniPublicAPICurveInputObject::ToHoudiniCurveMethod(const EHoudiniPublicAPICurveMethod InCurveMethod) +{ + switch (InCurveMethod) + { + case EHoudiniPublicAPICurveMethod::Invalid: + return EHoudiniCurveMethod::Invalid; + case EHoudiniPublicAPICurveMethod::CVs: + return EHoudiniCurveMethod::CVs; + case EHoudiniPublicAPICurveMethod::Breakpoints: + return EHoudiniCurveMethod::Breakpoints; + case EHoudiniPublicAPICurveMethod::Freehand: + return EHoudiniCurveMethod::Freehand; + } + + return EHoudiniCurveMethod::Invalid; +} + +EHoudiniCurveBreakpointParameterization UHoudiniPublicAPICurveInputObject::ToHoudiniCurveBreakpointParamterization( + const EHoudiniPublicAPICurveBreakpointParameterization InCurveBreakpointParameterization) +{ + switch (InCurveBreakpointParameterization) + { + case EHoudiniPublicAPICurveBreakpointParameterization::Invalid: + return EHoudiniCurveBreakpointParameterization::Invalid; + case EHoudiniPublicAPICurveBreakpointParameterization::Uniform: + return EHoudiniCurveBreakpointParameterization::Uniform; + case EHoudiniPublicAPICurveBreakpointParameterization::Chord: + return EHoudiniCurveBreakpointParameterization::Chord; + case EHoudiniPublicAPICurveBreakpointParameterization::Centripetal: + return EHoudiniCurveBreakpointParameterization::Centripetal; + } + + return EHoudiniCurveBreakpointParameterization::Invalid; +} + +EHoudiniPublicAPICurveType +UHoudiniPublicAPICurveInputObject::ToHoudiniPublicAPICurveType(const EHoudiniCurveType InCurveType) +{ + switch (InCurveType) + { + case EHoudiniCurveType::Invalid: + return EHoudiniPublicAPICurveType::Invalid; + case EHoudiniCurveType::Polygon: + return EHoudiniPublicAPICurveType::Polygon; + case EHoudiniCurveType::Nurbs: + return EHoudiniPublicAPICurveType::Nurbs; + case EHoudiniCurveType::Bezier: + return EHoudiniPublicAPICurveType::Bezier; + case EHoudiniCurveType::Points: + return EHoudiniPublicAPICurveType::Points; + } + + return EHoudiniPublicAPICurveType::Invalid; +} + +EHoudiniPublicAPICurveMethod +UHoudiniPublicAPICurveInputObject::ToHoudiniPublicAPICurveMethod(const EHoudiniCurveMethod InCurveMethod) +{ + switch (InCurveMethod) + { + case EHoudiniCurveMethod::Invalid: + return EHoudiniPublicAPICurveMethod::Invalid; + case EHoudiniCurveMethod::CVs: + return EHoudiniPublicAPICurveMethod::CVs; + case EHoudiniCurveMethod::Breakpoints: + return EHoudiniPublicAPICurveMethod::Breakpoints; + case EHoudiniCurveMethod::Freehand: + return EHoudiniPublicAPICurveMethod::Freehand; + } + + return EHoudiniPublicAPICurveMethod::Invalid; +} + + + +EHoudiniPublicAPICurveBreakpointParameterization UHoudiniPublicAPICurveInputObject:: +ToHoudiniPublicAPICurveBreakpointParameterization( + const EHoudiniCurveBreakpointParameterization InCurveBreakpointParameterization) +{ + switch (InCurveBreakpointParameterization) + { + case EHoudiniCurveBreakpointParameterization::Invalid: + return EHoudiniPublicAPICurveBreakpointParameterization::Invalid; + case EHoudiniCurveBreakpointParameterization::Uniform: + return EHoudiniPublicAPICurveBreakpointParameterization::Uniform; + case EHoudiniCurveBreakpointParameterization::Chord: + return EHoudiniPublicAPICurveBreakpointParameterization::Chord; + case EHoudiniCurveBreakpointParameterization::Centripetal: + return EHoudiniPublicAPICurveBreakpointParameterization::Centripetal; + } + + return EHoudiniPublicAPICurveBreakpointParameterization::Invalid; +} + +UHoudiniPublicAPICurveInput::UHoudiniPublicAPICurveInput() +{ + bKeepWorldTransform = false; + bCookOnCurveChanged = true; + bAddRotAndScaleAttributesOnCurves = false; + bUseLegacyInputCurves = false; +} + +bool +UHoudiniPublicAPICurveInput::IsAcceptableObjectForInput_Implementation(UObject* InObject) const +{ + if (!IsValid(InObject)) + return false; + + if (InObject->IsA()) + return true; + + return Super::IsAcceptableObjectForInput_Implementation(InObject); +} + +bool +UHoudiniPublicAPICurveInput::PopulateFromHoudiniInput(UHoudiniInput const* const InInput) +{ + if (!Super::PopulateFromHoudiniInput(InInput)) + return false; + + bCookOnCurveChanged = InInput->GetCookOnCurveChange(); + bAddRotAndScaleAttributesOnCurves = InInput->IsAddRotAndScaleAttributesEnabled(); + bUseLegacyInputCurves = InInput->IsUseLegacyInputCurvesEnabled(); + + return true; +} + +bool +UHoudiniPublicAPICurveInput::UpdateHoudiniInput(UHoudiniInput* const InInput) const +{ + if (!Super::UpdateHoudiniInput(InInput)) + return false; + + bool bAnyChanges = false; + if (InInput->GetCookOnCurveChange() != bCookOnCurveChanged) + { + InInput->SetCookOnCurveChange(bCookOnCurveChanged); + bAnyChanges = true; + } + + if (InInput->IsAddRotAndScaleAttributesEnabled() != bAddRotAndScaleAttributesOnCurves) + { + InInput->SetAddRotAndScaleAttributes(bAddRotAndScaleAttributesOnCurves); + bAnyChanges = true; + } + + if (InInput->IsUseLegacyInputCurvesEnabled() != bUseLegacyInputCurves) + { + InInput->SetUseLegacyInputCurve(bUseLegacyInputCurves); + bAnyChanges = true; + } + + if (bAnyChanges) + { + InInput->MarkChanged(true); + } + + return true; +} + +UObject* +UHoudiniPublicAPICurveInput::ConvertInternalInputObject(UObject* InInternalInputObject) +{ + UObject* Object = Super::ConvertInternalInputObject(InInternalInputObject); + + // If the input object is a houdini spline component, convert it to an API curve wrapper + if (IsValid(Object) && Object->IsA()) + { + UHoudiniPublicAPICurveInputObject* const Curve = NewObject( + this, UHoudiniPublicAPICurveInputObject::StaticClass()); + if (IsValid(Curve)) + { + Curve->PopulateFromHoudiniSplineComponent(Cast(Object)); + return Curve; + } + } + + return Object; +} + +UObject* +UHoudiniPublicAPICurveInput::ConvertAPIInputObjectAndAssignToInput(UObject* InAPIInputObject, UHoudiniInput* InHoudiniInput, const int32 InInputIndex) const +{ + UObject* Object = nullptr; + + // If the input is an API curve wrapper, convert it to a UHoudiniSplineComponent + if (IsValid(InAPIInputObject) && InAPIInputObject->IsA() && IsValid(InHoudiniInput)) + { + UHoudiniPublicAPICurveInputObject* const InAPICurveInputObject = Cast(InAPIInputObject); + + // If there is an existing input object at this index, and it is a HoudiniSplineComponent, then just update it + // otherwise, create a new input object wrapper + bool bCreateNew = false; + UHoudiniInputObject const* const CurrentHoudiniInputObject = InHoudiniInput->GetHoudiniInputObjectAt(InInputIndex); + UObject* const CurrentInputObject = InHoudiniInput->GetInputObjectAt(InInputIndex); + if (IsValid(CurrentInputObject) && CurrentInputObject->IsA() && + IsValid(CurrentHoudiniInputObject) && CurrentHoudiniInputObject->IsA()) + { + UHoudiniSplineComponent* CurrentSpline = Cast(CurrentInputObject); + if (IsValid(CurrentSpline)) + { + if (IsValid(InAPICurveInputObject)) + { + InAPICurveInputObject->CopyToHoudiniSplineComponent(CurrentSpline); + // Currently the CopyToHoudiniSplineComponent function does not return an indication of if anything + // actually changed, so we have to assume this is a change + + InHoudiniInput->MarkChanged(true); + } + Object = CurrentSpline; + } + else + { + bCreateNew = true; + } + } + else + { + bCreateNew = true; + } + + if (bCreateNew) + { + // Replace any object that is already at this index: we remove the current input object first, then + // we create the new one + if (CurrentInputObject) + { + // Keep the space/index in the array, we're going to set the new input object at the same index + const bool bRemoveIndexFromArray = false; + InHoudiniInput->DeleteInputObjectAt(InInputIndex, bRemoveIndexFromArray); + InHoudiniInput->MarkChanged(true); + } + + UHoudiniInputHoudiniSplineComponent* FromHoudiniSplineInputComponent = nullptr; + const bool bAttachToParent = true; + const bool bAppendToInputArray = false; + bool bBlueprintStructureModified; + UHoudiniInputHoudiniSplineComponent* const NewHoudiniInputObject = InHoudiniInput->CreateHoudiniSplineInput( + FromHoudiniSplineInputComponent, bAttachToParent, bAppendToInputArray, bBlueprintStructureModified); + if (IsValid(NewHoudiniInputObject)) + { + UHoudiniSplineComponent* HoudiniSplineComponent = NewHoudiniInputObject->GetCurveComponent(); + if (IsValid(HoudiniSplineComponent)) + { + // Populate the HoudiniSplineComponent from the curve wrapper + if (IsValid(InAPICurveInputObject)) + InAPICurveInputObject->CopyToHoudiniSplineComponent(HoudiniSplineComponent); + Object = HoudiniSplineComponent; + } + } + + TArray>* HoudiniInputObjectArray = InHoudiniInput->GetHoudiniInputObjectArray(InHoudiniInput->GetInputType()); + if (HoudiniInputObjectArray && HoudiniInputObjectArray->IsValidIndex(InInputIndex)) + { + (*HoudiniInputObjectArray)[InInputIndex] = IsValid(NewHoudiniInputObject) ? NewHoudiniInputObject : nullptr; + InHoudiniInput->MarkChanged(true); + } + } + } + else + { + Object = Super::ConvertAPIInputObjectAndAssignToInput(InAPIInputObject, InHoudiniInput, InInputIndex); + } + + return Object; +} + +UHoudiniPublicAPIWorldInput::UHoudiniPublicAPIWorldInput() +{ + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + + bKeepWorldTransform = true; + bIsWorldInputBoundSelector = false; + bWorldInputBoundSelectorAutoUpdate = false; + bWorldInputAutoUpdate = true; + UnrealSplineResolution = HoudiniRuntimeSettings ? HoudiniRuntimeSettings->MarshallingSplineResolution : 50.0f; + bPreferNaniteFallbackMesh = HoudiniRuntimeSettings ? HoudiniRuntimeSettings->bPreferNaniteFallbackMesh : false; + bExportLevelInstanceContent = true; + bDirectlyConnectHdas = true; + bExportHeightDataPerEditLayer = true; + bExportMergedPaintLayers = true; + bExportPaintLayersPerEditLayer = false; +} + +bool +UHoudiniPublicAPIWorldInput::SetInputObjects_Implementation(const TArray& InObjects) +{ + if (bIsWorldInputBoundSelector) + { + SetErrorMessage( + TEXT("This world input is not currently configured as a bound selector (bIsWorldInputBoundSelector == false)")); + return false; + } + + return Super::SetInputObjects_Implementation(InObjects); +} + +bool +UHoudiniPublicAPIWorldInput::PopulateFromHoudiniInput(UHoudiniInput const* const InInput) +{ + if (!Super::PopulateFromHoudiniInput(InInput)) + return false; + + TArray> const* const BoundSelectorObjectArray = InInput->GetBoundSelectorObjectArray(); + if (BoundSelectorObjectArray) + WorldInputBoundSelectorObjects = *BoundSelectorObjectArray; + else + WorldInputBoundSelectorObjects.Empty(); + + bIsWorldInputBoundSelector = InInput->IsWorldInputBoundSelector(); + bWorldInputBoundSelectorAutoUpdate = InInput->GetWorldInputBoundSelectorAutoUpdates(); + bWorldInputAutoUpdate = InInput->GetWorldInputAutoUpdates(); + UnrealSplineResolution = InInput->GetUnrealSplineResolution(); + bExportLevelInstanceContent = InInput->IsExportLevelInstanceContentEnabled(); + bDirectlyConnectHdas = InInput->GetDirectlyConnectHdas(); + bExportHeightDataPerEditLayer = InInput->IsEditLayerHeightExportEnabled(); + bExportPaintLayersPerEditLayer= InInput->IsPaintLayerPerEditLayerExportEnabled(); + bExportMergedPaintLayers = InInput->IsMergedPaintLayerExportEnabled(); + + return true; +} + +bool +UHoudiniPublicAPIWorldInput::UpdateHoudiniInput(UHoudiniInput* const InInput) const +{ + if (!Super::UpdateHoudiniInput(InInput)) + return false; + + bool bAnyChanges = false; + TArray>* const BoundSelectorObjectArray = InInput->GetBoundSelectorObjectArray(); + if (BoundSelectorObjectArray) + { + if (BoundSelectorObjectArray->Num() != WorldInputBoundSelectorObjects.Num()) + { + InInput->SetBoundSelectorObjectsNumber(WorldInputBoundSelectorObjects.Num()); + bAnyChanges = true; + } + + bool bNeedToUpdateBoundObjects = false; + for (int Idx = 0; Idx < WorldInputBoundSelectorObjects.Num(); Idx++) + { + if ((*BoundSelectorObjectArray)[Idx] != WorldInputBoundSelectorObjects[Idx]) + bNeedToUpdateBoundObjects = true; + } + + if (bNeedToUpdateBoundObjects) + { + *BoundSelectorObjectArray = WorldInputBoundSelectorObjects; + bAnyChanges = true; + } + } + + if (InInput->IsWorldInputBoundSelector() != bIsWorldInputBoundSelector) + { + InInput->SetWorldInputBoundSelector(bIsWorldInputBoundSelector); + bAnyChanges = true; + } + + if (InInput->GetWorldInputBoundSelectorAutoUpdates() != bWorldInputBoundSelectorAutoUpdate) + { + InInput->SetWorldInputBoundSelectorAutoUpdates(bWorldInputBoundSelectorAutoUpdate); + bAnyChanges = true; + } + + if (InInput->GetWorldInputAutoUpdates() != bWorldInputAutoUpdate) + { + InInput->SetWorldInputAutoUpdates(bWorldInputAutoUpdate); + bAnyChanges = true; + } + + if (InInput->GetUnrealSplineResolution() != UnrealSplineResolution) + { + InInput->SetUnrealSplineResolution(UnrealSplineResolution); + bAnyChanges = true; + } + + if (InInput->IsExportLevelInstanceContentEnabled() != bExportLevelInstanceContent) + { + InInput->SetExportLevelInstanceContent(bExportLevelInstanceContent); + bAnyChanges = true; + } + + if (InInput->GetDirectlyConnectHdas() != bDirectlyConnectHdas) + { + InInput->SetDirectlyConnectHdas(bDirectlyConnectHdas); + bAnyChanges = true; + } + + if (InInput->IsEditLayerHeightExportEnabled() != bExportHeightDataPerEditLayer) + { + InInput->SetExportHeightDataPerEditLayer(bExportHeightDataPerEditLayer); + bAnyChanges = true; + } + + if (InInput->IsPaintLayerPerEditLayerExportEnabled() != bExportPaintLayersPerEditLayer) + { + InInput->SetExportPaintLayerPerEditLayer(bExportPaintLayersPerEditLayer); + bAnyChanges = true; + } + + + if (InInput->IsMergedPaintLayerExportEnabled() != bExportMergedPaintLayers) + { + InInput->SetExportMergedPaintLayers(bExportMergedPaintLayers); + bAnyChanges = true; + } + + if (bAnyChanges) + { + InInput->MarkChanged(true); + } + + return true; +} + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPublicAPIObjectBase.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPublicAPIObjectBase.h" +#include "HoudiniEngineRuntimePrivatePCH.h" + +UHoudiniPublicAPIObjectBase::UHoudiniPublicAPIObjectBase() + : LastErrorMessage() + , bHasError(false) + , bIsLoggingErrors(true) +{ + +} + +bool +UHoudiniPublicAPIObjectBase::GetLastErrorMessage_Implementation(FString& OutLastErrorMessage) const +{ + if (!bHasError) + { + OutLastErrorMessage = FString(); + return false; + } + + OutLastErrorMessage = LastErrorMessage; + return true; +} + +void +UHoudiniPublicAPIObjectBase::ClearErrorMessages_Implementation() +{ + LastErrorMessage = FString(); + bHasError = false; +} + +void +UHoudiniPublicAPIObjectBase::SetErrorMessage_Implementation( + const FString& InErrorMessage, + const EHoudiniPublicAPIErrorLogOption InLoggingOption) const +{ + LastErrorMessage = InErrorMessage; + bHasError = true; + switch (InLoggingOption) + { + case EHoudiniPublicAPIErrorLogOption::Invalid: + case EHoudiniPublicAPIErrorLogOption::Auto: + case EHoudiniPublicAPIErrorLogOption::Log: + { + static const FString Prefix = TEXT("[HoudiniEngine:PublicAPI]"); + HOUDINI_LOG_WARNING(TEXT("%s %s"), *Prefix, *InErrorMessage); + break; + } + case EHoudiniPublicAPIErrorLogOption::NoLog: + // Don't log + break; + } +} + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPublicAPIOutputTypes.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPublicAPIOutputTypes.h" + +#include "HoudiniOutput.h" + +FHoudiniPublicAPIOutputObjectIdentifier::FHoudiniPublicAPIOutputObjectIdentifier() + : SplitIdentifier() + , PartName() + , ObjectId(-1) + , GeoId(-1) + , PartId(-1) + + , PrimitiveIndex(-1) + , PointIndex(-1) + , bLoaded(false) +{ +} + +FHoudiniPublicAPIOutputObjectIdentifier::FHoudiniPublicAPIOutputObjectIdentifier(const FHoudiniOutputObjectIdentifier& InIdentifier) + : SplitIdentifier() + , PartName() +{ + SetIdentifier(InIdentifier); +} + +void +FHoudiniPublicAPIOutputObjectIdentifier::SetIdentifier(const FHoudiniOutputObjectIdentifier& InIdentifier) +{ + ObjectId = InIdentifier.ObjectId; + GeoId = InIdentifier.GeoId; + PartId = InIdentifier.PartId; + SplitIdentifier = InIdentifier.SplitIdentifier; + PartName = InIdentifier.PartName; + PrimitiveIndex = InIdentifier.PrimitiveIndex; + PointIndex = InIdentifier.PointIndex; + bLoaded = InIdentifier.bLoaded; +} + +/** Returns the internal output object identifier wrapped by this class. */ +FHoudiniOutputObjectIdentifier +FHoudiniPublicAPIOutputObjectIdentifier::GetIdentifier() const +{ + FHoudiniOutputObjectIdentifier Identifier; + Identifier.ObjectId = ObjectId; + Identifier.GeoId = GeoId; + Identifier.PartId = PartId; + Identifier.SplitIdentifier = SplitIdentifier; + Identifier.PartName = PartName; + Identifier.PrimitiveIndex = PrimitiveIndex; + Identifier.PointIndex = PointIndex; + Identifier.bLoaded = bLoaded; + + return Identifier; +} + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniPublicAPIProcessHDANode.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPublicAPIProcessHDANode.h" + +#include "HoudiniPublicAPI.h" +#include "HoudiniPublicAPIBlueprintLib.h" +#include "HoudiniPublicAPIAssetWrapper.h" +#include "HoudiniPublicAPIInputTypes.h" + +#include "HoudiniEngineRuntimePrivatePCH.h" +#include + + +UHoudiniPublicAPIProcessHDANode::UHoudiniPublicAPIProcessHDANode(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + if ( HasAnyFlags(RF_ClassDefaultObject) == false ) + { + AddToRoot(); + } + + AssetWrapper = nullptr; + bCookSuccess = false; + bBakeSuccess = false; + + HoudiniAsset = nullptr; + InstantiateAt = FTransform::Identity; + WorldContextObject = nullptr; + SpawnInLevelOverride = nullptr; + bEnableAutoCook = true; + bEnableAutoBake = false; + BakeDirectoryPath = FString(); + BakeMethod = EHoudiniEngineBakeOption::ToActor; + bRemoveOutputAfterBake = false; + bRecenterBakedActors = false; + bReplacePreviousBake = false; + bDeleteInstantiatedAssetOnCompletionOrFailure = false; +} + +UHoudiniPublicAPIProcessHDANode* +UHoudiniPublicAPIProcessHDANode::ProcessHDA( + UHoudiniAsset* InHoudiniAsset, + const FTransform& InInstantiateAt, + const TMap& InParameters, + const TMap& InNodeInputs, + const TMap& InParameterInputs, + UObject* InWorldContextObject, + ULevel* InSpawnInLevelOverride, + const bool bInEnableAutoCook, + const bool bInEnableAutoBake, + const FString& InBakeDirectoryPath, + const EHoudiniEngineBakeOption InBakeMethod, + const bool bInRemoveOutputAfterBake, + const bool bInRecenterBakedActors, + const bool bInReplacePreviousBake, + const bool bInDeleteInstantiatedAssetOnCompletionOrFailure) +{ + UHoudiniPublicAPIProcessHDANode* Node = NewObject(); + + Node->HoudiniAsset = InHoudiniAsset; + Node->InstantiateAt = InInstantiateAt; + Node->Parameters = InParameters; + for (auto It : InNodeInputs) + Node->NodeInputs.Add(It.Key, It.Value); + for(auto It : InParameterInputs) + Node->ParameterInputs.Add(It.Key, It.Value); + Node->WorldContextObject = InWorldContextObject; + Node->SpawnInLevelOverride = InSpawnInLevelOverride; + Node->bEnableAutoCook = bInEnableAutoCook; + Node->bEnableAutoBake = bInEnableAutoBake; + Node->BakeDirectoryPath = InBakeDirectoryPath; + Node->BakeMethod = InBakeMethod; + Node->bRemoveOutputAfterBake = bInRemoveOutputAfterBake; + Node->bRecenterBakedActors = bInRecenterBakedActors; + Node->bReplacePreviousBake = bInReplacePreviousBake; + Node->bDeleteInstantiatedAssetOnCompletionOrFailure = bInDeleteInstantiatedAssetOnCompletionOrFailure; + + return Node; +} + + +void +UHoudiniPublicAPIProcessHDANode::Activate() +{ + UHoudiniPublicAPI* API = UHoudiniPublicAPIBlueprintLib::GetAPI(); + if (!IsValid(API)) + { + HandleFailure(); + return; + } + + AssetWrapper = UHoudiniPublicAPIAssetWrapper::CreateEmptyWrapper(API); + if (!IsValid(AssetWrapper)) + { + HandleFailure(); + return; + } + + AssetWrapper->GetOnPreInstantiationDelegate().AddDynamic(this, &UHoudiniPublicAPIProcessHDANode::HandlePreInstantiation); + AssetWrapper->GetOnPostInstantiationDelegate().AddDynamic(this, &UHoudiniPublicAPIProcessHDANode::HandlePostInstantiation); + AssetWrapper->GetOnPostCookDelegate().AddDynamic(this, &UHoudiniPublicAPIProcessHDANode::HandlePostAutoCook); + AssetWrapper->GetOnPreProcessStateExitedDelegate().AddDynamic(this, &UHoudiniPublicAPIProcessHDANode::HandlePreProcess); + AssetWrapper->GetOnPostProcessingDelegate().AddDynamic(this, &UHoudiniPublicAPIProcessHDANode::HandlePostProcessing); + AssetWrapper->GetOnPostBakeDelegate().AddDynamic(this, &UHoudiniPublicAPIProcessHDANode::HandlePostAutoBake); + + if (!API->InstantiateAssetWithExistingWrapper( + AssetWrapper, + HoudiniAsset, + InstantiateAt, + WorldContextObject, + SpawnInLevelOverride, + bEnableAutoCook, + bEnableAutoBake, + BakeDirectoryPath, + BakeMethod, + bRemoveOutputAfterBake, + bRecenterBakedActors, + bReplacePreviousBake)) + { + HandleFailure(); + return; + } +} + +void +UHoudiniPublicAPIProcessHDANode::UnbindDelegates() +{ + AssetWrapper->GetOnPreInstantiationDelegate().RemoveDynamic(this, &UHoudiniPublicAPIProcessHDANode::HandlePreInstantiation); + AssetWrapper->GetOnPostInstantiationDelegate().RemoveDynamic(this, &UHoudiniPublicAPIProcessHDANode::HandlePostInstantiation); + AssetWrapper->GetOnPostCookDelegate().RemoveDynamic(this, &UHoudiniPublicAPIProcessHDANode::HandlePostAutoCook); + AssetWrapper->GetOnPreProcessStateExitedDelegate().RemoveDynamic(this, &UHoudiniPublicAPIProcessHDANode::HandlePreProcess); + AssetWrapper->GetOnPostProcessingDelegate().RemoveDynamic(this, &UHoudiniPublicAPIProcessHDANode::HandlePostProcessing); + AssetWrapper->GetOnPostBakeDelegate().RemoveDynamic(this, &UHoudiniPublicAPIProcessHDANode::HandlePostAutoBake); +} + +void +UHoudiniPublicAPIProcessHDANode::HandleFailure() +{ + if (Failed.IsBound()) + Failed.Broadcast(AssetWrapper, bCookSuccess, bBakeSuccess); + + UnbindDelegates(); + + RemoveFromRoot(); + + if (bDeleteInstantiatedAssetOnCompletionOrFailure && IsValid(AssetWrapper)) + AssetWrapper->DeleteInstantiatedAsset(); +} + +void +UHoudiniPublicAPIProcessHDANode::HandleComplete() +{ + if (Completed.IsBound()) + Completed.Broadcast(AssetWrapper, bCookSuccess, bBakeSuccess); + + UnbindDelegates(); + + RemoveFromRoot(); + + if (bDeleteInstantiatedAssetOnCompletionOrFailure && IsValid(AssetWrapper)) + AssetWrapper->DeleteInstantiatedAsset(); +} + +void +UHoudiniPublicAPIProcessHDANode::HandlePreInstantiation(UHoudiniPublicAPIAssetWrapper* InAssetWrapper) +{ + if (InAssetWrapper != AssetWrapper) + { + HOUDINI_LOG_WARNING( + TEXT("[UHoudiniPublicAPIProcessHDANode] Received delegate event from unexpected asset wrapper (%s vs %s)!"), + IsValid(AssetWrapper) ? *(AssetWrapper->GetName()) : TEXT(""), + IsValid(InAssetWrapper) ? *(InAssetWrapper->GetName()) : TEXT("")); + return; + } + + // Set any parameters specified when the node was created + if (Parameters.Num() > 0 && IsValid(AssetWrapper)) + { + AssetWrapper->SetParameterTuples(Parameters); + } + + if (PreInstantiation.IsBound()) + PreInstantiation.Broadcast(AssetWrapper, bCookSuccess, bBakeSuccess); +} + +void +UHoudiniPublicAPIProcessHDANode::HandlePostInstantiation(UHoudiniPublicAPIAssetWrapper* InAssetWrapper) +{ + if (InAssetWrapper != AssetWrapper) + { + HOUDINI_LOG_WARNING( + TEXT("[UHoudiniPublicAPIProcessHDANode] Received delegate event from unexpected asset wrapper (%s vs %s)!"), + IsValid(AssetWrapper) ? *(AssetWrapper->GetName()) : TEXT(""), + IsValid(InAssetWrapper) ? *(InAssetWrapper->GetName()) : TEXT("")); + return; + } + + // Set any inputs specified when the node was created + if (IsValid(AssetWrapper)) + { + for (auto It : NodeInputs) + { + AssetWrapper->SetInputAtIndex(It.Key, It.Value); + } + + for(auto It : ParameterInputs) + { + AssetWrapper->SetInputParameter(It.Key, It.Value); + } + } + + if (PostInstantiation.IsBound()) + PostInstantiation.Broadcast(AssetWrapper, bCookSuccess, bBakeSuccess); + + if (!bEnableAutoCook) + HandleComplete(); +} + +void +UHoudiniPublicAPIProcessHDANode::HandlePostAutoCook(UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const bool bInCookSuccess) +{ + if (InAssetWrapper != AssetWrapper) + { + HOUDINI_LOG_WARNING( + TEXT("[UHoudiniPublicAPIProcessHDANode] Received delegate event from unexpected asset wrapper (%s vs %s)!"), + IsValid(AssetWrapper) ? *(AssetWrapper->GetName()) : TEXT(""), + IsValid(InAssetWrapper) ? *(InAssetWrapper->GetName()) : TEXT("")); + return; + } + + bCookSuccess = bInCookSuccess; + + if (PostAutoCook.IsBound()) + PostAutoCook.Broadcast(AssetWrapper, bCookSuccess, bBakeSuccess); +} + +void +UHoudiniPublicAPIProcessHDANode::HandlePreProcess(UHoudiniPublicAPIAssetWrapper* InAssetWrapper) +{ + if (InAssetWrapper != AssetWrapper) + { + HOUDINI_LOG_WARNING( + TEXT("[UHoudiniPublicAPIProcessHDANode] Received delegate event from unexpected asset wrapper (%s vs %s)!"), + IsValid(AssetWrapper) ? *(AssetWrapper->GetName()) : TEXT(""), + IsValid(InAssetWrapper) ? *(InAssetWrapper->GetName()) : TEXT("")); + return; + } + + if (PreProcess.IsBound()) + PreProcess.Broadcast(AssetWrapper, bCookSuccess, bBakeSuccess); +} + +void +UHoudiniPublicAPIProcessHDANode::HandlePostProcessing(UHoudiniPublicAPIAssetWrapper* InAssetWrapper) +{ + if (InAssetWrapper != AssetWrapper) + { + HOUDINI_LOG_WARNING( + TEXT("[UHoudiniPublicAPIProcessHDANode] Received delegate event from unexpected asset wrapper (%s vs %s)!"), + IsValid(AssetWrapper) ? *(AssetWrapper->GetName()) : TEXT(""), + IsValid(InAssetWrapper) ? *(InAssetWrapper->GetName()) : TEXT("")); + return; + } + + if (PostProcessing.IsBound()) + PostProcessing.Broadcast(AssetWrapper, bCookSuccess, bBakeSuccess); + + if (!bEnableAutoBake) + HandleComplete(); +} + +void +UHoudiniPublicAPIProcessHDANode::HandlePostAutoBake(UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const bool bInBakeSuccess) +{ + if (InAssetWrapper != AssetWrapper) + { + HOUDINI_LOG_WARNING( + TEXT("[UHoudiniPublicAPIProcessHDANode] Received delegate event from unexpected asset wrapper (%s vs %s)!"), + IsValid(AssetWrapper) ? *(AssetWrapper->GetName()) : TEXT(""), + IsValid(InAssetWrapper) ? *(InAssetWrapper->GetName()) : TEXT("")); + return; + } + + bBakeSuccess = bInBakeSuccess; + + if (PostAutoBake.IsBound()) + PostAutoBake.Broadcast(AssetWrapper, bCookSuccess, bBakeSuccess); + + HandleComplete(); +} + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniRampEditorWindowSubsystem.cpp`: + +```cpp +/* +* Copyright (c) <2024> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniRampEditorWindowSubsystem.h" + +#include "SHoudiniRampBase.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +void UHoudiniRampEditorWindowSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + +#if WITH_EDITOR + // Get the level editor module + FLevelEditorModule& LevelEditorModule = + FModuleManager::Get().GetModuleChecked("LevelEditor"); + + ActorSelectionChangedDelegateHandle = + LevelEditorModule.OnActorSelectionChanged().AddUObject( + this, &UHoudiniRampEditorWindowSubsystem::OnActorSelectionChanged); +#endif // WITH_EDITOR +} + +void +UHoudiniRampEditorWindowSubsystem::Deinitialize() +{ + DestroyAllEditorWindows(); +} + +void +UHoudiniRampEditorWindowSubsystem::DestroyAllEditorWindows() +{ + for (TWeakPtr& Window : Windows) + { + if (!Window.IsValid()) + { + continue; + } + + Window.Pin()->RequestDestroyWindow(); + } + + Windows.Empty(); + CurveEditors.Empty(); +} + +void +UHoudiniRampEditorWindowSubsystem::RefreshAllEditors() +{ + for (TWeakPtr& CurveEditor : CurveEditors) + { + if (!CurveEditor.IsValid()) + { + continue; + } + + CurveEditor.Pin()->RefreshCurveKeys(); + } +} + +void +UHoudiniRampEditorWindowSubsystem::OnActorSelectionChanged( + const TArray& NewSelection, bool bForceRefresh) +{ + // This is silly but we need a way to detect when the actor selection actually changes. This + // delegate is broadcasted during a force refresh of the details panel, even when the actor + // selection doesn't actually change! So we check ourselves if the selection has changed. + if (NewSelection == PreviousActorSelection) + { + // Since this is the same selection, don't destroy existing ramp curve editor windows, + // instead refresh them. + RefreshAllEditors(); + + return; + } + + DestroyAllEditorWindows(); + PreviousActorSelection = NewSelection; +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniRampEditorWindowSubsystem.h`: + +```h +/* +* Copyright (c) <2024> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "EditorSubsystem.h" +#include "Framework/Application/SlateApplication.h" +#include "Layout/WidgetPath.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/SWindow.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniRampEditorWindowSubsystem.generated.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +class IHoudiniRampCurveEditor; + +/** The purpose of this subsystem is to manage opening and closing of ramp editor windows. */ +UCLASS() +class UHoudiniRampEditorWindowSubsystem : public UEditorSubsystem +{ + GENERATED_BODY() + +public: + + DECLARE_DELEGATE(FOnValueCommitted); + + void Initialize(FSubsystemCollectionBase& Collection) override; + + void Deinitialize() override; + + /** Destroys all open curve editor windows. */ + void DestroyAllEditorWindows(); + + /** Refreshes all open curve editor windows with latest curve info. */ + void RefreshAllEditors(); + + /** + * Opens a ramp view in a resizable floating window. + * + * @tparam EditorWidgetType Widget to create in the window. Must have RampView argument which + * accepts RampViewType. Must implement IHoudiniRampCurveEditor. + * @tparam RampViewType Type used by EditorWidgetType to interface with the ramp state. + * + * @param RampView The ramp which this window will edit. + * @param ParentWidget The parent for the new float ramp curve editor window. + * @param OnValueCommittedDelegate Delegate to execute when ramp curve editor changes. + * + * @returns true if the window is created successfully, false otherwise. + */ + template + bool + OpenEditor( + TSharedPtr RampView, + TSharedPtr ParentWidget, + FOnValueCommitted OnValueCommittedDelegate) + { +#if WITH_EDITOR + + // A default window size for the ramp which looks nice + static const FVector2D DefaultWindowSize(800, 400); + + // Determine the position of the window so that it will spawn near the mouse, but not go off + // the screen. + FVector2D CursorPos = FSlateApplication::Get().GetCursorPos(); + + FSlateRect Anchor(CursorPos.X, CursorPos.Y, CursorPos.X, CursorPos.Y); + + constexpr bool bAutoAdjustForDPIScale = true; + FVector2D AdjustedSummonLocation = FSlateApplication::Get().CalculatePopupWindowPosition( + Anchor, + DefaultWindowSize, + bAutoAdjustForDPIScale, + FVector2D::ZeroVector, + Orient_Horizontal); + + TSharedRef WindowContent = SNew(SBorder) + .BorderImage(FAppStyle::Get().GetBrush("Brushes.Panel")) + .Padding(FMargin(8.0f, 8.0f)); + + TSharedRef Window = SNew(SWindow) + .AutoCenter(EAutoCenter::None) + .ScreenPosition(AdjustedSummonLocation) + .SupportsMaximize(false) + .SupportsMinimize(false) + .ClientSize(DefaultWindowSize) + .SizingRule(ESizingRule::UserSized) + .Title(LOCTEXT("WindowHeader", "Ramp Editor")) + [ + WindowContent + ]; + + TSharedRef CreatedCurveEditor = SNew(EditorWidgetType) + .RampView(RampView) + .OnCurveChanged_Lambda([=]() { OnValueCommittedDelegate.ExecuteIfBound(); }); + + WindowContent->SetContent(CreatedCurveEditor); + + if (ParentWidget.IsValid()) + { + // Find the window of the parent widget + FWidgetPath WidgetPath; + FSlateApplication::Get().GeneratePathToWidgetChecked(ParentWidget.ToSharedRef(), WidgetPath); + Window = FSlateApplication::Get().AddWindowAsNativeChild(Window, WidgetPath.GetWindow()); + } + else + { + Window = FSlateApplication::Get().AddWindow(Window); + } + + // Hold on to the window created for external use... + Windows.Add(Window); + CurveEditors.Add(CreatedCurveEditor); + + return true; +#endif // WITH_EDITOR + + return false; + } + +private: + + TArray> Windows; + TArray> CurveEditors; + + FDelegateHandle ActorSelectionChangedDelegateHandle; + + // Used to check if actor selection has changed! + // See note in definition of OnActorSelectionChanged + TArray PreviousActorSelection; + + void OnActorSelectionChanged(const TArray& NewSelection, bool bForceRefresh); +}; + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniRampView.h`: + +```h +/* +* Copyright (c) <2024> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineDetails.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineRuntimeCommon.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniAlgorithm.h" +#include "HoudiniParameterDetails.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterColor.h" +#include "HoudiniParameterChoice.h" + + +#include "HoudiniParameterRamp.h" + +/** + * The purpose of the ramp view is to provide a nicer way to interface with an array of ramp + * parameters, where the array represents a multi-selection of ramp parameters. + */ +template +class THoudiniRampViewBase +{ +public: + + using ValueType = InValueType; + using ParameterType = InParameterType; + using ParameterWeakPtr = TWeakObjectPtr; + using PointType = InPointType; + +private: + + TArray Parameters; + +public: + + explicit THoudiniRampViewBase(TArrayView Parameters) + : Parameters(Parameters) + { + } + + const TArray& + GetParameters() const + { + return Parameters; + } + + ParameterWeakPtr + GetMainParameter() const + { + if (Parameters.IsEmpty()) + { + return nullptr; + } + + return Parameters[0]; + } + + PointType* + GetRampPoint(const int32 Index) const + { + const ParameterWeakPtr MainParameter = GetMainParameter(); + if (!IsValidWeakPointer(MainParameter)) + { + return nullptr; + } + + const bool bIsCookingEnabled = FHoudiniEngine::Get().IsCookingEnabled(); + + const TArray& Points = (MainParameter->IsAutoUpdate() && bIsCookingEnabled) + ? MainParameter->Points + : MainParameter->CachedPoints; + + return Points.IsValidIndex(Index) ? Points[Index] : nullptr; + } + + /** Gets the number of points the ramp currently has. */ + int32 + GetPointCount() const + { + const bool bIsCookingEnabled = FHoudiniEngine::Get().IsCookingEnabled(); + + const ParameterWeakPtr MainParameter = GetMainParameter(); + if (!IsValidWeakPointer(MainParameter)) + { + return 0; + } + + if (MainParameter->IsAutoUpdate() && bIsCookingEnabled) + { + return MainParameter->Points.Num(); + } + else + { + return MainParameter->CachedPoints.Num(); + } + } + + bool + InsertRampPoint(const int32 Index) + { + const ParameterWeakPtr MainParameter = GetMainParameter(); + if (!IsValidWeakPointer(MainParameter)) + { + return false; + } + + const bool bIsCookingEnabled = FHoudiniEngine::Get().IsCookingEnabled(); + + float InsertPosition = 0.f; + ValueType InsertValue = Derived::DefaultInsertValue; + auto InsertInterp = EHoudiniRampInterpolationType::LINEAR; + + float PrevPosition = 0.0f; + float NextPosition = 1.0f; + + TArray>& CurrentPoints = MainParameter->Points; + TArray>& CachedPoints = MainParameter->CachedPoints; + + const int32 NumPoints = GetPointCount(); + + if (!MainParameter->IsAutoUpdate() || !bIsCookingEnabled) + { + MainParameter->SetCaching(true); + } + + if (Index >= NumPoints) + { + // Insert at the end + if (NumPoints > 0) + { + PointType* PrevPoint = nullptr; + if (MainParameter->IsAutoUpdate() && bIsCookingEnabled) + PrevPoint = CurrentPoints.Last(); + else + PrevPoint = CachedPoints.Last(); + + if (PrevPoint) + { + PrevPosition = PrevPoint->GetPosition(); + InsertInterp = PrevPoint->GetInterpolation(); + } + } + } + else if (Index <= 0) + { + // Insert at the beginning + if (NumPoints > 0) + { + PointType* NextPoint = nullptr; + if (MainParameter->IsAutoUpdate() && bIsCookingEnabled) + NextPoint = CurrentPoints[0]; + else + NextPoint = CachedPoints[0]; + + if (NextPoint) + { + NextPosition = NextPoint->GetPosition(); + InsertInterp = NextPoint->GetInterpolation(); + } + } + } + else + { + // Insert in the middle + if (NumPoints > 1) + { + PointType* PrevPoint = nullptr; + PointType* NextPoint = nullptr; + + if (MainParameter->IsAutoUpdate() && bIsCookingEnabled) + { + PrevPoint = CurrentPoints[Index - 1]; + NextPoint = CurrentPoints[Index]; + } + else + { + PrevPoint = CachedPoints[Index - 1]; + NextPoint = CachedPoints[Index]; + } + + if (PrevPoint) + { + PrevPosition = PrevPoint->GetPosition(); + InsertInterp = PrevPoint->GetInterpolation(); + } + + if (NextPoint) + { + NextPosition = NextPoint->GetPosition(); + } + + if (PrevPoint && NextPoint) + { + InsertValue = (PrevPoint->GetValue() + NextPoint->GetValue()) / 2.0; + } + } + } + + InsertPosition = (PrevPosition + NextPosition) / 2.0f; + + return InsertRampPoint(Index, InsertPosition, InsertValue, InsertInterp); + } + + bool + InsertRampPoint( + const int32 Index, + const float Position, + const ValueType Value, + const EHoudiniRampInterpolationType InterpolationType) + { + const bool bIsCookingEnabled = FHoudiniEngine::Get().IsCookingEnabled(); + + ReplaceAllParameterPointsWithMainParameter(); + + for (const ParameterWeakPtr Parameter : Parameters) + { + if (!IsValidWeakPointer(Parameter)) + continue; + + if (Parameter->IsAutoUpdate() && bIsCookingEnabled) + { + Parameter->CreateInsertEvent(Position, Value, InterpolationType); + Parameter->MarkChanged(true); + } + else + { + PointType* NewCachedPoint = NewObject(Parameter.Get(), PointType::StaticClass()); + NewCachedPoint->Position = Position; + NewCachedPoint->Value = Value; + NewCachedPoint->Interpolation = InterpolationType; + + Parameter->CachedPoints.Insert(NewCachedPoint, Index); + Parameter->SetCaching(true); + if (!bIsCookingEnabled) + { + // If cooking is not enabled, be sure to mark this parameter as changed + // so that it triggers an update once cooking is enabled again. + Parameter->MarkChanged(true); + } + } + } + + return true; + } + + bool + DeleteRampPoints(const TArrayView Indices) + { + const ParameterWeakPtr MainParameter = GetMainParameter(); + if (!IsValidWeakPointer(MainParameter)) + { + return false; + } + + const bool bIsCookingEnabled = FHoudiniEngine::Get().IsCookingEnabled(); + + ReplaceAllParameterPointsWithMainParameter(); + + for (const ParameterWeakPtr Parameter : Parameters) + { + if (!IsValidWeakPointer(Parameter)) + continue; + + if (Parameter->IsAutoUpdate() && bIsCookingEnabled) + { + if (Parameter->Points.Num() == 0) + return false; + + for (const int32 Index : Indices) + { + PointType* PointToDelete = nullptr; + + if (Index == -1) + PointToDelete = Parameter->Points.Last(); + else if (Parameter->Points.IsValidIndex(Index)) + PointToDelete = Parameter->Points[Index]; + + if (!PointToDelete) + return false; + + const int32 InstanceIndexToDelete = PointToDelete->InstanceIndex; + + Parameter->CreateDeleteEvent(InstanceIndexToDelete); + Parameter->MarkChanged(true); + } + } + else + { + if (Parameter->CachedPoints.Num() == 0) + { + return false; + } + + int32 OldPointCount = Parameter->CachedPoints.Num(); + + if (Indices.Contains(-1)) + { + Parameter->CachedPoints.Pop(); + } + + // The algorithm just shuffles the items to the front. Afterwards we need to remove + // the end of the array - otherwise we have items in undefined state at the end. + int32 NewPointCount = Algo::StableRemoveIfByIndex( + Parameter->CachedPoints, + [Indices](const int32 Index) -> bool { return Indices.Contains(Index); }); + + // Trim the end of the array + Parameter->CachedPoints.SetNum(NewPointCount); + + if (NewPointCount == OldPointCount) + { + return false; + } + + Parameter->SetCaching(true); + if (!bIsCookingEnabled) + Parameter->MarkChanged(true); + } + } + + return true; + } + + bool + DeleteRampPoint(const int32 Index) + { + return DeleteRampPoints(MakeArrayView(&Index, 1)); + } + + /** @returns Position if successful, unset optional if unsuccessful. */ + TOptional + GetRampPointPosition(const int32 Index) const + { + if (PointType* const Point = GetRampPoint(Index)) + { + return Point->Position; + } + + return {}; + } + + /** @returns Value if successful, unset optional if unsuccessful. */ + TOptional + GetRampPointValue(const int32 Index) const + { + if (PointType* const Point = GetRampPoint(Index)) + { + return Point->Value; + } + + return {}; + } + + /** @returns Interpolation if successful, unset optional if unsuccessful. */ + TOptional + GetRampPointInterpolationType(const int32 Index) const + { + if (PointType* const Point = GetRampPoint(Index)) + { + return Point->Interpolation; + } + + return {}; + } + +public: + + /** + * Sets position of individual ramp point. If setting multiple points, it is better to use + * @ref SetRampPoints as there is less overhead. + * + * @returns true if a change was made, false otherwise. + */ + bool + SetRampPointPosition(const int32 Index, const float NewPosition) + { + return SetRampPoints( + TArray({ Index }), + TArray({ TOptional(NewPosition) }), + TArray({ TOptional() }), + TArray({ TOptional() })); + } + + /** + * Sets value of individual ramp point. If setting multiple points, it is better to use + * @ref SetRampPoints as there is less overhead. + * + * @returns true if a change was made, false otherwise. + */ + bool + SetRampPointValue(const int32 Index, const ValueType NewValue) + { + return SetRampPoints( + TArray({ Index }), + TArray({ TOptional() }), + TArray({ TOptional(NewValue) }), + TArray({ TOptional() })); + } + + /** + * Sets interpolation of individual ramp point. If setting multiple points, it is better to use + * @ref SetRampPoints as there is less overhead. + * + * @returns true if a change was made, false otherwise. + */ + bool + SetRampPointInterpolationType( + const int32 Index, const EHoudiniRampInterpolationType NewInterpolationType) + { + return SetRampPoints( + TArray({ Index }), + TArray({ TOptional() }), + TArray({ TOptional() }), + TArray({ TOptional(NewInterpolationType) })); + } + + /** + * Sets data of multiple points. All input arrays must be of equal size. For each index, `i`, of + * the input arrays, the point at index `Indices[i]` will be updated with new position + * `NewPositions[i]`, new value `NewValues[i]`, and so on. Unset optional values are used to + * indicate that no change is desired. + * + * @returns true if at least one point was changed, false otherwise. + */ + bool + SetRampPoints( + const TArrayView Indices, + const TArrayView> NewPositions, + const TArrayView> NewValues, + const TArrayView> NewInterpolationTypes) + { + // All arrays must have same size. + if (Indices.Num() != NewPositions.Num() + || Indices.Num() != NewValues.Num() + || Indices.Num() != NewInterpolationTypes.Num()) + { + return false; + } + + if (Indices.IsEmpty()) + { + return false; + } + + const bool bIsCookingEnabled = FHoudiniEngine::Get().IsCookingEnabled(); + + bool bWasPointsChanged = false; + + ReplaceAllParameterPointsWithMainParameter(); + + for (int32 i = 0; i < Indices.Num(); ++i) + { + PointType* const MainPoint = GetRampPoint(Indices[i]); + + if (!MainPoint) + { + continue; + } + + // If nothing for this point has changed. + if ((!NewPositions[i].IsSet() || MainPoint->GetPosition() == NewPositions[i].GetValue()) + && (!NewValues[i].IsSet() || MainPoint->GetValue() == NewValues[i].GetValue()) + && (!NewInterpolationTypes[i].IsSet() + || MainPoint->GetInterpolation() == NewInterpolationTypes[i].GetValue())) + { + continue; + } + + bWasPointsChanged = true; + + for (const ParameterWeakPtr Parameter : Parameters) + { + if (!IsValidWeakPointer(Parameter)) + { + continue; + } + + if (Parameter->IsAutoUpdate() && bIsCookingEnabled) + { + if (Parameter->Points.IsValidIndex(Indices[i])) + { + PointType* const Point = Parameter->Points[Indices[i]]; + + if (!Point) + { + continue; + } + + if (NewPositions[i].IsSet()) + { + if (!Point->PositionParentParm) + { + continue; + } + Point->SetPosition(NewPositions[i].GetValue()); + Point->PositionParentParm->MarkChanged(true); + } + if (NewValues[i].IsSet()) + { + if (!Point->ValueParentParm) + { + continue; + } + Point->SetValue(NewValues[i].GetValue()); + Point->ValueParentParm->MarkChanged(true); + } + if (NewInterpolationTypes[i].IsSet()) + { + if (!Point->InterpolationParentParm) + { + continue; + } + Point->SetInterpolation(NewInterpolationTypes[i].GetValue()); + Point->InterpolationParentParm->MarkChanged(true); + } + } + else + { + int32 IdxInEventsArray = Indices[i] - Parameter->Points.Num(); + if (Parameter->ModificationEvents.IsValidIndex(IdxInEventsArray)) + { + UHoudiniParameterRampModificationEvent* const Event = + Parameter->ModificationEvents[IdxInEventsArray]; + + if (!Event) + { + continue; + } + + if (NewPositions[i].IsSet()) + { + Event->SetPosition(NewPositions[i].GetValue()); + } + if (NewValues[i].IsSet()) + { + Event->SetValue(NewValues[i].GetValue()); + } + if (NewInterpolationTypes[i].IsSet()) + { + Event->SetInterpolation(NewInterpolationTypes[i].GetValue()); + } + + } + } + } + else + { + if (Parameter->CachedPoints.IsValidIndex(Indices[i])) + { + PointType* const CachedPoint = Parameter->CachedPoints[Indices[i]]; + + if (!CachedPoint) + { + continue; + } + + if (NewPositions[i].IsSet()) + { + CachedPoint->Position = NewPositions[i].GetValue(); + } + if (NewValues[i].IsSet()) + { + CachedPoint->Value = NewValues[i].GetValue(); + } + if (NewInterpolationTypes[i].IsSet()) + { + CachedPoint->Interpolation = NewInterpolationTypes[i].GetValue(); + } + + Parameter->SetCaching(true); + } + } + } + } + return bWasPointsChanged; + } + + void + ReplaceAllParameterPointsWithMainParameter() + { + const ParameterWeakPtr MainParameter = GetMainParameter(); + + if (!IsValidWeakPointer(MainParameter)) + { + return; + } + + if (FHoudiniEngineUtils::IsHoudiniCookableCooking(MainParameter.Get())) + { + return; + } + + for (int32 Idx = 1; Idx < Parameters.Num(); ++Idx) + { + if (!IsValidWeakPointer(Parameters[Idx])) + { + continue; + } + + ReplaceParameterPointsWithMainParameter(MainParameter.Get(), Parameters[Idx].Get()); + } + } + +private: + + static void + ReplaceParameterPointsWithMainParameter(ParameterType* Param, ParameterType* MainParam) + { + if (!Param || !MainParam) + return; + + if (FHoudiniEngineUtils::IsHoudiniCookableCooking(Param)) + return; + + const bool bCookingEnabled = FHoudiniEngine::Get().IsCookingEnabled(); + + // Use Synced points if the MainParam is on auto update mode + // Use Cached points if the Mainparam is on manual update mode + + TArray>& MainPoints = (MainParam->IsAutoUpdate() && bCookingEnabled) + ? MainParam->Points + : MainParam->CachedPoints; + + if (Param->IsAutoUpdate() && bCookingEnabled) + { + TArray>& Points = Param->Points; + + int32 PointIdx = 0; + while (MainPoints.IsValidIndex(PointIdx) && Points.IsValidIndex(PointIdx)) + { + TObjectPtr& MainPoint = MainPoints[PointIdx]; + TObjectPtr& Point = Points[PointIdx]; + + if (!MainPoint || !Point) + continue; + + if (MainPoint->GetPosition() != Point->GetPosition()) + { + if (Point->PositionParentParm) + { + Point->SetPosition(MainPoint->GetPosition()); + Point->PositionParentParm->MarkChanged(true); + } + } + + if (MainPoint->GetValue() != Point->GetValue()) + { + if (Point->ValueParentParm) + { + Point->SetValue(MainPoint->GetValue()); + Point->ValueParentParm->MarkChanged(true); + } + } + + if (MainPoint->GetInterpolation() != Point->GetInterpolation()) + { + if (Point->InterpolationParentParm) + { + Point->SetInterpolation(MainPoint->GetInterpolation()); + Point->InterpolationParentParm->MarkChanged(true); + } + } + + PointIdx += 1; + } + + int32 PointInsertIdx = PointIdx; + int32 PointDeleteIdx = PointIdx; + + // skip the pending modification events + for (auto& Event : Param->ModificationEvents) + { + if (!Event) + continue; + + if (Event->IsInsertEvent()) + PointInsertIdx += 1; + + if (Event->IsDeleteEvent()) + PointDeleteIdx += 1; + } + + // There are more points in MainPoints array + for (; PointInsertIdx < MainPoints.Num(); ++PointInsertIdx) + { + TObjectPtr & NextMainPoint = MainPoints[PointInsertIdx]; + + if (!NextMainPoint) + continue; + + Param->CreateInsertEvent( + NextMainPoint->GetPosition(), + NextMainPoint->GetValue(), + NextMainPoint->GetInterpolation()); + + Param->MarkChanged(true); + } + + // There are more points in Points array + for (; PointDeleteIdx < Points.Num(); ++PointDeleteIdx) + { + TObjectPtr& NextPoint = Points[PointDeleteIdx]; + + if (!NextPoint) + continue; + + Param->CreateDeleteEvent(NextPoint->InstanceIndex); + + Param->MarkChanged(true); + } + + } + else + { + TArray>& Points = Param->CachedPoints; + + int32 PointIdx = 0; + while (MainPoints.IsValidIndex(PointIdx) && Points.IsValidIndex(PointIdx)) + { + TObjectPtr& MainPoint = MainPoints[PointIdx]; + TObjectPtr& Point = Points[PointIdx]; + + if (!MainPoint || !Point) + continue; + + if (Point->Position != MainPoint->Position) + { + Point->Position = MainPoint->Position; + Param->bCaching = true; + if (!bCookingEnabled) + { + if (Point->InterpolationParentParm) + Point->PositionParentParm->MarkChanged(true); + Param->MarkChanged(true); + } + } + + if (Point->Value != MainPoint->Value) + { + Point->Value = MainPoint->Value; + Param->bCaching = true; + if (!bCookingEnabled) + { + if (Point->ValueParentParm) + Point->ValueParentParm->MarkChanged(true); + Param->MarkChanged(true); + } + } + + if (Point->Interpolation != MainPoint->Interpolation) + { + Point->Interpolation = MainPoint->Interpolation; + Param->bCaching = true; + if (!bCookingEnabled) + { + if (Point->InterpolationParentParm) + Point->InterpolationParentParm->MarkChanged(true); + Param->MarkChanged(true); + } + } + + PointIdx += 1; + } + + // There are more points in MainPoints array + for (int32 MainPointsLeftIdx = PointIdx; MainPointsLeftIdx < MainPoints.Num(); ++MainPointsLeftIdx) + { + TObjectPtr NextMainPoint = MainPoints[MainPointsLeftIdx]; + + if (!NextMainPoint) + continue; + + PointType* NewCachedPoint = NewObject(Param, PointType::StaticClass()); + + if (!NewCachedPoint) + continue; + + NewCachedPoint->Position = NextMainPoint->GetPosition(); + NewCachedPoint->Value = NextMainPoint->GetValue(); + NewCachedPoint->Interpolation = NextMainPoint->GetInterpolation(); + + Points.Add(NewCachedPoint); + + Param->SetCaching(true); + } + + // there are more points in Points array + for (int32 PointsLeftIdx = PointIdx; PointIdx < Points.Num(); ++PointIdx) + { + Points.Pop(); + Param->SetCaching(true); + } + } + } +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniRuntimeSettingsDetails.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include "HoudiniRuntimeSettingsDetails.h" + +#include "HoudiniApi.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniEngineRuntimeUtils.h" + +#include "HAPI/HAPI_Version.h" + +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "Internationalization/Internationalization.h" +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "Widgets/Input/SNumericEntryBox.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +TSharedRef< IDetailCustomization > +FHoudiniRuntimeSettingsDetails::MakeInstance() +{ + return MakeShareable(new FHoudiniRuntimeSettingsDetails); +} + +FHoudiniRuntimeSettingsDetails::FHoudiniRuntimeSettingsDetails() +{} + +FHoudiniRuntimeSettingsDetails::~FHoudiniRuntimeSettingsDetails() +{} + +void +FHoudiniRuntimeSettingsDetails::CustomizeDetails(IDetailLayoutBuilder & DetailBuilder) +{ + // Create basic categories. + DetailBuilder.EditCategory("Session", FText::GetEmpty(), ECategoryPriority::Important); + DetailBuilder.EditCategory("Instantiating", FText::GetEmpty(), ECategoryPriority::Important); + DetailBuilder.EditCategory("Cooking", FText::GetEmpty(), ECategoryPriority::Important); + DetailBuilder.EditCategory("Parameters", FText::GetEmpty(), ECategoryPriority::Important); + DetailBuilder.EditCategory("CollisionGeneration", FText::GetEmpty(), ECategoryPriority::Important); + DetailBuilder.EditCategory("GeometryMarshalling", FText::GetEmpty(), ECategoryPriority::Important); + DetailBuilder.EditCategory("GeometryScalingAndImport", FText::GetEmpty(), ECategoryPriority::Important); + DetailBuilder.EditCategory("Static Mesh", FText::GetEmpty(), ECategoryPriority::Important); + DetailBuilder.EditCategory("GeneratedStaticMeshSettings", FText::GetEmpty(), ECategoryPriority::Important); + DetailBuilder.EditCategory("StaticMeshBuildSettings", FText::GetEmpty(), ECategoryPriority::Important); + DetailBuilder.EditCategory("PDG Settings", FText::GetEmpty(), ECategoryPriority::Important); + DetailBuilder.EditCategory("HoudiniLocation", FText::GetEmpty(), ECategoryPriority::Important); + + // Create Plugin Information category. + { + static const FName InformationCategory = TEXT("Plugin Information"); + IDetailCategoryBuilder & InformationCategoryBuilder = DetailBuilder.EditCategory(InformationCategory); + + // Add built Houdini version. + CreateHoudiniEntry( + LOCTEXT("HInformationBuilt", "Built against Houdini"), + InformationCategoryBuilder, HAPI_VERSION_HOUDINI_MAJOR, HAPI_VERSION_HOUDINI_MINOR, + HAPI_VERSION_HOUDINI_BUILD, HAPI_VERSION_HOUDINI_PATCH); + + // Add built against Houdini Engine version. + CreateHoudiniEngineEntry( + LOCTEXT("HEngineInformationBuilt", "Built against Houdini Engine"), + InformationCategoryBuilder, HAPI_VERSION_HOUDINI_ENGINE_MAJOR, + HAPI_VERSION_HOUDINI_ENGINE_MINOR, HAPI_VERSION_HOUDINI_ENGINE_API); + + // Add running against Houdini version. + { + int32 RunningMajor = 0; + int32 RunningMinor = 0; + int32 RunningBuild = 0; + int32 RunningPatch = 0; + + if (FHoudiniApi::IsHAPIInitialized()) + { + const HAPI_Session * Session = FHoudiniEngine::Get().GetSession(); + // Retrieve version numbers for running Houdini. + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_MAJOR, &RunningMajor); + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_MINOR, &RunningMinor); + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_BUILD, &RunningBuild); + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_PATCH, &RunningPatch); + } + + CreateHoudiniEntry( + LOCTEXT("HInformationRunning", "Running against Houdini"), + InformationCategoryBuilder, RunningMajor, RunningMinor, RunningBuild, RunningPatch); + } + + // Add running against Houdini Engine version. + { + int32 RunningEngineMajor = 0; + int32 RunningEngineMinor = 0; + int32 RunningEngineApi = 0; + + if (FHoudiniApi::IsHAPIInitialized()) + { + const HAPI_Session * Session = FHoudiniEngine::Get().GetSession(); + // Retrieve version numbers for running Houdini Engine. + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_ENGINE_MAJOR, &RunningEngineMajor); + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_ENGINE_MINOR, &RunningEngineMinor); + FHoudiniApi::GetEnvInt(HAPI_ENVINT_VERSION_HOUDINI_ENGINE_API, &RunningEngineApi); + } + + CreateHoudiniEngineEntry( + LOCTEXT("HEngineInformationRunning", "Running against Houdini Engine"), + InformationCategoryBuilder, RunningEngineMajor, RunningEngineMinor, RunningEngineApi); + } + + // Add path of libHAPI. + { + FString LibHAPILocation = FHoudiniEngine::Get().GetLibHAPILocation(); + if (LibHAPILocation.IsEmpty()) + LibHAPILocation = TEXT("Not Found"); + + CreateHAPILibraryPathEntry(LibHAPILocation, InformationCategoryBuilder); + } + + // Add licensing info. + { + FString HAPILicenseType = TEXT(""); + if (!FHoudiniEngineUtils::GetLicenseType(HAPILicenseType)) + HAPILicenseType = TEXT("Unknown"); + + CreateHAPILicenseEntry(HAPILicenseType, InformationCategoryBuilder); + } + } +} + +void +FHoudiniRuntimeSettingsDetails::CreateHoudiniEntry( + const FText & EntryName, + IDetailCategoryBuilder & DetailCategoryBuilder, + int32 VersionMajor, int32 VersionMinor, int32 VersionBuild, + int32 VersionPatch) +{ + FDetailWidgetRow & Row = DetailCategoryBuilder.AddCustomRow(FText::GetEmpty()); + + Row.NameWidget.Widget = + SNew(STextBlock) + .Text(EntryName) + .Font(IDetailLayoutBuilder::GetDetailFont()); + + TSharedRef< SHorizontalBox > HorizontalBox = SNew(SHorizontalBox); + + { + TSharedRef< SNumericEntryBox< int32 > > NumericEntryBox = SNew(SNumericEntryBox< int32 >); + HorizontalBox->AddSlot().Padding(0, 0, 5, 0) + [ + SAssignNew(NumericEntryBox, SNumericEntryBox< int32 >) + .AllowSpin(false) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Value(VersionMajor) + ]; + NumericEntryBox->SetEnabled(false); + } + + { + TSharedRef< SNumericEntryBox< int32 > > NumericEntryBox = SNew(SNumericEntryBox< int32 >); + HorizontalBox->AddSlot().Padding(0, 0, 5, 0) + [ + SAssignNew(NumericEntryBox, SNumericEntryBox< int32 >) + .AllowSpin(false) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Value(VersionMinor) + ]; + NumericEntryBox->SetEnabled(false); + } + + { + TSharedRef< SNumericEntryBox< int32 > > NumericEntryBox = SNew(SNumericEntryBox< int32 >); + HorizontalBox->AddSlot().Padding(0, 0, 5, 0) + [ + SAssignNew(NumericEntryBox, SNumericEntryBox< int32 >) + .AllowSpin(false) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Value(VersionBuild) + ]; + NumericEntryBox->SetEnabled(false); + } + + { + TSharedRef< SNumericEntryBox< int32 > > NumericEntryBox = SNew(SNumericEntryBox< int32 >); + HorizontalBox->AddSlot().Padding(0, 0, 5, 0) + [ + SAssignNew(NumericEntryBox, SNumericEntryBox< int32 >) + .AllowSpin(false) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Value(VersionPatch) + ]; + NumericEntryBox->SetEnabled(false); + } + + Row.ValueWidget.Widget = HorizontalBox; + Row.ValueWidget.MinDesiredWidth(HAPI_UNREAL_DESIRED_SETTINGS_ROW_VALUE_WIDGET_WIDTH); +} + +void +FHoudiniRuntimeSettingsDetails::CreateHoudiniEngineEntry( + const FText & EntryName, + IDetailCategoryBuilder & DetailCategoryBuilder, + int32 VersionMajor, int32 VersionMinor, int32 VersionApi) +{ + FDetailWidgetRow & Row = DetailCategoryBuilder.AddCustomRow(FText::GetEmpty()); + + Row.NameWidget.Widget = + SNew(STextBlock) + .Text(EntryName) + .Font(IDetailLayoutBuilder::GetDetailFont()); + + TSharedRef< SHorizontalBox > HorizontalBox = SNew(SHorizontalBox); + + { + TSharedRef< SNumericEntryBox< int32 > > NumericEntryBox = SNew(SNumericEntryBox< int32 >); + HorizontalBox->AddSlot().Padding(0, 0, 5, 0) + [ + SAssignNew(NumericEntryBox, SNumericEntryBox< int32 >) + .AllowSpin(false) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Value(VersionMajor) + ]; + NumericEntryBox->SetEnabled(false); + } + + { + TSharedRef< SNumericEntryBox< int32 > > NumericEntryBox = SNew(SNumericEntryBox< int32 >); + HorizontalBox->AddSlot().Padding(0, 0, 5, 0) + [ + SAssignNew(NumericEntryBox, SNumericEntryBox< int32 >) + .AllowSpin(false) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Value(VersionMinor) + ]; + NumericEntryBox->SetEnabled(false); + } + + { + TSharedRef< SNumericEntryBox< int32 > > NumericEntryBox = SNew(SNumericEntryBox< int32 >); + HorizontalBox->AddSlot().Padding(0, 0, 5, 0) + [ + SAssignNew(NumericEntryBox, SNumericEntryBox< int32 >) + .AllowSpin(false) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Value(VersionApi) + ]; + NumericEntryBox->SetEnabled(false); + } + + Row.ValueWidget.Widget = HorizontalBox; + Row.ValueWidget.MinDesiredWidth(HAPI_UNREAL_DESIRED_SETTINGS_ROW_VALUE_WIDGET_WIDTH); +} + +void +FHoudiniRuntimeSettingsDetails::CreateHAPILibraryPathEntry( + const FString & LibHAPIPath, + IDetailCategoryBuilder & DetailCategoryBuilder) +{ + FDetailWidgetRow & Row = DetailCategoryBuilder.AddCustomRow(FText::GetEmpty()); + + FString LibHAPIName = FString::Printf(TEXT("Location of %s"), *FHoudiniEngineRuntimeUtils::GetLibHAPIName()); + + Row.NameWidget.Widget = + SNew(STextBlock) + .Text(FText::FromString(LibHAPIName)) + .Font(IDetailLayoutBuilder::GetDetailFont()); + + TSharedRef TextBlock = + SNew(STextBlock) + .Text(FText::FromString(LibHAPIPath)) + .Font(IDetailLayoutBuilder::GetDetailFont()); + + TextBlock->SetEnabled(false); + Row.ValueWidget.Widget = TextBlock; + Row.ValueWidget.MinDesiredWidth(HAPI_UNREAL_DESIRED_SETTINGS_ROW_VALUE_WIDGET_WIDTH); +} + +void +FHoudiniRuntimeSettingsDetails::CreateHAPILicenseEntry( + const FString & LibHAPILicense, + IDetailCategoryBuilder & DetailCategoryBuilder) +{ + FDetailWidgetRow & Row = DetailCategoryBuilder.AddCustomRow(FText::GetEmpty()); + + FString LibHAPILicenseTypeText = TEXT("Acquired License Type"); + + Row.NameWidget.Widget = SNew(STextBlock) + .Text(FText::FromString(LibHAPILicenseTypeText)) + .Font(IDetailLayoutBuilder::GetDetailFont()); + + TSharedRef TextBlock = SNew(STextBlock) + .Text(FText::FromString(LibHAPILicense)) + .Font(IDetailLayoutBuilder::GetDetailFont()); + + TextBlock->SetEnabled(false); + Row.ValueWidget.Widget = TextBlock; + Row.ValueWidget.MinDesiredWidth(HAPI_UNREAL_DESIRED_SETTINGS_ROW_VALUE_WIDGET_WIDTH); +} + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngineEditor/Private/HoudiniRuntimeSettingsDetails.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "DetailCategoryBuilder.h" +#include "IDetailCustomization.h" + +class FHoudiniRuntimeSettingsDetails : public IDetailCustomization +{ +public: + + /** Constructor. **/ + FHoudiniRuntimeSettingsDetails(); + + /** Destructor. **/ + virtual ~FHoudiniRuntimeSettingsDetails(); + + /** IDetailCustomization methods. **/ +public: + + virtual void CustomizeDetails(IDetailLayoutBuilder & DetailBuilder) override; + +public: + + /** Create an instance of this detail layout class. **/ + static TSharedRef< IDetailCustomization > MakeInstance(); + +protected: + + /** Used to create Houdini version entry. **/ + void CreateHoudiniEntry( + const FText & EntryName, IDetailCategoryBuilder & DetailCategoryBuilder, + int32 VersionMajor, int32 VersionMinor, int32 VersionBuild, int32 VersionPatch); + + /** Used to create Houdini Engine version entry. **/ + void CreateHoudiniEngineEntry( + const FText & EntryName, IDetailCategoryBuilder & DetailCategoryBuilder, + int32 VersionMajor, int32 VersionMinor, int32 VersionApi); + + /** Used to create libHAPI dynamic library path entry. **/ + void CreateHAPILibraryPathEntry( + const FString & LibHAPIPath, IDetailCategoryBuilder & DetailCategoryBuilder); + + /** Used to create libHAPI license information entry. **/ + void CreateHAPILicenseEntry( + const FString & LibHAPILicense, IDetailCategoryBuilder & DetailCategoryBuilder); +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniSplineComponentVisualizer.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniSplineComponentVisualizer.h" + +#include "ActorEditorUtils.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniApi.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniSplineComponent.h" +#include "HoudiniInputObject.h" +#include "HoudiniInput.h" +#include "HoudiniEngineStyle.h" +#include "HoudiniEngineUtils.h" + +#include "Editor/UnrealEdEngine.h" +#include "UnrealEdGlobals.h" +#include "ComponentVisualizerManager.h" + +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "ScopedTransaction.h" +#include "EditorViewportClient.h" +#include "Engine/Selection.h" +#include "HModel.h" +#include "SceneView.h" +#include "Framework/Application/SlateApplication.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +IMPLEMENT_HIT_PROXY(HHoudiniSplineVisProxy, HComponentVisProxy); +IMPLEMENT_HIT_PROXY(HHoudiniSplineControlPointVisProxy, HHoudiniSplineVisProxy); +IMPLEMENT_HIT_PROXY(HHoudiniSplineCurveSegmentVisProxy, HHoudiniSplineVisProxy); + +FHoudiniSplineComponentVisualizerCommands::FHoudiniSplineComponentVisualizerCommands() + : TCommands< FHoudiniSplineComponentVisualizerCommands >( + "HoudiniSplineComponentVisualizer", + LOCTEXT("HoudiniSplineComponentVisualizer", "Houdini Spline Component Visualizer"), + NAME_None, + _GetEditorStyle().GetStyleSetName()) +{} + +void +FHoudiniSplineComponentVisualizerCommands::RegisterCommands() +{ + UI_COMMAND( + CommandAddControlPoint, "Add Control Point", "Add control points.", + EUserInterfaceActionType::Button, FInputChord()); + + UI_COMMAND( + CommandDuplicateControlPoint, "Duplicate Control Point", "Duplicate control points.", + EUserInterfaceActionType::Button, FInputChord()); + + UI_COMMAND( + CommandDeleteControlPoint, "Delete Control Point", "delete control points.", + EUserInterfaceActionType::Button, FInputChord(EKeys::Delete)); + + UI_COMMAND(CommandDeselectAllControlPoints, "Deselect All", "Deselect all control points.", + EUserInterfaceActionType::Button, FInputChord()); + + UI_COMMAND(CommandInsertControlPoint, "Insert Control Point", "Insert a control point on curve.", + EUserInterfaceActionType::Button, FInputChord()); +} + + +FHoudiniSplineComponentVisualizer::FHoudiniSplineComponentVisualizer() + :FComponentVisualizer() + ,bAllowDuplication(false) + ,EditedCurveSegmentIndex(-1) + ,CachedRotation(FQuat::Identity) + ,CachedScale3D(FVector::OneVector) + ,bMovingPoints(false) + ,bInsertingOnCurveControlPoints(false) + ,bRecordingMovingPoints(false) +{ + FHoudiniSplineComponentVisualizerCommands::Register(); + VisualizerActions = MakeShareable(new FUICommandList); +} + +void +FHoudiniSplineComponentVisualizer::OnRegister() +{ + HOUDINI_LOG_MESSAGE(TEXT("Houdini Spline Component Visualizer Registered!")); + const auto & Commands = FHoudiniSplineComponentVisualizerCommands::Get(); + + VisualizerActions->MapAction( + Commands.CommandAddControlPoint, + FExecuteAction::CreateSP(this, &FHoudiniSplineComponentVisualizer::OnAddControlPoint), + FCanExecuteAction::CreateSP(this, &FHoudiniSplineComponentVisualizer::IsAddControlPointValid)); + + VisualizerActions->MapAction( + Commands.CommandDuplicateControlPoint, + FExecuteAction::CreateSP(this, &FHoudiniSplineComponentVisualizer::OnDuplicateControlPoint), + FCanExecuteAction::CreateSP(this, &FHoudiniSplineComponentVisualizer::IsDuplicateControlPointValid)); + + VisualizerActions->MapAction( + Commands.CommandDeleteControlPoint, + FExecuteAction::CreateSP(this, &FHoudiniSplineComponentVisualizer::OnDeleteControlPoint), + FCanExecuteAction::CreateSP(this, &FHoudiniSplineComponentVisualizer::IsDeleteControlPointValid)); + + VisualizerActions->MapAction(Commands.CommandDeselectAllControlPoints, + FExecuteAction::CreateSP(this, &FHoudiniSplineComponentVisualizer::OnDeselectAllControlPoints), + FCanExecuteAction::CreateSP(this, &FHoudiniSplineComponentVisualizer::IsDeselectAllControlPointsValid)); + + VisualizerActions->MapAction(Commands.CommandInsertControlPoint, + FExecuteAction::CreateSP(this, &FHoudiniSplineComponentVisualizer::OnInsertControlPoint), + FCanExecuteAction::CreateSP(this, &FHoudiniSplineComponentVisualizer::IsInsertControlPointValid)); +} + + +void +FHoudiniSplineComponentVisualizer::DrawVisualization( + const UActorComponent * Component, + const FSceneView * View, + FPrimitiveDrawInterface * PDI) +{ + const UHoudiniSplineComponent * HoudiniSplineComponent = Cast< const UHoudiniSplineComponent >(Component); + + if (!IsValid(HoudiniSplineComponent) + || !PDI + || !HoudiniSplineComponent->IsHoudiniSplineVisible()) + return; + + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + + // Note: Undo a transaction clears the active visualizer in ComponnetVisMangaer, which is private to Visualizer manager. + // HandleProxyForComponentVis() sets the active visualizer. So the selection will be lost after undo. + + // A Way to bypass this annoying UE4 implementation: + // If the drawing spline is the one being edited and an undo just happened, + // force to trigger a 'bubble' hit proxy to re-activate the visualizer. + if (HoudiniSplineComponent == EditedHoudiniSplineComponent && EditedHoudiniSplineComponent->bPostUndo) + { + EditedHoudiniSplineComponent->bPostUndo = false; + + FEditorViewportClient * FoundViewportClient = FindViewportClient(EditedHoudiniSplineComponent, View); + HComponentVisProxy * BubbleComponentHitProxy = new HComponentVisProxy(EditedHoudiniSplineComponent); + + if (FoundViewportClient && BubbleComponentHitProxy) + { + FViewportClick BubbleClick(View, FoundViewportClient, FKey(), EInputEvent::IE_Axis, 0, 0); + GUnrealEd->ComponentVisManager.HandleProxyForComponentVis(FoundViewportClient, BubbleComponentHitProxy, BubbleClick); + } + } + + static const FColor ColorNormal = FColor(255.f, 255.f, 255.f); + static const FColor ColorNormalHandleFirst(172.f, 255.f, 172.f); + static const FColor ColorNormalHandleSecond(254.f, 216.f, 177.f); + + static const FColor ColorSelectedHandle(255.f, 0.f, 0.f); + static const FColor ColorSelectedHandleFirst(0.f, 192.f, 0.f); + static const FColor ColorSelectedHandleSecond(255.f, 159.f, 0.f); + + static const float SizeGrabHandleSelected = 15.f; + static const float SizeGrabHandleNormalLarge = 18.f; + static const float SizeGrabHandleNormalSmall = 12.f; + + FVector3d PreviousPosition; + if (HoudiniSplineComponent) + { + const FTransform& HoudiniSplineComponentTransform = HoudiniSplineComponent->GetComponentTransform(); + + const TArray & DisplayPoints = HoudiniSplineComponent->DisplayPoints; // not used yet + const TArray & CurvePoints = HoudiniSplineComponent->CurvePoints; + + // Draw display points (simply linearly connect the control points for temporary) + for (int32 Index = 0; Index < DisplayPoints.Num(); ++Index) + { + const FVector3d& CurrentPoint = DisplayPoints[Index]; + // Fix incorrect scale when actor has been scaled + //FVector CurrentPosition = CurrentPoint + HoudiniSplineComponentTransform.GetLocation(); + FVector3d CurrentPosition = HoudiniSplineComponentTransform.TransformPosition(CurrentPoint); + if (Index > 0) + { + // Add a hitproxy for the line segment + PDI->SetHitProxy(new HHoudiniSplineCurveSegmentVisProxy(HoudiniSplineComponent, Index)); + // Draw a line connecting the previous point and the current point + PDI->DrawLine(PreviousPosition, CurrentPosition, ColorNormal, SDPG_Foreground); + PDI->SetHitProxy(nullptr); + } + + PreviousPosition = CurrentPosition; + } + + // Draw control points (do not draw control points if the curve is an output) + if (!HoudiniSplineComponent->bIsOutputCurve) + { + for (int32 Index = 0; Index < CurvePoints.Num(); ++Index) + { + const FVector3d& ControlPoint = HoudiniSplineComponentTransform.TransformPosition(CurvePoints[Index].GetLocation()); + + HHoudiniSplineControlPointVisProxy* HitProxy = new HHoudiniSplineControlPointVisProxy(HoudiniSplineComponent, Index); + PDI->SetHitProxy(HitProxy); + + FColor DrawColor = ColorNormal; + float DrawSize = SizeGrabHandleNormalSmall; + + if (Index == 0) + { + DrawColor = ColorNormalHandleFirst; + DrawSize = SizeGrabHandleNormalLarge; + } + else if (Index == 1) + { + DrawColor = ColorNormalHandleSecond; + } + + // If this is an point that being editted + if (EditedHoudiniSplineComponent == HoudiniSplineComponent && EditedHoudiniSplineComponent->EditedControlPointsIndexes.Contains(Index)) + { + if (Index == 0) + { + DrawColor = ColorSelectedHandleFirst; + } + else if (Index == 1) + { + DrawColor = ColorSelectedHandleSecond; + DrawSize = SizeGrabHandleSelected; + } + else + { + DrawColor = ColorSelectedHandle; + DrawSize = SizeGrabHandleSelected; + + } + } + + PDI->DrawPoint(ControlPoint, DrawColor, DrawSize, SDPG_Foreground); + PDI->SetHitProxy(nullptr); + } + + } + } +} + + +bool +FHoudiniSplineComponentVisualizer::VisProxyHandleClick( + FEditorViewportClient* InViewportClient, + HComponentVisProxy* VisProxy, + const FViewportClick& Click) +{ + if (!InViewportClient || !VisProxy || !VisProxy->Component.IsValid()) + return false; + + const UHoudiniSplineComponent * HoudiniSplineComponent = CastChecked< const UHoudiniSplineComponent >(VisProxy->Component.Get()); + + AActor* OldSplineOwningActor = SplinePropertyPath.GetParentOwningActor(); + SplinePropertyPath = FComponentPropertyPath(HoudiniSplineComponent); + AActor* NewSplineOwningActor = SplinePropertyPath.GetParentOwningActor(); + + if (!SplinePropertyPath.IsValid()) + { + SplinePropertyPath.Reset(); + return false; + } + + if (OldSplineOwningActor != NewSplineOwningActor) + { + // Reset selection state if we are selecting a different actor to the one previously selected + EditedCurveSegmentIndex = INDEX_NONE; + } + + // Note: This is for re-activating the component visualizer an undo. + // Return true if the hit proxy is a bubble (Neither HHoudiniSplineControlPointVisProxy nor HHoudiniSplineCurveSegmentVisProxy ) + // + if (!VisProxy->IsA(HHoudiniSplineControlPointVisProxy::StaticGetType()) && !VisProxy->IsA(HHoudiniSplineCurveSegmentVisProxy::StaticGetType())) + return true; + + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + + EditedHoudiniSplineComponent = const_cast(HoudiniSplineComponent); + + if (!IsValid(EditedHoudiniSplineComponent)) + return false; + + + TArray& EditedControlPointsIndexes = EditedHoudiniSplineComponent->EditedControlPointsIndexes; + + bool editingCurve = false; + + // If VisProxy is a HHoudiniSplineControlPointVisProxy + if (VisProxy->IsA(HHoudiniSplineControlPointVisProxy::StaticGetType())) + { + HHoudiniSplineControlPointVisProxy * ControlPointProxy = (HHoudiniSplineControlPointVisProxy*)VisProxy; + + if (!ControlPointProxy) + return editingCurve; + + editingCurve = true; + + // Clear the edited curve segment if a control point is clicked. + EditedCurveSegmentIndex = -1; + + if (Click.GetKey() != EKeys::LeftMouseButton) + return editingCurve; + + + if (InViewportClient->IsCtrlPressed()) + { + if (EditedControlPointsIndexes.Contains(ControlPointProxy->ControlPointIndex)) + { + EditedControlPointsIndexes.Remove(ControlPointProxy->ControlPointIndex); + } + else + { + EditedControlPointsIndexes.Add(ControlPointProxy->ControlPointIndex); + } + } + else + { + EditedControlPointsIndexes.Empty(); + EditedControlPointsIndexes.Add(ControlPointProxy->ControlPointIndex); + } + } + // VisProxy is a HHoudiniSplineCurveSegmentProxy + else if (VisProxy->IsA(HHoudiniSplineCurveSegmentVisProxy::StaticGetType())) + { + //HHoudiniSplineCurveSegmentVisProxy * CurveSegmentProxy = Cast(VisProxy); + + HHoudiniSplineCurveSegmentVisProxy * CurveSegmentProxy = (HHoudiniSplineCurveSegmentVisProxy*)(VisProxy); + + if (!CurveSegmentProxy) + return false; + + editingCurve = true; + + if (Click.GetKey() == EKeys::LeftMouseButton && InViewportClient->IsAltPressed() && EditedHoudiniSplineComponent) + { + // Continuesly (Alt) inserting on-curve control points is only valid with Breakpoints mode, otherwise it has to be on linear curve type. + if (EditedHoudiniSplineComponent->CurveType != EHoudiniCurveType::Polygon && EditedHoudiniSplineComponent->CurveMethod != EHoudiniCurveMethod::Breakpoints) + return editingCurve; + + bInsertingOnCurveControlPoints = true; + + editingCurve = true; + EditedControlPointsIndexes.Empty(); + + EditedCurveSegmentIndex = CurveSegmentProxy->DisplayPointIndex; + int32 InsertedIndex = OnInsertControlPointWithoutUpdate(); + + if (InsertedIndex < 0) return false; + EditedControlPointsIndexes.Add(InsertedIndex); + + EditedCurveSegmentIndex = -1; + bInsertingOnCurveControlPoints = true; + + // Refresh viewport, but don't invalidate hit proxies otherwise it will error + RefreshViewport(false); + } + // Insert one on-curve control point. + else + { + EditedCurveSegmentIndex = CurveSegmentProxy->DisplayPointIndex; + return editingCurve; + } + } + + return editingCurve; +} + +bool +FHoudiniSplineComponentVisualizer::HandleInputKey(FEditorViewportClient * ViewportClient, FViewport * Viewport, FKey Key, EInputEvent Event) +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!IsValid(EditedHoudiniSplineComponent)) + return false; + + if (Key == EKeys::Enter) + { + EditedHoudiniSplineComponent->MarkChanged(true); + + return true; + } + + bool bHandled = false; + + if (Key == EKeys::LeftMouseButton) + { + if (Event == IE_Pressed) + { + bMovingPoints = true; // Started moving points when the left mouse button is pressed + bAllowDuplication = true; + bRecordingMovingPoints = false; + } + + if (Event == IE_Released) + { + bMovingPoints = false; // Stopped moving points when the left mouse button is released + bAllowDuplication = false; + + if (bRecordingMovingPoints) + { + // Only mark the component as changed if a point was actually moved otherwise it will + // cook even if a point was selected. + if (IsCookOnCurveChanged(EditedHoudiniSplineComponent)) + EditedHoudiniSplineComponent->MarkChanged(true); + } + + bRecordingMovingPoints = false; // allow recording pt moving again + } + } + + + if (Key == EKeys::Delete) + { + if (Event == IE_Pressed) return true; + + if (IsDeleteControlPointValid()) + { + OnDeleteControlPoint(); + + if (IsCookOnCurveChanged(EditedHoudiniSplineComponent)) + EditedHoudiniSplineComponent->MarkChanged(true); + return true; + } + } + + + if (Event == IE_Pressed && VisualizerActions) + { + if (FSlateApplication::IsInitialized()) + bHandled = VisualizerActions->ProcessCommandBindings(Key, FSlateApplication::Get().GetModifierKeys(), false); + } + + RefreshViewport(); + + return bHandled; +} + +void +FHoudiniSplineComponentVisualizer::EndEditing() +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!IsValid(EditedHoudiniSplineComponent)) + return; + + // Clear edited spline if the EndEditing() function is not called from postUndo + if (!EditedHoudiniSplineComponent->bPostUndo) + { + EditedHoudiniSplineComponent->EditedControlPointsIndexes.Empty(); + + EditedHoudiniSplineComponent = nullptr; + EditedCurveSegmentIndex = -1; + } + + //RefreshViewport(); +} + +bool +FHoudiniSplineComponentVisualizer::GetWidgetLocation( + const FEditorViewportClient* ViewportClient, + FVector& OutLocation) const +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!IsValid(EditedHoudiniSplineComponent)) + return false; + + TArray& EditedControlPointsIndexes = EditedHoudiniSplineComponent->EditedControlPointsIndexes; + if (EditedControlPointsIndexes.Num() <= 0) + return false; + + const TArray& CurvePoints = EditedHoudiniSplineComponent->CurvePoints; + + // Set the widget location to the center of mass of the selected control points + int32 Sum = 0; + FVector CenterLocation = FVector::ZeroVector; + for (int32 EditedIdx = 0; EditedIdx < EditedControlPointsIndexes.Num(); EditedIdx++) + { + if (!CurvePoints.IsValidIndex(EditedIdx)) + continue; + + if (EditedControlPointsIndexes[EditedIdx] >= CurvePoints.Num()) + continue; + + CenterLocation += CurvePoints[EditedControlPointsIndexes[EditedIdx]].GetLocation(); + Sum++; + } + + if(Sum > 0) + CenterLocation /= Sum; + + OutLocation = EditedHoudiniSplineComponent->GetComponentTransform().TransformPosition(CenterLocation); + + return true; +} + +bool +FHoudiniSplineComponentVisualizer::IsVisualizingArchetype() const +{ + UHoudiniSplineComponent* SplineComp = GetEditedHoudiniSplineComponent(); + return (SplineComp && SplineComp->GetOwner() && FActorEditorUtils::IsAPreviewOrInactiveActor(SplineComp->GetOwner())); +} + +bool +FHoudiniSplineComponentVisualizer::HandleInputDelta( + FEditorViewportClient* ViewportClient, + FViewport* Viewport, + FVector& DeltaTranslate, + FRotator& DeltaRotate, + FVector& DeltaScale) +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!ViewportClient || !IsValid(EditedHoudiniSplineComponent)) + return false; + + TArray & EditedControlPointsIndexes = EditedHoudiniSplineComponent->EditedControlPointsIndexes; + + if (EditedControlPointsIndexes.Num() <= 0) + return false; + + if (ViewportClient->IsAltPressed() && bAllowDuplication) + { + OnDuplicateControlPoint(); + bAllowDuplication = false; + } + else + { + if (!bRecordingMovingPoints) + { + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniSplineComponentMovingPointsTransaction", "Houdini Spline Component: Moving curve points."), + EditedHoudiniSplineComponent->GetOuter(), true); + + EditedHoudiniSplineComponent->Modify(); + + bRecordingMovingPoints = true; + } + } + + + TArray & CurvePoints = EditedHoudiniSplineComponent->CurvePoints; + + const FTransform & HoudiniSplineComponentTransform = EditedHoudiniSplineComponent->GetComponentTransform(); + + for (int i = 0; i < EditedControlPointsIndexes.Num(); ++i ) + { + + FTransform CurrentPoint = EditedHoudiniSplineComponent->CurvePoints[EditedControlPointsIndexes[i]]; + + if (!DeltaTranslate.IsZero()) + { + FVector OldWorldPosition = HoudiniSplineComponentTransform.TransformPosition(CurrentPoint.GetLocation()); + FVector NewWorldPosition = OldWorldPosition + DeltaTranslate; + FVector NewLocalPosition = HoudiniSplineComponentTransform.InverseTransformPosition(NewWorldPosition); + CurrentPoint.SetLocation( NewLocalPosition ); + } + + if (!DeltaRotate.IsZero()) + { + FQuat OldWorldRotation = HoudiniSplineComponentTransform.GetRotation() * CurrentPoint.GetRotation(); + FQuat NewWorldRotation = DeltaRotate.Quaternion() * OldWorldRotation; + FQuat NewLocalRotation = HoudiniSplineComponentTransform.GetRotation().Inverse() * NewWorldRotation; + CurrentPoint.SetRotation(NewLocalRotation); + } + + if (!DeltaScale.IsZero()) + { + FVector NewScale = CurrentPoint.GetScale3D() * (FVector(1.f, 1.f, 1.f) + DeltaScale); + CurrentPoint.SetScale3D(NewScale); + } + + + EditedHoudiniSplineComponent->EditPointAtindex(CurrentPoint, EditedControlPointsIndexes[i]); + } + + RefreshViewport(); + + return true; +} + +TSharedPtr +FHoudiniSplineComponentVisualizer::GenerateContextMenu() const +{ + FHoudiniEngineEditor& HoudiniEngineEditor = FHoudiniEngineEditor::Get(); + FName StyleSetName = FHoudiniEngineStyle::GetStyleSetName(); + + FMenuBuilder MenuBuilder(true, VisualizerActions); + MenuBuilder.BeginSection("Houdini Spline actions"); + + // Create the context menu section + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (IsValid(EditedHoudiniSplineComponent)) + { + MenuBuilder.AddMenuEntry( + FHoudiniSplineComponentVisualizerCommands::Get().CommandAddControlPoint, + NAME_None, TAttribute(), TAttribute(), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo")); + + MenuBuilder.AddMenuEntry( + FHoudiniSplineComponentVisualizerCommands::Get().CommandDuplicateControlPoint, + NAME_None, TAttribute< FText >(), TAttribute< FText >(), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo")); + + MenuBuilder.AddMenuEntry( + FHoudiniSplineComponentVisualizerCommands::Get().CommandDeleteControlPoint, + NAME_None, TAttribute< FText >(), TAttribute< FText >(), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo")); + + MenuBuilder.AddMenuEntry( + FHoudiniSplineComponentVisualizerCommands::Get().CommandDeselectAllControlPoints, + NAME_None, TAttribute< FText >(), TAttribute< FText >(), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo")); + + MenuBuilder.AddMenuEntry( + FHoudiniSplineComponentVisualizerCommands::Get().CommandInsertControlPoint, + NAME_None, TAttribute< FText >(), TAttribute< FText >(), + FSlateIcon(StyleSetName, "HoudiniEngine.HoudiniEngineLogo")); + } + + MenuBuilder.EndSection(); + TSharedPtr MenuWidget = MenuBuilder.MakeWidget(); + return MenuWidget; +} + +// Used by alt-pressed on-curve control port insertion. +// We don't want it to be cooked before finishing editing. +// * Need to call WaitForHoudiniInputUpdate() after done. +int32 +FHoudiniSplineComponentVisualizer::OnInsertControlPointWithoutUpdate() +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!IsValid(EditedHoudiniSplineComponent)) + return -1; + + TArray& CurvePoints = EditedHoudiniSplineComponent->CurvePoints; + TArray& DisplayPoints = EditedHoudiniSplineComponent->DisplayPoints; + + if (EditedCurveSegmentIndex >= DisplayPoints.Num()) + return -1; + + // ... // + int InsertAfterIndex = 0; + + TArray & DisplayPointIndexDivider = EditedHoudiniSplineComponent->DisplayPointIndexDivider; + for (int itr = 0; itr < DisplayPointIndexDivider.Num(); ++itr) + { + if (DisplayPointIndexDivider[itr] >= EditedCurveSegmentIndex) + { + InsertAfterIndex = itr; + break; + } + } + // ... // + + if (InsertAfterIndex >= CurvePoints.Num()) return -1; + + FTransform NewPoint = CurvePoints[InsertAfterIndex]; + NewPoint.SetLocation(DisplayPoints[EditedCurveSegmentIndex]); + // To Do: Should interpolate the rotation and scale as well here. + // ... + + // Insert new control point on curve, and add it to selected CP. + int32 NewPointIndex = AddControlPointAfter(NewPoint, InsertAfterIndex); + + // Don't have to reconstruct the index divider each time. + //EditedHoudiniSplineComponent->Construct(EditedHoudiniSplineComponent->DisplayPoints); + EditedHoudiniSplineComponent->DisplayPointIndexDivider.Insert(EditedCurveSegmentIndex, InsertAfterIndex); + + if (IsCookOnCurveChanged(EditedHoudiniSplineComponent)) + EditedHoudiniSplineComponent->MarkChanged(true); + + return NewPointIndex; +} + +void +FHoudiniSplineComponentVisualizer::OnInsertControlPoint() +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!IsValid(EditedHoudiniSplineComponent)) + return; + + int32 NewPointIndex = OnInsertControlPointWithoutUpdate(); + + if (NewPointIndex < 0) return; + + + EditedHoudiniSplineComponent->EditedControlPointsIndexes.Add(NewPointIndex); + + RefreshViewport(); + + if (IsCookOnCurveChanged(EditedHoudiniSplineComponent)) + EditedHoudiniSplineComponent->MarkChanged(true); +} + +bool +FHoudiniSplineComponentVisualizer::IsInsertControlPointValid() const +{ + return EditedCurveSegmentIndex >= 0; +} + +void +FHoudiniSplineComponentVisualizer::OnAddControlPoint() +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!IsValid(EditedHoudiniSplineComponent)) + return; + + TArray & EditedControlPointsIndexes = EditedHoudiniSplineComponent->EditedControlPointsIndexes; + + // Transaction for Undo/Redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniSplineComponentInsertingPointsTransaction", "Houdini Spline Component: Inserting curve points."), + EditedHoudiniSplineComponent->GetOuter(), true); + + EditedHoudiniSplineComponent->Modify(); + + EditedControlPointsIndexes.Sort(); + + const TArray & CurvePoints = EditedHoudiniSplineComponent->CurvePoints; + + TArray tNewSelectedPoints; + + if (EditedControlPointsIndexes.Num() == 1) + { + FTransform Point = CurvePoints[EditedControlPointsIndexes[0]]; + FTransform NewTransform = FTransform::Identity; + FVector Location = Point.GetLocation(); + //FQuat Rotation = Point.GetRotation(); + //FVector Scale = Point.GetScale3D(); + + NewTransform.SetLocation(Location + 1.f); + //NewTransform.SetRotation(Rotation); + //NewTransform.SetScale3D(Scale); + + + + int32 NewPointIndex = AddControlPointAfter(NewTransform, EditedControlPointsIndexes[0]); + tNewSelectedPoints.Add(NewPointIndex); + } + else + { + int IndexIncrement = 0; + int CurrentPointIndex, LastPointIndex; + FTransform CurrentPoint, LastPoint; + + for (int32 n = 0; n < EditedControlPointsIndexes.Num(); ++n) + { + // Insert a new point between each adjacent pair of points + if (n > 0) + { + CurrentPointIndex = EditedControlPointsIndexes[n]; + LastPointIndex = EditedControlPointsIndexes[n - 1]; + CurrentPoint = CurvePoints[CurrentPointIndex + IndexIncrement]; + LastPoint = CurvePoints[LastPointIndex + IndexIncrement]; + + // Insert a point in the middle of LastPoint and CurrentPoint + FVector NewPointLocation = LastPoint.GetLocation() + (CurrentPoint.GetLocation() - LastPoint.GetLocation()) / 2.f; + FVector NewPointScale = LastPoint.GetScale3D() + (CurrentPoint.GetScale3D() - LastPoint.GetScale3D()) / 2.f; + FQuat NewPointRotation = FQuat::Slerp(LastPoint.GetRotation(), CurrentPoint.GetRotation(), .5f); + + FTransform NewTransform = FTransform::Identity; + NewTransform.SetLocation(NewPointLocation); + NewTransform.SetScale3D(NewPointScale); + NewTransform.SetRotation(NewPointRotation); + + int32 NewPointIndex = AddControlPointAfter(NewTransform, EditedControlPointsIndexes[n - 1] + IndexIncrement); + tNewSelectedPoints.Add(NewPointIndex); + + + IndexIncrement += 1; + } + } + } + + EditedControlPointsIndexes.Empty(); + EditedControlPointsIndexes = tNewSelectedPoints; + + if (IsCookOnCurveChanged(EditedHoudiniSplineComponent)) + EditedHoudiniSplineComponent->MarkChanged(true); + + RefreshViewport(); +} + + +bool +FHoudiniSplineComponentVisualizer::IsAddControlPointValid() const +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + return IsValid(EditedHoudiniSplineComponent) && + EditedHoudiniSplineComponent->EditedControlPointsIndexes.Num() > 0; +} + +void +FHoudiniSplineComponentVisualizer::OnDeleteControlPoint() +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!IsValid(EditedHoudiniSplineComponent)) + return; + + TArray & EditedControlPointsIndexes = EditedHoudiniSplineComponent->EditedControlPointsIndexes; + + if (EditedControlPointsIndexes.Num() <= 0) + return; + + // Transaction for Undo/Redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniSplineComponentDeletingPointsTransaction", "Houdini Spline Component: Deleting curve points."), + EditedHoudiniSplineComponent->GetOuter(), true); + EditedHoudiniSplineComponent->Modify(); + + EditedControlPointsIndexes.Sort(); + + int32 SelectedIndexAfterDelete = EditedControlPointsIndexes[0] - 1; + SelectedIndexAfterDelete = FMath::Max(SelectedIndexAfterDelete, 0); + + for (int32 n = EditedControlPointsIndexes.Num() - 1; n >= 0; --n) + { + int32 RemoveIndex = EditedControlPointsIndexes[n]; + EditedHoudiniSplineComponent->RemovePointAtIndex(RemoveIndex); + + } + + EditedControlPointsIndexes.Empty(); + OnDeselectAllControlPoints(); + EditedControlPointsIndexes.Add(SelectedIndexAfterDelete); + + if (IsCookOnCurveChanged(EditedHoudiniSplineComponent)) + EditedHoudiniSplineComponent->MarkChanged(true); + + // Force refresh the viewport after deleting points to ensure the consistency of HitProxy + RefreshViewport(); + +} + +bool +FHoudiniSplineComponentVisualizer::IsDeleteControlPointValid() const +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!IsValid(EditedHoudiniSplineComponent)) + return false; + + TArray & EditedControlPointsIndexes = EditedHoudiniSplineComponent->EditedControlPointsIndexes; + + if (EditedControlPointsIndexes.Num() <= 0) + return false; + + // We only allow the number of Control Points is at least 2 after delete + if (EditedHoudiniSplineComponent->GetCurvePointCount() - EditedControlPointsIndexes.Num() < 2) + return false; + + return true; +} + +void +FHoudiniSplineComponentVisualizer::OnDuplicateControlPoint() +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!IsValid(EditedHoudiniSplineComponent)) + return; + + TArray & EditedControlPointsIndexes = EditedHoudiniSplineComponent->EditedControlPointsIndexes; + + if (EditedControlPointsIndexes.Num() <= 0) + return; + + // Transaction for Undo/Redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_RUNTIME), + LOCTEXT("HoudiniSplineComponentDuplicatingPointsTransaction", "Houdini Spline Component: Duplicating curve points."), + EditedHoudiniSplineComponent->GetOuter(), true); + EditedHoudiniSplineComponent->Modify(); + + const TArray & CurvePoints = EditedHoudiniSplineComponent->CurvePoints; + + EditedControlPointsIndexes.Sort(); + + TArray tNewSelectedPoints; + int IncrementIndex = 0; + for (int n = 0; n < EditedControlPointsIndexes.Num(); ++n) + { + int32 IndexAfter = EditedControlPointsIndexes[n] + IncrementIndex; + FTransform CurrentPoint = CurvePoints[IndexAfter]; + if (IndexAfter == 0) + IndexAfter = -1; + int32 NewPointIndex = AddControlPointAfter(CurrentPoint, IndexAfter); + tNewSelectedPoints.Add(NewPointIndex); + IncrementIndex ++; + } + + EditedControlPointsIndexes.Empty(); + EditedControlPointsIndexes = tNewSelectedPoints; + + EditedHoudiniSplineComponent->MarkModified(true); + + RefreshViewport(); +} + +bool +FHoudiniSplineComponentVisualizer::IsDuplicateControlPointValid() const +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if(!IsValid(EditedHoudiniSplineComponent) + || EditedHoudiniSplineComponent->EditedControlPointsIndexes.Num() == 0) + return false; + + return true; +} + +void +FHoudiniSplineComponentVisualizer::OnDeselectAllControlPoints() +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (IsValid(EditedHoudiniSplineComponent)) + EditedHoudiniSplineComponent->EditedControlPointsIndexes.Empty(); +} + +bool +FHoudiniSplineComponentVisualizer::IsDeselectAllControlPointsValid() const +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (IsValid(EditedHoudiniSplineComponent)) + return EditedHoudiniSplineComponent->EditedControlPointsIndexes.Num() > 0; + + return false; +} + +int32 +FHoudiniSplineComponentVisualizer::AddControlPointAfter( + const FTransform & NewPoint, + const int32 & nIndex) +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!IsValid(EditedHoudiniSplineComponent)) + return nIndex; + + const TArray & CurvePoints = EditedHoudiniSplineComponent->CurvePoints; + + int32 NewControlPointIndex = nIndex + 1; + + if (NewControlPointIndex == CurvePoints.Num()) + EditedHoudiniSplineComponent->AppendPoint(NewPoint); + else + EditedHoudiniSplineComponent->InsertPointAtIndex(NewPoint, NewControlPointIndex); + + // Return the index of the inserted control point + return NewControlPointIndex; +} + +void +FHoudiniSplineComponentVisualizer::RefreshViewport(bool bInvalidateHitProxies) +{ + if (GEditor) + GEditor->RedrawLevelEditingViewports(bInvalidateHitProxies); +} + +bool +FHoudiniSplineComponentVisualizer::AreSplinePointsSelected() +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!EditedHoudiniSplineComponent) + return false; + + TArray& EditedControlPointIndexes = EditedHoudiniSplineComponent->EditedControlPointsIndexes; + return EditedControlPointIndexes.Num() > 0; + +} + +int32 +FHoudiniSplineComponentVisualizer::GetEditedSplinePointsNum() +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!EditedHoudiniSplineComponent) + return 0; + + TArray& EditedControlPointIndexes = EditedHoudiniSplineComponent->EditedControlPointsIndexes; + return EditedControlPointIndexes.Num(); +} + +void +FHoudiniSplineComponentVisualizer::SelectSplinePoint(const int32& InPointIndex, const bool& bAddToSelection) +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!IsValid(EditedHoudiniSplineComponent)) + return; + + TArray& EditedControlPointIndexes = EditedHoudiniSplineComponent->EditedControlPointsIndexes; + if (!bAddToSelection) + EditedControlPointIndexes.Empty(); + + if (EditedControlPointIndexes.Contains(InPointIndex)) + return; + + EditedControlPointIndexes.Add(InPointIndex); +} + +void +FHoudiniSplineComponentVisualizer::ExtendSplinePointSelection(const int32& InNumPoints, const bool& bExtendFromRight, const bool& bAddToSelection) +{ + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!IsValid(EditedHoudiniSplineComponent)) + return; + + TArray& EditedControlPointIndexes = EditedHoudiniSplineComponent->EditedControlPointsIndexes; + int32 NumTotalPoints = EditedHoudiniSplineComponent->GetCurvePointCount(); + if (NumTotalPoints == 0) + return; + + int32 ExtendSelectionFromIndex = 0; + if (bExtendFromRight) + { + ExtendSelectionFromIndex = -1; + for (const int32& Index : EditedControlPointIndexes) + { + if (Index > ExtendSelectionFromIndex) + ExtendSelectionFromIndex = Index; + } + } + else + { + ExtendSelectionFromIndex = NumTotalPoints; + for (const int32& Index : EditedControlPointIndexes) + { + if (Index < ExtendSelectionFromIndex) + ExtendSelectionFromIndex = Index; + } + } + + if (!bAddToSelection) + EditedControlPointIndexes.Empty(); + + int32 NextPointDelta = bExtendFromRight ? 1 : -1; + int32 CurPointIndex = ExtendSelectionFromIndex; + + for (int Idx = 0; Idx < InNumPoints; ++Idx) + { + CurPointIndex += NextPointDelta; + if (CurPointIndex == NumTotalPoints) + CurPointIndex = 0; + else if (CurPointIndex == -1) + CurPointIndex = NumTotalPoints - 1; + + if (!EditedControlPointIndexes.Contains(CurPointIndex)) + EditedControlPointIndexes.Add(CurPointIndex); + } +} + +// Find the EditorViewportClient of the viewport where the Houdini Spline Component lives in +FEditorViewportClient * +FHoudiniSplineComponentVisualizer::FindViewportClient( + const UHoudiniSplineComponent * InHoudiniSplineComponent, + const FSceneView * View) +{ + if (!View || !InHoudiniSplineComponent) + return nullptr; + + UWorld * World = InHoudiniSplineComponent->GetWorld(); + uint32 ViewKey = View->GetViewKey(); + + const TArray & AllViewportClients = GUnrealEd->GetAllViewportClients(); + + for (auto & NextViewportClient : AllViewportClients) + { + if (!NextViewportClient) + continue; + + if (NextViewportClient->GetWorld() != World) + continue; + + // Found the viewport client which matches the unique key of the current scene view + if (NextViewportClient->ViewState.GetReference()->GetViewKey() == ViewKey) + return NextViewportClient; + } + + return nullptr; +} + +bool +FHoudiniSplineComponentVisualizer::IsCookOnCurveChanged(UHoudiniSplineComponent * InHoudiniSplineComponent) +{ + if (!InHoudiniSplineComponent) + return true; + + return InHoudiniSplineComponent->bCookOnCurveChanged; + + // UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + // UHoudiniInputObject * InputObject = Cast(EditedHoudiniSplineComponent->GetOuter()); + // if (!InputObject) + // return true; + // + // UHoudiniInput * Input = Cast(InputObject->GetOuter()); + // + // if (!Input) + // return true; + // + // return Input->GetCookOnCurveChange(); +}; + + +bool +FHoudiniSplineComponentVisualizer::GetCustomInputCoordinateSystem(const FEditorViewportClient* ViewportClient, FMatrix& OutMatrix) const +{ + if (ViewportClient->GetWidgetCoordSystemSpace() == COORD_Local || ViewportClient->GetWidgetMode() == UE::Widget::WM_Rotate) + { + UHoudiniSplineComponent* EditedHoudiniSplineComponent = GetEditedHoudiniSplineComponent(); + if (!IsValid(EditedHoudiniSplineComponent)) + return false; + + TArray& EditedControlPointsIndexes = EditedHoudiniSplineComponent->EditedControlPointsIndexes; + if (EditedControlPointsIndexes.Num() <= 0) + return false; + + // Use the first selected point for the coordinates + int32 PtIndex = EditedControlPointsIndexes[0]; + if (!EditedHoudiniSplineComponent->CurvePoints.IsValidIndex(PtIndex)) + return false; + + FTransform FirstPointTransform = EditedHoudiniSplineComponent->CurvePoints[EditedControlPointsIndexes[0]]; + OutMatrix = FRotationMatrix(FRotator(FirstPointTransform.GetRotation())); + return true; + } + + return false; +} + + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngineEditor/Private/HoudiniSplineComponentVisualizer.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniSplineComponent.h" + +#include "HitProxies.h" +#include "ComponentVisualizer.h" +#include "Framework/Commands/UICommandList.h" +#include "Framework/Commands/Commands.h" + +class FEditorViewportClient; + +/** Base class for clickable spline editing proxies. **/ +struct HHoudiniSplineVisProxy : public HComponentVisProxy +{ + DECLARE_HIT_PROXY(); + HHoudiniSplineVisProxy(const UActorComponent * InComponent) + : HComponentVisProxy(InComponent, HPP_Wireframe) + {} + + virtual EMouseCursor::Type GetMouseCursor() override + { + return EMouseCursor::CardinalCross; + } +}; + +/** Proxy for a spline control point. **/ +struct HHoudiniSplineControlPointVisProxy : public HHoudiniSplineVisProxy +{ + DECLARE_HIT_PROXY(); + HHoudiniSplineControlPointVisProxy(const UActorComponent * InComponent, int32 InControlPointIndex) + : HHoudiniSplineVisProxy(InComponent) + , ControlPointIndex(InControlPointIndex) + {} + + int32 ControlPointIndex; +}; + +/** Proxy for a spline display point. **/ +struct HHoudiniSplineCurveSegmentVisProxy : public HHoudiniSplineVisProxy +{ + DECLARE_HIT_PROXY(); + HHoudiniSplineCurveSegmentVisProxy(const UActorComponent * InComponent, int32 InDisplayPointIndex) + : HHoudiniSplineVisProxy(InComponent) + , DisplayPointIndex(InDisplayPointIndex) + {} + + int32 DisplayPointIndex; +}; + +class FHoudiniSplineComponentVisualizerCommands : public TCommands< FHoudiniSplineComponentVisualizerCommands> +{ + public: + FHoudiniSplineComponentVisualizerCommands(); + + /** Register commands. **/ + virtual void RegisterCommands() override; + + public: + TSharedPtr CommandAddControlPoint; + + TSharedPtr CommandDuplicateControlPoint; + + TSharedPtr CommandDeleteControlPoint; + + TSharedPtr CommandDeselectAllControlPoints; + + TSharedPtr CommandInsertControlPoint; +}; + + +/** **/ +class FHoudiniSplineComponentVisualizer : public FComponentVisualizer +{ + public: + FHoudiniSplineComponentVisualizer(); + + private: + void RefreshViewport(bool bInvalidateHitProxies=true); + + public: + virtual void OnRegister() override; + + virtual void DrawVisualization( + const UActorComponent * Component, + const FSceneView * View, + FPrimitiveDrawInterface * PDI) override; + + virtual bool VisProxyHandleClick( + FEditorViewportClient* InViewportClient, + HComponentVisProxy* VisProxy, + const FViewportClick& Click) override; + + virtual bool GetWidgetLocation( + const FEditorViewportClient* ViewportClient, FVector& OutLocation) const override; + + virtual bool IsVisualizingArchetype() const override; + + virtual void EndEditing() override; + + virtual bool HandleInputDelta( + FEditorViewportClient* ViewportClient, + FViewport* Viewport, + FVector& DeltaTranslate, + FRotator& DeltaRotate, + FVector& DeltaScale) override; + + virtual bool HandleInputKey( + FEditorViewportClient* ViewportClient, + FViewport* Viewport, + FKey Key, + EInputEvent Event) override; + + virtual TSharedPtr GenerateContextMenu() const override; + + virtual bool GetCustomInputCoordinateSystem( + const FEditorViewportClient* ViewportClient, FMatrix& OutMatrix) const override; + + protected: + + /** Callbacks for add control point action**/ + void OnAddControlPoint(); + bool IsAddControlPointValid() const; + + /** Callbacks for delete control point action. **/ + void OnDeleteControlPoint(); + bool IsDeleteControlPointValid() const; + + /** Callbacks for duplicate control point action. **/ + void OnDuplicateControlPoint(); + bool IsDuplicateControlPointValid() const; + + /** Callbacks for deselect all control points action. **/ + void OnDeselectAllControlPoints(); + bool IsDeselectAllControlPointsValid() const; + + /** Callbacks for inserting a control point action.**/ + void OnInsertControlPoint(); + bool IsInsertControlPointValid() const; + // For alt-pressed inserting control point on curve. + int32 OnInsertControlPointWithoutUpdate(); + + int32 AddControlPointAfter(const FTransform & NewPoint, const int32 & nIndex); + + public: + /** Property path from the parent actor to the component */ + // NOTE: We need to use SplinePropertyPath on the visualizer as opposed to a direct pointer since the + // direct pointer breaks during Blueprint reconstructions properly + // (see SplineComponent / SplineMeshComponent visualizers). + FComponentPropertyPath SplinePropertyPath; + UHoudiniSplineComponent* GetEditedHoudiniSplineComponent() const { return Cast(SplinePropertyPath.GetComponent()); } + + + // Returns true if at least one point is selected for the current + // houdini spline component + bool AreSplinePointsSelected(); + + // Returns the number of control points selected in the spline + // component that is currently being edited. + int32 GetEditedSplinePointsNum(); + + // Selects the point at the given index, if it is a valid point + // within the allowed index range. If the point is not to be added + // to selection, then the newly selected point will be the only + // selected point. + void SelectSplinePoint(const int32& InPointIndex, const bool& bAddToSelection); + + // Extends the current seletion by the specified number of points + // to the left, or right, of the current selection. If not adding + // to selection, then the currently selected points are cleared first. + void ExtendSplinePointSelection(const int32& InNumPoints, const bool& bExtendFromLeft, const bool& bAddToSelection); + + protected: + + bool bAllowDuplication; + + int32 EditedCurveSegmentIndex; + + TSharedPtr VisualizerActions; + + /** Rotation used for the gizmo widgets **/ + FQuat CachedRotation; + + FVector CachedScale3D; + + /** Indicates wether or not a transaction should be recorded when moving a point **/ + bool bMovingPoints; + + bool bInsertingOnCurveControlPoints; + + bool bRecordingMovingPoints; + + private: + FEditorViewportClient * FindViewportClient(const UHoudiniSplineComponent * InHoudiniSplineComponent, const FSceneView * View); + + bool IsCookOnCurveChanged(UHoudiniSplineComponent* InHoudiniSplineComponent); + +}; +``` + +`Source/HoudiniEngineEditor/Private/HoudiniToolTypesEditor.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniToolTypesEditor.h" +#include "HoudiniEngineToolTypes.h" + +#include "HoudiniPreset.h" +#include "HoudiniAsset.h" + +UHoudiniToolEditorProperties::UHoudiniToolEditorProperties() + : Name() + , Type(EHoudiniToolType::HTOOLTYPE_OPERATOR_SINGLE) + , SelectionType(EHoudiniToolSelectionType::HTOOL_SELECTION_ALL) + , ToolTip() + , ToolType(EHoudiniPackageToolType::HoudiniAsset) + , AssetPath(FFilePath()) + , HelpURL() + , bClearCachedIcon(false) + , IconPath(FFilePath()) + , HoudiniAsset(nullptr) + , HoudiniPreset(nullptr) +{ +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniToolTypesEditor.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniToolsEditor.h" +#include "HoudiniToolTypesEditor.generated.h" + +class UHoudiniAsset; +class UHoudiniPreset; + +// Class describing the various properties for a Houdini Tool +// adding a UClass was necessary to use the PropertyEditor window +UCLASS( EditInlineNew ) +class UHoudiniToolEditorProperties : public UObject +{ + GENERATED_BODY() + + public: + + UHoudiniToolEditorProperties(); + + /** Name of the tool */ + UPROPERTY( Category = Tool, EditAnywhere ) + FString Name; + + /** Type of the tool */ + UPROPERTY( Category = Tool, EditAnywhere ) + EHoudiniToolType Type; + + /** Selection Type of the tool */ + UPROPERTY( Category = Tool, EditAnywhere ) + EHoudiniToolSelectionType SelectionType; + + /** Tooltip shown on mouse hover */ + UPROPERTY( Category = Tool, EditAnywhere, meta=(MultiLine=true) ) + FString ToolTip; + + // Type of tool that these properties represent. + UPROPERTY() + EHoudiniPackageToolType ToolType; + + /** Houdini Asset path **/ + UPROPERTY(Category = Tool, EditAnywhere, meta = (FilePathFilter="hda", EditCondition="ToolType==EHoudiniPackageToolType::HoudiniAsset", EditConditionHides)) + FFilePath AssetPath; + + /** Clicking on help icon will bring up this URL */ + UPROPERTY( Category = Tool, EditAnywhere ) + FString HelpURL; + + /** This will ensure that the cached icon for this HDA is removed. Note that if an IconPath has been + * specified, this option will have no effect. + */ + UPROPERTY( Category = Tool, EditAnywhere ) + bool bClearCachedIcon; + + /** Import a new icon for this HDA. If this field is left blank, we will look for an + * icon next to the HDA with matching name. + */ + UPROPERTY( Category = Tool, EditAnywhere, meta = (FilePathFilter = "png") ) + FFilePath IconPath; + + + UPROPERTY() + TObjectPtr HoudiniAsset; + + UPROPERTY() + TObjectPtr HoudiniPreset; +}; + + + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniToolsEditor.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniToolsEditor.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniAssetFactory.h" +#include "HoudiniCookable.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniEngineEditorSettings.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniNodeSyncComponent.h" +#include "HoudiniParameter.h" +#include "HoudiniParameterColor.h" +#include "HoudiniParameterFile.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterOperatorPath.h" +#include "HoudiniParameterRamp.h" +#include "HoudiniParameterString.h" +#include "HoudiniPreset.h" +#include "HoudiniToolsPackageAsset.h" +#include "HoudiniToolsPackageAssetFactory.h" +#include "HoudiniToolsRuntimeUtils.h" +#include "HoudiniToolTypesEditor.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetViewUtils.h" +#include "Editor.h" +#include "EditorFramework/AssetImportData.h" +#include "EditorReimportHandler.h" +#include "Engine/Texture2D.h" +#include "Framework/Application/SlateApplication.h" +#include "GameProjectUtils.h" +#include "HAL/FileManager.h" +#include "HAL/PlatformFileManager.h" +#include "Interfaces/IMainFrameModule.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "Modules/ModuleManager.h" +#include "ObjectTools.h" +#include "PackageTools.h" +#include "PropertyEditorModule.h" +#include "ScopedTransaction.h" +#include "Selection.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonSerializer.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SUniformGridPanel.h" +#include "HoudiniParameterTranslator.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + #include "Subsystems/EditorAssetSubsystem.h" +#else + #include "EditorAssetLibrary.h" +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + #include "TextureResource.h" +#endif + +#ifdef LOCTEXT_NAMESPACE +// This undef is here to get rid of the definition from UE's DecoratedDragDropOp.h. +#undef LOCTEXT_NAMESPACE +#endif + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +DEFINE_LOG_CATEGORY(LogHoudiniTools); + +// Wrapper interface to manage code compatibility across multiple UE versions +struct FToolsWrapper +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + static FSoftObjectPath ConvertAssetObjectPath(const FString& Name) { return FSoftObjectPath(Name); } +#else + static FName ConvertAssetObjectPath(const FString& Name) { return FName(Name); } +#endif + + static void CheckValidEditorAssetSubsystem() + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UEditorAssetSubsystem* EditorAssetSubsystem = GEditor->GetEditorSubsystem(); + checkf(EditorAssetSubsystem != nullptr, TEXT("Invalid EditorAssetSubsystem")); +#else + // Nothing to do here +#endif + } + + static void ReimportAsync(UObject* Object, bool bAskForNewFileIfMissing, bool bShowNotification, const FString& PreferredReimportFile = TEXT("")) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + FReimportManager::Instance()->ReimportAsync(Object, bAskForNewFileIfMissing, bShowNotification, PreferredReimportFile); +#else + FReimportManager::Instance()->Reimport(Object, bAskForNewFileIfMissing, bShowNotification, PreferredReimportFile); +#endif + } + + static bool DoesDirectoryExist(const FString& DirectoryPath) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UEditorAssetSubsystem* EditorAssetSubsystem = GEditor->GetEditorSubsystem(); + return EditorAssetSubsystem->DoesDirectoryExist(DirectoryPath); +#else + return UEditorAssetLibrary::DoesDirectoryExist(DirectoryPath); +#endif + } + + static bool MakeDirectory(const FString& DirectoryPath) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UEditorAssetSubsystem* EditorAssetSubsystem = GEditor->GetEditorSubsystem(); + return EditorAssetSubsystem->DoesDirectoryExist(DirectoryPath); +#else + return UEditorAssetLibrary::DoesDirectoryExist(DirectoryPath); +#endif + } +}; + + +IAssetRegistry& +FHoudiniToolsEditor::GetAssetRegistry() +{ + return FModuleManager::LoadModuleChecked("AssetRegistry").Get(); +} + + +FHoudiniToolsEditor::FHoudiniToolsEditor() +{ + +} + +FString +FHoudiniToolsEditor::GetDefaultHoudiniToolIconPath() +{ + return FHoudiniEngineEditor::Get().GetHoudiniEnginePluginDir() / TEXT("Content/Icons/icon_houdini_logo_40.png"); +} + +FString FHoudiniToolsEditor::GetDefaultHoudiniToolIconBrushName() +{ + return TEXT("HoudiniEngine.HoudiniEngineLogo40"); +} + +FString FHoudiniToolsEditor::GetAbsoluteGameContentDir() +{ + return FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()); +} + + +FString +FHoudiniToolsEditor::GetDefaultHoudiniToolsPath() +{ + return TEXT("/Game/HoudiniEngine/Tools/"); +} + +FString +FHoudiniToolsEditor::GetDefaultPackagePath(const FString& PackageName) +{ + FString BasePath = FHoudiniToolsEditor::GetDefaultHoudiniToolsPath(); + return UPackageTools::SanitizePackageName( FPaths::Combine(BasePath, PackageName) ); +} + +FString FHoudiniToolsEditor::GetDefaultPackageAssetPath(const FString& PackageName) +{ + const FString PkgBasePath = GetDefaultPackagePath(PackageName); + const FString AssetPath = FPaths::Combine(PkgBasePath, FHoudiniToolsRuntimeUtils::GetPackageUAssetName()); + return AssetPath; +} + +TSoftObjectPtr FHoudiniToolsEditor::GetPackageAssetRef(const FString& PackagePath) +{ + const FString PackageName = FString::Format(TEXT("{0}.{0}"), { FHoudiniToolsRuntimeUtils::GetPackageUAssetName() } ); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + return TSoftObjectPtr(FSoftObjectPath(FString::Format(TEXT("UHoudiniToolsPackageAsset'{0}/{1}'"), {PackagePath, PackageName}))); +#else + return TSoftObjectPtr(FString::Format(TEXT("UHoudiniToolsPackageAsset'{0}/{1}'"), { PackagePath, PackageName })); +#endif +} + +FString FHoudiniToolsEditor::GetAbsoluteToolsPackagePath(const UHoudiniToolsPackageAsset* ToolsPackage) +{ + if (!IsValid(ToolsPackage)) + { + return FString(); + } + if (ToolsPackage->ExternalPackageDir.Path.Len() == 0) + { + return FString(); + } + + // TODO: Can we normalize this path to get rid of the relative path bits? + FString AbsPath = FPaths::ConvertRelativePathToFull(FPaths::GetPath(FPaths::GetProjectFilePath()), ToolsPackage->ExternalPackageDir.Path); + + FPaths::NormalizeDirectoryName(AbsPath); + FPaths::CollapseRelativeDirectories(AbsPath); + AbsPath = FPaths::ConvertRelativePathToFull(AbsPath); + + return AbsPath; +} + +FString FHoudiniToolsEditor::ResolveHoudiniAssetLabel(const UHoudiniAsset* HoudiniAsset) +{ + if (!IsValid(HoudiniAsset)) + { + return FString(); + } + + if (IsValid(HoudiniAsset->HoudiniToolData)) + { + return HoudiniAsset->HoudiniToolData->Name; + } + else + { + return FPaths::GetBaseFilename(HoudiniAsset->AssetFileName); + } +} + + +bool +FHoudiniToolsEditor::GetHoudiniAssetJSONPath(const UHoudiniAsset* HoudiniAsset, FString& OutJSONFilePath) +{ + if (!IsValid(HoudiniAsset)) + { + return false; + } + + if (!IsValid(HoudiniAsset->AssetImportData)) + { + return false; + } + + // Compute the filepath of where we expect this HoudiniAsset's json file resides. + const FString& ReimportPath = HoudiniAsset->AssetImportData->GetFirstFilename(); + FString Path, Filename, Extension; + FPaths::Split(ReimportPath, Path, Filename, Extension); + OutJSONFilePath = FPaths::Combine(Path, Filename + ".json"); + + return true; +} + + +FString +FHoudiniToolsEditor::ResolveHoudiniAssetIconPath(const UHoudiniAsset* HoudiniAsset) +{ + if (!IsValid(HoudiniAsset)) + { + return FString(); + } + if (IsValid(HoudiniAsset->HoudiniToolData) && HoudiniAsset->HoudiniToolData->IconSourcePath.FilePath.Len() > 0) + { + // We have a custom icon path for this HoudiniAsset. + return HoudiniAsset->HoudiniToolData->IconSourcePath.FilePath; + } + + // We couldn't find a valid icon path for this asset. Use the default path. + return FHoudiniToolsRuntimeUtils::GetDefaultHoudiniAssetIconPath(HoudiniAsset); +} + + +bool +FHoudiniToolsEditor::ResolveHoudiniAssetRelativePath(const UObject* Object, FString& OutPath) +{ + if (!IsValid(Object)) + return false; + + const UHoudiniToolsPackageAsset* ToolsPackage = FindOwningToolsPackage(Object); + if (!IsValid(ToolsPackage)) + return false; + + return ResolveHoudiniAssetRelativePath(Object, ToolsPackage, OutPath); +} + + +bool +FHoudiniToolsEditor::ResolveHoudiniAssetRelativePath( + const UObject* AssetObject, + const UHoudiniToolsPackageAsset* OwningPackageAsset, + FString& OutPath) +{ + if (!IsValid(AssetObject)) + return false; + + if (!IsValid(OwningPackageAsset)) + return false; + + //NOTE: We're constructing a relative path here for the purposes of include/exclude pattern matching. + // to keep things as straightforward as possible, we always use the path name of the relevant + // asset for pattern matching. We will NOT be using the operator type or the tool label. + + const FString ToolsPackagePath = OwningPackageAsset->GetPackage()->GetPathName(); + + const UPackage* Package = AssetObject->GetPackage(); + if (!IsValid(Package)) + { + return false; + } + + // Use the HoudiniAsset's relative (to the Package descriptor) path for category matching. + OutPath = Package->GetPathName(); + FPaths::MakePathRelativeTo(OutPath, *ToolsPackagePath); + + return true; +} + + +FAssetData FHoudiniToolsEditor::GetAssetDataByObject(const UObject* AssetObject) +{ + if (!IsValid(AssetObject)) + { + return FAssetData(); + } + + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + + const FAssetData AssetData = AssetRegistryModule.Get().GetAssetByObjectPath( FToolsWrapper::ConvertAssetObjectPath(AssetObject->GetPathName()) ); + + if (!AssetData.IsInstanceOf( AssetObject->GetClass() )) + { + return FAssetData(); + } + + return AssetData; +} + +bool FHoudiniToolsEditor::IsHoudiniToolsPackageDir(const FString& PackageBasePath) const +{ + const FString PkgPath = FPaths::Combine(PackageBasePath, FString::Format(TEXT("{0}.{0}"), { FHoudiniToolsRuntimeUtils::GetPackageUAssetName() }) ); + + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + const FAssetData AssetData = AssetRegistryModule.Get().GetAssetByObjectPath(FToolsWrapper::ConvertAssetObjectPath(PkgPath)); + if (!AssetData.IsValid()) + { + return false; + } + + if (!AssetData.IsInstanceOf(UHoudiniToolsPackageAsset::StaticClass())) + { + return false; + } + return true; +} + + +void +FHoudiniToolsEditor::FindHoudiniToolsPackagePaths( TArray& HoudiniToolsDirectoryArray ) const +{ + // Scan the UE project to find valid HoudiniTools package directories. + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + const TArray& SearchPaths = HoudiniRuntimeSettings->HoudiniToolsSearchPath; + HoudiniToolsDirectoryArray.Empty(); + + for(const FString& SearchPath : SearchPaths) + { + // Check if the current directory is a package directory + // if not, then check each subdirectory for a houdini package. + if (IsHoudiniToolsPackageDir(SearchPath)) + { + HoudiniToolsDirectoryArray.Add(SearchPath); + continue; + } + IAssetRegistry& AssetRegistry = GetAssetRegistry(); + + TArray FoundFolders; + AssetRegistry.EnumerateSubPaths(SearchPath, [&FoundFolders](const FString& Folder) + { + FoundFolders.Add(Folder); + return true; + }, + false); + + for (const FString& SubFolder : FoundFolders) + { + if (IsHoudiniToolsPackageDir(SubFolder)) + { + HoudiniToolsDirectoryArray.Add(SubFolder); + } + } + } +} + +bool FHoudiniToolsEditor::LoadJSONFile(const FString& JSONFilePath, TSharedPtr& OutJSONObject) +{ + if ( JSONFilePath.IsEmpty() ) + return false; + + // Read the file as a string + FString FileContents; + if ( !FFileHelper::LoadFileToString( FileContents, *JSONFilePath ) ) + { + HOUDINI_LOG_ERROR( TEXT("Failed to import Package Descriptor .json file: %s"), *JSONFilePath); + return false; + } + + // Parse it as JSON + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create( FileContents ); + if (!FJsonSerializer::Deserialize(Reader, OutJSONObject) || !OutJSONObject.IsValid()) + { + HOUDINI_LOG_ERROR( TEXT("Invalid json file: %s"), *JSONFilePath); + return false; + } + + return true; +} + +bool FHoudiniToolsEditor::WriteJSONFile(const FString& JSONFilePath, const TSharedPtr JSONObject) +{ + // Output the JSON to a String + FString OutputString; + const TSharedRef< TJsonWriter<> > Writer = TJsonWriterFactory<>::Create(&OutputString); + FJsonSerializer::Serialize(JSONObject.ToSharedRef(), Writer); + + // Then write the output string to the json file itself + const FString SaveDirectory = FPaths::GetPath(JSONFilePath); + + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + // Returns true if the directory existed or has been created + if ( PlatformFile.CreateDirectoryTree(*SaveDirectory) ) + { + return FFileHelper::SaveStringToFile(OutputString, *JSONFilePath); + } + + return false; +} + +void FHoudiniToolsEditor::FindHoudiniToolsPackages(TArray& HoudiniToolPackages) const +{ + TArray PackagePaths; + FindHoudiniToolsPackagePaths(PackagePaths); + + for (const FString& PackagePath : PackagePaths) + { + // Ingest tools from this package path + UHoudiniToolsPackageAsset* PackageAsset = LoadHoudiniToolsPackage(PackagePath); + if (!IsValid(PackageAsset)) + { + continue; + } + HoudiniToolPackages.Add(PackageAsset); + } +} + +UHoudiniToolsPackageAsset* +FHoudiniToolsEditor::FindOwningToolsPackage(const UObject* Object) +{ + if (!IsValid(Object)) + return nullptr; + + // No need to load/find Tools package while cooking or running a commandlet + // This would only generate unneeded warnings + if (IsRunningCommandlet() || IsRunningCookCommandlet() || GIsCookerLoadingPackage) + return nullptr; + + FString CurrentPath = FPaths::GetPath(Object->GetPathName()); + + // Define a depth limit to break out of the loop, in case something + // goes wrong and we get stuck. If ever needed, we can move this to a + // project setting, but for now we'll leave it here. + constexpr int LoopLimit = 100; + int LoopCount = 0; + + while (CurrentPath.Len() > 0) + { + // Try to load a package at the current path. + UHoudiniToolsPackageAsset* PackageAsset = LoadHoudiniToolsPackage(CurrentPath); + if (IsValid(PackageAsset)) + { + // We found a valid package asset. + return PackageAsset; + } + + // Proceed to the next parent directory. + CurrentPath = FPaths::GetPath(CurrentPath); + + LoopCount++; + if (LoopCount >= LoopLimit) + { + return nullptr; + } + } + + return nullptr; +} + +void FHoudiniToolsEditor::BrowseToObjectInContentBrowser(UObject* InObject) +{ + if (!IsValid(InObject)) + return; + + if (GEditor) + { + TArray Objects; + Objects.Add(InObject); + GEditor->SyncBrowserToObjects(Objects); + } +} + +void FHoudiniToolsEditor::BrowseToOwningPackageInContentBrowser(const UHoudiniAsset* HoudiniAsset) +{ + if (!IsValid(HoudiniAsset)) + return; + + UHoudiniToolsPackageAsset* ToolsPackage = FHoudiniToolsEditor::FindOwningToolsPackage(HoudiniAsset); + BrowseToObjectInContentBrowser(ToolsPackage); +} + +void FHoudiniToolsEditor::FindHoudiniToolsInPackage(const UHoudiniToolsPackageAsset* ToolsPackage, TArray>& OutHoudiniTools) +{ + OutHoudiniTools.Empty(); + if (!IsValid(ToolsPackage)) + return; + + const FString PackagePath = ToolsPackage->GetPathName(); + const FString PackageDir = FPaths::GetPath(PackagePath); + + const IAssetRegistry& AssetRegistry = GetAssetRegistry(); + + TArray AssetDataArray; + AssetRegistry.GetAssetsByPath(FName(PackageDir), AssetDataArray, true, false); + + for (const FAssetData& AssetData : AssetDataArray) + { + if (!AssetData.IsInstanceOf(UHoudiniAsset::StaticClass()) && + !AssetData.IsInstanceOf(UHoudiniPreset::StaticClass())) + { + continue; + } + const UHoudiniPreset* HoudiniPreset = Cast( AssetData.GetAsset() ); + const UHoudiniAsset* HoudiniAsset = Cast( AssetData.GetAsset() ); + + if (IsValid(HoudiniPreset)) + { + HoudiniAsset = HoudiniPreset->SourceHoudiniAsset; + } + + if (!IsValid(HoudiniAsset)) + continue; + + // Construct FHoudiniTool and populate from the current Houdini Asset and package. + TSharedPtr HoudiniTool; + + // Create a new FHoudiniTool for this asset. + HoudiniTool = MakeShareable( new FHoudiniTool() ); + + PopulateHoudiniTool(HoudiniTool, HoudiniAsset, HoudiniPreset, ToolsPackage, false); + OutHoudiniTools.Add(HoudiniTool); + } +} + +void FHoudiniToolsEditor::FindHoudiniAssetsInPackage(const UHoudiniToolsPackageAsset* ToolsPackage, TArray& OutAssets) +{ + OutAssets.Empty(); + if (!IsValid(ToolsPackage)) + return; + + const FString PackagePath = ToolsPackage->GetPathName(); + const FString PackageDir = FPaths::GetPath(PackagePath); + + const IAssetRegistry& AssetRegistry = GetAssetRegistry(); + + TArray AssetDataArray; + AssetRegistry.GetAssetsByPath(FName(PackageDir), AssetDataArray, true, false); + + for (const FAssetData& AssetData : AssetDataArray) + { + if (!AssetData.IsInstanceOf(UHoudiniAsset::StaticClass())) + { + continue; + } + UHoudiniAsset* HoudiniAsset = Cast( AssetData.GetAsset() ); + if (!IsValid(HoudiniAsset)) + continue; + + OutAssets.Add(HoudiniAsset); + } +} + +void +FHoudiniToolsEditor::ApplyCategories( + const TMap& InCategories, + const TSharedPtr& HoudiniTool, + const bool bIgnoreExclusionRules, + TArray& OutIncludeCategories, + TArray& OutExcludeCategories + ) +{ + if (!HoudiniTool.IsValid()) + return; + + const UObject* AssetObject = HoudiniTool->GetAssetObject(); + + if (!IsValid(AssetObject)) + return; + + //NOTE: We're constructing a relative path here for the purposes of include/exclude pattern matching. + // to keep things as straightforward as possible, we always use the path name of the relevant + // asset for pattern matching. We will NOT use the operator type or the tool label. + + FString RelPath; + if (!ResolveHoudiniAssetRelativePath(AssetObject, RelPath)) + { + // if we can't resolve a relative path for this HoudiniAsset tool, we can't apply category matching. + return; + } + + for(const TTuple& Entry : InCategories) + { + const FString& CategoryName = Entry.Key.Name; + const FCategoryRules& Rules = Entry.Value; + + ApplyCategoryRules(RelPath, CategoryName, Rules.Include, Rules.Exclude, bIgnoreExclusionRules, OutIncludeCategories, OutExcludeCategories); + } +} + +void +FHoudiniToolsEditor::ApplyCategoryRules( + const FString& HoudiniAssetRelPath, + const FString& CategoryName, + const TArray& IncludeRules, + const TArray& ExcludeRules, + const bool bIgnoreExclusionRules, + TArray& OutIncludeCategories, + TArray& OutExcludeCategories + ) +{ + if (!bIgnoreExclusionRules) + { + bool bExclude = false; + + // Apply exclude rules + for (const FString& Pattern : ExcludeRules) + { + if (HoudiniAssetRelPath.MatchesWildcard(Pattern)) + { + bExclude = true; + break; + } + } + if (bExclude) + { + // The HDA should be excluded from this category. + OutExcludeCategories.Add(CategoryName); + return; + } + } + + bool bExclude = false; + + // Apply exclude rules + for (const FString& Pattern : ExcludeRules) + { + if (HoudiniAssetRelPath.MatchesWildcard(Pattern)) + { + bExclude = true; + break; + } + } + if (bExclude) + { + // The HDA should be excluded from this category but will be included due + OutExcludeCategories.Add(CategoryName); + + if (bIgnoreExclusionRules == false) + { + return; + } + } + + // Apply include rules + for (const FString& Pattern : IncludeRules) + { + if (HoudiniAssetRelPath.MatchesWildcard(Pattern)) + { + OutIncludeCategories.Add(CategoryName); + break; + } + } +} + +bool +FHoudiniToolsEditor::IsValidPackageName(const FString& PkgName, FText* OutFailReason) +{ + // Code adapted from: GameProjectUtils::IsValidClassNameForCreation() + if ( PkgName.IsEmpty() ) + { + if (OutFailReason) + { + *OutFailReason = LOCTEXT( "HoudiniTools_NoPackageName", "You must specify a package name." ); + } + return false; + } + + if ( PkgName.Contains(TEXT(" ")) ) + { + if (OutFailReason) + { + *OutFailReason = LOCTEXT( "HoudiniTools_PackageNameContainsSpace", "Your package name may not contain a space." ); + } + return false; + } + + if ( !FChar::IsAlpha(PkgName[0]) ) + { + if (OutFailReason) + { + *OutFailReason = LOCTEXT( "HoudiniTools_PackageNameMustBeginWithACharacter", "Your package name must begin with an alphabetic character." ); + } + return false; + } + + FString IllegalNameCharacters; + if ( !GameProjectUtils::NameContainsOnlyLegalCharacters(PkgName, IllegalNameCharacters) ) + { + if (OutFailReason) + { + FFormatNamedArguments Args; + Args.Add( TEXT("HoudiniTools_IllegalNameCharacters"), FText::FromString( IllegalNameCharacters ) ); + *OutFailReason = FText::Format( LOCTEXT( "HoudiniTools_PackageNameContainsIllegalCharacters", "The package name may not contain the following characters: '{IllegalNameCharacters}'" ), Args ); + } + return false; + } + + return true; +} + +bool FHoudiniToolsEditor::CanCreateCreateToolsPackage(const FString& PackageName, FText* FailReason) +{ + if (!IsValidPackageName(PackageName, FailReason)) + return false; + + const FString AssetPath = GetDefaultPackageAssetPath(PackageName); + + if ( _DoesAssetExist(AssetPath) ) + { + // Package already exists. + if (FailReason) + { + *FailReason = LOCTEXT("PackageAlreadyExists", "Package already exists: '{0}'"); + *FailReason = FText::Format(*FailReason, {FText::FromString(AssetPath)}); + } + return false; + } + + return true; +} + +UHoudiniToolsPackageAsset* FHoudiniToolsEditor::CreateToolsPackageAsset( + const FString& PackageDir, + const FString& DefaultCategory, + const FString& ExternalPackageDir) +{ + UHoudiniToolsPackageAssetFactory* Factory = NewObject(); + + const FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + const FString PackageAssetName = FHoudiniToolsRuntimeUtils::GetPackageUAssetName(); // Name of the Houdini Tools Package asset. + const FString FullPath = FPaths::Combine(PackageDir, PackageAssetName); + + UHoudiniToolsPackageAsset* Asset = Cast(AssetToolsModule.Get().CreateAsset( + PackageAssetName, PackageDir, + UHoudiniToolsPackageAsset::StaticClass(), Factory, FName("ContentBrowserNewAsset"))); + + if (!Asset) + { + return nullptr; + } + + // Clear out categories since new Houdini Tools Package assets have default category values. + Asset->Categories.Empty(); + + // By default add a catch-all category. + FCategoryRules DefaultRule; + DefaultRule.Include.Add("*"); + Asset->Categories.Add(DefaultCategory, DefaultRule); + + Asset->ExternalPackageDir.Path = ExternalPackageDir; + FHoudiniEngineRuntimeUtils::DoPostEditChangeProperty(Asset, "ExternalPackageDir"); + + return Asset; +} + +UHoudiniToolData* FHoudiniToolsEditor::GetOrCreateHoudiniToolData(UHoudiniAsset* HoudiniAsset) +{ + UHoudiniToolData* ToolData = HoudiniAsset->HoudiniToolData; + + if (!IsValid(ToolData)) + { + HoudiniAsset->Modify(); + ToolData = NewObject(HoudiniAsset); + ToolData->Modify(); + HoudiniAsset->HoudiniToolData = ToolData; + } + + return ToolData; +} + +void +FHoudiniToolsEditor::PopulateHoudiniTool(const TSharedPtr& HoudiniTool, + const UHoudiniAsset* InHoudiniAsset, + const UHoudiniPreset* InHoudiniPreset, + const UHoudiniToolsPackageAsset* InToolsPackage, + bool bIgnoreToolData) +{ + if (!IsValid(InHoudiniAsset)) + return; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + HoudiniTool->HoudiniAsset = TSoftObjectPtr(FSoftObjectPath(InHoudiniAsset->GetPathName())); + HoudiniTool->ToolsPackage = TSoftObjectPtr(FSoftObjectPath(InToolsPackage->GetPathName())); +#else + HoudiniTool->HoudiniAsset = InHoudiniAsset; + HoudiniTool->ToolsPackage = InToolsPackage; +#endif + + if (IsValid(InHoudiniPreset)) + { + HoudiniTool->Name = FText::FromString(InHoudiniPreset->Name); + HoudiniTool->ToolTipText = FText::FromString(InHoudiniPreset->Description); + } + else + { + if (!bIgnoreToolData && (IsValid(InHoudiniAsset->HoudiniToolData))) + { + // Populate from ToolData (this is our preferred data source) + UHoudiniToolData* ToolData = InHoudiniAsset->HoudiniToolData; + + HoudiniTool->Name = FText::FromString(ToolData->Name); + HoudiniTool->ToolTipText = FText::FromString(ToolData->ToolTip); + } + else + { + // Populate from any data we can extract from UHoudiniAsset without looking outside the Unreal project. + // i.e., no peeking at external json files. + HoudiniTool->Name = FText::FromString(FPaths::GetBaseFilename(InHoudiniAsset->AssetFileName)); + HoudiniTool->ToolTipText = HoudiniTool->Name; + } + } + + if (!bIgnoreToolData && (IsValid(InHoudiniAsset->HoudiniToolData))) + { + // Populate from ToolData (this is our preferred data source) + UHoudiniToolData* ToolData = InHoudiniAsset->HoudiniToolData; + + HoudiniTool->HelpURL = ToolData->HelpURL; + HoudiniTool->Type = ToolData->Type; + HoudiniTool->SelectionType = ToolData->SelectionType; + } + else + { + HoudiniTool->HelpURL = FString(); + // We don't really know what this HDA is, so we're just going with "no inputs" as a default. + HoudiniTool->Type = EHoudiniToolType::HTOOLTYPE_GENERATOR; + HoudiniTool->SelectionType = EHoudiniToolSelectionType::HTOOL_SELECTION_ALL; + } +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + HoudiniTool->HoudiniPreset = TSoftObjectPtr(FSoftObjectPath(InHoudiniPreset->GetPathName())); +#else + HoudiniTool->HoudiniPreset = InHoudiniPreset; +#endif + HoudiniTool->PackageToolType = IsValid(InHoudiniPreset) ? EHoudiniPackageToolType::Preset : EHoudiniPackageToolType::HoudiniAsset; + + // if (IsValid(InHoudiniAsset->AssetImportData)) + // { + // UAssetImportData* ImportData = InHoudiniAsset->AssetImportData; + // if (ImportData->SourceData.SourceFiles.Num() > 0) + // { + // HoudiniTool->SourceAssetPath.FilePath = ImportData->SourceData.SourceFiles[0].RelativeFilename; + // } + // } + + CreateUniqueAssetIconBrush(HoudiniTool); +} + +UHoudiniToolsPackageAsset* FHoudiniToolsEditor::LoadHoudiniToolsPackage(const FString& PackageBasePath) +{ + // No need to load/find Tools package while cooking or running a commandlet + // This would only generate unneeded warnings + if (IsRunningCommandlet() || IsRunningCookCommandlet() || GIsCookerLoadingPackage) + return nullptr; + + const FString PkgPath = FPaths::Combine(PackageBasePath, FString::Format(TEXT("{0}.{0}"), { FHoudiniToolsRuntimeUtils::GetPackageUAssetName() }) ); + return LoadObject(nullptr, *PkgPath); +} + +bool FHoudiniToolsEditor::ExcludeToolFromPackageCategory(UObject* Object, const FString& CategoryName, bool bAddCategoryIfMissing) +{ + if (!IsValid(Object)) + { + return false; + } + + UHoudiniToolsPackageAsset* PackageAsset = FindOwningToolsPackage(Object); + if (!IsValid(PackageAsset)) + { + HOUDINI_LOG_WARNING(TEXT("[ExcludeToolFromCategory] Could not find owning package")); + return false; + } + + const bool bHasCategory = PackageAsset->Categories.Contains(CategoryName); + if (!bHasCategory && !bAddCategoryIfMissing) + { + // The category doesn't exist, and we don't want to add it. + return false; + } + + // Attempt to resolve the tool path relative to its owning package. + FString RelToolPath; + if (!ResolveHoudiniAssetRelativePath(Object, RelToolPath)) + { + HOUDINI_LOG_WARNING(TEXT("Could not resolve tool path relative to an owning package: %s."), *Object->GetPathName()); + return false; + } + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniTools_ExcludeToolFromCategory", "Houdini Tools: Excluding tool from category"), + PackageAsset); + + PackageAsset->Modify(); + + // At this point, we either have the category, or we need to add it. + // NOTE: We always perform category matching against the 'filename' of the asset + FCategoryRules& CategoryRules = PackageAsset->Categories.FindOrAdd(CategoryName); + if (!CategoryRules.Exclude.Contains(RelToolPath)) + { + CategoryRules.Exclude.Add( RelToolPath ); + } + + PackageAsset->MarkPackageDirty(); + + return true; +} + + +bool +FHoudiniToolsEditor::CanRemoveToolExcludeFromPackageCategory(UObject* Object, const FString& CategoryName) +{ + if (!IsValid(Object)) + { + return false; + } + + UHoudiniToolsPackageAsset* PackageAsset = FindOwningToolsPackage(Object); + if (!IsValid(PackageAsset)) + { + HOUDINI_LOG_WARNING(TEXT("[ExcludeToolFromCategory] Could not find owning package")); + return false; + } + + const bool bHasCategory = PackageAsset->Categories.Contains(CategoryName); + if (!bHasCategory) + { + // The category doesn't exist, and we don't want to add it. + return false; + } + + // Attempt to resolve the tool path relative to its owning package. + FString RelToolPath; + if (!ResolveHoudiniAssetRelativePath(Object, RelToolPath)) + { + HOUDINI_LOG_WARNING(TEXT("Could not resolve tool path relative to an owning package: %s."), *Object->GetPathName()); + return false; + } + + // NOTE: We always perform exclude pattern matching against the relative path of the asset + const FCategoryRules& CategoryRules = PackageAsset->Categories.FindChecked(CategoryName); + return CategoryRules.Exclude.Contains(RelToolPath); +} + + +bool +FHoudiniToolsEditor::RemoveToolExcludeFromPackageCategory(UObject* Object, const FString& CategoryName) +{ + if (!IsValid(Object)) + { + return false; + } + + UHoudiniToolsPackageAsset* PackageAsset = FindOwningToolsPackage(Object); + if (!IsValid(PackageAsset)) + { + HOUDINI_LOG_WARNING(TEXT("[ExcludeToolFromCategory] Could not find owning package")); + return false; + } + + if (!PackageAsset->Categories.Contains(CategoryName)) + { + // Category does not exist + return false; + } + + // Attempt to resolve the tool path relative to its owning package. + FString RelToolPath; + if (!ResolveHoudiniAssetRelativePath(Object, RelToolPath)) + { + HOUDINI_LOG_WARNING(TEXT("Could not resolve tool path relative to an owning package: %s."), *Object->GetPathName()); + return false; + } + + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniTools_ExcludeToolFromCategory", "Houdini Tools: Excluding tool from category"), + PackageAsset); + + PackageAsset->Modify(); + + // NOTE: We always perform exclude pattern matching against the relative path of the asset + FCategoryRules& CategoryRules = PackageAsset->Categories.FindChecked(CategoryName); + CategoryRules.Exclude.Remove( RelToolPath ); + + PackageAsset->MarkPackageDirty(); + + return true; +} + + +void +FHoudiniToolsEditor::UpdateHoudiniToolListFromProject(bool bIgnoreExcludePatterns) +{ + const UHoudiniEngineEditorSettings* HoudiniEngineEditorSettings = GetDefault(); + + Categories.Empty(); + + TArray ToolsPackages; + FindHoudiniToolsPackages(ToolsPackages); + + auto AddToolToCategoriesFn = [this](TSharedPtr HoudiniTool, const TArray& MatchingCategories) + { + for(const FString& CategoryName : MatchingCategories) + { + TSharedPtr& CategoryTools = Categories.FindOrAdd(FHoudiniToolCategory(CategoryName, HoudiniTool->CategoryType)); + if (!CategoryTools.IsValid()) + { + CategoryTools = MakeShared(); + } + + CategoryTools->Tools.Add(HoudiniTool); + CategoryTools->Packages.Add(HoudiniTool->ToolsPackage); + } + }; + + auto AffirmToolCategoryFn = [this](const FString& CategoryName, const EHoudiniToolCategoryType CategoryType) + { + TSharedPtr& ToolList = Categories.FindOrAdd(FHoudiniToolCategory(CategoryName, CategoryType)); + if (!ToolList.IsValid()) + { + ToolList = MakeShared(); + } + }; + + // Process categories defined in Tools Packages. + for (const UHoudiniToolsPackageAsset* ToolsPackage : ToolsPackages) + { + if (!IsValid(ToolsPackage)) + { + continue; + } + + // Ensure that we have categories for each category defined in this package to allow us to display empty + // categories. + for (auto& Entry : ToolsPackage->Categories) + { + const FString& CategoryName = Entry.Key; + AffirmToolCategoryFn(CategoryName, EHoudiniToolCategoryType::Package); + } + + // Find all tools in this package + TArray> PackageTools; + FindHoudiniToolsInPackage(ToolsPackage, PackageTools); + + // Apply category rules to package tools + for(TSharedPtr SharedHoudiniTool : PackageTools) + { + // We cannot use shared FHoudiniTools in list views, so we'll create unique copies for the items + // to be used in slate views. + TSharedPtr HoudiniTool = MakeShareable(new FHoudiniTool()); + *HoudiniTool = *SharedHoudiniTool; + + TMap CategoryRules; + for(auto& Entry : ToolsPackage->Categories) + { + CategoryRules.Add(FHoudiniToolCategory(Entry.Key, EHoudiniToolCategoryType::Package), Entry.Value); + } + + // Find the matching categories from the package + TArray MatchingCategories, ExcludedCategories; + ApplyCategories(CategoryRules, HoudiniTool, bIgnoreExcludePatterns, MatchingCategories, ExcludedCategories); + HoudiniTool->CategoryType = EHoudiniToolCategoryType::Package; + HoudiniTool->ExcludedFromCategories.Append(ExcludedCategories); + + // Add this tool to the respective category entries + AddToolToCategoriesFn(HoudiniTool, MatchingCategories); + } + } + + // Process User defined categories + if (HoudiniEngineEditorSettings) + { + const TMap& UserCategories = HoudiniEngineEditorSettings->UserToolCategories; + for (auto& Entry : UserCategories) + { + const FString& CategoryName = Entry.Key; + const FUserCategoryRules& Rules = Entry.Value; + + // Ensure that we have category entry to allow us to display empty categories + AffirmToolCategoryFn(CategoryName, EHoudiniToolCategoryType::User); + + for(const FUserPackageRules& PackageRules : Rules.Packages) + { + // Find all tools in this package + // TODO: Cache packages from previous for loop so that we don't have to do this work twice + TArray> PackageTools; + FindHoudiniToolsInPackage(PackageRules.ToolsPackageAsset, PackageTools); + + // Apply category rules to each package tool + for(const TSharedPtr& SharedHoudiniTool : PackageTools) + { + TSharedPtr HoudiniTool = MakeShareable(new FHoudiniTool()); + *HoudiniTool = *SharedHoudiniTool; + + const UObject* AssetObject = HoudiniTool->GetAssetObject(); + + if (!IsValid(AssetObject)) + { + continue; + } + + //NOTE: We're constructing a relative path here for the purposes of include/exclude pattern matching. + // to keep things as straightforward as possible, we always use the path name of the relevant + // asset for pattern matching. We will NOT use the operator type or the tool label. + + FString HoudiniAssetRelPath; + if (!ResolveHoudiniAssetRelativePath(AssetObject, HoudiniAssetRelPath)) + { + // if we can't resolve a relative path for this HoudiniAsset tool, we can't apply category matching. + continue; + } + + TArray MatchingCategories, ExcludedCategories; + ApplyCategoryRules(HoudiniAssetRelPath, CategoryName, PackageRules.Include, PackageRules.Exclude, bIgnoreExcludePatterns, MatchingCategories, ExcludedCategories); + HoudiniTool->CategoryType = EHoudiniToolCategoryType::User; + HoudiniTool->ExcludedFromCategories.Append(ExcludedCategories); + + // Add this tool to the respective category entries + AddToolToCategoriesFn(HoudiniTool, MatchingCategories); + } + } + } + } + + // For now, ensure each category is sorted by HoudiniTool display name. + // TODO: Implement a mechanism by which we can combine absolute order and wildcards/patterns (this might require + // sorting within confined subgroups) + for(const TTuple>& Entry : Categories) + { + const TSharedPtr CategoryTools = Entry.Value; + CategoryTools->Tools.Sort([](const TSharedPtr& LHS, const TSharedPtr& RHS) -> bool + { + return LHS->Name.ToString() < RHS->Name.ToString(); + }); + } +} + +bool +FHoudiniToolsEditor::IsToolInCategory( + const FHoudiniToolCategory& InCategory, + TSharedPtr InHoudiniTool) const +{ + if (!Categories.Contains(InCategory)) + { + return false; + } + + const TSharedPtr Tools = Categories.FindChecked(InCategory); + for (const TSharedPtr& Tool : Tools->Tools) + { + if (Tool == InHoudiniTool) + { + return true; + } + } + + return false; +} + + +TSet> +FHoudiniToolsEditor::FindPackagesContainingCategory(const FString& InCategoryName) const +{ + for(const TTuple>& Entry : Categories) + { + const FHoudiniToolCategory& Category = Entry.Key; + const TSharedPtr CategoryTools = Entry.Value; + + if (Category.Name == InCategoryName) + { + return CategoryTools->Packages; + } + } + + return {}; +} + + +TSoftObjectPtr +FHoudiniToolsEditor::FindFirstPackageContainingCategory(const FString& InCategoryName) const +{ + TSet> CategoryPackages; + CategoryPackages = FindPackagesContainingCategory(InCategoryName); + + TArray> SortedPackages = CategoryPackages.Array(); + + SortedPackages.Sort([](const TSoftObjectPtr& LHS, const TSoftObjectPtr& RHS) -> bool + { + return LHS->GetPathName() < RHS->GetPathName(); + }); + + // Return the first valid Tools package asset. + for (TSoftObjectPtr PackagePtr : SortedPackages) + { + if (PackagePtr.IsValid()) + { + return PackagePtr; + } + } + + return TSoftObjectPtr(); +} + + +bool FHoudiniToolsEditor::GetHoudiniPackageDescriptionFromJSON(const FString& JsonFilePath, + TMap& OutCategories, + bool& OutImportPackageDescription, + bool& OutExportPackageDescription, + bool& OutImportToolsDescription, + bool& OutExportToolsDescription + ) +{ + if ( JsonFilePath.IsEmpty() ) + return false; + + // Read the file as a string + FString FileContents; + if ( !FFileHelper::LoadFileToString( FileContents, *JsonFilePath ) ) + { + HOUDINI_LOG_ERROR( TEXT("Failed to import Package Descriptor .json file: %s"), *JsonFilePath); + return false; + } + + // Parse it as JSON + TSharedPtr JSONObject; + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create( FileContents ); + if (!FJsonSerializer::Deserialize(Reader, JSONObject) || !JSONObject.IsValid()) + { + HOUDINI_LOG_ERROR( TEXT("Invalid json in Houdini Tool .json file: %s"), *JsonFilePath); + return false; + } + + // Categories + OutCategories.Empty(); + + // Then, we need to make sure the json file has a correponding hda + // Start by looking at the assetPath field + FString HDAFilePath = FString(); + if ( JSONObject->HasField( TEXT("categories") ) ) + { + // If the json has the assetPath field, read it from there + TArray> CategoryEntries; + + const TArray>* CategoriesArray = nullptr; + if (JSONObject->TryGetArrayField(TEXT("categories"), CategoriesArray)) + { + for (auto Value : *CategoriesArray) + { + CategoryEntries.Add(Value->AsObject()); + } + } + + for(TSharedPtr& CategoryEntry : CategoryEntries) + { + FString CategoryName; + // Category rules that will be added to the output. + FCategoryRules OutCategoryRules; + + if (CategoryEntry->HasField(TEXT("name"))) + { + CategoryName = CategoryEntry->GetStringField(TEXT("name")); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Found category entry without 'name' field. Skipping.")); + continue; + } + + if (!CategoryEntry->TryGetStringArrayField(TEXT("include"), OutCategoryRules.Include)) + { + // Categories should really have an include section, so we'll print a warning here. + HOUDINI_LOG_WARNING(TEXT("Could not parse 'include' string array for category '%s'."), *CategoryName); + } + + // Exclude sections are optional, so just quietly ignore it if its not present. + CategoryEntry->TryGetStringArrayField(TEXT("exclude"), OutCategoryRules.Exclude); + + OutCategories.Add(CategoryName, OutCategoryRules); + } + } + + auto GetBoolFieldFn = [JSONObject](const FString& FieldName, const bool DefaultValue) -> bool + { + if ( JSONObject->HasField( FieldName ) ) + { + return JSONObject->GetBoolField(FieldName); + } + return DefaultValue; + }; + + // By default, we won't enable importing and exporting of JSON data data for both packages and tools. This + // prevents situations where "default use" of the plugin starts creating external files. Instead, for pipelines + // that prefer this behaviour, it can be easily enabled in the package descriptor. + OutImportPackageDescription = GetBoolFieldFn(TEXT("reimport_package_description"), false); + OutExportPackageDescription = GetBoolFieldFn(TEXT("export_package_description"), false); + OutImportToolsDescription = GetBoolFieldFn(TEXT("reimport_tools_description"), false); + OutExportToolsDescription = GetBoolFieldFn(TEXT("export_tools_description"), false); + return true; +} + +bool FHoudiniToolsEditor::ImportExternalHoudiniAssetData(UHoudiniAsset* HoudiniAsset) +{ + if (!IsValid(HoudiniAsset)) + { + return false; + } + + if (!IsValid(HoudiniAsset->AssetImportData)) + { + return false; + } + + // Get the expected path of the Houdini Asset's JSON file. + FString JSONFilePath; + if (!GetHoudiniAssetJSONPath(HoudiniAsset, JSONFilePath)) + { + return false; + } + + // If this HoudiniAsset has a matching JSON file, import it. + bool bHasValidJSON = false; + if (FPaths::FileExists(JSONFilePath)) + { + bHasValidJSON = true; + } + + bool bResult = false; + + if (bHasValidJSON) + { + // Ingest external JSON data and cache it on the HDA. + // If a valid icon is referenced inside the JSON, it will overwrite the previously loaded icon. + UHoudiniToolData* ToolData = GetOrCreateHoudiniToolData(HoudiniAsset); + bResult = FHoudiniToolsEditor::PopulateHoudiniToolDataFromJSON(JSONFilePath, *ToolData); + } + + if (!bResult) + { + // We don't have valid JSON data, so we'll populate ToolData with default data. + UHoudiniToolData* ToolData = GetOrCreateHoudiniToolData(HoudiniAsset); + ToolData->Name = FPaths::GetBaseFilename(HoudiniAsset->AssetFileName); + ToolData->ToolTip = ToolData->Name; + ToolData->Type = EHoudiniToolType::HTOOLTYPE_GENERATOR; + ToolData->SelectionType = EHoudiniToolSelectionType::HTOOL_SELECTION_ALL; + } + + const FString IconPath = ResolveHoudiniAssetIconPath(HoudiniAsset); + if (FPaths::FileExists(IconPath)) + { + // If we have found an icon for this HDA, load it now. + UHoudiniToolData* ToolData = GetOrCreateHoudiniToolData(HoudiniAsset); + HoudiniAsset->Modify(); + HoudiniAsset->MarkPackageDirty(); + ToolData->LoadIconFromPath(IconPath); + } + else + { + // Could not find an icon. Clear any cached icon data. + UHoudiniToolData* ToolData = GetOrCreateHoudiniToolData(HoudiniAsset); + ToolData->ClearCachedIcon(); + } + + return bResult; +} + +bool +FHoudiniToolsEditor::GetHoudiniToolDescriptionFromJSON(const FString& JsonFilePath, + FString& OutName, EHoudiniToolType& OutType, EHoudiniToolSelectionType& OutSelectionType, + FString& OutToolTip, FFilePath& OutIconPath, FFilePath& OutAssetPath, FString& OutHelpURL ) +{ + if ( JsonFilePath.IsEmpty() ) + return false; + + // Read the file as a string + FString FileContents; + if ( !FFileHelper::LoadFileToString( FileContents, *JsonFilePath ) ) + { + HOUDINI_LOG_ERROR( TEXT("Failed to import Houdini Tool .json file: %s"), *JsonFilePath); + return false; + } + + // Parse it as JSON + TSharedPtr JSONObject; + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create( FileContents ); + if (!FJsonSerializer::Deserialize(Reader, JSONObject) || !JSONObject.IsValid()) + { + HOUDINI_LOG_ERROR( TEXT("Invalid json in Houdini Tool .json file: %s"), *JsonFilePath); + return false; + } + + // First, check that the tool is compatible with Unreal + bool IsCompatible = true; + if (JSONObject->HasField(TEXT("target"))) + { + IsCompatible = false; + TArray >TargetArray = JSONObject->GetArrayField(TEXT("target")); + for (TSharedPtr TargetValue : TargetArray) + { + if ( !TargetValue.IsValid() ) + continue; + + // Check the target array field contains either "all" or "unreal" + FString TargetString = TargetValue->AsString(); + if ( TargetString.Equals( TEXT("all"), ESearchCase::IgnoreCase ) + || TargetString.Equals(TEXT("unreal"), ESearchCase::IgnoreCase) ) + { + IsCompatible = true; + break; + } + } + } + + if ( !IsCompatible ) + { + // The tool is not compatible with unreal, skip it + HOUDINI_LOG_MESSAGE( TEXT("Skipped Houdini Tool due to invalid target in JSON file: %s"), *JsonFilePath ); + return false; + } + + // Then, we need to make sure the json file has a correponding hda + // Start by looking at the assetPath field + FString HDAFilePath = FString(); + if ( JSONObject->HasField( TEXT("assetPath") ) ) + { + // If the json has the assetPath field, read it from there + HDAFilePath = JSONObject->GetStringField(TEXT("assetPath")); + if (!FPaths::FileExists(HDAFilePath)) + HDAFilePath = FString(); + } + + if (HDAFilePath.IsEmpty()) + { + // Look for an hda file with the same name as the json file + HDAFilePath = JsonFilePath.Replace(TEXT(".json"), TEXT(".hda")); + if (!FPaths::FileExists(HDAFilePath)) + { + // Try .hdalc + HDAFilePath = JsonFilePath.Replace(TEXT(".json"), TEXT(".hdalc")); + if (!FPaths::FileExists(HDAFilePath)) + { + // Try .hdanc + HDAFilePath = JsonFilePath.Replace(TEXT(".json"), TEXT(".hdanc")); + if (!FPaths::FileExists(HDAFilePath)) + HDAFilePath = FString(); + } + } + } + + if ( HDAFilePath.IsEmpty() ) + { + HOUDINI_LOG_ERROR(TEXT("Could not find the hda for the Houdini Tool .json file: %s"), *JsonFilePath); + return false; + } + + // Create a new asset. + OutAssetPath = FFilePath{ HDAFilePath }; + + // Read the tool name + OutName = FPaths::GetBaseFilename(JsonFilePath); + if ( JSONObject->HasField(TEXT("name") ) ) + OutName = JSONObject->GetStringField(TEXT("name")); + + // Read the tool type + OutType = EHoudiniToolType::HTOOLTYPE_OPERATOR_SINGLE; + if ( JSONObject->HasField( TEXT("toolType") ) ) + { + FString ToolType = JSONObject->GetStringField(TEXT("toolType")); + if ( ToolType.Equals( TEXT("GENERATOR"), ESearchCase::IgnoreCase ) ) + OutType = EHoudiniToolType::HTOOLTYPE_GENERATOR; + else if (ToolType.Equals( TEXT("OPERATOR_SINGLE"), ESearchCase::IgnoreCase ) ) + OutType = EHoudiniToolType::HTOOLTYPE_OPERATOR_SINGLE; + else if (ToolType.Equals(TEXT("OPERATOR_MULTI"), ESearchCase::IgnoreCase)) + OutType = EHoudiniToolType::HTOOLTYPE_OPERATOR_MULTI; + else if (ToolType.Equals(TEXT("BATCH"), ESearchCase::IgnoreCase)) + OutType = EHoudiniToolType::HTOOLTYPE_OPERATOR_BATCH; + } + + // Read the tooltip + OutToolTip = FString(); + if ( JSONObject->HasField( TEXT("toolTip") ) ) + { + OutToolTip = JSONObject->GetStringField(TEXT("toolTip")); + } + + // Read the help url + OutHelpURL = FString(); + if (JSONObject->HasField(TEXT("helpURL"))) + { + OutHelpURL = JSONObject->GetStringField(TEXT("helpURL")); + } + + // Read the icon path + FString IconPath = FString(); + if (JSONObject->HasField(TEXT("iconPath"))) + { + // If the json has the iconPath field, read it from there + IconPath = JSONObject->GetStringField(TEXT("iconPath")); + if (!FPaths::FileExists(IconPath)) + { + // Try to use this as a relative path and see if we can find a file + IconPath = FPaths::Combine(FPaths::GetPath(JsonFilePath), IconPath); + if (!FPaths::FileExists(IconPath)) + { + // We still couldn't find anything. Giving up. + IconPath = FString(); + } + } + + } + + if (IconPath.IsEmpty()) + { + // Look for a png file with the same name as the json file + IconPath = JsonFilePath.Replace(TEXT(".json"), TEXT(".png")); + if (!FPaths::FileExists(IconPath)) + { + // We don't want to embed the default houdini tool icon. We only want to + // embed custom icons. This allows the UI to change the default icon without + // having to modify each asset. + IconPath = FString(); + } + } + + OutIconPath = FFilePath{ IconPath }; + + // Read the selection types + FString SelectionType = TEXT("All"); + if ( JSONObject->HasField(TEXT("UE_SelectionType")) ) + SelectionType = JSONObject->GetStringField( TEXT("UE_SelectionType") ); + + if ( SelectionType.Equals( TEXT("CB"), ESearchCase::IgnoreCase ) ) + OutSelectionType = EHoudiniToolSelectionType::HTOOL_SELECTION_CB_ONLY; + else if ( SelectionType.Equals( TEXT("World"), ESearchCase::IgnoreCase ) ) + OutSelectionType = EHoudiniToolSelectionType::HTOOL_SELECTION_WORLD_ONLY; + else + OutSelectionType = EHoudiniToolSelectionType::HTOOL_SELECTION_ALL; + + return true; +} + +UHoudiniToolsPackageAsset* +FHoudiniToolsEditor::ImportExternalToolsPackage( + const FString& DestDir, + const FString& PackageJsonPath, + const bool bForceReimport) +{ + if (!FPaths::FileExists(PackageJsonPath)) + { + HOUDINI_LOG_WARNING(TEXT("Unable to import tool package. JSON File does not exist: %s"), *PackageJsonPath); + return nullptr; + } + + + if (!bForceReimport) + { + // If the destination directory doesn't contain any assets, we'll allow the import to take place. + const FString PackageAssetPath = DestDir / FHoudiniToolsRuntimeUtils::GetPackageUAssetName(); + if (_DoesAssetExist(PackageAssetPath)) + { + HOUDINI_LOG_WARNING(TEXT("Unable to import tool package. Asset already exists: %s"), *PackageAssetPath); + return nullptr; + } + } + + // Import the package description json data + + TMap CategoryRules; + bool bImportPackageDescription = false; + bool bExportPackageDescription = false; + bool bImportToolsDescription = false; + bool bExportToolsDescription = false; + if (!GetHoudiniPackageDescriptionFromJSON( + PackageJsonPath, + CategoryRules, + bImportPackageDescription, + bExportPackageDescription, + bImportToolsDescription, + bExportToolsDescription)) + { + HOUDINI_LOG_WARNING(TEXT("Cannot load package description: %s"), *PackageJsonPath); + return nullptr; + } + + // Create the package + UHoudiniToolsPackageAssetFactory* Factory = NewObject(); + const FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + UHoudiniToolsPackageAsset* Asset = Cast(AssetToolsModule.Get().CreateAsset( + FHoudiniToolsRuntimeUtils::GetPackageUAssetName(), DestDir, + UHoudiniToolsPackageAsset::StaticClass(), Factory, FName("ContentBrowserNewAsset"))); + + if (!IsValid(Asset)) + { + HOUDINI_LOG_WARNING(TEXT("Could not create tools package: %s/%s"), *DestDir, *FHoudiniToolsRuntimeUtils::GetPackageUAssetName()); + return nullptr; + } + + Asset->Modify(); + Asset->Categories = CategoryRules; + Asset->bReimportPackageDescription = bImportPackageDescription; + Asset->bExportPackageDescription = bExportPackageDescription; + Asset->bReimportToolsDescription = bImportToolsDescription; + Asset->bExportToolsDescription = bExportToolsDescription; + + Asset->ExternalPackageDir.Path = FPaths::GetPath(PackageJsonPath); + + FHoudiniEngineRuntimeUtils::DoPostEditChangeProperty(Asset, "ExternalPackageDir"); + + return Asset; +} + +UHoudiniToolsPackageAsset* FHoudiniToolsEditor::ImportOrCreateToolsPackage( + const FString& DestDir, + const FString& ExternalPackageDir, + const FString& DefaultCategoryName) +{ + // If there is an external JSON file, import it. Otherwise, create a new package. + + UHoudiniToolsPackageAsset* PackageAsset = nullptr; + + const FString JSONFilePath = FPaths::Combine(DestDir, FHoudiniToolsRuntimeUtils::GetPackageJSONName()); + if (FPaths::FileExists(JSONFilePath)) + { + // Try to import the external package. + PackageAsset = ImportExternalToolsPackage(DestDir, JSONFilePath, true); + } + + if (!IsValid(PackageAsset)) + { + // Create a new, default, package and configure it to point to the external package dir + PackageAsset = FHoudiniToolsEditor::CreateToolsPackageAsset( + DestDir, + DefaultCategoryName, + ExternalPackageDir + ); + } + + return PackageAsset; +} + +bool +FHoudiniToolsEditor::ReimportExternalToolsPackageDescription(UHoudiniToolsPackageAsset* PackageAsset) +{ + if (!IsValid(PackageAsset)) + { + return false; + } + + const FString JSONPath = FHoudiniToolsRuntimeUtils::GetHoudiniToolsPackageJSONPath(PackageAsset); + + if (!PackageAsset->bReimportPackageDescription) + { + // This package does not permit package description reimports. Stop now. + HOUDINI_LOG_WARNING(TEXT("bReimportPackageDescription is false for Tools package '%s'. Skipping"), *PackageAsset->GetPathName()); + return false; + } + + return PopulateHoudiniToolsPackageFromJSON(JSONPath, *PackageAsset); +} + +bool +FHoudiniToolsEditor::ReimportPackageHDAs(const UHoudiniToolsPackageAsset* PackageAsset, const bool bOnlyMissing, int* NumImportedHDAs) +{ + if (!IsValid(PackageAsset)) + { + return false; + } + + const FString ExternalPackageDir = GetAbsoluteToolsPackagePath(PackageAsset) + TEXT("/"); + if (!FPaths::DirectoryExists(ExternalPackageDir)) + { + HOUDINI_LOG_WARNING(TEXT("Could not find external package dir: %s"), *ExternalPackageDir); + return false; + } + + FToolsWrapper::CheckValidEditorAssetSubsystem(); + + // First, collect all the internal HDAs present in this package. + // We'll be removing entries from this array while reimporting any external HDAs. + // If anything is left over in this array, it means they have a different external + // source and will have to be reimported manually. + TArray InternalHoudiniAssets; + FindHoudiniAssetsInPackage(PackageAsset, InternalHoudiniAssets); + + // Find all HDAs in the external package directory + const FString PackageAssetDir = FPaths::GetPath( PackageAsset->GetPathName() ); + + TArray PackageFiles; + IFileManager::Get().FindFilesRecursive(PackageFiles, *ExternalPackageDir, TEXT("*"), true, false); + + + for(const FString& ExternalHDAFile : PackageFiles) + { + if (ExternalHDAFile.MatchesWildcard("*/backup/*")) + { + // Ignore anything in a backup directory. + continue; + } + + // Check whether this is an .hda, .hdalc, .hdanc file: + const FString Extension = FPaths::GetExtension(ExternalHDAFile); + if (!(Extension == TEXT("hda") || Extension == TEXT("hdalc") || Extension == TEXT("hdanc"))) + { + // this is not the hda we're looking for. + continue; + } + + // NOTE the HDAFile paths are absolute. Convert them to relative and start checking whether we can import them. + FString RelExternalHDAFile = ExternalHDAFile; + FPaths::MakePathRelativeTo(RelExternalHDAFile, *ExternalPackageDir); + + if (NumImportedHDAs) + { + (*NumImportedHDAs)++; + } + + // Check if the HDA already exists on the game project + const FString ToolName = ObjectTools::SanitizeObjectName( FPaths::GetBaseFilename( RelExternalHDAFile ) ); + const FString RelToolPath = ObjectTools::SanitizeObjectPath( FPaths::GetPath(RelExternalHDAFile) ); + const FString ToolDir = FPaths::Combine(PackageAssetDir, RelToolPath); + const FString ToolPath = FPaths::Combine(ToolDir, ToolName); + const FString ToolAssetRefPath = FString::Printf(TEXT("HoudiniAsset'%s.%s'"), *ToolPath, *ToolName); + + // We are now ready to import the HDA. + // If it doesn't exist, create a new UHoudiniAsset. If it already exists, reimport it using the above ToolPath. + + // Construct the asset's ref +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + TSoftObjectPtr HoudiniAssetRef = TSoftObjectPtr(FSoftObjectPath(ToolAssetRefPath)); +#else + TSoftObjectPtr HoudiniAssetRef(ToolAssetRefPath); +#endif + UHoudiniAsset* LoadedAsset = nullptr; + + // + // See if the HDA needs to be imported in Unreal, or just loaded + bool bHDAExists = false; + if ( !HoudiniAssetRef.IsValid() ) + { + if (_DoesAssetExist(ToolPath)) + { + // Try to load the asset, if it exists + LoadedAsset = HoudiniAssetRef.LoadSynchronous(); + if ( IsValid(LoadedAsset) ) + { + bHDAExists = HoudiniAssetRef.IsValid(); + } + } + } + else + { + bHDAExists = true; + LoadedAsset = HoudiniAssetRef.LoadSynchronous(); + } + + if (bHDAExists && bOnlyMissing) + { + // We're only interesting in missing HDAs + continue; + } + + if (!bHDAExists) + { + // We check an edge case here where an asset might exist at the toolpath, but it's not an HoudiniAsset. + if (_DoesAssetExist(ToolPath)) + { + // Scatter/prox/va.he_proximity_scatter.1.0.hda -> /Game/HoudiniEngine/Tools/Scatter/va_he_proximity_scatter_1_0 + HOUDINI_LOG_WARNING(TEXT("Cannot not reimport existing HoudiniAsset due to invalid type: %s"), *ToolPath); + continue; + } + } + + // If we have reached this point, the HDA needs to be imported or created. + + if (bHDAExists) + { + // Reimport existing asset from the current tool path + + InternalHoudiniAssets.Remove(LoadedAsset); + FToolsWrapper::ReimportAsync(LoadedAsset, false, true, ExternalHDAFile); + } + else + { + // Create / import a new one + // Automatically import the HDA and create a uasset for it + FAssetToolsModule& AssetToolsModule = FModuleManager::Get().LoadModuleChecked("AssetTools"); + TArray FileNames; + FileNames.Add(ExternalHDAFile); + + if ( !FToolsWrapper::DoesDirectoryExist(ToolDir) ) + { + FToolsWrapper::MakeDirectory(ToolDir); + } + + UAutomatedAssetImportData* ImportData = NewObject(); + ImportData->bReplaceExisting = true; + ImportData->bSkipReadOnly = true; + ImportData->Filenames = FileNames; + ImportData->DestinationPath = ToolDir; + ImportData->FactoryName = UHoudiniAssetFactory::StaticClass()->GetName(); + TArray AssetArray = AssetToolsModule.Get().ImportAssetsAutomated(ImportData); + if ( AssetArray.Num() <= 0 ) + continue; + + HoudiniAssetRef = Cast< UHoudiniAsset >(AssetArray[0]); + if (!HoudiniAssetRef.IsValid() ) + continue; + + // // Try to save the newly created package + // UPackage* Pckg = Cast( HoudiniAssetRef->GetOuter() ); + // if (IsValid(Pckg) && Pckg->IsDirty() ) + // { + // Pckg->FullyLoad(); + // UPackage::SavePackage( + // Pckg, nullptr, EObjectFlags::RF_Public | EObjectFlags::RF_Standalone, + // *FPackageName::LongPackageNameToFilename( Pckg->GetName(), FPackageName::GetAssetPackageExtension() ) ); + // } + HoudiniAssetRef->MarkPackageDirty(); + } + + } + + // reimport any HDAs left over in this array + for (UHoudiniAsset* HoudiniAsset : InternalHoudiniAssets) + { + FToolsWrapper::ReimportAsync(HoudiniAsset, false, true); + } + + return false; +} + +void +FHoudiniToolsEditor::CreateUniqueAssetIconBrush(TSharedPtr HoudiniTool) +{ + if (!HoudiniTool.IsValid()) + { + return; + } + + FString ResourceName; + bool bHasSourceImage = false; + FImage SrcImage; + FString RawDataMD5; + + UHoudiniPreset* HoudiniPreset = HoudiniTool->HoudiniPreset.LoadSynchronous(); + if (IsValid(HoudiniPreset)) + { + // We have a preset we must try to use this icon. Don't fall back to the SourceAsset icon. We want the + // icon (and preset thumbnail) to be managed independently from the source asset, otherwise things can get + // visually confusing in the frontend. + + const FHImageData& SrcImageData = HoudiniPreset->IconImageData; + + if (SrcImageData.SizeX == 0 || SrcImageData.SizeY == 0 || SrcImageData.RawData.Num() == 0) + { + return; + } + + if (SrcImageData.Format != ERawImageFormat::BGRA8) + { + HOUDINI_LOG_WARNING(TEXT("Cannot create unique icon brush. Requires raw image format BGRA8.")); + return; + } + + // Copy cached image data to SrcImage + SrcImageData.ToImage(SrcImage); + RawDataMD5 = SrcImageData.RawDataMD5; + bHasSourceImage = true; + ResourceName = UObject::RemoveClassPrefix( *HoudiniPreset->GetPathName() ); + } + else + { + // We dont have a valid source image yet. Try to get one from the Houdini Asset. + const UHoudiniAsset* HoudiniAsset = HoudiniTool->HoudiniAsset.LoadSynchronous(); + if (IsValid(HoudiniAsset) && IsValid(HoudiniAsset->HoudiniToolData)) + { + // We have a valid HoudiniAsset in the tool data. Let's try to create an icon brush. + + const FHImageData& SrcImageData = HoudiniAsset->HoudiniToolData->IconImageData; + + if (SrcImageData.SizeX == 0 || SrcImageData.SizeY == 0 || SrcImageData.RawData.Num() == 0) + { + return; + } + + if (SrcImageData.Format != ERawImageFormat::BGRA8) + { + HOUDINI_LOG_WARNING(TEXT("Cannot create unique icon brush. Requires raw image format BGRA8.")); + return; + } + + // Copy cached image data to SrcImage + SrcImageData.ToImage(SrcImage); + RawDataMD5 = SrcImageData.RawDataMD5; + bHasSourceImage = true; + ResourceName = UObject::RemoveClassPrefix( *HoudiniAsset->GetPathName() ); + } + } + + if (bHasSourceImage) + { + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + if (!SrcImage.IsImageInfoValid()) + { + return; + } +#endif + + + + + // Append the hash of the image data. This will allow the icon to create a unique resource + // for different image data, even if the icon image path is the same. At the same time, we're + // able to reuse image resource that haven't changed. + const FString UniqueResourceName = FString::Format(TEXT("{0}_{1}"), {ResourceName, RawDataMD5}); + const FName UniqueResourceFName(UniqueResourceName); + + + if (HoudiniTool->Icon.IsValid()) + { + // We have an existing slate brush. We'll reuse it. + if ( HoudiniTool->Icon->GetResourceName().Compare(FName(UniqueResourceName)) == 0 ) + { + return; + } + } + + const FString TextureName = FString::Format(TEXT("{0}_{1}"), {FPaths::GetBaseFilename(ResourceName), RawDataMD5}); + + UTexture2D* IconTexture = nullptr; + + if (CachedTextures.Contains(RawDataMD5)) + { + // Reuse the previously cached texture + IconTexture = CachedTextures.FindChecked(RawDataMD5); + } + + if (!IsValid(IconTexture)) + { + // Create a new texture and cache it + IconTexture = UTexture2D::CreateTransient(SrcImage.SizeX, SrcImage.SizeY, PF_B8G8R8A8, FName(TextureName)); + if ( IconTexture ) + { + IconTexture->bNotOfflineProcessed = true; + uint8* MipData = static_cast< uint8* >( IconTexture->GetPlatformData()->Mips[ 0 ].BulkData.Lock( LOCK_READ_WRITE ) ); + + // Bulk data was already allocated for the correct size when we called CreateTransient above + FMemory::Memcpy( MipData, &SrcImage.RawData[0], IconTexture->GetPlatformData()->Mips[ 0 ].BulkData.GetBulkDataSize() ); + + IconTexture->GetPlatformData()->Mips[ 0 ].BulkData.Unlock(); + IconTexture->UpdateResource(); + + // Set compression options. + IconTexture->SRGB = true; + IconTexture->CompressionSettings = TC_EditorIcon; + IconTexture->MipGenSettings = TMGS_FromTextureGroup; + IconTexture->DeferCompression = true; + IconTexture->PostEditChange(); + + // Cache the texture so that we can reuse it + CachedTextures.Add(RawDataMD5, IconTexture); + IconTexture->AddToRoot(); + HoudiniTool->IconTexture = IconTexture; + } + } + + // Create a new brush, using the current IconTexture. + const TSharedPtr Icon = MakeShareable( new FSlateDynamicImageBrush( + IconTexture, + FVector2D(SrcImage.SizeX, SrcImage.SizeY), + UniqueResourceFName + )); + + HoudiniTool->Icon = Icon; + return; + } + + HoudiniTool->Icon = nullptr; + HoudiniTool->IconTexture = nullptr; +} + + +void +FHoudiniToolsEditor::CaptureThumbnailFromViewport(UObject* Asset) +{ + // Capture thumbnail for the preset from the viewport, and use it as the preset icon. + FViewport* Viewport = GEditor->GetActiveViewport(); + if ( ensure(GCurrentLevelEditingViewportClient) && ensure(Viewport) ) + { + //have to re-render the requested viewport + FLevelEditorViewportClient* OldViewportClient = GCurrentLevelEditingViewportClient; + //remove selection box around client during render + GCurrentLevelEditingViewportClient = NULL; + Viewport->Draw(); + + + TArray AssetDataArray; + AssetDataArray.Add( FHoudiniToolsEditor::GetAssetDataByObject(Asset) ); + AssetViewUtils::CaptureThumbnailFromViewport(Viewport, AssetDataArray); + + //redraw viewport to have the yellow highlight again + GCurrentLevelEditingViewportClient = OldViewportClient; + Viewport->Draw(); + } +} + + +bool +FHoudiniToolsEditor::CopyThumbnailToImage(const UObject* Asset, FHImageData& OutImageData) +{ + FObjectThumbnail Thumbnail; + if (ThumbnailTools::AssetHasCustomThumbnail(Asset->GetFullName(), Thumbnail)) + { + TArray ImageData = Thumbnail.GetUncompressedImageData(); + OutImageData.RawData = MoveTemp(ImageData); + OutImageData.SizeX = Thumbnail.GetImageWidth(); + OutImageData.SizeY = Thumbnail.GetImageHeight(); + OutImageData.Format = ERawImageFormat::BGRA8; + return true; + } + return false; +} + + +bool +FHoudiniToolsEditor::PopulateHoudiniToolDataFromJSON(const FString& JSONFilePath, UHoudiniToolData& OutToolData) +{ + OutToolData.Modify(); + + // Attempt to extract the Tool info from the JSON file + return GetHoudiniToolDescriptionFromJSON( + JSONFilePath, + OutToolData.Name, + OutToolData.Type, + OutToolData.SelectionType, + OutToolData.ToolTip, + OutToolData.IconSourcePath, + OutToolData.SourceAssetPath, + OutToolData.HelpURL); +} + +bool FHoudiniToolsEditor::PopulateHoudiniToolsPackageFromJSON(const FString& JSONFilePath, UHoudiniToolsPackageAsset& OutPackage) +{ + OutPackage.Modify(); + + const bool bResult = GetHoudiniPackageDescriptionFromJSON( + JSONFilePath, + OutPackage.Categories, + OutPackage.bReimportPackageDescription, + OutPackage.bExportPackageDescription, + OutPackage.bReimportToolsDescription, + OutPackage.bExportToolsDescription); + + OutPackage.PostEditChange(); + OutPackage.MarkPackageDirty(); + return bResult; +} + +TSharedPtr FHoudiniToolsEditor::MakeDefaultJSONObject(const UHoudiniAsset* HoudiniAsset) +{ + if (!IsValid(HoudiniAsset)) + return nullptr; + + // Start by building a JSON object from the tool + TSharedPtr JSONObject = MakeShareable(new FJsonObject); + + // Mark the target as unreal only + TArray< TSharedPtr > TargetValue; + TargetValue.Add(MakeShareable(new FJsonValueString(TEXT("unreal")))); + JSONObject->SetArrayField(TEXT("target"), TargetValue ); + + const UHoudiniToolData* ToolData = HoudiniAsset->HoudiniToolData; + const bool bValidToolData = IsValid(ToolData); + + // The Tool Name + const FString ToolName = bValidToolData ? ToolData->Name : FPaths::GetBaseFilename(HoudiniAsset->AssetFileName); + JSONObject->SetStringField(TEXT("name"), ToolName); + + // Tooltype + FString ToolTypeString; + const EHoudiniToolType ToolType = bValidToolData ? ToolData->Type : EHoudiniToolType::HTOOLTYPE_GENERATOR; + ToolTypeToString(ToolType, ToolTypeString); + JSONObject->SetStringField(TEXT("toolType"), ToolTypeString); + + // Tooltip + if ( bValidToolData && !ToolData->ToolTip.IsEmpty() ) + JSONObject->SetStringField(TEXT("toolTip"), ToolData->ToolTip); + + // Help URL + if ( bValidToolData && !ToolData->HelpURL.IsEmpty() ) + JSONObject->SetStringField(TEXT("helpURL"), ToolData->HelpURL); + + // // IconPath + // if ( bValidToolData && !ToolData->IconResource ) + // { + // const FString IconPath = ToolData->IconResource->GetName(); + // JSONObject->SetStringField(TEXT("iconPath"), IconPath); + // } + + // Selection Type + FString SelectionTypeString; + const EHoudiniToolSelectionType SelectionType = bValidToolData ? ToolData->SelectionType : EHoudiniToolSelectionType::HTOOL_SELECTION_ALL; + FHoudiniToolsEditor::SelectionTypeToString(SelectionType, SelectionTypeString); + JSONObject->SetStringField(TEXT("UE_SelectionType"), SelectionTypeString); + + return JSONObject; +} + +TSharedPtr FHoudiniToolsEditor::MakeDefaultJSONObject(const UHoudiniToolsPackageAsset* ToolsPackage) +{ + if (!IsValid(ToolsPackage)) + return nullptr; + + // A default JSON file for a tools package will contain a single category using the directory + // name of the ToolsPackage. + // All reimport/export settings will be disabled by default. + + const FString AssetPath = FPaths::GetPath(ToolsPackage->GetPathName()); + const FString PackageName = FPaths::GetBaseFilename(AssetPath); + + // Start by building a JSON object from the tool + TSharedPtr JSONObject = MakeShareable(new FJsonObject); + + // Add an array for category entries + TArray< TSharedPtr > CategoryEntries; + + // Populate a default category with an include entry, and an empty exclude list. + const TSharedPtr CategoryEntry = MakeShareable(new FJsonObject); + CategoryEntry->SetStringField(TEXT("name"), PackageName); + + TArray< TSharedPtr > IncludeValues; + IncludeValues.Add(MakeShareable( new FJsonValueString(TEXT("*")) )); + CategoryEntry->SetArrayField(TEXT("include"), IncludeValues); + + const TArray< TSharedPtr > ExcludeValues; + CategoryEntry->SetArrayField(TEXT("exclude"), ExcludeValues); + + CategoryEntries.Add( MakeShareable( new FJsonValueObject(CategoryEntry) ) ); + + // Add the default category entry. + JSONObject->SetArrayField(TEXT("categories"), CategoryEntries); + + // Set defaults for the reimport/export settings. + + JSONObject->SetBoolField(TEXT("export_package_description"), false); + JSONObject->SetBoolField(TEXT("reimport_package_description"), false); + + JSONObject->SetBoolField(TEXT("reimport_tools_description"), false); + JSONObject->SetBoolField(TEXT("export_tools_description"), false); + + return JSONObject; +} + +bool FHoudiniToolsEditor::WriteDefaultJSONFile(const UHoudiniAsset* HoudiniAsset, const FString& JSONFilePath) +{ + if (!IsValid(HoudiniAsset)) + { + return false; + } + + const TSharedPtr JSONObject = MakeDefaultJSONObject(HoudiniAsset); + if (!JSONObject.IsValid()) + { + return false; + } + + return WriteJSONFile(JSONFilePath, JSONObject); +} + +bool FHoudiniToolsEditor::WriteDefaultJSONFile(const UHoudiniToolsPackageAsset* ToolsPackage, const FString& JSONFilePath) +{ + if (!IsValid(ToolsPackage)) + { + return false; + } + + const TSharedPtr JSONObject = MakeDefaultJSONObject(ToolsPackage); + if (!JSONObject.IsValid()) + { + return false; + } + + return WriteJSONFile(JSONFilePath, JSONObject); +} + +bool FHoudiniToolsEditor::WriteJSONFromHoudiniAsset(const UHoudiniAsset* HoudiniAsset, const FString& JSONFilePath) +{ + if (!IsValid(HoudiniAsset)) + { + return false; + } + + TSharedPtr JSONObject; + if (FPaths::FileExists(JSONFilePath)) + { + // This asset already has a JSON file. + if (!LoadJSONFile(JSONFilePath, JSONObject)) + { + // We were unable to parse it, so we'll fail here. We don't want to override + // files if we can't preserve its contents. + return false; + } + } + + if (!JSONObject.IsValid()) + { + // We don't have a valid JSON object, it means we should be creating a new default JSON object for this asset + JSONObject = MakeDefaultJSONObject(HoudiniAsset); + if (!JSONObject.IsValid()) + { + return false; + } + } + + // NOTE: If we start writing out HDA JSON files in multiple places we should move the following + // into a wrapper class with an specific accessors so that the field manipulations can be unified in a single place. + + FString Name; + FString Tooltip; + EHoudiniToolType ToolType; + EHoudiniToolSelectionType SelectionType; + FString HelpURL; + FString IconSourcePath; + + // Populate the JSON object from the Houdini Asset. + if (IsValid(HoudiniAsset->HoudiniToolData)) + { + // Populate from ToolData (this is our preferred data source) + UHoudiniToolData* ToolData = HoudiniAsset->HoudiniToolData; + + Name = ToolData->Name; + Tooltip = ToolData->ToolTip; + // We don't really know what this HDA is, so we're just going with "no inputs" as a default. + ToolType = ToolData->Type; + SelectionType = ToolData->SelectionType; + HelpURL = ToolData->HelpURL; + IconSourcePath = ToolData->IconSourcePath.FilePath; + } + else + { + // Populate from any data we can extract from UHoudiniAsset without looking outside the Unreal project. + // i.e., no peeking at external json files. + Name = FPaths::GetBaseFilename(HoudiniAsset->AssetFileName); + Tooltip = Name; + // We don't really know what this HDA is, so we're just going with "no inputs" as a default. + ToolType = EHoudiniToolType::HTOOLTYPE_GENERATOR; + SelectionType = EHoudiniToolSelectionType::HTOOL_SELECTION_ALL; + } + + // Mark the target as unreal only + TArray< TSharedPtr > TargetValue; + TargetValue.Add(MakeShareable(new FJsonValueString(TEXT("unreal")))); + JSONObject->SetArrayField(TEXT("target"), TargetValue ); + + // Tool Name + if ( !Name.IsEmpty() ) + { + JSONObject->SetStringField(TEXT("name"), Name); + } + + // Tool Type + FString ToolTypeString = TEXT("GENERATOR"); + if ( ToolType == EHoudiniToolType::HTOOLTYPE_OPERATOR_SINGLE) + ToolTypeString = TEXT("OPERATOR_SINGLE"); + else if (ToolType == EHoudiniToolType::HTOOLTYPE_OPERATOR_MULTI) + ToolTypeString = TEXT("OPERATOR_MULTI"); + else if ( ToolType == EHoudiniToolType::HTOOLTYPE_OPERATOR_BATCH) + ToolTypeString = TEXT("BATCH"); + + JSONObject->SetStringField(TEXT("toolType"), ToolTypeString); + + // Tooltip + JSONObject->SetStringField(TEXT("toolTip"), Tooltip); + + // Help URL + JSONObject->SetStringField(TEXT("helpURL"), HelpURL); + + // IconPath + if ( !IconSourcePath.IsEmpty() ) + { + JSONObject->SetStringField(TEXT("iconPath"), IconSourcePath); + } + + // Selection Type + FString SelectionTypeString = TEXT("All"); + if (SelectionType == EHoudiniToolSelectionType::HTOOL_SELECTION_CB_ONLY) + SelectionTypeString = TEXT("CB"); + else if (SelectionType == EHoudiniToolSelectionType::HTOOL_SELECTION_WORLD_ONLY) + SelectionTypeString = TEXT("World"); + JSONObject->SetStringField(TEXT("UE_SelectionType"), SelectionTypeString); + + WriteJSONFile(JSONFilePath, JSONObject); + + return true; +} + +bool FHoudiniToolsEditor::WriteJSONFromToolPackageAsset(const UHoudiniToolsPackageAsset* ToolsPackage, + const FString& JSONFilePath) +{ + TSharedPtr JSONObject; + if (FPaths::FileExists(JSONFilePath)) + { + // This asset already has a JSON file. + if (!LoadJSONFile(JSONFilePath, JSONObject)) + { + // We were unable to parse it, so we'll fail here. We don't want to override + // files if we can't preserve its contents. + return false; + } + } + + if (!JSONObject.IsValid()) + { + // We don't have a valid JSON object, it means we should be creating a new default JSON object for this asset + JSONObject = MakeDefaultJSONObject(ToolsPackage); + if (!JSONObject.IsValid()) + { + return false; + } + } + + // Populate the JSON Object from the asset + + if (ToolsPackage->Categories.Num() > 0) + { + // Add an array for category entries + TArray< TSharedPtr > CategoryValues; + + for( auto Entry : ToolsPackage->Categories) + { + // Create a Category entry + const FString& CategoryName = Entry.Key; + const FCategoryRules& CategoryRules = Entry.Value; + + TSharedPtr CategoryObject = MakeShareable(new FJsonObject); + CategoryObject->SetStringField(TEXT("name"), CategoryName); + + // include patterns + if (CategoryRules.Include.Num() > 0) + { + TArray< TSharedPtr > IncludeValues; + for (const FString& Pattern : CategoryRules.Include) + { + IncludeValues.Add(MakeShareable( new FJsonValueString(Pattern) )); + } + CategoryObject->SetArrayField(TEXT("include"), IncludeValues); + } + + // exclude patterns + if (CategoryRules.Include.Num() > 0) + { + TArray< TSharedPtr > ExcludeValues; + for (const FString& Pattern : CategoryRules.Exclude) + { + ExcludeValues.Add(MakeShareable( new FJsonValueString(Pattern) )); + } + CategoryObject->SetArrayField(TEXT("exclude"), ExcludeValues); + } + + CategoryValues.Add(MakeShareable(new FJsonValueObject(CategoryObject))); + } + + JSONObject->SetArrayField(TEXT("categories"), CategoryValues); + } + + JSONObject->SetBoolField(TEXT("export_package_description"), ToolsPackage->bExportPackageDescription); + JSONObject->SetBoolField(TEXT("reimport_package_description"), ToolsPackage->bReimportPackageDescription); + + JSONObject->SetBoolField(TEXT("export_tools_description"), ToolsPackage->bReimportToolsDescription); + JSONObject->SetBoolField(TEXT("reimport_tools_description"), ToolsPackage->bExportToolsDescription); + + WriteJSONFile(JSONFilePath, JSONObject); + + return true; +} + +bool FHoudiniToolsEditor::ToolTypeToString(const EHoudiniToolType ToolType, FString& OutString) +{ + switch(ToolType) + { + case EHoudiniToolType::HTOOLTYPE_GENERATOR: + { + OutString = TEXT("GENERATOR"); + return true; + } + case EHoudiniToolType::HTOOLTYPE_OPERATOR_SINGLE: + { + OutString = TEXT("OPERATOR_SINGLE"); + return true; + } + case EHoudiniToolType::HTOOLTYPE_OPERATOR_MULTI: + { + OutString = TEXT("OPERATOR_MULTI"); + return true; + } + case EHoudiniToolType::HTOOLTYPE_OPERATOR_BATCH: + { + OutString = TEXT("BATCH"); + return true; + } + default: + break; + } + return false; +} + +bool FHoudiniToolsEditor::SelectionTypeToString(const EHoudiniToolSelectionType SelectionType, FString& OutString) +{ + switch (SelectionType) + { + case EHoudiniToolSelectionType::HTOOL_SELECTION_ALL: + { + OutString = TEXT("ALL"); + return true; + } + case EHoudiniToolSelectionType::HTOOL_SELECTION_CB_ONLY: + { + OutString = TEXT("CB"); + return true; + } + case EHoudiniToolSelectionType::HTOOL_SELECTION_WORLD_ONLY: + { + OutString = TEXT("CB"); + return true; + } + default: + break; + } + + return false; +} + +bool FHoudiniToolsEditor::ImportSideFXTools(int* NumImportedHDAs) +{ + // Read the default tools from the $HFS/engine/tool folder + FDirectoryPath DefaultToolPath; + const FString HFSPath = FPaths::GetPath(FHoudiniEngine::Get().GetLibHAPILocation()); + const FString ExternalPackagePath = FPaths::Combine(HFSPath, TEXT("engine"), TEXT("tools")); + const FString PackageJSONPath = FPaths::Combine(ExternalPackagePath, FHoudiniToolsRuntimeUtils::GetPackageJSONName()); + + // Base Path of the tools directory in the Unreal project. + const FString PackageName = TEXT("SideFX"); + const FString PackageDestPath = FHoudiniToolsEditor::GetDefaultPackagePath(PackageName); + + // Path to the Tools Package asset contained in the PackageDestPath. + const FString PackageAssetPath = FHoudiniToolsEditor::GetDefaultPackageAssetPath(PackageName); + UHoudiniToolsPackageAsset* PackageAsset = nullptr; + + // See if there is an existing SideFX package that we can work with. + if (_DoesAssetExist(PackageAssetPath)) + { + // Try to load the asset + const TSoftObjectPtr SideFXPackageRef = GetPackageAssetRef(PackageDestPath); + PackageAsset = SideFXPackageRef.LoadSynchronous(); + + if (IsValid(PackageAsset)) + { + PackageAsset->Modify(); + + // We have a valid existing package. Be sure to update the package path + PackageAsset->ExternalPackageDir.Path = ExternalPackagePath; + FHoudiniEngineRuntimeUtils::DoPostEditChangeProperty(PackageAsset, "ExternalPackageDir"); + + PackageAsset->MarkPackageDirty(); + } + } + + // Regardless of whether we have found an existing SideFX package in the Unreal project, we'll reimport the + // external package. + PackageAsset = FHoudiniToolsEditor::ImportOrCreateToolsPackage( + PackageDestPath, + ExternalPackagePath, + PackageName + ); + + if (!PackageAsset) + { + // Could not import / create a package + HOUDINI_LOG_WARNING(TEXT("Error importing SideFX tools. Could not import or create HoudiniTools package.")); + return false; + } + + // Reimport HDAs for the current package. + FHoudiniToolsEditor::ReimportPackageHDAs(PackageAsset, false, NumImportedHDAs); + + return true; +} + +void FHoudiniToolsEditor::PopulatePackageWithDefaultData(UHoudiniToolsPackageAsset* PackageAsset) +{ + if (!IsValid(PackageAsset)) + { + return; + } + + FCategoryRules DefaultRules; + DefaultRules.Include.Add("*"); + + PackageAsset->Modify(); + + PackageAsset->Categories.Empty(); + PackageAsset->Categories.Add("Default Package Category", DefaultRules); +} + + +FText +FHoudiniToolsEditor::GetFavoritesCategoryName() +{ + return NSLOCTEXT("HoudiniEngine", "HoudiniTools_FavoritesCategoryName", "Favorites"); +} + + +void +FHoudiniToolsEditor::AddToolToUserCategory(const UObject* Object, const FString& CategoryName) +{ + if (!IsValid(Object)) + { + HOUDINI_LOG_ERROR(TEXT("Cannot add tool to category. Invalid asset.")); + return; + } + + UHoudiniToolsPackageAsset* Package = FHoudiniToolsEditor::FindOwningToolsPackage(Object); + if (!IsValid(Package)) + { + HOUDINI_LOG_ERROR(TEXT("Cannot add tool to category. Invalid owning package")); + return; + } + + UHoudiniEngineEditorSettings* Settings = GetMutableDefault(); + if (!Settings) + { + return; + } + + FUserCategoryRules& Rules = Settings->UserToolCategories.FindOrAdd(CategoryName); + + // Try to find an entry containing the owning package. + int FoundIndex = INDEX_NONE; + for (int i = 0; i < Rules.Packages.Num(); ++i) + { + if (Rules.Packages[i].ToolsPackageAsset == Package) + { + FoundIndex = i; + break; + } + } + + if (FoundIndex == INDEX_NONE) + { + // This package does not yet have an entry in this category. Create one now. + FoundIndex = Rules.Packages.Add(FUserPackageRules()); + Rules.Packages[FoundIndex].ToolsPackageAsset = Package; + } + + FUserPackageRules& PackageRules = Rules.Packages[FoundIndex]; + + // Get the relative tool path and add it to the package rules. + FString RelPath; + ResolveHoudiniAssetRelativePath(Object, RelPath); + + PackageRules.Include.AddUnique(RelPath); + FHoudiniEngineRuntimeUtils::DoPostEditChangeProperty(Settings, "UserToolCategories"); + + Settings->SaveConfig(); +} + +void FHoudiniToolsEditor::RemoveToolFromUserCategory(const UObject* Object, const FString& CategoryName) +{ + if (!IsValid(Object)) + { + return; + } + + UHoudiniToolsPackageAsset* Package = FHoudiniToolsEditor::FindOwningToolsPackage(Object); + if (!IsValid(Package)) + { + return; + } + + UHoudiniEngineEditorSettings* Settings = GetMutableDefault(); + if (!Settings) + { + return; + } + + if (!Settings->UserToolCategories.Contains(CategoryName)) + { + return; + } + + FUserCategoryRules& Rules = Settings->UserToolCategories.FindChecked(CategoryName); + + // Try to find an entry containing the owning package. + int FoundIndex = INDEX_NONE; + for (int i = 0; i < Rules.Packages.Num(); ++i) + { + if (Rules.Packages[i].ToolsPackageAsset == Package) + { + FoundIndex = i; + break; + } + } + + if (FoundIndex == INDEX_NONE) + { + // We don't have a record of this tool in the user categories settings. + return; + } + + FUserPackageRules PackageRules = Rules.Packages[FoundIndex]; + + // Get the relative tool path and add it to the package rules. + FString RelPath; + ResolveHoudiniAssetRelativePath(Object, RelPath); + + // Remove all instances of this tool's path. + PackageRules.Include.Remove(RelPath); + + if (PackageRules.Include.Num() == 0 && PackageRules.Exclude.Num() == 0) + { + // Remove this empty ruleset from the settings. + Rules.Packages.RemoveAt(FoundIndex); + } + else + { + // We have a non-empty ruleset. write it back into user category settings. + Rules.Packages[FoundIndex] = PackageRules; + } + + FHoudiniEngineRuntimeUtils::DoPostEditChangeProperty(Settings, "UserToolCategories"); + + Settings->SaveConfig(); +} + +bool +FHoudiniToolsEditor::UserCategoryContainsTool( + const FString& CategoryName, + const UObject* AssetObject, + const UHoudiniToolsPackageAsset* PackageAsset) +{ + if (!IsValid(AssetObject)) + { + return false; + } + + if (!IsValid(PackageAsset)) + { + return false; + } + + UHoudiniEngineEditorSettings* Settings = GetMutableDefault(); + if (!Settings) + { + return false; + } + + if (!Settings->UserToolCategories.Contains(CategoryName)) + { + return false; + } + + FUserCategoryRules& Rules = Settings->UserToolCategories.FindChecked(CategoryName); + + // Try to find an entry containing the owning package. + int FoundIndex = INDEX_NONE; + for (int i = 0; i < Rules.Packages.Num(); ++i) + { + if (Rules.Packages[i].ToolsPackageAsset == PackageAsset) + { + FoundIndex = i; + break; + } + } + + if (FoundIndex == INDEX_NONE) + { + // We don't have a record of this tool in the user categories settings. + return false; + } + + const FUserPackageRules PackageRules = Rules.Packages[FoundIndex]; + + // Get the relative tool path and add it to the package rules. + FString RelPath; + ResolveHoudiniAssetRelativePath(AssetObject, PackageAsset, RelPath); + + if (PackageRules.Include.Contains(RelPath)) + return true; + + return false; +} + +void +FHoudiniToolsEditor::GetUserCategoriesList(TArray& OutCategories) +{ + OutCategories.Empty(); + const UHoudiniEngineEditorSettings* Settings = GetDefault(); + if (!Settings) + { + return; + } + + Settings->UserToolCategories.GetKeys(OutCategories); +} + +void +FHoudiniToolsEditor::CopySettingsToPreset( + const UHoudiniCookable* HC, + const bool bApplyAssetOptions, + const bool bApplyBakeOptions, + const bool bApplyMeshGenSettings, + const bool bApplyProxyMeshGenSettings, + UHoudiniPreset* Preset) +{ + // Populate Bake options + Preset->bApplyBakeOptions = bApplyBakeOptions; + Preset->HoudiniEngineBakeOption = HC->GetHoudiniEngineBakeOption(); + Preset->bRemoveOutputAfterBake = HC->GetRemoveOutputAfterBake(); + Preset->bRecenterBakedActors = HC->GetRecenterBakedActors(); + Preset->bAutoBake = HC->IsBakeAfterNextCookEnabled(); + Preset->bReplacePreviousBake = HC->GetReplacePreviousBake(); + + // Populate Asset Settings + Preset->bApplyAssetOptions = bApplyAssetOptions; + Preset->bCookOnParameterChange = HC->GetCookOnParameterChange(); + Preset->bCookOnTransformChange = HC->GetCookOnTransformChange(); + Preset->bCookOnAssetInputCook = HC->GetCookOnCookableInputCook(); + Preset->bDoNotGenerateOutputs = HC->IsOutputless(); + Preset->bUseOutputNodes = HC->GetUseOutputNodes(); + Preset->bOutputTemplateGeos = HC->GetOutputTemplateGeos(); + + Preset->bUploadTransformsToHoudiniEngine = HC->GetUploadTransformsToHoudiniEngine(); + Preset->bLandscapeUseTempLayers = HC->GetLandscapeUseTempLayers(); + + // Populate Mesh Gen Settings + Preset->bApplyStaticMeshGenSettings = bApplyMeshGenSettings; + Preset->StaticMeshGenerationProperties = HC->GetStaticMeshGenerationProperties(); + Preset->StaticMeshBuildSettings = HC->GetStaticMeshBuildSettings(); + + // Populate Proxy Mesh Gen Settings + Preset->bApplyProxyMeshGenSettings = bApplyProxyMeshGenSettings; + Preset->bOverrideGlobalProxyStaticMeshSettings = HC->IsOverrideGlobalProxyStaticMeshSettings(); + Preset->bEnableProxyStaticMeshOverride = HC->IsProxyStaticMeshEnabled(); + Preset->bEnableProxyStaticMeshRefinementByTimerOverride = HC->IsProxyStaticMeshRefinementByTimerEnabled(); + Preset->ProxyMeshAutoRefineTimeoutSecondsOverride = HC->GetProxyMeshAutoRefineTimeoutSeconds(); + Preset->bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride = HC->IsProxyStaticMeshRefinementOnPreSaveWorldEnabled(); + Preset->bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride = HC->IsProxyStaticMeshRefinementOnPreBeginPIEEnabled(); +} + + +void +FHoudiniToolsEditor::FindPresetsForHoudiniAsset(const UHoudiniAsset* HoudiniAsset, TArray& OutPresets) +{ + if (!IsValid(HoudiniAsset)) + { + return; + } + + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + + TArray Assets; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + AssetRegistryModule.Get().GetAssetsByClass(FTopLevelAssetPath(UHoudiniPreset::StaticClass()), Assets, false); +#else + AssetRegistryModule.Get().GetAssetsByClass( UHoudiniPreset::StaticClass()->GetFName(), Assets, false ); +#endif + + // Currently we're scanning the whole project for presets. If this becomes a performance problem, we can limit preset + // scanning to the HoudiniTools search path. + + for(const FAssetData& AssetData : Assets) + { + UHoudiniPreset* Preset = Cast(AssetData.GetAsset()); + if (!IsValid(Preset)) + { + continue; + } + + if (Preset->bApplyOnlyToSource == false) + { + // This preset can be applied to any HDA. + OutPresets.Add(Preset); + continue; + } + if (Preset->bApplyOnlyToSource == true && Preset->SourceHoudiniAsset == HoudiniAsset) + { + // This preset can be applied to this HDA. + OutPresets.Add(Preset); + continue; + } + } +} + +bool +FHoudiniToolsEditor::CanApplyPresetToHoudiniCookable( + const UHoudiniPreset* Preset, + UHoudiniCookable* HC) +{ + if (!IsValid(Preset)) + { + return false; + } + if (!IsValid(HC)) + { + return false; + } + + if (IsValid(HC->GetComponent())) + { + // We can't apply presets to Node Sync components + if (HC->GetComponent()->IsA()) + return false; + } + + if (!Preset->bApplyOnlyToSource) + { + // We can apply this preset to any HoudiniAsset + return true; + } + + // We can only apply this preset to the SourceHoudiniAsset + if (!IsValid(Preset->SourceHoudiniAsset)) + { + return false; + } + return Preset->SourceHoudiniAsset == HC->GetHoudiniAsset(); +} + +void +FHoudiniToolsEditor::ApplyPresetToHoudiniCookable( + const UHoudiniPreset* Preset, + UHoudiniCookable* HC, + bool bReselectSelectedActors) +{ + if (!IsValid(HC) || !IsValid(Preset)) + return; + + if (!CanApplyPresetToHoudiniCookable(Preset, HC)) + return; + + // Try to upload changed parameters + FHoudiniParameterTranslator::UploadChangedParameters(HC->GetParameters(), HC->GetNodeId()); + + // Record a transaction for undo/redo + FScopedTransaction Transaction( + TEXT(HOUDINI_MODULE_EDITOR), + LOCTEXT("HoudiniPresets_ApplyToAssetComponent", "Apply Preset to Houdini Asset Component"), + HC->GetOuter()); + + HC->Modify(); + + if (Preset->bRevertHDAParameters) + { + // Reset parameters to default values? + for (int32 n = 0; n < HC->GetNumParameters(); ++n) + { + UHoudiniParameter* Parm = HC->GetParameterAt(n); + if (IsValid(Parm) && !Parm->IsDefault()) + { + Parm->RevertToDefault(); + } + } + } + + // Populate Bake options + if (Preset->bApplyBakeOptions) + { + HC->SetHoudiniEngineBakeOption(Preset->HoudiniEngineBakeOption); + HC->SetRemoveOutputAfterBake(Preset->bRemoveOutputAfterBake); + HC->SetRecenterBakedActors(Preset->bRecenterBakedActors); + HC->SetBakeAfterNextCook(Preset->bAutoBake ? EHoudiniBakeAfterNextCook::Always : EHoudiniBakeAfterNextCook::Disabled); + HC->SetReplacePreviousBake(Preset->bReplacePreviousBake); + } + + // Populate Asset Settings + if (Preset->bApplyAssetOptions) + { + HC->SetCookOnParameterChange(Preset->bCookOnParameterChange); + HC->SetCookOnTransformChange(Preset->bCookOnTransformChange); + HC->SetCookOnCookableInputCook(Preset->bCookOnAssetInputCook); + HC->SetOutputless(Preset->bDoNotGenerateOutputs); + HC->SetUseOutputNodes(Preset->bUseOutputNodes); + HC->SetOutputTemplateGeos(Preset->bOutputTemplateGeos); + HC->SetUploadTransformsToHoudiniEngine(Preset->bUploadTransformsToHoudiniEngine); + HC->SetLandscapeUseTempLayers(Preset->bLandscapeUseTempLayers); + } + + // Set Temporary Cook folder + if(Preset->bApplyTemporaryCookFolder) + { + HC->SetTemporaryCookFolderPath(Preset->TemporaryCookFolder); + } + + // Set Bake folder + if(Preset->bApplyBakeFolder) + { + HC->SetBakeFolderPath(Preset->BakeFolder); + } + + // When recooking/rebuilding the HDA, force a full update of all params + const bool bForceFullUpdate = HC->HasRebuildBeenRequested() || HC->HasRecookBeenRequested() || HC->IsParameterDefinitionUpdateNeeded(); + const bool bCacheRampParms = !HC->HasBeenLoaded() && !HC->HasBeenDuplicated(); + + // TODO: COOKABLE - replace me? + bool bNeedToUpdateEditorProperties = false; + + // Update the parameters + FHoudiniParameterTranslator::UpdateParameters( + HC->GetNodeId(), + HC, + HC->GetParameters(), + HC->GetHoudiniAsset(), + HC->GetHapiAssetName(), + bForceFullUpdate, + bCacheRampParms, + bNeedToUpdateEditorProperties); + + // Iterate over all the parameters and settings in the preset and apply it to the Houdini Asset Component. + + // Apply all Multiparam parameters. Since multiparms may contain multiparms we need to perform a loop + // setting as many multiparms as we can, then updating parameters, then setting the remaining multiparams + // again, and repeat. Until we end up with no multiparams remaining or nothing changed on the last + // parameter update. + + TSet UnprocessedMultiParms; + Preset->MultiParmParameters.GetKeys(UnprocessedMultiParms); + + while (!UnprocessedMultiParms.IsEmpty()) + { + // Create a temp array of all param names we haven't processed yet. Do this so we can modify UnprocessedMultiParms + // in the loop below. + TArray CurrentParms; + for (const FString& Element : UnprocessedMultiParms) + CurrentParms.Add(Element); + + bool bProcessedAtLeastOne = false; + + // now loop over all parms we haven't process and try to set them. If we can set them, remove them from the + // unprocessed set. + for (FString& ParmName : CurrentParms) + { + const FHoudiniPresetMultiParmValues& ParmValues = Preset->MultiParmParameters[ParmName]; + + UHoudiniParameter* Parm = HC->FindParameterByName(ParmName); + if (!IsValid(Parm)) + continue; + + FHoudiniPresetHelpers::ApplyPresetParameterValues(ParmValues, Cast(Parm)); + UnprocessedMultiParms.Remove(ParmName); + bProcessedAtLeastOne = true; + } + if (!bProcessedAtLeastOne) + break; + + FHoudiniParameterTranslator::UploadChangedParameters(HC->GetParameters(), HC->GetNodeId()); + FHoudiniParameterTranslator::UpdateParameters( + HC->GetNodeId(), + HC, + HC->GetParameters(), + HC->GetHoudiniAsset(), + HC->GetHapiAssetName(), + bForceFullUpdate, + bCacheRampParms, + bNeedToUpdateEditorProperties); + } + + + if (Preset->MultiParmParameters.Num() > 0) + { + FHoudiniParameterTranslator::UploadChangedParameters(HC->GetParameters(), HC->GetNodeId()); + FHoudiniParameterTranslator::UpdateParameters( + HC->GetNodeId(), + HC, + HC->GetParameters(), + HC->GetHoudiniAsset(), + HC->GetHapiAssetName(), + bForceFullUpdate, + bCacheRampParms, + bNeedToUpdateEditorProperties); + } + + // Apply all the Int parameters + for (const auto& Entry : Preset->IntParameters) + { + const FString& ParmName = Entry.Key; + const FHoudiniPresetIntValues& ParmValues = Entry.Value; + + UHoudiniParameter* Parm = HC->FindParameterByName(ParmName); + if (!IsValid(Parm)) + { + continue; + } + + const EHoudiniParameterType ParmType = Parm->GetParameterType(); + switch (ParmType) + { + case EHoudiniParameterType::Int: + FHoudiniPresetHelpers::ApplyPresetParameterValues(ParmValues, Cast(Parm)); + break; + case EHoudiniParameterType::IntChoice: + FHoudiniPresetHelpers::ApplyPresetParameterValues(ParmValues, Cast(Parm)); + break; + case EHoudiniParameterType::Toggle: + FHoudiniPresetHelpers::ApplyPresetParameterValues(ParmValues, Cast(Parm)); + break; + default:; + } + } + + // Apply all the Float parameters + for (const auto& Entry : Preset->FloatParameters) + { + const FString& ParmName = Entry.Key; + const FHoudiniPresetFloatValues& ParmValues = Entry.Value; + + UHoudiniParameter* Parm = HC->FindParameterByName(ParmName); + if (!IsValid(Parm)) + { + continue; + } + + const EHoudiniParameterType ParmType = Parm->GetParameterType(); + switch (ParmType) + { + case EHoudiniParameterType::Color: + FHoudiniPresetHelpers::ApplyPresetParameterValues(ParmValues, Cast(Parm)); + break; + case EHoudiniParameterType::Float: + FHoudiniPresetHelpers::ApplyPresetParameterValues(ParmValues, Cast(Parm)); + break; + default:; + } + } + + // Apply all the String parameters + + for (const auto& Entry : Preset->StringParameters) + { + const FString& ParmName = Entry.Key; + const FHoudiniPresetStringValues& ParmValues = Entry.Value; + + UHoudiniParameter* Parm = HC->FindParameterByName(ParmName); + if (!IsValid(Parm)) + { + continue; + } + + const EHoudiniParameterType ParmType = Parm->GetParameterType(); + switch (ParmType) + { + case EHoudiniParameterType::File: + case EHoudiniParameterType::FileDir: + case EHoudiniParameterType::FileGeo: + case EHoudiniParameterType::FileImage: + FHoudiniPresetHelpers::ApplyPresetParameterValues(ParmValues, Cast(Parm)); + break; + case EHoudiniParameterType::String: + case EHoudiniParameterType::StringAssetRef: + FHoudiniPresetHelpers::ApplyPresetParameterValues(ParmValues, Cast(Parm)); + break; + case EHoudiniParameterType::StringChoice: + FHoudiniPresetHelpers::ApplyPresetParameterValues(ParmValues, Cast(Parm)); + break; + default:; + } + } + + // Apply all the Ramp Float parameters + for (const auto& Entry : Preset->RampFloatParameters) + { + const FString& ParmName = Entry.Key; + const FHoudiniPresetRampFloatValues& ParmValues = Entry.Value; + + UHoudiniParameter* Parm = HC->FindParameterByName(ParmName); + if (!IsValid(Parm)) + { + continue; + } + + const EHoudiniParameterType ParmType = Parm->GetParameterType(); + switch (ParmType) + { + case EHoudiniParameterType::FloatRamp: + FHoudiniPresetHelpers::ApplyPresetParameterValues(ParmValues, Cast(Parm)); + break; + default:; + } + } + + // Apply all the Ramp Color parameters + for (const auto& Entry : Preset->RampColorParameters) + { + const FString& ParmName = Entry.Key; + const FHoudiniPresetRampColorValues& ParmValues = Entry.Value; + + UHoudiniParameter* Parm = HC->FindParameterByName(ParmName); + if (!IsValid(Parm)) + { + continue; + } + + const EHoudiniParameterType ParmType = Parm->GetParameterType(); + switch (ParmType) + { + case EHoudiniParameterType::ColorRamp: + FHoudiniPresetHelpers::ApplyPresetParameterValues(ParmValues, Cast(Parm)); + break; + default:; + } + } + + // Apply inputs + for (const FHoudiniPresetInputValue& PresetInput : Preset->InputParameters) + { + if (PresetInput.InputType == EHoudiniInputType::Invalid) + { + continue; + } + + if (PresetInput.bIsParameterInput) + { + // Parameter based input + UHoudiniParameterOperatorPath* Param = Cast(HC->FindParameterByName(PresetInput.ParameterName)); + if (IsValid(Param)) + { + UHoudiniInput* Input = Param->HoudiniInput.Get(); + FHoudiniPresetHelpers::ApplyPresetParameterValues(PresetInput, Input); + } + } + else + { + // Absolute input + UHoudiniInput* Input = HC->GetInputAt(PresetInput.InputIndex); + FHoudiniPresetHelpers::ApplyPresetParameterValues(PresetInput, Input); + } + } + + if (Preset->bApplyStaticMeshGenSettings) + { + HC->SetStaticMeshGenerationProperties(Preset->StaticMeshGenerationProperties); + HC->SetStaticMeshBuildSettings(Preset->StaticMeshBuildSettings); + } + + if (Preset->bApplyProxyMeshGenSettings) + { + // Populate Proxy Mesh Gen Settings + HC->SetOverrideGlobalProxyStaticMeshSettings(Preset->bOverrideGlobalProxyStaticMeshSettings); + HC->SetEnableProxyStaticMeshOverride(Preset->bEnableProxyStaticMeshOverride); + HC->SetEnableProxyStaticMeshRefinementByTimerOverride(Preset->bEnableProxyStaticMeshRefinementByTimerOverride); + HC->SetProxyMeshAutoRefineTimeoutSecondsOverride(Preset->ProxyMeshAutoRefineTimeoutSecondsOverride); + HC->SetEnableProxyStaticMeshRefinementOnPreSaveWorldOverride(Preset->bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride); + HC->SetEnableProxyStaticMeshRefinementOnPreBeginPIEOverride(Preset->bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride); + } + + // TODO COOKABLE + // Do something about bNeedToUpdateEditorProperties + if (bReselectSelectedActors) + { + FHoudiniEngineEditorUtils::ReselectSelectedActors(); + } + + for (auto& Callback : Preset->PostInstantiationCallbacks) + { + Callback(Preset, HC); + } +} + +void FHoudiniToolsEditor::ApplyObjectsAsHoudiniAssetInputs( + const TMap& InputObjects, + UHoudiniCookable* HC) +{ + if (InputObjects.Num() <= 0) + return; + +#if WITH_EDITOR + + struct FInputTypeCount + { + FInputTypeCount(): NumWorldInputs(0), NumGeoInputs(0), NumCurveInputs(0) {} + + int32 NumWorldInputs; + int32 NumGeoInputs; + int32 NumCurveInputs; + }; + + // Ignore inputs that have been preset to curve + TArray InputArray; + TArray InputIsCleared; + TMap TypeCountMap; + + const int32 NumInputs = HC->GetNumInputs(); + for (int32 InputIndex = 0; InputIndex < NumInputs; InputIndex++) + { + UHoudiniInput* CurrentInput = HC->GetInputAt(InputIndex); + if (!IsValid(CurrentInput)) + continue; + + InputArray.Add(CurrentInput); + InputIsCleared.Add(false); + } + + // Try to apply the supplied Object to the Input + for (TMap< UObject*, int32 >::TConstIterator IterToolPreset(InputObjects); IterToolPreset; ++IterToolPreset) + { + UObject* Object = IterToolPreset.Key(); + if (!IsValid(Object)) + continue; + + const int32 InputNumber = IterToolPreset.Value(); + if (!InputArray.IsValidIndex(InputNumber)) + continue; + + FInputTypeCount& Count = TypeCountMap.FindOrAdd(InputNumber); + + UBlueprint* BlueprintObj = Cast(Object); + + // If the object is a landscape, add a new landscape input + if (Object->IsA()) + { + // selecting a landscape + int32 InsertNum = Count.NumWorldInputs; + InputArray[InputNumber]->SetInputObjectAt(EHoudiniInputType::World, InsertNum, Object); + Count.NumWorldInputs++; + } + // If the object is a static mesh, add a new geometry input (TODO: or BP ? ) + else if (Object->IsA()) + { + // selecting a Static Mesh + int32 InsertNum = Count.NumGeoInputs; + InputArray[InputNumber]->SetInputObjectAt(EHoudiniInputType::Geometry, InsertNum, Object); + Count.NumGeoInputs++; + } + else if (IsValid(BlueprintObj) && BlueprintObj->BlueprintType == EBlueprintType::BPTYPE_Normal) + { + // selecting a normal Blueprint asset as Geometry input type + int32 InsertNum = Count.NumGeoInputs; + InputArray[InputNumber]->SetInputObjectAt(EHoudiniInputType::Geometry, InsertNum, Object); + Count.NumGeoInputs++; + } + else if (Object->IsA()) + { + // selecting a Houdini Asset + int32 InsertNum = Count.NumWorldInputs; + InputArray[InputNumber]->SetInputObjectAt(EHoudiniInputType::World, InsertNum, Object); + Count.NumWorldInputs++; + } + // If the object is an actor, add a new world input + else if (Object->IsA()) + { + // selecting a generic actor + int32 InsertNum = Count.NumWorldInputs; + InputArray[InputNumber]->SetInputObjectAt(EHoudiniInputType::World, InsertNum, Object); + Count.NumWorldInputs++; + } + } + + bool bBPStructureModified = false; + + // We try to guess the input type based on the count of the received input objects. + for (const auto& Entry : TypeCountMap) + { + const int32 InputIndex = Entry.Key; + const FInputTypeCount& Count = Entry.Value; + + if (!InputArray.IsValidIndex(InputIndex) || !IsValid(InputArray[InputIndex])) + { + continue; + } + UHoudiniInput* Input = InputArray[InputIndex]; + if (Count.NumWorldInputs > 0 && Count.NumWorldInputs >= Count.NumGeoInputs && Count.NumWorldInputs >= Count.NumCurveInputs) + { + Input->SetInputType(EHoudiniInputType::World, bBPStructureModified); + } + else if (Count.NumGeoInputs >= 0 && Count.NumGeoInputs >= Count.NumCurveInputs) + { + Input->SetInputType(EHoudiniInputType::Geometry, bBPStructureModified); + } + else if (Count.NumGeoInputs >= 0 && Count.NumGeoInputs >= Count.NumCurveInputs) + { + Input->SetInputType(EHoudiniInputType::Geometry, bBPStructureModified); + } + else + { + // We don't know what input type this is. Skip it. + continue; + } + } + + if (bBPStructureModified) + { + UHoudiniAssetComponent* HAC = Cast(HC->GetComponent()); + if(IsValid(HAC)) + HAC->MarkAsBlueprintStructureModified(); + } +#endif + +} + +void +FHoudiniToolsEditor::ApplyPresetToSelectedHoudiniAssetActors(const UHoudiniPreset* Preset, bool bReselectSelectedActors) +{ + USelection* Selection = GEditor->GetSelectedActors(); + TArray SelectedActors; + SelectedActors.SetNum(GEditor->GetSelectedActorCount()); + Selection->GetSelectedObjects(SelectedActors); + + for (AActor* SelectedActor : SelectedActors) + { + const AHoudiniAssetActor* HouActor = Cast(SelectedActor); + if (IsValid(HouActor)) + { + UHoudiniCookable* HC = HouActor->GetHoudiniCookable(); + if (IsValid(HC)) + { + ApplyPresetToHoudiniCookable(Preset, HC, false); + } + } + } + + if (bReselectSelectedActors) + { + FHoudiniEngineEditorUtils::ReselectSelectedActors(); + } +} + + +void +FHoudiniToolsEditor::LaunchHoudiniToolPropertyEditor(const TSharedPtr ToolData) +{ + if (!ToolData.IsValid()) + return; + + UHoudiniAsset* HoudiniAsset = ToolData->HoudiniAsset.LoadSynchronous(); + UHoudiniPreset* HoudiniPreset = ToolData->HoudiniPreset.LoadSynchronous(); + UObject* AssetObject = nullptr; + FString PathName; + + if (ToolData->PackageToolType == EHoudiniPackageToolType::HoudiniAsset) + { + if (!IsValid(HoudiniAsset)) + { + HOUDINI_LOG_ERROR(TEXT("Could not launch HoudiniTool Property Editor. Invalid HoudiniAsset.")); + return; + } + + AssetObject = HoudiniAsset; + PathName = HoudiniAsset->GetPathName(); + } + + if (ToolData->PackageToolType == EHoudiniPackageToolType::Preset) + { + if (!IsValid(HoudiniPreset)) + { + HOUDINI_LOG_ERROR(TEXT("Could not launch HoudiniTool Property Editor. Invalid HoudiniPreset.")); + return; + } + AssetObject = HoudiniPreset; + PathName = HoudiniPreset->GetPathName(); + } + + const FName ViewIdentifier = FName(TEXT("HoudiniToolPropertyEditor:") + PathName); + + // See if we can find an existing property editor for this asset + + FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked("PropertyEditor"); + TSharedPtr DetailsView = PropertyEditorModule.FindDetailView(ViewIdentifier); + if (DetailsView.IsValid()) + { + TSharedPtr ContainingWindow = FSlateApplication::Get().FindWidgetWindow(DetailsView->AsShared()); + if ( ContainingWindow.IsValid() ) + { + ContainingWindow->BringToFront(); + } + // Focus the existing details view + return; + } + + // Create a new Tool property object for the property dialog + FString ToolName = ToolData->Name.ToString(); + if (IsValid(HoudiniAsset)) + { + ToolName += TEXT(" (") + HoudiniAsset->AssetFileName + TEXT(")"); + } + + UHoudiniToolEditorProperties* ToolProperties = + NewObject( GetTransientPackage(), FName( *ToolName ) ); + + // Set the default values for this asset + ToolProperties->Name = ToolData->Name.ToString(); + ToolProperties->Type = ToolData->Type; + ToolProperties->ToolTip = ToolData->ToolTipText.ToString(); + ToolProperties->HelpURL = ToolData->HelpURL; + ToolProperties->SelectionType = ToolData->SelectionType; + // Always leave this field blank. The user can use this to import a new icon from an arbitrary location. + ToolProperties->IconPath.FilePath = FString(); + ToolProperties->ToolType = ToolData->PackageToolType; + ToolProperties->HoudiniAsset = HoudiniAsset; + ToolProperties->HoudiniPreset = HoudiniPreset; + + TArray ActiveHoudiniTools; + ActiveHoudiniTools.Add(ToolProperties); + + TSharedPtr EditingTool = ToolData; + + // Create a new property editor window + TSharedRef Window = CreateFloatingDetailsView( + ActiveHoudiniTools, + ViewIdentifier, + FVector2D(450,650), + [EditingTool](TArray InObjects) + { + switch (EditingTool->PackageToolType) + { + case EHoudiniPackageToolType::HoudiniAsset: + HandleHoudiniAssetPropertyEditorSaveClicked(EditingTool, InObjects); + break; + case EHoudiniPackageToolType::Preset: + HandleHoudiniPresetPropertyEditorSaveClicked(EditingTool, InObjects); + break; + default: + HOUDINI_LOG_ERROR(TEXT("Could not save due to unrecognized PackageToolType.")); + } + } + ); +} + + +TSharedRef +FHoudiniToolsEditor::CreateFloatingDetailsView( + TArray& InObjects, + FName InViewIdentifier, + const FVector2D InClientSize, + const TFunction)> OnSaveClickedFn) +{ + TSharedRef NewSlateWindow = SNew(SWindow) + .Title(NSLOCTEXT("PropertyEditor", "WindowTitle", "Houdini Tools Property Editor")) + .ClientSize(InClientSize); + + // If the main frame exists parent the window to it + TSharedPtr ParentWindow; + if (FModuleManager::Get().IsModuleLoaded("MainFrame")) + { + IMainFrameModule& MainFrame = FModuleManager::GetModuleChecked("MainFrame"); + ParentWindow = MainFrame.GetParentWindow(); + } + + if (ParentWindow.IsValid()) + { + // Parent the window to the main frame + FSlateApplication::Get().AddWindowAsNativeChild(NewSlateWindow, ParentWindow.ToSharedRef()); + } + else + { + FSlateApplication::Get().AddWindow(NewSlateWindow); + } + + FDetailsViewArgs Args; + Args.bHideSelectionTip = true; + Args.bLockable = false; + Args.bAllowMultipleTopLevelObjects = true; + Args.ViewIdentifier = InViewIdentifier; + Args.NameAreaSettings = FDetailsViewArgs::HideNameArea; + Args.bShowPropertyMatrixButton = false; + Args.bShowOptions = false; + Args.bShowModifiedPropertiesOption = false; + Args.bShowObjectLabel = false; + + FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked("PropertyEditor"); + const TSharedRef DetailView = PropertyEditorModule.CreateDetailView( Args ); + DetailView->SetObjects(InObjects); + TWeakPtr WindowWeakPtr = NewSlateWindow; + + NewSlateWindow->SetContent( + SNew(SBorder) + .BorderImage(_GetBrush(TEXT("PropertyWindow.WindowBorder"))) + .BorderImage(FAppStyle::Get().GetBrush("Brushes.Panel")) + [ + SNew(SVerticalBox) + + // Detail View + + SVerticalBox::Slot() + .AutoHeight() + [ + DetailView + ] + + // Button Row + + SVerticalBox::Slot() + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Right) + .FillHeight(1.f) + [ + SNew(SUniformGridPanel) + .SlotPadding(FCoreStyle::Get().GetMargin("StandardDialog.SlotPadding")) + .MinDesiredSlotWidth(FCoreStyle::Get().GetFloat("StandardDialog.MinDesiredSlotWidth")) + .MinDesiredSlotHeight(FCoreStyle::Get().GetFloat("StandardDialog.MinDesiredSlotHeight")) + + // Save Button + + SUniformGridPanel::Slot(0, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .ButtonStyle(&_GetEditorStyle().GetWidgetStyle("PrimaryButton")) + .Content() + [ + SNew(STextBlock) + .TextStyle( &_GetEditorStyle().GetWidgetStyle("PrimaryButtonText")) + .Text( LOCTEXT("HoudiniTools_Details_Save","Save")) + ] + .OnClicked_Lambda([WindowWeakPtr, OnSaveClickedFn, InObjects]() -> FReply + { + OnSaveClickedFn(InObjects); + if (WindowWeakPtr.IsValid()) + { + WindowWeakPtr.Pin()->RequestDestroyWindow(); + } + return FReply::Handled(); + }) + ] + + // Cancel Button + + SUniformGridPanel::Slot(1, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .ButtonStyle(&_GetEditorStyle().GetWidgetStyle("Button")) + .Content() + [ + SNew(STextBlock) + .TextStyle( &_GetEditorStyle().GetWidgetStyle("ButtonText")) + .Text( LOCTEXT("HoudiniTools_Details_Cancel","Cancel")) + ] + .OnClicked_Lambda([WindowWeakPtr]() -> FReply + { + if (WindowWeakPtr.IsValid()) + { + WindowWeakPtr.Pin()->RequestDestroyWindow(); + } + return FReply::Handled(); + }) + ] + ] // Button Row + ] // SBorder + ); + + return NewSlateWindow; +} + + +void +FHoudiniToolsEditor::HandleHoudiniAssetPropertyEditorSaveClicked( + TSharedPtr InToolData, + TArray& InObjects) +{ + // Sanity check, we can only edit one tool at a time! + if (InObjects.Num() != 1) + return; + + if (!InToolData.IsValid()) + return; + + checkf(InToolData->PackageToolType == EHoudiniPackageToolType::HoudiniAsset, TEXT("This function should only be called for HoudiniAsset tools.")); + + UHoudiniAsset* HoudiniAsset = InToolData->HoudiniAsset.LoadSynchronous(); + if (!HoudiniAsset) + { + HOUDINI_LOG_ERROR(TEXT("Could not locate active tool. Unable to save changes.")); + return; + } + + // Reimport assets from their new sources. + TArray ReimportAssets; + + TArray< FHoudiniTool > EditedToolArray; + for (int32 ObjIdx = 0; ObjIdx < InObjects.Num(); ObjIdx++) + { + UHoudiniToolEditorProperties* ToolProperties = Cast( InObjects[ ObjIdx ] ); + if ( !ToolProperties ) + continue; + + // FString IconPath = FPaths::ConvertRelativePathToFull( ToolProperties->IconPath.FilePath ); + // const FSlateBrush* CustomIconBrush = nullptr; + // if ( FPaths::FileExists( IconPath ) ) + // { + // + // // If we have a valid icon path, load the file. + // FName BrushName = *IconPath; + // CustomIconBrush = new FSlateDynamicImageBrush( BrushName, FVector2D( 40.f, 40.f ) ); + // } + + // - Store all the edited properties on the HoudiniToolData. + // - Populate the relevant FHoudiniTool descriptor from the HoudiniAsset. + // - Save out the JSON description, if required. + + bool bModified = false; + + // Helper macro for Property assignments and modify flag management + #define ASSIGNFN(Src, Dst) \ + {\ + if (Src != Dst)\ + {\ + bModified = true;\ + Dst = Src;\ + }\ + } + + UHoudiniToolData* ToolData = FHoudiniToolsEditor::GetOrCreateHoudiniToolData(HoudiniAsset); + ToolData->Modify(); + ASSIGNFN(ToolProperties->Name, ToolData->Name); + ASSIGNFN(ToolProperties->Type, ToolData->Type); + ASSIGNFN(ToolProperties->SelectionType, ToolData->SelectionType); + ASSIGNFN(ToolProperties->ToolTip, ToolData->ToolTip); + ASSIGNFN(ToolProperties->HelpURL, ToolData->HelpURL); + + if (FPaths::FileExists(ToolProperties->AssetPath.FilePath)) + { + // If the AssetPath has changed, reimport the HDA with the new source + if (ToolProperties->AssetPath.FilePath != ToolData->SourceAssetPath.FilePath) + { + TArray Filenames; + Filenames.Add(ToolProperties->AssetPath.FilePath); + FReimportManager::Instance()->UpdateReimportPaths(HoudiniAsset, Filenames); + + ReimportAssets.Add(HoudiniAsset); + } + ASSIGNFN(ToolProperties->AssetPath.FilePath, ToolData->SourceAssetPath.FilePath); + } + else + { + HOUDINI_LOG_WARNING(TEXT("The specified AssetPath does not exist. Source Asset Path will remain unchanged")); + } + + bool bModifiedIcon = false; + if (ToolProperties->IconPath.FilePath.Len() > 0) + { + bModified = true; + bModifiedIcon = true; + ToolData->LoadIconFromPath(ToolProperties->IconPath.FilePath); + } + else if (ToolProperties->bClearCachedIcon) + { + bModified = true; + bModifiedIcon = true; + ToolData->ClearCachedIcon(); + } + else + { + // If we're not changed the icon, at least ensure our thumbnail icon is up to date + FHoudiniToolsRuntimeUtils::UpdateAssetThumbnailFromImageData(HoudiniAsset, ToolData->IconImageData); + } + + if (bModifiedIcon) + { + // Ensure the content browser reflects icon changes. + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + AssetRegistryModule.Get().OnAssetUpdated().Broadcast(FAssetData(HoudiniAsset)); + } + + ToolData->DefaultTool = false; + + if (bModified) + { + ToolData->MarkPackageDirty(); + // We modified the HoudiniAsset. Request a panel refresh. + HoudiniAsset->PostEditChange(); + } + + // Remove the tool from Root + ToolProperties->RemoveFromRoot(); + } + + FAssetRegistryModule& AssetRegistry = FModuleManager::Get().LoadModuleChecked("AssetRegistry"); + + for (UHoudiniAsset* Asset : ReimportAssets) + { + FReimportManager::Instance()->Reimport(Asset, false /* Ask for new file */, true /* Show notification */); + } +} + + +void +FHoudiniToolsEditor::HandleHoudiniPresetPropertyEditorSaveClicked( + TSharedPtr InToolData, + TArray& InObjects) +{ + // Sanity check, we can only edit one tool at a time! + if (InObjects.Num() != 1) + return; + + if (!InToolData.IsValid()) + return; + + checkf(InToolData->PackageToolType == EHoudiniPackageToolType::Preset, TEXT("This function should only be called for HoudiniPreset tools.")); + + // UHoudiniAsset* HoudiniAsset = InToolData->HoudiniAsset.LoadSynchronous(); + // if (!HoudiniAsset) + // { + // HOUDINI_LOG_ERROR(TEXT("Could not locate active tool. Unable to save changes.")); + // return; + // } + + UHoudiniPreset* HoudiniPreset = InToolData->HoudiniPreset.LoadSynchronous(); + if (!HoudiniPreset) + { + HOUDINI_LOG_ERROR(TEXT("Could not locate active tool. Unable to save changes.")); + return; + } + + // Reimport assets from their new sources. + TArray ReimportAssets; + + TArray EditedToolArray; + for (int32 ObjIdx = 0; ObjIdx < InObjects.Num(); ObjIdx++) + { + UHoudiniToolEditorProperties* ToolProperties = Cast(InObjects[ObjIdx]); + if (!ToolProperties) + continue; + + bool bModified = false; + // Helper macro for Property assignments and modify flag management + #define ASSIGNFN(Src, Dst) \ + {\ + if (Src != Dst)\ + {\ + bModified = true;\ + Dst = Src;\ + }\ + } + + HoudiniPreset->Modify(); + + ASSIGNFN(ToolProperties->Name, HoudiniPreset->Name); + ASSIGNFN(ToolProperties->ToolTip, HoudiniPreset->Description); + + bool bModifiedIcon = false; + if (ToolProperties->IconPath.FilePath.Len() > 0) + { + bModified = true; + bModifiedIcon = true; + FHoudiniToolsRuntimeUtils::LoadFHImageFromFile(ToolProperties->IconPath.FilePath, HoudiniPreset->IconImageData); + FHoudiniToolsRuntimeUtils::UpdateAssetThumbnailFromImageData(HoudiniPreset, HoudiniPreset->IconImageData); + } + else if (ToolProperties->bClearCachedIcon) + { + bModified = true; + bModifiedIcon = true; + HoudiniPreset->IconImageData = FHImageData(); + FHoudiniToolsRuntimeUtils::UpdateAssetThumbnailFromImageData(HoudiniPreset, FHImageData()); + } + else + { + // If we're not changed the icon, at least ensure our thumbnail icon is up to date + FHoudiniToolsRuntimeUtils::UpdateAssetThumbnailFromImageData(HoudiniPreset, HoudiniPreset->IconImageData); + } + + if (bModifiedIcon) + { + // Ensure the content browser reflects icon changes. + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + AssetRegistryModule.Get().OnAssetUpdated().Broadcast(FAssetData(HoudiniPreset)); + } + + if (bModified) + { + HoudiniPreset->MarkPackageDirty(); + // We modified an existing tool. Call PostEditChange to trigger HoudiniPanel update. + HoudiniPreset->PostEditChange(); + } + + // Remove the tool from Root + ToolProperties->RemoveFromRoot(); + } +} + + +void +FHoudiniToolsEditor::Shutdown() +{ + Categories.Empty(); + for (auto& Entry : CachedTextures) + { + UTexture2D* CachedTexture = Entry.Value; + if(CachedTexture && CachedTexture->IsValidLowLevel()) + { + CachedTexture->RemoveFromRoot(); + } + } + CachedTextures.Empty(); +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniToolsEditor.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniAsset.h" +#include "HoudiniEngineToolTypes.h" +#include "HoudiniParameterChoice.h" +#include "HoudiniParameterToggle.h" +#include "HoudiniPreset.h" +#include "HoudiniToolsPackageAsset.h" +#include "HoudiniToolTypes.h" +#include "AssetRegistry/AssetRegistryModule.h" + +// ----------------------------- +// Houdini Tools +// ----------------------------- +// Houdini Tools maintains a cache of packages, tools, and categories. It also provided accessors for any UI +// tools to access this data + +class FJsonObject; +struct FHoudiniToolList; +struct FSlateBrush; +class UHoudiniAsset; +DECLARE_LOG_CATEGORY_EXTERN(LogHoudiniTools, Log, All); + +struct FFilePath; +enum class EHoudiniToolSelectionType : uint8; +enum class EHoudiniToolType : uint8; +struct FHoudiniToolDirectory; +struct FHoudiniTool; +class UHoudiniToolsPackageAsset; + + +class FHoudiniToolsEditor +{ + protected: + static IAssetRegistry& GetAssetRegistry(); + +public: + FHoudiniToolsEditor(); + + // Returns the Default Icon to be used by Houdini Tools + static FString GetDefaultHoudiniToolIconPath(); + static FString GetDefaultHoudiniToolIconBrushName(); + + static FString GetAbsoluteGameContentDir(); + + static FString GetDefaultHoudiniToolsPath(); + static FString GetDefaultPackagePath(const FString& PackageName); + static FString GetDefaultPackageAssetPath(const FString& PackageName); + + static TSoftObjectPtr GetPackageAssetRef(const FString& PackagePath); + + static FString GetAbsoluteToolsPackagePath(const UHoudiniToolsPackageAsset* ToolsPackage); + + static FString ResolveHoudiniAssetLabel(const UHoudiniAsset* HoudiniAsset); + + // Compute the file path of where we expect this HoudiniAsset's JSON file resides. + static bool GetHoudiniAssetJSONPath(const UHoudiniAsset* HoudiniAsset, FString& OutJSONFilePath); + + // Resolve the icon path for the given HoudiniAsset. + static FString ResolveHoudiniAssetIconPath(const UHoudiniAsset* HoudiniAsset); + + // Get the path of the houdini asset / preset relative to its owning package. + static bool ResolveHoudiniAssetRelativePath(const UObject* Object, FString& OutPath); + + // Get the path of the houdini asset / preset relative to its owning package. This version doesn't + // search for a PackageAsset from the given object, instead it assumes that the given package asset is the + // owner of AssetObject. This is to save us from searching for the owning package if we already have it. + static bool ResolveHoudiniAssetRelativePath( + const UObject* AssetObject, const UHoudiniToolsPackageAsset* OwningPackageAsset, FString& OutPath); + + static FAssetData GetAssetDataByObject(const UObject* AssetObject); + + // Check if the given directory is a valid HoudiniTools package. + bool IsHoudiniToolsPackageDir(const FString& PackageBasePath) const; + + // Find all the Houdini Tools packages, in the Unreal project, where we should look for tools. + void FindHoudiniToolsPackagePaths(TArray& HoudiniToolsDirectoryArray) const; + + static bool LoadJSONFile(const FString& JSONFilePath, TSharedPtr& OutJSONObject); + static bool WriteJSONFile(const FString& JSONFilePath, const TSharedPtr JSONObject); + + static UHoudiniToolsPackageAsset* LoadHoudiniToolsPackage(const FString& PackageBasePath); + + // Add the given tool the exclusion list of the owning package for the given category. + // If the package name was added to the Category exclusion list (or if it was already present), return true. + // If the given category cannot be found in the package, return false. + static bool ExcludeToolFromPackageCategory( + UObject* Object, const FString& CategoryName, bool bAddCategoryIfMissing); + + static bool CanRemoveToolExcludeFromPackageCategory( + UObject* Object, const FString& CategoryName); + + static bool RemoveToolExcludeFromPackageCategory( + UObject* Object, const FString& CategoryName); + + void FindHoudiniToolsPackages(TArray& HoudiniToolPackages ) const; + + // From the current HoudiniAsset, search up the directory tree to see if we can find a + // HoudiniToolsPackageAsset, since HoudiniAssets can live in subfolders in a package. + // Returns the first (closest) HoudiniToolsPackageAsset found. + static UHoudiniToolsPackageAsset* FindOwningToolsPackage(const UObject* Object); + + // Browse to the given UObject in the content browser. + static void BrowseToObjectInContentBrowser(UObject* InObject); + + // Browse to the owning Tools Package in the content browser for the given Houdini Asset. + static void BrowseToOwningPackageInContentBrowser(const UHoudiniAsset* HoudiniAsset); + + // Find all the HoudiniTools in the given tools package, in the Unreal project. + // FHoudiniTools are cached for reuse. + void FindHoudiniToolsInPackage( + const UHoudiniToolsPackageAsset* ToolsPackage, TArray>& OutHoudiniTools); + + static void FindHoudiniAssetsInPackage( + const UHoudiniToolsPackageAsset* ToolsPackage, TArray& OutAssets); + + // Apply the category rules to the given HoudiniTool. Append all valid categories for the HoudiniTool in OutCategories. + static void ApplyCategories( + const TMap& InCategories, + const TSharedPtr& HoudiniTool, + const bool bIgnoreExclusionRules, + TArray& OutIncludeCategories, + TArray& OutExcludeCategories); + + // Apply category rules to an asset inside a HoudiniToolsPackage. + // The OutCategories will contain all the categories in which this asset should be displayed. + // OutExcludedCategories (a subset of OutCategories) contains all the categories from which the asset is normally + // excluded but is now included/displayed due to bIgnoreExclusionRules being true. + static void ApplyCategoryRules( + const FString& HoudiniAssetRelPath, + const FString& CategoryName, + const TArray& IncludeRules, + const TArray& ExcludeRules, + const bool bIgnoreExclusionRules, + TArray& OutIncludeCategories, + TArray& OutExcludeCategories); + + // Check whether the given name is a valid package name. If not, return the reason. + static bool IsValidPackageName(const FString& PkgName, FText* OutFailReason); + + static bool CanCreateCreateToolsPackage(const FString& PackageName, FText* FailReason = nullptr); + + static UHoudiniToolsPackageAsset* CreateToolsPackageAsset(const FString& PackageDir, const FString& DefaultCategory, const FString& ExternalPackageDir=FString()); + + // Return the UHoudiniToolData object on the HoudiniAsset. If it doesn't exist, create it. + static UHoudiniToolData* GetOrCreateHoudiniToolData(UHoudiniAsset* HoudiniAsset); + + // Populate FHoudiniTool from HoudiniAsset. + void PopulateHoudiniTool(const TSharedPtr& HoudiniTool, + const UHoudiniAsset* InHoudiniAsset, + const UHoudiniPreset* InHoudiniPreset, + const UHoudiniToolsPackageAsset* InToolsPackage, + bool bIgnoreToolData = false); + + /** + * Rebuild the internal list of HoudiniTools + * + * This will refresh the tool list from the Unreal project. This will + * not scan any external data. + * **/ + void UpdateHoudiniToolListFromProject(bool bIgnoreExcludePatterns); + + // Get the cached categorized tools + const TMap< FHoudiniToolCategory, TSharedPtr >& GetCategorizedTools() const { return Categories; } + + bool IsToolInCategory(const FHoudiniToolCategory& InCategory, TSharedPtr InHoudiniTool) const; + + // Find the packages for the given category name in the cached categories. + TSet> FindPackagesContainingCategory(const FString& InCategoryName) const; + TSoftObjectPtr FindFirstPackageContainingCategory(const FString& InCategoryName) const; + + // External Data + + // Read HDA description + static bool GetHoudiniPackageDescriptionFromJSON( + const FString& JsonFilePath, + TMap& OutCategories, + bool& OutImportPackageDescription, bool& OutExportPackageDescription, bool& OutImportToolsDescription, bool& + OutExportToolsDescription + ); + + // Import external data for the given HoudiniAsset, if available + static bool ImportExternalHoudiniAssetData(UHoudiniAsset* HoudiniAsset); + + // Reads the Houdini Tool Package Description from a JSON file + static bool GetHoudiniToolDescriptionFromJSON( + const FString& JsonFilePath, + FString& OutName, EHoudiniToolType& OutType, EHoudiniToolSelectionType& OutSelectionType, + FString& OutToolTip, FFilePath& OutIconPath, FFilePath& OutAssetPath, FString& OutHelpURL); + + // Import the Tools Package Asset only from the external json file into the given (internal) directory. + // To import HDAs for this package, call ImportPackageHDA. + static UHoudiniToolsPackageAsset* ImportExternalToolsPackage( + const FString& DestDir, + const FString& PackageJsonPath, + const bool bForceReimport); + + // Helper function that will try to import an external package from JSON, if it exists. + // If not, simply create a default package in the Unreal project. + static UHoudiniToolsPackageAsset* ImportOrCreateToolsPackage( + const FString& DestDir, + const FString& ExternalPackageDir, + const FString& DefaultCategoryName); + + // Import the Tools Package Asset only from the external json file into the given (internal) directory. + // To import HDAs for this package, call ImportPackageHDA. + static bool ReimportExternalToolsPackageDescription(UHoudiniToolsPackageAsset* PackageAsset); + + // TODO: Reimport all the HDAs for the given houdini asset package. + // if bOnlyMissing == true, then only missing HDAs will be imported. Existing HDAs will be left untouched. + static bool ReimportPackageHDAs(const UHoudiniToolsPackageAsset* PackageAsset, const bool bOnlyMissing=false, int* NumImportedHDAs=nullptr); + + /** + * This will create a new unique slate brush from the HoudiniAsset's icon data, so be sure to keep a reference to this + * brush for as long as it is needed. + * @param HoudiniTool The HoudiniTool for which the unique icon brush needs to be created. The brush will be + * created from the referenced UHoudiniAsset's ToolData. + * @returns nullptr if not able to create a dynamic brush. + */ + void CreateUniqueAssetIconBrush(TSharedPtr HoudiniTool); + + static void CaptureThumbnailFromViewport(UObject* Asset); + + static bool CopyThumbnailToImage(const UObject* Asset, FHImageData& ImageData); + + // Populate the HoudiniToolData from the given JSON file. + static bool PopulateHoudiniToolDataFromJSON(const FString& JSONFilePath, UHoudiniToolData& OutToolData); + + static bool PopulateHoudiniToolsPackageFromJSON(const FString& JSONFilePath, UHoudiniToolsPackageAsset& OutPackage); + + // Make a default JSON object from the given Houdini Asset. + static TSharedPtr MakeDefaultJSONObject(const UHoudiniAsset* HoudiniAsset); + + // Make a default JSON object from the given Houdini Tools Package. + static TSharedPtr MakeDefaultJSONObject(const UHoudiniToolsPackageAsset* ToolsPackage); + + // Write a default JSON file for the given HoudiniAsset + static bool WriteDefaultJSONFile(const UHoudiniAsset* HoudiniAsset, const FString& JSONFilePath); + + // Write a default JSON file for the given HoudiniToolPackageAsset + static bool WriteDefaultJSONFile(const UHoudiniToolsPackageAsset* ToolsPackage, const FString& JSONFilePath); + + // Use all the data that can be extracted from the UHoudiniAsset to write out a JSON + // file, while preserving any existing JSON data. + static bool WriteJSONFromHoudiniAsset(const UHoudiniAsset* HoudiniAsset, const FString& JSONFilePath); + + // Use all the data that can be extracted from the UHoudiniAsset to write out a JSON + // file, while preserving any existing JSON data. + static bool WriteJSONFromToolPackageAsset(const UHoudiniToolsPackageAsset* ToolsPackage, const FString& JSONFilePath); + + // Convert between enums and string values used in JSON files + static bool ToolTypeToString(const EHoudiniToolType ToolType, FString& OutString); + static bool SelectionTypeToString(const EHoudiniToolSelectionType SelectionType, FString& OutString); + + // Attempt to import default SideFX HDAs from Houdini + static bool ImportSideFXTools(int* NumImportedHDAs); + + // -------------------------------- + // Tools Package + // -------------------------------- + + static void PopulatePackageWithDefaultData(UHoudiniToolsPackageAsset* PackageAsset); + + // -------------------------------- + // User Categories + // -------------------------------- + + static FText GetFavoritesCategoryName(); + + // Add the tool to the specified user category. If the category does not exist, it will be created. + static void AddToolToUserCategory(const UObject* Object, const FString& CategoryName); + static void RemoveToolFromUserCategory(const UObject* Object, const FString& CategoryName); + static bool UserCategoryContainsTool(const FString& CategoryName, const UObject* AssetObject, const UHoudiniToolsPackageAsset* PackageAsset); + + // Get a list of user categories + static void GetUserCategoriesList(TArray& OutCategories); + + // -------------------------------- + // Presets + // -------------------------------- + + static void CopySettingsToPreset( + const UHoudiniCookable* HC, + const bool bApplyAssetOptions, + const bool bApplyBakeOptions, + const bool bApplyMeshGenSettings, + const bool bApplyProxyMeshGenSettings, + UHoudiniPreset* Preset); + + // Find all the presets that can be applied the given Houdini Asset. + static void FindPresetsForHoudiniAsset( + const UHoudiniAsset* HoudiniAsset, + TArray& OutPresets); + + static bool CanApplyPresetToHoudiniCookable( + const UHoudiniPreset* Preset, + UHoudiniCookable* HC); + + // Apply the preset to the given Cookable. + // Optionally, reselect selected actors to update the component visualizers. + // This is typically used in conjunction with UHoudiniCookable::QueueOneShotPreCookCallback + static void ApplyPresetToHoudiniCookable( + const UHoudiniPreset* Preset, + UHoudiniCookable* HC, + bool bReselectSelectedActors = true); + + // Apply the given objects to the specified input index. The input type will automatically be determined (and set) + // based on the types of the mapped inputs. + // This is typically used in conjunction with UHoudiniCookable::QueueOneShotPreCookCallback + static void ApplyObjectsAsHoudiniAssetInputs( + const TMap& InputObjects, + UHoudiniCookable* HAC); + + // Apply the given preset to the currently selected actors. Optionally reselecting them to update viewport drawings. + static void ApplyPresetToSelectedHoudiniAssetActors( + const UHoudiniPreset* Preset, + bool bReselectSelectedActors=true); + + // -------------------------------- + // Editors + // -------------------------------- + + static void LaunchHoudiniToolPropertyEditor(const TSharedPtr HoudiniTool); + + static TSharedRef CreateFloatingDetailsView( + TArray& InObjects, + FName InViewIdentifier, + const FVector2D InClientSize=FVector2D(400,550), + const TFunction /*InObjects*/)> OnSaveClickedFn = nullptr); + +protected: + + // Save the settings from the HoudiniTool Property Editor to a HoudiniAsset + static void HandleHoudiniAssetPropertyEditorSaveClicked(TSharedPtr ToolData, TArray& InObjects); + // Save the settings from the HoudiniTool Property Editor to a HoudiniPreset + static void HandleHoudiniPresetPropertyEditorSaveClicked(TSharedPtr ToolData, TArray& InObjects); + +public: + + // -------------------------------- + // Shutdown + // -------------------------------- + + // Be sure to call this method when the owning module shuts down so that we can remove + // all our cached textures from root. + void Shutdown(); + +protected: + + // Categories + TMap> Categories; + + TMap CachedTextures; +}; + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniToolsEditorUISubsystem.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniToolsEditorUISubsystem.h" + +#include "HoudiniEngineCommands.h" +#include "HoudiniEngineEditorPrivatePCH.h" + +#include "LevelEditor.h" +#include "Misc/Attribute.h" +#include "Modules/ModuleManager.h" +#include "Styling/AppStyle.h" + +#define LOCTEXT_NAMESPACE "HoudiniToolsEditorUISubsystem" + +void UHoudiniToolsEditorUISubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked("LevelEditor"); + LevelEditorModule.OnRegisterLayoutExtensions().AddUObject(this, &UHoudiniToolsEditorUISubsystem::RegisterLayoutExtensions); +} + +void UHoudiniToolsEditorUISubsystem::Deinitialize() +{ + FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked("LevelEditor"); + LevelEditorModule.OnRegisterLayoutExtensions().RemoveAll(this); +} + +void UHoudiniToolsEditorUISubsystem::RegisterLayoutExtensions(FLayoutExtender& Extender) +{ + Extender.ExtendLayout(_GetPlacementBrowserName(), ELayoutExtensionPosition::After, FTabManager::FTab(HoudiniToolsTabName, ETabState::ClosedTab)); +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/HoudiniToolsEditorUISubsystem.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "Toolkits/AssetEditorModeUILayer.h" + +#include "HoudiniToolsEditorUISubsystem.generated.h" + +class FExtender; +class SBorder; +class IToolkit; +class SDockTab; +class ILevelEditor; + +UCLASS() +class UHoudiniToolsEditorUISubsystem : public UAssetEditorUISubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + virtual void RegisterLayoutExtensions(FLayoutExtender& Extender) override; +}; +``` + +`Source/HoudiniEngineEditor/Private/HoudiniToolsPackageAssetFactory.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "AssetToolsModule.h" +#include "HoudiniToolsPackageAsset.h" +#include "Factories/Factory.h" +#include "Modules/ModuleManager.h" +#include "UObject/Object.h" +#include "HoudiniToolsEditor.h" +#include "HoudiniToolsRuntimeUtils.h" +#include "HoudiniEngineRuntimePrivatePCH.h" + +#include "HoudiniToolsPackageAssetFactory.generated.h" + +enum class EAssetCommandResult : uint8; +struct FAssetCategoryPath; +struct FAssetOpenArgs; + + +// -------------------------------------------- +// HoudiniToolsPackageAsset factory +// -------------------------------------------- + +UCLASS(hidecategories = Object) +class UHoudiniToolsPackageAssetFactory : public UFactory +{ + GENERATED_BODY() + +public: + + UHoudiniToolsPackageAssetFactory() + { + SupportedClass = UHoudiniToolsPackageAsset::StaticClass(); + + bCreateNew = true; + bEditAfterNew = true; + } + + virtual FText GetDisplayName() const override + { + return NSLOCTEXT("DisplayName", "AssetDefinition_HoudiniToolsPackage", "Houdini Tools Package"); + } + + virtual FText GetToolTip() const override + { + return NSLOCTEXT("Tooltip", "AssetDefinition_HoudiniToolsPackage", "Describes a Houdini Tools package."); + } + + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override + { + UHoudiniToolsPackageAsset* ToolsPackage = nullptr; + if (ensure(SupportedClass == Class)) + { + ensure(0 != (RF_Public & Flags)); + ToolsPackage = NewObject(InParent, Class, Name, Flags); + FHoudiniToolsEditor::PopulatePackageWithDefaultData(ToolsPackage); + } + + // If the name is not "HoudiniToolsPackage", rename it immediately + FString DefaultName = FHoudiniToolsRuntimeUtils::GetPackageUAssetName(); + if (Name.ToString() != DefaultName) + { + if (!FHoudiniToolsRuntimeUtils::ShowToolsPackageRenameConfirmDialog()) + { + ToolsPackage->Rename(*DefaultName); + } + else + { + // Dont rename back, but add a warning in the logs... + HOUDINI_LOG_WARNING(TEXT("Renaming a HoudiniToolsPackage to anything but \"HoudiniToolsPackage\" disables it.")); + } + } + + + return ToolsPackage; + } + + virtual uint32 GetMenuCategories() const override + { + IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); + return AssetTools.RegisterAdvancedAssetCategory("HoudiniEngine", NSLOCTEXT("AssetCategoryName", "HoudiniEngineCategory", "Houdini Engine")); + } + + virtual FString GetDefaultNewAssetName() const override + { + return TEXT("HoudiniToolsPackage"); + } + + virtual bool ShouldShowInNewMenu() const override { return true; } +}; + + +// -------------------------------------------- +// HoudiniToolsPackage asset definition +// -------------------------------------------- +// +// UCLASS() +// class UAssetDefinition_HoudiniToolsPackageAsset : public UAssetDefinitionDefault +// { +// GENERATED_BODY() +// +// public: +// // UAssetDefinition Begin +// virtual FText GetAssetDisplayName() const override { return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_HoudiniToolsPackageAsset", "Houdini Tools Package Asset"); } +// virtual FLinearColor GetAssetColor() const override { return FLinearColor(FColor(175, 0, 128)); } +// virtual TSoftClassPtr GetAssetClass() const override { return UHoudiniToolsPackageAsset::StaticClass(); } +// virtual TConstArrayView GetAssetCategories() const override +// { +// return TConstArrayView(); +// } +// virtual FText GetObjectDisplayNameText(UObject* Object) const override { return FText::FromString(TEXT("UHoudiniToolsPackageAsset")); } +// // UAssetDefinition End +// }; + +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniAssetEditorViewport.cpp`: + +```cpp +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "SHoudiniAssetEditorViewport.h" + +#include "HoudiniAssetEditorViewportClient.h" +#include "HoudiniAssetEditor.h" + +#include "AssetEditorModeManager.h" +#include "Components/PostProcessComponent.h" +#include "Editor/AdvancedPreviewScene/Public/AdvancedPreviewScene.h" +#include "Editor/AdvancedPreviewScene/Public/AdvancedPreviewSceneModule.h" +#include "PreviewProfileController.h" +#include "ToolMenus.h" +#include "Widgets/SViewport.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + #include "ViewportToolbar/UnrealEdViewportToolbar.h" +#endif +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + #include "AdvancedPreviewSceneMenus.h" +#endif + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +//----------------------------------------------------------------------------- +// SHoudiniAssetEditorViewport +//----------------------------------------------------------------------------- +void +SHoudiniAssetEditorViewport::AddReferencedObjects(FReferenceCollector& Collector) +{ + Collector.AddReferencedObject(HoudiniCookable); +} + +TSharedRef +SHoudiniAssetEditorViewport::GetViewportWidget() +{ + return SharedThis(this); +} + +TSharedPtr +SHoudiniAssetEditorViewport::GetExtenders() const +{ + TSharedPtr Result(MakeShareable(new FExtender)); + return Result; +} + +void +SHoudiniAssetEditorViewport::OnFloatingButtonClicked() +{ + // Nothing to do +} + +// Create the advanced preview scene and initiate our component? +SHoudiniAssetEditorViewport::SHoudiniAssetEditorViewport() + : PreviewScene(MakeShareable(new FAdvancedPreviewScene(FPreviewScene::ConstructionValues()))) +{ + // Nothing to do +} + +SHoudiniAssetEditorViewport::~SHoudiniAssetEditorViewport() +{ + if (TypedViewportClient.IsValid()) + { + TypedViewportClient->Viewport = NULL; + } +} + +void +SHoudiniAssetEditorViewport::Construct(const FArguments& InArgs, TSharedPtr InHoudiniAssetEditor) +{ + HoudiniAssetEditorPtr = InHoudiniAssetEditor; + + SEditorViewport::Construct(SEditorViewport::FArguments()); +} + +void +SHoudiniAssetEditorViewport::Construct(const FArguments& InArgs) +{ + SEditorViewport::Construct(SEditorViewport::FArguments()); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + UE::AdvancedPreviewScene::BindDefaultOnSettingsChangedHandler(PreviewScene, TypedViewportClient); +#endif +} + + +TSharedRef +SHoudiniAssetEditorViewport::MakeEditorViewportClient() +{ + TypedViewportClient = MakeShareable(new FHoudiniAssetEditorViewportClient(SharedThis(this), PreviewScene.ToSharedRef())); + + TypedViewportClient->ToggleOrbitCamera(true); + + return TypedViewportClient.ToSharedRef(); +} + +void +SHoudiniAssetEditorViewport::BindCommands() +{ + SEditorViewport::BindCommands(); +} + +EVisibility +SHoudiniAssetEditorViewport::GetTransformToolbarVisibility() const +{ + return EVisibility::Visible; +} + +void +SHoudiniAssetEditorViewport::OnFocusViewportToSelection() +{ + +} + +void +SHoudiniAssetEditorViewport::SetHoudiniAsset(UHoudiniAsset* InAsset) +{ + if (!InAsset) + return; + + TypedViewportClient->SetHoudiniAsset(InAsset); +} + +FText +SHoudiniAssetEditorViewport::GetTitleText() const +{ + return FText::FromString("Houdini Asset Editor"); +} + + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 +TSharedPtr +SHoudiniAssetEditorViewport::BuildViewportToolbar() +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + // Register the viewport toolbar if another viewport hasn't already (it's shared). + const FName ViewportToolbarName = "HoudiniAssetEditor.ViewportToolbar"; + if (!UToolMenus::Get()->IsMenuRegistered(ViewportToolbarName)) + { + UToolMenu* const ViewportToolbarMenu = UToolMenus::Get()->RegisterMenu( + ViewportToolbarName, NAME_None /* parent */, EMultiBoxType::SlimHorizontalToolBar + ); + + ViewportToolbarMenu->StyleName = "ViewportToolbar"; + + + // Add the left-aligned part of the viewport toolbar. + { + FToolMenuSection& LeftSection = ViewportToolbarMenu->AddSection("Left"); + + // We don't need transform/snapping settings for now + //LeftSection.AddEntry(UE::UnrealEd::CreateTransformsSubmenu()); + //LeftSection.AddEntry(UE::UnrealEd::CreateSnappingSubmenu()); + } + + // Add the right-aligned part of the viewport toolbar. + { + FToolMenuSection& RightSection = ViewportToolbarMenu->AddSection("Right"); + RightSection.Alignment = EToolMenuSectionAlign::Last; + + // Add the "Camera" submenu. + RightSection.AddEntry(UE::UnrealEd::CreateCameraSubmenu(UE::UnrealEd::FViewportCameraMenuOptions().ShowAll())); + + // Add the "View Modes" sub menu. + { + // Stay backward-compatible with the old viewport toolbar. + { + const FName ParentSubmenuName = "UnrealEd.ViewportToolbar.View"; + // Create our parent menu. + if (!UToolMenus::Get()->IsMenuRegistered(ParentSubmenuName)) + { + UToolMenus::Get()->RegisterMenu(ParentSubmenuName); + } + + // Register our ToolMenu here first, before we create the submenu, so we can set our parent. + UToolMenus::Get()->RegisterMenu("HoudiniAssetEditor.ViewportToolbar.ViewModes", ParentSubmenuName); + } + RightSection.AddEntry(UE::UnrealEd::CreateViewModesSubmenu()); + } + + // Add the performance and scalability settings + RightSection.AddEntry(UE::UnrealEd::CreatePerformanceAndScalabilitySubmenu()); + + // Add the Preview Scene setting submenu + { + const FName PreviewSceneMenuName = "HoudiniAssetEditor.ViewportToolbar.AssetViewerProfile"; + RightSection.AddEntry(UE::UnrealEd::CreateAssetViewerProfileSubmenu()); + UE::AdvancedPreviewScene::Menus::ExtendAdvancedPreviewSceneSettings(PreviewSceneMenuName); + UE::UnrealEd::ExtendPreviewSceneSettingsWithTabEntry(PreviewSceneMenuName); + } + } + } + + FToolMenuContext ViewportToolbarContext; + { + ViewportToolbarContext.AppendCommandList(PreviewScene->GetCommandList()); + ViewportToolbarContext.AppendCommandList(GetCommandList()); + + // Add the UnrealEd viewport toolbar context. + { + UUnrealEdViewportToolbarContext* const ContextObject = + UE::UnrealEd::CreateViewportToolbarDefaultContext(SharedThis(this)); + + ContextObject->bShowCoordinateSystemControls = false; + + ContextObject->AssetEditorToolkit = HoudiniAssetEditorPtr; + ContextObject->PreviewSettingsTabId = FName(TEXT("PreviewSceneSettings")); + ViewportToolbarContext.AddObject(ContextObject); + } + } + + return UToolMenus::Get()->GenerateWidget(ViewportToolbarName, ViewportToolbarContext); +#else + // Register the viewport toolbar if another viewport hasn't already (it's shared). + const FName ViewportToolbarName = "HoudiniAssetEditor.ViewportToolbar"; + + if (!UToolMenus::Get()->IsMenuRegistered(ViewportToolbarName)) + { + UToolMenu* const ViewportToolbarMenu = UToolMenus::Get()->RegisterMenu( + ViewportToolbarName, NAME_None /* parent */, EMultiBoxType::SlimHorizontalToolBar + ); + + ViewportToolbarMenu->StyleName = "ViewportToolbar"; + + // Add the right-aligned part of the viewport toolbar. + { + // Add the submenus of this section as EToolMenuInsertType::Last to sort them after any + // default-positioned submenus external code might add. + FToolMenuSection& RightSection = ViewportToolbarMenu->FindOrAddSection("Right"); + RightSection.Alignment = EToolMenuSectionAlign::Last; + + // Add the "Camera" submenu. + { + const FName GrandParentSubmenuName = "UnrealEd.ViewportToolbar.Camera"; + const FName ParentSubmenuName = "HoudiniAssetEditor.ViewportToolbar.Camera"; + const FName SubmenuName = "HoudiniAssetEditor.ViewportToolbar.CameraOptions"; + + // Create our grandparent menu. + if (!UToolMenus::Get()->IsMenuRegistered(GrandParentSubmenuName)) + { + UToolMenus::Get()->RegisterMenu(GrandParentSubmenuName); + } + + // Create our parent menu. + if (!UToolMenus::Get()->IsMenuRegistered(ParentSubmenuName)) + { + UToolMenus::Get()->RegisterMenu(ParentSubmenuName, GrandParentSubmenuName); + } + + // Create our menu. + UToolMenus::Get()->RegisterMenu(SubmenuName, ParentSubmenuName); + + UE::UnrealEd::ExtendCameraSubmenu(SubmenuName); + + FToolMenuEntry CameraSubmenu = UE::UnrealEd::CreateViewportToolbarCameraSubmenu(); + CameraSubmenu.InsertPosition.Position = EToolMenuInsertType::First; + RightSection.AddEntry(CameraSubmenu); + } + + // Add the "View Modes" sub menu. + { + // Stay backward-compatible with the old viewport toolbar. + { + const FName ParentSubmenuName = "UnrealEd.ViewportToolbar.View"; + // Create our parent menu. + if (!UToolMenus::Get()->IsMenuRegistered(ParentSubmenuName)) + { + UToolMenus::Get()->RegisterMenu(ParentSubmenuName); + } + + // Register our ToolMenu here first, before we create the submenu, so we can set our parent. + UToolMenus::Get()->RegisterMenu("HoudiniAssetEditor.ViewportToolbar.ViewModes", ParentSubmenuName); + } + + FToolMenuEntry ViewModesSubmenu = UE::UnrealEd::CreateViewportToolbarViewModesSubmenu(); + ViewModesSubmenu.InsertPosition.Position = EToolMenuInsertType::Last; + RightSection.AddEntry(ViewModesSubmenu); + } + + // Add the "Performance and Scalability" sub menu. + { + FToolMenuEntry PerformanceAndScalabilitySubmenu = UE::UnrealEd::CreatePerformanceAndScalabilitySubmenu(); + PerformanceAndScalabilitySubmenu.InsertPosition.Position = EToolMenuInsertType::Last; + RightSection.AddEntry(PerformanceAndScalabilitySubmenu); + } + + /* + // Add the "Preview Profile" sub menu. + { + //PreviewProfileController = MakeShared(); + FToolMenuEntry PreviewProfileSubmenu = + UE::UnrealEd::CreateViewportToolbarAssetViewerProfileSubmenu(MakeShared()); + PreviewProfileSubmenu.InsertPosition.Position = EToolMenuInsertType::Last; + RightSection.AddEntry(PreviewProfileSubmenu); + } + */ + } + } + + FToolMenuContext ViewportToolbarContext; + { + ViewportToolbarContext.AppendCommandList(GetCommandList()); + + // Add the UnrealEd viewport toolbar context. + { + UUnrealEdViewportToolbarContext* const ContextObject = + UE::UnrealEd::CreateViewportToolbarDefaultContext(SharedThis(this)); + + ViewportToolbarContext.AddObject(ContextObject); + } + } + + return UToolMenus::Get()->GenerateWidget(ViewportToolbarName, ViewportToolbarContext); +#endif +} +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 +TSharedPtr +SHoudiniAssetEditorViewport::CreatePreviewProfileController() +{ + return MakeShared(); +} +#endif +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniAssetEditorViewport.h`: + +```h +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#pragma once + +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" + +#include "CoreMinimal.h" +#include "Components/Viewport.h" +#include "Editor/AdvancedPreviewScene/Public/AdvancedPreviewScene.h" +#include "Editor/AdvancedPreviewScene/Public/AdvancedPreviewSceneModule.h" +#include "Editor/UnrealEd/Public/SCommonEditorViewportToolbarBase.h" +#include "SAssetEditorViewport.h" +#include "SEditorViewport.h" +#include "SlateFwd.h" +#include "UObject/GCObject.h" +#include "Widgets/SViewport.h" +#include "LevelEditorViewport.h" + +class FHoudiniAssetEditor; + +class SHoudiniAssetEditorViewport + : public SAssetEditorViewport, public FGCObject, public ICommonEditorViewportToolbarInfoProvider +{ +public: + + SLATE_BEGIN_ARGS(SHoudiniAssetEditorViewport) {} + SLATE_END_ARGS() + + SHoudiniAssetEditorViewport(); + ~SHoudiniAssetEditorViewport(); + + // Construct this widget + void Construct(const FArguments& InArgs); + void Construct(const FArguments& InArgs, TSharedPtr InHoudiniAssetEditor); + + // Viewport widget accessor + virtual TSharedRef GetViewportWidget() override; + + // Creates the ViewPortClient for this viewport + virtual TSharedRef MakeEditorViewportClient() override; + + // Get the viewport client + TSharedPtr GetViewportClient() { return TypedViewportClient; }; + + // FGCObject + void AddReferencedObjects(FReferenceCollector& Collector) override; + + // Toolbar interface + virtual TSharedPtr GetExtenders() const override; + virtual void OnFloatingButtonClicked() override; + void BindCommands() override; + EVisibility GetTransformToolbarVisibility() const; + void OnFocusViewportToSelection() override; + + virtual FString GetReferencerName() const override + { + return TEXT("SHoudiniAssetEditorViewport"); + } + + // Houdini Asset setter + void SetHoudiniAsset(UHoudiniAsset* InAsset); + + // Returns the preview scene being rendered in the viewport + TSharedRef GetPreviewScene() { return PreviewScene.ToSharedRef(); } + +protected: + + FText GetTitleText() const; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + virtual TSharedPtr BuildViewportToolbar() override; +#endif +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + virtual TSharedPtr CreatePreviewProfileController() override; +#endif + + +private: + + // Pointer to our Houdini Asset Editor owner + TWeakPtr HoudiniAssetEditorPtr; + // The Preview Scene for this viewport + TSharedPtr PreviewScene; + //Shared ptr to the client + TSharedPtr TypedViewportClient; + //All components to use in the client + TObjectPtr HoudiniCookable; +}; + +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniColorRamp.cpp`: + +```cpp +/* +* Copyright (c) <2024> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "SHoudiniColorRamp.h" + +#include "HoudiniParameterRamp.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterColor.h" +#include "HoudiniParameterChoice.h" +#include "Editor/CurveEditor/Public/CurveEditorSettings.h" +#include "Widgets/Colors/SColorBlock.h" +#include "Widgets/Colors/SColorPicker.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineDetails.h" +#include "HoudiniEngineUtils.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +void +SHoudiniColorRampCurveEditor::Construct(const FArguments& InArgs) +{ + Curve = NewObject( + GetTransientPackage(), + UCurveLinearColor::StaticClass(), + NAME_None, + RF_Transactional | RF_Public); + + if (!Curve) + { + return; + } + + // Add the ramp curve to root to avoid garbage collected + Curve->AddToRoot(); + + OnUpdateCurveDelegateHandle = Curve->OnUpdateCurve.AddRaw( + this, &SHoudiniColorRampCurveEditor::OnUpdateCurve); + + RampView = InArgs._RampView; + OnCurveChangedDelegate = InArgs._OnCurveChanged; + + SColorGradientEditor::Construct( + SColorGradientEditor::FArguments() + .ViewMinInput(InArgs._ViewMinInput) + .ViewMaxInput(InArgs._ViewMaxInput)); + + // Avoid showing tooltips inside of the curve editor + EnableToolTipForceField(true); + + SetCurveOwner(Curve); + + RefreshCurveKeys(); +} + +SHoudiniColorRampCurveEditor::~SHoudiniColorRampCurveEditor() +{ + if (Curve) + { + SetCurveOwner(nullptr); + + Curve->OnUpdateCurve.Remove(OnUpdateCurveDelegateHandle); + + // Remove the ramp curve to root so it can be garbage collected + Curve->RemoveFromRoot(); + } +} + +FReply +SHoudiniColorRampCurveEditor::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + bIsMouseButtonDown = true; + return SColorGradientEditor::OnMouseButtonDown(MyGeometry, MouseEvent); +} + +FReply +SHoudiniColorRampCurveEditor::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + bIsMouseButtonDown = false; + + OnCurveChanged(); + + return SColorGradientEditor::OnMouseButtonUp(MyGeometry, MouseEvent); +} + +void +SHoudiniColorRampCurveEditor::OnUpdateCurve(UCurveBase*, EPropertyChangeType::Type) +{ + if (bIsMouseButtonDown) + { + return; + } + + OnCurveChanged(); +} + +void +SHoudiniColorRampCurveEditor::RefreshCurveKeys() +{ + if (!RampView.IsValid()) + { + return; + } + + if (!Curve) + { + return; + } + + TArrayView FloatCurves(Curve->FloatCurves, 4); + + const int32 PointCount = RampView->GetPointCount(); + for (int32 CurveIdx = 0; CurveIdx < 4; ++CurveIdx) + { + FRichCurve& RichCurve = FloatCurves[CurveIdx]; + RichCurve.Reset(); + + for (int32 i = 0; i < PointCount; ++i) + { + ERichCurveInterpMode RichCurveInterpMode = + UHoudiniParameter::EHoudiniRampInterpolationTypeToERichCurveInterpMode( + RampView->GetRampPointInterpolationType(i).GetValue()); + + const FKeyHandle KeyHandle = RichCurve.AddKey( + RampView->GetRampPointPosition(i).GetValue(), + RampView->GetRampPointValue(i).GetValue().Component(CurveIdx)); + RichCurve.SetKeyInterpMode(KeyHandle, RichCurveInterpMode); + } + } +} + +TOptional +SHoudiniColorRampCurveEditor::GetNumCurveKeys() const +{ + if (!Curve) + { + return {}; + } + + return Curve->FloatCurves[0].GetNumKeys(); +} + +TOptional +SHoudiniColorRampCurveEditor::GetCurveKeyPosition(const int32 Index) const +{ + if (!Curve) + { + return {}; + } + + if (!Curve->FloatCurves[0].Keys.IsValidIndex(Index)) + { + return {}; + } + + return Curve->FloatCurves[0].Keys[Index].Time; +} + +TOptional +SHoudiniColorRampCurveEditor::GetCurveKeyValue(const int32 Index) const +{ + if (!Curve) + { + return {}; + } + + if (!Curve->FloatCurves[0].Keys.IsValidIndex(Index)) + { + return {}; + } + + return FLinearColor( + Curve->FloatCurves[0].Keys[Index].Value, + Curve->FloatCurves[1].Keys[Index].Value, + Curve->FloatCurves[2].Keys[Index].Value); +} + +TOptional +SHoudiniColorRampCurveEditor::GetCurveKeyInterpolationType(const int32 Index) const +{ + if (!Curve) + { + return {}; + } + + if (!Curve->FloatCurves[0].Keys.IsValidIndex(Index)) + { + return {}; + } + + return Curve->FloatCurves[0].Keys[Index].InterpMode.GetValue(); +} + +TSharedRef +SHoudiniColorRamp::ConstructRampPointValueWidget(const int32 Index) +{ + if (!RampView.IsValid()) + { + return SNullWidget::NullWidget; + } + + return SNew(SColorBlock) + .Color(RampView->GetRampPointValue(Index).Get(FLinearColor::Black)) + .OnMouseButtonDown(FPointerEventHandler::CreateLambda( + [this, Index](const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) + { + if (MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton) + return FReply::Unhandled(); + + FColorPickerArgs PickerArgs; + PickerArgs.bUseAlpha = true; + PickerArgs.DisplayGamma = TAttribute::Create( + TAttribute::FGetter::CreateUObject(GEngine, &UEngine::GetDisplayGamma)); + PickerArgs.OnColorCommitted = FOnLinearColorValueChanged::CreateLambda( + [this, Index](FLinearColor InColor) + { + if (OnPointValueCommit(Index, InColor)) + { + OnValueCommitted.ExecuteIfBound(); + } + }); + FLinearColor InitColor = RampView->GetRampPointValue(Index).Get(FLinearColor::Black); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + PickerArgs.InitialColor = InitColor; +#else + PickerArgs.InitialColorOverride = InitColor; +#endif + PickerArgs.bOnlyRefreshOnOk = true; + OpenColorPicker(PickerArgs); + return FReply::Handled(); + })); +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniColorRamp.h`: + +```h +/* +* Copyright (c) <2024> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "Curves/CurveLinearColor.h" +#include "SCurveEditor.h" +#include "SColorGradientEditor.h" +#include "SHoudiniRampBase.h" +#include "HoudiniRampEditorWindowSubsystem.h" + +class UHoudiniParameterRampColor; +class UHoudiniParameterRampColorPoint; + +class FHoudiniColorRampView + : public THoudiniRampViewBase< + FHoudiniColorRampView, + FLinearColor, + UHoudiniParameterRampColor, + UHoudiniParameterRampColorPoint> +{ +public: + + static inline const ValueType DefaultInsertValue = FLinearColor::Black; + + explicit FHoudiniColorRampView(TArrayView Parameters) + : THoudiniRampViewBase(Parameters) + { + } +}; + +class SHoudiniColorRampCurveEditor + : public SHoudiniRampCurveEditorBase +{ +public: + SLATE_BEGIN_ARGS(SHoudiniColorRampCurveEditor) + : _ViewMinInput(0.0f) + , _ViewMaxInput(1.0f) + , _InputSnap(0.1f) + , _OutputSnap(0.05f) + , _InputSnappingEnabled(false) + , _OutputSnappingEnabled(false) + , _ShowTimeInFrames(false) + , _TimelineLength(5.0f) + , _DesiredSize(FVector2D::ZeroVector) + , _DrawCurve(true) + , _HideUI(true) + , _AllowZoomOutput(true) + , _AlwaysDisplayColorCurves(false) + , _ZoomToFitVertical(true) + , _ZoomToFitHorizontal(true) + , _ShowZoomButtons(true) + , _XAxisName() + , _YAxisName() + , _ShowInputGridNumbers(true) + , _ShowOutputGridNumbers(true) + , _ShowCurveSelector(true) + , _GridColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.3f)) + { + _Clipping = EWidgetClipping::ClipToBounds; + } + + SLATE_ARGUMENT(TSharedPtr, RampView) + SLATE_EVENT(FOnCurveChanged, OnCurveChanged) + SLATE_ATTRIBUTE(float, ViewMinInput) + SLATE_ATTRIBUTE(float, ViewMaxInput) + SLATE_ATTRIBUTE(TOptional, DataMinInput) + SLATE_ATTRIBUTE(TOptional, DataMaxInput) + SLATE_ATTRIBUTE(float, InputSnap) + SLATE_ATTRIBUTE(float, OutputSnap) + SLATE_ATTRIBUTE(bool, InputSnappingEnabled) + SLATE_ATTRIBUTE(bool, OutputSnappingEnabled) + SLATE_ATTRIBUTE(bool, ShowTimeInFrames) + SLATE_ATTRIBUTE(float, TimelineLength) + SLATE_ATTRIBUTE(FVector2D, DesiredSize) + SLATE_ATTRIBUTE(bool, AreCurvesVisible) + SLATE_ARGUMENT(bool, DrawCurve) + SLATE_ARGUMENT(bool, HideUI) + SLATE_ARGUMENT(bool, AllowZoomOutput) + SLATE_ARGUMENT(bool, AlwaysDisplayColorCurves) + SLATE_ARGUMENT(bool, ZoomToFitVertical) + SLATE_ARGUMENT(bool, ZoomToFitHorizontal) + SLATE_ARGUMENT(bool, ShowZoomButtons) + SLATE_ARGUMENT(TOptional, XAxisName) + SLATE_ARGUMENT(TOptional, YAxisName) + SLATE_ARGUMENT(bool, ShowInputGridNumbers) + SLATE_ARGUMENT(bool, ShowOutputGridNumbers) + SLATE_ARGUMENT(bool, ShowCurveSelector) + SLATE_ARGUMENT(FLinearColor, GridColor) + SLATE_EVENT(FOnSetInputViewRange, OnSetInputViewRange) + SLATE_EVENT(FOnSetOutputViewRange, OnSetOutputViewRange) + SLATE_EVENT(FOnSetAreCurvesVisible, OnSetAreCurvesVisible) + SLATE_EVENT(FSimpleDelegate, OnCreateAsset) + + SLATE_END_ARGS() + +public: + + /** Widget construction. **/ + void Construct(const FArguments& InArgs); + + ~SHoudiniColorRampCurveEditor(); + + FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + /** Refreshes rich curve keys using current parameter info. */ + void RefreshCurveKeys() override; + +protected: + + TOptional GetNumCurveKeys() const override; + TOptional GetCurveKeyPosition(const int32 Index) const override; + TOptional GetCurveKeyValue(const int32 Index) const override; + TOptional GetCurveKeyInterpolationType(const int32 Index) const override; + +private: + + // We only want to commit curve editor value to the parameter on mouse release. OnUpdateCurve + // gets called continuously as we drag a point, so we use this value to determine if we should + // notify the ramp view of a change. + bool bIsMouseButtonDown = false; + + // Unreal representation of the curve which we display. Typically, this curve object is owned by + // some object, however we only use it for the purpose of using Unreal's curve editor widget. + // In order to avoid being garbage collected, we add the curve to root. Thus we need to remove + // the curve from root when the widget is destroyed. + UCurveLinearColor* Curve; + + FDelegateHandle OnUpdateCurveDelegateHandle; + + void OnUpdateCurve(UCurveBase*, EPropertyChangeType::Type); +}; + + +class SHoudiniColorRamp + : public SHoudiniRampBase +{ +protected: + + /** Gets the column header label for value type of the ramp. */ + FORCEINLINE FString GetValueString() const override + { + return TEXT("Color"); + } + + TSharedRef ConstructRampPointValueWidget(const int32 Index) override; +}; + +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniFloatRamp.cpp`: + +```cpp +/* +* Copyright (c) <2024> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "SHoudiniFloatRamp.h" + +#include "HoudiniParameterChoice.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterRamp.h" +#include "Editor/CurveEditor/Public/CurveEditorSettings.h" +#include "Framework/Application/SlateApplication.h" +#include "Widgets/Input/SNumericEntryBox.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineDetails.h" +#include "HoudiniEngineUtils.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +void +SHoudiniFloatRampCurveEditor::Construct(const FArguments& InArgs) +{ + Curve = NewObject( + GetTransientPackage(), + UCurveFloat::StaticClass(), + NAME_None, + RF_Transactional | RF_Public); + + if (!Curve) + { + return; + } + + // Add the ramp curve to root to avoid garbage collected + Curve->AddToRoot(); + + OnUpdateCurveDelegateHandle = Curve->OnUpdateCurve.AddRaw( + this, &SHoudiniFloatRampCurveEditor::OnUpdateCurve); + + RampView = InArgs._RampView; + OnCurveChangedDelegate = InArgs._OnCurveChanged; + + SCurveEditor::Construct( + SCurveEditor::FArguments() + .ViewMinInput(InArgs._ViewMinInput) + .ViewMaxInput(InArgs._ViewMaxInput) + .ViewMinOutput(InArgs._ViewMinOutput) + .ViewMaxOutput(InArgs._ViewMaxOutput) + .XAxisName(InArgs._XAxisName) + .YAxisName(InArgs._YAxisName) + .HideUI(InArgs._HideUI) + .DrawCurve(InArgs._DrawCurve) + .TimelineLength(InArgs._TimelineLength) + .AllowZoomOutput(InArgs._AllowZoomOutput) + .ShowInputGridNumbers(InArgs._ShowInputGridNumbers) + .ShowOutputGridNumbers(InArgs._ShowOutputGridNumbers) + .ShowZoomButtons(InArgs._ShowZoomButtons) + .ZoomToFitHorizontal(InArgs._ZoomToFitHorizontal) + .ZoomToFitVertical(InArgs._ZoomToFitVertical)); + + this->SetRequireFocusToZoom(true); + + UCurveEditorSettings* CurveEditorSettings = GetSettings(); + if (CurveEditorSettings) + { + CurveEditorSettings->SetTangentVisibility(ECurveEditorTangentVisibility::NoTangents); + } + + // Avoid showing tooltips inside of the curve editor + EnableToolTipForceField(true); + + SetCurveOwner(Curve); + + RefreshCurveKeys(); +} + +SHoudiniFloatRampCurveEditor::~SHoudiniFloatRampCurveEditor() +{ + if (Curve) + { + SetCurveOwner(nullptr); + + Curve->OnUpdateCurve.Remove(OnUpdateCurveDelegateHandle); + + // Remove the ramp curve to root so it can be garbage collected + Curve->RemoveFromRoot(); + } +} + +void +SHoudiniFloatRampCurveEditor::OnUpdateCurve(UCurveBase*, EPropertyChangeType::Type) +{ + if (bIsMouseButtonDown) + { + return; // See comment in declaration of bIsMouseButtonDown + } + + OnCurveChanged(); +} + +void +SHoudiniFloatRampCurveEditor::RefreshCurveKeys() +{ + if (!RampView.IsValid()) + { + return; + } + + if (!Curve) + { + return; + } + + FRichCurve& FloatCurve = Curve->FloatCurve; + + FloatCurve.Reset(); + + const int32 PointCount = RampView->GetPointCount(); + for (int32 i = 0; i < PointCount; ++i) + { + ERichCurveInterpMode RichCurveInterpMode = + UHoudiniParameter::EHoudiniRampInterpolationTypeToERichCurveInterpMode( + RampView->GetRampPointInterpolationType(i).GetValue()); + + const FKeyHandle KeyHandle = FloatCurve.AddKey( + RampView->GetRampPointPosition(i).GetValue(), + RampView->GetRampPointValue(i).GetValue()); + + FloatCurve.SetKeyInterpMode(KeyHandle, RichCurveInterpMode); + } +} + +TOptional +SHoudiniFloatRampCurveEditor::GetNumCurveKeys() const +{ + if (!Curve) + { + return {}; + } + + return Curve->FloatCurve.GetNumKeys(); +} + +TOptional +SHoudiniFloatRampCurveEditor::GetCurveKeyPosition(const int32 Index) const +{ + if (!Curve) + { + return {}; + } + + if (!Curve->FloatCurve.Keys.IsValidIndex(Index)) + { + return {}; + } + + return Curve->FloatCurve.Keys[Index].Time; +} + +TOptional +SHoudiniFloatRampCurveEditor::GetCurveKeyValue(const int32 Index) const +{ + if (!Curve) + { + return {}; + } + + if (!Curve->FloatCurve.Keys.IsValidIndex(Index)) + { + return {}; + } + + return Curve->FloatCurve.Keys[Index].Value; +} + +TOptional +SHoudiniFloatRampCurveEditor::GetCurveKeyInterpolationType(const int32 Index) const +{ + if (!Curve) + { + return {}; + } + + if (!Curve->FloatCurve.Keys.IsValidIndex(Index)) + { + return {}; + } + + return Curve->FloatCurve.Keys[Index].InterpMode.GetValue(); +} + +FReply +SHoudiniFloatRampCurveEditor::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + bIsMouseButtonDown = false; + + return SCurveEditor::OnMouseButtonUp(MyGeometry, MouseEvent); +} + +FReply SHoudiniFloatRampCurveEditor::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + bIsMouseButtonDown = true; + + return SCurveEditor::OnMouseButtonDown(MyGeometry, MouseEvent);; +} + +TSharedRef SHoudiniFloatRamp::ConstructRampPointValueWidget(const int32 Index) +{ + if (!RampView.IsValid()) + { + return SNullWidget::NullWidget; + } + + return SNew(SNumericEntryBox) + .AllowSpin(true) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Value(RampView->GetRampPointValue(Index).Get(0.f)) + .OnValueChanged_Lambda([](float Val) {}) + .OnValueCommitted_Lambda( + [this, Index](float Val, ETextCommit::Type TextCommitType) + { + // For some reason I can't figure out, UE sends a second commit event with type Default, which has the old + // value, causing the first commit to be reset. So ignore it. + if (TextCommitType == ETextCommit::Type::Default) + return; + + if (OnPointValueCommit(Index, Val)) + { + OnValueCommitted.ExecuteIfBound(); + } + }) + .OnBeginSliderMovement_Lambda([]() {}) + .OnEndSliderMovement_Lambda( + [this, Index](const float Val) + { + if (OnPointValueCommit(Index, Val)) + { + OnValueCommitted.ExecuteIfBound(); + } + }) + .SliderExponent(1.0f); +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniFloatRamp.h`: + +```h +/* +* Copyright (c) <2024> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "Curves/CurveFloat.h" +#include "SCurveEditor.h" +#include "SHoudiniRampBase.h" +#include "HoudiniRampEditorWindowSubsystem.h" + +class UHoudiniParameterRampFloat; +class UHoudiniParameterRampFloatPoint; + +class FHoudiniFloatRampView : public THoudiniRampViewBase< + FHoudiniFloatRampView, float, UHoudiniParameterRampFloat, UHoudiniParameterRampFloatPoint> +{ +public: + + static constexpr ValueType DefaultInsertValue = 1.f; + + explicit FHoudiniFloatRampView(TArrayView Parameters) + : THoudiniRampViewBase(Parameters) + { + } +}; + +class SHoudiniFloatRampCurveEditor + : public SHoudiniRampCurveEditorBase +{ +public: + + using ValueType = SHoudiniRampCurveEditorBase::ValueType; + + SLATE_BEGIN_ARGS(SHoudiniFloatRampCurveEditor) + : _ViewMinInput(0.0f) + , _ViewMaxInput(1.0f) + , _ViewMinOutput(0.0f) + , _ViewMaxOutput(1.0f) + , _InputSnap(0.1f) + , _OutputSnap(0.05f) + , _InputSnappingEnabled(false) + , _OutputSnappingEnabled(false) + , _ShowTimeInFrames(false) + , _TimelineLength(1.0f) + , _DesiredSize(FVector2D::ZeroVector) + , _DrawCurve(true) + , _HideUI(true) + , _AllowZoomOutput(false) + , _AlwaysDisplayColorCurves(false) + , _ZoomToFitVertical(false) + , _ZoomToFitHorizontal(false) + , _ShowZoomButtons(false) + , _XAxisName("X") + , _YAxisName("Y") + , _ShowInputGridNumbers(false) + , _ShowOutputGridNumbers(false) + , _ShowCurveSelector(false) + , _GridColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.3f)) + { + _Clipping = EWidgetClipping::ClipToBounds; + } + + SLATE_ARGUMENT(TSharedPtr, RampView) + SLATE_EVENT(FOnCurveChanged, OnCurveChanged) + SLATE_ATTRIBUTE(float, ViewMinInput) + SLATE_ATTRIBUTE(float, ViewMaxInput) + SLATE_ATTRIBUTE(TOptional, DataMinInput) + SLATE_ATTRIBUTE(TOptional, DataMaxInput) + SLATE_ATTRIBUTE(float, ViewMinOutput) + SLATE_ATTRIBUTE(float, ViewMaxOutput) + SLATE_ATTRIBUTE(float, InputSnap) + SLATE_ATTRIBUTE(float, OutputSnap) + SLATE_ATTRIBUTE(bool, InputSnappingEnabled) + SLATE_ATTRIBUTE(bool, OutputSnappingEnabled) + SLATE_ATTRIBUTE(bool, ShowTimeInFrames) + SLATE_ATTRIBUTE(float, TimelineLength) + SLATE_ATTRIBUTE(FVector2D, DesiredSize) + SLATE_ATTRIBUTE(bool, AreCurvesVisible) + SLATE_ARGUMENT(bool, DrawCurve) + SLATE_ARGUMENT(bool, HideUI) + SLATE_ARGUMENT(bool, AllowZoomOutput) + SLATE_ARGUMENT(bool, AlwaysDisplayColorCurves) + SLATE_ARGUMENT(bool, ZoomToFitVertical) + SLATE_ARGUMENT(bool, ZoomToFitHorizontal) + SLATE_ARGUMENT(bool, ShowZoomButtons) + SLATE_ARGUMENT(TOptional, XAxisName) + SLATE_ARGUMENT(TOptional, YAxisName) + SLATE_ARGUMENT(bool, ShowInputGridNumbers) + SLATE_ARGUMENT(bool, ShowOutputGridNumbers) + SLATE_ARGUMENT(bool, ShowCurveSelector) + SLATE_ARGUMENT(FLinearColor, GridColor) + SLATE_EVENT(FOnSetInputViewRange, OnSetInputViewRange) + SLATE_EVENT(FOnSetOutputViewRange, OnSetOutputViewRange) + SLATE_EVENT(FOnSetAreCurvesVisible, OnSetAreCurvesVisible) + SLATE_EVENT(FSimpleDelegate, OnCreateAsset) + + SLATE_END_ARGS() + +public: + + /** Widget construction. **/ + void Construct(const FArguments& InArgs); + + ~SHoudiniFloatRampCurveEditor(); + + FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + /** Refreshes rich curve keys using info from ramp view. */ + void RefreshCurveKeys() override; + +protected: + + TOptional GetNumCurveKeys() const override; + TOptional GetCurveKeyPosition(const int32 Index) const override; + TOptional GetCurveKeyValue(const int32 Index) const override; + TOptional GetCurveKeyInterpolationType(const int32 Index) const override; + +private: + + // We only want to commit curve editor value to the parameter on mouse release. OnUpdateCurve + // gets called continuously as we drag a point, so we use this value to determine if we should + // notify the ramp view of a change. + bool bIsMouseButtonDown = false; + + // Unreal representation of the curve which we display. Typically, this curve object is owned by + // some object, however we only use it for the purpose of using Unreal's curve editor widget. + // In order to avoid being garbage collected, we add the curve to root. Thus we need to remove + // the curve from root when the widget is destroyed. + UCurveFloat* Curve; + + FDelegateHandle OnUpdateCurveDelegateHandle; + + void OnUpdateCurve(UCurveBase*, EPropertyChangeType::Type); +}; + + +class SHoudiniFloatRamp + : public SHoudiniRampBase +{ +protected: + + /** Gets the column header label for value type of the ramp. */ + FORCEINLINE FString GetValueString() const override + { + return TEXT("Value"); + } + + TSharedRef ConstructRampPointValueWidget(const int32 Index) override; +}; + +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniNodeSyncPanel.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "SHoudiniNodeSyncPanel.h" + +#include "HoudiniEngineEditorPrivatePCH.h" + +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniEditorNodeSyncSubsystem.h" +#include "HoudiniInputDetails.h" +#include "HoudiniInput.h" + +#include "SNewFilePathPicker.h" +#include "SSelectFolderPathDialog.h" +#include "SSelectHoudiniPathDialog.h" +#include "SHoudiniNodeTreeView.h" + +#include "ActorTreeItem.h" +#include "DetailsViewArgs.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "PropertyCustomizationHelpers.h" +#include "PropertyEditorModule.h" +#include "SceneOutlinerModule.h" +#include "SlateOptMacros.h" +#include "UObject/UObjectGlobals.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SDirectoryPicker.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Layout/SExpandableArea.h" +#include "Widgets/Layout/SScrollBox.h" + + +#define LOCTEXT_NAMESPACE "HoudiniNodeSync" + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION + +SHoudiniNodeSyncPanel::SHoudiniNodeSyncPanel() + : SelectedActors(true, nullptr) +{ + +} + + +SHoudiniNodeSyncPanel::~SHoudiniNodeSyncPanel() +{ + +} + +void +SHoudiniNodeSyncPanel::Construct( const FArguments& InArgs ) +{ + bIsAssetEditorPanel = InArgs._IsAssetEditor.Get(); + + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + TSharedPtr HoudiniLogoBox; + TSharedPtr ImportOptionsArea; + TSharedPtr FetchToWorldOptionsArea; + + SelectionContainer = SNew(SVerticalBox); + RebuildSelectionView(); + + ExportOptionsVBox = SNew(SVerticalBox); + LandscapeOptionsVBox = SNew(SVerticalBox); + LandscapeSplineOptionsVBox = SNew(SVerticalBox); + + TSharedPtr FetchButton; + TSharedPtr SendWorldButton; + + const FSlateFontInfo BoldFontStyle = FCoreStyle::GetDefaultFontStyle("Bold", 14); + TSharedPtr CheckBoxUseExistingSkeleton; + TSharedPtr CheckBoxUseOutputNodes; + TSharedPtr CheckBoxFetchToWorld; + TSharedPtr CheckBoxReplaceExisting; + TSharedPtr CheckBoxAutoBake; + TSharedPtr CheckBoxSyncWorld; + + auto OnImportFolderBrowseButtonClickedLambda = []() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + if (!HoudiniEditorNodeSyncSubsystem) + return FReply::Handled(); + + HoudiniEditorNodeSyncSubsystem->CreateSessionIfNeeded(); + + TSharedRef Dialog = + SNew(SSelectFolderPathDialog) + .InitialPath(FText::FromString(HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.UnrealAssetFolder)) + .TitleText(LOCTEXT("CookFolderDialogTitle", "Select Temporary Cook Folder")); + + if (Dialog->ShowModal() != EAppReturnType::Cancel) + { + HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.UnrealAssetFolder = Dialog->GetFolderPath().ToString(); + } + return FReply::Handled(); + + }; + + auto OnFetchFolderBrowseButtonClickedLambda = []() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + if (!HoudiniEditorNodeSyncSubsystem) + return FReply::Handled(); + + TSharedRef Dialog = + SNew(SSelectHoudiniPathDialog) + .InitialPath(FText::FromString(HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.FetchNodePath)) + .TitleText(LOCTEXT("FetchPathDialogTitle", "Select Houdini nodes to fetch")) + .SingleSelection(false); + + if (Dialog->ShowModal() != EAppReturnType::Cancel) + { + HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.FetchNodePath = Dialog->GetFolderPath().ToString(); + } + + return FReply::Handled(); + }; + + // Get the session status + auto GetSessionSyncStatusAndColor = [](FString& OutStatus, FLinearColor& OutStatusColor) + { + OutStatus = TEXT("Session Status"); + OutStatusColor = FLinearColor::Red; + + FHoudiniEngine::Get().GetSessionStatusAndColor(OutStatus, OutStatusColor); + + // For valid state, check if session sync is enabled + if (OutStatusColor != FLinearColor::Red && OutStatusColor != FLinearColor::White) + { + bool bSessionSyncON = FHoudiniEngine::Get().IsSessionSyncEnabled(); + if (!bSessionSyncON) + { + // Append a warning and change the color to orange + OutStatus += TEXT(" - Session Sync OFF"); + OutStatusColor = FLinearColor(1.0f, 0.647f, 0.0f); + } + else + { + // Append a warning and change the color to orange + OutStatus += TEXT(" - Session Sync READY"); + } + } + }; + + // Tool for the fetch path + FString FetchPathTooltipString = + "The path of the nodes in Houdini that you want to fetch.\ne.g /obj/MyNetwork/Mynode \nThe paths can easily be obtained by using the browse button and selecting them in the dialog.\ + \nAlternatively, you can copy/paste a node to this text box to get its path.\nMultiple paths can be separated by using ; delimiters."; + + TSharedPtr NodeSyncVerticalBox; + ChildSlot + [ + SNew(SScrollBox) + .Orientation(Orient_Vertical) + + SScrollBox::Slot() + [ + SAssignNew(NodeSyncVerticalBox, SVerticalBox) + ] + ]; + + //------------------------------------------------------------------------------------------ + // Session status + //------------------------------------------------------------------------------------------ + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Left) + .Padding(15.0, 0.0, 0.0, 0.0) + .AutoHeight() + [ + SNew(SBox) + .HAlign(HAlign_Left) + .VAlign(VAlign_Top) + .MinDesiredWidth(500.0) + [ + SAssignNew(HoudiniLogoBox, SHorizontalBox) + ] + ]; + + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(15.0, 0.0, 15.0, 15.0) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Top) + [ + SNew(STextBlock) + .Justification(ETextJustify::Left) + .Text_Lambda([GetSessionSyncStatusAndColor]() + { + FString StatusString; + FLinearColor StatusColor; + GetSessionSyncStatusAndColor(StatusString, StatusColor); + return FText::FromString(StatusString); + }) + .ColorAndOpacity_Lambda([GetSessionSyncStatusAndColor]() + { + FString StatusString; + FLinearColor StatusColor; + GetSessionSyncStatusAndColor(StatusString, StatusColor); + return FSlateColor(StatusColor); + }) + ] + ]; + + //------------------------------------------------------------------------------------------ + // FETCH from Houdini + //------------------------------------------------------------------------------------------ + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0, 20.0, 0.0, 15.0) + [ + SNew(SBox) + .HAlign(HAlign_Center) + .VAlign(VAlign_Top) + [ + SNew(STextBlock) + .Font(BoldFontStyle) + .Text(LOCTEXT("FetchLabel", "FETCH from Houdini")) + ] + ]; + + // HOUDINI NODE PATH + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0, 0.0, 0.0, 5.0) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .HAlign(HAlign_Left) + //.MaxWidth(HAPI_UNREAL_DESIRED_SETTINGS_ROW_FULL_WIDGET_WIDTH) + [ + SNew(SBox) + .WidthOverride(335.0f) + //.MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + .ToolTipText(FText::FromString(FetchPathTooltipString)) + [ + SNew(STextBlock) + .Text(LOCTEXT("FetchNodePathLabel", "Houdini Node Paths To Fetch")) + ] + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Left) + //.FillWidth(1.0f) + [ + SNew(SEditableTextBox) + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + .ToolTipText_Lambda([FetchPathTooltipString]() + { + FString TooltipString = FetchPathTooltipString; + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + if (!HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.FetchNodePath.IsEmpty()) + { + TooltipString += "\n\nCurrent value:\n"; + TooltipString += HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.FetchNodePath.Replace(TEXT(";"), TEXT("\n")); + } + + return FText::FromString(TooltipString); + }) + .HintText(LOCTEXT("NodePathLabel", "Houdini Node Paths To Fetch")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + return FText::FromString(HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.FetchNodePath); + }) + .OnTextCommitted_Lambda([](const FText& Val, ETextCommit::Type TextCommitType) + { + FString NewPathStr = Val.ToString(); + + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.FetchNodePath = NewPathStr; + }) + ] + + + SHorizontalBox::Slot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SButton) + //.ContentPadding(FMargin(6.0, 2.0)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled(true) + .Text(LOCTEXT("BrowseButtonText", "...")) + .ToolTipText(LOCTEXT("FetchBrowseButtonToolTip", "Browse to select the nodes to fetch...")) + .OnClicked_Lambda(OnFetchFolderBrowseButtonClickedLambda) + ] + ]; + + // USE OUTPUT NODE + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0f, 0.0f, 0.0f, 5.0f) + [ + SNew(SBox) + .WidthOverride(160.f) + [ + SAssignNew(CheckBoxUseOutputNodes, SCheckBox) + .Content() + [ + SNew(STextBlock).Text(LOCTEXT("UseOutputNodes", "Use Output Nodes")) + .ToolTipText(LOCTEXT("UseOutputNodesToolTip", "If enabled, output nodes will be prefered over the display flag when fetching a node's data.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + return HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.bUseOutputNodes ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked); + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.bUseOutputNodes = bNewState; + }) + ] + ]; + + // REPLACE EXISTING + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0f, 0.0f, 0.0f, 5.0f) + [ + SNew(SBox) + .WidthOverride(160.f) + [ + SAssignNew(CheckBoxReplaceExisting, SCheckBox) + .Content() + [ + SNew(STextBlock).Text(LOCTEXT("ReplaceExisting", "Replace Existing Assets/Actors")) + .ToolTipText(LOCTEXT("ReplaceExisitngToolTip", "If enabled, existing Assets or Actors will be overwritten and replaced by the newly fetched data.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + return HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.bReplaceExisting ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked); + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.bReplaceExisting = bNewState; + }) + ] + ]; + + // UNREAL ASSET NAME + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0, 0.0, 0.0, 5.0) + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .HAlign(HAlign_Left) + [ + SNew(SBox) + .WidthOverride(335.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("UnrealAssetName", "Unreal Asset Name")) + ] + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Right) + //.AutoWidth() + [ + SNew(SEditableTextBox) + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH + 45) + .ToolTipText(LOCTEXT("UnrealAssetNameTooltip", + "Name to be given to the fetched data in Unreal.\nLeaving this field empty will use the node name for the unreal names.")) + .HintText(LOCTEXT("UnrealAssetNameLabel", "Name the of Asset in Unreal")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + return FText::FromString(HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.UnrealAssetName); + }) + .OnTextCommitted_Lambda([](const FText& Val, ETextCommit::Type TextCommitType) + { + FString NewPathStr = Val.ToString(); + + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.UnrealAssetName = NewPathStr; + }) + ] + ]; + + // UNREAL ASSET FOLDER + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0, 0.0, 0.0, 5.0) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .HAlign(HAlign_Left) + [ + SNew(SBox) + .WidthOverride(335.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("UnrealAssetFolder", "Unreal Asset Import Folder")) + ] + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Right) + [ + SNew(SEditableTextBox) + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + .ToolTipText(LOCTEXT("UnrealAssetFolderTooltip","Path to the project folder that will contain the generated assets in unreal")) + .HintText(LOCTEXT("UnrealAssetFolderLabel", "Unreal Asset Import Folder")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + return FText::FromString(HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.UnrealAssetFolder); + }) + .OnTextCommitted_Lambda([](const FText& Val, ETextCommit::Type TextCommitType) + { + FString NewPathStr = Val.ToString(); + + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.UnrealAssetFolder = NewPathStr; + }) + ] + + SHorizontalBox::Slot() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SButton) + //.ContentPadding(FMargin(6.0, 2.0)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled(true) + .Text(LOCTEXT("BrowseButtonText", "...")) + .ToolTipText(LOCTEXT("ImportFolderBrowseButtonToolTip", "Browse to select the Import Asset folder...")) + .OnClicked_Lambda(OnImportFolderBrowseButtonClickedLambda) + ] + /* + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(5.0, 0.0, 0.0, 0.0) + .VAlign(VAlign_Center) + [ + SNew(SButton) + //.ContentPadding(FMargin(6.0, 2.0)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .IsEnabled(true) + .Text(LOCTEXT("ResetButtonText", "Reset")) + .ToolTipText(LOCTEXT("CookFolderResetButtonToolTip", "Reset the cook folder to default setting")) + .OnClicked_Lambda(OnImportFolderBrowseButtonClickedLambda) + ]; + */ + ]; + + if (!bIsAssetEditorPanel) + { + // FETCH TO WORLD? + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0f, 0.0f, 0.0f, 5.0f) + [ + SNew(SBox) + .WidthOverride(160.f) + //.IsEnabled(false) + [ + SAssignNew(CheckBoxFetchToWorld, SCheckBox) + .Content() + [ + SNew(STextBlock).Text(LOCTEXT("FetchToWorld", "Fetch to World Outliner")) + .ToolTipText(LOCTEXT("FetchToWorldToolTip", "If enabled, the data fetched from Houdini will be instantiated as an Actor in the current level.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + return HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.bFetchToWorld ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked); + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.bFetchToWorld = bNewState; + }) + ] + ]; + + // FETCH TO WORLD OPTIONS + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0, 0.0, 0.0, 5.0) + [ + SAssignNew(FetchToWorldOptionsArea, SExpandableArea) + .InitiallyCollapsed(true) + .HeaderContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("FetchToWorldOptions", "Fetch to World Options")) + .Font(FAppStyle::Get().GetFontStyle("DetailsView.CategoryFontStyle")) + .ShadowOffset(FVector2D(1.0f, 1.0f)) + ] + .BodyContent() + [ + SNew(SVerticalBox) + // AutoBake? + + SVerticalBox::Slot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0f, 0.0f, 0.0f, 5.0f) + [ + SNew(SBox) + .WidthOverride(160.f) + [ + SAssignNew(CheckBoxAutoBake, SCheckBox) + .Content() + [ + SNew(STextBlock).Text(LOCTEXT("AutoBake", "Auto Bake")) + .ToolTipText(LOCTEXT("AutoBakeToolTip", "If enabled, output data fetched to world will automatically be baked. If disabled, they will be created as temporary cooked data, and attached to a Houdini Node Sync Component.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + return HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.bAutoBake ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked); + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.bAutoBake = bNewState; + }) + ] + ] + // UNREAL ACTOR NAME + + SVerticalBox::Slot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0, 0.0, 0.0, 5.0) + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .HAlign(HAlign_Left) + [ + SNew(SBox) + .WidthOverride(335.0f) + //.IsEnabled(false) + [ + SNew(STextBlock) + .Text(LOCTEXT("UnrealActorName", "Unreal Actor Name")) + ] + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Right) + [ + SNew(SEditableTextBox) + //.IsEnabled(false) + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + .ToolTipText(LOCTEXT("UnrealActorNameTooltip", "Name of the generated Actor in unreal")) + .HintText(LOCTEXT("UnrealActorNameLabel", "Unreal Actor Name")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + return FText::FromString(HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.UnrealActorName); + }) + .OnTextCommitted_Lambda([](const FText& Val, ETextCommit::Type TextCommitType) + { + FString NewPathStr = Val.ToString(); + + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.UnrealActorName = NewPathStr; + }) + ] + ] + + // UNREAL ACTOR FOLDER + + SVerticalBox::Slot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0, 0.0, 0.0, 5.0) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .HAlign(HAlign_Left) + [ + SNew(SBox) + .WidthOverride(335.0f) + //.IsEnabled(false) + [ + SNew(STextBlock) + .Text(LOCTEXT("UnrealActorFolderLabel", "World Outliner Folder")) + ] + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Right) + [ + SNew(SEditableTextBox) + //.IsEnabled(false) + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + .ToolTipText(LOCTEXT("UnrealActorFolderTooltip","Path to a world outliner folder that will contain the created Actor")) + .HintText(LOCTEXT("UnrealActorFolderLabel", "Unreal Actor World Outliner Folder")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + return FText::FromString(HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.UnrealActorFolder); + }) + .OnTextCommitted_Lambda([](const FText& Val, ETextCommit::Type TextCommitType) + { + FString NewPathStr = Val.ToString(); + + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.UnrealActorFolder = NewPathStr; + }) + ] + ] + ] + ]; + } + + /* + // Build Settings + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Center) + .AutoHeight() + .Padding(5.0, 5.0, 5.0, 5.0) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + [ + MakeMBSDetailsView() + ] + ]; + + // MeshGenerationProeprties + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Center) + .AutoHeight() + .Padding(5.0, 5.0, 5.0, 5.0) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + [ + MakeHSMGPDetailsView() + ] + ]; + */ + + // FETCH BUTTON + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Center) + .AutoHeight() + .Padding(5.0, 5.0, 5.0, 5.0) + [ + SNew(SBox) + .HAlign(HAlign_Center) + .VAlign(VAlign_Top) + [ + SNew(SBox) + .WidthOverride(135.0f) + [ + SAssignNew(FetchButton, SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("FetchFromHoudiniLabel", "Fetch Asset From Houdini")) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->FetchFromHoudini(); + return FReply::Handled(); + }) + .Content() + [ + SNew(STextBlock) + .Text(FText::FromString("Fetch")) + ] + ] + ] + ]; + + // Last FETCH status + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Center) + .AutoHeight() + .Padding(15.0, 0.0, 15.0, 15.0) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Top) + [ + SNew(STextBlock) + .Justification(ETextJustify::Left) + .Text_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + return FText::FromString(HoudiniEditorNodeSyncSubsystem->FetchStatusMessage); + }) + .ColorAndOpacity_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + FLinearColor StatusColor = UHoudiniEditorNodeSyncSubsystem::GetStatusColor(HoudiniEditorNodeSyncSubsystem->LastFetchStatus); + return FSlateColor(StatusColor); + }) + .ToolTipText_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + if(!HoudiniEditorNodeSyncSubsystem->FetchStatusDetails.IsEmpty()) + return FText::FromString(HoudiniEditorNodeSyncSubsystem->FetchStatusDetails); + else + return FText::FromString(HoudiniEditorNodeSyncSubsystem->FetchStatusMessage); + }) + ] + ]; + + + //------------------------------------------------------------------------------------------ + // SEND to Houdini + //------------------------------------------------------------------------------------------ + if(!bIsAssetEditorPanel) + { + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0, 20.0, 0.0, 15.0) + [ + SNew(SBox) + .HAlign(HAlign_Center) + .VAlign(VAlign_Top) + [ + SNew(STextBlock) + .Font(BoldFontStyle) + .Text(LOCTEXT("SendLabel", "SEND to Houdini")) + ] + ]; + + // Houdini Node Path + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0, 0.0, 0.0, 5.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().HAlign(HAlign_Left) + [ + SNew(SBox) + .WidthOverride(335.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("SendNodePathLabel", "Houdini Node Path To Send To")) + ] + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Right) + [ + SNew(SEditableTextBox) + .MinDesiredWidth(HAPI_UNREAL_DESIRED_ROW_VALUE_WIDGET_WIDTH) + .ToolTipText(LOCTEXT("HoudiniNodePathTooltip", + "The path of the node in Houdini that will receive the sent data. e.g /obj/UnrealContent ")) + .HintText(LOCTEXT("NodePathLabel", "Houdini Node Path To Send To")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + return FText::FromString(HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.SendNodePath); + }) + .OnTextCommitted_Lambda([](const FText& Val, ETextCommit::Type TextCommitType) + { + FString NewPathStr = Val.ToString(); + + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.SendNodePath = NewPathStr; + }) + ] + ]; + + // Export Options + NodeSyncVerticalBox->AddSlot() + .AutoHeight() + .Padding(10.0, 0.0, 0.0, 5.0) + [ + ExportOptionsVBox.ToSharedRef() + ]; + + // Landscape Options + NodeSyncVerticalBox->AddSlot() + .AutoHeight() + .Padding(10.0, 0.0, 0.0, 5.0) + [ + LandscapeOptionsVBox.ToSharedRef() + ]; + + // Landscape Spline Options + NodeSyncVerticalBox->AddSlot() + .AutoHeight() + .Padding(10.0, 0.0, 0.0, 5.0) + [ + LandscapeSplineOptionsVBox.ToSharedRef() + ]; + + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Left) + .AutoHeight() + .Padding(10.0f, 0.0f, 0.0f, 5.0f) + [ + SNew(SBox) + .WidthOverride(160.f) + [ + SAssignNew(CheckBoxSyncWorld, SCheckBox) + .Content() + [ + SNew(STextBlock).Text(LOCTEXT("SyncWorld", "Sync World Inputs")) + .ToolTipText(LOCTEXT("SyncWorldToolTip", "If enabled, actors sent to Houdini will be automatically updated in Houdini if they are modified in the level.")) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + .IsChecked_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + return HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.bSyncWorldInput ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([](ECheckBoxState NewState) + { + const bool bNewState = (NewState == ECheckBoxState::Checked); + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->NodeSyncOptions.bSyncWorldInput = bNewState; + if (bNewState) + HoudiniEditorNodeSyncSubsystem->StartTicking(); + else + HoudiniEditorNodeSyncSubsystem->StopTicking(); + }) + ] + ]; + + + // SEND Button + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Center) + .AutoHeight() + .Padding(5.0, 5.0, 5.0, 5.0) + [ + SNew(SBox) + .HAlign(HAlign_Center) + .VAlign(VAlign_Top) + [ + SNew(SBox) + .WidthOverride(135.0f) + [ + SAssignNew(SendWorldButton, SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("SendWorldToHoudiniLabel", "Send World Selection To Houdini")) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->SendWorldSelection(); + return FReply::Handled(); + }) + .Content() + [ + SNew(STextBlock) + .Text(FText::FromString("Send")) + ] + ] + ] + ]; + + // Last SEND status + NodeSyncVerticalBox->AddSlot() + .HAlign(HAlign_Center) + .AutoHeight() + .Padding(15.0, 0.0, 15.0, 15.0) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Top) + [ + SNew(STextBlock) + .Justification(ETextJustify::Left) + .Text_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + return FText::FromString(HoudiniEditorNodeSyncSubsystem->SendStatusMessage); + }) + .ColorAndOpacity_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + FLinearColor StatusColor = UHoudiniEditorNodeSyncSubsystem::GetStatusColor(HoudiniEditorNodeSyncSubsystem->LastSendStatus); + return FSlateColor(StatusColor); + }) + .ToolTipText_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + if (!HoudiniEditorNodeSyncSubsystem->SendStatusDetails.IsEmpty()) + return FText::FromString(HoudiniEditorNodeSyncSubsystem->SendStatusDetails); + else + return FText::FromString(HoudiniEditorNodeSyncSubsystem->SendStatusMessage); + }) + ] + ]; + + // World IN UI + NodeSyncVerticalBox->AddSlot() + .AutoHeight() + .Padding(10.0, 0.0, 0.0, 5.0) + [ + SelectionContainer.ToSharedRef() + ]; + + // World IN UI + NodeSyncVerticalBox->AddSlot() + .AutoHeight() + .Padding(10.0, 0.0, 0.0, 5.0) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .HAlign(HAlign_Right) + .Padding(5.0, 5.0, 5.0, 5.0) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("UpdateAll", "Update All Sent Data")) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->UpdateAllSelection(); + return FReply::Handled(); + }) + .Content() + [ + SNew(STextBlock) + .Text(FText::FromString("Update All")) + ] + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Left) + .Padding(5.0, 5.0, 5.0, 5.0) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ToolTipText(LOCTEXT("DeleteAll", "Delete All Sent Data")) + .Visibility(EVisibility::Visible) + .OnClicked_Lambda([]() + { + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + HoudiniEditorNodeSyncSubsystem->DeleteAllSelection(); + return FReply::Handled(); + }) + .Content() + [ + SNew(STextBlock) + .Text(FText::FromString("Delete All")) + ] + ] + ]; + } + + // Get the NodeSync inputs from the editor subsystem + UHoudiniInput* NodeSyncWorldInput = nullptr; + if (!HoudiniEditorNodeSyncSubsystem || !HoudiniEditorNodeSyncSubsystem->GetNodeSyncWorldInput(NodeSyncWorldInput)) + return; + + UHoudiniInput* NodeSyncCBInput = nullptr; + if (!HoudiniEditorNodeSyncSubsystem || !HoudiniEditorNodeSyncSubsystem->GetNodeSyncCBInput(NodeSyncCBInput)) + return; + + // ... and create an input array ... + TArray> NodeSyncInputs; + NodeSyncInputs.Add(NodeSyncWorldInput); + NodeSyncInputs.Add(NodeSyncCBInput); + + // ... so we can reuse the input UI code + FHoudiniInputDetails::AddExportOptions(ExportOptionsVBox.ToSharedRef(), NodeSyncInputs); + FHoudiniInputDetails::AddLandscapeOptions(LandscapeOptionsVBox.ToSharedRef(), NodeSyncInputs); + FHoudiniInputDetails::AddLandscapeSplinesOptions(LandscapeSplineOptionsVBox.ToSharedRef(), NodeSyncInputs); + + // Handle the Houdini logo box + TSharedPtr Image; + HoudiniLogoBox->AddSlot() + .AutoWidth() + .Padding(0.0f, 5.0f, 5.0f, 10.0f) + .HAlign(HAlign_Left) + [ + SNew(SBox) + .HeightOverride(30) + .WidthOverride(208) + [ + SAssignNew(Image, SImage) + .ColorAndOpacity(FSlateColor::UseForeground()) + ] + ]; + + // Skip drawing the icon if the icon image is not loaded correctly. + TSharedPtr IconBrush = FHoudiniEngineEditor::Get().GetHoudiniEngineUIIconBrush(); + if (IconBrush.IsValid()) + { + Image->SetImage( + TAttribute::Create( + TAttribute::FGetter::CreateLambda( + [IconBrush](){return IconBrush.Get();} + ))); + } +} + + +FMenuBuilder +SHoudiniNodeSyncPanel::Helper_CreateSelectionWidget() +{ + auto OnShouldFilter = [](const AActor* const Actor) + { + if (!IsValid(Actor)) + return false; + + UHoudiniEditorNodeSyncSubsystem* HoudiniEditorNodeSyncSubsystem = GEditor->GetEditorSubsystem(); + if (!HoudiniEditorNodeSyncSubsystem) + return false; + + // Get the NodeSync input from the editor subsystem + UHoudiniInput* NodeSyncInput = nullptr; + if (!HoudiniEditorNodeSyncSubsystem || !HoudiniEditorNodeSyncSubsystem->GetNodeSyncWorldInput(NodeSyncInput)) + return false; + + const TArray>* InputObjects = NodeSyncInput->GetHoudiniInputObjectArray(EHoudiniInputType::World); + if (!InputObjects) + return false; + + // Only return actors that are currently selected by our input + for (const auto& CurInputObject : *InputObjects) + { + AActor* CurActor = Cast(CurInputObject->GetObject()); + if (!IsValid(CurActor)) + { + // See if the input object is a HAC, if it is, get its parent actor + UHoudiniAssetComponent* CurHAC = Cast(CurInputObject->GetObject()); + if (IsValid(CurHAC)) + CurActor = CurHAC->GetOwner(); + } + + if (!IsValid(CurActor)) + continue; + + if (CurActor == Actor) + return true; + } + + return false; + }; + + + auto OnSelected = [](AActor* Actor) + { + // Do Nothing + }; + + FMenuBuilder MenuBuilder(true, nullptr); + MenuBuilder.BeginSection(NAME_None, LOCTEXT("WorldInputSentData", "Sent Data")); + { + FSceneOutlinerModule& SceneOutlinerModule = + FModuleManager::Get().LoadModuleChecked(TEXT("SceneOutliner")); + FSceneOutlinerInitializationOptions InitOptions; + { + InitOptions.Filters->AddFilterPredicate(FActorTreeItem::FFilterPredicate::CreateLambda(OnShouldFilter)); + InitOptions.bFocusSearchBoxWhenOpened = false; + InitOptions.bShowCreateNewFolder = false; + + // Add the gutter so we can change the selection's visibility + InitOptions.ColumnMap.Add(FSceneOutlinerBuiltInColumnTypes::Gutter(), FSceneOutlinerColumnInfo(ESceneOutlinerColumnVisibility::Visible, 0, FCreateSceneOutlinerColumn(), true, TOptional(), FSceneOutlinerBuiltInColumnTypes::Gutter_Localized())); + InitOptions.ColumnMap.Add(FSceneOutlinerBuiltInColumnTypes::Label(), FSceneOutlinerColumnInfo(ESceneOutlinerColumnVisibility::Visible, 10, FCreateSceneOutlinerColumn(), true, TOptional(), FSceneOutlinerBuiltInColumnTypes::Label_Localized())); + InitOptions.ColumnMap.Add(FSceneOutlinerBuiltInColumnTypes::ActorInfo(), FSceneOutlinerColumnInfo(ESceneOutlinerColumnVisibility::Visible, 20, FCreateSceneOutlinerColumn(), true, TOptional(), FSceneOutlinerBuiltInColumnTypes::ActorInfo_Localized())); + } + + static const FVector2D SceneOutlinerWindowSize(350.0f, 200.0f); + TSharedRef< SWidget > MenuWidget = + SNew(SBox) + .WidthOverride(SceneOutlinerWindowSize.X) + .HeightOverride(SceneOutlinerWindowSize.Y) + [ + SNew(SBorder) + .BorderImage(_GetEditorStyle().GetBrush("Menu.Background")) + [ + SceneOutlinerModule.CreateActorPicker( + InitOptions, + FOnActorPicked::CreateLambda(OnSelected)) + ] + ]; + + MenuBuilder.AddWidget(MenuWidget, FText::GetEmpty(), true); + } + MenuBuilder.EndSection(); + + return MenuBuilder; +} + + + +void +SHoudiniNodeSyncPanel::RebuildSelectionView() +{ + // Clear the Box that contains the current sent data + SelectionContainer->ClearChildren(); + + // Recreate the actor picker with updated contents + FMenuBuilder MenuBuilder = Helper_CreateSelectionWidget(); + SelectionContainer->AddSlot() + .Padding(2, 2, 5, 2) + .AutoHeight() + [ + MenuBuilder.MakeWidget() + ]; +} + + +/* +// Create the widget that displays the Mesh Build Settings struct +TSharedRef +SHoudiniNodeSyncPanel::MakeMBSDetailsView() +{ + FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked("PropertyEditor"); + + FDetailsViewArgs DetailsViewArgs; + DetailsViewArgs.bUpdatesFromSelection = false; + DetailsViewArgs.bLockable = false; + DetailsViewArgs.bShowPropertyMatrixButton = false; + DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; + DetailsViewArgs.ViewIdentifier = NAME_None; + DetailsViewArgs.bShowCustomFilterOption = false; + DetailsViewArgs.bShowOptions = false; + DetailsViewArgs.bAllowSearch = false; + + FStructureDetailsViewArgs StructDetailsArgs; + MBS_DetailsView = PropertyEditorModule.CreateStructureDetailView(DetailsViewArgs, StructDetailsArgs, MBS_Ptr); + + return MBS_DetailsView->GetWidget().ToSharedRef(); +} + + +// Create the widget that displays the Houdini Static Mesh Generation Settings struct +TSharedRef +SHoudiniNodeSyncPanel::MakeHSMGPDetailsView() +{ + FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked("PropertyEditor"); + + FDetailsViewArgs DetailsViewArgs; + DetailsViewArgs.bUpdatesFromSelection = false; + DetailsViewArgs.bLockable = false; + DetailsViewArgs.bShowPropertyMatrixButton = false; + DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; + DetailsViewArgs.ViewIdentifier = NAME_None; + DetailsViewArgs.bShowCustomFilterOption = false; + DetailsViewArgs.bShowOptions = false; + DetailsViewArgs.bAllowSearch = false; + + FStructureDetailsViewArgs StructDetailsArgs; + HSMGP_DetailsView = PropertyEditorModule.CreateStructureDetailView(DetailsViewArgs, StructDetailsArgs, HSMGP_Ptr); + + return HSMGP_DetailsView->GetWidget().ToSharedRef(); +} +*/ + + + +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniNodeSyncPanel.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniInput.h" +#include "HoudiniRuntimeSettings.h" + +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "IDetailsView.h" +#include "IStructureDetailsView.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/SCompoundWidget.h" +#include "UObject/StructOnScope.h" + +class SHoudiniNodeSyncPanel : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SHoudiniNodeSyncPanel) + : _IsAssetEditor(false) + { + } + SLATE_ATTRIBUTE(bool, IsAssetEditor) + SLATE_END_ARGS(); + + SHoudiniNodeSyncPanel(); + virtual ~SHoudiniNodeSyncPanel() override; + + void Construct( const FArguments& InArgs ); + + FMenuBuilder Helper_CreateSelectionWidget(); + + void RebuildSelectionView(); + + //TSharedRef MakeMBSDetailsView(); + + //TSharedRef MakeHSMGPDetailsView(); + + //void SetIsAssetEditorPanel(const bool& IsAssetEditor) { bIsAssetEditorPanel = IsAssetEditor; }; + +private: + + TSharedPtr ExportOptionsVBox; + TSharedPtr LandscapeOptionsVBox; + TSharedPtr LandscapeSplineOptionsVBox; + + + TSharedPtr SelectionContainer; + + /*TSharedPtr> MBS_Ptr; + TSharedPtr> HSMGP_Ptr;*/ + + FMenuBuilder SelectedActors; + + // Details view for MeshBuildSettings + //TSharedPtr MBS_DetailsView; + // Details view for HoudiniStaticMeshGenerationProperties + //TSharedPtr HSMGP_DetailsView; + + bool bIsAssetEditorPanel = false; +}; + +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniNodeTreeView.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "SHoudiniNodeTreeView.h" + +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniEnginePrivatePCH.h" + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" + +// dlg? +#include "Internationalization/Internationalization.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SUniformGridPanel.h" + +#include "Camera/CameraComponent.h" +#include "Components/DirectionalLightComponent.h" +#include "Components/LightComponent.h" +#include "Components/PointLightComponent.h" +#include "Components/SpotLightComponent.h" +#include "Containers/Map.h" +#include "Containers/UnrealString.h" +#include "Framework/Commands/UIAction.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Framework/Views/ITypedTableView.h" +#include "GameFramework/Actor.h" +#include "HAL/PlatformMath.h" +#include "Internationalization/Internationalization.h" +#include "Internationalization/Text.h" +#include "Layout/Children.h" +#include "Layout/Visibility.h" +#include "Misc/AssertionMacros.h" +#include "Misc/Attribute.h" +#include "SlotBase.h" +#include "Styling/AppStyle.h" +#include "Styling/SlateIconFinder.h" +#include "Textures/SlateIcon.h" +#include "UObject/NameTypes.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/SCompoundWidget.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Views/SExpanderArrow.h" +#include "Widgets/Views/STableRow.h" + +class FUICommandList; +class ITableRow; +class SWidget; +class UClass; +struct FSlateBrush; + +#define LOCTEXT_NAMESPACE "HoudiniNodeTreeview" + + + +void +FHoudiniNodeInfo::RecursiveSetImport(FHoudiniNodeInfoPtr NodeInfoPtr, bool bImport) +{ + NodeInfoPtr->bImportNode = bImport; + for (auto Child : NodeInfoPtr->Childrens) + { + RecursiveSetImport(Child, bImport); + } + + if (!bImport) + { + // If we're not imported anymore we need to disable our parents' import + // as they are no longer fully selected... + FHoudiniNodeInfoPtr ParentPtr = NodeInfoPtr->Parent; + while (ParentPtr != NULL) + { + ParentPtr->bImportNode = false; + ParentPtr = ParentPtr->Parent; + } + } +} + +bool +FHoudiniNodeInfo::RecursiveGetImport(FHoudiniNodeInfoPtr NodeInfoPtr) +{ + if(NodeInfoPtr->bImportNode) + return true; + + for (auto Child : NodeInfoPtr->Childrens) + { + if (RecursiveGetImport(Child)) + return true; + } + + return false; +} + +// +// SHoudiniNodeTreeViewItem +// + +// Constructs the widget for one row of the TreeView +void +SHoudiniNodeTreeViewItem::Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) +{ + HoudiniNodeInfo = InArgs._HoudiniNodeInfo; + bool bExpanded = InArgs._Expanded; + bSingleSelectionOnly = InArgs._SingleSelection; + HoudiniRootNodesInfo = InArgs._HoudiniRootNodeArray; + + //This is suppose to always be valid + check(HoudiniNodeInfo.IsValid()); + + const FSlateBrush* ClassIcon = nullptr; + if (HoudiniNodeInfo->NodeType.Equals("OBJ")) + ClassIcon = bExpanded ? FAppStyle::GetBrush("Icons.FolderOpen") : FAppStyle::GetBrush("Icons.FolderClosed"); + else + ClassIcon = FSlateIconFinder::FindIconBrushForClass(AActor::StaticClass()); + + //Prepare the tooltip + FString Tooltip = HoudiniNodeInfo->NodeName; + if (!HoudiniNodeInfo->NodeType.IsEmpty()) + { + Tooltip += TEXT(" [") + HoudiniNodeInfo->NodeType + TEXT("]"); + } + + if (!HoudiniNodeInfo->NodeHierarchyPath.IsEmpty()) + { + Tooltip += TEXT("\n") + HoudiniNodeInfo->NodeHierarchyPath; + } + + this->ChildSlot + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f, 2.0f, 0.0f) + [ + SNew(SCheckBox) + .OnCheckStateChanged(this, &SHoudiniNodeTreeViewItem::OnItemCheckChanged) + .IsChecked(this, &SHoudiniNodeTreeViewItem::IsItemChecked) + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SExpanderArrow, SharedThis(this)) + ] + + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0.0f, 2.0f, 6.0f, 2.0f) + [ + SNew(SImage) + .Image(ClassIcon) + .Visibility(ClassIcon != FAppStyle::GetDefaultBrush() ? EVisibility::Visible : EVisibility::Collapsed) + ] + + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(0.0f, 3.0f, 6.0f, 3.0f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(HoudiniNodeInfo->NodeName)) + .ToolTipText(FText::FromString(Tooltip)) + ] + ]; + + STableRow::ConstructInternal( + STableRow::FArguments() + .ShowSelection(true), + InOwnerTableView + ); +} + +void +SHoudiniNodeTreeViewItem::OnItemCheckChanged(ECheckBoxState CheckType) +{ + if (!HoudiniNodeInfo.IsValid()) + return; + + bool bImport = CheckType == ECheckBoxState::Checked; + HoudiniNodeInfo->bImportNode = bImport; + + // If in single selection mode - disable all other nodes but us + if (bSingleSelectionOnly)// && bImport) + { + for (int32 Idx = 0; Idx < HoudiniRootNodesInfo.Num(); Idx++) + { + //FHoudiniNodeInfoPtr NodeInfo = (*NodeInfoIt); + FHoudiniNodeInfoPtr NodeInfo = HoudiniRootNodesInfo[Idx]; + if (!NodeInfo.IsValid()) + continue; + + if (NodeInfo->bIsRootNode) + { + FHoudiniNodeInfo::RecursiveSetImport(NodeInfo, false); + } + } + } + + // Recursively set our children's import state + FHoudiniNodeInfo::RecursiveSetImport(HoudiniNodeInfo, bImport); +} + +ECheckBoxState +SHoudiniNodeTreeViewItem::IsItemChecked() const +{ + return HoudiniNodeInfo->bImportNode ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; +} + + +// +// SHoudiniNodeTreeView +// + +SHoudiniNodeTreeView::~SHoudiniNodeTreeView() +{ + HoudiniRootNodeArray.Empty(); + bSingleSelectionOnly = false; +} + +void +SHoudiniNodeTreeView::Construct(const SHoudiniNodeTreeView::FArguments& InArgs) +{ + TSharedPtr HoudiniNetworkInfo = InArgs._HoudiniNetworkInfo; + //Build the FHoudiniNodeInfoPtr tree data + check(HoudiniNetworkInfo.IsValid()); + for (auto NodeInfoIt = HoudiniNetworkInfo->RootNodesInfos.CreateIterator(); NodeInfoIt; ++NodeInfoIt) + { + FHoudiniNodeInfoPtr NodeInfo = (*NodeInfoIt); + if (NodeInfo->bIsRootNode) + { + HoudiniRootNodeArray.Add(NodeInfo); + } + } + + bSingleSelectionOnly = InArgs._SingleSelection; + + STreeView::Construct + ( + STreeView::FArguments() + .TreeItemsSource(&HoudiniRootNodeArray) + .SelectionMode(bSingleSelectionOnly ? ESelectionMode::SingleToggle : ESelectionMode::Multi) + .OnGenerateRow(this, &SHoudiniNodeTreeView::OnGenerateRowHoudiniNodeTreeView) + .OnGetChildren(this, &SHoudiniNodeTreeView::OnGetChildrenHoudiniNodeTreeView) + .OnContextMenuOpening(this, &SHoudiniNodeTreeView::OnOpenContextMenu) + .OnSelectionChanged(this, &SHoudiniNodeTreeView::OnSelectionChanged) + .OnSetExpansionRecursive(this, &SHoudiniNodeTreeView::OnSetExpandRecursive) + ); + + // Expand the previous selection + for (auto NodeInfo : HoudiniRootNodeArray) + { + RecursiveSetDefaultExpand(NodeInfo); + } +} + + +TSharedRef +SHoudiniNodeTreeView::OnGenerateRowHoudiniNodeTreeView(FHoudiniNodeInfoPtr Item, const TSharedRef& OwnerTable) +{ + bool bExpanded = IsItemExpanded(Item) || FHoudiniNodeInfo::RecursiveGetImport(Item); + TSharedRef ReturnRow = SNew(SHoudiniNodeTreeViewItem, OwnerTable) + .HoudiniNodeInfo(Item) + .Expanded(bExpanded) + .SingleSelection(bSingleSelectionOnly) + .HoudiniRootNodeArray(HoudiniRootNodeArray); + return ReturnRow; +} + + +void +SHoudiniNodeTreeView::OnGetChildrenHoudiniNodeTreeView(FHoudiniNodeInfoPtr InParent, TArray& OutChildren) +{ + for (auto Child : InParent->Childrens) + { + OutChildren.Add(Child); + } +} + + +void +SHoudiniNodeTreeView::OnToggleSelectAll(ECheckBoxState CheckType) +{ + //check all actor for import + for(int32 Idx = 0; Idx < HoudiniRootNodeArray.Num(); Idx++) + { + FHoudiniNodeInfoPtr NodeInfo = HoudiniRootNodeArray[Idx]; + if (!NodeInfo.IsValid()) + continue; + + if(NodeInfo->bIsRootNode) + { + FHoudiniNodeInfo::RecursiveSetImport(NodeInfo, CheckType == ECheckBoxState::Checked); + } + } +} + +void +RecursiveSetExpand(SHoudiniNodeTreeView* TreeView, FHoudiniNodeInfoPtr NodeInfoPtr, bool ExpandState) +{ + TreeView->SetItemExpansion(NodeInfoPtr, ExpandState); + for (auto Child : NodeInfoPtr->Childrens) + { + RecursiveSetExpand(TreeView, Child, ExpandState); + } +} + +void +SHoudiniNodeTreeView::OnSetExpandRecursive(FHoudiniNodeInfoPtr NodeInfoPtr, bool ExpandState) +{ + RecursiveSetExpand(this, NodeInfoPtr, ExpandState); +} + +void +SHoudiniNodeTreeView::RecursiveSetDefaultExpand(FHoudiniNodeInfoPtr NodeInfo) +{ + bool bExpanded = IsItemExpanded(NodeInfo) || (FHoudiniNodeInfo::RecursiveGetImport(NodeInfo) && !NodeInfo->bImportNode); + SetItemExpansion(NodeInfo, bExpanded); + + for (auto Child : NodeInfo->Childrens) + { + RecursiveSetDefaultExpand(Child); + } +} + +FReply +SHoudiniNodeTreeView::OnExpandAll() +{ + for (int32 Idx = 0; Idx < HoudiniRootNodeArray.Num(); Idx++) + { + FHoudiniNodeInfoPtr NodeInfo = HoudiniRootNodeArray[Idx]; + if (!NodeInfo.IsValid()) + continue; + + if (NodeInfo->bIsRootNode) + { + RecursiveSetExpand(this, NodeInfo, true); + } + } + return FReply::Handled(); +} + +FReply +SHoudiniNodeTreeView::OnCollapseAll() +{ + for (int32 Idx = 0; Idx < HoudiniRootNodeArray.Num(); Idx++) + { + FHoudiniNodeInfoPtr NodeInfo = HoudiniRootNodeArray[Idx]; + if (!NodeInfo.IsValid()) + continue; + + if (NodeInfo->bIsRootNode) + { + RecursiveSetExpand(this, NodeInfo, false); + } + } + return FReply::Handled(); +} + +TSharedPtr +SHoudiniNodeTreeView::OnOpenContextMenu() +{ + // Build up the menu for a selection + const bool bCloseAfterSelection = true; + FMenuBuilder MenuBuilder(bCloseAfterSelection, TSharedPtr()); + + //Get the different type of the multi selection + TArray SelectedHoudiniNodeInfos; + const auto NumSelectedItems = GetSelectedItems(SelectedHoudiniNodeInfos); + + // We always create a section here, even if there is no parent so that clients can still extend the menu + MenuBuilder.BeginSection("HoudiniSceneTreeViewContextMenuImportSection"); + { + const FSlateIcon PlusIcon(FAppStyle::GetAppStyleSetName(), "Icons.Plus"); + MenuBuilder.AddMenuEntry(LOCTEXT("CheckForImport", "Add Selection To Import"), FText(), PlusIcon, FUIAction(FExecuteAction::CreateSP(this, &SHoudiniNodeTreeView::AddSelectionToImport))); + const FSlateIcon MinusIcon(FAppStyle::GetAppStyleSetName(), "Icons.Minus"); + MenuBuilder.AddMenuEntry(LOCTEXT("UncheckForImport", "Remove Selection From Import"), FText(), MinusIcon, FUIAction(FExecuteAction::CreateSP(this, &SHoudiniNodeTreeView::RemoveSelectionFromImport))); + } + MenuBuilder.EndSection(); + + return MenuBuilder.MakeWidget(); +} + +void +SHoudiniNodeTreeView::AddSelectionToImport() +{ + SetSelectionImportState(true); +} + +void +SHoudiniNodeTreeView::RemoveSelectionFromImport() +{ + SetSelectionImportState(false); +} + +void +SHoudiniNodeTreeView::SetSelectionImportState(bool MarkForImport) +{ + TArray SelectedHoudiniNodeInfos; + GetSelectedItems(SelectedHoudiniNodeInfos); + for (auto Item : SelectedHoudiniNodeInfos) + { + FHoudiniNodeInfoPtr ItemPtr = Item; + FHoudiniNodeInfo::RecursiveSetImport(ItemPtr, MarkForImport); + } +} + +void +SHoudiniNodeTreeView::OnSelectionChanged(FHoudiniNodeInfoPtr Item, ESelectInfo::Type SelectionType) +{ +} + + +#undef LOCTEXT_NAMESPACE + + +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniNodeTreeView.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "Containers/Array.h" +#include "Containers/BitArray.h" +#include "Containers/Set.h" +#include "Containers/SparseArray.h" +#include "Delegates/Delegate.h" +#include "HAL/PlatformCrt.h" +#include "Input/Reply.h" +#include "Misc/Optional.h" +#include "Styling/SlateTypes.h" +#include "Templates/SharedPointer.h" +#include "Templates/TypeHash.h" +#include "Types/SlateEnums.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/Views/STableViewBase.h" +#include "Widgets/Views/STreeView.h" + + + +class FHoudiniNodeInfo; +class FHoudiniNetworkInfo; +class ITableRow; +class SWidget; + +typedef TSharedPtr FHoudiniNodeInfoPtr; + + +// Largely inspired by SFbxSceneTreeView + +//Node use to store the scene hierarchy transform will be relative to the parent +class FHoudiniNodeInfo : public TSharedFromThis +{ +public: + + FHoudiniNodeInfo() + : NodeName(TEXT("")) + , NodeId(-1) + , NodeHierarchyPath(TEXT("")) + , bIsRootNode(false) + , bImportNode(true) + , Parent(NULL) + {} + + static void RecursiveSetImport(FHoudiniNodeInfoPtr NodeInfoPtr, bool ImportStatus); + + static bool RecursiveGetImport(FHoudiniNodeInfoPtr NodeInfoPtr); + + FString NodeName; + int32 NodeId; + FString NodeHierarchyPath; + bool bIsRootNode; + FString NodeType; + + bool bImportNode; + + TArray Childrens; + FHoudiniNodeInfoPtr Parent; +}; + + + +class FHoudiniNetworkInfo : public TSharedFromThis +{ +public: + + FHoudiniNetworkInfo() + {} + + TArray RootNodesInfos; +}; + + + +// The item used for visualizing the class in the tree. +class SHoudiniNodeTreeViewItem : public STableRow +{ +public: + + SLATE_BEGIN_ARGS(SHoudiniNodeTreeViewItem) + : _HoudiniNodeInfo(nullptr) + , _Expanded(false) + , _SingleSelection(false) + {} + + // Content of this item + SLATE_ARGUMENT(FHoudiniNodeInfoPtr, HoudiniNodeInfo) + SLATE_ARGUMENT(bool, Expanded) + SLATE_ARGUMENT(bool, SingleSelection) + SLATE_ARGUMENT(TArray, HoudiniRootNodeArray) + SLATE_END_ARGS() + + // Construct the widget + void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView); + +private: + + void OnItemCheckChanged(ECheckBoxState CheckType); + + ECheckBoxState IsItemChecked() const; + + // The node info to build the tree view row from. + FHoudiniNodeInfoPtr HoudiniNodeInfo; + + // Are we in single selection mode ? + bool bSingleSelectionOnly; + + TArray HoudiniRootNodesInfo; +}; + + + +class SHoudiniNodeTreeView : public STreeView +{ +public: + ~SHoudiniNodeTreeView(); + + SLATE_BEGIN_ARGS(SHoudiniNodeTreeView) + : _HoudiniNetworkInfo(NULL) + , _SingleSelection(false) + {} + SLATE_ARGUMENT(TSharedPtr, HoudiniNetworkInfo) + SLATE_ARGUMENT(bool, SingleSelection) + SLATE_END_ARGS() + + /** Construct this widget */ + void Construct(const FArguments& InArgs); + + TSharedRef< ITableRow > OnGenerateRowHoudiniNodeTreeView(FHoudiniNodeInfoPtr Item, const TSharedRef& OwnerTable); + void OnGetChildrenHoudiniNodeTreeView(FHoudiniNodeInfoPtr InParent, TArray& OutChildren); + + void OnToggleSelectAll(ECheckBoxState CheckType); + FReply OnExpandAll(); + FReply OnCollapseAll(); + +protected: + + // Info for all the nodes in the current session + //TSharedPtr HoudiniNetworkInfo; + //FHoudiniNetworkInfo HoudiniRootNodesInfo; + + // The elements shown in the tree view + TArray HoudiniRootNodeArray; + + // Are we in single selection mode ? + bool bSingleSelectionOnly; + + /** Open a context menu for the current selection */ + TSharedPtr OnOpenContextMenu(); + void AddSelectionToImport(); + void RemoveSelectionFromImport(); + void SetSelectionImportState(bool MarkForImport); + void OnSelectionChanged(FHoudiniNodeInfoPtr Item, ESelectInfo::Type SelectionType); + void OnSetExpandRecursive(FHoudiniNodeInfoPtr NodeInfoPtr, bool ExpandState); + + // Used to automatically expand previously selected items' parents upon construction + void RecursiveSetDefaultExpand(FHoudiniNodeInfoPtr NodeInfo); +}; +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniPresets.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "SHoudiniPresets.h" + +#include "AssetSelection.h" +#include "HoudiniAsset.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniParameter.h" +#include "HoudiniParameterColor.h" +#include "HoudiniParameterFile.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterRamp.h" +#include "HoudiniParameterString.h" +#include "HoudiniPreset.h" +#include "HoudiniPresetFactory.h" +#include "HoudiniToolsEditor.h" + +#include "AssetViewUtils.h" +#include "Framework/Application/SlateApplication.h" +#include "Interfaces/IMainFrameModule.h" +#include "PropertyCustomizationHelpers.h" +#include "SWarningOrErrorBox.h" +#include "ThumbnailRendering/ThumbnailManager.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Layout/SGridPanel.h" +#include "Widgets/Layout/SScrollBox.h" +#include "HoudiniToolsRuntimeUtils.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "UnrealClient.h" +#endif + +#define LOCTEXT_NAMESPACE "HoudiniPresets" + +SHoudiniPresetUIBase::SHoudiniPresetUIBase() + : bIsValidPresetPath(true) + , bApplyOnlyToSource(true) + , bRevertHDAParameters(false) + , bCanInstantiate(true) + , bApplyTempCookFolder(false) + , bApplyBakeFolder(false) + , bApplyBakeOptions(false) + , bApplyAssetOptions(false) + , bApplyStaticMeshGenSettings(false) + , bApplyProxyMeshGenSettings(false) + , LabelColumnWidth(0.35f) + , NameColumnWidth(0.3f) + , ValueColumnWidth(0.35f) + , bSelectAll(true) +{ +} + +#define PRESET_CAST_OR_CONTINUE(Type, Var) \ + Type* Var = Cast(Param); \ + if (!IsValid(Var)) \ + { \ + continue; \ + } \ + +void +SHoudiniPresetUIBase::Construct(const FArguments& InArgs) +{ + TWeakObjectPtr HC = InArgs._HoudiniCookable.Get(); + HoudiniCookable = HC; + + if (!HoudiniCookable.IsValid()) + { + HOUDINI_LOG_ERROR(TEXT("Trying to create a preset using an invalid Cookable")); + return; + } + + SourceHoudiniAsset = HC->GetHoudiniAsset(); + + PresetName = SourceHoudiniAsset->GetName(); + PresetLabel = PresetName; + PresetDescription = FString(); + + if (SourceHoudiniAsset.IsValid() && IsValid(SourceHoudiniAsset->HoudiniToolData)) + { + // Extract preset name, label from HoudiniToolData + PresetName = SourceHoudiniAsset->GetName(); + PresetLabel = FHoudiniToolsEditor::ResolveHoudiniAssetLabel(SourceHoudiniAsset.Get()); + PresetDescription = SourceHoudiniAsset->HoudiniToolData->ToolTip; + } + + const int32 NumParms = HC->GetNumParameters(); + bApplyBakeFolder = false; + bApplyTempCookFolder = false; + bApplyBakeOptions = true; + bApplyAssetOptions = true; + bApplyStaticMeshGenSettings = true; + bApplyProxyMeshGenSettings = true; + + TSharedRef Container = SNew(SVerticalBox); + auto CreateSplitterFn = [] () -> TSharedRef + { + return SNew(SSplitter) + + .Style(_GetEditorStyle(), "DetailsView.Splitter") + .PhysicalSplitterHandleSize(1.0f) + .HitDetectionSplitterHandleSize(5.0f); + // .HighlightedHandleIndex(ColumnSizeData.HoveredSplitterIndex) + // .OnHandleHovered(ColumnSizeData.OnSplitterHandleHovered); + }; + + const FMargin ContentPadding(4.f, 2.f); + + auto AddParameterItemRowFn = [this, Container, ContentPadding] ( + int Indentation, TSharedRef Splitter, FString Label, + const FString& Name, const FString& Tooltip, const FString& ValueStr) + { + FString Indent = FString::ChrN(Indentation * 8, ' '); + Label = Indent + Label; + + // Item Row + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 0.f, 0.f, 1.f) + [ + SNew(SBorder) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Panel")) + .Content() + [ + Splitter + ] + ]; + + // Item Label + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetLabelColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnLabelColumnResized) + [ + SNew(SBorder) + .Padding(ContentPadding) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Panel")) + .Content() + [ + SNew(SHorizontalBox) + + // Parameter Checkbox + + SHorizontalBox::Slot() + [ + SNew(SCheckBox) + .IsChecked_Lambda( [this, Name]() -> ECheckBoxState { return GetParameterCheckState(Name); } ) + .OnCheckStateChanged_Lambda( [this, Name](ECheckBoxState NewState) -> void { OnParameterCheckStateChanged(NewState, Name); bSelectAll = false; } ) + .ToolTipText( FText::FromString(Tooltip) ) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(FText::FromString(Label)) + ] + ] + ] + ]; + + // Item Name + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetNameColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnNameColumnResized) + [ + SNew(SBorder) + .Padding(ContentPadding) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Panel")) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(FText::FromString(Name)) + .ToolTipText(FText::FromString(Name)) + ] + ]; + + // Item Value + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetValueColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnValueColumnResized) + [ + SNew(SBorder) + .Padding(ContentPadding) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Panel")) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(FText::FromString(ValueStr)) + .ToolTipText(FText::FromString(ValueStr)) + ] + ]; + }; + + + auto AddInputItemRowFn = [this, Container, ContentPadding] ( + TSharedRef Splitter, const bool bIsParameterInput, const int32 InputIndex, + const FString& ParameterName, const FString& Label, const FString& Tooltip, + const FString& Type, const int32 NumObjects) + { + // Item Row + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 0.f, 0.f, 1.f) + [ + SNew(SBorder) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Panel")) + .Content() + [ + Splitter + ] + ]; + + // Input Label + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetLabelColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnLabelColumnResized) + [ + SNew(SBorder) + .Padding(ContentPadding) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Panel")) + .Content() + [ + SNew(SHorizontalBox) + + // Parameter Checkbox + + + SHorizontalBox::Slot() + [ + SNew(SCheckBox) + .IsChecked_Lambda( [this, bIsParameterInput, InputIndex, ParameterName]() -> ECheckBoxState { return bIsParameterInput ? GetParameterCheckState(ParameterName) : GetInputCheckState(InputIndex); } ) + .OnCheckStateChanged_Lambda( [this, bIsParameterInput, InputIndex, ParameterName](const ECheckBoxState NewState) -> void { if (bIsParameterInput) OnParameterCheckStateChanged(NewState, ParameterName); else OnInputCheckStateChanged(NewState, InputIndex); bSelectAll = false; } ) + // .ToolTipText( FText::FromString(Tooltip) ) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(FText::FromString(Label)) + .ToolTipText(FText::FromString(Tooltip)) + ] + ] + ] + ]; + + // Input Type + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetNameColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnNameColumnResized) + [ + SNew(SBorder) + .Padding(ContentPadding) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Panel")) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(FText::FromString(Type)) + .ToolTipText(FText::FromString(Type)) + ] + ]; + + // Number of Objects + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetValueColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnValueColumnResized) + [ + SNew(SBorder) + .Padding(ContentPadding) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Panel")) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(FText::FromString(FString::FromInt(NumObjects))) + .ToolTipText(FText::FromString(FString::FromInt(NumObjects))) + ] + ]; + }; + + // Preset settings + + // Grid panel for Preset properties + TSharedRef GridPanel = + SNew(SGridPanel) + .FillColumn(1, 1.0f); + + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 0.f, 0.f, 10.f) + [ + SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("HeadingExtraSmall")) + .Text( LOCTEXT("CreatePresetFromHDA_PresetPropertiesHeading", "Properties") ) + .ToolTipText( LOCTEXT("CreatePresetFromHDA_PresetPropertiesHeadingTooltip", "Properties that define the behaviour of the preset") ) + .TransformPolicy(ETextTransformPolicy::ToUpper) + ]; + + Container->AddSlot() + .AutoHeight() + [ + GridPanel + ]; + + // Preset - Name + int Row = 0; + GridPanel->AddSlot(0, Row) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreatePresetFromHDA_Name", "Asset Name")) + .ToolTipText(LOCTEXT("CreatePresetFromHDA_NameTooltip", "The name of the HoudiniPreset asset.")) + ]; + + GridPanel->AddSlot(1, Row) + .VAlign(VAlign_Center) + .Padding(0.f, 3.f) + [ + SNew(SEditableTextBox) + .Text_Lambda( [this]() -> FText { return FText::FromString(PresetName); } ) + .IsEnabled( IsAssetNameEnabled() ) + .OnTextChanged(this, &SHoudiniPresetUIBase::OnPresetNameChanged) + .OnTextCommitted(this, &SHoudiniPresetUIBase::OnPresetNameCommitted) + ]; + + // Preset - Label + + Row++; + GridPanel->AddSlot(0, Row) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreatePresetFromHDA_Label", "Label")) + .ToolTipText(LOCTEXT("CreatePresetFromHDA_LabelTooltip", "The label is the display name of the preset, typically seen in the UI.")) + ]; + + GridPanel->AddSlot(1, Row) + .VAlign(VAlign_Center) + .Padding(0.f, 3.f) + [ + SNew(SEditableTextBox) + .Text_Lambda( [this]() -> FText { return FText::FromString(PresetLabel); } ) + .OnTextChanged(this, &SHoudiniPresetUIBase::OnPresetLabelChanged) + .OnTextCommitted(this, &SHoudiniPresetUIBase::OnPresetLabelCommitted) + ]; + + // Preset - Description + + Row++; + GridPanel->AddSlot(0, Row) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreatePresetFromHDA_Label", "Description")) + .ToolTipText(LOCTEXT("CreatePresetFromHDA_LabelTooltip", "Description for the preset.")) + ]; + + GridPanel->AddSlot(1, Row) + .VAlign(VAlign_Center) + .Padding(0.f, 3.f) + [ + SNew(SEditableTextBox) + .Text_Lambda( [this]() -> FText { return FText::FromString(PresetDescription); } ) + .OnTextChanged(this, &SHoudiniPresetUIBase::OnPresetDescriptionChanged) + .OnTextCommitted(this, &SHoudiniPresetUIBase::OnPresetDescriptionCommitted) + ]; + + // Preset - SourceHoudiniAsset + Row++; + GridPanel->AddSlot(0, Row) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreatePresetFromHDA_Label", "Source Asset")) + .ToolTipText(LOCTEXT("CreatePresetFromHDA_LabelTooltip", "The source HoudiniAsset for this preset. This can be used to instantiate the preset directly or limit the preset to only be applied to this HDA.")) + ]; + + GridPanel->AddSlot(1, Row) + .VAlign(VAlign_Center) + .Padding(0.f, 3.f) + [ + SNew(SObjectPropertyEntryBox) + .AllowedClass(UHoudiniAsset::StaticClass()) + .EnableContentPicker(true) + .DisplayUseSelected(true) + .DisplayBrowse(true) + .DisplayThumbnail(true) + .ObjectPath(this, &SHoudiniPresetUIBase::GetSourceAssetPath) + .ThumbnailPool( UThumbnailManager::Get().GetSharedThumbnailPool() ) + .OnObjectChanged(this, &SHoudiniPresetUIBase::OnPresetAssetChanged) + ]; + + // Preset - Apply Only To Source + Row++; + GridPanel->AddSlot(0, Row) + .VAlign(VAlign_Center) + .ColumnSpan(2) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(SCheckBox) + .IsChecked(this, &SHoudiniPresetUIBase::GetApplyOnlyToSourceCheckState) + .OnCheckStateChanged(this, &SHoudiniPresetUIBase::OnApplyOnlyToSourceCheckStateChanged) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(LOCTEXT("CreatePresetFromHDA_ApplyOnlyToSource", "Apply Only To Source Asset")) + .ToolTipText(LOCTEXT("CreatePresetFromHDA_ApplyOnlyToSourceTooltip", "The preset can only be applied to HDA instances that match the SourceHoudiniAsset type.")) + ] + ]; + + // Preset - Can Be Instantiated + Row++; + GridPanel->AddSlot(0, Row) + .VAlign(VAlign_Center) + .ColumnSpan(2) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(SCheckBox) + .IsChecked(this, &SHoudiniPresetUIBase::GetCanBeInstantiatedCheckState) + .OnCheckStateChanged(this, &SHoudiniPresetUIBase::OnCanBeInstantiatedCheckStateChanged) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(LOCTEXT("CreatePresetFromHDA_CanBeInstantiated", "Can be instantiated")) + .ToolTipText(LOCTEXT("CreatePresetFromHDA_CanBeInstantiatedTooltip", "This allows the preset to be instantiated directly (instantiate the source asset and apply this preset).")) + ] + ]; + + // Preset - RevertHDAParameters + Row++; + GridPanel->AddSlot(0, Row) + .VAlign(VAlign_Center) + .ColumnSpan(2) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(SCheckBox) + .IsChecked(this, &SHoudiniPresetUIBase::GetRevertHDAParametersCheckState) + .OnCheckStateChanged(this, &SHoudiniPresetUIBase::OnRevertHDAParametersCheckStateChanged) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(LOCTEXT("CreatePresetFromHDA_RevertHDAParameters", "Revert HDA Parameters")) + .ToolTipText(LOCTEXT("CreatePresetFromHDA_RevertHDAParametersTooltip", "Revert all the HDA parameters before applying this preset.")) + ] + ]; + + + // Options + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 8.f, 0.f, 10.f) + [ + SNew(STextBlock) + .Font( FAppStyle::Get().GetFontStyle("HeadingExtraSmall") ) + .Text( LOCTEXT("CreatePresetFromHDA_PresetOptionsHeading", "Options") ) + .ToolTipText( LOCTEXT("CreatePresetFromHDA_PresetOptionsHeadingTooltip", "HDA generate, bake and asset options to include in this preset.") ) + .TransformPolicy(ETextTransformPolicy::ToUpper) + ]; + + { + // Bake Options + { + // Temp Cook Folder + TSharedPtr Splitter = CreateSplitterFn(); + + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 0.f, 0.f, 1.f) + [ + Splitter.ToSharedRef() + ]; + + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetLabelColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnLabelColumnResized) + [ + SNew(SCheckBox) + .IsChecked_Lambda( [this]() -> ECheckBoxState { return bApplyTempCookFolder ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } ) + .OnCheckStateChanged_Lambda( [this](const ECheckBoxState NewState) -> void { bApplyTempCookFolder = NewState == ECheckBoxState::Checked; } ) + // .ToolTipText( LOCTEXT("CreatePresetFromHDA_TempCookFolderTooltip", "Whether or not this preset will apply bake options.") ) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(LOCTEXT("CreatePresetFromHDA_TempCookFolderLabel", "Temporary Cook Folder")) + ] + ]; + + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetNameColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnNameColumnResized) + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text( FText::FromString(HC->GetTemporaryCookFolderOrDefault()) ) + ]; + + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetValueColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnValueColumnResized); + + // Bake Folder + Splitter = CreateSplitterFn(); + + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 0.f, 0.f, 1.f) + [ + Splitter.ToSharedRef() + ]; + + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetLabelColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnLabelColumnResized) + [ + SNew(SCheckBox) + .IsChecked_Lambda( [this]() -> ECheckBoxState { return bApplyBakeFolder ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } ) + .OnCheckStateChanged_Lambda( [this](const ECheckBoxState NewState) -> void { bApplyBakeFolder = NewState == ECheckBoxState::Checked; } ) + // .ToolTipText( LOCTEXT("CreatePresetFromHDA_TempCookFolderTooltip", "Whether or not this preset will apply bake options.") ) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(LOCTEXT("CreatePresetFromHDA_BakeFolderLabel", "Bake Folder")) + ] + ]; + + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetNameColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnNameColumnResized) + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text( FText::FromString(HC->GetBakeFolderOrDefault()) ) + ]; + + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetValueColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnValueColumnResized); + } + + + // Apply Bake Options + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 0.f, 0.f, 1.f) + [ + SNew(SCheckBox) + .IsChecked_Lambda( [this]() -> ECheckBoxState { return bApplyBakeOptions ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } ) + .OnCheckStateChanged_Lambda( [this](const ECheckBoxState NewState) -> void { bApplyBakeOptions = NewState == ECheckBoxState::Checked; } ) + .ToolTipText( LOCTEXT("CreatePresetFromHDA_ApplyBakeOptionsTooltip", "Whether or not this preset will apply bake options.") ) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(LOCTEXT("CreatePresetFromHDA_ApplyBakeOptionsTooltip", "Apply Bake Options")) + ] + ]; + + TSharedPtr BakeOptionsContainer = SNew(SVerticalBox) + .Visibility_Lambda([this]() -> EVisibility + { + return bApplyBakeOptions ? EVisibility::Visible : EVisibility::Collapsed; + }); + + } + + { + // Asset Options + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 0.f, 0.f, 1.f) + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() -> ECheckBoxState { return bApplyAssetOptions ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) + .OnCheckStateChanged_Lambda([this](const ECheckBoxState NewState) -> void { bApplyAssetOptions = NewState == ECheckBoxState::Checked; }) + .ToolTipText(LOCTEXT("CreatePresetFromHDA_ApplyAssetOptionsTooltip", "Whether or not this preset will apply asset options.")) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(LOCTEXT("CreatePresetFromHDA_ApplyAssetOptionsTooltip", "Apply Asset Options")) + ] + ]; + } + + + // Parameter List Header + + { + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 8.f, 0.f, 10.f) + [ + SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("HeadingExtraSmall")) + .Text( LOCTEXT("CreatePresetFromHDA_PresetParametersHeading", "Parameters") ) + .ToolTipText( LOCTEXT("CreatePresetFromHDA_PresetPropertiesHeadingTooltip", "HDA parameters that will be included in this preset.") ) + .TransformPolicy(ETextTransformPolicy::ToUpper) + ]; + + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 0.f, 0.f, 0.f) + [ + SNew(SCheckBox) + .IsChecked_Lambda( [this]() -> ECheckBoxState { return bSelectAll ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } ) + .OnCheckStateChanged_Lambda( [this](ECheckBoxState NewState) -> void + { + bSelectAll = NewState == ECheckBoxState::Checked; + if (bSelectAll) + { + SelectAllParameters(); + } + else + { + KeepParameters.Empty(); + } + } ) + .ToolTipText( LOCTEXT("CreatePresetFromHDA_SelectAllTooltip", "Select or deselect all parameters") ) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text( LOCTEXT("CreatePresetFromHDA_SelectAllTooltip", "Select All") ) + ] + ]; + + + // Header (transparent splitter) + TSharedRef Splitter = SNew(SSplitter) + .Style(_GetEditorStyle(), "PropertyTable.InViewport.Splitter") + .PhysicalSplitterHandleSize(1.0f) + .HitDetectionSplitterHandleSize(5.0f); + + // Header - Row + Container->AddSlot() + .Padding(FMargin(0.f, 5.f)) + .AutoHeight() + [ + SNew(SBorder) + .Padding(FMargin(4.f, 4.f, 4.f, 4.f)) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Header")) + .Content() + [ + Splitter + ] + ]; + + // Header - Label + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetLabelColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnLabelColumnResized) + [ + SNew(SBorder) + .Padding(ContentPadding) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Header")) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.BoldFont"))) + .Text(FText::FromString("Label")) + ] + ]; + + // Header - Name + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetNameColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnNameColumnResized) + [ + SNew(SBorder) + .Padding(ContentPadding) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Header")) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.BoldFont"))) + .Text(FText::FromString("Name")) + ] + ]; + + // Header - Value + Splitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetValueColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnValueColumnResized) + [ + SNew(SBorder) + .Padding(ContentPadding) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Header")) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.BoldFont"))) + .Text(FText::FromString("Value")) + ] + ]; + } + + // Construct of map of parms by Id for quick look up. + TMap IdToParam; + for (int Index = 0; Index < NumParms; Index++) + { + UHoudiniParameter* Param = HC->GetParameterAt(Index); + IdToParam.Add(Param->GetParmId(), Param); + } + + // List of parameter checkboxes + for (int i = 0; i < NumParms; i++) + { + UHoudiniParameter* Param = HC->GetParameterAt(i); + if (!IsValid(Param)) + { + continue; + } + + if (Param->GetParameterName() == TEXT("HAPI")) + { + continue; + } + + TSharedRef Splitter = CreateSplitterFn(); + + const FString ParamName = Param->GetParameterName(); + const FString ParamLabel = Param->GetParameterLabel(); + const FString ParamTooltip = Param->GetParameterHelp(); + + // By default we'll include all parameters for the preset + KeepParameters.Add(ParamName); + + FString ValueStr; + bool bValidRow = false; + + switch (Param->GetParameterType()) + { + // Float based parameters + case EHoudiniParameterType::Color: + bValidRow = FHoudiniPresetHelpers::GetParameterValues(Cast(Param), FloatValues, ValueStr); + break; + case EHoudiniParameterType::ColorRamp: + bValidRow = FHoudiniPresetHelpers::GetParameterValues(Cast(Param), RampColorValues, ValueStr); + break; + case EHoudiniParameterType::File: + case EHoudiniParameterType::FileDir: + case EHoudiniParameterType::FileGeo: + case EHoudiniParameterType::FileImage: + bValidRow = FHoudiniPresetHelpers::GetParameterValues(Cast(Param), StringValues, ValueStr); + break; + case EHoudiniParameterType::Float: + bValidRow = FHoudiniPresetHelpers::GetParameterValues(Cast(Param), FloatValues, ValueStr); + break; + case EHoudiniParameterType::FloatRamp: + bValidRow = FHoudiniPresetHelpers::GetParameterValues(Cast(Param), RampFloatValues, ValueStr); + break; + case EHoudiniParameterType::Input: + // This input will be processed during the Input Loop at the end + break; + case EHoudiniParameterType::Int: + bValidRow = FHoudiniPresetHelpers::GetParameterValues(Cast(Param), IntValues, ValueStr); + break; + case EHoudiniParameterType::IntChoice: + bValidRow = FHoudiniPresetHelpers::GetParameterValues(Cast(Param), IntValues, ValueStr); + break; + case EHoudiniParameterType::Toggle: + bValidRow = FHoudiniPresetHelpers::GetParameterValues(Cast(Param), IntValues, ValueStr); + break; + case EHoudiniParameterType::String: + case EHoudiniParameterType::StringAssetRef: + bValidRow = FHoudiniPresetHelpers::GetParameterValues(Cast(Param), StringValues, ValueStr); + break; + case EHoudiniParameterType::StringChoice: + bValidRow = FHoudiniPresetHelpers::GetParameterValues(Cast(Param), StringValues, ValueStr); + break; + case EHoudiniParameterType::MultiParm: + bValidRow = FHoudiniPresetHelpers::GetParameterValues(Cast(Param), MultiParmValues, ValueStr); + MultiParmChildren.Add(ParamName, {}); + break; + default: + bValidRow = false; + break; + } + + if (bValidRow) + { + // Calculate an appropriate indentation for multi-parms + int Indentation = 0; + auto * IndentParam = Param; + while(IndentParam->GetIsChildOfMultiParm()) + { + ++Indentation; + + int ParentId = IndentParam->GetParentParmId(); + + UHoudiniParameter ** Parent = IdToParam.Find(ParentId); + if (!Parent) + break; + + IndentParam = *Parent; + } + + AddParameterItemRowFn(Indentation, Splitter, ParamLabel, ParamName, ParamTooltip, ValueStr); + } + + if (Param->IsDirectChildOfMultiParm()) + { + // If this Parm is a child of a multiparm, keep track of it + int ParentId = Param->GetParentParmId(); + UHoudiniParameter** ParentParm = IdToParam.Find(ParentId); + if (ParentParm) + { + MultiParmChildren[(*ParentParm)->GetParameterName()].Add(Param->GetParameterName()); + MultiParmParent.Add(Param->GetParameterName(), (*ParentParm)->GetParameterName()); + } + } + + } // End for loop over parms + + // Inputs + { + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 8.f, 0.f, 10.f) + [ + SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("HeadingExtraSmall")) + .Text(LOCTEXT("CreatePresetFromHDA_PresetInputsHeading", "Inputs")) + .ToolTipText(LOCTEXT("CreatePresetFromHDA_PresetPropertiesHeadingTooltip", "HDA parameters that will be included in this preset.")) + .TransformPolicy(ETextTransformPolicy::ToUpper) + ]; + + // Input Column Header (transparent splitter) + TSharedRef HeaderSplitter = SNew(SSplitter) + .Style(_GetEditorStyle(), "PropertyTable.InViewport.Splitter") + .PhysicalSplitterHandleSize(1.0f) + .HitDetectionSplitterHandleSize(5.0f); + + // Header - Row + Container->AddSlot() + .Padding(FMargin(0.f, 5.f)) + .AutoHeight() + [ + SNew(SBorder) + .Padding(FMargin(4.f, 4.f, 4.f, 4.f)) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Header")) + .Content() + [ + HeaderSplitter + ] + ]; + + // Header - Label + HeaderSplitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetLabelColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnLabelColumnResized) + [ + SNew(SBorder) + .Padding(ContentPadding) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Header")) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.BoldFont"))) + .Text(FText::FromString("Label")) + ] + ]; + + // Header - Type + HeaderSplitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetNameColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnNameColumnResized) + [ + SNew(SBorder) + .Padding(ContentPadding) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Header")) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.BoldFont"))) + .Text(FText::FromString("Type")) + ] + ]; + + // Header - Number of Objects + HeaderSplitter->AddSlot() + .Value(this, &SHoudiniPresetUIBase::GetValueColumnWidth) + .OnSlotResized(this, &SHoudiniPresetUIBase::OnValueColumnResized) + [ + SNew(SBorder) + .Padding(ContentPadding) + .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) + .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Header")) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.BoldFont"))) + .Text(FText::FromString("Number of Objects")) + ] + ]; + + // Ingest hard wired inputs + const int32 NumInputs = HC->GetNumInputs(); + for (int32 i = 0; i < NumInputs; i++) + { + UHoudiniInput* Input = HC->GetInputAt(i); + if (!IsValid(Input)) + { + continue; + } + + if (!FHoudiniPresetHelpers::IsSupportedInputType(Input->GetInputType())) + { + continue; + } + + const bool bIsObjectPathParameter = Input->IsObjectPathParameter(); + FString ParameterName; + FString InputLabel; + FString InputTooltip; + int32 InputIndex = Input->GetInputIndex(); + if (bIsObjectPathParameter) + { + ParameterName = Input->GetInputName(); + UHoudiniParameter* Param = HC->FindParameterByName(ParameterName); + if (IsValid(Param)) + { + InputLabel = Param->GetParameterLabel(); + InputTooltip = FString::Format(TEXT("{0} ({1})"), { Param->GetParameterLabel(), ParameterName}); + } + } + else + { + InputLabel = FString::Format(TEXT("Input: {0}"), {Input->GetInputIndex()}); + InputTooltip = InputLabel; + } + + if (bIsObjectPathParameter) + { + // This input is a parameter-based input. Track it along with other inputs. + KeepParameters.Add(ParameterName); + } + else + { + // This is an absolute (hardwired) input. Track it by index. + KeepInputs.Add(InputIndex); + } + + const int32 NumObjects = Input->GetNumberOfInputObjects(); + FString InputTypeName = UHoudiniInput::InputTypeToString( Input->GetInputType() ); + FHoudiniPresetHelpers::GetGenericInput(Input, bIsObjectPathParameter, ParameterName, InputValues); + + TSharedRef Splitter = CreateSplitterFn(); + + AddInputItemRowFn(Splitter, bIsObjectPathParameter, InputIndex, ParameterName, InputLabel, InputTooltip, InputTypeName, NumObjects); + } + } + + // Mesh Generation Settings + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 8.f, 0.f, 10.f) + [ + SNew(STextBlock) + .Font( FAppStyle::Get().GetFontStyle("HeadingExtraSmall") ) + .Text( LOCTEXT("CreatePresetFromHDA_PresetMeshGenHeading", "Mesh Generation") ) + .ToolTipText( LOCTEXT("CreatePresetFromHDA_PresetMeshGenHeadingTooltip", "Mesh generation settings to include in this preset.") ) + .TransformPolicy(ETextTransformPolicy::ToUpper) + ]; + + { + // Houdini Mesh Generation + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 0.f, 0.f, 1.f) + [ + SNew(SCheckBox) + .IsChecked_Lambda( [this]() -> ECheckBoxState { return bApplyStaticMeshGenSettings ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } ) + .OnCheckStateChanged_Lambda( [this](const ECheckBoxState NewState) -> void { bApplyStaticMeshGenSettings = NewState == ECheckBoxState::Checked; } ) + .ToolTipText( LOCTEXT("CreatePresetFromHDA_ApplyMeshGenSettingsTooltip", "Whether or not this preset will apply Mesh Generation settings.") ) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(LOCTEXT("CreatePresetFromHDA_ApplyMeshGenSettingsTooltip", "Apply Mesh Generation Settings")) + ] + ]; + + // Houdini Proxy Mesh Generation + Container->AddSlot() + .AutoHeight() + .Padding(0.f, 0.f, 0.f, 1.f) + [ + SNew(SCheckBox) + .IsChecked_Lambda( [this]() -> ECheckBoxState { return bApplyProxyMeshGenSettings ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } ) + .OnCheckStateChanged_Lambda( [this](const ECheckBoxState NewState) -> void { bApplyProxyMeshGenSettings = NewState == ECheckBoxState::Checked; } ) + .ToolTipText( LOCTEXT("CreatePresetFromHDA_ApplyProxyMeshGenSettingsTooltip", "Whether or not this preset will apply Proxy Mesh Generation settings.") ) + .Content() + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Text(LOCTEXT("CreatePresetFromHDA_ApplyProxyMeshGenSettingsTooltip", "Apply Proxy Mesh Generation Settings")) + ] + ]; + } + + // Main Panel Structure + TSharedPtr ActionButtons = CreateActionButtonsRow(); + TSharedPtr DescriptionLinesContainer = SNew(SVerticalBox); + + for(const FText& Line : GetDescriptionLines()) + { + DescriptionLinesContainer->AddSlot() + .AutoHeight() + [ + SNew(STextBlock) + .AutoWrapText(true) + .Text(Line) + ]; + } + + ChildSlot + [ + SNew(SBorder) + .Padding(18.0f) + .BorderImage( _GetBrush("Docking.Tab.ContentAreaBrush") ) + [ + SNew(SBorder) + .BorderImage(FAppStyle::Get().GetBrush("Brushes.Panel")) + .Padding(10.0f) + [ + SNew(SVerticalBox) + + // ----------------------------- + // Dialog Title + // ----------------------------- + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + .AutoHeight() + [ + SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("HeadingExtraSmall")) + .Text( GetDialogTitle() ) + .TransformPolicy(ETextTransformPolicy::ToUpper) + ] + + // ----------------------------- + // Description + // ----------------------------- + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 10.0f) + [ + DescriptionLinesContainer.ToSharedRef() + ] + + // ----------------------------- + // Error Box + // ----------------------------- + +SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 5.0f) + [ + SNew(SWarningOrErrorBox) + .MessageStyle(EMessageStyle::Error) + .Visibility(this, &SHoudiniPresetUIBase::GetErrorVisibility) + .Message(this, &SHoudiniPresetUIBase::GetErrorText) + ] + + // Scrollable properties / parameters container + +SVerticalBox::Slot() + .FillHeight(1.0f) + [ + SNew(SScrollBox) + +SScrollBox::Slot() + [ + Container + ] + ] + + // Status Bar + + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SGridPanel) + + // Preset Location + + +SGridPanel::Slot(0,0) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreatePresetFromHDA_LocationLabel", "Location")) + .ToolTipText(LOCTEXT("CreatePresetFromHDA_LocationTooltip", "Location of the resulting preset asset.")) + ] + +SGridPanel::Slot(1, 0) + .VAlign(VAlign_Center) + .Padding(0.f, 5.0f) + [ + SNew(STextBlock) + .Text(this, &SHoudiniPresetUIBase::GetPresetAssetPathText) + ] + ] + + // Action Bar + + +SVerticalBox::Slot() + .AutoHeight() + [ + // Action Buttons Row + ActionButtons.IsValid() ? ActionButtons.ToSharedRef() : CreateEmptyActionButtonsRow() + ] + ] + ] + ]; + + PostConstruct(); +} + +TSharedRef SHoudiniPresetUIBase::CreateEmptyActionButtonsRow() +{ + return SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .HAlign(HAlign_Right) + [ + // Create Preset Button + + SNew(SButton) + .ButtonStyle(&_GetEditorStyle().GetWidgetStyle("PrimaryButton")) + .IsEnabled(false) + .Visibility(EVisibility::Hidden) + .Content() + [ + SNew(STextBlock) + .TextStyle( &_GetEditorStyle().GetWidgetStyle("PrimaryButtonText") ) + .Text(FText::FromString(TEXT("DummyText"))) + ] + ]; +} + + +TSharedRef +SHoudiniPresetUIBase::CreateFloatingWindow( + const FText& WindowTitle, + const FVector2D InitialSize) +{ + TSharedRef NewSlateWindow = SNew(SWindow) + .Title(WindowTitle) + .ClientSize(InitialSize) + .SizingRule( ESizingRule::UserSized ) + .SupportsMaximize(false) + .SupportsMinimize(false); + + // If the main frame exists parent the window to it + TSharedPtr< SWindow > ParentWindow; + if (FModuleManager::Get().IsModuleLoaded("MainFrame")) + { + IMainFrameModule& MainFrame = FModuleManager::GetModuleChecked("MainFrame"); + ParentWindow = MainFrame.GetParentWindow(); + } + + if (ParentWindow.IsValid()) + { + // Parent the window to the main frame + FSlateApplication::Get().AddWindowAsNativeChild( NewSlateWindow, ParentWindow.ToSharedRef() ); + } + else + { + FSlateApplication::Get().AddWindow( NewSlateWindow ); + } + + FDetailsViewArgs Args; + Args.bHideSelectionTip = true; + Args.bLockable = false; + Args.bAllowMultipleTopLevelObjects = true; + Args.ViewIdentifier = FName(WindowTitle.ToString()); + Args.NameAreaSettings = FDetailsViewArgs::HideNameArea; + Args.bShowPropertyMatrixButton = false; + Args.bShowOptions = false; + Args.bShowModifiedPropertiesOption = false; + Args.bShowObjectLabel = false; + + return NewSlateWindow; +} + +ECheckBoxState +SHoudiniPresetUIBase::GetParameterCheckState(const FString& ParamName) const +{ + if (KeepParameters.Contains(ParamName)) + { + return ECheckBoxState::Checked; + } + return ECheckBoxState::Unchecked; +} + +void +SHoudiniPresetUIBase::OnParameterCheckStateChanged(ECheckBoxState CheckState, const FString& ParamName) +{ + if (CheckState == ECheckBoxState::Checked) + { + KeepParameters.Add(ParamName); + + // If a child of a multi-parm is activated, make sure all parents are activated. + FString * ParentName = MultiParmParent.Find(ParamName); + while(ParentName) + { + KeepParameters.Add(*ParentName); + ParentName = MultiParmParent.Find(*ParentName); + } + } + else + { + KeepParameters.Remove(ParamName); + } + + // If the parent of a multi-parm is changed, change all its children. + TArray ParamsToProcess; + ParamsToProcess.Add(ParamName); + + while(!ParamsToProcess.IsEmpty()) + { + FString ParentName = ParamsToProcess.Pop(); + if (auto* Children = MultiParmChildren.Find(ParentName)) + { + for (const FString& Child : *Children) + { + ParamsToProcess.Add(Child); + if (CheckState == ECheckBoxState::Checked) + KeepParameters.Add(Child); + else + KeepParameters.Remove(Child); + } + } + } +} + +ECheckBoxState +SHoudiniPresetUIBase::GetInputCheckState(const int32 InputIndex) const +{ + if (KeepInputs.Contains(InputIndex)) + { + return ECheckBoxState::Checked; + } + return ECheckBoxState::Unchecked; +} + +void +SHoudiniPresetUIBase::OnInputCheckStateChanged(ECheckBoxState CheckState, const int32 InputIndex) +{ + if (CheckState == ECheckBoxState::Checked) + { + KeepInputs.Add(InputIndex); + } + else + { + KeepInputs.Remove(InputIndex); + } +} + +FString +SHoudiniPresetUIBase::GetSourceAssetPath() const +{ + return SourceHoudiniAsset.IsValid() ? SourceHoudiniAsset->GetPathName() : FString(""); +} + +void +SHoudiniPresetUIBase::OnPresetAssetChanged(const FAssetData& AssetData) +{ + SourceHoudiniAsset = Cast(AssetData.GetAsset()); +} + +FString +SHoudiniPresetUIBase::GetPresetAssetBasePath() const +{ + FString PresetBasePath; + + if (HoudiniCookable.IsValid() && IsValid(HoudiniCookable->GetHoudiniAsset())) + { + return FPaths::Combine(FPaths::GetPath(HoudiniCookable->GetHoudiniAsset()->GetPathName()), TEXT("Presets")); + } + + return FString(); +} + +FString +SHoudiniPresetUIBase::GetPresetAssetPath() const +{ + FString PresetBasePath = GetPresetAssetBasePath(); + + if (PresetBasePath.IsEmpty()) + { + return FString(); + } + + return FPaths::Combine(PresetBasePath, PresetName); +} + +bool +SHoudiniPresetUIBase::IsValidPresetPath() +{ + if (!FHoudiniToolsEditor::IsValidPackageName(PresetName, &PresetNameError)) + { + return false; + } + + return true; +} + +EVisibility +SHoudiniPresetUIBase::GetErrorVisibility() const +{ + if (!bIsValidPresetPath) + { + return EVisibility::Visible; + } + + return EVisibility::Hidden; +} + +FText +SHoudiniPresetUIBase::GetErrorText() const +{ + if (!bIsValidPresetPath) + { + return PresetNameError; + } + + return FText(); +} + +void +SHoudiniPresetUIBase::SelectAllParameters() +{ + if (!HoudiniCookable.IsValid()) + return; + + UHoudiniCookable* HC = HoudiniCookable.Get(); + const int32 NumParams = HC->GetNumParameters(); + for (int i = 0; i < NumParams; i++) + { + UHoudiniParameter* Param = HC->GetParameterAt(i); + if (!IsValid(Param)) + { + continue; + } + KeepParameters.Add(Param->GetParameterName()); + } +} + +void +SHoudiniPresetUIBase::PopulateAssetFromUI(UHoudiniPreset* Preset) +{ + if (!IsValid(Preset)) + { + HOUDINI_LOG_ERROR(TEXT("Error populating Preset from UI. Invalid UHoudiniPreset asset.")); + return; + } + + if (!HoudiniCookable.IsValid()) + { + HOUDINI_LOG_ERROR(TEXT("Error populating Preset from UI. Invalid Houdini Preset Cookable asset.")); + return; + } + + const UHoudiniCookable* HC = HoudiniCookable.Get(); + + Preset->Modify(); + + // Populate the preset asset + Preset->Name = PresetLabel; + Preset->Description = PresetDescription; + Preset->SourceHoudiniAsset = SourceHoudiniAsset.Get(); + Preset->bApplyOnlyToSource = bApplyOnlyToSource; + Preset->bCanInstantiate = bCanInstantiate; + Preset->bRevertHDAParameters = bRevertHDAParameters; + + Preset->bApplyTemporaryCookFolder = bApplyTempCookFolder; + Preset->TemporaryCookFolder = HC->GetTemporaryCookFolderOrDefault(); + Preset->bApplyBakeFolder = bApplyBakeFolder; + Preset->BakeFolder = HC->GetBakeFolderOrDefault(); + + FHoudiniToolsEditor::CopySettingsToPreset( + HC, bApplyAssetOptions, bApplyBakeOptions, bApplyStaticMeshGenSettings, bApplyProxyMeshGenSettings, Preset); + + // Transfer int params that we want to keep (checked by the user) + Preset->IntParameters.Empty(); + for (auto& Entry : IntValues) + { + if (KeepParameters.Contains(Entry.Key)) + { + Preset->IntParameters.Add(Entry.Key, Entry.Value); + } + } + + // Transfer float parameters + Preset->FloatParameters.Empty(); + for (auto& Entry : FloatValues) + { + if (KeepParameters.Contains(Entry.Key)) + { + Preset->FloatParameters.Add(Entry.Key, Entry.Value); + } + } + + // Transfer string parameters + Preset->StringParameters.Empty(); + for (auto& Entry : StringValues) + { + if (KeepParameters.Contains(Entry.Key)) + { + Preset->StringParameters.Add(Entry.Key, Entry.Value); + } + } + + // Transform Ramp (float) parameters + Preset->RampFloatParameters.Empty(); + for (auto& Entry : RampFloatValues) + { + if (KeepParameters.Contains(Entry.Key)) + { + Preset->RampFloatParameters.Add(Entry.Key, Entry.Value); + } + } + + // Transform Ramp (color) parameters + Preset->RampColorParameters.Empty(); + for (auto& Entry : RampColorValues) + { + if (KeepParameters.Contains(Entry.Key)) + { + Preset->RampColorParameters.Add(Entry.Key, Entry.Value); + } + } + + // Transform multiparam parameters + Preset->MultiParmParameters.Empty(); + for (auto& Entry : MultiParmValues) + { + if (KeepParameters.Contains(Entry.Key)) + { + Preset->MultiParmParameters.Add(Entry.Key, Entry.Value); + } + } + + // Transfer input parameters + Preset->InputParameters.Empty(); + for (int32 InputIndex = 0; InputIndex < InputValues.Num(); ++InputIndex) + { + auto& Entry = InputValues[InputIndex]; + if (Entry.bIsParameterInput) + { + if (KeepParameters.Contains(Entry.ParameterName)) + { + Preset->InputParameters.Add(Entry); + } + } + else + { + if (KeepInputs.Contains(Entry.InputIndex)) + { + Preset->InputParameters.Add(Entry); + } + } + } + + // Copy the icon from the source houdini asset, if possible + if (IsValid(Preset->SourceHoudiniAsset)) + { + const UHoudiniToolData* ToolData = Preset->SourceHoudiniAsset->HoudiniToolData; + if (IsValid(ToolData)) + { + Preset->IconImageData = ToolData->IconImageData; + FHoudiniToolsRuntimeUtils::UpdateAssetThumbnailFromImageData(Preset, Preset->IconImageData); + } + } + + Preset->MarkPackageDirty(); +} + + +FReply +SHoudiniPresetUIBase::HandleCancelClicked() +{ + CloseWindow(); + + return FReply::Handled(); +} + + +void +SHoudiniPresetUIBase::CloseWindow() +{ + TSharedPtr ContainingWindow = FSlateApplication::Get().FindWidgetWindow(AsShared()); + + if ( ContainingWindow.IsValid() ) + { + ContainingWindow->RequestDestroyWindow(); + } +} + + +void +SHoudiniCreatePresetFromHDA::CreateDialog(TWeakObjectPtr HC) +{ + const FText Title = LOCTEXT("CreatePreset_WindowTitle", "Create Preset"); + const TSharedRef Window = CreateFloatingWindow(Title, FVector2D(750, 600)); + + Window->SetContent( + SNew(SHoudiniCreatePresetFromHDA) + .HoudiniCookable(HC) + ); +} + + +TSharedPtr +SHoudiniCreatePresetFromHDA::CreateActionButtonsRow() +{ + return SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .HAlign(HAlign_Right) + [ + // Create Preset Button + + SNew(SButton) + .ButtonStyle(&_GetEditorStyle().GetWidgetStyle("PrimaryButton")) + .OnClicked(this, &SHoudiniCreatePresetFromHDA::HandleCreatePresetClicked) + .IsEnabled_Lambda([this]()->bool { return bIsValidPresetPath; }) + .Content() + [ + SNew(STextBlock) + .TextStyle( &_GetEditorStyle().GetWidgetStyle("PrimaryButtonText") ) + .Text(LOCTEXT("CreatePresetFromHDA_CreatePreset", "Create Preset")) + ] + ]; +} + +FText +SHoudiniCreatePresetFromHDA::GetDialogTitle() +{ + return LOCTEXT( "HoudiniTool_CreatePackageTitle", "Create Houdini Tool Preset " ); +} + +TArray +SHoudiniCreatePresetFromHDA::GetDescriptionLines() +{ + return + { + LOCTEXT("HoudiniTool_CreatePackageDescription", "Enter a name for your new Preset asset. Asset names may only contain alphanumeric characters, and may not contain a space."), + LOCTEXT("HoudiniTool_CreatePackageDescription2", "When you click the Create Preset button, a new Preset asset will created at the displayed location.") + }; +} + + +FReply +SHoudiniCreatePresetFromHDA::HandleCreatePresetClicked() +{ + if (!HoudiniCookable.IsValid()) + { + return FReply::Handled(); + } + //const UHoudiniCookable* HC = HoudiniCookable.Get(); + + const FString PresetBasePath = GetPresetAssetBasePath(); + UHoudiniPresetFactory* Factory = NewObject(); + + const FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + UHoudiniPreset* Asset = Cast(AssetToolsModule.Get().CreateAsset( + PresetName, PresetBasePath, + UHoudiniPreset::StaticClass(), Factory, FName("CreateNewHoudiniToolsPreset"))); + + if (!Asset) + { + return FReply::Handled(); + } + + PopulateAssetFromUI(Asset); + + CloseWindow(); + + FHoudiniToolsEditor::BrowseToObjectInContentBrowser(Asset); + return FReply::Handled(); +} + + +void +SHoudiniUpdatePresetFromHDA::CreateDialog(TWeakObjectPtr HC) +{ + const FText Title = LOCTEXT("UpdatePreset_WindowTitle", "Update Preset"); + const TSharedRef Window = CreateFloatingWindow(Title, FVector2D(750, 600)); + + Window->SetContent( + SNew(SHoudiniUpdatePresetFromHDA) + .HoudiniCookable(HC)); +} + + +FReply +SHoudiniUpdatePresetFromHDA::HandleUpdatePresetClicked() +{ + if (!Preset.IsValid()) + { + HOUDINI_LOG_ERROR(TEXT("Preset asset is invalid. Cannot update.")); + return FReply::Handled(); + } + + PopulateAssetFromUI(Preset.Get()); + + CloseWindow(); + + return FReply::Handled(); +} + + +TSharedPtr +SHoudiniUpdatePresetFromHDA::CreateActionButtonsRow() +{ + return SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .HAlign(HAlign_Right) + [ + // Create Preset Button + SNew(SButton) + .ButtonStyle(&_GetEditorStyle().GetWidgetStyle("PrimaryButton")) + .OnClicked(this, &SHoudiniUpdatePresetFromHDA::HandleUpdatePresetClicked) + .IsEnabled_Lambda([this]()->bool { return bIsValidPresetPath; }) + .Content() + [ + SNew(STextBlock) + .TextStyle( &_GetEditorStyle().GetWidgetStyle("PrimaryButtonText") ) + .Text(LOCTEXT("UpdatePreset_UpdatePresetButtonText", "Update Preset")) + ] + ]; +} + + +void +SHoudiniUpdatePresetFromHDA::PostConstruct() +{ + SHoudiniPresetUIBase::PostConstruct(); + + Preset.Reset(); + + // Get the selected asset that needs to be updated + TArray SelectedAssets; + AssetSelectionUtils::GetSelectedAssets(SelectedAssets); + if (SelectedAssets.Num() != 1) + { + HOUDINI_LOG_ERROR(TEXT("Error constructing Update Preset window. Only one HoudiniPreset asset may be selected.")); + CloseWindow(); + return; + } + + const FAssetData& AssetData = SelectedAssets[0]; + UHoudiniPreset* SelectedPreset = Cast(AssetData.GetAsset()); + if (!IsValid(SelectedPreset)) + { + HOUDINI_LOG_ERROR(TEXT("Error constructing Update Preset window. A valid HoudiniPreset asset must be selected.")); + CloseWindow(); + return; + } + + Preset = SelectedPreset; + + // Only select the parameters that are already preset in the HoudiniPreset asset. + + bSelectAll = false; + KeepParameters.Empty(); + TArray Keys; + + SelectedPreset->FloatParameters.GetKeys(Keys); + KeepParameters.Append( Keys ); + Keys.Empty(); + + SelectedPreset->IntParameters.GetKeys(Keys); + KeepParameters.Append( Keys ); + Keys.Empty(); + + SelectedPreset->StringParameters.GetKeys(Keys); + KeepParameters.Append( Keys ); + Keys.Empty(); + + SelectedPreset->RampFloatParameters.GetKeys(Keys); + KeepParameters.Append( Keys ); + Keys.Empty(); + + SelectedPreset->RampColorParameters.GetKeys(Keys); + KeepParameters.Append( Keys ); + Keys.Empty(); + + SelectedPreset->MultiParmParameters.GetKeys(Keys); + KeepParameters.Append(Keys); + Keys.Empty(); + + KeepInputs.Empty(); + for(const FHoudiniPresetInputValue& Entry : SelectedPreset->InputParameters) + { + if (Entry.bIsParameterInput) + { + KeepParameters.Add(Entry.ParameterName); + } + else + { + KeepInputs.Add( Entry.InputIndex ); + } + } + + // Sync other parameters from the preset to the UI + bApplyOnlyToSource = SelectedPreset->bApplyOnlyToSource; + bRevertHDAParameters = SelectedPreset->bRevertHDAParameters; + bCanInstantiate = SelectedPreset->bCanInstantiate; + + bApplyTempCookFolder = SelectedPreset->bApplyTemporaryCookFolder; + bApplyBakeFolder = SelectedPreset->bApplyBakeFolder; + bApplyBakeOptions = SelectedPreset->bApplyBakeOptions; + bApplyAssetOptions = SelectedPreset->bApplyAssetOptions; + bApplyStaticMeshGenSettings = SelectedPreset->bApplyStaticMeshGenSettings; + bApplyProxyMeshGenSettings = SelectedPreset->bApplyProxyMeshGenSettings; + + PresetLabel = SelectedPreset->Name; + PresetDescription = SelectedPreset->Description; + SourceHoudiniAsset = SelectedPreset->SourceHoudiniAsset; +} + + +FText +SHoudiniUpdatePresetFromHDA::GetDialogTitle() +{ + return LOCTEXT( "HoudiniTool_UpdatePackageTitle", "Update Houdini Tool Preset " ); +} + + +TArray +SHoudiniUpdatePresetFromHDA::GetDescriptionLines() +{ + return + { + LOCTEXT("HoudiniTool_UpdatePackageDescription1", "Options and selected parameters have been populated from the selected preset. Review all parameters to ensure the desired parameters have been selected."), + LOCTEXT("HoudiniTool_UpdatePackageDescription2", "When you click the Update Preset button, the preset will be updated to match the options and selected parameters from this dialog.") + }; +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniPresets.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniPreset.h" + +class SHoudiniPresetUIBase : public SCompoundWidget //, public FNotifyHook +{ +public: + SLATE_BEGIN_ARGS( SHoudiniPresetUIBase ) + : _HoudiniCookable(nullptr) + {} + SLATE_ATTRIBUTE(TWeakObjectPtr, HoudiniCookable) + SLATE_END_ARGS(); + + SHoudiniPresetUIBase(); + + + + void Construct( const FArguments& InArgs ); + + // Post construct handler that can be overridden by child classes + virtual void PostConstruct() {}; + + TSharedRef CreateEmptyActionButtonsRow(); + + // Create action buttons for the Preset user interface. Should be implemented by child classes. + virtual TSharedPtr CreateActionButtonsRow() { return nullptr; }; + +protected: + static TSharedRef CreateFloatingWindow( + const FText& WindowTitle, + const FVector2D InitialSize=FVector2D(400, 550) + ); + + virtual FText GetDialogTitle() { return FText(); } + virtual TArray GetDescriptionLines() { return {}; } + virtual bool IsAssetNameEnabled() { return true; } + + void OnLabelColumnResized(float NewSize) + { + const float TotalSize = LabelColumnWidth + NameColumnWidth; + LabelColumnWidth = NewSize; + // NameColumnWidth = TotalSize - LabelColumnWidth; // New width of the name column + } + + void OnNameColumnResized(float NewSize) + { + NameColumnWidth = NewSize; + } + + void OnValueColumnResized(float NewSize) + { + ValueColumnWidth = NewSize; + // NameColumnWidth = 1.0f - NewSize; + } + + float GetLabelColumnWidth() const + { + return LabelColumnWidth; + } + + float GetNameColumnWidth() const + { + return NameColumnWidth; + } + + float GetValueColumnWidth() const + { + return ValueColumnWidth; + // return 1.0f - NameColumnWidth - LabelColumnWidth; + } + + ECheckBoxState GetParameterCheckState(const FString& ParamName) const; + void OnParameterCheckStateChanged(ECheckBoxState CheckState, const FString& ParamName); + + ECheckBoxState GetInputCheckState(const int32 InputIndex) const; + void OnInputCheckStateChanged(ECheckBoxState CheckState, const int32 InputIndex); + + void OnPresetNameChanged(const FText& NewText) { PresetName = NewText.ToString(); bIsValidPresetPath = IsValidPresetPath(); } + void OnPresetNameCommitted(const FText& NewText, ETextCommit::Type CommitMethod) { PresetName = NewText.ToString(); bIsValidPresetPath = IsValidPresetPath(); } + + void OnPresetLabelChanged(const FText& NewText) { PresetLabel = NewText.ToString(); } + void OnPresetLabelCommitted(const FText& NewText, ETextCommit::Type CommitMethod) { PresetLabel = NewText.ToString(); } + + void OnPresetDescriptionChanged(const FText& NewText) { PresetDescription = NewText.ToString(); } + void OnPresetDescriptionCommitted(const FText& NewText, ETextCommit::Type CommitMethod) { PresetDescription = NewText.ToString(); } + + ECheckBoxState GetApplyOnlyToSourceCheckState() const { return bApplyOnlyToSource ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } + void OnApplyOnlyToSourceCheckStateChanged(ECheckBoxState NewState) { bApplyOnlyToSource = NewState == ECheckBoxState::Checked; } + + ECheckBoxState GetCanBeInstantiatedCheckState() const { return bCanInstantiate ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } + void OnCanBeInstantiatedCheckStateChanged(ECheckBoxState NewState) { bCanInstantiate = NewState == ECheckBoxState::Checked; } + + ECheckBoxState GetRevertHDAParametersCheckState() const { return bRevertHDAParameters ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } + void OnRevertHDAParametersCheckStateChanged(ECheckBoxState NewState) { bRevertHDAParameters = NewState == ECheckBoxState::Checked; } + + FString GetSourceAssetPath() const; + void OnPresetAssetChanged(const FAssetData& AssetData); + + FString GetPresetAssetBasePath() const; + FString GetPresetAssetPath() const; + FText GetPresetAssetPathText() const { return FText::FromString(GetPresetAssetPath()); } + bool IsValidPresetPath(); + + virtual EVisibility GetErrorVisibility() const; + FText GetErrorText() const; + + void SelectAllParameters(); + + void PopulateAssetFromUI(UHoudiniPreset* Preset); + + FReply HandleCancelClicked(); + + void CloseWindow(); + + + // Houdini Asset components from which we'll be creating a preset + TWeakObjectPtr HoudiniCookable; + + FString PresetName; + FString PresetLabel; + FString PresetDescription; + TWeakObjectPtr SourceHoudiniAsset; + + bool bIsValidPresetPath; + + bool bApplyOnlyToSource; + bool bRevertHDAParameters; + bool bCanInstantiate; + + bool bApplyTempCookFolder; + bool bApplyBakeFolder; + bool bApplyBakeOptions; + bool bApplyAssetOptions; + bool bApplyStaticMeshGenSettings; + bool bApplyProxyMeshGenSettings; + + FText PresetNameError; + + float LabelColumnWidth; + float NameColumnWidth; + float ValueColumnWidth; + + // Parameters, inputs and their values that should be kept for this preset + bool bSelectAll; + TSet KeepParameters; + TMap> MultiParmChildren; + TMap MultiParmParent; + TSet KeepInputs; + + TMap FloatValues; + TMap IntValues; + TMap StringValues; + TMap RampFloatValues; + TMap RampColorValues; + TMap MultiParmValues; + + TArray InputValues; + +}; + + +class SHoudiniCreatePresetFromHDA : public SHoudiniPresetUIBase +{ +public: + // Create a dialog to create a preset from the given Cookable + static void CreateDialog(TWeakObjectPtr HC); + +protected: + virtual TSharedPtr CreateActionButtonsRow() override; + + virtual FText GetDialogTitle() override; + virtual TArray GetDescriptionLines() override; + virtual bool IsAssetNameEnabled() override { return true; } + + FReply HandleCreatePresetClicked(); +}; + + +class SHoudiniUpdatePresetFromHDA : public SHoudiniPresetUIBase +{ +public: + // Create a dialog to update the selected preset (in the content browser) from the given Cookable + static void CreateDialog(TWeakObjectPtr HC); + +protected: + TWeakObjectPtr Preset; + + FReply HandleUpdatePresetClicked(); + + virtual TSharedPtr CreateActionButtonsRow() override; + + virtual void PostConstruct() override; + + virtual EVisibility GetErrorVisibility() const override { return EVisibility::Collapsed; }; + virtual FText GetDialogTitle() override; + virtual TArray GetDescriptionLines() override; + virtual bool IsAssetNameEnabled() override { return false; } +}; +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniRampBase.h`: + +```h +/* +* Copyright (c) <2024> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#include "HoudiniEngine.h" +#include "HoudiniEngineDetails.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineRuntimeCommon.h" +#include "HoudiniEngineUtils.h" + +#include "HoudiniParameterRamp.h" + +#include "HoudiniRampView.h" +#include "HoudiniRampEditorWindowSubsystem.h" + +#include "Widgets/Input/SNumericEntryBox.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SUniformGridPanel.h" +#include "PropertyCustomizationHelpers.h" +#include "HoudiniEngineEditorPrivatePCH.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +class IHoudiniRampCurveEditor +{ +public: + + /** Called when a ramp editor widget or window is being refreshed. */ + virtual void RefreshCurveKeys() = 0; +}; + +template +class SHoudiniRampCurveEditorBase : public Base, public IHoudiniRampCurveEditor +{ +public: + + using RampViewType = InRampViewType; + using ValueType = typename RampViewType::ValueType; + using ParameterType = typename RampViewType::ParameterType; + using ParameterWeakPtr = typename RampViewType::ParameterWeakPtr; + using PointType = typename RampViewType::PointType; + + DECLARE_DELEGATE(FOnCurveChanged); + +protected: + + TSharedPtr RampView; + + /** Delegate to call when the curve is changed */ + FOnCurveChanged OnCurveChangedDelegate; + + /** + * Get data about the curve used by the editor. + * + * @return Unset optional is only returned if and only if the curve is invalid or if the index + * is out of bounds. + */ + virtual TOptional GetNumCurveKeys() const = 0; + virtual TOptional GetCurveKeyPosition(const int32 Index) const = 0; + virtual TOptional GetCurveKeyValue(const int32 Index) const = 0; + virtual TOptional GetCurveKeyInterpolationType( + const int32 Index) const = 0; + + void OnCurveChanged() + { + if (!RampView.IsValid()) + { + return; + } + + const int32 NumPoints = RampView->GetPointCount(); + const TOptional NumCurveKeys = GetNumCurveKeys(); + + if (!NumCurveKeys.IsSet()) + { + return; + } + + // Note! The EPropertyChangeType is (unintuitively) always EPropertyChangeType::ValueSet + const bool bIsAddingPoints = NumCurveKeys.GetValue() > NumPoints; + const bool bIsDeletingPoints = NumCurveKeys.GetValue() < NumPoints; + + TArray ModifiedIndices; + TArray> NewPositions; + TArray> NewValues; + TArray> NewInterpolationTypes; + + // Find what points were changed... + { + int32 PointIndex = 0; + int32 CurveIndex = 0; + + while (PointIndex < NumPoints && CurveIndex < NumCurveKeys.GetValue()) + { + const float PointPosition = RampView->GetRampPointPosition(PointIndex).GetValue(); + const ValueType PointValue = RampView->GetRampPointValue(PointIndex).GetValue(); + const EHoudiniRampInterpolationType PointInterpolationType = + RampView->GetRampPointInterpolationType(PointIndex).GetValue(); + + const float CurvePosition = GetCurveKeyPosition(CurveIndex).GetValue(); + const ValueType CurveValue = GetCurveKeyValue(CurveIndex).GetValue(); + const ERichCurveInterpMode CurveInterpolationType = + GetCurveKeyInterpolationType(CurveIndex).GetValue(); + + const bool bIsInterpolationEquivalent = + IsInterpolationEquivalent(CurveInterpolationType, PointInterpolationType); + + if (CurvePosition == PointPosition + && CurveValue == PointValue + && bIsInterpolationEquivalent) + { + ++PointIndex; + ++CurveIndex; + } + else // We found a difference! + { + if (bIsAddingPoints) + { + ModifiedIndices.Add(PointIndex); + NewPositions.Add(CurvePosition); + NewValues.Add(CurveValue); + NewInterpolationTypes.Add(TranslateInterpolation(CurveInterpolationType)); + ++CurveIndex; + } + else if (bIsDeletingPoints) + { + ModifiedIndices.Add(PointIndex); + ++PointIndex; + } + else + { + ModifiedIndices.Add(PointIndex); + NewPositions.Add(CurvePosition); + NewValues.Add(CurveValue); + + // Interpolation is a special case - since Unreal and Houdini interpolation + // types are different, we do our best to convert the Unreal type to the + // Houdini type. But, in the case that we consider the two equivalent, to + // prevent loss of current setting, we re-use the old interpolation type. + if (bIsInterpolationEquivalent) + { + NewInterpolationTypes.Add(PointInterpolationType); + } + else + { + NewInterpolationTypes.Add( + TranslateInterpolation(CurveInterpolationType)); + } + + ++CurveIndex; + ++PointIndex; + } + } + } + + // We have more points than curve keys (as a result of deletion) + while (PointIndex < NumPoints) + { + ModifiedIndices.Add(PointIndex); + + ++PointIndex; + } + + // We have more curve keys than points (as a result of insertion) + while (CurveIndex < NumCurveKeys.GetValue()) + { + ModifiedIndices.Add(CurveIndex); + NewPositions.Add(GetCurveKeyPosition(CurveIndex).GetValue()); + NewValues.Add(GetCurveKeyValue(CurveIndex).GetValue()); + NewInterpolationTypes.Add(EHoudiniRampInterpolationType::LINEAR); + + ++CurveIndex; + } + } + + if (bIsAddingPoints) + { + if (!ModifiedIndices.IsEmpty() + && !NewPositions.IsEmpty() + && !NewValues.IsEmpty() + && !NewInterpolationTypes.IsEmpty()) + { + if (NewPositions[0].IsSet() + && NewValues[0].IsSet() + && NewInterpolationTypes[0].IsSet()) + { + RampView->InsertRampPoint( + ModifiedIndices[0], + NewPositions[0].GetValue(), + NewValues[0].GetValue(), + NewInterpolationTypes[0].GetValue()); + } + } + } + else if (bIsDeletingPoints) + { + RampView->DeleteRampPoints(ModifiedIndices); + } + else + { + RampView->SetRampPoints( + ModifiedIndices, NewPositions, NewValues, NewInterpolationTypes); + } + + OnCurveChangedDelegate.ExecuteIfBound(); + } + +private: + + /** + * Since there are fewer Unreal interpolation types than Houdini interpolation types, we map + * multiple Houdini interpolation types to Unreal's cubic interpolation type. Use this to check + * if we consider the two equivalent. + */ + static bool + IsInterpolationEquivalent( + const ERichCurveInterpMode UnrealInterpolation, + const EHoudiniRampInterpolationType HoudiniInterpolation) + { + switch (UnrealInterpolation) + { + case RCIM_Linear: + return HoudiniInterpolation == EHoudiniRampInterpolationType::LINEAR; + case RCIM_Constant: + return HoudiniInterpolation == EHoudiniRampInterpolationType::CONSTANT; + case RCIM_Cubic: + switch (HoudiniInterpolation) + { + case EHoudiniRampInterpolationType::BEZIER: + case EHoudiniRampInterpolationType::BSPLINE: + case EHoudiniRampInterpolationType::CATMULL_ROM: + case EHoudiniRampInterpolationType::HERMITE: + case EHoudiniRampInterpolationType::MONOTONE_CUBIC: + return true; + default: + return false; + } + case RCIM_None: + default: + return HoudiniInterpolation == EHoudiniRampInterpolationType::InValid; + } + } + + /** Converts Unreal interpolation type to the most appropriate Houdini interpolation type. */ + static EHoudiniRampInterpolationType + TranslateInterpolation(const ERichCurveInterpMode InterpMode) + { + switch (InterpMode) + { + case RCIM_Linear: + return EHoudiniRampInterpolationType::LINEAR; + case RCIM_Constant: + return EHoudiniRampInterpolationType::CONSTANT; + case RCIM_Cubic: + return EHoudiniRampInterpolationType::CATMULL_ROM; + case RCIM_None: + default: + return EHoudiniRampInterpolationType::InValid; + } + } +}; + +/** Common widget elements for all types of ramps. */ +template +class SHoudiniRampBase : public SCompoundWidget +{ +public: + + using RampViewType = InRampViewType; + using ValueType = typename RampViewType::ValueType; + using CurveEditorWidgetType = InCurveEditorWidgetType; + + DECLARE_DELEGATE(FOnValueCommitted); + + SLATE_BEGIN_ARGS(SHoudiniRampBase) + {} + + SLATE_ARGUMENT(TArrayView, RampParameters) + + /** Called when the ramp is modified using the UI from outside the ramp editor. */ + SLATE_EVENT(FOnValueCommitted, OnValueCommitted) + + SLATE_END_ARGS() + + virtual void Construct(const FArguments& InArgs) + { + OnValueCommitted = InArgs._OnValueCommitted; + + RampView = MakeShared(InArgs._RampParameters); + + ChildSlot + .HAlign(HAlign_Fill) + [ + SNew(SVerticalBox) + + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SHorizontalBox) + + + SHorizontalBox::Slot() + .Padding(2, 2, 5, 2) + [ + SNew(SBorder) + .VAlign(VAlign_Fill) + [ + SAssignNew(CurveEditor, CurveEditorWidgetType) + .RampView(RampView) + .OnCurveChanged_Lambda( + [this]() { OnValueCommitted.ExecuteIfBound(); }) + ] + ] + + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Top) + [ + ConstructOpenInNewWindowButton() + ] + ] + + + SVerticalBox::Slot() + .AutoHeight() + [ + ConstructRampPoints() + ] + ]; + } + +protected: + + /** Delegate to call when the value is committed */ + FOnValueCommitted OnValueCommitted; + + TSharedPtr RampView; + + TSharedPtr CurveEditor; + + /** Called when a point's position is modified using the UI from outside the curve editor. */ + virtual bool OnPointPositionCommit(const int32 Index, const float NewPosition) + { + if (!RampView.IsValid()) + { + return false; + } + + return RampView->SetRampPointPosition(Index, NewPosition); + } + + /** Called when a point's value is modified using the UI from outside the curve editor. */ + virtual bool OnPointValueCommit(const int32 Index, const ValueType NewValue) + { + if (!RampView.IsValid()) + { + return false; + } + + return RampView->SetRampPointValue(Index, NewValue); + } + + /** + * Called when a point's interpolation type is modified using the UI from outside the curve + * editor. + */ + virtual bool OnPointInterpolationTypeCommit( + const int32 Index, const EHoudiniRampInterpolationType NewInterpolationType) + { + if (!RampView.IsValid()) + { + return false; + } + + return RampView->SetRampPointInterpolationType(Index, NewInterpolationType); + } + + /** Gets the column header label for value type of the ramp. */ + virtual FString GetValueString() const = 0; + + virtual TSharedRef ConstructRampPointValueWidget(const int32 Index) = 0; + + /** Button to call @ref OpenNewWindow. */ + TSharedRef ConstructOpenInNewWindowButton() + { + return SNew(SButton) + .ButtonStyle(_GetEditorStyle(), "SimpleButton") + .ToolTipText(LOCTEXT("OpenInNewWindow", "Open In New Window")) + .OnClicked(FOnClicked::CreateLambda( + [this]() -> FReply + { + OpenNewWindow(); + return FReply::Handled(); + })) + [ + SNew(SImage) + .Image(_GetEditorStyle().GetBrush("Icons.OpenInExternalEditor")) + ]; + } + + /** Opens the curve editor in a pop-out window. */ + virtual bool OpenNewWindow() + { + if (GEditor) + { + if (auto RampEditorWindowSubsystem = + GEditor->GetEditorSubsystem()) + { + // We want a copy of the OnValueCommitted delegate inside the lambda as the created + // window (and the lambda it will own) can outlive this widget's lifetime. + return RampEditorWindowSubsystem->OpenEditor( + RampView, + AsShared(), + UHoudiniRampEditorWindowSubsystem::FOnValueCommitted::CreateLambda( + [OnValueCommitted = OnValueCommitted]() + { + OnValueCommitted.ExecuteIfBound(); + })); + } + } + return false; + } + + /** Create the UI for the ramp's stop points. */ + TSharedRef ConstructRampPoints() + { + const bool bCookingEnabled = FHoudiniEngine::Get().IsCookingEnabled(); + + TSharedRef GridPanel = SNew(SUniformGridPanel); + + int32 RowIndex = 0; + + GridPanel->SetSlotPadding(FMargin(2.f, 2.f, 5.f, 3.f)); + GridPanel->AddSlot(0, RowIndex) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Position"))) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ]; + + GridPanel->AddSlot(1, RowIndex) + [ + SNew(STextBlock) + .Text(FText::FromString(GetValueString())) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ]; + + GridPanel->AddSlot(2, RowIndex) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Interp."))) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ]; + + GridPanel->AddSlot(3, RowIndex) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(3.f, 0.f) + .MaxWidth(35.f) + .AutoWidth() + [ + PropertyCustomizationHelpers::MakeAddButton( + FSimpleDelegate::CreateLambda( + [this]() + { + if (!RampView.IsValid()) + { + return; + } + if (RampView->InsertRampPoint(RampView->GetPointCount())) + { + OnValueCommitted.ExecuteIfBound(); + } + }), + LOCTEXT("AddRampPoint", "Add a ramp point to the end"), true) + ] + + + SHorizontalBox::Slot() + .Padding(3.f, 0.f) + .MaxWidth(35.f) + .AutoWidth() + [ + PropertyCustomizationHelpers::MakeRemoveButton( + FSimpleDelegate::CreateLambda( + [this]() + { + if (!RampView.IsValid()) + { + return; + } + if (RampView->DeleteRampPoint(-1)) + { + OnValueCommitted.ExecuteIfBound(); + } + }), + LOCTEXT("DeleteRampPoint", "Delete the last ramp point"), true) + ] + ]; + + int32 PointCount = RampView.IsValid() ? RampView->GetPointCount() : 0; + for (int32 Index = 0; Index < PointCount; ++Index) + { + ++RowIndex; + + float CurPos = RampView.IsValid() ? RampView->GetRampPointPosition(Index).Get(0.f) : 0.f; + + GridPanel->AddSlot(0, RowIndex) + [ + SNew(SNumericEntryBox) + .AllowSpin(true) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + .Value(CurPos) + .OnValueChanged_Lambda([](float Val) {}) + .OnValueCommitted_Lambda( + [this, Index](float Val, ETextCommit::Type TextCommitType) + { + // For some reason I can't figure out, UE sends a second commit event with + // type Default, which has the old value, causing the first commit to be + // reset. So ignore it. + if (TextCommitType == ETextCommit::Type::Default) + return; + + if (OnPointPositionCommit(Index, Val)) + { + OnValueCommitted.ExecuteIfBound(); + } + }) + .OnBeginSliderMovement_Lambda([](){}) + .OnEndSliderMovement_Lambda( + [this, Index](const float Val) + { + if (OnPointPositionCommit(Index, Val)) + { + OnValueCommitted.ExecuteIfBound(); + } + }) + .SliderExponent(1.0f) + ]; + + GridPanel->AddSlot(1, RowIndex) + [ + ConstructRampPointValueWidget(Index) + ]; + + int32 CurChoice = RampView.IsValid() + ? static_cast(RampView->GetRampPointInterpolationType(Index).Get( + EHoudiniRampInterpolationType::InValid)) + : 0; + + TSharedPtr>> ComboBoxCurveMethod; + GridPanel->AddSlot(2, RowIndex) + [ + SAssignNew(ComboBoxCurveMethod, SComboBox>) + .OptionsSource( + FHoudiniEngineEditor::Get().GetHoudiniParameterRampInterpolationMethodLabels()) + .InitiallySelectedItem( + (*FHoudiniEngineEditor::Get().GetHoudiniParameterRampInterpolationMethodLabels())[CurChoice]) + .OnGenerateWidget_Lambda( + [](TSharedPtr ChoiceEntry) + { + FText ChoiceEntryText = FText::FromString(*ChoiceEntry); + return SNew(STextBlock) + .Text(ChoiceEntryText) + .ToolTipText(ChoiceEntryText) + .Font(_GetEditorStyle().GetFontStyle( + TEXT("PropertyWindow.NormalFont"))); + }) + .OnSelectionChanged_Lambda( + [this, Index](TSharedPtr NewChoice, ESelectInfo::Type SelectType) + { + EHoudiniRampInterpolationType NewInterpType = + UHoudiniParameter::GetHoudiniInterpMethodFromString(*NewChoice.Get()); + + if (OnPointInterpolationTypeCommit(Index, NewInterpType)) + { + OnValueCommitted.ExecuteIfBound(); + } + }) + [ + SNew(STextBlock) + .Text_Lambda( + [this, Index]() + { + const auto CurInterpType = RampView.IsValid() + ? RampView->GetRampPointInterpolationType(Index).Get( + EHoudiniRampInterpolationType::InValid) + : EHoudiniRampInterpolationType::InValid; + + return FText::FromString( + UHoudiniParameter::GetStringFromHoudiniInterpMethod(CurInterpType)); + }) + .Font(_GetEditorStyle().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) + ] + ]; + + GridPanel->AddSlot(3, RowIndex) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(3.f, 0.f) + .MaxWidth(35.f) + .AutoWidth() + [ + PropertyCustomizationHelpers::MakeAddButton(FSimpleDelegate::CreateLambda( + [this, Index]() + { + if (!RampView.IsValid()) + { + return; + } + if (RampView->InsertRampPoint(Index)) + { + OnValueCommitted.ExecuteIfBound(); + } + }), + LOCTEXT("AddRampPoint", "Add a ramp point before this point"), true) + ] + + + SHorizontalBox::Slot() + .Padding(3.f, 0.f) + .MaxWidth(35.f) + .AutoWidth() + [ + PropertyCustomizationHelpers::MakeDeleteButton(FSimpleDelegate::CreateLambda( + [this, Index]() + { + if (!RampView.IsValid()) + { + return; + } + if (RampView->DeleteRampPoint(Index)) + { + OnValueCommitted.ExecuteIfBound(); + } + }), + LOCTEXT("DeleteRampPoint", "Delete this ramp point"), true) + ] + ]; + } + return GridPanel; + } +}; + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniToolsPanel.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "SHoudiniToolsPanel.h" + +#include "HoudiniEngineEditorPrivatePCH.h" + +#include "Editor.h" +#include "Modules/ModuleManager.h" +#include "Widgets/SBoxPanel.h" +#include "SlateOptMacros.h" +#include "Styling/CoreStyle.h" +#include "Widgets/Images/SImage.h" +#include "Styling/SlateTypes.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/Views/STableViewBase.h" +#include "Widgets/Views/STableRow.h" +#include "Widgets/Views/SListView.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SComboBox.h" +#include "EditorStyleSet.h" +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "DragAndDrop/AssetDragDropOp.h" +#include "ActorFactories/ActorFactory.h" +#include "Engine/Selection.h" +#include "IContentBrowserSingleton.h" +#include "EditorViewportClient.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "AssetToolsModule.h" +#include "EditorDirectories.h" +#include "EditorReimportHandler.h" +#include "FileHelpers.h" +#include "HoudiniEngineCommands.h" +#include "HoudiniEngineEditorSettings.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEngineStyle.h" +#include "HoudiniToolsEditor.h" +#include "HoudiniToolsPackageAsset.h" +#include "HoudiniToolsPackageAssetFactory.h" +#include "LandscapeProxy.h" +#include "Framework/Application/SlateApplication.h" +#include "PropertyEditorModule.h" +#include "SWarningOrErrorBox.h" +#include "Interfaces/IMainFrameModule.h" +#include "Misc/MessageDialog.h" +#include "Widgets/Images/SLayeredImage.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SDirectoryPicker.h" +#include "Widgets/Input/SSearchBox.h" +#include "Widgets/Layout/SExpandableArea.h" +#include "Widgets/Layout/SGridPanel.h" +#include "Widgets/Layout/SScrollBox.h" +#include "Widgets/Layout/SUniformGridPanel.h" +#include "HoudiniToolsRuntimeUtils.h" +#include "Misc/ConfigCacheIni.h" +#include "Widgets/Layout/SWidgetSwitcher.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniPresetActorFactory.h" +#include "HoudiniToolTypesEditor.h" + +#define LOCTEXT_NAMESPACE "HoudiniTools" + +const FString SHoudiniToolsPanel::SettingsIniSection = TEXT("HoudiniEngine"); + +struct FHoudiniToolsPanelUtils +{ + static FHoudiniToolsEditor& GetHoudiniTools() { return FHoudiniEngineEditor::Get().GetHoudiniTools(); } + + static FString GetAbsoluteGameContentDir(); + + static FString GetLastDirectory(const bool bRelativeToGameContentDir, const ELastDirectory::Type LastDirectory = ELastDirectory::GENERIC_IMPORT); + + // Check whether the path adheres to the RelativeToGameContentDir restriction. + // This does not check whether the path actually exists. + static bool IsValidPath(const FString& AbsolutePath, bool bRelativeToGameContentDir, FText* OutReason=nullptr); +}; + + +FString +FHoudiniToolsPanelUtils::GetAbsoluteGameContentDir() +{ + return FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()); +} + + +FString +FHoudiniToolsPanelUtils::GetLastDirectory(const bool bRelativeToGameContentDir, const ELastDirectory::Type LastDirectory) +{ + FString StartDirectory = FEditorDirectories::Get().GetLastDirectory(LastDirectory); + const FString AbsoluteGameContentDir = GetAbsoluteGameContentDir(); + if (bRelativeToGameContentDir && !IsValidPath(StartDirectory, bRelativeToGameContentDir)) + { + StartDirectory = AbsoluteGameContentDir; + } + return StartDirectory; +} + + +bool +FHoudiniToolsPanelUtils::IsValidPath(const FString& AbsolutePath, const bool bRelativeToGameContentDir, FText* const OutReason) +{ + if(bRelativeToGameContentDir) + { + const FString AbsoluteGameContentDir = GetAbsoluteGameContentDir(); + if(!AbsolutePath.StartsWith(AbsoluteGameContentDir)) + { + if(OutReason) + { + *OutReason = FText::Format(LOCTEXT("Error_InvalidRootPath", "The chosen directory must be within {0}"), FText::FromString(AbsoluteGameContentDir)); + } + return false; + } + } + return true; +} + +UHoudiniToolDirectoryProperties::UHoudiniToolDirectoryProperties(const FObjectInitializer & ObjectInitializer) + :Super(ObjectInitializer) + , CustomHoudiniToolsDirectories() +{ +}; + +FName SHoudiniToolCategory::TypeName = TEXT("SHoudiniToolCategory"); + + + +SHoudiniToolsCategoryFilter::SHoudiniToolsCategoryFilter() + : ShowAll(true) + , HiddenCategories(nullptr) + , Categories(nullptr) +{ + +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void +SHoudiniToolsCategoryFilter::Construct(const FArguments& InArgs) +{ + SetShowAll(InArgs._ShowAll); + + OnShowAllChanged = InArgs._OnShowAllChanged; + OnCategoryStateChanged = InArgs._OnCategoryStateChanged; + HiddenCategories = InArgs._HiddenCategoriesSource; + Categories = InArgs._CategoriesSource; + + RebuildWidget(); +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +void +SHoudiniToolsCategoryFilter::SetShowAll(TAttribute InShowAll) +{ + ShowAll = InShowAll; +} + +bool +SHoudiniToolsCategoryFilter::IsCategoryEnabled(FString CategoryName) const +{ + if (!HiddenCategories) + { + return false; + } + + return !HiddenCategories->Contains(CategoryName); +} + + +TSharedPtr +SHoudiniToolsCategoryFilter::RebuildWidget() +{ + FMenuBuilder MenuBuilder( true, NULL ); + + MenuBuilder.AddMenuEntry( + FText::FromString("Show All"), + FText::FromString("Show all categories."), + FSlateIcon(), + FUIAction( + FExecuteAction::CreateLambda([&]() -> void + { + OnShowAllChanged.ExecuteIfBound( !ShowAll.Get() ); + }), + FCanExecuteAction(), + FGetActionCheckState::CreateLambda([&]() -> ECheckBoxState { return ShowAll.Get() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } ) + ), + NAME_None, + EUserInterfaceActionType::ToggleButton + ); + + MenuBuilder.BeginSection("Categories", LOCTEXT("FilterMenu_Section_Categories", "Categories")); + + // Add all the categories to the menu + + if (Categories) + { + for (const FString& CategoryName : *Categories) + { + MenuBuilder.AddMenuEntry( + FText::FromString(CategoryName), + FText::FromString( FString::Format(TEXT("Show/Hide {0}."), {CategoryName}) ), + FSlateIcon(), + FUIAction( + FExecuteAction::CreateLambda([&, CategoryName]() -> void + { + OnCategoryStateChanged.ExecuteIfBound(CategoryName, !IsCategoryEnabled(CategoryName)); + }), + FCanExecuteAction(), + FGetActionCheckState::CreateLambda([&, CategoryName]() -> ECheckBoxState + { + return HiddenCategories->Contains(CategoryName) ? ECheckBoxState::Unchecked : ECheckBoxState::Checked; + } ) + ), + NAME_None, + EUserInterfaceActionType::ToggleButton + ); + } + } + + TSharedPtr Widget = MenuBuilder.MakeWidget(); + ChildSlot + [ + Widget.ToSharedRef() + ]; + + return Widget; +} + +SHoudiniToolCategory::SHoudiniToolCategory() + : CategoryType(EHoudiniToolCategoryType::Package) + , TextWrapWidth(0) +{ +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION + +void +SHoudiniToolCategory::Construct(const FArguments& InArgs) +{ + CategoryLabel = InArgs._CategoryLabel.Get().ToString(); + CategoryType = InArgs._CategoryType.Get(); + SourceEntries = InArgs._HoudiniToolsItemSource.Get(); + IsVisible = InArgs._IsVisible; + ShowEmptyCategory = InArgs._ShowEmptyCategory; + ShowHoudiniAssets = InArgs._ShowHoudiniAssets; + ShowPresets = InArgs._ShowPresets; + ShowHiddenTools = InArgs._ShowHiddenTools; + ToolContextMenuOpening = InArgs._OnToolContextMenuOpening; + CategoryContextMenuOpening = InArgs._OnCategoryContextMenuOpening; + + const FText UserCategoryTooltip = FText::Format(LOCTEXT("HoudiniToolsPanel_UserCategoryTooltip", "{0} (User Category)"), FText::FromString(CategoryLabel)); + const FText PackageCategoryTooltip = FText::Format( LOCTEXT("HoudiniToolsPanel_PackageCategoryTooltip", "{0} (Package Category)"), FText::FromString(CategoryLabel)); + + if (InArgs._ViewMode.Get() == EHoudiniToolsViewMode::TileView) + { + // Configure Tile View + SAssignNew(HoudiniToolsView, SHoudiniToolTileView) + .ScrollbarVisibility(EVisibility::Collapsed) + .SelectionMode( ESelectionMode::Single ) + .ListItemsSource( &VisibleEntries ) + .OnGenerateTile( InArgs._OnGenerateTile ) + .OnSelectionChanged_Lambda( [this, InArgs](const TSharedPtr& HoudiniTool, ESelectInfo::Type SelectInfo) + { + ActiveTool = HoudiniTool; + InArgs._OnToolSelectionChanged.ExecuteIfBound(this, HoudiniTool, SelectInfo); + } ) + .OnMouseButtonDoubleClick( InArgs._OnMouseButtonDoubleClick ) + .OnContextMenuOpening(this, &SHoudiniToolCategory::HandleContextMenuOpening ) + .ItemHeight( 64 ) + .ItemWidth( 120 ) + .ItemAlignment(EListItemAlignment::LeftAligned); + } + else + { + // Configure List View + SAssignNew(HoudiniToolsView, SHoudiniToolListView) + .ScrollbarVisibility(EVisibility::Collapsed) + .SelectionMode(ESelectionMode::Single) + .ListItemsSource(&VisibleEntries) + .OnGenerateRow(InArgs._OnGenerateRow) + .OnSelectionChanged_Lambda([this, InArgs](const TSharedPtr& HoudiniTool, ESelectInfo::Type SelectInfo) + { + ActiveTool = HoudiniTool; + InArgs._OnToolSelectionChanged.ExecuteIfBound(this, HoudiniTool, SelectInfo); + }) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 5 + .ItemHeight(64) //UE5.5 deprecated this - only for tile view +#endif + .OnMouseButtonDoubleClick(InArgs._OnMouseButtonDoubleClick) + .OnContextMenuOpening(this, &SHoudiniToolCategory::HandleContextMenuOpening); + } + + ChildSlot + [ + SNew(SExpandableArea) + .HeaderContent() + [ + SNew(SHorizontalBox) + .ToolTipText(CategoryType == EHoudiniToolCategoryType::User ? UserCategoryTooltip : PackageCategoryTooltip) + // Label + +SHorizontalBox::Slot() + .FillWidth(1.f) + [ + SNew(STextBlock) + .Font(_GetEditorStyle().GetFontStyle("HeadingExtraSmall")) + .Text( InArgs._CategoryLabel ) + .TransformPolicy(ETextTransformPolicy::ToUpper) + ] + + // Category Type Icon ( displayed for user categories ) + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .TextStyle(_GetEditorStyle(), "ContentBrowser.TopBar.Font") + .Font(_GetEditorStyle().GetFontStyle("FontAwesome.12")) + .Text(FText::FromString(FString( + CategoryType == EHoudiniToolCategoryType::User ? + TEXT("\xf007") : /* fa-user */ + TEXT("\xf187") /* fa-archive */ + )) ) + .ColorAndOpacity( FSlateColor::UseForeground() ) + .Visibility( CategoryType == EHoudiniToolCategoryType::User ? EVisibility::Visible : EVisibility::Collapsed ) + .ToolTipText( CategoryType == EHoudiniToolCategoryType::User ? UserCategoryTooltip : PackageCategoryTooltip ) + ] + ] + .BodyContent() + [ + SAssignNew(CategoryContentSwitcher, SWidgetSwitcher) + // Houdini Tools View + +SWidgetSwitcher::Slot() + [ + HoudiniToolsView.ToSharedRef() + ] + // Message if we don't have any tools in this category + +SWidgetSwitcher::Slot() + .HAlign(HAlign_Left) + .Padding(FMargin(10.f, 15.f, 10.f, 10.f)) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniToolsPanel_NoToolsInCategory", "Import Houdini Assets next to this HoudiniToolsPackage asset to add tools to this category.")) + .ColorAndOpacity(FLinearColor(1.f, 1.f, 1.f, 0.25f)) + .WrapTextAt_Lambda([this]()->float { return TextWrapWidth; }) + ] + // Message if we don't have any _visible_ tools in this category + +SWidgetSwitcher::Slot() + .HAlign(HAlign_Left) + .Padding(FMargin(10.f, 15.f, 10.f, 10.f)) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniToolsPanel_NoVisibleToolsInCategory2", "Enable \"Show Hidden Tools\" or import Houdini Assets next to this HoudiniToolsPackage asset to add tools to this category.")) + .ColorAndOpacity(FLinearColor(1.f, 1.f, 1.f, 0.25f)) + .WrapTextAt_Lambda([this]()->float { return TextWrapWidth; }) + ] + ] + ] + ]; + + UpdateVisibleItems(); +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +void +SHoudiniToolCategory::SetFilterString(const FString& NewFilterString) +{ + if (FilterString != NewFilterString) + { + FilterString = NewFilterString; + UpdateVisibleItems(); + } +} + +void +SHoudiniToolCategory::ClearSelection() +{ + if (!HoudiniToolsView.IsValid()) + return; + + HoudiniToolsView->ClearHighlightedItems(); + HoudiniToolsView->ClearSelection(); +} + +void +SHoudiniToolCategory::RequestRefresh() +{ + ClearSelection(); + if (HoudiniToolsView.IsValid()) + { + HoudiniToolsView->RequestListRefresh(); + } +} + +void +SHoudiniToolCategory::UpdateVisibleItems() +{ + VisibleEntries.Empty(); + HoudiniToolsView->RequestListRefresh(); + + ECategoryContentType ContentType = ECategoryContentType::Tools; + + ON_SCOPE_EXIT + { + if (CategoryContentSwitcher.IsValid()) + { + CategoryContentSwitcher->SetActiveWidgetIndex(static_cast(ContentType)); + } + }; + + if (!SourceEntries.IsValid()) + { + ContentType = ECategoryContentType::NoToolsInCategory; + return; + } + + for (TSharedPtr HoudiniTool : SourceEntries->Tools ) + { + if (!HoudiniTool.IsValid()) + continue; + + if (ShowHiddenTools.Get() == false) + { + if (HoudiniTool->IsHiddenInCategory(GetCategoryLabel())) + { + // This tool should not be visible in this category + continue; + } + } + + if (HoudiniTool->PackageToolType == EHoudiniPackageToolType::HoudiniAsset && ShowHoudiniAssets.Get() == false) + { + // We don't want to show HoudiniAssets + continue; + } + if (HoudiniTool->PackageToolType == EHoudiniPackageToolType::Preset && ShowPresets.Get() == false) + { + // We don't want to show Presets + continue; + } + if (HoudiniTool->Name.ToString().Contains(FilterString) || FilterString.Len() == 0) + { + VisibleEntries.Add(HoudiniTool); + } + } + + if (IsVisible.Get() == false) + { + SetVisibility(EVisibility::Collapsed); + } + else if (VisibleEntries.Num() == 0 ) + { + if (FilterString.Len() > 0) + { + // We are searching for something. Collapse this category. + SetVisibility(EVisibility::Collapsed); + } + else if (ShowEmptyCategory.Get() == true) + { + // We want to show this (empty) category + SetVisibility(EVisibility::Visible); + } + else + { + SetVisibility(EVisibility::Collapsed); + } + } + else + { + SetVisibility(EVisibility::Visible); + } + + if (SourceEntries->Tools.Num() == 0) + { + // We have no tools in this category. + ContentType = ECategoryContentType::NoToolsInCategory; + } + else + { + if (VisibleEntries.Num() == 0) + { + // We have tools, they are just not visible. + ContentType = ECategoryContentType::NoVisibleToolsInCategory; + } + } +} + + +void +SHoudiniToolCategory::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) +{ + SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); + + // Set cached wrap text width based on new "LastGeometry" value. + // We set this only when changed because binding a delegate to text wrapping attributes is expensive + constexpr float Padding = 25.f; + if(!FMath::IsNearlyEqual(TextWrapWidth, AllottedGeometry.Size.X-Padding)) + { + TextWrapWidth = AllottedGeometry.Size.X - Padding; // Add a tiny bit of padding to avoid words being cut off. + } +} + + +FReply +SHoudiniToolCategory::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + FReply Reply = SCompoundWidget::OnMouseButtonUp(MyGeometry, MouseEvent); + + if ( MouseEvent.GetEffectingButton() == EKeys::RightMouseButton ) + { + if (!Reply.IsEventHandled()) + { + if (CategoryContextMenuOpening.IsSet() && CategoryContextMenuOpening.Get().IsBound()) + { + const TSharedPtr MenuContent = CategoryContextMenuOpening.Get().Execute(); + if (MenuContent.IsValid()) + { + const FWidgetPath WidgetPath = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath(); +#if ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION >= 1 + const FVector2f SummonLocation = MouseEvent.GetScreenSpacePosition(); +#else + const FVector2D SummonLocation = MouseEvent.GetScreenSpacePosition(); +#endif + FSlateApplication::Get().PushMenu(AsShared(), WidgetPath, MenuContent.ToSharedRef(), SummonLocation, FPopupTransitionEffect(FPopupTransitionEffect::ContextMenu)); + return FReply::Handled(); + } + } + } + } + + return Reply; +} + + +TSharedPtr +SHoudiniToolCategory::HandleContextMenuOpening() +{ + // otherwise, invoke the categeory context menu + if (HoudiniToolsView->GetNumItemsSelected() > 0) + { + if (ToolContextMenuOpening.IsSet() && ToolContextMenuOpening.Get().IsBound()) + { + // If we have an active item, invoke the Tool context menu. + return ToolContextMenuOpening.Get().Execute(); + } + } + else + { + if (CategoryContextMenuOpening.IsSet() && CategoryContextMenuOpening.Get().IsBound()) + { + // otherwise, invoke the category + return CategoryContextMenuOpening.Get().Execute(); + } + } + + return nullptr; +} + + +SHoudiniToolNewPackage::SHoudiniToolNewPackage() + : CalculatedPackageName(FText()) + , bValidPackageName(false) + , bSyncCategory(true) +{ + +} + + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void +SHoudiniToolNewPackage::Construct(const FArguments& InArgs) +{ + const FText InitialPackageName = LOCTEXT("CreatePackage_PackageNameValue", "PackageName"); + int row = 0; + + const int RowPackageName = row++; + const int RowCategoryName = row++; + const int RowPackageLocation = row++; + + // Assign our widget events to our delegates + OnCanceled = InArgs._OnCanceled; + OnCreated = InArgs._OnCreated; + + this->ChildSlot + [ + SNew(SBorder) + .Padding(18.0f) + .BorderImage( _GetBrush("Docking.Tab.ContentAreaBrush") ) + [ + SNew(SBorder) + .BorderImage(FAppStyle::Get().GetBrush("Brushes.Panel")) + .Padding(10.0f) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + .AutoHeight() + [ + SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("HeadingExtraSmall")) + .Text( LOCTEXT( "HoudiniTool_CreatePackageTitle", "Create Houdini Tools Package" ) ) + .TransformPolicy(ETextTransformPolicy::ToUpper) + ] + + // ----------------------------- + // Description + // ----------------------------- + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 10.0f) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .AutoWrapText(true) + .Text(LOCTEXT("HoudiniTool_CreatePackageDescription", "Enter a name for your new Houdini Tools package. Package names may only contain alphanumeric characters, and may not contain a space.")) + ] + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .AutoWrapText(true) + .Text(LOCTEXT("HoudiniTool_CreatePackageDescription2", "When you click the Create button, an empty Houdini Tools package will created at the displayed location.")) + ] + + ] + + // ----------------------------- + // Name Error label + // ----------------------------- + +SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 5.0f) + [ + SNew(SWarningOrErrorBox) + .MessageStyle(EMessageStyle::Error) + .Visibility(this, &SHoudiniToolNewPackage::GetNameErrorLabelVisibility) + .Message(this, &SHoudiniToolNewPackage::GetNameErrorLabelText) + ] + + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBorder) + .BorderImage(_GetBrush("DetailsView.CategoryTop")) + .BorderBackgroundColor(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f )) + .Padding(FMargin(6.0f, 4.0f, 7.0f, 4.0f)) + [ + SNew(SGridPanel) + .FillColumn(1, 1.0f) + + //-------------------------- + // Houdini Tools package name + //-------------------------- + + + SGridPanel::Slot(0, RowPackageName) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreatePackage_NameLabel", "Name")) + ] + + SGridPanel::Slot(1, RowPackageName) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SAssignNew(PackageNameEditBox, SEditableTextBox) + .Text(InitialPackageName) + .ToolTipText(LOCTEXT("CreatePackage_NameTooltip", "The name for the package folder as seen in the content browser.")) + .OnTextChanged( this, &SHoudiniToolNewPackage::OnPackageNameTextChanged ) + .OnTextCommitted( this, &SHoudiniToolNewPackage::OnPackageNameTextCommitted ) + ] + + //-------------------------- + // Houdini Tools category name + //-------------------------- + + + SGridPanel::Slot(0, RowCategoryName) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreatePackage_CategoryLabel", "Category")) + ] + + SGridPanel::Slot(1, RowCategoryName) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SAssignNew(PackageCategoryEditBox, SEditableTextBox) + .Text(InitialPackageName) + .ToolTipText(LOCTEXT("CreatePackage_CategoryTooltip", "The category that will show up in the Houdini Tools Panel, containing all the tools in this package.")) + .OnTextChanged( this, &SHoudiniToolNewPackage::OnPackageCategoryTextChanged ) + .OnTextCommitted( this, &SHoudiniToolNewPackage::OnPackageCategoryTextCommitted ) + ] + + //-------------------------- + // Houdini Tools package location + //-------------------------- + + + SGridPanel::Slot(0, RowPackageLocation) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreatePackage_LocationLabel", "Location")) + ] + + SGridPanel::Slot(1, RowPackageLocation) + .VAlign(VAlign_Center) + .Padding(0.f, 5.0f) + [ + SNew(STextBlock) + .Text(this, &SHoudiniToolNewPackage::OnGetPackagePathText) + ] + ] + ] + + SVerticalBox::Slot() + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Right) + .FillHeight(1.0f) + [ + SNew(SUniformGridPanel) + .SlotPadding(FCoreStyle::Get().GetMargin("StandardDialog.SlotPadding")) + .MinDesiredSlotWidth(FCoreStyle::Get().GetFloat("StandardDialog.MinDesiredSlotWidth")) + .MinDesiredSlotHeight(FCoreStyle::Get().GetFloat("StandardDialog.MinDesiredSlotHeight")) + + + SUniformGridPanel::Slot(0, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .OnClicked(this, &SHoudiniToolNewPackage::HandleCreateClicked) + .IsEnabled(this, &SHoudiniToolNewPackage::IsCreateEnabled) + [ + SNew(STextBlock) + .Text(LOCTEXT( "CreatePackage_CreateButtonLabel", "Create" )) + ] + ] + + + SUniformGridPanel::Slot(1, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .OnClicked(this, &SHoudiniToolNewPackage::HandleCancelClicked) + [ + SNew(STextBlock) + .Text(LOCTEXT( "CreatePackage_CancelButtonLabel", "Cancel" )) + ] + ] + ] + ] + ]]; + + // Perform initial package name check to initialize the UI. + UpdatePackageName(InitialPackageName); +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +void +SHoudiniToolNewPackage::OnPackageNameTextChanged(const FText& Text) +{ + UpdatePackageName(Text); +} + + +void +SHoudiniToolNewPackage::OnPackageNameTextCommitted(const FText& Text, ETextCommit::Type Arg) +{ + UpdatePackageName(Text); +} + + +FText +SHoudiniToolNewPackage::OnGetPackagePathText() const +{ + return FText::FromString(FHoudiniToolsEditor::GetDefaultPackagePath(CalculatedPackageName.ToString())); +} + + +void +SHoudiniToolNewPackage::OnPackageCategoryTextChanged(const FText& Text) +{ + bSyncCategory = false; +} + + +void +SHoudiniToolNewPackage::OnPackageCategoryTextCommitted(const FText& Text, ETextCommit::Type Arg) +{ + bSyncCategory = false; +} + + +EVisibility +SHoudiniToolNewPackage::GetNameErrorLabelVisibility() const +{ + return bValidPackageName ? EVisibility::Hidden : EVisibility::SelfHitTestInvisible; +} + + +FText +SHoudiniToolNewPackage::GetNameErrorLabelText() const +{ + return PackageNameError; +} + + +bool +SHoudiniToolNewPackage::IsCreateEnabled() const +{ + return bValidPackageName; +} + + +FReply +SHoudiniToolNewPackage::HandleCreateClicked() +{ + // Create HoudiniToolsAssetPackageInfo + UHoudiniToolsPackageAssetFactory* Factory = NewObject(); + + const FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + const FString PackageAssetName = FHoudiniToolsRuntimeUtils::GetPackageUAssetName(); + const FString PackageBasePath = FHoudiniToolsEditor::GetDefaultPackagePath(CalculatedPackageName.ToString()); + const FString FullPath = FPaths::Combine(PackageBasePath, PackageAssetName); + + UHoudiniToolsPackageAsset* Asset = Cast(AssetToolsModule.Get().CreateAsset( + PackageAssetName, PackageBasePath, + UHoudiniToolsPackageAsset::StaticClass(), Factory, FName("ContentBrowserNewAsset"))); + + if (!Asset) + { + return FReply::Handled(); + } + + // By default add a catch-all category. + FCategoryRules DefaultRule; + DefaultRule.Include.Add("*"); + Asset->Categories.Add(GetCategory().ToString(), DefaultRule); + + OnCreated.ExecuteIfBound(FullPath); + + // Open the content browser at the newly created asset package. + TArray Objs = {Asset}; + GEditor->SyncBrowserToObjects(Objs, true); + + CloseContainingWindow(); + + return FReply::Handled(); +} + + +FReply +SHoudiniToolNewPackage::HandleCancelClicked() +{ + OnCanceled.ExecuteIfBound(); + CloseContainingWindow(); + + return FReply::Handled(); +} + + +FText +SHoudiniToolNewPackage::GetCategory() const +{ + return PackageCategoryEditBox->GetText(); +} + +void +SHoudiniToolNewPackage::UpdatePackageName(FText NewName) +{ + bValidPackageName = FHoudiniToolsEditor::IsValidPackageName(NewName.ToString(), &PackageNameError); + CalculatedPackageName = NewName; + + if (bSyncCategory) + { + PackageCategoryEditBox->SetText(NewName); + // Ensure bSyncCategory remains true until the Sync Category is + // changed by the user. + bSyncCategory = true; + } + + if (!bValidPackageName) + { + // Invalid package name + return; + } + + // Check whether a HoudiniTools package already exists at this location. + const FString PkgBasePath = FHoudiniToolsEditor::GetDefaultPackagePath(CalculatedPackageName.ToString()); + const FString PkgPath = FPaths::Combine(PkgBasePath, FHoudiniToolsRuntimeUtils::GetPackageUAssetName()); + + if ( _DoesAssetExist(PkgPath) ) + { + // Package already exists. + PackageNameError = LOCTEXT("PackageAlreadyExists", "Package already exists: '{0}'"); + PackageNameError = FText::Format(PackageNameError, {FText::FromString(PkgPath)}); + bValidPackageName = false; + return; + } + + PackageNameError = FText(); +} + +void +SHoudiniToolNewPackage::CloseContainingWindow() +{ + TSharedPtr ContainingWindow = FSlateApplication::Get().FindWidgetWindow(AsShared()); + + if ( ContainingWindow.IsValid() ) + { + ContainingWindow->RequestDestroyWindow(); + } +} + + +SHoudiniToolImportPackage::SHoudiniToolImportPackage() + : bIsValidPackageDirectory(false) + , bCreateMissingPackageJSON(true) +{ + +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void +SHoudiniToolImportPackage::Construct(const FArguments& InArgs) +{ + const FText ImportPackageTitle = LOCTEXT( "HoudiniTool_ImportPackage_Title", "Import Houdini Tools Package" ); + const FText InitialPackageName = LOCTEXT("CreatePackage_PackageNameValue", "PackageName"); + // Row counter for widgets in a uniform grid. + // Allows us to easily insert rows without having to worry about renumbering everything. + int row = 0; + + // First screen + const int RowPackageDirSelector = row++; + const int RowPackageName = row++; + const int RowPackageLocation = row++; + + // Second screen + row = 0; + const int Row2PackageDir = row++; + const int Row2PackageName = row++; + const int Row2PackageLocation = row++; + const int Row2CreateExternalJSONFile = row++; + + this->ChildSlot + [ + SNew(SBorder) + .Padding(18.0f) + .BorderImage( _GetBrush("Docking.Tab.ContentAreaBrush") ) + [ + SNew(SBorder) + .BorderImage(FAppStyle::Get().GetBrush("Brushes.Panel")) + .Padding(10.0f) + [ + // Vertical container for the widget switcher and row of action buttons + SNew(SVerticalBox) + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + .AutoHeight() + + // -------------------------------- + // Content Switcher Row + // -------------------------------- + + + SVerticalBox::Slot() + [ + SAssignNew(WidgetSwitcher, SWidgetSwitcher) + + // -------------------------------- + // Import Tool - First Panel + // -------------------------------- + + + SWidgetSwitcher::Slot() + [ + // ----------------------------- + // Title + // ----------------------------- + + SNew(SVerticalBox) + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + .AutoHeight() + [ + SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("HeadingExtraSmall")) + .Text( ImportPackageTitle ) + .TransformPolicy(ETextTransformPolicy::ToUpper) + ] + + // ----------------------------- + // Description + // ----------------------------- + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 10.0f) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .AutoWrapText(true) + .Text(LOCTEXT("HoudiniTool_ImportPackageDescription1", "Select the external package directory.")) + ] + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .AutoWrapText(true) + .Text(LOCTEXT("HoudiniTool_ImportPackageDescription2", "Enter a name for your new Houdini Tools package. Package names may only contain alphanumeric characters, and may not contain a space.")) + ] + ] + + // ----------------------------- + // Name Error label + // ----------------------------- + +SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 5.0f) + [ + SNew(SWarningOrErrorBox) + .MessageStyle(EMessageStyle::Error) + .Visibility(this, &SHoudiniToolImportPackage::GetNameErrorLabelVisibility) + .Message(this, &SHoudiniToolImportPackage::GetNameErrorLabelText) + ] + + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBorder) + .BorderImage(_GetBrush("DetailsView.CategoryTop")) + .BorderBackgroundColor(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f )) + .Padding(FMargin(6.0f, 4.0f, 7.0f, 4.0f)) + [ + SNew(SGridPanel) + .FillColumn(1, 1.0f) + + //-------------------------- + // Package Directory Selector + //-------------------------- + + + SGridPanel::Slot(0, RowPackageDirSelector) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("ImportPackage_DirectoryLabel", "Package Directory")) + ] + + SGridPanel::Slot(1, RowPackageDirSelector) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SAssignNew(DirectoryPicker, SDirectoryPicker) + .OnDirectoryChanged(this, &SHoudiniToolImportPackage::OnDirectoryChanged) + ] + + //-------------------------- + // Houdini Tools package name + //-------------------------- + + + SGridPanel::Slot(0, RowPackageName) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("ImportPackage_NameLabel", "Name")) + ] + + SGridPanel::Slot(1, RowPackageName) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SAssignNew(PackageNameEditBox, SEditableTextBox) + .Text(InitialPackageName) + .ToolTipText(LOCTEXT("ImportPackage_NameTooltip", "The name for the package folder as seen in the content browser.")) + .OnTextChanged( this, &SHoudiniToolImportPackage::OnPackageNameTextChanged ) + .OnTextCommitted( this, &SHoudiniToolImportPackage::OnPackageNameTextCommitted ) + ] + + //-------------------------- + // Houdini Tools package location + //-------------------------- + + + SGridPanel::Slot(0, RowPackageLocation) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("ImportPackage_LocationLabel", "Location")) + ] + + SGridPanel::Slot(1, RowPackageLocation) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SNew(STextBlock) + .Text(this, &SHoudiniToolImportPackage::OnGetPackagePathText) + ] + ] + ] // VerticalBox Slot + ] // Switcher - First Panel + + // -------------------------------- + // Import Tool - Second Panel + // -------------------------------- + + + SWidgetSwitcher::Slot() + [ + // ----------------------------- + // Title + // ----------------------------- + + SNew(SVerticalBox) + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + .AutoHeight() + [ + SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("HeadingExtraSmall")) + .Text( ImportPackageTitle ) + .TransformPolicy(ETextTransformPolicy::ToUpper) + ] + + // ----------------------------- + // Description + // ----------------------------- + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 10.0f) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .AutoWrapText(true) + .Text(LOCTEXT("HoudiniTool_ImportPackageDescription", "External package descriptor does not exist.")) + ] + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .AutoWrapText(true) + .Text(LOCTEXT("HoudiniTool_ImportPackageDescription", "Create a new package instead and import HDAs from the package directory.")) + ] + ] + + // ----------------------------- + // Grid panel for fields + // ----------------------------- + + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBorder) + .BorderImage(_GetBrush("DetailsView.CategoryTop")) + .BorderBackgroundColor(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f )) + .Padding(FMargin(6.0f, 4.0f, 7.0f, 4.0f)) + [ + SNew(SGridPanel) + .FillColumn(1, 1.0f) + + //-------------------------- + // External Package Directory + //-------------------------- + + + SGridPanel::Slot(0, Row2PackageDir) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("ImportPackage_DirectoryLabel", "Package Directory")) + ] + + SGridPanel::Slot(1, Row2PackageDir) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SNew(STextBlock) + .Text_Lambda([&]() -> FText + { + if (DirectoryPicker.IsValid()) + { + return FText::FromString(DirectoryPicker->GetDirectory()); + } + return FText(); + }) + ] + + //-------------------------- + // External Package Directory + //-------------------------- + + + SGridPanel::Slot(0, Row2PackageName) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("ImportPackage_PackageNameLabel", "Package Name")) + ] + + SGridPanel::Slot(1, Row2PackageName) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SNew(STextBlock) + .Text_Lambda([&]() -> FText + { + return FText::FromString(NewPackageName); + }) + ] + + //-------------------------- + // Package Location + //-------------------------- + + + SGridPanel::Slot(0, Row2PackageLocation) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("ImportPackage_LocationLabel", "Location")) + ] + + SGridPanel::Slot(1, Row2PackageLocation) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SNew(STextBlock) + .Text(this, &SHoudiniToolImportPackage::OnGetPackagePathText) + ] + + //-------------------------- + // Create external JSON file + //-------------------------- + + + SGridPanel::Slot(0, Row2CreateExternalJSONFile) + .VAlign(VAlign_Center) + .ColumnSpan(2) + .Padding(0.0f, 3.0f, 12.0f, 0.0f) + [ + SAssignNew(CreateExternalJSON, SCheckBox) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("ImportPackage_CreateJSONLabel", "Create external package description (JSON file)")) + ] + ] + ] + ] + ] + ] // Content Switcher Row + + // -------------------------------- + // Action Buttons row + // -------------------------------- + + + SVerticalBox::Slot() + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Right) + .FillHeight(1.0f) + [ + SNew(SUniformGridPanel) + .SlotPadding(FCoreStyle::Get().GetMargin("StandardDialog.SlotPadding")) + .MinDesiredSlotWidth(FCoreStyle::Get().GetFloat("StandardDialog.MinDesiredSlotWidth")) + .MinDesiredSlotHeight(FCoreStyle::Get().GetFloat("StandardDialog.MinDesiredSlotHeight")) + + // Back Button + + SUniformGridPanel::Slot(0, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .ButtonStyle(&FAppStyle::Get().GetWidgetStyle("Button")) + .OnClicked(this, &SHoudiniToolImportPackage::HandleBackClicked) + .Visibility(this, &SHoudiniToolImportPackage::GetBackVisibility) + [ + SNew(STextBlock) + .Text(LOCTEXT( "ImportPackage_BackButtonLabel", "Back" )) + ] + ] + + // Next Button + + SUniformGridPanel::Slot(1, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .ButtonStyle(&FAppStyle::Get().GetWidgetStyle("PrimaryButton")) + .OnClicked(this, &SHoudiniToolImportPackage::HandleNextClicked) + .IsEnabled(this, &SHoudiniToolImportPackage::IsNextEnabled) + .Visibility(this, &SHoudiniToolImportPackage::GetNextVisibility) + [ + SNew(STextBlock) + .Text(LOCTEXT( "ImportPackage_NextButtonLabel", "Next" )) + ] + ] + + // Import Button + + SUniformGridPanel::Slot(2, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .ButtonStyle(&FAppStyle::Get().GetWidgetStyle("PrimaryButton")) + .OnClicked(this, &SHoudiniToolImportPackage::HandleImportClicked) + .IsEnabled(this, &SHoudiniToolImportPackage::IsImportEnabled) + .Visibility(this, &SHoudiniToolImportPackage::GetImportVisibility) + [ + SNew(STextBlock) + .Text(LOCTEXT( "ImportPackage_ImportButtonLabel", "Import" )) + ] + ] + + // Create Button + + SUniformGridPanel::Slot(2, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .ButtonStyle(&FAppStyle::Get().GetWidgetStyle("PrimaryButton")) + .OnClicked(this, &SHoudiniToolImportPackage::HandleCreateClicked) + .IsEnabled(this, &SHoudiniToolImportPackage::IsCreateEnabled) + .Visibility(this, &SHoudiniToolImportPackage::GetCreateVisibility) + [ + SNew(STextBlock) + .Text(LOCTEXT( "ImportPackage_CreateButtonLabel", "Create" )) + ] + ] + + + // Cancel Button + + + SUniformGridPanel::Slot(3, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .OnClicked(this, &SHoudiniToolImportPackage::HandleCancelClicked) + [ + SNew(STextBlock) + .Text(LOCTEXT( "ImportPackage_CancelButtonLabel", "Cancel" )) + ] + ] + ] // Action Buttons Row + ] + ]]; +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +EVisibility +SHoudiniToolImportPackage::GetNameErrorLabelVisibility() const +{ + // TODO: Implement error conditions here + if (!bIsValidPackageDirectory) + { + return EVisibility::Visible; + } + + if (!bValidPackageName) + { + return EVisibility::Visible; + } + + if (WidgetSwitcher->GetActiveWidgetIndex() == 0) + { + return EVisibility::Hidden; + } + else if (WidgetSwitcher->GetActiveWidgetIndex() == 1) + { + return EVisibility::Visible; + } + + // Hidden (not collapsed) by default. + return EVisibility::Hidden; +} + +auto +SHoudiniToolImportPackage::GetNameErrorLabelText() const -> FText +{ + if (WidgetSwitcher->GetActiveWidgetIndex() == 0) + { + if (DirectoryPicker->GetDirectory().IsEmpty()) + { + return LOCTEXT("ImportPackage_EmptyDirectory", "Select a package directory."); + } + + if (!bIsValidPackageDirectory) + { + return LOCTEXT("ImportPackage_DirectoryNotExist", "Directory does not exist."); + } + + if (!bValidPackageName) + { + return PackageNameError; + } + } + else if (WidgetSwitcher->GetActiveWidgetIndex() == 1) + { + return LOCTEXT("ImportPackage_ExternalPackageNotExist", "External package descriptor does not exist. Create new package."); + } + + return FText(); +} + +void +SHoudiniToolImportPackage::OnDirectoryChanged(const FString& Directory) +{ + bHasPackageJSON = false; + bIsValidPackageDirectory = false; + + if (Directory.IsEmpty()) + { + return; + } + + bIsValidPackageDirectory = FPaths::DirectoryExists(Directory); + if (bIsValidPackageDirectory) + { + // Check whether the directory contains a JSON file. + const FString JSONPath = FPaths::Combine(Directory, FHoudiniToolsRuntimeUtils::GetPackageJSONName()); + if (FPaths::FileExists(JSONPath)) + { + bHasPackageJSON = true; + } + + // Populate the PackageName with the selected directory name. + FString PackageName = Directory.TrimStartAndEnd(); + if (PackageName.EndsWith("/")) + { + // Remove trailing slash + PackageName = PackageName.LeftChop(1); + } + PackageName = FPaths::GetBaseFilename(PackageName); + PackageNameEditBox->SetText(FText::FromString(PackageName)); + } +} + +void +SHoudiniToolImportPackage::OnPackageNameTextChanged(const FText& Text) +{ + UpdatePackageName(Text.ToString()); +} + +void +SHoudiniToolImportPackage::OnPackageNameTextCommitted(const FText& Text, ETextCommit::Type Arg) +{ + UpdatePackageName(Text.ToString()); +} + +FText +SHoudiniToolImportPackage::OnGetPackagePathText() const +{ + return FText::FromString( FHoudiniToolsEditor::GetDefaultPackagePath(NewPackageName) ); +} + +bool +SHoudiniToolImportPackage::IsImportEnabled() const +{ + if (!bIsValidPackageDirectory) + return false; + if (!bValidPackageName) + return false; + + if (WidgetSwitcher->GetActiveWidgetIndex() == 0) + { + // On the first page, if we have a valid package JSON, enable import button. + return bHasPackageJSON; + } + + if (WidgetSwitcher->GetActiveWidgetIndex() == 1) + { + // On the second page, TODO + return false; + } + + return false; +} + +bool +SHoudiniToolImportPackage::IsNextEnabled() const +{ + if (!bIsValidPackageDirectory) + return false; + if (!bValidPackageName) + return false; + + if (WidgetSwitcher->GetActiveWidgetIndex() == 0) + { + // On the first page, if don't have a valid package JSON, enable next button. + return !bHasPackageJSON; + } + + return false; +} + +FReply +SHoudiniToolImportPackage::HandleImportClicked() +{ + const FString PackageJSONPath = FPaths::Combine(DirectoryPicker->GetDirectory(), FHoudiniToolsRuntimeUtils::GetPackageJSONName()); + + const FString PackageName = PackageNameEditBox->GetText().ToString(); + const FString PackageDestPath = FHoudiniToolsEditor::GetDefaultPackagePath(PackageName); + UHoudiniToolsPackageAsset* ImportedPackage = FHoudiniToolsEditor::ImportExternalToolsPackage(PackageDestPath, PackageJSONPath, true); + + if (!ImportedPackage) + { + const FText Title = LOCTEXT("ImportPackage_ImportError_Title", "Import Error"); + FMessageDialog::Open(EAppMsgType::Ok, + LOCTEXT("ImportPackage_ImportError", "An error occured during import."), +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + Title +#else + &Title +#endif + ); + } + else + { + // Performing a reimport on this package will import all hdas found in the external package directory. + int NumHDAs = 0; + FHoudiniToolsEditor::ReimportPackageHDAs(ImportedPackage, false, &NumHDAs); + + if (NumHDAs > 0) + { + const FText Message = LOCTEXT("ImportPackage_ImportedHDAs", "Imported {0} HDAs."); + const FText Title = LOCTEXT("ImportPackage_ImportSuccess_Title", "Package Import Successful"); + FMessageDialog::Open(EAppMsgType::Ok, + FText::Format(Message, {NumHDAs}), +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + Title +#else + &Title +#endif + ); + } + else + { + const FText Message = LOCTEXT("ImportPackage_NoHDAs", "No HDAs to import."); + const FText Title = LOCTEXT("ImportPackage_ImportSuccess_Title", "Package Import Successful"); + FMessageDialog::Open(EAppMsgType::Ok, + FText::Format(Message, {NumHDAs}), +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + Title +#else + &Title +#endif + ); + } + + FHoudiniToolsEditor::BrowseToObjectInContentBrowser(ImportedPackage); + } + + CloseContainingWindow(); + + return FReply::Handled(); +} + +EVisibility +SHoudiniToolImportPackage::GetImportVisibility() const +{ + if (WidgetSwitcher->GetActiveWidgetIndex() == 1) + { + // We don't want to see the import button on this screen. + return EVisibility::Collapsed; + } + + return EVisibility::Visible; +} + +FReply +SHoudiniToolImportPackage::HandleNextClicked() const +{ + WidgetSwitcher->SetActiveWidgetIndex( WidgetSwitcher->GetActiveWidgetIndex() + 1 ); + + return FReply::Handled(); +} + +EVisibility +SHoudiniToolImportPackage::GetNextVisibility() const +{ + if (WidgetSwitcher->GetActiveWidgetIndex() < 1) + { + // We only want to see the create button on this screen. + return EVisibility::Visible; + } + + return EVisibility::Hidden; +} + +FReply +SHoudiniToolImportPackage::HandleCreateClicked() +{ + const FString PackageDir = FHoudiniToolsEditor::GetDefaultPackagePath(NewPackageName); + UHoudiniToolsPackageAsset* Asset = FHoudiniToolsEditor::CreateToolsPackageAsset( + PackageDir, + NewPackageName, + DirectoryPicker->GetDirectory() + ); + if (!Asset) + { + return FReply::Handled(); + } + + if (CreateExternalJSON.IsValid() && CreateExternalJSON->IsChecked()) + { + // Create a default external JSON + const FString JSONFilePath = FHoudiniToolsRuntimeUtils::GetHoudiniToolsPackageJSONPath(Asset); + FHoudiniToolsEditor::WriteDefaultJSONFile(Asset, JSONFilePath); + } + + // Import HDAs + int NumHDAs = 0; + FHoudiniToolsEditor::ReimportPackageHDAs(Asset, false, &NumHDAs); + + if (NumHDAs > 0) + { + const FText Message = LOCTEXT("ImportPackage_ImportedHDAs", "Imported {0} HDAs."); + const FText Title = LOCTEXT("ImportPackage_ImportSuccess_Title", "Package Creation Successful"); + FMessageDialog::Open(EAppMsgType::Ok, + FText::Format(Message, {NumHDAs}), +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + Title +#else + &Title +#endif + ); + } + else + { + const FText Message = LOCTEXT("ImportPackage_NoHDAs", "No HDAs to import."); + const FText Title = LOCTEXT("ImportPackage_ImportSuccess_Title", "Package Creation Successful"); + FMessageDialog::Open(EAppMsgType::Ok, + Message, +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + Title +#else + &Title +#endif + ); + } + + // Open the content browser at the newly created asset package. + FHoudiniToolsEditor::BrowseToObjectInContentBrowser(Asset); + + CloseContainingWindow(); + + return FReply::Handled(); +} + +bool +SHoudiniToolImportPackage::IsCreateEnabled() const +{ + if (WidgetSwitcher->GetActiveWidgetIndex() == 1) + { + return true; + } + + return false; +} + +EVisibility +SHoudiniToolImportPackage::GetCreateVisibility() const +{ + if (WidgetSwitcher->GetActiveWidgetIndex() == 1) + { + return EVisibility::Visible; + } + return EVisibility::Collapsed; +} + +FReply +SHoudiniToolImportPackage::HandleBackClicked() const +{ + WidgetSwitcher->SetActiveWidgetIndex( WidgetSwitcher->GetActiveWidgetIndex() - 1 ); + return FReply::Handled(); +} + +EVisibility +SHoudiniToolImportPackage::GetBackVisibility() const +{ + if (WidgetSwitcher->GetActiveWidgetIndex() >= 1) + { + return EVisibility::Visible; + } + + return EVisibility::Hidden; +} + +FReply +SHoudiniToolImportPackage::HandleCancelClicked() +{ + OnCanceled.ExecuteIfBound(); + CloseContainingWindow(); + + return FReply::Handled(); +} + +FText +SHoudiniToolImportPackage::GetCategory() const +{ + return FText::FromString(TEXT("PlaceholderCategory")); +} + +void +SHoudiniToolImportPackage::UpdatePackageName(const FString& InName) +{ + NewPackageName = InName; + + bValidPackageName = FHoudiniToolsEditor::CanCreateCreateToolsPackage(InName, &PackageNameError); + if (!bValidPackageName) + { + return; + } + + PackageNameError = FText(); +} + +void +SHoudiniToolImportPackage::CloseContainingWindow() +{ + TSharedPtr ContainingWindow = FSlateApplication::Get().FindWidgetWindow(AsShared()); + + if ( ContainingWindow.IsValid() ) + { + ContainingWindow->RequestDestroyWindow(); + } +} + + +SHoudiniToolAddToUserCategory::SHoudiniToolAddToUserCategory() +{ +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void +SHoudiniToolAddToUserCategory::Construct(const FArguments& InArgs) +{ + const FText DialogTitle = LOCTEXT( "HoudiniTool_AddToUserCategory_Title", "Add Tool To User Category" ); + + ActiveTool = InArgs._ActiveTool.Get(); + checkf(ActiveTool.IsValid(), TEXT("Required a valid FHoudiniTool.")); + + Package = ActiveTool->ToolsPackage.LoadSynchronous(); + checkf(IsValid(Package), TEXT("FHoudiniTools must have a valid owning package.")); + + // Capture user categories + TArray Categories; + FHoudiniToolsEditor::GetUserCategoriesList(Categories); + for(const FString& CategoryName : Categories) + { + UserCategories.Add( MakeShareable(new FString(CategoryName)) ); + } + + int row = 0; + + // First view + const int RowToolName = row++; + const int RowPackagePath = row++; + const int RowUserCategorySelection = row++; + + // Second screen + row = 0; + + this->ChildSlot + [ + SNew(SBorder) + .Padding(18.0f) + .BorderImage( _GetBrush("Docking.Tab.ContentAreaBrush") ) + [ + SNew(SBorder) + .BorderImage(FAppStyle::Get().GetBrush("Brushes.Panel")) + .Padding(10.0f) + [ + // Vertical container for the widget switcher and row of action buttons + SNew(SVerticalBox) + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + .AutoHeight() + + // -------------------------------- + // Content Switcher Row + // -------------------------------- + + + SVerticalBox::Slot() + [ + SAssignNew(WidgetSwitcher, SWidgetSwitcher) + + // -------------------------------- + // Add To Category View + // -------------------------------- + + + SWidgetSwitcher::Slot() + [ + SAssignNew(AddToCategoryView, SVerticalBox) + + // ----------------------------- + // Title + // ----------------------------- + + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + .AutoHeight() + [ + SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("HeadingExtraSmall")) + .Text( DialogTitle ) + .TransformPolicy(ETextTransformPolicy::ToUpper) + ] + + // ----------------------------- + // Description + // ----------------------------- + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 10.0f) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .AutoWrapText(true) + .Text(LOCTEXT("HoudiniTool_AddToUserCategory_AddToCategoryDescription1", "Select the category to which the active tool should be added.")) + ] + // +SVerticalBox::Slot() + // .AutoHeight() + // [ + // SNew(STextBlock) + // .AutoWrapText(true) + // .Text(LOCTEXT("HoudiniTool_AddToUserCategory_AddToCategoryDescription2", "Enter a name for your new Houdini Tools package. Package names may only contain alphanumeric characters, and may not contain a space.")) + // ] + ] + + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBorder) + .BorderImage(_GetBrush("DetailsView.CategoryTop")) + .BorderBackgroundColor(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f )) + .Padding(FMargin(6.0f, 4.0f, 7.0f, 4.0f)) + [ + SNew(SGridPanel) + .FillColumn(1, 1.0f) + + //-------------------------- + // Tool Name + //-------------------------- + + + SGridPanel::Slot(0, RowToolName) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniTool_AddToUserCategory_ToolName", "Tool Name")) + ] + + SGridPanel::Slot(1, RowToolName) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SNew(STextBlock) + .Text( ActiveTool->Name ) + .ToolTipText(LOCTEXT("HoudiniTool_AddToUserCategory_NameTooltip", "Location of the package containing the active tool.")) + ] + + //-------------------------- + // Houdini Tools package location + //-------------------------- + + + SGridPanel::Slot(0, RowPackagePath) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniTool_AddToUserCategory_PackagePath", "Package Path")) + ] + + SGridPanel::Slot(1, RowPackagePath) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SNew(STextBlock) + .Text(SHoudiniToolAddToUserCategory::ToolPackagePathText()) + ] + + //-------------------------- + // Category Selection + //-------------------------- + + + SGridPanel::Slot(0, RowUserCategorySelection) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniTool_AddToUserCategory_UserCategory", "User Category")) + ] + + SGridPanel::Slot(1, RowUserCategorySelection) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SNew(SComboBox>) + .OptionsSource(&UserCategories) + .OnGenerateWidget_Lambda([](const TSharedPtr& InItem) + { + return SNew(STextBlock).Text(FText::FromString(*InItem)); + }) + .OnSelectionChanged(this, &SHoudiniToolAddToUserCategory::OnUserCategorySelectionChanged) + [ + SNew(STextBlock) + .Text_Lambda([&]() + { + if (SelectedCategory.IsValid()) + { + return FText::FromString(*SelectedCategory); + } + return FText(LOCTEXT("HoudiniTool_AddToUserCategory_SelectCategory", "-- Select User Category --")); + }) + ] + ] + ] + ] // VerticalBox Slot + ] // Add To Category View + + // -------------------------------- + // Create User Category View + // -------------------------------- + + + SWidgetSwitcher::Slot() + [ + SAssignNew(CreateCategoryView, SVerticalBox) + + // ----------------------------- + // Title + // ----------------------------- + + + SVerticalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + .AutoHeight() + [ + SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("HeadingExtraSmall")) + .Text( DialogTitle ) + .TransformPolicy(ETextTransformPolicy::ToUpper) + ] + + // ----------------------------- + // Description + // ----------------------------- + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 10.0f) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .AutoWrapText(true) + .Text(LOCTEXT("HoudiniTool_AddToUserCategory_CreateCategoryDescription1", "Enter the new user category name.")) + ] + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .AutoWrapText(true) + .Text(LOCTEXT("HoudiniTool_AddToUserCategory_CreateCategoryDescription2", "If the category exists, the tool will be added to the existing category. If the category doesn't exist, it will be created before adding the tool to the new category.")) + ] + ] + + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBorder) + .BorderImage(_GetBrush("DetailsView.CategoryTop")) + .BorderBackgroundColor(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f )) + .Padding(FMargin(6.0f, 4.0f, 7.0f, 4.0f)) + [ + SNew(SGridPanel) + .FillColumn(1, 1.0f) + + //-------------------------- + // Tool Name + //-------------------------- + + + SGridPanel::Slot(0, RowToolName) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniTool_AddToUserCategory_ToolName", "Tool Name")) + ] + + SGridPanel::Slot(1, RowToolName) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SNew(STextBlock) + .Text( ActiveTool->Name ) + .ToolTipText(LOCTEXT("HoudiniTool_AddToUserCategory_NameTooltip", "Location of the package containing the active tool.")) + ] + + //-------------------------- + // Houdini Tools package location + //-------------------------- + + + SGridPanel::Slot(0, RowPackagePath) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniTool_AddToUserCategory_PackagePath", "Package Path")) + ] + + SGridPanel::Slot(1, RowPackagePath) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SNew(STextBlock) + .Text(SHoudiniToolAddToUserCategory::ToolPackagePathText()) + ] + + //-------------------------- + // Category Creation + //-------------------------- + + + SGridPanel::Slot(0, RowUserCategorySelection) + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniTool_AddToUserCategory_UserCategory", "User Category")) + ] + + SGridPanel::Slot(1, RowUserCategorySelection) + .VAlign(VAlign_Center) + .Padding(0.f, 3.0f) + [ + SNew(SEditableTextBox) + .OnTextChanged(this, &SHoudiniToolAddToUserCategory::OnUserCategoryCreateTextChanged) + .OnTextCommitted(this, &SHoudiniToolAddToUserCategory::OnUserCategoryCreateTextCommitted) + ] + ] + ] // VerticalBox Slot + + ] // Create User Category View + ] // Content Switcher Row + + // -------------------------------- + // Action Buttons row + // -------------------------------- + + + SVerticalBox::Slot() + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Right) + .FillHeight(1.0f) + [ + SNew(SUniformGridPanel) + .SlotPadding(FCoreStyle::Get().GetMargin("StandardDialog.SlotPadding")) + .MinDesiredSlotWidth(FCoreStyle::Get().GetFloat("StandardDialog.MinDesiredSlotWidth")) + .MinDesiredSlotHeight(FCoreStyle::Get().GetFloat("StandardDialog.MinDesiredSlotHeight")) + + // Back Button + + SUniformGridPanel::Slot(0, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .ButtonStyle(&FAppStyle::Get().GetWidgetStyle("Button")) + .OnClicked(this, &SHoudiniToolAddToUserCategory::HandleBackClicked) + .Visibility(this, &SHoudiniToolAddToUserCategory::GetBackButtonVisibility) + [ + SNew(STextBlock) + .Text(LOCTEXT( "HoudiniTool_AddToUserCategory_BackButtonLabel", "Back" )) + ] + ] + + // Add to Selected Category + + SUniformGridPanel::Slot(0, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .ButtonStyle(&FAppStyle::Get().GetWidgetStyle("PrimaryButton")) + .OnClicked(this, &SHoudiniToolAddToUserCategory::HandleAddToCategoryClicked) + .IsEnabled(this, &SHoudiniToolAddToUserCategory::GetAddToCategoryEnabled) + .Visibility(this, &SHoudiniToolAddToUserCategory::GetAddToCategoryVisibility) + [ + SNew(STextBlock) + .Text(LOCTEXT( "HoudiniTool_AddToUserCategory_AddToCategoryLabel", "Add" )) + ] + ] + + // Goto New Category + + SUniformGridPanel::Slot(1, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .ButtonStyle(&FAppStyle::Get().GetWidgetStyle("Button")) + .OnClicked(this, &SHoudiniToolAddToUserCategory::HandleGotoCreateCategoryClicked) + .IsEnabled(this, &SHoudiniToolAddToUserCategory::GetGotoCreateCategoryEnabled) + .Visibility(this, &SHoudiniToolAddToUserCategory::GetGotoCreateCategoryVisibility) + [ + SNew(STextBlock) + .Text(LOCTEXT( "HoudiniTool_AddToUserCategory_GotoNewCategoryLabel", "New Category" )) + ] + ] + + // Create and Add Category Button + + SUniformGridPanel::Slot(1, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .ButtonStyle(&FAppStyle::Get().GetWidgetStyle("PrimaryButton")) + .OnClicked(this, &SHoudiniToolAddToUserCategory::HandleAddNewCategoryClicked) + .IsEnabled(this, &SHoudiniToolAddToUserCategory::GetCreateCategoryEnabled) + .Visibility(this, &SHoudiniToolAddToUserCategory::GetCreateCategoryVisibility) + [ + SNew(STextBlock) + .Text(LOCTEXT( "HoudiniTool_AddToUserCategory_CreateButtonLabel", "Create And Add" )) + // .ToolTip(LOCTEXT( "ImportPackage_CreateButtonLabelTooltip", "Create the user category and add the tool to the category." )) + ] + ] + + + // Cancel Button + + SUniformGridPanel::Slot(2, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .OnClicked(this, &SHoudiniToolAddToUserCategory::HandleCancelClicked) + [ + SNew(STextBlock) + .Text(LOCTEXT( "HoudiniTool_AddToUserCategory_CancelButtonLabel", "Cancel" )) + ] + ] + ] // Action Buttons Row + + ] + ] + ]; // this->ChildSlot +} + +FText SHoudiniToolAddToUserCategory::ToolPackagePathText() +{ + const FString ToolsPackagePath = FPaths::GetPath(Package->GetPackage()->GetPathName()); + return FText::FromString(ToolsPackagePath); +} + +FReply SHoudiniToolAddToUserCategory::HandleBackClicked() +{ + WidgetSwitcher->SetActiveWidgetIndex( WidgetSwitcher->GetActiveWidgetIndex() - 1 ); + return FReply::Handled(); +} + +FReply SHoudiniToolAddToUserCategory::HandleAddToCategoryClicked() +{ + UHoudiniAsset* HoudiniAsset = ActiveTool->HoudiniAsset.LoadSynchronous(); + if (!IsValid(HoudiniAsset)) + { + HOUDINI_LOG_ERROR(TEXT("Cannot add tool to category. HoudiniAsset is invalid.")); + return FReply::Handled(); + } + + if (!SelectedCategory.IsValid()) + { + HOUDINI_LOG_ERROR(TEXT("Cannot add tool to category. Selected Category is invalid.")); + return FReply::Handled(); + } + + if (ActiveTool->PackageToolType == EHoudiniPackageToolType::Preset) + { + UHoudiniPreset* Preset = ActiveTool->HoudiniPreset.LoadSynchronous(); + if (IsValid(Preset)) + { + FHoudiniToolsEditor::AddToolToUserCategory(Preset, *SelectedCategory); + } + } + else + { + FHoudiniToolsEditor::AddToolToUserCategory(HoudiniAsset, *SelectedCategory); + } + + CloseContainingWindow(); + + return FReply::Handled(); +} + +bool SHoudiniToolAddToUserCategory::GetAddToCategoryEnabled() const +{ + return UserCategories.Contains(SelectedCategory); +} + +EVisibility +SHoudiniToolAddToUserCategory::GetAddToCategoryVisibility() const +{ + if (WidgetSwitcher->GetActiveWidgetIndex() == 0) + { + // First Screen + return EVisibility::Visible; + } + + return EVisibility::Collapsed; +} + +FReply +SHoudiniToolAddToUserCategory::HandleGotoCreateCategoryClicked() +{ + // Goto Create New Category page + WidgetSwitcher->SetActiveWidgetIndex(1); + return FReply::Handled(); +} + +bool +SHoudiniToolAddToUserCategory::GetGotoCreateCategoryEnabled() const +{ + if (WidgetSwitcher->GetActiveWidgetIndex() == 0) + { + // First Screen + return true; + } + + return false; +} + +EVisibility +SHoudiniToolAddToUserCategory::GetGotoCreateCategoryVisibility() const +{ + if (WidgetSwitcher->GetActiveWidgetIndex() == 0) + { + // First Screen + return EVisibility::Visible; + } + + return EVisibility::Collapsed; +} + +FReply +SHoudiniToolAddToUserCategory::HandleAddNewCategoryClicked() +{ + UHoudiniAsset* HoudiniAsset = ActiveTool->HoudiniAsset.LoadSynchronous(); + if (!IsValid(HoudiniAsset)) + { + HOUDINI_LOG_ERROR(TEXT("Cannot add tool to category. HoudiniAsset is invalid.")); + return FReply::Handled(); + } + + FHoudiniToolsEditor::AddToolToUserCategory(HoudiniAsset, UserCategoryToCreate); + + // const FText Title = LOCTEXT("HoudiniTools_AddToUserCategory_Title", "Created User Category."); + // const FText Message = LOCTEXT("HoudiniTools_AddToUserCategory_Message", "Added '{0}' to new '{1}' category."); + // + // + // FMessageDialog::Open(EAppMsgType::Ok, + // FText::Format( Message, ActiveTool->Name, FText::FromString(UserCategoryToCreate)), + // &Title + // ); + + CloseContainingWindow(); + return FReply::Handled(); +} + +bool +SHoudiniToolAddToUserCategory::GetCreateCategoryEnabled() const +{ + return !UserCategoryToCreate.IsEmpty(); +} + +EVisibility +SHoudiniToolAddToUserCategory::GetCreateCategoryVisibility() const +{ + if (WidgetSwitcher->GetActiveWidgetIndex() == 1) + { + // Second Screen + return EVisibility::Visible; + } + + return EVisibility::Collapsed; +} + +EVisibility +SHoudiniToolAddToUserCategory::GetBackButtonVisibility() const +{ + if (WidgetSwitcher->GetActiveWidgetIndex() == 1) + { + // Second Screen + return EVisibility::Visible; + } + + return EVisibility::Collapsed; +} + +FReply +SHoudiniToolAddToUserCategory::HandleCancelClicked() +{ + CloseContainingWindow(); + + return FReply::Handled(); +} + +void SHoudiniToolAddToUserCategory::OnUserCategorySelectionChanged(TSharedPtr NewSelection, + ESelectInfo::Type SelectType) +{ + SelectedCategory = NewSelection; +} + +void SHoudiniToolAddToUserCategory::OnUserCategoryCreateTextChanged(const FText& NewText) +{ + UserCategoryToCreate = NewText.ToString(); +} + +void SHoudiniToolAddToUserCategory::OnUserCategoryCreateTextCommitted(const FText& NewText, + ETextCommit::Type CommitType) +{ + UserCategoryToCreate = NewText.ToString(); +} + +void SHoudiniToolAddToUserCategory::CloseContainingWindow() +{ + TSharedPtr ContainingWindow = FSlateApplication::Get().FindWidgetWindow(AsShared()); + + if ( ContainingWindow.IsValid() ) + { + ContainingWindow->RequestDestroyWindow(); + } +} + +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +SHoudiniToolsPanel::SHoudiniToolsPanel() + : bRefreshPanelRequested(false) + , bFilterShowAll(false) + , bShowEmptyCategories(true) + , bShowHiddenTools(false) + , bAutoRefresh(true) + , bShowHoudiniAssets(true) + , bShowPresets(true) + , ViewMode(EHoudiniToolsViewMode::TileView) +{ +} + +#define UNSUBSCRIBE(Delegate, Handle) \ + if ((Handle).IsValid()) { (Delegate).Remove(Handle); } + +SHoudiniToolsPanel::~SHoudiniToolsPanel() +{ + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + + // Unsubscribe from delegates + UNSUBSCRIBE(AssetRegistryModule.Get().OnInMemoryAssetCreated(), AssetMemCreatedHandle); + UNSUBSCRIBE(AssetRegistryModule.Get().OnInMemoryAssetDeleted(), AssetMemDeletedHandle); + UNSUBSCRIBE(AssetRegistryModule.Get().OnAssetAdded(), AssetAddedHandle); + UNSUBSCRIBE(AssetRegistryModule.Get().OnAssetRenamed(), AssetRenamedHandle); + UNSUBSCRIBE(AssetRegistryModule.Get().OnAssetUpdated(), AssetUpdatedHandle); + UNSUBSCRIBE(AssetRegistryModule.Get().OnAssetUpdatedOnDisk(), AssetUpdatedOnDiskHandle); + + UHoudiniEngineEditorSettings* HoudiniEngineEditorSettings = const_cast(GetDefault()); + if (HoudiniEngineEditorSettings) + { + UNSUBSCRIBE(HoudiniEngineEditorSettings->OnUserToolCategoriesChanged, UserToolCategoriesChangedHandle); + } + +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void +SHoudiniToolsPanel::Construct( const FArguments& InArgs ) +{ + LoadConfig(); + + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + FHoudiniEngineEditor& HoudiniEngineEditor = FModuleManager::GetModuleChecked("HoudiniEngineEditor"); + FHoudiniEngineRuntime& HoudiniEngineRuntime = FModuleManager::GetModuleChecked("HoudiniEngineRuntime"); + // FHoudiniToolsEditor& HoudiniTools = FHoudiniToolsPanelUtils::GetHoudiniTools(); + + // Handler to trigger UI updates on asset changes. + auto AssetChangedHandlerFn = [this](UObject* InObject) + { + if (!bAutoRefresh) + return; + if (!InObject) + return; + if ( InObject->IsA() || InObject->IsA() || InObject->IsA() ) + { + HandleToolChanged(); + } + }; + + AssetMemCreatedHandle = AssetRegistryModule.Get().OnInMemoryAssetCreated().AddLambda(AssetChangedHandlerFn); + AssetMemDeletedHandle = AssetRegistryModule.Get().OnInMemoryAssetDeleted().AddLambda(AssetChangedHandlerFn); + + // Handler for asset imports. PostImport event is broadcast on both imports and reimports. + AssetPostImportHandle = GEditor->GetEditorSubsystem()->OnAssetReimport.AddLambda(AssetChangedHandlerFn); + + // Handler for asset renames + AssetRenamedHandle = AssetRegistryModule.Get().OnAssetRenamed().AddLambda([this](const FAssetData& AssetData, const FString& AssetName) + { + if (!bAutoRefresh) + return; + const UClass* AssetClass = AssetData.GetClass(); + if (!AssetClass) + return; + if (AssetClass->IsChildOf(UHoudiniAsset::StaticClass()) || + AssetClass->IsChildOf(UHoudiniToolsPackageAsset::StaticClass()) || + AssetClass->IsChildOf(UHoudiniPreset::StaticClass()) + ) + { + HandleToolChanged(); + } + }); + + auto AssetUpdatedHandlerFn = [this](const FAssetData& AssetData) + { + if (!bAutoRefresh) + return; + const UClass* AssetClass = AssetData.GetClass(); + if (!AssetClass) + return; + if (AssetClass->IsChildOf(UHoudiniAsset::StaticClass()) || + AssetClass->IsChildOf(UHoudiniToolsPackageAsset::StaticClass()) || + AssetClass->IsChildOf(UHoudiniPreset::StaticClass()) + ) + { + HandleToolChanged(); + } + }; + + // Handlers for asset creation and updates. + AssetAddedHandle = AssetRegistryModule.Get().OnAssetAdded().AddLambda(AssetUpdatedHandlerFn); + AssetUpdatedHandle = AssetRegistryModule.Get().OnAssetUpdated().AddLambda(AssetUpdatedHandlerFn); + AssetUpdatedOnDiskHandle = AssetRegistryModule.Get().OnAssetUpdatedOnDisk().AddLambda(AssetUpdatedHandlerFn); + + // Handler for HoudiniEngineEditorSettings changes + UHoudiniEngineEditorSettings* HoudiniEngineEditorSettings = GetMutableDefault(); + if (HoudiniEngineEditorSettings) + { + UserToolCategoriesChangedHandle = HoudiniEngineEditorSettings->OnUserToolCategoriesChanged.AddLambda([&]() + { + RequestPanelRefresh(); + }); + } + + // Handler for tool or package change broadcasts + ToolOrPackageChangedHandle = HoudiniEngineRuntime.GetOnToolOrPackageChangedEvent().AddLambda([&]() + { + RequestPanelRefresh(); + }); + + UpdateHoudiniToolDirectories(); + + CategoriesContainer = SNew(SVerticalBox); + + RebuildCategories(); + + TSharedPtr OptionsImage = SNew(SLayeredImage) + .Image(FAppStyle::Get().GetBrush("DetailsView.ViewOptions")) + .ColorAndOpacity(FSlateColor::UseForeground()); + + TSharedPtr FilterImage = SNew(SLayeredImage) + .Image(FAppStyle::Get().GetBrush("Icons.Filter")) + .ColorAndOpacity(FSlateColor::UseForeground()); + + ChildSlot + [ + SNew(SBorder) + .BorderImage(FAppStyle::Get().GetBrush("Brushes.Panel")) + [ + SNew(SVerticalBox) + + // Toolbar + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5,5) + [ + SNew(SHorizontalBox) + + // Search Box + + + SHorizontalBox::Slot() + .FillWidth(1.f) + [ + SNew(SSearchBox) + .OnTextChanged_Lambda([this](const FText& NewFilterString) -> void + { + // Handle Filter Text Changes. + if (NewFilterString.ToString() != FilterString) + { + // Search filter changed. Update categories. + FilterString = NewFilterString.ToString(); + ForEachCategory([this](SHoudiniToolCategory* Category) -> bool + { + Category->SetFilterString(FilterString); + return true; + }); + } + }) + ] // Search Box + + // Filter Menu Button + + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SComboButton) + .HasDownArrow(false) + .ContentPadding(0) + .ForegroundColor( FSlateColor::UseForeground() ) + .ButtonStyle( FAppStyle::Get(), "SimpleButton" ) + .AddMetaData(FTagMetaData(TEXT("ViewOptions"))) + .OnGetMenuContent_Lambda([&]() + { + // This menu widget will be rebuilt dynamically when the menu is needed + return SAssignNew(CategoryFilterWidget, SHoudiniToolsCategoryFilter) + .ShowAll_Lambda([this]() { return bFilterShowAll; }) + .CategoriesSource(&FilterCategoryList) + .HiddenCategoriesSource(&FilterHiddenCategories) + .OnShowAllChanged_Lambda([this](bool NewShowAll) + { + bFilterShowAll = NewShowAll; + if (bFilterShowAll) + { + FilterHiddenCategories.Empty(); + } + else + { + // Set all categories as hidden + ForEachCategory([&](SHoudiniToolCategory* Category) -> bool + { + if (Category) + { + FilterHiddenCategories.Add(Category->GetCategoryLabel()); + } + return true; + }); + } + SaveConfig(); + RequestPanelRefresh(); + }) + .OnCategoryStateChanged_Lambda([this] (const FString& CategoryName, const bool bIsEnabled) + { + if (bIsEnabled) + { + FilterHiddenCategories.Remove(CategoryName); + } + else + { + FilterHiddenCategories.Add(CategoryName); + } + SaveConfig(); + RequestPanelRefresh(); + }); + }) + .ButtonContent() + [ + FilterImage.ToSharedRef() + ] + ] // Filter menu button + + // Action Menu Button + + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SComboButton) + .HasDownArrow(false) + .ContentPadding(0) + .ForegroundColor( FSlateColor::UseForeground() ) + .ButtonStyle( FAppStyle::Get(), "SimpleButton" ) + .AddMetaData(FTagMetaData(TEXT("ViewOptions"))) + .MenuContent() + [ + ConstructHoudiniToolsActionMenu().ToSharedRef() + ] + .ButtonContent() + [ + OptionsImage.ToSharedRef() + ] + ] + ] + + // Categories Container + + + SVerticalBox::Slot() + .FillHeight(1.0) + [ + SNew(SBorder) + .BorderImage(FAppStyle::Get().GetBrush("Brushes.Recessed")) + .Visibility_Lambda([this]() -> EVisibility + { + // Only show the categories panel if we have categories + if (CategoriesContainer.IsValid() && CategoriesContainer->NumSlots() > 0) + { + return EVisibility::Visible; + } + return EVisibility::Collapsed; + }) + [ + SNew(SScrollBox) + .ScrollBarAlwaysVisible(true) + + SScrollBox::Slot() + [ + CategoriesContainer.ToSharedRef() // vertical box + ] + ] + ] + + + SVerticalBox::Slot() + .FillHeight(1.0) + [ + SNew(SBorder) + .BorderImage(FAppStyle::Get().GetBrush("Brushes.Recessed")) + .Visibility_Lambda([this]() -> EVisibility + { + // Show the "Import SideFX Tools" panel only if we don't have any existing categories. + if (CategoriesContainer.IsValid() && CategoriesContainer->NumSlots() == 0) + { + return EVisibility::Visible; + } + return EVisibility::Collapsed; + }) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .FillHeight(1.f) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("HoudiniTools_ImportSideFXToolsPanel_Description", "No tools here yet! Import default SideFX Tools?")) + ] + + SVerticalBox::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .Padding(FMargin(0.f, 10.f)) + .AutoHeight() + [ + SNew(SButton) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) + .ButtonStyle(&_GetEditorStyle().GetWidgetStyle("PrimaryButton")) + .OnClicked_Lambda([&]() -> FReply + { + HandleImportSideFXTools(); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(LOCTEXT( "ImportPackage_CreateButtonLabel", "Import SideFX Tools" )) + ] + ] + ] + ] + ] + ] + ]; +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +TSharedRef< ITableRow > +SHoudiniToolsPanel::MakeListViewWidget( TSharedPtr< FHoudiniTool > HoudiniTool, const FString& CategoryName, const TSharedRef< STableViewBase >& OwnerTable ) +{ + check( HoudiniTool.IsValid() ); + + auto DefaultTool = FHoudiniEngineStyle::Get()->GetBrush( "HoudiniEngine.HoudiniEngineLogo"); + // TSharedPtr< SButton > HelpButton; + TSharedPtr< SImage > DefaultToolImage; + + // Building the tool's tooltip + FString HoudiniToolTip; + if (HoudiniTool->PackageToolType == EHoudiniPackageToolType::Preset) + { + // HoudiniToolTip += HoudiniTool->Name.ToString() + TEXT(" (Preset) ") + TEXT( "\n\n" ); + HoudiniToolTip += HoudiniTool->Name.ToString() + TEXT(" - Preset") + TEXT( "\n\n" ); + if ( HoudiniTool->HoudiniPreset.IsValid() ) + HoudiniToolTip += HoudiniTool->HoudiniPreset.ToSoftObjectPath().ToString() /*->AssetFileName */+ TEXT( "\n\n" ); + } + else + { + HoudiniToolTip += HoudiniTool->Name.ToString() + TEXT(" - Houdini Asset") + TEXT( "\n\n" ); + if ( HoudiniTool->HoudiniAsset.IsValid() ) + HoudiniToolTip += HoudiniTool->HoudiniAsset.ToSoftObjectPath().ToString() /*->AssetFileName */+ TEXT( "\n\n" ); + } + + // if ( HoudiniTool->HoudiniAsset.IsValid() ) + // HoudiniToolTip += HoudiniTool->HoudiniAsset.ToSoftObjectPath().ToString() /*->AssetFileName */+ TEXT( "\n\n" ); + if ( !HoudiniTool->ToolTipText.IsEmpty() ) + HoudiniToolTip += HoudiniTool->ToolTipText.ToString() + TEXT("\n\n"); + + // Adding a description of the tools behavior deriving from its type + switch ( HoudiniTool->Type ) + { + case EHoudiniToolType::HTOOLTYPE_OPERATOR_SINGLE: + HoudiniToolTip += TEXT("Operator (Single):\nDouble clicking on this tool will instantiate the asset in the world.\nThe current selection will be automatically assigned to the asset's first input.\n" ); + break; + case EHoudiniToolType::HTOOLTYPE_OPERATOR_MULTI: + HoudiniToolTip += TEXT("Operator (Multiple):\nDouble clicking on this tool will instantiate the asset in the world.\nEach of the selected object will be assigned to one of the asset's input (object1 to input1, object2 to input2 etc.)\n" ); + break; + case EHoudiniToolType::HTOOLTYPE_OPERATOR_BATCH: + HoudiniToolTip += TEXT("Operator (Batch):\nDouble clicking on this tool will instantiate the asset in the world.\nAn instance of the asset will be created for each of the selected object, and the asset's first input will be set to that object.\n"); + break; + case EHoudiniToolType::HTOOLTYPE_GENERATOR: + default: + HoudiniToolTip += TEXT("Generator:\nDouble clicking on this tool will instantiate the asset in the world.\n"); + break; + } + + // Add a description from the tools selection type + if ( HoudiniTool->Type != EHoudiniToolType::HTOOLTYPE_GENERATOR ) + { + switch ( HoudiniTool->SelectionType ) + { + case EHoudiniToolSelectionType::HTOOL_SELECTION_ALL: + HoudiniToolTip += TEXT("\nBoth Content Browser and World Outliner selection will be considered.\nIf objects are selected in the content browser, the world selection will be ignored.\n"); + break; + + case EHoudiniToolSelectionType::HTOOL_SELECTION_WORLD_ONLY: + HoudiniToolTip += TEXT("\nOnly World Outliner selection will be considered.\n"); + break; + + case EHoudiniToolSelectionType::HTOOL_SELECTION_CB_ONLY: + HoudiniToolTip += TEXT("\nOnly Content Browser selection will be considered.\n"); + break; + } + } + + if ( HoudiniTool->Type != EHoudiniToolType::HTOOLTYPE_OPERATOR_BATCH ) + { + if ( HoudiniTool->SelectionType != EHoudiniToolSelectionType::HTOOL_SELECTION_CB_ONLY ) + HoudiniToolTip += TEXT( "If objects are selected in the world outliner, the asset's transform will default to the mean Transform of the select objects.\n" ); + } + else + { + if ( HoudiniTool->SelectionType != EHoudiniToolSelectionType::HTOOL_SELECTION_CB_ONLY ) + HoudiniToolTip += TEXT( "If objects are selected in the world outliner, each created asset will use its object transform.\n" ); + } + + FText ToolTipText = FText::FromString( HoudiniToolTip ); + + // Build the Help Tooltip + FString HelpURL = HoudiniTool->HelpURL; + bool HasHelp = HelpURL.Len() > 0; + FText HelpTip = HasHelp ? FText::Format( LOCTEXT( "OpenHelp", "Click to view tool help: {0}" ), FText::FromString( HelpURL ) ) : HoudiniTool->Name; + + // // Build the "DefaultTool" tool tip + // bool IsDefault = HoudiniTool->DefaultTool; + // FText DefaultToolText = FText::FromString( TEXT("Default Houdini Engine for Unreal plug-in tool") ); + + TSharedRef< STableRow> > TableRowWidget = + SNew( STableRow>, OwnerTable ) + .Style( FHoudiniEngineStyle::Get(), "HoudiniEngine.TableRow" ) + .OnDragDetected( this, &SHoudiniToolsPanel::OnDraggingListViewWidget ); + + float IconOpacity = 1.f; + + if (HoudiniTool->IsHiddenInCategory(CategoryName)) + { + IconOpacity = 0.5f; + } + + TSharedPtr< SHorizontalBox > ContentBox; + TSharedRef Content = + SNew( SBorder ) + .BorderImage( FCoreStyle::Get().GetBrush( "NoBorder" ) ) + .Padding( 0 ) + .ToolTip( SNew( SToolTip ).Text( ToolTipText ) ) + .Cursor( EMouseCursor::GrabHand ) + .ColorAndOpacity(FLinearColor(1.f, 1.f, 1.f, IconOpacity)) + [ SAssignNew( ContentBox, SHorizontalBox ) ]; + + constexpr float IconWidth = 36.f; + + // Tool Icon + const FSlateBrush* ToolIcon = nullptr; + if (HoudiniTool->Icon) + { + ToolIcon = HoudiniTool->Icon.Get(); + } + else + { + ToolIcon = FHoudiniEngineStyle::Get()->GetBrush( FName(FHoudiniToolsEditor::GetDefaultHoudiniToolIconBrushName()) ); + } + + TSharedPtr IconOverlay = SNew(SOverlay); + + IconOverlay->AddSlot().AttachWidget( + SNew( SBorder ) + .BorderImage( FHoudiniEngineStyle::Get()->GetBrush( "HoudiniEngine.ThumbnailBackground" ) ) + .HAlign( HAlign_Center ) + .VAlign( VAlign_Center ) + [ + SNew( SImage ) + .Image( ToolIcon ) + .ToolTip( SNew( SToolTip ).Text( ToolTipText ) ) + ] + ); + + UnderlineBrush.DrawAs = ESlateBrushDrawType::Box; + UnderlineBrush.ImageSize = FVector2D(2.f, 2.f); + + FSlateColor UnderlineColor; + + if (HoudiniTool->PackageToolType == EHoudiniPackageToolType::Preset) + { + UnderlineColor = FSlateColor(FLinearColor(0.0f, 0.3f, 1.0f, 1.f)); + } + else + { + // Default (HoudiniAsset) color + UnderlineColor = FSlateColor(FLinearColor(1.0f, 0.25f, 0.0f, 1.f)); + } + + + // Add icon underline using UnderlineColor + IconOverlay->AddSlot() + .HAlign(HAlign_Fill) + .VAlign(VAlign_Bottom) + .Padding(FMargin(0.0f, 0.0f, 0.0f, 0.0f)) + .AttachWidget( + SNew(SImage) + .Image(&UnderlineBrush) + .ColorAndOpacity( UnderlineColor ) + .DesiredSizeOverride(FVector2D(10.0f, 2.f)) + ); + + ContentBox->AddSlot() + .AutoWidth() + [ + SNew( SBorder ) + .Padding( 4.0f ) + .BorderImage( FHoudiniEngineStyle::Get()->GetBrush( "HoudiniEngine.ThumbnailShadow" ) ) + [ + SNew( SBox ) + .WidthOverride( IconWidth ) + .HeightOverride( IconWidth ) + [ + IconOverlay.ToSharedRef() + // SNew( SBorder ) + // .BorderImage( FHoudiniEngineStyle::Get()->GetBrush( "HoudiniEngine.ThumbnailBackground" ) ) + // .HAlign( HAlign_Center ) + // .VAlign( VAlign_Center ) + // [ + // SNew( SImage ) + // .Image_Lambda([ToolIcon]()->const FSlateBrush* + // { + // if (ToolIcon.IsValid()) + // { + // return ToolIcon.Get(); + // } + // else + // { + // return FHoudiniEngineStyle::Get()->GetBrush( TEXT( "HoudiniEngine.HoudiniEngineLogo40" ) ); + // } + // }) + // // .Image( ToolIcon ) + // .ToolTip( SNew( SToolTip ).Text( ToolTipText ) ) + // ] + ] + ] + ]; + + // Tool name + tooltip + ContentBox->AddSlot() + .HAlign( HAlign_Left ) + .VAlign( VAlign_Center ) + .FillWidth( 1.f ) + [ + SNew( STextBlock ) + .TextStyle( FHoudiniEngineStyle::Get(), "HoudiniEngine.ThumbnailText" ) + .HighlightText_Lambda([&]() + { + if (FilterString.Len() > 0) + { + return FText::FromString(FilterString); + } + return FText(); + }) + .Text( HoudiniTool->Name ) + .ToolTip( SNew( SToolTip ).Text( ToolTipText ) ) + ]; + + if ( HasHelp ) + { + // const FButtonStyle* BtnStyle = &FHoudiniEngineStyle::Get()->GetWidgetStyle("HoudiniEngine.HelpButton"); + + ContentBox->AddSlot() + .VAlign( VAlign_Center ) + .AutoWidth() + [ + // Help Button + + SNew( SButton ) + .ContentPadding( 0 ) + .ButtonStyle( FHoudiniEngineStyle::Get(), "HoudiniEngine.HelpButton" ) + // .ButtonStyle( &DGB_HelpButtonStyle ) + .OnClicked_Lambda( [ &, HelpURL ]() { + if ( HelpURL.Len() ) + { + FPlatformProcess::LaunchURL( *HelpURL, nullptr, nullptr ); + } + return FReply::Handled(); + } ) + .HAlign( HAlign_Center ) + .VAlign( VAlign_Center ) + .ToolTip( SNew( SToolTip ).Text( HelpTip ) ) + [ + // Help Button Image + + // At the time of writing, the UE help icon looks better than the Houdini Engine help icon. + SNew( SImage ) + .Image( _GetBrush("Icons.Help") ) + // .Image( FHoudiniEngineStyle::Get()->GetBrush("HoudiniEngine.AssetHelp") ) + ] + ]; + } + + TableRowWidget->SetContent( Content ); + + return TableRowWidget; +} + +TSharedRef< ITableRow > +SHoudiniToolsPanel::MakeTileViewWidget( TSharedPtr< FHoudiniTool > HoudiniTool, const FString& CategoryName, const TSharedRef< STableViewBase >& OwnerTable ) +{ + check( HoudiniTool.IsValid() ); + + auto DefaultTool = FHoudiniEngineStyle::Get()->GetBrush( "HoudiniEngine.HoudiniEngineLogo"); + TSharedPtr< SImage > HelpButtonImage; + TSharedPtr< SButton > HelpButton; + TSharedPtr< SImage > DefaultToolImage; + + // Building the tool's tooltip + FString HoudiniToolTip; + if (HoudiniTool->PackageToolType == EHoudiniPackageToolType::Preset) + { + // HoudiniToolTip += HoudiniTool->Name.ToString() + TEXT(" (Preset) ") + TEXT( "\n\n" ); + HoudiniToolTip += HoudiniTool->Name.ToString() + TEXT(" - Preset") + TEXT( "\n\n" ); + if ( HoudiniTool->HoudiniPreset.IsValid() ) + HoudiniToolTip += HoudiniTool->HoudiniPreset.ToSoftObjectPath().ToString() /*->AssetFileName */+ TEXT( "\n\n" ); + } + else + { + HoudiniToolTip += HoudiniTool->Name.ToString() + TEXT(" - Houdini Asset") + TEXT( "\n\n" ); + if ( HoudiniTool->HoudiniAsset.IsValid() ) + HoudiniToolTip += HoudiniTool->HoudiniAsset.ToSoftObjectPath().ToString() /*->AssetFileName */+ TEXT( "\n\n" ); + } + + // if ( HoudiniTool->HoudiniAsset.IsValid() ) + // HoudiniToolTip += HoudiniTool->HoudiniAsset.ToSoftObjectPath().ToString() /*->AssetFileName */+ TEXT( "\n\n" ); + if ( !HoudiniTool->ToolTipText.IsEmpty() ) + HoudiniToolTip += HoudiniTool->ToolTipText.ToString() + TEXT("\n\n"); + + // Adding a description of the tools behavior deriving from its type + switch ( HoudiniTool->Type ) + { + case EHoudiniToolType::HTOOLTYPE_OPERATOR_SINGLE: + HoudiniToolTip += TEXT("Operator (Single):\nDouble clicking on this tool will instantiate the asset in the world.\nThe current selection will be automatically assigned to the asset's first input.\n" ); + break; + case EHoudiniToolType::HTOOLTYPE_OPERATOR_MULTI: + HoudiniToolTip += TEXT("Operator (Multiple):\nDouble clicking on this tool will instantiate the asset in the world.\nEach of the selected object will be assigned to one of the asset's input (object1 to input1, object2 to input2 etc.)\n" ); + break; + case EHoudiniToolType::HTOOLTYPE_OPERATOR_BATCH: + HoudiniToolTip += TEXT("Operator (Batch):\nDouble clicking on this tool will instantiate the asset in the world.\nAn instance of the asset will be created for each of the selected object, and the asset's first input will be set to that object.\n"); + break; + case EHoudiniToolType::HTOOLTYPE_GENERATOR: + default: + HoudiniToolTip += TEXT("Generator:\nDouble clicking on this tool will instantiate the asset in the world.\n"); + break; + } + + // Add a description from the tools selection type + if ( HoudiniTool->Type != EHoudiniToolType::HTOOLTYPE_GENERATOR ) + { + switch ( HoudiniTool->SelectionType ) + { + case EHoudiniToolSelectionType::HTOOL_SELECTION_ALL: + HoudiniToolTip += TEXT("\nBoth Content Browser and World Outliner selection will be considered.\nIf objects are selected in the content browser, the world selection will be ignored.\n"); + break; + + case EHoudiniToolSelectionType::HTOOL_SELECTION_WORLD_ONLY: + HoudiniToolTip += TEXT("\nOnly World Outliner selection will be considered.\n"); + break; + + case EHoudiniToolSelectionType::HTOOL_SELECTION_CB_ONLY: + HoudiniToolTip += TEXT("\nOnly Content Browser selection will be considered.\n"); + break; + } + } + + if ( HoudiniTool->Type != EHoudiniToolType::HTOOLTYPE_OPERATOR_BATCH ) + { + if ( HoudiniTool->SelectionType != EHoudiniToolSelectionType::HTOOL_SELECTION_CB_ONLY ) + HoudiniToolTip += TEXT( "If objects are selected in the world outliner, the asset's transform will default to the mean Transform of the select objects.\n" ); + } + else + { + if ( HoudiniTool->SelectionType != EHoudiniToolSelectionType::HTOOL_SELECTION_CB_ONLY ) + HoudiniToolTip += TEXT( "If objects are selected in the world outliner, each created asset will use its object transform.\n" ); + } + + FText ToolTipText = FText::FromString( HoudiniToolTip ); + + // Build the Help Tooltip + FString HelpURL = HoudiniTool->HelpURL; + bool HasHelp = HelpURL.Len() > 0; + FText HelpTip = HasHelp ? FText::Format( LOCTEXT( "OpenHelp", "Click to view tool help: {0}" ), FText::FromString( HelpURL ) ) : HoudiniTool->Name; + + // // Build the "DefaultTool" tool tip + // bool IsDefault = HoudiniTool->DefaultTool; + // FText DefaultToolText = FText::FromString( TEXT("Default Houdini Engine for Unreal plug-in tool") ); + + TSharedRef< STableRow> > TableRowWidget = + SNew( STableRow>, OwnerTable ) + .Style( FHoudiniEngineStyle::Get(), "HoudiniEngine.TableRow" ) + .OnDragDetected( this, &SHoudiniToolsPanel::OnDraggingListViewWidget ) + ; + + float IconOpacity = 1.f; + + if (HoudiniTool->IsHiddenInCategory(CategoryName)) + { + IconOpacity = 0.5f; + } + + TSharedPtr< SVerticalBox > ContentBox; + TSharedRef Content = SNew(SOverlay); + + Content->AddSlot().AttachWidget( + SNew( SBorder ) + .BorderImage( FCoreStyle::Get().GetBrush( "NoBorder" ) ) + .Padding( 0 ) + .ToolTip( SNew( SToolTip ).Text( ToolTipText ) ) + .Cursor( EMouseCursor::GrabHand ) + .ColorAndOpacity(FLinearColor(1.f, 1.f, 1.f, IconOpacity)) + [ + SAssignNew( ContentBox, SVerticalBox ) + ]); + + constexpr float IconWidth = 36.f; + + // Tool Icon + const FSlateBrush* ToolIcon = nullptr; + if (HoudiniTool->Icon) + { + ToolIcon = HoudiniTool->Icon.Get(); + } + else + { + ToolIcon = FHoudiniEngineStyle::Get()->GetBrush( FName(FHoudiniToolsEditor::GetDefaultHoudiniToolIconBrushName()) ); + } + + TSharedPtr IconOverlay = SNew(SOverlay); + + IconOverlay->AddSlot().AttachWidget( + SNew( SBorder ) + .BorderImage( FHoudiniEngineStyle::Get()->GetBrush( "HoudiniEngine.ThumbnailBackground" ) ) + .HAlign( HAlign_Center ) + .VAlign( VAlign_Center ) + [ + SNew( SImage ) + .Image( ToolIcon ) + .ToolTip( SNew( SToolTip ).Text( ToolTipText ) ) + ] + ); + + UnderlineBrush.DrawAs = ESlateBrushDrawType::Box; + UnderlineBrush.ImageSize = FVector2D(2.f, 2.f); + + FSlateColor UnderlineColor; + + if (HoudiniTool->PackageToolType == EHoudiniPackageToolType::Preset) + { + UnderlineColor = FSlateColor(FLinearColor(0.0f, 0.3f, 1.0f, 1.f)); + } + else + { + // Default (HoudiniAsset) color + UnderlineColor = FSlateColor(FLinearColor(1.0f, 0.25f, 0.0f, 1.f)); + } + + + // Add icon underline using UnderlineColor + IconOverlay->AddSlot() + .HAlign(HAlign_Fill) + .VAlign(VAlign_Bottom) + .Padding(FMargin(0.0f, 0.0f, 0.0f, 0.0f)) + .AttachWidget( + SNew(SImage) + .Image(&UnderlineBrush) + .ColorAndOpacity( UnderlineColor ) + .DesiredSizeOverride(FVector2D(10.0f, 2.f)) + ); + + ContentBox->AddSlot() + .HAlign(EHorizontalAlignment::HAlign_Center) + .VAlign(EVerticalAlignment::VAlign_Center) + .AutoHeight() + [ + SNew( SBorder ) + .Padding( 4.0f ) + .BorderImage( FHoudiniEngineStyle::Get()->GetBrush( "HoudiniEngine.ThumbnailShadow" ) ) + [ + SNew( SBox ) + .WidthOverride( IconWidth ) + .HeightOverride( IconWidth ) + [ + IconOverlay.ToSharedRef() + ] + ] + ]; + + // Tool name + tooltip + ContentBox->AddSlot() + .HAlign( HAlign_Center ) + .VAlign( VAlign_Center ) + .FillHeight( 1.f ) + [ + SNew( STextBlock ) + .TextStyle( FHoudiniEngineStyle::Get(), "HoudiniEngine.ThumbnailText" ) + .Text( HoudiniTool->Name ) + .HighlightText_Lambda([&]() + { + if (FilterString.Len() > 0) + { + return FText::FromString(FilterString); + } + return FText(); + }) + .ToolTip( SNew( SToolTip ).Text( ToolTipText ) ) + .WrappingPolicy(ETextWrappingPolicy::AllowPerCharacterWrapping) + ]; + + TableRowWidget->SetContent( Content ); + + return TableRowWidget; +} + +void +SHoudiniToolsPanel::OnToolSelectionChanged( SHoudiniToolCategory* Category, const TSharedPtr& HoudiniTool, ESelectInfo::Type SelectionType ) +{ + if (!Category) + return; + + if ( HoudiniTool.IsValid() ) + { + // Clear selections on any other categories. + ForEachCategory([&Category](SHoudiniToolCategory* InCategory) + { + if (!InCategory) + return true; + if (InCategory != Category) + { + // The child category differs from the category with the last selection. Clear its selection. + InCategory->ClearSelection(); + } + return true; + }); + // After we have cleared selections on the all the other categories, set the current Houdini Tool as active. + ActiveTool = HoudiniTool; + ActiveCategoryName = Category->GetCategoryLabel(); + } + else + { + ActiveTool = nullptr; + ActiveCategoryName.Empty(); + } +} + +FReply +SHoudiniToolsPanel::OnDraggingListViewWidget( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent ) +{ + if ( MouseEvent.IsMouseButtonDown( EKeys::LeftMouseButton ) ) + { + if ( ActiveTool.IsValid() ) + { + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( "AssetRegistry" ); + UObject* AssetObj = ActiveTool->HoudiniAsset.LoadSynchronous(); + if ( AssetObj ) + { + FString AssetPathName; + + UHoudiniPreset* Preset = ActiveTool->HoudiniPreset.LoadSynchronous(); + UActorFactory* Factory = nullptr; + + if (IsValid(Preset)) + { + Factory = GEditor->FindActorFactoryByClass( UHoudiniPresetActorFactory::StaticClass() ); + AssetPathName = Preset->GetPathName(); + } + + if (!IsValid(Factory)) + { + Factory = GEditor->FindActorFactoryForActorClass( AHoudiniAssetActor::StaticClass() ); + AssetPathName = AssetObj->GetPathName(); + } + + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0 + FAssetData BackingAssetData = AssetRegistryModule.Get().GetAssetByObjectPath(*AssetPathName); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + FAssetData BackingAssetData = AssetRegistryModule.Get().GetAssetByObjectPath(FSoftObjectPath(*AssetPathName)); +#endif + + return FReply::Handled().BeginDragDrop( FAssetDragDropOp::New( BackingAssetData, Factory ) ); + } + } + } + + return FReply::Unhandled(); +} + +void +SHoudiniToolsPanel::OnDoubleClickedListViewWidget( TSharedPtr HoudiniTool ) +{ + if ( !HoudiniTool.IsValid() ) + return; + + return InstantiateHoudiniTool( HoudiniTool.Get() ); +} + +void +SHoudiniToolsPanel::InstantiateHoudiniTool( FHoudiniTool* HoudiniTool ) +{ + if ( !HoudiniTool ) + return; + + // Load the asset + UHoudiniAsset* AssetObj = Cast(HoudiniTool->HoudiniAsset.LoadSynchronous()); + if ( !AssetObj ) + return; + + FHoudiniEngineEditorUtils::InstantiateHoudiniAsset(AssetObj, HoudiniTool->Type, HoudiniTool->SelectionType, HoudiniTool->HoudiniPreset.LoadSynchronous()); +} + +FTransform +SHoudiniToolsPanel::GetDefaulToolSpawnTransform() +{ + FTransform SpawnTransform = FTransform::Identity; + + // Get the editor viewport LookAt position to spawn the new objects + if ( GEditor && GEditor->GetActiveViewport() ) + { + FEditorViewportClient* ViewportClient = (FEditorViewportClient*)GEditor->GetActiveViewport()->GetClient(); + if ( ViewportClient ) + { + // We need to toggle the orbit camera on to get the proper LookAtLocation to spawn our asset + ViewportClient->ToggleOrbitCamera( true ); + SpawnTransform.SetLocation( ViewportClient->GetLookAtLocation() ); + ViewportClient->ToggleOrbitCamera( false ); + } + } + + return SpawnTransform; +} + +FTransform +SHoudiniToolsPanel::GetMeanWorldSelectionTransform() +{ + FTransform SpawnTransform = GetDefaulToolSpawnTransform(); + + if ( GEditor && ( GEditor->GetSelectedActorCount() > 0 ) ) + { + // Get the current Level Editor Selection + USelection* SelectedActors = GEditor->GetSelectedActors(); + + int NumAppliedTransform = 0; + for ( FSelectionIterator It( *SelectedActors ); It; ++It ) + { + AActor * Actor = Cast< AActor >( *It ); + if ( !Actor ) + continue; + + // Just Ignore the SkySphere... + FString ClassName = Actor->GetClass() ? Actor->GetClass()->GetName() : FString(); + if ( ClassName == TEXT( "BP_Sky_Sphere_C" ) ) + continue; + + FTransform CurrentTransform = Actor->GetTransform(); + + ALandscapeProxy* Landscape = Cast< ALandscapeProxy >( Actor ); + if ( Landscape ) + { + // We need to offset Landscape's transform in X/Y to center them properly + FVector Origin, Extent; + Actor->GetActorBounds(false, Origin, Extent); + + // Use the origin's XY Position + FVector Location = CurrentTransform.GetLocation(); + Location.X = Origin.X; + Location.Y = Origin.Y; + CurrentTransform.SetLocation( Location ); + } + + // Accumulate all the actor transforms... + if ( NumAppliedTransform == 0 ) + SpawnTransform = CurrentTransform; + else + SpawnTransform.Accumulate( CurrentTransform ); + + NumAppliedTransform++; + } + + if ( NumAppliedTransform > 0 ) + { + // "Mean" all the accumulated Transform + SpawnTransform.SetScale3D( FVector::OneVector ); + SpawnTransform.NormalizeRotation(); + + if ( NumAppliedTransform > 1 ) + SpawnTransform.SetLocation( SpawnTransform.GetLocation() / (float)NumAppliedTransform ); + } + } + + return SpawnTransform; +} + + +// TSharedPtr +// SHoudiniToolsPanel::ConstructContextMenu(const FString& CategoryName, const EHoudiniToolCategoryType CategoryType) +// { +// FHoudiniToolCategory Category(CategoryName, CategoryType); +// if (ActiveTool.IsValid() && GetHoudiniTools().IsToolInCategory(Category, ActiveTool)) +// { +// // if there is an active tool, and the context menu was summoned from the same category, only then do we +// // open the Tool context menu. +// return ConstructHoudiniToolContextMenu(); +// } +// +// return ConstructCategoryContextMenu(CategoryName, CategoryType); +// } + + +// Builds the context menu for the selected tool +TSharedPtr +SHoudiniToolsPanel::ConstructHoudiniToolContextMenu() +{ + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< FAssetRegistryModule >( "AssetRegistry" ); + + FMenuBuilder MenuBuilder( true, NULL ); + + // Prevent crashes caused by null active tool + if (!ActiveTool.IsValid()) + { + return MenuBuilder.MakeWidget(); + } + + bool bHasHelp = false; + bool bIsFavoritesCategory = ActiveCategoryName == FHoudiniToolsEditor::GetFavoritesCategoryName().ToString(); + FString HelpURL; + TArray< UObject* > AssetArray; + EHoudiniToolCategoryType CategoryType = EHoudiniToolCategoryType::Package; + const UClass* AssetClass = nullptr; + UObject* AssetObj = ActiveTool->GetAssetObject(); + if ( ActiveTool.IsValid() && !ActiveTool->HoudiniAsset.IsNull() ) + { + // Load the underlying asset (HoudiniAsset or HoudiniPreset) for the active tool + if( AssetObj ) + { + AssetArray.Add( AssetObj ); + } + + HelpURL = ActiveTool->HelpURL; + bHasHelp = !HelpURL.IsEmpty(); + CategoryType = ActiveTool->CategoryType; + AssetClass = ActiveTool->GetAssetObjectClass(); + } + + if (!IsValid(AssetClass)) + { + return MenuBuilder.MakeWidget(); + } + + FAssetToolsModule & AssetToolsModule = FModuleManager::GetModuleChecked< FAssetToolsModule >( "AssetTools" ); + TSharedPtr< IAssetTypeActions > EngineActions = AssetToolsModule.Get().GetAssetTypeActionsForClass( AssetClass ).Pin(); + if ( EngineActions.IsValid() ) + EngineActions->GetActions( AssetArray, MenuBuilder ); + + // Add HoudiniTools actions + MenuBuilder.AddMenuSeparator(); + + if (FHoudiniToolsEditor::UserCategoryContainsTool("Favorites", AssetObj, ActiveTool->ToolsPackage.LoadSynchronous())) + { + // Remove From Favorites + const FText HideLabel = LOCTEXT( "HoudiniTool_ContextMenu_ShowInPackageCategory", "Unfavorite" ); + const FText HideTooltip = LOCTEXT( "HoudiniTool_ContextMenu_ShowInPackageCategoryTooltip", "Remove this tool from the 'Favorites' user category." ); + MenuBuilder.AddMenuEntry( + HideLabel, + HideTooltip, + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateSP(this, &SHoudiniToolsPanel::HandleRemoveFromFavorites ) + ) + ); + } + else + { + // Add To Favorites + const FText HideLabel = LOCTEXT( "HoudiniTool_ContextMenu_ShowInPackageCategory", "Favorite" ); + const FText HideTooltip = LOCTEXT( "HoudiniTool_ContextMenu_ShowInPackageCategoryTooltip", "Add this tool the 'Favorites' user category" ); + MenuBuilder.AddMenuEntry( + HideLabel, + HideTooltip, + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateSP(this, &SHoudiniToolsPanel::HandleAddToFavorites ) + ) + ); + } + + if (!bIsFavoritesCategory) + { + if (ActiveTool->IsHiddenInCategory(ActiveCategoryName)) + { + // The tool is already hidden in this category. Allow the user to 'show' this tool in this category. + // TODO: First check whether it's even possible to remove this tool from the exclusion list. If not, disable these options. + const FText HideLabel = LOCTEXT( "HoudiniTool_ContextMenu_ShowInPackageCategory", "Make Visible In Category" ); + const FText HideTooltip = LOCTEXT( "HoudiniTool_ContextMenu_ShowInPackageCategoryTooltip", "Make this tool visible in the Package Category by removing it from the category exclusion list, if possible." ); + MenuBuilder.AddMenuEntry( + HideLabel, + HideTooltip, + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateSP(this, &SHoudiniToolsPanel::ShowActiveToolInCategory ), + FCanExecuteAction::CreateLambda([this] { return IsActiveHoudiniToolEditable() && CanShowActiveToolInCategory(); } ) + ) + ); + } + else + { + // Hide Tool from category + const FText HideLabel = CategoryType == EHoudiniToolCategoryType::Package ? + LOCTEXT( "HoudiniTool_ContextMenu_HideFromPackageCategory", "Hide From Category" ) : + LOCTEXT( "HoudiniTool_ContextMenu_RemoveFromPackageCategory", "Remove From Category" ); + const FText HideTooltip = CategoryType == EHoudiniToolCategoryType::Package ? + LOCTEXT( "HoudiniTool_ContextMenu_HideFromPackageCategoryTooltip", "Hide the selected tool from this package category." ) : + LOCTEXT( "HoudiniTool_ContextMenu_RemoveFromPackageCategoryTooltip", "Remove the selected tool from this user category." ); + MenuBuilder.AddMenuEntry( + HideLabel, + HideTooltip, + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateSP(this, &SHoudiniToolsPanel::HideActiveToolFromCategory ), + FCanExecuteAction::CreateLambda([this] { return IsActiveHoudiniToolEditable(); } ) + ) + ); + } + } + + // Add To User Category + + MenuBuilder.AddMenuEntry( + LOCTEXT( "HoudiniTool_ContextMenu_AddToUserCategory", "Add To User Category" ), + LOCTEXT("HoudiniTool_ContextMenu_AddToUserCategoryTooltip", "Add the selected tool a user category." ), + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateSP(this, &SHoudiniToolsPanel::HandleAddToolToUserCategory ), + // FCanExecuteAction::CreateLambda([&] { return IsActiveHoudiniToolEditable(); } ) + FCanExecuteAction() + ) + ); + + // Add HoudiniTools actions + MenuBuilder.AddMenuSeparator(); + + if (ActiveTool->PackageToolType == EHoudiniPackageToolType::Preset) + { + // Browse to tool package in the content browser + MenuBuilder.AddMenuEntry( + NSLOCTEXT( "HoudiniToolsTypeActions", "HoudiniTool_BrowseToPreset", "Browse to Preset" ), + NSLOCTEXT( "HoudiniToolsTypeActions", "HoudiniTool_BrowseToPresetTooltip", "Browse to the preset in the content browser." ), + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateSP(this, &SHoudiniToolsPanel::BrowseToActiveToolAsset ), + FCanExecuteAction::CreateLambda([&] { return IsActiveHoudiniToolEditable(); } ) + ) + ); + + // Browse to tool package in the content browser + MenuBuilder.AddMenuEntry( + NSLOCTEXT( "HoudiniToolsTypeActions", "HoudiniTool_BrowseToSourceAsset", "Browse to Source Asset" ), + NSLOCTEXT( "HoudiniToolsTypeActions", "HoudiniTool_BrowseToSourceAssetTooltip", "Browse to the preset's source asset in the content browser." ), + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateSP(this, &SHoudiniToolsPanel::BrowseToSourceAsset ), + FCanExecuteAction::CreateLambda([&] { return IsActiveHoudiniToolEditable(); } ) + ) + ); + } + + if (ActiveTool->PackageToolType == EHoudiniPackageToolType::HoudiniAsset) + { + // Browse to tool package in the content browser + MenuBuilder.AddMenuEntry( + NSLOCTEXT( "HoudiniToolsTypeActions", "HoudiniTool_BrowseToAsset", "Browse to Asset" ), + NSLOCTEXT( "HoudiniToolsTypeActions", "HoudiniTool_BrowseToAssetTooltip", "Browse to the tool asset in the content browser." ), + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateSP(this, &SHoudiniToolsPanel::BrowseToActiveToolAsset ), + FCanExecuteAction::CreateLambda([&] { return IsActiveHoudiniToolEditable(); } ) + ) + ); + } + + + + // Browse to tool package in the content browser + MenuBuilder.AddMenuEntry( + NSLOCTEXT( "HoudiniToolsTypeActions", "HoudiniTool_FindPackage", "Browse to Package" ), + NSLOCTEXT( "HoudiniToolsTypeActions", "HoudiniTool_FindPackageTooltip", "Browse to the owning package in the content browser." ), + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateSP(this, &SHoudiniToolsPanel::BrowseToActiveToolPackage ), + FCanExecuteAction::CreateLambda([&] { return IsActiveHoudiniToolEditable(); } ) + ) + ); + + // Launch Tool Help + const FText ToolHelpTooltip = NSLOCTEXT( "HoudiniToolsTypeActions", "HoudiniTool_HelpTooltip", "Launch the browser to open the tools help." ); + const FText ToolNoHelpTooltip = NSLOCTEXT( "HoudiniToolsTypeActions", "HoudiniTool_NoHelpTooltip", "This tool doesn't have a Help URL." ); + MenuBuilder.AddMenuEntry( + NSLOCTEXT( "HoudiniToolsTypeActions", "HoudiniTool_Edit", "Open Tool Help" ), + bHasHelp ? ToolHelpTooltip : ToolNoHelpTooltip, + // FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FSlateIcon( FAppStyle::GetAppStyleSetName(), "Icons.Help" ), + FUIAction( + FExecuteAction::CreateLambda([HelpURL]() + { + if (!HelpURL.IsEmpty()) + { + FPlatformProcess::LaunchURL( *HelpURL, nullptr, nullptr ); + } + } ), + FCanExecuteAction::CreateLambda([bHasHelp] { return bHasHelp; } ) + ) + ); + + return MenuBuilder.MakeWidget(); +} + +TSharedPtr SHoudiniToolsPanel::ConstructCategoryContextMenu(const FString& CategoryName, const EHoudiniToolCategoryType CategoryType) +{ + FMenuBuilder MenuBuilder( true, NULL ); + + TArray UserCategories; + FHoudiniToolsEditor::GetUserCategoriesList(UserCategories); + + const bool bIsUserCategory = UserCategories.Contains(CategoryName); + const bool bIsFavoriteCategory = CategoryName == FHoudiniToolsEditor::GetFavoritesCategoryName().ToString(); + + MenuBuilder.BeginSection("HoudiniTool_CategoryContextMenu_CategorySection", FText::FromString(CategoryName) ); + + if (bIsUserCategory) + { + // Edit User Category + const FText Label = LOCTEXT( "HoudiniTool_CategoryContextMenu_EditCategory", "Edit User Category" ); + const FText Tooltip = LOCTEXT( "HoudiniTool_CategoryContextMenu_EditCategoryTooltip", "Edit user settings containing this category." ); + MenuBuilder.AddMenuEntry( + Label, + Tooltip, + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateLambda([this, CategoryName](){ HandleEditUserCategory(CategoryName); }), + FCanExecuteAction::CreateLambda([bIsFavoriteCategory]() { return !bIsFavoriteCategory; }) + ) + ); + } + else + { + // Edit Package Category + const FText Label = LOCTEXT( "HoudiniTool_CategoryContextMenu_EditCategory", "Edit Category" ); + const FText Tooltip = LOCTEXT( "HoudiniTool_CategoryContextMenu_EditCategoryTooltip", "Edit the package containing this category." ); + MenuBuilder.AddMenuEntry( + Label, + Tooltip, + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateLambda([this, CategoryName](){ HandleEditPackageCategory(CategoryName); }) + ) + ); + } + + // Browse To + { + const FText Label = LOCTEXT( "HoudiniTool_CategoryContextMenu_BrowseTo", "Browse To" ); + const FText Tooltip = LOCTEXT( "HoudiniTool_CategoryContextMenu_BrowseToTooltip", "Browse to the package containing this category." ); + MenuBuilder.AddMenuEntry( + Label, + Tooltip, + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateLambda([this, CategoryName](){ HandleBrowseToCategoryPackage(CategoryName); }), + FCanExecuteAction::CreateLambda([bIsUserCategory]() { return !bIsUserCategory; } ) + ) + ); + } + + // Save Category Tools + { + const FText Label = LOCTEXT( "HoudiniTool_CategoryContextMenu_SaveCategory", "Save Category Tools" ); + const FText Tooltip = LOCTEXT( "HoudiniTool_CategoryContextMenu_SaveCategoryTooltip", "Save all the unsaved tools in this category." ); + MenuBuilder.AddMenuEntry( + Label, + Tooltip, + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateLambda([this, CategoryName, CategoryType](){ HandleSaveToolsInCategory(CategoryName, CategoryType); }) + ) + ); + } + + // Import HoudiniTool into category + { + const FText Label = LOCTEXT( "HoudiniTool_CategoryContextMenu_ImportTool", "Import HoudiniTool" ); + const FText Tooltip = LOCTEXT( "HoudiniTool_CategoryContextMenu_ImportToolTooltip", "Import HoudiniTool into this category." ); + MenuBuilder.AddMenuEntry( + Label, + Tooltip, + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateLambda([this, CategoryName, CategoryType](){ HandleImportToolsIntoCategory(CategoryName, CategoryType); }), + FCanExecuteAction::CreateLambda([bIsUserCategory] { return !bIsUserCategory; } ) + ) + ); + } + + MenuBuilder.EndSection(); + + return MenuBuilder.MakeWidget(); +} + +TSharedPtr +SHoudiniToolsPanel::ConstructHoudiniToolsActionMenu() +{ + FMenuBuilder MenuBuilder( true, NULL ); + + // Section - Options + + MenuBuilder.BeginSection("PackageCreate", LOCTEXT("ActionMenu_Section_Options", "Options")); + + // Options - Auto Refresh + MenuBuilder.AddMenuEntry( + LOCTEXT("ActionMenu_AutoRefresh", "Auto Refresh"), + LOCTEXT("ActionMenu_AutoRefreshTooltip", "Automatically refresh this panel when asset changes have been detected."), + FSlateIcon(), + FUIAction( + FExecuteAction::CreateLambda([this]() -> void + { + bAutoRefresh = !bAutoRefresh; + SaveConfig(); + if (bAutoRefresh) + { + RefreshPanel(); + } + }), + FCanExecuteAction(), + FGetActionCheckState::CreateLambda([this]() -> ECheckBoxState { return bAutoRefresh ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } ) + ), + NAME_None, + EUserInterfaceActionType::ToggleButton + ); + + // Options - Show Hidden Tools + MenuBuilder.AddMenuEntry( + LOCTEXT("ActionMenu_ShowHiddenTools", "Show Hidden Tools"), + LOCTEXT("ActionMenu_ShowHiddenToolsTooltip", "Show hidden tools (ignoring any exclusion patterns)."), + FSlateIcon(), + FUIAction( + FExecuteAction::CreateLambda([this]() -> void + { + bShowHiddenTools = !bShowHiddenTools; + SaveConfig(); + RequestPanelRefresh(); + }), + FCanExecuteAction(), + FGetActionCheckState::CreateLambda([this]() -> ECheckBoxState { return bShowHiddenTools ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } ) + ), + NAME_None, + EUserInterfaceActionType::ToggleButton + ); + + // Options - Show Empty Categories + MenuBuilder.AddMenuEntry( + LOCTEXT("ActionMenu_ShowEmptyCategories", "Show Empty Categories"), + LOCTEXT("ActionMenu_ShowEmptyCategoriesTooltip", "Show empty categories."), + FSlateIcon(), + FUIAction( + FExecuteAction::CreateLambda([this]() -> void + { + bShowEmptyCategories = !bShowEmptyCategories; + SaveConfig(); + RequestPanelRefresh(); + }), + FCanExecuteAction(), + FGetActionCheckState::CreateLambda([this]() -> ECheckBoxState { return bShowEmptyCategories ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } ) + ), + NAME_None, + EUserInterfaceActionType::ToggleButton + ); + + // Options - Show Houdini Assets + MenuBuilder.AddMenuEntry( + LOCTEXT("ActionMenu_ShowHoudiniAssets", "Show Houdini Assets"), + LOCTEXT("ActionMenu_ShowHoudiniAssetsTooltip", "Show Houdini Assets."), + FSlateIcon(), + FUIAction( + FExecuteAction::CreateLambda([this]() -> void + { + bShowHoudiniAssets = !bShowHoudiniAssets; + SaveConfig(); + RequestPanelRefresh(); + }), + FCanExecuteAction(), + FGetActionCheckState::CreateLambda([this]() -> ECheckBoxState { return bShowHoudiniAssets ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } ) + ), + NAME_None, + EUserInterfaceActionType::ToggleButton + ); + + // Options - Show Houdini Assets + MenuBuilder.AddMenuEntry( + LOCTEXT("ActionMenu_ShowPresets", "Show Presets"), + LOCTEXT("ActionMenu_ShowPresetsTooltip", "Show Presets."), + FSlateIcon(), + FUIAction( + FExecuteAction::CreateLambda([this]() -> void + { + bShowPresets = !bShowPresets; + SaveConfig(); + RequestPanelRefresh(); + }), + FCanExecuteAction(), + FGetActionCheckState::CreateLambda([this]() -> ECheckBoxState { return bShowPresets ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } ) + ), + NAME_None, + EUserInterfaceActionType::ToggleButton + ); + + // Options - Tile View + + MenuBuilder.AddMenuEntry( + LOCTEXT("ActionMenu_TileView", "Tile View"), + LOCTEXT("ActionMenu_TileViewTooltip", "Show Houdini Tools in a compact tile view."), + FSlateIcon(), + FUIAction( + FExecuteAction::CreateLambda([this]() -> void + { + ViewMode = EHoudiniToolsViewMode::TileView; + SaveConfig(); + RequestPanelRefresh(); + }), + FCanExecuteAction(), + FGetActionCheckState::CreateLambda([this]() -> ECheckBoxState { return ViewMode == EHoudiniToolsViewMode::TileView ? ECheckBoxState::Checked : ECheckBoxState::Unchecked ; } ) + ), + NAME_None, + EUserInterfaceActionType::RadioButton + ); + + // Options - List View + + MenuBuilder.AddMenuEntry( + LOCTEXT("ActionMenu_ListView", "List View"), + LOCTEXT("ActionMenu_ListViewTooltip", "Show Houdini Tools in a list view."), + FSlateIcon(), + FUIAction( + FExecuteAction::CreateLambda([this]() -> void + { + ViewMode = EHoudiniToolsViewMode::ListView; + SaveConfig(); + RequestPanelRefresh(); + }), + FCanExecuteAction(), + FGetActionCheckState::CreateLambda([this]() -> ECheckBoxState { return ViewMode == EHoudiniToolsViewMode::ListView ? ECheckBoxState::Checked : ECheckBoxState::Unchecked ; } ) + ), + NAME_None, + EUserInterfaceActionType::RadioButton + ); + + MenuBuilder.EndSection(); + + // Section - Create + + MenuBuilder.BeginSection("PackageCreate", LOCTEXT("ActionMenu_Section_Create", "Create")); + // New HTools Package + MenuBuilder.AddMenuEntry( + LOCTEXT( "ActionMenu_NewPackage", "Create HoudiniTools package" ), + LOCTEXT( "ActionMenu_NewPackageTooltip", "Create an empty HoudiniTools package in the Unreal project." ), + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateSP(this, &SHoudiniToolsPanel::CreateEmptyToolsPackage ) + ) + ); + + // Import HTools Package + MenuBuilder.AddMenuEntry( + LOCTEXT( "ActionMenu_ImportPackage", "Import HoudiniTools package" ), + LOCTEXT( "ActionMenu_ImportPackageTooltip", "Import an external HTools package into Unreal." ), + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateSP(this, &SHoudiniToolsPanel::ImportToolsPackage ) + ) + ); + + // Import HTools Package + MenuBuilder.AddMenuEntry( + LOCTEXT( "ActionMenu_ImportSideFXTools", "Import SideFX Tools" ), + LOCTEXT( "ActionMenu_ImportSideFXToolsTooltip", "Import default SideFX tools from Houdini." ), + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateSP(this, &SHoudiniToolsPanel::HandleImportSideFXTools ) + ) + ); + + MenuBuilder.EndSection(); + + MenuBuilder.BeginSection("PackageUpdate", LOCTEXT("ActionMenu_Section_Update", "Update")); + + // Rescan project and refresh tool panels. + MenuBuilder.AddMenuEntry( + LOCTEXT( "ActionMenu_RefreshPanel", "Refresh Panel" ), + LOCTEXT( "ActionMenu_RefreshPanel_Description", "Rescan the Unreal project for Houdini Tools packages." ), + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateSP(this, &SHoudiniToolsPanel::RefreshPanel ) + ) + ); + + // Add HoudiniTools actions + MenuBuilder.EndSection(); + + MenuBuilder.BeginSection("PackageUpdate", LOCTEXT("ActionMenu_Section_Modify", "Modify")); + + MenuBuilder.AddMenuEntry( + LOCTEXT( "ActionMenu_EditUserCategories", "Edit User Categories" ), + LOCTEXT( "ActionMenu_EditUserCategories_Description", "Edit User Categories in the Houdini Engine editor settings." ), + FSlateIcon( FHoudiniEngineStyle::GetStyleSetName(), "HoudiniEngine.HoudiniEngineLogo" ), + FUIAction( + FExecuteAction::CreateLambda([](){ FHoudiniEngineCommands::ShowPluginEditorSettings(); }) + ) + ); + + // Add HoudiniTools actions + MenuBuilder.EndSection(); + + return MenuBuilder.MakeWidget(); +} + +TSharedPtr +SHoudiniToolsPanel::ConstructCategoryFilterMenu() +{ + FMenuBuilder MenuBuilder( true, NULL ); + + MenuBuilder.AddMenuEntry( + FText::FromString("Show All"), + FText::FromString("Show all categories."), + FSlateIcon(), + FUIAction( + FExecuteAction::CreateLambda([&]() -> void + { + bFilterShowAll = !bFilterShowAll; + if (bFilterShowAll) + { + FilterHiddenCategories.Empty(); + } + else + { + // Set all categories as hidden. + ForEachCategory([&](SHoudiniToolCategory* Category) -> bool + { + if (Category) + { + FilterHiddenCategories.Add(Category->GetCategoryLabel()); + } + return true; + }); + } + SaveConfig(); + RequestPanelRefresh(); + }), + FCanExecuteAction(), + FGetActionCheckState::CreateLambda([&]() -> ECheckBoxState { return bFilterShowAll ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } ) + ), + NAME_None, + EUserInterfaceActionType::ToggleButton + ); + + MenuBuilder.BeginSection("Categories", LOCTEXT("FilterMenu_Section_Categories", "Categories")); + + + // Add all the categories to the menu + + TArray CategoryList; + + ForEachCategory([&CategoryList](SHoudiniToolCategory* Category) -> bool + { + if (Category) + { + CategoryList.Add(Category->GetCategoryLabel()); + } + return true; + }); + CategoryList.Sort(); + + for (const FString& CategoryName : CategoryList) + { + MenuBuilder.AddMenuEntry( + FText::FromString(CategoryName), + FText::FromString( FString::Format(TEXT("Show/Hide {0}."), {CategoryName}) ), + FSlateIcon(), + FUIAction( + FExecuteAction::CreateLambda([&, CategoryName]() -> void + { + // Toggle the current category name + if (FilterHiddenCategories.Contains(CategoryName)) + { + FilterHiddenCategories.Remove(CategoryName); + } + else + { + FilterHiddenCategories.Add(CategoryName); + } + SaveConfig(); + RequestPanelRefresh(); + }), + FCanExecuteAction(), + FGetActionCheckState::CreateLambda([&, CategoryName]() -> ECheckBoxState + { + return FilterHiddenCategories.Contains(CategoryName) ? ECheckBoxState::Unchecked : ECheckBoxState::Checked; + } ) + ), + NAME_None, + EUserInterfaceActionType::ToggleButton + ); + } + + return MenuBuilder.MakeWidget(); +} + +void +SHoudiniToolsPanel::EditActiveHoudiniTool() const +{ + if ( !ActiveTool.IsValid() ) + return; + + if ( !IsActiveHoudiniToolEditable() ) + return; + + FHoudiniToolsEditor::LaunchHoudiniToolPropertyEditor(ActiveTool); + + // const UHoudiniAsset* HoudiniAsset = ActiveTool->HoudiniAsset.LoadSynchronous(); + // if (!IsValid(HoudiniAsset)) + // return; + // + // const UHoudiniToolData* CachedToolData = HoudiniAsset->HoudiniToolData; + // const bool bValidToolData = IsValid(CachedToolData); + // + // // Create a new Tool property object for the property dialog + // FString ToolName = ActiveTool->Name.ToString(); + // if ( ActiveTool->HoudiniAsset ) + // ToolName += TEXT(" (") + ActiveTool->HoudiniAsset->AssetFileName + TEXT(")"); + // + // UHoudiniToolEditorProperties* NewToolProperty = NewObject< UHoudiniToolEditorProperties >( GetTransientPackage(), FName( *ToolName ) ); + // NewToolProperty->AddToRoot(); + // + // // Set the default values for this asset + // NewToolProperty->Name = ActiveTool->Name.ToString(); + // NewToolProperty->Type = ActiveTool->Type; + // NewToolProperty->ToolTip = ActiveTool->ToolTipText.ToString(); + // NewToolProperty->HelpURL = ActiveTool->HelpURL; + // NewToolProperty->SelectionType = ActiveTool->SelectionType; + // // Always leave this field blank. The user can use this to import a new icon from an arbitrary location. + // NewToolProperty->IconPath.FilePath = FString(); + // NewToolProperty->ToolType = ActiveTool->PackageToolType; + // + // TArray ActiveHoudiniTools; + // ActiveHoudiniTools.Add( NewToolProperty ); + // + // + // TSharedPtr EditingTool = ActiveTool; + // + // // Create a new property editor window + // TSharedRef< SWindow > Window = CreateFloatingDetailsView( + // ActiveHoudiniTools, + // FVector2D(450,350), + // [&,EditingTool](TArray InObjects) + // { + // HandleEditHoudiniToolSavedClicked(EditingTool, InObjects); + // } + // ); +} + +void +SHoudiniToolsPanel::BrowseToActiveToolAsset() const +{ + if (!ActiveTool.IsValid()) + return; + + if (GEditor) + { + FHoudiniToolsEditor::BrowseToObjectInContentBrowser(ActiveTool->GetAssetObject()); + } +} + +void +SHoudiniToolsPanel::BrowseToSourceAsset() const +{ + if (!ActiveTool.IsValid()) + return; + + if (GEditor) + { + UHoudiniAsset* SourceAsset = ActiveTool->HoudiniAsset.LoadSynchronous(); + FHoudiniToolsEditor::BrowseToObjectInContentBrowser(SourceAsset); + } +} + +void +SHoudiniToolsPanel::BrowseToActiveToolPackage() const +{ + if (!ActiveTool.IsValid()) + return; + + if (GEditor) + { + const TSoftObjectPtr HoudiniAsset = ActiveTool->HoudiniAsset; + FHoudiniToolsEditor::BrowseToOwningPackageInContentBrowser(HoudiniAsset.LoadSynchronous()); + } +} + +// void +// SHoudiniToolsPanel::HandleEditHoudiniToolSavedClicked(TSharedPtr HoudiniTool, TArray& InObjects ) +// { +// // Sanity check, we can only edit one tool at a time! +// if ( InObjects.Num() != 1 ) +// return; +// +// if (!HoudiniTool.IsValid()) +// return; +// +// UHoudiniAsset* HoudiniAsset = HoudiniTool->HoudiniAsset.LoadSynchronous(); +// if (!HoudiniAsset) +// { +// HOUDINI_LOG_WARNING(TEXT("Could not locate active tool. Unable to save changes.")); +// return; +// } +// +// // Reimport assets from their new sources. +// TArray ReimportAssets; +// +// TArray< FHoudiniTool > EditedToolArray; +// for ( int32 ObjIdx = 0; ObjIdx < InObjects.Num(); ObjIdx++ ) +// { +// UHoudiniToolProperties* ToolProperties = Cast< UHoudiniToolProperties >( InObjects[ ObjIdx ] ); +// if ( !ToolProperties ) +// continue; +// +// // FString IconPath = FPaths::ConvertRelativePathToFull( ToolProperties->IconPath.FilePath ); +// // const FSlateBrush* CustomIconBrush = nullptr; +// // if ( FPaths::FileExists( IconPath ) ) +// // { +// // +// // // If we have a valid icon path, load the file. +// // FName BrushName = *IconPath; +// // CustomIconBrush = new FSlateDynamicImageBrush( BrushName, FVector2D( 40.f, 40.f ) ); +// // } +// +// // - Store all the edited properties on the HoudiniToolData. +// // - Populate the relevant FHoudiniTool descriptor from the HoudiniAsset. +// // - Save out the JSON description, if required. +// +// bool bModified = false; +// +// // Helper macro for Property assignments and modify flag management +// #define ASSIGNFN(Src, Dst) \ +// {\ +// if (Src != Dst)\ +// {\ +// bModified = true;\ +// Dst = Src;\ +// }\ +// } +// +// UHoudiniToolData* ToolData = FHoudiniToolsEditor::GetOrCreateHoudiniToolData(HoudiniAsset); +// ToolData->Modify(); +// ASSIGNFN(ToolProperties->Name, ToolData->Name); +// ASSIGNFN(ToolProperties->Type, ToolData->Type); +// ASSIGNFN(ToolProperties->SelectionType, ToolData->SelectionType); +// ASSIGNFN(ToolProperties->ToolTip, ToolData->ToolTip); +// ASSIGNFN(ToolProperties->HelpURL, ToolData->HelpURL); +// +// if (FPaths::FileExists(ToolProperties->AssetPath.FilePath)) +// { +// // If the AssetPath has changed, reimport the HDA with the new source +// if (ToolProperties->AssetPath.FilePath != ToolData->SourceAssetPath.FilePath) +// { +// TArray Filenames; +// Filenames.Add(ToolProperties->AssetPath.FilePath); +// FReimportManager::Instance()->UpdateReimportPaths(HoudiniAsset, Filenames); +// +// ReimportAssets.Add(HoudiniAsset); +// } +// ASSIGNFN(ToolProperties->AssetPath.FilePath, ToolData->SourceAssetPath.FilePath); +// } +// else +// { +// HOUDINI_LOG_WARNING(TEXT("The specified AssetPath does not exist. Source Asset Path will remain unchanged")); +// } +// +// +// +// bool bModifiedIcon = false; +// +// if (ToolProperties->IconPath.FilePath.Len() > 0) +// { +// bModified = true; +// bModifiedIcon = true; +// ToolData->LoadIconFromPath(ToolProperties->IconPath.FilePath); +// } +// else if (ToolProperties->bClearCachedIcon) +// { +// bModified = true; +// bModifiedIcon = true; +// ToolData->ClearCachedIcon(); +// } +// +// if (bModifiedIcon) +// { +// // Ensure the content browser reflects icon changes. +// const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); +// AssetRegistryModule.Get().OnAssetUpdated().Broadcast(FAssetData(HoudiniAsset)); +// } +// +// ToolData->DefaultTool = false; +// +// if (bModified) +// { +// ToolData->MarkPackageDirty(); +// +// // We modified an existing tool. Request a panel refresh. +// RequestPanelRefresh(); +// } +// +// // Remove the tool from Root +// ToolProperties->RemoveFromRoot(); +// } +// +// FAssetRegistryModule& AssetRegistry = FModuleManager::Get().LoadModuleChecked("AssetRegistry"); +// +// +// for (UHoudiniAsset* Asset : ReimportAssets) +// { +// FReimportManager::Instance()->Reimport(Asset, false /* Ask for new file */, true /* Show notification */); +// } +// } + +// TSharedRef +// SHoudiniToolsPanel::CreateFloatingDetailsView( +// TArray& InObjects, +// const FVector2D InClientSize, +// const TFunction)> OnSaveClickedFn +// ) +// { +// TSharedRef NewSlateWindow = SNew(SWindow) +// .Title(NSLOCTEXT("PropertyEditor", "WindowTitle", "Houdini Tools Property Editor")) +// .ClientSize(InClientSize); +// +// // If the main frame exists parent the window to it +// TSharedPtr< SWindow > ParentWindow; +// if ( FModuleManager::Get().IsModuleLoaded("MainFrame") ) +// { +// IMainFrameModule& MainFrame = FModuleManager::GetModuleChecked("MainFrame"); +// ParentWindow = MainFrame.GetParentWindow(); +// } +// +// if ( ParentWindow.IsValid() ) +// { +// // Parent the window to the main frame +// FSlateApplication::Get().AddWindowAsNativeChild( NewSlateWindow, ParentWindow.ToSharedRef() ); +// } +// else +// { +// FSlateApplication::Get().AddWindow( NewSlateWindow ); +// } +// +// FDetailsViewArgs Args; +// Args.bHideSelectionTip = true; +// Args.bLockable = false; +// Args.bAllowMultipleTopLevelObjects = true; +// Args.ViewIdentifier = TEXT("Houdini Tools Properties"); +// Args.NameAreaSettings = FDetailsViewArgs::HideNameArea; +// Args.bShowPropertyMatrixButton = false; +// Args.bShowOptions = false; +// Args.bShowModifiedPropertiesOption = false; +// Args.bShowObjectLabel = false; +// +// FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked("PropertyEditor"); +// TSharedRef DetailView = PropertyEditorModule.CreateDetailView( Args ); +// DetailView->SetObjects( InObjects ); +// +// NewSlateWindow->SetContent( +// SNew( SBorder ) +// .BorderImage(_GetBrush(TEXT("PropertyWindow.WindowBorder"))) +// .BorderImage(FAppStyle::Get().GetBrush("Brushes.Panel")) +// [ +// SNew(SVerticalBox) +// +// // Detail View +// + SVerticalBox::Slot() +// .AutoHeight() +// [ +// DetailView +// ] +// +// // Button Row +// + SVerticalBox::Slot() +// .VAlign(VAlign_Bottom) +// .HAlign(HAlign_Right) +// .FillHeight(1.f) +// [ +// SNew(SUniformGridPanel) +// .SlotPadding(FCoreStyle::Get().GetMargin("StandardDialog.SlotPadding")) +// .MinDesiredSlotWidth(FCoreStyle::Get().GetFloat("StandardDialog.MinDesiredSlotWidth")) +// .MinDesiredSlotHeight(FCoreStyle::Get().GetFloat("StandardDialog.MinDesiredSlotHeight")) +// +// // Save Button +// + SUniformGridPanel::Slot(0, 0) +// [ +// SNew(SButton) +// .HAlign(HAlign_Center) +// .VAlign(VAlign_Center) +// .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) +// .ButtonStyle(&_GetEditorStyle().GetWidgetStyle("PrimaryButton")) +// .Content() +// [ +// SNew(STextBlock) +// .TextStyle( &_GetEditorStyle().GetWidgetStyle("PrimaryButtonText") ) +// .Text( LOCTEXT("HoudiniTools_Details_Save","Save") ) +// ] +// .OnClicked_Lambda([NewSlateWindow, OnSaveClickedFn, InObjects]() -> FReply +// { +// OnSaveClickedFn(InObjects); +// NewSlateWindow->RequestDestroyWindow(); +// return FReply::Handled(); +// }) +// ] +// +// // Cancel Button +// + SUniformGridPanel::Slot(1, 0) +// [ +// SNew(SButton) +// .HAlign(HAlign_Center) +// .VAlign(VAlign_Center) +// .ContentPadding(FCoreStyle::Get().GetMargin("StandardDialog.ContentPadding")) +// .ButtonStyle(&_GetEditorStyle().GetWidgetStyle("Button")) +// .Content() +// [ +// SNew(STextBlock) +// .TextStyle( &_GetEditorStyle().GetWidgetStyle("ButtonText") ) +// .Text( LOCTEXT("HoudiniTools_Details_Cancel","Cancel") ) +// ] +// .OnClicked_Lambda([NewSlateWindow]() -> FReply +// { +// NewSlateWindow->RequestDestroyWindow(); +// return FReply::Handled(); +// }) +// ] +// ] // Button Row +// ] // SBorder +// ); +// +// return NewSlateWindow; +// } + +TSharedRef +SHoudiniToolsPanel::CreateFloatingWindow( + const FText& WindowTitle, + const FVector2D InitialSize + ) const +{ + TSharedRef NewSlateWindow = SNew(SWindow) + .Title(WindowTitle) + .ClientSize(InitialSize) + .SizingRule( ESizingRule::FixedSize ) + .SupportsMaximize(false).SupportsMinimize(false); + + // If the main frame exists parent the window to it + TSharedPtr< SWindow > ParentWindow; + if ( FModuleManager::Get().IsModuleLoaded("MainFrame") ) + { + IMainFrameModule& MainFrame = FModuleManager::GetModuleChecked("MainFrame"); + ParentWindow = MainFrame.GetParentWindow(); + } + + if ( ParentWindow.IsValid() ) + { + // Parent the window to the main frame + FSlateApplication::Get().AddWindowAsNativeChild( NewSlateWindow, ParentWindow.ToSharedRef() ); + } + else + { + FSlateApplication::Get().AddWindow( NewSlateWindow ); + } + + FDetailsViewArgs Args; + Args.bHideSelectionTip = true; + Args.bLockable = false; + Args.bAllowMultipleTopLevelObjects = true; + Args.ViewIdentifier = TEXT("Houdini Tools Properties"); + Args.NameAreaSettings = FDetailsViewArgs::HideNameArea; + Args.bShowPropertyMatrixButton = false; + Args.bShowOptions = false; + Args.bShowModifiedPropertiesOption = false; + Args.bShowObjectLabel = false; + + return NewSlateWindow; +} + +FReply +SHoudiniToolsPanel::OnKeyDown( const FGeometry& InGeometry, const FKeyEvent& InKeyEvent ) +{ + if ( InKeyEvent.GetKey() == EKeys::Delete ) + { + HideActiveToolFromCategory(); + return FReply::Handled(); + } + else if ( InKeyEvent.GetKey() == EKeys::Enter ) + { + EditActiveHoudiniTool(); + return FReply::Handled(); + } + else if (InKeyEvent.GetKey() == EKeys::F5 ) + { + RefreshPanel(); + return FReply::Handled(); + } + + return FReply::Unhandled(); +} + +bool SHoudiniToolsPanel::CanShowActiveToolInCategory() const +{ + if ( !ActiveTool.IsValid() ) + return false; + + if (ActiveTool->CategoryType == EHoudiniToolCategoryType::Package) + { + return GetHoudiniTools().CanRemoveToolExcludeFromPackageCategory(ActiveTool->GetAssetObject(), ActiveCategoryName); + } + else if (ActiveTool->CategoryType == EHoudiniToolCategoryType::User) + { + // User categories does not typically contain 'hidden' tools. We simple 'remove' tools from user categories + // instead of hiding them. + return false; + //return GetHoudiniTools().CanRemoveToolExcludeFromPackageCategory(ActiveTool->GetAssetObject(), ActiveCategoryName); + } + + return false; +} + +void +SHoudiniToolsPanel::ShowActiveToolInCategory() +{ + if ( !ActiveTool.IsValid() ) + return; + + if (ActiveTool->CategoryType == EHoudiniToolCategoryType::Package) + { + GetHoudiniTools().RemoveToolExcludeFromPackageCategory(ActiveTool->GetAssetObject(), ActiveCategoryName); + } + else if (ActiveTool->CategoryType == EHoudiniToolCategoryType::User) + { + FHoudiniToolsEditor::RemoveToolFromUserCategory(ActiveTool->GetAssetObject(), ActiveCategoryName); + } + + // Manually trigger a rebuild. The ExcludeToolFromCategory will modify the package, but it doesn't trigger an update because + // the package is not automatically saved. + RequestPanelRefresh(); +} + +bool SHoudiniToolsPanel::CanHideActiveToolFromCategory() +{ + if ( !ActiveTool.IsValid() ) + return false; + + // If this tool is not yet excluded from the active category, we can hide it. + return !ActiveTool->ExcludedFromCategories.Contains(ActiveCategoryName); +} + +void +SHoudiniToolsPanel::HideActiveToolFromCategory() +{ + if ( !ActiveTool.IsValid() ) + return; + + if (ActiveTool->CategoryType == EHoudiniToolCategoryType::Package) + { + GetHoudiniTools().ExcludeToolFromPackageCategory(ActiveTool->GetAssetObject(), ActiveCategoryName, false); + } + else if (ActiveTool->CategoryType == EHoudiniToolCategoryType::User) + { + FHoudiniToolsEditor::RemoveToolFromUserCategory(ActiveTool->GetAssetObject(), ActiveCategoryName); + } + + // Manually trigger a rebuild. The ExcludeToolFromCategory will modify the package, but it doesn't trigger an update because + // the package is not automatically saved. + RequestPanelRefresh(); +} + +void +SHoudiniToolsPanel::RefreshPanel() +{ + UpdateHoudiniToolDirectories(); + RebuildCategories(); +} + +bool +SHoudiniToolsPanel::IsActiveHoudiniToolEditable() const +{ + if ( !ActiveTool.IsValid() ) + return false; + + return true; +} + + +void +SHoudiniToolsPanel::CreateEmptyToolsPackage() +{ + if (!CreatePackageWindow.IsValid()) + { + // Create package window is not valid. Create a new one. + TSharedPtr Window = CreateFloatingWindow( + NSLOCTEXT("HoudiniToolsTypeActions", "HoudiniTools_Action_CreateEmptyToolsPackage", "Create Package"), + FVector2D(700,350) + ); + + Window->SetContent(SNew(SHoudiniToolNewPackage)); + } + else + { + TSharedPtr WindowPtr = CreatePackageWindow.Pin(); + WindowPtr->BringToFront(); + } +} + +void +SHoudiniToolsPanel::ImportToolsPackage() +{ + TSharedRef< SWindow > Window = CreateFloatingWindow( + NSLOCTEXT("HoudiniToolsTypeActions", "HoudiniTools_Action_ImportToolsPackage", "Import Package"), + FVector2D(700,350) + ); + + // TODO + Window->SetContent(SNew(SHoudiniToolImportPackage)); + + // TArray HoudiniToolsDirs; + // GetHoudiniTools().FindAllProjectHoudiniToolsPackages(HoudiniToolsDirs); +} + +void +SHoudiniToolsPanel::HandleImportSideFXTools() +{ + int NumImportedHDAs = 0; + const bool bResult = FHoudiniToolsEditor::ImportSideFXTools(&NumImportedHDAs); + + if (bResult) + { + const FText Title = LOCTEXT("ImportSideFXTools_ImportSuccess_Title", "Import Success"); + FMessageDialog::Open(EAppMsgType::Ok, + FText::Format(LOCTEXT("ImportSideFXTools_ImportSuccess_Title", "Imported {0} HDAs."), + FText::AsNumber(NumImportedHDAs)), +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + Title +#else + &Title +#endif + ); + } + else + { + const FText Title = LOCTEXT("ImportSideFXTools_ImportFailed_Title", "Import Failed"); + FMessageDialog::Open(EAppMsgType::Ok, + FText::Format( LOCTEXT("ImportSideFXTools_ImportFailed", "Could not import SideFX Tools package."), + FText::AsNumber(NumImportedHDAs)), +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + Title +#else + &Title +#endif + ); + } +} + +void +SHoudiniToolsPanel::HandleAddToolToUserCategory() const +{ + TSharedRef< SWindow > Window = CreateFloatingWindow( + NSLOCTEXT("HoudiniTools", "HoudiniTools_AddToUserCategory_Title", "Add to user category"), + FVector2D(700,350) + ); + + Window->SetContent( + SNew(SHoudiniToolAddToUserCategory) + .ActiveTool(ActiveTool) + ); +} + + +void +SHoudiniToolsPanel::HandleAddToFavorites() const +{ + FHoudiniToolsEditor::AddToolToUserCategory(ActiveTool->GetAssetObject(), FHoudiniToolsEditor::GetFavoritesCategoryName().ToString()); +} + + +void +SHoudiniToolsPanel::HandleRemoveFromFavorites() const +{ + FHoudiniToolsEditor::RemoveToolFromUserCategory(ActiveTool->GetAssetObject(), FHoudiniToolsEditor::GetFavoritesCategoryName().ToString()); +} + + +void +SHoudiniToolsPanel::HandleEditUserCategory(const FString& CategoryName) const +{ + FHoudiniEngineCommands::ShowPluginEditorSettings(); +} + + +void +SHoudiniToolsPanel::HandleEditPackageCategory(const FString& CategoryName) const +{ + TSoftObjectPtr PackagePtr = GetHoudiniTools().FindFirstPackageContainingCategory(CategoryName); + if (!PackagePtr.IsValid()) + { + return; + } + + if (!GEditor) + { + return; + } + GEditor->EditObject(PackagePtr.LoadSynchronous()); +} + + +void +SHoudiniToolsPanel::HandleBrowseToCategoryPackage(const FString& CategoryName) const +{ + TSoftObjectPtr PackagePtr = GetHoudiniTools().FindFirstPackageContainingCategory(CategoryName); + FHoudiniToolsEditor::BrowseToObjectInContentBrowser(PackagePtr.LoadSynchronous()); +} + + +void +SHoudiniToolsPanel::HandleSaveToolsInCategory(const FString& CategoryName, const EHoudiniToolCategoryType CategoryType) const +{ + const TMap>& ToolsMap = GetHoudiniTools().GetCategorizedTools(); + FHoudiniToolCategory Category(CategoryName, CategoryType); + if (!ToolsMap.Contains(Category)) + { + return; + } + TSharedPtr ToolsList = ToolsMap.FindChecked(Category); + if (!ToolsList.IsValid()) + { + return; + } + + // Collect all the package to save + TArray PackagesToSave; + for (TSharedPtr Tool : ToolsList->Tools) + { + if (!Tool.IsValid()) + { + continue; + } + UHoudiniAsset* Asset = Tool->HoudiniAsset.LoadSynchronous(); + UHoudiniPreset* Preset = Tool->HoudiniPreset.LoadSynchronous(); + if (IsValid(Asset) && IsValid(Asset->GetPackage())) + { + PackagesToSave.Add( Asset->GetPackage() ); + } + if (IsValid(Preset) && IsValid(Preset->GetPackage())) + { + PackagesToSave.Add( Preset->GetPackage() ); + } + } + + UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, true); +} + + +void +SHoudiniToolsPanel::HandleImportToolsIntoCategory(const FString& CategoryName, const EHoudiniToolCategoryType CategoryType) const +{ + + FAssetToolsModule& AssetToolsModule = FModuleManager::Get().LoadModuleChecked(TEXT("AssetTools")); + TSoftObjectPtr PackagePtr = GetHoudiniTools().FindFirstPackageContainingCategory(CategoryName); + UHoudiniToolsPackageAsset* PackageAsset = PackagePtr.LoadSynchronous(); + if (!IsValid(PackageAsset)) + { + HOUDINI_LOG_ERROR(TEXT("Category '%s' does not have a valid package"), *CategoryName); + return; + } + const FString Path = FPaths::GetPath( PackageAsset->GetPathName() ); + const TArray ImportedObjects = AssetToolsModule.Get().ImportAssetsWithDialog(Path); + if (ImportedObjects.Num() > 0) + { + FHoudiniToolsEditor::BrowseToObjectInContentBrowser(ImportedObjects[0]); + } +} + + +void +SHoudiniToolsPanel::UpdateHoudiniToolDirectories() +{ + FHoudiniToolsEditor& HoudiniTools = FHoudiniToolsPanelUtils::GetHoudiniTools(); + // Refresh the Editor's Houdini Tool list + // Retrieve all tools, including hidden ones. The categories will filter out hidden tools if needed. + HoudiniTools.UpdateHoudiniToolListFromProject(true); +} + + +void +SHoudiniToolsPanel::RequestPanelRefresh() +{ + bRefreshPanelRequested = true; +} + +void +SHoudiniToolsPanel::RebuildCategories() +{ + // Rebuild categories container + CategoriesContainer->ClearChildren(); + + const TMap>& CategoriesToolsMap = GetHoudiniTools().GetCategorizedTools(); + TArray SortedKeys; + CategoriesToolsMap.GetKeys(SortedKeys); + SortedKeys.Sort(); + + // Store all the category names for filtering. + FilterCategoryList.Empty(); + + for (const FHoudiniToolCategory& Category : SortedKeys) + { + FilterCategoryList.Add(Category.Name); + FString CategoryName = Category.Name; + EHoudiniToolCategoryType CategoryType = Category.CategoryType; + + // Add Category view widget + TSharedPtr CategorizedTools = CategoriesToolsMap.FindChecked(Category); + CategoriesContainer->AddSlot() + .Padding(0,0,0,5) + .AutoHeight() + [ + SNew(SHoudiniToolCategory) + .CategoryLabel(FText::FromString(Category.Name)) + .CategoryType(Category.CategoryType) + .ShowEmptyCategory_Lambda( [this]() -> bool { return bShowEmptyCategories; } ) + .ShowHoudiniAssets_Lambda( [this]() -> bool { return bShowHoudiniAssets; } ) + .ShowPresets_Lambda( [this]() -> bool { return bShowPresets; } ) + .HoudiniToolsItemSource(CategorizedTools) + .ViewMode(ViewMode) + .OnGenerateTile_Lambda( [this, CategoryName](TSharedPtr< FHoudiniTool > HoudiniTool, const TSharedRef< STableViewBase >& OwnerTable) + { + return MakeTileViewWidget(HoudiniTool, CategoryName, OwnerTable); + } ) + .OnGenerateRow_Lambda([this, CategoryName](TSharedPtr< FHoudiniTool > HoudiniTool, const TSharedRef< STableViewBase >& OwnerTable) + { + return MakeListViewWidget(HoudiniTool, CategoryName, OwnerTable); + } ) + .OnToolSelectionChanged( this, &SHoudiniToolsPanel::OnToolSelectionChanged ) + .OnMouseButtonDoubleClick( this, &SHoudiniToolsPanel::OnDoubleClickedListViewWidget ) + .OnToolContextMenuOpening_Lambda([this, CategoryName, CategoryType]() + { + return ConstructHoudiniToolContextMenu(); + }) + .OnCategoryContextMenuOpening_Lambda([this, CategoryName, CategoryType]() + { + return ConstructCategoryContextMenu(CategoryName, CategoryType); + }) + .IsVisible_Lambda([&, Category](){ return !FilterHiddenCategories.Contains(Category.Name); }) + .ShowHiddenTools_Lambda([this]() -> bool { return bShowHiddenTools; }) + ]; + } +} + +void +SHoudiniToolsPanel::HandleToolChanged() +{ + // An external tool change was detected. + // Rebuild the categories. + RequestPanelRefresh(); +} + + +void +SHoudiniToolsPanel::ForEachCategory(const TFunctionRef& ForEachCategoryFunc) const +{ + // Clear selections on any other categories. + FChildren* ChildWidgets = CategoriesContainer->GetChildren(); + const int32 ChildCount = ChildWidgets->Num(); + for (int32 i = 0; i < ChildCount; i++) + { + TSharedPtr Widget = ChildWidgets->GetChildAt(i); + if (Widget.IsValid() && Widget->GetType().Compare(SHoudiniToolCategory::TypeName)==0) + { + SHoudiniToolCategory* ChildCategory = static_cast( Widget.Get() ); + if (!ForEachCategoryFunc(ChildCategory)) + break; // Stop iterating if the Lambda returned false + } + } +} + +void +SHoudiniToolsPanel::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) +{ + if (bRefreshPanelRequested) + { + // Update tools list and refresh categories view. + RefreshPanel(); + bRefreshPanelRequested = false; + } + + SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); +} + +void +SHoudiniToolsPanel::LoadConfig() +{ + check(GConfig); + const FString SettingsString = TEXT("HoudiniTools"); + + // Show Empty Categories + if (!GConfig->GetBool(*SettingsIniSection, *(SettingsString + TEXT(".ShowEmptyCategories")), bShowEmptyCategories, GEditorPerProjectIni)) + { + bShowEmptyCategories = true; + } + + // Show Hidden tools + if (!GConfig->GetBool(*SettingsIniSection, *(SettingsString + TEXT(".ShowHiddenTools")), bShowHiddenTools, GEditorPerProjectIni)) + { + bShowHiddenTools = false; + } + + // Show Houdini Assets + if (!GConfig->GetBool(*SettingsIniSection, *(SettingsString + TEXT(".ShowHoudiniAssets")), bShowHoudiniAssets, GEditorPerProjectIni)) + { + bShowHoudiniAssets = true; + } + + // Show Presets + if (!GConfig->GetBool(*SettingsIniSection, *(SettingsString + TEXT(".ShowPresets")), bShowPresets, GEditorPerProjectIni)) + { + bShowPresets = true; + } + + // Filter - Show All + if (!GConfig->GetBool(*SettingsIniSection, *(SettingsString + TEXT(".Filter.ShowAll")), bFilterShowAll, GEditorPerProjectIni)) + { + bFilterShowAll = true; + } + + // Filter - Hidden categories + TArray HiddenCategories; + FilterHiddenCategories.Empty(); + if (GConfig->GetArray(*SettingsIniSection, *(SettingsString + TEXT(".Filter.HiddenCategories")), HiddenCategories, GEditorPerProjectIni)) + { + FilterHiddenCategories.Append(HiddenCategories); + } + + // View Mode + FString ViewModeName; + if (!GConfig->GetString(*SettingsIniSection, *(SettingsString + TEXT(".ViewMode")), ViewModeName, GEditorPerProjectIni)) + { + ViewModeName = TEXT("Tile"); + } + + if (ViewModeName == TEXT("List")) + { + ViewMode = EHoudiniToolsViewMode::ListView; + } + else // default: (ViewModeName == TEXT("Tile")) + { + ViewMode = EHoudiniToolsViewMode::TileView; + } + + // Auto Refresh + if (!GConfig->GetBool(*SettingsIniSection, *(SettingsString + TEXT(".AutoRefresh")), bAutoRefresh, GEditorPerProjectIni)) + { + bAutoRefresh = true; + } +} + +void +SHoudiniToolsPanel::SaveConfig() const +{ + check(GConfig); + const FString SettingsString = TEXT("HoudiniTools"); + + GConfig->SetBool(*SettingsIniSection, *(SettingsString + TEXT(".ShowEmptyCategories")), bShowEmptyCategories, GEditorPerProjectIni); + GConfig->SetBool(*SettingsIniSection, *(SettingsString + TEXT(".ShowHiddenTools")), bShowHiddenTools, GEditorPerProjectIni); + GConfig->SetBool(*SettingsIniSection, *(SettingsString + TEXT(".ShowHoudiniAssets")), bShowHoudiniAssets, GEditorPerProjectIni); + GConfig->SetBool(*SettingsIniSection, *(SettingsString + TEXT(".ShowPresets")), bShowPresets, GEditorPerProjectIni); + GConfig->SetBool(*SettingsIniSection, *(SettingsString + TEXT(".AutoRefresh")), bAutoRefresh, GEditorPerProjectIni); + + GConfig->SetBool(*SettingsIniSection, *(SettingsString + TEXT(".Filter.ShowAll")), bFilterShowAll, GEditorPerProjectIni); + GConfig->SetArray(*SettingsIniSection, *(SettingsString + TEXT(".Filter.HiddenCategories")), FilterHiddenCategories.Array(), GEditorPerProjectIni); + + FString ViewModeName; + switch (ViewMode) + { + case EHoudiniToolsViewMode::ListView: + ViewModeName = TEXT("List"); + break; + case EHoudiniToolsViewMode::TileView: + ViewModeName = TEXT("Tile"); + break; + default: + ViewModeName = TEXT("Tile"); + break; + } + GConfig->SetString(*SettingsIniSection, *(SettingsString + TEXT(".ViewMode")), *ViewModeName, GEditorPerProjectIni); + + GConfig->Flush(false, GEditorPerProjectIni); +} + + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineEditor/Private/SHoudiniToolsPanel.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "AssetTypeActions_HoudiniAsset.h" +#include "HoudiniEngineEditor.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Input/Reply.h" +#include "Widgets/SCompoundWidget.h" +#include "Widgets/Views/SListView.h" +#include "Misc/NotifyHook.h" +#include "HoudiniEngineToolTypes.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Views/STileView.h" + +#include "SHoudiniToolsPanel.generated.h" + +class SCheckBox; +class SDirectoryPicker; +class IDetailsView; +class ITableRow; +class STableViewBase; +struct FSlateBrush; +enum class ECheckBoxState : uint8; + +// View mode for the HoudiniTools panel +UENUM() +enum class EHoudiniToolsViewMode +{ + TileView, + ListView +}; + +// Toolbar of the tool panel +class SHoudiniToolsCategoryFilter : public SCompoundWidget +{ +public: + + DECLARE_DELEGATE_TwoParams(FOnCategoryFilterChanged, bool /*ShowAllSource*/, const TSet& /*HiddenCategories*/); + DECLARE_DELEGATE_OneParam(FOnShowAllChanged, bool /*ShowAllSource*/); + DECLARE_DELEGATE_TwoParams(FOnCategoryStateChanged, const FString& /*CategoryName*/, const bool /*bIsEnabled*/); + + SHoudiniToolsCategoryFilter(); + + SLATE_BEGIN_ARGS( SHoudiniToolsCategoryFilter ) + : _ShowAll(true) + , _HiddenCategoriesSource(nullptr) + , _CategoriesSource(nullptr) + {} + + SLATE_EVENT( FOnShowAllChanged, OnShowAllChanged ) + SLATE_EVENT( FOnCategoryStateChanged, OnCategoryStateChanged ) + + SLATE_ATTRIBUTE(bool, ShowAll) + SLATE_ARGUMENT(const TSet*, HiddenCategoriesSource) + SLATE_ARGUMENT(const TArray*, CategoriesSource) + + SLATE_END_ARGS(); + + void Construct( const FArguments& InArgs ); + + void SetShowAll(TAttribute InShowAll); + + bool IsCategoryEnabled(FString CategoryName) const; + +private: + TSharedPtr RebuildWidget(); + + TAttribute ShowAll; + const TSet* HiddenCategories; + const TArray* Categories; + + FOnShowAllChanged OnShowAllChanged; + FOnCategoryStateChanged OnCategoryStateChanged; +}; + + +// // Toolbar of the tool panel +// class SHoudiniToolsToolbar : public SCompoundWidget +// { +// public: +// +// DECLARE_DELEGATE_OneParam(FOn) +// +// SLATE_BEGIN_ARGS( SHoudiniToolsToolbar ) {} +// +// SLATE_EVENT( FOnTextChanged, OnSearchTextChanged ) +// SLATE_NAMED_SLOT( FArguments, FilterMenu ) +// SLATE_NAMED_SLOT( FArguments, ActionMenu ) +// SLATE_ATTRIBUTE( ) +// SLATE_END_ARGS(); +// +// void Construct( const FArguments& InArgs ); +// +// protected: +// +// }; + +/** The list view mode of the asset view */ +class SHoudiniToolListView : public SListView< TSharedPtr > +{ + public: + virtual bool SupportsKeyboardFocus() const override { return true; } + virtual FReply OnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override + { + return FReply::Unhandled(); + } +}; + +/** The grid/tile view mode of the asset view */ +class SHoudiniToolTileView : public STileView< TSharedPtr > +{ + public: + virtual bool SupportsKeyboardFocus() const override { return true; } + virtual FReply OnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override + { + return FReply::Unhandled(); + } +}; + +enum class ECategoryContentType: uint8 +{ + Tools = 0, + NoToolsInCategory = 1, + NoVisibleToolsInCategory = 2 +}; + +// A collapsible category containing a Tile view of houdini tools. +class SHoudiniToolCategory : public SCompoundWidget +{ +public: + SHoudiniToolCategory(); + + // Typename of this widget + static FName TypeName; + + typedef SListView< TSharedPtr > FBaseViewType; + typedef TSharedPtr FItemType; + typedef TListTypeTraits< FItemType >::NullableType FNullableItemType; + + typedef TSlateDelegates< FItemType >::FOnGenerateRow FOnGenerateRow; + typedef TSlateDelegates< FItemType >::FOnMouseButtonClick FOnMouseButtonClick; + typedef TSlateDelegates< FItemType >::FOnMouseButtonDoubleClick FOnMouseButtonDoubleClick; + + DECLARE_DELEGATE_ThreeParams( FOnToolSelectionChanged, + SHoudiniToolCategory* /*CategoryWidget*/, + const TSharedPtr& /*HoudiniTool*/, + ESelectInfo::Type /*SelectInfo*/ + ); + + + SLATE_BEGIN_ARGS( SHoudiniToolCategory ) + : _ShowEmptyCategory(true) + , _ShowHoudiniAssets(true) + , _ShowPresets(true) + , _ShowHiddenTools(true) + , _ViewMode(EHoudiniToolsViewMode::TileView) + , _IsVisible(true) + {} + SLATE_EVENT( FOnGenerateRow, OnGenerateTile ) + SLATE_EVENT( FOnGenerateRow, OnGenerateRow ) + SLATE_EVENT( FOnMouseButtonClick, OnMouseButtonClick ) + SLATE_EVENT( FOnMouseButtonDoubleClick, OnMouseButtonDoubleClick ) + SLATE_EVENT( FOnContextMenuOpening, OnToolContextMenuOpening ) + SLATE_EVENT( FOnContextMenuOpening, OnCategoryContextMenuOpening ) + SLATE_EVENT( FOnToolSelectionChanged, OnToolSelectionChanged ) + + SLATE_ATTRIBUTE( FText, CategoryLabel ) + SLATE_ATTRIBUTE( bool, ShowEmptyCategory ) + SLATE_ATTRIBUTE( bool, ShowHoudiniAssets ) + SLATE_ATTRIBUTE( bool, ShowPresets ) + SLATE_ATTRIBUTE( bool, ShowHiddenTools ) + SLATE_ATTRIBUTE( EHoudiniToolCategoryType, CategoryType ) + SLATE_ATTRIBUTE( TSharedPtr, HoudiniToolsItemSource ) + SLATE_ATTRIBUTE( EHoudiniToolsViewMode, ViewMode ) + SLATE_ATTRIBUTE( bool, IsVisible ) + + SLATE_END_ARGS(); + + void Construct( const FArguments& InArgs ); + + // Get the active tool for this category, if any. + TSharedPtr GetActiveTool() const { return ActiveTool; } + FString GetCategoryLabel() const { return CategoryLabel; } + EHoudiniToolCategoryType GetCategoryType() const { return CategoryType; } + + FHoudiniToolCategory GetHoudiniToolCategory() const { return FHoudiniToolCategory(CategoryLabel, CategoryType); } + + void SetFilterString(const FString& NewFilterString); + + void ClearSelection(); + void RequestRefresh(); + + +protected: + + void UpdateVisibleItems(); + + virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override; + virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + TSharedPtr HandleContextMenuOpening(); + + TSharedPtr HoudiniToolsView; + + FString CategoryLabel; + EHoudiniToolCategoryType CategoryType; + FString FilterString; + + TAttribute ShowEmptyCategory; + TAttribute ShowHoudiniAssets; + TAttribute ShowPresets; + TAttribute ShowHiddenTools; + TAttribute IsVisible; + TAttribute ToolContextMenuOpening; + TAttribute CategoryContextMenuOpening; + + TSharedPtr ActiveTool; + + // TODO: Maybe this should become a shared ref? + // The source entries are intentionally stored as a SharedPtr since the category / tool list + // may be removed in HoudiniTools but we don't want this widget to crash if that happens. + TSharedPtr SourceEntries; + TArray< TSharedPtr > VisibleEntries; + + // Use ECategoryContentType to set this switcher's index. + TSharedPtr CategoryContentSwitcher; + float TextWrapWidth; +}; + + +// Class describing a Houdini Tool directory +// adding a UClass was necessary to use the PropertyEditor window +UCLASS(EditInlineNew) +class UHoudiniToolDirectoryProperties : public UObject +{ + GENERATED_UCLASS_BODY() + + public: + + /** Custom Houdini Tool Directories **/ + UPROPERTY(EditAnywhere, Category = CustomHoudiniTools) + TArray CustomHoudiniToolsDirectories; +}; + + +// Houdini Tool Helper operations. +// These operations should be callable from the UI as well as no-UI code. We don't want to +// end up in a situation where we are only able to perform these operations from the frontend. +UCLASS() +class UHoudiniToolHelpers : public UObject +{ + GENERATED_BODY() + +public: + /** Create a new empty package */ + static bool CreateEmptyPackage(const FName& PackageName); + + /** Import a package from disk */ + static bool ImportPackage(const FName& PackageName, const FString& PackagePath); +}; + + +/** + * SHoudiniToolNewPackage + */ + +class SHoudiniToolNewPackage : public SCompoundWidget, public FNotifyHook +{ +public: + DECLARE_DELEGATE_OneParam(FOnCreated, const FString&); + DECLARE_DELEGATE(FOnCanceled); + + SLATE_BEGIN_ARGS( SHoudiniToolNewPackage ) {} + SLATE_EVENT( FOnCreated, OnCreated ) + SLATE_EVENT( FOnCanceled, OnCanceled ) + SLATE_END_ARGS(); + + /** + * Default constructor. + */ + SHoudiniToolNewPackage(); + + void Construct( const FArguments& InArgs ); + + void OnPackageNameTextChanged(const FText& Text); + void OnPackageNameTextCommitted(const FText& Text, ETextCommit::Type Arg); + FText OnGetPackagePathText() const; + + void OnPackageCategoryTextChanged(const FText& Text); + void OnPackageCategoryTextCommitted(const FText& Text, ETextCommit::Type Arg); + + EVisibility GetNameErrorLabelVisibility() const; + auto GetNameErrorLabelText() const -> FText; + + bool IsCreateEnabled() const; + + // Create the empty tools package + FReply HandleCreateClicked(); + // Cancel this dialog + FReply HandleCancelClicked(); + +protected: + + TSharedPtr PackageNameEditBox; + TSharedPtr PackageCategoryEditBox; + + FText GetCategory() const; + + void UpdatePackageName(FText NewName); + void CloseContainingWindow(); + + /** The calculated name for the new Houdini Tools package */ + FText CalculatedPackageName; + FText PackageNameError; + bool bValidPackageName; + bool bSyncCategory; + + FOnCreated OnCreated; + FOnCanceled OnCanceled; +}; + +/** + * SHoudiniToolImportPackage + */ + +class SHoudiniToolImportPackage : public SCompoundWidget, public FNotifyHook +{ +public: + DECLARE_DELEGATE_OneParam(FOnImported, const FString&); + DECLARE_DELEGATE(FOnCanceled); + + SLATE_BEGIN_ARGS( SHoudiniToolImportPackage ) {} + SLATE_EVENT( FOnImported, OnImported ) + SLATE_EVENT( FOnCanceled, OnCanceled ) + SLATE_END_ARGS(); + + /** + * Default constructor. + */ + SHoudiniToolImportPackage(); + + void Construct( const FArguments& InArgs ); + + // Error label + + EVisibility GetNameErrorLabelVisibility() const; + auto GetNameErrorLabelText() const -> FText; + + // Package directory + + void OnDirectoryChanged(const FString& Directory); + + void OnPackageNameTextChanged(const FText& Text); + void OnPackageNameTextCommitted(const FText& Text, ETextCommit::Type Arg); + FText OnGetPackagePathText() const; + + bool IsImportEnabled() const; + bool IsNextEnabled() const; + + // Create the empty tools package + FReply HandleImportClicked(); + EVisibility GetImportVisibility() const; + + FReply HandleNextClicked() const; + EVisibility GetNextVisibility() const; + + FReply HandleCreateClicked(); + bool IsCreateEnabled() const; + EVisibility GetCreateVisibility() const; + + FReply HandleBackClicked() const; + EVisibility GetBackVisibility() const; + + // Cancel this dialog + FReply HandleCancelClicked(); + +protected: + + static FHoudiniToolsEditor& GetHoudiniTools() { return FHoudiniEngineEditor::Get().GetHoudiniTools(); } + + TSharedPtr WidgetSwitcher; + + TSharedPtr DirectoryPicker; + TSharedPtr PackageNameEditBox; + TSharedPtr PackageCategoryEditBox; + + TSharedPtr CreateExternalJSON; + + + FText GetCategory() const; + + void UpdatePackageName(const FString& InName); + void CloseContainingWindow(); + + // Package Directory state + bool bIsValidPackageDirectory; + FText DirectoryInvalidReason; + + // Package JSON state + bool bHasPackageJSON; + + // Package Name state + FString NewPackageName; + FText PackageNameError; + bool bValidPackageName; + + // If the external directory being imported doesn't + // have a package description, create a default one. + bool bCreateMissingPackageJSON; + + FOnImported OnImported; + FOnCanceled OnCanceled; +}; + + +/** + * Add Tool to User Category + */ + +class SHoudiniToolAddToUserCategory : public SCompoundWidget, public FNotifyHook +{ +public: + SLATE_BEGIN_ARGS( SHoudiniToolAddToUserCategory ) {} + + SLATE_ATTRIBUTE(TSharedPtr, ActiveTool) + + SLATE_END_ARGS(); + + SHoudiniToolAddToUserCategory(); + + void Construct( const FArguments& InArgs ); + + +protected: + + FText ToolPackagePathText(); + + // Add To Category Button + FReply HandleAddToCategoryClicked(); + bool GetAddToCategoryEnabled() const; + EVisibility GetAddToCategoryVisibility() const; + + // Goto Create New Category button + FReply HandleGotoCreateCategoryClicked(); + bool GetGotoCreateCategoryEnabled() const; + EVisibility GetGotoCreateCategoryVisibility() const; + + // Add New Category Button + FReply HandleAddNewCategoryClicked(); + bool GetCreateCategoryEnabled() const; + EVisibility GetCreateCategoryVisibility() const; + + // Back Button + FReply HandleBackClicked(); + EVisibility GetBackButtonVisibility() const; + + // Cancel Button + FReply HandleCancelClicked(); + + // User Category Selection + void OnUserCategorySelectionChanged(TSharedPtr NewSelection, ESelectInfo::Type SelectType); + + // User Category Creation + void OnUserCategoryCreateTextChanged(const FText& NewText); + void OnUserCategoryCreateTextCommitted(const FText& NewText, ETextCommit::Type CommitType); + + void CloseContainingWindow(); + + TSharedPtr ActiveTool; + UHoudiniToolsPackageAsset* Package; + + TSharedPtr WidgetSwitcher; + + TSharedPtr AddToCategoryView; + TSharedPtr CreateCategoryView; + + TArray> UserCategories; + TSharedPtr SelectedCategory; + + FString UserCategoryToCreate; +}; + +/** + * Houdini Tools panel + */ + +class SHoudiniToolsPanel : public SCompoundWidget, public FNotifyHook +{ +public: + SLATE_BEGIN_ARGS( SHoudiniToolsPanel ) {} + SLATE_END_ARGS(); + + SHoudiniToolsPanel(); + virtual ~SHoudiniToolsPanel() override; + + void Construct( const FArguments& InArgs ); + + static FTransform GetDefaulToolSpawnTransform(); + static FTransform GetMeanWorldSelectionTransform(); + + /** Instantiate the selected HoudiniTool and assigns input depending on the current selection and tool type **/ + static void InstantiateHoudiniTool( FHoudiniTool* HoudiniTool ); + + /** Handler for Key presses**/ + virtual FReply OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent ); + + bool IsActiveHoudiniToolEditable() const; + + // Iterate over all the category widgets until the lambda returns false. + void ForEachCategory(const TFunctionRef& ForEachCategoryFunc) const; + + virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override; + +private: + static FHoudiniToolsEditor& GetHoudiniTools() { return FHoudiniEngineEditor::Get().GetHoudiniTools(); } + + void LoadConfig(); + void SaveConfig() const; + + bool CanShowActiveToolInCategory() const; + void ShowActiveToolInCategory(); + + /** Remove the current tool from the tool list **/ + bool CanHideActiveToolFromCategory(); + void HideActiveToolFromCategory(); + + void RefreshPanel(); + + // Actions Menu + + /** Create an empty tools package in the project **/ + void CreateEmptyToolsPackage(); + + /** Import a tools package **/ + void ImportToolsPackage(); + + void HandleImportSideFXTools(); + + void HandleAddToolToUserCategory() const; + + void HandleAddToFavorites() const; + + void HandleRemoveFromFavorites() const; + + void HandleEditUserCategory(const FString& CategoryName) const; + + void HandleEditPackageCategory(const FString& CategoryName) const; + + void HandleBrowseToCategoryPackage(const FString& CategoryName) const; + + void HandleSaveToolsInCategory(const FString& CategoryName, const EHoudiniToolCategoryType CategoryType) const; + + void HandleImportToolsIntoCategory(const FString& CategoryName, const EHoudiniToolCategoryType CategoryType) const; + + + static const FString SettingsIniSection; + + /** Make a widget for the list view display */ + TSharedRef MakeListViewWidget( TSharedPtr HoudiniTool, const FString& CategoryName, const TSharedRef& OwnerTable ); + + TSharedRef MakeTileViewWidget( TSharedPtr HoudiniTool, const FString& CategoryName, const TSharedRef& OwnerTable ); + + /** Delegate for when the list view selection changes */ + void OnToolSelectionChanged( SHoudiniToolCategory* Category, const TSharedPtr& HoudiniTool, ESelectInfo::Type SelectionType ); + + /** Begin dragging a list widget */ + FReply OnDraggingListViewWidget( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent ); + + /** Handler for double clicking on a Houdini tool **/ + void OnDoubleClickedListViewWidget( TSharedPtr ListItem ); + + // /** Return the appropriate context menu, depending on current Tools panel context **/ + // TSharedPtr ConstructContextMenu(const FString& CategoryName, const EHoudiniToolCategoryType CategoryType); + + /** Handler for the right click context menu on a Houdini tool **/ + TSharedPtr ConstructHoudiniToolContextMenu(); + + /** Context menu for right clicking on any empty area in a category */ + TSharedPtr ConstructCategoryContextMenu(const FString& CategoryName, const EHoudiniToolCategoryType CategoryType); + + /** Construct menu widget for the Actions popup button **/ + TSharedPtr ConstructHoudiniToolsActionMenu(); + + TSharedPtr ConstructCategoryFilterMenu(); + + /** Shows a Property Window for editing the properties of new HoudiniTools**/ + void EditActiveHoudiniTool() const; + + /** Focus the content browser on the asset object that relates to the active tool **/ + void BrowseToActiveToolAsset() const; + + /** Focus the content browser on the HoudiniAsset that relates to the active tool **/ + void BrowseToSourceAsset() const; + + /** Focus the content browser on the package that owns the active tool **/ + void BrowseToActiveToolPackage() const; + + /** Handler for Save clicked in the Edit Active Tool dialog. **/ + // void HandleEditHoudiniToolSavedClicked(TSharedPtr HoudiniTool, TArray& InObjects ); + + TSharedRef CreateFloatingWindow( + const ::FText& WindowTitle, + const FVector2D IntialSize=FVector2D(400, 550)) const; + + void UpdateHoudiniToolDirectories(); + + void RequestPanelRefresh(); + void RebuildCategories(); + + void HandleToolChanged(); + + + // The active create package window is tracked here so that we can reuse the + // active window if summoned again. + TWeakPtr CreatePackageWindow; + + TSharedPtr ActiveTool; + FString ActiveCategoryName; + + // Temporary button style for testing. Remove when done. + FButtonStyle DGB_HelpButtonStyle; + + TSharedPtr CategoriesContainer; + bool bRefreshPanelRequested; + + // Tools filter string + FString FilterString; + + // Categories filter menu + TSharedPtr CategoryFilterWidget; + // When the user clicks on the ShowAll action, it immediately shows all categories (i.e., removes them + // from the FilterHiddenCategories array). If the user disables the Show All setting, all categories will immediate + // be hidden. Individual categories can still be toggled in both cases. + bool bFilterShowAll; + // Any categories that are NOT in this set is considered to be visible. + TSet FilterHiddenCategories; + // List of categories to be displayed by the in the filter. + TArray FilterCategoryList; + + // Tools Settings + bool bShowEmptyCategories; + bool bShowHiddenTools; + bool bAutoRefresh; + bool bShowHoudiniAssets; + bool bShowPresets; + + EHoudiniToolsViewMode ViewMode; + + // Styling + + // Brush used to draw the underline for icons in the HoudiniTools panel. + FSlateBrush UnderlineBrush; + + + // Handles to unsubscribe during destruction + FDelegateHandle AssetMemCreatedHandle; + FDelegateHandle AssetMemDeletedHandle; + FDelegateHandle AssetAddedHandle; + FDelegateHandle AssetRenamedHandle; + FDelegateHandle AssetUpdatedHandle; + FDelegateHandle AssetUpdatedOnDiskHandle; + FDelegateHandle AssetPostImportHandle; + FDelegateHandle UserToolCategoriesChangedHandle; + FDelegateHandle ToolOrPackageChangedHandle; +}; + +``` + +`Source/HoudiniEngineEditor/Private/SNewFilePathPicker.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "SNewFilePathPicker.h" + +#include "HoudiniApi.h" + +#include "DesktopPlatformModule.h" +#include "Framework/Application/SlateApplication.h" +#include "Runtime/Launch/Resources/Version.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/SBoxPanel.h" + +#define LOCTEXT_NAMESPACE "SNewFilePathPicker" + +/* SNewFilePathPicker interface + *****************************************************************************/ + +void SNewFilePathPicker::Construct( const FArguments& InArgs ) +{ + BrowseDirectory = InArgs._BrowseDirectory; + BrowseTitle = InArgs._BrowseTitle; + FilePath = InArgs._FilePath; + FileTypeFilter = InArgs._FileTypeFilter; + OnPathPicked = InArgs._OnPathPicked; + IsNewFile = InArgs._IsNewFile; + IsDirectoryPicker = InArgs._IsDirectoryPicker; + + ChildSlot + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .VAlign(VAlign_Center) + [ + // file path text box + SAssignNew(TextBox, SEditableTextBox) + .Text(this, &SNewFilePathPicker::HandleTextBoxText) + .Font(InArgs._Font) + .SelectAllTextWhenFocused(true) + .ClearKeyboardFocusOnCommit(false) + .OnTextCommitted(this, &SNewFilePathPicker::HandleTextBoxTextCommitted) + .SelectAllTextOnCommit(false) + .IsReadOnly(InArgs._IsReadOnly) + ] + + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(4.0f, 0.0f, 0.0f, 0.0f) + .VAlign(VAlign_Center) + [ + // browse button + SNew(SButton) + .ButtonStyle(InArgs._BrowseButtonStyle) + .ToolTipText(InArgs._BrowseButtonToolTip) + .OnClicked(this, &SNewFilePathPicker::HandleBrowseButtonClicked) + .ContentPadding(2.0f) + .ForegroundColor(FSlateColor::UseForeground()) + .IsFocusable(false) + [ + SNew(SImage) + .Image(InArgs._BrowseButtonImage) + .ColorAndOpacity(FSlateColor::UseForeground()) + ] + ] + ]; +} + + +/* SNewFilePathPicker callbacks + *****************************************************************************/ +#if PLATFORM_WINDOWS + +#include "Windows/WindowsHWrapper.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "Microsoft/COMPointer.h" +#else + #include "Windows/COMPointer.h" +#endif +#include "HAL/FileManager.h" +#include "Windows/AllowWindowsPlatformTypes.h" +#include +#include +#include "Windows/HideWindowsPlatformTypes.h" + +bool FileDialogShared( bool bSave, const void* ParentWindowHandle, const FString& DialogTitle, const FString& DefaultPath, const FString& DefaultFile, const FString& FileTypes, uint32 Flags, TArray& OutFilenames, int32& OutFilterIndex ) +{ + FScopedSystemModalMode SystemModalScope; + + bool bSuccess = false; + TComPtr FileDialog; + if ( SUCCEEDED( ::CoCreateInstance( bSave ? CLSID_FileSaveDialog : CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, bSave ? IID_IFileSaveDialog : IID_IFileOpenDialog, IID_PPV_ARGS_Helper( &FileDialog ) ) ) ) + { + if ( bSave ) + { + // Set the default "filename" + if ( !DefaultFile.IsEmpty() ) + { + FileDialog->SetFileName( *FPaths::GetCleanFilename( DefaultFile ) ); + } + DWORD dwFlags = 0; + FileDialog->GetOptions( &dwFlags ); + FileDialog->SetOptions( dwFlags & ~FOS_OVERWRITEPROMPT ); + } + else + { + // Set this up as a multi-select picker + if ( Flags & EFileDialogFlags::Multiple ) + { + DWORD dwFlags = 0; + FileDialog->GetOptions( &dwFlags ); + FileDialog->SetOptions( dwFlags | FOS_ALLOWMULTISELECT ); + } + } + + // Set up common settings + FileDialog->SetTitle( *DialogTitle ); + if ( !DefaultPath.IsEmpty() ) + { + // SHCreateItemFromParsingName requires the given path be absolute and use \ rather than / as our normalized paths do + FString DefaultWindowsPath = FPaths::ConvertRelativePathToFull( DefaultPath ); + DefaultWindowsPath.ReplaceInline( TEXT( "/" ), TEXT( "\\" ), ESearchCase::CaseSensitive ); + + TComPtr DefaultPathItem; + if ( SUCCEEDED( ::SHCreateItemFromParsingName( *DefaultWindowsPath, nullptr, IID_PPV_ARGS( &DefaultPathItem ) ) ) ) + { + FileDialog->SetFolder( DefaultPathItem ); + } + } + + // Set-up the file type filters + TArray UnformattedExtensions; + TArray FileDialogFilters; + { + // Split the given filter string (formatted as "Pair1String1|Pair1String2|Pair2String1|Pair2String2") into the Windows specific filter struct + FileTypes.ParseIntoArray( UnformattedExtensions, TEXT( "|" ), true ); + + if ( UnformattedExtensions.Num() % 2 == 0 ) + { + FileDialogFilters.Reserve( UnformattedExtensions.Num() / 2 ); + for ( int32 ExtensionIndex = 0; ExtensionIndex < UnformattedExtensions.Num();) + { + COMDLG_FILTERSPEC& NewFilterSpec = FileDialogFilters[FileDialogFilters.AddDefaulted()]; + NewFilterSpec.pszName = *UnformattedExtensions[ExtensionIndex++]; + NewFilterSpec.pszSpec = *UnformattedExtensions[ExtensionIndex++]; + } + } + } + FileDialog->SetFileTypes( FileDialogFilters.Num(), FileDialogFilters.GetData() ); + + // Show the picker + if ( SUCCEEDED( FileDialog->Show( (HWND)ParentWindowHandle ) ) ) + { + OutFilterIndex = 0; + if ( SUCCEEDED( FileDialog->GetFileTypeIndex( (UINT*)&OutFilterIndex ) ) ) + { + OutFilterIndex -= 1; // GetFileTypeIndex returns a 1-based index + } + + auto AddOutFilename = [&OutFilenames]( const FString& InFilename ) + { + FString& OutFilename = OutFilenames[OutFilenames.Add( InFilename )]; + OutFilename = IFileManager::Get().ConvertToRelativePath( *OutFilename ); + FPaths::NormalizeFilename( OutFilename ); + }; + + if ( bSave ) + { + TComPtr Result; + if ( SUCCEEDED( FileDialog->GetResult( &Result ) ) ) + { + PWSTR pFilePath = nullptr; + if ( SUCCEEDED( Result->GetDisplayName( SIGDN_FILESYSPATH, &pFilePath ) ) ) + { + bSuccess = true; + + // Apply the selected extension if the given filename doesn't already have one + FString SaveFilePath = pFilePath; + if ( FileDialogFilters.IsValidIndex( OutFilterIndex ) ) + { + // Build a "clean" version of the selected extension (without the wildcard) + FString CleanExtension = FileDialogFilters[OutFilterIndex].pszSpec; + if ( CleanExtension == TEXT( "*.*" ) ) + { + CleanExtension.Reset(); + } + else + { + const int32 WildCardIndex = CleanExtension.Find( TEXT( "*" ) ); + if ( WildCardIndex != INDEX_NONE ) + { + CleanExtension = CleanExtension.RightChop( WildCardIndex + 1 ); + } + } + + // We need to split these before testing the extension to avoid anything within the path being treated as a file extension + FString SaveFileName = FPaths::GetCleanFilename( SaveFilePath ); + SaveFilePath = FPaths::GetPath( SaveFilePath ); + + // Apply the extension if the file name doesn't already have one + if ( FPaths::GetExtension( SaveFileName ).IsEmpty() && !CleanExtension.IsEmpty() ) + { + SaveFileName = FPaths::SetExtension( SaveFileName, CleanExtension ); + } + + SaveFilePath /= SaveFileName; + } + AddOutFilename( SaveFilePath ); + + ::CoTaskMemFree( pFilePath ); + } + } + } + else + { + IFileOpenDialog* FileOpenDialog = static_cast( FileDialog.Get() ); + + TComPtr Results; + if ( SUCCEEDED( FileOpenDialog->GetResults( &Results ) ) ) + { + DWORD NumResults = 0; + Results->GetCount( &NumResults ); + for ( DWORD ResultIndex = 0; ResultIndex < NumResults; ++ResultIndex ) + { + TComPtr Result; + if ( SUCCEEDED( Results->GetItemAt( ResultIndex, &Result ) ) ) + { + PWSTR pFilePath = nullptr; + if ( SUCCEEDED( Result->GetDisplayName( SIGDN_FILESYSPATH, &pFilePath ) ) ) + { + bSuccess = true; + AddOutFilename( pFilePath ); + ::CoTaskMemFree( pFilePath ); + } + } + } + } + } + } + } + + return bSuccess; +} + +bool SaveFileDialog( const void* ParentWindowHandle, const FString& DialogTitle, const FString& DefaultPath, const FString& DefaultFile, const FString& FileTypes, uint32 Flags, TArray& OutFilenames ) +{ + int32 DummyFilterIndex = 0; + return FileDialogShared( true, ParentWindowHandle, DialogTitle, DefaultPath, DefaultFile, FileTypes, Flags, OutFilenames, DummyFilterIndex ); +} + +#else + +bool SaveFileDialog( const void* ParentWindowHandle, const FString& DialogTitle, const FString& DefaultPath, const FString& DefaultFile, const FString& FileTypes, uint32 Flags, TArray& OutFilenames ) +{ + IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); + return DesktopPlatform->SaveFileDialog( ParentWindowHandle, DialogTitle, DefaultPath, DefaultFile, FileTypes, Flags, OutFilenames ); +} +#endif + +FReply SNewFilePathPicker::HandleBrowseButtonClicked() +{ + IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); + + if (DesktopPlatform == nullptr) + { + return FReply::Handled(); + } + + const FString DefaultPath = BrowseDirectory.IsSet() + ? BrowseDirectory.Get() + : FPaths::GetPath(FilePath.Get()); + + // show the file browse dialog + if (!FSlateApplication::IsInitialized()) + return FReply::Handled(); + + TSharedPtr ParentWindow = FSlateApplication::Get().FindWidgetWindow(AsShared()); + void* ParentWindowHandle = (ParentWindow.IsValid() && ParentWindow->GetNativeWindow().IsValid()) + ? ParentWindow->GetNativeWindow()->GetOSWindowHandle() + : nullptr; + + if(!IsDirectoryPicker.Get()) + { + TArray OutFiles; + // CG: Use SaveFileDialog instead of OpenFileDialog + if ( IsNewFile.Get() ) + { + if ( SaveFileDialog( ParentWindowHandle, BrowseTitle.Get().ToString(), DefaultPath, TEXT( "" ), FileTypeFilter.Get(), EFileDialogFlags::None, OutFiles ) ) + { + OnPathPicked.ExecuteIfBound( OutFiles[0] ); + } + } + else + { + if ( DesktopPlatform->OpenFileDialog( ParentWindowHandle, BrowseTitle.Get().ToString(), DefaultPath, TEXT( "" ), FileTypeFilter.Get(), EFileDialogFlags::None, OutFiles ) ) + { + OnPathPicked.ExecuteIfBound( OutFiles[0] ); + } + } + } + else + { + FString OutDir; + if (DesktopPlatform->OpenDirectoryDialog(ParentWindowHandle, LOCTEXT("FolderDialogTitle", "Choose a directory").ToString(), DefaultPath, OutDir)) + { + OnPathPicked.ExecuteIfBound(OutDir); + } + } + + return FReply::Handled(); +} + + +FText SNewFilePathPicker::HandleTextBoxText() const +{ + return FText::FromString(FilePath.Get()); +} + + +void SNewFilePathPicker::HandleTextBoxTextCommitted( const FText& NewText, ETextCommit::Type CommitInfo ) +{ + OnPathPicked.ExecuteIfBound(NewText.ToString()); +} + +#undef LOCTEXT_NAMESPACE + + +``` + +`Source/HoudiniEngineEditor/Private/SNewFilePathPicker.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// This is the same as SFilePathPicker but it uses SaveFileDialog instead of OpenFileDialog +// to allow browsing to a new path + +#pragma once + +#include "CoreMinimal.h" +#include "Misc/Attribute.h" +#include "Fonts/SlateFontInfo.h" +#include "Input/Reply.h" +#include "Styling/SlateWidgetStyleAsset.h" +#include "Styling/ISlateStyle.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SCompoundWidget.h" +#include "Styling/SlateTypes.h" + +class SEditableTextBox; + +/** + * Declares a delegate that is executed when a file was picked in the SFilePathPicker widget. + * + * The first parameter will contain the path to the picked file. + */ +DECLARE_DELEGATE_OneParam(FOnPathPicked, const FString& /*PickedPath*/); + + +/** + * Implements an editable text box with a browse button. + */ +class SNewFilePathPicker + : public SCompoundWidget +{ +public: + + SLATE_BEGIN_ARGS(SNewFilePathPicker) + : _BrowseButtonToolTip(NSLOCTEXT("SNewFilePathPicker", "BrowseButtonToolTip", "Choose a file from this computer")) + , _FileTypeFilter(TEXT("All files (*.*)|*.*")) + , _Font() + , _IsReadOnly(false) + , _IsNewFile(true) + , _IsDirectoryPicker(false) + { } + + /** Browse button image resource. */ + SLATE_ATTRIBUTE(const FSlateBrush*, BrowseButtonImage) + + /** Browse button visual style. */ + SLATE_STYLE_ARGUMENT(FButtonStyle, BrowseButtonStyle) + + /** Browse button tool tip text. */ + SLATE_ATTRIBUTE(FText, BrowseButtonToolTip) + + /** The directory to browse by default */ + SLATE_ATTRIBUTE(FString, BrowseDirectory) + + /** Title for the browse dialog window. */ + SLATE_ATTRIBUTE(FText, BrowseTitle) + + /** The currently selected file path. */ + SLATE_ATTRIBUTE(FString, FilePath) + + /** File type filter string. */ + SLATE_ATTRIBUTE(FString, FileTypeFilter) + + /** Font color and opacity of the path text box. */ + SLATE_ATTRIBUTE(FSlateFontInfo, Font) + + /** Whether the path text box can be modified by the user. */ + SLATE_ATTRIBUTE(bool, IsReadOnly) + + /** Whether to use the new-file dialog instead of open-file */ + SLATE_ATTRIBUTE(bool, IsNewFile) + + /** Whether to use the a directory picker dialog */ + SLATE_ATTRIBUTE(bool, IsDirectoryPicker) + + /** Called when a file path has been picked. */ + SLATE_EVENT(FOnPathPicked, OnPathPicked) + + SLATE_END_ARGS() + + /** + * Constructs a new widget. + * + * @param InArgs The construction arguments. + */ + void Construct( const FArguments& InArgs ); + +private: + + /** Callback for clicking the browse button. */ + FReply HandleBrowseButtonClicked( ); + + /** Callback for getting the text in the path text box. */ + FText HandleTextBoxText( ) const; + + /** Callback for committing the text in the path text box. */ + void HandleTextBoxTextCommitted( const FText& NewText, ETextCommit::Type /*CommitInfo*/ ); + +private: + + /** Holds the directory path to browse by default. */ + TAttribute BrowseDirectory; + + /** Holds the title for the browse dialog window. */ + TAttribute BrowseTitle; + + /** Holds the currently selected file path. */ + TAttribute FilePath; + + /** Holds the file type filter string. */ + TAttribute FileTypeFilter; + + /** Holds the editable text box. */ + TSharedPtr TextBox; + + TAttribute IsNewFile; + + TAttribute IsDirectoryPicker; + +private: + + /** Holds a delegate that is executed when a file was picked. */ + FOnPathPicked OnPathPicked; +}; + + +``` + +`Source/HoudiniEngineEditor/Private/SSelectFolderPathDialog.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "SSelectFolderPathDialog.h" + +#include "ContentBrowserModule.h" +#include "IContentBrowserSingleton.h" +#include "Internationalization/Internationalization.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SUniformGridPanel.h" +#include "Widgets/Text/STextBlock.h" +#include "HoudiniEngineEditorPrivatePCH.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +SSelectFolderPathDialog::SSelectFolderPathDialog() + : UserResponse(EAppReturnType::Cancel) +{ + +} + +void +SSelectFolderPathDialog::Construct(const FArguments& InArgs) +{ + FolderPath = InArgs._InitialPath; + + if (FolderPath.IsEmpty()) + { + FolderPath = FText::FromString(TEXT("/Game")); + } + + FPathPickerConfig PathPickerConfig; + PathPickerConfig.DefaultPath = FolderPath.ToString(); + PathPickerConfig.OnPathSelected = FOnPathSelected::CreateSP(this, &SSelectFolderPathDialog::OnPathChange); + PathPickerConfig.bAddDefaultPath = true; + + FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + + SWindow::Construct(SWindow::FArguments() + .Title(InArgs._TitleText) + .SupportsMinimize(false) + .SupportsMaximize(false) + .IsTopmostWindow(true) + .ClientSize(FVector2D(450, 450)) + [ + SNew(SVerticalBox) + + + SVerticalBox::Slot() + .Padding(2) + [ + SNew(SBorder) + .BorderImage(_GetBrush("ToolPanel.GroupBorder")) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .Text(LOCTEXT("SelectPath", "Select Path")) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 14)) + ] + + + SVerticalBox::Slot() + .FillHeight(1) + .Padding(3) + [ + ContentBrowserModule.Get().CreatePathPicker(PathPickerConfig) + ] + ] + ] + + + SVerticalBox::Slot() + .AutoHeight() + .HAlign(HAlign_Right) + .Padding(5) + [ + SNew(SUniformGridPanel) + .SlotPadding(_GetMargin("StandardDialog.SlotPadding")) + .MinDesiredSlotWidth(_GetFloat("StandardDialog.MinDesiredSlotWidth")) + .MinDesiredSlotHeight(_GetFloat("StandardDialog.MinDesiredSlotHeight")) + + SUniformGridPanel::Slot(0, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .ContentPadding(_GetMargin("StandardDialog.ContentPadding")) + .Text(LOCTEXT("OK", "OK")) + .OnClicked(this, &SSelectFolderPathDialog::OnButtonClick, EAppReturnType::Ok) + ] + + SUniformGridPanel::Slot(1, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .ContentPadding(_GetMargin("StandardDialog.ContentPadding")) + .Text(LOCTEXT("Cancel", "Cancel")) + .OnClicked(this, &SSelectFolderPathDialog::OnButtonClick, EAppReturnType::Cancel) + ] + ] + ]); +} + +EAppReturnType::Type +SSelectFolderPathDialog::ShowModal() +{ + GEditor->EditorAddModalWindow(SharedThis(this)); + return UserResponse; +} + +const FText& +SSelectFolderPathDialog::GetFolderPath() const +{ + return FolderPath; +} + +void +SSelectFolderPathDialog::OnPathChange(const FString& NewPath) +{ + FolderPath = FText::FromString(NewPath); +} + +FReply +SSelectFolderPathDialog::OnButtonClick(EAppReturnType::Type ButtonID) +{ + UserResponse = ButtonID; + RequestDestroyWindow(); + return FReply::Handled(); +} + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngineEditor/Private/SSelectFolderPathDialog.h`: + +```h +/* +* Copyright (c) <2024> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +/* +#include "Misc/Attribute.h" +#include "Fonts/SlateFontInfo.h" +#include "Input/Reply.h" +#include "Styling/SlateWidgetStyleAsset.h" +#include "Styling/ISlateStyle.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SCompoundWidget.h" +#include "Styling/SlateTypes.h"*/ + +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SWindow.h" + +class SSelectFolderPathDialog : public SWindow +{ +public: + + SLATE_BEGIN_ARGS(SSelectFolderPathDialog) {} + SLATE_ARGUMENT(FText, InitialPath) + SLATE_ARGUMENT(FText, TitleText) + SLATE_END_ARGS() + + SSelectFolderPathDialog(); + + void Construct(const FArguments& InArgs); + + EAppReturnType::Type ShowModal(); + + const FText& GetFolderPath() const; + +private: + + void OnPathChange(const FString& NewPath); + + FReply OnButtonClick(EAppReturnType::Type ButtonID); + + EAppReturnType::Type UserResponse; + FText FolderPath; +}; + +``` + +`Source/HoudiniEngineEditor/Private/SSelectHoudiniPathDialog.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "SSelectHoudiniPathDialog.h" + +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniEnginePrivatePCH.h" + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineCommands.h" +#include "HoudiniEngineUtils.h" + +// dlg? +#include "Internationalization/Internationalization.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SUniformGridPanel.h" + +#include "Containers/Map.h" +#include "Containers/UnrealString.h" +#include "Framework/Commands/UIAction.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Framework/Views/ITypedTableView.h" +#include "GameFramework/Actor.h" +#include "HAL/PlatformMath.h" +#include "Internationalization/Internationalization.h" +#include "Internationalization/Text.h" +#include "Layout/Children.h" +#include "Layout/Visibility.h" +#include "Misc/AssertionMacros.h" +#include "Misc/Attribute.h" +#include "SlotBase.h" +#include "Styling/AppStyle.h" +#include "Styling/SlateIconFinder.h" +#include "Textures/SlateIcon.h" +#include "UObject/NameTypes.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/SCompoundWidget.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Views/SExpanderArrow.h" +#include "Widgets/Views/STableRow.h" + +class FUICommandList; +class ITableRow; +class SWidget; +class UClass; +struct FSlateBrush; + +#define LOCTEXT_NAMESPACE "HoudiniNodeTreeview" + + +SSelectHoudiniPathDialog::SSelectHoudiniPathDialog() + : UserResponse(EAppReturnType::Cancel), + bSingleSelectionOnly(false) +{ + +} + +void +SSelectHoudiniPathDialog::Construct(const FArguments& InArgs) +{ + FolderPath = InArgs._InitialPath; + if (FolderPath.IsEmpty()) + { + FolderPath = FText::FromString(TEXT("/Game")); + } + + bSingleSelectionOnly = InArgs._SingleSelection; + + // Split the initial folder path to multiple strings if needed + // This will allow us to re-select previously selected nodes. + FString FolderPathStr = FolderPath.ToString(); + FolderPathStr.ParseIntoArray(SplitFolderPath, TEXT(";"), true); + + // If we're in single selection mode - only keep the first node path! + if (bSingleSelectionOnly && SplitFolderPath.Num() > 1) + { + FolderPath = FText::FromString(SplitFolderPath[0]); + SplitFolderPath.Empty(); + SplitFolderPath.Add(FolderPath.ToString()); + } + + auto IsSessionValid = []() + { + return (HAPI_RESULT_SUCCESS == FHoudiniApi::IsSessionValid(FHoudiniEngine::Get().GetSession())); + }; + + auto IsTreeViewVisible = [IsSessionValid]() + { + if (IsSessionValid()) + return EVisibility::All; + else + return EVisibility::Hidden; + }; + + // Create the Network info and fill all the node hierarchy for it + FillHoudiniNetworkInfo(); + + //Create the treeview + HoudiniNodeTreeView = SNew(SHoudiniNodeTreeView) + .HoudiniNetworkInfo(MakeShared(NetworkInfo)) + .SingleSelection(bSingleSelectionOnly); + + SWindow::Construct(SWindow::FArguments() + .Title(InArgs._TitleText) + .SupportsMinimize(false) + .SupportsMaximize(false) + .IsTopmostWindow(true) + .ClientSize(FVector2D(450, 450)) + [ + SNew(SVerticalBox) + + + SVerticalBox::Slot() + .Padding(2) + [ + SNew(SBorder) + .BorderImage(_GetBrush("ToolPanel.GroupBorder")) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .Text_Lambda([IsSessionValid, this]() + { + if (IsSessionValid()) + { + if(bSingleSelectionOnly) + return LOCTEXT("SelectPath", "Select a Houdini Node..."); + else + return LOCTEXT("SelectPath", "Select Houdini Nodes..."); + } + else + return LOCTEXT("SelectPathInvalid", "\nNo valid Houdini Engine session...\n"); + }) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 12)) + ] + + SVerticalBox::Slot() + .HAlign(HAlign_Left) + .AutoHeight() + [ + SNew(SUniformGridPanel) + .SlotPadding(2.0f) + .Visibility_Lambda(IsTreeViewVisible) + + SUniformGridPanel::Slot(0, 0) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SCheckBox) + .HAlign(HAlign_Center) + .OnCheckStateChanged(HoudiniNodeTreeView.Get(), &SHoudiniNodeTreeView::OnToggleSelectAll) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(0.0f, 3.0f, 6.0f, 3.0f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("SelectHoudiniNodePath_SelectAll", "All")) + ] + ] + + SUniformGridPanel::Slot(1, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .Text(LOCTEXT("SelectHoudiniNodePath_ExpandAll", "Expand All")) + .OnClicked(HoudiniNodeTreeView.Get(), &SHoudiniNodeTreeView::OnExpandAll) + ] + + SUniformGridPanel::Slot(2, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .Text(LOCTEXT("SelectHoudiniNodePath_CollapseAll", "Collapse All")) + .OnClicked(HoudiniNodeTreeView.Get(), &SHoudiniNodeTreeView::OnCollapseAll) + ] + ] + + SVerticalBox::Slot() + .FillHeight(1.0f) + [ + SNew(SBox) + [ + HoudiniNodeTreeView.ToSharedRef() + ] + ] + ] + ] + + + SVerticalBox::Slot() + .AutoHeight() + .HAlign(HAlign_Right) + .Padding(5) + [ + SNew(SUniformGridPanel) + .SlotPadding(_GetMargin("StandardDialog.SlotPadding")) + .MinDesiredSlotWidth(_GetFloat("StandardDialog.MinDesiredSlotWidth")) + .MinDesiredSlotHeight(_GetFloat("StandardDialog.MinDesiredSlotHeight")) + + SUniformGridPanel::Slot(0, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .ContentPadding(_GetMargin("StandardDialog.ContentPadding")) + .Text(LOCTEXT("OK", "OK")) + .OnClicked(this, &SSelectHoudiniPathDialog::OnButtonClick, IsSessionValid() ? EAppReturnType::Ok : EAppReturnType::Cancel) + ] + + SUniformGridPanel::Slot(1, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .ContentPadding(_GetMargin("StandardDialog.ContentPadding")) + .Text(LOCTEXT("Cancel", "Cancel")) + .OnClicked(this, &SSelectHoudiniPathDialog::OnButtonClick, EAppReturnType::Cancel) + ] + ] + ]); +} + +EAppReturnType::Type +SSelectHoudiniPathDialog::ShowModal() +{ + GEditor->EditorAddModalWindow(SharedThis(this)); + return UserResponse; +} + +const FText& +SSelectHoudiniPathDialog::GetFolderPath() const +{ + return FolderPath; +} + + +void +SSelectHoudiniPathDialog::FillHoudiniNodeInfo(FHoudiniNodeInfoPtr InNodeInfo) +{ + if (!InNodeInfo.IsValid()) + return; + + if (InNodeInfo->NodeId < 0) + return; + + HAPI_NodeInfo NodeInfo; + FHoudiniApi::NodeInfo_Init(&NodeInfo); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), InNodeInfo->NodeId, &NodeInfo)) + { + // Invalid node ? + return; + } + + // Node Name + InNodeInfo->NodeName = FHoudiniEngineUtils::HapiGetString(NodeInfo.nameSH); + + // Node Path + FHoudiniEngineUtils::HapiGetAbsNodePath(InNodeInfo->NodeId, InNodeInfo->NodeHierarchyPath); + + // Do we need to initialise import? + // This is not necessary if import was set to true by our parent + if (!InNodeInfo->bImportNode) + { + // See if our path is in the current split folder path + if (!InNodeInfo->NodeHierarchyPath.IsEmpty() && SplitFolderPath.Contains(InNodeInfo->NodeHierarchyPath)) + InNodeInfo->bImportNode = true; + } + + bool bLookForChildrens = false; + switch (NodeInfo.type) + { + case HAPI_NODETYPE_ANY: + case HAPI_NODETYPE_NONE: + InNodeInfo->NodeType = TEXT("INVALID"); + return; + break; + + case HAPI_NODETYPE_OBJ: + InNodeInfo->NodeType = TEXT("OBJ"); + bLookForChildrens = true; + break; + + case HAPI_NODETYPE_SOP: + InNodeInfo->NodeType = TEXT("SOP"); + bLookForChildrens = true; + break; + + case HAPI_NODETYPE_CHOP: + InNodeInfo->NodeType = TEXT("CHOP"); + break; + + case HAPI_NODETYPE_ROP: + InNodeInfo->NodeType = TEXT("ROP"); + break; + + case HAPI_NODETYPE_SHOP: + InNodeInfo->NodeType = TEXT("SHOP"); + break; + + case HAPI_NODETYPE_COP2: + InNodeInfo->NodeType = TEXT("COP2"); + bLookForChildrens = true; + break; + + case HAPI_NODETYPE_VOP: + InNodeInfo->NodeType = TEXT("VOP"); + break; + + case HAPI_NODETYPE_DOP: + InNodeInfo->NodeType = TEXT("DOP"); + break; + + case HAPI_NODETYPE_TOP: + InNodeInfo->NodeType = TEXT("TOP"); + bLookForChildrens = true; + break; + + case HAPI_NODETYPE_COP: + InNodeInfo->NodeType = TEXT("COP"); + bLookForChildrens = true; + break; + } + + // Should we look at this node's children ? + InNodeInfo->Childrens.SetNum(0); + if (!bLookForChildrens) + return; + + // See if this node has children ? + int32 ChildrenCount = 0; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), InNodeInfo->NodeId, HAPI_NODETYPE_ANY, HAPI_NODEFLAGS_NON_BYPASS, false, &ChildrenCount)) + { + // ? + return; + } + + if (ChildrenCount <= 0) + return; + + TArray ChildrenNodeIds; + ChildrenNodeIds.SetNumUninitialized(ChildrenCount); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), InNodeInfo->NodeId, ChildrenNodeIds.GetData(), ChildrenCount)) + { + return; + } + + // Initialize the node hierarchy + InNodeInfo->Childrens.SetNum(ChildrenCount); + + for (int32 Idx = 0; Idx < ChildrenCount; Idx++) + { + InNodeInfo->Childrens[Idx] = MakeShared(); + InNodeInfo->Childrens[Idx]->NodeId = ChildrenNodeIds[Idx]; + InNodeInfo->Childrens[Idx]->bIsRootNode = false; + InNodeInfo->Childrens[Idx]->Parent = InNodeInfo; + + // Initialize our children's import value to us + // They should be marked as imported if we are + InNodeInfo->Childrens[Idx]->bImportNode = InNodeInfo->bImportNode; + + FillHoudiniNodeInfo(InNodeInfo->Childrens[Idx]); + } +} + +void +SSelectHoudiniPathDialog::FillHoudiniNetworkInfo() +{ + // Start everything from /obj + FString RootNodePath = "/obj"; + HAPI_NodeId RootNodeId = -1; + HAPI_Result result = FHoudiniApi::GetNodeFromPath( + FHoudiniEngine::Get().GetSession(), -1, TCHAR_TO_ANSI(*RootNodePath), &RootNodeId); + + HAPI_NodeInfo RootNodeInfo; + FHoudiniApi::NodeInfo_Init(&RootNodeInfo); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetNodeInfo( + FHoudiniEngine::Get().GetSession(), RootNodeId, &RootNodeInfo)) + { + // No root ? + return; + } + + // See if this OBJ has children ? + int32 Count = 0; + if (HAPI_RESULT_SUCCESS != FHoudiniApi::ComposeChildNodeList( + FHoudiniEngine::Get().GetSession(), RootNodeId, HAPI_NODETYPE_ANY, HAPI_NODEFLAGS_NON_BYPASS, false, &Count)) + { + // ? + return; + } + + if (Count <= 0) + return; + + TArray ChildrenNodeIds; + ChildrenNodeIds.SetNumUninitialized(Count); + if (HAPI_RESULT_SUCCESS != FHoudiniApi::GetComposedChildNodeList( + FHoudiniEngine::Get().GetSession(), RootNodeId, ChildrenNodeIds.GetData(), Count)) + { + return; + } + + // Initialize the node hierarchy + NetworkInfo.RootNodesInfos.SetNum(Count); + + for (int32 Idx = 0; Idx < Count; Idx++) + { + NetworkInfo.RootNodesInfos[Idx] = MakeShared(); + NetworkInfo.RootNodesInfos[Idx]->NodeId = ChildrenNodeIds[Idx]; + NetworkInfo.RootNodesInfos[Idx]->bIsRootNode = true; + NetworkInfo.RootNodesInfos[Idx]->bImportNode = false; + NetworkInfo.RootNodesInfos[Idx]->Parent = NULL; + FillHoudiniNodeInfo(NetworkInfo.RootNodesInfos[Idx]); + } +} + +/* +void +SSelectHoudiniPathDialog::OnPathChange(const FString& NewPath) +{ + FolderPath = FText::FromString(NewPath); +} +*/ + + +void +SSelectHoudiniPathDialog::UpdateNodePathFromTreeView(FHoudiniNodeInfoPtr& InNodeInfo, FString& OutPath) +{ + if (!InNodeInfo.IsValid()) + return; + + if (InNodeInfo->bImportNode) + { + if (!OutPath.IsEmpty()) + OutPath.Append(";"); + + // Import this node + OutPath.Append(InNodeInfo->NodeHierarchyPath); + } + else + { + // Look for node to import in this node's children + for (auto& CurrentNodeInfoPtr : InNodeInfo->Childrens) + { + UpdateNodePathFromTreeView(CurrentNodeInfoPtr, OutPath); + } + } +} + + + +FReply +SSelectHoudiniPathDialog::OnButtonClick(EAppReturnType::Type ButtonID) +{ + UserResponse = ButtonID; + + // Update the folder path on OK + FString NewPath = FString(); + if (ButtonID == EAppReturnType::Ok) + { + for (auto& CurrentNodeInfoPtr : NetworkInfo.RootNodesInfos) + { + UpdateNodePathFromTreeView(CurrentNodeInfoPtr, NewPath); + } + + FolderPath = FText::FromString(NewPath); + } + + RequestDestroyWindow(); + return FReply::Handled(); +} + +#undef LOCTEXT_NAMESPACE + + +``` + +`Source/HoudiniEngineEditor/Private/SSelectHoudiniPathDialog.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +// #include "Containers/Array.h" +// #include "Containers/BitArray.h" +// #include "Containers/Set.h" +// #include "Containers/SparseArray.h" +// #include "Delegates/Delegate.h" +// #include "HAL/PlatformCrt.h" +// #include "Input/Reply.h" +// #include "Misc/Optional.h" +// #include "Styling/SlateTypes.h" +#include "Templates/SharedPointer.h" +// #include "Templates/TypeHash.h" +// #include "Types/SlateEnums.h" +// #include "Widgets/DeclarativeSyntaxSupport.h" +// #include "Widgets/Views/STableViewBase.h" +// #include "Widgets/Views/STreeView.h" + +#include "SHoudiniNodeTreeView.h" + +// Dlg +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SWindow.h" + +class FHoudiniNodeInfo; +class FHoudiniNetworkInfo; +class ITableRow; +class SWidget; + +typedef TSharedPtr FHoudiniNodeInfoPtr; + + +class SSelectHoudiniPathDialog : public SWindow +{ +public: + + SLATE_BEGIN_ARGS(SSelectHoudiniPathDialog) {} + SLATE_ARGUMENT(FText, InitialPath) + SLATE_ARGUMENT(FText, TitleText) + SLATE_ARGUMENT(bool, SingleSelection) + SLATE_END_ARGS() + + SSelectHoudiniPathDialog(); + + void Construct(const FArguments& InArgs); + + EAppReturnType::Type ShowModal(); + + const FText& GetFolderPath() const; + + void UpdateNodePathFromTreeView(FHoudiniNodeInfoPtr& InNodeInfo, FString& OutPath); + + void FillHoudiniNetworkInfo(); + + void FillHoudiniNodeInfo(FHoudiniNodeInfoPtr InNodeInfo); + +private: + + void OnPathChange(const FString& NewPath); + FReply OnButtonClick(EAppReturnType::Type ButtonID); + + TSharedPtr HoudiniNodeTreeView; + + FHoudiniNetworkInfo NetworkInfo; + + EAppReturnType::Type UserResponse; + FText FolderPath; + TArray SplitFolderPath; + bool bSingleSelectionOnly; +}; + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorColliderTests.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorColliderTests.h" + +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" + +FString FHoudiniEditorColliderTests::EquivalenceTestMapName = TEXT("Colliders"); +FString FHoudiniEditorColliderTests::TestHDAPath = TEXT("/Game/TestHDAs/Colliders/"); + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorColliderTest_Colliders_Common, "Houdini.Editor.Colliders.Colliders_Common", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorColliderTest_Colliders_Common::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorColliderTests::EquivalenceTestMapName; + const FString ActorName = TEXT("Colliders_Common"); + const FString HDAAssetPath = FHoudiniEditorColliderTests::TestHDAPath + TEXT("Colliders_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); + + return true; +} + + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorColliderTests.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +// Class just for containing static member variables +class FHoudiniEditorColliderTests +{ +public: + static FString EquivalenceTestMapName; + static FString TestHDAPath; +}; + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorEquivalenceUtils.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorEquivalenceUtils.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniParameter.h" +#include "HoudiniOutput.h" +#include "HoudiniHandleComponent.h" +#include "HoudiniRuntimeSettings.h" +#include "HoudiniPDGAssetLink.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniInstancedActorComponent.h" +#include "HoudiniParameterButton.h" +#include "HoudiniParameterButtonStrip.h" +#include "HoudiniParameterChoice.h" +#include "HoudiniParameterColor.h" +#include "HoudiniParameterFile.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterFolder.h" +#include "HoudiniParameterFolderList.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterLabel.h" +#include "HoudiniParameterMultiParm.h" +#include "HoudiniParameterOperatorPath.h" +#include "HoudiniParameterRamp.h" +#include "HoudiniParameterSeparator.h" +#include "HoudiniParameterString.h" +#include "HoudiniParameterToggle.h" +#include "HoudiniSplineComponent.h" +#include "HoudiniStaticMesh.h" +#include "HoudiniStaticMeshComponent.h" + +#include "Containers/Set.h" +#include "Misc/AutomationTest.h" +#include "PhysicalMaterials/PhysicalMaterial.h" +#include "Materials/MaterialInterface.h" +#include "Engine/StaticMesh.h" +#include "PhysicsEngine/BodyInstance.h" + +// #define WITH_DEV_AUTOMATION_TESTS 0 + +#define FLOAT_TOLERANCE 0.001F // Mac tests need higher float tolerance + +#if WITH_DEV_AUTOMATION_TESTS + +FAutomationTestBase* FHoudiniEditorEquivalenceUtils::TestBase = nullptr; +#endif + +bool +FHoudiniEditorEquivalenceUtils::TestExpressionErrorEnabled = true; + +bool +FHoudiniEditorEquivalenceUtils::IsEquivalent(UHoudiniAssetComponent * A, UHoudiniAssetComponent * B) +{ + const FString Header = "UHoudiniAssetComponent"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(IsEquivalent(A->GetHoudiniAsset(), B->GetHoudiniAsset()), Header, "HoudiniAsset"); + Result &= TestExpressionError(A->GetCookOnParameterChange() == B->GetCookOnParameterChange(), Header, "bCookOnParameterChange"); + Result &= TestExpressionError(A->GetUploadTransformsToHoudiniEngine() == B->GetUploadTransformsToHoudiniEngine(), Header, "bUploadTransformsToHoudiniEngine"); + Result &= TestExpressionError(A->GetCookOnTransformChange() == B->GetCookOnTransformChange(), Header, "bCookOnTransformChange"); + Result &= TestExpressionError(A->GetCookOnAssetInputCook() == B->GetCookOnAssetInputCook(), Header, "bCookOnAssetInputCook"); + Result &= TestExpressionError(A->IsOutputless() == B->IsOutputless(), Header, "bOutputless"); + Result &= TestExpressionError(A->GetOutputTemplateGeos() == B->GetOutputTemplateGeos(), Header, "bOutputTemplateGeos"); + Result &= TestExpressionError(A->GetCookOnParameterChange() == B->GetCookOnParameterChange(), Header, "bCookOnParameterChange"); + // We change the temporary cook folder to organize the tests. Do not test this. + // Result &= TestExpressionError(IsEquivalent(A->GetTemporaryCookFolder(), B->GetTemporaryCookFolder()), Header, "TemporaryCookFolder"); + // Result &= TestExpressionError(IsEquivalent(A->GetBakeFolder(), B->GetBakeFolder()), Header, "BakeFolder"); + Result &= TestExpressionError(IsEquivalent(A->GetStaticMeshGenerationProperties(), B->GetStaticMeshGenerationProperties()), Header, "StaticMeshGenerationProperties"); + Result &= TestExpressionError(IsEquivalent(A->GetStaticMeshBuildSettings(), B->GetStaticMeshBuildSettings()), Header, "StaticMeshBuildSettings"); + Result &= TestExpressionError(A->IsOverrideGlobalProxyStaticMeshSettings() == B->IsOverrideGlobalProxyStaticMeshSettings(), Header, "bOverrideGlobalProxyStaticMeshSettings"); + Result &= TestExpressionError(A->IsProxyStaticMeshEnabled() == B->IsProxyStaticMeshEnabled(), Header, "bEnableProxyStaticMeshOverride"); + Result &= TestExpressionError(A->IsProxyStaticMeshRefinementByTimerEnabled() == B->IsProxyStaticMeshRefinementByTimerEnabled(), Header, "bEnableProxyStaticMeshRefinementByTimerOverride"); + Result &= TestExpressionError(A->GetProxyMeshAutoRefineTimeoutSeconds() == B->GetProxyMeshAutoRefineTimeoutSeconds(), Header, "ProxyMeshAutoRefineTimeoutSecondsOverride"); + Result &= TestExpressionError(A->IsProxyStaticMeshRefinementOnPreSaveWorldEnabled() == B->IsProxyStaticMeshRefinementOnPreSaveWorldEnabled(), Header, "bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride"); + Result &= TestExpressionError(A->IsProxyStaticMeshRefinementOnPreBeginPIEEnabled() == B->IsProxyStaticMeshRefinementOnPreBeginPIEEnabled(), Header, "bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride"); + + // Skip UI data + // #if WITH_EDITORONLY_DATA + // Result &= TestExpressionError(A->bGenerateMenuExpanded == B->bGenerateMenuExpanded, Header, "bGenerateMenuExpanded"); + // Result &= TestExpressionError(A->bBakeMenuExpanded == B->bBakeMenuExpanded, Header, "bBakeMenuExpanded"); + // Result &= TestExpressionError(A->bAssetOptionMenuExpanded == B->bAssetOptionMenuExpanded, Header, "bAssetOptionMenuExpanded"); + // Result &= TestExpressionError(A->bHelpAndDebugMenuExpanded == B->bHelpAndDebugMenuExpanded, Header, "bHelpAndDebugMenuExpanded"); + // Result &= TestExpressionError(A->HoudiniEngineBakeOption == B->HoudiniEngineBakeOption, Header, "HoudiniEngineBakeOption"); + // Result &= TestExpressionError(A->bRemoveOutputAfterBake == B->bRemoveOutputAfterBake, Header, "bRemoveOutputAfterBake"); + // Result &= TestExpressionError(A->bRecenterBakedActors == B->bRecenterBakedActors, Header, "bRecenterBakedActor"); + // Result &= TestExpressionError(A->bReplacePreviousBake == B->bReplacePreviousBake, Header, "bReplacePreviousBake"); + // #endif + + // Skip AssetId + + // Note: Despite DownstreamHoudiniAssets being transient, want to test + // TODO: COOKABLE fix me! + Result &= TestExpressionError(A->DownstreamHoudiniAssets.Num() == B->DownstreamHoudiniAssets.Num(), Header, "DownstreamHoudiniAssets.Num"); + for (int i = 0; i < FMath::Min(A->DownstreamHoudiniAssets.Num(), B->DownstreamHoudiniAssets.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->DownstreamHoudiniAssets.Array()[i], B->DownstreamHoudiniAssets.Array()[i]), Header, "DownstreamHoudiniAssets"); + } + + // Skip ComponentGUID + // Skip HapiGUID + + // SKip DebugLastAssetState + + // Skip HapiAssetName + // Skip AssetState + // Skip DebugLastAssetState + // Skip AssetStateResult + + // Skip SubAssetIndex + // Skip AssetCookCount + // Skip DuplicateTransient cook flags: bHasBeenLoaded, HasBeenDuplicated, bPendingDelete + + Result &= TestExpressionError(A->GetNumParameters() == B->GetNumParameters(), Header, "Parameters.Num"); + for (int i = 0; i < FMath::Min(A->GetNumParameters(), B->GetNumParameters()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->GetParameterAt(i), B->GetParameterAt(i)), Header, "Parameters"); + } + + Result &= TestExpressionError(A->GetNumInputs() == B->GetNumInputs(), Header, "Inputs.Num"); + for (int i = 0; i < FMath::Min(A->GetNumInputs(), B->GetNumInputs()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->GetInputAt(i), B->GetInputAt(i)), Header, "Inputs"); + } + + Result &= TestExpressionError(A->GetNumOutputs() == B->GetNumOutputs(), Header, "Outputs.Num"); + for (int i = 0; i < FMath::Min(A->GetNumOutputs(), B->GetNumOutputs()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->GetOutputAt(i), B->GetOutputAt(i)), Header, "Outputs"); + } + + Result &= TestExpressionError(A->GetBakedOutputs().Num() == B->GetBakedOutputs().Num(), Header, "BakedOutputs.Num"); + for (int i = 0; i < FMath::Min(A->GetBakedOutputs().Num(), B->GetBakedOutputs().Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->GetBakedOutputs()[i], B->GetBakedOutputs()[i]), Header, "BakedOutputs"); + } + + // Result &= TestExpressionError(A->UntrackedOutputs.Num() == B->UntrackedOutputs.Num(), Header, "UntrackedOutputs.Num"); + // for (int i = 0; i < FMath::Min(A->UntrackedOutputs.Num(), B->UntrackedOutputs.Num()); i++) + // { + // Result &= TestExpressionError(IsEquivalent(A->UntrackedOutputs[i].Get(), B->UntrackedOutputs[i].Get()), Header, "UntrackedOutputs"); + // } + + Result &= TestExpressionError(A->GetNumHandles() == B->GetNumHandles(), Header, "HandleComponents.Num"); + for (int i = 0; i < FMath::Min(A->GetNumHandles(), A->GetNumHandles()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->GetHandleComponentAt(i), B->GetHandleComponentAt(i)), Header, "HandleComponents"); + } + + // Skip bHasComponentTransformChanged, bFullyLoaded + Result &= TestExpressionError(IsEquivalent(A->GetPDGAssetLink(), B->GetPDGAssetLink()), Header, "PDGAssetLink"); + + // Not sure if these are necessary + // Skip refine meshes timer/delegate + + // Skip bNoProxyMeshNextCookRequested + + // Skip bBakeAfterNextCook + + // Skip delegates + // Skip bCachedIsPreview + // Skip LastTickTime + // Skip Version1CompatibilityHAC (Just for simplicity) + // Skip LastTIckTime + // Skip remaining delegates + + return Result; +} + + +bool +FHoudiniEditorEquivalenceUtils::IsEquivalent(UHoudiniCookable* A, UHoudiniCookable* B) +{ + const FString Header = "UHoudiniCookable"; + + bool Result = true; + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + // ASSET + Result &= TestExpressionError(A->IsHoudiniAssetSupported() == B->IsHoudiniAssetSupported(), Header, "IsHoudiniAssetSupported"); + Result &= TestExpressionError(IsEquivalent(A->GetHoudiniAsset(), B->GetHoudiniAsset()), Header, "HoudiniAsset"); + // Subasset index?? + + // PARAMS + Result &= TestExpressionError(A->IsParameterSupported() == B->IsParameterSupported(), Header, "IsParameterSupported"); + Result &= TestExpressionError(A->GetNumParameters() == B->GetNumParameters(), Header, "Parameters.Num"); + for (int i = 0; i < FMath::Min(A->GetNumParameters(), B->GetNumParameters()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->GetParameterAt(i), B->GetParameterAt(i)), Header, "Parameters"); + } + Result &= TestExpressionError(A->GetCookOnParameterChange() == B->GetCookOnParameterChange(), Header, "CookOnParameterChange"); + + + // INPUTS + Result &= TestExpressionError(A->IsInputSupported() == B->IsInputSupported(), Header, "IsInputSupported"); + Result &= TestExpressionError(A->GetNumInputs() == B->GetNumInputs(), Header, "Inputs.Num"); + for (int i = 0; i < FMath::Min(A->GetNumInputs(), B->GetNumInputs()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->GetInputAt(i), B->GetInputAt(i)), Header, "Inputs"); + } + Result &= TestExpressionError(A->GetCookOnInputChange() == B->GetCookOnInputChange(), Header, "bCookOnInputChange"); + Result &= TestExpressionError(A->GetCookOnCookableInputCook() == B->GetCookOnCookableInputCook(), Header, "bCookOnCookableInputCook"); + // Note: Despite DownstreamCookables being transient, want to test + Result &= TestExpressionError(A->GetInputData()->DownstreamCookables.Num() == B->GetInputData()->DownstreamCookables.Num(), Header, "DownstreamCookables.Num"); + for (int i = 0; i < FMath::Min(A->GetInputData()->DownstreamCookables.Num(), B->GetInputData()->DownstreamCookables.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->GetInputData()->DownstreamCookables.Array()[i], B->GetInputData()->DownstreamCookables.Array()[i]), Header, "DownstreamCookables"); + } + + // OUTPUTS + Result &= TestExpressionError(A->IsOutputSupported() == B->IsOutputSupported(), Header, "IsOutputSupported"); + Result &= TestExpressionError(A->GetNumOutputs() == B->GetNumOutputs(), Header, "Outputs.Num"); + for (int i = 0; i < FMath::Min(A->GetNumOutputs(), B->GetNumOutputs()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->GetOutputAt(i), B->GetOutputAt(i)), Header, "Outputs"); + } + // We change the temporary cook folder to organize the tests. Do not test this. + // Result &= TestExpressionError(IsEquivalent(A->GetTemporaryCookFolder(), B->GetTemporaryCookFolder()), Header, "TemporaryCookFolder"); + // Result &= TestExpressionError(IsEquivalent(A->GetBakeFolder(), B->GetBakeFolder()), Header, "BakeFolder"); + // TODO : Untracked outputs? + // Result &= TestExpressionError(A->UntrackedOutputs.Num() == B->UntrackedOutputs.Num(), Header, "UntrackedOutputs.Num"); + // for (int i = 0; i < FMath::Min(A->UntrackedOutputs.Num(), B->UntrackedOutputs.Num()); i++) + // { + // Result &= TestExpressionError(IsEquivalent(A->UntrackedOutputs[i].Get(), B->UntrackedOutputs[i].Get()), Header, "UntrackedOutputs"); + // } + Result &= TestExpressionError(A->IsOutputless() == B->IsOutputless(), Header, "bOutputless"); + Result &= TestExpressionError(A->GetOutputTemplateGeos() == B->GetOutputTemplateGeos(), Header, "bOutputTemplateGeos"); + Result &= TestExpressionError(A->GetUseOutputNodes() == B->GetUseOutputNodes(), Header, "UseOutputNodes"); + // TODO : split mesh support? + Result &= TestExpressionError(A->GetEnableCurveEditing() == B->GetEnableCurveEditing(), Header, "EnableCurveEditing"); + Result &= TestExpressionError(IsEquivalent(A->GetStaticMeshGenerationProperties(), B->GetStaticMeshGenerationProperties()), Header, "StaticMeshGenerationProperties"); + Result &= TestExpressionError(IsEquivalent(A->GetStaticMeshBuildSettings(), B->GetStaticMeshBuildSettings()), Header, "StaticMeshBuildSettings"); + // BAKE + Result &= TestExpressionError(A->GetBakedOutputs().Num() == B->GetBakedOutputs().Num(), Header, "BakedOutputs.Num"); + for (int i = 0; i < FMath::Min(A->GetBakedOutputs().Num(), B->GetBakedOutputs().Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->GetBakedOutputs()[i], B->GetBakedOutputs()[i]), Header, "BakedOutputs"); + } + Result &= TestExpressionError(A->GetHoudiniEngineBakeOption() == B->GetHoudiniEngineBakeOption(), Header, "HoudiniEngineBakeOption"); + Result &= TestExpressionError(A->GetRemoveOutputAfterBake() == B->GetRemoveOutputAfterBake(), Header, "RemoveOutputAfterBake"); + Result &= TestExpressionError(A->GetRecenterBakedActors() == B->GetRecenterBakedActors(), Header, "RecenterBakedActors"); + Result &= TestExpressionError(A->GetReplacePreviousBake() == B->GetReplacePreviousBake(), Header, "ReplacePreviousBake"); + Result &= TestExpressionError(A->GetActorBakeOption() == B->GetActorBakeOption(), Header, "ActorBakeOption"); + Result &= TestExpressionError(A->GetLandscapeUseTempLayers() == B->GetLandscapeUseTempLayers(), Header, "LandscapeUseTempLayers"); + // PROXY + Result &= TestExpressionError(A->IsOverrideGlobalProxyStaticMeshSettings() == B->IsOverrideGlobalProxyStaticMeshSettings(), Header, "bOverrideGlobalProxyStaticMeshSettings"); + Result &= TestExpressionError(A->IsProxyStaticMeshEnabled() == B->IsProxyStaticMeshEnabled(), Header, "bEnableProxyStaticMeshOverride"); + Result &= TestExpressionError(A->IsProxyStaticMeshRefinementByTimerEnabled() == B->IsProxyStaticMeshRefinementByTimerEnabled(), Header, "bEnableProxyStaticMeshRefinementByTimerOverride"); + Result &= TestExpressionError(A->GetProxyMeshAutoRefineTimeoutSeconds() == B->GetProxyMeshAutoRefineTimeoutSeconds(), Header, "ProxyMeshAutoRefineTimeoutSecondsOverride"); + Result &= TestExpressionError(A->IsProxyStaticMeshRefinementOnPreSaveWorldEnabled() == B->IsProxyStaticMeshRefinementOnPreSaveWorldEnabled(), Header, "bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride"); + Result &= TestExpressionError(A->IsProxyStaticMeshRefinementOnPreBeginPIEEnabled() == B->IsProxyStaticMeshRefinementOnPreBeginPIEEnabled(), Header, "bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride"); + Result &= TestExpressionError(A->IsPlayInEditorRefinementAllowed() == B->IsPlayInEditorRefinementAllowed(), Header, "IsPlayInEditorRefinementAllowed"); + + // COMPONENT + Result &= TestExpressionError(A->IsComponentSupported() == B->IsComponentSupported(), Header, "IsComponentSupported"); + Result &= TestExpressionError(IsEquivalent(A->GetComponent()->GetClass(), B->GetComponent()->GetClass()), Header, "GetComponent-Class"); + Result &= TestExpressionError(IsEquivalent(A->GetLastComponentTransform(), B->GetLastComponentTransform()), Header, "GetLastComponentTransform"); + Result &= TestExpressionError(A->GetUploadTransformsToHoudiniEngine() == B->GetUploadTransformsToHoudiniEngine(), Header, "UploadTransformsToHoudiniEngine"); + Result &= TestExpressionError(A->GetCookOnTransformChange() == B->GetCookOnTransformChange(), Header, "CookOnTransformChange"); + Result &= TestExpressionError(A->GetNumHandles() == B->GetNumHandles(), Header, "HandleComponents.Num"); + for (int i = 0; i < FMath::Min(A->GetNumHandles(), A->GetNumHandles()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->GetHandleComponentAt(i), B->GetHandleComponentAt(i)), Header, "HandleComponents"); + } + + // PDG + Result &= TestExpressionError(A->IsPDGSupported() == B->IsPDGSupported(), Header, "IsPDGSupported"); + Result &= TestExpressionError(IsEquivalent(A->GetPDGAssetLink(), B->GetPDGAssetLink()), Header, "PDGAssetLink"); + + // Skip UI data + // #if WITH_EDITORONLY_DATA + // Result &= TestExpressionError(A->bGenerateMenuExpanded == B->bGenerateMenuExpanded, Header, "bGenerateMenuExpanded"); + // Result &= TestExpressionError(A->bBakeMenuExpanded == B->bBakeMenuExpanded, Header, "bBakeMenuExpanded"); + // Result &= TestExpressionError(A->bAssetOptionMenuExpanded == B->bAssetOptionMenuExpanded, Header, "bAssetOptionMenuExpanded"); + // Result &= TestExpressionError(A->bHelpAndDebugMenuExpanded == B->bHelpAndDebugMenuExpanded, Header, "bHelpAndDebugMenuExpanded"); + // #endif + // + // Skip bHasComponentTransformChanged, bFullyLoaded + // Skip NodeId + // Skip ComponentGUID + // Skip HapiGUID + // SKip DebugLastAssetState + // Skip HapiAssetName + // Skip AssetState + // Skip DebugLastAssetState + // Skip AssetStateResult + // Skip SubAssetIndex + // Skip AssetCookCount + // Skip DuplicateTransient cook flags: bHasBeenLoaded, HasBeenDuplicated, bPendingDelete + + // Not sure if these are necessary + // Skip refine meshes timer/delegate + // Skip bNoProxyMeshNextCookRequested + // Skip bBakeAfterNextCook + // Skip delegates + // Skip bCachedIsPreview + // Skip LastTickTime + // Skip Version1CompatibilityHAC (Just for simplicity) + // Skip remaining delegates + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniAsset* A, const UHoudiniAsset* B) +{ + const FString Header = "UHoudiniAsset"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->AssetFileName.Equals(B->AssetFileName), Header, "AssetFileName"); + // Skip AssetImportData + // Skip AssetBytes/AssetBytesCount? + + Result &= TestExpressionError(A->bAssetLimitedCommercial == B->bAssetLimitedCommercial, Header, "bAssetLimitedCommercial"); + Result &= TestExpressionError(A->bAssetNonCommercial == B->bAssetNonCommercial, Header, "bAssetLimitedCommercial"); + Result &= TestExpressionError(A->bAssetExpanded == B->bAssetExpanded, Header, "bAssetExpanded"); + + return Result; +} + + + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameter* A, const UHoudiniParameter* B) +{ + const FString Header = "UHoudiniParameter"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->Name.Equals(B->Name), Header, "Name"); + Result &= TestExpressionError(A->Label.Equals(B->Label), Header, "Label"); + Result &= TestExpressionError(A->ParmType == B->ParmType, Header, "ParmType"); + Result &= TestExpressionError(A->TupleSize == B->TupleSize, Header, "TupleSize"); + + // Skip NodeId + // Skip ParmId + // Skip ParentParmId + + Result &= TestExpressionError(A->ChildIndex == B->ChildIndex, Header, "ChildIndex"); + Result &= TestExpressionError(A->bIsVisible == B->bIsVisible, Header, "bVisible"); + Result &= TestExpressionError(A->bIsDisabled == B->bIsDisabled, Header, "bIsDisabled"); + // We set bHasChanged on scene change, so don't compare this + // Result &= TestExpressionError(A->bHasChanged == B->bHasChanged, Header, "bHasChanged"); + // Result &= TestExpressionError(A->bNeedsToTriggerUpdate == B->bNeedsToTriggerUpdate, Header, "bNeedsToTriggerUpdate"); + Result &= TestExpressionError(A->bIsDefault == B->bIsDefault, Header, "bIsDefault"); + Result &= TestExpressionError(A->bIsSpare == B->bIsSpare, Header, "bIsSpare"); + Result &= TestExpressionError(A->bJoinNext == B->bJoinNext, Header, "bJoinNext"); + Result &= TestExpressionError(A->bIsChildOfMultiParm == B->bIsChildOfMultiParm, Header, "bIsChildOfMultiParm"); + Result &= TestExpressionError(A->bIsDirectChildOfMultiParm == B->bIsDirectChildOfMultiParm, Header, "bIsDirectChildOfMultiParm"); + + // Skip bPendingRevertToDefault + // Skip TuplePendingRevertToDefault + + Result &= TestExpressionError(A->Help.Equals(B->Help), Header, "Help"); + Result &= TestExpressionError(A->TagCount == B->TagCount, Header, "TagCount"); + //Result &= TestExpressionError(A->ValueIndex == B->ValueIndex, Header, "ValueIndex"); + Result &= TestExpressionError(A->bHasExpression == B->bHasExpression, Header, "bHasExpression"); + Result &= TestExpressionError(A->ParamExpression.Equals(B->ParamExpression), Header, "ParamExpression"); + + Result &= TestExpressionError(A->Tags.Num() == B->Tags.Num(), Header, "Tags.Num"); + for (auto PairA : A->Tags) + { + SetTestExpressionError(false); + + bool HasEquivalent = false; + for (auto PairB : B->Tags) + { + if (PairA.Value.Equals(PairB.Value)) + { + HasEquivalent = true; + break; + } + } + SetTestExpressionError(true); + Result &= TestExpressionError(HasEquivalent, Header, "Tags"); + } + + Result &= TestExpressionError(A->bAutoUpdate == B->bAutoUpdate, Header, "bAutoUpdate"); + + if (A->IsA(UHoudiniParameterButton::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterButtonStrip::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterChoice::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterColor::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterFile::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterFloat::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterFolder::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterFolderList::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterInt::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterLabel::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterMultiParm::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterOperatorPath::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + /* + else if (A->IsA(UHoudiniParameterRampFloatPoint::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterRampColorPoint::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + */ + else if (A->IsA(UHoudiniParameterRampFloat::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterRampColor::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterSeparator::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterString::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterToggle::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniInput* A, const UHoudiniInput* B) +{ + const FString Header = "UHoudiniInput"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->Name.Equals(B->Name), Header, "Name"); + Result &= TestExpressionError(A->Label.Equals(B->Label), Header, "Label"); + Result &= TestExpressionError(A->Type == B->Type, Header, "Type"); + + // Skip PreviousType + // Skip AssetNodeId + // Skip InputNodeId + + Result &= TestExpressionError(A->InputIndex == B->InputIndex, Header, "InputIndex"); + Result &= TestExpressionError(A->bIsObjectPathParameter == B->bIsObjectPathParameter, Header, "bIsObjectPathParameter"); + // Skip bDataUploadNeeded + //Result &= TestExpressionError(A->bDataUploadNeeded == B->bDataUploadNeeded, Header, "bDataUploadNeeded"); + + const FHoudiniInputObjectSettings& InputSettingsA = A->GetInputSettings(); + const FHoudiniInputObjectSettings& InputSettingsB = B->GetInputSettings(); + + Result &= TestExpressionError(A->Help.Equals(B->Help), Header, "Help"); + Result &= TestExpressionError(InputSettingsA.KeepWorldTransform == InputSettingsB.KeepWorldTransform, Header, "KeepWorldTransform"); + Result &= TestExpressionError(A->bPackBeforeMerge == B->bPackBeforeMerge, Header, "bPackBeforeMerge"); + Result &= TestExpressionError(InputSettingsA.bImportAsReference == InputSettingsB.bImportAsReference, Header, "bImportAsReference"); + Result &= TestExpressionError(InputSettingsA.bImportAsReferenceRotScaleEnabled == InputSettingsB.bImportAsReferenceRotScaleEnabled, Header, "bImportAsReferenceRotScaleEnabled"); + Result &= TestExpressionError(InputSettingsA.bExportMainGeometry == InputSettingsB.bExportMainGeometry, Header, "bExportMainGeometry"); + Result &= TestExpressionError(InputSettingsA.bExportLODs == InputSettingsB.bExportLODs, Header, "bExportLODs"); + Result &= TestExpressionError(InputSettingsA.bExportSockets == InputSettingsB.bExportSockets, Header, "bExportSockets"); + Result &= TestExpressionError(InputSettingsA.bPreferNaniteFallbackMesh == InputSettingsB.bPreferNaniteFallbackMesh, Header, "bPreferNaniteFallbackMesh"); + Result &= TestExpressionError(InputSettingsA.bExportColliders == InputSettingsB.bExportColliders, Header, "bExportColliders"); + // Result &= TestExpressionError(A->bCookOnCurveChanged == B->bCookOnCurveChanged, Header, "bCookOnCurveChanged"); + + Result &= TestExpressionError(A->GeometryInputObjects.Num() == B->GeometryInputObjects.Num(), Header, "GeometryInputObjects.Num"); + for (int i = 0; i < FMath::Min(A->GeometryInputObjects.Num(), B->GeometryInputObjects.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->GeometryInputObjects[i], B->GeometryInputObjects[i]), Header, "GeometryInputObjects"); + } + + // Result &= TestExpressionError(A->bStaticMeshChanged == B->bStaticMeshChanged, Header, "bStaticMeshChanged"); + + // Skip EDITORONLYDATA (for simplicity) + Result &= TestExpressionError(A->bInputAssetConnectedInHoudini == B->bInputAssetConnectedInHoudini, Header, "bInputAssetConnectedInHoudini"); + + Result &= TestExpressionError(A->CurveInputObjects.Num() == B->CurveInputObjects.Num(), Header, "CurveInputObjects.Num"); + for (int i = 0; i < FMath::Min(A->CurveInputObjects.Num(), B->CurveInputObjects.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->CurveInputObjects[i], B->CurveInputObjects[i]), Header, "CurveInputObjects"); + } + + Result &= TestExpressionError(FMath::IsNearlyEqual(A->DefaultCurveOffset, B->DefaultCurveOffset, FLOAT_TOLERANCE), Header, "DefaultCurveOffset"); + + Result &= TestExpressionError(InputSettingsA.bAddRotAndScaleAttributesOnCurves == InputSettingsB.bAddRotAndScaleAttributesOnCurves, Header, "bAddRotAndScaleAttributeOnCurves"); + + Result &= TestExpressionError(InputSettingsA.bUseLegacyInputCurves == InputSettingsB.bUseLegacyInputCurves, Header, "bUseLegacyInputCurves"); + + // Result &= TestExpressionError(A->bLandscapeHasExportTypeChanged == B->bLandscapeHasExportTypeChanged, Header, "bLandscapeHasExportTypeChanged"); + + Result &= TestExpressionError(A->WorldInputObjects.Num() == B->WorldInputObjects.Num(), Header, "WorldInputObjects.Num"); + for (int i = 0; i < FMath::Min(A->WorldInputObjects.Num(), B->WorldInputObjects.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->WorldInputObjects[i], B->WorldInputObjects[i]), Header, "WorldInputObjects"); + } + + Result &= TestExpressionError(A->WorldInputBoundSelectorObjects.Num() == B->WorldInputBoundSelectorObjects.Num(), Header, "WorldInputBoundSelectorObjects.Num"); + for (int i = 0; i < FMath::Min(A->WorldInputBoundSelectorObjects.Num(), B->WorldInputBoundSelectorObjects.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->WorldInputBoundSelectorObjects[i], B->WorldInputBoundSelectorObjects[i]), Header, "WorldInputBoundSelectorObjects"); + } + + Result &= TestExpressionError(A->bIsWorldInputBoundSelector == B->bIsWorldInputBoundSelector, Header, "bIsWorldInputBoundSelector"); + Result &= TestExpressionError(A->bWorldInputBoundSelectorAutoUpdate == B->bWorldInputBoundSelectorAutoUpdate, Header, "bWorldInputBoundSelectorAutoUpdate"); + Result &= TestExpressionError(A->bWorldInputAutoUpdate == B->bWorldInputAutoUpdate, Header, "bWorldInputAutoUpdate"); + Result &= TestExpressionError(FMath::IsNearlyEqual(InputSettingsA.UnrealSplineResolution, InputSettingsB.UnrealSplineResolution, FLOAT_TOLERANCE), Header, "UnrealSplineResolution"); + + // Skip LastInsertedInputs + // Skip LastUndoDeletedInputs + + Result &= TestExpressionError(InputSettingsA.LandscapeExportType == InputSettingsB.LandscapeExportType, Header, "LandscapeExportType"); + Result &= TestExpressionError(InputSettingsA.bLandscapeExportSelectionOnly == InputSettingsB.bLandscapeExportSelectionOnly, Header, "bLandscapeExportSelectionOnly"); + Result &= TestExpressionError(InputSettingsA.bLandscapeAutoSelectComponent == InputSettingsB.bLandscapeAutoSelectComponent, Header, "bLandscapeAutoSelectComponent"); + Result &= TestExpressionError(InputSettingsA.bLandscapeExportMaterials == InputSettingsB.bLandscapeExportMaterials, Header, "bLandscapeExportMaterials"); + Result &= TestExpressionError(InputSettingsA.bLandscapeExportLighting == InputSettingsB.bLandscapeExportLighting, Header, "bLandscapeExportLighting"); + Result &= TestExpressionError(InputSettingsA.bLandscapeExportNormalizedUVs == InputSettingsB.bLandscapeExportNormalizedUVs, Header, "bLandscapeExportNormalizedUVs"); + Result &= TestExpressionError(InputSettingsA.bLandscapeExportTileUVs == InputSettingsB.bLandscapeExportTileUVs, Header, "bLandscapeExportTileUVs"); + Result &= TestExpressionError(A->bCanDeleteHoudiniNodes == B->bCanDeleteHoudiniNodes, Header, "bCanDeleteHoudiniNodes"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniInputObject* A, const UHoudiniInputObject* B) +{ + const FString Header = "UHoudiniInputObject"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(IsEquivalent(A->InputObject.Get(), B->InputObject.Get()), Header, "InputObject"); + Result &= TestExpressionError(IsEquivalent(A->Transform, B->Transform), Header, "Transform"); + Result &= TestExpressionError(A->Type == B->Type, Header, "Type"); + Result &= TestExpressionError(A->GetImportAsReference() == B->GetImportAsReference(), Header, "bImportAsReference"); + + // Skip EditorOnly data + + Result &= TestExpressionError(A->bCanDeleteHoudiniNodes == B->bCanDeleteHoudiniNodes, Header, "bCanDeleteHoudiniNodes"); + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniOutput* A, const UHoudiniOutput* B) +{ + const FString Header = "UHoudiniOutput"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->Type == B->Type, Header, "Type"); + + Result &= TestExpressionError(A->HoudiniGeoPartObjects.Num() == B->HoudiniGeoPartObjects.Num(), Header, "HoudiniGeoPartObjects.Num"); + for (int i = 0; i < FMath::Min(A->HoudiniGeoPartObjects.Num(), B->HoudiniGeoPartObjects.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->HoudiniGeoPartObjects[i], B->HoudiniGeoPartObjects[i]), Header, "HoudiniGeoPartObjects"); + } + Result &= TestExpressionError(A->OutputObjects.Num() == B->OutputObjects.Num(), Header, "OutputObjects.Num"); + for (auto PairA : A->OutputObjects) + { + SetTestExpressionError(false); + bool HasEquivalent = false; + for (auto PairB : B->OutputObjects) + { + if (IsEquivalent(PairA.Value, PairB.Value)) + { + HasEquivalent = true; + break; + } + } + SetTestExpressionError(true); + Result &= TestExpressionError(HasEquivalent, Header, "OutputObjects"); + } + Result &= TestExpressionError(A->InstancedOutputs.Num() == B->InstancedOutputs.Num(), Header, "InstancedOutputs.Num"); + for (auto PairA : A->InstancedOutputs) + { + SetTestExpressionError(false); + + bool HasEquivalent = false; + for (auto PairB : B->InstancedOutputs) + { + if (IsEquivalent(PairA.Value, PairB.Value)) + { + HasEquivalent = true; + break; + } + } + SetTestExpressionError(true); + Result &= TestExpressionError(HasEquivalent, Header, "InstancedOutputs"); + } + Result &= TestExpressionError(A->AssignmentMaterialsById.Num() == B->AssignmentMaterialsById.Num(), Header, "AssignmentMaterialsById.Num"); + for (auto PairA : A->AssignmentMaterialsById) + { + SetTestExpressionError(false); + + bool HasEquivalent = false; + for (auto PairB : B->AssignmentMaterialsById) + { + if (IsEquivalent(PairA.Value, PairB.Value)) + { + HasEquivalent = true; + break; + } + } + SetTestExpressionError(true); + Result &= TestExpressionError(HasEquivalent, Header, "AssignmentMaterialsById"); + } + Result &= TestExpressionError(A->ReplacementMaterialsById.Num() == B->ReplacementMaterialsById.Num(), Header, "ReplacementMaterialsById.Num"); + + for (auto PairA : A->ReplacementMaterialsById) + { + SetTestExpressionError(false); + bool HasEquivalent = false; + for (auto PairB : B->ReplacementMaterialsById) + { + if (IsEquivalent(PairA.Value, PairB.Value)) + { + HasEquivalent = true; + break; + } + } + SetTestExpressionError(true); + Result &= TestExpressionError(HasEquivalent, Header, "ReplacementMaterialsById"); + } + + + // Result &= TestExpressionError(A->StaleCount == B->StaleCount, Header, "StaleCount"); + Result &= TestExpressionError(A->bLandscapeWorldComposition == B->bLandscapeWorldComposition, Header, "bLandscapeWorldComposition"); + Result &= TestExpressionError(A->HoudiniCreatedSocketActors.Num() == B->HoudiniCreatedSocketActors.Num(), Header, "HoudiniCreatedSocketActors.Num"); + for (int i = 0; i < FMath::Min(A->HoudiniCreatedSocketActors.Num(), B->HoudiniCreatedSocketActors.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->HoudiniCreatedSocketActors[i], B->HoudiniCreatedSocketActors[i]), Header, "HoudiniCreatedSocketActors"); + } + Result &= TestExpressionError(A->HoudiniAttachedSocketActors.Num() == B->HoudiniAttachedSocketActors.Num(), Header, "HoudiniAttachedSocketActors.Num"); + for (int i = 0; i < FMath::Min(A->HoudiniAttachedSocketActors.Num(), B->HoudiniAttachedSocketActors.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->HoudiniAttachedSocketActors[i], B->HoudiniAttachedSocketActors[i]), Header, "HoudiniAttachedSocketActors"); + } + Result &= TestExpressionError(A->bIsEditableNode == B->bIsEditableNode, Header, "bIsEditableNode"); + // Result &= TestExpressionError(A->bIsUpdating == B->bIsUpdating, Header, "bIsUpdating"); + Result &= TestExpressionError(A->bCanDeleteHoudiniNodes == B->bCanDeleteHoudiniNodes, Header, "bCanDeleteHoudiniNodes"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniHandleComponent* A, const UHoudiniHandleComponent* B) +{ + const FString Header = "UHoudiniHandleComponent"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->XformParms.Num() == B->XformParms.Num(), Header, "XformParms.Num"); + for (int i = 0; i < FMath::Min(A->XformParms.Num(), B->XformParms.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->XformParms[i], B->XformParms[i]), Header, "XformParms"); + } + Result &= TestExpressionError(IsEquivalent(A->RSTParm, B->RSTParm), Header, "RSTParm"); + Result &= TestExpressionError(IsEquivalent(A->RotOrderParm, B->RotOrderParm), Header, "RotOrderParm"); + Result &= TestExpressionError(A->HandleType == B->HandleType, Header, "HandleType"); + Result &= TestExpressionError(A->HandleName.Equals(B->HandleName), Header, "HandleName"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniPDGAssetLink* A, const UHoudiniPDGAssetLink* B) +{ + const FString Header = "UHoudiniPDGAssetLink"; + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->AllTOPNetworks.Num() == B->AllTOPNetworks.Num(), Header, "AllTOPNetworks.Num"); + for (int i = 0; i < FMath::Min(A->AllTOPNetworks.Num(), B->AllTOPNetworks.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->AllTOPNetworks[i], B->AllTOPNetworks[i]), Header, "AllTOPNetworks"); + } + + Result &= TestExpressionError(A->SelectedTOPNetworkIndex == B->SelectedTOPNetworkIndex, Header, "SelectedTOPNetworkIndex"); + Result &= TestExpressionError(A->bAutoCook == B->bAutoCook, Header, "bAutoCook"); + Result &= TestExpressionError(A->bUseTOPNodeFilter == B->bUseTOPNodeFilter, Header, "bUseTOPNodeFilter"); + Result &= TestExpressionError(A->bUseTOPOutputFilter == B->bUseTOPOutputFilter, Header, "bUseTOPOutputFilter"); + Result &= TestExpressionError(A->TOPNodeFilter.Equals(B->TOPNodeFilter), Header, "TOPNodeFilter"); + Result &= TestExpressionError(A->TOPOutputFilter.Equals(B->TOPOutputFilter), Header, "TOPOutputFilter"); + Result &= TestExpressionError(A->NumWorkItems == B->NumWorkItems, Header, "NumWorkItems"); + Result &= TestExpressionError(A->OutputCachePath.Equals(B->OutputCachePath), Header, "OutputCachePath"); + Result &= TestExpressionError(IsEquivalent(A->OutputParentActor, B->OutputParentActor), Header, "OutputParentActor"); + Result &= TestExpressionError(IsEquivalent(A->BakeFolder, B->BakeFolder), Header, "BakeFolder"); + + // Skip editorOnly data + + return Result; +} + +// Houdini struct equivalence functions === + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FHoudiniStaticMeshGenerationProperties& A, + const FHoudiniStaticMeshGenerationProperties& B) +{ + const FString Header = "FHoudiniStaticMeshGenerationProperties"; + bool Result = true; + + Result &= TestExpressionError(A.bGeneratedDoubleSidedGeometry == B.bGeneratedDoubleSidedGeometry, Header, "bGeneratedDoubleSidedGeometry"); + Result &= TestExpressionError(IsEquivalent(A.GeneratedPhysMaterial, B.GeneratedPhysMaterial), Header, "GeneratedPhysMaterial"); + Result &= TestExpressionError(IsEquivalent(A.DefaultBodyInstance, B.DefaultBodyInstance), Header, "DefaultBodyInstance"); + Result &= TestExpressionError(A.GeneratedCollisionTraceFlag == B.GeneratedCollisionTraceFlag, Header, "GeneratedCollisionTraceFlag"); + Result &= TestExpressionError(A.GeneratedLightMapResolution == B.GeneratedLightMapResolution, Header, "GeneratedLightMapResolution"); + //Result &= TestExpressionError(FMath::IsNearlyEqual(A.GeneratedLpvBiasMultiplier, B.GeneratedLpvBiasMultiplier), Header, "GeneratedLpvBiasMultiplier"); + Result &= TestExpressionError(IsEquivalent(A.GeneratedWalkableSlopeOverride, B.GeneratedWalkableSlopeOverride), Header, "GeneratedWalkableSlopOverride"); + Result &= TestExpressionError(A.GeneratedLightMapCoordinateIndex == B.GeneratedLightMapCoordinateIndex, Header, "GeneratedLightMapCoordinateIndex"); + Result &= TestExpressionError(A.bGeneratedUseMaximumStreamingTexelRatio == B.bGeneratedUseMaximumStreamingTexelRatio, Header, "bGeneratedUseMaximumStreamingTexelRatio"); + Result &= TestExpressionError(A.GeneratedFoliageDefaultSettings == B.GeneratedFoliageDefaultSettings, Header, "GeneratedFoliageDefaultSettings"); + + Result &= TestExpressionError(A.GeneratedAssetUserData.Num() == B.GeneratedAssetUserData.Num(), Header, "GeneratedAssetUserData.Num"); + for (int i = 0; i < FMath::Min(A.GeneratedAssetUserData.Num(), B.GeneratedAssetUserData.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A.GeneratedAssetUserData[i], B.GeneratedAssetUserData[i]), Header, "GeneratedAssetUserData"); + } + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FHoudiniBakedOutput& A, const FHoudiniBakedOutput& B) +{ + const FString Header = "FHoudiniBakedOutput"; + bool Result = true; + + Result &= TestExpressionError(A.BakedOutputObjects.Num() == B.BakedOutputObjects.Num(), Header, "BakedOutputObjects.Num"); + + for (auto PairA : A.BakedOutputObjects) + { + SetTestExpressionError(false); + bool HasEquivalent = false; + for (auto PairB : B.BakedOutputObjects) + { + if (IsEquivalent(PairA.Value, PairB.Value)) + { + HasEquivalent = true; + break; + } + } + SetTestExpressionError(true); + Result &= TestExpressionError(HasEquivalent, Header, "BakedOutputObjects"); + } + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FHoudiniGeoPartObject& A, const FHoudiniGeoPartObject& B) +{ + const FString Header = "FHoudiniGeoPartObject"; + bool Result = true; + + // Skip AssetId, ObjectId, AssetName, ObjectName, GeoId + Result &= TestExpressionError(A.PartId == B.PartId, Header, "PartId"); + Result &= TestExpressionError(A.PartName.Equals(B.PartName), Header, "PartName"); + Result &= TestExpressionError(A.bHasCustomPartName == B.bHasCustomPartName, Header, "bHasCustomPartName"); + Result &= TestExpressionError(A.SplitGroups.Num() == B.SplitGroups.Num(), Header, "SplitGroups.Num"); + for (int i = 0; i < FMath::Min(A.SplitGroups.Num(), B.SplitGroups.Num()); i++) + { + Result &= TestExpressionError(A.SplitGroups[i].Equals(B.SplitGroups[i]), Header, "SplitGroups"); + } + Result &= TestExpressionError(IsEquivalent(A.TransformMatrix, B.TransformMatrix), Header, "TransformMatrix"); + // Result &= TestExpressionError(A.NodePath.Equals(B.NodePath), Header, "NodePath"); + Result &= TestExpressionError(A.Type == B.Type, Header, "Type"); + Result &= TestExpressionError(A.InstancerType == B.InstancerType, Header, "InstancerType"); + Result &= TestExpressionError(A.VolumeName.Equals(B.VolumeName), Header, "VolumeName"); + Result &= TestExpressionError(A.VolumeTileIndex == B.VolumeTileIndex, Header, "VolumeTileIndex"); + Result &= TestExpressionError(A.bIsVisible == B.bIsVisible, Header, "bIsVisible"); + Result &= TestExpressionError(A.bIsEditable == B.bIsEditable, Header, "bIsEditable"); + Result &= TestExpressionError(A.bIsTemplated == B.bIsTemplated, Header, "bIsTemplated"); + Result &= TestExpressionError(A.bIsInstanced == B.bIsInstanced, Header, "bIsInstanced"); + //Result &= TestExpressionError(A.bHasGeoChanged == B.bHasGeoChanged, Header, "bHasGeoChanged"); + //Result &= TestExpressionError(A.bHasPartChanged == B.bHasPartChanged, Header, "bHasPartChanged"); + //Result &= TestExpressionError(A.bHasTransformChanged == B.bHasTransformChanged, Header, "bHasTransformChanged"); + //Result &= TestExpressionError(A.bHasMaterialsChanged == B.bHasMaterialsChanged, Header, "bHasMaterialsChanged"); + Result &= TestExpressionError(A.bLoaded == B.bLoaded, Header, "bLoaded"); + //Result &= TestExpressionError(IsEquivalent(A.ObjectInfo, B.ObjectInfo), Header, "ObjectInfo"); + //Result &= TestExpressionError(IsEquivalent(A.GeoInfo, B.GeoInfo), Header, "GeoInfo"); + //Result &= TestExpressionError(IsEquivalent(A.PartInfo, B.PartInfo), Header, "PartInfo"); + //Result &= TestExpressionError(IsEquivalent(A.VolumeInfo, B.VolumeInfo), Header, "VolumeInfo"); + //Result &= TestExpressionError(IsEquivalent(A.CurveInfo, B.CurveInfo), Header, "CurveInfo"); + Result &= TestExpressionError(A.AllMeshSockets.Num() == B.AllMeshSockets.Num(), Header, "AllMeshSockets.Num"); + for (int i = 0; i < FMath::Min(A.AllMeshSockets.Num(), B.AllMeshSockets.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A.AllMeshSockets[i], B.AllMeshSockets[i]), Header, "AllMeshSockets"); + } + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FHoudiniInstancedOutput& A, const FHoudiniInstancedOutput& B) +{ + const FString Header = "FHoudiniInstancedOutput"; + bool Result = true; + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FHoudiniBakedOutputObject& A, const FHoudiniBakedOutputObject& B) +{ + const FString Header = "FHoudiniBakedOutputObject"; + bool Result = true; + + // Result &= TestExpressionError(A.Actor.Equals(B.Actor), Header, "Actor"); + // Result &= TestExpressionError(A.Blueprint.Equals(B.Blueprint), Header, "Blueprint"); + Result &= TestExpressionError(A.ActorBakeName.IsEqual(B.ActorBakeName), Header, "ActorBakeName"); + Result &= TestExpressionError(A.BakedObject.Equals(B.BakedObject), Header, "BakedObject"); + Result &= TestExpressionError(A.BakedComponent.Equals(B.BakedComponent), Header, "BakedComponent"); + Result &= TestExpressionError(A.InstancedActors.Num() == B.InstancedActors.Num(), Header, "InstancedActors.Num"); + for (int i = 0; i < FMath::Min(A.InstancedActors.Num(), B.InstancedActors.Num()); i++) + { + Result &= TestExpressionError(A.InstancedActors[i].Equals(B.InstancedActors[i]), Header, "InstancedActors"); + } + Result &= TestExpressionError(A.InstancedComponents.Num() == B.InstancedComponents.Num(), Header, "InstancedComponents.Num"); + for (int i = 0; i < FMath::Min(A.InstancedComponents.Num(), B.InstancedComponents.Num()); i++) + { + Result &= TestExpressionError(A.InstancedComponents[i].Equals(B.InstancedComponents[i]), Header, "InstancedComponents"); + } + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FHoudiniObjectInfo& A, const FHoudiniObjectInfo& B) +{ + const FString Header = "FHoudiniObjectInfo"; + bool Result = true; + + // Skip Name + // Result &= TestExpressionError(A.bHasTransformChanged == B.bHasTransformChanged, Header, "bHasTransformChanged"); + // Result &= TestExpressionError(A.bHaveGeosChanged == B.bHaveGeosChanged, Header, "bHaveGeosChanged"); + Result &= TestExpressionError(A.bIsVisible == B.bIsVisible, Header, "bIsVisible"); + Result &= TestExpressionError(A.bIsInstancer == B.bIsInstancer, Header, "bIsInstancer"); + Result &= TestExpressionError(A.bIsInstanced == B.bIsInstanced, Header, "bIsInstanced"); + Result &= TestExpressionError(A.GeoCount == B.GeoCount, Header, "GeoCount"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FHoudiniGeoInfo& A, const FHoudiniGeoInfo& B) +{ + const FString Header = "FHoudiniGeoInfo"; + bool Result = true; + + Result &= TestExpressionError(A.Type == B.Type, Header, "Type"); + Result &= TestExpressionError(A.Name.Equals(B.Name), Header, "Name"); + Result &= TestExpressionError(A.bIsEditable == B.bIsEditable, Header, "bIsEditable"); + Result &= TestExpressionError(A.bIsTemplated == B.bIsTemplated, Header, "bIsTemplated"); + Result &= TestExpressionError(A.bIsDisplayGeo == B.bIsDisplayGeo, Header, "bIsDisplayGeo"); + // Result &= TestExpressionError(A.bHasGeoChanged == B.bHasGeoChanged, Header, "bHasGeoChanged"); + // Result &= TestExpressionError(A.bHasMaterialChanged == B.bHasMaterialChanged, Header, "bHasMaterialChanged"); + Result &= TestExpressionError(A.PartCount == B.PartCount, Header, "PartCount"); + Result &= TestExpressionError(A.PointGroupCount == B.PointGroupCount, Header, "PointGroupCount"); + Result &= TestExpressionError(A.PrimitiveGroupCount == B.PrimitiveGroupCount, Header, "PrimitiveGroupCount"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FHoudiniPartInfo& A, const FHoudiniPartInfo& B) +{ + const FString Header = "FHoudiniPartInfo"; + bool Result = true; + + Result &= TestExpressionError(A.PartId == B.PartId, Header, "PartId"); + Result &= TestExpressionError(A.Name.Equals(B.Name), Header, "Name"); + Result &= TestExpressionError(A.Type == B.Type, Header, "Type"); + Result &= TestExpressionError(A.FaceCount == B.FaceCount, Header, "FaceCount"); + Result &= TestExpressionError(A.VertexCount == B.VertexCount, Header, "VertexCount"); + Result &= TestExpressionError(A.PointCount == B.PointCount, Header, "PointCount"); + Result &= TestExpressionError(A.PointAttributeCounts == B.PointAttributeCounts, Header, "PointAttributeCounts"); + Result &= TestExpressionError(A.VertexAttributeCounts == B.VertexAttributeCounts, Header, "VertexAttributeCounts"); + Result &= TestExpressionError(A.PrimitiveAttributeCounts == B.PrimitiveAttributeCounts, Header, "PrimitiveAttributeCounts"); + Result &= TestExpressionError(A.DetailAttributeCounts == B.DetailAttributeCounts, Header, "DetailAttributeCounts"); + Result &= TestExpressionError(A.bIsInstanced == B.bIsInstanced, Header, "bIsInstanced"); + Result &= TestExpressionError(A.InstancedPartCount == B.InstancedPartCount, Header, "InstancedPartCount"); + Result &= TestExpressionError(A.InstanceCount == B.InstanceCount, Header, "InstanceCount"); + // Result &= TestExpressionError(A.bHasChanged == B.bHasChanged, Header, "bHasChanged"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FHoudiniVolumeInfo& A, const FHoudiniVolumeInfo& B) +{ + const FString Header = "FHoudiniVolumeInfo"; + bool Result = true; + + Result &= TestExpressionError(A.Name.Equals(B.Name), Header, "Name"); + Result &= TestExpressionError(A.bIsVDB == B.bIsVDB, Header, "bIsVDB"); + Result &= TestExpressionError(A.TupleSize == B.TupleSize, Header, "TupleSize"); + Result &= TestExpressionError(A.bIsFloat == B.bIsFloat, Header, "bIsFloat"); + Result &= TestExpressionError(A.TileSize == B.TileSize, Header, "TileSize"); + Result &= TestExpressionError(IsEquivalent(A.Transform, B.Transform), Header, "Transform"); + Result &= TestExpressionError(A.bHasTaper == B.bHasTaper, Header, "bHasTaper"); + Result &= TestExpressionError(A.XLength == B.XLength, Header, "XLength"); + Result &= TestExpressionError(A.YLength == B.YLength, Header, "YLength"); + Result &= TestExpressionError(A.ZLength == B.ZLength, Header, "ZLength"); + Result &= TestExpressionError(A.MinX == B.MinX, Header, "MinX"); + Result &= TestExpressionError(A.MinY == B.MinY, Header, "MinY"); + Result &= TestExpressionError(A.MinZ == B.MinZ, Header, "MinZ"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A.XTaper, B.XTaper, FLOAT_TOLERANCE), Header, "XTaper"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A.YTaper, B.YTaper, FLOAT_TOLERANCE), Header, "YTaper"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FHoudiniCurveInfo& A, const FHoudiniCurveInfo& B) +{ + const FString Header = "FHoudiniCurveInfo"; + bool Result = true; + + Result &= TestExpressionError(A.Type == B.Type, Header, "Type"); + Result &= TestExpressionError(A.CurveCount == B.CurveCount, Header, "CurveCount"); + Result &= TestExpressionError(A.VertexCount == B.VertexCount, Header, "VertexCount"); + Result &= TestExpressionError(A.KnotCount == B.KnotCount, Header, "KnotCount"); + Result &= TestExpressionError(A.bIsPeriodic == B.bIsPeriodic, Header, "bIsPeriodic"); + Result &= TestExpressionError(A.bIsRational == B.bIsRational, Header, "bIsRational"); + Result &= TestExpressionError(A.Order == B.Order, Header, "Order"); + Result &= TestExpressionError(A.bHasKnots == B.bHasKnots, Header, "bHasKnots"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FHoudiniMeshSocket& A, const FHoudiniMeshSocket& B) +{ + const FString Header = "FHoudiniMeshSocket"; + bool Result = true; + + Result &= TestExpressionError(A == B, Header, "Equals"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FHoudiniOutputObject& A, const FHoudiniOutputObject& B) +{ + const FString Header = "FHoudiniOutputObject"; + bool Result = true; + + Result &= TestExpressionError(IsEquivalent(A.OutputObject, B.OutputObject), Header, "OutputObject"); + + if (A.OutputComponents.Num() != B.OutputComponents.Num()) + return false; + + for(int Index = 0; Index < A.OutputComponents.Num(); Index++) + { + Result &= TestExpressionError(IsEquivalent(A.OutputComponents[Index], B.OutputComponents[Index]), Header, "OutputComponent"); + } + + // Proxies can't be saved, so comparing them will be difficult (saving refines the proxies to SM) + // Result &= TestExpressionError(IsEquivalent(A.ProxyObject, B.ProxyObject), Header, "ProxyObject"); + // Result &= TestExpressionError(IsEquivalent(A.ProxyComponent, B.ProxyComponent), Header, "ProxyComponent"); + Result &= TestExpressionError(A.bProxyIsCurrent == B.bProxyIsCurrent, Header, "bProxyIsCurrent"); + Result &= TestExpressionError(A.bIsImplicit == B.bIsImplicit, Header, "bIsImplicit"); + Result &= TestExpressionError(A.BakeName.Equals(B.BakeName), Header, "BakeName"); + Result &= TestExpressionError(IsEquivalent(A.CurveOutputProperty, B.CurveOutputProperty), Header, "CurveOutputProperty"); + // Comparing CachedAttributes/CachedTokens will be specific to certain advanced tests + // Result &= TestExpressionError(A.CachedAttributes.Num() == B.CachedAttributes.Num(), Header, "CachedAttributes.Num"); + // for (auto PairA : A.CachedAttributes) + // { + // SetTestExpressionError(false); + // bool HasEquivalent = false; + // for (auto PairB : B.CachedAttributes) + // { + // if (PairA.Value.Equals(PairB.Value)) + // { + // HasEquivalent = true; + // break; + // } + // } + // SetTestExpressionError(true); + // Result &= TestExpressionError(HasEquivalent, Header, "CachedAttributes"); + // } + // Skip CachedTokens as they can be different. + //Result &= TestExpressionError(A.CachedTokens.Num() == B.CachedTokens.Num(), Header, "CachedTokens.Num"); +// + //for (auto PairA : A.CachedTokens) + //{ + // SetTestExpressionError(false); + // bool HasEquivalent = false; + // for (auto PairB : B.CachedTokens) + // { + // if (PairA.Value.Equals(PairB.Value)) + // { + // HasEquivalent = true; + // break; + // } + // } + // SetTestExpressionError(true); + // Result &= TestExpressionError(HasEquivalent, Header, "CachedTokens"); + //} + + + return Result; +} + + +// Unreal Equivalence functions === +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FDirectoryPath & A, const FDirectoryPath & B) +{ + const FString Header = "FDirectoryPath"; + bool Result = true; + + Result &= TestExpressionError(A.Path.Equals(B.Path), Header, "Path"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FMeshBuildSettings& A, const FMeshBuildSettings& B) +{ + const FString Header = "FMeshBuildSettings"; + bool Result = true; + + // Skip everything (for simplicity) + Result &= TestExpressionError(A == B, Header, "Equality"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FTransform& A, const FTransform& B) +{ + const FString Header = "FTransform"; + bool Result = true; + + Result &= TestExpressionError(IsEquivalent(A.GetTranslation(), B.GetTranslation()), Header, "Translation"); + Result &= TestExpressionError(IsEquivalent(A.GetRotation(), B.GetRotation()), Header, "Rotation"); + Result &= TestExpressionError(IsEquivalent(A.GetScale3D(), B.GetScale3D()), Header, "Scale3D"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FBodyInstance& A, const FBodyInstance& B) +{ + const FString Header = "FBodyInstance"; + bool Result = true; + + // Nothing interesting to test + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FWalkableSlopeOverride& A, const FWalkableSlopeOverride& B) +{ + const FString Header = "FWalkableSlopeOverride"; + bool Result = true; + + // Nothing interesting to test + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FHoudiniCurveOutputProperties& A, const FHoudiniCurveOutputProperties& B) +{ + const FString Header = "FHoudiniCurveOutputProperties"; + bool Result = true; + + Result &= TestExpressionError(A.CurveOutputType == B.CurveOutputType, Header, "CurveOutputType"); + Result &= TestExpressionError(A.NumPoints == B.NumPoints, Header, "NumPoints"); + Result &= TestExpressionError(A.bClosed == B.bClosed, Header, "bClosed"); + Result &= TestExpressionError(A.CurveType == B.CurveType, Header, "CurveType"); + Result &= TestExpressionError(A.CurveMethod == B.CurveMethod, Header, "CurveMethod"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FColor& A, const FColor& B) +{ + const FString Header = "FColor"; + bool Result = true; + + Result &= TestExpressionError(A.R == B.R, Header, "R"); + Result &= TestExpressionError(A.G == B.G, Header, "G"); + Result &= TestExpressionError(A.B == B.B, Header, "B"); + Result &= TestExpressionError(A.A == B.A, Header, "A"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FLinearColor& A, const FLinearColor& B) +{ + const FString Header = "FLinearColor"; + bool Result = true; + + Result &= TestExpressionError(FMath::IsNearlyEqual(A.R, B.R, FLOAT_TOLERANCE), Header, "R"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A.G, B.G, FLOAT_TOLERANCE), Header, "G"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A.B, B.B, FLOAT_TOLERANCE), Header, "B"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A.A, B.A, FLOAT_TOLERANCE), Header, "A"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FVector& A, const FVector& B) +{ + const FString Header = "FVector"; + bool Result = true; + + Result &= TestExpressionError(FMath::IsNearlyEqual(A.X, B.X, FLOAT_TOLERANCE), Header, "X"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A.Y, B.Y, FLOAT_TOLERANCE), Header, "Y"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A.Z, B.Z, FLOAT_TOLERANCE), Header, "Z"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FQuat& A, const FQuat& B) +{ + const FString Header = "FQuat"; + + bool Result = true; + Result &= TestExpressionError(FMath::IsNearlyEqual(A.X, B.X, FLOAT_TOLERANCE), Header, "X"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A.Y, B.Y, FLOAT_TOLERANCE), Header, "Y"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A.Z, B.Z, FLOAT_TOLERANCE), Header, "Z"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A.W, B.W, FLOAT_TOLERANCE), Header, "W"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FIntVector& A, const FIntVector& B) +{ + const FString Header = "FIntVector"; + bool Result = true; + + Result &= TestExpressionError(A.X == B.X, Header, "X"); + Result &= TestExpressionError(A.Y == B.Y, Header, "Y"); + Result &= TestExpressionError(A.Z == B.Z, Header, "Z"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FVector2D& A, const FVector2D& B) +{ + const FString Header = "FVector2D"; + bool Result = true; + + Result &= TestExpressionError(FMath::IsNearlyEqual(A.X, B.X, FLOAT_TOLERANCE), Header, "X"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A.Y, B.Y, FLOAT_TOLERANCE), Header, "Y"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FVector2f& A, const FVector2f& B) +{ + const FString Header = "FVector2f"; + bool Result = true; + + Result &= TestExpressionError(FMath::IsNearlyEqual(A.X, B.X, FLOAT_TOLERANCE), Header, "X"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A.Y, B.Y, FLOAT_TOLERANCE), Header, "Y"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FStaticMaterial& A, const FStaticMaterial& B) +{ + const FString Header = "FStaticMaterial"; + bool Result = true; + + Result &= TestExpressionError(A == B, Header, "Equality"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const FBox& A, const FBox& B) +{ + const FString Header = "FBox"; + bool Result = true; + + Result &= TestExpressionError(IsEquivalent(A.Min, B.Min), Header, "Min"); + Result &= TestExpressionError(IsEquivalent(A.Max, B.Max), Header, "Max"); + Result &= TestExpressionError(A.IsValid == B.IsValid, Header, "IsValid"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UObject* A, const UObject* B) +{ + const FString Header = "UObject"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + // Compare class + Result &= TestExpressionError(A->StaticClass() == B->StaticClass(), Header, "StaticClass"); + + // Test all possible Runtime UObjects in Houdini + // NOTE: MAKE SURE TO IMPLEMENT THE FUNCTIONS OTHERWISE INFINITE RECURSION WILL OCCUR + + // Misc + if (A->IsA(UHoudiniAsset::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(AHoudiniAssetActor::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniHandleComponent::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniInput::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniInputObject::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniInstancedActorComponent::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniOutput::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + // Parameters + else if (A->IsA(UHoudiniParameterButton::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterButtonStrip::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterChoice::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterColor::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterFile::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterFloat::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterFolder::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterFolderList::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterInt::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterLabel::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterMultiParm::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterOperatorPath::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterRampFloatPoint::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterRampColorPoint::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterRampFloat::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterRampColor::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterSeparator::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterString::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniParameterToggle::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + // Outputs + else if (A->IsA(UHoudiniSplineComponent::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniStaticMesh::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniStaticMeshComponent::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniLandscapePtr::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + else if (A->IsA(UHoudiniLandscapeTargetLayerOutput::StaticClass())) + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Object cast"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UPhysicalMaterial * A, const UPhysicalMaterial * B) +{ + const FString Header = "UPhysicalMaterial"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->GetName().Equals(B->GetName()), Header, "Name"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const AActor* A, const AActor* B) +{ + const FString Header = "AActor"; + + bool Result = true; + + Result &= TestExpressionError((A != nullptr) == (B != nullptr), Header, "Null check"); + + if (A == nullptr || B == nullptr) + { + return true; + } + + // Compare the actors class + Result &= TestExpressionError(IsEquivalent(A->GetClass(), B->GetClass()), Header, "Cast"); + + // Just compare UObject + Result &= TestExpressionError(IsEquivalent(Cast(A), Cast(B)), Header, "Cast"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const ALandscapeProxy* A, const ALandscapeProxy* B) +{ + const FString Header = "ALandscapeProxy"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + // TODO: Not sure what's important to test here + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const AHoudiniAssetActor* A, const AHoudiniAssetActor* B) +{ + const FString Header = "AHoudiniAssetActor"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(IsEquivalent(A->HoudiniAssetComponent, B->HoudiniAssetComponent), Header, "HoudiniAssetComponent"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniInstancedActorComponent* A, + const UHoudiniInstancedActorComponent* B) +{ + const FString Header = "UHoudiniInstancedActorComponent"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(IsEquivalent(A->InstancedObject, B->InstancedObject), Header, "InstancedObject"); + + Result &= TestExpressionError(A->InstancedActors.Num() == B->InstancedActors.Num(), Header, "InstancedActors.Num"); + for (int i = 0; i < FMath::Min(A->InstancedActors.Num(), B->InstancedActors.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->InstancedActors[i], B->InstancedActors[i]), Header, "InstancedActors"); + } + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterButton* A, const UHoudiniParameterButton* B) +{ + const FString Header = "UHoudiniParameterButton"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterButtonStrip* A, const UHoudiniParameterButtonStrip* B) +{ + const FString Header = "UHoudiniParameterButtonStrip"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->GetNumValues() == B->GetNumValues(), Header, "NumValues"); + for (uint32 i = 0; i < FMath::Min(A->GetNumValues(), B->GetNumValues()); i++) + { + const FString* LabelA = A->GetStringLabelAt(i); + const FString* LabelB = B->GetStringLabelAt(i); + + if (!LabelA || !LabelB) + { + Result = false; + continue; + } + + Result &= TestExpressionError(LabelA->Equals(*LabelB), Header, "Labels"); + } + + for (uint32 i = 0; i < FMath::Min(A->GetNumValues(), B->GetNumValues()); i++) + { + Result &= TestExpressionError(A->GetValueAt(i) == B->GetValueAt(i), Header, "Values"); + } + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterChoice* A, const UHoudiniParameterChoice* B) +{ + const FString Header = "UHoudiniParameterChoice"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->IntValue == B->IntValue, Header, "IntValue"); + Result &= TestExpressionError(A->DefaultIntValue == B->DefaultIntValue, Header, "DefaultIntValue"); + Result &= TestExpressionError(A->StringValue.Equals(B->StringValue), Header, "StringValue"); + Result &= TestExpressionError(A->DefaultStringValue.Equals(B->DefaultStringValue), Header, "DefaultStringValue"); + Result &= TestExpressionError(A->StringChoiceValues.Num() == B->StringChoiceValues.Num(), Header, "StringChoiceValues.Num"); + for (int i = 0; i < FMath::Min(A->StringChoiceValues.Num(), B->StringChoiceValues.Num()); i++) + { + Result &= TestExpressionError(A->StringChoiceValues[i].Equals(B->StringChoiceValues[i]), Header, "StringChoiceValues"); + } + Result &= TestExpressionError(A->StringChoiceLabels.Num() == B->StringChoiceLabels.Num(), Header, "StringChoiceLabels.Num"); + for (int i = 0; i < FMath::Min(A->StringChoiceLabels.Num(), B->StringChoiceLabels.Num()); i++) + { + Result &= TestExpressionError(A->StringChoiceLabels[i].Equals(B->StringChoiceLabels[i]), Header, "StringChoiceLabels"); + } + // Result &= TestExpressionError(A->ChoiceLabelsPtr.Num() == B->ChoiceLabelsPtr.Num(), Header, "ChoiceLabelsPtr.Num"); + // for (int i = 0; i < A->ChoiceLabelsPtr.Num(); i++) + // { + // Result &= TestExpressionError(A->ChoiceLabelsPtr[i]->Equals(B->ChoiceLabelsPtr[i])), Header, "ChoiceLabelsPtr"); + // } + Result &= TestExpressionError(A->bIsChildOfRamp == B->bIsChildOfRamp, Header, "bIsChildOfRamp"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterColor* A, const UHoudiniParameterColor* B) +{ + const FString Header = "UHoudiniParameterColor"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(IsEquivalent(A->Color, B->Color), Header, "Color"); + Result &= TestExpressionError(IsEquivalent(A->DefaultColor, B->DefaultColor), Header, "DefaultColor"); + Result &= TestExpressionError(A->bIsChildOfRamp == B->bIsChildOfRamp, Header, "bIsChildOfRamp"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterFile* A, const UHoudiniParameterFile* B) +{ + const FString Header = "UHoudiniParameterColor"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->Values.Num() == B->Values.Num(), Header, "Values.Num"); + for (int i = 0; i < FMath::Min(A->Values.Num(), B->Values.Num()); i++) + { + Result &= TestExpressionError(A->Values[i].Equals(B->Values[i]), Header, "Values"); + } + Result &= TestExpressionError(A->DefaultValues.Num() == B->DefaultValues.Num(), Header, "DefaultValues.Num"); + for (int i = 0; i < FMath::Min(A->DefaultValues.Num(), B->DefaultValues.Num()); i++) + { + Result &= TestExpressionError(A->DefaultValues[i].Equals(B->DefaultValues[i]), Header, "DefaultValues"); + } + Result &= TestExpressionError(A->Filters.Equals(B->Filters), Header, "Filters"); + Result &= TestExpressionError(A->bIsReadOnly == B->bIsReadOnly, Header, "bIsReadOnly"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterFloat* A, const UHoudiniParameterFloat* B) +{ + const FString Header = "UHoudiniParameterFloat"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->Values.Num() == B->Values.Num(), Header, "Values.Num"); + for (int i = 0; i < FMath::Min(A->Values.Num(), B->Values.Num()); i++) + { + Result &= TestExpressionError(FMath::IsNearlyEqual(A->Values[i], B->Values[i], FLOAT_TOLERANCE), Header, "Values"); + } + Result &= TestExpressionError(A->DefaultValues.Num() == B->DefaultValues.Num(), Header, "DefaultValues.Num"); + for (int i = 0; i < FMath::Min(A->DefaultValues.Num(), B->DefaultValues.Num()); i++) + { + Result &= TestExpressionError(FMath::IsNearlyEqual(A->DefaultValues[i], B->DefaultValues[i]), Header, "DefaultValues"); + } + Result &= TestExpressionError(A->Unit.Equals(B->Unit), Header, "Unit"); + Result &= TestExpressionError(A->bNoSwap == B->bNoSwap, Header, "bNoSwap"); + Result &= TestExpressionError(A->bHasMin == B->bHasMin, Header, "bHasMin"); + Result &= TestExpressionError(A->bHasMax == B->bHasMax, Header, "bHasMax"); + Result &= TestExpressionError(A->bHasUIMin == B->bHasUIMin, Header, "bHasUIMin"); + Result &= TestExpressionError(A->bHasUIMax == B->bHasUIMax, Header, "bHasUIMax"); + Result &= TestExpressionError(A->bIsLogarithmic == B->bIsLogarithmic, Header, "bIsLogarithmic"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A->Min, B->Min, FLOAT_TOLERANCE), Header, "Min"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A->Max, B->Max, FLOAT_TOLERANCE), Header, "Max"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A->UIMin, B->UIMin, FLOAT_TOLERANCE), Header, "UIMin"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A->UIMax, B->UIMax, FLOAT_TOLERANCE), Header, "UIMax"); + Result &= TestExpressionError(A->bIsChildOfRamp == B->bIsChildOfRamp, Header, "bIsChildOfRamp"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterFolder* A, const UHoudiniParameterFolder* B) +{ + const FString Header = "UHoudiniParameterColor"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->FolderType == B->FolderType, Header, "FolderType"); + Result &= TestExpressionError(A->bExpanded == B->bExpanded, Header, "bExpanded"); + Result &= TestExpressionError(A->bChosen == B->bChosen, Header, "bChosen"); + Result &= TestExpressionError(A->ChildCounter == B->ChildCounter, Header, "ChildCounter"); + Result &= TestExpressionError(A->bIsContentShown == B->bIsContentShown, Header, "bIsContentShown"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterFolderList* A, const UHoudiniParameterFolderList* B) +{ + const FString Header = "UHoudiniParameterFolderList"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->bIsTabMenu == B->bIsTabMenu, Header, "bIsTabMenu"); + Result &= TestExpressionError(A->bIsTabsShown == B->bIsTabsShown, Header, "bIsTabsShown"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterInt* A, const UHoudiniParameterInt* B) +{ + const FString Header = "UHoudiniParameterInt"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->Values.Num() == B->Values.Num(), Header, "Values.Num"); + for (int i = 0; i < FMath::Min(A->Values.Num(), B->Values.Num()); i++) + { + Result &= TestExpressionError(A->Values[i] == B->Values[i], Header, "Values"); + } + Result &= TestExpressionError(A->DefaultValues.Num() == B->DefaultValues.Num(), Header, "DefaultValues.Num"); + for (int i = 0; i < FMath::Min(A->DefaultValues.Num(), B->DefaultValues.Num()); i++) + { + Result &= TestExpressionError(A->DefaultValues[i] == B->DefaultValues[i], Header, "DefaultValues"); + } + Result &= TestExpressionError(A->Unit.Equals(B->Unit), Header, "Unit"); + Result &= TestExpressionError(A->bHasMin == B->bHasMin, Header, "bHasMin"); + Result &= TestExpressionError(A->bHasMax == B->bHasMax, Header, "bHasMax"); + Result &= TestExpressionError(A->bHasUIMin == B->bHasUIMin, Header, "bHasUIMin"); + Result &= TestExpressionError(A->bHasUIMax == B->bHasUIMax, Header, "bHasUIMax"); + Result &= TestExpressionError(A->bIsLogarithmic == B->bIsLogarithmic, Header, "bIsLogarithmic"); + Result &= TestExpressionError(A->Min == B->Min, Header, "Min"); + Result &= TestExpressionError(A->Max == B->Max, Header, "Max"); + Result &= TestExpressionError(A->UIMin == B->UIMin, Header, "UIMin"); + Result &= TestExpressionError(A->UIMax == B->UIMax, Header, "UIMax"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterLabel* A, const UHoudiniParameterLabel* B) +{ + const FString Header = "UHoudiniParameterLabel"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->LabelStrings.Num() == B->LabelStrings.Num(), Header, "LabelStrings.Num"); + for (int i = 0; i < FMath::Min(A->LabelStrings.Num(), B->LabelStrings.Num()); i++) + { + Result &= TestExpressionError(A->LabelStrings[i].Equals(B->LabelStrings[i]), Header, "LabelStrings"); + } + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterMultiParm* A, const UHoudiniParameterMultiParm* B) +{ + const FString Header = "UHoudiniParameterMultiParm"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->bIsShown == B->bIsShown, Header, "bIsShown"); + Result &= TestExpressionError(A->Value == B->Value, Header, "Value"); + Result &= TestExpressionError(A->TemplateName.Equals(B->TemplateName), Header, "TemplateName"); + Result &= TestExpressionError(A->MultiparmValue == B->MultiparmValue, Header, "MultiparmValue"); + Result &= TestExpressionError(A->MultiParmInstanceNum == B->MultiParmInstanceNum, Header, "MultiParmInstanceNum"); + Result &= TestExpressionError(A->MultiParmInstanceLength == B->MultiParmInstanceLength, Header, "MultiParmInstanceLength"); + Result &= TestExpressionError(A->MultiParmInstanceCount == B->MultiParmInstanceCount, Header, "MultiParmInstanceCount"); + Result &= TestExpressionError(A->InstanceStartOffset == B->InstanceStartOffset, Header, "InstanceStartOffset"); + Result &= TestExpressionError(A->MultiParmInstanceLastModifyArray.Num() == B->MultiParmInstanceLastModifyArray.Num(), Header, "MultiParmInstanceLastModifyArray.Num"); + for (int i = 0; i < FMath::Min(A->MultiParmInstanceLastModifyArray.Num(), B->MultiParmInstanceLastModifyArray.Num()); i++) + { + Result &= TestExpressionError(A->MultiParmInstanceLastModifyArray[i] == B->MultiParmInstanceLastModifyArray[i], Header, "MultiParmInstanceLastModifyArray"); + } + Result &= TestExpressionError(A->DefaultInstanceCount == B->DefaultInstanceCount, Header, "DefaultInstanceCount"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterOperatorPath* A, const UHoudiniParameterOperatorPath* B) +{ + const FString Header = "UHoudiniParameterOperatorPath"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + if (A->HoudiniInput != nullptr && B->HoudiniInput != nullptr) + { + Result &= TestExpressionError(IsEquivalent(A->HoudiniInput.Get(), B->HoudiniInput.Get()), Header, "UHoudiniParameter"); + } + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterRampFloatPoint* A, + const UHoudiniParameterRampFloatPoint* B) +{ + const FString Header = "UHoudiniParameterRampFloatPoint"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(FMath::IsNearlyEqual(A->Position, B->Position, FLOAT_TOLERANCE), Header, "Position"); + Result &= TestExpressionError(FMath::IsNearlyEqual(A->Value, B->Value, FLOAT_TOLERANCE), Header, "Value"); + Result &= TestExpressionError(A->Interpolation == B->Interpolation, Header, "Interpolation"); + Result &= TestExpressionError(A->InstanceIndex == B->InstanceIndex, Header, "InstanceIndex"); + Result &= TestExpressionError(IsEquivalent(A->PositionParentParm, B->PositionParentParm), Header, "PositionParentParm"); + Result &= TestExpressionError(IsEquivalent(A->ValueParentParm, B->ValueParentParm), Header, "ValueParentParm"); + Result &= TestExpressionError(IsEquivalent(A->InterpolationParentParm, B->InterpolationParentParm), Header, "InterpolationParentParm"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterRampColorPoint* A, + const UHoudiniParameterRampColorPoint* B) +{ + const FString Header = "UHoudiniParameterRampColorPoint"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(FMath::IsNearlyEqual(A->Position, B->Position, FLOAT_TOLERANCE), Header, "Position"); + Result &= TestExpressionError(IsEquivalent(A->Value, B->Value), Header, "Value"); + Result &= TestExpressionError(A->Interpolation == B->Interpolation, Header, "Interpolation"); + Result &= TestExpressionError(A->InstanceIndex == B->InstanceIndex, Header, "InstanceIndex"); + Result &= TestExpressionError(IsEquivalent(A->PositionParentParm, B->PositionParentParm), Header, "PositionParentParm"); + Result &= TestExpressionError(IsEquivalent(A->ValueParentParm, B->ValueParentParm), Header, "ValueParentParm"); + Result &= TestExpressionError(IsEquivalent(A->InterpolationParentParm, B->InterpolationParentParm), Header, "InterpolationParentParm"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterRampFloat* A, const UHoudiniParameterRampFloat* B) +{ + const FString Header = "UHoudiniParameterRampFloat"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterRampColor* A, const UHoudiniParameterRampColor* B) +{ + const FString Header = "UHoudiniParameterRampFloat"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->Points.Num() == B->Points.Num(), Header, "Points.Num"); + for (int i = 0; i < FMath::Min(A->Points.Num(), B->Points.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->Points[i], B->Points[i]), Header, "Points"); + } + Result &= TestExpressionError(A->CachedPoints.Num() == B->CachedPoints.Num(), Header, "CachedPoints.Num"); + for (int i = 0; i < FMath::Min(A->CachedPoints.Num(), B->CachedPoints.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->CachedPoints[i], B->CachedPoints[i]), Header, "CachedPoints"); + } + Result &= TestExpressionError(A->DefaultPositions.Num() == B->DefaultPositions.Num(), Header, "DefaultPositions.Num"); + for (int i = 0; i < FMath::Min(A->DefaultPositions.Num(), B->DefaultPositions.Num()); i++) + { + Result &= TestExpressionError(FMath::IsNearlyEqual(A->DefaultPositions[i], B->DefaultPositions[i], FLOAT_TOLERANCE), Header, "DefaultPositions"); + } + Result &= TestExpressionError(A->DefaultValues.Num() == B->DefaultValues.Num(), Header, "DefaultValues.Num"); + for (int i = 0; i < FMath::Min(A->DefaultValues.Num(), B->DefaultValues.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->DefaultValues[i], B->DefaultValues[i]), Header, "DefaultValues"); + } + Result &= TestExpressionError(A->DefaultChoices.Num() == B->DefaultChoices.Num(), Header, "DefaultChoices.Num"); + for (int i = 0; i < FMath::Min(A->DefaultChoices.Num(), B->DefaultChoices.Num()); i++) + { + Result &= TestExpressionError(A->DefaultChoices[i] == B->DefaultChoices[i], Header, "DefaultChoices"); + } + Result &= TestExpressionError(A->NumDefaultPoints == B->NumDefaultPoints, Header, "NumDefaultPoints"); + Result &= TestExpressionError(A->bCaching == B->bCaching, Header, "bCaching"); + + // Skip ModificationEvents + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterSeparator* A, const UHoudiniParameterSeparator* B) +{ + const FString Header = "UHoudiniParameterSeparator"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterString* A, const UHoudiniParameterString* B) +{ + const FString Header = "UHoudiniParameterSeparator"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->Values.Num() == B->Values.Num(), Header, "Values.Num"); + for (int i = 0; i < FMath::Min(A->Values.Num(), B->Values.Num()); i++) + { + Result &= TestExpressionError(A->Values[i].Equals(B->Values[i]), Header, "Values"); + } + Result &= TestExpressionError(A->DefaultValues.Num() == B->DefaultValues.Num(), Header, "DefaultValues.Num"); + for (int i = 0; i < FMath::Min(A->DefaultValues.Num(), B->DefaultValues.Num()); i++) + { + Result &= TestExpressionError(A->DefaultValues[i].Equals(B->DefaultValues[i]), Header, "DefaultValues"); + } + Result &= TestExpressionError(A->ChosenAssets.Num() == B->ChosenAssets.Num(), Header, "ChosenAssets.Num"); + for (int i = 0; i < FMath::Min(A->ChosenAssets.Num(), B->ChosenAssets.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->ChosenAssets[i], B->ChosenAssets[i]), Header, "ChosenAssets"); + } + Result &= TestExpressionError(A->bIsAssetRef == B->bIsAssetRef, Header, "bIsAssetRef"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniParameterToggle* A, const UHoudiniParameterToggle* B) +{ + const FString Header = "UHoudiniParameterToggle"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->Values.Num() == B->Values.Num(), Header, "Values.Num"); + for (int i = 0; i < FMath::Min(A->Values.Num(), B->Values.Num()); i++) + { + Result &= TestExpressionError(A->Values[i] == B->Values[i], Header, "Values"); + } + Result &= TestExpressionError(A->DefaultValues.Num() == B->DefaultValues.Num(), Header, "DefaultValues.Num"); + for (int i = 0; i < FMath::Min(A->DefaultValues.Num(), B->DefaultValues.Num()); i++) + { + Result &= TestExpressionError(A->DefaultValues[i] == B->DefaultValues[i], Header, "DefaultValues"); + } + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniSplineComponent* A, const UHoudiniSplineComponent* B) +{ + const FString Header = "UHoudiniSplineComponent"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->CurvePoints.Num() == B->CurvePoints.Num(), Header, "CurvePoints.Num"); + for (int i = 0; i < FMath::Min(A->CurvePoints.Num(), B->CurvePoints.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->CurvePoints[i], B->CurvePoints[i]), Header, "CurvePoints"); + } + Result &= TestExpressionError(A->DisplayPoints.Num() == B->DisplayPoints.Num(), Header, "DisplayPoints.Num"); + for (int i = 0; i < FMath::Min(A->DisplayPoints.Num(), B->DisplayPoints.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->DisplayPoints[i], B->DisplayPoints[i]), Header, "DisplayPoints"); + } + Result &= TestExpressionError(A->DisplayPointIndexDivider.Num() == B->DisplayPointIndexDivider.Num(), Header, "DisplayPointIndexDivider.Num"); + for (int i = 0; i < FMath::Min(A->DisplayPointIndexDivider.Num(), B->DisplayPointIndexDivider.Num()); i++) + { + Result &= TestExpressionError(A->DisplayPointIndexDivider[i] == B->DisplayPointIndexDivider[i], Header, "DisplayPointIndexDivider"); + } + Result &= TestExpressionError(A->HoudiniSplineName.Equals(B->HoudiniSplineName), Header, "HoudiniSplineName"); + Result &= TestExpressionError(A->bClosed == B->bClosed, Header, "bClosed"); + Result &= TestExpressionError(A->bReversed == B->bReversed, Header, "bReversed"); + Result &= TestExpressionError(A->bIsHoudiniSplineVisible == B->bIsHoudiniSplineVisible, Header, "bIsHoudiniSplineVisible"); + Result &= TestExpressionError(A->CurveType == B->CurveType, Header, "CurveType"); + Result &= TestExpressionError(A->CurveMethod == B->CurveMethod, Header, "CurveMethod"); + Result &= TestExpressionError(A->bIsOutputCurve == B->bIsOutputCurve, Header, "bIsOutputCurve"); + Result &= TestExpressionError(A->bCookOnCurveChanged == B->bCookOnCurveChanged, Header, "bCookOnCurveChanged"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniStaticMesh* A, const UHoudiniStaticMesh* B) +{ + const FString Header = "UHoudiniStaticMesh"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->bHasNormals == B->bHasNormals, Header, "bHasNormals"); + Result &= TestExpressionError(A->bHasTangents == B->bHasTangents, Header, "bHasTangents"); + Result &= TestExpressionError(A->bHasColors == B->bHasColors, Header, "bHasColors"); + Result &= TestExpressionError(A->NumUVLayers == B->NumUVLayers, Header, "NumUVLayers"); + Result &= TestExpressionError(A->bHasPerFaceMaterials == B->bHasPerFaceMaterials, Header, "bHasPerFaceMaterials"); + Result &= TestExpressionError(A->VertexPositions.Num() == B->VertexPositions.Num(), Header, "VertexPositions.Num"); + for (int i = 0; i < FMath::Min(A->VertexPositions.Num(), B->VertexPositions.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->VertexPositions[i], B->VertexPositions[i]), Header, "VertexPositions"); + } + Result &= TestExpressionError(A->TriangleIndices.Num() == B->TriangleIndices.Num(), Header, "TriangleIndices.Num"); + for (int i = 0; i < FMath::Min(A->TriangleIndices.Num(), B->TriangleIndices.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->TriangleIndices[i], B->TriangleIndices[i]), Header, "TriangleIndices"); + } + Result &= TestExpressionError(A->VertexInstanceColors.Num() == B->VertexInstanceColors.Num(), Header, "VertexInstanceColors.Num"); + for (int i = 0; i < FMath::Min(A->VertexInstanceColors.Num(), B->VertexInstanceColors.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->VertexInstanceColors[i], B->VertexInstanceColors[i]), Header, "VertexInstanceColors"); + } + Result &= TestExpressionError(A->VertexInstanceNormals.Num() == B->VertexInstanceNormals.Num(), Header, "VertexInstanceNormals.Num"); + for (int i = 0; i < FMath::Min(A->VertexInstanceNormals.Num(), B->VertexInstanceNormals.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->VertexInstanceNormals[i], B->VertexInstanceNormals[i]), Header, "VertexInstanceNormals"); + } + Result &= TestExpressionError(A->VertexInstanceUTangents.Num() == B->VertexInstanceUTangents.Num(), Header, "VertexInstanceUTangents.Num"); + for (int i = 0; i < FMath::Min(A->VertexInstanceUTangents.Num(), B->VertexInstanceUTangents.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->VertexInstanceUTangents[i], B->VertexInstanceUTangents[i]), Header, "VertexInstanceUTangents"); + } + Result &= TestExpressionError(A->VertexInstanceVTangents.Num() == B->VertexInstanceVTangents.Num(), Header, "VertexInstanceVTangents.Num"); + for (int i = 0; i < FMath::Min(A->VertexInstanceVTangents.Num(), B->VertexInstanceVTangents.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->VertexInstanceVTangents[i], B->VertexInstanceVTangents[i]), Header, "VertexInstanceVTangents"); + } + Result &= TestExpressionError(A->VertexInstanceUVs.Num() == B->VertexInstanceUVs.Num(), Header, "VertexInstanceUVs.Num"); + for (int i = 0; i < FMath::Min(A->VertexInstanceUVs.Num(), B->VertexInstanceUVs.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->VertexInstanceUVs[i], B->VertexInstanceUVs[i]), Header, "VertexInstanceUVs"); + } + Result &= TestExpressionError(A->MaterialIDsPerTriangle.Num() == B->MaterialIDsPerTriangle.Num(), Header, "MaterialIDsPerTriangle.Num"); + for (int i = 0; i < FMath::Min(A->MaterialIDsPerTriangle.Num(), B->MaterialIDsPerTriangle.Num()); i++) + { + Result &= TestExpressionError(A->MaterialIDsPerTriangle[i] == B->MaterialIDsPerTriangle[i], Header, "MaterialIDsPerTriangle"); + } + Result &= TestExpressionError(A->StaticMaterials.Num() == B->StaticMaterials.Num(), Header, "StaticMaterials.Num"); + for (int i = 0; i < FMath::Min(A->StaticMaterials.Num(), B->StaticMaterials.Num()); i++) + { + Result &= TestExpressionError(IsEquivalent(A->StaticMaterials[i], B->StaticMaterials[i]), Header, "StaticMaterials"); + } + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniStaticMeshComponent* A, const UHoudiniStaticMeshComponent* B) +{ + const FString Header = "UHoudiniStaticMeshComponent"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(IsEquivalent(A->Mesh, B->Mesh), Header, "Mesh"); + Result &= TestExpressionError(IsEquivalent(A->LocalBounds, B->LocalBounds), Header, "LocalBounds"); + Result &= TestExpressionError(A->bHoudiniIconVisible == B->bHoudiniIconVisible, Header, "bHoudiniIconVisible"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniLandscapePtr* A, const UHoudiniLandscapePtr* B) +{ + const FString Header = "UHoudiniLandscapePtr"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(IsEquivalent(A->LandscapeSoftPtr.Get(), B->LandscapeSoftPtr.Get()), Header, "LandscapeSoftPtr"); + Result &= TestExpressionError(A->BakeType == B->BakeType, Header, "BakeType"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UHoudiniLandscapeTargetLayerOutput* A, const UHoudiniLandscapeTargetLayerOutput* B) +{ + const FString Header = "UHoudiniLandscapeTargetLayerOutput"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + // TODO: Add here + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UStaticMeshComponent* A, const UStaticMeshComponent* B) +{ + const FString Header = "UStaticMeshComponent"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(IsEquivalent(A->GetStaticMesh(), B->GetStaticMesh()), Header, "StaticMesh"); + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UMaterialInterface* A, const UMaterialInterface* B) +{ + const FString Header = "UMaterialInterface"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + // Result &= TestExpressionError(A->GetHeight() == B->GetHeight(), Header, "Height"); + // Result &= TestExpressionError(A->GetWidth() == B->GetWidth(), Header, "Width"); + + // TODO: Not sure what to test here. + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::IsEquivalent(const UStaticMesh* A, const UStaticMesh* B) +{ + const FString Header = "UStaticMesh"; + + bool Result = true; + + Result &= TestExpressionError((IsValid(A)) == (IsValid(B)), Header, "Null check"); + + if (!IsValid(A) || !IsValid(B)) + { + return true; + } + + Result &= TestExpressionError(A->GetNumLODs() == B->GetNumLODs(), Header, "LODs"); + for (int i = 0; i < FMath::Min(A->GetNumLODs(), B->GetNumLODs()); i++) + { + Result &= TestExpressionError(A->GetNumVertices(i) == B->GetNumVertices(i), Header, "Vertices"); + } + + // TODO: Actually check the vertices + + return Result; +} + +bool FHoudiniEditorEquivalenceUtils::TestExpressionError(const bool Expression, const FString &Header, const FString &Subject) +{ + if (!Expression) + { + + if (TestExpressionErrorEnabled == false) + { + // Andy: Comment out this message, it really spams the logs. + //const FString OutputStr = FString::Printf(TEXT("%s: %s is not equivalent, but may just be a TMap comparison"), *Header, *Subject); + //UE_LOG(LogTemp, Display, TEXT("%s"), *OutputStr); + return false; + } + + const FString OutputStr = FString::Printf(TEXT("%s: %s is not equivalent"), *Header, *Subject); + +#if WITH_DEV_AUTOMATION_TESTS + if (FHoudiniEditorEquivalenceUtils::TestBase != nullptr) + { + FHoudiniEditorEquivalenceUtils::TestBase->AddError(OutputStr); + } + else + { + UE_LOG(LogTemp, Error, TEXT("%s"), *OutputStr); + } +#else + UE_LOG(LogTemp, Error, TEXT("%s"), *OutputStr); +#endif + } + + return Expression; +} + +void FHoudiniEditorEquivalenceUtils::SetTestExpressionError(bool Enabled) +{ + TestExpressionErrorEnabled = Enabled; + if (TestExpressionErrorEnabled) + { + // UE_LOG(LogTemp, Display, TEXT("Enabling test expression errors.")) + } + else + { + // UE_LOG(LogTemp, Display, TEXT("Ignoring test expression errors (Usually we do this for TMaps)")) + } +} + +#if WITH_DEV_AUTOMATION_TESTS +void FHoudiniEditorEquivalenceUtils::SetAutomationBase(FAutomationTestBase* Test) +{ + FHoudiniEditorEquivalenceUtils::TestBase = Test; +} + + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorEquivalenceUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#pragma once + +#include "CoreMinimal.h" +#include "Engine/EngineTypes.h" + +/* + *TBD; + *FHoudiniEditorTestUtils: replace call of utility functions like FHoudiniEditorTestUtils::InstantiateAsset by their public API equivalent. + **/ + +// General +class UHoudiniAsset; +class UHoudiniAssetComponent; +class UHoudiniCookable; +class UHoudiniParameter; +class UHoudiniInput; +class UHoudiniOutput; +class UHoudiniHandleComponent; +class UHoudiniPDGAssetLink; +class UHoudiniInputObject; + +// Runtime objects: +class AHoudiniAssetActor; +class UHoudiniInstancedActorComponent; +class UHoudiniMeshSplitInstancerComponent; +class UHoudiniParameterButton; +class UHoudiniParameterButtonStrip; +class UHoudiniParameterChoice; +class UHoudiniParameterColor; +class UHoudiniParameterFile; +class UHoudiniParameterFloat; +class UHoudiniParameterFolder; +class UHoudiniParameterFolderList; +class UHoudiniParameterInt; +class UHoudiniParameterLabel; +class UHoudiniParameterMultiParm; +class UHoudiniParameterOperatorPath; +class UHoudiniParameterRampFloatPoint; +class UHoudiniParameterRampColorPoint; +class UHoudiniParameterRampFloat; +class UHoudiniParameterRampColor; +class UHoudiniParameterSeparator; +class UHoudiniParameterString; +class UHoudiniParameterToggle; +class UHoudiniSplineComponent; +class UHoudiniStaticMesh; +class UHoudiniStaticMeshComponent; +class UHoudiniLandscapePtr; +class UHoudiniLandscapeTargetLayerOutput; +class UHoudiniLandscapeTargetLayerOutput; +class ALandscapeProxy; +class UMaterialInterface; + +// structs +struct FHoudiniBakedOutput; +struct FHoudiniStaticMeshGenerationProperties; +struct FHoudiniGeoPartObject; +struct FHoudiniInstancedOutput; +struct FHoudiniBakedOutputObject; +struct FHoudiniObjectInfo; +struct FHoudiniGeoInfo; +struct FHoudiniPartInfo; +struct FHoudiniVolumeInfo; +struct FHoudiniCurveInfo; +struct FHoudiniMeshSocket; +struct FHoudiniOutputObject; +struct FHoudiniCurveOutputProperties; +struct FBodyInstance; +struct FStaticMaterial; + +enum class EHoudiniOutputType : uint8; + +class FHoudiniEditorEquivalenceUtils +{ +public: + static bool IsEquivalent(UHoudiniCookable* A, UHoudiniCookable* B); + static bool IsEquivalent(UHoudiniAssetComponent* A, UHoudiniAssetComponent* B); + + static bool IsEquivalent(const UHoudiniAsset* A, const UHoudiniAsset* B); + static bool IsEquivalent(const UHoudiniParameter* A, const UHoudiniParameter* B); + static bool IsEquivalent(const UHoudiniInput* A, const UHoudiniInput* B); + static bool IsEquivalent(const UHoudiniInputObject* A, const UHoudiniInputObject* B); + static bool IsEquivalent(const UHoudiniOutput* A, const UHoudiniOutput* B); + static bool IsEquivalent(const UHoudiniHandleComponent* A, const UHoudiniHandleComponent* B); + static bool IsEquivalent(const UHoudiniPDGAssetLink* A, const UHoudiniPDGAssetLink* B); + + // Struct equivalence + static bool IsEquivalent(const FHoudiniStaticMeshGenerationProperties& A, const FHoudiniStaticMeshGenerationProperties& B); + static bool IsEquivalent(const FHoudiniBakedOutput& A, const FHoudiniBakedOutput& B); + static bool IsEquivalent(const FHoudiniGeoPartObject& A, const FHoudiniGeoPartObject& B); + static bool IsEquivalent(const FHoudiniInstancedOutput& A, const FHoudiniInstancedOutput& B); + static bool IsEquivalent(const FHoudiniBakedOutputObject& A, const FHoudiniBakedOutputObject& B); + static bool IsEquivalent(const FHoudiniObjectInfo& A, const FHoudiniObjectInfo& B); + static bool IsEquivalent(const FHoudiniGeoInfo& A, const FHoudiniGeoInfo& B); + static bool IsEquivalent(const FHoudiniPartInfo& A, const FHoudiniPartInfo& B); + static bool IsEquivalent(const FHoudiniVolumeInfo& A, const FHoudiniVolumeInfo& B); + static bool IsEquivalent(const FHoudiniCurveInfo& A, const FHoudiniCurveInfo& B); + static bool IsEquivalent(const FHoudiniMeshSocket& A, const FHoudiniMeshSocket& B); + static bool IsEquivalent(const FHoudiniOutputObject& A, const FHoudiniOutputObject& B); + static bool IsEquivalent(const FHoudiniCurveOutputProperties& A, const FHoudiniCurveOutputProperties& B); + + // Unreal equivalence + static bool IsEquivalent(const FDirectoryPath& A, const FDirectoryPath& B); + static bool IsEquivalent(const FMeshBuildSettings& A, const FMeshBuildSettings& B); + static bool IsEquivalent(const FTransform& A, const FTransform& B); + static bool IsEquivalent(const FBodyInstance& A, const FBodyInstance& B); + static bool IsEquivalent(const FWalkableSlopeOverride& A, const FWalkableSlopeOverride& B); + static bool IsEquivalent(const FColor& A, const FColor& B); + static bool IsEquivalent(const FLinearColor& A, const FLinearColor& B); + static bool IsEquivalent(const FVector& A, const FVector& B); + static bool IsEquivalent(const FQuat& A, const FQuat& B); + static bool IsEquivalent(const FIntVector& A, const FIntVector& B); + static bool IsEquivalent(const FVector2D& A, const FVector2D& B); + static bool IsEquivalent(const FVector2f& A, const FVector2f& B); + static bool IsEquivalent(const FStaticMaterial& A, const FStaticMaterial& B); + static bool IsEquivalent(const FBox& A, const FBox& B); + + // Runtime UObjects: + static bool IsEquivalent(const UObject * A, const UObject * B); + static bool IsEquivalent(const UPhysicalMaterial * A, const UPhysicalMaterial * B); + static bool IsEquivalent(const AActor * A, const AActor * B); + static bool IsEquivalent(const ALandscapeProxy * A, const ALandscapeProxy * B); + + static bool IsEquivalent(const AHoudiniAssetActor* A, const AHoudiniAssetActor* B); + static bool IsEquivalent(const UHoudiniInstancedActorComponent* A, const UHoudiniInstancedActorComponent* B); + static bool IsEquivalent(const UHoudiniMeshSplitInstancerComponent* A, const UHoudiniMeshSplitInstancerComponent* B); + static bool IsEquivalent(const UHoudiniParameterButton* A, const UHoudiniParameterButton* B); + static bool IsEquivalent(const UHoudiniParameterButtonStrip* A, const UHoudiniParameterButtonStrip* B); + static bool IsEquivalent(const UHoudiniParameterChoice* A, const UHoudiniParameterChoice* B); + static bool IsEquivalent(const UHoudiniParameterColor* A, const UHoudiniParameterColor* B); + static bool IsEquivalent(const UHoudiniParameterFile* A, const UHoudiniParameterFile* B); + static bool IsEquivalent(const UHoudiniParameterFloat* A, const UHoudiniParameterFloat* B); + static bool IsEquivalent(const UHoudiniParameterFolder* A, const UHoudiniParameterFolder* B); + static bool IsEquivalent(const UHoudiniParameterFolderList* A, const UHoudiniParameterFolderList* B); + static bool IsEquivalent(const UHoudiniParameterInt* A, const UHoudiniParameterInt* B); + static bool IsEquivalent(const UHoudiniParameterLabel* A, const UHoudiniParameterLabel* B); + static bool IsEquivalent(const UHoudiniParameterMultiParm* A, const UHoudiniParameterMultiParm* B); + static bool IsEquivalent(const UHoudiniParameterOperatorPath* A, const UHoudiniParameterOperatorPath* B); + static bool IsEquivalent(const UHoudiniParameterRampFloatPoint* A, const UHoudiniParameterRampFloatPoint* B); + static bool IsEquivalent(const UHoudiniParameterRampColorPoint* A, const UHoudiniParameterRampColorPoint* B); + static bool IsEquivalent(const UHoudiniParameterRampFloat* A, const UHoudiniParameterRampFloat* B); + static bool IsEquivalent(const UHoudiniParameterRampColor* A, const UHoudiniParameterRampColor* B); + static bool IsEquivalent(const UHoudiniParameterSeparator* A, const UHoudiniParameterSeparator* B); + static bool IsEquivalent(const UHoudiniParameterString* A, const UHoudiniParameterString* B); + static bool IsEquivalent(const UHoudiniParameterToggle* A, const UHoudiniParameterToggle* B); + static bool IsEquivalent(const UHoudiniSplineComponent* A, const UHoudiniSplineComponent* B); + static bool IsEquivalent(const UHoudiniStaticMesh* A, const UHoudiniStaticMesh* B); + static bool IsEquivalent(const UHoudiniStaticMeshComponent* A, const UHoudiniStaticMeshComponent* B); + static bool IsEquivalent(const UHoudiniLandscapePtr* A, const UHoudiniLandscapePtr* B); + static bool IsEquivalent(const UHoudiniLandscapeTargetLayerOutput* A, const UHoudiniLandscapeTargetLayerOutput* B); + static bool IsEquivalent(const UStaticMeshComponent* A, const UStaticMeshComponent* B); + static bool IsEquivalent(const UMaterialInterface* A, const UMaterialInterface* B); + static bool IsEquivalent(const UStaticMesh* A, const UStaticMesh* B); + +private: +#if WITH_DEV_AUTOMATION_TESTS + static void SetAutomationBase(FAutomationTestBase* Test); + static FAutomationTestBase* TestBase; +#endif + + static bool TestExpressionErrorEnabled; + + static bool TestExpressionError(const bool Expression, const FString& Header, const FString& Subject); + + static void SetTestExpressionError(bool Enabled); +}; + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorHeightfieldTests.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorHeightfieldTests.h" + +#include "Runtime/Launch/Resources/Version.h" + +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" + +FString FHoudiniEditorHeightfieldTests::EquivalenceTestMapName = TEXT("Heightfields"); +FString FHoudiniEditorHeightfieldTests::TestHDAPath = TEXT("/Game/TestHDAs/Heightfields/"); + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorHeightfieldTest_Heightfields_Common, "Houdini.Editor.Heightfields.Heightfield_Common", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorHeightfieldTest_Heightfields_Common::RunTest(const FString & Parameters) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorHeightfieldTests::EquivalenceTestMapName; + const FString ActorName = TEXT("Heightfield_Common"); + const FString HDAAssetPath = FHoudiniEditorHeightfieldTests::TestHDAPath + TEXT("Heightfield_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); +#endif + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorHeightfieldTest_hf_tile_scatter_instance, "Houdini.Editor.Heightfields.hf_tile_scatter_instance", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorHeightfieldTest_hf_tile_scatter_instance::RunTest(const FString & Parameters) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorHeightfieldTests::EquivalenceTestMapName; + const FString ActorName = TEXT("hf_tile_scatter_instance"); + const FString HDAAssetPath = FHoudiniEditorHeightfieldTests::TestHDAPath + TEXT("hf_tile_scatter_instance"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); +#endif + return true; +} + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorHeightfieldTests.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +// Class just for containing static member variables +class FHoudiniEditorHeightfieldTests +{ +public: + static FString EquivalenceTestMapName; + static FString TestHDAPath; +}; + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorInputTests.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorInputTests.h" + +#include "HoudiniPublicAPIAssetWrapper.h" +#include "HoudiniPublicAPIInputTypes.h" +#include "HoudiniAssetActor.h" + +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" + +FString FHoudiniEditorInputTests::EquivalenceTestMapName = TEXT("Inputs"); +FString FHoudiniEditorInputTests::TestHDAPath = TEXT("/Game/TestHDAs/Inputs/"); +/* +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInputTest_Mesh_Input, "Houdini.Editor.Inputs.Mesh_Input", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorInputTest_Mesh_Input::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorInputTests::EquivalenceTestMapName; + const FString ActorName = TEXT("Mesh_Input"); + const FString HDAAssetPath = FHoudiniEditorInputTests::TestHDAPath + TEXT("InputAsOutput"); + const FHoudiniActorTestSettings Settings = {}; + + // Suppress temp file warning + this->AddSupressedWarning("failed to load '/Game/HoudiniEngine/Temp/plain_cube"); + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + const FString InputHDAPath = FHoudiniEditorInputTests::TestHDAPath + TEXT("Helpers/plain_cube"); + const FString InputActorName = TEXT("Mesh_Input_input"); + + if (bSaveAssets) + FHoudiniEditorTestUtils::DeleteHACWithNameInLevelAndSave(InputActorName); + + FHoudiniEditorTestUtils::InstantiateAsset(this, FName(InputHDAPath), + [this, ContinueCallback, InAssetWrapper, InputActorName, bSaveAssets, DefaultCookFolder] (UHoudiniPublicAPIAssetWrapper* InInputAssetWrapper, const bool Success) + { + if (!Success) + { + this->AddError("Instantiation failed!"); + ContinueCallback(false); + + return; + } + + InInputAssetWrapper->GetHoudiniAssetActor()->SetActorLabel(InputActorName); + + // Sets the input to the curve + const TSubclassOf APIInputClass = UHoudiniPublicAPIWorldInput::StaticClass(); + + UHoudiniPublicAPIInput* AssetInput = InAssetWrapper->CreateEmptyInput(APIInputClass); + AssetInput->SetInputObjects({InInputAssetWrapper}); + InAssetWrapper->SetInputAtIndex(0, AssetInput); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, DefaultCookFolder + ); + }); + } + ); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInputTest_Heightfield_Input, "Houdini.Editor.Inputs.Heightfield_Input", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorInputTest_Heightfield_Input::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorInputTests::EquivalenceTestMapName; + const FString ActorName = TEXT("Heightfield_Input"); + const FString HDAAssetPath = FHoudiniEditorInputTests::TestHDAPath + TEXT("InputAsOutput"); + const FHoudiniActorTestSettings Settings = {}; + + // Suppress temp file warning + this->AddSupressedWarning("failed to load '/Game/HoudiniEngine/Temp/plain_cube"); + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + const FString InputHDAPath = FHoudiniEditorInputTests::TestHDAPath + TEXT("Helpers/plain_heightfield"); + const FString InputActorName = TEXT("Heightfield_Input_input"); + + if (bSaveAssets) + FHoudiniEditorTestUtils::DeleteHACWithNameInLevelAndSave(InputActorName); + + FHoudiniEditorTestUtils::InstantiateAsset(this, FName(InputHDAPath), + [this, ContinueCallback, InputActorName, InAssetWrapper, bSaveAssets, DefaultCookFolder] (UHoudiniPublicAPIAssetWrapper* InInputAssetWrapper, const bool Success) + { + if (!Success) + { + this->AddError("Instantiation failed!"); + ContinueCallback(false); + + return; + } + + InInputAssetWrapper->GetHoudiniAssetActor()->SetActorLabel(InputActorName); + + // Sets the input to the curve + const TSubclassOf APIInputClass = UHoudiniPublicAPIWorldInput::StaticClass(); + + UHoudiniPublicAPIInput* AssetInput = InAssetWrapper->CreateEmptyInput(APIInputClass); + AssetInput->SetInputObjects({InInputAssetWrapper}); + InAssetWrapper->SetInputAtIndex(0, AssetInput); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [=](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, DefaultCookFolder + ); + }); + } + ); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInputTest_Mesh_Geo_Input, "Houdini.Editor.Inputs.Mesh_Geo_Input", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorInputTest_Mesh_Geo_Input::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorInputTests::EquivalenceTestMapName; + const FString ActorName = TEXT("Mesh_Geo_Input"); + const FString HDAAssetPath = FHoudiniEditorInputTests::TestHDAPath + TEXT("InputAsOutput"); + const FHoudiniActorTestSettings Settings = {}; + + // Suppress temp file warning + this->AddSupressedWarning("failed to load '/Game/HoudiniEngine/Temp/plain_cube"); + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + + // const FString AssetName = "Blueprint'/Game/TestObjects/BP_Cube.BP_Cube'"; // BUGGED - Please copy/paste this test and remove this line when fixed. + const FString AssetName = "Blueprint'/Game/TestObjects/SM_Cube.SM_Cube'"; + UObject * InputObject = StaticLoadObject(UObject::StaticClass(), nullptr, *AssetName, nullptr, LOAD_None, nullptr); + + const TSubclassOf APIInputClass = UHoudiniPublicAPIGeoInput::StaticClass(); + + UHoudiniPublicAPIInput* AssetInput = InAssetWrapper->CreateEmptyInput(APIInputClass); + AssetInput->SetInputObjects({InputObject}); + InAssetWrapper->SetInputAtIndex(0, AssetInput); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, DefaultCookFolder + ); + } + ); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInputTest_Curve_Input, "Houdini.Editor.Inputs.Curve_Input", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorInputTest_Curve_Input::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorInputTests::EquivalenceTestMapName; + const FString ActorName = TEXT("Curve_Input"); + const FString HDAAssetPath = FHoudiniEditorInputTests::TestHDAPath + TEXT("simple_curve_input"); + const FHoudiniActorTestSettings Settings = {}; + + // Suppress temp file warning + this->AddSupressedWarning("failed to load '/Game/HoudiniEngine/Temp/plain_cube"); + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + const FString CurveHDAPath = FHoudiniEditorInputTests::TestHDAPath + TEXT("Helpers/plain_editable_curve"); + const FString CurveActorName = TEXT("curve_input_hda"); + + if (bSaveAssets) + FHoudiniEditorTestUtils::DeleteHACWithNameInLevelAndSave(CurveActorName); + + FHoudiniEditorTestUtils::InstantiateAsset(this, FName(CurveHDAPath), + [this, CurveActorName, InAssetWrapper, bSaveAssets, ContinueCallback, DefaultCookFolder] (UHoudiniPublicAPIAssetWrapper* InAssetWrapperCurve, const bool Success) + { + if (!Success) + { + this->AddError("Instantiation failed!"); + ContinueCallback(false); + + return; + } + + InAssetWrapperCurve->GetHoudiniAssetActor()->SetActorLabel(CurveActorName); + + // Sets the input to the curve + const TSubclassOf APIInputClass = UHoudiniPublicAPICurveInput::StaticClass(); + + UHoudiniPublicAPIInput* AssetInput = InAssetWrapper->CreateEmptyInput(APIInputClass); + AssetInput->SetInputObjects({InAssetWrapperCurve}); + InAssetWrapper->SetInputAtIndex(0, AssetInput); + + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, DefaultCookFolder + ); + }); + } + ); + }); + + return true; +} +*/ + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorInputTests.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +// Class just for containing static member variables +class FHoudiniEditorInputTests +{ +public: + static FString EquivalenceTestMapName; + static FString TestHDAPath; +}; + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorInstancerTests.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorInstancerTests.h" + +#include "HoudiniEditorEquivalenceUtils.h" +#include "HoudiniPublicAPIAssetWrapper.h" + +#include "FileHelpers.h" + +#if WITH_DEV_AUTOMATION_TESTS + +#include "HoudiniEditorTestUtils.h" +#include "HoudiniAsset.h" +#include "HoudiniAssetComponent.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "HAL/FileManager.h" +#else + #include "Core/Public/HAL/FileManager.h" +#endif +#include "Misc/AutomationTest.h" + +FString FHoudiniEditorInstancerTests::EquivalenceTestMapName = TEXT("Instancer"); +FString FHoudiniEditorInstancerTests::TestHDAPath = TEXT("/Game/TestHDAs/Instancer/"); + + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerPackedPrimsOBJ, "Houdini.Editor.Instancer.PackedPrimitivesOBJ", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerPackedPrimsOBJ::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("PackedPrimitives_OBJ"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("packedpriminstancers_obj"); + const FHoudiniActorTestSettings Settings = {}; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); + + return true; +} + + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerAttributeInstancerOBJ, "Houdini.Editor.Instancer.AttributeInstancerOBJ", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerAttributeInstancerOBJ::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("AttributeInstancer_OBJ"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("attributeinstancers_obj"); + const FHoudiniActorTestSettings Settings = {}; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); + + return true; +} + + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerSplittingOBJ, "Houdini.Editor.Instancer.SplittingOBJ", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerSplittingOBJ::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerSplitting_OBJ"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_splitting_obj"); + const FHoudiniActorTestSettings Settings = {}; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); + + return true; +} + + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerTypesOBJ, "Houdini.Editor.Instancer.TypesOBJ", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerTypesOBJ::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerTypes_OBJ"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_types_obj"); + const FHoudiniActorTestSettings Settings = {}; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); + + return true; +} + + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerCustomDataOBJ, "Houdini.Editor.Instancer.CustomDataOBJ", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerCustomDataOBJ::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerCustomData_OBJ"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_customdata_obj"); + const FHoudiniActorTestSettings Settings = {}; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); + + return true; +} + + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerPackedPrimsSOPOneInstancer, "Houdini.Editor.Instancer.PackedPrimitivesSOP.OneInstancer", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerPackedPrimsSOPOneInstancer::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("PackedPrimitives_SOP_OneInstancer"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("packedprimitives_sop"); + const FHoudiniActorTestSettings Settings = {}; + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 0; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this,InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerPackedPrimsSOPMultipleInstancer, "Houdini.Editor.Instancer.PackedPrimitivesSOP.MultipleInstancer", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerPackedPrimsSOPMultipleInstancer::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("PackedPrimitives_SOP_MultipleInstancer"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("packedprimitives_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 1; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerPackedPrimsSOPSingleInstance, "Houdini.Editor.Instancer.PackedPrimitivesSOP.SingleInstance", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerPackedPrimsSOPSingleInstance::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("PackedPrimitives_SOP_SingleInstance"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("packedprimitives_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 2; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerPackedPrimsSOPMultipleSingleInstance, "Houdini.Editor.Instancer.PackedPrimitivesSOP.MultipleSingleInstance", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerPackedPrimsSOPMultipleSingleInstance::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("PackedPrimitives_SOP_MultipleSingleInstance"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("packedprimitives_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 2; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerAttributeInstancerSOPOneInstancer, "Houdini.Editor.Instancer.AttributeInstancerSOP.OneInstancer", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerAttributeInstancerSOPOneInstancer::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("AttributeInstancer_SOP_OneInstancer"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("attribinstancer_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 0; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerAttributeInstancerSOPMultipleInstancer, "Houdini.Editor.Instancer.AttributeInstancerSOP.MultipleInstancer", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerAttributeInstancerSOPMultipleInstancer::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("AttributeInstancer_SOP_MultipleInstancer"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("attribinstancer_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 1; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerAttributeInstancerSOPSingleInstance, "Houdini.Editor.Instancer.AttributeInstancerSOP.SingleInstance", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerAttributeInstancerSOPSingleInstance::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("AttributeInstancer_SOP_SingleInstance"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("attribinstancer_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 2; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerAttributeInstancerSOPMultipleSingleInstance, "Houdini.Editor.Instancer.AttributeInstancerSOP.MultipleSingleInstance", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerAttributeInstancerSOPMultipleSingleInstance::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("AttributeInstancer_SOP_MultipleSingleInstance"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("attribinstancer_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 2; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerSplittingSOPByColorPacked, "Houdini.Editor.Instancer.SplittingSOP.ByColorPacked", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerSplittingSOPByColorPacked::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerSplitting_SOP_ByColorPacked"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_splitting_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 0; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerSplittingSOPByColorAttrInstancer, "Houdini.Editor.Instancer.SplittingSOP.ByColorAttrInstancer", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerSplittingSOPByColorAttrInstancer::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerSplitting_SOP_ByColorAttrInstancer"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_splitting_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 1; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerSplittingSOPByStringPacked, "Houdini.Editor.Instancer.SplittingSOP.ByStringPacked", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerSplittingSOPByStringPacked::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerSplitting_SOP_ByStringPacked"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_splitting_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 2; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerSplittingSOPByStringAttrInstancer, "Houdini.Editor.Instancer.SplittingSOP.ByStringAttrInstancer", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerSplittingSOPByStringAttrInstancer::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerSplitting_SOP_ByStringAttrInstancer"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_splitting_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 3; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerSplittingSOPByMatPacked, "Houdini.Editor.Instancer.SplittingSOP.ByMatPacked", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerSplittingSOPByMatPacked::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerSplitting_SOP_ByMatPacked"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_splitting_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 4; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerSplittingSOPByMatAttrInstancer, "Houdini.Editor.Instancer.SplittingSOP.ByMatAttrInstancer", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerSplittingSOPByMatAttrInstancer::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerSplitting_SOP_ByMatAttrInstancer"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_splitting_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 5; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerTypesSOPISMC, "Houdini.Editor.Instancer.TypesSOP.ISMC", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerTypesSOPISMC::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerTypes_SOP_ISMC"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_types_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 0; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerTypesSOPHISMC, "Houdini.Editor.Instancer.TypesSOP.HISMC", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerTypesSOPHISMC::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerTypes_SOP_HISMC"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_types_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 1; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerTypesSOPHISMCForced, "Houdini.Editor.Instancer.TypesSOP.HISMCForced", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerTypesSOPHISMCForced::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerTypes_SOP_HISMCForced"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_types_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 2; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerTypesSOPHISMCForcedOff, "Houdini.Editor.Instancer.TypesSOP.HISMCForcedOff", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerTypesSOPHISMCForcedOff::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerTypes_SOP_HISMCForcedOff"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_types_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 3; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerTypesSOPMSIC, "Houdini.Editor.Instancer.TypesSOP.MSIC", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerTypesSOPMSIC::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerTypes_SOP_MSIC"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_types_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 4; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerTypesSOPIAC, "Houdini.Editor.Instancer.TypesSOP.IAC", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerTypesSOPIAC::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerTypes_SOP_IAC"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_types_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 5; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerTypesSOPAttribClass, "Houdini.Editor.Instancer.TypesSOP.AttribClass", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerTypesSOPAttribClass::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerTypes_SOP_AttribClass"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_types_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 6; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerTypesSOPFoliage, "Houdini.Editor.Instancer.TypesSOP.Foliage", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerTypesSOPFoliage::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerTypes_SOP_Foliage"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_types_sop"); + const FHoudiniActorTestSettings Settings = {}; + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 7; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerTypesSOPDecals, "Houdini.Editor.Instancer.TypesSOP.Decals", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerTypesSOPDecals::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerTypes_SOP_Decals"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_types_sop"); + const FHoudiniActorTestSettings Settings = {}; + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 8; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerTypesSOPInvalid, "Houdini.Editor.Instancer.TypesSOP.Invalid", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerTypesSOPInvalid::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerTypes_SOP_Invalid"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_types_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 9; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerCustomDataSOPNumberSuite, "Houdini.Editor.Instancer.CustomDataSOP.NumberSuite", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerCustomDataSOPNumberSuite::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerCustomData_SOP_NumberSuite"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_customdata_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 0; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerCustomDataSOPColors, "Houdini.Editor.Instancer.CustomDataSOP.Colors", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerCustomDataSOPColors::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerCustomData_SOP_Colors"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_customdata_sop"); + const FHoudiniActorTestSettings Settings = {}; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + int32 ParamValue = 1; + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait(this, InAssetWrapper, + [this, bSaveAssets, ContinueCallback](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, Settings, + nullptr, nullptr, SetParamsOnPostInstantiate); + }); + + return true; +} + + + +/* +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerPackedPrimsSOP, "Houdini.Editor.Instancer.PackedPrimitivesSOP", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerPackedPrimsSOP::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("PackedPrimitives_SOP"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("packedprimitives_sop"); + + // Param Value, we need to test 0 - 3 + int32 ParamValue = 0; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait( + this, + InAssetWrapper, + [this](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 1; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 2; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 3; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + nullptr, nullptr, SetParamsOnPostInstantiate); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }); + + return true; +} + + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerAttributeInstancerSOP, "Houdini.Editor.Instancer.AttributeInstancerSOP", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerAttributeInstancerSOP::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("AttributeInstancer_SOP"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("attribinstancer_sop"); + + // Param Value, we need to test 0 - 3 + int32 ParamValue = 0; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait( + this, + InAssetWrapper, + [this](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 1; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 2; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 3; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + nullptr, nullptr, SetParamsOnPostInstantiate); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerSplittingSOP, "Houdini.Editor.Instancer.SplittingSOP", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerSplittingSOP::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerSplitting_SOP"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_splitting_sop"); + + // Param Value, we need to test 0 - 5 + int32 ParamValue = 0; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait( + this, + InAssetWrapper, + [this](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 1; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 2; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 3; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 4; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 5; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + nullptr, nullptr, SetParamsOnPostInstantiate); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }); + + return true; +} + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerTypesSOP, "Houdini.Editor.Instancer.TypesSOP", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerTypesSOP::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerTypes_SOP"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_types_sop"); + + // Param Value, we need to test 0 - 9 + int32 ParamValue = 0; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait( + this, + InAssetWrapper, + [this](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 1; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 2; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 3; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 4; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 5; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 6; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 7; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 8; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 9; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + nullptr, nullptr, SetParamsOnPostInstantiate); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }, + nullptr, SetParamsOnPostInstantiate + ); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorInstancerCustomDataSOP, "Houdini.Editor.Instancer.CustomDataSOP", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) +bool HoudiniEditorInstancerCustomDataSOP::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("InstancerCustomData_SOP"); + + const FString MapName = FHoudiniEditorInstancerTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorInstancerTests::TestHDAPath + TEXT("instancer_customdata_sop"); + + // Param Value, we need to test 0 - 1 + int32 ParamValue = 0; + + // Lambda that sets the "input" parameter after instantiation, then recooks before proceeding with the test. + auto SetParamsOnPostInstantiate = [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, TFunction ContinueCallback, const bool bSaveAssets, const FString DefaultCookFolder) + { + FString ParamName = TEXT("input"); + InAssetWrapper->SetIntParameterValue(FName(ParamName), ParamValue, 0); + + FHoudiniEditorTestUtils::RecookAndWait( + this, + InAssetWrapper, + [this](UHoudiniPublicAPIAssetWrapper* _, const bool __) + { + if (bSaveAssets) + FHoudiniEditorTestUtils::SaveCurrentLevel(); + + ContinueCallback(true); + }, + DefaultCookFolder + ); + }; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + [=, &ParamValue](bool IsSuccessful) + { + ParamValue = 1; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + this, MapName, HDAAssetPath, ActorName, + nullptr, nullptr, SetParamsOnPostInstantiate); + }, + nullptr, SetParamsOnPostInstantiate + ); + }); + + return true; +} +*/ + +#endif + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorInstancerTests.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +// Class just for containing static member variables +class FHoudiniEditorInstancerTests +{ +public: + static FString EquivalenceTestMapName; + static FString TestHDAPath; + +}; +#endif + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorMaterialTests.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorMaterialTests.h" + +#if WITH_DEV_AUTOMATION_TESTS + +#include "HoudiniCookable.h" +#include "HoudiniEditorTestUtils.h" +#include "HoudiniEditorUnitTestUtils.h" + +#include "Engine/EngineTypes.h" +#include "Materials/Material.h" +#include "Materials/MaterialExpressionMultiply.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Misc/AutomationTest.h" +#include "TextureResource.h" + +FString FHoudiniEditorMaterialTests::EquivalenceTestMapName = TEXT("Materials"); +FString FHoudiniEditorMaterialTests::TestHDAPath = TEXT("/Game/TestHDAs/Materials/"); + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorMaterialTest_Material_Simple, "Houdini.Editor.Materials.Material_Simple", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorMaterialTest_Material_Simple::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorMaterialTests::EquivalenceTestMapName; + const FString ActorName = TEXT("Material_Simple"); + const FString HDAAssetPath = FHoudiniEditorMaterialTests::TestHDAPath + TEXT("Material_Simple"); + const FHoudiniActorTestSettings Settings = {}; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorMaterialTest_Material_Maps, "Houdini.Editor.Materials.Material_Maps", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorMaterialTest_Material_Maps::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorMaterialTests::EquivalenceTestMapName; + const FString ActorName = TEXT("Material_Maps"); + const FString HDAAssetPath = FHoudiniEditorMaterialTests::TestHDAPath + TEXT("Material_Maps"); + const FHoudiniActorTestSettings Settings = {}; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorMaterialTest_MaterialAttributes_Common, "Houdini.Editor.Materials.MaterialAttributes_Common", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorMaterialTest_MaterialAttributes_Common::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorMaterialTests::EquivalenceTestMapName; + const FString ActorName = TEXT("MaterialAttributes_Common"); + const FString HDAAssetPath = FHoudiniEditorMaterialTests::TestHDAPath + TEXT("MaterialAttributes_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorMaterialTest_Material_Textures, "Houdini.Editor.Materials.Material_Textures", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorMaterialTest_Material_Textures::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorMaterialTests::TestHDAPath + TEXT("Material_Textures"), FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + // Start cooking the HDA. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingHDA(); + return true; + })); + + // Check that the material has been created correctly. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + // Lambda which opens a texture expression's raw texture data and checks the values of some pixels. + auto CheckPixelTestCases = [this](UMaterialExpression* Expression, TArray PixelTestCases) + { + // Grab the texture from the texture expression. + UMaterialExpressionTextureSampleParameter2D* TextureExpression = Cast(Expression); + HOUDINI_TEST_NOT_NULL_ON_FAIL(TextureExpression, return true); + UTexture2D* Texture = Cast(TextureExpression->Texture); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Texture, return true); + + // The texture needs these settings, otherwise RawData->Lock() will fail. + Texture->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap; + Texture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps; + Texture->SRGB = false; + Texture->UpdateResource(); + + // Get the texture's first mipmap (i.e. the full resolution texture). + HOUDINI_TEST_NOT_EQUAL_ON_FAIL(Texture->GetNumMips(), 0, return true); + FTexture2DMipMap* MipMap = &Texture->GetPlatformData()->Mips[0]; + HOUDINI_TEST_NOT_NULL_ON_FAIL(MipMap, return true); + + // Get a pointer to the mipmap's raw data. + FByteBulkData* RawData = &MipMap->BulkData; + HOUDINI_TEST_NOT_NULL_ON_FAIL(RawData, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(RawData->IsUnlocked(), true, return true); + FColor* FormattedImageData = static_cast( RawData->Lock( LOCK_READ_ONLY ) ); + + if (!FormattedImageData) + { + // Unlocks the raw data if the image data wasn't casted successfully. + if (RawData->IsLocked()) + RawData->Unlock(); + // FormattedImageData is nullptr here; this macro fails on purpose. + HOUDINI_TEST_NOT_NULL_ON_FAIL(FormattedImageData, return true); + } + + // Check that the test cases pass. + bool AllCorrect = true; + for (auto& PixelTestCase : PixelTestCases) + { + int32 Index = PixelTestCase.y * MipMap->SizeX + PixelTestCase.x; + if (FormattedImageData) + { + HOUDINI_TEST_EQUAL_ON_FAIL(FormattedImageData[Index].R, PixelTestCase.r, AllCorrect = false); + HOUDINI_TEST_EQUAL_ON_FAIL(FormattedImageData[Index].G, PixelTestCase.g, AllCorrect = false); + HOUDINI_TEST_EQUAL_ON_FAIL(FormattedImageData[Index].B, PixelTestCase.b, AllCorrect = false); + HOUDINI_TEST_EQUAL_ON_FAIL(FormattedImageData[Index].A, PixelTestCase.a, AllCorrect = false); + } + } + + RawData->Unlock(); + + return AllCorrect; + }; + + // We should have one output. + TArray Outputs; + Context->GetOutputs(Outputs); + + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + + // And the one output should have a static mesh. + TArray StaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshOutputs.Num(), 1, return true); + UStaticMeshComponent* Mesh = StaticMeshOutputs[0]; + + // And that static meshs hould have one material. + HOUDINI_TEST_EQUAL_ON_FAIL(Mesh->GetNumMaterials(), 1, return true); + UMaterial* Material = Cast(Mesh->GetMaterial(0)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Material, return true); + + // Grab the relevant expressions attached to the material. +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + UMaterialEditorOnlyData* MaterialEditorOnly = Material->GetEditorOnlyData(); + FColorMaterialInput& MatInputDiffuse = MaterialEditorOnly->BaseColor; + FScalarMaterialInput& MatInputMetallic = MaterialEditorOnly->Metallic; + FScalarMaterialInput& MatInputSpecular = MaterialEditorOnly->Specular; + FScalarMaterialInput& MatInputRoughness = MaterialEditorOnly->Roughness; + FColorMaterialInput& MatInputEmissive = MaterialEditorOnly->EmissiveColor; + FScalarMaterialInput& MatInputOpacity = MaterialEditorOnly->Opacity; + FVectorMaterialInput& MatInputNormal = MaterialEditorOnly->Normal; +#else + FColorMaterialInput& MatInputDiffuse = Material->BaseColor; + FScalarMaterialInput& MatInputMetallic = Material->Metallic; + FScalarMaterialInput& MatInputSpecular = Material->Specular; + FScalarMaterialInput& MatInputRoughness = Material->Roughness; + FColorMaterialInput& MatInputEmissive = Material->EmissiveColor; + FScalarMaterialInput& MatInputOpacity = Material->Opacity; + FVectorMaterialInput& MatInputNormal = Material->Normal; +#endif + + // Check that every expression exists. + HOUDINI_TEST_NOT_NULL_ON_FAIL(MatInputDiffuse.Expression, return true); + HOUDINI_TEST_NOT_NULL_ON_FAIL(MatInputMetallic.Expression, return true); + HOUDINI_TEST_NOT_NULL_ON_FAIL(MatInputSpecular.Expression, return true); + HOUDINI_TEST_NOT_NULL_ON_FAIL(MatInputRoughness.Expression, return true); + HOUDINI_TEST_NOT_NULL_ON_FAIL(MatInputEmissive.Expression, return true); + HOUDINI_TEST_NOT_NULL_ON_FAIL(MatInputOpacity.Expression, return true); + HOUDINI_TEST_NOT_NULL_ON_FAIL(MatInputNormal.Expression, return true); + + // Find the multiply expression attached to the diffuse texture, and the constant diffuse expression. + UMaterialExpressionMultiply* FirstMultiplyExpressionDiffuse = Cast(MatInputDiffuse.Expression); + HOUDINI_TEST_NOT_NULL_ON_FAIL(FirstMultiplyExpressionDiffuse, return true); + UMaterialExpressionMultiply* SecondMultiplyExpressionDiffuse = Cast(FirstMultiplyExpressionDiffuse->A.Expression); + HOUDINI_TEST_NOT_NULL_ON_FAIL(SecondMultiplyExpressionDiffuse, return true); + UMaterialExpressionVectorParameter* ConstantDiffuse = Cast(SecondMultiplyExpressionDiffuse->A.Expression); + HOUDINI_TEST_NOT_NULL_ON_FAIL(ConstantDiffuse, return true); + + // Find the constant roughness expression. + UMaterialExpressionScalarParameter* ConstantRoughness = Cast(MatInputRoughness.Expression); + HOUDINI_TEST_NOT_NULL_ON_FAIL(ConstantRoughness, return true); + + // Find the multiply expression attached to the emissive texture. + UMaterialExpressionMultiply* FirstMultiplyExpressionEmissive = Cast(MatInputEmissive.Expression); + HOUDINI_TEST_NOT_NULL_ON_FAIL(FirstMultiplyExpressionEmissive, return true); + + // Find the constant opacity expression. + UMaterialExpressionMultiply* MultiplyExpressionOpacity = Cast(MatInputOpacity.Expression); + HOUDINI_TEST_NOT_NULL_ON_FAIL(MultiplyExpressionOpacity, return true); + UMaterialExpressionScalarParameter* ConstantOpacity = Cast(MultiplyExpressionOpacity->B.Expression); + HOUDINI_TEST_NOT_NULL_ON_FAIL(ConstantOpacity, return true); + + // Check the constant diffuse value. + // Note that the values on the HDA are equal to 0.333, not 0.333... repeating. + const float Tolerance = 0.00001f; + HOUDINI_TEST_EQUALISH(ConstantDiffuse->DefaultValue.R, 0.333f, Tolerance); + HOUDINI_TEST_EQUALISH(ConstantDiffuse->DefaultValue.G, 0.333f, Tolerance); + HOUDINI_TEST_EQUALISH(ConstantDiffuse->DefaultValue.B, 0.333f, Tolerance); + HOUDINI_TEST_EQUALISH(ConstantDiffuse->DefaultValue.A, 1.0f, Tolerance); + + // Diffuse is the butterfly image pulled using a File COP. + TArray DiffuseTestCases = { + {0, 0, 0, 0, 0, 0}, + {65, 85, 85, 89, 68, 213}, + {81, 109, 87, 72, 59, 255}, + {388, 378, 216, 92, 92, 255}, + {468, 425, 72, 76, 56, 207} + }; + HOUDINI_TEST_EQUAL(CheckPixelTestCases(FirstMultiplyExpressionDiffuse->B.Expression, DiffuseTestCases), true); + + // Metallic is the butterfly image pulled directly from file. + TArray MetallicTestCases = { + {0, 0, 0, 0, 0, 255}, + {65, 85, 85, 89, 66, 255}, + {81, 109, 85, 70, 56, 255}, + {388, 378, 217, 90, 92, 255}, + {468, 425, 70, 74, 53, 255} + }; + HOUDINI_TEST_EQUAL(CheckPixelTestCases(MatInputMetallic.Expression, MetallicTestCases), true); + + // Specular is the default Worley Noise COP. + TArray SpecularTestCases = { + {0, 0, 115, 115, 115, 255}, + {912, 194, 28, 28, 28, 255}, + {483, 540, 221, 221, 221, 255}, + {657, 826, 114, 114, 114, 255}, + {1023, 1023, 118, 118, 118, 255} + }; + HOUDINI_TEST_EQUAL(CheckPixelTestCases(MatInputSpecular.Expression, SpecularTestCases), true); + + // Check the constant roughness value. + HOUDINI_TEST_EQUALISH(ConstantRoughness->DefaultValue, 0.5f, Tolerance); + + // Emissive is a red to black Ramp COP. + TArray EmissiveTestCases = { + {0, 0, 255, 0, 0, 255}, + {221, 788, 228, 0, 0, 255}, + {541, 301, 181, 0, 0, 255}, + {956, 744, 74, 0, 0, 255}, + {1023, 1023, 0, 0, 0, 255} + }; + HOUDINI_TEST_EQUAL(CheckPixelTestCases(FirstMultiplyExpressionEmissive->B.Expression, EmissiveTestCases), true); + + // Check the constant opacity value. + HOUDINI_TEST_EQUALISH(ConstantOpacity->DefaultValue, 1.0f, Tolerance); + + // Normal is the butterfly image pulled directly from file. + TArray NormalTestCases = { + {0, 0, 0, 0, 0, 255}, + {65, 85, 85, 89, 66, 255}, + {81, 109, 85, 70, 56, 255}, + {388, 378, 217, 90, 92, 255}, + {468, 425, 70, 74, 53, 255} + }; + HOUDINI_TEST_EQUAL(CheckPixelTestCases(MatInputNormal.Expression, NormalTestCases), true); + + return true; + })); + + return true; +} + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorMaterialTests.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +class FHoudiniEditorMaterialTests +{ +public: + struct FTexturePixelTestCase + { + uint32 x; + uint32 y; + uint8 r; + uint8 g; + uint8 b; + uint8 a; + }; + static FString EquivalenceTestMapName; + static FString TestHDAPath; +}; + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorMeshTests.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorMeshTests.h" + +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" + +FString FHoudiniEditorMeshTests::EquivalenceTestMapName = TEXT("Mesh"); +FString FHoudiniEditorMeshTests::TestHDAPath = TEXT("/Game/TestHDAs/Mesh/"); + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorMeshTest_Single, "Houdini.Editor.Mesh.Single", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorMeshTest_Single::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorMeshTests::EquivalenceTestMapName; + const FString ActorName = TEXT("Single"); + const FString HDAAssetPath = FHoudiniEditorMeshTests::TestHDAPath + TEXT("Single"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorMeshTest_Proxy, "Houdini.Editor.Mesh.Proxy", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorMeshTest_Proxy::RunTest(const FString& Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorMeshTests::EquivalenceTestMapName; + const FString ActorName = TEXT("Single_Proxy"); + const FString HDAAssetPath = FHoudiniEditorMeshTests::TestHDAPath + TEXT("Single"); + FHoudiniActorTestSettings Settings = {}; + Settings.bUseProxyMesh = true; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorMeshTest_Multiple, "Houdini.Editor.Mesh.Multiple", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorMeshTest_Multiple::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorMeshTests::EquivalenceTestMapName; + const FString ActorName = TEXT("Multiple"); + const FString HDAAssetPath = FHoudiniEditorMeshTests::TestHDAPath + TEXT("Multiple"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorMeshTest_LODs_Common, "Houdini.Editor.Mesh.LODs_Common", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorMeshTest_LODs_Common::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = FHoudiniEditorMeshTests::EquivalenceTestMapName; + const FString ActorName = TEXT("LODs_Common"); + const FString HDAAssetPath = FHoudiniEditorMeshTests::TestHDAPath + TEXT("LODs_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr); + }); + + return true; +} + + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorMeshTests.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +// Class just for containing static member variables +class FHoudiniEditorMeshTests +{ +public: + static FString EquivalenceTestMapName; + static FString TestHDAPath; +}; + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorParametersTests.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorParametersTests.h" + +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "HoudiniPublicAPIAssetWrapper.h" +#include "HoudiniAssetComponent.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "HAL/FileManager.h" +#else + #include "Core/Public/HAL/FileManager.h" +#endif +#include "Misc/AutomationTest.h" + +FString FHoudiniEditorParametersTests::EquivalenceTestMapName = TEXT("Parameters"); +FString FHoudiniEditorParametersTests::TestHDAPath = TEXT("/Game/TestHDAs/Parameters/"); + +// Test setting a single float value +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorParametersTest_Float_Single, "Houdini.Editor.Parameters.Float_Single", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorParametersTest_Float_Single::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("Float_Single"); + const FString MapName = FHoudiniEditorParametersTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorParametersTests::TestHDAPath + TEXT("Parameters_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject(this, InAssetWrapper, InTestObject)) + return; + + // Trigger a parameter update after the first cook, test setting on pre-instantiate as a separate test + InTestObject->ExpectedCookCount = 2; + InTestObject->OnPostProcessingDelegate.AddLambda([this](UHoudiniPublicAPIAssetWrapper* const InWrapper, UHoudiniEditorTestObject* const InTestObj) + { + if (!InWrapper->SetFloatParameterValue(TEXT("uniform_scale"), 2.5f)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InWrapper, TEXT("float"), TEXT("uniform_scale"), TEXT("2.5"), 0); + } + }); + }); + }); + + return true; +} + +// Test setting the values of a float tuple +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorParametersTest_Float_Tuple, "Houdini.Editor.Parameters.Float_Tuple", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorParametersTest_Float_Tuple::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("Float_Tuple"); + const FString MapName = FHoudiniEditorParametersTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorParametersTests::TestHDAPath + TEXT("Parameters_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject(this, InAssetWrapper, InTestObject)) + return; + + // Trigger a parameter update after the first cook, test setting on pre-instantiate as a separate test + InTestObject->ExpectedCookCount = 2; + InTestObject->OnPostProcessingDelegate.AddLambda([this](UHoudiniPublicAPIAssetWrapper* const InWrapper, UHoudiniEditorTestObject* const InTestObj) + { + if (!InWrapper->SetFloatParameterValue(TEXT("scale"), 1.5f, 0)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InWrapper, TEXT("float"), TEXT("scale"), TEXT("1.5f"), 0); + } + if (!InWrapper->SetFloatParameterValue(TEXT("scale"), 3.0f, 1)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InWrapper, TEXT("float"), TEXT("scale"), TEXT("3.0f"), 1); + } + if (!InWrapper->SetFloatParameterValue(TEXT("scale"), 6.0f, 2)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InWrapper, TEXT("float"), TEXT("scale"), TEXT("6.0f"), 2); + } + }); + }); + }); + + return true; +} + + +// Test setting the values of a float vector +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorParametersTest_Float_Vector, "Houdini.Editor.Parameters.Float_Vector", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorParametersTest_Float_Vector::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("Float_Vector"); + const FString MapName = FHoudiniEditorParametersTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorParametersTests::TestHDAPath + TEXT("Parameters_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject(this, InAssetWrapper, InTestObject)) + return; + + // Trigger a parameter update after the first cook, test setting on pre-instantiate as a separate test + InTestObject->ExpectedCookCount = 2; + InTestObject->OnPostProcessingDelegate.AddLambda([this, InAssetWrapper](UHoudiniPublicAPIAssetWrapper* const InWrapper, UHoudiniEditorTestObject* const InTestObj) + { + if (!InAssetWrapper->SetFloatParameterValue(TEXT("t"), 100.0f, 0)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("float"), TEXT("t"), TEXT("100.0f"), 0); + } + if (!InAssetWrapper->SetFloatParameterValue(TEXT("t"), 0.0f, 1)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("float"), TEXT("t"), TEXT("0.0f"), 1); + } + if (!InAssetWrapper->SetFloatParameterValue(TEXT("t"), 10.0f, 2)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("float"), TEXT("t"), TEXT("10.0f"), 2); + } + + if (!InAssetWrapper->SetFloatParameterValue(TEXT("r"), 33.3f, 0)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("float"), TEXT("r"), TEXT("33.3f"), 0); + } + if (!InAssetWrapper->SetFloatParameterValue(TEXT("r"), 66.6f, 1)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("float"), TEXT("r"), TEXT("66.6f"), 1); + } + if (!InAssetWrapper->SetFloatParameterValue(TEXT("r"), 99.9f, 2)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("float"), TEXT("r"), TEXT("99.9f"), 2); + } + }); + }); + }); + + return true; +} + + +// Test setting the values of a float tuple +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorParametersTest_Float_Tuple_PreInstantiate, "Houdini.Editor.Parameters.Float_Tuple_PreInstantiate", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorParametersTest_Float_Tuple_PreInstantiate::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("Float_Tuple_PreInstantiate"); + const FString MapName = FHoudiniEditorParametersTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorParametersTests::TestHDAPath + TEXT("Parameters_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject(this, InAssetWrapper, InTestObject)) + return; + + // Trigger a parameter update during pre-instantiate + InTestObject->ExpectedCookCount = 2; + InTestObject->OnPostProcessingDelegate.AddLambda([this](UHoudiniPublicAPIAssetWrapper* const InWrapper, UHoudiniEditorTestObject* const InTestObj) + { + if (!InWrapper->SetFloatParameterValue(TEXT("scale"), 1.5f, 0)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InWrapper, TEXT("float"), TEXT("scale"), TEXT("1.5f"), 0); + } + if (!InWrapper->SetFloatParameterValue(TEXT("scale"), 3.0f, 1)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InWrapper, TEXT("float"), TEXT("scale"), TEXT("3.0f"), 1); + } + if (!InWrapper->SetFloatParameterValue(TEXT("scale"), 6.0f, 2)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InWrapper, TEXT("float"), TEXT("scale"), TEXT("6.0f"), 2); + } + }); + }); + }); + + return true; +} + + +// Test unchecking a toggle +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorParametersTest_Toggle_Uncheck, "Houdini.Editor.Parameters.Toggle_Uncheck", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorParametersTest_Toggle_Uncheck::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("Toggle_Uncheck"); + const FString MapName = FHoudiniEditorParametersTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorParametersTests::TestHDAPath + TEXT("Parameters_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject(this, InAssetWrapper, InTestObject)) + return; + + // Trigger a parameter update after the first cook, test setting on pre-instantiate as a separate test + InTestObject->ExpectedCookCount = 2; + InTestObject->OnPostProcessingDelegate.AddLambda([this, InAssetWrapper](UHoudiniPublicAPIAssetWrapper* const InWrapper, UHoudiniEditorTestObject* const InTestObj) + { + if (!InAssetWrapper->SetBoolParameterValue(TEXT("enable_polyreduce"), false)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("bool"), TEXT("enable_polyreduce"), TEXT("false"), 0); + } + }); + }); + }); + + return true; +} + + +// Test unchecking a toggle +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorParametersTest_Toggle_Check, "Houdini.Editor.Parameters.Toggle_Check", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorParametersTest_Toggle_Check::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("Toggle_Check"); + const FString MapName = FHoudiniEditorParametersTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorParametersTests::TestHDAPath + TEXT("Parameters_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject(this, InAssetWrapper, InTestObject)) + return; + + // Trigger a parameter update after the first cook, test setting on pre-instantiate as a separate test + InTestObject->ExpectedCookCount = 2; + InTestObject->OnPostProcessingDelegate.AddLambda([this, InAssetWrapper](UHoudiniPublicAPIAssetWrapper* const InWrapper, UHoudiniEditorTestObject* const InTestObj) + { + if (!InAssetWrapper->SetBoolParameterValue(TEXT("screws"), true)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("bool"), TEXT("screws"), TEXT("true"), 0); + } + }); + }); + }); + + return true; +} + +// Test unchecking a toggle +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorParametersTest_Toggle_Check_PreInstantiate, "Houdini.Editor.Parameters.Toggle_Check_PreInstantiate", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorParametersTest_Toggle_Check_PreInstantiate::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("Toggle_Check_PreInstantiate"); + const FString MapName = FHoudiniEditorParametersTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorParametersTests::TestHDAPath + TEXT("Parameters_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject(this, InAssetWrapper, InTestObject)) + return; + + // Trigger a parameter update during preinstantiate + if (!InAssetWrapper->SetBoolParameterValue(TEXT("screws"), true)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("bool"), TEXT("screws"), TEXT("true"), 0); + } + }); + }); + + return true; +} + +// Test pressing a button +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorParametersTest_Button, "Houdini.Editor.Parameters.Button", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorParametersTest_Button::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("Button"); + const FString MapName = FHoudiniEditorParametersTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorParametersTests::TestHDAPath + TEXT("Parameters_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject(this, InAssetWrapper, InTestObject)) + return; + + // Triggering buttons during pre-instantiation is not supported, we have to wait for 1 cook and then + // triggering the button and check the results after the 2nd cook + InTestObject->ExpectedCookCount = 2; + InTestObject->OnPostProcessingDelegate.AddLambda([this](UHoudiniPublicAPIAssetWrapper* const InWrapper, UHoudiniEditorTestObject* const InTestObj) + { + if (!InWrapper->TriggerButtonParameter(TEXT("instances_preset_1_button"))) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InWrapper, TEXT("button"), TEXT("instances_preset_1_button"), TEXT("trigger"), 0); + } + }); + }); + }); + + return true; +} + + +// Test setting a single value (r) of a color +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorParametersTest_Color_Single, "Houdini.Editor.Parameters.Color_Single", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorParametersTest_Color_Single::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("Color_Single"); + const FString MapName = FHoudiniEditorParametersTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorParametersTests::TestHDAPath + TEXT("Parameters_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject(this, InAssetWrapper, InTestObject)) + return; + + // Trigger a parameter update after the first cook, test setting on pre-instantiate as a separate test + InTestObject->ExpectedCookCount = 2; + InTestObject->OnPostProcessingDelegate.AddLambda([this, InAssetWrapper](UHoudiniPublicAPIAssetWrapper* const InWrapper, UHoudiniEditorTestObject* const InTestObj) + { + if (!InAssetWrapper->SetFloatParameterValue(TEXT("color"), 0.27f)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("color"), TEXT("color"), TEXT("0.27"), 0); + } + }); + }); + }); + + return true; +} + +static const FLinearColor TestParameterColor = FLinearColor(0.5f, 0.25f, 0.125f); + + // Test setting a color parameter +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorParametersTest_Color, "Houdini.Editor.Parameters.Color", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorParametersTest_Color::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("Color"); + const FString MapName = FHoudiniEditorParametersTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorParametersTests::TestHDAPath + TEXT("Parameters_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject(this, InAssetWrapper, InTestObject)) + return; + + // Trigger a parameter update after the first cook, test setting on pre-instantiate as a separate test + InTestObject->ExpectedCookCount = 2; + InTestObject->OnPostProcessingDelegate.AddLambda([this, InAssetWrapper](UHoudiniPublicAPIAssetWrapper* const InWrapper, UHoudiniEditorTestObject* const InTestObj) + { + if (!InAssetWrapper->SetColorParameterValue(TEXT("color"), TestParameterColor)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("color"), TEXT("color"), TEXT("(153, 50, 204)"), 0); + } + }); + }); + }); + + return true; +} + + +// Test setting a color parameter +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorParametersTest_Color_PreInstantiate, "Houdini.Editor.Parameters.Color_PreInstantiate", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorParametersTest_Color_PreInstantiate::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("Color_PreInstantiate"); + const FString MapName = FHoudiniEditorParametersTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorParametersTests::TestHDAPath + TEXT("Parameters_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject(this, InAssetWrapper, InTestObject)) + return; + + // Trigger a parameter update during pre-instantiate + if (!InAssetWrapper->SetColorParameterValue(TEXT("color"), TestParameterColor)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("color"), TEXT("color"), TEXT("(153, 50, 204)"), 0); + } + }); + }); + + return true; +} + + +// Test setting a radio button / folder +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorParametersTest_String_Choice_Single, "Houdini.Editor.Parameters.String_Choice_Single", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorParametersTest_String_Choice_Single::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("String_Choice_Single"); + const FString MapName = FHoudiniEditorParametersTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorParametersTests::TestHDAPath + TEXT("Parameters_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject(this, InAssetWrapper, InTestObject)) + return; + + // Trigger a parameter update after the first cook, test setting on pre-instantiate as a separate test + InTestObject->ExpectedCookCount = 2; + InTestObject->OnPostProcessingDelegate.AddLambda([this, InAssetWrapper](UHoudiniPublicAPIAssetWrapper* const InWrapper, UHoudiniEditorTestObject* const InTestObj) + { + if (!InAssetWrapper->SetStringParameterValue(TEXT("copy_type"), TEXT("copy"))) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("string_choice"), TEXT("copy_type"), TEXT("copy"), 0); + } + }); + }); + }); + + return true; +} + +// Test selecting a menu item in a single selection menu +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorParametersTest_Menu_Single_Select, "Houdini.Editor.Parameters.Int_Choice_Single", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorParametersTest_Menu_Single_Select::RunTest(const FString & Parameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("Int_Choice_Single"); + const FString MapName = FHoudiniEditorParametersTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorParametersTests::TestHDAPath + TEXT("Parameters_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject(this, InAssetWrapper, InTestObject)) + return; + + // Trigger a parameter update after the first cook, test setting on pre-instantiate as a separate test + InTestObject->ExpectedCookCount = 2; + InTestObject->OnPostProcessingDelegate.AddLambda([this, InAssetWrapper](UHoudiniPublicAPIAssetWrapper* const InWrapper, UHoudiniEditorTestObject* const InTestObj) + { + // Enable instancers + if (!InAssetWrapper->SetStringParameterValue(TEXT("copy_type"), TEXT("pack"))) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("string_choice"), TEXT("copy_type"), TEXT("pack"), 0); + } + + // Set 2 * 3 grid + if (!InAssetWrapper->SetIntParameterValue(TEXT("rows2"), 2)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("integer"), TEXT("rows2"), TEXT("2"), 0); + } + if (!InAssetWrapper->SetIntParameterValue(TEXT("cols2"), 3)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("integer"), TEXT("cols2"), TEXT("3"), 0); + } + + // Grid orient: YZ Plane + if (!InAssetWrapper->SetIntParameterValue(TEXT("orient2"), 1)) + { + FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure(this, InAssetWrapper, TEXT("int_choice/ordered_menu"), TEXT("orient2"), TEXT("1"), 0); + } + }); + }); + }); + + return true; +} + +#endif + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorParametersTests.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +// Class for containing static member variables for the tests +class FHoudiniEditorParametersTests +{ +public: + static FString EquivalenceTestMapName; + static FString TestHDAPath; +}; + +#endif + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTemplatedGeoTests.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTemplatedGeoTests.h" + +#if WITH_DEV_AUTOMATION_TESTS +#include "Runtime/Launch/Resources/Version.h" + +#include "HoudiniEditorTestUtils.h" +#include "HoudiniPublicAPIAssetWrapper.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "HAL/FileManager.h" +#else + #include "Core/Public/HAL/FileManager.h" +#endif +#include "Misc/AutomationTest.h" +#include "HoudiniAssetComponent.h" + +FString FHoudiniEditorTemplatedGeoTests::EquivalenceTestMapName = TEXT("TemplatedGeo"); +FString FHoudiniEditorTemplatedGeoTests::TestHDAPath = TEXT("/Game/TestHDAs/TemplatedGeo/"); + +/** Tests with an HDA that has templated geo, but with the "Output Templated Geos" checkbox unchecked */ +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorTemplatedGeoTest_HACDisabled, "Houdini.Editor.TemplatedGeo.HACDisabled", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorTemplatedGeoTest_HACDisabled::RunTest(const FString& InParameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("HACDisabled"); + const FString MapName = FHoudiniEditorTemplatedGeoTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorTemplatedGeoTests::TestHDAPath + TEXT("TemplatedGeo_Enabled_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + // OnPreInstantiate -- set parameters or HAC properties etc + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + if (!IsValid(InAssetWrapper)) + { + this->AddError(TEXT("Asset wrapper is invalid during pre-instantiation!")); + return; + } + + // Do nothing... templated geo is off by default + }); + }); + + return true; +} + + +/** Tests with an HDA that has templated geo, but with the "Output Templated Geos" checkbox checked */ +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorTemplatedGeoTest_HACEnabled, "Houdini.Editor.TemplatedGeo.HACEnabled", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorTemplatedGeoTest_HACEnabled::RunTest(const FString& InParameters) +{ + // Currently there is an issue in HAPI that will add an error about HAPI_CookOptions::cookTemplatedGeos == false, + // even though it is not, ignore that for now + /* + AddExpectedError( + TEXT("Hapi failed: Invalid argument given: Tempated non-editable geo did not cook because " + "HAPI_CookOptions::cookTemplatedGeos is false. Geo info may be out of date."), + EAutomationExpectedErrorFlags::Contains, 0); + */ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("HACEnabled"); + const FString MapName = FHoudiniEditorTemplatedGeoTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorTemplatedGeoTests::TestHDAPath + TEXT("TemplatedGeo_Enabled_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + // OnPreInstantiate -- set parameters or HAC properties etc + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + UHoudiniAssetComponent* HAC = nullptr; + if (!FHoudiniEditorTestUtils::CheckAssetWrapperTestObjectAndGetValidHAC(this, InAssetWrapper, InTestObject, HAC)) + return; + + // Enable templated geo output via the HAC + HAC->SetOutputTemplateGeos(true); + }); + }); + + return true; +} + + +/** Tests with an HDA that does not have templated geo, but with the "Output Templated Geos" checkbox checked */ +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorTemplatedGeoTest_HACEnabled_NoTemplateInOutput, "Houdini.Editor.TemplatedGeo.HACEnabled_NoTemplateInOutput", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorTemplatedGeoTest_HACEnabled_NoTemplateInOutput::RunTest(const FString& InParameters) +{ + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString TestName = TEXT("HACEnabled_NoTemplateInOutput"); + const FString MapName = FHoudiniEditorTemplatedGeoTests::EquivalenceTestMapName + TEXT("/") + TestName; + const FString ActorName = TestName; + const FString HDAAssetPath = FHoudiniEditorTemplatedGeoTests::TestHDAPath + TEXT("TemplatedGeo_Disabled_Common"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, nullptr, + // OnPreInstantiate -- set parameters or HAC properties etc + [this](UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, UHoudiniEditorTestObject* const InTestObject) + { + UHoudiniAssetComponent* HAC = nullptr; + if (!FHoudiniEditorTestUtils::CheckAssetWrapperTestObjectAndGetValidHAC(this, InAssetWrapper, InTestObject, HAC)) + return; + + // Enable templated geo output via the HAC + HAC->SetOutputTemplateGeos(true); + }); + }); + + return true; +} + +#endif + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTemplatedGeoTests.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +// Class for containing static member variables for the tests +class FHoudiniEditorTemplatedGeoTests +{ +public: + static FString EquivalenceTestMapName; + static FString TestHDAPath; +}; + +#endif + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestAnimation.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestAnimation.h" + +#include "HoudiniCookable.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterString.h" +#include "HoudiniParameterToggle.h" + +#include "Animation/SkeletalMeshActor.h" +#include "Animation/Skeleton.h" +#include "Chaos/HeightField.h" +#include "Components/SkeletalMeshComponent.h" +#include "EditorAnimUtils.h" +#include "Engine/SkeletalMesh.h" + +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" +#include "HoudiniEditorUnitTestUtils.h" + +#include "Animation/AnimSequence.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "Misc/AutomationTest.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "Engine/SkinnedAssetCommon.h" +#endif + +TMap> FHoudiniEditorTestAnimationUtils::GetAnimationTransforms(const UAnimSequence* AnimSequence) +{ + const IAnimationDataModel* DataModel = AnimSequence->GetDataModel(); + + TArray BonesTrackNames; + DataModel->GetBoneTrackNames(BonesTrackNames); + + TMap> TrackMap; //For Each Bone, Store Array of transforms (one for each key) + + //Iterate over BoneTracks + for (FName TrackName : BonesTrackNames) + { + //const FMeshBoneInfo& CurrentBone = RefSkeleton.GetRefBoneInfo()[BoneIndex]; + TArray ThisTrackTransforms; + DataModel->GetBoneTrackTransforms(TrackName, ThisTrackTransforms); + FString DetailAttributeName = FString::Printf(TEXT("%s_transforms"), *TrackName.ToString()); + TArray TrackFloatKeys; + TrackMap.Add(TrackName, ThisTrackTransforms); + } + return TrackMap; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestAnimationRoundtrip, "Houdini.UnitTests.Animation.Roundtrip", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestAnimationRoundtrip::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestAnimationUtils::AnimationRoundtripHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + UAnimSequence* OrigAnimSequence = LoadObject(Context->World, TEXT("/Script/Engine.SkeletalMesh'/Game/TestObjects/Animation/MM_Walk_Fwd.MM_Walk_Fwd'")); + + HOUDINI_TEST_NOT_NULL_ON_FAIL(OrigAnimSequence, return false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, OrigAnimSequence]() + { + bool bChanged = true; + + UHoudiniInput* Input = Context->GetInputAt(0); + + Input->InsertInputObjectAt(EHoudiniInputType::Geometry, 0); + + UHoudiniInputObject* CurrentInputObjectWrapper = Input->GetHoudiniInputObjectAt(0); + if (CurrentInputObjectWrapper) + CurrentInputObjectWrapper->Modify(); + + Input->Modify(); + Input->SetInputObjectAt(EHoudiniInputType::Geometry, 0, OrigAnimSequence); + Input->MarkChanged(true); + + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs[0]->GetType(), EHoudiniOutputType::AnimSequence, return true); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, OrigAnimSequence]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + auto BakedOutputs = Context->GetBakedOutputs(); + + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + auto BakedObjects = BakedOutputs[0].BakedOutputObjects; + TArray BakeOutputObjects; + BakedObjects.GenerateValueArray(BakeOutputObjects); + HOUDINI_TEST_EQUAL_ON_FAIL(BakeOutputObjects.Num(), 1, return true); + + UAnimSequence* AnimSequence = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakeOutputObjects[0].BakedObject)); + + HOUDINI_TEST_NOT_NULL_ON_FAIL(AnimSequence, return true); + + TMap> OrigAnim = FHoudiniEditorTestAnimationUtils::GetAnimationTransforms(OrigAnimSequence); + TMap> NewAnim = FHoudiniEditorTestAnimationUtils::GetAnimationTransforms(AnimSequence); + + + HOUDINI_TEST_EQUAL_ON_FAIL(OrigAnim.Num(), NewAnim.Num(), return true); + + TArray BoneNames; + OrigAnim.GetKeys(BoneNames); + + for(const FName & Name : BoneNames) + { + TArray * Transforms = NewAnim.Find(Name); + if (!Transforms) + { + FString ErrorString = FString::Printf(TEXT("Missing bone %s in new Animation"), *Name.ToString()); + AddError(ErrorString); + return true; + } + + TArray* OrigTransforms = OrigAnim.Find(Name); + if (OrigTransforms->Num() != Transforms->Num()) + { + FString ErrorString = FString::Printf(TEXT("Mismatched transforms %s in new Animation"), *Name.ToString()); + AddError(ErrorString); + return true; + } + + for (int Index = 0; Index < OrigTransforms->Num(); Index++) + { + FTransform * OrigTransform = &(*OrigTransforms)[Index]; + FTransform * Transform = &(*Transforms)[Index]; + + if (!OrigTransform->GetLocation().Equals(Transform->GetLocation(), 0.01f)) + { + FString Error = FString::Printf(TEXT("Bone %s Location Differs: Original %s Cooked %s"), *Name.ToString(), + *OrigTransform->GetLocation().ToString(), + *Transform->GetLocation().ToString()); + + AddError(Error); + } + + if (!OrigTransform->GetRotation().Equals(Transform->GetRotation(), 0.01f)) + { + FString Error = FString::Printf(TEXT("Bone %s Rotation Differs: Original %s Cooked %s"), *Name.ToString(), + *OrigTransform->GetRotation().ToString(), + *Transform->GetRotation().ToString()); + + AddError(Error); + } + + if (!OrigTransform->GetScale3D().Equals(Transform->GetScale3D(), 0.01f)) + { + FString Error = FString::Printf(TEXT("Bone %s Scale Differs: Original %s Cooked %s"), *Name.ToString(), + *OrigTransform->GetScale3D().ToString(), + *Transform->GetScale3D().ToString()); + + AddError(Error); + } + } + } + return true; + })); + return true; +} + + +#endif +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestAnimation.h`: + +```h +/* +* Copyright (c) <2024> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" +#include "Animation/AnimSequence.h" + +class FHoudiniEditorTestAnimationUtils +{ +public: + const static inline FString AnimationRoundtripHDA = TEXT("/Game/TestHDAs/Animation/Test_AnimationRoundtrip"); + + static TMap> GetAnimationTransforms(const UAnimSequence * AnimSequence); + +}; +#endif +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestBaking.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestBaking.h" + +#include "HoudiniCookable.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterString.h" +#include "HoudiniParameterToggle.h" + +#include "Chaos/HeightField.h" + +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" +#include "HoudiniEditorUnitTestUtils.h" + +#include "Components/InstancedStaticMeshComponent.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "Misc/AutomationTest.h" + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestInstancesMeshes, "Houdini.UnitTests.Baking.Meshes", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestInstancesMeshes::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking of meshes. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestBaking::BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "meshes", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_meshes", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_actors", false, 0); + + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 2, return true); + TArray StaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshOutputs.Num(), 2, return true); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 2, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + TArray Components; + Actor->GetComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 1, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Components[0]->IsA(), 1, continue); + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 2, return false); + + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + FHoudiniBakeSettings BakeSettings; + BakeSettings.ActorBakeOption = EHoudiniEngineActorBakeOption::OneActorPerHDA; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 2, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + TArray Components; + Actor->GetComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 2, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Components[0]->IsA(), 1, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Components[1]->IsA(), 1, continue); + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 1, return false); + + return true; + })); + + + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestBakingGrouped, "Houdini.UnitTests.Baking.MultipleComponentsOneActor", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool +FHoudiniEditorTestBakingGrouped::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Test baking multiple components to a single actor + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestBaking::BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "meshes", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_meshes", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_actors", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "geometry_collections", true, 0); + + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL(Outputs.Num(), 7); + TArray StaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL(StaticMeshOutputs.Num(), 2); + TArray InstancedStaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL(InstancedStaticMeshOutputs.Num(), 1); + auto GeometryCollections = FHoudiniEditorUnitTestUtils::GetOutputsWithActor(Outputs); + HOUDINI_TEST_EQUAL(GeometryCollections.Num(), 1); + return true; + })); + + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + BakeSettings.ActorBakeOption = EHoudiniEngineActorBakeOption::OneActorPerHDA; + + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 7, return true); + + // Gather outputs + + TArray Actors = FHoudiniEditorUnitTestUtils::GetOutputActors(BakedOutputs); + + // There should be one geometry collection actor. + TArray GeometryCollectionActors = FHoudiniEditorUnitTestUtils::FilterActors(Actors); + HOUDINI_TEST_EQUAL_ON_FAIL(GeometryCollectionActors.Num(), 1, return true); + + TArray StaticMeshActors = FHoudiniEditorUnitTestUtils::FilterActorsWithComponent(Actors); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshActors.Num(), 4, return true); + + TArray InstancedStaticMeshActors = FHoudiniEditorUnitTestUtils::FilterActorsWithComponent(Actors); + HOUDINI_TEST_EQUAL_ON_FAIL(InstancedStaticMeshActors.Num(), 4, return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestBakingGroupedToBlueprint, "Houdini.UnitTests.Baking.MultipleComponentsToOneBlueprint", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestBakingGroupedToBlueprint::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking multiple components to a single blueprint + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestBaking::BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "meshes", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_meshes", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_actors", true, 0); + + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 4, return true); + TArray StaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshOutputs.Num(), 2, return true); + TArray InstancedStaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(InstancedStaticMeshOutputs.Num(), 1, return true); + + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + BakeSettings.ActorBakeOption = EHoudiniEngineActorBakeOption::OneActorPerHDA; + if (Context->GetCookable()) + Context->GetCookable()->SetHoudiniEngineBakeOption(EHoudiniEngineBakeOption::ToBlueprint); + else + Context->GetHAC()->SetHoudiniEngineBakeOption(EHoudiniEngineBakeOption::ToBlueprint); + + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 4, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet BlueprintNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + UBlueprint* Blueprint = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Blueprint)); + UWorld * World = Context->GetWorld(); + AActor* Actor = World->SpawnActor(Blueprint->GeneratedClass, FVector::ZeroVector, FRotator::ZeroRotator); + + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + TArray Components; + Actor->GetComponents(Components); + + TArray MeshComponents = FHoudiniEditorUnitTestUtils::FilterComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(MeshComponents.Num(), 2, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(MeshComponents[0]->IsA(), true, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(MeshComponents[1]->IsA(), true, continue); + + TArray InstancedComponents = FHoudiniEditorUnitTestUtils::FilterComponents(Components);; + HOUDINI_TEST_EQUAL_ON_FAIL(InstancedComponents.Num(), 1, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(InstancedComponents[0]->IsA(), true, continue); + + BlueprintNames.Add(*OutputObject.Blueprint); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(BlueprintNames.Num(), 1, return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestBakingBPs, "Houdini.UnitTests.Baking.BakingBPs", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool +FHoudiniEditorTestBakingBPs::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Test baking multiple components to a single actor + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestBaking::BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "meshes", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_meshes", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_actors", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "geometry_collections", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "object", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "object_path", TEXT("/Script/Engine.Blueprint'/Game/TestObjects/BP_Cube.BP_Cube'"), 0); + + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + HOUDINI_TEST_EQUAL(Outputs.Num(), 1); + return true; + })); + + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + BakeSettings.ActorBakeOption = EHoudiniEngineActorBakeOption::OneActorPerComponent; + + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Gather outputs + + TArray Actors = FHoudiniEditorUnitTestUtils::GetOutputInstancedActors(BakedOutputs); + HOUDINI_TEST_EQUAL_ON_FAIL(Actors.Num(), 1, return true); + + HOUDINI_TEST_NOT_NULL(Actors[0]); + + USceneComponent* Root = Actors[0]->GetRootComponent(); + HOUDINI_TEST_NOT_NULL(Root); + + HOUDINI_TEST_NULL(Actors[0]->GetAttachParentActor()); + + + HOUDINI_TEST_EQUAL(Actors[0]->Tags.Num(), 1); + + return true; + })); + + return true; +} + + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestBaking.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +class FHoudiniEditorTestBaking +{ +public: + const static inline FString BakingHDA = TEXT("/Game/TestHDAs/Baking/Test_Baking"); + const static inline FString InstancedMesh = TEXT("/Script/Engine.StaticMesh'/Game/TestObjects/SM_Cube.SM_Cube'"); + const static inline FString InstancedActor = TEXT("/Script/Engine.Blueprint'/Game/TestObjects/BP_Cube.BP_Cube'"); + +}; +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestDataLayers.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestDataLayers.h" + +#include "FileHelpers.h" +#include "HoudiniAsset.h" +#include "HoudiniPublicAPIAssetWrapper.h" +#include "HoudiniPublicAPIInputTypes.h" +#include "HoudiniAssetActor.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniPDGManager.h" +#include "HoudiniParameterString.h" +#include "HoudiniPDGAssetLink.h" +#include "Landscape.h" +#include "AssetRegistry/AssetRegistryModule.h" +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" +#include "HoudiniAssetActorFactory.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "HoudiniParameterToggle.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "HoudiniEditorUnitTestUtils.h" +#include "FoliageType_InstancedStaticMesh.h" +#include "HoudiniEngineBakeUtils.h" +#include "LandscapeStreamingProxy.h" + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestsPDGDataLayers, "Houdini.UnitTests.DataLayers.PDGTest", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestsPDGDataLayers::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// This test enusres that we can cook an HDA multiple times and the outputs are removed on each recook. The test HDA + /// can create multiple outputs based off the parameters. By changing the parameters we can get different scenarios on + /// the same HDA. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString HDAName = TEXT("/Game/TestHDAs/PDG/PDGHarness"); + + // Now create the test context. This should be the last step before the tests start as it starts the timeout timer. Note + // the context live in a SharedPtr<> because each part of the test, in AddCommand(), are executed asyncronously + // after the test returns. + + TSharedPtr Context(new FHoudiniTestContext(this, HDAName, FTransform::Identity, true)); + Context->SetProxyMeshEnabled(false); + + // HDA Path and kick Cook. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FString HDAPath = FHoudiniEditorUnitTestUtils::GetAbsolutePathOfProjectFile(TEXT("TestHDAS/DataLayers/CreateMeshWithDataLayer.hda")); + HOUDINI_LOG_MESSAGE(TEXT("Resolved HDA to %s"), *HDAPath); + + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "hda_path", HDAPath, 0); + Context->StartCookingHDA(); + return true; + })); + + // kick PDG Cook. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingSelectedTOPNetwork(); + return true; + })); + + // Bake and check results. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + UHoudiniPDGAssetLink * AssetLink = Context->GetPDGAssetLink(); + UTOPNetwork * Network = AssetLink->GetTOPNetwork(0); + HOUDINI_TEST_NOT_NULL(Network); + + UTOPNode * Node = nullptr; + for(UTOPNode * It : Network->AllTOPNodes) + { + if (It->NodeName == "HE_OUT_X") + { + Node = It; + break; + } + } + HOUDINI_TEST_NOT_NULL(Node); + + if (!Node) + return false; + + auto & Results = Node->WorkResult; + + + // We should have one work result. Check this before baking. + HOUDINI_TEST_EQUAL_ON_FAIL(Results.Num(), 1, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(Results[0].ResultObjects.Num(), 1, return true); + + // Bake PDG Output + TArray BakedActors = Context->BakeSelectedTopNetwork(); + HOUDINI_TEST_EQUAL_ON_FAIL(BakedActors.Num(), 1, return true); + + TArray StaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(BakedActors); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshOutputs.Num(), 1, return true); + + AActor * Actor = StaticMeshOutputs[0]->GetOwner(); + + TArray DataLayers = FHoudiniDataLayerUtils::GetDataLayerInfoForActor(Actor); + + FString ExpectedName(TEXT("MyDataLayer")); + + HOUDINI_TEST_EQUAL_ON_FAIL(DataLayers.Num(), 1, return true); + HOUDINI_TEST_EQUAL(DataLayers[0].Name.Left(ExpectedName.Len()), ExpectedName); + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestLandscapeDataLayers, "Houdini.UnitTests.DataLayers.Landscapes", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestLandscapeDataLayers::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString HDAName = TEXT("/Game/TestHDAs/DataLayers/CreateLandscapeWithDataLayers"); + + // Now create the test context. This should be the last step before the tests start as it starts the timeout timer. Note + // the context live in a SharedPtr<> because each part of the test, in AddCommand(), are executed asyncronously + // after the test returns. + + TSharedPtr Context(new FHoudiniTestContext(this, HDAName, FTransform::Identity, true)); + Context->SetProxyMeshEnabled(false); + + // HDA Path and kick Cook. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingHDA(); + return true; + })); + + // Bake and check results. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + auto& BakedOutput = BakedOutputs[0]; + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutput.BakedOutputObjects.Num(), 1, return true); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().ElementIt->Value.Value;; +#else + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().Value(); +#endif + + ALandscape* Landscape = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.Landscape)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Landscape, return true); + + TArray DataLayers = FHoudiniDataLayerUtils::GetDataLayerInfoForActor(Landscape); + + FString ExpectedName(TEXT("TestDataLayer")); + HOUDINI_TEST_EQUAL_ON_FAIL(DataLayers.Num(), 1, return true); + HOUDINI_TEST_EQUAL(DataLayers[0].Name.Left(ExpectedName.Len()), ExpectedName); + + ULandscapeInfo* Info = Landscape->GetLandscapeInfo(); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + const TArray>& Proxies = Info->GetSortedStreamingProxies(); + for (auto ProxyPtr : Proxies) + { + ALandscapeStreamingProxy* Proxy = ProxyPtr.Get(); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + TArray>& Proxies = Info->StreamingProxies; + for(auto ProxyPtr : Proxies) + { + ALandscapeStreamingProxy* Proxy = ProxyPtr.Get(); +#else + TArray& Proxies = Info->Proxies; + for(ALandscapeStreamingProxy* Proxy : Proxies) + { +#endif + HOUDINI_TEST_NOT_NULL_ON_FAIL(Proxy, return true); + + TArray ProxyDataLayers = FHoudiniDataLayerUtils::GetDataLayerInfoForActor(Cast(Proxy)); + + FString ProxyName(TEXT("TestDataLayer")); + HOUDINI_TEST_EQUAL_ON_FAIL(ProxyDataLayers.Num(), 1, return true); + HOUDINI_TEST_EQUAL(ProxyDataLayers[0].Name.Left(ProxyName.Len()), ProxyName); + } + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestInstancesDataLayers, "Houdini.UnitTests.DataLayers.Instances", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + + bool FHoudiniEditorTestInstancesDataLayers::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString HDAName = TEXT("/Game/TestHDAs/DataLayers/CreateInstancesDataLayers"); + + // Now create the test context. This should be the last step before the tests start as it starts the timeout timer. Note + // the context live in a SharedPtr<> because each part of the test, in AddCommand(), are executed asyncronously + // after the test returns. + + TSharedPtr Context(new FHoudiniTestContext(this, HDAName, FTransform::Identity, true)); + Context->SetProxyMeshEnabled(false); + + // HDA Path and kick Cook. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingHDA(); + return true; + })); + + // Bake and check results. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + auto& BakedOutput = BakedOutputs[0]; + + auto ObjIt = BakedOutput.BakedOutputObjects.begin(); + + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutput.BakedOutputObjects.Num(), 2, return true); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + auto& BakedObject0 = ObjIt.ElementIt->Value.Value; +#else + auto& BakedObject0 = ObjIt.Value(); +#endif + + // Check first output instancer has DataLayer1. + AActor * Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject0.Actor)); + TArray DataLayers = FHoudiniDataLayerUtils::GetDataLayerInfoForActor(Actor); + HOUDINI_TEST_EQUAL_ON_FAIL(DataLayers.Num(), 1, return true); + + FString DataLayerName = DataLayers[0].Name; + DataLayerName = DataLayerName.Mid(0, 10); + HOUDINI_TEST_EQUAL(DataLayerName, TEXT("DataLayer1")); + + // Check second output instancer has DataLayer2. + ++ObjIt; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + auto& BakedObject1 = ObjIt.ElementIt->Value.Value; +#else + auto& BakedObject1 = ObjIt.Value(); +#endif + Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject1.Actor)); + DataLayers = FHoudiniDataLayerUtils::GetDataLayerInfoForActor(Actor); + HOUDINI_TEST_EQUAL_ON_FAIL(DataLayers.Num(), 1, return true); + DataLayerName = DataLayers[0].Name; + DataLayerName = DataLayerName.Mid(0, 10); + HOUDINI_TEST_EQUAL(DataLayerName, TEXT("DataLayer2")); + + return true; + })); + + return true; +} + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestDataLayers.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestGeometryCollections.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 +#include "GeometryCollection/GeometryCollectionActor.h" +#include "GeometryCollection/GeometryCollectionComponent.h" +#include "GeometryCollection/GeometryCollectionObject.h" +#else +#include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionActor.h" +#include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionComponent.h" +#include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionObject.h" +#endif +#include "HoudiniEditorTestGeometryCollections.h" +#include "HoudiniParameterToggle.h" +#include "Chaos/HeightField.h" +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "HoudiniEditorUnitTestUtils.h" + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestGeometryCollections, "Houdini.UnitTests.GeometryCollection.GeometryCollection", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestGeometryCollections::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Tests baking of instances meshes + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestGeometryCollection::GeometryCollectionHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 3, return true); + TArray OutputObjects = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObjects.Num(), 1, return true); + return true; + })); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Bake the geometry actor + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 3, return true); + + // Go through each output and check we have two actors with one mesh component each. + TArray GeometryActors; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AGeometryCollectionActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + if (IsValid(Actor)) + GeometryActors.Add(Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(GeometryActors.Num(), 1, return true); + + return true; + })); + + return true; +} + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestGeometryCollections.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +class FHoudiniEditorTestGeometryCollection +{ +public: + const static inline FString GeometryCollectionHDA = TEXT("/Game/TestHDAs/GeometryCollections/Test_GC"); +}; +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestHLOD.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestDataLayers.h" + +#include "FileHelpers.h" +#include "HoudiniAsset.h" +#include "HoudiniPublicAPIAssetWrapper.h" +#include "HoudiniPublicAPIInputTypes.h" +#include "HoudiniAssetActor.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniPDGManager.h" +#include "HoudiniParameterString.h" +#include "HoudiniPDGAssetLink.h" +#include "Landscape.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "WorldPartition/HLOD/HLODLayer.h" +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" +#include "HoudiniAssetActorFactory.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "HoudiniParameterToggle.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "HoudiniEditorUnitTestUtils.h" +#include "FoliageType_InstancedStaticMesh.h" +#include "HoudiniEngineBakeUtils.h" + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestLandscapeHLOD, "Houdini.UnitTests.HLOD.Landscapes", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestLandscapeHLOD::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString HDAName = TEXT("/Game/TestHDAs/HLOD/CreateHeightFieldHLOD"); + + // Now create the test context. This should be the last step before the tests start as it starts the timeout timer. Note + // the context live in a SharedPtr<> because each part of the test, in AddCommand(), are executed asyncronously + // after the test returns. + + TSharedPtr Context(new FHoudiniTestContext(this, HDAName, FTransform::Identity, true)); + Context->SetProxyMeshEnabled(false); + + // HDA Path and kick Cook. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingHDA(); + return true; + })); + + // Bake and check results. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + auto& BakedOutput = BakedOutputs[0]; + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutput.BakedOutputObjects.Num(), 1, return true); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().ElementIt->Value.Value; +#else + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().Value(); +#endif + + ALandscape* Landscape = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.Landscape)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Landscape, return true); + + UHLODLayer * HODLayer = Landscape->GetHLODLayer(); + + HOUDINI_TEST_NOT_NULL(HODLayer); + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestInstancesHLOD, "Houdini.UnitTests.HLOD.Instances", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + + bool FHoudiniEditorTestInstancesHLOD::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString HDAName = TEXT("/Game/TestHDAs/HLOD/CreateHLODInstances"); + + // Now create the test context. This should be the last step before the tests start as it starts the timeout timer. Note + // the context live in a SharedPtr<> because each part of the test, in AddCommand(), are executed asyncronously + // after the test returns. + + TSharedPtr Context(new FHoudiniTestContext(this, HDAName, FTransform::Identity, true)); + Context->SetProxyMeshEnabled(false); + + // HDA Path and kick Cook. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingHDA(); + return true; + })); + + // Bake and check results. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + auto& BakedOutput = BakedOutputs[0]; + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutput.BakedOutputObjects.Num(), 2, return true); + + auto ObjIt = BakedOutput.BakedOutputObjects.begin(); + + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutput.BakedOutputObjects.Num(), 2, return true); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + auto& BakedObject0 = ObjIt.ElementIt->Value.Value; +#else + auto& BakedObject0 = ObjIt.Value(); +#endif + + // Check first output instancer has DataLayer1. + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject0.Actor)); + UHLODLayer* HODLayer0 = Actor->GetHLODLayer(); + HOUDINI_TEST_NOT_NULL(HODLayer0); + HOUDINI_TEST_EQUAL(HODLayer0->GetName(), TEXT("TestHLODLayer1")); + //HOUDINI_TEST_EQUAL(); + + // Check second output instancer has HLODLayer. + ++ObjIt; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + auto& BakedObject1 = ObjIt.ElementIt->Value.Value; +#else + auto& BakedObject1 = ObjIt.Value(); +#endif + Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject1.Actor)); + UHLODLayer* HODLayer1 = Actor->GetHLODLayer(); + HOUDINI_TEST_NOT_NULL(HODLayer1); + HOUDINI_TEST_EQUAL(HODLayer1->GetName(), TEXT("TestHLODLayer")); + + return true; + })); + + return true; +} + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestHLOD.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestInstances.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestBaking.h" +#include "HoudiniEngineCommands.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterString.h" + +#include "HoudiniParameterToggle.h" +#include "InstancedFoliage.h" +#include "Chaos/HeightField.h" +#include "Materials/Material.h" +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "HoudiniEditorUnitTestUtils.h" +#include "HoudiniEditorTestInstances.h" +#include "Components/HierarchicalInstancedStaticMeshComponent.h" +#include "InstancedFoliageActor.h" +#include "Math/UnrealMathUtility.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 +#include "LevelInstance/LevelInstanceComponent.h" +#endif +#include "HoudiniEngine.h" + +FVector FHoudiniInstanceAutomationTest::GetHDAInstancePosition(int Index) +{ + // the Test HDA uses a magic formula to calculate the position. This is replicated here, but with Y/Z + // swapped.... + + FVector ExpectedGlobalPosition; + ExpectedGlobalPosition.X = 10.0 + Index * 10; + ExpectedGlobalPosition.Z = 20.0; + ExpectedGlobalPosition.Y = 30.0 + Index * 20; + + // ... and scale by 100 (as H->U conversion always does) + ExpectedGlobalPosition = ExpectedGlobalPosition * 100.0f; + + return ExpectedGlobalPosition; +} + +void FHoudiniInstanceAutomationTest::CheckPositions(const TArray& Positions, int StartIndex) +{ + for(int Index = 0; Index < Positions.Num(); Index++) + { + int HDA_AttribIndex = Index + StartIndex; + + FVector ExpectedGlobalPosition = GetHDAInstancePosition(HDA_AttribIndex); + + HOUDINI_TEST_EQUALISH_ON_FAIL(Positions[Index], ExpectedGlobalPosition, 0.1, break); + } +} + +TArray FHoudiniInstanceAutomationTest::GetAllFoliageTypes(UWorld* InWorld) +{ + TSet Results; + + for (TActorIterator It(InWorld, AInstancedFoliageActor::StaticClass()); It; ++It) + { + AInstancedFoliageActor* IFA = Cast(*It); + + auto InstanceMap = IFA->GetAllInstancesFoliageType(); + for (auto& InstanceIt : InstanceMap) + { + FFoliageInfo* FoliageInfo = InstanceIt.Value; + Results.Add(InstanceIt.Key); + } + } + + TArray Ar = Results.Array(); + return Ar; +} + +TArray FHoudiniInstanceAutomationTest::GetAllFoliageInstances(UWorld* InWorld, UFoliageType* FoliageType) +{ + TArray Results; + + for (TActorIterator It(InWorld, AInstancedFoliageActor::StaticClass()); It; ++It) + { + AInstancedFoliageActor* IFA = Cast(*It); + + auto InstanceMap = IFA->GetAllInstancesFoliageType(); + for (auto& InstanceIt : InstanceMap) + { + FFoliageInfo* FoliageInfo = InstanceIt.Value; + if (InstanceIt.Key == FoliageType) + Results.Append(FoliageInfo->Instances); + + } + } + return Results; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestInstancesActors, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.InstancedMeshes", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestInstancesActors::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Tests baking of instances meshes + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniInstanceAutomationTest::BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "instance_object", "/Script/Engine.StaticMesh'/Game/TestObjects/SM_Cube.SM_Cube'", 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "max_instances", 100, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "split_instance_meshes", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "foliage", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_origin", true, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + TArray StaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshOutputs.Num(), 1, return true); + return true; + })); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Step 1: Bake the output using ungroup components. We should have one actor per outputs (so 2 in this case), and one component per + // actor + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + TArray Components; + Actor->GetComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 1, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Components[0]->IsA(), 1, continue); + + UInstancedStaticMeshComponent * ISMC = Components[0]; + + const FTransform & Transform = ISMC->GetComponentTransform(); + + TArray Positions; + Positions.SetNum(ISMC->PerInstanceSMData.Num()); + for(int Index = 0; Index < ISMC->PerInstanceSMData.Num(); Index++) + { + Positions[Index] = Transform.TransformPosition(ISMC->PerInstanceSMData[Index].Transform.GetOrigin()); + } + + CheckPositions(Positions); + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 1, return true); + + return true; + })); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Part 2: Test baking multiple components to a single actor + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + FHoudiniBakeSettings BakeSettings; + BakeSettings.ActorBakeOption = EHoudiniEngineActorBakeOption::OneActorPerHDA; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + TArray Components; + Actor->GetComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 1, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Components[0]->IsA(), 1, continue); + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 1, return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestBakingInstanceActors, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.InstancedActors", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestBakingInstanceActors::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Tests baking of instanced actors. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "instance_object", "/Script/Engine.Blueprint'/Game/TestObjects/BP_Cube.BP_Cube'" ,0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "max_instances", 100, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "split_instance_meshes", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "foliage", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_origin", false, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two actors + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + return true; + })); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Step 1: Bake the output using ungroup components. We should have one actor per outputs (so 2 in this case), and one component per + // actor + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TArray ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + for (FString& InstanceActorName : OutputObject.InstancedActors) + { + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *InstanceActorName)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + ActorNames.Add(*OutputObject.Actor); + } + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 100, return true); + + return true; + })); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Part 2: Test baking multiple components to a single actor + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + FHoudiniBakeSettings BakeSettings; + BakeSettings.ActorBakeOption = EHoudiniEngineActorBakeOption::OneActorPerHDA; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TArray ActorNames; + AActor * ParentActor = nullptr; + + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + ParentActor = Actor; + + TArray Components; + Actor->GetComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 0, continue); + + ActorNames.Add(*OutputObject.Actor); + } + } + + // We should have found only one actor. + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 1, return true); + + TArray ChildActors; + if (ParentActor) + { + ParentActor->GetAttachedActors(ChildActors); + } + HOUDINI_TEST_EQUAL_ON_FAIL(ChildActors.Num(), 100, return true); + + return true; + })); + + return true; +} + + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestBakingSplitInstanceMeshes, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.SplitInstances", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestBakingSplitInstanceMeshes::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking of split instances. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "instance_object", "/Script/Engine.StaticMesh'/Game/TestObjects/SM_Cube.SM_Cube'", 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "max_instances", 100, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "split_instance_meshes", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "foliage", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_origin", true, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two actors + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + return true; + })); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Step 1: Bake the output using ungroup components. We should have one actor per outputs (so 2 in this case), and one component per + // actor + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TArray ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + FString InstanceActorName = OutputObject.Actor; + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *InstanceActorName)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 4, return true); + + return true; + })); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Part 2: Test baking multiple components to a single actor + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + FHoudiniBakeSettings BakeSettings; + BakeSettings.ActorBakeOption = EHoudiniEngineActorBakeOption::OneActorPerHDA; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have the instances. Build an array of instances. + + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + TArray Components; + Actor->GetComponents(Components); + + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 4, continue); + + // Each component should have a different transform using unreal_instance_origin. Since we don't know which + // component came from which split we need to look them up by position. We have 4 instancers, each containing + // 25 points. + + TArray Origins = { + GetHDAInstancePosition(0), + GetHDAInstancePosition(25), + GetHDAInstancePosition(50), + GetHDAInstancePosition(75) + }; + + for(int Index = 0; Index < Components.Num(); Index++) + { + int OriginIndex = 0; + for(; OriginIndex < Origins.Num(); OriginIndex++) + { + const FTransform RelativeTransform = Components[Index]->GetRelativeTransform(); + if (RelativeTransform.GetLocation().Equals(Origins[OriginIndex], 0.1)) + break; + } + if (OriginIndex == Origins.Num()) + { + TestEqual(TEXT("Failed to find a component with an expected origin"), false, true); + return true; + } + + HOUDINI_TEST_EQUAL_ON_FAIL(Components[Index]->IsA(), 1, continue); + HOUDINI_TEST_EQUAL(Components[Index]->GetNumRenderInstances(), 25); + const FTransform & Transform = Components[Index]->GetRelativeTransform(); + + TArray InstancePositions; + InstancePositions.SetNum(Components[Index]->GetNumRenderInstances()); + for (int InstanceIndex = 0; InstanceIndex < Components[Index]->PerInstanceSMData.Num(); InstanceIndex++) + { + InstancePositions[InstanceIndex] = + Transform.TransformPosition(Components[Index]->PerInstanceSMData[InstanceIndex].Transform.GetOrigin()); + } + CheckPositions(InstancePositions, OriginIndex * 25); + } + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 1, return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestSingleInstancedMesh, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.SingleInstancedMeshes", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestSingleInstancedMesh::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking of a single instance of a mesh. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "instance_object", "/Script/Engine.StaticMesh'/Game/TestObjects/SM_Cube.SM_Cube'", 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "max_instances", 1, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "split_instance_meshes", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "foliage", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_origin", false, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + TArray StaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshOutputs.Num(), 1, return true); + return true; + })); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Step 1: Bake the output using ungroup components. We should have one actor per outputs (so 2 in this case), and one component per + // actor + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + TArray Components; + Actor->GetComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 1, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Components[0]->IsA(), 1, continue); + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 1, return true); + + return true; + })); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Part 2: Test baking multiple components to a single actor + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + FHoudiniBakeSettings BakeSettings; + BakeSettings.ActorBakeOption = EHoudiniEngineActorBakeOption::OneActorPerHDA; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + TArray Components; + Actor->GetComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 1, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Components[0]->IsA(), 1, continue); + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 1, return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestInstancesHSM, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.HierarchicalInstancedStaticMeshes", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestInstancesHSM::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Tests baking of hierarchical instanced meshes. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "instance_object", "/Script/Engine.StaticMesh'/Game/TestObjects/SM_LODs.SM_LODs'", 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "max_instances", 100, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "split_instance_meshes", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "foliage", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_origin", false, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + TArray StaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshOutputs.Num(), 1, return true); + return true; + })); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Step 1: Bake the output using ungroup components. We should have one actor per outputs (so 2 in this case), and one component per + // actor + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + TArray Components; + Actor->GetComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 1, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Components[0]->IsA(), 1, continue); + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 1, return true); + + return true; + })); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Part 2: Test baking multiple components to a single actor + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + BakeSettings.ActorBakeOption = EHoudiniEngineActorBakeOption::OneActorPerHDA; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + TArray Components; + Actor->GetComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 1, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Components[0]->IsA(), 1, continue); + + UHierarchicalInstancedStaticMeshComponent* ISMC = Components[0]; + TArray Positions; + Positions.SetNum(ISMC->PerInstanceSMData.Num()); + for (int Index = 0; Index < ISMC->PerInstanceSMData.Num(); Index++) + Positions[Index] = ISMC->PerInstanceSMData[Index].Transform.GetOrigin(); + + CheckPositions(Positions); + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 1, return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPackedInstances, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.PackedInstances", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPackedInstances::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Tests baking of packed instanced meshes. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, PackedInstancesHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "num_copies", 3, 0); + Context->StartCookingHDA(); + return true; + })); + + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 2, return true); + TArray InstancedStaticMeshComponents = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(InstancedStaticMeshComponents.Num(), 1, return true); + auto InstancedStaticMeshComponent = InstancedStaticMeshComponents[0]; + + + HOUDINI_TEST_EQUAL_ON_FAIL(InstancedStaticMeshComponent->GetNumRenderInstances(), 3, return true); + + // Check expected positions and scales on each object. + float PrevScale = 1.0f; + for(int Index = 0; Index < 3; Index++) + { + auto InstanceTransform = InstancedStaticMeshComponent->PerInstanceSMData[Index].Transform; + FVector Pos = InstanceTransform.GetOrigin(); + FVector ExpectedPos = FVector(Index * 200.0, 0.0, 0.0); + HOUDINI_TEST_EQUAL(Pos, ExpectedPos); + + FVector Scale = InstanceTransform.GetScaleVector(); + FVector ExpectedScale = FVector(PrevScale, PrevScale, PrevScale); + HOUDINI_TEST_EQUAL(Scale, ExpectedScale); + PrevScale *= 1.5f; + + } + + TArray StaticMeshes = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshes.Num(), 1, return true); + + return true; + })); + + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestSinglePackedInstance, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.SinglePackedInstance", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestSinglePackedInstance::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Tests baking of 1 packed instanced meshes - this should result in a single instance. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, PackedInstancesHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "num_copies", 1, 0); + Context->StartCookingHDA(); + return true; + })); + + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 2, return true); + TArray InstancedStaticMeshComponents = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(InstancedStaticMeshComponents.Num(), 0, return true); + + TArray StaticMeshComponents = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshComponents.Num(), 1, return true); + + + TArray StaticMeshes = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshes.Num(), 1, return true); + + + return true; + })); + + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestFoliageStaticMesh, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.FoliageStaticMesh", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestFoliageStaticMesh::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking using a custom mesh. A foliage type should be created. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + // Set parameters. + + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "instance_object", "/Script/Engine.StaticMesh'/Game/TestObjects/SM_Cube.SM_Cube'", 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "max_instances", 100, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "split_instance_meshes", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "foliage", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_origin", false, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + // Post cook, check results. + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + auto OutputObjects = FHoudiniEditorUnitTestUtils::GetOutputsWithFoliageType(Outputs); + + + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObjects.Num(), 1, return true); + UFoliageType * FoliageType = OutputObjects[0]->FoliageType; + + TArray Instances = GetAllFoliageInstances(Context->GetWorld(), FoliageType); + HOUDINI_TEST_EQUAL(Instances.Num(), 100); + + TArray Positions; + Positions.SetNum(Instances.Num()); + for(int Index = 0; Index < Instances.Num(); Index++) + Positions[Index] = Instances[Index].Location; + + CheckPositions(Positions); + + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + // Post bake, check results. + + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + HOUDINI_TEST_NOT_NULL(OutputObject.FoliageType.Get()); + HOUDINI_TEST_EQUAL(OutputObject.FoliageInstancePositions.Num(), 100); + + CheckPositions(OutputObject.FoliageInstancePositions); + + } + } + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestFoliageUserFoliageType, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.UserFoliageType", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestFoliageUserFoliageType::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking user a custom foliage type. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + const char* UserFoliageType = "/Script/Foliage.FoliageType_InstancedStaticMesh'/Game/TestObjects/FoliageType.FoliageType'"; + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, UserFoliageType]() + { + // Set parameters + + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "instance_object", UserFoliageType, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "max_instances", 100, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "split_instance_meshes", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "foliage", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instance_origin", false, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + // Post Cook, test. + + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + auto OutputObjects = FHoudiniEditorUnitTestUtils::GetOutputsWithFoliageType(Outputs); + + + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObjects.Num(), 1, return true); + UFoliageType* FoliageType = OutputObjects[0]->FoliageType; + + TArray Instances = GetAllFoliageInstances(Context->GetWorld(), FoliageType); + HOUDINI_TEST_EQUAL(Instances.Num(), 100); + + UFoliageType* UserFoliageType = Cast(OutputObjects[0]->UserFoliageType); + HOUDINI_TEST_NOT_NULL(UserFoliageType); + + TArray Positions; + Positions.SetNum(Instances.Num()); + for (int Index = 0; Index < Instances.Num(); Index++) + Positions[Index] = Instances[Index].Location; + + CheckPositions(Positions); + + return true; + })); + + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, UserFoliageType]() + { + // Bake + + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + HOUDINI_TEST_NOT_NULL(OutputObject.FoliageType.Get()); + HOUDINI_TEST_EQUAL(OutputObject.FoliageInstancePositions.Num(), 100); + + CheckPositions(OutputObject.FoliageInstancePositions); + + FString AssetName(UserFoliageType); + + UFoliageType* UserFoliageTypeObject = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *AssetName));; + + HOUDINI_TEST_EQUAL(UserFoliageTypeObject, OutputObject.FoliageType.Get()); + + TArray FoliageTypes = GetAllFoliageTypes(Context->GetWorld()); + HOUDINI_TEST_EQUAL_ON_FAIL(FoliageTypes.Num(), 1, return true); + HOUDINI_TEST_EQUAL(FoliageTypes[0], UserFoliageTypeObject); + } + } + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestLevelInstances, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.LevelInstances", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestLevelInstances::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking of a single instance of a mesh. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "instance_object", "/Script/Engine.World'/Game/TestObjects/LevelInstance.LevelInstance'", 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "max_instances", 10, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "split_instance_meshes", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "foliage", false, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + TArray LevelInstances = FHoudiniEditorUnitTestUtils::GetOutputsWithActor(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(LevelInstances.Num(), 1, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(LevelInstances[0]->OutputActors.Num(), 10, return true); + + for(int ActorIndex = 0; ActorIndex < 10; ActorIndex++) + { + ALevelInstance * LevelInstance = Cast(LevelInstances[0]->OutputActors[ActorIndex].Get()); + HOUDINI_TEST_NOT_NULL(LevelInstance); + } + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + ActorNames.Append(OutputObject.LevelInstanceActors); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 10, return true); + + return true; + })); + + return true; +} + + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestActorInstances, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.ActorInstances", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestActorInstances::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking of a single instance of a mesh. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "instance_object", "/Script/Engine.Blueprint'/Game/TestObjects/BP_Cube.BP_Cube'", 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "max_instances", 10, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "split_instance_meshes", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "foliage", false, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + ActorNames.Append(OutputObject.InstancedActors); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 10, return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestProxyMeshInstances, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.ProxyMeshes", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestProxyMeshInstances::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking of a single instance of a mesh. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(true); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "instance_object", "/Script/HoudiniEngineRuntime.HoudiniStaticMesh'/Game/TestObjects/HoudiniMesh.HoudiniMesh'", 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "max_instances", 1, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "split_instance_meshes", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "foliage", false, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPDGInstances, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.PDGInstances.NoCommandlet", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPDGInstances::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test PDG. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + // Make sure commandlet is stopped. + FHoudiniEngine::Get().StopPDGCommandlet(); + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, PDGHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(true); + + // HDA Path and kick Cook. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FString TempDir = FPaths::ProjectIntermediateDir() / "Temp"; + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "working_dir", TempDir, 0); + + Context->StartCookingHDA(); + return true; + })); + + // kick PDG Cook. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingSelectedTOPNetwork(); + return true; + })); + + // Bake and check results. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + UHoudiniPDGAssetLink* AssetLink = Context->GetPDGAssetLink(); + UTOPNetwork* Network = AssetLink->GetTOPNetwork(0); + HOUDINI_TEST_NOT_NULL(Network); + + UTOPNode* Node = nullptr; + for (UTOPNode* It : Network->AllTOPNodes) + { + if (It->NodeName == "HE_OUT_X") + { + Node = It; + break; + } + } + HOUDINI_TEST_NOT_NULL(Node); + + if (!Node) + return false; + + HOUDINI_TEST_EQUAL_ON_FAIL(Node->WorkResult.Num(), 10, return true); + + + for( auto& Result : Node->WorkResult) + { + auto ResultOutputs = Result.ResultObjects[0].GetResultOutputs(); + HOUDINI_TEST_EQUAL_ON_FAIL(ResultOutputs.Num(), 1, return true); + + UHoudiniOutput * Output = ResultOutputs[0]; + + TArray OutputObjects; + Output->GetOutputObjects().GenerateValueArray(OutputObjects); + + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObjects.Num(), 1, return true); + + const FHoudiniOutputObject & OutputObject = OutputObjects[0]; + + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObject.OutputComponents.Num(), 1, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObject.OutputComponents[0]->IsA(UInstancedStaticMeshComponent::StaticClass()), 1, return true); + } + + return true; + })); + + return true; +} + + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPDGInstancesAsync, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.PDGInstances.Commandlet", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPDGInstancesAsync::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test PDG. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + // Disable for now + return true; + + + FHoudiniEngineCommands::SetPDGCommandletEnabled(true); + FHoudiniEngineCommands::StartPDGCommandlet(); + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, PDGHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(true); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + return FHoudiniEngine::Get().IsPDGCommandletConnected(); + })); + + // HDA Path and kick Cook. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FString TempDir = FPaths::ProjectIntermediateDir() / "Temp"; + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "working_dir", TempDir, 0); + + Context->StartCookingHDA(); + return true; + })); + + // kick PDG Cook. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingSelectedTOPNetwork(); + return true; + })); + + // Bake and check results. + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + UHoudiniPDGAssetLink* AssetLink = Context->GetPDGAssetLink(); + UTOPNetwork* Network = AssetLink->GetTOPNetwork(0); + HOUDINI_TEST_NOT_NULL(Network); + + UTOPNode* Node = nullptr; + for (UTOPNode* It : Network->AllTOPNodes) + { + if (It->NodeName == "HE_OUT_X") + { + Node = It; + break; + } + } + HOUDINI_TEST_NOT_NULL(Node); + if (!Node) + return false; + + HOUDINI_TEST_EQUAL_ON_FAIL(Node->WorkResult.Num(), 10, return true); + + + for (auto& Result : Node->WorkResult) + { + auto ResultOutputs = Result.ResultObjects[0].GetResultOutputs(); + HOUDINI_TEST_EQUAL_ON_FAIL(ResultOutputs.Num(), 1, return true); + + UHoudiniOutput* Output = ResultOutputs[0]; + + TArray OutputObjects; + Output->GetOutputObjects().GenerateValueArray(OutputObjects); + + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObjects.Num(), 1, return true); + + const FHoudiniOutputObject& OutputObject = OutputObjects[0]; + + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObject.OutputComponents.Num(), 1, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObject.OutputComponents[0]->IsA(UInstancedStaticMeshComponent::StaticClass()), 1, return true); + } + + FHoudiniEngineCommands::StopPDGCommandlet(); + FHoudiniEngineCommands::SetPDGCommandletEnabled(false); + return true; + })); + + return true; +} + + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestSplitInstanceMeshesMaterials, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.SplitInstancesMaterials", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestSplitInstanceMeshesMaterials::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking of split instances. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "instance_object", "/Script/Engine.StaticMesh'/Game/TestObjects/SM_Cube.SM_Cube'", 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "max_instances", 100, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "split_instance_meshes", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "foliage", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "custom_materials", true, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two actors + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + return true; + })); + + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TArray ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + FString InstanceActorName = OutputObject.Actor; + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *InstanceActorName)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 4, return true); + + return true; + })); + + + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + FHoudiniBakeSettings BakeSettings; + BakeSettings.ActorBakeOption = EHoudiniEngineActorBakeOption::OneActorPerHDA; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have the instances. Build an array of instances. + + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + const int NumInstanceComponents = 4; + + TArray Components; + Actor->GetComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), NumInstanceComponents, continue); + for (int Index = 0; Index < NumInstanceComponents; Index++) + { + HOUDINI_TEST_EQUAL_ON_FAIL(Components[Index]->IsA(), 1, continue); + HOUDINI_TEST_EQUAL(Components[Index]->GetNumRenderInstances(), 25); + + UMaterial * Material = Cast(Components[Index]->GetMaterial(0)); + FString MaterialName = Material->GetPathName(); + FString ExpectName = FString::Printf(TEXT("/Game/TestObjects/InstanceMaterial_%d.InstanceMaterial_%d"), Index, Index); + HOUDINI_TEST_EQUAL(MaterialName, ExpectName); + + + } + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 1, return true); + + TArray InstancePositions; + InstancePositions.Reserve(100); + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *(*ActorNames.CreateConstIterator()))); + TArray Components; + Actor->GetComponents(Components); + for (int Index = 0; Index < Components.Num(); Index++) + { + for (int InstanceIndex = 0; InstanceIndex < Components[Index]->PerInstanceSMData.Num(); InstanceIndex++) + { + InstancePositions.Add(Components[Index]->PerInstanceSMData[InstanceIndex].Transform.GetOrigin()); + } + } + + HOUDINI_TEST_EQUAL(InstancePositions.Num(), 100); + InstancePositions.Sort([](const FVector& First, const FVector& Second) { return First.X < Second.X; }); + CheckPositions(InstancePositions); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestSplitInstanceCustomFloats, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.SplitInstanceCustomData", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestSplitInstanceCustomFloats::RunTest(const FString& Parameters) +{ + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, BakingHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "instance_object", "/Script/Engine.StaticMesh'/Game/TestObjects/SM_Cube.SM_Cube'", 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "max_instances", 100, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "split_instance_meshes", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "foliage", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "custom_floats", true, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two actors + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + return true; + })); + + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TArray ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + FString InstanceActorName = OutputObject.Actor; + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *InstanceActorName)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 4, return true); + + return true; + })); + + + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + FHoudiniBakeSettings BakeSettings; + BakeSettings.ActorBakeOption = EHoudiniEngineActorBakeOption::OneActorPerHDA; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have the instances. Build an array of instances. + + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 1, return true); + + TArray InstancePositions; + InstancePositions.Reserve(100); + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *(*ActorNames.CreateConstIterator()))); + TArray Components; + Actor->GetComponents(Components); + for (int Index = 0; Index < Components.Num(); Index++) + { + for (int InstanceIndex = 0; InstanceIndex < Components[Index]->PerInstanceSMData.Num(); InstanceIndex++) + { + InstancePositions.Add(Components[Index]->PerInstanceSMData[InstanceIndex].Transform.GetOrigin()); + } + + HOUDINI_TEST_EQUAL_ON_FAIL(Components[Index]->IsA(), 1, continue); + HOUDINI_TEST_EQUAL(Components[Index]->GetNumRenderInstances(), 25); + + HOUDINI_TEST_EQUAL(Components[Index]->NumCustomDataFloats, Index + 1); + + for (int Instance = 0; Instance < Components[Index]->GetNumRenderInstances(); Instance++) + { + for (int CustomFloatIndex = 0; CustomFloatIndex < Components[Index]->NumCustomDataFloats; CustomFloatIndex++) + { + // Use unique calculated values so we can test values arrived in the correct slots. + float ExpectedValue = Index * 10000 + Instance * 100 + CustomFloatIndex; + float ActualValue = Components[Index]->PerInstanceSMCustomData[Instance * Components[Index]->NumCustomDataFloats + CustomFloatIndex]; + HOUDINI_TEST_EQUAL(ActualValue, ExpectedValue); + } + } + } + + HOUDINI_TEST_EQUAL(InstancePositions.Num(), 100); + InstancePositions.Sort([](const FVector& First, const FVector& Second) { return First.X < Second.X; }); + CheckPositions(InstancePositions); + + return true; + })); + + return true; +} + + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestSplitPackedInstancer, FHoudiniInstanceAutomationTest, "Houdini.UnitTests.Instances.SplitPackedInstancer", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestSplitPackedInstancer::RunTest(const FString& Parameters) +{ + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, SplitPackedInstancesHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two actors + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 2, return true); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + + FHoudiniBakeSettings BakeSettings; + BakeSettings.ActorBakeOption = EHoudiniEngineActorBakeOption::OneActorPerHDA; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 2, return true); + + // Go through each output and check we have the instances. Build an array of instances. + + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + if (!OutputObject.Actor.IsEmpty()) + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 1, return true); + + TArray InstancePositions; + InstancePositions.Reserve(100); + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *(*ActorNames.CreateConstIterator()))); + TArray Components; + Actor->GetComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 3, return true); + + for (int Index = 0; Index < Components.Num(); Index++) + { + for (int InstanceIndex = 0; InstanceIndex < Components[Index]->PerInstanceSMData.Num(); InstanceIndex++) + { + InstancePositions.Add(Components[Index]->PerInstanceSMData[InstanceIndex].Transform.GetOrigin()); + } + + HOUDINI_TEST_EQUAL_ON_FAIL(Components[Index]->IsA(), 1, continue); + HOUDINI_TEST_EQUAL(Components[Index]->GetNumRenderInstances(), 10); + } + + return true; + })); + + return true; +} + + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestInstances.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" +#include "FoliageType.h" +#include "HoudiniEditorTestUtils.h" +#include "InstancedFoliage.h" + +struct FHoudiniInstanceAutomationTest : public FHoudiniAutomationTest +{ + FHoudiniInstanceAutomationTest(const FString& InName, const bool bInComplexTask) + : FHoudiniAutomationTest(InName, bInComplexTask) + { + } + + void CheckPositions(const TArray & Positions, int Start = 0); + + const static inline FString BakingHDA = TEXT("/Game/TestHDAs/Instances/Test_Instances"); + const static inline FString PDGHDA = TEXT("/Game/TestHDAs/Instances/Test_PDGInstances"); + const static inline FString InstancedMesh = TEXT("/Script/Engine.StaticMesh'/Game/TestObjects/SM_Cube.SM_Cube'"); + const static inline FString InstancedActor = TEXT("/Script/Engine.Blueprint'/Game/TestObjects/BP_Cube.BP_Cube'"); + const static inline FString PackedInstancesHDA = TEXT("/Game/TestHDAs/Instances/Test_PackedInstances"); + const static inline FString SplitPackedInstancesHDA = TEXT("/Game/TestHDAs/Instances/Test_SplitPackedInstances"); + + static TArray GetAllFoliageInstances(UWorld* InWorld, UFoliageType* FoliageType); + static TArray GetAllFoliageTypes(UWorld* InWorld); + + static FVector GetHDAInstancePosition(int Index); + +}; + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestLandscapeSplines.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestLandscapes.h" + +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterToggle.h" +#include "Landscape.h" +#include "LandscapeSplineControlPoint.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Chaos/HeightField.h" +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "HoudiniEditorUnitTestUtils.h" +#include "LandscapeEdit.h" + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestLandscapeSplines_Simple, "Houdini.UnitTests.LandscapeSplines.Simple", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestLandscapeSplines_Simple::RunTest(const FString& Parameters) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// This test various aspects of Landscapes Splines. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, TEXT("/Game/TestHDAs/LandscapeSplines/Test_LandscapeSpline"), FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Create a small landscape and check it loads. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + // SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "size", LandscapeSize, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 2, return true); + + TArray SplineOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(SplineOutputs.Num(), 1, return true); + + TArray LandscapeOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL(LandscapeOutputs.Num(), 1); + ALandscape* LandscapeActor = LandscapeOutputs[0]->Landscape; + + // + // Check control points. 6 are specified in the Test HDA but only 4 should be output (the first and last + those marked + // with >= 0 ids). + // + + ULandscapeSplinesComponent * SplineComponent = SplineOutputs[0]->GetLandscapeSplinesComponent(); + auto ControlPoints = SplineComponent->GetControlPoints(); + + FTransform Transform = SplineComponent->GetComponentTransform(); + HOUDINI_TEST_EQUAL_ON_FAIL(ControlPoints.Num(), 4, return true); + TArray Positions; + for (int Index = 0; Index < ControlPoints.Num(); Index++) + { + Positions.Add(Transform.TransformPosition(ControlPoints[Index]->Location)); + } + HOUDINI_TEST_EQUAL(Positions[0], FVector(-50000.0, -50000.0, 0.0), 0.1); + HOUDINI_TEST_EQUAL(Positions[1], FVector(0.0, -50000.0, 0.0), 0.1); + HOUDINI_TEST_EQUAL(Positions[2], FVector(0.0, 50000.0, 0.0), 0.1); + HOUDINI_TEST_EQUAL(Positions[3], FVector(0.0, 0.0, 0.0), 0.1); + + HOUDINI_TEST_EQUAL(IsValid(ControlPoints[1]->Mesh), true); + HOUDINI_TEST_EQUAL(ControlPoints[1]->MeshScale, FVector(99.0, 99.0, 99.0)); + + // + // Check segments + // + auto Segments = SplineComponent->GetSegments(); + HOUDINI_TEST_EQUAL_ON_FAIL(Segments.Num(), 3, return true); + + return true; + })); + + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Done + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + +#endif + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestLandscapeSplines_WorldPartition, "Houdini.UnitTests.LandscapeSplines.WorldPartition", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestLandscapeSplines_WorldPartition::RunTest(const FString& Parameters) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// This test various aspects of Landscapes. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, TEXT("/Game/TestHDAs/LandscapeSplines/Test_LandscapeSpline"), FTransform::Identity, true)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Create a small landscape and check it loads. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + // SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "size", LandscapeSize, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 2, return true); + + TArray SplineOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(SplineOutputs.Num(), 1, return true); + + TArray LandscapeOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL(LandscapeOutputs.Num(), 1); + ALandscape* LandscapeActor = LandscapeOutputs[0]->Landscape; + + // + // Check control points. 6 are specified in the Test HDA but only 4 should be output (the first and last + those marked + // with >= 0 ids). + // + + ULandscapeSplinesComponent* SplineComponent = SplineOutputs[0]->GetLandscapeSplinesComponent(); + auto ControlPoints = SplineComponent->GetControlPoints(); + + FTransform Transform = SplineComponent->GetComponentTransform(); + HOUDINI_TEST_EQUAL_ON_FAIL(ControlPoints.Num(), 4, return true); + TArray Positions; + for (int Index = 0; Index < ControlPoints.Num(); Index++) + { + Positions.Add(Transform.TransformPosition(ControlPoints[Index]->Location)); + } + HOUDINI_TEST_EQUAL(Positions[0], FVector(-50000.0, -50000.0, 0.0), 0.1); + HOUDINI_TEST_EQUAL(Positions[1], FVector(0.0, -50000.0, 0.0), 0.1); + HOUDINI_TEST_EQUAL(Positions[2], FVector(0.0, 50000.0, 0.0), 0.1); + HOUDINI_TEST_EQUAL(Positions[3], FVector(0.0, 0.0, 0.0), 0.1); + + HOUDINI_TEST_EQUAL(IsValid(ControlPoints[1]->Mesh), true); + HOUDINI_TEST_EQUAL(ControlPoints[1]->MeshScale, FVector(99.0, 99.0, 99.0)); + + // + // Check segments + // + auto Segments = SplineComponent->GetSegments(); + HOUDINI_TEST_EQUAL_ON_FAIL(Segments.Num(), 3, return true); + + return true; + })); + + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Done + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#endif + return true; +} + +#endif +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestLandscapeSplines.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#include "Landscape.h" +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +class FHoudiniEditorTestLandscapeSplines +{ +public: + + +}; + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestLandscapes.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestLandscapes.h" + +#include "HoudiniCookable.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterToggle.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "Chaos/HeightField.h" +#include "Landscape.h" + +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" +#include "HoudiniEditorUnitTestUtils.h" +#include + +#include "Components/InstancedStaticMeshComponent.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "LandscapeEdit.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 +#include "LandscapeEditLayer.h" +#endif +#include "Misc/AutomationTest.h" + + +TArray FHoudiniEditorTestLandscapes::CheckLandscapeValues( + TArray & Results, + TArray & ExpectedResults, + const FIntPoint & Size, + float AbsError, int MaxErrors) +{ + TArray Errors; + + for(int Y = 0; Y < Size.Y; Y++) + { + for (int X = 0; X < Size.X; X++) + { + int ResultIndex = X + Y * Size.X; + int ExpectedIndex = X + Y * Size.X; + float ExpectedValue = ExpectedResults[ExpectedIndex]; + + float AbsDiff = FMath::Abs(ExpectedValue - Results[ResultIndex]); + if (AbsDiff > AbsError) + { + FString Error = FString::Printf(TEXT("(%d, %d) Expected %.2f but got %.2f"), X, Y, ExpectedValue, Results[ResultIndex]); + Errors.Add(Error); + if (Errors.Num() == MaxErrors) + { + FString Terminator = FString(TEXT("... skipping additional Height Field Checks...")); + Errors.Add(Terminator); + return Errors; + } + } + } + } + return Errors; +} + +TArray FHoudiniEditorTestLandscapes::GetLandscapeHeightValues(ALandscape * LandscapeActor) +{ + FIntPoint LandscapeQuadSize = LandscapeActor->GetBoundingRect().Size(); + FIntPoint LandscapeVertSize(LandscapeQuadSize.X + 1, LandscapeQuadSize.Y + 1); + + TArray HoudiniValues; + TArray Values; + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + auto MyEditLayer = LandscapeActor->GetLayerConst(0); +#else + auto MyEditLayer = LandscapeActor->GetLayer(0); +#endif + int NumPoints = LandscapeVertSize.X * LandscapeVertSize.Y; + Values.SetNum(NumPoints); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FGuid LayerGuid = MyEditLayer->EditLayer->GetGuid(); +#else + FGuid LayerGuid = MyEditLayer->Guid; +#endif + FScopedSetLandscapeEditingLayer Scope(LandscapeActor, LayerGuid, [&] {}); + + FLandscapeEditDataInterface LandscapeEdit(LandscapeActor->GetLandscapeInfo()); + LandscapeEdit.SetShouldDirtyPackage(false); + LandscapeEdit.GetHeightDataFast(0, 0, LandscapeVertSize.X - 1, LandscapeVertSize.Y - 1, Values.GetData(), 0); + } + + FTransform LandscapeTransform = LandscapeActor->GetActorTransform(); + + HoudiniValues.SetNum(Values.Num()); + + float ZScale = LandscapeTransform.GetScale3D().Z / 100.0f; + for (int Index = 0; Index < Values.Num(); Index++) + { + // https://docs.unrealengine.com/4.27/en-US/BuildingWorlds/Landscape/TechnicalGuide/ + float HoudiniValue = ZScale * (static_cast(Values[Index]) - 32768.0f) / 128.0f; + HoudiniValues[Index] = HoudiniValue; + } + + return HoudiniValues; +} + +TArray FHoudiniEditorTestLandscapes::CreateExpectedHeightValues(const FIntPoint & ExpectedSize, float HeightScale) +{ + TArray ExpectedResults; + ExpectedResults.SetNum(ExpectedSize.X * ExpectedSize.Y); + for (int Y = 0; Y < ExpectedSize.Y; Y++) + { + for (int X = 0; X < ExpectedSize.X; X++) + { + int Index = X + Y * ExpectedSize.X; + + // This line should mimic what the height field wrangle node is doing. + ExpectedResults[Index] = HeightScale * (Y/* * 2 + Y*/); + } + } + return ExpectedResults; +} + +float FHoudiniEditorTestLandscapes::GetMin(const TArray& Values) +{ + float MinValue = TNumericLimits::Max(); + for( float Value : Values) + MinValue = FMath::Min(MinValue, Value); + return MinValue; +} + +float FHoudiniEditorTestLandscapes::GetMax(const TArray& Values) +{ + float MaxValue = TNumericLimits::Min(); + for (float Value : Values) + MaxValue = FMath::Max(MaxValue, Value); + + return MaxValue; +} + +TArray FHoudiniEditorTestLandscapes::Resize(TArray& In, const FIntPoint& OriginalSize, const FIntPoint& NewSize) +{ + TArray Result; + Result.SetNum(NewSize.X * NewSize.Y); + + float XScale = float(OriginalSize.X - 1) / float(NewSize.X - 1); + float YScale = float(OriginalSize.Y - 1) / float(NewSize.Y - 1); + + for (int32 Y = 0; Y < NewSize.Y; ++Y) + { + for (int32 X = 0; X < NewSize.X; ++X) + { + float OldX = X * XScale; + float OldY = Y * YScale; + int32 X0 = FMath::FloorToInt(OldX); + int32 X1 = FMath::Min(FMath::FloorToInt(OldX) + 1, OriginalSize.X - 1); + int32 Y0 = FMath::FloorToInt(OldY); + int32 Y1 = FMath::Min(FMath::FloorToInt(OldY) + 1, OriginalSize.Y - 1); + float Original00 = In[X0 * OriginalSize.Y + Y0]; + float Original10 = In[X1 * OriginalSize.Y + Y0]; + float Original01 = In[X0 * OriginalSize.Y + Y1]; + float Original11 = In[X1 * OriginalSize.Y + Y1]; + float NewValue = FMath::BiLerp(Original00, Original10, Original01, Original11, FMath::Fractional(OldX), FMath::Fractional(OldY)); + Result[X * NewSize.Y + Y] = NewValue; + + } + } + return Result; +} + + +TArray +FHoudiniEditorTestLandscapes::CreateExpectedPaintLayer1Values(const FIntPoint& ExpectedSize) +{ + TArray Results; + Results.SetNum(ExpectedSize.X * ExpectedSize.Y); + + FIntPoint Mid(ExpectedSize.X / 2, ExpectedSize.Y / 2); + + + for(int Y = 0; Y < ExpectedSize.Y; Y++) + { + for (int X = 0; X < ExpectedSize.X; X++) + { + int Index = X + Y * ExpectedSize.X; + if (X < Mid.X && Y < Mid.Y) + Results[Index] = 1.0f; + else + Results[Index] = 0.0f; + } + + } + return Results; +} + +TArray +FHoudiniEditorTestLandscapes::CreateExpectedPaintLayer2Values(const FIntPoint& ExpectedSize) +{ + TArray Results; + Results.SetNum(ExpectedSize.X * ExpectedSize.Y); + + FIntPoint Mid(ExpectedSize.X / 2, ExpectedSize.Y / 2); + + for (int Y = 0; Y < ExpectedSize.Y; Y++) + { + for (int X = 0; X < ExpectedSize.X; X++) + { + int Index = X + Y * ExpectedSize.X; + if (X >= Mid.X && Y >= Mid.Y) + Results[Index] = 1.0f; + else + Results[Index] = 0.0f; + } + + } + return Results; +} + +ULandscapeLayerInfoObject* +FHoudiniEditorTestLandscapes::GetLayerInfo(ALandscape* LandscapeActor, const FString& LayerName) +{ + ULandscapeInfo* LandscapeInfo = LandscapeActor->GetLandscapeInfo(); + int TargetLayerIndex = LandscapeInfo->GetLayerInfoIndex(FName(LayerName), LandscapeActor); + if (TargetLayerIndex == -1) + return nullptr; + + FLandscapeInfoLayerSettings LayersSetting = LandscapeInfo->Layers[TargetLayerIndex]; + ULandscapeLayerInfoObject* LayerInfo = LayersSetting.LayerInfoObj; + return LayerInfo; +} + +TArray +FHoudiniEditorTestLandscapes::GetLandscapePaintLayerValues(ALandscape* LandscapeActor, const FString& LayerName) +{ + FIntPoint LandscapeQuadSize = LandscapeActor->GetBoundingRect().Size(); + FIntPoint LandscapeVertSize(LandscapeQuadSize.X + 1, LandscapeQuadSize.Y + 1); + TArray HoudiniValues; + + ULandscapeInfo * LandscapeInfo = LandscapeActor->GetLandscapeInfo(); + ULandscapeLayerInfoObject* LayerInfo = GetLayerInfo(LandscapeActor, LayerName); + + // Calc the X/Y size in points + + // extracting the uint8 values from the layer + FLandscapeEditDataInterface LandscapeEdit(LandscapeInfo); + LandscapeEdit.SetShouldDirtyPackage(false); // Ensure we're not triggering a checkout, as we're just reading data + + TArray Values; + Values.AddZeroed(LandscapeVertSize.X * LandscapeVertSize.Y); + LandscapeEdit.GetWeightDataFast(LayerInfo, 0, 0, LandscapeVertSize.X, LandscapeVertSize.Y, Values.GetData(), 0); + + // Convert to floats + HoudiniValues.SetNum(Values.Num()); + for(int Index = 0; Index < Values.Num(); Index++) + HoudiniValues[Index] = Values[Index] / 255.0f; + + return HoudiniValues; + +} + +TArray FHoudiniEditorTestLandscapes::GetLandscapeEditLayerValues(ALandscape* LandscapeActor, const FString& EditLayer, const FString& TargetLayer, const FIntPoint& Size) +{ + FHoudiniExtents Extents; + + Extents.Min = FIntPoint(0, 0); + Extents.Max = Size; + + TArray Values = FHoudiniLandscapeUtils::GetLayerData(LandscapeActor, Extents, FName(*EditLayer), FName(*TargetLayer)); + + // Convert to floats + TArray HoudiniValues; + HoudiniValues.SetNum(Values.Num()); + for(int Index = 0; Index < Values.Num(); Index++) + HoudiniValues[Index] = Values[Index] / 255.0f; + + return HoudiniValues; +} + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestLandscapes_Simple, "Houdini.UnitTests.Landscapes.SimpleLandscape", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestLandscapes_Simple::RunTest(const FString & Parameters) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// This test various aspects of Landscapes. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, TEXT("/Game/TestHDAs/Landscape/Test_Landscapes"), FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Create a small landscape and check it loads. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + constexpr int LandscapeSize = 64; + { + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, LandscapeSize]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "size", LandscapeSize, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "size", LandscapeSize, 1); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "grid_size", 1, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterFloat, "height_scale", 1.0f, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, LandscapeSize]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have one output. + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + + // Fetch the output as a landscape.. + TArray LandscapeOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL(LandscapeOutputs.Num(), 1); + ALandscape* LandscapeActor = LandscapeOutputs[0]->Landscape; + + const FIntPoint ExpectedGridSize = FIntPoint(LandscapeSize, LandscapeSize); + + // Check the size of the landscape is correct. + FIntPoint LandscapeQuadSize = LandscapeActor->GetBoundingRect().Size(); + FIntPoint LandscapeVertSize(LandscapeQuadSize.X + 1, LandscapeQuadSize.Y + 1); + HOUDINI_TEST_EQUAL(LandscapeVertSize, ExpectedGridSize); + + TArray ExpectedResults = FHoudiniEditorTestLandscapes::CreateExpectedHeightValues(ExpectedGridSize, 1.0f); + TArray HoudiniValues = FHoudiniEditorTestLandscapes::GetLandscapeHeightValues(LandscapeActor); + TArray Errors = FHoudiniEditorTestLandscapes::CheckLandscapeValues(HoudiniValues, ExpectedResults, ExpectedGridSize); + HOUDINI_TEST_EQUAL_ON_FAIL(Errors.Num(), 0, for(auto & Error : Errors) this->AddError(Error); ); + + FBox Bounds = LandscapeActor->GetLoadedBounds(); + + float MinValue = FHoudiniEditorTestLandscapes::GetMin(ExpectedResults); + float MaxValue = FHoudiniEditorTestLandscapes::GetMax(ExpectedResults); + + FVector3d ExpectedSize((ExpectedGridSize.X - 1) * 100.0f, (ExpectedGridSize.Y - 1) * 100.0f, (MaxValue - MinValue) * 100.0f); + FVector3d ActualSize = Bounds.GetSize(); + HOUDINI_TEST_EQUAL(ActualSize.X, ExpectedSize.X); + HOUDINI_TEST_EQUAL(ActualSize.Y, ExpectedSize.Y); + HOUDINI_TEST_EQUAL(ActualSize.Z, ExpectedSize.Z); + + float ZCenter = 100.0f * (MaxValue - MinValue) * 0.5f; + + FVector3d ExpectedCenter = FVector3d(0.0f, 0.0f, ZCenter); + FVector3d ActualCenter = Bounds.GetCenter(); + + float Tolerance = ZCenter * 0.001f; + HOUDINI_TEST_EQUAL(ActualCenter, ExpectedCenter, Tolerance); + + return true; + })); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Done + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// +#endif + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestLandscapes_GridSize, "Houdini.UnitTests.Landscapes.ResizedLandscape", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestLandscapes_GridSize::RunTest(const FString& Parameters) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// This test resizing of landscapes when the original Houdini height field does not fit in an Unreal landscape. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, TEXT("/Game/TestHDAs/Landscape/Test_Landscapes"), FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + const FIntPoint HeightFieldSize(143,63); + { + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, HeightFieldSize]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "size", HeightFieldSize.X, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "size", HeightFieldSize.Y, 1); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "grid_size", 1, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterFloat, "height_scale", 1.0f, 0); + Context->StartCookingHDA(); + return true; + })); + + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, HeightFieldSize]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have one output. + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + + // Fetch the output as a landscape.. + TArray LandscapeOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL(LandscapeOutputs.Num(), 1); + ALandscape* LandscapeActor = LandscapeOutputs[0]->Landscape; + + const FIntPoint ExpectedGridSize = FIntPoint(148, 64); + + // Check the size of the landscape is correct. + FIntPoint LandscapeQuadSize = LandscapeActor->GetBoundingRect().Size(); + FIntPoint LandscapeVertSize(LandscapeQuadSize.X + 1, LandscapeQuadSize.Y + 1); + HOUDINI_TEST_EQUAL_ON_FAIL(LandscapeVertSize, ExpectedGridSize, return true); + + TArray ExpectedResults = FHoudiniEditorTestLandscapes::CreateExpectedHeightValues(HeightFieldSize, 1.0f); + ExpectedResults = FHoudiniEditorTestLandscapes::Resize(ExpectedResults, HeightFieldSize, ExpectedGridSize); + + TArray HoudiniValues = FHoudiniEditorTestLandscapes::GetLandscapeHeightValues(LandscapeActor); + TArray Errors = FHoudiniEditorTestLandscapes::CheckLandscapeValues(HoudiniValues, ExpectedResults, ExpectedGridSize, 1.0f); + for (auto& Error : Errors) + this->AddError(Error); + + HOUDINI_TEST_EQUAL_ON_FAIL(Errors.Num(), 0, return true; ); + + FBox Bounds = LandscapeActor->GetLoadedBounds(); + + float MinValue = FHoudiniEditorTestLandscapes::GetMin(ExpectedResults); + float MaxValue = FHoudiniEditorTestLandscapes::GetMax(ExpectedResults); + + FVector3d ExpectedSize((HeightFieldSize.X - 1) * 100.0f, (HeightFieldSize.Y - 1) * 100.0f, (MaxValue - MinValue) * 100.0f); + FVector3d ActualSize = Bounds.GetSize(); + HOUDINI_TEST_EQUAL(ActualSize.X, ExpectedSize.X); + HOUDINI_TEST_EQUAL(ActualSize.Y, ExpectedSize.Y); + HOUDINI_TEST_EQUAL(ActualSize.Z, ExpectedSize.Z); + + float ZCenter = 100.0f * (MaxValue - MinValue) * 0.5f; + + FVector3d ExpectedCenter = FVector3d(0.0f, 0.0f, ZCenter); + FVector3d ActualCenter = Bounds.GetCenter(); + + float Tolerance = ZCenter * 0.001f; + HOUDINI_TEST_EQUAL(ActualCenter, ExpectedCenter, Tolerance); + + + return true; + })); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Done + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// +#endif + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestLandscapes_TargetLayers, "Houdini.UnitTests.Landscapes.TargetLayers", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestLandscapes_TargetLayers::RunTest(const FString& Parameters) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, TEXT("/Game/TestHDAs/Landscape/Test_Landscapes"), FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + const FIntPoint HeightFieldSize(63, 63); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, HeightFieldSize]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "size", HeightFieldSize.X, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "size", HeightFieldSize.Y, 1); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "grid_size", 1, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterFloat, "height_scale", 1.0f, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "paint_layer_1", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "paint_layer_2", true, 0); + // TODO: Added test for visibility layer too + Context->StartCookingHDA(); + return true; + })); + + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, HeightFieldSize]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have 1 Output with 3 objects. + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + TArray LandscapeOutputs = + FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + + HOUDINI_TEST_EQUAL(LandscapeOutputs.Num(), 3); + ALandscape* LandscapeActor = LandscapeOutputs[0]->Landscape; + + const FIntPoint ExpectedGridSize = FIntPoint(63, 63); + + // Check the size of the landscape is correct. + FIntPoint LandscapeQuadSize = LandscapeActor->GetBoundingRect().Size(); + FIntPoint LandscapeVertSize(LandscapeQuadSize.X + 1, LandscapeQuadSize.Y + 1); + HOUDINI_TEST_EQUAL_ON_FAIL(LandscapeVertSize, ExpectedGridSize, return true); + + FString TempFolder = Context->GetTemporaryCookFolderOrDefault(); + + // Check paint layer 1. + { + FString LayerName = TEXT("paint_layer1"); + + ULandscapeLayerInfoObject* LayerInfo = FHoudiniEditorTestLandscapes::GetLayerInfo(LandscapeActor, LayerName); + + FString ObjectPath = LayerInfo->GetPathName(); + HOUDINI_TEST_EQUAL(FHoudiniEditorUnitTestUtils::IsTemporary( + TempFolder, ObjectPath), true); + + TArray ExpectedResults = FHoudiniEditorTestLandscapes::CreateExpectedPaintLayer1Values(HeightFieldSize); + TArray GeneratedValues = FHoudiniEditorTestLandscapes::GetLandscapePaintLayerValues(LandscapeActor, LayerName); + TArray Errors = FHoudiniEditorTestLandscapes::CheckLandscapeValues(GeneratedValues, ExpectedResults, ExpectedGridSize, 1.0f); + for (auto& Error : Errors) + this->AddError(Error); + + HOUDINI_TEST_EQUAL_ON_FAIL(Errors.Num(), 0, return true; ); + } + + // Check paint layer 2. + { + FString LayerName = TEXT("paint_layer2"); + + ULandscapeLayerInfoObject* LayerInfo = FHoudiniEditorTestLandscapes::GetLayerInfo(LandscapeActor, LayerName); + + FString ObjectPath = LayerInfo->GetPathName(); + HOUDINI_TEST_EQUAL(FHoudiniEditorUnitTestUtils::IsTemporary( + TempFolder, ObjectPath), true); + + TArray ExpectedResults = FHoudiniEditorTestLandscapes::CreateExpectedPaintLayer1Values(HeightFieldSize); + TArray GeneratedValues = FHoudiniEditorTestLandscapes::GetLandscapePaintLayerValues(LandscapeActor, LayerName); + TArray Errors = FHoudiniEditorTestLandscapes::CheckLandscapeValues(GeneratedValues, ExpectedResults, ExpectedGridSize, 1.0f); + for (auto& Error : Errors) + this->AddError(Error); + + HOUDINI_TEST_EQUAL_ON_FAIL(Errors.Num(), 0, return true; ); + } + + return true; + })); +#endif + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestLandscapes_EditLayers, "Houdini.UnitTests.Landscapes.EditLayers", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestLandscapes_EditLayers::RunTest(const FString& Parameters) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, TEXT("/Game/TestHDAs/Landscape/Test_Landscapes"), FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + const FIntPoint HeightFieldSize(63, 63); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, HeightFieldSize]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "size", HeightFieldSize.X, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "size", HeightFieldSize.Y, 1); + SET_HDA_PARAMETER(Context, UHoudiniParameterInt, "grid_size", 1, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterFloat, "height_scale", 1.0f, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "paint_layer_1", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "paint_layer_2", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "height_edit_layer", true, 0); + // TODO: Added test for visibility layer too + Context->StartCookingHDA(); + return true; + })); + + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, HeightFieldSize]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have 1 Output with 3 objects. + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + TArray LandscapeOutputs = + FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL(LandscapeOutputs.Num(), 3); + + ALandscape* LandscapeActor = LandscapeOutputs[0]->Landscape; + const FIntPoint ExpectedGridSize = FIntPoint(63, 63); + + // Check the size of the landscape is correct. + FIntPoint LandscapeQuadSize = LandscapeActor->GetBoundingRect().Size(); + FIntPoint LandscapeVertSize(LandscapeQuadSize.X + 1, LandscapeQuadSize.Y + 1); + HOUDINI_TEST_EQUAL_ON_FAIL(LandscapeVertSize, ExpectedGridSize, return true); + + FString TempFolder = Context->GetTemporaryCookFolderOrDefault(); + + // Check paint layer 1. + { + FString LayerName = TEXT("paint_layer1"); + ULandscapeLayerInfoObject* LayerInfo = FHoudiniEditorTestLandscapes::GetLayerInfo(LandscapeActor, LayerName); + + FString ObjectPath = LayerInfo->GetPathName(); + HOUDINI_TEST_EQUAL(FHoudiniEditorUnitTestUtils::IsTemporary( + TempFolder, ObjectPath), true); + + TArray ExpectedResults = FHoudiniEditorTestLandscapes::CreateExpectedPaintLayer1Values(HeightFieldSize); + TArray GeneratedValues = FHoudiniEditorTestLandscapes::GetLandscapePaintLayerValues(LandscapeActor, LayerName); + TArray Errors = FHoudiniEditorTestLandscapes::CheckLandscapeValues(GeneratedValues, ExpectedResults, ExpectedGridSize, 1.0f); + for(auto& Error : Errors) + this->AddError(Error); + + HOUDINI_TEST_EQUAL_ON_FAIL(Errors.Num(), 0, return true; ); + } + + // Check paint layer 2. + { + FString LayerName = TEXT("paint_layer2"); + + ULandscapeLayerInfoObject* LayerInfo = FHoudiniEditorTestLandscapes::GetLayerInfo(LandscapeActor, LayerName); + + FString ObjectPath = LayerInfo->GetPathName(); + HOUDINI_TEST_EQUAL(FHoudiniEditorUnitTestUtils::IsTemporary( + TempFolder, ObjectPath), true); + + TArray ExpectedResults = FHoudiniEditorTestLandscapes::CreateExpectedPaintLayer1Values(HeightFieldSize); + TArray GeneratedValues = FHoudiniEditorTestLandscapes::GetLandscapePaintLayerValues(LandscapeActor, LayerName); + TArray Errors = FHoudiniEditorTestLandscapes::CheckLandscapeValues(GeneratedValues, ExpectedResults, ExpectedGridSize, 1.0f); + for(auto& Error : Errors) + this->AddError(Error); + + HOUDINI_TEST_EQUAL_ON_FAIL(Errors.Num(), 0, return true; ); + } + + // check Edit Layer + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + HOUDINI_TEST_EQUAL(LandscapeActor->GetLayersConst().Num(), 1); + HOUDINI_TEST_EQUAL(LandscapeActor->GetLayerConst(0)->EditLayer->GetName().ToString(), FString(TEXT("Edit Layer"))); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + HOUDINI_TEST_EQUAL(LandscapeActor->GetLayerCount(), 1); + HOUDINI_TEST_EQUAL(LandscapeActor->GetLayerConst(0)->Name.ToString(), FString(TEXT("Edit Layer"))); +#else + HOUDINI_TEST_EQUAL(LandscapeActor->LandscapeLayers.Num(), 1); + HOUDINI_TEST_EQUAL(LandscapeActor->LandscapeLayers[0].Name.ToString(), FString(TEXT("Edit Layer"))); +#endif + } + return true; + })); +#endif + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestLandscapes_ModifyExisting, "Houdini.UnitTests.Landscapes.ModifyExisting", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestLandscapes_ModifyExisting::RunTest(const FString& Parameters) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries( + this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + FString MapName = TEXT("/Game/TestObjects/Landscapes/Test_ModifyLandscape"); + TSharedPtr Context(new FHoudiniTestContext(this, MapName)); + + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + // Find Landscape Actor + ALandscape* LandscapeActor = nullptr; + for(TActorIterator ActorItr(Context->World, ALandscape::StaticClass()); ActorItr; ++ActorItr) + { + AActor* FoundActor = *ActorItr; + if(FoundActor) + { + LandscapeActor = Cast(FoundActor); + break; + } + } + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, LandscapeActor]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + FString TempFolder = Context->GetTemporaryCookFolderOrDefault(); + + const FIntPoint ExpectedGridSize = { 190, 190 }; + + // Check the size of the landscape is correct. + FIntPoint LandscapeQuadSize = LandscapeActor->GetBoundingRect().Size(); + FIntPoint LandscapeVertSize(LandscapeQuadSize.X + 1, LandscapeQuadSize.Y + 1); + HOUDINI_TEST_EQUAL_ON_FAIL(LandscapeVertSize, ExpectedGridSize, return true); + + // Check paint layer 1. + { + FString EditLayer = TEXT("Edit Layer 2"); + FString LayerName = TEXT("paint_layer2"); + + ULandscapeLayerInfoObject* LayerInfo = FHoudiniEditorTestLandscapes::GetLayerInfo(LandscapeActor, LayerName); + + FString ObjectPath = LayerInfo->GetPathName(); + HOUDINI_TEST_EQUAL(FHoudiniEditorUnitTestUtils::IsTemporary( + TempFolder, ObjectPath), false); + + TArray ExpectedResults = FHoudiniEditorTestLandscapes::CreateExpectedPaintLayer1Values(ExpectedGridSize); + TArray GeneratedValues = FHoudiniEditorTestLandscapes::GetLandscapeEditLayerValues(LandscapeActor, EditLayer, LayerName, ExpectedGridSize); + TArray Errors = FHoudiniEditorTestLandscapes::CheckLandscapeValues(GeneratedValues, ExpectedResults, ExpectedGridSize, 1.0f); + for(auto& Error : Errors) + this->AddError(Error); + + HOUDINI_TEST_EQUAL_ON_FAIL(Errors.Num(), 0, return true; ); + } + + // Check paint layer 2. + { + FString EditLayer = TEXT("Edit Layer 1"); + FString LayerName = TEXT("paint_layer1"); + + ULandscapeLayerInfoObject* LayerInfo = FHoudiniEditorTestLandscapes::GetLayerInfo(LandscapeActor, LayerName); + + FString ObjectPath = LayerInfo->GetPathName(); + HOUDINI_TEST_EQUAL(FHoudiniEditorUnitTestUtils::IsTemporary( + TempFolder, ObjectPath), false); + + TArray ExpectedResults = FHoudiniEditorTestLandscapes::CreateExpectedPaintLayer1Values(ExpectedGridSize); + TArray GeneratedValues = FHoudiniEditorTestLandscapes::GetLandscapeEditLayerValues(LandscapeActor, EditLayer, LayerName, ExpectedGridSize); + TArray Errors = FHoudiniEditorTestLandscapes::CheckLandscapeValues(GeneratedValues, ExpectedResults, ExpectedGridSize, 1.0f); + for(auto& Error : Errors) + this->AddError(Error); + + HOUDINI_TEST_EQUAL_ON_FAIL(Errors.Num(), 0, return true; ); + } + + // check Edit Layer + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + HOUDINI_TEST_EQUAL(LandscapeActor->GetLayersConst().Num(), 3); + HOUDINI_TEST_EQUAL(LandscapeActor->GetLayerConst(0)->EditLayer->GetName().ToString(), FString(TEXT("Layer"))); + HOUDINI_TEST_EQUAL(LandscapeActor->GetLayerConst(1)->EditLayer->GetName().ToString(), FString(TEXT("Edit Layer 1"))); + HOUDINI_TEST_EQUAL(LandscapeActor->GetLayerConst(2)->EditLayer->GetName().ToString(), FString(TEXT("Edit Layer 2"))); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + HOUDINI_TEST_EQUAL(LandscapeActor->GetLayerCount(), 3); + HOUDINI_TEST_EQUAL(LandscapeActor->GetLayerConst(0)->Name.ToString(), FString(TEXT("Layer"))); + HOUDINI_TEST_EQUAL(LandscapeActor->GetLayerConst(1)->Name.ToString(), FString(TEXT("Edit Layer 1"))); + HOUDINI_TEST_EQUAL(LandscapeActor->GetLayerConst(2)->Name.ToString(), FString(TEXT("Edit Layer 2"))); +#else + HOUDINI_TEST_EQUAL(LandscapeActor->LandscapeLayers.Num(), 3); + HOUDINI_TEST_EQUAL(LandscapeActor->LandscapeLayers[0].Name.ToString(), FString(TEXT("Layer"))); + HOUDINI_TEST_EQUAL(LandscapeActor->LandscapeLayers[1].Name.ToString(), FString(TEXT("Edit Layer 1"))); + HOUDINI_TEST_EQUAL(LandscapeActor->LandscapeLayers[2].Name.ToString(), FString(TEXT("Edit Layer 2"))); +#endif + } + return true; + })); +#endif + return true; +} +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestLandscapes.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#include "Landscape.h" +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +class FHoudiniEditorTestLandscapes +{ +public: + + // Functions for getting and check height values. + static TArray CheckLandscapeValues(TArray& Results, TArray& Expected, const FIntPoint& Size, float AbsError = 0.01f, int MaxErrors = 20); + static TArray GetLandscapeHeightValues(ALandscape* LandscapeActor); + static TArray CreateExpectedHeightValues(const FIntPoint& ExpectedSize, float HeightScale); + + // Functions for getting and setting paint layers + static TArray GetLandscapePaintLayerValues(ALandscape* LandscapeActor, const FString & TargetLayerName); + static TArray CreateExpectedPaintLayer1Values(const FIntPoint& ExpectedSize); + static TArray CreateExpectedPaintLayer2Values(const FIntPoint& ExpectedSize); + + static TArray GetLandscapeEditLayerValues(ALandscape* LandscapeActor, const FString& EditLayer, const FString& TargetLayerName, const FIntPoint & Size); + + static ULandscapeLayerInfoObject* GetLayerInfo(ALandscape* LandscapeActor, const FString& LayerName); + + static float GetMin(const TArray & Values); + static float GetMax(const TArray & Values); + static TArray Resize(TArray& In, const FIntPoint& OriginalSize, const FIntPoint& NewSize); + +}; + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestMeshGroups.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestMeshGroups.h" + +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterMultiParm.h" +#include "HoudiniParameterString.h" +#include "HoudiniParameterToggle.h" +#include "Landscape.h" +#include "LandscapeSplineControlPoint.h" +#include "NetworkMessage.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Chaos/HeightField.h" +#include "PhysicsEngine/BodySetup.h" +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "HoudiniEditorUnitTestUtils.h" +#include "LandscapeEdit.h" + + +FString BoolToString(bool value) +{ + return value ? TEXT("True") : TEXT("False"); +} + +void FHoudiniMeshAutomationTest::CheckMeshType(const FStaticMeshLODResources & Resource, EHoudiniMeshType MeshType) +{ + FString Result; + + auto& Section = Resource.Sections[0]; + + int ExpectedNumTriangles = 0; + switch(MeshType) + { + case EHoudiniMeshType::Cube: + ExpectedNumTriangles = 12; + break; + case EHoudiniMeshType::Sphere: + ExpectedNumTriangles = 528; + break; + case EHoudiniMeshType::Torus: + break; + default: + break; + } + + HOUDINI_TEST_EQUAL(Section.NumTriangles, ExpectedNumTriangles); + +} + + +FString FHoudiniMeshAutomationTest::GetCollisionTypeName(EHoudiniCollisionType Type) +{ + switch(Type) + { + case EHoudiniCollisionType::None: + return TEXT(""); + break; + case EHoudiniCollisionType::MainMesh: + return TEXT(""); + break; + case EHoudiniCollisionType::SimpleBox: + return TEXT("simple_box"); + break; + case EHoudiniCollisionType::SimpleSphere: + return TEXT("simple_sphere"); + break; + case EHoudiniCollisionType::SimpleCapsule: + return TEXT("simple_capsule"); + break; + case EHoudiniCollisionType::Kdop10x: + return TEXT("simple_kdop10x"); + break; + case EHoudiniCollisionType::Kdop10y: + return TEXT("simple_kdop10y"); + break; + case EHoudiniCollisionType::Kdop10z: + return TEXT("simple_kdop10z"); + break; + case EHoudiniCollisionType::Kdop18: + return TEXT("simple_kdop18"); + break; + case EHoudiniCollisionType::Kdop26: + return TEXT("simple_kdop266"); + } + return TEXT("unknown_collision_type"); +} + +void FHoudiniMeshAutomationTest::CheckMesh(UStaticMeshComponent* StaticMeshComponent, const FHoudiniMeshCheck & MeshCheck) +{ + HOUDINI_TEST_EQUAL_ON_FAIL(MeshCheck.bComponentIsVisible, StaticMeshComponent->IsVisible(), return); + + auto StaticMesh = StaticMeshComponent->GetStaticMesh(); + + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMesh.Get(), return); + + FStaticMeshRenderData * RenderData = StaticMesh->GetRenderData(); + HOUDINI_TEST_NOT_NULL_ON_FAIL(RenderData, return); + + int NumExpectedLODs = MeshCheck.LODMeshes.Num(); + HOUDINI_TEST_EQUAL(NumExpectedLODs, RenderData->LODResources.Num()); + + if (NumExpectedLODs == RenderData->LODResources.Num()) + { + // now check we got the right object in the right slot. + for(int LODIndex = 0; LODIndex < NumExpectedLODs; LODIndex++) + { + CheckMeshType(RenderData->LODResources[LODIndex], MeshCheck.LODMeshes[LODIndex]); + } + } + + bool bMeshHasComplexCollision = IsValid(StaticMesh->ComplexCollisionMesh); + + HOUDINI_TEST_EQUAL(bMeshHasComplexCollision, MeshCheck.ComplexCollisionType != EHoudiniMeshType::None); + + if (bMeshHasComplexCollision) + { + UStaticMesh * ComplexCollisionMesh = StaticMesh->ComplexCollisionMesh; + FStaticMeshRenderData* CollisionRenderData = ComplexCollisionMesh->GetRenderData(); + HOUDINI_TEST_NOT_NULL_ON_FAIL(CollisionRenderData, return); + HOUDINI_TEST_EQUAL_ON_FAIL(CollisionRenderData->LODResources.Num(), 1, return); + + CheckMeshType(CollisionRenderData->LODResources[0], MeshCheck.ComplexCollisionType); + } + + UBodySetup *BodySetup = StaticMesh->GetBodySetup(); + HOUDINI_TEST_NOT_NULL_ON_FAIL(BodySetup, return); + FKAggregateGeom & AggGeom = BodySetup->AggGeom; + + HOUDINI_TEST_EQUAL(AggGeom.BoxElems.Num(), MeshCheck.NumBoxCollisions); + HOUDINI_TEST_EQUAL(AggGeom.SphereElems.Num(), MeshCheck.NumSphereCollisions); + HOUDINI_TEST_EQUAL(AggGeom.SphylElems.Num(), MeshCheck.NumSphylCollisions); + HOUDINI_TEST_EQUAL(AggGeom.ConvexElems.Num(), MeshCheck.NumConvexCollisions); + +} + +TSharedPtr FHoudiniMeshAutomationTest::LoadHDA(FAutomationTestBase* Test) +{ + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(Test, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(Test, TEXT("/Game/TestHDAs/Mesh/Test_MeshGroups"), FTransform::Identity, false)); + Context->SetProxyMeshEnabled(false); + + return Context; +} + +TArray FHoudiniMeshAutomationTest::GetStaticMeshes(TSharedPtr Context) +{ + TArray Outputs; + Context->GetOutputs(Outputs); + + TArray StaticMeshOutputComponents = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + return StaticMeshOutputComponents; +} + + +void FHoudiniMeshAutomationTest::ExecuteMeshTest(TSharedPtr Context, const FHoudiniTestSettings& Settings, const FHoudiniMeshCheck& MeshCheck) +{ + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, Settings]() + { + // Set number of multiparms; cook before we update he values. + SET_HDA_PARAMETER_NUM_ELEMENTS(Context, UHoudiniParameterMultiParm, "cube_groups", Settings.CubeGroups.Num()); + SET_HDA_PARAMETER_NUM_ELEMENTS(Context, UHoudiniParameterMultiParm, "sphere_groups", Settings.SphereGroups.Num()); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, Settings]() + { + // Set the parm values. + for(int Index = 0; Index < Settings.CubeGroups.Num(); Index++) + { + FString ParmName = FString::Format(TEXT("cube_group{0}"), { Index + 1}); + SET_HDA_PARAMETER(Context, UHoudiniParameterString, TCHAR_TO_ANSI(*ParmName), Settings.CubeGroups[Index], 0); + } + for (int Index = 0; Index < Settings.SphereGroups.Num(); Index++) + { + FString ParmName = FString::Format(TEXT("sphere_group{0}"), { Index + 1 }); + SET_HDA_PARAMETER(Context, UHoudiniParameterString, TCHAR_TO_ANSI(*ParmName), Settings.SphereGroups[Index], 0); + } + + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "pack", Settings.bPack, 0); + + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, MeshCheck]() + { + // Check results + auto StaticMeshes = FHoudiniMeshAutomationTest::GetStaticMeshes(Context); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshes.Num(), 1, return true); + FHoudiniMeshAutomationTest::CheckMesh(StaticMeshes[0], MeshCheck); + + return true; + })); +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_None, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.None", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_None::RunTest(const FString& Parameters) +{ + // No special groups should export a single mesh, + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add(""); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 0; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_RenderedCollisionGeo, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.RenderedCollisionGeo", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_RenderedCollisionGeo::RunTest(const FString& Parameters) +{ + // rendered_collision_geo should export a simple mesh + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 0; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_CollisionGeo, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.CollisionGeo", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_CollisionGeo::RunTest(const FString& Parameters) +{ + // collision_geo should export a simple mesh where the component is invisible. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("collision_geo"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 0; + MeshCheck.bComponentIsVisible = false; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_MainGeo, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.MainGeo", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_MainGeo::RunTest(const FString& Parameters) +{ + // main_geo should export a simple mesh + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("main_geo"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 0; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_SimpleKDOP18Collision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.SimpleKDOP18Collision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_SimpleKDOP18Collision::RunTest(const FString& Parameters) +{ + // Convex hull test + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo_simple_kdop18"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumConvexCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_SimpleKDOP26Collision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.SimpleKDOP26Collision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_SimpleKDOP26Collision::RunTest(const FString& Parameters) +{ + // Convex hull test + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo_simple_kdop26"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumConvexCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_SimpleKDOP10xCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.SimpleKDOP10xCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_SimpleKDOP10xCollision::RunTest(const FString& Parameters) +{ + // Convex hull test + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo_simple_kdop10x"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumConvexCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_SimpleKDOP10yCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.SimpleKDOP10yCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_SimpleKDOP10yCollision::RunTest(const FString& Parameters) +{ + // Convex hull test + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo_simple_kdop10y"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumConvexCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_SimpleKDOP10zCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.SimpleKDOP10zCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_SimpleKDOP10zCollision::RunTest(const FString& Parameters) +{ + // Convex hull test + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo_simple_kdop10z"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumConvexCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_SimpleBoxCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.SimpleBoxCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_SimpleBoxCollision::RunTest(const FString& Parameters) +{ + // Convex hull test + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo_simple_box"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_SimpleBoxAndSphereCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.Simple1Box1SphereCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_SimpleBoxAndSphereCollision::RunTest(const FString& Parameters) +{ + // Test that 2 simple collisions can be added to the same mesh. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add(""); + Settings.CubeGroups.Add("collision_geo_simple_box_1"); + Settings.SphereGroups.Add("collision_geo_simple_sphere_1"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 1; + MeshCheck.NumSphereCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_Simple2BoxCollisions, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.Simple2BoxesCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_Simple2BoxCollisions::RunTest(const FString& Parameters) +{ + // Test that 2 simple collisions can be added to the same mesh. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add(""); + Settings.CubeGroups.Add("collision_geo_simple_box_1"); + Settings.CubeGroups.Add("collision_geo_simple_box_2"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 2; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_MainGeoBoxAndSphereCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.MainGeoSimpleBoxAndSphereCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_MainGeoBoxAndSphereCollision::RunTest(const FString& Parameters) +{ + // Test that 2 simple collisions can be added to the same mesh. with main_geo as the name. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("main_geo"); + Settings.CubeGroups.Add("collision_geo_simple_box_1"); + Settings.SphereGroups.Add("collision_geo_simple_sphere_1"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 1; + MeshCheck.NumSphereCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_LODs, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.LODs", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_LODs::RunTest(const FString& Parameters) +{ + // test lods. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("lod1"); + Settings.SphereGroups.Add("lod2"); + Settings.CubeGroups.Add("lod3"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Sphere); + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_MainGeoCustomCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.ComplexCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_MainGeoCustomCollision::RunTest(const FString& Parameters) +{ + // Test a complex collision, where a second mesh is created. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.SphereGroups.Add("collision_geo"); + Settings.CubeGroups.Add("rendered_geo"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.ComplexCollisionType = EHoudiniMeshType::Sphere; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_Everything, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Unpacked.Everything", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_Everything::RunTest(const FString& Parameters) +{ + // Lots of things tested together. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.SphereGroups.Add("collision_geo"); + Settings.CubeGroups.Add("rendered_geo"); + Settings.CubeGroups.Add("collision_geo_simple_box_1"); + Settings.SphereGroups.Add("lod1"); + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Sphere); + MeshCheck.ComplexCollisionType = EHoudiniMeshType::Sphere; + MeshCheck.NumBoxCollisions = 1; + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedNone, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.None", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedNone::RunTest(const FString& Parameters) +{ + // No special groups should export a single mesh, + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add(""); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 0; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedRenderedCollisionGeo, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.RenderedCollisionGeo", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedRenderedCollisionGeo::RunTest(const FString& Parameters) +{ + // rendered_collision_geo should export a simple mesh + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 0; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedCollisionGeo, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.CollisionGeo", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedCollisionGeo::RunTest(const FString& Parameters) +{ + // collision_geo should export a simple mesh where the component is invisible. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("collision_geo"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 0; + MeshCheck.bComponentIsVisible = false; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedMainGeo, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.MainGeo", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedMainGeo::RunTest(const FString& Parameters) +{ + // main_geo should export a simple mesh + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("main_geo"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 0; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedSimpleKDOP18Collision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.SimpleKDOP18Collision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedSimpleKDOP18Collision::RunTest(const FString& Parameters) +{ + // Convex hull test + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo_simple_kdop18"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumConvexCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedSimpleKDOP26Collision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.SimpleKDOP26Collision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedSimpleKDOP26Collision::RunTest(const FString& Parameters) +{ + // Convex hull test + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo_simple_kdop26"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumConvexCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedSimpleKDOP10xCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.SimpleKDOP10xCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedSimpleKDOP10xCollision::RunTest(const FString& Parameters) +{ + // Convex hull test + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo_simple_kdop10x"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumConvexCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedSimpleKDOP10yCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.SimpleKDOP10yCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedSimpleKDOP10yCollision::RunTest(const FString& Parameters) +{ + // Convex hull test + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo_simple_kdop10y"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumConvexCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedSimpleKDOP10zCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.SimpleKDOP10zCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedSimpleKDOP10zCollision::RunTest(const FString& Parameters) +{ + // Convex hull test + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo_simple_kdop10z"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumConvexCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedSimpleBoxCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.SimpleBoxCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedSimpleBoxCollision::RunTest(const FString& Parameters) +{ + // Convex hull test + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("rendered_collision_geo_simple_box"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedSimpleBoxAndSphereCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.Simple1Box1SphereCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedSimpleBoxAndSphereCollision::RunTest(const FString& Parameters) +{ + // Test that 2 simple collisions can be added to the same mesh. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add(""); + Settings.CubeGroups.Add("collision_geo_simple_box_1"); + Settings.SphereGroups.Add("collision_geo_simple_sphere_1"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 1; + MeshCheck.NumSphereCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedSimple2BoxCollisions, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.Simple2BoxesCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedSimple2BoxCollisions::RunTest(const FString& Parameters) +{ + // Test that 2 simple collisions can be added to the same mesh. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add(""); + Settings.CubeGroups.Add("collision_geo_simple_box_1"); + Settings.CubeGroups.Add("collision_geo_simple_box_2"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 2; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedMainGeoBoxAndSphereCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.MainGeoSimpleBoxAndSphereCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedMainGeoBoxAndSphereCollision::RunTest(const FString& Parameters) +{ + // Test that 2 simple collisions can be added to the same mesh. with main_geo as the name. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("main_geo"); + Settings.CubeGroups.Add("collision_geo_simple_box_1"); + Settings.SphereGroups.Add("collision_geo_simple_sphere_1"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.NumBoxCollisions = 1; + MeshCheck.NumSphereCollisions = 1; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedLODs, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.LODs", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedLODs::RunTest(const FString& Parameters) +{ + // test lods. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.CubeGroups.Add("lod1"); + Settings.SphereGroups.Add("lod2"); + Settings.CubeGroups.Add("lod3"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Sphere); + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedMainGeoCustomCollision, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.ComplexCollision", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedMainGeoCustomCollision::RunTest(const FString& Parameters) +{ + // Test a complex collision, where a second mesh is created. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.SphereGroups.Add("collision_geo"); + Settings.CubeGroups.Add("rendered_geo"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.ComplexCollisionType = EHoudiniMeshType::Sphere; + + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + + +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMeshGroups_PackedEverything, FHoudiniMeshAutomationTest, "Houdini.UnitTests.MeshGroups.MeshDesc.Packed.Everything", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMeshGroups_PackedEverything::RunTest(const FString& Parameters) +{ + // Lots of things tested together. + TSharedPtr Context = FHoudiniMeshAutomationTest::LoadHDA(this); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + FHoudiniTestSettings Settings; + Settings.SphereGroups.Add("collision_geo"); + Settings.CubeGroups.Add("rendered_geo"); + Settings.CubeGroups.Add("collision_geo_simple_box_1"); + Settings.SphereGroups.Add("lod1"); + Settings.bPack = true; + + FHoudiniMeshCheck MeshCheck; + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Cube); + MeshCheck.LODMeshes.Add(EHoudiniMeshType::Sphere); + MeshCheck.ComplexCollisionType = EHoudiniMeshType::Sphere; + MeshCheck.NumBoxCollisions = 1; + ExecuteMeshTest(Context, Settings, MeshCheck); + + return true; +} + + +#endif +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestMeshGroups.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#include "HoudiniEditorUnitTestUtils.h" +#include "Landscape.h" +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" +#include + +enum EHoudiniMeshType +{ + None, Cube, Sphere, Torus +}; + +struct FHoudiniMeshCheck +{ + TArray LODMeshes; + EHoudiniMeshType ComplexCollisionType = EHoudiniMeshType::None; + bool bComponentIsVisible = true; + int NumBoxCollisions = 0; + int NumSphereCollisions = 0; + int NumConvexCollisions = 0; + int NumSphylCollisions = 0; +}; + +struct FHoudiniMeshCheckErrors +{ + FHoudiniMeshCheckErrors & operator+=(const FString & Error) + { + Errors.Add(Error); + return *this; + } + + FHoudiniMeshCheckErrors & operator+=(const FHoudiniMeshCheckErrors& Error) + { + this->Errors.Append(Error.Errors); + return *this; + } + + TArray Errors; +}; + +struct FHoudiniTestSettings +{ + TArray CubeGroups; + TArray SphereGroups; + bool bPack = false; +}; + +struct FHoudiniMeshAutomationTest : public FHoudiniAutomationTest +{ + FHoudiniMeshAutomationTest(const FString& InName, const bool bInComplexTask) + : FHoudiniAutomationTest(InName, bInComplexTask) + { + } + + void ExecuteMeshTest(TSharedPtr Context, const FHoudiniTestSettings & Settings, const FHoudiniMeshCheck& TestErrors); + + static TArray GetStaticMeshes(TSharedPtr Context); + static TSharedPtr LoadHDA(FAutomationTestBase* Test); + + void CheckMesh(UStaticMeshComponent* StaticMeshComponent, const FHoudiniMeshCheck& Check); + + static FString GetCollisionTypeName(EHoudiniCollisionType Type); + + void CheckMeshType(const FStaticMeshLODResources& Resource, EHoudiniMeshType MeshType); + +}; + + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestMeshMisc.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include "HoudiniEditorTestMeshMisc.h" + +#include "HoudiniApi.h" +#include "HoudiniEditorTestUtils.h" +#include "HoudiniEditorUnitTestUtils.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineAttributes.h" +#include "HoudiniEngineUtils.h" +#include "HoudiniParameterFloat.h" + +#include "Components/InstancedStaticMeshComponent.h" +#include "Misc/DefaultValueHelper.h" + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMiscMeshes_ActorProperties, "Houdini.UnitTests.Mesh.ActorProperties", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMiscMeshes_ActorProperties::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + + TSharedPtr Context(new FHoudiniTestContext(this, FString(TEXT("/Game/TestHDAs/Mesh/Misc/TestMeshActorProperties.umap")))); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 2, return true); + TArray StaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshOutputs.Num(), 2, return true); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 2, return true); + + // Go through each output and check we have two actors with one mesh component each. + TArray Actors; + for(auto& BakedOutput : BakedOutputs) + { + for(auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + TArray Components; + Actor->GetComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 1, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Components[0]->IsA(), 1, continue); + + Actors.Add(Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(Actors.Num(), 2, return false); + + TSet TagsToFind; + TagsToFind.Add(TEXT("actor1")); + TagsToFind.Add(TEXT("actor2")); + + TArray ActorPaths; + + for (AActor* Actor : Actors) + { + auto Tags = Actor->Tags; + + for(int32 i = 0; i < Tags.Num(); ++i) + { + if(Tags[i].ToString().StartsWith(TEXT("path:"))) + { + ActorPaths.Add(Tags[i]); + Tags.RemoveAt(i); + } + } + + // Should have one tag left after finding the path. + HOUDINI_TEST_EQUAL_ON_FAIL(Tags.Num(), 1, continue); + + FString* Find = TagsToFind.Find(Tags[0].ToString()); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Find, continue); + FString Token = *Find; + TagsToFind.Remove(Token); + } + + // We should have found both tags, on seperate actors. + HOUDINI_TEST_EQUAL(TagsToFind.Num(), 0); + + // We should have found two actor paths + HOUDINI_TEST_EQUAL(ActorPaths.Num(), 2); + + return true; + })); + + return true; +} + +bool GetMaterialSlot(const FString& Input, int32& OutNumber, FString& OutString) +{ + // Find opening and closing brackets + int32 OpenBracket = -1; + int32 CloseBracket = -1; + + if(Input.FindChar('[', OpenBracket) && Input.FindChar(']', CloseBracket) && CloseBracket > OpenBracket) + { + FString NumberPart = Input.Mid(OpenBracket + 1, CloseBracket - OpenBracket - 1); + FString StringPart = Input.Mid(CloseBracket + 1); + + // Convert number + if(FDefaultValueHelper::ParseInt(NumberPart, OutNumber)) + { + OutString = StringPart; + return true; + } + } + + return false; // Failed to parse +} + +bool GetParameterNameAndSlot(const FString& Input, int32& Slot, FString& Parameter) +{ + // Split on underscore + FString Left; + FString Right; + if(Input.Split(TEXT("_"), &Left, &Right)) + { + if(Left.IsNumeric()) + { + Slot = FCString::Atoi(*Left); + Parameter = Right; + return true; + } + } + + return false; // parsing failed +} + + +struct FTestMeshMaterialGroup +{ + FString Material; + TMap ScalarParameters; + TMap VectorParameters; + TMap TextureParameters; +}; + +struct FTestMesh +{ + TArray Materials; +}; + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMiscMeshes_MaterialProperties, "Houdini.UnitTests.Mesh.MaterialProperties", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestMiscMeshes_MaterialProperties::RunTest(const FString& Parameters) +{ + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + + TSharedPtr Context(new FHoudiniTestContext(this, FString(TEXT("/Game/TestHDAs/Mesh/Misc/TestActorMaterials.umap")))); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + + const TArray& Parts = Outputs[0]->GetHoudiniGeoPartObjects(); + + HOUDINI_TEST_EQUAL_ON_FAIL(Parts.Num(), 1, return true); + + + const FHoudiniGeoPartObject& Part = Parts[0]; + + const HAPI_Session* Session = FHoudiniEngine::Get().GetSession(); + + + HAPI_PartInfo PartInfo; + HAPI_Result Result = FHoudiniApi::GetPartInfo(Session, Part.GeoId, Part.PartId, &PartInfo); + HOUDINI_TEST_EQUAL_ON_FAIL(Result, HAPI_RESULT_SUCCESS, return true); + + HAPI_Bool AllEqual = false; + TArray LOD1GroupMembership; + LOD1GroupMembership.SetNum(PartInfo.faceCount); + + + Result = FHoudiniApi::GetGroupMembership(Session, + Part.GeoId, Part.PartId, + HAPI_GroupType::HAPI_GROUPTYPE_PRIM, + "lod1", + &AllEqual, LOD1GroupMembership.GetData(), 0, PartInfo.faceCount); + + FString Error = FHoudiniEngineUtils::GetErrorDescription(); + + HOUDINI_TEST_EQUAL_ON_FAIL(Result, HAPI_RESULT_SUCCESS, return true); + + + TArray MaterialNames; + + FHoudiniHapiAccessor MaterialAccessor(Part.GeoId, Part.PartId, "unreal_material"); + bool bSuccess = MaterialAccessor.GetAttributeData(HAPI_AttributeOwner::HAPI_ATTROWNER_PRIM, MaterialNames); + HOUDINI_TEST_EQUAL_ON_FAIL(bSuccess, true, return true); + + FTestMesh Mesh; + + for(int FaceIndex = 0; FaceIndex < MaterialNames.Num(); FaceIndex++) + { + int LodIndex = LOD1GroupMembership[FaceIndex] != 0 ? 1 : 0; + + FString ActualMaterialName; + int Slot; + GetMaterialSlot(MaterialNames[FaceIndex], Slot, ActualMaterialName); + + if(!Mesh.Materials.IsValidIndex(Slot)) + Mesh.Materials.SetNum(Slot + 1); + + FTestMeshMaterialGroup& Material = Mesh.Materials[Slot]; + + Material.Material = ActualMaterialName; + } + + TArray AttributeNames = FHoudiniEngineUtils::GetAttributeNames(Session, Part.GeoId, Part.PartId, HAPI_AttributeOwner::HAPI_ATTROWNER_PRIM); + + for(FString& AttrName : AttributeNames) + { + FString Prefix = TEXT("unreal_material_parameter_"); + if(!AttrName.StartsWith(Prefix)) + continue; + + FString TruncatedParamName = AttrName; + TruncatedParamName.RemoveFromStart(Prefix); + + FString ParamName; + int Slot = 0; + GetParameterNameAndSlot(TruncatedParamName, Slot, ParamName); + + std::string CStr = TCHAR_TO_ANSI(*AttrName); + FHoudiniHapiAccessor Accessor(Part.GeoId, Part.PartId, CStr.c_str()); + HAPI_AttributeInfo Info; + Accessor.GetInfo(Info, HAPI_AttributeOwner::HAPI_ATTROWNER_PRIM); + + TArray Values; + TArray StringValues; + if(!Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, Values)) + { + Accessor.GetAttributeData(HAPI_ATTROWNER_PRIM, StringValues); + } + + for (int Index = 0; Index < PartInfo.faceCount; Index++) + { + auto& Material = Mesh.Materials[Slot]; + + int FaceSlot = 0; + FString FaceMaterial; + GetMaterialSlot(MaterialNames[Index], FaceSlot, FaceMaterial); + if(Slot != FaceSlot) + continue; + + const FString& MaterialName = MaterialNames[Index]; + + if (Info.storage == HAPI_STORAGETYPE_FLOAT && Info.tupleSize == 1) + { + float Value = Values[Index]; + auto* ParamPtr = Material.ScalarParameters.Find(ParamName); + if(!ParamPtr) + Material.ScalarParameters.Add(ParamName, Value); + } + + else if (Info.storage == HAPI_STORAGETYPE_FLOAT && Info.tupleSize == 4) + { + FVector4d Value = FVector4d(Values[Index * 4 + 0], Values[Index * 4 + 1], Values[Index * 4 + 2], Values[Index * 4 + 3]); + auto* ParamPtr = Material.VectorParameters.Find(ParamName); + if(!ParamPtr) + Material.VectorParameters.Add(ParamName, Value); + } + else if (Info.storage == HAPI_STORAGETYPE_STRING && Info.tupleSize == 1) + { + FString Value = StringValues[Index]; + auto* ParamPtr = Material.TextureParameters.Find(ParamName); + if(!ParamPtr) + Material.TextureParameters.Add(ParamName, Value); + } + else + { + HOUDINI_LOG_ERROR(TEXT("Invalid storage type, failing test.")); + HOUDINI_TEST_EQUAL_ON_FAIL(true, false, return true); + } + } + } + + { + HOUDINI_TEST_NOT_EQUAL_ON_FAIL(Mesh.Materials[0].VectorParameters.IsEmpty(), true, return true); + auto It = Mesh.Materials[0].VectorParameters.CreateConstIterator(); + HOUDINI_TEST_EQUAL(It->Value, FVector4d(0.0f, 0.0f, 1.0, 0.0)); + } + { + HOUDINI_TEST_NOT_EQUAL_ON_FAIL(Mesh.Materials[1].VectorParameters.IsEmpty(), true, return true); + auto It = Mesh.Materials[1].VectorParameters.CreateConstIterator(); + HOUDINI_TEST_EQUAL(It->Value, FVector4d(1.0f, 0.0f, 0.0, 0.0)); + } + { + HOUDINI_TEST_NOT_EQUAL_ON_FAIL(Mesh.Materials[2].VectorParameters.IsEmpty(), true, return true); + auto It = Mesh.Materials[2].VectorParameters.CreateConstIterator(); + HOUDINI_TEST_EQUAL(It->Value, FVector4d(0.5f, 0.0f, 0.0, 0.0)); + } + + return true; + })); + + return true; +} + + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestMiscMeshes_CustomPrimitiveData, "Houdini.UnitTests.Mesh.CustomPrimitiveData", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool +FHoudiniEditorTestMiscMeshes_CustomPrimitiveData::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries( + this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context( + new FHoudiniTestContext( + this, + TEXT("/Game/TestHDAs/Mesh/Misc/TestCustomPrimData"), + FTransform::Identity, + false)); + + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterFloat, "custprim1", 0.88f, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterFloat, "custprim1", 0.77f, 1); + SET_HDA_PARAMETER(Context, UHoudiniParameterFloat, "custprim1", 0.66f, 2); + SET_HDA_PARAMETER(Context, UHoudiniParameterFloat, "custprim1", 0.55f, 3); + + SET_HDA_PARAMETER(Context, UHoudiniParameterFloat, "custprim2", 0.234f, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterFloat, "custprim2", 0.666f, 1); + SET_HDA_PARAMETER(Context, UHoudiniParameterFloat, "custprim2", 0.42f, 2); + SET_HDA_PARAMETER(Context, UHoudiniParameterFloat, "custprim2", 0.12345678f, 3); + + Context->StartCookingHDA(); + + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have one outputs, a mesh + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + + UHoudiniOutput* TestOutput = Outputs[0]; + HOUDINI_TEST_NOT_NULL_ON_FAIL(TestOutput, return true); + + // We should have one output object + HOUDINI_TEST_EQUAL_ON_FAIL(TestOutput->GetOutputObjects().Num(), 1, return true); + + // That output object should have one valid output component - a SMC + TArray StaticMeshOutputComponents = + FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshOutputComponents.Num(), 1, return true); + + UStaticMeshComponent* SMC = StaticMeshOutputComponents[0]; + HOUDINI_TEST_NOT_NULL_ON_FAIL(SMC, return true); + + // Make sure the SMC has 8 custom primitive data + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetCustomPrimitiveData().Data.Num(), 8, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetDefaultCustomPrimitiveData().Data.Num(), 8, return true); + + // Check their values + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetCustomPrimitiveData().Data[0], 0.88f, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetCustomPrimitiveData().Data[1], 0.77f, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetCustomPrimitiveData().Data[2], 0.66f, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetCustomPrimitiveData().Data[3], 0.55f, return true); + + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetCustomPrimitiveData().Data[4], 0.234f, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetCustomPrimitiveData().Data[5], 0.666f, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetCustomPrimitiveData().Data[6], 0.42f, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetCustomPrimitiveData().Data[7], 0.12345678f, return true); + + // Check their default values + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetDefaultCustomPrimitiveData().Data[0], 0.88f, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetDefaultCustomPrimitiveData().Data[1], 0.77f, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetDefaultCustomPrimitiveData().Data[2], 0.66f, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetDefaultCustomPrimitiveData().Data[3], 0.55f, return true); + + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetDefaultCustomPrimitiveData().Data[4], 0.234f, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetDefaultCustomPrimitiveData().Data[5], 0.666f, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetDefaultCustomPrimitiveData().Data[6], 0.42f, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SMC->GetDefaultCustomPrimitiveData().Data[7], 0.12345678f, return true); + + return true; + })); + + return true; +} + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestMeshMisc.h`: + +```h +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +class FHoudiniEditorTestMeshMisc +{ +public: +}; + +#endif +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestOutputs.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestOutputs.h" + +#include "FileHelpers.h" +#include "HoudiniAsset.h" +#include "HoudiniPublicAPIAssetWrapper.h" +#include "HoudiniPublicAPIInputTypes.h" +#include "HoudiniAssetActor.h" +#include "HoudiniEngineEditorUtils.h" + +#include "HoudiniParameter.h" +#include "HoudiniParameterInt.h" +#include "Landscape.h" +#include "AssetRegistry/AssetRegistryModule.h" +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" +#include "HoudiniAssetActorFactory.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "HoudiniParameterToggle.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "HoudiniEditorUnitTestUtils.h" +#include "FoliageType_InstancedStaticMesh.h" + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestOutput, "Houdini.UnitTests.OutputTests", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestOutput::RunTest(const FString & Parameters) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// This test ensures that we can cook an HDA multiple times and the outputs are removed on each recook. The test HDA + /// can create multiple outputs based off the parameters. By changing the parameters we can get different scenarios on + /// the same HDA. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. This should be the last step before the tests start as it starts the timeout timer. Note + // the context live in a SharedPtr<> because each part of the test, in AddCommand(), are executed asyncronously + // after the test returns. + + TSharedPtr Context(new FHoudiniTestContext(this, TEXT("/Game/TestHDAs/Outputs/Test_Outputs"), FTransform::Identity, false)); + Context->SetProxyMeshEnabled(false); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Firstly: Enable the cube and disable the height field. This should result in one output, which is a static mesh. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + { + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "cube", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "heightfield", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instances", false, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have one output. + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + + // And the one output should have a static mesh. + TArray StaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL(StaticMeshOutputs.Num(), 1); + + return true; + })); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Next: Disable the cube and disable the height field. We should have no outputs. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + { + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "cube", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "heightfield", false, 0); + Context->StartCookingHDA(); + return true; // This part of the test is complete. + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 0, return true); + return true; // This part of the test is complete. + })); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Next: Enable the cube and enable the height field. This should result in two outputs. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + { + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "cube", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "heightfield", true, 0); + Context->StartCookingHDA(); + return true; // This part of the test is complete. + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have one mesh and one landscape + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 2, return true); + + // Check one output is a static mesh. + TArray StaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL(StaticMeshOutputs.Num(), 1); + + // Check one output is a landscape. + TArray LandscapeOutput = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL(LandscapeOutput.Num(), 1); + + // Cache the name of the landscape actor. We'll make sure its deleted in the next part of the test. + FString LandscapeName = LandscapeOutput[0]->Landscape->GetName(); + FString Key = FString("landscape"); + Context->Data.Add(Key, LandscapeName); + return true; // This part of the test is complete. + })); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Next: Disable the cube and disable the height field. This should result in no outputs. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + { + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "cube", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "heightfield", false, 0); + Context->StartCookingHDA(); + return true; // This part of the test is complete. + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // Check there are no outputs. + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 0, return true); + + // Check that we have valid context data + bool bNoData = Context->Data.IsEmpty(); + HOUDINI_TEST_EQUAL_ON_FAIL(bNoData, false, return true); + + // Check the landscape actor (whose name was cached from the last test) is deleted. + FString LandscapeName = Context->Data[TEXT("landscape")]; + AActor * LandscapeActor = FHoudiniEditorUnitTestUtils::GetActorWithName(Context->GetWorld(), LandscapeName); + HOUDINI_TEST_NULL(LandscapeActor); + + return true; // This part of the test is complete. + })); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Next: Just enable instances, make sure they're generated. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + { + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "cube", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "heightfield", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instances", true, 0); + Context->StartCookingHDA(); + return true; // This part of the test is complete. + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have one output. + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + + // And the one output should have an instanced static mesh. + TArray Components = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 1, return true); + + UInstancedStaticMeshComponent * Component = Components[0]; + + // Check we have 4 instances. + HOUDINI_TEST_EQUAL_ON_FAIL(Component->GetNumRenderInstances(), 4, return true); + + return true; // This part of the test is complete. + })); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Next: Just enable instances as foliage, make sure they're generated. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + { + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "cube", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "heightfield", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instances", true, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "as_foliage", true, 0); + Context->StartCookingHDA(); + return true; // This part of the test is complete. + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have one output. + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + + // There should be no instance components as we're creating foliage. + TArray ISMs = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(ISMs.Num(), 0, return true); + + // Unreal should have created one Foliage Instanced Static Mesh. + TArray Foliages = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(Foliages.Num(), 1, return true); + + return true; // This part of the test is complete. + })); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Next: Disable everything. This should result be no outputs. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + { + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "cube", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "heightfield", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "instances", false, 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "as_foliage", false, 0); + Context->StartCookingHDA(); + return true; // This part of the test is complete. + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // Check there are no outputs. + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 0, return true); + + return true; // This part of the test is complete. + })); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Done + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// +#endif + return true; +} + + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestOutputs.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestPCG.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include "HoudiniEditorTestPCG.h" + +#include "HoudiniEditorTestUtils.h" +#include "HoudiniEditorUnitTestUtils.h" +#include "HoudiniParameterFloat.h" +#include + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wshadow" +#endif + +#include "PCGComponent.h" + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#include "PCGVolume.h" +#include "PCGGraph.h" +#include "PCGDataAsset.h" +#include "Data/PCGPointData.h" +#include "HoudiniPCGDataObject.h" +#include "InstancedFoliageActor.h" +#include "Landscape.h" +#include "PCGParamData.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "Engine/StaticMeshActor.h" + +enum class EHoudiniTestPCGContextState : int +{ + None, + Cleanup, + Cleaned, + Generate, + Done +}; + +class EHoudiniTestPCGContext +{ +public: + void LoadPCGTestMap(const FString& MapName); + + void CleanupAndGenerateAsync(); + void Cleanup(); + void GenerateAsync(); + + bool Update(); + + UPCGComponent* PCGComponent = nullptr; + EHoudiniTestPCGContextState State = EHoudiniTestPCGContextState::None; + +private: + void OnGraphCleaned(UPCGComponent* PCGComponent_); + void OnGraphGenerated(UPCGComponent* PCGComponent_); + bool bDoGenerateAfterClean; +}; + + +void EHoudiniTestPCGContext::LoadPCGTestMap(const FString & MapName) +{ + // Now create the test context. + UWorld* World = UEditorLoadingAndSavingUtils::LoadMap(MapName); + if(!IsValid(World)) + return; + + for(TActorIterator ActorItr(World); ActorItr; ++ActorItr) + { + APCGVolume* PCGVolume = Cast(*ActorItr); + if(IsValid(PCGVolume)) + { + PCGComponent = PCGVolume->GetComponentByClass(); + PCGComponent->OnPCGGraphCleanedDelegate.AddRaw(this, &EHoudiniTestPCGContext::OnGraphCleaned); + PCGComponent->OnPCGGraphGeneratedDelegate.AddRaw(this, &EHoudiniTestPCGContext::OnGraphGenerated); + } + } +} + + +void EHoudiniTestPCGContext::OnGraphCleaned(UPCGComponent* PCGComponent_) +{ + this->State = EHoudiniTestPCGContextState::Cleaned; +} + +void EHoudiniTestPCGContext::OnGraphGenerated(UPCGComponent* PCGComponent_) +{ + if (this->State == EHoudiniTestPCGContextState::Generate) + { + this->State = EHoudiniTestPCGContextState::Done; + } + +} + +void EHoudiniTestPCGContext::GenerateAsync() +{ + this->State = EHoudiniTestPCGContextState::Generate; + PCGComponent->GenerateLocal(true); +} + +void EHoudiniTestPCGContext::CleanupAndGenerateAsync() +{ + this->bDoGenerateAfterClean = true; + + if(PCGComponent->bGenerated) + { + this->State = EHoudiniTestPCGContextState::Cleanup; + PCGComponent->Cleanup(); + } + else + { + this->State = EHoudiniTestPCGContextState::Cleaned; + } +} + +void EHoudiniTestPCGContext::Cleanup() +{ + this->bDoGenerateAfterClean = false; + + if(PCGComponent->bGenerated) + { + this->State = EHoudiniTestPCGContextState::Cleanup; + PCGComponent->Cleanup(); + } +} + + +bool EHoudiniTestPCGContext::Update() +{ + switch(this->State) + { + case EHoudiniTestPCGContextState::Cleaned: + if(bDoGenerateAfterClean) + { + GenerateAsync(); + } + else + { + this->State = EHoudiniTestPCGContextState::Done; + } + break; + default: + break; + } + + return this->State == EHoudiniTestPCGContextState::Done; +} + +UObject* FHoudiniEditorTestPCG::GetOutputObject(UHoudiniPCGDataObject* PCGDataObject, const FString & Field, int Index) +{ + auto * Attr = Cast(PCGDataObject->FindAttribute(Field)); + if(!IsValid(Attr) || !Attr->Values.IsValidIndex(Index)) + return nullptr; + + const FString & ObjectPath = Attr->Values[Index].ToString(); + + UObject* Object = StaticLoadObject(UObject::StaticClass(), nullptr, *ObjectPath); + return Object; + +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_MeshesCooked, "Houdini.UnitTests.PCG.Meshes.Cooked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_MeshesCooked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGMesh/PCGMeshLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + FString AssetPath = TEXT("/Game/"); + FString AssetName = TEXT("PCG_Out"); + FString PCGAssetFullPath = FString::Printf(TEXT("%s/%s"), *AssetPath, *AssetName); + + UPCGGraphInstance* GraphInstance = Context->PCGComponent->GetGraphInstance(); + GraphInstance->SetGraphParameter(FName("object"), FSoftObjectPath(TEXT("/Game/TestObjects/SM_Cube.SM_Cube"))); + GraphInstance->SetGraphParameter(FName("out_path"), AssetPath); + GraphInstance->SetGraphParameter(FName("out_name"), AssetName); + GraphInstance->SetGraphParameter(FName("scale_factor"), 1.0f); + + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Test 1: Load a cube, then use it to generate a new cube. + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, PCGAssetFullPath]() + { + if(!Context->Update()) + return false; + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *PCGAssetFullPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 1, return true); + // ... it should have data ... + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset->Data.TaggedData[0].Data.Get(), return true); + // ... which we'll now convert to an PCGDataObject so we can easily ready it... + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGDataAsset->Data.TaggedData[0].Data.Get()); + + // ... check we have a mesh + UStaticMesh* StaticMesh = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("object"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMesh, return true); + + // ... check the mesh's bounding box. + FBox Box = StaticMesh->GetBoundingBox(); + + HOUDINI_TEST_EQUAL(Box.Min.X, -50.0); + HOUDINI_TEST_EQUAL(Box.Min.Y, -50.0); + HOUDINI_TEST_EQUAL(Box.Min.Z, -50.0); + HOUDINI_TEST_EQUAL(Box.Max.X, 50.0); + HOUDINI_TEST_EQUAL(Box.Max.Y, 50.0); + HOUDINI_TEST_EQUAL(Box.Max.Z, 50.0); + + // ... check we have a mesh component + UStaticMeshComponent* StaticMeshComponent = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("component"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMeshComponent, return true); + + return true; + })); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Test 2: Load a cube, then use it to generate a new cube but using parameters to scale it. + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context, GraphInstance] + { + Context->State = EHoudiniTestPCGContextState::Generate; + GraphInstance->SetGraphParameter(FName("scale_factor"), 2.0f); + Context->CleanupAndGenerateAsync(); + + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, PCGAssetFullPath]() + { + if(!Context->Update()) + return false; + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *PCGAssetFullPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 1, return true); + // ... it should have data ... + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset->Data.TaggedData[0].Data.Get(), return true); + // ... which we'll now convert to an PCGDataObject so we can easily ready it... + UHoudiniPCGDataObject* PCGDataObject = NewObject(GetTransientPackage()); + PCGDataObject->SetFromPCGData(PCGDataAsset->Data.TaggedData[0].Data.Get()); + + // ... check we have a mesh + UStaticMesh* StaticMesh = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("object"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMesh, return true); + + // ... check the mesh's bounding box. + FBox Box = StaticMesh->GetBoundingBox(); + HOUDINI_TEST_EQUAL(Box.Min.X, -100.0); + HOUDINI_TEST_EQUAL(Box.Min.Y, -100.0); + HOUDINI_TEST_EQUAL(Box.Min.Z, -100.0); + HOUDINI_TEST_EQUAL(Box.Max.X, 100.0); + HOUDINI_TEST_EQUAL(Box.Max.Y, 100.0); + HOUDINI_TEST_EQUAL(Box.Max.Z, 100.0); + + // ... check we have a mesh component + UStaticMeshComponent* StaticMeshComponent = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("component"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMeshComponent, return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_MeshesBaked, "Houdini.UnitTests.PCG.Meshes.Baked.SceneComponents", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + + bool FHoudiniEditorTestPCG_MeshesBaked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGMesh/PCGMeshLevelBaked.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + FString AssetPath = TEXT("/Game/"); + FString AssetName = TEXT("PCG_Out"); + FString PCGAssetFullPath = FString::Printf(TEXT("%s/%s"), *AssetPath, *AssetName); + + UPCGGraphInstance* GraphInstance = Context->PCGComponent->GetGraphInstance(); + GraphInstance->SetGraphParameter(FName("object"), FSoftObjectPath(TEXT("/Game/TestObjects/SM_Cube.SM_Cube"))); + GraphInstance->SetGraphParameter(FName("out_path"), AssetPath); + GraphInstance->SetGraphParameter(FName("out_name"), AssetName); + GraphInstance->SetGraphParameter(FName("scale_factor"), 1.0f); + + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Test 1: Load a cube, then use it to generate a new cube. + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, PCGAssetFullPath]() + { + if(!Context->Update()) + return false; + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *PCGAssetFullPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 1, return true); + // ... it should have data ... + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset->Data.TaggedData[0].Data.Get(), return true); + // ... which we'll now convert to an PCGDataObject so we can easily ready it... + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGDataAsset->Data.TaggedData[0].Data.Get()); + + // ... check we have a mesh + UStaticMesh* StaticMesh = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("object"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMesh, return true); + + // ... check the mesh's bounding box. + FBox Box = StaticMesh->GetBoundingBox(); + + HOUDINI_TEST_EQUAL(Box.Min.X, -50.0); + HOUDINI_TEST_EQUAL(Box.Min.Y, -50.0); + HOUDINI_TEST_EQUAL(Box.Min.Z, -50.0); + HOUDINI_TEST_EQUAL(Box.Max.X, 50.0); + HOUDINI_TEST_EQUAL(Box.Max.Y, 50.0); + HOUDINI_TEST_EQUAL(Box.Max.Z, 50.0); + + // ... check we have a mesh actor + AStaticMeshActor* StaticMeshComponent = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("actor"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMeshComponent, return true); + + // The parent should be an actor. + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_MeshesBakedNoSceneComponents, "Houdini.UnitTests.PCG.Meshes.Baked.NoSceneComponents", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + + bool FHoudiniEditorTestPCG_MeshesBakedNoSceneComponents::RunTest(const FString& Parameters) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 7 + // GraphInstance->SetGraphParameter(FName("object"), FSoftObjectPath(TEXT("/Game/TestObjects/SM_Cube.SM_Cube"))); + // failed on UE5.7 due to a bug in Unreal. Possible fixed in a later version, so just disable in UE 5.7 for now. + + return true; +#else + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGMesh/PCGMeshLevelBakedNoSceneComponents.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + FString AssetPath = TEXT("/Game/"); + FString AssetName = TEXT("PCG_Out"); + FString PCGAssetFullPath = FString::Printf(TEXT("%s/%s"), *AssetPath, *AssetName); + + UPCGGraphInstance* GraphInstance = Context->PCGComponent->GetGraphInstance(); + GraphInstance->SetGraphParameter(FName("object"), FSoftObjectPath(TEXT("/Game/TestObjects/SM_Cube.SM_Cube"))); + GraphInstance->SetGraphParameter(FName("out_path"), AssetPath); + GraphInstance->SetGraphParameter(FName("out_name"), AssetName); + GraphInstance->SetGraphParameter(FName("scale_factor"), 1.0f); + + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Test 1: Load a cube, then use it to generate a new cube. + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, PCGAssetFullPath]() + { + if(!Context->Update()) + return false; + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *PCGAssetFullPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 1, return true); + // ... it should have data ... + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset->Data.TaggedData[0].Data.Get(), return true); + // ... which we'll now convert to an PCGDataObject so we can easily ready it... + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGDataAsset->Data.TaggedData[0].Data.Get()); + + // ... check we have a mesh + UStaticMesh* StaticMesh = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("object"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMesh, return true); + + // ... check the mesh's bounding box. + FBox Box = StaticMesh->GetBoundingBox(); + + HOUDINI_TEST_EQUAL(Box.Min.X, -50.0); + HOUDINI_TEST_EQUAL(Box.Min.Y, -50.0); + HOUDINI_TEST_EQUAL(Box.Min.Z, -50.0); + HOUDINI_TEST_EQUAL(Box.Max.X, 50.0); + HOUDINI_TEST_EQUAL(Box.Max.Y, 50.0); + HOUDINI_TEST_EQUAL(Box.Max.Z, 50.0); + + // ... check we have a mesh actor + AStaticMeshActor* StaticMeshComponent = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("actor"))); + HOUDINI_TEST_NULL_ON_FAIL(StaticMeshComponent, return true); + + // The parent should be an actor. + + return true; + })); + + return true; +#endif +} + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_FoliageBaked, "Houdini.UnitTests.PCG.Foliage.Baked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_FoliageBaked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGFoliage/PCGTestFoliageMap.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + auto GetFoliageCount = [](UWorld* World) + { + int32 TotalCount = 0; + + for(TActorIterator It(World, AInstancedFoliageActor::StaticClass()); It; ++It) + { + AInstancedFoliageActor* IFA = Cast(*It); + + auto InstanceMap = IFA->GetAllInstancesFoliageType(); + for(auto& InstanceIt : InstanceMap) + { + FFoliageInfo* FoliageInfo = InstanceIt.Value; + TotalCount += FoliageInfo->Instances.Num(); + } + } + + return TotalCount; + }; + + int BeginCount = GetFoliageCount(Context->PCGComponent->GetWorld()); + HOUDINI_TEST_EQUAL(BeginCount, 0); + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, GetFoliageCount]() + { + if(!Context->Update()) + return false; + + int FoliageCount = GetFoliageCount(Context->PCGComponent->GetWorld()); + HOUDINI_TEST_NOT_EQUAL(FoliageCount, 0); + + // Now clean to make sure clean up is performed. + + Context->Cleanup(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, GetFoliageCount]() + { + if(!Context->Update()) + return false; + + int FoliageCount = GetFoliageCount(Context->PCGComponent->GetWorld()); + HOUDINI_TEST_EQUAL(FoliageCount, 0); + + return true; + })); + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_LandscapesCookedCreate, "Houdini.UnitTests.PCG.Landscapes.Cooked.Create", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_LandscapesCookedCreate::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGLandscape/PCGTestLandscapeLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + FString AssetPath = TEXT("/Game/"); + FString AssetName = TEXT("PCG_Out"); + FString PCGAssetFullPath = FString::Printf(TEXT("%s/%s"), *AssetPath, *AssetName); + + UPCGGraphInstance* GraphInstance = Context->PCGComponent->GetGraphInstance(); + GraphInstance->SetGraphParameter(FName("out_path"), AssetPath); + GraphInstance->SetGraphParameter(FName("out_name"), AssetName); + GraphInstance->SetGraphParameter(FName("scale_factor"), 1.0f); + + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Test 1: Load a cube, then use it to generate a new cube. + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, PCGAssetFullPath]() + { + if(!Context->Update()) + return false; + + UPCGDataAsset* MyObject = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *PCGAssetFullPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(MyObject, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(MyObject->Data.TaggedData.Num(), 1, return true); + // ... it should have data ... + HOUDINI_TEST_NOT_NULL_ON_FAIL(MyObject->Data.TaggedData[0].Data.Get(), return true); + // ... which we'll now convert to an PCGDataObject so we can easily ready it... + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(MyObject->Data.TaggedData[0].Data.Get()); + + // ... check we have a mesh + ALandscape* Landscape = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("actor"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Landscape, return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_LandscapesCookedModiy, "Houdini.UnitTests.PCG.Landscapes.Cooked.Modify", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + + bool FHoudiniEditorTestPCG_LandscapesCookedModiy::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGLandscapeMods/PCGTestLandscapeCookedLevel.map")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + + ALandscape* Landscape = nullptr; + for(TActorIterator It(Context->PCGComponent->GetWorld()); It; ++It) + { + Landscape = Cast(*It); + if(Landscape) + break; + } + + if (!Landscape) + return false; + + FName LayerName = TEXT("Noise"); + + int32 EditLayerIndex = Landscape->GetLayerIndex(LayerName); + HOUDINI_TEST_EQUAL_ON_FAIL(EditLayerIndex, INDEX_NONE, return true); + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, Landscape, LayerName]() + { + if(!Context->Update()) + return false; + + // Make sure the layer was created. + int32 EditLayerIndex = Landscape->GetLayerIndex(LayerName); + HOUDINI_TEST_NOT_EQUAL_ON_FAIL(EditLayerIndex, int32(INDEX_NONE), return true); + + // Now start clean. + Context->Cleanup(); + + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, Landscape, LayerName]() + { + if(!Context->Update()) + return false; + + // Make sure the layer was removed. + int32 EditLayerIndex = Landscape->GetLayerIndex(LayerName); + HOUDINI_TEST_EQUAL_ON_FAIL(EditLayerIndex, int32(INDEX_NONE), return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_LandscapesBakedModiy, "Houdini.UnitTests.PCG.Landscapes.Baked.Modify", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + + bool FHoudiniEditorTestPCG_LandscapesBakedModiy::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGLandscapeMods/PCGTestLandscapeLevel.map")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + + ALandscape* Landscape = nullptr; + for(TActorIterator It(Context->PCGComponent->GetWorld()); It; ++It) + { + Landscape = Cast(*It); + if(Landscape) + break; + } + + FName LayerName = TEXT("Noise"); + + if (!Landscape) + return false; + + int32 EditLayerIndex = Landscape->GetLayerIndex(LayerName); + HOUDINI_TEST_EQUAL_ON_FAIL(EditLayerIndex, INDEX_NONE, return true); + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, Landscape, LayerName]() + { + if(!Context->Update()) + return false; + + // Make sure the layer was created. + int32 EditLayerIndex = Landscape->GetLayerIndex(LayerName); + HOUDINI_TEST_NOT_EQUAL_ON_FAIL(EditLayerIndex, int32(INDEX_NONE), return true); + + // Now start clean. + Context->Cleanup(); + + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, Landscape, LayerName]() + { + if(!Context->Update()) + return false; + + // Make sure the layer was removed. + int32 EditLayerIndex = Landscape->GetLayerIndex(LayerName); + HOUDINI_TEST_EQUAL_ON_FAIL(EditLayerIndex, int32(INDEX_NONE), return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_PCGNativeOutputsCooked, "Houdini.UnitTests.PCG.PCGOutputs.Cooked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_PCGNativeOutputsCooked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGInputsOutputs/PCGTestOutputsLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + FString AssetPath = TEXT("/Game/"); + FString AssetName = TEXT("PCG_Out"); + FString PCGAssetFullPath = FString::Printf(TEXT("%s/%s"), *AssetPath, *AssetName); + + UPCGGraphInstance* GraphInstance = Context->PCGComponent->GetGraphInstance(); + GraphInstance->SetGraphParameter(FName("out_path"), AssetPath); + GraphInstance->SetGraphParameter(FName("out_name"), AssetName); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, PCGAssetFullPath]() + { + if(!Context->Update()) + return false; + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *PCGAssetFullPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 4, return true); + + for (int TagIndex = 0; TagIndex < PCGDataAsset->Data.TaggedData.Num(); TagIndex++) + { + auto& TaggedData = PCGDataAsset->Data.TaggedData[TagIndex]; + TSet & Tags = TaggedData.Tags; + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // CHECK POINTS OUTPUT + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if (Tags.Contains(TEXT("Points"))) + { + + TArray ExpectedPoints; + ExpectedPoints.SetNum(8); + float CubeSize = 50.0f; + ExpectedPoints[0].Transform.SetLocation(FVector3d(+CubeSize, +CubeSize, -CubeSize)); + ExpectedPoints[1].Transform.SetLocation(FVector3d(-CubeSize, +CubeSize, -CubeSize)); + ExpectedPoints[2].Transform.SetLocation(FVector3d(+CubeSize, +CubeSize, +CubeSize)); + ExpectedPoints[3].Transform.SetLocation(FVector3d(-CubeSize, +CubeSize, +CubeSize)); + ExpectedPoints[4].Transform.SetLocation(FVector3d(-CubeSize, -CubeSize, -CubeSize)); + ExpectedPoints[5].Transform.SetLocation(FVector3d(+CubeSize, -CubeSize, -CubeSize)); + ExpectedPoints[6].Transform.SetLocation(FVector3d(-CubeSize, -CubeSize, +CubeSize)); + ExpectedPoints[7].Transform.SetLocation(FVector3d(+CubeSize, -CubeSize, +CubeSize)); + + for (int PointIndex = 0; PointIndex < ExpectedPoints.Num(); PointIndex++) + { + ExpectedPoints[PointIndex].Color = FVector4(0.25, 0.5, 0.75, 1.0); + ExpectedPoints[PointIndex].Density = 0.5; + ExpectedPoints[PointIndex].Steepness = 0.25; + ExpectedPoints[PointIndex].Seed = static_cast(PointIndex); + } + const UPCGPointData* PCGPointData = Cast(TaggedData.Data.Get()); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGPointData, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(PCGPointData->GetNumPoints(), 8, continue); + for (int PointIndex = 0; PointIndex < PCGPointData->GetNumPoints(); PointIndex++) + { + FPCGPoint Point = PCGPointData->GetPoint(PointIndex); + HOUDINI_TEST_EQUAL_ON_FAIL(Point.Transform.GetLocation(), ExpectedPoints[PointIndex].Transform.GetLocation(), continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Point.Color, ExpectedPoints[PointIndex].Color, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Point.Density, ExpectedPoints[PointIndex].Density, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Point.Steepness, ExpectedPoints[PointIndex].Steepness, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Point.Seed, ExpectedPoints[PointIndex].Seed, continue); + } + + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGPointData); + auto* BaseAttr = PCGDataObject->FindAttribute(TEXT("TestInt")); + auto* AttrInt = Cast< UHoudiniPCGDataAttributeInt>(BaseAttr); + + HOUDINI_TEST_NOT_NULL_ON_FAIL(AttrInt, return true); + + HOUDINI_TEST_EQUAL(AttrInt->GetNumValues(), 8); + for(int Index = 0; Index < AttrInt->GetNumValues(); Index++) + { + HOUDINI_TEST_EQUAL(AttrInt->Values[Index], Index * 10); + } + + BaseAttr = PCGDataObject->FindAttribute(TEXT("TestFloat")); + auto* AttrFloat = Cast< UHoudiniPCGDataAttributeFloat>(BaseAttr); + + HOUDINI_TEST_NOT_NULL_ON_FAIL(AttrFloat, return true); + + HOUDINI_TEST_EQUAL(AttrFloat->GetNumValues(), 8); + for(int Index = 0; Index < AttrFloat->GetNumValues(); Index++) + { + HOUDINI_TEST_EQUAL(AttrFloat->Values[Index], Index * 10.0f); + } + + BaseAttr = PCGDataObject->FindAttribute(TEXT("TestString")); + auto* AttrString = Cast< UHoudiniPCGDataAttributeString>(BaseAttr); + + HOUDINI_TEST_NOT_NULL_ON_FAIL(AttrString, return true); + + HOUDINI_TEST_EQUAL(AttrString->GetNumValues(), 8); + for(int Index = 0; Index < AttrString->GetNumValues(); Index++) + { + FString Expected = FString::Printf(TEXT("str-%d"), Index); + HOUDINI_TEST_EQUAL(AttrString->Values[Index], Expected); + } + + BaseAttr = PCGDataObject->FindAttribute(TEXT("TestVec3")); + auto* AttrVec3 = Cast(BaseAttr); + + HOUDINI_TEST_NOT_NULL_ON_FAIL(AttrVec3, return true); + + HOUDINI_TEST_EQUAL(AttrVec3->GetNumValues(), 8); + for(int Index = 0; Index < AttrVec3->GetNumValues(); Index++) + { + FVector3d Expected = FVector3d(Index * 1.0f, Index * 2.0, Index * 3.0); + HOUDINI_TEST_EQUAL(AttrVec3->Values[Index], Expected); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK VERTICES + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Vertices"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGParam); + auto * VertexIds = Cast(PCGDataObject->FindAttribute(TEXT("__vertex_id"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(VertexIds, continue); + + // 3 vertices per triangle, 2 triangles per face = 6 * 2 * 3 + HOUDINI_TEST_EQUAL(VertexIds->Values.Num(), 36); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK PRIMITIVES + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Primitives"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGParam); + + auto* PrimitiveIds = Cast(PCGDataObject->FindAttribute(TEXT("__primitive_id"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PrimitiveIds, continue); + + // 3 vertices per triangle, 2 triangles per face + HOUDINI_TEST_EQUAL(PrimitiveIds->Values.Num(), 12); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK DETAILS + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Details"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGParam); + + { + UHoudiniPCGDataAttributeInt64* Attrs = Cast(PCGDataObject->FindAttribute(TEXT("__primitivelist"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Attrs, continue); + HOUDINI_TEST_EQUAL(Attrs->Values.Num(), 1); + } + { + UHoudiniPCGDataAttributeInt64* Attrs = Cast(PCGDataObject->FindAttribute(TEXT("__topology"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Attrs, continue); + HOUDINI_TEST_EQUAL(Attrs->Values.Num(), 1); + } + { + UHoudiniPCGDataAttributeInt* Attrs = Cast(PCGDataObject->FindAttribute(TEXT("unreal_pcg_params"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Attrs, continue); + HOUDINI_TEST_EQUAL(Attrs->Values.Num(), 1); + } + } + } + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_PCGNativeOutputsBaked, "Houdini.UnitTests.PCG.PCGOutputs.Baked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + + bool FHoudiniEditorTestPCG_PCGNativeOutputsBaked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGInputsOutputs/PCGTestOutputsLevelBaked.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + FString AssetPath = TEXT("/Game/"); + FString AssetName = TEXT("PCG_Out"); + FString PCGAssetFullPath = FString::Printf(TEXT("%s/%s"), *AssetPath, *AssetName); + + UPCGGraphInstance* GraphInstance = Context->PCGComponent->GetGraphInstance(); + GraphInstance->SetGraphParameter(FName("out_path"), AssetPath); + GraphInstance->SetGraphParameter(FName("out_name"), AssetName); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, PCGAssetFullPath]() + { + if(!Context->Update()) + return false; + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *PCGAssetFullPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 4, return true); + + for(int TagIndex = 0; TagIndex < PCGDataAsset->Data.TaggedData.Num(); TagIndex++) + { + auto& TaggedData = PCGDataAsset->Data.TaggedData[TagIndex]; + TSet& Tags = TaggedData.Tags; + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // CHECK POINTS OUTPUT + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Points"))) + { + + TArray ExpectedPoints; + ExpectedPoints.SetNum(8); + float CubeSize = 50.0f; + ExpectedPoints[0].Transform.SetLocation(FVector3d(+CubeSize, +CubeSize, -CubeSize)); + ExpectedPoints[1].Transform.SetLocation(FVector3d(-CubeSize, +CubeSize, -CubeSize)); + ExpectedPoints[2].Transform.SetLocation(FVector3d(+CubeSize, +CubeSize, +CubeSize)); + ExpectedPoints[3].Transform.SetLocation(FVector3d(-CubeSize, +CubeSize, +CubeSize)); + ExpectedPoints[4].Transform.SetLocation(FVector3d(-CubeSize, -CubeSize, -CubeSize)); + ExpectedPoints[5].Transform.SetLocation(FVector3d(+CubeSize, -CubeSize, -CubeSize)); + ExpectedPoints[6].Transform.SetLocation(FVector3d(-CubeSize, -CubeSize, +CubeSize)); + ExpectedPoints[7].Transform.SetLocation(FVector3d(+CubeSize, -CubeSize, +CubeSize)); + + for(int PointIndex = 0; PointIndex < ExpectedPoints.Num(); PointIndex++) + { + ExpectedPoints[PointIndex].Color = FVector4(0.25, 0.5, 0.75, 1.0); + ExpectedPoints[PointIndex].Density = 0.5; + ExpectedPoints[PointIndex].Steepness = 0.25; + ExpectedPoints[PointIndex].Seed = static_cast(PointIndex); + } + const UPCGPointData* PCGPointData = Cast(TaggedData.Data.Get()); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGPointData, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(PCGPointData->GetNumPoints(), 8, continue); + for(int PointIndex = 0; PointIndex < PCGPointData->GetNumPoints(); PointIndex++) + { + FPCGPoint Point = PCGPointData->GetPoint(PointIndex); + HOUDINI_TEST_EQUAL_ON_FAIL(Point.Transform.GetLocation(), ExpectedPoints[PointIndex].Transform.GetLocation(), continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Point.Color, ExpectedPoints[PointIndex].Color, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Point.Density, ExpectedPoints[PointIndex].Density, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Point.Steepness, ExpectedPoints[PointIndex].Steepness, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Point.Seed, ExpectedPoints[PointIndex].Seed, continue); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK VERTICES + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Vertices"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGParam); + auto* VertexIds = Cast(PCGDataObject->FindAttribute(TEXT("__vertex_id"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(VertexIds, continue); + + // 3 vertices per triangle, 2 triangles per face = 6 * 2 * 3 + HOUDINI_TEST_EQUAL(VertexIds->Values.Num(), 36); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK PRIMITIVES + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Primitives"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGParam); + + auto* PrimitiveIds = Cast(PCGDataObject->FindAttribute(TEXT("__primitive_id"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PrimitiveIds, continue); + + // 3 vertices per triangle, 2 triangles per face + HOUDINI_TEST_EQUAL(PrimitiveIds->Values.Num(), 12); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK DETAILS + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Details"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGParam); + + { + UHoudiniPCGDataAttributeInt64* Attrs = Cast(PCGDataObject->FindAttribute(TEXT("__primitivelist"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Attrs, continue); + HOUDINI_TEST_EQUAL(Attrs->Values.Num(), 1); + } + { + UHoudiniPCGDataAttributeInt64* Attrs = Cast(PCGDataObject->FindAttribute(TEXT("__topology"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Attrs, continue); + HOUDINI_TEST_EQUAL(Attrs->Values.Num(), 1); + } + { + UHoudiniPCGDataAttributeInt* Attrs = Cast(PCGDataObject->FindAttribute(TEXT("unreal_pcg_params"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Attrs, continue); + HOUDINI_TEST_EQUAL(Attrs->Values.Num(), 1); + } + } + } + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_PCGNativeInputsCooked, "Houdini.UnitTests.PCG.PCGInputs.Cooked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_PCGNativeInputsCooked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGInputsOutputs/PCGTestInputsLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + FString AssetPath = TEXT("/Game/"); + FString AssetName = TEXT("PCG_Out"); + FString PCGAssetFullPath = FString::Printf(TEXT("%s/%s"), *AssetPath, *AssetName); + + UPCGGraphInstance* GraphInstance = Context->PCGComponent->GetGraphInstance(); + GraphInstance->SetGraphParameter(FName("out_path"), AssetPath); + GraphInstance->SetGraphParameter(FName("out_name"), AssetName); + + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, PCGAssetFullPath]() + { + if(!Context->Update()) + return false; + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *PCGAssetFullPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 4, return true); + + for(int TagIndex = 0; TagIndex < PCGDataAsset->Data.TaggedData.Num(); TagIndex++) + { + auto& TaggedData = PCGDataAsset->Data.TaggedData[TagIndex]; + TSet& Tags = TaggedData.Tags; + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // CHECK POINTS OUTPUT + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Points"))) + { + const UPCGPointData* PCGPointData = Cast(TaggedData.Data.Get()); + HOUDINI_TEST_EQUAL(PCGPointData->GetNumPoints(), 148); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK VERTICES + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Vertices"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* VerticesObject = NewObject(); + VerticesObject->SetFromPCGData(PCGParam); + HOUDINI_TEST_EQUAL(VerticesObject->Attributes.Num(), 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK PRIMITIVES + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Primitives"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* PrimitivesObject = NewObject(); + PrimitivesObject->SetFromPCGData(PCGParam); + HOUDINI_TEST_EQUAL(PrimitivesObject->Attributes.Num(), 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK DETAILS + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Details"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* DetailsObject = NewObject(); + DetailsObject->SetFromPCGData(PCGParam); + // topology, primitive list and unreal_pcg_params + HOUDINI_TEST_EQUAL(DetailsObject->Attributes.Num(), 3); + } + } + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_PCGNativeMultiInputsCooked, "Houdini.UnitTests.PCG.PCGMultiInputs.Cooked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_PCGNativeMultiInputsCooked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGInputsOutputs/PCGMultipleInputsLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + FString AssetPath = TEXT("/Game/"); + FString AssetName = TEXT("PCG_Out"); + FString PCGAssetFullPath = FString::Printf(TEXT("%s/%s"), *AssetPath, *AssetName); + + UPCGGraphInstance* GraphInstance = Context->PCGComponent->GetGraphInstance(); + GraphInstance->SetGraphParameter(FName("out_path"), AssetPath); + GraphInstance->SetGraphParameter(FName("out_name"), AssetName); + + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, PCGAssetFullPath]() + { + if(!Context->Update()) + return false; + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *PCGAssetFullPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 4, return true); + + for(int TagIndex = 0; TagIndex < PCGDataAsset->Data.TaggedData.Num(); TagIndex++) + { + auto& TaggedData = PCGDataAsset->Data.TaggedData[TagIndex]; + TSet& Tags = TaggedData.Tags; + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // CHECK POINTS OUTPUT + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Points"))) + { + const UPCGPointData* PCGPointData = Cast(TaggedData.Data.Get()); + HOUDINI_TEST_EQUAL(PCGPointData->GetNumPoints(), 148); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK VERTICES + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Vertices"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* VerticesObject = NewObject(); + VerticesObject->SetFromPCGData(PCGParam); + HOUDINI_TEST_EQUAL(VerticesObject->Attributes.Num(), 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK PRIMITIVES + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Primitives"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* PrimitivesObject = NewObject(); + PrimitivesObject->SetFromPCGData(PCGParam); + HOUDINI_TEST_EQUAL(PrimitivesObject->Attributes.Num(), 0); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK DETAILS + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Details"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* DetailsObject = NewObject(); + DetailsObject->SetFromPCGData(PCGParam); + // topology, primitive list and unreal_pcg_params + HOUDINI_TEST_EQUAL(DetailsObject->Attributes.Num(), 3); + } + } + + return true; + })); + + return true; +} + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_PCGSplinesCooked, "Houdini.UnitTests.PCG.PCGSplines.Cooked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_PCGSplinesCooked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGSplines/PCGSplinesLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + FString AssetPath = TEXT("/Game/"); + FString AssetName = TEXT("PCG_Out"); + FString PCGAssetFullPath = FString::Printf(TEXT("%s/%s"), *AssetPath, *AssetName); + + UPCGGraphInstance* GraphInstance = Context->PCGComponent->GetGraphInstance(); + GraphInstance->SetGraphParameter(FName("out_path"), AssetPath); + GraphInstance->SetGraphParameter(FName("out_name"), AssetName); + + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, PCGAssetFullPath]() + { + if(!Context->Update()) + return false; + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *PCGAssetFullPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 2, return true); + + { + const UPCGSplineData* PCGSplineData = Cast(PCGDataAsset->Data.TaggedData[0].Data); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + HOUDINI_TEST_EQUAL_ON_FAIL(PCGSplineData->SplineStruct.GetNumberOfPoints(), 4, return true); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[0].OutVal, FVector(-542.820597, 742.795944, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[1].OutVal, FVector(588.031721, 665.821791, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[2].OutVal, FVector(319.931078, -1339.993763, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[3].OutVal, FVector(864.991283, -1053.272057, 0.000000)); +#else + HOUDINI_TEST_EQUAL_ON_FAIL(PCGSplineData->SplineStruct.SplineCurves.Position.Points.Num(), 4, return true); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[0].OutVal,FVector(-542.820597, 742.795944, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[1].OutVal, FVector(588.031721, 665.821791, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[2].OutVal, FVector(319.931078, -1339.993763, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[3].OutVal, FVector(864.991283, -1053.272057, 0.000000)); +#endif + } + + { + const UPCGSplineData* PCGSplineData = Cast(PCGDataAsset->Data.TaggedData[1].Data); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetNumberOfPoints(), 3); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[0].OutVal, FVector(100.000000, 0.000000, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[1].OutVal, FVector(200.000000, 0.000000, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[2].OutVal, FVector(200.000000, -60.000002, 0.000000)); +#else + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points.Num(), 3); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[0].OutVal, FVector(100.000000, 0.000000, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[1].OutVal, FVector(200.000000, 0.000000, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[2].OutVal, FVector(200.000000, -60.000002, 0.000000)); +#endif + } + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_PCGSplinesBaked, "Houdini.UnitTests.PCG.PCGSplines.Baked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_PCGSplinesBaked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGSplines/PCGSplinesLevelBaked.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + FString AssetPath = TEXT("/Game/"); + FString AssetName = TEXT("PCG_Out"); + FString PCGAssetFullPath = FString::Printf(TEXT("%s/%s"), *AssetPath, *AssetName); + + UPCGGraphInstance* GraphInstance = Context->PCGComponent->GetGraphInstance(); + GraphInstance->SetGraphParameter(FName("out_path"), AssetPath); + GraphInstance->SetGraphParameter(FName("out_name"), AssetName); + + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context, PCGAssetFullPath]() + { + if(!Context->Update()) + return false; + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *PCGAssetFullPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 2, return true); + + { + const UPCGSplineData* PCGSplineData = Cast(PCGDataAsset->Data.TaggedData[0].Data); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + HOUDINI_TEST_EQUAL_ON_FAIL(PCGSplineData->SplineStruct.GetNumberOfPoints(), 4, return true); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[0].OutVal, FVector(-542.820597, 742.795944, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[1].OutVal, FVector(588.031721, 665.821791, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[2].OutVal, FVector(319.931078, -1339.993763, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[3].OutVal, FVector(864.991283, -1053.272057, 0.000000)); +#else + HOUDINI_TEST_EQUAL_ON_FAIL(PCGSplineData->SplineStruct.SplineCurves.Position.Points.Num(), 4, return true); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[0].OutVal, FVector(-542.820597, 742.795944, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[1].OutVal, FVector(588.031721, 665.821791, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[2].OutVal, FVector(319.931078, -1339.993763, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[3].OutVal, FVector(864.991283, -1053.272057, 0.000000)); +#endif + } + + { + const UPCGSplineData* PCGSplineData = Cast(PCGDataAsset->Data.TaggedData[1].Data); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetNumberOfPoints(), 3); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[0].OutVal, FVector(100.000000, 0.000000, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[1].OutVal, FVector(200.000000, 0.000000, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.GetSplinePointsPosition().Points[2].OutVal, FVector(200.000000, -60.000002, 0.000000)); + +#else + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points.Num(), 3); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[0].OutVal, FVector(100.000000, 0.000000, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[1].OutVal, FVector(200.000000, 0.000000, 0.000000)); + HOUDINI_TEST_EQUAL(PCGSplineData->SplineStruct.SplineCurves.Position.Points[2].OutVal, FVector(200.000000, -60.000002, 0.000000)); +#endif + } + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_PCGParametersDefaultsCooked, "Houdini.UnitTests.PCG.Parameters.Defaults.Cooked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_PCGParametersDefaultsCooked::RunTest(const FString& Parameters) +{ + // This test uses a simple HDA which reads its parameter and sets it back on the output. It tests whether the HDA can process + // its default parameter. + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGTestParameters/PCGTestParametersDefaultLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context]() + { + if(!Context->Update()) + return false; + + FString OutputPath = TEXT("/Game/HoudiniEngine/Temp/ParametersOutput"); + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *OutputPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 4, return true); + + for(int TagIndex = 0; TagIndex < PCGDataAsset->Data.TaggedData.Num(); TagIndex++) + { + auto& TaggedData = PCGDataAsset->Data.TaggedData[TagIndex]; + TSet& Tags = TaggedData.Tags; + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK DETAILS for the results. + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Details"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGParam); + + { + UHoudiniPCGDataAttributeInt* TestOutput = Cast(PCGDataObject->FindAttribute(TEXT("test_output"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(TestOutput, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(TestOutput->Values.Num(), 1, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(TestOutput->Values[0], 3, return true); + + } + { + UHoudiniPCGDataAttributeInt* Attrs = Cast(PCGDataObject->FindAttribute(TEXT("unreal_pcg_params"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Attrs, continue); + HOUDINI_TEST_EQUAL(Attrs->Values.Num(), 1); + } + } + } + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_PCGParametersMultiparm, "Houdini.UnitTests.PCG.Parameters.Defaults.Multiparm", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + + bool FHoudiniEditorTestPCG_PCGParametersMultiparm::RunTest(const FString& Parameters) +{ + // This test uses a simple HDA which reads its parameter and sets it back on the output. It tests whether the HDA can process + // its default parameter. + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGTestParameters/PCGMultiTestParmsLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context]() + { + if(!Context->Update()) + return false; + + FString OutputPath = TEXT("/Game/HoudiniEngine/Temp/ParametersOutput"); + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *OutputPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 2, return true); + + UInstancedStaticMeshComponent* ISM = nullptr; + + for (int Index = 0; Index < 2; Index++) + { + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGDataAsset->Data.TaggedData[Index].Data.Get()); + + if (!IsValid(ISM)) + ISM = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("component"))); + + } + + // The HDA creates 5 instances, jamming a 100.0 * instance number in the transform + + HOUDINI_TEST_NOT_NULL_ON_FAIL(ISM, return true); + if (!ISM) + return true; + + HOUDINI_TEST_EQUAL(ISM->GetNumInstances(), 5); + for (int Index = 0; Index < 5; Index++) + { + FTransform Transform; + ISM->GetInstanceTransform(Index, Transform); + HOUDINI_TEST_EQUAL(Transform.GetLocation().X, (Index + 1) * 500.0); + } + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_PCGParametersSetCooked, "Houdini.UnitTests.PCG.Parameters.Set.Cooked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_PCGParametersSetCooked::RunTest(const FString& Parameters) +{ + // This test uses a simple HDA which reads its parameter and sets it back on the output. It tests whether the HDA can process + // a set parameter. + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGTestParameters/PCGTestParametersSetLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context]() + { + if(!Context->Update()) + return false; + + FString OutputPath = TEXT("/Game/HoudiniEngine/Temp/ParametersOutput"); + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *OutputPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 4, return true); + + for(int TagIndex = 0; TagIndex < PCGDataAsset->Data.TaggedData.Num(); TagIndex++) + { + auto& TaggedData = PCGDataAsset->Data.TaggedData[TagIndex]; + TSet& Tags = TaggedData.Tags; + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK DETAILS for the results. + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Details"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGParam); + + { + UHoudiniPCGDataAttributeInt* TestOutput = Cast(PCGDataObject->FindAttribute(TEXT("test_output"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(TestOutput, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(TestOutput->Values.Num(), 1, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(TestOutput->Values[0], 5, return true); + + } + { + UHoudiniPCGDataAttributeInt* Attrs = Cast(PCGDataObject->FindAttribute(TEXT("unreal_pcg_params"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Attrs, continue); + HOUDINI_TEST_EQUAL(Attrs->Values.Num(), 1); + } + } + } + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_PCGParametersOverrideCooked, "Houdini.UnitTests.PCG.Parameters.Overrides.Cooked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_PCGParametersOverrideCooked::RunTest(const FString& Parameters) +{ + // This test uses a simple HDA which reads its parameter and sets it back on the output. It tests whether the HDA can process + // a set parameter. + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGTestParameters/PCGTestParametersOverridesLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + + AddCommand(new FFunctionLatentCommand([this, Context]() + { + if(!Context->Update()) + return false; + + FString OutputPath = TEXT("/Game/HoudiniEngine/Temp/ParametersOutput"); + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *OutputPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 4, return true); + + for(int TagIndex = 0; TagIndex < PCGDataAsset->Data.TaggedData.Num(); TagIndex++) + { + auto& TaggedData = PCGDataAsset->Data.TaggedData[TagIndex]; + TSet& Tags = TaggedData.Tags; + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CHECK DETAILS for the results. + /////////////////////////////////////////////////////////////////////////////////////////////////////// + + if(Tags.Contains(TEXT("Details"))) + { + const UPCGParamData* PCGParam = Cast(TaggedData.Data.Get()); + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGParam); + + { + UHoudiniPCGDataAttributeInt* TestOutput = Cast(PCGDataObject->FindAttribute(TEXT("test_output"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(TestOutput, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(TestOutput->Values.Num(), 1, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(TestOutput->Values[0], 9, return true); + + } + { + UHoudiniPCGDataAttributeInt* Attrs = Cast(PCGDataObject->FindAttribute(TEXT("unreal_pcg_params"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Attrs, continue); + HOUDINI_TEST_EQUAL(Attrs->Values.Num(), 1); + } + } + } + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_InputSetCooked, "Houdini.UnitTests.PCG.Inputs.Set.Cooked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_InputSetCooked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGTestInputs/PCGTestInputsSetLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Test 1: Load a cube, then use it to generate a new cube. + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context]() + { + if(!Context->Update()) + return false; + + FString OutputPath = TEXT("/Game/HoudiniEngine/Temp/InputsOutput"); + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *OutputPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 1, return true); + // ... it should have data ... + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset->Data.TaggedData[0].Data.Get(), return true); + // ... which we'll now convert to an PCGDataObject so we can easily ready it... + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGDataAsset->Data.TaggedData[0].Data.Get()); + + // ... check we have a mesh + UStaticMesh* StaticMesh = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("object"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMesh, return true); + + // ... check the mesh's bounding box. + FBox Box = StaticMesh->GetBoundingBox(); + + HOUDINI_TEST_EQUAL(Box.Min.X, -50.0); + HOUDINI_TEST_EQUAL(Box.Min.Y, -50.0); + HOUDINI_TEST_EQUAL(Box.Min.Z, -50.0); + HOUDINI_TEST_EQUAL(Box.Max.X, 250.0); // <- duplicated: 2 extra boxes, so add 2x100 + HOUDINI_TEST_EQUAL(Box.Max.Y, 50.0); + HOUDINI_TEST_EQUAL(Box.Max.Z, 50.0); + + // ... check we have a mesh component + UStaticMeshComponent* StaticMeshComponent = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("component"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMeshComponent, return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_InputOverrideCooked, "Houdini.UnitTests.PCG.Inputs.Override.Cooked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_InputOverrideCooked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGTestInputs/PCGTestInputsOverrideLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Test 1: Load a cube, then use it to generate a new cube. + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context]() + { + if(!Context->Update()) + return false; + + FString OutputPath = TEXT("/Game/HoudiniEngine/Temp/InputsOutput"); + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *OutputPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have one output... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 1, return true); + // ... it should have data ... + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset->Data.TaggedData[0].Data.Get(), return true); + // ... which we'll now convert to an PCGDataObject so we can easily ready it... + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGDataAsset->Data.TaggedData[0].Data.Get()); + + // ... check we have a mesh + UStaticMesh* StaticMesh = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("object"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMesh, return true); + + // ... check the mesh's bounding box. + FBox Box = StaticMesh->GetBoundingBox(); + + HOUDINI_TEST_EQUAL(Box.Min.X, -50.0); + HOUDINI_TEST_EQUAL(Box.Min.Y, -50.0); + HOUDINI_TEST_EQUAL(Box.Min.Z, -50.0); + HOUDINI_TEST_EQUAL(Box.Max.X, 450.0); // <- duplicated: 2 extra boxes, so add 2x100 + HOUDINI_TEST_EQUAL(Box.Max.Y, 50.0); + HOUDINI_TEST_EQUAL(Box.Max.Z, 50.0); + + // ... check we have a mesh component + UStaticMeshComponent* StaticMeshComponent = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("component"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMeshComponent, return true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_ForLoopsCooked, "Houdini.UnitTests.PCG.Inputs.ForLoops.Cooked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_ForLoopsCooked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGForLoops/PCGForLoopsLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Test 1: Load a cube, then use it to generate a new cube. + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context]() + { + if(!Context->Update()) + return false; + + FString OutputPath = TEXT("/Game/HoudiniEngine/Temp/TestForLoop"); + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *OutputPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have 5 outputs... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 1, return true); + + // ... it should have data ... + + + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset->Data.TaggedData[0].Data.Get(), return true); + // ... which we'll now convert to an PCGDataObject so we can easily ready it... + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGDataAsset->Data.TaggedData[0].Data.Get()); + + + // ... check we have a mesh for each point + for(int Index = 0; Index < 5; Index++) + { + + UStaticMesh* StaticMesh = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("object"), Index)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMesh, return true); + + // ... check the mesh's bounding box. + FBox Box = StaticMesh->GetBoundingBox(); + + double expectedSize = 100.0 * static_cast(Index + 1); + + double size = Box.Max.X - Box.Min.X; + + // Accurate to 1% as copy to points is not that accurate. + HOUDINI_TEST_EQUAL(size, expectedSize, expectedSize * 0.01); + + // ... check we have a mesh component + UStaticMeshComponent* StaticMeshComponent = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("component"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMeshComponent, return true); + } + + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_PDGCooked, "Houdini.UnitTests.PCG.PDG.Cooked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPCG_PDGCooked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGPDG/PCGPDGTestLevel.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Test 1: Load a cube, then use it to generate a new cube. + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context]() + { + if(!Context->Update()) + return false; + + FString OutputPath = TEXT("/Game/HoudiniEngine/Temp/TestPDGOutput"); + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *OutputPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have 5 outputs... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 6, return true); + + // ... it should have data ... + + // ... check we have a mesh for each point + for(int Index = 0; Index < 6; Index++) + { + + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset->Data.TaggedData[Index].Data.Get(), return true); + // ... which we'll now convert to an PCGDataObject so we can easily ready it... + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGDataAsset->Data.TaggedData[0].Data.Get()); + + UStaticMesh* StaticMesh = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("object"), 0)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMesh, return true); + + // ... check we have a mesh component + UStaticMeshComponent* StaticMeshComponent = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("component"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMeshComponent, return true); + } + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPCG_PDGBaked, "Houdini.UnitTests.PCG.PDG.Baked", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + + bool FHoudiniEditorTestPCG_PDGBaked::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + FString MapName(TEXT("/Game/TestHDAs/PCG/PCGPDG/PCGPDGTestLevelBaked.umap")); + TSharedPtr Context(new EHoudiniTestPCGContext()); + Context->LoadPCGTestMap(MapName); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->PCGComponent, return true); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Test 1: Load a cube, then use it to generate a new cube. + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FFunctionLatentCommand([Context] + { + Context->CleanupAndGenerateAsync(); + return true; + })); + + AddCommand(new FFunctionLatentCommand([this, Context]() + { + if(!Context->Update()) + return false; + + FString OutputPath = TEXT("/Game/HoudiniEngine/Temp/TestPDGOutput"); + + UPCGDataAsset* PCGDataAsset = Cast(StaticLoadObject(UPCGDataAsset::StaticClass(), nullptr, *OutputPath)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset, return true); + + // We should have 5 outputs... + HOUDINI_TEST_EQUAL_ON_FAIL(PCGDataAsset->Data.TaggedData.Num(), 6, return true); + + // ... it should have data ... + + // ... check we have a mesh for each point + for(int Index = 0; Index < 6; Index++) + { + + HOUDINI_TEST_NOT_NULL_ON_FAIL(PCGDataAsset->Data.TaggedData[Index].Data.Get(), return true); + // ... which we'll now convert to an PCGDataObject so we can easily ready it... + UHoudiniPCGDataObject* PCGDataObject = NewObject(); + PCGDataObject->SetFromPCGData(PCGDataAsset->Data.TaggedData[0].Data.Get()); + + UStaticMesh* StaticMesh = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("object"), 0)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMesh, return true); + + // ... check we have a mesh component + UStaticMeshComponent* StaticMeshComponent = Cast(FHoudiniEditorTestPCG::GetOutputObject(PCGDataObject, TEXT("component"))); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StaticMeshComponent, return true); + } + return true; + })); + + return true; +} + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestPCG.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +class UHoudiniPCGDataObject; + +class FHoudiniEditorTestPCG +{ +public: + + + static UObject* GetOutputObject(UHoudiniPCGDataObject* InputObject, const FString& Field, int Index = 0); +}; + +#endif +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestPDG.cpp`: + +```cpp +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestBaking.h" + +#include "HoudiniCookable.h" +#include "HoudiniEditorTestPDG.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterString.h" +#include "HoudiniParameterToggle.h" + +#include "Chaos/HeightField.h" +#include "Materials/Material.h" + +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" +#include "HoudiniEditorUnitTestUtils.h" + +#include "Components/InstancedStaticMeshComponent.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "Misc/AutomationTest.h" +#include + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPDGCommandletMesh, "Houdini.UnitTests.PDG.Commandlet.MeshExternalMaterials", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPDGCommandletMesh::RunTest(const FString& Parameters) +{ + FHoudiniEngine::Get().StartPDGCommandlet(); + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestPDG::TestHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + TSharedPtr WorkItemsComplete = MakeShared(0); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "pig_head", false, 0); + + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, WorkItemsComplete]() + { + UHoudiniPDGAssetLink* AssetLink = Context->GetPDGAssetLink(); + HOUDINI_TEST_NOT_NULL_ON_FAIL(AssetLink, return true); + + AssetLink->OnWorkResultObjectLoaded.AddLambda([this, WorkItemsComplete](UHoudiniPDGAssetLink* AL, UTOPNode* Node, int32 WorkItemArrayIndex, int32 WorkItemResultInfoIndex) + { + (*WorkItemsComplete)++; + }); + + + + bool bSuccess = Context->StartCookingSelectedTOPNetwork(); + HOUDINI_TEST_EQUAL(bSuccess, true); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, WorkItemsComplete]() + { + UHoudiniPDGAssetLink* AssetLink = Context->GetPDGAssetLink(); + HOUDINI_TEST_NOT_NULL_ON_FAIL(AssetLink, return true); + + UTOPNetwork* Network = AssetLink->GetTOPNetwork(0); + HOUDINI_TEST_NOT_NULL(Network); + + if (*WorkItemsComplete != 2) + { + return false; + } + + UTOPNode* Node = nullptr; + for(UTOPNode* It : Network->AllTOPNodes) + { + if(It->NodeName == "HE_OUT_X") + { + Node = It; + break; + } + } + HOUDINI_TEST_NOT_NULL(Node); + if (!Node) + return true; + + HOUDINI_TEST_EQUAL_ON_FAIL(Node->WorkResult.Num(), 2, return true); + + + for(auto& Result : Node->WorkResult) + { + auto ResultOutputs = Result.ResultObjects[0].GetResultOutputs(); + HOUDINI_TEST_EQUAL_ON_FAIL(ResultOutputs.Num(), 1, return true); + + UHoudiniOutput* Output = ResultOutputs[0]; + + TArray OutputObjects; + Output->GetOutputObjects().GenerateValueArray(OutputObjects); + + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObjects.Num(), 1, return true); + + const FHoudiniOutputObject& OutputObject = OutputObjects[0]; + + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObject.OutputComponents.Num(), 1, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObject.OutputComponents[0]->IsA(UStaticMeshComponent::StaticClass()), 1, return true); + + auto * SMC = Cast(OutputObject.OutputComponents[0]); + UStaticMesh * StaticMesh = SMC->GetStaticMesh(); + + int32 MaterialCount = StaticMesh->GetStaticMaterials().Num(); + HOUDINI_TEST_EQUAL_ON_FAIL(MaterialCount, 1, return true); + + UMaterial * Material = StaticMesh->GetMaterial(0)->GetMaterial(); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Material, return true); + + FString MaterialName = Material->GetPathName(); + + HOUDINI_TEST_EQUAL(MaterialName, TEXT("/Game/TestObjects/M_TestMaterial.M_TestMaterial")); + + + } + + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniEngine::Get().StopPDGCommandlet(); + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPDGCommandletMeshInternalMaterials, "Houdini.UnitTests.PDG.Commandlet.MeshInternalsMaterials", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPDGCommandletMeshInternalMaterials::RunTest(const FString& Parameters) +{ + FHoudiniEngine::Get().StartPDGCommandlet(); + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestPDG::TestHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + TSharedPtr WorkItemsComplete = MakeShared(0); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "pig_head", true, 0); + + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, WorkItemsComplete]() + { + UHoudiniPDGAssetLink* AssetLink = Context->GetPDGAssetLink(); + HOUDINI_TEST_NOT_NULL_ON_FAIL(AssetLink, return true); + + AssetLink->OnWorkResultObjectLoaded.AddLambda([this, WorkItemsComplete](UHoudiniPDGAssetLink* AL, UTOPNode* Node, int32 WorkItemArrayIndex, int32 WorkItemResultInfoIndex) + { + (*WorkItemsComplete)++; + }); + + + + bool bSuccess = Context->StartCookingSelectedTOPNetwork(); + HOUDINI_TEST_EQUAL(bSuccess, true); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, WorkItemsComplete]() + { + UHoudiniPDGAssetLink* AssetLink = Context->GetPDGAssetLink(); + HOUDINI_TEST_NOT_NULL_ON_FAIL(AssetLink, return true); + + UTOPNetwork* Network = AssetLink->GetTOPNetwork(0); + HOUDINI_TEST_NOT_NULL(Network); + + if(*WorkItemsComplete != 2) + { + return false; + } + + UTOPNode* Node = nullptr; + for(UTOPNode* It : Network->AllTOPNodes) + { + if(It->NodeName == "HE_OUT_X") + { + Node = It; + break; + } + } + HOUDINI_TEST_NOT_NULL(Node); + if (!Node) + return true; + + HOUDINI_TEST_EQUAL_ON_FAIL(Node->WorkResult.Num(), 2, return true); + + + for(auto& Result : Node->WorkResult) + { + auto ResultOutputs = Result.ResultObjects[0].GetResultOutputs(); + HOUDINI_TEST_EQUAL_ON_FAIL(ResultOutputs.Num(), 1, return true); + + UHoudiniOutput* Output = ResultOutputs[0]; + + TArray OutputObjects; + Output->GetOutputObjects().GenerateValueArray(OutputObjects); + + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObjects.Num(), 1, return true); + + const FHoudiniOutputObject& OutputObject = OutputObjects[0]; + + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObject.OutputComponents.Num(), 1, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(OutputObject.OutputComponents[0]->IsA(UStaticMeshComponent::StaticClass()), 1, return true); + + auto* SMC = Cast(OutputObject.OutputComponents[0]); + UStaticMesh* StaticMesh = SMC->GetStaticMesh(); + + int32 MaterialCount = StaticMesh->GetStaticMaterials().Num(); + HOUDINI_TEST_EQUAL_ON_FAIL(MaterialCount, 3, return true); + } + + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniEngine::Get().StopPDGCommandlet(); + return true; + })); + + return true; +} + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestPDG.h`: + +```h +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +class FHoudiniEditorTestPDG +{ +public: + const static inline FString TestHDA = TEXT("/Game/TestHDAs/PDG/PDGTestMesh"); +}; +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestPresets.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestPresets.h" + +#include "HoudiniCookable.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterString.h" +#include "HoudiniParameterToggle.h" +#include "HoudiniPreset.h" + +#include "Chaos/HeightField.h" +#include "Components/SkeletalMeshComponent.h" +#include "Engine/SkeletalMesh.h" + +#if WITH_DEV_AUTOMATION_TESTS + #include "HoudiniEditorTestUtils.h" + #include "HoudiniEditorUnitTestUtils.h" + + #include "Misc/AutomationTest.h" + #include "GenericPlatform/GenericPlatformProcess.h" + #include "Components/InstancedStaticMeshComponent.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "Engine/SkinnedAssetCommon.h" +#endif + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestPresetLoading, "Houdini.UnitTests.Presets.Loading", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestPresetLoading::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking of skeletal meshes. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, false)); + + UHoudiniPreset* Preset = Cast(StaticLoadObject(UHoudiniPreset::StaticClass(), nullptr, *FHoudiniEditorTestPresets::PresetAsset)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Preset, return true); + + volatile bool * bPresetInstantiated = new bool(false); + + Preset->PostInstantiationCallbacks.Add([bPresetInstantiated, Context](const UHoudiniPreset* Preset, UObject* InObject) + { + *bPresetInstantiated = true; + + UHoudiniCookable* HC = Cast(InObject); + if (HC) + Context->SetCookable(HC); + else + { + UHoudiniAssetComponent* HAC = Cast(InObject); + Context->SetHAC(HAC); + } + }); + + + UWorld * World = Context->World; + AActor* HoudiniAssetActor = FHoudiniEngineEditorUtils::InstantiateHoudiniAssetAt(Preset->SourceHoudiniAsset, FTransform::Identity, World, nullptr, Preset); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, bPresetInstantiated, Preset]() + { + if (*bPresetInstantiated == false) + return false; + delete bPresetInstantiated; + + Preset->PostInstantiationCallbacks.Empty(); + + HOUDINI_TEST_NOT_NULL_ON_FAIL(Context->GetHAC(), return true); + + TMap Parameters; + if (Context->GetCookable()) + { + for (int Index = 0; Index < Context->GetCookable()->GetNumParameters(); Index++) + { + auto* Parameter = Context->GetCookable()->GetParameterAt(Index); + Parameters.Add(Parameter->GetParameterName(), Parameter); + } + } + else + { + for (int Index = 0; Index < Context->GetHAC()->GetNumParameters(); Index++) + { + auto* Parameter = Context->GetHAC()->GetParameterAt(Index); + Parameters.Add(Parameter->GetParameterName(), Parameter); + } + } + + UHoudiniParameter * * Parm = nullptr; + UHoudiniParameterMultiParm * MultiParm; + + Parm = Parameters.Find(TEXT("multi_parm_folder1")); + MultiParm = Cast(Parm ? *Parm : nullptr); + HOUDINI_TEST_NOT_NULL_ON_FAIL(MultiParm, return true); + HOUDINI_TEST_EQUAL(MultiParm->GetInstanceCount(), 1); + + Parm = Parameters.Find(TEXT("multi_parm_folder2")); + MultiParm = Cast(Parm ? *Parm : nullptr); + HOUDINI_TEST_NOT_NULL_ON_FAIL(MultiParm, return true); + HOUDINI_TEST_EQUAL(MultiParm->GetInstanceCount(), 2); + + Parm = Parameters.Find(TEXT("multi_parm_nested1")); + MultiParm = Cast(Parm ? *Parm : nullptr); + HOUDINI_TEST_NOT_NULL_ON_FAIL(MultiParm, return true); + HOUDINI_TEST_EQUAL(MultiParm->GetInstanceCount(), 0); + + Parm = Parameters.Find(TEXT("multi_parm_nested2")); + MultiParm = Cast(Parm ? *Parm : nullptr); + HOUDINI_TEST_NOT_NULL_ON_FAIL(MultiParm, return true); + HOUDINI_TEST_EQUAL(MultiParm->GetInstanceCount(), 2); + + UHoudiniParameterInt * IntParm; + + Parm = Parameters.Find(TEXT("multi1_int1")); + IntParm = Cast(Parm ? *Parm : nullptr); + HOUDINI_TEST_NOT_NULL_ON_FAIL(IntParm, return true); + HOUDINI_TEST_EQUAL(IntParm->GetValue(0).GetValue(), 2); + + UHoudiniParameterFloat* FloatParm; + + Parm = Parameters.Find(TEXT("multi1_float1")); + FloatParm = Cast(Parm ? *Parm : nullptr); + HOUDINI_TEST_NOT_NULL_ON_FAIL(FloatParm, return true); + HOUDINI_TEST_EQUAL(FloatParm->GetValue(0).GetValue(), 0.0f); + + UHoudiniParameterString* StringParam; + + Parm = Parameters.Find(TEXT("multi_parm_double_nested2_1")); + StringParam = Cast(Parm ? *Parm : nullptr); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StringParam, return true); + HOUDINI_TEST_EQUAL(StringParam->GetValueAt(0), TEXT("b")); + + Parm = Parameters.Find(TEXT("multi_parm_double_nested2_2")); + StringParam = Cast(Parm ? *Parm : nullptr); + HOUDINI_TEST_NOT_NULL_ON_FAIL(StringParam, return true); + HOUDINI_TEST_EQUAL(StringParam->GetValueAt(0), TEXT("c")); + + + return true; + })); + + return true; +} + + + + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestPresets.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +class FHoudiniEditorTestPresets +{ +public: + const static inline FString PresetHda = TEXT("/Game/TestHDAs/SkeletalMesh/Test_Preset"); + const static inline FString PresetAsset = TEXT("/Script/HoudiniEngineEditor.HoudiniPreset'/Game/TestHDAs/Presets/Presets/Test_Preset.Test_Preset'"); +}; +#endif +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestProxyMesh.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestProxyMesh.h" + +#include "HoudiniPDGAssetLink.h" +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "HoudiniEditorUnitTestUtils.h" +#include "FoliageType_InstancedStaticMesh.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniEngineRuntimePrivatePCH.h" + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestsProxyMeshVertices, "Houdini.UnitTests.ProxyMesh.Vertices", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestsProxyMeshVertices::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Tests baking of hierarchical instanced meshes. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestProxyMeshes::HDAAsset, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(true); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + TArray StaticMeshOutputs = FHoudiniEditorUnitTestUtils::GetOutputsWithProxyComponent(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(StaticMeshOutputs.Num(), 1, return true); + + UHoudiniStaticMesh * Mesh = StaticMeshOutputs[0]->GetMesh(); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Mesh, return true); + + FHoudiniTestMeshData ExpectedMeshData = FHoudiniEditorUnitTestMeshUtils::GetExpectedMeshData(); + FHoudiniTestMeshData ActualData = FHoudiniEditorUnitTestMeshUtils::ExtractMeshData(*Mesh); + auto Errors = FHoudiniEditorUnitTestMeshUtils::CheckMesh(ExpectedMeshData , ActualData); + + HOUDINI_TEST_EQUAL(Errors.Num(), 0); + for(auto & Error : Errors) + HOUDINI_LOG_ERROR(TEXT("Mesh Error: %s"), *Error); + + return true; + })); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Bake + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + TArray& BakedOutputs = Context->GetBakedOutputs(); + // There should be two outputs as we have two meshes. + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + + // Go through each output and check we have two actors with one mesh component each. + TSet ActorNames; + for (auto& BakedOutput : BakedOutputs) + { + for (auto It : BakedOutput.BakedOutputObjects) + { + FHoudiniBakedOutputObject& OutputObject = It.Value; + + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Actor)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Actor, continue); + + TArray Components; + Actor->GetComponents(Components); + HOUDINI_TEST_EQUAL_ON_FAIL(Components.Num(), 1, continue); + HOUDINI_TEST_EQUAL_ON_FAIL(Components[0]->IsA(), 1, continue); + + UStaticMeshComponent * StaticMeshComponent = Cast(Components[0]); + UStaticMesh * StaticMesh = StaticMeshComponent->GetStaticMesh(); + + FHoudiniTestMeshData ExpectedMeshData = FHoudiniEditorUnitTestMeshUtils::GetExpectedMeshData(); + FHoudiniTestMeshData ActualData = FHoudiniEditorUnitTestMeshUtils::ExtractMeshData(*StaticMesh, 0); + auto Errors = FHoudiniEditorUnitTestMeshUtils::CheckMesh(ExpectedMeshData, ActualData); + + HOUDINI_TEST_EQUAL(Errors.Num(), 0); + for (auto& Error : Errors) + HOUDINI_LOG_ERROR(TEXT("Mesh Error: %s"), *Error); + + ActorNames.Add(*OutputObject.Actor); + } + } + + HOUDINI_TEST_EQUAL_ON_FAIL(ActorNames.Num(), 1, return true); + return true; + })); + + return true; +} + + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestProxyMesh.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#include "HoudiniEditorUnitTestMeshUtils.h" +#include "HoudiniStaticMesh.h" +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +class FHoudiniEditorTestProxyMeshes +{ +public: + const static inline FString HDAAsset = TEXT("/Game/TestHDAs/Mesh/Test_Mesh"); +}; +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestSkeletalMesh.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestSkeletalMesh.h" +#include "Components/SkeletalMeshComponent.h" +#include "Engine/SkeletalMesh.h" +#include "HoudiniParameterInt.h" +#include "HoudiniParameterString.h" +#include "HoudiniParameterToggle.h" +#include "Animation/SkeletalMeshActor.h" +#include "Chaos/HeightField.h" +#include "Animation/Skeleton.h" +#include "Evaluation/PreAnimatedState/MovieScenePreAnimatedCaptureSources.h" +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "HoudiniEditorUnitTestUtils.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 +#include "Engine/SkinnedAssetCommon.h" +#endif +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + #include "PhysicsEngine/BodySetup.h" + #include "PhysicsEngine/SkeletalBodySetup.h" +#endif +#include "PhysicsEngine/PhysicsAsset.h" + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestSkeletalMeshElectra, "Houdini.UnitTests.SkeletalMesh.Electra", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestSkeletalMeshElectra::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking of skeletal meshes. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestSkeletalMeshUtils::SkeletalMeshHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Cook and Bake Electra. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "unreal_skeleton", TEXT(""), 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + TArray SkeletalMeshComponents = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + TArray Skeleton = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMeshComponents.Num(), 1, return true); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + FString BakeFolder = Context->GetBakeFolderOrDefault(); + + // There should be one baked output object. + TArray& BakedOutputs = Context->GetBakedOutputs(); + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + auto & BakedOutput = BakedOutputs[0]; + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutput.BakedOutputObjects.Num(), 1, return true); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().ElementIt->Value.Value; +#else + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().Value(); +#endif + + HOUDINI_TEST_EQUAL_ON_FAIL(BakedObject.BakedSkeleton.IsEmpty(), false, return true); + + // For now, check we have the correct number of bones. Can add more complicated checks in the future if needed, such as checking + // parents, etc. Probably should not check the bone order though. + HOUDINI_TEST_EQUAL(FPaths::GetPath(*BakedObject.BakedSkeleton), BakeFolder); + + FString SkeletonName = FPaths::GetBaseFilename(*BakedObject.BakedSkeleton); + HOUDINI_TEST_EQUAL(SkeletonName.StartsWith(TEXT("TestSkeletalMeshOutputName")), true); + + USkeleton * Skeleton = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedSkeleton)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Skeleton, return false); + auto & ReferenceSkeleton = Skeleton->GetReferenceSkeleton(); + HOUDINI_TEST_EQUAL_ON_FAIL(ReferenceSkeleton.GetRawBoneNum(), 53, return true); + + // Check the skeletal mesh + HOUDINI_TEST_EQUAL(FPaths::GetPath(*BakedObject.BakedObject), BakeFolder); + FString SkeletalMeshName = FPaths::GetBaseFilename(*BakedObject.BakedSkeleton); + HOUDINI_TEST_EQUAL(SkeletalMeshName.StartsWith(TEXT("TestSkeletalMeshOutputName")), true); + + USkeletalMesh * SkeletalMesh = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedObject)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(SkeletalMesh, return false); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMesh->GetSkeleton(), Skeleton, return true); + auto & Materials = SkeletalMesh->GetMaterials(); + HOUDINI_TEST_EQUAL_ON_FAIL(Materials.Num(), 1, return true); + HOUDINI_TEST_EQUAL(FPaths::GetPath(Materials[0].MaterialInterface->GetPackage()->GetPathName()), BakeFolder); + + // Check the skeletal mesh component + USkeletalMeshComponent* SkeletalMeshComponent = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedComponent)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(SkeletalMeshComponent, return false); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMeshComponent->GetSkeletalMeshAsset(), SkeletalMesh, return true); + FString ActorName = SkeletalMeshComponent->GetOwner()->GetActorLabel(); + HOUDINI_TEST_EQUAL(ActorName.StartsWith(TEXT("TestSkeletonBakeActor")), true); + + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestSkeletalMeshElectraDefaultPhysicsAsset, "Houdini.UnitTests.SkeletalMesh.ElectraDefaultPhysicsAsset", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestSkeletalMeshElectraDefaultPhysicsAsset::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking of skeletal meshes. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestSkeletalMeshUtils::SkeletalMeshHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Cook and Bake Electra. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "unreal_skeleton", TEXT(""), 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "create_default_physics_asset", true, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + TArray SkeletalMeshComponents = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + TArray Skeleton = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMeshComponents.Num(), 1, return true); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + FString BakeFolder = Context->GetBakeFolderOrDefault(); + + // There should be one baked output object. + TArray& BakedOutputs = Context->GetBakedOutputs(); + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + auto& BakedOutput = BakedOutputs[0]; + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutput.BakedOutputObjects.Num(), 1, return true); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().ElementIt->Value.Value; +#else + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().Value(); +#endif + HOUDINI_TEST_EQUAL_ON_FAIL(BakedObject.BakedSkeleton.IsEmpty(), false, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(BakedObject.BakedPhysicsAsset.IsEmpty(), false, return true); + + // For now, check we have the correct number of bones. Can add more complicated checks in the future if needed, such as checking + // parents, etc. Probably should not check the bone order though. + HOUDINI_TEST_EQUAL(FPaths::GetPath(*BakedObject.BakedSkeleton), BakeFolder); + + FString SkeletonName = FPaths::GetBaseFilename(*BakedObject.BakedSkeleton); + HOUDINI_TEST_EQUAL(SkeletonName.StartsWith(TEXT("TestSkeletalMeshOutputName")), true); + + USkeleton* Skeleton = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedSkeleton)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Skeleton, return false); + auto& ReferenceSkeleton = Skeleton->GetReferenceSkeleton(); + HOUDINI_TEST_EQUAL_ON_FAIL(ReferenceSkeleton.GetRawBoneNum(), 53, return true); + + // Check the skeletal mesh + HOUDINI_TEST_EQUAL(FPaths::GetPath(*BakedObject.BakedObject), BakeFolder); + FString SkeletalMeshName = FPaths::GetBaseFilename(*BakedObject.BakedSkeleton); + HOUDINI_TEST_EQUAL(SkeletalMeshName.StartsWith(TEXT("TestSkeletalMeshOutputName")), true); + + USkeletalMesh* SkeletalMesh = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedObject)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(SkeletalMesh, return false); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMesh->GetSkeleton(), Skeleton, return true); + auto& Materials = SkeletalMesh->GetMaterials(); + HOUDINI_TEST_EQUAL_ON_FAIL(Materials.Num(), 1, return true); + HOUDINI_TEST_EQUAL(FPaths::GetPath(Materials[0].MaterialInterface->GetPackage()->GetPathName()), BakeFolder); + + // Check the skeletal mesh component + USkeletalMeshComponent* SkeletalMeshComponent = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedComponent)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(SkeletalMeshComponent, return false); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMeshComponent->GetSkeletalMeshAsset(), SkeletalMesh, return true); + FString ActorName = SkeletalMeshComponent->GetOwner()->GetActorLabel(); + HOUDINI_TEST_EQUAL(ActorName.StartsWith(TEXT("TestSkeletonBakeActor")), true); + + // Check the Physics Asset has some body setups. + UPhysicsAsset * PhysicsAsset = SkeletalMesh->GetPhysicsAsset(); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PhysicsAsset, return false); + HOUDINI_TEST_NOT_EQUAL(PhysicsAsset->SkeletalBodySetups.Num(), 0); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestSkeletalMeshElectraCustomPhysicsAsset, "Houdini.UnitTests.SkeletalMesh.ElectraCustomPhysicsAsset", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestSkeletalMeshElectraCustomPhysicsAsset::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking of skeletal meshes. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestSkeletalMeshUtils::SkeletalMeshHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Cook and Bake Electra. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "unreal_skeleton", TEXT(""), 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "enable_custom_physics_asset", true, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + TArray SkeletalMeshComponents = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + TArray Skeleton = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMeshComponents.Num(), 1, return true); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + FString BakeFolder = Context->GetBakeFolderOrDefault(); + + // There should be one baked output object. + TArray& BakedOutputs = Context->GetBakedOutputs(); + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + auto& BakedOutput = BakedOutputs[0]; + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutput.BakedOutputObjects.Num(), 1, return true); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().ElementIt->Value.Value; +#else + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().Value(); +#endif + + HOUDINI_TEST_EQUAL_ON_FAIL(BakedObject.BakedSkeleton.IsEmpty(), false, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(BakedObject.BakedPhysicsAsset.IsEmpty(), false, return true); + + // For now, check we have the correct number of bones. Can add more complicated checks in the future if needed, such as checking + // parents, etc. Probably should not check the bone order though. + HOUDINI_TEST_EQUAL(FPaths::GetPath(*BakedObject.BakedSkeleton), BakeFolder); + + FString SkeletonName = FPaths::GetBaseFilename(*BakedObject.BakedSkeleton); + HOUDINI_TEST_EQUAL(SkeletonName.StartsWith(TEXT("TestSkeletalMeshOutputName")), true); + + USkeleton* Skeleton = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedSkeleton)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Skeleton, return false); + auto& ReferenceSkeleton = Skeleton->GetReferenceSkeleton(); + HOUDINI_TEST_EQUAL_ON_FAIL(ReferenceSkeleton.GetRawBoneNum(), 53, return true); + + // Check the skeletal mesh + HOUDINI_TEST_EQUAL(FPaths::GetPath(*BakedObject.BakedObject), BakeFolder); + FString SkeletalMeshName = FPaths::GetBaseFilename(*BakedObject.BakedSkeleton); + HOUDINI_TEST_EQUAL(SkeletalMeshName.StartsWith(TEXT("TestSkeletalMeshOutputName")), true); + + USkeletalMesh* SkeletalMesh = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedObject)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(SkeletalMesh, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMesh->GetSkeleton(), Skeleton, return true); + auto& Materials = SkeletalMesh->GetMaterials(); + HOUDINI_TEST_EQUAL_ON_FAIL(Materials.Num(), 1, return true); + HOUDINI_TEST_EQUAL(FPaths::GetPath(Materials[0].MaterialInterface->GetPackage()->GetPathName()), BakeFolder); + + // Check the skeletal mesh component + USkeletalMeshComponent* SkeletalMeshComponent = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedComponent)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(SkeletalMeshComponent, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMeshComponent->GetSkeletalMeshAsset(), SkeletalMesh, return true); + FString ActorName = SkeletalMeshComponent->GetOwner()->GetActorLabel(); + HOUDINI_TEST_EQUAL(ActorName.StartsWith(TEXT("TestSkeletonBakeActor")), true); + + // Check the Physics Asset has some body setups. + UPhysicsAsset* PhysicsAsset = SkeletalMesh->GetPhysicsAsset(); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PhysicsAsset, return true); + + HOUDINI_TEST_EQUAL(PhysicsAsset->SkeletalBodySetups.Num(), 6); + + int32 BodyIndex = PhysicsAsset->FindBodyIndex(TEXT("head")); + HOUDINI_TEST_NOT_EQUAL_ON_FAIL(BodyIndex, (int32)INDEX_NONE, return true); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + UBodySetup* BodySetup = Cast(PhysicsAsset->SkeletalBodySetups[BodyIndex].Get()); +#else + UBodySetup * BodySetup = PhysicsAsset->SkeletalBodySetups[BodyIndex]; +#endif + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.BoxElems.Num(), 0); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.SphereElems.Num(), 1); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.SphylElems.Num(), 0); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.ConvexElems.Num(), 0); + + BodyIndex = PhysicsAsset->FindBodyIndex(TEXT("calf_l")); + HOUDINI_TEST_NOT_EQUAL_ON_FAIL(BodyIndex, (int32)INDEX_NONE, return true); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + BodySetup = Cast(PhysicsAsset->SkeletalBodySetups[BodyIndex].Get()); +#else + BodySetup = PhysicsAsset->SkeletalBodySetups[BodyIndex]; +#endif + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.BoxElems.Num(), 1); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.SphereElems.Num(), 0); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.SphylElems.Num(), 0); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.ConvexElems.Num(), 0); + + BodyIndex = PhysicsAsset->FindBodyIndex(TEXT("calf_r")); + HOUDINI_TEST_NOT_EQUAL_ON_FAIL(BodyIndex, (int32)INDEX_NONE, return true); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + BodySetup = Cast(PhysicsAsset->SkeletalBodySetups[BodyIndex].Get()); +#else + BodySetup = PhysicsAsset->SkeletalBodySetups[BodyIndex]; +#endif + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.BoxElems.Num(), 1); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.SphereElems.Num(), 0); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.SphylElems.Num(), 0); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.ConvexElems.Num(), 0); + + BodyIndex = PhysicsAsset->FindBodyIndex(TEXT("thigh_l")); + HOUDINI_TEST_NOT_EQUAL_ON_FAIL(BodyIndex, (int32)INDEX_NONE, return true); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + BodySetup = Cast(PhysicsAsset->SkeletalBodySetups[BodyIndex].Get()); +#else + BodySetup = PhysicsAsset->SkeletalBodySetups[BodyIndex]; +#endif + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.BoxElems.Num(), 0); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.SphereElems.Num(), 0); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.SphylElems.Num(), 1); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.ConvexElems.Num(), 0); + + BodyIndex = PhysicsAsset->FindBodyIndex(TEXT("thigh_r")); + HOUDINI_TEST_NOT_EQUAL_ON_FAIL(BodyIndex, (int32)INDEX_NONE, return true); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + BodySetup = Cast(PhysicsAsset->SkeletalBodySetups[BodyIndex].Get()); +#else + BodySetup = PhysicsAsset->SkeletalBodySetups[BodyIndex]; +#endif + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.BoxElems.Num(), 0); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.SphereElems.Num(), 0); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.SphylElems.Num(), 1); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.ConvexElems.Num(), 0); + + BodyIndex = PhysicsAsset->FindBodyIndex(TEXT("spine_03")); + HOUDINI_TEST_NOT_EQUAL_ON_FAIL(BodyIndex, (int32)INDEX_NONE, return true); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + BodySetup = Cast(PhysicsAsset->SkeletalBodySetups[BodyIndex].Get()); +#else + BodySetup = PhysicsAsset->SkeletalBodySetups[BodyIndex]; +#endif + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.BoxElems.Num(), 0); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.SphereElems.Num(), 0); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.SphylElems.Num(), 0); + HOUDINI_TEST_EQUAL(BodySetup->AggGeom.ConvexElems.Num(), 1); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestSkeletalMeshElectraExistingPhysicsAsset, "Houdini.UnitTests.SkeletalMesh.ElectraExistingPhysicsAsset", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestSkeletalMeshElectraExistingPhysicsAsset::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking of skeletal meshes. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestSkeletalMeshUtils::SkeletalMeshHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Cook and Bake Electra. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "unreal_skeleton", TEXT(""), 0); + SET_HDA_PARAMETER(Context, UHoudiniParameterToggle, "use_test_physics_asset", true, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + TArray SkeletalMeshComponents = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + TArray Skeleton = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMeshComponents.Num(), 1, return true); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + FString BakeFolder = Context->GetBakeFolderOrDefault(); + + // There should be one baked output object. + TArray& BakedOutputs = Context->GetBakedOutputs(); + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + auto& BakedOutput = BakedOutputs[0]; + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutput.BakedOutputObjects.Num(), 1, return true); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().ElementIt->Value.Value; +#else + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().Value(); +#endif + + HOUDINI_TEST_EQUAL_ON_FAIL(BakedObject.BakedSkeleton.IsEmpty(), false, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(BakedObject.BakedPhysicsAsset.IsEmpty(), true, return true); + + // For now, check we have the correct number of bones. Can add more complicated checks in the future if needed, such as checking + // parents, etc. Probably should not check the bone order though. + HOUDINI_TEST_EQUAL(FPaths::GetPath(*BakedObject.BakedSkeleton), BakeFolder); + + FString SkeletonName = FPaths::GetBaseFilename(*BakedObject.BakedSkeleton); + HOUDINI_TEST_EQUAL(SkeletonName.StartsWith(TEXT("TestSkeletalMeshOutputName")), true); + + USkeleton* Skeleton = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedSkeleton)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Skeleton, return false); + auto& ReferenceSkeleton = Skeleton->GetReferenceSkeleton(); + HOUDINI_TEST_EQUAL_ON_FAIL(ReferenceSkeleton.GetRawBoneNum(), 53, return true); + + // Check the skeletal mesh + HOUDINI_TEST_EQUAL(FPaths::GetPath(*BakedObject.BakedObject), BakeFolder); + FString SkeletalMeshName = FPaths::GetBaseFilename(*BakedObject.BakedSkeleton); + HOUDINI_TEST_EQUAL(SkeletalMeshName.StartsWith(TEXT("TestSkeletalMeshOutputName")), true); + + USkeletalMesh* SkeletalMesh = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedObject)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(SkeletalMesh, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMesh->GetSkeleton(), Skeleton, return true); + auto& Materials = SkeletalMesh->GetMaterials(); + HOUDINI_TEST_EQUAL_ON_FAIL(Materials.Num(), 1, return true); + HOUDINI_TEST_EQUAL(FPaths::GetPath(Materials[0].MaterialInterface->GetPackage()->GetPathName()), BakeFolder); + + // Check the skeletal mesh component + USkeletalMeshComponent* SkeletalMeshComponent = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedComponent)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(SkeletalMeshComponent, return true); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMeshComponent->GetSkeletalMeshAsset(), SkeletalMesh, return true); + FString ActorName = SkeletalMeshComponent->GetOwner()->GetActorLabel(); + HOUDINI_TEST_EQUAL(ActorName.StartsWith(TEXT("TestSkeletonBakeActor")), true); + + // Check the Physics Asset has some body setups. + UPhysicsAsset* PhysicsAsset = SkeletalMesh->GetPhysicsAsset(); + HOUDINI_TEST_NOT_NULL_ON_FAIL(PhysicsAsset, return true); + + FString PhysicsPathName = PhysicsAsset->GetPathName(); + HOUDINI_TEST_EQUAL(PhysicsPathName, TEXT("/Game/TestObjects/SkeletalMeshes/Test_Ref_Physics_Asset.Test_Ref_Physics_Asset")); + + return true; + })); + + return true; +} +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestSkeletalMeshElectraExistingSkeleton, "Houdini.UnitTests.SkeletalMesh.ElectraExistingSkeleton", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestSkeletalMeshElectraExistingSkeleton::RunTest(const FString& Parameters) +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// Test baking of skeletal meshes. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestSkeletalMeshUtils::SkeletalMeshHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Cook and Bake Electra, but use an existing Unreal Skeleton + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + FString SkeletonAssetName = TEXT("/Script/Engine.Skeleton'/Game/TestObjects/SkeletalMeshes/Test_Ref_Skeleton.Test_Ref_Skeleton'"); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, SkeletonAssetName]() + { + SET_HDA_PARAMETER(Context, UHoudiniParameterString, "unreal_skeleton", SkeletonAssetName, 0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + TArray SkeletalMeshComponents = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + TArray Skeleton = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMeshComponents.Num(), 1, return true); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, SkeletonAssetName]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + FString BakeFolder = Context->GetBakeFolderOrDefault(); + + // There should be one baked output object. + TArray& BakedOutputs = Context->GetBakedOutputs(); + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutputs.Num(), 1, return true); + auto& BakedOutput = BakedOutputs[0]; + HOUDINI_TEST_EQUAL_ON_FAIL(BakedOutput.BakedOutputObjects.Num(), 1, return true); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().ElementIt->Value.Value; +#else + auto& BakedObject = BakedOutput.BakedOutputObjects.begin().Value(); +#endif + + HOUDINI_TEST_EQUAL_ON_FAIL(BakedObject.BakedSkeleton.IsEmpty(), true, return true); + + // Check the skeletal mesh + HOUDINI_TEST_EQUAL(FPaths::GetPath(*BakedObject.BakedObject), BakeFolder); + + USkeletalMesh* SkeletalMesh = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedObject)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(SkeletalMesh, return true); + auto& Materials = SkeletalMesh->GetMaterials(); + HOUDINI_TEST_EQUAL_ON_FAIL(Materials.Num(), 1, return true); + HOUDINI_TEST_EQUAL(FPaths::GetPath(Materials[0].MaterialInterface->GetPackage()->GetPathName()), BakeFolder); + + // Check skeleton + USkeleton * Skeleton = SkeletalMesh->GetSkeleton(); + HOUDINI_TEST_NOT_NULL_ON_FAIL(Skeleton, return true); + HOUDINI_TEST_EQUAL(Skeleton->GetName(), TEXT("Test_Ref_Skeleton")); + + // Check the skeletal mesh component + USkeletalMeshComponent* SkeletalMeshComponent = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *BakedObject.BakedComponent)); + HOUDINI_TEST_NOT_NULL_ON_FAIL(SkeletalMeshComponent, return false); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMeshComponent->GetSkeletalMeshAsset(), SkeletalMesh, return true); + FString ActorName = SkeletalMeshComponent->GetOwner()->GetActorLabel(); + HOUDINI_TEST_EQUAL(ActorName.StartsWith(TEXT("TestSkeletonBakeActor")), true); + + return true; + })); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(FHoudiniEditorTestSkeletalMeshRoundtrip, "Houdini.UnitTests.SkeletalMesh.Roundtrip", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) + +bool FHoudiniEditorTestSkeletalMeshRoundtrip::RunTest(const FString& Parameters) +{ + /// Make sure we have a Houdini Session before doing anything. + FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries(this, FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName, {}, {}); + + // Now create the test context. + TSharedPtr Context(new FHoudiniTestContext(this, FHoudiniEditorTestSkeletalMeshUtils::RoundtripHDA, FTransform::Identity, false)); + HOUDINI_TEST_EQUAL_ON_FAIL(Context->IsValid(), true, return false); + + Context->SetProxyMeshEnabled(false); + + USkeletalMesh* OrigSkeletalMesh = LoadObject(Context->World, TEXT("/Script/Engine.SkeletalMesh'/Game/TestObjects/SkeletalMeshes/Test_Roundtrip_SKM.Test_Roundtrip_SKM'")); + + FActorSpawnParameters SpawnParams; + ASkeletalMeshActor * OrigSkeletalMeshActor = Context->World->SpawnActor(ASkeletalMeshActor::StaticClass(), SpawnParams); + OrigSkeletalMeshActor->GetSkeletalMeshComponent()->SetSkeletalMesh(OrigSkeletalMesh); + + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Cook and Bake Electra. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, OrigSkeletalMeshActor]() + { + UHoudiniInput* CurrentInput = Context->GetInputAt(0); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, OrigSkeletalMeshActor]() + { + TArray Actors; + Actors.Add(OrigSkeletalMeshActor); + UHoudiniInput* CurrentInput = Context->GetInputAt(0); + bool bChanged = true; + CurrentInput->SetInputType(EHoudiniInputType::World, bChanged); + + CurrentInput->UpdateWorldSelection(Actors); + Context->StartCookingHDA(); + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context, OrigSkeletalMesh]() + { + TArray Outputs; + Context->GetOutputs(Outputs); + + // We should have two outputs, two meshes + HOUDINI_TEST_EQUAL_ON_FAIL(Outputs.Num(), 1, return true); + TArray SkeletalMeshComponents = FHoudiniEditorUnitTestUtils::GetOutputsWithComponent(Outputs); + TArray Skeleton = FHoudiniEditorUnitTestUtils::GetOutputsWithObject(Outputs); + HOUDINI_TEST_EQUAL_ON_FAIL(SkeletalMeshComponents.Num(), 1, return true); + + USkeletalMesh * CookedSkeletalMesh = SkeletalMeshComponents[0]->GetSkeletalMeshAsset(); + + TArray CookedBoneNames = CookedSkeletalMesh->GetRefSkeleton().GetRawRefBoneNames(); + TArray OrigBoneNames = OrigSkeletalMesh->GetRefSkeleton().GetRawRefBoneNames(); + + TArray CookedRefPose = CookedSkeletalMesh->GetRefSkeleton().GetRefBonePose(); + TArray OrigRefPose = OrigSkeletalMesh->GetRefSkeleton().GetRefBonePose(); + + HOUDINI_TEST_EQUAL_ON_FAIL(CookedBoneNames.Num(), CookedRefPose.Num(), return true); + HOUDINI_TEST_EQUAL_ON_FAIL(OrigBoneNames.Num(), OrigRefPose.Num(), return true); + HOUDINI_TEST_EQUAL_ON_FAIL(CookedBoneNames.Num(), OrigBoneNames.Num(), return true); + + TMap OrigBoneMap; + TMap CoookedBoneMap; + + for(int Index = 0; Index < OrigBoneNames.Num(); Index++) + { + OrigBoneMap.Add(OrigBoneNames[Index].GetPlainNameString(), OrigRefPose[Index]); + CoookedBoneMap.Add(CookedBoneNames[Index].GetPlainNameString(), CookedRefPose[Index]); + } + + for(const FName & BoneName : OrigBoneNames) + { + FString Bone = BoneName.ToString(); + FTransform * OrigTransform = OrigBoneMap.Find(Bone); + FTransform * CookedTransform = CoookedBoneMap.Find(Bone); + HOUDINI_TEST_NOT_NULL_ON_FAIL(OrigTransform, return true); + HOUDINI_TEST_NOT_NULL_ON_FAIL(CookedTransform, return true); + + if (!OrigTransform->GetRotation().Equals(CookedTransform->GetRotation(), 0.01f)) + { + FString Error = FString::Printf(TEXT("Bone %s Rotation Differs: Original %s Cooked %s"), *Bone, + *OrigTransform->GetRotation().ToString(), + *CookedTransform->GetRotation().ToString()); + + AddError(Error); + } + + if (!OrigTransform->GetScale3D().Equals(CookedTransform->GetScale3D(), 0.01f)) + { + FString Error = FString::Printf(TEXT("Bone %s Scale Differs: Original %s Cooked %s"), *Bone, + *OrigTransform->GetScale3D().ToString(), + *CookedTransform->GetScale3D().ToString()); + + AddError(Error); + } + + } + return true; + })); + + AddCommand(new FHoudiniLatentTestCommand(Context, [this, Context]() + { + FHoudiniBakeSettings BakeSettings; + Context->Bake(BakeSettings); + + return true; + })); + + return true; +} + +#endif + + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestSkeletalMesh.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +class FHoudiniEditorTestSkeletalMeshUtils +{ +public: + const static inline FString SkeletalMeshHDA = TEXT("/Game/TestHDAs/SkeletalMesh/Test_SkeletalMesh"); + const static inline FString RoundtripHDA = TEXT("/Game/TestHDAs/SkeletalMesh/Test_SkeletalMeshRoundtrip"); +}; +#endif +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestUtils.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTestUtils.h" + +#include "HoudiniEditorEquivalenceUtils.h" +#include "HoudiniPublicAPI.h" +#include "HoudiniPublicAPIAssetWrapper.h" +#include "HoudiniPublicAPIBlueprintLib.h" + +#include "EngineUtils.h" +#include "HoudiniEngineCommands.h" + +#if WITH_DEV_AUTOMATION_TESTS + +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniEngineEditor.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniRuntimeSettings.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineUtils.h" + +#include "Tests/AutomationEditorCommon.h" +#include "IAssetViewport.h" +#include "Slate/SceneViewport.h" +#include "Widgets/SViewport.h" +#include "FileHelpers.h" +#include "LevelEditor.h" +#include "AssetRegistry/AssetRegistryModule.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "HAL/FileManager.h" + #include "HAL/PlatformFileManager.h" +#else + #include "Core/Public/HAL/FileManager.h" + #include "Core/Public/HAL/PlatformFileManager.h" +#endif +#include "Editor/EditorPerformanceSettings.h" +#include "Engine/Selection.h" +#include "Interfaces/IMainFrameModule.h" +#include "Misc/AutomationTest.h" +#include "Tests/AutomationCommon.h" +#include "SceneView.h" +#include "PropertyEditorModule.h" +#include "Framework/Application/SlateApplication.h" + +const FVector2D FHoudiniEditorTestUtils::GDefaultEditorSize = FVector2D(1280, 720); +const FString FHoudiniEditorTestUtils::SavedPathFolder = "/Game/TestSaved/"; +const FString FHoudiniEditorTestUtils::TempPathFolder = "/Game/TestTemp/"; +const float FHoudiniEditorTestUtils::TimeoutTime = 180; // Timeout, in seconds +const FName FHoudiniEditorTestUtils::HoudiniEngineSessionPipeName = TEXT("hapiunreal"); + +static TAutoConsoleVariable CVarHoudiniEngineSetupRegTest( + TEXT("HoudiniEngine.SetupRegTests"), + 0, + TEXT("If enabled, the regressions tests will set themselves up before being executed.\n") + TEXT("The needed levels will automatically be created in the TestSaved directory with the default HDA.\n") + TEXT("0: Disabled\n") + TEXT("1: Enabled\n") +); + +void FHoudiniAutomationTest::AddWarning(const FString& InWarning, int32 StackOffset) +{ + if (!UseSupressedWarnings) + { + FAutomationTestBase::AddWarning(InWarning, StackOffset); + } + else + { + if (!IsSupressedWarning(InWarning)) + { + ExecutionInfo.AddEvent(FAutomationEvent(EAutomationEventType::Warning, InWarning), StackOffset + 1); + } + } +} + +void FHoudiniAutomationTest::AddSupressedWarning(const FString& InWarningPattern) +{ + SupressedWarnings.Add(InWarningPattern); +} + +bool FHoudiniAutomationTest::IsSupressedWarning(const FString& InWarning) +{ + for (auto& Warning : SupressedWarnings) + { + FRegexPattern Pattern(Warning); + FRegexMatcher ErrorMatcher(Pattern, InWarning); + + if (ErrorMatcher.FindNext()) + { + return true; + } + } + + return false; +} + +void FHoudiniEditorTestUtils::InitializeTests(FHoudiniAutomationTest* Test, const TFunction& OnSuccess, const TFunction& OnFail) +{ + // Gpu Profiler runs out of space if we don't increase the buffer size from Unreal 5.5. + IConsoleVariable* ConsoleVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.GpuProfilerMaxEventBufferSizeKB")); + if(ConsoleVar) + { + ConsoleVar->Set(1024); + } + + LoadMap(Test, TEXT("/Game/TestSaved/Levels/TestLevel"), [=] + { + CreateSessionIfInvalidWithLatentRetries(Test, HoudiniEngineSessionPipeName, OnSuccess, OnFail); + }); +} + +UObject* FHoudiniEditorTestUtils::FindAssetUObject(FHoudiniAutomationTest* Test, const FName AssetUObjectPath) +{ + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + TArray AssetData; + AssetRegistryModule.Get().GetAssetsByPackageName(AssetUObjectPath, AssetData); + if (AssetData.Num() > 0) + { + return AssetData[0].GetAsset(); + } + + if (Test != nullptr) + { + Test->AddError(FString::Printf(TEXT("Could not find UObject: %s"), *AssetUObjectPath.ToString())); + } + + return nullptr; +} + +void FHoudiniEditorTestUtils::InstantiateAsset( + FHoudiniAutomationTest* Test, + const FName AssetUObjectPath, + const FPostTestHDAInstantiationCallback & OnFinishInstantiate, + const bool ErrorOnFail, + const FHoudiniActorTestSettings& Settings, + const FString DefaultCookFolder, + const FPreInstantiationCallback& OnPreInstantiation) +{ + SetUseLessCPUInTheBackground(); + + UHoudiniPublicAPI* HoudiniAPI = UHoudiniPublicAPIBlueprintLib::GetAPI(); + if (HoudiniAPI == nullptr || !IsValid(HoudiniAPI) || HoudiniAPI->IsUnreachable()) + { + UE_LOG(LogTemp, Error, TEXT("HoudiniAPI was not instantiated! Call FHoudiniEditorTestUtils::InitializeTest")); + OnFinishInstantiate(nullptr, false); + return; + } + + UHoudiniAsset * HoudiniAsset = Cast(FindAssetUObject(Test, AssetUObjectPath)); + + if (!HoudiniAsset) + { + Test->AddError(FString::Printf(TEXT("Could not find UObject: %s"), *AssetUObjectPath.ToString())); + OnFinishInstantiate(nullptr, false); + return; + } + + SetDefaultCookFolder(DefaultCookFolder); + + UHoudiniEditorTestObject* TestObject = GetTestObject(); + TestObject->ResetFields(); + + TestObject->IsInstantiating = true; + + const FTransform Location = FTransform::Identity; + UHoudiniPublicAPIAssetWrapper* Wrapper = HoudiniAPI->InstantiateAsset(HoudiniAsset, Location); + + // Make sure the default cook options are set as expected + // As changing the default behaviour of InstantiateAsset() will cause test failures + Wrapper->SetCookOnParameterOrInputChanges(true); + Wrapper->SetCookOnTransformChange(false); + + // + Wrapper->GetHoudiniCookable()->SetEnableCurveEditing(Settings.bEnableCurveEditing); + + TestObject->InAssetWrappers.Add(Wrapper); // Need to assign it to TestObject otherwise it will be garbage collected!!! + + // Set properties based off test settings. + Wrapper->GetHoudiniCookable()->SetOverrideGlobalProxyStaticMeshSettings(true); + Wrapper->GetHoudiniCookable()->SetEnableProxyStaticMeshOverride(Settings.bUseProxyMesh); + Wrapper->GetHoudiniCookable()->SetEnableProxyStaticMeshRefinementOnPreSaveWorldOverride(!Settings.bUseProxyMesh); + + // Bind delegates from the asset wrapper to UHoudiniEditorTestObject which we use to proxy to non-dynamic delegates + // like OnPreInstantiation. + Wrapper->GetOnPreInstantiationDelegate().AddUniqueDynamic(TestObject, &UHoudiniEditorTestObject::PreInstantiationCallback); + Wrapper->GetOnPostInstantiationDelegate().AddUniqueDynamic(TestObject, &UHoudiniEditorTestObject::PostInstantiationCallback); + Wrapper->GetOnPostCookDelegate().AddUniqueDynamic(TestObject, &UHoudiniEditorTestObject::PostCookCallback); + Wrapper->GetOnPostProcessingDelegate().AddUniqueDynamic(TestObject, &UHoudiniEditorTestObject::PostProcessingCallback); + + if (OnPreInstantiation) + TestObject->OnPreInstantiationDelegate.AddLambda(OnPreInstantiation); + + Wrapper->Recook(); // Make sure the callback is called! + + + const double StartTime = FPlatformTime::Seconds(); + Test->AddCommand(new FFunctionLatentCommand([=]() + { + const bool IsInstantiating = TestObject->IsInstantiating; + const bool CookSuccessfulResult = TestObject->InCookSuccess; + + const double CurrentTime = FPlatformTime::Seconds(); + + double DeltaTime = CurrentTime - StartTime; + + EHoudiniAssetState CurrentState = Wrapper->GetHoudiniCookable() ? Wrapper->GetHoudiniCookable()->GetCurrentState() : Wrapper->GetHoudiniAssetComponent()->GetAssetState(); + if (IsInstantiating == false && CurrentState == EHoudiniAssetState::None) + { + if (ErrorOnFail && CookSuccessfulResult == false) + { + Test->AddError(FString::Printf(TEXT("Cook was unsuccessful: %s"), *AssetUObjectPath.ToString())); + } + + // Sometimes hangs if things happen too fast. Hacky way to fix this! + if (CurrentTime - StartTime < 1) + { + return false; + } + + ForceRefreshViewport(); + + // Only complete once the asset has cooked the expected number of times, or if a cook fails (depending on + // ErrorOnFail). Cook success/failure is updated after each cook + if (TestObject->HasReachedExpectedCookCount() || (ErrorOnFail && !CookSuccessfulResult)) + { + SetDefaultCookFolder(TEXT("/Game/HoudiniEngine/Temp")); + TestObject->ResetFields(); + + OnFinishInstantiate(Wrapper, CookSuccessfulResult); + return true; + } + } + + if (DeltaTime > TimeoutTime) + { + FString ErrorString = FString::Printf(TEXT("Instantiation timeout after %.1f seconds. \n"), DeltaTime); + Test->AddError(ErrorString); + ErrorString = FString::Printf(TEXT("IsInstantiating: %d"), IsInstantiating ? 1 : 0); + Test->AddError(ErrorString); + ErrorString = FString::Printf(TEXT("CookSuccessfulResult: %d"), CookSuccessfulResult ? 1 : 0); + Test->AddError(ErrorString); + ErrorString = FString::Printf(TEXT("TestObject->HasReachedExpectedCookCount() : %d"), TestObject->HasReachedExpectedCookCount() ? 1 : 0); + Test->AddError(ErrorString); + + ErrorString = FString::Printf(TEXT("AssetState: %d("), static_cast(CurrentState)); + Test->AddError(ErrorString); + + OnFinishInstantiate(Wrapper, CookSuccessfulResult); + return true; + } + + return false; + } + )); +} + +void FHoudiniEditorTestUtils::SetUseLessCPUInTheBackground() +{ + // Equivalent of setting Edit > Editor Preferences > General > Performance > "Use less CPU when in background" is OFF + // This ensures that objects are rendered even in the background + UEditorPerformanceSettings* Settings = GetMutableDefault(); + Settings->bThrottleCPUWhenNotForeground = false; + Settings->bMonitorEditorPerformance = false; + Settings->PostEditChange(); +} + +TSharedPtr FHoudiniEditorTestUtils::GetMainFrameWindow() +{ + + if (FModuleManager::Get().IsModuleLoaded("MainFrame")) + { + // Check if the main frame is loaded. When using the old main frame it may not be. + IMainFrameModule& MainFrame = FModuleManager::LoadModuleChecked("MainFrame"); + return MainFrame.GetParentWindow(); + } + + return nullptr; +} + +TSharedPtr FHoudiniEditorTestUtils::GetActiveTopLevelWindow() +{ + return FSlateApplication::Get().GetActiveTopLevelWindow(); +} + +static bool ShouldShowProperty(const FPropertyAndParent& PropertyAndParent, bool bHaveTemplate) +{ + const FProperty& Property = PropertyAndParent.Property; + + if (bHaveTemplate) + { + const UClass* PropertyOwnerClass = Property.GetOwner(); + const bool bDisableEditOnTemplate = PropertyOwnerClass + && PropertyOwnerClass->IsNative() + && Property.HasAnyPropertyFlags(CPF_DisableEditOnTemplate); + if (bDisableEditOnTemplate) + { + return false; + } + } + return true; +} + +TSharedPtr FHoudiniEditorTestUtils::CreateNewDetailsWindow() +{ + if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) + { + // Check if the main frame is loaded. When using the old main frame it may not be. + FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + + USelection* SelectedActors = GEditor->GetSelectedActors(); + TArray Actors; + for (FSelectionIterator Iter(*SelectedActors); Iter; ++Iter) + { + AActor* Actor = Cast(*Iter); + if (Actor) + { + Actors.Add(Actor); + } + } + + TSharedRef Details = PropertyEditorModule.CreateFloatingDetailsView(Actors, false);// + + return Details; + } + + return nullptr; +} + +TSharedPtr FHoudiniEditorTestUtils::CreateViewportWindow() +{ + if (!FModuleManager::Get().IsModuleLoaded("LevelEditor")) + { + return nullptr; + } + + TSharedRef NewSlateWindow = SNew(SWindow) + .Title(NSLOCTEXT("ViewportEditor", "WindowTitle", "Viewport Editor")) + .ClientSize(FVector2D(400, 550)); + + // If the main frame exists parent the window to it + TSharedPtr< SWindow > ParentWindow; + if (FModuleManager::Get().IsModuleLoaded("MainFrame")) + { + IMainFrameModule& MainFrame = FModuleManager::GetModuleChecked("MainFrame"); + ParentWindow = MainFrame.GetParentWindow(); + } + + if (ParentWindow.IsValid()) + { + // Parent the window to the main frame + FSlateApplication::Get().AddWindowAsNativeChild(NewSlateWindow, ParentWindow.ToSharedRef()); + } + else + { + FSlateApplication::Get().AddWindow(NewSlateWindow); + } + + FLevelEditorModule& LevelEditor = FModuleManager::LoadModuleChecked("LevelEditor"); + TSharedPtr Viewport = LevelEditor.GetFirstActiveViewport(); + + NewSlateWindow->SetContent( + SNew(SBorder) + [ + Viewport->AsWidget() + ] + ); + + return NewSlateWindow; +} + +void FHoudiniEditorTestUtils::TakeScreenshotEditor(FHoudiniAutomationTest* Test, const FString ScreenshotName, const EEditorScreenshotType EditorScreenshotType, const FVector2D Size) +{ + // Wait one frame just in case for pending redraws + Test->AddCommand(new FDelayedFunctionLatentCommand([=]() + { + TSharedPtr ScreenshotWindow; + + bool DestroyWindowOnEnd = false; + + switch (EditorScreenshotType) + { + case EEditorScreenshotType::ENTIRE_EDITOR: + UE_LOG(LogTemp, Warning, TEXT("Entire editor screenshots don't work well with build machines. Use sparingly!")); + ScreenshotWindow = GetMainFrameWindow(); // Entire editor doesn't work well with build machines! + break; + case EEditorScreenshotType::ACTIVE_WINDOW: + ScreenshotWindow = GetActiveTopLevelWindow(); + break; + case EEditorScreenshotType::DETAILS_WINDOW: + ScreenshotWindow = CreateNewDetailsWindow(); + DestroyWindowOnEnd = true; + break; + case EEditorScreenshotType::VIEWPORT: + ScreenshotWindow = CreateViewportWindow(); + DestroyWindowOnEnd = true; + break; + default: + break; + } + + if (!ScreenshotWindow) + { + return; + } + + WindowScreenshotParameters ScreenshotParameters; + ScreenshotParameters.ScreenshotName = ScreenshotName; + ScreenshotParameters.CurrentWindow = ScreenshotWindow; + + // Creates a file in Engine\Saved\Automation\Tmp + TSharedRef WidgetToFind = ScreenshotWindow.ToSharedRef(); + + bool ScreenshotSuccessful; + + TArray OutImageData; + FIntVector OutImageSize; + + bool bRenderOffScreen = FParse::Param(FCommandLine::Get(), TEXT("RenderOffScreen")); + + if (!bRenderOffScreen) + { + // Take a screenshot like a normal person + // Note that this sizing method is slightly different than the offscreen one, so DO NOT copy the result to dev_reg + ScreenshotWindow->Resize(Size); + ScreenshotSuccessful = FSlateApplication::Get().TakeScreenshot(WidgetToFind, OutImageData, OutImageSize); + } + else + { + // Rendering offscreen results in a slightly different render pipeline. + // Resizing as usual doesn't seem to work unless we do it in this very specific way + // Mostly copied from FSlateApplication::Get().TakeScreenshot(WindowRef, OutImageData, OutImageSize) , but forces size + FWidgetPath WidgetPath; + FSlateApplication::Get().GeneratePathToWidgetChecked(WidgetToFind, WidgetPath); + + FArrangedWidget ArrangedWidget = WidgetPath.FindArrangedWidget(WidgetToFind).Get(FArrangedWidget::GetNullWidget()); + FVector2f Position = ArrangedWidget.Geometry.AbsolutePosition; + FVector2D WindowPosition = ScreenshotWindow->GetPositionInScreen(); + + FIntRect ScreenshotRect = FIntRect(0, 0, (int32)Size.X, (int32)Size.Y); + + ScreenshotRect.Min.X += (Position.X - WindowPosition.X); + ScreenshotRect.Min.Y += (Position.Y - WindowPosition.Y); + ScreenshotRect.Max.X += (Position.X - WindowPosition.X); + ScreenshotRect.Max.Y += (Position.Y - WindowPosition.Y); + + FSlateApplication::Get().GetRenderer()->RequestResize(ScreenshotWindow, Size.X, Size.Y); + ScreenshotWindow->Resize(Size); + FSlateApplication::Get().ForceRedrawWindow(ScreenshotWindow.ToSharedRef()); + + FSlateApplication::Get().GetRenderer()->PrepareToTakeScreenshot(ScreenshotRect, &OutImageData, ScreenshotWindow.Get()); + FSlateApplication::Get().ForceRedrawWindow(ScreenshotWindow.ToSharedRef()); + ScreenshotSuccessful = (ScreenshotRect.Size().X != 0 && ScreenshotRect.Size().Y != 0 && OutImageData.Num() >= ScreenshotRect.Size().X * ScreenshotRect.Size().Y); + OutImageSize.X = ScreenshotRect.Size().X; + OutImageSize.Y = ScreenshotRect.Size().Y; + } + + if (!ScreenshotSuccessful) + { + Test->AddError("Taking screenshot not successful!"); + return; + } + + FAutomationScreenshotData Data; + Data.Width = OutImageSize.X; + Data.Height = OutImageSize.Y; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + Data.ScreenShotName = ScreenshotName; +#else + Data.ScreenshotName = ScreenshotName; +#endif + + // Sometimes errors on 4.25 if not offscreen + if (bRenderOffScreen) + { + FAutomationTestFramework::Get().OnScreenshotCaptured().ExecuteIfBound(OutImageData, Data); + } + + WaitForScreenshotAndCopy(Test, ScreenshotName, [=] (FHoudiniAutomationTest* AutomationTest, FString BaseName) + { + CopyScreenshotToTestFolder(AutomationTest, BaseName); + + if (DestroyWindowOnEnd) + { + ScreenshotWindow->RequestDestroyWindow(); + } + }); + + }, 0.1f)); +} + +void FHoudiniEditorTestUtils::TakeScreenshotViewport(FHoudiniAutomationTest* Test, const FString ScreenshotName) +{ + Test->AddCommand(new FDelayedFunctionLatentCommand([=]() + { + const FString BaseName = ScreenshotName; + const FString ScreenshotPath = GetUnrealTestDirectory() + BaseName; + FScreenshotRequest::RequestScreenshot(ScreenshotPath, false, false); + ForceRefreshViewport(); + + WaitForScreenshotAndCopy(Test, BaseName, CopyScreenshotToTestFolder); + }, 0.1f)); +} + +UHoudiniAssetComponent* FHoudiniEditorTestUtils::GetAssetComponentWithName(const FString Name) +{ + TArray Out; + + for (TActorIterator It(GEditor->GetEditorWorldContext().World()); It; ++It) + { + AHoudiniAssetActor * Actor = *It; + FString ActorName = Actor->GetName(); + if (ActorName == Name || Name == Actor->GetActorLabel()) + { + Out.Add(*It); + } + } + + if (Out.Num() > 0) + { + UHoudiniAssetComponent* FoundHAC = Out[0]->GetHoudiniAssetComponent(); + if (FoundHAC == nullptr) + { + UHoudiniCookable* HC = Out[0]->GetHoudiniCookable(); + if (HC) + FoundHAC = Cast(HC->GetComponent()); + } + + return FoundHAC; + } + + return nullptr; +} + +bool FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject( + FHoudiniAutomationTest* InTest, UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, + UHoudiniEditorTestObject* const InTestObject, const bool bInAddTestErrors) +{ + bool bHasErrors = false; + if (!IsValid(InAssetWrapper)) + { + if (bInAddTestErrors) + InTest->AddError(TEXT("[FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject] Asset wrapper is invalid!")); + bHasErrors = true; + } + + if (!IsValid(InTestObject)) + { + if (bInAddTestErrors) + InTest->AddError(TEXT("[FHoudiniEditorTestUtils::CheckAssetWrapperAndTestObject] Test object is invalid!")); + bHasErrors = true; + } + + return !bHasErrors; +} + +bool FHoudiniEditorTestUtils::CheckAssetWrapperTestObjectAndGetValidHAC( + FHoudiniAutomationTest* InTest, UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, + UHoudiniEditorTestObject* const InTestObject, UHoudiniAssetComponent*& OutHAC, const bool bInAddTestErrors) +{ + if (!CheckAssetWrapperAndTestObject(InTest, InAssetWrapper, InTestObject, bInAddTestErrors)) + return false; + + UHoudiniAssetComponent* const HAC = InAssetWrapper->GetHoudiniAssetComponent(); + if (!IsValid(HAC)) + { + if (bInAddTestErrors) + InTest->AddError(TEXT("[FHoudiniEditorTestUtils::CheckAssetWrapperTestObjectAndGetValidHAC] HAC is invalid!")); + return false; + } + + OutHAC = HAC; + return true; +} + +void FHoudiniEditorTestUtils::LogSetParameterValueViaAPIFailure( + FHoudiniAutomationTest* const InTest, + UHoudiniPublicAPIAssetWrapper const * const InAssetWrapper, + const FString& InParamType, + const FName& InParamTupleName, + const FString& InValueAsString, + const int32 InIndex) +{ + FString ErrorMessage; + if (IsValid(InAssetWrapper)) + InAssetWrapper->GetLastErrorMessage(ErrorMessage); + else + ErrorMessage = TEXT("InAssetWrapper is null"); + + InTest->AddError(FString::Printf( + TEXT("API call to set %s parameter '%s' at index %d to %s failed: %s."), + *InParamType, *InParamTupleName.ToString(), InIndex, *InValueAsString, *ErrorMessage)); +} + +void FHoudiniEditorTestUtils::SetupDifferentialTest( + FHoudiniAutomationTest* Test, + const FString MapName, + const FString HDAAssetPath, + const FString ActorName, + const FHoudiniActorTestSettings& Settings, + const TFunction& OnFinishedCallback, + const FPreInstantiationCallback& OnPreInstantiation, + const FPostDiffTestHDAInstantiationCallback& OnInstantiationFinishedCallback) +{ + const FString SavedLevelPath = GetSavedLevelPath(MapName); + const FString SavedAssetsPath = GetSavedAssetsPath(MapName, ActorName); + + const FString TempLevelPath = GetTempLevelPath(MapName); + const FString TempAssetsPath = GetTempAssetsPath(MapName, ActorName); + + UObject * AssetObject = FHoudiniEditorTestUtils::FindAssetUObject(nullptr, FName(SavedLevelPath)); + bool Valid = AssetObject != nullptr; + + // If we don't have a Saved level, then create it, and end the test here. + if (!Valid) + { + // Save map, load it + FHoudiniEditorTestUtils::CreateAndLoadNewLevelWithAsset( + Test, + SavedLevelPath, + HDAAssetPath, + ActorName, + Settings, + [=](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const bool IsSuccessful) + { + Test->AddWarning("Created a new scene because it didn't exist! Please commit to dev_reg: " + SavedLevelPath + " and " + SavedAssetsPath); + if (OnFinishedCallback != nullptr) + OnFinishedCallback(false); + }, + SavedAssetsPath, + OnPreInstantiation, + OnInstantiationFinishedCallback + ); + + return; + } + + // Still need to check to see if the asset exists in the map, because I want you to be able to create multiple tests using the same saved scene + LoadMap(Test, SavedLevelPath, [=] + { + UHoudiniAssetComponent* SceneHAC = FHoudiniEditorTestUtils::GetAssetComponentWithName(ActorName); + if (SceneHAC == nullptr) + { + Test->AddWarning("Cannot find asset in saved scene. Instantiating the asset..." + SavedLevelPath); + + FHoudiniEditorTestUtils::InstantiateAsset( + Test, + FName(HDAAssetPath), + [=](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const bool IsSuccessful) + { + if (!IsSuccessful) + { + Test->AddError("Instantiation unsuccessful!"); + if (OnFinishedCallback != nullptr) + OnFinishedCallback(false); + return; + } + + InAssetWrapper->GetHoudiniAssetActor()->SetActorLabel(ActorName); + + auto ContinueTests = [=](bool bIsSuccessful) + { + if (!bIsSuccessful) + { + if (OnFinishedCallback != nullptr) + OnFinishedCallback(false); + return; + } + + if (!SaveCurrentLevel()) + { + Test->AddError("Unable to save level!"); + if (OnFinishedCallback != nullptr) + OnFinishedCallback(false); + return; + } + + if (OnFinishedCallback != nullptr) + OnFinishedCallback(true); + + Test->AddWarning("Instantiated asset into level. Please commit to dev_reg: " + SavedLevelPath + " and " + SavedAssetsPath); + }; + + if (OnInstantiationFinishedCallback != nullptr) + { + OnInstantiationFinishedCallback(InAssetWrapper, ContinueTests, false, SavedAssetsPath); + } + else + { + ContinueTests(true); + } + }, + true, + Settings, + SavedAssetsPath, + OnPreInstantiation + ); + + return; + } + + // Now actually run the differential test + RunDifferentialTest(Test, MapName, HDAAssetPath, ActorName, Settings, OnFinishedCallback, OnPreInstantiation, OnInstantiationFinishedCallback); + }); +} + + +void FHoudiniEditorTestUtils::RunOrSetupDifferentialTest( + FHoudiniAutomationTest* Test, + const FString MapName, + const FString HDAAssetPath, + const FString ActorName, + const FHoudiniActorTestSettings& Settings, + const TFunction& OnFinishedCallback, + const FPreInstantiationCallback& OnPreInstantiationCallback, + const FPostDiffTestHDAInstantiationCallback& OnPostInstantiationCallback) +{ + bool bSetupTest = CVarHoudiniEngineSetupRegTest.GetValueOnAnyThread() == 1 ? true : false; + + if (bSetupTest) + { + SetupDifferentialTest( + Test, MapName, HDAAssetPath, ActorName, Settings, + OnFinishedCallback, OnPreInstantiationCallback, OnPostInstantiationCallback); + } + else + { + RunDifferentialTest( + Test, MapName, HDAAssetPath, ActorName, Settings, + OnFinishedCallback, OnPreInstantiationCallback, OnPostInstantiationCallback); + } +} + +void FHoudiniEditorTestUtils::RunDifferentialTest( + FHoudiniAutomationTest* Test, + const FString MapName, + const FString HDAAssetPath, + const FString ActorName, + const FHoudiniActorTestSettings& Settings, + const TFunction& OnFinishedCallback, + const FPreInstantiationCallback& OnPreInstantiationCallback, + const FPostDiffTestHDAInstantiationCallback& OnPostInstantiationCallback) +{ + + // Suppress warnings - This is so that the output file is more readable + + // Warning that happens for most HDA instantiations + Test->AddSupressedWarning("loading from Memory: source asset file not found."); + + // Warning that often happens for landscapes + Test->AddSupressedWarning("RelativeLocation disagrees with its section base, attempted automated fix"); + + // We don't care about landscape data resize warning + Test->AddSupressedWarning("Landscape data had to be resized from"); + + // Warning that sometimes happens when switching maps + Test->AddSupressedWarning("Destroying .* which doesn't have a valid world pointer"); + + const FString SavedLevelPath = GetSavedLevelPath(MapName); + const FString SavedAssetsPath = GetSavedAssetsPath(MapName, ActorName); + + const FString TempLevelPath = GetTempLevelPath(MapName); + const FString TempAssetsPath = GetTempAssetsPath(MapName, ActorName); + + UObject * AssetObject = FHoudiniEditorTestUtils::FindAssetUObject(nullptr, FName(SavedLevelPath)); + + // If we don't have a Saved level, end the test here. + if (AssetObject == nullptr) + { + Test->AddError("Unable to find the asset in the saved level: " + SavedLevelPath); + if (OnFinishedCallback != nullptr) + OnFinishedCallback(false); + return; + } + + // Load the saved level that contains the cached HDA + LoadMap(Test, SavedLevelPath, [=] + { + UHoudiniAssetComponent* SceneHAC = FHoudiniEditorTestUtils::GetAssetComponentWithName(ActorName); + if (SceneHAC == nullptr) + { + Test->AddError("Unable to find the asset in the saved level: " + SavedLevelPath); + if (OnFinishedCallback != nullptr) + OnFinishedCallback(false); + return; + } + + const bool bErrorOnFail = true; + const FString DefaultCookFolder = "/Game/HoudiniEngine/Temp"; + FHoudiniEditorTestUtils::InstantiateAsset( + Test, + FName(HDAAssetPath), + [=](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const bool IsSuccessful) + { + if (!IsSuccessful) + { + Test->AddError("Instantiation unsuccessful!"); + if (OnFinishedCallback != nullptr) + OnFinishedCallback(false); + return; + } + + InAssetWrapper->GetHoudiniAssetActor()->SetActorLabel(ActorName + "_Compare"); + + auto ContinueTests = [=](const bool bIsSuccessful) + { + if (!FHoudiniEditorEquivalenceUtils::IsEquivalent(InAssetWrapper->GetHoudiniAssetComponent(), SceneHAC)) + { + Test->AddError(FString::Printf( + TEXT("HAC is not equivalent. If this change is intended, then DELETE %s and %s and MOVE %s and %s to those locations, fix redirectors in the TestCached folder from the content browser and commit to dev_reg. Do not copy/paste as it will break essential DuplicateTransient references like OutputObjects."), + *SavedLevelPath, *SavedAssetsPath, *TempLevelPath, *TempAssetsPath)); + + // CreateTestTempLevel(Test, MapName, HDAAssetPath, ActorName, OnFinishedCallback, OnPreInstantiationCallback, OnPostInstantiationCallback); + + return; + } + + if (!FHoudiniEditorEquivalenceUtils::IsEquivalent(InAssetWrapper->GetHoudiniCookable(), SceneHAC->GetCookable())) + { + Test->AddError(FString::Printf( + TEXT("Cookable is not equivalent. If this change is intended, then DELETE %s and %s and MOVE %s and %s to those locations, fix redirectors in the TestCached folder from the content browser and commit to dev_reg. Do not copy/paste as it will break essential DuplicateTransient references like OutputObjects."), + *SavedLevelPath, *SavedAssetsPath, *TempLevelPath, *TempAssetsPath)); + + // CreateTestTempLevel(Test, MapName, HDAAssetPath, ActorName, OnFinishedCallback, OnPreInstantiationCallback, OnPostInstantiationCallback); + + return; + } + + if (OnFinishedCallback != nullptr) + OnFinishedCallback(true); + }; + + if (OnPostInstantiationCallback != nullptr) + { + OnPostInstantiationCallback(InAssetWrapper, ContinueTests, false, DefaultCookFolder); + } + else + { + ContinueTests(true); + } + + }, bErrorOnFail, Settings, DefaultCookFolder, OnPreInstantiationCallback + ); + }); +} + + +void FHoudiniEditorTestUtils::CreateTestTempLevel( + FHoudiniAutomationTest* Test, + const FString MapName, + const FString HDAAssetPath, + const FString ActorName, + const FHoudiniActorTestSettings& Settings, + const TFunction& OnFinishedCallback, + const FPreInstantiationCallback& OnPreInstantiationCallback, + const FPostDiffTestHDAInstantiationCallback& OnPostInstantiationCallback) +{ + //const FString SavedLevelPath = GetSavedLevelPath(MapName); + //const FString SavedAssetsPath = GetSavedAssetsPath(MapName, ActorName); + + const FString TempLevelPath = GetTempLevelPath(MapName); + const FString TempAssetsPath = GetTempAssetsPath(MapName, ActorName); + + LoadMap(Test, TEXT("/Game/TestSaved/Levels/TestLevel"), [=] + { + // Cook the object in the "Cached" Level. + // Note: The reason why we do this is because we can't use the map "Save As" functionality, because it counts as duplicating + // and will remove essential Transient properties like OutputObjects + UObject * AssetObject = FHoudiniEditorTestUtils::FindAssetUObject(nullptr, FName(TempLevelPath)); + bool Valid = AssetObject != nullptr; + + // If the scene doesn't exists, then create a new one. + if (!Valid) + { + // Create the cached version of the map + FHoudiniEditorTestUtils::CreateAndLoadNewLevelWithAsset( + Test, + TempLevelPath, + HDAAssetPath, + ActorName, + Settings, + /*[=](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const bool IsSuccessful) + { + RunDifferentialTest(Test, MapName, HDAAssetPath, ActorName, OnFinishedCallback, OnPreInstantiation, OnInstantiationFinishedCallback); + },*/ + [=](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const bool IsSuccessful) { return; }, + TempAssetsPath, + OnPreInstantiationCallback, + OnPostInstantiationCallback + ); + } + else + { + // If the scene does exist, then load it, delete the existing object, cook the HDA, and replace the result in the Level + // and save it + LoadMap(Test, TempLevelPath, [=] + { + DeleteHACWithNameInLevelAndSave(ActorName); + + FHoudiniEditorTestUtils::InstantiateAsset( + Test, + FName(HDAAssetPath), + [=](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const bool IsSuccessful) + { + if (!IsSuccessful) + { + Test->AddError("Instantiation unsuccessful!"); + if (OnFinishedCallback != nullptr) + OnFinishedCallback(false); + return; + } + + InAssetWrapper->GetHoudiniAssetActor()->SetActorLabel(ActorName); + + auto ContinueTests = [=](bool bIsSuccessful) + { + if (!bIsSuccessful) + { + if (OnFinishedCallback != nullptr) + OnFinishedCallback(false); + return; + } + + if (!SaveCurrentLevel()) + { + Test->AddError("Unable to save level!"); + if (OnFinishedCallback != nullptr) + OnFinishedCallback(false); + return; + } + + //RunDifferentialTest(Test, MapName, HDAAssetPath, ActorName, OnFinishedCallback, OnPreInstantiation, OnInstantiationFinishedCallback); + }; + + if (OnPostInstantiationCallback != nullptr) + { + OnPostInstantiationCallback(InAssetWrapper, ContinueTests, true, TempAssetsPath); + } + else + { + ContinueTests(true); + } + + }, + true, + Settings, + TempAssetsPath, + OnPreInstantiationCallback + ); + }); + } + }); +} + + +bool FHoudiniEditorTestUtils::CreateAndLoadNewLevelWithAsset( + FHoudiniAutomationTest* Test, + const FString MapAssetPath, + const FString HDAAssetPath, + const FString ActorName, + const FHoudiniActorTestSettings& Settings, + const FPostTestHDAInstantiationCallback & OnFinishInstantiate, + FString DefaultCookFolder, + const FPreInstantiationCallback& OnPreInstantiationCallback, + const FPostDiffTestHDAInstantiationCallback & OnPostInstantiation) +{ + // Create a new default map! dont reuse the existing one! + UEditorLoadingAndSavingUtils::NewMapFromTemplate(TEXT("/Engine/Maps/Templates/Template_Default"), false); + + UWorld * World = GEditor->GetEditorWorldContext().World(); + + if (!UEditorLoadingAndSavingUtils::SaveMap(World, MapAssetPath)) + { + Test->AddError("Cannot save world!"); + OnFinishInstantiate(nullptr, false); + return false; + } + + LoadMap(Test, MapAssetPath, [=] + { + FHoudiniEditorTestUtils::InstantiateAsset(Test, FName(HDAAssetPath), + [=](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const bool bIsSuccessful) + { + if (!bIsSuccessful) + { + Test->AddError("Instantiation unsuccessful!"); + OnFinishInstantiate(nullptr, false); + return; + } + + InAssetWrapper->GetHoudiniAssetActor()->SetActorLabel(ActorName); + + auto ContinueTests = [=](const bool bIsSuccessful) + { + if (!bIsSuccessful) + { + OnFinishInstantiate(nullptr, false); + return; + } + + if (!SaveCurrentLevel()) + { + Test->AddError("Unable to save level!"); + OnFinishInstantiate(nullptr, false); + return; + } + + OnFinishInstantiate(InAssetWrapper, bIsSuccessful); + }; + + if (OnPostInstantiation != nullptr) + { + OnPostInstantiation(InAssetWrapper, ContinueTests, true, DefaultCookFolder); + } + else + { + ContinueTests(true); + } + + }, true, Settings, DefaultCookFolder, OnPreInstantiationCallback); + }); + + return true; +} + + +void FHoudiniEditorTestUtils::WaitForScreenshotAndCopy(FHoudiniAutomationTest* Test, FString BaseName, const TFunction& OnScreenshotGenerated) +{ + const FString TestDirectory = GetUnrealTestDirectory(); + const FString FileName = TestDirectory + BaseName; + + // Wait for screenshot to finish generating, and then copy to $RT/hapi/unreal/ + const double StartTime = FPlatformTime::Seconds(); + + Test->AddCommand(new FFunctionLatentCommand([=]() + { + IPlatformFile& FileManager = FPlatformFileManager::Get().GetPlatformFile(); + + if (FileManager.FileExists(*FileName)) + { + OnScreenshotGenerated(Test, BaseName); + return true; + } + else + { + const double CurrentTime = FPlatformTime::Seconds(); + if (CurrentTime - StartTime > TimeoutTime) + { + Test->AddError("WaitForScreenshotAndCopy timeout!"); + return true; + } + + ForceRefreshViewport(); + return false; + } + })); +} + +void FHoudiniEditorTestUtils::CopyScreenshotToTestFolder(FHoudiniAutomationTest* Test, FString BaseName) +{ + const FString TestDirectory = GetUnrealTestDirectory(); + const FString FileName = TestDirectory + BaseName; + FString DestFileName = GetTestDirectory(); + if (!DestFileName.IsEmpty()) + { + DestFileName += FormatScreenshotOutputName(BaseName); + } + + IPlatformFile& FileManager = FPlatformFileManager::Get().GetPlatformFile(); + + // Copy output file to our test directory, if it exists. + if (!DestFileName.IsEmpty()) + { + UE_LOG(LogTemp, Verbose, TEXT("Copied file to: %s"), *DestFileName); + FileManager.CopyFile(*DestFileName, *FileName); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("Unable to copy file!")); + } + + const double StartTime = FPlatformTime::Seconds(); + + Test->AddCommand(new FFunctionLatentCommand([=]() + { + IPlatformFile& CopyFileManager = FPlatformFileManager::Get().GetPlatformFile(); + if (CopyFileManager.FileExists(*FileName)) + { + return true; + } + else + { + const double CurrentTime = FPlatformTime::Seconds(); + if (CurrentTime - StartTime > TimeoutTime) + { + Test->AddError("CopyScreenshotToTestFolder timeout!"); + return true; + } + return false; + } + })); +} + +FString FHoudiniEditorTestUtils::GetTestDirectory() +{ + return FPlatformMisc::GetEnvironmentVariable(TEXT("TEST_OUTPUT_DIR")) + FPlatformMisc::GetDefaultPathSeparator(); +} + +FString FHoudiniEditorTestUtils::GetUnrealTestDirectory() +{ + //return FPaths::AutomationDir() + "/Incoming/"; // 4.25 + return FPaths::AutomationTransientDir(); // 4.26 +} + +FString FHoudiniEditorTestUtils::FormatScreenshotOutputName(FString BaseName) +{ + const FString Prefix = FPlatformMisc::GetEnvironmentVariable(TEXT("TEST_PREFIX")); + return FString::Printf(TEXT("%s_%s"), *Prefix, *BaseName); +} + +void FHoudiniEditorTestUtils::ForceRefreshViewport() +{ + // Force redraws viewport even if not in focus to prevent hanging + FLevelEditorModule &PropertyEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); + if (FModuleManager::Get().IsModuleLoaded("LevelEditor")) + { + PropertyEditorModule.BroadcastRedrawViewports(false); + } +} + +void FHoudiniEditorTestUtils::SetDefaultCookFolder(const FString Folder) +{ + FString FolderToUse = Folder; + if (Folder.IsEmpty()) + { + FolderToUse = TEXT("/Game/HoudiniEngine/Temp"); + } + + UHoudiniRuntimeSettings* const Settings = GetMutableDefault(); + if (IsValid(Settings)) + { + Settings->DefaultTemporaryCookFolder = FolderToUse; + } +} + +FString FHoudiniEditorTestUtils::GetSavedLevelPath(const FString MapName) +{ + return SavedPathFolder + "Levels/" + MapName; +} + +FString FHoudiniEditorTestUtils::GetSavedAssetsPath(const FString MapName, const FString ActorName) +{ + return SavedPathFolder + "Assets/" + MapName + "/" + ActorName; +} + +FString FHoudiniEditorTestUtils::GetTempLevelPath(const FString MapName) +{ + return TempPathFolder + "Levels/" + MapName; +} + +FString FHoudiniEditorTestUtils::GetTempAssetsPath(const FString MapName, const FString ActorName) +{ + return TempPathFolder + "Assets/" + MapName + "/" + ActorName; +} + +void FHoudiniEditorTestUtils::LoadMap(FHoudiniAutomationTest* Test, const FString Path, const TFunction& OnLoadedMap) +{ + const double StartTime = FPlatformTime::Seconds(); + + bool * HasLoadedMap = new bool(false); + + FDelegateHandle Handle = FEditorDelegates::OnMapOpened.AddLambda( + [=](const FString& Filename, bool bAsTemplate) + { + *HasLoadedMap = true; + }); + + // Introduce an artifical delay in attempt to work around UE5.6 garbage collection issues. + ADD_LATENT_AUTOMATION_COMMAND(FWaitLatentCommand(1.0f)); + + Test->AddCommand(new FFunctionLatentCommand([=]() + { + const double CurrentTime = FPlatformTime::Seconds(); + // Hacky way to allocate some time before and after loading map to prevent build machine from hanging! + if (*HasLoadedMap == false && CurrentTime - StartTime > 1) + { + FAutomationEditorCommonUtils::LoadMap(Path); + return false; + } + + if (*HasLoadedMap == true && CurrentTime - StartTime > 2) + { + FEditorDelegates::OnMapOpened.Remove(Handle); + + if (OnLoadedMap != nullptr) + { + OnLoadedMap(); + } + + delete HasLoadedMap; + + return true; + } + + return false; + })); +} + +void FHoudiniEditorTestUtils::DeleteHACWithNameInLevelAndSave(const FString Name) +{ + UHoudiniAssetComponent* SceneHAC = FHoudiniEditorTestUtils::GetAssetComponentWithName(Name); + if (SceneHAC != nullptr) + { + SceneHAC->GetOwner()->MarkAsGarbage(); + GEditor->ForceGarbageCollection(true); + + SaveCurrentLevel(); + } +} + + + +UHoudiniEditorTestObject* FHoudiniEditorTestUtils::GetTestObject() +{ + static UHoudiniEditorTestObject* Obj = NewObject(GetTransientPackage(), NAME_None, RF_MarkAsRootSet); + return Obj; +} + +// Copied from UEditorLoadingAndSavingUtils::SaveCurrentLevel from 4.25. For some reason, it always returns false in 4.26+, so make our own version. +bool FHoudiniEditorTestUtils::SaveCurrentLevel() +{ + //return UEditorLoadingAndSavingUtils::SaveCurrentLevel(); // Original in 4.25 + + bool bReturnCode = false; + + ULevel* Level = GWorld->GetCurrentLevel(); + if (Level && FEditorFileUtils::PromptToCheckoutLevels(false, Level)) + { + bReturnCode = FEditorFileUtils::SaveLevel(Level); + } + + return bReturnCode; +} + +void FHoudiniEditorTestUtils::RecookAndWait(FHoudiniAutomationTest* Test, UHoudiniPublicAPIAssetWrapper* InAssetWrapper, + const FPostTestHDAInstantiationCallback& OnCookFinished, const FString DefaultCookFolder) +{ + UHoudiniPublicAPI* HoudiniAPI = UHoudiniPublicAPIBlueprintLib::GetAPI(); + + SetDefaultCookFolder(DefaultCookFolder); + + UHoudiniEditorTestObject* TestObject = GetTestObject(); + TestObject->ResetFields(); + + TestObject->IsInstantiating = true; + + TestObject->InAssetWrappers.Add(InAssetWrapper); // Need to assign it to TestObject otherwise it will be garbage collected!!! + + // Bind delegates from the asset wrapper to UHoudiniEditorTestObject which we use to proxy to non-dynamic delegates + // like OnPreInstantiation. + InAssetWrapper->GetOnPostCookDelegate().AddUniqueDynamic(TestObject, &UHoudiniEditorTestObject::PostCookCallback); + InAssetWrapper->GetOnPostProcessingDelegate().AddUniqueDynamic(TestObject, &UHoudiniEditorTestObject::PostProcessingCallback); + + InAssetWrapper->Recook(); // Make sure the callback is called! + + const double StartTime = FPlatformTime::Seconds(); + + Test->AddCommand(new FFunctionLatentCommand([=]() + { + const bool CookSuccessfulResult = TestObject->InCookSuccess; + + const double CurrentTime = FPlatformTime::Seconds(); + + EHoudiniAssetState CurrentState = InAssetWrapper->GetHoudiniCookable() ? InAssetWrapper->GetHoudiniCookable()->GetCurrentState() : InAssetWrapper->GetHoudiniAssetComponent()->GetAssetState(); + if (TestObject->CookCount >= TestObject->ExpectedCookCount && CurrentState == EHoudiniAssetState::None) + { + if (CookSuccessfulResult == false) + { + Test->AddError(FString::Printf(TEXT("Cook was unsuccessful"))); + } + + // Sometimes hangs if things happen too fast. Hacky way to fix this! + if (CurrentTime - StartTime < 1) + { + return false; + } + + ForceRefreshViewport(); + + // Only complete once the asset has cooked the expected number of times, or if a cook fails (depending on + // ErrorOnFail). Cook success/failure is updated after each cook + if (TestObject->HasReachedExpectedCookCount() || !CookSuccessfulResult) + { + SetDefaultCookFolder(TEXT("/Game/HoudiniEngine/Temp")); + OnCookFinished(InAssetWrapper, CookSuccessfulResult); + + TestObject->ResetFields(); + return true; + } + } + + if (CurrentTime - StartTime > TimeoutTime) + { + Test->AddError("Instantiation timeout!"); + OnCookFinished(InAssetWrapper, CookSuccessfulResult); + return true; + } + + return false; + } + )); +} + +bool FHoudiniEditorTestUtils::CreateSessionIfInvalid(const FName& SessionPipeName) +{ + // If the session is valid then we can just return true + if (FHoudiniEngineCommands::IsSessionValid()) + return true; + + // No existing valid HE session: try to create a Houdini Engine Session + HOUDINI_LOG_MESSAGE(TEXT("[FHoudiniEditorTestUtils::CreateSessionIfInvalid] No current valid Houdini Engine session, attempting to create one...")); + + UHoudiniRuntimeSettings const* const HoudiniRuntimeSettings = GetDefault(); + const bool bSuccess = FHoudiniEngine::Get().CreateSession(HoudiniRuntimeSettings->SessionType, SessionPipeName); + if (bSuccess) + { + // We've successfully created the Houdini Engine session, + // We now need to notify all the cookables that they need to re instantiate + // themselves in the new Houdini engine session. + FHoudiniEngineUtils::MarkAllCookablesAsNeedInstantiation(); + } + + return bSuccess; +} + +bool FHoudiniEditorTestUtils::CreateSessionIfInvalidWithLatentRetries( + FAutomationTestBase* Test, const FName& SessionPipeName, const TFunction& OnSuccess, + const TFunction& OnFail, const int8 NumRetries, const double TimeBetweenRetriesSeconds) +{ + // Check if a valid session already exists, or try to create one + if (CreateSessionIfInvalid(SessionPipeName)) + { + // A valid session exists or was created + if (OnSuccess != nullptr) + { + OnSuccess(); + } + + return true; + } + + // If a session did not exist and could not be created, set up a latent command to try 2 more times + struct FSessionCheckState + { + double CountDown = 0; + int8 NumRetriesRemaining = 0; + }; + TSharedPtr SessionCheckState(new FSessionCheckState()); + SessionCheckState->CountDown = TimeBetweenRetriesSeconds; + SessionCheckState->NumRetriesRemaining = NumRetries; + + HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine session is not valid and a new session could not be created.")); + + HOUDINI_LOG_MESSAGE( + TEXT("Retrying Houdini Engine session creation in %.2f seconds (%d remaining retries)"), + SessionCheckState->CountDown, SessionCheckState->NumRetriesRemaining); + + Test->AddCommand(new FFunctionLatentCommand([SessionCheckState, TimeBetweenRetriesSeconds, Test, OnSuccess, OnFail]() + { + if (SessionCheckState->CountDown > 0) + { + SessionCheckState->CountDown -= GWorld->GetDeltaSeconds(); + + return false; + } + + SessionCheckState->NumRetriesRemaining -= 1; + if (!CreateSessionIfInvalid(TEXT("hapiunreal"))) + { + HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine session is not valid and a new session could not be created.")); + + if (SessionCheckState->NumRetriesRemaining > 0) + { + SessionCheckState->CountDown = TimeBetweenRetriesSeconds; + + HOUDINI_LOG_MESSAGE( + TEXT("Retrying Houdini Engine session creation in %.2f seconds (%d remaining retries)"), + SessionCheckState->CountDown, SessionCheckState->NumRetriesRemaining); + + return false; + } + + HOUDINI_LOG_MESSAGE(TEXT("Houdini Engine session creation retries exhausted.")); + + Test->AddError(TEXT("Failed to create the Houdini Engine session!")); + if (OnFail != nullptr) + { + OnFail(); + } + } + else if (OnSuccess != nullptr) + { + OnSuccess(); + } + + return true; + })); + + return false; +} + +#endif + +// UHoudiniEditorTestObject (Required due to dynamic delegates)s +UHoudiniEditorTestObject::UHoudiniEditorTestObject() +{ +} + +void UHoudiniEditorTestObject::PreInstantiationCallback(UHoudiniPublicAPIAssetWrapper* InAssetWrapper) +{ + InAssetWrapper->GetOnPreInstantiationDelegate().RemoveDynamic(this, &UHoudiniEditorTestObject::PreInstantiationCallback); + + if (OnPreInstantiationDelegate.IsBound()) + OnPreInstantiationDelegate.Broadcast(InAssetWrapper, this); +} + +void UHoudiniEditorTestObject::PostInstantiationCallback(UHoudiniPublicAPIAssetWrapper* AssetWrapper) +{ + this->IsInstantiating = false; + + AssetWrapper->GetOnPostInstantiationDelegate().RemoveDynamic(this, &UHoudiniEditorTestObject::PostInstantiationCallback); +} + +void UHoudiniEditorTestObject::PostCookCallback(UHoudiniPublicAPIAssetWrapper* AssetWrapper, const bool bInCookSuccess) +{ + this->CookCount++; + this->InCookSuccess = bInCookSuccess; + + // Unbind from the wrapper's post cook callback after the expected number of cooks have been done + if (HasReachedExpectedCookCount()) + AssetWrapper->GetOnPostCookDelegate().RemoveDynamic(this, &UHoudiniEditorTestObject::PostCookCallback); +} + +void UHoudiniEditorTestObject::PostProcessingCallback(UHoudiniPublicAPIAssetWrapper* InAssetWrapper) +{ + // Unbind from the wrapper's post processing callback after the expected number of cooks have been done + if (HasReachedExpectedCookCount()) + InAssetWrapper->GetOnPostProcessingDelegate().RemoveDynamic(this, &UHoudiniEditorTestObject::PostProcessingCallback); + + if (OnPostProcessingDelegate.IsBound()) + OnPostProcessingDelegate.Broadcast(InAssetWrapper, this); +} + +void UHoudiniEditorTestObject::ResetFields() +{ + this->IsInstantiating = false; + this->CookCount = 0; + this->ExpectedCookCount = 1; + this->InCookSuccess = false; + + OnPreInstantiationDelegate.Clear(); + OnPostProcessingDelegate.Clear(); +} + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTestUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#if WITH_DEV_AUTOMATION_TESTS +#include "Misc/AutomationTest.h" +#endif + +#include "HoudiniEditorTestUtils.generated.h" + +class UHoudiniEditorTestObject; +class UHoudiniAsset; +class UHoudiniAssetComponent; +class UHoudiniPublicAPI; +class UHoudiniPublicAPIAssetWrapper; +class ULevel; +class SWindow; + +#if WITH_DEV_AUTOMATION_TESTS + +/* +#define DO_TEST_SETUP 0 +#if DO_TEST_SETUP + #define RUN_DIFF_TEST FHoudiniEditorTestUtils::SetupDifferentialTest +#else + #define RUN_DIFF_TEST FHoudiniEditorTestUtils::RunDifferentialTest +#endif +*/ + + +struct FHoudiniActorTestSettings +{ + bool bUseProxyMesh = false; + bool bEnableCurveEditing = false; +}; + +// Struct to override FAutomationTestBase behaviour as we see fit +struct FHoudiniAutomationTest : FAutomationTestBase +{ + FHoudiniAutomationTest( const FString& InName, const bool bInComplexTask ) + : FAutomationTestBase(InName, bInComplexTask) + { + } + + void AddWarning(const FString& InWarning, int32 StackOffset = 0) override; + + // Similar to FAutomationTestBase::AddExpectedError, except only for warnings, and there is no + void AddSupressedWarning(const FString& InWarningPattern); + + // Overrides the default "AddExpectedError Behaviour to not require an exact number of warnings for the test to succeed + // If false, use default behaviour + bool UseSupressedWarnings = true; + + TSet SupressedWarnings; + + bool IsSupressedWarning(const FString& InWarning); + +}; + +// Similar to IMPLEMENT_SIMPLE_AUTOMATION_TEST, But use Houdini class +#define IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST( TClass, TBaseClass, PrettyName, TFlags ) \ +IMPLEMENT_SIMPLE_AUTOMATION_TEST_PRIVATE(TClass, TBaseClass, PrettyName, TFlags, __FILE__, __LINE__) \ +namespace\ +{\ +TClass TClass##AutomationTestInstance( TEXT(#TClass) );\ +} + +#define IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST( TClass, PrettyName, TFlags ) \ +IMPLEMENT_SIMPLE_CLASS_HOUDINI_AUTOMATION_TEST(TClass, FHoudiniAutomationTest, PrettyName, TFlags) + +/* + *TBD; + * FHoudiniMeshSocket: + * AActor: test number and types of attached component, label, transform etc.. + * ALandscapeProxy: test size, num comoponents/components size, bounds and extents, num of paint layers, num of edit layers + * UMaterialInterface: if unreal mat, should be the same, if Houdini generated mat, look for param/textures + * UStaticMesh: Check bounds? num lods, num sockets, num materials... + */ +class FHoudiniEditorTestUtils +{ +public: + enum EEditorScreenshotType + { + ENTIRE_EDITOR, + ACTIVE_WINDOW, // Gets the active window. Probably never use this. + DETAILS_WINDOW, + VIEWPORT + }; + + /** Function signature for HDA Pre-instantiation callbacks on test functions. */ + typedef TFunction FPreInstantiationCallback; + + /** Function signature for HDA Post-instantiation callbacks, specifically for the differential tests */ + // bSaveAssets is true if done in a temp folder, or if it is the first time creating it in the saved folder, and false otherwise. + // DefaultCookFolder represents the folder that you are cooking in. Generally, you do not want to modify TestSaved, but you do for TestTemp. + typedef TFunction ContinueTest, const bool bSaveAssets, const FString DefaultCookFolder)> FPostDiffTestHDAInstantiationCallback; + + typedef TFunction FPostTestHDAInstantiationCallback; + + static void InitializeTests(FHoudiniAutomationTest* Test, const TFunction& OnSuccess, const TFunction& OnFail=nullptr); + + static UObject* FindAssetUObject(FHoudiniAutomationTest* Test, const FName AssetUObjectPath); + + /** + * Instantiate an HDA from the uasset with path AssetUObjectPath. Calls on OnFinishInstantiate after the first + * cook is complete. + * @param Test The test calling this function. + * @param AssetUObjectPath The package path of the HDA uasset to instantiate. + * @param OnFinishInstantiate Function to call either on failure to instantiate/cook or after processing outputs. + * @param ErrorOnFail If true, add an error to the test if cooking fails. Defaults to true. + * @param DefaultCookFolder Sets the TemporaryCookFolder on the instantiated HDA's UHoudiniAssetComponent. Defaults to "". + * @param OnPreInstantiation Function to call when the asset enters the pre-instantiation phase (via the public API). Defaults to nullptr. + * @return The UHoudiniAssetComponent of the asset. + */ + static void InstantiateAsset( + FHoudiniAutomationTest* Test, + const FName AssetUObjectPath, + const FPostTestHDAInstantiationCallback & OnFinishInstantiate, + const bool ErrorOnFail = true, + const FHoudiniActorTestSettings& Settings = {}, + const FString DefaultCookFolder = "", + const FPreInstantiationCallback& OnPreInstantiation=nullptr); + + static void TakeScreenshotEditor(FHoudiniAutomationTest* Test, const FString ScreenshotName, const EEditorScreenshotType EditorScreenshotType, const FVector2D Size); + + static void TakeScreenshotViewport(FHoudiniAutomationTest* Test, const FString ScreenshotName); + + static void SetUseLessCPUInTheBackground(); + + static TSharedPtr GetMainFrameWindow(); + + static TSharedPtr GetActiveTopLevelWindow(); + + static TSharedPtr CreateNewDetailsWindow(); + + static TSharedPtr CreateViewportWindow(); + + static const FVector2D GDefaultEditorSize; + + static UHoudiniAssetComponent* GetAssetComponentWithName(const FString Name); + + /** + * Checks that InAssetWrapper and InTestObject are valid. + * @param InTest The test. + * @param InAssetWrapper The asset wrapper to check for validity. + * @param InTestObject The test object to check for validity. + * @param bInAddTestErrors If true, use InTest->AddError to add appropriate errors if InAssetWrapper and/or + * InTestObject is invalid. + * @return false if either InAssetWrapper or InTestObject is valid. + */ + static bool CheckAssetWrapperAndTestObject( + FHoudiniAutomationTest* InTest, UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, + UHoudiniEditorTestObject* const InTestObject, const bool bInAddTestErrors=true); + + /** + * Checks that InAssetWrapper, InTestObject and the HAC of InAssetWrapper are valid. Sets OutHAC to the HAC + * wrapped by InAssetWrapper if its valid. + * @param InTest The test. + * @param InAssetWrapper The asset wrapper to check for validity. + * @param InTestObject The test object to check for validity. + * @param OutHAC The HAC from asset wrapper, if it is valid. + * @param bInAddTestErrors If true, use InTest->AddError to add appropriate errors if InAssetWrapper, InTestObject + * and/or the HAC is invalid. + * @return false if InAssetWrapper, InTestObject or the HAC from InAssetWrapper is invalid. + */ + static bool CheckAssetWrapperTestObjectAndGetValidHAC( + FHoudiniAutomationTest* InTest, UHoudiniPublicAPIAssetWrapper* const InAssetWrapper, + UHoudiniEditorTestObject* const InTestObject, UHoudiniAssetComponent*& OutHAC, const bool bInAddTestErrors=true); + + /** + * Add an error if setting a paramater value via the API failed. Includes the parameter tuple name, type, value, index + * and the error message returned by the API. + * @param InTest The FHoudiniAutomationTest instance of the test where the error happened. + * @param InAssetWrapper The UHoudiniPublicAPIAssetWrapper instance. + * @param InParamType The parameter type as a string + * @param InParamTupleName The parameter tuple name + * @param InValueAsString The parameter value as a string + * @param InIndex The parameter index. + */ + static void LogSetParameterValueViaAPIFailure( + FHoudiniAutomationTest* const InTest, + UHoudiniPublicAPIAssetWrapper const * const InAssetWrapper, + const FString& InParamType, + const FName& InParamTupleName, + const FString& InValueAsString, + const int32 InIndex); + + /* + * Runs or setup the differential test + * based on the value of the HoudiniEngine.SetupRegTests Cvar + */ + static void RunOrSetupDifferentialTest( + FHoudiniAutomationTest* Test, + const FString MapName, + const FString HDAAssetPath, + const FString ActorName, + const FHoudiniActorTestSettings& Settings, + const TFunction& OnFinishedCallback, + const FPreInstantiationCallback& OnPreInstantiation = nullptr, + const FPostDiffTestHDAInstantiationCallback & OnPostInstantiation = nullptr); + + /* + * Runs the differential test: Requires the MapName, HDAAssetPath, ActorName + * Additionally, you can add a callback, which is called at the end of the test. + * Do not call this function twice. Chain it with the callback (See: HoudiniEditorRandomEquivalenceTest) + * How it works: + * - Does the map with TestSaved/MapName exist? + * - If no, create the map, instantiate the asset in the map, save and return. + * - If yes, + * - Load the map at TestSaved/Map + * - Does the actor named ActorName exist? + * - If no, instantiate the asset in the map, save, and return. + * - If yes, continue + * - Create a map in TestTemp/ and load it (This is so you have a duplicate version if you want to update the Saved version) + * - If ActorName exists, delete it + * - If ActorName doesn't exist, instantiate that + * - Load the map in TestSaved, instantiate the asset, and compare + * If the comparison is false, then an output message will appear, telling you to DELETE TestSaved/ and MOVE TestTemp/ to the previous location, fix redirectors, and commit to dev_reg + * Note: The reason why we do this is because we can't use the map "Save As" functionality, because it counts as duplicating + * Note: If you need to update any test HDAs / Saved scenes or assets, please commit it to: dev_reg/src/tests/hapi/unreal/CopyFiles + */ + static void SetupDifferentialTest( + FHoudiniAutomationTest* Test, + const FString MapName, + const FString HDAAssetPath, + const FString ActorName, + const FHoudiniActorTestSettings& Settings, + const TFunction& OnFinishedCallback, + const FPreInstantiationCallback& OnPreInstantiation=nullptr, + const FPostDiffTestHDAInstantiationCallback & OnPostInstantiation = nullptr); + + /* + * Runs the differential test: Requires the MapName, HDAAssetPath, ActorName + * Additionally, you can add a callback, which is called at the end of the test. + * Do not call this function twice. Chain it with the callback (See: HoudiniEditorRandomEquivalenceTest) + * How it works: + * - Does the map with TestSaved/MapName exist? + * - If no, fail and return + * - If yes, + * - Load the map at TestSaved/Map + * - Does the actor named ActorName exist? + * - If no, fail and return + * - If yes, continue + * - Load the map in TestSaved, instantiate the asset, and compare + * - if the comparison is true, success! + * - if not, then the call a temp version of the map that can potentially be used to update the test + * If the comparison is false, then an output message will appear, telling you to DELETE TestSaved/ and MOVE TestTemp/ to the previous location, fix redirectors, and commit to dev_reg + * Note: The reason why we do this is because we can't use the map "Save As" functionality, because it counts as duplicating + * Note: If you need to update any test HDAs / Saved scenes or assets, please commit it to: dev_reg/src/tests/hapi/unreal/CopyFiles + */ + static void RunDifferentialTest( + FHoudiniAutomationTest* Test, + const FString MapName, + const FString HDAAssetPath, + const FString ActorName, + const FHoudiniActorTestSettings& Settings, + const TFunction& OnFinishedCallback, + const FPreInstantiationCallback& OnPreInstantiationCallback = nullptr, + const FPostDiffTestHDAInstantiationCallback& OnPostInstantiationCallback = nullptr); + + /* + * Creates the map in TestTemp that can be used to update the differential test's results: + * -Create a map in TestTemp / and load it(This is so you have a duplicate version if you want to update the Saved version) + * -If ActorName exists, delete it + * -If ActorName doesn't exist, instantiate that + * Note: The reason why we do this is because we can't use the map "Save As" functionality, because it counts as duplicating + * Note: If you need to update any test HDAs / Saved scenes or assets, please commit it to: dev_reg/src/tests/hapi/unreal/CopyFiles + */ + static void CreateTestTempLevel( + FHoudiniAutomationTest* Test, + const FString MapName, + const FString HDAAssetPath, + const FString ActorName, + const FHoudiniActorTestSettings& Settings, + const TFunction& OnFinishedCallback, + const FPreInstantiationCallback& OnPreInstantiationCallback = nullptr, + const FPostDiffTestHDAInstantiationCallback& OnPostInstantiationCallback = nullptr); + + static bool CreateAndLoadNewLevelWithAsset( + FHoudiniAutomationTest* Test, + const FString MapAssetPath, + const FString HDAAssetPath, + const FString ActorName, + const FHoudiniActorTestSettings& Settings, + const FPostTestHDAInstantiationCallback & OnFinishInstantiate, + FString DefaultCookFolder = TEXT(""), + const FPreInstantiationCallback& OnPreInstantiation=nullptr, + const FPostDiffTestHDAInstantiationCallback & OnPostInstantiation = nullptr); + + static void LoadMap(FHoudiniAutomationTest* Test, const FString Path, const TFunction& OnLoadedMap); + + static void DeleteHACWithNameInLevelAndSave(const FString Name); + + static bool SaveCurrentLevel(); + + // Similar to InstantiateAsset, but for an existing object. + static void RecookAndWait(FHoudiniAutomationTest* Test, UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const FPostTestHDAInstantiationCallback& OnCookFinished, const FString DefaultCookFolder = TEXT("")); + + // Returns true if the session is valid or a valid session could be created. + static bool CreateSessionIfInvalid(const FName& SessionPipeName=NAME_None); + + // Checks for a valid session or tries to create one. If the first attempt fails, retry up to NumRetries times. + // Retrying is done with a latent command with TimeBetweenRetriesSeconds between attempts. + // + // Returns true if the first check / creation attempt of the session succeeds. Returns false if the first attempt + // fails (in which case latent retries will be set up). + // + // The OnSuccess function is called if a valid existing session is found or if one is created. OnFail is called if + // a session could not be found or created after exhausting all retry attempts. + static bool CreateSessionIfInvalidWithLatentRetries( + FAutomationTestBase* Test, const FName& SessionPipeName, const TFunction& OnSuccess, + const TFunction& OnFail, const int8 NumRetries=2, const double TimeBetweenRetriesSeconds=10.0); + + // The pipe name to use when creating the Houdini Engine session during tests + static const FName HoudiniEngineSessionPipeName; + +private: + static void WaitForScreenshotAndCopy(FHoudiniAutomationTest* Test, FString BaseName, const TFunction& OnScreenshotGenerated); + + static void CopyScreenshotToTestFolder(FHoudiniAutomationTest* Test, FString BaseName); + + static FString GetTestDirectory(); + + static FString GetUnrealTestDirectory(); + + static FString FormatScreenshotOutputName(FString BaseName); + + static void ForceRefreshViewport(); + + static void SetDefaultCookFolder(const FString Folder); + + static FString GetSavedLevelPath(const FString MapName); + static FString GetSavedAssetsPath(const FString MapName, const FString ActorName); + + static FString GetTempLevelPath(const FString MapName); + static FString GetTempAssetsPath(const FString MapName, const FString ActorName); + + static const FString SavedPathFolder; + static const FString TempPathFolder; + static const float TimeoutTime; + + + static UHoudiniEditorTestObject* GetTestObject(); + +}; + + +#endif + +// UObject is needed to add a dynamic delegate to the object. +// Note: Would it be beneficial to move everything to here? +UCLASS() +class UHoudiniEditorTestObject : public UObject +{ + GENERATED_BODY() + +public: + + DECLARE_MULTICAST_DELEGATE_TwoParams(FOnHoudiniAssetStateChange, UHoudiniPublicAPIAssetWrapper* /*, InAssetWrapper*/, UHoudiniEditorTestObject* /*, InTestObject*/); + + UHoudiniEditorTestObject(); + + /** + * Bound to UHoudiniPublicAPIAssetWrapper::OnPreInstantiationDelegate of an asset wrapper. Broadcasts + * ThisClass::OnPreInstantiationDelegate (if bound) when called, and unbinds from InAssetWrapper->OnPreInstantiationDelegate. + */ + UFUNCTION() + void PreInstantiationCallback(UHoudiniPublicAPIAssetWrapper* InAssetWrapper); + + UFUNCTION() + void PostInstantiationCallback(UHoudiniPublicAPIAssetWrapper* AssetWrapper); + + UFUNCTION() + void PostCookCallback(UHoudiniPublicAPIAssetWrapper* AssetWrapper, const bool bInCookSuccess); + + /** + * Bound to UHoudiniPublicAPIAssetWrapper::OnPostProcessingDelegate of an asset wrapper. Broadcasts + * ThisClass::OnPostProcessingDelegate (if bound) when called, and unbinds from InAssetWrapper->OnPostProcessingDelegate. + */ + UFUNCTION() + void PostProcessingCallback(UHoudiniPublicAPIAssetWrapper* InAssetWrapper); + + void ResetFields(); + + /** Returns true if the asset has cooked the expected number of times. */ + FORCEINLINE + bool HasReachedExpectedCookCount() const { return this->CookCount >= this->ExpectedCookCount; } + + UPROPERTY() + TArray> InAssetWrappers; + + FOnHoudiniAssetStateChange OnPreInstantiationDelegate; + FOnHoudiniAssetStateChange OnPostProcessingDelegate; + + bool IsInstantiating = false; + int32 CookCount = 0; + int32 ExpectedCookCount = 1; + bool InCookSuccess = false; +}; + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTests.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEditorTests.h" + +#include "HoudiniEditorEquivalenceUtils.h" +#include "HoudiniPublicAPIAssetWrapper.h" + +#include "FileHelpers.h" + +#if WITH_DEV_AUTOMATION_TESTS + +#include "HoudiniEditorTestUtils.h" +#include "HoudiniAsset.h" +#include "HoudiniAssetComponent.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "HAL/FileManager.h" +#else + #include "Core/Public/HAL/FileManager.h" +#endif +#include "Misc/AutomationTest.h" + + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorEvergreenTest, "Houdini.Editor.Screenshots.EvergreenScreenshots", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorEvergreenTest::RunTest(const FString & Parameters) +{ + // Don't do screenshot tests because they fail a lot. Leaving the code here for now. +#if 0 + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + FHoudiniEditorTestUtils::GetMainFrameWindow()->Resize(FHoudiniEditorTestUtils::GDefaultEditorSize); + + FHoudiniEditorTestUtils::InstantiateAsset(this, TEXT("/Game/TestHDAs/Evergreen"), + [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const bool IsSuccessful) + { + // Entire editor screenshots don't work well with build machines. Disable for now. + // FHoudiniEditorTestUtils::TakeScreenshotEditor(this, TEXT("EverGreen_EntireEditor.png"), FHoudiniEditorTestUtils::ENTIRE_EDITOR, FHoudiniEditorTestUtils::GDefaultEditorSize); + FHoudiniEditorTestUtils::TakeScreenshotEditor(this, TEXT("EverGreen_Details.png"), FHoudiniEditorTestUtils::DETAILS_WINDOW, FVector2D(400, 1000)); + FHoudiniEditorTestUtils::TakeScreenshotEditor(this, TEXT("EverGreen_EditorViewport.png"), FHoudiniEditorTestUtils::VIEWPORT, FVector2D(640, 360)); + }); + }); +#endif + return true; +} + +// Tests whether or not we need to update the FHoudiniEditorEquivalenceUtils::IsEquivalent function +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorDuplicateTest, "Houdini.Editor.Random.DuplicateTest", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorDuplicateTest::RunTest(const FString & Parameters) +{ + // Really force editor size + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + FHoudiniEditorTestUtils::InstantiateAsset(this, TEXT("/Game/TestHDAs/Evergreen"), + [this](UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const bool IsSuccessful) + { + FHoudiniEditorTestUtils::InstantiateAsset(this, TEXT("/Game/TestHDAs/Evergreen"), + [this, InAssetWrapper](UHoudiniPublicAPIAssetWrapper* InAssetWrapper2, const bool IsSuccessful2) + { + // TODO: Only compare cookables? + bool bIsEquivalent = true; + if (!FHoudiniEditorEquivalenceUtils::IsEquivalent(InAssetWrapper->GetHoudiniCookable(), InAssetWrapper2->GetHoudiniCookable())) + { + bIsEquivalent = false; + } + + if (!FHoudiniEditorEquivalenceUtils::IsEquivalent(InAssetWrapper->GetHoudiniAssetComponent(), InAssetWrapper2->GetHoudiniAssetComponent())) + { + bIsEquivalent = false; + } + + if (bIsEquivalent) + { + return true; + } + else + { + this->AddError("Duplication is not Equivalent! This is a sign we need to update the FHoudiniEditorEquivalenceUtils::IsEquivalent function"); + return false; + } + + }); + }); + }); + + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorEvergreenEquivalenceTest, "Houdini.Editor.Random.EvergreenEquivalence", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorEvergreenEquivalenceTest::RunTest(const FString & Parameters) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 + // Really force editor size + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + const FString MapName = TEXT("RandomTests"); + const FString ActorName = TEXT("EvergreenEquivalence"); + const FString HDAAssetPath = TEXT("/Game/TestHDAs/Evergreen"); + const FHoudiniActorTestSettings Settings = {}; + + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, MapName, HDAAssetPath, ActorName, Settings, [this](bool IsSuccessful) + { + + }); + }); +#endif + return true; +} + +IMPLEMENT_SIMPLE_HOUDINI_AUTOMATION_TEST(HoudiniEditorRandomEquivalenceTest, "Houdini.Editor.Random.RandomEquivalence", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniEditorRandomEquivalenceTest::RunTest(const FString & Parameters) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 + // Really force editor size + FHoudiniEditorTestUtils::InitializeTests(this, [this] + { + FHoudiniActorTestSettings Settings = {}; + Settings.bEnableCurveEditing = true; + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, TEXT("RandomTests"), TEXT("/Game/TestHDAs/Random/simple_curve"), TEXT("simple_curve"), Settings, [this](bool IsSuccessful) + { + FHoudiniEditorTestUtils::RunOrSetupDifferentialTest(this, TEXT("RandomTests"), TEXT("/Game/TestHDAs/Random/simple_heightfield"), TEXT("simple_heightfield"), {}, [this](bool IsSuccessful) + { + + }); + }); + }); +#endif + return true; +} + +#endif + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorTests.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +#endif + +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorUnitTestMeshUtils.cpp`: + +```cpp +#include "HoudiniEditorUnitTestMeshUtils.h" + +#include "HoudiniEditorUnitTestUtils.h" +#include "FileHelpers.h" +#include "HoudiniAsset.h" +#include "HoudiniPublicAPIAssetWrapper.h" +#include "HoudiniPublicAPIInputTypes.h" +#include "HoudiniAssetActor.h" +#include "HoudiniEditorTestProxyMesh.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniEngineEditorUtils.h" + +#include "HoudiniParameter.h" +#include "HoudiniParameterInt.h" +#include "HoudiniPDGAssetLink.h" +#include "Landscape.h" +#include "StaticMeshResources.h" +#include "AssetRegistry/AssetRegistryModule.h" +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniEditorTestUtils.h" + +#include "Misc/AutomationTest.h" +#include "HoudiniAssetActorFactory.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "HoudiniParameterToggle.h" +#include "HoudiniEngineOutputStats.h" +#include "HoudiniPDGManager.h" + +FHoudiniTestMeshData FHoudiniEditorUnitTestMeshUtils::GetExpectedMeshData() +{ + FHoudiniTestMeshData Result; + + for (int Index = 0; Index < 8; Index++) + { + FVector3f Vertex; + Vertex.X = Index & 1 ? -1.0f : +1.0f; + Vertex.Y = Index & 2 ? -1.0f : +1.0f; + Vertex.Z = Index & 4 ? -1.0f : +1.0f; + Vertex *= 800.0f; + + + Result.VertexPositions.Add(Vertex); + Result.VertexColors.Add(FColor(188, 188, 188, 255)); // Expected color is 0.5 (grey) but gamma corrected. + Result.NumTriangles = 6 * 2; + } + return Result; +} + +FHoudiniTestMeshData FHoudiniEditorUnitTestMeshUtils::ExtractMeshData(UHoudiniStaticMesh& Mesh) +{ + FHoudiniTestMeshData Result; + Result.VertexPositions = Mesh.GetVertexPositions(); + Result.VertexColors = Mesh.GetVertexInstanceColors(); + Result.NumTriangles = Mesh.GetNumTriangles(); + return Result; + +} + +FHoudiniTestMeshData FHoudiniEditorUnitTestMeshUtils::ExtractMeshData(UStaticMesh& Mesh, int LOD) +{ + FHoudiniTestMeshData Result; + + FStaticMeshLODResources& LODResource = Mesh.GetRenderData()->LODResources[LOD]; + int NumUnrealVertices = LODResource.VertexBuffers.PositionVertexBuffer.GetNumVertices(); + + Result.VertexPositions.SetNum(NumUnrealVertices); + Result.VertexColors.SetNum(NumUnrealVertices); + + for (int Index = 0; Index < NumUnrealVertices; Index++) + { + Result.VertexPositions[Index] = LODResource.VertexBuffers.PositionVertexBuffer.VertexPosition(Index); + Result.VertexColors[Index] = LODResource.VertexBuffers.ColorVertexBuffer.VertexColor(Index); + } + + Result.NumTriangles = LODResource.GetNumTriangles(); + return Result; + +} + +TArray FHoudiniEditorUnitTestMeshUtils::CheckMesh(FHoudiniTestMeshData& ExpectedMesh, FHoudiniTestMeshData& ActualData) +{ + TArray Errors; + + if (ExpectedMesh.VertexPositions.Num() != ActualData.VertexPositions.Num()) + { + FString Error = FString::Printf(TEXT("Expected %d vertices, not %d"), ExpectedMesh.VertexPositions.Num(), ActualData.VertexPositions.Num()); + + Errors.Add(Error); + return Errors; + } + + // The following comparison is O(n^2). This could be optimized greatly, but our test meshes are currently very simple. + // The order of the vertices will likely be different between UE and Houdini, so have to do a comparison. + + if (ExpectedMesh.VertexPositions.Num() > 100) + { + FString Error = FString::Printf(TEXT("TOO MANY VERTICES! Optimize this function or do something else.")); + Errors.Add(Error); + return Errors; + } + + int bFoundCount = 0;; + + // Keep track of which actual vertex was used + TArray bUsed; + bUsed.SetNum(ExpectedMesh.VertexPositions.Num()); + + + for(int ExpectedVertexIndex = 0; ExpectedVertexIndex < ExpectedMesh.VertexPositions.Num(); ExpectedVertexIndex++) + { + for (int ActualVertexIndex = 0; ActualVertexIndex < ActualData.VertexPositions.Num(); ActualVertexIndex++) + { + if (bUsed[ActualVertexIndex]) + continue; + + if (ExpectedMesh.VertexPositions[ExpectedVertexIndex] == ActualData.VertexPositions[ActualVertexIndex] && + ExpectedMesh.VertexColors[ExpectedVertexIndex] == ActualData.VertexColors[ActualVertexIndex]) + { + bFoundCount++; + bUsed[ActualVertexIndex] = true; + break; + } + } + } + + if (bFoundCount != ExpectedMesh.VertexPositions.Num()) + { + Errors.Add(FString("Could not match all vertices")); + Errors.Add(ExpectedMesh.ToString()); + Errors.Add(ActualData.ToString()); + } + + return Errors; +} + + +FString FHoudiniTestMeshData::ToString() +{ + FString Result; + + for(int Index = 0; Index < VertexPositions.Num(); Index++) + { + FString Vertex = FString::Printf(TEXT("P = %.1f %.1f %.1f, C = %d %d %d %d\n"), + VertexPositions[Index].X, VertexPositions[Index].Y, VertexPositions[Index].Z, + VertexColors[Index].R, VertexColors[Index].G, VertexColors[Index].B, VertexColors[Index].A); + + Result += Vertex; + } + return Result; +} + + +#endif +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorUnitTestMeshUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniEditorAssetStateSubsystem.h" +#include "HoudiniEditorTestUtils.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniOutput.h" +#include "HoudiniStaticMesh.h" +#include "HoudiniStaticMeshComponent.h" + +#if WITH_DEV_AUTOMATION_TESTS +#include "Misc/AutomationTest.h" +#endif + +class UHoudiniStaticMesh; + +struct FHoudiniTestMeshData +{ + // This struct stores the expected mesh data. + // TODO: This just contains vertex positions and colors, should be expanded to include UVs and triangles. + FString ToString(); + + TArray VertexPositions; + TArray VertexColors; + int NumTriangles; +}; + +struct FHoudiniEditorUnitTestMeshUtils +{ + static FHoudiniTestMeshData GetExpectedMeshData(); + + static FHoudiniTestMeshData ExtractMeshData(UHoudiniStaticMesh& Mesh); + static FHoudiniTestMeshData ExtractMeshData(UStaticMesh& Mesh, int LOD); + static TArray CheckMesh(FHoudiniTestMeshData & ExpectedMesh, FHoudiniTestMeshData& ActualData); + +}; + +#if WITH_DEV_AUTOMATION_TESTS + +#endif +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorUnitTestUtils.cpp`: + +```cpp +#include "HoudiniEditorUnitTestUtils.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniCookable.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniEngineEditorUtils.h" +#include "HoudiniParameter.h" +#include "HoudiniParameterInt.h" +#include "HoudiniPDGAssetLink.h" +#include "HoudiniPublicAPIAssetWrapper.h" +#include "HoudiniPublicAPIInputTypes.h" + +#include "Landscape.h" +#include "AssetRegistry/AssetRegistryModule.h" + +#if WITH_DEV_AUTOMATION_TESTS +#include "HoudiniAssetActorFactory.h" +#include "HoudiniEngineOutputStats.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniEditorTestUtils.h" +#include "HoudiniParameterToggle.h" +#include "HoudiniPDGManager.h" + +#include "FileHelpers.h" +#include "GenericPlatform/GenericPlatformProcess.h" +#include "Misc/AutomationTest.h" + + +UWorld* +FHoudiniEditorUnitTestUtils::CreateEmptyMap(bool bOpenWorld) +{ + FString MapName = bOpenWorld ? TEXT("/Engine/Maps/Templates/OpenWorld.umap") : TEXT("/Engine/Maps/Templates/Template_Default.umap"); + + UWorld* World = UEditorLoadingAndSavingUtils::NewMapFromTemplate(MapName, false); + return World; +} + +UHoudiniAssetComponent* FHoudiniEditorUnitTestUtils::LoadHDAIntoNewMap( + const FString& PackageName, + const FTransform& Transform, + bool bOpenWorld) +{ + UWorld * World = CreateEmptyMap(bOpenWorld); + + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + TArray AssetData; + AssetRegistryModule.Get().GetAssetsByPackageName(FName(PackageName), AssetData); + if (AssetData.IsEmpty()) + return nullptr; + UHoudiniAsset* HoudiniAsset = Cast(AssetData[0].GetAsset()); + + UActorFactory* Factory = GEditor->FindActorFactoryForActorClass(AHoudiniAssetActor::StaticClass()); + if (!Factory) + return nullptr; + + AActor* CreatedActor = Factory->CreateActor(Cast(HoudiniAsset), World->GetCurrentLevel(), Transform); + + UHoudiniAssetComponent* HAC = Cast(CreatedActor->GetComponentByClass(UHoudiniAssetComponent::StaticClass())); + return HAC; +} + + +bool +FHoudiniEditorUnitTestUtils::IsTemporary(const FString& TempFolder, const FString& ObjectPath) +{ + bool bIsTempAsset = ObjectPath.StartsWith(TempFolder); + return bIsTempAsset; +} + + + +AActor* +FHoudiniEditorUnitTestUtils::GetActorWithName(UWorld* World, FString& Name) +{ + if (!IsValid(World)) + return nullptr; + + ULevel* Level = World->GetLevel(0); + if (!IsValid(Level)) + return nullptr; + + for (AActor* Actor : Level->Actors) + { + if (IsValid(Actor) && Actor->GetName() == Name) + return Actor; + } + return nullptr; +} + + +bool +FHoudiniLatentTestCommand::Update() +{ + if(SingleContext.IsValid()) + { + bool bDone = CheckForCookingComplete(SingleContext.Get()); + if(!bDone) + { + return false; + } + } + else if (MultiContext.IsValid()) + { + for (auto Context : MultiContext->Contexts) + { + bool bDone = CheckForCookingComplete(Context.Get()); + if(!bDone) + { + return false; + } + } + } + + bool bDone = FFunctionLatentCommand::Update(); + return bDone; + +} + +bool +FHoudiniLatentTestCommand::CheckForCookingComplete(FHoudiniTestContext* Context) +{ + double DeltaTime = FPlatformTime::Seconds() - Context->TimeStarted; + if(DeltaTime > Context->MaxTime) + { + Context->Test->AddError(FString::Printf(TEXT("***************** Test timed out After %.2f seconds*************"), DeltaTime)); + return true; + } + + int CurrentFrame = GFrameCounter; + if(Context->WaitTickFrame) + { + if(CurrentFrame < Context->WaitTickFrame) + { + return false; + } + else + { + Context->WaitTickFrame = 0; + } + } + + if(Context->CookingState == EHoudiniContextState::Cooking) + { + return false; + } + + if(Context->PDGState == EHoudiniContextState::Cooking) + { + return false; + } + + return true; +} + +void +FHoudiniTestContext::StartCookingHDA() +{ + if (HC) + HC->MarkAsNeedCook(); + else + HAC->MarkAsNeedCook(); + + CookingState = EHoudiniContextState::Cooking; +} + +void +FHoudiniTestContext::WaitForTicks(int Count) +{ + WaitTickFrame = Count + GFrameCounter; +} + + +bool +FHoudiniTestContext::StartCookingSelectedTOPNetwork() +{ + UHoudiniPDGAssetLink * AssetLink = HC ? HC->GetPDGAssetLink() : HAC->GetPDGAssetLink(); + if(!AssetLink) + return false; + + UTOPNetwork* TopNetwork = AssetLink->GetSelectedTOPNetwork(); + if(!TopNetwork) + return false; + + TopNetwork->GetOnPostCookDelegate().AddLambda([this](UTOPNetwork* Link, bool bSuccess) + { + this->PDGState = EHoudiniContextState::Complete; + return; + }); + + FHoudiniPDGManager::CookOutput(TopNetwork); + + this->PDGState = EHoudiniContextState::Cooking; + + return true; +} + +TArray +FHoudiniTestContext::BakeSelectedTopNetwork() +{ + UHoudiniPDGAssetLink * PDGAssetLink = HAC->GetPDGAssetLink(); + + FHoudiniBakedObjectData BakeOutputs; + TArray BakedActors; + + FHoudiniEngineBakeUtils::BakePDGAssetLinkOutputsKeepActors( + PDGAssetLink, + PDGAssetLink->PDGBakeSelectionOption, + PDGAssetLink->PDGBakePackageReplaceMode, + PDGAssetLink->bRecenterBakedActors, + BakeOutputs, + BakedActors); + + return BakedActors; +} + +FString +FHoudiniEditorUnitTestUtils::GetAbsolutePathOfProjectFile(const FString& Object) +{ + FString Path = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()); + FString File = Path + TEXT("/") + Object; + FPaths::MakePlatformFilename(File); + return File; + +} + +UHoudiniParameter* +FHoudiniEditorUnitTestUtils::GetTypedParameter(UHoudiniAssetComponent* HAC, UClass* Class, const char* Name) +{ + FString ParamName = Name; + UHoudiniParameter * Parameter = HAC->FindParameterByName(FString(ParamName)); + if (!Parameter) + { + HOUDINI_LOG_ERROR(TEXT("Could not find parameter called %s. Dumping Parameters:"), *ParamName); + for(int Index = 0; Index < HAC->GetNumParameters(); Index++) + { + UHoudiniParameter * Param = HAC->GetParameterAt(Index); + HOUDINI_LOG_ERROR(TEXT("Parameter %d name=%s label=%s class=%s"), + Index, *Param->GetParameterName(), *Param->GetParameterLabel(), *Param->GetClass()->GetName()); + } + return nullptr; + } + + if (!Parameter->IsA(Class)) + { + HOUDINI_LOG_ERROR(TEXT("Parameter '%s' is of wrong type. IsA '%s' expected '%s'"), *ParamName, *Parameter->GetClass()->GetName(), *Class->GetName()); + return nullptr; + } + return Parameter; +} + +UHoudiniParameter* +FHoudiniEditorUnitTestUtils::GetTypedParameter(UHoudiniCookable* HC, UClass* Class, const char* Name) +{ + FString ParamName = Name; + UHoudiniParameter* Parameter = HC->FindParameterByName(FString(ParamName)); + if (!Parameter) + { + HOUDINI_LOG_ERROR(TEXT("Could not find parameter called %s. Dumping Parameters:"), *ParamName); + for (int Index = 0; Index < HC->GetNumParameters(); Index++) + { + UHoudiniParameter* Param = HC->GetParameterAt(Index); + HOUDINI_LOG_ERROR(TEXT("Parameter %d name=%s label=%s class=%s"), + Index, *Param->GetParameterName(), *Param->GetParameterLabel(), *Param->GetClass()->GetName()); + } + return nullptr; + } + + if (!Parameter->IsA(Class)) + { + HOUDINI_LOG_ERROR(TEXT("Parameter '%s' is of wrong type. IsA '%s' expected '%s'"), *ParamName, *Parameter->GetClass()->GetName(), *Class->GetName()); + return nullptr; + } + return Parameter; +} + +FHoudiniTestContext::FHoudiniTestContext(FAutomationTestBase* CurrentTest, UWorld* InWorld, const FString& ActorLabel) +{ + World = InWorld; + Test = CurrentTest; + TimeStarted = FPlatformTime::Seconds(); + + FindHACInWorld(ActorLabel); +} + +void +FHoudiniTestContext::FindHACInWorld(const FString& ActorLabel) +{ + // Find Houdini Asset Actor and then component. + UHoudiniAssetComponent* FoundHAC = nullptr; + for(TActorIterator ActorItr(World, AHoudiniAssetActor::StaticClass()); ActorItr; ++ActorItr) + { + AActor* FoundActor = *ActorItr; + if(!FoundActor) + continue; + + bool bNameMatches = true; + if(!ActorLabel.IsEmpty()) + bNameMatches = ActorLabel == FoundActor->GetActorLabel(); + + if (bNameMatches) + { + FoundHAC = FoundActor->FindComponentByClass(); + break; + } + } + + if(!FoundHAC) + return; + + SetHAC(FoundHAC); + + // Set time last so we don't include instantiation time. + TimeStarted = FPlatformTime::Seconds(); +} + +FHoudiniTestContext::FHoudiniTestContext( + FAutomationTestBase* CurrentTest, + const FString& MapName, + const FString& ActorLabel) +{ + World = UEditorLoadingAndSavingUtils::NewMapFromTemplate(MapName, false); + Test = CurrentTest; + TimeStarted = FPlatformTime::Seconds(); + + FindHACInWorld(ActorLabel); +} + +FHoudiniTestContext::FHoudiniTestContext( + FAutomationTestBase* CurrentTest, + bool bOpenWorld) +{ + World = FHoudiniEditorUnitTestUtils::CreateEmptyMap(bOpenWorld); + Test = CurrentTest; + TimeStarted = FPlatformTime::Seconds(); +} + +FHoudiniTestContext::FHoudiniTestContext( + FAutomationTestBase* CurrentTest, + const FString& HDAName, + const FTransform& Transform, + bool bOpenWorld) +{ + Test = CurrentTest; + + // Load the HDA into a new map and kick start the cook. We do an initial cook to make sure the parameters are available. + UHoudiniAssetComponent* CreatedHAC = FHoudiniEditorUnitTestUtils::LoadHDAIntoNewMap(HDAName, Transform, bOpenWorld); + if (!CreatedHAC) + { + HOUDINI_LOG_ERROR(TEXT("Failed to load HDA %s into map. Missing uasset?"), *HDAName); + return; + } + + // + SetHAC(CreatedHAC); + + World = HAC->GetHACWorld(); + + // Set time last so we don't include instantiation time. + TimeStarted = FPlatformTime::Seconds(); +} + +void +FHoudiniTestContext::SetHAC(UHoudiniAssetComponent* HACToUse) +{ + if (HACToUse && HACToUse->GetCookable()) + return SetCookable(HACToUse->GetCookable()); + + HAC = HACToUse; + OutputDelegateHandle = HAC->GetOnPostOutputProcessingDelegate().AddLambda([this](UHoudiniAssetComponent* _HAC, bool bSuccess) + { + this->CookingState = EHoudiniContextState::Complete; + }); +} + + +void +FHoudiniTestContext::SetCookable(UHoudiniCookable* HCToUse) +{ + HC = HCToUse; + HAC = Cast(HCToUse->GetComponent()); + + OutputDelegateHandle = HC->GetOnPostOutputProcessingDelegate().AddLambda([this](UHoudiniCookable* _HC, bool bSuccess) + { + this->CookingState = EHoudiniContextState::Complete; + }); +} + +UHoudiniAssetComponent* +FHoudiniTestContext::GetHAC() +{ + return HAC; +} + +UHoudiniCookable* +FHoudiniTestContext::GetCookable() +{ + return HC; +} + +void +FHoudiniTestContext::GetOutputs(TArray& OutOutputs) const +{ + if (HC) + HC->GetOutputs(OutOutputs); + else if (HAC) + HAC->GetOutputs(OutOutputs); +} + +TArray& +FHoudiniTestContext::GetBakedOutputs() +{ + if (HC) + return HC->GetBakedOutputs(); + else + return HAC->GetBakedOutputs(); +} + + +bool +FHoudiniTestContext::Bake(const FHoudiniBakeSettings& InBakeSettings) +{ + if (HC) + { + return FHoudiniEngineBakeUtils::BakeCookable( + HC, + InBakeSettings, + HC->GetHoudiniEngineBakeOption(), + HC->GetRemoveOutputAfterBake()); + } + /* + else if (HAC) + { + return FHoudiniEngineBakeUtils::BakeHoudiniAssetComponent( + HAC, + InBakeSettings, + HAC->GetHoudiniEngineBakeOption(), + HAC->GetRemoveOutputAfterBake()); + } + */ + + return false; +} + +UHoudiniInput* +FHoudiniTestContext::GetInputAt(const int Idx) +{ + if (HC) + return HC->GetInputAt(Idx); + else + return HAC->GetInputAt(Idx); +} + +void +FHoudiniTestContext::SetProxyMeshEnabled(const bool bEnabled) +{ + if (HC) + { + HC->SetOverrideGlobalProxyStaticMeshSettings(true); + HC->SetEnableProxyStaticMeshOverride(bEnabled); + } + else + { + HAC->SetOverrideGlobalProxyStaticMeshSettings(true); + HAC->SetEnableProxyStaticMeshOverride(bEnabled); + } +} + +FString +FHoudiniTestContext::GetBakeFolderOrDefault() const +{ + if (HC) + return HC->GetBakeFolderOrDefault(); + else + return HAC->GetBakeFolderOrDefault(); +} + +UWorld* +FHoudiniTestContext::GetWorld() const +{ + if (HC) + return HC->GetWorld(); + else + return HAC->GetHACWorld(); +} + +UHoudiniPDGAssetLink* +FHoudiniTestContext::GetPDGAssetLink() +{ + if (HC) + return HC->GetPDGAssetLink(); + else + return HAC->GetPDGAssetLink(); +} + +FString +FHoudiniTestContext::GetTemporaryCookFolderOrDefault() const +{ + if (HC) + return HC->GetTemporaryCookFolderOrDefault(); + else + return HAC->GetTemporaryCookFolderOrDefault(); +} + +FHoudiniTestContext::~FHoudiniTestContext() +{ + if(HC) + HC->GetOnPostOutputProcessingDelegate().Remove(OutputDelegateHandle); + else + HAC->GetOnPostOutputProcessingDelegate().Remove(OutputDelegateHandle); +} + +bool +FHoudiniTestContext::IsValid() +{ + return (HAC != nullptr || HC != nullptr); +} + +TArray +FHoudiniEditorUnitTestUtils::GetOutputActors(TArray& BakedOutputs) +{ + TArray Results; + for(auto & BakeOutput : BakedOutputs) + { + for(auto & OutputObject : BakeOutput.BakedOutputObjects) + { + if (!OutputObject.Value.Actor.IsEmpty()) + { + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Value.Actor)); + if (IsValid(Actor)) + { + Results.Add(Actor); + } + } + } + } + return Results; +} + +TArray +FHoudiniEditorUnitTestUtils::GetOutputInstancedActors(TArray& BakedOutputs) +{ + TArray Results; + for(auto& BakeOutput : BakedOutputs) + { + for(auto& OutputObject : BakeOutput.BakedOutputObjects) + { + if(!OutputObject.Value.InstancedActors.IsEmpty()) + { + AActor* Actor = Cast(StaticLoadObject(UObject::StaticClass(), nullptr, *OutputObject.Value.InstancedActors[0])); + if(IsValid(Actor)) + { + Results.Add(Actor); + } + } + } + } + return Results; +} + +#endif +``` + +`Source/HoudiniEngineEditor/Private/Tests/HoudiniEditorUnitTestUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniEditorAssetStateSubsystem.h" +#include "HoudiniEditorTestUtils.h" +#include "HoudiniEngineBakeUtils.h" +#include "HoudiniOutput.h" +#include "HoudiniStaticMesh.h" +#include "HoudiniStaticMeshComponent.h" +#include "CoreMinimal.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 +#include "GeometryCollection/GeometryCollectionActor.h" +#include "GeometryCollection/GeometryCollectionComponent.h" +#include "GeometryCollection/GeometryCollectionObject.h" +#else +#include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionActor.h" +#include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionComponent.h" +#include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionObject.h" +#endif + +#if WITH_DEV_AUTOMATION_TESTS +#include "Misc/AutomationTest.h" +#endif + +//#include "HoudiniEditorLatentUtils.generated.h" +class UHoudiniAssetComponent; + +// Test Macros. +// +// Use these macros to automatically print out useful info about the test automatically. +// The tests that end with "ON_FAIL" can be used to specify a statement to be executed on failure, +// for example a continue, break or return statement. + +#define REPORT_ERROR(_X) if (!(_X)) AddError(FString::Printf(TEXT("%s:%d"), TEXT(__FILE__), __LINE__)) + +#define HOUDINI_TEST_EQUAL(A,...) REPORT_ERROR(TestEqual(#A, A, __VA_ARGS__)) + +#define HOUDINI_TEST_EQUALISH(A,B,C) REPORT_ERROR(TestEqual(#A, A, B, C)); +#define HOUDINI_TEST_NOT_EQUAL(A,B) REPORT_ERROR(TestNotEqual(#A, A, B)) +#define HOUDINI_TEST_NULL(A) REPORT_ERROR(TestNull(#A, A)) +#define HOUDINI_TEST_NOT_NULL(A) REPORT_ERROR(TestNotNull(#A, A)) + +#define HOUDINI_TEST_EQUAL_ON_FAIL(A,B,_FAIL)\ + {\ + bool ____bSuccess = TestEqual(#A, A, B);\ + if (!____bSuccess)\ + {\ + REPORT_ERROR(true);\ + _FAIL;\ + }\ + } + +#define HOUDINI_TEST_EQUALISH_ON_FAIL(A,B, C, _FAIL)\ + {\ + bool ____bSuccess = TestEqual(#A, A, B, C);\ + if(!____bSuccess)\ + {\ + REPORT_ERROR(true);\ + _FAIL;\ + }\ + } + +#define HOUDINI_TEST_NOT_EQUAL_ON_FAIL(A,B,_FAIL)\ + {\ + bool ____bSuccess = TestNotEqual(#A, A, B);\ + if(!____bSuccess)\ + {\ + REPORT_ERROR(true);\ + _FAIL;\ + }\ + } + +#define HOUDINI_TEST_NULL_ON_FAIL(A,_FAIL)\ + {\ + bool ____bSuccess = TestNull(#A, A);\ + if(!____bSuccess)\ + {\ + REPORT_ERROR(true);\ + _FAIL;\ + }\ + } + +#define HOUDINI_TEST_NOT_NULL_ON_FAIL(A, _FAIL)\ + {\ + bool ____bSuccess = TestNotNull(#A, A);\ + if(!____bSuccess)\ + {\ + REPORT_ERROR(true);\ + _FAIL;\ + }\ + } + +// Utils functions + +struct FHoudiniEditorUnitTestUtils +{ + static UHoudiniAssetComponent* LoadHDAIntoNewMap(const FString& PackageName, const FTransform& Transform, bool bOpenWorld); + + static UWorld * CreateEmptyMap(bool bOpenWorld); + + static FString GetAbsolutePathOfProjectFile(const FString & Object); + + // Helper function to returns components from an output. + template + static TArray GetOutputsWithComponent(const TArray& Outputs) + { + TArray Results; + + for (UHoudiniOutput* Output : Outputs) + { + for (auto& OutputObject : Output->GetOutputObjects()) + { + if (OutputObject.Value.OutputComponents.Num() > 0) + { + if (OutputObject.Value.OutputComponents[0]->GetClass() == COMPONENT_TYPE::StaticClass()) + { + COMPONENT_TYPE* Out = Cast(OutputObject.Value.OutputComponents[0]); + Results.Add(Out); + } + } + } + } + return Results; + } + + // Helper function to returns proxy mesh components from an output. + static inline TArray GetOutputsWithProxyComponent(const TArray& Outputs) + { + TArray Results; + + for (UHoudiniOutput* Output : Outputs) + { + for (auto& OutputObject : Output->GetOutputObjects()) + { + if (OutputObject.Value.ProxyComponent != nullptr) + { + UObject * Ptr = OutputObject.Value.ProxyComponent; + UHoudiniStaticMeshComponent* ProxyMesh = Cast(Ptr); + Results.Add(ProxyMesh); + } + } + } + return Results; + } + + // Helper function to returns components from a baked output. + template + static TArray GetOutputsWithComponent(const TArray& Outputs) + { + TArray Results; + + for (const FHoudiniEngineBakedActor & Output : Outputs) + { + if (IsValid(Output.BakedComponent) && Output.BakedComponent->GetClass() == COMPONENT_TYPE::StaticClass()) + { + COMPONENT_TYPE* Out = Cast(Output.BakedComponent); + Results.Add(Out); + } + } + return Results; + } + + // Helper function to returns objects from an output. + template + static TArray GetOutputsWithActor(const TArray& Outputs) + { + TArray Results; + + for (UHoudiniOutput* Output : Outputs) + { + for (auto& OutputObject : Output->GetOutputObjects()) + { + if (OutputObject.Value.OutputActors.Num() > 0) + { + if (OutputObject.Value.OutputActors[0]->IsA(ACTOR_TYPE::StaticClass())) + { + FHoudiniOutputObject* Out = &OutputObject.Value; + Results.Add(Out); + } + } + } + } + return Results; + } + + // Helper function to return actors from an output. + template + static TArray GetOutputsWithObject(const TArray& Outputs) + { + TArray Results; + + for (UHoudiniOutput* Output : Outputs) + { + for (auto& OutputObject : Output->GetOutputObjects()) + { + if (IsValid(OutputObject.Value.OutputObject)) + { + if (OutputObject.Value.OutputObject->IsA()) + { + OBJECT_TYPE* Out = Cast(OutputObject.Value.OutputObject); + Results.Add(Out); + } + } + } + } + return Results; + } + + // Helper function to return foliage objects from an output. + static TArray GetOutputsWithFoliageType(const TArray& Outputs) + { + TArray Results; + + for (UHoudiniOutput* Output : Outputs) + { + for (auto& OutputObject : Output->GetOutputObjects()) + { + if (OutputObject.Value.FoliageType) + { + Results.Add(&OutputObject.Value); + } + } + } + return Results; + } + + // Finds an actor by name. + static AActor* GetActorWithName(UWorld* World, FString& Name); + + // Finds an HAC parameter of a specific class. + static UHoudiniParameter * GetTypedParameter(UHoudiniAssetComponent * HAC, UClass * Class, const char* Name); + + // Finds a Cookable parameter of a specific class. + static UHoudiniParameter* GetTypedParameter(UHoudiniCookable* HC, UClass* Class, const char* Name); + + // Finds an HAC parameter of a specific class. + template + static TYPED_PARAMETER* GetTypedParameter(UHoudiniAssetComponent* HAC, const char * Name) + { + return Cast(GetTypedParameter(HAC, TYPED_PARAMETER::StaticClass(), Name)); + } + + // Finds a Cookable parameter of a specific class. + template + static TYPED_PARAMETER* GetTypedParameter(UHoudiniCookable* HC, const char* Name) + { + return Cast(GetTypedParameter(HC, TYPED_PARAMETER::StaticClass(), Name)); + } + + // Returns only components on the exact type + template + static TArray FilterComponents(const TArray & Components) + { + TArray Results; + for(auto Component : Components) + { + if (Component->GetClass() == COMPONENT::StaticClass()) + Results.Add((COMPONENT*)Component); + } + return Results; + } + + // Returns all the output actors in all the outputs. + static TArray GetOutputActors(TArray& BakedOutputs); + + // Returns all the output instacnce actors in all the outputs. + static TArray GetOutputInstancedActors(TArray& BakedOutputs); + + // Returns only the actors that are based off a given type. + template + static TArray FilterActors(const TArray & Actors) + { + TArray Results; + for(AActor * Actor : Actors) + if (Actor->IsA()) + Results.Add(Cast(Actor)); + return Results; + } + + // Filters out those actors that have components of the given type. + template + static TArray FilterActorsWithComponent(TArray& Actors) + { + TArray Results; + for (auto Actor : Actors) + { + TArray Components; + Actor->GetComponents(Components); + for (auto Component : Components) + { + if (Component->IsA()) + { + Results.Add(Actor); + break; + } + } + } + return Results; + } + + // Checks if an object was saved to the HAC's temp folder. + static bool IsTemporary(const FString& TempFolder, const FString& ObjectPath); +}; + + +// Helper macro to set parm, ensures the parameter is valid. +#define SET_HDA_PARAMETER(_CTX, _PARAMETER_TYPE, _PARAMETER_NAME, _PARAMETER_VALUE, _PARAMETER_INDEX)\ + {\ + _PARAMETER_TYPE* __Parameter = nullptr;\ + if(_CTX->GetCookable())\ + __Parameter = FHoudiniEditorUnitTestUtils::GetTypedParameter<_PARAMETER_TYPE>(_CTX->GetCookable(), _PARAMETER_NAME);\ + else\ + __Parameter = FHoudiniEditorUnitTestUtils::GetTypedParameter<_PARAMETER_TYPE>(_CTX->GetHAC(), _PARAMETER_NAME); \ + if (!TestNotNull(#_PARAMETER_NAME, __Parameter))\ + {\ + return true;\ + }\ + __Parameter->SetValueAt(_PARAMETER_VALUE, _PARAMETER_INDEX);\ + } + +// Helper macro to set parm, ensures the parameter is valid. +#define SET_HDA_PARAMETER_NUM_ELEMENTS(_CTX, _PARAMETER_TYPE, _PARAMETER_NAME, _PARAMETER_VALUE)\ + {\ + _PARAMETER_TYPE* __Parameter = nullptr;\ + if(_CTX->GetCookable())\ + __Parameter = FHoudiniEditorUnitTestUtils::GetTypedParameter<_PARAMETER_TYPE>(_CTX->GetCookable(), _PARAMETER_NAME);\ + else\ + __Parameter = FHoudiniEditorUnitTestUtils::GetTypedParameter<_PARAMETER_TYPE>(_CTX->GetHAC(), _PARAMETER_NAME);\ + if (!TestNotNull(#_PARAMETER_NAME, __Parameter))\ + {\ + return true;\ + }\ + __Parameter->SetNumElements(_PARAMETER_VALUE);\ + } + +enum class EHoudiniContextState : uint8 +{ + Idle, + Cooking, + Complete +}; + +struct FHoudiniTestContext +{ + // Create and pass one of these structures between different latent parts of a test (ie. those added + // with AddCommand()). It keeps track of the test and stores timer info to handle timeouts (to avoid + // hanging build PCs). + // + // The "Data" map can be used to pass data between tests. + // + FHoudiniTestContext(FAutomationTestBase* CurrentTest, + const FString& HDAName, + const FTransform& Transform, + bool bOpenWorld); + + FHoudiniTestContext(FAutomationTestBase* CurrentTest, const FString& MapName, const FString& ActorLabel = TEXT("")); + + FHoudiniTestContext(FAutomationTestBase* CurrentTest, bool bOpenWorld); + + FHoudiniTestContext(FAutomationTestBase* CurrentTest, UWorld* World, const FString& ActorLabel); + + ~FHoudiniTestContext(); + + void FindHACInWorld(const FString& ActorLabel = TEXT("")); + + // Starts cooking the HDA asynchrously. + void StartCookingHDA(); + + // Starts cooking the Selected top network in the HDA asynchronously. + bool StartCookingSelectedTOPNetwork(); + + void WaitForTicks(int Count); + + void SetHAC(UHoudiniAssetComponent* HACToUse); + void SetCookable(UHoudiniCookable* HCToUse); + + UHoudiniAssetComponent* GetHAC(); + UHoudiniCookable* GetCookable(); + + // Helper function to simplify the code when dealing with HAC and Cookables + bool Bake(const FHoudiniBakeSettings& InBakeSettings); + void GetOutputs(TArray& OutOutputs) const; + TArray& GetBakedOutputs(); + UHoudiniInput* GetInputAt(const int Idx); + void SetProxyMeshEnabled(const bool bEnabled); + FString GetBakeFolderOrDefault() const; + UWorld* GetWorld() const; + UHoudiniPDGAssetLink* GetPDGAssetLink(); + FString GetTemporaryCookFolderOrDefault() const; + + // Check if the context is valid. This will be false if, for example, the HDA failed to load. + bool IsValid(); + + // Bakes the top network. Synchronous, returns the baked actors. + TArray BakeSelectedTopNetwork(); + + double MaxTime = 120.0f; // Max time (seconds) this test can run. + double TimeStarted = 0.0f; // Time this test started. Used to test for timeout. + + FAutomationTestBase* Test = nullptr; // Unit test underway + + UHoudiniAssetComponent* HAC = nullptr; // HAC being tested + UHoudiniCookable* HC = nullptr; // Cookable being tested + + TMap Data; // Use this to pass data between different tests. + + EHoudiniContextState CookingState = EHoudiniContextState::Idle; + EHoudiniContextState PDGState = EHoudiniContextState::Idle; + + int WaitTickFrame = 0; + UWorld * World = nullptr; + +private: + FDelegateHandle OutputDelegateHandle; + +}; + +class FHoudiniMultiTestContext +{ +public: + // Use this context if you have multuiple contexts, for example 2 HACs in one map. + TArray> Contexts; +}; + +#if WITH_DEV_AUTOMATION_TESTS +class FHoudiniLatentTestCommand : public FFunctionLatentCommand +{ +public: + // Each part of an HDA that requires a Cook should be its own FFunctionLatentCommand. Before the Update() + // function is called this class ensures the previous cook completed. + + // Use this constructor if you have one HDA. + FHoudiniLatentTestCommand(TSharedPtr InContext, TFunction InLatentPredicate) + : FFunctionLatentCommand(InLatentPredicate), SingleContext(InContext) {} + + FHoudiniLatentTestCommand(TSharedPtr InContext, TFunction InLatentPredicate) + : FFunctionLatentCommand(InLatentPredicate), MultiContext(InContext) { + } + + TSharedPtr SingleContext; + TSharedPtr MultiContext; + + // Like its base class, return true when the command is complete, false when it should be called again. + virtual bool Update() override; + + bool CheckForCookingComplete(FHoudiniTestContext* Context); +}; +#endif +``` + +`Source/HoudiniEngineEditor/Public/HoudiniEditorNodeSyncSubsystem.h`: + +```h +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "HoudiniApi.h" +#include "HoudiniEngine.h" +#include "HoudiniEngineEditorPrivatePCH.h" +#include "HoudiniInput.h" +#include "HoudiniNodeSyncComponent.h" + +#include "CoreMinimal.h" +#include "EditorSubsystem.h" +#include "Toolkits/AssetEditorModeUILayer.h" + +#include "HoudiniEditorNodeSyncSubsystem.generated.h" + +class USkeletalMesh; + + +USTRUCT() +struct HOUDINIENGINEEDITOR_API FHoudiniNodeSyncOptions +{ + GENERATED_BODY() + +public: + + UPROPERTY() + FString FetchNodePath = "/obj/..."; + + UPROPERTY() + FString SendNodePath = "/obj/UnrealContent"; + + UPROPERTY() + FString UnrealAssetName = ""; + + UPROPERTY() + FString UnrealAssetFolder = "/Game/NodeSync"; + + UPROPERTY() + bool bUseOutputNodes = true; + + UPROPERTY() + bool bFetchToWorld = false; + + UPROPERTY() + FString UnrealActorName = ""; + + UPROPERTY() + FString UnrealActorFolder = "/Houdini/NodeSync"; + + UPROPERTY() + bool bReplaceExisting = false; + + UPROPERTY() + bool bOverwriteSkeleton = false; + + UPROPERTY() + FString SkeletonAssetPath = ""; + + UPROPERTY() + bool bAutoBake = false; + + UPROPERTY() + bool bSyncWorldInput = false; + + // Generation properties for the Static Meshes generated by this Houdini Asset + UPROPERTY(Category = "HoudiniMeshGeneration", EditAnywhere, meta = (DisplayPriority = 1)/*, meta = (ShowOnlyInnerProperties)*/) + FHoudiniStaticMeshGenerationProperties StaticMeshGenerationProperties; + + // Build Settings to be used when generating the Static Meshes for this Houdini Asset + UPROPERTY(Category = "HoudiniMeshGeneration", EditAnywhere, meta = (DisplayPriority = 2)) + FMeshBuildSettings StaticMeshBuildSettings; + + // Returns the number of node paths in FetchNodePath - they're separated by ";" + int32 + GetNumberOfFetchNodePaths() + { + // Parse the fetch node path into a string array + TArray FetchNodePathsArray; + FetchNodePath.ParseIntoArray(FetchNodePathsArray, TEXT(";"), true); + return FetchNodePathsArray.Num(); + }; + + FString + GetFetchNodePathAt(int32 PathIdx = 0) + { + // Parse the fetch node path into a string array + TArray FetchNodePathsArray; + FetchNodePath.ParseIntoArray(FetchNodePathsArray, TEXT(";"), true); + + if (!FetchNodePathsArray.IsValidIndex(PathIdx)) + return FString(); + else + return FetchNodePathsArray[PathIdx]; + }; + + FString + GetFetchNodeNameAt(int32 PathIdx = 0) + { + FString NodePathAt = GetFetchNodePathAt(PathIdx); + + FString NodeNameAt = FString(); + int32 FoundPos = -1; + if (NodePathAt.FindLastChar(TEXT('/'), FoundPos)) + { + NodeNameAt = NodePathAt.RightChop(FoundPos + 1); + } + + return NodeNameAt; + }; + + FString + GetUnrealAssetName(int32 PathIdx = 0) + { + if (!UnrealAssetName.IsEmpty()) + return UnrealAssetName; + + FString NodeName = GetFetchNodeNameAt(PathIdx); + if (!NodeName.IsEmpty()) + return NodeName; + + // If we don't have a valid name/path - return a default one + return TEXT("NodeSyncAsset"); + }; + + FString + GetUnrealActorLabel(int32 PathIdx = 0) + { + FString NodeName = GetFetchNodeNameAt(PathIdx); + if (!NodeName.IsEmpty()) + return NodeName; + + // If we don't have a valid name/path - return a default one + return TEXT("HoudiniNodeSyncActor"); + }; +}; + + + +UCLASS() +class HOUDINIENGINEEDITOR_API UHoudiniEditorNodeSyncSubsystem : public UAssetEditorUISubsystem +{ + GENERATED_BODY() + +public: + + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + // Register layout for tab placement + virtual void RegisterLayoutExtensions(FLayoutExtender& Extender) override; + +public: + + UFUNCTION(BlueprintCallable, Category = "Houdini") + void SendToHoudini(const TArray& SelectedAssets, int32 ObjectIndex, const bool& bSendWorld); + + UFUNCTION(BlueprintCallable, Category = "Houdini") + void FetchFromHoudini(); + + UFUNCTION(BlueprintCallable, Category = "Houdini") + void SendWorldSelection(); + + UFUNCTION(BlueprintCallable, Category = "Houdini") + void SendContentBrowserSelection(const TArray& SelectedAssets); + + bool CreateSessionIfNeeded(); + + // Returns the color corresponding to a given node sync status + static FLinearColor GetStatusColor(const EHoudiniNodeSyncStatus& Status); + + bool GetNodeSyncWorldInput(UHoudiniInput*& OutInput); + + bool GetNodeSyncCBInput(UHoudiniInput*& OutInput); + + bool GatherAllFetchedNodeIds( + HAPI_NodeId UnrealFetchNodeId, + const bool bUseOutputNodes, + TArray& OutOutputNodes); + + bool ValidateFetchedNodePath( + const FString& InFetchedNodePath, + HAPI_NodeId& OutFetchedNodeId); + + // Node Sync ticks + void StartTicking(); + void StopTicking(); + bool IsTicking() const; + bool Tick(float DeltaTime); + + bool CheckNodeSyncInputNodesValid(); + + bool UpdateNodeSyncInputs(); + + void UpdateAllSelection(); + + void DeleteAllSelection(); + + // + // Public Members + // + + // Node sync options + FHoudiniNodeSyncOptions NodeSyncOptions; + + // SEND status + EHoudiniNodeSyncStatus LastSendStatus = EHoudiniNodeSyncStatus::None; + FString SendStatusMessage; + FString SendStatusDetails; + + // FETCH status + EHoudiniNodeSyncStatus LastFetchStatus = EHoudiniNodeSyncStatus::None; + FString FetchStatusMessage; + FString FetchStatusDetails; + +private: + + bool InitNodeSyncInputsIfNeeded(); + + UPROPERTY() + TObjectPtr NodeSyncWorldInput; + + UPROPERTY() + TObjectPtr NodeSyncCBInput; + + // Ticker handle, used for processing HAC. + FTSTicker::FDelegateHandle TickerHandle; + + // Stopping flag. + // Indicates that we should stop ticking asap + bool bMustStopTicking; + + // Last time we ticked NodeSync + double dLastTick; +}; + +``` + +`Source/HoudiniEngineEditor/Public/HoudiniPublicAPI.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "Templates/SubclassOf.h" + +#include "HoudiniPublicAPIInputTypes.h" +#include "HoudiniPublicAPIObjectBase.h" +#include "HoudiniEngineRuntimeCommon.h" + +#include "HoudiniPublicAPI.generated.h" + +// Public API version +// Increased with Houdini compatibility changes +#define HOUDINI_PUBLIC_API_VERSION_MAJOR 1 +// Increased for major API changes or changes that break backwards compatibility of the API +#define HOUDINI_PUBLIC_API_VERSION_MINOR 1 +// Increased for patches/revisions that don't change the API in a backwards incompatible manner +#define HOUDINI_PUBLIC_API_VERSION_PATCH 2 + +class ULevel; + +class UHoudiniAsset; +class UHoudiniPreset; +class UHoudiniPublicAPIAssetWrapper; +class UHoudiniPublicAPIInput; + +/** Public API version of EHoudiniRampInterpolationType: blueprints do not support int8 based enums. */ +UENUM(BlueprintType) +enum class EHoudiniPublicAPIRampInterpolationType : uint8 +{ + InValid = 0, + + CONSTANT = 1, + LINEAR = 2, + CATMULL_ROM = 3, + MONOTONE_CUBIC = 4, + BEZIER = 5, + BSPLINE = 6, + HERMITE = 7 +}; + +/** + * The Houdini Engine v2 Plug-in's Public API. + * + * The API allows one to manage a Houdini Engine session (Create/Stop/Restart), Pause/Resume asset cooking and + * instantiate HDA's and interact with it (set/update inputs, parameters, cook, iterate over outputs and bake outputs). + * + * Interaction with an instantiated HDA is done via UHoudiniPublicAPIAssetWrapper. + * + */ +UCLASS(BlueprintType, Blueprintable, Category="Houdini|Public API") +class HOUDINIENGINEEDITOR_API UHoudiniPublicAPI : public UHoudiniPublicAPIObjectBase +{ + GENERATED_BODY() + +public: + + UHoudiniPublicAPI(); + + // Session + + /** Returns true if there is a valid Houdini Engine session running/connected */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool IsSessionValid() const; + + /** Start a new Houdini Engine Session if there is no current session */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + void CreateSession(); + + /** Stops the current session */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + void StopSession(); + + /** Stops, then creates a new session */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + void RestartSession(); + + // Assets + + /** + * Instantiates an HDA in the specified world/level. Returns a wrapper for instantiated asset. + * + * Note: the lifecycle / ownership of the UHoudiniPublicAPIAssetWrapper* that is created and returned is not managed + * by the public API after creation, the caller must, for example, use a UProperty to prevent garbage collection of + * the wrapper if desired. + * + * @param InHoudiniAsset The HDA to instantiate. + * @param InInstantiateAt The Transform to instantiate the HDA with. + * @param InWorldContextObject A world context object for identifying the world to spawn in, if + * @InSpawnInLevelOverride is null. + * @param InSpawnInLevelOverride If not nullptr, then the AHoudiniAssetActor is spawned in that level. If both + * InSpawnInLevelOverride and InWorldContextObject are null, then the actor is spawned in the current editor + * context world's current level. + * @param bInEnableAutoCook If true (the default) the HDA will cook automatically after instantiation and after + * parameter, transform and input changes. + * @param bInEnableAutoBake If true, the HDA output is automatically baked after a cook. Defaults to false. + * @param InBakeDirectoryPath The directory to bake to if the bake path is not set via attributes on the HDA output. + * @param InBakeMethod The bake target (to actor vs blueprint). @see EHoudiniEngineBakeOption. + * @param bInRemoveOutputAfterBake If true, HDA temporary outputs are removed after a bake. Defaults to false. + * @param bInRecenterBakedActors Recenter the baked actors to their bounding box center. Defaults to false. + * @param bInReplacePreviousBake If true, on every bake replace the previous bake's output (assets + actors) with + * the new bake's output. Defaults to false. + * @return A wrapper for the instantiated asset, or nullptr if InHoudiniAsset or InInstantiateAt is invalid, or + * the AHoudiniAssetActor could not be spawned. See UHoudiniPublicAPIAssetWrapper. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API", Meta=(AutoCreateRefTerm="InInstantiateAt")) + UHoudiniPublicAPIAssetWrapper* InstantiateAsset( + UHoudiniAsset* InHoudiniAsset, + const FTransform& InInstantiateAt, + UObject* InWorldContextObject=nullptr, + ULevel* InSpawnInLevelOverride=nullptr, + const bool bInEnableAutoCook=true, + const bool bInEnableAutoBake=false, + const FString& InBakeDirectoryPath="", + const EHoudiniEngineBakeOption InBakeMethod=EHoudiniEngineBakeOption::ToActor, + const bool bInRemoveOutputAfterBake=false, + const bool bInRecenterBakedActors=false, + const bool bInReplacePreviousBake=false); + + /** + * Instantiates an HDA in the specified world/level using an existing wrapper. + * @param InWrapper The wrapper to instantiate the HDA with. + * @param InHoudiniAsset The HDA to instantiate. + * @param InInstantiateAt The Transform to instantiate the HDA with. + * @param InWorldContextObject A world context object for identifying the world to spawn in, if + * InSpawnInLevelOverride is null. + * @param InSpawnInLevelOverride If not nullptr, then the AHoudiniAssetActor is spawned in that level. If both + * InSpawnInLevelOverride and InWorldContextObject are null, then the actor is spawned in the current editor + * context world's current level. + * @param bInEnableAutoCook If true (the default) the HDA will cook automatically after instantiation and after + * parameter, transform and input changes. + * @param bInEnableAutoBake If true, the HDA output is automatically baked after a cook. Defaults to false. + * @param InBakeDirectoryPath The directory to bake to if the bake path is not set via attributes on the HDA output. + * @param InBakeMethod The bake target (to actor vs blueprint). @see EHoudiniEngineBakeOption. + * @param bInRemoveOutputAfterBake If true, HDA temporary outputs are removed after a bake. Defaults to false. + * @param bInRecenterBakedActors Recenter the baked actors to their bounding box center. Defaults to false. + * @param bInReplacePreviousBake If true, on every bake replace the previous bake's output (assets + actors) with + * the new bake's output. Defaults to false. + * @return true if InWrapper and InHoudiniAsset is valid and the AHoudiniAssetActor was spawned. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API", Meta=(AutoCreateRefTerm="InInstantiateAt")) + bool InstantiateAssetWithExistingWrapper( + UHoudiniPublicAPIAssetWrapper* InWrapper, + UHoudiniAsset* InHoudiniAsset, + const FTransform& InInstantiateAt, + UObject* InWorldContextObject=nullptr, + ULevel* InSpawnInLevelOverride=nullptr, + const bool bInEnableAutoCook=true, + const bool bInEnableAutoBake=false, + const FString& InBakeDirectoryPath="", + const EHoudiniEngineBakeOption InBakeMethod=EHoudiniEngineBakeOption::ToActor, + const bool bInRemoveOutputAfterBake=false, + const bool bInRecenterBakedActors=false, + const bool bInReplacePreviousBake=false); + + // Presets + + /** + * Instantiates an HDA Preset in the specified world/level. Returns a wrapper for instantiated asset. + * + * Note: the lifecycle / ownership of the UHoudiniPublicAPIAssetWrapper* that is created and returned is not managed + * by the public API after creation, the caller must, for example, use a UProperty to prevent garbage collection of + * the wrapper if desired. + * + * @param InHoudiniAssetPreset The HDA Preset to instantiate. + * @param InInstantiateAt The Transform to instantiate the HDA with. + * @param InWorldContextObject A world context object for identifying the world to spawn in, if + * @InSpawnInLevelOverride is null. + * @param InSpawnInLevelOverride If not nullptr, then the AHoudiniAssetActor is spawned in that level. If both + * InSpawnInLevelOverride and InWorldContextObject are null, then the actor is spawned in the current editor + * context world's current level. + * @param bInEnableAutoCook If true (the default) the HDA will cook automatically after instantiation and after + * parameter, transform and input changes. + * @param bInEnableAutoBake If true, the HDA output is automatically baked after a cook. Defaults to false. + * @param InBakeDirectoryPath The directory to bake to if the bake path is not set via attributes on the HDA output. + * @param InBakeMethod The bake target (to actor vs blueprint). @see EHoudiniEngineBakeOption. + * @param bInRemoveOutputAfterBake If true, HDA temporary outputs are removed after a bake. Defaults to false. + * @param bInRecenterBakedActors Recenter the baked actors to their bounding box center. Defaults to false. + * @param bInReplacePreviousBake If true, on every bake replace the previous bake's output (assets + actors) with + * the new bake's output. Defaults to false. + * @return A wrapper for the instantiated asset, or nullptr if InHoudiniAsset or InInstantiateAt is invalid, or + * the AHoudiniAssetActor could not be spawned. See UHoudiniPublicAPIAssetWrapper. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API", Meta=(AutoCreateRefTerm="InInstantiateAt")) + UHoudiniPublicAPIAssetWrapper* InstantiatePreset( + UHoudiniPreset* InHoudiniPreset, + const FTransform& InInstantiateAt, + UObject* InWorldContextObject=nullptr, + ULevel* InSpawnInLevelOverride=nullptr, + const bool bInEnableAutoCook=true, + const bool bInEnableAutoBake=false, + const FString& InBakeDirectoryPath="", + const EHoudiniEngineBakeOption InBakeMethod=EHoudiniEngineBakeOption::ToActor, + const bool bInRemoveOutputAfterBake=false, + const bool bInRecenterBakedActors=false, + const bool bInReplacePreviousBake=false); + + /** + * Instantiates an HDA Preset in the specified world/level using an existing wrapper. + * @param InWrapper The wrapper to instantiate the HDA with. + * @param InHoudiniPreset The HDA preset to instantiate. + * @param InInstantiateAt The Transform to instantiate the HDA with. + * @param InWorldContextObject A world context object for identifying the world to spawn in, if + * InSpawnInLevelOverride is null. + * @param InSpawnInLevelOverride If not nullptr, then the AHoudiniAssetActor is spawned in that level. If both + * InSpawnInLevelOverride and InWorldContextObject are null, then the actor is spawned in the current editor + * context world's current level. + * @param bInEnableAutoCook If true (the default) the HDA will cook automatically after instantiation and after + * parameter, transform and input changes. + * @param bInEnableAutoBake If true, the HDA output is automatically baked after a cook. Defaults to false. + * @param InBakeDirectoryPath The directory to bake to if the bake path is not set via attributes on the HDA output. + * @param InBakeMethod The bake target (to actor vs blueprint). @see EHoudiniEngineBakeOption. + * @param bInRemoveOutputAfterBake If true, HDA temporary outputs are removed after a bake. Defaults to false. + * @param bInRecenterBakedActors Recenter the baked actors to their bounding box center. Defaults to false. + * @param bInReplacePreviousBake If true, on every bake replace the previous bake's output (assets + actors) with + * the new bake's output. Defaults to false. + * @return true if InWrapper and InHoudiniAsset is valid and the AHoudiniAssetActor was spawned. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API", Meta=(AutoCreateRefTerm="InInstantiateAt")) + bool InstantiatePresetWithExistingWrapper( + UHoudiniPublicAPIAssetWrapper* InWrapper, + UHoudiniPreset* InHoudiniPreset, + const FTransform& InInstantiateAt, + UObject* InWorldContextObject=nullptr, + ULevel* InSpawnInLevelOverride=nullptr, + const bool bInEnableAutoCook=true, + const bool bInEnableAutoBake=false, + const FString& InBakeDirectoryPath="", + const EHoudiniEngineBakeOption InBakeMethod=EHoudiniEngineBakeOption::ToActor, + const bool bInRemoveOutputAfterBake=false, + const bool bInRecenterBakedActors=false, + const bool bInReplacePreviousBake=false); + + // Cooking + + /** Returns true if asset cooking is paused. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool IsAssetCookingPaused() const; + + /** Pause asset cooking (if not already paused) */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + void PauseAssetCooking(); + + /** Resume asset cooking (if it was paused) */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + void ResumeAssetCooking(); + + // Inputs + + /** + * Create a new empty API input object. The user must populate it and then set it as an input on an asset wrapper. + * @param InInputClass The class of the input to create, must be a subclass of UHoudiniPublicAPIInput. + * @param InOuter The owner of the input, if nullptr, then this API instance will be set as the outer. + * @return The newly created empty input object instance. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API", Meta=(DeterminesOutputType="InInputClass")) + UHoudiniPublicAPIInput* CreateEmptyInput(TSubclassOf InInputClass, UObject* InOuter=nullptr); + + // Helpers -- enum conversions + + /** + * Helper for converting from EHoudiniRampInterpolationType to EHoudiniPublicAPIRampInterpolationType + * @param InInterpolationType The EHoudiniRampInterpolationType to convert. + * @return The EHoudiniPublicAPIRampInterpolationType value of InInterpolationType. + */ + static EHoudiniPublicAPIRampInterpolationType ToHoudiniPublicAPIRampInterpolationType(const EHoudiniRampInterpolationType InInterpolationType); + + /** + * Helper for converting from EHoudiniPublicAPIRampInterpolationType to EHoudiniRampInterpolationType + * @param InInterpolationType The EHoudiniPublicAPIRampInterpolationType to convert. + * @return The EHoudiniRampInterpolationType value of InInterpolationType. + */ + static EHoudiniRampInterpolationType ToHoudiniRampInterpolationType(const EHoudiniPublicAPIRampInterpolationType InInterpolationType); + +}; + +``` + +`Source/HoudiniEngineEditor/Public/HoudiniPublicAPIAssetWrapper.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "GameFramework/Actor.h" + +#include "HoudiniPublicAPIObjectBase.h" +#include "HoudiniPublicAPI.h" +#include "HoudiniPublicAPIOutputTypes.h" +#include "HoudiniEngineRuntimeCommon.h" + +#include "HoudiniPublicAPIAssetWrapper.generated.h" + + +class UHoudiniPublicAPIInput; +class UHoudiniOutput; +class UHoudiniParameter; +class UHoudiniInput; +class UTOPNode; +class UHoudiniAssetComponent; +class UHoudiniCookable; +class AHoudiniAssetActor; + +/** + * The base class of a struct for Houdini Ramp points. + */ +USTRUCT(BlueprintType, Category="Houdini Engine | Public API") +struct HOUDINIENGINEEDITOR_API FHoudiniPublicAPIRampPoint +{ + GENERATED_BODY(); + +public: + FHoudiniPublicAPIRampPoint(); + + FHoudiniPublicAPIRampPoint( + const float InPosition, + const EHoudiniPublicAPIRampInterpolationType InInterpolation=EHoudiniPublicAPIRampInterpolationType::LINEAR); + + /** The position of the point on the Ramp's x-axis [0,1]. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API") + float Position; + + /** The interpolation type of the point. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API") + EHoudiniPublicAPIRampInterpolationType Interpolation; +}; + +/** + * A struct for Houdini float ramp points. + */ +USTRUCT(BlueprintType, Category="Houdini Engine | Public API") +struct HOUDINIENGINEEDITOR_API FHoudiniPublicAPIFloatRampPoint : public FHoudiniPublicAPIRampPoint +{ + GENERATED_BODY(); + +public: + + FHoudiniPublicAPIFloatRampPoint(); + + FHoudiniPublicAPIFloatRampPoint( + const float InPosition, + const float InValue, + const EHoudiniPublicAPIRampInterpolationType InInterpolation=EHoudiniPublicAPIRampInterpolationType::LINEAR); + + /** The value of the point. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API") + float Value; + +}; + +/** + * A struct for Houdini color ramp points. + */ +USTRUCT(BlueprintType, Category="Houdini Engine | Public API") +struct HOUDINIENGINEEDITOR_API FHoudiniPublicAPIColorRampPoint : public FHoudiniPublicAPIRampPoint +{ + GENERATED_BODY(); + +public: + + FHoudiniPublicAPIColorRampPoint(); + + FHoudiniPublicAPIColorRampPoint( + const float InPosition, + const FLinearColor& InValue, + const EHoudiniPublicAPIRampInterpolationType InInterpolation=EHoudiniPublicAPIRampInterpolationType::LINEAR); + + /** The value of the point. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API") + FLinearColor Value; + +}; + +/** + * A struct for storing the values of a Houdini parameter tuple. + * Currently supports bool, int32, float and FString storage. + */ +USTRUCT(BlueprintType, Category="Houdini Engine | Public API") +struct HOUDINIENGINEEDITOR_API FHoudiniParameterTuple +{ + GENERATED_BODY(); + +public: + FHoudiniParameterTuple(); + + /** + * Wrap a single bool value. + * @param InValue The value to wrap. + */ + FHoudiniParameterTuple(const bool& InValue); + /** + * Wrap a bool tuple. + * @param InValues The tuple to wrap. + */ + FHoudiniParameterTuple(const TArray& InValues); + + /** + * Wrap a single int32 value. + * @param InValue The value to wrap. + */ + FHoudiniParameterTuple(const int32& InValue); + /** + * Wrap a int32 tuple. + * @param InValues The tuple to wrap. + */ + FHoudiniParameterTuple(const TArray& InValues); + + /** + * Wrap a single float value. + * @param InValue The value to wrap. + */ + FHoudiniParameterTuple(const float& InValue); + /** + * Wrap a float tuple. + * @param InValues The tuple to wrap. + */ + FHoudiniParameterTuple(const TArray& InValues); + + /** + * Wrap a single FString value. + * @param InValue The value to wrap. + */ + FHoudiniParameterTuple(const FString& InValue); + /** + * Wrap a FString tuple. + * @param InValues The tuple to wrap. + */ + FHoudiniParameterTuple(const TArray& InValues); + + /** + * Wrap float ramp points + * @param InRampPoints The float ramp points. + */ + FHoudiniParameterTuple(const TArray& InRampPoints); + + /** + * Wrap color ramp points + * @param InRampPoints The color ramp points. + */ + FHoudiniParameterTuple(const TArray& InRampPoints); + + // Parameter tuple storage + + /** For bool compatible parameters, the bool parameter tuple values. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API") + TArray BoolValues; + + /** For int32 compatible parameters, the int32 parameter tuple values. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API") + TArray Int32Values; + + /** For float compatible parameters, the float parameter tuple values. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API") + TArray FloatValues; + + /** For string compatible parameters, the string parameter tuple values. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API") + TArray StringValues; + + /** For float ramp parameters, the ramp points. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API") + TArray FloatRampPoints; + + /** For color ramp parameters, the ramp points. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API") + TArray ColorRampPoints; +}; + +/** + * A wrapper for spawned/instantiating HDAs. + * + * The wrapper/HDA should be instantiated via UHoudiniPublicAPI::InstantiateAsset(). Alternatively an empty + * wrapper can be created via UHoudiniPublicAPIAssetWrapper::CreateEmptyWrapper() and an HDA later instantiated and + * assigned to the wrapper via UHoudiniPublicAPI::InstantiateAssetWithExistingWrapper(). + * + * The wrapper provides functionality for interacting/manipulating a AHoudiniAssetActor / UHoudiniAssetComponent: + * - Get/Set Inputs + * - Get/Set Parameters + * - Manually initiate a cook/recook + * - Subscribe to delegates: + * - #OnPreInstantiationDelegate (good place to set parameter values before the first cook) + * - #OnPostInstantiationDelegate (good place to set/configure inputs before the first cook) + * - #OnPostCookDelegate + * - #OnPreProcessStateExitedDelegate + * - #OnPostProcessingDelegate (output objects are available if the cook was successful) + * - #OnPostBakeDelegate + * - #OnPostPDGTOPNetworkCookDelegate + * - #OnPostPDGBakeDelegate + * - #OnProxyMeshesRefinedDelegate + * - Iterate over outputs and find the output assets + * - Bake outputs + * - PDG: Dirty all, cook outputs + * + * Important: In the current implementation of the plugin, nodes are cooked asynchronously. That means that cooking + * (including rebuilding the HDA and auto-cooks triggered from, for example, parameter changes) does not happen + * immediately. Functions in the API, such as Recook() and Rebuild(), do not block until the cook is complete, but + * instead immediately return after arranging for the cook to take place. This means that if a cook is triggered + * (either automatically, via parameter changes, or by calling Recook()) and there is a reliance on data that will only + * be available after the cook (such as an updated parameter interface, or the output objects of the cook), one of the + * delegates mentioned above (#OnPostProcessingDelegate or #OnPostCookDelegate, for example) would have to be used to + * execute the dependent code after the cook. + */ +UCLASS(BlueprintType, Blueprintable, Category="Houdini Engine|Public API") +class HOUDINIENGINEEDITOR_API UHoudiniPublicAPIAssetWrapper : public UHoudiniPublicAPIObjectBase +{ + GENERATED_BODY() + +public: + UHoudiniPublicAPIAssetWrapper(); + + // Delegate types + + DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHoudiniAssetStateChange, UHoudiniPublicAPIAssetWrapper*, InAssetWrapper); + DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHoudiniAssetPostCook, UHoudiniPublicAPIAssetWrapper*, InAssetWrapper, const bool, bInCookSuccess); + DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHoudiniAssetPostBake, UHoudiniPublicAPIAssetWrapper*, InAssetWrapper, const bool, bInBakeSuccess); + DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHoudiniAssetProxyMeshesRefinedDelegate, UHoudiniPublicAPIAssetWrapper* const, InAssetWrapper, const EHoudiniProxyRefineResult, InResult); + + /** + * Factory function for creating new wrapper instances around instantiated assets. + * + * Note: the lifecycle / ownership of the UHoudiniPublicAPIAssetWrapper* that is created and returned is not managed + * by the public API after creation, the caller must, for example, use a UProperty to prevent garbage collection of + * the wrapper if desired. + * + * @param InOuter The outer for the new wrapper instance. + * @param InHoudiniAssetActorOrComponent The AHoudiniAssetActor or UHoudiniAssetComponent to wrap. + * @return The newly instantiated wrapper that wraps InHoudiniAssetActor, or nullptr if the wrapper could not + * be created, or if InHoudiniAssetActorOrComponent is invalid or not of a supported type. + */ + UFUNCTION(BlueprintCallable, Category="Houdini|Public API") + static UHoudiniPublicAPIAssetWrapper* CreateWrapper(UObject* InOuter, UObject* InHoudiniAssetActorOrComponent); + + /** + * Factory function for creating a new empty wrapper instance. + * An instantiated actor can be wrapped using SetHoudiniAssetActor. + * + * Note: the lifecycle / ownership of the UHoudiniPublicAPIAssetWrapper* that is created and returned is not managed + * by the public API after creation, the caller must, for example, use a UProperty to prevent garbage collection of + * the wrapper if desired. + * + * @param InOuter the outer for the new wrapper instance. + * @return The newly instantiated wrapper. + */ + UFUNCTION(BlueprintCallable, Category="Houdini|Public API") + static UHoudiniPublicAPIAssetWrapper* CreateEmptyWrapper(UObject* InOuter); + + /** + * Checks if InObject can be wrapped by instances of UHoudiniPublicAPIAssetWrapper. + * @param InObject The object to check for compatiblity. + * @return true if InObject can be wrapped by instances of UHoudiniPublicAPIAssetWrapper. + */ + UFUNCTION(BlueprintCallable, Category="Houdini|Public API") + static bool CanWrapHoudiniObject(UObject* InObject); + + /** + * Wrap the specified instantiated houdini asset object. Supported objects are: AHoudiniAssetActor, + * UHoudiniAssetComponent. This will first unwrap/unbind the currently wrapped instantiated + * asset if InHoudiniAssetObjectToWrap is valid and of a supported class. + * + * If InHoudiniAssetObjectToWrap is nullptr, then this wrapper will unwrap/unbind the currently wrapped + * instantiated asset and return true. + * + * @param InHoudiniAssetObjectToWrap The object to wrap, or nullptr to unwrap/unbind if currently wrapping an + * asset. + * @return true if InHoudiniAssetObjectToWrap is valid, of a supported class and was successfully wrapped, or true + * if InHoudiniAssetObjectToWrap is nullptr. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool WrapHoudiniAssetObject(UObject* InHoudiniAssetObjectToWrap); + + // Accessors and mutators + + /** + * Get the wrapped instantiated houdini asset object. + * @return The wrapped instantiated houdini asset object. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + UObject* GetHoudiniAssetObject() const; + FORCEINLINE + virtual UObject* GetHoudiniAssetObject_Implementation() const { return HoudiniAssetObject.Get(); } + + /** + * Helper function for getting the instantiated HDA asset actor, if HoudiniAssetObject is an AHoudiniAssetActor or + * a UHoudiniAssetComponent owned by a AHoudiniAssetActor. + * @return The instantiated AHoudiniAssetActor, if HoudiniAssetObject is an AHoudiniAssetActor or + * a UHoudiniAssetComponent owned by a AHoudiniAssetActor, otherwise nullptr. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + AHoudiniAssetActor* GetHoudiniAssetActor() const; + + /** + * Helper function for getting the UHoudiniAssetComponent of the HDA, if HoudiniAssetObject is a + * UHoudiniAssetComponent or an AHoudiniAssetActor. + * @return The instantiated AHoudiniAssetActor, if HoudiniAssetObject is a + * UHoudiniAssetComponent or an AHoudiniAssetActor, otherwise nullptr. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + UHoudiniAssetComponent* GetHoudiniAssetComponent() const; + + /** + * Helper function for getting the UHoudiniCookable of the HDA, if HoudiniAssetObject is a + * UHoudiniCookable or an AHoudiniAssetActor. + * @return The instantiated UHoudiniCookable, if HoudiniAssetObject is a + * UHoudiniCookable or an AHoudiniAssetActor, otherwise nullptr. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + UHoudiniCookable* GetHoudiniCookable() const; + + /** + * Get the Temp Folder fallback as configured on asset details panel + * @param OutDirectoryPath The currently configured fallback temporary cook folder. + * @return true if the wrapper is valid and the value was fetched. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetTemporaryCookFolder(FDirectoryPath& OutDirectoryPath) const; + + /** + * Set the Temp Folder fallback as configured on asset details panel. Returns true if the value was changed. + * @param InDirectoryPath The new temp folder fallback to set. + * @return true if the wrapper is valid and the value was set. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetTemporaryCookFolder(const FDirectoryPath& InDirectoryPath) const; + + /** + * Get the Bake Folder fallback as configured on asset details panel. + * @param OutDirectoryPath The current bake folder fallback. + * @return true if the wrapper is valid and the value was fetched. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetBakeFolder(FDirectoryPath& OutDirectoryPath) const; + + /** + * Set the Bake Folder fallback as configured on asset details panel. Returns true if the value was changed. + * @param InDirectoryPath The new bake folder fallback. + * @return true if the wrapper is valid and the value was set. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetBakeFolder(const FDirectoryPath& InDirectoryPath) const; + + // Houdini Engine Actions + + /** Delete the instantiated asset from its level and mark the wrapper for destruction. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool DeleteInstantiatedAsset(); + + /** + * Marks the HDA as needing to be rebuilt in Houdini Engine and immediately returns. The rebuild happens + * asynchronously. If you need to take action after the rebuild and cook completes, one of the wrapper's delegates + * can be used, such as: OnPostCookDelegate or OnPostProcessingDelegate. + * + * @returns true If the HDA was successfully marked as needing to be rebuilt. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool Rebuild(); + + // Cooking + + /** + * Marks the HDA as needing to be cooked and immediately returns. The cook happens asynchronously. If you need + * to take action after the cook completes, one of the wrapper's delegates can be used, such as: + * OnPostCookDelegate or OnPostProcessingDelegate. + * + * @returns true If the HDA was successfully marked as needing to be cooked. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool Recook(); + + /** + * Enable or disable auto cooking of the asset (on parameter changes, input updates and transform changes, for + * example) + * @param bInSetEnabled Whether or not enable auto cooking. + * @return true if the value was changed. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetAutoCookingEnabled(const bool bInSetEnabled); + + /** Returns true if auto cooking is enabled for this instantiated asset. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool IsAutoCookingEnabled() const; + + + /** + * Enable or disable DoNotGenerateOutput + * @param bInSetEnabled Whether or not to enable "Do Not Generate Outputs" + * @return true if the value was changed. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetDoNotGenerateOutputs(const bool bInSetEnabled); + + /** Returns true if "Do Not Generate Outputs" is enabled for this instantiated asset. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool IsDoNotGenerateOutputsEnabled() const; + + /** + * Enable or disable triggering of an auto-cook on parameter/input changes. + * @param bInSetEnabled Whether or not to trigger an auto-cook on parameter/input changes. + * @return true if the value was changed. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetCookOnParameterOrInputChanges(const bool bInSetEnabled); + + /** Returns true if triggering a cook on parameter/input changes is enabled for this instantiated asset. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool IsCookOnParameterOrInputChangesEnabled() const; + + /** + * Enable or disable triggering of an auto-cook on transform changes. + * @param bInSetEnabled Whether or not to trigger an auto-cook on transform changes. + * @return true if the value was changed. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetCookOnTransformChange(const bool bInSetEnabled); + + /** Returns true if triggering a cook on transform changes is enabled for this instantiated asset. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool IsCookOnTransformChangeEnabled() const; + + /** + * Enable or disable triggering of an auto-cook on asset input cooks. + * @param bInSetEnabled Whether or not to trigger an auto-cook on asset input cooks. + * @return true if the value was changed. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetCookOnAssetInputCook(const bool bInSetEnabled); + + /** Returns true if triggering a cook on asset input cooks is enabled for this instantiated asset. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool IsCookOnAssetInputCookEnabled() const; + + // Baking + + /** + * Bake all outputs of the instantiated asset using the settings configured on the asset. + * @return true if the wrapper is valid and the baking process was started. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool BakeAllOutputs(); + + /** + * Bake all outputs of the instantiated asset using the specified settings. + * @param InBakeOption The bake method/target, (to actor vs blueprint, for example). + * @param bInReplacePreviousBake If true, replace the previous bake output (assets + actor) with the + * new results. + * @param bInRemoveTempOutputsOnSuccess If true, the temporary outputs of the wrapper asset are removed + * after a successful bake. + * @param bInRecenterBakedActors If true, recenter the baked actors to their bounding box center after the bake. + * @return true if the wrapper is valid and the baking process was started. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool BakeAllOutputsWithSettings( + EHoudiniEngineBakeOption InBakeOption, + bool bInReplacePreviousBake=false, + bool bInRemoveTempOutputsOnSuccess=false, + bool bInRecenterBakedActors=false); + + /** + * Set whether to automatically bake all outputs after a successful cook. + * @return false if the asset/wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetAutoBakeEnabled(const bool bInAutoBakeEnabled); + + /** Returns true if auto bake is enabled. See SetAutoBakeEnabled. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool IsAutoBakeEnabled() const; + + /** + * Sets the bake method to use (to actor, blueprint, foliage). + * @param InBakeMethod The new bake method to set. + * @return false if the asset/wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetBakeMethod(const EHoudiniEngineBakeOption InBakeMethod); + + /** + * Gets the currently set bake method to use (to actor, blueprint, foliage). + * @param OutBakeMethod The current bake method. + * @return false if the asset/wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetBakeMethod(EHoudiniEngineBakeOption& OutBakeMethod); + + /** + * Set the bRemoveOutputAfterBake property, that controls if temporary outputs are removed after a successful bake. + * @param bInRemoveOutputAfterBake If true, then after a successful bake, the HACs outputs will be cleared and + * removed. + * @return false if the asset/wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetRemoveOutputAfterBake(const bool bInRemoveOutputAfterBake); + + /** + * Get the bRemoveOutputAfterBake property, that controls if temporary outputs are removed after a successful bake. + * @return true if bRemoveOutputAfterBake is true. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetRemoveOutputAfterBake() const; + + /** + * Set the bRecenterBakedActors property that controls if baked actors are recentered around their bounding box center. + * @param bInRecenterBakedActors If true, recenter baked actors to their bounding box center after bake. + * @return false if the asset/wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetRecenterBakedActors(const bool bInRecenterBakedActors); + + /** Gets the bRecenterBakedActors property. If true, recenter baked actors to their bounding box center after bake. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetRecenterBakedActors() const; + + /** + * Set the bReplacePreviousBake property, if true, replace the previously baked output (if any) instead of creating + * new objects. + * @param bInReplacePreviousBake If true, replace the previously baked output (if any) instead of creating new + * objects. + * @return false if the asset/wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetReplacePreviousBake(const bool bInReplacePreviousBake); + + /** Get the bReplacePreviousBake property. + * @return The value of bReplacePreviousBake. If true, previous bake output (if any) will be replaced by subsequent + * bakes. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetReplacePreviousBake() const; + + /** + * Gets all generated Actors which are the result of a bake. + * @return array of scene actors from bake action. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + TArray GetBakedOutputActors(); + + // Parameters + + /** + * Set the value of a float-based parameter. + * Supported parameter types: + * - Float + * - Color + * @param InParameterTupleName The name of the parameter tuple. + * @param InValue The value to set. + * @param InAtIndex The index of the parameter in the parameter tuple to set the value at. Defaults to 0. + * @param bInMarkChanged If true, the parameter is marked as changed and will be uploaded to Houdini before the + * next cook. If auto-cook triggers are enabled, this will also trigger a auto-cook. Defaults to true. + * @return true if the value was set or the parameter already had the given value. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetFloatParameterValue(FName InParameterTupleName, float InValue, int32 InAtIndex=0, bool bInMarkChanged=true); + + /** + * Get the value of a float parameter. Returns true if the parameter and index was found and the value set in OutValue. + * Supported parameter types: + * - Float + * - Color + * @param InParameterTupleName The name of the parameter tuple. + * @param OutValue The value of the parameter that was fetched. + * @param InAtIndex The index of the parameter in the parameter tuple to get. Defaults to 0. + * @return true if the wrapper is valid and the parameter was found. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetFloatParameterValue(FName InParameterTupleName, float& OutValue, int32 InAtIndex=0) const; + + /** + * Set the value of a color parameter. + * Supported parameter types: + * - Color + * @param InParameterTupleName The name of the parameter tuple. + * @param InValue The value to set. + * @param bInMarkChanged If true, the parameter is marked as changed and will be uploaded to Houdini before the + * next cook. If auto-cook triggers are enabled, this will also trigger a auto-cook. Defaults to true. + * @return true if the value was set or the parameter already had the given value. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetColorParameterValue(FName InParameterTupleName, const FLinearColor& InValue, bool bInMarkChanged=true); + + /** + * Get the value of a color parameter. Returns true if the parameter was found and the value set in OutValue. + * Supported parameter types: + * - Color + * @param InParameterTupleName The name of the parameter tuple. + * @param OutValue The value of the parameter that was fetched. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetColorParameterValue(FName InParameterTupleName, FLinearColor& OutValue) const; + + /** + * Set the value of a int32 parameter. + * Supported parameter types: + * - Int + * - IntChoice + * - MultiParm + * - Toggle + * - Folder (set the folder as currently shown) + * @param InParameterTupleName The name of the parameter tuple. + * @param InValue The value to set. + * @param InAtIndex The index of the parameter in the parameter tuple to set the value at. Defaults to 0. + * @param bInMarkChanged If true, the parameter is marked as changed and will be uploaded to Houdini before the + * next cook. If auto-cook triggers are enabled, this will also trigger a auto-cook. Defaults to true. + * @return true if the value was set or the parameter already had the given value. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetIntParameterValue(FName InParameterTupleName, int32 InValue, int32 InAtIndex=0, bool bInMarkChanged=true); + + /** + * Get the value of a int32 parameter. + * Supported parameter types: + * - Int + * - IntChoice + * - MultiParm + * - Toggle + * - Folder (get if the folder is currently shown) + * @param InParameterTupleName The name of the parameter tuple. + * @param OutValue The value of the parameter that was fetched. + * @param InAtIndex The index of the parameter in the parameter tuple to get. Defaults to 0. + * @return true if the parameter and index was found and the value set in OutValue. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetIntParameterValue(FName InParameterTupleName, int32& OutValue, int32 InAtIndex=0) const; + + /** + * Set the value of a bool parameter. + * Supported parameter types: + * - Toggle + * - Folder (set the folder as currently shown) + * @param InParameterTupleName The name of the parameter tuple. + * @param InValue The value to set. + * @param InAtIndex The index of the parameter in the parameter tuple to set the value at. + * @param bInMarkChanged If true, the parameter is marked as changed and will be uploaded to Houdini before the + * next cook. If auto-cook triggers are enabled, this will also trigger a auto-cook. Defaults to true. + * @return true if the value was set or the parameter already had the given value. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetBoolParameterValue(FName InParameterTupleName, bool InValue, int32 InAtIndex=0, bool bInMarkChanged=true); + + /** + * Get the value of a bool parameter. + * Supported parameter types: + * - Toggle + * - Folder (get if the folder is currently shown) + * @param InParameterTupleName The name of the parameter tuple. + * @param OutValue The value of the parameter that was fetched. + * @param InAtIndex The index of the parameter in the parameter tuple to get. Defaults to 0. + * @return true if the parameter and index was found and the value set in OutValue. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetBoolParameterValue(FName InParameterTupleName, bool& OutValue, int32 InAtIndex=0) const; + + /** + * Set the value of a String parameter. + * Supported parameter types: + * - String + * - StringChoice + * - StringAssetRef + * - File + * - FileDir + * - FileGeo + * - FileImage + * @param InParameterTupleName The name of the parameter tuple. + * @param InValue The value to set. + * @param InAtIndex The index of the parameter in the parameter tuple to set the value at. + * @param bInMarkChanged If true, the parameter is marked as changed and will be uploaded to Houdini before the + * next cook. If auto-cook triggers are enabled, this will also trigger a auto-cook. Defaults to true. + * @return true if the value was set or the parameter already had the given value. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetStringParameterValue(FName InParameterTupleName, const FString& InValue, int32 InAtIndex=0, bool bInMarkChanged=true); + + /** + * Get the value of a String parameter. + * Supported parameter types: + * - String + * - StringChoice + * - StringAssetRef + * - File + * - FileDir + * - FileGeo + * - FileImage + * @param InParameterTupleName The name of the parameter tuple. + * @param OutValue The value of the parameter that was fetched. + * @param InAtIndex The index of the parameter in the parameter tuple to get. Defaults to 0. + * @return true if the parameter was found and the value set in OutValue. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetStringParameterValue(FName InParameterTupleName, FString& OutValue, int32 InAtIndex=0) const; + + /** + * Set the value of an AssetRef parameter. + * Supported parameter types: + * - StringAssetRef + * @param InParameterTupleName The name of the parameter tuple. + * @param InValue The value to set. + * @param InAtIndex The index of the parameter in the parameter tuple to set the value at. + * @param bInMarkChanged If true, the parameter is marked as changed and will be uploaded to Houdini before the + * next cook. If auto-cook triggers are enabled, this will also trigger a auto-cook. Defaults to true. + * @return true if the value was set or the parameter already had the given value. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetAssetRefParameterValue(FName InParameterTupleName, UObject* InValue, int32 InAtIndex=0, bool bInMarkChanged=true); + + /** + * Get the value of an AssetRef parameter. + * Supported parameter types: + * - StringAssetRef + * @param InParameterTupleName The name of the parameter tuple. + * @param OutValue The value of the parameter that was fetched. + * @param InAtIndex The index of the parameter in the parameter tuple to get. Defaults to 0. + * @return True if the parameter was found and the value set in OutValue. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetAssetRefParameterValue(FName InParameterTupleName, UObject*& OutValue, int32 InAtIndex=0) const; + + /** + * Sets the number of points of the specified ramp parameter. This will insert or remove points from the end + * as necessary. + * @param InParameterTupleName The name of the parameter tuple. + * @param InNumPoints The new number of points to set. Must be >= 1. + * @return true if the parameter was found and the number of points set, or if the number of points was already InNumPoints. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetRampParameterNumPoints(FName InParameterTupleName, const int32 InNumPoints) const; + + /** + * Gets the number of points of the specified ramp parameter. + * @param InParameterTupleName The name of the parameter tuple. + * @param OutNumPoints The number of points the ramp has. + * @return true if the parameter was found and the number of points fetched. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetRampParameterNumPoints(FName InParameterTupleName, int32& OutNumPoints) const; + + /** + * Set the position, value and interpolation of a point of a FloatRamp parameter. + * Supported parameter types: + * - FloatRamp + * @param InParameterTupleName The name of the parameter tuple. + * @param InPointIndex The index of the ramp point to set. + * @param InPointPosition The point position to set [0, 1]. + * @param InPointValue The value to set for the point. + * @param InInterpolationType The interpolation to set at the point. Defaults to EHoudiniPublicAPIRampInterpolationType.Linear. + * @param bInMarkChanged If true, the parameter is marked as changed and will be uploaded to Houdini before the + * next cook. If auto-cook triggers are enabled, this will also trigger a auto-cook. Defaults to true. + * @return true if the value was set. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetFloatRampParameterPointValue( + FName InParameterTupleName, + const int32 InPointIndex, + const float InPointPosition, + const float InPointValue, + const EHoudiniPublicAPIRampInterpolationType InInterpolationType=EHoudiniPublicAPIRampInterpolationType::LINEAR, + const bool bInMarkChanged=true); + + /** + * Get the position, value and interpolation of a point of a FloatRamp parameter. + * Supported parameter types: + * - FloatRamp + * @param InParameterTupleName The name of the parameter tuple. + * @param InPointIndex The index of the ramp point to get. + * @param OutPointPosition The point position [0, 1]. + * @param OutPointValue The value at the point. + * @param OutInterpolationType The interpolation of the point. + * @return True if the parameter was found and output values set. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetFloatRampParameterPointValue( + FName InParameterTupleName, + const int32 InPointIndex, + float& OutPointPosition, + float& OutPointValue, + EHoudiniPublicAPIRampInterpolationType& OutInterpolationType) const; + + /** + * Set all of the points (position, value and interpolation) of float ramp. + * Supported parameter types: + * - FloatRamp + * @param InParameterTupleName The name of the parameter tuple. + * @param InRampPoints An array of structs to set as the ramp's points. + * @param bInMarkChanged If true, parameters are marked as changed and will be uploaded to Houdini before the + * next cook. If auto-cook triggers are enabled, this will also trigger a auto-cook. Defaults to true. + * @return true if the values were set. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetFloatRampParameterPoints( + FName InParameterTupleName, + const TArray& InRampPoints, + const bool bInMarkChanged=true); + + /** + * Get the all of the points (position, value and interpolation) of a FloatRamp parameter. + * Supported parameter types: + * - FloatRamp + * @param InParameterTupleName The name of the parameter tuple. + * @param OutRampPoints The array to populate with the ramp's points. + * @return True if the parameter was found and output values set. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetFloatRampParameterPoints( + FName InParameterTupleName, + TArray& OutRampPoints) const; + + /** + * Set the position, value and interpolation of a point of a ColorRamp parameter. + * Supported parameter types: + * - ColorRamp + * @param InParameterTupleName The name of the parameter tuple. + * @param InPointIndex The index of the ramp point to set. + * @param InPointPosition The point position to set [0, 1]. + * @param InPointValue The value to set for the point. + * @param InInterpolationType The interpolation to set at the point. Defaults to EHoudiniPublicAPIRampInterpolationType.Linear. + * @param bInMarkChanged If true, the parameter is marked as changed and will be uploaded to Houdini before the + * next cook. If auto-cook triggers are enabled, this will also trigger a auto-cook. Defaults to true. + * @return true if the value was set. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetColorRampParameterPointValue( + FName InParameterTupleName, + const int32 InPointIndex, + const float InPointPosition, + const FLinearColor& InPointValue, + const EHoudiniPublicAPIRampInterpolationType InInterpolationType=EHoudiniPublicAPIRampInterpolationType::LINEAR, + const bool bInMarkChanged=true); + + /** + * Get the position, value and interpolation of a point of a ColorRamp parameter. + * Supported parameter types: + * - ColorRamp + * @param InParameterTupleName The name of the parameter tuple. + * @param InPointIndex The index of the ramp point to get. + * @param OutPointPosition The point position [0, 1]. + * @param OutPointValue The value at the point. + * @param OutInterpolationType The interpolation of the point. + * @return True if the parameter was found and output values set. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetColorRampParameterPointValue( + FName InParameterTupleName, + const int32 InPointIndex, + float& OutPointPosition, + FLinearColor& OutPointValue, + EHoudiniPublicAPIRampInterpolationType& OutInterpolationType) const; + + /** + * Set all of the points (position, value and interpolation) of color ramp. + * Supported parameter types: + * - ColorRamp + * @param InParameterTupleName The name of the parameter tuple. + * @param InRampPoints An array of structs to set as the ramp's points. + * @param bInMarkChanged If true, parameters are marked as changed and will be uploaded to Houdini before the + * next cook. If auto-cook triggers are enabled, this will also trigger a auto-cook. Defaults to true. + * @return true if the values were set. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetColorRampParameterPoints( + FName InParameterTupleName, + const TArray& InRampPoints, + const bool bInMarkChanged=true); + + /** + * Get the all of the points (position, value and interpolation) of a ColorRamp parameter. + * Supported parameter types: + * - ColorRamp + * @param InParameterTupleName The name of the parameter tuple. + * @param OutRampPoints The array to populate with the ramp's points. + * @return True if the parameter was found and output values set. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetColorRampParameterPoints( + FName InParameterTupleName, + TArray& OutRampPoints) const; + + /** + * Trigger / click the specified button parameter. + * @return True if the button was found and triggered/clicked, or was already marked to be triggered. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool TriggerButtonParameter(FName InButtonParameterName); + + /** + * Gets all parameter tuples (with their values) from this asset and outputs it to OutParameterTuples. + * @param OutParameterTuples Populated with all parameter tuples and their values. + * @return false if the asset/wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetParameterTuples(TMap& OutParameterTuples) const; + + /** + * Sets all parameter tuple values (matched by name and compatible type) from InParameterTuples on this + * instantiated asset. + * @param InParameterTuples The parameter tuples to set. + * @return false if any entry in InParameterTuples could not be found on the asset or had an incompatible type/size. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetParameterTuples(const TMap& InParameterTuples); + + // Inputs + + /** + * Creates an empty input wrapper. + * @param InInputClass the class of the input to create. See the UHoudiniPublicAPIInput class hierarchy. + * @return The newly created input wrapper, or null if the input wrapper could not be created. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API", Meta=(DeterminesOutputType="InInputClass")) + UHoudiniPublicAPIInput* CreateEmptyInput(TSubclassOf InInputClass); + + /** + * Get the number of node inputs supported by the asset. + * @return The number of node inputs (inputs on the HDA node, excluding parameter-based inputs). Returns -1 if the + * asset/wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + UPARAM(DisplayName="OutNumNodeInputs") int32 GetNumNodeInputs() const; + + /** + * Set a node input at the specific index. + * @param InNodeInputIndex The index of the node input, starts at 0. + * @param InInput The input wrapper to use to set the input. + * @return false if the the input could not be set, for example, if the wrapper is invalid, or if the input index + * is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetInputAtIndex(const int32 InNodeInputIndex, const UHoudiniPublicAPIInput* InInput); + + /** + * Get the node input at the specific index and sets OutInput. This is a copy of the input structure. Changes to + * properties in OutInput won't affect the instantiated HDA until a subsequent call to SetInputAtIndex. + * @param InNodeInputIndex The index of the node input to get. + * @param OutInput Copy of the input configuration and data for node input index InNodeInputIndex. + * @return false if the input could be fetched, for example if the wrapper is invalid or the input index is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetInputAtIndex(const int32 InNodeInputIndex, UHoudiniPublicAPIInput*& OutInput); + + /** + * Set node inputs at the specified indices via a map. + * @param InInputs A map of node input index to input wrapper to use to set inputs on the instantiated asset. + * @return true if all inputs from InInputs were set successfully. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetInputsAtIndices(const TMap& InInputs); + + /** + * Get all node inputs. + * @param OutInputs All node inputs as a map, with the node input index as key. The input configuration is copied + * from the instantiated asset, and changing an input property from the entry in this map will not affect the + * instantiated asset until a subsequent SetInputsAtIndices() call or SetInputAtIndex() call. + * @return false if the wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetInputsAtIndices(TMap& OutInputs); + + /** + * Set a parameter-based input via parameter name. + * @param InParameterName The name of the input parameter. + * @param InInput The input wrapper to use to set/configure the input. + * @return false if the wrapper is invalid, InParameterName is not a valid input parameter, or if InInput could + * not be used to successfully configure/set the input. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API", Meta=(AutoCreateRefTerm="InParameterName")) + bool SetInputParameter(const FName& InParameterName, const UHoudiniPublicAPIInput* InInput); + + /** + * Get a parameter-based input via parameter name. This is a copy of the input structure. Changes to properties in + * OutInput won't affect the instantiated HDA until a subsequent call to SetInputParameter. + * @param InParameterName The name of the input parameter. + * @param OutInput A copy of the input configuration for InParameterName. + * @return false if the wrapper is invalid, InParameterName is not a valid input parameter, or the current input + * configuration could not be successfully copied to a new UHoudiniPublicAPIInput wrapper. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API", Meta=(AutoCreateRefTerm="InParameterName")) + bool GetInputParameter(const FName& InParameterName, UHoudiniPublicAPIInput*& OutInput); + + /** + * Set a parameter-based inputs via a map, + * @param InInputs A map of input parameter names to input wrapper to use to set inputs on the instantiated asset. + * @return true if all inputs from InInputs were set successfully. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API", Meta=(AutoCreateRefTerm="InParameterName")) + bool SetInputParameters(const TMap& InInputs); + + /** + * Get a parameter-based inputs as a map + * @param OutInputs All parameter inputs as a map, with the input parameter name as key. The input configuration is + * copied from the instantiated asset, and changing an input property from the entry in this map will not affect the + * instantiated asset until a subsequent SetInputParameters() call or SetInputParameter() call. + * @return false if the wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API", Meta=(AutoCreateRefTerm="InParameterName")) + bool GetInputParameters(TMap& OutInputs); + + // Outputs + + /** + * Gets the number of outputs of the instantiated asset. + * @return the number of outputs of the instantiated asset. -1 if the asset/wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + int32 GetNumOutputs() const; + + /** + * Gets the output type of the output at index InIndex. + * @param InIndex The output index to get the type for. + * @return the output type of the output at index InIndex. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + EHoudiniOutputType GetOutputTypeAt(const int32 InIndex) const; + + /** + * Populates OutIdentifiers with the output object identifiers of all the output objects of the output at InIndex. + * @param InIndex The output index to get output identifiers for. + * @param OutIdentifiers The output identifiers of the output objects at output index InIndex. + * @return false if the asset/wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetOutputIdentifiersAt(const int32 InIndex, TArray& OutIdentifiers) const; + + /** + * Gets the output object at InIndex identified by InIdentifier. + * @param InIndex The output index to get output object from. + * @param InIdentifier The output identifier of the output object to get from output index InIndex. + * @return nullptr if the index/identifier is invalid or if the asset/wrapper is invalid, otherwise the output + * object. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + UObject* GetOutputObjectAt(const int32 InIndex, const FHoudiniPublicAPIOutputObjectIdentifier& InIdentifier) const; + + /** + * Gets the output component at InIndex identified by InIdentifier. + * @param InIndex The output index to get output component from. + * @param InIdentifier The output identifier of the output component to get from output index InIndex. + * @return nullptr if the index/identifier is invalid or if the asset/wrapper is invalid, otherwise the output + * component. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + UObject* GetOutputComponentAt(const int32 InIndex, const FHoudiniPublicAPIOutputObjectIdentifier& InIdentifier) const; + + /** + * Gets the output's fallback BakeName (as configured on the output details panel) for the output at InIndex + * identified by InIdentifier. + * @param InIndex The output index of the output object to get fallback BakeName for. + * @param InIdentifier The output identifier of the output object to get fallback BakeName for. + * @param OutBakeName The fallback BakeName configured for the output object identified by InIndex and InIdentifier. + * @return false if the index/identifier is invalid or if the asset/wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetOutputBakeNameFallbackAt(const int32 InIndex, const FHoudiniPublicAPIOutputObjectIdentifier& InIdentifier, FString& OutBakeName) const; + + /** + * Sets the output's fallback BakeName (as configured on the output details panel) for the output at InIndex + * identified by InIdentifier. + * @param InIndex The output index of the output object to set fallback BakeName for. + * @param InIdentifier The output identifier of the output object to set fallback BakeName for. + * @param InBakeName The fallback BakeName to set for the output object identified by InIndex and InIdentifier. + * @return false if the index/identifier is invalid or if the asset/wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetOutputBakeNameFallbackAt(const int32 InIndex, const FHoudiniPublicAPIOutputObjectIdentifier& InIdentifier, const FString& InBakeName); + + /** + * Bake the specified output object to the content browser. + * @param InIndex The output index of the output object to bake. + * @param InIdentifier The output identifier of the output object to bake. + * @param InBakeName The bake name to bake with. + * @param InLandscapeBakeType For landscape assets, the output bake type. + * @return true if the output was baked successfully, false if the wrapper/asset is invalid, or the output index + * and output identifier combination is invalid, or if baking failed. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool BakeOutputObjectAt(const int32 InIndex, const FHoudiniPublicAPIOutputObjectIdentifier& InIdentifier, const FName InBakeName=NAME_None, const EHoudiniLandscapeOutputBakeType InLandscapeBakeType=EHoudiniLandscapeOutputBakeType::InValid); + + /** + * Returns true if the wrapped asset has any proxy output on any outputs. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool HasAnyCurrentProxyOutput() const; + + /** + * Returns true if the wrapped asset has any proxy output at output InIndex. + * @param InIndex The output index to check for proxies. + * @return true if the wrapped asset has any proxy output at output InIndex. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool HasAnyCurrentProxyOutputAt(const int32 InIndex) const; + + /** + * Returns true if the output object at output InIndex with identifier InIdentifier is a proxy. + * @param InIndex The output index of the output object to check. + * @param InIdentifier The output identifier of the output object at output index InIndex to check. + * @return true if the output object at output InIndex with identifier InIdentifier is a proxy. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool IsOutputCurrentProxyAt(const int32 InIndex, const FHoudiniPublicAPIOutputObjectIdentifier& InIdentifier) const; + + // Proxy mesh + + /** + * Refines all current proxy mesh outputs (if any) to static mesh. This could trigger a cook if the asset is loaded + * and has no cook data. + * @param bInSilent If true, then slate notifications about the refinement process are not shown. + * @return Whether the refinement process is needed, requires a pending asynchronous cook, or was completed + * synchronously. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + EHoudiniProxyRefineRequestResult RefineAllCurrentProxyOutputs(const bool bInSilent); + + // PDG + + /** + * Returns true if the wrapped asset is valid and has a PDG asset link. + * @return true if the wrapped asset is valid and has a PDG asset link. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool HasPDGAssetLink() const; + + /** + * Gets the paths (relative to the instantiated asset) of all TOP networks in the HDA. + * @param OutTOPNetworkPaths The relative paths of the TOP networks in the HDA. + * @return false if the asset/wrapper is invalid, or does not contain any TOP networks. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetPDGTOPNetworkPaths(TArray& OutTOPNetworkPaths) const; + + /** + * Gets the paths (relative to the specified TOP network) of all TOP nodes in the network. + * @param InNetworkRelativePath The relative path of the network inside the instantiated asset, as returned by + * GetPDGTOPNetworkPaths(), to fetch TOP node paths for. + * @return false if the asset/wrapper is invalid, or does not contain the specified TOP network. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetPDGTOPNodePaths(const FString& InNetworkRelativePath, TArray& OutTOPNodePaths) const; + + /** + * Dirty all TOP networks in this asset. + * @return true if TOP networks were dirtied. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool PDGDirtyAllNetworks(); + + /** + * Dirty the specified TOP network. + * @param InNetworkRelativePath The relative path of the network inside the instantiated asset, as returned by + * GetPDGTOPNetworkPaths(). + * @return true if the TOP network was dirtied. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool PDGDirtyNetwork(const FString& InNetworkRelativePath); + + /** + * Dirty the specified TOP node. + * @param InNetworkRelativePath The relative path of the network inside the instantiated asset, as returned by + * GetPDGTOPNetworkPaths(). + * @param InNodeRelativePath The relative path of the TOP node inside the specified TOP network. + * @return true if TOP nodes were dirtied. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool PDGDirtyNode(const FString& InNetworkRelativePath, const FString& InNodeRelativePath); + + // // Cook all outputs for all TOP networks in this asset. + // // Returns true if TOP networks were set to cook. + // UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + // bool PDGCookOutputsForAllNetworks(); + + /** + * Cook all outputs for the specified TOP network. + * @param InNetworkRelativePath The relative path of the network inside the instantiated asset, as returned by + * GetPDGTOPNetworkPaths(). + * @return true if the TOP network was set to cook. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool PDGCookOutputsForNetwork(const FString& InNetworkRelativePath); + + /** + * Cook the specified TOP node. + * @param InNetworkRelativePath The relative path of the network inside the instantiated asset, as returned by + * GetPDGTOPNetworkPaths(). + * @param InNodeRelativePath The relative path of the TOP node inside the specified TOP network. + * @return true if the TOP node was set to cook. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool PDGCookNode(const FString& InNetworkRelativePath, const FString& InNodeRelativePath); + + /** + * Bake all outputs of the instantiated asset's PDG contexts using the settings configured on the asset. + * @return true if the bake was successful. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool PDGBakeAllOutputs(); + + /** + * Bake all outputs of the instantiated asset's PDG contexts using the specified settings. + * @param InBakeOption The bake option (to actors, blueprints or foliage). + * @param InBakeSelection Whether to bake outputs from all networks, the selected network or the selected node. + * @param InBakeReplacementMode Whether to replace previous bake results/existing assets with the same name + * when baking. + * @param bInRecenterBakedActors If true, recenter baked actors to their bounding box center. Defaults to false. + * @return true if the bake was successful. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool PDGBakeAllOutputsWithSettings( + const EHoudiniEngineBakeOption InBakeOption, + const EPDGBakeSelectionOption InBakeSelection, + const EPDGBakePackageReplaceModeOption InBakeReplacementMode, + const bool bInRecenterBakedActors=false); + + /** + * Set whether to automatically bake PDG work items after a successfully loading them. + * @param bInAutoBakeEnabled If true, automatically bake work items after successfully loading them. + * @return false if the asset/wrapper is invalid, or does not contain a TOP network. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetPDGAutoBakeEnabled(const bool bInAutoBakeEnabled); + + /** Returns true if PDG auto bake is enabled. See SetPDGAutoBakeEnabled(). */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool IsPDGAutoBakeEnabled() const; + + /** + * Set if, when automatically baking PDG work items after a successfully loading them (see SetPDGAutoBakeEnabled()), + * nodes with some failed work items should also be auto-baked. + * @param bInEnabled If true, also auto-bake nodes with some failed work items. + * @return false if the asset/wrapper is invalid, or does not contain a TOP network. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetPDGAutoBakeNodesWithFailedWorkItemsEnabled(const bool bInEnabled); + + /** + * Returns true if, when auto baking is enabled (see IsPDGAutoBakeEnabled()), nodes with some failed work items + * will also be auto-baked. + * See SetPDGAutoBakeNodesWithFailedWorkItemsEnabled(). + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool IsPDGAutoBakeNodesWithFailedWorkItemsEnabled() const; + + /** + * Sets the bake method to use for PDG baking (to actor, blueprint, foliage). + * @param InBakeMethod The new bake method to set. + * @return false if the asset/wrapper is invalid. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetPDGBakeMethod(const EHoudiniEngineBakeOption InBakeMethod); + + /** + * Gets the currently set bake method to use for PDG baking (to actor, blueprint, foliage). + * @param OutBakeMethod The current bake method. + * @return false if the asset/wrapper is invalid or does not contain a TOP network. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetPDGBakeMethod(EHoudiniEngineBakeOption& OutBakeMethod); + + /** + * Set which outputs to bake for PDG, for example, all, selected network, selected node + * @param InBakeSelection The new bake selection. + * @return false if the asset/wrapper is invalid or does not contain a TOP network. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetPDGBakeSelection(const EPDGBakeSelectionOption InBakeSelection); + + /** + * Get which outputs to bake for PDG, for example, all, selected network, selected node + * @param OutBakeSelection The current bake selection setting. + * @return false if the asset/wrapper is invalid or does not contain a TOP network. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetPDGBakeSelection(EPDGBakeSelectionOption& OutBakeSelection); + + /** + * Setter for the bRecenterBakedActors property, that determines if baked actors are recentered to their bounding + * box center after a PDG bake, on the PDG asset link. + * @param bInRecenterBakedActors If true, recenter baked actors to their bounding box center after bake (PDG) + * @return false if the asset/wrapper is invalid or does not contain a TOP network. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetPDGRecenterBakedActors(const bool bInRecenterBakedActors); + + /** + * Getter for the bRecenterBakedActors property, that determines if baked actors are recentered to their bounding + * box center after a PDG bake, on the PDG asset link. + * @return true if baked actors should be recentered to their bounding box center after bake (PDG) + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetPDGRecenterBakedActors() const; + + /** + * Set the replacement mode to use for PDG baking (replace previous bake output vs increment) + * @param InBakingReplacementMode The new replacement mode to set. + * @return false if the asset/wrapper is invalid or does not contain a TOP network. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool SetPDGBakingReplacementMode(const EPDGBakePackageReplaceModeOption InBakingReplacementMode); + + /** + * Get the replacement mode to use for PDG baking (replace previous bake output vs increment) + * @param OutBakingReplacementMode The current replacement mode. + * @return false if the asset/wrapper is invalid or does not contain a TOP network. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + bool GetPDGBakingReplacementMode(EPDGBakePackageReplaceModeOption& OutBakingReplacementMode) const; + + /** + * Helper function for manually processing the wrapped component until it reaches a blocking state + * This means that instantiating, cooking and processing of the HDA will be blocking, as the function + * only returns once all processing is finished. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + void ProcessComponentSynchronous(); + + /** + * Getter for the OnPreInstantiationDelegate, broadcast before the HDA is instantiated. The HDA's default parameter + * definitions are available, but the node has not yet been instantiated in HAPI/Houdini Engine. Parameter values + * can be set at this point. + */ + FOnHoudiniAssetStateChange& GetOnPreInstantiationDelegate() { return OnPreInstantiationDelegate; } + + /** + * Getter for the OnPostInstantiationDelegate, broadcast after the HDA is instantiated. This is a good place to + * set/configure inputs before the first cook. + */ + FOnHoudiniAssetStateChange& GetOnPostInstantiationDelegate() { return OnPostInstantiationDelegate; } + + /** + * Getter for the OnPostCookDelegate, broadcast after the HDA has cooked. + */ + FOnHoudiniAssetPostCook& GetOnPostCookDelegate() { return OnPostCookDelegate; } + + /** + * Getter for the OnPreProcessStateExitedDelegate, broadcast after the output pre-processing phase of the HDA, + * but before it enters the post processing phase. When this delegate is broadcast output objects/assets have not + * yet been created. + */ + FOnHoudiniAssetStateChange& GetOnPreProcessStateExitedDelegate() { return OnPreProcessStateExitedDelegate; } + + /** + * Getter for the OnPostProcessingDelegate, broadcast after the HDA has processed its outputs and created output + * objects/assets. + */ + FOnHoudiniAssetStateChange& GetOnPostProcessingDelegate() { return OnPostProcessingDelegate; } + + /** + * Getter for the OnPostBakeDelegate, broadcast after the HDA has finished baking outputs (not called for + * individual outputs that are baked to the content browser). + */ + FOnHoudiniAssetPostBake& GetOnPostBakeDelegate() { return OnPostBakeDelegate; } + + /** + * Getter for the OnPostPDGTOPNetworkCookDelegate, broadcast after the HDA/PDG has cooked a TOP Network. Work item + * results have not necessarily yet been loaded. + */ + FOnHoudiniAssetPostCook& GetOnPostPDGTOPNetworkCookDelegate() { return OnPostPDGTOPNetworkCookDelegate; } + + /** + * Getter for the OnPostPDGBakeDelegate, broadcast after the HDA/PDG has finished baking outputs (not called for + * individual outputs that are baked to the content browser). + */ + FOnHoudiniAssetPostBake& GetOnPostPDGBakeDelegate() { return OnPostPDGBakeDelegate; } + + /** + * Getter for the OnProxyMeshesRefinedDelegate, broadcast for each proxy mesh of the HDA's outputs that has been + * refined to a UStaticMesh. + */ + FOnHoudiniAssetProxyMeshesRefinedDelegate& GetOnProxyMeshesRefinedDelegate() { return OnProxyMeshesRefinedDelegate; } + +protected: + + /** This will unwrap/unbind the currently wrapped instantiated asset. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini|Public API") + void ClearHoudiniAssetObject(); + + /** + * Attempt to bind to the asset's PDG asset link, if it has one, and if the wrapper is not already bound to its + * events. + */ + UFUNCTION() + bool BindToPDGAssetLink(); + + /** Handler that is bound to the wrapped HAC's state change delegate. */ + UFUNCTION() + void HandleOnHoudiniCookableStateChange(UHoudiniCookable* InHC, const EHoudiniAssetState InFromState, const EHoudiniAssetState InToState); + + /** Handler that is bound to the wrapped HAC's PostCook delegate. */ + UFUNCTION() + void HandleOnHoudiniCookablePostCook(UHoudiniCookable* InHC, const bool bInCookSuccess); + + /** Handler that is bound to the wrapped HAC's PostBake delegate. */ + UFUNCTION() + void HandleOnHoudiniCookablePostBake(UHoudiniCookable* InHC, const bool bInBakeSuccess); + + /** Handler that is bound to the wrapped PDG asset link's OnPostTOPNetworkCookDelegate delegate. */ + UFUNCTION() + void HandleOnHoudiniPDGAssetLinkTOPNetPostCook(UHoudiniPDGAssetLink* InPDGAssetLink, UTOPNetwork* InTOPNet, const bool bInAnyWorkItemsFailed); + + /** Handler that is bound to the wrapped PDG asset link's OnPostBake delegate. */ + UFUNCTION() + void HandleOnHoudiniPDGAssetLinkPostBake(UHoudiniPDGAssetLink* InPDGAssetLink, const bool bInBakeSuccess); + + /** + * Handler that is bound to FHoudiniEngineCommands::GetOnHoudiniProxyMeshesRefined(). It is called for any HAC + * that has its proxy meshes refined. If relevant for this wrapped asset, then + * #OnProxyMeshesRefinedDelegate is broadcast. + */ + UFUNCTION() + void HandleOnHoudiniProxyMeshesRefinedGlobal(UHoudiniCookable* InHC, const EHoudiniProxyRefineResult InResult); + + /** + * Helper function for getting the instantiated asset's AHoudiniAssetActor. If there is no valid + * AHoudiniAssetActor an error is set with SetErrorMessage() and false is returned. + * @param OutActor Set to the AHoudiniAssetActor of the wrapped asset, if found. + * @return true if the wrapped asset has/is a valid AHoudiniAssetActor, false otherwise. + */ + bool GetValidHoudiniAssetActorWithError(AHoudiniAssetActor*& OutActor) const; + + /** + * Helper function for getting the instantiated asset's UHoudiniAssetComponent. If there is no valid + * HoudiniAssetComponent an error is set with SetErrorMessage() and false is returned. + * @param OutHAC Set to the HoudiniAssetComponent of the wrapped asset, if found. + * @return true if the wrapped asset has a valid HoudiniAssetComponent, false otherwise. + */ + bool GetValidHoudiniAssetComponentWithError(UHoudiniAssetComponent*& OutHAC) const; + + /** Helper function for getting the instantiated asset's Cookable. + *If there is no valid Cookable an error is set with SetErrorMessage() and false is returned. + * @param OutHAC Set to the HoudiniAssetComponent of the wrapped asset, if found. + * @return true if the wrapped asset has a valid HoudiniAssetComponent, false otherwise. + */ + bool GetValidHoudiniCookableWithError(UHoudiniCookable*& OutHC) const; + + /** + * Helper function for getting a valid output at the specified index. If there is no valid + * UHoudiniOutput at that index (either the index is out of range or the output is null/invalid) an error is set + * with SetErrorMessage() and false is returned. + * @param InOutputIndex The output index. + * @param OutOutput Set to the valid UHoudiniOutput at InOutputIndex, if found. + * @return true if there is a valid UHoudiniOutput at index InOutputIndex. + */ + bool GetValidOutputAtWithError(const int32 InOutputIndex, UHoudiniOutput*& OutOutput) const; + + /** Helper function for getting the instantiated asset's PDG asset link. */ + UHoudiniPDGAssetLink* GetHoudiniPDGAssetLink() const; + + /** + * Helper function for getting the instantiated asset's UHoudiniPDGAssetLink. If there is no valid + * UHoudiniPDGAssetLink an error is set with SetErrorMessage() and false is returned. + * @param OutAssetLink Set to the UHoudiniPDGAssetLink of the wrapped asset, if found. + * @return true if the wrapped asset has a valid UHoudiniPDGAssetLink, false otherwise. + */ + bool GetValidHoudiniPDGAssetLinkWithError(UHoudiniPDGAssetLink*& OutAssetLink) const; + + /** Helper function for getting a valid parameter by name */ + UHoudiniParameter* FindValidParameterByName(const FName& InParameterTupleName) const; + + /** + * Helper function to find the appropriate array index for a ramp point. + * @param InParam The parameter. + * @param InIndex The index of the ramp point to get. If INDEX_NONE, all points are fetched. + * @param OutPointData Array populated with all fetched points. The bool in the pair is true if the + * object is UHoudiniParameterRampFloatPoint or UHoudiniParameterRampColorPoint and false if it is + * UHoudiniParameterRampModificationEvent. + * @return true if the point was, or all points were, fetched successfully. + */ + bool FindRampPointData( + UHoudiniParameter* const InParam, const int32 InIndex, TArray>& OutPointData) const; + + /** + * Set the position, value and interpolation of a point of a ramp parameter. + * Supported parameter types: + * - FloatRamp + * - ColorRamp + * @param InParameterTupleName The name of the parameter tuple. + * @param InPointIndex The index of the ramp point to set. + * @param InPosition The point position [0, 1]. + * @param InFloatValue The float value at the point (if this is a float ramp). + * @param InColorValue The color value at the point (if this is a color ramp). + * @param InInterpolation The interpolation of the point. + * @param bInMarkChanged If true, the parameter is marked as changed and will be uploaded to Houdini before the + * next cook. If auto-cook triggers are enabled, this will also trigger a auto-cook. Defaults to true. + * @return true if the value was set. + */ + bool SetRampParameterPointValue( + FName InParameterTupleName, + const int32 InPointIndex, + const float InPosition, + const float InFloatValue, + const FLinearColor& InColorValue, + const EHoudiniPublicAPIRampInterpolationType InInterpolation, + const bool bInMarkChanged=true); + + /** + * Get the position, value and interpolation of a point of a ramp parameter. + * Supported parameter types: + * - FloatRamp + * - ColorRamp + * @param InParameterTupleName The name of the parameter tuple. + * @param InPointIndex The index of the ramp point to get. + * @param OutPosition The point position [0, 1]. + * @param OutFloatValue The float value at the point (if this is a float ramp). + * @param OutColorValue The color value at the point (if this is a color ramp). + * @param OutInterpolation The interpolation of the point. + * @return True if the parameter was found and output values set. + */ + bool GetRampParameterPointValue( + FName InParameterTupleName, + const int32 InPointIndex, + float& OutPosition, + float& OutFloatValue, + FLinearColor& OutColorValue, + EHoudiniPublicAPIRampInterpolationType& OutInterpolation) const; + + /** Helper function for getting an input by node index */ + UHoudiniInput* GetHoudiniNodeInputByIndex(const int32 InNodeInputIndex); + const UHoudiniInput* GetHoudiniNodeInputByIndex(const int32 InNodeInputIndex) const; + + /** Helper function for getting an input by parameter name */ + UHoudiniInput* FindValidHoudiniNodeInputParameter(const FName& InInputParameterName); + const UHoudiniInput* FindValidHoudiniNodeInputParameter(const FName& InInputParameterName) const; + + /** Helper function for populating a UHoudiniPublicAPIInput from a UHoudiniInput */ + bool CreateAndPopulateAPIInput(const UHoudiniInput* InHoudiniInput, UHoudiniPublicAPIInput*& OutAPIInput); + /** Helper function for populating a UHoudiniInput from a UHoudiniPublicAPIInput */ + bool PopulateHoudiniInput(const UHoudiniPublicAPIInput* InAPIInput, UHoudiniInput* InHoudiniInput) const; + + /** + * Helper functions for getting a TOP network by path. If the TOP network could not be found, an error is set + * with SetErrorMessage, and false is returned. + * @param InNetworkRelativePath The relative path to the network inside the asset. + * @param OutNetworkIndex The index to the network in the asset link's AllNetworks array. + * @param OutNetwork The network that was found at InNetworkRelativePath. + * @return true if the network was found and is valid, false otherwise. + */ + bool GetValidTOPNetworkByPathWithError(const FString& InNetworkRelativePath, int32& OutNetworkIndex, UTOPNetwork*& OutNetwork) const; + + /** + * Helper functions for getting a TOP node by path. If the TOP node could not be found, an error is set + * with SetErrorMessage, and false is returned. + * @param InNetworkRelativePath The relative path to the network inside the asset. + * @param InNodeRelativePath The relative path to the node inside the network. + * @param OutNetworkIndex The index to the network in the asset link's AllTOPNetworks array. + * @param OutNodeIndex The index to the TOP node in the network's AllTOPNodes array. + * @param OutNode The node that was found. + * @return true if the node was found and is valid, false otherwise. + */ + bool GetValidTOPNodeByPathWithError( + const FString& InNetworkRelativePath, + const FString& InNodeRelativePath, + int32& OutNetworkIndex, + int32& OutNodeIndex, + UTOPNode*& OutNode) const; + + // + // TSoftObjectPtr seems to potentially cause race conditions when running the UE test framework + // Switching to TWeakObjectPtr seem to solve the issue: + // + + /** The wrapped Houdini Asset object (not the uasset, an AHoudiniAssetActor or UHoudiniAssetComponent). */ + UPROPERTY(BlueprintReadOnly, Category="Houdini|Public API") + TWeakObjectPtr HoudiniAssetObject; + + /** The wrapped AHoudiniAssetActor (derived from HoudiniAssetObject when calling WrapHoudiniAssetObject()). */ + UPROPERTY(BlueprintReadOnly, Category="Houdini|Public API") + TWeakObjectPtr CachedHoudiniAssetActor; + + /** The wrapped UHoudiniAssetComponent (derived from HoudiniAssetObject when calling WrapHoudiniAssetObject()). */ + UPROPERTY(BlueprintReadOnly, Category="Houdini|Public API") + TWeakObjectPtr CachedHoudiniAssetComponent; + + /** The wrapped UHoudiniCookable (derived from HoudiniAssetObject when calling WrapHoudiniAssetObject()). */ + UPROPERTY(BlueprintReadOnly, Category="Houdini|Public API") + TWeakObjectPtr CachedHoudiniCookable; + + /** + * Delegate that is broadcast when entering the PreInstantiation state: the HDA's default parameter definitions are + * available, but the node has not yet been instantiated in HAPI/Houdini Engine. Parameters can be set at this point. + */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnHoudiniAssetStateChange OnPreInstantiationDelegate; + + /** + * Delegate that is broadcast after the asset was successfully instantiated. This is a good place to set/configure + * inputs before the first cook. + */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnHoudiniAssetStateChange OnPostInstantiationDelegate; + + /** Delegate that is broadcast after a cook completes. Output objects/assets have not yet been created/updated. */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnHoudiniAssetPostCook OnPostCookDelegate; + + /** Delegate that is broadcast when PreProcess is exited. Output objects/assets have not yet been created/updated. */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnHoudiniAssetStateChange OnPreProcessStateExitedDelegate; + + /** + * Delegate that is broadcast when the Processing state is exited and the None state is entered. Output objects + * assets have been created/updated. + */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnHoudiniAssetStateChange OnPostProcessingDelegate; + + /** + * Delegate that is broadcast after baking the asset (not called for individual outputs that are baked to the + * content browser). + */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnHoudiniAssetPostBake OnPostBakeDelegate; + + /** + * Delegate that is broadcast after a cook of a TOP network completes. Work item results have not necessarily yet + * been loaded. + */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnHoudiniAssetPostCook OnPostPDGTOPNetworkCookDelegate; + + /** + * Delegate that is broadcast after baking PDG outputs (not called for individual outputs that are baked to the + * content browser). + */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnHoudiniAssetPostBake OnPostPDGBakeDelegate; + + /** Delegate that is broadcast after refining all proxy meshes for this wrapped asset. */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnHoudiniAssetProxyMeshesRefinedDelegate OnProxyMeshesRefinedDelegate; + + /** + * This starts as false and is set to true in HandleOnHoudiniAssetComponentStateChange during post instantiation, + * once we have checked for a PDG asset link and configured the bindings if there is one. + */ + UPROPERTY() + bool bAssetLinkSetupAttemptComplete; + + // Delegate handles + + /** Handle for the binding to the HAC's asset state change delegate */ + FDelegateHandle OnAssetStateChangeDelegateHandle; + /** Handle for the binding to the HAC's post cook delegate */ + FDelegateHandle OnPostCookDelegateHandle; + /** Handle for the binding to the HAC's post bake delegate */ + FDelegateHandle OnPostBakeDelegateHandle; + /** Handle for the binding to the global proxy mesh refined delegate */ + FDelegateHandle OnHoudiniProxyMeshesRefinedDelegateHandle; + /** Handle for the binding to the HAC's PDG asset link's post TOP Network cook delegate */ + FDelegateHandle OnPDGPostTOPNetworkCookDelegateHandle; + /** Handle for the binding to the HAC's PDG asset link's post bake delegate */ + FDelegateHandle OnPDGPostBakeDelegateHandle; + +}; + +``` + +`Source/HoudiniEngineEditor/Public/HoudiniPublicAPIBlueprintLib.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" + +#include "HoudiniPublicAPIBlueprintLib.generated.h" + +class UHoudiniPublicAPI; + +/** + * Houdini Public API Blueprint function library + */ +UCLASS(Category="Houdini Engine|Public API") +class HOUDINIENGINEEDITOR_API UHoudiniPublicAPIBlueprintLib : public UBlueprintFunctionLibrary +{ + GENERATED_UCLASS_BODY() + +public: + /** Returns the Houdini Public API instance. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "GetHoudiniEnginePublicAPI"), Category = "Houdini Engine") + static UHoudiniPublicAPI* GetAPI(); +}; + +``` + +`Source/HoudiniEngineEditor/Public/HoudiniPublicAPIInputTypes.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#include "HoudiniEngineRuntimeCommon.h" +#include "HoudiniPublicAPIObjectBase.h" + +#include "HoudiniPublicAPIInputTypes.generated.h" + +class UHoudiniInput; +class UHoudiniInputObject; +class UHoudiniSplineComponent; + +/** + * This class is the base class of a hierarchy that represents an input to an HDA in the public API. + * + * Each type of input has a derived class: + * - Geometry: UHoudiniPublicAPIGeoInput + * - Curve: UHoudiniPublicAPICurveInput + * - World: UHoudiniPublicAPIWorldInput + * + * Each instance of one of these classes represents the configuration of an input and wraps the actor/object/asset + * used as the input. These instances are always treated as copies of the actual state of the HDA's input: changing + * a property of one of these instances does not immediately affect the instantiated HDA: one has to pass the input + * instances as arguments to UHoudiniPublicAPIAssetWrapper::SetInputAtIndex() or + * UHoudiniPublicAPIAssetWrapper::SetInputParameter() functions to actually change the inputs on the instantiated asset. + * A copy of the existing input state of an instantiated HDA can be fetched via + * UHoudiniPublicAPIAssetWrapper::GetInputAtIndex() and UHoudiniPublicAPIAssetWrapper::GetInputParameter(). + */ +UCLASS(BlueprintType, Category="Houdini Engine | Public API | Inputs") +class HOUDINIENGINEEDITOR_API UHoudiniPublicAPIInput : public UHoudiniPublicAPIObjectBase +{ + GENERATED_BODY() + +public: + UHoudiniPublicAPIInput(); + + /** Is set to true when this input's Transform Type is set to NONE, 2 will use the input's default value .*/ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bKeepWorldTransform; + + /** + * Indicates that all the input objects are imported to Houdini as references instead of actual geo + * (for Geo/World input types only) + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bImportAsReference; + + /** + * Indicates whether or not to add the rot / scale attributes for reference imports + * (for Geo/World input types only) + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bImportAsReferenceRotScaleEnabled; + + /** + * Indicates whether or not to add bounding box attributes for reference imports + * (for Geo/World input types only) + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bImportAsReferenceBboxEnabled; + + /** + * Indicates whether or not to add material attributes for reference imports + * (for Geo/World input types only) + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bImportAsReferenceMaterialEnabled; + + /** + * Indicates whether or not to export the detailled materials parameters with the input data + * (for Geo/World input types only) + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bExportMaterialParameters; + + /** Returns true if InObject is acceptable for this input type. + * @param InObject The object to check for acceptance as an input object on this input. + * @return true if InObject is acceptable for this input type. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs") + bool IsAcceptableObjectForInput(UObject* InObject) const; + virtual bool IsAcceptableObjectForInput_Implementation(UObject* InObject) const; + + /** + * Sets the specified objects as the input objects. + * @param InObjects The objects to set as input objects for this input. + * @return false if any object was incompatible (all compatible objects are added). + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs") + bool SetInputObjects(const TArray& InObjects); + + /** + * Gets the currently assigned input objects. + * @param OutObjects The current input objects of this input. + * @return true if input objects were successfully added to OutObjects (even if there are no input objects). + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs") + bool GetInputObjects(TArray& OutObjects); + + /** + * Populate this input instance from the internal UHoudiniInput instance InInput. + * This copies the configuration and UHoudiniInputObject(s). + * @param InInput The internal UHoudiniInput to copy to this instance. + * @return false if there were any errors while copying the input data. + */ + virtual bool PopulateFromHoudiniInput(UHoudiniInput const* const InInput); + + /** + * Update an internal UHoudiniInput instance InInput from this API input instance. + * This copies the configuration and UHoudiniInputObject(s) from this API instance instance to InInput. + * @param InInput The internal UHoudiniInput to update from to this API instance. + * @return false if there were any errors while copying the input data. + */ + virtual bool UpdateHoudiniInput(UHoudiniInput* const InInput) const; + +protected: + + /** + * Copy any properties we need from the UHoudiniInputObject InInputObject for the the input object it wraps. + * @param InHoudiniInputObject The UHoudiniInputObject to copy from. + * @param InInputObjectIndex The input object to copy per-object properties for. + * @return false if the copy failed, for example if InSrc is invalid or of incompatible type. + */ + virtual bool CopyHoudiniInputObjectPropertiesToInputObject(UHoudiniInputObject const* const InHoudiniInputObject, const int32 InInputObjectIndex); + + /** + * Copy any properties for InInputObjectIndex from this input wrapper to InHoudiniInputObject. + * @param InInputObjectIndex The index of the input object to copy per-object properties for. + * @param InHoudiniInputObject The Houdini input object to copy the properties to. + * @return false if the copy failed, for example if InSrc is invalid or of incompatible type. + */ + virtual bool CopyInputObjectPropertiesToHoudiniInputObject(const int32 InInputObjectIndex, UHoudiniInputObject* const InHoudiniInputObject) const; + + /** + * Convert an object used as an input in an internal UHoudiniInput to be API compatible. + * @param InInternalInputObject An object as an input by UHoudiniInput + * @return An API compatible input object created from InInternalInputObject. By default this is just + * InInternalInputObject. + */ + virtual UObject* ConvertInternalInputObject(UObject* InInternalInputObject) { return InInternalInputObject; } + + /** + * Convert an object used as an input in the API to be UHoudiniInput to be compatible and assign it to the + * InHoudiniInput at index InIndex. + * @param InAPIInputObject An input object in the API. + * @param InHoudiniInput The UHoudiniInput to assign the input to. + * @param InInputIndex The object index in InHoudiniInput to assign the converted input object to. + * @return A UHoudiniInput compatible input object created from InAPIInputObject. By default this is just + * InAPIInputObject. + */ + virtual UObject* ConvertAPIInputObjectAndAssignToInput(UObject* InAPIInputObject, UHoudiniInput* InHoudiniInput, const int32 InInputIndex) const; + + /** + * Returns the type of the input. Subclasses should override this and return the appropriate input type. + * The base class / default implementation returns EHoudiniInputType::Invalid + */ + virtual EHoudiniInputType GetInputType() const { return EHoudiniInputType::Invalid; } + + /** The input objects for this input. */ + UPROPERTY() + TArray> InputObjects; + +}; + + +/** + * API wrapper input class for geometry inputs. Derived from UHoudiniPublicAPIInput. + */ +UCLASS(BlueprintType, Category="Houdini Engine | Public API | Inputs") +class HOUDINIENGINEEDITOR_API UHoudiniPublicAPIGeoInput : public UHoudiniPublicAPIInput +{ + GENERATED_BODY() + +public: + UHoudiniPublicAPIGeoInput(); + + /** Indicates that the geometry must be packed before merging it into the input */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bPackBeforeMerge; + + /** Indicates that the main mesh/geo should be marshalled to Houdini */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bExportMainGeometry; + + /** Indicates that all LODs in the input should be marshalled to Houdini */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bExportLODs; + + /** Indicates that all sockets in the input should be marshalled to Houdini */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bExportSockets; + + /** Indicates that all colliders in the input should be marshalled to Houdini */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bExportColliders; + + /** Prefer Nanite fallback mesh when using Nanite asset as geometry input */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Houdini Engine | Public API | Inputs") + bool bPreferNaniteFallbackMesh; + + /** + * Set the transform offset of the input object at the specified index in InputObjects (must already have been set via SetInputObjects()). + * @param InInputObjectIndex The input object index to set a transform offset for. + * @param InTransform The transform offset to set. + * @return true if the index is valid and the transform offset set. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs", Meta=(AutoCreateRefTerm="InTransform")) + bool SetInputObjectTransformOffset(const int32 InInputObjectIndex, const FTransform& InTransform); + + /** + * Get the transform offset of the input object at the specified index in InputObjects (must already have been set via SetInputObjects()). + * @param InInputObjectIndex The input object index to get a transform offset for. + * @param OutTransform The transform offset that was fetched. + * @return true if the index is valid and the was transform offset fetched. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs") + bool GetInputObjectTransformOffset(const int32 InInputObjectIndex, FTransform& OutTransform) const; + + /** + * Get the array of input object transforms. + * @param OutInputObjectTransformOffsetArray The output array. + * @return false if the input type does not support input object transform offsets. See SupportsTransformOffset. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs") + bool GetInputObjectTransformOffsetArray(TArray& OutInputObjectTransformOffsetArray) const; + + /** + * Returns true if this input type supports Transform offsets. If false, + * functions that get/set transform offsets will return false and set an error: + * - SetObjectTransformOffset + * - GetObjectTransformOffset + * - SetInputObjectTransformOffset + * - GetInputObjectTransformOffset + */ + virtual bool SupportsTransformOffset() const { return true; } + + virtual bool SetInputObjects_Implementation(const TArray& InObjects) override; + + virtual bool PopulateFromHoudiniInput(UHoudiniInput const* const InInput) override; + + virtual bool UpdateHoudiniInput(UHoudiniInput* const InInput) const override; + + virtual void PostLoad() override; + +protected: + virtual bool CopyHoudiniInputObjectPropertiesToInputObject(UHoudiniInputObject const* const InHoudiniInputObject, const int32 InInputObjectIndex) override; + + virtual bool CopyInputObjectPropertiesToHoudiniInputObject(const int32 InInputObjectIndex, UHoudiniInputObject* const InHoudiniInputObject) const override; + + virtual EHoudiniInputType GetInputType() const override { return EHoudiniInputType::Geometry; } + +private: + /** Transform offset per input object (by input object array index). */ + UPROPERTY() + TArray InputObjectTransformOffsetArray; +}; + +UENUM(BlueprintType) +enum class EHoudiniPublicAPICurveType : uint8 +{ + Invalid = 0, + + Polygon = 1, + Nurbs = 2, + Bezier = 3, + Points = 4 +}; + +UENUM(BlueprintType) +enum class EHoudiniPublicAPICurveMethod : uint8 +{ + Invalid = 0, + + CVs = 1, + Breakpoints = 2, + Freehand = 3 +}; + +UENUM(BlueprintType) +enum class EHoudiniPublicAPICurveBreakpointParameterization : uint8 +{ + Invalid = 0, + + Uniform = 1, + Chord = 2, + Centripetal = 3 +}; + +/** + * API wrapper input class for curve inputs. Derived from UHoudiniPublicAPIInput. + */ +UCLASS(BlueprintType, Category="Houdini Engine | Public API | Inputs | Input Objects") +class HOUDINIENGINEEDITOR_API UHoudiniPublicAPICurveInputObject : public UHoudiniPublicAPIObjectBase +{ + GENERATED_BODY() + +public: + UHoudiniPublicAPICurveInputObject(); + + /** + * Set the points of the curve (replacing any previously set points with InCurvePoints). + * @param InCurvePoints The new points to set / replace the curve's points with. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs | Input Objects") + void SetCurvePoints(const TArray& InCurvePoints); + FORCEINLINE + virtual void SetCurvePoints_Implementation(const TArray& InCurvePoints) { CurvePoints = InCurvePoints; } + + /** + * Append a point to the end of this curve. + * @param InCurvePoint The point to append. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Meta=(AutoCreateRefTerm="InCurvePoint"), Category="Houdini Engine | Public API | Inputs | Input Objects") + void AppendCurvePoint(const FTransform& InCurvePoint); + FORCEINLINE + virtual void AppendCurvePoint_Implementation(const FTransform& InCurvePoint) { CurvePoints.Add(InCurvePoint); } + + /** Remove all points from the curve. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs | Input Objects") + void ClearCurvePoints(); + FORCEINLINE + virtual void ClearCurvePoints_Implementation() { CurvePoints.Empty(); } + + /** + * Get all points of the curve. + * @param OutCurvePoints Set to a copy of all of the points of the curve. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs | Input Objects") + void GetCurvePoints(TArray& OutCurvePoints) const; + FORCEINLINE + virtual void GetCurvePoints_Implementation(TArray& OutCurvePoints) const { OutCurvePoints = CurvePoints; } + + /** Returns true if this is a closed curve. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs | Input Objects") + bool IsClosed() const; + FORCEINLINE + virtual bool IsClosed_Implementation() const { return bClosed; } + + /** + * Set whether the curve is closed or not. + * @param bInClosed The new closed setting for the curve: set to true if the curve is closed. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs | Input Objects") + void SetClosed(const bool bInClosed); + FORCEINLINE + virtual void SetClosed_Implementation(const bool bInClosed) { bClosed = bInClosed; } + + /** Returns true if the curve is reversed. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs | Input Objects") + bool IsReversed() const; + FORCEINLINE + virtual bool IsReversed_Implementation() const { return bReversed; } + + /** + * Set whether the curve is reversed or not. + * @param bInReversed The new reversed setting for the curve: set to true if the curve is reversed. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs | Input Objects") + void SetReversed(const bool bInReversed); + FORCEINLINE + virtual void SetReversed_Implementation(const bool bInReversed) { bReversed = bInReversed; } + + /** Returns the curve type (for example: polygon, nurbs, bezier) */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs | Input Objects") + EHoudiniPublicAPICurveType GetCurveType() const; + FORCEINLINE + virtual EHoudiniPublicAPICurveType GetCurveType_Implementation() const { return CurveType; } + + /** + * Set the curve type (for example: polygon, nurbs, bezier). + * @param InCurveType The new curve type. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs | Input Objects") + void SetCurveType(const EHoudiniPublicAPICurveType InCurveType); + FORCEINLINE + virtual void SetCurveType_Implementation(const EHoudiniPublicAPICurveType InCurveType) { CurveType = InCurveType; } + + /** Get the curve method, for example CVs, or freehand. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs | Input Objects") + EHoudiniPublicAPICurveMethod GetCurveMethod() const; + FORCEINLINE + virtual EHoudiniPublicAPICurveMethod GetCurveMethod_Implementation() const { return CurveMethod; } + + /** + * Set the curve method, for example CVs, or freehand. + * @param InCurveMethod The new curve method. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs | Input Objects") + void SetCurveMethod(const EHoudiniPublicAPICurveMethod InCurveMethod); + FORCEINLINE + virtual void SetCurveMethod_Implementation(const EHoudiniPublicAPICurveMethod InCurveMethod) { CurveMethod = InCurveMethod; } + + /** Get the curve breakpoint parameterization, for example Uniform, or Chord. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs | Input Objects") + EHoudiniPublicAPICurveBreakpointParameterization GetCurveBreakpointParameterization() const; + FORCEINLINE + virtual EHoudiniPublicAPICurveBreakpointParameterization GetCurveBreakpointParameterization_Implementation() const { return CurveBreakpointParameterization; } + + /** + * Set the curve breakpoint parameterization, for example Uniform, or Chord. + * @param InCurveBreakpointParameterization The new curve method. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Inputs | Input Objects") + void SetCurveBreakpointParameterization(const EHoudiniPublicAPICurveBreakpointParameterization InCurveBreakpointParameterization); + FORCEINLINE + virtual void SetCurveBreakpointParameterization_Implementation(const EHoudiniPublicAPICurveBreakpointParameterization InCurveBreakpointParameterization) { CurveBreakpointParameterization = InCurveBreakpointParameterization; } + + /** + * Populate this wrapper from a UHoudiniSplineComponent. + * @param InSpline The spline to populate this wrapper from. + */ + void PopulateFromHoudiniSplineComponent(UHoudiniSplineComponent const* const InSpline); + + /** + * Copies the curve data to a UHoudiniSplineComponent. + * @param InSpline The spline to copy to. + */ + void CopyToHoudiniSplineComponent(UHoudiniSplineComponent* const InSpline) const; + +protected: + /** The control points of the curve. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Houdini Engine | Public API | Inputs | Input Objects") + TArray CurvePoints; + + /** Whether the curve is closed (true) or not. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Houdini Engine | Public API | Inputs | Input Objects") + bool bClosed; + + /** Whether the curve is reversed (true) or not. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Houdini Engine | Public API | Inputs | Input Objects") + bool bReversed; + + /** The curve type (for example: polygon, nurbs, bezier). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Houdini Engine | Public API | Inputs | Input Objects") + EHoudiniPublicAPICurveType CurveType; + + /** The curve method, for example CVs, or freehand. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Houdini Engine | Public API | Inputs | Input Objects") + EHoudiniPublicAPICurveMethod CurveMethod; + + /** The curve breakpoint parameterization, for example CVs, or freehand. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Houdini Engine | Public API | Inputs | Input Objects") + EHoudiniPublicAPICurveBreakpointParameterization CurveBreakpointParameterization; + + /** Helper function for converting EHoudiniPublicAPICurveType to EHoudiniCurveType */ + static EHoudiniCurveType ToHoudiniCurveType(const EHoudiniPublicAPICurveType InCurveType); + + /** Helper function for converting EHoudiniPublicAPICurveMethod to EHoudiniCurveMethod */ + static EHoudiniCurveMethod ToHoudiniCurveMethod(const EHoudiniPublicAPICurveMethod InCurveMethod); + + /** Helper function for converting EHoudiniPublicAPICurveBreakpointParameterization to EHoudiniCurveBreakpointParameterization */ + static EHoudiniCurveBreakpointParameterization ToHoudiniCurveBreakpointParamterization(const EHoudiniPublicAPICurveBreakpointParameterization InCurveBreakpointParameterization); + + /** Helper function for converting EHoudiniCurveType to EHoudiniPublicAPICurveType */ + static EHoudiniPublicAPICurveType ToHoudiniPublicAPICurveType(const EHoudiniCurveType InCurveType); + + /** Helper function for converting EHoudiniCurveMethod to EHoudiniPublicAPICurveMethod */ + static EHoudiniPublicAPICurveMethod ToHoudiniPublicAPICurveMethod(const EHoudiniCurveMethod InCurveMethod); + + /** Helper function for converting EHoudiniCurveBreakpointParameterization to EHoudiniPublicAPIBreakpointParameterization */ + static EHoudiniPublicAPICurveBreakpointParameterization ToHoudiniPublicAPICurveBreakpointParameterization(const EHoudiniCurveBreakpointParameterization InCurveBreakpointParameterization); +}; + +/** + * API wrapper input class for curve inputs. Derived from UHoudiniPublicAPIInput. + */ +UCLASS(BlueprintType, Category="Houdini Engine | Public API | Inputs") +class HOUDINIENGINEEDITOR_API UHoudiniPublicAPICurveInput : public UHoudiniPublicAPIInput +{ + GENERATED_BODY() + +public: + UHoudiniPublicAPICurveInput(); + + /** Indicates that if trigger cook automatically on curve Input spline modified */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bCookOnCurveChanged; + + /** Set this to true to add rot and scale attributes on curve inputs. */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bAddRotAndScaleAttributesOnCurves; + + /** Set this to true to use legacy input curves (i.e. curve::1.0). */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bUseLegacyInputCurves; + + virtual bool IsAcceptableObjectForInput_Implementation(UObject* InObject) const override; + + virtual bool PopulateFromHoudiniInput(UHoudiniInput const* const InInput) override; + + virtual bool UpdateHoudiniInput(UHoudiniInput* const InInput) const override; + +protected: + virtual EHoudiniInputType GetInputType() const override { return EHoudiniInputType::Curve; } + + virtual UObject* ConvertInternalInputObject(UObject* InInternalInputObject) override; + + virtual UObject* ConvertAPIInputObjectAndAssignToInput(UObject* InAPIInputObject, UHoudiniInput* InHoudiniInput, const int32 InInputIndex) const override; +}; + + +/** + * API wrapper input class for world inputs. Derived from UHoudiniPublicAPIGeoInput. + */ +UCLASS(BlueprintType, Category="Houdini Engine | Public API | Inputs") +class HOUDINIENGINEEDITOR_API UHoudiniPublicAPIWorldInput : public UHoudiniPublicAPIGeoInput +{ + GENERATED_BODY() + +public: + UHoudiniPublicAPIWorldInput(); + + /** Objects used for automatic bound selection */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + TArray> WorldInputBoundSelectorObjects; + + /** Indicates that this world input is in "BoundSelector" mode */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bIsWorldInputBoundSelector; + + /** Indicates that selected actors by the bound selectors should update automatically */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bWorldInputBoundSelectorAutoUpdate; + + /** Indicates that selected actors by the bound selectors should update automatically */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bWorldInputAutoUpdate; + + /** Resolution used when converting unreal splines to houdini curves */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + float UnrealSplineResolution; + + /** + * If enabled, level instances (and packed level actor) content is exported vs just exporting a single point + * with attributes identifying the level instance / packed level actor. + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bExportLevelInstanceContent; + + /** + * If enabled, HDA used as inputs objects will be directly connected to this HDAs input in Houdini + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bDirectlyConnectHdas; + + /** + * If enabled, Edit Layers data will be exported with Landscapes. + * If Edit Layers are not needed, disabling this will improve performance + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Houdini Engine | Public API | Inputs") + bool bExportHeightDataPerEditLayer; + + /** + * Paint Layer export. + */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Houdini Engine | Public API | Inputs") + bool bExportPaintLayersPerEditLayer; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Houdini Engine | Public API | Inputs") + bool bExportMergedPaintLayers; + + /** + * Setter for world input object array. If this is a bounds selector (#bIsWorldInputBoundSelector is true), then + * this function always returns false (and sets nothing), in that case only #WorldInputBoundSelectorObjects can be + * modified. + * @param InObjects The world input objects. + * @return true if all objects in InObjects could be set as world input objects, false otherwise. Always false + * if #bIsWorldInputBoundSelector is true. + */ + virtual bool SetInputObjects_Implementation(const TArray& InObjects) override; + + virtual bool SupportsTransformOffset() const override { return false; } + + virtual bool PopulateFromHoudiniInput(UHoudiniInput const* const InInput) override; + + virtual bool UpdateHoudiniInput(UHoudiniInput* const InInput) const override; + +protected: + virtual EHoudiniInputType GetInputType() const override { return EHoudiniInputType::World; } + +}; + +``` + +`Source/HoudiniEngineEditor/Public/HoudiniPublicAPIObjectBase.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "HoudiniPublicAPIObjectBase.generated.h" + +/** An enum with values that determine if API errors are logged. */ +UENUM(BlueprintType) +enum class EHoudiniPublicAPIErrorLogOption : uint8 +{ + Invalid = 0, + Auto = 1, + Log = 2, + NoLog = 3, +}; + +/** + * Base class for API UObjects. Implements error logging: record and get a error messages for Houdini Public API objects. + */ +UCLASS() +class HOUDINIENGINEEDITOR_API UHoudiniPublicAPIObjectBase : public UObject +{ + GENERATED_BODY() + +public: + UHoudiniPublicAPIObjectBase(); + + /** + * Gets the last error message recorded. + * @param OutLastErrorMessage Set to the last error message recorded, or the empty string if there are no errors + * messages. + * @return true if there was an error message to set. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Errors") + bool GetLastErrorMessage(FString& OutLastErrorMessage) const; + + /** Clear any error messages that have been set. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Errors") + void ClearErrorMessages(); + + /** + * Returns whether or not API errors are written to the log. + * @return true if API errors are logged as warnings, false if API errors are not logged. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Errors") + bool IsLoggingErrors() const; + FORCEINLINE + virtual bool IsLoggingErrors_Implementation() const { return bIsLoggingErrors; } + + /** + * Sets whether or not API errors are written to the log. + * @param bInEnabled True if API errors should be logged as warnings, false if API errors should not logged. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Errors") + void SetLoggingErrorsEnabled(const bool bInEnabled); + FORCEINLINE + virtual void SetLoggingErrorsEnabled_Implementation(const bool bInEnabled) { bIsLoggingErrors = bInEnabled; } + +protected: + /** + * Set an error message. This is recorded as the current/last error message. + * @param InErrorMessage The error message to set. + * @param InLoggingOption Determines the behavior around logging the error message. If + * EHoudiniPublicAPIErrorLogOption.Invalid or EHoudiniPublicAPIErrorLogOption.Auto then IsLoggingErrors() is used to + * determine if the error message should be logged. If EHoudiniPublicAPIErrorLogOption.Log, then the error message + * is logged as a warning. If EHoudiniPublicAPIErrorLogOption.NoLog then the error message is not logged. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Houdini Engine | Public API | Errors") + void SetErrorMessage( + const FString& InErrorMessage, + const EHoudiniPublicAPIErrorLogOption InLoggingOption=EHoudiniPublicAPIErrorLogOption::Auto) const; + + /** The last error message that was set. */ + UPROPERTY() + mutable FString LastErrorMessage; + + /** True if an errors have been set and not yet cleared. */ + UPROPERTY() + mutable bool bHasError; + + /** If true, API errors logged with SetErrorMessage are written to the log as warnings by default. */ + UPROPERTY() + bool bIsLoggingErrors; +}; + +``` + +`Source/HoudiniEngineEditor/Public/HoudiniPublicAPIOutputTypes.h`: + +```h +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#include "HoudiniEngineRuntimeCommon.h" + +#include "HoudiniPublicAPIOutputTypes.generated.h" + + +struct FHoudiniOutputObjectIdentifier; + +/** + * This class represents an output object identifier for an output object of a wrapped Houdini asset in the + * public API. + */ +USTRUCT(BlueprintType, Category="Houdini Engine | Public API | Outputs") +struct FHoudiniPublicAPIOutputObjectIdentifier +{ + GENERATED_BODY() + +public: + FHoudiniPublicAPIOutputObjectIdentifier(); + + FHoudiniPublicAPIOutputObjectIdentifier(const FHoudiniOutputObjectIdentifier& InIdentifier); + + /** Returns the internal output object identifier wrapped by this class. */ + FHoudiniOutputObjectIdentifier GetIdentifier() const; + + /** + * Sets the internal output object identifier wrapped by this class. + * @param InIdentifier The internal output object identifier. + */ + void SetIdentifier(const FHoudiniOutputObjectIdentifier& InIdentifier); + + /** String identifier for the split that created the output object identified by this identifier. */ + UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category="Houdini Engine | Public API | Outputs") + FString SplitIdentifier; + + /** Name of the part used to generate the output object identified by this identifier. */ + UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category="Houdini Engine | Public API | Outputs") + FString PartName; + +protected: + + // NodeId of corresponding Houdini Object. + UPROPERTY() + int32 ObjectId = -1; + + // NodeId of corresponding Houdini Geo. + UPROPERTY() + int32 GeoId = -1; + + // PartId + UPROPERTY() + int32 PartId = -1; + + // First valid primitive index for this output + // (used to read generic attributes) + UPROPERTY() + int32 PrimitiveIndex = -1; + + // First valid point index for this output + // (used to read generic attributes) + UPROPERTY() + int32 PointIndex = -1; + + bool bLoaded = false; +}; + +``` + +`Source/HoudiniEngineEditor/Public/HoudiniPublicAPIProcessHDANode.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#include "Kismet/BlueprintAsyncActionBase.h" + +#include "HoudiniPublicAPIAssetWrapper.h" + +#include "HoudiniPublicAPIProcessHDANode.generated.h" + + +class UHoudiniPublicAPIInput; +class UHoudiniAsset; + +// Delegate type for output pins on the node. +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnProcessHDANodeOutputPinDelegate, UHoudiniPublicAPIAssetWrapper*, AssetWrapper, const bool, bCookSuccess, const bool, bBakeSuccess); + +/** + * A Blueprint async node for instantiating and cooking/processing an HDA, with delegate output pins for the + * various phases/state changes of the HDA. + */ +UCLASS() +class HOUDINIENGINEEDITOR_API UHoudiniPublicAPIProcessHDANode : public UBlueprintAsyncActionBase +{ + GENERATED_BODY() + +public: + UHoudiniPublicAPIProcessHDANode(const FObjectInitializer& ObjectInitializer); + + /** + * Instantiates an HDA in the specified world/level. Sets parameters and inputs supplied in InParameters, + * InNodeInputs and InParameterInputs. If bInEnableAutoCook is true, cooks the HDA. If bInEnableAutoBake is + * true, bakes the cooked outputs according to the supplied baking parameters. + * This all happens asynchronously, with the various output pins firing at the various points in the process: + * - PreInstantiation: before the HDA is instantiated, a good place to set parameter values before the first cook. + * - PostInstantiation: after the HDA is instantiated, a good place to set/configure inputs before the first cook. + * - PostAutoCook: right after a cook + * - PreProcess: after a cook but before output objects have been created/processed + * - PostProcessing: after output objects have been created + * - PostAutoBake: after outputs have been baked + * - Completed: upon successful completion (could be PostInstantiation if auto cook is disabled, PostProcessing + * if auto bake is disabled, or after PostAutoBake if auto bake is enabled. + * - Failed: If the process failed at any point. + * @param InHoudiniAsset The HDA to instantiate. + * @param InInstantiateAt The Transform to instantiate the HDA with. + * @param InParameters The parameter values to set before cooking the instantiated HDA. + * @param InNodeInputs The node inputs to set before cooking the instantiated HDA. + * @param InParameterInputs The parameter-based inputs to set before cooking the instantiated HDA. + * @param InWorldContextObject A world context object for identifying the world to spawn in, if + * InSpawnInLevelOverride is null. + * @param InSpawnInLevelOverride If not nullptr, then the AHoudiniAssetActor is spawned in that level. If both + * InSpawnInLevelOverride and InWorldContextObject are null, then the actor is spawned in the current editor + * context world's current level. + * @param bInEnableAutoCook If true (the default) the HDA will cook automatically after instantiation and after + * parameter, transform and input changes. + * @param bInEnableAutoBake If true, the HDA output is automatically baked after a cook. Defaults to false. + * @param InBakeDirectoryPath The directory to bake to if the bake path is not set via attributes on the HDA output. + * @param InBakeMethod The bake target (to actor vs blueprint). @see EHoudiniEngineBakeOption. + * @param bInRemoveOutputAfterBake If true, HDA temporary outputs are removed after a bake. Defaults to false. + * @param bInRecenterBakedActors Recenter the baked actors to their bounding box center. Defaults to false. + * @param bInReplacePreviousBake If true, on every bake replace the previous bake's output (assets + actors) with + * the new bake's output. Defaults to false. + * @param bInDeleteInstantiatedAssetOnCompletionOrFailure If true, deletes the instantiated asset actor on + * completion or failure. Defaults to false. + * @return The blueprint async node. + */ + UFUNCTION(BlueprintCallable, meta=(AdvancedDisplay=5,AutoCreateRefTerm="InInstantiateAt,InParameters,InNodeInputs,InParameterInputs",BlueprintInternalUseOnly="true", WorldContext="WorldContextObject"), Category="Houdini|Public API") + static UHoudiniPublicAPIProcessHDANode* ProcessHDA( + UHoudiniAsset* InHoudiniAsset, + const FTransform& InInstantiateAt, + const TMap& InParameters, + const TMap& InNodeInputs, + const TMap& InParameterInputs, + UObject* InWorldContextObject=nullptr, + ULevel* InSpawnInLevelOverride=nullptr, + const bool bInEnableAutoCook=true, + const bool bInEnableAutoBake=false, + const FString& InBakeDirectoryPath="", + const EHoudiniEngineBakeOption InBakeMethod=EHoudiniEngineBakeOption::ToActor, + const bool bInRemoveOutputAfterBake=false, + const bool bInRecenterBakedActors=false, + const bool bInReplacePreviousBake=false, + const bool bInDeleteInstantiatedAssetOnCompletionOrFailure=false); + + virtual void Activate() override; + + /** + * Delegate that is broadcast when entering the PreInstantiation state: the HDA's default parameter definitions are + * available, but the node has not yet been instantiated in HAPI/Houdini Engine + */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnProcessHDANodeOutputPinDelegate PreInstantiation; + + /** Delegate that is broadcast after the asset was successfully instantiated */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnProcessHDANodeOutputPinDelegate PostInstantiation; + + /** + * Delegate that is broadcast after a cook completes, but before outputs have been created. This will not be + * broadcast from this node if bInAutoCookEnabled is false. + */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnProcessHDANodeOutputPinDelegate PostAutoCook; + + /** + * Delegate that is broadcast just after PostCook (after parameters have been updated) but before creating + * output objects. This will not be broadcast from this node if bInAutoCookEnabled is false. + */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnProcessHDANodeOutputPinDelegate PreProcess; + + /** + * Delegate that is broadcast after processing HDA outputs after a cook, the output objects have been created. + * This will not be broadcast from this node if bInAutoCookEnabled is false. + */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnProcessHDANodeOutputPinDelegate PostProcessing; + + /** + * Delegate that is broadcast after auto-baking the asset (not called for individual outputs that are baked to the + * content browser). This will not be broadcast from this node if bInAutoBake or bInAutoCookEnabled is false. + */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnProcessHDANodeOutputPinDelegate PostAutoBake; + + /** + * Deletate that is broadcast on completion of async processing of the instantiated asset by this node. + * After this broadcast, the instantiated asset will be deleted if bInDeleteInstantiatedAssetOnCompletion=true + * was set on creation. + */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnProcessHDANodeOutputPinDelegate Completed; + + /** Deletate that is broadcast if we fail during activation of the node. */ + UPROPERTY(BlueprintAssignable, Category="Houdini|Public API") + FOnProcessHDANodeOutputPinDelegate Failed; + +protected: + + // Output pin data + + /** The asset wrapper for the instantiated HDA processed by this node. */ + UPROPERTY() + TObjectPtr AssetWrapper; + + /** True if the last cook was successful. */ + UPROPERTY() + bool bCookSuccess; + + /** True if the last bake was successful. */ + UPROPERTY() + bool bBakeSuccess; + + // End: Output pin data + + /** The HDA to instantiate. */ + UPROPERTY() + TObjectPtr HoudiniAsset; + + /** The transform the instantiate the asset with. */ + UPROPERTY() + FTransform InstantiateAt; + + /** The parameters to set on #PreInstantiation */ + UPROPERTY() + TMap Parameters; + + /** The node inputs to set on #PostInstantiation */ + UPROPERTY() + TMap> NodeInputs; + + /** The object path parameter inputs to set on #PostInstantiation */ + UPROPERTY() + TMap> ParameterInputs; + + /** The world context object: spawn in this world if #SpawnInLevelOverride is not set. */ + UPROPERTY() + TObjectPtr WorldContextObject; + + /** The level to spawn in. If both this and #WorldContextObject is not set, spawn in the editor context's level. */ + UPROPERTY() + TObjectPtr SpawnInLevelOverride; + + /** Whether to set the instantiated asset to auto cook. */ + UPROPERTY() + bool bEnableAutoCook; + + /** Whether to set the instantiated asset to auto bake after a cook. */ + UPROPERTY() + bool bEnableAutoBake; + + /** Set the fallback bake directory, for if output attributes do not specify it. */ + UPROPERTY() + FString BakeDirectoryPath; + + /** The bake method/target: for example, to actors vs to blueprints. */ + UPROPERTY() + EHoudiniEngineBakeOption BakeMethod; + + /** Remove temporary HDA output after a bake. */ + UPROPERTY() + bool bRemoveOutputAfterBake; + + /** Recenter the baked actors at their bounding box center. */ + UPROPERTY() + bool bRecenterBakedActors; + + /** + * Replace previous bake output on each bake. For the purposes of this node, this would mostly apply to .uassets and + * not actors. + */ + UPROPERTY() + bool bReplacePreviousBake; + + /** Whether or not to delete the instantiated asset after Complete is called. */ + UPROPERTY() + bool bDeleteInstantiatedAssetOnCompletionOrFailure; + + /** Unbind all delegates */ + void UnbindDelegates(); + + /** Broadcast Failure and removes the node from the root set. */ + UFUNCTION() + virtual void HandleFailure(); + + /** Broadcast Complete and removes the node from the root set. */ + UFUNCTION() + virtual void HandleComplete(); + + /** + * Bound to the asset wrapper's pre-instantiation delegate. Sets the HDAs parameters from #Parameters and + * broadcasts #PreInstantiation. + */ + UFUNCTION() + virtual void HandlePreInstantiation(UHoudiniPublicAPIAssetWrapper* InAssetWrapper); + + /** + * Bound to the asset wrapper's post-instantiation delegate. Sets the HDAs inputs from #NodeInputs and + * #ParameterInputs and broadcasts #PostInstantiation. + */ + UFUNCTION() + virtual void HandlePostInstantiation(UHoudiniPublicAPIAssetWrapper* InAssetWrapper); + + /** Bound to the asset wrapper's post-cook delegate. Broadcasts #PostAutoCook. */ + UFUNCTION() + virtual void HandlePostAutoCook(UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const bool bInCookSuccess); + + /** Bound to the asset wrapper's pre-processing delegate. Broadcasts #PreProcess. */ + UFUNCTION() + virtual void HandlePreProcess(UHoudiniPublicAPIAssetWrapper* InAssetWrapper); + + /** Bound to the asset wrapper's post-processing delegate. Broadcasts #PostProcessing. */ + UFUNCTION() + virtual void HandlePostProcessing(UHoudiniPublicAPIAssetWrapper* InAssetWrapper); + + /** Bound to the asset wrapper's post-bake delegate. Broadcasts #PostAutoBake. */ + UFUNCTION() + virtual void HandlePostAutoBake(UHoudiniPublicAPIAssetWrapper* InAssetWrapper, const bool bInBakeSuccess); +}; + +``` + +`Source/HoudiniEngineEditor/Public/IHoudiniEngineEditor.h`: + +```h +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "Modules/ModuleInterface.h" + +class IHoudiniEngineEditor : public IModuleInterface +{ + public: + /** Register and unregister component visualizers used by this module. **/ + virtual void RegisterComponentVisualizers() {} + virtual void UnregisterComponentVisualizers() {} + + /** Register and unregister detail presenters used by this module. **/ + virtual void RegisterDetails() {} + virtual void UnregisterDetails() {} + + /** Register and unregister asset type actions. **/ + virtual void RegisterAssetTypeActions() {} + virtual void UnregisterAssetTypeActions() {} + + /** Create and register / unregister asset brokers. **/ + virtual void RegisterAssetBrokers() {} + virtual void UnregisterAssetBrokers() {} + + /** Create and register actor factories. **/ + virtual void RegisterActorFactories() {} + + /** Extend menu. **/ + virtual void ExtendMenu() {} + + /** Register and unregister thumbnails. **/ + virtual void RegisterThumbnails() {} + virtual void UnregisterThumbnails() {} + + /** Register and unregister for undo/redo notifications. **/ + virtual void RegisterForUndo() {} + virtual void UnregisterForUndo() {} + + /** Create custom modes **/ + virtual void RegisterModes() {} + virtual void UnregisterModes() {} + + /** Create custom placement extensions */ + virtual void RegisterPlacementModeExtensions() {} + virtual void UnregisterPlacementModeExtensions() {} + + /** Custom Tabs */ + virtual void RegisterEditorTabs() {} + virtual void UnRegisterEditorTabs() {} +}; + +``` + +`Source/HoudiniEngineRuntime/HoudiniEngineRuntime.Build.cs`: + +```cs +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +using UnrealBuildTool; +using System; +using System.IO; + +public class HoudiniEngineRuntime : ModuleRules +{ + private int HOUDINI_USE_PCG = 1; + + + public HoudiniEngineRuntime( ReadOnlyTargetRules Target ) : base( Target ) + { + bPrecompile = true; + PCHUsage = PCHUsageMode.NoSharedPCHs; + PrivatePCHHeaderFile = "Private/HoudiniEngineRuntimePrivatePCH.h"; + PrecompileForTargets = PrecompileTargetsType.Any; + + // Check if we are compiling for unsupported platforms. + if ( Target.Platform != UnrealTargetPlatform.Win64 && + Target.Platform != UnrealTargetPlatform.Mac && + Target.Platform != UnrealTargetPlatform.Linux /*&& + Target.Platform != UnrealTargetPlatform.Switch */) + { + System.Console.WriteLine( string.Format( "Houdini Engine Runtime: Compiling for untested target platform. Please let us know how it goes!" ) ); + } + + + PublicIncludePaths.AddRange( + new string[] {} + ); + + PrivateIncludePaths.AddRange( + new string[] { } + ); + + // Add common dependencies. + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "RenderCore", + "ImageCore", + "InputCore", + "RHI", + "Foliage", + "Landscape", + "MeshUtilitiesCommon", + "Chaos", + "GeometryCollectionEngine" + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "Landscape", + "PhysicsCore", + "DeveloperSettings", + "Slate", + "SlateCore" + } + ); + + if (HOUDINI_USE_PCG == 1) + { + PrivateDependencyModuleNames.AddRange( + new string[] + { + "PCG" + } + ); + PrivateDefinitions.Add("HOUDINI_USE_PCG=1"); + } + + if (Target.bBuildEditor == true) + { + PrivateDependencyModuleNames.AddRange( + new string[] + { + "UnrealEd", + "Kismet", + "EditorFramework", + "SubobjectEditor", + "Json" + } + ); + } + } +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniAsset.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniAsset.h" + +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniPluginSerializationVersion.h" +#include "HoudiniToolsPackageAsset.h" +#include "HoudiniToolsRuntimeUtils.h" + +#include "Misc/Paths.h" +#include "HAL/UnrealMemory.h" +#include "UObject/ObjectSaveContext.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 +#include "UObject/AssetRegistryTagsContext.h" +#endif + +UHoudiniAsset::UHoudiniAsset(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) + , AssetFileName(TEXT("")) + , AssetBytesCount(0) + , bAssetLimitedCommercial(false) + , bAssetNonCommercial(false) + , bAssetExpanded(false) +{} + +void +UHoudiniAsset::CreateAsset(const uint8 * BufferStart, const uint8 * BufferEnd, const FString & InFileName) +{ + AssetFileName = InFileName; + + // Calculate buffer size. + AssetBytesCount = BufferEnd - BufferStart; + + if (AssetBytesCount) + { + // Allocate buffer to store the raw data. + AssetBytes.SetNumUninitialized(AssetBytesCount); + // Copy data into the newly allocated buffer. + FMemory::Memcpy(AssetBytes.GetData(), BufferStart, AssetBytesCount); + } + + FString FileExtension = FPaths::GetExtension(InFileName); + + // Expanded HDAs are imported via a "houdini.hdalibrary" file inside the .hda directory + // Identify them first, then update the file path to point to the .hda dir + if (FileExtension.Equals(TEXT("hdalibrary"), ESearchCase::IgnoreCase)) + { + bAssetExpanded = true; + + // Use the parent ".hda" directory as the filename + AssetFileName = FPaths::GetPath(AssetFileName); + FileExtension = FPaths::GetExtension(AssetFileName); + } + + if (FileExtension.Equals(TEXT("hdalc"), ESearchCase::IgnoreCase) + || FileExtension.Equals(TEXT("otlc"), ESearchCase::IgnoreCase)) + { + // Check if the HDA is limited (Indie) ... + bAssetLimitedCommercial = true; + } + else if (FileExtension.Equals(TEXT("hdanc"), ESearchCase::IgnoreCase) + || FileExtension.Equals(TEXT("otlnc"), ESearchCase::IgnoreCase)) + { + // ... or non commercial (Apprentice) + bAssetNonCommercial = true; + } +} + +void +UHoudiniAsset::FinishDestroy() +{ + // Release buffer which was used to store raw OTL data. + AssetBytes.Empty(); + Super::FinishDestroy(); +} + +const uint8 * +UHoudiniAsset::GetAssetBytes() const +{ + return AssetBytes.GetData(); +} + +const FString & +UHoudiniAsset::GetAssetFileName() const +{ + return AssetFileName; +} + +uint32 +UHoudiniAsset::GetAssetBytesCount() const +{ + return AssetBytesCount; +} + +void +UHoudiniAsset::Serialize(FArchive & Ar) +{ + // Serializes our UProperties + Super::Serialize(Ar); + Ar.UsingCustomVersion(FHoudiniCustomSerializationVersion::GUID); + + // Get the version + uint32 HoudiniAssetVersion = Ar.CustomVer(FHoudiniCustomSerializationVersion::GUID); + + // Only version 1 assets needs manual serialization + if ( HoudiniAssetVersion < VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_V2_BASE + || HoudiniAssetVersion > VER_HOUDINI_PLUGIN_SERIALIZATION_AUTOMATIC_VERSION ) + return SerializeLegacy(Ar); +} + +void +UHoudiniAsset::SerializeLegacy(FArchive & Ar) +{ + uint32 FileFormatVersion; + Ar << FileFormatVersion; + + Ar << AssetBytesCount; + if (Ar.IsLoading()) + { + // Allocate sufficient space to read stored raw OTL data. + AssetBytes.SetNumUninitialized(AssetBytesCount); + } + + if (AssetBytesCount) + Ar.Serialize(AssetBytes.GetData(), AssetBytesCount); + + // Serialized flags. + union + { + struct + { + uint32 bLegacyPreviewHoudiniLogo : 1; + uint32 bLegacyAssetLimitedCommercial : 1; + uint32 bLegacyAssetNonCommercial : 1; + }; + uint32 HoudiniAssetFlagsPacked; + }; + Ar << HoudiniAssetFlagsPacked; + + bAssetNonCommercial = bLegacyAssetNonCommercial; + bAssetLimitedCommercial = bLegacyAssetLimitedCommercial; + + // Serialize asset file path. + Ar << AssetFileName; +} + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 +void +UHoudiniAsset::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const +{ + + Context.AddTag(FAssetRegistryTag("FileName", AssetFileName, FAssetRegistryTag::TT_Alphabetical)); + + // Bytes + Context.AddTag(FAssetRegistryTag("Bytes", FString::FromInt(AssetBytesCount), FAssetRegistryTag::TT_Numerical)); + + // Indicate if the Asset is Full / Indie / NC + FString AssetType = TEXT("Full"); + if (bAssetLimitedCommercial) + AssetType = TEXT("Limited Commercial (LC)"); + else if (bAssetNonCommercial) + AssetType = TEXT("Non Commercial (NC)"); + + Context.AddTag(FAssetRegistryTag("Asset Type", AssetType, FAssetRegistryTag::TT_Alphabetical)); + Super::GetAssetRegistryTags(Context); +} +#endif + +void +UHoudiniAsset::GetAssetRegistryTags(TArray& OutTags) const +{ + // Filename + OutTags.Add(FAssetRegistryTag("FileName", AssetFileName, FAssetRegistryTag::TT_Alphabetical)); + + // Bytes + OutTags.Add(FAssetRegistryTag("Bytes", FString::FromInt(AssetBytesCount), FAssetRegistryTag::TT_Numerical)); + + // Indicate if the Asset is Full / Indie / NC + FString AssetType = TEXT("Full"); + if (bAssetLimitedCommercial) + AssetType = TEXT("Limited Commercial (LC)"); + else if (bAssetNonCommercial) + AssetType = TEXT("Non Commercial (NC)"); + + OutTags.Add(FAssetRegistryTag("Asset Type", AssetType, FAssetRegistryTag::TT_Alphabetical)); + + PRAGMA_DISABLE_DEPRECATION_WARNINGS; + Super::GetAssetRegistryTags(OutTags); + PRAGMA_ENABLE_DEPRECATION_WARNINGS; +} + +bool +UHoudiniAsset::IsAssetLimitedCommercial() const +{ + return bAssetLimitedCommercial; +} + +bool +UHoudiniAsset::IsAssetNonCommercial() const +{ + return bAssetNonCommercial; +} + +bool +UHoudiniAsset::IsExpandedHDA() const +{ + return bAssetExpanded; +} + +#if WITH_EDITOR +void +UHoudiniAsset::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + UObject::PostEditChangeProperty(PropertyChangedEvent); + + if (FModuleManager::Get().IsModuleLoaded("HoudiniEngineRuntime")) + { + const FHoudiniEngineRuntime& HoudiniEngineRuntime = FModuleManager::GetModuleChecked("HoudiniEngineRuntime"); + HoudiniEngineRuntime.BroadcastToolOrPackageChanged(); + } +} +#endif + +void +UHoudiniAsset::PostSaveRoot(FObjectPostSaveRootContext ObjectSaveContext) +{ + UObject::PostSaveRoot(ObjectSaveContext); +#if WITH_EDITOR + // Find the owning package, if it exists. + UHoudiniToolsPackageAsset* OwningPackage = FHoudiniToolsRuntimeUtils::FindOwningToolsPackage(this); + if (IsValid(OwningPackage) && OwningPackage->bExportToolsDescription) + { + // If the owning package allows description exporting, export it now. + FString JSONFilePath; + FHoudiniToolsRuntimeUtils::GetHoudiniAssetJSONPath(this, JSONFilePath); + FHoudiniToolsRuntimeUtils::WriteJSONFromHoudiniAsset(this, JSONFilePath); + } +#endif +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniAssetActor.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniNodeSyncComponent.h" +#include "HoudiniAsset.h" +#include "HoudiniPDGAssetLink.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +AHoudiniAssetActor::AHoudiniAssetActor(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) +{ + SetCanBeDamaged(false); + //PrimaryActorTick.bCanEverTick = true; + //PrimaryActorTick.bStartWithTickEnabled = true; + + bool bUseCookable = true; + if (!bUseCookable) + { + // Create Houdini component and attach it to a root component. + HoudiniAssetComponent = + ObjectInitializer.CreateDefaultSubobject(this, TEXT("HoudiniAssetComponent")); + + HoudiniCookable = nullptr; + } + else + { + HoudiniCookable = + ObjectInitializer.CreateDefaultSubobject(this, TEXT("HoudiniCookable")); + + // Create Houdini component with the Cookable as outer + HoudiniAssetComponent = + ObjectInitializer.CreateDefaultSubobject(HoudiniCookable, TEXT("HoudiniAssetCookableComponent")); + + if (HoudiniCookable) + { + // HoudiniAssetActor support all cookable features + HoudiniCookable->SetHoudiniAssetSupported(true); + HoudiniCookable->SetParameterSupported(true); + HoudiniCookable->SetInputSupported(true); + HoudiniCookable->SetOutputSupported(true); + HoudiniCookable->SetComponentSupported(true); + HoudiniCookable->SetPDGSupported(true); + HoudiniCookable->SetBakingSupported(true); + HoudiniCookable->SetProxySupported(true); + + // Assign the HAC to the Cookable + HoudiniCookable->SetComponent(HoudiniAssetComponent); + } + } + + //HoudiniAssetComponent->SetCollisionProfileName(UCollisionProfile::BlockAll_ProfileName); + RootComponent = HoudiniAssetComponent; +} + + +void +AHoudiniAssetActor::SetNodeSyncActor(bool bNodeSyncActor) +{ + if (IsNodeSyncActor() == bNodeSyncActor) + return; + + // TODO: COOKABLE CHECK ME! + if (bNodeSyncActor) + { + // Already a nodesync component + if (HoudiniAssetComponent->IsA()) + return; + } + else + { + // Already NOT a nodesync component + if (!HoudiniAssetComponent->IsA()) + return; + } + + // Remove and destroy the existing component + RemoveInstanceComponent(HoudiniAssetComponent); + HoudiniAssetComponent->DestroyComponent(); + + // Get our cookable as it will be the outer + UHoudiniCookable* MyCookable = GetHoudiniCookable(); + if (bNodeSyncActor) + { + // Create a new NodeSyncComponent to replace it + HoudiniAssetComponent = NewObject(MyCookable); + + // Update our cookable to reflect node sync feature support + MyCookable->SetHoudiniAssetSupported(false); + MyCookable->SetInputSupported(false); + MyCookable->SetParameterSupported(false); + MyCookable->SetPDGSupported(false); + //MyCookable->SetCanDeleteHoudiniNodes(false); + } + else + { + // Create a new HoudiniAssetComponent to replace it + HoudiniAssetComponent = NewObject(MyCookable); + + // Re-enable disabled node sync features + MyCookable->SetHoudiniAssetSupported(true); + MyCookable->SetInputSupported(true); + MyCookable->SetParameterSupported(true); + MyCookable->SetPDGSupported(true); + //MyCookable->SetCanDeleteHoudiniNodes(true); + } + + // Set/Register/Add the new component + RootComponent = HoudiniAssetComponent; + HoudiniAssetComponent->RegisterComponent(); + AddInstanceComponent(HoudiniAssetComponent); + + // Update our cookable's component + MyCookable->SetComponent(HoudiniAssetComponent); + + // TODO: Not necessary? + FHoudiniEngineRuntime::Get().RegisterHoudiniCookable(MyCookable); + //FHoudiniEngineRuntime::Get().RegisterHoudiniComponent(HoudiniAssetComponent); +} + +// Indicates if this Actor is a NodeSyncActor +bool +AHoudiniAssetActor::IsNodeSyncActor() const +{ + return HoudiniAssetComponent->IsA(); +} + + +UHoudiniAssetComponent * +AHoudiniAssetActor::GetHoudiniAssetComponent() const +{ + return HoudiniAssetComponent; +} + +UHoudiniCookable* +AHoudiniAssetActor::GetHoudiniCookable() const +{ + return HoudiniCookable; +} + +#if WITH_EDITOR +bool +AHoudiniAssetActor::GetReferencedContentObjects(TArray& Objects) const +{ + Super::GetReferencedContentObjects(Objects); + + if (IsValid(HoudiniCookable)) + { + UHoudiniAsset* HoudiniAsset = HoudiniCookable->GetHoudiniAsset(); + if (IsValid(HoudiniAsset)) + Objects.AddUnique(HoudiniAsset); + } + + return true; +} +#endif + +#if WITH_EDITOR +void +AHoudiniAssetActor::PostEditChangeProperty(FPropertyChangedEvent & PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (!IsValid(HoudiniCookable)) + return; + + // Some property changes need to be forwarded to the component (ie Transform) + USceneComponent* SC = HoudiniCookable->GetComponent(); + if (!IsValid(SC)) + return; + + FProperty* Property = PropertyChangedEvent.MemberProperty; + if (!Property) + return; + + FName PropertyName = Property->GetFName(); + if (PropertyName == SC->GetRelativeLocationPropertyName() + || PropertyName == SC->GetRelativeRotationPropertyName() + || PropertyName == SC->GetRelativeScale3DPropertyName()) + { + HoudiniCookable->SetHasComponentTransformChanged(true); + } +} +#endif + + +bool +AHoudiniAssetActor::IsUsedForPreview() const +{ +#if WITH_EDITORONLY_DATA + return HasAnyFlags(RF_Transient) || bIsEditorPreviewActor; +#else + return HasAnyFlags(RF_Transient); +#endif +} + +UHoudiniPDGAssetLink* +AHoudiniAssetActor::GetPDGAssetLink() const +{ + return IsValid(HoudiniCookable) ? HoudiniCookable->GetPDGAssetLink() : nullptr; +} + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniAssetBlueprintComponent.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniAssetBlueprintComponent.h" + +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniEngineCopyPropertiesInterface.h" +#include "HoudiniOutput.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "Engine/SCS_Node.h" +#include "Engine/SimpleConstructionScript.h" +#include "UObject/Object.h" +#include "Logging/LogMacros.h" + +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterToggle.h" +#include "HoudiniInput.h" + +#if WITH_EDITOR + #include "Editor.h" + #include "Kismet2/BlueprintEditorUtils.h" + #include "Kismet2/KismetEditorUtilities.h" + #include "Kismet2/ComponentEditorUtils.h" + #include "ComponentAssetBroker.h" +#endif + +HOUDINI_BP_DEFINE_LOG_CATEGORY(); + +UHoudiniAssetBlueprintComponent::UHoudiniAssetBlueprintComponent(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) +{ + +#if WITH_EDITOR + if (IsTemplate()) + { + // CachedAssetEditorSubsystem = GEditor->GetEditorSubsystem(); + //GEditor->GetEditorSubsystem()->OnAssetEditorRequestClose().AddUObject( this, &UHoudiniAssetBlueprintComponent::ReceivedAssetEditorRequestCloseEvent ); + } +#endif + + bForceNeedUpdate_DEPRECATED = false; + bHoudiniAssetChanged = false; + bIsInBlueprintEditor = false; + bCanDeleteHoudiniNodes = false; + + // AssetState will be updated by changes to the HoudiniAsset + // or parameter changes on the Component template. + AssetState_DEPRECATED = EHoudiniAssetState::None; + bHasRegisteredComponentTemplate = false; + bHasBeenLoaded_DEPRECATED = false; + bUpdatedFromTemplate = false; + + // Disable proxy mesh by default (unsupported for now) + bOverrideGlobalProxyStaticMeshSettings_DEPRECATED = true; + bEnableProxyStaticMeshOverride_DEPRECATED = false; + bEnableProxyStaticMeshRefinementByTimerOverride_DEPRECATED = false; + bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride_DEPRECATED = false; + bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride_DEPRECATED = false; + + // Set default mobility to Movable + Mobility = EComponentMobility::Movable; +} + +#if WITH_EDITOR +void +UHoudiniAssetBlueprintComponent::CopyStateToTemplateComponent() +{ + // We need to propagate changes made here back to the corresponding component in + // the Blueprint Generated Class ("_GEN_VARIABLE"). The reason being that + // the Blueprint editor works directly with the GEN_VARIABLE component (all + // PostEditChange() calls, Details Customizations, etc will receive the GEN_VARIABLE instance) BUT + // when the Editor runs the construction script it uses a different component instance, so all changes + // made to that instance won't write back to the Blueprint definition. + // To Summarize: + // Be sure to sync the Parameters array (and any other relevant properties) back + // to the corresponding component on the Blueprint Generated class otherwise these wont be + // accessible in the Details Customization callbacks. + + HOUDINI_BP_MESSAGE(TEXT("[UHoudiniAssetBlueprintComponent::CopyStateFromTemplateComponent] Component: %s"), *(GetPathName())); + HOUDINI_BP_MESSAGE(TEXT("[UHoudiniAssetBlueprintComponent::CopyStateFromTemplateComponent] To Component: %s"), *(CachedTemplateComponent->GetPathName())); + + // This should never be called by component templates. + check(!IsTemplate()); + + if (!CachedTemplateComponent.IsValid()) + return; + + USimpleConstructionScript* SCS = CachedBlueprint->SimpleConstructionScript; + check(SCS); + + //// If we don't have an SCS node for this preview instance, we need to create one, regardless + //// of whether output updates are required. + //if (!CachedTemplateComponent->bOutputsRequireUpdate && SCSNodeForInstance != nullptr) + // return; + + // TODO: If the blueprint editor is NOT open, then we shouldn't attempting + // to copy state back to the BPGC at all! + FBlueprintEditor* BlueprintEditor = FHoudiniEngineRuntimeUtils::GetBlueprintEditor(this); + check(BlueprintEditor); + + USCS_Node* SCSHACNode = FindSCSNodeForTemplateComponentInClassHierarchy(CachedTemplateComponent.Get()); + // check(SCSHACNode); + + // This is the actor instance that is being used for component editing. + AActor* PreviewActor = GetPreviewActor(); + check(PreviewActor); + + // NOTE: Inputs are only from component templates to instances, not the other way around ... I think. + + // ----------------------------------------------------- + // Copy outputs to component template + // ----------------------------------------------------- + + // Populate / update the outputs for the template from the preview / instance. + // TODO: Wrap the Blueprint manipulation in a transaction + TArray>& TemplateOutputs = CachedTemplateComponent->Outputs_DEPRECATED; + TSet> StaleTemplateOutputs(TemplateOutputs); + + TemplateOutputs.SetNum(Outputs_DEPRECATED.Num()); + CachedOutputNodes.Empty(); + + for (int i = 0; i < Outputs_DEPRECATED.Num(); i++) + { + // Find a output on the template that corresponds to this output from the instance. + UHoudiniOutput* TemplateOutput = nullptr; + UHoudiniOutput* InstanceOutput = nullptr; + InstanceOutput = Outputs_DEPRECATED[i]; + + //check(InstanceOutput) + if (!IsValid(InstanceOutput)) + continue; + + // Ensure that instance outputs won't delete houdini content. + // Houdini content should only be allowed to be deleted from + // the component template. + InstanceOutput->SetCanDeleteHoudiniNodes(false); + + TemplateOutput = TemplateOutputs[i]; + + if (TemplateOutput) + { + check(TemplateOutput->GetOuter() == CachedTemplateComponent.Get()); + StaleTemplateOutputs.Remove(TemplateOutput); + } + + + if (TemplateOutput) + { + // Copy properties from the current instance component while preserving output objects + // and instanced outputs. + TemplateOutput->CopyPropertiesFrom(InstanceOutput, true); + } + else + { + // NOTE: If the template output is NULL it means that the HDA spawned a new component / output in the transient world + // and the new output object needs to be copied back to the BPGC. + + // Corresponding template output could not be found. Create one by duplicating the instance output. + TemplateOutput = InstanceOutput->DuplicateAndCopyProperties(CachedTemplateComponent.Get(), FName(InstanceOutput->GetName())); + // Treat these the same one would components created by CreateDefaultSubObject. + // NOTE: CreateDefaultSubobject performs lots of checks, and unfortunately we can't use it directly (it is + // only allowed to be used in a constructor). Not sure whether we need to either. For now, we just set the + // object flags to be similar to components created by CreateDefaultSubobject. + TemplateOutput->SetFlags(RF_Public|RF_ArchetypeObject|RF_DefaultSubObject); + TemplateOutputs[i] = TemplateOutput; + } + + check(TemplateOutput); + TemplateOutput->SetCanDeleteHoudiniNodes(false); + + // Keep track of potential stale output objects on the template component, for this output. + TMap& TemplateOutputObjects = TemplateOutput->GetOutputObjects(); + TArray StaleTemplateObjects; + TemplateOutputObjects.GetKeys(StaleTemplateObjects); + + for (auto& Entry : InstanceOutput->GetOutputObjects()) + { + + // Prepare the FHoudiniOutputObject for the template component + const FHoudiniOutputObject& InstanceObj = Entry.Value; + FHoudiniOutputObject TemplateObj; + + // Any output present in the Instance Outputs should be + // transferred to the template. + // Remove this output object from stale outputs list. + StaleTemplateObjects.Remove(Entry.Key); + + if (TemplateOutputObjects.Contains(Entry.Key)) + { + // Reuse the existing template object + TemplateObj = TemplateOutputObjects.FindChecked(Entry.Key); + } + else + { + // Create a new template output object object by duplicating the instance object. + // Keep the output object, but clear the output component since we have to + // create a new component template. + TemplateObj = InstanceObj; + TemplateObj.ProxyComponent = nullptr; + TemplateObj.OutputComponents.Empty(); + TemplateObj.ProxyObject = nullptr; + } + + // If the there is only one component we try to reuse templates. However, if there is more than + // one component, don't bother; the logic for doing so can become quite complicated and multiple components + // outputs are only currently used for world partition, which don't make sense for use in blueprints. + USceneComponent* ComponentTemplate = nullptr; + if (TemplateObj.OutputComponents.Num() == 1) + { + ComponentTemplate = Cast(TemplateObj.OutputComponents[0]); + } + else + { + TemplateObj.OutputComponents.Empty(); + } + + for(auto Component : InstanceObj.OutputComponents) + { + USceneComponent* ComponentInstance = Cast(Component); + + // The translation process has either constructed new components, or it is + // reusing existing components, or changed an output (or all or none of the aforementioned). + // Carefully inspect the SCS graph to determine whether there is a corresponding + // (and compatible) node for this output. If not, create a new node and remove unusable node, if any. + + USCS_Node* ComponentNode = nullptr; + + if (InstanceObj.OutputComponents.Num() == 1) + { + // Check whether the current OutputComponent being referenced by the template is still valid. + // Even if it was removed in the editor, it doesn't have any associated destroyed / pendingkill state. + // Instead we're going to check for validity by finding an SCS node with a matching template component. + bool bValidComponentTemplate = (ComponentTemplate != nullptr); + if (ComponentTemplate) + { + + ComponentNode = FindSCSNodeForTemplateComponentInClassHierarchy(ComponentTemplate); + bValidComponentTemplate = bValidComponentTemplate && (ComponentNode != nullptr); + } + + if (!bValidComponentTemplate) + { + // Either this component was removed from the editor or it doesn't exist yet. + // Ensure the references are cleared + TemplateObj.OutputComponents.Empty(); + ComponentTemplate = nullptr; + } + } + + // NOTE: we can't use the component instance name directly due to the Blueprint compiler performing an internal checking + // using FComponentEditorUtils::IsValidVariableNameString(), which will return false if the name looks like an autogenerated name... + //FString ComponentName = ComponentInstance->GetName(); + FString ComponentName = FBlueprintEditorUtils::GetClassNameWithoutSuffix(ComponentInstance->GetClass()); + FName ComponentFName = FName(ComponentName); + + + const auto ComponentCopyOptions = ( EditorUtilities::ECopyOptions::Type )( + EditorUtilities::ECopyOptions::PropagateChangesToArchetypeInstances | + EditorUtilities::ECopyOptions::CallPostEditChangeProperty | + EditorUtilities::ECopyOptions::CallPostEditMove); + + if (IsValid(ComponentNode)) + { + // Check if we have an existing and compatible SCS node containing a USceneComponent as a template component. + bool bComponentNodeIsValid = true; + + ComponentTemplate = Cast(ComponentNode->ComponentTemplate); + + bComponentNodeIsValid = bComponentNodeIsValid && ComponentInstance->GetClass() == ComponentNode->ComponentClass; + bComponentNodeIsValid = bComponentNodeIsValid && ComponentTemplate != nullptr; + // TODO: Do we need to perform any other compatibility checks? + + if (!bComponentNodeIsValid) + { + // Component template is not compatible. We can't reuse it. + + SCSHACNode->RemoveChildNode(ComponentNode); + SCS->RemoveNode(ComponentNode); + ComponentNode = nullptr; + ComponentTemplate = nullptr; + CachedTemplateComponent->MarkAsBlueprintStructureModified(); + } + } + + if (ComponentNode) + { + // We found a reusable SCS node. Just copy the component instance + // properties over to the existing template. + check(ComponentNode->ComponentTemplate); + + // UEngine::FCopyPropertiesForUnrelatedObjectsParams Params; + // //Params.bReplaceObjectClassReferences = false; + // Params.bDoDelta = false; // Perform a deep copy + // Params.bClearReferences = false; + // UEngine::CopyPropertiesForUnrelatedObjects(ComponentInstance, ComponentNode->ComponentTemplate, Params); + + FHoudiniEngineRuntimeUtils::CopyComponentProperties(ComponentInstance, ComponentNode->ComponentTemplate, ComponentCopyOptions); + CachedTemplateComponent->MarkAsBlueprintStructureModified(); + + ComponentNode->ComponentTemplate->CreationMethod = EComponentCreationMethod::Native; + } + else + { + // We couldn't find a reusable SCS node. + // Duplicate the instance component and create a new corresponding SCS node + ComponentNode = SCS->CreateNode(ComponentInstance->GetClass(), ComponentFName); + + UEditorEngine::FCopyPropertiesForUnrelatedObjectsParams Params; + Params.bDoDelta = false; // We need a deep copy of parameters here so the CDO values get copied as well + UEditorEngine::CopyPropertiesForUnrelatedObjects(ComponentInstance, ComponentNode->ComponentTemplate, Params); + // FHoudiniEngineRuntimeUtils::CopyComponentProperties(ComponentInstance, ComponentNode->ComponentTemplate, ComponentCopyOptions); + + // { + // UInstancedStaticMeshComponent* Component = Cast(ComponentNode->ComponentTemplate); + // if (Component) + // { + // } + // } + + // NOTE: The EComponentCreationMethod here is currently set to be the same as a component that was + // created manually in the editor. + ComponentNode->ComponentTemplate->CreationMethod = EComponentCreationMethod::Native; + + // Add this node to the SCS root set. + + // Attach the new node the HAC SCS node + // NOTE: This will add the node to the SCS->AllNodes list too but it won't update + // the nodename map. We can't forcibly update the Node/Name map either since the + // relevant functions have not been exported. + SCSHACNode->AddChildNode(ComponentNode); + + // Set the output component. + TemplateObj.OutputComponents.Empty(); + TemplateObj.OutputComponents.Add(ComponentNode->ComponentTemplate); + + CachedTemplateComponent->MarkAsBlueprintStructureModified(); + } + + // Cache the mapping between the output and the SCS node. + check(ComponentNode); + CachedOutputNodes.Add(Entry.Key, ComponentNode->VariableGuid); + } // if (ComponentInstance) + + + // Add the updated output object to the template output + TemplateOutputObjects.Add(Entry.Key, TemplateObj); + } + + // Cleanup stale objects for this template output. + for (const auto& StaleId : StaleTemplateObjects) + { + FHoudiniOutputObject& OutputObj = TemplateOutputObjects.FindChecked(StaleId); + + // Ensure the component template is no longer referencing this output. + TemplateOutputObjects.Remove(StaleId); + + for(auto Component : OutputObj.OutputComponents) + { + USceneComponent* TemplateComponent = Cast(Component); + + if (TemplateComponent) + { + USCS_Node* StaleNode = FindSCSNodeForTemplateComponentInClassHierarchy(TemplateComponent); + if (StaleNode) + { + + SCS->RemoveNode(StaleNode, false); + CachedTemplateComponent->MarkAsBlueprintStructureModified(); + } + } + } + } + } //for (int i = 0; i < Outputs.Num(); i++) + + // Clean up stale outputs on the component template. + for (UHoudiniOutput* StaleOutput : StaleTemplateOutputs) + { + if (!StaleOutput) + continue; + + // Remove any components contained in this output from the SCS graph + for (auto& Entry : StaleOutput->GetOutputObjects()) + { + FHoudiniOutputObject& StaleObject = Entry.Value; + + for(auto Component : StaleObject.OutputComponents) + { + USceneComponent* OutputComponent = Cast(Component); + + if (OutputComponent) + { + + USCS_Node* StaleNode = FindSCSNodeForTemplateComponentInClassHierarchy(OutputComponent); + if (StaleNode) + { + + SCS->RemoveNode(StaleNode, false); + CachedTemplateComponent->MarkAsBlueprintStructureModified(); + } + } + } + } + + TemplateOutputs.Remove(StaleOutput); + //StaleOutput->ConditionalBeginDestroy(); + } + + SCS->ValidateSceneRootNodes(); + + // Copy parameters from this component to the template component. + // NOTE: We need to do this since the preview component will be cooking the HDA and get populated with + // all the parameters. This data needs to be sent back to the component template. + UClass* ComponentClass = CachedTemplateComponent->GetClass(); + UHoudiniAssetBlueprintComponent* DefaultObj = Cast(ComponentClass->GetDefaultObject()); + bool bBPStructureModified = false; + CachedTemplateComponent->CopyDetailsFromComponent( + this, + true, + true, + true, + false, + true, + bBPStructureModified, + /* SetFlags */ CachedTemplateComponent->GetMaskedFlags(RF_PropagateToSubObjects)); + + if (bBPStructureModified) + CachedTemplateComponent->MarkAsBlueprintStructureModified(); + + // Copy the cached output nodes back to the template so that + // reconstructed actors can correctly update output objects + // with newly constructed components during ApplyComponentInstanceData() calls. + CachedTemplateComponent->CachedOutputNodes = CachedOutputNodes; + + CachedTemplateComponent->MarkPackageDirty(); + PostEditChange(); + + CachedTemplateComponent->AssetId_DEPRECATED = AssetId_DEPRECATED; + CachedTemplateComponent->HapiGUID_DEPRECATED = HapiGUID_DEPRECATED; + CachedTemplateComponent->AssetCookCount_DEPRECATED = AssetCookCount_DEPRECATED; + CachedTemplateComponent->AssetStateResult_DEPRECATED = AssetStateResult_DEPRECATED; + CachedTemplateComponent->bLastCookSuccess_DEPRECATED = bLastCookSuccess_DEPRECATED; + +#if WITH_EDITOR + // TODO: Do we need to handle this right now or can we wait for the next Houdini Engine manager tick to process it? + if (CachedTemplateComponent->NeedBlueprintStructureUpdate()) + { + // We are about to recompile the blueprint. This will reconstruct the preview actor so we need to ensure + // that the old actor won't release the houdini nodes. + FHoudiniEngineRuntimeUtils::MarkBlueprintAsStructurallyModified(CachedTemplateComponent.Get()); + SetCanDeleteHoudiniNodes(false); + } + /*else if (CachedTemplateComponent->NeedBlueprintUpdate()) + { + FHoudiniEngineRuntimeUtils::MarkBlueprintAsModified(CachedTemplateComponent.Get()); + }*/ +#endif +} +#endif + +#if WITH_EDITOR +void +UHoudiniAssetBlueprintComponent::CopyStateFromTemplateComponent(UHoudiniAssetBlueprintComponent* FromComponent, const bool bClearFromInputs, const bool bClearToInputs, const bool bCopyInputObjectComponentProperties) +{ + HOUDINI_BP_MESSAGE(TEXT("[UHoudiniAssetBlueprintComponent::CopyStateFromTemplateComponent] Component: %s"), *(GetPathName())); + HOUDINI_BP_MESSAGE(TEXT("[UHoudiniAssetBlueprintComponent::CopyStateFromTemplateComponent] From Component: %s"), *(FromComponent->GetPathName())); + + // This should never be called by component templates. + check(!IsTemplate()); + + // Make sure all TransientDuplicate properties from the Template Component needed by this transient component + // gets copied. + + ComponentGUID_DEPRECATED = FromComponent->ComponentGUID_DEPRECATED; + + /* + { + const TArray Children = GetAttachChildren(); + for (USceneComponent* Child : Children) + { + if (!Child) + continue; + } + } + */ + + // AssetState = FromComponent->PreviewAssetState; + + // This state should not be shared between template / instance components. + //bFullyLoaded = FromComponent->bFullyLoaded; + + bNoProxyMeshNextCookRequested_DEPRECATED = FromComponent->bNoProxyMeshNextCookRequested_DEPRECATED; + + // Reconstruct outputs and update them to point to component instances as opposed to templates. + UObject* TemplateOuter = CachedTemplateComponent->GetOuter(); + + USimpleConstructionScript* SCS = CachedBlueprint->SimpleConstructionScript; + check(SCS); + + // NOTE: We can find the SCS node for the HoudiniAssetComponent from either the template component or the instance (editor preview) component. + USCS_Node* SCSHACNode = FindSCSNodeForTemplateComponentInClassHierarchy(CachedTemplateComponent.Get()); + check(SCSHACNode); + + // ----------------------------------------------------- + // Copy outputs to component template + // ----------------------------------------------------- + + TArray>& TemplateOutputs = CachedTemplateComponent->Outputs_DEPRECATED; + + TSet StaleInstanceOutputs(Outputs_DEPRECATED); + + Outputs_DEPRECATED.SetNum(TemplateOutputs.Num()); + + for (int i = 0; i < TemplateOutputs.Num(); i++) + { + UHoudiniOutput* TemplateOutput = TemplateOutputs[i]; + if (!IsValid(TemplateOutput)) + continue; + + UHoudiniOutput* InstanceOutput = Outputs_DEPRECATED[i]; + if (!(InstanceOutput->GetOuter() == this)) + InstanceOutput = nullptr; + + if (InstanceOutput) + { + StaleInstanceOutputs.Remove(InstanceOutput); + } + + if (InstanceOutput) + { + // Copy properties from the current instance component while preserving output objects + // and instanced outputs. + InstanceOutput->CopyPropertiesFrom(TemplateOutput, true); + } + else + { + InstanceOutput = TemplateOutput->DuplicateAndCopyProperties(this, FName(TemplateOutput->GetName())); + if (IsValid(InstanceOutput)) + InstanceOutput->ClearFlags(RF_ArchetypeObject|RF_DefaultSubObject); + } + + Outputs_DEPRECATED[i] = InstanceOutput; + + if (!IsValid(InstanceOutput)) + continue; + + InstanceOutput->SetCanDeleteHoudiniNodes(false); + + TMap& TemplateOutputObjects = TemplateOutput->GetOutputObjects(); + TMap& InstanceOutputObjects = InstanceOutput->GetOutputObjects(); + TArray StaleOutputObjects; + InstanceOutputObjects.GetKeys(StaleOutputObjects); + + for (auto& Entry : TemplateOutputObjects) + { + const FHoudiniOutputObject& TemplateObj = Entry.Value; + FHoudiniOutputObject InstanceObj = TemplateObj; + + if (!InstanceOutputObjects.Contains(Entry.Key)) + continue; + + StaleOutputObjects.Remove(Entry.Key); + InstanceObj = InstanceOutputObjects.FindChecked(Entry.Key); + + } // for (auto& Entry : TemplateOutputObjects) + + // Cleanup stale output objects for this output. + for (const auto& StaleId : StaleOutputObjects) + { + //TemplateOutput + //check(TemplateOutputs); + + FHoudiniOutputObject& OutputObj = InstanceOutputObjects.FindChecked(StaleId); + + InstanceOutputObjects.Remove(StaleId); + if (OutputObj.OutputComponents.Num() > 0) + { + //OutputObj.OutputComponent->ConditionalBeginDestroy(); + OutputObj.OutputComponents.Empty(); + } + } + } // for (int i = 0; i < TemplateOutputs.Num(); i++) + + // Cleanup any stale outputs found on the component instance. + for (UHoudiniOutput* StaleOutput : StaleInstanceOutputs) + { + if (!StaleOutput) + continue; + + if (!(StaleOutput->GetOuter() == this)) + continue; + + // We don't want to clear stale outputs on components instances. Only on template components. + StaleOutput->SetCanDeleteHoudiniNodes(false); + } + + // Copy parameters from the component template to the instance. + bool bBlueprintStructureChanged = false; + CopyDetailsFromComponent(FromComponent, + false, + bClearFromInputs, + bClearToInputs, + false, + true, + bBlueprintStructureChanged, + /*SetFlags*/ RF_Public, + /*ClearFlags*/ RF_DefaultSubObject|RF_ArchetypeObject); +} +#endif + +#if WITH_EDITOR +void +UHoudiniAssetBlueprintComponent::CopyDetailsFromComponent( + UHoudiniAssetBlueprintComponent* FromComponent, + const bool bCreateSCSNodes, + const bool bClearChangedToInputs, + const bool bClearChangedFromInputs, + const bool bInCanDeleteHoudiniNodes, + const bool bCopyInputObjectComponentProperties, + bool &bOutBlueprintStructureChanged, + EObjectFlags SetFlags, + EObjectFlags ClearFlags) +{ + check(FromComponent); + + HOUDINI_BP_MESSAGE(TEXT("[UHoudiniAssetBlueprintComponent::CopyDetailsFromComponent] Component: %s"), *(GetPathName())); + HOUDINI_BP_MESSAGE(TEXT("[UHoudiniAssetBlueprintComponent::CopyDetailsFromComponent] FromComponent: %s"), *(FromComponent->GetPathName())); + + /* + if (!FromComponent->HoudiniAsset) + { + return; + } + */ + + // TODO: Try to reuse objects here when we're able. + //// Copy UHoudiniOutput state from instance to template + //UEngine::FCopyPropertiesForUnrelatedObjectsParams Params; + ////Params.bReplaceObjectClassReferences = false; + ////Params.bClearReferences = false; + //Params.bDoDelta = true; + //UEngine::CopyPropertiesForUnrelatedObjects(ComponentInstance, ComponentNode->ComponentTemplate, Params); + + // Record input remapping that will need to take place when duplicating parameters. + TMap InputMapping; + + // ----------------------------------------------------- + // Copy inputs + // ----------------------------------------------------- + + // TODO: Add support for input components + { + TArray>& FromInputs = FromComponent->Inputs_DEPRECATED; + TSet StaleInputs(Inputs_DEPRECATED); + USimpleConstructionScript* SCS = GetSCS(); + USCS_Node* SCSHACNode = nullptr; + + if (bCreateSCSNodes) + { + SCSHACNode = FindSCSNodeForTemplateComponentInClassHierarchy(CachedTemplateComponent.Get()); + } + + Inputs_DEPRECATED.SetNum(FromInputs.Num()); + for (int i = 0; i < FromInputs.Num(); i++) + { + UHoudiniInput* FromInput = nullptr; + UHoudiniInput* ToInput = nullptr; + FromInput = FromInputs[i]; + + check(FromInput); + + ToInput = Inputs_DEPRECATED[i]; + + if (ToInput) + { + // Check whether the instance and template input objects are compatible. + bool bIsValid = true; + bIsValid = bIsValid && ToInput->Matches(*FromInput); + bIsValid = bIsValid && ToInput->GetOuter() == this; + + if (!bIsValid) + { + ToInput = nullptr; + } + } + + // TODO: Process stale input objects + + // NOTE: The CopyStateFrom() / DuplicateAndCopyState() will copy/duplicate/cleanup internal inputs to + // ensure that there aren't any shared instances between the ToInput/FromInput. + if (ToInput) + { + // We have a compatible input that we can reuse. + StaleInputs.Remove(ToInput); + ToInput->CopyStateFrom(FromInput, true, bInCanDeleteHoudiniNodes); + } + else + { + + // We don't have an existing / compatible input. Create a new one. + ToInput = FromInput->DuplicateAndCopyState(this, bInCanDeleteHoudiniNodes); + if (SetFlags != RF_NoFlags) + ToInput->SetFlags(SetFlags); + if (ClearFlags != RF_NoFlags) + ToInput->ClearFlags( ClearFlags ); + } + + check(ToInput); + + + UpdateInputObjectComponentReferences(SCS, FromInput, ToInput, bCopyInputObjectComponentProperties, bCreateSCSNodes, SCSHACNode, &bOutBlueprintStructureChanged); + + Inputs_DEPRECATED[i] = ToInput; + InputMapping.Add(FromInput, ToInput); + + if (bClearChangedToInputs) + { + // Clear the changed flags on the FromInput so that it doesn't trigger + // another update. The ToInput will now be carrying to changed/update flags. + ToInput->MarkChanged(false); + ToInput->MarkAllInputObjectsChanged(false); + } + + if (bClearChangedFromInputs) + { + // Clear the changed flags on the FromInput so that it doesn't trigger + // another update. The ToInput will now be carrying to changed/update flags. + FromInput->MarkChanged(false); + FromInput->MarkAllInputObjectsChanged(false); + } + } + + // Cleanup any stale inputs from this component. + // NOTE: We would typically only have stale inputs when copying state from + // the component instance to the component template. Garbage collection + // eventually picks up the input objects and removes the content + // but until such time we are stuck with those nodes as inputs in the Houdini session + // so we get rid of those nodes immediately here to avoid some user confusion. + for (UHoudiniInput* StaleInput : StaleInputs) + { + if (!IsValid(StaleInput)) + continue; + + check(StaleInput->GetOuter() == this); + + if (StaleInput->HasAnyFlags(RF_NeedLoad | RF_NeedPostLoad)) + continue; + + StaleInput->ConditionalBeginDestroy(); + } + } + + + // ----------------------------------------------------- + // Copy parameters (and optionally remap inputs). + // ----------------------------------------------------- + TMap ParameterMapping; + + TArray>& FromParameters = FromComponent->Parameters_DEPRECATED; + Parameters_DEPRECATED.SetNum(FromParameters.Num()); + + for (int i = 0; i < FromParameters.Num(); i++) + { + UHoudiniParameter* FromParameter = nullptr; + UHoudiniParameter* ToParameter = nullptr; + + FromParameter = FromParameters[i]; + + check(FromParameter); + + if (Parameters_DEPRECATED.IsValidIndex(i)) + { + ToParameter = Parameters_DEPRECATED[i]; + } + + if (ToParameter) + { + bool bIsValid = true; + // Check whether To/From parameters are compatible + bIsValid = bIsValid && ToParameter->Matches(*FromParameter); + bIsValid = bIsValid && ToParameter->GetOuter() == this; + + if (!bIsValid) + ToParameter = nullptr; + } + + if (ToParameter) + { + // Parameter already exists. Simply sync the state. + ToParameter->CopyStateFrom(FromParameter, true, ClearFlags, SetFlags); + } + else + { + // TODO: Check whether parameters are the same to avoid recreating them. + ToParameter = FromParameter->DuplicateAndCopyState(this, ClearFlags, SetFlags); + Parameters_DEPRECATED[i] = ToParameter; + } + + check(ToParameter); + ParameterMapping.Add(FromParameter, ToParameter); + + if (bClearChangedFromInputs) + { + // We clear the Changed flag on the FromParameter (most likely on the component template) + // since the template parameter state has now been transfered to the preview component and + // will resume processing from there. + FromParameter->MarkChanged(false); + } + } + + // Apply remappings on the new parameters + for (UHoudiniParameter* ToParameter : Parameters_DEPRECATED) + { + ToParameter->RemapParameters(ParameterMapping); + ToParameter->RemapInputs(InputMapping); + } + + FProperty* ParametersProperty = GetClass()->FindPropertyByName(TEXT("Parameters")); + FPropertyChangedEvent Evt(ParametersProperty); + PostEditChangeProperty(Evt); + + bEnableCooking_DEPRECATED = FromComponent->bEnableCooking_DEPRECATED; + bRecookRequested_DEPRECATED = FromComponent->bRecookRequested_DEPRECATED; + bRebuildRequested_DEPRECATED = FromComponent->bRebuildRequested_DEPRECATED; +} + +void +UHoudiniAssetBlueprintComponent::UpdateInputObjectComponentReferences( + USimpleConstructionScript* SCS, + UHoudiniInput* FromInput, + UHoudiniInput* ToInput, + const bool bCopyInputObjectProperties, + const bool bCreateMissingSCSNodes, + USCS_Node* SCSHACParent, + bool* bOutSCSNodeCreated) +{ + TArray ToInputObjects; + TArray FromInputObjects; + TArray StaleInputObjects; + + ToInput->GetAllHoudiniInputSplineComponents(ToInputObjects); + FromInput->GetAllHoudiniInputSplineComponents(FromInputObjects); + + FHoudiniInputObjectSettings ToInputSettings(ToInput); + + StaleInputObjects = ToInputObjects; + + const int32 NumInputObjects = FromInputObjects.Num(); + ToInputObjects.SetNum(NumInputObjects); + + const auto ComponentCopyOptions = ( EditorUtilities::ECopyOptions::Type )(EditorUtilities::ECopyOptions::Default); + UEngine::FCopyPropertiesForUnrelatedObjectsParams Params; + //Params.bDoDelta = false; // Perform a deep copy + Params.bClearReferences = false; + + for(int32 InputObjectIndex = 0; InputObjectIndex < NumInputObjects; ++InputObjectIndex) + { + UHoudiniInputHoudiniSplineComponent* FromInputObject = FromInputObjects[InputObjectIndex]; + UHoudiniInputHoudiniSplineComponent* ToInputObject = ToInputObjects[InputObjectIndex]; + if (!FromInputObject) + continue; + if (!ToInputObject) + continue; + + USCS_Node* SCSNode = nullptr; + if (CachedInputNodes.Contains(ToInputObject->Guid)) + { + // Reuse / update the existing SCS node. + SCSNode = SCS->FindSCSNodeByGuid( CachedInputNodes.FindChecked(ToInputObject->Guid) ); + } + + if (!SCSNode) + { + if (!bCreateMissingSCSNodes) + continue; // This input object should be removed. + } + + USceneComponent* ToComponent = nullptr; + USceneComponent* FromComponent = Cast(FromInputObject->GetObject()); + + StaleInputObjects.Remove(ToInputObject); + + if (FromComponent) + { + if (!SCSNode) + { + if (bCreateMissingSCSNodes) + { + // Create a new SCS node + SCSNode = SCS->CreateNode(FromComponent->GetClass()); + SCSHACParent->AddChildNode(SCSNode); + if (bOutSCSNodeCreated) + { + *bOutSCSNodeCreated = true; + } + AddInputObjectMapping(ToInputObject->Guid, SCSNode->VariableGuid); + } + } + + if (SCSNode) + { + if (bCreateMissingSCSNodes) + { + // If we have been instructed to create missing SCS nodes, assume we are copying + // the the component template. + ToComponent = Cast(SCSNode->ComponentTemplate); + } + else + { + // We are not copying to the component template, so we're assuming this is a + // component instance. Find the component on the owning actor that matches the SCS node. + AActor* ToOwningActor = ToInput->GetTypedOuter(); + check(ToOwningActor); + + ToComponent = Cast(FindComponentInstanceInActor(ToOwningActor, SCSNode)); + } + + if (bCopyInputObjectProperties && ToComponent) + { + USceneComponent* ToAttachParent = ToComponent->GetAttachParent(); + // Copy specific properties from the component template to the instance, if supported by the component. + // We typically resort to this in order to transfer Transient and TransientDuplicate properties from the + // component template over to the instance (typically HasChanged / NeedsToTriggerUpdate flags) in order for + // the instance to cook properly. + IHoudiniEngineCopyPropertiesInterface* ToCopyableComponent = Cast(ToComponent); + if (ToCopyableComponent) + { + // Let the component manage its own data copying. + ToCopyableComponent->CopyPropertiesFrom(FromComponent); + } + else + { + // The component doesn't implement the property copy interface. Simply do a general property copy. + //UEngine::CopyPropertiesForUnrelatedObjects(FromComponent, ToComponent, Params); + FHoudiniEngineRuntimeUtils::CopyComponentProperties(FromComponent, ToComponent, ComponentCopyOptions); + } + ToComponent->PostEditChange(); + } + } + } + + ToInputObject->Update(ToComponent, ToInputSettings); + ToInputObjects[InputObjectIndex] = ToInputObject; + } + + for (UHoudiniInputObject* StaleInputObject : StaleInputObjects) + { + if (!StaleInputObject) + continue; + StaleInputObject->InvalidateData(); + ToInput->RemoveHoudiniInputObject(StaleInputObject); + ToInput->MarkChanged(true); + // TODO: Find the corresponding SCS node and remove it + } +} + +#endif + +#if WITH_EDITOR +bool +UHoudiniAssetBlueprintComponent::HasOpenEditor() const +{ + if (IsTemplate()) + { + IAssetEditorInstance* EditorInstance = FindEditorInstance(); + + return EditorInstance != nullptr; + } + + return false; +} +#endif + +#if WITH_EDITOR +IAssetEditorInstance* +UHoudiniAssetBlueprintComponent::FindEditorInstance() const +{ + UClass* BPGC = Cast(GetOuter()); + if (!IsValid(BPGC)) + return nullptr; + UBlueprint* Blueprint = Cast(BPGC->ClassGeneratedBy); + if (!IsValid(Blueprint)) + return nullptr; + if (!CachedAssetEditorSubsystem.IsValid()) + return nullptr; + + IAssetEditorInstance* EditorInstance = CachedAssetEditorSubsystem->FindEditorForAsset(Blueprint, false); + + return EditorInstance; +} +#endif + +#if WITH_EDITOR +AActor* +UHoudiniAssetBlueprintComponent::GetPreviewActor() const +{ + FBlueprintEditor* BlueprintEditor = FHoudiniEngineRuntimeUtils::GetBlueprintEditor(this); + if (BlueprintEditor) + { + return BlueprintEditor->GetPreviewActor(); + } + return nullptr; +} +#endif + +UHoudiniAssetComponent* +UHoudiniAssetBlueprintComponent::GetCachedTemplate() const +{ + return CachedTemplateComponent.Get(); +} + +//bool +//UHoudiniAssetBlueprintComponent::CanUpdateParameters() const +//{ +// return IsTemplate(); +//} +// +//bool +//UHoudiniAssetBlueprintComponent::CanUpdateInputs() const +//{ +// return !IsTemplate(); +//} +// +//bool +//UHoudiniAssetBlueprintComponent::CanUpdateOutputs() const +//{ +// return !IsTemplate(); +//} +// +//bool +//UHoudiniAssetBlueprintComponent::CanProcessOutputs() const +//{ +// return !IsTemplate(); +//} + +//bool +//UHoudiniAssetBlueprintComponent::CanInstantiateAsset() const +//{ +// // If this is a preview component, it should not trigger an asset instantiation. It should wait +// // for the BPGC template component to finish the cook, get the synced data and then translate. +// +// if (IsPreview()) +// return false; +// +// return true; +//} +// +//// Check whether the HAC can translate Houdini outputs at all +//bool +//UHoudiniAssetBlueprintComponent::CanTranslateFromHoudini() const +//{ +// // Template components can't translate Houdini output since they typically do not exist in a world. +// if (IsTemplate()) +// return false; +// // Preview components and normally instanced actors can translate Houdini outputs. +// return true; +//} +// +//// Check whether the HAC can translate a specific output type. +//bool +//UHoudiniAssetBlueprintComponent::CanTranslateFromHoudini(EHoudiniOutputType OutputType) const +//{ +// // Blueprint components have limited translation support, for now. +// if (OutputType == EHoudiniOutputType::Mesh) +// return true; +// +// return false; +//} +// +bool +UHoudiniAssetBlueprintComponent::CanDeleteHoudiniNodes() const +{ + return bCanDeleteHoudiniNodes; +} + +void +UHoudiniAssetBlueprintComponent::SetCanDeleteHoudiniNodes(bool bInCanDeleteNodes) +{ + bCanDeleteHoudiniNodes = bInCanDeleteNodes; + +#if WITH_EDITORONLY_DATA + for (UHoudiniInput* Input : Inputs_DEPRECATED) + { + Input->SetCanDeleteHoudiniNodes(bInCanDeleteNodes); + } +#endif + for (UHoudiniOutput* Output : Outputs_DEPRECATED) + { + Output->SetCanDeleteHoudiniNodes(bInCanDeleteNodes); + } +} + +bool +UHoudiniAssetBlueprintComponent::IsValidComponent() const +{ + if (!Super::IsValidComponent()) + return false; + + if (IsTemplate()) + { + UObject* Outer = this->GetOuter(); + if (!IsValid(Outer)) + return false; + UBlueprintGeneratedClass* BPGC = Cast(Outer); + if (!BPGC) + return false; + // Ensure this component is still in the SCS + USimpleConstructionScript* SCS = BPGC->SimpleConstructionScript; + if (!SCS) + return false; + USCS_Node* SCSNode = FindSCSNodeForTemplateComponentInClassHierarchy(this); + if (!SCSNode) + return false; + /*UClass* OwnerClass = Outer->GetClass(); + if (!IsValid(OwnerClass)) + return false;*/ + /*UBlueprint* Blueprint = Cast(Outhe); + if (Blueprint) + { + + }*/ + } + +#if WITH_EDITOR + if (!IsTemplate()) + { + if (!GetOwner()) + { + // If it's not a template, it needs an owner! + return false; + } + + USimpleConstructionScript* SCS = GetSCS(); + if (SCS) + { + // We're dealing with a Blueprint related component. + AActor* PreviewActor = GetPreviewActor(); + AActor* OwningActor = GetOwner(); + if (!OwningActor) + return false; + if (OwningActor != PreviewActor) + { + return false; + } + } + + } + + if (IsPreview() && false) + { + USimpleConstructionScript* SCS = GetSCS(); + if (!SCS) + return false; // Preview components should have an SCS. + + // We want to specifically detect whether an editor component is still being previewed. We do this + // by checking whether the owning actor is still the active editor actor in the SCS. + AActor* PreviewActor = GetPreviewActor(); + if (!PreviewActor) + { + return false; + } + + // Ensure this component still belongs the to the current preview actor. + if (PreviewActor != GetOwner()) + { + return false; + } + + /* + AActor* EditorActor = SCS->GetComponentEditorActorInstance(); + if (GetOwner() != EditorActor) + { + return false; + } + */ + } +#endif + + return true; +} + +bool +UHoudiniAssetBlueprintComponent::IsInputTypeSupported(EHoudiniInputType InType) const +{ + switch (InType) + { + case EHoudiniInputType::Geometry: + case EHoudiniInputType::Curve: + return true; + break; + + default: + break; + } + return false; +} + +bool +UHoudiniAssetBlueprintComponent::IsOutputTypeSupported(EHoudiniOutputType InType) const +{ + switch (InType) + { + case EHoudiniOutputType::Mesh: + case EHoudiniOutputType::Instancer: + return true; + break; + default: + break; + } + return false; +} + +bool +UHoudiniAssetBlueprintComponent::IsProxyStaticMeshEnabled() const +{ + // TODO: Investigate adding support for proxy meshes in BP + // Disabled for now + return false; +} + +//void +//UHoudiniAssetBlueprintComponent::BroadcastPreAssetCook() +//{ +// // ------------------------------------------------ +// // NOTE: This code will run on TEMPLATE components +// // ------------------------------------------------ +// +// // The HoudiniAsset is about to be recooked. This flag will indicate to +// // the transient components that output processing needs to be baked +// // back to the BP definition. +// bOutputsRequireUpdate = true; +// +// Super::BroadcastPreAssetCook(); +//} + +void +UHoudiniAssetBlueprintComponent::OnPrePreCook() +{ + check(IsPreview()); + + Super::OnPrePreCook(); + + // We need to allow deleting houdini nodes + SetCanDeleteHoudiniNodes(true); +} + +void +UHoudiniAssetBlueprintComponent::OnPostPreCook() +{ + check(IsPreview()); + + Super::OnPostPreCook(); + + // Ensure the houdini nodes can be deleted during the translation process. + SetCanDeleteHoudiniNodes(false); +} + +void +UHoudiniAssetBlueprintComponent::OnPreOutputProcessing() +{ + check(IsPreview()); + + Super::OnPreOutputProcessing(); + + // Ensure the houdini nodes can be deleted during the translation process. + SetCanDeleteHoudiniNodes(true); +} + +void +UHoudiniAssetBlueprintComponent::OnPostOutputProcessing() +{ + Super::OnPostOutputProcessing(); + + // ------------------------------------------------ + // NOTE: + // In Blueprint editor mode, this code will run on PREVIEW components + // In Map editor mode, this code will run on component instances. + // ------------------------------------------------ + if (IsPreview()) + { + // Ensure all the inputs / outputs belonging to the + // preview actor won't be deleted by PreviewActor destruction. + SetCanDeleteHoudiniNodes(false); + +#if WITH_EDITOR + CopyStateToTemplateComponent(); +#endif + + } + bUpdatedFromTemplate = false; +} + +void UHoudiniAssetBlueprintComponent::OnPrePreInstantiation() +{ + HOUDINI_BP_MESSAGE(TEXT("[UHoudiniAssetBlueprintComponent::OnPrePreInstantiation] Component: %s"), *(GetPathName())); + + check(IsPreview()); + + if (bUpdatedFromTemplate) + return; + + check(CachedTemplateComponent.IsValid()); + + // This HDA is about to be cooked but not through template parameter changes. It is likely that an input changed directly in the preview world. + // We need to flag our inputs and parameters appropriately in order to preserve their values. + + // We need to mark all our parameters as changed/not triggering update +#if WITH_EDITORONLY_DATA + for (auto CurrentParam : Parameters_DEPRECATED) + { + if (CurrentParam) + { + CurrentParam->MarkChanged(true); + CurrentParam->SetNeedsToTriggerUpdate(false); + } + } +#endif + +#if WITH_EDITORONLY_DATA + // We need to mark all our inputs as changed/not triggering update + for (auto CurrentInput : Inputs_DEPRECATED) + { + if (CurrentInput) + { + CurrentInput->MarkChanged(true); + CurrentInput->SetNeedsToTriggerUpdate(false); + CurrentInput->MarkDataUploadNeeded(true); + } + } +#endif +} + +void +UHoudiniAssetBlueprintComponent::NotifyHoudiniRegisterCompleted() +{ + if (IsTemplate()) + { + // TODO: Do we need to set any status flags or clear stuff to ensure + // the BP HAC will cook properly when the BP is opened again... + + // If the template is being registered, we need to invalidate the AssetId here since it likely + // contains a stale asset id from its last cook. + AssetId_DEPRECATED = -1; + // Template component's have very limited update requirements / capabilities. + // Mostly just cache parameters and cook state. + SetAssetState(EHoudiniAssetState::ProcessTemplate); + } + + Super::NotifyHoudiniRegisterCompleted(); +} + +void +UHoudiniAssetBlueprintComponent::NotifyHoudiniPreUnregister() +{ + if (IsTemplate()) + { + // Templates can delete Houdini nodes when they get deregistered. + SetCanDeleteHoudiniNodes(true); + } + Super::NotifyHoudiniPreUnregister(); +} + +void +UHoudiniAssetBlueprintComponent::NotifyHoudiniPostUnregister() +{ + InvalidateData(); + + Super::NotifyHoudiniPostUnregister(); + + if (IsTemplate()) + { + SetCanDeleteHoudiniNodes(false); + } +} + +#if WITH_EDITOR +void +UHoudiniAssetBlueprintComponent::OnComponentCreated() +{ + HOUDINI_BP_MESSAGE(TEXT("[UHoudiniAssetBlueprintComponent::OnComponentCreated] Component: %s"), *(GetPathName())); + + Super::OnComponentCreated(); + bUpdatedFromTemplate = false; + + CachePreviewState(); + + if (IsPreview()) + { + // Don't set an initial AssetState here. Preview components should only cook when template's + // Houdini Asset or HDA parameters have changed. + + // Clear these to ensure that we're not sharing references with the component template (otherwise + // the shared objects will get deleted when the component instance gets destroyed). + // These objects will be properly duplicated when copying state from the component template. + Inputs_DEPRECATED.Empty(); + Parameters_DEPRECATED.Empty(); + } + + // Wait until InitializeComponent() for blueprint construction to complete before we start caching blueprint data. + +} +#endif + + +void +UHoudiniAssetBlueprintComponent::OnRegister() +{ + HOUDINI_BP_MESSAGE(TEXT("[UHoudiniAssetBlueprintComponent::OnRegister] Component: %s"), *(GetPathName())); + + Super::OnRegister(); + + // We run our Blueprint caching functions here since this the last hook that we have before + // entering HoudiniEngineTick(); + + CacheBlueprintData(); + CachePreviewState(); + + if (IsPreview()) + { + check(CachedTemplateComponent.Get()); + + UHoudiniCookable* CachedCookable = CachedTemplateComponent->GetCookable(); + + // Ensure that the component template has been registered since it needs to be processed for parameter updates by the HE manager. + if (!FHoudiniEngineRuntime::Get().IsCookableRegistered(CachedCookable)) + { + // The template component has not been registered yet, which means that we're probably busy opening a Blueprint editor and this + // preview component will need to be updated. + + FHoudiniEngineRuntime::Get().RegisterHoudiniCookable(CachedCookable, true); + CachedTemplateComponent->SetCanDeleteHoudiniNodes(false); + // Since we're likely opening a fresh blueprint editor, we'll need to instantiate the HDA. + bHasRegisteredComponentTemplate = true; + } + } + + if (IsTemplate()) + { + // We're initializing the asset id for HAC template here since it doesn't get unloaded + // from memory, for example, between Blueprint Editor open/close so we need to make sure + // that the AssetId has indeed been reset between registrations. + AssetId_DEPRECATED = -1; + } + + //TickInitialization(); +} + +void +UHoudiniAssetBlueprintComponent::BeginDestroy() +{ + Super::BeginDestroy(); +} + +void +UHoudiniAssetBlueprintComponent::DestroyComponent(bool bPromoteChildren) +{ + //FDebug::DumpStackTraceToLog(); + if (CachedTemplateComponent.IsValid() && TemplatePropertiesChangedHandle.IsValid()) + { + CachedTemplateComponent->Modify(); + CachedTemplateComponent->OnParametersChangedEvent.Remove(TemplatePropertiesChangedHandle); + } + Super::DestroyComponent(bPromoteChildren); +} + +void +UHoudiniAssetBlueprintComponent::OnComponentDestroyed(bool bDestroyingHierarchy) +{ + Super::OnComponentDestroyed(bDestroyingHierarchy); +} + +TStructOnScope +UHoudiniAssetBlueprintComponent::GetComponentInstanceData() const +{ + HOUDINI_BP_MESSAGE(TEXT("[UHoudiniAssetBlueprintComponent::GetComponentInstanceData] Component: %s"), *(GetPathName())); + + TStructOnScope ComponentInstanceData = MakeStructOnScope(this); + FHoudiniAssetBlueprintInstanceData* InstanceData = ComponentInstanceData.Cast(); + + InstanceData->AssetId = AssetId_DEPRECATED; + InstanceData->AssetState = AssetState_DEPRECATED; + InstanceData->SubAssetIndex = SubAssetIndex_DEPRECATED; + InstanceData->ComponentGUID = ComponentGUID_DEPRECATED; + InstanceData->HapiGUID = HapiGUID_DEPRECATED; + InstanceData->HoudiniAsset = HoudiniAsset_DEPRECATED; + InstanceData->SourceName = GetPathName(); + InstanceData->AssetCookCount = AssetCookCount_DEPRECATED; + InstanceData->bHasBeenLoaded = bHasBeenLoaded_DEPRECATED; + InstanceData->bHasBeenDuplicated = bHasBeenDuplicated_DEPRECATED; + InstanceData->bPendingDelete = bPendingDelete_DEPRECATED; + InstanceData->bRecookRequested = bRecookRequested_DEPRECATED; + InstanceData->bEnableCooking = bEnableCooking_DEPRECATED; + InstanceData->bForceNeedUpdate = bForceNeedUpdate_DEPRECATED; + InstanceData->bLastCookSuccess = bLastCookSuccess_DEPRECATED; + InstanceData->bRegisteredComponentTemplate = bHasRegisteredComponentTemplate; + + InstanceData->Inputs.Empty(); + +#if WITH_EDITORONLY_DATA + for (UHoudiniInput* Input : Inputs_DEPRECATED) + { + if (!Input) + continue; + UHoudiniInput* TransientInput = Input->DuplicateAndCopyState(GetTransientPackage(), false); + InstanceData->Inputs.Add(TransientInput); + } +#endif + + // Cache the current outputs + InstanceData->Outputs.Empty(); + int OutputIndex = 0; + for(UHoudiniOutput* Output : Outputs_DEPRECATED) + { + if (!Output) + continue; + + TMap OutputObjects = Output->GetOutputObjects(); + for (auto& Entry : OutputObjects) + { + FHoudiniAssetBlueprintOutput OutputObjectData; + OutputObjectData.OutputIndex = OutputIndex; + OutputObjectData.OutputObject = Entry.Value; + InstanceData->Outputs.Add(Entry.Key, OutputObjectData); + } + + ++OutputIndex; + } + + return ComponentInstanceData; + +} + +void +UHoudiniAssetBlueprintComponent::ApplyComponentInstanceData(FHoudiniAssetBlueprintInstanceData* InstanceData, const bool bPostUCS) +{ + HOUDINI_BP_MESSAGE(TEXT("[UHoudiniAssetBlueprintComponent::ApplyComponentInstanceData] Component: %s"), *(GetPathName())); + check(InstanceData); + + if (!bPostUCS) + { + // Initialize the component before the User Construction Script runs + USimpleConstructionScript* SCS = GetSCS(); + check(SCS); + +#if WITH_EDITORONLY_DATA + TArray StaleInputs(Inputs_DEPRECATED); + + // We need to update references contain in inputs / outputs to point to new reconstructed components. + const int32 NumInputs = InstanceData->Inputs.Num(); + Inputs_DEPRECATED.SetNum(NumInputs); + for (int i = 0; i < NumInputs; ++i) + { + UHoudiniInput* FromInput = InstanceData->Inputs[i]; + if (!IsValid(FromInput)) + continue; + + UHoudiniInput* ToInput = Inputs_DEPRECATED[i]; + if (IsValid(ToInput)) + { + bool bIsValid = true; + bIsValid = bIsValid && ToInput->Matches(*FromInput); + bIsValid = bIsValid && ToInput->GetOuter() == this; + if (!bIsValid) + { + ToInput = nullptr; + } + } + + if (IsValid(ToInput)) + { + // Reuse input + StaleInputs.Remove(ToInput); + ToInput->CopyStateFrom(FromInput, true, false); + } + else + { + // Create new input + ToInput = FromInput->DuplicateAndCopyState(this, false); + } + +#if WITH_EDITOR + // We can't create missing SCS nodes here since we're likely already in the middle of a + // Blueprint reconstruction. We'll have to recreate missing SCS nodes next time the + // component state if copied to the template. + UpdateInputObjectComponentReferences(SCS, FromInput, ToInput, true, false); +#endif + + Inputs_DEPRECATED[i] = ToInput; + } +#endif + // We need to update FHoudiniOutputObject SceneComponent references to + // the newly created components. Since we cached a map of Output Object IDs to + // SCSNodes (during CopyStateToTemplateComponent), we can the SCSNode that corresponds to this output objects and find + // the SceneComponent that matches the SCSNode's variable name. + // It is important to note that it is safe to do it this way since we're in the pre-UCS + // phase so that current components should match the SCS graph exactly (no user construction script + // interference here yet). + + for (auto& Entry : InstanceData->Outputs) + { + FHoudiniOutputObjectIdentifier& ObjectId = Entry.Key; + FHoudiniAssetBlueprintOutput& OutputData = Entry.Value; + + // NOTE: Output objects are going to be empty here since they dissapear during actor reconstruction. + // We'll need to repopulate from the instance data. + + check(Outputs_DEPRECATED.IsValidIndex(OutputData.OutputIndex)); + UHoudiniOutput* Output = Outputs_DEPRECATED[OutputData.OutputIndex]; + check(Output); + TMap& OutputObjects = Output->GetOutputObjects(); + FHoudiniOutputObject NewObject = OutputData.OutputObject; + + if (OutputData.OutputObject.OutputComponents.Num() > 0) + { + // Update the output component reference. + check(CachedOutputNodes.Contains(ObjectId)) + const FGuid VariableGuid = CachedOutputNodes.FindChecked(ObjectId); + USCS_Node* SCSNode = SCS->FindSCSNodeByGuid(VariableGuid); + + if (SCSNode) + { + // Find the component that corresponds to the SCS node. + USceneComponent* SceneComponent = FindActorComponentByName(GetOwner(), SCSNode->GetVariableName()); + NewObject.OutputComponents[0] = SceneComponent; + } + else + { + NewObject.OutputComponents.Empty(); + } + } + + OutputObjects.Add(ObjectId, NewObject); + } + + if (CachedTemplateComponent.IsValid()) + { +#if WITH_EDITOR + CopyStateFromTemplateComponent( CachedTemplateComponent.Get(), false, false, true); +#endif + } + + AssetId_DEPRECATED = InstanceData->AssetId; + SubAssetIndex_DEPRECATED = InstanceData->SubAssetIndex; + ComponentGUID_DEPRECATED = InstanceData->ComponentGUID; + HapiGUID_DEPRECATED = InstanceData->HapiGUID; + + // Apply the previous HoudiniAsset to the component + // so that we can compare it against the template during CopyStateFromTemplate() calls to see whether it changed. + HoudiniAsset_DEPRECATED = InstanceData->HoudiniAsset; + + AssetCookCount_DEPRECATED = InstanceData->AssetCookCount; + bHasBeenLoaded_DEPRECATED = InstanceData->bHasBeenLoaded; + bHasBeenDuplicated_DEPRECATED = InstanceData->bHasBeenDuplicated; + bPendingDelete_DEPRECATED = InstanceData->bPendingDelete; + bRecookRequested_DEPRECATED = InstanceData->bRecookRequested; + bEnableCooking_DEPRECATED = InstanceData->bEnableCooking; + bForceNeedUpdate_DEPRECATED = InstanceData->bForceNeedUpdate; + bLastCookSuccess_DEPRECATED = InstanceData->bLastCookSuccess; + bHasRegisteredComponentTemplate = InstanceData->bRegisteredComponentTemplate; + + AssetState_DEPRECATED = InstanceData->AssetState; + + SetCanDeleteHoudiniNodes(false); + + } // if (!bPostUCS) + /* + else + { + // PostUCS + + } + */ +} + + +void +UHoudiniAssetBlueprintComponent::HoudiniEngineTick() +{ + if (!IsFullyLoaded()) + { + USimpleConstructionScript* SCS = GetSCS(); + if (SCS == nullptr) + { + OnFullyLoaded(); + } + else if (IsPreview()) + { + AActor* OwningActor = GetOwner(); + check(OwningActor); + + // If this is a *preview component*, it is important to wait for the template component to be fully loaded + // since it needs to be initialized so that the component instance can copy initial values from the template. + check(CachedTemplateComponent.Get()); + + if (CachedTemplateComponent->IsFullyLoaded()) + { +#if WITH_EDITOR + if(SCS->IsConstructingEditorComponents()) + { + // We're stuck in an editor blueprint construction / preview actor update. Wait some more. + } + else + { + OnFullyLoaded(); + } +#else + OnFullyLoaded(); +#endif + } + } + else + { + // Anything else can go onto being fully loaded at this point. + OnFullyLoaded(); + } + } +} + +void +UHoudiniAssetBlueprintComponent::OnFullyLoaded() +{ + Super::OnFullyLoaded(); + + // Check whether this component is inside a Blueprint editor. If this object lives outside the blueprint editor (in , then we need to ensure that this + // component won't be influencing the Blueprint asset. + + if (!IsTemplate()) + { +#if WITH_EDITOR + AActor* PreviewActor = GetPreviewActor(); +#else + AActor* PreviewActor = nullptr; +#endif + AActor* OwningActor = GetOwner(); + + if (!PreviewActor) + { + bIsInBlueprintEditor = false; + SetAssetState(EHoudiniAssetState::None); + return; + } + + if (OwningActor && PreviewActor != OwningActor) + { + bIsInBlueprintEditor = false; + SetAssetState(EHoudiniAssetState::None); + return; + } + } + + bIsInBlueprintEditor = true; + + CachePreviewState(); + CacheBlueprintData(); + + /* + for (UHoudiniOutput* Output : Outputs) + { + if (!Output) + continue; + } + */ + + if (IsTemplate()) + { + AssetId_DEPRECATED = -1; + AssetState_DEPRECATED = EHoudiniAssetState::ProcessTemplate; + } + + if (IsPreview()) + { + check(CachedTemplateComponent.Get()); + + // If this is a preview actor, sync initial settings from the component template +#if WITH_EDITOR + CopyStateFromTemplateComponent(CachedTemplateComponent.Get(), false, false, true); +#endif + + TemplatePropertiesChangedHandle = CachedTemplateComponent->OnParametersChangedEvent.AddUObject(this, &UHoudiniAssetBlueprintComponent::OnTemplateParametersChangedHandler); + if (bHoudiniAssetChanged) + { + + // The HoudiniAsset has changed, so we need to force the PreviewInstance to re-instantiate + AssetState_DEPRECATED = EHoudiniAssetState::NeedInstantiation; + bForceNeedUpdate_DEPRECATED = true; + bHoudiniAssetChanged = false; + // TODO: Make this better? + CachedTemplateComponent->bHoudiniAssetChanged = false; + } + + if (bHasRegisteredComponentTemplate) + { + // We have a newly registered component template. One of two things happened to cause this: + // 1. A new HoudiniAssetBlueprintComponent was created and registered. + // 2. The Blueprint Editor was closed / opened. + // The problem that arises in the #2 is that the template component was never fully unloaded + // from memory (it was deregistered but not destroyed). After deregistration we had the + // opportunity to invalidate asset/node ids but now that it has reregistered (without going + // through the normal initialization process) we will have to force a call to MarkAsNeedInstantiation + // during the next OnTemplateParametersChangedHandler() invocation. + bHasBeenLoaded_DEPRECATED = true; + } + } +} + +void +UHoudiniAssetBlueprintComponent::OnTemplateParametersChanged() +{ + OnParametersChangedEvent.Broadcast(this); +} + +void UHoudiniAssetBlueprintComponent::OnBlueprintStructureModified() +{ + check(IsTemplate()); + bBlueprintStructureModified = false; + +#if WITH_EDITOR + if (IsTemplate()) + { + FHoudiniEngineRuntimeUtils::MarkBlueprintAsStructurallyModified(this); + } + else + { + check(CachedTemplateComponent.IsValid()); + FHoudiniEngineRuntimeUtils::MarkBlueprintAsStructurallyModified(CachedTemplateComponent.Get()); + } +#endif +} + +void UHoudiniAssetBlueprintComponent::OnBlueprintModified() +{ + check(IsTemplate()); + bBlueprintModified = false; +#if WITH_EDITOR + FHoudiniEngineRuntimeUtils::MarkBlueprintAsModified(this); +#endif +} + +void +UHoudiniAssetBlueprintComponent::OnHoudiniAssetChanged() +{ + if (IsTemplate()) + { + // Invalidate data associated with this component since we're about to change and reinstantiate the Houdini Asset. + SetCanDeleteHoudiniNodes(true); + InvalidateData(); + SetCanDeleteHoudiniNodes(false); +#if WITH_EDITORONLY_DATA + Parameters_DEPRECATED.Empty(); + Inputs_DEPRECATED.Empty(); +#endif + } + + Super::OnHoudiniAssetChanged(); + + // Set on template components, then copied to preview components, and + // then used (and reset) during OnFullyLoaded. + bHoudiniAssetChanged = true; +} + +void +UHoudiniAssetBlueprintComponent::RegisterHoudiniComponent(UHoudiniAssetComponent *InComponent) +{ + // We only want to register this component if it is the preview actor for the Blueprint editor. +#if WITH_EDITOR + AActor* PreviewActor = GetPreviewActor(); +#else + AActor* PreviewActor = nullptr; +#endif + AActor* OwningActor = GetOwner(); + if (!OwningActor) + return; + + if (PreviewActor != OwningActor) + return; + + Super::RegisterHoudiniComponent(InComponent); +} + + + + +//bool UHoudiniAssetBlueprintComponent::TickInitialization() +//{ +// return true; +// +// if (IsFullyLoaded()) +// return true; +// +// bool bHasFinishedLoading = Super::TickInitialization(); +// +// if (!bHasFinishedLoading) +// return false; +// +// if (!IsTemplate()) +// { +// +// if (CachedTemplateComponent.Get()) +// { +// // Now that that SCS has finished constructing editor components, we can continue. +// // Copy the current state from the template component, in case there is something that can be processed. +// CopyStateFromTemplateComponent(CachedTemplateComponent.Get()); +// } +// +// AssetStateResult = EHoudiniAssetStateResult::None; +// AssetState = EHoudiniAssetState::None; +// +// bForceNeedUpdate = true; +// AssetState = EHoudiniAssetState::PostCook; +// } +// +// return true; +//} + +template +inline void +UHoudiniAssetBlueprintComponent::SetTypedValueAt(const FString& Name, ValueT& Value, int Index) +{ + ParamT* Parameter = Cast(FindParameterByName(Name)); + if (!Parameter) + return; + + Parameter->SetValueAt(Value, Index); +} + +bool +UHoudiniAssetBlueprintComponent::HasParameter(FString Name) +{ + return FindParameterByName(Name) != nullptr; +} + +void +UHoudiniAssetBlueprintComponent::SetFloatParameter(FString Name, float Value, int Index) +{ + SetTypedValueAt(Name, Value, Index); +} + +void +UHoudiniAssetBlueprintComponent::SetToggleValueAt(FString Name, bool Value, int Index) +{ + UHoudiniParameterToggle* Parameter = Cast(FindParameterByName(Name)); + if (!Parameter) + return; + + Parameter->SetValueAt(Value, Index); +} + +//void UHoudiniAssetBlueprintComponent::OnPostCookHandler(UHoudiniAssetComponent* InComponent) +//{ +// +// // Before this component handles any translation, we need to make sure that it still belongs to the editor actor. +// // When a blueprint gets recompiled, the editor actor gets replaced with a new one but the old actor has not yet +// // been ftroyed / garbage collected so its components still receive cook events from the template. +// +// UHoudiniAssetBlueprintComponent* ComponentTemplate = Cast(InComponent); +// if (!IsValid(ComponentTemplate)) +// return; +// +// CopyStateFromTemplateComponent(ComponentTemplate); +// bForceNeedUpdate = true; +//} + +void +UHoudiniAssetBlueprintComponent::OnTemplateParametersChangedHandler(UHoudiniAssetComponent* InComponentTemplate) +{ + if (!(AssetState_DEPRECATED == EHoudiniAssetState::None || AssetState_DEPRECATED == EHoudiniAssetState::NeedInstantiation || AssetState_DEPRECATED == EHoudiniAssetState::NeedRebuild)) + // Don't process parameter changes since we're already cooking -- it is going to break things badly if we do. + return; + + if (!IsValidComponent()) + return; + + UHoudiniAssetBlueprintComponent* ComponentTemplate = Cast(InComponentTemplate); + if (!ComponentTemplate) + return; + + // The component instance needs to copy values from the template. + bool bBlueprintStructureChanged = false; +#if WITH_EDITOR + CopyDetailsFromComponent(ComponentTemplate, + false, + false, + true, + false, + true, + bBlueprintStructureChanged, + RF_Public, + RF_ClassDefaultObject|RF_ArchetypeObject); +#endif + + SetCanDeleteHoudiniNodes(false); + + if (bHasRegisteredComponentTemplate) + { + // NOTE: It is very important to call this *after* CopyDetailsFromComponent(), since CopyDetailsFromComponent + // will clobber the inputs and parameter states on this component. + + // If we already have a valid asset id, keep it. + if (AssetId_DEPRECATED >= 0) + { + MarkAsNeedCook(); + } + else + { + MarkAsNeedInstantiation(); + } + + bHasRegisteredComponentTemplate = false; + bFullyLoaded_DEPRECATED = true; // MarkAsNeedInstantiation sets this to false. Force to true. + // While MarkAsNeedInstantiation() sets ParametersChanged to true, it does not + // set the 'NeedToTriggerUpdate' flag (both of which needs to be true in order + // to trigger an HDA update) so we are going to force NeedUpdate() to return true + // in order to get an initial cook. + bForceNeedUpdate_DEPRECATED = true; + } + + bUpdatedFromTemplate = true; +} + +void +UHoudiniAssetBlueprintComponent::InvalidateData() +{ + if (IsTemplate()) + { + // Ensure transient properties are invalidated/released for parameters, inputs and outputs as if the + // the object was undergoing destruction since the template component will likely be reregistered + // without being destroyed. +#if WITH_EDITORONLY_DATA + for(UHoudiniParameter* Param : Parameters_DEPRECATED) + { + Param->InvalidateData(); + } +#endif + +#if WITH_EDITORONLY_DATA + for(UHoudiniInput* Input : Inputs_DEPRECATED) + { + Input->InvalidateData(); + } +#endif + + FHoudiniEngineRuntime::Get().MarkNodeIdAsPendingDelete(AssetId_DEPRECATED, true); + AssetId_DEPRECATED = -1; + } +} + +//void UHoudiniAssetBlueprintComponent::OnTemplateHoudiniAssetChangedHandler(UHoudiniAssetComponent* InComponentTemplate) +//{ +// +// UHoudiniAssetBlueprintComponent* ComponentTemplate = Cast(InComponentTemplate); +// if (!ComponentTemplate) +// return; +// check(IsPreview()); +// +// // The Houdini Asset was changed on the template. We need to recook. +// AssetState = EHoudiniAssetState::NeedInstantiation; +// +//} + +USceneComponent* +UHoudiniAssetBlueprintComponent::FindOwnerComponentByName(FName ComponentName) const +{ + AActor* Owner = GetOwner(); + if (!Owner) + return nullptr; + + return FindActorComponentByName(Owner, ComponentName); +} + +USceneComponent* +UHoudiniAssetBlueprintComponent::FindActorComponentByName(AActor* InActor, FName ComponentName) const +{ + const TSet& Components = InActor->GetComponents(); + + for (UActorComponent* Component : Components) + { + USceneComponent* SceneComponent = Cast(Component); + if (!IsValid(SceneComponent)) + continue; + if (FName(SceneComponent->GetName()) == ComponentName) + return SceneComponent; + } + + return nullptr; +} + +bool UHoudiniAssetBlueprintComponent::GetInputObjectSCSVariableGuid(const FGuid& InputGuid, FGuid& OutSCSGuid) +{ + FGuid* SCSGuid = CachedInputNodes.Find(InputGuid); + if (!SCSGuid) + return false; + OutSCSGuid = *SCSGuid; + return true; +} + +USCS_Node* +UHoudiniAssetBlueprintComponent::FindSCSNodeForTemplateComponent(USimpleConstructionScript* SCS, const UActorComponent* InComponent) const +{ + const TArray& AllNodes = SCS->GetAllNodes(); + + for (USCS_Node* Node : AllNodes) + { + if (!Node) + continue; + + if (Node->ComponentTemplate == InComponent) + return Node; + } + + return nullptr; +} + +USCS_Node* +UHoudiniAssetBlueprintComponent::FindSCSNodeForTemplateComponentInClassHierarchy( + const UActorComponent* InComponent) const +{ + UObject* Outer = this->GetOuter(); + if (!IsValid(Outer)) + return nullptr; + UBlueprintGeneratedClass* MainBPGC; + if (IsTemplate()) + { + MainBPGC = Cast(Outer); + } + else + { + AActor* OwningActor = GetOwner(); + MainBPGC = Cast(OwningActor->GetClass()); + } + + check(MainBPGC); + TArray BPGCStack; + UBlueprintGeneratedClass::GetGeneratedClassesHierarchy(MainBPGC, BPGCStack); + for(const UBlueprintGeneratedClass* BPGC : BPGCStack) + { + USimpleConstructionScript* SCS = BPGC->SimpleConstructionScript; + if (!SCS) + return nullptr; + USCS_Node* SCSNode = FindSCSNodeForTemplateComponent(SCS, InComponent); + SCSNode = SCS->FindSCSNode(InComponent->GetFName()); + if (SCSNode) + return SCSNode; + } + + return nullptr; +} + +#if WITH_EDITOR +USCS_Node* +UHoudiniAssetBlueprintComponent::FindSCSNodeForInstanceComponent(USimpleConstructionScript* SCS, const UActorComponent* InComponent) const +{ + const TArray& AllNodes = SCS->GetAllNodes(); + + if (!InComponent) + return nullptr; + + for (USCS_Node* Node : AllNodes) + { + if (!Node) + continue; + if (Node->EditorComponentInstance.Get() == InComponent) + return Node; + } + + return nullptr; +} +#endif + +UActorComponent* +UHoudiniAssetBlueprintComponent::FindComponentInstanceInActor(const AActor* InActor, + USCS_Node* SCSNode) const +{ + UActorComponent* ComponentTemplate = SCSNode->ComponentTemplate; + + UActorComponent* ComponentInstance = NULL; + if (InActor != NULL) + { + if (SCSNode != NULL) + { + FName VariableName = SCSNode->GetVariableName(); + if (VariableName != NAME_None) + { + UWorld* World = InActor->GetWorld(); + FObjectPropertyBase* Property = FindFProperty(InActor->GetClass(), VariableName); + if (Property != NULL) + { + // Return the component instance that's stored in the property with the given variable name + ComponentInstance = Cast(Property->GetObjectPropertyValue_InContainer(InActor)); + } + else if (World != nullptr && World->WorldType == EWorldType::EditorPreview) + { + // If this is the preview actor, return the cached component instance that's being used for the pmnaview actor prior to recompiling the Blueprint +#if WITH_EDITOR + ComponentInstance = SCSNode->EditorComponentInstance.Get(); +#endif + } + } + } + else if (ComponentTemplate != NULL) + { +#if WITH_EDITOR + TInlineComponentArray Components; + InActor->GetComponents(Components); + ComponentInstance = FComponentEditorUtils::FindMatchingComponent(ComponentTemplate, Components); +#endif + } + } + + return ComponentInstance; +} + + +//void UHoudiniAssetBlueprintComponent::OnOutputProcessingCompletedHandler(UHoudiniAssetComponent* InComponent) +//{ +// +// UHoudiniAssetBlueprintComponent* TemplateComponent = Cast(InComponent); +// if (!IsValid(TemplateComponent)) +// return; +// +// CopyStateFromComponent(TemplateComponent); +// bForceNeedUpdate = true; +//} + +//#if WITH_EDITOR +//void UHoudiniAssetBlueprintComponent::ReceivedAssetEditorRequestCloseEvent(UObject* Asset, EAssetEditorCloseReason CloseReason) +//{ +// +// if (CachedBlueprint.Get()) +// { +// } +// +// if (Asset) +// { +// +// } +// +//} +//#endif + +void +UHoudiniAssetBlueprintComponent::CachePreviewState() +{ + bCachedIsPreview = false; + +#if WITH_EDITOR + AActor* ComponentOwner = GetOwner(); + if (!IsValid(ComponentOwner)) + return; + + USimpleConstructionScript* SCS = GetSCS(); + if (SCS == nullptr) + return; + + // Get the preview actor directly from the BlueprintEditor. + FBlueprintEditor* BlueprintEditor = FHoudiniEngineRuntimeUtils::GetBlueprintEditor(this); + if (BlueprintEditor) + { + AActor* PreviewActor = BlueprintEditor->GetPreviewActor(); + if (PreviewActor == ComponentOwner) + { + bCachedIsPreview = true; + return; + } + } +#endif +} + +void +UHoudiniAssetBlueprintComponent::CacheBlueprintData() +{ + CachedBlueprint = nullptr; + CachedActorCDO = nullptr; + CachedTemplateComponent = IsTemplate() ? this : nullptr; + +#if WITH_EDITOR + CachedAssetEditorSubsystem = GEditor->GetEditorSubsystem(); +#endif + +#if WITH_EDITORONLY_DATA + UBlueprintGeneratedClass* BPGC = Cast(GetOuter()); + if (BPGC) + { + // Dealing with a component template + CachedBlueprint = Cast(BPGC->ClassGeneratedBy); + } + else + { + // Dealing with a component instance. + CachedBlueprint = Cast(GetOuter()->GetClass()->ClassGeneratedBy); + } +#endif + + if (CreationMethod != EComponentCreationMethod::SimpleConstructionScript) + return; + + AActor* ComponentOwner = this->GetOwner(); + if (!IsValid(ComponentOwner)) + return; + UClass* OwnerClass = ComponentOwner->GetClass(); + if (!IsValid(OwnerClass)) + return; + + if (!IsTemplate()) + { + // NOTE: The following code allows us to find the component template from an instance. + CachedActorCDO = Cast< AActor >(CachedBlueprint->GeneratedClass->GetDefaultObject()); + if (!CachedActorCDO.IsValid() || (CachedActorCDO.Get() == ComponentOwner)) + return; +#if WITH_EDITOR + UActorComponent* TargetComponent = EditorUtilities::FindMatchingComponentInstance(this, CachedActorCDO.Get()); + CachedTemplateComponent = Cast(TargetComponent); +#endif + } + +} + +USimpleConstructionScript* +UHoudiniAssetBlueprintComponent::GetSCS() const +{ + if (!CachedBlueprint.Get()) + return nullptr; + + return CachedBlueprint->SimpleConstructionScript; +} + +//------------------------------------------------------------------------------------------------ +// FHoudiniAssetBlueprintInstanceData +//------------------------------------------------------------------------------------------------ + +FHoudiniAssetBlueprintInstanceData::FHoudiniAssetBlueprintInstanceData() + : HoudiniAsset(nullptr) + , AssetId(-1) + , AssetState(EHoudiniAssetState::None) + , SubAssetIndex(-1) + , AssetCookCount(0) + , bHasBeenLoaded(false) + , bHasBeenDuplicated(false) + , bPendingDelete(false) + , bRecookRequested(false) + , bRebuildRequested(false) + , bEnableCooking(true) + , bForceNeedUpdate(false) + , bLastCookSuccess(false) + , ComponentGUID(FGuid()) + , HapiGUID(FGuid()) + , bRegisteredComponentTemplate(false) + , SourceName() +{ + +} + +FHoudiniAssetBlueprintInstanceData::FHoudiniAssetBlueprintInstanceData(const UHoudiniAssetBlueprintComponent* SourceComponent) + : FActorComponentInstanceData(SourceComponent) +{ + +} + +void +FHoudiniAssetBlueprintInstanceData::AddReferencedObjects(FReferenceCollector & Collector) +{ + Super::AddReferencedObjects(Collector); + // TODO: Do we need to add references to output objects here? + // Any other references? + // What are the implications? +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniAssetBlueprintComponent.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#include "Delegates/IDelegateInstance.h" +#include "Engine/Blueprint.h" +#include "HoudiniAssetComponent.h" + +#if WITH_EDITOR + #include "Subsystems/AssetEditorSubsystem.h" +#endif + +#include "HoudiniAssetBlueprintComponent.generated.h" + +class USCS_Node; + +UCLASS(NotBlueprintType, Experimental, meta=(BlueprintSpawnableComponent, DisplayName="Houdini Asset")) +class HOUDINIENGINERUNTIME_API UHoudiniAssetBlueprintComponent : public UHoudiniAssetComponent +{ + GENERATED_BODY() + +public: + UHoudiniAssetBlueprintComponent(const FObjectInitializer & ObjectInitializer); + +#if WITH_EDITOR + // Sync certain variables of this HoudiniAssetComponent to the blueprint generated class. + // This is typically used when the Blueprint definition is being edited and the HAC cook + // took place in a transient HoudiniAssetComponent. Certain properties needs to be copied + // from the transient component back to the Blueprint generated class in order to be retained + // as part of the Client MeetingBlueprint definition. + + void CopyStateToTemplateComponent(); + + void CopyStateFromTemplateComponent(UHoudiniAssetBlueprintComponent* FromComponent, const bool bClearFromInputs, const bool bClearToInputs, const bool bCopyInputObjectComponentProperties); + + void CopyDetailsFromComponent( + UHoudiniAssetBlueprintComponent* FromComponent, + const bool bCreateSCSNodes, + const bool bClearChangedToInputs, + const bool bClearChangedFromInputs, + const bool bInCanDeleteHoudiniNodes, + const bool bCopyInputObjectComponentProperties, + bool &bOutBlueprintStructureChanged, + EObjectFlags SetFlags=RF_NoFlags, + EObjectFlags ClearFlags=RF_NoFlags); + + // Update references on ToInput by looking up component references on FromInput in the SCS graph, on locating the correct component for ToInput. + void UpdateInputObjectComponentReferences( + USimpleConstructionScript* SCS, + UHoudiniInput* FromInput, + UHoudiniInput* ToInput, + const bool bCopyInputObjectProperties, + const bool bCreateMissingSCSNodes=false, + USCS_Node* ParentSCSNode=nullptr, + bool* bOutSCSNodeCreated=nullptr); + + virtual bool HasOpenEditor() const override; + IAssetEditorInstance* FindEditorInstance() const; + AActor* GetPreviewActor() const; +#endif + + virtual UHoudiniAssetComponent* GetCachedTemplate() const override; + + //------------------------------------------------------------------------------------------------ + // Supported Features + //------------------------------------------------------------------------------------------------ + + // Some features may be unavaible depending on the context in which the Houdini Asset Component + // has been instantiated. + + virtual bool CanDeleteHoudiniNodes() const override; + + void SetCanDeleteHoudiniNodes(bool bInCanDeleteNodes); + + virtual bool IsValidComponent() const override; + + virtual bool IsInputTypeSupported(EHoudiniInputType InType) const override; + virtual bool IsOutputTypeSupported(EHoudiniOutputType InType) const override; + + virtual bool IsProxyStaticMeshEnabled() const override; + + //------------------------------------------------------------------------------------------------ + // Notifications + //------------------------------------------------------------------------------------------------ + + //virtual void BroadcastPreAssetCook() override; + virtual void OnPrePreCook() override; + virtual void OnPostPreCook() override; + virtual void OnPreOutputProcessing() override; + virtual void OnPostOutputProcessing() override; + virtual void OnPrePreInstantiation() override; + virtual void NotifyHoudiniRegisterCompleted() override; + virtual void NotifyHoudiniPreUnregister() override; + virtual void NotifyHoudiniPostUnregister() override; + + //------------------------------------------------------------------------------------------------ + // UActorComponent overrides + //------------------------------------------------------------------------------------------------ +#if WITH_EDITOR + virtual void OnComponentCreated() override; +#endif + + virtual void OnRegister() override; + + virtual void BeginDestroy() override; + virtual void DestroyComponent(bool bPromoteChildren = false) override; + + virtual void OnComponentDestroyed(bool bDestroyingHierarchy) override; + // Refer USplineComponent for a decent reference on how to use component instance data. + virtual TStructOnScope GetComponentInstanceData() const override; + void ApplyComponentInstanceData(struct FHoudiniAssetBlueprintInstanceData* ComponentInstanceData, const bool bPostUCS); + + //------------------------------------------------------------------------------------------------ + // UHoudiniAssetComponent overrides + //------------------------------------------------------------------------------------------------ + + FHoudiniAssetComponentEvent OnParametersChangedEvent; + FHoudiniAssetComponentEvent OnHoudiniAssetChangedEvent; + + virtual void HoudiniEngineTick() override; + virtual void OnFullyLoaded() override; + virtual void OnTemplateParametersChanged() override; + virtual void OnHoudiniAssetChanged() override; + virtual void RegisterHoudiniComponent(UHoudiniAssetComponent* InComponent) override; + + virtual void OnBlueprintStructureModified() override; + virtual void OnBlueprintModified() override; + + + //------------------------------------------------------------------------------------------------ + // Blueprint functions + //------------------------------------------------------------------------------------------------ + + UFUNCTION(BlueprintCallable, Category="Houdini Asset Component") + bool HasParameter(FString Name); + + UFUNCTION(BlueprintCallable, Category="Houdini Asset Component") + void SetFloatParameter(FString Name, float Value, int Index=0); + + UFUNCTION(BlueprintCallable, Category="Houdini Asset Component") + void SetToggleValueAt(FString Name, bool Value, int Index=0); + + void AddInputObjectMapping(const FGuid& InputGuid, const FGuid& SCSVariableGuid) { CachedInputNodes.Add(InputGuid, SCSVariableGuid); } + bool GetInputObjectSCSVariableGuid(const FGuid& InputGuid, FGuid& OutSCSGuid); + void RemoveInputObjectSCSVariableGuid(const FGuid& InputGuid) { CachedInputNodes.Remove(InputGuid); }; + + USCS_Node* FindSCSNodeForTemplateComponent(USimpleConstructionScript* SCS, const UActorComponent* InComponent) const; + USCS_Node* FindSCSNodeForTemplateComponentInClassHierarchy(const UActorComponent* InComponent) const; +#if WITH_EDITOR + USCS_Node* FindSCSNodeForInstanceComponent(USimpleConstructionScript* SCS, const UActorComponent* InComponent) const; +#endif // WITH_EDITOR + UActorComponent* FindComponentInstanceInActor(const AActor* InActor, USCS_Node* SCSNode) const; + +protected: + + template + void SetTypedValueAt(const FString& Name, ValueT& Value, int Index=0); + + void OnTemplateParametersChangedHandler(UHoudiniAssetComponent* ComponentTemplate); + void InvalidateData(); + + USceneComponent* FindOwnerComponentByName(FName ComponentName) const; + USceneComponent* FindActorComponentByName(AActor * InActor, FName ComponentName) const; + + void CachePreviewState(); + void CacheBlueprintData(); + + USimpleConstructionScript* GetSCS() const; + + //// The output translation has finished. + //void OnOutputProcessingCompletedHandler(UHoudiniAssetComponent * InComponent); + +#if WITH_EDITOR + //void ReceivedAssetEditorRequestCloseEvent(UObject* Asset, EAssetEditorCloseReason CloseReason); + TWeakObjectPtr CachedAssetEditorSubsystem; +#endif + + TWeakObjectPtr CachedBlueprint; + TWeakObjectPtr CachedActorCDO; + TWeakObjectPtr CachedTemplateComponent; + + /*UPROPERTY(DuplicateTransient) + bool bOutputsRequireUpdate;*/ + UPROPERTY() + bool FauxBPProperty; + + UPROPERTY() + bool bHoudiniAssetChanged; + + UPROPERTY(Transient, DuplicateTransient) + bool bUpdatedFromTemplate; + + UPROPERTY() + bool bIsInBlueprintEditor; + + UPROPERTY(Transient, DuplicateTransient) + bool bCanDeleteHoudiniNodes; + + UPROPERTY(Transient, DuplicateTransient) + bool bHasRegisteredComponentTemplate; + + FDelegateHandle TemplatePropertiesChangedHandle; + + // This is used to keep track of which SCS variable names correspond to which + // output objects. + // This seems like it will cause issues in the map. + UPROPERTY() + TMap CachedOutputNodes; + + // This is used to keep track of which (SCS) variable guids correspond to which + // input objects. + UPROPERTY() + TMap CachedInputNodes; +}; + + +///** Used to keep track of output data and mappings during reconstruction */ +USTRUCT() +struct FHoudiniAssetBlueprintOutput +{ + GENERATED_BODY() + + UPROPERTY() + int32 OutputIndex; + + UPROPERTY() + FHoudiniOutputObject OutputObject; + + FHoudiniAssetBlueprintOutput() + : OutputIndex(INDEX_NONE) + { + + } +}; + + +/** Used to store HoudiniAssetComponent data during BP reconstruction */ +USTRUCT() +struct FHoudiniAssetBlueprintInstanceData : public FActorComponentInstanceData +{ + GENERATED_BODY() +public: + FHoudiniAssetBlueprintInstanceData(); + FHoudiniAssetBlueprintInstanceData(const UHoudiniAssetBlueprintComponent* SourceComponent); + + virtual ~FHoudiniAssetBlueprintInstanceData() = default; + + /*virtual bool ContainsData() const override + { + return (HAC != nullptr) || Super::ContainsData(); + }*/ + + virtual void ApplyToComponent(UActorComponent* Component, const ECacheApplyPhase CacheApplyPhase) override + { + Super::ApplyToComponent(Component, CacheApplyPhase); + CastChecked(Component)->ApplyComponentInstanceData(this, (CacheApplyPhase == ECacheApplyPhase::PostUserConstructionScript)); + } + + virtual void AddReferencedObjects(FReferenceCollector& Collector) override; + + // Persist all the required properties for being able to recook the HoudiniAsset from its existing state. + UPROPERTY() + TObjectPtr HoudiniAsset; + + UPROPERTY() + int32 AssetId; + + UPROPERTY() + EHoudiniAssetState AssetState; + + // Subasset index + UPROPERTY() + uint32 SubAssetIndex; + + UPROPERTY() + uint32 AssetCookCount; + + UPROPERTY() + bool bHasBeenLoaded; + + UPROPERTY() + bool bHasBeenDuplicated; + + UPROPERTY() + bool bPendingDelete; + + UPROPERTY() + bool bRecookRequested; + + UPROPERTY() + bool bRebuildRequested; + + UPROPERTY() + bool bEnableCooking; + + UPROPERTY() + bool bForceNeedUpdate; + + UPROPERTY() + bool bLastCookSuccess; + + UPROPERTY() + FGuid ComponentGUID; + + UPROPERTY() + FGuid HapiGUID; + + UPROPERTY() + bool bRegisteredComponentTemplate; + + // Name of the component from which this + // data was copied. Used for debugging purposes. + UPROPERTY() + FString SourceName; + + UPROPERTY() + TMap Outputs; + + UPROPERTY() + TArray> Inputs; +}; + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniAssetComponent.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniAssetComponent.h" + +#include "HoudiniEngineRuntimePrivatePCH.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetActor.h" +#include "HoudiniCookable.h" +#include "HoudiniInput.h" +#include "HoudiniNodeSyncComponent.h" +#include "HoudiniOutput.h" +#include "HoudiniParameter.h" +#include "HoudiniParameterButton.h" +#include "HoudiniParameterButtonStrip.h" +#include "HoudiniParameterOperatorPath.h" +#include "HoudiniHandleComponent.h" +#include "HoudiniPDGAssetLink.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniStaticMeshComponent.h" +#include "HoudiniInstancedActorComponent.h" + +#if WITH_EDITOR +#include "HoudiniEditorAssetStateSubsystemInterface.h" +#endif + +#include "Engine/StaticMesh.h" +#include "Components/StaticMeshComponent.h" +#include "TimerManager.h" +#include "Landscape.h" +#include "Components/HierarchicalInstancedStaticMeshComponent.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "Components/SplineComponent.h" +#include "InstancedFoliageActor.h" +#include "UObject/DevObjectVersion.h" +#include "Serialization/CustomVersion.h" +#include "PhysicsEngine/BodySetup.h" +#include "UObject/UObjectGlobals.h" +#include "BodySetupEnums.h" +#include "PhysicalMaterials/PhysicalMaterial.h" +#include "Materials/MaterialInstance.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "GeometryCollection/GeometryCollectionComponent.h" +#else + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionComponent.h" +#endif + +#include "PrimitiveSceneProxy.h" + +#if WITH_EDITOR + #include "Editor/UnrealEd/Private/GeomFitUtils.h" +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + #include "UObject/Linker.h" +#endif + +#include "ComponentReregisterContext.h" +#include "HoudiniLandscapeRuntimeUtils.h" +#if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 0) + #include "LevelInstance/LevelInstanceInterface.h" +#endif +#include "LevelInstance/LevelInstanceSubsystem.h" + +// Macro to update given properties on all children components of the HAC. +#define HOUDINI_UPDATE_ALL_CHILD_COMPONENTS( COMPONENT_CLASS, PROPERTY ) \ + do \ + { \ + TArray ReregisterComponents; \ + TArray LocalAttachChildren;\ + GetChildrenComponents(true, LocalAttachChildren); \ + for (TArray::TConstIterator Iter(LocalAttachChildren); Iter; ++Iter) \ + { \ + COMPONENT_CLASS * Component = Cast(*Iter); \ + if (Component) \ + { \ + Component->PROPERTY = PROPERTY; \ + ReregisterComponents.Add(Component); \ + } \ + } \ + \ + if (ReregisterComponents.Num() > 0) \ + { \ + FMultiComponentReregisterContext MultiComponentReregisterContext(ReregisterComponents); \ + } \ + } \ + while(0) + + +void +UHoudiniAssetComponent::Serialize(FArchive& Ar) +{ + Ar.UsingCustomVersion(FHoudiniCustomSerializationVersion::GUID); + + bool bLegacyComponent = false; + bool bV2Component = false; + if (Ar.IsLoading()) + { + int32 Ver = Ar.CustomVer(FHoudiniCustomSerializationVersion::GUID); + if (Ver < VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_V2_BASE && Ver >= VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_BASE) + { + bLegacyComponent = true; + } + else if (Ver < VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_V3_BASE) + { + // V2 HAC component - enable this so we can transfer our data to the cookable during PostLoad() + bMigrateDataToCookableOnPostLoad = true; + + HOUDINI_LOG_MESSAGE(TEXT("Loading deprecated version of UHoudiniAssetComponent : V2 HAC will be converted to Cookable.")); + } + } + + // + Super::Serialize(Ar); + + if (bLegacyComponent) + { + int64 InitialOffset = Ar.Tell(); + + // We will just skip the v1 HAC data + HOUDINI_LOG_WARNING(TEXT("Loading deprecated version of UHoudiniAssetComponent : serialization will be skipped.")); + + // Skip old Serialized data + if (FLinker* Linker = Ar.GetLinker()) + { + int32 const ExportIndex = this->GetLinkerIndex(); + FObjectExport& Export = Linker->ExportMap[ExportIndex]; + Ar.Seek(InitialOffset + Export.SerialSize); + return; + } + } +} + + +UHoudiniAssetComponent::UHoudiniAssetComponent(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) +{ + HoudiniAsset_DEPRECATED = nullptr; + bCookOnParameterChange_DEPRECATED = true; + bUploadTransformsToHoudiniEngine_DEPRECATED = true; + bCookOnTransformChange_DEPRECATED = false; + //bUseNativeHoudiniMaterials = true; + bCookOnAssetInputCook_DEPRECATED = true; + + AssetId_DEPRECATED = -1; + AssetState_DEPRECATED = EHoudiniAssetState::NewHDA; + AssetStateResult_DEPRECATED = EHoudiniAssetStateResult::None; + AssetCookCount_DEPRECATED = 0; + + SubAssetIndex_DEPRECATED = -1; + + // Make an invalid GUID, since we do not have any cooking requests. + HapiGUID_DEPRECATED.Invalidate(); + + HapiAssetName_DEPRECATED = FString(); + + // Create unique component GUID. + ComponentGUID_DEPRECATED = FGuid::NewGuid(); + LastComponentTransform_DEPRECATED = FTransform(); + + bUploadTransformsToHoudiniEngine_DEPRECATED = true; + + bHasBeenLoaded_DEPRECATED = false; + bHasBeenDuplicated_DEPRECATED = false; + bPendingDelete_DEPRECATED = false; + bRecookRequested_DEPRECATED = false; + bRebuildRequested_DEPRECATED = false; + bEnableCooking_DEPRECATED = true; + bForceNeedUpdate_DEPRECATED = false; + bLastCookSuccess_DEPRECATED = false; + bBlueprintStructureModified = false; + bBlueprintModified = false; + + //bEditorPropertiesNeedFullUpdate = true; + + // Folder used for cooking, the value is initialized by Output Translator + // TemporaryCookFolder.Path = HAPI_UNREAL_DEFAULT_TEMP_COOK_FOLDER; + + // Folder used for baking this asset's outputs, the value is initialized by Output Translator + // BakeFolder.Path = HAPI_UNREAL_DEFAULT_BAKE_FOLDER; + + bHasComponentTransformChanged_DEPRECATED = false; + + bFullyLoaded_DEPRECATED = false; + + bOutputless_DEPRECATED = false; + bOutputTemplateGeos_DEPRECATED = false; + bUseOutputNodes_DEPRECATED = true; + PDGAssetLink_DEPRECATED = nullptr; + + bOverrideGlobalProxyStaticMeshSettings_DEPRECATED = false; + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings) + { + bEnableProxyStaticMeshOverride_DEPRECATED = HoudiniRuntimeSettings->bEnableProxyStaticMesh; + bEnableProxyStaticMeshRefinementByTimerOverride_DEPRECATED = HoudiniRuntimeSettings->bEnableProxyStaticMeshRefinementByTimer; + ProxyMeshAutoRefineTimeoutSecondsOverride_DEPRECATED = HoudiniRuntimeSettings->ProxyMeshAutoRefineTimeoutSeconds; + bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride_DEPRECATED = HoudiniRuntimeSettings->bEnableProxyStaticMeshRefinementOnPreSaveWorld; + bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride_DEPRECATED = HoudiniRuntimeSettings->bEnableProxyStaticMeshRefinementOnPreBeginPIE; + } + else + { + bEnableProxyStaticMeshOverride_DEPRECATED = false; + bEnableProxyStaticMeshRefinementByTimerOverride_DEPRECATED = true; + ProxyMeshAutoRefineTimeoutSecondsOverride_DEPRECATED = 10.0f; + bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride_DEPRECATED = true; + bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride_DEPRECATED = true; + } + + bNoProxyMeshNextCookRequested_DEPRECATED = false; + BakeAfterNextCook_DEPRECATED = EHoudiniBakeAfterNextCook::Disabled; + +#if WITH_EDITORONLY_DATA + bGenerateMenuExpanded_DEPRECATED = true; + bBakeMenuExpanded_DEPRECATED = true; + bAssetOptionMenuExpanded_DEPRECATED = true; + bHelpAndDebugMenuExpanded_DEPRECATED = true; + + HoudiniEngineBakeOption_DEPRECATED = EHoudiniEngineBakeOption::ToActor; + + bRemoveOutputAfterBake_DEPRECATED = false; + bRecenterBakedActors_DEPRECATED = false; + bReplacePreviousBake_DEPRECATED = false; + ActorBakeOption_DEPRECATED = EHoudiniEngineActorBakeOption::OneActorPerComponent; + bAllowPlayInEditorRefinement_DEPRECATED = false; + bNeedToUpdateEditorProperties_DEPRECATED = false; + bLandscapeUseTempLayers_DEPRECATED = false; + bEnableCurveEditing_DEPRECATED = false; +#endif + + // + // Set component properties. + // + + Mobility = EComponentMobility::Static; + + SetGenerateOverlapEvents(false); + + // Similar to UMeshComponent. + CastShadow = true; + bUseAsOccluder = true; + bCanEverAffectNavigation = true; + + // This component requires render update. + bNeverNeedsRenderUpdate = false; + + Bounds = FBox(ForceInitToZero); + + LastTickTime_DEPRECATED = 0.0; + LastLiveSyncPingTime_DEPRECATED = 0.0; + + // Initialize the default SM Build settings with the plugin's settings default values + StaticMeshBuildSettings_DEPRECATED = FHoudiniEngineRuntimeUtils::GetDefaultMeshBuildSettings(); + + //bWantsOnUpdateTransform = true; + + bIsPDGAssetLinkInitialized_DEPRECATED = false; + + bMigrateDataToCookableOnPostLoad = false; +} + +UHoudiniAssetComponent::~UHoudiniAssetComponent() +{ + // Unregister ourself so our houdini node can be delete. + + // This gets called in UnRegisterHoudiniComponent, with appropriate checks. Don't call it here. + //FHoudiniEngineRuntime::Get().MarkNodeIdAsPendingDelete(AssetId, true); + + //FHoudiniEngineRuntime::Get().UnRegisterHoudiniCookable(GetCookable()); +} + +void UHoudiniAssetComponent::PostInitProperties() +{ + Super::PostInitProperties(); + + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (HoudiniRuntimeSettings) + { + // Copy default static mesh generation parameters from settings. + StaticMeshGenerationProperties_DEPRECATED.bGeneratedDoubleSidedGeometry = HoudiniRuntimeSettings->bDoubleSidedGeometry; + StaticMeshGenerationProperties_DEPRECATED.GeneratedPhysMaterial = HoudiniRuntimeSettings->PhysMaterial; + StaticMeshGenerationProperties_DEPRECATED.DefaultBodyInstance = HoudiniRuntimeSettings->DefaultBodyInstance; + StaticMeshGenerationProperties_DEPRECATED.GeneratedCollisionTraceFlag = HoudiniRuntimeSettings->CollisionTraceFlag; + StaticMeshGenerationProperties_DEPRECATED.GeneratedLightMapResolution = HoudiniRuntimeSettings->LightMapResolution; + StaticMeshGenerationProperties_DEPRECATED.GeneratedLightMapCoordinateIndex = HoudiniRuntimeSettings->LightMapCoordinateIndex; + StaticMeshGenerationProperties_DEPRECATED.bGeneratedUseMaximumStreamingTexelRatio = HoudiniRuntimeSettings->bUseMaximumStreamingTexelRatio; + StaticMeshGenerationProperties_DEPRECATED.GeneratedStreamingDistanceMultiplier = HoudiniRuntimeSettings->StreamingDistanceMultiplier; + StaticMeshGenerationProperties_DEPRECATED.GeneratedWalkableSlopeOverride = HoudiniRuntimeSettings->WalkableSlopeOverride; + StaticMeshGenerationProperties_DEPRECATED.GeneratedFoliageDefaultSettings = HoudiniRuntimeSettings->FoliageDefaultSettings; + StaticMeshGenerationProperties_DEPRECATED.GeneratedAssetUserData = HoudiniRuntimeSettings->AssetUserData; + } + + // Register ourself to the HER singleton + FHoudiniEngineRuntime::Get().RegisterHoudiniCookable(GetCookable()); +} + +UWorld* +UHoudiniAssetComponent::GetHACWorld() const +{ + UWorld* World = GetWorld(); + if (!IsValid(World)) + World = GetOwner() ? GetOwner()->GetWorld() : nullptr; + + return World; +} + + +UHoudiniAsset * +UHoudiniAssetComponent::GetHoudiniAsset() const +{ + if (GetCookable()) + return GetCookable()->GetHoudiniAsset(); + + return HoudiniAsset_DEPRECATED; +} + +FString +UHoudiniAssetComponent::GetHoudiniAssetName() const +{ + return IsValid(GetHoudiniAsset()) ? GetHoudiniAsset()->GetName() : TEXT(""); +} + +FString +UHoudiniAssetComponent::GetDisplayName() const +{ + return GetOwner() ? GetOwner()->GetActorNameOrLabel() : GetName(); +} + +#if WITH_EDITORONLY_DATA +TArray>& +UHoudiniAssetComponent::GetParameters() +{ + if (GetCookable()) + return GetCookable()->GetParameters(); + + return Parameters_DEPRECATED; +} +#endif + +#if WITH_EDITORONLY_DATA +const TArray>& +UHoudiniAssetComponent::GetParameters() const +{ + if (GetCookable()) + return GetCookable()->GetParameters(); + + return Parameters_DEPRECATED; +} +#endif + +#if WITH_EDITORONLY_DATA +TArray>& +UHoudiniAssetComponent::GetInputs() +{ + if (GetCookable()) + return GetCookable()->GetInputs(); + + return Inputs_DEPRECATED; +} +#endif + +#if WITH_EDITORONLY_DATA +const TArray>& +UHoudiniAssetComponent::GetInputs() const +{ + if (GetCookable()) + return GetCookable()->GetInputs(); + + return Inputs_DEPRECATED; +} +#endif + +TArray>& +UHoudiniAssetComponent::GetOutputs() +{ + if (GetCookable()) + return GetCookable()->GetOutputs(); + + return Outputs_DEPRECATED; +} + +TArray>& +UHoudiniAssetComponent::GetHandleComponents() +{ + if (GetCookable()) + return GetCookable()->GetHandleComponents(); + + return HandleComponents_DEPRECATED; +} + + +void +UHoudiniAssetComponent::GetOutputs(TArray& OutOutputs) const +{ + if (GetCookable()) + return GetCookable()->GetOutputs(OutOutputs); + + for (UHoudiniOutput* Output : Outputs_DEPRECATED) + { + OutOutputs.Add(Output); + } +} + +TArray& +UHoudiniAssetComponent::GetBakedOutputs() +{ + if (GetCookable()) + return GetCookable()->GetBakedOutputs(); + + return BakedOutputs_DEPRECATED; +} + +const TArray& +UHoudiniAssetComponent::GetBakedOutputs() const +{ + if (GetCookable()) + return GetCookable()->GetBakedOutputs(); + + return BakedOutputs_DEPRECATED; +} + +bool +UHoudiniAssetComponent::GetSplitMeshSupport() const +{ + if (GetCookable()) + return GetCookable()->GetSplitMeshSupport(); + + return bSplitMeshSupport_DEPRECATED; +} + +FHoudiniStaticMeshGenerationProperties +UHoudiniAssetComponent::GetStaticMeshGenerationProperties() const +{ + if (GetCookable()) + return GetCookable()->GetStaticMeshGenerationProperties(); + + return StaticMeshGenerationProperties_DEPRECATED; +} + +FMeshBuildSettings +UHoudiniAssetComponent::GetStaticMeshBuildSettings() const +{ + if (GetCookable()) + return GetCookable()->GetStaticMeshBuildSettings(); + + return StaticMeshBuildSettings_DEPRECATED; +} + +bool +UHoudiniAssetComponent::IsOverrideGlobalProxyStaticMeshSettings() const +{ + if (GetCookable()) + return GetCookable()->IsOverrideGlobalProxyStaticMeshSettings(); + + return bOverrideGlobalProxyStaticMeshSettings_DEPRECATED; +} + + +bool +UHoudiniAssetComponent::IsProxyStaticMeshEnabled() const +{ + if (GetCookable()) + return GetCookable()->IsProxyStaticMeshEnabled(); + + if (bOverrideGlobalProxyStaticMeshSettings_DEPRECATED) + { + return bEnableProxyStaticMeshOverride_DEPRECATED; + } + else + { + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings) + { + return HoudiniRuntimeSettings->bEnableProxyStaticMesh; + } + else + { + return false; + } + } +} + +bool +UHoudiniAssetComponent::IsProxyStaticMeshRefinementByTimerEnabled() const +{ + if (GetCookable()) + return GetCookable()->IsProxyStaticMeshRefinementByTimerEnabled(); + + if (bOverrideGlobalProxyStaticMeshSettings_DEPRECATED) + { + return bEnableProxyStaticMeshOverride_DEPRECATED && bEnableProxyStaticMeshRefinementByTimerOverride_DEPRECATED; + } + else + { + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings) + { + return HoudiniRuntimeSettings->bEnableProxyStaticMesh && HoudiniRuntimeSettings->bEnableProxyStaticMeshRefinementByTimer; + } + else + { + return false; + } + } +} + +float +UHoudiniAssetComponent::GetProxyMeshAutoRefineTimeoutSeconds() const +{ + if (GetCookable()) + return GetCookable()->GetProxyMeshAutoRefineTimeoutSeconds(); + + if (bOverrideGlobalProxyStaticMeshSettings_DEPRECATED) + { + return ProxyMeshAutoRefineTimeoutSecondsOverride_DEPRECATED; + } + else + { + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings) + { + return HoudiniRuntimeSettings->ProxyMeshAutoRefineTimeoutSeconds; + } + else + { + return 5.0f; + } + } +} + +bool +UHoudiniAssetComponent::IsProxyStaticMeshRefinementOnPreSaveWorldEnabled() const +{ + if (GetCookable()) + return GetCookable()->IsProxyStaticMeshRefinementOnPreSaveWorldEnabled(); + + if (bOverrideGlobalProxyStaticMeshSettings_DEPRECATED) + { + return bEnableProxyStaticMeshOverride_DEPRECATED && bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride_DEPRECATED; + } + else + { + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings) + { + return HoudiniRuntimeSettings->bEnableProxyStaticMesh && HoudiniRuntimeSettings->bEnableProxyStaticMeshRefinementOnPreSaveWorld; + } + else + { + return false; + } + } +} + +bool +UHoudiniAssetComponent::IsProxyStaticMeshRefinementOnPreBeginPIEEnabled() const +{ + if (GetCookable()) + return GetCookable()->IsProxyStaticMeshRefinementOnPreBeginPIEEnabled(); + + if (bOverrideGlobalProxyStaticMeshSettings_DEPRECATED) + { + return bEnableProxyStaticMeshOverride_DEPRECATED && bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride_DEPRECATED; + } + else + { + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings) + { + return HoudiniRuntimeSettings->bEnableProxyStaticMesh && HoudiniRuntimeSettings->bEnableProxyStaticMeshRefinementOnPreBeginPIE; + } + else + { + return false; + } + } +} + + +void +UHoudiniAssetComponent::SetOverrideGlobalProxyStaticMeshSettings(bool InEnable) +{ + if (GetCookable()) + return GetCookable()->SetOverrideGlobalProxyStaticMeshSettings(InEnable); + + bOverrideGlobalProxyStaticMeshSettings_DEPRECATED = InEnable; +} + +void +UHoudiniAssetComponent::SetEnableProxyStaticMeshOverride(bool InEnable) +{ + if (GetCookable()) + return GetCookable()->SetEnableProxyStaticMeshOverride(InEnable); + + bEnableProxyStaticMeshOverride_DEPRECATED = InEnable; +} + +void +UHoudiniAssetComponent::SetEnableProxyStaticMeshRefinementByTimerOverride(bool InEnable) +{ + if (GetCookable()) + return GetCookable()->SetEnableProxyStaticMeshRefinementByTimerOverride(InEnable); + + bEnableProxyStaticMeshRefinementByTimerOverride_DEPRECATED = InEnable; +} + +void +UHoudiniAssetComponent::SetProxyMeshAutoRefineTimeoutSecondsOverride(float InValue) +{ + if (GetCookable()) + return GetCookable()->SetProxyMeshAutoRefineTimeoutSecondsOverride(InValue); + + ProxyMeshAutoRefineTimeoutSecondsOverride_DEPRECATED = InValue; +} + +void +UHoudiniAssetComponent::SetEnableProxyStaticMeshRefinementOnPreSaveWorldOverride(bool InEnable) +{ + if (GetCookable()) + return GetCookable()->SetEnableProxyStaticMeshRefinementOnPreSaveWorldOverride(InEnable); + + bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride_DEPRECATED = InEnable; +} + +void +UHoudiniAssetComponent::SetEnableProxyStaticMeshRefinementOnPreBeginPIEOverride(bool InEnable) +{ + if (GetCookable()) + return GetCookable()->SetEnableProxyStaticMeshRefinementOnPreBeginPIEOverride(InEnable); + + bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride_DEPRECATED = InEnable; +} + +void +UHoudiniAssetComponent::SetHoudiniAsset(UHoudiniAsset * InHoudiniAsset) +{ + // Check the asset validity + if (!IsValid(InHoudiniAsset)) + return; + + if (GetCookable()) + GetCookable()->SetHoudiniAsset(InHoudiniAsset); +} + + +void +UHoudiniAssetComponent::OnHoudiniAssetChanged() +{ + if (GetCookable()) + return GetCookable()->OnHoudiniAssetChanged(); + +#if WITH_EDITORONLY_DATA + // TODO: clear input/params/outputs? + Parameters_DEPRECATED.Empty(); +#endif + + // The asset has been changed, mark us as needing to be reinstantiated + MarkAsNeedInstantiation(); + + // Force an update on the next tick + bForceNeedUpdate_DEPRECATED = true; +} + +void +UHoudiniAssetComponent::SetCookingEnabled(const bool& bInCookingEnabled) +{ + if (GetCookable()) + GetCookable()->SetCookingEnabled(bInCookingEnabled); + + bEnableCooking_DEPRECATED = bInCookingEnabled; +} + +void +UHoudiniAssetComponent::SetHasBeenLoaded(const bool& InLoaded) +{ + if (GetCookable()) + GetCookable()->SetHasBeenLoaded(InLoaded); + + bHasBeenLoaded_DEPRECATED = InLoaded; +} + +void +UHoudiniAssetComponent::SetHasBeenDuplicated(const bool& InDuplicated) +{ + if (GetCookable()) + GetCookable()->SetHasBeenDuplicated(InDuplicated); + + bHasBeenDuplicated_DEPRECATED = InDuplicated; +} + + +bool +UHoudiniAssetComponent::NeedUpdateParameters() const +{ + if (GetCookable()) + return GetCookable()->NeedUpdateParameters(); + + return false; +} + +bool +UHoudiniAssetComponent::NeedUpdateInputs() const +{ + if (GetCookable()) + return GetCookable()->NeedUpdateInputs(); + + return false; +} + +bool +UHoudiniAssetComponent::WasLastCookSuccessful() const +{ + if (GetCookable()) + return GetCookable()->WasLastCookSuccessful(); + + return bLastCookSuccess_DEPRECATED; +} + +bool +UHoudiniAssetComponent::IsParameterDefinitionUpdateNeeded() const +{ + if (GetCookable()) + return GetCookable()->IsParameterDefinitionUpdateNeeded(); + + return bParameterDefinitionUpdateNeeded_DEPRECATED; +} + +FDirectoryPath +UHoudiniAssetComponent::GetBakeFolder() const +{ + if (GetCookable()) + return GetCookable()->GetBakeFolder(); + + return BakeFolder_DEPRECATED; +} + +FDirectoryPath +UHoudiniAssetComponent::GetTemporaryCookFolder() const +{ + if (GetCookable()) + return GetCookable()->GetTemporaryCookFolder(); + + return TemporaryCookFolder_DEPRECATED; +} + +FString +UHoudiniAssetComponent::GetBakeFolderOrDefault() const +{ + if (GetCookable()) + return GetCookable()->GetBakeFolderOrDefault(); + + return !BakeFolder_DEPRECATED.Path.IsEmpty() ? BakeFolder_DEPRECATED.Path : FHoudiniEngineRuntime::Get().GetDefaultBakeFolder(); +} + +FString +UHoudiniAssetComponent::GetTemporaryCookFolderOrDefault() const +{ + if (GetCookable()) + return GetCookable()->GetTemporaryCookFolderOrDefault(); + + return !TemporaryCookFolder_DEPRECATED.Path.IsEmpty() ? TemporaryCookFolder_DEPRECATED.Path : FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder(); +} + + +bool UHoudiniAssetComponent::NeedBlueprintStructureUpdate() const +{ + // TODO: Add similar flags to inputs, parameters + return bBlueprintStructureModified; +} + +bool UHoudiniAssetComponent::NeedBlueprintUpdate() const +{ + // TODO: Add similar flags to inputs, parameters + return bBlueprintModified; +} + +void +UHoudiniAssetComponent::BeginDestroy() +{ + // Unregister ourself so our houdini node can be deleted + FHoudiniEngineRuntime::Get().UnRegisterHoudiniCookable(GetCookable()); + + Super::BeginDestroy(); +} + +void +UHoudiniAssetComponent::MarkAsNeedCook() +{ + if (GetCookable()) + return GetCookable()->MarkAsNeedCook(); +} + +void +UHoudiniAssetComponent::MarkAsNeedRebuild() +{ + if (GetCookable()) + return GetCookable()->MarkAsNeedRebuild(); + + return; +} + +// Marks the asset as needing to be instantiated +void +UHoudiniAssetComponent::MarkAsNeedInstantiation() +{ + if (GetCookable()) + return GetCookable()->MarkAsNeedInstantiation(); +} + +void +UHoudiniAssetComponent::MarkAsBlueprintStructureModified() +{ + bBlueprintStructureModified = true; +} + +void +UHoudiniAssetComponent::MarkAsBlueprintModified() +{ + bBlueprintModified = true; +} + +void +UHoudiniAssetComponent::PostLoad() +{ + Super::PostLoad(); + + if (bMigrateDataToCookableOnPostLoad) + { + HOUDINI_LOG_MESSAGE(TEXT("Loading deprecated version of UHoudiniAssetComponent : V2 HAC will be converted to Cookable.")); + + // V2 component - we need to move data to the cookable + AHoudiniAssetActor* HAA = Cast(this->GetOwner()); + UHoudiniCookable* HC = HAA ? HAA->GetHoudiniCookable() : nullptr; + if (!HC) + { + HOUDINI_LOG_WARNING(TEXT("Actor has no Cookable.")); + } + else + { + // Move data to the cookable + if (!TransferDataToCookable(HC)) + { + HOUDINI_LOG_ERROR(TEXT("Unable to convert v2 Houdini Asset Component to Cookable - will need to be recreated.")); + } + else + { + // Indicate that we are the cookable's component + HC->SetComponent(this); + + // Set the Cookable as our outer + // TODO: UE doesn't like doing this on PostLoad (get stuck) + //this->Rename(nullptr, HC); + + // Once everything is done - set ourselves as a component of the HAA. + // Why is this needed ? + HAA->SetRootComponent(this); + HAA->AddInstanceComponent(this); + } + } + + bMigrateDataToCookableOnPostLoad = false; + } + + // TODO: Cookable ?? Needed?? + //if (GetCookable()) + // GetCookable()->PostLoad(); + + // We still need this PostLoad function as saved v2 component don't have + // a cookable, dso dont call the cookable's PostLoad function + + // Mark as need instantiation + MarkAsNeedInstantiation(); + + // Component has been loaded, not duplicated + SetHasBeenDuplicated(false); + + // We need to register ourself + // TODO: Cookable - clean me up + if (GetCookable()) + { + FHoudiniEngineRuntime::Get().RegisterHoudiniCookable(GetCookable()); + } + else + { + HOUDINI_LOG_ERROR(TEXT("Post Loading an HAC with no Cookable!!! Trouble ahead!!")); + } + + + // Register our PDG Asset link if we have any + + // !!! Do not update rendering while loading, do it when setting up the render state + // UpdateRenderingInformation(); + +#if WITH_EDITORONLY_DATA + auto MaxValue = StaticEnum()->GetMaxEnumValue() - 1; + if (static_cast(HoudiniEngineBakeOption_DEPRECATED) > MaxValue) + { + HOUDINI_LOG_WARNING(TEXT("Invalid Bake Type found, setting to To Actor. Possibly Foliage, which is deprecated, use the unreal_foliage attribute instead.")); + HoudiniEngineBakeOption_DEPRECATED = EHoudiniEngineBakeOption::ToActor; + } +#endif +} + + +void +UHoudiniAssetComponent::PostEditImport() +{ + Super::PostEditImport(); + + // TODO: Cookable ?? Needed?? + MarkAsNeedInstantiation(); + + // Component has been duplicated, not loaded + // We do need the loaded flag to reapply parameters, inputs + // and properly update some of the output objects + SetHasBeenDuplicated(true); + + SetAssetState(EHoudiniAssetState::PreInstantiation); + SetAssetStateResult(EHoudiniAssetStateResult::None); +} + + +void +UHoudiniAssetComponent::CreateRenderState_Concurrent(FRegisterComponentContext* Context) +{ + // Just call parent class for now. + Super::CreateRenderState_Concurrent(Context); +} + + +void +UHoudiniAssetComponent::OnFullyLoaded() +{ + if (GetCookable()) + GetCookable()->bFullyLoaded = true; + + bFullyLoaded_DEPRECATED = true; +} + + +void +UHoudiniAssetComponent::OnComponentCreated() +{ + // This event will only be fired for native Actor and native Component. + Super::OnComponentCreated(); +} + +void +UHoudiniAssetComponent::OnComponentDestroyed(bool bDestroyingHierarchy) +{ + if (GetCookable()) + { + // Call the cookable's OnDestroy + GetCookable()->OnDestroy(bDestroyingHierarchy); + + // Call our super + return Super::OnComponentDestroyed(bDestroyingHierarchy); + } + + HoudiniAsset_DEPRECATED = nullptr; + +#if WITH_EDITORONLY_DATA + // Clear Parameters + for (TObjectPtr& CurrentParm : Parameters_DEPRECATED) + { + if (IsValid(CurrentParm)) + { + CurrentParm->ConditionalBeginDestroy(); + } + else if (GetHACWorld() != nullptr && GetHACWorld()->WorldType != EWorldType::PIE) + { + // TODO unneeded log? + // Avoid spamming that error when leaving PIE mode + HOUDINI_LOG_WARNING(TEXT("%s: null parameter when clearing"), GetOwner() ? *(GetOwner()->GetName()) : *GetName()); + } + + CurrentParm = nullptr; + } + + Parameters_DEPRECATED.Empty(); +#endif + +#if WITH_EDITORONLY_DATA + // Clear Inputs + for (TObjectPtr& CurrentInput : Inputs_DEPRECATED) + { + if (!IsValid(CurrentInput)) + continue; + + if (CurrentInput->HasAnyFlags(RF_NeedLoad | RF_NeedPostLoad)) + continue; + + // Destroy connected Houdini asset. + CurrentInput->ConditionalBeginDestroy(); + CurrentInput = nullptr; + } + + Inputs_DEPRECATED.Empty(); +#endif + + // Clear Output + for (TObjectPtr& CurrentOutput : Outputs_DEPRECATED) + { + if (!IsValid(CurrentOutput)) + continue; + + if (CurrentOutput->HasAnyFlags(RF_NeedLoad | RF_NeedPostLoad)) + continue; + + // Destroy all Houdini created socket actors. + TArray> & CurCreatedSocketActors = CurrentOutput->GetHoudiniCreatedSocketActors(); + for (auto & CurCreatedActor : CurCreatedSocketActors) + { + if (!IsValid(CurCreatedActor)) + continue; + + CurCreatedActor->Destroy(); + } + CurCreatedSocketActors.Empty(); + + // Detach all Houdini attached socket actors + TArray> & CurAttachedSocketActors = CurrentOutput->GetHoudiniAttachedSocketActors(); + for (auto & CurAttachedSocketActor : CurAttachedSocketActors) + { + if (!IsValid(CurAttachedSocketActor)) + continue; + + CurAttachedSocketActor->DetachFromActor(FDetachmentTransformRules::KeepRelativeTransform); + } + CurAttachedSocketActors.Empty(); + +#if WITH_EDITOR + // Cleanup landscape splines + FHoudiniLandscapeRuntimeUtils::DeleteLandscapeSplineCookedData(CurrentOutput); + + // Cleanup landscapes + FHoudiniLandscapeRuntimeUtils::DeleteLandscapeCookedData(CurrentOutput); + + // Clean up foliage instances + for (auto& CurrentOutputObject : CurrentOutput->GetOutputObjects()) + { + for(int Index = 0; Index < CurrentOutputObject.Value.OutputComponents.Num(); Index++) + { + auto Component = CurrentOutputObject.Value.OutputComponents[Index]; + // Foliage instancers store a HISMC in the components + UHierarchicalInstancedStaticMeshComponent* FoliageHISMC = Cast(Component); + if (!FoliageHISMC) + continue; + + UStaticMesh* FoliageSM = FoliageHISMC->GetStaticMesh(); + if (!IsValid(FoliageSM)) + continue; + + // If we are a foliage HISMC, then our owner is an Instanced Foliage Actor, + // if it is not, then we are just a "regular" HISMC + AInstancedFoliageActor* InstancedFoliageActor = Cast(FoliageHISMC->GetOwner()); + if (!IsValid(InstancedFoliageActor)) + continue; + + UFoliageType *FoliageType = InstancedFoliageActor->GetLocalFoliageTypeForSource(FoliageSM); + if (!IsValid(FoliageType)) + continue; + + if (IsInGameThread() && IsGarbageCollecting()) + { + // TODO: ?? + // Calling DeleteInstancesForComponent during GC will cause unreal to crash... + HOUDINI_LOG_WARNING(TEXT("%s: Unable to clear foliage instances because of GC"), GetOwner() ? *(GetOwner()->GetName()) : *GetName()); + } + else + { + // Clean up the instances generated for that component + InstancedFoliageActor->DeleteInstancesForComponent(this, FoliageType); + } + + if (FoliageHISMC->GetInstanceCount() > 0) + { + // If the component still has instances left after the cleanup, + // make sure that we dont delete it, as the leftover instances are likely hand-placed + CurrentOutputObject.Value.OutputComponents[Index] = nullptr; + } + else + { + // Remove the foliage type if it doesn't have any more instances + InstancedFoliageActor->RemoveFoliageType(&FoliageType, 1); + } + } + } +#endif + + CurrentOutput->Clear(); + // Destroy connected Houdini asset. + CurrentOutput->ConditionalBeginDestroy(); + CurrentOutput = nullptr; + } + + Outputs_DEPRECATED.Empty(); + + // Clear the static mesh bake timer + ClearRefineMeshesTimer(); + + // Clear all TOP data and temporary geo/objects from the PDG asset link (if valid) + if (IsValid(PDGAssetLink_DEPRECATED)) + { +#if WITH_EDITOR + const UWorld* const World = GetHACWorld(); + if (IsValid(World)) + { + // Only do this for editor worlds, only interactively (not during engine shutdown or garbage collection) + if (World->WorldType == EWorldType::Editor && GIsRunning && !GIsGarbageCollecting) + { + // In case we are recording a transaction (undo, for example) notify that the object will be + // modified. + PDGAssetLink_DEPRECATED->Modify(); + PDGAssetLink_DEPRECATED->ClearAllTOPData(); + } + } +#endif + } + + Super::OnComponentDestroyed(bDestroyingHierarchy); +} + +void +UHoudiniAssetComponent::RegisterHoudiniComponent(UHoudiniAssetComponent* InComponent) +{ + UHoudiniCookable* MyCookable = GetCookable(); + if (!IsValid(MyCookable)) + return; + + // Registration of this component is wrapped in this virtual function to allow + // derived classed to override this behaviour. + FHoudiniEngineRuntime::Get().RegisterHoudiniCookable(MyCookable); +} + +void +UHoudiniAssetComponent::OnRegister() +{ + Super::OnRegister(); + + // NOTE: Wait until HoudiniEngineTick() before deciding to mark this object as fully loaded + // since preview components need to wait for component templates to finish their initialization + // before being able to perform state transfers. +} + + +UHoudiniParameter* +UHoudiniAssetComponent::FindParameterByName(const FString& InParamName) +{ + if (GetCookable()) + return GetCookable()->FindParameterByName(InParamName); + +#if WITH_EDITORONLY_DATA + for (auto CurrentParam : Parameters_DEPRECATED) + { + if (!IsValid(CurrentParam)) + continue; + + if (CurrentParam->GetParameterName().Equals(InParamName)) + return CurrentParam; + } +#endif + + return nullptr; +} + + +void +UHoudiniAssetComponent::OnChildAttached(USceneComponent* ChildComponent) +{ + Super::OnChildAttached(ChildComponent); + + // ... Do corresponding things for other houdini component types. + // ... +} + + +void +UHoudiniAssetComponent::OnUpdateTransform(EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport) +{ + Super::OnUpdateTransform(UpdateTransformFlags, Teleport); + +#if WITH_EDITOR + if (!GetUploadTransformsToHoudiniEngine()) + return; + + if (!GetComponentTransform().Equals(GetLastComponentTransform())) + { + // Only set transform changed flag if the transform actually changed. + // WorldComposition can call ApplyWorldOffset with a zero vector (for example during a map save) + // which triggers unexpected recooks. + SetHasComponentTransformChanged(true); + } +#endif +} + +void UHoudiniAssetComponent::HoudiniEngineTick() +{ + if (!IsFullyLoaded()) + { + OnFullyLoaded(); + } +} + + +#if WITH_EDITOR +void +UHoudiniAssetComponent::PostEditChangeProperty(FPropertyChangedEvent & PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + FProperty * Property = PropertyChangedEvent.MemberProperty; + if (!Property) + return; + + FName PropertyName = Property->GetFName(); + + // TODO: COOKABLE - Still working? + + // Changing the Houdini Asset? + if (PropertyName == GET_MEMBER_NAME_CHECKED(UHoudiniAssetComponent, HoudiniAsset_DEPRECATED)) + { + OnHoudiniAssetChanged(); + } + else if (PropertyName == GetRelativeLocationPropertyName() + || PropertyName == GetRelativeRotationPropertyName() + || PropertyName == GetRelativeScale3DPropertyName()) + { + SetHasComponentTransformChanged(true); + } + else if (PropertyName == + GET_MEMBER_NAME_CHECKED(UHoudiniAssetComponent, bOverrideGlobalProxyStaticMeshSettings_DEPRECATED)) + { + if (GetCookable()) + GetCookable()->SetOverrideGlobalProxyStaticMeshSettings(bOverrideGlobalProxyStaticMeshSettings_DEPRECATED); + + // Reset the timer + ClearRefineMeshesTimer(); + // SetRefineMeshesTimer will check the relevant settings and only set the timer if enabled via settings + SetRefineMeshesTimer(); + } + else if (PropertyName == + GET_MEMBER_NAME_CHECKED(UHoudiniAssetComponent, bEnableProxyStaticMeshOverride_DEPRECATED)) + { + if (GetCookable()) + GetCookable()->SetEnableProxyStaticMeshOverride(bEnableProxyStaticMeshOverride_DEPRECATED); + } + else if (PropertyName == + GET_MEMBER_NAME_CHECKED(UHoudiniAssetComponent, bEnableProxyStaticMeshRefinementByTimerOverride_DEPRECATED)) + { + if (GetCookable()) + GetCookable()->SetEnableProxyStaticMeshRefinementByTimerOverride(bEnableProxyStaticMeshRefinementByTimerOverride_DEPRECATED); + + // Reset the timer + ClearRefineMeshesTimer(); + // SetRefineMeshesTimer will check the relevant settings and only set the timer if enabled via settings + SetRefineMeshesTimer(); + } + else if (PropertyName == + GET_MEMBER_NAME_CHECKED(UHoudiniAssetComponent, ProxyMeshAutoRefineTimeoutSecondsOverride_DEPRECATED)) + { + if (GetCookable()) + GetCookable()->SetProxyMeshAutoRefineTimeoutSecondsOverride(ProxyMeshAutoRefineTimeoutSecondsOverride_DEPRECATED); + + // Reset the timer + ClearRefineMeshesTimer(); + // SetRefineMeshesTimer will check the relevant settings and only set the timer if enabled via settings + SetRefineMeshesTimer(); + } + else if (PropertyName == + GET_MEMBER_NAME_CHECKED(UHoudiniAssetComponent, bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride_DEPRECATED)) + { + if (GetCookable()) + GetCookable()->SetEnableProxyStaticMeshRefinementOnPreSaveWorldOverride(bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride_DEPRECATED); + } + else if (PropertyName == + GET_MEMBER_NAME_CHECKED(UHoudiniAssetComponent, bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride_DEPRECATED)) + { + if (GetCookable()) + GetCookable()->SetEnableProxyStaticMeshRefinementOnPreBeginPIEOverride(bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride_DEPRECATED); + } + else if (PropertyName == GET_MEMBER_NAME_CHECKED(UHoudiniAssetComponent, Mobility)) + { + // Changed GetAttachChildren to 'GetAllDescendants' due to HoudiniMeshSplitInstanceComponent + // not propagating property changes to their own child StaticMeshComponents. + TArray LocalAttachChildren; + GetChildrenComponents(true, LocalAttachChildren); + + // Mobility was changed, we need to update it for all attached components as well. + for (TArray< USceneComponent * >::TConstIterator Iter(LocalAttachChildren); Iter; ++Iter) + { + USceneComponent * SceneComponent = *Iter; + SceneComponent->SetMobility(Mobility); + } + } + else if (PropertyName == TEXT("bVisible")) + { + // Visibility has changed, propagate it to children. + SetVisibility(IsVisible(), true); + } + else if (PropertyName == GET_MEMBER_NAME_CHECKED(UHoudiniAssetComponent, bHiddenInGame)) + { + // Visibility has changed, propagate it to children. + SetHiddenInGame(bHiddenInGame, true); + } + + if (Property->HasMetaData(TEXT("Category"))) + { + const FString & Category = Property->GetMetaData(TEXT("Category")); + static const FString CategoryHoudiniGeneratedStaticMeshSettings = TEXT("HoudiniMeshGeneration"); + static const FString CategoryLighting = TEXT("Lighting"); + static const FString CategoryRendering = TEXT("Rendering"); + static const FString CategoryCollision = TEXT("Collision"); + static const FString CategoryPhysics = TEXT("Physics"); + static const FString CategoryLOD = TEXT("LOD"); + + if (CategoryHoudiniGeneratedStaticMeshSettings == Category) + { + // TODO: COOKABLE + // We are changing one of the mesh generation properties, we need to update all static meshes. + // As the StaticMeshComponents map contains only top-level static mesh components only, use the StaticMeshes map instead + for (int Idx = 0; Idx < GetNumOutputs(); Idx++) + { + UHoudiniOutput* CurOutput = GetOutputAt(Idx); + if (!CurOutput) + continue; + + for (auto& Pair : CurOutput->GetOutputObjects()) + { + UStaticMesh* StaticMesh = Cast(Pair.Value.OutputObject); + if (!IsValid(StaticMesh)) + continue; + + SetStaticMeshGenerationProperties(StaticMesh); + FHoudiniScopedGlobalSilence ScopedGlobalSilence; + StaticMesh->Build(true); + RefreshCollisionChange(*StaticMesh); + } + } + + return; + } + else if (CategoryLighting == Category) + { + if (Property->GetName() == TEXT("CastShadow")) + { + // Stop cast-shadow being applied to invisible colliders children + // This prevent colliders only meshes from casting shadows + TArray ReregisterComponents; + { + TArray LocalAttachChildren; + GetChildrenComponents(true, LocalAttachChildren); + for (TArray< USceneComponent * >::TConstIterator Iter(LocalAttachChildren); Iter; ++Iter) + { + UStaticMeshComponent * Component = Cast< UStaticMeshComponent >(*Iter); + if (!IsValid(Component)) + continue; + + /*const FHoudiniGeoPartObject * pGeoPart = StaticMeshes.FindKey(Component->GetStaticMesh()); + if (pGeoPart && pGeoPart->IsCollidable()) + { + // This is an invisible collision mesh: + // Do not interfere with lightmap builds - disable shadow casting + Component->SetCastShadow(false); + } + else*/ + { + // Set normally + Component->SetCastShadow(CastShadow); + } + + ReregisterComponents.Add(Component); + } + } + + if (ReregisterComponents.Num() > 0) + { + FMultiComponentReregisterContext MultiComponentReregisterContext(ReregisterComponents); + } + } + else if (Property->GetName() == TEXT("bCastDynamicShadow")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bCastDynamicShadow); + } + else if (Property->GetName() == TEXT("bCastStaticShadow")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bCastStaticShadow); + } + else if (Property->GetName() == TEXT("bCastVolumetricTranslucentShadow")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bCastVolumetricTranslucentShadow); + } + else if (Property->GetName() == TEXT("bCastInsetShadow")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bCastInsetShadow); + } + else if (Property->GetName() == TEXT("bCastHiddenShadow")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bCastHiddenShadow); + } + else if (Property->GetName() == TEXT("bCastShadowAsTwoSided")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bCastShadowAsTwoSided); + } + /*else if ( Property->GetName() == TEXT( "bLightAsIfStatic" ) ) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS( UPrimitiveComponent, bLightAsIfStatic ); + }*/ + else if (Property->GetName() == TEXT("bLightAttachmentsAsGroup")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bLightAttachmentsAsGroup); + } + else if (Property->GetName() == TEXT("IndirectLightingCacheQuality")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, IndirectLightingCacheQuality); + } + } + else if (CategoryRendering == Category) + { + if (Property->GetName() == TEXT("bVisibleInReflectionCaptures")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bVisibleInReflectionCaptures); + } + else if (Property->GetName() == TEXT("bRenderInMainPass")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bRenderInMainPass); + } + /* + else if ( Property->GetName() == TEXT( "bRenderInMono" ) ) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS( UPrimitiveComponent, bRenderInMono ); + } + */ + else if (Property->GetName() == TEXT("bOwnerNoSee")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bOwnerNoSee); + } + else if (Property->GetName() == TEXT("bOnlyOwnerSee")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bOnlyOwnerSee); + } + else if (Property->GetName() == TEXT("bTreatAsBackgroundForOcclusion")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bTreatAsBackgroundForOcclusion); + } + else if (Property->GetName() == TEXT("bUseAsOccluder")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bUseAsOccluder); + } + else if (Property->GetName() == TEXT("bRenderCustomDepth")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bRenderCustomDepth); + } + else if (Property->GetName() == TEXT("CustomDepthStencilValue")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, CustomDepthStencilValue); + } + else if (Property->GetName() == TEXT("CustomDepthStencilWriteMask")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, CustomDepthStencilWriteMask); + } + else if (Property->GetName() == TEXT("TranslucencySortPriority")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, TranslucencySortPriority); + } + else if (Property->GetName() == TEXT("bReceivesDecals")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bReceivesDecals); + } + else if (Property->GetName() == TEXT("BoundsScale")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, BoundsScale); + } + else if (Property->GetName() == TEXT("bUseAttachParentBound")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(USceneComponent, bUseAttachParentBound); + } + } + else if (CategoryCollision == Category) + { + if (Property->GetName() == TEXT("bAlwaysCreatePhysicsState")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bAlwaysCreatePhysicsState); + } + /*else if ( Property->GetName() == TEXT( "bGenerateOverlapEvents" ) ) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS( UPrimitiveComponent, bGenerateOverlapEvents ); + }*/ + else if (Property->GetName() == TEXT("bMultiBodyOverlap")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bMultiBodyOverlap); + } + /* + else if ( Property->GetName() == TEXT( "bCheckAsyncSceneOnMove" ) ) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS( UPrimitiveComponent, bCheckAsyncSceneOnMove ); + } + */ + else if (Property->GetName() == TEXT("bTraceComplexOnMove")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bTraceComplexOnMove); + } + else if (Property->GetName() == TEXT("bReturnMaterialOnMove")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bReturnMaterialOnMove); + } + else if (Property->GetName() == TEXT("BodyInstance")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, BodyInstance); + } + else if (Property->GetName() == TEXT("CanCharacterStepUpOn")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, CanCharacterStepUpOn); + } + /*else if ( Property->GetName() == TEXT( "bCanEverAffectNavigation" ) ) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS( UActorComponent, bCanEverAffectNavigation ); + }*/ + } + else if (CategoryPhysics == Category) + { + if (Property->GetName() == TEXT("bIgnoreRadialImpulse")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bIgnoreRadialImpulse); + } + else if (Property->GetName() == TEXT("bIgnoreRadialForce")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bIgnoreRadialForce); + } + else if (Property->GetName() == TEXT("bApplyImpulseOnDamage")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bApplyImpulseOnDamage); + } + /* + else if ( Property->GetName() == TEXT( "bShouldUpdatePhysicsVolume" ) ) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS( USceneComponent, bShouldUpdatePhysicsVolume ); + } + */ + } + else if (CategoryLOD == Category) + { + if (Property->GetName() == TEXT("MinDrawDistance")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, MinDrawDistance); + } + else if (Property->GetName() == TEXT("LDMaxDrawDistance")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, LDMaxDrawDistance); + } + else if (Property->GetName() == TEXT("CachedMaxDrawDistance")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, CachedMaxDrawDistance); + } + else if (Property->GetName() == TEXT("bAllowCullDistanceVolume")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(UPrimitiveComponent, bAllowCullDistanceVolume); + } + else if (Property->GetName() == TEXT("DetailMode")) + { + HOUDINI_UPDATE_ALL_CHILD_COMPONENTS(USceneComponent, DetailMode); + } + } + } +} +#endif + + +#if WITH_EDITOR +void +UHoudiniAssetComponent::PostEditUndo() +{ + Super::PostEditUndo(); + + if (!IsValid(this)) + return; + + if(!GetCookable()) + { + HOUDINI_LOG_ERROR(TEXT("PostEditUndo called on a HAC with no cookable!!! Trouble Ahead!!")); + return; + } + + // Make sure we are registered with the HER singleton + // We could be undoing a HoudiniActor delete + if (!FHoudiniEngineRuntime::Get().IsCookableRegistered(GetCookable())) + { + MarkAsNeedInstantiation(); + + // Component has been loaded, not duplicated + SetHasBeenDuplicated(false); + + FHoudiniEngineRuntime::Get().RegisterHoudiniCookable(GetCookable()); + } +} + +#endif + + + +#if WITH_EDITOR +void +UHoudiniAssetComponent::OnActorMoved(AActor* Actor) +{ + if (GetOwner() != Actor) + return; + + SetHasComponentTransformChanged(true); +} +#endif + +void +UHoudiniAssetComponent::SetHasComponentTransformChanged(const bool& InHasChanged) +{ + if (GetCookable()) + return GetCookable()->SetHasComponentTransformChanged(InHasChanged); + + // Only update the value if we're fully loaded + // This avoid triggering a recook when loading a level + if(bFullyLoaded_DEPRECATED) + { + bHasComponentTransformChanged_DEPRECATED = InHasChanged; + LastComponentTransform_DEPRECATED = GetComponentTransform(); + } +} + + +void +UHoudiniAssetComponent::SetAssetCookCount(const int32& InCount) +{ + if (GetCookable()) + GetCookable()->SetCookCount(InCount); + + AssetCookCount_DEPRECATED = InCount; +} + +FBoxSphereBounds +UHoudiniAssetComponent::CalcBounds(const FTransform & LocalToWorld) const +{ + FBoxSphereBounds LocalBounds; + FBox BoundingBox = GetAssetBounds(nullptr, false); + if (BoundingBox.GetExtent() == FVector3d::ZeroVector) + BoundingBox = BoundingBox.ExpandBy(1.0); + + LocalBounds = FBoxSphereBounds(BoundingBox); + // fix for offset bounds - maintain local bounds origin + LocalBounds = LocalBounds.TransformBy(LocalToWorld); + + const auto& LocalAttachedChildren = GetAttachChildren(); + for (int32 Idx = 0; Idx < LocalAttachedChildren.Num(); ++Idx) + { + if (!LocalAttachedChildren[Idx]) + continue; + + FBoxSphereBounds ChildBounds = LocalAttachedChildren[Idx]->CalcBounds(LocalToWorld); + if (!ChildBounds.ContainsNaN()) + LocalBounds = LocalBounds + ChildBounds; + } + + return LocalBounds; +} + + +FBox +UHoudiniAssetComponent::GetAssetBounds(UHoudiniInput* IgnoreInput, bool bIgnoreGeneratedLandscape) const +{ + FBox BoxBounds(ForceInitToZero); + + // This function may be called during destruction of the HAC, when the world is not set, so gracefully + // do nothing in this case. + if (!IsValid(this->GetHACWorld())) + return BoxBounds; + + // Return an empty Box if it is being destroyed + // This can cause random ensure to trigger when deleting HACs + if (IsBeingDestroyed() || HasAnyFlags(RF_BeginDestroyed | RF_FinishDestroyed)) + return BoxBounds; + + + // Commented out: Creates incorrect focus bounds.. + // Query the bounds for all output objects + if (GetCookable() && GetCookable()->GetOutputData()) + { + for (auto& CurOutput : GetCookable()->GetOutputData()->Outputs) + { + if (!IsValid(CurOutput)) + continue; + + BoxBounds += CurOutput->GetBounds(); + } + } + + /* + // Query the bounds for all our inputs + // Update! Bug: 148321. Thi casues other issues too. Just don't. + // Bug: 134158: For some reason using inputs in this manner during cooking will crash the cooker + // when using World Partition. So ignore inputs during cooking. + if (!IsRunningCookCommandlet()) + { + //TArray>& MyInputs = GetInputs(); + for (auto& CurInput : GetInputs()) + { + if (!IsValid(CurInput)) + continue; + + BoxBounds += CurInput->GetBounds(this->GetHACWorld()); + } + } + */ + + // Query the bounds for all input parameters + //TArray>& MyParams = GetParameters(); +#if WITH_EDITORONLY_DATA + for (auto& CurParam : GetParameters()) + { + if (!IsValid(CurParam)) + continue; + + if (CurParam->GetParameterType() != EHoudiniParameterType::Input) + continue; + + UHoudiniParameterOperatorPath* InputParam = Cast(CurParam); + if (!IsValid(CurParam)) + continue; + + if (!InputParam->HoudiniInput.IsValid()) + continue; + + BoxBounds += InputParam->HoudiniInput.Get()->GetBounds(this->GetHACWorld()); + } +#endif + + // Query the bounds for all our Houdini handles + for (auto & CurHandleComp : HandleComponents_DEPRECATED) + { + if (!IsValid(CurHandleComp)) + continue; + + BoxBounds += CurHandleComp->GetBounds(); + } + + /* + // Commented out: Creates incorrect focus bounds.. + // Also scan all our decendants for SMC bounds not just top-level children + // ( split mesh instances' mesh bounds were not gathered properly ) + TArray LocalAttachedChildren; + LocalAttachedChildren.Reserve(16); + GetChildrenComponents(true, LocalAttachedChildren); + for (int32 Idx = 0; Idx < LocalAttachedChildren.Num(); ++Idx) + { + if (!LocalAttachedChildren[Idx]) + continue; + + USceneComponent * pChild = LocalAttachedChildren[Idx]; + if (UStaticMeshComponent * StaticMeshComponent = Cast(pChild)) + { + if (!IsValid(StaticMeshComponent)) + continue; + + FBox StaticMeshBounds = StaticMeshComponent->Bounds.GetBox(); + if (StaticMeshBounds.IsValid) + BoxBounds += StaticMeshBounds; + } + } + */ + + /* + // Commented out: This also created incorrect focus bounds.. + // If nothing was found, init with the asset's location + if (BoxBounds.GetVolume() == 0.0f) + BoxBounds += GetComponentLocation(); + */ + return BoxBounds; +} + +#if WITH_EDITORONLY_DATA +EHoudiniEngineBakeOption +UHoudiniAssetComponent::GetHoudiniEngineBakeOption() const +{ + if (GetCookable()) + return GetCookable()->GetHoudiniEngineBakeOption(); + + return HoudiniEngineBakeOption_DEPRECATED; +} +#endif + +#if WITH_EDITORONLY_DATA +void +UHoudiniAssetComponent::SetHoudiniEngineBakeOption(const EHoudiniEngineBakeOption& InBakeOption) +{ + if (GetCookable()) + return GetCookable()->SetHoudiniEngineBakeOption(InBakeOption); + + if (HoudiniEngineBakeOption_DEPRECATED == InBakeOption) + return; + + HoudiniEngineBakeOption_DEPRECATED = InBakeOption; +} +#endif + +#if WITH_EDITORONLY_DATA +bool +UHoudiniAssetComponent::GetReplacePreviousBake() const +{ + if (GetCookable()) + return GetCookable()->GetReplacePreviousBake(); + + return bReplacePreviousBake_DEPRECATED; +} +#endif + +#if WITH_EDITORONLY_DATA +void +UHoudiniAssetComponent::SetReplacePreviousBake(bool bInReplace) +{ + if (GetCookable()) + return GetCookable()->SetReplacePreviousBake(bInReplace); + + bReplacePreviousBake_DEPRECATED = bInReplace; +} +#endif + + +#if WITH_EDITORONLY_DATA +bool +UHoudiniAssetComponent::GetRemoveOutputAfterBake() const +{ + if (GetCookable()) + return GetCookable()->GetRemoveOutputAfterBake(); + + return bRemoveOutputAfterBake_DEPRECATED; +} +#endif + +#if WITH_EDITORONLY_DATA +void +UHoudiniAssetComponent::SetRemoveOutputAfterBake(bool bInRemove) +{ + if (GetCookable()) + return GetCookable()->SetRemoveOutputAfterBake(bInRemove); + + bRemoveOutputAfterBake_DEPRECATED = bInRemove; +} +#endif + +#if WITH_EDITORONLY_DATA +bool +UHoudiniAssetComponent::GetRecenterBakedActors() const +{ + if (GetCookable()) + return GetCookable()->GetRecenterBakedActors(); + + return bRecenterBakedActors_DEPRECATED; +} +#endif + +#if WITH_EDITORONLY_DATA +void +UHoudiniAssetComponent::SetRecenterBakedActors(bool bInRecenter) +{ + if (GetCookable()) + return GetCookable()->SetRemoveOutputAfterBake(bInRecenter); + + bRecenterBakedActors_DEPRECATED = bInRecenter; +} +#endif + +bool +UHoudiniAssetComponent::IsCookingEnabled() const +{ + if (GetCookable()) + return GetCookable()->IsCookingEnabled(); + + return bEnableCooking_DEPRECATED; +} + +bool +UHoudiniAssetComponent::HasBeenLoaded() const +{ + if (GetCookable()) + return GetCookable()->HasBeenLoaded(); + + return bHasBeenLoaded_DEPRECATED; +} + +bool +UHoudiniAssetComponent::HasBeenDuplicated() const +{ + if (GetCookable()) + return GetCookable()->HasBeenDuplicated(); + + return bHasBeenDuplicated_DEPRECATED; +} + +bool +UHoudiniAssetComponent::HasRecookBeenRequested() const +{ + if (GetCookable()) + return GetCookable()->HasRecookBeenRequested(); + + return bRecookRequested_DEPRECATED; +} + +bool +UHoudiniAssetComponent::HasRebuildBeenRequested() const +{ + if (GetCookable()) + return GetCookable()->HasRebuildBeenRequested(); + + return bRebuildRequested_DEPRECATED; +} + +bool +UHoudiniAssetComponent::GetCookOnParameterChange() const +{ + if (GetCookable()) + return GetCookable()->GetCookOnParameterChange(); + + return bCookOnParameterChange_DEPRECATED; +} + +bool +UHoudiniAssetComponent::GetCookOnTransformChange() const +{ + if (GetCookable()) + return GetCookable()->GetCookOnTransformChange(); + + return bCookOnTransformChange_DEPRECATED; +} + +bool +UHoudiniAssetComponent::GetCookOnAssetInputCook() const +{ + if (GetCookable()) + return GetCookable()->GetCookOnCookableInputCook(); + + return bCookOnAssetInputCook_DEPRECATED; +} + +bool +UHoudiniAssetComponent::IsOutputless() const +{ + if (GetCookable()) + return GetCookable()->IsOutputless(); + + return bOutputless_DEPRECATED; +} + +bool +UHoudiniAssetComponent::GetUseOutputNodes() const +{ + if (GetCookable()) + return GetCookable()->GetUseOutputNodes(); + + return bUseOutputNodes_DEPRECATED; +} + +bool +UHoudiniAssetComponent::GetOutputTemplateGeos() const +{ + if (GetCookable()) + return GetCookable()->GetOutputTemplateGeos(); + + return bOutputTemplateGeos_DEPRECATED; +} + +bool +UHoudiniAssetComponent::GetUploadTransformsToHoudiniEngine() const +{ + if (GetCookable()) + return GetCookable()->GetUploadTransformsToHoudiniEngine(); + + return bUploadTransformsToHoudiniEngine_DEPRECATED; +} + +FTransform +UHoudiniAssetComponent::GetLastComponentTransform() const +{ + if (GetCookable()) + return GetCookable()->GetLastComponentTransform(); + + return LastComponentTransform_DEPRECATED; +} + + +#if WITH_EDITORONLY_DATA +bool +UHoudiniAssetComponent::GetLandscapeUseTempLayers() const +{ + if (GetCookable()) + return GetCookable()->GetLandscapeUseTempLayers(); + + return bLandscapeUseTempLayers_DEPRECATED; +} +#endif + +#if WITH_EDITORONLY_DATA +bool +UHoudiniAssetComponent::GetEnableCurveEditing() const +{ + if (GetCookable()) + return GetCookable()->GetEnableCurveEditing(); + + return bEnableCurveEditing_DEPRECATED; +} +#endif + +void +UHoudiniAssetComponent::SetCookOnParameterChange(bool bEnable) +{ + if (GetCookable()) + return GetCookable()->SetCookOnParameterChange(bEnable); + + bCookOnParameterChange_DEPRECATED = bEnable; +} + +void +UHoudiniAssetComponent::SetCookOnTransformChange(bool bEnable) +{ + if (GetCookable()) + return GetCookable()->SetCookOnTransformChange(bEnable); + + bCookOnTransformChange_DEPRECATED = bEnable; +} + +void +UHoudiniAssetComponent::SetCookOnAssetInputCook(bool bEnable) +{ + if (GetCookable()) + return GetCookable()->SetCookOnCookableInputCook(bEnable); + + bCookOnAssetInputCook_DEPRECATED = bEnable; +} + +void +UHoudiniAssetComponent::SetOutputless(bool bEnable) +{ + if (GetCookable()) + return GetCookable()->SetOutputless(bEnable); + + bOutputless_DEPRECATED = bEnable; +} + +void +UHoudiniAssetComponent::SetUseOutputNodes(bool bEnable) +{ + if (GetCookable()) + return GetCookable()->SetUseOutputNodes(bEnable); + + bUseOutputNodes_DEPRECATED = bEnable; +} + +void +UHoudiniAssetComponent::SetOutputTemplateGeos(bool bEnable) +{ + if (GetCookable()) + return GetCookable()->SetOutputTemplateGeos(bEnable); + + bOutputTemplateGeos_DEPRECATED = bEnable; +} + +void +UHoudiniAssetComponent::SetUploadTransformsToHoudiniEngine(bool bEnable) +{ + if (GetCookable()) + return GetCookable()->SetUploadTransformsToHoudiniEngine(bEnable); + + bUploadTransformsToHoudiniEngine_DEPRECATED = bEnable; +} + +#if WITH_EDITORONLY_DATA +void +UHoudiniAssetComponent::SetLandscapeUseTempLayers(bool bEnable) +{ + if (GetCookable()) + return GetCookable()->SetLandscapeUseTempLayers(bEnable); + + bLandscapeUseTempLayers_DEPRECATED = bEnable; +} +#endif + +#if WITH_EDITORONLY_DATA +void +UHoudiniAssetComponent::SetEnableCurveEditing(bool bEnable) +{ + if (GetCookable()) + return GetCookable()->SetEnableCurveEditing(bEnable); + + bEnableCurveEditing_DEPRECATED = bEnable; +} +#endif + +void +UHoudiniAssetComponent::ClearRefineMeshesTimer() +{ + if (GetCookable()) + return GetCookable()->ClearRefineMeshesTimer(); + + UWorld *World = GetHACWorld(); + if (!World) + { + //HOUDINI_LOG_ERROR(TEXT("Cannot ClearRefineMeshesTimer, World is nullptr!")); + return; + } + + World->GetTimerManager().ClearTimer(RefineMeshesTimer_DEPRECATED); +} + +void +UHoudiniAssetComponent::SetRefineMeshesTimer() +{ + if (GetCookable()) + return GetCookable()->SetRefineMeshesTimer(); + + UWorld* World = GetHACWorld(); + if (!World) + { + HOUDINI_LOG_ERROR(TEXT("Cannot SetRefineMeshesTimer, World is nullptr!")); + return; + } + + // Check if timer-based proxy mesh refinement is enable for this component + const bool bEnableTimer = IsProxyStaticMeshRefinementByTimerEnabled(); + const float TimeSeconds = GetProxyMeshAutoRefineTimeoutSeconds(); + if (bEnableTimer) + { + World->GetTimerManager().SetTimer(RefineMeshesTimer_DEPRECATED, this, &UHoudiniAssetComponent::OnRefineMeshesTimerFired, 1.0f, false, TimeSeconds); + } + else + { + World->GetTimerManager().ClearTimer(RefineMeshesTimer_DEPRECATED); + } +} + +void +UHoudiniAssetComponent::OnRefineMeshesTimerFired() +{ + if (GetCookable()) + return GetCookable()->OnRefineMeshesTimerFired(); + + HOUDINI_LOG_MESSAGE(TEXT("UHoudiniAssetComponent::OnRefineMeshesTimerFired()")); + if (OnRefineMeshesTimerDelegate_DEPRECATED.IsBound()) + { + OnRefineMeshesTimerDelegate_DEPRECATED.Broadcast(this); + } +} + +bool +UHoudiniAssetComponent::IsComponentValid() const +{ + if (!IsValidLowLevel()) + return false; + + if (IsTemplate()) + return false; + + if (IsUnreachable()) + return false; + + if (!GetOuter()) //|| !GetOuter()->GetLevel() ) + return false; + + return true; +} + + + +void +UHoudiniAssetComponent::SetStaticMeshGenerationProperties(const FHoudiniStaticMeshGenerationProperties& InHSMGP) +{ + if (GetCookable()) + return GetCookable()->SetStaticMeshGenerationProperties(InHSMGP); + + StaticMeshGenerationProperties_DEPRECATED = InHSMGP; +}; + +void +UHoudiniAssetComponent::SetStaticMeshGenerationProperties(UStaticMesh* InStaticMesh) +{ +#if WITH_EDITOR + if (!InStaticMesh) + return; + + // Make sure static mesh has a new lighting guid. + InStaticMesh->SetLightingGuid(FGuid::NewGuid()); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + InStaticMesh->SetLODGroup(NAME_None); +#else + InStaticMesh->LODGroup = NAME_None; +#endif + + // Set resolution of lightmap. + InStaticMesh->SetLightMapResolution(StaticMeshGenerationProperties_DEPRECATED.GeneratedLightMapResolution); + + const FStaticMeshRenderData* InRenderData = InStaticMesh->GetRenderData(); + // Set the global light map coordinate index if it looks valid + if (InRenderData && InRenderData->LODResources.Num() > 0) + { + int32 NumUVs = InRenderData->LODResources[0].GetNumTexCoords(); + if (NumUVs > StaticMeshGenerationProperties_DEPRECATED.GeneratedLightMapCoordinateIndex) + { + InStaticMesh->SetLightMapCoordinateIndex(StaticMeshGenerationProperties_DEPRECATED.GeneratedLightMapCoordinateIndex); + } + } + + // TODO + // Set method for LOD texture factor computation. + // InStaticMesh->bUseMaximumStreamingTexelRatio = StaticMeshGenerationProperties.bGeneratedUseMaximumStreamingTexelRatio; + // Set distance where textures using UV 0 are streamed in/out. - GOES ON COMPONENT + // InStaticMesh->StreamingDistanceMultiplier = StaticMeshGenerationProperties.GeneratedStreamingDistanceMultiplier; + + // Add user data. + for (int32 AssetUserDataIdx = 0; AssetUserDataIdx < StaticMeshGenerationProperties_DEPRECATED.GeneratedAssetUserData.Num(); AssetUserDataIdx++) + InStaticMesh->AddAssetUserData(StaticMeshGenerationProperties_DEPRECATED.GeneratedAssetUserData[AssetUserDataIdx]); + + // Create a body setup if needed + if (!InStaticMesh->GetBodySetup()) + InStaticMesh->CreateBodySetup(); + + UBodySetup* BodySetup = InStaticMesh->GetBodySetup(); + if (!BodySetup) + return; + + // Set flag whether physics triangle mesh will use double sided faces when doing scene queries. + BodySetup->bDoubleSidedGeometry = StaticMeshGenerationProperties_DEPRECATED.bGeneratedDoubleSidedGeometry; + + // Assign physical material for simple collision. + BodySetup->PhysMaterial = StaticMeshGenerationProperties_DEPRECATED.GeneratedPhysMaterial; + + BodySetup->DefaultInstance.CopyBodyInstancePropertiesFrom(&StaticMeshGenerationProperties_DEPRECATED.DefaultBodyInstance); + + // Assign collision trace behavior. + BodySetup->CollisionTraceFlag = StaticMeshGenerationProperties_DEPRECATED.GeneratedCollisionTraceFlag; + + // Assign walkable slope behavior. + BodySetup->WalkableSlopeOverride = StaticMeshGenerationProperties_DEPRECATED.GeneratedWalkableSlopeOverride; + + // We want to use all of geometry for collision detection purposes. + BodySetup->bMeshCollideAll = true; + +#endif +} + + +void +UHoudiniAssetComponent::UpdatePhysicsState() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(UHoudiniAssetComponent::UpdatePhysicsState); + + // Update physics representation right away. + RecreatePhysicsState(); + + // Changed GetAttachChildren to 'GetAllDescendants' due to HoudiniMeshSplitInstanceComponent + // not propagating property changes to their own child StaticMeshComponents. + TArray LocalAttachChildren; + GetChildrenComponents(true, LocalAttachChildren); + for (TArray::TConstIterator Iter(LocalAttachChildren); Iter; ++Iter) + { + USceneComponent * SceneComponent = *Iter; + if (IsValid(SceneComponent)) + { + SceneComponent->RecreatePhysicsState(); + } + } + + // !!! Do not call UpdateBounds() here as this could cause + // a loading loop in post load on game builds! +} + + +FPrimitiveSceneProxy* +UHoudiniAssetComponent::CreateSceneProxy() +{ + /** Represents a UHoudiniAssetComponent to the scene manager. */ + class FHoudiniAssetSceneProxy final : public FPrimitiveSceneProxy + { + public: + SIZE_T GetTypeHash() const override + { + static size_t UniquePointer; + return reinterpret_cast(&UniquePointer); + } + + FHoudiniAssetSceneProxy(const UHoudiniAssetComponent* InComponent) + : FPrimitiveSceneProxy(InComponent) + { + } + + virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const override + { + FPrimitiveViewRelevance Result; + Result.bDrawRelevance = IsShown(View); + return Result; + } + + virtual uint32 GetMemoryFootprint(void) const override { return(sizeof(*this) + GetAllocatedSize()); } + uint32 GetAllocatedSize(void) const { return(FPrimitiveSceneProxy::GetAllocatedSize()); } + }; + + return new FHoudiniAssetSceneProxy(this); +} + +void +UHoudiniAssetComponent::SetAssetState(EHoudiniAssetState InNewState) +{ + if (GetCookable()) + return GetCookable()->SetCurrentState(InNewState); + + const EHoudiniAssetState OldState = AssetState_DEPRECATED; + AssetState_DEPRECATED = InNewState; + +#if WITH_EDITOR + IHoudiniEditorAssetStateSubsystemInterface* const EditorSubsystem = IHoudiniEditorAssetStateSubsystemInterface::Get(); + if (EditorSubsystem) + EditorSubsystem->NotifyOfHoudiniAssetStateChange(this, OldState, InNewState); +#endif + HandleOnHoudiniAssetStateChange(this, OldState, InNewState); +} + +void +UHoudiniAssetComponent::SetAssetStateResult(EHoudiniAssetStateResult InResult) +{ + if (GetCookable()) + return GetCookable()->SetCurrentStateResult(InResult); + + AssetStateResult_DEPRECATED = InResult; +} + +void +UHoudiniAssetComponent::HandleOnHoudiniAssetStateChange(UObject* InHoudiniAssetContext, const EHoudiniAssetState InFromState, const EHoudiniAssetState InToState) +{ + IHoudiniAssetStateEvents::HandleOnHoudiniAssetStateChange(InHoudiniAssetContext, InFromState, InToState); + + if (InFromState == InToState) + return; + + if (this != InHoudiniAssetContext) + return; + + FOnAssetStateChangeDelegate& StateChangeDelegate = GetOnAssetStateChangeDelegate(); + if (StateChangeDelegate.IsBound()) + StateChangeDelegate.Broadcast(this, InFromState, InToState); + + if (InToState == EHoudiniAssetState::PreInstantiation) + { + HandleOnPreInstantiation(); + } + + if (InToState == EHoudiniAssetState::PreCook) + { + HandleOnPreCook(); + } + + if (InToState == EHoudiniAssetState::PostCook) + { + HandleOnPostCook(); + } + +} + +void +UHoudiniAssetComponent::HandleOnPreInstantiation() +{ + if (GetCookable()) + GetCookable()->HandleOnPreInstantiation(); + + if (OnPreInstantiationDelegate_DEPRECATED.IsBound()) + OnPreInstantiationDelegate_DEPRECATED.Broadcast(this); +} + +void +UHoudiniAssetComponent::HandleOnPreCook() +{ + if (GetCookable()) + GetCookable()->HandleOnPreCook(); + + // Process the PreCookCallbacks array first + for(auto CallbackFn : PreCookCallbacks_DEPRECATED) + { + CallbackFn(this); + } + PreCookCallbacks_DEPRECATED.Empty(); + + if (OnPreCookDelegate_DEPRECATED.IsBound()) + OnPreCookDelegate_DEPRECATED.Broadcast(this); +} + +void +UHoudiniAssetComponent::HandleOnPostCook() +{ + if (GetCookable()) + GetCookable()->HandleOnPostCook(); + + if (OnPostCookDelegate_DEPRECATED.IsBound()) + OnPostCookDelegate_DEPRECATED.Broadcast(this, bLastCookSuccess_DEPRECATED); +} + +void +UHoudiniAssetComponent::HandleOnPreOutputProcessing() +{ + if (GetCookable()) + GetCookable()->HandleOnPreOutputProcessing(); + + if (OnPreOutputProcessingDelegate_DEPRECATED.IsBound()) + { + OnPreOutputProcessingDelegate_DEPRECATED.Broadcast(this, true); + } +} + +void +UHoudiniAssetComponent::HandleOnPostOutputProcessing() +{ + if (GetCookable()) + GetCookable()->HandleOnPostOutputProcessing(); + + if (OnPostOutputProcessingDelegate_DEPRECATED.IsBound()) + { + OnPostOutputProcessingDelegate_DEPRECATED.Broadcast(this, true); + } +} + +void +UHoudiniAssetComponent::HandleOnPostBake(bool bInSuccess) +{ + if (GetCookable()) + GetCookable()->HandleOnPostBake(bInSuccess); + + if (OnPostBakeDelegate_DEPRECATED.IsBound()) + OnPostBakeDelegate_DEPRECATED.Broadcast(this, bInSuccess); +} + +#if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 0) +ILevelInstanceInterface* +UHoudiniAssetComponent::GetLevelInstance() const +{ + // Find the level instanced which "owns" this HDA, if it exists. + + AActor* Actor = Cast(this->GetOwner()); + if (!Actor) + return nullptr; + + UWorld* World = Actor->GetWorld(); + if (!World) + return nullptr; + + ULevelInstanceSubsystem* LevelInstanceSystem = World->GetSubsystem(); + if (!LevelInstanceSystem) + return nullptr; + + return LevelInstanceSystem->GetOwningLevelInstance(Actor->GetLevel()); +} +#endif + +void UHoudiniAssetComponent::OnSessionConnected() +{ + if (GetCookable()) + GetCookable()->OnSessionConnected(); + +#if WITH_EDITORONLY_DATA + for(auto& Param : Parameters_DEPRECATED) + Param->OnSessionConnected(); + + for (auto & Input : Inputs_DEPRECATED) + Input->OnSessionConnected(); +#endif + + AssetId_DEPRECATED = INDEX_NONE; +} + +void +UHoudiniAssetComponent::ProcessBPTemplate(const bool& InIsGlobalCookingEnabled) +{ + // Handle template processing (for BP) + if (GetAssetState() != EHoudiniAssetState::ProcessTemplate) + return; + + if (IsTemplate() && !HasOpenEditor()) + { + // This component template no longer has an open editor and can be deregistered. + // TODO: Replace this polling mechanism with an "On Asset Closed" event if we + // can find one that actually works. + FHoudiniEngineRuntime::Get().UnRegisterHoudiniCookable(GetCookable()); + return; + } + + if (NeedBlueprintStructureUpdate()) + { + OnBlueprintStructureModified(); + } + + if (NeedBlueprintUpdate()) + { + OnBlueprintModified(); + } + + if (InIsGlobalCookingEnabled) + { + // Only process component template parameter updates when cooking is enabled. + if (NeedUpdateParameters() || NeedUpdateInputs()) + { + OnTemplateParametersChanged(); + } + } +} + +int32 +UHoudiniAssetComponent::GetAssetId() const +{ + if (GetCookable()) + return GetCookable()->GetNodeId(); + + return AssetId_DEPRECATED; +} + +EHoudiniAssetState +UHoudiniAssetComponent::GetAssetState() const +{ + if (GetCookable()) + return GetCookable()->GetCurrentState(); + + return AssetState_DEPRECATED; +} + +EHoudiniAssetStateResult +UHoudiniAssetComponent::GetAssetStateResult() const +{ + if (GetCookable()) + return GetCookable()->GetCurrentStateResult(); + + return AssetStateResult_DEPRECATED; + +} + +FGuid& +UHoudiniAssetComponent::GetHapiGUID() +{ + if (GetCookable()) + return GetCookable()->GetHapiGUID(); + + return HapiGUID_DEPRECATED; +} + +FString +UHoudiniAssetComponent::GetHapiAssetName() const +{ + if (GetCookable()) + return GetCookable()->GetHapiAssetName(); + + return HapiAssetName_DEPRECATED; +} + +FGuid +UHoudiniAssetComponent::GetComponentGUID() const +{ + if (GetCookable()) + return GetCookable()->GetCookableGUID(); + + return ComponentGUID_DEPRECATED; +} + +UHoudiniCookable* +UHoudiniAssetComponent::GetCookable() const +{ + UHoudiniCookable* HC = Cast(GetOuter()); + if (HC) + return HC; + + // Try to get the Cookable via our Actor - we might be a loaded v2 HAC + // This is required for loaded v2 HAC - as the Cookable is not the HAC's outer + AHoudiniAssetActor* HAA = Cast(GetOwner()); + if (HAA) + return HAA->GetHoudiniCookable(); + + return nullptr; +} + +int32 +UHoudiniAssetComponent::GetNumInputs() const +{ + if (GetCookable()) + return GetCookable()->GetNumInputs(); +#if WITH_EDITORONLY_DATA + return Inputs_DEPRECATED.Num(); +#else + return 0; +#endif +} + +int32 +UHoudiniAssetComponent::GetNumOutputs() const +{ + if (GetCookable()) + return GetCookable()->GetNumOutputs(); + + return Outputs_DEPRECATED.Num(); +} + +int32 +UHoudiniAssetComponent::GetNumParameters() const +{ + if (GetCookable()) + return GetCookable()->GetNumParameters(); + +#if WITH_EDITORONLY_DATA + return Parameters_DEPRECATED.Num(); +#else + return 0; +#endif +} + +int32 +UHoudiniAssetComponent::GetNumHandles() const +{ + if (GetCookable()) + return GetCookable()->GetNumHandles(); + + return HandleComponents_DEPRECATED.Num(); +} + +UHoudiniInput* +UHoudiniAssetComponent::GetInputAt(const int32& Idx) +{ + if (GetCookable()) + return GetCookable()->GetInputAt(Idx); +#if WITH_EDITORONLY_DATA + return Inputs_DEPRECATED.IsValidIndex(Idx) ? Inputs_DEPRECATED[Idx] : nullptr; +#else + return nullptr; +#endif +} + +UHoudiniOutput* +UHoudiniAssetComponent::GetOutputAt(const int32& Idx) +{ + if (GetCookable()) + return GetCookable()->GetOutputAt(Idx); + + return Outputs_DEPRECATED.IsValidIndex(Idx) ? Outputs_DEPRECATED[Idx] : nullptr; +} + +UHoudiniParameter* +UHoudiniAssetComponent::GetParameterAt(const int32& Idx) +{ + if (GetCookable()) + return GetCookable()->GetParameterAt(Idx); + +#if WITH_EDITORONLY_DATA + return Parameters_DEPRECATED.IsValidIndex(Idx) ? Parameters_DEPRECATED[Idx] : nullptr; +#else + return nullptr; +#endif +} + +UHoudiniHandleComponent* +UHoudiniAssetComponent::GetHandleComponentAt(const int32& Idx) +{ + if (GetCookable()) + return GetCookable()->GetHandleComponentAt(Idx); + + return HandleComponents_DEPRECATED.IsValidIndex(Idx) ? HandleComponents_DEPRECATED[Idx] : nullptr; +} + +UHoudiniPDGAssetLink* +UHoudiniAssetComponent::GetPDGAssetLink() +{ + if (GetCookable()) + return GetCookable()->GetPDGAssetLink(); + + return PDGAssetLink_DEPRECATED; +}; + + +bool +UHoudiniAssetComponent::IsFullyLoaded() const +{ + if (GetCookable()) + return GetCookable()->IsFullyLoaded(); + + return bFullyLoaded_DEPRECATED; +} + + + +bool +UHoudiniAssetComponent::SetTemporaryCookFolderPath(const FString& NewPath) +{ + if (GetCookable()) + return GetCookable()->SetTemporaryCookFolderPath(NewPath); + + if (TemporaryCookFolder_DEPRECATED.Path.Equals(NewPath)) + return false; + + if (TemporaryCookFolder_DEPRECATED.Path == NewPath) + return false; + + TemporaryCookFolder_DEPRECATED.Path = NewPath; + + return true; +} + +bool +UHoudiniAssetComponent::SetBakeFolderPath(const FString& NewPath) +{ + if (GetCookable()) + return GetCookable()->SetBakeFolderPath(NewPath); + + if (BakeFolder_DEPRECATED.Path.Equals(NewPath)) + return false; + + if (BakeFolder_DEPRECATED.Path == NewPath) + return false; + + BakeFolder_DEPRECATED.Path = NewPath; + + return true; +} + +bool +UHoudiniAssetComponent::SetTemporaryCookFolder(const FDirectoryPath& InPath) +{ + if (GetCookable()) + return GetCookable()->SetTemporaryCookFolder(InPath); + + if (TemporaryCookFolder_DEPRECATED.Path.Equals(InPath.Path)) + return false; + + TemporaryCookFolder_DEPRECATED = InPath; + + return true; +} + +bool +UHoudiniAssetComponent::SetBakeFolder(const FDirectoryPath& InPath) +{ + if (GetCookable()) + return GetCookable()->SetBakeFolder(InPath); + + if (BakeFolder_DEPRECATED.Path.Equals(InPath.Path)) + return false; + + BakeFolder_DEPRECATED = InPath; + + return true; +} + +bool +UHoudiniAssetComponent::TransferDataToCookable(UHoudiniCookable* HC) +{ + if (!HC) + return false; + + HC->SetHoudiniAsset(HoudiniAsset_DEPRECATED); + + HC->SetCookOnParameterChange(bCookOnParameterChange_DEPRECATED); + HC->SetUploadTransformsToHoudiniEngine(bUploadTransformsToHoudiniEngine_DEPRECATED); + HC->SetCookOnTransformChange(bCookOnTransformChange_DEPRECATED); + HC->SetCookOnCookableInputCook(bCookOnAssetInputCook_DEPRECATED); + HC->SetOutputless(bOutputless_DEPRECATED); + HC->SetOutputTemplateGeos(bOutputTemplateGeos_DEPRECATED); + HC->SetUseOutputNodes(bUseOutputNodes_DEPRECATED); + + HC->SetTemporaryCookFolder(TemporaryCookFolder_DEPRECATED); + HC->SetBakeFolder(BakeFolder_DEPRECATED); + HC->OutputData->bSplitMeshSupport = bSplitMeshSupport_DEPRECATED; + HC->SetStaticMeshGenerationProperties(StaticMeshGenerationProperties_DEPRECATED); + HC->SetStaticMeshBuildSettings(StaticMeshBuildSettings_DEPRECATED); + + HC->SetOverrideGlobalProxyStaticMeshSettings(bOverrideGlobalProxyStaticMeshSettings_DEPRECATED); + HC->SetEnableProxyStaticMeshOverride(bEnableProxyStaticMeshOverride_DEPRECATED); + HC->SetEnableProxyStaticMeshRefinementByTimerOverride(bEnableProxyStaticMeshRefinementByTimerOverride_DEPRECATED); + HC->SetProxyMeshAutoRefineTimeoutSecondsOverride(ProxyMeshAutoRefineTimeoutSecondsOverride_DEPRECATED); + HC->SetEnableProxyStaticMeshRefinementOnPreSaveWorldOverride(bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride_DEPRECATED); + HC->SetEnableProxyStaticMeshRefinementOnPreBeginPIEOverride(bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride_DEPRECATED); + +#if WITH_EDITORONLY_DATA + // bool bGenerateMenuExpanded; // COOKABLE + // bool bBakeMenuExpanded; // COOKABLE + // bool bAssetOptionMenuExpanded; // COOKABLE + // bool bHelpAndDebugMenuExpanded; // COOKABLE + + HC->SetHoudiniEngineBakeOption(HoudiniEngineBakeOption_DEPRECATED); + HC->SetRemoveOutputAfterBake(bRemoveOutputAfterBake_DEPRECATED); + HC->SetRecenterBakedActors(bRecenterBakedActors_DEPRECATED); + HC->SetReplacePreviousBake(bReplacePreviousBake_DEPRECATED); + HC->SetActorBakeOption(ActorBakeOption_DEPRECATED); + HC->SetLandscapeUseTempLayers(bLandscapeUseTempLayers_DEPRECATED); + HC->SetEnableCurveEditing(bEnableCurveEditing_DEPRECATED); + + // bool bNeedToUpdateEditorProperties; // COOKABLE +#endif + + // HC->NodeId = AssetId; // COOKABLE - NodeId + // HC->SetNodeIdsToCook(NodeIdsToCook); + // HC->NodesToCookCookCounts(OutputNodeCookCounts); + + // + for (auto& CurHAC : DownstreamHoudiniAssets) + { + if (!IsValid(CurHAC)) + continue; + + UHoudiniCookable* CurHC = CurHAC->GetCookable(); + if (!IsValid(CurHC)) + continue; + +#if WITH_EDITORONLY_DATA + HC->InputData->DownstreamCookables.Add(CurHC); +#endif + } + //DownstreamHoudiniAssets_DEPRECATED.Empty(); + + // HC->CookableGUID = ComponentGUID; + // HC->HapiGUID = HapiGUID; + HC->HoudiniAssetData->HapiAssetName = HapiAssetName_DEPRECATED; // COOKABLE - Name + // HC->SetCurrentState(AssetState); // COOKABLE + // EHoudiniAssetState DebugLastAssetState; // NOT COOKABLE + // HC->SetCurrentStateResult(AssetStateResult); // COOKABLE + // LastComponentTransform; // COOKABLE - COMPONENT + + HC->HoudiniAssetData->SubAssetIndex = SubAssetIndex_DEPRECATED; + // HC->SetCookCount(AssetCookCount); // COOKABLE - CookCount + // HC->SetHasBeenLoaded(bHasBeenLoaded); // COOKABLE + HC->SetHasBeenDuplicated(bHasBeenDuplicated_DEPRECATED); // COOKABLE + // HC->bPendingDelete = bPendingDelete; + // HC->SetRecookRequested(bRecookRequested); + // HC->SetRebuildRequested(bRebuildRequested); + // HC->SetCookingEnabled(bEnableCooking); + // HC->bForceNeedUpdate = bForceNeedUpdate; + // HC->bLastCookSuccess = bLastCookSuccess; + + // HC->ParameterData->bParameterDefinitionUpdateNeeded = bParameterDefinitionUpdateNeeded; + + // bBlueprintStructureModified; // NOT COOKABLE + // bBlueprintModified; // NOT COOKABLE + +#if WITH_EDITORONLY_DATA + HC->ParameterData->Parameters = Parameters_DEPRECATED; // COOKABLE - PARAMETERS + Parameters_DEPRECATED.Empty(); +#endif + +#if WITH_EDITORONLY_DATA + HC->InputData->Inputs = Inputs_DEPRECATED; // COOKABLE - INPUTS + Inputs_DEPRECATED.Empty(); +#endif + + HC->OutputData->Outputs = Outputs_DEPRECATED; // COOKABLE - OUTPUTS + Outputs_DEPRECATED.Empty(); + + HC->BakingData->BakedOutputs = BakedOutputs_DEPRECATED; // COOKABLE - OUTPUTS + BakedOutputs_DEPRECATED.Empty(); + + HC->OutputData->UntrackedOutputs = UntrackedOutputs_DEPRECATED; // COOKABLE - OUTPUTS + UntrackedOutputs_DEPRECATED.Empty(); + + HC->ComponentData->HandleComponents = HandleComponents_DEPRECATED; // COOKABLE - COMPONENT + HandleComponents_DEPRECATED.Empty(); + + // HC->SetHasComponentTransformChanged(bHasComponentTransformChanged); // COOKABLE - COMPONENT + // HC->bFullyLoaded = bFullyLoaded; // COOKABLE + + HC->PDGData->PDGAssetLink = PDGAssetLink_DEPRECATED; + PDGAssetLink_DEPRECATED = nullptr; + + // HC->PDGData->bIsPDGAssetLinkInitialized = bIsPDGAssetLinkInitialized; // COOKABLE - PDG + // HC->OutputData->RefineMeshesTimer = RefineMeshesTimer; // COOKABLE - OUTPUTS + // HC->OutputData->OnRefineMeshesTimerDelegate = OnRefineMeshesTimerDelegate; // COOKABLE - OUTPUTS + // HC->SetNoProxyMeshNextCookRequested(bNoProxyMeshNextCookRequested); + // HC->SetBakeAfterNextCook(BakeAfterNextCook); // COOKABLE - OUTPUTS + +#if WITH_EDITORONLY_DATA + HC->ParameterData->ParameterPresetBuffer = ParameterPresetBuffer_DEPRECATED; + ParameterPresetBuffer_DEPRECATED.Empty(); +#endif + + return true; +} +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniAssetComponent.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/ObjectMacros.h" +#include "Misc/Paths.h" + +#include "HoudiniEngineRuntimeCommon.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniRuntimeSettings.h" +#include "HoudiniOutput.h" +#include "HoudiniPluginSerializationVersion.h" +#include "HoudiniAssetStateTypes.h" +#include "IHoudiniAssetStateEvents.h" + +#include "Engine/EngineTypes.h" +#include "Components/PrimitiveComponent.h" +#include "Components/SceneComponent.h" + +#include "HoudiniAssetComponent.generated.h" + +class UHoudiniAsset; +class UHoudiniCookable; +class UHoudiniParameter; +class UHoudiniInput; +class UHoudiniOutput; +class UHoudiniHandleComponent; +class UHoudiniPDGAssetLink; +class UHoudiniAssetComponent; + +DECLARE_MULTICAST_DELEGATE_OneParam(FHoudiniAssetEvent, UHoudiniAsset*); +DECLARE_MULTICAST_DELEGATE_OneParam(FHoudiniAssetComponentEvent, UHoudiniAssetComponent*) + +UCLASS(ClassGroup = (Rendering, Common), hidecategories = (Object, Activation, "Components|Activation"), ShowCategories = (Mobility), editinlinenew) +class HOUDINIENGINERUNTIME_API UHoudiniAssetComponent : public UPrimitiveComponent, public IHoudiniAssetStateEvents +{ + GENERATED_UCLASS_BODY() + + // Declare translators as friend so they can easily directly modify + // Inputs, outputs and parameters + friend class FHoudiniEngineManager; + friend struct FHoudiniOutputTranslator; + friend struct FHoudiniInputTranslator; + friend struct FHoudiniSplineTranslator; + friend struct FHoudiniParameterTranslator; + friend struct FHoudiniPDGManager; + friend struct FHoudiniHandleTranslator; + friend class FHoudiniToolsEditor; + //friend struct FHoudiniEngineBakeUtils; + +#if WITH_EDITORONLY_DATA + friend class FHoudiniAssetComponentDetails; +#endif + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Declare the delegate that is broadcast when RefineMeshesTimer fires + DECLARE_MULTICAST_DELEGATE_OneParam(FOnRefineMeshesTimerDelegate, UHoudiniAssetComponent*); + // Delegate for when EHoudiniAssetState changes from InFromState to InToState on a Houdini Asset Component (InHAC). + DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnAssetStateChangeDelegate, UHoudiniAssetComponent*, const EHoudiniAssetState, const EHoudiniAssetState); + DECLARE_MULTICAST_DELEGATE_OneParam(FOnPreInstantiationDelegate, UHoudiniAssetComponent*); + DECLARE_MULTICAST_DELEGATE_OneParam(FOnPreCookDelegate, UHoudiniAssetComponent*); + DECLARE_MULTICAST_DELEGATE_TwoParams(FOnPostCookDelegate, UHoudiniAssetComponent*, bool); + DECLARE_MULTICAST_DELEGATE_TwoParams(FOnPostBakeDelegate, UHoudiniAssetComponent*, bool); + DECLARE_MULTICAST_DELEGATE_TwoParams(FOnPostOutputProcessingDelegate, UHoudiniAssetComponent*, bool); + DECLARE_MULTICAST_DELEGATE_TwoParams(FOnPreOutputProcessingDelegate, UHoudiniAssetComponent*, bool); + + virtual ~UHoudiniAssetComponent(); + + virtual void Serialize(FArchive & Ar) override; + + // Called after the C++ constructor and after the properties have been initialized, including those loaded from config. + // This is called before any serialization or other setup has happened. + virtual void PostInitProperties() override; + + // Returns the Owner actor / HAC name + FString GetDisplayName() const; + + // Check whether any inputs / outputs / parameters have made blueprint modifications. + bool NeedBlueprintStructureUpdate() const; + bool NeedBlueprintUpdate() const; + + // Finds a parameter by name + UHoudiniParameter* FindParameterByName(const FString& InParamName); + + /** Getter for the cached world pointer, will return null if the component is not actually spawned in a level */ + virtual UWorld* GetHACWorld() const; + + //------------------------------------------------------------------------------------------------ + // Accessors + //------------------------------------------------------------------------------------------------ + UHoudiniCookable* GetCookable() const; + + UHoudiniAsset* GetHoudiniAsset() const; + int32 GetAssetId() const; + EHoudiniAssetState GetAssetState() const; +// FString GetAssetStateAsString() const { return FHoudiniEngineRuntimeUtils::EnumToString(TEXT("EHoudiniAssetState"), GetAssetState()); }; + + virtual FString GetHoudiniAssetName() const; + + EHoudiniAssetStateResult GetAssetStateResult() const; + FGuid& GetHapiGUID(); + FString GetHapiAssetName() const; + FGuid GetComponentGUID() const; + + int32 GetNumInputs() const; + int32 GetNumOutputs() const; + int32 GetNumParameters() const; + int32 GetNumHandles() const; + + UHoudiniInput* GetInputAt(const int32& Idx); + UHoudiniOutput* GetOutputAt(const int32& Idx); + UHoudiniParameter* GetParameterAt(const int32& Idx); + UHoudiniHandleComponent* GetHandleComponentAt(const int32& Idx); + + void GetOutputs(TArray& OutOutputs) const; + + UHoudiniPDGAssetLink* GetPDGAssetLink(); + + TArray& GetBakedOutputs(); + const TArray& GetBakedOutputs() const; + +#if WITH_EDITORONLY_DATA + TArray>& GetParameters(); + const TArray>& GetParameters() const; +#endif + +#if WITH_EDITORONLY_DATA + TArray>& GetInputs(); + const TArray>& GetInputs() const; +#endif + + TArray>& GetOutputs(); + TArray>& GetHandleComponents(); + + bool IsCookingEnabled() const; + bool HasBeenLoaded() const; + bool HasBeenDuplicated() const; + bool HasRecookBeenRequested() const; + bool HasRebuildBeenRequested() const; + + bool GetCookOnParameterChange() const; + bool GetCookOnTransformChange() const; + bool GetCookOnAssetInputCook() const; + bool IsOutputless() const; + bool GetUseOutputNodes() const; + bool GetOutputTemplateGeos() const; + bool GetUploadTransformsToHoudiniEngine() const; + bool GetLandscapeUseTempLayers() const; + bool GetEnableCurveEditing() const; + bool GetSplitMeshSupport() const; + + FTransform GetLastComponentTransform() const; + + FHoudiniStaticMeshGenerationProperties GetStaticMeshGenerationProperties() const; + FMeshBuildSettings GetStaticMeshBuildSettings() const; + + bool IsFullyLoaded() const; + + bool IsOverrideGlobalProxyStaticMeshSettings() const; + virtual bool IsProxyStaticMeshEnabled() const; + bool IsProxyStaticMeshRefinementByTimerEnabled() const; + float GetProxyMeshAutoRefineTimeoutSeconds() const; + bool IsProxyStaticMeshRefinementOnPreSaveWorldEnabled() const; + bool IsProxyStaticMeshRefinementOnPreBeginPIEEnabled() const; + + + FOnPostOutputProcessingDelegate& GetOnPostOutputProcessingDelegate() { return OnPostOutputProcessingDelegate_DEPRECATED; } + FOnAssetStateChangeDelegate& GetOnAssetStateChangeDelegate() { return OnAssetStateChangeDelegate_DEPRECATED; } + + // Derived blueprint based components will check whether the template + // component contains updates that needs to processed. + bool NeedUpdateParameters() const; + bool NeedUpdateInputs() const; + + // Returns true if the last cook of the HDA was successful + bool WasLastCookSuccessful() const; + + // Returns true if a parameter definition update (excluding values) is needed. + bool IsParameterDefinitionUpdateNeeded() const; + + // Returns the BakeFolder. + FDirectoryPath GetBakeFolder() const; + + // Returns the TemporaryCookFolder. + FDirectoryPath GetTemporaryCookFolder() const; + + // Returns the BakeFolder, if it is not empty. Otherwise returns the plugin default bake folder. This + // function does not take the unreal_bake_folder attribute into account. + FString GetBakeFolderOrDefault() const; + + // Returns the TemporaryCookFolder, if it is not empty. Otherwise returns the plugin default temporary + // cook folder. This function does not take the unreal_temp_folder attribute into account. + FString GetTemporaryCookFolderOrDefault() const; + + EHoudiniEngineBakeOption GetHoudiniEngineBakeOption() const; + + bool GetReplacePreviousBake() const; + bool GetRemoveOutputAfterBake() const; + bool GetRecenterBakedActors() const; + + //------------------------------------------------------------------------------------------------ + // Mutators + //------------------------------------------------------------------------------------------------ + + // Set asset state + void SetAssetState(EHoudiniAssetState InNewState); + void SetAssetStateResult(EHoudiniAssetStateResult InAssetStateResult); + + //UFUNCTION(BlueprintSetter) + virtual void SetHoudiniAsset(UHoudiniAsset * NewHoudiniAsset); + + void SetCookingEnabled(const bool& bInCookingEnabled); + + void SetHasBeenLoaded(const bool& InLoaded); + + void SetHasBeenDuplicated(const bool& InDuplicated); + + bool SetTemporaryCookFolder(const FDirectoryPath& InDirectoryPath); + bool SetBakeFolder(const FDirectoryPath& InDirectoryPath); + + bool SetTemporaryCookFolderPath(const FString& NewPath); + bool SetBakeFolderPath(const FString& NewPath); + + // Marks the assets as needing a recook + void MarkAsNeedCook(); + // Marks the assets as needing a full rebuild + void MarkAsNeedRebuild(); + // Marks the asset as needing to be instantiated + void MarkAsNeedInstantiation(); + // The blueprint has been structurally modified + void MarkAsBlueprintStructureModified(); + // The blueprint has been modified but not structurally changed. + void MarkAsBlueprintModified(); + + // + void SetAssetCookCount(const int32& InCount); + + // + void SetHasComponentTransformChanged(const bool& InHasChanged); + + void SetOverrideGlobalProxyStaticMeshSettings(bool InEnable); + void SetEnableProxyStaticMeshOverride(bool InEnable); + void SetEnableProxyStaticMeshRefinementByTimerOverride(bool InEnable); + void SetProxyMeshAutoRefineTimeoutSecondsOverride(float InValue); + void SetEnableProxyStaticMeshRefinementOnPreSaveWorldOverride(bool InEnable); + void SetEnableProxyStaticMeshRefinementOnPreBeginPIEOverride(bool InEnable); + + // + virtual void OnHoudiniAssetChanged(); + + // Clear/disable the RefineMeshesTimer. + void ClearRefineMeshesTimer(); + + // Re-set the RefineMeshesTimer to its default value. + void SetRefineMeshesTimer(); + + virtual void OnRefineMeshesTimerFired(); + + // Returns true if the asset is valid for cook/bake + virtual bool IsComponentValid() const; + + // HoudiniEngineTick will be called by HoudiniEngineManager::Tick() + virtual void HoudiniEngineTick(); + + void ProcessBPTemplate(const bool& InIsGlobalCookingEnabled); + + void SetCookOnParameterChange(bool bEnable); + void SetCookOnTransformChange(bool bEnable); + void SetCookOnAssetInputCook(bool bEnable); + void SetOutputless(bool bEnable); + void SetUseOutputNodes(bool bEnable); + void SetOutputTemplateGeos(bool bEnable); + void SetUploadTransformsToHoudiniEngine(bool bEnable); + void SetLandscapeUseTempLayers(bool bEnable); + void SetEnableCurveEditing(bool bEnable); + + void SetHoudiniEngineBakeOption(const EHoudiniEngineBakeOption& InBakeOption); + void SetReplacePreviousBake(bool InReplace); + void SetRemoveOutputAfterBake(bool bInRemove); + void SetRecenterBakedActors(bool bInRecenter); + +#if WITH_EDITOR + // This alternate version of PostEditChange is called when properties inside structs are modified. The property that was actually modified + // is located at the tail of the list. The head of the list of the FStructProperty member variable that contains the property that was modified. + virtual void PostEditChangeProperty(FPropertyChangedEvent & PropertyChangedEvent) override; + + //Called after applying a transaction to the object. Default implementation simply calls PostEditChange. + virtual void PostEditUndo() override; +#endif + + // Whether this component is currently open in a Blueprint editor. This + // method is overridden by HoudiniAssetBlueprintComponent. + virtual bool HasOpenEditor() const { return false; }; + + void SetStaticMeshGenerationProperties(UStaticMesh* InStaticMesh); + void SetStaticMeshGenerationProperties(const FHoudiniStaticMeshGenerationProperties& InHSMGP); + void SetStaticMeshBuildSettings(const FMeshBuildSettings& InMBS); + + virtual void RegisterHoudiniComponent(UHoudiniAssetComponent* InComponent); + + virtual void OnRegister() override; + + // USceneComponent methods. + virtual FBoxSphereBounds CalcBounds(const FTransform & LocalToWorld) const override; + virtual void OnUpdateTransform(EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport) override; + + FBox GetAssetBounds(UHoudiniInput* IgnoreInput, bool bIgnoreGeneratedLandscape) const; + + // return the cached component template, if available. + virtual UHoudiniAssetComponent* GetCachedTemplate() const { return nullptr; } + + virtual FPrimitiveSceneProxy* CreateSceneProxy() override; + + + //------------------------------------------------------------------------------------------------ + // Supported Features + //------------------------------------------------------------------------------------------------ + + // Whether or not this component should be able to delete the Houdini nodes + // that correspond to the HoudiniAsset when being deregistered. + virtual bool CanDeleteHoudiniNodes() const { return true; } + virtual bool IsInputTypeSupported(EHoudiniInputType InType) const { return true; }; + virtual bool IsOutputTypeSupported(EHoudiniOutputType InType) const { return true; }; + + //------------------------------------------------------------------------------------------------ + // Characteristics + //------------------------------------------------------------------------------------------------ + + // Try to determine whether this component belongs to a preview actor. + // Preview / Template components need to sync their data for HDA cooks and output translations. + bool IsPreview() const { return bCachedIsPreview; }; + + virtual bool IsValidComponent() const { return true; }; + + //------------------------------------------------------------------------------------------------ + // Notifications + //------------------------------------------------------------------------------------------------ + + // TODO: After the cook worfklow rework, most of these won't be needed anymore, so clean up! + //FHoudiniAssetComponentEvent OnTemplateParametersChanged; + //FHoudiniAssetComponentEvent OnPreAssetCook; + //FHoudiniAssetComponentEvent OnCookCompleted; + //FHoudiniAssetComponentEvent OnOutputProcessingCompleted; + + /*virtual void BroadcastParametersChanged(); + virtual void BroadcastPreAssetCook(); + virtual void BroadcastCookCompleted();*/ + + virtual void OnPrePreCook() {}; + virtual void OnPostPreCook() {}; + virtual void OnPreOutputProcessing() {}; + virtual void OnPostOutputProcessing() {}; + virtual void OnPrePreInstantiation() {}; + + virtual void NotifyHoudiniRegisterCompleted() {}; + virtual void NotifyHoudiniPreUnregister() {}; + virtual void NotifyHoudiniPostUnregister() {}; + + virtual void OnFullyLoaded(); + + // Component template parameters have been updated. + // Broadcast delegate, and let preview components take care of the rest. + virtual void OnTemplateParametersChanged() { }; + virtual void OnBlueprintStructureModified() { }; + virtual void OnBlueprintModified() { }; + + // + // Begin: IHoudiniAssetStateEvents + // + + virtual void HandleOnHoudiniAssetStateChange(UObject* InHoudiniAssetContext, const EHoudiniAssetState InFromState, const EHoudiniAssetState InToState) override; + + FORCEINLINE + virtual FOnHoudiniAssetStateChange& GetOnHoudiniAssetStateChangeDelegate() override { return OnHoudiniAssetStateChangeDelegate_DEPRECATED; } + + // + // End: IHoudiniAssetStateEvents + // + + // Called by HandleOnHoudiniAssetStateChange when entering the PostCook state. Broadcasts OnPostCookDelegate. + void HandleOnPreInstantiation(); + void HandleOnPreCook(); + void HandleOnPostCook(); + void HandleOnPreOutputProcessing(); + void HandleOnPostOutputProcessing(); + + // Called by baking code after baking all outputs of this HAC (HoudiniEngineBakeOption) + void HandleOnPostBake(const bool bInSuccess); + +protected: + + // UActorComponents Method + virtual void OnComponentCreated() override; + virtual void OnComponentDestroyed(bool bDestroyingHierarchy) override; + + virtual void OnChildAttached(USceneComponent* ChildComponent) override; + + virtual void BeginDestroy() override; + + // + virtual void CreateRenderState_Concurrent(FRegisterComponentContext* Context) override; + + // Do any object - specific cleanup required immediately after loading an object. + // This is not called for newly - created objects, and by default will always execute on the game thread. + virtual void PostLoad() override; + + // Called after importing property values for this object (paste, duplicate or .t3d import) + // Allow the object to perform any cleanup for properties which shouldn't be duplicated or + // Are unsupported by the script serialization + virtual void PostEditImport() override; + + // + void OnActorMoved(AActor* Actor); + + // + void UpdatePostDuplicate(); + // + //static void AddReferencedObjects(UObject * InThis, FReferenceCollector & Collector); + + // Updates physics state & bounds. + // Should be call PostLoad and PostProcessing + void UpdatePhysicsState(); + +#if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 0) + ILevelInstanceInterface* GetLevelInstance() const; +#endif + + // Used to Convert this HAC's data to a cookable + bool TransferDataToCookable(UHoudiniCookable* HC); + +public: + + + void OnSessionConnected(); + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetHoudiniAssetData()->HoudiniAsset + // Houdini Asset associated with this component. + UPROPERTY() + TObjectPtr HoudiniAsset_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetParameterData()->bCookOnParameterChange + // Automatically cook when a parameter or input is changed + UPROPERTY() + bool bCookOnParameterChange_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetComponentData()->bUploadTransformsToHoudiniEngine + // Enables uploading of transformation changes back to Houdini Engine. + UPROPERTY() + bool bUploadTransformsToHoudiniEngine_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetComponentData()->bCookOnTransformChange + // Transform changes automatically trigger cooks. + UPROPERTY() + bool bCookOnTransformChange_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetInputData()->bCookOnCookableInputCook + // This asset will cook when its asset input cook + UPROPERTY() + bool bCookOnAssetInputCook_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetOutputData()->bOutputless + // Enabling this will prevent the HDA from producing any output after cooking. + UPROPERTY() + bool bOutputless_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetOutputData()->bOutputTemplateGeos + // Enabling this will allow outputing the asset's templated geos + UPROPERTY() + bool bOutputTemplateGeos_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetOutputData()->bUseOutputNodes + // Enabling this will allow outputing the asset's output nodes + UPROPERTY() + bool bUseOutputNodes_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetOutputData()->TemporaryCookFolder + // Temporary cook folder + UPROPERTY() + FDirectoryPath TemporaryCookFolder_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetBakingData()->BakeFolder + // Folder used for baking this asset's outputs (unless set by prim/detail attribute on the output). Falls back to + // the default from the plugin settings if not set. + UPROPERTY() + FDirectoryPath BakeFolder_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetOutputData()->bSplitMeshSupport + // Whether or not to support multiple mesh outputs on one HDA output. This is currently in Alpha testing. + UPROPERTY() + bool bSplitMeshSupport_DEPRECATED = false; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetOutputData()->StaticMeshGenerationProperties + // Generation properties for the Static Meshes generated by this Houdini Asset + UPROPERTY() + FHoudiniStaticMeshGenerationProperties StaticMeshGenerationProperties_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetOutputData()->StaticMeshBuildSettings + // Build Settings to be used when generating the Static Meshes for this Houdini Asset + UPROPERTY() + FMeshBuildSettings StaticMeshBuildSettings_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetProxyData()->bOverrideGlobalProxyStaticMeshSettings + // Override the global fast proxy mesh settings on this component? + UPROPERTY() + bool bOverrideGlobalProxyStaticMeshSettings_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetProxyData()->bEnableProxyStaticMeshOverride + // For StaticMesh outputs: should a fast proxy be created first? + UPROPERTY() + bool bEnableProxyStaticMeshOverride_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetProxyData()->bEnableProxyStaticMeshRefinementByTimerOverride + // If fast proxy meshes are being created, must it be baked as a StaticMesh after a period of no updates? + UPROPERTY() + bool bEnableProxyStaticMeshRefinementByTimerOverride_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetProxyData()->ProxyMeshAutoRefineTimeoutSecondsOverride + // If the option to automatically refine the proxy mesh via a timer has been selected, this controls the timeout in seconds. + UPROPERTY() + float ProxyMeshAutoRefineTimeoutSecondsOverride_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetProxyData()->bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride + // Automatically refine proxy meshes to UStaticMesh before the map is saved + UPROPERTY() + bool bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetProxyData()->bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride + // Automatically refine proxy meshes to UStaticMesh before starting a play in editor session + UPROPERTY() + bool bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride_DEPRECATED; + +#if WITH_EDITORONLY_DATA + + // DEPRECATED: get from Cookable instead: + // GetCookable()->bGenerateMenuExpanded + UPROPERTY() + bool bGenerateMenuExpanded_DEPRECATED; + + // DEPRECATED: get from Cookable instead: + // GetCookable()->bBakeMenuExpanded + bool bBakeMenuExpanded_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->bAssetOptionMenuExpanded + UPROPERTY() + bool bAssetOptionMenuExpanded_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetXXXXData()->bHelpAndDebugMenuExpanded + UPROPERTY() + bool bHelpAndDebugMenuExpanded_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetBakingData()->HoudiniEngineBakeOption + UPROPERTY() + EHoudiniEngineBakeOption HoudiniEngineBakeOption_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetBakingData()->bRemoveOutputAfterBake + // If true, then after a successful bake, the HACs outputs will be cleared and removed. + UPROPERTY() + bool bRemoveOutputAfterBake_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetBakingData()->bRecenterBakedActors + // If true, recenter baked actors to their bounding box center after bake + UPROPERTY() + bool bRecenterBakedActors_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetBakingData()->bReplacePreviousBake + // If true, replace the previously baked output (if any) instead of creating new objects + UPROPERTY() + bool bReplacePreviousBake_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetBakingData()->ActorBakeOption + UPROPERTY() + EHoudiniEngineActorBakeOption ActorBakeOption_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetOutputData()->bLandscapeUseTempLayers + UPROPERTY() + bool bLandscapeUseTempLayers_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetOutputData()->bEnableCurveEditing + UPROPERTY() + bool bEnableCurveEditing_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->bNeedToUpdateEditorProperties + // Indicates whether or not this component should update the editor's UI + // This is to prevent successive calls of the function for the same HDAs + UPROPERTY(Transient, DuplicateTransient) + bool bNeedToUpdateEditorProperties_DEPRECATED; +#endif + +protected: + + // DEPRECATED: get from CookableInstead: + // GetCookable()->NodeId + // Id of corresponding Houdini asset. + UPROPERTY(DuplicateTransient) + int32 AssetId_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->NodeIdsToCook + // Ids of the nodes that should be cook for this HAC + // This is for additional output and templated nodes if they are used. + UPROPERTY(Transient, DuplicateTransient) + TArray NodeIdsToCook_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->NodesToCookCookCounts + // Cook counts for nodes in the NodeIdsToCook array. + UPROPERTY(Transient, DuplicateTransient) + TMap OutputNodeCookCounts_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetInputData()->DownstreamCookables + // List of dependent downstream HACs that have us as an asset input + UPROPERTY(DuplicateTransient) + TSet> DownstreamHoudiniAssets; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->CookableGUID + // Unique GUID created by component. + UPROPERTY(DuplicateTransient) + FGuid ComponentGUID_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->HapiGUID + // GUID used to track asynchronous cooking requests. + UPROPERTY(DuplicateTransient) + FGuid HapiGUID_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetHoudiniAssetData()->HapiAssetName + // The asset name of the selected asset inside the asset library + UPROPERTY(DuplicateTransient) + FString HapiAssetName_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->CurrentState + // Current state of the asset + UPROPERTY(DuplicateTransient) + EHoudiniAssetState AssetState_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->CurrentStateResult + // Result of the current asset's state + UPROPERTY(DuplicateTransient) + EHoudiniAssetStateResult AssetStateResult_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetComponentData()->LastComponentTransform + // Used to compare transform changes and whether we need to + // send transform updates to Houdini. + UPROPERTY(DuplicateTransient) + FTransform LastComponentTransform_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetHoudiniAssetData()->SubAssetIndex + // Subasset index + UPROPERTY() + uint32 SubAssetIndex_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->CookCount + // Number of times this asset has been cooked. + UPROPERTY(DuplicateTransient) + int32 AssetCookCount_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->bHasBeenLoaded + UPROPERTY(DuplicateTransient) + bool bHasBeenLoaded_DEPRECATED; + + // DEPRECATED: get from CookableInstead: GetCookable()->bHasBeenDuplicated + // Sometimes, specifically when editing level instances, the Unreal Editor will duplicate the HDA, + // then duplicate it again, before we get a change to call UpdatePostDuplicate(). + // So bHasBeenDuplicated should not be cleared and is so not marked DuplicateTransient. + UPROPERTY() + bool bHasBeenDuplicated_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->bPendingDelete + UPROPERTY(DuplicateTransient) + bool bPendingDelete_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->bRecookRequested + UPROPERTY(DuplicateTransient) + bool bRecookRequested_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->bRebuildRequested + UPROPERTY(DuplicateTransient) + bool bRebuildRequested_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->bEnableCooking + UPROPERTY(DuplicateTransient) + bool bEnableCooking_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->bForceNeedUpdate + UPROPERTY(DuplicateTransient) + bool bForceNeedUpdate_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->bLastCookSuccess + UPROPERTY(DuplicateTransient) + bool bLastCookSuccess_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetParameterData()->bParameterDefinitionUpdateNeeded + // Indicates that the parameter state (excluding values) on the HAC and the instantiated node needs to be synced. + // The most common use for this would be a newly instantiated HDA that has only a default parameter interface + // from its asset definition, and needs to sync pre-cook. + UPROPERTY(DuplicateTransient) + bool bParameterDefinitionUpdateNeeded_DEPRECATED; + + UPROPERTY(DuplicateTransient) + bool bBlueprintStructureModified; // NOT COOKABLE + + UPROPERTY(DuplicateTransient) + bool bBlueprintModified; // NOT COOKABLE + +#if WITH_EDITORONLY_DATA + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetParameterData()->Parameters + UPROPERTY(Instanced) + TArray> Parameters_DEPRECATED; +#endif + +#if WITH_EDITORONLY_DATA + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetInputData()->Inputs + UPROPERTY(Instanced) + TArray> Inputs_DEPRECATED; +#endif + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetOutputData()->Outputs + UPROPERTY(Instanced) + TArray> Outputs_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetBakingData()->BakedOutputs + // The baked outputs from the last bake. + UPROPERTY() + TArray BakedOutputs_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetOutputData()->UntrackedOutputs + // Any actors that aren't explicitly + // tracked by output objects should be registered + // here so that they can be cleaned up. + UPROPERTY() + TArray> UntrackedOutputs_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetComponentData()->HandleComponents + UPROPERTY() + TArray> HandleComponents_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetComponentData()->bHasComponentTransformChanged + UPROPERTY(Transient, DuplicateTransient) + bool bHasComponentTransformChanged_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->bFullyLoaded + UPROPERTY(Transient, DuplicateTransient) + bool bFullyLoaded_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetPDGData()->PDGAssetLink + UPROPERTY() + TObjectPtr PDGAssetLink_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetPDGData()->PDGAssetLink + UPROPERTY() + bool bIsPDGAssetLinkInitialized_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetProxyData()->RefineMeshesTimer + // Timer that is used to trigger creation of UStaticMesh for all mesh outputs + // that still have UHoudiniStaticMeshes. The timer is cleared on PreCook and reset + // at the end of the PostCook. + UPROPERTY() + FTimerHandle RefineMeshesTimer_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetProxyData()->OnRefineMeshesTimerDelegate + // Delegate that is used to broadcast when RefineMeshesTimer fires + FOnRefineMeshesTimerDelegate OnRefineMeshesTimerDelegate_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetProxyData()->bNoProxyMeshNextCookRequested + // If true, don't build a proxy mesh next cook (regardless of global or override settings), + // instead build the UStaticMesh directly (if applicable for the output types). + UPROPERTY(DuplicateTransient) + bool bNoProxyMeshNextCookRequested_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetBakingData()->BakeAfterNextCook + // If true, bake the asset after its next cook. + UPROPERTY(DuplicateTransient) + EHoudiniBakeAfterNextCook BakeAfterNextCook_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->OnPreInstantiationDelegate + // Delegate to broadcast before instantiation + // Arguments are (HoudiniAssetComponent* HAC) + FOnPreInstantiationDelegate OnPreInstantiationDelegate_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->OnPreCookDelegate + // Delegate to broadcast after a post cook event + // Arguments are (HoudiniAssetComponent* HAC, bool IsSuccessful) + FOnPreCookDelegate OnPreCookDelegate_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->OnPostCookDelegate + // Delegate to broadcast after a post cook event + // Arguments are (HoudiniAssetComponent* HAC, bool IsSuccessful) + FOnPostCookDelegate OnPostCookDelegate_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->OnPostBakeDelegate + // Delegate to broadcast after baking the HAC. Not called when just baking individual outputs directly. + // Arguments are (HoudiniAssetComponent* HAC, bool bIsSuccessful) + FOnPostBakeDelegate OnPostBakeDelegate_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->OnPostOutputProcessingDelegate + FOnPostOutputProcessingDelegate OnPostOutputProcessingDelegate_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->OnPreOutputProcessingDelegate + FOnPreOutputProcessingDelegate OnPreOutputProcessingDelegate_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->OnAssetStateChangeDelegate + // Delegate that is broadcast when the asset state changes (HAC version). + FOnAssetStateChangeDelegate OnAssetStateChangeDelegate_DEPRECATED; + + // Cached flag of whether this object is considered to be a 'preview' component or not. + // This is typically useful in destructors when references to the World, for example, + // is no longer available. + UPROPERTY(Transient, DuplicateTransient) + bool bCachedIsPreview; // NOT COOKABLE + + // DEPRECATED: get from CookableInstead: + // GetCookable()->LastTickTime + // The last timestamp this component was ticked + // used to prioritize/limit the number of HAC processed per tick + UPROPERTY(Transient) + double LastTickTime_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetComponentData()->LastLiveSyncPingTime + // The last timestamp this component received a session sync update ping + // used to limit the frequency at which we ping HDAs for session sync updates + UPROPERTY(Transient) + double LastLiveSyncPingTime_DEPRECATED; + + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetParameterData()->ParameterPresetBuffer + UPROPERTY() + TArray ParameterPresetBuffer_DEPRECATED; + + // + // Begin: IHoudiniAssetStateEvents + // + + // DEPRECATED: get from CookableInstead: + // GetCookable()->OnHoudiniAssetStateChangeDelegate + // Delegate that is broadcast when AssetState changes + FOnHoudiniAssetStateChange OnHoudiniAssetStateChangeDelegate_DEPRECATED; + + // + // End: IHoudiniAssetStateEvents + // + + // DEPRECATED: get from CookableInstead: + // GetCookable()->PreCookCallbacks + // Store any PreCookCallbacks here until they HAC is ready to process them during the PreCook event. + TArray< TFunction > PreCookCallbacks_DEPRECATED; + +#if WITH_EDITORONLY_DATA + +protected: + // DEPRECATED: get from CookableInstead: + // GetCookable()->GetProxyData()->bAllowPlayInEditorRefinement + UPROPERTY(Transient, DuplicateTransient) + bool bAllowPlayInEditorRefinement_DEPRECATED; + +#endif + + bool bMigrateDataToCookableOnPostLoad; +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniAssetStateTypes.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/ObjectMacros.h" + +#include "HoudiniAssetStateTypes.generated.h" + + +UENUM() +enum class EHoudiniAssetState : uint8 +{ + // Loaded / Duplicated HDA, + // Will need to be instantiated upon change/update + NeedInstantiation, + + // Newly created HDA, fetch its default parameters then proceed to PreInstantiation + NewHDA, + + // Newly created HDA, after default parameters fetch, needs to be instantiated immediately + PreInstantiation, + + // Instantiating task in progress + Instantiating, + + // Instantiated HDA, needs to be cooked immediately + PreCook, + + // Cooking task in progress + Cooking, + + // Cooking has finished + PostCook, + + // Cooked HDA, needs to be processed immediately + PreProcess, + + // Processing task in progress + Processing, + + // Processed / Updated HDA + // Will need to be cooked upon change/update + None, + + // Asset needs to be rebuilt (Deleted/Instantiated/Cooked) + NeedRebuild, + + // Asset needs to be deleted + NeedDelete, + + // Deleting + Deleting, + + // Process component template. This is ticking has very limited + // functionality, typically limited to checking for parameter updates + // in order to trigger PostEditChange() to run construction scripts again. + ProcessTemplate, + + // Used when an HDA has been instanced using Level Instances. + Dormant, +}; + +UENUM() +enum class EHoudiniAssetStateResult : uint8 +{ + None, + Working, + Success, + FinishedWithError, + FinishedWithFatalError, + Aborted +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniCookable.cpp`: + +```cpp +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniCookable.h" + +#include "HoudiniEngineRuntimePrivatePCH.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetBlueprintComponent.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniHandleComponent.h" +#include "HoudiniInstancedActorComponent.h" +#include "HoudiniLandscapeRuntimeUtils.h" +#include "HoudiniNodeSyncComponent.h" +#include "HoudiniOutput.h" +#include "HoudiniParameter.h" +#include "HoudiniParameterButton.h" +#include "HoudiniParameterButtonStrip.h" +#include "HoudiniPDGAssetLink.h" +#include "HoudiniStaticMeshComponent.h" +#if WITH_EDITOR + #include "HoudiniEditorAssetStateSubsystemInterface.h" +#endif + +#include "HoudiniParameterInt.h" +#include "Components/SplineComponent.h" +#include "Components/HierarchicalInstancedStaticMeshComponent.h" +#include "InstancedFoliageActor.h" +#include "LevelInstance/LevelInstanceSubsystem.h" +#include "TimerManager.h" + +UHoudiniParameter* +UCookableParameterData::FindMatchingParameter(UHoudiniParameter* InOtherParam) +{ + if (!IsValid(InOtherParam)) + return nullptr; +#if WITH_EDITORONLY_DATA + for (auto CurrentParam : Parameters) + { + if (!IsValid(CurrentParam)) + continue; + + if (CurrentParam->Matches(*InOtherParam)) + return CurrentParam; + } +#endif + + return nullptr; +} + +// +// HOUDINI ASSET DATA +// +UCookableHoudiniAssetData::UCookableHoudiniAssetData(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , SubAssetIndex(-1) + , HapiAssetName(TEXT("")) +{ +#if WITH_EDITORONLY_DATA + HoudiniAsset = nullptr; +#endif +} + + + +// +// PARAMETER DATA +// +UCookableParameterData::UCookableParameterData(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , bCookOnParameterChange(true) + , bParameterDefinitionUpdateNeeded(false) +{ + +} + + + +// +// INPUT DATA +// +UCookableInputData::UCookableInputData(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , bCookOnInputChange(true) + , bCookOnCookableInputCook(true) +{ + +} + + +bool +UCookableInputData::NeedsToWaitForInputHoudiniAssets() +{ +#if WITH_EDITORONLY_DATA + for (auto& CurrentInput : Inputs) + { + if (!IsValid(CurrentInput)) + continue; + + EHoudiniInputType CurrentInputType = CurrentInput->GetInputType(); + if (!CurrentInput->IsAssetInput()) + continue; + + TArray>* ObjectArray = CurrentInput->GetHoudiniInputObjectArray(CurrentInputType); + if (!ObjectArray) + continue; + + for (auto& CurrentInputObject : (*ObjectArray)) + { + // Get the input HDA + UHoudiniAssetComponent* InputHAC = CurrentInputObject + ? Cast(CurrentInputObject->GetObject()) + : nullptr; + + if (!InputHAC) + continue; + + // If the input HDA needs to be instantiated, force him to instantiate + // if the input HDA is in any other state than None, we need to wait for him + // to finish whatever it's doing + if (InputHAC->GetAssetState() == EHoudiniAssetState::NeedInstantiation) + { + // Tell the input HAC to instantiate + InputHAC->SetAssetState(EHoudiniAssetState::PreInstantiation); + + // We need to wait + return true; + } + else if (InputHAC->GetAssetState() != EHoudiniAssetState::None) + { + // We need to wait + return true; + } + } + } +#endif + + return false; +} + + + +// +// OUTPUT DATA +// +UCookableOutputData::UCookableOutputData(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , TemporaryCookFolder() + , bOutputless(false) + , bCreateSceneComponents (true) + , bOutputTemplateGeos(false) + , bUseOutputNodes(true) + , bSplitMeshSupport(false) + , bEnableCurveEditing(false) + , bLandscapeUseTempLayers(false) +{ + StaticMeshGenerationProperties = FHoudiniEngineRuntimeUtils::GetDefaultStaticMeshGenerationProperties(); + StaticMeshBuildSettings = FHoudiniEngineRuntimeUtils::GetDefaultMeshBuildSettings(); +} + + +// +// BAKE DATA +// +UCookableBakingData::UCookableBakingData(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , HoudiniEngineBakeOption(EHoudiniEngineBakeOption::ToActor) + , BakeFolder() + , BakeAfterNextCook(EHoudiniBakeAfterNextCook::Disabled) + , bRemoveOutputAfterBake(false) + , bRecenterBakedActors(false) + , bReplacePreviousBake(false) + , ActorBakeOption(EHoudiniEngineActorBakeOption::OneActorPerComponent) +{ + +} + + +// +// PROXY DATA +// +UCookableProxyData::UCookableProxyData(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , bNoProxyMeshNextCookRequested(false) + , bOverrideGlobalProxyStaticMeshSettings(false) + , bEnableProxyStaticMeshOverride(false) + , bEnableProxyStaticMeshRefinementByTimerOverride(true) + , ProxyMeshAutoRefineTimeoutSecondsOverride(10.0f) + , bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride(true) + , bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride(true) + , bAllowPlayInEditorRefinement(false) +{ + // Initialize default proxy settings + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings) + { + bEnableProxyStaticMeshOverride = HoudiniRuntimeSettings->bEnableProxyStaticMesh; + bEnableProxyStaticMeshRefinementByTimerOverride = HoudiniRuntimeSettings->bEnableProxyStaticMeshRefinementByTimer; + ProxyMeshAutoRefineTimeoutSecondsOverride = HoudiniRuntimeSettings->ProxyMeshAutoRefineTimeoutSeconds; + bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride = HoudiniRuntimeSettings->bEnableProxyStaticMeshRefinementOnPreSaveWorld; + bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride = HoudiniRuntimeSettings->bEnableProxyStaticMeshRefinementOnPreBeginPIE; + } +} + + + +// +// COMPONENT DATA +// +UCookableComponentData::UCookableComponentData(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , LastComponentTransform(FTransform()) + , bHasComponentTransformChanged(false) + , bUploadTransformsToHoudiniEngine(true) + , bCookOnTransformChange(false) + , LastLiveSyncPingTime(0.0) +{ + +} + + + +// +// PDG DATA +// +UCookablePDGData::UCookablePDGData(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , bIsPDGAssetLinkInitialized(false) +{ + PDGAssetLink = nullptr; +} + +void +UCookablePDGData::SetPDGAssetLink(UHoudiniPDGAssetLink* InPDGAssetLink) +{ + // Check the object validity + if (!IsValid(InPDGAssetLink)) + return; + + // If it is the same object, do nothing. + if (InPDGAssetLink == PDGAssetLink) + return; + + PDGAssetLink = InPDGAssetLink; +} + + + +// +// HOUDINI COOKABLE +// +UHoudiniCookable::UHoudiniCookable(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + NodeId = -1; + CurrentState = EHoudiniAssetState::NewHDA; + CurrentStateResult = EHoudiniAssetStateResult::None; + CookCount = 0; + Name = FString(); + + // Create unique cookable GUID. + CookableGUID = FGuid::NewGuid(); + + // Make an invalid GUID, since we do not have any cooking requests. + HapiGUID.Invalidate(); + + bHasBeenLoaded = false; + bHasBeenDuplicated = false; + bPendingDelete = false; + bRecookRequested = false; + bRebuildRequested = false; + bEnableCooking = true; + bForceNeedUpdate = false; + bLastCookSuccess = false; + //bBlueprintStructureModified = false; + //bBlueprintModified = false; + bFullyLoaded = false; + LastTickTime = 0.0; + //LastLiveSyncPingTime = 0.0; + + //bCanDeleteHoudiniNodes = true; + + bHasHoudiniAsset = false; + HoudiniAssetData = CreateDefaultSubobject(TEXT("HoudiniAssetData")); + //HoudiniAssetData = NewObject(this, TEXT("HoudiniAssetData"), RF_Public); + + bHasInputs = false; + InputData = CreateDefaultSubobject(TEXT("InputData")); + + bHasParameters = false; + ParameterData = CreateDefaultSubobject(TEXT("ParameterData")); + //ParameterData = NewObject(this, TEXT("ParameterData"), RF_Public); + + bHasComponent = false; + ComponentData = CreateDefaultSubobject(TEXT("ComponentData")); + + bHasOutputs = false; + OutputData = CreateDefaultSubobject(TEXT("OutputData")); + + bHasPDG = false; + PDGData = CreateDefaultSubobject(TEXT("PDGData")); + + bHasBaking = false; + BakingData = CreateDefaultSubobject(TEXT("BakingData")); + + bHasProxy = false; + ProxyData = CreateDefaultSubobject(TEXT("ProxyData")); + + bNeedToUpdateEditorProperties = false; + bIsPCG = false; + bIsLandscapeModification = true; + bDoSlateNotifications = true; + bAllowUpdateEditorProperties = true; + + /* + // + // Set component properties. + // + Mobility = EComponentMobility::Static; + + SetGenerateOverlapEvents(false); + + // Similar to UMeshComponent. + CastShadow = true; + bUseAsOccluder = true; + bCanEverAffectNavigation = true; + + // This component requires render update. + bNeverNeedsRenderUpdate = false; + + Bounds = FBox(ForceInitToZero); + */ +#if WITH_EDITORONLY_DATA + bGenerateMenuExpanded = true; + bBakeMenuExpanded = true; + bAssetOptionMenuExpanded = true; + bHelpAndDebugMenuExpanded = true; +#endif + + AssetEditorId = FName(); +} + +UHoudiniCookable::~UHoudiniCookable() +{ + // Handled by GC + // Unregister ourself so our houdini nodes can be deleted. + // FHoudiniEngineRuntime::Get().UnRegisterHoudiniCookable(this); +} + +template bool +HoudiniCheckAndSetValue(Type & Dest, Type & Src) +{ + if(Dest == Src) + return false; + Dest = Src; + return true; +} + +bool +HoudiniAreObjectsEqual(const UObject* A, const UObject*B) +{ + if(!IsValid(A) && !IsValid(B)) + return true; + + if(!IsValid(A) || !IsValid(B)) + return false; + + if(A->GetClass() != B->GetClass()) + return false; + + for(TFieldIterator PropIt(A->GetClass()); PropIt; ++PropIt) + { + FProperty* Property = *PropIt; + + const void* ValueA = Property->ContainerPtrToValuePtr(A); + const void* ValueB = Property->ContainerPtrToValuePtr(B); + + if(!Property->Identical(ValueA, ValueB)) + { + return false; + } + } + return true; +} + + +bool +UHoudiniCookable::SetParameterData(UCookableParameterData* InParameterData) +{ + bool bChanged = false; +#if WITH_EDITORONLY_DATA + bChanged |= HoudiniCheckAndSetValue(ParameterData->bCookOnParameterChange, InParameterData->bCookOnParameterChange); + bChanged |= HoudiniCheckAndSetValue(ParameterData->ParameterPresetBuffer, InParameterData->ParameterPresetBuffer); + bChanged |= HoudiniCheckAndSetValue(ParameterData->bParameterDefinitionUpdateNeeded, InParameterData->bParameterDefinitionUpdateNeeded); + + if(ParameterData->Parameters.Num() != InParameterData->Parameters.Num()) + { + ParameterData->Parameters.SetNum(InParameterData->Parameters.Num()); + bChanged = true; + } + + for (int Index = 0; Index < ParameterData->Parameters.Num(); Index++) + { + if (!HoudiniAreObjectsEqual(ParameterData->Parameters[Index], InParameterData->Parameters[Index])) + { + bChanged = true; + if(IsValid(InParameterData->Parameters[Index])) + { + + // ParameterData->Parameters[Index] = DuplicateObject(InParameterData->Parameters[Index], ParameterData); + ParameterData->Parameters[Index] = NewObject(ParameterData, InParameterData->Parameters[Index].GetClass()); + // UEngine::FCopyPropertiesForUnrelatedObjectsParams Params; + UEngine::CopyPropertiesForUnrelatedObjects(InParameterData->Parameters[Index], ParameterData->Parameters[Index]); + + ParameterData->Parameters[Index]->MarkChanged(true); + } + else + { + ParameterData->Parameters[Index] = nullptr; + } + } + } +#endif + return bChanged; + +} + +bool +UHoudiniCookable::SetInputData(UCookableInputData* InInputData) +{ + bool bChanged = false; +#if WITH_EDITORONLY_DATA + bChanged |= HoudiniCheckAndSetValue(InputData->bCookOnInputChange, InInputData->bCookOnInputChange); + bChanged |= HoudiniCheckAndSetValue(InputData->bCookOnCookableInputCook, InInputData->bCookOnCookableInputCook); + + if(InputData->Inputs.Num() != InInputData->Inputs.Num()) + { + InputData->Inputs.SetNum(InInputData->Inputs.Num()); + bChanged = true; + } + + for(int Index = 0; Index < InputData->Inputs.Num(); Index++) + { + if(!HoudiniAreObjectsEqual(InputData->Inputs[Index], InInputData->Inputs[Index])) + { + bChanged = true; + if(IsValid(InInputData->Inputs[Index])) + { + InputData->Inputs[Index] = DuplicateObject(InInputData->Inputs[Index], this); + InputData->Inputs[Index]->MarkChanged(true); + } + else + { + InputData->Inputs[Index] = nullptr; + } + } + } +#endif + return bChanged; +} + +UHoudiniAsset* +UHoudiniCookable::GetHoudiniAsset() +{ +#if WITH_EDITORONLY_DATA + return IsHoudiniAssetSupported() ? HoudiniAssetData->HoudiniAsset : nullptr; +#else + return nullptr; +#endif +} + +UHoudiniPDGAssetLink* +UHoudiniCookable::GetPDGAssetLink() +{ + return IsPDGSupported() ? PDGData->PDGAssetLink : nullptr; +} + +FString +UHoudiniCookable::GetHoudiniAssetName() const +{ + if (!IsHoudiniAssetSupported()) + return FString(); + +#if WITH_EDITORONLY_DATA + return IsValid(HoudiniAssetData->HoudiniAsset) ? HoudiniAssetData->HoudiniAsset->GetName() : TEXT(""); +#else + return TEXT(""); +#endif +} + +USceneComponent* +UHoudiniCookable::GetComponent() const +{ + if (!IsComponentSupported()) + return nullptr; + + return ComponentData->Component.Get(); +} + +AActor* +UHoudiniCookable::GetOwner() const +{ + USceneComponent* Comp = GetComponent(); + if (!Comp) + return nullptr; + + return Comp->GetOwner(); +} + + +UWorld* +UHoudiniCookable::GetWorld() const +{ + // Should we return GetComponent()->GetWold() first? it should be same as the Actor's world... + return GetOwner() ? GetOwner()->GetWorld() : nullptr; +} + + +ULevel* +UHoudiniCookable::GetLevel() const +{ + USceneComponent* MyComp = GetComponent(); + AActor* MyOwner = MyComp ? MyComp->GetOwner() : nullptr; + + if (MyOwner) + return MyOwner->GetLevel(); + + if(MyComp) + return MyComp->GetTypedOuter(); + + return nullptr; +} + +FDirectoryPath +UHoudiniCookable::GetBakeFolder() const +{ + return BakingData->BakeFolder; +} + +FDirectoryPath +UHoudiniCookable::GetTemporaryCookFolder() const +{ + return OutputData->TemporaryCookFolder; +} + +FString +UHoudiniCookable::GetTemporaryCookFolderOrDefault() const +{ + return !OutputData->TemporaryCookFolder.Path.IsEmpty() ? OutputData->TemporaryCookFolder.Path : FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder(); +} + +FString +UHoudiniCookable::GetBakeFolderOrDefault() const +{ + return !BakingData->BakeFolder.Path.IsEmpty() ? BakingData->BakeFolder.Path : FHoudiniEngineRuntime::Get().GetDefaultBakeFolder(); +} + +FGuid& +UHoudiniCookable::GetHapiGUID() +{ + return HapiGUID; +} + +FString +UHoudiniCookable::GetHapiAssetName() const +{ + if(IsHoudiniAssetSupported()) + return HoudiniAssetData->HapiAssetName; + + // TODO COOKABLE: return empty? return name? + return NodeName; +} + +FGuid +UHoudiniCookable::GetCookableGUID() const +{ + return CookableGUID; +} + +bool +UHoudiniCookable::SetTemporaryCookFolderPath(const FString& NewPath) +{ + if (OutputData->TemporaryCookFolder.Path.Equals(NewPath)) + return false; + + if (OutputData->TemporaryCookFolder.Path == NewPath) + return false; + + OutputData->TemporaryCookFolder.Path = NewPath; + + return true; +} + +bool +UHoudiniCookable::SetBakeFolderPath(const FString& NewPath) +{ + if (BakingData->BakeFolder.Path.Equals(NewPath)) + return false; + + if (BakingData->BakeFolder.Path == NewPath) + return false; + + BakingData->BakeFolder.Path = NewPath; + + return true; +} + +bool +UHoudiniCookable::SetTemporaryCookFolder(const FDirectoryPath& InPath) +{ + if (OutputData->TemporaryCookFolder.Path.Equals(InPath.Path)) + return false; + + OutputData->TemporaryCookFolder = InPath; + + return true; +} + +bool +UHoudiniCookable::SetBakeFolder(const FDirectoryPath& InPath) +{ + if (BakingData->BakeFolder.Path.Equals(InPath.Path)) + return false; + + BakingData->BakeFolder = InPath; + + return true; +} + +bool +UHoudiniCookable::IsOwnerSelected() const +{ + return GetOwner() ? GetOwner()->IsSelected() : false; +} + +bool +UHoudiniCookable::ShouldTryToStartFirstSession() const +{ +#if WITH_EDITORONLY_DATA + if(IsHoudiniAssetSupported() && !HoudiniAssetData->HoudiniAsset) + return false; +#endif + + if (GetComponent()) + { + // We dont want NodeSync components to automatically start sessions + if (GetComponent()->IsA()) + return false; + } + + // Only try to start the default session if we have an "active" HAC + switch (CurrentState) + { + case EHoudiniAssetState::NewHDA: + case EHoudiniAssetState::PreInstantiation: + case EHoudiniAssetState::Instantiating: + case EHoudiniAssetState::PreCook: + case EHoudiniAssetState::Cooking: + return true; + + case EHoudiniAssetState::NeedInstantiation: + case EHoudiniAssetState::PostCook: + case EHoudiniAssetState::PreProcess: + case EHoudiniAssetState::Processing: + case EHoudiniAssetState::None: + case EHoudiniAssetState::NeedRebuild: + case EHoudiniAssetState::NeedDelete: + case EHoudiniAssetState::Deleting: + case EHoudiniAssetState::ProcessTemplate: + case EHoudiniAssetState::Dormant: + return false; + }; + + return false; +} + + +#if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 0) +ILevelInstanceInterface* +UHoudiniCookable::GetLevelInstance() const +{ + // Find the level instanced which "owns" this HDA, if it exists. + AActor* Actor = Cast(this->GetOwner()); + if (!Actor) + return nullptr; + + UWorld* World = Actor->GetWorld(); + if (!World) + return nullptr; + + ULevelInstanceSubsystem* LevelInstanceSystem = World->GetSubsystem(); + if (!LevelInstanceSystem) + return nullptr; + + return LevelInstanceSystem->GetOwningLevelInstance(Actor->GetLevel()); +} +#endif + + +void +UHoudiniCookable::SetHoudiniAsset(UHoudiniAsset* InHoudiniAsset) +{ +#if WITH_EDITORONLY_DATA + // Check the asset validity + if (!IsValid(InHoudiniAsset)) + return; + + if (!IsHoudiniAssetSupported()) + return; + + // If it is the same asset, do nothing. + if (InHoudiniAsset == HoudiniAssetData->HoudiniAsset) + return; + + HoudiniAssetData->HoudiniAsset = InHoudiniAsset; +#endif +} + +void +UHoudiniCookable::OnHoudiniAssetChanged() +{ +#if WITH_EDITORONLY_DATA + // TODO: clear input/params/outputs? + if(IsParameterSupported()) + ParameterData->Parameters.Empty(); +#endif + +#if WITH_EDITORONLY_DATA + if (IsInputSupported()) + InputData->Inputs.Empty(); +#endif + + if (IsOutputSupported()) + OutputData->Outputs.Empty(); + + // The asset has been changed, mark us as needing to be reinstantiated + MarkAsNeedInstantiation(); + + // Force an update on the next tick + bForceNeedUpdate = true; +} + + +void UHoudiniCookable::SetComponent(USceneComponent* InComp) +{ + if (!IsComponentSupported()) + return; + + ComponentData->Component = InComp; +} + +void UHoudiniCookable::SetHoudiniAssetComponent(UHoudiniAssetComponent* InComp) +{ + if (!IsComponentSupported()) + return; + + ComponentData->Component = InComp; +} + +void +UHoudiniCookable::SetCurrentState(EHoudiniAssetState InNewState) +{ + const EHoudiniAssetState OldState = CurrentState; + CurrentState = InNewState; + +#if WITH_EDITOR + IHoudiniEditorAssetStateSubsystemInterface* const EditorSubsystem = IHoudiniEditorAssetStateSubsystemInterface::Get(); + if (EditorSubsystem) + EditorSubsystem->NotifyOfHoudiniAssetStateChange(this, OldState, InNewState); +#endif + HandleOnHoudiniAssetStateChange(this, OldState, InNewState); +} + +void +UHoudiniCookable::SetCurrentStateResult(EHoudiniAssetStateResult InResult) +{ + CurrentStateResult = InResult; +} + +void +UHoudiniCookable::HandleOnHoudiniAssetStateChange(UObject* InHoudiniAssetContext, const EHoudiniAssetState InFromState, const EHoudiniAssetState InToState) +{ + IHoudiniAssetStateEvents::HandleOnHoudiniAssetStateChange(InHoudiniAssetContext, InFromState, InToState); + + if (InFromState == InToState) + return; + + if (this != InHoudiniAssetContext) + return; + + FOnAssetStateChangeDelegate StateChangedDelegate = GetOnAssetStateChangeDelegate(); + if (StateChangedDelegate.IsBound()) + StateChangedDelegate.Broadcast(this, InFromState, InToState); + + FOnCookableStateChangeDelegate& CookableStateChangeDelegate = GetOnCookableStateChangeDelegate(); + if (CookableStateChangeDelegate.IsBound()) + CookableStateChangeDelegate.Broadcast(this, InFromState, InToState); + + if (InToState == EHoudiniAssetState::PreInstantiation) + { + HandleOnPreInstantiation(); + } + + if (InToState == EHoudiniAssetState::PreCook) + { + HandleOnPreCook(); + } + + if (InToState == EHoudiniAssetState::PostCook) + { + HandleOnPostCook(); + } +} + + +void +UHoudiniCookable::HandleOnPreInstantiation() +{ + if (OnPreInstantiationDelegate.IsBound()) + OnPreInstantiationDelegate.Broadcast(this); +} + +void +UHoudiniCookable::QueuePreCookCallback(const TFunction& CallbackFn) +{ + PreCookCallbacks.Add(CallbackFn); +} + + +void +UHoudiniCookable::HandleOnPreCook() +{ + // Process the PreCookCallbacks array first + for (auto CallbackFn : PreCookCallbacks) + { + CallbackFn(this); + } + PreCookCallbacks.Empty(); + + if (OnPreCookDelegate.IsBound()) + OnPreCookDelegate.Broadcast(this); +} + +void +UHoudiniCookable::HandleOnPostCook() +{ + if (OnPostCookDelegate.IsBound()) + OnPostCookDelegate.Broadcast(this, bLastCookSuccess); +} + +void +UHoudiniCookable::HandleOnPreOutputProcessing() +{ + if (OnPreOutputProcessingDelegate.IsBound()) + { + OnPreOutputProcessingDelegate.Broadcast(this, true); + } +} + +void +UHoudiniCookable::HandleOnPostOutputProcessing() +{ + if (OnPostOutputProcessingDelegate.IsBound()) + { + OnPostOutputProcessingDelegate.Broadcast(this, true); + } +} + +void +UHoudiniCookable::HandleOnPostBake(bool bInSuccess) +{ + if (BakingData->OnPostBakeDelegate.IsBound()) + BakingData->OnPostBakeDelegate.Broadcast(this, bInSuccess); +} + +void +UHoudiniCookable::UpdateDormantStatus() +{ +#if WITH_EDITOR + // This function checks if we should go into or out of dormant status. +#if (ENGINE_MAJOR_VERSION <= 5 && ENGINE_MINOR_VERSION < 1) + return; +#else + ILevelInstanceInterface* LevelInstance = GetLevelInstance(); + if (!LevelInstance) + return; + + if (GetCurrentState() == EHoudiniAssetState::Dormant) + { + // If this HDA was previously dormant, and the level instance is editable, it means + // the level instance has just been made editable. So reset to a state where the HDA + // can be used. + if (LevelInstance->IsEditing()) + SetCurrentState(EHoudiniAssetState::None); + } + else if (GetCurrentState() == EHoudiniAssetState::None) + { + // If we're not doing anything, and the level instance not editable, flip the state + // back to dormant. This highlights a potential problem that the user could commit + // a level instance before its finished cooking, but I'm not sure we can prevent that. + if (!LevelInstance->IsEditing()) + SetCurrentState(EHoudiniAssetState::Dormant); + } +#endif +#endif +} + + +bool +UHoudiniCookable::NeedUpdateParameters() const +{ +#if WITH_EDITORONLY_DATA + if (!IsParameterSupported()) + return false; + + // No need to cook on param change + if(!ParameterData->bCookOnParameterChange) + return false; + + // Go through all our parameters, return true if they have been updated + for (auto CurrentParm : ParameterData->Parameters) + { + if (!IsValid(CurrentParm)) + continue; + + if (!CurrentParm->HasChanged()) + continue; + + // See if the parameter doesn't require an update + // (because it has failed to upload previously or has been loaded) + if (!CurrentParm->NeedsToTriggerUpdate()) + continue; + + return true; + } +#endif + return false; +} + +bool +UHoudiniCookable::IsInputTypeSupported(EHoudiniInputType InType) +{ + if (!IsInputSupported()) + return false; + + if (GetComponent()) + { + // If we have a component, let it decide what input types are supported + UHoudiniAssetComponent* MyHAC = Cast(GetComponent()); + if (MyHAC) + return MyHAC->IsInputTypeSupported(InType); + } + + return true; +}; + +bool +UHoudiniCookable::IsOutputTypeSupported(EHoudiniOutputType InType) +{ + if (!IsOutputSupported()) + return false; + + if (GetComponent()) + { + // If we have a component, let it decide what output types it supprots + UHoudiniAssetComponent* MyHAC = Cast(GetComponent()); + if (MyHAC) + return MyHAC->IsOutputTypeSupported(InType); + } + + return true; +} + + + +bool +UHoudiniCookable::NeedUpdateInputs() const +{ +#if WITH_EDITORONLY_DATA + if (!IsInputSupported()) + return false; + + // No need to cook on input change + if (!InputData->bCookOnInputChange) + return false; + + // Go through all our inputs, return true if they have been updated + for (auto CurrentInput : InputData->Inputs) + { + if (!IsValid(CurrentInput)) + continue; + + if (!CurrentInput->HasChanged()) + continue; + + // See if the input doesn't require an update + // (because it has failed to upload previously or has been loaded) + if (!CurrentInput->NeedsToTriggerUpdate()) + continue; + + return true; + } +#endif + + return false; +} + +bool +UHoudiniCookable::NeedUpdateOutputs() const +{ + if (!IsOutputSupported()) + return false; + + // Go through all outputs, filter the editable nodes. Return true if they have been updated. + for (auto CurrentOutput : OutputData->Outputs) + { + if (!IsValid(CurrentOutput)) + continue; + + // We only care about editable outputs + if (!CurrentOutput->IsEditableNode()) + continue; + + // Trigger an update if the output object is marked as modified by user. + TMap& OutputObjects = CurrentOutput->GetOutputObjects(); + for (auto& NextPair : OutputObjects) + { + for (auto Component : NextPair.Value.OutputComponents) + { + // For now, only editable curves can trigger update + UHoudiniSplineComponent* HoudiniSplineComponent = Cast(Component); + if (!HoudiniSplineComponent) + continue; + + // Output curves cant trigger an update! + if (HoudiniSplineComponent->bIsOutputCurve) + continue; + + if (HoudiniSplineComponent->NeedsToTriggerUpdate()) + return true; + } + } + } + + return false; +} + +bool +UHoudiniCookable::NeedUpdate() const +{ + TRACE_CPUPROFILER_EVENT_SCOPE(UHoudiniCookable::NeedUpdate); + + // It is important to check this when dealing with Blueprints since the + // preview components start receiving events from the template component + // before the preview component have finished initialization. + if (!IsFullyLoaded()) + return false; + +#if WITH_EDITORONLY_DATA + // If we support HDAs - we should have one assigned. + if (IsHoudiniAssetSupported() && !HoudiniAssetData->HoudiniAsset) + return false; +#endif + + if (bForceNeedUpdate || bRecookRequested) // || bRebuildRequested ?? + return true; + + // Check if the HAC's transform has changed and we need to cook because of it + if (IsComponentSupported() && ComponentData->bHasComponentTransformChanged && ComponentData->bCookOnTransformChange) + return true; + + // If we don't want to cook on parameter/input change dont bother looking for updates + //if (!bCookOnParameterChange && !bRecookRequested && !bRebuildRequested) + // return false; + + // If we support parameters - see if one has changed + if(IsParameterSupported() && NeedUpdateParameters()) + return true; + + // If we support inputs - see if one has changed + if ( IsInputSupported() && NeedUpdateInputs()) + return true; + + // See if our (editable) outputs needs an update + if ( IsOutputSupported() && NeedUpdateOutputs()) + return true; + + return false; +} + + +FString +UHoudiniCookable::GetDisplayName() const +{ + if (GetOwner()) + return GetOwner()->GetActorNameOrLabel(); + + return GetName(); +} + +void +UHoudiniCookable::ClearNodesToCook() +{ + NodeIdsToCook.Empty(); + NodesToCookCookCounts.Empty(); +} + + +void +UHoudiniCookable::UpdatePostDuplicate() +{ + if (IsComponentSupported() && IsValid(ComponentData->Component.Get())) + { + // TODO: + // - Keep the output objects/components (remove duplicatetransient on the output object uproperties) + // - Duplicate created objects (ie SM) and materials + // - Update the output components to use these instead + // This should remove the need for a cook on duplicate + + // For now, we simply clean some of our component's children component manually + const TArray Children = ComponentData->Component->GetAttachChildren(); + + for (auto& NextChild : Children) + { + if (!NextChild || !IsValid(NextChild)) + continue; + + // We don't want to remove components that were added in a Blueprint Template + if (NextChild->IsCreatedByConstructionScript()) + continue; + + USceneComponent* ComponentToRemove = nullptr; + if (NextChild->IsA()) + { + // This also covers UStaticMeshComponent derived instancers, such as UInstancedStaticMeshComponent, + // and UHierarchicalInstancedStaticMeshComponent + ComponentToRemove = NextChild; + } + else if (NextChild->IsA()) + { + ComponentToRemove = NextChild; + } + else if (NextChild->IsA()) + { + ComponentToRemove = NextChild; + } + else if (NextChild->IsA()) + { + // The actors attached to the HoudiniAssetActor are not duplicated, so we only + // have to handle the component. + ComponentToRemove = NextChild; + } + /* do not destroy attached duplicated editable curves, they are needed to restore editable curves + else if (NextChild->IsA()) + { + // Remove duplicated editable curve output's Houdini Spline Component, since they will be re-built at duplication. + UHoudiniSplineComponent * HoudiniSplineComponent = Cast(NextChild); + if (HoudiniSplineComponent && HoudiniSplineComponent->IsEditableOutputCurve()) + ComponentToRemove = NextChild; + } + */ + if (ComponentToRemove) + { + if(ComponentToRemove->GetOwner()) + ComponentToRemove->UnregisterComponent(); + + ComponentToRemove->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + ComponentToRemove->DestroyComponent(); + } + } + } + + if(IsPDGSupported() && IsValid(PDGData->PDGAssetLink)) + { + // if there is an associated PDG asset link, call its UpdatePostDuplicate to cleanup references to + // to the original instance's PDG output actors + PDGData->PDGAssetLink->UpdatePostDuplicate(); + } + + bHasBeenDuplicated = false; +} + +void +UHoudiniCookable::SetHasComponentTransformChanged(bool InHasChanged) +{ + if (!IsComponentSupported()) + return; + + // Only update the value if we're fully loaded + // This avoid triggering a recook when loading a level + if (!bFullyLoaded) + return; + + ComponentData->bHasComponentTransformChanged = InHasChanged; + ComponentData->LastComponentTransform = ComponentData->Component->GetComponentTransform(); +} + + +void +UHoudiniCookable::ClearRefineMeshesTimer() +{ + if (!IsProxySupported()) + return; + + UWorld* World = GetWorld(); + if (!World) + return; + + World->GetTimerManager().ClearTimer(ProxyData->RefineMeshesTimer); +} + + +void +UHoudiniCookable::SetNodeIdsToCook(const TArray& InNodeIds) +{ + NodeIdsToCook = InNodeIds; + + // Remove stale entries from NodesToCookCookCounts: + TArray CachedNodeIds; + NodesToCookCookCounts.GetKeys(CachedNodeIds); + for (const int32 CachedNodeId : CachedNodeIds) + { + if (!NodeIdsToCook.Contains(CachedNodeId)) + { + NodesToCookCookCounts.Remove(CachedNodeId); + } + } +} + +void +UHoudiniCookable::MarkAsNeedCook() +{ + MarkAsNeedRecookOrRebuild(false); +} + +void +UHoudiniCookable::MarkAsNeedRebuild() +{ + MarkAsNeedRecookOrRebuild(true); +} + +void +UHoudiniCookable::MarkAsNeedRecookOrRebuild(bool bDoRebuild) +{ + if (bDoRebuild) + { + // Force the asset state to NeedRebuild + SetCurrentState(EHoudiniAssetState::NeedRebuild); + CurrentStateResult = EHoudiniAssetStateResult::None; + } + + // Reset some of the asset's flag + bHasBeenLoaded = true; + bPendingDelete = false; + // Indicate whether a recook or rebuild has been requested + bRecookRequested = bDoRebuild ? false : true; + bRebuildRequested = bDoRebuild ? true : false; + // ?? only when doing a rebuild + if (bDoRebuild) + bFullyLoaded = false; + +#if WITH_EDITORONLY_DATA + // TODO COOKABLE: This was somehow only for recook ? + if (IsParameterSupported() && !bDoRebuild) + { + // We need to mark all our parameters as changed/trigger update + for (auto CurrentParam : ParameterData->Parameters) + { + if (!IsValid(CurrentParam)) + continue; + + // Do not trigger parameter update for Button/Button strip when recooking + // As we don't want to trigger the buttons + if (CurrentParam->IsA() || CurrentParam->IsA()) + continue; + + CurrentParam->MarkChanged(true); + CurrentParam->SetNeedsToTriggerUpdate(true); + } + } +#endif + + if (IsOutputSupported()) + { + // We need to mark all of our editable curves as changed + for (auto Output : OutputData->Outputs) + { + if (!IsValid(Output) || Output->GetType() != EHoudiniOutputType::Curve || !Output->IsEditableNode()) + continue; + + for (auto& OutputObjectEntry : Output->GetOutputObjects()) + { + FHoudiniOutputObject& OutputObject = OutputObjectEntry.Value; + if (OutputObject.CurveOutputProperty.CurveOutputType != EHoudiniCurveOutputType::HoudiniSpline) + continue; + + for (auto Component : OutputObject.OutputComponents) + { + UHoudiniSplineComponent* SplineComponent = Cast(Component); + if (!IsValid(SplineComponent)) + continue; + + // This sets bHasChanged and bNeedsToTriggerUpdate + SplineComponent->MarkChanged(true); + } + } + } + } + +#if WITH_EDITORONLY_DATA + if (IsInputSupported()) + { + // We need to mark all our inputs as changed/trigger update + for (auto CurrentInput : InputData->Inputs) + { + if (!IsValid(CurrentInput)) + continue; + + CurrentInput->MarkChanged(true); + CurrentInput->SetNeedsToTriggerUpdate(true); + CurrentInput->MarkDataUploadNeeded(true); + + // TODO COOKABLE: Next was recook only somehow?? + if (bDoRebuild) + continue; + + FHoudiniInputObjectSettings CurrentInputSettings(CurrentInput); + + // In addition to marking the input as changed/need update, we also need to make sure that any changes on the + // Unreal side have been recorded for the input before sending to Houdini. For that we also mark each input + // object as changed/need update and explicitly call the Update function on each input object. For example, for + // input actors this would recreate the Houdini input actor components from the actor's components, picking up + // any new components since the last call to Update. + TArray>* InputObjectArray = CurrentInput->GetHoudiniInputObjectArray(CurrentInput->GetInputType()); + if (InputObjectArray && InputObjectArray->Num() > 0) + { + for (auto CurrentInputObject : *InputObjectArray) + { + if (!IsValid(CurrentInputObject)) + continue; + + UObject* const Object = CurrentInputObject->GetObject(); + if (IsValid(Object)) + CurrentInputObject->Update(Object, CurrentInputSettings); + + CurrentInputObject->MarkChanged(true); + CurrentInputObject->SetNeedsToTriggerUpdate(true); + CurrentInputObject->MarkTransformChanged(true); + } + } + } + } +#endif + + // Clear the static mesh bake timer + if(IsOutputSupported()) + ClearRefineMeshesTimer(); +} + + +// Marks the asset as needing to be instantiated +void +UHoudiniCookable::MarkAsNeedInstantiation() +{ + // Invalidate the asset ID + NodeId = -1; + +#if WITH_EDITORONLY_DATA + if((IsParameterSupported() && ParameterData->Parameters.Num() <= 0) + && (IsInputSupported() && InputData->Inputs.Num() <= 0) + && (IsOutputSupported() && OutputData->Outputs.Num() <= 0)) + { + // The asset has no parameters or inputs. + // This likely indicates it has never cooked/been instantiated. + // Set its state to NewHDA to force its instantiation + // so that we can have its parameters/input interface + SetCurrentState(EHoudiniAssetState::NewHDA); + } + else + { + // The asset has cooked before since we have a parameter/input interface + // Set its state to need instantiation so that the asset is instantiated + // after being modified + SetCurrentState(EHoudiniAssetState::NeedInstantiation); + } +#endif + CurrentStateResult = EHoudiniAssetStateResult::None; + + // Reset some of the asset's flag + CookCount = 0; + bHasBeenLoaded = true; + bPendingDelete = false; + bRecookRequested = false; + bRebuildRequested = false; + bFullyLoaded = false; + + //bEditorPropertiesNeedFullUpdate = true; + +#if WITH_EDITORONLY_DATA + if (IsParameterSupported()) + { + // We need to mark all our parameters as changed/not triggering update + for (auto CurrentParam : ParameterData->Parameters) + { + if (CurrentParam) + { + CurrentParam->MarkChanged(true); + CurrentParam->SetNeedsToTriggerUpdate(false); + } + } + } +#endif + +#if WITH_EDITORONLY_DATA + if (IsInputSupported()) + { + // We need to mark all our inputs as changed/not triggering update + for (auto CurrentInput : InputData->Inputs) + { + if (CurrentInput) + { + CurrentInput->MarkChanged(true); + CurrentInput->SetNeedsToTriggerUpdate(false); + CurrentInput->MarkDataUploadNeeded(true); + } + } + } +#endif + + // Clear the static mesh bake timer + ClearRefineMeshesTimer(); +} + + +void +UHoudiniCookable::PreventAutoUpdates() +{ + // It is important to check this when dealing with Blueprints since the + // preview components start receiving events from the template component + // before the preview component have finished initialization. + if (!IsFullyLoaded()) + return; + + bForceNeedUpdate = false; + bRecookRequested = false; + bRebuildRequested = false; + + if(IsComponentSupported()) + ComponentData->bHasComponentTransformChanged = false; + +#if WITH_EDITORONLY_DATA + if (IsParameterSupported()) + { + // Go through all our parameters, prevent them from triggering updates + for (auto CurrentParm : ParameterData->Parameters) + { + if (!IsValid(CurrentParm)) + continue; + + // Prevent the parm from triggering an update + CurrentParm->SetNeedsToTriggerUpdate(false); + } + } +#endif + +#if WITH_EDITORONLY_DATA + // Same with inputs + if (IsInputSupported()) + { + for (auto CurrentInput : InputData->Inputs) + { + if (!IsValid(CurrentInput)) + continue; + + // Prevent the input from triggering an update + CurrentInput->SetNeedsToTriggerUpdate(false); + } + } +#endif + + if (IsOutputSupported()) + { + // Go through all outputs, filter the editable nodes. + for (auto CurrentOutput : OutputData->Outputs) + { + if (!IsValid(CurrentOutput)) + continue; + + // We only care about editable outputs + if (!CurrentOutput->IsEditableNode()) + continue; + + TMap& OutputObjects = CurrentOutput->GetOutputObjects(); + for (auto& NextPair : OutputObjects) + { + // For now, only editable curves can trigger update + for (auto Component : NextPair.Value.OutputComponents) + { + UHoudiniSplineComponent* HoudiniSplineComponent = Cast(Component); + if (!HoudiniSplineComponent) + continue; + + // Output curves cant trigger an update! + if (HoudiniSplineComponent->bIsOutputCurve) + continue; + + HoudiniSplineComponent->SetNeedsToTriggerUpdate(false); + } + } + } + } +} + +void +UHoudiniCookable::OnSessionConnected() +{ +#if WITH_EDITORONLY_DATA + if (IsParameterSupported()) + { + for (auto& Param : ParameterData->Parameters) + Param->OnSessionConnected(); + } +#endif + +#if WITH_EDITORONLY_DATA + if (IsInputSupported()) + { + for (auto& Input : InputData->Inputs) + { + Input->OnSessionConnected(); + } + } +#endif + + NodeId = INDEX_NONE; +} + + +UHoudiniParameter* +UHoudiniCookable::FindMatchingParameter(UHoudiniParameter* InOtherParam) +{ +#if WITH_EDITORONLY_DATA + if (!IsValid(InOtherParam)) + return nullptr; + + if (!IsParameterSupported()) + return nullptr; + + for (auto CurrentParam : ParameterData->Parameters) + { + if (!IsValid(CurrentParam)) + continue; + + if (CurrentParam->Matches(*InOtherParam)) + return CurrentParam; + } +#endif + return nullptr; +} + +UHoudiniInput* +UHoudiniCookable::FindMatchingInput(UHoudiniInput* InOtherInput) +{ +#if WITH_EDITORONLY_DATA + if (!IsValid(InOtherInput)) + return nullptr; + + if (!IsInputSupported()) + return nullptr; + + for (auto CurrentInput : InputData->Inputs) + { + if (!IsValid(CurrentInput)) + continue; + + if (CurrentInput->Matches(*InOtherInput)) + return CurrentInput; + } +#endif + + return nullptr; +} + +UHoudiniHandleComponent* +UHoudiniCookable::FindMatchingHandle(UHoudiniHandleComponent* InOtherHandle) +{ + if (!IsValid(InOtherHandle)) + return nullptr; + + if (!IsComponentSupported()) + return nullptr; + + for (auto CurrentHandle : ComponentData->HandleComponents) + { + if (!IsValid(CurrentHandle)) + continue; + + if (CurrentHandle->Matches(*InOtherHandle)) + return CurrentHandle; + } + + return nullptr; +} + +UHoudiniParameter* +UHoudiniCookable::FindParameterByName(const FString& InParamName) +{ +#if WITH_EDITORONLY_DATA + if (!IsParameterSupported()) + return nullptr; + + for (auto CurrentParam : ParameterData->Parameters) + { + if (!IsValid(CurrentParam)) + continue; + + if (CurrentParam->GetParameterName().Equals(InParamName)) + return CurrentParam; + } +#endif + + return nullptr; +} + + +#if WITH_EDITORONLY_DATA +TArray>& +UHoudiniCookable::GetParameters() +{ + return ParameterData->Parameters; +} +#endif + +#if WITH_EDITORONLY_DATA +const TArray>& +UHoudiniCookable::GetParameters() const +{ + return ParameterData->Parameters; +} +#endif + +#if WITH_EDITORONLY_DATA +TArray>& +UHoudiniCookable::GetInputs() +{ + return InputData->Inputs; +} +#endif + +#if WITH_EDITORONLY_DATA +const TArray>& +UHoudiniCookable::GetInputs() const +{ + return InputData->Inputs; +} +#endif + +TArray>& +UHoudiniCookable::GetOutputs() +{ + return OutputData->Outputs; +} + +TArray>& +UHoudiniCookable::GetHandleComponents() +{ + return ComponentData->HandleComponents; +} + +void +UHoudiniCookable::GetOutputs(TArray& OutOutputs) const +{ + if (!IsOutputSupported()) + return; + + for (UHoudiniOutput* Output : OutputData->Outputs) + { + OutOutputs.Add(Output); + } +} + +TArray +UHoudiniCookable::GetNodeIdsToCook() const +{ + return NodeIdsToCook; +} + +TMap +UHoudiniCookable::GetNodesToCookCookCounts() const +{ + return NodesToCookCookCounts; +} + +bool +UHoudiniCookable::IsOverrideGlobalProxyStaticMeshSettings() const +{ + return ProxyData->bOverrideGlobalProxyStaticMeshSettings; +} + +bool +UHoudiniCookable::IsProxyStaticMeshEnabled() const +{ + // BP don't support Proxies for now + if (GetComponent()) + { + if (GetComponent()->IsA()) + return false; + } + + if (!IsProxySupported()) + return false; + + if (ProxyData->bOverrideGlobalProxyStaticMeshSettings) + { + return ProxyData->bEnableProxyStaticMeshOverride; + } + else + { + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings) + { + return HoudiniRuntimeSettings->bEnableProxyStaticMesh; + } + else + { + return false; + } + } +} + +bool +UHoudiniCookable::IsProxyStaticMeshRefinementByTimerEnabled() const +{ + if (!IsProxySupported()) + return false; + + if (ProxyData->bOverrideGlobalProxyStaticMeshSettings) + { + return ProxyData->bEnableProxyStaticMeshOverride && ProxyData->bEnableProxyStaticMeshRefinementByTimerOverride; + } + else + { + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings) + { + return HoudiniRuntimeSettings->bEnableProxyStaticMesh && HoudiniRuntimeSettings->bEnableProxyStaticMeshRefinementByTimer; + } + else + { + return false; + } + } +} + +float +UHoudiniCookable::GetProxyMeshAutoRefineTimeoutSeconds() const +{ + if (ProxyData->bOverrideGlobalProxyStaticMeshSettings) + { + return ProxyData->ProxyMeshAutoRefineTimeoutSecondsOverride; + } + else + { + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings) + { + return HoudiniRuntimeSettings->ProxyMeshAutoRefineTimeoutSeconds; + } + else + { + return 5.0f; + } + } +} + +bool +UHoudiniCookable::IsProxyStaticMeshRefinementOnPreSaveWorldEnabled() const +{ + if (!IsProxySupported()) + return false; + + if (ProxyData->bOverrideGlobalProxyStaticMeshSettings) + { + return ProxyData->bEnableProxyStaticMeshOverride && ProxyData->bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride; + } + else + { + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings) + { + return HoudiniRuntimeSettings->bEnableProxyStaticMesh && HoudiniRuntimeSettings->bEnableProxyStaticMeshRefinementOnPreSaveWorld; + } + else + { + return false; + } + } +} + +bool +UHoudiniCookable::IsProxyStaticMeshRefinementOnPreBeginPIEEnabled() const +{ + if (!IsProxySupported()) + return false; + + if (ProxyData->bOverrideGlobalProxyStaticMeshSettings) + { + return ProxyData->bEnableProxyStaticMeshOverride && ProxyData->bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride; + } + else + { + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault< UHoudiniRuntimeSettings >(); + if (HoudiniRuntimeSettings) + { + return HoudiniRuntimeSettings->bEnableProxyStaticMesh && HoudiniRuntimeSettings->bEnableProxyStaticMeshRefinementOnPreBeginPIE; + } + else + { + return false; + } + } +} + +bool +UHoudiniCookable::HasAnyOutputComponent() const +{ + if (!IsOutputSupported()) + return false; + + for (UHoudiniOutput* Output : OutputData->Outputs) + { + for (auto& CurrentOutputObject : Output->GetOutputObjects()) + { + for (auto Component : CurrentOutputObject.Value.OutputComponents) + { + if (Component) + return true; + } + } + } + + return false; +} + +bool +UHoudiniCookable::HasNoProxyMeshNextCookBeenRequested() const +{ + if (!IsProxySupported()) + return false; + + return ProxyData->bNoProxyMeshNextCookRequested; +} + +bool +UHoudiniCookable::HasAnyCurrentProxyOutput() const +{ + if (!IsProxySupported()) + return false; + + for (const UHoudiniOutput* Output : OutputData->Outputs) + { + if (Output->HasAnyCurrentProxy()) + { + return true; + } + } + + return false; +} + +bool +UHoudiniCookable::HasAnyProxyOutput() const +{ + if (!IsOutputSupported()) + return false; + + for (const UHoudiniOutput* Output : OutputData->Outputs) + { + if (Output->HasAnyProxy()) + { + return true; + } + } + + return false; +} + +void +UHoudiniCookable::SetNoProxyMeshNextCookRequested(bool bInNoProxyMeshNextCookRequested) +{ + ProxyData->bNoProxyMeshNextCookRequested = bInNoProxyMeshNextCookRequested; +} + + +void +UHoudiniCookable::SetOverrideGlobalProxyStaticMeshSettings(bool InEnable) +{ + ProxyData->bOverrideGlobalProxyStaticMeshSettings = InEnable; +} + +void +UHoudiniCookable::SetEnableProxyStaticMeshOverride(bool InEnable) +{ + ProxyData->bEnableProxyStaticMeshOverride = InEnable; +} + +void +UHoudiniCookable::SetEnableProxyStaticMeshRefinementByTimerOverride(bool InEnable) +{ + ProxyData->bEnableProxyStaticMeshRefinementByTimerOverride = InEnable; +} + +void +UHoudiniCookable::SetProxyMeshAutoRefineTimeoutSecondsOverride(float InValue) +{ + ProxyData->ProxyMeshAutoRefineTimeoutSecondsOverride = InValue; +} + +void +UHoudiniCookable::SetEnableProxyStaticMeshRefinementOnPreSaveWorldOverride(bool InEnable) +{ + ProxyData->bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride = InEnable; +} + +void +UHoudiniCookable::SetEnableProxyStaticMeshRefinementOnPreBeginPIEOverride(bool InEnable) +{ + ProxyData->bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride = InEnable; +} + + +void +UHoudiniCookable::SetBakeAfterNextCook(const EHoudiniBakeAfterNextCook InBakeAfterNextCook) +{ + BakingData->BakeAfterNextCook = InBakeAfterNextCook; +} + +void +UHoudiniCookable::SetActorBakeOption(const EHoudiniEngineActorBakeOption InBakeOption) +{ + BakingData->ActorBakeOption = InBakeOption; +} + +void +UHoudiniCookable::SetAllowPlayInEditorRefinement(bool bEnabled) +{ + ProxyData->bAllowPlayInEditorRefinement = bEnabled; +} + +bool +UHoudiniCookable::IsPlayInEditorRefinementAllowed() const +{ + return ProxyData->bAllowPlayInEditorRefinement; +} + +void +UHoudiniCookable::SetRefineMeshesTimer() +{ + if (!IsProxySupported()) + return; + + UWorld* World = GetWorld(); + if (!World) + { + HOUDINI_LOG_ERROR(TEXT("Cannot SetRefineMeshesTimer, World is nullptr!")); + return; + } + + // Check if timer-based proxy mesh refinement is enable for this component + const bool bEnableTimer = IsProxyStaticMeshRefinementByTimerEnabled(); + const float TimeSeconds = GetProxyMeshAutoRefineTimeoutSeconds(); + if (bEnableTimer) + { + World->GetTimerManager().SetTimer(ProxyData->RefineMeshesTimer, this, &UHoudiniCookable::OnRefineMeshesTimerFired, 1.0f, false, TimeSeconds); + } + else + { + World->GetTimerManager().ClearTimer(ProxyData->RefineMeshesTimer); + } +} + +void +UHoudiniCookable::OnRefineMeshesTimerFired() +{ + if (!IsProxySupported()) + return; + + HOUDINI_LOG_MESSAGE(TEXT("UHoudiniAssetComponent::OnRefineMeshesTimerFired()")); + if (ProxyData->OnRefineMeshesTimerDelegate.IsBound()) + { + ProxyData->OnRefineMeshesTimerDelegate.Broadcast(this); + } +} + +bool +UHoudiniCookable::IsHoudiniCookedDataAvailable(bool& bOutNeedsRebuildOrDelete, bool& bOutInvalidState) const +{ + // Get the state of the asset and check if it is pre-cook, cooked, pending delete/rebuild or invalid + bOutNeedsRebuildOrDelete = false; + bOutInvalidState = false; + switch (CurrentState) + { + case EHoudiniAssetState::NewHDA: + case EHoudiniAssetState::NeedInstantiation: + case EHoudiniAssetState::PreInstantiation: + case EHoudiniAssetState::Instantiating: + case EHoudiniAssetState::PreCook: + case EHoudiniAssetState::Cooking: + case EHoudiniAssetState::PostCook: + case EHoudiniAssetState::PreProcess: + case EHoudiniAssetState::Processing: + return false; + break; + case EHoudiniAssetState::None: + return true; + break; + case EHoudiniAssetState::NeedRebuild: + case EHoudiniAssetState::NeedDelete: + case EHoudiniAssetState::Deleting: + bOutNeedsRebuildOrDelete = true; + break; + default: + bOutInvalidState = true; + break; + } + + return false; +} + + +bool +UHoudiniCookable::IsBakeAfterNextCookEnabled() const +{ + return BakingData->BakeAfterNextCook != EHoudiniBakeAfterNextCook::Disabled; +} + +EHoudiniBakeAfterNextCook +UHoudiniCookable::GetBakeAfterNextCook() const +{ + return BakingData->BakeAfterNextCook; +} + +EHoudiniEngineActorBakeOption +UHoudiniCookable::GetActorBakeOption() const +{ + return BakingData->ActorBakeOption; +} + +TArray& +UHoudiniCookable::GetBakedOutputs() +{ + return BakingData->BakedOutputs; +} + +const TArray& +UHoudiniCookable::GetBakedOutputs() const +{ + return BakingData->BakedOutputs; +} + + +EHoudiniEngineBakeOption +UHoudiniCookable::GetHoudiniEngineBakeOption() const +{ + return BakingData->HoudiniEngineBakeOption; +} + +void +UHoudiniCookable::SetHoudiniEngineBakeOption(const EHoudiniEngineBakeOption& InBakeOption) +{ + BakingData->HoudiniEngineBakeOption = InBakeOption; +} + +bool +UHoudiniCookable::GetReplacePreviousBake() const +{ + return BakingData->bReplacePreviousBake; +} + +void +UHoudiniCookable::SetReplacePreviousBake(bool bInReplace) +{ + BakingData->bReplacePreviousBake = bInReplace; +} + +bool +UHoudiniCookable::GetRemoveOutputAfterBake() const +{ + return BakingData->bRemoveOutputAfterBake; +} + +void +UHoudiniCookable::SetRemoveOutputAfterBake(bool bInRemove) +{ + BakingData->bRemoveOutputAfterBake = bInRemove; +} + +bool +UHoudiniCookable::GetRecenterBakedActors() const +{ + return BakingData->bRecenterBakedActors; +} + +void +UHoudiniCookable::SetRecenterBakedActors(bool bInRecenter) +{ + BakingData->bRecenterBakedActors = bInRecenter; +} + +bool +UHoudiniCookable::GetCookOnParameterChange() const +{ + return ParameterData->bCookOnParameterChange; +} + +bool +UHoudiniCookable::GetCookOnTransformChange() const +{ + return ComponentData->bCookOnTransformChange; +} + +bool +UHoudiniCookable::GetCookOnInputChange() const +{ + return InputData->bCookOnInputChange; +} + +bool +UHoudiniCookable::GetCookOnCookableInputCook() const +{ + return InputData->bCookOnCookableInputCook; +} + +bool +UHoudiniCookable::IsInstantiatingOrCooking() const +{ + return HapiGUID.IsValid(); +} + +bool +UHoudiniCookable::IsOutputless() const +{ + return OutputData->bOutputless; +} + +bool +UHoudiniCookable::GetUseOutputNodes() const +{ + return OutputData->bUseOutputNodes; +} + +bool +UHoudiniCookable::GetOutputTemplateGeos() const +{ + return OutputData->bOutputTemplateGeos; +} + +bool +UHoudiniCookable::GetUploadTransformsToHoudiniEngine() const +{ + return ComponentData->bUploadTransformsToHoudiniEngine; +} + +FTransform +UHoudiniCookable::GetLastComponentTransform() const +{ + return ComponentData->LastComponentTransform; +} + +FTransform +UHoudiniCookable::GetComponentTransform() const +{ + if (!IsComponentSupported()) + return FTransform::Identity; + + if (!GetComponent()) + return FTransform::Identity; + + return GetComponent()->GetComponentTransform(); +} + +bool +UHoudiniCookable::GetLandscapeUseTempLayers() const +{ + return OutputData->bLandscapeUseTempLayers; +} + +bool +UHoudiniCookable::GetEnableCurveEditing() const +{ + return OutputData->bEnableCurveEditing; +} + +bool +UHoudiniCookable::GetSplitMeshSupport() const +{ + return OutputData->bSplitMeshSupport; +} + + +FHoudiniStaticMeshGenerationProperties +UHoudiniCookable::GetStaticMeshGenerationProperties() const +{ + return OutputData->StaticMeshGenerationProperties; +} + +FMeshBuildSettings +UHoudiniCookable::GetStaticMeshBuildSettings() const +{ + return OutputData->StaticMeshBuildSettings; +} + +FHoudiniStaticMeshGenerationProperties& +UHoudiniCookable::GetStaticMeshGenerationProperties() +{ + return OutputData->StaticMeshGenerationProperties; +} + +FMeshBuildSettings& +UHoudiniCookable::GetStaticMeshBuildSettings() +{ + return OutputData->StaticMeshBuildSettings; +} + +bool +UHoudiniCookable::CanDeleteHoudiniNodes() const +{ + // TODO: Cookable - Make this a member instead of relying on components? + // Our Component dictates if we're allowed to delete our nodes + if (IsComponentSupported() && GetComponent()) + { + UHoudiniAssetComponent* HAC = Cast(GetComponent()); + if (IsValid(HAC)) + return HAC->CanDeleteHoudiniNodes(); + } + + return true; +} + +void +UHoudiniCookable::SetStaticMeshGenerationProperties(const FHoudiniStaticMeshGenerationProperties& InHSMGP) +{ + OutputData->StaticMeshGenerationProperties = InHSMGP; +}; + +void +UHoudiniCookable::SetStaticMeshBuildSettings(const FMeshBuildSettings& InMBS) +{ + OutputData->StaticMeshBuildSettings = InMBS; +}; + +void +UHoudiniCookable::SetCookOnParameterChange(bool bEnable) +{ + if (!IsParameterSupported()) + return; + + ParameterData->bCookOnParameterChange = bEnable; +} + +void +UHoudiniCookable::SetCookOnTransformChange(bool bEnable) +{ + if (!IsComponentSupported()) + return; + + ComponentData->bCookOnTransformChange = bEnable; +} + +void +UHoudiniCookable::SetCookOnInputChange(bool bEnable) +{ + if (!IsInputSupported()) + return; + + InputData->bCookOnInputChange = bEnable; +} + +void +UHoudiniCookable::SetCookOnCookableInputCook(bool bEnable) +{ + if (!IsInputSupported()) + return; + + InputData->bCookOnCookableInputCook = bEnable; +} + +void +UHoudiniCookable::SetCookingEnabled(const bool& bInCookingEnabled) +{ + bEnableCooking = bInCookingEnabled; +} + +void +UHoudiniCookable::SetHasBeenLoaded(const bool& InLoaded) +{ + bHasBeenLoaded = InLoaded; +} + +void +UHoudiniCookable::SetHasBeenDuplicated(const bool& InDuplicated) +{ + bHasBeenDuplicated = InDuplicated; +} + +void +UHoudiniCookable::SetCookCount(const int32& InCount) +{ + CookCount = InCount; +} + +void +UHoudiniCookable::SetRecookRequested(const bool& InRecook) +{ + bRecookRequested = InRecook; +} + +void +UHoudiniCookable::SetRebuildRequested(const bool& InRebuild) +{ + bRebuildRequested = InRebuild; +} + +bool +UHoudiniCookable::WasLastCookSuccessful() const +{ + return bLastCookSuccess; +} + +/* +void +UHoudiniCookable::SetCookOnAssetInputCook(bool bEnable) +{ + if (!IsParameterSupported()) + return; + + OutputData->bCookOnAssetInputCook = bEnable; +} +*/ + +void +UHoudiniCookable::SetOutputless(bool bEnable) +{ + OutputData->bOutputless = bEnable; +} + +void +UHoudiniCookable::SetUseOutputNodes(bool bEnable) +{ + if (!IsOutputSupported()) + return; + + OutputData->bUseOutputNodes = bEnable; +} + +void +UHoudiniCookable::SetOutputTemplateGeos(bool bEnable) +{ + OutputData->bOutputTemplateGeos = bEnable; +} + +void +UHoudiniCookable::SetUploadTransformsToHoudiniEngine(bool bEnable) +{ + if (!IsComponentSupported()) + return; + + ComponentData->bUploadTransformsToHoudiniEngine = bEnable; +} + +void +UHoudiniCookable::SetLandscapeUseTempLayers(bool bEnable) +{ + OutputData->bLandscapeUseTempLayers = bEnable; +} + +void +UHoudiniCookable::SetEnableCurveEditing(bool bEnable) +{ + OutputData->bEnableCurveEditing = bEnable; +} + + +void +UHoudiniCookable::OnDestroy(bool bDestroyingHierarchy) +{ +#if WITH_EDITORONLY_DATA + if(IsHoudiniAssetSupported()) + HoudiniAssetData->HoudiniAsset = nullptr; +#endif + +#if WITH_EDITORONLY_DATA + if (IsParameterSupported()) + { + // Clear Parameters + for (TObjectPtr& CurrentParm : ParameterData->Parameters) + { + if (IsValid(CurrentParm)) + { + CurrentParm->ConditionalBeginDestroy(); + } + else if (GetWorld() != nullptr && GetWorld()->WorldType != EWorldType::PIE) + { + // TODO unneeded log? + // Avoid spamming that error when leaving PIE mode + HOUDINI_LOG_WARNING(TEXT("%s: null parameter when clearing"), GetOwner() ? *(GetOwner()->GetName()) : *GetName()); + } + CurrentParm = nullptr; + } + + ParameterData->Parameters.Empty(); + } +#endif + +#if WITH_EDITORONLY_DATA + if (IsInputSupported()) + { + // Clear Inputs + for (TObjectPtr& CurrentInput : InputData->Inputs) + { + if (!IsValid(CurrentInput)) + continue; + + if (CurrentInput->HasAnyFlags(RF_NeedLoad | RF_NeedPostLoad)) + continue; + + // Destroy connected Houdini asset. + CurrentInput->ConditionalBeginDestroy(); + CurrentInput = nullptr; + } + + InputData->Inputs.Empty(); + } +#endif + + if (IsOutputSupported()) + { + // Clear Output + for (TObjectPtr& CurrentOutput : OutputData->Outputs) + { + if (!IsValid(CurrentOutput)) + continue; + + if (CurrentOutput->HasAnyFlags(RF_NeedLoad | RF_NeedPostLoad)) + continue; + + // Destroy all Houdini created socket actors. + TArray>& CurCreatedSocketActors = CurrentOutput->GetHoudiniCreatedSocketActors(); + for (auto& CurCreatedActor : CurCreatedSocketActors) + { + if (!IsValid(CurCreatedActor)) + continue; + + CurCreatedActor->Destroy(); + } + CurCreatedSocketActors.Empty(); + + // Detach all Houdini attached socket actors + TArray>& CurAttachedSocketActors = CurrentOutput->GetHoudiniAttachedSocketActors(); + for (auto& CurAttachedSocketActor : CurAttachedSocketActors) + { + if (!IsValid(CurAttachedSocketActor)) + continue; + + CurAttachedSocketActor->DetachFromActor(FDetachmentTransformRules::KeepRelativeTransform); + } + CurAttachedSocketActors.Empty(); + +#if WITH_EDITOR + // Cleanup landscape splines + FHoudiniLandscapeRuntimeUtils::DeleteLandscapeSplineCookedData(CurrentOutput); + + // Cleanup landscapes + FHoudiniLandscapeRuntimeUtils::DeleteLandscapeCookedData(CurrentOutput); + + // Clean up foliage instances + for (auto& CurrentOutputObject : CurrentOutput->GetOutputObjects()) + { + for (int Index = 0; Index < CurrentOutputObject.Value.OutputComponents.Num(); Index++) + { + auto Component = CurrentOutputObject.Value.OutputComponents[Index]; + // Foliage instancers store a HISMC in the components + UHierarchicalInstancedStaticMeshComponent* FoliageHISMC = Cast(Component); + if (!FoliageHISMC) + continue; + + UStaticMesh* FoliageSM = FoliageHISMC->GetStaticMesh(); + if (!IsValid(FoliageSM)) + continue; + + // If we are a foliage HISMC, then our owner is an Instanced Foliage Actor, + // if it is not, then we are just a "regular" HISMC + AInstancedFoliageActor* InstancedFoliageActor = Cast(FoliageHISMC->GetOwner()); + if (!IsValid(InstancedFoliageActor)) + continue; + + UFoliageType* FoliageType = InstancedFoliageActor->GetLocalFoliageTypeForSource(FoliageSM); + if (!IsValid(FoliageType)) + continue; + + if (IsInGameThread() && IsGarbageCollecting()) + { + // Calling DeleteInstancesForComponent during GC will cause unreal to crash... + HOUDINI_LOG_WARNING(TEXT("%s: Unable to clear foliage instances because of GC"), GetOwner() ? *(GetOwner()->GetName()) : *GetName()); + } + else + { + // Clean up the instances generated for that component + InstancedFoliageActor->DeleteInstancesForComponent(GetComponent(), FoliageType); + } + + if (FoliageHISMC->GetInstanceCount() > 0) + { + // If the component still has instances left after the cleanup, + // make sure that we dont delete it, as the leftover instances are likely hand-placed + CurrentOutputObject.Value.OutputComponents[Index] = nullptr; + } + else + { + // Remove the foliage type if it doesn't have any more instances + InstancedFoliageActor->RemoveFoliageType(&FoliageType, 1); + } + } + } +#endif + + CurrentOutput->Clear(); + // Destroy connected Houdini asset. + CurrentOutput->ConditionalBeginDestroy(); + CurrentOutput = nullptr; + } + + OutputData->Outputs.Empty(); + } + + // Clear the static mesh bake timer + ClearRefineMeshesTimer(); + + // Clear all TOP data and temporary geo/objects from the PDG asset link (if valid) + if (IsPDGSupported() && IsValid(PDGData->PDGAssetLink)) + { +#if WITH_EDITOR + const UWorld* const World = GetWorld(); + if (IsValid(World)) + { + // Only do this for editor worlds, only interactively (not during engine shutdown or garbage collection) + if (World->WorldType == EWorldType::Editor && GIsRunning && !GIsGarbageCollecting) + { + // In case we are recording a transaction (undo, for example) notify that the object will be + // modified. + PDGData->PDGAssetLink->Modify(); + PDGData->PDGAssetLink->ClearAllTOPData(); + PDGData->PDGAssetLink->ConditionalBeginDestroy(); + } + } +#endif + } + + // Unregister ourself so our houdini node can be deleted + FHoudiniEngineRuntime::Get().UnRegisterHoudiniCookable(this); +} + +bool +UHoudiniCookable::NotifyCookedToDownstreamCookables() +{ +#if WITH_EDITORONLY_DATA + // Before notifying, clean up our downstream cookables + // - check that they are still valid + // - check that we are still connected to one of its inputs + // - check that the asset has the CookOnAssetInputCook trigger enabled + TArray DownstreamToDelete; + for (auto& CurrentDownstreamHC : InputData->DownstreamCookables) + { + // Remove the downstream connection by default, + // unless we actually were properly connected to one of this HDA's input. + bool bRemoveDownstream = true; + if (IsValid(CurrentDownstreamHC)) + { + // Go through the HAC's input + for (auto& CurrentDownstreamInput : CurrentDownstreamHC->GetInputs()) + { + if (!IsValid(CurrentDownstreamInput)) + continue; + + EHoudiniInputType CurrentDownstreamInputType = CurrentDownstreamInput->GetInputType(); + + // Require an asset input type, not just all World/NewWorld + if (!CurrentDownstreamInput->IsAssetInput()) + continue; + + // Ensure that we are an input object of that input + if (!CurrentDownstreamInput->ContainsInputObject(this, CurrentDownstreamInputType)) + continue; + + // We are an input to this HDA + // Make sure that the + if (!CurrentDownstreamInput->GetImportAsReference()) + { + const TArray>* ObjectArray = CurrentDownstreamInput->GetHoudiniInputObjectArray(CurrentDownstreamInputType); + if (ObjectArray) + { + for (auto& CurrentInputObject : (*ObjectArray)) + { + if (!IsValid(CurrentInputObject)) + continue; + + if (CurrentInputObject->GetObject() != this) + continue; + + CurrentInputObject->SetInputNodeId(GetNodeId()); + CurrentInputObject->SetInputObjectNodeId(GetNodeId()); + } + } + } + + if (CurrentDownstreamHC->GetCookOnCookableInputCook()) + { + // Mark that HAC's input has changed + CurrentDownstreamInput->MarkChanged(true); + } + bRemoveDownstream = false; + } + } + + if (bRemoveDownstream) + { + DownstreamToDelete.Add(CurrentDownstreamHC); + } + } + + for (auto ToDelete : DownstreamToDelete) + { + InputData->DownstreamCookables.Remove(ToDelete); + } +#endif + + return true; +} + +void +UHoudiniCookable::AddDownstreamCookable(UHoudiniCookable* InDownstreamCookable) +{ +#if WITH_EDITORONLY_DATA + if (!IsValid(InDownstreamCookable)) + return; + + if (!IsInputSupported()) + return; + + InputData->DownstreamCookables.Add(InDownstreamCookable); +#else + return; +#endif +} + +void +UHoudiniCookable::RemoveDownstreamCookable(UHoudiniCookable* InDownstreamCookable) +{ +#if WITH_EDITORONLY_DATA + if (!IsInputSupported()) + return; + + InputData->DownstreamCookables.Remove(InDownstreamCookable); +#endif +} + +void +UHoudiniCookable::ClearDownstreamCookable() +{ +#if WITH_EDITORONLY_DATA + InputData->DownstreamCookables.Empty(); +#endif +} + +void +UHoudiniCookable::PostLoad() +{ + Super::PostLoad(); + + // Mark as need instantiation + MarkAsNeedInstantiation(); + + // Component has been loaded, not duplicated + SetHasBeenDuplicated(false); + + // We need to register ourself + FHoudiniEngineRuntime::Get().RegisterHoudiniCookable(this); + +#if WITH_EDITORONLY_DATA + if (IsBakingSupported()) + { + // TODO: Not necessary anymore? + auto MaxValue = StaticEnum()->GetMaxEnumValue() - 1; + if (static_cast(BakingData->HoudiniEngineBakeOption) > MaxValue) + { + HOUDINI_LOG_WARNING(TEXT("Invalid Bake Type found, setting to To Actor. Possibly Foliage, which is deprecated, use the unreal_foliage attribute instead.")); + BakingData->HoudiniEngineBakeOption = EHoudiniEngineBakeOption::ToActor; + } + } +#endif +} + +void +UHoudiniCookable::PostEditImport() +{ + Super::PostEditImport(); + + MarkAsNeedInstantiation(); + + // Component has been duplicated, not loaded + // We do need the loaded flag to reapply parameters, inputs + // and properly update some of the output objects + SetHasBeenDuplicated(true); + + SetCurrentState(EHoudiniAssetState::PreInstantiation); + SetCurrentStateResult(EHoudiniAssetStateResult::None); +} + +void +UHoudiniCookable::BeginDestroy() +{ + OnDestroy(true); + + // Unregister ourself so our houdini node can be deleted + FHoudiniEngineRuntime::Get().UnRegisterHoudiniCookable(this); + + Super::BeginDestroy(); +} + +void UHoudiniCookable::SetNodeLabelPrefix(const FString& Prefix) +{ + NodeLabelPrefix = Prefix; +} + +const FString& UHoudiniCookable::GetNodeLabelPrefix() const +{ + return NodeLabelPrefix; +} + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniCookable.h`: + +```h +/* +* Copyright (c) <2025> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreTypes.h" +#include "UObject/Object.h" +#include "UObject/ObjectMacros.h" + +#include "HoudiniAssetComponent.h" +#include "HoudiniEngineRuntimeCommon.h" +#include "HoudiniRuntimeSettings.h" +#include "HoudiniAssetStateTypes.h" +#include "IHoudiniAssetStateEvents.h" + +#include "Delegates/DelegateCombinations.h" +#include "Engine/EngineTypes.h" +#include "Components/SceneComponent.h" +#if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 0) + #include "LevelInstance/LevelInstanceInterface.h" +#endif + +#include "HoudiniCookable.generated.h" + +class UHoudiniAsset; +class UHoudiniHandleComponent; +class UHoudiniInput; +class UHoudiniOutput; +class UHoudiniPDGAssetLink; +class UHoudiniParameter; + +UCLASS() +class HOUDINIENGINERUNTIME_API UCookableHoudiniAssetData : public UObject +{ + GENERATED_UCLASS_BODY() + + friend class UHoudiniCookable; + +public: + + UCookableHoudiniAssetData(); + +#if WITH_EDITORONLY_DATA + // Houdini Asset associated with this component. + UPROPERTY(Category = HoudiniAsset, EditAnywhere)// BlueprintSetter = SetHoudiniAsset, BlueprintReadWrite, ) + TObjectPtr HoudiniAsset; +#endif + // Subasset index + UPROPERTY() + uint32 SubAssetIndex; + + // The asset name of the selected asset inside the asset library + UPROPERTY(DuplicateTransient) + FString HapiAssetName; +}; + + +UCLASS() +class HOUDINIENGINERUNTIME_API UCookableParameterData : public UObject +{ + GENERATED_UCLASS_BODY() + + friend class UHoudiniCookable; + +public: + + UCookableParameterData(); + +#if WITH_EDITORONLY_DATA + UPROPERTY(Instanced) + TArray> Parameters; +#endif + +#if WITH_EDITORONLY_DATA + // Used to store the current state of parameters + // This allows fast - batch setting of parameters (upon rebuild/load) + UPROPERTY() + TArray ParameterPresetBuffer; +#endif + + // Automatically cook when a parameter is changed + UPROPERTY() + bool bCookOnParameterChange; + + // Indicates that the parameter state (excluding values) on the HAC and the instantiated node needs to be synced. + // The most common use for this would be a newly instantiated HDA that has only a default parameter interface + // from its asset definition, and needs to sync pre-cook. + UPROPERTY(DuplicateTransient) + bool bParameterDefinitionUpdateNeeded; + + // Try to find one of our parameter that matches another (name, type, size and enabled) + UHoudiniParameter* FindMatchingParameter(UHoudiniParameter* InOtherParam); +}; + + +UCLASS() +class HOUDINIENGINERUNTIME_API UCookableInputData : public UObject +{ + GENERATED_UCLASS_BODY() + + friend class UHoudiniCookable; + +public: + + UCookableInputData(); + +#if WITH_EDITORONLY_DATA + // Store data for a cookable's inputs + UPROPERTY(Instanced) + TArray> Inputs; +#endif + // Automatically cook when an input is changed + UPROPERTY() + bool bCookOnInputChange; // bCookOnParameterChange + + UPROPERTY() + bool bCookOnCookableInputCook; + +#if WITH_EDITORONLY_DATA + // TODO: Cookable Move to output data? + // List of dependent downstream Cookables that have us as an asset input + UPROPERTY(DuplicateTransient) + TSet> DownstreamCookables; +#endif + + // Accessors + int32 GetNumInputs() const + { +#if WITH_EDITORONLY_DATA + return Inputs.Num(); +#else + return 0; +#endif + }; + UHoudiniInput* GetInputAt(const int32& Idx) + { +#if WITH_EDITORONLY_DATA + return Inputs.IsValidIndex(Idx) ? Inputs[Idx] : nullptr; +#else + return nullptr; +#endif + }; + + // + //bool NeedUpdateInputs() const; + + // + bool NeedsToWaitForInputHoudiniAssets(); +}; + + +UCLASS() +class HOUDINIENGINERUNTIME_API UCookableOutputData : public UObject +{ + GENERATED_UCLASS_BODY() + + friend class UHoudiniCookable; + +public: + + UCookableOutputData(); + + UPROPERTY(Instanced) + TArray> Outputs; + + // Any actors that aren't explicitly tracked by output objects + // should be registered here so that they can be cleaned up. + UPROPERTY() + TArray> UntrackedOutputs; + + // Temporary cook folder + UPROPERTY() + FDirectoryPath TemporaryCookFolder; + + // Enabling this will prevent producing any output after cooking. + UPROPERTY() + bool bOutputless; + + // Enabling this allows scene components to be produced. Used to stop scene components + // being generated for meshes. Possibly other types in the future. + UPROPERTY() + bool bCreateSceneComponents; + + // Enabling this will allow outputing the asset's templated geos + UPROPERTY() + bool bOutputTemplateGeos; + + // Enabling this will allow outputing using output nodes + // Disabling it will either fall back to display flag node (legacy workflow) + UPROPERTY() + bool bUseOutputNodes; + + // Whether or not to support multiple mesh outputs on one HDA output. This is currently in Alpha testing. + UPROPERTY(Category = "HoudiniMeshGeneration", EditAnywhere, meta = (DisplayPriority = 0)) + bool bSplitMeshSupport = false; + + // Look for editable curve::1.0 nodes to create curve inputs + // Legacy workflow - deprecated - disabled by default + UPROPERTY() + bool bEnableCurveEditing = false; + + // Generation properties for the Static Meshes + UPROPERTY(Category = "HoudiniMeshGeneration", EditAnywhere, meta = (DisplayPriority = 1)/*, meta = (ShowOnlyInnerProperties)*/) + FHoudiniStaticMeshGenerationProperties StaticMeshGenerationProperties; + + // Build Settings to be used when generating the Static Meshes for this Houdini Asset + UPROPERTY(Category = "HoudiniMeshGeneration", EditAnywhere, meta = (DisplayPriority = 2)) + FMeshBuildSettings StaticMeshBuildSettings; + + UPROPERTY() + bool bLandscapeUseTempLayers; +}; + + +UCLASS() +class HOUDINIENGINERUNTIME_API UCookableBakingData : public UObject +{ + GENERATED_UCLASS_BODY() + + friend class UHoudiniCookable; + + DECLARE_MULTICAST_DELEGATE_TwoParams(FOnPostBakeDelegate, UHoudiniCookable*, bool); + +public: + + FOnPostBakeDelegate& GetOnPostBakeDelegate() { return OnPostBakeDelegate; }; + + //----------------------------------- + // BAKE + + // Previously baked outputs + UPROPERTY() + TArray BakedOutputs; + + // Bake Options + UPROPERTY() + EHoudiniEngineBakeOption HoudiniEngineBakeOption; + + // Folder used for baking this asset's outputs (unless set by prim/detail attribute on the output). Falls back to + // the default from the plugin settings if not set. + UPROPERTY() + FDirectoryPath BakeFolder; + + // If true, bake the asset after its next cook. + UPROPERTY(DuplicateTransient) + EHoudiniBakeAfterNextCook BakeAfterNextCook; + + // If true, then after a successful bake, outputs will be cleared and removed. + UPROPERTY() + bool bRemoveOutputAfterBake; + + // If true, recenter baked actors to their bounding box center after bake + UPROPERTY() + bool bRecenterBakedActors; + + // If true, replace the previously baked output (if any) instead of creating new objects + UPROPERTY() + bool bReplacePreviousBake; + + UPROPERTY() + EHoudiniEngineActorBakeOption ActorBakeOption; + + // Delegate to broadcast after baking the HAC. Not called when just baking individual outputs directly. + // Arguments are (HoudiniAssetComponent* HAC, bool bIsSuccessful) + FOnPostBakeDelegate OnPostBakeDelegate; + +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UCookableProxyData : public UObject +{ + GENERATED_UCLASS_BODY() + + friend class UHoudiniCookable; + +public: + + // Declare the delegate that is broadcast when RefineMeshesTimer fires + DECLARE_MULTICAST_DELEGATE_OneParam(FOnRefineMeshesTimerDelegate, UHoudiniCookable*); + + // Called by RefineMeshesTimer when the timer is triggered. + FOnRefineMeshesTimerDelegate& GetOnRefineMeshesTimerDelegate() { return OnRefineMeshesTimerDelegate; } + + //----------------------------------- + // PROXY MESH + + // If true, don't build a proxy mesh next cook (regardless of global or override settings), + // instead build the UStaticMesh directly (if applicable for the output types). + UPROPERTY(DuplicateTransient) + bool bNoProxyMeshNextCookRequested; + + // Override the global fast proxy mesh settings + UPROPERTY(Category = "HoudiniProxyMeshGeneration", EditAnywhere/*, meta = (DisplayAfter = "StaticMeshGenerationProperties")*/) + bool bOverrideGlobalProxyStaticMeshSettings; + + // For StaticMesh outputs: should a fast proxy be created first? + UPROPERTY(Category = "HoudiniProxyMeshGeneration", EditAnywhere, meta = (DisplayName = "Enable Proxy Static Mesh", EditCondition = "bOverrideGlobalProxyStaticMeshSettings")) + bool bEnableProxyStaticMeshOverride; + + // If fast proxy meshes are being created, must it be baked as a StaticMesh after a period of no updates? + UPROPERTY(Category = "HoudiniProxyMeshGeneration", EditAnywhere, meta = (DisplayName = "Refine Proxy Static Meshes After a Timeout", EditCondition = "bOverrideGlobalProxyStaticMeshSettings && bEnableProxyStaticMeshOverride")) + bool bEnableProxyStaticMeshRefinementByTimerOverride; + + // If the option to automatically refine the proxy mesh via a timer has been selected, this controls the timeout in seconds. + UPROPERTY(Category = "HoudiniProxyMeshGeneration", EditAnywhere, meta = (DisplayName = "Proxy Mesh Auto Refine Timeout Seconds", EditCondition = "bOverrideGlobalProxyStaticMeshSettings && bEnableProxyStaticMeshOverride && bEnableProxyStaticMeshRefinementByTimerOverride")) + float ProxyMeshAutoRefineTimeoutSecondsOverride; + + // Automatically refine proxy meshes to UStaticMesh before the map is saved + UPROPERTY(Category = "HoudiniProxyMeshGeneration", EditAnywhere, meta = (DisplayName = "Refine Proxy Static Meshes When Saving a Map", EditCondition = "bOverrideGlobalProxyStaticMeshSettings && bEnableProxyStaticMeshOverride")) + bool bEnableProxyStaticMeshRefinementOnPreSaveWorldOverride; + + // Automatically refine proxy meshes to UStaticMesh before starting a play in editor session + UPROPERTY(Category = "HoudiniProxyMeshGeneration", EditAnywhere, meta = (DisplayName = "Refine Proxy Static Meshes On PIE", EditCondition = "bOverrideGlobalProxyStaticMeshSettings && bEnableProxyStaticMeshOverride")) + bool bEnableProxyStaticMeshRefinementOnPreBeginPIEOverride; + + UPROPERTY(Transient, DuplicateTransient) + bool bAllowPlayInEditorRefinement; + + // Timer that is used to trigger creation of UStaticMesh for all mesh outputs + // that still have UHoudiniStaticMeshes. The timer is cleared on PreCook and reset + // at the end of the PostCook. + UPROPERTY() + FTimerHandle RefineMeshesTimer; + + // Delegate that is used to broadcast when RefineMeshesTimer fires + FOnRefineMeshesTimerDelegate OnRefineMeshesTimerDelegate; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UCookableComponentData : public UObject +{ + GENERATED_UCLASS_BODY() + + friend class UHoudiniCookable; + +public: + + UCookableComponentData(); + + UPROPERTY(VisibleAnywhere, Category = "Houdini Cookable") + TWeakObjectPtr Component; + + // Used to compare transform changes and whether we need to + // send transform updates to Houdini. + UPROPERTY(DuplicateTransient) + FTransform LastComponentTransform; + + UPROPERTY(Transient, DuplicateTransient) + bool bHasComponentTransformChanged; + + // Enables uploading of transformation changes back to Houdini Engine. + UPROPERTY() + bool bUploadTransformsToHoudiniEngine; + + // Transform changes automatically trigger cooks. + UPROPERTY() + bool bCookOnTransformChange; + + // Handles found on this cookable + UPROPERTY() + TArray> HandleComponents; + + // The last timestamp this cookable's component received a session sync update ping + // used to limit the frequency at which we ping HDAs for session sync updates + UPROPERTY(Transient) + double LastLiveSyncPingTime; +}; + + +UCLASS() +class HOUDINIENGINERUNTIME_API UCookablePDGData : public UObject +{ + GENERATED_UCLASS_BODY() + + friend class UHoudiniCookable; + +public: + + UCookablePDGData(); + + void SetPDGAssetLink(UHoudiniPDGAssetLink* InPDGAssetLink); + + UPROPERTY() + TObjectPtr PDGAssetLink; + + UPROPERTY(DuplicateTransient, Transient) + bool bIsPDGAssetLinkInitialized; +}; + + + + + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniCookable : public UObject, public IHoudiniAssetStateEvents +{ + GENERATED_UCLASS_BODY() + + // Declare translators as friend so they can easily directly modify + // Inputs, outputs and parameters + friend class FHoudiniEngineManager; + friend struct FHoudiniEngineUtils; + friend struct FHoudiniOutputTranslator; + //friend struct FHoudiniInputTranslator; + //friend struct FHoudiniSplineTranslator; + friend struct FHoudiniParameterTranslator; + //friend struct FHoudiniPDGManager; + friend struct FHoudiniHandleTranslator; + friend class UHoudiniAssetComponent; + + // Delegate for when EHoudiniAssetState changes from InFromState to InToState on a HoudiniCookable (InHC) + DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnCookableStateChangeDelegate, UHoudiniCookable*, const EHoudiniAssetState, const EHoudiniAssetState); + // Delegate for when EHoudiniAssetState changes from InFromState to InToState on a Houdini Asset Component (InHAC). + DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnAssetStateChangeDelegate, UHoudiniCookable*, const EHoudiniAssetState, const EHoudiniAssetState); + + DECLARE_MULTICAST_DELEGATE_OneParam(FOnPreInstantiationDelegate, UHoudiniCookable*); + DECLARE_MULTICAST_DELEGATE_OneParam(FOnPreCookDelegate, UHoudiniCookable*); + DECLARE_MULTICAST_DELEGATE_TwoParams(FOnPostCookDelegate, UHoudiniCookable*, bool); + DECLARE_MULTICAST_DELEGATE_TwoParams(FOnPreOutputProcessingDelegate, UHoudiniCookable*, bool); + DECLARE_MULTICAST_DELEGATE_TwoParams(FOnPostOutputProcessingDelegate, UHoudiniCookable*, bool); + + +public: + + virtual ~UHoudiniCookable(); + + //------------------------------------------------------------------------------------------------ + // Accessors + //------------------------------------------------------------------------------------------------ + + int32 GetNodeId() const { return NodeId; }; + void SetNodeId(int InNodeId) { NodeId = InNodeId; } + + EHoudiniAssetState GetCurrentState() const { return CurrentState; }; + EHoudiniAssetStateResult GetCurrentStateResult() const { return CurrentStateResult; }; + //virtual FString GetHoudiniAssetName() const; + + // Feature accessors + UCookableHoudiniAssetData* GetHoudiniAssetData() { return IsHoudiniAssetSupported() ? HoudiniAssetData : nullptr; }; + UCookableParameterData* GetParameterData() { return IsParameterSupported() ? ParameterData : nullptr; }; + UCookableInputData* GetInputData() { return IsInputSupported() ? InputData : nullptr; }; + UCookableOutputData* GetOutputData() { return IsOutputSupported() ? OutputData : nullptr;}; + UCookableComponentData* GetComponentData() { return IsComponentSupported() ? ComponentData : nullptr; }; + UCookablePDGData* GetPDGData() { return IsPDGSupported() ? PDGData : nullptr; }; + UCookableBakingData* GetBakingData() { return IsBakingSupported() ? BakingData : nullptr; }; + UCookableProxyData* GetProxyData() { return IsProxySupported() ? ProxyData : nullptr; }; + + bool SetParameterData(UCookableParameterData * ParameterData); + bool SetInputData(UCookableInputData*); + + USceneComponent* GetComponent() const; + AActor* GetOwner() const; + UWorld* GetWorld() const; + ULevel* GetLevel() const; + bool IsOwnerSelected() const; + UHoudiniAsset* GetHoudiniAsset(); + UHoudiniPDGAssetLink* GetPDGAssetLink(); + FString GetHoudiniAssetName() const; + + FGuid& GetHapiGUID(); + FString GetHapiAssetName() const; + FGuid GetCookableGUID() const; + + bool IsCookingEnabled() const { return bEnableCooking; }; + bool HasBeenLoaded() const { return bHasBeenLoaded; }; + bool HasBeenDuplicated() const { return bHasBeenDuplicated; }; + bool HasRecookBeenRequested() const { return bRecookRequested; }; + bool HasRebuildBeenRequested() const { return bRebuildRequested; }; + bool IsInstantiatingOrCooking() const; + + bool GetCookOnParameterChange() const; + bool GetCookOnTransformChange() const; + bool GetCookOnInputChange() const; + bool GetCookOnCookableInputCook() const; + bool IsOutputless() const; + bool GetUseOutputNodes() const; + bool GetOutputTemplateGeos() const; + bool GetUploadTransformsToHoudiniEngine() const; + bool GetLandscapeUseTempLayers() const; + bool GetEnableCurveEditing() const; + bool GetSplitMeshSupport() const; + + // Returns the last cached transform for the component - identity if not supported + FTransform GetLastComponentTransform() const; + + // Returns the current component transform - identity if not supported + FTransform GetComponentTransform() const; + + FHoudiniStaticMeshGenerationProperties GetStaticMeshGenerationProperties() const; + FMeshBuildSettings GetStaticMeshBuildSettings() const; + + FHoudiniStaticMeshGenerationProperties& GetStaticMeshGenerationProperties(); + FMeshBuildSettings& GetStaticMeshBuildSettings(); + + // Feature data accessors + int32 GetNumInputs() const + { +#if WITH_EDITORONLY_DATA + return IsInputSupported() ? InputData->GetNumInputs() : 0; +#else + return 0; +#endif + }; + + int32 GetNumOutputs() const { return IsOutputSupported() ? OutputData->Outputs.Num() : 0; }; + + int32 GetNumParameters() const + { +#if WITH_EDITORONLY_DATA + return IsParameterSupported() ? ParameterData->Parameters.Num() : 0; +#else + return 0; +#endif + }; + + int32 GetNumHandles() const { return IsComponentSupported() ? ComponentData->HandleComponents.Num() : 0; }; + + UHoudiniInput* GetInputAt(const int32& Idx) + { +#if WITH_EDITORONLY_DATA + return IsInputSupported() ? (InputData->Inputs.IsValidIndex(Idx) ? InputData->Inputs[Idx] : nullptr) : nullptr; +#else + return nullptr; +#endif + }; + + UHoudiniOutput* GetOutputAt(const int32& Idx) { return IsOutputSupported() ? (OutputData->Outputs.IsValidIndex(Idx) ? OutputData->Outputs[Idx] : nullptr) : nullptr;}; + + UHoudiniParameter* GetParameterAt(const int32& Idx) + { +#if WITH_EDITORONLY_DATA + return IsParameterSupported() ? (ParameterData->Parameters.IsValidIndex(Idx) ? ParameterData->Parameters[Idx] : nullptr) : nullptr; +#else + return nullptr; +#endif + }; + + UHoudiniHandleComponent* GetHandleComponentAt(const int32& Idx) { return IsComponentSupported() ? (ComponentData->HandleComponents.IsValidIndex(Idx) ? ComponentData->HandleComponents[Idx] : nullptr) : nullptr; }; + + // Try to find one of our parameter that matches another (name, type, size and enabled) + UHoudiniParameter* FindMatchingParameter(UHoudiniParameter* InOtherParam); + // Try to find one of our input that matches another one (name, isobjpath, index / parmId) + UHoudiniInput* FindMatchingInput(UHoudiniInput* InOtherInput); + // Try to find one of our handle that matches another one (name and handle type) + UHoudiniHandleComponent* FindMatchingHandle(UHoudiniHandleComponent* InOtherHandle); + // Finds a parameter by name + UHoudiniParameter* FindParameterByName(const FString& InParamName); + + // Output bake folder accessor + FDirectoryPath GetBakeFolder() const; + // Output temp folder accessor + FDirectoryPath GetTemporaryCookFolder() const; + // Returns the TemporaryCookFolder, if it is not empty. Otherwise returns the plugin default temporary + // cook folder. This function does not take the unreal_temp_folder attribute into account. + FString GetTemporaryCookFolderOrDefault() const; + // Returns the BakeFolder, if it is not empty. Otherwise returns the plugin default bake folder. This + // function does not take the unreal_bake_folder attribute into account. + FString GetBakeFolderOrDefault() const; + + // Returns true if a parameter definition update (excluding values) is needed. + bool IsParameterDefinitionUpdateNeeded() const { return IsParameterSupported() ? ParameterData->bParameterDefinitionUpdateNeeded : false; }; + + FString GetDisplayName() const; + + // Returns true if this cookable should try to start a session + virtual bool ShouldTryToStartFirstSession() const; + + // Needed for BP support + bool IsFullyLoaded() const { return bFullyLoaded; }; + // Whether this component is currently open in a Blueprint editor. This + // method is overridden by HoudiniAssetBlueprintComponent. + virtual bool HasOpenEditor() const { return false; }; + + int32 GetCookCount() const { return CookCount; }; + + bool WasLastCookSuccessful() const; + + // TODO COOKABLE: Move to component? +#if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION > 0) + ILevelInstanceInterface* GetLevelInstance() const; +#endif + + // Indicates if the cookable needs to be updated + bool NeedUpdate() const; + + // Derived blueprint based components will check whether the template component contains updates that needs to processed. + + // Indicates if the cookable's parameters need an update + bool NeedUpdateParameters() const; + + // Indicates if the cookable's inputs need an update + bool NeedUpdateInputs() const; + + // Indicates if any of cookable's outputs need an update + bool NeedUpdateOutputs() const; + +#if WITH_EDITORONLY_DATA + TArray>& GetParameters(); + const TArray>& GetParameters() const; +#endif +#if WITH_EDITORONLY_DATA + TArray>& GetInputs(); + const TArray>& GetInputs() const; +#endif + TArray>& GetOutputs(); + TArray>& GetHandleComponents(); + + void GetOutputs(TArray& OutOutputs) const; + bool HasAnyOutputComponent() const; + + TArray GetNodeIdsToCook() const; + TMap GetNodesToCookCookCounts() const; + + TArray& GetBakedOutputs(); + const TArray& GetBakedOutputs() const; + EHoudiniEngineBakeOption GetHoudiniEngineBakeOption() const; + bool GetReplacePreviousBake() const; + bool GetRemoveOutputAfterBake() const; + bool GetRecenterBakedActors() const; + + // Proxies + bool IsOverrideGlobalProxyStaticMeshSettings() const; + bool IsProxyStaticMeshEnabled() const; + bool IsProxyStaticMeshRefinementByTimerEnabled() const; + float GetProxyMeshAutoRefineTimeoutSeconds() const; + bool IsProxyStaticMeshRefinementOnPreSaveWorldEnabled() const; + bool IsProxyStaticMeshRefinementOnPreBeginPIEEnabled() const; + bool HasNoProxyMeshNextCookBeenRequested() const; + bool HasAnyCurrentProxyOutput() const; + bool HasAnyProxyOutput() const; + bool IsHoudiniCookedDataAvailable(bool& bOutNeedsRebuildOrDelete, bool& bOutInvalidState) const; + bool IsBakeAfterNextCookEnabled() const; + EHoudiniBakeAfterNextCook GetBakeAfterNextCook() const; + bool IsPlayInEditorRefinementAllowed() const; + EHoudiniEngineActorBakeOption GetActorBakeOption() const; + + FName GetAssetEditorId() const { return AssetEditorId; }; + + //------------------------------------------------------------------------------------------------ + // Mutators + //------------------------------------------------------------------------------------------------ + + // Set asset state + void SetCurrentState(EHoudiniAssetState InNewState); + // Set asset state + void SetCurrentStateResult(EHoudiniAssetStateResult InResult); + + void UpdateDormantStatus(); + + // Cleans up children components and PDG asset link (if any) after a duplication + void UpdatePostDuplicate(); + + // Clear nodes to cook. This will also clear their cook counts. + void ClearNodesToCook(); + void ClearOutputNodes() { return ClearNodesToCook(); }; + + void SetNodeIdsToCook(const TArray& InNodeIds); + + // Clear/disable the RefineMeshesTimer. + void ClearRefineMeshesTimer(); + + void SetHasComponentTransformChanged(bool InHasChanged); + + void MarkAsNeedCook(); + void MarkAsNeedRebuild(); + void MarkAsNeedInstantiation(); + + // TODO COOKABLE: protect me! + void MarkAsNeedRecookOrRebuild(bool bDoRebuild); + + void PreventAutoUpdates(); + + void OnDestroy(bool bDestroyingHierarchy); + void OnSessionConnected(); + void OnHoudiniAssetChanged(); + + void SetComponent(USceneComponent* InComp); + void SetHoudiniAssetComponent(UHoudiniAssetComponent* InComp); + void SetHoudiniAsset(UHoudiniAsset* InHAsset); + + bool SetTemporaryCookFolderPath(const FString& NewPath); + bool SetBakeFolderPath(const FString& NewPath); + bool SetTemporaryCookFolder(const FDirectoryPath& InPath); + bool SetBakeFolder(const FDirectoryPath& InPath); + + void SetCookingEnabled(const bool& bInCookingEnabled); + void SetHasBeenLoaded(const bool& InLoaded); + void SetHasBeenDuplicated(const bool& InDuplicated); + void SetCookCount(const int32& InCount); + void SetRecookRequested(const bool& InRecook); + void SetRebuildRequested(const bool& InRebuild); + + // Set to True to force the next cook to not build a proxy mesh (regardless of global or override settings) and + // instead build a UStaticMesh directly (if applicable for the output type). + void SetNoProxyMeshNextCookRequested(bool bInNoProxyMeshNextCookRequested); + void SetOverrideGlobalProxyStaticMeshSettings(bool InEnable); + void SetEnableProxyStaticMeshOverride(bool InEnable); + void SetEnableProxyStaticMeshRefinementByTimerOverride(bool InEnable); + void SetProxyMeshAutoRefineTimeoutSecondsOverride(float InValue); + void SetEnableProxyStaticMeshRefinementOnPreSaveWorldOverride(bool InEnable); + void SetEnableProxyStaticMeshRefinementOnPreBeginPIEOverride(bool InEnable); + + void SetCookOnParameterChange(bool bEnable); + void SetCookOnTransformChange(bool bEnable); + void SetCookOnInputChange(bool bEnable); + void SetCookOnCookableInputCook(bool bEnable); + void SetOutputless(bool bEnable); + void SetUseOutputNodes(bool bEnable); + void SetOutputTemplateGeos(bool bEnable); + void SetUploadTransformsToHoudiniEngine(bool bEnable); + void SetLandscapeUseTempLayers(bool bEnable); + void SetEnableCurveEditing(bool bEnable); + + // Set whether or not bake after cooking (disabled, always or once). + void SetBakeAfterNextCook(const EHoudiniBakeAfterNextCook InBakeAfterNextCook); + void SetHoudiniEngineBakeOption(const EHoudiniEngineBakeOption& InBakeOption); + void SetReplacePreviousBake(bool bInReplace); + void SetRemoveOutputAfterBake(bool bInRemove); + void SetRecenterBakedActors(bool bInRecenter); + void SetAllowPlayInEditorRefinement(bool bEnabled); + void SetActorBakeOption(const EHoudiniEngineActorBakeOption InBakeOption); + + void SetStaticMeshGenerationProperties(const FHoudiniStaticMeshGenerationProperties& InHSMGP); + void SetStaticMeshBuildSettings(const FMeshBuildSettings& InMBS); + + void SetNeedToUpdateEditorProperties(const bool& bNeedToUpdate) { bNeedToUpdateEditorProperties = bNeedToUpdate; }; + + bool NotifyCookedToDownstreamCookables(); + void AddDownstreamCookable(UHoudiniCookable* InDownstreamCookable); + void RemoveDownstreamCookable(UHoudiniCookable* InDownstreamCookable); + void ClearDownstreamCookable(); + + void SetAssetEditorId(const FName& InName) { AssetEditorId = InName; }; + + void SetNodeLabelPrefix(const FString& Prefix); + + const FString & GetNodeLabelPrefix() const; + + //------------------------------------------------------------------------------------------------ + // Supported Features + //------------------------------------------------------------------------------------------------ + + // Whether or not this component should be able to delete the Houdini nodes + // that correspond to the HoudiniAsset when being deregistered. + virtual bool CanDeleteHoudiniNodes() const; + + virtual bool IsInputTypeSupported(EHoudiniInputType InType); + virtual bool IsOutputTypeSupported(EHoudiniOutputType InType); + + // Feature accessors + virtual bool IsHoudiniAssetSupported() const { return bHasHoudiniAsset && HoudiniAssetData; }; + virtual bool IsParameterSupported() const { return bHasParameters && ParameterData; }; + virtual bool IsInputSupported() const { return bHasInputs && InputData; }; + virtual bool IsOutputSupported() const { return bHasOutputs && OutputData; }; + virtual bool IsComponentSupported() const { return bHasComponent && ComponentData; }; + virtual bool IsPDGSupported() const { return bHasPDG && PDGData; }; + virtual bool IsBakingSupported() const { return IsOutputSupported() && bHasBaking && BakingData; }; + virtual bool IsProxySupported() const { return IsOutputSupported() && bHasProxy && ProxyData; }; + + // Needed for BP support + virtual void NotifyHoudiniRegisterCompleted() {}; + virtual void NotifyHoudiniPreUnregister() {}; + virtual void NotifyHoudiniPostUnregister() {}; + + // Feature mutators + virtual void SetHoudiniAssetSupported(bool bSupport) { bHasHoudiniAsset = bSupport; }; + virtual void SetParameterSupported(bool bSupport) { bHasParameters = bSupport; }; + virtual void SetInputSupported(bool bSupport) { bHasInputs = bSupport; }; + virtual void SetOutputSupported(bool bSupport) { bHasOutputs = bSupport; }; + virtual void SetComponentSupported(bool bSupport) { bHasComponent = bSupport; }; + virtual void SetPDGSupported(bool bSupport) { bHasPDG = bSupport; }; + virtual void SetBakingSupported(bool bSupport) { bHasBaking = bSupport; }; + virtual void SetProxySupported(bool bSupport) { bHasProxy = bSupport; }; + + // Turn On/Off Notifications & Unreal UI + void SetDoSlateNotifications(bool bOnOff) { bDoSlateNotifications = bOnOff; } + bool GetDoSlateNotifications() const { return bDoSlateNotifications; } + void SetAllowUpdateEditorProperties(bool bOnOff) { bAllowUpdateEditorProperties = bOnOff; } + + void SetAutoCook(bool bOnOff) { bAutoCook = bOnOff; } + //------------------------------------------------------------------------------------------------ + // Delegates / Public API + //------------------------------------------------------------------------------------------------ + + // + // Begin: IHoudiniAssetStateEvents + // + + virtual void HandleOnHoudiniAssetStateChange(UObject* InHoudiniAssetContext, const EHoudiniAssetState InFromState, const EHoudiniAssetState InToState) override; + + FORCEINLINE virtual FOnHoudiniAssetStateChange& GetOnHoudiniAssetStateChangeDelegate() override { return OnHoudiniAssetStateChangeDelegate; } + FOnCookableStateChangeDelegate& GetOnCookableStateChangeDelegate() { return OnCookableStateChangeDelegate; } + + + // + // End: IHoudiniAssetStateEvents + // + + // Called by HandleOnHoudiniAssetStateChange when entering the PostCook state. Broadcasts OnPostCookDelegate. + void HandleOnPreInstantiation(); + void HandleOnPreCook(); + void HandleOnPostCook(); + void HandleOnPostBake(bool bInSuccess); + + // Other public API delegates + void HandleOnPreOutputProcessing(); + void HandleOnPostOutputProcessing(); + + void QueuePreCookCallback(const TFunction& CallbackFn); + + void SetRefineMeshesTimer(); + void OnRefineMeshesTimerFired(); + + // Delegates + FOnPreInstantiationDelegate& GetOnPreInstantiationDelegate() { return OnPreInstantiationDelegate; }; + FOnPreCookDelegate& GetOnPreCookDelegate() { return OnPreCookDelegate; }; + FOnPostCookDelegate& GetOnPostCookDelegate() { return OnPostCookDelegate; }; + FOnPreOutputProcessingDelegate& GetOnPreOutputProcessingDelegate() { return OnPreOutputProcessingDelegate; }; + FOnPostOutputProcessingDelegate& GetOnPostOutputProcessingDelegate() { return OnPostOutputProcessingDelegate; }; + FOnAssetStateChangeDelegate& GetOnAssetStateChangeDelegate() { return OnAssetStateChangeDelegate; }; + + bool GetIsPCG() { return bIsPCG; } + void SetIsPCG(bool bOnfOff) { bIsPCG = bOnfOff; } + + void SetLandscapeModificationEnabled(bool bOnOff) { bIsLandscapeModification = bOnOff; } + bool IsLandscapeModificationEnabled() { return bIsLandscapeModification; } + +protected: + // Do any object - specific cleanup required immediately after loading an object. + // This is not called for newly - created objects, and by default will always execute on the game thread. + virtual void PostLoad() override; + + virtual void PostEditImport() override; + + virtual void BeginDestroy() override; + +protected: + + // Id of the corresponding Houdini node. + UPROPERTY(DuplicateTransient,Transient) + int32 NodeId; // AssetId + + FString NodeLabelPrefix; + + // NEW: The name of the node we're creating/fetching + // This is NOT an ASSET name - Assets are handled via ASSET DATA + // TODO COOKABLE: ? path name ? + FString NodeName; + + // NEW: Indicates if our node should be created when this cookable + // is instantiated - or if we should simply read data/output from it + bool bNodeNeedsToBeCreated; + + // Current state (switch to new State?) + UPROPERTY(DuplicateTransient) + EHoudiniAssetState CurrentState; // AssetState + + // Result of the current state + UPROPERTY(DuplicateTransient) + EHoudiniAssetStateResult CurrentStateResult; // AssetStateResult + + // Unique GUID created per cookable + UPROPERTY(DuplicateTransient) + FGuid CookableGUID; // ComponentGUID + + // GUID used to track asynchronous cooking requests of this cookable + UPROPERTY(DuplicateTransient) + FGuid HapiGUID; // HapiGUID + + // This cookable's name + UPROPERTY(DuplicateTransient) + FString Name; + + // Number of times this has been cooked. + UPROPERTY(DuplicateTransient) + int32 CookCount; // AssetCookCount + + UPROPERTY() + bool bAutoCook = true; + + UPROPERTY() + bool bShouldUpdateUI = false; + + // Ids of the nodes that should also be cooked with this cookable + // This can be used for additional outputs or templated nodes if used. + UPROPERTY(Transient, DuplicateTransient) + TArray NodeIdsToCook; // NodeIdsToCook + + // Cook counts for nodes in the NodeIdsToCook array. + UPROPERTY(Transient, DuplicateTransient) + TMap NodesToCookCookCounts; // OutputNodeCookCounts + + UPROPERTY(DuplicateTransient) + bool bPendingDelete; // bPendingDelete + + UPROPERTY(DuplicateTransient) + bool bRecookRequested; // bRecookRequested + + UPROPERTY(DuplicateTransient) + bool bRebuildRequested; // bRebuildRequested + + UPROPERTY(DuplicateTransient) + bool bEnableCooking; // bEnableCooking + + UPROPERTY(DuplicateTransient) + bool bForceNeedUpdate; // bForceNeedUpdate + + UPROPERTY(DuplicateTransient) + bool bLastCookSuccess; // bLastCookSuccess + + // The last timestamp this cookable was ticked + // Used to prioritize/limit the number of Cookable processed per tick + UPROPERTY(Transient) + double LastTickTime; // LastTickTime + + // TODO COOKABLE: Assess if needed? + UPROPERTY(DuplicateTransient) + bool bHasBeenLoaded; // bHasBeenLoaded + + // TODO COOKABLE: Assess if needed? + UPROPERTY(Transient, DuplicateTransient) + bool bFullyLoaded; // bFullyLoaded + + // TODO COOKABLE: Assess if needed? + // Sometimes, specifically when editing level instances, the Unreal Editor will duplicate the HDA, + // then duplicate it again, before we get a change to call UpdatePostDuplicate(). + // So bHasBeenDuplicated should not be cleared and is so not marked DuplicateTransient. + UPROPERTY() + bool bHasBeenDuplicated; // bHasBeenDuplicated + + // Indicates whether or not this cookable should update its editor UI + // This is to prevent successive calls of the function for the same cookables + UPROPERTY(Transient, DuplicateTransient) + bool bNeedToUpdateEditorProperties; + + + // Used to enable certain detail panel widgets. + UPROPERTY() + bool bIsPCG; + + // Can the cookable modify landscapes? Primarily for PCG. + UPROPERTY() + bool bIsLandscapeModification; + + + // + // COOKABLE DATA + // + + // HOUDINI ASSET + // Indicates if this cookable is using an HDA + UPROPERTY() + bool bHasHoudiniAsset; + + // Structure containing the HDA data + UPROPERTY(EditAnywhere, Category = "Houdini Cookable") + TObjectPtr HoudiniAssetData; + + // PARAMETERS + // Indicates if this cookable has parameters + UPROPERTY() + bool bHasParameters; + + // Structure containing the parameter data + UPROPERTY(EditAnywhere, Category = "Houdini Cookable") + TObjectPtr ParameterData; + + // INPUTS + // Indicates if this cookable has inputs + UPROPERTY() + bool bHasInputs; + + // Structure containing the input data + UPROPERTY(EditAnywhere, Category = "Houdini Cookable") + TObjectPtr InputData; + + // OUTPUTS + // Indicates if this cookable has outputs + UPROPERTY() + bool bHasOutputs; + + // Structure containing the output data + UPROPERTY(EditAnywhere, Category = "Houdini Cookable") + TObjectPtr OutputData; + + // COMPONENTS / TRANSFORM + // Indicates if this cookable has a component/is placed in the level + UPROPERTY() + bool bHasComponent; // bIsInWorld? + + // Structure containing the component's data + UPROPERTY(EditAnywhere, Category = "Houdini Cookable") + TObjectPtr ComponentData; + + // PDG + // Indicates if this cookable has access to PDG + UPROPERTY() + bool bHasPDG; + + // Structure containing the PDG data + UPROPERTY(EditAnywhere, Category = "Houdini Cookable") + TObjectPtr PDGData; + + // Baking + // Indicates if this cookable has access to baking + UPROPERTY() + bool bHasBaking; + // Structure containing the Baking data + UPROPERTY(EditAnywhere, Category = "Houdini Cookable") + TObjectPtr BakingData; + + // Proxy + // Indicates if this cookable canm use proxy meshes + UPROPERTY() + bool bHasProxy; + // Structure containing the Proxy data + UPROPERTY(EditAnywhere, Category = "Houdini Cookable") + TObjectPtr ProxyData; + + // + // Public API delegates + // + + // Delegate that is broadcast when current State changes + FOnHoudiniAssetStateChange OnHoudiniAssetStateChangeDelegate; + + // Delegate that is broadcast when the current state changes (Cookable version) + FOnCookableStateChangeDelegate OnCookableStateChangeDelegate; + + // Delegate to broadcast before instantiation + // Arguments are (HoudiniAssetComponent* HAC) + FOnPreInstantiationDelegate OnPreInstantiationDelegate; + + // Delegate to broadcast after a post cook event + // Arguments are (HoudiniAssetComponent* HAC, bool IsSuccessful) + FOnPreCookDelegate OnPreCookDelegate; + + // Delegate to broadcast after a post cook event + // Arguments are (HoudiniAssetComponent* HAC, bool IsSuccessful) + FOnPostCookDelegate OnPostCookDelegate; + + FOnPostOutputProcessingDelegate OnPostOutputProcessingDelegate; + FOnPreOutputProcessingDelegate OnPreOutputProcessingDelegate; + + // Delegate that is broadcast when the asset state changes (HAC version). ?? + FOnAssetStateChangeDelegate OnAssetStateChangeDelegate; + + // Store any PreCookCallbacks here until the Cookable is ready to process them during the PreCook event. + TArray> PreCookCallbacks; + + UPROPERTY(Transient) + bool bDoSlateNotifications; + + UPROPERTY(Transient) + bool bAllowUpdateEditorProperties; + +#if WITH_EDITORONLY_DATA +public: + + UPROPERTY() + bool bGenerateMenuExpanded; + + UPROPERTY() + bool bBakeMenuExpanded; + + UPROPERTY() + bool bAssetOptionMenuExpanded; + + UPROPERTY() + bool bHelpAndDebugMenuExpanded; +#endif + + // Indicates the Id of the AssetEditor viewing this cookable + // Can be null/empty if not viewded by an AssetEditor + UPROPERTY() + FName AssetEditorId; +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniEditorAssetStateSubsystemInterface.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include "HoudiniEditorAssetStateSubsystemInterface.h" + +#if WITH_EDITOR + +IHoudiniEditorAssetStateSubsystemInterface* IHoudiniEditorAssetStateSubsystemInterface::Instance = nullptr; + +void IHoudiniEditorAssetStateSubsystemInterface::SetInstance(IHoudiniEditorAssetStateSubsystemInterface* InInstance) +{ + check(InInstance == nullptr || Instance == nullptr); + Instance = InInstance; +} + +#endif + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniEditorAssetStateSubsystemInterface.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreTypes.h" + +#include "HoudiniAssetStateTypes.h" + +#if WITH_EDITOR +/** + * Interface for the HoudiniEditorAssetStateSubsystem. The interface defines / give us access to the API of the + * HoudiniEditorAssetStateSubsystem in the runtime module, while the implementation of the editor subsystem is in + * the editor module. + * + * Based on WorldPartition/ContentBundle/ContentBundleEditorSubsystemInterface.h + */ +class IHoudiniEditorAssetStateSubsystemInterface +{ + +public: + static IHoudiniEditorAssetStateSubsystemInterface* Get() { return Instance; } + + virtual ~IHoudiniEditorAssetStateSubsystemInterface() {}; + +protected: + // This should be called by the HoudiniEngineManager to notify the subsystem that a Houdini Asset object, such + // as a HoudiniAssetComponent, has changed state. + virtual void NotifyOfHoudiniAssetStateChange(UObject* InHoudiniAssetContext, const EHoudiniAssetState InFromState, const EHoudiniAssetState InToState) = 0; + + friend class UHoudiniAssetComponent; + friend class UHoudiniCookable; + + static HOUDINIENGINERUNTIME_API void SetInstance(IHoudiniEditorAssetStateSubsystemInterface* InInstance); + static HOUDINIENGINERUNTIME_API IHoudiniEditorAssetStateSubsystemInterface* Instance; +}; +#endif + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniEngineCopyPropertiesInterface.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineCopyPropertiesInterface.h" + +void IHoudiniEngineCopyPropertiesInterface::CopyPropertiesFrom(UObject* FromObject) +{ + +} + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniEngineCopyPropertiesInterface.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#include "Engine/Engine.h" +#include "UObject/ObjectMacros.h" +#include "UObject/Interface.h" +#include "HoudiniEngineCopyPropertiesInterface.generated.h" + + +UINTERFACE(MinimalAPI, meta = (CannotImplementInterfaceInBlueprint)) +class UHoudiniEngineCopyPropertiesInterface : public UInterface +{ + GENERATED_BODY() +}; + +class HOUDINIENGINERUNTIME_API IHoudiniEngineCopyPropertiesInterface +{ + GENERATED_BODY() + +public: + virtual void CopyPropertiesFrom(UObject* FromObject); +}; + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniEngineEditorSettings.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineEditorSettings.h" +#include "HoudiniEngineRuntimeUtils.h" + +#include "HoudiniEngineRuntimePrivatePCH.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + + +UHoudiniEngineEditorSettings::UHoudiniEngineEditorSettings(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + CategoryName = TEXT("Plugins"); + SectionName = TEXT("HoudiniEngine"); + +#if WITH_EDITORONLY_DATA + // Custom Houdini location. + UseCustomHoudiniLocation = EHoudiniEngineEditorSettingUseCustomLocation::Project; + CustomHoudiniLocation.Path = TEXT(""); +#endif +} + +#if WITH_EDITOR +FText UHoudiniEngineEditorSettings::GetSectionText() const +{ + return LOCTEXT("HoudiniEngineUserSettingsDisplayName", "Houdini Engine"); +} +#endif // WITH_EDITOR + +#if WITH_EDITOR +void UHoudiniEngineEditorSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + UObject::PostEditChangeProperty(PropertyChangedEvent); + + static FName NAME_UserToolCategories = GET_MEMBER_NAME_CHECKED(UHoudiniEngineEditorSettings, UserToolCategories); + + if (PropertyChangedEvent.Property->GetFName() == NAME_UserToolCategories) + { + OnUserToolCategoriesChanged.Broadcast(); + } +} + +void +UHoudiniEngineEditorSettings::PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) +{ + UObject::PostEditChangeChainProperty(PropertyChangedEvent); + + static FName NAME_UserToolCategories = GET_MEMBER_NAME_CHECKED(UHoudiniEngineEditorSettings, UserToolCategories); + + const FName& HeadName = PropertyChangedEvent.PropertyChain.GetHead()->GetValue()->GetFName(); + + if (HeadName == NAME_UserToolCategories) + { + OnUserToolCategoriesChanged.Broadcast(); + } + else if (HeadName == GET_MEMBER_NAME_CHECKED(UHoudiniEngineEditorSettings, CustomHoudiniLocation)) + { + if (!FHoudiniEngineRuntimeUtils::CheckCustomHoudiniLocation(CustomHoudiniLocation.Path)) + CustomHoudiniLocation.Path = TEXT(""); + } +} +#endif + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniEngineEditorSettings.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DeveloperSettings.h" +#include "UObject/Object.h" + +#include "HoudiniToolTypes.h" + +#include "HoudiniEngineEditorSettings.generated.h" + +USTRUCT() +struct FUserPackageRules +{ + GENERATED_BODY() + + FUserPackageRules() + : ToolsPackageAsset(nullptr) + {} + + // Tools package that will be used to resolve the include/exclude patterns. + UPROPERTY(EditAnywhere, Category="User Category") + TObjectPtr ToolsPackageAsset; + + // Include any tools that match the following patterns (relative to the Tools Package). + UPROPERTY(EditAnywhere, Category="User Category") + TArray Include; + + // Any tools matching these patterns (relative to the Tools Package) will be omitted from the inclusion set. + UPROPERTY(EditAnywhere, Category="User Category") + TArray Exclude; +}; + +UENUM() +enum class EHoudiniEngineEditorSettingUseCustomLocation : uint8 +{ + // Use the per-project plugin settings. + Project UMETA(DisplayName = "Use The Per-Project Plugin Settings"), + // Do not use a custom location at all. + Disabled UMETA(DisplayName = "Do Not Use Custom Location"), + // Use the custom location defined here in editor preferences. + Enabled UMETA(DisplayName = "Use Custom Location"), +}; + +USTRUCT() +struct FUserCategoryRules +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category="User Category") + TArray Packages; +}; + +UCLASS(config=HoudiniEngine) +class HOUDINIENGINERUNTIME_API UHoudiniEngineEditorSettings : public UDeveloperSettings +{ + GENERATED_BODY() +public: + + DECLARE_MULTICAST_DELEGATE(FOnUserToolCategoriesChanged); + + UHoudiniEngineEditorSettings(const FObjectInitializer& ObjectInitializer); + + // Begin -- UDeveloperSettings interface + + // Gets the settings container name for the settings, either Project or Editor + // We have to override the container name to Editor since we are using a custom config name in the UCLASS macro. + virtual FName GetContainerName() const override { return TEXT("Editor"); } + +#if WITH_EDITOR + virtual FText GetSectionText() const override; +#endif + + //End -- UDeveloperSettings interface + +#if WITH_EDITOR + + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + virtual void PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) override; + +#endif + +#if WITH_EDITORONLY_DATA + // User defined categories for HoudiniTools + UPROPERTY(GlobalConfig, EditAnywhere, Category=HoudiniTools) + TMap UserToolCategories; + + FOnUserToolCategoriesChanged OnUserToolCategoriesChanged; + + //------------------------------------------------------------------------------------------------------------- + // Custom Houdini Location + //------------------------------------------------------------------------------------------------------------- + + // Whether to use custom Houdini location from the project plugin settings, the editor setting, or do not use a custom location. + UPROPERTY(config, EditAnywhere, Category = HoudiniLocation, Meta = (DisplayName = "Use custom Houdini location (requires restart)", ConfigRestartRequired=true)) + EHoudiniEngineEditorSettingUseCustomLocation UseCustomHoudiniLocation; + + // Custom Houdini location (where HAPI library is located). + UPROPERTY(config, EditAnywhere, Category = HoudiniLocation, Meta = (EditCondition = "UseCustomHoudiniLocation == EHoudiniEngineEditorSettingUseCustomLocation::Enabled", DisplayName = "Custom Houdini location", ConfigRestartRequired=true)) + FDirectoryPath CustomHoudiniLocation; +#endif +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniEngineRuntime.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniRuntimeSettings.h" + +#include "Modules/ModuleManager.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FHoudiniEngineRuntime, HoudiniEngineRuntime); +DEFINE_LOG_CATEGORY(LogHoudiniEngineRuntime); + +FHoudiniEngineRuntime * +FHoudiniEngineRuntime::HoudiniEngineRuntimeInstance = nullptr; + + +FHoudiniEngineRuntime & +FHoudiniEngineRuntime::Get() +{ + return *HoudiniEngineRuntimeInstance; +} + + +bool +FHoudiniEngineRuntime::IsInitialized() +{ + return FHoudiniEngineRuntime::HoudiniEngineRuntimeInstance != nullptr; +} + + +FHoudiniEngineRuntime::FHoudiniEngineRuntime() +{ +} + + +void FHoudiniEngineRuntime::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module + // Store the instance. + FHoudiniEngineRuntime::HoudiniEngineRuntimeInstance = this; +} + + +void FHoudiniEngineRuntime::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. + FHoudiniEngineRuntime::HoudiniEngineRuntimeInstance = nullptr; +} + + +void +FHoudiniEngineRuntime::MarkNodeIdAsPendingDelete(const int32& InNodeId, bool bDeleteParent) +{ + if (InNodeId >= 0) + { + // FDebug::DumpStackTraceToLog(); + NodeIdsPendingDelete.AddUnique(InNodeId); + + if (bDeleteParent) + { + NodeIdsParentPendingDelete.AddUnique(InNodeId); + } + } +} + + +int32 +FHoudiniEngineRuntime::GetNodeIdsPendingDeleteCount() +{ + if (!IsInitialized()) + return 0; + + FScopeLock ScopeLock(&CriticalSection); + + return NodeIdsPendingDelete.Num(); +} + + +int32 +FHoudiniEngineRuntime::GetNodeIdsPendingDeleteAt(const int32& Index) +{ + if (!IsInitialized()) + return -1; + + FScopeLock ScopeLock(&CriticalSection); + + if (!NodeIdsPendingDelete.IsValidIndex(Index)) + return -1; + + return NodeIdsPendingDelete[Index]; +} + + +void +FHoudiniEngineRuntime::RemoveNodeIdPendingDeleteAt(const int32& Index) +{ + if (!IsInitialized()) + return; + + FScopeLock ScopeLock(&CriticalSection); + if (!NodeIdsPendingDelete.IsValidIndex(Index)) + return; + + NodeIdsPendingDelete.RemoveAt(Index); +} + + +bool +FHoudiniEngineRuntime::IsParentNodePendingDelete(const int32& NodeId) +{ + return NodeIdsParentPendingDelete.Contains(NodeId); +} + + +void +FHoudiniEngineRuntime::RemoveParentNodePendingDelete(const int32& NodeId) +{ + if (NodeIdsParentPendingDelete.Contains(NodeId)) + NodeIdsParentPendingDelete.Remove(NodeId); +} + + +FString +FHoudiniEngineRuntime::GetDefaultTemporaryCookFolder() const +{ + // Get Runtime settings to get the Temp Cook Folder + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (!HoudiniRuntimeSettings) + return HAPI_UNREAL_DEFAULT_TEMP_COOK_FOLDER; + + return HoudiniRuntimeSettings->DefaultTemporaryCookFolder; +} + + +FString +FHoudiniEngineRuntime::GetDefaultBakeFolder() const +{ + // Get Runtime settings to get the default bake Folder + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (!HoudiniRuntimeSettings) + return HAPI_UNREAL_DEFAULT_BAKE_FOLDER; + + return HoudiniRuntimeSettings->DefaultBakeFolder; +} + + + +int32 +FHoudiniEngineRuntime::GetRegisteredHoudiniCookableCount() +{ + if (!IsInitialized()) + return 0; + + FScopeLock ScopeLock(&CriticalSection); + return RegisteredHoudiniCookables.Num(); +} + + +UHoudiniCookable* +FHoudiniEngineRuntime::GetRegisteredHoudiniCookableAt(const int32& Index) +{ + if (!IsInitialized()) + return nullptr; + + FScopeLock ScopeLock(&CriticalSection); + if (!RegisteredHoudiniCookables.IsValidIndex(Index)) + return nullptr; + + TWeakObjectPtr Ptr = RegisteredHoudiniCookables[Index]; + if (!Ptr.IsValid()) + return nullptr; + + if (Ptr.IsStale()) + return nullptr; + + return Ptr.Get(); +} + + +void +FHoudiniEngineRuntime::CleanUpRegisteredHoudiniCookables() +{ + // Remove Stale and invalid Cookables + FScopeLock ScopeLock(&CriticalSection); + for (int Idx = RegisteredHoudiniCookables.Num() - 1; Idx >= 0; Idx--) + { + TWeakObjectPtr Ptr = RegisteredHoudiniCookables[Idx]; + if (!Ptr.IsValid() || Ptr.IsStale()) + { + UnRegisterHoudiniCookable(Idx); + continue; + } + + UHoudiniCookable* CurrentHC = Ptr.Get(); + if (!IsValid(CurrentHC)) + { + UnRegisterHoudiniCookable(Idx); + continue; + } + } +} + + +bool +FHoudiniEngineRuntime::IsCookableRegistered(UHoudiniCookable* HC) const +{ + // No need for duplicates + if (HC && RegisteredHoudiniCookables.Find(HC) != INDEX_NONE) + return true; + + return false; +} + + +void +FHoudiniEngineRuntime::RegisterHoudiniCookable(UHoudiniCookable* HC, bool bAllowArchetype) +{ + if (!FHoudiniEngineRuntime::IsInitialized()) + return; + + if (!IsValid(HC)) + return; + + // RF_Transient indicates a temporary/preview object + // No need to instantiate/cook those in Houdini + // RF_ArchetypeObject is the template for blueprinted HDA, so we need to be able to register those. + if (HC->HasAnyFlags(RF_Transient) || (HC->HasAnyFlags(RF_ArchetypeObject) && !bAllowArchetype) || HC->HasAnyFlags(RF_ClassDefaultObject)) + return; + + // No need for duplicates + if (IsCookableRegistered(HC)) + return; + + HOUDINI_BP_MESSAGE(TEXT("[FHoudiniEngineRuntime::RegisterHoudiniCookable] HAC: %s"), *(HC->GetPathName())); + + // Before adding, clean up the all ready registered + CleanUpRegisteredHoudiniCookables(); + + // Add the new Cookable + { + FScopeLock ScopeLock(&CriticalSection); + RegisteredHoudiniCookables.Add(HC); + } + + HC->NotifyHoudiniRegisterCompleted(); +} + + + +void +FHoudiniEngineRuntime::UnRegisterHoudiniCookable(UHoudiniCookable* HC) +{ + if (!IsInitialized()) + return; + + if (!IsValid(HC)) + return; + + if (RegisteredHoudiniCookables.IsEmpty()) + return; + + // Calling GetPathName here may lead to some crashes due to invalid outers... + //HOUDINI_LOG_DISPLAY(TEXT("[FHoudiniEngineRuntime::UnRegisterHoudiniCookable] HC: %s"), *(HC->GetPathName()) ); + + FScopeLock ScopeLock(&CriticalSection); + + /* + int32 FoundIdx = -1; + for (int32 n = RegisteredHoudiniCookables.Num() - 1; n >= 0; n--) + { + TWeakObjectPtr& CurHC = RegisteredHoudiniCookables[n]; + if (!CurHC.IsValid(true) || CurHC.IsStale(false)) + { + // Remove stale/invalid HAC from Array? + RegisteredHoudiniCookables.RemoveAt(n); + continue; + } + + if (CurHC.Get() == HC) + FoundIdx = n; + } + + if (FoundIdx < 0 || !RegisteredHoudiniCookables.IsValidIndex(FoundIdx)) + return; + */ + + int32 FoundIdx = -1; + for(int nIdx = 0; nIdx < RegisteredHoudiniCookables.Num(); nIdx++) + { + TWeakObjectPtr Ptr = RegisteredHoudiniCookables[nIdx]; + if(!Ptr.IsValid(true, true)) + continue; + + UHoudiniCookable* CurrentHC = Ptr.GetEvenIfUnreachable(); + if(CurrentHC && CurrentHC == HC) + { + FoundIdx = nIdx; + break; + } + } + + if (FoundIdx != -1) + { + HC->NotifyHoudiniPreUnregister(); + UnRegisterHoudiniCookable(FoundIdx); + HC->NotifyHoudiniPostUnregister(); + } +} + + +void +FHoudiniEngineRuntime::UnRegisterHoudiniCookable(const int32& ValidIndex) +{ + if (!IsInitialized()) + return; + + FScopeLock ScopeLock(&CriticalSection); + TWeakObjectPtr Ptr = RegisteredHoudiniCookables[ValidIndex]; + + if (Ptr.IsValid(true, true)) + { + UHoudiniCookable* HC = Ptr.GetEvenIfUnreachable(); + if (HC && HC->CanDeleteHoudiniNodes() && HC->GetNodeId() >= 0) + { + MarkNodeIdAsPendingDelete(HC->GetNodeId(), true); + HC->SetNodeId(INDEX_NONE); + } + } + + RegisteredHoudiniCookables.RemoveAt(ValidIndex); +} + +static TAutoConsoleVariable CVarHoudiniPCGLogging( + TEXT("Houdini.PCGLogging"), + 0, // default value + TEXT("Enable (1) or disable (0) PCG Logging."), + ECVF_Default +); + +bool IsHoudiniPCGLoggingEnabled() +{ + bool Enabled = CVarHoudiniPCGLogging.GetValueOnAnyThread() != 0; + return Enabled; +} +#undef LOCTEXT_NAMESPACE + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniEngineRuntime.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniPDGAssetLink.h" + +#include "Modules/ModuleInterface.h" +#include "Misc/ScopeLock.h" +#include "UObject/WeakObjectPtrTemplates.h" + +class HOUDINIENGINERUNTIME_API FHoudiniEngineRuntime : public IModuleInterface +{ + public: + DECLARE_MULTICAST_DELEGATE(FOnToolOrPackageChanged) + + FHoudiniEngineRuntime(); + + // + // IModuleInterface methods. + // + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + // Return singleton instance of Houdini Engine Runtime, used internally. + static FHoudiniEngineRuntime & Get(); + + // Return true if singleton instance has been created. + static bool IsInitialized(); + + // + // Houdini Cookable registry + // + void CleanUpRegisteredHoudiniCookables(); + + void RegisterHoudiniCookable(UHoudiniCookable* HC, bool bAllowArchetype = false); + + void UnRegisterHoudiniCookable(UHoudiniCookable* HC); + void UnRegisterHoudiniCookable(const int32& ValidIdx); + + bool IsCookableRegistered(UHoudiniCookable* HC) const; + + int32 GetRegisteredHoudiniCookableCount(); + + UHoudiniCookable* GetRegisteredHoudiniCookableAt(const int32& Index); + + virtual TArray>* GetRegisteredHoudiniCookable() { return &RegisteredHoudiniCookables; }; + + // + // Node deletion + // + void MarkNodeIdAsPendingDelete(const int32& InNodeId, bool bDeleteParent = false); + + int32 GetNodeIdsPendingDeleteCount(); + + int32 GetNodeIdsPendingDeleteAt(const int32& Index); + + void RemoveNodeIdPendingDeleteAt(const int32& Index); + + bool IsParentNodePendingDelete(const int32& NodeId); + + void RemoveParentNodePendingDelete(const int32& NodeId); + + // + // + // + + // Returns the folder to be used for temporary cook content + FString GetDefaultTemporaryCookFolder() const; + + // Returns the defualt folder used for baking + FString GetDefaultBakeFolder() const; + + void BroadcastToolOrPackageChanged() const { OnToolOrPackageChanged.Broadcast(); } + + FOnToolOrPackageChanged& GetOnToolOrPackageChangedEvent() { return OnToolOrPackageChanged; } + + private: + + // Synchronization primitive. + FCriticalSection CriticalSection; + + // Singleton instance. + static FHoudiniEngineRuntime * HoudiniEngineRuntimeInstance; + + // Array of Cookable + TArray> RegisteredHoudiniCookables; + + TArray NodeIdsPendingDelete; + + TArray NodeIdsParentPendingDelete; + + FOnToolOrPackageChanged OnToolOrPackageChanged; +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniEngineRuntimePrivatePCH.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "Logging/LogMacros.h" +#include "Runtime/Launch/Resources/Version.h" + +// Define module names. +#define HOUDINI_MODULE "HoudiniEngine" +#define HOUDINI_MODULE_EDITOR "HoudiniEngineEditor" +#define HOUDINI_MODULE_RUNTIME "HoudiniEngineRuntime" + +// Declare the log category depending on the module we're in +#ifdef HOUDINI_ENGINE_EDITOR + #define HOUDINI_LOCTEXT_NAMESPACE HOUDINI_MODULE_EDITOR + HOUDINIENGINEEDITOR_API DECLARE_LOG_CATEGORY_EXTERN(LogHoudiniEngineEditor, Log, All); + #define HOUDINI_ENGINE_EDITOR_LOG_CATEGORY_DECLARED +#else + #ifdef HOUDINI_ENGINE + #define HOUDINI_LOCTEXT_NAMESPACE HOUDINI_MODULE + HOUDINIENGINE_API DECLARE_LOG_CATEGORY_EXTERN(LogHoudiniEngine, Log, All); + #else + #define HOUDINI_LOCTEXT_NAMESPACE HOUDINI_MODULE_RUNTIME + HOUDINIENGINERUNTIME_API DECLARE_LOG_CATEGORY_EXTERN(LogHoudiniEngineRuntime, Log, All); + #endif +#endif + +//--------------------------------------------------------------------------------------------------------------------- +// Default session settings +//--------------------------------------------------------------------------------------------------------------------- + +#define HAPI_UNREAL_SESSION_SERVER_AUTOSTART true +#define HAPI_UNREAL_SESSION_SERVER_TIMEOUT 3000.0f +#define HAPI_UNREAL_SESSION_SERVER_HOST TEXT( "localhost" ) +#define HAPI_UNREAL_SESSION_SERVER_PORT 9090 +#if PLATFORM_MAC + #define HAPI_UNREAL_SESSION_SERVER_PIPENAME TEXT( "/tmp/hapi" ) +#else + #define HAPI_UNREAL_SESSION_SERVER_PIPENAME TEXT( "hapi" ) +#endif + + + +// Names of HAPI libraries on different platforms. +#define HAPI_LIB_OBJECT_WINDOWS TEXT( "libHAPIL.dll" ) +#define HAPI_LIB_OBJECT_MAC TEXT( "libHAPIL.dylib" ) +#define HAPI_LIB_OBJECT_LINUX TEXT( "libHAPIL.so" ) + +//--------------------------------------------------------------------------------------------------------------------- +// LOG MACROS +//--------------------------------------------------------------------------------------------------------------------- + +// Whether to enable logging. +#define HOUDINI_ENGINE_LOGGING 1 + +#ifdef HOUDINI_ENGINE_LOGGING + #ifdef HOUDINI_ENGINE + #define HOUDINI_LOG_HELPER(VERBOSITY, HOUDINI_LOG_TEXT, ...) \ + do \ + { \ + UE_LOG( LogHoudiniEngine, VERBOSITY, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ); \ + } \ + while ( 0 ) + #else + #ifdef HOUDINI_ENGINE_EDITOR + #define HOUDINI_LOG_HELPER(VERBOSITY, HOUDINI_LOG_TEXT, ...) \ + do \ + { \ + UE_LOG( LogHoudiniEngineEditor, VERBOSITY, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ); \ + } \ + while ( 0 ) + #else + #define HOUDINI_LOG_HELPER( VERBOSITY, HOUDINI_LOG_TEXT, ... ) \ + do \ + { \ + UE_LOG( LogHoudiniEngineRuntime, VERBOSITY, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ); \ + } \ + while ( 0 ) + #endif + #endif + + #define HOUDINI_LOG_MESSAGE( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_LOG_HELPER( Log, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + + #define HOUDINI_LOG_FATAL( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_LOG_HELPER( Fatal, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + + #define HOUDINI_LOG_ERROR( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_LOG_HELPER( Error, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + + #define HOUDINI_LOG_WARNING( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_LOG_HELPER( Warning, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + + #define HOUDINI_LOG_DISPLAY( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_LOG_HELPER( Display, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) +#else + #define HOUDINI_LOG_MESSAGE( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_LOG_FATAL( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_LOG_ERROR( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_LOG_WARNING( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_LOG_DISPLAY( HOUDINI_LOG_TEXT, ... ) +#endif + + +#define HOUDINI_DEBUG_EXPAND_UE_LOG(LONG_NAME, VERBOSITY, HOUDINI_LOG_TEXT, ...) \ + do \ + { \ + UE_LOG( LogHoudiniEngine##LONG_NAME, VERBOSITY, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ); \ + } \ + while ( 0 ) + +// --------------------------------------------------------- +// PCG Debug Logging +// --------------------------------------------------------- + +extern bool HOUDINIENGINERUNTIME_API IsHoudiniPCGLoggingEnabled(); + +#define HOUDINI_ENGINE_DEBUG_PCG (1) +#if defined(HOUDINI_ENGINE_LOGGING) && defined(HOUDINI_ENGINE_DEBUG_PCG) + DECLARE_LOG_CATEGORY_EXTERN(LogHoudiniEnginePCG, Log, All); +#define HOUDINI_PCG_DEFINE_LOG_CATEGORY() \ + DEFINE_LOG_CATEGORY(LogHoudiniEnginePCG); +#define HOUDINI_PCG_MESSAGE( HOUDINI_LOG_TEXT, ... ) \ + if (IsHoudiniPCGLoggingEnabled())\ + {\ + HOUDINI_DEBUG_EXPAND_UE_LOG(PCG, Log, HOUDINI_LOG_TEXT, ##__VA_ARGS__);\ + } +#define HOUDINI_PCG_FATAL( HOUDINI_LOG_TEXT, ... ) \ + if (IsHoudiniPCGLoggingEnabled())\ + {\ + HOUDINI_DEBUG_EXPAND_UE_LOG(PCG, Fatal, HOUDINI_LOG_TEXT, ##__VA_ARGS__);\ + } +#define HOUDINI_PCG_ERROR( HOUDINI_LOG_TEXT, ... ) \ + if (IsHoudiniPCGLoggingEnabled())\ + {\ + HOUDINI_DEBUG_EXPAND_UE_LOG(PCG, Error, HOUDINI_LOG_TEXT, ##__VA_ARGS__);\ + } +#define HOUDINI_PCG_WARNING( HOUDINI_LOG_TEXT, ... ) \ + if (IsHoudiniPCGLoggingEnabled())\ + {\ + HOUDINI_DEBUG_EXPAND_UE_LOG(PCG, Warning, HOUDINI_LOG_TEXT, ##__VA_ARGS__);\ + } +#else +#define HOUDINI_PCG_DEFINE_LOG_CATEGORY() +#define HOUDINI_PCG_MESSAGE( HOUDINI_LOG_TEXT, ... ) +#define HOUDINI_PCG_FATAL( HOUDINI_LOG_TEXT, ... ) +#define HOUDINI_PCG_ERROR( HOUDINI_LOG_TEXT, ... ) +#define HOUDINI_PCG_WARNING( HOUDINI_LOG_TEXT, ... ) +#endif + +// --------------------------------------------------------- +// Blueprint Debug Logging +// --------------------------------------------------------- +// Set HOUDINI_ENGINE_DEBUG_BP=1 to enable Blueprint logging +#if defined(HOUDINI_ENGINE_LOGGING) && defined(HOUDINI_ENGINE_DEBUG_BP) + DECLARE_LOG_CATEGORY_EXTERN(LogHoudiniEngineBlueprint, Log, All); + #define HOUDINI_BP_DEFINE_LOG_CATEGORY() \ + DEFINE_LOG_CATEGORY(LogHoudiniEngineBlueprint); + #define HOUDINI_BP_MESSAGE( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( Blueprint, Log, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + #define HOUDINI_BP_FATAL( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( Blueprint, Fatal, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + #define HOUDINI_BP_ERROR( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( Blueprint, Error, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + #define HOUDINI_BP_WARNING( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( Blueprint, Warning, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) +#else + #define HOUDINI_BP_DEFINE_LOG_CATEGORY() + #define HOUDINI_BP_MESSAGE( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_BP_FATAL( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_BP_ERROR( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_BP_WARNING( HOUDINI_LOG_TEXT, ... ) +#endif + + +// --------------------------------------------------------- +// PDG Debug Logging +// --------------------------------------------------------- +// Set HOUDINI_ENGINE_DEBUG_PDG=1 to enable PDG logging +#if defined(HOUDINI_ENGINE_LOGGING) && defined(HOUDINI_ENGINE_DEBUG_PDG) + DECLARE_LOG_CATEGORY_EXTERN(LogHoudiniEnginePDG, Log, All); + #define HOUDINI_PDG_DEFINE_LOG_CATEGORY() \ + DEFINE_LOG_CATEGORY(LogHoudiniEnginePDG); + #define HOUDINI_PDG_MESSAGE( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( PDG, Log, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + #define HOUDINI_PDG_FATAL( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( PDG, Fatal, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + #define HOUDINI_PDG_ERROR( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( PDG, Error, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + #define HOUDINI_PDG_WARNING( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( PDG, Warning, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) +#else + #define HOUDINI_PDG_DEFINE_LOG_CATEGORY() + #define HOUDINI_PDG_MESSAGE( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_PDG_FATAL( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_PDG_ERROR( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_PDG_WARNING( HOUDINI_LOG_TEXT, ... ) +#endif + + +// --------------------------------------------------------- +// Landscape Debug Logging +// --------------------------------------------------------- +// Set HOUDINI_ENGINE_DEBUG_LANDSCAPE=1 to enable PDG logging +#if defined(HOUDINI_ENGINE_LOGGING) && defined(HOUDINI_ENGINE_DEBUG_LANDSCAPE) + DECLARE_LOG_CATEGORY_EXTERN(LogHoudiniEngineLandscape, Log, All); + #define HOUDINI_LANDSCAPE_DEFINE_LOG_CATEGORY() \ + DEFINE_LOG_CATEGORY(LogHoudiniEngineLandscape); + #define HOUDINI_LANDSCAPE_MESSAGE( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( Landscape, Log, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + #define HOUDINI_LANDSCAPE_FATAL( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( Landscape, Fatal, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + #define HOUDINI_LANDSCAPE_ERROR( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( Landscape, Error, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + #define HOUDINI_LANDSCAPE_WARNING( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( Landscape, Warning, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) +#else + #define HOUDINI_LANDSCAPE_DEFINE_LOG_CATEGORY() + #define HOUDINI_LANDSCAPE_MESSAGE( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_LANDSCAPE_FATAL( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_LANDSCAPE_ERROR( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_LANDSCAPE_WARNING( HOUDINI_LOG_TEXT, ... ) +#endif + +// ------------------------------------------------------------------------------------ +// Checks. HOUDINI_CHECK_RETURN can be used like UE's check(), except it outputs a +// message which maybe useful to debug a problem and then returns _RETURN_VALUE +// instead of ending the program. It is not intended for "user error" situations but +// for internal code errors, ie. assumptions we believe are true. +// ------------------------------------------------------------------------------------- +#define HOUDINI_CHECK_RETURN_STRING(x) #x +#define HOUDINI_CHECK_RETURN_STRINGFY(x) HOUDINI_CHECK_RETURN_STRING(x) + +#define HOUDINI_CHECK_RETURN(_EXPRESSION_TO_CHECK, _RETURN_VALUE) \ + if (!(_EXPRESSION_TO_CHECK)) {\ + HOUDINI_LOG_ERROR( TEXT("Internal Error: " __FILE__ ":" HOUDINI_CHECK_RETURN_STRINGFY(__LINE__) " " #_EXPRESSION_TO_CHECK));\ + return _RETURN_VALUE;\ + } + +#define HOUDINI_CHECK(_EXPRESSION_TO_CHECK) HOUDINI_CHECK_RETURN(_EXPRESSION_TO_CHECK, ) + +// --------------------------------------------------------- +// Baking Debug Logging +// --------------------------------------------------------- +// Set HOUDINI_ENGINE_DEBUG_BAKING=1 to enable PDG logging +#if defined(HOUDINI_ENGINE_LOGGING) && defined(HOUDINI_ENGINE_DEBUG_BAKING) + DECLARE_LOG_CATEGORY_EXTERN(LogHoudiniEngineBaking, Log, All); + #define HOUDINI_BAKING_DEFINE_LOG_CATEGORY() \ + DEFINE_LOG_CATEGORY(LogHoudiniEngineBaking); + #define HOUDINI_BAKING_MESSAGE( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( Landscape, Log, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + #define HOUDINI_BAKING_FATAL( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( Landscape, Fatal, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + #define HOUDINI_BAKING_ERROR( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( Landscape, Error, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) + #define HOUDINI_BAKING_WARNING( HOUDINI_LOG_TEXT, ... ) \ + HOUDINI_DEBUG_EXPAND_UE_LOG( Landscape, Warning, HOUDINI_LOG_TEXT, ##__VA_ARGS__ ) +#else + #define HOUDINI_BAKING_DEFINE_LOG_CATEGORY() + #define HOUDINI_BAKING_MESSAGE( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_BAKING_FATAL( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_BAKING_ERROR( HOUDINI_LOG_TEXT, ... ) + #define HOUDINI_BAKING_WARNING( HOUDINI_LOG_TEXT, ... ) +#endif + + +// HAPI_Common +enum HAPI_UNREAL_NodeType +{ + HAPI_UNREAL_NODETYPE_ANY = -1, + HAPU_UNREAL_NODETYPE_NONE = 0 +}; + +enum HAPI_UNREAL_NodeFlags +{ + HAPI_UNREAL_NODEFLAGS_ANY = -1, + HAPI_UNREAL_NODEFLAGS_NONE = 0 +}; + +// Default cook/bake folder +#define HAPI_UNREAL_DEFAULT_BAKE_FOLDER TEXT("/Game/HoudiniEngine/Bake"); +#define HAPI_UNREAL_DEFAULT_TEMP_COOK_FOLDER TEXT("/Game/HoudiniEngine/Temp"); + +// Various variable names used to store meta information in generated packages. +// More in HoudiniEnginePrivatePCH.h +#define HAPI_UNREAL_PACKAGE_META_TEMP_GUID TEXT( "HoudiniPackageTempGUID" ) +#define HAPI_UNREAL_PACKAGE_META_COMPONENT_GUID TEXT( "HoudiniComponentGUID" ) + +// Default PDG Filters +#define HAPI_UNREAL_PDG_DEFAULT_TOP_FILTER "HE_"; +#define HAPI_UNREAL_PDG_DEFAULT_TOP_OUTPUT_FILTER "HE_OUT_"; + +// Struct to enable global silent flag - this will force dialogs to not show up. +struct FHoudiniScopedGlobalSilence +{ + FHoudiniScopedGlobalSilence() + { + bGlobalSilent = GIsSilent; + GIsSilent = true; + } + + ~FHoudiniScopedGlobalSilence() + { + GIsSilent = bGlobalSilent; + } + + bool bGlobalSilent; +}; + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniEngineRuntimeUtils.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniEngineRuntimeUtils.h" + +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniRuntimeSettings.h" + +#include "EngineUtils.h" +#include "Engine/EngineTypes.h" +#include "Landscape.h" +#include "LandscapeProxy.h" +#include "LandscapeStreamingProxy.h" +#include "LandscapeInfo.h" +#include "LandscapeSplinesComponent.h" +#include "Misc/Paths.h" +#include "Components/StaticMeshComponent.h" +#include "Runtime/Launch/Resources/Version.h" + +#if WITH_EDITOR + #include "Editor.h" + #include "Kismet2/BlueprintEditorUtils.h" + #include "SSubobjectBlueprintEditor.h" + #include "PropertyEditorModule.h" +#if ENGINE_MAJOR_VERSION < 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0) + #include "LandscapeSplineControlPoint.h" +#endif +#endif + +FString +FHoudiniEngineRuntimeUtils::GetLibHAPIName() +{ + static const FString LibHAPIName = + +#if PLATFORM_WINDOWS + HAPI_LIB_OBJECT_WINDOWS; +#elif PLATFORM_MAC + HAPI_LIB_OBJECT_MAC; +#elif PLATFORM_LINUX + HAPI_LIB_OBJECT_LINUX; +#else + TEXT(""); +#endif + + return LibHAPIName; +} + + +bool +FHoudiniEngineRuntimeUtils::CheckCustomHoudiniLocation(const FString& InCustomHoudiniLocationPath) +{ + const FString LibHAPIName = GetLibHAPIName(); + const FString LibHAPICustomPath = FString::Printf(TEXT("%s/%s"), *InCustomHoudiniLocationPath, *LibHAPIName); + + // If path does not point to libHAPI location, we need to let user know. + if (!FPaths::FileExists(LibHAPICustomPath)) + { + const FString MessageString = FString::Printf( + TEXT("%s was not found in %s"), *LibHAPIName, *InCustomHoudiniLocationPath); + + FPlatformMisc::MessageBoxExt( + EAppMsgType::Ok, *MessageString, + TEXT("Invalid Custom Location Specified, resetting.")); + + return false; + } + + return true; +} + + +void +FHoudiniEngineRuntimeUtils::GetBoundingBoxesFromActors(const TArray InActors, TArray& OutBBoxes) +{ + OutBBoxes.Empty(); + + for (auto CurrentActor : InActors) + { + if (!IsValid(CurrentActor)) + continue; + + OutBBoxes.Add(CurrentActor->GetComponentsBoundingBox(true, true)); + } +} + + +bool +FHoudiniEngineRuntimeUtils::FindActorsOfClassInBounds(UWorld* World, TSubclassOf ActorType, const TArray& BBoxes, const TArray* ExcludeActors, TArray& OutActors) +{ + if (!IsValid(World)) + return false; + + OutActors.Empty(); + for (TActorIterator ActorItr(World); ActorItr; ++ActorItr) + { + AActor* CurrentActor = *ActorItr; + if (!IsValid(CurrentActor)) + continue; + + if (!CurrentActor->GetClass()->IsChildOf(ActorType.Get())) + continue; + + if (ExcludeActors && ExcludeActors->Contains(CurrentActor)) + continue; + + // Special case + // Ignore the SkySpheres? + FString ClassName = CurrentActor->GetClass() ? CurrentActor->GetClass()->GetName() : FString(); + if (ClassName.Contains("BP_Sky_Sphere")) + continue; + + FBox ActorBounds = CurrentActor->GetComponentsBoundingBox(true); + for (auto InBounds : BBoxes) + { + // Check if both actor's bounds intersects + if (!ActorBounds.Intersect(InBounds)) + continue; + + OutActors.Add(CurrentActor); + break; + } + } + + return true; +} + +bool +FHoudiniEngineRuntimeUtils::SafeDeleteSingleObject(UObject* const InObjectToDelete, UPackage*& OutPackage, bool& bOutPackageIsInMemoryOnly) +{ + bool bDeleted = false; + OutPackage = nullptr; + bOutPackageIsInMemoryOnly = false; + + if (!IsValid(InObjectToDelete)) + return false; + + // Don't try to delete the object if it has references (we do this here to avoid the FMessageDialog in DeleteSingleObject + bool bIsReferenced = false; + bool bIsReferencedByUndo = false; + if (!GatherObjectReferencersForDeletion(InObjectToDelete, bIsReferenced, bIsReferencedByUndo)) + return false; + + if (bIsReferenced) + { + HOUDINI_LOG_WARNING(TEXT("[FHoudiniEngineRuntimeUtils::SafeDeleteSingleObject] Not deleting %s: there are still references to it."), *InObjectToDelete->GetFullName()); + } + else + { + // Even though we already checked for references, we still let DeleteSingleObject check for references, since + // we want that code path where it'll clean up in-memory references (undo buffer/transactions) + const bool bCheckForReferences = true; + if (DeleteSingleObject(InObjectToDelete, bCheckForReferences)) + { + bDeleted = true; + + OutPackage = InObjectToDelete->GetOutermost(); + + FString PackageFilename; + if (!IsValid(OutPackage) || !FPackageName::DoesPackageExist(OutPackage->GetName(), &PackageFilename)) + { + // Package is in memory only, we don't have call CleanUpAfterSuccessfulDelete on it, just do garbage + // collection to pick up the stale package + bOutPackageIsInMemoryOnly = true; + } + else + { + // There is an on-disk package that is now potentially empty, CleanUpAfterSuccessfulDelete must be + // called on this. Since CleanUpAfterSuccessfulDelete does garbage collection, we return the Package + // as part of this function so that the caller can collect all Packages and do one call to + // CleanUpAfterSuccessfulDelete with an array + } + } + } + + return bDeleted; +} + +int32 +FHoudiniEngineRuntimeUtils::SafeDeleteObjects(TArray& InObjectsToDelete, TArray* OutObjectsNotDeleted) +{ + int32 NumDeleted = 0; + bool bGarbageCollectionRequired = false; + TSet PackagesToCleanUp; + TSet ProcessedObjects; + while (InObjectsToDelete.Num() > 0) + { + UObject* const ObjectToDelete = InObjectsToDelete.Pop(); + + if (ProcessedObjects.Contains(ObjectToDelete)) + continue; + + ProcessedObjects.Add(ObjectToDelete); + + if (!IsValid(ObjectToDelete)) + continue; + + UPackage* Package = nullptr; + bool bInMemoryPackageOnly = false; + if (SafeDeleteSingleObject(ObjectToDelete, Package, bInMemoryPackageOnly)) + { + NumDeleted++; + if (bInMemoryPackageOnly) + { + // Packages that are in-memory only are cleaned up by garbage collection + if (!bGarbageCollectionRequired) + bGarbageCollectionRequired = true; + } + else + { + // Clean up potentially empty packages in one call to CleanupAfterSuccessfulDelete at the end + PackagesToCleanUp.Add(Package); + } + } + else if (OutObjectsNotDeleted) + { + OutObjectsNotDeleted->Add(ObjectToDelete); + } + } + + // CleanupAfterSuccessfulDelete calls CollectGarbage, so don't call it here if we have PackagesToCleanUp + if (bGarbageCollectionRequired && PackagesToCleanUp.Num() <= 0) + CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); + + if (PackagesToCleanUp.Num() > 0) + CleanupAfterSuccessfulDelete(PackagesToCleanUp.Array()); + + return NumDeleted; +} + + +#if WITH_EDITOR +int32 +FHoudiniEngineRuntimeUtils::CopyComponentProperties(UActorComponent* SourceComponent, UActorComponent* TargetComponent, const EditorUtilities::FCopyOptions& Options) +{ + UClass* ComponentClass = SourceComponent->GetClass(); + check( ComponentClass == TargetComponent->GetClass() ); + + const bool bIsPreviewing = ( Options.Flags & EditorUtilities::ECopyOptions::PreviewOnly ) != 0; + int32 CopiedPropertyCount = 0; + bool bTransformChanged = false; + + // Build a list of matching component archetype instances for propagation (if requested) + TArray ComponentArchetypeInstances; + if( Options.Flags & EditorUtilities::ECopyOptions::PropagateChangesToArchetypeInstances ) + { + TArray Instances; + TargetComponent->GetArchetypeInstances(Instances); + for(UObject* ObjInstance : Instances) + { + UActorComponent* ComponentInstance = Cast(ObjInstance); + if (ComponentInstance && ComponentInstance != SourceComponent && ComponentInstance != TargetComponent) + { + ComponentArchetypeInstances.Add(ComponentInstance); + } + } + } + + TSet SourceUCSModifiedProperties; + SourceComponent->GetUCSModifiedProperties(SourceUCSModifiedProperties); + + TArray ComponentInstancesToReregister; + + // Copy component properties + for( FProperty* Property = ComponentClass->PropertyLink; Property != nullptr; Property = Property->PropertyLinkNext ) + { + const bool bIsTransient = !!( Property->PropertyFlags & CPF_Transient ); + const bool bIsIdentical = Property->Identical_InContainer( SourceComponent, TargetComponent ); + const bool bIsComponent = !!( Property->PropertyFlags & ( CPF_InstancedReference | CPF_ContainsInstancedReference ) ); + const bool bIsTransform = + Property->GetFName() == USceneComponent::GetRelativeScale3DPropertyName() || + Property->GetFName() == USceneComponent::GetRelativeLocationPropertyName() || + Property->GetFName() == USceneComponent::GetRelativeRotationPropertyName(); + + // auto SourceComponentIsRoot = [&]() + // { + // USceneComponent* RootComponent = SourceActor->GetRootComponent(); + // if (SourceComponent == RootComponent) + // { + // return true; + // } + // else if (RootComponent == nullptr && bSourceActorIsBPCDO) + // { + // // If we're dealing with a BP CDO as source, then look at the target for whether this is the root component + // return (TargetComponent == TargetActor->GetRootComponent()); + // } + // return false; + // }; + + TSet ModifiedObjects; + // if( !bIsTransient && !bIsIdentical && !bIsComponent && !SourceUCSModifiedProperties.Contains(Property) + // && ( !bIsTransform || (!bSourceActorIsCDO && !bTargetActorIsCDO) || !SourceComponentIsRoot() ) ) + if( !bIsTransient && !bIsIdentical && !bIsComponent && !SourceUCSModifiedProperties.Contains(Property) + && ( !bIsTransform )) + { + const bool bIsSafeToCopy = (!(Options.Flags & EditorUtilities::ECopyOptions::OnlyCopyEditOrInterpProperties) || (Property->HasAnyPropertyFlags(CPF_Edit | CPF_Interp))) + && (!(Options.Flags & EditorUtilities::ECopyOptions::SkipInstanceOnlyProperties) || (!Property->HasAllPropertyFlags(CPF_DisableEditOnTemplate))); + if( bIsSafeToCopy ) + { + // if (!Options.CanCopyProperty(*Property, *SourceActor)) + // { + // continue; + // } + if (!Options.CanCopyProperty(*Property, *SourceComponent)) + { + continue; + } + + if( !bIsPreviewing ) + { + if( !ModifiedObjects.Contains(TargetComponent) ) + { + TargetComponent->SetFlags(RF_Transactional); + TargetComponent->Modify(); + ModifiedObjects.Add(TargetComponent); + } + + if( Options.Flags & EditorUtilities::ECopyOptions::CallPostEditChangeProperty ) + { + TargetComponent->PreEditChange( Property ); + } + + // Determine which component archetype instances match the current property value of the target component (before it gets changed). We only want to propagate the change to those instances. + TArray ComponentArchetypeInstancesToChange; + if( Options.Flags & EditorUtilities::ECopyOptions::PropagateChangesToArchetypeInstances ) + { + for (UActorComponent* ComponentArchetypeInstance : ComponentArchetypeInstances) + { + if( ComponentArchetypeInstance != nullptr && Property->Identical_InContainer( ComponentArchetypeInstance, TargetComponent ) ) + { + bool bAdd = true; + // We also need to double check that either the direct archetype of the target is also identical + if (ComponentArchetypeInstance->GetArchetype() != TargetComponent) + { + UActorComponent* CheckComponent = CastChecked(ComponentArchetypeInstance->GetArchetype()); + while (CheckComponent != ComponentArchetypeInstance) + { + if (!Property->Identical_InContainer( CheckComponent, TargetComponent )) + { + bAdd = false; + break; + } + CheckComponent = CastChecked(CheckComponent->GetArchetype()); + } + } + + if (bAdd) + { + ComponentArchetypeInstancesToChange.Add( ComponentArchetypeInstance ); + } + } + } + } + + EditorUtilities::CopySingleProperty(SourceComponent, TargetComponent, Property); + + if( Options.Flags & EditorUtilities::ECopyOptions::CallPostEditChangeProperty ) + { + FPropertyChangedEvent PropertyChangedEvent( Property ); + TargetComponent->PostEditChangeProperty( PropertyChangedEvent ); + } + + if( Options.Flags & EditorUtilities::ECopyOptions::PropagateChangesToArchetypeInstances ) + { + for( int32 InstanceIndex = 0; InstanceIndex < ComponentArchetypeInstancesToChange.Num(); ++InstanceIndex ) + { + UActorComponent* ComponentArchetypeInstance = ComponentArchetypeInstancesToChange[InstanceIndex]; + if( ComponentArchetypeInstance != nullptr ) + { + if( !ModifiedObjects.Contains(ComponentArchetypeInstance) ) + { + // Ensure that this instance will be included in any undo/redo operations, and record it into the transaction buffer. + // Note: We don't do this for components that originate from script, because they will be re-instanced from the template after an undo, so there is no need to record them. + if (!ComponentArchetypeInstance->IsCreatedByConstructionScript()) + { + ComponentArchetypeInstance->SetFlags(RF_Transactional); + ComponentArchetypeInstance->Modify(); + ModifiedObjects.Add(ComponentArchetypeInstance); + } + + // We must also modify the owner, because we'll need script components to be reconstructed as part of an undo operation. + AActor* Owner = ComponentArchetypeInstance->GetOwner(); + if( Owner != nullptr && !ModifiedObjects.Contains(Owner)) + { + Owner->Modify(); + ModifiedObjects.Add(Owner); + } + } + + if (ComponentArchetypeInstance->IsRegistered()) + { + ComponentArchetypeInstance->UnregisterComponent(); + ComponentInstancesToReregister.Add(ComponentArchetypeInstance); + } + + EditorUtilities::CopySingleProperty( TargetComponent, ComponentArchetypeInstance, Property ); + } + } + } + } + + ++CopiedPropertyCount; + + if( bIsTransform ) + { + bTransformChanged = true; + } + } + } + } + + for (UActorComponent* ModifiedComponentInstance : ComponentInstancesToReregister) + { + if (ModifiedComponentInstance->GetOwner()) + ModifiedComponentInstance->RegisterComponent(); + } + + return CopiedPropertyCount; +} +#endif + + +#if WITH_EDITOR +FBlueprintEditor* +FHoudiniEngineRuntimeUtils::GetBlueprintEditor(const UObject* InObject) +{ + if (!IsValid(InObject)) + return nullptr; + + UObject* Outer = InObject->GetOuter(); + if (!IsValid(Outer)) + return nullptr; + + UBlueprintGeneratedClass* OuterBPClass = Cast(Outer->GetClass()); + + if (!OuterBPClass) + return nullptr; + + UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); + return static_cast(AssetEditorSubsystem->FindEditorForAsset(OuterBPClass->ClassGeneratedBy, false)); +} +#endif + + +#if WITH_EDITOR +void +FHoudiniEngineRuntimeUtils::MarkBlueprintAsStructurallyModified(UActorComponent* ComponentTemplate) +{ + if (!ComponentTemplate) + return; + + UBlueprintGeneratedClass* BPGC = Cast(ComponentTemplate->GetOuter()); + if (!BPGC) + return; + + UBlueprint* Blueprint = Cast(BPGC->ClassGeneratedBy); + if (!Blueprint) + return; + + Blueprint->Modify(); + + UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); + FBlueprintEditor* BlueprintEditor = static_cast(AssetEditorSubsystem->FindEditorForAsset(Blueprint, false)); + check(BlueprintEditor); + + USimpleConstructionScript* SCS = Blueprint->SimpleConstructionScript; + if (SCS) + { + SCS->SaveToTransactionBuffer(); + } + + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint); + BlueprintEditor->GetSubobjectEditor()->UpdateTree(true); +} +#endif + +#if WITH_EDITOR +void +FHoudiniEngineRuntimeUtils::MarkBlueprintAsModified(UActorComponent* ComponentTemplate) +{ + if (!ComponentTemplate) + return; + + UBlueprintGeneratedClass* BPGC = Cast(ComponentTemplate->GetOuter()); + if (!BPGC) + return; + + UBlueprint* Blueprint = Cast(BPGC->ClassGeneratedBy); + if (!Blueprint) + return; + + Blueprint->Modify(); + + FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint); +} +#endif + +FTransform +FHoudiniEngineRuntimeUtils::CalculateHoudiniLandscapeTransform(ALandscapeProxy* LandscapeProxy) +{ + // This function calculates the transform of the geometry node which contains the height field SOPs + // that represent the Unreal landscape. + // + // The transform contains the rotation and translation, but not the scale as this is applied to the + // individual volume nodes. Trying to apply the scale here causes off issues in Houdini, it really + // needs to be specified on the height field. + // + // Note that the location of the Unreal transform is in the center of the landscape + // whereas its in the center in Houdini. + // + // In addition, this function takes into account that not all landscape streaming proxies are loaded + // at one time in World Partition. If a subset of the landscape is loaded then only that part of the + // landscape is loaded into Houdini. + // + // For example if there are 4x4 streaming proxies, and only the center 2x2 proxies + // are loaded then a height field representing the 2x2 proxies loaded. If one of those 2x2 proxies is not + // loaded the landscape of size 2x2 is still created, but the height/masks are set to default values. + +#if WITH_EDITOR + FTransform OutTransform = LandscapeProxy->GetTransform(); + + // The final landscape transform that should go into Houdini consist of the following two components: + // - Shared Landscape Transform + // - Extents of all the loaded landscape components + + // The houdini transform will always be in the center of the currently loaded landscape components. + FIntRect Extent; + Extent.Min.X = INT32_MAX; + Extent.Min.Y = INT32_MAX; + Extent.Max.X = INT32_MIN; + Extent.Max.Y = INT32_MIN; + + ALandscape* Landscape = LandscapeProxy->GetLandscapeActor(); + if (LandscapeProxy == Landscape) + { + // The proxy is a landscape actor, so we have to use the whole landscape extent + ULandscapeInfo* LandscapeInfo = LandscapeProxy->GetLandscapeInfo(); + if (IsValid(LandscapeInfo)) + { + //LandscapeInfo->GetLandscapeExtent(Extent.Min.X, Extent.Min.Y, Extent.Max.X, Extent.Max.Y); + LandscapeInfo->ForAllLandscapeComponents([&Extent](ULandscapeComponent* LandscapeComponent) + { + LandscapeComponent->GetComponentExtent(Extent.Min.X, Extent.Min.Y, Extent.Max.X, Extent.Max.Y); + }); + } + + // In World Partition the landscape may not be loaded so we must correct the transform offset. + FVector Offset = OutTransform.GetLocation(); + Offset.X += OutTransform.GetScale3D().X * Extent.Min.X; + Offset.Y += OutTransform.GetScale3D().Y * Extent.Min.Y; + OutTransform.SetLocation(Offset); + } + else + { + // We only want to get the size for this landscape proxy. + // Get the extents via all the components, not by calling GetLandscapeExtent or we'll end up with the size of ALL the streaming proxies. + for (const ULandscapeComponent* Comp : LandscapeProxy->LandscapeComponents) + { + Comp->GetComponentExtent(Extent.Min.X, Extent.Min.Y, Extent.Max.X, Extent.Max.Y); + } + } + + // HF are centered, Landscape aren't + // Calculate the offset needed to properly represent the Landscape in H + FVector3d CenterOffset( + (double)(Extent.Max.X - Extent.Min.X) / 2.0, + (double)(Extent.Max.Y - Extent.Min.Y) / 2.0, + 1.0); + + // Extract the Landscape rotation/scale and apply them to the offset + FTransform TransformWithRot = FTransform::Identity; + TransformWithRot.CopyRotation(OutTransform); + FVector3d LandscapeScale = OutTransform.GetScale3D(); + + const FVector RotScaledOffset = TransformWithRot.TransformPosition(FVector(CenterOffset.X * LandscapeScale.X, CenterOffset.Y * LandscapeScale.Y, 0)); + + // Apply the rotated offset to the transform's position + FVector3d Loc = OutTransform.GetLocation() + RotScaledOffset; + OutTransform.SetLocation(Loc); + OutTransform.SetScale3D(FVector3d::One()); + return OutTransform; +#else + return FTransform::Identity; +#endif +} + + +#if WITH_EDITOR + +// Centralized call to set actor label (changing Actor's implementation was too risky) +bool FHoudiniEngineRuntimeUtils::SetActorLabel(AActor* Actor, const FString& ActorLabel) +{ + // Clean up the incoming string a bit + FString NewActorLabel = ActorLabel.TrimStartAndEnd(); + if (NewActorLabel == Actor->GetActorLabel(/*bCreateIfNone*/false)) + { + return false; + } + Actor->SetActorLabel(NewActorLabel); + return true; +} + +void +FHoudiniEngineRuntimeUtils::DoPostEditChangeProperty(UObject* Obj, FName PropertyName) +{ + FPropertyChangedEvent Evt(FindFieldChecked(Obj->GetClass(), PropertyName)); + Obj->PostEditChangeProperty(Evt); +} + +void FHoudiniEngineRuntimeUtils::DoPostEditChangeProperty(UObject* Obj, FProperty* Property) +{ + FPropertyChangedEvent Evt(Property); + Obj->PostEditChangeProperty(Evt); +} + +void FHoudiniEngineRuntimeUtils::PropagateObjectDeltaChangeToArchetypeInstance(UObject* InObject, const FTransactionObjectDeltaChange& DeltaChange) +{ + if (!InObject) + return; + if (!InObject->HasAnyFlags(RF_ArchetypeObject)) + return; + + // Iterate over the modified properties and propagate value changed to all archetype instances + TArray ArchetypeInstances; + InObject->GetArchetypeInstances(ArchetypeInstances); + for (UObject* Instance : ArchetypeInstances) + { + UE_LOG(LogTemp, Log, TEXT("[void FHoudiniEngineRuntimeUtils::PropagateTransactionToArchetypeInstance] Found Archetype instance: %s"), *(Instance->GetPathName())); + for (FName PropertyName : DeltaChange.ChangedProperties) + { + UE_LOG(LogTemp, Log, TEXT("[void FHoudiniEngineRuntimeUtils::PropagateTransactionToArchetypeInstance] Changed property: %s"), *(PropertyName.ToString())); + // FComponentEditorUtils::ApplyDefaultValueChange(SceneComp, SceneComp->GetRelativeLocation_DirectMutable(), OldRelativeLocation, SelectedTemplate->GetRelativeLocation()); + } + } +} + +void FHoudiniEngineRuntimeUtils::ForAllArchetypeInstances(UObject* InTemplateObj, TFunctionRef Operation) +{ + if (!InTemplateObj) + return; + if (!InTemplateObj->HasAnyFlags(RF_ArchetypeObject|RF_DefaultSubObject)) + return; + + TArray Instances; + InTemplateObj->GetArchetypeInstances(Instances); + + for(UObject* Instance : Instances) + { + Operation(Instance); + } +} + +#endif + + +bool +FHoudiniEngineRuntimeUtils::SetDefaultCollisionFlag(UObject* InObject, bool bUseDefaultCollision) +{ + if(UStaticMeshComponent* SMC = Cast(InObject)) + { + SMC->bUseDefaultCollision = bUseDefaultCollision; + return true; + } + return false; +} + + +FHoudiniStaticMeshGenerationProperties +FHoudiniEngineRuntimeUtils::GetDefaultStaticMeshGenerationProperties() +{ + FHoudiniStaticMeshGenerationProperties SMGP; + + const UHoudiniRuntimeSettings * HoudiniRuntimeSettings = GetDefault(); + if (HoudiniRuntimeSettings) + { + SMGP.bGeneratedDoubleSidedGeometry = HoudiniRuntimeSettings->bDoubleSidedGeometry; + SMGP.GeneratedPhysMaterial = HoudiniRuntimeSettings->PhysMaterial; + SMGP.DefaultBodyInstance = HoudiniRuntimeSettings->DefaultBodyInstance; + SMGP.GeneratedCollisionTraceFlag = HoudiniRuntimeSettings->CollisionTraceFlag; + //SMGP.GeneratedLpvBiasMultiplier = HoudiniRuntimeSettings->LpvBiasMultiplier; + SMGP.GeneratedLightMapResolution = HoudiniRuntimeSettings->LightMapResolution; + SMGP.GeneratedLightMapCoordinateIndex = HoudiniRuntimeSettings->LightMapCoordinateIndex; + SMGP.bGeneratedUseMaximumStreamingTexelRatio = HoudiniRuntimeSettings->bUseMaximumStreamingTexelRatio; + SMGP.GeneratedStreamingDistanceMultiplier = HoudiniRuntimeSettings->StreamingDistanceMultiplier; + SMGP.GeneratedWalkableSlopeOverride = HoudiniRuntimeSettings->WalkableSlopeOverride; + SMGP.GeneratedFoliageDefaultSettings = HoudiniRuntimeSettings->FoliageDefaultSettings; + SMGP.GeneratedAssetUserData = HoudiniRuntimeSettings->AssetUserData; + } + + return SMGP; +} + +FMeshBuildSettings +FHoudiniEngineRuntimeUtils::GetDefaultMeshBuildSettings() +{ + FMeshBuildSettings DefaultBuildSettings; + + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + if(HoudiniRuntimeSettings) + { + DefaultBuildSettings.bRemoveDegenerates = HoudiniRuntimeSettings->bRemoveDegenerates; + DefaultBuildSettings.bUseMikkTSpace = HoudiniRuntimeSettings->bUseMikkTSpace; + //DefaultBuildSettings.bBuildAdjacencyBuffer = HoudiniRuntimeSettings->bBuildAdjacencyBuffer; + DefaultBuildSettings.MinLightmapResolution = HoudiniRuntimeSettings->MinLightmapResolution; + DefaultBuildSettings.bUseFullPrecisionUVs = HoudiniRuntimeSettings->bUseFullPrecisionUVs; + DefaultBuildSettings.SrcLightmapIndex = HoudiniRuntimeSettings->SrcLightmapIndex; + DefaultBuildSettings.DstLightmapIndex = HoudiniRuntimeSettings->DstLightmapIndex; + + DefaultBuildSettings.bComputeWeightedNormals = HoudiniRuntimeSettings->bComputeWeightedNormals; + DefaultBuildSettings.bBuildReversedIndexBuffer = HoudiniRuntimeSettings->bBuildReversedIndexBuffer; + DefaultBuildSettings.bUseHighPrecisionTangentBasis = HoudiniRuntimeSettings->bUseHighPrecisionTangentBasis; + DefaultBuildSettings.bGenerateDistanceFieldAsIfTwoSided = HoudiniRuntimeSettings->bGenerateDistanceFieldAsIfTwoSided; + DefaultBuildSettings.bSupportFaceRemap = HoudiniRuntimeSettings->bSupportFaceRemap; + DefaultBuildSettings.DistanceFieldResolutionScale = HoudiniRuntimeSettings->DistanceFieldResolutionScale; + + // Recomputing normals. + EHoudiniRuntimeSettingsRecomputeFlag RecomputeNormalFlag = (EHoudiniRuntimeSettingsRecomputeFlag)HoudiniRuntimeSettings->RecomputeNormalsFlag; + switch (RecomputeNormalFlag) + { + case HRSRF_Never: + { + DefaultBuildSettings.bRecomputeNormals = false; + break; + } + + case HRSRF_Always: + case HRSRF_OnlyIfMissing: + default: + { + DefaultBuildSettings.bRecomputeNormals = true; + break; + } + } + + // Recomputing tangents. + EHoudiniRuntimeSettingsRecomputeFlag RecomputeTangentFlag = (EHoudiniRuntimeSettingsRecomputeFlag)HoudiniRuntimeSettings->RecomputeTangentsFlag; + switch (RecomputeTangentFlag) + { + case HRSRF_Never: + { + DefaultBuildSettings.bRecomputeTangents = false; + break; + } + + case HRSRF_Always: + case HRSRF_OnlyIfMissing: + default: + { + DefaultBuildSettings.bRecomputeTangents = true; + break; + } + } + + // Lightmap UV generation. + EHoudiniRuntimeSettingsRecomputeFlag GenerateLightmapUVsFlag = (EHoudiniRuntimeSettingsRecomputeFlag)HoudiniRuntimeSettings->GenerateLightmapUVsFlag; + switch (GenerateLightmapUVsFlag) + { + case HRSRF_Never: + { + DefaultBuildSettings.bGenerateLightmapUVs = false; + break; + } + + case HRSRF_Always: + case HRSRF_OnlyIfMissing: + default: + { + DefaultBuildSettings.bGenerateLightmapUVs = true; + break; + } + } + } + + return DefaultBuildSettings; +} + +bool +FHoudiniEngineRuntimeUtils::GetLandscapeSplinesControlPointsAndSegments( + ULandscapeSplinesComponent* const InSplinesComponent, + TArray>* OutControlPoints, + TArray>* OutSegments) +{ + if (!IsValid(InSplinesComponent)) + return false; + if (!OutControlPoints && !OutSegments) + return false; +#if ENGINE_MAJOR_VERSION < 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0) +#if !WITH_EDITOR + return false; +#else + TSet> SegmentsSet; + const bool bSkipSegments = (OutSegments == nullptr); + InSplinesComponent->ForEachControlPoint([OutControlPoints, bSkipSegments, &SegmentsSet](ULandscapeSplineControlPoint* const InControlPoint) + { + if (OutControlPoints) + OutControlPoints->Add(InControlPoint); + if (bSkipSegments || !IsValid(InControlPoint)) + return; + for (const FLandscapeSplineConnection& Connection : InControlPoint->ConnectedSegments) + { + if (!IsValid(Connection.Segment)) + continue; + SegmentsSet.Add(Connection.Segment); + } + }); + if (OutSegments) + (*OutSegments) = SegmentsSet.Array(); +#endif +#else + if (OutControlPoints) + (*OutControlPoints) = InSplinesComponent->GetControlPoints(); + if (OutSegments) + (*OutSegments) = InSplinesComponent->GetSegments(); +#endif + + return true; +} + +bool +FHoudiniEngineRuntimeUtils::GetLandscapeSplinesControlPoints(ULandscapeSplinesComponent* const InSplinesComponent, TArray>& OutControlPoints) +{ + return GetLandscapeSplinesControlPointsAndSegments(InSplinesComponent, &OutControlPoints); +} + +bool +FHoudiniEngineRuntimeUtils::GetLandscapeSplinesSegments(ULandscapeSplinesComponent* const InSplinesComponent, TArray>& OutSegments) +{ + return GetLandscapeSplinesControlPointsAndSegments(InSplinesComponent, nullptr, &OutSegments); +} + +bool +FHoudiniEngineRuntimeUtils::AddOrSetAsInstanceComponent(UActorComponent* InActorComponent) +{ + if (!IsValid(InActorComponent)) + return false; + + AActor* const Owner = InActorComponent->GetOwner(); + if (!IsValid(Owner)) + { + HOUDINI_LOG_WARNING( + TEXT("[FHoudiniEngineRuntimeUtils::AddOrSetAsInstanceComponent] Owner of component '%s' is null / invalid, " + "only setting the CreationMethod to EComponentCreationMethod::Instance."), + *InActorComponent->GetFName().ToString()); + InActorComponent->CreationMethod = EComponentCreationMethod::Instance; + } + else + { + Owner->AddInstanceComponent(InActorComponent); + } + + return true; +} + +void +FHoudiniEngineRuntimeUtils::SetHoudiniHomeEnvironmentVariable() +{ + const UHoudiniRuntimeSettings* HoudiniRuntimeSettings = GetDefault(); + if (!HoudiniRuntimeSettings) + return; + + if (HoudiniRuntimeSettings->CustomHoudiniHomeLocation.Path.IsEmpty()) + return; + + FString CustomHoudiniHomeLocationPath = HoudiniRuntimeSettings->CustomHoudiniHomeLocation.Path; + if (FPaths::IsRelative(CustomHoudiniHomeLocationPath)) + CustomHoudiniHomeLocationPath = FPaths::ConvertRelativePathToFull(CustomHoudiniHomeLocationPath); + + if (!FPaths::DirectoryExists(CustomHoudiniHomeLocationPath)) + return; + + FPlatformMisc::SetEnvironmentVar(TEXT("HOME"), CustomHoudiniHomeLocationPath.GetCharArray().GetData()); +} + +UClass* +FHoudiniEngineRuntimeUtils::GetClassByName(const FString& InName) +{ + if (InName.IsEmpty()) + return nullptr; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + // UE5.1 deprecated ANY_PACKAGE, using a null outer doesn't work so use FindFirstObject instead + return FindFirstObject(*InName, EFindFirstObjectOptions::NativeFirst); +#else + return FindObject(ANY_PACKAGE, *InName); +#endif +} + +void FHoudiniEngineRuntimeUtils::ForceDetailsPanelToUpdate() +{ +#if WITH_EDITOR + FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + PropertyEditorModule.NotifyCustomizationModuleChanged(); +#endif +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniEngineRuntimeUtils.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "EngineUtils.h" +#include "LandscapeInfo.h" +#include "UObject/ObjectMacros.h" +#include "UObject/UObjectGlobals.h" +#include "UObject/Class.h" +#include "UObject/Package.h" +#include "Math/Box.h" + +#if WITH_EDITOR + #include "SSCSEditor.h" + #include "ObjectTools.h" + #include "Kismet2/ComponentEditorUtils.h" + #include "Editor/Transactor.h" +#endif + +class AActor; +class UWorld; +class ULandscapeSplinesComponent; +struct FHoudiniStaticMeshGenerationProperties; +struct FMeshBuildSettings; + +template +class TSubclassOf; + + +struct HOUDINIENGINERUNTIME_API FHoudiniEngineRuntimeUtils +{ + public: + + // Return platform specific name of libHAPI. + static FString GetLibHAPIName(); + + // Check InCustomHoudiniLocationPath is a valid Houdini installation. + static bool CheckCustomHoudiniLocation(const FString& InCustomHoudiniLocationPath); + + // Returns default SM Generation Properties using the default settings values + static FHoudiniStaticMeshGenerationProperties GetDefaultStaticMeshGenerationProperties(); + + // Returns default SM Build Settings using the default settings values + static FMeshBuildSettings GetDefaultMeshBuildSettings(); + + // Sets to the custom dir in the settings + static void SetHoudiniHomeEnvironmentVariable(); + + // Helper function for returning a class from its name + static UClass* GetClassByName(const FString& InName); + + // ----------------------------------------------- + // Bounding Box utilities + // ----------------------------------------------- + + // Collect all the bounding boxes form the specified list of actors. OutBBoxes will be emptied. + static void GetBoundingBoxesFromActors(const TArray InActors, TArray& OutBBoxes); + + // Collect actors that derive from the given class that intersect with the given array of bounding boxes. + static bool FindActorsOfClassInBounds(UWorld* World, TSubclassOf ActorType, const TArray& BBoxes, const TArray* ExcludeActors, TArray& OutActors); + + // ----------------------------------------------- + // File path utilities + // ----------------------------------------------- + + // Joins paths by taking into account whether paths + // successive paths are relative or absolute. + // Truncate everything preceding an absolute path. + // Taken and adapted from FPaths::Combine(). + template + FORCEINLINE static FString JoinPaths(PathTypes&&... InPaths) + { + const TCHAR* Paths[] = { GetTCharPtr(Forward(InPaths))... }; + const int32 NumPaths = UE_ARRAY_COUNT(Paths); + + FString Out = TEXT(""); + if (NumPaths <= 0) + return Out; + Out = Paths[NumPaths-1]; + // Process paths in reverse and terminate when we reach an absolute path. + for (int32 i=NumPaths-2; i >= 0; --i) + { + if (FCString::Strlen(Paths[i]) == 0) + continue; + if (Out[0] == '/') + { + // We already have an absolute path. Terminate. + break; + } + Out = Paths[i] / Out; + } + if (Out.Len() > 0 && Out[0] != '/') + Out = TEXT("/") + Out; + return Out; + } + + // ------------------------------------------------------------------ + // ObjectTools (Make some editor only ObjectTools functions available + // in editor builds) + // ------------------------------------------------------------------ + + // Check/gather references to InObject. + // Returns true if the function could execute (editor vs runtime) + FORCEINLINE static bool GatherObjectReferencersForDeletion(UObject* InObject, bool& bOutIsReferenced, bool& bOutIsReferencedByUndo, FReferencerInformationList* OutMemoryReferences = nullptr, bool bInRequireReferencingProperties = false) + { +#if WITH_EDITOR + ObjectTools::GatherObjectReferencersForDeletion(InObject, bOutIsReferenced, bOutIsReferencedByUndo, OutMemoryReferences, bInRequireReferencingProperties); + + return true; +#else + return false; +#endif + } + + // Delete a single object from its package. Returns true if the object was deleted. In non-editor + // builds this function returns false. + FORCEINLINE static bool DeleteSingleObject(UObject* InObjectToDelete, bool bInPerformReferenceCheck=true) + { +#if WITH_EDITOR + return ObjectTools::DeleteSingleObject(InObjectToDelete, bInPerformReferenceCheck); +#else + return false; +#endif + } + + // Collects garbage and marks truely empty packages for delete + // Returns true if the function could execute (editor vs runtime) + FORCEINLINE static bool CleanupAfterSuccessfulDelete(const TArray& InObjectsDeletedSuccessfully, bool bInPerformReferenceCheck=true) + { +#if WITH_EDITOR + ObjectTools::CleanupAfterSuccessfulDelete(InObjectsDeletedSuccessfully, bInPerformReferenceCheck); + return true; +#else + return false; +#endif + } + + // Deletes a single object. Returns true if the object was deleted. + // The object is only deleted if there are no references to it. + // If the package is on disk then bOutPackageIsInMemoryOnly is false and the CleanUpAfterSuccessfulDelete + // must be called on the package after execution of this function. + static bool SafeDeleteSingleObject(UObject* const InObjectToDelete, UPackage*& OutPackage, bool& bOutPackageIsInMemoryOnly); + + // Deletes and cleans up on disk empty-packages for the objects in InObjectsToDelete. + // Objects are popped from InObjectsToDelete as they are processed (ran into cases where objects are garbage collected + // before we can properly delete them and cleanup their packages, so we tend to pass in a UPROPERTY based TArray + // that holds references to UObject to prevent garbage collection until we can delete them in this function) + // OutObjectsNotDeleted can optionally be used to return objects that could not be deleted. + // The function returns the number of objects that were deleted. + static int32 SafeDeleteObjects(TArray& InObjectsToDelete, TArray* OutObjectsNotDeleted=nullptr); + + // ------------------------------------------------- + // Blueprint utilities + // ------------------------------------------------- +#if WITH_EDITOR + // This function contains an excerpt from UEditorUtilities::CopyActorProperties() + // for specifically dealing with copying properties between components as well as propagating + // property changes to archetype instances. + static int32 CopyComponentProperties(UActorComponent* FromComponent, UActorComponent* ToComponent, const EditorUtilities::FCopyOptions& Options); + + // Get the SCSEditor for the given HoudiniAssetComponent + static FBlueprintEditor* GetBlueprintEditor(const UObject* InObject); + + static void MarkBlueprintAsStructurallyModified(UActorComponent* ComponentTemplate); + static void MarkBlueprintAsModified(UActorComponent* ComponentTemplate); +#endif + + // ------------------------------------------------- + // Landscape utilities + // ------------------------------------------------- + + static FTransform CalculateHoudiniLandscapeTransform(ALandscapeProxy* LandscapeProxy); + + // ------------------------------------------------- + // Editor Helpers + // ------------------------------------------------- +#if WITH_EDITOR + static bool SetActorLabel(AActor* Actor, const FString& ActorLabel); + + static void DoPostEditChangeProperty(UObject* Obj, FName PropertyName); + + static void DoPostEditChangeProperty(UObject* Obj, FProperty* Property); + + static void PropagateObjectDeltaChangeToArchetypeInstance(UObject* InObject, const FTransactionObjectDeltaChange& DeltaChange); + + template + static TSet PropagateDefaultValueChange(USceneComponent* InSceneComponentTemplate, const FName& PropertyName, const T& OldValue, const T& NewValue) + { + TSet UpdatedInstances; + FComponentEditorUtils::PropagateDefaultValueChange(InSceneComponentTemplate, FindFieldChecked(InSceneComponentTemplate->GetClass(), PropertyName), OldValue, NewValue, UpdatedInstances); + return UpdatedInstances; + } + + // Perform this operation on the given archetype as well as each archetype instance. If the given + // object in not an archetype instance, then don't do anything. + static void ForAllArchetypeInstances(UObject* Archetype, TFunctionRef Operation); + +#endif + + static bool SetDefaultCollisionFlag(UObject* InObject, bool bUseDefaultCollision); + + /** + // * Set the value on an UObject using reflection. + // * @param Object The object to copy the value into. + // * @param PropertyName The name of the property to set. + // * @param Value The value to assign to the property. + // * + // * @return true if the value was set correctly + // */ + //template + //static bool SetPropertyValue(ObjectType* Object, FName PropertyName, ValueType Value) + //{ + // // Get the property addresses for the source and destination objects. + // FProperty* Property = FindFieldChecked(ObjectType::StaticClass(), PropertyName); + + // // Get the property addresses for the object + // ValueType* SourceAddr = Property->ContainerPtrToValuePtr(Object); + + // if ( SourceAddr == NULL ) + // { + // return false; + // } + + // if ( !Object->HasAnyFlags(RF_ClassDefaultObject) ) + // { + // FEditPropertyChain PropertyChain; + // PropertyChain.AddHead(Property); + // Object->PreEditChange(PropertyChain); + // } + + // // Set the value on the destination object. + // *SourceAddr = Value; + + // if ( !Object->HasAnyFlags(RF_ClassDefaultObject) ) + // { + // FPropertyChangedEvent PropertyEvent(Property); + // Object->PostEditChangeProperty(PropertyEvent); + // } + + // return true; + //} +#if WITH_EDITOR + template + static bool SetTemplatePropertyValue(ObjectType* Object, FName PropertyName, ValueType Value) + { + // Get the property addresses for the source and destination objects. + FProperty* Property = FindFieldChecked(ObjectType::StaticClass(), PropertyName); + + // Get the property addresses for the object + ValueType* SourceAddr = Property->ContainerPtrToValuePtr(Object); + + if ( SourceAddr == NULL ) + { + return false; + } + + if ( !Object->HasAnyFlags(RF_ClassDefaultObject) ) + { + FEditPropertyChain PropertyChain; + PropertyChain.AddHead(Property); + ((UObject*)Object)->PreEditChange(PropertyChain); + } + + // Set the value on the destination object. + if (*SourceAddr != Value) + { + TSet UpdatedInstances; + *SourceAddr = Value; + PropagateDefaultValueChange(Object, Property, *SourceAddr, Value, UpdatedInstances); + } + + if ( !Object->HasAnyFlags(RF_ClassDefaultObject) ) + { + FPropertyChangedEvent PropertyEvent(Property); + Object->PostEditChangeProperty(PropertyEvent); + } + + return true; + } +#endif + +#if WITH_EDITOR + // Bool specialization + template + static bool SetTemplatePropertyValue(ObjectType* Object, FName PropertyName, bool NewBool) + { + // Get the property addresses for the source and destination objects. + FBoolProperty* BoolProperty = FindFieldChecked(ObjectType::StaticClass(), PropertyName); + check(BoolProperty); + + // Get the property addresses for the object + const int32 PropertyOffset = INDEX_NONE; + void* CurrentValue = PropertyOffset == INDEX_NONE ? BoolProperty->ContainerPtrToValuePtr(Object) : ((uint8*)Object + PropertyOffset); + check(CurrentValue); + + const bool CurrentBool = BoolProperty->GetPropertyValue(CurrentValue); + + // if ( !Object->HasAnyFlags(RF_ClassDefaultObject) ) + { + FEditPropertyChain PropertyChain; + PropertyChain.AddHead(BoolProperty); + ((UObject*)Object)->PreEditChange(PropertyChain); + } + + // Set the value on the destination object. + if (CurrentBool != NewBool) + { + TSet UpdatedInstances; + BoolProperty->SetPropertyValue(CurrentValue, NewBool); + FComponentEditorUtils::PropagateDefaultValueChange(Object, BoolProperty, CurrentBool, NewBool, UpdatedInstances); + } + + // if ( !Object->HasAnyFlags(RF_ClassDefaultObject) ) + { + FPropertyChangedEvent PropertyEvent(BoolProperty); + Object->PostEditChangeProperty(PropertyEvent); + } + + return true; + } +#endif + + // Find actor in a given world by label or name + template + static T* FindActorInWorldByLabelOrName(UWorld* InWorld, FString ActorLabelOrName, EActorIteratorFlags Flags = EActorIteratorFlags::AllActors) + { + T* OutActor = nullptr; + for (TActorIterator ActorIt(InWorld, T::StaticClass(), Flags); ActorIt; ++ActorIt) + { + OutActor = *ActorIt; + if (!OutActor) + continue; + + // Try the label first, then the name + if (OutActor->GetActorLabel() == ActorLabelOrName) + return OutActor; + if (OutActor->GetFName().ToString() == ActorLabelOrName) + return OutActor; + } + return nullptr; + } + + + // ------------------------------------------------- + // Landscape Splines + // ------------------------------------------------- + + // Gets the control points and/or segments of the given landscape spline + static bool GetLandscapeSplinesControlPointsAndSegments( + ULandscapeSplinesComponent* const InSplinesComponent, + TArray>* OutControlPoints=nullptr, + TArray>* OutSegments=nullptr); + + // Gets the control points of a ULandscapeSplinesComponent + static bool GetLandscapeSplinesControlPoints(ULandscapeSplinesComponent* const InSplinesComponent, TArray>& OutControlPoints); + + // Gets the segments of a ULandscapeSplinesComponent + static bool GetLandscapeSplinesSegments(ULandscapeSplinesComponent* const InSplinesComponent, TArray>& OutSegments); + + // Helper function for destroying landscapes. + + // ------------------------------------------------- + // Component utilities + // ------------------------------------------------- + + // Adds InActorComponent to its Owner as an instance component via Actor::AddInstanceComponent. If the Owner is + // null/invalid, then just set the creation method of InActorComponent to Instance. + // Returns false if InActorComponent is null / invalid. + static bool AddOrSetAsInstanceComponent(UActorComponent* InActorComponent); + + + static void ForceDetailsPanelToUpdate(); + protected: + // taken from FPaths::GetTCharPtr + FORCEINLINE static const TCHAR* GetTCharPtr(const TCHAR* Ptr) + { + return Ptr; + } + FORCEINLINE static const TCHAR* GetTCharPtr(const FString& Str) + { + return *Str; + } +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniFoliageUtils.cpp`: + +```cpp + +#include "HoudiniFoliageUtils.h" + +#include "FoliageType.h" +#include "InstancedFoliage.h" +#include "EngineUtils.h" + +void +FHoudiniFoliageUtils::RemoveFoliageTypeFromWorld(UWorld* World, UFoliageType* FoliageType) +{ + +#if WITH_EDITOR + // Avoid needlessly dirtying every FoliageInstanceActor by calling RemoveFoliageType() with null + if (!IsValid(FoliageType)) + return; + + for (TActorIterator ActorIt(World, AInstancedFoliageActor::StaticClass()); ActorIt; ++ActorIt) + { + AInstancedFoliageActor* IFA = *ActorIt; + + // Check to see if the IFA actually contains the FoliageType, then remove it. Do this so we + // don't dirty IFAs needlessly because Unreal doesn't check for this and always marks the actor as dirty. + FFoliageInfo* Info = IFA->FindInfo(FoliageType); + if (Info != nullptr) + { + IFA->RemoveFoliageType(&FoliageType, 1); + } + } +#endif +} + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniFoliageUtils.h`: + +```h +/* +* Copyright (c) <2022> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "FoliageType.h" +#include "InstancedFoliageActor.h" + +class HOUDINIENGINERUNTIME_API FHoudiniFoliageUtils +{ +public: + static void RemoveFoliageTypeFromWorld(UWorld* World, UFoliageType* FoliageType); +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniGenericAttribute.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniGenericAttribute.h" + +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +//#include "HoudiniEngineUtils.h" + +#include "AI/Navigation/NavCollisionBase.h" +#include "Components/ActorComponent.h" +#include "Components/PrimitiveComponent.h" +#include "Components/StaticMeshComponent.h" +#include "EditorFramework/AssetImportData.h" +#include "Engine/StaticMesh.h" +#include "Landscape.h" +#include "PhysicsEngine/BodySetup.h" + + + +FHoudiniGenericAttributeChangedProperty::FHoudiniGenericAttributeChangedProperty() + : Object() + , Property(nullptr) +{ + +} + +FHoudiniGenericAttributeChangedProperty::FHoudiniGenericAttributeChangedProperty(const FHoudiniGenericAttributeChangedProperty& InOther) +{ + *this = InOther; +} + +FHoudiniGenericAttributeChangedProperty::FHoudiniGenericAttributeChangedProperty(UObject* InObject, FEditPropertyChain& InPropertyChain, FProperty* InProperty) + : Object(InObject) + , Property(InProperty) +{ + PropertyChain.Empty(); + for (const auto& Entry : InPropertyChain) + { + PropertyChain.AddTail(Entry); + } + if (InPropertyChain.GetActiveNode()) + PropertyChain.SetActivePropertyNode(InPropertyChain.GetActiveNode()->GetValue()); + if (InPropertyChain.GetActiveMemberNode()) + PropertyChain.SetActivePropertyNode(InPropertyChain.GetActiveMemberNode()->GetValue()); +} + +void +FHoudiniGenericAttributeChangedProperty::operator=(const FHoudiniGenericAttributeChangedProperty& InOther) +{ + Object = InOther.Object; + Property = InOther.Property; + + PropertyChain.Empty(); + for (const auto& Entry : InOther.PropertyChain) + { + PropertyChain.AddTail(Entry); + } + if (InOther.PropertyChain.GetActiveNode()) + PropertyChain.SetActivePropertyNode(InOther.PropertyChain.GetActiveNode()->GetValue()); + if (InOther.PropertyChain.GetActiveMemberNode()) + PropertyChain.SetActivePropertyNode(InOther.PropertyChain.GetActiveMemberNode()->GetValue()); +} + +double +FHoudiniGenericAttribute::GetDoubleValue(int32 index) const +{ + if ((AttributeType == EAttribStorageType::FLOAT) || (AttributeType == EAttribStorageType::FLOAT64)) + { + if (DoubleValues.IsValidIndex(index)) + return DoubleValues[index]; + } + else if ((AttributeType == EAttribStorageType::INT) || (AttributeType == EAttribStorageType::INT64)) + { + if (IntValues.IsValidIndex(index)) + return (double)IntValues[index]; + } + else if (AttributeType == EAttribStorageType::STRING) + { + if (StringValues.IsValidIndex(index)) + return FCString::Atod(*StringValues[index]); + } + + return 0.0f; +} + +void +FHoudiniGenericAttribute::GetDoubleTuple(TArray& TupleValues, int32 index) const +{ + TupleValues.SetNumZeroed(AttributeTupleSize); + + for (int32 n = 0; n < AttributeTupleSize; n++) + TupleValues[n] = GetDoubleValue(index * AttributeTupleSize + n); +} + +int64 +FHoudiniGenericAttribute::GetIntValue(int32 index) const +{ + if ((AttributeType == EAttribStorageType::INT) || (AttributeType == EAttribStorageType::INT64)) + { + if (IntValues.IsValidIndex(index)) + return IntValues[index]; + } + else if ((AttributeType == EAttribStorageType::FLOAT) || (AttributeType == EAttribStorageType::FLOAT64)) + { + if (DoubleValues.IsValidIndex(index)) + return (int64)DoubleValues[index]; + } + else if (AttributeType == EAttribStorageType::STRING) + { + if (StringValues.IsValidIndex(index)) + return FCString::Atoi64(*StringValues[index]); + } + + return 0; +} + +void +FHoudiniGenericAttribute::GetIntTuple(TArray& TupleValues, int32 index) const +{ + TupleValues.SetNumZeroed(AttributeTupleSize); + + for (int32 n = 0; n < AttributeTupleSize; n++) + TupleValues[n] = GetIntValue(index * AttributeTupleSize + n); +} + +FString +FHoudiniGenericAttribute::GetStringValue(int32 index) const +{ + if (AttributeType == EAttribStorageType::STRING) + { + if (StringValues.IsValidIndex(index)) + return StringValues[index]; + } + else if ((AttributeType == EAttribStorageType::INT) || (AttributeType == EAttribStorageType::INT64)) + { + if (IntValues.IsValidIndex(index)) + return FString::FromInt((int32)IntValues[index]); + } + else if ((AttributeType == EAttribStorageType::FLOAT) || (AttributeType == EAttribStorageType::FLOAT64)) + { + if (DoubleValues.IsValidIndex(index)) + return FString::SanitizeFloat(DoubleValues[index]); + } + + return FString(); +} + +void +FHoudiniGenericAttribute::GetStringTuple(TArray& TupleValues, int32 index) const +{ + TupleValues.SetNumZeroed(AttributeTupleSize); + + for (int32 n = 0; n < AttributeTupleSize; n++) + TupleValues[n] = GetStringValue(index * AttributeTupleSize + n); +} + +bool +FHoudiniGenericAttribute::GetBoolValue(int32 index) const +{ + if ((AttributeType == EAttribStorageType::FLOAT) || (AttributeType == EAttribStorageType::FLOAT64)) + { + if (DoubleValues.IsValidIndex(index)) + return DoubleValues[index] == 0.0 ? false : true; + } + else if ((AttributeType == EAttribStorageType::INT) || (AttributeType == EAttribStorageType::INT64)) + { + if (IntValues.IsValidIndex(index)) + return IntValues[index] == 0 ? false : true; + } + else if (AttributeType == EAttribStorageType::STRING) + { + if (StringValues.IsValidIndex(index)) + return StringValues[index].Equals(TEXT("true"), ESearchCase::IgnoreCase) ? true : false; + } + + return false; +} + +void +FHoudiniGenericAttribute::GetBoolTuple(TArray& TupleValues, int32 index) const +{ + TupleValues.SetNumZeroed(AttributeTupleSize); + + for (int32 n = 0; n < AttributeTupleSize; n++) + TupleValues[n] = GetBoolValue(index * AttributeTupleSize + n); +} + +void* +FHoudiniGenericAttribute::GetData() +{ + if (AttributeType == EAttribStorageType::STRING) + { + if (StringValues.Num() > 0) + return StringValues.GetData(); + } + else if ((AttributeType == EAttribStorageType::INT) || (AttributeType == EAttribStorageType::INT64)) + { + if (IntValues.Num() > 0) + return IntValues.GetData(); + } + else if ((AttributeType == EAttribStorageType::FLOAT) || (AttributeType == EAttribStorageType::FLOAT64)) + { + if (DoubleValues.Num() > 0) + return DoubleValues.GetData(); + } + + return nullptr; +} + +void +FHoudiniGenericAttribute::SetDoubleValue(double InValue, int32 index) +{ + if ((AttributeType == EAttribStorageType::FLOAT) || (AttributeType == EAttribStorageType::FLOAT64)) + { + if (!DoubleValues.IsValidIndex(index)) + DoubleValues.SetNum(index + 1); + DoubleValues[index] = InValue; + } + else if ((AttributeType == EAttribStorageType::INT) || (AttributeType == EAttribStorageType::INT64)) + { + if (!IntValues.IsValidIndex(index)) + IntValues.SetNum(index + 1); + IntValues[index] = InValue; + } + else if (AttributeType == EAttribStorageType::STRING) + { + if (!StringValues.IsValidIndex(index)) + StringValues.SetNum(index + 1); + StringValues[index] = FString::SanitizeFloat(InValue); + } +} + +void +FHoudiniGenericAttribute::SetDoubleTuple(const TArray& InTupleValues, int32 index) +{ + if (!InTupleValues.IsValidIndex(AttributeTupleSize - 1)) + return; + + for (int32 n = 0; n < AttributeTupleSize; n++) + SetDoubleValue(InTupleValues[n], index * AttributeTupleSize + n); +} + +void +FHoudiniGenericAttribute::SetIntValue(int64 InValue, int32 index) +{ + if ((AttributeType == EAttribStorageType::INT) || (AttributeType == EAttribStorageType::INT64)) + { + if (!IntValues.IsValidIndex(index)) + IntValues.SetNum(index + 1); + IntValues[index] = InValue; + } + else if ((AttributeType == EAttribStorageType::FLOAT) || (AttributeType == EAttribStorageType::FLOAT64)) + { + if (!DoubleValues.IsValidIndex(index)) + DoubleValues.SetNum(index + 1); + DoubleValues[index] = InValue; + } + else if (AttributeType == EAttribStorageType::STRING) + { + if (!StringValues.IsValidIndex(index)) + StringValues.SetNum(index + 1); + StringValues[index] = FString::Printf(TEXT("%lld"), InValue); + } +} + +void +FHoudiniGenericAttribute::SetIntTuple(const TArray& InTupleValues, int32 index) +{ + if (!InTupleValues.IsValidIndex(AttributeTupleSize - 1)) + return; + + for (int32 n = 0; n < AttributeTupleSize; n++) + SetIntValue(InTupleValues[n], index * AttributeTupleSize + n); +} + +void +FHoudiniGenericAttribute::SetStringValue(const FString& InValue, int32 index) +{ + if (AttributeType == EAttribStorageType::STRING) + { + if (!StringValues.IsValidIndex(index)) + StringValues.SetNum(index + 1); + StringValues[index] = InValue; + } + else if ((AttributeType == EAttribStorageType::INT) || (AttributeType == EAttribStorageType::INT64)) + { + if (!IntValues.IsValidIndex(index)) + IntValues.SetNum(index + 1); + IntValues[index] = FCString::Atoi64(*InValue); + } + else if ((AttributeType == EAttribStorageType::FLOAT) || (AttributeType == EAttribStorageType::FLOAT64)) + { + if (!DoubleValues.IsValidIndex(index)) + DoubleValues.SetNum(index + 1); + DoubleValues[index] = FCString::Atod(*InValue); + } +} + +void +FHoudiniGenericAttribute::SetStringTuple(const TArray& InTupleValues, int32 index) +{ + if (!InTupleValues.IsValidIndex(AttributeTupleSize - 1)) + return; + + for (int32 n = 0; n < AttributeTupleSize; n++) + SetStringValue(InTupleValues[n], index * AttributeTupleSize + n); +} + +void +FHoudiniGenericAttribute::SetBoolValue(bool InValue, int32 index) +{ + if ((AttributeType == EAttribStorageType::FLOAT) || (AttributeType == EAttribStorageType::FLOAT64)) + { + if (!DoubleValues.IsValidIndex(index)) + DoubleValues.SetNum(index + 1); + DoubleValues[index] = InValue ? 1.0 : 0.0; + } + else if ((AttributeType == EAttribStorageType::INT) || (AttributeType == EAttribStorageType::INT64)) + { + if (!IntValues.IsValidIndex(index)) + IntValues.SetNum(index + 1); + IntValues[index] = InValue ? 1 : 0; + } + else if (AttributeType == EAttribStorageType::STRING) + { + if (!StringValues.IsValidIndex(index)) + StringValues.SetNum(index + 1); + StringValues[index] = InValue ? "true" : "false"; + } +} + +void +FHoudiniGenericAttribute::SetBoolTuple(const TArray& InTupleValues, int32 index) +{ + if (!InTupleValues.IsValidIndex(AttributeTupleSize - 1)) + return; + + for (int32 n = 0; n < AttributeTupleSize; n++) + SetBoolValue(InTupleValues[n], index * AttributeTupleSize + n); +} + +bool +FHoudiniGenericAttribute::UpdatePropertyAttributeOnObject( + UObject* InObject, + const FHoudiniGenericAttribute& InPropertyAttribute, + const int32& AtIndex, + const bool bInDeferPostPropertyChangedEvents, + TArray* OutChangedProperties, + const FFindPropertyFunctionType& InFindPropertyFunction) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniGenericAttribute::UpdatePropertyAttributeOnObject); + + if (!IsValid(InObject)) + return false; + + // Get the Property name + const FString& PropertyName = InPropertyAttribute.AttributeName; + if (PropertyName.IsEmpty()) + return false; + + // Skip, as setting NumCustomDataFloats this way causes Unreal to crash! + if (PropertyName.Equals(TEXT("NumCustomDataFloats"), ESearchCase::IgnoreCase)) + { + HOUDINI_LOG_WARNING( + TEXT("Skipping UProperty %s on %s, custom data floats should be modified via the unreal_num_custom_floats and unreal_per_instance_custom_dataX attributes"), + *PropertyName, *InObject->GetName()); + return false; + } + + // Some Properties need to be handle and modified manually... + if (PropertyName.Equals("CollisionProfileName", ESearchCase::IgnoreCase)) + { + UPrimitiveComponent* PC = Cast(InObject); + if (IsValid(PC)) + { + FString StringValue = InPropertyAttribute.GetStringValue(AtIndex); + FName Value = FName(*StringValue); + + if (Value == "Default") + { + // The "Default" collision profile can only be applied to StaticMesh components + FHoudiniEngineRuntimeUtils::SetDefaultCollisionFlag(PC, true); + } + else + { + // Only set if the component has a valid BodySetup + if(IsValid(PC->GetBodySetup())) + { + // NOTE: Setting to collision profile name will automatically disable the default collision flag + PC->SetCollisionProfileName(Value); + } + + // Patch the StaticMeshGenerationProperties on the HAC + UHoudiniAssetComponent* HAC = Cast(InObject); + if (IsValid(HAC)) + { + HAC->GetStaticMeshGenerationProperties().DefaultBodyInstance.SetCollisionProfileName(Value); + } + } + + return true; + } + + FBodyInstance* FoundBodyInst = nullptr; + + // If this is a UStaticMesh, get its default body instance + UStaticMesh* SM = Cast(InObject); + if (IsValid(SM)) + { + // Create a body setup if needed + if (!SM->GetBodySetup()) + SM->CreateBodySetup(); + + FoundBodyInst = &(SM->GetBodySetup()->DefaultInstance); + } + + // If this is a Landscape, get its body instance + ALandscapeProxy* Landscape = Cast(InObject); + if (IsValid(Landscape)) + { + FoundBodyInst = &(Landscape->BodyInstance); + } + + // Set the default collision profile name on the found body instance. + if (FoundBodyInst != nullptr) + { + FString StringValue = InPropertyAttribute.GetStringValue(AtIndex); + FName Value = FName(*StringValue); + + FoundBodyInst->SetCollisionProfileName(Value); + + return true; + } + + return false; + } + + if (PropertyName.Equals("CollisionEnabled", ESearchCase::IgnoreCase)) + { + UPrimitiveComponent* PC = Cast(InObject); + if (IsValid(PC)) + { + FString StringValue = InPropertyAttribute.GetStringValue(AtIndex); + if (StringValue.Equals("NoCollision", ESearchCase::IgnoreCase)) + { + PC->SetCollisionEnabled(ECollisionEnabled::NoCollision); + return true; + } + else if (StringValue.Equals("QueryOnly", ESearchCase::IgnoreCase)) + { + PC->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + return true; + } + else if (StringValue.Equals("PhysicsOnly", ESearchCase::IgnoreCase)) + { + PC->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly); + return true; + } + else if (StringValue.Equals("QueryAndPhysics", ESearchCase::IgnoreCase)) + { + PC->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + return true; + } + return false; + } + } + + // Specialize CastShadow to avoid paying the cost of finding property + calling Property change twice + if (PropertyName.Equals("CastShadow", ESearchCase::IgnoreCase)) + { + UPrimitiveComponent* Component = Cast< UPrimitiveComponent >(InObject); + if (IsValid(Component)) + { + bool Value = InPropertyAttribute.GetBoolValue(AtIndex); + Component->SetCastShadow(Value); + return true; + } + + ALandscapeProxy* Landscape = Cast(InObject); + if (IsValid(Landscape)) + { + bool Value = InPropertyAttribute.GetBoolValue(AtIndex); + Landscape->CastShadow = Value; + return true; + } + + return false; + } + + // Handle Actor and Component tags manually here + if (PropertyName.Contains("tag")) + { + // lambda to set the tag values + auto SetTags = [InPropertyAttribute, AtIndex](TArray* FoundTags) + { + if (FoundTags == nullptr) + return false; + + FName NameAttr = FName(*InPropertyAttribute.GetStringValue(AtIndex)); + FoundTags->AddUnique(NameAttr); + return true; + }; + + bool bApplyTagsToActor = false; + bool bApplyTagsToMainComponent = false; + bool bApplyTagsToAllComponents = false; + + if (PropertyName.StartsWith("ActorTag")) + { + bApplyTagsToActor = true; + } + if (PropertyName.StartsWith("MainComponentTag")) + { + bApplyTagsToMainComponent = true; + } + if (PropertyName.StartsWith("ComponentTag")) + { + bApplyTagsToAllComponents = true; + } + if (PropertyName.StartsWith("Tag")) + { + bApplyTagsToActor = true; + bApplyTagsToAllComponents = true; + } + + bool bSuccess = false; + // This is a component. Apply tags accordingly. + UActorComponent* AC = Cast(InObject); + if (IsValid(AC)) + { + if (bApplyTagsToMainComponent || bApplyTagsToAllComponents) + { + // The Main Component, in the context of a component, is simply the component itself. + bSuccess |= SetTags(&(AC->ComponentTags)); + } + + if (bApplyTagsToActor) + { + // Try to set Actor tags on this component's owner + AActor* Actor = AC->GetOwner(); + if (IsValid(Actor)) + { + bSuccess |= SetTags(&(Actor->Tags)); + } + } + } + + // If this is an Actor, apply tags accordingly + AActor* Actor = Cast(InObject); + if (IsValid(Actor)) + { + if (bApplyTagsToActor) + { + bSuccess |= SetTags(&(Actor->Tags)); + } + if (bApplyTagsToAllComponents) + { + // ComponentTags were specifically asked, try to set them on all actor component of this actors, including + // the root component. + for (auto& CurrentComponent : Actor->GetComponents()) + { + if (!IsValid(CurrentComponent)) + continue; + + bSuccess |= SetTags(&(CurrentComponent->ComponentTags)); + } + + } + else if (bApplyTagsToMainComponent) + { + // The Main Component, in the context of an actor, is the root component. + bSuccess |= SetTags(&(Actor->GetRootComponent()->ComponentTags)); + } + } + + if (bSuccess) + { + // Return only if we were able to process this property + return true; + } + } +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION < 7 +#if WITH_EDITOR + // Handle landscape edit layers toggling - no longer available in 5.7 + if (PropertyName.Equals("EnableEditLayers", ESearchCase::IgnoreCase) + || PropertyName.Equals("bCanHaveLayersContent", ESearchCase::IgnoreCase)) + { + ALandscape* Landscape = Cast(InObject); + if (IsValid(Landscape)) + { + if(InPropertyAttribute.GetBoolValue(AtIndex) != Landscape->CanHaveLayersContent()) + Landscape->ToggleCanHaveLayersContent(); + + return true; + } + + return false; + } +#endif +#endif + + // Try to find the corresponding UProperty + void* OutContainer = nullptr; + FProperty* FoundProperty = nullptr; + UObject* FoundPropertyObject = nullptr; + FEditPropertyChain FoundPropertyChain; + + if (InFindPropertyFunction) + { + bool bOutSkipDefaultIfPropertyNotFound = false; + const bool bFoundProperty = InFindPropertyFunction(InObject, PropertyName, bOutSkipDefaultIfPropertyNotFound, FoundPropertyChain, FoundProperty, FoundPropertyObject, OutContainer); + if (!bFoundProperty && bOutSkipDefaultIfPropertyNotFound) + return false; + } +// #if WITH_EDITOR +// // Try to match to source model properties when possible +// if (UStaticMesh* SM = Cast(InObject)) +// { +// if (IsValid(SM) && SM->GetNumSourceModels() > AtIndex) +// { +// bool bFoundProperty = false; +// TryToFindProperty(&SM->GetSourceModel(AtIndex), SM->GetSourceModel(AtIndex).StaticStruct(), PropertyName, FoundProperty, bFoundProperty, OutContainer); +// if (bFoundProperty) +// { +// FoundPropertyObject = InObject; +// } +// } +// } +// #endif + + bool bExactPropertyFound = false; + if (!FoundProperty && !FindPropertyOnObject(InObject, PropertyName, FoundPropertyChain, FoundProperty, FoundPropertyObject, OutContainer, bExactPropertyFound, false)) + return false; + + // Set the member and active properties on the chain + if (FoundPropertyChain.Num() > 0) + { + if (FoundPropertyChain.GetTail()) + FoundPropertyChain.SetActivePropertyNode(FoundPropertyChain.GetTail()->GetValue()); + if (FoundPropertyChain.GetHead()) + FoundPropertyChain.SetActiveMemberPropertyNode(FoundPropertyChain.GetHead()->GetValue()); + } + + // Modify the Property we found + if (!ModifyPropertyValueOnObject(FoundPropertyObject, InPropertyAttribute, FoundPropertyChain, FoundProperty, OutContainer, AtIndex, bInDeferPostPropertyChangedEvents, OutChangedProperties)) + return false; + + return true; +} + + +bool +FHoudiniGenericAttribute::FindPropertyOnObject( + UObject* InObject, + const FString& InPropertyName, + FEditPropertyChain& InPropertyChain, + FProperty*& OutFoundProperty, + UObject*& OutFoundPropertyObject, + void*& OutContainer, + bool& OutExactPropertyFound, + bool bDumpAttributes) +{ +#if WITH_EDITOR + if (!IsValid(InObject)) + return false; + + if (InPropertyName.IsEmpty()) + { + if (InObject->IsA()) + bDumpAttributes = true; + else if (!bDumpAttributes) + return false; + } + + UClass* ObjectClass = (InObject->IsA() ? Cast(InObject) : InObject->GetClass()); + if (!IsValid(ObjectClass)) + return false; + + // Set the result pointer to null + OutContainer = nullptr; + OutFoundProperty = nullptr; + OutFoundPropertyObject = InObject; + + FHoudiniGenericAttribute::TryToFindProperty( + InObject, + ObjectClass, + InPropertyName, + InPropertyChain, + OutFoundProperty, + OutExactPropertyFound, + OutContainer, + bDumpAttributes); + + // Try with FindField?? + if (!OutFoundProperty) + OutFoundProperty = FindFProperty(ObjectClass, *InPropertyName); + + // Try with FindPropertyByName ?? + if (!OutFoundProperty) + OutFoundProperty = ObjectClass->FindPropertyByName(*InPropertyName); + + // We found exactly the Property we were looking for + if (OutFoundProperty && OutExactPropertyFound) + return true; + + // Secondaries will store our current "best bet" + FProperty* SecondaryFoundProperty = nullptr ; + UObject* SecondaryPropertyObject = InObject; + void* SecondaryContainer = nullptr; + bool bSecondaryFound = false; + + // Lambda function used mainly to avoid code duplication + // Searches for a uproperty on a given object + // If an exact match is found - OutExactPropertyFound is set to true and the out parameters are set - we can exit immediately + // If a partial match is found - then we set the SecondaryFoundProperties and should keep looking for an exact match + auto SearchPropertyOn = [bDumpAttributes, InPropertyName, &InPropertyChain, + &OutFoundProperty, &OutFoundPropertyObject, &OutContainer, &OutExactPropertyFound, + &SecondaryFoundProperty, &SecondaryPropertyObject, &SecondaryContainer, &bSecondaryFound] + (UObject* InObject, const FString& InObjClass, const FString& InDumpStringBegin, const FString& InDumpStringEnd) + { + if (bDumpAttributes) + { + HOUDINI_LOG_MESSAGE(TEXT(" ")); + HOUDINI_LOG_MESSAGE(TEXT("%s"), *InDumpStringBegin); + + if (!IsValid(InObject)) + InObject = FHoudiniEngineRuntimeUtils::GetClassByName(InObjClass); + } + + // We may have found a property that matches what we're looking for + // but not exactly - see if we can find a better match on some nested classes + FProperty* CurrentFoundProperty = nullptr; + UObject* CurrentPropertyObject = InObject; + void* CurrentContainer = nullptr; + bool bCurrentFound = false; + if (InObject && FindPropertyOnObject( + InObject, InPropertyName, InPropertyChain, CurrentFoundProperty, CurrentPropertyObject, CurrentContainer, OutExactPropertyFound, bDumpAttributes)) + { + bCurrentFound = true; + } + + if (bDumpAttributes) + { + HOUDINI_LOG_MESSAGE(TEXT("%s"), *InDumpStringEnd); + } + else if (bCurrentFound) + { + // See if we have a perfect match for the component found property + if (OutExactPropertyFound) + { + // Exact match - use this found property + OutFoundProperty = CurrentFoundProperty; + OutFoundPropertyObject = CurrentPropertyObject; + OutContainer = CurrentContainer; + //return true; + } + else if (!bSecondaryFound) + { + // Not an exact match - use this as secondary but keep looking in other components + SecondaryFoundProperty = CurrentFoundProperty; + SecondaryPropertyObject = CurrentPropertyObject; + SecondaryContainer = CurrentContainer; + bSecondaryFound = true; + } + } + }; + + // Handle common properties nested in classes + // Static Meshes + UStaticMesh* SM = Cast(InObject); + if (IsValid(SM)) + { + // Search on the BodySetup + UObject* BS = SM->GetBodySetup(); + FString DumpBegin = TEXT("\n------------ BODY SETUP ------------------------------------------------------------------------------------"); + FString DumpEnd = TEXT("\n------------ BODY SETUP END --------------------------------------------------------------------------------"); + SearchPropertyOn(BS, TEXT("BodySetup"), DumpBegin, DumpEnd); + if (OutExactPropertyFound) + return true; + + // Search in the Asset Import Data + UObject* AID = SM->GetAssetImportData(); + DumpBegin = TEXT("------------ ASSET IMPORT DATA -----------------------------------------------------------------------------"); + DumpEnd = TEXT("\n------------ ASSET IMPORT DATA END -------------------------------------------------------------------------"); + SearchPropertyOn(AID, TEXT("AssetImportData"), DumpBegin, DumpEnd); + if (OutExactPropertyFound) + return true; + + // Search in Nav Collision + UObject* NC = SM->GetNavCollision(); + DumpBegin = TEXT("------------ NAV COLLISION ---------------------------------------------------------------------------------"); + DumpEnd = TEXT("\n------------ NAV COLLISION END -----------------------------------------------------------------------------"); + SearchPropertyOn(NC, TEXT("NavCollision"), DumpBegin, DumpEnd); + if (OutExactPropertyFound) + return true; + } + + // For Actors, parse their components + AActor* Actor = Cast(InObject); + if (IsValid(Actor)) + { + TArray AllComponents; + Actor->GetComponents(AllComponents, true); + + if (bDumpAttributes && AllComponents.Num() > 0) + { + HOUDINI_LOG_MESSAGE(TEXT(" ")); + HOUDINI_LOG_MESSAGE(TEXT("------------ ACTOR COMPONENTS ------------------------------------------------------------------------------")); + } + + int32 CompIdx = 0; + for (USceneComponent* SceneComponent : AllComponents) + { + if (!IsValid(SceneComponent)) + continue; + + // Search for the property on the component + FString DumpBegin = TEXT("------------ ") + SceneComponent->GetClass()->GetName(); + FString DumpEnd = DumpBegin + TEXT(" END"); + SearchPropertyOn(SceneComponent, TEXT("SceneComponent"), DumpBegin, DumpEnd); + if (OutExactPropertyFound) + return true; + } + } + + // Houdini Asset Component + UHoudiniAssetComponent* HAC = Cast(InObject); + if (IsValid(HAC)) + { + // Search the UHoudiniCookable + UObject* BakingObj = HAC->GetCookable(); + FString DumpBegin = TEXT("\n------------ HOUDINI COOKABLE ------------------------------------------------------------------------------------"); + FString DumpEnd = TEXT("\n------------ HOUDINI COOKABLE END --------------------------------------------------------------------------------"); + SearchPropertyOn(BakingObj, TEXT("HoudiniCookable"), DumpBegin, DumpEnd); + if (OutExactPropertyFound) + return true; + } + + + // Houdini Cookable + UHoudiniCookable* Cookable = Cast(InObject); + if (IsValid(Cookable) || (bDumpAttributes && InObject == UHoudiniCookable::StaticClass())) + { + //if (Cookable->IsBakingSupported()) + { + // Search the UCookableBakingData + UObject* BakingObj = Cookable ? Cookable->GetBakingData() : (UObject*)UCookableBakingData::StaticClass(); + FString DumpBegin = TEXT("\n------------ BAKING DATA ------------------------------------------------------------------------------------"); + FString DumpEnd = TEXT("\n------------ BAKING DATA END --------------------------------------------------------------------------------"); + SearchPropertyOn(BakingObj, TEXT("CookableBakingData"), DumpBegin, DumpEnd); + if (OutExactPropertyFound) + return true; + } + + //if (Cookable->IsComponentSupported()) + { + // Search the UCookableComponentData; + UObject* ComponentObj = Cookable ? Cookable->GetComponentData() : (UObject*)UCookableComponentData::StaticClass(); + FString DumpBegin = TEXT("\n------------ COMPONENT DATA ------------------------------------------------------------------------------------"); + FString DumpEnd = TEXT("\n------------ COMPONENT DATA END --------------------------------------------------------------------------------"); + SearchPropertyOn(ComponentObj, TEXT("CookableComponentData"), DumpBegin, DumpEnd); + if (OutExactPropertyFound) + return true; + } + + //if (Cookable->IsHoudiniAssetSupported()) + { + // Search the UCookableHoudiniAssetData; + UObject* HDAObj = Cookable ? Cookable->GetHoudiniAssetData() : (UObject*)UCookableHoudiniAssetData::StaticClass(); + FString DumpBegin = TEXT("\n------------ HOUDINI ASSET DATA ------------------------------------------------------------------------------------"); + FString DumpEnd = TEXT("\n------------ HOUDINI ASSET DATA END --------------------------------------------------------------------------------"); + SearchPropertyOn(HDAObj, TEXT("CookableHoudiniAssetData"), DumpBegin, DumpEnd); + if (OutExactPropertyFound) + return true; + } + + //if (Cookable->IsInputSupported()) + { + // Search the UCookableInputData; + UObject* InputObj = Cookable ? Cookable->GetInputData() : (UObject*)UCookableInputData::StaticClass(); + FString DumpBegin = TEXT("\n------------ INPUT DATA ------------------------------------------------------------------------------------"); + FString DumpEnd = TEXT("\n------------ INPUT DATA END --------------------------------------------------------------------------------"); + SearchPropertyOn(InputObj, TEXT("CookableInputData"), DumpBegin, DumpEnd); + if (OutExactPropertyFound) + return true; + } + + //if (Cookable->IsOutputSupported()) + { + // Search the UCookableOutputData; + UObject* OutputObj = Cookable ? Cookable->GetOutputData() : (UObject*)UCookableOutputData::StaticClass(); + FString DumpBegin = TEXT("\n------------ OUTPUT DATA ------------------------------------------------------------------------------------"); + FString DumpEnd = TEXT("\n------------ OUTPUT DATA END --------------------------------------------------------------------------------"); + SearchPropertyOn(OutputObj, TEXT("CookableOutputData"), DumpBegin, DumpEnd); + if (OutExactPropertyFound) + return true; + } + + //if (Cookable->IsParameterSupported()) + { + // Search the UCookableParameterData; + UObject* ParamObj = Cookable ? Cookable->GetParameterData() : (UObject*)UCookableParameterData::StaticClass(); + FString DumpBegin = TEXT("\n------------ PARAMETER DATA ------------------------------------------------------------------------------------"); + FString DumpEnd = TEXT("\n------------ PARAMETER DATA END --------------------------------------------------------------------------------"); + SearchPropertyOn(ParamObj, TEXT("CookableParameterData"), DumpBegin, DumpEnd); + if (OutExactPropertyFound) + return true; + } + + //if (Cookable->IsPDGSupported()) + { + // Search the UCookablePDGData; + UObject* PDGObj = Cookable ? Cookable->GetPDGData() : (UObject*)UCookablePDGData::StaticClass(); + FString DumpBegin = TEXT("\n------------ PDG DATA ------------------------------------------------------------------------------------"); + FString DumpEnd = TEXT("\n------------ PDG DATA END --------------------------------------------------------------------------------"); + SearchPropertyOn(PDGObj, TEXT("CookablePDGData"), DumpBegin, DumpEnd); + if (OutExactPropertyFound) + return true; + } + + //if (Cookable->IsProxySupported()) + { + // Search the UCookableProxyData; + UObject* ProxyObj = Cookable ? Cookable->GetProxyData() : (UObject*)UCookableProxyData::StaticClass(); + FString DumpBegin = TEXT("\n------------ PROXY DATA ------------------------------------------------------------------------------------"); + FString DumpEnd = TEXT("\n------------ PROXY DATA END --------------------------------------------------------------------------------"); + SearchPropertyOn(ProxyObj, TEXT("CookableProxyData"), DumpBegin, DumpEnd); + if (OutExactPropertyFound) + return true; + } + } + + if (OutExactPropertyFound) + return true; + + // If we found a secondary option for our property but haven't found an exact match + // - use the secondary property if we didn't find a property the first time + if (!OutFoundProperty && bSecondaryFound && SecondaryFoundProperty) + { + OutFoundProperty = SecondaryFoundProperty; + OutFoundPropertyObject = SecondaryPropertyObject; + OutContainer = SecondaryContainer; + } + + // We found the Property we were looking for + if (OutFoundProperty) + return true; +#endif + return false; +} + + +bool +FHoudiniGenericAttribute::TryToFindProperty( + void* InContainer, + UStruct* InStruct, + const FString& InPropertyName, + FEditPropertyChain& InPropertyChain, + FProperty*& OutFoundProperty, + bool& bOutExactPropertyHasBeenFound, + void*& OutContainer, + bool bDumpAttributes) +{ +#if WITH_EDITOR + if (!InContainer) + return false; + + if (!IsValid(InStruct)) + return false; + + if (InPropertyName.IsEmpty() && !bDumpAttributes) + return false; + + // Iterate manually on the properties, in order to handle StructProperties correctly + for (TFieldIterator PropIt(InStruct, EFieldIteratorFlags::IncludeSuper); PropIt; ++PropIt) + { + FProperty* CurrentProperty = *PropIt; + if (!CurrentProperty) + continue; + + FString DisplayName = CurrentProperty->GetDisplayNameText().ToString().Replace(TEXT(" "), TEXT("")); + FString Name = CurrentProperty->GetName(); + + // If the property name contains the uprop attribute name, we have a candidate + if (Name.Contains(InPropertyName) || DisplayName.Contains(InPropertyName)) + { + OutFoundProperty = CurrentProperty; + OutContainer = InContainer; + + // Stop if we found a property that perfectly match the name we're looking for + if ((Name == InPropertyName) || (DisplayName == InPropertyName)) + { + bOutExactPropertyHasBeenFound = true; + InPropertyChain.AddTail(OutFoundProperty); + break; + } + } + + // Do a recursive parsing for StructProperties + FStructProperty* StructProperty = CastField(CurrentProperty); + + // Handle dumping the generic attributes for this property + if (bDumpAttributes) + { + bool bSupportedProp = DumpGenericAttributeForProperty(CurrentProperty, InPropertyChain.Num()); + if (StructProperty) + { + // Dont dive inside supported struct properties + if (bSupportedProp) + continue; + } + + // Make sure we never find the property (unnecessary?) + //bOutExactPropertyHasBeenFound = false; + } + + if (StructProperty) + { + if (bDumpAttributes) + { + HOUDINI_LOG_MESSAGE(TEXT(" ")); + HOUDINI_LOG_MESSAGE(TEXT("-------- STRUCT %s BEGIN "), *StructProperty->GetName()); + } + + // Walk the structs' properties and try to find the one we're looking for + UScriptStruct* Struct = StructProperty->Struct; + if (!IsValid(Struct)) + continue; + + InPropertyChain.AddTail(StructProperty); + TryToFindProperty( + StructProperty->ContainerPtrToValuePtr(InContainer, 0), + Struct, + InPropertyName, + InPropertyChain, + OutFoundProperty, + bOutExactPropertyHasBeenFound, + OutContainer, + bDumpAttributes); + + if (bDumpAttributes) + { + HOUDINI_LOG_MESSAGE(TEXT("-------- STRUCT %s END "), *StructProperty->GetName()); + HOUDINI_LOG_MESSAGE(TEXT(" ")); + } + + if (!bOutExactPropertyHasBeenFound) + InPropertyChain.RemoveNode(InPropertyChain.GetTail()); + } + + if (!bDumpAttributes) + { + // Stop if we found a property that perfectly match the name we're looking for + if (bOutExactPropertyHasBeenFound) + break; + } + + } + + if (bOutExactPropertyHasBeenFound) + return true; + + // We found the Property we were looking for + if (OutFoundProperty) + return true; + +#endif + return false; +} + + +bool +FHoudiniGenericAttribute::HandlePostEditChangeProperty(UObject* InObject, FEditPropertyChain& InPropertyChain, FProperty* InProperty) +{ +#if WITH_EDITOR + if (InPropertyChain.Num() == 0) + { + FPropertyChangedEvent Evt(InProperty); + InObject->PostEditChangeProperty(Evt); + AActor* InOwner = Cast(InObject); + if (IsValid(InOwner)) + { + // If we are setting properties on an Actor component, we want to notify the + // actor of the changes too since the property change might be handled in the actor's + // PostEditChange callbacks (one such an example occurs when changing the material for a decal actor). + InOwner->PostEditChangeProperty(Evt); + } + + return true; + } + else if (InPropertyChain.Num() > 0) + { + FPropertyChangedEvent Evt(InPropertyChain.GetActiveNode() ? InPropertyChain.GetActiveNode()->GetValue() : nullptr); + FPropertyChangedChainEvent ChainEvt(InPropertyChain, Evt); + InObject->PostEditChangeChainProperty(ChainEvt); + AActor* InOwner = Cast(InObject); + if (IsValid(InOwner)) + { + // If we are setting properties on an Actor component, we want to notify the + // actor of the changes too since the property change might be handled in the actor's + // PostEditChange callbacks (one such an example occurs when changing the material for a decal actor). + InOwner->PostEditChangeChainProperty(ChainEvt); + } + + return true; + } + return false; +#else + return true; +#endif +} + +bool +FHoudiniGenericAttribute::ModifyPropertyValueOnObject( + UObject* InObject, + const FHoudiniGenericAttribute& InGenericAttribute, + FEditPropertyChain& InPropertyChain, + FProperty* FoundProperty, + void* InContainer, + const int32& InAtIndex, + const bool bInDeferPostPropertyChangedEvents, + TArray* OutChangedProperties) +{ + if (!IsValid(InObject) || !FoundProperty) + return false; + + // Determine the container to use (either InContainer if specified, or InObject) + void* Container = InContainer ? InContainer : InObject; + + // Property class name, used for logging + const FString PropertyClassName = FoundProperty->GetClass() ? FoundProperty->GetClass()->GetName() : TEXT("Unknown"); + + // Initialize using the found property + FProperty* InnerProperty = FoundProperty; + + AActor* InOwner = Cast(InObject->GetOuter()); + bool bPropertyValueChanged = false; + bool bPostEditChangePropertyCalled = false; + + auto OnPrePropertyChanged = [InObject, &InPropertyChain](FProperty* InProperty) + { + if (!InProperty) + return; + +#if WITH_EDITOR + if (InPropertyChain.Num() == 0) + { + InObject->PreEditChange(InProperty); + } + else if (InPropertyChain.Num() > 0) + { + InObject->PreEditChange(InPropertyChain); + } +#endif + }; + + auto OnPropertyChanged = [InObject, &InPropertyChain, InOwner, bInDeferPostPropertyChangedEvents, OutChangedProperties, &InGenericAttribute, &bPostEditChangePropertyCalled, &bPropertyValueChanged](FProperty* InProperty) + { + bPropertyValueChanged = true; + + if (!InProperty) + return; + +#if WITH_EDITOR + if (InPropertyChain.Num() == 0) + { + HOUDINI_LOG_MESSAGE( + TEXT("Modified UProperty %s (searched for %s) on %s named %s"), + *InProperty->GetName(), + *InGenericAttribute.AttributeName, + *(InObject->GetClass() ? InObject->GetClass()->GetName() : TEXT("Object")), + *(InObject->GetName())); + + if (bInDeferPostPropertyChangedEvents) + { + if (OutChangedProperties) + { + FHoudiniGenericAttributeChangedProperty ChangeData; + ChangeData.PropertyChain.AddTail(InProperty); + ChangeData.Property = InProperty; + ChangeData.Object = InObject; + OutChangedProperties->Add(ChangeData); + } + } + else + { + bPostEditChangePropertyCalled = HandlePostEditChangeProperty(InObject, InPropertyChain, InProperty); + } + } + else if (InPropertyChain.Num() > 0) + { + HOUDINI_LOG_MESSAGE( + TEXT("Modified UProperty %s (searched for %s) on %s named %s"), + *FString::JoinBy(InPropertyChain, TEXT("."), [](FProperty const* const Property) + { + if (!Property) + return FString(); + return Property->GetName(); + }), + *InGenericAttribute.AttributeName, + *(InObject->GetClass() ? InObject->GetClass()->GetName() : TEXT("Object")), + *(InObject->GetName())); + + if (bInDeferPostPropertyChangedEvents) + { + if (OutChangedProperties) + { + FHoudiniGenericAttributeChangedProperty ChangeData; + for (FProperty* Property : InPropertyChain) + ChangeData.PropertyChain.AddTail(Property); + + if (InPropertyChain.GetActiveNode()) + ChangeData.PropertyChain.SetActivePropertyNode(InPropertyChain.GetActiveNode()->GetValue()); + if (InPropertyChain.GetActiveMemberNode()) + ChangeData.PropertyChain.SetActiveMemberPropertyNode(InPropertyChain.GetActiveMemberNode()->GetValue()); + + ChangeData.Property = InProperty; + ChangeData.Object = InObject; + OutChangedProperties->Add(ChangeData); + } + } + else + { + bPostEditChangePropertyCalled = HandlePostEditChangeProperty(InObject, InPropertyChain, InProperty); + } + } +#endif + }; + + FArrayProperty* ArrayProperty = CastField(FoundProperty); + TSharedPtr ArrayHelper; + if (ArrayProperty) + { + InnerProperty = ArrayProperty->Inner; + ArrayHelper = MakeShareable(new FScriptArrayHelper_InContainer(ArrayProperty, Container)); + } + + // TODO: implement support for array attributes received from Houdini + + // Get the "proper" AtIndex in the flat array by using the attribute tuple size + // TODO: fix the issue when changing array of tuple properties! + const int32 TupleSize = InGenericAttribute.AttributeTupleSize; + int32 AtIndex = InAtIndex * TupleSize; + FFieldClass* PropertyClass = InnerProperty->GetClass(); + if (PropertyClass->IsChildOf(FNumericProperty::StaticClass()) || + PropertyClass->IsChildOf(FBoolProperty::StaticClass()) || + PropertyClass->IsChildOf(FEnumProperty::StaticClass()) || + PropertyClass->IsChildOf(FStrProperty::StaticClass()) || + PropertyClass->IsChildOf(FNameProperty::StaticClass())) + { + // Supported non-struct properties + + // If the attribute from Houdini has a tuple size > 1, we support setting it on arrays on the unreal side + // For example: a 3 float from Houdini can be set as a TArray in Unreal. + + // If this is an ArrayProperty, ensure that it is at least large enough for our tuple + // TODO: should we just set this to exactly our tuple size? + if (ArrayHelper.IsValid()) + ArrayHelper->ExpandForIndex(TupleSize - 1); + + for (int32 TupleIndex = 0; TupleIndex < TupleSize; ++TupleIndex) + { + void* ValuePtr = nullptr; + if (ArrayHelper.IsValid()) + { + ValuePtr = ArrayHelper->GetRawPtr(TupleIndex); + } + else + { + // If this is not an ArrayProperty, it could be a fixed (standard C/C++ array), check the ArrayDim + // on the property to determine if our TupleIndex is in range, if not, give up, we cannot set any more + // of our tuple indices on this property. + if (TupleIndex >= InnerProperty->ArrayDim) + break; + + ValuePtr = InnerProperty->ContainerPtrToValuePtr(Container, TupleIndex); + } + + if (ValuePtr) + { + // Handle each property type that we support + if (PropertyClass->IsChildOf(FNumericProperty::StaticClass())) + { + // Numeric properties are supported as floats and ints, and can also be set from a received string + FNumericProperty* const Property = CastField(InnerProperty); + if (InGenericAttribute.AttributeType == EAttribStorageType::STRING) + { + const FString NewValue = InGenericAttribute.GetStringValue(AtIndex + TupleIndex); + const FString CurrentValue = Property->GetNumericPropertyValueToString(ValuePtr); + if (NewValue != CurrentValue) + { + OnPrePropertyChanged(Property); + Property->SetNumericPropertyValueFromString(ValuePtr, *NewValue); + OnPropertyChanged(Property); + } + } + else if (Property->IsFloatingPoint()) + { + const double NewValue = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex); + const double CurrentValue = Property->GetFloatingPointPropertyValue(ValuePtr); + if (NewValue != CurrentValue) + { + OnPrePropertyChanged(Property); + Property->SetFloatingPointPropertyValue(ValuePtr, NewValue); + OnPropertyChanged(Property); + } + } + else if (Property->IsInteger()) + { + const int64 NewValue = InGenericAttribute.GetIntValue(AtIndex + TupleIndex); + const int64 CurrentValue = Property->GetSignedIntPropertyValue(ValuePtr); + if (NewValue != CurrentValue) + { + OnPrePropertyChanged(Property); + Property->SetIntPropertyValue(ValuePtr, NewValue); + OnPropertyChanged(Property); + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("Unsupported numeric property for %s (Class %s)"), *InGenericAttribute.AttributeName, *PropertyClassName); + return false; + } + } + else if (PropertyClass->IsChildOf(FBoolProperty::StaticClass())) + { + FBoolProperty* const Property = CastField(InnerProperty); + const bool NewValue = InGenericAttribute.GetBoolValue(AtIndex + TupleIndex); + const bool CurrentValue = Property->GetPropertyValue(ValuePtr); + if (NewValue != CurrentValue) + { + OnPrePropertyChanged(Property); + Property->SetPropertyValue(ValuePtr, NewValue); + OnPropertyChanged(Property); + } + } + else if (PropertyClass->IsChildOf(FEnumProperty::StaticClass())) + { + FNumericProperty* const Property = CastField(InnerProperty)->GetUnderlyingProperty(); + if (Property->IsInteger()) + { + const int64 NewValue = InGenericAttribute.GetIntValue(AtIndex + TupleIndex); + const int64 CurrentValue = Property->GetSignedIntPropertyValue(ValuePtr); + if (NewValue != CurrentValue) + { + OnPrePropertyChanged(Property); + Property->SetIntPropertyValue(ValuePtr, NewValue); + OnPropertyChanged(Property); + } + } + } + else if (PropertyClass->IsChildOf(FStrProperty::StaticClass())) + { + FStrProperty* const Property = CastField(InnerProperty); + const FString NewValue = InGenericAttribute.GetStringValue(AtIndex + TupleIndex); + const FString CurrentValue = Property->GetPropertyValue(ValuePtr); + if (NewValue != CurrentValue) + { + OnPrePropertyChanged(Property); + Property->SetPropertyValue(ValuePtr, NewValue); + OnPropertyChanged(Property); + } + } + else if (PropertyClass->IsChildOf(FNameProperty::StaticClass())) + { + FNameProperty* const Property = CastField(InnerProperty); + const FName NewValue = FName(*InGenericAttribute.GetStringValue(AtIndex + TupleIndex)); + const FName CurrentValue = Property->GetPropertyValue(ValuePtr); + if (NewValue != CurrentValue) + { + OnPrePropertyChanged(Property); + Property->SetPropertyValue(ValuePtr, NewValue); + OnPropertyChanged(Property); + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("Unsupported property for %s (Class %s)"), *InGenericAttribute.AttributeName, *PropertyClassName); + return false; + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("Could net get a valid value ptr for uproperty %s (Class %s), tuple index %i"), *InGenericAttribute.AttributeName, *PropertyClassName, TupleIndex); + if (TupleIndex == 0) + return false; + } + } + } + else if (FStructProperty* StructProperty = CastField(InnerProperty)) + { + // struct properties + + // If we receive an attribute with tuple size > 1 and the target is an Unreal struct property, then we set + // as many of the values as we can in the struct. For example: a 4-float received from Houdini where the + // target is an FVector, the FVector.X, Y and Z would be set from the 4-float indices 0-2. Index 3 from the + // 4-float would then be ignored. + + const int32 TupleIndex = 0; + // If this is an array property, ensure it has enough space + // TODO: should we just set the array size to 1 for non-arrays and to the array size for arrays (once we support array attributes from Houdini)? + // vs just ensuring there is enough space (and then potentially leaving previous/old data behind?) + if (ArrayHelper.IsValid()) + ArrayHelper->ExpandForIndex(TupleIndex); + + void* PropertyValue = nullptr; + if (ArrayHelper.IsValid()) + PropertyValue = ArrayHelper->GetRawPtr(TupleIndex); + else + PropertyValue = InnerProperty->ContainerPtrToValuePtr(Container, TupleIndex); + + if (PropertyValue) + { + const FName PropertyName = StructProperty->Struct->GetFName(); + if (PropertyName == NAME_Vector || PropertyName == NAME_Vector3d) + { + // Found a vector property, fill it with up to 3 tuple values + FVector& Vector = *static_cast(PropertyValue); + FVector NewVector = FVector::ZeroVector; + NewVector.X = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 0); + if (InGenericAttribute.AttributeTupleSize > 1) + NewVector.Y = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 1); + if (InGenericAttribute.AttributeTupleSize > 2) + NewVector.Z = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 2); + + if (NewVector != Vector) + { + OnPrePropertyChanged(StructProperty); + Vector = NewVector; + OnPropertyChanged(StructProperty); + } + } + else if (PropertyName == NAME_Vector3f) + { + // Found a vector property, fill it with up to 3 tuple values + FVector3f& Vector = *static_cast(PropertyValue); + FVector3f NewVector = FVector3f::ZeroVector; + NewVector.X = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 0); + if (InGenericAttribute.AttributeTupleSize > 1) + NewVector.Y = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 1); + if (InGenericAttribute.AttributeTupleSize > 2) + NewVector.Z = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 2); + + if (NewVector != Vector) + { + OnPrePropertyChanged(StructProperty); + Vector = NewVector; + OnPropertyChanged(StructProperty); + } + } + else if (PropertyName == NAME_Rotator || PropertyName == NAME_Rotator3d) + { + // Found a Rotator property, fill it with up to 3 tuple values + FRotator& Rotator = *static_cast(PropertyValue); + FRotator NewRotator = FRotator::ZeroRotator; + NewRotator.Roll = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 0); + if (InGenericAttribute.AttributeTupleSize > 1) + NewRotator.Pitch = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 1); + if (InGenericAttribute.AttributeTupleSize > 2) + NewRotator.Yaw = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 2); + + if (NewRotator != Rotator) + { + OnPrePropertyChanged(StructProperty); + Rotator = NewRotator; + OnPropertyChanged(StructProperty); + } + } + else if (PropertyName == NAME_Rotator3f) + { + // Found a Rotator property, fill it with up to 3 tuple values + FRotator3f& Rotator = *static_cast(PropertyValue); + FRotator3f NewRotator = FRotator3f::ZeroRotator; + NewRotator.Roll = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 0); + if (InGenericAttribute.AttributeTupleSize > 1) + NewRotator.Pitch = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 1); + if (InGenericAttribute.AttributeTupleSize > 2) + NewRotator.Yaw = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 2); + + if (NewRotator != Rotator) + { + OnPrePropertyChanged(StructProperty); + Rotator = NewRotator; + OnPropertyChanged(StructProperty); + } + } + else if (PropertyName == "Vector_NetQuantize100") + { + // Found a vector property, fill it with up to 3 tuple values + FVector_NetQuantize100& Vector = *static_cast(PropertyValue); + FVector_NetQuantize100 NewVector = FVector_NetQuantize100::ZeroVector; + NewVector.X = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 0); + if (InGenericAttribute.AttributeTupleSize > 1) + NewVector.Y = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 1); + if (InGenericAttribute.AttributeTupleSize > 2) + NewVector.Z = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 2); + + if (NewVector != Vector) + { + OnPrePropertyChanged(StructProperty); + Vector = NewVector; + OnPropertyChanged(StructProperty); + } + } + else if (PropertyName == NAME_Vector2D || PropertyName == NAME_Vector2d) + { + // Found a vector property, fill it with up to 2 tuple values + FVector2D& Vector = *static_cast(PropertyValue); + FVector2D NewVector = FVector2D::ZeroVector; + + NewVector.X = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 0); + if (InGenericAttribute.AttributeTupleSize > 1) + NewVector.Y = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 1); + + if (NewVector != Vector) + { + OnPrePropertyChanged(StructProperty); + Vector = NewVector; + OnPropertyChanged(StructProperty); + } + } + else if (PropertyName == NAME_Vector2f) + { + // Found a vector property, fill it with up to 2 tuple values + FVector2f& Vector = *static_cast(PropertyValue); + FVector2f NewVector = FVector2f::ZeroVector; + + NewVector.X = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 0); + if (InGenericAttribute.AttributeTupleSize > 1) + NewVector.Y = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 1); + + if (NewVector != Vector) + { + OnPrePropertyChanged(StructProperty); + Vector = NewVector; + OnPropertyChanged(StructProperty); + } + } + else if (PropertyName == NAME_Transform) + { + // Found a transform property fill it with the attribute tuple values + FVector3d Translation; + Translation.X = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 0); + if (InGenericAttribute.AttributeTupleSize > 1) + Translation.Y = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 1); + if (InGenericAttribute.AttributeTupleSize > 2) + Translation.Z = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 2); + + FQuat4d Rotation; + if (InGenericAttribute.AttributeTupleSize > 3) + Rotation.W = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 3); + if (InGenericAttribute.AttributeTupleSize > 4) + Rotation.X = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 4); + if (InGenericAttribute.AttributeTupleSize > 5) + Rotation.Y = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 5); + if (InGenericAttribute.AttributeTupleSize > 6) + Rotation.Z = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 6); + + FVector3d Scale(1, 1, 1); + if (InGenericAttribute.AttributeTupleSize > 7) + Scale.X = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 7); + if (InGenericAttribute.AttributeTupleSize > 8) + Scale.Y = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 8); + if (InGenericAttribute.AttributeTupleSize > 9) + Scale.Z = InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 9); + + FTransform& Transform = *static_cast(PropertyValue); + const FTransform NewTransform(Rotation, Translation, Scale); + + if (!NewTransform.Equals(Transform)) + { + OnPrePropertyChanged(StructProperty); + Transform = NewTransform; + OnPropertyChanged(StructProperty); + } + } + else if (PropertyName == NAME_Color) + { + FColor& Color = *static_cast(PropertyValue); + FColor NewColor = FColor::Black; + NewColor.R = (int8)InGenericAttribute.GetIntValue(AtIndex + TupleIndex); + if (InGenericAttribute.AttributeTupleSize > 1) + NewColor.G = (int8)InGenericAttribute.GetIntValue(AtIndex + TupleIndex + 1); + if (InGenericAttribute.AttributeTupleSize > 2) + NewColor.B = (int8)InGenericAttribute.GetIntValue(AtIndex + TupleIndex + 2); + if (InGenericAttribute.AttributeTupleSize > 3) + NewColor.A = (int8)InGenericAttribute.GetIntValue(AtIndex + TupleIndex + 3); + + if (NewColor != Color) + { + OnPrePropertyChanged(StructProperty); + Color = NewColor; + OnPropertyChanged(StructProperty); + } + } + else if (PropertyName == NAME_LinearColor) + { + FLinearColor& LinearColor = *static_cast(PropertyValue); + FLinearColor NewLinearColor = FLinearColor::Black; + NewLinearColor.R = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex); + if (InGenericAttribute.AttributeTupleSize > 1) + NewLinearColor.G = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 1); + if (InGenericAttribute.AttributeTupleSize > 2) + NewLinearColor.B = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 2); + if (InGenericAttribute.AttributeTupleSize > 3) + NewLinearColor.A = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 3); + + if (NewLinearColor != LinearColor) + { + OnPrePropertyChanged(StructProperty); + LinearColor = NewLinearColor; + OnPropertyChanged(StructProperty); + } + } + else if (PropertyName == "Int32Interval") + { + FInt32Interval& Interval = *static_cast(PropertyValue); + FInt32Interval NewInterval; + NewInterval.Min = (int32)InGenericAttribute.GetIntValue(AtIndex + TupleIndex); + if (InGenericAttribute.AttributeTupleSize > 1) + NewInterval.Max = (int32)InGenericAttribute.GetIntValue(AtIndex + TupleIndex + 1); + + if (NewInterval.Min != Interval.Min || NewInterval.Max != Interval.Max) + { + OnPrePropertyChanged(StructProperty); + Interval = NewInterval; + OnPropertyChanged(StructProperty); + } + } + else if (PropertyName == "FloatInterval") + { + FFloatInterval& Interval = *static_cast(PropertyValue); + FFloatInterval NewInterval; + NewInterval.Min = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex); + if (InGenericAttribute.AttributeTupleSize > 1) + NewInterval.Max = (float)InGenericAttribute.GetDoubleValue(AtIndex + TupleIndex + 1); + + if (NewInterval.Min != Interval.Min || NewInterval.Max != Interval.Max) + { + OnPrePropertyChanged(StructProperty); + Interval = NewInterval; + OnPropertyChanged(StructProperty); + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("For uproperty %s (Class %s): unsupported struct property type: %s"), *InGenericAttribute.AttributeName, *PropertyClassName, *PropertyName.ToString()); + return false; + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("Could net get a valid value ptr for uproperty %s (Class %s)"), *InGenericAttribute.AttributeName, *PropertyClassName); + return false; + } + } + else if (FObjectProperty* ObjectProperty = CastField(InnerProperty)) + { + // OBJECT PATH PROPERTY + const int32 TupleIndex = 0; + // If this is an array property, ensure it has enough space + // TODO: should we just set the array size to 1 for non-arrays or to the array size for arrays (once we support array attributes from Houdini)? + // vs just ensuring there is enough space (and then potentially leaving previous/old data behind?) + if (ArrayHelper.IsValid()) + ArrayHelper->ExpandForIndex(TupleIndex); + + FString Value = InGenericAttribute.GetStringValue(AtIndex + TupleIndex); + void* ValuePtr = nullptr; + if (ArrayHelper.IsValid()) + ValuePtr = ArrayHelper->GetRawPtr(TupleIndex); + else + ValuePtr = InnerProperty->ContainerPtrToValuePtr(Container, TupleIndex); + + if (ValuePtr) + { + // Using TryLoad() over LoadSynchronous() as the later could crash with invalid path + UObject* ValueObject = nullptr; + FSoftObjectPath ObjPath(Value); + if(ObjPath.IsValid()) + { + ValueObject = ObjPath.TryLoad(); + } + + // Ensure the ObjectProperty class matches the ValueObject that we just loaded + if (!ValueObject || (ValueObject && ValueObject->IsA(ObjectProperty->PropertyClass))) + { + UObject const* const CurrentValue = ObjectProperty->GetObjectPropertyValue(ValuePtr); + if (CurrentValue != ValueObject) + { + OnPrePropertyChanged(ObjectProperty); + ObjectProperty->SetObjectPropertyValue(ValuePtr, ValueObject); + OnPropertyChanged(ObjectProperty); + } + } + else + { + HOUDINI_LOG_WARNING( + TEXT("Could net set object property %s: ObjectProperty's object class (%s) does not match referenced object class (%s)!"), + *InGenericAttribute.AttributeName, *(ObjectProperty->PropertyClass->GetName()), IsValid(ValueObject) ? *(ValueObject->GetClass()->GetName()) : TEXT("NULL")); + return false; + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("Could net get a valid value ptr for uproperty %s (Class %s)"), *InGenericAttribute.AttributeName, *PropertyClassName); + return false; + } + } + else if (FSoftObjectProperty* SoftObjectProperty = CastField(InnerProperty)) + { + // OBJECT PATH PROPERTY + const int32 TupleIndex = 0; + // If this is an array property, ensure it has enough space + // TODO: should we just set the array size to 1 for non-arrays or to the array size for arrays (once we support array attributes from Houdini)? + // vs just ensuring there is enough space (and then potentially leaving previous/old data behind?) + if (ArrayHelper.IsValid()) + ArrayHelper->ExpandForIndex(TupleIndex); + + FString Value = InGenericAttribute.GetStringValue(AtIndex + TupleIndex); + void* ValuePtr = nullptr; + if (ArrayHelper.IsValid()) + ValuePtr = ArrayHelper->GetRawPtr(TupleIndex); + else + ValuePtr = InnerProperty->ContainerPtrToValuePtr(Container, TupleIndex); + + if (ValuePtr) + { + const FSoftObjectPtr SoftObjectPtr = SoftObjectProperty->GetPropertyValue(ValuePtr); + const FSoftObjectPath SoftObjectPath = SoftObjectPtr.ToSoftObjectPath(); + const FString CurrentValue = SoftObjectPath.GetAssetPathString(); + if (CurrentValue != Value) + { + OnPrePropertyChanged(SoftObjectProperty); + SoftObjectProperty->SetPropertyValue(ValuePtr, FSoftObjectPtr(FSoftObjectPath(Value))); + OnPropertyChanged(SoftObjectProperty); + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("Could net get a valid value ptr for uproperty %s (Class %s)"), *InGenericAttribute.AttributeName, *PropertyClassName); + return false; + } + } + else + { + // Property was found, but is of an unsupported type + HOUDINI_LOG_WARNING(TEXT("Unsupported UProperty Class: %s found for uproperty %s"), *PropertyClassName, *InGenericAttribute.AttributeName); + return false; + } + + if (bPropertyValueChanged && !bPostEditChangePropertyCalled && !bInDeferPostPropertyChangedEvents) + { +#if WITH_EDITOR + InObject->PostEditChange(); + if (InOwner) + { + InOwner->PostEditChange(); + } +#endif + } + + return true; +} + +bool +FHoudiniGenericAttribute::GetPropertyValueFromObject( + UObject* InObject, + FProperty* InFoundProperty, + void* InContainer, + FHoudiniGenericAttribute& InGenericAttribute, + const int32& InAtIndex) +{ + if (!IsValid(InObject) || !InFoundProperty) + return false; + + // Determine the container to use (either InContainer if specified, or InObject) + void* Container = InContainer ? InContainer : InObject; + + // Property class name, used for logging + const FString PropertyClassName = InFoundProperty->GetClass() ? InFoundProperty->GetClass()->GetName() : TEXT("Unknown"); + + // Initialize using the found property + FProperty* InnerProperty = InFoundProperty; + + FArrayProperty* ArrayProperty = CastField(InFoundProperty); + TSharedPtr ArrayHelper; + if (ArrayProperty) + { + InnerProperty = ArrayProperty->Inner; + ArrayHelper = MakeShareable(new FScriptArrayHelper_InContainer(ArrayProperty, Container)); + } + + // TODO: implement support for array attributes received from Houdini + + // Get the "proper" AtIndex in the flat array by using the attribute tuple size + // TODO: fix the issue when changing array of tuple properties! + const int32 TupleSize = InGenericAttribute.AttributeTupleSize; + int32 AtIndex = InAtIndex * TupleSize; + FFieldClass* PropertyClass = InnerProperty->GetClass(); + if (PropertyClass->IsChildOf(FNumericProperty::StaticClass()) || PropertyClass->IsChildOf(FBoolProperty::StaticClass()) || + PropertyClass->IsChildOf(FStrProperty::StaticClass()) || PropertyClass->IsChildOf(FNameProperty::StaticClass())) + { + // Supported non-struct properties + + // If the attribute from Houdini has a tuple size > 1, we support getting it on arrays on the unreal side + // For example: a 3 float in Houdini can be set from a TArray in Unreal. + + for (int32 TupleIndex = 0; TupleIndex < TupleSize; ++TupleIndex) + { + void* ValuePtr = nullptr; + if (ArrayHelper.IsValid()) + { + // Check that we are not out of range + if (TupleIndex >= ArrayHelper->Num()) + break; + + ValuePtr = ArrayHelper->GetRawPtr(TupleIndex); + } + else + { + // If this is not an ArrayProperty, it could be a fixed (standard C/C++ array), check the ArrayDim + // on the property to determine if our TupleIndex is in range, if not, give up, we cannot get any more + // of our tuple indices from this property. + if (TupleIndex >= InnerProperty->ArrayDim) + break; + + ValuePtr = InnerProperty->ContainerPtrToValuePtr(Container, TupleIndex); + } + + if (ValuePtr) + { + // Handle each property type that we support + if (PropertyClass->IsChildOf(FNumericProperty::StaticClass())) + { + // Numeric properties are supported as floats and ints, and can also be set from a received string + FNumericProperty* const Property = CastField(InnerProperty); + if (InGenericAttribute.AttributeType == EAttribStorageType::STRING) + { + InGenericAttribute.SetStringValue(Property->GetNumericPropertyValueToString(ValuePtr), AtIndex + TupleIndex); + } + else if (Property->IsFloatingPoint()) + { + InGenericAttribute.SetDoubleValue(Property->GetFloatingPointPropertyValue(ValuePtr), AtIndex + TupleIndex); + } + else if (Property->IsInteger()) + { + InGenericAttribute.SetIntValue(Property->GetSignedIntPropertyValue(ValuePtr), AtIndex + TupleIndex); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Unsupported numeric property for %s (Class %s)"), *InGenericAttribute.AttributeName, *PropertyClassName); + return false; + } + } + else if (PropertyClass->IsChildOf(FBoolProperty::StaticClass())) + { + FBoolProperty* const Property = CastField(InnerProperty); + InGenericAttribute.SetBoolValue(Property->GetPropertyValue(ValuePtr), AtIndex + TupleIndex); + } + else if (PropertyClass->IsChildOf(FStrProperty::StaticClass())) + { + FStrProperty* const Property = CastField(InnerProperty); + InGenericAttribute.SetStringValue(Property->GetPropertyValue(ValuePtr), AtIndex + TupleIndex); + } + else if (PropertyClass->IsChildOf(FNameProperty::StaticClass())) + { + FNameProperty* const Property = CastField(InnerProperty); + InGenericAttribute.SetStringValue(Property->GetPropertyValue(ValuePtr).ToString(), AtIndex + TupleIndex); + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("Could net get a valid value ptr for uproperty %s (Class %s), tuple index %i"), *InGenericAttribute.AttributeName, *PropertyClassName, TupleIndex); + if (TupleIndex == 0) + return false; + } + } + } + else if (FStructProperty* StructProperty = CastField(InnerProperty)) + { + // struct properties + + // Set as many as the tuple values as we can from the Unreal struct. + + const int32 TupleIndex = 0; + + void* PropertyValue = nullptr; + if (ArrayHelper.IsValid()) + { + if (ArrayHelper->IsValidIndex(TupleIndex)) + PropertyValue = ArrayHelper->GetRawPtr(TupleIndex); + } + else if (TupleIndex < InnerProperty->ArrayDim) + { + PropertyValue = InnerProperty->ContainerPtrToValuePtr(Container, TupleIndex); + } + + if (PropertyValue) + { + const FName PropertyName = StructProperty->Struct->GetFName(); + if (PropertyName == NAME_Vector || PropertyName == NAME_Vector3d) + { + // Found a vector property, fill it with up to 3 tuple values + const FVector& Vector = *static_cast(PropertyValue); + InGenericAttribute.SetDoubleValue(Vector.X, AtIndex + TupleIndex + 0); + if (InGenericAttribute.AttributeTupleSize > 1) + InGenericAttribute.SetDoubleValue(Vector.Y, AtIndex + TupleIndex + 1); + if (InGenericAttribute.AttributeTupleSize > 2) + InGenericAttribute.SetDoubleValue(Vector.Z, AtIndex + TupleIndex + 2); + } + else if (PropertyName == NAME_Vector3f) + { + // Found a vector property, fill it with up to 3 tuple values + const FVector3f& Vector = *static_cast(PropertyValue); + InGenericAttribute.SetDoubleValue((double)Vector.X, AtIndex + TupleIndex + 0); + if (InGenericAttribute.AttributeTupleSize > 1) + InGenericAttribute.SetDoubleValue((double)Vector.Y, AtIndex + TupleIndex + 1); + if (InGenericAttribute.AttributeTupleSize > 2) + InGenericAttribute.SetDoubleValue((double)Vector.Z, AtIndex + TupleIndex + 2); + } + else if (PropertyName == NAME_Vector2D || PropertyName == NAME_Vector2d) + { + // Found a vector property, fill it with up to 3 tuple values + const FVector2D& Vector = *static_cast(PropertyValue); + InGenericAttribute.SetDoubleValue(Vector.X, AtIndex + TupleIndex + 0); + if (InGenericAttribute.AttributeTupleSize > 1) + InGenericAttribute.SetDoubleValue(Vector.Y, AtIndex + TupleIndex + 1); + } + else if (PropertyName == NAME_Vector2f) + { + // Found a vector property, fill it with up to 3 tuple values + const FVector2f& Vector = *static_cast(PropertyValue); + InGenericAttribute.SetDoubleValue((double)Vector.X, AtIndex + TupleIndex + 0); + if (InGenericAttribute.AttributeTupleSize > 1) + InGenericAttribute.SetDoubleValue((double)Vector.Y, AtIndex + TupleIndex + 1); + } + else if (PropertyName == NAME_Transform) + { + // Found a transform property fill it with the attribute tuple values + const FTransform& Transform = *static_cast(PropertyValue); + const FVector3d Translation = Transform.GetTranslation(); + const FQuat4d Rotation = Transform.GetRotation(); + const FVector Scale = Transform.GetScale3D(); + + InGenericAttribute.SetDoubleValue(Translation.X, AtIndex + TupleIndex + 0); + if (InGenericAttribute.AttributeTupleSize > 1) + InGenericAttribute.SetDoubleValue(Translation.Y, AtIndex + TupleIndex + 1); + if (InGenericAttribute.AttributeTupleSize > 2) + InGenericAttribute.SetDoubleValue(Translation.Z, AtIndex + TupleIndex + 2); + + if (InGenericAttribute.AttributeTupleSize > 3) + InGenericAttribute.SetDoubleValue(Rotation.W, AtIndex + TupleIndex + 3); + if (InGenericAttribute.AttributeTupleSize > 4) + InGenericAttribute.SetDoubleValue(Rotation.X, AtIndex + TupleIndex + 4); + if (InGenericAttribute.AttributeTupleSize > 5) + InGenericAttribute.SetDoubleValue(Rotation.Y, AtIndex + TupleIndex + 5); + if (InGenericAttribute.AttributeTupleSize > 6) + InGenericAttribute.SetDoubleValue(Rotation.Z, AtIndex + TupleIndex + 6); + + if (InGenericAttribute.AttributeTupleSize > 7) + InGenericAttribute.SetDoubleValue(Scale.X, AtIndex + TupleIndex + 7); + if (InGenericAttribute.AttributeTupleSize > 8) + InGenericAttribute.SetDoubleValue(Scale.Y, AtIndex + TupleIndex + 8); + if (InGenericAttribute.AttributeTupleSize > 9) + InGenericAttribute.SetDoubleValue(Scale.Z, AtIndex + TupleIndex + 9); + } + else if (PropertyName == NAME_Color) + { + const FColor& Color = *static_cast(PropertyValue); + InGenericAttribute.SetIntValue(Color.R, AtIndex + TupleIndex); + if (InGenericAttribute.AttributeTupleSize > 1) + InGenericAttribute.SetIntValue(Color.G, AtIndex + TupleIndex + 1); + if (InGenericAttribute.AttributeTupleSize > 2) + InGenericAttribute.SetIntValue(Color.B, AtIndex + TupleIndex + 2); + if (InGenericAttribute.AttributeTupleSize > 3) + InGenericAttribute.SetIntValue(Color.A, AtIndex + TupleIndex + 3); + } + else if (PropertyName == NAME_LinearColor) + { + const FLinearColor& LinearColor = *static_cast(PropertyValue); + InGenericAttribute.SetDoubleValue(LinearColor.R, AtIndex + TupleIndex); + if (InGenericAttribute.AttributeTupleSize > 1) + InGenericAttribute.SetDoubleValue(LinearColor.G, AtIndex + TupleIndex + 1); + if (InGenericAttribute.AttributeTupleSize > 2) + InGenericAttribute.SetDoubleValue(LinearColor.B, AtIndex + TupleIndex + 2); + if (InGenericAttribute.AttributeTupleSize > 3) + InGenericAttribute.SetDoubleValue(LinearColor.A, AtIndex + TupleIndex + 3); + } + else if (PropertyName == "Int32Interval") + { + const FInt32Interval& Interval = *static_cast(PropertyValue); + InGenericAttribute.SetIntValue(Interval.Min, AtIndex + TupleIndex); + if (InGenericAttribute.AttributeTupleSize > 1) + InGenericAttribute.SetIntValue(Interval.Max, AtIndex + TupleIndex + 1); + } + else if (PropertyName == "FloatInterval") + { + const FFloatInterval& Interval = *static_cast(PropertyValue); + InGenericAttribute.SetDoubleValue(Interval.Min, AtIndex + TupleIndex); + if (InGenericAttribute.AttributeTupleSize > 1) + InGenericAttribute.SetDoubleValue(Interval.Max, AtIndex + TupleIndex + 1); + } + else + { + HOUDINI_LOG_WARNING(TEXT("For uproperty %s (Class %s): unsupported struct property type: %s"), *InGenericAttribute.AttributeName, *PropertyClassName, *PropertyName.ToString()); + return false; + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("Could net get a valid value ptr for uproperty %s (Class %s)"), *InGenericAttribute.AttributeName, *PropertyClassName); + return false; + } + } + else if (FObjectProperty* ObjectProperty = CastField(InnerProperty)) + { + // OBJECT PATH PROPERTY + const int32 TupleIndex = 0; + + void* ValuePtr = nullptr; + if (ArrayHelper.IsValid()) + { + if (ArrayHelper->IsValidIndex(TupleIndex)) + ValuePtr = ArrayHelper->GetRawPtr(TupleIndex); + } + else if (TupleIndex < InnerProperty->ArrayDim) + { + ValuePtr = InnerProperty->ContainerPtrToValuePtr(Container, TupleIndex); + } + + if (ValuePtr) + { + UObject* ValueObject = ObjectProperty->GetObjectPropertyValue(ValuePtr); + const TSoftObjectPtr ValueObjectPtr = ValueObject; + InGenericAttribute.SetStringValue(ValueObjectPtr.ToString(), AtIndex + TupleIndex); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Could net get a valid value ptr for uproperty %s (Class %s)"), *InGenericAttribute.AttributeName, *PropertyClassName); + return false; + } + } + else + { + // Property was found, but is of an unsupported type + HOUDINI_LOG_WARNING(TEXT("Unsupported UProperty Class: %s found for uproperty %s"), *PropertyClassName, *InGenericAttribute.AttributeName); + return false; + } + + return true; +} + +bool +FHoudiniGenericAttribute::GetAttributeTupleSizeAndStorageFromProperty( + UObject* InObject, + FProperty* InFoundProperty, + void* InContainer, + int32& OutAttributeTupleSize, + EAttribStorageType& OutAttributeStorageType) +{ + if (!IsValid(InObject) || !InFoundProperty) + return false; + + // Determine the container to use (either InContainer if specified, or InObject) + void* Container = InContainer ? InContainer : InObject; + + // Property class name, used for logging + const FString PropertyClassName = InFoundProperty->GetClass() ? InFoundProperty->GetClass()->GetName() : TEXT("Unknown"); + + // Initialize using the found property + FProperty* InnerProperty = InFoundProperty; + + // FArrayProperty* ArrayProperty = CastField(InFoundProperty); + // TSharedPtr ArrayHelper; + // if (ArrayProperty) + // { + // InnerProperty = ArrayProperty->Inner; + // ArrayHelper = MakeShareable(new FScriptArrayHelper_InContainer(ArrayProperty, Container)); + // } + + FFieldClass* PropertyClass = InnerProperty->GetClass(); + if (PropertyClass->IsChildOf(FNumericProperty::StaticClass()) + || PropertyClass->IsChildOf(FBoolProperty::StaticClass()) + || PropertyClass->IsChildOf(FStrProperty::StaticClass()) + || PropertyClass->IsChildOf(FNameProperty::StaticClass())) + { + // Supported non-struct properties + + // Here we cannot really do better than tuple size of 1 (since we need to support arrays in the future, we + // cannot just assume array size == tuple size going to Houdini) + OutAttributeTupleSize = 1; + + // Handle each property type that we support + if (PropertyClass->IsChildOf(FNumericProperty::StaticClass())) + { + // Numeric properties are supported as floats and ints, and can also be set from a received string + FNumericProperty* const Property = CastField(InnerProperty); + if (Property->IsFloatingPoint()) + { + OutAttributeStorageType = EAttribStorageType::FLOAT; + } + else if (Property->IsInteger()) + { + OutAttributeStorageType = EAttribStorageType::INT; + } + else + { + HOUDINI_LOG_WARNING(TEXT("Unsupported numeric property for %s (Class %s)"), *Property->GetName(), *PropertyClassName); + return false; + } + } + else if (PropertyClass->IsChildOf(FBoolProperty::StaticClass())) + { + OutAttributeStorageType = EAttribStorageType::INT; + } + else if (PropertyClass->IsChildOf(FStrProperty::StaticClass())) + { + OutAttributeStorageType = EAttribStorageType::STRING; + } + else if (PropertyClass->IsChildOf(FNameProperty::StaticClass())) + { + OutAttributeStorageType = EAttribStorageType::STRING; + } + else if (PropertyClass->IsChildOf(FEnumProperty::StaticClass())) + { + OutAttributeStorageType = EAttribStorageType::INT; + } + } + else if (FStructProperty* StructProperty = CastField(InnerProperty)) + { + // struct properties + + const FName PropertyName = StructProperty->Struct->GetFName(); + if (PropertyName == NAME_Vector + || PropertyName == NAME_Vector3d + || PropertyName == NAME_Vector3f + || PropertyName == "Vector_NetQuantize100") + { + OutAttributeTupleSize = 3; + OutAttributeStorageType = EAttribStorageType::FLOAT; + } + else if (PropertyName == NAME_Vector2D + || PropertyName == NAME_Vector2d + || PropertyName == NAME_Vector2f) + { + OutAttributeTupleSize = 2; + OutAttributeStorageType = EAttribStorageType::FLOAT; + } + else if (PropertyName == NAME_Rotator + || PropertyName == NAME_Rotator3d + || PropertyName == NAME_Rotator3f) + { + OutAttributeTupleSize = 3; + OutAttributeStorageType = EAttribStorageType::FLOAT; + } + else if (PropertyName == NAME_Transform) + { + OutAttributeTupleSize = 10; + OutAttributeStorageType = EAttribStorageType::FLOAT; + } + else if (PropertyName == NAME_Color) + { + OutAttributeTupleSize = 4; + OutAttributeStorageType = EAttribStorageType::INT; + } + else if (PropertyName == NAME_LinearColor) + { + OutAttributeTupleSize = 4; + OutAttributeStorageType = EAttribStorageType::FLOAT; + } + else if (PropertyName == "Int32Interval") + { + OutAttributeTupleSize = 2; + OutAttributeStorageType = EAttribStorageType::INT; + } + else if (PropertyName == "FloatInterval") + { + OutAttributeTupleSize = 2; + OutAttributeStorageType = EAttribStorageType::FLOAT; + } + else + { + HOUDINI_LOG_WARNING(TEXT("For uproperty %s (Class %s): unsupported struct property type: %s"), *InFoundProperty->GetName(), *PropertyClassName, *PropertyName.ToString()); + return false; + } + } + else if (FObjectProperty* ObjectProperty = CastField(InnerProperty)) + { + OutAttributeTupleSize = 1; + OutAttributeStorageType = EAttribStorageType::STRING; + } + else + { + // Property was found, but is of an unsupported type + HOUDINI_LOG_WARNING(TEXT("Unsupported UProperty Class: %s found for uproperty %s"), *PropertyClassName, *InFoundProperty->GetName()); + return false; + } + + return true; +} + +/* +bool +FHoudiniEngineUtils::TryToFindInStructProperty( + UObject* Object, FString UPropertyNameToFind, UStructProperty* StructProperty, UProperty*& FoundProperty, void*& StructContainer ) +{ + if ( !StructProperty || !Object ) + return false; + + // Walk the structs' properties and try to find the one we're looking for + UScriptStruct* Struct = StructProperty->Struct; + for (TFieldIterator< UProperty > It(Struct); It; ++It) + { + UProperty* Property = *It; + if ( !Property ) + continue; + + FString DisplayName = It->GetDisplayNameText().ToString().Replace(TEXT(" "), TEXT("")); + FString Name = It->GetName(); + + // If the property name contains the uprop attribute name, we have a candidate + if ( Name.Contains( UPropertyNameToFind ) || DisplayName.Contains( UPropertyNameToFind ) ) + { + // We found the property in the struct property, we need to keep the ValuePtr in the object + // of the structProp in order to be able to access the property value afterwards... + FoundProperty = Property; + StructContainer = StructProperty->ContainerPtrToValuePtr< void >( Object, 0); + + // If it's an equality, we dont need to keep searching + if ( ( Name == UPropertyNameToFind ) || ( DisplayName == UPropertyNameToFind ) ) + return true; + } + + if ( FoundProperty ) + continue; + + UStructProperty* NestedStruct = Cast( Property ); + if ( NestedStruct && TryToFindInStructProperty( Object, UPropertyNameToFind, NestedStruct, FoundProperty, StructContainer ) ) + return true; + + UArrayProperty* ArrayProp = Cast( Property ); + if ( ArrayProp && TryToFindInArrayProperty( Object, UPropertyNameToFind, ArrayProp, FoundProperty, StructContainer ) ) + return true; + } + + return false; +} + +bool +FHoudiniEngineUtils::TryToFindInArrayProperty( + UObject* Object, FString UPropertyNameToFind, UArrayProperty* ArrayProperty, UProperty*& FoundProperty, void*& StructContainer ) +{ + if ( !ArrayProperty || !Object ) + return false; + + UProperty* Property = ArrayProperty->Inner; + if ( !Property ) + return false; + + FString DisplayName = Property->GetDisplayNameText().ToString().Replace(TEXT(" "), TEXT("")); + FString Name = Property->GetName(); + + // If the property name contains the uprop attribute name, we have a candidate + if ( Name.Contains( UPropertyNameToFind ) || DisplayName.Contains( UPropertyNameToFind ) ) + { + // We found the property in the struct property, we need to keep the ValuePtr in the object + // of the structProp in order to be able to access the property value afterwards... + FoundProperty = Property; + StructContainer = ArrayProperty->ContainerPtrToValuePtr< void >( Object, 0); + + // If it's an equality, we dont need to keep searching + if ( ( Name == UPropertyNameToFind ) || ( DisplayName == UPropertyNameToFind ) ) + return true; + } + + if ( !FoundProperty ) + { + UStructProperty* NestedStruct = Cast( Property ); + if ( NestedStruct && TryToFindInStructProperty( ArrayProperty, UPropertyNameToFind, NestedStruct, FoundProperty, StructContainer ) ) + return true; + + UArrayProperty* ArrayProp = Cast( Property ); + if ( ArrayProp && TryToFindInArrayProperty( ArrayProperty, UPropertyNameToFind, ArrayProp, FoundProperty, StructContainer ) ) + return true; + } + + return false; +} +*/ + +bool +FHoudiniGenericAttribute::DumpGenericAttributeForProperty(FProperty* InProperty, int32 InPropChainNumber) +{ +#if WITH_EDITOR + if (!InProperty) + return false; + + FString Offset; + for (int n = 0; n < InPropChainNumber; n++) + Offset += "-"; + + // We will only dump the property types that are currently supported by the plugin + // for other indicate the unreal type + FProperty* DumpedProperty = InProperty; + + bool bIsArrayProperty = false; + FArrayProperty* ArrayProperty = CastField(InProperty); + TSharedPtr ArrayHelper; + if (ArrayProperty) + { + bIsArrayProperty = true; + DumpedProperty = ArrayProperty->Inner; + } + + FString HoudiniType; + int32 HoudiniTupleSize = 0; + + FFieldClass* PropertyClass = DumpedProperty->GetClass(); + FString UnrealType = PropertyClass->GetName(); + if (PropertyClass->IsChildOf(FNumericProperty::StaticClass()) + || PropertyClass->IsChildOf(FBoolProperty::StaticClass()) + || PropertyClass->IsChildOf(FEnumProperty::StaticClass()) + || PropertyClass->IsChildOf(FStrProperty::StaticClass()) + || PropertyClass->IsChildOf(FNameProperty::StaticClass())) + { + // Supported non-struct properties + HoudiniTupleSize = 1; + + // Handle each property type that we support + if (PropertyClass->IsChildOf(FNumericProperty::StaticClass())) + { + // Numeric properties are supported as floats and ints, and can also be set from a received string + FNumericProperty* const Property = CastField(DumpedProperty); + if (Property->IsFloatingPoint()) + { + HoudiniType = FString("float"); + } + else if (Property->IsInteger()) + { + HoudiniType = FString("int"); + } + else + { + // unsupported + //HoudiniType = FString("unknown"); + } + } + else if (PropertyClass->IsChildOf(FBoolProperty::StaticClass())) + { + HoudiniType = FString("int"); + } + else if (PropertyClass->IsChildOf(FStrProperty::StaticClass())) + { + HoudiniType = FString("string"); + } + else if (PropertyClass->IsChildOf(FNameProperty::StaticClass())) + { + HoudiniType = FString("string"); + } + else if (PropertyClass->IsChildOf(FEnumProperty::StaticClass())) + { + HoudiniType = FString("int"); + } + } + else if (FStructProperty* StructProperty = CastField(DumpedProperty)) + { + // struct properties + const FName PropertyName = StructProperty->Struct->GetFName(); + UnrealType = PropertyName.ToString() + " (struct)"; + + if (PropertyName == NAME_Vector + || PropertyName == NAME_Vector3d + || PropertyName == NAME_Vector3f + || PropertyName == NAME_Rotator + || PropertyName == NAME_Rotator3d + || PropertyName == NAME_Rotator3f + || PropertyName == "Vector_NetQuantize100") + { + HoudiniTupleSize = 3; + HoudiniType = FString("float"); + } + if (PropertyName == NAME_Vector2D + || PropertyName == NAME_Vector2d + || PropertyName == NAME_Vector2f) + { + HoudiniTupleSize = 2; + HoudiniType = FString("float"); + } + else if (PropertyName == NAME_Transform) + { + HoudiniTupleSize = 10; + HoudiniType = FString("float"); + } + else if (PropertyName == NAME_Color) + { + HoudiniTupleSize = 4; + HoudiniType = FString("int"); + } + else if (PropertyName == NAME_LinearColor) + { + HoudiniTupleSize = 4; + HoudiniType = FString("float"); + } + else if (PropertyName == "Int32Interval") + { + HoudiniTupleSize = 2; + HoudiniType = FString("int"); + } + else if (PropertyName == "FloatInterval") + { + HoudiniTupleSize = 2; + HoudiniType = FString("float"); + } + else + { + // unsupported + } + } + else if (FObjectProperty* ObjectProperty = CastField(DumpedProperty)) + { + HoudiniTupleSize = 1; + HoudiniType = FString("string"); + UnrealType = PropertyClass->GetName(); + } + else + { + // Property was found, but is of an unsupported type + UnrealType = PropertyClass->GetName(); + } + + FString ArrayString = bIsArrayProperty ? " array" : ""; + FString Name = DumpedProperty->GetName(); + //FString DisplayName = DumpedProperty->GetDisplayNameText().ToString().Replace(TEXT(" "), TEXT("")); + FString DisplayName = DumpedProperty->GetDisplayNameText().ToString(); + + if (HoudiniType.IsEmpty() || HoudiniTupleSize < 1) + { + // The property is not supported + // Just show the unreal names / types + HOUDINI_LOG_WARNING(TEXT("%sunreal_uproperty_%s : %s (%s) - UE TYPE: %s%s - unsupported."),*Offset, *Name, *Name, *DisplayName, *UnrealType, *ArrayString); + + // return true to skip diving into certain known unsupported struct + bool bReturn = false; + if (UnrealType.StartsWith("Guid")) + bReturn = true; + + return bReturn; + } + + // The property is supported + // Show both the unreal and houdini names / types + FString TupleString; + if (HoudiniTupleSize > 1) + { + if(HoudiniTupleSize < 5) + TupleString = FString::FromInt(HoudiniTupleSize); + else + TupleString = "(tuple " + FString::FromInt(HoudiniTupleSize) + ")"; + } + + HOUDINI_LOG_MESSAGE(TEXT("%sunreal_uproperty_%s : %s (%s) - UE TYPE: %s%s - H TYPE: %s%s."), *Offset, *Name, *Name, *DisplayName, *UnrealType, *ArrayString, *HoudiniType, *TupleString); +#endif + + return true; +} + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniGenericAttribute.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniGenericAttribute.generated.h" + +UENUM() +enum class EAttribStorageType : int8 +{ + Invalid = -1, + + INT = 0, + INT64 = 1, + FLOAT = 2, + FLOAT64 = 3, + STRING = 4 +}; + +UENUM() +enum class EAttribOwner : int8 +{ + Invalid = -1, + + Vertex, + Point, + Prim, + Detail, +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniGenericAttributeChangedProperty +{ + GENERATED_USTRUCT_BODY() + + FHoudiniGenericAttributeChangedProperty(); + + FHoudiniGenericAttributeChangedProperty(const FHoudiniGenericAttributeChangedProperty& InOther); + + FHoudiniGenericAttributeChangedProperty(UObject* InObject, FEditPropertyChain& InPropertyChain, FProperty* InProperty); + + void operator=(const FHoudiniGenericAttributeChangedProperty& InOther); + + /** The object from where to follow the PropertyChain to the changed property. */ + UPROPERTY() + TObjectPtr Object; + + /** The property chain, relative to Object, for the changed property. */ + FEditPropertyChain PropertyChain; + + /** The changed property. */ + FProperty* Property; +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniGenericAttribute +{ + GENERATED_USTRUCT_BODY() + + typedef TFunction FFindPropertyFunctionType; + + UPROPERTY() + FString AttributeName; + + UPROPERTY() + EAttribStorageType AttributeType = EAttribStorageType::Invalid; + UPROPERTY() + EAttribOwner AttributeOwner = EAttribOwner::Invalid; + + UPROPERTY() + int32 AttributeCount = -1; + UPROPERTY() + int32 AttributeTupleSize = -1; + + UPROPERTY() + TArray DoubleValues; + UPROPERTY() + TArray IntValues; + UPROPERTY() + TArray StringValues; + + // Accessors + double GetDoubleValue(int32 index = 0) const; + void GetDoubleTuple(TArray& TupleValues, int32 index = 0) const; + + int64 GetIntValue(int32 index = 0) const; + void GetIntTuple(TArray& TupleValues, int32 index = 0) const; + + FString GetStringValue(int32 index = 0) const; + void GetStringTuple(TArray& TupleValues, int32 index = 0) const; + + bool GetBoolValue(int32 index = 0) const; + void GetBoolTuple(TArray& TupleValues, int32 index = 0) const; + + void* GetData(); + + // Mutators + void SetDoubleValue(double InValue, int32 index = 0); + void SetDoubleTuple(const TArray& InTupleValues, int32 index = 0); + + void SetIntValue(int64 InValue, int32 index = 0); + void SetIntTuple(const TArray& InTupleValues, int32 index = 0); + + void SetStringValue(const FString& InValue, int32 index = 0); + void SetStringTuple(const TArray& InTupleValues, int32 index = 0); + + void SetBoolValue(bool InValue, int32 index = 0); + void SetBoolTuple(const TArray& InTupleValues, int32 index = 0); + + // Tries to find/update a single property on an Object + static bool UpdatePropertyAttributeOnObject( + UObject* InObject, + const FHoudiniGenericAttribute& InPropertyAttribute, + const int32& AtIndex = 0, + const bool bInDeferPostPropertyChangedEvents=false, + TArray* OutChangedProperties=nullptr, + const FFindPropertyFunctionType& InFindPropertyFunction=nullptr); + + // Tries to find a Uproperty by name/label on an object + // FoundPropertyObject will be the object that actually contains the property + // and can be different from InObject if the property is nested. + static bool FindPropertyOnObject( + UObject* InObject, + const FString& InPropertyName, + FEditPropertyChain& InPropertyChain, + FProperty*& OutFoundProperty, + UObject*& OutFoundPropertyObject, + void*& OutContainer, + bool& OutExactPropertyFound, + bool bDumpAttributes); + + // Modifies the value of a found Property + static bool ModifyPropertyValueOnObject( + UObject* InObject, + const FHoudiniGenericAttribute& InGenericAttribute, + FEditPropertyChain& InPropertyChain, + FProperty* FoundProperty, + void* InContainer, + const int32& AtIndex = 0, + const bool bInDeferPostPropertyChangedEvents=false, + TArray* OutChangedProperties=nullptr); + + // Gets the value of a found Property and sets it in the appropriate + // array and index in InGenericAttribute. + static bool GetPropertyValueFromObject( + UObject* InObject, + FProperty* InFoundProperty, + void* InContainer, + FHoudiniGenericAttribute& InGenericAttribute, + const int32& InAtIndex = 0); + + // Helper: determines a valid tuple size and storage type for a Houdini attribute from an Unreal FProperty + static bool GetAttributeTupleSizeAndStorageFromProperty( + UObject* InObject, + FProperty* InFoundProperty, + void* InContainer, + int32& OutAttributeTupleSize, + EAttribStorageType& OutAttributeStorageType); + + // Recursive search for a given property on a UObject + static bool TryToFindProperty( + void* InContainer, + UStruct* InStruct, + const FString& InPropertyName, + FEditPropertyChain& InPropertyChain, + FProperty*& OutFoundProperty, + bool& bOutExactPropertyHasBeenFound, + void*& OutContainer, + bool bDumpAttributes); + + // Helper to call PostEditChangePropertyChain on InObject for the InPropertyChain. + static bool HandlePostEditChangeProperty( + UObject* InObject, + FEditPropertyChain& InPropertyChain, + FProperty* InProperty); + + // Helper function for dumping generic property attributre details for a given property + // returns true if the property is supported + static bool DumpGenericAttributeForProperty(FProperty* InProperty, int32 InPropChainNumber); + + +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniGeoAsset.cpp`: + +```cpp +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniGeoAsset.h`: + +```h +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniGeoPartObject.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniGeoPartObject.h" + +// +FHoudiniGeoPartObject::FHoudiniGeoPartObject() + : AssetId(-1) + , AssetName(TEXT("")) + , ObjectId(-1) + , ObjectName(TEXT("")) + , GeoId(-1) + , PartId(-1) + , PartName(TEXT("")) + , bHasCustomPartName(false) + , TransformMatrix(FMatrix::Identity) + , NodePath(TEXT("")) + , Type(EHoudiniPartType::Invalid) + , InstancerType(EHoudiniInstancerType::Invalid) + , VolumeName(TEXT("")) + , bHasEditLayers(false) + , VolumeTileIndex(-1) + , bIsVisible(false) + , bIsEditable(false) + , bIsTemplated(false) + , bIsInstanced(false) + , bHasGeoChanged(true) + , bHasPartChanged(true) + , bHasTransformChanged(true) + , bHasMaterialsChanged(true) + , bLoaded(false) + , bKeepTags(false) +{ + +} + +bool +FHoudiniGeoPartObject::IsValid() const +{ + return (ObjectId >= 0 && GeoId >= 0 && PartId >= 0); +} + +bool +FHoudiniGeoPartObject::operator==(const FHoudiniGeoPartObject & InGeoPartObject) const +{ + // TODO: split?? + return Equals(InGeoPartObject, true); +} + +bool +FHoudiniGeoPartObject::Equals(const FHoudiniGeoPartObject & GeoPartObject, const bool& bIgnoreSplit) const +{ + // TODO: This will likely need some improvement! + + // See if objects / geo / part ids match + bool MatchingIDs = true; + if (ObjectId != GeoPartObject.ObjectId || GeoId != GeoPartObject.GeoId || PartId != GeoPartObject.PartId) + MatchingIDs = false; + + // See if the type matches + bool MatchingType = (Type == GeoPartObject.Type); + // Both IDs and type match, consider the two HGPO as equals + if (MatchingIDs && MatchingType) + return true; + + // Both IDs and type do not match, consider the two HGPOs as different + if (!MatchingIDs && !MatchingType) + return false; + + // If only the ID dont match we can do some further checks + + // If one of the two HGPO has been loaded + if ((bLoaded && !GeoPartObject.bLoaded) + || (!bLoaded && GeoPartObject.bLoaded)) + { + // For loaded HGPOs, part names should be a sufficent comparison + if (PartName.Equals(GeoPartObject.PartName)) + return true; + } + + // TODO: This was causing issues somehow with tiled landscapes + // ... if not, compare by names + if(!MatchingIDs) + return CompareNames(GeoPartObject, bIgnoreSplit); + + return false; +} + +void +FHoudiniGeoPartObject::SetCustomPartName(const FString & InName) +{ + if (InName.IsEmpty()) + return; + + PartName = InName; + bHasCustomPartName = true; +} + +bool +FHoudiniGeoPartObject::CompareNames(const FHoudiniGeoPartObject & HoudiniGeoPartObject, const bool& bIgnoreSplit) const +{ + //TODO: AssetName? + + // Object, part and split names must match + if (!ObjectName.Equals(HoudiniGeoPartObject.ObjectName) + || !PartName.Equals(HoudiniGeoPartObject.PartName)) + { + return false; + } + + /* + // Split should also match if we dont ignore it + if (!bIgnoreSplit && !SplitName.Equals(HoudiniGeoPartObject.SplitName)) + { + return false; + } + */ + + return true; +} + +FString +FHoudiniGeoPartObject::HoudiniPartTypeToString(const EHoudiniPartType& InType) +{ + FString OutTypeStr; + switch (InType) + { + case EHoudiniPartType::Mesh: + OutTypeStr = TEXT("Mesh"); + break; + case EHoudiniPartType::Instancer: + OutTypeStr = TEXT("Instancer"); + break; + case EHoudiniPartType::Curve: + OutTypeStr = TEXT("Curve"); + break; + case EHoudiniPartType::Volume: + OutTypeStr = TEXT("Volume"); + break; + case EHoudiniPartType::LandscapeSpline: + OutTypeStr = TEXT("LandscapeSpline"); + break; + + default: + case EHoudiniPartType::Invalid: + OutTypeStr = TEXT("Invalid"); + break; + } + + return OutTypeStr; +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniGeoPartObject.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniEngineRuntimeCommon.h" +#include "HoudiniGenericAttribute.h" + +#include "HoudiniGeoPartObject.generated.h" + +UENUM() +enum class EHoudiniGeoType : uint8 +{ + Invalid, + + Default, + Intermediate, + Input, + Curve +}; + +UENUM() +enum class EHoudiniPartType : uint8 +{ + Invalid, + + Mesh, + Instancer, + Curve, + Volume, + DataTable, + LandscapeSpline, + SkeletalMeshShape, // Rest Geometry for the skeletal mesh + SkeletalMeshPose, // Capture Pose for the skeletal mesh + MotionClip, + SkeletalMeshPhysAsset, + Cop, + PCG +}; + +UENUM() +enum class EHoudiniInstancerType : uint8 +{ + Invalid, + + ObjectInstancer, + PackedPrimitive, + AttributeInstancer, + OldSchoolAttributeInstancer, + GeometryCollection, + SkeletalMesh, + MotionClip +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniObjectInfo +{ + GENERATED_USTRUCT_BODY() + + FString Name = TEXT(""); + + int32 NodeId = -1; + int32 ObjectToInstanceID = -1; + + bool bHasTransformChanged = false; + bool bHaveGeosChanged = false; + bool bIsVisible = false; + bool bIsInstancer = false; + bool bIsInstanced = false; + + int32 GeoCount = -1; +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniGeoInfo +{ + GENERATED_USTRUCT_BODY() + + EHoudiniGeoType Type = EHoudiniGeoType::Invalid; + FString Name = TEXT(""); + int32 NodeId = -1; + + bool bIsEditable = false; + bool bIsTemplated = false; + bool bIsDisplayGeo = false; + bool bHasGeoChanged = false; + bool bHasMaterialChanged = false; + + int32 PartCount = -1; + int32 PointGroupCount = -1; + int32 PrimitiveGroupCount = -1; +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniPartInfo +{ + GENERATED_USTRUCT_BODY() + + int32 PartId = -1; + FString Name = TEXT(""); + + EHoudiniPartType Type = EHoudiniPartType::Invalid; + + int32 FaceCount = -1; + int32 VertexCount = -1; + int32 PointCount = -1; + + int32 PointAttributeCounts = -1; + int32 VertexAttributeCounts = -1; + int32 PrimitiveAttributeCounts = -1; + int32 DetailAttributeCounts = -1; + + bool bIsInstanced = false; + + int32 InstancedPartCount = -1; + int32 InstanceCount = -1; + + bool bHasChanged = false; +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniVolumeInfo +{ + GENERATED_USTRUCT_BODY() + + FString Name = TEXT(""); + bool bIsVDB = false; // replaces VolumeType Type; + + int32 TupleSize = -1; + bool bIsFloat = false; // replaces StorageType StorageType; + int32 TileSize = -1; + + FTransform Transform = FTransform::Identity; + bool bHasTaper = false; + + int32 XLength = -1; + int32 YLength = -1; + int32 ZLength = -1; + + int32 MinX = -1; + int32 MinY = -1; + int32 MinZ = -1; + + float XTaper = 0.0f; + float YTaper = 0.0f; +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniCurveInfo +{ + GENERATED_USTRUCT_BODY() + + EHoudiniCurveType Type = EHoudiniCurveType::Invalid; + + int32 CurveCount = -1; + int32 VertexCount = -1; + int32 KnotCount = -1; + + bool bIsPeriodic = false; + bool bIsRational = false; + + int32 Order = -1; + + bool bHasKnots = false; +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniMeshSocket +{ + GENERATED_USTRUCT_BODY() + + // Equality operator, used by containers + bool operator==(const FHoudiniMeshSocket& InSocket) const + { + return Transform.Equals(InSocket.Transform) + && Name == InSocket.Name + && Actor == InSocket.Actor + && Tag == InSocket.Tag; + } + + // Members + FTransform Transform = FTransform::Identity; + FString Name = TEXT("Socket"); + FString Actor = FString(); + FString Tag = FString(); +}; + +/* +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniSplitDataCache +{ + GENERATED_USTRUCT_BODY() + + UPROPERTY() + FString SplitName; + //UPROPERTY() + //FHoudiniOutputObjectIdentifier SplitIdentifier; + //EHoudiniSplitType SplitType; + + UPROPERTY() + TArray Positions; + UPROPERTY() + TArray Indices; + + UPROPERTY() + TArray Normals; + UPROPERTY() + TArray Tangents; + UPROPERTY() + TArray Binormals; + + UPROPERTY() + TArray Colors; + + //UPROPERTY() + //TArray> UVs; + + //TArray EdgeHardnesses; + UPROPERTY() + TArray FaceSmoothingMasks; + UPROPERTY() + TArray LightMapResolutions; + + UPROPERTY() + TArray MaterialIndices; + UPROPERTY() + TArray Materials; + + UPROPERTY() + float lod_screensize; + + UPROPERTY() + FKAggregateGeom AggregateCollisions; +}; +*/ + + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniGeoPartObject +{ +public: + + GENERATED_USTRUCT_BODY() + + FHoudiniGeoPartObject(); + + // Indicates if this HGPO is valid + bool IsValid() const; + + // Equality operator, used by containers + bool operator==(const FHoudiniGeoPartObject & GeoPartObject) const; + + // Checks equality, with the possibility to ignore the HGPO's splits + bool Equals(const FHoudiniGeoPartObject & GeoPartObject, const bool& bIgnoreSplit) const; + + // Comparison based on object/part/split name. + bool CompareNames(const FHoudiniGeoPartObject & HoudiniGeoPartObject, const bool& bIgnoreSplit) const; + + void SetCustomPartName(const FString & InName); + + static FString HoudiniPartTypeToString(const EHoudiniPartType& InType); + +public: + + // NodeId of corresponding HAPI Asset. + UPROPERTY() + int32 AssetId; + + // Name of corresponding HDA. + UPROPERTY() + FString AssetName; + + // NodeId of corresponding HAPI Object. + UPROPERTY() + int32 ObjectId; + + // Name of associated object. + UPROPERTY() + FString ObjectName; + + // NodeId of corresponding HAPI Geo. + UPROPERTY() + int32 GeoId; + + // PartId of corresponding HAPI Part. + UPROPERTY() + int32 PartId; + + // Name of associated part. + UPROPERTY() + FString PartName; + + UPROPERTY() + bool bHasCustomPartName; + + /* + // Type of the split. + UPROPERTY() + EHoudiniSplitType SplitType; + + // Index of a split. In most cases this will be 0. + UPROPERTY() + int32 SplitIndex; + + // Name of group which was used for splitting, empty if there's none. + UPROPERTY() + FString SplitName; + */ + + // Split groups handled by this HGPO + UPROPERTY() + TArray SplitGroups; + + // Transform of this geo part object. + UPROPERTY() + FTransform TransformMatrix; + + // Path to the corresponding node + UPROPERTY() + FString NodePath; + + // Indicates the type of the referenced object + UPROPERTY() + EHoudiniPartType Type; + + // Indicates the type of instancer + UPROPERTY() + EHoudiniInstancerType InstancerType; + + // + UPROPERTY() + FString VolumeName; + + UPROPERTY() + bool bHasEditLayers = false; + + // Name of edit layer + UPROPERTY() + FString VolumeLayerName; + + // + UPROPERTY() + int32 VolumeTileIndex; + + UPROPERTY() + FString InstancerName; + + // Is set to true when referenced object is visible. + UPROPERTY() + bool bIsVisible; + + // Is set to true when referenced object is editable. + UPROPERTY() + bool bIsEditable; + + // Is set to true when referenced object is templated. + UPROPERTY() + bool bIsTemplated; + + // Is set to true when the referenced object is instanced. + UPROPERTY() + bool bIsInstanced; + + // Indicates the parent geo has changed and needs to be rebuilt + UPROPERTY() + bool bHasGeoChanged; + + // Indicates the part has changed and needs to be rebuilt + UPROPERTY() + bool bHasPartChanged; + + // Indicates only the transform needs to be updated + UPROPERTY() + bool bHasTransformChanged; + + // Indicates only the material needs to be updated + UPROPERTY() + bool bHasMaterialsChanged; + + // Indicates this object has been loaded + bool bLoaded; + + // We also keep a cache of the various info objects + // That we've extracted from HAPI + + // ObjectInfo cache + FHoudiniObjectInfo ObjectInfo; + // GeoInfo cache + FHoudiniGeoInfo GeoInfo; + // PartInfo cache + FHoudiniPartInfo PartInfo; + // VolumeInfo cache + FHoudiniVolumeInfo VolumeInfo; + // CurveInfo cache + FHoudiniCurveInfo CurveInfo; + + // Stores the Mesh Sockets found for a given HGPO + UPROPERTY() + TArray AllMeshSockets; + + // Cache of this HGPO split data + //TArray SplitCache; + + // Cached generic properties that we can pass through to the baking process + UPROPERTY() + TArray GenericPropertyAttributes; + + UPROPERTY() + bool bKeepTags; +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniHandleComponent.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniHandleComponent.h" + +#include "HoudiniEngineRuntimePrivatePCH.h" + +#include "HoudiniParameter.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterChoice.h" +#include "HoudiniPluginSerializationVersion.h" +#include "HoudiniRuntimeSettings.h" + +#include "Runtime/Launch/Resources/Version.h" + +#include "UObject/DevObjectVersion.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + #include "UObject/Linker.h" +#endif + +#include "Serialization/CustomVersion.h" +#include "HAL/IConsoleManager.h" + +static TAutoConsoleVariable CVarHoudiniEngineHandleTickTime( + TEXT("HoudiniEngine.HandleTickTime"), + 0.5, + TEXT("The frequency (in s) at which handles will be updated.\n") + TEXT("<= 0.0: Disable updates\n") + TEXT("0.5: Default\n") +); + +void +UHoudiniHandleComponent::Serialize(FArchive& Ar) +{ + int64 InitialOffset = Ar.Tell(); + Ar.UsingCustomVersion(FHoudiniCustomSerializationVersion::GUID); + + bool bLegacyComponent = false; + if (Ar.IsLoading()) + { + int32 Ver = Ar.CustomVer(FHoudiniCustomSerializationVersion::GUID); + if (Ver < VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_V2_BASE && Ver >= VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_BASE) + { + bLegacyComponent = true; + } + } + + if (bLegacyComponent) + { + HOUDINI_LOG_WARNING(TEXT("Loading deprecated version of UHoudiniHandleComponent : serialized data will be skipped.")); + + Super::Serialize(Ar); + + // Skip v1 Serialized data + if (FLinker* Linker = Ar.GetLinker()) + { + int32 const ExportIndex = this->GetLinkerIndex(); + FObjectExport& Export = Linker->ExportMap[ExportIndex]; + Ar.Seek(InitialOffset + Export.SerialSize); + return; + } + } + else + { + // Normal v2 serialization + Super::Serialize(Ar); + } +} + +UHoudiniHandleParameter::UHoudiniHandleParameter(const FObjectInitializer & ObjectInitializer) + :Super(ObjectInitializer) +{}; + +UHoudiniHandleComponent::UHoudiniHandleComponent(const FObjectInitializer & ObjectInitializer) + :Super(ObjectInitializer) +{ + bNeedToUpdateTransform = false; + bWantsOnUpdateTransform = true; + LastSentTransform = FTransform::Identity; +}; + + +bool +UHoudiniHandleParameter::Bind(float & OutValue, const char * CmpName, int32 InTupleIdx, + const FString & HandleParmName, UHoudiniParameter* Parameter) +{ + if (!Parameter) + return false; + + if (HandleParmName != CmpName) + return false; + + UHoudiniParameterFloat* FloatParameter = Cast(Parameter); + + if (!FloatParameter) + return false; + + AssetParameter = Parameter; + + if (FloatParameter) + { + // It is possible that the handle param is bound to a single tuple param. + // Ignore the preset tuple index if that's the case or we'll crash. + if (Parameter->GetTupleSize() <= InTupleIdx) + InTupleIdx = 0; + + auto Optional = FloatParameter->GetValue(InTupleIdx); + if (Optional.IsSet()) + { + TupleIndex = InTupleIdx; + OutValue = Optional.GetValue(); + return true; + } + } + + return false; +} + +bool +UHoudiniHandleParameter::Bind(TSharedPtr & OutValue, const char * CmpName, + int32 InTupleIdx, const FString & HandleParmName, UHoudiniParameter* Parameter) +{ + if (!Parameter) + return false; + + if (HandleParmName != CmpName) + return false; + + UHoudiniParameterChoice* ChoiceParameter = Cast(Parameter); + + if (!ChoiceParameter) + return false; + + AssetParameter = Parameter; + + if (ChoiceParameter) + { + // It is possible that the handle param is bound to a single tuple param. + // Ignore the preset tuple index if that's the case or we'll crash. + if (Parameter->GetTupleSize() <= InTupleIdx) + InTupleIdx = 0; + + auto Optional = ChoiceParameter->GetValue(InTupleIdx); + if (Optional.IsSet()) + { + TupleIndex = InTupleIdx; + OutValue = Optional.GetValue(); + return true; + } + } + + return false; +} + +TSharedPtr +UHoudiniHandleParameter::Get(TSharedPtr DefaultValue) const +{ + UHoudiniParameterChoice* ChoiceParameter = Cast(AssetParameter); + if (ChoiceParameter) + { + auto Optional = ChoiceParameter->GetValue(TupleIndex); + if (Optional.IsSet()) + return Optional.GetValue(); + } + + return DefaultValue; +} + +UHoudiniHandleParameter & +UHoudiniHandleParameter::operator=(float Value) +{ + UHoudiniParameterFloat* FloatParameter = Cast(AssetParameter); + if (FloatParameter) + { + FloatParameter->SetValue(Value, TupleIndex); + FloatParameter->MarkChanged(true); + } + + return *this; +} + +void +UHoudiniHandleComponent::InitializeHandleParameters() +{ + if (XformParms.Num() < int32(EXformParameter::COUNT)) + { + XformParms.Empty(); + for (int32 n = 0; n < int32(EXformParameter::COUNT); ++n) + { + UHoudiniHandleParameter* XformHandle = NewObject(this, UHoudiniHandleParameter::StaticClass()); + XformParms.Add(XformHandle); + } + } + + if (!RSTParm) + { + RSTParm = NewObject(this, UHoudiniHandleParameter::StaticClass()); + } + + if (!RotOrderParm) + { + RotOrderParm = NewObject(this, UHoudiniHandleParameter::StaticClass()); + } +} + +bool +UHoudiniHandleComponent::CheckHandleValid() const +{ + if (XformParms.Num() < int32(EXformParameter::COUNT)) + return false; + + for (auto& XformParm : XformParms) + { + if (!XformParm) + return false; + } + + if (!RSTParm) + return false; + + if (!RotOrderParm) + return false; + + return true; +} + +FBox +UHoudiniHandleComponent::GetBounds() const +{ + FBox BoxBounds(ForceInitToZero); + return BoxBounds + GetComponentLocation(); +} + + +void +UHoudiniHandleComponent::OnUpdateTransform(EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport) +{ + Super::OnUpdateTransform(UpdateTransformFlags, Teleport); + +#if WITH_EDITOR + //if (UpdateTransformFlags != EUpdateTransformFlags::PropagateFromParent) + { + FTransform NewTransform = GetRelativeTransform(); + if (!NewTransform.Equals(LastSentTransform)) + { + bNeedToUpdateTransform = true; + dLastTransformUpdateTime = FPlatformTime::Seconds(); + } + } +#endif +} + +bool +UHoudiniHandleComponent::IsTransformUpdateNeeded() +{ + if (!bNeedToUpdateTransform) + return false; + + // Use a timer to reduce the frequency of handle updates + double dHandleTick = CVarHoudiniEngineHandleTickTime.GetValueOnAnyThread(); + if (dHandleTick < 0) + return false; + + double dNow = FPlatformTime::Seconds(); + double dTimeDiff = dNow - dLastTransformUpdateTime; + if (dTimeDiff < 0.0 || dTimeDiff < dHandleTick) + return false; + + return true; +} +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniHandleComponent.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "Components/SceneComponent.h" + +#include "HoudiniHandleComponent.generated.h" + +class UHoudiniParameter; + +UENUM() +enum class EXformParameter : uint8 +{ + TX, TY, TZ, + RX, RY, RZ, + SX, SY, SZ, + COUNT +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniHandleParameter : public UObject +{ +public: + GENERATED_UCLASS_BODY() + + UPROPERTY() + TObjectPtr AssetParameter; + + UPROPERTY() + int32 TupleIndex; + + + bool Bind( + float & OutValue, + const char * CmpName, + int32 InTupleIdx, + const FString & HandleParmName, + UHoudiniParameter* Parameter); + + bool Bind( + TSharedPtr & OutValue, + const char * CmpName, + int32 InTupleIdx, + const FString & HandleParmName, + UHoudiniParameter* Parameter); + + TSharedPtr Get(TSharedPtr DefaultValue) const; + + UHoudiniHandleParameter & operator=(float Value); + +}; + +UENUM() +enum class EHoudiniHandleType : uint8 +{ + Xform, + Bounder, + Unsupported +}; + +UCLASS(Blueprintable, BlueprintType, EditInlineNew, config = Engine, meta = (BlueprintSpawnableComponent)) +class HOUDINIENGINERUNTIME_API UHoudiniHandleComponent : public USceneComponent +{ +public: + + friend class UHoudiniAssetComponent; + + friend class FHoudiniHandleComponentVisualizer; + + friend class FHoudiniEditorEquivalenceUtils; + + GENERATED_UCLASS_BODY() + + virtual void Serialize(FArchive & Ar) override; + + FString GetHandleName() const { return HandleName; }; + EHoudiniHandleType GetHandleType() const { return HandleType; }; + + void SetHandleName(const FString& InHandleName) { HandleName = InHandleName; }; + void SetHandleType(const EHoudiniHandleType& InHandleType) { HandleType = InHandleType; }; + + // Equality, consider two handle equals if they have the same name, type, tuple size and disabled status + bool operator==(const UHoudiniHandleComponent& other) const + { + return (HandleType == other.HandleType && HandleName.Equals(other.HandleName)); + } + + bool Matches(const UHoudiniHandleComponent& other) const { return (*this == other); }; + + void InitializeHandleParameters(); + + bool CheckHandleValid() const; + + FBox GetBounds() const; + + virtual void OnUpdateTransform(EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport) override; + + bool IsTransformUpdateNeeded(); + +public: + UPROPERTY() + TArray> XformParms; + + UPROPERTY() + TObjectPtr RSTParm; + + UPROPERTY() + TObjectPtr RotOrderParm; + + UPROPERTY() + FTransform LastSentTransform; + + bool bNeedToUpdateTransform; + +private: + UPROPERTY() + EHoudiniHandleType HandleType; + + UPROPERTY() + FString HandleName; + + double dLastTransformUpdateTime; +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniInput.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniInput.h" + +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniOutput.h" +#include "HoudiniSplineComponent.h" +#include "HoudiniAsset.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniAssetBlueprintComponent.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputManager.h" +#include "UnrealObjectInputRuntimeUtils.h" +#include "LandscapeSplineActor.h" + +#include "EngineUtils.h" +#include "Engine/Brush.h" +#include "Engine/Engine.h" +#include "Engine/DataTable.h" +#include "Model.h" +#include "Engine/StaticMesh.h" +#include "Engine/SkeletalMesh.h" +#include "UObject/UObjectGlobals.h" +#include "FoliageType_InstancedStaticMesh.h" +#include "Animation/AnimSequence.h" +#include "Components/SplineComponent.h" +#include "Components/StaticMeshComponent.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "Landscape.h" +#include "LandscapeInfo.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollection/GeometryCollectionComponent.h" + #include "GeometryCollection/GeometryCollectionObject.h" +#else + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionComponent.h" + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionObject.h" +#endif + + +#if WITH_EDITOR + #include "Kismet2/BlueprintEditorUtils.h" + #include "Kismet2/KismetEditorUtilities.h" +#endif + +// +UHoudiniInput::UHoudiniInput() + : Type(EHoudiniInputType::Invalid) + , PreviousType(EHoudiniInputType::Invalid) + , AssetNodeId(-1) + , InputNodeId(-1) + , InputIndex(0) + , ParmId(-1) + , bIsObjectPathParameter(false) + , bIsCOPInput(false) + , bHasChanged(false) + , bPackBeforeMerge(false) + , bDirectlyConnectHdas(true) + , bExportOptionsMenuExpanded(false) + , bGeometryInputsMenuExpanded(true) + , bLandscapeOptionsMenuExpanded(false) + , bWorldInputsMenuExpanded(true) + , bCurveInputsMenuExpanded(true) + , bCurvePointSelectionMenuExpanded(true) + , bCurvePointSelectionUseAbsLocation(false) + , bCurvePointSelectionUseAbsRotation(false) + , bCookOnCurveChanged(true) + , bStaticMeshChanged(false) + , bInputAssetConnectedInHoudini(false) + , DefaultCurveOffset(0.f) + , bIsWorldInputBoundSelector(false) + , bWorldInputBoundSelectorAutoUpdate(false) + , bWorldInputAutoUpdate(true) + , bCanDeleteHoudiniNodes(true) +{ + Name = TEXT(""); + Label = TEXT(""); + SetFlags(RF_Transactional); + + // Geometry inputs always have one null default object + GeometryInputObjects.Add(nullptr); + + InputSettings.KeepWorldTransform = GetDefaultXTransformType(); + +#if WITH_EDITORONLY_DATA + bLandscapeUIAdvancedIsExpanded = false; +#endif + + CachedBounds.Init(); +} + +void +UHoudiniInput::BeginDestroy() +{ + InvalidateData(); + + // DO NOT MANUALLY DESTROY OUR INPUT OBJECTS! + // This messes up unreal's Garbage collection and would cause crashes on duplication + + // Mark all our input objects for destruction + ForAllHoudiniInputObjectArrays([](TArray>& ObjectArray) { + ObjectArray.Empty(); + }); + + Super::BeginDestroy(); +} + +#if WITH_EDITOR +void UHoudiniInput::PostEditUndo() +{ + Super::PostEditUndo(); + TArray>* InputObjectsPtr = GetHoudiniInputObjectArray(Type); + if (!InputObjectsPtr) + return; + + MarkChanged(true); + bool bBlueprintStructureChanged = false; + + if (HasInputTypeChanged()) + { + // If the input type has changed on undo, previousType becomes new type + /* This does not work properly, see the corresponding part in FHoudiniInputDetails::AddInputTypeComboBox(...), after Transaction(... + ) + { + EHoudiniInputType NewType = PreviousType; + SetInputType(NewType); + } + */ + EHoudiniInputType Temp = Type; + Type = PreviousType; + PreviousType = EHoudiniInputType::Invalid; + + // If the undo action caused input type changing, treat it as a regular type changing + // after set up the new and prev types properly + SetInputType(Temp, bBlueprintStructureChanged); + } + else + { + if (Type == EHoudiniInputType::World) + { + if (WorldInputObjects.Num() == 0 && InputNodeId >= 0) + { + // If the ref counted input system is being used, we must not delete nodes managed by the system + TSet ManagedNodeIds; + { + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (Manager) + { + TArray ManagedNodeIdArray; + if (Manager->GetAllHAPINodeIds(ManagedNodeIdArray)) + ManagedNodeIds.Append(ManagedNodeIdArray); + } + } + + for (auto & NextNodeId : CreatedDataNodeIds) + { + if (ManagedNodeIds.Contains(NextNodeId)) + continue; + + if (bCanDeleteHoudiniNodes) + FHoudiniEngineRuntime::Get().MarkNodeIdAsPendingDelete(NextNodeId, true); + } + + CreatedDataNodeIds.Empty(); + + if (bCanDeleteHoudiniNodes) + MarkInputNodeAsPendingDelete(); + InputNodeId = -1; + } + } + + if (Type == EHoudiniInputType::Curve) + { + if (PreviousType != EHoudiniInputType::Curve) + { + for (auto& NextInput : *GetHoudiniInputObjectArray(Type)) + { + UHoudiniInputHoudiniSplineComponent* SplineInput = Cast< UHoudiniInputHoudiniSplineComponent>(NextInput); + if (!IsValid(SplineInput)) + continue; + + UHoudiniSplineComponent * HoudiniSplineComponent = SplineInput->GetCurveComponent(); + if (!IsValid(HoudiniSplineComponent)) + continue; + + UHoudiniCookable* OuterCookable = Cast(GetOuter()); + USceneComponent* OuterComponent = OuterCookable ? OuterCookable->GetComponent() : Cast(GetOuter()); + + // Attach the new Houdini spline component to it's owner + if (HoudiniSplineComponent->GetOwner()) + HoudiniSplineComponent->RegisterComponent(); + HoudiniSplineComponent->AttachToComponent(OuterComponent, FAttachmentTransformRules::KeepRelativeTransform); + HoudiniSplineComponent->SetVisibility(true, true); + HoudiniSplineComponent->SetHoudiniSplineVisible(true); + HoudiniSplineComponent->SetHiddenInGame(false, true); + HoudiniSplineComponent->MarkChanged(true); + } + return; + } + bool bUndoDelete = false; + bool bUndoInsert = false; + bool bUndoDeletedObjArrayEmptied = false; + + TArray childActor; + UHoudiniCookable* OuterHC = Cast(GetOuter()); + if (IsValid(OuterHC)) + { + USceneComponent* OuterComponent = OuterHC->GetComponent(); + if (IsValid(OuterComponent)) + childActor = OuterComponent->GetAttachChildren(); + } + + // Undo delete input objects action + for (int Index = 0; Index < GetNumberOfInputObjects(); ++Index) + { + UHoudiniInputObject* InputObject = (*InputObjectsPtr)[Index]; + if (!IsValid(InputObject)) + continue; + + UHoudiniInputHoudiniSplineComponent * HoudiniSplineInputObject = Cast(InputObject); + if (!IsValid(HoudiniSplineInputObject)) + continue; + + UHoudiniSplineComponent* SplineComponent = HoudiniSplineInputObject->GetCurveComponent(); + if (!IsValid(SplineComponent)) + continue; + + // If the last change deleted this curve input, recreate this Houdini Spline input. + if (!SplineComponent->GetAttachParent()) + { + bUndoDelete = true; + + if (!bUndoDeletedObjArrayEmptied) + LastUndoDeletedInputs.Empty(); + + bUndoDeletedObjArrayEmptied = true; + + UHoudiniSplineComponent * ReconstructedSpline = NewObject( + GetOuter(), UHoudiniSplineComponent::StaticClass()); + + if (!IsValid(ReconstructedSpline)) + continue; + + ReconstructedSpline->SetFlags(RF_Transactional); + ReconstructedSpline->CopyHoudiniData(SplineComponent); + + UHoudiniInputObject * ReconstructedInputObject = UHoudiniInputHoudiniSplineComponent::Create( + ReconstructedSpline, GetOuter(), ReconstructedSpline->GetHoudiniSplineName(), InputSettings); + UHoudiniInputHoudiniSplineComponent *ReconstructedHoudiniSplineInput = (UHoudiniInputHoudiniSplineComponent*)ReconstructedInputObject; + (*InputObjectsPtr)[Index] = ReconstructedHoudiniSplineInput; + + if (ReconstructedSpline->GetOwner()) + ReconstructedSpline->RegisterComponent(); + ReconstructedSpline->SetFlags(RF_Transactional); + + CreateHoudiniSplineInput(ReconstructedHoudiniSplineInput, true, true, bBlueprintStructureChanged); + + // Cast the reconstructed Houdini Spline Input to a generic HoudiniInput object. + UHoudiniInputObject * ReconstructedHoudiniInput = Cast(ReconstructedHoudiniSplineInput); + + LastUndoDeletedInputs.Add(ReconstructedHoudiniInput); + // Reset the LastInsertedInputsArray for redoing this undo action. + } + } + + if (bUndoDelete) + return; + + // Undo insert input objects action + for (int Index = 0; Index < LastInsertedInputs.Num(); ++Index) + { + bUndoInsert = true; + UHoudiniInputHoudiniSplineComponent* SplineInputComponent = LastInsertedInputs[Index]; + if (!IsValid(SplineInputComponent)) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = SplineInputComponent->GetCurveComponent(); + if (!IsValid(HoudiniSplineComponent)) + continue; + + HoudiniSplineComponent->DestroyComponent(); + } + + if (bUndoInsert) + return; + + for (int Index = 0; Index < LastUndoDeletedInputs.Num(); ++Index) + { + UHoudiniInputObject* NextInputObject = LastUndoDeletedInputs[Index]; + + UHoudiniInputHoudiniSplineComponent* SplineInputComponent = Cast(NextInputObject); + if (!IsValid(SplineInputComponent)) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = SplineInputComponent->GetCurveComponent(); + if (!IsValid(HoudiniSplineComponent)) + continue; + + FDetachmentTransformRules DetachTransRules(EDetachmentRule::KeepRelative, EDetachmentRule::KeepRelative, EDetachmentRule::KeepRelative, false); + HoudiniSplineComponent->DetachFromComponent(DetachTransRules); + + HoudiniSplineComponent->DestroyComponent(); + } + } + } + + if (bBlueprintStructureChanged) + { + UHoudiniCookable* OuterHC = Cast(GetOuter()); + if (IsValid(OuterHC)) + { + UHoudiniAssetComponent* OuterHAC = Cast(OuterHC->GetComponent()); + FHoudiniEngineRuntimeUtils::MarkBlueprintAsStructurallyModified(OuterHAC); + } + } +} +#endif + +void +UHoudiniInput::Serialize(FArchive& Ar) +{ + Super::Serialize(Ar); + + Ar.UsingCustomVersion(FHoudiniCustomSerializationVersion::GUID); + if (Ar.CustomVer(FHoudiniCustomSerializationVersion::GUID) < VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_INPUT_OBJECT_SETTINGS_STRUCT) + { + HOUDINI_LOG_WARNING(TEXT("Loading a deprecated input - its input settings will be lost")); + } + + // On load, change the input selection from old input types to + // the new input types (to Geometry or World) + // Move the selected objects to the appropriate array + if (Ar.IsLoading()) + { + int32 IntType = (int32)Type; + if ((IntType == 6) || (IntType == 7)) + { + // Deprecated Skeletal or geometry input - Change to a geometry input + HOUDINI_LOG_WARNING(TEXT("Loading a deprecated input - its input objects will be lost")); + Type = EHoudiniInputType::Geometry; + } + else if ((IntType == 3) || (IntType == 4)) + { + // Deprecated Landscape or Asset input - Change to a world input + HOUDINI_LOG_WARNING(TEXT("Loading a deprecated input - its input objects will be lost")); + Type = EHoudiniInputType::World; + } + } +} + +FBox +UHoudiniInput::GetBounds(UWorld * World) +{ + FBox BoxBounds(ForceInitToZero); + + // Prevent crash when World is null + if (!IsValid(World)) + return BoxBounds; + + // Return the cached bounds to prevent crashing when trying to access the input objects during GC/ saving + // Fixes #126804 + // + // Also, don't do this when playing, only in Editor, This fixes issue in world partition when actors are not + // loaded. + + bool bUseCachedBounds = UE::IsSavingPackage(nullptr) || World->IsGameWorld() || GIsCookerLoadingPackage; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + bUseCachedBounds |= IsGarbageCollecting() || IsGarbageCollectingAndLockingUObjectHashTables(); +#else + bUseCachedBounds |= IsGarbageCollecting(); +#endif + + if (bUseCachedBounds) + return CachedBounds; + + switch (Type) + { + case EHoudiniInputType::Curve: + { + for (int32 Idx = 0; Idx < CurveInputObjects.Num(); ++Idx) + { + const UHoudiniInputHoudiniSplineComponent* CurInCurve = Cast(CurveInputObjects[Idx]); + if (!IsValid(CurInCurve)) + continue; + + UHoudiniSplineComponent* CurCurve = CurInCurve->GetCurveComponent(); + if (!IsValid(CurCurve)) + continue; + + FBox CurCurveBound(ForceInitToZero); + for (auto & Trans : CurCurve->CurvePoints) + { + CurCurveBound += Trans.GetLocation(); + } + + UHoudiniCookable* OuterHC = Cast(GetOuter()); + if (IsValid(OuterHC)) + { + USceneComponent* OuterComp = OuterHC->GetComponent(); + if (IsValid(OuterComp)) + BoxBounds += CurCurveBound.MoveTo(OuterComp->GetComponentLocation()); + } + } + } + break; + + case EHoudiniInputType::World: + { + for (int32 Idx = 0; Idx < WorldInputObjects.Num(); ++Idx) + { + UHoudiniInputActor* CurInActor = nullptr; + UHoudiniInputHoudiniAsset* CurInAsset = nullptr; + UHoudiniInputLandscape* CurInLandscape = nullptr; + if (IsValid(CurInActor = Cast(WorldInputObjects[Idx]))) + { + AActor* Actor = CurInActor->GetActor(); + if (!IsValid(Actor)) + continue; + + FVector Origin, Extent; + Actor->GetActorBounds(false, Origin, Extent); + + BoxBounds += FBox::BuildAABB(Origin, Extent); + } + else if (IsValid(CurInAsset = Cast(WorldInputObjects[Idx]))) + { + // World Input now also support HoudiniAssets + UHoudiniCookable* CurInHC = CurInAsset->GetHoudiniCookable(); + if (!IsValid(CurInHC)) + continue; + + UHoudiniAssetComponent* CookableComp = Cast(CurInHC->GetComponent()); + if(IsValid(CookableComp)) + BoxBounds += CookableComp->GetAssetBounds(nullptr, false); + } + else if (IsValid(CurInLandscape = Cast(WorldInputObjects[Idx]))) + { + ALandscapeProxy* CurLandscape = CurInLandscape->GetLandscapeProxy(); + if (!IsValid(CurLandscape)) + continue; + + FVector Origin, Extent; + CurLandscape->GetActorBounds(false, Origin, Extent); + + BoxBounds += FBox::BuildAABB(Origin, Extent); + } + } + } + break; + + case EHoudiniInputType::Invalid: + default: + break; + } + + // Cache the new bounds + CachedBounds = BoxBounds; + + return BoxBounds; +} + +bool +UHoudiniInput::IsAssetInput() const +{ + if (Type != EHoudiniInputType::World) + return false; + + if (!bDirectlyConnectHdas) + return false; + + for (auto CurObj : *GetHoudiniInputObjectArray(Type)) + { + UHoudiniInputHoudiniAsset* CurObjAsAsset = Cast(CurObj); + if (IsValid(CurObjAsAsset)) + return true; + } + return false; +} + +bool +UHoudiniInput::IsLandscapeInput() const +{ + if (Type != EHoudiniInputType::World) + return false; + + for (auto CurObj : *GetHoudiniInputObjectArray(Type)) + { + UHoudiniInputLandscape* CurObjAsLandscape = Cast(CurObj); + if (IsValid(CurObjAsLandscape)) + return true; + } + return false; +} + +void UHoudiniInput::UpdateLandscapeInputSelection() +{ + LandscapeSelectedComponents.Reset(); + if (!InputSettings.bLandscapeExportSelectionOnly) return; + +#if WITH_EDITOR + if (!IsLandscapeInput()) + return; + + for (UHoudiniInputObject* NextInputObj : *GetHoudiniInputObjectArray(Type)) + { + UHoudiniInputLandscape* CurrentInputLandscape = Cast(NextInputObj); + if (!CurrentInputLandscape) + continue; + + ALandscapeProxy* CurrentInputLandscapeProxy = CurrentInputLandscape->GetLandscapeProxy(); + if (!CurrentInputLandscapeProxy) + continue; + + // Get selected components if bLandscapeExportSelectionOnly or bLandscapeAutoSelectComponent is true + FBox Bounds(ForceInitToZero); + if ( InputSettings.bLandscapeAutoSelectComponent ) + { + // Get our asset's or our connected input asset's bounds + UHoudiniCookable* OuterCookable = Cast(GetOuter()); + if (IsValid(OuterCookable)) + { + UHoudiniAssetComponent* OuterHAC = Cast(OuterCookable->GetComponent()); + if(IsValid(OuterHAC)) + Bounds = OuterHAC->GetAssetBounds(this, true); + } + } + + if ( InputSettings.bLandscapeExportSelectionOnly ) + { + const ULandscapeInfo * LandscapeInfo = CurrentInputLandscapeProxy->GetLandscapeInfo(); + if ( IsValid(LandscapeInfo) ) + { + // Get the currently selected components + for(ULandscapeComponent* LandscapeComponent : LandscapeInfo->GetSelectedComponents()) + LandscapeSelectedComponents.Add(LandscapeComponent); + } + + if ( InputSettings.bLandscapeAutoSelectComponent && LandscapeSelectedComponents.Num() <= 0 && Bounds.IsValid ) + { + // We'll try to use the asset bounds to automatically "select" the components + for ( int32 ComponentIdx = 0; ComponentIdx < CurrentInputLandscapeProxy->LandscapeComponents.Num(); ComponentIdx++ ) + { + ULandscapeComponent * LandscapeComponent = CurrentInputLandscapeProxy->LandscapeComponents[ ComponentIdx ]; + if ( !IsValid(LandscapeComponent) ) + continue; + + FBoxSphereBounds WorldBounds = LandscapeComponent->CalcBounds( LandscapeComponent->GetComponentTransform()); + + if ( Bounds.IntersectXY( WorldBounds.GetBox() ) ) + LandscapeSelectedComponents.Add( LandscapeComponent ); + } + + int32 Num = LandscapeSelectedComponents.Num(); + HOUDINI_LOG_MESSAGE( TEXT("Landscape input: automatically selected %d components within the asset's bounds."), Num ); + } + } + else + { + // Add all the components of the landscape to the selected set + ULandscapeInfo* LandscapeInfo = CurrentInputLandscapeProxy->GetLandscapeInfo(); + if (LandscapeInfo) + { + LandscapeInfo->ForAllLandscapeComponents([&](ULandscapeComponent* Component) + { + LandscapeSelectedComponents.Add(Component); + }); + } + } + + CurrentInputLandscape->MarkChanged(true); + } +#endif +} + +void +UHoudiniInput::MarkInputNodeAsPendingDelete() +{ + if (InputNodeId < 0) + return; + + InputNodesPendingDelete.Add(InputNodeId); + InputNodeId = -1; +} + +FString +UHoudiniInput::InputTypeToString(const EHoudiniInputType& InInputType) +{ + FString InputTypeStr; + switch (InInputType) + { + case EHoudiniInputType::Geometry: + { + InputTypeStr = TEXT("Geometry Input"); + } + break; + + case EHoudiniInputType::Curve: + { + InputTypeStr = TEXT("Curve Input"); + } + break; + + case EHoudiniInputType::World: + { + InputTypeStr = TEXT("World Input"); + } + break; + + case EHoudiniInputType::PCGInput: + { + InputTypeStr = TEXT("PCG Input"); + } + break; + + case EHoudiniInputType::Invalid: + { + InputTypeStr = TEXT("INVALID INPUT"); + } + break; + + default: + { + InputTypeStr = TEXT("(deprecated) INVALID INPUT"); + } + break; + } + + return InputTypeStr; +} + +EHoudiniInputType +UHoudiniInput::StringToInputType(const FString& InInputTypeString) +{ + if (InInputTypeString.StartsWith(TEXT("Geometry"), ESearchCase::IgnoreCase)) + { + return EHoudiniInputType::Geometry; + } + else if (InInputTypeString.StartsWith(TEXT("Curve"), ESearchCase::IgnoreCase)) + { + return EHoudiniInputType::Curve; + } + else if (InInputTypeString.StartsWith(TEXT("World"), ESearchCase::IgnoreCase)) + { + return EHoudiniInputType::World; + } + else if(InInputTypeString.StartsWith(TEXT("PCG"), ESearchCase::IgnoreCase)) + { + return EHoudiniInputType::PCGInput; + } + + return EHoudiniInputType::Invalid; +} + +EHoudiniCurveType UHoudiniInput::StringToHoudiniCurveType(const FString& HoudiniCurveTypeString) +{ + if (HoudiniCurveTypeString.StartsWith(TEXT("Polygon"), ESearchCase::IgnoreCase)) + { + return EHoudiniCurveType::Polygon; + } + else if (HoudiniCurveTypeString.StartsWith(TEXT("Nurbs"), ESearchCase::IgnoreCase)) + { + return EHoudiniCurveType::Nurbs; + } + else if (HoudiniCurveTypeString.StartsWith(TEXT("Bezier"), ESearchCase::IgnoreCase)) + { + return EHoudiniCurveType::Bezier; + } + else if (HoudiniCurveTypeString.StartsWith(TEXT("Points"), ESearchCase::IgnoreCase)) + { + return EHoudiniCurveType::Points; + } + + return EHoudiniCurveType::Invalid; +} + +EHoudiniCurveMethod UHoudiniInput::StringToHoudiniCurveMethod(const FString& HoudiniCurveMethodString) +{ + if (HoudiniCurveMethodString.StartsWith(TEXT("CVs"), ESearchCase::IgnoreCase)) + { + return EHoudiniCurveMethod::CVs; + } + else if (HoudiniCurveMethodString.StartsWith(TEXT("Breakpoints"), ESearchCase::IgnoreCase)) + { + return EHoudiniCurveMethod::Breakpoints; + } + + else if (HoudiniCurveMethodString.StartsWith(TEXT("Freehand"), ESearchCase::IgnoreCase)) + { + return EHoudiniCurveMethod::Freehand; + } + + return EHoudiniCurveMethod::Invalid; + +} + +EHoudiniCurveBreakpointParameterization UHoudiniInput::StringToHoudiniCurveBreakpointParameterization(const FString& CurveParameterizationString) +{ + if (CurveParameterizationString.StartsWith(TEXT("Uniform"), ESearchCase::IgnoreCase)) + { + return EHoudiniCurveBreakpointParameterization::Uniform; + } + else if (CurveParameterizationString.StartsWith(TEXT("Chord"), ESearchCase::IgnoreCase)) + { + return EHoudiniCurveBreakpointParameterization::Chord; + } + + else if (CurveParameterizationString.StartsWith(TEXT("Centripetal"), ESearchCase::IgnoreCase)) + { + return EHoudiniCurveBreakpointParameterization::Centripetal; + } + + return EHoudiniCurveBreakpointParameterization::Invalid; +} + +// +void +UHoudiniInput::SetSOPInput(const int32& InInputIndex) +{ + // Set the input index + InputIndex = InInputIndex; + + // Invalidate objpath parameter + ParmId = -1; + bIsObjectPathParameter = false; + bIsCOPInput = false; +} + +void +UHoudiniInput::SetObjectPathParameter(const int32& InParmId, const bool& bIsCOP) +{ + // Set as objpath parameter + ParmId = InParmId; + bIsObjectPathParameter = true; + bIsCOPInput = bIsCOP; + + // Invalidate the geo input + InputIndex = -1; +} + +// +void +UHoudiniInput::SetCOPInput(const int32& InInputIndex) +{ + // Set the input index + InputIndex = InInputIndex; + + // Invalidate objpath parameter + ParmId = -1; + bIsObjectPathParameter = false; + bIsCOPInput = true; +} + +EHoudiniXformType +UHoudiniInput::GetDefaultXTransformType() +{ + switch (Type) + { + case EHoudiniInputType::Curve: + case EHoudiniInputType::Geometry: + return EHoudiniXformType::None; + + case EHoudiniInputType::World: + return EHoudiniXformType::IntoThisObject; + } + + return EHoudiniXformType::Auto; +} + +bool +UHoudiniInput::GetKeepWorldTransform() const +{ + bool bReturn = false; + switch (InputSettings.KeepWorldTransform) + { + case EHoudiniXformType::Auto: + { + // Return default values corresponding to the input type: + if (Type == EHoudiniInputType::Curve || Type == EHoudiniInputType::Geometry) + { + // NONE for Geo, and Curve IN + bReturn = false; + } + else + { + // INTO THIS OBJECT for World IN + bReturn = true; + } + break; + } + + case EHoudiniXformType::None: + { + bReturn = false; + break; + } + + case EHoudiniXformType::IntoThisObject: + { + bReturn = true; + break; + } + } + + return bReturn; +} + +void +UHoudiniInput::SetKeepWorldTransform(const bool& bInKeepWorldTransform) +{ + if (bInKeepWorldTransform) + { + InputSettings.KeepWorldTransform = EHoudiniXformType::IntoThisObject; + } + else + { + InputSettings.KeepWorldTransform = EHoudiniXformType::None; + } +} + +void +UHoudiniInput::SetInputType(const EHoudiniInputType& InInputType, bool& bOutBlueprintStructureModified) +{ + UHoudiniCookable* OuterCookable = Cast(GetOuter()); + USceneComponent* OuterComp = OuterCookable ? OuterCookable->GetComponent() : Cast(GetOuter()); + if (InInputType == Type) + return; + + SetPreviousInputType(Type); + + // Mark this input as changed + MarkChanged(true); + bOutBlueprintStructureModified = true; + + // Check previous input type + switch (PreviousType) + { + case EHoudiniInputType::Curve: + { + // detach the input curves from the asset component + if (GetNumberOfInputObjects() > 0) + { + for (UHoudiniInputObject * CurrentInput : *GetHoudiniInputObjectArray(Type)) + { + UHoudiniInputHoudiniSplineComponent * CurrentInputHoudiniSpline = Cast(CurrentInput); + + if (!IsValid(CurrentInputHoudiniSpline)) + continue; + + UHoudiniSplineComponent * HoudiniSplineComponent = CurrentInputHoudiniSpline->GetCurveComponent(); + + + if (!IsValid(HoudiniSplineComponent)) + continue; + + HoudiniSplineComponent->Modify(); + + const bool bIsArchetype = HoudiniSplineComponent->HasAnyFlags(RF_ArchetypeObject|RF_ClassDefaultObject); + + if (bIsArchetype) + { +#if WITH_EDITOR + check(HoudiniSplineComponent->IsTemplate()); + FHoudiniEngineRuntimeUtils::SetTemplatePropertyValue(HoudiniSplineComponent, "bVisible", false); + FHoudiniEngineRuntimeUtils::SetTemplatePropertyValue(HoudiniSplineComponent, "bHiddenInGame", true); + FHoudiniEngineRuntimeUtils::SetTemplatePropertyValue(HoudiniSplineComponent, "bIsHoudiniSplineVisible", false); + FHoudiniEngineRuntimeUtils::SetTemplatePropertyValue(HoudiniSplineComponent, "bHasChanged", true); + FHoudiniEngineRuntimeUtils::SetTemplatePropertyValue(HoudiniSplineComponent, "bNeedsToTriggerUpdate", true); +#endif + } + else + { + AActor* OwningActor = HoudiniSplineComponent->GetOwner(); + check(OwningActor); + + FDetachmentTransformRules DetachTransRules(EDetachmentRule::KeepRelative, EDetachmentRule::KeepRelative, EDetachmentRule::KeepRelative, false); + HoudiniSplineComponent->DetachFromComponent(DetachTransRules); + HoudiniSplineComponent->SetVisibility(false, true); + HoudiniSplineComponent->SetHoudiniSplineVisible(false); + HoudiniSplineComponent->SetHiddenInGame(true, true); + + // This NodeId shouldn't be invalidated like this. If a spline component + // or curve input is no longer valid, the input object should be removed from the HoudinInput + // to get cleaned up properly. + // HoudiniSplineComponent->SetNodeId(-1); + HoudiniSplineComponent->MarkChanged(true); + } + + bOutBlueprintStructureModified = true; + } + } + break; + } + + case EHoudiniInputType::Geometry: + case EHoudiniInputType::World: + break; + + default: + break; + } + + + Type = InInputType; + + // TODO: NOPE, not needed + // Set keep world transform to default w.r.t to new input type. + //KeepWorldTransform = GetDefaultXTransformType(); + + // Check current input type + switch (InInputType) + { + case EHoudiniInputType::World: + { + if (OuterCookable && !InputSettings.bImportAsReference) + { + for (auto& CurrentInput : *GetHoudiniInputObjectArray(EHoudiniInputType::World)) + { + UHoudiniInputHoudiniAsset* HoudiniAssetInput = Cast(CurrentInput); + if (!IsValid(HoudiniAssetInput)) + continue; + + UHoudiniCookable* CurrentHC = HoudiniAssetInput->GetHoudiniCookable(); + if (!IsValid(CurrentHC)) + continue; + + CurrentHC->AddDownstreamCookable(OuterCookable); + } + } + } + break; + + case EHoudiniInputType::Curve: + { + if (GetNumberOfInputObjects() == 0) + { + // Create a curve input, but only if part of an actor. + if(IsValid(OuterComp)) + { + CreateNewCurveInputObject(bOutBlueprintStructureModified); + MarkChanged(true); + } + } + else + { + for (auto& CurrentInput : *GetHoudiniInputObjectArray(Type)) + { + UHoudiniInputHoudiniSplineComponent* SplineInput = Cast< UHoudiniInputHoudiniSplineComponent>(CurrentInput); + if (!IsValid(SplineInput)) + continue; + + UHoudiniSplineComponent * HoudiniSplineComponent = SplineInput->GetCurveComponent(); + if (!IsValid(HoudiniSplineComponent)) + continue; + + HoudiniSplineComponent->Modify(); + + const bool bIsArchetype = HoudiniSplineComponent->HasAnyFlags(RF_ArchetypeObject|RF_ClassDefaultObject); + + if (bIsArchetype) + { +#if WITH_EDITOR + check(HoudiniSplineComponent->IsTemplate()); + FHoudiniEngineRuntimeUtils::SetTemplatePropertyValue(HoudiniSplineComponent, "bVisible", true); + FHoudiniEngineRuntimeUtils::SetTemplatePropertyValue(HoudiniSplineComponent, "bHiddenInGame", false); + FHoudiniEngineRuntimeUtils::SetTemplatePropertyValue(HoudiniSplineComponent, "bIsHoudiniSplineVisible", true); + FHoudiniEngineRuntimeUtils::SetTemplatePropertyValue(HoudiniSplineComponent, "bHasChanged", true); + FHoudiniEngineRuntimeUtils::SetTemplatePropertyValue(HoudiniSplineComponent, "bNeedsToTriggerUpdate", true); +#endif + } + else + { + // Attach the new Houdini spline component to it's owner + if (HoudiniSplineComponent->GetOwner()) + HoudiniSplineComponent->RegisterComponent(); + + HoudiniSplineComponent->AttachToComponent(OuterComp, FAttachmentTransformRules::KeepRelativeTransform); + HoudiniSplineComponent->SetHoudiniSplineVisible(true); + HoudiniSplineComponent->SetHiddenInGame(false, true); + HoudiniSplineComponent->SetVisibility(true, true); + HoudiniSplineComponent->MarkChanged(true); + + } + + bOutBlueprintStructureModified = true; + } + } + } + break; + + case EHoudiniInputType::Geometry: + default: + break; + } + + FHoudiniEngineRuntimeUtils::ForceDetailsPanelToUpdate(); +} + +UHoudiniInputObject* +UHoudiniInput::CreateNewCurveInputObject(bool& bOutBlueprintStructureModified) +{ + if (CurveInputObjects.Num() > 0) + return nullptr; + + UHoudiniInputHoudiniSplineComponent* NewCurveInputObject = CreateHoudiniSplineInput(nullptr, true, false, bOutBlueprintStructureModified); + if (!IsValid(NewCurveInputObject)) + return nullptr; + + UHoudiniSplineComponent * HoudiniSplineComponent = NewCurveInputObject->GetCurveComponent(); + if (!IsValid(HoudiniSplineComponent)) + return nullptr; + + // Default Houdini spline component input should not be visible at initialization + HoudiniSplineComponent->SetVisibility(true, true); + HoudiniSplineComponent->SetHoudiniSplineVisible(true); + HoudiniSplineComponent->SetHiddenInGame(false, true); + + CurveInputObjects.Add(NewCurveInputObject); + SetInputObjectsNumber(EHoudiniInputType::Curve, 1); + CurveInputObjects.SetNum(1); + + return NewCurveInputObject; +} + +UHoudiniInputHoudiniSplineComponent* UHoudiniInput::GetOrCreateCurveInputObjectAt(const int32 Index, + const bool bCreateIndex, + bool& bOutBlueprintStructureModified) +{ + + if (!bCreateIndex && !CurveInputObjects.IsValidIndex(Index)) + { + // Index is not valid, and we shouldn't create an entry here either + return nullptr; + } + + // If the given index does not exist, resize the CurveInputs array to accomodate the entry. + if (!CurveInputObjects.IsValidIndex(Index)) + { + CurveInputObjects.SetNum(Index+1); + } + + UHoudiniInputObject* ExistingInputObject = Cast(GetInputObjectAt(EHoudiniInputType::Curve, Index)); + UHoudiniInputHoudiniSplineComponent* CurveInputObject = Cast( ExistingInputObject ); + + if (IsValid(CurveInputObject)) + { + return CurveInputObject; + } + + if (IsValid(ExistingInputObject) && !IsValid(CurveInputObject)) + { + // There is an existing input object, but it is not a curve. Delete the existing input object and so that + // we can create a new object in its place + DeleteInputObjectAt(Index, false); + MarkChanged(true); + } + + CurveInputObject = CreateHoudiniSplineInput(nullptr, true, false, bOutBlueprintStructureModified); + if (!IsValid(CurveInputObject)) + { + return nullptr; + } + + CurveInputObjects[Index] = CurveInputObject; + + UHoudiniSplineComponent* HoudiniSplineComponent = CurveInputObject->GetCurveComponent(); + if (!IsValid(HoudiniSplineComponent)) + { + return nullptr; + } + + // Default Houdini spline component input should not be visible at initialization + HoudiniSplineComponent->SetVisibility(true, true); + HoudiniSplineComponent->SetHoudiniSplineVisible(true); + HoudiniSplineComponent->SetHiddenInGame(false, true); + + return CurveInputObject; +} + +void +UHoudiniInput::MarkAllInputObjectsChanged(const bool& bInChanged) +{ + MarkDataUploadNeeded(bInChanged); + + // Mark all the objects from this input has changed so they upload themselves + + TSet InputTypes; + InputTypes.Add(Type); + InputTypes.Add(EHoudiniInputType::Curve); + + TArray>* NewInputObjects = GetHoudiniInputObjectArray(Type); + if (NewInputObjects) + { + for (auto CurInputObject : *NewInputObjects) + { + if (IsValid(CurInputObject)) + CurInputObject->MarkChanged(bInChanged); + } + } +} + +UHoudiniInput * UHoudiniInput::DuplicateAndCopyState(UObject * DestOuter, bool bInCanDeleteHoudiniNodes) +{ + UHoudiniInput* NewInput = Cast(StaticDuplicateObject(this, DestOuter)); + + NewInput->CopyStateFrom(this, false, bInCanDeleteHoudiniNodes); + + return NewInput; +} + +void UHoudiniInput::CopyStateFrom(UHoudiniInput* InInput, bool bCopyAllProperties, bool bInCanDeleteHoudiniNodes) +{ + // Preserve the current input objects before the copy to ensure we don't lose + // access to input objects and have them end up in the garbage. + TMap>*> PrevInputObjectsMap; + for(EHoudiniInputType InputType : HoudiniInputTypeList) + { + PrevInputObjectsMap.Add(InputType, GetHoudiniInputObjectArray(InputType)); + } + + // TArray PrevInputObjects; + // TArray* OldToInputObjects = GetHoudiniInputObjectArray(Type); + // if (OldToInputObjects) + // PrevInputObjects = *OldToInputObjects; + + // Copy the state of this UHoudiniInput object. + if (bCopyAllProperties) + { + UEngine::FCopyPropertiesForUnrelatedObjectsParams Params; + Params.bDoDelta = false; // Perform a deep copy + Params.bClearReferences = false; // References will be replaced afterwards. + UEngine::CopyPropertiesForUnrelatedObjects(InInput, this, Params); + } + + AssetNodeId = InInput->AssetNodeId; + InputNodeId = InInput->GetInputNodeId(); + ParmId = InInput->ParmId; + bCanDeleteHoudiniNodes = bInCanDeleteHoudiniNodes; + + //if (bInCanDeleteHoudiniNodes) + //{ + // // Delete stale data nodes before they get overwritten. + // TSet NewNodeIds(InInput->CreatedDataNodeIds); + // for (int32 NodeId : CreatedDataNodeIds) + // { + // if (!NewNodeIds.Contains(NodeId)) + // { + // FHoudiniEngineRuntime::Get().MarkNodeIdAsPendingDelete(NodeId); + // } + // } + //} + + CreatedDataNodeIds = InInput->CreatedDataNodeIds; + + // Important note: At this point the new object may still share objects with InInput. + // The CopyInputs() will properly duplicate inputs where necessary. + + // Copy states of Input Objects that correspond to the current type. + for(auto& Entry : PrevInputObjectsMap) + { + EHoudiniInputType InputType = Entry.Key; + TArray>* PrevInputObjects = Entry.Value; + TArray>* ToInputObjects = GetHoudiniInputObjectArray(InputType); + TArray>* FromInputObjects = InInput->GetHoudiniInputObjectArray(InputType); + + if (ToInputObjects && FromInputObjects) + { + *ToInputObjects = *PrevInputObjects; + CopyInputs(*ToInputObjects, *FromInputObjects, bInCanDeleteHoudiniNodes); + } + } +} + +void UHoudiniInput::SetCanDeleteHoudiniNodes(bool bInCanDeleteNodes) +{ + bCanDeleteHoudiniNodes = bInCanDeleteNodes; + for(UHoudiniInputObject* InputObject : GeometryInputObjects) + { + if (!InputObject) + continue; + InputObject->SetCanDeleteHoudiniNodes(bInCanDeleteNodes); + } + + for(UHoudiniInputObject* InputObject : CurveInputObjects) + { + if (!InputObject) + continue; + InputObject->SetCanDeleteHoudiniNodes(bInCanDeleteNodes); + } + + for(UHoudiniInputObject* InputObject : WorldInputObjects) + { + if (!InputObject) + continue; + InputObject->SetCanDeleteHoudiniNodes(bInCanDeleteNodes); + } +} + +void UHoudiniInput::InvalidateData() +{ + // If the ref counted input system is being used, we must not delete nodes managed by the system. + // Do this before invalidating data so the list is accurate so we don't delete nodes twice. + // (InvalidateData() may remove nodes that are managed). + + TSet ManagedNodeIds; + { + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if(Manager) + { + TArray ManagedNodeIdArray; + if(Manager->GetAllHAPINodeIds(ManagedNodeIdArray)) + ManagedNodeIds.Append(ManagedNodeIdArray); + } + } + + for(UHoudiniInputObject* InputObject : GeometryInputObjects) + { + if (!InputObject) + continue; + InputObject->InvalidateData(); + } + + for(UHoudiniInputObject* InputObject : CurveInputObjects) + { + if (!InputObject) + continue; + InputObject->InvalidateData(); + } + + for(UHoudiniInputObject* InputObject : WorldInputObjects) + { + if (!InputObject) + continue; + + if (InputObject->IsA()) + { + // When the input object is a Cookable, + // we need to be sure that this HDA node id is not in CreatedDataNodeIds + // We dont want to delete the input HDA node! + const int32 ObjInputNodeId = InputObject->GetInputNodeId(); + if (ObjInputNodeId >= 0) + CreatedDataNodeIds.Remove(ObjInputNodeId); + } + + InputObject->InvalidateData(); + } + + if (bCanDeleteHoudiniNodes && !CreatedDataNodeIds.IsEmpty()) + { + + + auto& HoudiniEngineRuntime = FHoudiniEngineRuntime::Get(); + for (int32 NodeId : CreatedDataNodeIds) + { + if (ManagedNodeIds.Contains(NodeId)) + continue; + + HoudiniEngineRuntime.MarkNodeIdAsPendingDelete(NodeId, true); + } + } + + CreatedDataNodeIds.Empty(); +} + +void UHoudiniInput::CopyInputs(TArray>& ToInputObjects, TArray>& FromInputObjects, bool bInCanDeleteHoudiniNodes) +{ + TSet StaleObjects(ToInputObjects); + + const int32 NumInputs = FromInputObjects.Num(); + UObject* TargetOuter = GetOuter(); + + ToInputObjects.SetNum(NumInputs); + + + for (int i = 0; i < NumInputs; i++) + { + UHoudiniInputObject* FromObject = FromInputObjects[i]; + UHoudiniInputObject* ToObject = ToInputObjects[i]; + + if (!FromObject) + { + ToInputObjects[i] = nullptr; + continue; + } + + if (ToObject) + { + bool IsValid = true; + // Is ToInput and FromInput the same or do we have to create a input object? + IsValid = IsValid && ToObject->Matches(*FromObject); + IsValid = IsValid && ToObject->GetOuter() == TargetOuter; + + if (!IsValid) + { + ToObject = nullptr; + } + } + + if (ToObject) + { + // We have an existing (matching) object. Copy the + // state from the incoming input. + StaleObjects.Remove(ToObject); + ToObject->CopyStateFrom(FromObject, true); + } + else + { + // We need to create a new input here. + ToObject = FromObject->DuplicateAndCopyState(TargetOuter); + ToInputObjects[i] = ToObject; + } + + ToObject->SetCanDeleteHoudiniNodes(bInCanDeleteHoudiniNodes); + } + + + for (UHoudiniInputObject* StaleInputObject : StaleObjects) + { + if (!StaleInputObject) + continue; + if (StaleInputObject->GetOuter() == this) + { + StaleInputObject->SetCanDeleteHoudiniNodes(bInCanDeleteHoudiniNodes); + } + } +} + + +UHoudiniInputHoudiniSplineComponent* +UHoudiniInput::CreateHoudiniSplineInput(UHoudiniInputHoudiniSplineComponent * FromHoudiniSplineInputComponent, const bool & bAttachToparent, const bool & bAppendToInputArray, bool& bOutBlueprintStructureModified) +{ + UHoudiniInputHoudiniSplineComponent* HoudiniSplineInput = nullptr; + UHoudiniSplineComponent* HoudiniSplineComponent = nullptr; + + UObject* OuterObj = GetOuter(); + + UHoudiniCookable* OuterCookable = Cast(GetOuter()); + USceneComponent* OuterComp = OuterCookable ? OuterCookable->GetComponent() : Cast(GetOuter()); + bool bOuterIsTemplate = (OuterObj && OuterObj->IsTemplate()); + + if (!FromHoudiniSplineInputComponent) + { + // NOTE: If we're inside the Blueprint editor, the outer here is going to the be HAC component template. + check(OuterObj) + + // Create a default Houdini spline input if a null pointer is passed in. + FName HoudiniSplineName = MakeUniqueObjectName(OuterComp, UHoudiniSplineComponent::StaticClass(), TEXT("HoudiniSpline")); + + // Create a Houdini Input Object. + UHoudiniInputObject * NewInputObject = UHoudiniInputHoudiniSplineComponent::Create( + nullptr, OuterObj, HoudiniSplineName.ToString(), InputSettings); + + if (!IsValid(NewInputObject)) + return nullptr; + + HoudiniSplineInput = Cast(NewInputObject); + if (!HoudiniSplineInput) + return nullptr; + + HoudiniSplineComponent = NewObject( + HoudiniSplineInput, UHoudiniSplineComponent::StaticClass()); + + if (!IsValid(HoudiniSplineComponent)) + return nullptr; + + HoudiniSplineInput->Update(HoudiniSplineComponent, InputSettings); + + HoudiniSplineComponent->SetHoudiniSplineName(HoudiniSplineName.ToString()); + HoudiniSplineComponent->SetFlags(RF_Transactional); + + // Set the default position of curve to avoid overlapping. + HoudiniSplineComponent->SetOffset(DefaultCurveOffset); + DefaultCurveOffset += 100.f; + + if (!bOuterIsTemplate) + { + if (HoudiniSplineComponent->GetOwner()) + HoudiniSplineComponent->RegisterComponent(); + + // Attach the new Houdini spline component to it's owner. + if (bAttachToparent) + HoudiniSplineComponent->AttachToComponent(OuterComp, FAttachmentTransformRules::KeepRelativeTransform); + } + + //push the new input object to the array for new type. + if (bAppendToInputArray && Type == EHoudiniInputType::Curve) + GetHoudiniInputObjectArray(Type)->Add(NewInputObject); + +#if WITH_EDITOR + if (bOuterIsTemplate) + { + UHoudiniAssetBlueprintComponent* HAB = Cast(OuterObj); + if (HAB) + { + UBlueprintGeneratedClass* BPGC = Cast(HAB->GetOuter()); + UBlueprint* Blueprint = Cast(BPGC->ClassGeneratedBy); + if (Blueprint) + { + TArray Components; + Components.Add(HoudiniSplineComponent); + + USCS_Node* HABNode = HAB->FindSCSNodeForTemplateComponentInClassHierarchy(HAB); + + // NOTE: FAddComponentsToBlueprintParams was introduced in 4.26 so for the sake of + // backwards compatibility, manually determine which SCSNode was added instead of + // relying on Params.OutNodes. + FKismetEditorUtilities::FAddComponentsToBlueprintParams Params; + Params.OptionalNewRootNode = HABNode; + const TSet PreviousSCSNodes(Blueprint->SimpleConstructionScript->GetAllNodes()); + + FKismetEditorUtilities::AddComponentsToBlueprint(Blueprint, Components, Params); + USCS_Node* NewNode = nullptr; + const TSet CurrentSCSNodes(Blueprint->SimpleConstructionScript->GetAllNodes()); + const TSet AddedNodes = CurrentSCSNodes.Difference(PreviousSCSNodes); + + if (AddedNodes.Num() > 0) + { + // Record Input / SCS node mapping + USCS_Node* SCSNode = AddedNodes.Array()[0]; + HAB->AddInputObjectMapping(NewInputObject->GetInputGuid(), SCSNode->VariableGuid); + SCSNode->ComponentTemplate->SetFlags(RF_Public | RF_ArchetypeObject | RF_DefaultSubObject); + } + + Blueprint->Modify(); + bOutBlueprintStructureModified = true; + } + } + } +#endif + } + else + { + // Otherwise, get the Houdini spline, and Houdini spline input from the argument. + HoudiniSplineInput = FromHoudiniSplineInputComponent; + HoudiniSplineComponent = FromHoudiniSplineInputComponent->GetCurveComponent(); + + if (!IsValid(HoudiniSplineComponent)) + return nullptr; + + // Attach the new Houdini spline component to it's owner. + HoudiniSplineComponent->AttachToComponent(OuterComp, FAttachmentTransformRules::KeepRelativeTransform); + } + + // Mark the created UHoudiniSplineComponent as an input, and set its InputObject. + HoudiniSplineComponent->SetIsInputCurve(true); + + // HoudiniSplineComponent->SetInputObject(HoudiniSplineInput); + + // Set Houdini Spline Component bHasChanged and bNeedsToTrigerUpdate to true. + HoudiniSplineComponent->MarkChanged(true); + + + + return HoudiniSplineInput; +} + +void +UHoudiniInput::RemoveSplineFromInputObject( + UHoudiniInputHoudiniSplineComponent* InHoudiniSplineInputObject, + bool& bOutBlueprintStructureModified) const +{ + if (!InHoudiniSplineInputObject) + return; + + UObject* OuterObj = GetOuter(); + const bool bOuterIsTemplate = OuterObj && OuterObj->IsTemplate(); + + if (bOuterIsTemplate) + { +#if WITH_EDITOR + // Find the SCS node that corresponds to this input and remove it. + UHoudiniAssetBlueprintComponent* HAB = Cast(OuterObj); + if (HAB) + { + const UBlueprintGeneratedClass* BPGC = Cast(HAB->GetOuter()); + UBlueprint* Blueprint = Cast(BPGC->ClassGeneratedBy); + if (Blueprint) + { + USimpleConstructionScript* SCS = Blueprint->SimpleConstructionScript; + check(SCS); + FGuid SCSGuid; + if (HAB->GetInputObjectSCSVariableGuid(InHoudiniSplineInputObject->Guid, SCSGuid)) + { + // TODO: Move this SCS variable removal code to a reusable utility function. We're + // going to need to reuse this in a few other places too. + USCS_Node* SCSNode = SCS->FindSCSNodeByGuid(SCSGuid); + if (SCSNode) + { + SCS->RemoveNodeAndPromoteChildren(SCSNode); + SCSNode->SetOnNameChanged(FSCSNodeNameChanged()); + bOutBlueprintStructureModified = true; + HAB->RemoveInputObjectSCSVariableGuid(InHoudiniSplineInputObject->Guid); + + if (SCSNode->ComponentTemplate != nullptr) + { + const FName TemplateName = SCSNode->ComponentTemplate->GetFName(); + const FString RemovedName = SCSNode->GetVariableName().ToString() + TEXT("_REMOVED_") + FGuid::NewGuid().ToString(); + + SCSNode->ComponentTemplate->Modify(); + SCSNode->ComponentTemplate->Rename(*RemovedName, /*NewOuter =*/nullptr, REN_DontCreateRedirectors); + + TArray ArchetypeInstances; + auto DestroyArchetypeInstances = [&ArchetypeInstances, &RemovedName](UActorComponent* ComponentTemplate) + { + ComponentTemplate->GetArchetypeInstances(ArchetypeInstances); + for (UObject* ArchetypeInstance : ArchetypeInstances) + { + if (!ArchetypeInstance->HasAllFlags(RF_ArchetypeObject | RF_InheritableComponentTemplate)) + { + CastChecked(ArchetypeInstance)->DestroyComponent(); + ArchetypeInstance->Rename(*RemovedName, nullptr, REN_DontCreateRedirectors); + } + } + }; + + DestroyArchetypeInstances(SCSNode->ComponentTemplate); + + if (Blueprint) + { + // Children need to have their inherited component template instance of the component renamed out of the way as well + TArray ChildrenOfClass; + GetDerivedClasses(Blueprint->GeneratedClass, ChildrenOfClass); + + for (UClass* ChildClass : ChildrenOfClass) + { + UBlueprintGeneratedClass* BPChildClass = CastChecked(ChildClass); + + if (UActorComponent* Component = (UActorComponent*)FindObjectWithOuter(BPChildClass, UActorComponent::StaticClass(), TemplateName)) + { + Component->Modify(); + Component->Rename(*RemovedName, /*NewOuter =*/nullptr, REN_DontCreateRedirectors); + + DestroyArchetypeInstances(Component); + } + } + } + } + } + } // if (HAB->GetInputObjectSCSVariableGuid(InHoudiniSplineInputObject->Guid, SCSGuid)) + } // if (Blueprint) + } +#endif + } // if (bIsOuterTemplate) + else + { + UHoudiniSplineComponent* HoudiniSplineComponent = InHoudiniSplineInputObject->GetCurveComponent(); + if (HoudiniSplineComponent) + { + // detach the input curves from the asset component + //FDetachmentTransformRules DetachTransRules(EDetachmentRule::KeepRelative, EDetachmentRule::KeepRelative, EDetachmentRule::KeepRelative, false); + //HoudiniSplineComponent->DetachFromComponent(DetachTransRules); + // Destroy the Houdini Spline Component + //InputObjectsPtr->RemoveAt(AtIndex); + HoudiniSplineComponent->DestroyComponent(); + } + } + InHoudiniSplineInputObject->Update(nullptr, InputSettings); +} + + +TArray>* +UHoudiniInput::GetHoudiniInputObjectArray(const EHoudiniInputType& InType) +{ + switch (InType) + { + case EHoudiniInputType::Geometry: + return &GeometryInputObjects; + + case EHoudiniInputType::Curve: + return &CurveInputObjects; + + case EHoudiniInputType::World: + return &WorldInputObjects; + + case EHoudiniInputType::PCGInput: + return &PCGInputObjects; + default: + case EHoudiniInputType::Invalid: + return nullptr; + } +} + +TArray>* +UHoudiniInput::GetBoundSelectorObjectArray() +{ + switch (Type) + { + case EHoudiniInputType::World: + return &WorldInputBoundSelectorObjects; + + default: + return nullptr; + } +} + +const TArray>* +UHoudiniInput::GetBoundSelectorObjectArray() const +{ + switch (Type) + { + case EHoudiniInputType::World: + return &WorldInputBoundSelectorObjects; + + default: + return nullptr; + } +} + +const TArray>* +UHoudiniInput::GetHoudiniInputObjectArray(const EHoudiniInputType& InType) const +{ + switch (InType) + { + case EHoudiniInputType::Geometry: + return &GeometryInputObjects; + + case EHoudiniInputType::Curve: + return &CurveInputObjects; + + case EHoudiniInputType::World: + return &WorldInputObjects; + + case EHoudiniInputType::Invalid: + default: + return nullptr; + } +} + +UHoudiniInputObject* +UHoudiniInput::GetHoudiniInputObjectAt(const int32& AtIndex) +{ + return GetHoudiniInputObjectAt(Type, AtIndex); +} + +const UHoudiniInputObject* +UHoudiniInput::GetHoudiniInputObjectAt(const int32& AtIndex) const +{ + const TArray>* InputObjectsArray = GetHoudiniInputObjectArray(Type); + if (!InputObjectsArray || !InputObjectsArray->IsValidIndex(AtIndex)) + return nullptr; + + return (*InputObjectsArray)[AtIndex]; +} + +UHoudiniInputObject* +UHoudiniInput::GetHoudiniInputObjectAt(const EHoudiniInputType& InType, const int32& AtIndex) +{ + TArray>* InputObjectsArray = GetHoudiniInputObjectArray(InType); + if (!InputObjectsArray || !InputObjectsArray->IsValidIndex(AtIndex)) + return nullptr; + + return (*InputObjectsArray)[AtIndex]; +} + +UObject* +UHoudiniInput::GetInputObjectAt(const int32& AtIndex) +{ + return GetInputObjectAt(Type, AtIndex); +} + +AActor* +UHoudiniInput::GetBoundSelectorObjectAt(const int32& AtIndex) +{ + TArray>* BoundSelectorObjects = GetBoundSelectorObjectArray(); + + if (!BoundSelectorObjects) + return nullptr; + + if (!BoundSelectorObjects->IsValidIndex(AtIndex)) + return nullptr; + + return (*BoundSelectorObjects)[AtIndex]; +} + +UObject* +UHoudiniInput::GetInputObjectAt(const EHoudiniInputType& InType, const int32& AtIndex) +{ + UHoudiniInputObject* HoudiniInputObject = GetHoudiniInputObjectAt(InType, AtIndex); + if (!IsValid(HoudiniInputObject)) + return nullptr; + + return HoudiniInputObject->GetObject(); +} + +void +UHoudiniInput::InsertInputObjectAt(const int32& AtIndex) +{ + InsertInputObjectAt(Type, AtIndex); +} + +void +UHoudiniInput::InsertInputObjectAt(const EHoudiniInputType& InType, const int32& AtIndex) +{ + TArray>* InputObjectsPtr = GetHoudiniInputObjectArray(InType); + if (!InputObjectsPtr) + return; + + InputObjectsPtr->InsertDefaulted(AtIndex, 1); + MarkChanged(true); +} + +void +UHoudiniInput::DeleteInputObjectAt(const int32& AtIndex, const bool bInRemoveIndexFromArray) +{ + DeleteInputObjectAt(Type, AtIndex, bInRemoveIndexFromArray); +} + +void +UHoudiniInput::DeleteInputObjectAt(const EHoudiniInputType& InType, const int32& AtIndex, const bool bInRemoveIndexFromArray) +{ + TArray>* InputObjectsPtr = GetHoudiniInputObjectArray(InType); + if (!InputObjectsPtr) + return; + + if (!InputObjectsPtr->IsValidIndex(AtIndex)) + return; + + bool bBlueprintStructureModified = false; + + if (Type == EHoudiniInputType::Curve) + { + UHoudiniInputHoudiniSplineComponent* HoudiniSplineInputObject = Cast((*InputObjectsPtr)[AtIndex]); + if (HoudiniSplineInputObject) + { + RemoveSplineFromInputObject(HoudiniSplineInputObject, bBlueprintStructureModified); + } + } + else if (Type == EHoudiniInputType::Geometry) + { + // ... TODO operations for removing geometry input type + } + else if (Type == EHoudiniInputType::World) + { + // ... TODO operations for removing world input type + } + else + { + // ... invalid input type + } + + MarkChanged(true); + + UHoudiniInputObject* InputObjectToDelete = (*InputObjectsPtr)[AtIndex]; + if (IsValid(InputObjectToDelete)) + { + // Mark the input object's nodes for deletion + InputObjectToDelete->InvalidateData(); + + // If the deleted object wasnt null, trigger a re upload of the input data + MarkDataUploadNeeded(true); + } + + if (bInRemoveIndexFromArray) + { + InputObjectsPtr->RemoveAt(AtIndex); + + // Delete the merge node when all the input objects are deleted. + if (InputObjectsPtr->Num() == 0 && InputNodeId >= 0) + { + if (bCanDeleteHoudiniNodes) + MarkInputNodeAsPendingDelete(); + InputNodeId = -1; + } + } + else + { + (*InputObjectsPtr)[AtIndex] = nullptr; + } + +#if WITH_EDITOR + if (bBlueprintStructureModified) + { + UActorComponent* Component = Cast(GetOuter()); + FHoudiniEngineRuntimeUtils::MarkBlueprintAsStructurallyModified(Component); + } +#endif +} + +void +UHoudiniInput::DuplicateInputObjectAt(const int32& AtIndex) +{ + DuplicateInputObjectAt(Type, AtIndex); +} + +void +UHoudiniInput::DuplicateInputObjectAt(const EHoudiniInputType& InType, const int32& AtIndex) +{ + TArray>* InputObjectsPtr = GetHoudiniInputObjectArray(InType); + if (!InputObjectsPtr) + return; + + if (!InputObjectsPtr->IsValidIndex(AtIndex)) + return; + + // If the duplicated object is not null, trigger a re upload of the input data + bool bTriggerUpload = (*InputObjectsPtr)[AtIndex] != nullptr; + + // TODO: Duplicate the UHoudiniInputObject!! + UHoudiniInputObject* DuplicateInput = (*InputObjectsPtr)[AtIndex]; + InputObjectsPtr->Insert(DuplicateInput, AtIndex); + + MarkChanged(true); + + if (bTriggerUpload) + MarkDataUploadNeeded(true); +} + +int32 +UHoudiniInput::GetNumberOfInputObjects() +{ + return GetNumberOfInputObjects(Type); +} + +int32 +UHoudiniInput::GetNumberOfInputObjects(const EHoudiniInputType& InType) +{ + TArray>* InputObjectsPtr = GetHoudiniInputObjectArray(InType); + if (!InputObjectsPtr) + return 0; + + return InputObjectsPtr->Num(); +} + +int32 +UHoudiniInput::GetNumberOfInputMeshes() +{ + return GetNumberOfInputMeshes(Type); +} + +int32 +UHoudiniInput::GetNumberOfInputMeshes(const EHoudiniInputType& InType) +{ + TArray>* InputObjectsPtr = GetHoudiniInputObjectArray(InType); + if (!InputObjectsPtr) + return 0; + + // TODO? + // If geometry input, and we only have one null object, return 0 + int32 Num = InputObjectsPtr->Num(); + + // Special case for Actors and BP + // we need to add extra input objects stored in the components array + for (auto InputObj : *InputObjectsPtr) + { + if (!IsValid(InputObj)) + continue; + + UHoudiniInputActor* InputActor = Cast(InputObj); + if (IsValid(InputActor)) + { + if (InputActor->GetActorComponents().Num() > 0) + { + Num += (InputActor->GetActorComponents().Num() - 1); + } + } + + UHoudiniInputBlueprint* InputBP = Cast(InputObj); + if (IsValid(InputBP)) + { + if (InputBP->GetComponents().Num() > 0) + { + Num += (InputBP->GetComponents().Num() - 1); + } + } + } + + return Num; +} + + +int32 +UHoudiniInput::GetNumberOfBoundSelectorObjects() const +{ + const TArray>* BoundSelectorObjects = GetBoundSelectorObjectArray(); + + if (!BoundSelectorObjects) + return 0; + + return BoundSelectorObjects->Num(); +} + +void +UHoudiniInput::SetInputObjectAt(const int32& AtIndex, UObject* InObject) +{ + return SetInputObjectAt(Type, AtIndex, InObject); +} + +void +UHoudiniInput::SetInputObjectAt(const EHoudiniInputType& InType, const int32& AtIndex, UObject* InObject) +{ + // Start by making sure we have the proper number of input objects + int32 NumIntObject = GetNumberOfInputObjects(InType); + if (NumIntObject <= AtIndex) + { + // We need to resize the array + SetInputObjectsNumber(InType, AtIndex + 1); + } + + UObject* CurrentInputObject = GetInputObjectAt(InType, AtIndex); + if (CurrentInputObject == InObject) + { + // Nothing to do + return; + } + + UHoudiniInputObject* CurrentInputObjectWrapper = GetHoudiniInputObjectAt(InType, AtIndex); + if (!InObject) + { + // We want to set the input object to null + TArray>* InputObjectsPtr = GetHoudiniInputObjectArray(InType); + if (!ensure(InputObjectsPtr != nullptr && InputObjectsPtr->IsValidIndex(AtIndex))) + return; + + if (CurrentInputObjectWrapper) + { + // TODO: Check this case + // Do not destroy the input object manually! this messes up GC + //CurrentInputObjectWrapper->ConditionalBeginDestroy(); + MarkDataUploadNeeded(true); + } + + (*InputObjectsPtr)[AtIndex] = nullptr; + return; + } + + // Get the type of the previous and new input objects + EHoudiniInputObjectType NewObjectType = InObject ? UHoudiniInputObject::GetInputObjectTypeFromObject(InObject) : EHoudiniInputObjectType::Invalid; + EHoudiniInputObjectType CurrentObjectType = CurrentInputObjectWrapper ? CurrentInputObjectWrapper->Type : EHoudiniInputObjectType::Invalid; + + // See if we can reuse the existing InputObject + if (CurrentObjectType == NewObjectType && NewObjectType != EHoudiniInputObjectType::Invalid) + { + // The InputObjectTypes match, we can just update the existing object + CurrentInputObjectWrapper->Update(InObject, InputSettings); + CurrentInputObjectWrapper->MarkChanged(true); + return; + } + + // Destroy the existing input object + TArray>* InputObjectsPtr = GetHoudiniInputObjectArray(InType); + if (!ensure(InputObjectsPtr)) + return; + + UHoudiniInputObject* NewInputObject = UHoudiniInputObject::CreateTypedInputObject(InObject, this, FString::FromInt(AtIndex + 1), InputSettings); + if (!ensure(NewInputObject)) + return; + + // Mark that input object as changed so we know we need to update it + NewInputObject->MarkChanged(true); + if (IsValid(CurrentInputObjectWrapper)) + { + // TODO: + // For some input type, we may have to copy some of the previous object's property before deleting it + + // Delete the previous object + CurrentInputObjectWrapper->MarkAsGarbage(); + (*InputObjectsPtr)[AtIndex] = nullptr; + } + + // Update the input object array with the newly created input object + (*InputObjectsPtr)[AtIndex] = NewInputObject; + + // Mark the outer package as dirty, to ensure that the changes are saved when using OFPA / World partition + MarkPackageDirty(); +} + +void +UHoudiniInput::SetInputObjectsNumber(const EHoudiniInputType& InType, const int32& InNewCount) +{ + TArray>* InputObjectsPtr = GetHoudiniInputObjectArray(InType); + if (!InputObjectsPtr) + return; + + if (InputObjectsPtr->Num() == InNewCount) + { + // Nothing to do + return; + } + + if (InNewCount > InputObjectsPtr->Num()) + { + // Simply add new default InputObjects + InputObjectsPtr->SetNum(InNewCount); + } + else + { + // TODO: Check this case! + // Do not destroy the input object themselves manually, + // destroy the input object's nodes and reduce the array's size + for (int32 InObjIdx = InputObjectsPtr->Num() - 1; InObjIdx >= InNewCount; InObjIdx--) + { + UHoudiniInputObject* CurrentInputObject = (*InputObjectsPtr)[InObjIdx]; + if (!IsValid(CurrentInputObject)) + continue; + + if (bCanDeleteHoudiniNodes) + CurrentInputObject->InvalidateData(); + + /*/ + //FHoudiniInputTranslator::DestroyInput(Inputs[InputIdx]); + CurrentObject->ConditionalBeginDestroy(); + (*InputObjectsPtr)[InObjIdx] = nullptr; + */ + } + + // Decrease the input object array size + InputObjectsPtr->SetNum(InNewCount); + } + + // Also delete the input's merge node when all the input objects are deleted. + if (InNewCount == 0 && InputNodeId >= 0) + { + if (bCanDeleteHoudiniNodes) + MarkInputNodeAsPendingDelete(); + InputNodeId = -1; + } +} + +void +UHoudiniInput::SetBoundSelectorObjectsNumber(const int32& InNewCount) +{ + TArray>* BoundSelectorObjects = GetBoundSelectorObjectArray(); + + if (!BoundSelectorObjects) + return; + + if (BoundSelectorObjects->Num() == InNewCount) + return; + + BoundSelectorObjects->SetNum(InNewCount); + + /* + // For InNewCount <= BoundSelectorObjects->Num(): + // TODO: Not Needed? + // Do not destroy the input object themselves manually, + // destroy the input object's nodes and reduce the array's size + for (int32 InObjIdx = WorldInputBoundSelectorObjects.Num() - 1; InObjIdx >= InNewCount; InObjIdx--) + { + UHoudiniInputObject* CurrentInputObject = WorldInputBoundSelectorObjects[InObjIdx]; + if (!CurrentInputObject) + continue; + + CurrentInputObject->MarkInputNodesForDeletion(); + } + */ +} + +void +UHoudiniInput::SetBoundSelectorObjectAt(const int32& AtIndex, AActor* InActor) +{ + // Start by making sure we have the proper number of objects + int32 NumIntObject = GetNumberOfBoundSelectorObjects(); + if (NumIntObject <= AtIndex) + { + // We need to resize the array + SetBoundSelectorObjectsNumber(AtIndex + 1); + } + + AActor* CurrentActor = GetBoundSelectorObjectAt(AtIndex); + if (CurrentActor == InActor) + { + // Nothing to do + return; + } + + TArray>* BoundSelectorObjects = GetBoundSelectorObjectArray(); + + if (!BoundSelectorObjects) + return; + + (*BoundSelectorObjects)[AtIndex] = InActor; +} + +// Helper function indicating what classes are supported by an input type +TArray +UHoudiniInput::GetAllowedClasses(const EHoudiniInputType& InInputType) +{ + TArray AllowedClasses; + switch (InInputType) + { + case EHoudiniInputType::Geometry: + AllowedClasses.Add(UStaticMesh::StaticClass()); + AllowedClasses.Add(USkeletalMesh::StaticClass()); + AllowedClasses.Add(UAnimSequence::StaticClass()); + AllowedClasses.Add(UGeometryCollection::StaticClass()); + AllowedClasses.Add(UGeometryCollectionComponent::StaticClass()); + AllowedClasses.Add(AGeometryCollectionActor::StaticClass()); + AllowedClasses.Add(UBlueprint::StaticClass()); + AllowedClasses.Add(UDataTable::StaticClass()); + AllowedClasses.Add(UFoliageType_InstancedStaticMesh::StaticClass()); + AllowedClasses.Add(UTexture2D::StaticClass()); + break; + + case EHoudiniInputType::Curve: + AllowedClasses.Add(USplineComponent::StaticClass()); + AllowedClasses.Add(UHoudiniSplineComponent::StaticClass()); + break; + + case EHoudiniInputType::World: + AllowedClasses.Add(AActor::StaticClass()); + AllowedClasses.Add(UHoudiniAssetComponent::StaticClass()); + AllowedClasses.Add(ALandscapeProxy::StaticClass()); + break; + + default: + break; + } + + return AllowedClasses; +} + +// Helper function indicating if an object is supported by an input type +bool +UHoudiniInput::IsObjectAcceptable(const EHoudiniInputType& InInputType, const UObject* InObject) +{ + TArray AllowedClasses = GetAllowedClasses(InInputType); + for (auto CurClass : AllowedClasses) + { + if (InObject->IsA(CurClass)) + return true; + } + + return false; +} + +bool +UHoudiniInput::IsDataUploadNeeded() +{ + if (bDataUploadNeeded) + return true; + + return HasChanged(); +} + +// Indicates if this input has changed and should be updated +bool +UHoudiniInput::HasChanged() +{ + if (bHasChanged) + return true; + + TArray>* InputObjectsPtr = GetHoudiniInputObjectArray(Type); + if (!InputObjectsPtr) + return false; + + for (auto CurrentInputObject : (*InputObjectsPtr)) + { + if (CurrentInputObject && CurrentInputObject->HasChanged()) + return true; + } + + return false; +} + +bool +UHoudiniInput::IsTransformUploadNeeded() +{ + TArray>* InputObjectsPtr = GetHoudiniInputObjectArray(Type); + if (!ensure(InputObjectsPtr)) + return false; + + for (auto CurrentInputObject : (*InputObjectsPtr)) + { + if (CurrentInputObject && CurrentInputObject->HasTransformChanged()) + return true; + } + + return false; +} + +// Indicates if this input needs to trigger an update +bool +UHoudiniInput::NeedsToTriggerUpdate() +{ + if (bNeedsToTriggerUpdate) + return true; + + const TArray>* InputObjectsPtr = GetHoudiniInputObjectArray(Type); + if (!ensure(InputObjectsPtr)) + return false; + + for (auto CurrentInputObject : (*InputObjectsPtr)) + { + if (CurrentInputObject && CurrentInputObject->NeedsToTriggerUpdate()) + return true; + } + + return false; +} + +FString +UHoudiniInput::GetNodeBaseName() const +{ + UHoudiniCookable* HC = Cast(GetOuter()); + FString NodeBaseName = HC ? HC->GetDisplayName() : TEXT("HoudiniAsset"); + + // Unfortunately CreateInputNode always prefix with input_... + if (IsObjectPathParameter()) + NodeBaseName += TEXT("_") + GetInputName(); + else + NodeBaseName += TEXT("_input") + FString::FromInt(GetInputIndex()); + + return NodeBaseName; +} + +void +UHoudiniInput::OnTransformUIExpand(const int32& AtIndex) +{ +#if WITH_EDITORONLY_DATA + if (TransformUIExpanded.IsValidIndex(AtIndex)) + { + TransformUIExpanded[AtIndex] = !TransformUIExpanded[AtIndex]; + } + else + { + // We need to append values to the expanded array + for (int32 Index = TransformUIExpanded.Num(); Index <= AtIndex; Index++) + { + TransformUIExpanded.Add(Index == AtIndex ? true : false); + } + } +#endif +} + +bool +UHoudiniInput::IsTransformUIExpanded(const int32& AtIndex) +{ +#if WITH_EDITORONLY_DATA + return TransformUIExpanded.IsValidIndex(AtIndex) ? TransformUIExpanded[AtIndex] : false; +#else + return false; +#endif +}; + +FTransform* +UHoudiniInput::GetTransformOffset(const int32& AtIndex) +{ + UHoudiniInputObject* InObject = GetHoudiniInputObjectAt(AtIndex); + if (InObject) + return &InObject->GetTransform(); + + return nullptr; +} + +void +UHoudiniInput::SetUserInputAndTransformRotators(int AtIndex, const FRotator& Rotator) +{ + UHoudiniInputObject* InObject = GetHoudiniInputObjectAt(AtIndex); + if (!InObject) + return; + + // Set both the User Input and the transform. + InObject->UserInputRotator = Rotator; + InObject->GetTransform().SetRotation(Rotator.Quaternion()); +} + +FRotator +UHoudiniInput::GetUserInputRotator(int AtIndex) const +{ + const UHoudiniInputObject* InObject = GetHoudiniInputObjectAt(AtIndex); + if (InObject) + return InObject->UserInputRotator; + + return FRotator::ZeroRotator; +} + +FTransform +UHoudiniInput::GetTransformOffset(const int32& AtIndex) const +{ + const UHoudiniInputObject* InObject = GetHoudiniInputObjectAt(AtIndex); + if (InObject) + return InObject->GetTransform(); + + return FTransform::Identity; +} + +TOptional +UHoudiniInput::GetPositionOffsetX(int32 AtIndex) const +{ + return GetTransformOffset(AtIndex).GetLocation().X; +} + +TOptional +UHoudiniInput::GetPositionOffsetY(int32 AtIndex) const +{ + return GetTransformOffset(AtIndex).GetLocation().Y; +} + +TOptional +UHoudiniInput::GetPositionOffsetZ(int32 AtIndex) const +{ + return GetTransformOffset(AtIndex).GetLocation().Z; +} + +TOptional +UHoudiniInput::GetUserInputRoll(int32 AtIndex) const +{ + return GetUserInputRotator(AtIndex).Roll; +} + +TOptional +UHoudiniInput::GetUserInputPitch(int32 AtIndex) const +{ + return GetUserInputRotator(AtIndex).Pitch; +} + +TOptional +UHoudiniInput::GetUserInputYaw(int32 AtIndex) const +{ + return GetUserInputRotator(AtIndex).Yaw; +} + +TOptional +UHoudiniInput::GetScaleOffsetX(int32 AtIndex) const +{ + return GetTransformOffset(AtIndex).GetScale3D().X; +} + +TOptional +UHoudiniInput::GetScaleOffsetY(int32 AtIndex) const +{ + return GetTransformOffset(AtIndex).GetScale3D().Y; +} + +TOptional +UHoudiniInput::GetScaleOffsetZ(int32 AtIndex) const +{ + return GetTransformOffset(AtIndex).GetScale3D().Z; +} + +bool +UHoudiniInput::SetTransformOffsetAt(const float& Value, const int32& AtIndex, const int32& PosRotScaleIndex, const int32& XYZIndex) +{ + FTransform* Transform = GetTransformOffset(AtIndex); + if (!Transform) + return false; + + FRotator UIRotator = GetUserInputRotator(AtIndex); + + if (PosRotScaleIndex == 0) + { + FVector Position = Transform->GetLocation(); + if (Position[XYZIndex] == Value) + return false; + Position[XYZIndex] = Value; + Transform->SetLocation(Position); + } + else if (PosRotScaleIndex == 1) + { + // FRotator Rotator = Transform->Rotator(); + switch (XYZIndex) + { + case 0: + { + if (UIRotator.Roll == Value) + return false; + UIRotator.Roll = Value; + break; + } + + case 1: + { + if (UIRotator.Pitch == Value) + return false; + UIRotator.Pitch = Value; + break; + } + + case 2: + { + if (UIRotator.Yaw == Value) + return false; + UIRotator.Yaw = Value; + break; + } + } + SetUserInputAndTransformRotators(AtIndex, UIRotator); + } + else if (PosRotScaleIndex == 2) + { + FVector Scale = Transform->GetScale3D(); + if (Scale[XYZIndex] == Value) + return false; + + Scale[XYZIndex] = Value; + Transform->SetScale3D(Scale); + } + + MarkChanged(true); + bStaticMeshChanged = true; + + return true; +} + +void +UHoudiniInput::SetAddRotAndScaleAttributes(const bool& InValue) +{ + if (InputSettings.bAddRotAndScaleAttributesOnCurves == InValue) + return; + + InputSettings.bAddRotAndScaleAttributesOnCurves = InValue; + + // Mark all input obj as changed + MarkAllInputObjectsChanged(true); +} + +void +UHoudiniInput::SetUseLegacyInputCurve(const bool& InValue) +{ + if (InputSettings.bUseLegacyInputCurves == InValue) + return; + + InputSettings.bUseLegacyInputCurves = InValue; + + // Mark all input obj as changed + MarkAllInputObjectsChanged(true); +} + + +bool +UHoudiniInput::HasLandscapeExportTypeChanged () const +{ + if (Type != EHoudiniInputType::World) + return false; + + return bLandscapeHasExportTypeChanged; +} + +void +UHoudiniInput::SetHasLandscapeExportTypeChanged(const bool InChanged) +{ + if (Type != EHoudiniInputType::World) + return; + + bLandscapeHasExportTypeChanged = InChanged; +} + +bool +UHoudiniInput::UpdateWorldSelectionFromBoundSelectors() +{ + TArray>* BoundSelectorObjects = GetBoundSelectorObjectArray(); + if (!BoundSelectorObjects) + return false; + + // Build an array of the current selection's bounds + TArray AllBBox; + for (auto CurrentActor : *BoundSelectorObjects) + { + if (!IsValid(CurrentActor)) + continue; + + AllBBox.Add(CurrentActor->GetComponentsBoundingBox(true, true)); + } + + // + // Select all actors in our bound selectors bounding boxes + // + + // Get our parent component/actor + UHoudiniCookable* OuterCookable = Cast(GetOuter()); + USceneComponent* ParentComponent = OuterCookable ? OuterCookable->GetComponent() : Cast(GetOuter()); + AActor* ParentActor = OuterCookable ? OuterCookable->GetOwner() : nullptr; + + UWorld* MyWorld = GetWorld(); + TArray NewSelectedActors; + for (TActorIterator ActorItr(MyWorld); ActorItr; ++ActorItr) + { + AActor *CurrentActor = *ActorItr; + if (!IsValid(CurrentActor)) + continue; + + // Check that actor is currently not selected + if (BoundSelectorObjects->Contains(CurrentActor)) + continue; + + // Ignore the SkySpheres? + FString ClassName = CurrentActor->GetClass() ? CurrentActor->GetClass()->GetName() : FString(); + if (ClassName.Contains("BP_Sky_Sphere")) + continue; + + // Hacky solution to prevent selecting the UE5 Sky sphere mesh + FString ActorLabel = CurrentActor->GetActorNameOrLabel(); + if (ActorLabel.Contains("SkySphere") || ActorLabel.Contains("Sky_Sphere")) + { + continue; + } + + // Don't allow selection of ourselves. Bad things happen if we do. + if (ParentActor && (CurrentActor == ParentActor)) + continue; + + // For BrushActors, both the actor and its brush must be valid + ABrush* BrushActor = Cast(CurrentActor); + if (BrushActor) + { + if (!IsValid(BrushActor->Brush)) + continue; + } + + FBox ActorBounds = CurrentActor->GetComponentsBoundingBox(true); + for (auto InBounds : AllBBox) + { + // Check if both actor's bounds intersects + if (!ActorBounds.Intersect(InBounds)) + continue; + + NewSelectedActors.Add(CurrentActor); + break; + } + } + + return UpdateWorldSelection(NewSelectedActors); +} + +bool +UHoudiniInput::UpdateWorldSelection(const TArray& InNewSelection) +{ + // TODO: Can we use a set for NewSelectedActors instead? + TArray NewSelectedActors = InNewSelection; + TArray>* InputObjects = GetHoudiniInputObjectArray(EHoudiniInputType::World); + + // Update our current selection with the new one + // Keep actors that are still selected, remove the one that are not selected anymore + bool bHasSelectionChanged = false; + for (int32 Idx = InputObjects->Num() - 1; Idx >= 0; Idx--) + { + UHoudiniInputActor* InputActor = Cast((*InputObjects)[Idx]); + AActor* CurActor = InputActor ? InputActor->GetActor() : nullptr; + + if (!InputActor) + { + // Try HoudiniAsset inputs + UHoudiniInputHoudiniAsset* HDAInput = Cast((*InputObjects)[Idx]); + if(HDAInput && HDAInput->GetHoudiniCookable()) + CurActor = HDAInput->GetHoudiniCookable()->GetOwner(); + } + + if (CurActor && NewSelectedActors.Contains(CurActor)) + { + // The actor is still selected, remove it from the new selection + NewSelectedActors.Remove(CurActor); + } + else + { + // That actor is no longer selected, remove it from our current selection + DeleteInputObjectAt(EHoudiniInputType::World, Idx); + bHasSelectionChanged = true; + } + } + + if (NewSelectedActors.Num() > 0) + bHasSelectionChanged = true; + + // Then add the newly selected Actors + int32 InputObjectIdx = GetNumberOfInputObjects(EHoudiniInputType::World); + int32 NewInputObjectNumber = InputObjectIdx + NewSelectedActors.Num(); + SetInputObjectsNumber(EHoudiniInputType::World, NewInputObjectNumber); + for (const auto& CurActor : NewSelectedActors) + { + // Update the input objects from the valid selected actors array + SetInputObjectAt(InputObjectIdx++, CurActor); + } + + if (!bIsWorldInputBoundSelector && InputSettings.bLandscapeAutoSelectSplines && AddAllLandscapeSplineActorsForInputLandscapes()) + bHasSelectionChanged = true; + + MarkChanged(bHasSelectionChanged); + + return bHasSelectionChanged; +} + + +bool +UHoudiniInput::ContainsInputObject(const UObject* InObject, const EHoudiniInputType& InType) const +{ + if (!IsValid(InObject)) + return false; + + // Returns true if the object is one of our input object for the given type + const TArray>* ObjectArray = GetHoudiniInputObjectArray(InType); + if (!ObjectArray) + return false; + + for (auto& CurrentInputObject : (*ObjectArray)) + { + if (!IsValid(CurrentInputObject)) + continue; + + if (CurrentInputObject->GetObject() == InObject) + return true; + } + + return false; +} + +void UHoudiniInput::ForAllHoudiniInputObjects(TFunctionRef Fn, const bool bFilterByInputType) const +{ + auto ShouldIncludeFn = [&](const EHoudiniInputType InInputType) -> bool + { + return !bFilterByInputType || (bFilterByInputType && GetInputType() == InInputType); + }; + + if (ShouldIncludeFn(EHoudiniInputType::Geometry)) + { + for(UHoudiniInputObject* InputObject : GeometryInputObjects) + { + Fn(InputObject); + } + } + + if (ShouldIncludeFn(EHoudiniInputType::Curve)) + { + for(UHoudiniInputObject* InputObject : CurveInputObjects) + { + Fn(InputObject); + } + } + + if (ShouldIncludeFn(EHoudiniInputType::World)) + { + for(UHoudiniInputObject* InputObject : WorldInputObjects) + { + Fn(InputObject); + } + } +} + +TArray>*> UHoudiniInput::GetAllObjectArrays() const +{ + return { &GeometryInputObjects, &CurveInputObjects, &WorldInputObjects }; +} + +TArray>*> UHoudiniInput::GetAllObjectArrays() +{ + return { &GeometryInputObjects, &CurveInputObjects, &WorldInputObjects }; +} + +void UHoudiniInput::ForAllHoudiniInputObjectArrays(TFunctionRef>&)> Fn) const +{ + TArray>*> ObjectArrays = GetAllObjectArrays(); + for (const TArray>* ObjectArrayPtr : ObjectArrays) + { + if (!ObjectArrayPtr) + continue; + Fn(*ObjectArrayPtr); + } +} + +void UHoudiniInput::ForAllHoudiniInputObjectArrays(TFunctionRef>&)> Fn) +{ + TArray>*> ObjectArrays = GetAllObjectArrays(); + for (TArray>* ObjectArrayPtr : ObjectArrays) + { + if (!ObjectArrayPtr) + continue; + Fn(*ObjectArrayPtr); + } +} + +void UHoudiniInput::GetAllHoudiniInputObjects(TArray& OutObjects) const +{ + OutObjects.Empty(); + auto AddInputObject = [&OutObjects](UHoudiniInputObject* InputObject) + { + if (InputObject) + OutObjects.Add(InputObject); + }; + ForAllHoudiniInputObjects(AddInputObject); +} + +void UHoudiniInput::ForAllHoudiniInputSceneComponents(TFunctionRef Fn) const +{ + auto ProcessSceneComponent = [Fn](UHoudiniInputObject* InputObject) + { + if (!InputObject) + return; + UHoudiniInputSceneComponent* SceneComponentInput = Cast(InputObject); + if (!SceneComponentInput) + return; + Fn(SceneComponentInput); + }; + ForAllHoudiniInputObjects(ProcessSceneComponent); +} + +void UHoudiniInput::GetAllHoudiniInputSceneComponents(TArray& OutObjects) const +{ + OutObjects.Empty(); + auto AddSceneComponent = [&OutObjects](UHoudiniInputObject* InputObject) + { + if (!InputObject) + return; + UHoudiniInputSceneComponent* SceneComponentInput = Cast(InputObject); + if (!SceneComponentInput) + return; + OutObjects.Add(SceneComponentInput); + }; + ForAllHoudiniInputObjects(AddSceneComponent); +} + +void UHoudiniInput::GetAllHoudiniInputSplineComponents(TArray& OutObjects) const +{ + OutObjects.Empty(); + auto AddSceneComponent = [&OutObjects](UHoudiniInputObject* InputObject) + { + if (!InputObject) + return; + UHoudiniInputHoudiniSplineComponent* SceneComponentInput = Cast(InputObject); + if (!SceneComponentInput) + return; + OutObjects.Add(SceneComponentInput); + }; + ForAllHoudiniInputObjects(AddSceneComponent); +} + + +void UHoudiniInput::RemoveHoudiniInputObject(UHoudiniInputObject* InInputObject) +{ + if (!InInputObject) + return; + + ForAllHoudiniInputObjectArrays([InInputObject](TArray>& ObjectArray) { + ObjectArray.Remove(InInputObject); + }); + + return; +} + +void UHoudiniInput::SetLandscapeAutoSelectSplines(const bool bInLandscapeAutoSelectSplines) +{ + if (bInLandscapeAutoSelectSplines == InputSettings.bLandscapeAutoSelectSplines) + return; + + InputSettings.bLandscapeAutoSelectSplines = bInLandscapeAutoSelectSplines; +} + +bool +UHoudiniInput::RemoveAllLandscapeSplineActorsForInputLandscapes() +{ + // Only support world inputs + if (Type != EHoudiniInputType::World) + return false; + + // First determine all selected landscapes + const int32 NumWorldInputObjects = WorldInputObjects.Num(); + TSet SelectedLandscapes; + for (UHoudiniInputObject const* const InputObject : WorldInputObjects) + { + if (!IsValid(InputObject)) + continue; + UHoudiniInputLandscape const* const InputLandscape = Cast(InputObject); + if (!IsValid(InputLandscape)) + continue; + ALandscapeProxy const* const InputLandscapeProxy = InputLandscape->GetLandscapeProxy(); + if (!IsValid(InputLandscapeProxy)) + continue; + if (SelectedLandscapes.IsEmpty()) + SelectedLandscapes.Reserve(NumWorldInputObjects); + SelectedLandscapes.Add(InputLandscapeProxy); + } + + // If no landscapes are selected, then we don't have to deselect any landscape splines + if (SelectedLandscapes.IsEmpty()) + return false; + + // Get the index of each landscape spline that is in our world input objects selection that belongs to one of + // the selected landscapes + TArray IndicesToRemove; + for (int32 Index = 0; Index < NumWorldInputObjects; ++Index) + { + UHoudiniInputObject const* const InputObject = WorldInputObjects[Index]; + if (!IsValid(InputObject)) + continue; + UHoudiniInputLandscapeSplineActor const* const InputObjectSplineActor = Cast(InputObject); + if (!IsValid(InputObjectSplineActor)) + continue; + ALandscapeSplineActor const* const SplineActor = InputObjectSplineActor->GetLandscapeSplineActor(); + if (!IsValid(SplineActor)) + continue; + ULandscapeInfo const* const LandscapeInfo = SplineActor->GetLandscapeInfo(); + if (!IsValid(LandscapeInfo)) + continue; + ALandscapeProxy const* const LandscapeProxy = LandscapeInfo->GetLandscapeProxy(); + if (!IsValid(LandscapeProxy)) + continue; + if (!SelectedLandscapes.Contains(LandscapeProxy)) + continue; + if (IndicesToRemove.IsEmpty()) + IndicesToRemove.Reserve(NumWorldInputObjects); + IndicesToRemove.Add(Index); + } + + // Remove the landscape splines from highest index to lowest index + const int32 NumIndicesToRemove = IndicesToRemove.Num(); + if (NumIndicesToRemove <= 0) + return false; + + for (int32 Index = NumIndicesToRemove - 1; Index >= 0; --Index) + { + const int32 IndexToRemove = IndicesToRemove[Index]; + DeleteInputObjectAt(EHoudiniInputType::World, IndexToRemove); + } + + return true; +} + +bool +UHoudiniInput::AddAllLandscapeSplineActorsForInputLandscapes() +{ +#if WITH_EDITOR + // Only support world inputs + if (Type != EHoudiniInputType::World) + return false; + + // Find the current input landscape and splines + TSet InputLandscapes; + TSet InputLandscapeSpineActors; + + for (UHoudiniInputObject const* const InputObject : WorldInputObjects) + { + if (!IsValid(InputObject)) + continue; + + UHoudiniInputLandscape const* const LandscapeObject = Cast(InputObject); + if (IsValid(LandscapeObject)) + { + InputLandscapes.Add(LandscapeObject); + continue; + } + + UHoudiniInputLandscapeSplineActor const* const LandscapeSplineActor = Cast(InputObject); + if (!IsValid(LandscapeSplineActor)) + continue; + + ALandscapeSplineActor const* const SplineActor = LandscapeSplineActor->GetLandscapeSplineActor(); + if (!IsValid(SplineActor)) + continue; + + InputLandscapeSpineActors.Add(SplineActor); + } + + if (InputLandscapes.IsEmpty()) + return false; + + // Get all spline actors that belong to InputLandscape splines but that are not in InputLandscapeSplineActors + TArray SplinesToAdd; + for (UHoudiniInputLandscape const* const InputLandscape : InputLandscapes) + { + if (!IsValid(InputLandscape)) + continue; + ALandscapeProxy const* const LandscapeProxy = InputLandscape->GetLandscapeProxy(); + if (!IsValid(LandscapeProxy)) + continue; + ULandscapeInfo const* const LandscapeInfo = LandscapeProxy->GetLandscapeInfo(); + if (!IsValid(LandscapeInfo)) + continue; + LandscapeInfo->ForAllSplineActors([&SplinesToAdd, &InputLandscapeSpineActors](TScriptInterface InSplineActor) + { + ALandscapeSplineActor* const SplineActor = Cast(InSplineActor.GetObject()); + if (IsValid(SplineActor) && !InputLandscapeSpineActors.Contains(SplineActor)) + SplinesToAdd.Add(SplineActor); + }); + } + + // There are no new spline actors to add + if (SplinesToAdd.IsEmpty()) + return false; + + // Add SplinesToAdd to the input object array + const int32 NumInputObjects = GetNumberOfInputObjects(); + const int32 NumToAdd = SplinesToAdd.Num(); + SetInputObjectsNumber(GetInputType(), NumInputObjects + NumToAdd); + for (int32 Index = 0; Index < NumToAdd; ++Index) + SetInputObjectAt(NumInputObjects + Index, SplinesToAdd[Index]); + + return true; +#else + return false; +#endif +} + +void +UHoudiniInput::OnSessionConnected() +{ + AssetNodeId = INDEX_NONE; + InputNodeId = INDEX_NONE; + + auto ResetInputObjects = [](TArray>* InputObjectArray) + { + if(!InputObjectArray) + return; + for(UHoudiniInputObject* InputObject : *InputObjectArray) + { + if(InputObject) + { + InputObject->SetInputNodeId(INDEX_NONE); + InputObject->SetInputObjectNodeId(INDEX_NONE); + } + } + }; + + TArray>* InputObjectArray = GetHoudiniInputObjectArray(EHoudiniInputType::Curve); + ResetInputObjects(InputObjectArray); + if(InputObjectArray) + { + for(UHoudiniInputObject* InputObject : *InputObjectArray) + { + UHoudiniInputHoudiniSplineComponent* HoudiniSplineInput = Cast(InputObject); + if(!HoudiniSplineInput) + continue; + + UHoudiniSplineComponent* HoudiniSplineComponent = HoudiniSplineInput->GetCurveComponent(); + HoudiniSplineComponent->SetNodeId(INDEX_NONE); + } + } + + InputObjectArray = GetHoudiniInputObjectArray(EHoudiniInputType::Geometry); + ResetInputObjects(InputObjectArray); + + InputObjectArray = GetHoudiniInputObjectArray(EHoudiniInputType::World); + ResetInputObjects(InputObjectArray); + +} + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniInput.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreTypes.h" +#include "UObject/Object.h" +#include "UObject/ObjectMacros.h" +#include "Misc/Optional.h" + +#include "HoudiniInputTypes.h" +#include "HoudiniInputObject.h" + +#include "GameFramework/Actor.h" +#include "LandscapeProxy.h" + +#include "HoudiniInput.generated.h" + + + +class FReply; + +enum class EHoudiniCurveType : int8; +enum class ECheckBoxState : unsigned char; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInput : public UObject +{ + GENERATED_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + UHoudiniInput(); + + void OnSessionConnected(); + + virtual void Serialize(FArchive& Ar) override; + + // Equality operator, + // We consider two inputs equals if they have the same name, objparam state, and input index/parmId + // TODO: ParmId might be an incorrect condition + bool operator==(const UHoudiniInput& other) const + { + return (bIsObjectPathParameter == other.bIsObjectPathParameter + && InputIndex == other.InputIndex + && ParmId == other.ParmId + && Name.Equals(other.Name) + && Label.Equals(other.Label)); + } + + bool Matches(const UHoudiniInput& other) const { return (*this == other); }; + + // Helper function returning a string from an InputType + static FString InputTypeToString(const EHoudiniInputType& InInputType); + + // Helper function returning an InputType from a string + static EHoudiniInputType StringToInputType(const FString& InInputTypeString); + // Helper function returning a Houdini curve type from a string + static EHoudiniCurveType StringToHoudiniCurveType(const FString& CurveTypeString); + // Helper function returning a Houdini curve method from a string + static EHoudiniCurveMethod StringToHoudiniCurveMethod(const FString& CurveMethodString); + // Helper function returning a Houdini curve breakpoint parameterization from a string + static EHoudiniCurveBreakpointParameterization StringToHoudiniCurveBreakpointParameterization( + const FString& CurveParameterizationString); + + // Helper function indicating what classes are supported by an input type + static TArray GetAllowedClasses(const EHoudiniInputType& InInputType); + + // Helper function indicating if an object is supported by an input type + static bool IsObjectAcceptable(const EHoudiniInputType& InInputType, const UObject* InObject); + + //------------------------------------------------------------------------------------------------ + // Accessors + //------------------------------------------------------------------------------------------------ + + // Returns the NodeId of the asset / object merge we are associated with + int32 GetAssetNodeId() const + { return AssetNodeId; }; + // For objpath parameter, return the associated ParamId, -1 if we're a Geo input + int32 GetParameterId() const + { return bIsObjectPathParameter ? ParmId : -1; }; + // Returns the NodeId of the node plugged into this input + int32 GetInputNodeId() const + { return InputNodeId; }; + + bool IsAssetInput() const; + bool IsLandscapeInput() const; + + // For Geo inputs, returns the InputIndex, -1 if we're an object path parameter + int32 GetInputIndex() const + { return bIsObjectPathParameter ? -1 : InputIndex; }; + // Return the array containing all the nodes created for this input's data + TArray& GetCreatedDataNodeIds() + { return CreatedDataNodeIds; }; + // Returns the current input type + EHoudiniInputType GetInputType() const + { return Type; }; + // Returns the previous input type + EHoudiniInputType GetPreviousInputType() const + { return PreviousType; }; + // Returns the current input type as a string + FString GetInputTypeAsString() const + { return InputTypeToString(Type); }; + + EHoudiniXformType GetDefaultXTransformType(); + // Returns true when this input's Transform Type is set to NONE, + // false if set to INTO_THIS_OBJECT, 2 will use the input's type default value + bool GetKeepWorldTransform() const; + // Indicates if this input has changed and should be updated + bool HasChanged(); + // Indicates if this input needs to trigger an update + bool NeedsToTriggerUpdate(); + // Indicates this input should upload its data + bool IsDataUploadNeeded(); + // Indicates this input's transform need to be uploaded + bool IsTransformUploadNeeded(); + // Indicates if this input type has been changed + bool HasInputTypeChanged() const + { return PreviousType != EHoudiniInputType::Invalid ? PreviousType != Type : false; } + + FString GetInputName() const { return Name; }; + FString GetInputLabel() const { return Label; }; + FString GetInputHelp() const { return Help; }; + bool GetPackBeforeMerge() const { return bPackBeforeMerge; }; + bool GetImportAsReference() const { return InputSettings.bImportAsReference; }; + bool GetImportAsReferenceRotScaleEnabled() const { return InputSettings.bImportAsReferenceRotScaleEnabled; }; + bool GetImportAsReferenceBboxEnabled() const { return InputSettings.bImportAsReferenceBboxEnabled; }; + bool GetImportAsReferenceMaterialEnabled() const { return InputSettings.bImportAsReferenceMaterialEnabled; }; + bool GetExportMainGeometry() const { return InputSettings.bExportMainGeometry; }; + bool GetExportLODs() const { return InputSettings.bExportLODs; }; + bool GetExportSockets() const { return InputSettings.bExportSockets; }; + bool GetPreferNaniteFallbackMesh() const { return InputSettings.bPreferNaniteFallbackMesh; } + bool GetExportColliders() const { return InputSettings.bExportColliders; }; + bool GetExportMaterialParameters() const { return InputSettings.bExportMaterialParameters; }; + bool GetDirectlyConnectHdas() const { return bDirectlyConnectHdas; } + bool GetExportOptionsMenuExpanded() const { return bExportOptionsMenuExpanded; } + bool GetGeometryInputsMenuExpanded() const { return bGeometryInputsMenuExpanded; } + bool GetLandscapeOptionsMenuExpanded() const { return bLandscapeOptionsMenuExpanded; } + bool GetWorldInputsMenuExpanded() const { return bWorldInputsMenuExpanded; } + bool GetCurveInputsMenuExpanded() const { return bCurveInputsMenuExpanded; } + bool GetCurvePointSelectionMenuExpanded() const { return bCurvePointSelectionMenuExpanded; } + bool GetCurvePointSelectionUseAbsLocation() const { return bCurvePointSelectionUseAbsLocation; } + bool GetCurvePointSelectionUseAbsRotation() const { return bCurvePointSelectionUseAbsRotation; } + bool IsObjectPathParameter() const { return bIsObjectPathParameter; }; + bool IsCOPInput() const { return bIsCOPInput; }; + float GetUnrealSplineResolution() const { return InputSettings.UnrealSplineResolution; }; + virtual bool GetCookOnCurveChange() const { return bCookOnCurveChanged; }; + + TArray>* GetHoudiniInputObjectArray(const EHoudiniInputType& InType); + const TArray>* GetHoudiniInputObjectArray(const EHoudiniInputType& InType) const; + TArray>* GetBoundSelectorObjectArray(); + const TArray>* GetBoundSelectorObjectArray() const; + + UHoudiniInputObject* GetHoudiniInputObjectAt(const int32& AtIndex); + const UHoudiniInputObject* GetHoudiniInputObjectAt(const int32& AtIndex) const; + AActor* GetBoundSelectorObjectAt(const int32& AtIndex); + + UHoudiniInputObject* GetHoudiniInputObjectAt(const EHoudiniInputType& InType, const int32& AtIndex); + + UObject* GetInputObjectAt(const int32& AtIndex); + UObject* GetInputObjectAt(const EHoudiniInputType& InType, const int32& AtIndex); + + int32 GetNumberOfInputObjects(); + int32 GetNumberOfInputObjects(const EHoudiniInputType& InType); + + int32 GetNumberOfInputMeshes(); + int32 GetNumberOfInputMeshes(const EHoudiniInputType& InType); + + int32 GetNumberOfBoundSelectorObjects() const; + + bool IsWorldInputBoundSelector() const { return bIsWorldInputBoundSelector; }; + bool GetWorldInputBoundSelectorAutoUpdates() const { return bWorldInputBoundSelectorAutoUpdate; }; + bool GetWorldInputAutoUpdates() const { return bWorldInputAutoUpdate; }; + + FString GetNodeBaseName() const; + + bool IsTransformUIExpanded(const int32& AtIndex); + + // Return the transform offset for a given input object + FTransform* GetTransformOffset(const int32& AtIndex); + FTransform GetTransformOffset(const int32& AtIndex) const; + + // Returns the position offset for a given input object + TOptional GetPositionOffsetX(int32 AtIndex) const; + TOptional GetPositionOffsetY(int32 AtIndex) const; + TOptional GetPositionOffsetZ(int32 AtIndex) const; + + // Returns the scale offset for a given input object + TOptional GetScaleOffsetX(int32 AtIndex) const; + TOptional GetScaleOffsetY(int32 AtIndex) const; + TOptional GetScaleOffsetZ(int32 AtIndex) const; + + // User input functions. We keep a user readable version of Roll, Pitch, Yaw that + // the user can edit rather than getting the Quaternion from the FTransform as + // converting Roll,Pitch,Yaw to and back from a Quaternion gets different results + // which is neither user friendly or particularly useable. + FRotator GetUserInputRotator(int AtIndex) const; + void SetUserInputAndTransformRotators(int AtIndex, const FRotator& Rotator); + + TOptional GetUserInputRoll(int32 AtIndex) const; + TOptional GetUserInputPitch(int32 AtIndex) const; + TOptional GetUserInputYaw(int32 AtIndex) const; + + // Returns true if the object is one of our input object for the given type + bool ContainsInputObject(const UObject* InObject, const EHoudiniInputType& InType) const; + + // Get all input object arrays + TArray>*> GetAllObjectArrays() const; + TArray>*> GetAllObjectArrays(); + + // Iterate over all input object arrays + void ForAllHoudiniInputObjectArrays(TFunctionRef>&)> Fn) const; + void ForAllHoudiniInputObjectArrays(TFunctionRef>&)> Fn); + + // Return ALL input objects. Optionally, the results can be filtered to only return input objects + // relevant to the current *input type*. + void ForAllHoudiniInputObjects(TFunctionRef Fn, const bool bFilterByInputType=false) const; + // Collect top-level HoudiniInputObjects from this UHoudiniInput. Does not traverse nested input objects. + void GetAllHoudiniInputObjects(TArray& OutObjects) const; + // Collect top-level UHoudiniInputSceneComponent from this UHoudiniInput. Does not traverse nested input objects. + void ForAllHoudiniInputSceneComponents(TFunctionRef Fn) const; + void GetAllHoudiniInputSceneComponents(TArray& OutObjects) const; + void GetAllHoudiniInputSplineComponents(TArray& OutObjects) const; + + // Remove all instances of this input object from all object arrays. + void RemoveHoudiniInputObject(UHoudiniInputObject* InInputObject); + + bool IsAddRotAndScaleAttributesEnabled() const { return InputSettings.bAddRotAndScaleAttributesOnCurves; }; + + bool IsUseLegacyInputCurvesEnabled() const { return InputSettings.bUseLegacyInputCurves; }; + + bool IsLandscapeAutoSelectComponentEnabled() const { return InputSettings.bLandscapeAutoSelectComponent; } + bool IsLandscapeExportSelectionOnlyEnabled() const { return InputSettings.bLandscapeExportSelectionOnly; } + bool IsLandscapeExportLightingEnabled() const { return InputSettings.bLandscapeExportLighting; } + bool IsLandscapeExportMaterialsEnabled() const { return InputSettings.bLandscapeExportMaterials; } + bool IsLandscapeExportNormalizedUVsEnabled() const { return InputSettings.bLandscapeExportNormalizedUVs; } + bool IsLandscapeExportTileUVsEnabled() const { return InputSettings.bLandscapeExportTileUVs; } + + bool IsEditLayerHeightExportEnabled() const { return InputSettings.bExportHeightDataPerEditLayer; } + bool IsPaintLayerPerEditLayerExportEnabled() const { return InputSettings.bExportPaintLayersPerEditLayer; } + bool IsMergedPaintLayerExportEnabled() const { return InputSettings.bExportMergedPaintLayers; } + + bool IsExportLevelInstanceContentEnabled() const { return InputSettings.bExportLevelInstanceContent; } + + const TSet> GetLandscapeSelectedComponents() const { return LandscapeSelectedComponents; }; + + // Get a constant reference to the InputSettings + const FHoudiniInputObjectSettings& GetInputSettings() const { return InputSettings; } + + // Copy the InputSettings into OutInputSettings. + void CopyInputSettingsTo(FHoudiniInputObjectSettings& OutInputSettings) const { OutInputSettings = InputSettings; } + + //------------------------------------------------------------------------------------------------ + // Mutators + //------------------------------------------------------------------------------------------------ + + void MarkChanged(const bool& bInChanged) + { + bHasChanged = bInChanged; + SetNeedsToTriggerUpdate(bInChanged); + }; + + void SetNeedsToTriggerUpdate(const bool& bInTriggersUpdate) + { bNeedsToTriggerUpdate = bInTriggersUpdate; }; + void MarkDataUploadNeeded(const bool& bInDataUploadNeeded) + { bDataUploadNeeded = bInDataUploadNeeded; }; + void MarkAllInputObjectsChanged(const bool& bInChanged); + + void SetSOPInput(const int32& InInputIndex); + void SetCOPInput(const int32& InInputIndex); + void SetObjectPathParameter(const int32& InParmId, const bool& bIsCOP); + void SetKeepWorldTransform(const bool& bInKeepWorldTransform); + + void SetName(const FString& InName) + { Name = InName; }; + void SetLabel(const FString& InLabel) + { Label = InLabel; }; + void SetHelp(const FString& InHelp) + { Help = InHelp; }; + void SetAssetNodeId(const int32& InNodeId) + { AssetNodeId = InNodeId; }; + void SetInputType(const EHoudiniInputType &InInputType, bool& bOutBlueprintStructureModified); + + void SetPreviousInputType(const EHoudiniInputType& InType) + { PreviousType = InType; }; + void SetPackBeforeMerge(const bool& bInPackBeforeMerge) + { bPackBeforeMerge = bInPackBeforeMerge; }; + void SetImportAsReference(const bool& bInImportAsReference) + { InputSettings.bImportAsReference = bInImportAsReference; }; + void SetImportAsReferenceRotScaleEnabled(const bool& bInImportAsReferenceRotScaleEnabled) + { InputSettings.bImportAsReferenceRotScaleEnabled = bInImportAsReferenceRotScaleEnabled; }; + void SetImportAsReferenceBboxEnabled(const bool& bInImportAsReferenceBboxEnabled) + { InputSettings.bImportAsReferenceBboxEnabled = bInImportAsReferenceBboxEnabled; }; + void SetImportAsReferenceMaterialEnabled(const bool& bInImportAsReferenceMaterialEnabled) + { InputSettings.bImportAsReferenceMaterialEnabled = bInImportAsReferenceMaterialEnabled; }; + + void SetExportMainGeometry(const bool& bInExportMainGeo) + { InputSettings.bExportMainGeometry = bInExportMainGeo; }; + void SetExportLODs(const bool& bInExportLODs) + { InputSettings.bExportLODs = bInExportLODs; }; + void SetExportSockets(const bool& bInExportSockets) + { InputSettings.bExportSockets = bInExportSockets; }; + void SetPreferNaniteFallbackMesh(const bool& bInPreferNaniteFallbackMesh) + { InputSettings.bPreferNaniteFallbackMesh = bInPreferNaniteFallbackMesh; }; + void SetExportColliders(const bool& bInExportColliders) + { InputSettings.bExportColliders = bInExportColliders; }; + void SetExportMaterialParameters(const bool& bInExportMaterialParameters) + { InputSettings.bExportMaterialParameters = bInExportMaterialParameters; }; + void SetDirectlyConnectHdas(const bool& bInDirectlyConnectHdas) + { bDirectlyConnectHdas = bInDirectlyConnectHdas; } + void SetExportOptionsMenuExpanded(const bool& bInExportOptionsMenuExpanded) + { bExportOptionsMenuExpanded = bInExportOptionsMenuExpanded; }; + void SetGeometryInputsMenuExpanded(const bool& bInGeometryInputsMenuExpanded) + { bGeometryInputsMenuExpanded = bInGeometryInputsMenuExpanded; } + void SetLandscapeOptionsMenuExpanded(const bool& bInLandscapeOptionsMenuExpanded) + { bLandscapeOptionsMenuExpanded = bInLandscapeOptionsMenuExpanded; } + void SetWorldInputsMenuExpanded(const bool& bInWorldInputsMenuExpanded) + { bWorldInputsMenuExpanded = bInWorldInputsMenuExpanded; } + void SetCurveInputsMenuExpanded(const bool& bInCurveInputsMenuExpanded) + { bCurveInputsMenuExpanded = bInCurveInputsMenuExpanded; } + void SetCurvePointSelectionMenuExpanded(const bool& bInCurvePointSelectionMenuExpanded) + { bCurvePointSelectionMenuExpanded = bInCurvePointSelectionMenuExpanded; } + void SetCurvePointSelectionUseAbsLocation(const bool & bInCurvePointSelectionUseAbsLocation) + { bCurvePointSelectionUseAbsLocation = bInCurvePointSelectionUseAbsLocation;} + void SetCurvePointSelectionUseAbsRotation(const bool & bInCurvePointSelectionUseAbsRotation) + { bCurvePointSelectionUseAbsRotation = bInCurvePointSelectionUseAbsRotation;} + void SetInputNodeId(const int32& InCreatedNodeId) + { InputNodeId = InCreatedNodeId; }; + void SetUnrealSplineResolution(const float& InResolution) + { InputSettings.UnrealSplineResolution = InResolution; }; + void SetExportHeightDataPerEditLayer(bool bOnOff) + { InputSettings.bExportHeightDataPerEditLayer = bOnOff; } + void SetExportPaintLayerPerEditLayer(bool bOnOff) + { InputSettings.bExportPaintLayersPerEditLayer = bOnOff; } + void SetExportMergedPaintLayers(bool bOnOff) + { InputSettings.bExportMergedPaintLayers = bOnOff; } + void SetExportLevelInstanceContent(bool bOnOff) + { InputSettings.bExportLevelInstanceContent = bOnOff; } + + virtual void SetCookOnCurveChange(const bool & bInCookOnCurveChanged) + { bCookOnCurveChanged = bInCookOnCurveChanged; }; + + void ResetDefaultCurveOffset() + { DefaultCurveOffset = 0.f; } + + UHoudiniInputObject* CreateNewCurveInputObject(bool& bBlueprintStructureModified); + + UHoudiniInputHoudiniSplineComponent* GetOrCreateCurveInputObjectAt( + const int32 Index, + const bool bCreateIndex, + bool& bOutBlueprintStructureModified); + + void SetGeometryInputObjectsNumber(const int32& NewCount); + void SetInputObjectsNumber(const EHoudiniInputType& InType, const int32& InNewCount); + + void InsertInputObjectAt(const int32& AtIndex); + void InsertInputObjectAt(const EHoudiniInputType& InType, const int32& AtIndex); + + void DeleteInputObjectAt(const int32& AtIndex, const bool bInRemoveIndexFromArray=true); + void DeleteInputObjectAt( + const EHoudiniInputType& InType, + const int32& AtIndex, + const bool bInRemoveIndexFromArray=true); + + void DuplicateInputObjectAt(const int32& AtIndex); + void DuplicateInputObjectAt(const EHoudiniInputType& InType, const int32& AtIndex); + + void SetInputObjectAt(const int32& AtIndex, UObject* InObject); + void SetInputObjectAt(const EHoudiniInputType& InType, const int32& AtIndex, UObject* InObject); + + void SetBoundSelectorObjectsNumber(const int32& InNewCount); + void SetBoundSelectorObjectAt(const int32& AtIndex, AActor* InActor); + void SetWorldInputBoundSelector(const bool& InIsBoundSelector) + { bIsWorldInputBoundSelector = InIsBoundSelector; }; + void SetWorldInputBoundSelectorAutoUpdates(const bool& InAutoUpdate) + { bWorldInputBoundSelectorAutoUpdate = InAutoUpdate; }; + void SetWorldInputAutoUpdates(const bool& InAutoUpdate) + { bWorldInputAutoUpdate = InAutoUpdate; }; + + // Updates the world selection using bound selectors + // returns false if the selection hasn't changed + bool UpdateWorldSelectionFromBoundSelectors(); + // Updates the world selection + // returns false if the selection hasn't changed + bool UpdateWorldSelection(const TArray& InNewSelection); + + void OnTransformUIExpand(const int32& AtIndex); + + // Sets the input's transform offset + bool SetTransformOffsetAt( + const float& Value, + const int32& AtIndex, + const int32& PosRotScaleIndex, + const int32& XYZIndex); + + // Sets the input's transform scale values + void SetPositionOffsetX(float InValue, int32 AtIndex); + void SetPositionOffsetY(float InValue, int32 AtIndex); + void SetPositionOffsetZ(float InValue, int32 AtIndex); + + // Sets the input's transform rotation value + void SetRotationOffsetRoll(float InValue, int32 AtIndex); + void SetRotationOffsetPitch(float InValue, int32 AtIndex); + void SetRotationOffsetYaw(float InValue, int32 AtIndex); + + // Sets the input's transform scale values + void SetScaleOffsetX(float InValue, int32 AtIndex); + void SetScaleOffsetY(float InValue, int32 AtIndex); + void SetScaleOffsetZ(float InValue, int32 AtIndex); + + void SetAddRotAndScaleAttributes(const bool& InValue); + void SetUseLegacyInputCurve(const bool& InValue); + + void SetLandscapeAutoSelectComponentEnabled(bool bInEnabled) + { InputSettings.bLandscapeAutoSelectComponent = bInEnabled; } + void SetLandscapeExportSelectionOnlyEnabled(bool bInEnabled) + { InputSettings.bLandscapeExportSelectionOnly = bInEnabled; } + void SetLandscapeExportLightingEnabled(bool bInEnabled) + { InputSettings.bLandscapeExportLighting = bInEnabled; } + void SetLandscapeExportMaterialsEnabled(bool bInEnabled) + { InputSettings.bLandscapeExportMaterials = bInEnabled; } + void SetLandscapeExportNormalizedUVsEnabled(bool bInEnabled) + { InputSettings.bLandscapeExportNormalizedUVs = bInEnabled; } + void SetLandscapeExportTileUVsEnabled(bool bInEnabled) + { InputSettings.bLandscapeExportTileUVs = bInEnabled; } + + // Duplicate this object and copy its state to the resulting object. + // This is typically used to transfer state between between template and instance components. + UHoudiniInput* DuplicateAndCopyState(UObject* DestOuter, bool bInCanDeleteHoudiniNodes); + virtual void CopyStateFrom(UHoudiniInput* InInput, bool bCopyAllProperties, bool bInCanDeleteHoudiniNodes); + + void SetCanDeleteHoudiniNodes(bool bInCanDeleteNodes); + bool CanDeleteHoudiniNodes() + { return bCanDeleteHoudiniNodes; } + + virtual void InvalidateData(); + +protected: + void CopyInputs( + TArray>& ToInputs, + TArray>& FromInputs, + bool bInCanDeleteHoudiniNodes); + +public: + + // Create a Houdini Spline input component, with an existing Houdini Spline input Object. + // Pass in nullptr to create a default Houdini Spline + UHoudiniInputHoudiniSplineComponent* CreateHoudiniSplineInput( + UHoudiniInputHoudiniSplineComponent* FromHoudiniSplineInputObject, + const bool & bAttachToParent, + const bool & bAppendToInputArray, + bool& bOutBlueprintStructureModified); + + // Given an existing spline input object, remove the associated + // Houdini Spline component from the owning actor / blueprint. + void RemoveSplineFromInputObject( + UHoudiniInputHoudiniSplineComponent* InHoudiniSplineInputObject, + bool& bOutBlueprintStructureModified) const; + + bool HasLandscapeExportTypeChanged () const; + + void SetHasLandscapeExportTypeChanged(const bool InChanged); + + EHoudiniLandscapeExportType GetLandscapeExportType() const + { return InputSettings.LandscapeExportType; }; + + void SetLandscapeExportType(const EHoudiniLandscapeExportType InType) + { InputSettings.LandscapeExportType = InType; }; + + virtual void BeginDestroy() override; + +#if WITH_EDITOR + virtual void PostEditUndo() override; +#endif + + FBox GetBounds(UWorld * World); + + void UpdateLandscapeInputSelection(); + + // Add the current InputNodeId to the pending delete set and set it to -1 + void MarkInputNodeAsPendingDelete(); + + // Return the set of previous InputNodeIds that are pending delete + const TSet& GetInputNodesPendingDelete() const + { return InputNodesPendingDelete; } + + // Clear the InputNodesPendingDelete set + void ClearInputNodesPendingDelete() + { InputNodesPendingDelete.Empty(); } + +#if WITH_EDITORONLY_DATA + + UPROPERTY() + bool bLandscapeUIAdvancedIsExpanded; + +#endif + +protected: + + // Name of the input / Object path parameter + UPROPERTY() + FString Name; + + // Label of the SOP input or of the object path parameter + UPROPERTY() + FString Label; + + // Input type + UPROPERTY() + EHoudiniInputType Type; + + // Previous type, used to detect input type changes + UPROPERTY(Transient, DuplicateTransient) + EHoudiniInputType PreviousType; + + // NodeId of the asset / object merge we are associated with + UPROPERTY(Transient, DuplicateTransient) + int32 AssetNodeId; + + // NodeId of the created input node + // when there is multiple inputs objects, this will be the merge node. + UPROPERTY(Transient, DuplicateTransient, NonTransactional) + int32 InputNodeId; + + // SOP input index (-1 if we're an object path input) + UPROPERTY() + int32 InputIndex; + + // Parameter Id of the associated object path parameter (-1 if we're a SOP input) + UPROPERTY(Transient, DuplicateTransient, NonTransactional) + int32 ParmId; + + // Indicates if we're an object path parameter input + UPROPERTY() + bool bIsObjectPathParameter; + + // Indicates if we're a COP input + bool bIsCOPInput; + + // Array containing all the node Ids created by this input + UPROPERTY(Transient, DuplicateTransient, NonTransactional) + TArray CreatedDataNodeIds; + + // Indicates data connected to this input should be uploaded + UPROPERTY(Transient, DuplicateTransient) + bool bHasChanged; + + // Indicates this input should trigger an HDA update/cook + UPROPERTY(Transient, DuplicateTransient) + bool bNeedsToTriggerUpdate; + + // Indicates data for this input needs to be uploaded + // If this is false but the input has changed, we may have just updated in input parameter, + // and don't need to resend all the input data + bool bDataUploadNeeded; + + // Cached Bounds of this input + // Used when we cannot access the input objects (ie, during GC) + UPROPERTY() + FBox CachedBounds; + + // Help for this parameter/input + UPROPERTY() + FString Help; + + //------------------------------------------------------------------------------------------------------------------------- + // General Input options + + // Indicates that the geometry must be packed before merging it into the input + UPROPERTY() + bool bPackBeforeMerge; + + // Indicates whether HDAs are directly connected in Houdini + UPROPERTY() + bool bDirectlyConnectHdas; + + // Indicates whether the export options menu is initially collapsed + UPROPERTY() + bool bExportOptionsMenuExpanded; + + // Indicates whether the input selection list in the NewGeometry UI is collapsed + UPROPERTY() + bool bGeometryInputsMenuExpanded; + + // Indicates whether the landscape options menu is collapsed + UPROPERTY() + bool bLandscapeOptionsMenuExpanded; + + // Indicates whether the input selection menu in the NewWorld UI is collapsed + UPROPERTY() + bool bWorldInputsMenuExpanded; + + // Indicates whether the input selection menu in the new Curve UI is collapsed + UPROPERTY() + bool bCurveInputsMenuExpanded; + + // Indicates whether the spline component point selection control menu in the + // new Curve input UI is collapsed + UPROPERTY() + bool bCurvePointSelectionMenuExpanded; + + // Indicates whether the spline component point selection control menu + // uses absolute location + UPROPERTY() + bool bCurvePointSelectionUseAbsLocation; + + // Indicates whether the spline component point selection control menu + // uses absolute rotation + UPROPERTY() + bool bCurvePointSelectionUseAbsRotation; + + // Indicates that if trigger cook automatically on curve Input spline modified + UPROPERTY() + bool bCookOnCurveChanged; + + //------------------------------------------------------------------------------------------------------------------------- + // Geometry objects + UPROPERTY() + TArray> GeometryInputObjects; + + // Is set to true when static mesh used for geometry input has changed. + UPROPERTY() + bool bStaticMeshChanged; + +#if WITH_EDITORONLY_DATA + // Are the transform UI expanded ? + // Values default to false and are actually added to the array in OnTransformUIExpand() + UPROPERTY() + TArray TransformUIExpanded; +#endif + + //------------------------------------------------------------------------------------------------------------------------- + // Asset inputs + + // Is set to true if the asset input is actually connected inside Houdini. + UPROPERTY() + bool bInputAssetConnectedInHoudini; + + //------------------------------------------------------------------------------------------------------------------------- + // Curve/Spline inputs + UPROPERTY() + TArray> CurveInputObjects; + + // Offset used when using muiltiple curves + UPROPERTY() + float DefaultCurveOffset; + + //------------------------------------------------------------------------------------------------------------------------- + // Landscape inputs + + UPROPERTY() + bool bLandscapeHasExportTypeChanged = false; + + //------------------------------------------------------------------------------------------------------------------------- + // World inputs + UPROPERTY() + TArray> WorldInputObjects; + + // Objects used for automatic bound selection + UPROPERTY() + TArray> WorldInputBoundSelectorObjects; + + // Indicates that this world input is in "BoundSelector" mode + UPROPERTY() + bool bIsWorldInputBoundSelector; + + // Indicates that selected actors by the bound selectors should update automatically + UPROPERTY() + bool bWorldInputBoundSelectorAutoUpdate; + + // Indicates that this world input will update automatically if it detects a change + UPROPERTY() + bool bWorldInputAutoUpdate; + + // A cache of the selected landscape components so that it is saved across levels + UPROPERTY() + TSet> LandscapeSelectedComponents; + + // The node ids of InputNodeIds previously used by this input that are pending delete + UPROPERTY(Transient, DuplicateTransient, NonTransactional) + TSet InputNodesPendingDelete; + + //------------------------------------------------------------------------------------------------------------------------- + // PCG inputs + UPROPERTY() + TArray> PCGInputObjects; +public: + + // Enable/disable auto selecting landscape splines (for landscapes via world input). + void SetLandscapeAutoSelectSplines(const bool bInLandscapeAutoSelectSplines); + + // Is landscape spline auto-selection (for landscapes via world inputs) enabled? + bool IsLandscapeAutoSelectSplinesEnabled() const { return InputSettings.bLandscapeAutoSelectSplines; } + + // Enable/disable exporting a separate control point cloud for landscape splines + void SetLandscapeSplinesExportControlPoints(const bool bInLandscapeSendControlPoints) { InputSettings.bLandscapeSplinesExportControlPoints = bInLandscapeSendControlPoints; } + + // Is exporting a separate control point cloud for landscape splines enabled? + bool IsLandscapeSplinesExportControlPointsEnabled() const { return InputSettings.bLandscapeSplinesExportControlPoints; } + + // Enable/disable exporting left/right curves for landscape splines + void SetLandscapeSplinesExportLeftRightCurves(const bool bInLandscapeSplinesExportLeftRightCurves) { InputSettings.bLandscapeSplinesExportLeftRightCurves = bInLandscapeSplinesExportLeftRightCurves; } + + // Is exporting left/right curves for landscape splines enabled? + bool IsLandscapeSplinesExportLeftRightCurvesEnabled() const { return InputSettings.bLandscapeSplinesExportLeftRightCurves; } + + // Enable/disable exporting the spline mesh components for landscape splines + void SetLandscapeSplinesExportSplineMeshComponents(const bool bInLandscapeSplinesExportSplineMeshes) { InputSettings.bLandscapeSplinesExportSplineMeshComponents = bInLandscapeSplinesExportSplineMeshes; } + + // Is exporting spline mesh components for landscape splines enabled? + bool IsLandscapeSplinesExportSplineMeshComponentsEnabled() const { return InputSettings.bLandscapeSplinesExportSplineMeshComponents; } + + // Returns true if the landscape splines export menu is expanded. + bool IsLandscapeSplinesExportOptionsMenuExpanded() const { return bLandscapeSplinesExportOptionsMenuExpanded; } + + // Setter for if the landscape splines export menu is expanded. + void SetLandscapeSplinesExportOptionsMenuExpanded(const bool bInLandscapeSplinesExportOptionsMenuExpanded) { bLandscapeSplinesExportOptionsMenuExpanded = bInLandscapeSplinesExportOptionsMenuExpanded; } + + // Setter for if spline mesh components should be merged into one SM when exported + void SetMergeSplineMeshComponents(const bool bInMergeSplineMeshComponents) { InputSettings.bMergeSplineMeshComponents = bInMergeSplineMeshComponents; } + + // Getter for if spline mesh components should be merged into one SM when exported + bool IsMergeSplineMeshComponentsEnabled() const { return InputSettings.bMergeSplineMeshComponents; } + + // Remove all landscape splines of the landscape input objects currently in the world input object array. + // Return true if any objects were removed. + bool RemoveAllLandscapeSplineActorsForInputLandscapes(); + + // Add all landscape splines of the landscape input objects currently in the world input object array as + // world input objects. Will not add splines that are already in the array. + // Returns true if any splines were added. + bool AddAllLandscapeSplineActorsForInputLandscapes(); + + // This array is to record the last insert action, for undo input insertion actions. + UPROPERTY(Transient, DuplicateTransient) + TArray> LastInsertedInputs; + + // This array is to cache the action of last undo delete action, and redo that action. + UPROPERTY(Transient, DuplicateTransient, NonTransactional) + TArray> LastUndoDeletedInputs; + + // Is set to true when layer visibility is controlled by the plugin. + UPROPERTY() + bool bLandscapeControlVisiblity = false; + + UPROPERTY() + bool bCanDeleteHoudiniNodes = true; + +protected: + + // If true, then the landscape spline export options menu is expanded + UPROPERTY() + bool bLandscapeSplinesExportOptionsMenuExpanded = false; + + // Various input settings, such as bExportLODs, bExportSockets etc. + UPROPERTY() + FHoudiniInputObjectSettings InputSettings; + +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniInputObject.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniInputTypes.h" +#include "HoudiniInputObject.h" + +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniAssetActor.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniSplineComponent.h" +#include "HoudiniInput.h" +#include "HoudiniLandscapeRuntimeUtils.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputManager.h" +#include "UnrealObjectInputRuntimeUtils.h" + +#include "Engine/StaticMesh.h" +#include "Engine/SkeletalMesh.h" +#include "Animation/AnimSequence.h" +#include "Engine/DataTable.h" +#include "Components/StaticMeshComponent.h" +#include "Components/InstancedStaticMeshComponent.h" +#include "Components/SplineComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "Landscape.h" +#include "Engine/Brush.h" +#include "Engine/Engine.h" +#include "GameFramework/Volume.h" +#include "Camera/CameraComponent.h" +#include "FoliageType_InstancedStaticMesh.h" +#include "Engine/SimpleConstructionScript.h" +#include "Engine/SCS_Node.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "Engine/SkinnedAssetCommon.h" +#endif + +#include "LandscapeSplineActor.h" +#include "LandscapeSplineControlPoint.h" +#include "LandscapeSplinesComponent.h" +#include "Model.h" +#include "Engine/Brush.h" + +#include "Kismet/KismetSystemLibrary.h" +#if defined(HOUDINI_USE_PCG) +#include "PCGData.h" +#endif +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + #include "GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollection/GeometryCollectionComponent.h" + #include "GeometryCollection/GeometryCollectionObject.h" +#else + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionActor.h" + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionComponent.h" + #include "GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionObject.h" +#endif +#include "LevelInstance/LevelInstanceActor.h" +#include "PackedLevelActor/PackedLevelActor.h" +#include "EngineUtils.h" +#if defined(HOUDINI_USE_PCG) +#include "HoudiniPCGDataObject.h" +#endif +//----------------------------------------------------------------------------------------------------------------------------- +// Constructors +//----------------------------------------------------------------------------------------------------------------------------- + +// +UHoudiniInputObject::UHoudiniInputObject(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , Type(EHoudiniInputObjectType::Invalid) + , bHasChanged(false) + , bNeedsToTriggerUpdate(false) + , bTransformChanged(false) + , MaterialReferences() + , bCanDeleteHoudiniNodes(true) + , bInputNodeHandleOverridesNodeIds(true) + , Transform(FTransform::Identity) + , InputNodeId(-1) + , InputObjectNodeId(-1) +{ + Guid = FGuid::NewGuid(); +} + +// +UHoudiniInputStaticMesh::UHoudiniInputStaticMesh(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +// +UHoudiniInputSkeletalMesh::UHoudiniInputSkeletalMesh(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +UHoudiniInputAnimation::UHoudiniInputAnimation(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +// +UHoudiniInputSkeletalMeshComponent::UHoudiniInputSkeletalMeshComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +// +UHoudiniInputGeometryCollection::UHoudiniInputGeometryCollection(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +// +UHoudiniInputGeometryCollectionComponent::UHoudiniInputGeometryCollectionComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +FHoudiniLandscapeSplineControlPointData::FHoudiniLandscapeSplineControlPointData() + : Location(FVector::ZeroVector) + , Rotation(FRotator::ZeroRotator) + , Width(1000.0f) +#if WITH_EDITORONLY_DATA + , SegmentMeshOffset(0) + , LayerName(NAME_None) + , bRaiseTerrain(true) + , bLowerTerrain(true) + , Mesh(nullptr) + , MeshScale(FVector::OneVector) +#endif +{ + +} + +FHoudiniLandscapeSplineSegmentData::FHoudiniLandscapeSplineSegmentData() +#if WITH_EDITORONLY_DATA + : LayerName(NAME_None) + , bRaiseTerrain(true) + , bLowerTerrain(true) +#endif +{ + +} + +// +UHoudiniInputLandscapeSplineActor::UHoudiniInputLandscapeSplineActor(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +// +UHoudiniInputLandscapeSplinesComponent::UHoudiniInputLandscapeSplinesComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , NextControlPointId(0) +{ + +} + +// +UHoudiniInputSceneComponent::UHoudiniInputSceneComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +// +UHoudiniInputMeshComponent::UHoudiniInputMeshComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +// +UHoudiniInputInstancedMeshComponent::UHoudiniInputInstancedMeshComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +// +UHoudiniInputSplineComponent::UHoudiniInputSplineComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , NumberOfSplineControlPoints(-1) + , SplineLength(-1.0f) + , SplineResolution(-1.0f) + , SplineClosed(false) +{ + +} + +// +UHoudiniInputCameraComponent::UHoudiniInputCameraComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , FOV(0.0f) + , AspectRatio(1.0f) + , bIsOrthographic(false) + , OrthoWidth(2.0f) + , OrthoNearClipPlane(0.0f) + , OrthoFarClipPlane(-1.0f) +{ + +} + +// Return true if the component itself has been modified +bool +UHoudiniInputSplineComponent::HasComponentChanged(const FHoudiniInputObjectSettings& InSettings) const +{ + USplineComponent* SplineComponent = Cast(InputObject.LoadSynchronous()); + + if (!SplineComponent) + return false; + + if (SplineClosed != SplineComponent->IsClosedLoop()) + return true; + + if (SplineComponent->GetNumberOfSplinePoints() != NumberOfSplineControlPoints) + return true; + + if (SplineComponent->GetSplineLength() != SplineLength) + return true; + + for (int32 n = 0; n < SplineComponent->GetNumberOfSplinePoints(); ++n) + { + const FTransform &CurSplineComponentTransform = SplineComponent->GetTransformAtSplinePoint(n, ESplineCoordinateSpace::Local, true); + const FTransform &CurInputTransform = SplineControlPoints[n]; + + if (!CurInputTransform.TranslationEquals(CurSplineComponentTransform)) + return true; + + if (!CurInputTransform.RotationEquals(CurSplineComponentTransform)) + return true; + + if (!CurInputTransform.Scale3DEquals(CurSplineComponentTransform)) + return true; + } + + return false; +} + + +bool +UHoudiniInputLandscapeSplineActor::ShouldTrackComponent(UActorComponent const* InComponent, const FHoudiniInputObjectSettings* InSettings) const +{ + // We only track ULandscapeSplinesComponents for landscape splines at this stage. + if (!IsValid(InComponent)) + return false; + + // Use InSettings if provided, otherwise use the settings cached at the last update + const FHoudiniInputObjectSettings& Settings = InSettings ? *InSettings : CachedInputSettings; + + if (InComponent->IsA(ULandscapeSplinesComponent::StaticClass())) + return true; + + if (Settings.bLandscapeSplinesExportSplineMeshComponents && InComponent->IsA(USplineMeshComponent::StaticClass())) + return true; + + return false; +} + + +// Return true if the component itself has been modified +bool +UHoudiniInputLandscapeSplinesComponent::HasComponentChanged(const FHoudiniInputObjectSettings& InSettings) const +{ + if (Super::HasComponentChanged(InSettings)) + { + return true; + } + + ULandscapeSplinesComponent* const SplinesComponent = Cast(InputObject.LoadSynchronous()); + + if (!IsValid(SplinesComponent)) + return false; + + // Get the control point and segment arrays and check if the counts have changed. + // Use helper to fetch control points since the landscape splines API differs between UE 5.0 and 5.1+ + TArray> ControlPoints; + TArray> Segments; + FHoudiniEngineRuntimeUtils::GetLandscapeSplinesControlPointsAndSegments(SplinesComponent, &ControlPoints, &Segments); + + const int32 NumControlPoints = ControlPoints.Num(); + if (NumControlPoints != CachedControlPoints.Num()) + { + return true; + } + const int32 NumSegments = Segments.Num(); + if (NumSegments != CachedSegments.Num()) + { + return true; + } + + // If the number of control points / segments remain the same, check each point / segment for differences + if (NumControlPoints > 0) + { + for (int32 Idx = 0; Idx < NumControlPoints; ++Idx) + { + ULandscapeSplineControlPoint const* const ControlPoint = ControlPoints[Idx]; + const FHoudiniLandscapeSplineControlPointData& CachedControlPoint = CachedControlPoints[Idx]; + + // Skip invalid points + if (!IsValid(ControlPoint)) + { + continue; + } + + // Compare supported fields ... + // TODO: could we simplify this by some kind of UProperty based diff, like UEdGraphNode does? + if (ControlPoint->Location != CachedControlPoint.Location + || ControlPoint->Rotation != CachedControlPoint.Rotation +#if WITH_EDITORONLY_DATA + || ControlPoint->SegmentMeshOffset != CachedControlPoint.SegmentMeshOffset + || ControlPoint->LayerName != CachedControlPoint.LayerName + || ControlPoint->bRaiseTerrain != CachedControlPoint.bRaiseTerrain + || ControlPoint->bLowerTerrain != CachedControlPoint.bLowerTerrain + || ControlPoint->Mesh != CachedControlPoint.Mesh + || ControlPoint->MaterialOverrides != CachedControlPoint.MaterialOverrides + || ControlPoint->MeshScale != CachedControlPoint.MeshScale +#endif + || ControlPoint->Width != CachedControlPoint.Width) + { + return true; + } + } + } + + if (NumSegments > 0) + { + for (int32 Idx = 0; Idx < NumSegments; ++Idx) + { + ULandscapeSplineSegment const* const Segment = Segments[Idx]; + const FHoudiniLandscapeSplineSegmentData& CachedSegment = CachedSegments[Idx]; + + // Skip invalid segments + if (!IsValid(Segment)) + { + continue; + } + + // Compare supported fields ... + // TODO: could we simplify this by some kind of UProperty based diff, like UEdGraphNode does? +#if WITH_EDITORONLY_DATA + if (Segment->LayerName != CachedSegment.LayerName + || Segment->bRaiseTerrain != CachedSegment.bRaiseTerrain + || Segment->bLowerTerrain != CachedSegment.bLowerTerrain) + { + return true; + } + + const int32 NumSplineMeshes = Segment->SplineMeshes.Num(); + if (NumSplineMeshes != CachedSegment.SplineMeshes.Num()) + { + return true; + } + + // The FLandscapeSplineMeshEntry struct is not comparable with == operator + for (int32 SplineMeshIdx = 0; SplineMeshIdx < NumSplineMeshes; ++SplineMeshIdx) + { + const FLandscapeSplineMeshEntry& SplineMeshEntry = Segment->SplineMeshes[SplineMeshIdx]; + const FLandscapeSplineMeshEntry& CachedSplineMeshEntry = CachedSegment.SplineMeshes[SplineMeshIdx]; + + if (SplineMeshEntry.Mesh != CachedSplineMeshEntry.Mesh + || SplineMeshEntry.MaterialOverrides != CachedSplineMeshEntry.MaterialOverrides + || SplineMeshEntry.bCenterH != CachedSplineMeshEntry.bCenterH + || SplineMeshEntry.CenterAdjust != CachedSplineMeshEntry.CenterAdjust + || SplineMeshEntry.bScaleToWidth != CachedSplineMeshEntry.bScaleToWidth + || SplineMeshEntry.Scale != CachedSplineMeshEntry.Scale + || SplineMeshEntry.ForwardAxis != CachedSplineMeshEntry.ForwardAxis + || SplineMeshEntry.UpAxis != CachedSplineMeshEntry.UpAxis) + { + return true; + } + } +#endif + } + } + + return false; +} + +// +UHoudiniInputHoudiniSplineComponent::UHoudiniInputHoudiniSplineComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , CurveType(EHoudiniCurveType::Polygon) + , CurveMethod(EHoudiniCurveMethod::CVs) + , Reversed(false) +{ + bInputNodeHandleOverridesNodeIds = false; +} + +// +UHoudiniInputHoudiniAsset::UHoudiniInputHoudiniAsset(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , OutputIndex(-1) + , NodeId(-1) +{ + bInputNodeHandleOverridesNodeIds = false; +} + +// +UHoudiniInputActor::UHoudiniInputActor(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , SplinesMeshObjectNodeId(-1) + , SplinesMeshNodeId(-1) + , LastUpdateNumComponentsAdded(0) + , LastUpdateNumComponentsRemoved(0) +{ + +} + +// +UHoudiniInputLevelInstance::UHoudiniInputLevelInstance(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +// +UHoudiniInputLandscape::UHoudiniInputLandscape(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , CachedNumLandscapeComponents(0) +{ + +} + +// +UHoudiniInputBrush::UHoudiniInputBrush() + : CombinedModel(nullptr) + , bIgnoreInputObject(false) +{ + +} + +// +UHoudiniInputBlueprint::UHoudiniInputBlueprint(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , LastUpdateNumComponentsAdded(0) + , LastUpdateNumComponentsRemoved(0) +{ + +} + +// +UHoudiniInputPackedLevelActor::UHoudiniInputPackedLevelActor(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , BlueprintInputObject(nullptr) +{ + +} + +//----------------------------------------------------------------------------------------------------------------------------- +// Accessors +//----------------------------------------------------------------------------------------------------------------------------- + +UObject* +UHoudiniInputObject::GetObject() const +{ + return InputObject.LoadSynchronous(); +} + +void +UHoudiniInputObject::SetInputNodeId(const int32 InInputNodeId) +{ + // Only set the node id if the ref counted system is not used. + if (bInputNodeHandleOverridesNodeIds) + return; + InputNodeId = InInputNodeId; +} + +int32 +UHoudiniInputObject::GetInputNodeId() const +{ + // If the ref counted system is enabled then we return the node id via the handle, otherwise return the id stored + // on this object + + if (!bInputNodeHandleOverridesNodeIds) + return InputNodeId; + + if (!InputNodeHandle.IsValid()) + return -1; + + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return -1; + + FUnrealObjectInputNode const* Node = nullptr; + if (!Manager->GetNode(InputNodeHandle, Node)) + return -1; + if (!Node) + return -1; + + return Node->GetHAPINodeId(); +} + +void +UHoudiniInputObject::SetInputObjectNodeId(const int32 InInputObjectNodeId) +{ + // Only set the node id if the ref counted system is not used. + if (bInputNodeHandleOverridesNodeIds) + return; + InputObjectNodeId = InInputObjectNodeId; +} + +int32 +UHoudiniInputObject::GetInputObjectNodeId() const +{ + // If the ref counted system is enabled then we return the node id via the handle, otherwise return the id stored + // on this object + + if (!bInputNodeHandleOverridesNodeIds) + return InputObjectNodeId; + + if (!InputNodeHandle.IsValid()) + return -1; + + const EUnrealObjectInputNodeType NodeType = InputNodeHandle.GetIdentifier().GetNodeType(); + if (NodeType != EUnrealObjectInputNodeType::Leaf && NodeType != EUnrealObjectInputNodeType::Reference) + return -1; + + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return -1; + + FUnrealObjectInputNode const* Node = nullptr; + if (!Manager->GetNode(InputNodeHandle, Node)) + return -1; + if (!Node) + return -1; + + FUnrealObjectInputLeafNode const* const LeafNode = static_cast(Node); + if (!LeafNode) + return -1; + + return LeafNode->GetObjectHAPINodeId(); +} + +const TArray& +UHoudiniInputObject::GetMaterialReferences() +{ + UpdateMaterialReferences(); + return MaterialReferences; +} + +UStaticMesh* +UHoudiniInputStaticMesh::GetStaticMesh() const +{ + return Cast(InputObject.LoadSynchronous()); +} + +USkeletalMesh* +UHoudiniInputSkeletalMesh::GetSkeletalMesh() +{ + return Cast(InputObject.LoadSynchronous()); +} + +UAnimSequence* +UHoudiniInputAnimation::GetAnimation() +{ + return Cast(InputObject.LoadSynchronous()); +} + +USkeletalMeshComponent* +UHoudiniInputSkeletalMeshComponent::GetSkeletalMeshComponent() +{ + return Cast(InputObject.LoadSynchronous()); +} + +USkeletalMesh* +UHoudiniInputSkeletalMeshComponent::GetSkeletalMesh() +{ + USkeletalMeshComponent* SkeletalMeshComponent = GetSkeletalMeshComponent(); + if (!IsValid(SkeletalMeshComponent)) + return nullptr; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + return SkeletalMeshComponent->GetSkeletalMeshAsset(); +#else + return SkeletalMeshComponent->SkeletalMesh.Get(); +#endif +} + +UGeometryCollection* +UHoudiniInputGeometryCollection::GetGeometryCollection() +{ + return Cast(InputObject.LoadSynchronous()); +} + +UGeometryCollectionComponent* +UHoudiniInputGeometryCollectionComponent::GetGeometryCollectionComponent() +{ + return Cast(InputObject.LoadSynchronous()); +} + +UGeometryCollection* +UHoudiniInputGeometryCollectionComponent::GetGeometryCollection() +{ + UGeometryCollectionComponent * GeometryCollectionComponent = GetGeometryCollectionComponent(); + if (!IsValid(GeometryCollectionComponent)) + { + return nullptr; + } + + FGeometryCollectionEdit GeometryCollectionEdit = GeometryCollectionComponent->EditRestCollection(); + return GeometryCollectionEdit.GetRestCollection(); +} + +ALandscapeSplineActor* +UHoudiniInputLandscapeSplineActor::GetLandscapeSplineActor() const +{ + return Cast(InputObject.LoadSynchronous()); +} + +ULandscapeSplinesComponent* +UHoudiniInputLandscapeSplinesComponent::GetLandscapeSplinesComponent() const +{ + return Cast(InputObject.LoadSynchronous()); +} + +USceneComponent* +UHoudiniInputSceneComponent::GetSceneComponent() +{ + return Cast(InputObject.LoadSynchronous()); +} + +UStaticMeshComponent* +UHoudiniInputMeshComponent::GetStaticMeshComponent() +{ + return Cast(InputObject.LoadSynchronous()); +} + +UStaticMesh* +UHoudiniInputMeshComponent::GetStaticMesh() +{ + return StaticMesh.Get(); +} + +UInstancedStaticMeshComponent* +UHoudiniInputInstancedMeshComponent::GetInstancedStaticMeshComponent() +{ + return Cast(InputObject.LoadSynchronous()); +} + +USplineComponent* +UHoudiniInputSplineComponent::GetSplineComponent() +{ + return Cast(InputObject.LoadSynchronous()); +} + +UHoudiniSplineComponent* +UHoudiniInputHoudiniSplineComponent::GetCurveComponent() const +{ + return Cast(GetObject()); + //return Cast(InputObject.LoadSynchronous()); +} + +UCameraComponent* +UHoudiniInputCameraComponent::GetCameraComponent() +{ + return Cast(InputObject.LoadSynchronous()); +} + +UHoudiniAssetComponent* +UHoudiniInputHoudiniAsset::GetHoudiniAssetComponent() +{ + UObject* InObj = InputObject.LoadSynchronous(); + UHoudiniCookable* HC = Cast(InObj); + if (IsValid(HC)) + { + return Cast(HC->GetComponent()); + } + + return Cast(InObj); +} + +UHoudiniCookable* +UHoudiniInputHoudiniAsset::GetHoudiniCookable() +{ + UObject* InObj = InputObject.LoadSynchronous(); + UHoudiniCookable* HC = Cast(InObj); + if (IsValid(HC)) + return HC; + + UHoudiniAssetComponent* HAC = Cast(InObj); + if (!IsValid(HAC)) + return nullptr; + + return HAC->GetCookable(); +} + +AActor* +UHoudiniInputActor::GetActor() const +{ + AActor * Actor = Cast(InputObject.LoadSynchronous()); + if (!Actor) + return Actor; + + // Ensure transforms are up to date. It will not be if using sub levels. + USceneComponent* RootComponent = Actor->GetRootComponent(); + if (IsValid(RootComponent)) + RootComponent->ConditionalUpdateComponentToWorld(); + + return Actor; +} + +ALevelInstance* +UHoudiniInputLevelInstance::GetLevelInstance() const +{ + return Cast(InputObject.LoadSynchronous()); +} + +void +UHoudiniInputLevelInstance::InvalidateData() +{ + // Invalidate data on the tracked actor objects + for (const auto& Entry : TrackedActorObjects) + { + UHoudiniInputObject* const TrackedObject = Entry.Value; + if (!IsValid(TrackedObject)) + continue; + TrackedObject->InvalidateData(); + } + + Super::InvalidateData(); +} + +ALandscapeProxy* +UHoudiniInputLandscape::GetLandscapeProxy() const +{ + return Cast(InputObject.LoadSynchronous()); +} + +void +UHoudiniInputLandscape::SetLandscapeProxy(UObject* InLandscapeProxy) +{ + UObject* LandscapeProxy = Cast(InLandscapeProxy); + if (LandscapeProxy) + InputObject = LandscapeProxy; +} + +int32 UHoudiniInputLandscape::CountLandscapeComponents() const +{ + ALandscapeProxy* LandscapeProxy = GetLandscapeProxy(); + if (!IsValid(LandscapeProxy)) + { + return false; + } + + ULandscapeInfo* LandscapeInfo = LandscapeProxy->GetLandscapeInfo(); + if (!IsValid(LandscapeInfo)) + { + return false; + } + + int32 NumComponents = 0; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + LandscapeInfo->ForEachLandscapeProxy([&NumComponents](ALandscapeProxy* Proxy) +#else + LandscapeInfo->ForAllLandscapeProxies([&NumComponents](ALandscapeProxy* Proxy) +#endif + { + if (IsValid(Proxy)) + { + NumComponents += Proxy->LandscapeComponents.Num(); + } +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + return true; +#endif + }); + return NumComponents; +} + + +ABrush* +UHoudiniInputBrush::GetBrush() const +{ + return Cast(InputObject.LoadSynchronous()); +} + + +UBlueprint* +UHoudiniInputBlueprint::GetBlueprint() const +{ + return Cast(InputObject.LoadSynchronous()); +} + +//----------------------------------------------------------------------------------------------------------------------------- +// MARK CHANGED METHODS +//----------------------------------------------------------------------------------------------------------------------------- + +void +UHoudiniInputObject::MarkChanged(const bool& bInChanged) +{ + bHasChanged = bInChanged; + SetNeedsToTriggerUpdate(bInChanged); + + if (bInChanged && InputNodeHandle.IsValid()) + { + static constexpr bool bAlsoDirtyReferencedNodes = true; + FUnrealObjectInputRuntimeUtils::MarkInputNodeAsDirty(InputNodeHandle.GetIdentifier(), bAlsoDirtyReferencedNodes); + } +} + +void +UHoudiniInputActor::MarkChanged(const bool& bInChanged) +{ + bHasChanged = bInChanged; + SetNeedsToTriggerUpdate(bInChanged); + + static constexpr bool bAlsoDirtyReferencedNodes = true; + if (bInChanged && InputNodeHandle.IsValid()) + FUnrealObjectInputRuntimeUtils::MarkInputNodeAsDirty(InputNodeHandle.GetIdentifier(), bAlsoDirtyReferencedNodes); + + if (bInChanged && SplinesMeshInputNodeHandle.IsValid()) + FUnrealObjectInputRuntimeUtils::MarkInputNodeAsDirty(SplinesMeshInputNodeHandle.GetIdentifier(), bAlsoDirtyReferencedNodes); + + for (auto& CurComponent : ActorComponents) + { + CurComponent->MarkChanged(bInChanged); + } +} + +void +UHoudiniInputHoudiniSplineComponent::MarkChanged(const bool& bInChanged) +{ + Super::MarkChanged(bInChanged); + + UHoudiniSplineComponent* HoudiniSplineComponent = GetCurveComponent(); + if (HoudiniSplineComponent) + { + HoudiniSplineComponent->MarkChanged(bInChanged); + } +} + + +//----------------------------------------------------------------------------------------------------------------------------- +// CREATE METHODS +//----------------------------------------------------------------------------------------------------------------------------- + +UHoudiniInputObject * +UHoudiniInputObject::CreateTypedInputObject(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor) +{ + if (!InObject) + return nullptr; + + UHoudiniInputObject* HoudiniInputObject = nullptr; + + EHoudiniInputObjectType InputObjectType = GetInputObjectTypeFromObject(InObject); + switch (InputObjectType) + { + case EHoudiniInputObjectType::Object: + HoudiniInputObject = UHoudiniInputObject::Create(InObject, InOuter, InName, InInputSettings); + break; + + case EHoudiniInputObjectType::StaticMesh: + HoudiniInputObject = UHoudiniInputStaticMesh::Create(InObject, InOuter, InName, InInputSettings); + break; + + case EHoudiniInputObjectType::SkeletalMesh: + HoudiniInputObject = UHoudiniInputSkeletalMesh::Create(InObject, InOuter, InName, InInputSettings); + break; + case EHoudiniInputObjectType::Animation: + HoudiniInputObject = UHoudiniInputAnimation::Create(InObject, InOuter, InName, InInputSettings); + break; + case EHoudiniInputObjectType::SceneComponent: + // Do not create input objects for unknown scene component! + //HoudiniInputObject = UHoudiniInputSceneComponent::Create(InObject, InOuter, InName, InInputSettings, InParentInputActor); + break; + + case EHoudiniInputObjectType::StaticMeshComponent: + HoudiniInputObject = UHoudiniInputMeshComponent::Create(InObject, InOuter, InName, InInputSettings, InParentInputActor); + break; + + case EHoudiniInputObjectType::InstancedStaticMeshComponent: + HoudiniInputObject = UHoudiniInputInstancedMeshComponent::Create(InObject, InOuter, InName, InInputSettings, InParentInputActor); + break; + case EHoudiniInputObjectType::SplineComponent: + HoudiniInputObject = UHoudiniInputSplineComponent::Create(InObject, InOuter, InName, InInputSettings, InParentInputActor); + break; + + case EHoudiniInputObjectType::HoudiniSplineComponent: + HoudiniInputObject = UHoudiniInputHoudiniSplineComponent::Create(InObject, InOuter, InName, InInputSettings); + break; + + case EHoudiniInputObjectType::HoudiniAssetActor: + { + AHoudiniAssetActor* HoudiniActor = Cast(InObject); + if (HoudiniActor) + { + HoudiniInputObject = UHoudiniInputHoudiniAsset::Create(HoudiniActor->GetHoudiniCookable(), InOuter, InName, InInputSettings); + } + else + { + HoudiniInputObject = nullptr; + } + } + break; + + case EHoudiniInputObjectType::HoudiniAssetComponent: + case EHoudiniInputObjectType::HoudiniCookable: + { + UHoudiniCookable* OuterHC = Cast(InObject->GetOuter()); + if (IsValid(OuterHC)) + { + HoudiniInputObject = UHoudiniInputHoudiniAsset::Create(InObject, InOuter, InName, InInputSettings); + } + else + { + HoudiniInputObject = nullptr; + } + } + break; + + case EHoudiniInputObjectType::Actor: + case EHoudiniInputObjectType::GeometryCollectionActor_Deprecated: + HoudiniInputObject = UHoudiniInputActor::Create(InObject, InOuter, InName, InInputSettings); + break; + + case EHoudiniInputObjectType::Landscape: + HoudiniInputObject = UHoudiniInputLandscape::Create(InObject, InOuter, InName, InInputSettings); + break; + + case EHoudiniInputObjectType::Brush: + HoudiniInputObject = UHoudiniInputBrush::Create(InObject, InOuter, InName, InInputSettings); + break; + + case EHoudiniInputObjectType::CameraComponent: + HoudiniInputObject = UHoudiniInputCameraComponent::Create(InObject, InOuter, InName, InInputSettings, InParentInputActor); + break; + + case EHoudiniInputObjectType::DataTable: + HoudiniInputObject = UHoudiniInputDataTable::Create(InObject, InOuter, InName, InInputSettings); + break; + + case EHoudiniInputObjectType::PCGData: + HoudiniInputObject = UHoudiniInputPCGData::Create(InObject, InOuter, InName, InInputSettings); + break; + + case EHoudiniInputObjectType::FoliageType_InstancedStaticMesh: + HoudiniInputObject = UHoudiniInputFoliageType_InstancedStaticMesh::Create(InObject, InOuter, InName, InInputSettings); + break; + case EHoudiniInputObjectType::GeometryCollection: + HoudiniInputObject = UHoudiniInputGeometryCollection::Create(InObject, InOuter, InName, InInputSettings); + break; + case EHoudiniInputObjectType::GeometryCollectionComponent: + HoudiniInputObject = UHoudiniInputGeometryCollectionComponent::Create(InObject, InOuter, InName, InInputSettings, InParentInputActor); + break; + + case EHoudiniInputObjectType::SkeletalMeshComponent: + HoudiniInputObject = UHoudiniInputSkeletalMeshComponent::Create(InObject, InOuter, InName, InInputSettings, InParentInputActor); + break; + + case EHoudiniInputObjectType::Blueprint: + HoudiniInputObject = UHoudiniInputBlueprint::Create(InObject, InOuter, InName, InInputSettings); + break; + + case EHoudiniInputObjectType::LandscapeSplineActor: + HoudiniInputObject = UHoudiniInputLandscapeSplineActor::Create(InObject, InOuter, InName, InInputSettings); + break; + + case EHoudiniInputObjectType::LandscapeSplinesComponent: + HoudiniInputObject = UHoudiniInputLandscapeSplinesComponent::Create(InObject, InOuter, InName, InInputSettings, InParentInputActor); + break; + + case EHoudiniInputObjectType::SplineMeshComponent: + HoudiniInputObject = UHoudiniInputSplineMeshComponent::Create(InObject, InOuter, InName, InInputSettings, InParentInputActor); + break; + + case EHoudiniInputObjectType::LevelInstance: + HoudiniInputObject = UHoudiniInputLevelInstance::Create(InObject, InOuter, InName, InInputSettings); + break; + + case EHoudiniInputObjectType::PackedLevelActor: + HoudiniInputObject = UHoudiniInputPackedLevelActor::Create(InObject, InOuter, InName, InInputSettings); + break; + + case EHoudiniInputObjectType::Texture: + HoudiniInputObject = UHoudiniInputTexture::Create(InObject, InOuter, InName, InInputSettings); + break; + + case EHoudiniInputObjectType::Invalid: + default: + break; + } + + return HoudiniInputObject; +} + + +UHoudiniInputObject * +UHoudiniInputInstancedMeshComponent::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor) +{ + FString InputObjectNameStr = "HoudiniInputObject_ISMC_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputInstancedMeshComponent::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputInstancedMeshComponent * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputInstancedMeshComponent::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->SetParentInputActor(InParentInputActor); + HoudiniInputObject->Type = EHoudiniInputObjectType::InstancedStaticMeshComponent; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject * +UHoudiniInputMeshComponent::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor) +{ + FString InputObjectNameStr = "HoudiniInputObject_SMC_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputMeshComponent::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputMeshComponent * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputMeshComponent::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->SetParentInputActor(InParentInputActor); + HoudiniInputObject->Type = EHoudiniInputObjectType::StaticMeshComponent; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject * +UHoudiniInputSplineComponent::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor) +{ + FString InputObjectNameStr = "HoudiniInputObject_Spline_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputSplineComponent::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputSplineComponent * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputSplineComponent::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->SetParentInputActor(InParentInputActor); + HoudiniInputObject->Type = EHoudiniInputObjectType::SplineComponent; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject * +UHoudiniInputHoudiniSplineComponent::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_HoudiniSpline_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputHoudiniSplineComponent::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputHoudiniSplineComponent * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputHoudiniSplineComponent::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::HoudiniSplineComponent; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject * +UHoudiniInputCameraComponent::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor) +{ + FString InputObjectNameStr = "HoudiniInputObject_Camera_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputCameraComponent::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputCameraComponent * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputCameraComponent::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->SetParentInputActor(InParentInputActor); + HoudiniInputObject->Type = EHoudiniInputObjectType::CameraComponent; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject * +UHoudiniInputHoudiniAsset::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + UHoudiniCookable* InHC = Cast(InObject); + if (!IsValid(InHC)) + { + UHoudiniAssetComponent* InHoudiniAssetComponent = Cast(InObject); + if (!InHoudiniAssetComponent) + return nullptr; + + InHC = Cast(InHoudiniAssetComponent->GetOuter()); + if (!IsValid(InHC)) + return nullptr; + } + + FString InputObjectNameStr = "HoudiniInputObject_HC_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputHoudiniAsset::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputHoudiniAsset * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputHoudiniAsset::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::HoudiniCookable; // TODO: EHoudiniInputObjectType::HoudiniAssetComponent + + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject * +UHoudiniInputSceneComponent::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor) +{ + FString InputObjectNameStr = "HoudiniInputObject_SceneComp_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputSceneComponent::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputSceneComponent * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputSceneComponent::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::SceneComponent; + HoudiniInputObject->ParentInputActor = InParentInputActor; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject * +UHoudiniInputLandscape::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_Landscape_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputLandscape::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputLandscape * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputLandscape::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::Landscape; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject* +UHoudiniInputLevelInstance::Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_LevelInstance_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputLevelInstance::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputLevelInstance* HoudiniInputObject = NewObject( + InOuter, UHoudiniInputLevelInstance::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::LevelInstance; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + return HoudiniInputObject; +} + + +UHoudiniInputObject * +UHoudiniInputPackedLevelActor::Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + const FString InputObjectNameStr = "HoudiniInputObject_PackedLevelActor_" + InName; + const FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputPackedLevelActor::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputPackedLevelActor * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputPackedLevelActor::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::PackedLevelActor; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + + +UHoudiniInputBrush * +UHoudiniInputBrush::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_Brush_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputBrush::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputBrush * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputBrush::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::Brush; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject * +UHoudiniInputActor::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_Actor_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputActor::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputActor * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputActor::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::Actor; + HoudiniInputObject->GeneratedSplinesMeshPackageGuid = FGuid::NewGuid(); + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject* +UHoudiniInputBlueprint::Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_BP_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputBlueprint::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputBlueprint* HoudiniInputObject = NewObject( + InOuter, UHoudiniInputBlueprint::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::Blueprint; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject * +UHoudiniInputStaticMesh::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_SM_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputStaticMesh::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputStaticMesh * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputStaticMesh::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::StaticMesh; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + + +UHoudiniInputObject * +UHoudiniInputSkeletalMesh::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_SkelMesh_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputSkeletalMesh::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputSkeletalMesh * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputSkeletalMesh::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::SkeletalMesh; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject* +UHoudiniInputAnimation::Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_Animation_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputAnimation::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputAnimation* HoudiniInputObject = NewObject( + InOuter, UHoudiniInputAnimation::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::Animation; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + + +UHoudiniInputObject* +UHoudiniInputSkeletalMeshComponent::Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor) +{ + FString InputObjectNameStr = "HoudiniInputObject_SKC_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputSkeletalMeshComponent::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputSkeletalMeshComponent* HoudiniInputObject = NewObject( + InOuter, UHoudiniInputSkeletalMeshComponent::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->SetParentInputActor(InParentInputActor); + HoudiniInputObject->Type = EHoudiniInputObjectType::SkeletalMeshComponent; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject * +UHoudiniInputGeometryCollection::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_GC_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputGeometryCollection::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputGeometryCollection * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputGeometryCollection::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::GeometryCollection; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UHoudiniInputObject * +UHoudiniInputGeometryCollectionComponent::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor) +{ + FString InputObjectNameStr = "HoudiniInputObject_GCC_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputGeometryCollectionComponent::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputGeometryCollectionComponent * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputGeometryCollectionComponent::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->SetParentInputActor(InParentInputActor); + HoudiniInputObject->Type = EHoudiniInputObjectType::GeometryCollectionComponent; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + + +UHoudiniInputObject* +UHoudiniInputLandscapeSplineActor::Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + const FString InputObjectNameStr = "HoudiniInputObject_LandscapeSplines_" + InName; + const FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputLandscapeSplineActor::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputLandscapeSplineActor* HoudiniInputObject = NewObject( + InOuter, UHoudiniInputLandscapeSplineActor::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::LandscapeSplineActor; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + + +UHoudiniInputObject* +UHoudiniInputLandscapeSplinesComponent::Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor) +{ + const FString InputObjectNameStr = "HoudiniInputObject_LandscapeSplines_" + InName; + const FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputLandscapeSplinesComponent::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputLandscapeSplinesComponent* HoudiniInputObject = NewObject( + InOuter, UHoudiniInputLandscapeSplinesComponent::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->SetParentInputActor(InParentInputActor); + HoudiniInputObject->Type = EHoudiniInputObjectType::LandscapeSplinesComponent; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + // Ensure that NextControlPointId is valid + if (HoudiniInputObject->NextControlPointId < 0) + HoudiniInputObject->NextControlPointId = 0; + + return HoudiniInputObject; +} + + +UHoudiniInputObject * +UHoudiniInputObject::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputObject::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputObject * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputObject::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::Object; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +bool +UHoudiniInputObject::Matches(const UHoudiniInputObject& Other) const +{ + bool Matches = (Type == Other.Type + && InputNodeId == Other.InputNodeId + && InputObjectNodeId == Other.InputObjectNodeId + ); + + Matches &= InputNodeHandle == Other.InputNodeHandle; + + return Matches; +} + +//----------------------------------------------------------------------------------------------------------------------------- +// DELETE METHODS +//----------------------------------------------------------------------------------------------------------------------------- + +void +UHoudiniInputObject::InvalidateData() +{ + // If valid, mark our input nodes for deletion.. + if (this->IsA() || !CanDeleteHoudiniNodes()) + { + // Unless if we're a HoudiniAssetInput! we don't want to delete the other HDA's node! + // just invalidate the node IDs! + InputNodeId = -1; + InputObjectNodeId = -1; + InputNodeHandle.Reset(); + return; + } + + // If we are using the new input system, then don't delete the nodes if we have a valid handle, and the HAPI + // nodes associated with the handle matches InputNodeId / InputObjectNodeId + if (InputNodeHandle.IsValid()) + { + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (Manager) + { + TArray ManagedNodeIds; + if (Manager->GetHAPINodeIds(InputNodeHandle.GetIdentifier(), ManagedNodeIds)) + { + if (ManagedNodeIds.Contains(InputNodeId)) + InputNodeId = -1; + if (ManagedNodeIds.Contains(InputObjectNodeId)) + InputObjectNodeId = -1; + } + } + } + + InputNodeHandle.Reset(); + + if (InputNodeId >= 0) + { + FHoudiniEngineRuntime::Get().MarkNodeIdAsPendingDelete(InputNodeId); + InputNodeId = -1; + } + + // ... and the parent OBJ as well to clean up + if (InputObjectNodeId >= 0) + { + FHoudiniEngineRuntime::Get().MarkNodeIdAsPendingDelete(InputObjectNodeId); + InputObjectNodeId = -1; + } + + +} + +void +UHoudiniInputObject::BeginDestroy() +{ + // Invalidate and mark our input node for deletion + InvalidateData(); + + Super::BeginDestroy(); +} + +//----------------------------------------------------------------------------------------------------------------------------- +// UPDATE METHODS +//----------------------------------------------------------------------------------------------------------------------------- + +void +UHoudiniInputObject::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + InputObject = InObject; + CachedInputSettings = InSettings; +} + +FString UHoudiniInputObject::FormatAssetReference(FString AssetReference) { + // Replace the first space with a single quote + for (int32 Itr = 0; Itr < AssetReference.Len(); Itr++) + { + if (AssetReference[Itr] == ' ') + { + AssetReference[Itr] = '\''; + break; + } + } + + // Attach another single quote to the end + AssetReference += FString("'"); + return AssetReference; +} + +void +UHoudiniInputObject::UpdateMaterialReferences() +{ + UObject* InObject = GetObject(); + if (!InObject) + return; + + MaterialReferences.Empty(); + + EHoudiniInputObjectType InputObjectType = GetInputObjectTypeFromObject(InObject); + switch (InputObjectType) + { + case EHoudiniInputObjectType::StaticMesh: + { + const UStaticMesh* SM = Cast(InObject); + ensure(SM); + + if (!SM || !IsValid(SM)) + return; + + const TArray Materials = SM->GetStaticMaterials(); + for (const FStaticMaterial& Material : Materials) + { + FString AssetReference = FormatAssetReference(Material.MaterialInterface->GetFullName()); + MaterialReferences.Add(AssetReference); + } + break; + } + + case EHoudiniInputObjectType::SkeletalMesh: + { + const USkeletalMesh* SK = Cast(InObject); + ensure(SK); + + if (!SK || !IsValid(SK)) + return; + + const TArray Materials = SK->GetMaterials(); + for (const FSkeletalMaterial& Material : Materials) + { + FString AssetReference = FormatAssetReference(Material.MaterialInterface->GetFullName()); + MaterialReferences.Add(AssetReference); + } + break; + } + + case EHoudiniInputObjectType::StaticMeshComponent: + case EHoudiniInputObjectType::SkeletalMeshComponent: + case EHoudiniInputObjectType::GeometryCollectionComponent: + { + const UMeshComponent* MC = Cast(InObject); + ensure(MC); + + if (!MC || !IsValid(MC)) + return; + + const TArray Materials = MC->GetMaterials(); + for (const UObject* Material : Materials) + { + FString AssetReference = FormatAssetReference(Material->GetFullName()); + MaterialReferences.Add(AssetReference); + } + + break; + } + + case EHoudiniInputObjectType::FoliageType_InstancedStaticMesh: + { + const UFoliageType_InstancedStaticMesh* FT = Cast(InObject); + ensure(FT); + if (!FT || !IsValid(FT)) + return; + + const UStaticMesh* SM = FT->GetStaticMesh(); + + // Use the override materials from the Instancer if available, otherwise use the original materials from the instanced Static Mesh + const TArray OverrideMaterials = FT->OverrideMaterials; + const TArray StaticMaterials = SM->GetStaticMaterials(); + for (const UObject* Material : OverrideMaterials) + { + FString AssetReference = FormatAssetReference(Material->GetFullName()); + MaterialReferences.Add(AssetReference); + } + + if (OverrideMaterials.Num() > 0) + break; + + for (const FStaticMaterial& Material : StaticMaterials) + { + FString AssetReference = FormatAssetReference(Material.MaterialInterface->GetFullName()); + MaterialReferences.Add(AssetReference); + } + + break; + } + + case EHoudiniInputObjectType::GeometryCollection: + { + UGeometryCollection* const GC = Cast(InObject); + ensure(GC); + if (!GC || !IsValid(GC)) + return; + + const TArray Materials = GC->Materials; + + for (const UObject* Material : Materials) + { + FString AssetReference = FormatAssetReference(Material->GetFullName()); + MaterialReferences.Add(AssetReference); + } + break; + } + + default: + break; + } +} + +void +UHoudiniInputStaticMesh::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + // Nothing to do + Super::Update(InObject, InSettings); + // Static Mesh input accepts SM, BP, FoliageType_InstancedStaticMesh (static mesh) and FoliageType_Actor (if blueprint actor). + UStaticMesh* SM = Cast(InObject); + UBlueprint* BP = Cast(InObject); + + ensure(SM || BP); +} + +void +UHoudiniInputSkeletalMesh::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + // Nothing to do + Super::Update(InObject, InSettings); + + USkeletalMesh* SkelMesh = Cast(InObject); + ensure(SkelMesh); +} + +void +UHoudiniInputAnimation::Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) +{ + // Nothing to do + Super::Update(InObject, InSettings); + + UAnimSequence* Animation = Cast(InObject); + ensure(Animation); +} + + +void +UHoudiniInputSkeletalMeshComponent::Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) +{ + // Nothing to do + Super::Update(InObject, InSettings); + + USkeletalMeshComponent* SkeletalMeshComponent = Cast(InObject); + ensure(SkeletalMeshComponent); +} + + +void +UHoudiniInputGeometryCollection::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + // Nothing to do + Super::Update(InObject, InSettings); + + UGeometryCollection* GeometryCollection = Cast(InObject); + ensure(GeometryCollection); +} + + +void +UHoudiniInputGeometryCollectionComponent::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + // Nothing to do + Super::Update(InObject, InSettings); + + UGeometryCollectionComponent* GeometryCollectionComponent = Cast(InObject); + ensure(GeometryCollectionComponent); +} + +void +UHoudiniInputLandscapeSplineActor::Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) +{ + // Get the current value for if exporting spline meshes is enabled + if (!InSettings.bLandscapeSplinesExportSplineMeshComponents || !InSettings.bMergeSplineMeshComponents) + { + // Invalidate the merged splines nodes + InvalidateSplinesMeshData(); + } + + Super::Update(InObject, InSettings); +} + +void +UHoudiniInputLandscapeSplinesComponent::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + Super::Update(InObject, InSettings); + + ULandscapeSplinesComponent* const LandscapeSplinesComponent = Cast(InObject); + ensure(LandscapeSplinesComponent); + + // Cache the control points and segments from landscape splines component, for use in HasComponentChanged() + // Use helper to fetch control points since the landscape splines API differs between UE 5.0 and 5.1+ + TArray> ControlPoints; + TArray> Segments; + FHoudiniEngineRuntimeUtils::GetLandscapeSplinesControlPointsAndSegments(LandscapeSplinesComponent, &ControlPoints, &Segments); + + const int32 NumControlPoints = ControlPoints.Num(); + + TMap, int32> OldControlPointIdMap(ControlPointIdMap); + ControlPointIdMap.Empty(NumControlPoints); + + CachedControlPoints.SetNum(NumControlPoints); + if (NumControlPoints > 0) + { + for (int32 Idx = 0; Idx < NumControlPoints; ++Idx) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + TObjectPtr& ControlPoint = ControlPoints[Idx]; +#else + ULandscapeSplineControlPoint const* const ControlPoint = ControlPoints[Idx]; +#endif + FHoudiniLandscapeSplineControlPointData& CachedControlPoint = CachedControlPoints[Idx]; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + if (!ControlPoint) +#else + if (!IsValid(ControlPoint)) +#endif + { + // Reset entry to default + CachedControlPoint = {}; + continue; + } + + // See if we already have a valid id for this control point and copy it + bool bHasValidControlPointId = false; + TSoftObjectPtr ControlPointPtr(ControlPoint); + if (int32 const* const ControlPointIdPtr = OldControlPointIdMap.Find(ControlPointPtr)) + { + const int32 ControlPointId = *ControlPointIdPtr; + if (ControlPointId >= 0) + { + ControlPointIdMap.Add(ControlPointPtr, ControlPointId); + bHasValidControlPointId = true; + } + } + if (!bHasValidControlPointId) + { + // Generate a new valid id for ControlPoint and update the map + FHoudiniLandscapeRuntimeUtils::GetOrGenerateValidControlPointId(ControlPoint, ControlPointIdMap, NextControlPointId); + } + + CachedControlPoint.Location = ControlPoint->Location; + CachedControlPoint.Rotation = ControlPoint->Rotation; + CachedControlPoint.Width = ControlPoint->Width; +#if WITH_EDITORONLY_DATA + CachedControlPoint.SegmentMeshOffset = ControlPoint->SegmentMeshOffset; + CachedControlPoint.LayerName = ControlPoint->LayerName; + CachedControlPoint.bRaiseTerrain = ControlPoint->bRaiseTerrain; + CachedControlPoint.bLowerTerrain = ControlPoint->bLowerTerrain; + CachedControlPoint.Mesh = ControlPoint->Mesh; + CachedControlPoint.MaterialOverrides = ControlPoint->MaterialOverrides; + CachedControlPoint.MeshScale = ControlPoint->MeshScale; +#endif + } + } + + const int32 NumSegments = Segments.Num(); + CachedSegments.SetNum(NumSegments); + if (NumSegments > 0) + { + for (int32 Idx = 0; Idx < NumSegments; ++Idx) + { + ULandscapeSplineSegment const* const Segment = Segments[Idx]; + FHoudiniLandscapeSplineSegmentData& CachedSegment = CachedSegments[Idx]; + if (!IsValid(Segment)) + { + // Reset entry to default + CachedSegment = {}; + continue; + } + +#if WITH_EDITORONLY_DATA + CachedSegment.LayerName = Segment->LayerName; + CachedSegment.bRaiseTerrain = Segment->bRaiseTerrain; + CachedSegment.bLowerTerrain = Segment->bLowerTerrain; + CachedSegment.SplineMeshes = Segment->SplineMeshes; +#endif + } + } +} + + +void +UHoudiniInputSceneComponent::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + Super::Update(InObject, InSettings); + UpdateTransform(); +} + +void +UHoudiniInputSceneComponent::UpdateTransform() +{ + USceneComponent* const USC = GetSceneComponent(); + ensure(USC); + if (USC && IsValid(USC)) + { + // For components in Blueprints we have to ensure that the ComponentToWorld is calculated + USC->ConditionalUpdateComponentToWorld(); + Transform = USC->GetComponentTransform(); + } + + if (IsValid(ParentInputActor)) + ActorTransform = ParentInputActor->GetHoudiniObjectTransform(); + else + ActorTransform = FTransform::Identity; +} + +bool +UHoudiniInputSceneComponent::HasActorTransformChanged() const +{ + // Returns true if the attached actor's (parent) transform has been modified + if (!IsValid(ParentInputActor)) + return false; + + UHoudiniInputActor const* const InputActor = Cast(ParentInputActor); + if (IsValid(InputActor) && InputActor->HasActorTransformChanged()) + return true; + + return !ActorTransform.Equals(ParentInputActor->GetHoudiniObjectTransform()); +} + + +bool +UHoudiniInputSceneComponent::HasComponentTransformChanged() const +{ + // Returns true if the attached actor's (parent) transform has been modified + USceneComponent* MyComp = Cast(InputObject.LoadSynchronous()); + if (!IsValid(MyComp)) + return false; + + // For components in Blueprints we have to ensure that the ComponentToWorld is calculated + MyComp->ConditionalUpdateComponentToWorld(); + return !Transform.Equals(MyComp->GetComponentTransform()); +} + + +bool +UHoudiniInputSceneComponent::HasComponentChanged(const FHoudiniInputObjectSettings& InSettings) const +{ + // Should return true if the component itself has been modified + // Should be overriden in child classes + return false; +} + +FTransform +UHoudiniInputSceneComponent::GetHoudiniObjectTransform() const +{ + return GetTransformRelativeToOwner(); +} + +bool +UHoudiniInputMeshComponent::HasComponentChanged(const FHoudiniInputObjectSettings& InSettings) const +{ + UStaticMeshComponent* SMC = Cast(InputObject.LoadSynchronous()); + UStaticMesh* SMCSM = nullptr; + if (IsValid(SMC)) + SMCSM = SMC->GetStaticMesh(); + + UStaticMesh* MySM = StaticMesh.Get(); + + // Return true if SMC's static mesh has been modified + return (MySM != SMCSM); +} + +bool +UHoudiniInputCameraComponent::HasComponentChanged(const FHoudiniInputObjectSettings& InSettings) const +{ + UCameraComponent* Camera = Cast(InputObject.LoadSynchronous()); + if (IsValid(Camera)) + { + bool bOrtho = Camera->ProjectionMode == ECameraProjectionMode::Type::Orthographic; + if (bOrtho != bIsOrthographic) + return true; + + if (Camera->FieldOfView != FOV) + return true; + + if (Camera->AspectRatio != AspectRatio) + return true; + + if (Camera->OrthoWidth != OrthoWidth) + return true; + + if (Camera->OrthoNearClipPlane != OrthoNearClipPlane) + return true; + + if (Camera->OrthoFarClipPlane != OrthoFarClipPlane) + return true; + } + + return false; +} + + + +void +UHoudiniInputCameraComponent::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + Super::Update(InObject, InSettings); + + UCameraComponent* Camera = Cast(InputObject.LoadSynchronous()); + + ensure(Camera); + + if (Camera && IsValid(Camera)) + { + bIsOrthographic = Camera->ProjectionMode == ECameraProjectionMode::Type::Orthographic; + FOV = Camera->FieldOfView; + AspectRatio = Camera->AspectRatio; + OrthoWidth = Camera->OrthoWidth; + OrthoNearClipPlane = Camera->OrthoNearClipPlane; + OrthoFarClipPlane = Camera->OrthoFarClipPlane; + } +} + +void +UHoudiniInputMeshComponent::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + Super::Update(InObject, InSettings); + + UStaticMeshComponent* SMC = Cast(InObject); + + ensure(SMC); + + if (SMC && IsValid(SMC)) + { + StaticMesh = TSoftObjectPtr(SMC->GetStaticMesh()); + } +} + +void +UHoudiniInputInstancedMeshComponent::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + Super::Update(InObject, InSettings); + + UInstancedStaticMeshComponent* ISMC = Cast(InObject); + + ensure(ISMC); + + if (ISMC) + { + uint32 InstanceCount = ISMC->GetInstanceCount(); + InstanceTransforms.SetNum(InstanceCount); + + // Copy the instances' transforms + for (uint32 InstIdx = 0; InstIdx < InstanceCount; InstIdx++) + { + FTransform CurTransform = FTransform::Identity; + ISMC->GetInstanceTransform(InstIdx, CurTransform); + InstanceTransforms[InstIdx] = CurTransform; + } + } +} + +bool +UHoudiniInputInstancedMeshComponent::HasInstancesChanged() const +{ + UInstancedStaticMeshComponent* ISMC = Cast(InputObject.LoadSynchronous()); + if (!ISMC) + return false; + + uint32 InstanceCount = ISMC->GetInstanceCount(); + if (InstanceTransforms.Num() != InstanceCount) + return true; + + // Copy the instances' transforms + for (uint32 InstIdx = 0; InstIdx < InstanceCount; InstIdx++) + { + FTransform CurTransform = FTransform::Identity; + ISMC->GetInstanceTransform(InstIdx, CurTransform); + + if(!InstanceTransforms[InstIdx].Equals(CurTransform)) + return true; + } + + return false; +} + +bool +UHoudiniInputInstancedMeshComponent::HasComponentTransformChanged() const +{ + if (Super::HasComponentTransformChanged()) + return true; + + return HasInstancesChanged(); +} + +void +UHoudiniInputSplineComponent::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + Super::Update(InObject, InSettings); + + USplineComponent* Spline = Cast(InObject); + + ensure(Spline); + + if (Spline) + { + NumberOfSplineControlPoints = Spline->GetNumberOfSplinePoints(); + SplineLength = Spline->GetSplineLength(); + SplineClosed = Spline->IsClosedLoop(); + + //SplineResolution = -1.0f; + + SplineControlPoints.SetNumZeroed(NumberOfSplineControlPoints); + for (int32 Idx = 0; Idx < NumberOfSplineControlPoints; Idx++) + { + SplineControlPoints[Idx] = Spline->GetTransformAtSplinePoint(Idx, ESplineCoordinateSpace::Local, true); + } + } +} + +void +UHoudiniInputHoudiniSplineComponent::Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) +{ + Super::Update(InObject, InSettings); + + // We store the component references as a normal pointer property instead of using a soft object reference. + // If we use a soft object reference, the editor will complain about deleting a reference that is in use + // everytime we try to delete the actor, even though everything is contained within the actor. + + CachedComponent = Cast(InObject); + InputObject = nullptr; + + // We need a strong ref to the spline component to prevent it from being GCed + //MyHoudiniSplineComponent = Cast(InObject); + UHoudiniSplineComponent* HoudiniSplineComponent = GetCurveComponent(); + + if (!IsValid(HoudiniSplineComponent)) + { + // Use default values + CurveType = EHoudiniCurveType::Polygon; + CurveMethod = EHoudiniCurveMethod::CVs; + Reversed = false; + } + else + { + CurveType = HoudiniSplineComponent->GetCurveType(); + CurveMethod = HoudiniSplineComponent->GetCurveMethod(); + Reversed = false;//Spline->IsReversed(); + } +} + +UObject* +UHoudiniInputHoudiniSplineComponent::GetObject() const +{ + return CachedComponent; +} + +void +UHoudiniInputHoudiniSplineComponent::SetNeedsToTriggerUpdate(const bool& bInTriggersUpdate) +{ + Super::SetNeedsToTriggerUpdate(bInTriggersUpdate); + + UHoudiniSplineComponent* HoudiniSplineComponent = GetCurveComponent(); + if (HoudiniSplineComponent) + { + HoudiniSplineComponent->SetNeedsToTriggerUpdate(bInTriggersUpdate); + } +} + +bool +UHoudiniInputHoudiniSplineComponent::HasChanged() const +{ + if (Super::HasChanged()) + return true; + + UHoudiniSplineComponent* HoudiniSplineComponent = GetCurveComponent(); + if (HoudiniSplineComponent && HoudiniSplineComponent->HasChanged()) + return true; + + return false; +} + +bool +UHoudiniInputHoudiniSplineComponent::NeedsToTriggerUpdate() const +{ + if (Super::NeedsToTriggerUpdate()) + return true; + UHoudiniSplineComponent* HoudiniSplineComponent = GetCurveComponent(); + if (HoudiniSplineComponent && HoudiniSplineComponent->NeedsToTriggerUpdate()) + return true; + + return false; +} + +void +UHoudiniInputHoudiniAsset::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + Super::Update(InObject, InSettings); + + UHoudiniCookable* HC = Cast(InObject); + if (!IsValid(HC)) + { + // TODO: Cookable - unecessary - remove me + UHoudiniAssetComponent* HAC = Cast(InObject); + HC = HAC ? Cast(HAC->GetOuter()) : nullptr; + } + + if (!IsValid(HC)) + return; + + // TODO: Allow selection of the asset output + OutputIndex = 0; + if (HC) + NodeId = HC->GetNodeId(); +} + + +void +UHoudiniInputActor::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + const bool bHasInputObjectChanged = InputObject != InObject; + + Super::Update(InObject, InSettings); + + // Ensure we have a valid GUID for merged spline mesh components + if (!GeneratedSplinesMeshPackageGuid.IsValid()) + GeneratedSplinesMeshPackageGuid = FGuid::NewGuid(); + + AActor* Actor = Cast(InObject); + ensure(Actor); + + if (Actor) + { + Transform = Actor->GetTransform(); + + // If we are updating (InObject == InputObject), then remove stale components and add new components, + // if InObject != InputObject, remove all components and rebuild + + if (bHasInputObjectChanged) + { + // The actor's components that can be sent as inputs + LastUpdateNumComponentsRemoved = ActorComponents.Num(); + + ActorComponents.Empty(); + ActorSceneComponents.Empty(); + NumSplineMeshComponents = 0; + + TArray AllComponents; + Actor->GetComponents(AllComponents, true); + + ActorComponents.Reserve(AllComponents.Num()); + for (USceneComponent * SceneComponent : AllComponents) + { + if (!IsValid(SceneComponent)) + continue; + if (!ShouldTrackComponent(SceneComponent)) + continue; + + UHoudiniInputObject* InputObj = UHoudiniInputObject::CreateTypedInputObject( + SceneComponent, GetOuter(), Actor->GetActorNameOrLabel(), InSettings, this); + if (!InputObj) + continue; + + UHoudiniInputSceneComponent* SceneInput = Cast(InputObj); + if (!SceneInput) + continue; + + if (SceneInput->IsA()) + NumSplineMeshComponents++; + + ActorComponents.Add(SceneInput); + ActorSceneComponents.Add(TSoftObjectPtr(SceneComponent)); + } + LastUpdateNumComponentsAdded = ActorComponents.Num(); + } + else + { + LastUpdateNumComponentsAdded = 0; + LastUpdateNumComponentsRemoved = 0; + NumSplineMeshComponents = 0; + + // Look for any components to add or remove + TSet NewComponents; + const bool bIncludeFromChildActors = true; + Actor->ForEachComponent(bIncludeFromChildActors, [&](USceneComponent* InComp) + { + if (!IsValid(InComp)) + return; + if (!ShouldTrackComponent(InComp)) + return; + + if (InComp->IsA()) + NumSplineMeshComponents++; + + if (ActorSceneComponents.Contains(InComp)) + return; + + NewComponents.Add(InComp); + }); + + // Update the actor input components (from the same actor) + TArray ComponentIndicesToRemove; + const int32 NumActorComponents = ActorComponents.Num(); + for (int32 Index = 0; Index < NumActorComponents; ++Index) + { + UHoudiniInputSceneComponent* CurActorComp = ActorComponents[Index]; + if (!IsValid(CurActorComp)) + { + ComponentIndicesToRemove.Add(Index); + continue; + } + + // If the tracked object is no longer valid, remove the input component. + // If the tracked object is valid, and it is a scene component, then if we should no longer be tracking + // components of that type remove it. + bool bShouldRemove = !IsValid(CurActorComp->GetObject()); + if (!bShouldRemove) + { + USceneComponent* const InputComp = CurActorComp->GetSceneComponent(); + if (IsValid(InputComp) && !ShouldTrackComponent(InputComp)) + bShouldRemove = true; + } + if (bShouldRemove) + { + // If it's not, mark it for deletion + if ((CurActorComp->GetInputNodeId() > 0) || (CurActorComp->GetInputObjectNodeId() > 0)) + { + CurActorComp->InvalidateData(); + } + + ComponentIndicesToRemove.Add(Index); + continue; + } + + // If the component is valid and should still be tracked, but it doesn't have the new ParentInputActor + // property set, then it needs to be recreated + if (!CurActorComp->GetParentInputActor()) + { + ComponentIndicesToRemove.Add(Index); + NewComponents.Add(CurActorComp->GetSceneComponent()); + } + } + + // Remove the destroyed/invalid components + const int32 NumToRemove = ComponentIndicesToRemove.Num(); + if (NumToRemove > 0) + { + for (int32 Index = NumToRemove - 1; Index >= 0; --Index) + { + const int32& IndexToRemove = ComponentIndicesToRemove[Index]; + + UHoudiniInputSceneComponent* const CurActorComp = ActorComponents[IndexToRemove]; + if (CurActorComp) + { + ActorSceneComponents.Remove(CurActorComp->InputObject); + } + + const bool bAllowShrink = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + ActorComponents.RemoveAtSwap(IndexToRemove, 1, bAllowShrink ? EAllowShrinking::Yes : EAllowShrinking::No); +#else + ActorComponents.RemoveAtSwap(IndexToRemove, 1, bAllowShrink); +#endif + + LastUpdateNumComponentsRemoved++; + } + } + + if (NewComponents.Num() > 0) + { + for (USceneComponent * SceneComponent : NewComponents) + { + if (!IsValid(SceneComponent)) + continue; + + UHoudiniInputObject* InputObj = UHoudiniInputObject::CreateTypedInputObject( + SceneComponent, GetOuter(), Actor->GetActorNameOrLabel(), InSettings, this); + if (!InputObj) + continue; + + UHoudiniInputSceneComponent* SceneInput = Cast(InputObj); + if (!SceneInput) + continue; + + ActorComponents.Add(SceneInput); + ActorSceneComponents.Add(SceneComponent); + + LastUpdateNumComponentsAdded++; + } + } + + if (LastUpdateNumComponentsAdded > 0 || LastUpdateNumComponentsRemoved > 0) + { + ActorComponents.Shrink(); + } + } + } + else + { + // If we don't have a valid actor or null, delete any input components we still have and mark as changed + if (ActorComponents.Num() > 0) + { + LastUpdateNumComponentsAdded = 0; + LastUpdateNumComponentsRemoved = ActorComponents.Num(); + ActorComponents.Empty(); + ActorSceneComponents.Empty(); + } + else + { + LastUpdateNumComponentsAdded = 0; + LastUpdateNumComponentsRemoved = 0; + } + NumSplineMeshComponents = 0; + } +} + +bool +UHoudiniInputActor::HasActorTransformChanged() const +{ + if (!GetActor()) + return false; + + if (HasRootComponentTransformChanged()) + return true; + + if (HasComponentsTransformChanged()) + return true; + + return false; +} + +bool UHoudiniInputActor::HasRootComponentTransformChanged() const +{ + if (!Transform.Equals(GetActor()->GetTransform())) + return true; + return false; +} + +bool UHoudiniInputActor::HasComponentsTransformChanged() const +{ + // Search through all the child components to see if we have changed + // transforms in there. + for (auto CurActorComp : GetActorComponents()) + { + if (CurActorComp->HasComponentTransformChanged()) + { + return true; + } + } + + return false; +} + +bool +UHoudiniInputActor::HasContentChanged(const FHoudiniInputObjectSettings& InSettings) const +{ + // In the new input system, treat the input actor as changed if it has any component changes + { + if (GetLastUpdateNumComponentsAdded() > 0 || GetLastUpdateNumComponentsRemoved() > 0) + return true; + for (UHoudiniInputSceneComponent const* const Component : ActorComponents) + { + if (!IsValid(Component)) + continue; + if (Component->HasComponentChanged(InSettings)) + return true; + } + } + + // Check if any of the spline mesh components that we are tracking has changed + TSet TrackedSMComponents; + bool bSplineMeshComponentChanged = false; + for (UHoudiniInputSceneComponent const* const Component : ActorComponents) + { + if (!IsValid(Component)) + continue; + + UHoudiniInputSplineMeshComponent const* const SMComponent = Cast(Component); + if (!IsValid(SMComponent)) + continue; + + TrackedSMComponents.Add(SMComponent->GetSplineMeshComponent()); + if (!SMComponent->HasComponentTransformChanged() && !SMComponent->HasComponentChanged(InSettings)) + continue; + + bSplineMeshComponentChanged = true; + } + + // Count the number of spline mesh components the actor has + AActor const* const Actor = GetActor(); + int32 CurrentNumSplineMeshComponents = 0; + if (IsValid(Actor)) + { + static constexpr bool bIncludeFromChildActors = false; + Actor->ForEachComponent(bIncludeFromChildActors, [this, &InSettings, &CurrentNumSplineMeshComponents, &bSplineMeshComponentChanged, &TrackedSMComponents](UActorComponent const* const InComponent) + { + if (!this->ShouldTrackComponent(InComponent, &InSettings)) + return; + + CurrentNumSplineMeshComponents++; + + if (!TrackedSMComponents.Contains(InComponent)) + bSplineMeshComponentChanged = true; + }); + } + + // This actor should be considered changed if: + // - Merge is switching from enabled to disabled, or disabled to enabled + // - Merging is enabled and at least one spline mesh component has changed, or the number of spline mesh components have changed + const bool bNeedsToMergeSplineMeshes = InSettings.bMergeSplineMeshComponents && CurrentNumSplineMeshComponents > 1; + if (bUsedMergeSplinesMeshAtLastTranslate != bNeedsToMergeSplineMeshes) + return true; + + if (bNeedsToMergeSplineMeshes && (bSplineMeshComponentChanged || NumSplineMeshComponents != CurrentNumSplineMeshComponents)) + return true; + + return false; +} + +bool +UHoudiniInputActor::GetChangedObjectsAndValidNodes(TArray& OutChangedObjects, TArray& OutNodeIdsOfUnchangedValidObjects) +{ + if (Super::GetChangedObjectsAndValidNodes(OutChangedObjects, OutNodeIdsOfUnchangedValidObjects)) + return true; + + // If the actor is merging spline mesh components then it should have a valid SplinesMeshObjectNodeId + if (bUsedMergeSplinesMeshAtLastTranslate) + { + if (SplinesMeshObjectNodeId >= 0) + { + // Old input system code removed. + } + else + { + OutChangedObjects.Add(this); + return true; + } + } + + bool bAnyChanges = false; + + return bAnyChanges; +} + +void +UHoudiniInputActor::InvalidateSplinesMeshData() +{ + GeneratedSplinesMesh = nullptr; + // If valid, mark our input nodes for deletion. + if (!CanDeleteHoudiniNodes()) + { + // Unless if we're a HoudiniAssetInput! we don't want to delete the other HDA's node! + // just invalidate the node IDs! + SplinesMeshNodeId = -1; + SplinesMeshObjectNodeId = -1; + SplinesMeshInputNodeHandle.Reset(); + } + else + { + // If we are using the new input system, then don't delete the nodes if we have a valid handle, and the HAPI + // nodes associated with the handle matches SplinesMeshNodeId / SplinesMeshObjectNodeId + if (SplinesMeshInputNodeHandle.IsValid()) + { + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (Manager) + { + TArray ManagedNodeIds; + if (Manager->GetHAPINodeIds(SplinesMeshInputNodeHandle.GetIdentifier(), ManagedNodeIds)) + { + if (ManagedNodeIds.Contains(SplinesMeshNodeId)) + SplinesMeshNodeId = -1; + if (ManagedNodeIds.Contains(SplinesMeshObjectNodeId)) + SplinesMeshObjectNodeId = -1; + } + } + } + + SplinesMeshInputNodeHandle.Reset(); + + if (SplinesMeshNodeId >= 0) + { + FHoudiniEngineRuntime::Get().MarkNodeIdAsPendingDelete(SplinesMeshNodeId); + SplinesMeshNodeId = -1; + } + + // ... and the parent OBJ as well to clean up + if (SplinesMeshObjectNodeId >= 0) + { + FHoudiniEngineRuntime::Get().MarkNodeIdAsPendingDelete(SplinesMeshObjectNodeId); + SplinesMeshObjectNodeId = -1; + } + } +} + +bool +UHoudiniInputActor::UsesInputObjectNode() const +{ + return true; +} + +void +UHoudiniInputActor::InvalidateData() +{ + InvalidateSplinesMeshData(); + + // Call invalidate on input component objects + for (auto* const CurrentComp : GetActorComponents()) + { + if (!IsValid(CurrentComp)) + continue; + + CurrentComp->InvalidateData(); + } + + Super::InvalidateData(); +} + +bool +UHoudiniInputActor::ShouldTrackComponent(UActorComponent const* InComponent, const FHoudiniInputObjectSettings* InSettings) const +{ + return true; +} + +void UHoudiniInputLevelInstance::Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) +{ + Super::Update(InObject, InSettings); + + NumActorsAddedLastUpdate = 0; + NumActorsRemovedLastUpdate = 0; + + if (!CachedInputSettings.bExportLevelInstanceContent) + { + NumActorsRemovedLastUpdate = TrackedActorObjects.Num(); + TrackedActorObjects.Empty(); + return; + } + + // We are exporting level content, make sure our TrackedActorObjects set is up to date + ALevelInstance* const LevelInstance = GetLevelInstance(); + if (!IsValid(LevelInstance)) + { + NumActorsRemovedLastUpdate = TrackedActorObjects.Num(); + TrackedActorObjects.Empty(); + return; + } + + UWorld* World = LevelInstance->GetWorldAsset().LoadSynchronous(); + if (!IsValid(World)) + { + NumActorsRemovedLastUpdate = TrackedActorObjects.Num(); + TrackedActorObjects.Empty(); + return; + } + + // Identify current and new actors + TSet CurrentActors; + CurrentActors.Reserve(World->GetActorCount()); + TSet NewActors; + for (TActorIterator It(World); It; ++It) + { + AActor* const Actor = *It; + if (!IsValid(Actor)) + continue; + + CurrentActors.Emplace(Actor); + if (TrackedActorObjects.Contains(Actor)) + continue; + NewActors.Add(Actor); + } + + // Remove actors from tracked set that are no longer in level instance + TArray> Keys; + TrackedActorObjects.GetKeys(Keys); + for (TSoftObjectPtr& Actor : Keys) + { + AActor const* const LoadedActor = Actor.LoadSynchronous(); + if (IsValid(LoadedActor) && CurrentActors.Contains(LoadedActor)) + continue; + + TrackedActorObjects.Remove(Actor); + NumActorsRemovedLastUpdate++; + } + + // Call Update on all tracked input objects (except for the new actors in NewActors: Update will be called in CreateTypeInputObject below) + for (const auto& Entry : TrackedActorObjects) + { + UHoudiniInputObject* const TrackedInputObject = Entry.Value; + if (!IsValid(TrackedInputObject)) + continue; + TrackedInputObject->Update(Entry.Key.Get(), InSettings); + } + + // Add / track new actors + for (AActor* const NewActor : NewActors) + { + if (!IsValid(NewActor)) + continue; + + UHoudiniInputObject* const InputObj = CreateTypedInputObject(NewActor, GetOuter(), NewActor->GetActorNameOrLabel(), InSettings); + if (!IsValid(InputObj)) + continue; + TrackedActorObjects.Emplace(NewActor, InputObj); + NumActorsAddedLastUpdate++; + } +} + + +bool UHoudiniInputLevelInstance::HasContentChanged(const FHoudiniInputObjectSettings& InSettings) const +{ + if (!InSettings.bExportLevelInstanceContent) + { + // Since we store positions as attributes, we need to update content after changes to the transform. + return bTransformChanged; + } + + if (NumActorsAddedLastUpdate > 0 || NumActorsRemovedLastUpdate > 0) + return true; + + // If we are exporting content, we need to check the content of our tracked actors + for (const auto& Entry : TrackedActorObjects) + { + UHoudiniInputObject const* const Object = Entry.Value; + if (!IsValid(Object)) + continue; + + UHoudiniInputActor const* const ActorObject = Cast(Object); + if (!IsValid(ActorObject)) + continue; + + if (ActorObject->HasContentChanged(InSettings)) + return true; + } + + return false; +} + +bool UHoudiniInputLevelInstance::HasTransformChanged() const +{ + if (bTransformChanged) + return true; + + for (const auto& Entry : TrackedActorObjects) + { + UHoudiniInputObject const* const Object = Entry.Value; + if (!IsValid(Object)) + continue; + + if (Object->HasTransformChanged()) + return true; + } + + return false; +} + +void UHoudiniInputPackedLevelActor::Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) +{ + Super::Update(InObject, InSettings); + + UBlueprint* BPObject = nullptr; + APackedLevelActor const* PackedLevelActor = GetPackedLevelActor(); + +#if WITH_EDITOR +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3) + if (IsValid(PackedLevelActor)) + BPObject = PackedLevelActor->GetRootBlueprint(); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + if (IsValid(PackedLevelActor)) + { + // From UE 5.3 APackedLevelActor::GetRootBlueprint() + UClass* Class = PackedLevelActor->GetClass(); + while (Class->GetSuperClass() && !Class->GetSuperClass()->IsNative()) + { + Class = Class->GetSuperClass(); + } + + BPObject = Cast(Class->ClassGeneratedBy); + } +#else + if (IsValid(PackedLevelActor)) + BPObject = PackedLevelActor->BlueprintAsset.LoadSynchronous(); +#endif +#endif + + if (!IsValid(BPObject)) + { + BlueprintInputObject = nullptr; + return; + } + + if (!IsValid(BlueprintInputObject) || BPObject != BlueprintInputObject->GetBlueprint()) + { + if (BPObject) + BlueprintInputObject = Cast(CreateTypedInputObject(BPObject, this, BPObject->GetName(), InSettings)); + return; + } + + BlueprintInputObject->Update(BPObject, InSettings); +} + +bool UHoudiniInputPackedLevelActor::HasContentChanged(const FHoudiniInputObjectSettings& InSettings) const +{ + if (Super::HasContentChanged(InSettings)) + return true; + + if (!IsValid(BlueprintInputObject)) + return false; + + return BlueprintInputObject->HasContentChanged(InSettings); +} + +APackedLevelActor* UHoudiniInputPackedLevelActor::GetPackedLevelActor() const +{ + return Cast(InputObject.LoadSynchronous()); +} + +void UHoudiniInputPackedLevelActor::InvalidateData() +{ + // Invalidate BP input object + if (IsValid(BlueprintInputObject)) + BlueprintInputObject->InvalidateData(); + + Super::InvalidateData(); +} + +bool UHoudiniInputPackedLevelActor::ShouldTrackComponent(UActorComponent const* InComponent, const FHoudiniInputObjectSettings* InSettings) const +{ + // Only track components when the old input system is being used: the old input system treats a packed level actor + // instance like a normal actor/world input + return false; +} + +bool UHoudiniInputLandscape::ShouldTrackComponent(UActorComponent const* InComponent, const FHoudiniInputObjectSettings* InSettings) const +{ + // We only track LandscapeComponents for landscape inputs since the Landscape tools + // have this very interesting and creative way of adding components when the tool is activated + // (looking at you Flatten tool) which causes cooking loops. + if (!IsValid(InComponent)) + return false; + + if (InComponent->IsA(ULandscapeComponent::StaticClass())) + return true; + + // Use InSettings if provided, otherwise use the settings cached at the last update + const FHoudiniInputObjectSettings& Settings = InSettings ? *InSettings : CachedInputSettings; + + if (Settings.bLandscapeAutoSelectSplines) + { + if (InComponent->IsA(ULandscapeSplinesComponent::StaticClass())) + return true; + + if (Settings.bLandscapeSplinesExportSplineMeshComponents && InComponent->IsA(USplineMeshComponent::StaticClass())) + return true; + } + + return false; +} + +bool UHoudiniInputLandscape::HasContentChanged(const FHoudiniInputObjectSettings& InSettings) const +{ + if (Super::HasContentChanged(InSettings)) + { + return true; + } + + // Ignore changes to landscapes in world partition, loading and unloading landscapes will trigger cooks + // which is very tiresome. + if (GetLandscapeProxy()->GetWorld()->IsPartitionedWorld()) + return false; + + const int32 NumComponents = CountLandscapeComponents(); + return NumComponents != CachedNumLandscapeComponents; +} + +FTransform +UHoudiniInputLandscape::GetHoudiniObjectTransform() const +{ + ALandscapeProxy* LandscapeProxy = GetLandscapeProxy(); + FTransform T = FHoudiniEngineRuntimeUtils::CalculateHoudiniLandscapeTransform(LandscapeProxy); + return T; +} + +void +UHoudiniInputLandscape::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + // Get the current value for if exporting spline meshes is enabled + if (!InSettings.bLandscapeSplinesExportSplineMeshComponents || !InSettings.bMergeSplineMeshComponents) + { + // Invalidate the merged splines nodes + InvalidateSplinesMeshData(); + } + + Super::Update(InObject, InSettings); + + ALandscapeProxy* LandscapeProxy = Cast(InObject); + if (LandscapeProxy) + { + Transform = LandscapeProxy->GetActorTransform(); + CachedNumLandscapeComponents = CountLandscapeComponents(); + } +} + +void UHoudiniInputLandscape::MarkTransformChanged(const bool bInChanged) +{ + // Landscape transforms aren't just set on one node in Houdini, + // so if the actor transform has changed, update all nodes. + MarkChanged(true); +} + +bool UHoudiniInputLandscape::HasActorTransformChanged() const +{ + if (!GetActor()) + return false; + + if (HasComponentsTransformChanged()) + return true; + + ALandscapeProxy* LandscapeProxy = GetLandscapeProxy(); + if (IsValid(LandscapeProxy)) + { + const FTransform ActorTransform = LandscapeProxy->GetActorTransform(); + if (!Transform.Equals(ActorTransform)) + { + return true; + } + } + + return false; +} + +bool +UHoudiniInputBlueprint::GetChangedObjectsAndValidNodes(TArray& OutChangedObjects, TArray& OutNodeIdsOfUnchangedValidObjects) +{ + if (Super::GetChangedObjectsAndValidNodes(OutChangedObjects, OutNodeIdsOfUnchangedValidObjects)) + return true; + + bool bAnyChanges = false; + return bAnyChanges; +} + +bool +UHoudiniInputBlueprint::HasComponentsTransformChanged() const +{ + // Search through all the child components to see if we have changed transforms in there. + for (auto CurrentComp : GetComponents()) + { + if (CurrentComp->HasComponentTransformChanged()) + { + return true; + } + } + + return false; +} + +bool +UHoudiniInputBlueprint::HasContentChanged(const FHoudiniInputObjectSettings& InSettings) const +{ + // In the new input system, treat the input blueprint as changed if it has any component changes + + { + if (GetLastUpdateNumComponentsAdded() > 0 || GetLastUpdateNumComponentsRemoved() > 0) + return true; + for (UHoudiniInputSceneComponent const* const Component : GetComponents()) + { + if (!IsValid(Component)) + continue; + if (Component->HasComponentChanged(InSettings)) + return true; + } + } + + return false; +} + +void +UHoudiniInputBlueprint::InvalidateData() +{ + // Call invalidate on input component objects + for (auto* const CurrentComp : GetComponents()) + { + if (!IsValid(CurrentComp)) + continue; + + CurrentComp->InvalidateData(); + } + + Super::InvalidateData(); +} + + +bool +UHoudiniInputBlueprint::UsesInputObjectNode() const +{ + return true; +} + +void +UHoudiniInputBlueprint::Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) +{ + const bool bHasInputObjectChanged = InputObject != InObject; + + Super::Update(InObject, InSettings); + + UBlueprint* BP = Cast(InObject); + ensure(BP); + + if (BP) + { + // If we are updating (InObject == InputObject), then remove stale components and add new components, + // if InObject != InputObject, remove all components and rebuild + if (bHasInputObjectChanged) + { + // The actor's components that can be sent as inputs + LastUpdateNumComponentsRemoved = BPComponents.Num(); + + BPComponents.Empty(); + BPSceneComponents.Empty(); + + TArray AllComponents; + USimpleConstructionScript* SCS = BP->SimpleConstructionScript; + if (IsValid(SCS)) + { + const TArray& Nodes = SCS->GetAllNodes(); + for (auto& CurNode : Nodes) + { + if (!IsValid(CurNode)) + continue; + + UActorComponent* CurComp = CurNode->ComponentTemplate; + if (!IsValid(CurComp)) + continue; + + USceneComponent* CurSceneComp = Cast(CurComp); + if (!IsValid(CurSceneComp)) + continue; + + AllComponents.Add(CurSceneComp); + } + } + + BPComponents.Reserve(AllComponents.Num()); + for (USceneComponent* SceneComponent : AllComponents) + { + if (!IsValid(SceneComponent)) + continue; + + UHoudiniInputObject* InputObj = UHoudiniInputObject::CreateTypedInputObject( + SceneComponent, GetOuter(), BP->GetName(), InSettings); + if (!InputObj) + continue; + + UHoudiniInputSceneComponent* SceneInput = Cast(InputObj); + if (!SceneInput) + continue; + + BPComponents.Add(SceneInput); + BPSceneComponents.Add(TSoftObjectPtr(SceneComponent)); + } + LastUpdateNumComponentsAdded = BPComponents.Num(); + } + else + { + LastUpdateNumComponentsAdded = 0; + LastUpdateNumComponentsRemoved = 0; + + // Look for any components to add or remove + TArray NewComponents; + USimpleConstructionScript* SCS = BP->SimpleConstructionScript; + if (IsValid(SCS)) + { + const TArray& Nodes = SCS->GetAllNodes(); + for (auto& CurNode : Nodes) + { + if (!IsValid(CurNode)) + continue; + + UActorComponent* CurComp = CurNode->ComponentTemplate; + if (!IsValid(CurComp)) + continue; + + USceneComponent* CurSceneComp = Cast(CurComp); + if (!IsValid(CurSceneComp)) + continue; + + if (!BPSceneComponents.Contains(CurSceneComp)) + { + NewComponents.Add(CurSceneComp); + } + } + } + + // Update the BP input components (from the same actor) + TArray ComponentIndicesToRemove; + const int32 NumActorComponents = BPComponents.Num(); + for (int32 Index = 0; Index < NumActorComponents; ++Index) + { + UHoudiniInputSceneComponent* CurBPComp = BPComponents[Index]; + if (!IsValid(CurBPComp)) + { + ComponentIndicesToRemove.Add(Index); + continue; + } + + // Does the component still exist on the BP? + UObject* const CompObj = CurBPComp->GetObject(); + // Make sure the BP is still valid + if (!IsValid(CompObj)) + { + // If it's not, mark it for deletion + if ((CurBPComp->GetInputNodeId() > 0) || (CurBPComp->GetInputObjectNodeId() > 0)) + { + CurBPComp->InvalidateData(); + } + + ComponentIndicesToRemove.Add(Index); + continue; + } + } + + // Remove the destroyed/invalid components + const int32 NumToRemove = ComponentIndicesToRemove.Num(); + if (NumToRemove > 0) + { + for (int32 Index = NumToRemove - 1; Index >= 0; --Index) + { + const int32& IndexToRemove = ComponentIndicesToRemove[Index]; + + UHoudiniInputSceneComponent* const CurBPComp = BPComponents[IndexToRemove]; + if (CurBPComp) + BPSceneComponents.Remove(CurBPComp->InputObject); + + const bool bAllowShrink = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + BPComponents.RemoveAtSwap(IndexToRemove, 1, bAllowShrink ? EAllowShrinking::Yes : EAllowShrinking::No); +#else + BPComponents.RemoveAtSwap(IndexToRemove, 1, bAllowShrink); +#endif + + LastUpdateNumComponentsRemoved++; + } + } + + // Call Update on the remaining scene components (new components have Update called in CreateTypeInputObject below) + for (UHoudiniInputSceneComponent* const ComponentInputObject : BPComponents) + { + if (!IsValid(ComponentInputObject)) + continue; + USceneComponent* const SceneComponent = ComponentInputObject->GetSceneComponent(); + ComponentInputObject->Update(SceneComponent, InSettings); + } + + if (NewComponents.Num() > 0) + { + for (USceneComponent* SceneComponent : NewComponents) + { + if (!IsValid(SceneComponent)) + continue; + + UHoudiniInputObject* InputObj = UHoudiniInputObject::CreateTypedInputObject( + SceneComponent, GetOuter(), BP->GetName(), InSettings); + if (!InputObj) + continue; + + UHoudiniInputSceneComponent* SceneInput = Cast(InputObj); + if (!SceneInput) + continue; + + BPComponents.Add(SceneInput); + BPSceneComponents.Add(SceneComponent); + + LastUpdateNumComponentsAdded++; + } + } + + if (LastUpdateNumComponentsAdded > 0 || LastUpdateNumComponentsRemoved > 0) + { + BPComponents.Shrink(); + } + } + } + else + { + // If we don't have a valid BP or null, delete any input components we still have and mark as changed + if (BPComponents.Num() > 0) + { + LastUpdateNumComponentsAdded = 0; + LastUpdateNumComponentsRemoved = BPComponents.Num(); + BPComponents.Empty(); + BPSceneComponents.Empty(); + } + else + { + LastUpdateNumComponentsAdded = 0; + LastUpdateNumComponentsRemoved = 0; + } + } +} + + +EHoudiniInputObjectType +UHoudiniInputObject::GetInputObjectTypeFromObject(UObject* InObject) +{ + if (InObject->IsA(USceneComponent::StaticClass())) + { + // Handle component inputs + if (InObject->IsA(UInstancedStaticMeshComponent::StaticClass())) + { + // ISMC derives from SMC, so always test instances before static meshes + return EHoudiniInputObjectType::InstancedStaticMeshComponent; + } + else if (InObject->IsA(USkeletalMeshComponent::StaticClass())) + { + // SKMC also derives from SMC, so SKMC must be tested before SMC + return EHoudiniInputObjectType::SkeletalMeshComponent; + } + else if (InObject->IsA(USplineMeshComponent::StaticClass())) + { + return EHoudiniInputObjectType::SplineMeshComponent; + } + else if (InObject->IsA(UStaticMeshComponent::StaticClass())) + { + return EHoudiniInputObjectType::StaticMeshComponent; + } + else if (InObject->IsA(USplineComponent::StaticClass())) + { + return EHoudiniInputObjectType::SplineComponent; + } + else if (InObject->IsA(UHoudiniSplineComponent::StaticClass())) + { + return EHoudiniInputObjectType::HoudiniSplineComponent; + } + else if (InObject->IsA(UHoudiniAssetComponent::StaticClass())) + { + return EHoudiniInputObjectType::HoudiniAssetComponent; + } + else if (InObject->IsA(UHoudiniCookable::StaticClass())) + { + return EHoudiniInputObjectType::HoudiniCookable; + } + else if (InObject->IsA(UCameraComponent::StaticClass())) + { + return EHoudiniInputObjectType::CameraComponent; + } + else if (InObject->IsA(UGeometryCollectionComponent::StaticClass())) + { + return EHoudiniInputObjectType::GeometryCollectionComponent; + } + else if (InObject->IsA(ULandscapeSplinesComponent::StaticClass())) + { + return EHoudiniInputObjectType::LandscapeSplinesComponent; + } + else if (InObject->IsA(UAnimSequence::StaticClass())) + { + return EHoudiniInputObjectType::Animation; + } + else + { + return EHoudiniInputObjectType::SceneComponent; + } + } + else if (InObject->IsA(AActor::StaticClass())) + { + // Handle actors + if (InObject->IsA(ALandscapeProxy::StaticClass())) + { + return EHoudiniInputObjectType::Landscape; + } + else if (InObject->IsA(APackedLevelActor::StaticClass())) + { + return EHoudiniInputObjectType::PackedLevelActor; + } + else if (InObject->IsA(ALevelInstance::StaticClass())) + { + return EHoudiniInputObjectType::LevelInstance; + } + else if (InObject->IsA(ABrush::StaticClass())) + { + return EHoudiniInputObjectType::Brush; + } + else if (InObject->IsA(AHoudiniAssetActor::StaticClass())) + { + return EHoudiniInputObjectType::HoudiniAssetActor; + } + else if (InObject->IsA(ALandscapeSplineActor::StaticClass())) + { + return EHoudiniInputObjectType::LandscapeSplineActor; + } + else + { + return EHoudiniInputObjectType::Actor; + } + } + else if (InObject->IsA(UBlueprint::StaticClass())) + { + return EHoudiniInputObjectType::Blueprint; + } + else if (InObject->IsA(UFoliageType_InstancedStaticMesh::StaticClass())) + { + return EHoudiniInputObjectType::FoliageType_InstancedStaticMesh; + } + else + { + if (InObject->IsA(UStaticMesh::StaticClass())) + { + return EHoudiniInputObjectType::StaticMesh; + } + else if (InObject->IsA(USkeletalMesh::StaticClass())) + { + return EHoudiniInputObjectType::SkeletalMesh; + } + else if (InObject->IsA(UAnimSequence::StaticClass())) + { + return EHoudiniInputObjectType::Animation; + } + else if (InObject->IsA(UGeometryCollection::StaticClass())) + { + return EHoudiniInputObjectType::GeometryCollection; + } + else if (InObject->IsA(UDataTable::StaticClass())) + { + return EHoudiniInputObjectType::DataTable; + } +#if defined(HOUDINI_USE_PCG) + else if(InObject->IsA(UHoudiniPCGDataCollection::StaticClass())) + { + return EHoudiniInputObjectType::PCGData; + } +#endif + else if (InObject->IsA(UTexture2D::StaticClass())) + { + return EHoudiniInputObjectType::Texture; + } + else + { + return EHoudiniInputObjectType::Object; + } + } + // unreachable + // return EHoudiniInputObjectType::Invalid; +} + + + +//----------------------------------------------------------------------------------------------------------------------------- +// UHoudiniInputBrush +//----------------------------------------------------------------------------------------------------------------------------- + +FHoudiniBrushInfo::FHoudiniBrushInfo() + : CachedTransform() + , CachedOrigin(ForceInitToZero) + , CachedExtent(ForceInitToZero) + , CachedBrushType(EBrushType::Brush_Default) + , CachedSurfaceHash(0) +{ +} + +FHoudiniBrushInfo::FHoudiniBrushInfo(ABrush* InBrushActor) +{ + if (!InBrushActor) + return; + + BrushActor = InBrushActor; + CachedTransform = BrushActor->GetActorTransform(); + BrushActor->GetActorBounds(false, CachedOrigin, CachedExtent); + CachedBrushType = BrushActor->BrushType; + +#if WITH_EDITOR + UModel* Model = BrushActor->Brush; + + // Cache the hash of the surface properties + if (IsValid(Model) && IsValid(Model->Polys)) + { + int32 NumPolys = Model->Polys->Element.Num(); + CachedSurfaceHash = 0; + for(int32 iPoly = 0; iPoly < NumPolys; ++iPoly) + { + const FPoly& Poly = Model->Polys->Element[iPoly]; + CombinePolyHash(CachedSurfaceHash, Poly); + } + } + else + { + CachedSurfaceHash = 0; + } +#endif +} + +bool FHoudiniBrushInfo::HasChanged() const +{ + if (!BrushActor.IsValid()) + return false; + + // Has the transform changed? + if (!BrushActor->GetActorTransform().Equals(CachedTransform)) + return true; + + if (BrushActor->BrushType != CachedBrushType) + return true; + + // Has the actor bounds changed? + FVector TmpOrigin, TmpExtent; + BrushActor->GetActorBounds(false, TmpOrigin, TmpExtent); + + if (!(TmpOrigin.Equals(CachedOrigin) && TmpExtent.Equals(CachedExtent) )) + return true; +#if WITH_EDITOR + // Is there a tracked surface property that changed? + UModel* Model = BrushActor->Brush; + if (IsValid(Model) && IsValid(Model->Polys)) + { + // Hash the incoming surface properties and compared it against the cached hash. + int32 NumPolys = Model->Polys->Element.Num(); + uint64 SurfaceHash = 0; + for (int32 iPoly = 0; iPoly < NumPolys; ++iPoly) + { + const FPoly& Poly = Model->Polys->Element[iPoly]; + CombinePolyHash(SurfaceHash, Poly); + } + if (SurfaceHash != CachedSurfaceHash) + return true; + } + else + { + if (CachedSurfaceHash != 0) + return true; + } +#endif + return false; +} + +int32 FHoudiniBrushInfo::GetNumVertexIndicesFromModel(const UModel* Model) +{ + const TArray& Nodes = Model->Nodes; + int32 NumIndices = 0; + // Build the face counts buffer by iterating over the BSP nodes. + for(const FBspNode& Node : Nodes) + { + NumIndices += Node.NumVertices; + } + return NumIndices; +} + +UModel* UHoudiniInputBrush::GetCachedModel() const +{ + return CombinedModel; +} + +bool UHoudiniInputBrush::HasBrushesChanged(const TArray& InBrushes) const +{ + if (InBrushes.Num() != BrushesInfo.Num()) + return true; + + int32 NumBrushes = BrushesInfo.Num(); + + for (int32 InfoIndex = 0; InfoIndex < NumBrushes; ++InfoIndex) + { + const FHoudiniBrushInfo& BrushInfo = BrushesInfo[InfoIndex]; + // Has the cached brush actor invalid? + if (!BrushInfo.BrushActor.IsValid()) + return true; + + // Has there been an order change in the actors list? + if (InBrushes[InfoIndex] != BrushInfo.BrushActor.Get()) + return true; + + // Has there been any other changes to the brush? + if (BrushInfo.HasChanged()) + return true; + } + + // Nothing has changed. + return false; +} + +void UHoudiniInputBrush::UpdateCachedData(UModel* InCombinedModel, const TArray& InBrushes) +{ + ABrush* InputBrush = GetBrush(); + if (IsValid(InputBrush)) + { + CachedInputBrushType = InputBrush->BrushType; + } + + // Cache the combined model aswell as the brushes used to generate this model. + CombinedModel = InCombinedModel; + + BrushesInfo.SetNum(InBrushes.Num()); + for (int i = 0; i < InBrushes.Num(); ++i) + { + if (!InBrushes[i]) + continue; + BrushesInfo[i] = FHoudiniBrushInfo(InBrushes[i]); + } +} + + +void +UHoudiniInputBrush::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + Super::Update(InObject, InSettings); + + ABrush* BrushActor = GetBrush(); + if (!IsValid(BrushActor)) + { + bIgnoreInputObject = true; + return; + } + + CachedInputBrushType = BrushActor->BrushType; + + bIgnoreInputObject = ShouldIgnoreThisInput(); +} + +bool +UHoudiniInputBrush::ShouldIgnoreThisInput() +{ + // Invalid brush, should be ignored + ABrush* BrushActor = GetBrush(); + ensure(BrushActor); + if (!BrushActor) + return true; + + // If the BrushType has changed since caching this object, this object cannot be ignored. + if (CachedInputBrushType != BrushActor->BrushType) + return false; + + // If it's not an additive brush, we want to ignore it + bool bShouldBeIgnored = BrushActor->BrushType != EBrushType::Brush_Add; + + // If this is not a static brush (e.g., AVolume), ignore it. + if (!bShouldBeIgnored) + bShouldBeIgnored = !BrushActor->IsStaticBrush(); + + return bShouldBeIgnored; +} + +bool UHoudiniInputBrush::HasContentChanged(const FHoudiniInputObjectSettings& InSettings) const +{ + ABrush* BrushActor = GetBrush(); + ensure(BrushActor); + + if (!BrushActor) + return false; + + if (BrushActor->BrushType != CachedInputBrushType) + return true; + + if (bIgnoreInputObject) + return false; + + // Find intersecting actors and capture their properties so that + // we can determine whether something has changed. + TArray IntersectingBrushes; + FindIntersectingSubtractiveBrushes(this, IntersectingBrushes); + + if (HasBrushesChanged(IntersectingBrushes)) + { + return true; + } + + return false; +} + +bool +UHoudiniInputBrush::HasActorTransformChanged() const +{ + if (bIgnoreInputObject) + return false; + + return Super::HasActorTransformChanged(); +} + + +bool UHoudiniInputBrush::FindIntersectingSubtractiveBrushes(const UHoudiniInputBrush* InputBrush, TArray& OutBrushes) +{ + TArray IntersectingActors; + TArray Bounds; + + if (!IsValid(InputBrush)) + return false; + + ABrush* BrushActor = InputBrush->GetBrush(); + if (!IsValid(BrushActor)) + return false; + + + OutBrushes.Empty(); + + Bounds.Add( BrushActor->GetComponentsBoundingBox(true, true) ); + + FHoudiniEngineRuntimeUtils::FindActorsOfClassInBounds(BrushActor->GetWorld(), ABrush::StaticClass(), Bounds, nullptr, IntersectingActors); + + //-------------------------------------------------------------------------------------------------- + // Filter the actors to only keep intersecting subtractive brushes. + //-------------------------------------------------------------------------------------------------- + for (AActor* Actor : IntersectingActors) + { + // Filter out anything that is not a static brush (typically volume actors). + ABrush* Brush = Cast(Actor); + + // NOTE: The brush actor needs to be added in the correct map/level order + // together with the subtractive brushes otherwise the CSG operations + // will not match the BSP in the level. + if (Actor == BrushActor) + OutBrushes.Add(Brush); + + if (!(Brush && Brush->IsStaticBrush())) + continue; + + if (Brush->BrushType == Brush_Subtract) + OutBrushes.Add(Brush); + } + + return true; +} + +#if WITH_EDITOR +void +UHoudiniInputObject::PostEditUndo() +{ + Super::PostEditUndo(); + MarkChanged(true); +} +#endif + +UHoudiniInputObject* +UHoudiniInputObject::DuplicateAndCopyState(UObject * DestOuter) +{ + UHoudiniInputObject* NewInput = Cast(StaticDuplicateObject(this, DestOuter)); + NewInput->CopyStateFrom(this, false); + return NewInput; +} + +void +UHoudiniInputObject::CopyStateFrom(UHoudiniInputObject* InInput, bool bCopyAllProperties) +{ + // Copy the state of this UHoudiniInput object. + if (bCopyAllProperties) + { + UEngine::FCopyPropertiesForUnrelatedObjectsParams Params; + Params.bDoDelta = false; // Perform a deep copy + Params.bClearReferences = false; // References should be replaced afterwards. + UEngine::CopyPropertiesForUnrelatedObjects(InInput, this, Params); + } + + InputNodeId = InInput->InputNodeId; + InputObjectNodeId = InInput->InputObjectNodeId; + bHasChanged = InInput->bHasChanged; + bNeedsToTriggerUpdate = InInput->bNeedsToTriggerUpdate; + bTransformChanged = InInput->bTransformChanged; + Guid = InInput->Guid; + InputNodeHandle = InInput->InputNodeHandle; + +#if WITH_EDITORONLY_DATA + bUniformScaleLocked = InInput->bUniformScaleLocked; +#endif + +} + +void +UHoudiniInputObject::SetCanDeleteHoudiniNodes(bool bInCanDeleteNodes) +{ + bCanDeleteHoudiniNodes = bInCanDeleteNodes; +} + +bool +UHoudiniInputObject::GetChangedObjectsAndValidNodes(TArray& OutChangedObjects, TArray& OutNodeIdsOfUnchangedValidObjects) +{ + if (HasChanged()) + { + // Has changed, needs to be recreated + OutChangedObjects.Add(this); + return true; + } + + if (UsesInputObjectNode()) + { + // Do not use InputObjectNodeId directly here! + // This can cause issues when dealing with InputActors + int32 InObjNodeId = GetInputObjectNodeId(); + if (InObjNodeId >= 0) + { + // No changes, keep it + OutNodeIdsOfUnchangedValidObjects.Add(InObjNodeId); + } + else + { + // Needs, but does not have, a current HAPI input node + OutChangedObjects.Add(this); + return true; + } + } + + // No changes and valid object node exists (or no node is used by this object type) + return false; +} + +UHoudiniInputPCGData::UHoudiniInputPCGData(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +UHoudiniInputObject* +UHoudiniInputPCGData::Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_PCG_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputPCGData::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputPCGData* HoudiniInputObject = NewObject( + InOuter, UHoudiniInputPCGData::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::PCGData; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + HoudiniInputObject->SetTransform(FTransform::Identity); + return HoudiniInputObject; +} + +UHoudiniPCGDataCollection* +UHoudiniInputPCGData::GetPCGData() const +{ +#if defined(HOUDINI_USE_PCG) + return Cast(InputObject.LoadSynchronous()); +#else + return nullptr; +#endif +} + + +// +UHoudiniInputDataTable::UHoudiniInputDataTable(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +UHoudiniInputObject * +UHoudiniInputDataTable::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_DT_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputDataTable::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputDataTable * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputDataTable::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::DataTable; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UDataTable* +UHoudiniInputDataTable::GetDataTable() const +{ + return Cast(InputObject.LoadSynchronous()); +} + +UHoudiniInputFoliageType_InstancedStaticMesh::UHoudiniInputFoliageType_InstancedStaticMesh(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +UHoudiniInputObject* +UHoudiniInputFoliageType_InstancedStaticMesh::Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_FoliageSM_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputFoliageType_InstancedStaticMesh::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputFoliageType_InstancedStaticMesh * HoudiniInputObject = NewObject( + InOuter, UHoudiniInputFoliageType_InstancedStaticMesh::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::FoliageType_InstancedStaticMesh; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +void +UHoudiniInputFoliageType_InstancedStaticMesh::Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) +{ + UHoudiniInputObject::Update(InObject, InSettings); + UFoliageType_InstancedStaticMesh* const FoliageType = Cast(InObject); + ensure(FoliageType); + if (FoliageType) + { + ensure(FoliageType->GetStaticMesh()); + } +} + +UStaticMesh* +UHoudiniInputFoliageType_InstancedStaticMesh::GetStaticMesh() const +{ + if (!InputObject.IsValid()) + return nullptr; + + UFoliageType_InstancedStaticMesh* const FoliageType = Cast(InputObject.LoadSynchronous()); + if (!IsValid(FoliageType)) + return nullptr; + + return FoliageType->GetStaticMesh(); +} + + +UHoudiniInputSplineMeshComponent::UHoudiniInputSplineMeshComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , CachedForwardAxis() + , CachedSplineUpDir(FVector::UpVector) + , CachedSplineBoundaryMax(0) + , CachedSplineBoundaryMin(0) + , CachedbSmoothInterpRollScale(false) +{ + +} + +UHoudiniInputObject* +UHoudiniInputSplineMeshComponent::Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor) +{ + const FString InputObjectNameStr = "HoudiniInputObject_SplineMeshComponent_" + InName; + const FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputSplineMeshComponent::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputSplineMeshComponent* HoudiniInputObject = NewObject( + InOuter, UHoudiniInputSplineMeshComponent::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->SetParentInputActor(InParentInputActor); + HoudiniInputObject->Type = EHoudiniInputObjectType::SplineMeshComponent; + HoudiniInputObject->MeshPackageGuid = FGuid::NewGuid(); + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +void +UHoudiniInputSplineMeshComponent::Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) +{ + Super::Update(InObject, InSettings); + + USplineMeshComponent const* const SplineMeshComponent = Cast(InObject); + ensure(SplineMeshComponent); + + if (!SplineMeshComponent) + return; + + CachedForwardAxis = SplineMeshComponent->ForwardAxis; + CachedSplineParams = SplineMeshComponent->SplineParams; + CachedSplineUpDir = SplineMeshComponent->SplineUpDir; + CachedSplineBoundaryMax = SplineMeshComponent->SplineBoundaryMax; + CachedSplineBoundaryMin = SplineMeshComponent->SplineBoundaryMin; + CachedbSmoothInterpRollScale = SplineMeshComponent->bSmoothInterpRollScale; +} + +USplineMeshComponent* +UHoudiniInputSplineMeshComponent::GetSplineMeshComponent() const +{ + return Cast(InputObject.LoadSynchronous()); +} + +bool +UHoudiniInputSplineMeshComponent::HasComponentChanged(const FHoudiniInputObjectSettings& InSettings) const +{ + if (Super::HasComponentChanged(InSettings)) + { + return true; + } + + USplineMeshComponent const* const SplineMeshComponent = Cast(InputObject.LoadSynchronous()); + + if (!IsValid(SplineMeshComponent)) + return false; + + if (SplineMeshComponent->ForwardAxis != CachedForwardAxis + || SplineMeshComponent->SplineUpDir != CachedSplineUpDir + || SplineMeshComponent->SplineBoundaryMax != CachedSplineBoundaryMax + || SplineMeshComponent->SplineBoundaryMin != CachedSplineBoundaryMin + || SplineMeshComponent->bSmoothInterpRollScale != CachedbSmoothInterpRollScale) + { + return true; + } + + if (SplineMeshComponent->SplineParams.StartPos != CachedSplineParams.StartPos + || SplineMeshComponent->SplineParams.StartTangent != CachedSplineParams.StartTangent + || SplineMeshComponent->SplineParams.StartScale != CachedSplineParams.StartScale + || SplineMeshComponent->SplineParams.StartRoll != CachedSplineParams.StartRoll + || SplineMeshComponent->SplineParams.StartOffset != CachedSplineParams.StartOffset + || SplineMeshComponent->SplineParams.EndPos != CachedSplineParams.EndPos + || SplineMeshComponent->SplineParams.EndScale != CachedSplineParams.EndScale + || SplineMeshComponent->SplineParams.EndTangent != CachedSplineParams.EndTangent + || SplineMeshComponent->SplineParams.EndRoll != CachedSplineParams.EndRoll + || SplineMeshComponent->SplineParams.EndOffset != CachedSplineParams.EndOffset) + { + return true; + } + + return false; +} + +UHoudiniInputTexture::UHoudiniInputTexture(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + bInputNodeHandleOverridesNodeIds = false; +} + +UHoudiniInputObject* +UHoudiniInputTexture::Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings) +{ + FString InputObjectNameStr = "HoudiniInputObject_Texture_" + InName; + FName InputObjectName = MakeUniqueObjectName(InOuter, UHoudiniInputTexture::StaticClass(), *InputObjectNameStr); + + // We need to create a new object + UHoudiniInputTexture* HoudiniInputObject = NewObject( + InOuter, UHoudiniInputTexture::StaticClass(), InputObjectName, RF_Public | RF_Transactional); + + HoudiniInputObject->Type = EHoudiniInputObjectType::Texture; + HoudiniInputObject->Update(InObject, InInputSettings); + HoudiniInputObject->bHasChanged = true; + + return HoudiniInputObject; +} + +UTexture2D* +UHoudiniInputTexture::GetTexture() const +{ + return Cast(InputObject.LoadSynchronous()); +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniInputObject.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniInputTypes.h" +#include "HoudiniSplineComponent.h" +#include "HoudiniGeoPartObject.h" +#include "UnrealObjectInputRuntimeTypes.h" + +#include "CoreTypes.h" +#include "Materials/MaterialInterface.h" +#include "UObject/Object.h" +#include "UObject/ObjectMacros.h" + +#include "Engine/Brush.h" +#include "Engine/Polys.h" +#include "UObject/SoftObjectPtr.h" + +#include "LandscapeSplineSegment.h" + +#include "HoudiniInputObject.generated.h" + +class UHoudiniPCGDataObject; +class UHoudiniPCGDataCollection; +class ULandscapeSplineControlPoint; +class UStaticMesh; +class USkeletalMesh; +class USceneComponent; +class UStaticMeshComponent; +class USkeletalMeshComponent; +class UInstancedStaticMeshComponent; +class USplineComponent; +class UHoudiniAssetComponent; +class UHoudiniCookable; +class AActor; +class ALandscapeProxy; +class ABrush; +class UHoudiniInput; +class ALandscapeProxy; +class UModel; +class UHoudiniInput; +class UCameraComponent; +class ALevelInstance; +class APackedLevelActor; +class UHoudiniInputActor; +class UPCGData; + +UENUM() +enum class EHoudiniInputObjectType : uint8 +{ + Invalid, + + Object, + StaticMesh, + SkeletalMesh, + SceneComponent, + StaticMeshComponent, + InstancedStaticMeshComponent, + SplineComponent, + HoudiniSplineComponent, + HoudiniAssetComponent, // TODO: Change to cookable? + Actor, + Landscape, + Brush, + CameraComponent, + DataTable, + HoudiniAssetActor, + FoliageType_InstancedStaticMesh, + GeometryCollection, + GeometryCollectionComponent, + GeometryCollectionActor_Deprecated, + SkeletalMeshComponent, + LandscapeSplineActor, + LandscapeSplinesComponent, + Blueprint, + Animation, + SplineMeshComponent, + LevelInstance, + PackedLevelActor, + PCGData, + Texture, + HoudiniCookable +}; + + +//----------------------------------------------------------------------------------------------------------------------------- +// UObjects input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputObject : public UObject +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Create the proper input object + static UHoudiniInputObject * CreateTypedInputObject(UObject * InObject, UObject* InOuter, const FString& InParamName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor=nullptr); + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // Check whether two input objects match + virtual bool Matches(const UHoudiniInputObject& Other) const; + + // + static EHoudiniInputObjectType GetInputObjectTypeFromObject(UObject* InObject); + + // + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings); + + // Set the Transform of this input object. For geo inputs this is a transform offset, for actors the actor world + // transform and for scene components the component world transform. + void SetTransform(const FTransform& InTransform) { Transform = InTransform; } + const FTransform& GetTransform() const { return Transform; } + FTransform& GetTransform() { return Transform; } + + // Get the transform to use on the Obj node associated with this object in Houdini. The default implementation + // calls GetTransform(). + virtual FTransform GetHoudiniObjectTransform() const { return GetTransform(); } + + // Invalidate and ask for the deletion of this input object's node + virtual void InvalidateData(); + + // UObject accessor + virtual UObject* GetObject() const; + + // Indicates if this input has changed and should be updated + virtual bool HasChanged() const { return bHasChanged; }; + + // Indicates if this input has changed and should be updated + virtual bool HasTransformChanged() const { return bTransformChanged; }; + + // Indicates if this input needs to trigger an update + virtual bool NeedsToTriggerUpdate() const { return bNeedsToTriggerUpdate; }; + + virtual void MarkChanged(const bool& bInChanged); + virtual void SetNeedsToTriggerUpdate(const bool& bInTriggersUpdate) { bNeedsToTriggerUpdate = bInTriggersUpdate; }; + virtual void MarkTransformChanged(const bool bInChanged) { bTransformChanged = bInChanged; SetNeedsToTriggerUpdate(bInChanged); }; + + // Set the InputNodeId. + void SetInputNodeId(int32 InInputNodeId); + + // If the ref counted system is enabled, then return the node via the InputNodeHandle, otherwise return the InputNodeId. + int32 GetInputNodeId() const; + + // Set the InputObjectNodeId + void SetInputObjectNodeId(int32 InInputObjectNodeId); + + // If the ref counted system is enabled, then return the node via the InputNodeHandle, otherwise return the InputObjectNodeId. + int32 GetInputObjectNodeId() const; + + bool GetImportAsReference() const { return CachedInputSettings.bImportAsReference; }; + + bool GetImportAsReferenceRotScaleEnabled() const { return CachedInputSettings.bImportAsReferenceRotScaleEnabled; }; + + bool GetImportAsReferenceBboxEnabled() const { return CachedInputSettings.bImportAsReferenceBboxEnabled; }; + + bool GetImportAsReferenceMaterialEnabled() const { return CachedInputSettings.bImportAsReferenceMaterialEnabled; }; + + const TArray& GetMaterialReferences(); + + // Formats Input Reference path strings obtained by UObject::GetFullName() of format: + //`Material /path/to/asset` and adds single quotes: `Material'/path/to/asset'` + static FString FormatAssetReference(FString AssetReference); + +#if WITH_EDITOR + void SwitchUniformScaleLock() { bUniformScaleLocked = !bUniformScaleLocked; }; + bool IsUniformScaleLocked() const { return bUniformScaleLocked; }; + + void PostEditUndo() override; +#endif + + virtual UHoudiniInputObject* DuplicateAndCopyState(UObject* DestOuter); + virtual void CopyStateFrom(UHoudiniInputObject* InInput, bool bCopyAllProperties); + + // Set whether this object can delete Houdini nodes. + virtual void SetCanDeleteHoudiniNodes(bool bInCanDeleteNodes); + bool CanDeleteHoudiniNodes() const { return bCanDeleteHoudiniNodes; } + + // If true then this input object uses the InputNodeHandle instead of InputNodeId and InputObjectNodeId if the + // ref counted input system is enabled. + void SetInputNodeHandleOverridesNodeIds(bool bInInputNodeHandleOverridesNodeIds) { bInputNodeHandleOverridesNodeIds = bInInputNodeHandleOverridesNodeIds; } + bool InputNodeHandleOverridesNodeIds() const { return bInputNodeHandleOverridesNodeIds; } + + FGuid GetInputGuid() const { return Guid; } + + /** + * Populate OutChangedObjects with any output objects (this object and its children if it has any) that has changed + * or have invalid HAPI node ids. + * Any objects that have not changed and have valid HAPI node ids have their node ids added to + * OutNodeIdsOfUnchangedValidObjects. + * + * @return true if this object, or any of its child objects, were added to OutChangedObjects. + */ + virtual bool GetChangedObjectsAndValidNodes(TArray& OutChangedObjects, TArray& OutNodeIdsOfUnchangedValidObjects); + +protected: + + /** + * Returns true if this object type uses an input object (OBJ) node. This is not a recursive check, for objects + * with child objects (such as an actor with components), each child input object must be checked as well. + */ + virtual bool UsesInputObjectNode() const { return true; } + + virtual void BeginDestroy() override; + + // If this type of input object has materials, this function updates its list of string material references. + void UpdateMaterialReferences(); + +public: + + // The object referenced by this input + // This property should be protected. Don't access this directly. Use GetObject() / Update() instead. + UPROPERTY() + TSoftObjectPtr InputObject; + + // The type of Object this input refers to + UPROPERTY() + EHoudiniInputObjectType Type; + + // Guid that uniquely identifies this input object. + // Also useful to correlate inputs between blueprint component templates and instances. + UPROPERTY(DuplicateTransient) + FGuid Guid; + + // Handle to the entry that this input object uses in the new ref counted input system. + FUnrealObjectInputHandle InputNodeHandle; + +protected: + + // Indicates this input object has changed + UPROPERTY(DuplicateTransient) + bool bHasChanged; + + // Indicates this input object should trigger an input update/cook + UPROPERTY(DuplicateTransient) + bool bNeedsToTriggerUpdate; + + // Indicates that this input transform should be updated + UPROPERTY(DuplicateTransient) + bool bTransformChanged; + + // String References to the materials on the input object. + // This is used when sending input by reference to Houdini. + // These strings are in the form of: Material'/path/to/reference' + UPROPERTY() + TArray MaterialReferences; + + // Indicates if change the scale of Transfrom Offset of this object uniformly +#if WITH_EDITORONLY_DATA + UPROPERTY(Transient, DuplicateTransient, NonTransactional) + bool bUniformScaleLocked; +#endif + + // The settings cached from the Input at the last Update() call. + UPROPERTY() + FHoudiniInputObjectSettings CachedInputSettings; + + UPROPERTY() + bool bCanDeleteHoudiniNodes; + + // If true then this input object uses the InputNodeHandle instead of InputNodeId and InputObjectNodeId if the + // ref counted input system is enabled. + UPROPERTY() + bool bInputNodeHandleOverridesNodeIds; + + // The object's transform/transform offset + UPROPERTY() + FTransform Transform; + +public: + // While the Transform property represents the transform, we need to keep a human readable version of the Rotation + // ie. not just a Quaternion, or the UI becomes non-sensical to humans, since converting too and from Roll,Pitch,Yaw + // and Quaternion ends up with different results. + UPROPERTY() + FRotator UserInputRotator; + +private: + // This input object's "main" (SOP) NodeId + UPROPERTY(Transient, DuplicateTransient, NonTransactional) + int32 InputNodeId; + + // This input object's "container" (OBJ) NodeId + UPROPERTY(Transient, DuplicateTransient, NonTransactional) + int32 InputObjectNodeId; +}; + + +//----------------------------------------------------------------------------------------------------------------------------- +// UStaticMesh input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputStaticMesh : public UHoudiniInputObject +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // Nothing to add for Static Meshes? + + // StaticMesh accessor + virtual class UStaticMesh* GetStaticMesh() const; +}; + + + +//----------------------------------------------------------------------------------------------------------------------------- +// USkeletalMesh input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputSkeletalMesh : public UHoudiniInputObject +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // Nothing to add for SkeletalMesh Meshes? + + // StaticMesh accessor + class USkeletalMesh* GetSkeletalMesh(); +}; + +//----------------------------------------------------------------------------------------------------------------------------- +// UAnimSequence input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputAnimation : public UHoudiniInputObject +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // + virtual void Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // Nothing to add for SkeletalMesh Meshes? + + // StaticMesh accessor + class UAnimSequence* GetAnimation(); +}; + + + +//----------------------------------------------------------------------------------------------------------------------------- +// UGeometryCollection input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputGeometryCollection : public UHoudiniInputObject +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // GeometryCollection accessor + class UGeometryCollection* GetGeometryCollection(); +}; + + + + +//----------------------------------------------------------------------------------------------------------------------------- +// USceneComponent input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputSceneComponent : public UHoudiniInputObject +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor=nullptr); + + // + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + virtual void UpdateTransform(); + + // Get the cached world transform of this scene component relative to the cached transform of its owner actor. + FTransform GetTransformRelativeToOwner() const { return Transform.GetRelativeTransform(ActorTransform); } + + // SceneComponent accessor + class USceneComponent* GetSceneComponent(); + + // Returns true if the attached actor's (parent) transform has been modified + virtual bool HasActorTransformChanged() const; + + // Returns true if the attached component's transform has been modified + virtual bool HasComponentTransformChanged() const; + + // Return true if the component itself has been modified + virtual bool HasComponentChanged(const FHoudiniInputObjectSettings& InSettings) const; + + virtual FTransform GetHoudiniObjectTransform() const override; + + UHoudiniInputActor* GetParentInputActor() const { return ParentInputActor; } + +protected: + void SetParentInputActor(UHoudiniInputActor* InParentInputActor) { ParentInputActor = InParentInputActor; } + +public: + + // This component's parent Actor transform + UPROPERTY() + FTransform ActorTransform = FTransform::Identity; + +private: + + // The input object of the parent actor of the component + UPROPERTY() + TObjectPtr ParentInputActor; +}; + + + +//----------------------------------------------------------------------------------------------------------------------------- +// UStaticMeshComponent input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputMeshComponent : public UHoudiniInputSceneComponent +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor=nullptr); + + // + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // StaticMeshComponent accessor + UStaticMeshComponent* GetStaticMeshComponent(); + + // Get the referenced StaticMesh + UStaticMesh* GetStaticMesh(); + + // Returns true if the attached component's materials have been modified + bool HasComponentMaterialsChanged() const; + + // Return true if SMC's static mesh has been modified + virtual bool HasComponentChanged(const FHoudiniInputObjectSettings& InSettings) const override; + +public: + + // Keep track of the selected Static Mesh + UPROPERTY() + TSoftObjectPtr StaticMesh = nullptr; +}; + + + + +//----------------------------------------------------------------------------------------------------------------------------- +// UInstancedStaticMeshComponent input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputInstancedMeshComponent : public UHoudiniInputMeshComponent +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor=nullptr); + + // + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // InstancedStaticMeshComponent accessor + UInstancedStaticMeshComponent* GetInstancedStaticMeshComponent(); + + // Returns true if the instances have changed + bool HasInstancesChanged() const; + + // Returns true if the attached component's transform has been modified + virtual bool HasComponentTransformChanged() const override; + +public: + + // Array of transform for this ISMC's instances + UPROPERTY() + TArray InstanceTransforms; +}; + + + + +//----------------------------------------------------------------------------------------------------------------------------- +// USplineComponent input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputSplineComponent : public UHoudiniInputSceneComponent +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor=nullptr); + + // + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // USplineComponent accessor + USplineComponent* GetSplineComponent(); + + // Return true if the component itself has been modified + virtual bool HasComponentChanged(const FHoudiniInputObjectSettings& InSettings) const override; + +public: + + // Number of CVs used by the spline component, used to detect modification + UPROPERTY() + int32 NumberOfSplineControlPoints = -1; + + // Spline Length, used for fast detection of modifications of the spline.. + UPROPERTY() + float SplineLength = -1.0f; + + // Spline resolution used to generate the asset, used to detect setting modification + UPROPERTY() + float SplineResolution = -1.0f; + + // Is the spline closed? + UPROPERTY() + bool SplineClosed = false; + + // Transforms of each of the spline's control points + UPROPERTY() + TArray SplineControlPoints; +}; + + +//----------------------------------------------------------------------------------------------------------------------------- +// UGeometryCollectionComponent input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputGeometryCollectionComponent : public UHoudiniInputSceneComponent +{ + GENERATED_UCLASS_BODY() + +public: + // + static UHoudiniInputObject* Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor=nullptr); + + // + virtual void Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // GeometryCollection accessor + class UGeometryCollectionComponent* GetGeometryCollectionComponent(); + class UGeometryCollection* GetGeometryCollection(); +}; + +//----------------------------------------------------------------------------------------------------------------------------- +// UHoudiniInputSkeletalMeshComponent input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputSkeletalMeshComponent : public UHoudiniInputSceneComponent +{ + GENERATED_UCLASS_BODY() + +public: + // + static UHoudiniInputObject* Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor=nullptr); + + // + virtual void Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // Skeletal Mesh accessor + class USkeletalMeshComponent* GetSkeletalMeshComponent(); + class USkeletalMesh* GetSkeletalMesh(); +}; + +//----------------------------------------------------------------------------------------------------------------------------- +// UHoudiniSplineComponent input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputHoudiniSplineComponent : public UHoudiniInputObject +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + virtual UObject* GetObject() const override; + + virtual void MarkChanged(const bool& bInChanged) override; + + virtual void SetNeedsToTriggerUpdate(const bool& bInTriggersUpdate) override; + + // Indicates if this input has changed and should be updated + virtual bool HasChanged() const override; + + // Indicates if this input needs to trigger an update + virtual bool NeedsToTriggerUpdate() const override; + + // UHoudiniSplineComponent accessor + UHoudiniSplineComponent* GetCurveComponent() const; + +public: + + // The type of curve (polygon, NURBS, bezier) + UPROPERTY() + EHoudiniCurveType CurveType = EHoudiniCurveType::Polygon; + + // The curve's method (CVs, Breakpoint, Freehand) + UPROPERTY() + EHoudiniCurveMethod CurveMethod = EHoudiniCurveMethod::CVs; + + UPROPERTY() + bool Reversed = false; + + +protected: + + // NOTE: We are using this reference to the component since the component, for now, + // lives on the same actor as this input object. If we use a Soft Object Reference instead the editor + // will complain about breaking references everytime we try to delete the actor. + UPROPERTY(Instanced) + TObjectPtr CachedComponent; +}; + + + +//----------------------------------------------------------------------------------------------------------------------------- +// UCameraComponent input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputCameraComponent : public UHoudiniInputSceneComponent +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor=nullptr); + + // + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // UCameraComponent accessor + UCameraComponent* GetCameraComponent(); + + // Return true if SMC's static mesh has been modified + virtual bool HasComponentChanged(const FHoudiniInputObjectSettings& InSettings) const override; + +public: + + UPROPERTY() + float FOV; + + UPROPERTY() + float AspectRatio; + + UPROPERTY() + //TEnumAsByte ProjectionType; + bool bIsOrthographic; + + UPROPERTY() + float OrthoWidth; + UPROPERTY() + float OrthoNearClipPlane; + UPROPERTY() + float OrthoFarClipPlane; + +}; + + +//----------------------------------------------------------------------------------------------------------------------------- +// UHoudiniAssetComponent input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputHoudiniAsset : public UHoudiniInputObject +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // Getter for the asset Id of the tracked HAC. Updated via Update(). + int32 GetNodeId() const { return NodeId; } + + // Cookable accessor + UHoudiniCookable* GetHoudiniCookable(); + + // UHoudiniAssetComponent accessor + // TODO: Legacy - replaced by GetCookable + UHoudiniAssetComponent* GetHoudiniAssetComponent(); +public: + + // The output index of the node that we want to use as input + UPROPERTY() + int32 OutputIndex; + +protected: + + // The node ID recorded at the last Update(). + UPROPERTY() + int32 NodeId; + +}; + + + +//----------------------------------------------------------------------------------------------------------------------------- +// AActor input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputActor : public UHoudiniInputObject +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // Check whether the actor transform, or any of its components have transform changes. + virtual bool HasActorTransformChanged() const; + + // Indicates this object is dirty and should be updated + virtual void MarkChanged(const bool& bInChanged) override; + + // GUID for temp merged SM of all spline mesh components of this actor (if any) + FGuid GetSplinesMeshPackageGuid() const { return GeneratedSplinesMeshPackageGuid; } + + // Getter for temp merged SM of all spline mesh components of this actor (if any) + UStaticMesh* GetGeneratedSplineMesh() const { return GeneratedSplinesMesh; } + + // Setter for temp merged SM of all spline mesh components of this actor (if any) + void SetGeneratedSplineMesh(UStaticMesh* const InSM) { GeneratedSplinesMesh = InSM; } + +protected: + virtual bool HasRootComponentTransformChanged() const; + virtual bool HasComponentsTransformChanged() const; + +public: + // + virtual bool ShouldTrackComponent(UActorComponent const* InComponent, const FHoudiniInputObjectSettings* InSettings=nullptr) const; + + // Return true if any content of this actor has possibly changed (for example geometry edits on a + // Brush or changes on procedurally generated content). + // NOTE: This is more generally applicable and could be moved to the HoudiniInputObject class. + virtual bool HasContentChanged(const FHoudiniInputObjectSettings& InSettings) const; + + // AActor accessor + AActor* GetActor() const; + + const TArray& GetActorComponents() const { return ActorComponents; } + + // The number of components added with the last call to Update + int32 GetLastUpdateNumComponentsAdded() const { return LastUpdateNumComponentsAdded; } + // The number of components remove with the last call to Update + int32 GetLastUpdateNumComponentsRemoved() const { return LastUpdateNumComponentsRemoved; } + + /** + * Populate OutChangedObjects with any output objects (this object and its children if it has any) that has changed + * or have invalid HAPI node ids. + * Any objects that have not changed and have valid HAPI node is have their node ids added to + * OutNodeIdsOfUnchangedValidObjects. + * + * @return true if this object, or any of its child objects, were added to OutChangedObjects. + */ + virtual bool GetChangedObjectsAndValidNodes(TArray& OutChangedObjects, TArray& OutNodeIdsOfUnchangedValidObjects) override; + + // Returns the number of spline mesh components after the last update. + int32 GetNumSplineMeshComponents() const { return NumSplineMeshComponents; } + + bool UsedMergeSplinesMeshAtLastTranslate() const { return bUsedMergeSplinesMeshAtLastTranslate; } + void SetUsedMergeSplinesMeshAtLastTranslate(bool bInValue) { bUsedMergeSplinesMeshAtLastTranslate = bInValue; } + + virtual void InvalidateData() override; + + virtual void InvalidateSplinesMeshData(); + + UPROPERTY() + int32 SplinesMeshObjectNodeId; + + UPROPERTY() + int32 SplinesMeshNodeId; + + FUnrealObjectInputHandle SplinesMeshInputNodeHandle; + +protected: + + virtual bool UsesInputObjectNode() const override; + + // The actor's components that can be sent as inputs + UPROPERTY() + TArray> ActorComponents; + + // The USceneComponents the actor had the last time we called Update (matches the ones in ActorComponents). + UPROPERTY() + TSet> ActorSceneComponents; + + // The number of components added with the last call to Update + UPROPERTY() + int32 LastUpdateNumComponentsAdded; + + // The number of components remove with the last call to Update + UPROPERTY() + int32 LastUpdateNumComponentsRemoved; + + // The number of spline mesh components in ActorComponents the last time this object was updated. + UPROPERTY() + int32 NumSplineMeshComponents; + + // Package GUID for temp static mesh for the merged spline mesh components (if any). + UPROPERTY() + FGuid GeneratedSplinesMeshPackageGuid; + + // The merged static mesh generated for the spline mesh components. + UPROPERTY() + TObjectPtr GeneratedSplinesMesh; + + // True if the merged spline mesh was sent at the last translation. + UPROPERTY() + bool bUsedMergeSplinesMeshAtLastTranslate; +}; + + +//----------------------------------------------------------------------------------------------------------------------------- +// ALevelInstance input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputLevelInstance: public UHoudiniInputActor +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // + + virtual void Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) override; + + virtual bool HasContentChanged(const FHoudiniInputObjectSettings& InSettings) const override; + + virtual bool HasTransformChanged() const override; + + ALevelInstance* GetLevelInstance() const; + + virtual bool ShouldTrackComponent(UActorComponent const* InComponent, const FHoudiniInputObjectSettings* InSettings) const override { return false; } + + const TMap, TObjectPtr>& GetTrackedActorObjects() const { return TrackedActorObjects; } + + virtual void InvalidateData() override; + +private: + UPROPERTY() + TMap, TObjectPtr> TrackedActorObjects; + + UPROPERTY() + int32 NumActorsAddedLastUpdate; + + UPROPERTY() + int32 NumActorsRemovedLastUpdate; + +}; + + +//----------------------------------------------------------------------------------------------------------------------------- +// ALandscapeProxy input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputLandscape : public UHoudiniInputActor +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // + + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + virtual bool HasActorTransformChanged() const override; + + virtual bool ShouldTrackComponent(UActorComponent const* InComponent, const FHoudiniInputObjectSettings* InSettings=nullptr) const override; + + virtual void MarkTransformChanged(const bool bInChanged) override; + + virtual bool HasContentChanged(const FHoudiniInputObjectSettings& InSettings) const override; + + virtual FTransform GetHoudiniObjectTransform() const override; + + // ALandscapeProxy accessor + ALandscapeProxy* GetLandscapeProxy() const; + + void SetLandscapeProxy(UObject* InLandscapeProxy); + + // The number of landscape components that was processed. If this count changes, .e.g, levels have been + // loaded / unloaded then the input content has changed. + UPROPERTY() + int32 CachedNumLandscapeComponents; + +protected: + virtual bool UsesInputObjectNode() const override { return true; } + + // Count the number of landscape components that are currently registered with LandscapeInfo, i.e., loaded into + // the current world. + virtual int32 CountLandscapeComponents() const; +}; + + + +//----------------------------------------------------------------------------------------------------------------------------- +// ABrush input +//----------------------------------------------------------------------------------------------------------------------------- +// Cache info for a brush in order to determine whether it has changed. + +#define BRUSH_HASH_SURFACE_PROPERTIES 0 + +USTRUCT() +struct FHoudiniBrushInfo +{ + GENERATED_BODY() + + UPROPERTY() + TWeakObjectPtr BrushActor; + UPROPERTY() + FTransform CachedTransform; + UPROPERTY() + FVector CachedOrigin; + UPROPERTY() + FVector CachedExtent; + UPROPERTY() + TEnumAsByte CachedBrushType; + + UPROPERTY() + uint64 CachedSurfaceHash; + + bool HasChanged() const; + + static int32 GetNumVertexIndicesFromModel(const UModel* Model); + + FHoudiniBrushInfo(); + FHoudiniBrushInfo(ABrush* InBrushActor); + + template + inline void HashCombine(uint64& s, const T & v) const + { + //std::hash h; + s^= ::GetTypeHash(v) + 0x9e3779b9 + (s<< 6) + (s>> 2); + } + + inline void HashCombine(uint64& s, const FVector3f & V) const + { + HashCombine(s, V.X); + HashCombine(s, V.Y); + HashCombine(s, V.Z); + } + + inline void CombinePolyHash(uint64& Hash, const FPoly& Poly) const + { + HashCombine(Hash, Poly.Base); + HashCombine(Hash, Poly.TextureU); + HashCombine(Hash, Poly.TextureV); + HashCombine(Hash, Poly.Normal); + // Do not add addresses to the hash, otherwise it would force a recook every unreal session! + // HashCombine(Hash, (uint64)(Poly.Material)); + } +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputBrush : public UHoudiniInputActor +{ + GENERATED_BODY() + +public: + + UHoudiniInputBrush(); + + // Factory function + static UHoudiniInputBrush* Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + //---------------------------------------------------------------------- + // UHoudiniInputActor Interface - Begin + //---------------------------------------------------------------------- + + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // Indicates if this input has changed and should be updated + virtual bool HasContentChanged(const FHoudiniInputObjectSettings& InSettings) const override; + + // Indicates if this input has changed and should be updated + virtual bool HasChanged() const override { return (!bIgnoreInputObject) && bHasChanged; }; + + // Indicates if this input has changed and should be updated + virtual bool HasTransformChanged() const override { return (!bIgnoreInputObject) && bTransformChanged; }; + + virtual bool HasActorTransformChanged() const override; + + virtual bool NeedsToTriggerUpdate() const override { return (!bIgnoreInputObject) && bNeedsToTriggerUpdate; }; + + //---------------------------------------------------------------------- + // UHoudiniInputActor Interface - End + //---------------------------------------------------------------------- + + + // ABrush accessor + ABrush* GetBrush() const; + + UModel* GetCachedModel() const; + + // Check whether any of the brushes, or their transforms, used to generate this model have changed. + bool HasBrushesChanged(const TArray& InBrushes) const; + + // Cache the combined model as well as the input brushes. + void UpdateCachedData(UModel* InCombinedModel, const TArray& InBrushes); + + // Returns whether this input object should be ignored when uploading objects to Houdini. + // This mechanism could be implemented on UHoudiniInputObject. + bool ShouldIgnoreThisInput(); + + + // Find only the subtractive brush actors that intersect with the InputObject (Brush actor) bounding box but + // excluding any selector bounds actors. + static bool FindIntersectingSubtractiveBrushes(const UHoudiniInputBrush* InputBrush, TArray& OutBrushes); + +protected: + virtual bool UsesInputObjectNode() const override { return true; } + + UPROPERTY() + TArray BrushesInfo; + + UPROPERTY(Transient, DuplicateTransient) + TObjectPtr CombinedModel; + + UPROPERTY() + bool bIgnoreInputObject; + + UPROPERTY() + TEnumAsByte CachedInputBrushType; +}; + +//----------------------------------------------------------------------------------------------------------------------------- +// UPCGData input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputPCGData : public UHoudiniInputObject +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // DataTable accessor + + UHoudiniPCGDataCollection* GetPCGData() const; +}; + +//----------------------------------------------------------------------------------------------------------------------------- +// UDataTable input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputDataTable : public UHoudiniInputObject +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // DataTable accessor + class UDataTable* GetDataTable() const; +}; + +//----------------------------------------------------------------------------------------------------------------------------- +// UFoliageType_InstancedStaticMesh input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputFoliageType_InstancedStaticMesh : public UHoudiniInputStaticMesh +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // UHoudiniInputObject overrides + + // + virtual void Update(UObject * InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // StaticMesh accessor + virtual class UStaticMesh* GetStaticMesh() const override; +}; + +//----------------------------------------------------------------------------------------------------------------------------- +// Blueprint input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputBlueprint : public UHoudiniInputObject +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // + virtual void Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) override; + + virtual bool HasComponentsTransformChanged() const; + +public: + + // Return true if any content of this actor has possibly changed (for example geometry edits on a + // Brush or changes on procedurally generated content). + // NOTE: This is more generally applicable and could be moved to the HoudiniInputObject class. + virtual bool HasContentChanged(const FHoudiniInputObjectSettings& InSettings) const; + + // UBlueprint accessor + UBlueprint* GetBlueprint() const; + + const TArray& GetComponents() const { return BPComponents; } + + // The number of components added with the last call to Update + int32 GetLastUpdateNumComponentsAdded() const { return LastUpdateNumComponentsAdded; } + // The number of components remove with the last call to Update + int32 GetLastUpdateNumComponentsRemoved() const { return LastUpdateNumComponentsRemoved; } + + /** + * Populate OutChangedObjects with any output objects (this object and its children if it has any) that has changed + * or have invalid HAPI node ids. + * Any objects that have not changed and have valid HAPI node is have their node ids added to + * OutNodeIdsOfUnchangedValidObjects. + * + * @return true if this object, or any of its child objects, were added to OutChangedObjects. + */ + virtual bool GetChangedObjectsAndValidNodes(TArray& OutChangedObjects, TArray& OutNodeIdsOfUnchangedValidObjects) override; + + virtual void InvalidateData() override; + +protected: + + virtual bool UsesInputObjectNode() const override; + + // The BP's components that can be sent as inputs + UPROPERTY() + TArray> BPComponents; + + // The USceneComponents the BP had the last time we called Update (matches the ones in BPComponents). + UPROPERTY() + TSet> BPSceneComponents; + + // The number of components added with the last call to Update + UPROPERTY() + int32 LastUpdateNumComponentsAdded; + + // The number of components remove with the last call to Update + UPROPERTY() + int32 LastUpdateNumComponentsRemoved; +}; + +//----------------------------------------------------------------------------------------------------------------------------- +// APackedLevelActor input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputPackedLevelActor: public UHoudiniInputActor +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // + + virtual void Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) override; + + virtual bool HasContentChanged(const FHoudiniInputObjectSettings& InSettings) const override; + + APackedLevelActor* GetPackedLevelActor() const; + + virtual bool ShouldTrackComponent(UActorComponent const* InComponent, const FHoudiniInputObjectSettings* InSettings) const override; + + UHoudiniInputBlueprint* GetBlueprintInputObject() const { return BlueprintInputObject; } + + virtual void InvalidateData() override; + +private: + UPROPERTY() + TObjectPtr BlueprintInputObject; + +}; + +//----------------------------------------------------------------------------------------------------------------------------- +// ALandscapeSplinesActor input +//----------------------------------------------------------------------------------------------------------------------------- + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputLandscapeSplineActor : public UHoudiniInputActor +{ + GENERATED_UCLASS_BODY() + +public: + // + static UHoudiniInputObject* Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // ULandscapeSplinesComponent accessor + class ALandscapeSplineActor* GetLandscapeSplineActor() const; + + virtual void Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) override; + + /** + * Returns true if InComponent (a component of the input actor) should be tracked by the input system. + * @param InComponent The component to check for tracking. + * @return true if InComponent should be tracked. + */ + virtual bool ShouldTrackComponent(UActorComponent const* InComponent, const FHoudiniInputObjectSettings* InSettings=nullptr) const override; +}; + +//----------------------------------------------------------------------------------------------------------------------------- +// ULandscapeSplinesComponent input +//----------------------------------------------------------------------------------------------------------------------------- + +/** + * Struct for caching landscape spline control points. ULandscapeSplineControlPoint cannot be duplicated with + * an outer other than ULandscapeSplinesComponent. * + */ +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniLandscapeSplineControlPointData +{ + GENERATED_BODY(); + + FHoudiniLandscapeSplineControlPointData(); + + // Copied properties from ULandscapeSplineControlPoint for which we want to track changes + + /** Location in Landscape-space */ + UPROPERTY() + FVector Location; + + /** Rotation of tangent vector at this point (in landscape-space) */ + UPROPERTY() + FRotator Rotation; + + /** Half-Width of the spline at this point. */ + UPROPERTY() + float Width; + +#if WITH_EDITORONLY_DATA + /** Vertical offset of the spline segment mesh. Useful for a river's surface, among other things. */ + UPROPERTY() + float SegmentMeshOffset; + + /** + * Name of blend layer to paint when applying spline to landscape + * If "none", no layer is painted + */ + UPROPERTY() + FName LayerName; + + /** If the spline is above the terrain, whether to raise the terrain up to the level of the spline when applying it to the landscape. */ + UPROPERTY() + uint32 bRaiseTerrain:1; + + /** If the spline is below the terrain, whether to lower the terrain down to the level of the spline when applying it to the landscape. */ + UPROPERTY() + uint32 bLowerTerrain:1; + + /** Mesh to use on the control point */ + UPROPERTY() + TObjectPtr Mesh; + + /** Overrides mesh's materials */ + UPROPERTY() + TArray> MaterialOverrides; + + /** Scale of the control point mesh */ + UPROPERTY() + FVector MeshScale; +#endif + +}; + +/** + * Struct for caching landscape spline control points. ULandscapeSplineSegment cannot be duplicated with + * an outer other than ULandscapeSplinesComponent. * + */ +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniLandscapeSplineSegmentData +{ + GENERATED_BODY(); + + FHoudiniLandscapeSplineSegmentData(); + + // Copied properties from ULandscapeSplineSegment for which we want to track changes + +#if WITH_EDITORONLY_DATA + /** + * Name of blend layer to paint when applying spline to landscape + * If "none", no layer is painted + */ + UPROPERTY() + FName LayerName; + + /** If the spline is above the terrain, whether to raise the terrain up to the level of the spline when applying it to the landscape. */ + UPROPERTY() + uint32 bRaiseTerrain:1; + + /** If the spline is below the terrain, whether to lower the terrain down to the level of the spline when applying it to the landscape. */ + UPROPERTY() + uint32 bLowerTerrain:1; + + /** Spline meshes from this list are used in random order along the spline. */ + UPROPERTY() + TArray SplineMeshes; +#endif + +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputLandscapeSplinesComponent : public UHoudiniInputSceneComponent +{ + GENERATED_UCLASS_BODY() + +public: + // + static UHoudiniInputObject* Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor=nullptr); + + // + virtual void Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // ULandscapeSplinesComponent accessor + class ULandscapeSplinesComponent* GetLandscapeSplinesComponent() const; + + virtual bool HasComponentChanged(const FHoudiniInputObjectSettings& InSettings) const override; + + const TArray& GetCachedControlPoints() const { return CachedControlPoints; } + + const TArray& GetCachedSegments() const { return CachedSegments; } + + const TMap, int32>& GetControlPointIdMap() const { return ControlPointIdMap; } + void SetControlPointIdMap(const TMap, int32>& InControlPointIdMap) { ControlPointIdMap = InControlPointIdMap; } + void SetControlPointIdMap(TMap, int32>&& InControlPointIdMap) { ControlPointIdMap = InControlPointIdMap; } + + int32 GetNextControlPointId() const { return NextControlPointId; } + void SetNextControlPointId(int32 InNextControlPointId) { NextControlPointId = InNextControlPointId; } + +protected: + /** A copy of the control points of the spline the last time this object was updated. */ + UPROPERTY() + TArray CachedControlPoints; + + /** A copy of the segments of the landscape spline the last time this was object was updated. */ + UPROPERTY() + TArray CachedSegments; + + /** The generated ids for the control points from the last time this object was updated. */ + UPROPERTY() + TMap, int32> ControlPointIdMap; + + /** The id to assign the next new control point we encounter. */ + UPROPERTY() + int32 NextControlPointId; + +}; + +//----------------------------------------------------------------------------------------------------------------------------- +// USplineMeshComponent input +//----------------------------------------------------------------------------------------------------------------------------- + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputSplineMeshComponent : public UHoudiniInputMeshComponent +{ + GENERATED_UCLASS_BODY() + +public: + // + static UHoudiniInputObject* Create(UObject* InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings, UHoudiniInputActor* InParentInputActor=nullptr); + + // + virtual void Update(UObject* InObject, const FHoudiniInputObjectSettings& InSettings) override; + + // USplineMeshComponent accessor + class USplineMeshComponent* GetSplineMeshComponent() const; + + virtual bool HasComponentChanged(const FHoudiniInputObjectSettings& InSettings) const override; + + const FGuid& GetMeshPackageGuid() const { return MeshPackageGuid; } + + TObjectPtr GetGeneratedMesh() { return GeneratedMesh; } + + void SetGeneratedMesh(UStaticMesh* const InMesh) { GeneratedMesh = InMesh; } + +protected: + UPROPERTY() + FGuid MeshPackageGuid; + + UPROPERTY() + TObjectPtr GeneratedMesh; + + UPROPERTY() + TEnumAsByte CachedForwardAxis; + + UPROPERTY() + FSplineMeshParams CachedSplineParams; + + UPROPERTY() + FVector CachedSplineUpDir; + + UPROPERTY() + float CachedSplineBoundaryMax; + + UPROPERTY() + float CachedSplineBoundaryMin; + + UPROPERTY() + uint8 CachedbSmoothInterpRollScale:1; +}; + +//----------------------------------------------------------------------------------------------------------------------------- +// UTexture2D input +//----------------------------------------------------------------------------------------------------------------------------- +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniInputTexture : public UHoudiniInputObject +{ + GENERATED_UCLASS_BODY() + +public: + + // + static UHoudiniInputObject* Create(UObject * InObject, UObject* InOuter, const FString& InName, const FHoudiniInputObjectSettings& InInputSettings); + + // Texture2D accessor + class UTexture2D* GetTexture() const; +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniInputTypes.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include "HoudiniInputTypes.h" +#include "HoudiniEngineRuntimeCommon.h" +#include "HoudiniInput.h" +#include "HoudiniRuntimeSettings.h" + + +FHoudiniInputObjectSettings::FHoudiniInputObjectSettings() + : KeepWorldTransform(EHoudiniXformType::Auto) + , bImportAsReference(false) + , bImportAsReferenceRotScaleEnabled(true) + , bImportAsReferenceBboxEnabled(true) + , bImportAsReferenceMaterialEnabled(true) + , bExportMainGeometry(true) + , bExportLODs(false) + , bExportSockets(false) + , bPreferNaniteFallbackMesh(false) + , bExportColliders(false) + , bExportMaterialParameters(false) + , bAddRotAndScaleAttributesOnCurves(false) + , bUseLegacyInputCurves(false) + , UnrealSplineResolution(0.0f) + , LandscapeExportType(EHoudiniLandscapeExportType::Heightfield) + , bLandscapeExportSelectionOnly(false) + , bLandscapeAutoSelectComponent(false) + , bLandscapeExportMaterials(false) + , bLandscapeExportLighting(false) + , bLandscapeExportNormalizedUVs(false) + , bLandscapeExportTileUVs(false) + , bLandscapeAutoSelectSplines(false) + , bLandscapeSplinesExportControlPoints(false) + , bLandscapeSplinesExportLeftRightCurves(false) + , bLandscapeSplinesExportSplineMeshComponents(false) + , bMergeSplineMeshComponents(true) + , bExportHeightDataPerEditLayer(true) + , bExportPaintLayersPerEditLayer(false) + , bExportMergedPaintLayers(true) + , bExportLevelInstanceContent(true) +{ + UHoudiniRuntimeSettings const* const HoudiniRuntimeSettings = GetDefault(); + if (IsValid(HoudiniRuntimeSettings)) + { + UnrealSplineResolution = HoudiniRuntimeSettings->MarshallingSplineResolution; + bAddRotAndScaleAttributesOnCurves = HoudiniRuntimeSettings->bAddRotAndScaleAttributesOnCurves; + bUseLegacyInputCurves = HoudiniRuntimeSettings->bUseLegacyInputCurves; + bPreferNaniteFallbackMesh = HoudiniRuntimeSettings->bPreferNaniteFallbackMesh; + } +} + +FHoudiniInputObjectSettings::FHoudiniInputObjectSettings(UHoudiniInput const* InInput) + : FHoudiniInputObjectSettings() +{ + InInput->CopyInputSettingsTo(*this); +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniInputTypes.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/ObjectMacros.h" +#include "HoudiniEngineRuntimeCommon.h" + +#include "HoudiniInputTypes.generated.h" + +// Forward declarations +class UHoudiniInput; + +// Maintain an iterable list of houdini input types +static const EHoudiniInputType HoudiniInputTypeList[] = +{ + EHoudiniInputType::Geometry, + EHoudiniInputType::Curve, + EHoudiniInputType::World +}; + +UENUM() +enum class EHoudiniXformType : uint8 +{ + None, + IntoThisObject, + Auto +}; + +// Struct of input settings that affect object exporting to Houdini +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniInputObjectSettings +{ + GENERATED_BODY(); + +public: + FHoudiniInputObjectSettings(); + + // Construct from a HoudiniInput. Copies all supported properties. + FHoudiniInputObjectSettings(UHoudiniInput const* InInput); + + UPROPERTY() + EHoudiniXformType KeepWorldTransform; + + // Indicates that all the input objects are imported to Houdini as references instead of actual geo + // (for Geo/World/Asset input types only) + UPROPERTY() + bool bImportAsReference; + + // Indicates that whether or not to add the rot / scale attributes for reference imports + UPROPERTY() + bool bImportAsReferenceRotScaleEnabled; + + // Indicates whether or not to add bbox attributes for reference imports + UPROPERTY() + bool bImportAsReferenceBboxEnabled; + + // Indicates whether or not to add material attributes for reference imports + UPROPERTY() + bool bImportAsReferenceMaterialEnabled; + + // Indicates that we want to export the main mesh for a given object + // Turning this off will allow to only send colliders for example + UPROPERTY() + bool bExportMainGeometry; + + // Indicates that all LODs in the input should be marshalled to Houdini + UPROPERTY() + bool bExportLODs; + + // Indicates that all sockets in the input should be marshalled to Houdini + UPROPERTY() + bool bExportSockets; + + // Override property for preferring the Nanite fallback mesh when using a Nanite geometry as input + UPROPERTY() + bool bPreferNaniteFallbackMesh; + + // Indicates that all colliders in the input should be marshalled to Houdini + UPROPERTY() + bool bExportColliders; + + // Indicates that material parameters should be exported as attributes + UPROPERTY() + bool bExportMaterialParameters; + + // Set this to true to add rot and scale attributes on curve inputs. + UPROPERTY() + bool bAddRotAndScaleAttributesOnCurves; + + // Set this to true to use legacy (curve::1.0) input curves + UPROPERTY() + bool bUseLegacyInputCurves; + + // Resolution used when converting unreal splines to houdini curves + UPROPERTY() + float UnrealSplineResolution; + + // Indicates if the landscape should be exported as heightfield, mesh or points + UPROPERTY() + EHoudiniLandscapeExportType LandscapeExportType; + + // Is set to true when landscape input is set to selection only. + UPROPERTY() + bool bLandscapeExportSelectionOnly; + + // Is set to true when the automatic selection of landscape component is active + UPROPERTY() + bool bLandscapeAutoSelectComponent; + + // Is set to true when materials are to be exported. + UPROPERTY() + bool bLandscapeExportMaterials; + + // Is set to true when lightmap information export is desired. + UPROPERTY() + bool bLandscapeExportLighting; + + // Is set to true when uvs should be exported in [0,1] space. + UPROPERTY() + bool bLandscapeExportNormalizedUVs; + + // Is set to true when uvs should be exported for each tile separately. + UPROPERTY() + bool bLandscapeExportTileUVs; + + // If true, also export a landscape's splines + UPROPERTY() + bool bLandscapeAutoSelectSplines; + + // If true, send a separate control point cloud of the landscape splines control points. + UPROPERTY() + bool bLandscapeSplinesExportControlPoints; + + // If true, export left and right curves as well + UPROPERTY() + bool bLandscapeSplinesExportLeftRightCurves; + + // If true, export the spline mesh components of landscape splines + UPROPERTY() + bool bLandscapeSplinesExportSplineMeshComponents; + + // If true, the deformed meshes of all spline mesh components of an actor are merged into temporary input mesh. + // If false, the meshes are sent individually. + UPROPERTY() + bool bMergeSplineMeshComponents; + + // If enabled, height data is exported per Edit Layer. + UPROPERTY() + bool bExportHeightDataPerEditLayer; + + // Export paint layers. + UPROPERTY() + bool bExportPaintLayersPerEditLayer; + + // Export paint layers. + UPROPERTY() + bool bExportMergedPaintLayers; + + // If enabled, level instances (and packed level actor) content is exported vs just exporting a single point + // with attributes identifying the level instance / packed level actor. + UPROPERTY() + bool bExportLevelInstanceContent; + +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniInstancedActorComponent.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniInstancedActorComponent.h" + +#include "HoudiniRuntimeSettings.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniPluginSerializationVersion.h" + +#include "Components/InstancedStaticMeshComponent.h" +#include "Components/HierarchicalInstancedStaticMeshComponent.h" +#include "Engine/Light.h" +#include "Engine/World.h" +#include "Internationalization/Internationalization.h" +#include "Runtime/Launch/Resources/Version.h" +#include "Serialization/CustomVersion.h" +#include "UObject/DevObjectVersion.h" +#include "UObject/ObjectResource.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + #include "UObject/Linker.h" +#endif + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + +UHoudiniInstancedActorComponent::UHoudiniInstancedActorComponent(const FObjectInitializer& ObjectInitializer) +: Super(ObjectInitializer) +, InstancedObject(nullptr) +, InstancedActorClass(nullptr) +{ + // + // Set default component properties. + // + Mobility = EComponentMobility::Static; + bCanEverAffectNavigation = true; + bNeverNeedsRenderUpdate = false; + Bounds = FBox(ForceInitToZero); +} + + +void +UHoudiniInstancedActorComponent::Serialize(FArchive& Ar) +{ + int64 InitialOffset = Ar.Tell(); + + bool bLegacyComponent = false; + if (Ar.IsLoading()) + { + int32 Ver = Ar.CustomVer(FHoudiniCustomSerializationVersion::GUID); + if (Ver < VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_V2_BASE && Ver >= VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_BASE) + { + bLegacyComponent = true; + } + } + + if (bLegacyComponent) + { + // Legacy serialization + HOUDINI_LOG_WARNING(TEXT("Loading deprecated version of UHoudiniInstancedActorComponent : serialization will be skipped.")); + + Super::Serialize(Ar); + + // Skip v1 Serialized data + if (FLinker* Linker = Ar.GetLinker()) + { + int32 const ExportIndex = this->GetLinkerIndex(); + FObjectExport& Export = Linker->ExportMap[ExportIndex]; + Ar.Seek(InitialOffset + Export.SerialSize); + return; + } + } + else + { + // Normal v2 serialization + Super::Serialize(Ar); + } +} + + +void UHoudiniInstancedActorComponent::OnComponentDestroyed( bool bDestroyingHierarchy ) +{ + ClearAllInstances(); + Super::OnComponentDestroyed( bDestroyingHierarchy ); +} + + +void +UHoudiniInstancedActorComponent::AddReferencedObjects(UObject * InThis, FReferenceCollector & Collector ) +{ + Super::AddReferencedObjects(InThis, Collector); + + UHoudiniInstancedActorComponent * ThisHIAC = Cast< UHoudiniInstancedActorComponent >(InThis); + if ( IsValid(ThisHIAC) ) + { + if ( IsValid(ThisHIAC->InstancedObject) ) + Collector.AddReferencedObject( +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + ObjectPtrWrap(ThisHIAC->InstancedObject), +#else + ThisHIAC->InstancedObject, +#endif + ThisHIAC ); + + Collector.AddReferencedObjects( +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + ObjectPtrWrap(ThisHIAC->InstancedActors), +#else + ThisHIAC->InstancedActors, +#endif + ThisHIAC ); + } +} + + +void +UHoudiniInstancedActorComponent::SetInstancedObject(class UObject* InObject) +{ + if (InObject == InstancedObject) + return; + + InstancedObject = InObject; + InstancedActorClass = nullptr; +} + + +int32 +UHoudiniInstancedActorComponent::AddInstance(const FTransform& InstanceTransform, AActor * NewActor) +{ + if (!IsValid(NewActor)) + return -1; + + NewActor->AttachToComponent(this, FAttachmentTransformRules::KeepRelativeTransform); + NewActor->SetActorRelativeTransform(InstanceTransform); + return InstancedActors.Add(NewActor); +} + + +bool +UHoudiniInstancedActorComponent::SetInstanceAt(const int32& Idx, const FTransform& InstanceTransform, AActor * NewActor) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(UHoudiniInstancedActorComponent::SetInstanceAt); + if (!IsValid(NewActor)) + return false; + + if (!InstancedActors.IsValidIndex(Idx)) + return false; + + NewActor->AttachToComponent(this, FAttachmentTransformRules::KeepRelativeTransform); + NewActor->SetActorRelativeTransform(InstanceTransform); + NewActor->RegisterAllComponents(); + InstancedActors[Idx] = NewActor; + + return true; +} + + +bool +UHoudiniInstancedActorComponent::SetInstanceTransformAt(const int32& Idx, const FTransform& InstanceTransform) +{ + if (!InstancedActors.IsValidIndex(Idx)) + return false; + + InstancedActors[Idx]->AttachToComponent(this, FAttachmentTransformRules::KeepRelativeTransform); + InstancedActors[Idx]->SetActorRelativeTransform(InstanceTransform); + + return true; +} + + +void +UHoudiniInstancedActorComponent::ClearAllInstances() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(UHoudiniInstancedActorComponent::ClearAllInstances); + + for(AActor* Instance : InstancedActors) + { + if(IsValid(Instance) && IsValid(Instance->GetWorld())) + { + // Lights can take a relatively long time to destroy their lighting caches. Oddly, + // setting to moveable prevents this. + ALight* Light = Cast(Instance); + if (IsValid(Light)) + Light->SetMobility(EComponentMobility::Movable); + + Instance->GetWorld()->DestroyActor(Instance); + } + + } + + InstancedActors.Empty(); +} + + +void +UHoudiniInstancedActorComponent::SetNumberOfInstances(const int32& NewInstanceNum) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(UHoudiniInstancedActorComponent::SetNumberOfInstances); + int32 OldInstanceNum = InstancedActors.Num(); + + // If we want less instances than we already have, destroy the extra properly + if (NewInstanceNum < OldInstanceNum) + { + for (int32 Idx = NewInstanceNum - 1; Idx < InstancedActors.Num(); Idx++) + { + AActor* Instance = InstancedActors.IsValidIndex(Idx) ? InstancedActors[Idx] : nullptr; + if (IsValid(Instance)) + { + UWorld* const World = Instance->GetWorld(); + if (IsValid(World)) + World->DestroyActor(Instance); + } + } + } + + // Grow the array with nulls if needed + InstancedActors.SetNumZeroed(NewInstanceNum); +} + + +void +UHoudiniInstancedActorComponent::OnComponentCreated() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(UHoudiniInstancedActorComponent::OnComponentCreated); + + Super::OnComponentCreated(); + + // If our instances are parented to another actor we should duplicate them + bool bNeedDuplicate = false; + for (auto CurrentInstance : InstancedActors) + { + if ( !IsValid(CurrentInstance) ) + continue; + + if ( CurrentInstance->GetAttachParentActor() != GetOwner() ) + bNeedDuplicate = true; + } + + if ( !bNeedDuplicate ) + return; + + // TODO: CHECK ME! + // We need to duplicate our instances + TArray SourceInstances = InstancedActors; + InstancedActors.Empty(); + for (AActor* CurrentInstance : SourceInstances) + { + if ( !IsValid(CurrentInstance) ) + continue; + + FTransform InstanceTransform; + if ( CurrentInstance->GetRootComponent() ) + InstanceTransform = CurrentInstance->GetRootComponent()->GetRelativeTransform(); + + // AddInstance( InstanceTransform ); + } +} + +#undef LOCTEXT_NAMESPACE +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniInstancedActorComponent.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "Components/SceneComponent.h" + +#include "HoudiniInstancedActorComponent.generated.h" + + +UCLASS()//( config = Engine ) +class HOUDINIENGINERUNTIME_API UHoudiniInstancedActorComponent : public USceneComponent +{ + GENERATED_UCLASS_BODY() + + friend class UHoudiniInstancedActorComponent_V1; + friend class FHoudiniEditorEquivalenceUtils; + + public: + + virtual void Serialize(FArchive & Ar) override; + + virtual void OnComponentCreated() override; + virtual void OnComponentDestroyed( bool bDestroyingHierarchy ) override; + + static void AddReferencedObjects( UObject * InThis, FReferenceCollector & Collector ); + + // Object mutator + void SetInstancedObject(class UObject* InObject); + void SetInstancedActorClass(class UClass* InClass) { InstancedActorClass = InClass; } + // Object accessor + class UObject* GetInstancedObject() const { return InstancedObject; } + class UClass* GetInstancedActorClass() const { return InstancedActorClass; } + + // Instance Accessor + TArray>& GetInstancedActorsForWrite() { return InstancedActors; } + // const Instance accessor + const TArray>& GetInstancedActors() const { return InstancedActors; } + + // Returns the instanced actor at a given index + AActor* GetInstancedActorAt(const int32& Idx) { return InstancedActors.IsValidIndex(Idx) ? InstancedActors[Idx] : nullptr; } + + // Add an instance to this component. Transform is given in local space of this component. + int32 AddInstance(const FTransform& InstanceTransform, AActor * NewActor); + + // Sets the instance at a given index in this component. Transform is given in local space of this component. + bool SetInstanceAt(const int32& Idx, const FTransform& InstanceTransform, AActor * NewActor); + + // Updates the transform for a given actor. Transform is given in local space of this component. + bool SetInstanceTransformAt(const int32& Idx, const FTransform& InstanceTransform); + + // Destroy all existing instances + void ClearAllInstances(); + + // Sets the number of instances needed + // Properly deletes extras, new instance actors are nulled + void SetNumberOfInstances(const int32& NewInstanceNum); + + // Set the instances. Transforms are given in local space of this component. + bool SetInstanceTransforms(const TArray& InstanceTransforms); + + private: + + UPROPERTY(VisibleAnywhere, Category = Instances ) + TObjectPtr InstancedObject; + + UPROPERTY() + TObjectPtr InstancedActorClass; + + UPROPERTY(VisibleInstanceOnly, Category = Instances ) + TArray> InstancedActors; + +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniLandscapeRuntimeUtils.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniLandscapeRuntimeUtils.h" +#include "HoudiniOutput.h" +#include "LandscapeEdit.h" +#include "HoudiniAssetComponent.h" +#include "Landscape.h" +#include "HoudiniAsset.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "PhysicalMaterials/PhysicalMaterial.h" +#include "LandscapeStreamingProxy.h" +#include "LandscapeSplineActor.h" +#include "LandscapeSplinesComponent.h" +#include "LandscapeSplineControlPoint.h" +#include "LandscapeSplineSegment.h" +#include "Runtime/Launch/Resources/Version.h" + +void +FHoudiniLandscapeRuntimeUtils::DeleteLandscapeCookedData(UHoudiniOutput* InOutput) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniLandscapeRuntimeUtils::DeleteLandscapeCookedData); + + TSet LandscapesToDelete; + TArray OutputObjectsToDelete; + TMap& OutputObjects = InOutput->GetOutputObjects(); + for (auto& OutputObjectPair : OutputObjects) + { + const FHoudiniOutputObject& PrevObj = OutputObjectPair.Value; + if (IsValid(PrevObj.OutputObject) && PrevObj.OutputObject->IsA()) + { + // Get the layer data stored during cooking + UHoudiniLandscapeTargetLayerOutput* OldLayer = Cast(PrevObj.OutputObject); + + OutputObjectsToDelete.Add(OutputObjectPair.Key); + + if (IsValid(OldLayer->Landscape)) + { + // Delete the edit layers + if (OldLayer->BakedEditLayer != OldLayer->CookedEditLayer) + { + DeleteEditLayer(OldLayer->Landscape, FName(OldLayer->CookedEditLayer)); + } + + // Remove any landscapes that were created. + if (OldLayer->bCreatedLandscape && OldLayer->Landscape) + LandscapesToDelete.Add(OldLayer->Landscape); + } + + PrevObj.OutputObject->ConditionalBeginDestroy(); + } + } + + // Dont delete everything as we need to keep previous existing output data for the other output objectss! + // This prevents reusing the data/components/objects from previous cooks + //InOutput->GetOutputObjects().Empty(); + + // Just delete the output objects we marked as to be deleted + for (auto OutputObjectIdentifierToDelete : OutputObjectsToDelete) + { + OutputObjects.Remove(OutputObjectIdentifierToDelete); + } + + for (ALandscape* Landscape : LandscapesToDelete) + DestroyLandscape(Landscape); +} + +void FHoudiniLandscapeRuntimeUtils::DeleteEditLayer(ALandscape* Landscape, const FName& LayerName) +{ +#if WITH_EDITOR + if(!IsValid(Landscape)) + return; + + int32 EditLayerIndex = Landscape->GetLayerIndex(LayerName); + if (EditLayerIndex == INDEX_NONE) + return; + Landscape->DeleteLayer(EditLayerIndex); +#endif +} + + +void +FHoudiniLandscapeRuntimeUtils::DestroyLandscape(ALandscape* Landscape) +{ + if (!IsValid(Landscape)) + return; + + ULandscapeInfo* Info = Landscape->GetLandscapeInfo(); + if (!IsValid(Info)) + return; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + TArray> Proxies = Info->GetSortedStreamingProxies(); + for (auto ProxyPtr : Proxies) + { + ALandscapeStreamingProxy* Proxy = ProxyPtr.Get(); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + TArray> Proxies = Info->StreamingProxies; + for (auto ProxyPtr : Proxies) + { + ALandscapeStreamingProxy* Proxy = ProxyPtr.Get(); +#else + TArray Proxies = Info->Proxies; + for (ALandscapeStreamingProxy* Proxy : Proxies) + { +#endif + if (!IsValid(Proxy)) + continue; + + Info->UnregisterActor(Proxy); + FHoudiniLandscapeRuntimeUtils::DestroyLandscapeProxy(Proxy); + } + Landscape->Destroy(); +} + +void +FHoudiniLandscapeRuntimeUtils::DestroyLandscapeProxy(ALandscapeProxy* Proxy) +{ + // UE5 does not automatically delete streaming proxies, so clean them up before detroying the parent actor. + // Note the Proxies array must be copied, not referenced, as it is modified when each Proxy is destroyed. + +#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 7 + TArray> Proxies = Proxy->GetLandscapeInfo()->GetSortedStreamingProxies(); +#elif ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1 + TArray> Proxies = Proxy->GetLandscapeInfo()->StreamingProxies; +#else + TArray> Proxies = Proxy->GetLandscapeInfo()->Proxies; +#endif + + for (TWeakObjectPtr Child : Proxies) + { + if (Child.Get() != nullptr && IsValid(Child.Get())) + Child.Get()->Destroy(); + } + + // Once we've removed the streaming proxies delete the landscape actor itself. + + Proxy->Destroy(); +} + + +bool +FHoudiniLandscapeRuntimeUtils::DestroyLandscapeSplinesControlPoint( + ULandscapeSplineControlPoint* InControlPoint, + const bool bInDestroyOnlyIfUnused, + const bool bInRemoveDestroyedControlPointFromComponent) +{ + if (!IsValid(InControlPoint)) + return false; + +#if ENGINE_MAJOR_VERSION < 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0) + HOUDINI_LOG_WARNING(TEXT("Landscape Spline Output is only supported in UE5.1+")); + return false; +#else +#if WITH_EDITOR + InControlPoint->DeleteSplinePoints(); +#endif + + // Remove the point from connected segments + for (FLandscapeSplineConnection& Connection : InControlPoint->ConnectedSegments) + { + if (!IsValid(Connection.Segment)) + continue; + + if (bInDestroyOnlyIfUnused) + return false; + +#if WITH_EDITOR + Connection.Segment->DeleteSplinePoints(); +#endif + + // Get the control point at the *other* end of the segment and remove it from it + ULandscapeSplineControlPoint* OtherEnd = Connection.GetFarConnection().ControlPoint; + if (!IsValid(OtherEnd)) + continue; + + OtherEnd->ConnectedSegments.Remove(FLandscapeSplineConnection(Connection.Segment, 1 - Connection.End)); +#if WITH_EDITOR + OtherEnd->UpdateSplinePoints(); +#endif + } + + InControlPoint->ConnectedSegments.Empty(); + + if (bInRemoveDestroyedControlPointFromComponent) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 7 + ULandscapeSplinesComponent* const SplinesComponent = InControlPoint->GetOuterULandscapeSplinesComponent(); +#else +#if WITH_EDITOR + ULandscapeSplinesComponent* const SplinesComponent = InControlPoint->GetOuterSafe(); +#else + ULandscapeSplinesComponent* const SplinesComponent = nullptr; +#endif +#endif + if (IsValid(SplinesComponent)) + SplinesComponent->GetControlPoints().Remove(InControlPoint); + } + + return true; +#endif +} + +bool +FHoudiniLandscapeRuntimeUtils::DestroyLandscapeSplinesSegment( + ULandscapeSplineSegment* InSegment, + const bool bInRemoveSegmentFromComponent, + const bool bInDestroyUnusedControlPoints, + const bool bInRemoveDestroyedControlPointsFromComponent, + TArray* const RemainingControlPoints) +{ + if (!IsValid(InSegment)) + return false; + +#if ENGINE_MAJOR_VERSION < 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0) + HOUDINI_LOG_WARNING(TEXT("Landscape Spline Output is only supported in UE5.1+")); + return false; +#else +#if WITH_EDITOR + InSegment->DeleteSplinePoints(); +#endif + + ULandscapeSplineControlPoint* CP0 = InSegment->Connections[0].ControlPoint; + ULandscapeSplineControlPoint* CP1 = InSegment->Connections[1].ControlPoint; + const bool bCP0Valid = IsValid(CP0); + const bool bCP1Valid = IsValid(CP1); + if (bCP0Valid) + CP0->ConnectedSegments.Remove(FLandscapeSplineConnection(InSegment, 0)); + if (bCP1Valid) + CP1->ConnectedSegments.Remove(FLandscapeSplineConnection(InSegment, 1)); + +#if WITH_EDITOR + if (bCP0Valid) + CP0->UpdateSplinePoints(); + if (bCP1Valid) + CP1->UpdateSplinePoints(); +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 7 + ULandscapeSplinesComponent* const SplinesComponent = InSegment->GetOuterULandscapeSplinesComponent(); +#else + #if WITH_EDITOR + ULandscapeSplinesComponent* const SplinesComponent = InSegment->GetOuterSafe(); + #else + ULandscapeSplinesComponent* const SplinesComponent = nullptr; + #endif +#endif + + const bool bIsSplinesComponentValid = IsValid(SplinesComponent); + + if (bInDestroyUnusedControlPoints) + { + if (CP0->ConnectedSegments.Num() == 0) + { +#if WITH_EDITOR + CP0->DeleteSplinePoints(); +#endif + CP0->ConnectedSegments.Empty(); + if (bInRemoveDestroyedControlPointsFromComponent && bIsSplinesComponentValid) + SplinesComponent->GetControlPoints().Remove(CP0); + CP0 = nullptr; + } + if (CP1->ConnectedSegments.Num() == 0) + { +#if WITH_EDITOR + CP1->DeleteSplinePoints(); +#endif + CP1->ConnectedSegments.Empty(); + if (bInRemoveDestroyedControlPointsFromComponent && bIsSplinesComponentValid) + SplinesComponent->GetControlPoints().Remove(CP1); + CP1 = nullptr; + } + } + + if (RemainingControlPoints) + { + if (CP0) + RemainingControlPoints->Add(CP0); + if (CP1) + RemainingControlPoints->Add(CP1); + } + + if (bInRemoveSegmentFromComponent && bIsSplinesComponentValid) + SplinesComponent->GetSegments().Remove(InSegment); + + return true; +#endif +} + +bool +FHoudiniLandscapeRuntimeUtils::DestroyLandscapeSplinesSegmentsAndControlPoints(ULandscapeSplinesComponent* const InSplinesComponent) +{ +#if ENGINE_MAJOR_VERSION < 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0) + return false; +#else + if (!IsValid(InSplinesComponent)) + return false; + + TArray>& Segments = InSplinesComponent->GetSegments(); + TArray>& ControlPoints = InSplinesComponent->GetControlPoints(); + + bool bSuccess = true; + for (ULandscapeSplineSegment* const Segment : Segments) + { + if (!IsValid(Segment)) + continue; + + // We don't have to remove the segment from the component here, or destroy the control points, since we'll + // empty component's Segments array and destroy all control points below + static constexpr bool bRemoveSegmentFromComponent = false; + static constexpr bool bDestroyUnusedControlPoints = false; + static constexpr bool bRemoveDestroyedControlPointsFromComponent = false; + if (!DestroyLandscapeSplinesSegment( + Segment, + bRemoveSegmentFromComponent, + bDestroyUnusedControlPoints, + bRemoveDestroyedControlPointsFromComponent)) + { + bSuccess = false; + } + } + Segments.Empty(); + + for (ULandscapeSplineControlPoint* const ControlPoint : ControlPoints) + { + if (!IsValid(ControlPoint)) + continue; + + // We don't have to remove the control points from the component here as we'll empty component's ControlPoints + // array below. + static constexpr bool bDestroyOnlyIfUnused = false; + static constexpr bool bRemoveControlPointFromComponent = false; + if (!DestroyLandscapeSplinesControlPoint(ControlPoint, bDestroyOnlyIfUnused, bRemoveControlPointFromComponent)) + bSuccess = false; + } + ControlPoints.Empty(); + + return bSuccess; +#endif +} + +bool +FHoudiniLandscapeRuntimeUtils::DestroyLandscapeSplinesSegmentsAndControlPoints(UHoudiniLandscapeSplinesOutput* const InOutputObject) +{ + + if (!IsValid(InOutputObject)) + return false; + + bool bSuccess = true; + for (ULandscapeSplineSegment* const Segment : InOutputObject->GetSegments()) + { + if (!IsValid(Segment)) + continue; + static constexpr bool bRemoveSegmentFromComponent = true; + static constexpr bool bDestroyUnusedControlPoints = false; + static constexpr bool bRemoveDestroyedControlPointsFromComponent = false; + if (!DestroyLandscapeSplinesSegment( + Segment, + bRemoveSegmentFromComponent, + bDestroyUnusedControlPoints, + bRemoveDestroyedControlPointsFromComponent)) + { + bSuccess = false; + } + } + InOutputObject->GetSegments().Empty(); + + for (ULandscapeSplineControlPoint* const ControlPoint : InOutputObject->GetControlPoints()) + { + if (!IsValid(ControlPoint)) + continue; + + static constexpr bool bDestroyOnlyIfUnused = true; + static constexpr bool bRemoveControlPointFromComponent = true; + if (!DestroyLandscapeSplinesControlPoint(ControlPoint, bDestroyOnlyIfUnused, bRemoveControlPointFromComponent)) + bSuccess = false; + } + InOutputObject->GetControlPoints().Empty(); + + // Return false if we had any failures + return bSuccess; +} + + +void +FHoudiniLandscapeRuntimeUtils::DeleteLandscapeSplineCookedData(UHoudiniOutput* const InOutput) +{ + if (!IsValid(InOutput)) + return; + + TArray OutputObjectsToDelete; + TMap& OutputObjects = InOutput->GetOutputObjects(); + for (auto& OutputObjectPair : OutputObjects) + { + const FHoudiniOutputObject& PrevObj = OutputObjectPair.Value; + if (!IsValid(PrevObj.OutputObject) || !PrevObj.OutputObject->IsA()) + continue; + + // Get the data stored during cooking + UHoudiniLandscapeSplinesOutput* const OldOutputObject = Cast(PrevObj.OutputObject); + OldOutputObject->Clear(); + + OutputObjectsToDelete.Add(OutputObjectPair.Key); + + OldOutputObject->ConditionalBeginDestroy(); + } + + // Delete the output objects we marked as to be deleted + for (const auto& OutputObjectIdentifierToDelete : OutputObjectsToDelete) + { + OutputObjects.Remove(OutputObjectIdentifierToDelete); + } +} + + +int32 +FHoudiniLandscapeRuntimeUtils::GetOrGenerateValidControlPointId( +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + TObjectPtr& InControlPoint, +#else + ULandscapeSplineControlPoint const* const InControlPoint, +#endif + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId) +{ + // Look for the id of ControlPoint in the map +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + const TSoftObjectPtr ControlPointPtr(InControlPoint.Get()); +#else + const TSoftObjectPtr ControlPointPtr(InControlPoint); +#endif + int32 ControlPointId = INDEX_NONE; + if (int32 const* const ControlPointIdPtr = InControlPointIdMap.Find(ControlPointPtr)) + ControlPointId = *ControlPointIdPtr; + + if (ControlPointId < 0) + { + // Control point does not have a valid id, generate one + ControlPointId = InNextControlPointId; + InNextControlPointId++; + + // Add / update the ControlPoint's id to / in the map + InControlPointIdMap.Add(ControlPointPtr, ControlPointId); + } + + return ControlPointId; +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniLandscapeRuntimeUtils.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreTypes.h" +#include "UObject/Object.h" +#include "UObject/ObjectMacros.h" +#include "Misc/Optional.h" + +#include "HoudiniInputTypes.h" +#include "HoudiniInputObject.h" + +#include "GameFramework/Actor.h" +#include "LandscapeProxy.h" + +class UHoudiniOutput; +class ULandscapeSplinesComponent; +class ULandscapeSplineControlPoint; +class ULandscapeSplineSegment; +class ALandscape; + +class UHoudiniLandscapeSplinesOutput; + +struct HOUDINIENGINERUNTIME_API FHoudiniLandscapeRuntimeUtils +{ + static void DeleteLandscapeCookedData(UHoudiniOutput* Output); + + static void DeleteEditLayer(ALandscape* Landscape, const FName& LayerName); + + static void DestroyLandscape(ALandscape* Landscape); + + static void DestroyLandscapeProxy(ALandscapeProxy* Proxy); + + // ------------------ + // Landscape splines + // ------------------ + + /** + * Destroy a landscape spline control point, optionally only if it + * is unused. The interpolated spline points associated with the + * point is deleted and it is removed from segment connections. The + * connected segments' interpolated spline points are also removed. + * + * Removing the control point from its spline component can be deferred. This + * can be useful in bulk delete situations where we want to cleanup the + * interpolated data and segment connections first and then clear the + * control point / segment arrays on the owning splines component in one go. + * + * @param InControlPoint The control point to destroy. + * @param bInDestroyOnlyIfUnused Defaults to true. If this is true, then if + * the control point has one or more valid segment connections the point + * and its associated spline data is not destroyed, and the function + * returns false. + * @param bInRemoveDestroyedControlPointFromComponent Defaults to True. + * If false, then we do all the cleanup of segment connections and + * interpolated spline points, but InControlPoint is not removed + * from its splines component. It is expected to be removed by the caller + * during, for example, a bulk delete. The function returns true. + + * @return true if the control point was destroyed (even if it was technically + * not removed from its owning splines component). + */ + static bool DestroyLandscapeSplinesControlPoint( + ULandscapeSplineControlPoint* InControlPoint, + bool bInDestroyOnlyIfUnused = true, + bool bInRemoveDestroyedControlPointFromComponent = true); + + /** + * Destroy a landscape spline segment, and optionally all of its control + * points that are unused after removal from the segment. The interpolated + * spline points associated with the segment are deleted and it is removed + * from segment connections with control points. The + * connected control points' interpolated spline points are also removed. + * + * Removing the segment from its spline component can be deferred. This + * can be useful in bulk delete situations where we want to cleanup the + * interpolated data and segment connections first and then clear the + * control point / segment arrays on the owning splines component in one go. + * + * @param InSegment The segment to destroy. + * @param bInRemoveSegmentFromComponent Defaults to true. If false this + * function continues normally but does not remove the segment from its + * owning component. It is expected that the caller will remove the segment + * from the component, during a bulk delete for example. + * @param bInDestroyOnlyUnusedControlPoints Defaults to true. If this is + * true, then if a control point has no more valid segment connections + * left after removal from InSegment then it is also destroyed. + * @param bInRemoveDestroyedControlPointsFromComponent Defaults to True. + * If true, then any control points that are destroyed in this function + * as a result of bInDestroyUnusedControlPoints, will also be removed + * from their owning splines component. This could be set to false in + * bulk deletion scenarios where the caller will remove all segments/ + * control points from the splines component. + * + * @return true if the segment was destroyed (even if it was technically + * not removed from its owning splines component). + */ + static bool DestroyLandscapeSplinesSegment( + ULandscapeSplineSegment* InSegment, + bool bInRemoveSegmentFromComponent = true, + bool bInDestroyUnusedControlPoints = true, + bool bInRemoveDestroyedControlPointsFromComponent = true, + TArray* RemainingControlPoints = nullptr); + + static bool DestroyLandscapeSplinesSegmentsAndControlPoints(ULandscapeSplinesComponent* InSplinesComponent); + + static bool DestroyLandscapeSplinesSegmentsAndControlPoints(UHoudiniLandscapeSplinesOutput* InOutputObject); + + static void DeleteLandscapeSplineCookedData(UHoudiniOutput* InOutput); + + /** + * Get or generate a control point id. The ids are stored in InControlPointIdMap and InNextControlPointId is the + * next valid id we can use for a newly seen control point. + * @param InControlPoint The control point to get the id for. + * @param InControlPointIdMap The map to get the id from. If no valid id for control point in the map, generates one + * via InNextControlPointId and add it to the map. + * @param InNextControlPointId The next valid control point id. Updated if we needed to generate a new id for + * InControlPoint. + * @return The control point id. + */ + static int32 GetOrGenerateValidControlPointId( +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + TObjectPtr& InControlPoint, +#else + ULandscapeSplineControlPoint const* const InControlPoint, +#endif + TMap, int32>& InControlPointIdMap, + int32& InNextControlPointId); +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniMaterialMap.cpp`: + +```cpp +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniMaterialMap.h`: + +```h +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniNodeSyncComponent.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniNodeSyncComponent.h" + +UHoudiniNodeSyncComponent::UHoudiniNodeSyncComponent(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + // Not needed? + //bForceNeedUpdate_DEPRECATED = false; + + // AssetState will be updated by changes to the HoudiniAsset + // or parameter changes on the Component template. + SetHoudiniAssetState(EHoudiniAssetState::None); + SetHasBeenLoaded(false); + + // Disable proxy mesh by default (unsupported for now) + SetOverrideGlobalProxyStaticMeshSettings(true); + SetEnableProxyStaticMeshOverride(false); + SetEnableProxyStaticMeshRefinementByTimerOverride(false); + SetEnableProxyStaticMeshRefinementOnPreSaveWorldOverride(false); + SetEnableProxyStaticMeshRefinementOnPreBeginPIEOverride(false); + + // Set default mobility to Movable + Mobility = EComponentMobility::Movable; + + bLiveSyncEnabled = true; + FetchStatus = EHoudiniNodeSyncStatus::None; + FetchMessage = FString(); +} + + +bool +UHoudiniNodeSyncComponent::IsValidComponent() const +{ + return true; +} + +FString +UHoudiniNodeSyncComponent::GetHoudiniAssetName() const +{ + if (FetchNodePath.IsEmpty()) + return FString(); + + // Extract the node name from our fetch node path + int32 FoundPos = -1; + if (FetchNodePath.FindLastChar(TEXT('/'), FoundPos)) + { + return FetchNodePath.RightChop(FoundPos + 1); + } + else + { + return FetchNodePath; + } +} +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniNodeSyncComponent.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniAssetComponent.h" + +#include "CoreMinimal.h" + +#include "HoudiniNodeSyncComponent.generated.h" + + +UENUM() +enum class EHoudiniNodeSyncStatus : uint8 +{ + + None, // Fetch/Send not used yet + Failed, // Last operation failed + Success, // Last operation was successful + SuccessWithErrors, // Last operation was successful, but reported errors + Running, // Sending/Fetching + Warning // Display a warning +}; + + +UCLASS(ClassGroup = (Rendering, Common), hidecategories = (Object, Activation, "Components|Activation", HoudiniAsset), ShowCategories = (Mobility), editinlinenew) +class HOUDINIENGINERUNTIME_API UHoudiniNodeSyncComponent : public UHoudiniAssetComponent +{ + GENERATED_BODY() + +public: + UHoudiniNodeSyncComponent(const FObjectInitializer & ObjectInitializer); + + virtual bool IsValidComponent() const override; + + virtual FString GetHoudiniAssetName() const override; + + void SetHoudiniAssetState(EHoudiniAssetState InNewState) { SetAssetState(InNewState); }; + + void SetFetchNodePath(const FString& InNodePath) { FetchNodePath = InNodePath; }; + FString GetFetchNodePath() { return FetchNodePath; }; + + void SetLiveSyncEnabled(const bool& bEnableLiveSync) { bLiveSyncEnabled = bEnableLiveSync; }; + bool GetLiveSyncEnabled() { return bLiveSyncEnabled; }; + + // Whether or not this component should be able to delete the Houdini nodes + // that correspond to the HoudiniAsset when being deregistered. + // Node Sync component shall NOT delete nodes! + virtual bool CanDeleteHoudiniNodes() const { return false; } + + // + // Public Members + // + + // Last status + EHoudiniNodeSyncStatus FetchStatus; + // Last Fetch message + FString FetchMessage; + +protected: + + UPROPERTY() + FString FetchNodePath; + + UPROPERTY() + bool bLiveSyncEnabled; +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniOutput.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniOutput.h" + +#include "HoudiniAssetComponent.h" +#include "HoudiniCookable.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniLandscapeRuntimeUtils.h" +#include "HoudiniSplineComponent.h" + +#include "Components/SceneComponent.h" +#include "Components/MeshComponent.h" +#include "Components/SplineComponent.h" +#include "Engine/Blueprint.h" +#include "Engine/Engine.h" +#include "Misc/StringFormatArg.h" +#include "Landscape.h" +#include "LandscapeLayerInfoObject.h" +#include "LandscapeSplinesComponent.h" +#include "LandscapeSplineActor.h" +#include "LandscapeSplineControlPoint.h" +#include "LandscapeSplineSegment.h" +#include "Animation/Skeleton.h" +#include "Templates/Tuple.h" +#include "HoudiniFoliageUtils.h" +#include "Engine/DataTable.h" +#include "PhysicsEngine/PhysicsAsset.h" +#include "HAL/FileManager.h" +#include "Materials/Material.h" +#if defined(HOUDINI_USE_PCG) +#include "HoudiniPCGDataObject.h" +#endif + + +FHoudiniMaterialIdentifier::FHoudiniMaterialIdentifier( + const FString& InMaterialObjectPath, + const bool bInMakeMaterialInstance, + const FString& InMaterialInstanceParametersSlug) + : MaterialObjectPath(InMaterialObjectPath) + , bIsHoudiniMaterial(false) + , bMakeMaterialInstance(bInMakeMaterialInstance) + , MaterialInstanceParametersSlug(InMaterialInstanceParametersSlug) +{ +} + +FHoudiniMaterialIdentifier::FHoudiniMaterialIdentifier(const FString& InMaterialPath, const bool bInIsHoudiniMaterial) + : MaterialObjectPath(InMaterialPath) + , bIsHoudiniMaterial(bInIsHoudiniMaterial) + , bMakeMaterialInstance(false) + , MaterialInstanceParametersSlug() +{ +} + +uint32 +FHoudiniMaterialIdentifier::GetTypeHash() const +{ + // bMakeMaterialInstance is only relevant if bIsHoudiniMaterial is false + const bool bNotHoudiniMaterialAndMakeInstance = !bIsHoudiniMaterial && bMakeMaterialInstance; + const TTuple T { + MaterialObjectPath, bIsHoudiniMaterial, bNotHoudiniMaterialAndMakeInstance, bNotHoudiniMaterialAndMakeInstance ? MaterialInstanceParametersSlug : FString() }; + return ::GetTypeHash(T); +} + +bool +FHoudiniMaterialIdentifier::operator==(const FHoudiniMaterialIdentifier& InRhs) const +{ + if (MaterialObjectPath != InRhs.MaterialObjectPath || bIsHoudiniMaterial != InRhs.bIsHoudiniMaterial) + return false; + if (bIsHoudiniMaterial) + return true; + if (bMakeMaterialInstance != InRhs.bMakeMaterialInstance) + return false; + return !bMakeMaterialInstance || MaterialInstanceParametersSlug == InRhs.MaterialInstanceParametersSlug; +} + + +UHoudiniLandscapePtr::UHoudiniLandscapePtr(class FObjectInitializer const& Initializer) +{ + // bIsWorldCompositionLandscape = false; + // BakeType = EHoudiniLandscapeOutputBakeType::Detachment; +}; + + +bool +UHoudiniLandscapeSplinesOutput::GetLayerSegments(const FName InEditLayer, TArray& OutSegments) const +{ + const TObjectPtr * LayerOutputPtr = LayerOutputs.Find(InEditLayer); + if (!LayerOutputPtr) + return false; + + UHoudiniLandscapeSplineTargetLayerOutput* LayerOutput = LayerOutputPtr->Get(); + if (!IsValid(LayerOutput)) + return false; + + OutSegments = LayerOutput->Segments; + return true; +} + + +void +UHoudiniLandscapeSplinesOutput::Clear(bool bInClearTempLayers) +{ + // Delete the splines (segments and control points) + FHoudiniLandscapeRuntimeUtils::DestroyLandscapeSplinesSegmentsAndControlPoints(this); + + // Delete the edit layers + for (const auto& LayerOutputPair : LayerOutputs) + { + UHoudiniLandscapeSplineTargetLayerOutput const* const LayerOutput = LayerOutputPair.Value; + if (!IsValid(LayerOutput) || !IsValid(LayerOutput->Landscape)) + continue; + + if (bInClearTempLayers && LayerOutput->BakedEditLayer != LayerOutput->CookedEditLayer) + FHoudiniLandscapeRuntimeUtils::DeleteEditLayer(LayerOutput->Landscape, FName(LayerOutput->CookedEditLayer)); + } + + if (IsValid(LandscapeSplineActor)) + { + ULandscapeInfo* const LSInfo = LandscapeSplineActor->GetLandscapeInfo(); + if (IsValid(LSInfo)) + { +#if WITH_EDITOR + LSInfo->UnregisterSplineActor(LandscapeSplineActor); +#endif + LandscapeSplineActor->Destroy(); + } + } + + Landscape = nullptr; + LandscapeProxy = nullptr; + LandscapeSplineActor = nullptr; + LandscapeSplinesComponent = nullptr; + LayerOutputs.Empty(); + Segments.Empty(); + ControlPoints.Empty(); +} + + +uint32 +GetTypeHash(const FHoudiniOutputObjectIdentifier& HoudiniOutputObjectIdentifier) +{ + return HoudiniOutputObjectIdentifier.GetTypeHash(); +} + +// ---------------------------------------------------- +// FHoudiniOutputObjectIdentifier +// ---------------------------------------------------- + +FHoudiniOutputObjectIdentifier::FHoudiniOutputObjectIdentifier() +{ + ObjectId = -1; + GeoId = -1; + PartId = -1; + SplitIdentifier = FString(); + PartName = FString(); +} + +FHoudiniOutputObjectIdentifier::FHoudiniOutputObjectIdentifier( + int32 InObjectId, int32 InGeoId, int32 InPartId, const FString& InSplitIdentifier) +{ + ObjectId = InObjectId; + GeoId = InGeoId; + PartId = InPartId; + SplitIdentifier = InSplitIdentifier; +} + +uint32 +FHoudiniOutputObjectIdentifier::GetTypeHash() const +{ + int32 HashBuffer[3] = { ObjectId, GeoId, PartId }; + int32 Hash = FCrc::MemCrc32((void *)&HashBuffer[0], sizeof(HashBuffer)); + return FCrc::StrCrc32(*SplitIdentifier, Hash); +} + +bool +FHoudiniOutputObjectIdentifier::operator==(const FHoudiniOutputObjectIdentifier& InOutputObjectIdentifier) const +{ + // Object/Geo/Part IDs must match + bool bMatchingIds = true; + if (ObjectId != InOutputObjectIdentifier.ObjectId + || GeoId != InOutputObjectIdentifier.GeoId + || PartId != InOutputObjectIdentifier.PartId) + bMatchingIds = false; + + if ((bLoaded && !InOutputObjectIdentifier.bLoaded) + || (!bLoaded && InOutputObjectIdentifier.bLoaded)) + { + // If one of the two identifier is loaded, + // we can simply compare the part names + if (PartName.Equals(InOutputObjectIdentifier.PartName) + && SplitIdentifier.Equals(InOutputObjectIdentifier.SplitIdentifier)) + return true; + } + + if (!bMatchingIds) + { + return false; + } + + // If split ID and name match, we're equal... + if (SplitIdentifier.Equals(InOutputObjectIdentifier.SplitIdentifier)) + return true; + + // ... if not we're different + return false; +} + +bool +FHoudiniOutputObjectIdentifier::Matches(const FHoudiniGeoPartObject& InHGPO) const +{ + // Object/Geo/Part IDs must match + bool bMatchingIds = true; + if (ObjectId != InHGPO.ObjectId + || GeoId != InHGPO.GeoId + || PartId != InHGPO.PartId) + bMatchingIds = false; + + if ((bLoaded && !InHGPO.bLoaded) || (!bLoaded && InHGPO.bLoaded)) + { + // If either the HGPO or the Identifer is nmarked as loaded, + // we can simply compare the part names + if (PartName.Equals(InHGPO.PartName)) + return true; + } + + if (!bMatchingIds) + { + return false; + } + + // If the HGPO has our split identifier + //if (InHGPO.SplitGroups.Contains(SplitIdentifier)) + // return true; + + // + return true; +} + + +// ---------------------------------------------------- +// FHoudiniBakedOutputObjectIdentifier +// ---------------------------------------------------- + + +FHoudiniBakedOutputObjectIdentifier::FHoudiniBakedOutputObjectIdentifier() +{ + PartId = -1; + SplitIdentifier = FString(); +} + +FHoudiniBakedOutputObjectIdentifier::FHoudiniBakedOutputObjectIdentifier( + const int32& InPartId, const FString& InSplitIdentifier) +{ + PartId = InPartId; + SplitIdentifier = InSplitIdentifier; +} + +FHoudiniBakedOutputObjectIdentifier::FHoudiniBakedOutputObjectIdentifier(const FHoudiniOutputObjectIdentifier& InIdentifier) +{ + PartId = InIdentifier.PartId; + SplitIdentifier = InIdentifier.SplitIdentifier; +} + +uint32 +FHoudiniBakedOutputObjectIdentifier::GetTypeHash() const +{ + const int32 HashBuffer = PartId; + const int32 Hash = FCrc::MemCrc32((void *)&HashBuffer, sizeof(HashBuffer)); + return FCrc::StrCrc32(*SplitIdentifier, Hash); +} + +uint32 +GetTypeHash(const FHoudiniBakedOutputObjectIdentifier& InIdentifier) +{ + return InIdentifier.GetTypeHash(); +} + +bool +FHoudiniBakedOutputObjectIdentifier::operator==(const FHoudiniBakedOutputObjectIdentifier& InIdentifier) const +{ + return (InIdentifier.PartId == PartId && InIdentifier.SplitIdentifier.Equals(SplitIdentifier)); +} + + +// ---------------------------------------------------- +// FHoudiniBakedOutputObject +// ---------------------------------------------------- + + +FHoudiniBakedOutputObject::FHoudiniBakedOutputObject() + : Actor() + , ActorBakeName(NAME_None) + , BakedObject() + , BakedComponent() + , BakedSkeleton() +{ +} + + +FHoudiniBakedOutputObject::FHoudiniBakedOutputObject(AActor* InActor, FName InActorBakeName, UObject* InBakeObject, UObject* InBakedComponent) + : Actor(FSoftObjectPath(InActor).ToString()) + , ActorBakeName(InActorBakeName) + , BakedObject(FSoftObjectPath(InBakeObject).ToString()) + , BakedComponent(FSoftObjectPath(InBakedComponent).ToString()) + , BakedSkeleton() +{ +} + + +AActor* +FHoudiniBakedOutputObject::GetActorIfValid(bool bInTryLoad) const +{ + const FSoftObjectPath ActorPath(Actor); + + if (!ActorPath.IsValid()) + return nullptr; + + UObject* Object = ActorPath.ResolveObject(); + if (!Object && bInTryLoad) + Object = ActorPath.TryLoad(); + + if (!IsValid(Object)) + return nullptr; + + return Cast(Object); +} + +UObject* +FHoudiniBakedOutputObject::GetBakedObjectIfValid(bool bInTryLoad) const +{ + const FSoftObjectPath ObjectPath(BakedObject); + + if (!ObjectPath.IsValid()) + return nullptr; + + UObject* Object = ObjectPath.ResolveObject(); + if (!Object && bInTryLoad) + Object = ObjectPath.TryLoad(); + + if (!IsValid(Object)) + return nullptr; + + return Object; +} + +UObject* +FHoudiniBakedOutputObject::GetBakedComponentIfValid(bool bInTryLoad) const +{ + const FSoftObjectPath ComponentPath(BakedComponent); + + if (!ComponentPath.IsValid()) + return nullptr; + + UObject* Object = ComponentPath.ResolveObject(); + if (!Object && bInTryLoad) + Object = ComponentPath.TryLoad(); + + if (!IsValid(Object)) + return nullptr; + + return Object; +} + +UBlueprint* +FHoudiniBakedOutputObject::GetBlueprintIfValid(bool bInTryLoad) const +{ + const FSoftObjectPath BlueprintPath(Blueprint); + + if (!BlueprintPath.IsValid()) + return nullptr; + + UObject* Object = BlueprintPath.ResolveObject(); + if (!Object && bInTryLoad) + Object = BlueprintPath.TryLoad(); + + if (!IsValid(Object)) + return nullptr; + + return Cast(Object); +} + +ULandscapeLayerInfoObject* +FHoudiniBakedOutputObject::GetLandscapeLayerInfoIfValid(const FName& InLayerName, const bool bInTryLoad) const +{ + if (!LandscapeLayers.Contains(InLayerName)) + return nullptr; + + const FString& LayerInfoPathStr = LandscapeLayers.FindChecked(InLayerName); + const FSoftObjectPath LayerInfoPath(LayerInfoPathStr); + + if (!LayerInfoPath.IsValid()) + return nullptr; + + UObject* Object = LayerInfoPath.ResolveObject(); + if (!Object && bInTryLoad) + Object = LayerInfoPath.TryLoad(); + + if (!IsValid(Object)) + return nullptr; + + return Cast(Object); +} + +ALandscape* +FHoudiniBakedOutputObject::GetLandscapeIfValid(bool bInTryLoad) const +{ + const FSoftObjectPath LandscapePath(Landscape); + + if (!LandscapePath.IsValid()) + return nullptr; + + UObject* Object = LandscapePath.ResolveObject(); + if (!Object && bInTryLoad) + Object = LandscapePath.TryLoad(); + + if (!IsValid(Object)) + return nullptr; + + return Cast(Object); +} + +USkeleton* +FHoudiniBakedOutputObject::GetBakedSkeletonIfValid(bool bInTryLoad) const +{ + const FSoftObjectPath SkeletonPath(BakedSkeleton); + + if (!SkeletonPath.IsValid()) + return nullptr; + + UObject* Object = SkeletonPath.ResolveObject(); + if (!Object && bInTryLoad) + Object = SkeletonPath.TryLoad(); + + if (!IsValid(Object)) + return nullptr; + + return Cast(Object); +} + +UPhysicsAsset* +FHoudiniBakedOutputObject::GetBakedPhysicsAssetIfValid(bool bInTryLoad) const +{ + const FSoftObjectPath PhyscsAssetPath(BakedPhysicsAsset); + + if (!PhyscsAssetPath.IsValid()) + return nullptr; + + UObject* Object = PhyscsAssetPath.ResolveObject(); + if (!Object && bInTryLoad) + Object = PhyscsAssetPath.TryLoad(); + + if (!IsValid(Object)) + return nullptr; + + return Cast(Object); +} + +TArray +FHoudiniBakedOutputObject::GetFoliageActorsIfValid(bool bInTryLoad) const +{ + TArray ValidActors; + + for (const FString& ActorPathString : FoliageActors) + { + FSoftObjectPath ActorPath(ActorPathString); + + if (!ActorPath.IsValid()) + continue; + + UObject* ResolvedObject = ActorPath.ResolveObject(); + if (!ResolvedObject && bInTryLoad) + ResolvedObject = ActorPath.TryLoad(); + + if (!IsValid(ResolvedObject)) + continue; + + AActor* ResolvedActor = Cast(ResolvedObject); + if (ResolvedActor) + { + ValidActors.Add(ResolvedActor); + } + } + + return ValidActors; +} + +TArray FHoudiniBakedOutputObject::GetInstancedActorsIfValid(bool bInTryLoad) const +{ + TArray ValidActors; + + for (const FString& ActorPathString : InstancedActors) + { + FSoftObjectPath ActorPath(ActorPathString); + + if (!ActorPath.IsValid()) + continue; + + UObject* ResolvedObject = ActorPath.ResolveObject(); + if (!ResolvedObject && bInTryLoad) + ResolvedObject = ActorPath.TryLoad(); + + if (!IsValid(ResolvedObject)) + continue; + + AActor* ResolvedActor = Cast(ResolvedObject); + if (ResolvedActor) + { + ValidActors.Add(ResolvedActor); + } + } + + return ValidActors; +} + +UHoudiniOutput::UHoudiniOutput(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) + , Type(EHoudiniOutputType::Invalid) + , StaleCount(0) + , bCreateSceneComponents(true) + , bLandscapeWorldComposition(false) + , bIsEditableNode(false) + , bHasEditableNodeBuilt(false) + , bCanDeleteHoudiniNodes(true) +{ + +} + +UHoudiniOutput::~UHoudiniOutput() +{ + Type = EHoudiniOutputType::Invalid; + StaleCount = 0; + bIsUpdating = false; + + HoudiniGeoPartObjects.Empty(); + OutputObjects.Empty(); + InstancedOutputs.Empty(); + AssignmentMaterialsById.Empty(); + ReplacementMaterialsById.Empty(); +} + +void +UHoudiniOutput::BeginDestroy() +{ + Super::BeginDestroy(); +} + +void +UHoudiniOutput::PostLoad() +{ + Super::PostLoad(); +} + + +FBox +UHoudiniOutput::GetBounds() const +{ + FBox BoxBounds(ForceInitToZero); + + switch (GetType()) + { + case EHoudiniOutputType::Mesh: + { + for (auto & CurPair : OutputObjects) + { + const FHoudiniOutputObject& CurObj = CurPair.Value; + + if (CurObj.bProxyIsCurrent) + { + UMeshComponent* MeshComp = Cast(CurObj.ProxyComponent); + if (!IsValid(MeshComp)) + continue; + + BoxBounds += MeshComp->Bounds.GetBox(); + } + else + { + for(auto Component : CurObj.OutputComponents) + { + UMeshComponent* MeshComp = Cast(Component); + if (!IsValid(MeshComp)) + continue; + + BoxBounds += MeshComp->Bounds.GetBox(); + } + } + } + } + break; + + case EHoudiniOutputType::Landscape: + { + for (auto & CurPair : OutputObjects) + { + const FHoudiniOutputObject& CurObj = CurPair.Value; + UHoudiniLandscapePtr* CurLandscapeObj = Cast(CurObj.OutputObject); + if (!IsValid(CurLandscapeObj)) + continue; + + ALandscapeProxy* Landscape = Cast(CurLandscapeObj->GetRawPtr()); + if (!IsValid(Landscape)) + continue; + + FVector Origin, Extent; + Landscape->GetActorBounds(false, Origin, Extent); + + FBox LandscapeBounds = FBox::BuildAABB(Origin, Extent); + BoxBounds += LandscapeBounds; + } + } + break; + + case EHoudiniOutputType::Instancer: + { + for (auto & CurPair : OutputObjects) + { + const FHoudiniOutputObject& CurObj = CurPair.Value; + USceneComponent* InstancedComp = Cast(CurObj.OutputObject); + if (!IsValid(InstancedComp)) + continue; + + BoxBounds += InstancedComp->Bounds.GetBox(); + } + } + break; + + case EHoudiniOutputType::Curve: + { + for (auto & CurPair : OutputObjects) + { + const FHoudiniOutputObject& CurObj = CurPair.Value; + for(auto Component : CurObj.OutputComponents) + { + UHoudiniSplineComponent* CurHoudiniSplineComp = Cast(Component); + if (!IsValid(CurHoudiniSplineComp)) + continue; + + FBox CurCurveBound(ForceInitToZero); + for (auto & Trans : CurHoudiniSplineComp->CurvePoints) + { + CurCurveBound += Trans.GetLocation(); + } + + UHoudiniCookable* OuterHC = Cast(GetOuter()); + if (IsValid(OuterHC)) + { + USceneComponent* OuterComp = OuterHC->GetComponent(); + if (IsValid(OuterComp)) + BoxBounds += CurCurveBound.MoveTo(OuterComp->GetComponentLocation()); + } + } + } + } + break; + + case EHoudiniOutputType::LandscapeSpline: + { + for (const auto& CurPair : OutputObjects) + { + const FHoudiniOutputObject& CurObj = CurPair.Value; + ALandscapeSplineActor* CurLandscapeSpline = Cast(CurObj.OutputObject); + if (!IsValid(CurLandscapeSpline)) + continue; + + FVector Origin, Extent; + CurLandscapeSpline->GetActorBounds(false, Origin, Extent); + + FBox LandscapeBounds = FBox::BuildAABB(Origin, Extent); + BoxBounds += LandscapeBounds; + } + break; + } + + case EHoudiniOutputType::Skeletal: + case EHoudiniOutputType::Invalid: + break; + + default: + break; + } + + return BoxBounds; +} + +void +UHoudiniOutput::Clear() +{ + StaleCount = 0; + + HoudiniGeoPartObjects.Empty(); + + for (auto& CurrentOutputObject : OutputObjects) + { + for (auto Component : CurrentOutputObject.Value.OutputComponents) + { + UHoudiniSplineComponent* SplineComponent = Cast(Component); + if (IsValid(SplineComponent)) + { + // The spline component is a special case where the output + // object as associated Houdini nodes (as input object). + // We can only explicitly remove those nodes when the output object gets + // removed. + SplineComponent->MarkInputNodesAsPendingKill(); + } + + // Clear the output component + USceneComponent* SceneComp = Cast(Component); + if (IsValid(SceneComp)) + { + if(SceneComp->GetOwner()) + SceneComp->UnregisterComponent(); + + SceneComp->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + SceneComp->DestroyComponent(); + } + + if (Type == EHoudiniOutputType::Landscape && !bLandscapeWorldComposition && !IsGarbageCollecting()) + { + // NOTE: We cannot resolve soft pointers during garbage collection. Any Get() or IsValid() call + // will result in a StaticFindObject() call which will raise an exception during GC. + UHoudiniLandscapePtr* LandscapePtr = Cast(CurrentOutputObject.Value.OutputObject); + TSoftObjectPtr LandscapeProxy = LandscapePtr ? LandscapePtr->GetSoftPtr() : nullptr; + if (!LandscapeProxy.IsNull() && LandscapeProxy.IsValid()) + { + LandscapeProxy->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); + LandscapeProxy->ConditionalBeginDestroy(); + FHoudiniLandscapeRuntimeUtils::DestroyLandscapeProxy(LandscapeProxy.Get()); + LandscapePtr->SetSoftPtr(nullptr); + } + } + } + + // Also destroy proxy components + USceneComponent* ProxyComp = Cast(CurrentOutputObject.Value.ProxyComponent); + if (IsValid(ProxyComp)) + { + if (ProxyComp->GetOwner()) + ProxyComp->UnregisterComponent(); + + ProxyComp->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + ProxyComp->DestroyComponent(); + } + + // Destroy Landscape Spline Output Object + if (IsValid(CurrentOutputObject.Value.OutputObject) && CurrentOutputObject.Value.OutputObject->IsA()) + { + UHoudiniLandscapeSplinesOutput* const LandscapeSplinesOutputObject = Cast(CurrentOutputObject.Value.OutputObject); + LandscapeSplinesOutputObject->Clear(); + } + } + + OutputObjects.Empty(); + InstancedOutputs.Empty(); + AssignmentMaterialsById.Empty(); + ReplacementMaterialsById.Empty(); + + Type = EHoudiniOutputType::Invalid; +} + +bool +UHoudiniOutput::ShouldDeferClear() const +{ + if (Type == EHoudiniOutputType::Landscape) + return true; + + return false; +} + +const bool +UHoudiniOutput::HasGeoChanged() const +{ + for (auto currentHGPO : HoudiniGeoPartObjects) + { + if (currentHGPO.bHasGeoChanged) + return true; + } + + return false; +} + +const bool +UHoudiniOutput::HasTransformChanged() const +{ + for (auto currentHGPO : HoudiniGeoPartObjects) + { + if (currentHGPO.bHasTransformChanged) + return true; + } + + return false; +} + +const bool +UHoudiniOutput::HasMaterialsChanged() const +{ + for (auto currentHGPO : HoudiniGeoPartObjects) + { + if (currentHGPO.bHasMaterialsChanged) + return true; + } + + return false; +} + +const bool +UHoudiniOutput::HasHoudiniGeoPartObject(const FHoudiniGeoPartObject& InHGPO) const +{ + return HoudiniGeoPartObjects.Find(InHGPO) != INDEX_NONE; +} + +const bool +UHoudiniOutput::HeightfieldMatch(const FHoudiniGeoPartObject& InHGPO, const bool& bVolumeNameShouldMatch) const +{ + if (InHGPO.Type != EHoudiniPartType::Volume) + return false; + + if (InHGPO.VolumeName.IsEmpty()) + return false; + + for (auto& currentHGPO : HoudiniGeoPartObjects) + { + // Asset/Object/Geo IDs should match + if (currentHGPO.AssetId != InHGPO.AssetId + || currentHGPO.ObjectId != InHGPO.ObjectId + || currentHGPO.GeoId != InHGPO.GeoId) + { + continue; + } + + // Both HGPO type should be volumes + if (currentHGPO.Type != EHoudiniPartType::Volume) + { + continue; + } + + // Volume tile index should match + if (currentHGPO.VolumeTileIndex != InHGPO.VolumeTileIndex) + { + continue; + } + + // We've specified if we want the name to match/to be different: + // when looking in previous outputs, we want the name to match + // when looking in newly created outputs, we want to be sure the names are different + if (bVolumeNameShouldMatch) + { + // HasEditLayers state should match. + if (!(InHGPO.bHasEditLayers == currentHGPO.bHasEditLayers)) + { + continue; + } + + // If we have edit layers, ensure the names match + if (InHGPO.bHasEditLayers && !InHGPO.VolumeLayerName.Equals(currentHGPO.VolumeLayerName, ESearchCase::IgnoreCase)) + { + continue; + } + + // Check whether the volume name match. + if (!(InHGPO.VolumeName.Equals(currentHGPO.VolumeName, ESearchCase::IgnoreCase))) + { + continue; + } + } + + return true; + } + + return false; +} + +const bool UHoudiniOutput::GeoMatch(const FHoudiniGeoPartObject& InHGPO) const +{ + for (auto& currentHGPO : HoudiniGeoPartObjects) + { + // Asset/Object/Geo IDs should match + if (currentHGPO.AssetId != InHGPO.AssetId + || currentHGPO.ObjectId != InHGPO.ObjectId + || currentHGPO.GeoId != InHGPO.GeoId) + { + continue; + } + + return true; + } + + return false; +} + +const bool UHoudiniOutput::InstancerNameMatch(const FHoudiniGeoPartObject& InHGPO) const +{ + for (auto& currentHGPO : HoudiniGeoPartObjects) + { + // Asset/Object/Geo IDs should match + if (currentHGPO.AssetId != InHGPO.AssetId + || currentHGPO.ObjectId != InHGPO.ObjectId + || currentHGPO.GeoId != InHGPO.GeoId + || currentHGPO.InstancerName != InHGPO.InstancerName + ) + { + continue; + } + + return true; + } + + return false; +} + +void +UHoudiniOutput::MarkAllHGPOsAsStale(const bool& bInStale) +{ + // Since objects can only be added to this array, + // Simply keep track of the current number of HoudiniGeoPartObject + StaleCount = bInStale ? HoudiniGeoPartObjects.Num() : 0; +} + +void +UHoudiniOutput::DeleteAllStaleHGPOs() +{ + // Simply delete the first "StaleCount" objects and reset the stale marker + HoudiniGeoPartObjects.RemoveAt(0, StaleCount); + StaleCount = 0; +} + +void +UHoudiniOutput::AddNewHGPO(const FHoudiniGeoPartObject& InHGPO) +{ + HoudiniGeoPartObjects.Add(InHGPO); +} + +void +UHoudiniOutput::UpdateOutputType() +{ + int32 MeshCount = 0; + int32 CurveCount = 0; + int32 VolumeCount = 0; + int32 InstancerCount = 0; + int32 DataTableCount = 0; + int32 LandscapeSplineCount = 0; + int32 AnimSequenceCount = 0; + int32 SkeletonCount = 0; + int32 CopCount = 0; + int32 PCGCount = 0; + + for (auto& HGPO : HoudiniGeoPartObjects) + { + switch (HGPO.Type) + { + case EHoudiniPartType::Mesh: + MeshCount++; + break; + case EHoudiniPartType::Curve: + CurveCount++; + break; + case EHoudiniPartType::Volume: + VolumeCount++; + break; + case EHoudiniPartType::Instancer: + InstancerCount++; + break; + case EHoudiniPartType::DataTable: + DataTableCount++; + break; + case EHoudiniPartType::LandscapeSpline: + LandscapeSplineCount++; + break; + case EHoudiniPartType::MotionClip: + AnimSequenceCount++; + break; + case EHoudiniPartType::SkeletalMeshPose: + SkeletonCount++; + break; + case EHoudiniPartType::SkeletalMeshShape: + SkeletonCount++; + break; + case EHoudiniPartType::Cop: + CopCount++; + break; + case EHoudiniPartType::PCG: + PCGCount++; + default: + case EHoudiniPartType::Invalid: + break; + } + } + + if (VolumeCount > 0) + { + // If we have a volume, we're a landscape + Type = EHoudiniOutputType::Landscape; + } + else if (AnimSequenceCount > 0) + { + // Anim sequence take precedence over instancers and meshes since it contains both + Type = EHoudiniOutputType::AnimSequence; + } + else if (SkeletonCount > 0) + { + // Skeletal Meshes take precedence over instancers and meshes since it contains both + Type = EHoudiniOutputType::Skeletal; + } + else if (InstancerCount > 0) + { + // if we have at least an instancer, we're one + Type = EHoudiniOutputType::Instancer; + } + else if (MeshCount > 0) + { + Type = EHoudiniOutputType::Mesh; + } + else if (CurveCount > 0) + { + Type = EHoudiniOutputType::Curve; + } + else if (Type == EHoudiniOutputType::GeometryCollection) + { + // Geometry collections don't rely on HoudiniGeoPartObjects for construction, so keep the same Type. + } + else if (DataTableCount > 0) + { + Type = EHoudiniOutputType::DataTable; + } + else if (LandscapeSplineCount > 0) + { + Type = EHoudiniOutputType::LandscapeSpline; + } + else if (CopCount > 0) + { + Type = EHoudiniOutputType::Cop; + } + else if (PCGCount > 0) + { + Type = EHoudiniOutputType::PCG; + } + else + { + // No valid HGPO detected... + Type = EHoudiniOutputType::Invalid; + } +} + +UHoudiniOutput* +UHoudiniOutput::DuplicateAndCopyProperties(UObject* DestOuter, FName NewName) +{ + UHoudiniOutput* NewOutput = Cast(StaticDuplicateObject(this, DestOuter, NewName)); + + NewOutput->CopyPropertiesFrom(this, false); + + return NewOutput; +} + +void +UHoudiniOutput::CopyPropertiesFrom(UHoudiniOutput* InInput, bool bCopyAllProperties) +{ + // Copy the state of this UHoudiniInput object. + if (bCopyAllProperties) + { + // Stash all the data that we want to preserve, and re-apply after property copy took place + // (similar to Get/Apply component instance data). This is typically only needed + // for certain properties that require cleanup when being replaced / removed. + TMap PrevOutputObjects = OutputObjects; + TMap PrevInstancedOutputs = InstancedOutputs; + + UEngine::FCopyPropertiesForUnrelatedObjectsParams Params; + Params.bDoDelta = false; // Perform a deep copy + Params.bClearReferences = false; // References will be replaced afterwards. + UEngine::CopyPropertiesForUnrelatedObjects(InInput, this, Params); + + // Restore the desired properties. + OutputObjects = PrevOutputObjects; + InstancedOutputs = PrevInstancedOutputs; + } + + // Copy any additional DuplicateTransient properties. + bHasEditableNodeBuilt = InInput->bHasEditableNodeBuilt; +} + +void +UHoudiniOutput::SetCanDeleteHoudiniNodes(bool bInCanDeleteNodes) +{ + bCanDeleteHoudiniNodes = bInCanDeleteNodes; +} + +FString +UHoudiniOutput::OutputTypeToString(const EHoudiniOutputType& InOutputType) +{ + FString OutputTypeStr; + switch (InOutputType) + { + case EHoudiniOutputType::Mesh: + OutputTypeStr = TEXT("Mesh"); + break; + case EHoudiniOutputType::Instancer: + OutputTypeStr = TEXT("Instancer"); + break; + case EHoudiniOutputType::Landscape: + OutputTypeStr = TEXT("Landscape"); + break; + case EHoudiniOutputType::Curve: + OutputTypeStr = TEXT("Curve"); + break; + case EHoudiniOutputType::Skeletal: + OutputTypeStr = TEXT("Skeletal"); + break; + case EHoudiniOutputType::GeometryCollection: + OutputTypeStr = TEXT("GeometryCollection"); + break; + case EHoudiniOutputType::DataTable: + OutputTypeStr = TEXT("DataTable"); + break; + case EHoudiniOutputType::LandscapeSpline: + OutputTypeStr = TEXT("LandscapeSpline"); + break; + case EHoudiniOutputType::AnimSequence: + OutputTypeStr = TEXT("AnimSequence"); + break; + case EHoudiniOutputType::Cop: + OutputTypeStr = TEXT("Cop"); + break; + case EHoudiniOutputType::PCG: + OutputTypeStr = TEXT("PCG"); + break; + default: + case EHoudiniOutputType::Invalid: + OutputTypeStr = TEXT("Invalid"); + break; + } + + return OutputTypeStr; +} + +void +UHoudiniOutput::MarkAsLoaded(const bool& InLoaded) +{ + // Mark all HGPO as loaded + for (auto& HGPO : HoudiniGeoPartObjects) + { + HGPO.bLoaded = InLoaded; + } + + // Mark all output object's identifier as loaded + for (auto& Iter : OutputObjects) + { + FHoudiniOutputObjectIdentifier& Identifier = Iter.Key; + Identifier.bLoaded = InLoaded; + } + + // Instanced outputs + for (auto& Iter : InstancedOutputs) + { + FHoudiniOutputObjectIdentifier& Identifier = Iter.Key; + Identifier.bLoaded = InLoaded; + } +} + + +const bool +UHoudiniOutput::HasAnyProxy() const +{ + for (const auto& Pair : OutputObjects) + { + UObject* FoundProxy = Pair.Value.ProxyObject; + if (IsValid(FoundProxy)) + { + return true; + } + } + + return false; +} + +const bool +UHoudiniOutput::HasProxy(const FHoudiniOutputObjectIdentifier& InIdentifier) const +{ + const FHoudiniOutputObject* FoundOutputObject = OutputObjects.Find(InIdentifier); + if (!FoundOutputObject) + return false; + + UObject* FoundProxy = FoundOutputObject->ProxyObject; + if (!IsValid(FoundProxy)) + return false; + + return true; +} + +const bool +UHoudiniOutput::HasAnyCurrentProxy() const +{ + for (const auto& Pair : OutputObjects) + { + UObject* FoundProxy = Pair.Value.ProxyObject; + if (IsValid(FoundProxy)) + { + if(Pair.Value.bProxyIsCurrent) + { + return true; + } + } + } + + return false; +} + +const bool +UHoudiniOutput::IsProxyCurrent(const FHoudiniOutputObjectIdentifier &InIdentifier) const +{ + if (!HasProxy(InIdentifier)) + return false; + + const FHoudiniOutputObject* FoundOutputObject = OutputObjects.Find(InIdentifier); + if (!FoundOutputObject) + return false; + + return FoundOutputObject->bProxyIsCurrent; +} + + +void FHoudiniClearedEditLayers::Empty() +{ + EditLayers.Empty(); +} + + +bool FHoudiniClearedEditLayers::Contains(FString& EditLayer, FString& TargetLayer) +{ + return EditLayers.Contains(EditLayer) && EditLayers[EditLayer].TargetLayers.Contains(TargetLayer); +} + +void FHoudiniClearedEditLayers::Add(FString& EditLayer, FString& TargetLayer) +{ + if (!EditLayers.Contains(EditLayer)) + EditLayers.Add(EditLayer, {}); + + EditLayers[EditLayer].TargetLayers.Add(TargetLayer); +}; + +void DestroyComponent(UObject * Component) +{ + if (Component == nullptr || !IsValid(Component)) + return; + + USceneComponent* SceneComponent = Cast(Component); + if (IsValid(SceneComponent)) + { + // Remove from the HoudiniAssetActor + if (SceneComponent->GetOwner()) + { + SceneComponent->GetOwner()->RemoveOwnedComponent(SceneComponent); + SceneComponent->UnregisterComponent(); + } + + SceneComponent->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + SceneComponent->DestroyComponent(); + } +} + +void FHoudiniOutputObject::DestroyCookedData(EHoudiniClearFlags ClearFlags) +{ + //-------------------------------------------------------------------------------------------------------------------- + // Destroy all components + //-------------------------------------------------------------------------------------------------------------------- + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniOutputObject::DestroyCookedData); + + TArray ComponentsToDestroy; + + for (auto Component : OutputComponents) + { + if (IsValid(Component)) + { + ComponentsToDestroy.Add(Component); + } + } + OutputComponents.Empty(); + + if (IsValid(ProxyComponent)) + ComponentsToDestroy.Add(ProxyComponent); + + ProxyComponent = nullptr; + + for(UObject* Component : ComponentsToDestroy) + { + USceneComponent* SceneComponent = Cast(Component); + if (!SceneComponent) + continue; + + if(SceneComponent->GetOwner()) + SceneComponent->UnregisterComponent(); + + SceneComponent->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + SceneComponent->DestroyComponent(); + } + + + //-------------------------------------------------------------------------------------------------------------------- + // Delete output packages, if they exist + //-------------------------------------------------------------------------------------------------------------------- + +#if WITH_EDITOR + + bool bClearAssets = (ClearFlags & EHoudiniClearFlags::EHoudiniClear_Assets) == EHoudiniClearFlags::EHoudiniClear_Assets; + + if(bClearAssets && IsValid(OutputObject)) + { + TArray PackagesDeleted; + + bool bCanDelete = false; + if(OutputObject->IsA() || + OutputObject->IsA()) + { + bCanDelete = true; + } + + if (bCanDelete) + { + if(UPackage* Package = OutputObject->GetPackage()) + { + TArray ObjectsToDelete; + ObjectsToDelete.Add(Package); + GetObjectsWithOuter(Package, ObjectsToDelete, true); + + // Use ObjectTools to delete + ObjectTools::DeleteObjectsUnchecked(ObjectsToDelete); + + // Also delete the package file from disk + FString PackagePath = Package->GetPathName(); + FString FilePath = FPackageName::LongPackageNameToFilename(PackagePath, FPackageName::GetAssetPackageExtension()); + + FString DirectoryPath = FPaths::GetPath(FilePath); + + TArray Files; + TArray SubDirs; + IFileManager::Get().FindFiles(Files, *(DirectoryPath / TEXT("*")), true, false); + IFileManager::Get().FindFiles(SubDirs, *(DirectoryPath / TEXT("*")), false, true); + + if(Files.IsEmpty() && SubDirs.IsEmpty()) + { + IFileManager::Get().DeleteDirectory(*DirectoryPath, false, true); + } + } + } + } + + bool bClearLandscapeLayers = (ClearFlags & EHoudiniClearFlags::EHoudiniClear_LandscapeLayers) == EHoudiniClearFlags::EHoudiniClear_LandscapeLayers; + UHoudiniLandscapeTargetLayerOutput* LayerOutput = Cast(OutputObject); + + if (bClearLandscapeLayers && IsValid(LayerOutput)) + { + // For now, only delete layers we created. + if (LayerOutput->bLayerWasCreated) + FHoudiniLandscapeRuntimeUtils::DeleteEditLayer(LayerOutput->Landscape, FName(LayerOutput->CookedEditLayer)); + } + +#endif + + //-------------------------------------------------------------------------------------------------------------------- + // Remove spline output + //-------------------------------------------------------------------------------------------------------------------- + + // Destroy any segments that we previously created + UHoudiniLandscapeSplinesOutput* SplinesOutputObject = Cast(this->OutputObject); + if (IsValid(SplinesOutputObject)) + { + SplinesOutputObject->Clear(); + } + + //-------------------------------------------------------------------------------------------------------------------- + // If we overwrite a package that contains a data table, Unreal can assert if it points an old RowStruct. So + // a workaround is to point the Data Table to a temp structure before its deleted. + //-------------------------------------------------------------------------------------------------------------------- + + if(UDataTable* Table = Cast(OutputObject.Get())) + { + // Only delete Non-Native RowStructs: + // This prevents a crash when deleting a DataTable whose RowStruct is native, + // as it seems that UE has issues duplicating native structs. + // In that case - Table->RowStruct is already transient anyway. + if (Table->RowStruct && !Table->RowStruct->IsNative()) + { + Table->RowStruct = Cast(StaticDuplicateObject(Table->RowStruct, GetTransientPackage())); + Table->RowStruct->SetFlags(RF_Transient); + } + } + + //-------------------------------------------------------------------------------------------------------------------- + // Destroy all objects + //-------------------------------------------------------------------------------------------------------------------- + + if (UHoudiniLandscapeTargetLayerOutput * LandscapeOutput = Cast(OutputObject)) + { + // We can only clean up new landscapes. Modifications to existing landscapes are destructive, so not much + // we can do... + + if (LandscapeOutput->bCreatedLandscape) + { + if(IsValid(LandscapeOutput->Landscape)) + { + LandscapeOutput->Landscape->Destroy(); + LandscapeOutput->Landscape = nullptr; + } + } + } + else if (IsValid(OutputObject)) + { +#if WITH_EDITOR + if (!OutputObject->IsA()) + { + // Except for the TextureEditor - close other opened asset editor as was causing crashes + UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); + AssetEditorSubsystem->CloseAllEditorsForAsset(OutputObject); + } +#endif + } + OutputObject = nullptr; + ProxyObject = nullptr; + + //-------------------------------------------------------------------------------------------------------------------- + // Remove Foliage creating during cooking; we just need to move it from the world, and all the instances will be deleted. + //-------------------------------------------------------------------------------------------------------------------- + + if (IsValid(FoliageType)) + { + FHoudiniFoliageUtils::RemoveFoliageTypeFromWorld(World, FoliageType); + } + + //-------------------------------------------------------------------------------------------------------------------- + // Remove actors + //-------------------------------------------------------------------------------------------------------------------- + + for (auto Actor : OutputActors) + { + if (Actor.IsValid()) + { + Actor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); + Actor->Destroy(); + } + } + OutputActors.Empty(); +} + + +void UHoudiniOutput::DestroyCookedData(EHoudiniClearFlags ClearFlags) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(UHoudiniOutput::DestroyCookedData); + for (auto It : OutputObjects) + { + FHoudiniOutputObject* FoundOutputObject = &It.Value; + FoundOutputObject->DestroyCookedData(ClearFlags); + } + OutputObjects.Empty(); +} + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniOutput.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniGeoPartObject.h" +#include "HoudiniEngineRuntimeCommon.h" +#include "LevelInstance/LevelInstanceTypes.h" +#include "LandscapeProxy.h" +#include "Misc/StringFormatArg.h" +#include "HoudiniGenericAttribute.h" +#include "UObject/SoftObjectPtr.h" +#if defined(HOUDINI_USE_PCG) +#include "PCGParamData.h" +#include "Data/PCGSplineData.h" +#endif +#include "HoudiniOutput.generated.h" + +class UFoliageType; +class UMaterialInterface; +class ULandscapeLayerInfoObject; +class ULandscapeSplinesComponent; +class ULandscapeSplineControlPoint; +class ULandscapeSplineSegment; +class USkeleton; +class ALandscapeSplineActor; +struct FHoudiniDataLayer; + +enum class EHoudiniClearFlags : uint32 +{ + EHoudiniClear_Actors = 1 << 1, + EHoudiniClear_Assets = 1 << 2, + EHoudiniClear_LandscapeLayers = 1 << 3, +}; + +// Overloaded operators for setting flags +inline EHoudiniClearFlags operator|(EHoudiniClearFlags A, EHoudiniClearFlags B) +{ + return static_cast(static_cast(A) | static_cast(B)); +} + +inline EHoudiniClearFlags& operator|=(EHoudiniClearFlags& A, EHoudiniClearFlags B) +{ + A = A | B; + return A; +} + +inline EHoudiniClearFlags operator&(EHoudiniClearFlags A, EHoudiniClearFlags B) +{ + return static_cast(static_cast(A) & static_cast(B)); +} + +inline EHoudiniClearFlags& operator&=(EHoudiniClearFlags& A, EHoudiniClearFlags B) +{ + A = A & B; + return A; +} + +inline EHoudiniClearFlags operator~(EHoudiniClearFlags A) +{ + return static_cast(~static_cast(A)); +} + + +UENUM() +enum class EHoudiniCurveOutputType : uint8 +{ + UnrealSpline, + HoudiniSpline, +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniCurveOutputProperties +{ + GENERATED_USTRUCT_BODY() + + // Curve output properties + UPROPERTY() + EHoudiniCurveOutputType CurveOutputType = EHoudiniCurveOutputType::HoudiniSpline; + + UPROPERTY() + int32 NumPoints = -1; + + UPROPERTY() + bool bClosed = false; + + UPROPERTY() + EHoudiniCurveType CurveType = EHoudiniCurveType::Invalid; + + UPROPERTY() + EHoudiniCurveMethod CurveMethod = EHoudiniCurveMethod::Invalid; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniLandscapePtr : public UObject +{ + GENERATED_UCLASS_BODY() + + // THIS CLASS IS DEPRECATED. Its only kept around so old assets can be cleaned correctly. +public: + FORCEINLINE + void SetSoftPtr(TSoftObjectPtr InSoftPtr) { LandscapeSoftPtr = InSoftPtr; }; + + FORCEINLINE + TSoftObjectPtr GetSoftPtr() const { return LandscapeSoftPtr; }; + + // Calling Get() during GC will raise an exception because Get calls StaticFindObject. + FORCEINLINE + ALandscapeProxy* GetRawPtr() { return !IsGarbageCollecting() ? Cast(LandscapeSoftPtr.Get()) : nullptr; }; + + FORCEINLINE + FString GetSoftPtrPath() const { return LandscapeSoftPtr.ToSoftObjectPath().ToString(); }; + + FORCEINLINE + void SetLandscapeOutputBakeType(const EHoudiniLandscapeOutputBakeType & InBakeType) { BakeType = InBakeType; }; + + FORCEINLINE + EHoudiniLandscapeOutputBakeType GetLandscapeOutputBakeType() const { return BakeType; }; + + UPROPERTY() + TSoftObjectPtr LandscapeSoftPtr; + + UPROPERTY() + EHoudiniLandscapeOutputBakeType BakeType; + + // Edit layer to which this output corresponds, if applicable. + UPROPERTY() + FName EditLayerName; +}; + +class UHoudiniOutput; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniExtents +{ + GENERATED_BODY() + +public: + UPROPERTY() + FIntPoint Min = FIntPoint::ZeroValue; + + UPROPERTY() + FIntPoint Max = FIntPoint::ZeroValue; +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniClearedTargetLayer +{ + GENERATED_BODY() + + UPROPERTY() + TSet TargetLayers; +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniClearedEditLayers +{ + GENERATED_BODY() + + UPROPERTY() + TMap EditLayers; + + void Empty(); + bool Contains(FString & EditLayer, FString & TargetLayer); + void Add(FString& EditLayer, FString& TargetLayer); +}; + +// A struct to identify a material from a Houdini output +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniMaterialIdentifier +{ + GENERATED_BODY() + + FHoudiniMaterialIdentifier() : bIsHoudiniMaterial(false), bMakeMaterialInstance(false) {} + + // Constructor for Unreal materials + FHoudiniMaterialIdentifier( + const FString& InMaterialObjectPath, + bool bInMakeMaterialInstance, + const FString& InMaterialInstanceParametersSlug); + + // Constructor for Houdini materials or non-instance unreal materials + FHoudiniMaterialIdentifier(const FString& InMaterialPath, bool bIsHoudiniMaterial); + + virtual ~FHoudiniMaterialIdentifier() {} + + // Unreal UMaterial object path if bIsHoudiniMaterial is false. Otherwise this is a Houdini material node path. + UPROPERTY() + FString MaterialObjectPath; + + // True if this identifies a Houdini material (vs an Unreal material). + UPROPERTY() + bool bIsHoudiniMaterial; + + // If not a Houdini material, does this identify a material instance of MaterialObjectPath? + UPROPERTY() + bool bMakeMaterialInstance; + + // A string that encodes the overridden parameter value set of the material instance. + UPROPERTY() + FString MaterialInstanceParametersSlug; + + bool IsValid() const { return !MaterialObjectPath.IsEmpty(); } + + uint32 GetTypeHash() const; + + virtual bool operator==(const FHoudiniMaterialIdentifier& InRhs) const; +}; + +static uint32 GetTypeHash(const FHoudiniMaterialIdentifier& InIdentifier) { return InIdentifier.GetTypeHash(); } + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniLandscapeTargetLayerOutput : public UObject +{ + GENERATED_BODY() + +public: + UPROPERTY() + TObjectPtr Landscape; // Parent Landscape + + UPROPERTY() + TObjectPtr LandscapeProxy; // Landscape Proxy + + UPROPERTY() + FString BakedEditLayer; // Final baked layer name + + UPROPERTY() + FString CookedEditLayer; // Temp cooked layer name + + UPROPERTY() + FString TargetLayer; // Target layer name + + UPROPERTY() + FHoudiniExtents Extents; // Extents of the grid updated + + UPROPERTY() + bool bClearLayer; + + UPROPERTY() + bool bCreatedLandscape; + + UPROPERTY() + bool bCookedLayerRequiresBaking; + + UPROPERTY() + FString BakedLandscapeName; + + UPROPERTY() + TArray> LayerInfoObjects; + + UPROPERTY() + FString BakeFolder; + + UPROPERTY() + TObjectPtr MaterialInstance = nullptr; + + UPROPERTY() + bool bWriteLockedLayers = false; + + UPROPERTY() + bool bLockLayer = false; + + UPROPERTY() + TArray PropertyAttributes; + + UPROPERTY() + bool bLayerWasCreated = false; + + +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniLandscapeOutput : public UObject +{ + GENERATED_BODY() + +public: + UPROPERTY() + TObjectPtr Landscape; + + UPROPERTY() + FString BakedName; + + UPROPERTY() + TArray> Layers; + + UPROPERTY() + bool bCreated; + +}; + + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniLandscapeSplineTargetLayerOutput : public UHoudiniLandscapeTargetLayerOutput +{ + GENERATED_BODY() + +public: + UPROPERTY() + FName AfterEditLayer = NAME_None; + + UPROPERTY() + TArray> Segments; +}; + + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniLandscapeSplinesOutput : public UObject +{ + GENERATED_BODY() + +public: + + TMap>& GetLayerOutputs() { return LayerOutputs; } + const TMap>& GetLayerOutputs() const { return LayerOutputs; } + + bool GetLayerSegments(FName InEditLayer, TArray& OutSegments) const; + + TArray>& GetSegments() { return Segments; } + const TArray>& GetSegments() const { return Segments; } + + TArray>& GetControlPoints() { return ControlPoints; } + const TArray>& GetControlPoints() const { return ControlPoints; } + + void SetLandscape(ALandscape* InLandscape) { Landscape = InLandscape; } + ALandscape* GetLandscape() const { return Landscape; } + + void SetLandscapeProxy(ALandscapeProxy* InLandscapeProxy) { LandscapeProxy = InLandscapeProxy; } + ALandscapeProxy* GetLandscapeProxy() const { return LandscapeProxy; } + + void SetLandscapeSplineActor(ALandscapeSplineActor* InLandscapeSplineActor) { LandscapeSplineActor = InLandscapeSplineActor; } + ALandscapeSplineActor* GetLandscapeSplineActor() const { return LandscapeSplineActor; } + + void SetLandscapeSplinesComponent(ULandscapeSplinesComponent* InLandscapeSplinesComponent) { LandscapeSplinesComponent = InLandscapeSplinesComponent; } + ULandscapeSplinesComponent* GetLandscapeSplinesComponent() const { return LandscapeSplinesComponent; } + + // Clear the output object, destroying the segments, control points and landscape spline actor (if applicable). + void Clear(bool bInClearTempLayers=true); + +private: + UPROPERTY() + TObjectPtr Landscape; + + UPROPERTY() + TObjectPtr LandscapeProxy; + + UPROPERTY() + TObjectPtr LandscapeSplineActor; + + UPROPERTY() + TObjectPtr LandscapeSplinesComponent; + + UPROPERTY() + TMap> LayerOutputs; + + UPROPERTY() + TArray> Segments; + + UPROPERTY() + TArray> ControlPoints; +}; + + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniOutputObjectIdentifier +{ + GENERATED_USTRUCT_BODY() + +public: + // Constructors + FHoudiniOutputObjectIdentifier(); + FHoudiniOutputObjectIdentifier(int32 InObjectId, int32 InGeoId, int32 InPartId, const FString& InSplitIdentifier); + + // Return hash value for this object, used when using this object as a key inside hashing containers. + uint32 GetTypeHash() const; + + // Comparison operator, used by hashing containers. + bool operator==(const FHoudiniOutputObjectIdentifier& HoudiniGeoPartObject) const; + + bool Matches(const FHoudiniGeoPartObject& HoudiniGeoPartObject) const; + +public: + + // NodeId of corresponding Houdini Object. + UPROPERTY() + int32 ObjectId = -1; + + // NodeId of corresponding Houdini Geo. + UPROPERTY() + int32 GeoId = -1; + + // PartId + UPROPERTY() + int32 PartId = -1; + + // String identifier for the split that created this + UPROPERTY() + FString SplitIdentifier = FString(); + + // Name of the part used to generate the output + UPROPERTY() + FString PartName = FString(); + + // First valid primitive index for this output + // (used to read generic attributes) + UPROPERTY() + int32 PrimitiveIndex = -1; + + // First valid point index for this output + // (used to read generic attributes) + UPROPERTY() + int32 PointIndex = -1; + + bool bLoaded = false; +}; + +/** Function used by hashing containers to create a unique hash for this type of object. **/ +HOUDINIENGINERUNTIME_API uint32 GetTypeHash(const FHoudiniOutputObjectIdentifier& HoudiniOutputObjectIdentifier); + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniBakedOutputObjectIdentifier +{ + GENERATED_USTRUCT_BODY() + +public: + // Constructors + FHoudiniBakedOutputObjectIdentifier(); + FHoudiniBakedOutputObjectIdentifier(const int32& InPartId, const FString& InSplitIdentifier); + FHoudiniBakedOutputObjectIdentifier(const FHoudiniOutputObjectIdentifier& InIdentifier); + + // Return hash value for this object, used when using this object as a key inside hashing containers. + uint32 GetTypeHash() const; + + // Comparison operator, used by hashing containers. + bool operator==(const FHoudiniBakedOutputObjectIdentifier& InIdentifier) const; + +public: + + // PartId + UPROPERTY() + int32 PartId = -1; + + // String identifier for the split that created this + UPROPERTY() + FString SplitIdentifier = FString(); +}; + +/** Function used by hashing containers to create a unique hash for this type of object. **/ +HOUDINIENGINERUNTIME_API uint32 GetTypeHash(const FHoudiniBakedOutputObjectIdentifier& InIdentifier); + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniInstancedOutput +{ + GENERATED_USTRUCT_BODY() + + void MarkChanged(const bool InChanged) { bChanged = InChanged; }; + + UPROPERTY() + TSoftObjectPtr InstancedObject = nullptr; + + UPROPERTY() + int NumInstances = 0; + + // Indicates this instanced output's component should be recreated + UPROPERTY() + bool bChanged = false; +}; + +// Parameters used to create the level instance. +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniLevelInstanceParams +{ + GENERATED_USTRUCT_BODY() + + UPROPERTY() + ELevelInstanceCreationType Type = ELevelInstanceCreationType::LevelInstance; + + UPROPERTY() + FString OutputName; +}; + +inline uint32 GetTypeHash(const FHoudiniLevelInstanceParams& Params) +{ + return (GetTypeHash((int32)Params.Type) + 23 * GetTypeHash(Params.OutputName)); +} + +inline bool operator==(const FHoudiniLevelInstanceParams& p1, const FHoudiniLevelInstanceParams& p2) +{ + return p1.Type == p2.Type && p1.OutputName == p2.OutputName; +} + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniBakedOutputObject +{ + GENERATED_USTRUCT_BODY() + + public: + FHoudiniBakedOutputObject(); + + FHoudiniBakedOutputObject(AActor* InActor, FName InActorBakeName, UObject* InBakeObject=nullptr, UObject* InBakedComponent=nullptr); + + // Returns Actor if valid, otherwise nullptr + AActor* GetActorIfValid(bool bInTryLoad=true) const; + + // Returns BakedObject if valid, otherwise nullptr + UObject* GetBakedObjectIfValid(bool bInTryLoad=true) const; + + // Returns BakedComponent if valid, otherwise nullptr + UObject* GetBakedComponentIfValid(bool bInTryLoad=true) const; + + // Returns Blueprint if valid, otherwise nullptr + UBlueprint* GetBlueprintIfValid(bool bInTryLoad=true) const; + + // Returns the ULandscapeLayerInfoObject, if valid and found in LandscapeLayers, otherwise nullptr + ULandscapeLayerInfoObject* GetLandscapeLayerInfoIfValid(const FName& InLayerName, const bool bInTryLoad=true) const; + + // Returns the Generated Landscape Actor if valid + ALandscape* GetLandscapeIfValid(bool bInTryLoad=true) const; + + // Returns BakedSkeleton if valid, otherwise nullptr + USkeleton* GetBakedSkeletonIfValid(bool bInTryLoad=true) const; + + // Returns BakedPhysicsAsset if valid, otherwise nullptr + UPhysicsAsset* GetBakedPhysicsAssetIfValid(bool bInTryLoad = true) const; + + // Returns the generated or modified Foliage actors if valid + TArray GetFoliageActorsIfValid(bool bInTryLoad=true) const; + + // Returns an array of valid instanced actors + TArray GetInstancedActorsIfValid(bool bInTryLoad=true) const; + + // The actor that the baked output was associated with + UPROPERTY() + FString Actor; + + // The blueprint that baked output was associated with, if any + UPROPERTY() + FString Blueprint; + + // The intended bake actor name. The actor's actual name could have a numeric suffix for uniqueness. + UPROPERTY() + FName ActorBakeName = NAME_None; + + // The baked output asset + UPROPERTY() + FString BakedObject; + + // The baked output component + UPROPERTY() + FString BakedComponent; + + // In the case of instance actor component baking, this is the array of instanced actors + UPROPERTY() + TArray InstancedActors; + + // In the case of mesh split instancer baking: this is the array of instance components + UPROPERTY() + TArray InstancedComponents; + + // For landscapes this is the previously bake layer info assets (layer name as key, soft object path as value) + UPROPERTY() + TMap LandscapeLayers; + + // For landscapes this is the layers we created. + UPROPERTY() + TArray CreatedLandscapeLayers; + + // Positions of Foliage instances; used for removal on rebake. + UPROPERTY() + TArray FoliageInstancePositions; + + // Foliage Type (Baked or user-defined) + UPROPERTY() + TObjectPtr FoliageType = nullptr; + + // Foliage Actor Instances + UPROPERTY() + TArray FoliageActors; + + // All exported level instance actors. + UPROPERTY() + TArray LevelInstanceActors; + + // For landscape splines, this is the landscape that contains the splines. + UPROPERTY() + FString Landscape; + + // For skeletal meshes, this is the skeleton that was baked for the skeletal mesh. + UPROPERTY() + FString BakedSkeleton; + + // For skeletal meshes, this is the physics that was baked for the skeletal mesh. + UPROPERTY() + FString BakedPhysicsAsset; + + // PCG Output Object. Referenced asa UObject so it compiles in non-PCG builds. + UPROPERTY() + TObjectPtr PCGOutputData; + +}; + +// Container to hold the map of baked objects. There should be one of +// these for each UHoudiniOutput. We manage this separately from UHoudiniOutput so +// that the "previous/last" bake objects can survive output reconstruction or PDG +// dirty/dirty all operations. +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniBakedOutput +{ + GENERATED_USTRUCT_BODY() + + public: + UPROPERTY() + TMap BakedOutputObjects; +}; + +// Information about the data the output is to be placed in. +USTRUCT() +struct FHoudiniDataLayer +{ + GENERATED_USTRUCT_BODY() + + UPROPERTY() + FString Name; + + UPROPERTY() + bool bCreateIfNeeded = false; +}; + + +USTRUCT() +struct FHoudiniHLODLayer +{ + GENERATED_USTRUCT_BODY() + + UPROPERTY() + FString Name; +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniOutputObject +{ + GENERATED_USTRUCT_BODY() + + public: + + void DestroyCookedData(EHoudiniClearFlags ClearFlags); + + // The main output object + UPROPERTY() + TObjectPtr OutputObject = nullptr; + + // The main output component + UPROPERTY() + TArray> OutputComponents; + + // The main output component + UPROPERTY() + TArray> OutputActors; + + // Proxy object + UPROPERTY() + TObjectPtr ProxyObject = nullptr; + + // Proxy Component + UPROPERTY() + TObjectPtr ProxyComponent = nullptr; + + // Mesh output properties + // If this is true the proxy mesh is "current", + // in other words, it is newer than the UStaticMesh + UPROPERTY() + bool bProxyIsCurrent = false; + + // Implicit output objects shouldn't be created as actors / components in the scene. + UPROPERTY() + bool bIsImplicit = false; + + // When creating an invisible collision mesh we need to make tweaks to the component. + UPROPERTY() + bool bIsInvisibleCollisionMesh = false; + + // Is this mesh a part of a geometry collection? + UPROPERTY() + bool bIsGeometryCollectionPiece = false; + + // Associated geometry collection. Only valid if bIsGeometryCollectionPiece is true; + // Cached on mesh generation to avoid a Houdini session requirement for baking + UPROPERTY() + FString GeometryCollectionPieceName = TEXT(""); + + // Bake Name override for this output object + UPROPERTY() + FString BakeName; + + UPROPERTY() + FHoudiniCurveOutputProperties CurveOutputProperty; + + + // NOTE: The idea behind CachedAttributes and CachedTokens is to + // collect attributes (such as unreal_level_path and unreal_output_name) + // and context-specific tokens (hda name, hda actor name, geo and part ids, tile_id, etc) + // and cache them directly on the output object. When the object gets baked, + // certain tokens can be updated specifically for the bake pass afterwhich the + // the string / path attributes can be resolved with the updated tokens. + // + // A more concrete example: + // unreal_output_name = "{hda_actor_name}_PurplePlants_{geo_id}_{part_id}" + // unreal_level_path = "{out}/{hda_name}/{guid}/PurplePlants/{geo_id}/{part_id}" + // + // All of the aforementions tokens and attributes would be cached on the output object + // when it is being cooked so that the same values are available at bake time. During + // a bake some tokens may be updated, such as `{out}` to change where assets get serialized. + + UPROPERTY() + TMap CachedAttributes; + + // Cache any tokens here that is needed for string resolving + // at bake time. + UPROPERTY() + TMap CachedTokens; + + // Object that was instanced. + UPROPERTY() + TObjectPtr UserFoliageType = nullptr; + + // Foliage Type was that used. + UPROPERTY() + TObjectPtr FoliageType = nullptr; + + // World used when creating the output. This is used for Foliage may have no explicit objects + // are created and so we cannot track the original world when we want to remove instances. + UPROPERTY() + TObjectPtr World = nullptr; + + // Data Layers which should be applied (during Baking only). There can be multiple data layers per actor. + UPROPERTY() + TArray DataLayers; + + // HLOD Layers which should be applied (during Baking only). Currently UE only supports one HLOD layer + // per Actor, but we store this an array, since changing that would cause issues. + UPROPERTY() + TArray HLODLayers; + + UPROPERTY() + FHoudiniLevelInstanceParams LevelInstanceParams; +}; + + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniOutput : public UObject +{ + GENERATED_UCLASS_BODY() + + // Declare translators as friend so they can easily directly modify + // and access our HGPO and Output objects + friend class FHoudiniEditorEquivalenceUtils; + friend struct FHoudiniMeshTranslator; + friend struct FHoudiniSkeletalMeshTranslator; + friend struct FHoudiniInstanceTranslator; + friend struct FHoudiniOutputTranslator; + friend struct FHoudiniGeometryCollectionTranslator; + friend struct FHoudiniPDGTranslator; + + virtual ~UHoudiniOutput(); + +public: + + void DestroyCookedData(EHoudiniClearFlags ClearFlags); + + //------------------------------------------------------------------------------------------------ + // Accessors + //------------------------------------------------------------------------------------------------ + const EHoudiniOutputType& GetType() const { return Type; }; + + const TArray& GetHoudiniGeoPartObjects() const { return HoudiniGeoPartObjects; }; + + // Returns true if we have a HGPO that matches + const bool HasHoudiniGeoPartObject(const FHoudiniGeoPartObject& InHGPO) const; + + // Returns true if the HGPO is fromn the same HF as us + const bool HeightfieldMatch(const FHoudiniGeoPartObject& InHGPO, const bool& bVolumeNameShouldMatch) const; + + // Returns true if the HGPO is from the same Geo (output), ignoring the PartID + const bool GeoMatch(const FHoudiniGeoPartObject& InHGPO) const; + + // Returns true if the HGPO is from the same Geo (output), ignoring the PartID but including the InstancerName property. + // This is useful if multiple HGPOs need be to grouped together in a single Output Object, such as Skeletal Mesh parts. + const bool InstancerNameMatch(const FHoudiniGeoPartObject& InHGPO) const; + + // Returns the output objects and their corresponding identifiers + TMap& GetOutputObjects() { return OutputObjects; }; + + // Returns the output objects and their corresponding identifiers + const TMap& GetOutputObjects() const { return OutputObjects; }; + + // Returns this output's assignement material map + TMap>& GetAssignementMaterials() { return AssignmentMaterialsById; }; + + // Returns this output's replacement material map + TMap>& GetReplacementMaterials() { return ReplacementMaterialsById; }; + + // Returns the instanced outputs maps + TMap& GetInstancedOutputs() { return InstancedOutputs; }; + + const bool HasGeoChanged() const; + const bool HasTransformChanged() const; + const bool HasMaterialsChanged() const; + + // Returns true if there are any proxy objects in output (current or not) + const bool HasAnyProxy() const; + // Returns true if the specified identifier has a proxy object (current or not) + const bool HasProxy(const FHoudiniOutputObjectIdentifier &InIdentifier) const; + // Returns true if there are any current (most up to date and visible) proxy in the output + const bool HasAnyCurrentProxy() const; + // Returns true if the specified identifier's proxy is "current" (in other words, newer than + // the non-proxy and the proxy should thus be shown instead. + const bool IsProxyCurrent(const FHoudiniOutputObjectIdentifier &InIdentifier) const; + + + //------------------------------------------------------------------------------------------------ + // Mutators + //------------------------------------------------------------------------------------------------ + void UpdateOutputType(); + + // Adds a new HoudiniGeoPartObject to our array + void AddNewHGPO(const FHoudiniGeoPartObject& InHGPO); + + // Mark all the current HGPO as stale (from a previous cook) + // So we can delte them all by calling DeleteAllStaleHGPOs after. + void MarkAllHGPOsAsStale(const bool& InStale); + + // Delete all the HGPO that were marked as stale + void DeleteAllStaleHGPOs(); + + void SetOutputObjects(const TMap& InOutputObjects) { OutputObjects = InOutputObjects; }; + + void SetInstancedOutputs(const TMap& InInstancedOuput) { InstancedOutputs = InInstancedOuput; }; + + // Marks all HGPO and OutputIdentifier as loaded + void MarkAsLoaded(const bool& InLoaded); + + FORCEINLINE + const bool IsEditableNode() { return bIsEditableNode; }; + + FORCEINLINE + void SetIsEditableNode(bool IsEditable) { bIsEditableNode = IsEditable; } + + FORCEINLINE + const bool HasEditableNodeBuilt() { return bHasEditableNodeBuilt; }; + + FORCEINLINE + void SetHasEditableNodeBuilt(bool HasBuilt) { bHasEditableNodeBuilt = HasBuilt; } + + FORCEINLINE + void SetIsUpdating(bool bInIsUpdating) { bIsUpdating = bInIsUpdating; }; + + FORCEINLINE + bool IsUpdating() const { return bIsUpdating; }; + + FORCEINLINE + void SetLandscapeWorldComposition(const bool bInLandscapeWorldComposition) { bLandscapeWorldComposition = bInLandscapeWorldComposition; }; + + FORCEINLINE + bool IsLandscapeWorldComposition () const { return bLandscapeWorldComposition; }; + + FORCEINLINE + TArray> & GetHoudiniCreatedSocketActors() { return HoudiniCreatedSocketActors; }; + + FORCEINLINE + TArray> & GetHoudiniAttachedSocketActors() { return HoudiniAttachedSocketActors; } + + // Duplicate this object and copy its state to the resulting object. + // This is typically used to transfer state between between template and instance components. + UHoudiniOutput* DuplicateAndCopyProperties(UObject* DestOuter, FName NewName); + + // Copy properties but preserve output objects + virtual void CopyPropertiesFrom(UHoudiniOutput* InOutput, bool bCopyAllProperties); + + // Set whether this object can delete Houdini nodes. + void SetCanDeleteHoudiniNodes(bool bInCanDeleteNodes); + bool CanDeleteHoudiniNodes() const { return bCanDeleteHoudiniNodes; } + + //------------------------------------------------------------------------------------------------ + // Helpers + //------------------------------------------------------------------------------------------------ + static FString OutputTypeToString(const EHoudiniOutputType& InOutputType); + + FBox GetBounds() const; + + void Clear(); + + bool ShouldDeferClear() const; + +protected: + + virtual void BeginDestroy() override; + + virtual void PostLoad() override; + +protected: + // Indicates the type of output we're dealing with + UPROPERTY() + EHoudiniOutputType Type; + + // The output's corresponding HGPO + UPROPERTY() + TArray HoudiniGeoPartObjects; + + // + UPROPERTY(DuplicateTransient) + TMap OutputObjects; + + // Instanced outputs + // Stores the instance variations objects (replacement), transform offsets + UPROPERTY() + TMap InstancedOutputs; + + // The material assignments for this output + UPROPERTY() + TMap> AssignmentMaterialsById; + + // The material replacements for this output + UPROPERTY() + TMap> ReplacementMaterialsById; + + // Indicates the number of stale HGPO + int32 StaleCount; + + UPROPERTY() + bool bCreateSceneComponents = true; + + UPROPERTY() + bool bLandscapeWorldComposition; + + // stores the created actors for sockets with actor references. + // + UPROPERTY() + TArray> HoudiniCreatedSocketActors; + + UPROPERTY() + TArray> HoudiniAttachedSocketActors; + +private: + // Use HoudiniOutput to represent an editable curve. + // This flag tells whether this output is an editable curve. + UPROPERTY() + bool bIsEditableNode; + + // An editable node is only built once. This flag indicates whether this node has been built. + // Transient, so resets every unreal session so curves must be rebuilt to work properly. + UPROPERTY(Transient, DuplicateTransient) + bool bHasEditableNodeBuilt; + + // The IsUpdating flag is set to true when this out exists and is being updated. + UPROPERTY() + bool bIsUpdating; + + UPROPERTY() + bool bCanDeleteHoudiniNodes; +}; + + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniOutputTypes.h`: + +```h +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniPCGDataObject.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPCGDataObject.h" + +#include "Runtime/Launch/Resources/Version.h" +#include +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 +#include +#endif +#include "HoudiniEngineRuntimeUtils.h" +#include "UObject/TextProperty.h" +#include "PCGParamData.h" + +void UHoudiniPCGDataAttributeBase::SetAttrName(const FString& Name) +{ + AttrName = FName(Name); +} + + +const FName& UHoudiniPCGDataAttributeBase::GetAttrName() const +{ + return AttrName; +} + +bool UHoudiniPCGDataObject::operator==(const UHoudiniPCGDataObject& Other) const +{ + // very simple, optimize? + return (this->Attributes == Other.Attributes); +} + +bool UHoudiniPCGDataObject::operator!=(const UHoudiniPCGDataObject& Other) const +{ + return !(*this == Other); +} + +void UHoudiniPCGDataObject::SetFromPCGData(const UPCGData* PCGData, const TSet & Tags) +{ + PCGDataType = PCGData->GetDataType(); + PCGTags = Tags; + if(PCGData->IsA()) + SetFromPCGData(Cast(PCGData)); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + else if(PCGData->IsA()) + SetFromPCGBasePointData(Cast(PCGData)); +#else + else if(PCGData->IsA()) + SetFromPCGData(Cast(PCGData)); +#endif + else if(PCGData->IsA()) + SetFromPCGData(Cast(PCGData)); + +} + +void UHoudiniPCGDataObject::SetFromPCGData(const UPCGSplineData* PCGSplineData) +{ + const UPCGMetadata* Metadata = PCGSplineData->ConstMetadata(); + this->PCGDataType = PCGSplineData->GetDataType(); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION <= 5 + auto & SplineCurves = PCGSplineData->SplineStruct.SplineCurves; + auto& Points = SplineCurves.Position.Points; +#else + auto& Points = PCGSplineData->SplineStruct.GetSplinePointsPosition().Points; +#endif + + + auto AttrDest = CreateAttributeVector3d(TEXT("P")); + AttrDest->Values.SetNum(Points.Num()); + + for(int Index = 0; Index < Points.Num(); Index++) + { + FVector Position = PCGSplineData->GetTransform().TransformPosition(Points[Index].OutVal); + + AttrDest->Values[Index][0] = Position.X / 100.0f; + AttrDest->Values[Index][1] = Position.Z / 100.0f; + AttrDest->Values[Index][2] = Position.Y / 100.0f; + } + Attributes.Emplace(MoveTemp(AttrDest)); + + bIsClosed = PCGSplineData->SplineStruct.IsClosedLoop(); + +} + +void UHoudiniPCGDataObject::SetFromPCGData(const UPCGPointData* PCGPointData) +{ + const UPCGMetadata* Metadata = PCGPointData->ConstMetadata(); + this->PCGDataType = PCGPointData->GetDataType(); + + const TArray& Points = PCGPointData->GetPoints(); + + { + auto AttrDest = CreateAttributeVector3d(TEXT("P")); + AttrDest->Values.SetNum(Points.Num()); + for(int Index = 0; Index < Points.Num(); Index++) + { + FVector Position = Points[Index].Transform.GetLocation(); + AttrDest->Values[Index][0] = Position.X / 100.0f; + AttrDest->Values[Index][1] = Position.Z / 100.0f; + AttrDest->Values[Index][2] = Position.Y / 100.0f; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeVector3d(TEXT("Scale")); + AttrDest->Values.SetNum(Points.Num()); + for(int Index = 0; Index < Points.Num(); Index++) + { + FVector Scale = Points[Index].Transform.GetScale3D(); + AttrDest->Values[Index][0] = Scale.X; + AttrDest->Values[Index][1] = Scale.Z; + AttrDest->Values[Index][2] = Scale.Y; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeVector3d(TEXT("BoundsMin")); + AttrDest->Values.SetNum(Points.Num()); + for(int Index = 0; Index < Points.Num(); Index++) + { + FVector Value = Points[Index].BoundsMin; + AttrDest->Values[Index][0] = Value.X / 100.0; + AttrDest->Values[Index][1] = Value.Z / 100.0; + AttrDest->Values[Index][2] = Value.Y / 100.0; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeVector3d(TEXT("BoundsMax")); + AttrDest->Values.SetNum(Points.Num()); + for(int Index = 0; Index < Points.Num(); Index++) + { + FVector Value = Points[Index].BoundsMin; + AttrDest->Values[Index][0] = Value.X / 100.0; + AttrDest->Values[Index][1] = Value.Z / 100.0; + AttrDest->Values[Index][2] = Value.Y / 100.0; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeVector4d(TEXT("Cd")); + AttrDest->Values.SetNum(Points.Num()); + for(int Index = 0; Index < Points.Num(); Index++) + { + FVector4d Value = Points[Index].Color; + AttrDest->Values[Index][0] = Value.X; + AttrDest->Values[Index][1] = Value.Y; + AttrDest->Values[Index][2] = Value.Z; + AttrDest->Values[Index][3] = Value.W; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeVector4d(TEXT("orient")); + AttrDest->Values.SetNum(Points.Num()); + for(int Index = 0; Index < Points.Num(); Index++) + { + FQuat Rotation = Points[Index].Transform.GetRotation(); + AttrDest->Values[Index][0] = Rotation.X; + AttrDest->Values[Index][1] = Rotation.Z; + AttrDest->Values[Index][2] = Rotation.Y; + AttrDest->Values[Index][3] = -Rotation.W; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeFloat(TEXT("Density")); + AttrDest->Values.SetNum(Points.Num()); + for(int Index = 0; Index < Points.Num(); Index++) + { + AttrDest->Values[Index] = Points[Index].Density; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeFloat(TEXT("Steepness")); + AttrDest->Values.SetNum(Points.Num()); + for(int Index = 0; Index < Points.Num(); Index++) + { + AttrDest->Values[Index] = Points[Index].Steepness; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeInt(TEXT("Seed")); + AttrDest->Values.SetNum(Points.Num()); + for(int Index = 0; Index < Points.Num(); Index++) + { + AttrDest->Values[Index] = Points[Index].Seed; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + + TArray Keys; + Keys.SetNum(Points.Num()); + for(int Index = 0; Index < Keys.Num(); Index++) + { + Keys[Index] = Points[Index].MetadataEntry; + } + + AddMetaDataAttributes(Metadata, Keys); +} + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 +void UHoudiniPCGDataObject::SetFromPCGBasePointData(const UPCGBasePointData* PCGPointData) +{ + const UPCGMetadata* Metadata = PCGPointData->ConstMetadata(); + this->PCGDataType = PCGPointData->GetDataType(); + + int NumPoints = PCGPointData->GetNumPoints(); + + { + auto AttrDest = CreateAttributeVector3d(TEXT("P")); + AttrDest->Values.SetNum(NumPoints); + auto ValueRange = PCGPointData->GetConstTransformValueRange(); + + for(int Index = 0; Index < NumPoints; Index++) + { + FVector Position = ValueRange[Index].GetLocation(); + AttrDest->Values[Index][0] = Position.X / 100.0f; + AttrDest->Values[Index][1] = Position.Z / 100.0f; + AttrDest->Values[Index][2] = Position.Y / 100.0f; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeVector3d(TEXT("Scale")); + AttrDest->Values.SetNum(NumPoints); + auto ValueRange = PCGPointData->GetConstTransformValueRange(); + + for(int Index = 0; Index < NumPoints; Index++) + { + FVector Scale = ValueRange[Index].GetScale3D(); + AttrDest->Values[Index][0] = Scale.X; + AttrDest->Values[Index][1] = Scale.Z; + AttrDest->Values[Index][2] = Scale.Y; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeVector3d(TEXT("BoundsMin")); + AttrDest->Values.SetNum(NumPoints); + auto ValueRange = PCGPointData->GetConstBoundsMinValueRange(); + + for(int Index = 0; Index < NumPoints; Index++) + { + FVector Value = ValueRange[Index]; + AttrDest->Values[Index][0] = Value.X / 100.0; + AttrDest->Values[Index][1] = Value.Z / 100.0; + AttrDest->Values[Index][2] = Value.Y / 100.0; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeVector3d(TEXT("BoundsMax")); + AttrDest->Values.SetNum(NumPoints); + auto ValueRange = PCGPointData->GetConstBoundsMaxValueRange(); + + for(int Index = 0; Index < NumPoints; Index++) + { + FVector Value = ValueRange[Index]; + AttrDest->Values[Index][0] = Value.X / 100.0; + AttrDest->Values[Index][1] = Value.Z / 100.0; + AttrDest->Values[Index][2] = Value.Y / 100.0; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeVector4d(TEXT("Cd")); + AttrDest->Values.SetNum(NumPoints); + auto ValueRange = PCGPointData->GetConstColorValueRange(); + + for(int Index = 0; Index < NumPoints; Index++) + { + FVector4d Value = ValueRange[Index]; + AttrDest->Values[Index][0] = Value.X; + AttrDest->Values[Index][1] = Value.Y; + AttrDest->Values[Index][2] = Value.Z; + AttrDest->Values[Index][3] = Value.W; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeVector4d(TEXT("orient")); + AttrDest->Values.SetNum(NumPoints); + auto ValueRange = PCGPointData->GetConstTransformValueRange(); + + for(int Index = 0; Index < NumPoints; Index++) + { + FQuat Rotation = ValueRange[Index].GetRotation(); + AttrDest->Values[Index][0] = Rotation.X; + AttrDest->Values[Index][1] = Rotation.Z; + AttrDest->Values[Index][2] = Rotation.Y; + AttrDest->Values[Index][3] = -Rotation.W; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeFloat(TEXT("Density")); + AttrDest->Values.SetNum(NumPoints); + auto ValueRange = PCGPointData->GetConstDensityValueRange(); + + for(int Index = 0; Index < NumPoints; Index++) + { + AttrDest->Values[Index] = ValueRange[Index]; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeFloat(TEXT("Steepness")); + AttrDest->Values.SetNum(NumPoints); + auto ValueRange = PCGPointData->GetConstSteepnessValueRange(); + + for(int Index = 0; Index < NumPoints; Index++) + { + AttrDest->Values[Index] = ValueRange[Index]; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + { + auto AttrDest = CreateAttributeInt(TEXT("Seed")); + AttrDest->Values.SetNum(NumPoints); + auto ValueRange = PCGPointData->GetConstSeedValueRange(); + + for(int Index = 0; Index < NumPoints; Index++) + { + AttrDest->Values[Index] = ValueRange[Index]; + } + Attributes.Emplace(MoveTemp(AttrDest)); + } + + auto ValueRange = PCGPointData->GetConstMetadataEntryValueRange(); + + TArray Keys; + Keys.SetNum(NumPoints); + for (int Index = 0; Index < NumPoints; Index++) + { + Keys[Index] = ValueRange[Index]; + } + + AddMetaDataAttributes(Metadata, Keys); +} +#endif + +void UHoudiniPCGDataObject::SetFromPCGData(const UPCGParamData* PCGParamData) +{ + const UPCGMetadata* Metadata = PCGParamData->ConstMetadata(); + + AddMetaDataAttributes(Metadata, {}); +} + +void UHoudiniPCGDataObject::AddMetaDataAttributes(const UPCGMetadata* ParamMetadata, const TArray& Keys) +{ + TArray AttributeNames; + TArray AttributeTypes; + + ParamMetadata->GetAttributes(AttributeNames, AttributeTypes); + + const int64 InvalidIndex = -1; + + + for(int AttrIndex = 0; AttrIndex < AttributeTypes.Num(); AttrIndex++) + { + EPCGMetadataTypes AttrType = AttributeTypes[AttrIndex]; + const FString & AttrName = AttributeNames[AttrIndex].ToString(); + + const FPCGMetadataAttributeBase* AttrBase = ParamMetadata->GetConstAttribute(AttributeNames[AttrIndex]); + + const UPCGMetadata* Metadata = AttrBase->GetMetadata(); + + int NumRows = Metadata->GetItemCountForChild(); + if(!Keys.IsEmpty()) + NumRows = Keys.Num(); + + + switch(AttrType) + { + case EPCGMetadataTypes::Float: + { + auto* Attr = static_cast*>(AttrBase); + auto AttrDest = CreateAttributeFloat(AttrName); + + AttrDest->Values.SetNum(NumRows * 1); + for(int Index = 0; Index < NumRows; Index++) + { + int ValueIndex = Keys.IsEmpty() ? Index : Keys[Index]; + if (ValueIndex != InvalidIndex) + AttrDest->Values[Index] = Attr->GetValueFromItemKey(ValueIndex); + } + + Attributes.Emplace(MoveTemp(AttrDest)); + } + break; + case EPCGMetadataTypes::Double: + { + auto* Attr = static_cast*>(AttrBase); + auto AttrDest = CreateAttributeDouble(AttrName); + + AttrDest->Values.SetNum(NumRows * 1); + for(int Index = 0; Index < NumRows; Index++) + { + int ValueIndex = Keys.IsEmpty() ? Index : Keys[Index]; + if(ValueIndex != InvalidIndex) + AttrDest->Values[Index] = Attr->GetValueFromItemKey(ValueIndex); + } + + Attributes.Emplace(MoveTemp(AttrDest)); + } + break; + case EPCGMetadataTypes::Integer32: + { + auto* Attr = static_cast*>(AttrBase); + + auto AttrDest = CreateAttributeInt(AttrName); + AttrDest->Values.SetNum(NumRows * 1); + for(int Index = 0; Index < NumRows; Index++) + { + int ValueIndex = Keys.IsEmpty() ? Index : Keys[Index]; + AttrDest->Values[Index] = Attr->GetValueFromItemKey(ValueIndex); + } + + Attributes.Emplace(MoveTemp(AttrDest)); + } + break; + case EPCGMetadataTypes::Integer64: + { + auto* Attr = static_cast*>(AttrBase); + auto AttrDest = CreateAttributeInt64(AttrName); + + AttrDest->Values.SetNum(NumRows * 1); + for(int Index = 0; Index < NumRows; Index++) + { + int ValueIndex = Keys.IsEmpty() ? Index : Keys[Index]; + AttrDest->Values[Index] = Attr->GetValueFromItemKey(ValueIndex); + } + + Attributes.Emplace(MoveTemp(AttrDest)); + } + break; + case EPCGMetadataTypes::Boolean: + { + auto* Attr = static_cast*>(AttrBase); + + auto AttrDest = CreateAttributeInt(AttrName); + AttrDest->Values.SetNum(NumRows * 1); + for(int Index = 0; Index < NumRows; Index++) + { + int ValueIndex = Keys.IsEmpty() ? Index : Keys[Index]; + AttrDest->Values[Index] = Attr->GetValueFromItemKey(ValueIndex); + } + + Attributes.Emplace(MoveTemp(AttrDest)); + } + break; + case EPCGMetadataTypes::Vector2: + { + auto* Attr = static_cast*>(AttrBase); + auto AttrDest = CreateAttributeVector2d(AttrName); + + AttrDest->Values.SetNum(NumRows); + for(int Index = 0; Index < NumRows; Index++) + { + int ValueIndex = Keys.IsEmpty() ? Index : Keys[Index]; + AttrDest->Values[Index] = Attr->GetValueFromItemKey(ValueIndex); + } + + Attributes.Emplace(MoveTemp(AttrDest)); + } + break; + case EPCGMetadataTypes::Vector: + { + auto* Attr = static_cast*>(AttrBase); + auto AttrDest = CreateAttributeVector3d(AttrName); + + AttrDest->Values.SetNum(NumRows); + for(int Index = 0; Index < NumRows; Index++) + { + int ValueIndex = Keys.IsEmpty() ? Index : Keys[Index]; + AttrDest->Values[Index] = Attr->GetValueFromItemKey(ValueIndex); + } + + Attributes.Emplace(MoveTemp(AttrDest)); + } + break; + case EPCGMetadataTypes::Vector4: + { + auto* Attr = static_cast*>(AttrBase); + auto AttrDest = CreateAttributeVector4d(AttrName); + + AttrDest->Values.SetNum(NumRows); + for(int Index = 0; Index < NumRows; Index++) + { + int ValueIndex = Keys.IsEmpty() ? Index : Keys[Index]; + AttrDest->Values[Index] = Attr->GetValueFromItemKey(ValueIndex); + } + + Attributes.Emplace(MoveTemp(AttrDest)); + } + break; + case EPCGMetadataTypes::Quaternion: + { + auto* Attr = static_cast*>(AttrBase); + auto AttrDest = CreateAttributeVector4d(AttrName); + + AttrDest->Values.SetNum(NumRows); + for(int Index = 0; Index < NumRows; Index++) + { + int ValueIndex = Keys.IsEmpty() ? Index : Keys[Index]; + FQuat Quat = Attr->GetValueFromItemKey(ValueIndex); + AttrDest->Values[Index].X = Quat.X; + AttrDest->Values[Index].Y = Quat.Y; + AttrDest->Values[Index].Z = Quat.Z; + AttrDest->Values[Index].W = Quat.W; + } + + Attributes.Emplace(MoveTemp(AttrDest)); + } + break; + case EPCGMetadataTypes::String: + { + auto* Attr = static_cast*>(AttrBase); + auto AttrDest = CreateAttributeString(AttrName); + + AttrDest->Values.SetNum(NumRows); + for(int Index = 0; Index < NumRows; Index++) + { + int ValueIndex = Keys.IsEmpty() ? Index : Keys[Index]; + AttrDest->Values[Index] = Attr->GetValueFromItemKey(ValueIndex); + } + + Attributes.Emplace(MoveTemp(AttrDest)); + } + break; + case EPCGMetadataTypes::Name: + { + auto* Attr = static_cast*>(AttrBase); + auto AttrDest = CreateAttributeString(AttrName); + + AttrDest->Values.SetNum(NumRows); + for(int Index = 0; Index < NumRows; Index++) + { + int ValueIndex = Keys.IsEmpty() ? Index : Keys[Index]; + AttrDest->Values[Index] = Attr->GetValueFromItemKey(ValueIndex).ToString(); + } + + Attributes.Emplace(MoveTemp(AttrDest)); + } + break; + case EPCGMetadataTypes::SoftObjectPath: + { + auto* Attr = static_cast*>(AttrBase); + auto AttrDest = CreateAttributeSoftObjectPath(AttrName); + + AttrDest->Values.SetNum(NumRows); + for(int Index = 0; Index < NumRows; Index++) + { + int ValueIndex = Keys.IsEmpty() ? Index : Keys[Index]; + AttrDest->Values[Index] = Attr->GetValueFromItemKey(ValueIndex).ToString(); + } + + Attributes.Emplace(MoveTemp(AttrDest)); + } + break; + case EPCGMetadataTypes::SoftClassPath: + { + auto* Attr = static_cast*>(AttrBase); + auto AttrDest = CreateAttributeSoftClassPath(AttrName); + + AttrDest->Values.SetNum(NumRows); + for(int Index = 0; Index < NumRows; Index++) + { + int ValueIndex = Keys.IsEmpty() ? Index : Keys[Index]; + AttrDest->Values[Index] = Attr->GetValueFromItemKey(ValueIndex).ToString(); + } + + Attributes.Emplace(MoveTemp(AttrDest)); + } + break; + default: + break; + } + } +} + + +UHoudiniPCGDataAttributeBase* UHoudiniPCGDataObject::FindAttribute(const FString& AttrName) +{ + for (auto& Attr : Attributes) + { + if(Attr->GetAttrName() == AttrName) + return Attr.Get(); + } + return nullptr; +} + +UHoudiniPCGDataAttributeFloat* UHoudiniPCGDataObject::CreateAttributeFloat(const FString& AttributeName) +{ + auto* Result = NewObject(this, FName(AttributeName)); + Result->SetAttrName(AttributeName); + return Result; +} + +UHoudiniPCGDataAttributeDouble * UHoudiniPCGDataObject::CreateAttributeDouble(const FString& AttributeName) +{ + auto* Result = NewObject(this, FName(AttributeName)); + Result->SetAttrName(AttributeName); + return Result; +} + +UHoudiniPCGDataAttributeInt* UHoudiniPCGDataObject::CreateAttributeInt(const FString& AttributeName) +{ + auto* Result = NewObject(this, FName(AttributeName)); + Result->SetAttrName(AttributeName); + return Result; +} + +UHoudiniPCGDataAttributeInt64* UHoudiniPCGDataObject::CreateAttributeInt64(const FString& AttributeName) +{ + auto* Result = NewObject(this, FName(AttributeName)); + Result->SetAttrName(AttributeName); + return Result; +} + +UHoudiniPCGDataAttributeString* UHoudiniPCGDataObject::CreateAttributeString(const FString& AttributeName) +{ + auto* Result = NewObject(this, FName(AttributeName)); + Result->SetAttrName(AttributeName); + return Result; +} + +UHoudiniPCGDataAttributeVector2d * UHoudiniPCGDataObject::CreateAttributeVector2d(const FString& AttributeName) +{ + auto* Result = NewObject(this, FName(AttributeName)); + Result->SetAttrName(AttributeName); + return Result; +} + +UHoudiniPCGDataAttributeVector3d* UHoudiniPCGDataObject::CreateAttributeVector3d(const FString& AttributeName) +{ + auto* Result = NewObject(this, FName(AttributeName)); + Result->SetAttrName(AttributeName); + return Result; +} + +UHoudiniPCGDataAttributeVector4d* UHoudiniPCGDataObject::CreateAttributeVector4d(const FString& AttributeName) +{ + auto* Result = NewObject(this, FName(AttributeName)); + Result->SetAttrName(AttributeName); + return Result; +} + +UHoudiniPCGDataAttributeQuat * UHoudiniPCGDataObject::CreateAttributeQuat(const FString& AttributeName) +{ + auto* Result = NewObject(this, FName(AttributeName)); + Result->SetAttrName(AttributeName); + return Result; +} + +UHoudiniPCGDataAttributeSoftObjectPath * UHoudiniPCGDataObject::CreateAttributeSoftObjectPath(const FString& AttributeName) +{ + auto* Result = NewObject(this, FName(AttributeName)); + Result->SetAttrName(AttributeName); + return Result; +} + +UHoudiniPCGDataAttributeSoftClassPath* UHoudiniPCGDataObject::CreateAttributeSoftClassPath(const FString& AttributeName) +{ + auto* Result = NewObject(this, FName(AttributeName)); + Result->SetAttrName(AttributeName); + return Result; +} + +int UHoudiniPCGDataObject::GetNumRows() const +{ + if(Attributes.IsEmpty()) + return 0; + return Attributes[0]->GetNumValues(); +} + + +bool UHoudiniPCGDataCollection::operator==(const UHoudiniPCGDataCollection& Other) const +{ + auto CheckObjects = [](UHoudiniPCGDataObject* Obj1, UHoudiniPCGDataObject*Obj2) + { + if(IsValid(Obj1) && IsValid(Obj2)) + { + return (*Obj1) == (*Obj2); + } + else if(Obj1 == nullptr && Obj2 == nullptr) + { + return true; + } + else + { + return false; + } + }; + + bool bSame = true; + bSame &= CheckObjects(this->Details, Other.Details); + bSame &= CheckObjects(this->Vertices, Other.Vertices); + bSame &= CheckObjects(this->Primitives, Other.Primitives); + bSame &= CheckObjects(this->Points, Other.Points); + return bSame; + + +} + +bool UHoudiniPCGDataCollection::operator!=(const UHoudiniPCGDataCollection& Other) const +{ + return !(*this == Other); +} + +void UHoudiniPCGDataCollection::AddObject(UHoudiniPCGDataObject* Object) +{ + if(Object->PCGDataType == EPCGDataType::Point) + { + Type = EHoudiniPCGDataType::InputPCGGeometry; + Points = Object; + } + else if (Object->PCGDataType == EPCGDataType::Spline) + { + Type = EHoudiniPCGDataType::InputPCGSplines; + Splines.Add(Object); + } + else if (Object->PCGTags.Contains(TEXT("Vertices"))) + { + Type = EHoudiniPCGDataType::InputPCGGeometry; + Vertices = Object; + } + else if(Object->PCGTags.Contains(TEXT("Primitives"))) + { + Type = EHoudiniPCGDataType::InputPCGGeometry; + Primitives = Object; + } + else if(Object->PCGTags.Contains(TEXT("Details"))) + { + Type = EHoudiniPCGDataType::InputPCGGeometry; + Details = Object; + } +} + +UHoudiniPCGOutputData::UHoudiniPCGOutputData(class FObjectInitializer const& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniPDGAssetLink.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPDGAssetLink.h" + +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniLandscapeRuntimeUtils.h" +#include "HoudiniOutput.h" + +#include "Components/HierarchicalInstancedStaticMeshComponent.h" +#include "Engine/StaticMesh.h" +#include "GameFramework/Actor.h" +#include "InstancedFoliageActor.h" +#include "Landscape.h" +#include "UObject/MetaData.h" + +#if WITH_EDITOR + #include "FileHelpers.h" + #include "EditorModeManager.h" + #include "EditorModes.h" +#endif + +// +UHoudiniPDGAssetLink::UHoudiniPDGAssetLink(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , AssetName() + , AssetNodePath() + , AssetID(-1) + , SelectedTOPNetworkIndex(-1) + , LinkState(EPDGLinkState::Inactive) + , bAutoCook(false) + , bUseTOPNodeFilter(true) + , bUseTOPOutputFilter(true) + , NumWorkItems(0) + , WorkItemTally() + , OutputCachePath() + , bNeedsUIRefresh(false) + , OutputParentActor(nullptr) + , NumAttemptedNodeAutoBakes(0) + , NumSuccessfulNodeAutoBakes(0) +{ + TOPNodeFilter = HAPI_UNREAL_PDG_DEFAULT_TOP_FILTER; + TOPOutputFilter = HAPI_UNREAL_PDG_DEFAULT_TOP_OUTPUT_FILTER; + +#if WITH_EDITORONLY_DATA + bBakeMenuExpanded = true; + HoudiniEngineBakeOption = EHoudiniEngineBakeOption::ToActor; + PDGBakeSelectionOption = EPDGBakeSelectionOption::All; + PDGBakePackageReplaceMode = EPDGBakePackageReplaceModeOption::ReplaceExistingAssets; + bRecenterBakedActors = false; + bBakeAfterAllWorkResultObjectsLoaded = false; + bAutoBakeNodesWithFailedWorkItems = true; +#endif + + // Folder used for baking PDG outputs + BakeFolder.Path = HAPI_UNREAL_DEFAULT_BAKE_FOLDER; + + // TODO: + // Update init, move default filter to PCH +} + +FTOPWorkResultObject::FTOPWorkResultObject() +{ + // ResultObjects = nullptr; + Name = FString(); + FilePath = FString(); + State = EPDGWorkResultState::None; + WorkItemResultInfoIndex = INDEX_NONE; + bAutoBakedSinceLastLoad = false; +} + +FTOPWorkResultObject::~FTOPWorkResultObject() +{ + // DestroyResultOutputs(); +} + +FTOPWorkResult::FTOPWorkResult() +{ + WorkItemIndex = -1; + WorkItemID = -1; + + ResultObjects.SetNum(0); +} + +bool +FTOPWorkResult::operator==(const FTOPWorkResult& OtherWorkResult) const +{ + if (WorkItemIndex != OtherWorkResult.WorkItemIndex) + return false; + if (WorkItemID != OtherWorkResult.WorkItemID) + return false; + /* + if (ResultObjects != OtherWorkResult.ResultObjects) + return false; + */ + + return true; +} + +void +FTOPWorkResult::ClearAndDestroyResultObjects(const FGuid& InHoudiniComponentGuid) +{ + if (ResultObjects.Num() <= 0) + return; + + for (FTOPWorkResultObject& ResultObject : ResultObjects) + { + ResultObject.DestroyResultOutputsAndRemoveOutputActor(InHoudiniComponentGuid); + } + + ResultObjects.Empty(); +} + +int32 +FTOPWorkResult::IndexOfWorkResultObjectByHAPIResultInfoIndex(const int32& InWorkItemResultInfoIndex) +{ + const int32 NumEntries = ResultObjects.Num(); + for (int32 Index = 0; Index < NumEntries; ++Index) + { + const FTOPWorkResultObject& CurResultObject = ResultObjects[Index]; + if (CurResultObject.WorkItemResultInfoIndex == InWorkItemResultInfoIndex) + { + return Index; + } + } + + return INDEX_NONE; +} + +FTOPWorkResultObject* +FTOPWorkResult::GetWorkResultObjectByHAPIResultInfoIndex(const int32& InWorkItemResultInfoIndex) +{ + const int32 ArrayIndex = IndexOfWorkResultObjectByHAPIResultInfoIndex(InWorkItemResultInfoIndex); + if (ArrayIndex == INDEX_NONE) + { + return nullptr; + } + + return GetWorkResultObjectByArrayIndex(ArrayIndex); +} + +FTOPWorkResultObject* +FTOPWorkResult::GetWorkResultObjectByArrayIndex(const int32& InArrayIndex) +{ + if (!ResultObjects.IsValidIndex(InArrayIndex)) + { + return nullptr; + } + + return &ResultObjects[InArrayIndex]; +} + + +FWorkItemTallyBase::~FWorkItemTallyBase() +{ +} + +bool +FWorkItemTallyBase::AreAllWorkItemsComplete() const +{ + return ( + NumWaitingWorkItems() == 0 && NumCookingWorkItems() == 0 && NumScheduledWorkItems() == 0 + && (NumWorkItems() == (NumCookedWorkItems() + NumErroredWorkItems())) ); +} + +bool +FWorkItemTallyBase::AnyWorkItemsFailed() const +{ + return NumErroredWorkItems() > 0; +} + +bool +FWorkItemTallyBase::AnyWorkItemsPending() const +{ + return (NumWorkItems() > 0 && (NumWaitingWorkItems() > 0 || NumCookingWorkItems() > 0 || NumScheduledWorkItems() > 0)); +} + +FString +FWorkItemTallyBase::ProgressRatio() const +{ + const float Ratio = NumWorkItems() > 0 ? (NumCookedWorkItems() / NumWorkItems()) * 100.f : 0; + + return FString::Printf(TEXT("%.1f%%"), Ratio); +} + + +FWorkItemTally::FWorkItemTally() +{ + AllWorkItems.Empty(); + WaitingWorkItems.Empty(); + ScheduledWorkItems.Empty(); + CookingWorkItems.Empty(); + CookedWorkItems.Empty(); + ErroredWorkItems.Empty(); + CookCancelledWorkItems.Empty(); +} + +void +FWorkItemTally::ZeroAll() +{ + AllWorkItems.Empty(); + WaitingWorkItems.Empty(); + ScheduledWorkItems.Empty(); + CookingWorkItems.Empty(); + CookedWorkItems.Empty(); + ErroredWorkItems.Empty(); + CookCancelledWorkItems.Empty(); +} + +void +FWorkItemTally::RemoveWorkItem(int32 InWorkItemID) +{ + RemoveWorkItemFromAllStateSets(InWorkItemID); + AllWorkItems.Remove(InWorkItemID); +} + +void +FWorkItemTally::RecordWorkItemAsWaiting(int32 InWorkItemID) +{ + RemoveWorkItemFromAllStateSets(InWorkItemID); + WaitingWorkItems.Add(InWorkItemID); + AllWorkItems.Add(InWorkItemID); +} + +void +FWorkItemTally::RecordWorkItemAsScheduled(int32 InWorkItemID) +{ + RemoveWorkItemFromAllStateSets(InWorkItemID); + ScheduledWorkItems.Add(InWorkItemID); + AllWorkItems.Add(InWorkItemID); +} + +void +FWorkItemTally::RecordWorkItemAsCooking(int32 InWorkItemID) +{ + RemoveWorkItemFromAllStateSets(InWorkItemID); + CookingWorkItems.Add(InWorkItemID); + AllWorkItems.Add(InWorkItemID); +} + +void +FWorkItemTally::RecordWorkItemAsCooked(int32 InWorkItemID) +{ + RemoveWorkItemFromAllStateSets(InWorkItemID); + CookedWorkItems.Add(InWorkItemID); + AllWorkItems.Add(InWorkItemID); +} + +void +FWorkItemTally::RecordWorkItemAsErrored(int32 InWorkItemID) +{ + RemoveWorkItemFromAllStateSets(InWorkItemID); + ErroredWorkItems.Add(InWorkItemID); + AllWorkItems.Add(InWorkItemID); +} + +void +FWorkItemTally::RecordWorkItemAsCookCancelled(int32 InWorkItemID) +{ + RemoveWorkItemFromAllStateSets(InWorkItemID); + CookCancelledWorkItems.Add(InWorkItemID); + AllWorkItems.Add(InWorkItemID); +} + +void +FWorkItemTally::RemoveWorkItemFromAllStateSets(int32 InWorkItemID) +{ + WaitingWorkItems.Remove(InWorkItemID); + ScheduledWorkItems.Remove(InWorkItemID); + CookingWorkItems.Remove(InWorkItemID); + CookedWorkItems.Remove(InWorkItemID); + ErroredWorkItems.Remove(InWorkItemID); + CookCancelledWorkItems.Remove(InWorkItemID); +} + + +FAggregatedWorkItemTally::FAggregatedWorkItemTally() +{ + TotalWorkItems = 0; + WaitingWorkItems = 0; + ScheduledWorkItems = 0; + CookingWorkItems = 0; + CookedWorkItems = 0; + ErroredWorkItems = 0; + CookCancelledWorkItems = 0; +} + +void +FAggregatedWorkItemTally::ZeroAll() +{ + TotalWorkItems = 0; + WaitingWorkItems = 0; + ScheduledWorkItems = 0; + CookingWorkItems = 0; + CookedWorkItems = 0; + ErroredWorkItems = 0; + CookCancelledWorkItems = 0; +} + +void +FAggregatedWorkItemTally::Add(const FWorkItemTallyBase& InWorkItemTally) +{ + TotalWorkItems += InWorkItemTally.NumWorkItems(); + WaitingWorkItems += InWorkItemTally.NumWaitingWorkItems(); + ScheduledWorkItems += InWorkItemTally.NumScheduledWorkItems(); + CookingWorkItems += InWorkItemTally.NumCookingWorkItems(); + CookedWorkItems += InWorkItemTally.NumCookedWorkItems(); + ErroredWorkItems += InWorkItemTally.NumErroredWorkItems(); + CookCancelledWorkItems += InWorkItemTally.NumCookCancelledWorkItems(); +} + +void +FAggregatedWorkItemTally::Subtract(const FWorkItemTallyBase& InWorkItemTally) +{ + TotalWorkItems -= InWorkItemTally.NumWorkItems(); + WaitingWorkItems -= InWorkItemTally.NumWaitingWorkItems(); + ScheduledWorkItems -= InWorkItemTally.NumScheduledWorkItems(); + CookingWorkItems -= InWorkItemTally.NumCookingWorkItems(); + CookedWorkItems -= InWorkItemTally.NumCookedWorkItems(); + ErroredWorkItems -= InWorkItemTally.NumErroredWorkItems(); + CookCancelledWorkItems -= InWorkItemTally.NumCookCancelledWorkItems(); +} + + +UTOPNode::UTOPNode() +{ + NodeId = -1; + NodeName = FString(); + NodePath = FString(); + ParentName = FString(); + + WorkResultParent = nullptr; + WorkResult.SetNum(0); + + bHidden = false; + bAutoLoad = false; + + NodeState = EPDGNodeState::None; + + bCachedHaveNotLoadedWorkResults = false; + bCachedHaveLoadedWorkResults = false; + bHasChildNodes = false; + + bShow = false; + + bHasReceivedCookCompleteEvent = false; + + InvalidateLandscapeCache(); +} + +bool +UTOPNode::operator==(const UTOPNode& Other) const +{ + if (!NodeName.Equals(Other.NodeName)) + return false; + + if (!ParentName.Equals(Other.ParentName)) + return false; + + //if (NodeId != Other.NodeId) + // return false; + + return true; +} + +void +UTOPNode::Reset() +{ + NodeState = EPDGNodeState::None; + WorkItemTally.ZeroAll(); + AggregatedWorkItemTally.ZeroAll(); +} + +UHoudiniPDGAssetLink* UTOPNode::GetOuterAssetLink() const +{ + return GetTypedOuter(); +} + +void UTOPNode::OnWorkItemWaiting(int32 InWorkItemID) +{ + FTOPWorkResult* const WorkItem = GetWorkResultByID(InWorkItemID); + if (WorkItem) + { + // Clear the bAutoBakedSinceLastLoad flag on the work results since we are expecting a cook of the work item + for (FTOPWorkResultObject& WRO : WorkItem->ResultObjects) + { + WRO.SetAutoBakedSinceLastLoad(false); + } + } + WorkItemTally.RecordWorkItemAsWaiting(InWorkItemID); +} + +void +UTOPNode::OnWorkItemCooked(int32 InWorkItemID) +{ + if (GetWorkItemTally().NumCookedWorkItems() == 0) + { + // We want to invalidate the landscape cache values in any situation where + // all the work items are being recooked. + InvalidateLandscapeCache(); + } + WorkItemTally.RecordWorkItemAsCooked(InWorkItemID); +} + +void +UTOPNode::SetVisibleInLevel(bool bInVisible) +{ + if (bShow == bInVisible) + return; + + bShow = bInVisible; + UpdateOutputVisibilityInLevel(); +} + +void +UTOPNode::UpdateOutputVisibilityInLevel() +{ + AActor* Actor = OutputActorOwner.GetOutputActor(); + if (IsValid(Actor)) + { + Actor->SetHidden(!bShow); +#if WITH_EDITOR + Actor->SetIsTemporarilyHiddenInEditor(!bShow); +#endif + } + for (FTOPWorkResult& WorkItem : WorkResult) + { + for (FTOPWorkResultObject& WRO : WorkItem.ResultObjects) + { + AActor* WROActor = WRO.GetOutputActorOwner().GetOutputActor(); + if (IsValid(WROActor)) + { + WROActor->SetHidden(!bShow); +#if WITH_EDITOR + WROActor->SetIsTemporarilyHiddenInEditor(!bShow); +#endif + } + + // We need to manually handle child landscape's visiblity + for (UHoudiniOutput* ResultOutput : WRO.GetResultOutputs()) + { + if (!IsValid(ResultOutput)) + continue; + + for (auto& Pair : ResultOutput->GetOutputObjects()) + { + FHoudiniOutputObject& OutputObject = Pair.Value; + ALandscapeProxy* LandscapeProxy = Cast(OutputObject.OutputObject); + if (!IsValid(LandscapeProxy)) + continue; + + ALandscape* Landscape = LandscapeProxy->GetLandscapeActor(); + if (!IsValid(Landscape)) + continue; + + Landscape->SetHidden(!bShow); +#if WITH_EDITOR + Landscape->SetIsTemporarilyHiddenInEditor(!bShow); +#endif + } + } + } + } +} + +void +UTOPNode::SetNotLoadedWorkResultsToLoad(bool bInAlsoSetDeletedToLoad) +{ + for (FTOPWorkResult& WorkItem : WorkResult) + { + for (FTOPWorkResultObject& WRO : WorkItem.ResultObjects) + { + if (WRO.State == EPDGWorkResultState::NotLoaded || + (WRO.State == EPDGWorkResultState::Deleted && bInAlsoSetDeletedToLoad)) + { + WRO.State = EPDGWorkResultState::ToLoad; + WRO.SetAutoBakedSinceLastLoad(false); + } + } + } +} + +void +UTOPNode::SetLoadedWorkResultsToDelete() +{ + for (FTOPWorkResult& WorkItem : WorkResult) + { + for (FTOPWorkResultObject& WRO : WorkItem.ResultObjects) + { + if (WRO.State == EPDGWorkResultState::Loaded) + WRO.State = EPDGWorkResultState::ToDelete; + } + } +} + +FGuid +UTOPNode::GetHoudiniCookableGuid() const +{ + UHoudiniPDGAssetLink const* const AssetLink = GetOuterAssetLink(); + if (!IsValid(AssetLink)) + return FGuid(); + + return AssetLink->GetOuterCookableGuid(); +} + +void +UTOPNode::DeleteWorkResultObjectOutputs(const int32 InWorkResultArrayIndex, const int32 InWorkResultObjectArrayIndex, const bool bInDeleteOutputActors) +{ + if (!WorkResult.IsValidIndex(InWorkResultArrayIndex)) + return; + + FTOPWorkResult& WorkItem = WorkResult[InWorkResultArrayIndex]; + if (!WorkItem.ResultObjects.IsValidIndex(InWorkResultObjectArrayIndex)) + return; + + FTOPWorkResultObject& WRO = WorkItem.ResultObjects[InWorkResultObjectArrayIndex]; + // Delete and clean up that WRObj + WRO.DestroyResultOutputs(GetHoudiniCookableGuid()); + if (bInDeleteOutputActors) + WRO.GetOutputActorOwner().DestroyOutputActor(); + WRO.State = EPDGWorkResultState::Deleted; + + // Ensure that the outer level (or actor in the case of OFPA) is marked as dirty so that references to the + // output actors / objects are saved + MarkPackageDirty(); +} + +void +UTOPNode::DeleteWorkItemOutputs(const int32 InWorkResultArrayIndex, const bool bInDeleteOutputActors) +{ + if (!WorkResult.IsValidIndex(InWorkResultArrayIndex)) + return; + + const FTOPWorkResult& WorkItem = WorkResult[InWorkResultArrayIndex]; + const int32 NumResultObjects = WorkItem.ResultObjects.Num(); + for (int32 ResultObjectIndex = 0; ResultObjectIndex < NumResultObjects; ++ResultObjectIndex) + { + DeleteWorkResultObjectOutputs(InWorkResultArrayIndex, ResultObjectIndex, bInDeleteOutputActors); + } +} + +void +UTOPNode::DeleteAllWorkResultObjectOutputs(const bool bInDeleteOutputActors) +{ + const int32 NumWorkItems = WorkResult.Num(); + for (int32 WorkItemIndex = 0; WorkItemIndex < NumWorkItems; ++WorkItemIndex) + { + DeleteWorkItemOutputs(WorkItemIndex, bInDeleteOutputActors); + } + bCachedHaveLoadedWorkResults = false; +} + +FString +UTOPNode::GetBakedWorkResultObjectOutputsKey(int32 InWorkResultArrayIndex, int32 InWorkResultObjectArrayIndex) +{ + return FString::Printf(TEXT("%d_%d"), InWorkResultArrayIndex, InWorkResultObjectArrayIndex); +} + +FString +UTOPNode::GetBakedWorkResultObjectOutputsKey(const FTOPWorkResult& InWorkResult, int32 InWorkResultObjectArrayIndex) const +{ + if (InWorkResult.WorkItemID == INDEX_NONE) + return FString(); + + const int32 WorkResultArrayIndex = ArrayIndexOfWorkResultByID(InWorkResult.WorkItemID); + if (WorkResultArrayIndex == INDEX_NONE) + return FString(); + + return GetBakedWorkResultObjectOutputsKey(WorkResultArrayIndex, InWorkResultObjectArrayIndex); +} + +bool +UTOPNode::GetBakedWorkResultObjectOutputsKey(int32 InWorkResultArrayIndex, int32 InWorkResultObjectArrayIndex, FString& OutKey) const +{ + // Check that indices are valid + if (!WorkResult.IsValidIndex(InWorkResultArrayIndex)) + return false; + const FTOPWorkResult& WorkResultEntry = WorkResult[InWorkResultArrayIndex]; + if (!WorkResultEntry.ResultObjects.IsValidIndex(InWorkResultObjectArrayIndex)) + return false; + + OutKey = GetBakedWorkResultObjectOutputsKey(InWorkResultArrayIndex, InWorkResultObjectArrayIndex); + + return true; +} + +bool +UTOPNode::GetBakedWorkResultObjectOutputs(int32 InWorkResultArrayIndex, int32 InWorkResultObjectArrayIndex, FHoudiniPDGWorkResultObjectBakedOutput*& OutBakedOutput) +{ + FString Key; + if (!GetBakedWorkResultObjectOutputsKey(InWorkResultArrayIndex, InWorkResultObjectArrayIndex, Key)) + return false; + OutBakedOutput = BakedWorkResultObjectOutputs.Find(Key); + if (!OutBakedOutput) + return false; + + return true; +} + +bool +UTOPNode::GetBakedWorkResultObjectOutputs(int32 InWorkResultArrayIndex, int32 InWorkResultObjectArrayIndex, FHoudiniPDGWorkResultObjectBakedOutput const*& OutBakedOutput) const +{ + FString Key; + if (!GetBakedWorkResultObjectOutputsKey(InWorkResultArrayIndex, InWorkResultObjectArrayIndex, Key)) + return false; + OutBakedOutput = BakedWorkResultObjectOutputs.Find(Key); + if (!OutBakedOutput) + return false; + + return true; +} + +int32 +UTOPNode::ArrayIndexOfWorkResultByID(const int32& InWorkItemID) const +{ + const int32 NumEntries = WorkResult.Num(); + for (int32 Index = 0; Index < NumEntries; ++Index) + { + const FTOPWorkResult& CurResult = WorkResult[Index]; + if (CurResult.WorkItemID == InWorkItemID) + { + return Index; + } + } + + return INDEX_NONE; +} + +FTOPWorkResult* +UTOPNode::GetWorkResultByID(const int32& InWorkItemID) +{ + const int32 ArrayIndex = ArrayIndexOfWorkResultByID(InWorkItemID); + if (!WorkResult.IsValidIndex(ArrayIndex)) + return nullptr; + + return &WorkResult[ArrayIndex]; +} + +int32 +UTOPNode::ArrayIndexOfFirstInvalidWorkResult() const +{ + const int32 NumEntries = WorkResult.Num(); + for (int32 Index = 0; Index < NumEntries; ++Index) + { + const FTOPWorkResult& CurResult = WorkResult[Index]; + if (CurResult.WorkItemID == INDEX_NONE) + { + return Index; + } + } + + return INDEX_NONE; +} + +FTOPWorkResult* +UTOPNode::GetWorkResultByArrayIndex(const int32& InArrayIndex) +{ + if (!WorkResult.IsValidIndex(InArrayIndex)) + return nullptr; + return &WorkResult[InArrayIndex]; +} + +bool +UTOPNode::IsParentTOPNetwork(UTOPNetwork const * const InNetwork) const +{ + if (!IsValid(InNetwork)) + { + return false; + } + + return ParentName == FString::Printf(TEXT("%s_%s"), *InNetwork->ParentName, *InNetwork->NodeName); +} + +bool +UTOPNode::CanStillBeAutoBaked(const bool bInAutoBakeWithFailedWorkItems) const +{ + // Only nodes that have results auto-loaded are auto-baked + if (!bAutoLoad) + return false; + + // Nodes with failures are not auto-baked depending on the value of bAutoBakeWithFailedWorkItems + if (!bInAutoBakeWithFailedWorkItems && AnyWorkItemsFailed()) + return false; + + // All work items are not yet complete, so node cannot yet be baked + if (!AreAllWorkItemsComplete()) + return true; + + // Work items that are currently loaded or has not tagged has auto baked since last load can still be baked + for (const FTOPWorkResult& WorkResultEntry : WorkResult) + { + for (const FTOPWorkResultObject& WRO : WorkResultEntry.ResultObjects) + { + switch (WRO.State) + { + case EPDGWorkResultState::NotLoaded: + case EPDGWorkResultState::ToLoad: + case EPDGWorkResultState::Loading: + return true; + case EPDGWorkResultState::Loaded: + if (!WRO.AutoBakedSinceLastLoad()) + return true; + break; + case EPDGWorkResultState::ToDelete: + case EPDGWorkResultState::Deleting: + case EPDGWorkResultState::Deleted: + case EPDGWorkResultState::None: + break; + } + } + } + + return false; +} + +#if WITH_EDITOR +void +UTOPNode::PostEditChangeChainProperty(FPropertyChangedChainEvent& InPropertyChangedEvent) +{ + Super::PostEditChangeChainProperty(InPropertyChangedEvent); + + const FName PropertyName = InPropertyChangedEvent.GetPropertyName(); + if (PropertyName == GET_MEMBER_NAME_CHECKED(UTOPNode, bShow)) + { + UpdateOutputVisibilityInLevel(); + } +} +#endif + +#if WITH_EDITOR +void +UTOPNode::PostTransacted(const FTransactionObjectEvent& TransactionEvent) +{ + Super::PostTransacted(TransactionEvent); + + if (TransactionEvent.GetEventType() != ETransactionObjectEventType::UndoRedo) + return; + + bool bUpdateVisibility = false; + for (const FName& PropName : TransactionEvent.GetChangedProperties()) + { + if (PropName == GET_MEMBER_NAME_CHECKED(UTOPNode, bShow)) + { + bUpdateVisibility = true; + } + } + + if (bUpdateVisibility) + UpdateOutputVisibilityInLevel(); +} +#endif + +void +UTOPNode::OnDirtyNode() +{ + InvalidateLandscapeCache(); + bHasReceivedCookCompleteEvent = false; +} + +void +UTOPNode::InvalidateLandscapeCache() +{ + ClearedLayers.Empty(); +} + +UTOPNetwork::UTOPNetwork() +{ + NodeId = -1; + NodeName = FString(); + + AllTOPNodes.SetNum(0); + SelectedTOPIndex = -1; + + ParentName = FString(); + + bShowResults = false; + bAutoLoadResults = false; +} + +bool +UTOPNetwork::operator==(const UTOPNetwork& Other) const +{ + if (!NodeName.Equals(Other.NodeName)) + return false; + + if (!ParentName.Equals(Other.ParentName)) + return false; + + //if (NodeId != Other.NodeId) + // return false; + + return true; +} + +void +UTOPNetwork::SetLoadedWorkResultsToDelete() +{ + for (UTOPNode* Node : AllTOPNodes) + { + if (!IsValid(Node)) + continue; + + Node->SetLoadedWorkResultsToDelete(); + } +} + +void +UTOPNetwork::DeleteAllWorkResultObjectOutputs() +{ + for (UTOPNode* Node : AllTOPNodes) + { + if (!IsValid(Node)) + continue; + + Node->DeleteAllWorkResultObjectOutputs(); + } +} + +bool +UTOPNetwork::AnyWorkItemsPending() const +{ + for (const UTOPNode* const TOPNode : AllTOPNodes) + { + if (!IsValid(TOPNode)) + continue; + + if (TOPNode->AnyWorkItemsPending()) + return true; + } + + return false; +} + +bool +UTOPNetwork::AnyWorkItemsFailed() const +{ + for (const UTOPNode* const TOPNode : AllTOPNodes) + { + if (!IsValid(TOPNode)) + continue; + + if (TOPNode->AnyWorkItemsFailed()) + return true; + } + + return false; +} + +bool +UTOPNetwork::CanStillBeAutoBaked(const bool bInAutoBakeWithFailedWorkItems) const +{ + // As long as there are still pending work items in the network, something could still be auto baked + if (AnyWorkItemsPending()) + return true; + + for (const UTOPNode* const TOPNode : AllTOPNodes) + { + if (TOPNode->CanStillBeAutoBaked(bInAutoBakeWithFailedWorkItems)) + return true; + } + + return false; +} + +void +UTOPNetwork::HandleOnPDGEventCookCompleteReceivedByChildNode(UHoudiniPDGAssetLink* const InAssetLink, UTOPNode* const InTOPNode) +{ + if (!IsValid(InAssetLink)) + return; + + // Ensure that the work item tally is up to date, since we rely on it below + InAssetLink->UpdateWorkItemTally(); + + // Check if all nodes have received the HAPI_PDG_EVENT_COOK_COMPLETE event, if so, broadcast the OnPostCook handler. + for (const UTOPNode* const TOPNode : AllTOPNodes) + { + if (!IsValid(TOPNode)) + continue; + + // For nodes with child nodes, only the child nodes receive HAPI_PDG_EVENT_COOK_COMPLETE + if (TOPNode->bHasChildNodes) + continue; + + // If the node has not received cook complete, and it has work items, then the network is not done cooking + // (Nodes that are bypassed in the network, or that don't have any work items generated, don't receive + // HAPI_PDG_EVENT_COOK_COMPLETE) + if (!TOPNode->HasReceivedCookCompleteEvent() && TOPNode->GetWorkItemTally().NumWorkItems() > 0) + return; + } + + if (OnPostCookDelegate.IsBound()) + OnPostCookDelegate.Broadcast(this, AnyWorkItemsFailed()); + + InAssetLink->HandleOnTOPNetworkCookComplete(this); +} + +void +UHoudiniPDGAssetLink::SelectTOPNetwork(const int32& AtIndex) +{ + if (!AllTOPNetworks.IsValidIndex(AtIndex)) + return; + + SelectedTOPNetworkIndex = AtIndex; +} + +UWorld* +UHoudiniPDGAssetLink::GetOutputWorld() +{ + if(IsValid(OutputWorld)) + return OutputWorld; + else + return GetWorld(); +} + +void +UHoudiniPDGAssetLink::SetOutputWorld(UWorld * World) +{ + OutputWorld = World; +} + +void +UHoudiniPDGAssetLink::SelectTOPNode(UTOPNetwork* InTOPNetwork, const int32& AtIndex) +{ + if (!IsValid(InTOPNetwork)) + return; + + if (!InTOPNetwork->AllTOPNodes.IsValidIndex(AtIndex)) + return; + + InTOPNetwork->SelectedTOPIndex = AtIndex; +} + + +UTOPNetwork* +UHoudiniPDGAssetLink::GetSelectedTOPNetwork() +{ + return GetTOPNetwork(SelectedTOPNetworkIndex); +} + +const UTOPNetwork* +UHoudiniPDGAssetLink::GetSelectedTOPNetwork() const +{ + return GetTOPNetwork(SelectedTOPNetworkIndex); +} + +UTOPNode* +UHoudiniPDGAssetLink::GetSelectedTOPNode() +{ + UTOPNetwork* const SelectedTOPNetwork = GetSelectedTOPNetwork(); + if (!IsValid(SelectedTOPNetwork)) + return nullptr; + + if (!SelectedTOPNetwork->AllTOPNodes.IsValidIndex(SelectedTOPNetwork->SelectedTOPIndex)) + return nullptr; + + UTOPNode* const SelectedTOPNode = SelectedTOPNetwork->AllTOPNodes[SelectedTOPNetwork->SelectedTOPIndex]; + if (!IsValid(SelectedTOPNode)) + return nullptr; + + return SelectedTOPNode; +} + +const UTOPNode* +UHoudiniPDGAssetLink::GetSelectedTOPNode() const +{ + UTOPNetwork const* const SelectedTOPNetwork = GetSelectedTOPNetwork(); + if (!IsValid(SelectedTOPNetwork)) + return nullptr; + + if (!SelectedTOPNetwork->AllTOPNodes.IsValidIndex(SelectedTOPNetwork->SelectedTOPIndex)) + return nullptr; + + UTOPNode const* const SelectedTOPNode = SelectedTOPNetwork->AllTOPNodes[SelectedTOPNetwork->SelectedTOPIndex]; + if (!IsValid(SelectedTOPNode)) + return nullptr; + + return SelectedTOPNode; +} + +FString +UHoudiniPDGAssetLink::GetSelectedTOPNodeName() +{ + FString NodeName = FString(); + + const UTOPNode* const SelectedTOPNode = GetSelectedTOPNode(); + if (IsValid(SelectedTOPNode)) + NodeName = SelectedTOPNode->NodeName; + + return NodeName; +} + +FString +UHoudiniPDGAssetLink::GetSelectedTOPNetworkName() +{ + FString NetworkName = FString(); + + const UTOPNetwork* const SelectedTOPNetwork = GetSelectedTOPNetwork(); + if (IsValid(SelectedTOPNetwork)) + NetworkName = SelectedTOPNetwork->NodeName; + + return NetworkName; +} + +UTOPNetwork* +UHoudiniPDGAssetLink::GetTOPNetwork(const int32& AtIndex) +{ + if(AllTOPNetworks.IsValidIndex(AtIndex)) + { + return AllTOPNetworks[AtIndex]; + } + + return nullptr; +} + +const UTOPNetwork* +UHoudiniPDGAssetLink::GetTOPNetwork(const int32& AtIndex) const +{ + if(AllTOPNetworks.IsValidIndex(AtIndex)) + { + return AllTOPNetworks[AtIndex]; + } + + return nullptr; +} + +UTOPNetwork* +UHoudiniPDGAssetLink::GetTOPNetworkForNode(const UTOPNode* InNode) const +{ + if (!IsValid(InNode)) + return nullptr; + + // TODO: add a parent node pointer (or perhaps have the parent network as the outer / in the outer chain of the node?) + // to UTOPNode to prevent having to do these parent name and array Contains searches + const int32 NumTOPNet = AllTOPNetworks.Num(); + for (int32 Index = 0; Index < NumTOPNet; ++Index) + { + UTOPNetwork* const TOPNet = AllTOPNetworks[Index]; + if (!IsValid(TOPNet)) + continue; + + if (InNode->ParentName != TOPNet->ParentName + TEXT("_") + TOPNet->NodeName) + continue; + + if (TOPNet->AllTOPNodes.Contains(InNode)) + return TOPNet; + } + + return nullptr; +} + +UTOPNetwork* +UHoudiniPDGAssetLink::GetTOPNetworkByNodePath(const FString& InNodePath, const TArray& InTOPNetworks, int32& OutIndex) +{ + OutIndex = INDEX_NONE; + int32 Index = -1; + for (UTOPNetwork* CurrentTOPNet : InTOPNetworks) + { + Index += 1; + + if (!IsValid(CurrentTOPNet)) + continue; + + if (CurrentTOPNet->NodePath.Equals(InNodePath)) + { + OutIndex = Index; + return CurrentTOPNet; + } + } + + return nullptr; +} + +UTOPNode* +UHoudiniPDGAssetLink::GetParentTOPNode(const UTOPNode* InNode) +{ + if (!IsValid(InNode)) + return nullptr; + + FString NodePath = InNode->NodePath; + FString ParentPath; + + if (NodePath.EndsWith("/")) + NodePath.LeftChopInline(1); + + if (NodePath.Split("/", &ParentPath, nullptr, ESearchCase::IgnoreCase, ESearchDir::FromEnd) && !ParentPath.IsEmpty()) + { + for (UTOPNetwork* TOPNet : AllTOPNetworks) + { + if (!IsValid(TOPNet)) + continue; + + for (UTOPNode* TOPNode : TOPNet->AllTOPNodes) + { + if (!IsValid(TOPNode)) + continue; + + if (TOPNode->NodePath == ParentPath && InNode->NodeId != TOPNode->NodeId) + { + return TOPNode; + } + } + } + } + + return nullptr; +} + +UTOPNode* +UHoudiniPDGAssetLink::GetTOPNodeByNodePath(const FString& InNodePath, const TArray& InTOPNodes, int32& OutIndex) +{ + OutIndex = INDEX_NONE; + int32 Index = -1; + for (UTOPNode* CurrentTOPNode : InTOPNodes) + { + Index += 1; + + if (!IsValid(CurrentTOPNode)) + continue; + + if (CurrentTOPNode->NodePath.Equals(InNodePath)) + { + OutIndex = Index; + return CurrentTOPNode; + } + } + + return nullptr; +} + +void +UHoudiniPDGAssetLink::ClearAllTOPData() +{ + // Clears all TOP data + for(UTOPNetwork* CurrentNetwork : AllTOPNetworks) + { + if (!IsValid(CurrentNetwork)) + continue; + + for(UTOPNode* CurrentTOPNode : CurrentNetwork->AllTOPNodes) + { + if (!IsValid(CurrentTOPNode)) + continue; + + ClearTOPNodeWorkItemResults(CurrentTOPNode); + } + } + + AllTOPNetworks.Empty(); +} + +void +UHoudiniPDGAssetLink::ClearTOPNetworkWorkItemResults(UTOPNetwork* TOPNetwork) +{ + if (!IsValid(TOPNetwork)) + return; + + for(UTOPNode* CurrentTOPNode : TOPNetwork->AllTOPNodes) + { + if (!IsValid(CurrentTOPNode)) + continue; + + ClearTOPNodeWorkItemResults(CurrentTOPNode); + } +} + +void +UHoudiniPDGAssetLink::ClearTOPNodeWorkItemResults(UTOPNode* TOPNode) +{ + if (!IsValid(TOPNode)) + return; + + TOPNode->OnDirtyNode(); + + const FGuid CookableGuid(TOPNode->GetHoudiniCookableGuid()); + for(FTOPWorkResult& CurrentWorkResult : TOPNode->WorkResult) + { + CurrentWorkResult.ClearAndDestroyResultObjects(CookableGuid); + } + TOPNode->WorkResult.Empty(); + + FOutputActorOwner& OutputActorOwner = TOPNode->GetOutputActorOwner(); + AActor* OutputActor = OutputActorOwner.GetOutputActor(); + if (IsValid(OutputActor)) + { + // Destroy any attached actors (which we'll assume that any attachments left + // are untracked actors associated with the TOPNode) + TArray AttachedActors; + OutputActor->GetAttachedActors(AttachedActors); + for (AActor* Actor : AttachedActors) + { + if (!IsValid(Actor)) + continue; + Actor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); + Actor->Destroy(); + } + } + + if (IsValid(TOPNode->WorkResultParent)) + { + + // TODO: Destroy the Parent Object + // DestroyImmediate(topNode._workResultParentGO); + } + + OutputActorOwner.DestroyOutputActor(); + + // Ensure that the outer level (or actor in the case of OFPA) is marked as dirty so that changes to the + // output actors / objects are saved + TOPNode->MarkPackageDirty(); +} + + +void +UHoudiniPDGAssetLink::ClearWorkItemResultByID(const int32& InWorkItemID, UTOPNode* InTOPNode) +{ + if (!IsValid(InTOPNode)) + return; + + FTOPWorkResult* WorkResult = GetWorkResultByID(InWorkItemID, InTOPNode); + if (WorkResult) + { + WorkResult->ClearAndDestroyResultObjects(InTOPNode->GetHoudiniCookableGuid()); + // TODO: Should we destroy the FTOPWorkResult struct entirely here? + //TOPNode.WorkResult.RemoveByPredicate + + // Ensure that the outer level (or actor in the case of OFPA) is marked as dirty so that changes to the + // output actors / objects are saved + InTOPNode->MarkPackageDirty(); + } +} + +void +UHoudiniPDGAssetLink::DestroyWorkItemByID(const int32& InWorkItemID, UTOPNode* InTOPNode) +{ + if (!IsValid(InTOPNode)) + return; + + // TODO: Update ClearWorkItemResultByID or GetWorkResultByID to return the index of the work item + // so that we don't have to find its index again to remove it from the array + ClearWorkItemResultByID(InWorkItemID, InTOPNode); + // Find the index of the FTOPWorkResult for InWorkItemID in InTOPNode.WorkResult and remove it + const int32 Index = InTOPNode->WorkResult.IndexOfByPredicate( + [InWorkItemID](const FTOPWorkResult& InWorkItem) { return InWorkItem.WorkItemID == InWorkItemID; }); + if (Index != INDEX_NONE && Index >= 0) + InTOPNode->WorkResult.RemoveAt(Index); +} + +FTOPWorkResult* +UHoudiniPDGAssetLink::GetWorkResultByID(const int32& InWorkItemID, UTOPNode* InTOPNode) +{ + if (!IsValid(InTOPNode)) + return nullptr; + return InTOPNode->GetWorkResultByID(InWorkItemID); +} + +FDirectoryPath +UHoudiniPDGAssetLink::GetTemporaryCookFolder() const +{ + UHoudiniCookable* HC = GetOuterHoudiniCookable(); + if (HC) + return HC->GetTemporaryCookFolder(); + + FDirectoryPath TempPath; + TempPath.Path = FHoudiniEngineRuntime::Get().GetDefaultTemporaryCookFolder(); + return TempPath; +} + +FGuid +UHoudiniPDGAssetLink::GetOuterCookableGuid() const +{ + UHoudiniCookable* HC = GetOuterHoudiniCookable(); + if (!IsValid(HC)) + return FGuid(); + + return HC->GetCookableGUID(); +} + +void +UHoudiniPDGAssetLink::HandleOnTOPNetworkCookComplete(UTOPNetwork* const InTOPNet) +{ + if (!IsValid(InTOPNet)) + return; + + if (OnPostTOPNetworkCookDelegate.IsBound()) + { + OnPostTOPNetworkCookDelegate.Broadcast(this, InTOPNet, InTOPNet->AnyWorkItemsFailed()); + } +} + + +UTOPNode* +UHoudiniPDGAssetLink::GetTOPNode(const int32& InNodeID) +{ + UTOPNetwork* Network = nullptr; + UTOPNode* Node = nullptr; + + if (GetTOPNodeAndNetworkByNodeId(InNodeID, Network, Node)) + return Node; + + return nullptr; +} + + +bool +UHoudiniPDGAssetLink::GetTOPNodeAndNetworkByNodeId(const int32& InNodeID, UTOPNetwork*& OutNetwork, UTOPNode*& OutNode) +{ + OutNetwork = nullptr; + OutNode = nullptr; + + for (UTOPNetwork* CurrentTOPNet : AllTOPNetworks) + { + if (!IsValid(CurrentTOPNet)) + continue; + + for (UTOPNode* CurrentTOPNode : CurrentTOPNet->AllTOPNodes) + { + if (!IsValid(CurrentTOPNode)) + continue; + + if (CurrentTOPNode->NodeId == InNodeID) + { + OutNetwork = CurrentTOPNet; + OutNode = CurrentTOPNode; + return true; + } + } + } + + return false; +} + + +void +UHoudiniPDGAssetLink::UpdateTOPNodeWithChildrenWorkItemTallyAndState(UTOPNode* InNode, UTOPNetwork* InNetwork) +{ + if (!IsValid(InNode) || !IsValid(InNetwork)) + return; + + if (!InNode->bHasChildNodes) + return; + + FString PrefixPath = InNode->NodePath; + if (!PrefixPath.EndsWith("/")) + PrefixPath += "/"; + InNode->ZeroWorkItemTally(); + InNode->NodeState = EPDGNodeState::None; + + TMap NodeStateOrder; + NodeStateOrder.Add(EPDGNodeState::None, 0); + NodeStateOrder.Add(EPDGNodeState::Cook_Complete, 1); + NodeStateOrder.Add(EPDGNodeState::Dirtied, 2); + NodeStateOrder.Add(EPDGNodeState::Cook_Failed, 3); + NodeStateOrder.Add(EPDGNodeState::Dirtying, 4); + NodeStateOrder.Add(EPDGNodeState::Cooking, 5); + + int8 CurrentState = 0; + + for (const UTOPNode* Node : InNetwork->AllTOPNodes) + { + if (!IsValid(Node)) + continue; + + if (Node->NodePath.StartsWith(PrefixPath) && !Node->bHasChildNodes) + { + InNode->AggregateTallyFromChildNode(Node); + const int8 VisitedNodeState = NodeStateOrder.FindChecked(Node->NodeState); + if (VisitedNodeState > CurrentState) + CurrentState = VisitedNodeState; + } + } + + EPDGNodeState const* const NewState = NodeStateOrder.FindKey(CurrentState); + if (NewState) + InNode->NodeState = *NewState; +} + +void +UHoudiniPDGAssetLink::UpdateWorkItemTally() +{ + WorkItemTally.ZeroAll(); + for(UTOPNetwork* CurrentTOPNet : AllTOPNetworks) + { + if (!IsValid(CurrentTOPNet)) + continue; + + for(UTOPNode* CurrentTOPNode : CurrentTOPNet->AllTOPNodes) + { + if (!IsValid(CurrentTOPNode)) + continue; + + // Only add up the tallys from nodes without children (since parent's aggregate the child work items counts) + if (CurrentTOPNode->bHasChildNodes) + { + UpdateTOPNodeWithChildrenWorkItemTallyAndState(CurrentTOPNode, CurrentTOPNet); + } + else + { + WorkItemTally.Add(CurrentTOPNode->GetWorkItemTally()); + } + } + } +} + + +void +UHoudiniPDGAssetLink::ResetTOPNetworkWorkItemTally(UTOPNetwork* TOPNetwork) +{ + if (!IsValid(TOPNetwork)) + return; + + for (UTOPNode* CurTOPNode : TOPNetwork->AllTOPNodes) + { + if (!IsValid(CurTOPNode)) + continue; + + CurTOPNode->ZeroWorkItemTally(); + } +} + +FString +UHoudiniPDGAssetLink::GetTOPNodeStatus(const UTOPNode* InTOPNode) +{ + static const FString InvalidOrUnknownStatus = TEXT(""); + + if (!IsValid(InTOPNode)) + return InvalidOrUnknownStatus; + + if (InTOPNode->NodeState == EPDGNodeState::Cook_Failed || InTOPNode->AnyWorkItemsFailed()) + { + return TEXT("Cook Failed"); + } + else if (InTOPNode->NodeState == EPDGNodeState::Cook_Complete) + { + return TEXT("Cook Completed"); + } + else if (InTOPNode->NodeState == EPDGNodeState::Cooking) + { + return TEXT("Cook In Progress"); + } + else if (InTOPNode->NodeState == EPDGNodeState::Dirtied) + { + return TEXT("Dirtied"); + } + else if (InTOPNode->NodeState == EPDGNodeState::Dirtying) + { + return TEXT("Dirtying"); + } + + return InvalidOrUnknownStatus; +} + +FLinearColor +UHoudiniPDGAssetLink::GetTOPNodeStatusColor(const UTOPNode* InTOPNode) +{ + if (!IsValid(InTOPNode)) + return FLinearColor::White; + + if (InTOPNode->NodeState == EPDGNodeState::Cook_Failed || InTOPNode->AnyWorkItemsFailed()) + { + return FLinearColor::Red; + } + else if (InTOPNode->NodeState == EPDGNodeState::Cook_Complete) + { + return FLinearColor::Green; + } + else if (InTOPNode->NodeState == EPDGNodeState::Cooking) + { + return FLinearColor(0.0, 1.0f, 1.0f); + } + else if (InTOPNode->NodeState == EPDGNodeState::Dirtied) + { + return FLinearColor(1.0f, 0.5f, 0.0f); + } + else if (InTOPNode->NodeState == EPDGNodeState::Dirtying) + { + return FLinearColor::Yellow; + } + + return FLinearColor::White; +} + +AActor* +UHoudiniPDGAssetLink::GetOwnerActor() const +{ + UObject* Outer = GetOuter(); + UActorComponent* Component = Cast(Outer); + if (IsValid(Component)) + return Component->GetOwner(); + else + return Cast(Outer); +} + +bool +UHoudiniPDGAssetLink::HasTemporaryOutputs() const +{ + // Loop over all networks, all nodes, all work items and check for any valid output objects + for (const UTOPNetwork* TOPNetwork : AllTOPNetworks) + { + if (!IsValid(TOPNetwork)) + continue; + + for (const UTOPNode* TOPNode : TOPNetwork->AllTOPNodes) + { + if (!IsValid(TOPNode)) + continue; + + for (const FTOPWorkResult& WorkResult : TOPNode->WorkResult) + { + for (const FTOPWorkResultObject& WorkResultObject : WorkResult.ResultObjects) + { + // If the WorkResultObject's actor is not valid, then it no longer has temporary objects in the + // scene + if (!IsValid(WorkResultObject.GetOutputActorOwner().GetOutputActor())) + continue; + + for (UHoudiniOutput* Output : WorkResultObject.GetResultOutputs()) + { + if (!IsValid(Output)) + continue; + + const EHoudiniOutputType OutputType = Output->GetType(); + for (const auto& OutputObjectPair : Output->GetOutputObjects()) + { + if ((OutputType == EHoudiniOutputType::Landscape && IsValid(OutputObjectPair.Value.OutputObject)) || + OutputObjectPair.Value.OutputComponents.Num() > 0) + { + return true; + } + } + } + } + } + } + } + + return false; +} + +void +UHoudiniPDGAssetLink::UpdatePostDuplicate() +{ + // Loop over all networks, all nodes, all work items and clear output actors + for (UTOPNetwork* TOPNetwork : AllTOPNetworks) + { + if (!IsValid(TOPNetwork)) + continue; + + for (UTOPNode* TOPNode : TOPNetwork->AllTOPNodes) + { + if (!IsValid(TOPNode)) + continue; + + for (FTOPWorkResult& WorkResult : TOPNode->WorkResult) + { + for (FTOPWorkResultObject& WorkResultObject : WorkResult.ResultObjects) + { + WorkResultObject.GetOutputActorOwner().SetOutputActor(nullptr); + WorkResultObject.State = EPDGWorkResultState::None; + WorkResultObject.SetResultOutputs(TArray()); + } + } + TOPNode->GetOutputActorOwner().SetOutputActor(nullptr); + TOPNode->bCachedHaveNotLoadedWorkResults = false; + TOPNode->bCachedHaveLoadedWorkResults = false; + } + } +} + +UHoudiniCookable* +UHoudiniPDGAssetLink::GetOuterHoudiniCookable() const +{ + UHoudiniCookable* FoundHC = Cast(GetTypedOuter()); + if (FoundHC) + return FoundHC; + + return nullptr; +} + +void +UHoudiniPDGAssetLink::UpdateTOPNodeAutoloadAndVisibility() +{ + for (UTOPNetwork* TOPNetwork : AllTOPNetworks) + { + if (!IsValid(TOPNetwork)) + continue; + + for (UTOPNode* TOPNode : TOPNetwork->AllTOPNodes) + { + if (!IsValid(TOPNode)) + continue; + + if (TOPNode->bAutoLoad) + { + // // Set work results that are cooked but in NotLoaded state to ToLoad + // TOPNode.SetNotLoadedWorkResultsToLoad(); + } + + TOPNode->UpdateOutputVisibilityInLevel(); + } + } +} + +void +UHoudiniPDGAssetLink::FilterTOPNodesAndOutputs() +{ + for (UTOPNetwork* TOPNetwork : AllTOPNetworks) + { + if (!IsValid(TOPNetwork)) + continue; + + for (UTOPNode* TOPNode : TOPNetwork->AllTOPNodes) + { + if (!IsValid(TOPNode)) + continue; + + // TOP Node visibility filter via TOPNodeFilter + if (bUseTOPNodeFilter) + { + TOPNode->bHidden = !TOPNodeFilter.IsEmpty() && !TOPNode->NodeName.StartsWith(TOPNodeFilter); + } + else + { + TOPNode->bHidden = false; + } + + // Auto load results filter via TOPNodeOutputFilter + if (bUseTOPOutputFilter) + { + const bool bNewAutoLoad = TOPOutputFilter.IsEmpty() || TOPNode->NodeName.StartsWith(TOPOutputFilter); + if (bNewAutoLoad != TOPNode->bAutoLoad) + { + if (bNewAutoLoad) + { + // Set work results that are cooked but in NotLoaded state to ToLoad + TOPNode->bAutoLoad = true; + // TOPNode->SetNotLoadedWorkResultsToLoad(); + TOPNode->SetVisibleInLevel(true); + } + else + { + TOPNode->bAutoLoad = false; + TOPNode->SetVisibleInLevel(false); + } + TOPNode->UpdateOutputVisibilityInLevel(); + } + } + } + } +} + +#if WITH_EDITORONLY_DATA +bool +UHoudiniPDGAssetLink::AnyRemainingAutoBakeNodes() const +{ + if (!bBakeAfterAllWorkResultObjectsLoaded) + return false; + + switch (PDGBakeSelectionOption) + { + case EPDGBakeSelectionOption::All: + { + for (const UTOPNetwork* const TOPNet : AllTOPNetworks) + { + if (!IsValid(TOPNet)) + continue; + + if (TOPNet->CanStillBeAutoBaked(bAutoBakeNodesWithFailedWorkItems)) + { + return true; + } + } + break; + } + case EPDGBakeSelectionOption::SelectedNetwork: + { + const UTOPNetwork* const TOPNet = GetSelectedTOPNetwork(); + if (IsValid(TOPNet) && TOPNet->CanStillBeAutoBaked(bAutoBakeNodesWithFailedWorkItems)) + { + return true; + } + } + case EPDGBakeSelectionOption::SelectedNode: + { + UTOPNode const* const TOPNode = GetSelectedTOPNode(); + if (IsValid(TOPNode)) + { + const UTOPNetwork* const TOPNet = GetTOPNetworkForNode(TOPNode); + // If the network still has pending items, it could add dynamic items to TOPNode (we don't currently + // have a way to check for dynamic work items explicitly.) + if (IsValid(TOPNet) && TOPNet->AnyWorkItemsPending()) + return true; + if (TOPNode->CanStillBeAutoBaked(bAutoBakeNodesWithFailedWorkItems)) + return true; + } + } + default: + return false; + } + + return false; +} +#endif + +void +UHoudiniPDGAssetLink::OnNodeAutoBaked(UTOPNode* InTOPNode, const bool bInSuccess) +{ + if (!IsValid(InTOPNode)) + return; + + NumAttemptedNodeAutoBakes++; + if (bInSuccess) + NumSuccessfulNodeAutoBakes++; +} + +void +UHoudiniPDGAssetLink::HandleOnPostBake(const bool bInSuccess) +{ + if (OnPostBakeDelegate.IsBound()) + OnPostBakeDelegate.Broadcast(this, bInSuccess); + NumAttemptedNodeAutoBakes = 0; + NumSuccessfulNodeAutoBakes = 0; +} + +bool +UHoudiniPDGAssetLink::BroadcastPostAutoBakeDelegateIfRequired() +{ +#if WITH_EDITOR + if (!bBakeAfterAllWorkResultObjectsLoaded || NumAttemptedNodeAutoBakes <= 0 || AnyRemainingAutoBakeNodes()) + return false; + + HandleOnPostBake(NumSuccessfulNodeAutoBakes > 0); + + return true; +#else + return false; +#endif +} + +#if WITH_EDITORONLY_DATA +void +UHoudiniPDGAssetLink::PostEditChangeChainProperty(FPropertyChangedChainEvent& InPropertyChangedChainEvent) +{ + Super::PostEditChangeChainProperty(InPropertyChangedChainEvent); + + const FName PropertyName = InPropertyChangedChainEvent.GetPropertyName(); + if (PropertyName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, bUseTOPNodeFilter) || + PropertyName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, TOPNodeFilter) || + PropertyName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, bUseTOPOutputFilter) || + PropertyName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, TOPOutputFilter)) + { + // Refilter TOP nodes + FilterTOPNodesAndOutputs(); + bNeedsUIRefresh = true; + } + else if (PropertyName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, SelectedTOPNetworkIndex) || + PropertyName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, bBakeMenuExpanded)) + { + bNeedsUIRefresh = true; + } +} + +#endif + +#if WITH_EDITORONLY_DATA +void +UHoudiniPDGAssetLink::PostTransacted(const FTransactionObjectEvent& TransactionEvent) +{ + Super::PostTransacted(TransactionEvent); + + if (TransactionEvent.GetEventType() != ETransactionObjectEventType::UndoRedo) + return; + + bool bDoFilterTOPNodesAndOutputs = false; + for (const FName& PropName : TransactionEvent.GetChangedProperties()) + { + if (PropName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, bUseTOPNodeFilter) || + PropName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, TOPNodeFilter) || + PropName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, bUseTOPOutputFilter) || + PropName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, TOPOutputFilter)) + { + bDoFilterTOPNodesAndOutputs = true; + bNeedsUIRefresh = true; + } + else if (PropName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, SelectedTOPNetworkIndex) || + PropName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, HoudiniEngineBakeOption) || + PropName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, PDGBakeSelectionOption) || + PropName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, PDGBakePackageReplaceMode) || + PropName == GET_MEMBER_NAME_STRING_CHECKED(UHoudiniPDGAssetLink, bBakeMenuExpanded)) + { + bNeedsUIRefresh = true; + } + } + + if (bDoFilterTOPNodesAndOutputs) + FilterTOPNodesAndOutputs(); +} +#endif + +void +FTOPWorkResultObject::DestroyResultOutputs(const FGuid& InHoudiniComponentGuid) +{ + TSet OutputObjectsToDelete; + + const FString ComponentGuidString = InHoudiniComponentGuid.IsValid() ? InHoudiniComponentGuid.ToString() : FString(); + + // Delete output components and gather output objects for deletion + bool bDidDestroyObjects = false; + bool bDidModifyFoliage = false; + + AActor* const OutputActor = OutputActorOwner.GetOutputActor(); + for (UHoudiniOutput* CurOutput : ResultOutputs) + { + if (!IsValid(CurOutput)) + continue; + FHoudiniLandscapeRuntimeUtils::DeleteLandscapeSplineCookedData(CurOutput); + FHoudiniLandscapeRuntimeUtils::DeleteLandscapeCookedData(CurOutput); + + for (auto& Pair : CurOutput->GetOutputObjects()) + { + FHoudiniOutputObjectIdentifier& Identifier = Pair.Key; + FHoudiniOutputObject& OutputObject = Pair.Value; + for(int Index = 0; Index < OutputObject.OutputComponents.Num(); Index++) + { + UObject* Component = OutputObject.OutputComponents[Index]; + if (!IsValid(Component)) + continue; + + // Instancer components require some special handling around foliage + // TODO: move/refactor so that we can use the InstanceTranslator's helper functions (RemoveAndDestroyComponent and CleanupFoliageInstances) + bool bDestroyComponent = true; + if (Component->IsA()) + { + UHierarchicalInstancedStaticMeshComponent* HISMC = Cast(Component); + if (HISMC->GetOwner() && HISMC->GetOwner()->IsA()) + { + // Make sure foliage our foliage instances have been removed + USceneComponent* ParentComponent = nullptr; + if (IsValid(OutputActor)) + { + ParentComponent = Cast(OutputActor->GetRootComponent()); + } + else + { + ParentComponent = Cast(HISMC->GetOuter()); + if (!ParentComponent) + { + UHoudiniCookable* OuterCookable = Cast(HISMC->GetOuter()); + ParentComponent = OuterCookable ? OuterCookable->GetComponent() : nullptr; + } + } + + if (IsValid(ParentComponent)) + { + UStaticMesh* FoliageSM = HISMC->GetStaticMesh(); + if (!IsValid(FoliageSM)) + return; + + // If we are a foliage HISMC, then our owner is an Instanced Foliage Actor, + // if it is not, then we are just a "regular" HISMC + AInstancedFoliageActor* InstancedFoliageActor = Cast(HISMC->GetOwner()); + if (!IsValid(InstancedFoliageActor)) + return; + + UFoliageType *FoliageType = InstancedFoliageActor->GetLocalFoliageTypeForSource(FoliageSM); + if (!IsValid(FoliageType)) + return; +#if WITH_EDITOR + // Clean up the instances previously generated for that component + InstancedFoliageActor->DeleteInstancesForComponent(ParentComponent, FoliageType); + + // Remove the foliage type if it doesn't have any more instances + if(HISMC->GetInstanceCount() == 0) + InstancedFoliageActor->RemoveFoliageType(&FoliageType, 1); + + bDidModifyFoliage = true; +#endif + } + + // // do not delete FISMC that still have instances left + // // as we have cleaned up our instances before, these have been hand-placed + // if (HISMC->GetInstanceCount() > 0) + bDestroyComponent = false; + + OutputObject.OutputComponents[Index] = nullptr; + } + } + + if (bDestroyComponent) + { + USceneComponent* SceneComponent = Cast(Component); + if (IsValid(SceneComponent)) + { + // Remove from its actor first + if (SceneComponent->GetOwner()) + { + SceneComponent->UnregisterComponent(); + SceneComponent->GetOwner()->RemoveOwnedComponent(SceneComponent); + } + + // Detach from its parent component if attached + SceneComponent->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); + SceneComponent->DestroyComponent(); + + bDidDestroyObjects = true; + + OutputObject.OutputComponents[Index] = nullptr; + } + } + } + + if (IsValid(OutputObject.OutputObject)) + { + // For actors we detach them first and then destroy + AActor* Actor = Cast(OutputObject.OutputObject); + UHoudiniLandscapePtr* LandscapePtr = Cast(OutputObject.OutputObject); + if (LandscapePtr) + { + Actor = LandscapePtr->GetRawPtr(); + } + + if (Actor) + { + Actor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); + Actor->Destroy(); + + bDidDestroyObjects = true; + } + else + { +// Technically this should be WITH_METADATA but it's 5.6 only.. +#if WITH_EDITORONLY_DATA + // ... if not an actor, destroy the object if it is a temp object created by the owning + // HoudiniAssetComponent. Don't delete anything if we don't have a valid component GUID. + if (IsValid(OutputObject.OutputObject) && !OutputObject.OutputObject->HasAnyFlags(RF_Transient) && + !ComponentGuidString.IsEmpty() && !OutputObjectsToDelete.Contains(OutputObject.OutputObject)) + { + // Only delete if the object has metadata indicating it is a plugin created temp object + UPackage* const Package = OutputObject.OutputObject->GetOutermost(); + if (IsValid(Package)) + { +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + FMetaData& MetaData = Package->GetMetaData(); +#else + UMetaData* const MetaData = Package->GetMetaData(); + if (IsValid(MetaData)) +#endif + { + // Check for HAPI_UNREAL_PACKAGE_META_TEMP_GUID in the package metadata to confirm that + // this is a temp package, and then ensure that the component GUID in the package metadata + // for the object matches the GUID of the owning HAC +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + if (MetaData.RootMetaDataMap.Contains(HAPI_UNREAL_PACKAGE_META_TEMP_GUID)) +#else + if (MetaData->RootMetaDataMap.Contains(HAPI_UNREAL_PACKAGE_META_TEMP_GUID)) +#endif + { + FString TempGUID; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + TempGUID = MetaData.RootMetaDataMap.FindChecked(HAPI_UNREAL_PACKAGE_META_TEMP_GUID); +#else + TempGUID = MetaData->RootMetaDataMap.FindChecked(HAPI_UNREAL_PACKAGE_META_TEMP_GUID); +#endif + TempGUID.TrimStartAndEndInline(); + if (!TempGUID.IsEmpty()) + { + // PackageComponentGuidString will be the empty string if the object does not + // have the HAPI_UNREAL_PACKAGE_META_COMPONENT_GUID metadata key in the package +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + const FString PackageComponentGuidString = MetaData.GetValue(OutputObject.OutputObject, HAPI_UNREAL_PACKAGE_META_COMPONENT_GUID); +#else + const FString PackageComponentGuidString = MetaData->GetValue(OutputObject.OutputObject, HAPI_UNREAL_PACKAGE_META_COMPONENT_GUID); +#endif + if (!PackageComponentGuidString.IsEmpty() && PackageComponentGuidString == ComponentGuidString) + OutputObjectsToDelete.Add(OutputObject.OutputObject); + } + } + } + } + } +#endif + OutputObject.OutputObject = nullptr; + } + } + } + } + + ResultOutputs.Empty(); + + if (bDidDestroyObjects) + TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); + + // Delete the output objects we found + if (OutputObjectsToDelete.Num() > 0) + { + TArray ObjectsToDelete(OutputObjectsToDelete.Array()); + FHoudiniEngineRuntimeUtils::SafeDeleteObjects(ObjectsToDelete); + } + +#if WITH_EDITOR + if (bDidModifyFoliage) + { + // Repopulate the foliage types in the foliage mode UI if foliage mode is active + // There is a helper function in FHoudiniEngineUtils for this, but we cannot access it from this module. + // TODO: refactor? + FEditorModeTools& EditorModeTools = GLevelEditorModeTools(); + if (EditorModeTools.IsModeActive(FBuiltinEditorModes::EM_Foliage)) + { + EditorModeTools.DeactivateMode(FBuiltinEditorModes::EM_Foliage); + EditorModeTools.ActivateMode(FBuiltinEditorModes::EM_Foliage); + } + } +#endif +} + +void FTOPWorkResultObject::DestroyResultOutputsAndRemoveOutputActor(const FGuid& InHoudiniComponentGuid) +{ + DestroyResultOutputs(InHoudiniComponentGuid); + GetOutputActorOwner().DestroyOutputActor(); +} + +bool +FOutputActorOwner::CreateOutputActor(UWorld* InWorld, UHoudiniPDGAssetLink* InAssetLink, AActor *InParentActor, const FName& InName) +{ + // InAssetLink and InWorld must not be null + if (!IsValid(InAssetLink)) + { + HOUDINI_LOG_ERROR(TEXT("[FTOPWorkResultObject::CreateWorkResultActor]: InAssetLink is null!")); + return false; + } + if (!IsValid(InWorld)) + { + HOUDINI_LOG_ERROR(TEXT("[FTOPWorkResultObject::CreateWorkResultActor]: InWorld is null!")); + return false; + } + + AActor* AssetLinkActor = InAssetLink->GetOwnerActor(); + + const bool bParentActorIsValid = IsValid(InParentActor); + ULevel* LevelToSpawnIn = nullptr; + if (bParentActorIsValid) + { + LevelToSpawnIn = InParentActor->GetLevel(); + } + else + { + // Get the level containing the asset link's actor + if (IsValid(AssetLinkActor)) + LevelToSpawnIn = AssetLinkActor->GetLevel(); + } + + // Fallback to InWorld's current level + UWorld* WorldToSpawnIn = nullptr; + if (!LevelToSpawnIn || !IsValid(LevelToSpawnIn)) + { + LevelToSpawnIn = InWorld->GetCurrentLevel(); + WorldToSpawnIn = InWorld; + } + else + { + WorldToSpawnIn = LevelToSpawnIn->GetWorld(); + } + + if (!IsValid(WorldToSpawnIn) || !IsValid(LevelToSpawnIn)) + { + HOUDINI_LOG_WARNING( + TEXT("Could not determine level and world to spawn PDG output actor in: asset link %s, name %s"), + *(InAssetLink->GetPathName()), + *(InName.ToString())); + return false; + } + + FActorSpawnParameters SpawnParams; +#if WITH_EDITOR + if (!LevelToSpawnIn->bUseExternalActors) +#endif + { + SpawnParams.Name = MakeUniqueObjectName(InWorld, AActor::StaticClass(), InName); + } + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + SpawnParams.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested; + SpawnParams.OverrideLevel = LevelToSpawnIn; + AActor *Actor = WorldToSpawnIn->SpawnActor(SpawnParams); + SetOutputActor(Actor); +#if WITH_EDITOR + FHoudiniEngineRuntimeUtils::SetActorLabel(Actor, InName.ToString()); +#endif + + // Set the actor transform: create a root component if it does not have one + USceneComponent* RootComponent = Actor->GetRootComponent(); + if (!IsValid(RootComponent)) + { + RootComponent = NewObject(Actor, USceneComponent::StaticClass(), NAME_None, RF_Transactional); + + // Change the creation method so the component is listed in the details panels + FHoudiniEngineRuntimeUtils::AddOrSetAsInstanceComponent(RootComponent); + Actor->SetRootComponent(RootComponent); + RootComponent->OnComponentCreated(); + RootComponent->RegisterComponent(); + } + + RootComponent->SetVisibility(true); + + // If we have a parent actor, set mobility to Movable (or it won't work!) + if (InParentActor) + RootComponent->SetMobility(EComponentMobility::Movable); + else + RootComponent->SetMobility(EComponentMobility::Static); + + const FVector ActorSpawnLocation = InParentActor ? InParentActor->GetActorLocation() : FVector::ZeroVector; + const FRotator ActorSpawnRotator = InParentActor ? InParentActor->GetActorRotation() : FRotator::ZeroRotator; + Actor->SetActorLocation(ActorSpawnLocation); + Actor->SetActorRotation(ActorSpawnRotator); + +#if WITH_EDITOR + if ((InParentActor && IsValid(InParentActor)) && InParentActor->GetLevel() == LevelToSpawnIn) + { + Actor->SetFolderPath(InParentActor->GetFolderPath()); + Actor->AttachToActor(InParentActor, FAttachmentTransformRules::KeepWorldTransform); + } + else if (IsValid(AssetLinkActor) && AssetLinkActor->GetLevel() == LevelToSpawnIn) + { + Actor->SetFolderPath(*FString::Format( + TEXT("{0}/{1}_Output"), + { FStringFormatArg(AssetLinkActor->GetFolderPath().ToString()), FStringFormatArg(AssetLinkActor->GetActorLabel()) } + )); + } + else + { + Actor->SetFolderPath(*FString::Format(TEXT("{0}_Output"), { FStringFormatArg(InAssetLink->GetName()) })); + } +#else + if(IsValid(InParentActor) && InParentActor->GetLevel() == LevelToSpawnIn) + { + OutputActor->AttachToActor(InParentActor, FAttachmentTransformRules::KeepWorldTransform); + } +#endif + + return true; +} + +bool +FOutputActorOwner::DestroyOutputActor() +{ + bool bDestroyed = false; + AActor *Actor = GetOutputActor(); + if (IsValid(Actor)) + { + // Detach from parent before destroying the actor + Actor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); + Actor->Destroy(); + + bDestroyed = true; + } + + SetOutputActor(nullptr); + + return bDestroyed; +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniPDGAssetLink.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +//#include "CoreMinimal.h" +#include "UObject/ObjectMacros.h" +#include "Misc/Guid.h" + +// #include "HoudiniAsset.h" +#include "HoudiniAssetComponent.h" + +#include "HoudiniPDGAssetLink.generated.h" + +struct FHoudiniPackageParams; + +UENUM() +enum class EPDGLinkState : uint8 +{ + Inactive, + Linking, + Linked, + Error_Not_Linked +}; + + +UENUM() +enum class EPDGNodeState : uint8 +{ + None, + Dirtied, + Dirtying, + Cooking, + Cook_Complete, + Cook_Failed +}; + +UENUM() +enum class EPDGWorkResultState : uint8 +{ + None, + ToLoad, + Loading, + Loaded, + ToDelete, + Deleting, + Deleted, + NotLoaded +}; + + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FOutputActorOwner +{ + GENERATED_BODY(); +public: + FOutputActorOwner() + : OutputActor(nullptr) {}; + + virtual ~FOutputActorOwner() {}; + + // Create OutputActor, an actor to hold work result output + virtual bool CreateOutputActor(UWorld* InWorld, UHoudiniPDGAssetLink* InAssetLink, AActor *InParentActor, const FName& InName); + + // Return OutputActor + virtual AActor* GetOutputActor() const { return OutputActor; } + + // Setter for OutputActor + virtual void SetOutputActor(AActor* InActor) { OutputActor = InActor; } + + // Destroy OutputActor if it is valid. + virtual bool DestroyOutputActor(); + +private: + UPROPERTY(NonTransactional) + TObjectPtr OutputActor; + +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FTOPWorkResultObject +{ + GENERATED_USTRUCT_BODY() + +public: + + // Constructor + FTOPWorkResultObject(); + + // Call DestroyResultObjects in the destructor + virtual ~FTOPWorkResultObject(); + + // Set ResultObjects to a copy of InUpdatedOutputs + void SetResultOutputs(const TArray& InUpdatedOutputs) { ResultOutputs = InUpdatedOutputs; } + + // Getter for ResultOutputs + TArray>& GetResultOutputs() { return ResultOutputs; } + + // Getter for ResultOutputs + const TArray>& GetResultOutputs() const { return ResultOutputs; } + + // Destroy ResultOutputs + void DestroyResultOutputs(const FGuid& InHoudiniComponentGuid); + + // Get the OutputActor owner struct + FOutputActorOwner& GetOutputActorOwner() { return OutputActorOwner; } + + // Get the OutputActor owner struct + const FOutputActorOwner& GetOutputActorOwner() const { return OutputActorOwner; } + + // Destroy the ResultOutputs and remove the output actor. + void DestroyResultOutputsAndRemoveOutputActor(const FGuid& InHoudiniComponentGuid); + + // Getter for bAutoBakedSinceLastLoad: indicates if this work result object has been auto-baked since it's last load. + bool AutoBakedSinceLastLoad() const { return bAutoBakedSinceLastLoad; } + // Setter for bAutoBakedSinceLastLoad + void SetAutoBakedSinceLastLoad(bool bInAutoBakedSinceLastLoad) { bAutoBakedSinceLastLoad = bInAutoBakedSinceLastLoad; } + +public: + + UPROPERTY(NonTransactional) + FString Name; + UPROPERTY(NonTransactional) + FString FilePath; + UPROPERTY(NonTransactional) + EPDGWorkResultState State; + // The index in the WorkItemResultInfo array of this item as it was received from HAPI. + UPROPERTY(NonTransactional) + int32 WorkItemResultInfoIndex; + +protected: + // UPROPERTY() + // TArray ResultObjects; + + UPROPERTY(NonTransactional) + TArray> ResultOutputs; + + // If true, indicates that the work result object has been auto-baked since it was last loaded. + UPROPERTY(NonTransactional) + bool bAutoBakedSinceLastLoad = false; + +private: + UPROPERTY(NonTransactional) + FOutputActorOwner OutputActorOwner; +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FTOPWorkResult +{ + GENERATED_USTRUCT_BODY() + +public: + + // Constructor + FTOPWorkResult(); + + // Comparison operator, used by hashing containers and arrays. + bool operator==(const FTOPWorkResult& OtherWorkResult) const; + + // Calls FTOPWorkResultObject::DestroyResultOutputsAndRemoveOutputActor on each entry in ResultObjects and clears the array. + void ClearAndDestroyResultObjects(const FGuid& InHoudiniComponentGuid); + + // Search for the first FTOPWorkResultObject entry by WorkItemResultInfoIndex and return it, or nullptr if it could not be found. + int32 IndexOfWorkResultObjectByHAPIResultInfoIndex(const int32& InWorkItemResultInfoIndex); + // Search for the first FTOPWorkResultObject entry by WorkItemResultInfoIndex and return it, or nullptr if it could not be found. + FTOPWorkResultObject* GetWorkResultObjectByHAPIResultInfoIndex(const int32& InWorkItemResultInfoIndex); + // Return the FTOPWorkResultObject at InArrayIndex in the ResultObjects array, or nullptr if InArrayIndex is not a valid index. + FTOPWorkResultObject* GetWorkResultObjectByArrayIndex(const int32& InArrayIndex); + +public: + + UPROPERTY(NonTransactional) + int32 WorkItemIndex; + UPROPERTY(Transient) + int32 WorkItemID; + + UPROPERTY(NonTransactional) + TArray ResultObjects; + + /* + UPROPERTY() + TArray ResultObjects; + + UPROPERTY() + TArray ResultNames; + UPROPERTY() + TArray ResultFilePaths; + UPROPERTY() + TArray ResultStates; + */ +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FWorkItemTallyBase +{ + GENERATED_USTRUCT_BODY() + +public: + virtual ~FWorkItemTallyBase(); + + // + // Mutators + // + + // Zero all counts, including total. + virtual void ZeroAll() {}; + + // + // Accessors + // + + bool AreAllWorkItemsComplete() const; + bool AnyWorkItemsFailed() const; + bool AnyWorkItemsPending() const; + + virtual int32 NumWorkItems() const { return 0; }; + virtual int32 NumWaitingWorkItems() const { return 0; }; + virtual int32 NumScheduledWorkItems() const { return 0; }; + virtual int32 NumCookingWorkItems() const { return 0; }; + virtual int32 NumCookedWorkItems() const { return 0; }; + virtual int32 NumErroredWorkItems() const { return 0; }; + virtual int32 NumCookCancelledWorkItems() const { return 0; }; + + FString ProgressRatio() const; +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FWorkItemTally : public FWorkItemTallyBase +{ + GENERATED_USTRUCT_BODY() + +public: + + // Constructor + FWorkItemTally(); + + // + // Mutators + // + + // Empty all state sets, as well as AllWorkItems. + virtual void ZeroAll() override; + + // Remove a work item from all state sets and AllWorkItems. + void RemoveWorkItem(int32 InWorkItemID); + + void RecordWorkItemAsWaiting(int32 InWorkItemID); + void RecordWorkItemAsScheduled(int32 InWorkItemID); + void RecordWorkItemAsCooking(int32 InWorkItemID); + void RecordWorkItemAsCooked(int32 InWorkItemID); + void RecordWorkItemAsErrored(int32 InWorkItemID); + void RecordWorkItemAsCookCancelled(int32 InWorkItemID); + + // + // Accessors + // + + virtual int32 NumWorkItems() const override { return AllWorkItems.Num(); } + virtual int32 NumWaitingWorkItems() const override { return WaitingWorkItems.Num(); } + virtual int32 NumScheduledWorkItems() const override { return ScheduledWorkItems.Num(); } + virtual int32 NumCookingWorkItems() const override { return CookingWorkItems.Num(); } + virtual int32 NumCookedWorkItems() const override { return CookedWorkItems.Num(); } + virtual int32 NumErroredWorkItems() const override { return ErroredWorkItems.Num(); } + virtual int32 NumCookCancelledWorkItems() const override { return CookCancelledWorkItems.Num(); } + +protected: + + // Removes the work item id from all state sets (but not from AllWorkItems -- use RemoveWorkItem for that). + void RemoveWorkItemFromAllStateSets(int32 InWorkItemID); + + // We use sets to keep track of in what state a work item is. The set stores the WorkItemID. + + UPROPERTY() + TSet AllWorkItems; + UPROPERTY() + TSet WaitingWorkItems; + UPROPERTY() + TSet ScheduledWorkItems; + UPROPERTY() + TSet CookingWorkItems; + UPROPERTY() + TSet CookedWorkItems; + UPROPERTY() + TSet ErroredWorkItems; + UPROPERTY() + TSet CookCancelledWorkItems; +}; + +USTRUCT() +struct HOUDINIENGINERUNTIME_API FAggregatedWorkItemTally : public FWorkItemTallyBase +{ + GENERATED_USTRUCT_BODY() + +public: + + // Constructor + FAggregatedWorkItemTally(); + + virtual void ZeroAll() override; + + void Add(const FWorkItemTallyBase& InWorkItemTally); + + void Subtract(const FWorkItemTallyBase& InWorkItemTally); + + virtual int32 NumWorkItems() const override { return TotalWorkItems; } + virtual int32 NumWaitingWorkItems() const override { return WaitingWorkItems; } + virtual int32 NumScheduledWorkItems() const override { return ScheduledWorkItems; } + virtual int32 NumCookingWorkItems() const override { return CookingWorkItems; } + virtual int32 NumCookedWorkItems() const override { return CookedWorkItems; } + virtual int32 NumErroredWorkItems() const override { return ErroredWorkItems; } + +protected: + UPROPERTY() + int32 TotalWorkItems; + UPROPERTY() + int32 WaitingWorkItems; + UPROPERTY() + int32 ScheduledWorkItems; + UPROPERTY() + int32 CookingWorkItems; + UPROPERTY() + int32 CookedWorkItems; + UPROPERTY() + int32 ErroredWorkItems; + UPROPERTY() + int32 CookCancelledWorkItems; + +}; + +// Container for baked outputs of a PDG work result object. +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHoudiniPDGWorkResultObjectBakedOutput +{ + GENERATED_BODY() + + public: + // Array of baked output per output index of the work result object's outputs. + UPROPERTY() + TArray BakedOutputs; +}; + +// Forward declare the UTOPNetwork here for some references in the UTOPNode +class UTOPNetwork; + +UCLASS() +class HOUDINIENGINERUNTIME_API UTOPNode : public UObject +{ + GENERATED_BODY() + +public: + // Constructor + UTOPNode(); + + // Comparison operator, used by hashing containers and arrays. + bool operator==(const UTOPNode& Other) const; + + void Reset(); + + // Get the owning/outer UHoudiniPDGAssetLink of this UTOPNode. + UHoudiniPDGAssetLink* GetOuterAssetLink() const; + + // Get the Guid of the Cookable that owns the AssetLink. + // Returns an invalid Guid if the HAC or asset link could not be found / is invalid. + FGuid GetHoudiniCookableGuid() const; + + const FWorkItemTallyBase& GetWorkItemTally() const + { + if (bHasChildNodes) + return AggregatedWorkItemTally; + return WorkItemTally; + } + + void AggregateTallyFromChildNode(const UTOPNode* InChildNode) + { + if (IsValid(InChildNode)) + AggregatedWorkItemTally.Add(InChildNode->GetWorkItemTally()); + } + + bool AreAllWorkItemsComplete() const { return GetWorkItemTally().AreAllWorkItemsComplete(); }; + bool AnyWorkItemsFailed() const { return GetWorkItemTally().AnyWorkItemsFailed(); }; + bool AnyWorkItemsPending() const { return GetWorkItemTally().AnyWorkItemsPending(); }; + void ZeroWorkItemTally() + { + WorkItemTally.ZeroAll(); + AggregatedWorkItemTally.ZeroAll(); + } + + // Called by PDG manager when work item events are received + + // Notification that a work item has been created + void OnWorkItemCreated(int32 InWorkItemID) { }; + + // Notification that a work item has been removed. + void OnWorkItemRemoved(int32 InWorkItemID) { WorkItemTally.RemoveWorkItem(InWorkItemID); }; + + // Notification that a work item has moved to the waiting state. + void OnWorkItemWaiting(int32 InWorkItemID); + + // Notification that a work item has been scheduled. + void OnWorkItemScheduled(int32 InWorkItemID) { WorkItemTally.RecordWorkItemAsScheduled(InWorkItemID); }; + + // Notification that a work item has started cooking. + void OnWorkItemCooking(int32 InWorkItemID) { WorkItemTally.RecordWorkItemAsCooking(InWorkItemID); }; + + // Notification that a work item has been cooked. + void OnWorkItemCooked(int32 InWorkItemID); + + // Notification that a work item has errored. + void OnWorkItemErrored(int32 InWorkItemID) { WorkItemTally.RecordWorkItemAsErrored(InWorkItemID); }; + + // Notification that a work item cook has been cancelled. + void OnWorkItemCookCancelled(int32 InWorkItemID) { WorkItemTally.RecordWorkItemAsCookCancelled(InWorkItemID); }; + + bool IsVisibleInLevel() const { return bShow; } + void SetVisibleInLevel(bool bInVisible); + void UpdateOutputVisibilityInLevel(); + + // Sets all WorkResultObjects that are in the NotLoaded state to ToLoad. + void SetNotLoadedWorkResultsToLoad(bool bInAlsoSetDeletedToLoad=false); + + // Sets all WorkResultObjects that are in the Loaded state to ToDelete (will delete output objects and output + // actors). + void SetLoadedWorkResultsToDelete(); + + // Immediately delete the Loaded work result output object (keeps the work item and result structs in the arrays but + // deletes the output object and the actor and sets the state to Deleted. + void DeleteWorkResultObjectOutputs(const int32 InWorkResultArrayIndex, const int32 InWorkResultObjectArrayIndex, const bool bInDeleteOutputActors=true); + + // Immediately delete Loaded work result output objects for the specified work item (keeps the work item and result + // arrays but deletes the output objects and actors and sets the state to Deleted. + void DeleteWorkItemOutputs(const int32 InWorkResultArrayIndex, const bool bInDeleteOutputActors=true); + + // Immediately delete Loaded work result output objects (keeps the work items and result arrays but deletes the output + // objects and actors and sets the state to Deleted. + void DeleteAllWorkResultObjectOutputs(const bool bInDeleteOutputActors=true); + + // Get the OutputActor owner struct + FOutputActorOwner& GetOutputActorOwner() { return OutputActorOwner; } + + // Get the OutputActor owner struct + const FOutputActorOwner& GetOutputActorOwner() const { return OutputActorOwner; } + + // Get the baked outputs from the last bake. The map keys are [work_result.work_item_index]_[work_result_object_index] + TMap& GetBakedWorkResultObjectsOutputs() { return BakedWorkResultObjectOutputs; } + const TMap& GetBakedWorkResultObjectsOutputs() const { return BakedWorkResultObjectOutputs; } + // Helper to construct the key used to look up baked work results. + static FString GetBakedWorkResultObjectOutputsKey(int32 InWorkResultArrayIndex, int32 InWorkResultObjectArrayIndex); + // Helper to construct the key used to look up baked work results. + FString GetBakedWorkResultObjectOutputsKey(const FTOPWorkResult& InWorkResult, int32 InWorkResultObjectArrayIndex) const; + // Helper to construct the key used to look up baked work results. + bool GetBakedWorkResultObjectOutputsKey(int32 InWorkResultArrayIndex, int32 InWorkResultObjectArrayIndex, FString& OutKey) const; + // Get the FHoudiniPDGWorkResultObjectBakedOutput for a work item (FTOPWorkResult) and specific result object. + bool GetBakedWorkResultObjectOutputs(int32 InWorkResultArrayIndex, int32 InWorkResultObjectArrayIndex, FHoudiniPDGWorkResultObjectBakedOutput*& OutBakedOutput); + // Get the FHoudiniPDGWorkResultObjectBakedOutput for a work item (FTOPWorkResult) and specific result object (const version). + bool GetBakedWorkResultObjectOutputs(int32 InWorkResultArrayIndex, int32 InWorkResultObjectArrayIndex, FHoudiniPDGWorkResultObjectBakedOutput const*& OutBakedOutput) const; + + // Search for the first FTOPWorkResult entry by WorkItemID and return its array index or INDEX_NONE, if it could not be found. + int32 ArrayIndexOfWorkResultByID(const int32& InWorkItemID) const; + // Search for the first FTOPWorkResult entry by WorkItemID and return it, or nullptr if it could not be found. + FTOPWorkResult* GetWorkResultByID(const int32& InWorkItemID); + // Search for the first FTOPWorkResult entry with an invalid (INDEX_NONE) work item id and return it, or INDEX_None + // if no invalid entry could be found. + int32 ArrayIndexOfFirstInvalidWorkResult() const; + // Return the FTOPWorkResult at InArrayIndex in the WorkResult array, or nullptr if InArrayIndex is not a valid index. + FTOPWorkResult* GetWorkResultByArrayIndex(const int32& InArrayIndex); + + // Returns true if InNetwork is the parent TOP Net of this node. + bool IsParentTOPNetwork(UTOPNetwork const * const InNetwork) const; + + // Returns true if this node can still be auto-baked + bool CanStillBeAutoBaked(bool bInAutoBakeWithFailedWorkItems=true) const; + +#if WITH_EDITOR + void PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) override; +#endif + +#if WITH_EDITOR + void PostTransacted(const FTransactionObjectEvent& TransactionEvent) override; +#endif + +public: + + UPROPERTY(Transient, NonTransactional) + int32 NodeId; + UPROPERTY(NonTransactional) + FString NodeName; + UPROPERTY(NonTransactional) + FString NodePath; + UPROPERTY(NonTransactional) + FString ParentName; + + UPROPERTY() + TObjectPtr WorkResultParent; + UPROPERTY(NonTransactional) + TArray WorkResult; + + // Hidden in the nodes combobox + UPROPERTY() + bool bHidden; + UPROPERTY() + bool bAutoLoad; + + UPROPERTY(Transient, NonTransactional) + EPDGNodeState NodeState; + + // This is set when the TOP node's work items are processed by + // FHoudiniPDGManager based on if any NotLoaded work result objects are found + UPROPERTY(NonTransactional) + bool bCachedHaveNotLoadedWorkResults; + + // This is set when the TOP node's work items are processed by + // FHoudiniPDGManager based on if any Loaded work result objects are found + UPROPERTY(NonTransactional) + bool bCachedHaveLoadedWorkResults; + + // True if this node has child nodes + UPROPERTY(NonTransactional) + bool bHasChildNodes; + + // These notification events have been introduced so that we can start encapsulating code. + // in this class as opposed to modifying this object in various places throughout the codebase. + + // Notification that this TOP node has been dirtied. + void OnDirtyNode(); + + // Keep track of cleared layers + UPROPERTY() + FHoudiniClearedEditLayers ClearedLayers; + + // Returns true if the node has received the HAPI_PDG_EVENT_COOK_COMPLETE event since the last the cook started + bool HasReceivedCookCompleteEvent() const { return bHasReceivedCookCompleteEvent; } + // Handler for when the node receives the HAPI_PDG_EVENT_COOK_START (called for each node when a TOPNet starts cooking) + void HandleOnPDGEventCookStart() { bHasReceivedCookCompleteEvent = false; } + // Handler for when the node receives the HAPI_PDG_EVENT_COOK_COMPLETE event (called for each node when a TOPNet completes cooking) + void HandleOnPDGEventCookComplete() { bHasReceivedCookCompleteEvent = true; } + +protected: + void InvalidateLandscapeCache(); + + // Visible in the level + UPROPERTY() + bool bShow; + + // Map of [work_result_index]_[work_result_object_index] to the work result object's baked outputs. + UPROPERTY() + TMap BakedWorkResultObjectOutputs; + + // This node's own work items, used when bHasChildNodes is false. + UPROPERTY(Transient, NonTransactional) + FWorkItemTally WorkItemTally; + // This node's aggregated work item tallys (sum of child work item tally, use when bHasChildNodes is true) + UPROPERTY(Transient, NonTransactional) + FAggregatedWorkItemTally AggregatedWorkItemTally; + + // Set to true when the node recieves HAPI_PDG_EVENT_COOK_COMPLETE event + UPROPERTY(Transient, NonTransactional) + bool bHasReceivedCookCompleteEvent; + +private: + UPROPERTY() + FOutputActorOwner OutputActorOwner; +}; + + +UCLASS() +class HOUDINIENGINERUNTIME_API UTOPNetwork : public UObject +{ + GENERATED_BODY() + +public: + + // Delegate that is broadcast when cook of the network is complete. Parameters are the UTOPNetwork and bAnyFailedWorkItems. + DECLARE_MULTICAST_DELEGATE_TwoParams(FOnPostCookDelegate, UTOPNetwork*, const bool); + + // Constructor + UTOPNetwork(); + + // Comparison operator, used by hashing containers and arrays. + bool operator==(const UTOPNetwork& Other) const; + + // Sets all WorkResultObjects that are in the Loaded state to ToDelete (will delete output objects and output + // actors). + void SetLoadedWorkResultsToDelete(); + + // Immediately delete Loaded work result output objects (keeps the work items and result arrays but deletes the output + // objects and actors and sets the state to Deleted. + void DeleteAllWorkResultObjectOutputs(); + + // Returns true if any node in this TOP net has pending (waiting, scheduled, cooking) work items. + bool AnyWorkItemsPending() const; + + // Returns true if any node in this TOP net has failed/errored work items. + bool AnyWorkItemsFailed() const; + + // Returns true if this network has nodes that can still be auto-baked + bool CanStillBeAutoBaked(bool bInAutoBakeWithFailedWorkItems=true) const; + + // Handler for when a node in the newtork receives the HAPI_PDG_EVENT_COOK_COMPLETE event (called for each node when a TOPNet completes cooking) + void HandleOnPDGEventCookCompleteReceivedByChildNode(UHoudiniPDGAssetLink* const InAssetLink, UTOPNode* const InTOPNode); + + FOnPostCookDelegate& GetOnPostCookDelegate() { return OnPostCookDelegate; } + +public: + + UPROPERTY(Transient, NonTransactional) + int32 NodeId; + UPROPERTY(NonTransactional) + FString NodeName; + // HAPI path to this node (relative to the HDA) + UPROPERTY(NonTransactional) + FString NodePath; + + UPROPERTY() + TArray> AllTOPNodes; + + // TODO: Should be using SelectedNodeName instead? + // Index is not consistent after updating filter + UPROPERTY() + int32 SelectedTOPIndex; + + UPROPERTY(NonTransactional) + FString ParentName; + + UPROPERTY() + bool bShowResults; + UPROPERTY() + bool bAutoLoadResults; + + FOnPostCookDelegate OnPostCookDelegate; +}; + + +class UHoudiniPDGAssetLink; +DECLARE_MULTICAST_DELEGATE_FourParams(FHoudiniPDGAssetLinkWorkResultObjectLoaded, UHoudiniPDGAssetLink*, UTOPNode*, int32 /*WorkItemArrayIndex*/, int32 /*WorkItemResultInfoIndex*/); + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPDGAssetLink : public UObject +{ + GENERATED_UCLASS_BODY() + +public: + + friend class UHoudiniAssetComponent; + friend class UHoudiniCookable; + + // Delegate for when the entire bake operation is complete (all selected nodes/networks have been baked). + DECLARE_MULTICAST_DELEGATE_TwoParams(FOnPostBakeDelegate, UHoudiniPDGAssetLink*, const bool); + // Delegate for when a network completes a cook. Passes the asset link, the network, a bAnyWorkItemsFailed. + DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnPostTOPNetworkCookDelegate, UHoudiniPDGAssetLink*, UTOPNetwork*, const bool); + + static FString GetTOPNodeStatus(const UTOPNode* InTOPNode); + static FLinearColor GetTOPNodeStatusColor(const UTOPNode* InTOPNode); + + void UpdateTOPNodeWithChildrenWorkItemTallyAndState(UTOPNode* InNode, UTOPNetwork* InNetwork); + void UpdateWorkItemTally(); + static void ResetTOPNetworkWorkItemTally(UTOPNetwork* TOPNetwork); + + // Set the TOP network at the given index as currently selected TOP network + void SelectTOPNetwork(const int32& AtIndex); + // Set the TOP node at the given index in the given TOP network as currently selected TOP node + void SelectTOPNode(UTOPNetwork* InTOPNetwork, const int32& AtIndex); + + UTOPNode* GetSelectedTOPNode(); + const UTOPNode* GetSelectedTOPNode() const; + UTOPNetwork* GetSelectedTOPNetwork(); + const UTOPNetwork* GetSelectedTOPNetwork() const; + + FString GetSelectedTOPNodeName(); + FString GetSelectedTOPNetworkName(); + + UTOPNode* GetTOPNode(const int32& InNodeID); + bool GetTOPNodeAndNetworkByNodeId(const int32& InNodeID, UTOPNetwork*& OutNetwork, UTOPNode*& OutNode); + UTOPNetwork* GetTOPNetwork(const int32& AtIndex); + const UTOPNetwork* GetTOPNetwork(const int32& AtIndex) const; + + // Get the UTOPNetwork that contains InNode. + UTOPNetwork* GetTOPNetworkForNode(const UTOPNode* InNode) const; + + // Find the node with relative path 'InNodePath' from its topnet. + static UTOPNode* GetTOPNodeByNodePath(const FString& InNodePath, const TArray& InTOPNodes, int32& OutIndex); + // Find the network with relative path 'InNetPath' from the HDA + static UTOPNetwork* GetTOPNetworkByNodePath(const FString& InNodePath, const TArray& InTOPNetworks, int32& OutIndex); + + // Get the parent TOP node of the specified node. This is resolved + UTOPNode* GetParentTOPNode(const UTOPNode* InNode); + + static void ClearTOPNodeWorkItemResults(UTOPNode* TOPNode); + static void ClearTOPNetworkWorkItemResults(UTOPNetwork* TOPNetwork); + // Clear the result objects of a work item (FTOPWorkResult.ResultObjects), but don't delete the work item from + // TOPNode.WorkResults (for example, the work item was dirtied but not removed from PDG) + static void ClearWorkItemResultByID(const int32& InWorkItemID, UTOPNode* InTOPNode); + // Calls ClearWorkItemResultByID and then deletes the FTOPWorkResult from InTOPNode.Result as well. For example: + // the work item was removed in PDG. + static void DestroyWorkItemByID(const int32& InWorkItemID, UTOPNode* InTOPNode); + static FTOPWorkResult* GetWorkResultByID(const int32& InWorkItemID, UTOPNode* InTOPNode); + + // This should be called after the owner and this PDG asset link is duplicated. Set all output parent actors to + // null in all TOP networks/nodes. Since the TOP Networks/TOP nodes are all structs, we cannot set + // DuplicateTransient property on their OutputActor properties. + void UpdatePostDuplicate(); + + // Load the geometry generated as results of the given work item, of the given TOP node. + // The load will be done asynchronously. + // Results must be tagged with 'file', and must have a file path, otherwise will not be loaded. + //void LoadResults(FTOPNode TOPNode, HAPI_PDG_WorkitemInfo workItemInfo, HAPI_PDG_WorkitemResultInfo[] resultInfos, HAPI_PDG_WorkitemId workItemID) + + // Return the first UHoudiniCookable in the parent chain. + // If this asset link is not owned by a HoudiniCookable, a nullptr will be returned. + UHoudiniCookable* GetOuterHoudiniCookable() const; + + // Helper function to get the GUID of the owning HoudiniCookable. + // Returns an invalid FGuid if GetOuterHoudiniAssetComponent() returns null. + FGuid GetOuterCookableGuid() const; + + // Gets the temporary cook folder. If the parent of this asset link is a HoudiniCookable use that, + // otherwise use the default static mesh temporary cook folder. + FDirectoryPath GetTemporaryCookFolder() const; + + // Get the actor that owns this PDG asset link. If the asset link is owned by a component, + // then the component's owning actor is returned. Can return null if this is now owned by + // an actor or component. + AActor* GetOwnerActor() const; + + // Checks if the asset link has any temporary outputs and returns true if it has + bool HasTemporaryOutputs() const; + + // Filter TOP nodes and outputs (hidden/visible) by TOPNodeFilter and TOPOutputFilter. + void FilterTOPNodesAndOutputs(); + + // On all FTOPNodes: Load not loaded items if bAutoload is true, and update the level visibility of work items + // result. Used when FTOPNode.bShow and/or FTOPNode.bAutoload changed. + void UpdateTOPNodeAutoloadAndVisibility(); + +#if WITH_EDITORONLY_DATA + // Returns true if there are any nodes left that can/must still be auto-baked. + bool AnyRemainingAutoBakeNodes() const; +#endif + + // Used to notify the asset link that InTOPNode was auto-baked. This increments the attempts and success counters. + void OnNodeAutoBaked(UTOPNode* InTOPNode, bool bInSuccess); + + // Delegate handlers + + // Get the post bake delegate + FOnPostBakeDelegate& GetOnPostBakeDelegate() { return OnPostBakeDelegate; } + + // Called by baking code after baking all of the outputs + void HandleOnPostBake(const bool bInSuccess); + + // Checks if the post bake delegate should be broadcast. If all auto-baking is done (and there are > 0 items baked) + // calls HandleOnPostBake(). + // Returns true if HandleOnPostBake() was called. + bool BroadcastPostAutoBakeDelegateIfRequired(); + + FOnPostTOPNetworkCookDelegate& GetOnPostTOPNetworkCookDelegate() { return OnPostTOPNetworkCookDelegate; } + + // Handler for when a TOP network completes a cook. Called by the TOP Net once all of its nodes have received + // HAPI_PDG_EVENT_COOK_COMPLETE. + void HandleOnTOPNetworkCookComplete(UTOPNetwork* const InTOPNet); + +#if WITH_EDITORONLY_DATA + void PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) override; +#endif + +#if WITH_EDITORONLY_DATA + void PostTransacted(const FTransactionObjectEvent& TransactionEvent) override; +#endif + +#if WITH_EDITORONLY_DATA + // Setter for bAutoBakeNodesWithFailedWorkItems. + void SetAutoBakeNodesWithFailedWorkItemsEnabled(const bool bInEnabled) { bAutoBakeNodesWithFailedWorkItems = bInEnabled; } +#endif + +#if WITH_EDITORONLY_DATA + // Getter for bAutoBakeNodesWithFailedWorkItems. + bool IsAutoBakeNodesWithFailedWorkItemsEnabled() const { return bAutoBakeNodesWithFailedWorkItems; } +#endif + +#if WITH_EDITORONLY_DATA + // Getter for the property name of bAutoBakeNodesWithFailedWorkItems. This is for subclasses or code outside of this + // class that needs to access the property name (since bAutoBakeNodesWithFailedWorkItems is private). + static FName GetbAutoBakeNodesWithFailedWorkItemsPropertyName() + { + return GET_MEMBER_NAME_CHECKED(UHoudiniPDGAssetLink, bAutoBakeNodesWithFailedWorkItems); + } +#endif + + void SetOutputWorld(UWorld * InWorld); + UWorld* GetOutputWorld(); + +private: + + void ClearAllTOPData(); + +public: + + UPROPERTY(DuplicateTransient, NonTransactional) + FString AssetName; + + // The full path to the HDA in HAPI + UPROPERTY(DuplicateTransient, NonTransactional) + FString AssetNodePath; + + UPROPERTY(DuplicateTransient, NonTransactional) + int32 AssetID; + + UPROPERTY() + TArray> AllTOPNetworks; + + UPROPERTY() + int32 SelectedTOPNetworkIndex; + + UPROPERTY(Transient, NonTransactional) + EPDGLinkState LinkState; + + UPROPERTY() + bool bAutoCook; + UPROPERTY() + bool bUseTOPNodeFilter; + UPROPERTY() + bool bUseTOPOutputFilter; + UPROPERTY() + FString TOPNodeFilter; + UPROPERTY() + FString TOPOutputFilter; + + UPROPERTY(NonTransactional) + int32 NumWorkItems; + UPROPERTY(Transient, NonTransactional) + FAggregatedWorkItemTally WorkItemTally; + + UPROPERTY() + FString OutputCachePath; + + UPROPERTY(Transient) + bool bNeedsUIRefresh; + + // A parent actor to serve as the parent of any output actors + // that are created. + // If null, then output actors are created under a folder + UPROPERTY(EditAnywhere, Category="Output") + TObjectPtr OutputParentActor; + + // Folder used for baking PDG outputs + UPROPERTY() + FDirectoryPath BakeFolder; + + UPROPERTY() + TObjectPtr OutputWorld = nullptr; + // + // Notifications + // + + // Delegate that is broadcast when a work result object has been loaded + FHoudiniPDGAssetLinkWorkResultObjectLoaded OnWorkResultObjectLoaded; + + // Delegate that is broadcast after a bake. + FOnPostBakeDelegate OnPostBakeDelegate; + + // Delegate that is broadcast after a TOP Network completes a cook. + FOnPostTOPNetworkCookDelegate OnPostTOPNetworkCookDelegate; + + // + // End: Notifications + // + +#if WITH_EDITORONLY_DATA + UPROPERTY() + bool bBakeMenuExpanded; + + // What kind of output to bake, for example, bake actors, bake to blueprint + UPROPERTY() + EHoudiniEngineBakeOption HoudiniEngineBakeOption; + + // Which outputs to bake, for example, all, selected network, selected node + UPROPERTY() + EPDGBakeSelectionOption PDGBakeSelectionOption; + + // This determines if the baked assets should replace existing assets with the same name, + // or always generate new assets (with numerical suffixes if needed to create unique names) + UPROPERTY() + EPDGBakePackageReplaceModeOption PDGBakePackageReplaceMode; + + // If true, recenter baked actors to their bounding box center after bake + UPROPERTY() + bool bRecenterBakedActors; + + // Auto-bake: if this is true, it indicates that once all work result objects for the node is loaded they should + // all be baked + UPROPERTY() + bool bBakeAfterAllWorkResultObjectsLoaded; + + // The delegate handle of the auto bake helper function bound to OnWorkResultObjectLoaded. + FDelegateHandle AutoBakeDelegateHandle; +#endif + +private: +#if WITH_EDITORONLY_DATA + // If true then a TOP Node with failed work items will still have the successful items auto-baked. + UPROPERTY() + bool bAutoBakeNodesWithFailedWorkItems; +#endif + + // The number of TOP nodes that have been attempted to be baked since the last time HandlePostBake has been called. + UPROPERTY() + int32 NumAttemptedNodeAutoBakes; + + // The number of TOP nodes that have been successfully baked since the last time HandlePostBake has been called. + UPROPERTY() + int32 NumSuccessfulNodeAutoBakes; +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameter.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameter.h" + +UHoudiniParameter::UHoudiniParameter(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) + , ParmType(EHoudiniParameterType::Invalid) + , ChoiceListType(EHoudiniParameterChoiceListType::None) + , TupleSize(0) + , NodeId(-1) + , ParmId(-1) + , ParentParmId(-1) + , ChildIndex(-1) + , bIsVisible(true) + , bIsParentFolderVisible(true) + , bIsDisabled(false) + , bHasChanged(false) + , bNeedsToTriggerUpdate(true) + , bIsDefault(false) + , bIsSpare(false) + , bJoinNext(false) + , bIsLabelVisible(true) + , bIsChildOfMultiParm(false) + , bIsDirectChildOfMultiParm(false) + , bPendingRevertToDefault(false) + , TagCount(0) + , ValueIndex(-1) + , bHasExpression(false) + , bShowExpression(false) +{ + Name = TEXT(""); + Label = TEXT(""); + Help = TEXT(""); + +} + +UHoudiniParameter * +UHoudiniParameter::Create( UObject* InOuter, const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameter_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameter::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameter * HoudiniAssetParameter = NewObject< UHoudiniParameter >( + InOuter, UHoudiniParameter::StaticClass(), ParamName, RF_Public | RF_Transactional); + + return HoudiniAssetParameter; +} + +bool +UHoudiniParameter::IsChildParameter() const +{ + return ParentParmId >= 0; +} + +void +UHoudiniParameter::RevertToDefault() +{ + bPendingRevertToDefault = true; + TuplePendingRevertToDefault.Empty(); + TuplePendingRevertToDefault.Add(-1); + + MarkChanged(true); +} + +void +UHoudiniParameter::RevertToDefault(const int32& InAtTupleIndex) +{ + bPendingRevertToDefault = true; + TuplePendingRevertToDefault.AddUnique(InAtTupleIndex); + + MarkChanged(true); +} + +void +UHoudiniParameter::MarkDefault(const bool& bInDefault) +{ + bIsDefault = bInDefault; + + if (bInDefault) + { + // No need to revert default parameter + bPendingRevertToDefault = false; + TuplePendingRevertToDefault.Empty(); + } +} + +EHoudiniRampInterpolationType UHoudiniParameter::GetHoudiniInterpMethodFromInt(int32 InInt) +{ + EHoudiniRampInterpolationType Result = EHoudiniRampInterpolationType::InValid; + switch (InInt) + { + case 0: + Result = EHoudiniRampInterpolationType::CONSTANT; + break; + case 1: + Result = EHoudiniRampInterpolationType::LINEAR; + break; + case 2: + Result = EHoudiniRampInterpolationType::CATMULL_ROM; + break; + case 3: + Result = EHoudiniRampInterpolationType::MONOTONE_CUBIC; + break; + case 4: + Result = EHoudiniRampInterpolationType::BEZIER; + break; + case 5: + Result = EHoudiniRampInterpolationType::BSPLINE; + break; + case 6: + Result = EHoudiniRampInterpolationType::HERMITE; + break; + } + + return Result; +} + +FString +UHoudiniParameter::GetStringFromHoudiniInterpMethod(EHoudiniRampInterpolationType InType) +{ + FString Result = FString("InValid"); + switch (InType) + { + case EHoudiniRampInterpolationType::CONSTANT: + Result = FString("Constant"); + break; + case EHoudiniRampInterpolationType::LINEAR: + Result = FString("Linear"); + break; + case EHoudiniRampInterpolationType::BEZIER: + Result = FString("Bezier"); + break; + case EHoudiniRampInterpolationType::BSPLINE: + Result = FString("B-Spline"); + break; + case EHoudiniRampInterpolationType::MONOTONE_CUBIC: + Result = FString("Monotone Cubic"); + break; + case EHoudiniRampInterpolationType::CATMULL_ROM: + Result = FString("Catmull Rom"); + break; + case EHoudiniRampInterpolationType::HERMITE: + Result = FString("Hermite"); + break; + } + + return Result; +} +EHoudiniRampInterpolationType +UHoudiniParameter::GetHoudiniInterpMethodFromString(const FString& InString) +{ + if (InString.StartsWith(TEXT("Constant"))) + return EHoudiniRampInterpolationType::CONSTANT; + + else if (InString.StartsWith("Linear")) + return EHoudiniRampInterpolationType::LINEAR; + + else if (InString.StartsWith("Bezier")) + return EHoudiniRampInterpolationType::BEZIER; + + else if (InString.StartsWith("B-Spline")) + return EHoudiniRampInterpolationType::BSPLINE; + + else if (InString.StartsWith("Monotone Cubic")) + return EHoudiniRampInterpolationType::MONOTONE_CUBIC; + + else if (InString.StartsWith("Catmull Rom")) + return EHoudiniRampInterpolationType::CATMULL_ROM; + + else if (InString.StartsWith("Hermite")) + return EHoudiniRampInterpolationType::HERMITE; + + return EHoudiniRampInterpolationType::InValid; +} + + + +ERichCurveInterpMode +UHoudiniParameter::EHoudiniRampInterpolationTypeToERichCurveInterpMode(EHoudiniRampInterpolationType InType) +{ + switch (InType) + { + case EHoudiniRampInterpolationType::CONSTANT: + return ERichCurveInterpMode::RCIM_Constant; + + case EHoudiniRampInterpolationType::LINEAR: + return ERichCurveInterpMode::RCIM_Linear; + + default: + break; + } + + return ERichCurveInterpMode::RCIM_Cubic; +} + +UHoudiniParameter* +UHoudiniParameter::DuplicateAndCopyState(UObject* DestOuter, EObjectFlags ClearFlags, EObjectFlags SetFlags) +{ + UHoudiniParameter* NewParameter = Cast(StaticDuplicateObject(this, DestOuter)); + + NewParameter->CopyStateFrom(this, false); + + return NewParameter; +} + +void +UHoudiniParameter::CopyStateFrom(UHoudiniParameter * InParameter, bool bCopyAllProperties, EObjectFlags InClearFlags, EObjectFlags InSetFlags) +{ + if (bCopyAllProperties) + { + UEngine::FCopyPropertiesForUnrelatedObjectsParams Params; + Params.bDoDelta = false; // Perform a deep copy + Params.bClearReferences = false; // References will be replaced afterwards. + UEngine::CopyPropertiesForUnrelatedObjects(InParameter, this, Params); + } + + if (InSetFlags != RF_NoFlags) + SetFlags(InSetFlags); + if (InClearFlags != RF_NoFlags) + ClearFlags( InClearFlags ); + + NodeId = InParameter->NodeId; + ParmId = InParameter->ParmId; + ParentParmId = InParameter->ParentParmId; + bIsDefault = InParameter->bIsDefault; + bPendingRevertToDefault = InParameter->bPendingRevertToDefault; + TuplePendingRevertToDefault = InParameter->TuplePendingRevertToDefault; + bShowExpression = InParameter->bShowExpression; +} + +void +UHoudiniParameter::InvalidateData() +{ + +} + +void +UHoudiniParameter::OnSessionConnected() +{ + NodeId = INDEX_NONE; + + // Dont invalidate existing parmId as this will prevent the Parm UI + // From properly showing up. + //ParmId = INDEX_NONE; + //ParentParmId = INDEX_NONE; +} + + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameter.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "UObject/Object.h" +#include "Curves/RealCurve.h" +#include "HoudiniInput.h" +#include "HoudiniEngineRuntimeCommon.h" + +#include "HoudiniParameter.generated.h" + +UENUM() +enum class EHoudiniParameterType : uint8 +{ + Invalid, + + Button, + ButtonStrip, + Color, + ColorRamp, + File, + FileDir, + FileGeo, + FileImage, + Float, + FloatRamp, + Folder, + FolderList, + Input, + Int, + IntChoice, + Label, + MultiParm, + Separator, + String, + StringChoice, + StringAssetRef, + Toggle, +}; + +UENUM() +enum class EHoudiniParameterChoiceListType : uint8 +{ + /** Parameter is not a menu */ + None = 0, + + /** Menu Only, Single Selection */ + Normal, + + /** Mini Menu Only, Single Selection */ + Mini, + + /** Field + Single Selection Menu */ + Replace, + + /** Field + Multiple Selection Menu */ + Toggle +}; + +UCLASS(DefaultToInstanced) +class HOUDINIENGINERUNTIME_API UHoudiniParameter : public UObject +{ + +public: + + GENERATED_UCLASS_BODY() + + friend class UHoudiniAssetParameter; + friend class FHoudiniEditorEquivalenceUtils; + + // + static UHoudiniParameter * Create(UObject* Outer, const FString& ParamName); + + // Equality, consider two param equals if they have the same name, type, tuple size and disabled status + bool operator==(const UHoudiniParameter& other) const + { + return ( TupleSize == other.TupleSize && ParmType == other.ParmType + && Name.Equals(other.Name) && bIsDisabled == other.bIsDisabled ); + } + + bool Matches(const UHoudiniParameter& other) const { return (*this == other); }; + + //------------------------------------------------------------------------------------------------ + // Accessors + //------------------------------------------------------------------------------------------------ + + // Get parent parameter for this parameter. + virtual const FString & GetParameterName() const { return Name; }; + virtual const FString & GetParameterLabel() const { return Label; }; + virtual const FString GetParameterHelp() const { return Help; }; + + virtual EHoudiniParameterType GetParameterType() const { return ParmType; }; + virtual EHoudiniParameterChoiceListType GetChoiceListType() const { return ChoiceListType; }; + virtual int32 GetTupleSize() const { return TupleSize; }; + virtual int32 GetNodeId() const { return NodeId; }; + virtual int32 GetParmId() const { return ParmId; }; + virtual int32 GetParentParmId() const { return ParentParmId; }; + virtual int32 GetChildIndex() const { return ChildIndex; }; + + virtual bool IsVisible() const { return bIsVisible; }; + virtual bool ShouldDisplay() const{ return bIsVisible && bIsParentFolderVisible && ParmType != EHoudiniParameterType::Invalid; }; + virtual bool IsDisabled() const { return bIsDisabled; }; + virtual bool HasChanged() const { return bHasChanged; }; + virtual bool NeedsToTriggerUpdate() const { return bNeedsToTriggerUpdate; }; + virtual bool IsDefault() const { return true; }; + virtual bool IsSpare() const { return bIsSpare; }; + virtual bool GetJoinNext() const { return bJoinNext; }; + virtual bool IsLabelVisible() const { return bIsLabelVisible; }; + virtual bool IsAutoUpdate() const { return bAutoUpdate; }; + + virtual int32 GetTagCount() const { return TagCount; }; + virtual int32 GetValueIndex() const { return ValueIndex; }; + + virtual TMap& GetTags() { return Tags; }; + + virtual bool IsChildParameter() const; + + virtual bool IsPendingRevertToDefault() const { return bPendingRevertToDefault; }; + virtual void GetTuplePendingRevertToDefault(TArray& OutArray) { OutArray = TuplePendingRevertToDefault; }; + + virtual bool HasExpression() const { return bHasExpression; }; + virtual bool IsShowingExpression() const { return bShowExpression; }; + virtual FString GetExpression() const { return ParamExpression; }; + + //------------------------------------------------------------------------------------------------ + // Mutators + //------------------------------------------------------------------------------------------------ + + // Set parent parameter for this parameter. + virtual void SetParameterName(const FString& InName) { Name = InName; }; + virtual void SetParameterLabel(const FString& InLabel) { Label = InLabel; }; + virtual void SetParameterHelp(const FString& InHelp) { Help = InHelp; }; + + virtual void SetParameterType(const EHoudiniParameterType& InType) { ParmType = InType; }; + virtual void SetChoiceListType(const EHoudiniParameterChoiceListType& InType) { ChoiceListType = InType; }; + virtual void SetTupleSize(const uint32& InTupleSize) { TupleSize = InTupleSize; }; + virtual void SetNodeId(const int32& InNodeId) { NodeId = InNodeId; }; + virtual void SetParmId(const int32& InParmId) { ParmId = InParmId; }; + virtual void SetParentParmId(const int32& InParentParmId) { ParentParmId = InParentParmId; }; + virtual void SetChildIndex(const int32& InChildIndex) { ChildIndex = InChildIndex; }; + + virtual void SetIsChildOfMultiParm(const bool& IsChildOfMultiParam) { bIsChildOfMultiParm = IsChildOfMultiParam; }; + virtual bool GetIsChildOfMultiParm() const { return bIsChildOfMultiParm; }; + + virtual void SetIsDirectChildOfMultiParm(const bool& IsDirectChildOfMultiParam) { bIsDirectChildOfMultiParm = IsDirectChildOfMultiParam; }; + virtual bool IsDirectChildOfMultiParm() const { return bIsDirectChildOfMultiParm; }; + + virtual void SetVisible(const bool& InIsVisible) { bIsVisible = InIsVisible; }; + virtual void SetVisibleParent(const bool& InIsVisible) { bIsParentFolderVisible = InIsVisible; }; + virtual void SetDisabled(const bool& InIsDisabled) { bIsDisabled = InIsDisabled; }; + virtual void SetDefault(const bool& InIsDefault) { bIsDefault = InIsDefault; }; + virtual void SetSpare(const bool& InIsSpare) { bIsSpare = InIsSpare; }; + virtual void SetJoinNext(const bool& InJoinNext) { bJoinNext = InJoinNext; }; + virtual void SetLabelVisible(const bool& bInIsLabelVisible) { bIsLabelVisible = bInIsLabelVisible; }; + + virtual void SetTagCount(const uint32& InTagCount) { TagCount = InTagCount; }; + virtual void SetValueIndex(const uint32& InValueIndex) { ValueIndex = InValueIndex; }; + + virtual void MarkChanged(const bool& bInChanged) { bHasChanged = bInChanged; SetNeedsToTriggerUpdate(bInChanged); }; + virtual void SetNeedsToTriggerUpdate(const bool& bInTriggersUpdate) { bNeedsToTriggerUpdate = bInTriggersUpdate; }; + virtual void RevertToDefault(); + virtual void RevertToDefault(const int32& TupleIndex); + virtual void MarkDefault(const bool& bInDefault); + + virtual void SetHasExpression(const bool& InHasExpression) { bHasExpression = InHasExpression; }; + virtual void SetShowExpression(const bool& InShowExpression) { bShowExpression = InShowExpression; }; + virtual void SetExpression(const FString& InParamExpression) { ParamExpression = InParamExpression; }; + + virtual void SetAutoUpdate(const bool& InAutoUpdate) { bAutoUpdate = InAutoUpdate; }; + + static FString GetStringFromHoudiniInterpMethod(EHoudiniRampInterpolationType InType); + static EHoudiniRampInterpolationType GetHoudiniInterpMethodFromInt(int32 InInt); + static EHoudiniRampInterpolationType GetHoudiniInterpMethodFromString(const FString& InString); + + static ERichCurveInterpMode EHoudiniRampInterpolationTypeToERichCurveInterpMode(EHoudiniRampInterpolationType InType); + + // Duplicate this object for state transfer between component instances and templates + UHoudiniParameter* DuplicateAndCopyState(UObject* DestOuter, EObjectFlags InClearFlags=RF_NoFlags, EObjectFlags InSetFlags=RF_NoFlags); + + virtual void CopyStateFrom(UHoudiniParameter* InParameter, bool bCopyAllProperties, EObjectFlags InClearFlags=RF_NoFlags, EObjectFlags InSetFlags=RF_NoFlags); + + // Replace any input references using the provided mapping + virtual void RemapInputs(const TMap& InputMapping) {}; + + // Replace any parameter references using the provided mapping + virtual void RemapParameters(const TMap& ParameterMapping) {}; + + // Invalidate ids + virtual void InvalidateData(); + + //------------------------------------------------------------------------------------------------ + // Notifications + //------------------------------------------------------------------------------------------------ + + virtual void OnPreCook() {}; + + void OnSessionConnected(); + +protected: + + //--------------------------------------------------------------------------------------------- + // ParmInfos + //--------------------------------------------------------------------------------------------- + + // + UPROPERTY() + FString Name; + + // + UPROPERTY() + FString Label; + + // Unreal type of the parameter + UPROPERTY() + EHoudiniParameterType ParmType; + + // The menu type of this parameter, None if it is not a menu. + UPROPERTY() + EHoudiniParameterChoiceListType ChoiceListType; + + // Tuple size. For scalar parameters this value is 1, but for vector parameters this value can be greater. + UPROPERTY() + uint32 TupleSize; + + // Node this parameter belongs to. + UPROPERTY(DuplicateTransient) + int32 NodeId; + + // Id of this parameter. + UPROPERTY() + int32 ParmId; + + // Id of parent parameter, -1 if root is parent. + UPROPERTY() + int32 ParentParmId; + + // Child index within its parent parameter. + UPROPERTY() + int32 ChildIndex; + + // + UPROPERTY() + bool bIsVisible; + + // Is visible in hierarchy. (e.g. parm can be visible, but containing folder is not) + UPROPERTY() + bool bIsParentFolderVisible; + + // + UPROPERTY() + bool bIsDisabled; + + // Is set to true if value of this parameter has been changed by user. + UPROPERTY() + bool bHasChanged; + + // Is set to true if value of this parameter will trigger an update of the asset + UPROPERTY() + bool bNeedsToTriggerUpdate; + + // Indicates that this parameter is still using its default value + UPROPERTY(Transient, DuplicateTransient) + bool bIsDefault; + + // Permissions for file parms + UPROPERTY() + bool bIsSpare; + + // + UPROPERTY() + bool bJoinNext; + + // Whether the label should be displayed. + UPROPERTY() + bool bIsLabelVisible; + + // + UPROPERTY() + bool bIsChildOfMultiParm; + + UPROPERTY() + bool bIsDirectChildOfMultiParm; + + // Indicates a parameter value needs to be reverted to its default + UPROPERTY(DuplicateTransient) + bool bPendingRevertToDefault; + + UPROPERTY(DuplicateTransient) + TArray TuplePendingRevertToDefault; + + // + UPROPERTY() + FString Help; + + // Number of tags on this parameter + UPROPERTY() + uint32 TagCount; + + // The index to use to look into the values array in order to retrieve the actual value(s) of this parameter. + UPROPERTY() + int32 ValueIndex; + + //------------------------------------------------------------------------------------------------------------------------- + // Expression + // TODO: Use tuple array for this + // Indicates the parameters has an expression value + UPROPERTY() + bool bHasExpression; + + // Indicates we are currently displaying the parameter's value + UPROPERTY(DuplicateTransient) + bool bShowExpression; + + // The parameter's expression + UPROPERTY() + FString ParamExpression; + + UPROPERTY() + TMap Tags; + + UPROPERTY() + bool bAutoUpdate = true; + + +}; + + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterButton.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterButton.h" + +UHoudiniParameterButton::UHoudiniParameterButton(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) +{ + ParmType = EHoudiniParameterType::Button; +} + +UHoudiniParameterButton * +UHoudiniParameterButton::Create( UObject* InOuter, const FString& InParamName ) +{ + FString ParamNameStr = "HoudiniParameterButton_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterButton::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterButton * HoudiniAssetParameter = NewObject< UHoudiniParameterButton >( + InOuter, UHoudiniParameterButton::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::Button); + + //HoudiniAssetParameterButton->UpdateFromParmInfo(InParentParameter, InNodeId, ParmInfo); + + return HoudiniAssetParameter; +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterButton.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterButton.generated.h" + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterButton : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Create instance of this class. + static UHoudiniParameterButton * Create( + UObject* Outer, + const FString& ParamName); +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterButtonStrip.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterButtonStrip.h" + +UHoudiniParameterButtonStrip::UHoudiniParameterButtonStrip(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) +{ + ParmType = EHoudiniParameterType::ButtonStrip; +} + +UHoudiniParameterButtonStrip * +UHoudiniParameterButtonStrip::Create( + UObject* InOuter, + const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterButtonStrip_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterButtonStrip::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterButtonStrip * HoudiniAssetParameter = NewObject< UHoudiniParameterButtonStrip >( + InOuter, UHoudiniParameterButtonStrip::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::ButtonStrip); + + return HoudiniAssetParameter; +} + +bool UHoudiniParameterButtonStrip::GetValueAt(const uint32 Index) const +{ + if (!Labels.IsValidIndex(Index)) + { + return false; + } + + switch (GetChoiceListType()) + { + // Single Selection + case EHoudiniParameterChoiceListType::Normal: + case EHoudiniParameterChoiceListType::Mini: + case EHoudiniParameterChoiceListType::Replace: + return Value == Index; + + // Multiple Selection + case EHoudiniParameterChoiceListType::Toggle: + // Multiple selection is implemented as bitmask + return (Value >> Index) & 1; + + default: + return false; + } +} + +bool UHoudiniParameterButtonStrip::SetValueAt(const bool InValue, const uint32 Index) +{ + if (!Labels.IsValidIndex(Index)) + { + return false; + } + + if (InValue == GetValueAt(Index)) + { + return false; + } + + switch (GetChoiceListType()) + { + // Single Selection + case EHoudiniParameterChoiceListType::Normal: + case EHoudiniParameterChoiceListType::Mini: + case EHoudiniParameterChoiceListType::Replace: + Value = Index; + break; + + // Multiple Selection + case EHoudiniParameterChoiceListType::Toggle: + // Multiple selection is implemented as bitmask + Value ^= (1U << Index); + break; + + default: + // Button strip should have a choice list type. + // If it does not, this parameter was likely generated before choice list type was + // implemented, and thus was assigned default value of None. + return false; + } + + return true; +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterButtonStrip.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterButtonStrip.generated.h" + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterButtonStrip : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Create instance of this class. + static UHoudiniParameterButtonStrip * Create( + UObject* InOuter, + const FString& InParamName); + + // Accessor + bool GetValueAt(const uint32 Index) const; + + FString* GetStringLabelAt(const uint32 Index) + { + return Labels.IsValidIndex(Index) ? &(Labels[Index]) : nullptr; + } + + const FString* GetStringLabelAt(const uint32 Index) const + { + return Labels.IsValidIndex(Index) ? &(Labels[Index]) : nullptr; + } + + int32* GetValuesPtr() { return reinterpret_cast(&Value); } + + bool IsDefault() const override { return DefaultValue == Value; } + + // Mutators + bool SetValueAt(const bool InValue, const uint32 Index); + + /** + * @warning Depending on this parameters choice list type, the value is interpreted differently + * by Houdini. That is, if it is single selection, then the value is an index of the + * current selection. If it is multiple selection, then the value is a bitmask of the + * selected values. + */ + void SetValue(const uint32 InValue) { Value = InValue; } + + void SetNumberOfValues(const uint32 InNumValues) { Labels.SetNum(InNumValues); } + + uint32 GetNumValues() const { return Labels.Num(); } + + void SetDefaultValues() { DefaultValue = Value; } + +protected: + + UPROPERTY() + TArray Labels; + + /** See @ref SetValue */ + UPROPERTY() + uint32 Value; + + UPROPERTY() + uint32 DefaultValue; +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterChoice.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterChoice.h" + +UHoudiniParameterChoice::UHoudiniParameterChoice(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) + , IntValue(-1) +{ + ParmType = EHoudiniParameterType::IntChoice; +} + +UHoudiniParameterChoice * +UHoudiniParameterChoice::Create( UObject* InOuter, const FString& InParamName, const EHoudiniParameterType& InParmType) +{ + FString ParamNameStr = "HoudiniParameterChoice_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterChoice::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterChoice * HoudiniAssetParameter = NewObject< UHoudiniParameterChoice >( + InOuter, UHoudiniParameterChoice::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(InParmType); + + //HoudiniAssetParameter->UpdateFromParmInfo(InParentParameter, InNodeId, ParmInfo); + + return HoudiniAssetParameter; +} + +void +UHoudiniParameterChoice::BeginDestroy() +{ + // We need to clean up our arrays + for (auto& Ptr : ChoiceLabelsPtr) + { + Ptr.Reset(); + } + ChoiceLabelsPtr.Empty(); + + // Then the string arrays + StringChoiceLabels.Empty(); + StringChoiceValues.Empty(); + + IntValuesArray.Empty(); + + Super::BeginDestroy(); +} + + +FString* +UHoudiniParameterChoice::GetStringChoiceValueAt(const int32& InAtIndex) +{ + if (!StringChoiceValues.IsValidIndex(InAtIndex)) + return nullptr; + + return &(StringChoiceValues[InAtIndex]); +} + +FString* +UHoudiniParameterChoice::GetStringChoiceLabelAt(const int32& InAtIndex) +{ + if (!StringChoiceLabels.IsValidIndex(InAtIndex)) + return nullptr; + + return &(StringChoiceLabels[InAtIndex]); +} + +void +UHoudiniParameterChoice::SetNumChoices(const int32& InNumChoices) +{ + // Set the array sizes + StringChoiceValues.SetNumZeroed(InNumChoices); + StringChoiceLabels.SetNumZeroed(InNumChoices); + + IntValuesArray.SetNumZeroed(InNumChoices); + + UpdateChoiceLabelsPtr(); +} + +// Update the pointers to the ChoiceLabels +bool +UHoudiniParameterChoice::UpdateChoiceLabelsPtr() +{ + /* + bool bNeedUpdate = false; + if (StringChoiceLabels.Num() != ChoiceLabelsPtr.Num()) + { + bNeedUpdate = true; + } + else + { + for (int32 Idx = 0; Idx < ChoiceLabelsPtr.Num(); Idx++) + { + if (ChoiceLabelsPtr[Idx].Get() == &(StringChoiceLabels[Idx])) + continue; + + bNeedUpdate = true; + break; + } + } + + if (!bNeedUpdate) + return true; + */ + + // Updates the Label Ptr array + ChoiceLabelsPtr.SetNumZeroed(StringChoiceLabels.Num()); + for (int32 Idx = 0; Idx < ChoiceLabelsPtr.Num(); Idx++) + { + ChoiceLabelsPtr[Idx] = MakeShared(StringChoiceLabels[Idx]); + } + + return true; +} + +bool +UHoudiniParameterChoice::SetIntValue(const int32& InIntValue) +{ + if (InIntValue == IntValue) + return false; + + IntValue = InIntValue; + + return true; +} + +bool +UHoudiniParameterChoice::SetStringValue(const FString& InStringValue) +{ + if (InStringValue.Equals(StringValue)) + return false; + + StringValue = InStringValue; + + return true; +}; + +bool +UHoudiniParameterChoice::UpdateIntValueFromString() +{ + int32 FoundInt = INDEX_NONE; + if (GetParameterType() == EHoudiniParameterType::IntChoice) + { + // Update the int values from the string value + FoundInt = StringChoiceLabels.Find(StringValue); + } + else + { + // Update the int values from the string value + FoundInt = StringChoiceValues.Find(StringValue); + } + + if (FoundInt == INDEX_NONE) + return false; + + if (IntValue == FoundInt) + return false; + + IntValue = FoundInt; + + return true; +} + +bool +UHoudiniParameterChoice::UpdateStringValueFromInt() +{ + // Update the string value from the int value + FString NewStringValue; + if (GetParameterType() == EHoudiniParameterType::IntChoice) + { + // IntChoices only have labels + if (!StringChoiceLabels.IsValidIndex(IntValue)) + return false; + + NewStringValue = StringChoiceLabels[IntValue]; + } + else + { + // StringChoices should use values + if (!StringChoiceValues.IsValidIndex(IntValue)) + return false; + + NewStringValue = StringChoiceValues[IntValue]; + } + + if (StringValue.Equals(NewStringValue)) + return false; + + StringValue = NewStringValue; + + return true; +} + +const int32 +UHoudiniParameterChoice::GetIntValueFromLabel(const FString& InSelectedLabel) const +{ + return StringChoiceLabels.Find(InSelectedLabel); +} + +TOptional< TSharedPtr > +UHoudiniParameterChoice::GetValue(int32 Idx) const +{ + if (Idx == 0 && StringChoiceValues.IsValidIndex(IntValue)) + { + return TOptional< TSharedPtr< FString > >(MakeShared(StringChoiceValues[IntValue])); + } + + return TOptional< TSharedPtr< FString > >(); +} + +bool +UHoudiniParameterChoice::IsDefault() const +{ + if (bIsChildOfRamp) + return true; + + if (GetParameterType() == EHoudiniParameterType::IntChoice) + { + return IntValue == DefaultIntValue; + } + + if (GetParameterType() == EHoudiniParameterType::StringChoice) + { + return StringValue == DefaultStringValue; + } + + return true; +} + +void +UHoudiniParameterChoice::RevertToDefault() +{ + if (!bIsChildOfRamp) + { + bPendingRevertToDefault = true; + TuplePendingRevertToDefault.Empty(); + TuplePendingRevertToDefault.Add(-1); + + MarkChanged(true); + } +} + +int32 UHoudiniParameterChoice::GetIndexFromValueArray(int32 Index) const +{ + return IntValuesArray.Find(Index); +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterChoice.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterChoice.generated.h" + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterChoice : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Create an instance of this class. + static UHoudiniParameterChoice * Create( + UObject* Outer, + const FString& ParamName, + const EHoudiniParameterType& ParmType); + + virtual void BeginDestroy() override; + + //------------------------------------------------------------------------------------------------ + // Accessors + //------------------------------------------------------------------------------------------------ + + const int32 GetIntValueIndex() const { return IntValue; }; + const FString GetStringValue() const { return StringValue; }; + const int32 GetNumChoices() const { return StringChoiceLabels.Num(); }; + const FString GetLabel() const { return StringChoiceLabels.IsValidIndex(IntValue) ? StringChoiceLabels[IntValue] : FString(); }; + const bool IsStringChoice() const { return ParmType == EHoudiniParameterType::StringChoice; }; + const int32 GetIntValue(int32 InIntValueIndex) { return IntValuesArray.IsValidIndex(InIntValueIndex) ? IntValuesArray[InIntValueIndex] : IntValue; } + void SetIntValueArray(int32 Index, int32 Value) { if (IntValuesArray.IsValidIndex(Index)) IntValuesArray[Index] = Value; } + + bool IsDefault() const override; + + TOptional> GetValue(int32 Idx) const; + + const int32 GetIntValueFromLabel(const FString& InSelectedLabel) const; + + FString* GetStringChoiceValueAt(const int32& InAtIndex); + FString* GetStringChoiceLabelAt(const int32& InAtIndex); + + bool IsChildOfRamp() const { return bIsChildOfRamp; }; + + // Returns the ChoiceLabel SharedPtr array, used for UI only + TArray>* GetChoiceLabelsPtr() { return &ChoiceLabelsPtr; }; + + //------------------------------------------------------------------------------------------------ + // Mutators + //------------------------------------------------------------------------------------------------ + bool SetIntValue(const int32& InIntValue); + bool SetStringValue(const FString& InStringValue); + void SetNumChoices(const int32& InNumChoices); + + // For string choices only, update the int values from the string value + bool UpdateIntValueFromString(); + // For int choices only, update the string value from the int value + bool UpdateStringValueFromInt(); + // Update the pointers to the ChoiceLabels + bool UpdateChoiceLabelsPtr(); + + void SetDefaultIntValue() { DefaultIntValue = IntValue; }; + void SetDefaultStringValue() { DefaultStringValue = StringValue; }; + + void SetIsChildOfRamp() { bIsChildOfRamp = true; }; + + void RevertToDefault() override; + + int32 GetIndexFromValueArray(int32 Index) const; + +protected: + + // Current int value for this property. + // More of an index to IntValuesArray + UPROPERTY() + int32 IntValue; + + // Default int value for this property, assigned at creating the parameter. + UPROPERTY() + int32 DefaultIntValue; + + // Current string value for this property + UPROPERTY() + FString StringValue; + + // Default string value for this property, assigned at creating the parameter. + UPROPERTY() + FString DefaultStringValue; + + // Used only for StringChoices! + // All the possible string values for this parameter's choices + UPROPERTY() + TArray StringChoiceValues; + + // Labels corresponding to this parameter's choices. + UPROPERTY() + TArray StringChoiceLabels; + + // Array of SharedPtr pointing to this parameter's label, used for UI only + TArray> ChoiceLabelsPtr; + + UPROPERTY() + bool bIsChildOfRamp; + + // An array containing the values of all choices + // IntValues[i] should be i unless UseMenuItemTokenAsValue is enabled. + UPROPERTY() + TArray IntValuesArray; + + +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterColor.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterColor.h" + +UHoudiniParameterColor::UHoudiniParameterColor(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) + , Color( FLinearColor::White ) +{ + ParmType = EHoudiniParameterType::Color; +} + +UHoudiniParameterColor * +UHoudiniParameterColor::Create( UObject* InOuter, const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterColor_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterColor::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterColor * HoudiniAssetParameter = NewObject< UHoudiniParameterColor >( + InOuter, UHoudiniParameterColor::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::Color); + //HoudiniAssetParameter->UpdateFromParmInfo(InParentParameter, InNodeId, ParmInfo); + + HoudiniAssetParameter->bIsChildOfRamp = false; + return HoudiniAssetParameter; +} + +bool +UHoudiniParameterColor::SetColorValue(const FLinearColor& InColor) +{ + if (InColor == Color) + return false; + + Color = InColor; + + return true; +} + +bool +UHoudiniParameterColor::IsDefault() const +{ + if (bIsChildOfRamp) + return true; + + return Color == DefaultColor; +} + +void +UHoudiniParameterColor::RevertToDefault() +{ + if (!bIsChildOfRamp) + { + bPendingRevertToDefault = true; + TuplePendingRevertToDefault.Empty(); + TuplePendingRevertToDefault.Add(-1); + + MarkChanged(true); + } +} +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterColor.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterColor.generated.h" + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterColor : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + + public: + + // Create instance of this class. + static UHoudiniParameterColor * Create( + UObject* Outer, + const FString& ParamName); + + // Accessor + FLinearColor GetColorValue() const { return Color; }; + + bool IsDefault() const override; + + bool IsChildOfRamp() const { return bIsChildOfRamp; }; + + // Mutators + bool SetColorValue(const FLinearColor& InColor); + + void SetDefaultValue() { DefaultColor = Color; }; + + void SetIsChildOfRamp() { bIsChildOfRamp = true; }; + + void RevertToDefault() override; + + protected: + + // Color for this property. + UPROPERTY() + FLinearColor Color; + + // Default color for this property + UPROPERTY() + FLinearColor DefaultColor; + + UPROPERTY() + bool bIsChildOfRamp; +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterFile.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterFile.h" + +#include "HoudiniAsset.h" +#include "HoudiniAssetComponent.h" + +#include "Misc/Paths.h" + +UHoudiniParameterFile::UHoudiniParameterFile(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) + , Filters() + , bIsReadOnly(false) +{ + ParmType = EHoudiniParameterType::File; +} + +UHoudiniParameterFile * +UHoudiniParameterFile::Create( + UObject* InOuter, + const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterFile_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterFile::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterFile * HoudiniAssetParameter = NewObject< UHoudiniParameterFile >( + InOuter, UHoudiniParameterFile::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::File); + + return HoudiniAssetParameter; +} + + +bool +UHoudiniParameterFile::SetValueAt(const FString& InValue, const uint32& Index) +{ + if (!Values.IsValidIndex(Index)) + return false; + + if (Values[Index] == InValue) + return false; + + Values[Index] = InValue; + + return true; +} + +bool +UHoudiniParameterFile::IsDefault() const +{ + for (int32 Idx = 0; Idx < Values.Num(); ++Idx) + { + if (!DefaultValues.IsValidIndex(Idx)) + break; + + if (Values[Idx] != DefaultValues[Idx]) + return false; + } + + return true; +} + +void +UHoudiniParameterFile::SetDefaultValues() +{ + if (DefaultValues.Num() > 0) + return; + + DefaultValues.Empty(); + for (int32 Idx = 0; Idx < Values.Num(); ++Idx) + { + DefaultValues.Add(Values[Idx]); + } +} + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterFile.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterFile.generated.h" + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterFile : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Create instance of this class. + static UHoudiniParameterFile * Create( + UObject* Outer, + const FString& ParamName); + + // Accessors + FString GetFileFilters() const { return Filters; }; + bool IsReadOnly() const { return bIsReadOnly; }; + FString GetValueAt(int32 Index) const { return Values[Index]; }; + int32 GetNumValues() const { return Values.Num(); }; + + void SetNumberOfValues(const uint32& NumValues) { Values.SetNum(NumValues); }; + bool SetValueAt(const FString& InValue, const uint32& Index); + + bool IsDefault() const override; + + // Mutators + void SetFileFilters(const FString& InFilters) { Filters = InFilters; }; + void SetReadOnly(const bool& InReadOnly) { bIsReadOnly = InReadOnly; }; + + void SetDefaultValues(); + +protected: + + // Values of this property. + UPROPERTY() + TArray Values; + + UPROPERTY() + TArray DefaultValues; + + // Filters of this property. + UPROPERTY() + FString Filters; + + // Is the file parameter read-only? + UPROPERTY() + bool bIsReadOnly; +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterFloat.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterFloat.h" + +UHoudiniParameterFloat::UHoudiniParameterFloat(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) + , Unit(TEXT("")) + , bNoSwap(false) + , bHasMin(false) + , bHasMax(false) + , bHasUIMin(false) + , bHasUIMax(false) + , bIsLogarithmic(false) + , Min(TNumericLimits::Lowest()) + , Max(TNumericLimits::Max()) + , UIMin(TNumericLimits::Lowest()) + , UIMax(TNumericLimits::Max()) +{ + ParmType = EHoudiniParameterType::Float; +} + +UHoudiniParameterFloat * +UHoudiniParameterFloat::Create( + UObject* InOuter, + const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterFloat_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterFloat::StaticClass(), *ParamNameStr); + // We need to create a new parameter + UHoudiniParameterFloat * HoudiniAssetParameter = NewObject< UHoudiniParameterFloat >( + InOuter, UHoudiniParameterFloat::StaticClass(), ParamName, RF_Public | RF_Transactional); + + //HoudiniAssetParameter->UpdateFromParmInfo(InParentParameter, InNodeId, ParmInfo); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::Float); + + HoudiniAssetParameter->bIsChildOfRamp = false; + HoudiniAssetParameter->bIsLogarithmic = false; + return HoudiniAssetParameter; +} + +bool +UHoudiniParameterFloat::SetValuesIfChanged(const TArray& NewValues) +{ + // Sets the new values, and returns true if any (or number) of parameters changed. + if (NewValues.Num() != Values.Num()) + { + Values = NewValues; + bHasChanged = true; + return true; + } + + for (int Index = 0; Index < NewValues.Num(); Index++) + { + if (Values[Index] != NewValues[Index]) + { + Values = NewValues; + bHasChanged = true; + return true; + } + } + return false; +} + +TOptional< float > +UHoudiniParameterFloat::GetValue(int32 Idx) const +{ + if (Values.IsValidIndex(Idx)) + return TOptional< float >(Values[Idx]); + else + return TOptional< float >(); +} + +bool +UHoudiniParameterFloat::GetValueAt(const int32& AtIndex, float& OutValue) const +{ + if (!Values.IsValidIndex(AtIndex)) + return false; + + OutValue = Values[AtIndex]; + + return true; +} + +bool +UHoudiniParameterFloat::SetValueAt(const float& InValue, const int32& AtIndex) +{ + if (!Values.IsValidIndex(AtIndex)) + return false; + + if (InValue == Values[AtIndex]) + return false; + + Values[AtIndex] = FMath::Clamp< float >(InValue, Min, Max); + + return true; +} + +void +UHoudiniParameterFloat::SetDefaultValues() +{ + if (DefaultValues.Num() > 0) + return; + + DefaultValues.Empty(); + for (int32 Idx = 0; Idx < Values.Num(); ++Idx) + { + DefaultValues.Add(Values[Idx]); + } +} + +bool +UHoudiniParameterFloat::IsDefaultValueAtIndex(const int32& Idx) const +{ + if (!Values.IsValidIndex(Idx) || !DefaultValues.IsValidIndex(Idx)) + return true; + + return Values[Idx] == DefaultValues[Idx]; +} + +bool +UHoudiniParameterFloat::IsDefault() const +{ + if (bIsChildOfRamp) + return true; + + for (int32 Idx = 0; Idx < Values.Num(); ++Idx) + { + if (!IsDefaultValueAtIndex(Idx)) + return false; + } + + return true; +} + +void +UHoudiniParameterFloat::SetValue(float InValue, int32 Idx) +{ + if (!Values.IsValidIndex(Idx)) + return; + + if (InValue == Values[Idx]) + return; + + Values[Idx] = FMath::Clamp< float >(InValue, Min, Max); +} + +void +UHoudiniParameterFloat::RevertToDefault() +{ + if (!bIsChildOfRamp) + { + bPendingRevertToDefault = true; + TuplePendingRevertToDefault.Empty(); + TuplePendingRevertToDefault.Add(-1); + + MarkChanged(true); + } +} + +void +UHoudiniParameterFloat::RevertToDefault(const int32& TupleIndex) +{ + bPendingRevertToDefault = true; + TuplePendingRevertToDefault.AddUnique(TupleIndex); + + MarkChanged(true); +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterFloat.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterFloat.generated.h" + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterFloat : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Create instance of this class. + static UHoudiniParameterFloat * Create( + UObject* Outer, + const FString& ParamName); + + // Accessors + FString GetUnit() const { return Unit; }; + bool GetNoSwap() const { return bNoSwap; }; + + bool HasMin() const { return bHasMin; }; + bool HasMax() const { return bHasMax; }; + bool HasUIMin() const { return bHasUIMin; }; + bool HasUIMax() const { return bHasUIMax; }; + bool IsLogarithmic() const { return bIsLogarithmic; }; + + float GetMin() const { return Min; }; + float GetMax() const { return Max; }; + float GetUIMin() const { return UIMin; }; + float GetUIMax() const { return UIMax; }; + + bool IsChildOfRamp() const { return bIsChildOfRamp; }; + + // Check if current value at Index is the default + bool IsDefaultValueAtIndex(const int32& Idx) const; + + bool IsDefault() const override; + + // Get value of this property + TOptional< float > GetValue(int32 Idx) const; + bool GetValueAt(const int32& AtIndex, float& OutValue) const; + + // Write access to the value array + float* GetValuesPtr() { return Values.Num() > 0 ? &Values[0] : nullptr; }; + + int32 GetNumberOfValues() const { return Values.Num(); }; + + // Mutators + void SetHasMin(const bool& InHasMin) { bHasMin = InHasMin; }; + void SetHasMax(const bool& InHasMax) { bHasMax = InHasMax; }; + void SetHasUIMin(const bool& InHasUIMin) { bHasUIMin = InHasUIMin; }; + void SetHasUIMax(const bool& InHasUIMax) { bHasUIMax = InHasUIMax; }; + void SetIsLogarithmic(const bool& bInLog) { bIsLogarithmic = bInLog; }; + + void SetMin(const float& InMin) { Min = InMin; }; + void SetMax(const float& InMax) { Max = InMax; }; + void SetUIMin(const float& InUIMin) { UIMin = InUIMin; }; + void SetUIMax(const float& InUIMax) { UIMax = InUIMax; }; + + void SetUnit(const FString& InUnit) { Unit = InUnit; }; + void SetNoSwap(const bool& InNoSwap) { bNoSwap = InNoSwap; }; + + void SetDefaultValues(); + + void SetNumberOfValues(const uint32& InNumValues) { Values.SetNum(InNumValues); }; + bool SetValueAt(const float& InValue, const int32& AtIndex); + + void SetIsChildOfRamp() { bIsChildOfRamp = true; }; + + /** Set value of this property, used by Slate. **/ + void SetValue(float InValue, int32 Idx); + + void RevertToDefault() override; + void RevertToDefault(const int32& TupleIndex) override; + +#if WITH_EDITOR + void SwitchUniformLock() { bUniformLocked = !bUniformLocked; }; + bool IsUniformLocked() const { return bUniformLocked; }; +#endif + + const TArray& GetDefaultValues() const { return DefaultValues; } + bool SetValuesIfChanged(const TArray & Values); + +protected: + + // Float Values + UPROPERTY() + TArray Values; + + // Default float values, assigned at creating the parameter + UPROPERTY() + TArray DefaultValues; + + // Unit for this property + UPROPERTY() + FString Unit; + + // Do we have the noswap tag? + UPROPERTY() + bool bNoSwap; + + // Indicates we have a min/max value + UPROPERTY() + bool bHasMin; + // + UPROPERTY() + bool bHasMax; + + // Indicates we have a UI min/max + UPROPERTY() + bool bHasUIMin; + // + UPROPERTY() + bool bHasUIMax; + + UPROPERTY() + bool bIsLogarithmic; + + // Min and Max values for this property. + UPROPERTY() + float Min; + // + UPROPERTY() + float Max; + + // Min and Max values of this property for slider UI + UPROPERTY() + float UIMin; + // + UPROPERTY() + float UIMax; + + UPROPERTY() + bool bIsChildOfRamp; + +#if WITH_EDITORONLY_DATA + // Indicates whether the float VEC change value uniformly + UPROPERTY(Transient, DuplicateTransient, NonTransactional) + bool bUniformLocked; +#endif +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterFolder.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterFolder.h" + +UHoudiniParameterFolder::UHoudiniParameterFolder(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) + ,bExpanded(true) + ,bChosen(false) +{ + ParmType = EHoudiniParameterType::Folder; +} + +UHoudiniParameterFolder * +UHoudiniParameterFolder::Create( + UObject* InOuter, + const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterFolder_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterFolder::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterFolder * HoudiniAssetParameter = NewObject< UHoudiniParameterFolder >( + InOuter, UHoudiniParameterFolder::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::Folder); + //HoudiniAssetParameter->UpdateFromParmInfo(InParentParameter, InNodeId, ParmInfo); + return HoudiniAssetParameter; +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterFolder.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterFolder.generated.h" + +UENUM() +enum class EHoudiniFolderParameterType : uint8 +{ + Invalid, + + Collapsible, + Simple, + Tabs, + Radio, + Other, +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterFolder : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Create instance of this class. + static UHoudiniParameterFolder * Create( + UObject* Outer, + const FString& ParamName); + + FORCEINLINE + EHoudiniFolderParameterType GetFolderType() const { return FolderType; }; + + FORCEINLINE + void SetFolderType(EHoudiniFolderParameterType Type) { FolderType = Type; }; + + FORCEINLINE + void SetExpanded(const bool InExpanded) { bExpanded = InExpanded; }; + FORCEINLINE + bool IsExpanded() const { return bExpanded; }; + FORCEINLINE + void ExpandButtonClicked() { bExpanded = !bExpanded; }; + + FORCEINLINE + void SetChosen(const bool InChosen) { bChosen = InChosen; }; + FORCEINLINE + bool IsChosen() const { return bChosen; }; + + FORCEINLINE + bool IsTab() const { return FolderType == EHoudiniFolderParameterType::Tabs || FolderType == EHoudiniFolderParameterType::Radio; }; + + + FORCEINLINE + void ResetChildCounter() { ChildCounter = TupleSize; } + + FORCEINLINE + int32& GetChildCounter() { return ChildCounter; }; + + FORCEINLINE + bool IsContentShown() const { return bIsContentShown; }; + + FORCEINLINE + void SetIsContentShown(const bool& bInShown) { bIsContentShown = bInShown; }; + + + +private: + UPROPERTY() + EHoudiniFolderParameterType FolderType; + + UPROPERTY() + bool bExpanded; + + UPROPERTY() + bool bChosen; + + + UPROPERTY() + int32 ChildCounter; + + UPROPERTY() + bool bIsContentShown; + +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterFolderList.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterFolderList.h" +#include "HoudiniParameterFolder.h" + +#include "HoudiniEngineRuntimePrivatePCH.h" + +UHoudiniParameterFolderList::UHoudiniParameterFolderList(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer), bIsTabMenu(false), bIsTabsShown(false) +{ + ParmType = EHoudiniParameterType::FolderList; +} + +UHoudiniParameterFolderList * +UHoudiniParameterFolderList::Create( + UObject* InOuter, + const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterFolderList_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterFolderList::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterFolderList * HoudiniAssetParameter = NewObject< UHoudiniParameterFolderList >( + InOuter, UHoudiniParameterFolderList::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::FolderList); + + HoudiniAssetParameter->bIsTabMenu = false; + return HoudiniAssetParameter; +} + +void +UHoudiniParameterFolderList::AddTabFolder(UHoudiniParameterFolder* InFolderParm) +{ + TabFolders.Add(InFolderParm); +} + +bool +UHoudiniParameterFolderList::IsTabParseFinished() const +{ + for (auto & CurTab : TabFolders) + { + if (!IsValid(CurTab)) + continue; + + if (!CurTab->IsTab()) + continue; + + // Go through visible tab only + if (!CurTab->IsChosen()) + continue; + + if (CurTab->GetChildCounter() > 0) + return false; + } + + return true; +} + +void +UHoudiniParameterFolderList::RemapParameters(const TMap& InputMapping) +{ + const int32 NumFolders = TabFolders.Num(); + for (int i = 0; i < NumFolders; i++) + { + UHoudiniParameter* FromParameter = TabFolders[i]; + + if (!FromParameter) + continue; + + UHoudiniParameterFolder* ToParameter = nullptr; + if (InputMapping.Contains(FromParameter)) + { + ToParameter = Cast(InputMapping.FindRef(FromParameter)); + } + + if (!ToParameter) + { + HOUDINI_LOG_WARNING(TEXT("[UHoudiniParameterFolderList::RemapParameters] Could not find mapping for existing parameter %s (%s)."), *(FromParameter->GetParameterName()), *(FromParameter->GetPathName()) ); + } + + TabFolders[i] = ToParameter; + } +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterFolderList.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterFolderList.generated.h" + +class UHoudiniParameterFolder; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterFolderList : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Create instance of this class. + static UHoudiniParameterFolderList * Create( + UObject* Outer, + const FString& ParamName); + + void AddTabFolder(UHoudiniParameterFolder* InFolderParm); + + FORCEINLINE + TArray>& GetTabs() { return TabFolders; }; + + FORCEINLINE + bool IsTabMenu() const { return bIsTabMenu; }; + + FORCEINLINE + void SetIsTabMenu(const bool InIsTabMenu) { bIsTabMenu = InIsTabMenu; }; + + FORCEINLINE + bool IsTabsShown() const { return bIsTabsShown; }; + + FORCEINLINE + void SetTabsShown(const bool& bInTabsShown) { bIsTabsShown = bInTabsShown; }; + + bool IsTabParseFinished() const; + + UPROPERTY() + bool bIsTabMenu; + + UPROPERTY() + bool bIsTabsShown; + + // NOTE: TabFolders was previously not transient, but it just seems to be set and cleared in the details panel. + // Having it non-transient creates error when duplicating properties. + UPROPERTY(Transient) + TArray> TabFolders; + + + //------------------------------------------------------------------------------------------------ + // UHoudiniParameter overrides + //------------------------------------------------------------------------------------------------ + virtual void RemapParameters(const TMap& InputMapping) override; + +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterInt.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterInt.h" + +UHoudiniParameterInt::UHoudiniParameterInt(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) + , Unit() + , bHasMin(false) + , bHasMax(false) + , bHasUIMin(false) + , bHasUIMax(false) + , bIsLogarithmic(false) + , Min(0) + , Max(0) + , UIMin(0) + , UIMax(0) +{ + ParmType = EHoudiniParameterType::Int; +} + +UHoudiniParameterInt * +UHoudiniParameterInt::Create( + UObject* InOuter, + const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterInt_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterInt::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterInt * HoudiniAssetParameter = NewObject< UHoudiniParameterInt >( + InOuter, UHoudiniParameterInt::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::Int); + HoudiniAssetParameter->bIsLogarithmic = false; + //HoudiniAssetParameter->UpdateFromParmInfo(InParentParameter, InNodeId, ParmInfo); + + return HoudiniAssetParameter; +} + +bool +UHoudiniParameterInt::SetValuesIfChanged(const TArray& NewValues) +{ + // Sets the new values, and returns true if any (or number) of parameters changed. + if(NewValues.Num() != Values.Num()) + { + Values = NewValues; + bHasChanged = true; + return true; + } + + for(int Index = 0; Index < NewValues.Num(); Index++) + { + if(Values[Index] != NewValues[Index]) + { + Values = NewValues; + bHasChanged = true; + return true; + } + } + return false; +} + + + +TOptional +UHoudiniParameterInt::GetValue(int32 Idx) const +{ + if (Values.IsValidIndex(Idx)) + return TOptional(Values[Idx]); + else + return TOptional(); +} + +bool +UHoudiniParameterInt::GetValueAt(const int32& AtIndex, int32& OutValue) const +{ + if (!Values.IsValidIndex(AtIndex)) + return false; + + OutValue = Values[AtIndex]; + + return true; +} + +bool +UHoudiniParameterInt::SetValueAt(const int32& InValue, const int32& AtIndex) +{ + if (!Values.IsValidIndex(AtIndex)) + return false; + + if (InValue == Values[AtIndex]) + return false; + + Values[AtIndex] = FMath::Clamp< int32 >(InValue, Min, Max); + + return true; +} + +void +UHoudiniParameterInt::SetDefaultValues() +{ + if (DefaultValues.Num() > 0) + return; + + DefaultValues.Empty(); + for (int32 Idx = 0; Idx < Values.Num(); ++Idx) + { + DefaultValues.Add(Values[Idx]); + } +} + +bool +UHoudiniParameterInt::IsDefaultValueAtIndex(const int32& Idx) const +{ + if (!Values.IsValidIndex(Idx) || !DefaultValues.IsValidIndex(Idx)) + return true; + + return Values[Idx] == DefaultValues[Idx]; +} + +bool +UHoudiniParameterInt::IsDefault() const +{ + for (int32 Idx = 0; Idx < Values.Num(); ++Idx) + { + if (!IsDefaultValueAtIndex(Idx)) + return false; + } + + return true; +} +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterInt.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterInt.generated.h" + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterInt : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Create instance of this class. + static UHoudiniParameterInt * Create( + UObject* Outer, + const FString& ParamName); + + // Accessors + FString GetUnit() const { return Unit; }; + + bool HasMin() const { return bHasMin; }; + bool HasMax() const { return bHasMax; }; + bool HasUIMin() const { return bHasUIMin; }; + bool HasUIMax() const { return bHasUIMax; }; + bool IsLogarithmic() const { return bIsLogarithmic; }; + + int32 GetMin() const { return Min; }; + int32 GetMax() const { return Max; }; + int32 GetUIMin() const { return UIMin; }; + int32 GetUIMax() const { return UIMax; }; + + // Get value of this property + TOptional GetValue(int32 Idx) const; + bool GetValueAt(const int32& AtIndex, int32& OutValue) const; + + int32* GetValuesPtr() { return Values.Num() > 0 ? &Values[0] : nullptr; }; + + int32 GetNumberOfValues() const { return Values.Num(); }; + + bool IsDefaultValueAtIndex(const int32& Idx) const; + bool IsDefault() const override; + + // Mutators + void SetHasMin(const bool& InHasMin) { bHasMin = InHasMin; }; + void SetHasMax(const bool& InHasMax) { bHasMax = InHasMax; }; + void SetHasUIMin(const bool& InHasUIMin) { bHasUIMin = InHasUIMin; }; + void SetHasUIMax(const bool& InHasUIMax) { bHasUIMax = InHasUIMax; }; + + void SetMin(const int32& InMin) { Min = InMin; }; + void SetMax(const int32& InMax) { Max = InMax; }; + void SetUIMin(const int32& InUIMin) { UIMin = InUIMin; }; + void SetUIMax(const int32& InUIMax) { UIMax = InUIMax; }; + void SetIsLogarithmic(const bool& bInIsLog) { bIsLogarithmic = bInIsLog; }; + + void SetUnit(const FString& InUnit) { Unit = InUnit; }; + + void SetNumberOfValues(const uint32& InNumValues) { Values.SetNum(InNumValues); }; + bool SetValueAt(const int32& InValue, const int32& AtIndex); + + void SetDefaultValues(); + + const TArray& GetDefaultValues() { return DefaultValues; }; + bool SetValuesIfChanged(const TArray& Values); + +protected: + + // Int Values + UPROPERTY() + TArray Values; + + UPROPERTY() + TArray DefaultValues; + + // Unit for this property + UPROPERTY() + FString Unit; + + // Indicates we have a min/max value + UPROPERTY() + bool bHasMin; + // + UPROPERTY() + bool bHasMax; + + // Indicates we have a UI min/max + UPROPERTY() + bool bHasUIMin; + // + UPROPERTY() + bool bHasUIMax; + + UPROPERTY() + bool bIsLogarithmic; + + // Min and Max values for this property. + UPROPERTY() + int32 Min; + // + UPROPERTY() + int32 Max; + + // Min and Max values of this property for slider UI + UPROPERTY() + int32 UIMin; + // + UPROPERTY() + int32 UIMax; +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterLabel.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterLabel.h" + +UHoudiniParameterLabel::UHoudiniParameterLabel(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) +{ + ParmType = EHoudiniParameterType::Label; +} + +UHoudiniParameterLabel * +UHoudiniParameterLabel::Create( + UObject* InOuter, + const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterLabel_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterLabel::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterLabel * HoudiniAssetParameter = NewObject< UHoudiniParameterLabel >( + InOuter, UHoudiniParameterLabel::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::Label); + //HoudiniAssetParameter->UpdateFromParmInfo(InParentParameter, InNodeId, ParmInfo); + + return HoudiniAssetParameter; +} + +FString +UHoudiniParameterLabel::GetStringAtIndex(int32 Index) +{ + if (LabelStrings.IsValidIndex(Index)) + { + return LabelStrings[Index]; + } + + return FString(""); +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterLabel.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterLabel.generated.h" + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterLabel : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Create instance of this class. + static UHoudiniParameterLabel * Create( + UObject* Outer, + const FString& ParamName); + + UPROPERTY() + TArray LabelStrings; + + FORCEINLINE + void AddLabelString(FString &NewString) { LabelStrings.Add(NewString); }; + + FString GetStringAtIndex(int32 Index); + + FORCEINLINE + void EmptyLabelString() { LabelStrings.Empty(); }; + +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterMultiParm.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterMultiParm.h" + +UHoudiniParameterMultiParm::UHoudiniParameterMultiParm(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer), bIsShown(false), InstanceStartOffset(0) +{ + // TODO Proper Init + ParmType = EHoudiniParameterType::MultiParm; +} + +UHoudiniParameterMultiParm * +UHoudiniParameterMultiParm::Create( + UObject* InOuter, + const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterMultiParm_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterMultiParm::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterMultiParm * HoudiniAssetParameter = NewObject< UHoudiniParameterMultiParm >( + InOuter, UHoudiniParameterMultiParm::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::MultiParm); + //HoudiniAssetParameter->UpdateFromParmInfo(InParentParameter, InNodeId, ParmInfo); + + HoudiniAssetParameter->DefaultInstanceCount = -1; + + return HoudiniAssetParameter; +} + +bool +UHoudiniParameterMultiParm::SetValue(const int32& InValue) +{ + if (InValue == Value) + return false; + + Value = InValue; + + return true; +} + +void +UHoudiniParameterMultiParm::InsertElement() +{ + if (MultiParmInstanceCount > 0 && MultiParmInstanceLastModifyArray.Num() == 0) + InitializeModifyArray(); + + MultiParmInstanceLastModifyArray.Add(EHoudiniMultiParmModificationType::Inserted); +} + +void +UHoudiniParameterMultiParm::InsertElementAt(int32 Index) +{ + if (MultiParmInstanceCount > 0 && MultiParmInstanceLastModifyArray.Num() == 0) + InitializeModifyArray(); + + if (Index >= MultiParmInstanceLastModifyArray.Num()) + MultiParmInstanceLastModifyArray.Add(EHoudiniMultiParmModificationType::Inserted); + else + MultiParmInstanceLastModifyArray.Insert(EHoudiniMultiParmModificationType::Inserted, Index); +} + +/** Decrement value, used by Slate. **/ +void +UHoudiniParameterMultiParm::RemoveElement(int32 Index) +{ + if (MultiParmInstanceCount > 0 && MultiParmInstanceLastModifyArray.Num() == 0) + InitializeModifyArray(); + + // Remove the last element + if (Index == -1) + { + Index = MultiParmInstanceLastModifyArray.Num() - 1; + while (MultiParmInstanceLastModifyArray.IsValidIndex(Index) && MultiParmInstanceLastModifyArray[Index] == EHoudiniMultiParmModificationType::Removed) + Index -= 1; + } + + if (MultiParmInstanceLastModifyArray.IsValidIndex(Index)) + { + // If the removed is a to be inserted instance, simply remove it. + if (MultiParmInstanceLastModifyArray[Index] == EHoudiniMultiParmModificationType::Inserted) + MultiParmInstanceLastModifyArray.RemoveAt(Index); + // Otherwise mark it as to be removed. + else + MultiParmInstanceLastModifyArray[Index] = EHoudiniMultiParmModificationType::Removed; + } +} + +void +UHoudiniParameterMultiParm::EmptyElements() +{ + if (MultiParmInstanceCount > 0 && MultiParmInstanceLastModifyArray.Num() == 0) + InitializeModifyArray(); + + for (int32 Index = MultiParmInstanceLastModifyArray.Num() - 1; Index >= 0; --Index) + { + // If the removed is a to be inserted instance, simply remove it. + // Interation starts from the tail, so that the indices won't be changed by element removal. + if (MultiParmInstanceLastModifyArray[Index] == EHoudiniMultiParmModificationType::Inserted) + MultiParmInstanceLastModifyArray.RemoveAt(Index); + else // Otherwise mark it as to be removed. + MultiParmInstanceLastModifyArray[Index] = EHoudiniMultiParmModificationType::Removed; + } +} + +int32 +UHoudiniParameterMultiParm::GetNextInstanceCount() const +{ + if (MultiParmInstanceCount > 0 && MultiParmInstanceLastModifyArray.Num() == 0) + { + return MultiParmInstanceCount; + } + + int32 CurrentInstanceCount = 0; + // First determine how many instances the multi parm would have based on the current values in + // MultiParmInstanceLastModifyArray + for (const EHoudiniMultiParmModificationType& ModificationType : MultiParmInstanceLastModifyArray) + { + switch (ModificationType) + { + case EHoudiniMultiParmModificationType::Inserted: + case EHoudiniMultiParmModificationType::Modified: + case EHoudiniMultiParmModificationType::None: + CurrentInstanceCount++; + break; + case EHoudiniMultiParmModificationType::Removed: + // Removed indices don't add to CurrentInstanceCount + break; + } + } + + return CurrentInstanceCount; +} + +bool +UHoudiniParameterMultiParm::SetNumElements(const int32 InInstanceCount) +{ + if (MultiParmInstanceCount > 0 && MultiParmInstanceLastModifyArray.Num() == 0) + InitializeModifyArray(); + + // // Log the MultiParmInstanceLastModifyArray before the modification + // HOUDINI_LOG_WARNING(TEXT("MultiParmInstanceLastModifyArray (before): ")); + // for (const EHoudiniMultiParmModificationType Modification : MultiParmInstanceLastModifyArray) + // { + // HOUDINI_LOG_WARNING(TEXT("\t%s"), *UEnum::GetValueAsString(Modification)); + // } + + const int32 TargetInstanceCount = InInstanceCount >= 0 ? InInstanceCount : 0; + const int32 CurrentInstanceCount = GetNextInstanceCount(); + bool bModified = false; + if (CurrentInstanceCount > TargetInstanceCount) + { + // Remove entries from the end of the array + for (int32 Count = CurrentInstanceCount; Count > TargetInstanceCount; --Count) + { + RemoveElement(-1); + } + + bModified = true; + } + else if (CurrentInstanceCount < TargetInstanceCount) + { + // Insert new instances at the end + for (int32 Count = CurrentInstanceCount; Count < TargetInstanceCount; ++Count) + { + InsertElement(); + } + + bModified = true; + } + + // // Log the MultiParmInstanceLastModifyArray after the modification + // HOUDINI_LOG_WARNING(TEXT("MultiParmInstanceLastModifyArray (after): ")); + // for (const EHoudiniMultiParmModificationType Modification : MultiParmInstanceLastModifyArray) + // { + // HOUDINI_LOG_WARNING(TEXT("\t%s"), *UEnum::GetValueAsString(Modification)); + // } + + return bModified; +} + +void +UHoudiniParameterMultiParm::InitializeModifyArray() +{ + for (uint32 Index = 0; Index < MultiParmInstanceCount; ++Index) + { + MultiParmInstanceLastModifyArray.Add(EHoudiniMultiParmModificationType::None); + } +} + +bool +UHoudiniParameterMultiParm::IsDefault() const +{ + //UE_LOG(LogTemp, Warning, TEXT("%d, %d"), MultiParmInstanceNum, MultiParmInstanceCount); + return DefaultInstanceCount == MultiParmInstanceCount; +} + +void +UHoudiniParameterMultiParm::SetDefaultInstanceCount(int32 InCount) +{ + if (DefaultInstanceCount >= 0) + return; + + DefaultInstanceCount = InCount; +} + +void UHoudiniParameterMultiParm::MarkDefault(const bool& bInDefault) +{ + Super::MarkDefault(bInDefault); + if (bInDefault) + MultiParmInstanceLastModifyArray.Empty(); +} +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterMultiParm.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterMultiParm.generated.h" + +UENUM() +enum class EHoudiniMultiParmModificationType : uint8 +{ + None, + + Inserted, + Removed, + Modified +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterMultiParm : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + + +public: + + // Create instance of this class. + static UHoudiniParameterMultiParm * Create( + UObject* Outer, + const FString& ParamName); + + // Accessors + FORCEINLINE + int32 GetValue() const { return Value; }; + FORCEINLINE + int32 GetInstanceCount() const { return MultiParmInstanceCount; }; + + // Mutators + bool SetValue(const int32& InValue); + FORCEINLINE + void SetInstanceCount(const int32 InCount) { MultiParmInstanceCount = InCount; }; + + FORCEINLINE + void SetIsShown(const bool InIsShown) { bIsShown = InIsShown; }; + + FORCEINLINE + bool IsShown() const { return bIsShown; }; + + + /** Increment value, used by Slate. **/ + void InsertElement(); + + void InsertElementAt(int32 Index); + + /** Decrement value, used by Slate. **/ + void RemoveElement(int32 Index); + + /** Empty the values, used by Slate. **/ + void EmptyElements(); + + /** + * Returns the number of multiparm instances there'll be after the next upload to HAPI, after + * the current state of MultiParmInstanceLastModifyArray is applied. + */ + int32 GetNextInstanceCount() const; + + /** + * Helper function to modify MultiParmInstanceLastModifyArray with inserts/removes (at the end) as necessary so + * that the multi parm instance count will be equal to InInstanceCount after the next upload to HAPI. + * @param InInstanceCount The number of instances the multiparm should have. + * @returns True if any changes were made to MultiParmInstanceLastModifyArray. + */ + bool SetNumElements(const int32 InInstanceCount); + + UPROPERTY() + bool bIsShown; + + // Value of the multiparm + UPROPERTY() + int32 Value; + + // + UPROPERTY() + FString TemplateName; + + // Value of this property. + UPROPERTY() + int32 MultiparmValue; + + // + UPROPERTY() + uint32 MultiParmInstanceNum; + + // + UPROPERTY() + uint32 MultiParmInstanceLength; + + // + UPROPERTY() + uint32 MultiParmInstanceCount; + + UPROPERTY() + uint32 InstanceStartOffset; + + // This array records the last modified instance of the multiparm + UPROPERTY() + TArray MultiParmInstanceLastModifyArray; + + UPROPERTY() + int32 DefaultInstanceCount; + + bool IsDefault() const override; + + void SetDefaultInstanceCount(int32 InCount); + + void MarkDefault(const bool& bInDefault) override; + +private: + void InitializeModifyArray(); + +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterOperatorPath.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterOperatorPath.h" + +#include "HoudiniInput.h" + + +UHoudiniParameterOperatorPath::UHoudiniParameterOperatorPath( + const FObjectInitializer &ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +UHoudiniParameterOperatorPath * +UHoudiniParameterOperatorPath::Create(UObject *InOuter, const FString &InParamName) +{ + FString ParamNameStr = "HoudiniParameterOperatorPath_" + InParamName; + FName ParamName = MakeUniqueObjectName( + InOuter, UHoudiniParameterOperatorPath::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterOperatorPath *HoudiniAssetParameter = + NewObject( + InOuter, UHoudiniParameterOperatorPath::StaticClass(), ParamName, + RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::Input); + + return HoudiniAssetParameter; +} + +bool UHoudiniParameterOperatorPath::HasChanged() const +{ + if (Super::HasChanged()) + return true; + if (HoudiniInput.IsValid() && HoudiniInput->HasChanged()) + return true; + return false; +} + +bool UHoudiniParameterOperatorPath::NeedsToTriggerUpdate() const +{ + if (Super::NeedsToTriggerUpdate()) + return true; + if (HoudiniInput.IsValid() && HoudiniInput->NeedsToTriggerUpdate()) + return true; + return false; +} + +void UHoudiniParameterOperatorPath::MarkChanged(const bool & bInChanged) +{ + Super::MarkChanged(bInChanged); +} + +void +UHoudiniParameterOperatorPath::RemapInputs(const TMap& InputMapping) +{ + if (!HoudiniInput.IsValid()) + return; + + if (InputMapping.Contains(HoudiniInput.Get())) + { + HoudiniInput = InputMapping.FindRef(HoudiniInput.Get()); + } + else + { + HoudiniInput = nullptr; + } +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterOperatorPath.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterOperatorPath.generated.h" + + +class UHoudiniInput; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterOperatorPath + : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + // Create instance of this class. + static UHoudiniParameterOperatorPath * + Create(UObject *Outer, const FString &ParamName); + + virtual bool HasChanged() const override; + virtual bool NeedsToTriggerUpdate() const override; + virtual void MarkChanged(const bool& bInChanged) override; + + //------------------------------------------------------------------------------------------------ + // UHoudiniParameter overrides + //------------------------------------------------------------------------------------------------ + virtual void RemapInputs(const TMap& InputMapping) override; + + UPROPERTY() + TWeakObjectPtr HoudiniInput; +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterRamp.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterRamp.h" +#include "HoudiniParameter.h" +#include "HoudiniParameterFloat.h" +#include "HoudiniParameterColor.h" +#include "HoudiniParameterChoice.h" + +#include "UObject/UnrealType.h" + +// Generic helpers to avoid logic duplication across ramp parameters +class HoudiniParameterRampStatics +{ +public: + template + static void SyncCachedPoints(ParamT* Param); +}; + + +template +void +HoudiniParameterRampStatics::SyncCachedPoints(ParamT* Param) +{ + int32 Idx = 0; + + while (Idx < Param->CachedPoints.Num() && Idx < Param->Points.Num()) + { + TObjectPtr &CachedPoint = Param->CachedPoints[Idx]; + TObjectPtr &CurrentPoint = Param->Points[Idx]; + + if (!CachedPoint || !CurrentPoint) + continue; + + if (CachedPoint->GetPosition() != CurrentPoint->GetPosition()) + { + if (CurrentPoint->PositionParentParm) + { + CurrentPoint->SetPosition(CachedPoint->GetPosition()); + CurrentPoint->PositionParentParm->MarkChanged(true); + } + } + + if (CachedPoint->GetValue() != CurrentPoint->GetValue()) + { + if (CurrentPoint->ValueParentParm) + { + CurrentPoint->SetValue(CachedPoint->GetValue()); + CurrentPoint->ValueParentParm->MarkChanged(true); + } + } + + if (CachedPoint->GetInterpolation() != CurrentPoint->GetInterpolation()) + { + if (CurrentPoint->InterpolationParentParm) + { + CurrentPoint->SetInterpolation(CachedPoint->GetInterpolation()); + CurrentPoint->InterpolationParentParm->MarkChanged(true); + } + } + + Idx += 1; + } + + // Insert points + for (int32 IdxCachedPointLeft = Idx; IdxCachedPointLeft < Param->CachedPoints.Num(); ++IdxCachedPointLeft) + { + TObjectPtr &CachedPoint = Param->CachedPoints[IdxCachedPointLeft]; + if (!CachedPoint) + continue; + + Param->CreateInsertEvent(CachedPoint->Position, CachedPoint->Value, CachedPoint->Interpolation); + + Param->MarkChanged(true); + } + + // Remove points + for (int32 IdxCurrentPointLeft = Idx; IdxCurrentPointLeft < Param->Points.Num(); ++IdxCurrentPointLeft) + { + Param->RemoveElement(IdxCurrentPointLeft); + + TObjectPtr Point = Param->Points[IdxCurrentPointLeft]; + + if (!Point) + continue; + + Param->CreateDeleteEvent(Point->InstanceIndex); + + Param->MarkChanged(true); + } +} + + +void +UHoudiniParameterRampFloatPoint::SetPosition(const float InPosition) +{ + if (PositionParentParm && PositionParentParm->GetNumberOfValues() > 0) + { + Position = InPosition; + PositionParentParm->SetValueAt(InPosition, 0); + PositionParentParm->SetIsChildOfRamp(); + } +}; + +void +UHoudiniParameterRampFloatPoint::SetValue(const float InValue) +{ + if (ValueParentParm && PositionParentParm->GetNumberOfValues() > 0) + { + Value = InValue; + ValueParentParm->SetValueAt(InValue, 0); + ValueParentParm->SetIsChildOfRamp(); + } +}; + +void +UHoudiniParameterRampFloatPoint::SetInterpolation(const EHoudiniRampInterpolationType InInterpolation) +{ + if (InterpolationParentParm) + { + Interpolation = InInterpolation; + InterpolationParentParm->SetIntValue((int32)InInterpolation); + InterpolationParentParm->UpdateStringValueFromInt(); + InterpolationParentParm->SetIsChildOfRamp(); + } +} + +UHoudiniParameterRampFloatPoint* +UHoudiniParameterRampFloatPoint::DuplicateAndCopyState(UObject* DestOuter, EObjectFlags InClearFlags, EObjectFlags InSetFlags) +{ + UHoudiniParameterRampFloatPoint* NewPoint = Cast(StaticDuplicateObject(this, DestOuter)); + + NewPoint->CopyStateFrom(this, false, InClearFlags, InSetFlags); + + return NewPoint; +} + +void +UHoudiniParameterRampFloatPoint::CopyStateFrom(UHoudiniParameterRampFloatPoint * InParameter, bool bCopyAllProperties, EObjectFlags InClearFlags, EObjectFlags InSetFlags) +{ + if (bCopyAllProperties) + { + UEngine::FCopyPropertiesForUnrelatedObjectsParams Params; + Params.bDoDelta = false; // Perform a deep copy + Params.bClearReferences = false; // References will be replaced afterwards. + UEngine::CopyPropertiesForUnrelatedObjects(InParameter, this, Params); + } + + // Ensure this object's flags match the desired flags. + if (InClearFlags != RF_NoFlags) + ClearFlags( InClearFlags ); + if (InSetFlags != RF_NoFlags) + SetFlags(InSetFlags); +} + +void +UHoudiniParameterRampFloatPoint::RemapParameters( + const TMap& ParameterMapping) +{ + if (ParameterMapping.Contains(PositionParentParm)) + { + PositionParentParm = CastChecked(ParameterMapping.FindChecked(PositionParentParm)); + } + else + { + PositionParentParm = nullptr; + } + + if (ParameterMapping.Contains(ValueParentParm)) + { + ValueParentParm = CastChecked(ParameterMapping.FindChecked(ValueParentParm)); + } + else + { + ValueParentParm = nullptr; + } + + if (ParameterMapping.Contains(InterpolationParentParm)) + { + InterpolationParentParm = CastChecked(ParameterMapping.FindChecked(InterpolationParentParm)); + } + else + { + InterpolationParentParm = nullptr; + } +}; + + +void +UHoudiniParameterRampColorPoint::SetPosition(const float InPosition) +{ + if (PositionParentParm && PositionParentParm->GetNumberOfValues() > 0) + { + Position = InPosition; + PositionParentParm->SetValueAt(InPosition, 0); + PositionParentParm->SetIsChildOfRamp(); + } +}; + +void +UHoudiniParameterRampColorPoint::SetValue(const FLinearColor InValue) +{ + if (!ValueParentParm) + return; + + Value = InValue; + ValueParentParm->SetColorValue(InValue); + ValueParentParm->SetIsChildOfRamp(); +}; + +void +UHoudiniParameterRampColorPoint::SetInterpolation(const EHoudiniRampInterpolationType InInterpolation) +{ + if (!InterpolationParentParm) + return; + + Interpolation = InInterpolation; + InterpolationParentParm->SetIntValue((int32)InInterpolation); + InterpolationParentParm->UpdateStringValueFromInt(); + InterpolationParentParm->SetIsChildOfRamp(); +} +UHoudiniParameterRampColorPoint* +UHoudiniParameterRampColorPoint::DuplicateAndCopyState( + UObject * DestOuter, EObjectFlags InClearFlags, EObjectFlags InSetFlags) +{ + + UHoudiniParameterRampColorPoint* NewPoint = Cast(StaticDuplicateObject(this, DestOuter)); + + NewPoint->CopyStateFrom(this, false, InClearFlags, InSetFlags); + + return NewPoint; + +} +void +UHoudiniParameterRampColorPoint::CopyStateFrom( + UHoudiniParameterRampColorPoint* InParameter, bool bCopyAllProperties, EObjectFlags InClearFlags, EObjectFlags InSetFlags) +{ +#if WITH_EDITOR + PreEditChange(nullptr); +#endif + if (bCopyAllProperties) + { + UEngine::FCopyPropertiesForUnrelatedObjectsParams Params; + Params.bDoDelta = false; // Perform a deep copy + Params.bClearReferences = false; // References will be replaced afterwards. + UEngine::CopyPropertiesForUnrelatedObjects(InParameter, this, Params); + } + + // Ensure this object's flags match the desired flags. + if (InClearFlags != RF_NoFlags) + ClearFlags( InClearFlags ); + if (InSetFlags != RF_NoFlags) + SetFlags(InSetFlags); +#if WITH_EDITOR + PostEditChange(); +#endif +} + +void +UHoudiniParameterRampColorPoint::RemapParameters(const TMap& ParameterMapping) +{ + if (ParameterMapping.Contains(PositionParentParm)) + { + PositionParentParm = CastChecked(ParameterMapping.FindChecked(PositionParentParm)); + } + else + { + PositionParentParm = nullptr; + } + + if (ParameterMapping.Contains(ValueParentParm)) + { + ValueParentParm = CastChecked(ParameterMapping.FindChecked(ValueParentParm)); + } + else + { + ValueParentParm = nullptr; + } + + if (ParameterMapping.Contains(InterpolationParentParm)) + { + InterpolationParentParm = CastChecked(ParameterMapping.FindChecked(InterpolationParentParm)); + } + else + { + InterpolationParentParm = nullptr; + } +} + + +UHoudiniParameterRampFloat::UHoudiniParameterRampFloat(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer), + NumDefaultPoints(-1), + bCaching(false) +{ + ParmType = EHoudiniParameterType::FloatRamp; +} + +void +UHoudiniParameterRampFloat::OnPreCook() +{ + if (bCaching) + { + SyncCachedPoints(); + bCaching = false; + } +} + +UHoudiniParameterRampFloat * +UHoudiniParameterRampFloat::Create( + UObject* InOuter, + const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterRamp_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterRampFloat::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterRampFloat * HoudiniParameter = NewObject< UHoudiniParameterRampFloat >( + InOuter, UHoudiniParameterRampFloat::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniParameter->SetParameterType(EHoudiniParameterType::FloatRamp); + + HoudiniParameter->NumDefaultPoints = -1; + + HoudiniParameter->bCaching = false; + + return HoudiniParameter; +} + +void +UHoudiniParameterRampFloat::CopyStateFrom( + UHoudiniParameter* InParameter, bool bCopyAllProperties, EObjectFlags InClearFlags, EObjectFlags InSetFlags) +{ +#if WITH_EDITOR + PreEditChange(nullptr); +#endif + + UHoudiniParameterRampFloat* FromParameter = Cast(InParameter); + check(FromParameter); + + TArray PrevCachedPoints = CachedPoints; + TArray PrevPoints = Points; + + Super::CopyStateFrom(InParameter, bCopyAllProperties, InClearFlags, InSetFlags); + + CachedPoints = PrevCachedPoints; + Points = PrevPoints; + + + auto CopyPointsStateFn = [InClearFlags, InSetFlags] (TArray>& FromArray, + TArray>& ToArray, UObject* NewOuter) + { + const int32 NumPts = FromArray.Num(); + + ToArray.SetNum(NumPts); + + for(int32 i = 0; i < NumPts; ++i) + { + UHoudiniParameterRampFloatPoint* FromPoint = FromArray[i]; + UHoudiniParameterRampFloatPoint* ToPoint = ToArray[i]; + + check(FromPoint); + + if (ToPoint) + { + // Ensure the destination point is outered to this parameter + bool bIsValid = ToPoint->GetOuter() == NewOuter; + if (!bIsValid) + ToPoint = nullptr; + } + + if (!ToPoint) + { + // Duplicate a new copy using FromPoint + ToPoint = FromPoint->DuplicateAndCopyState(NewOuter, InClearFlags, InSetFlags); + } + else + { + // We have a valid point that we can reuse. Simply copy state. + ToPoint->CopyStateFrom(FromPoint, true, InClearFlags, InSetFlags); + } + + ToArray[i] = ToPoint; + } + }; + + CopyPointsStateFn(FromParameter->CachedPoints, CachedPoints, this); + CopyPointsStateFn(FromParameter->Points, Points, this); + +#if WITH_EDITOR + PostEditChange(); +#endif +} + +void +UHoudiniParameterRampFloat::RemapParameters(const TMap& ParameterMapping) +{ + Super::RemapParameters(ParameterMapping); + + AActor* OuterActor = GetTypedOuter(); + + + for(UHoudiniParameterRampFloatPoint* CurrentPoint : Points) + { + CurrentPoint->RemapParameters(ParameterMapping); + } + + for(UHoudiniParameterRampFloatPoint* CurrentPoint : CachedPoints) + { + CurrentPoint->RemapParameters(ParameterMapping); + } +} + +void +UHoudiniParameterRampFloat::SyncCachedPoints() +{ + HoudiniParameterRampStatics::SyncCachedPoints(this); +} + + +void +UHoudiniParameterRampFloat::CreateInsertEvent(const float& InPosition, const float& InValue, const EHoudiniRampInterpolationType &InInterp) +{ + UHoudiniParameterRampModificationEvent* InsertEvent = NewObject( + this, UHoudiniParameterRampModificationEvent::StaticClass()); + + if (!InsertEvent) + return; + + InsertEvent->SetFloatRampEvent(); + InsertEvent->SetInsertEvent(); + InsertEvent->InsertPosition = InPosition; + InsertEvent->InsertFloat = InValue; + InsertEvent->InsertInterpolation = InInterp; + + ModificationEvents.Add(InsertEvent); +} + +void +UHoudiniParameterRampFloat::CreateDeleteEvent(const int32 &InDeleteIndex) +{ + UHoudiniParameterRampModificationEvent* DeleteEvent = NewObject( + this, UHoudiniParameterRampModificationEvent::StaticClass()); + + if (!DeleteEvent) + return; + + DeleteEvent->SetFloatRampEvent(); + DeleteEvent->SetDeleteEvent(); + DeleteEvent->DeleteInstanceIndex = InDeleteIndex; + + ModificationEvents.Add(DeleteEvent); +} + +bool +UHoudiniParameterRampFloat::UpdatePointsArray(const TArray& InParameters, const int32 InStartParamIndex) +{ + // Copy existing points to NewPoints + TArray NewPoints; + const int32 NumInstances = GetInstanceCount(); + NewPoints.Reserve(NumInstances); + for (UHoudiniParameterRampFloatPoint* const PointData : Points) + { + if (!IsValid(PointData)) + continue; + + PointData->InstanceIndex = NewPoints.Num(); + PointData->PositionParentParm = nullptr; + PointData->ValueParentParm = nullptr; + PointData->InterpolationParentParm = nullptr; + NewPoints.Add(PointData); + + // We don't need more than NumInstances points in the array + if (NewPoints.Num() == NumInstances) + break; + } + + // Loop over InParameters and look for children of this ramp + int32 CurrentInstanceIndex = 0; + const int32 NumParameters = InParameters.Num(); + for (int32 Index = InStartParamIndex; Index < NumParameters; ++Index) + { + UHoudiniParameter* const Param = InParameters[Index]; + if (!IsValid(Param)) + continue; + + if (!Param->GetIsChildOfMultiParm() || Param->GetParentParmId() != ParmId) + continue; + + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType != EHoudiniParameterType::Float && ParamType != EHoudiniParameterType::IntChoice) + continue; + + // Ensure we have a valid point object at the current index + UHoudiniParameterRampFloatPoint* Point = nullptr; + if (!NewPoints.IsValidIndex(CurrentInstanceIndex)) + { + Point = NewObject(this, FName(), this->GetMaskedFlags(RF_PropagateToSubObjects)); + Point->InstanceIndex = CurrentInstanceIndex; + NewPoints.Add(Point); + } + else + { + Point = NewPoints[CurrentInstanceIndex]; + } + + if (ParamType == EHoudiniParameterType::Float) + { + UHoudiniParameterFloat* FloatParameter = Cast(Param); + if (FloatParameter) + { + //*****Float Parameter (position)*****// + if (!Point->PositionParentParm) + { + if (FloatParameter->GetNumberOfValues() <= 0) + continue; + // Set the float ramp point's position parent parm, and value + Point->PositionParentParm = FloatParameter; + Point->SetPosition(FloatParameter->GetValuesPtr()[0]); + } + //*****Float Parameter (value)*****// + else + { + if (FloatParameter->GetNumberOfValues() <= 0) + continue;; + Point->ValueParentParm = FloatParameter; + Point->SetValue(FloatParameter->GetValuesPtr()[0]); + } + } + } + //*****Choice parameter (Interpolation)*****// + else if (ParamType == EHoudiniParameterType::IntChoice) + { + UHoudiniParameterChoice* ChoiceParameter = Cast(Param); + if (ChoiceParameter) + { + Point->InterpolationParentParm = ChoiceParameter; + Point->SetInterpolation(UHoudiniParameter::GetHoudiniInterpMethodFromInt(ChoiceParameter->GetIntValueIndex())); + CurrentInstanceIndex++; + } + } + } + + //*****All ramp points have been parsed, finish!*****// + if (NewPoints.Num() == NumInstances) + { + NewPoints.Sort([](const UHoudiniParameterRampFloatPoint& P1, const UHoudiniParameterRampFloatPoint& P2) { + return P1.Position < P2.Position; + }); + + Points = MoveTemp(NewPoints); + + // Not caching, points are synced, update cached points + if (!bCaching) + { + const int32 NumPoints = Points.Num(); + CachedPoints.SetNumZeroed(NumPoints); + + for (int32 i = 0; i < NumPoints; ++i) + { + UHoudiniParameterRampFloatPoint* const FromPoint = Points[i]; + UHoudiniParameterRampFloatPoint* ToPoint = CachedPoints[i]; + + // Nothing we can do/copy if FromPoint is null/pending kill + if (!IsValid(FromPoint)) + continue; + + if (!IsValid(ToPoint)) + { + ToPoint = FromPoint->DuplicateAndCopyState(this, RF_NoFlags, GetMaskedFlags(RF_PropagateToSubObjects)); + CachedPoints[i] = ToPoint; + } + else + { + ToPoint->CopyStateFrom(FromPoint, true); + } + } + } + + SetDefaultValues(); + + return true; + } + + return false; +} + +UHoudiniParameterRampColor::UHoudiniParameterRampColor(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer), + bCaching(false), + NumDefaultPoints(-1) +{ + ParmType = EHoudiniParameterType::ColorRamp; +} + + +UHoudiniParameterRampColor * +UHoudiniParameterRampColor::Create( + UObject* InOuter, + const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterRamp_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterRampColor::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterRampColor * HoudiniParameter = NewObject< UHoudiniParameterRampColor >( + InOuter, UHoudiniParameterRampColor::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniParameter->SetParameterType(EHoudiniParameterType::ColorRamp); + + HoudiniParameter->NumDefaultPoints = -1; + + HoudiniParameter->bCaching = false; + + return HoudiniParameter; +} + + +bool +UHoudiniParameterRampFloat::IsDefault() const +{ + if (NumDefaultPoints < 0) + return true; + + if (NumDefaultPoints != Points.Num()) + { + return false; + } + + TArray Positions = DefaultPositions; + TArray Values = DefaultValues; + TArray Choices = DefaultChoices; + + for (auto & NextPt : Points) + { + if (!NextPt) + return false; + + bool bFoundMatch = false; + for (int32 DefaultIdx = 0; DefaultIdx < Positions.Num(); ++DefaultIdx) + { + if (Positions[DefaultIdx] == NextPt->Position && + Values[DefaultIdx] == NextPt->Value && + Choices[DefaultIdx] == (int32)NextPt->Interpolation) + { + Positions.RemoveAt(DefaultIdx); + Values.RemoveAt(DefaultIdx); + Choices.RemoveAt(DefaultIdx); + bFoundMatch = true; + } + } + + if (!bFoundMatch) + return false; + } + + if (Positions.Num() > 0) + return false; + + return true; +} + +void +UHoudiniParameterRampColor::CopyStateFrom(UHoudiniParameter * InParameter, bool bCopyAllProperties, EObjectFlags InClearFlags, EObjectFlags InSetFlags) +{ + UHoudiniParameterRampColor* FromParameter = Cast(InParameter); + check(FromParameter); + + TArray PrevCachedPoints = CachedPoints; + TArray PrevPoints = Points; + + Super::CopyStateFrom(InParameter, bCopyAllProperties, InClearFlags, InSetFlags); + + CachedPoints = PrevCachedPoints; + Points = PrevPoints; + + + auto CopyPointsStateFn = [InClearFlags, InSetFlags] ( + TArray>& FromArray, TArray>& ToArray, UObject* NewOuter) + { + const int32 NumPts = FromArray.Num(); + + ToArray.SetNum(NumPts); + + for(int32 i = 0; i < NumPts; ++i) + { + UHoudiniParameterRampColorPoint* FromPoint = FromArray[i]; + UHoudiniParameterRampColorPoint* ToPoint = ToArray[i]; + + check(FromPoint); + + if (ToPoint) + { + // Ensure the destination point is outered to this parameter + bool bIsValid = ToPoint->GetOuter() == NewOuter; + if (!bIsValid) + ToPoint = nullptr; + } + + if (!ToPoint) + { + // Duplicate a new copy using FromPoint + ToPoint = FromPoint->DuplicateAndCopyState(NewOuter, InClearFlags, InSetFlags); + } + else + { + // We have a valid point that we can reuse. Simply copy state. + ToPoint->CopyStateFrom(FromPoint, true, InClearFlags, InSetFlags); + } + + ToArray[i] = ToPoint; + } + }; + + CopyPointsStateFn(FromParameter->CachedPoints, CachedPoints, this); + CopyPointsStateFn(FromParameter->Points, Points, this); + + if (InClearFlags != RF_NoFlags) + ClearFlags( InClearFlags ); + if (InSetFlags != RF_NoFlags) + SetFlags(InSetFlags); +} + +void +UHoudiniParameterRampColor::RemapParameters(const TMap& ParameterMapping) +{ + Super::RemapParameters(ParameterMapping); + + AActor* OuterActor = GetTypedOuter(); + + + for(UHoudiniParameterRampColorPoint* CurrentPoint : Points) + { + CurrentPoint->RemapParameters(ParameterMapping); + } + + for(UHoudiniParameterRampColorPoint* CurrentPoint : CachedPoints) + { + CurrentPoint->RemapParameters(ParameterMapping); + } +} + +bool +UHoudiniParameterRampColor::UpdatePointsArray(const TArray& InParameters, const int32 InStartParamIndex) +{ + // Copy existing points to NewPoints + TArray NewPoints; + const int32 NumInstances = GetInstanceCount(); + NewPoints.Reserve(NumInstances); + for (UHoudiniParameterRampColorPoint* const PointData : Points) + { + if (!IsValid(PointData)) + continue; + + PointData->InstanceIndex = NewPoints.Num(); + PointData->PositionParentParm = nullptr; + PointData->ValueParentParm = nullptr; + PointData->InterpolationParentParm = nullptr; + NewPoints.Add(PointData); + + // We don't need more than NumInstances points in the array + if (NewPoints.Num() == NumInstances) + break; + } + + // Loop over InParameters and look for children of this ramp + int32 CurrentInstanceIndex = 0; + const int32 NumParameters = InParameters.Num(); + for (int32 Index = InStartParamIndex; Index < NumParameters; ++Index) + { + UHoudiniParameter* const Param = InParameters[Index]; + if (!IsValid(Param)) + continue; + + if (!Param->GetIsChildOfMultiParm() || Param->GetParentParmId() != ParmId) + continue; + + const EHoudiniParameterType ParamType = Param->GetParameterType(); + if (ParamType != EHoudiniParameterType::Float && ParamType != EHoudiniParameterType::Color && + ParamType != EHoudiniParameterType::IntChoice) + continue; + + // Ensure we have a valid point object at the current index + UHoudiniParameterRampColorPoint* Point = nullptr; + if (!NewPoints.IsValidIndex(CurrentInstanceIndex)) + { + Point = NewObject(this, FName(), this->GetMaskedFlags(RF_PropagateToSubObjects)); + Point->InstanceIndex = CurrentInstanceIndex; + NewPoints.Add(Point); + } + else + { + Point = NewPoints[CurrentInstanceIndex]; + } + + if (ParamType == EHoudiniParameterType::Float) + { + UHoudiniParameterFloat* FloatParameter = Cast(Param); + if (FloatParameter) + { + //*****Float Parameter (position)*****// + if (!Point->PositionParentParm) + { + if (FloatParameter->GetNumberOfValues() <= 0) + continue; + // Set the float ramp point's position parent parm, and value + Point->PositionParentParm = FloatParameter; + Point->SetPosition(FloatParameter->GetValuesPtr()[0]); + } + } + } + else if (ParamType == EHoudiniParameterType::Color) + { + UHoudiniParameterColor* ColorParameter = Cast(Param); + if (ColorParameter) + { + //*****Color Parameter (value)*****// + Point->ValueParentParm = ColorParameter; + Point->SetValue(ColorParameter->GetColorValue()); + } + } + //*****Choice parameter (Interpolation)*****// + else if (ParamType == EHoudiniParameterType::IntChoice) + { + UHoudiniParameterChoice* ChoiceParameter = Cast(Param); + if (ChoiceParameter) + { + Point->InterpolationParentParm = ChoiceParameter; + Point->SetInterpolation(UHoudiniParameter::GetHoudiniInterpMethodFromInt(ChoiceParameter->GetIntValueIndex())); + CurrentInstanceIndex++; + } + } + } + + //*****All ramp points have been parsed, finish!*****// + if (NewPoints.Num() == NumInstances) + { + NewPoints.Sort([](const UHoudiniParameterRampColorPoint& P1, const UHoudiniParameterRampColorPoint& P2) { + return P1.Position < P2.Position; + }); + + Points = MoveTemp(NewPoints); + + // Not caching, points are synced, update cached points + if (!bCaching) + { + const int32 NumPoints = Points.Num(); + CachedPoints.SetNumZeroed(NumPoints); + + for (int32 i = 0; i < NumPoints; ++i) + { + UHoudiniParameterRampColorPoint* const FromPoint = Points[i]; + UHoudiniParameterRampColorPoint* ToPoint = CachedPoints[i]; + + // Nothing we can do/copy if FromPoint is null/pending kill + if (!IsValid(FromPoint)) + continue; + + if (!IsValid(ToPoint)) + { + ToPoint = FromPoint->DuplicateAndCopyState(this, RF_NoFlags, GetMaskedFlags(RF_PropagateToSubObjects)); + CachedPoints[i] = ToPoint; + } + else + { + ToPoint->CopyStateFrom(FromPoint, true); + } + } + } + + SetDefaultValues(); + + return true; + } + + return false; +} + +void UHoudiniParameterRampColor::SyncCachedPoints() +{ + HoudiniParameterRampStatics::SyncCachedPoints(this); +} + +void UHoudiniParameterRampColor::CreateInsertEvent(const float& InPosition, const FLinearColor& InValue, + const EHoudiniRampInterpolationType& InInterp) +{ + UHoudiniParameterRampModificationEvent* InsertEvent = NewObject( + this, UHoudiniParameterRampModificationEvent::StaticClass()); + + if (!InsertEvent) + return; + + InsertEvent->SetColorRampEvent(); + InsertEvent->SetInsertEvent(); + InsertEvent->InsertPosition = InPosition; + InsertEvent->InsertColor = InValue; + InsertEvent->InsertInterpolation = InInterp; + + ModificationEvents.Add(InsertEvent); +} + +void UHoudiniParameterRampColor::CreateDeleteEvent(const int32& InDeleteIndex) +{ + UHoudiniParameterRampModificationEvent* DeleteEvent = NewObject( + this, UHoudiniParameterRampModificationEvent::StaticClass()); + + if (!DeleteEvent) + return; + + DeleteEvent->SetColorRampEvent(); + DeleteEvent->SetDeleteEvent(); + DeleteEvent->DeleteInstanceIndex = InDeleteIndex; + + ModificationEvents.Add(DeleteEvent); +} + + +void +UHoudiniParameterRampColor::SetNumCachedPoints(const int32 NumPoints) +{ + CachedPoints.SetNum(NumPoints); +} + + +bool +UHoudiniParameterRampColor::SetCachedPointAtIndex(const int32 InIndex, const float InPosition, const FLinearColor& InValue, + const EHoudiniRampInterpolationType InInterpolation) +{ + // Set points directly in the cache + if (!CachedPoints.IsValidIndex(InIndex)) + { + return false; + } + + UHoudiniParameterRampColorPoint* Point = CachedPoints[InIndex]; + if (!IsValid(Point)) + { + // Create a new point at this index + Point = NewObject(this, UHoudiniParameterRampColorPoint::StaticClass()); + if (!Point) + return false; + CachedPoints[InIndex] = Point; + } + Point->Position = InPosition; + Point->Value = InValue; + Point->Interpolation = InInterpolation == EHoudiniRampInterpolationType::InValid ? EHoudiniRampInterpolationType::LINEAR : InInterpolation; + + return true; +} + + +bool +UHoudiniParameterRampColor::IsDefault() const +{ + if (NumDefaultPoints < 0) + return true; + + if (NumDefaultPoints != Points.Num()) + return false; + + TArray Positions = DefaultPositions; + TArray Values = DefaultValues; + TArray Choices = DefaultChoices; + + for (auto & NextPt : Points) + { + if (!NextPt) + return false; + + bool bFoundMatch = false; + for (int32 DefaultIdx = 0; DefaultIdx < Positions.Num(); ++DefaultIdx) + { + if (Positions[DefaultIdx] == NextPt->Position && + Values[DefaultIdx] == NextPt->Value && + Choices[DefaultIdx] == (int32)NextPt->Interpolation) + { + Positions.RemoveAt(DefaultIdx); + Values.RemoveAt(DefaultIdx); + Choices.RemoveAt(DefaultIdx); + bFoundMatch = true; + } + } + + if (!bFoundMatch) + return false; + } + + if (Positions.Num() > 0) + return false; + + return true; +} + + +void +UHoudiniParameterRampColor::SetDefaultValues() +{ + if (NumDefaultPoints >= 0) + return; + + + if (DefaultPositions.Num() > 0) + return; + + DefaultPositions.Empty(); + DefaultValues.Empty(); + DefaultChoices.Empty(); + + for (auto & NextPoint : Points) + { + if (!NextPoint) + continue; + + DefaultPositions.Add(NextPoint->Position); + DefaultValues.Add(NextPoint->Value); + DefaultChoices.Add((int32)NextPoint->Interpolation); + + } + + NumDefaultPoints = Points.Num(); +} + +void +UHoudiniParameterRampFloat::SetDefaultValues() +{ + if (DefaultPositions.Num() > 0) + return; + + DefaultPositions.Empty(); + DefaultValues.Empty(); + DefaultChoices.Empty(); + + for (auto & NextPoint : Points) + { + if (!NextPoint) + continue; + + DefaultPositions.Add(NextPoint->Position); + DefaultValues.Add(NextPoint->Value); + DefaultChoices.Add((int32)NextPoint->Interpolation); + + } + + NumDefaultPoints = Points.Num(); + +} + +void UHoudiniParameterRampColor::OnPreCook() +{ + Super::OnPreCook(); + + if (bCaching) + { + SyncCachedPoints(); + bCaching = false; + } +} + +void UHoudiniParameterRampFloat::SetNumCachedPoints(const int32 NumPoints) +{ + CachedPoints.SetNum(NumPoints); +} + +bool UHoudiniParameterRampFloat::SetCachedPointAtIndex(const int32 InIndex, const float InPosition, const float InValue, + const EHoudiniRampInterpolationType InInterpolation) +{ + // Set points directly in the cache + if (!CachedPoints.IsValidIndex(InIndex)) + { + return false; + } + + UHoudiniParameterRampFloatPoint* Point = CachedPoints[InIndex]; + if (!IsValid(Point)) + { + // Create a new point at this index + Point = NewObject(this, UHoudiniParameterRampFloatPoint::StaticClass()); + if (!Point) + return false; + CachedPoints[InIndex] = Point; + } + Point->Position = InPosition; + Point->Value = InValue; + Point->Interpolation = InInterpolation == EHoudiniRampInterpolationType::InValid ? EHoudiniRampInterpolationType::LINEAR : InInterpolation; + + return true; +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterRamp.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "Curves/CurveFloat.h" +#include "Curves/CurveLinearColor.h" +#include "HoudiniParameterMultiParm.h" +#include "HoudiniParameterRamp.generated.h" + +class UHoudiniParameterRampFloat; +class UHoudiniParameterRampColor; +class UHoudiniParameter; +class UHoudiniParameterFloat; +class UHoudiniParameterChoice; +class UHoudiniParameterColor; + +UENUM() +enum class EHoudiniRampPointConstructStatus : uint8 +{ + None, + + INITIALIZED, + POSITION_INSERTED, + VALUE_INSERTED, + INTERPTYPE_INSERTED +}; + +UCLASS(DefaultToInstanced) +class HOUDINIENGINERUNTIME_API UHoudiniParameterRampModificationEvent : public UObject +{ + GENERATED_BODY() +public: + FORCEINLINE + void SetInsertEvent() { bIsInsertEvent = true; }; + + FORCEINLINE + void SetDeleteEvent() { bIsInsertEvent = false; }; + + FORCEINLINE + void SetFloatRampEvent() { bIsFloatRamp = true; }; + + FORCEINLINE + void SetColorRampEvent() { bIsFloatRamp = false; }; + + FORCEINLINE + bool IsInsertEvent() const { return bIsInsertEvent; }; + + FORCEINLINE + bool IsDeleteEvent() const { return !bIsInsertEvent; }; + + FORCEINLINE + bool IsFloatRampEvent() { return bIsFloatRamp; }; + + FORCEINLINE + bool IsColorRampEvent() { return !bIsFloatRamp; }; + + FORCEINLINE + void SetPosition(float Position) { InsertPosition = Position; } + + FORCEINLINE + void SetValue(float Value) { InsertFloat = Value; } + + FORCEINLINE + void SetValue(FLinearColor Value) { InsertColor = Value; } + + FORCEINLINE + void SetInterpolation(EHoudiniRampInterpolationType Interpolation) { InsertInterpolation = Interpolation; } + +private: + UPROPERTY() + bool bIsInsertEvent = false; + + UPROPERTY() + bool bIsFloatRamp = false; + +public: + UPROPERTY() + int32 DeleteInstanceIndex = -1; + + UPROPERTY() + float InsertPosition; + + UPROPERTY() + float InsertFloat; + + UPROPERTY() + FLinearColor InsertColor; + + UPROPERTY() + EHoudiniRampInterpolationType InsertInterpolation; +}; + +UCLASS(DefaultToInstanced) +class HOUDINIENGINERUNTIME_API UHoudiniParameterRampFloatPoint : public UObject +{ + GENERATED_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + UPROPERTY() + float Position; + + UPROPERTY() + float Value; + + UPROPERTY() + EHoudiniRampInterpolationType Interpolation = EHoudiniRampInterpolationType::InValid; + + UPROPERTY() + int32 InstanceIndex = -1; + + UPROPERTY() + TObjectPtr PositionParentParm = nullptr; + + UPROPERTY() + TObjectPtr ValueParentParm = nullptr; + + UPROPERTY() + TObjectPtr InterpolationParentParm = nullptr; + + FORCEINLINE + float GetPosition() const { return Position; }; + + void SetPosition(const float InPosition); + + FORCEINLINE + float GetValue() const { return Value; }; + + void SetValue(const float InValue); + + FORCEINLINE + EHoudiniRampInterpolationType GetInterpolation() const { return Interpolation; }; + + void SetInterpolation(const EHoudiniRampInterpolationType InInterpolation); + + UHoudiniParameterRampFloatPoint* DuplicateAndCopyState(UObject* DestOuter, EObjectFlags ClearFlags=RF_NoFlags, EObjectFlags SetFlags=RF_NoFlags); + + void CopyStateFrom(UHoudiniParameterRampFloatPoint* InParameter, bool bCopyAllProperties, EObjectFlags ClearFlags=RF_NoFlags, EObjectFlags SetFlags=RF_NoFlags); + + void RemapParameters(const TMap& ParameterMapping); + +}; + +UCLASS(DefaultToInstanced) +class HOUDINIENGINERUNTIME_API UHoudiniParameterRampColorPoint : public UObject +{ + GENERATED_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + UPROPERTY() + float Position; + + UPROPERTY() + FLinearColor Value; + + UPROPERTY() + EHoudiniRampInterpolationType Interpolation = EHoudiniRampInterpolationType::InValid; + + UPROPERTY() + int32 InstanceIndex = -1; + + UPROPERTY() + TObjectPtr PositionParentParm = nullptr; + + UPROPERTY() + TObjectPtr ValueParentParm = nullptr; + + UPROPERTY() + TObjectPtr InterpolationParentParm = nullptr; + + FORCEINLINE + float GetPosition() const { return Position; }; + + void SetPosition(const float InPosition); + FORCEINLINE + FLinearColor GetValue() const { return Value; }; + + void SetValue(const FLinearColor InValue); + + FORCEINLINE + EHoudiniRampInterpolationType GetInterpolation() const { return Interpolation; }; + + void SetInterpolation(const EHoudiniRampInterpolationType InInterpolation); + + UHoudiniParameterRampColorPoint* DuplicateAndCopyState(UObject* DestOuter, EObjectFlags InClearFlags=RF_NoFlags, EObjectFlags InSetFlags=RF_NoFlags); + + void CopyStateFrom(UHoudiniParameterRampColorPoint* InParameter, bool bCopyAllProperties, EObjectFlags InClearFlags=RF_NoFlags, EObjectFlags InSetFlags=RF_NoFlags); + + void RemapParameters(const TMap& ParameterMapping); +}; + +/** + * A note on Houdini Ramp Parameter processes + * + * The Ramp Parameters changes are applied through HAPI by way of Modification Events. + * The FHoudiniParameterTranslator::UploadRampParameter method will process all the pending modification events on a + * ramp parameter and execute the desired HAPI calls to send these modifications through to Houdini. When the HDA + * has finished cooking, the values from the ramp parameters on the Houdini side will be copied back onto the HDA in + * Unreal (the `points` member of the respective ramp parameter will be overwritten). + * + * It is for this reason that we cannot simply store our desired state in the `Points` member; anything stored here will + * get clobbered after the HDA has been cooked. So we record any modifications as events instead, and apply those + * modification events on the next cook. + * + * We can store our desired state in the CachedPoints member, as is the case when auto-update is disabled for a + * Ramp parameter or HoudiniEngine cooking is disabled. Once the HDA is ready to cook, the data in CachedPoints is + * "synced" (SyncCachedPoints). This will generate Modification Events based on the differences in data between + * `Points` and `CachedPoints`. Once the modification events have been generated, the HDA can be cooked as usual. + */ + + +/** + * Houdini Ramp Parameter (float) + */ +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterRampFloat : public UHoudiniParameterMultiParm +{ + GENERATED_UCLASS_BODY() + +public: + + virtual void OnPreCook() override; + + // Create instance of this class. + static UHoudiniParameterRampFloat * Create( + UObject* Outer, + const FString& ParamName); + + FORCEINLINE + bool IsCaching() const { return bCaching; }; + + FORCEINLINE + void SetCaching(const bool bInCaching) { bCaching = bInCaching; }; + + virtual void CopyStateFrom(UHoudiniParameter* InParameter, bool bCopyAllProperties, EObjectFlags InClearFlags=RF_NoFlags, EObjectFlags InSetFlags=RF_NoFlags) override; + + virtual void RemapParameters(const TMap& ParameterMapping) override; + + void SyncCachedPoints(); + + void CreateInsertEvent(const float& InPosition, const float& InValue, const EHoudiniRampInterpolationType &InInterp); + + void CreateDeleteEvent(const int32 &InDeleteIndex); + + // Resize the number of cached points contained in this parameter + void SetNumCachedPoints(const int32 NumPoints); + + // Set the cached point at the current index. If no point object exist at the current index, create it. + bool SetCachedPointAtIndex(const int32 InIndex, const float InPosition, const float InValue, const EHoudiniRampInterpolationType InInterpolation); + + /** + * Update/populates the Points array from InParameters. + * @param InParameters An array of parameters containing this ramp multiparm's instances (the parameters for each + * of its points). + * @param InStartParamIndex The index in InParameters where this ramp multiparm's child parameters start. + * @return true if we found enough parameters to build a number of points == NumInstances(). + */ + bool UpdatePointsArray(const TArray& InParameters, const int32 InStartParamIndex); + + UPROPERTY() + TArray> Points; + + UPROPERTY() + TArray> CachedPoints; + + UPROPERTY() + TArray DefaultPositions; + + UPROPERTY() + TArray DefaultValues; + + UPROPERTY() + TArray DefaultChoices; + + UPROPERTY() + int32 NumDefaultPoints; + + // If this is true, the cached points will be synced prior to cooking. + UPROPERTY() + bool bCaching; + + UPROPERTY() + TArray> ModificationEvents; + + bool IsDefault() const override; + + void SetDefaultValues(); + +}; + + +/** + * Houdini Ramp Parameter (FLinearColor) + */ +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterRampColor : public UHoudiniParameterMultiParm +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + virtual void OnPreCook() override; + + // Create instance of this class. + static UHoudiniParameterRampColor * Create( + UObject* Outer, + const FString& ParamName); + + FORCEINLINE + bool IsCaching() const { return bCaching; }; + + FORCEINLINE + void SetCaching(const bool bInCaching) { bCaching = bInCaching; }; + + virtual void CopyStateFrom(UHoudiniParameter* InParameter, bool bCopyAllProperties, EObjectFlags InClearFlags=RF_NoFlags, EObjectFlags InSetFlags=RF_NoFlags) override; + + virtual void RemapParameters(const TMap& ParameterMapping) override; + + /** + * Update/populates the Points array from InParameters. + * @param InParameters An array of parameters containing this ramp multiparm's instances (the parameters for each + * of its points). + * @param InStartParamIndex The index in InParameters where this ramp multiparm's child parameters start. + * @return true if we found enough parameters to build a number of points == NumInstances(). + */ + bool UpdatePointsArray(const TArray& InParameters, const int32 InStartParamIndex); + + void SyncCachedPoints(); + + void CreateInsertEvent(const float& InPosition, const FLinearColor& InValue, const EHoudiniRampInterpolationType &InInterp); + + void CreateDeleteEvent(const int32 &InDeleteIndex); + + // Resize the number of cached points contained in this parameter + void SetNumCachedPoints(const int32 NumPoints); + + // Set the cached point at the current index. If no point object exist at the current index, create it. + bool SetCachedPointAtIndex(const int32 InIndex, const float InPosition, const FLinearColor& InValue, const EHoudiniRampInterpolationType InInterpolation); + + UPROPERTY(Instanced) + TArray> Points; + + UPROPERTY() + bool bCaching; + + UPROPERTY(Instanced) + TArray> CachedPoints; + + UPROPERTY() + TArray DefaultPositions; + + UPROPERTY() + TArray DefaultValues; + + UPROPERTY() + TArray DefaultChoices; + + UPROPERTY() + int32 NumDefaultPoints; + + UPROPERTY() + TArray> ModificationEvents; + + bool IsDefault() const override; + + void SetDefaultValues(); + +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterSeparator.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterSeparator.h" + +UHoudiniParameterSeparator::UHoudiniParameterSeparator(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) +{ + ParmType = EHoudiniParameterType::Separator; +} + +UHoudiniParameterSeparator * +UHoudiniParameterSeparator::Create( + UObject* InOuter, + const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterSeparator_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterSeparator::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterSeparator * HoudiniAssetParameter = NewObject< UHoudiniParameterSeparator >( + InOuter, UHoudiniParameterSeparator::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::Separator); + //HoudiniAssetParameter->UpdateFromParmInfo(InParentParameter, InNodeId, ParmInfo); + + return HoudiniAssetParameter; +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterSeparator.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterSeparator.generated.h" + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterSeparator : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Create instance of this class. + static UHoudiniParameterSeparator * Create( + UObject* Outer, + const FString& ParamName); +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterString.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterString.h" + +UHoudiniParameterString::UHoudiniParameterString(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) + , bIsAssetRef(false) +{ + ParmType = EHoudiniParameterType::String; +} + +UHoudiniParameterString * +UHoudiniParameterString::Create( + UObject* InOuter, + const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterString_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterString::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterString * HoudiniAssetParameter = NewObject< UHoudiniParameterString >( + InOuter, UHoudiniParameterString::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::String); + //HoudiniAssetParameter->UpdateFromParmInfo(InParentParameter, InNodeId, ParmInfo); + + return HoudiniAssetParameter; +} + +bool +UHoudiniParameterString::SetValuesIfChanged(const TArray& NewValues) +{ + // Sets the new values, and returns true if any (or number) of parameters changed. + if(NewValues.Num() != Values.Num()) + { + Values = NewValues; + bHasChanged = true; + return true; + } + + for(int Index = 0; Index < NewValues.Num(); Index++) + { + if(Values[Index] != NewValues[Index]) + { + Values = NewValues; + bHasChanged = true; + return true; + } + } + return false; +} + +bool +UHoudiniParameterString::SetValueAt(const FString& InValue, const uint32& Index) +{ + if (!Values.IsValidIndex(Index)) + return false; + + if (Values[Index].Equals(InValue, ESearchCase::CaseSensitive)) + return false; + + Values[Index] = InValue; + + return true; +} + +void +UHoudiniParameterString::SetAssetAt(UObject* InObject, const uint32& Index) +{ + if (!ChosenAssets.IsValidIndex(Index)) + return; + + ChosenAssets[Index] = InObject; + +} + +FString +UHoudiniParameterString::GetAssetReference(UObject* InObject) +{ + // Get the asset reference string for a given UObject + if (!IsValid(InObject)) + return FString(); + + // Start by getting the Object's full name + FString AssetReference = InObject->GetFullName(); + + // Replace the first space to '\'' + for (int32 Itr = 0; Itr < AssetReference.Len(); Itr++) + { + if (AssetReference[Itr] == ' ') + { + AssetReference[Itr] = '\''; + break; + } + } + + // Attach another '\'' to the end + AssetReference += FString("'"); + + return AssetReference; +} + +void +UHoudiniParameterString::SetDefaultValues() +{ + if (DefaultValues.Num() > 0) + return; + + DefaultValues.Empty(); + for (int32 Idx = 0; Idx < Values.Num(); ++Idx) + { + DefaultValues.Add(Values[Idx]); + } +} + +bool +UHoudiniParameterString::IsDefaultValueAtIndex(const int32& Idx) const +{ + if (!Values.IsValidIndex(Idx) || !DefaultValues.IsValidIndex(Idx)) + return true; + + return Values[Idx] == DefaultValues[Idx]; +} + +bool +UHoudiniParameterString::IsDefault() const +{ + for (int32 Idx = 0; Idx < Values.Num(); ++Idx) + { + if (!IsDefaultValueAtIndex(Idx)) + return false; + } + + return true; +} +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterString.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterString.generated.h" + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterString : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Create instance of this class. + static UHoudiniParameterString * Create( + UObject* Outer, const FString& ParamName); + + // Accessor + FString GetValueAt(const uint32& Index) const { return Values.IsValidIndex(Index) ? Values[Index] : FString(); }; + + UObject* GetAssetAt(const uint32& Index) const { return ChosenAssets.IsValidIndex(Index) ? ChosenAssets[Index] : nullptr; }; + + int32 GetNumberOfValues() const { return Values.Num(); }; + + bool IsAssetRef() const { return bIsAssetRef; }; + + bool IsDefaultValueAtIndex(const int32& Idx) const; + bool IsDefault() const override; + + // Mutators + void SetIsAssetRef(const bool& InIsAssetRef) { bIsAssetRef = InIsAssetRef; }; + + bool SetValueAt(const FString& InValue, const uint32& Index); + + void SetAssetAt(UObject* InObject, const uint32& Index); + + void SetNumberOfValues(const uint32& InNumValues) { Values.SetNum(InNumValues); ChosenAssets.SetNum(InNumValues); }; + + TArray> & GetChosenAssets() { return ChosenAssets; }; + + void SetDefaultValues(); + + // Utility + + // Get the asset reference string for a given UObject + static FString GetAssetReference(UObject* InObject); + + const TArray& GetDefaultValues() { return DefaultValues; }; + bool SetValuesIfChanged(const TArray& Values); + +protected: + + // Values of this property. + UPROPERTY() + TArray< FString > Values; + + UPROPERTY() + TArray< FString > DefaultValues; + + UPROPERTY() + TArray> ChosenAssets; + + // Indicates this string parameter should be treated as an asset reference + // and display an object picker + UPROPERTY() + bool bIsAssetRef; +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterToggle.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniParameterToggle.h" + +UHoudiniParameterToggle::UHoudiniParameterToggle(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) +{ + ParmType = EHoudiniParameterType::Toggle; +} + +UHoudiniParameterToggle * +UHoudiniParameterToggle::Create( + UObject* InOuter, + const FString& InParamName) +{ + FString ParamNameStr = "HoudiniParameterToggle_" + InParamName; + FName ParamName = MakeUniqueObjectName(InOuter, UHoudiniParameterToggle::StaticClass(), *ParamNameStr); + + // We need to create a new parameter + UHoudiniParameterToggle * HoudiniAssetParameter = NewObject< UHoudiniParameterToggle >( + InOuter, UHoudiniParameterToggle::StaticClass(), ParamName, RF_Public | RF_Transactional); + + HoudiniAssetParameter->SetParameterType(EHoudiniParameterType::Toggle); + //HoudiniAssetParameter->UpdateFromParmInfo(InParentParameter, InNodeId, ParmInfo); + + return HoudiniAssetParameter; +} + +bool +UHoudiniParameterToggle::SetValueAt(const bool& InValue, const uint32& Index) +{ + if (!Values.IsValidIndex(Index)) + return false; + + if (Values[Index] == 0 && !InValue) + return false; + + if (Values[Index] == 1 && InValue) + return false; + + Values[Index] = InValue ? 1 : 0; + return true; +} + +bool +UHoudiniParameterToggle::IsDefault() const +{ + for (int32 Idx = 0; Idx < Values.Num(); ++Idx) + { + if (!DefaultValues.IsValidIndex(Idx)) + break; + + if (Values[Idx] != DefaultValues[Idx]) + return false; + } + + return true; +} + +void +UHoudiniParameterToggle::SetDefaultValues() +{ + if (DefaultValues.Num() > 0) + return; + + DefaultValues.Empty(); + for (int32 Idx = 0; Idx < Values.Num(); ++Idx) + { + DefaultValues.Add(Values[Idx]); + } +} + +bool +UHoudiniParameterToggle::SetValuesIfChanged(const TArray& NewValues) +{ + // Sets the new values, and returns true if any (or number) of parameters changed. + if(NewValues.Num() != Values.Num()) + { + Values = NewValues; + bHasChanged = true; + return true; + } + + for(int Index = 0; Index < NewValues.Num(); Index++) + { + if(Values[Index] != NewValues[Index]) + { + Values = NewValues; + bHasChanged = true; + return true; + } + } + return false; +} +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniParameterToggle.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniParameter.h" + +#include "HoudiniParameterToggle.generated.h" + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniParameterToggle : public UHoudiniParameter +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + // Create instance of this class. + static UHoudiniParameterToggle * Create( + UObject* Outer, + const FString& ParamName); + + // Accessor + bool GetValueAt(const uint32& Index) const { return Values.IsValidIndex(Index) ? (bool)Values[Index] : false; }; + + int32* GetValuesPtr() { return Values.Num() > 0 ? &Values[0] : nullptr; }; + + bool IsDefault() const override; + + // Mutators + bool SetValueAt(const bool& InValue, const uint32& Index); + void SetNumberOfValues(const uint32& InNumValues) { Values.SetNum(InNumValues); }; + + int32 GetNumValues() const { return Values.Num(); }; + + void SetDefaultValues(); + + bool SetValuesIfChanged(const TArray& NewValues); + + const TArray& GetDefaultValues() { return DefaultValues; }; + +protected: + + // Values of this property. + UPROPERTY() + TArray Values; + + UPROPERTY() + TArray DefaultValues; +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniPluginSerializationVersion.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniPluginSerializationVersion.h" + +#include "Serialization/CustomVersion.h" +#include "Misc/Guid.h" + +const FGuid FHoudiniCustomSerializationVersion::GUID( 0x1AB9CECC, 0x6913, 0x4875, 0x203d51fb ); + +// Register the custom version with core +FCustomVersionRegistration GRegisterHoudiniCustomVersion( FHoudiniCustomSerializationVersion::GUID, VER_HOUDINI_PLUGIN_SERIALIZATION_AUTOMATIC_VERSION, TEXT( "HoudiniUE4PluginVer" ) ); + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniPluginSerializationVersion.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "Misc/Guid.h" + +// Deprecated per-class versions used to load old files +// +// Serialization of parameter name map. +#define VER_HOUDINI_ENGINE_COMPONENT_PARAMETER_NAME_MAP 2 +// Serialization of instancer material, if it is available. +#define VER_HOUDINI_ENGINE_GEOPARTOBJECT_INSTANCER_MATERIAL_NAME 1 +// Serialization of attribute instancer material, if it is available. +#define VER_HOUDINI_ENGINE_GEOPARTOBJECT_INSTANCER_ATTRIBUTE_MATERIAL_NAME 2 +// Landscape serialization in asset inputs. +#define VER_HOUDINI_ENGINE_PARAM_LANDSCAPE_INPUT 1 +// Asset instance member. +#define VER_HOUDINI_ENGINE_PARAM_ASSET_INSTANCE_MEMBER 2 +// World Outliner inputs. +#define VER_HOUDINI_ENGINE_PARAM_WORLD_OUTLINER_INPUT 3 + + +enum EHoudiniPluginSerializationVersion +{ + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_BASE = 5, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_ADDED_UNREAL_SPLINE = 6, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_ADDED_KEEP_TRANSFORM = 7, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_MULTI_GEO_INPUT = 8, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_GEO_PART_NODE_PATH = 9, + VER_HOUDINI_PLUGIN_SERIALIZATION_HOUDINI_SPLINE_TO_TRANSFORM = 10, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_UNREAL_SPLINE_RESOLUTION_PER_INPUT = 11, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_CUSTOM_LINKER = 12, // added custom linker version to archives + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_ACTOR_INSTANCING = 13, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_LANDSCAPES = 14, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_PARAMETERS_UNIT = 15, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_BAKENAME_OVERRIDE = 16, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_COOK_TEMP_PACKAGES = 17, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_COOK_TEMP_PACKAGES_MESH_AND_LAYERS = 18, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_OUTLINER_INPUT_SAVE_ACTOR_ONLY = 19, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_GEOMETRY_INPUT_TRANSFORMS = 20, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_ADDED_PARAM_HELP = 21, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_INSTANCE_COLORS = 22, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_PARAMETERS_NOSWAP = 23, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_OUTLINER_INPUT_SAVE_MAT = 24, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_FILE_PARAM_READ_ONLY = 25, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_OUTLINER_INSTANCE_INDEX = 26, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_INPUT_LANDSCAPE_TRANSFORM = 27, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_OUTLINER_INPUT_SAVE_ACTOR_PATHNAME = 28, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_419_SERIALIZATION_FIX = 29, // Version 29 is a fix for a serialization issue with UE4.19 / H17.0/16.5, 29 is actually version 26 minus the version 24 changes... + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_POST_419_SERIALIZATION_FIX = 30, + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_INPUT_SOFT_REF = 31, + + //------------------------------------------------------------ + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_V2_BASE = 100, + + // Introduced the FHoudiniInputObjectSettings struct and added it to UHoudiniInput, migrating some input settings + // from UHoudiniInput to a member FHoudiniInputObjectSettings struct: UHoudiniInput::InputSettings + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_INPUT_OBJECT_SETTINGS_STRUCT = 101, + + //----- VERSION 3 - HOUDINI21 - Introduces the Cookable --------------------------------------------- + // Commented until we enable cookables by default + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_V3_BASE = 300, + + // ------------------------------------------------------ + // - this needs to be the last line (see note below) + VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_BASE_PLUS_ONE, + VER_HOUDINI_PLUGIN_SERIALIZATION_AUTOMATIC_VERSION = VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_BASE_PLUS_ONE - 1 +}; + +struct FHoudiniCustomSerializationVersion +{ + // The GUID for this custom version number + const static FGuid GUID; + +private: + FHoudiniCustomSerializationVersion() {} +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniRuntimeSettings.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniRuntimeSettings.h" + + +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "Misc/Paths.h" +// #include "Internationalization/Internationalization.h" + +#include "HoudiniEngineRuntimePrivatePCH.h" + +#define LOCTEXT_NAMESPACE HOUDINI_LOCTEXT_NAMESPACE + + +FHoudiniStaticMeshGenerationProperties::FHoudiniStaticMeshGenerationProperties() + : bGeneratedDoubleSidedGeometry(false) + , GeneratedPhysMaterial(nullptr) + , GeneratedCollisionTraceFlag(CTF_UseDefault) + , GeneratedLightMapResolution(64) + , GeneratedWalkableSlopeOverride() + , GeneratedLightMapCoordinateIndex(1) + , bGeneratedUseMaximumStreamingTexelRatio(false) + , GeneratedStreamingDistanceMultiplier(1.0f) + , GeneratedFoliageDefaultSettings(nullptr) + , GeneratedAssetUserData() +{ + DefaultBodyInstance.SetCollisionProfileName("BlockAll"); +} + + +UHoudiniRuntimeSettings::UHoudiniRuntimeSettings( const FObjectInitializer & ObjectInitializer ) + : Super( ObjectInitializer ) +{ + // Session options. + SessionType = HRSST_NamedPipe; + NumSessions = 1; + ServerHost = HAPI_UNREAL_SESSION_SERVER_HOST; + ServerPort = HAPI_UNREAL_SESSION_SERVER_PORT; + ServerPipeName = HAPI_UNREAL_SESSION_SERVER_PIPENAME; + bStartAutomaticServer = HAPI_UNREAL_SESSION_SERVER_AUTOSTART; + AutomaticServerTimeout = HAPI_UNREAL_SESSION_SERVER_TIMEOUT; + bUseSessionSyncForAutomaticServer = false; + + SharedMemoryBufferSize = 500; + bSharedMemoryBufferCyclic = true; + bEnableSharedMemoryDataTransfer = false; + + bSyncWithHoudiniCook = true; + bCookUsingHoudiniTime = true; + bSyncViewport = false; + bSyncHoudiniViewport = false; + bSyncUnrealViewport = false; + + // Instantiating options. + bShowMultiAssetDialog = true; + bPreferHdaMemoryCopyOverHdaSourceFile = false; + + // Cooking options. + bPauseCookingOnStart = false; + bDisplaySlateCookingNotifications = true; + DefaultTemporaryCookFolder = HAPI_UNREAL_DEFAULT_TEMP_COOK_FOLDER; + DefaultBakeFolder = HAPI_UNREAL_DEFAULT_BAKE_FOLDER; + bUsePresetsForParameters = true; + + // Parameter options + //bTreatRampParametersAsMultiparms = false; + + // Custom Houdini location. + bUseCustomHoudiniLocation = false; + CustomHoudiniLocation.Path = TEXT(""); + HoudiniExecutable = HRSHE_Houdini; + CustomHoudiniHomeLocation.Path = TEXT(""); + + // Arguments for HAPI_Initialize + CookingThreadStackSize = -1; + + // Landscape marshalling default values. + MarshallingLandscapesUseDefaultUnrealScaling = false; + MarshallingLandscapesUseFullResolution = true; + MarshallingLandscapesForceMinMaxValues = false; + MarshallingLandscapesForcedMinValue = -2000.0f; + MarshallingLandscapesForcedMaxValue = 4553.0f; + + // Spline marshalling + MarshallingSplineResolution = 50.0f; + + // Static mesh proxy refinement settings + bEnableProxyStaticMesh = true; + bShowDefaultMesh = true; + bPreferNaniteFallbackMesh = false; + + bEnableProxyStaticMeshRefinementByTimer = false; + ProxyMeshAutoRefineTimeoutSeconds = 10.0f; + bEnableProxyStaticMeshRefinementOnPreSaveWorld = true; + bEnableProxyStaticMeshRefinementOnPreBeginPIE = true; + + // Generated StaticMesh settings. + bDoubleSidedGeometry = false; + PhysMaterial = nullptr; + DefaultBodyInstance.SetCollisionProfileName("BlockAll"); + CollisionTraceFlag = CTF_UseDefault; + LightMapResolution = 32; + LightMapCoordinateIndex = 1; + bUseMaximumStreamingTexelRatio = false; + StreamingDistanceMultiplier = 1.0f; + GeneratedDistanceFieldResolutionScale = 0.0f; + + // Static Mesh build settings. + bUseFullPrecisionUVs = false; + SrcLightmapIndex = 0; + DstLightmapIndex = 1; + MinLightmapResolution = 64; + bRemoveDegenerates = true; + GenerateLightmapUVsFlag = HRSRF_OnlyIfMissing; + RecomputeNormalsFlag = HRSRF_OnlyIfMissing; + RecomputeTangentsFlag = HRSRF_OnlyIfMissing; + bUseMikkTSpace = true; + + bComputeWeightedNormals = false; + bBuildReversedIndexBuffer = true; + bUseHighPrecisionTangentBasis = false; + bGenerateDistanceFieldAsIfTwoSided = false; + bSupportFaceRemap = false; + //BuildScale3D = FVector(1.0f, 1.0f, 1.0f); + DistanceFieldResolutionScale = 2.0f; // ue default is 1.0 + + bPDGAsyncCommandletImportEnabled = false; + + // Curve inputs and editable output curves + bAddRotAndScaleAttributesOnCurves = false; + bUseLegacyInputCurves = true; + + // Houdini Tools + HoudiniToolsSearchPath = { TEXT("/Game/HoudiniEngine/Tools") }; +} + +UHoudiniRuntimeSettings::~UHoudiniRuntimeSettings() +{} + + +FProperty * +UHoudiniRuntimeSettings::LocateProperty(const FString & PropertyName) const +{ + for (TFieldIterator< FProperty > PropIt(GetClass()); PropIt; ++PropIt) + { + FProperty * Property = *PropIt; + + if (Property->GetNameCPP() == PropertyName) + return Property; + } + + return nullptr; +} + + +void +UHoudiniRuntimeSettings::SetPropertyReadOnly(const FString & PropertyName, bool bReadOnly) +{ + FProperty * Property = LocateProperty(PropertyName); + if (Property) + { + if (bReadOnly) + Property->SetPropertyFlags(CPF_EditConst); + else + Property->ClearPropertyFlags(CPF_EditConst); + } +} + + +void +UHoudiniRuntimeSettings::PostInitProperties() +{ + Super::PostInitProperties(); + + // Set Collision generation options as read only + { + if (FProperty* Property = LocateProperty(TEXT("CollisionGroupNamePrefix"))) + Property->SetPropertyFlags(CPF_EditConst); + + if (FProperty* Property = LocateProperty(TEXT("RenderedCollisionGroupNamePrefix"))) + Property->SetPropertyFlags(CPF_EditConst); + + if (FProperty* Property = LocateProperty(TEXT("UCXCollisionGroupNamePrefix"))) + Property->SetPropertyFlags(CPF_EditConst); + + if (FProperty* Property = LocateProperty(TEXT("UCXRenderedCollisionGroupNamePrefix"))) + Property->SetPropertyFlags(CPF_EditConst); + + if (FProperty* Property = LocateProperty(TEXT("SimpleCollisionGroupNamePrefix"))) + Property->SetPropertyFlags(CPF_EditConst); + + if (FProperty* Property = LocateProperty(TEXT("SimpleRenderedCollisionGroupNamePrefix"))) + Property->SetPropertyFlags(CPF_EditConst); + } + + // Set marshalling attributes options as read only + { + if (FProperty* Property = LocateProperty(TEXT("MarshallingAttributeMaterial"))) + Property->SetPropertyFlags(CPF_EditConst); + + if (FProperty* Property = LocateProperty(TEXT("MarshallingAttributeMaterialHole"))) + Property->SetPropertyFlags(CPF_EditConst); + + if (FProperty* Property = LocateProperty(TEXT("MarshallingAttributeInstanceOverride"))) + Property->SetPropertyFlags(CPF_EditConst); + + if (FProperty* Property = LocateProperty(TEXT("MarshallingAttributeFaceSmoothingMask"))) + Property->SetPropertyFlags(CPF_EditConst); + + if (FProperty* Property = LocateProperty(TEXT("MarshallingAttributeLightmapResolution"))) + Property->SetPropertyFlags(CPF_EditConst); + + if (FProperty* Property = LocateProperty(TEXT("MarshallingAttributeGeneratedMeshName"))) + Property->SetPropertyFlags(CPF_EditConst); + + if (FProperty* Property = LocateProperty(TEXT("MarshallingAttributeInputMeshName"))) + Property->SetPropertyFlags(CPF_EditConst); + } + + /* + // Set Cook Folder as read-only + { + if ( FProperty* Property = LocateProperty( TEXT( "TemporaryCookFolder" ) ) ) + Property->SetPropertyFlags( CPF_EditConst ); + } + */ + + // Set Landscape forced min/max as read only when not overriden + if (!MarshallingLandscapesForceMinMaxValues) + { + if (FProperty* Property = LocateProperty(TEXT("MarshallingLandscapesForcedMinValue"))) + Property->SetPropertyFlags(CPF_EditConst); + if (FProperty* Property = LocateProperty(TEXT("MarshallingLandscapesForcedMaxValue"))) + Property->SetPropertyFlags(CPF_EditConst); + } + + // Disable UI elements depending on current session type. +#if WITH_EDITOR + + UpdateSessionUI(); + +#endif // WITH_EDITOR +} + + +#if WITH_EDITOR +void +UHoudiniRuntimeSettings::PostEditChangeProperty(struct FPropertyChangedEvent & PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + FProperty * Property = PropertyChangedEvent.MemberProperty; + FProperty * LookupProperty = nullptr; + + if (!Property) + return; + if (Property->GetName() == TEXT("SessionType")) + UpdateSessionUI(); + else if (Property->GetName() == TEXT("CustomHoudiniLocation")) + { + if (!FHoudiniEngineRuntimeUtils::CheckCustomHoudiniLocation(CustomHoudiniLocation.Path)) + CustomHoudiniLocation.Path = TEXT(""); + } + else if (Property->GetName() == TEXT("MarshallingLandscapesForceMinMaxValues")) + { + // Set Landscape forced min/max as read only when not overriden + if (!MarshallingLandscapesForceMinMaxValues) + { + if (FProperty* MinProperty = LocateProperty(TEXT("MarshallingLandscapesForcedMinValue"))) + MinProperty->SetPropertyFlags(CPF_EditConst); + if (FProperty* MaxProperty = LocateProperty(TEXT("MarshallingLandscapesForcedMaxValue"))) + MaxProperty->SetPropertyFlags(CPF_EditConst); + } + else + { + if (FProperty* MinProperty = LocateProperty(TEXT("MarshallingLandscapesForcedMinValue"))) + MinProperty->ClearPropertyFlags(CPF_EditConst); + if (FProperty* MaxProperty = LocateProperty(TEXT("MarshallingLandscapesForcedMaxValue"))) + MaxProperty->ClearPropertyFlags(CPF_EditConst); + } + } + + /* + if ( Property->GetName() == TEXT( "bEnableCooking" ) ) + { + // Cooking is disabled, we need to disable transform change triggers cooks option is as well. + if ( bEnableCooking ) + { + LookupProperty = LocateProperty( TEXT( "bUploadTransformsToHoudiniEngine" ) ); + if ( LookupProperty ) + LookupProperty->ClearPropertyFlags( CPF_EditConst ); + + LookupProperty = LocateProperty( TEXT( "bTransformChangeTriggersCooks" ) ); + if ( LookupProperty ) + LookupProperty->ClearPropertyFlags( CPF_EditConst ); + } + else + { + LookupProperty = LocateProperty( TEXT( "bUploadTransformsToHoudiniEngine" ) ); + if ( LookupProperty ) + LookupProperty->SetPropertyFlags( CPF_EditConst ); + + LookupProperty = LocateProperty( TEXT( "bTransformChangeTriggersCooks" ) ); + if ( LookupProperty ) + LookupProperty->SetPropertyFlags( CPF_EditConst ); + } + } + else if ( Property->GetName() == TEXT( "bUploadTransformsToHoudiniEngine" ) ) + { + // If upload of transformations is disabled, then there's no sense in cooking asset on transformation change. + if ( bUploadTransformsToHoudiniEngine ) + { + LookupProperty = LocateProperty( TEXT( "bTransformChangeTriggersCooks" ) ); + if ( LookupProperty ) + LookupProperty->ClearPropertyFlags( CPF_EditConst ); + } + else + { + LookupProperty = LocateProperty( TEXT( "bTransformChangeTriggersCooks" ) ); + if ( LookupProperty ) + LookupProperty->SetPropertyFlags( CPF_EditConst ); + } + } + */ +} + + + +void +UHoudiniRuntimeSettings::UpdateSessionUI() +{ + SetPropertyReadOnly(TEXT("ServerHost"), true); + SetPropertyReadOnly(TEXT("ServerPort"), true); + SetPropertyReadOnly(TEXT("ServerPipeName"), true); + SetPropertyReadOnly(TEXT("bStartAutomaticServer"), true); + SetPropertyReadOnly(TEXT("AutomaticServerTimeout"), true); + SetPropertyReadOnly(TEXT("SharedMemoryBufferSize"), true); + SetPropertyReadOnly(TEXT("bSharedMemoryBufferCyclic"), true); + SetPropertyReadOnly(TEXT("bEnableSharedMemoryDataTransfer"), true); + + bool bServerType = false; + + switch (SessionType) + { + case HRSST_Socket: + { + SetPropertyReadOnly(TEXT("ServerHost"), false); + SetPropertyReadOnly(TEXT("ServerPort"), false); + SetPropertyReadOnly(TEXT("bEnableSharedMemoryDataTransfer"), false); + bServerType = true; + break; + } + + case HRSST_NamedPipe: + { + SetPropertyReadOnly(TEXT("ServerPipeName"), false); + SetPropertyReadOnly(TEXT("bEnableSharedMemoryDataTransfer"), false); + bServerType = true; + break; + } + + case HRSST_MemoryBuffer: + { + SetPropertyReadOnly(TEXT("ServerPipeName"), false); + SetPropertyReadOnly(TEXT("SharedMemoryBufferSize"), false); + SetPropertyReadOnly(TEXT("bSharedMemoryBufferCyclic"), false); + //SetPropertyReadOnly(TEXT("bEnableSharedMemoryDataTransfer"), true); + + bServerType = true; + break; + } + + default: + break; + } + + if (bServerType) + { + SetPropertyReadOnly(TEXT("bStartAutomaticServer"), false); + SetPropertyReadOnly(TEXT("AutomaticServerTimeout"), false); + } + + if (bEnableSharedMemoryDataTransfer) + { + SetPropertyReadOnly(TEXT("SharedMemoryBufferSize"), false); + } +} + +#endif // WITH_EDITOR + +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniRuntimeSettings.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "Engine/EngineTypes.h" +#include "Engine/AssetUserData.h" +#include "PhysicsEngine/BodyInstance.h" + +#include "HoudiniRuntimeSettings.generated.h" + +class UFoliageType_InstancedStaticMesh; + +UENUM() +enum EHoudiniRuntimeSettingsSessionType : int8 +{ + // In process session. + HRSST_InProcess UMETA(Hidden, DisplayName = "In Process (experimental)"), + + // TCP socket connection to Houdini Engine server. + HRSST_Socket UMETA(DisplayName = "TCP socket"), + + // Connection to Houdini Engine server via pipe connection. + HRSST_NamedPipe UMETA(DisplayName = "Named pipe or domain socket"), + + // No session, prevents license/Engine cook + HRSST_None UMETA(DisplayName = "None"), + + // Memory Buffer + HRSST_MemoryBuffer UMETA(DisplayName = "Shared Memory Buffer"), + + HRSST_MAX +}; + + +UENUM() +enum EHoudiniRuntimeSettingsRecomputeFlag : int8 +{ + // Recompute always. + HRSRF_Always UMETA(DisplayName = "Always"), + + // Recompute only if missing. + HRSRF_OnlyIfMissing UMETA(DisplayName = "Only if missing"), + + // Do not recompute. + HRSRF_Never UMETA(DisplayName = "Never"), + + HRSRF_MAX, +}; + +UENUM() +enum EHoudiniExecutableType : int8 +{ + // Houdini + HRSHE_Houdini UMETA(DisplayName = "Houdini"), + + // Houdini FX + HRSHE_HoudiniFX UMETA(DisplayName = "Houdini FX"), + + // Houdini Core + HRSHE_HoudiniCore UMETA(DisplayName = "Houdini Core"), + + // Houdini Indie + HRSHE_HoudiniIndie UMETA(DisplayName = "Houdini Indie"), +}; + +USTRUCT(BlueprintType) +struct HOUDINIENGINERUNTIME_API FHoudiniStaticMeshGenerationProperties +{ + GENERATED_USTRUCT_BODY() + + // Constructor + FHoudiniStaticMeshGenerationProperties(); + + public: + + /** If true, the physics triangle mesh will use double sided faces when doing scene queries. */ + UPROPERTY(EditAnywhere, Category = "HoudiniMeshGeneration | StaticMeshGeneration", meta = (DisplayName = "Double Sided Geometry")) + uint32 bGeneratedDoubleSidedGeometry : 1; + + /** Physical material to use for simple collision on this body. Encodes information about density, friction etc. */ + UPROPERTY(EditAnywhere, Category = "HoudiniMeshGeneration | StaticMeshGeneration", meta = (DisplayName = "Simple Collision Physical Material")) + TObjectPtr GeneratedPhysMaterial; + + // TODO: DEPRECATE - COULD BE USED! + /** Default properties of the body instance, copied into objects on instantiation, was URB_BodyInstance */ + UPROPERTY(EditAnywhere, Category = "HoudiniMeshGeneration | StaticMeshGeneration", meta = (FullyExpand = "true")) + struct FBodyInstance DefaultBodyInstance; + + /** Collision Trace behavior - by default, it will keep simple(convex)/complex(per-poly) separate. */ + UPROPERTY(EditAnywhere, Category = "HoudiniMeshGeneration | StaticMeshGeneration", meta = (DisplayName = "Collision Complexity")) + TEnumAsByte GeneratedCollisionTraceFlag; + + // TODO: DEPRECATE + /** Resolution of lightmap. */ + UPROPERTY(EditAnywhere, Category = "HoudiniMeshGeneration | StaticMeshGeneration", meta = (DisplayName = "Light Map Resolution", FixedIncrement = "4.0")) + int32 GeneratedLightMapResolution; + + // TODO: DEPRECATE - COULD BE USED + /** Custom walkable slope setting for generated mesh's body. */ + UPROPERTY(EditAnywhere, AdvancedDisplay, Category = "HoudiniMeshGeneration | StaticMeshGeneration", meta = (DisplayName = "Walkable Slope Override")) + FWalkableSlopeOverride GeneratedWalkableSlopeOverride; + + // TODO: DEPRECATE - COULD BE USED - AUTO + /** The light map coordinate index. */ + UPROPERTY(EditAnywhere, AdvancedDisplay, Category = "HoudiniMeshGeneration | StaticMeshGeneration", meta = (DisplayName = "Light map coordinate index")) + int32 GeneratedLightMapCoordinateIndex; + + // TODO: DEPRECATE + /** True if mesh should use a less-conservative method of mip LOD texture factor computation. */ + UPROPERTY(EditAnywhere, AdvancedDisplay, Category = "HoudiniMeshGeneration | StaticMeshGeneration", meta = (DisplayName = "Use Maximum Streaming Texel Ratio")) + uint32 bGeneratedUseMaximumStreamingTexelRatio : 1; + + // TODO: DEPRECATE + /** Allows artists to adjust the distance where textures using UV 0 are streamed in/out. */ + UPROPERTY(EditAnywhere, AdvancedDisplay, Category = "HoudiniMeshGeneration | StaticMeshGeneration", meta = (DisplayName = "Streaming Distance Multiplier")) + float GeneratedStreamingDistanceMultiplier; + + // TODO: DEPRECATE + /** Default settings when using this mesh for instanced foliage. */ + UPROPERTY(EditAnywhere, AdvancedDisplay, Instanced, Category = "HoudiniMeshGeneration | StaticMeshGeneration", meta = (DisplayName = "Foliage Default Settings")) + TObjectPtr GeneratedFoliageDefaultSettings = nullptr; + + // TODO: DEPRECATE + /** Array of user data stored with the asset. */ + UPROPERTY(EditAnywhere, AdvancedDisplay, Instanced, Category = "HoudiniMeshGeneration | StaticMeshGeneration", meta = (DisplayName = "Asset User Data")) + TArray> GeneratedAssetUserData; +}; + + +UCLASS(config = Engine, defaultconfig) +class HOUDINIENGINERUNTIME_API UHoudiniRuntimeSettings : public UObject +{ + GENERATED_UCLASS_BODY() + + public: + + // Destructor. + virtual ~UHoudiniRuntimeSettings(); + + // + virtual void PostInitProperties() override; + +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent & PropertyChangedEvent) override; +#endif + +protected: + + // Locate property of this class by name. + FProperty * LocateProperty(const FString & PropertyName) const; + + // Make specified property read only. + void SetPropertyReadOnly(const FString & PropertyName, bool bReadOnly = true); + +#if WITH_EDITOR + // Update session ui elements. + void UpdateSessionUI(); +#endif + + public: + + //------------------------------------------------------------------------------------------------------------- + // Session options. + //------------------------------------------------------------------------------------------------------------- + UPROPERTY(GlobalConfig, EditAnywhere, Category = Session) + TEnumAsByte SessionType; + + // The number of threaded sessions to be used to send/receive data (default: 1) + UPROPERTY(GlobalConfig, EditAnywhere, Category = Session, meta = (ClampMin = "1", ClampMax = "128")) + int32 NumSessions; + + // The name of the machine that hosts the houdini server (default: localhost) + UPROPERTY(GlobalConfig, EditAnywhere, Category = Session) + FString ServerHost; + + // The port to be used for a TCP socket session (default: 9090) + UPROPERTY(GlobalConfig, EditAnywhere, Category = Session) + int32 ServerPort; + + // The name of the server used for Name Pipe or Shared Memory buffer session (default: hapi) + UPROPERTY(GlobalConfig, EditAnywhere, Category = Session, meta = (DisplayName = "Server Name")) + FString ServerPipeName; + + // Indicates if shared memory data transfer should be used in the current session. + // Shared memory data transfer will make data transfer between Unreal and Houdini a lot faster, especially for large amount of data. + // This can be used with any type of sessions as long as the Houdini Engine session is running on the same machine as the plugin (default: disabled) + UPROPERTY(GlobalConfig, EditAnywhere, Category = Session) + bool bEnableSharedMemoryDataTransfer; + + // The size (in MB) of the Memory buffer used for shared memory buffer sessions (default: 500) + // Value are clamped between 1MB - 128GB and UI between 1MB and 16GB + UPROPERTY(GlobalConfig, EditAnywhere, Category = Session, meta = (ClampMin = "1", ClampMax = "131072", UIMin = "1", UIMax = "16384")) + int64 SharedMemoryBufferSize; + + // Indicates if the shared memory buffer session use a cyclic (ring) buffer. If disabled, the buffer will be of fixed size. (default: enabled) + UPROPERTY(GlobalConfig, EditAnywhere, Category = Session) + bool bSharedMemoryBufferCyclic; + // Automatically try to start a HARS process matching the current session settings when restarting the session (default: enabled) + UPROPERTY(GlobalConfig, EditAnywhere, Category = Session) + bool bStartAutomaticServer; + + // Automatically created servers create a Session Sync session. (default: disabled) + UPROPERTY(GlobalConfig, EditAnywhere, Category = Session) + bool bUseSessionSyncForAutomaticServer; + + // The timeout (in ms) to be used when attempting to create a Houdini Engine Session (default: 3000) + UPROPERTY(GlobalConfig, EditAnywhere, Category = Session) + float AutomaticServerTimeout; + + // If enabled, changes made in Houdini, when connected to Houdini running in Session Sync mode will be automatically be pushed to Unreal. + UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = Session) + bool bSyncWithHoudiniCook; + + // If enabled, the Houdini Timeline time will be used to cook assets. + UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = Session) + bool bCookUsingHoudiniTime; + + // Enable when wanting to sync the Houdini and Unreal viewport when using Session Sync. + UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = Session) + bool bSyncViewport; + + // If enabled, Houdini's viewport will be synchronized to Unreal's when using Session Sync. + UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = Session, meta = (DisplayName = "Sync the Houdini Viewport to Unreal's viewport.", EditCondition = "bSyncViewport")) + bool bSyncHoudiniViewport; + + // If enabled, Unreal's viewport will be synchronized to Houdini's when using Session Sync. + UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = Session, meta = (DisplayName = "Sync the Unreal Viewport to Houdini's viewport", EditCondition = "bSyncViewport")) + bool bSyncUnrealViewport; + + //------------------------------------------------------------------------------------------------------------- + // Instantiating options. + //------------------------------------------------------------------------------------------------------------- + + // Whether to ask user to select an asset when instantiating an HDA with multiple assets inside. If disabled, will always instantiate first asset. + UPROPERTY(GlobalConfig, EditAnywhere, Category = Instantiating) + bool bShowMultiAssetDialog; + + // When enabled, the plugin will always instantiate the memory copy of a HDA stored in the .uasset file + // instead of using the latest version of the HDA file itself. + // This helps ensuring consistency between users when using HDAs, but will not work with expanded HDAs. + // When disabled, the plugin will always instantiate the latest version of the source HDA file if it is + // available, and will fallback to the memory copy if the source file cannot be found + UPROPERTY(GlobalConfig, EditAnywhere, Category = Instantiating) + bool bPreferHdaMemoryCopyOverHdaSourceFile; + + //------------------------------------------------------------------------------------------------------------- + // Cooking options. + //------------------------------------------------------------------------------------------------------------- + + // Whether houdini engine cooking is paused or not upon initializing the plugin + UPROPERTY(GlobalConfig, EditAnywhere, Category = Cooking) + bool bPauseCookingOnStart; + + // Whether to display instantiation and cooking Slate notifications. + UPROPERTY(GlobalConfig, EditAnywhere, Category = Cooking) + bool bDisplaySlateCookingNotifications; + + // Default content folder storing all the temporary cook data (Static meshes, materials, textures, landscape layer infos...) + UPROPERTY(GlobalConfig, EditAnywhere, Category = Cooking) + FString DefaultTemporaryCookFolder; + + // Default content folder used when baking houdini asset data to native unreal objects + UPROPERTY(GlobalConfig, EditAnywhere, Category = Cooking) + FString DefaultBakeFolder; + + // Whether Houdini Engine can store/set multiple parameters via presets + UPROPERTY(GlobalConfig, EditAnywhere, Category = Cooking) + bool bUsePresetsForParameters; + + //------------------------------------------------------------------------------------------------------------- + // Geometry Marshalling + //------------------------------------------------------------------------------------------------------------- + + // If true, generated Landscapes will be marshalled using default unreal scaling. + // Generated landscape will loose a lot of precision on the Z axis but will use the same transforms + // as Unreal's default landscape + UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeometryMarshalling", meta = (DisplayName = "Landscape - Use default Unreal scaling.")) + bool MarshallingLandscapesUseDefaultUnrealScaling; + + // If true, generated Landscapes will be using full precision for their ZAxis, + // allowing for more precision but preventing them from being sculpted higher/lower than their min/max. + UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeometryMarshalling", meta = (DisplayName = "Landscape - Use full resolution for data conversion.")) + bool MarshallingLandscapesUseFullResolution; + + // If true, the min/max values used to convert heightfields to landscape will be forced values + // This is usefull when importing multiple landscapes from different HDAs + UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeometryMarshalling", meta = (DisplayName = "Landscape - Force Min/Max values for data conversion")) + bool MarshallingLandscapesForceMinMaxValues; + + // The minimum value to be used for Landscape conversion when MarshallingLandscapesForceMinMaxValues is enabled + UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeometryMarshalling", meta = (DisplayName = "Landscape - Forced min value")) + float MarshallingLandscapesForcedMinValue; + + // The maximum value to be used for Landscape conversion when MarshallingLandscapesForceMinMaxValues is enabled + UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeometryMarshalling", meta = (DisplayName = "Landscape - Forced max value")) + float MarshallingLandscapesForcedMaxValue; + + // If this is enabled, additional rot & scale attributes are added on curve inputs + UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeometryMarshalling", meta = (DisplayName = "Curves - Add rot & scale attributes on curve inputs")) + bool bAddRotAndScaleAttributesOnCurves; + + // If this is enabled, additional rot & scale attributes are added on curve inputs + UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeometryMarshalling", meta = (DisplayName = "Curves - Use Legacy Input Curves")) + bool bUseLegacyInputCurves; + + // Default resolution used when converting Unreal Spline Components to Houdini Curves (step in cm between control points, 0 only send the control points) + UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeometryMarshalling", meta = (DisplayName = "Curves - Default spline resolution (cm)")) + float MarshallingSplineResolution; + + //------------------------------------------------------------------------------------------------------------- + // Static Mesh Options + //------------------------------------------------------------------------------------------------------------- + + // For StaticMesh outputs: should a fast proxy be created first? + UPROPERTY(GlobalConfig, EditAnywhere, Category = "Static Mesh", meta = (DisplayName = "Enable Proxy Static Mesh")) + bool bEnableProxyStaticMesh; + + // For static mesh outputs and socket actors: should spawn a default actor if the reference is invalid? + UPROPERTY(GlobalConfig, EditAnywhere, Category = "Static Mesh", meta = (DisplayName = "Show Default Mesh")) + bool bShowDefaultMesh; + + // Prefer the Nanite fallback mesh when a Nanite asset is used as Houdini input. + UPROPERTY(GlobalConfig, EditAnywhere, Category = "Static Mesh", meta = (DisplayName = "Prefer Nanite Fallback Mesh")) + bool bPreferNaniteFallbackMesh; + + // If fast proxy meshes are being created, must it be baked as a StaticMesh after a period of no updates? + UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = "Static Mesh", meta = (DisplayName = "Refine Proxy Static Meshes After a Timeout", EditCondition = "bEnableProxyStaticMesh")) + bool bEnableProxyStaticMeshRefinementByTimer; + + // If the option to automatically refine the proxy mesh via a timer has been selected, this controls the timeout in seconds. + UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = "Static Mesh", meta = (DisplayName = "Proxy Mesh Auto Refine Timeout Seconds", EditCondition = "bEnableProxyStaticMesh && bEnableProxyStaticMeshRefinementByTimer")) + float ProxyMeshAutoRefineTimeoutSeconds; + + // Automatically refine proxy meshes to UStaticMesh before the map is saved + UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = "Static Mesh", meta = (DisplayName = "Refine Proxy Static Meshes When Saving a Map", EditCondition = "bEnableProxyStaticMesh")) + bool bEnableProxyStaticMeshRefinementOnPreSaveWorld; + + // Automatically refine proxy meshes to UStaticMesh before starting a play in editor session + UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = "Static Mesh", meta = (DisplayName = "Refine Proxy Static Meshes On PIE", EditCondition = "bEnableProxyStaticMesh")) + bool bEnableProxyStaticMeshRefinementOnPreBeginPIE; + + //------------------------------------------------------------------------------------------------------------- + // Generated StaticMesh settings. + //------------------------------------------------------------------------------------------------------------- + + /// If true, the physics triangle mesh will use double sided faces for new Houdini Assets when doing scene queries. + UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Double Sided Geometry")) + uint32 bDoubleSidedGeometry : 1; + + /// Physical material to use for simple collision of new Houdini Assets. Encodes information about density, friction etc. + UPROPERTY(EditAnywhere, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Simple Collision Physical Material")) + TObjectPtr PhysMaterial; + + /// Default properties of the body instance + UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeneratedStaticMeshSettings", meta = (FullyExpand = "true")) + struct FBodyInstance DefaultBodyInstance; + + /// Collision Trace behavior - by default, it will keep simple(convex)/complex(per-poly) separate for new Houdini Assets. + UPROPERTY(GlobalConfig, VisibleDefaultsOnly, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Collision Complexity")) + TEnumAsByte CollisionTraceFlag; + + /// Resolution of lightmap for baked lighting. + UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Light Map Resolution", FixedIncrement = "4.0")) + int32 LightMapResolution; + + /// Bias multiplier for Light Propagation Volume lighting for new Houdini Assets. + UPROPERTY(GlobalConfig, EditAnywhere, BlueprintReadOnly, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Lpv Bias Multiplier", UIMin = "0.0", UIMax = "3.0")) + float LpvBiasMultiplier; + + /// Default Mesh distance field resolution, setting it to 0 will prevent the mesh distance field generation while editing the asset + UPROPERTY(GlobalConfig, EditAnywhere, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Distance Field Resolution Scale", UIMin = "0.0", UIMax = "100.0")) + float GeneratedDistanceFieldResolutionScale; + + /// Custom walkable slope setting for bodies of new Houdini Assets. + UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Walkable Slope Override")) + FWalkableSlopeOverride WalkableSlopeOverride; + + /// The UV coordinate index of lightmap + UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Light map coordinate index")) + int32 LightMapCoordinateIndex; + + /// True if mesh should use a less-conservative method of mip LOD texture factor computation for new Houdini Assets. + UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Use Maximum Streaming Texel Ratio")) + uint32 bUseMaximumStreamingTexelRatio : 1; + + /// Allows artists to adjust the distance where textures using UV 0 are streamed in/out for new Houdini Assets. + UPROPERTY(GlobalConfig, EditAnywhere, AdvancedDisplay, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Streaming Distance Multiplier")) + float StreamingDistanceMultiplier; + + /// Default settings when using new Houdini Asset mesh for instanced foliage. + UPROPERTY(EditAnywhere, AdvancedDisplay, Instanced, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Foliage Default Settings")) + TObjectPtr FoliageDefaultSettings; + + /// Array of user data stored with the new Houdini Asset. + UPROPERTY(EditAnywhere, AdvancedDisplay, Instanced, Category = "GeneratedStaticMeshSettings", meta = (DisplayName = "Asset User Data")) + TArray > AssetUserData; + + //------------------------------------------------------------------------------------------------------------- + // Static Mesh build settings. + //------------------------------------------------------------------------------------------------------------- + + // If true, UVs will be stored at full floating point precision. + UPROPERTY(GlobalConfig, EditAnywhere, Category = "StaticMeshBuildSettings") + bool bUseFullPrecisionUVs; + + // Source UV set for generated lightmap. + UPROPERTY(GlobalConfig, EditAnywhere, Category = "StaticMeshBuildSettings", meta = (DisplayName = "Source Lightmap Index")) + int32 SrcLightmapIndex; + + // Destination UV set for generated lightmap. + UPROPERTY(GlobalConfig, EditAnywhere, Category = "StaticMeshBuildSettings", meta = (DisplayName = "Destination Lightmap Index")) + int32 DstLightmapIndex; + + // Target lightmap resolution to for generated lightmap. Determines the padding between UV shells in a packed lightmap. + UPROPERTY(GlobalConfig, EditAnywhere, Category = "StaticMeshBuildSettings") + int32 MinLightmapResolution; + + // If true, degenerate triangles will be removed. + UPROPERTY(GlobalConfig, EditAnywhere, Category = "StaticMeshBuildSettings") + bool bRemoveDegenerates; + + // Lightmap UV generation + UPROPERTY(GlobalConfig, EditAnywhere, Category = "StaticMeshBuildSettings", meta = (DisplayName = "Generate Lightmap UVs")) + TEnumAsByte GenerateLightmapUVsFlag; + + // Normals generation + UPROPERTY(GlobalConfig, EditAnywhere, Category = "StaticMeshBuildSettings", meta = (DisplayName = "Recompute Normals")) + TEnumAsByte RecomputeNormalsFlag; + + // Tangents generation + UPROPERTY(GlobalConfig, EditAnywhere, Category = "StaticMeshBuildSettings", meta = (DisplayName = "Recompute Tangents")) + TEnumAsByte RecomputeTangentsFlag; + + // If true, recomputed tangents and normals will be calculated using MikkT Space. This method does require properly laid out UVs though otherwise you'll get a degenerate tangent warning + UPROPERTY(GlobalConfig, EditAnywhere, Category = "StaticMeshBuildSettings", meta = (DisplayName = "Generate Using MikkT Space")) + bool bUseMikkTSpace; + + // If true, we will use the surface area and the corner angle of the triangle as a ratio when computing the normals. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "StaticMeshBuildSettings") + uint8 bComputeWeightedNormals : 1; + + // Required to optimize mesh in mirrored transform. Double index buffer size. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "StaticMeshBuildSettings") + uint8 bBuildReversedIndexBuffer : 1; + + // If true, Tangents will be stored at 16 bit vs 8 bit precision. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "StaticMeshBuildSettings") + uint8 bUseHighPrecisionTangentBasis : 1; + + // Scale to apply to the mesh when allocating the distance field volume texture. + // The default scale is 1, which is assuming that the mesh will be placed unscaled in the world. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "StaticMeshBuildSettings") + float DistanceFieldResolutionScale; + + // Whether to generate the distance field treating every triangle hit as a front face. + // When enabled prevents the distance field from being discarded due to the mesh being open, but also lowers Distance Field AO quality. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "StaticMeshBuildSettings", meta = (DisplayName = "Two-Sided Distance Field Generation")) + uint8 bGenerateDistanceFieldAsIfTwoSided : 1; + + // Enable the Physical Material Mask + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "StaticMeshBuildSettings", meta = (DisplayName = "Enable Physical Material Mask")) + uint8 bSupportFaceRemap : 1; + + //------------------------------------------------------------------------------------------------------------- + // PDG Commandlet import + //------------------------------------------------------------------------------------------------------------- + // Is the PDG commandlet enabled? + UPROPERTY(GlobalConfig, EditAnywhere, Category = "PDG Settings", Meta = (DisplayName = "Async Importer Enabled")) + bool bPDGAsyncCommandletImportEnabled; + + + //------------------------------------------------------------------------------------------------------------- + // Houdini Tools Paths + //------------------------------------------------------------------------------------------------------------- + + // Project-specific search paths should be search for HoudiniTools packages, inside of Unreal. + // Each subdirectory will be considered for a HoudiniTools package. + UPROPERTY(GlobalConfig, EditAnywhere, Category = HoudiniTools) + TArray HoudiniToolsSearchPath; + + //------------------------------------------------------------------------------------------------------------- + // Custom Houdini Location + //------------------------------------------------------------------------------------------------------------- + + // Whether to use custom Houdini location. + UPROPERTY(GlobalConfig, EditAnywhere, Category = HoudiniLocation, Meta = (DisplayName = "Use custom Houdini location (requires restart)", ConfigRestartRequired=true)) + bool bUseCustomHoudiniLocation; + + // Custom Houdini location (where HAPI library is located). + UPROPERTY(GlobalConfig, EditAnywhere, Category = HoudiniLocation, Meta = (EditCondition = "bUseCustomHoudiniLocation", DisplayName = "Custom Houdini location", ConfigRestartRequired=true)) + FDirectoryPath CustomHoudiniLocation; + + // Select the Houdini executable to be used when opening session sync or opening hip files + UPROPERTY(GlobalConfig, EditAnywhere, Category = HoudiniLocation, Meta = (DisplayName = "Houdini Executable")) + TEnumAsByte HoudiniExecutable; + + // Custom directory to be used for $HOME + UPROPERTY(GlobalConfig, EditAnywhere, Category = HoudiniLocation, Meta = (DisplayName = "Custom Houdini Home Location")) + FDirectoryPath CustomHoudiniHomeLocation; + + //------------------------------------------------------------------------------------------------------------- + // HAPI_Initialize + //------------------------------------------------------------------------------------------------------------- + // Evaluation thread stack size in bytes. -1 for default + UPROPERTY(GlobalConfig, EditAnywhere, Category = HoudiniEngineInitialization) + int32 CookingThreadStackSize; + + // List of paths to Houdini-compatible .env files (; separated on Windows, : otherwise) + UPROPERTY(GlobalConfig, EditAnywhere, Category = HoudiniEngineInitialization) + FString HoudiniEnvironmentFiles; + + // Path to find other OTL/HDA files + UPROPERTY(GlobalConfig, EditAnywhere, Category = HoudiniEngineInitialization) + FString OtlSearchPath; + + // Sets HOUDINI_DSO_PATH + UPROPERTY(GlobalConfig, EditAnywhere, Category = HoudiniEngineInitialization) + FString DsoSearchPath; + + // Sets HOUDINI_IMAGE_DSO_PATH + UPROPERTY(GlobalConfig, EditAnywhere, Category = HoudiniEngineInitialization) + FString ImageDsoSearchPath; + + // Sets HOUDINI_AUDIO_DSO_PATH + UPROPERTY(GlobalConfig, EditAnywhere, Category = HoudiniEngineInitialization) + FString AudioDsoSearchPath; +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniSplineComponent.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniSplineComponent.h" + +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniAssetComponent.h" +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimeUtils.h" +#include "HoudiniInput.h" +#include "HoudiniInputObject.h" +#include "HoudiniPluginSerializationVersion.h" + +#include "Algo/Reverse.h" +#include "Components/MeshComponent.h" +#include "Runtime/Launch/Resources/Version.h" +#include "Serialization/CustomVersion.h" +#include "UObject/DevObjectVersion.h" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + #include "UObject/Linker.h" +#endif + +void +UHoudiniSplineComponent::Serialize(FArchive& Ar) +{ + int64 InitialOffset = Ar.Tell(); + + bool bLegacyComponent = false; + if (Ar.IsLoading()) + { + int32 Ver = Ar.CustomVer(FHoudiniCustomSerializationVersion::GUID); + if (Ver < VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_V2_BASE && Ver >= VER_HOUDINI_PLUGIN_SERIALIZATION_VERSION_BASE) + { + bLegacyComponent = true; + } + } + + if (bLegacyComponent) + { + // Legacy serialization + HOUDINI_LOG_WARNING(TEXT("Loading deprecated version of UHoudiniSplineComponent : serialization will be skipped.")); + + Super::Serialize(Ar); + + // Skip v1 Serialized data + if (FLinker* Linker = Ar.GetLinker()) + { + int32 const ExportIndex = this->GetLinkerIndex(); + FObjectExport& Export = Linker->ExportMap[ExportIndex]; + Ar.Seek(InitialOffset + Export.SerialSize); + return; + } + } + else + { + // Normal v2 serialization + Super::Serialize(Ar); + } +} + +UHoudiniSplineComponent::UHoudiniSplineComponent(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) + , bClosed(false) + , bReversed(false) + , CurveOrder(2) + , bIsHoudiniSplineVisible(true) + , CurveType(EHoudiniCurveType::Polygon) + , CurveMethod(EHoudiniCurveMethod::CVs) + , CurveBreakpointParameterization(EHoudiniCurveBreakpointParameterization::Uniform) + , bHasChanged(false) + , bNeedsToTriggerUpdate(false) + , bIsInputCurve(false) + , bIsEditableOutputCurve(false) + , NodeId(-1) +{ + + // Add two default points to the curve + FTransform defaultPoint = FTransform::Identity; + + // Set this component to not tick? + // SetComponentTickEnabled(false); + + // Default curve. + CurvePoints.Add(defaultPoint); + DisplayPoints.Add(defaultPoint.GetLocation()); + + defaultPoint.SetTranslation(FVector(200.f, 0.f, 0.f)); + CurvePoints.Add(defaultPoint); + DisplayPoints.Add(defaultPoint.GetLocation()); + + bIsOutputCurve = false; + bCookOnCurveChanged = true; + +#if WITH_EDITOR + bPostUndo = false; +#endif +} + +void UHoudiniSplineComponent::OnSessionConnected() +{ + NodeId = INDEX_NONE; +} + +void +UHoudiniSplineComponent::Construct(TArray& InCurveDisplayPoints, int32 InsertedPoint) +{ + DisplayPoints.Empty(); + DisplayPointIndexDivider.Empty(); + + float DisplayPointStepSize; + + // Resample the display points for linear curve. + + if (InCurveDisplayPoints.Num() <= 0) + return; + + // Add an additional displaypoint to the end for closed curve + if (bClosed && InCurveDisplayPoints.Num() > 2) + { + FVector & FirstPoint = InCurveDisplayPoints[0]; + FVector ClosingPoint; + ClosingPoint.X = FirstPoint.X; + ClosingPoint.Y = FirstPoint.Y; + ClosingPoint.Z = FirstPoint.Z; + + InCurveDisplayPoints.Add(ClosingPoint); + } + + + if (CurveType == EHoudiniCurveType::Polygon) + { + FVector Pt1, Pt2; + Pt1 = InCurveDisplayPoints[0]; + int32 CurrentDisplayPointIndex = 0; + + for (int Index = 1; Index < InCurveDisplayPoints.Num(); ++Index) + { + DisplayPointStepSize = 10.f; + + Pt2 = InCurveDisplayPoints[Index]; + + FVector Direction = Pt2 - Pt1; + + float SegmentLength = Direction.Size(); + + int32 NumOfDisplayPt = SegmentLength / DisplayPointStepSize; + + // Make sure there are at least 20 display points on a segment. + while( NumOfDisplayPt < 20 && SegmentLength > 0.01) + { + DisplayPointStepSize /= 2.f; + NumOfDisplayPt = SegmentLength / DisplayPointStepSize; + } + + Direction.Normalize(0.01f); + + FVector StepVector = Direction * DisplayPointStepSize; + + // Always add the start point of a line segment + FVector NextDisplayPt = Pt1; + if (NumOfDisplayPt == 0) DisplayPoints.Add(NextDisplayPt); + + for (int32 itr = 0; itr < NumOfDisplayPt; ++itr) + { + DisplayPoints.Add(NextDisplayPt); + NextDisplayPt += StepVector; + CurrentDisplayPointIndex += 1; + } + + DisplayPointIndexDivider.Add(CurrentDisplayPointIndex); + + Pt1 = Pt2; + } + + // Add the ending point + DisplayPoints.Add(Pt1); + // Duplicate the last index, to make the DisplaPointyIndexDivider array matches the length of DP array + DisplayPointIndexDivider.Add(CurrentDisplayPointIndex + 1); + } + else if (CurveType == EHoudiniCurveType::Points) + { + // do not add display points for Points curve type, just show the CVs + } + else + { + // Needs a better algorithm to divide the display points + // Refined display points does not strictly interpolate the curve points. + + FVector Pt1, Pt2; + Pt1 = InCurveDisplayPoints[0]; + + int Itr = 1; + + int32 ClosestIndex = -1; + float ClosestDistance = -1.f; + + for (int32 Index = 1; Index < InCurveDisplayPoints.Num(); ++Index) + { + if (Itr >= CurvePoints.Num()) break; + + Pt2 = InCurveDisplayPoints[Index]; + + FVector ClosestPointOnSegment = FMath::ClosestPointOnSegment(CurvePoints[Itr].GetLocation(), Pt1, Pt2); + + float Distance = (ClosestPointOnSegment - CurvePoints[Itr].GetLocation()).Size(); + + if (ClosestDistance < 0.f || Distance < ClosestDistance) + { + ClosestDistance = Distance; + ClosestIndex = Index; + } + else + { + Itr += 1; + if (Itr >= CurvePoints.Num()) break; + + DisplayPointIndexDivider.Add(Index-1); + + ClosestPointOnSegment = FMath::ClosestPointOnSegment(CurvePoints[Itr].GetLocation(), Pt1, Pt2); + ClosestDistance = (ClosestPointOnSegment - CurvePoints[Itr].GetLocation()).Size(); + + } + } + + DisplayPointIndexDivider.Add(InCurveDisplayPoints.Num()); + + DisplayPoints = InCurveDisplayPoints; + } +} + + +void +UHoudiniSplineComponent::CopyHoudiniData(const UHoudiniSplineComponent* OtherHoudiniSplineComponent) +{ + if (!OtherHoudiniSplineComponent) + return; + + CurvePoints = OtherHoudiniSplineComponent->CurvePoints; + DisplayPoints = OtherHoudiniSplineComponent->DisplayPoints; + DisplayPointIndexDivider = OtherHoudiniSplineComponent->DisplayPointIndexDivider; + CurveType = OtherHoudiniSplineComponent->CurveType; + CurveMethod = OtherHoudiniSplineComponent->CurveMethod; + CurveBreakpointParameterization = OtherHoudiniSplineComponent->CurveBreakpointParameterization; + CurveOrder = OtherHoudiniSplineComponent->CurveOrder; + bClosed = OtherHoudiniSplineComponent->bClosed; +#if WITH_EDITORONLY_DATA + bVisualizeComponent = OtherHoudiniSplineComponent->bVisualizeComponent; +#endif + bReversed = OtherHoudiniSplineComponent->bReversed; + SetVisibility(OtherHoudiniSplineComponent->IsVisible()); + + HoudiniSplineName = OtherHoudiniSplineComponent->HoudiniSplineName; + bIsLegacyInputCurve = OtherHoudiniSplineComponent->bIsLegacyInputCurve; +} + +UHoudiniSplineComponent::~UHoudiniSplineComponent() +{} + +void +UHoudiniSplineComponent::AppendPoint(const FTransform& NewPoint) +{ + CurvePoints.Add(NewPoint); +} + +void +UHoudiniSplineComponent::InsertPointAtIndex(const FTransform& NewPoint, const int32& Index) +{ + check(Index >= 0 && Index < CurvePoints.Num()); + CurvePoints.Insert(NewPoint, Index); + bHasChanged = true; +} + + +void +UHoudiniSplineComponent::RemovePointAtIndex(const int32& Index) +{ + check(Index >= 0 && Index < CurvePoints.Num()); + CurvePoints.RemoveAt(Index); + bHasChanged = true; +} + +void +UHoudiniSplineComponent::SetReversed(const bool& InReversed) +{ + // don't need to do anything if the reversed state doesn't change. + if (InReversed == bReversed) + return; + + bReversed = InReversed; + ReverseCurvePoints(); + MarkChanged(true); +} + +void +UHoudiniSplineComponent::ReverseCurvePoints() +{ + if (CurvePoints.Num() < 2) + return; + + Algo::Reverse(CurvePoints); +} + +void +UHoudiniSplineComponent::EditPointAtindex(const FTransform& NewPoint, const int32& Index) +{ + if (!CurvePoints.IsValidIndex(Index)) + return; + + CurvePoints[Index] = NewPoint; + bHasChanged = true; +} + +#if WITH_EDITOR +void +UHoudiniSplineComponent::PostEditChangeProperty(FPropertyChangedEvent& PeopertyChangedEvent) +{ + Super::PostEditChangeProperty(PeopertyChangedEvent); + + FName PropertyName = (PeopertyChangedEvent.Property != nullptr) ? PeopertyChangedEvent.Property->GetFName() : NAME_None; + + // Responses to the uproperty changes + if (PropertyName == GET_MEMBER_NAME_CHECKED(UHoudiniSplineComponent, bClosed)) + { + UE_LOG(LogTemp, Warning, TEXT("UPROPERTY Changed : bClosed")); + MarkChanged(true); + } + + if (PropertyName == GET_MEMBER_NAME_CHECKED(UHoudiniSplineComponent, bReversed)) + { + UE_LOG(LogTemp, Warning, TEXT("UPROPERTY Changed : bReversed")); + ReverseCurvePoints(); + MarkChanged(true); + } + + if (PropertyName == GET_MEMBER_NAME_CHECKED(UHoudiniSplineComponent, CurveType)) + { + UE_LOG(LogTemp, Warning, TEXT("UPROPERTY Changed : CurveType")); + MarkChanged(true); + } + + if (PropertyName == GET_MEMBER_NAME_CHECKED(UHoudiniSplineComponent, CurveMethod)) + { + UE_LOG(LogTemp, Warning, TEXT("UPROPERTY Changed : CurveMethod")); + MarkChanged(true); + } + + if (PropertyName == GET_MEMBER_NAME_CHECKED(UHoudiniSplineComponent, CurveOrder)) + { + UE_LOG(LogTemp, Warning, TEXT("UPROPERTY Changed : CurveOrder")); + MarkChanged(true); + } + + if (PropertyName == GET_MEMBER_NAME_CHECKED(UHoudiniSplineComponent, CurveBreakpointParameterization)) + { + UE_LOG(LogTemp, Warning, TEXT("UPROPERTY Changed : CurveBreakpointParameterization")); + MarkChanged(true); + } +} +#endif + +void +UHoudiniSplineComponent::PostLoad() +{ + Super::PostLoad(); +} + +TStructOnScope +UHoudiniSplineComponent::GetComponentInstanceData() const +{ + TStructOnScope ComponentInstanceData = MakeStructOnScope(this); + FHoudiniSplineComponentInstanceData* InstanceData = ComponentInstanceData.Cast(); + + return ComponentInstanceData; +} + +void +UHoudiniSplineComponent::ApplyComponentInstanceData( + FHoudiniSplineComponentInstanceData* ComponentInstanceData, const bool bPostUCS) +{ + check(ComponentInstanceData); +} + +void +UHoudiniSplineComponent::CopyPropertiesFrom(UObject* FromObject) +{ + // Capture properties that we want to preserve during copy + const int32 PrevNodeId = NodeId; + + UActorComponent* FromComponent = Cast(FromObject); + check(FromComponent); + + UHoudiniSplineComponent* FromSplineComponent = Cast(FromObject); + if (FromSplineComponent) + { + CurvePoints = FromSplineComponent->CurvePoints; + DisplayPoints = FromSplineComponent->DisplayPoints; + DisplayPointIndexDivider = FromSplineComponent->DisplayPointIndexDivider; +#if WITH_EDITORONLY_DATA + EditedControlPointsIndexes = FromSplineComponent->EditedControlPointsIndexes; +#endif + + HoudiniSplineName = FromSplineComponent->HoudiniSplineName; + bClosed = FromSplineComponent->bClosed; + bIsHoudiniSplineVisible = FromSplineComponent->bIsHoudiniSplineVisible; + CurveType = FromSplineComponent->CurveType; + CurveMethod = FromSplineComponent->CurveMethod; + CurveOrder = FromSplineComponent->CurveOrder; + CurveBreakpointParameterization = FromSplineComponent->CurveBreakpointParameterization; + bIsInputCurve = FromSplineComponent->bIsInputCurve; + bIsOutputCurve = FromSplineComponent->bIsOutputCurve; + bIsEditableOutputCurve = FromSplineComponent->bIsEditableOutputCurve; + bCookOnCurveChanged = FromSplineComponent->bCookOnCurveChanged; + + bHasChanged = FromSplineComponent->bHasChanged; + bNeedsToTriggerUpdate = FromSplineComponent->bNeedsToTriggerUpdate; + } + + // Restore properties that we want to preserve + NodeId = PrevNodeId; +} + + +void +UHoudiniSplineComponent::OnUnregister() +{ + Super::OnUnregister(); +} + +void +UHoudiniSplineComponent::OnComponentCreated() +{ + Super::OnComponentCreated(); +} + +void +UHoudiniSplineComponent::OnComponentDestroyed(bool bDestroyingHierarchy) +{ + Super::OnComponentDestroyed(bDestroyingHierarchy); + + if (IsInputCurve()) + { + // This component can't just come out of nowhere and decide to delete an input object! + // We have rules and regulations for this sort of thing. Protocols to follow, forms to fill out, in triplicate! + // The reason we can't delete these nodes here is because when we're using this component in a Blueprint, + // components will go through multiple reconstructions and destructions and in multiple worlds as well, causing + // the nodes in the Houdini session to disappear when we expect them to still be there. + + // InputObject->MarkPendingKill(); + + // if(NodeId > -1) + // FHoudiniEngineRuntime::Get().MarkNodeIdAsPendingDelete(NodeId); + + SetNodeId(-1); // Set nodeId to invalid for reconstruct on re-do + } +} + + +#if WITH_EDITOR +void +UHoudiniSplineComponent::PostEditUndo() +{ + Super::PostEditUndo(); + + bPostUndo = true; + + MarkChanged(true); +} +#endif + +void +UHoudiniSplineComponent::SetOffset(const float& Offset) +{ + for (int n = 0; n < CurvePoints.Num(); ++n) + CurvePoints[n].AddToTranslation(FVector(0.f, Offset, 0.f)); + + for (int n = 0; n < DisplayPoints.Num(); ++n) + DisplayPoints[n] += FVector(0.f, Offset, 0.f); +} + +void +UHoudiniSplineComponent::ResetCurvePoints() +{ + CurvePoints.Empty(); +} + +void +UHoudiniSplineComponent::ResetDisplayPoints() +{ + DisplayPoints.Empty(); +} + +void +UHoudiniSplineComponent::AddCurvePoints(const TArray& Points) +{ + CurvePoints.Append(Points); +} + +void +UHoudiniSplineComponent::AddDisplayPoints(const TArray& Points) +{ + DisplayPoints.Append(Points); +} + +bool +UHoudiniSplineComponent::NeedsToTriggerUpdate() const +{ + return bNeedsToTriggerUpdate; +} + +void UHoudiniSplineComponent::SetNeedsToTriggerUpdate(const bool& NeedsToTriggerUpdate) +{ + bNeedsToTriggerUpdate = NeedsToTriggerUpdate; +} + +void UHoudiniSplineComponent::SetCurveType(const EHoudiniCurveType & NewCurveType) +{ + CurveType = NewCurveType; +} + +bool UHoudiniSplineComponent::HasChanged() const +{ + return bHasChanged; +} + +void UHoudiniSplineComponent::MarkChanged(const bool& Changed) +{ + bHasChanged = Changed; + bNeedsToTriggerUpdate = Changed; +} + +void UHoudiniSplineComponent::MarkInputNodesAsPendingKill() +{ + // InputObject->MarkPendingKill(); + if(NodeId > -1) + FHoudiniEngineRuntime::Get().MarkNodeIdAsPendingDelete(NodeId); + + SetNodeId(-1); // Set nodeId to invalid for reconstruct on re-do +} + +FHoudiniSplineComponentInstanceData::FHoudiniSplineComponentInstanceData() +{ +} + +FHoudiniSplineComponentInstanceData::FHoudiniSplineComponentInstanceData(const UHoudiniSplineComponent* SourceComponent) + : FActorComponentInstanceData(SourceComponent) +{ +} + + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniSplineComponent.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" + +#include "HoudiniEngineCopyPropertiesInterface.h" +#include "UObject/ObjectMacros.h" +#include "Components/SceneComponent.h" +#include "HoudiniGeoPartObject.h" + +#include "HoudiniSplineComponent.generated.h" + +class UHoudiniAssetComponent; +enum class EHoudiniCurveType : int8; +enum class EHoudiniCurveMethod : int8; +class UHoudiniInputObject; + +UCLASS(Blueprintable, BlueprintType, EditInlineNew, config = Engine, meta = (BlueprintSpawnableComponent)) +class HOUDINIENGINERUNTIME_API UHoudiniSplineComponent : public USceneComponent, public IHoudiniEngineCopyPropertiesInterface +{ + GENERATED_UCLASS_BODY() + + friend class UHoudiniSplineComponent_V1; + + friend class FHoudiniEditorEquivalenceUtils; + + virtual ~UHoudiniSplineComponent(); + + virtual void Serialize(FArchive & Ar) override; + + public: + + void Construct(TArray& InCurveDisplayPoints, int32 InsertedPoint = -1); + + void OnSessionConnected(); + + void CopyHoudiniData(const UHoudiniSplineComponent* OtherHoudiniSplineComponent); + + void ResetCurvePoints(); + + void ResetDisplayPoints(); + + void AddCurvePoints(const TArray& Points); + + void AddDisplayPoints(const TArray& Points); + + void AppendPoint(const FTransform& NewPoint); + + void InsertPointAtIndex(const FTransform& NewPoint, const int32& Index); + + void RemovePointAtIndex(const int32& Index); + + void EditPointAtindex(const FTransform& NewPoint, const int32& Index); + + void MarkModified(const bool & InModified) { bHasChanged = InModified; }; + + // To set the offset of default position of houdini curve + void SetOffset(const float& Offset); + + bool HasChanged() const; + + void MarkChanged(const bool& Changed); + + // Mark the associated Houdini nodes for pending kill + void MarkInputNodesAsPendingKill(); + + FORCEINLINE + FString& GetHoudiniSplineName() { return HoudiniSplineName; } + + FORCEINLINE + void SetHoudiniSplineName(const FString& NewName) { HoudiniSplineName = NewName; } + + bool NeedsToTriggerUpdate() const; + + void SetNeedsToTriggerUpdate(const bool& NeedsToTriggerUpdate); + + FORCEINLINE + EHoudiniCurveType GetCurveType() const { return CurveType; } + + void SetCurveType(const EHoudiniCurveType& NewCurveType); + + FORCEINLINE + EHoudiniCurveMethod GetCurveMethod() const { return CurveMethod; } + + FORCEINLINE + void SetCurveMethod(const EHoudiniCurveMethod& NewCurveMethod) { CurveMethod = NewCurveMethod; } + + FORCEINLINE + EHoudiniCurveBreakpointParameterization GetCurveBreakpointParameterization() const { return CurveBreakpointParameterization; } + + FORCEINLINE + void SetCurveBreakpointParameterization(const EHoudiniCurveBreakpointParameterization& NewBreakpointMethod) { CurveBreakpointParameterization = NewBreakpointMethod; } + + FORCEINLINE + int32 GetCurvePointCount() const { return CurvePoints.Num(); } + + FORCEINLINE + bool IsClosedCurve() const { return bClosed; } + + FORCEINLINE + void SetClosedCurve(const bool& Closed) { bClosed = Closed; } + + FORCEINLINE + bool IsReversed() const { return bReversed; } + + void SetReversed(const bool& Reversed); + + FORCEINLINE + int32 GetCurveOrder() const { return CurveOrder; } + + void SetCurveOrder(const int32& Order) { CurveOrder = Order; } + + FORCEINLINE + bool IsInputCurve() const { return bIsInputCurve; } + + FORCEINLINE + void SetIsInputCurve(const bool& bIsInput) { bIsInputCurve = bIsInput; } + + FORCEINLINE + bool IsEditableOutputCurve() const { return bIsEditableOutputCurve; } + + FORCEINLINE + void SetIsEditableOutputCurve(const bool& bInIsEditable) { bIsEditableOutputCurve = bInIsEditable; }; + + FORCEINLINE + int32 GetNodeId() const { return NodeId; } + + FORCEINLINE + void SetNodeId(const int32& NewNodeId) { NodeId = NewNodeId; } + + FORCEINLINE + FString GetGeoPartName() const { return PartName; } + + FORCEINLINE + bool IsHoudiniSplineVisible() const { return bIsHoudiniSplineVisible; } + + FORCEINLINE + void SetHoudiniSplineVisible(bool Visible) { bIsHoudiniSplineVisible = Visible; } + + FORCEINLINE + void SetGeoPartName(const FString & InPartName) { PartName = InPartName; } + + FORCEINLINE + bool IsLegacyInputCurve() const { return bIsLegacyInputCurve; } + + FORCEINLINE + void SetIsLegacyInputCurve(const bool IsLegacyInputCurve) { bIsLegacyInputCurve = IsLegacyInputCurve; } + + virtual void OnUnregister() override; + + virtual void OnComponentCreated() override; + + virtual void OnComponentDestroyed(bool bDestroyingHierarchy) override; + +#if WITH_EDITOR + virtual void PostEditUndo() override; + virtual void PostEditChangeProperty(FPropertyChangedEvent & PeopertyChangedEvent) override; +#endif + + virtual void PostLoad() override; + + virtual TStructOnScope GetComponentInstanceData() const override; + void ApplyComponentInstanceData(struct FHoudiniSplineComponentInstanceData* ComponentInstanceData, const bool bPostUCS); + + virtual void CopyPropertiesFrom(UObject* FromObject) override; + + private: + + void ReverseCurvePoints(); + + public: + + UPROPERTY() + TArray CurvePoints; + + UPROPERTY() + TArray DisplayPoints; + + UPROPERTY() + TArray DisplayPointIndexDivider; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Houdini Spline Properties") + FString HoudiniSplineName; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Houdini Spline Properties") + bool bClosed; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Houdini Spline Properties") + bool bReversed; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Houdini Spline Properties") + int32 CurveOrder; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Houdini Spline Properties") + bool bIsHoudiniSplineVisible; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Houdini Spline Properties") + EHoudiniCurveType CurveType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Houdini Spline Properties") + EHoudiniCurveMethod CurveMethod; + + // Only used for new HAPI curve / breakpoints + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Houdini Spline Properties") + EHoudiniCurveBreakpointParameterization CurveBreakpointParameterization; + + UPROPERTY() + bool bIsOutputCurve; + + UPROPERTY() + bool bCookOnCurveChanged; + + UPROPERTY() + bool bIsLegacyInputCurve; + + +#if WITH_EDITORONLY_DATA + UPROPERTY() + TArray EditedControlPointsIndexes; + + UPROPERTY(NonTransactional) + bool bPostUndo; +#endif + + protected: + // Corresponding geo part object. + FHoudiniGeoPartObject HoudiniGeoPartObject; + + private: + UPROPERTY(Transient, DuplicateTransient) + bool bHasChanged; + + UPROPERTY(Transient, DuplicateTransient) + bool bNeedsToTriggerUpdate; + + // Whether this is a Houdini curve input + UPROPERTY() + bool bIsInputCurve; + + UPROPERTY() + bool bIsEditableOutputCurve; + + // Corresponds to the Curve NodeId in Houdini + UPROPERTY(Transient, DuplicateTransient) + int32 NodeId; + + UPROPERTY() + FString PartName; +}; + +// Used to store HoudiniAssetComponent data during BP reconstruction +USTRUCT() +struct FHoudiniSplineComponentInstanceData : public FActorComponentInstanceData +{ + GENERATED_BODY() +public: + + FHoudiniSplineComponentInstanceData(); + FHoudiniSplineComponentInstanceData(const UHoudiniSplineComponent* SourceComponent); + + virtual ~FHoudiniSplineComponentInstanceData() = default; + + virtual void ApplyToComponent(UActorComponent* Component, const ECacheApplyPhase CacheApplyPhase) override + { + Super::ApplyToComponent(Component, CacheApplyPhase); + CastChecked(Component)->ApplyComponentInstanceData(this, (CacheApplyPhase == ECacheApplyPhase::PostUserConstructionScript)); + } + + UPROPERTY() + TArray CurvePoints; + + UPROPERTY() + TArray DisplayPoints; + + UPROPERTY() + TArray DisplayPointIndexDivider; + +#if WITH_EDITORONLY_DATA + UPROPERTY() + TArray EditedControlPointsIndexes; +#endif + +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniStaticMesh.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniStaticMesh.h" +#include "HoudiniEngineRuntimePrivatePCH.h" + +#include "Async/ParallelFor.h" +#include "MeshUtilitiesCommon.h" + +UHoudiniStaticMesh::UHoudiniStaticMesh(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + bHasNormals = false; + bHasTangents = false; + bHasColors = false; + NumUVLayers = 0; + bHasPerFaceMaterials = false; +} + +void +UHoudiniStaticMesh::Initialize( + uint32 InNumVertices, + uint32 InNumTriangles, + uint32 InNumUVLayers, + uint32 InInitialNumStaticMaterials, + bool bInHasNormals, + bool bInHasTangents, + bool bInHasColors, + bool bInHasPerFaceMaterials) +{ + // Initialize the vertex positions and triangle indices arrays + VertexPositions.SetNumUninitialized(InNumVertices); + for(int32 n = 0; n < VertexPositions.Num(); n++) + VertexPositions[n] = FVector3f::ZeroVector; + + TriangleIndices.SetNumUninitialized(InNumTriangles); + for (int32 n = 0; n < TriangleIndices.Num(); n++) + TriangleIndices[n] = FIntVector(-1, -1, -1); + + if (InInitialNumStaticMaterials > 0) + { + StaticMaterials.SetNumUninitialized(InNumTriangles); + for (int32 n = 0; n < StaticMaterials.Num(); n++) + StaticMaterials[n] = FStaticMaterial(); + } + else + StaticMaterials.Empty(); + + SetNumUVLayers(InNumUVLayers); + SetHasNormals(bInHasNormals); + SetHasTangents(bInHasTangents); + SetHasColors(bInHasColors); + SetHasPerFaceMaterials(bInHasPerFaceMaterials); +} + +void +UHoudiniStaticMesh::SetHasPerFaceMaterials(bool bInHasPerFaceMaterials) +{ + bHasPerFaceMaterials = bInHasPerFaceMaterials; + if (bHasPerFaceMaterials) + { + MaterialIDsPerTriangle.SetNumUninitialized(GetNumTriangles()); + for (int32 n = 0; n < MaterialIDsPerTriangle.Num(); n++) + MaterialIDsPerTriangle[n] = -1; + } + else + MaterialIDsPerTriangle.Empty(); +} + +void +UHoudiniStaticMesh::SetHasNormals(bool bInHasNormals) +{ + bHasNormals = bInHasNormals; + if (bHasNormals) + { + VertexInstanceNormals.SetNumUninitialized(GetNumVertexInstances()); + for (int32 n = 0; n < VertexInstanceNormals.Num(); n++) + VertexInstanceNormals[n] = FVector3f(0, 0, 1); + } + else + VertexInstanceNormals.Empty(); +} + +void +UHoudiniStaticMesh::SetHasTangents(bool bInHasTangents) +{ + bHasTangents = bInHasTangents; + if (bHasTangents) + { + VertexInstanceUTangents.SetNumUninitialized(GetNumVertexInstances()); + for (int32 n = 0; n < VertexInstanceUTangents.Num(); n++) + VertexInstanceUTangents[n] = FVector3f(1, 0, 0); + + VertexInstanceVTangents.SetNumUninitialized(GetNumVertexInstances()); + for (int32 n = 0; n < VertexInstanceVTangents.Num(); n++) + VertexInstanceVTangents[n] = FVector3f(0, 1, 0); + } + else + { + VertexInstanceUTangents.Empty(); + VertexInstanceVTangents.Empty(); + } +} + +void +UHoudiniStaticMesh::SetHasColors(bool bInHasColors) +{ + bHasColors = bInHasColors; + if (bHasColors) + { + VertexInstanceColors.SetNumUninitialized(GetNumVertexInstances()); + for (int32 n = 0; n < VertexInstanceColors.Num(); n++) + VertexInstanceColors[n] = FColor(127, 127, 127); + } + else + VertexInstanceColors.Empty(); +} + +void UHoudiniStaticMesh::SetNumUVLayers(uint32 InNumUVLayers) +{ + NumUVLayers = InNumUVLayers; + if (NumUVLayers > 0) + { + VertexInstanceUVs.SetNumUninitialized(GetNumVertexInstances() * NumUVLayers); + for (int32 n = 0; n < VertexInstanceUVs.Num(); n++) + VertexInstanceUVs[n] = FVector2f::ZeroVector; + } + else + VertexInstanceUVs.Empty(); +} + +void UHoudiniStaticMesh::SetNumStaticMaterials(uint32 InNumMaterials) +{ + if (InNumMaterials > 0) + StaticMaterials.SetNum(InNumMaterials); + else + StaticMaterials.Empty(); +} + +void UHoudiniStaticMesh::SetVertexPosition(uint32 InVertexIndex, const FVector3f& InPosition) +{ + check(VertexPositions.IsValidIndex(InVertexIndex)); + + VertexPositions[InVertexIndex] = InPosition; +} + +void UHoudiniStaticMesh::SetTriangleVertexIndices(uint32 InTriangleIndex, const FIntVector& InTriangleVertexIndices) +{ + check(TriangleIndices.IsValidIndex(InTriangleIndex)); + check(VertexPositions.IsValidIndex(InTriangleVertexIndices[0])); + check(VertexPositions.IsValidIndex(InTriangleVertexIndices[1])); + check(VertexPositions.IsValidIndex(InTriangleVertexIndices[2])); + + TriangleIndices[InTriangleIndex] = InTriangleVertexIndices; +} + +void UHoudiniStaticMesh::SetTriangleVertexNormal(uint32 InTriangleIndex, uint8 InTriangleVertexIndex, const FVector3f& InNormal) +{ + if (!bHasNormals) + { + return; + } + + check(TriangleIndices.IsValidIndex(InTriangleIndex)); + const uint32 VertexInstanceIndex = InTriangleIndex * 3 + InTriangleVertexIndex; + check(VertexInstanceNormals.IsValidIndex(VertexInstanceIndex)); + + VertexInstanceNormals[VertexInstanceIndex] = InNormal; +} + +void UHoudiniStaticMesh::SetTriangleVertexUTangent(uint32 InTriangleIndex, uint8 InTriangleVertexIndex, const FVector3f& InUTangent) +{ + if (!bHasTangents) + { + return; + } + + check(TriangleIndices.IsValidIndex(InTriangleIndex)); + const uint32 VertexInstanceIndex = InTriangleIndex * 3 + InTriangleVertexIndex; + check(VertexInstanceUTangents.IsValidIndex(VertexInstanceIndex)); + + VertexInstanceUTangents[VertexInstanceIndex] = InUTangent; +} + +void UHoudiniStaticMesh::SetTriangleVertexVTangent(uint32 InTriangleIndex, uint8 InTriangleVertexIndex, const FVector3f& InVTangent) +{ + if (!bHasTangents) + { + return; + } + + check(TriangleIndices.IsValidIndex(InTriangleIndex)); + const uint32 VertexInstanceIndex = InTriangleIndex * 3 + InTriangleVertexIndex; + check(VertexInstanceVTangents.IsValidIndex(VertexInstanceIndex)); + + VertexInstanceVTangents[VertexInstanceIndex] = InVTangent; +} + +void UHoudiniStaticMesh::SetTriangleVertexColor(uint32 InTriangleIndex, uint8 InTriangleVertexIndex, const FColor& InColor) +{ + if (!bHasColors) + { + return; + } + + check(TriangleIndices.IsValidIndex(InTriangleIndex)); + const uint32 VertexInstanceIndex = InTriangleIndex * 3 + InTriangleVertexIndex; + check(VertexInstanceColors.IsValidIndex(VertexInstanceIndex)); + + // Convert to linear + FLinearColor LinearColor = InColor.ReinterpretAsLinear(); + FColor Color = LinearColor.ToFColor(true); + + // Store + VertexInstanceColors[VertexInstanceIndex].R = Color.R; + VertexInstanceColors[VertexInstanceIndex].G = Color.G; + VertexInstanceColors[VertexInstanceIndex].B = Color.B; + VertexInstanceColors[VertexInstanceIndex].A = Color.A; +} + +void UHoudiniStaticMesh::SetTriangleVertexUV(uint32 InTriangleIndex, uint8 InTriangleVertexIndex, uint8 InUVLayer, const FVector2f& InUV) +{ + if (NumUVLayers <= 0) + { + return; + } + + check(TriangleIndices.IsValidIndex(InTriangleIndex)); + const uint32 VertexInstanceUVIndex = InUVLayer * GetNumVertexInstances() + InTriangleIndex * 3 + InTriangleVertexIndex; + check(VertexInstanceUVs.IsValidIndex(VertexInstanceUVIndex)); + + VertexInstanceUVs[VertexInstanceUVIndex] = InUV; +} + +void UHoudiniStaticMesh::SetTriangleMaterialID(uint32 InTriangleIndex, int32 InMaterialID) +{ + if (!bHasPerFaceMaterials) + { + return; + } + + check(TriangleIndices.IsValidIndex(InTriangleIndex)); + check(MaterialIDsPerTriangle.IsValidIndex(InTriangleIndex)); + + MaterialIDsPerTriangle[InTriangleIndex] = InMaterialID; +} + +void UHoudiniStaticMesh::SetStaticMaterial(uint32 InMaterialIndex, const FStaticMaterial& InStaticMaterial) +{ + check(StaticMaterials.IsValidIndex(InMaterialIndex)); + StaticMaterials[InMaterialIndex] = InStaticMaterial; +} + +void UHoudiniStaticMesh::CalculateNormals(bool bInComputeWeightedNormals) +{ + const int32 NumVertexInstances = GetNumVertexInstances(); + + // Pre-allocate space in the vertex instance normals array + VertexInstanceNormals.SetNum(NumVertexInstances); + + const int32 NumTriangles = GetNumTriangles(); + const int32 NumVertices = GetNumVertices(); + + // Setup a vertex normal array + TArray VertexNormals; + VertexNormals.SetNum(NumVertices); + + // Zero all entries in VertexNormals + // for (int32 VertexIndex = 0; VertexIndex < NumVertices; ++VertexIndex) + ParallelFor(NumVertices, [&VertexNormals](int32 VertexIndex) + { + VertexNormals[VertexIndex] = FVector3f::ZeroVector; + }); + + // Calculate face normals and sum them for each vertex that shares the triangle + // for (int32 TriangleIndex = 0; TriangleIndex < NumTriangles; ++TriangleIndex) + ParallelFor(NumTriangles, [this, &VertexNormals, bInComputeWeightedNormals](int32 TriangleIndex) + { + const FIntVector& TriangleVertexIndices = TriangleIndices[TriangleIndex]; + + if (!VertexPositions.IsValidIndex(TriangleVertexIndices[0]) || + !VertexPositions.IsValidIndex(TriangleVertexIndices[1]) || + !VertexPositions.IsValidIndex(TriangleVertexIndices[2])) + { + HOUDINI_LOG_WARNING( + TEXT("[UHoudiniStaticMesh::CalculateNormals]: VertexPositions index out of range %d, %d, %d, Num %d"), + TriangleVertexIndices[0], TriangleVertexIndices[1], TriangleVertexIndices[2], VertexPositions.Num()); + return; + } + + const FVector3f& V0 = VertexPositions[TriangleVertexIndices[0]]; + const FVector3f& V1 = VertexPositions[TriangleVertexIndices[1]]; + const FVector3f& V2 = VertexPositions[TriangleVertexIndices[2]]; + + FVector3f TriangleNormal = FVector3f::CrossProduct(V2 - V0, V1 - V0); + float Area = TriangleNormal.Size(); + TriangleNormal /= Area; + Area /= 2.0f; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + const float Weight[3] = { + static_cast(bInComputeWeightedNormals ? Area * TriangleUtilities::ComputeTriangleCornerAngle(V0, V1, V2) : 1.0f), + static_cast(bInComputeWeightedNormals ? Area * TriangleUtilities::ComputeTriangleCornerAngle(V1, V2, V0) : 1.0f), + static_cast(bInComputeWeightedNormals ? Area * TriangleUtilities::ComputeTriangleCornerAngle(V2, V0, V1) : 1.0f), + }; +#else + const float Weight[3] = { + static_cast(bInComputeWeightedNormals ? Area * TriangleUtilities::ComputeTriangleCornerAngle((FVector)V0, (FVector)V1, (FVector)V2) : 1.0f), + static_cast(bInComputeWeightedNormals ? Area * TriangleUtilities::ComputeTriangleCornerAngle((FVector)V1, (FVector)V2, (FVector)V0) : 1.0f), + static_cast(bInComputeWeightedNormals ? Area * TriangleUtilities::ComputeTriangleCornerAngle((FVector)V2, (FVector)V0, (FVector)V1) : 1.0f), + }; +#endif + + for (int CornerIndex = 0; CornerIndex < 3; ++CornerIndex) + { + const FVector3f WeightedNormal = TriangleNormal * Weight[CornerIndex]; + if (!WeightedNormal.IsNearlyZero(SMALL_NUMBER) && !WeightedNormal.ContainsNaN()) + { + if (!VertexNormals.IsValidIndex(TriangleVertexIndices[CornerIndex])) + { + HOUDINI_LOG_WARNING( + TEXT("[UHoudiniStaticMesh::CalculateNormals]: VertexNormal index out of range %d, Num %d"), + TriangleVertexIndices[CornerIndex], VertexNormals.Num()); + continue; + } + VertexNormals[TriangleVertexIndices[CornerIndex]] += WeightedNormal; + } + } + }); + + // Normalize the vertex normals + // for (int32 VertexIndex = 0; VertexIndex < NumVertices; ++VertexIndex) + ParallelFor(NumVertices, [&VertexNormals](int32 VertexIndex) + { + VertexNormals[VertexIndex].Normalize(); + }); + + // Copy vertex normals to vertex instance normals + // for (int32 VertexInstanceIndex = 0; VertexInstanceIndex < NumVertexInstances; ++VertexInstanceIndex) + ParallelFor(NumVertexInstances, [this, &VertexNormals](int32 VertexInstanceIndex) + { + const int32 TriangleIndex = VertexInstanceIndex / 3; + const int32 CornerIndex = VertexInstanceIndex % 3; + const FIntVector& TriangleVertexIndices = TriangleIndices[TriangleIndex]; + if (!VertexNormals.IsValidIndex(TriangleVertexIndices[CornerIndex])) + { + HOUDINI_LOG_WARNING( + TEXT("[UHoudiniStaticMesh::CalculateNormals]: VertexNormals index out of range %d, Num %d"), + TriangleVertexIndices[CornerIndex], VertexNormals.Num()); + return; + } + VertexInstanceNormals[VertexInstanceIndex] = VertexNormals[TriangleVertexIndices[CornerIndex]]; + }); + + bHasNormals = true; +} + +void UHoudiniStaticMesh::CalculateTangents(bool bInComputeWeightedNormals) +{ + const int32 NumVertexInstances = GetNumVertexInstances(); + + VertexInstanceUTangents.SetNum(NumVertexInstances); + VertexInstanceVTangents.SetNum(NumVertexInstances); + + // Calculate normals first if we don't have any + if (!HasNormals() || VertexInstanceNormals.Num() != NumVertexInstances) + CalculateNormals(bInComputeWeightedNormals); + + // for (int32 VertexInstanceIndex = 0; VertexInstanceIndex < NumVertexInstances; ++VertexInstanceIndex) + ParallelFor(NumVertexInstances, [this](int32 VertexInstanceIndex) + { + const FVector3f& Normal = VertexInstanceNormals[VertexInstanceIndex]; + Normal.FindBestAxisVectors( + VertexInstanceUTangents[VertexInstanceIndex], VertexInstanceVTangents[VertexInstanceIndex]); + }); + + bHasTangents = true; +} + +void UHoudiniStaticMesh::Optimize() +{ + VertexPositions.Shrink(); + TriangleIndices.Shrink(); + VertexInstanceColors.Shrink(); + VertexInstanceNormals.Shrink(); + VertexInstanceUTangents.Shrink(); + VertexInstanceVTangents.Shrink(); + VertexInstanceUVs.Shrink(); + MaterialIDsPerTriangle.Shrink(); + StaticMaterials.Shrink(); +} + +FBox UHoudiniStaticMesh::CalcBounds() const +{ + const uint32 NumVertices = VertexPositions.Num(); + + if (NumVertices == 0) + return FBox(); + + const FVector3f InitPosition = VertexPositions[0]; + double MinX = InitPosition.X, MaxX = InitPosition.X, MinY = InitPosition.Y, MaxY = InitPosition.Y, MinZ = InitPosition.Z, MaxZ = InitPosition.Z; + for (uint32 VertIdx = 0; VertIdx < NumVertices; ++VertIdx) + { + const FVector3f Position = VertexPositions[VertIdx]; + if (Position.X < MinX) MinX = Position.X; else if (Position.X > MaxX) MaxX = Position.X; + if (Position.Y < MinY) MinY = Position.Y; else if (Position.Y > MaxY) MaxY = Position.Y; + if (Position.Z < MinZ) MinZ = Position.Z; else if (Position.Z > MaxZ) MaxZ = Position.Z; + } + + return FBox(FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)); +} + +UMaterialInterface* UHoudiniStaticMesh::GetMaterial(int32 InMaterialIndex) +{ + check(StaticMaterials.IsValidIndex(InMaterialIndex)); + + return StaticMaterials[InMaterialIndex].MaterialInterface; +} + +int32 UHoudiniStaticMesh::GetMaterialIndex(FName InMaterialSlotName) const +{ + if (InMaterialSlotName == NAME_None) + return -1; + + const uint32 NumMaterials = StaticMaterials.Num(); + for (uint32 MaterialIndex = 0; MaterialIndex < NumMaterials; ++MaterialIndex) + { + if (StaticMaterials[MaterialIndex].MaterialSlotName == InMaterialSlotName) + return (int32)MaterialIndex; + } + + return -1; +} + +bool UHoudiniStaticMesh::IsValid(bool bInSkipVertexIndicesCheck) const +{ + // Validate the number of vertices, indices and triangles. This is basically the same function as FRawMesh::IsValid() + const int32 NumVertices = GetNumVertices(); + const int32 NumVertexInstances = GetNumVertexInstances(); + const int32 NumTriangles = GetNumTriangles(); + + auto ValidateAttributeArraySize = [](int32 InArrayNum, int32 InExpectedSize) + { + return InArrayNum == 0 || InArrayNum == InExpectedSize; + }; + + bool bValid = NumVertices > 0 + && NumVertexInstances > 0 + && NumTriangles > 0 + && (NumVertexInstances / 3) == NumTriangles + && ValidateAttributeArraySize(MaterialIDsPerTriangle.Num(), NumTriangles) + && ValidateAttributeArraySize(VertexInstanceNormals.Num(), NumVertexInstances) + && ValidateAttributeArraySize(VertexInstanceUTangents.Num(), NumVertexInstances) + && ValidateAttributeArraySize(VertexInstanceVTangents.Num(), NumVertexInstances) + && ValidateAttributeArraySize(VertexInstanceColors.Num(), NumVertexInstances) + && NumUVLayers >= 0 + && VertexInstanceUVs.Num() == NumUVLayers * NumVertexInstances; + + if (!bInSkipVertexIndicesCheck) + { + int32 TriangleIndex = 0; + while (bValid && TriangleIndex < NumTriangles) + { + bValid = bValid && (TriangleIndices[TriangleIndex].X < NumVertices); + bValid = bValid && (TriangleIndices[TriangleIndex].Y < NumVertices); + bValid = bValid && (TriangleIndices[TriangleIndex].Z < NumVertices); + TriangleIndex++; + } + } + + return bValid; +} + +void UHoudiniStaticMesh::Serialize(FArchive &InArchive) +{ + Super::Serialize(InArchive); + + VertexPositions.Shrink(); + VertexPositions.BulkSerialize(InArchive); + + TriangleIndices.Shrink(); + TriangleIndices.BulkSerialize(InArchive); + + VertexInstanceColors.Shrink(); + VertexInstanceColors.BulkSerialize(InArchive); + + VertexInstanceNormals.Shrink(); + VertexInstanceNormals.BulkSerialize(InArchive); + + VertexInstanceUTangents.Shrink(); + VertexInstanceUTangents.BulkSerialize(InArchive); + + VertexInstanceVTangents.Shrink(); + VertexInstanceVTangents.BulkSerialize(InArchive); + + VertexInstanceUVs.Shrink(); + VertexInstanceUVs.BulkSerialize(InArchive); + + MaterialIDsPerTriangle.Shrink(); + MaterialIDsPerTriangle.BulkSerialize(InArchive); +} + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniStaticMesh.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/StaticMesh.h" + +#include "HoudiniStaticMesh.generated.h" + +/** + * This is a simple static mesh that is meant to be built in one go, without modifications afterwards. + * The number of vertices and triangles must be known before hand. + */ +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniStaticMesh : public UObject +{ + GENERATED_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + + UHoudiniStaticMesh(const FObjectInitializer &ObjectInitializer); + + // Clears all existing data and initializes internal arrays to the relevant sizes to accommodate the + // mesh based InNumVertices, InNumTriangles, UVs etc. + UFUNCTION() + void Initialize(uint32 InNumVertices, uint32 InNumTriangles, uint32 InNumUVLayers, uint32 InInitialNumStaticMaterials, bool bInHasNormals, bool bInHasTangents, bool bInHasColors, bool bInHasPerFaceMaterials); + + UFUNCTION() + bool HasPerFaceMaterials() const { return bHasPerFaceMaterials; } + + UFUNCTION() + void SetHasPerFaceMaterials(bool bInHasPerFaceMaterials); + + UFUNCTION() + bool HasNormals() const { return bHasNormals; } + + UFUNCTION() + void SetHasNormals(bool bInHasNormals); + + UFUNCTION() + bool HasTangents() const { return bHasTangents; } + + UFUNCTION() + void SetHasTangents(bool bInHasTangents); + + UFUNCTION() + bool HasColors() const { return bHasColors; } + + UFUNCTION() + void SetHasColors(bool bInHasColors); + + UFUNCTION() + uint32 GetNumUVLayers() const { return NumUVLayers; } + + UFUNCTION() + void SetNumUVLayers(uint32 InNumUVLayers); + + UFUNCTION() + uint32 GetNumStaticMaterials() const { return StaticMaterials.Num(); } + + UFUNCTION() + void SetNumStaticMaterials(uint32 InNumStaticMaterials); + + UFUNCTION() + uint32 GetNumVertices() const { return VertexPositions.Num(); } + + UFUNCTION() + uint32 GetNumTriangles() const { return TriangleIndices.Num(); } + + UFUNCTION() + uint32 GetNumVertexInstances() const { return TriangleIndices.Num() * 3; } + + UFUNCTION() + void SetVertexPosition(uint32 InVertexIndex, const FVector3f& InPosition); + + UFUNCTION() + void SetTriangleVertexIndices(uint32 InTriangleIndex, const FIntVector& InTriangleVertexIndices); + + UFUNCTION() + void SetTriangleVertexNormal(uint32 InTriangleIndex, uint8 InTriangleVertexIndex, const FVector3f& InNormal); + + UFUNCTION() + void SetTriangleVertexUTangent(uint32 InTriangleIndex, uint8 InTriangleVertexIndex, const FVector3f& InUTangent); + + UFUNCTION() + void SetTriangleVertexVTangent(uint32 InTriangleIndex, uint8 InTriangleVertexIndex, const FVector3f& InVTangent); + + UFUNCTION() + void SetTriangleVertexColor(uint32 InTriangleIndex, uint8 InTriangleVertexIndex, const FColor& InColor); + + UFUNCTION() + void SetTriangleVertexUV(uint32 InTriangleIndex, uint8 InTriangleVertexIndex, uint8 InUVLayer, const FVector2f& InUV); + + UFUNCTION() + void SetTriangleMaterialID(uint32 InTriangleIndex, int32 InMaterialID); + + UFUNCTION() + void SetStaticMaterial(uint32 InMaterialIndex, const FStaticMaterial& InStaticMaterial); + + UFUNCTION() + uint32 AddStaticMaterial(const FStaticMaterial& InStaticMaterial) { return StaticMaterials.Add(InStaticMaterial); } + + /** Calculate the normals of the mesh by calculating the face normal of each triangle (if a triangle has vertices + * V0, V1, V2, get the vector perpendicular to the face Pf = (V2 - V0) x (V1 - V0). To calculate the + * vertex normal for V0 sum and then normalize all its shared face normals. If bInComputeWeightedNormals is true + * then the weight of each face normal that contributes to V0's normal is the area of the face multiplied by the V0 + * corner angle of that face. If bInComputeWeightedNormals is false then the weight is 1. + * + * @param bInComputeWeightedNormals Whether or not to use weighted normal calculation. Defaults to false. + */ + UFUNCTION() + void CalculateNormals(bool bInComputeWeightedNormals=false); + + /** + * Calculate tangents from the normals. Calculates normals first via CalculateNormals() if the mesh does not yet + * have normals. + * + * @param bInComputeWeightedNormals Whether or not to use weighted normal calculation if CalculateNormals() is + * called. Defaults to false. + */ + UFUNCTION() + void CalculateTangents(bool bInComputeWeightedNormals=false); + + /** + * Meant to be called after the mesh data arrays are populated. + * Currently only calls Shrink on the arrays + */ + UFUNCTION() + void Optimize(); + + UFUNCTION() + FBox CalcBounds() const; + + UFUNCTION() + const TArray& GetVertexPositions() const { return VertexPositions; } + + UFUNCTION() + const TArray& GetTriangleIndices() const { return TriangleIndices; } + + UFUNCTION() + const TArray& GetVertexInstanceColors() const { return VertexInstanceColors; } + + UFUNCTION() + const TArray& GetVertexInstanceNormals() const { return VertexInstanceNormals; } + + UFUNCTION() + const TArray& GetVertexInstanceUTangents() const { return VertexInstanceUTangents; } + + UFUNCTION() + const TArray& GetVertexInstanceVTangents() const { return VertexInstanceVTangents; } + + UFUNCTION() + const TArray& GetVertexInstanceUVs() const { return VertexInstanceUVs; } + + UFUNCTION() + const TArray& GetMaterialIDsPerTriangle() const { return MaterialIDsPerTriangle; } + + UFUNCTION() + const TArray& GetStaticMaterials() const { return StaticMaterials; } + + TArray& GetStaticMaterials() { return StaticMaterials; } + + UFUNCTION() + UMaterialInterface* GetMaterial(int32 InMaterialIndex); + + UFUNCTION() + int32 GetMaterialIndex(FName InMaterialSlotName) const; + + // Checks if the mesh is valid by checking face, vertex and attribute (normals etc) counts. + // If bSkipVertexIndicesCheck is true, then we don't loop over all triangle vertex indices to + // check if each index is valid (< NumVertices) + UFUNCTION() + bool IsValid(bool bInSkipVertexIndicesCheck=false) const; + + // Custom serialization: we use TArray::BulkSerialize to speed up array serialization + virtual void Serialize(FArchive &InArchive) override; + +protected: + + UPROPERTY() + bool bHasNormals; + + UPROPERTY() + bool bHasTangents; + + UPROPERTY() + bool bHasColors; + + /** The number of UV layers that the mesh has */ + UPROPERTY() + uint32 NumUVLayers; + + UPROPERTY() + bool bHasPerFaceMaterials; + + /** Vertex positions. The vertex id == vertex index => indexes into this array. */ + UPROPERTY(SkipSerialization) + TArray VertexPositions; + + /** Triangle vertices. Triangle id == triangle index => indexes into this array, which returns a FIntVector of + * vertex ids/indices for VertexPositions. + */ + UPROPERTY(SkipSerialization) + TArray TriangleIndices; + + /** Array of colors per vertex instance, in other words, a color per triangle-vertex. Index 3 * TriangleID + LocalTriangleVertexIndex. */ + UPROPERTY(SkipSerialization) + TArray VertexInstanceColors; + + /** Array of normals per vertex instance, in other words, a normal per triangle-vertex. Index 3 * TriangleID + LocalTriangleVertexIndex. */ + UPROPERTY(SkipSerialization) + TArray VertexInstanceNormals; + + /** Array of U tangents per vertex instance, in other words, a tangent per triangle-vertex. Index 3 * TriangleID + LocalTriangleVertexIndex. */ + UPROPERTY(SkipSerialization) + TArray VertexInstanceUTangents; + + /** Array of V tangents per vertex instance, in other words, a tangent per triangle-vertex. Index 3 * TriangleID + LocalTriangleVertexIndex. */ + UPROPERTY(SkipSerialization) + TArray VertexInstanceVTangents; + + /** Array of UV layers to array of per triangle-vertex UVs. Index: UVLayerIndex * (NumVertexInstances) + 3 * TriangleID + LocalTriangleVertexIndex. */ + UPROPERTY(SkipSerialization) + TArray VertexInstanceUVs; + + /** Array of material ID per triangle. Indexed by Triangle ID/Index. */ + UPROPERTY(SkipSerialization) + TArray MaterialIDsPerTriangle; + + /** The materials of the mesh. Index by MaterialID (MaterialIndex). */ + UPROPERTY() + TArray StaticMaterials; +}; + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniStaticMeshComponent.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniStaticMeshComponent.h" + +#include "Components/BillboardComponent.h" +#include "Engine/CollisionProfile.h" +#include "SceneInterface.h" +#include "Engine/Texture2D.h" + +#include "HoudiniStaticMesh.h" +#include "HoudiniStaticMeshSceneProxy.h" + + +UHoudiniStaticMeshComponent::UHoudiniStaticMeshComponent(const FObjectInitializer &InInitialzer) : + Super(InInitialzer) +{ + PrimaryComponentTick.bCanEverTick = false; + + SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName); + + Mesh = nullptr; + bHoudiniIconVisible = true; + +#if WITH_EDITOR + bVisualizeComponent = true; +#endif +} + +void UHoudiniStaticMeshComponent::OnRegister() +{ + Super::OnRegister(); + +#if WITH_EDITORONLY_DATA + if (bVisualizeComponent && SpriteComponent != nullptr && GetOwner()) + { + SpriteComponent->SetSprite(LoadObject(nullptr, TEXT("/HoudiniEngine/Textures/icon_houdini_logo_128.icon_houdini_logo_128"))); + UpdateSpriteComponent(); + } +#endif +} + +//void +//UHoudiniStaticMeshComponent::PostLoad() +//{ +// Super::PostLoad(); +// +// //NotifyMeshUpdated(); +//} + +void UHoudiniStaticMeshComponent::SetMesh(UHoudiniStaticMesh *InMesh) +{ + if (Mesh == InMesh) + return; + + Mesh = InMesh; + NotifyMeshUpdated(); +} + +FPrimitiveSceneProxy* UHoudiniStaticMeshComponent::CreateSceneProxy() +{ + check(SceneProxy == nullptr); + + FHoudiniStaticMeshSceneProxy* NewProxy = nullptr; + if (Mesh && Mesh->GetNumTriangles() > 0) + { + NewProxy = new FHoudiniStaticMeshSceneProxy(this, GetScene()->GetFeatureLevel()); + NewProxy->Build(); + } + return NewProxy; +} + +FBoxSphereBounds UHoudiniStaticMeshComponent::CalcBounds(const FTransform& InLocalToWorld) const +{ + if (Mesh) + { + // mesh bounds + FBoxSphereBounds NewBounds = LocalBounds.TransformBy(InLocalToWorld); + NewBounds.BoxExtent *= BoundsScale; + NewBounds.SphereRadius *= BoundsScale; + + return NewBounds; + } + else + { + return FBoxSphereBounds(InLocalToWorld.GetLocation(), FVector::ZeroVector, 0.f); + } +} + +#if WITH_EDITOR +void UHoudiniStaticMeshComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + const FName NAME_HoudiniIconVisible(TEXT("bHoudiniIconVisible")); + const FName NAME_Mesh(TEXT("Mesh")); + const FName NAME_PropertyChanged = PropertyChangedEvent.GetPropertyName(); + if (NAME_PropertyChanged == NAME_HoudiniIconVisible) + { + UpdateSpriteComponent(); + } + else if (NAME_PropertyChanged == NAME_Mesh) + { + NotifyMeshUpdated(); + } +} +#endif + +void UHoudiniStaticMeshComponent::SetHoudiniIconVisible(bool bInHoudiniIconVisible) +{ + bHoudiniIconVisible = bInHoudiniIconVisible; +#if WITH_EDITORONLY_DATA + UpdateSpriteComponent(); +#endif +} + +void UHoudiniStaticMeshComponent::NotifyMeshUpdated() +{ + MarkRenderStateDirty(); + if (Mesh) + { + LocalBounds = Mesh->CalcBounds(); + } + else + { + LocalBounds.Init(); + } + + UpdateBounds(); + +#if WITH_EDITORONLY_DATA + UpdateSpriteComponent(); +#endif +} + +#if WITH_EDITORONLY_DATA +void UHoudiniStaticMeshComponent::UpdateSpriteComponent() +{ + if (SpriteComponent) + { + const FBoxSphereBounds B = Bounds.TransformBy(GetComponentTransform().ToInverseMatrixWithScale()); + SpriteComponent->SetRelativeLocation(B.Origin + FVector(0, 0, B.BoxExtent.Size())); + SpriteComponent->SetVisibility(bHoudiniIconVisible); + } +} +#endif + +int32 UHoudiniStaticMeshComponent::GetNumMaterials() const +{ + // From UStaticMesh: + // @note : you don't have to consider Materials.Num() + // that only counts if overridden and it can't be more than GetStaticMesh()->Materials. + if (Mesh) + { + return Mesh->GetNumStaticMaterials(); + } + else + { + return 0; + } +} + +int32 UHoudiniStaticMeshComponent::GetMaterialIndex(FName MaterialSlotName) const +{ + return Mesh ? Mesh->GetMaterialIndex(MaterialSlotName) : -1; +} + +TArray UHoudiniStaticMeshComponent::GetMaterialSlotNames() const +{ + TArray MaterialNames; + if (Mesh) + { + const TArray &StaticMaterials = Mesh->GetStaticMaterials(); + const int32 NumMaterials = StaticMaterials.Num(); + for (int32 MaterialIndex = 0; MaterialIndex < NumMaterials; ++MaterialIndex) + { + const FStaticMaterial &StaticMaterial = StaticMaterials[MaterialIndex]; + MaterialNames.Add(StaticMaterial.MaterialSlotName); + } + } + return MaterialNames; +} + +bool UHoudiniStaticMeshComponent::IsMaterialSlotNameValid(FName MaterialSlotName) const +{ + return GetMaterialIndex(MaterialSlotName) >= 0; +} + +UMaterialInterface* UHoudiniStaticMeshComponent::GetMaterial(int32 MaterialIndex) const +{ + // From UStaticMesh: + // If we have a base materials array, use that + if (OverrideMaterials.IsValidIndex(MaterialIndex) && OverrideMaterials[MaterialIndex]) + { + return OverrideMaterials[MaterialIndex]; + } + // Otherwise get from static mesh + else + { + return Mesh ? Mesh->GetMaterial(MaterialIndex) : nullptr; + } +} + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniStaticMeshComponent.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "Components/MeshComponent.h" + +#include "HoudiniStaticMeshComponent.generated.h" + +class UHoudiniStaticMesh; +class UBillboardComponent; + +UCLASS(EditInlineNew, ClassGroup = "Houdini Engine | Rendering") +class HOUDINIENGINERUNTIME_API UHoudiniStaticMeshComponent : public UMeshComponent +{ + GENERATED_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + +public: + UHoudiniStaticMeshComponent(const FObjectInitializer &InInitialzer); + + UFUNCTION() + void SetMesh(UHoudiniStaticMesh *InMesh); + + UFUNCTION() + UHoudiniStaticMesh* GetMesh() { return Mesh; } + + // Call this if the mesh updated (outside of calling SetMesh). + UFUNCTION() + void NotifyMeshUpdated(); + + virtual void OnRegister() override; + + //virtual void PostLoad() override; + + // UPrimitiveComponent interface + virtual FPrimitiveSceneProxy* CreateSceneProxy() override; + virtual int32 GetNumMaterials() const override; + virtual UMaterialInterface* GetMaterial(int32 ElementIndex) const override; + virtual int32 GetMaterialIndex(FName MaterialSlotName) const override; + virtual TArray GetMaterialSlotNames() const override; + virtual bool IsMaterialSlotNameValid(FName MaterialSlotName) const override; + // end - UPrimitiveComponent interface + + // USceneComponent Interface. + virtual FBoxSphereBounds CalcBounds(const FTransform& InLocalToWorld) const override; + // end - USceneComponent Interface. + +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + + UFUNCTION() + bool IsHoudiniIconVisible() const { return bHoudiniIconVisible; } + + UFUNCTION() + void SetHoudiniIconVisible(bool bInHoudiniIconVisible); + +protected: +#if WITH_EDITORONLY_DATA + virtual void UpdateSpriteComponent(); +#endif + + /** The mesh. */ + UPROPERTY(EditAnywhere, Category = "Mesh") + TObjectPtr Mesh; + + /** Local space bounds of mesh. */ + UPROPERTY() + FBox LocalBounds; + + UPROPERTY(EditAnywhere, Category = "Icons") + bool bHoudiniIconVisible; + +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniStaticMeshSceneProxy.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniStaticMeshSceneProxy.h" + +#include "HoudiniStaticMesh.h" +#include "HoudiniStaticMeshComponent.h" + +#include "Async/ParallelFor.h" +#include "Engine/Engine.h" +#include "Materials/Material.h" +#include "PrimitiveViewRelevance.h" +#include "ProfilingDebugging/CpuProfilerTrace.h" +#include "Runtime/Launch/Resources/Version.h" +#include "SceneView.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 1 + #include "MaterialDomain.h" + #include "Materials/MaterialRenderProxy.h" + #include "SceneInterface.h" +#endif + +// Based on: Plugins\Experimental\MeshModelingToolset\Source\ModelingComponents\Private\BaseDynamicMeshSceneProxy.h + +// +// FHoudiniStaticMeshRenderBufferSet +// + +FHoudiniStaticMeshRenderBufferSet::FHoudiniStaticMeshRenderBufferSet(ERHIFeatureLevel::Type InFeatureLevel) + : NumTriangles(0) + , LocalVertexFactory(InFeatureLevel, "FHoudiniStaticMeshRenderBufferSet") +{ +} + + +FHoudiniStaticMeshRenderBufferSet::~FHoudiniStaticMeshRenderBufferSet() +{ + check(IsInRenderingThread()); + + if (NumTriangles > 0) + { + PositionVertexBuffer.ReleaseResource(); + ColorVertexBuffer.ReleaseResource(); + StaticMeshVertexBuffer.ReleaseResource(); + LocalVertexFactory.ReleaseResource(); + if (TriangleIndexBuffer.IsInitialized()) + { + TriangleIndexBuffer.ReleaseResource(); + } + } +} + +void FHoudiniStaticMeshRenderBufferSet::CopyBuffers() +{ + check(IsInRenderingThread()); + + if (NumTriangles == 0) + { + return; + } + + InitOrUpdateResource(&PositionVertexBuffer); + InitOrUpdateResource(&ColorVertexBuffer); + InitOrUpdateResource(&StaticMeshVertexBuffer); + + FLocalVertexFactory::FDataType Data; + PositionVertexBuffer.BindPositionVertexBuffer(&LocalVertexFactory, Data); + StaticMeshVertexBuffer.BindTangentVertexBuffer(&LocalVertexFactory, Data); + StaticMeshVertexBuffer.BindPackedTexCoordVertexBuffer(&LocalVertexFactory, Data); + ColorVertexBuffer.BindColorVertexBuffer(&LocalVertexFactory, Data); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + FRHICommandListBase& RHICmdList = FRHICommandListExecutor::GetImmediateCommandList(); + LocalVertexFactory.SetData(RHICmdList, Data); +#else + LocalVertexFactory.SetData(Data); +#endif + InitOrUpdateResource(&LocalVertexFactory); + + if (TriangleIndexBuffer.Indices.Num() > 0) + { +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3) + TriangleIndexBuffer.InitResource(FRHICommandListExecutor::GetImmediateCommandList()); +#else + TriangleIndexBuffer.InitResource(); +#endif + } +} + +void FHoudiniStaticMeshRenderBufferSet::InitOrUpdateResource(FRenderResource* Resource) +{ + check(IsInRenderingThread()); + + if (Resource->IsInitialized()) + { +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3) + Resource->UpdateRHI(FRHICommandListExecutor::GetImmediateCommandList()); +#else + Resource->UpdateRHI(); +#endif + } + else + { +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3) + Resource->InitResource(FRHICommandListExecutor::GetImmediateCommandList()); +#else + Resource->InitResource(); +#endif + } +} + +void FHoudiniStaticMeshRenderBufferSet::DestroyRenderBufferSet(FHoudiniStaticMeshRenderBufferSet* BufferSet) +{ + if (BufferSet->NumTriangles == 0) + { + return; + } + + ENQUEUE_RENDER_COMMAND(FMeshRenderBufferSetDestroy)( + [BufferSet](FRHICommandListImmediate& RHICmdList) + { + delete BufferSet; + }); +} + +// +// End - FHoudiniStaticMeshRenderBufferSet +// + +// +// FHoudiniStaticMeshSceneProxy +// + +FHoudiniStaticMeshSceneProxy::FHoudiniStaticMeshSceneProxy(UHoudiniStaticMeshComponent* InComponent, ERHIFeatureLevel::Type InFeatureLevel) + : FPrimitiveSceneProxy(InComponent) + , DefaultVertexColor(255, 255, 255) + , FeatureLevel(InFeatureLevel) + , Component(InComponent) +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 7 + , MaterialRelevance(InComponent ? InComponent->GetMaterialRelevance(GetFeatureLevelShaderPlatform_Checked(InFeatureLevel)) : FMaterialRelevance()) +#else + , MaterialRelevance(InComponent ? InComponent->GetMaterialRelevance(InFeatureLevel) : FMaterialRelevance()) +#endif +#if STATICMESH_ENABLE_DEBUG_RENDERING + , Owner(InComponent ? InComponent->GetOwner() : nullptr) +#endif +{ +} + +FHoudiniStaticMeshSceneProxy::~FHoudiniStaticMeshSceneProxy() +{ + //check(IsInRenderingThread()); + + for (FHoudiniStaticMeshRenderBufferSet* BufferSet : BufferSets) + { + FHoudiniStaticMeshRenderBufferSet::DestroyRenderBufferSet(BufferSet); + } +} + +uint32 FHoudiniStaticMeshSceneProxy::AllocateNewRenderBufferSet(FHoudiniStaticMeshRenderBufferSet*& OutBufferSet) +{ + OutBufferSet = MakeNewBufferSet(); + OutBufferSet->Material = UMaterial::GetDefaultMaterial(MD_Surface); + + BufferSetsLock.Lock(); + const uint32 NewIndex = BufferSets.Add(OutBufferSet); + BufferSetsLock.Unlock(); + return NewIndex; +} + +void FHoudiniStaticMeshSceneProxy::ReleaseRenderBufferSet(uint32 InIndex) +{ + FHoudiniStaticMeshRenderBufferSet *BufferSet = nullptr; + + BufferSetsLock.Lock(); + check(BufferSets.IsValidIndex(InIndex)); + BufferSet = BufferSets[InIndex]; + BufferSets.RemoveAt(InIndex); + BufferSetsLock.Unlock(); + + FHoudiniStaticMeshRenderBufferSet::DestroyRenderBufferSet(BufferSet); +} + +void FHoudiniStaticMeshSceneProxy::UpdatedReferencedMaterials() +{ + // copied from FPrimitiveSceneProxy::FPrimitiveSceneProxy() +#if WITH_EDITOR + TArray Materials; + Component->GetUsedMaterials(Materials, true); + ENQUEUE_RENDER_COMMAND(FHoudiniStaticMeshRenderBufferSetDestroy)( + [this, Materials](FRHICommandListImmediate& RHICmdList) + { + this->SetUsedMaterialForVerification(Materials); + }); +#endif +} + +void FHoudiniStaticMeshSceneProxy::Build() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniStaticMeshSceneProxy::Build); + + // Allocate a buffer set per material + const uint32 NumMaterials = GetNumMaterials(); + if (NumMaterials == 0) + { + // No materials, allocate a singel buffer set using the default material + FHoudiniStaticMeshRenderBufferSet *BufferSet = nullptr; + AllocateNewRenderBufferSet(BufferSet); + BufferSet->Material = UMaterial::GetDefaultMaterial(MD_Surface); + } + else + { + for (uint32 MaterialIdx = 0; MaterialIdx < NumMaterials; ++MaterialIdx) + { + FHoudiniStaticMeshRenderBufferSet *BufferSet = nullptr; + AllocateNewRenderBufferSet(BufferSet); + BufferSet->Material = GetMaterial(MaterialIdx); + } + } + + if (Component) + { + UHoudiniStaticMesh *Mesh = Component->GetMesh(); + if (Mesh) + { + if (NumMaterials > 1 && Mesh->HasPerFaceMaterials()) + { + BuildBufferSetsByMaterial(); + } + else + { + BuildSingleBufferSet(); + } + } + } +} + +void FHoudiniStaticMeshSceneProxy::GetDynamicMeshElements(const TArray& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const +{ + const FEngineShowFlags EngineShowFlags = ViewFamily.EngineShowFlags; + const bool bRenderAsWireframe = (AllowDebugViewmodes() && EngineShowFlags.Wireframe); + + // Set up the wireframe material + FMaterialRenderProxy *WireframeMaterialProxy = nullptr; + if (bRenderAsWireframe) + { + FColoredMaterialRenderProxy *WireframeMaterialInstance = new FColoredMaterialRenderProxy( + GEngine->WireframeMaterial ? GEngine->WireframeMaterial->GetRenderProxy() : nullptr, + FLinearColor(0.6f, 0.6f, 0.6f) + ); + Collector.RegisterOneFrameMaterialProxy(WireframeMaterialInstance); + WireframeMaterialProxy = WireframeMaterialInstance; + } + + const ESceneDepthPriorityGroup DepthPriority = SDPG_World; + + const int32 NumViews = Views.Num(); + for (int32 ViewIdx = 0; ViewIdx < NumViews; ++ViewIdx) + { + if (!(VisibilityMap & (1 << ViewIdx))) + continue; + + const FSceneView *View = Views[ViewIdx]; + + bool bHasPrecomputedVolumetricLightmap; + FMatrix PreviousLocalToWorld; + int32 SingleCaptureIndex; + bool bOutputVelocity; + GetScene().GetPrimitiveUniformShaderParameters_RenderThread( + GetPrimitiveSceneInfo(), bHasPrecomputedVolumetricLightmap, PreviousLocalToWorld, SingleCaptureIndex, bOutputVelocity); + + const uint32 NumBufferSets = BufferSets.Num(); + for (uint32 BufferSetIdx = 0; BufferSetIdx < NumBufferSets; ++BufferSetIdx) + { + FHoudiniStaticMeshRenderBufferSet *BufferSet = BufferSets[BufferSetIdx]; + + UMaterialInterface *Material = BufferSet->Material; + FMaterialRenderProxy *MaterialProxy = Material->GetRenderProxy(); + + if (BufferSet->NumTriangles == 0) + continue; + + FDynamicPrimitiveUniformBuffer &DynamicPrimitiveUniformBuffer = Collector.AllocateOneFrameResource(); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + //FRHICommandListBase& RHICmdList = FRHICommandListExecutor::GetImmediateCommandList(); + DynamicPrimitiveUniformBuffer.Set( + Collector.GetRHICommandList(), + GetLocalToWorld(), + PreviousLocalToWorld, + GetBounds(), + GetLocalBounds(), + GetLocalBounds(), + true, + bHasPrecomputedVolumetricLightmap, + bOutputVelocity, + GetCustomPrimitiveData()); +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 + DynamicPrimitiveUniformBuffer.Set( + GetLocalToWorld(), + PreviousLocalToWorld, + GetBounds(), + GetLocalBounds(), + true, + bHasPrecomputedVolumetricLightmap, + bOutputVelocity); +#else + DynamicPrimitiveUniformBuffer.Set( + GetLocalToWorld(), + PreviousLocalToWorld, + GetBounds(), + GetLocalBounds(), + true, + bHasPrecomputedVolumetricLightmap, + DrawsVelocity(), + bOutputVelocity); +#endif + + if (BufferSet->TriangleIndexBuffer.Indices.Num() > 0) + { + FMeshBatch& Mesh = Collector.AllocateMesh(); + if (PopulateMeshElement(Mesh, *BufferSet, MaterialProxy, false, DepthPriority, ViewIdx, DynamicPrimitiveUniformBuffer)) + { + Collector.AddMesh(ViewIdx, Mesh); + } + if (bRenderAsWireframe) + { + FMeshBatch& WireframeMesh = Collector.AllocateMesh(); + if (PopulateMeshElement(WireframeMesh, *BufferSet, WireframeMaterialProxy, true, DepthPriority, ViewIdx, DynamicPrimitiveUniformBuffer)) + { + Collector.AddMesh(ViewIdx, WireframeMesh); + } + } + } + } + +#if STATICMESH_ENABLE_DEBUG_RENDERING + if (EngineShowFlags.StaticMeshes) + { + RenderBounds(Collector.GetPDI(ViewIdx), EngineShowFlags, GetBounds(), !Owner || IsSelected()); + } +#endif // STATICMESH_ENABLE_DEBUG_RENDERING + } +} + +bool FHoudiniStaticMeshSceneProxy::PopulateMeshElement( + FMeshBatch &InMeshBatch, + const FHoudiniStaticMeshRenderBufferSet& Buffers, + FMaterialRenderProxy* Material, + bool bRenderAsWireframe, + ESceneDepthPriorityGroup DepthPriority, + int ViewIndex, + FDynamicPrimitiveUniformBuffer& DynamicPrimitiveUniformBuffer) const +{ + FMeshBatchElement& BatchElement = InMeshBatch.Elements[0]; + BatchElement.IndexBuffer = &Buffers.TriangleIndexBuffer; + InMeshBatch.bWireframe = bRenderAsWireframe; + InMeshBatch.VertexFactory = &Buffers.LocalVertexFactory; + InMeshBatch.MaterialRenderProxy = Material; + + BatchElement.PrimitiveUniformBufferResource = &DynamicPrimitiveUniformBuffer.UniformBuffer; + + BatchElement.FirstIndex = 0; + BatchElement.NumPrimitives = Buffers.NumTriangles; + BatchElement.MinVertexIndex = 0; + BatchElement.MaxVertexIndex = Buffers.PositionVertexBuffer.GetNumVertices() - 1; + InMeshBatch.ReverseCulling = IsLocalToWorldDeterminantNegative(); + InMeshBatch.Type = PT_TriangleList; + InMeshBatch.DepthPriorityGroup = DepthPriority; + InMeshBatch.bCanApplyViewModeOverrides = false; + + return true; +} + + +FPrimitiveViewRelevance FHoudiniStaticMeshSceneProxy::GetViewRelevance(const FSceneView* View) const +{ + FPrimitiveViewRelevance Result; + + Result.bDrawRelevance = IsShown(View); + Result.bDynamicRelevance = true; + Result.bRenderCustomDepth = ShouldRenderCustomDepth(); + Result.bRenderInMainPass = ShouldRenderInMainPass(); + Result.bShadowRelevance = IsShadowCast(View); + Result.bTranslucentSelfShadow = bCastVolumetricTranslucentShadow; + Result.bUsesLightingChannels = GetLightingChannelMask() != GetDefaultLightingChannelMask(); + MaterialRelevance.SetPrimitiveViewRelevance(Result); + Result.bVelocityRelevance = IsMovable() && Result.bOpaque && Result.bRenderInMainPass; + + return Result; +} + +bool FHoudiniStaticMeshSceneProxy::CanBeOccluded() const +{ + return !MaterialRelevance.bDisableDepthTest; +} + +void FHoudiniStaticMeshSceneProxy::PopulateBuffers(const UHoudiniStaticMesh *InMesh, FHoudiniStaticMeshRenderBufferSet *InBuffers, const TArray* InTriangleIDs, uint32 InTriangleGroupStartIdx, uint32 InNumTrianglesInGroup) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniStaticMeshSceneProxy::PopulateBuffers); + + check(InMesh); + check(InBuffers); + + const uint32 NumTriangles = InTriangleIDs ? InNumTrianglesInGroup : InMesh->GetNumTriangles(); + InBuffers->NumTriangles = NumTriangles; + + if (NumTriangles == 0) + return; + + const uint32 NumVertices = NumTriangles * 3; + const uint32 NumUVLayers = InMesh->GetNumUVLayers(); + + InBuffers->PositionVertexBuffer.Init(NumVertices); + // There must be at least one UV layer + // TODO: Would it be possible to have no UV layers and bind to a dummy 0/black SRV? + InBuffers->StaticMeshVertexBuffer.Init(NumVertices, NumUVLayers > 0 ? NumUVLayers : 1); + InBuffers->ColorVertexBuffer.Init(NumVertices); + InBuffers->TriangleIndexBuffer.Indices.AddUninitialized(NumTriangles * 3); + + const TArray& VertexPositions = InMesh->GetVertexPositions(); + const TArray& TriangleIndices = InMesh->GetTriangleIndices(); + const TArray& VertexInstanceColors = InMesh->GetVertexInstanceColors(); + const TArray& VertexInstanceNormals = InMesh->GetVertexInstanceNormals(); + const TArray& VertexInstanceUTangents = InMesh->GetVertexInstanceUTangents(); + const TArray& VertexInstanceVTangents = InMesh->GetVertexInstanceVTangents(); + const TArray& VertexInstanceUVs = InMesh->GetVertexInstanceUVs(); + + const bool bHasColors = InMesh->HasColors(); + const bool bHasNormals = InMesh->HasNormals(); + const bool bHasTangents = InMesh->HasTangents(); + + FThreadSafeCounter VertCounter(0); + //for (uint32 TriangleIDIdx = 0; TriangleIDIdx < NumTriangles; ++TriangleIDIdx) + ParallelFor(NumTriangles, [&](uint32 TriangleIDIdx) + { + const uint32 TriangleID = InTriangleIDs ? (*InTriangleIDs)[InTriangleGroupStartIdx + TriangleIDIdx] : TriangleIDIdx; + const FIntVector &TriIndices = TriangleIndices[TriangleID]; + + FVector3f TangentU; + FVector3f TangentV; + uint32 VertIdx = VertCounter.Add(3); + for (uint8 TriVertIdx = 0; TriVertIdx < 3; ++TriVertIdx) + { + const uint32 MeshVtxIdx = TriIndices[TriVertIdx]; + const uint32 MeshVtxInstanceIdx = TriangleID * 3 + TriVertIdx; + + InBuffers->PositionVertexBuffer.VertexPosition(VertIdx) = VertexPositions[TriIndices[TriVertIdx]]; + + FVector3f Normal = bHasNormals ? VertexInstanceNormals[MeshVtxInstanceIdx] : FVector3f(0, 0, 1); + if (bHasTangents) + { + TangentU = VertexInstanceUTangents[MeshVtxInstanceIdx]; + TangentV = VertexInstanceVTangents[MeshVtxInstanceIdx]; + } + else + { + Normal.FindBestAxisVectors(TangentU, TangentV); + } + InBuffers->StaticMeshVertexBuffer.SetVertexTangents(VertIdx, TangentU, TangentV, Normal); + + if (NumUVLayers > 0) + { + for (uint8 UVLayerIdx = 0; UVLayerIdx < NumUVLayers; ++UVLayerIdx) + { + InBuffers->StaticMeshVertexBuffer.SetVertexUV(VertIdx, UVLayerIdx, VertexInstanceUVs[MeshVtxInstanceIdx]); + } + } + else + { + InBuffers->StaticMeshVertexBuffer.SetVertexUV(VertIdx, 0, FVector2f::ZeroVector); + } + + InBuffers->ColorVertexBuffer.VertexColor(VertIdx) = bHasColors ? VertexInstanceColors[MeshVtxInstanceIdx] : DefaultVertexColor; + + InBuffers->TriangleIndexBuffer.Indices[VertIdx] = VertIdx; + VertIdx++; + } + }); +} + +void FHoudiniStaticMeshSceneProxy::BuildSingleBufferSet() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniStaticMeshSceneProxy::BuildSingleBufferSet); + + if (!Component) + return; + + UHoudiniStaticMesh *Mesh = Component->GetMesh(); + if (!Mesh) + return; + + if (BufferSets.Num() == 0) + return; + + FHoudiniStaticMeshRenderBufferSet *Buffers = BufferSets.Last(); + + PopulateBuffers(Mesh, Buffers); + + ENQUEUE_RENDER_COMMAND(FHoudiniStaticMeshSceneProxy_BuildSingleBufferSet)( + [Buffers](FRHICommandListImmediate& RHICMdList) + { + Buffers->CopyBuffers(); + }); +} + +void FHoudiniStaticMeshSceneProxy::BuildBufferSetsByMaterial() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FHoudiniStaticMeshSceneProxy::BuildBufferSetsByMaterial); + + // We need to group tris by which material they use, and populate a buffer set for each group + if (!Component) + return; + + UHoudiniStaticMesh *Mesh = Component->GetMesh(); + if (!Mesh) + return; + + if (BufferSets.Num() == 0) + return; + + const TArray& MaterialIDsPerTriangle = Mesh->GetMaterialIDsPerTriangle(); + + const uint32 NumTriangles = MaterialIDsPerTriangle.Num(); + const uint32 NumMaterials = GetNumMaterials(); + TArray TriCountPerMaterialSafe; + TriCountPerMaterialSafe.Init(FThreadSafeCounter(0), NumMaterials); + + ParallelFor(NumTriangles, [&](uint32 TriangleID) + { + const int32 MatID = MaterialIDsPerTriangle[TriangleID]; + if (MatID >= 0 && (uint32) MatID < NumMaterials) + { + TriCountPerMaterialSafe[MatID].Increment(); + } + }); + + TArray TriCountPerMaterial; + TriCountPerMaterial.SetNumZeroed(NumMaterials); + TArray OffsetPerMaterial; + OffsetPerMaterial.SetNumZeroed(NumMaterials); + TArray WrittenPerMaterial; + WrittenPerMaterial.Init(FThreadSafeCounter(0), NumMaterials); + for (int32 MatID = 0; (uint32) MatID < NumMaterials; ++MatID) + { + const uint32 Count = TriCountPerMaterialSafe[MatID].GetValue(); + TriCountPerMaterial[MatID] = Count; + if (MatID > 0) + { + OffsetPerMaterial[MatID] = OffsetPerMaterial[MatID - 1] + TriCountPerMaterial[MatID - 1]; + } + } + + TArray GroupTriangleIDs; + GroupTriangleIDs.SetNumZeroed(NumTriangles); + ParallelFor(NumTriangles, [&](uint32 TriangleID) + { + const int32 MatID = MaterialIDsPerTriangle[TriangleID]; + if (MatID >= 0 && (uint32) MatID < NumMaterials) + { + GroupTriangleIDs[OffsetPerMaterial[MatID] + WrittenPerMaterial[MatID].Add(1)] = TriangleID; + } + }); + + for (int32 MatID = 0; (uint32) MatID < NumMaterials; ++MatID) + { + if (TriCountPerMaterial[MatID] == 0) + continue; + + FHoudiniStaticMeshRenderBufferSet *Buffers = BufferSets[MatID]; + + PopulateBuffers( + Mesh, Buffers, + &GroupTriangleIDs, OffsetPerMaterial[MatID], TriCountPerMaterial[MatID] + ); + + ENQUEUE_RENDER_COMMAND(FHoudiniStaticMeshSceneProxy_BuildSingleBufferSet)( + [Buffers](FRHICommandListImmediate& RHICMdList) + { + Buffers->CopyBuffers(); + }); + } +} + +UMaterialInterface* FHoudiniStaticMeshSceneProxy::GetMaterial(uint32 InMaterialIdx) const +{ + if (!Component) + return UMaterial::GetDefaultMaterial(MD_Surface); + UMaterialInterface *Material = Component->GetMaterial(InMaterialIdx); + return Material ? Material : UMaterial::GetDefaultMaterial(MD_Surface); +} + +// +// End - FHoudiniStaticMeshSceneProxy +// + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniStaticMeshSceneProxy.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// Based on: Plugins\Experimental\MeshModelingToolset\Source\ModelingComponents\Private\BaseDynamicMeshSceneProxy.h + +#pragma once + +#include "CoreMinimal.h" +#include "PrimitiveSceneProxy.h" +#include "VertexFactory.h" +#include "LocalVertexFactory.h" +#include "Rendering/ColorVertexBuffer.h" +#include "Rendering/PositionVertexBuffer.h" +#include "Rendering/StaticMeshVertexBuffer.h" +#include "DynamicMeshBuilder.h" +#include "StaticMeshResources.h" + +#include "HoudiniStaticMeshComponent.h" + +class UHoudiniStaticMesh; + +class FHoudiniStaticMeshRenderBufferSet +{ +public: + // Data members + + /** The number of triangles in the buffer set. */ + int NumTriangles; + + /** The static mesh data buffer. */ + FStaticMeshVertexBuffer StaticMeshVertexBuffer; + + /** The position buffer. */ + FPositionVertexBuffer PositionVertexBuffer; + + /** The triangle indices buffer. */ + FDynamicMeshIndexBuffer32 TriangleIndexBuffer; + + /** The color buffer */ + FColorVertexBuffer ColorVertexBuffer; + + FLocalVertexFactory LocalVertexFactory; + + /** Default material for this mesh. */ + UMaterialInterface* Material = nullptr; + + // Functions + + FHoudiniStaticMeshRenderBufferSet(ERHIFeatureLevel::Type FeatureLevelType); + + virtual ~FHoudiniStaticMeshRenderBufferSet(); + + /** + * Copy buffers to GPU. + * @warning render thread only. + */ + virtual void CopyBuffers(); + + /** + * Initialize (or update) a render resource. + * @warning Render thread only. + */ + void InitOrUpdateResource(FRenderResource* Resource); + +protected: + friend class FHoudiniStaticMeshSceneProxy; + + // Queue a command on the render thread to destroy the given buffer set + static void DestroyRenderBufferSet(FHoudiniStaticMeshRenderBufferSet* BufferSet); +}; + + +class FHoudiniStaticMeshSceneProxy : public FPrimitiveSceneProxy +{ +public: + FHoudiniStaticMeshSceneProxy(UHoudiniStaticMeshComponent* InComponent, ERHIFeatureLevel::Type InFeatureLevel); + + virtual ~FHoudiniStaticMeshSceneProxy(); + + uint32 AllocateNewRenderBufferSet(FHoudiniStaticMeshRenderBufferSet*& OutBufferSet); + + void ReleaseRenderBufferSet(uint32 InIndex); + + void UpdatedReferencedMaterials(); + + // Build buffer sets to render the mesh. + virtual void Build(); + + // FPrimitiveSceneProxy + virtual void GetDynamicMeshElements(const TArray& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override; + + virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const override; + + virtual bool CanBeOccluded() const override; + + virtual uint32 GetMemoryFootprint(void) const override { return(sizeof(*this) + FPrimitiveSceneProxy::GetAllocatedSize()); } + + SIZE_T GetTypeHash() const override + { + static size_t UniquePointer; + return reinterpret_cast(&UniquePointer); + } + + // end - FPrimitiveSceneProxy + + // Color to use if vertex does not have an assigned color. + FColor DefaultVertexColor; + + ERHIFeatureLevel::Type FeatureLevel; + +protected: + void PopulateBuffers(const UHoudiniStaticMesh *InMesh, FHoudiniStaticMeshRenderBufferSet *InBuffers, const TArray* InTriangleIDs=nullptr, uint32 InTriangleGroupStartIdx=0u, uint32 InNumTrianglesInGroup=0u); + + // Virtual function for creating a new buffer set instances. + // Subclasses can overwrite this is they use a different buffer set with + // different instantiation requirements. + virtual FHoudiniStaticMeshRenderBufferSet* MakeNewBufferSet() { return new FHoudiniStaticMeshRenderBufferSet(FeatureLevel); } + + // Build a single buffer set for the entire mesh (one material for the entire mesh). + void BuildSingleBufferSet(); + + void BuildBufferSetsByMaterial(); + + // Get the number of materials from the parent mesh/component + uint32 GetNumMaterials() const { return Component ? Component->GetNumMaterials() : 0; } + + virtual bool PopulateMeshElement( + FMeshBatch &InMeshBatch, + const FHoudiniStaticMeshRenderBufferSet& Buffers, + FMaterialRenderProxy* Material, + bool bRenderAsWireframe, + ESceneDepthPriorityGroup DepthPriority, + int ViewIndex, + FDynamicPrimitiveUniformBuffer& DynamicPrimitiveUniformBuffer) const; + + virtual UMaterialInterface* GetMaterial(uint32 InMaterialIdx) const; + + UHoudiniStaticMeshComponent *Component; + + TArray BufferSets; + + FCriticalSection BufferSetsLock; + + FMaterialRelevance MaterialRelevance; + +private: +#if STATICMESH_ENABLE_DEBUG_RENDERING + AActor* Owner; +#endif +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniToolData.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniToolData.h" + +#include "HoudiniToolsRuntimeUtils.h" +#include "ImageCore.h" +#include "ImageUtils.h" +#include "Engine/Texture2D.h" +#include "Misc/Paths.h" + +#ifndef ENGINE_MAJOR_VERSION +#include "Runtime/Launch/Resources/Version.h" +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 +#define DEFAULT_GAMMA_SPACE EGammaSpace::Invalid +#define DEFAULT_RAW_IMAGE_FORMAT_TYPE ERawImageFormat::Invalid +#else +#define DEFAULT_GAMMA_SPACE EGammaSpace::sRGB +#define DEFAULT_RAW_IMAGE_FORMAT_TYPE ERawImageFormat::Type::BGRA8 +#endif + + + +FHImageData::FHImageData() + : SizeX(0) + , SizeY(0) + , NumSlices(0) + , Format(DEFAULT_RAW_IMAGE_FORMAT_TYPE) + , GammaSpace(static_cast(DEFAULT_GAMMA_SPACE)) + , RawDataMD5(FString()) +{ } + +void FHImageData::FromImage(const FImage& InImage) +{ + SizeX = InImage.SizeX; + SizeY = InImage.SizeY; + NumSlices = InImage.NumSlices; + Format = static_cast(InImage.Format); + GammaSpace = static_cast(InImage.GammaSpace); + RawData = InImage.RawData; + RawDataMD5 = FMD5::HashBytes(RawData.GetData(), RawData.Num()); +} + +void FHImageData::ToImage(FImage& OutImage) const +{ + OutImage.SizeX = SizeX; + OutImage.SizeY = SizeY; + OutImage.NumSlices = NumSlices; + OutImage.Format = static_cast(Format); + OutImage.GammaSpace = static_cast(GammaSpace); + OutImage.RawData = RawData; +} + + +UHoudiniToolData::UHoudiniToolData() +{ +} + +bool +UHoudiniToolData::PopulateFromJSONData(const FString& JSONData) +{ + return false; +} + +bool +UHoudiniToolData::PopulateFromJSONFile(const FString& JsonFilePath) +{ + return false; +} + +bool +UHoudiniToolData::ConvertToJSONData(FString& JSONData) const +{ + return false; +} + +bool +UHoudiniToolData::SaveToJSONFile(const FString& JsonFilePath) +{ + return false; +} + +#if WITH_EDITOR +void UHoudiniToolData::LoadIconFromPath(const FString& IconPath) +{ + const FString FullIconPath = FPaths::ConvertRelativePathToFull(IconPath); + bool bSuccess = false; + + if (FPaths::FileExists(FullIconPath)) + { + FName BrushName = *IconPath; + FHImageData ImageData; + if (FHoudiniToolsRuntimeUtils::LoadFHImageFromFile(*FullIconPath, ImageData)) + { + Modify(); + MarkPackageDirty(); + IconImageData = MoveTemp(ImageData); + IconSourcePath.FilePath = FullIconPath; + bSuccess = true; + } + } + + if (bSuccess) + { + UpdateOwningAssetThumbnail(); + } + else + { + ClearCachedIcon(); + } +} +#endif + +#if WITH_EDITOR +void UHoudiniToolData::ClearCachedIcon() +{ + IconImageData = FHImageData(); + IconSourcePath.FilePath = FString(); + UpdateOwningAssetThumbnail(); +} + +void UHoudiniToolData::UpdateOwningAssetThumbnail() +{ + UHoudiniAsset* HoudiniAsset = this->GetTypedOuter(); + FHoudiniToolsRuntimeUtils::UpdateHoudiniAssetThumbnail(HoudiniAsset); +} +#endif + +void UHoudiniToolData::CopyFrom(const UHoudiniToolData& Other) +{ + if (this == &Other) + { + return; + } + + Name = Other.Name; + ToolTip = Other.ToolTip; + IconImageData = Other.IconImageData; + IconSourcePath = Other.IconSourcePath; + HelpURL = Other.HelpURL; + Type = Other.Type; + DefaultTool = Other.DefaultTool; + SelectionType = Other.SelectionType; + SourceAssetPath = Other.SourceAssetPath; +} + +#undef DEFAULT_GAMMA_SPACE +#undef DEFAULT_RAW_IMAGE_FORMAT_TYPE + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniToolTypes.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniToolTypes.generated.h" + +USTRUCT(BlueprintType) +struct FCategoryRules +{ + GENERATED_BODY(); + + // Include any tools that match the following patterns. + UPROPERTY(EditAnywhere, Category="Category Patterns") + TArray Include; + + // Any tools matching these patterns will be omitted from the inclusion set. + UPROPERTY(EditAnywhere, Category="Category Patterns") + TArray Exclude; +}; + +UENUM() +enum class EHoudiniToolType : uint8 +{ + // For tools that generates geometry, and do not need input + HTOOLTYPE_GENERATOR UMETA(DisplayName = "Generator"), + + // For tools that have a single input, the selection will be merged in that single input + HTOOLTYPE_OPERATOR_SINGLE UMETA(DisplayName = "Operator (single)"), + + // For Tools that have multiple input, a single selected asset will be applied to each input + HTOOLTYPE_OPERATOR_MULTI UMETA(DisplayName = "Operator (multiple)"), + + // For tools that needs to be applied each time for each single selected + HTOOLTYPE_OPERATOR_BATCH UMETA(DisplayName = "Batch Operator") +}; + +UENUM() +enum class EHoudiniToolSelectionType : uint8 +{ + // For tools that can be applied both to Content Browser and World selection + HTOOL_SELECTION_ALL UMETA(DisplayName = "Content Browser AND World"), + + // For tools that can be applied only to World selection + HTOOL_SELECTION_WORLD_ONLY UMETA(DisplayName = "World selection only"), + + // For tools that can be applied only to Content Browser selection + HTOOL_SELECTION_CB_ONLY UMETA(DisplayName = "Content browser selection only") +}; +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniToolsPackageAsset.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include "HoudiniToolsPackageAsset.h" + +#include "HoudiniEngineRuntime.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniToolsRuntimeUtils.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "Serialization/JsonSerializer.h" +#include "UObject/ObjectSaveContext.h" + +UHoudiniToolsPackageAsset::UHoudiniToolsPackageAsset() + : bReimportPackageDescription(false) + , bExportPackageDescription(false) + , bReimportToolsDescription(false) + , bExportToolsDescription(false) +{ + +} + +#if WITH_EDITOR +void +UHoudiniToolsPackageAsset::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + UObject::PostEditChangeProperty(PropertyChangedEvent); + if (PropertyChangedEvent.MemberProperty->GetFName() == GET_MEMBER_NAME_CHECKED(UHoudiniToolsPackageAsset, ExternalPackageDir)) + { + UpdateProperties(); + } + + if (FModuleManager::Get().IsModuleLoaded("HoudiniEngineRuntime")) + { + const FHoudiniEngineRuntime& HoudiniEngineRuntime = FModuleManager::GetModuleChecked("HoudiniEngineRuntime"); + HoudiniEngineRuntime.BroadcastToolOrPackageChanged(); + } +} +#endif + +#if WITH_EDITOR +void +UHoudiniToolsPackageAsset::MakeRelativePackagePath() +{ + const FString ProjectDir = FPaths::ProjectDir(); + FPaths::MakePathRelativeTo(ExternalPackageDir.Path, *ProjectDir); +} +#endif + + +#if WITH_EDITOR +void +UHoudiniToolsPackageAsset::UpdateProperties() +{ + if (ExternalPackageDir.Path.Len()>0) + { + MakeRelativePackagePath(); + } +} +#endif + + +#if WITH_EDITOR +void +UHoudiniToolsPackageAsset::PreSave(FObjectPreSaveContext SaveContext) +{ + UObject::PreSave(SaveContext); + + // Update our properties to ensure they are correct (relative paths, etc). + UpdateProperties(); +} +#endif + +#if WITH_EDITOR +void +UHoudiniToolsPackageAsset::PostSaveRoot(FObjectPostSaveRootContext ObjectSaveContext) +{ + UObject::PostSaveRoot(ObjectSaveContext); + + if (bExportPackageDescription) + { + // Export JSON description for this package. + const FString JSONPath = FHoudiniToolsRuntimeUtils::GetHoudiniToolsPackageJSONPath(this); + if (JSONPath.Len()) + { + FHoudiniToolsRuntimeUtils::WriteJSONFromToolPackageAsset(this, JSONPath); + } + } +} +#endif + +bool +UHoudiniToolsPackageAsset::Rename(const TCHAR* InName, UObject* NewOuter, ERenameFlags Flags) +{ +#if WITH_EDITOR + // Warn if the name is incorrect + if (InName != FHoudiniToolsRuntimeUtils::GetPackageUAssetName()) //&& Flags != REN_Test) + { + if (!FHoudiniToolsRuntimeUtils::ShowToolsPackageRenameConfirmDialog()) + { + return false; + } + } +#endif + + // Proceed with the renaming, but add a warning for good measure + HOUDINI_LOG_WARNING(TEXT("Renaming a HoudiniToolsPackage to anything but \"HoudiniToolsPackage\" disables it.")); + + return Super::Rename(InName, NewOuter, Flags); +} + + + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniToolsPackageAsset.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniToolTypes.h" +#include "UObject/Object.h" + +#include "HoudiniToolsPackageAsset.generated.h" + +enum class EAssetCommandResult : uint8; +struct FAssetCategoryPath; +struct FAssetOpenArgs; + +/** + * This is a Houdini Tools package descriptor inside of UE, typically created + * after a HoudiniToolsPackage has been imported into the UE project. + */ +UCLASS(BlueprintType, hidecategories=(Object)) +class HOUDINIENGINERUNTIME_API UHoudiniToolsPackageAsset : public UObject +{ + GENERATED_BODY() +public: + // Package data, typically extracted from imported JSON data. + UHoudiniToolsPackageAsset(); + + // Rename override - prevents renaming tools package + virtual bool Rename(const TCHAR* NewName/* =nullptr */, UObject* NewOuter=nullptr, ERenameFlags Flags=REN_None) override; + + // Category names mapped to string patterns that will be used to determine whether + // HDAs in this package is assigned to the respective category. + UPROPERTY(EditAnywhere, Category = "HoudiniToolsPackage") + TMap Categories; + + // Optional path to an external directory containing HDAs associated with this package. + // This path will be used to import new HDAs into this package. + // This path is always relative to the Project File Path. + UPROPERTY(EditAnywhere, Category = "HoudiniToolsPackage") + FDirectoryPath ExternalPackageDir; + + // If a package update/reimport is executed, reimport external package JSON data. + UPROPERTY(EditAnywhere, Category = "HoudiniToolsPackage") + bool bReimportPackageDescription; + + // Any changes to this package description will be written to the package's external JSON file. If the external + // JSON file doesn't exist, it will be created. + UPROPERTY(EditAnywhere, Category = "HoudiniToolsPackage") + bool bExportPackageDescription; + + // When an HDA is reimported into this package, and it has an external JSON file, the data from the external + // JSON file will be imported. NOTE: When HDA is imported into for the first time, it will always import + // external JSON data, if it exists. + UPROPERTY(EditAnywhere, Category = "HoudiniToolsPackage") + bool bReimportToolsDescription; + + // Any changes to HDAs in this package will be written to the HDA's external JSON file. If the external JSON file + // doesn't exist, it will be created. + UPROPERTY(EditAnywhere, Category = "HoudiniToolsPackage") + bool bExportToolsDescription; + + +#if WITH_EDITOR + /** + * UObject overrides + */ + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + + virtual void PreSave(FObjectPreSaveContext SaveContext) override; + virtual void PostSaveRoot(FObjectPostSaveRootContext ObjectSaveContext) override; + +private: + // Helper function to convert SourcePackagePath to a path that is relative to the ProjectDir. + void MakeRelativePackagePath(); + + // Apply property transformations if needed. + void UpdateProperties(); + +#endif +}; + + + +// // -------------------------------------------- +// // HoudiniToolsPackageAsset factory +// // -------------------------------------------- +// +// UCLASS(hidecategories = Object) +// class HOUDINIENGINERUNTIME_API UHoudiniToolsPackageAssetFactory : public UFactory +// { +// GENERATED_BODY() +// +// public: +// +// UHoudiniToolsPackageAssetFactory() +// { +// SupportedClass = UHoudiniToolsPackageAsset::StaticClass(); +// +// bCreateNew = true; +// bEditAfterNew = true; +// } +// +// FText GetDisplayName() const override +// { +// return NSLOCTEXT("DisplayName", "AssetDefinition_HoudiniToolsPackage", "Houdini Tools Package"); +// } +// +// FText GetToolTip() const override +// { +// return NSLOCTEXT("Tooltip", "AssetDefinition_HoudiniToolsPackage", "Describes a Houdini Tools package."); +// } +// +// UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override +// { +// UHoudiniToolsPackageAsset* Preset = nullptr; +// if (ensure(SupportedClass == Class)) +// { +// ensure(0 != (RF_Public & Flags)); +// Preset = NewObject(InParent, Class, Name, Flags); +// } +// return Preset; +// } +// +// uint32 GetMenuCategories() const override +// { +// IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); +// return AssetTools.RegisterAdvancedAssetCategory("HoudiniEngine", NSLOCTEXT("AssetCategoryName", "AssetDefinition_HoudiniToolsPackage", "HoudiniEngine")); +// } +// +// FString GetDefaultNewAssetName() const override +// { +// return TEXT("Houdini Asset Package"); +// } +// +// bool ShouldShowInNewMenu() const override { return true; } +// }; + + +// -------------------------------------------- +// HoudiniToolsPackage asset definition +// -------------------------------------------- +// +// UCLASS() +// class UAssetDefinition_HoudiniToolsPackageAsset : public UAssetDefinitionDefault +// { +// GENERATED_BODY() +// +// public: +// // UAssetDefinition Begin +// virtual FText GetAssetDisplayName() const override { return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_HoudiniToolsPackageAsset", "Houdini Tools Package Asset"); } +// virtual FLinearColor GetAssetColor() const override { return FLinearColor(FColor(175, 0, 128)); } +// virtual TSoftClassPtr GetAssetClass() const override { return UHoudiniToolsPackageAsset::StaticClass(); } +// virtual TConstArrayView GetAssetCategories() const override +// { +// return TConstArrayView(); +// } +// virtual FText GetObjectDisplayNameText(UObject* Object) const override { return FText::FromString(TEXT("UHoudiniToolsPackageAsset")); } +// // UAssetDefinition End +// }; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniToolsRuntimeUtils.cpp`: + +```cpp +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniToolsRuntimeUtils.h" + +#if WITH_EDITOR +#include "HoudiniAsset.h" +#include "HoudiniEngineRuntimePrivatePCH.h" +#include "HoudiniToolsPackageAsset.h" +#include "ImageUtils.h" +#include "HoudiniToolData.h" +#include "ImageCore.h" +#include "ImageUtils.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "EditorFramework/AssetImportData.h" +#include "Engine/Texture2D.h" +#include "HAL/FileManager.h" +#include "HAL/PlatformFileManager.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "Modules/ModuleManager.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonSerializer.h" +#include "Misc/MessageDialog.h" +#include "GenericPlatform/GenericPlatformMisc.h" + +#include "ObjectTools.h" + +#ifndef ENGINE_MAJOR_VERSION +#include "Runtime/Launch/Resources/Version.h" +#endif + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 +#define DEFAULT_GAMMA_SPACE EGammaSpace::Invalid +#define DEFAULT_RAW_IMAGE_FORMAT_TYPE ERawImageFormat::Invalid +#else +#define DEFAULT_GAMMA_SPACE EGammaSpace::sRGB +#define DEFAULT_RAW_IMAGE_FORMAT_TYPE ERawImageFormat::Type::BGRA8 +#endif + +#define LOCTEXT_NAMESPACE "HoudiniTools" + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0 +// Unreal 5.0 implementation +#include "Misc/FileHelper.h" +#include "Modules/ModuleManager.h" +#include "IImageWrapper.h" +#include "IImageWrapperModule.h" + +struct FToolsWrapper +{ + + static bool LoadImage(const TCHAR * Filename, FImage & OutImage) + { + // implementation adapted from: FSlateRHIResourceManager::LoadTexture + bool bSucceeded = false; + uint32 BytesPerPixel = 4; + + TArray RawFileData; + if( FFileHelper::LoadFileToArray( RawFileData, Filename ) ) + { + IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked( FName("ImageWrapper") ); + + //Try and determine format, if that fails assume PNG + EImageFormat ImageFormat = ImageWrapperModule.DetectImageFormat(RawFileData.GetData(), RawFileData.Num()); + if (ImageFormat == EImageFormat::Invalid) + { + ImageFormat = EImageFormat::PNG; + } + + TSharedPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(ImageFormat); + + if ( ImageWrapper.IsValid() && ImageWrapper->SetCompressed( RawFileData.GetData(), RawFileData.Num()) ) + { + OutImage.SizeX = ImageWrapper->GetWidth(); + OutImage.SizeY = ImageWrapper->GetHeight(); + OutImage.Format = DEFAULT_RAW_IMAGE_FORMAT_TYPE; + OutImage.NumSlices = 1; + + if (ImageWrapper->GetRaw( ERGBFormat::BGRA, 8, OutImage.RawData)) + { + bSucceeded = true; + } + else + { + HOUDINI_LOG_WARNING(TEXT("Invalid texture format for Slate resource only RGBA and RGB pngs are supported: %s"), Filename ); + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("Only pngs are supported in Slate.")); + } + } + else + { + HOUDINI_LOG_WARNING(TEXT("Could not find file: %s"), Filename); + } + + return bSucceeded; + } + + +}; + +#elif ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 +// Unreal 5.1+ implementation + +struct FToolsWrapper +{ + static bool LoadImage(const TCHAR * Filename, FImage & OutImage) + { + return FImageUtils::LoadImage(Filename, OutImage); + } +}; + +#endif + + +IAssetRegistry& +FHoudiniToolsRuntimeUtils::GetAssetRegistry() +{ + return FModuleManager::LoadModuleChecked("AssetRegistry").Get(); +} + + +FHoudiniToolsRuntimeUtils::FHoudiniToolsRuntimeUtils() +{ + +} + + +bool +FHoudiniToolsRuntimeUtils::GetHoudiniAssetJSONPath(const UHoudiniAsset* HoudiniAsset, FString& OutJSONFilePath) +{ + if (!IsValid(HoudiniAsset)) + { + return false; + } + + if (!IsValid(HoudiniAsset->AssetImportData)) + { + return false; + } + + // Compute the filepath of where we expect this HoudiniAsset's json file resides. + const FString& ReimportPath = HoudiniAsset->AssetImportData->GetFirstFilename(); + FString Path, Filename, Extension; + FPaths::Split(ReimportPath, Path, Filename, Extension); + OutJSONFilePath = FPaths::Combine(Path, Filename + ".json"); + + return true; +} + + +FString +FHoudiniToolsRuntimeUtils::GetPackageUAssetName() +{ + return TEXT("HoudiniToolsPackage"); +} + + +FString +FHoudiniToolsRuntimeUtils::GetPackageJSONName() +{ + return TEXT("HoudiniToolsPackage.json"); +} + + +FString +FHoudiniToolsRuntimeUtils::GetDefaultHoudiniAssetIconPath(const UHoudiniAsset* HoudiniAsset) +{ + if (!IsValid(HoudiniAsset)) + return FString(); + + if (!IsValid(HoudiniAsset->AssetImportData)) + return FString(); + + // Compute the filepath of where we expect this HoudiniAsset's json file resides. + const FString& ReimportPath = HoudiniAsset->AssetImportData->GetFirstFilename(); + FString Path, Filename, Extension; + FPaths::Split(ReimportPath, Path, Filename, Extension); + return FPaths::Combine(Path, Filename + ".png"); +} + + +FString +FHoudiniToolsRuntimeUtils::GetHoudiniToolsPackageJSONPath( + const UHoudiniToolsPackageAsset* ToolsPackage) +{ + if (!IsValid(ToolsPackage)) + { + return FString(); + } + + return FPaths::Combine(FPaths::GetPath(FPaths::GetProjectFilePath()), ToolsPackage->ExternalPackageDir.Path, GetPackageJSONName()); +} + + +bool +FHoudiniToolsRuntimeUtils::LoadJSONFile(const FString& JSONFilePath, TSharedPtr& OutJSONObject) +{ + if ( JSONFilePath.IsEmpty() ) + return false; + + // Read the file as a string + FString FileContents; + if ( !FFileHelper::LoadFileToString( FileContents, *JSONFilePath ) ) + { + HOUDINI_LOG_ERROR( TEXT("Failed to import Package Descriptor .json file: %s"), *JSONFilePath); + return false; + } + + // Parse it as JSON + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create( FileContents ); + if (!FJsonSerializer::Deserialize(Reader, OutJSONObject) || !OutJSONObject.IsValid()) + { + HOUDINI_LOG_ERROR( TEXT("Invalid json file: %s"), *JSONFilePath); + return false; + } + + return true; +} + + +bool +FHoudiniToolsRuntimeUtils::WriteJSONFile(const FString& JSONFilePath, const TSharedPtr JSONObject) +{ + // Output the JSON to a String + FString OutputString; + const TSharedRef< TJsonWriter<> > Writer = TJsonWriterFactory<>::Create(&OutputString); + FJsonSerializer::Serialize(JSONObject.ToSharedRef(), Writer); + + // Then write the output string to the json file itself + const FString SaveDirectory = FPaths::GetPath(JSONFilePath); + + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + // Returns true if the directory existed or has been created + if ( PlatformFile.CreateDirectoryTree(*SaveDirectory) ) + { + return FFileHelper::SaveStringToFile(OutputString, *JSONFilePath); + } + + return false; +} + + +bool +FHoudiniToolsRuntimeUtils::ToolTypeToString(const EHoudiniToolType ToolType, FString& OutString) +{ + switch(ToolType) + { + case EHoudiniToolType::HTOOLTYPE_GENERATOR: + { + OutString = TEXT("GENERATOR"); + return true; + } + case EHoudiniToolType::HTOOLTYPE_OPERATOR_SINGLE: + { + OutString = TEXT("OPERATOR_SINGLE"); + return true; + } + case EHoudiniToolType::HTOOLTYPE_OPERATOR_MULTI: + { + OutString = TEXT("OPERATOR_MULTI"); + return true; + } + case EHoudiniToolType::HTOOLTYPE_OPERATOR_BATCH: + { + OutString = TEXT("BATCH"); + return true; + } + default: + break; + } + return false; +} + + +bool +FHoudiniToolsRuntimeUtils::SelectionTypeToString(const EHoudiniToolSelectionType SelectionType, FString& OutString) +{ + switch (SelectionType) + { + case EHoudiniToolSelectionType::HTOOL_SELECTION_ALL: + { + OutString = TEXT("ALL"); + return true; + } + case EHoudiniToolSelectionType::HTOOL_SELECTION_CB_ONLY: + { + OutString = TEXT("CB"); + return true; + } + case EHoudiniToolSelectionType::HTOOL_SELECTION_WORLD_ONLY: + { + OutString = TEXT("CB"); + return true; + } + default: + break; + } + + return false; +} + + +UHoudiniToolsPackageAsset* +FHoudiniToolsRuntimeUtils::LoadHoudiniToolsPackage(const FString& PackageBasePath) +{ + // No need to load/find Tools package while cooking or running a commandlet + // This would only generate unneeded warnings + if (IsRunningCommandlet() || IsRunningCookCommandlet() || GIsCookerLoadingPackage) + return nullptr; + + const FString PkgPath = FPaths::Combine(PackageBasePath, FString::Format(TEXT("{0}.{0}"), { GetPackageUAssetName() }) ); + return LoadObject(nullptr, *PkgPath); +} + + +UHoudiniToolsPackageAsset* +FHoudiniToolsRuntimeUtils::FindOwningToolsPackage(const UHoudiniAsset* HoudiniAsset) +{ + if (!IsValid(HoudiniAsset)) + return nullptr; + + // No need to load/find Tools package while cooking or running a commandlet + // This would only generate unneeded warnings + if (IsRunningCommandlet() || IsRunningCookCommandlet() || GIsCookerLoadingPackage) + return nullptr; + + FString CurrentPath = FPaths::GetPath(HoudiniAsset->GetPathName()); + + // Define a depth limit to break out of the loop, in case something + // goes wrong and we get stuck. If ever needed, we can move this to a + // project setting, but for now we'll leave it here. + constexpr int LoopLimit = 100; + int LoopCount = 0; + + while (CurrentPath.Len() > 0) + { + // Try to load a package at the current path. + UHoudiniToolsPackageAsset* PackageAsset = LoadHoudiniToolsPackage(CurrentPath); + if (IsValid(PackageAsset)) + { + // We found a valid package asset. + return PackageAsset; + } + + // Proceed to the next parent directory. + CurrentPath = FPaths::GetPath(CurrentPath); + + LoopCount++; + if (LoopCount >= LoopLimit) + { + return nullptr; + } + } + + return nullptr; +} + + +UHoudiniToolData* +FHoudiniToolsRuntimeUtils::GetOrCreateHoudiniToolData(UHoudiniAsset* HoudiniAsset) +{ + UHoudiniToolData* ToolData = HoudiniAsset->HoudiniToolData; + + if (!IsValid(ToolData)) + { + HoudiniAsset->Modify(); + ToolData = NewObject(HoudiniAsset); + ToolData->Modify(); + HoudiniAsset->HoudiniToolData = ToolData; + } + + return ToolData; +} + + +bool +FHoudiniToolsRuntimeUtils::GetHoudiniPackageDescriptionFromJSON(const FString& JsonFilePath, + TMap& OutCategories, + bool& OutImportPackageDescription, + bool& OutExportPackageDescription, + bool& OutImportToolsDescription, + bool& OutExportToolsDescription + ) +{ + if ( JsonFilePath.IsEmpty() ) + return false; + + // Read the file as a string + FString FileContents; + if ( !FFileHelper::LoadFileToString( FileContents, *JsonFilePath ) ) + { + HOUDINI_LOG_ERROR( TEXT("Failed to import Package Descriptor .json file: %s"), *JsonFilePath); + return false; + } + + // Parse it as JSON + TSharedPtr JSONObject; + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create( FileContents ); + if (!FJsonSerializer::Deserialize(Reader, JSONObject) || !JSONObject.IsValid()) + { + HOUDINI_LOG_ERROR( TEXT("Invalid json in Houdini Tool .json file: %s"), *JsonFilePath); + return false; + } + + // Categories + OutCategories.Empty(); + + // Then, we need to make sure the json file has a correponding hda + // Start by looking at the assetPath field + FString HDAFilePath = FString(); + if ( JSONObject->HasField( TEXT("categories") ) ) + { + // If the json has the assetPath field, read it from there + TArray> CategoryEntries; + + const TArray>* CategoriesArray = nullptr; + if (JSONObject->TryGetArrayField(TEXT("categories"), CategoriesArray)) + { + for (auto Value : *CategoriesArray) + { + CategoryEntries.Add(Value->AsObject()); + } + } + + for(TSharedPtr& CategoryEntry : CategoryEntries) + { + FString CategoryName; + // Category rules that will be added to the output. + FCategoryRules OutCategoryRules; + + if (CategoryEntry->HasField(TEXT("name"))) + { + CategoryName = CategoryEntry->GetStringField(TEXT("name")); + } + else + { + HOUDINI_LOG_WARNING(TEXT("Found category entry without 'name' field. Skipping.")); + continue; + } + + if (!CategoryEntry->TryGetStringArrayField(TEXT("include"), OutCategoryRules.Include)) + { + // Categories should really have an include section, so we'll print a warning here. + HOUDINI_LOG_WARNING(TEXT("Could not parse 'include' string array for category '%s'."), *CategoryName); + } + + // Exclude sections are optional, so just quietly ignore it if its not present. + CategoryEntry->TryGetStringArrayField(TEXT("exclude"), OutCategoryRules.Exclude); + + OutCategories.Add(CategoryName, OutCategoryRules); + + HOUDINI_LOG_WARNING(TEXT("[GetHoudiniPackageDescriptionFromJSON] Category '%s': includes %d, excludes, %d"), *CategoryName, OutCategoryRules.Include.Num(), OutCategoryRules.Exclude.Num()); + } + } + + auto GetBoolFieldFn = [JSONObject](const FString& FieldName, const bool DefaultValue) -> bool + { + if ( JSONObject->HasField( FieldName ) ) + { + return JSONObject->GetBoolField(FieldName); + } + return DefaultValue; + }; + + // By default, we won't enable importing and exporting of JSON data data for both packages and tools. This + // prevents situations where "default use" of the plugin starts creating external files. Instead, for pipelines + // that prefer this behaviour, it can be easily enabled in the package descriptor. + OutImportPackageDescription = GetBoolFieldFn(TEXT("reimport_package_description"), false); + OutExportPackageDescription = GetBoolFieldFn(TEXT("export_package_description"), false); + OutImportToolsDescription = GetBoolFieldFn(TEXT("reimport_tools_description"), false); + OutExportToolsDescription = GetBoolFieldFn(TEXT("export_tools_description"), false); + return true; +} + + +TSharedPtr +FHoudiniToolsRuntimeUtils::MakeDefaultJSONObject(const UHoudiniAsset* HoudiniAsset) +{ + if (!IsValid(HoudiniAsset)) + return nullptr; + + // Start by building a JSON object from the tool + TSharedPtr JSONObject = MakeShareable(new FJsonObject); + + // Mark the target as unreal only + TArray< TSharedPtr > TargetValue; + TargetValue.Add(MakeShareable(new FJsonValueString(TEXT("unreal")))); + JSONObject->SetArrayField(TEXT("target"), TargetValue ); + + const UHoudiniToolData* ToolData = HoudiniAsset->HoudiniToolData; + const bool bValidToolData = IsValid(ToolData); + + // The Tool Name + const FString ToolName = bValidToolData ? ToolData->Name : FPaths::GetBaseFilename(HoudiniAsset->AssetFileName); + JSONObject->SetStringField(TEXT("name"), ToolName); + + // Tooltype + FString ToolTypeString; + const EHoudiniToolType ToolType = bValidToolData ? ToolData->Type : EHoudiniToolType::HTOOLTYPE_GENERATOR; + ToolTypeToString(ToolType, ToolTypeString); + JSONObject->SetStringField(TEXT("toolType"), ToolTypeString); + + // Tooltip + if ( bValidToolData && !ToolData->ToolTip.IsEmpty() ) + JSONObject->SetStringField(TEXT("toolTip"), ToolData->ToolTip); + + // Help URL + if ( bValidToolData && !ToolData->HelpURL.IsEmpty() ) + JSONObject->SetStringField(TEXT("helpURL"), ToolData->HelpURL); + + // // IconPath + // if ( bValidToolData && !ToolData->IconResource ) + // { + // const FString IconPath = ToolData->IconResource->GetName(); + // JSONObject->SetStringField(TEXT("iconPath"), IconPath); + // } + + // Selection Type + FString SelectionTypeString; + const EHoudiniToolSelectionType SelectionType = bValidToolData ? ToolData->SelectionType : EHoudiniToolSelectionType::HTOOL_SELECTION_ALL; + FHoudiniToolsRuntimeUtils::SelectionTypeToString(SelectionType, SelectionTypeString); + JSONObject->SetStringField(TEXT("UE_SelectionType"), SelectionTypeString); + + return JSONObject; +} + + +TSharedPtr +FHoudiniToolsRuntimeUtils::MakeDefaultJSONObject(const UHoudiniToolsPackageAsset* ToolsPackage) +{ + if (!IsValid(ToolsPackage)) + return nullptr; + + // A default JSON file for a tools package will contain a single category using the directory + // name of the ToolsPackage. + // All reimport/export settings will be disabled by default. + + FString AssetPath = FPaths::GetPath(ToolsPackage->GetPathName()); + + const FString PackageName = TEXT("PackageName"); + + // Start by building a JSON object from the tool + TSharedPtr JSONObject = MakeShareable(new FJsonObject); + + // Add an array for category entries + TArray< TSharedPtr > CategoryEntries; + + // Populate a default category with an include entry, and an empty exclude list. + const TSharedPtr CategoryEntry = MakeShareable(new FJsonObject); + CategoryEntry->SetStringField(TEXT("name"), PackageName); + + TArray< TSharedPtr > IncludeValues; + IncludeValues.Add(MakeShareable( new FJsonValueString(TEXT("*")) )); + CategoryEntry->SetArrayField(TEXT("include"), IncludeValues); + + const TArray< TSharedPtr > ExcludeValues; + CategoryEntry->SetArrayField(TEXT("exclude"), ExcludeValues); + + CategoryEntries.Add( MakeShareable( new FJsonValueObject(CategoryEntry) ) ); + + // Add the default category entry. + JSONObject->SetArrayField(TEXT("categories"), CategoryEntries); + + // Set defaults for the reimport/export settings. + + JSONObject->SetBoolField(TEXT("export_package_description"), false); + JSONObject->SetBoolField(TEXT("reimport_package_description"), false); + + JSONObject->SetBoolField(TEXT("reimport_tools_description"), false); + JSONObject->SetBoolField(TEXT("export_tools_description"), false); + + return JSONObject; +} + + +bool +FHoudiniToolsRuntimeUtils::WriteDefaultJSONFile(const UHoudiniAsset* HoudiniAsset, const FString& JSONFilePath) +{ + if (!IsValid(HoudiniAsset)) + { + return false; + } + + const TSharedPtr JSONObject = MakeDefaultJSONObject(HoudiniAsset); + if (!JSONObject.IsValid()) + { + return false; + } + + return WriteJSONFile(JSONFilePath, JSONObject); +} + + +bool +FHoudiniToolsRuntimeUtils::WriteDefaultJSONFile(const UHoudiniToolsPackageAsset* ToolsPackage, const FString& JSONFilePath) +{ + if (!IsValid(ToolsPackage)) + { + return false; + } + + const TSharedPtr JSONObject = MakeDefaultJSONObject(ToolsPackage); + if (!JSONObject.IsValid()) + { + return false; + } + + return WriteJSONFile(JSONFilePath, JSONObject); +} + + +bool +FHoudiniToolsRuntimeUtils::WriteJSONFromHoudiniAsset(const UHoudiniAsset* HoudiniAsset, const FString& JSONFilePath) +{ + if (!IsValid(HoudiniAsset)) + { + return false; + } + + TSharedPtr JSONObject; + if (FPaths::FileExists(JSONFilePath)) + { + // This asset already has a JSON file. + if (!LoadJSONFile(JSONFilePath, JSONObject)) + { + // We were unable to parse it, so we'll fail here. We don't want to overwrite + // files if we can't preserve its contents. + HOUDINI_LOG_ERROR(TEXT("Unable to parse JSON file. We don't want to overwrite and possibly lose data. Skipping JSON data export for this asset.")); + return false; + } + } + + if (!JSONObject.IsValid()) + { + // We don't have a valid JSON object, it means we should be creating a new default JSON object for this asset + JSONObject = MakeDefaultJSONObject(HoudiniAsset); + if (!JSONObject.IsValid()) + { + return false; + } + } + + // NOTE: If we start writing out HDA JSON files in multiple places we should move the following + // into a wrapper class with an specific accessors so that the field manipulations can be unified in a single place. + + FString Name; + FString Tooltip; + EHoudiniToolType ToolType; + EHoudiniToolSelectionType SelectionType; + FString HelpURL; + FString IconSourcePath; + + // Populate the JSON object from the Houdini Asset. + if (IsValid(HoudiniAsset->HoudiniToolData)) + { + // Populate from ToolData (this is our preferred data source) + UHoudiniToolData* ToolData = HoudiniAsset->HoudiniToolData; + + Name = ToolData->Name; + Tooltip = ToolData->ToolTip; + // We don't really know what this HDA is, so we're just going with "no inputs" as a default. + ToolType = ToolData->Type; + SelectionType = ToolData->SelectionType; + HelpURL = ToolData->HelpURL; + IconSourcePath = ToolData->IconSourcePath.FilePath; + } + else + { + // Populate from any data we can extract from UHoudiniAsset without looking outside the Unreal project. + // i.e., no peeking at external json files. + Name = FPaths::GetBaseFilename(HoudiniAsset->AssetFileName); + Tooltip = Name; + // We don't really know what this HDA is, so we're just going with "no inputs" as a default. + ToolType = EHoudiniToolType::HTOOLTYPE_GENERATOR; + SelectionType = EHoudiniToolSelectionType::HTOOL_SELECTION_ALL; + } + + // Mark the target as unreal only + TArray< TSharedPtr > TargetValue; + TargetValue.Add(MakeShareable(new FJsonValueString(TEXT("unreal")))); + JSONObject->SetArrayField(TEXT("target"), TargetValue ); + + // Tool Name + if ( !Name.IsEmpty() ) + { + JSONObject->SetStringField(TEXT("name"), Name); + } + + // Tool Type + FString ToolTypeString = TEXT("GENERATOR"); + if ( ToolType == EHoudiniToolType::HTOOLTYPE_OPERATOR_SINGLE) + ToolTypeString = TEXT("OPERATOR_SINGLE"); + else if (ToolType == EHoudiniToolType::HTOOLTYPE_OPERATOR_MULTI) + ToolTypeString = TEXT("OPERATOR_MULTI"); + else if ( ToolType == EHoudiniToolType::HTOOLTYPE_OPERATOR_BATCH) + ToolTypeString = TEXT("BATCH"); + + JSONObject->SetStringField(TEXT("toolType"), ToolTypeString); + + // Tooltip + JSONObject->SetStringField(TEXT("toolTip"), Tooltip); + + // Help URL + JSONObject->SetStringField(TEXT("helpURL"), HelpURL); + + // IconPath + bool bRemoveIconPath = true; + if ( !IconSourcePath.IsEmpty() ) + { + // Only write out the icon path if it differs from the 'default' icon path for the asset. + const FString DefaultIconPath = GetDefaultHoudiniAssetIconPath(HoudiniAsset); + if (IconSourcePath.Compare(DefaultIconPath) != 0) + { + JSONObject->SetStringField(TEXT("iconPath"), IconSourcePath); + bRemoveIconPath = false; + } + } + if (bRemoveIconPath) + { + JSONObject->RemoveField(TEXT("iconPath")); + } + + // Selection Type + FString SelectionTypeString = TEXT("All"); + if (SelectionType == EHoudiniToolSelectionType::HTOOL_SELECTION_CB_ONLY) + SelectionTypeString = TEXT("CB"); + else if (SelectionType == EHoudiniToolSelectionType::HTOOL_SELECTION_WORLD_ONLY) + SelectionTypeString = TEXT("World"); + JSONObject->SetStringField(TEXT("UE_SelectionType"), SelectionTypeString); + + WriteJSONFile(JSONFilePath, JSONObject); + + return true; +} + + +bool FHoudiniToolsRuntimeUtils::WriteJSONFromToolPackageAsset(const UHoudiniToolsPackageAsset* ToolsPackage, + const FString& JSONFilePath) +{ + TSharedPtr JSONObject; + if (FPaths::FileExists(JSONFilePath)) + { + // This asset already has a JSON file. + if (!LoadJSONFile(JSONFilePath, JSONObject)) + { + // We were unable to parse it, so we'll fail here. We don't want to override + // files if we can't preserve its contents. + return false; + } + } + + if (!JSONObject.IsValid()) + { + // We don't have a valid JSON object, it means we should be creating a new default JSON object for this asset + JSONObject = MakeDefaultJSONObject(ToolsPackage); + if (!JSONObject.IsValid()) + { + return false; + } + } + + // Populate the JSON Object from the asset + + if (ToolsPackage->Categories.Num() > 0) + { + // Add an array for category entries + TArray< TSharedPtr > CategoryValues; + + for( auto Entry : ToolsPackage->Categories) + { + // Create a Category entry + const FString& CategoryName = Entry.Key; + const FCategoryRules& CategoryRules = Entry.Value; + + TSharedPtr CategoryObject = MakeShareable(new FJsonObject); + CategoryObject->SetStringField(TEXT("name"), CategoryName); + + // include patterns + if (CategoryRules.Include.Num() > 0) + { + TArray< TSharedPtr > IncludeValues; + for (const FString& Pattern : CategoryRules.Include) + { + IncludeValues.Add(MakeShareable( new FJsonValueString(Pattern) )); + } + CategoryObject->SetArrayField(TEXT("include"), IncludeValues); + } + + // exclude patterns + if (CategoryRules.Include.Num() > 0) + { + TArray< TSharedPtr > ExcludeValues; + for (const FString& Pattern : CategoryRules.Exclude) + { + ExcludeValues.Add(MakeShareable( new FJsonValueString(Pattern) )); + } + CategoryObject->SetArrayField(TEXT("exclude"), ExcludeValues); + } + + CategoryValues.Add(MakeShareable(new FJsonValueObject(CategoryObject))); + } + + JSONObject->SetArrayField(TEXT("categories"), CategoryValues); + } + + JSONObject->SetBoolField(TEXT("export_package_description"), ToolsPackage->bExportPackageDescription); + JSONObject->SetBoolField(TEXT("reimport_package_description"), ToolsPackage->bReimportPackageDescription); + + JSONObject->SetBoolField(TEXT("export_tools_description"), ToolsPackage->bReimportToolsDescription); + JSONObject->SetBoolField(TEXT("reimport_tools_description"), ToolsPackage->bExportToolsDescription); + + WriteJSONFile(JSONFilePath, JSONObject); + + return true; +} + +void FHoudiniToolsRuntimeUtils::PadImage(const FImage& SrcImage, FImage& DestImage, const int32 NumPixels) +{ +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0 + const int32 BytesPerPixel = SrcImage.GetBytesPerPixel(); +#else + const int32 BytesPerPixel = ERawImageFormat::GetBytesPerPixel(SrcImage.Format); +#endif + + DestImage.SizeX = SrcImage.SizeX + NumPixels*2; + DestImage.SizeY = SrcImage.SizeY + NumPixels*2; + DestImage.NumSlices = 1; + DestImage.Format = SrcImage.Format; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0 + DestImage.GammaSpace = SrcImage.GammaSpace; +#else + DestImage.GammaSpace = SrcImage.GetGammaSpace(); +#endif + + // Init dest image storage +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0 + const int64 NumBytes = DestImage.SizeX * DestImage.SizeY * DestImage.NumSlices * BytesPerPixel; +#else + const int64 NumBytes = DestImage.GetImageSizeBytes(); +#endif + DestImage.RawData.Empty(NumBytes); + DestImage.RawData.AddZeroed(NumBytes); + + // Copy the source image into the center of the destination image + const int32 SourceRowBytes = SrcImage.SizeX * BytesPerPixel; + for (int Row = 0; Row < SrcImage.SizeY; Row++) + { + const int SourceIndex = (Row * SrcImage.SizeX) * BytesPerPixel; + const int DestIndex = (NumPixels + ((Row+NumPixels) * DestImage.SizeX)) * BytesPerPixel; + memcpy(&DestImage.RawData[DestIndex], &SrcImage.RawData[SourceIndex], SourceRowBytes ); + } +} + + +bool +FHoudiniToolsRuntimeUtils::LoadFHImageFromFile(const FString& ImagePath, FHImageData& OutImageData) +{ + const FString FullPath = FPaths::ConvertRelativePathToFull(ImagePath); + bool bSuccess = false; + + if (FPaths::FileExists(FullPath)) + { + FImage TmpImage; + if (FToolsWrapper::LoadImage(*FullPath, TmpImage)) + { + OutImageData.FromImage(TmpImage); + return true; + } + } + return false; +} + + +void +FHoudiniToolsRuntimeUtils::UpdateHoudiniAssetThumbnail(UHoudiniAsset* HoudiniAsset) +{ + if (!IsValid(HoudiniAsset) || !IsValid(HoudiniAsset->HoudiniToolData)) + { + return; + } + + const FHImageData& IconImageData = HoudiniAsset->HoudiniToolData->IconImageData; + + UpdateAssetThumbnailFromImageData(HoudiniAsset, HoudiniAsset->HoudiniToolData->IconImageData); +} + + +void +FHoudiniToolsRuntimeUtils::UpdateAssetThumbnailFromImageData(UObject* Asset, const FHImageData& ImageData, const float MarginPct) +{ + if (!IsValid(Asset)) + { + return; + } + + const FString ObjectFullName = Asset->GetFullName(); + UPackage* AssetPackage = Asset->GetPackage(); + if (!IsValid(AssetPackage)) + { + return; + } + + if (ImageData.SizeX == 0 || ImageData.SizeY == 0) + { + // Clear the thumbnail + ThumbnailTools::CacheEmptyThumbnail(ObjectFullName, AssetPackage); + return; + } + + if (ImageData.SizeX == 0 || ImageData.SizeY == 0) + { + // Clear the thumbnail + ThumbnailTools::CacheEmptyThumbnail(ObjectFullName, AssetPackage); + return; + } + + FImage SrcImage; + ImageData.ToImage(SrcImage); + + // We can expose the margin as a parameter if we want/need to. + if (MarginPct > 0.f) + { + // We can't configure a margin on the FImage, so we'll pad the image with + // zero-byte pixels to give the illusion that we're applying a margin. + FImage PaddedImage; + PadImage(SrcImage, PaddedImage, SrcImage.SizeX*MarginPct); + SrcImage = PaddedImage; + } + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0 + const int32 BytesPerPixel = SrcImage.GetBytesPerPixel(); +#else + const int32 BytesPerPixel = ERawImageFormat::GetBytesPerPixel(SrcImage.Format); +#endif + + FObjectThumbnail TempThumbnail; + TempThumbnail.SetImageSize( SrcImage.SizeX, SrcImage.SizeY ); + TArray& ThumbnailData = TempThumbnail.AccessImageData(); + + const int32 NumBytes = SrcImage.SizeX * SrcImage.SizeY * BytesPerPixel * SrcImage.NumSlices; + + ThumbnailData.AddUninitialized(NumBytes); + FMemory::Memcpy(&(ThumbnailData[0]), &(SrcImage.RawData[0]), NumBytes); + + FObjectThumbnail* NewThumbnail = ThumbnailTools::CacheThumbnail(ObjectFullName, &TempThumbnail, AssetPackage); + if ( ensure(NewThumbnail) ) + { + //we need to indicate that the package needs to be resaved + AssetPackage->MarkPackageDirty(); + + // Let the content browser know that we've changed the thumbnail + NewThumbnail->MarkAsDirty(); + + // Signal that the asset was changed so thumbnail pools will update + Asset->PostEditChange(); + + //Set that thumbnail as a valid custom thumbnail so it'll be saved out + NewThumbnail->SetCreatedAfterCustomThumbsEnabled(); + } +} + +bool +FHoudiniToolsRuntimeUtils::ShowToolsPackageRenameConfirmDialog() +{ + FText WarningMessage(LOCTEXT("Warning_ToolsPackageRename", "Naming a HoudiniToolsPackage to anything but \"HoudiniToolsPackage\" disables it - Are you sure you want to proceed with the rename?")); + EAppReturnType::Type ReturnType = FMessageDialog::Open(EAppMsgType::YesNo, WarningMessage);// , & MessageTitle); + if (ReturnType == EAppReturnType::Yes) + { + return true; + } + else + { + return false; + } +} + +#endif + +#undef DEFAULT_GAMMA_SPACE +#undef DEFAULT_RAW_IMAGE_FORMAT_TYPE +#undef LOCTEXT_NAMESPACE + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniToolsRuntimeUtils.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "HoudiniAsset.h" +#include "HoudiniToolData.h" +#include "HoudiniToolsPackageAsset.h" +#include "HoudiniToolTypes.h" +#include "ImageCore.h" +#include "AssetRegistry/AssetRegistryModule.h" + +// ----------------------------- +// Houdini Tools +// ----------------------------- +// Houdini Tools maintains a cache of packages, tools, and categories. It also provided accessors for any UI +// tools to access this data + +class FJsonObject; +struct FHoudiniToolList; +struct FSlateBrush; +class UHoudiniAsset; + +struct FFilePath; +enum class EHoudiniToolSelectionType : uint8; +enum class EHoudiniToolType : uint8; +struct FHoudiniToolDirectory; +struct FHoudiniTool; +class UHoudiniToolsPackageAsset; + +class HOUDINIENGINERUNTIME_API FHoudiniToolsRuntimeUtils +{ + protected: + static IAssetRegistry& GetAssetRegistry(); + +public: + FHoudiniToolsRuntimeUtils(); + +#if WITH_EDITOR + // DECLARE_MULTICAST_DELEGATE(FOnToolChanged) + + + + // Compute the file path of where we expect this HoudiniAsset's JSON file resides. + static bool GetHoudiniAssetJSONPath(const UHoudiniAsset* HoudiniAsset, FString& OutJSONFilePath); + + static FString GetPackageUAssetName(); + static FString GetPackageJSONName(); + + // Compute the default file path of where we expect this HoudiniAsset's JSON file resides. + static FString GetDefaultHoudiniAssetIconPath(const UHoudiniAsset* HoudiniAsset); + + static FString GetHoudiniToolsPackageJSONPath(const UHoudiniToolsPackageAsset* ToolsPackage); + + static bool LoadJSONFile(const FString& JSONFilePath, TSharedPtr& OutJSONObject); + static bool WriteJSONFile(const FString& JSONFilePath, const TSharedPtr JSONObject); + + // Convert between enums and string values used in JSON files + static bool ToolTypeToString(const EHoudiniToolType ToolType, FString& OutString); + static bool SelectionTypeToString(const EHoudiniToolSelectionType SelectionType, FString& OutString); + + static UHoudiniToolsPackageAsset* LoadHoudiniToolsPackage(const FString& PackageBasePath); + + // From the current HoudiniAsset, search up the directory tree to see if we can find a + // HoudiniToolsPackageAsset, since HoudiniAssets can live in subfolders in a package. + // Returns the first (closest) HoudiniToolsPackageAsset found. + static UHoudiniToolsPackageAsset* FindOwningToolsPackage(const UHoudiniAsset* HoudiniAsset); + + // Return the UHoudiniToolData object on the HoudiniAsset. If it doesn't exist, create it. + static UHoudiniToolData* GetOrCreateHoudiniToolData(UHoudiniAsset* HoudiniAsset); + + // External Data + + // Read HDA description + static bool GetHoudiniPackageDescriptionFromJSON( + const FString& JsonFilePath, + TMap& OutCategories, + bool& OutImportPackageDescription, bool& OutExportPackageDescription, bool& OutImportToolsDescription, bool& + OutExportToolsDescription + ); + + // Make a default JSON object from the given Houdini Asset. + static TSharedPtr MakeDefaultJSONObject(const UHoudiniAsset* HoudiniAsset); + + // Make a default JSON object from the given Houdini Tools Package. + static TSharedPtr MakeDefaultJSONObject(const UHoudiniToolsPackageAsset* ToolsPackage); + + // Write a default JSON file for the given HoudiniAsset + static bool WriteDefaultJSONFile(const UHoudiniAsset* HoudiniAsset, const FString& JSONFilePath); + + // Write a default JSON file for the given HoudiniToolPackageAsset + static bool WriteDefaultJSONFile(const UHoudiniToolsPackageAsset* ToolsPackage, const FString& JSONFilePath); + + // Use all the data that can be extracted from the UHoudiniAsset to write out a JSON + // file, while preserving any existing JSON data. + static bool WriteJSONFromHoudiniAsset(const UHoudiniAsset* HoudiniAsset, const FString& JSONFilePath); + + // Use all the data that can be extracted from the UHoudiniAsset to write out a JSON + // file, while preserving any existing JSON data. + static bool WriteJSONFromToolPackageAsset(const UHoudiniToolsPackageAsset* ToolsPackage, const FString& JSONFilePath); + + // -------------------------------- + // Icons and Thumbnails + // -------------------------------- + + static void PadImage(const FImage& SrcImage, FImage& DestImage, const int32 NumPixels); + + static bool LoadFHImageFromFile(const FString& ImagePath, FHImageData& OutImageData); + + // Cache the HoudiniAsset's icon, if it has one, with the Thumbnail Manager. + static void UpdateHoudiniAssetThumbnail(UHoudiniAsset* HoudiniAsset); + static void UpdateAssetThumbnailFromImageData(UObject* Asset, const FHImageData& ImageData, const float MarginPct=0.2f); + + static bool ShowToolsPackageRenameConfirmDialog(); + +#endif +}; + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniTranslatorTypes.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "HoudiniTranslatorTypes.h" + +FHoudiniLandscapeTileSizeInfo::FHoudiniLandscapeTileSizeInfo() + : bIsCached(false) + , UnrealSizeX(-1) + , UnrealSizeY(-1) + , NumSectionsPerComponent(-1) + , NumQuadsPerSection(-1) +{ + +} + +FHoudiniLandscapeExtent::FHoudiniLandscapeExtent() + : bIsCached(false) + , MinX(0), MaxX(0), MinY(0), MaxY(0) + , ExtentsX(0) + , ExtentsY(0) +{ + +} + +FHoudiniLandscapeReferenceLocation::FHoudiniLandscapeReferenceLocation() + : bIsCached(false) + , SectionCoordX(0) + , SectionCoordY(0) + , TileLocationX(0.f) + , TileLocationY(0.f) +{ +} + +``` + +`Source/HoudiniEngineRuntime/Private/HoudiniTranslatorTypes.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniEngineRuntimePrivatePCH.h" + +class ALandscape; + +// Used to cache data to use as a reference point during landscape construction +// The very first tile will calculate landscape +struct HOUDINIENGINERUNTIME_API FHoudiniLandscapeTileSizeInfo +{ + FHoudiniLandscapeTileSizeInfo(); + bool bIsCached; + + // Tile sizes + int32 UnrealSizeX; + int32 UnrealSizeY; + int32 NumSectionsPerComponent; + int32 NumQuadsPerSection; +}; + +// Used to cache the extent of the landscape so that it doesn't have to be recalculated +// for each landscape tile. +// The very first tile will calculate landscape +struct HOUDINIENGINERUNTIME_API FHoudiniLandscapeExtent +{ + FHoudiniLandscapeExtent(); + bool bIsCached; + + // Landscape extents (in quads) + int32 MinX, MaxX, MinY, MaxY; + int32 ExtentsX; + int32 ExtentsY; +}; + +// Used to cache data to use as a reference point during landscape construction +// The very first tile will calculate a reference point as well as a component-space location. +// Every subsequent tile can then derive a component-space location from this location. +struct HOUDINIENGINERUNTIME_API FHoudiniLandscapeReferenceLocation +{ + FHoudiniLandscapeReferenceLocation(); + + bool bIsCached; + // Absolute section base coordinate for the cached tile. + int32 SectionCoordX; + int32 SectionCoordY; + // Scaled location for the reference tile. + float TileLocationX; + float TileLocationY; + // Transform of the main landscape actor. + FTransform MainTransform; +}; + +// Used to store cached spatial data. See above structs for more detail. +struct HOUDINIENGINERUNTIME_API FHoudiniLandscapeSpatialData +{ + FHoudiniLandscapeExtent Extent; + FHoudiniLandscapeTileSizeInfo TileSizeInfo; + FHoudiniLandscapeReferenceLocation ReferenceLocation; +}; + +``` + +`Source/HoudiniEngineRuntime/Private/IHoudiniAssetStateEvents.cpp`: + +```cpp +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "IHoudiniAssetStateEvents.h" + +void +IHoudiniAssetStateEvents::HandleOnHoudiniAssetStateChange(UObject* InHoudiniAssetContext, const EHoudiniAssetState InFromState, const EHoudiniAssetState InToState) +{ + if (InFromState == InToState) + return; + + FOnHoudiniAssetStateChange& StateChangeDelegate = GetOnHoudiniAssetStateChangeDelegate(); + if (StateChangeDelegate.IsBound()) + StateChangeDelegate.Broadcast(InHoudiniAssetContext, InFromState, InToState); +} + +``` + +`Source/HoudiniEngineRuntime/Private/IHoudiniAssetStateEvents.h`: + +```h +/* +* Copyright (c) <2018> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" + +#include "HoudiniAssetStateTypes.h" + +#include "IHoudiniAssetStateEvents.generated.h" + +// Delegate for when EHoudiniAssetState changes from InFromState to InToState on an instantiated Houdini Asset (InHoudiniAssetContext). +DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnHoudiniAssetStateChange, UObject*, const EHoudiniAssetState, const EHoudiniAssetState); + +UINTERFACE() +class HOUDINIENGINERUNTIME_API UHoudiniAssetStateEvents : public UInterface +{ + GENERATED_BODY() +}; + + +/** + * EHoudiniAssetState events: event handlers for when a Houdini Asset changes state. + */ +class HOUDINIENGINERUNTIME_API IHoudiniAssetStateEvents +{ + GENERATED_BODY() + +public: + virtual void HandleOnHoudiniAssetStateChange(UObject* InHoudiniAssetContext, const EHoudiniAssetState InFromState, const EHoudiniAssetState InToState); + + virtual FOnHoudiniAssetStateChange& GetOnHoudiniAssetStateChangeDelegate() = 0; +}; + +``` + +`Source/HoudiniEngineRuntime/Private/Tests/HoudiniRuntimeTests.cpp`: + +```cpp +// Fill out your copyright notice in the Description page of Project Settings. + +#if WITH_DEV_AUTOMATION_TESTS + +#include "HoudiniRuntimeTests.h" +#include "HoudiniEngineRuntime.h" +#include "Misc/AutomationTest.h" + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(HoudiniRuntimeTestAutomation, "Houdini.Runtime.TestAutomation", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) + +bool HoudiniRuntimeTestAutomation::RunTest(const FString & Parameters) +{ + + return true; +} + +#endif +``` + +`Source/HoudiniEngineRuntime/Private/Tests/HoudiniRuntimeTests.h`: + +```h +#pragma once + +#if WITH_DEV_AUTOMATION_TESTS + +#include "CoreMinimal.h" + +#endif + +``` + +`Source/HoudiniEngineRuntime/Private/UnrealObjectInputManager.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealObjectInputManager.h" +#include "HoudiniEngineRuntimePrivatePCH.h" + +FUnrealObjectInputManager* FUnrealObjectInputManager::Singleton = nullptr; + +FUnrealObjectInputManager::~FUnrealObjectInputManager() {} + +bool +FUnrealObjectInputManager::SetSingleton(FUnrealObjectInputManager* InImplementation) +{ + Singleton = InImplementation; + return Singleton != nullptr; +} + +bool +FUnrealObjectInputManager::DestroySingleton() +{ + if (!Singleton) + return false; + + delete Singleton; + Singleton = nullptr; + + return true; +} + +FUnrealObjectInputManager* FUnrealObjectInputManager::Get() +{ + if (Singleton == nullptr) + { + // This should not happen! + HOUDINI_LOG_ERROR(TEXT("FUnrealObjectInputManager not initialized correctly!!!!!!!!!!!!!!!!!!!!")); + HOUDINI_LOG_ERROR(TEXT("Plugin will not function correctly !!!!!!!!!!!!!!!!!!!!")); + SetSingleton(new FUnrealObjectInputManager()); + } + return Singleton; +} + + +bool +FUnrealObjectInputManager::FindNode(const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutHandle) const +{ + return false; +} + +bool +FUnrealObjectInputManager::Contains(const FUnrealObjectInputHandle& InHandle) const +{ + return false; +} + +bool +FUnrealObjectInputManager::Contains(const FUnrealObjectInputIdentifier& InIdentifier) const +{ + return false; +} + +bool +FUnrealObjectInputManager::GetNode(const FUnrealObjectInputHandle& InHandle, const FUnrealObjectInputNode*& OutNode) const +{ + return false; +} + +bool +FUnrealObjectInputManager::GetNode(const FUnrealObjectInputHandle& InHandle, FUnrealObjectInputNode*& OutNode) const +{ + return false; +} + +bool +FUnrealObjectInputManager::AddContainer(const FUnrealObjectInputIdentifier& InIdentifier, const int32 InNodeId, FUnrealObjectInputHandle& OutHandle) +{ + return false; +} + +bool +FUnrealObjectInputManager::AddReferenceNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InObjectNodeId, + const int32 InNodeId, + FUnrealObjectInputHandle& OutHandle, + TSet const* const InReferencedNodes, + const int32 InReferencesConnectToNodeId) +{ + return false; +} + +bool +FUnrealObjectInputManager::AddLeaf( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InObjectNodeId, + const int32 InNodeId, + FUnrealObjectInputHandle& OutHandle) +{ + return false; +} + +bool +FUnrealObjectInputManager::UpdateContainer( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InNodeId, + const bool bInClearDirtyFlag) +{ + return false; +} + +bool +FUnrealObjectInputManager::UpdateReferenceNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const TOptional InObjectNodeId, + const TOptional InNodeId, + TSet const* const InReferencedNodes, + const TOptional InReferencesConnectToNodeId, + const bool bInClearDirtyFlag) +{ + return false; +} + +bool +FUnrealObjectInputManager::UpdateLeaf( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InObjectNodeId, + const int32 InNodeId, + const bool bInClearDirtyFlag) +{ + return false; +} + +FString +FUnrealObjectInputManager::GetDefaultNodeName(const FUnrealObjectInputIdentifier& InIdentifier) const +{ + return FString(); +} + +bool +FUnrealObjectInputManager::GetUniqueHoudiniNodeId(const int32 InHAPINodeId, int32& OutUniqueHoudiniNodeId) const +{ + return false; +} + +bool +FUnrealObjectInputManager::AreHAPINodesValid(const FUnrealObjectInputIdentifier& InIdentifier) const +{ + return false; +} + +bool +FUnrealObjectInputManager::IsHAPINodeValid(const FUnrealObjectInputHAPINodeId& InNodeId) const +{ + return false; +} + +bool +FUnrealObjectInputManager::DeleteHAPINode(FUnrealObjectInputHAPINodeId& InNodeId) const +{ + return false; +} + +bool +FUnrealObjectInputManager::SetHAPINodeDisplay(const FUnrealObjectInputHAPINodeId& InNodeId, const bool bInOnOff) const +{ + return false; +} + +bool +FUnrealObjectInputManager::SetHAPINodeDisplay(const int32 InNodeId, const bool bInOnOff) const +{ + return false; +} + +bool +FUnrealObjectInputManager::GetHAPINodeIds(const FUnrealObjectInputIdentifier& InIdentifier, TArray& OutNodeIds) const +{ + return false; +} + +bool +FUnrealObjectInputManager::GetHAPINodeIds(const FUnrealObjectInputIdentifier& InIdentifier, TArray& OutNodeIds) const +{ + return false; +} + +bool +FUnrealObjectInputManager::GetAllHAPINodeIds(TArray& OutNodeIds) const +{ + return false; +} + +bool +FUnrealObjectInputManager::GetAllHAPINodeIds(TArray& OutNodeIds) const +{ + return false; +} + +bool +FUnrealObjectInputManager::EnsureParentsExist(const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutParentHandle, const bool& bInputNodesCanBeDeleted) +{ + return false; +} + +bool +FUnrealObjectInputManager::IsDirty(const FUnrealObjectInputIdentifier& InIdentifier) const +{ + return false; +} + +bool +FUnrealObjectInputManager::MarkAsDirty(const FUnrealObjectInputIdentifier& InIdentifier, const bool bInAlsoDirtyReferencedNodes) +{ + return false; +} + +bool +FUnrealObjectInputManager::ClearDirtyFlag(const FUnrealObjectInputIdentifier& InIdentifier) +{ + return false; +} + +bool +FUnrealObjectInputManager::Clear() +{ + return false; +} + +FUnrealObjectInputHAPINodeId +FUnrealObjectInputManager::GetWorldOriginNodeId(const bool bInCreateIfMissingOrInvalid) +{ + return FUnrealObjectInputHAPINodeId(); +} + +int32 +FUnrealObjectInputManager::GetWorldOriginHAPINodeId(const bool bInCreateIfMissingOrInvalid) +{ + return -1; +} + +bool +FUnrealObjectInputManager::AddRef(const FUnrealObjectInputIdentifier& InIdentifier) +{ + return false; +} + +bool +FUnrealObjectInputManager::RemoveRef(const FUnrealObjectInputIdentifier& InIdentifier) +{ + return false; +} + +bool +FUnrealObjectInputManager::AddBackLink(const FUnrealObjectInputIdentifier& InReferencedIdentifier, const FUnrealObjectInputIdentifier& InReferencedBy) +{ + return false; +} + +bool +FUnrealObjectInputManager::RemoveBackLink(const FUnrealObjectInputIdentifier& InReferencedIdentifier, const FUnrealObjectInputIdentifier& InReferencedBy) +{ + return false; +} + +bool +FUnrealObjectInputManager::GetReferencedBy(const FUnrealObjectInputIdentifier& InReferencedIdentifier, TSet& OutReferencedBy) const +{ + return false; +} + +FUnrealObjectInputManager::FOnNodeAddUpdateDelete& +FUnrealObjectInputManager::GetOnNodeAddedDelegate() +{ + static FOnNodeAddUpdateDelete Dummy; + return Dummy; +} + +FUnrealObjectInputManager::FOnNodeAddUpdateDelete& +FUnrealObjectInputManager::GetOnNodeUpdatedDelegate() +{ + static FOnNodeAddUpdateDelete Dummy; + return Dummy; +} + +FUnrealObjectInputManager::FOnNodeAddUpdateDelete& +FUnrealObjectInputManager::GetOnNodeDeletedDelegate() +{ + static FOnNodeAddUpdateDelete Dummy; + return Dummy; +} + +void FUnrealObjectInputManager::Dump() +{ + +} + +``` + +`Source/HoudiniEngineRuntime/Private/UnrealObjectInputManager.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "UnrealObjectInputRuntimeTypes.h" +//#include "HoudiniOutput.h" +//#include "UnrealObjectInputTypes.h" + +// #include "UnrealObjectInputManager.generated.h" + +class FUnrealObjectInputOptions; +class FUnrealObjectInputHandle; +class FUnrealObjectInputNode; + +class FUnrealObjectInputIdentifier; + +class HOUDINIENGINERUNTIME_API FUnrealObjectInputManager +{ +public: + /** Multicast delegate type to be used for notifications when a node entry in the manager is added, updated or deleted. */ + DECLARE_MULTICAST_DELEGATE_OneParam(FOnNodeAddUpdateDelete, const FUnrealObjectInputIdentifier&) + + virtual ~FUnrealObjectInputManager(); + + static FUnrealObjectInputManager* Get(); + + virtual bool FindNode(const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutHandle) const ; + + virtual bool Contains(const FUnrealObjectInputHandle& InHandle) const ; + virtual bool Contains(const FUnrealObjectInputIdentifier& InIdentifier) const ; + + virtual bool GetNode(const FUnrealObjectInputHandle& InHandle, const FUnrealObjectInputNode*& OutNode) const ; + virtual bool GetNode(const FUnrealObjectInputHandle& InHandle, FUnrealObjectInputNode*& OutNode) const ; + + virtual bool AddContainer( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InNodeId, + FUnrealObjectInputHandle& OutHandle) ; + + virtual bool AddReferenceNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InObjectNodeId, + const int32 InNodeId, + FUnrealObjectInputHandle& OutHandle, + TSet const* const InReferencedNodes=nullptr, + const int32 InReferencesConnectToNodeId=INDEX_NONE) ; + + virtual bool AddLeaf( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InObjectNodeId, + const int32 InNodeId, + FUnrealObjectInputHandle& OutHandle) ; + + virtual bool UpdateContainer( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InNodeId, + const bool bInClearDirtyFlag=true) ; + + virtual bool UpdateReferenceNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const TOptional InObjectNodeId=TOptional(), + const TOptional InNodeId=TOptional(), + TSet const* const InReferencedNodes=nullptr, + const TOptional InReferencesConnectToNodeId=TOptional(), + const bool bInClearDirtyFlag=true) ; + + virtual bool UpdateLeaf( + const FUnrealObjectInputIdentifier& InIdentifier, + const int32 InObjectNodeId, + const int32 InNodeId, + const bool bInClearDirtyFlag=true) ; + + virtual FString GetDefaultNodeName(const FUnrealObjectInputIdentifier& InIdentifier) const ; + + virtual bool GetUniqueHoudiniNodeId(const int32 InHAPINodeId, int32& OutUniqueHoudiniNodeId) const ; + + virtual bool AreHAPINodesValid(const FUnrealObjectInputIdentifier& InIdentifier) const ; + virtual bool IsHAPINodeValid(const FUnrealObjectInputHAPINodeId& InNodeId) const ; + virtual bool DeleteHAPINode(FUnrealObjectInputHAPINodeId& InNodeId) const ; + virtual bool SetHAPINodeDisplay(const FUnrealObjectInputHAPINodeId& InNodeId, const bool bInOnOff) const ; + virtual bool SetHAPINodeDisplay(const int32 InNodeId, const bool bInOnOff) const ; + virtual bool GetHAPINodeIds(const FUnrealObjectInputIdentifier& InIdentifier, TArray& OutNodeIds) const ; + virtual bool GetHAPINodeIds(const FUnrealObjectInputIdentifier& InIdentifier, TArray& OutNodeIds) const ; + virtual bool GetAllHAPINodeIds(TArray& OutNodeIds) const ; + virtual bool GetAllHAPINodeIds(TArray& OutNodeIds) const ; + + virtual bool EnsureParentsExist(const FUnrealObjectInputIdentifier& InIdentifier, FUnrealObjectInputHandle& OutParentHandle, const bool& bInputNodesCanBeDeleted) ; + + virtual bool IsDirty(const FUnrealObjectInputIdentifier& InIdentifier) const ; + virtual bool MarkAsDirty(const FUnrealObjectInputIdentifier& InIdentifier, bool bInAlsoDirtyReferencedNodes) ; + virtual bool ClearDirtyFlag(const FUnrealObjectInputIdentifier& InIdentifier) ; + + virtual bool Clear(); + + virtual FUnrealObjectInputHAPINodeId GetWorldOriginNodeId(const bool bInCreateIfMissingOrInvalid=true) ; + virtual int32 GetWorldOriginHAPINodeId(const bool bInCreateIfMissingOrInvalid=true) ; + + virtual bool AddRef(const FUnrealObjectInputIdentifier& InIdentifier) ; + virtual bool RemoveRef(const FUnrealObjectInputIdentifier& InIdentifier) ; + + virtual bool AddBackLink(const FUnrealObjectInputIdentifier& InReferencedIdentifier, const FUnrealObjectInputIdentifier& InReferencedBy) ; + virtual bool RemoveBackLink(const FUnrealObjectInputIdentifier& InReferencedIdentifier, const FUnrealObjectInputIdentifier& InReferencedBy) ; + virtual bool GetReferencedBy(const FUnrealObjectInputIdentifier& InReferencedIdentifier, TSet& OutReferencedBy) const ; + + virtual void Dump(); + + virtual FOnNodeAddUpdateDelete& GetOnNodeAddedDelegate() ; + virtual FOnNodeAddUpdateDelete& GetOnNodeUpdatedDelegate() ; + virtual FOnNodeAddUpdateDelete& GetOnNodeDeletedDelegate() ; + +protected: + friend class FHoudiniEngine; + + /** Sets the singleton */ + static bool SetSingleton(FUnrealObjectInputManager* InImplementation); + + /** Destroys the Singleton and InImplementation. */ + static bool DestroySingleton(); + +private: + + static FUnrealObjectInputManager* Singleton; +}; + + +``` + +`Source/HoudiniEngineRuntime/Private/UnrealObjectInputRuntimeTypes.cpp`: + +```cpp +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "UnrealObjectInputRuntimeTypes.h" + +#include "HoudiniEngineRuntimePrivatePCH.h" + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "LandscapeComponent.h" +#include "UObject/NameTypes.h" +#include "UObject/Package.h" +#include "Templates/Casts.h" +#include "Misc/Paths.h" +#include "Runtime/Launch/Resources/Version.h" + +#include "HoudiniInputObject.h" +#include "HoudiniInputTypes.h" +#include "UnrealObjectInputManager.h" + + +// GetArrayHash from Core/Public/Templates/TypeHash.h is only available from UE5.3+ +template +uint32 _GetArrayHash(const T* Ptr, uint64 Size, uint32 PreviousHash = 0) +{ +#if ENGINE_MAJOR_VERSION < 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 3) + uint32 Result = PreviousHash; + while (Size) + { + Result = HashCombineFast(Result, GetTypeHash(*Ptr)); + ++Ptr; + --Size; + } + + return Result; +#else + return GetArrayHash(Ptr, Size, PreviousHash); +#endif +} + + +bool +FUnrealObjectInputHAPINodeId::Set(const int32 InHAPINodeId, const int32 InUniqueHoudiniNodeId) +{ + HAPINodeId = InHAPINodeId; + UniqueHoudiniNodeId = InUniqueHoudiniNodeId; + if (!IsValid()) + { + Reset(); + return false; + } + return true; +} +bool +FUnrealObjectInputHAPINodeId::Set(const int32 InHAPINodeId) +{ + Reset(); + + if (InHAPINodeId < 0) + return false; + + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + int32 TheUniqueHoudiniNodeId = -1; + if (!Manager->GetUniqueHoudiniNodeId(InHAPINodeId, TheUniqueHoudiniNodeId)) + return false; + + HAPINodeId = InHAPINodeId; + UniqueHoudiniNodeId = TheUniqueHoudiniNodeId; + + return true; +} + +bool +FUnrealObjectInputHAPINodeId::IsSet() const +{ + return HAPINodeId >= 0 && UniqueHoudiniNodeId >= 0; +} + +bool +FUnrealObjectInputHAPINodeId::IsValid() const +{ + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + return Manager->IsHAPINodeValid(*this); +} + +FUnrealObjectInputOptions::FUnrealObjectInputOptions() + : bImportAsReference(false) + , bImportAsReferenceRotScaleEnabled(false) + , bExportMainGeometry(true) + , bExportLODs(false) + , bExportSockets(false) + , bExportColliders(false) + , bMainMeshIsNaniteFallbackMesh(false) + , bExportMaterialParameters(false) + , bAddRotAndScaleAttributesOnCurves(false) + , bUseLegacyInputCurves(false) + , UnrealSplineResolution(0.0f) + , LandscapeExportType(EHoudiniLandscapeExportType::Heightfield) + , bLandscapeExportMaterials(false) + , bLandscapeExportLighting(false) + , bLandscapeExportNormalizedUVs(false) + , bLandscapeExportTileUVs(false) + , bExportLandscapeSplineControlPoints(false) + , bExportLandscapeSplineLeftRightCurves(false) + , bExportPerEditLayerData(false) + , bExportLevelInstanceContent(true) + , bExportSelectedComponentsOnly(false) + , BoolOptions() + , BoolOptionsHash(0) + , SelectedComponents() + , SelectedComponentsHash(0) +{ +} + + +FUnrealObjectInputOptions +FUnrealObjectInputOptions::MakeOptionsForPackedLevelActor(const FHoudiniInputObjectSettings& InInputSettings) +{ + FUnrealObjectInputOptions Options = MakeOptionsForGenericActor(InInputSettings); + Options.bExportLevelInstanceContent = InInputSettings.bExportLevelInstanceContent; + return Options; +} + + +FUnrealObjectInputOptions +FUnrealObjectInputOptions::MakeOptionsForLevelInstanceActor(const FHoudiniInputObjectSettings& InInputSettings) +{ + FUnrealObjectInputOptions Options; + Options.bImportAsReference = InInputSettings.bImportAsReference; + Options.bImportAsReferenceRotScaleEnabled = Options.bImportAsReference && InInputSettings.bImportAsReferenceRotScaleEnabled; + if (!Options.bImportAsReference) + { + Options.bExportMainGeometry = InInputSettings.bExportMainGeometry; + Options.bExportLODs = InInputSettings.bExportLODs; + Options.bExportSockets = InInputSettings.bExportSockets; + Options.bExportColliders = InInputSettings.bExportColliders; + Options.bMainMeshIsNaniteFallbackMesh = InInputSettings.bPreferNaniteFallbackMesh; + Options.bExportMaterialParameters = InInputSettings.bExportMaterialParameters; + Options.bUseLegacyInputCurves = InInputSettings.bUseLegacyInputCurves; + Options.UnrealSplineResolution = InInputSettings.UnrealSplineResolution; + Options.LandscapeExportType = InInputSettings.LandscapeExportType; + if (Options.LandscapeExportType != EHoudiniLandscapeExportType::Heightfield) + { + Options.bLandscapeExportMaterials = InInputSettings.bLandscapeExportMaterials; + Options.bLandscapeExportLighting = InInputSettings.bLandscapeExportLighting; + Options.bLandscapeExportNormalizedUVs = InInputSettings.bLandscapeExportNormalizedUVs; + Options.bLandscapeExportTileUVs = InInputSettings.bLandscapeExportTileUVs; + } + Options.bExportPerEditLayerData = InInputSettings.bExportHeightDataPerEditLayer; + Options.bExportLandscapeSplineControlPoints = InInputSettings.bLandscapeSplinesExportControlPoints; + Options.bExportLandscapeSplineLeftRightCurves = InInputSettings.bLandscapeSplinesExportLeftRightCurves; + } + Options.bExportLevelInstanceContent = InInputSettings.bExportLevelInstanceContent; + + return Options; +} + + +FUnrealObjectInputOptions +FUnrealObjectInputOptions::MakeOptionsForLandscapeActor(const FHoudiniInputObjectSettings& InInputSettings, const TSet>* InSelectedComponents) +{ + FUnrealObjectInputOptions Options = MakeOptionsForLandscapeSplineActor(InInputSettings); + if (!Options.bImportAsReference) + { + Options.LandscapeExportType = InInputSettings.LandscapeExportType; + if (Options.LandscapeExportType != EHoudiniLandscapeExportType::Heightfield) + { + Options.bLandscapeExportMaterials = InInputSettings.bLandscapeExportMaterials; + Options.bLandscapeExportLighting = InInputSettings.bLandscapeExportLighting; + Options.bLandscapeExportNormalizedUVs = InInputSettings.bLandscapeExportNormalizedUVs; + Options.bLandscapeExportTileUVs = InInputSettings.bLandscapeExportTileUVs; + } + Options.bExportPerEditLayerData = InInputSettings.bExportHeightDataPerEditLayer; + Options.bExportSelectedComponentsOnly = InInputSettings.bLandscapeExportSelectionOnly; + if (Options.bExportSelectedComponentsOnly && InSelectedComponents) + Options.SetSelectedComponents(*InSelectedComponents); + } + + return Options; +} + + +FUnrealObjectInputOptions +FUnrealObjectInputOptions::MakeOptionsForLandscapeData(const FHoudiniInputObjectSettings& InInputSettings, const TSet>* InSelectedComponents) +{ + FUnrealObjectInputOptions Options; + Options.bImportAsReference = InInputSettings.bImportAsReference; + Options.bImportAsReferenceRotScaleEnabled = Options.bImportAsReference && InInputSettings.bImportAsReferenceRotScaleEnabled; + if (!Options.bImportAsReference) + { + Options.LandscapeExportType = InInputSettings.LandscapeExportType; + if (Options.LandscapeExportType != EHoudiniLandscapeExportType::Heightfield) + { + Options.bLandscapeExportMaterials = InInputSettings.bLandscapeExportMaterials; + Options.bLandscapeExportLighting = InInputSettings.bLandscapeExportLighting; + Options.bLandscapeExportNormalizedUVs = InInputSettings.bLandscapeExportNormalizedUVs; + Options.bLandscapeExportTileUVs = InInputSettings.bLandscapeExportTileUVs; + } + Options.bExportPerEditLayerData = InInputSettings.bExportHeightDataPerEditLayer; + Options.bExportSelectedComponentsOnly = InInputSettings.bLandscapeExportSelectionOnly; + if (Options.bExportSelectedComponentsOnly && InSelectedComponents) + Options.SetSelectedComponents(*InSelectedComponents); + } + + return Options; +} + + +FUnrealObjectInputOptions +FUnrealObjectInputOptions::MakeOptionsForLandscapeSplineActor(const FHoudiniInputObjectSettings& InInputSettings) +{ + FUnrealObjectInputOptions Options = MakeOptionsForGenericActor(InInputSettings); + if (!Options.bImportAsReference) + { + Options.bExportLandscapeSplineControlPoints = InInputSettings.bLandscapeSplinesExportControlPoints; + Options.bExportLandscapeSplineLeftRightCurves = InInputSettings.bLandscapeSplinesExportLeftRightCurves; + } + + return Options; +} + + +FUnrealObjectInputOptions +FUnrealObjectInputOptions::MakeOptionsForGenericActor(const FHoudiniInputObjectSettings& InInputSettings) +{ + FUnrealObjectInputOptions Options; + Options.bImportAsReference = InInputSettings.bImportAsReference; + Options.bImportAsReferenceRotScaleEnabled = Options.bImportAsReference && InInputSettings.bImportAsReferenceRotScaleEnabled; + if (!Options.bImportAsReference) + { + Options.bExportMainGeometry = InInputSettings.bExportMainGeometry; + Options.bExportLODs = InInputSettings.bExportLODs; + Options.bExportSockets = InInputSettings.bExportSockets; + Options.bExportColliders = InInputSettings.bExportColliders; + Options.bMainMeshIsNaniteFallbackMesh = InInputSettings.bPreferNaniteFallbackMesh; + Options.bExportMaterialParameters = InInputSettings.bExportMaterialParameters; + Options.bUseLegacyInputCurves = InInputSettings.bUseLegacyInputCurves; + Options.UnrealSplineResolution = InInputSettings.UnrealSplineResolution; + } + // Options.bExportLevelInstanceContent = InInputSettings.bExportLevelInstanceContent; + + return Options; +} + + +void +FUnrealObjectInputOptions::ComputeBoolOptionsHash() +{ + if (BoolOptions.Num() == 0) + { + BoolOptionsHash = 0; + return; + } + + TArray> BoolOptionsArray; +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1) + BoolOptionsArray = BoolOptions.Array(); +#else + BoolOptionsArray.Reserve(BoolOptions.Num()); + for (const auto& BoolOption : BoolOptions) + { + BoolOptionsArray.Add(BoolOption); + } +#endif + BoolOptionsArray.StableSort([](const TTuple& InLHS, const TTuple& InRHS) + { + return InLHS.Key.ToString() < InRHS.Key.ToString(); + }); + BoolOptionsHash = _GetArrayHash(BoolOptionsArray.GetData(), BoolOptionsArray.Num()); +} + + +void +FUnrealObjectInputOptions::ComputeSelectedComponentsHash() +{ + if (SelectedComponents.Num() == 0) + { + SelectedComponentsHash = 0; + return; + } + + TArray> SelectedComponentsArray = SelectedComponents.Array(); + SelectedComponentsArray.StableSort([](const TWeakObjectPtr& InLHS, const TWeakObjectPtr& InRHS) + { + return InLHS.Get() < InRHS.Get(); + }); + SelectedComponentsHash = _GetArrayHash(SelectedComponentsArray.GetData(), SelectedComponentsArray.Num()); +} + + +void +FUnrealObjectInputOptions::SetSelectedComponents(const TSet>& InSelectedComponents) +{ + SelectedComponents = InSelectedComponents; + ComputeSelectedComponentsHash(); +} + + +void +FUnrealObjectInputOptions::SetSelectedComponents(TSet>&& InSelectedComponents) +{ + SelectedComponents = InSelectedComponents; + ComputeSelectedComponentsHash(); +} + + +FString +FUnrealObjectInputOptions::GenerateNodeNameSuffix() const +{ + TArray NameParts; + if (bExportColliders) + NameParts.Add(TEXT("colliders")); + if (bExportLODs) + NameParts.Add(TEXT("lods")); + if (bExportSockets) + NameParts.Add(TEXT("sockets")); + if (bMainMeshIsNaniteFallbackMesh) + NameParts.Add(TEXT("nanite_fallback")); + if (bImportAsReference) + NameParts.Add(TEXT("reference")); + if (bImportAsReferenceRotScaleEnabled) + NameParts.Add(TEXT("reference_with_rot_scale")); + if (bExportLandscapeSplineControlPoints) + NameParts.Add(TEXT("control_points")); + if (bExportLandscapeSplineLeftRightCurves) + NameParts.Add(TEXT("left_right_curves")); + if (UnrealSplineResolution != 0) + NameParts.Add(FString::Printf(TEXT("spline_res_%.6f"), UnrealSplineResolution)); + if (bExportMaterialParameters) + NameParts.Add(TEXT("material_params")); + if (bAddRotAndScaleAttributesOnCurves) + NameParts.Add(TEXT("curve_rot_scale")); + if (bUseLegacyInputCurves) + NameParts.Add(TEXT("legacy_curves")); + if (bLandscapeExportMaterials) + NameParts.Add(TEXT("landscape_materials")); + if (bLandscapeExportLighting) + NameParts.Add(TEXT("landscape_lighting")); + if (bLandscapeExportNormalizedUVs) + NameParts.Add(TEXT("landscape_normalized_uvs")); + if (bLandscapeExportTileUVs) + NameParts.Add(TEXT("landscape_tile_uvs")); + if (bExportPerEditLayerData) + NameParts.Add(TEXT("landscape_edit_layers")); + if (!bExportLevelInstanceContent) + NameParts.Add(TEXT("level_instance_ref")); + if(!bExportMainGeometry) + NameParts.Add(TEXT("no_main_geo")); + if (BoolOptions.Num() > 0) + { + for (const auto& BoolOption : BoolOptions) + { + if (!BoolOption.Value) + continue; + NameParts.Add(BoolOption.Key.ToString()); + } + } + if (bExportSelectedComponentsOnly) + { + NameParts.Add(TEXT("selected_components")); + NameParts.Add(FString::Printf(TEXT("%u"), SelectedComponentsHash)); + } + + switch (LandscapeExportType) + { + case EHoudiniLandscapeExportType::Heightfield: + break; + case EHoudiniLandscapeExportType::Mesh: + NameParts.Add(TEXT("landscape_mesh")); + break; + case EHoudiniLandscapeExportType::Points: + NameParts.Add(TEXT("landscape_points")); + break; + } + + return FString::Join(NameParts, TEXT("_")); +} + +uint32 +FUnrealObjectInputOptions::GetTypeHash() const +{ + static constexpr uint32 NumByteOptions = 18; + const uint8 ByteOptions[NumByteOptions] { + bImportAsReferenceRotScaleEnabled, + bExportLODs, + bExportSockets, + bExportColliders, + bMainMeshIsNaniteFallbackMesh, + bExportMaterialParameters, + bAddRotAndScaleAttributesOnCurves, + bUseLegacyInputCurves, + static_cast(LandscapeExportType), + bLandscapeExportMaterials, + bLandscapeExportLighting, + bLandscapeExportNormalizedUVs, + bLandscapeExportTileUVs, + bExportLandscapeSplineControlPoints, + bExportLandscapeSplineLeftRightCurves, + bExportPerEditLayerData, + bExportLevelInstanceContent, + bExportSelectedComponentsOnly, + }; + + uint32 Hash = HashCombineFast(_GetArrayHash(ByteOptions, NumByteOptions), ::GetTypeHash(UnrealSplineResolution)); + if (BoolOptions.Num() > 0) + Hash = HashCombineFast(Hash, BoolOptionsHash); + if (!bExportSelectedComponentsOnly) + return Hash; + + return HashCombineFast(Hash, SelectedComponentsHash); +} + +bool +FUnrealObjectInputOptions::operator==(const FUnrealObjectInputOptions& InOther) const +{ + if (bImportAsReference != InOther.bImportAsReference || + bImportAsReferenceRotScaleEnabled != InOther.bImportAsReferenceRotScaleEnabled || + bExportLODs != InOther.bExportLODs || + bExportSockets != InOther.bExportSockets || + bExportColliders != InOther.bExportColliders || + bMainMeshIsNaniteFallbackMesh != InOther.bMainMeshIsNaniteFallbackMesh || + bExportMaterialParameters != InOther.bExportMaterialParameters || + bAddRotAndScaleAttributesOnCurves != InOther.bAddRotAndScaleAttributesOnCurves || + bUseLegacyInputCurves != InOther.bUseLegacyInputCurves || + !FMath::IsNearlyEqual(UnrealSplineResolution, InOther.UnrealSplineResolution) || + LandscapeExportType != InOther.LandscapeExportType || + bLandscapeExportMaterials != InOther.bLandscapeExportMaterials || + bLandscapeExportLighting != InOther.bLandscapeExportLighting || + bLandscapeExportNormalizedUVs != InOther.bLandscapeExportNormalizedUVs || + bLandscapeExportTileUVs != InOther.bLandscapeExportTileUVs || + bExportLandscapeSplineControlPoints != InOther.bExportLandscapeSplineControlPoints || + bExportLandscapeSplineLeftRightCurves != InOther.bExportLandscapeSplineLeftRightCurves || + bExportPerEditLayerData != InOther.bExportPerEditLayerData || + bExportLevelInstanceContent != InOther.bExportLevelInstanceContent || + BoolOptions.Num() != InOther.BoolOptions.Num() || + bExportSelectedComponentsOnly != InOther.bExportSelectedComponentsOnly) + return false; + + // Compare bool options + for (const auto& BoolOption : BoolOptions) + { + bool const* const OtherValue = InOther.BoolOptions.Find(BoolOption.Key); + if (!OtherValue) + return false; + if (BoolOption.Value != *OtherValue) + return false; + } + + if (!bExportSelectedComponentsOnly) + return true; + + return SelectedComponents.Num() == InOther.SelectedComponents.Num() && SelectedComponents.Includes(InOther.SelectedComponents); +} + + +FUnrealObjectInputIdentifier::FUnrealObjectInputIdentifier() + : Object(nullptr) + , Path() + , Options() + , NodeType(EUnrealObjectInputNodeType::Invalid) +{ +} + +FUnrealObjectInputIdentifier::FUnrealObjectInputIdentifier(UObject const* const InObject, const FUnrealObjectInputOptions& InOptions, const bool bIsLeaf) + : Object(InObject) + , Path() + , Options(InOptions) + , NodeType(bIsLeaf ? EUnrealObjectInputNodeType::Leaf : EUnrealObjectInputNodeType::Reference) +{ +} + +FUnrealObjectInputIdentifier::FUnrealObjectInputIdentifier(UObject const* const InObject) + : Object(InObject) + , Path() + , Options() + , NodeType(EUnrealObjectInputNodeType::Container) +{ +} + +FUnrealObjectInputIdentifier::FUnrealObjectInputIdentifier(UPackage const* const InPackage) + : Object(nullptr) + , Path(::IsValid(InPackage) ? InPackage->GetPathName() : FString()) + , Options() + , NodeType(EUnrealObjectInputNodeType::Container) +{ +} + +FUnrealObjectInputIdentifier::FUnrealObjectInputIdentifier(const FString& InPath, EUnrealObjectInputNodeType Type) + : Object(nullptr) + , Path(InPath) + , Options() + , NodeType(Type) +{ +} + + +bool FUnrealObjectInputIdentifier::IsValid() const +{ + switch(NodeType) + { + case EUnrealObjectInputNodeType::Invalid: + return false; + + case EUnrealObjectInputNodeType::Container: + return Object.IsValid() || !Path.IsEmpty(); + + case EUnrealObjectInputNodeType::Reference: + case EUnrealObjectInputNodeType::Leaf: + return Object.IsValid() || !Path.IsEmpty(); + } + + return false; +} + +void +FUnrealObjectInputIdentifier::Reset() +{ + Object.Reset(); + Path = FString(); + Options = FUnrealObjectInputOptions(); + NodeType = EUnrealObjectInputNodeType::Invalid; +} + +uint32 +FUnrealObjectInputIdentifier::GetTypeHash() const +{ + FString ObjectPath = Object.IsValid() ? Object->GetPathName() : Path; + + switch(NodeType) + { + case EUnrealObjectInputNodeType::Invalid: +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + return 0; +#else + return ::GetTypeHash(FString()); +#endif + case EUnrealObjectInputNodeType::Container: + { + //return FName::GetTypeHash(ObjectPath); + //return ::GetTypeHash(ObjectPath.GetComparisonIndex()) + ObjectPath.GetNumber(); + const TPair Pair(ObjectPath, Options); + return ::GetTypeHash(Pair); + } + + case EUnrealObjectInputNodeType::Reference: + case EUnrealObjectInputNodeType::Leaf: + { + const TPair Pair(ObjectPath, Options); + return ::GetTypeHash(Pair); + } + } + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2 + return 0; +#else + return ::GetTypeHash(FString()); +#endif +} + +bool +FUnrealObjectInputIdentifier::operator==(const FUnrealObjectInputIdentifier& InOther) const +{ + // NodeTypes must be the same + if (NodeType != InOther.NodeType) + return false; + + // NodeTypes are equal and invalid + if (NodeType == EUnrealObjectInputNodeType::Invalid) + return true; + + bool bOptionsEqual = Object == InOther.Object && Options == InOther.Options; + bool bPathsEqual = Path == InOther.Path; + if (NodeType == EUnrealObjectInputNodeType::Leaf) + return bOptionsEqual && bPathsEqual; + + if(NodeType == EUnrealObjectInputNodeType::Reference) + return bOptionsEqual && bPathsEqual; + + if (Object.IsValid() && InOther.Object.IsValid()) + return Object == InOther.Object; + + if(Object.Get() == nullptr && InOther.Object.Get() == nullptr) + return bPathsEqual; + + return false; +} + + +FString +FUnrealObjectInputIdentifier::ToString() const +{ + FStringBuilderBase Builder; + + Builder.Append(TEXT("type: ")); + switch(this->GetNodeType()) + { + case EUnrealObjectInputNodeType::Container: + Builder.Append(TEXT("container")); + break; + case EUnrealObjectInputNodeType::Leaf: + Builder.Append(TEXT("leaf")); + break; + case EUnrealObjectInputNodeType::Reference: + Builder.Append(TEXT("reference")); + break; + default: + Builder.Append(TEXT("unknown")); + break; + } + + if (this->Object.IsValid()) + { + Builder.Append(TEXT(" object: ")); + Builder.Append(this->Object->GetPathName()); + } + else + { + Builder.Append(TEXT(" path: ")); + Builder.Append(this->Path); + } + + return Builder.ToString(); +} + +bool +FUnrealObjectInputIdentifier::MakeParentIdentifier(FUnrealObjectInputIdentifier& OutParentIdentifier) const +{ + if (!IsValid()) + return false; + + // if (NodeType == EUnrealObjectInputNodeType::Leaf || NodeType == EUnrealObjectInputNodeType::Reference) + if (Object.IsValid()) + { + FUnrealObjectInputIdentifier ParentIdentifier; + UObject const* const Outer = Object->GetOuter(); + if (::IsValid(Outer)) + { + UPackage const* const Package = Cast(Outer); + if (::IsValid(Package)) + { + ParentIdentifier = FUnrealObjectInputIdentifier(Package); + } + else + { + ParentIdentifier = FUnrealObjectInputIdentifier(Outer); + } + } + else + { + const FString ParentPath(FPaths::GetPath(Object->GetPathName())); + ParentIdentifier = FUnrealObjectInputIdentifier(ParentPath); + } + + if (ParentIdentifier.IsValid()) + { + OutParentIdentifier = ParentIdentifier; + return true; + } + + return false; + } + + const FUnrealObjectInputIdentifier ParentIdentifier = FUnrealObjectInputIdentifier(FPaths::GetPath(Path)); + if (ParentIdentifier.IsValid()) + { + OutParentIdentifier = ParentIdentifier; + return true; + } + + return false; +} + + +FUnrealObjectInputHandle::FUnrealObjectInputHandle() + : bIsInitialized(false) + , Identifier() +{ +} + +FUnrealObjectInputHandle::FUnrealObjectInputHandle(const FUnrealObjectInputIdentifier& InIdentifier) + : bIsInitialized(false) + , Identifier() +{ + Initialize(InIdentifier); +} + +FUnrealObjectInputHandle::FUnrealObjectInputHandle(FUnrealObjectInputHandle&& InHandle) +{ + bIsInitialized = InHandle.bIsInitialized; + Identifier = InHandle.Identifier; + + InHandle.bIsInitialized = false; + InHandle.Identifier = FUnrealObjectInputIdentifier(); +} + +FUnrealObjectInputHandle& FUnrealObjectInputHandle::operator=(FUnrealObjectInputHandle&& InOther) +{ + bIsInitialized = InOther.bIsInitialized; + Identifier = InOther.Identifier; + + InOther.bIsInitialized = false; + InOther.Identifier = FUnrealObjectInputIdentifier(); + return *this; +} + +FUnrealObjectInputHandle::FUnrealObjectInputHandle(const FUnrealObjectInputHandle& InHandle) +{ + bIsInitialized = false; + if (InHandle.bIsInitialized) + Initialize(InHandle.Identifier); + else + Identifier = InHandle.Identifier; +} + +FUnrealObjectInputHandle::~FUnrealObjectInputHandle() +{ + DeInitialize(); +} + +bool +FUnrealObjectInputHandle::Initialize(const FUnrealObjectInputIdentifier& InIdentifier) +{ + if (bIsInitialized) + return false; + + Identifier = InIdentifier; + if (!Identifier.IsValid()) + return false; + + if (FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get()) + { + bIsInitialized = Manager->AddRef(Identifier); + } + + return bIsInitialized; +} + +void +FUnrealObjectInputHandle::DeInitialize() +{ + if (!bIsInitialized) + return; + +#if 0 + // I think is warning is not valid, since the Garbage Collector can determine that "this" needs to be destroyed while + // the object is still valid. + if (!Identifier.IsValid()) + { + // Should not happen... + HOUDINI_LOG_WARNING(TEXT("Found an initialized FUnrealObjectInputHandle with invalid identifier...")); + bIsInitialized = false; + return; + } +#endif + + if (FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get()) + { + Manager->RemoveRef(Identifier); + } + + Identifier = FUnrealObjectInputIdentifier(); + bIsInitialized = false; +} + +uint32 +FUnrealObjectInputHandle::GetTypeHash() const +{ + const uint32 Values[2] = {bIsInitialized, Identifier.GetTypeHash()}; + return FCrc::MemCrc32(Values, sizeof(Values)); +} + +bool +FUnrealObjectInputHandle::operator==(const FUnrealObjectInputHandle& InOther) const +{ + return bIsInitialized == InOther.bIsInitialized && Identifier == InOther.Identifier; +} + +bool +FUnrealObjectInputHandle::IsValid() const +{ + if (!Identifier.IsValid() || !bIsInitialized) + return false; + + FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + return Manager->Contains(*this); +} + +void FUnrealObjectInputHandle::Reset() +{ + if (bIsInitialized) + DeInitialize(); + else + Identifier = FUnrealObjectInputIdentifier(); +} + +FUnrealObjectInputHandle& +FUnrealObjectInputHandle::operator=(const FUnrealObjectInputHandle& InOther) +{ + if (Identifier == InOther.Identifier && bIsInitialized == InOther.bIsInitialized) + return *this; + + if (bIsInitialized) + DeInitialize(); + + if (InOther.bIsInitialized) + Initialize(InOther.Identifier); + + return *this; +} + + +FUnrealObjectInputBackLinkHandle::FUnrealObjectInputBackLinkHandle() + : FUnrealObjectInputHandle() + , SourceIdentifier() +{ +} + +FUnrealObjectInputBackLinkHandle::FUnrealObjectInputBackLinkHandle(const FUnrealObjectInputIdentifier& InSourceIdentifier, const FUnrealObjectInputIdentifier& InTargetIdentifier) + : FUnrealObjectInputHandle() + , SourceIdentifier() +{ + Initialize(InSourceIdentifier, InTargetIdentifier); +} + +FUnrealObjectInputBackLinkHandle::FUnrealObjectInputBackLinkHandle(const FUnrealObjectInputBackLinkHandle& InHandle) +{ + bIsInitialized = false; + if (InHandle.bIsInitialized) + { + Initialize(InHandle.GetSourceIdentifier(), InHandle.GetTargetIdentifier()); + } + else + { + SourceIdentifier = InHandle.GetSourceIdentifier(); + Identifier = InHandle.GetTargetIdentifier(); + } +} + +FUnrealObjectInputBackLinkHandle::~FUnrealObjectInputBackLinkHandle() +{ + DeInitialize(); +} + +bool +FUnrealObjectInputBackLinkHandle::Initialize(const FUnrealObjectInputIdentifier& InSourceIdentifier, const FUnrealObjectInputIdentifier& InTargetIdentifier) +{ + if (bIsInitialized) + return false; + + Identifier = InTargetIdentifier; + SourceIdentifier = InSourceIdentifier; + if (!Identifier.IsValid() || !SourceIdentifier.IsValid()) + return false; + + if (FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get()) + { + if (Manager->AddRef(Identifier)) + { + bIsInitialized = true; + Manager->AddBackLink(Identifier, SourceIdentifier); + } + } + + return bIsInitialized; +} + +void +FUnrealObjectInputBackLinkHandle::DeInitialize() +{ + if (!bIsInitialized) + return; + + if (!Identifier.IsValid()) + { + // Should not happen... + HOUDINI_LOG_WARNING(TEXT("Found an initialized FUnrealObjectInputBackLinkHandle with invalid identifier...")); + bIsInitialized = false; + return; + } + + if (FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get()) + { + Manager->RemoveBackLink(Identifier, SourceIdentifier); + Manager->RemoveRef(Identifier); + } + + SourceIdentifier = FUnrealObjectInputIdentifier(); + Identifier = FUnrealObjectInputIdentifier(); + bIsInitialized = false; +} + +uint32 +FUnrealObjectInputBackLinkHandle::GetTypeHash() const +{ + const uint32 Values[3] = {bIsInitialized, Identifier.GetTypeHash(), SourceIdentifier.GetTypeHash()}; + return FCrc::MemCrc32(Values, sizeof(Values)); +} + +bool +FUnrealObjectInputBackLinkHandle::operator==(const FUnrealObjectInputBackLinkHandle& InOther) const +{ + return bIsInitialized == InOther.bIsInitialized && Identifier == InOther.GetTargetIdentifier() + && SourceIdentifier == InOther.GetSourceIdentifier(); +} + +void FUnrealObjectInputBackLinkHandle::Reset() +{ + if (bIsInitialized) + { + DeInitialize(); + } + else + { + Identifier = FUnrealObjectInputIdentifier(); + SourceIdentifier = FUnrealObjectInputIdentifier(); + } +} + +FUnrealObjectInputBackLinkHandle& +FUnrealObjectInputBackLinkHandle::operator=(const FUnrealObjectInputBackLinkHandle& InOther) +{ + if (InOther == *this) + return *this; + + if (bIsInitialized) + DeInitialize(); + + if (InOther.bIsInitialized) + Initialize(InOther.GetSourceIdentifier(), InOther.GetTargetIdentifier()); + + return *this; +} + + +const FName FUnrealObjectInputNode::OutputChainName(TEXT("output")); + + +FUnrealObjectInputNode::FUnrealObjectInputNode(const FUnrealObjectInputIdentifier& InIdentifier) + : Identifier(InIdentifier) + , Parent() + , bIsDirty(false) + , ReferenceCount(0) + , bCanBeDeleted(true) +{ + +} + +FUnrealObjectInputNode::FUnrealObjectInputNode(const FUnrealObjectInputIdentifier& InIdentifier, const FUnrealObjectInputHandle& InParent, const int32 InNodeId) + : Identifier(InIdentifier) + , Parent(InParent) + , bIsDirty(false) + , ReferenceCount(0) + , bCanBeDeleted(true) +{ + NodeId.Set(InNodeId); +} + +FUnrealObjectInputNode::~FUnrealObjectInputNode() +{ + DestroyAllModifierChains(); + if (AreHAPINodesValid()) + DeleteHAPINodes(); +} + +bool FUnrealObjectInputNode::AreHAPINodesValid() const +{ + return NodeId.IsValid(); +} + +bool FUnrealObjectInputNode::DeleteHAPINodes() +{ + if (!CanBeDeleted()) + return true; + + if (!NodeId.IsValid()) + return false; + + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + return Manager->DeleteHAPINode(NodeId); +} + +void +FUnrealObjectInputNode::GetHAPINodeIds(TArray& OutHAPINodeIds) const +{ + TArray NodeIds; + GetHAPINodeIds(NodeIds); + OutHAPINodeIds.Empty(NodeIds.Num()); + for (const FUnrealObjectInputHAPINodeId& TheNodeId : NodeIds) + OutHAPINodeIds.Add(TheNodeId.GetHAPINodeId()); +} + +void +FUnrealObjectInputNode::GetHAPINodeIds(TArray& OutNodeIds) const +{ + if (!NodeId.IsValid()) + return; + + OutNodeIds.Add(NodeId); +} + +bool +FUnrealObjectInputNode::AddModifierChain(const FName InChainName, const int32 InNodeIdToConnectTo) +{ + FUnrealObjectInputHAPINodeId NodeToConnectTo; + if (!NodeToConnectTo.Set(InNodeIdToConnectTo)) + return false; + return AddModifierChain(InChainName, NodeToConnectTo); +} + +bool +FUnrealObjectInputNode::AddModifierChain(const FName InChainName, const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) +{ + if (ModifierChains.Contains(InChainName)) + return false; + FUnrealObjectInputModifierChain& Chain = ModifierChains.Add(InChainName); + Chain.ConnectToNodeId = InNodeIdToConnectTo; + return true; +} + +bool +FUnrealObjectInputNode::SetModifierChainNodeToConnectTo(const FName InChainName, const int32 InNodeToConnectTo) +{ + FUnrealObjectInputHAPINodeId NodeToConnectTo; + if (!NodeToConnectTo.Set(InNodeToConnectTo)) + return false; + return SetModifierChainNodeToConnectTo(InChainName, NodeToConnectTo); +} + +bool +FUnrealObjectInputNode::SetModifierChainNodeToConnectTo(const FName InChainName, const FUnrealObjectInputHAPINodeId& InNodeToConnectTo) +{ + FUnrealObjectInputModifierChain* Chain = ModifierChains.Find(InChainName); + if (!Chain) + return false; + Chain->ConnectToNodeId = InNodeToConnectTo; + return true; +} + +int32 +FUnrealObjectInputNode::GetInputHAPINodeIdOfModifierChain(const FName InChainName) const +{ + const FUnrealObjectInputHAPINodeId InputNodeId = GetInputNodeIdOfModifierChain(InChainName); + if (!InputNodeId.IsValid()) + return -1; + return InputNodeId.GetHAPINodeId(); +} + +FUnrealObjectInputHAPINodeId +FUnrealObjectInputNode::GetInputNodeIdOfModifierChain(const FName InChainName) const +{ + static const FUnrealObjectInputHAPINodeId InvalidNodeId; + FUnrealObjectInputModifierChain const* const Chain = ModifierChains.Find(InChainName); + if (!Chain) + return InvalidNodeId; + if (Chain->Modifiers.Num() <= 0) + return InvalidNodeId; + FUnrealObjectInputModifier const* const Modifier = Chain->Modifiers[0]; + if (!Modifier) + return InvalidNodeId; + return Modifier->GetInputNodeId(); +} + +int32 +FUnrealObjectInputNode::GetOutputHAPINodeIdOfModifierChain(const FName InChainName) const +{ + const FUnrealObjectInputHAPINodeId OutputNodeId = GetOutputNodeIdOfModifierChain(InChainName); + if (!OutputNodeId.IsValid()) + return -1; + return OutputNodeId.GetHAPINodeId(); +} + +FUnrealObjectInputHAPINodeId +FUnrealObjectInputNode::GetOutputNodeIdOfModifierChain(const FName InChainName) const +{ + static const FUnrealObjectInputHAPINodeId InvalidNodeId; + FUnrealObjectInputModifierChain const* const Chain = ModifierChains.Find(InChainName); + if (!Chain) + return InvalidNodeId; + FUnrealObjectInputModifier const* const Modifier = Chain->Modifiers.Last(); + if (!Modifier) + return InvalidNodeId; + return Modifier->GetOutputNodeId(); +} + +bool +FUnrealObjectInputNode::RemoveModifierChain(const FName InChainName) +{ + FUnrealObjectInputModifierChain* Chain = ModifierChains.Find(InChainName); + if (!Chain) + return false; + DestroyModifiers(InChainName); + ModifierChains.Remove(InChainName); + return true; +} + +bool +FUnrealObjectInputNode::AddModifier(const FName InChainName, FUnrealObjectInputModifier* const InModifierToAdd) +{ + FUnrealObjectInputModifierChain* const Chain = ModifierChains.Find(InChainName); + if (!Chain) + return false; + for (FUnrealObjectInputModifier const* const Modifier : Chain->Modifiers) + { + if (Modifier == InModifierToAdd) + return false; + } + Chain->Modifiers.Emplace(InModifierToAdd); + InModifierToAdd->OnAddedToOwner(); + return true; +} + +FUnrealObjectInputModifier* +FUnrealObjectInputNode::FindFirstModifierOfType(const FName InChainName, const EUnrealObjectInputModifierType InModifierType) const +{ + FUnrealObjectInputModifierChain const* const Chain = ModifierChains.Find(InChainName); + if (!Chain) + return nullptr; + FUnrealObjectInputModifier* const* const Result = Chain->Modifiers.FindByPredicate([InModifierType](FUnrealObjectInputModifier const* const InModifier) + { + if (!InModifier) + return false; + return InModifier->GetType() == InModifierType; + }); + + if (!Result) + return nullptr; + + return *Result; +} + +bool +FUnrealObjectInputNode::GetAllModifiersOfType(const FName InChainName, const EUnrealObjectInputModifierType InModifierType, TArray& OutModifiers) const +{ + FUnrealObjectInputModifierChain const* const Chain = ModifierChains.Find(InChainName); + if (!Chain) + return false; + TArray FoundModifiers; + for (FUnrealObjectInputModifier* const Modifier : Chain->Modifiers) + { + if (!Modifier) + continue; + if (Modifier->GetType() != InModifierType) + continue; + FoundModifiers.Emplace(Modifier); + } + + if (FoundModifiers.Num() <= 0) + return false; + + OutModifiers = MoveTemp(FoundModifiers); + return true; +} + +bool +FUnrealObjectInputNode::DestroyModifier(const FName InChainName, FUnrealObjectInputModifier* InModifier) +{ + if (!InModifier) + return false; + + FUnrealObjectInputModifierChain* const Chain = ModifierChains.Find(InChainName); + if (!Chain) + return false; + + int32 ModifierIndex = INDEX_NONE; + const int32 NumModifiers = Chain->Modifiers.Num(); + for (int32 Index = 0; Index < NumModifiers; ++Index) + { + FUnrealObjectInputModifier const* const Modifier = Chain->Modifiers[Index]; + if (!Modifier) + continue; + if (Modifier != InModifier) + continue; + + ModifierIndex = Index; + break; + } + + if (ModifierIndex == INDEX_NONE) + return false; + + Chain->Modifiers.RemoveAt(ModifierIndex); + InModifier->OnRemovedFromOwner(); + delete InModifier; + + return true; +} + +bool +FUnrealObjectInputNode::DestroyModifiers(const FName InChainName) +{ + FUnrealObjectInputModifierChain* const Chain = ModifierChains.Find(InChainName); + if (!Chain) + return false; + + while (Chain->Modifiers.Num() > 0) + { + static constexpr bool bAllowShrinking = false; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 5 + FUnrealObjectInputModifier* const Modifier = Chain->Modifiers.Pop(bAllowShrinking ? EAllowShrinking::Yes : EAllowShrinking::No); +#else + FUnrealObjectInputModifier* const Modifier = Chain->Modifiers.Pop(bAllowShrinking); +#endif + if (!Modifier) + continue; + Modifier->OnRemovedFromOwner(); + delete Modifier; + } + + Chain->Modifiers.Empty(); + + return true; +} + +bool +FUnrealObjectInputNode::DestroyAllModifierChains() +{ + bool bSuccess = true; + TArray ChainNames; + ModifierChains.GetKeys(ChainNames); + for (const FName& ChainName : ChainNames) + { + if (!DestroyModifiers(ChainName)) + bSuccess = false; + } + + return bSuccess; +} + +bool +FUnrealObjectInputNode::UpdateModifiers(const FName InChainName) +{ + FUnrealObjectInputModifierChain* const Chain = ModifierChains.Find(InChainName); + if (!Chain) + return false; + + bool bSuccess = true; + FUnrealObjectInputHAPINodeId NodeToConnectTo = Chain->ConnectToNodeId; + if (!NodeToConnectTo.IsValid()) + NodeToConnectTo = NodeId; + for (FUnrealObjectInputModifier* const Modifier : Chain->Modifiers) + { + if (!Modifier) + continue; + if (!Modifier->Update(NodeToConnectTo)) + { + bSuccess = false; + } + else + { + const FUnrealObjectInputHAPINodeId OutputNodeId = Modifier->GetOutputNodeId(); + if (OutputNodeId.IsSet()) + NodeToConnectTo = OutputNodeId; + } + } + + // If this is the output chain, set it its display node + if (OutputChainName == InChainName) + { + const FUnrealObjectInputHAPINodeId OutputNodeId = GetOutputNodeIdOfModifierChain(OutputChainName); + if (OutputNodeId.IsValid()) + { + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (Manager) + Manager->SetHAPINodeDisplay(OutputNodeId, true); + } + } + + return bSuccess; +} + +bool +FUnrealObjectInputNode::UpdateAllModifierChains() +{ + bool bSuccess = true; + TArray ChainNames; + ModifierChains.GetKeys(ChainNames); + for (const FName& ChainName : ChainNames) + { + if (!UpdateModifiers(ChainName)) + bSuccess = false; + } + + + + return bSuccess; +} + +bool +FUnrealObjectInputNode::AddRef() const +{ + if (!IsRefCounted()) + return false; + + FPlatformAtomics::InterlockedIncrement(&ReferenceCount); + return true; +} + +bool +FUnrealObjectInputNode::RemoveRef() const +{ + if (!IsRefCounted()) + return false; + +#if DO_GUARD_SLOW + if (ReferenceCount == 0) + { + check(ReferenceCount != 0 && TEXT("Trying to RemoveRef when ReferenceCount is 0.")); + } +#endif + + FPlatformAtomics::InterlockedDecrement(&ReferenceCount); + return true; +} + +FUnrealObjectInputContainerNode::FUnrealObjectInputContainerNode(const FUnrealObjectInputIdentifier& InIdentifier) + : FUnrealObjectInputNode(InIdentifier) +{ + +} + +FUnrealObjectInputContainerNode::FUnrealObjectInputContainerNode(const FUnrealObjectInputIdentifier& InIdentifier, const FUnrealObjectInputHandle& InParent, const int32 InNodeId) + : FUnrealObjectInputNode(InIdentifier, InParent, InNodeId) +{ + +} + + +FUnrealObjectInputLeafNode::FUnrealObjectInputLeafNode(const FUnrealObjectInputIdentifier& InIdentifier) + : FUnrealObjectInputNode(InIdentifier) +{ + +} + +FUnrealObjectInputLeafNode::FUnrealObjectInputLeafNode(const FUnrealObjectInputIdentifier& InIdentifier, const FUnrealObjectInputHandle& InParent, const int32 InObjectNodeId, const int32 InNodeId) + : FUnrealObjectInputNode(InIdentifier, InParent, InNodeId) +{ + ObjectNodeId.Set(InObjectNodeId); +} + +FUnrealObjectInputLeafNode::~FUnrealObjectInputLeafNode() +{ + if (AreHAPINodesValid()) + DeleteHAPINodes(); +} + +bool +FUnrealObjectInputLeafNode::AreHAPINodesValid() const +{ + if (!ObjectNodeId.IsValid()) + return false; + + if (!FUnrealObjectInputNode::AreHAPINodesValid()) + return false; + + return true; +} + +bool FUnrealObjectInputLeafNode::DeleteHAPINodes() +{ + // If the node cant be deleted, return true + if (!CanBeDeleted()) + return true; + + if (!FUnrealObjectInputNode::DeleteHAPINodes()) + return false; + + if (!ObjectNodeId.IsValid()) + return false; + + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + return Manager->DeleteHAPINode(ObjectNodeId); +} + +void +FUnrealObjectInputLeafNode::GetHAPINodeIds(TArray& OutNodeIds) const +{ + FUnrealObjectInputNode::GetHAPINodeIds(OutNodeIds); + + if (!ObjectNodeId.IsValid()) + return; + + OutNodeIds.Add(ObjectNodeId); +} + +FUnrealObjectInputReferenceNode::FUnrealObjectInputReferenceNode(const FUnrealObjectInputIdentifier& InIdentifier) + : FUnrealObjectInputLeafNode(InIdentifier) +{ + +} + +FUnrealObjectInputReferenceNode::FUnrealObjectInputReferenceNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const FUnrealObjectInputHandle& InParent, + const int32 InObjectNodeId, + const int32 InNodeId, + const int32 InReferencesConnectToNodeId) + : FUnrealObjectInputLeafNode(InIdentifier, InParent, InObjectNodeId, InNodeId) +{ + SetReferencesConnectToNodeId(InReferencesConnectToNodeId); +} + +FUnrealObjectInputReferenceNode::FUnrealObjectInputReferenceNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const FUnrealObjectInputHandle& InParent, + const int32 InObjectNodeId, + const int32 InNodeId, + const TSet& InReferencedNodes, + const int32 InReferencesConnectToNodeId) + : FUnrealObjectInputReferenceNode(InIdentifier, InParent, InObjectNodeId, InNodeId, InReferencesConnectToNodeId) +{ + SetReferencedNodes(InReferencedNodes); +} + +bool FUnrealObjectInputReferenceNode::AddReferencedNode(const FUnrealObjectInputHandle& InHandle) +{ + bool bAlreadySet = false; + ReferencedNodes.Add( + FUnrealObjectInputBackLinkHandle(GetIdentifier(), InHandle.GetIdentifier()), + &bAlreadySet); + + return !bAlreadySet; +} + +void FUnrealObjectInputReferenceNode::SetReferencedNodes(const TSet& InReferencedNodes) +{ + ReferencedNodes.Empty(InReferencedNodes.Num()); + const FUnrealObjectInputIdentifier& MyIdentifier = GetIdentifier(); + for (const FUnrealObjectInputHandle& Handle : InReferencedNodes) + { + ReferencedNodes.Add(FUnrealObjectInputBackLinkHandle(MyIdentifier, Handle.GetIdentifier())); + } +} + +void FUnrealObjectInputReferenceNode::GetReferencedNodes(TSet& OutReferencedNodes) const +{ + OutReferencedNodes.Empty(ReferencedNodes.Num()); + for (const FUnrealObjectInputBackLinkHandle& Handle : ReferencedNodes) + { + OutReferencedNodes.Add(Handle); + } +} + +bool FUnrealObjectInputReferenceNode::AreReferencedHAPINodesValid() const +{ + if (ReferencedNodes.IsEmpty()) + return true; + + FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + for (const FUnrealObjectInputHandle& Handle : ReferencedNodes) + { + if (!Manager->AreHAPINodesValid(Handle.GetIdentifier())) + return false; + } + + return true; +} + + +// Marks this and its referenced nodes as dirty. See IsDirty(). +void FUnrealObjectInputReferenceNode::MarkAsDirty(const bool bInAlsoDirtyReferencedNodes) +{ + MarkAsDirty(); + + if (!bInAlsoDirtyReferencedNodes || ReferencedNodes.IsEmpty()) + return; + + FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return; + + for (const FUnrealObjectInputHandle& Handle : ReferencedNodes) + { + Manager->MarkAsDirty(Handle.GetIdentifier(), bInAlsoDirtyReferencedNodes); + } +} + + +void +FUnrealObjectInputReferenceNode::SetReferencesConnectToNodeId(const int32 InReferencesConnectToNodeId) +{ + if (InReferencesConnectToNodeId < 0) + { + ReferencesConnectToNodeId.Reset(); + } + else + { + FUnrealObjectInputHAPINodeId ConnectToNodeId; + ConnectToNodeId.Set(InReferencesConnectToNodeId); + ReferencesConnectToNodeId = ConnectToNodeId; + } +} + + +bool +FUnrealObjectInputModifier::DestroyHAPINodes() +{ + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + bool bSuccess = true; + for (FUnrealObjectInputHAPINodeId NodeId : HAPINodeIds) + { + if (!NodeId.IsValid()) + continue; + // TODO: can we ensure that node input and output nodes are linked after deleting node? + if (!Manager->DeleteHAPINode(NodeId)) + bSuccess = false; + } + HAPINodeIds.Empty(); + + return bSuccess; +} + +FUnrealObjectInputUpdateScope::FUnrealObjectInputUpdateScope() +{ + FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get(); + if (Manager) + { + OnCreatedHandle = Manager->GetOnNodeAddedDelegate().AddRaw(this, &FUnrealObjectInputUpdateScope::OnNodeCreatedOrUpdated); + OnUpdatedHandle = Manager->GetOnNodeUpdatedDelegate().AddRaw(this, &FUnrealObjectInputUpdateScope::OnNodeCreatedOrUpdated); + OnDestroyedHandle = Manager->GetOnNodeDeletedDelegate().AddRaw(this, &FUnrealObjectInputUpdateScope::OnNodeDestroyed); + } +} + +FUnrealObjectInputUpdateScope::~FUnrealObjectInputUpdateScope() +{ + FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get(); + if (Manager) + { + if (OnDestroyedHandle.IsValid()) + Manager->GetOnNodeDeletedDelegate().Remove(OnDestroyedHandle); + if (OnUpdatedHandle.IsValid()) + Manager->GetOnNodeUpdatedDelegate().Remove(OnUpdatedHandle); + if (OnCreatedHandle.IsValid()) + Manager->GetOnNodeAddedDelegate().Remove(OnCreatedHandle); + } +} + +void FUnrealObjectInputUpdateScope::OnNodeCreatedOrUpdated(const FUnrealObjectInputIdentifier& InIdentifier) +{ + NodesDestroyed.Remove(InIdentifier); + NodesCreatedOrUpdated.Add(InIdentifier); +} + +void FUnrealObjectInputUpdateScope::OnNodeDestroyed(const FUnrealObjectInputIdentifier& InIdentifier) +{ + NodesCreatedOrUpdated.Remove(InIdentifier); + NodesDestroyed.Add(InIdentifier); +} + +``` + +`Source/HoudiniEngineRuntime/Private/UnrealObjectInputRuntimeTypes.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "Misc/Optional.h" +#include "UObject/Object.h" +#include "UObject/WeakObjectPtr.h" +#include "HoudiniEngineRuntimeCommon.h" +#include "HoudiniInputTypes.h" + +// Houdini Engine forward declarations +class FUnrealObjectInputModifier; +struct FHoudiniInputObjectSettings; +enum class EHoudiniInputObjectType : uint8; + +// UE forward declarations +class UActorComponent; +class ULandscapeComponent; + + +/** + * This class wraps a HAPI_NodeId and the Unique Houdini Node Id. The IsValid() function (via the manager) confirms + * that HAPINodeId is still a valid node in the Houdini session, and that that node's unique Houdini Node Id matches + * the one cached in the instance of this class. HAPI Node Ids can be reused if nodes are deleted, so this added + * validation is necessary. + */ +class HOUDINIENGINERUNTIME_API FUnrealObjectInputHAPINodeId +{ +public: + /** Constructs a new invalid node id */ + FUnrealObjectInputHAPINodeId() : HAPINodeId(-1), UniqueHoudiniNodeId(-1) {} + + /** Get the HAPI Node Id */ + int32 GetHAPINodeId() const { return HAPINodeId; } + /** Get the unique Houdini node Id that was cached the last time this Id was set. */ + int32 GetUniqueHoudiniNodeId() const { return UniqueHoudiniNodeId; } + + /** Set both the HAPI node id and unique Houdini node id. */ + bool Set(const int32 InHAPINodeId, const int32 InUniqueHoudiniNodeId); + /** Set the HAPI Node Id and fetch the unique Houdini node Id from Houdini and cache it. */ + bool Set(const int32 InHAPINodeId); + + /** Reset this id to be invalid. */ + void Reset() { HAPINodeId = -1; UniqueHoudiniNodeId = -1; } + + /** Returns true if the both id values are >= 0. */ + bool IsSet() const; + /** + * Uses the manager to check that HAPINodeId is a valid node in the Houdini session, and that that node's unique + * Houdini Node Id matches the cached UniqueHoudiniNodeId. + */ + bool IsValid() const; + +private: + int32 HAPINodeId; + int32 UniqueHoudiniNodeId; +}; + +/** + * A struct of options that are used by FUnrealObjectInputIdentifier to differentiate between variations of the + * same object. + */ +class HOUDINIENGINERUNTIME_API FUnrealObjectInputOptions +{ +public: + FUnrealObjectInputOptions(); + + /** Helper to make FUnrealObjectInputOptions for PackedLevelActors. */ + static FUnrealObjectInputOptions MakeOptionsForPackedLevelActor(const FHoudiniInputObjectSettings& InInputSettings); + + /** Helper to make FUnrealObjectInputOptions for LevelInstance actors. */ + static FUnrealObjectInputOptions MakeOptionsForLevelInstanceActor(const FHoudiniInputObjectSettings& InInputSettings); + + /** Helper to make FUnrealObjectInputOptions for Landscape actors. */ + static FUnrealObjectInputOptions MakeOptionsForLandscapeActor( + const FHoudiniInputObjectSettings& InInputSettings, const TSet>* InSelectedComponents=nullptr); + + /** Helper to make FUnrealObjectInputOptions for Landscape data. */ + static FUnrealObjectInputOptions MakeOptionsForLandscapeData( + const FHoudiniInputObjectSettings& InInputSettings, const TSet>* InSelectedComponents=nullptr); + + /** Helper to make FUnrealObjectInputOptions for LandscapeSplineActors. */ + static FUnrealObjectInputOptions MakeOptionsForLandscapeSplineActor(const FHoudiniInputObjectSettings& InInputSettings); + + /** Helper to make FUnrealObjectInputOptions for generic Actors. */ + static FUnrealObjectInputOptions MakeOptionsForGenericActor(const FHoudiniInputObjectSettings& InInputSettings); + + void SetBoolOptions(const TMap& InBoolOptions) { BoolOptions = InBoolOptions; ComputeBoolOptionsHash(); } + void AddBoolOption(const FName InBoolOption, const bool bInValue) { BoolOptions.Add(InBoolOption, bInValue); ComputeBoolOptionsHash(); } + bool RemoveBoolOption(const FName InBoolOptionToRemove) + { + if (BoolOptions.Remove(InBoolOptionToRemove) <= 0) + return false; + + ComputeBoolOptionsHash(); + return true; + } + + const TMap& GetBoolOptions() const { return BoolOptions; } + + uint32 GetBoolOptionsHash() const { return BoolOptionsHash; } + + void SetSelectedComponents(const TSet>& InSelectedComponents); + void SetSelectedComponents(TSet>&& InSelectedComponents); + + template + void SetSelectedComponents(const TSet>& InSelectedComponents); + + const TSet>& GetSelectedComponents() const { return SelectedComponents; } + + uint32 GetSelectedComponentsHash() const { return SelectedComponentsHash; } + + /** Return a suffix to apply to input node name's in Houdini that represent the current options selection. */ + FString GenerateNodeNameSuffix() const; + + /** Return hash value for this object, used when using this object as a key inside hashing containers. */ + uint32 GetTypeHash() const; + + /** Hashing containers need the == operator. */ + bool operator==(const FUnrealObjectInputOptions& InOther) const; + + /** + * Indicates that all the input objects are imported to Houdini as references instead of actual geo + * (for Geo/World input types only) + */ + bool bImportAsReference; + + /** Indicates that whether or not to add the rot / scale attributes for reference imports */ + bool bImportAsReferenceRotScaleEnabled; + + /** Indicates that the object's main geometry should be sent to Houdini */ + bool bExportMainGeometry; + + /** Indicates that all LODs in the input should be marshalled to Houdini */ + bool bExportLODs; + + /** Indicates that all sockets in the input should be marshalled to Houdini */ + bool bExportSockets; + + /** Indicates that all colliders in the input should be marshalled to Houdini */ + bool bExportColliders; + + /** Use the Nanite fallback mesh for the main mesh, instead of the full Nanite mesh. */ + bool bMainMeshIsNaniteFallbackMesh; + + /** Indicates that material parameters should be exported as attributes */ + bool bExportMaterialParameters; + + /** Set this to true to add rot and scale attributes on curve inputs. */ + bool bAddRotAndScaleAttributesOnCurves; + + /** Set this to true to use legacy (curve::1.0) input curves */ + bool bUseLegacyInputCurves; + + /** Resolution used when converting unreal splines to houdini curves */ + float UnrealSplineResolution; + + /** Indicates the export type if this input is a Landscape */ + EHoudiniLandscapeExportType LandscapeExportType; + + /** Is set to true when materials are to be exported. */ + bool bLandscapeExportMaterials; + + /** Is set to true when lightmap information export is desired. */ + bool bLandscapeExportLighting; + + /** Is set to true when uvs should be exported in [0,1] space. */ + bool bLandscapeExportNormalizedUVs; + + /** Is set to true when uvs should be exported for each tile separately. */ + bool bLandscapeExportTileUVs; + + /** Export landscape spline control points as a point cloud. */ + bool bExportLandscapeSplineControlPoints; + + /** Export landscape spline left and right curves. */ + bool bExportLandscapeSplineLeftRightCurves; + + /** If enabled, target layers are exported per Edit Layer. */ + bool bExportPerEditLayerData; + + /** + * If enabled, level instances (and packed level actor) content is exported vs just exporting a single point + * with attributes identifying the level instance / packed level actor. + */ + bool bExportLevelInstanceContent; + + /** Export selected components only. */ + bool bExportSelectedComponentsOnly; + +protected: + /** Compute BoolOptionsHash from BoolOptions. */ + void ComputeBoolOptionsHash(); + + /** Compute SelectedComponentsHash from SelectedComponents. */ + void ComputeSelectedComponentsHash(); + +private: + /** Boolean options for the input object. */ + TMap BoolOptions; + + /** The computed hash of BoolOptions. */ + uint32 BoolOptionsHash; + + /** The set of selected components. */ + TSet> SelectedComponents; + + /** The computed hash of SelectedComponents. */ + uint32 SelectedComponentsHash; +}; + +/** Function used by hashing containers to create a unique hash for this type of object. */ +inline HOUDINIENGINERUNTIME_API uint32 GetTypeHash(const FUnrealObjectInputOptions& InOptions) { return InOptions.GetTypeHash(); } + +/** + * An enum that defines the different node types. Used to differentiate identifiers to the same object for different + * purposes. + */ +enum class EUnrealObjectInputNodeType : uint8 +{ + Invalid, + Container, /** A node that contains other nodes. */ + Reference, /** A node that references (or merges) other leaf nodes. */ + Leaf /** A node that contains an object/asset, such as static mesh geometry. */ +}; + + +/** + * A class that acts as an identifier of objects in the input system, differentiated by type and import options. + * It can be constructed from an Object + import options, representing a leaf node (single option variations of a SM + * for example) or a reference node (merge of a combination of options for a SM, for example). + * + * It can also be constructed from an Object, Package or Path to act as a container (analogous to directories, usually + * represented with subnet Objects). + */ +class HOUDINIENGINERUNTIME_API FUnrealObjectInputIdentifier +{ +public: + /** Construct a blank, invalid identifier */ + FUnrealObjectInputIdentifier(); + + /** + * Construct an identifier to a leaf or reference node for an object with certain import options. + * @param InObject The object to construct an identifier for. + * @param InOptions The import options. Typically this would be a combination of options (such as LODs + Sockets) + * for a reference node, or a single option or no options for a leaf node. + * @param bIsLeaf If true, this is an identifier ot a leaf node, otherwise it is an identifier to a reference node. + */ + FUnrealObjectInputIdentifier(UObject const* const InObject, const FUnrealObjectInputOptions& InOptions, const bool bIsLeaf); + + /** + * Construct an identifier for a container associated with InObject. This is useful for actors or blueprints, that + * that do not directly contain geo but are objects that contain other nodes with geo (such as static mesh + * components). + */ + FUnrealObjectInputIdentifier(UObject const* const InObject); + + /** Construct an identifier for a container associated with a package. Analogous to directories. */ + FUnrealObjectInputIdentifier(UPackage const* const InPackage); + + /** Construct an identifier for a container associated with a path. Analogous to directories. */ + FUnrealObjectInputIdentifier(const FString& InPath, EUnrealObjectInputNodeType Type = EUnrealObjectInputNodeType::Container); + + /** + * Returns true if the identifier is valid. + * A leaf identifier is valid if it has a valid Object. + * A reference identifier is valid if it has a valid Object. + * A container is valid if it has a valid Object or non-empty Path. + */ + bool IsValid() const; + + /** Reset/clear the identifier. It makes the identifier blank/invalid (same state as calling the default constructor). */ + void Reset(); + + /** Return hash value for this object, used when using this object as a key inside hashing containers. */ + uint32 GetTypeHash() const; + + /** Hashing containers need the == operator. */ + bool operator==(const FUnrealObjectInputIdentifier& InOther) const; + + /** + * Creates an identifier for the parent of this node. + * @param OutIdentifier Set to the parent identifier. + * @return true if a valid parent identifier could be built. + */ + bool MakeParentIdentifier(FUnrealObjectInputIdentifier& OutParentIdentifier) const; + + /** Returns the UObject that this identifier is associated with, if Object was set during construction. */ + const UObject* GetObject() const { return Object.Get(); } + + /** Returns the Path this identifier is associated with, if Path was set during construction. */ + FString GetPath() const { return Path; } + + /** Helper to return object paths with . and : replaced by / */ + static FString NormalizeObjectPath(const FString& InObjectPath) { return InObjectPath.Replace(TEXT("."), TEXT("/")).Replace(TEXT(":"), TEXT("/")); } + + /** Helper to return object paths with . and : replaced by / */ + static FString NormalizeObjectPath(const FName& InObjectPath) { return NormalizeObjectPath(InObjectPath.ToString()); } + + /** Get the object path. This is the full path of Object if it's valid, otherwise this returns Path. */ + FString GetObjectPath() const { return Object.IsValid() ? Object->GetPathName() : Path; } + + /** Get the normalized object path. See NormalizeObjectPath() */ + FString GetNormalizedObjectPath() const { return NormalizeObjectPath(GetObjectPath()); } + + /** Gets the node type this identifier represents. See EUnrealObjectInputNodeType. */ + EUnrealObjectInputNodeType GetNodeType() const { return NodeType; } + + /** Gets the Options that forms part of this identifier. */ + const FUnrealObjectInputOptions& GetOptions() const { return Options; } + + FString ToString() const ; + +private: + /** The object this identifier is associated with. */ + TWeakObjectPtr Object; + + /** The path this identifier is associated, for cases where it does not reference an object. */ + FString Path; + + /** The import options associated with the Object of this identifier. */ + FUnrealObjectInputOptions Options; + + /** The type of node this identifier represents. */ + EUnrealObjectInputNodeType NodeType; +}; + +/** Function used by hashing containers to create a unique hash for this type of object. */ +inline HOUDINIENGINERUNTIME_API uint32 GetTypeHash(const FUnrealObjectInputIdentifier& InIdentifier) { return InIdentifier.GetTypeHash(); } + + +/** + * A reference counting handle that wraps FUnrealObjectInputIdentifier. + * Upon construction and destruction, if the identifier is valid, the handle automatically increments/decrements the + * reference count to the entry in the manager via the manager. + * + * Reference counting is also handled with the assignment operator. + */ +class HOUDINIENGINERUNTIME_API FUnrealObjectInputHandle +{ +public: + /** Construct and empty invalid handle. */ + FUnrealObjectInputHandle(); + /** + * Construct a handle for InIdentifier. Increments the reference count to the entry in the manager if the + * identifier is valid. + */ + FUnrealObjectInputHandle(const FUnrealObjectInputIdentifier& InIdentifier); + /** + * Copies InHandle. Increments the reference count to the entry in the manager if the + * identifier is valid. + */ + FUnrealObjectInputHandle(const FUnrealObjectInputHandle& InHandle); + + FUnrealObjectInputHandle(FUnrealObjectInputHandle&& InHandle); + + /** Destructor: decrements reference count via the manager if the handle is valid. */ + virtual ~FUnrealObjectInputHandle(); + + /** Return hash value for this object, used when using this object as a key inside hashing containers. */ + virtual uint32 GetTypeHash() const; + + /** Hashing containers need the == operator. */ + bool operator==(const FUnrealObjectInputHandle& InOther) const; + + /** + * Returns true if the handle is valid. A handle is valid if it is initialized (see bIsInitialized) and its + * identifier is valid. + */ + virtual bool IsValid() const; + + /** + * Resets the handle. If the handle is valid the reference count of the entry it points to will be decremented and + * the handle will be reset so that it is invalid/points to nothing. + */ + virtual void Reset(); + + /** Gets the identifier that this handle wraps. */ + const FUnrealObjectInputIdentifier& GetIdentifier() const { return Identifier; } + + /** + * Assignment operator. After assignment this handle will wrap the identifier from InOther. If this handle was + * initialized, it will decrement the reference count of the previous identifier via the manager. If InOther is + * initialized, this handle will increment the reference count to the new identifier. + */ + FUnrealObjectInputHandle& operator=(const FUnrealObjectInputHandle& InOther); + FUnrealObjectInputHandle& operator=(FUnrealObjectInputHandle&& InOther); + +protected: + /** + * Initialize the handle: check that InIdentifier is valid, assign it internally and increment the reference count + * via the manager. If successful, bIsInitialized is set to true. + * @return true if successfully initialized. + */ + virtual bool Initialize(const FUnrealObjectInputIdentifier& InIdentifier); + + /** + * Deinitialize the handle if it was initialized. Decrements the reference count for Identifier (if valid) via + * the manager. + */ + virtual void DeInitialize(); + + /** True if the handle has been successfully initialized and the reference count for Identifier incremented. */ + bool bIsInitialized; + + /** The identifier that is wrapped by this handled and for which reference counts will be incremented/decremented. */ + FUnrealObjectInputIdentifier Identifier; + +}; + +/** + * A reference counting handle derived from FUnrealObjectInputHandle. The back link handle + * stores the source object as well, so that manager is aware that A is referencing B versus a + * normal handle where the manager would only be aware that B is being referenced. + * Upon construction and destruction, if the target identifier is valid, the handle automatically increments/decrements the + * reference count to the entry in the manager via the manager. + * + * Reference counting is also handled with the assignment operator. + */ +class HOUDINIENGINERUNTIME_API FUnrealObjectInputBackLinkHandle : public FUnrealObjectInputHandle +{ +public: + /** Construct and empty invalid handle. */ + FUnrealObjectInputBackLinkHandle(); + /** + * Construct a handle for a reference to InTargetIdentifier from InSourceIdentifier. The reference count is effective for + * InTargetIdentifier. Increments the reference count to the entry in the manager if + * InTargetIdentifier is valid. + */ + FUnrealObjectInputBackLinkHandle(const FUnrealObjectInputIdentifier& InSourceIdentifier, const FUnrealObjectInputIdentifier& InTargetIdentifier); + /** + * Copies InHandle. Increments the reference count to the entry in the manager if + * InTargetIdentifier is valid. + */ + FUnrealObjectInputBackLinkHandle(const FUnrealObjectInputBackLinkHandle& InHandle); + + /** Destructor: decrements reference count via the manager if the handle is valid. */ + virtual ~FUnrealObjectInputBackLinkHandle(); + + /** Return hash value for this object, used when using this object as a key inside hashing containers. */ + virtual uint32 GetTypeHash() const override; + + /** Hashing containers need the == operator. */ + bool operator==(const FUnrealObjectInputBackLinkHandle& InOther) const; + + /** + * Resets the handle. If the handle is valid the reference count of the entry it points to will be decremented and + * the handle will be reset so that it is invalid/points to nothing. + */ + virtual void Reset(); + + /** Gets the identifier to the target. */ + const FUnrealObjectInputIdentifier& GetTargetIdentifier() const { return GetIdentifier(); } + + /** Gets the identifier to the source. */ + const FUnrealObjectInputIdentifier& GetSourceIdentifier() const { return SourceIdentifier; } + + /** + * Assignment operator. After assignment this handle will wrap the identifiers from InOther. If this handle was + * initialized, it will decrement the reference count of the previous target identifier via the manager. If InOther is + * initialized, this handle will increment the reference count to the new target identifier. + */ + FUnrealObjectInputBackLinkHandle& operator=(const FUnrealObjectInputBackLinkHandle& InOther); + +protected: + /** + * Initialize the handle: check that InIdentifier is valid, assign it internally and increment the reference count + * via the manager. If successful, bIsInitialized is set to true. + * @return true if successfully initialized. + */ + virtual bool Initialize(const FUnrealObjectInputIdentifier& InSourceIdentifier, const FUnrealObjectInputIdentifier& InTargetIdentifier); + + /** + * Deinitialize the handle if it was initialized. Decrements the reference count for Identifier (if valid) via + * the manager. + */ + virtual void DeInitialize() override; + + /** The identifier that of the source object (or the object to which the back link points). */ + FUnrealObjectInputIdentifier SourceIdentifier; + +private: + /** This function always returns false in this implementation since a source is required. */ + virtual bool Initialize(const FUnrealObjectInputIdentifier& InIdentifier) override { return false; } + +}; + +/** Function used by hashing containers to create a unique hash for this type of object. */ +inline HOUDINIENGINERUNTIME_API uint32 GetTypeHash(const FUnrealObjectInputHandle& InHandle) { return InHandle.GetTypeHash(); } + + +enum class EUnrealObjectInputModifierType : uint8 +{ + Invalid, + MaterialOverrides, + PhysicalMaterialOverride, + ActorAsReference, + DataLayerGroups, + HLODAttributes, + ActorProperties, + CustomPrimitiveData +}; + +/** Represents a chain of FUnrealObjectInputModifiers, owned by a single FUnrealObjectInputNode. See FUnrealObjectInputModifier. */ +struct HOUDINIENGINERUNTIME_API FUnrealObjectInputModifierChain +{ + /** The node to connect to the input of the first node of the first modifiers. */ + FUnrealObjectInputHAPINodeId ConnectToNodeId; + + /** The modifiers of this chain. */ + TArray Modifiers; +}; + +/** + * The base class for entries in the FUnrealObjectInputManager. + * + */ +class HOUDINIENGINERUNTIME_API FUnrealObjectInputNode +{ +public: + static const FName OutputChainName; + + /** Do not allow construction without an identifier. */ + FUnrealObjectInputNode() = delete; + + /** Construct with only the identifier. */ + FUnrealObjectInputNode(const FUnrealObjectInputIdentifier& InIdentifier); + /** Construct with identifier, parent and optionally HAPI node id. */ + FUnrealObjectInputNode(const FUnrealObjectInputIdentifier& InIdentifier, const FUnrealObjectInputHandle& InParent, const int32 InNodeId=INDEX_NONE); + + /** Destructor: deletes the HAPI node, if any, that is associated with this node. */ + virtual ~FUnrealObjectInputNode(); + + /** Getter for the identifier of this node. */ + const FUnrealObjectInputIdentifier& GetIdentifier() const { return Identifier; } + /** Gets a handle to the parent of this node. */ + const FUnrealObjectInputHandle& GetParent() const { return Parent; } + + /** Get the HAPI node id for this node. This can be -1 if no node has yet been created. */ + int32 GetHAPINodeId() const { return NodeId.IsValid() ? NodeId.GetHAPINodeId() : -1; } + /** Get the node id for this node. */ + FUnrealObjectInputHAPINodeId GetNodeId() const { return NodeId; } + /** Set the HAPI node id for this node.*/ + bool SetHAPINodeId(const int32 InHAPINodeId) { return NodeId.Set(InHAPINodeId); } + /** Set the node id for this node.*/ + void SetNodeId(const FUnrealObjectInputHAPINodeId& InNodeId) { NodeId = InNodeId; } + + /** + * Returns true if the node is dirty. That means that even though the node exists, it has been dirtied and + * its data should be resent to Houdini at the next opportunity. + */ + virtual bool IsDirty() const { return bIsDirty; } + + /** Marks the node as dirty. See IsDirty(). */ + virtual void MarkAsDirty() { bIsDirty = true; } + + /** Clears the dirty flag on the node. See IsDirty(). */ + virtual void ClearDirtyFlag() { bIsDirty = false; } + + /** Returns true if this node is ref counted by the manager. */ + virtual bool IsRefCounted() const { return false; } + + /** Gets the current reference count of this node. */ + virtual uint32 GetRefCount() const { return static_cast(ReferenceCount); } + + /** Returns true if the HAPI nodes (NodeId) associated with this node are valid. */ + virtual bool AreHAPINodesValid() const; + + /** Delete the HAPI node(s) of this node, if valid/exists. Returns true if nodes were deleted. */ + virtual bool DeleteHAPINodes(); + + /** Get the the valid HAPI node ids that this node is using. */ + virtual void GetHAPINodeIds(TArray& OutHAPINodeIds) const; + + /** Get the the valid HAPI node ids that this node is using. */ + virtual void GetHAPINodeIds(TArray& OutNodeIds) const; + + /** Returns true if the node can be deleted. */ + virtual bool CanBeDeleted() const { return bCanBeDeleted; } + + /** Returns true if the node can be deleted. */ + virtual void SetCanBeDeleted(const bool& InCanBeDeleted) { bCanBeDeleted = InCanBeDeleted; } + + /** + * Add a new modifier chain to the node. + * @param InChainName The name of the chain. + * @param InNodeIdToConnectTo The node to connect the first modifier's input to. + * @return True if the chain was added (and did not already exist). + */ + bool AddModifierChain(FName InChainName, int32 InNodeIdToConnectTo); + + /** + * Add a new modifier chain to the node. + * @param InChainName The name of the chain. + * @param InNodeIdToConnectTo The node to connect the first modifier's input to. + * @return True if the chain was added (and did not already exist). + */ + bool AddModifierChain(FName InChainName, const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo); + + /** Set the node that the first modifier's input should be connected to for the given chain. */ + bool SetModifierChainNodeToConnectTo(FName InChainName, int32 InNodeToConnectTo); + + /** Set the node that the first modifier's input should be connected to for the given chain. */ + bool SetModifierChainNodeToConnectTo(FName InChainName, const FUnrealObjectInputHAPINodeId& InNodeToConnectTo); + + /** Returns the number of modifier chains of this node. */ + int32 GetNumModifierChains() const { return ModifierChains.Num(); } + + /** Gets a modifier chain by name. Returns nullptr if no such chain exists. */ + const FUnrealObjectInputModifierChain* GetModifierChain(FName InChainName) const { return ModifierChains.Find(InChainName); } + + /** Get the input node of the chain given by InChainName. Returns < 0 if there is no such chain. */ + int32 GetInputHAPINodeIdOfModifierChain(FName InChainName) const; + + /** Get the input node of the chain given by InChainName. Returns an invalid id if there is no such chain. */ + FUnrealObjectInputHAPINodeId GetInputNodeIdOfModifierChain(FName InChainName) const; + + /** Get the output node of the chain given by InChainName. Returns < 0 if there is no such chain. */ + int32 GetOutputHAPINodeIdOfModifierChain(FName InChainName) const; + + /** Get the output node of the chain given by InChainName. Returns an invalid id if there is no such chain. */ + FUnrealObjectInputHAPINodeId GetOutputNodeIdOfModifierChain(FName InChainName) const; + + /** Removes the modifier chain with the name InChainName. Returns True if a chain was removed. */ + bool RemoveModifierChain(FName InChainName); + + /** + * Create and add a new modifier to the chain with name InChainName. The node owns the modifier and is responsible + * for deleting it (and freeing memory) when appropriate. + * @tparam T The class of the modifier, a subclass for FUnrealInputObjectModifier. + * @tparam Args The argument types to the constructor of T. + * @param InChainName The name of the modifier chain to create the modifier in. + * @param ConstructorArguments The arguments to the constructor of the modifier. + * @return The newly created and added modifier, or nullptr if the operation failed. + */ + template + T* CreateAndAddModifier(FName InChainName, Args... ConstructorArguments); + + /** + * Add a pre-created modifier to the given chain. The node takes over ownership of the modifier and becomes + * responsible for deleting it (freeing memory) when appropriate. + */ + bool AddModifier(FName InChainName, FUnrealObjectInputModifier* InModifierToAdd); + + /** Return the first modifier in the chain InChainName of the given InModifierType. Returns nullptr if no such modifier was found. */ + FUnrealObjectInputModifier* FindFirstModifierOfType(FName InChainName, EUnrealObjectInputModifierType InModifierType) const; + + /** + * Sets OutModifiers to contain all modifiers from the chain InChainName with type InModifierType to OutModifiers. + * @return True if the chain exists. + */ + bool GetAllModifiersOfType(FName InChainName, EUnrealObjectInputModifierType InModifierType, TArray& OutModifiers) const; + + /** + * Remove the given modifier, InModifier, from chain InChainName and delete it. + * @param InChainName The name of the chain that contains InModifier. + * @param InModifier The modifier to remove and delete. + * @return True if the chain exists and modifier was found, removed and deleted. + */ + bool DestroyModifier(FName InChainName, FUnrealObjectInputModifier* InModifier); + + /** Remove and delete all modifiers from the chain InChainName. Returns true if the chain exists. */ + bool DestroyModifiers(FName InChainName); + + /** + * Remove and delete all modifiers from all chains and also remove the chains themselves. + * @return True if all underlying calls to DestroyModifiers() returned true. + */ + bool DestroyAllModifierChains(); + + /** Get a const reference to the map of modifier chains. */ + const TMap& GetModifierChains() const { return ModifierChains; } + + /** + * Call FUnrealObjectInputModifier::Update() on all modifiers in chain InChainName. + * @return True if InChainName exists. + */ + bool UpdateModifiers(FName InChainName); + + /** + * Call UpdateModifiers() on all chains. + * @return True if all calls to UpdateModifiers() returned true. + */ + bool UpdateAllModifierChains(); + +protected: + friend class FUnrealObjectInputManagerImpl; + + /** Set the handle to the parent of this node. */ + void SetParent(const FUnrealObjectInputHandle& InParent) { Parent = InParent; } + + /** + * Increment the reference count of this node. Returns true if the count was increased. This should not be called + * directly, only via the manager. + */ + virtual bool AddRef() const; + + /** + * Decrement the reference count of this node. Returns true if the count was decreased (ie, it was > 0 when the + * function was called. + * This function does not delete anything if the count reaches 0, use FUnrealObjectInputManager::RemoveRef() for + * that. + * This should not be called directly, only via the manager. + */ + virtual bool RemoveRef() const; + +private: + /** The identifier of this node. */ + FUnrealObjectInputIdentifier Identifier; + + /** A handle to the parent of this node. */ + FUnrealObjectInputHandle Parent; + + /** The id of the main HAPI node associated with this node. */ + FUnrealObjectInputHAPINodeId NodeId; + + /** Indicates if the node is dirty or not. Dirty nodes should have their data resent to Houdini. */ + bool bIsDirty; + + /** The reference count of this node. */ + mutable int32 ReferenceCount; + + /** Indicates if this node can be deleted. Used to prevent the destruction of input nodes created by NodeSync*/ + bool bCanBeDeleted; + + /** Modifiers on this node */ + TMap ModifierChains; +}; + + +/** + * A container node: a node that acts as a parent node to other nodes, usually a Subnet object. + */ +class HOUDINIENGINERUNTIME_API FUnrealObjectInputContainerNode : public FUnrealObjectInputNode +{ +public: + FUnrealObjectInputContainerNode(const FUnrealObjectInputIdentifier& InIdentifier); + FUnrealObjectInputContainerNode(const FUnrealObjectInputIdentifier& InIdentifier, const FUnrealObjectInputHandle& InParent, const int32 InNodeId); + + virtual bool IsRefCounted() const override { return true; } +}; + + +/** + * A leaf node: a node that represents an object/asset from Unreal. For example, a locked null SOP with geometry sent + * from Unreal for a static mesh. + * The identifier must have Options set. + */ +class HOUDINIENGINERUNTIME_API FUnrealObjectInputLeafNode : public FUnrealObjectInputNode +{ +public: + FUnrealObjectInputLeafNode(const FUnrealObjectInputIdentifier& InIdentifier); + /** Construct via an identifier, parent, HAPI object node id (for example, a geo Object) and HAPI node id (for example a null SOP). */ + FUnrealObjectInputLeafNode(const FUnrealObjectInputIdentifier& InIdentifier, const FUnrealObjectInputHandle& InParent, const int32 InObjectNodeId, const int32 InNodeId); + /** Destructor: destroys the HAPI nodes is valid/exists. */ + virtual ~FUnrealObjectInputLeafNode(); + + virtual bool IsRefCounted() const override { return true; } + + /** Gets the HAPI object node id for this node. Can be < 0 if the node has not yet been created. */ + int32 GetObjectHAPINodeId() const { return ObjectNodeId.GetHAPINodeId(); } + /** Sets the HAPI object node id for this node. */ + bool SetObjectHAPINodeId(const int32 InObjectNodeId) { return ObjectNodeId.Set(InObjectNodeId); } + + /** Gets the HAPI object node id for this node. Can be -1 if the node has not yet been created. */ + FUnrealObjectInputHAPINodeId GetObjectNodeId() const { return ObjectNodeId; } + /** Sets the HAPI object node id for this node. */ + void SetObjectNodeId(const FUnrealObjectInputHAPINodeId& InObjectNodeId) { ObjectNodeId = InObjectNodeId; } + + virtual bool AreHAPINodesValid() const override; + + virtual bool DeleteHAPINodes() override; + + virtual void GetHAPINodeIds(TArray& OutNodeIds) const override; +private: + /** The HAPI object node id for this node, for example a geo Object. */ + FUnrealObjectInputHAPINodeId ObjectNodeId; +}; + + +/** + * A reference node, a node that references leaf nodes. For example, a node for a static mesh asset that references + * specific nodes for main mesh, sockets, lods and colliders. + * The identifier must have Options set. + */ +class HOUDINIENGINERUNTIME_API FUnrealObjectInputReferenceNode : public FUnrealObjectInputLeafNode +{ +public: + FUnrealObjectInputReferenceNode(const FUnrealObjectInputIdentifier& InIdentifier); + /** Construct via an identifier, parent, HAPI object node id (for example, a geo Object) and HAPI node id (for example a merge SOP). */ + FUnrealObjectInputReferenceNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const FUnrealObjectInputHandle& InParent, + const int32 InObjectNodeId, + const int32 InNodeId, + const int32 InReferencesConnectToNodeId=INDEX_NONE); + /** + * Construct via an identifier, parent, HAPI object node id (for example, a geo Object) and HAPI node id (for + * example a merge SOP), and a set of handles for nodes it requires. + */ + FUnrealObjectInputReferenceNode( + const FUnrealObjectInputIdentifier& InIdentifier, + const FUnrealObjectInputHandle& InParent, + const int32 InObjectNodeId, + const int32 InNodeId, + const TSet& InReferencedNodes, + const int32 InReferencesConnectToNodeId=INDEX_NONE); + + virtual bool IsRefCounted() const override { return true; } + + /** + * Add a node to the set of nodes this node references. + * @param InHandle Handle to the node to add. + * @return if the node was added to the references. + */ + bool AddReferencedNode(const FUnrealObjectInputHandle& InHandle); + + /** + * Remove a node from the node's set of referenced nodes. + * @param InHandle Handle to the node to remove. + * @return if the node was removed to the references. + */ + bool RemoveReferencedNode(const FUnrealObjectInputHandle& InHandle) { return ReferencedNodes.Remove(FUnrealObjectInputBackLinkHandle(GetIdentifier(), InHandle.GetIdentifier())) > 0; } + + /** Set the set of nodes that this node references via handles. */ + void SetReferencedNodes(const TSet& InReferencedNodes); + + /** Get the set of handles to the nodes that this node references. */ + const TSet& GetReferencedNodes() const { return ReferencedNodes; } + + /** Get the set of handles to the nodes that this node references. */ + void GetReferencedNodes(TSet& OutReferencedNodes) const { OutReferencedNodes = ReferencedNodes; } + + /** Get the set of handles to the nodes that this node references. */ + void GetReferencedNodes(TSet& OutReferencedNodes) const; + + /** Returns true if all of the nodes that this node references have valid HAPI nodes. */ + virtual bool AreReferencedHAPINodesValid() const; + + /** Marks this and optionally its referenced nodes as dirty. See IsDirty(). */ + virtual void MarkAsDirty(bool bInAlsoDirtyReferencedNodes); + + /** + * Get the NodeId of nodes that references are connected to (such as a Merge SOP). + * @return If ReferencesConnectToNodeId is set, then return that node id, otherwise return NodeId. + */ + FUnrealObjectInputHAPINodeId GetReferencesConnectToNodeId() const + { + if (!ReferencesConnectToNodeId.IsSet()) + return GetNodeId(); + return ReferencesConnectToNodeId.GetValue(); + } + + /** Set the node that references connect to. See GetReferencesConnectToNodeId() */ + void SetReferencesConnectToNodeId(const FUnrealObjectInputHAPINodeId& InReferencesConnectToNodeId) { ReferencesConnectToNodeId = InReferencesConnectToNodeId; } + + /** Set the node that references connect to. See GetReferencesConnectToNodeId() */ + void SetReferencesConnectToNodeId(const int32 InReferencesConnectToNodeId); + + /** Clears ReferencesConnectToNodeId. See GetReferencesConnectToNodeId() */ + void ResetReferencesConnectToNodeId() { ReferencesConnectToNodeId.Reset(); } + +private: + /** Marks this node as dirty. See IsDirty(). Internal use only, prefer MarkAsDirty(bool bInAlsoDirtyReferencedNodes). */ + virtual void MarkAsDirty() override { FUnrealObjectInputLeafNode::MarkAsDirty(); } + + /** The set of nodes that this node references, by handle. */ + TSet ReferencedNodes; + + /** HAPI Node id of the node that the references connect to, if different from NodeId (GetNodeId) */ + TOptional ReferencesConnectToNodeId; +}; + + +/** + * Modifiers represent additional nodes that are applied to input nodes. For example, adding a wrangle for setting + * material overrides for an input node that represents a StaticMeshComponent. + * + * The modifiers are added to chains (FUnrealObjectInputModifierChain) and the chains and the modifiers are owned + * by a FUnrealObjectInputNode. + */ +class HOUDINIENGINERUNTIME_API FUnrealObjectInputModifier +{ +public: + FUnrealObjectInputModifier() = delete; + // Don't allow copy constructing since the modifier is linked to HAPI nodes + FUnrealObjectInputModifier(const FUnrealObjectInputModifier& InCopyFrom) = delete; + + FUnrealObjectInputModifier(FUnrealObjectInputNode& InOwner) : bNeedsRebuild(true), Owner(InOwner) {} + + /** Static function for getting the modifier type as EUnrealObjectInputModifierType enum. */ + static EUnrealObjectInputModifierType StaticGetType() { return EUnrealObjectInputModifierType::Invalid; } + /** Return the modifier type as EUnrealObjectInputModifierType enum. */ + virtual EUnrealObjectInputModifierType GetType() const { return StaticGetType(); } + + virtual ~FUnrealObjectInputModifier() { DestroyHAPINodes(); } + + /** Returns the FUnrealObjectInputNode that owns this modifier. */ + FUnrealObjectInputNode& GetOwner() const { return Owner; } + + /** Mark this modifier as requiring rebuild: at the next update all of its HAPI nodes will be removed and recreated. */ + void MarkAsNeedsRebuild() { bNeedsRebuild = true; } + + /** Returns true if this modifier needs to be rebuilt. See MarkAsNeedsRebuild(). */ + bool NeedsRebuild() const { return bNeedsRebuild; } + + /** Get the HAPI nodes that this modifier created. */ + const TArray& GetHAPINodeIds() const { return HAPINodeIds; } + + /** Return the first node / input node of the modifier. */ + virtual FUnrealObjectInputHAPINodeId GetInputNodeId() const { return HAPINodeIds.Num() > 0 ? HAPINodeIds[0] : FUnrealObjectInputHAPINodeId(); } + + /** Return the first node / input node of the modifier. */ + virtual int32 GetInputHAPINodeId() const { return GetInputNodeId().GetHAPINodeId(); } + + /** Return the last node / output node of the modifier. */ + virtual FUnrealObjectInputHAPINodeId GetOutputNodeId() const { return HAPINodeIds.Num() > 0 ? HAPINodeIds.Last() : FUnrealObjectInputHAPINodeId(); } + + /** Return the last node / output node of the modifier. */ + virtual int32 GetOutputHAPINodeId() const { return GetOutputNodeId().GetHAPINodeId(); } + + /** This function is called when a modifier is added to its owner FUnrealObjectInputNode, just after creation. */ + virtual void OnAddedToOwner() {} + + /** This function is called when this modifier is removed from its owner. */ + virtual void OnRemovedFromOwner() { DestroyHAPINodes(); } + + /** Destroy all existing / valid nodes in HAPINodeIds and empty HAPINodeIds. */ + bool DestroyHAPINodes(); + + /** + * Updates the modifier. Creates missing nodes and re-sets all parameters on the nodes. + * @param InNodeIdToConnectTo The node to connect to the input of GetInputHAPINodeId(). + */ + virtual bool Update(const FUnrealObjectInputHAPINodeId& InNodeIdToConnectTo) = 0; + +protected: + /** HAPI nodes created/managed by this modifier. */ + TArray HAPINodeIds; + + /** If true indicates that the existing, if any, HAPI nodes need to be deleted and rebuilt. */ + bool bNeedsRebuild; + +private: + /** The FUnrealObjectInputNode that owns this modifier. */ + FUnrealObjectInputNode& Owner; +}; + +/** + * An update scope that registers itself with the manager on construction and removes itself on destruction. + * + * While active the manager reports every node that is created or updated to the scope. Identifiers to the nodes are + * available as a set before destruction of the scope. + */ +class HOUDINIENGINERUNTIME_API FUnrealObjectInputUpdateScope +{ +public: + FUnrealObjectInputUpdateScope(); + + ~FUnrealObjectInputUpdateScope(); + +public: + + const TSet& GetNodesCreatedOrUpdated() const { return NodesCreatedOrUpdated; } + const TSet& GetNodesDestroyed() const { return NodesDestroyed; } + +protected: + void OnNodeCreatedOrUpdated(const FUnrealObjectInputIdentifier& InIdentifier); + void OnNodeDestroyed(const FUnrealObjectInputIdentifier& InIdentifier); + +private: + TSet NodesCreatedOrUpdated; + TSet NodesDestroyed; + + FDelegateHandle OnCreatedHandle; + FDelegateHandle OnUpdatedHandle; + FDelegateHandle OnDestroyedHandle; +}; + + +template +void FUnrealObjectInputOptions::SetSelectedComponents(const TSet>& InSelectedComponents) +{ + static_assert(std::is_base_of::value, "T must derive from UActorComponent"); + + SelectedComponents.Empty(InSelectedComponents.Num()); + for (T* const ActorComponent : InSelectedComponents) + { + if (!IsValid(ActorComponent)) + continue; + SelectedComponents.Add(ActorComponent); + } + ComputeSelectedComponentsHash(); +} + + +template +T* FUnrealObjectInputNode::CreateAndAddModifier(const FName InChainName, Args... ConstructorArguments) +{ + static_assert(std::is_base_of::value, "T must derive from FUnrealObjectInputModifier"); + + FUnrealObjectInputModifierChain* const Chain = ModifierChains.Find(InChainName); + if (!Chain) + return nullptr; + + T* Modifier = new T(*this, ConstructorArguments...); + check(Modifier); + + Chain->Modifiers.Emplace(Modifier); + + Modifier->OnAddedToOwner(); + + return Modifier; +} + + +``` + +`Source/HoudiniEngineRuntime/Private/UnrealObjectInputRuntimeUtils.cpp`: + +```cpp +#include "UnrealObjectInputRuntimeUtils.h" + +#include "HoudiniRuntimeSettings.h" +#include "UnrealObjectInputRuntimeTypes.h" +#include "UnrealObjectInputManager.h" + +bool +FUnrealObjectInputRuntimeUtils::IsInputNodeDirty(const FUnrealObjectInputIdentifier& InIdentifier) +{ + if (!InIdentifier.IsValid()) + return false; + + const FUnrealObjectInputManager * Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + return Manager->IsDirty(InIdentifier); +} + +bool +FUnrealObjectInputRuntimeUtils::MarkInputNodeAsDirty(const FUnrealObjectInputIdentifier& InIdentifier, const bool bInAlsoDirtyReferencedNodes) +{ + if (!InIdentifier.IsValid()) + return false; + + FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + return Manager->MarkAsDirty(InIdentifier, bInAlsoDirtyReferencedNodes); +} + +bool +FUnrealObjectInputRuntimeUtils::ClearInputNodeDirtyFlag(const FUnrealObjectInputIdentifier& InIdentifier) +{ + if (!InIdentifier.IsValid()) + return false; + + FUnrealObjectInputManager* const Manager = FUnrealObjectInputManager::Get(); + if (!Manager) + return false; + + return Manager->ClearDirtyFlag(InIdentifier); +} + +``` + +`Source/HoudiniEngineRuntime/Private/UnrealObjectInputRuntimeUtils.h`: + +```h +#pragma once + +#include "HoudiniEngineRuntimePrivatePCH.h" + +// Houdini Engine forward declarations +class FUnrealObjectInputIdentifier; + + +struct HOUDINIENGINERUNTIME_API FUnrealObjectInputRuntimeUtils +{ +public: + // Helper for checking if an input node is marked as dirty in the ref counted input system. + static bool IsInputNodeDirty(const FUnrealObjectInputIdentifier& InIdentifier); + + // Helper for marking an input node, and optionally for reference nodes its referenced nodes, as dirty in the ref counted input system + static bool MarkInputNodeAsDirty(const FUnrealObjectInputIdentifier& InIdentifier, bool bInAlsoDirtyReferencedNodes); + + // Helper for clearing the dirty flag an input node in the ref counted input system. + static bool ClearInputNodeDirtyFlag(const FUnrealObjectInputIdentifier& InIdentifier); +}; + +``` + +`Source/HoudiniEngineRuntime/Public/HoudiniAsset.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "UObject/Object.h" +#include "Runtime/Launch/Resources/Version.h" +#include "HoudiniAsset.generated.h" + +class UAssetImportData; +class UHoudiniToolData; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 +class FAssetRegistryTagsContext; +#endif + +UCLASS(BlueprintType, EditInlineNew, config = Engine) +class HOUDINIENGINERUNTIME_API UHoudiniAsset : public UObject +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + + public: + + // UOBject functions + virtual void FinishDestroy() override; + virtual void Serialize(FArchive & Ar) override; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4 + virtual void GetAssetRegistryTags(FAssetRegistryTagsContext Context) const override; +#endif + virtual void GetAssetRegistryTags(TArray& OutTags) const override; + + // Creates and initialize this asset from a given buffer / file. + void CreateAsset(const uint8 * BufferStart, const uint8 * BufferEnd, const FString & InFileName); + + // Return buffer containing the raw Houdini OTL data. + const uint8* GetAssetBytes() const; + + // Return path of the corresponding OTL/HDA file. + const FString& GetAssetFileName() const; + + // Return the size in bytes of raw Houdini OTL data. + uint32 GetAssetBytesCount() const; + + // Return true if this asset is a limited commercial asset. + bool IsAssetLimitedCommercial() const; + + // Return true if this asset is a non commercial asset. + bool IsAssetNonCommercial() const; + + // Return true if this asset is an expanded HDA (HDA dir) + bool IsExpandedHDA() const; + +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + + virtual void PostSaveRoot(FObjectPostSaveRootContext ObjectSaveContext) override; + + private: + // Used to load old (version1) versions of HoudiniAssets + void SerializeLegacy(FArchive & Ar); + + public: + + // Source filename of the OTL. + UPROPERTY() + FString AssetFileName; + +#if WITH_EDITORONLY_DATA + // Importing data and options used for this Houdini asset. + UPROPERTY(Category = ImportSettings, VisibleAnywhere, Instanced) + TObjectPtr AssetImportData; + + // HoudiniTool related data stored on the Houdini asset. + UPROPERTY(Category = HoudiniTools, VisibleAnywhere, Instanced) + TObjectPtr HoudiniToolData; +#endif + + private: + + // Buffer containing the raw HDA OTL data. + UPROPERTY() + TArray AssetBytes; + + // Size in bytes of the raw HDA data. + UPROPERTY() + uint32 AssetBytesCount; + + // Indicates if this is a limited commercial asset. + UPROPERTY() + bool bAssetLimitedCommercial; + + // Indicates if this is a non-commercial license asset. + UPROPERTY() + bool bAssetNonCommercial; + + // Indicates if this is an expanded HDA file + UPROPERTY() + bool bAssetExpanded; +}; +``` + +`Source/HoudiniEngineRuntime/Public/HoudiniAssetActor.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "UObject/ObjectMacros.h" +#include "Components/ActorComponent.h" +#include "GameFramework/Actor.h" + +#include "HoudiniAssetActor.generated.h" + +class UHoudiniAssetComponent; +class UHoudiniCookable; +class UHoudiniPDGAssetLink; + +UCLASS(hidecategories = (Input), ConversionRoot, meta = (ChildCanTick), Blueprintable) +class HOUDINIENGINERUNTIME_API AHoudiniAssetActor : public AActor +{ + GENERATED_UCLASS_BODY() + + friend class FHoudiniEditorEquivalenceUtils; + + // Pointer to the root HoudiniAssetComponent + UPROPERTY(Category = HoudiniAssetActor, VisibleAnywhere, BlueprintReadOnly, meta = (ExposeFunctionCategories = "Mesh,Rendering,Physics,Components|HoudiniEngine")/*, AllowPrivateAccess = "true"*/) + TObjectPtr HoudiniAssetComponent; + + // Cookable for this HAA + UPROPERTY() + TObjectPtr HoudiniCookable; + +public: + + // Returns the actor's houdini component. + UHoudiniAssetComponent* GetHoudiniAssetComponent() const; + + // Returns the actor's cookable. + UHoudiniCookable* GetHoudiniCookable() const; + + bool IsUsedForPreview() const; + + // Gets the Houdini PDG asset link associated with this actor, if it has one. + UHoudiniPDGAssetLink* GetPDGAssetLink() const; + + // Set the Houdini Actor to be a NodeSyncActor + void SetNodeSyncActor(bool bIsNodeSync); + + // Indicates if this Actor is a NodeSyncActor + bool IsNodeSyncActor() const; + + +#if WITH_EDITOR + + // Called after a property has been changed + // Used to forward property changes to the HAC + virtual void PostEditChangeProperty(FPropertyChangedEvent & PropertyChangedEvent) override; + + // Used by the "Sync to Content Browser" right-click menu option in the editor. + virtual bool GetReferencedContentObjects(TArray& Objects) const; + +#endif +}; + +``` + +`Source/HoudiniEngineRuntime/Public/HoudiniEngineRuntimeCommon.h`: + +```h +/* +* Copyright (c) <2021> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "HoudiniEngineRuntimeCommon.generated.h" + +UENUM() +enum class EHoudiniRampInterpolationType : int8 +{ + InValid = -1, + + CONSTANT = 0, + LINEAR = 1, + CATMULL_ROM = 2, + MONOTONE_CUBIC = 3, + BEZIER = 4, + BSPLINE = 5, + HERMITE = 6 +}; + +UENUM() +enum class EHoudiniEngineBakeOption : uint8 +{ + ToActor, + ToBlueprint, + ToAsset +}; + +UENUM() +enum class EHoudiniEngineActorBakeOption : uint8 +{ + OneActorPerComponent, + OneActorPerHDA +}; + +UENUM() +enum class EHoudiniLandscapeOutputBakeType : uint8 +{ + Detachment, + BakeToImage, + BakeToWorld, + InValid, +}; + +UENUM() +enum class EHoudiniInputType : uint8 +{ + Invalid = 0, + + Geometry = 1, + + Curve = 2, + + /* Asset = 3 - deprecated in Houdini 20 */ + + /* Landscape = 4 - deprecated in Houdini 20 */ + + World = 5, + + /* Skeletal = 6 - deprecated in Houdini 20 */ + + /* Geometry Collection = 7 - deprecated in Houdini 20 */ + PCGInput = 8 + // !! The next input type should be 9 !! + +}; + +UENUM() +enum class EHoudiniOutputType : uint8 +{ + Invalid, + + Mesh, + Instancer, + Landscape, + Curve, + Skeletal, + GeometryCollection, + DataTable, + LandscapeSpline, + AnimSequence, + Cop, + PCG +}; + +UENUM() +enum class EHoudiniCurveType : int8 +{ + Invalid = -1, + + Polygon = 0, + Nurbs = 1, + Bezier = 2, + Points = 3 +}; + +UENUM() +enum class EHoudiniCurveMethod : int8 +{ + Invalid = -1, + + CVs = 0, + Breakpoints = 1, + Freehand = 2 +}; + +UENUM() +enum class EHoudiniCurveBreakpointParameterization : int8 +{ + Invalid = -1, + Uniform = 0, + Chord = 1, + Centripetal = 2 +}; + +UENUM() +enum class EHoudiniLandscapeExportType : uint8 +{ + Heightfield, + Mesh, + Points +}; + +#if WITH_EDITORONLY_DATA +UENUM() +enum class EPDGBakeSelectionOption : uint8 +{ + All, + SelectedNetwork, + SelectedNode +}; +#endif + +#if WITH_EDITORONLY_DATA +UENUM() +enum class EPDGBakePackageReplaceModeOption : uint8 +{ + CreateNewAssets, + ReplaceExistingAssets +}; +#endif + +// When attempting to refine proxy mesh outputs it is a possible that a cook is needed. +// This enum defines the possible return values on a request to refine proxies. +UENUM() +enum class EHoudiniProxyRefineResult : uint8 +{ + Invalid, + + // Refinement (or cook if needed) failed + Failed, + // Refinement completed successfully + Success, + // Refinement was skipped, either it was not necessary or the operation was cancelled by the user + Skipped +}; + +// When attempting to refine proxy mesh outputs it is a possible that a cook is needed. +// This enum defines the possible return values on a request to refine proxies. +UENUM() +enum class EHoudiniProxyRefineRequestResult : uint8 +{ + Invalid, + + // No refinement is needed + None, + // A cook is needed, refinement will commence automatically after the cook + PendingCooks, + // Successfully refined + Refined +}; + +UENUM() +enum class EHoudiniStaticMeshMethod : uint8 +{ + // Static Meshes will be generated by using Raw Meshes. + RawMesh_DELETED, + // Static Meshes will be generated by using Mesh Descriptions. + FMeshDescription, + // Always build Houdini Proxy Meshes (dev) + UHoudiniStaticMesh, +}; + +UENUM() +enum class EHoudiniBakeAfterNextCook : uint8 +{ + // Do not bake after cook + Disabled, + // Always bake after cook if cook was successful, + Always, + // Bake after the next successful cook, then reset to Disabled. + Once +}; + + +``` + +`Source/HoudiniEngineRuntime/Public/HoudiniPCGDataObject.h`: + +```h +/* + * Copyright (c) <2021> Side Effects Software Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. The name of Side Effects Software may not be used to endorse or + * promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include "UObject/NameTypes.h" +#include +#include "Runtime/Launch/Resources/Version.h" + +#include "HoudiniPCGDataObject.generated.h" + +class UPCGPointData; +class FUnrealObjectInputHandle; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataAttributeBase : public UObject +{ + GENERATED_BODY() +public: + + void SetAttrName(const FString& Name); + const FName& GetAttrName() const; + + virtual int GetNumValues() const { return 0; } +private: + UPROPERTY() + FName AttrName; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataAttributeFloat : public UHoudiniPCGDataAttributeBase +{ + GENERATED_BODY() +public: + virtual int GetNumValues() const override { return Values.Num(); } + + UPROPERTY() + TArray Values; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataAttributeDouble : public UHoudiniPCGDataAttributeBase +{ + GENERATED_BODY() + +public: + virtual int GetNumValues() const override { return Values.Num(); } + + UPROPERTY() + TArray Values; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataAttributeInt : public UHoudiniPCGDataAttributeBase +{ + GENERATED_BODY() +public: + virtual int GetNumValues() const override { return Values.Num(); } + + UPROPERTY() + TArray Values; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataAttributeInt64 : public UHoudiniPCGDataAttributeBase +{ + GENERATED_BODY() +public: + virtual int GetNumValues() const override { return Values.Num(); } + + UPROPERTY() + TArray Values; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataAttributeVector2d : public UHoudiniPCGDataAttributeBase +{ + GENERATED_BODY() +public: + virtual int GetNumValues() const override { return Values.Num(); } + + UPROPERTY() + TArray Values; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataAttributeVector3d : public UHoudiniPCGDataAttributeBase +{ + GENERATED_BODY() +public: + virtual int GetNumValues() const override { return Values.Num(); } + + UPROPERTY() + TArray Values; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataAttributeQuat : public UHoudiniPCGDataAttributeBase +{ + GENERATED_BODY() + +public: + virtual int GetNumValues() const override { return Values.Num(); } + + UPROPERTY() + TArray Values; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataAttributeVector4d : public UHoudiniPCGDataAttributeBase +{ + GENERATED_BODY() + +public: + virtual int GetNumValues() const override { return Values.Num(); } + + UPROPERTY() + TArray Values; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataAttributeString: public UHoudiniPCGDataAttributeBase +{ + GENERATED_BODY() + +public: + virtual int GetNumValues() const override { return Values.Num(); } + + UPROPERTY() + TArray Values; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataAttributeSoftObjectPath : public UHoudiniPCGDataAttributeBase +{ + GENERATED_BODY() + +public: + virtual int GetNumValues() const override { return Values.Num(); } + + UPROPERTY() + TArray Values; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataAttributeSoftClassPath : public UHoudiniPCGDataAttributeBase +{ + GENERATED_BODY() + +public: + virtual int GetNumValues() const override { return Values.Num(); } + + UPROPERTY() + TArray Values; +}; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataObject : public UObject +{ + // This class serves a cache of UPCGData (Point or Param) data. We copy it out as + // the Unreal data is not serializable. Possibly + // this is not needed is we just store a CRC, and not the data... but this also + // protects us from API changes. + + GENERATED_BODY() +public: + void SetFromPCGData(const UPCGData* PCGParamaData, const TSet & Tags = {}); + void SetFromPCGData(const UPCGParamData* PCGParamaData); + void SetFromPCGData(const UPCGSplineData* PCGSplineData); + void SetFromPCGData(const UPCGPointData* PCGPointData); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6 + void SetFromPCGBasePointData(const UPCGBasePointData* PCGParamaData); + void SetFromPCGData(const UPCGPointArrayData* PCGParamaData); +#endif + + bool operator==(const UHoudiniPCGDataObject& Other) const; + bool operator!=(const UHoudiniPCGDataObject& Other) const; + int GetNumRows() const; + + UHoudiniPCGDataAttributeBase* FindAttribute(const FString& AttrName); + + UPROPERTY() + TArray> Attributes; + + UPROPERTY() + EPCGDataType PCGDataType; + + UPROPERTY() + TSet PCGTags; + + UPROPERTY() + bool bIsClosed = false; + +private: + void AddMetaDataAttributes(const UPCGMetadata* PCGParamaData, const TArray & Keys); + + UHoudiniPCGDataAttributeString* CreateAttributeString(const FString& AttributeName); + UHoudiniPCGDataAttributeVector4d* CreateAttributeVector4d(const FString& AttributeName); + UHoudiniPCGDataAttributeFloat* CreateAttributeFloat(const FString& AttributeName); + UHoudiniPCGDataAttributeDouble* CreateAttributeDouble(const FString& AttributeName); + UHoudiniPCGDataAttributeInt* CreateAttributeInt(const FString& AttributeName); + UHoudiniPCGDataAttributeInt64* CreateAttributeInt64(const FString& AttributeName); + UHoudiniPCGDataAttributeVector2d* CreateAttributeVector2d(const FString& AttributeName); + UHoudiniPCGDataAttributeVector3d* CreateAttributeVector3d(const FString& AttributeName); + UHoudiniPCGDataAttributeQuat* CreateAttributeQuat(const FString& AttributeName); + UHoudiniPCGDataAttributeSoftObjectPath* CreateAttributeSoftObjectPath(const FString& AttributeName); + UHoudiniPCGDataAttributeSoftClassPath* CreateAttributeSoftClassPath(const FString& AttributeName); + +}; + +UENUM() +enum class EHoudiniPCGDataType +{ + InputPCGNone, + InputPCGGeometry, + InputPCGSplines +}; +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGDataCollection : public UObject +{ + GENERATED_BODY() + +public: + + bool operator==(const UHoudiniPCGDataCollection& Other) const; + bool operator!=(const UHoudiniPCGDataCollection& Other) const; + + void AddObject(UHoudiniPCGDataObject * Object); + + UPROPERTY() + EHoudiniPCGDataType Type; + + UPROPERTY() + TObjectPtr Details; + + UPROPERTY() + TObjectPtr Primitives; + + + UPROPERTY() + TObjectPtr Vertices; + + UPROPERTY() + TObjectPtr Points; + + UPROPERTY() + TArray> Splines; + +}; + +class UPCGMetadata; +class UPCGParamData; + +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniPCGOutputData : public UObject +{ +public: + GENERATED_UCLASS_BODY() + + UPROPERTY() + TObjectPtr DetailsParams = nullptr; + + UPROPERTY() + TObjectPtr PrimsParams = nullptr; + + UPROPERTY() + TObjectPtr VertexParams = nullptr; + + UPROPERTY() + TObjectPtr PointParams = nullptr; + + UPROPERTY() + TArray> SplineParams; + +}; +``` + +`Source/HoudiniEngineRuntime/Public/HoudiniToolData.h`: + +```h +/* +* Copyright (c) <2023> Side Effects Software Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. The name of Side Effects Software may not be used to endorse or +* promote products derived from this software without specific prior +* written permission. +* +* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS +* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "HoudiniToolTypes.h" + +#include "HoudiniToolData.generated.h" + +struct FImage; +enum class EAssetCommandResult : uint8; +struct FAssetCategoryPath; +struct FAssetOpenArgs; +class UHoudiniAsset; + +// Mirrors the data from FImage +// We make our own struct so that we have control over any struct changes +USTRUCT() +struct HOUDINIENGINERUNTIME_API FHImageData +{ + GENERATED_BODY() + + FHImageData(); + + /** Width of the image. */ + UPROPERTY() + int32 SizeX; + + /** Height of the image. */ + UPROPERTY() + int32 SizeY; + + /** Number of image slices. */ + UPROPERTY() + int32 NumSlices; + + /** Format in which the image is stored. (ERawImageFormat::Type)*/ + UPROPERTY() + uint8 Format; + + /** The gamma space the image is stored in. (EGammaSpace)*/ + UPROPERTY() + uint8 GammaSpace; + + UPROPERTY() + TArray RawData; + + /** MD5 hash of the RawData **/ + UPROPERTY() + FString RawDataMD5; + + // Assignment overloads + void FromImage(const FImage& InImage); + void ToImage(FImage& OutImage) const; + +}; + +// Houdini tool data that is cached on the Houdini Asset to allow the +// Houdini Tools to function independently of external tool packages. +UCLASS() +class HOUDINIENGINERUNTIME_API UHoudiniToolData : public UObject +{ + GENERATED_BODY() + + /** Only valid in the editor */ + virtual bool IsEditorOnly() const override { return true; } + +public: + + UHoudiniToolData(); + + // Populate this object from the given JSON data. + UFUNCTION(BlueprintCallable, Category="HoudiniToolData") + bool PopulateFromJSONData(const FString& JSONData); + + // Read the content of a JSON file, then populate this object. + UFUNCTION(BlueprintCallable, Category="HoudiniToolData") + bool PopulateFromJSONFile(const FString& JsonFilePath); + + // Convert the data from this HoudiniTool into JSON. + UFUNCTION(BlueprintCallable, Category="HoudiniToolData") + bool ConvertToJSONData(FString& JSONData) const; + + // After converting the data from this object to JSON, save it to a file. + UFUNCTION(BlueprintCallable, Category="HoudiniToolData") + bool SaveToJSONFile(const FString& JsonFilePath); + +#if WITH_EDITOR + // Load an icon from the given path (on the filesystem) + // and cache it in this UHoudiniToolData. This will also + // cache it as a thumbnail for the owning package of this asset. + void LoadIconFromPath(const FString& IconPath); + + // Clear the icon cached on this UHoudiniToolData instance and + // clear the cached thumbnail for the owning package. + void ClearCachedIcon(); + + // Update the owning asset's thumbnail + void UpdateOwningAssetThumbnail(); +#endif + + void CopyFrom(const UHoudiniToolData& Other); + + /** The name (label) to be displayed. Not to be confused with the operator type name or UAsset name */ + UPROPERTY() + FString Name; + + /** The tooltip for this HDA. */ + UPROPERTY() + FString ToolTip; + + /** Raw image data of the icon to be displayed */ + UPROPERTY() + FHImageData IconImageData; + + /** Path from which the current icon was imported */ + UPROPERTY() + FFilePath IconSourcePath; + + /** The help URL for this tool */ + UPROPERTY() + FString HelpURL; + + /** The type of tool, this will change how the asset handles the current selection **/ + UPROPERTY() + EHoudiniToolType Type; + + /** DEPRECATED: Indicate this is one of the default tools **/ + // TODO: Replace DefaultTool usages with the ReadOnlyTools property from the owning tools package instead. + UPROPERTY() + bool DefaultTool; + + /** Indicate what the tool should consider for selection **/ + UPROPERTY() + EHoudiniToolSelectionType SelectionType; + + /** Path to the Asset used **/ + UPROPERTY() + FFilePath SourceAssetPath; + + // /** Directory containing the tool **/ + // FHoudiniToolDirectory ToolDirectory; + + // NOTE: This should be inferred from the SourceAssetPath. + /** Name of the JSON containing the tool's description **/ + // UPROPERTY() + // FString JSONFile; + + /** Returns the file path to the JSON file containing the tool's description **/ + // FString GetJSonFilePath() { return ToolDirectory.Path.Path / JSONFile; }; + // TODO: Resolve the JSON file path based using the path relative to the Houdini Package. + FString GetJSonFilePath() { return FString(); }; + +}; + +``` \ No newline at end of file diff --git a/archive/stungeye/UE5-With-Dear-ImGui.txt b/archive/stungeye/UE5-With-Dear-ImGui.txt new file mode 100644 index 00000000..51884352 --- /dev/null +++ b/archive/stungeye/UE5-With-Dear-ImGui.txt @@ -0,0 +1,66046 @@ +Project Path: arc_stungeye_UE5-With-Dear-ImGui_5_q8tzrw + +Source Tree: + +```txt +arc_stungeye_UE5-With-Dear-ImGui_5_q8tzrw +├── Config +│ ├── DefaultEditor.ini +│ ├── DefaultEngine.ini +│ ├── DefaultGame.ini +│ ├── DefaultImGui.ini +│ └── HoloLens +│ └── HoloLensEngine.ini +├── Content +│ ├── BP_ImGuiActor.uasset +│ ├── DefaultMap.umap +│ ├── DefaultMap_HLOD0_Instancing.uasset +│ ├── __ExternalActors__ +│ │ └── DefaultMap +│ │ ├── 0 +│ │ │ ├── 9Y +│ │ │ │ └── SVHX05RKVPDG54W0TXA3GJ.uasset +│ │ │ ├── N0 +│ │ │ │ └── TA7GG875592Q1ZOU4INV5F.uasset +│ │ │ ├── OH +│ │ │ │ └── VQI7LWES8TQNDD3223OQN8.uasset +│ │ │ ├── RP +│ │ │ │ └── 7GEKV0H1SAZB54U1YABVXP.uasset +│ │ │ ├── TT +│ │ │ │ └── IE5HN27ZM73KFQ7XPFDWAT.uasset +│ │ │ └── W7 +│ │ │ └── UDR8BUMP7JNALUCWU3UEPC.uasset +│ │ ├── 1 +│ │ │ ├── 2T +│ │ │ │ └── L36SOHOKWLN2LKCLGEGYZS.uasset +│ │ │ ├── 6T +│ │ │ │ └── RHET9IUIP2TWOQSM8C4421.uasset +│ │ │ ├── AR +│ │ │ │ └── M46ACWCVADJIAOZ4J8XZPJ.uasset +│ │ │ ├── BG +│ │ │ │ └── MORE026HY14B78V4DXN723.uasset +│ │ │ ├── C8 +│ │ │ │ └── DXGFYELEW1RXWMVJG4UJKF.uasset +│ │ │ ├── DN +│ │ │ │ └── KGLPEQZ4QHRQ6KI3XLKBL5.uasset +│ │ │ ├── MH +│ │ │ │ └── NHN581J1T5I2Q93GDAYGLG.uasset +│ │ │ └── ON +│ │ │ └── UQUK8IG9XVFUHW0W4DJQL2.uasset +│ │ ├── 2 +│ │ │ ├── UV +│ │ │ │ └── 86AXHCF318E8GWO27GOZBA.uasset +│ │ │ └── YM +│ │ │ └── N0NQBHZG1Q33CVPJ2E6ARD.uasset +│ │ ├── 3 +│ │ │ ├── 4T +│ │ │ │ └── AHGWTQO2YY6J2ATT8QPBTG.uasset +│ │ │ ├── 5D +│ │ │ │ └── UXS1PGXU5R3NURIV6Y2RE8.uasset +│ │ │ ├── 6X +│ │ │ │ └── SJT638ZF72BVVKB0OJ39WP.uasset +│ │ │ ├── CQ +│ │ │ │ └── HFR32SPB22Z3A939AAH9PJ.uasset +│ │ │ ├── GT +│ │ │ │ └── A8XU7OTW3VK8V1U5JANH0P.uasset +│ │ │ ├── SZ +│ │ │ │ └── 379Q2JQD4RW3EAC60TQJP5.uasset +│ │ │ └── ZB +│ │ │ └── OD5RQR6GFGX19XV5JEC43R.uasset +│ │ ├── 4 +│ │ │ ├── 9F +│ │ │ │ └── FB227JCUJ0BVOP1BEUSV5J.uasset +│ │ │ ├── FQ +│ │ │ │ └── 3ZUJBOD56G08G98RW2QORV.uasset +│ │ │ ├── K2 +│ │ │ │ └── 3S74UE8DHJ1GGJ38KZNFDH.uasset +│ │ │ └── RW +│ │ │ └── B3X1E7OAF7S5FJOBVFI4KH.uasset +│ │ ├── 5 +│ │ │ ├── 4E +│ │ │ │ └── V75S3NU9ANW1BQ8BXXQACG.uasset +│ │ │ ├── B2 +│ │ │ │ └── LAJ3G1JCJSLYA3TSMZQM2R.uasset +│ │ │ └── V8 +│ │ │ └── ZGD17RLC4SF4XI1JB9H64K.uasset +│ │ ├── 6 +│ │ │ ├── 7B +│ │ │ │ └── DHV10ER34CPZW7SEVEHG40.uasset +│ │ │ ├── FH +│ │ │ │ └── 34SAMXGX6OI9I1N53LNSBZ.uasset +│ │ │ ├── K4 +│ │ │ │ └── B0LHPXQ9HE02ZLUZWK8SER.uasset +│ │ │ ├── Z2 +│ │ │ │ └── GFB4I2O9P4JIC98S4B1QEV.uasset +│ │ │ ├── Z9 +│ │ │ │ └── TAU73KBLBQN2C54XIDADAD.uasset +│ │ │ └── ZN +│ │ │ └── S3JQAYCMLW6POJ8QR27A85.uasset +│ │ ├── 7 +│ │ │ ├── 1R +│ │ │ │ └── 7G734Z2K6ERSZQ7YZ2X5OL.uasset +│ │ │ ├── 28 +│ │ │ │ └── L0OG8697CQFMATGI417LC6.uasset +│ │ │ ├── 2N +│ │ │ │ └── 0QFVEE9VQ0SBFFU11EUW2A.uasset +│ │ │ ├── 7L +│ │ │ │ └── 28PXTPA18G6G6VLAZW0QT7.uasset +│ │ │ ├── E6 +│ │ │ │ └── NLCUMLQJG8LOHJZPYSRKUK.uasset +│ │ │ ├── EM +│ │ │ │ └── 4F6HF1TE7DPHVJ7Z7VGZH0.uasset +│ │ │ ├── L5 +│ │ │ │ └── Y5GA81XN3J1QOBVXLO4B5W.uasset +│ │ │ ├── LB +│ │ │ │ └── QUOX4HUH1X8X8UPSUPRT0A.uasset +│ │ │ ├── LY +│ │ │ │ └── 6BWYB4OCI1GZ4UYQF0K5OL.uasset +│ │ │ └── QC +│ │ │ └── U0GQEOZUSO1T4CF9S62B4X.uasset +│ │ ├── 8 +│ │ │ ├── 2E +│ │ │ │ └── CFKV9WFUAT4K9HJEGB4CNK.uasset +│ │ │ ├── 4V +│ │ │ │ └── BLJ8QA7XUNFPY8THYT7XIK.uasset +│ │ │ ├── 7Z +│ │ │ │ └── 50P9U0415GQTZW9X5UA9PL.uasset +│ │ │ ├── 9B +│ │ │ │ └── 1TE845AF89TV0ORYB5BUDQ.uasset +│ │ │ └── EO +│ │ │ └── QUTNZHHH6Y0AKP418SN2DI.uasset +│ │ ├── 9 +│ │ │ ├── 1W +│ │ │ │ └── IOF8HZR1Z5YNKJN0U8PV66.uasset +│ │ │ ├── 6R +│ │ │ │ └── W5GPVB1X3AYPQMA8MPX0E8.uasset +│ │ │ ├── HR +│ │ │ │ └── VXYM8P2A8FKS7YGGCQZ5H9.uasset +│ │ │ └── LF +│ │ │ └── RJOBQMMZDJ9QQ4MVE7JUGM.uasset +│ │ ├── A +│ │ │ ├── 1X +│ │ │ │ └── U9QYQ9KSUXFL8OY22UEPY4.uasset +│ │ │ ├── 90 +│ │ │ │ └── XBODAJ126PYG770SLIGBAH.uasset +│ │ │ ├── AO +│ │ │ │ └── 0BAHM2A7SCWTL2ZHP5V8JW.uasset +│ │ │ ├── BI +│ │ │ │ └── QU8L0M6CEJS2GHCH6Y4VHB.uasset +│ │ │ ├── D5 +│ │ │ │ └── 7J8N4OJ93MFSA212IZZVAR.uasset +│ │ │ ├── OC +│ │ │ │ └── 828LG4NW7GEFM0FI74OJTQ.uasset +│ │ │ └── SU +│ │ │ └── HRITGRW2JZHMDB0BV4V3I6.uasset +│ │ ├── B +│ │ │ ├── JP +│ │ │ │ └── HYB31QKP4BSYZDMXFAX06V.uasset +│ │ │ ├── VY +│ │ │ │ └── B0HQD5D2JSQ75FQTV753OV.uasset +│ │ │ └── Y0 +│ │ │ └── NEM3J2MEAHTVMOSWPO4Q0M.uasset +│ │ ├── C +│ │ │ ├── 4V +│ │ │ │ └── MPVZ9XSV8Q72D7S1TGRA3G.uasset +│ │ │ ├── 8B +│ │ │ │ └── 17R9ULEOINC5S9Y66V09UC.uasset +│ │ │ ├── CY +│ │ │ │ └── N1CFJ75TNL4U98WXE3Z4PK.uasset +│ │ │ ├── H9 +│ │ │ │ └── KP46E4L81626LN1DBJ2H7H.uasset +│ │ │ ├── HB +│ │ │ │ └── GZ5P07750FVBG63CO3L5DY.uasset +│ │ │ ├── LR +│ │ │ │ └── IO4KW6GP5FVAWZGK7MACYU.uasset +│ │ │ ├── PO +│ │ │ │ └── H6ZPXIGKVHADV6OF7HAJQH.uasset +│ │ │ ├── Q0 +│ │ │ │ └── TD714GIOTXWXS9HSF38T03.uasset +│ │ │ ├── VJ +│ │ │ │ └── TWY0PQXTTVA24WZYGWXSNF.uasset +│ │ │ ├── W3 +│ │ │ │ └── PI8EEEUL8XGWBM0K3X70F2.uasset +│ │ │ └── YG +│ │ │ └── S851XLMV7AJ7782FIWMR25.uasset +│ │ ├── D +│ │ │ ├── 7E +│ │ │ │ └── 65XQW1874KIQEPX3MP1L9S.uasset +│ │ │ ├── 7T +│ │ │ │ └── IOBPB7Q9WSX4WDY27SLGMM.uasset +│ │ │ ├── 9V +│ │ │ │ └── 8M260NBP5ED2C01VXHUFB1.uasset +│ │ │ ├── C3 +│ │ │ │ └── 1GCIA6L77XQFDUMGC3MP14.uasset +│ │ │ ├── IT +│ │ │ │ └── W4AQ98QYDECB39RGPMCAWN.uasset +│ │ │ ├── IU +│ │ │ │ └── DUR0O0UIOVHR2N810ZHX0F.uasset +│ │ │ ├── LE +│ │ │ │ └── 4Q8M7CGZ6NA32IAIXBIZLE.uasset +│ │ │ ├── OC +│ │ │ │ └── YYD79NVVG8G2XIDEQOVJVJ.uasset +│ │ │ └── YQ +│ │ │ └── 37NRWPISTKSO9ILS1UXRLV.uasset +│ │ ├── E +│ │ │ ├── AZ +│ │ │ │ └── 8OSBHYS5RF86U1P38SBD1T.uasset +│ │ │ ├── B2 +│ │ │ │ └── 11QII6YZ53KRZ5XIG8KLWN.uasset +│ │ │ ├── DG +│ │ │ │ └── J4W76I4Q56JN2E1SS1FW5C.uasset +│ │ │ ├── FG +│ │ │ │ └── RXOLQXEKC6HQDABKHZF7JY.uasset +│ │ │ ├── HZ +│ │ │ │ └── 1P2J5EVWRH4S87IRR1I0R9.uasset +│ │ │ ├── M1 +│ │ │ │ └── EN3NORWQ7TGEXJU0ATK9U6.uasset +│ │ │ ├── NZ +│ │ │ │ └── 1VSGJ3JTOP6F4IQUDTVM11.uasset +│ │ │ ├── W8 +│ │ │ │ └── EQ9K681SIWR455XNGR52LB.uasset +│ │ │ └── Z2 +│ │ │ └── LWSPTPL6H8PTN5NSU9VLL8.uasset +│ │ └── F +│ │ └── 1M +│ │ └── 3L7EQKFB63WOIDWZFUAFI1.uasset +│ └── __ExternalObjects__ +│ └── DefaultMap +│ ├── 4 +│ │ └── OF +│ │ └── GS512JZKQELQZIU750VW5J.uasset +│ ├── 7 +│ │ └── VG +│ │ └── AWNYZPDY40J3LSLH62P3LX.uasset +│ ├── D +│ │ └── F5 +│ │ └── H0XW5QLM9FWEG9PITJ4HGL.uasset +│ └── E +│ ├── 2R +│ │ └── GQTCW3GPMHA7CWKBNJ24W8.uasset +│ └── GO +│ └── OZPSPNJCKXC1FXZQCSKYS1.uasset +├── GuiTest.uproject +├── Plugins +│ └── ImGui +│ ├── CHANGES.md +│ ├── ImGui.uplugin +│ ├── LICENSE +│ ├── README.md +│ ├── Resources +│ │ └── Icon128.png +│ └── Source +│ ├── ImGui +│ │ ├── ImGui.Build.cs +│ │ ├── Private +│ │ │ ├── Editor +│ │ │ │ ├── ImGuiCanvasSizeInfoCustomization.cpp +│ │ │ │ ├── ImGuiCanvasSizeInfoCustomization.h +│ │ │ │ ├── ImGuiEditor.cpp +│ │ │ │ ├── ImGuiEditor.h +│ │ │ │ ├── ImGuiKeyInfoCustomization.cpp +│ │ │ │ └── ImGuiKeyInfoCustomization.h +│ │ │ ├── ImGuiContextManager.cpp +│ │ │ ├── ImGuiContextManager.h +│ │ │ ├── ImGuiContextProxy.cpp +│ │ │ ├── ImGuiContextProxy.h +│ │ │ ├── ImGuiDelegates.cpp +│ │ │ ├── ImGuiDelegatesContainer.cpp +│ │ │ ├── ImGuiDelegatesContainer.h +│ │ │ ├── ImGuiDemo.cpp +│ │ │ ├── ImGuiDemo.h +│ │ │ ├── ImGuiDrawData.cpp +│ │ │ ├── ImGuiDrawData.h +│ │ │ ├── ImGuiImplementation.cpp +│ │ │ ├── ImGuiImplementation.h +│ │ │ ├── ImGuiInputHandler.cpp +│ │ │ ├── ImGuiInputHandlerFactory.cpp +│ │ │ ├── ImGuiInputHandlerFactory.h +│ │ │ ├── ImGuiInputState.cpp +│ │ │ ├── ImGuiInputState.h +│ │ │ ├── ImGuiInteroperability.cpp +│ │ │ ├── ImGuiInteroperability.h +│ │ │ ├── ImGuiModule.cpp +│ │ │ ├── ImGuiModuleCommands.cpp +│ │ │ ├── ImGuiModuleCommands.h +│ │ │ ├── ImGuiModuleDebug.h +│ │ │ ├── ImGuiModuleManager.cpp +│ │ │ ├── ImGuiModuleManager.h +│ │ │ ├── ImGuiModuleSettings.cpp +│ │ │ ├── ImGuiModuleSettings.h +│ │ │ ├── ImGuiTextureHandle.cpp +│ │ │ ├── TextureManager.cpp +│ │ │ ├── TextureManager.h +│ │ │ ├── Utilities +│ │ │ │ ├── Arrays.h +│ │ │ │ ├── DebugExecBindings.cpp +│ │ │ │ ├── DebugExecBindings.h +│ │ │ │ ├── Range.h +│ │ │ │ ├── RedirectingHandle.h +│ │ │ │ ├── WorldContext.cpp +│ │ │ │ ├── WorldContext.h +│ │ │ │ └── WorldContextIndex.h +│ │ │ ├── VersionCompatibility.h +│ │ │ └── Widgets +│ │ │ ├── SImGuiCanvasControl.cpp +│ │ │ ├── SImGuiCanvasControl.h +│ │ │ ├── SImGuiLayout.cpp +│ │ │ ├── SImGuiLayout.h +│ │ │ ├── SImGuiWidget.cpp +│ │ │ └── SImGuiWidget.h +│ │ └── Public +│ │ ├── ImGuiDelegates.h +│ │ ├── ImGuiInputHandler.h +│ │ ├── ImGuiModule.h +│ │ ├── ImGuiModuleProperties.h +│ │ └── ImGuiTextureHandle.h +│ └── ThirdParty +│ └── ImGuiLibrary +│ ├── Docs +│ │ ├── BACKENDS.md +│ │ ├── CHANGELOG.txt +│ │ ├── EXAMPLES.md +│ │ ├── FAQ.md +│ │ ├── FONTS.md +│ │ ├── README.md +│ │ └── TODO.txt +│ ├── ImGuiLibrary.Build.cs +│ ├── ImGuiLibrary.tps +│ ├── Include +│ │ ├── imconfig.h +│ │ ├── imgui.h +│ │ ├── imgui_internal.h +│ │ └── imstb_textedit.h +│ ├── LICENSE.txt +│ └── Private +│ ├── imgui.cpp +│ ├── imgui_demo.cpp +│ ├── imgui_draw.cpp +│ ├── imgui_tables.cpp +│ ├── imgui_widgets.cpp +│ ├── imstb_rectpack.h +│ └── imstb_truetype.h +├── README.md +└── Source + ├── GuiTest + │ ├── GuiTest.Build.cs + │ ├── GuiTest.cpp + │ ├── GuiTest.h + │ ├── GuiTestGameModeBase.cpp + │ ├── GuiTestGameModeBase.h + │ ├── Private + │ │ └── ImGuiActor.cpp + │ └── Public + │ └── ImGuiActor.h + ├── GuiTest.Target.cs + └── GuiTestEditor.Target.cs + +``` + +`Config/DefaultEngine.ini`: + +```ini + + +[/Script/EngineSettings.GameMapsSettings] +GameDefaultMap=/Game/DefaultMap.DefaultMap +EditorStartupMap=/Game/DefaultMap.DefaultMap + +[/Script/HardwareTargeting.HardwareTargetingSettings] +TargetedHardwareClass=Desktop +AppliedTargetedHardwareClass=Desktop +DefaultGraphicsPerformance=Maximum +AppliedDefaultGraphicsPerformance=Maximum + +[/Script/WindowsTargetPlatform.WindowsTargetSettings] +DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 + +[/Script/Engine.RendererSettings] +r.GenerateMeshDistanceFields=True +r.DynamicGlobalIlluminationMethod=1 +r.ReflectionMethod=1 +r.Shadow.Virtual.Enable=1 + +[/Script/WorldPartitionEditor.WorldPartitionEditorSettings] +CommandletClass=Class'/Script/UnrealEd.WorldPartitionConvertCommandlet' + +[/Script/Engine.Engine] ++ActiveGameNameRedirects=(OldGameName="TP_Blank",NewGameName="/Script/GuiTest") ++ActiveGameNameRedirects=(OldGameName="/Script/TP_Blank",NewGameName="/Script/GuiTest") ++ActiveClassRedirects=(OldClassName="TP_BlankGameModeBase",NewClassName="GuiTestGameModeBase") + +[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] +bEnablePlugin=True +bAllowNetworkConnection=True +SecurityToken=0BAB6FFA4B3F44EBB5BF078F3F2C5826 +bIncludeInShipping=False +bAllowExternalStartInShipping=False +bCompileAFSProject=False +bUseCompression=False +bLogFiles=False +bReportStats=False +ConnectionType=USBOnly +bUseManualIPAddress=False +ManualIPAddress= + + +``` + +`Config/DefaultGame.ini`: + +```ini + +[/Script/EngineSettings.GeneralProjectSettings] +ProjectID=F98884B14E28E1D3BD2850858CE90C8A + +``` + +`Config/DefaultImGui.ini`: + +```ini + + +[/Script/ImGui.ImGuiSettings] +ToggleInput=(Key=T,Shift=Checked,Ctrl=Undetermined,Alt=Checked,Cmd=Undetermined) + + +``` + +`Config/HoloLens/HoloLensEngine.ini`: + +```ini + + +[/Script/HoloLensPlatformEditor.HoloLensTargetSettings] +bBuildForEmulation=False +bBuildForDevice=True +bUseNameForLogo=True +bBuildForRetailWindowsStore=False +bAutoIncrementVersion=False +bShouldCreateAppInstaller=False +AppInstallerInstallationURL= +HoursBetweenUpdateChecks=0 +bEnablePIXProfiling=False +TileBackgroundColor=(B=64,G=0,R=0,A=255) +SplashScreenBackgroundColor=(B=64,G=0,R=0,A=255) ++PerCultureResources=(CultureId="",Strings=(PackageDisplayName="",PublisherDisplayName="",PackageDescription="",ApplicationDisplayName="",ApplicationDescription=""),Images=()) +TargetDeviceFamily=Windows.Holographic +MinimumPlatformVersion= +MaximumPlatformVersionTested=10.0.18362.0 +MaxTrianglesPerCubicMeter=500.000000 +SpatialMeshingVolumeSize=20.000000 +CompilerVersion=Default +Windows10SDKVersion=10.0.18362.0 ++CapabilityList=internetClientServer ++CapabilityList=privateNetworkClientServer ++Uap2CapabilityList=spatialPerception +bSetDefaultCapabilities=False +SpatializationPlugin= +ReverbPlugin= +OcclusionPlugin= +SoundCueCookQualityIndex=-1 + + +``` + +`GuiTest.uproject`: + +```uproject +{ + "FileVersion": 3, + "EngineAssociation": "5.0", + "Category": "", + "Description": "", + "Modules": [ + { + "Name": "GuiTest", + "Type": "Runtime", + "LoadingPhase": "Default", + "AdditionalDependencies": [ + "Engine" + ] + } + ], + "Plugins": [ + { + "Name": "ModelingToolsEditorMode", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + }, + { + "Name": "Bridge", + "Enabled": true, + "SupportedTargetPlatforms": [ + "Win64", + "Mac", + "Linux" + ] + } + ] +} +``` + +`Plugins/ImGui/CHANGES.md`: + +```md +This is only a summary provided to give a quick overview of changes. It does not list details which can be found in commit messages. If you think that more detailed changelog would be beneficiary, please raise it as an issue. + +Versions marked as 'unofficial' are labelled only for the needs of this changelog. Officially I maintain version numbers since 2019/04, starting from version 1.14. If you have any of the earlier commits then you will see plugin signed as a version 1.0. + +Change History +-------------- + +Version: 1.22 (2021/04) +- Fixed potential for initialization fiasco when using delegates container. +- Fixed bug in code protecting redirecting handles from self-referencing. +- Fixed cached resource handles getting invalid after reloading texture resources. + +Version: 1.21 (2020/07-09) +Improving stability +- Fixed a crash in the input handler caused by invalidated by hot-reload instance trying to unregister a delegate. +- Improved hot-reload stability and support for reloading after recompiling outside of the editor. Both methods should be equally supported and work together. +- Improved behaviour of delegates when hot-reloading. +- Changed context index mapping to fix issues with multi-PIE debugging in 4.25. +- Fixed Linux crash caused by wrong mapping of key codes. + +Version: 1.20 (2020/06) +Transition to IWYU and maintenance: +- Replaced includes of monolithic headers. +- Removed explicit PCH and switched to IWYU-style PCH model. +- Fixed includes to compile without explicit PCH in non-unity mode. +- Fixed a few issues causing compilation errors in older engine versions. +- Fixed debug code to compile on platforms using other than char or wchar_t characters. +- Fixed issues in recently added DPI scaling. +- Cleaned obsolete and unused code. +- Replaced custom scope guards with the template provided by the engine. + +Version: 1.19 (2020/03-06) +- Integrated fix for issue with ImGui popup/modal windows not being able to be closed in transparent mouse input mode. +- Integrated first version of Adaptive Canvas Size. +- Added different options to define canvas size, with Adaptive Canvas Size being one of the options (viewport). +- Added option for DPI scaling. + +Version: 1.18 (2020/01) +- Updated to engine version 4.24. +- Updated to ImGui version 1.74. + +Version: 1.17 (2019/04) +- Added experimental support for touch input. +- Integrated fixes allowing to build this as an engine plugin: +- Added support for sharing with game mouse input. +- Refactorization of input handling, with changes in SImGuiWidget and compatibility breaking changes in UImGuiInputHandler. + +Version: 1.16 (2019/05) +- Fixed issue with SImGuiLayout blocking mouse input for other Slate widgets, which was introduced by refactorization of widgets (version 1.14, commit c144658f). + +Version: 1.15 (2019/04) +- Added new FImGuiDelegates interface for ImGui debug delegates. +- Added code preserving delegates during hot-reloading and moving them to a new module. +- DEPRECIATED old FImGuiModule delegates interface and FImGuiDelegateHandle. +- Delegates registered with depreciated interface are redirected and get benefit of being preserved during hot-reloading. This can be controlled with IMGUI_REDIRECT_OBSOLETE_DELEGATES. +- Added IMGUI_WITH_OBSOLETE_DELEGATES allowing to strip depreciated interface from builds (that interface will be officially removed in one of later releases). +- Added new ImGui early debug delegates called during world tick start. +- Delegates are called in fixed order: multi-context early debug, world early debug (called during world tick start), world debug, multi-context debug (called during world post actor tick or if not available, during world tick start). +- Removed from build script configuration of debug delegates. + +Version: 1.14 (2019/03) +- Added SImGuiLayout to resets layout for SImGuiWidget. +- Refactored rendering in SImGuiWidget to take advantage of layout reset. +- Reworked ImGui canvas dragging and scaling and moved it to SImGuiCanvasControl. +- Removed dependency on ImGui internal cursor data. + +Version: 1.13 (unofficial) (2019/03) +- Fixed mapping from FKey to ImGui key index to gracefully handle unsupported keys and work on platforms that do not support all the keys defined in ImGui key map. +- Fixed non-unity compile warnings and errors. + +Version: 1.12 (unofficial) (2018/12) +- Added support for sharing with game keyboard and gamepad input. +- Added FImGuiModuleSettings to handle delayed loading of UImGuiSettings and serve as settings proxy for other classes. + +Version: 1.11 (unofficial) (2018/10-11) +- Moved ImGui Draw events to be called at the end of the world update during post actor tick event. Only available in UE 4.18 or later, with old behaviour available as an option. +- Replaced console variable based configuration of ImGui Draw events with macros. +- Replaced console variable based configuration of software cursor with a setting. +- Console variables and logging that are primarily focused on module development are hidden by default and can be enabled by setting IMGUI_MODULE_DEVELOPER to 1. +- Replaced console variables with module properties and settings. +- Added console commands to control module properties. +- Added support to preserve and move module properties to hot-reloaded module. +- Moved properties to public interface. +- Added FImGuiModule interface to access properties instance. +- DEPRECIATED FImGuiModule functions to modify single properties. + +Version: 1.10 (unofficial) (2018/10) +- Changed module type to 'Developer' to make it easier strip it from shipping or other builds. +- Added runtime loader to allow automatically load developer module in runtime builds. + +Version: 1.09 (unofficial) (2018/08-09) +- Added interface to register user textures for use in ImGui. +- Fixed bad deallocation in Texture Manager. +- Updated to ImGui 1.61. +- Updated to UE 4.20. + +Version: 1.08 (unofficial) (2018/07-08) +- Added ImGui Input Handler to allow to customization of input handling. +- Added ImGui settings with editor page in project properties. +- Added command to switch input mode with configurable key binding. +- Added backward compatibility macros. +- Fixed hot-reloading issues with using ImGui implementation. + +Version: 1.07 (unofficial) (2018/05) +- Improved initialization to allow loading module in any loading phase. + +Version: 1.06 (unofficial) (2018/05) +- Updated to ImGui 1.61 +- Added support for gamepad and keyboard navigation. + +Version: 1.05 (unofficial) (2018/04) +- Added mode to scale and drag ImGui canvas. +- Using ImGui internal cursor data to draw drag icon. + +Version: 1.04 (unofficial) (2018/03) +- Minimised lag between ending ImGui frame and rendering draw data in Slate. +- Moved ImGui Draw event to be called during world tick start with configuration to use old behaviour. + +Version: 1.03 (unofficial) (2018/01-03) +- Fixed warnings and errors found in non-unity, Linux, PS4 or XBox builds. +- Added configuration to choose between hardware and software cursor with hardware cursor used by default. + +Version: 1.02 (unofficial) (2018/01) +- Updated to ImGui 1.53. +- Fixed problems with ImGui Demo working in multi-context environment. +- Added FImGuiModule interface to change input mode and demo visibility. +- Fixed input state issues after application loses focus. +- Added input state debugging and `ImGui.Debug.Input` console variable to switch it. + +Version: 1.01 (unofficial) (2017/10) +- Added `ImGui.ShowDemo` console variable to show/hide ImGui demo. +- Added automatic switching to right ImGui context at the beginning of the world tick. +- Updated to UE 4.18 + +Version: <=1.00 (unofficial) (2017/04-10) +- Added ImGui source code as an external module to expose it in IDE. +- Integrated ImGui source code to build as part of the ImGui module. +- Added FImGuiModule to implement ImGui module interface. +- Added FImGuiModuleManager to control other module components. +- Added FTextureManager to manage texture resources and map them to ImTextureID. +- Added SImGuiWidget to handle Slate input and render in Slate ImGui draw data. +- Added FImGuiInputState to collect and store input before it can be copied to ImGui IO. +- Added FContextProxy to represent a single ImGui context. +- Added FImGuiContextManager to create and manage ImGui context proxies. +- Added Multi-PIE support with each PIE instance getting a dedicated ImGui context proxy, input state and widget. +- Added `ImGui.InputEnabled` console variable to control whether input mode is enabled. +- Added widget debugging and `ImGui.Debug.Widget` console variable to switch it. +- Added ImGui software cursor. +- Added support for session reloading with ini file names based on world type and PIE index. +- Added FImGuiModule interface to register ImGui delegates called to draw ImGui controls. + +``` + +`Plugins/ImGui/ImGui.uplugin`: + +```uplugin +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.22", + "FriendlyName": "ImGui", + "Description": "", + "Category": "Debug", + "CreatedBy": "", + "CreatedByURL": "", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": false, + "IsBetaVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "ImGui", + "Type": "DeveloperTool", + "LoadingPhase": "PreDefault" + } + ] +} +``` + +`Plugins/ImGui/LICENSE`: + +``` +The MIT License (MIT) + +Copyright (c) 2017-2021 Sebastian Gross + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +`Plugins/ImGui/README.md`: + +```md +Unreal ImGui +============ +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md) + +Unreal ImGui is an Unreal Engine 4 plug-in that integrates [Dear ImGui](https://github.com/ocornut/imgui) developed by Omar Cornut. + +Dear ImGui is an immediate-mode graphical user interface library that is very lightweight and easy to use. It can be very useful when creating debugging tools. + +:stop_button: Read Me First +--------------------------- +Please note that this is a forked project from [https://github.com/segross/UnrealImGui](https://github.com/segross/UnrealImGui). I do not take credit for the work he's put into making Dear ImGui work in Unreal Engine. The work I've done to this fork is listed below. + +I've removed large portions of this readme.md to keep redundant information between the base project and this fork to a minimum. If you wish to read the original readme.md, please see this link: [UnrealImGui ReadMe.md](https://github.com/segross/UnrealImGui/blob/master/README.md). + +Also note that the NetImGui branch is not up to date with any of this fork's changes. + +Fork Additions/Fixes +-------------------- + - Updated core source files for Unreal Engine 5. + - Updated Dear ImGui to 1.87. + - Added ImPlot v0.13 WIP. + - `ImGui::IsKey*` now functional with all known ImGui keys. + - Updated input handling flow to be [standard compliant](https://github.com/ocornut/imgui/issues/4921) with Dear ImGui 1.87 which makes ImGui react better at low FPS. Will add `IMGUI_DISABLE_OBSOLETE_KEYIO` preprocessor once I've ripped out old style input. + - Allowed `UTexture` for Texture Manager so render targets can also be rendered to quads rather than just being limited to using `UTexture2D` instances. + - Added the ability to instruct ImGui context to build custom fonts (like FontAwesome). + +Status +------ +UnrealImGui Version: 1.22 + +ImGui version: 1.87 + +ImPlot version: v0.13 WIP + +Supported Unreal Engine version: 5.0* + +\* *The original repository has support for later versions of UE4. I've not tested this fork on UE4 variants, I only know it works for UE5 currently.* + +How to Set up +------------- +On top of reading the base repository's [How to Set up](https://github.com/segross/UnrealImGui/blob/master/README.md#how-to-set-up) segment, you'll need to add the following line to your `[GameName].Build.cs` file otherwise you'll get linking errors: + +```cpp +// Tell the compiler we want to import the ImPlot symbols when linking against ImGui plugin +PrivateDefinitions.Add(string.Format("IMPLOT_API=DLLIMPORT")); +``` + +# Additional Knowledge + +## Using ImPlot + +It's pretty easy to use ImPlot, it's pretty much the same drill as using Dear ImGui with the UnrealImGui plugin. You can see documentation on how to use ImPlot here: [ImPlot](https://github.com/epezent/implot). + +The only thing you won't need to do is call the `ImPlot::CreateContext()` and `ImPlot::DestroyContext` routines as they're already called when ImGui's context is created within UnrealImGui's guts. + +## Drawing a UTextureRenderTarget2D + +One might want to render viewports into the world in an ImGui window. You can do this pretty simply by generating a `UTextureRenderTarget2D` then assigning that to a `ASceneCapture2D` actor in your world. Here's some sample code for generating an correctly managing the `UTextureRenderTarget2D`: +```cpp +void Init() +{ + TextureRenderTarget = NewObject(); + if(IsValid(TextureRenderTarget)) + { + TextureRenderTarget->InitAutoFormat(512, 512); + TextureRenderTarget->UpdateResourceImmediate(true); + } + + // ... Generate a unique TextureName here + // Register this render target as an ImGui interop handled texture + ImGuiTextureHandle = FImGuiModule::Get().FindTextureHandle(TextureName); + if(!ImGuiTextureHandle.IsValid()) + { + if(IsValid(TextureRenderTarget)) + { + ImGuiTextureHandle = FImGuiModule::Get().RegisterTexture(TextureName, TextureRenderTarget, true); + } + } +} + +~Class() +{ + // Requires releasing to avoid memory leak + FImGuiModule::Get().ReleaseTexture(ImGuiTextureHandle); +} + +void Render() +{ + // Actually submit the draw command to ImGui to render the quad with the texture + if(ImGuiTextureHandle.IsValid()) + { + ImGui::Image(ImGuiTextureHandle.GetTextureId(), {512.f, 512.f}); + } +} +``` + +Then generating the `ASceneCapture2D`: +```cpp +void Init() +{ + FActorSpawnParameters SpawnInfo; + SceneCapture2D = World->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator, SpawnInfo); + SceneCaptureComponent2D->TextureTarget = TextureRenderTarget; + SceneCaptureComponent2D->UpdateContent(); + + // Need to use this in order to force capture to use tone curve and also set alpha to scene alpha (1) + SceneCaptureComponent2D->CaptureSource = ESceneCaptureSource::SCS_FinalToneCurveHDR; +} +``` + +### Troubleshooting +If you're using a scene capture and your quad is not drawing at all, make sure your scene capture "Capture Source" is set to "Final Color (with tone curve) in Linear sRGB gamut" to avoid alpha being set to 0 (since there's no way to instruct ImGui to ignore alpha without modding the core UnrealImGui plugin). + +If you're getting crashes or seg faults during rendering, make sure you're using `UPROPERTY()` on your class variables! + +## Adding custom fonts +### FontAwesome +Adding custom fonts is fairly simple. As a more complex and more commonly done, we're going to embed and build FontAwesome 6 into the font atlas. First thing you'll need is a binary C version of the FontAwesome font along with the necessary [companion descriptors](https://github.com/juliettef/IconFontCppHeaders/blob/main/IconsFontAwesome6.h). The descriptors are pre-generated, however if you have a new version of FA you wish to use, then use the Python script in that repository. As for the binary C, you'll need to compile Dear ImGui's [binary_to_compressed_c.cpp](https://github.com/ocornut/imgui/blob/master/misc/fonts/binary_to_compressed_c.cpp). + +Once you have the necessary files, you'll need to encode your FontAwesome font using the command: +``` +binary_to_compressed_c.exe -nocompress fa-solid-900.ttf FontAwesomeFont > FontAwesomeFont.h +``` +The only mandatory field here is `-nocompress` as this instructs the encoder to create an uncompressed binary file (1:1) since currently there's no immediate support for compressed fonts. + +Move over your descriptors and your binary C font file to an appropriate location for inclusion in your Unreal Engine project. The following code is how to instruct ImGui to build the font atlas with your FontAwesome font: + +```cpp +#include "FontAwesomeFont.h" +#include "IconsFontAwesome6.h" + +// Add FontAwesome font glyphs from memory +if(TSharedPtr FAFontConfig = MakeShareable(new ImFontConfig())) +{ + static const ImWchar IconRange[] = { ICON_MIN_FA, ICON_MAX_FA, 0 }; + + FAFontConfig->FontDataOwnedByAtlas = false; // Global font data lifetime + FAFontConfig->FontData = (void*)FontAwesomeData; // Declared in binary C .h file + FAFontConfig->FontDataSize = FontAwesomeSize; // Declared in binary C .h file + FAFontConfig->SizePixels = 16; + FAFontConfig->MergeMode = true; // Forces ImGui to place this font into the same atlas as the previous font + FAFontConfig->GlyphRanges = IconRange; // Required; instructs ImGui to use these glyphs + FAFontConfig->GlyphMinAdvanceX = 16.f; // Use for monospaced icons + FAFontConfig->PixelSnapH = true; // Better rendering (align to pixel grid) + FAFontConfig->GlyphOffset = {0, 3}; // Moves icons around, for alignment with general typesets + + FImGuiModule::Get().GetProperties().AddCustomFont("FontAwesome", FAFontConfig); + FImGuiModule::Get().RebuildFontAtlas(); +} +``` + +That's it. Make sure you execute the above code once at the beginning of the first ImGui frame (or at any point of your framework where the ImGui context has been initialized correctly) and it should build the main atlas with FontAwesome inside it. ImFontConfig lifetime is currently managed via reference counting (`TSharedPtr`). + +### Using the icons +```cpp +#include "IconsFontAwesome6.h" + +void OnPaint() +{ + ImGui::Text(ICON_FA_AWARD); +} +``` + +Pretty simple. Building FStrings that incorporate FontAwesome icons however gets a little trickier: +```cpp +FString Str = "Hello " + FString(UTF8_TO_TCHAR(ICON_FA_WAVE_SQUARE)) " World"; +ImGui::TextUnformatted(TCHAR_TO_UTF8(*Str)); +``` + +### More info + - [Dear ImGui: Using Fonts](https://github.com/ocornut/imgui/blob/master/docs/FONTS.md) + - [IconFontCppHeaders](https://github.com/juliettef/IconFontCppHeaders) + - [FontAwesome with general Dear ImGui](https://pixtur.github.io/mkdocs-for-imgui/site/FONTS/) + +# Misc + +See also +-------- + - [Original Project by segross](https://github.com/segross/UnrealImGui) + - [Dear ImGui](https://github.com/ocornut/imgui) + - [ImPlot](https://github.com/epezent/implot) + + +License +------- +Unreal ImGui (and this fork) is licensed under the MIT License, see LICENSE for more information. + +``` + +`Plugins/ImGui/Source/ImGui/ImGui.Build.cs`: + +```cs +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +using System.Collections.Generic; +using System.IO; +using UnrealBuildTool; + +public class ImGui : ModuleRules +{ +#if WITH_FORWARDED_MODULE_RULES_CTOR + public ImGui(ReadOnlyTargetRules Target) : base(Target) +#else + public ImGui(TargetInfo Target) +#endif + { + +#if WITH_FORWARDED_MODULE_RULES_CTOR + bool bBuildEditor = Target.bBuildEditor; +#else + bool bBuildEditor = (Target.Type == TargetRules.TargetType.Editor); +#endif + + // Developer modules are automatically loaded only in editor builds but can be stripped out from other builds. + // Enable runtime loader, if you want this module to be automatically loaded in runtime builds (monolithic). + bool bEnableRuntimeLoader = true; + + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + +#if UE_4_24_OR_LATER + bLegacyPublicIncludePaths = false; + ShadowVariableWarningLevel = WarningLevel.Error; + bTreatAsEngineModule = true; +#endif + + PublicIncludePaths.AddRange( + new string[] { + Path.Combine(ModuleDirectory, "../ThirdParty/ImGuiLibrary/Include"), + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + "ImGui/Private", + "ThirdParty/ImGuiLibrary/Private", + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "Projects" + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "InputCore", + "Slate", + "SlateCore" + // ... add private dependencies that you statically link with here ... + } + ); + + + if (bBuildEditor) + { + PrivateDependencyModuleNames.AddRange( + new string[] + { + "EditorStyle", + "Settings", + "UnrealEd", + } + ); + } + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + + +#if !UE_4_19_OR_LATER + List PrivateDefinitions = Definitions; +#endif + + PrivateDefinitions.Add(string.Format("RUNTIME_LOADER_ENABLED={0}", bEnableRuntimeLoader ? 1 : 0)); + } +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/Editor/ImGuiCanvasSizeInfoCustomization.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiCanvasSizeInfoCustomization.h" + +#if WITH_EDITOR + +#include "ImGuiModuleSettings.h" + +#include + + +#define LOCTEXT_NAMESPACE "ImGuiEditor" + + +namespace +{ + EImGuiCanvasSizeType GetCanvasSizeTypeEnumValue(const TSharedPtr& TypeHandle) + { + uint8 ValueAsByte = 0; + if (TypeHandle.IsValid()) + { + TypeHandle->GetValue(ValueAsByte); + } + return static_cast(ValueAsByte); + } + + bool IsAny(const TSharedPtr& TypeHandle, EImGuiCanvasSizeType Value) + { + return GetCanvasSizeTypeEnumValue(TypeHandle) == Value; + } + + template + bool IsAny(const TSharedPtr& TypeHandle, EImGuiCanvasSizeType First, TRest... Rest) + { + return IsAny(TypeHandle, First) || IsAny(TypeHandle, Rest...); + } + + float ShowToHeight(bool bShow) + { + return bShow ? 0.f /* Infinity */ : SMALL_NUMBER; + } + + EVisibility ShowToVisibility(bool bShow) + { + return bShow ? EVisibility::SelfHitTestInvisible : EVisibility::Hidden; + } +} + +//---------------------------------------------------------------------------------------------------- +// FImGuiKeyInfoCustomization implementation +//---------------------------------------------------------------------------------------------------- + +TSharedRef FImGuiCanvasSizeInfoCustomization::MakeInstance() +{ + return MakeShareable(new FImGuiCanvasSizeInfoCustomization); +} + +void FImGuiCanvasSizeInfoCustomization::CustomizeHeader(TSharedRef InStructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + TSharedPtr TypeHandle = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FImGuiCanvasSizeInfo, SizeType)); + TSharedPtr WidthHandle = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FImGuiCanvasSizeInfo, Width)); + TSharedPtr HeightHandle = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FImGuiCanvasSizeInfo, Height)); + TSharedPtr ExtendToViewportHandle = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FImGuiCanvasSizeInfo, bExtendToViewport)); + +#define ADD_DIMENSION_SLOT(Handle, LPadding) \ + + SHorizontalBox::Slot()\ + .Padding(LPadding, 0.f, 0.f, 0.f)\ + .HAlign(HAlign_Fill)\ + .MaxWidth(80.f)\ + [\ + SNew(SVerticalBox)\ + + SVerticalBox::Slot()\ + .AutoHeight()\ + [\ + Handle->CreatePropertyNameWidget()\ + ]\ + + SVerticalBox::Slot()\ + .HAlign(HAlign_Fill)\ + .AutoHeight()\ + [\ + Handle->CreatePropertyValueWidget()\ + ]\ + ] + + auto SizeRowHeight = TAttribute::Create([TypeHandle]() + { + return ShowToHeight(IsAny(TypeHandle, EImGuiCanvasSizeType::Custom)); + }); + + auto SizeRowVisibility = TAttribute::Create([TypeHandle]() + { + return ShowToVisibility(IsAny(TypeHandle, EImGuiCanvasSizeType::Custom)); + }); + + auto ExtendRowHeight = TAttribute::Create([TypeHandle]() + { + return ShowToHeight(IsAny(TypeHandle, EImGuiCanvasSizeType::Custom, EImGuiCanvasSizeType::Desktop)); + }); + + auto ExtendRowVisibility = TAttribute::Create([TypeHandle]() + { + return ShowToVisibility(IsAny(TypeHandle, EImGuiCanvasSizeType::Custom, EImGuiCanvasSizeType::Desktop)); + }); + + HeaderRow + .NameContent() + [ + InStructPropertyHandle->CreatePropertyNameWidget() + ] + .ValueContent() + .MinDesiredWidth(168.f) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + TypeHandle->CreatePropertyValueWidget() + ] + + SVerticalBox::Slot() + .AutoHeight() + .MaxHeight(SizeRowHeight) + [ + SNew(SHorizontalBox) + .Visibility(SizeRowVisibility) + ADD_DIMENSION_SLOT(WidthHandle, 0.f) + ADD_DIMENSION_SLOT(HeightHandle, 6.f) + ] + + SVerticalBox::Slot() + .AutoHeight() + .MaxHeight(ExtendRowHeight) + [ + SNew(SHorizontalBox) + .Visibility(ExtendRowVisibility) + + SHorizontalBox::Slot() + .AutoWidth() + [ + ExtendToViewportHandle->CreatePropertyValueWidget() + ] + + SHorizontalBox::Slot() + .Padding(4.f, 0.f, 0.f, 0.f) + .AutoWidth() + [ + ExtendToViewportHandle->CreatePropertyNameWidget() + ] + ] + ]; + +#undef ADD_DIMENSION_SLOT +} + +void FImGuiCanvasSizeInfoCustomization::CustomizeChildren(TSharedRef InStructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ +} + + +#undef LOCTEXT_NAMESPACE + +#endif // WITH_EDITOR + +``` + +`Plugins/ImGui/Source/ImGui/Private/Editor/ImGuiCanvasSizeInfoCustomization.h`: + +```h +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#if WITH_EDITOR + +#include +#include + + +// Property type customization for FImGuiCanvasSizeInfo. +class FImGuiCanvasSizeInfoCustomization : public IPropertyTypeCustomization +{ +public: + static TSharedRef MakeInstance(); + + // IPropertyTypeCustomization interface + virtual void CustomizeHeader(TSharedRef InStructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + virtual void CustomizeChildren(TSharedRef InStructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; +}; + +#endif // WITH_EDITOR + +``` + +`Plugins/ImGui/Source/ImGui/Private/Editor/ImGuiEditor.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiEditor.h" + +#if WITH_EDITOR + +#include "ImGuiCanvasSizeInfoCustomization.h" +#include "ImGuiKeyInfoCustomization.h" +#include "ImGuiModuleSettings.h" + +#include +#include + + +#define LOCTEXT_NAMESPACE "ImGuiEditor" + +#define SETTINGS_CONTAINER TEXT("Project"), TEXT("Plugins"), TEXT("ImGui") + + +namespace +{ + ISettingsModule* GetSettingsModule() + { + return FModuleManager::GetModulePtr("Settings"); + } + + FPropertyEditorModule* GetPropertyEditorModule() + { + return FModuleManager::GetModulePtr("PropertyEditor"); + } +} + +FImGuiEditor::FImGuiEditor() +{ + Register(); + + // As a side effect of being part of the ImGui module, we need to support deferred registration (only executed if + // module is loaded manually at the very early stage). + if (!IsRegistrationCompleted()) + { + CreateRegistrator(); + } +} + +FImGuiEditor::~FImGuiEditor() +{ + Unregister(); +} + +void FImGuiEditor::Register() +{ + // Only register after UImGuiSettings class is initialized (necessary to check in early loading stages). + if (!bSettingsRegistered && UImGuiSettings::Get()) + { + if (ISettingsModule* SettingsModule = GetSettingsModule()) + { + bSettingsRegistered = true; + + SettingsModule->RegisterSettings(SETTINGS_CONTAINER, + LOCTEXT("ImGuiSettingsName", "ImGui"), + LOCTEXT("ImGuiSettingsDescription", "Configure the Unreal ImGui plugin."), + UImGuiSettings::Get()); + } + } + + if (!bCustomPropertyTypeLayoutsRegistered) + { + if (FPropertyEditorModule* PropertyModule = GetPropertyEditorModule()) + { + bCustomPropertyTypeLayoutsRegistered = true; + + PropertyModule->RegisterCustomPropertyTypeLayout("ImGuiCanvasSizeInfo", + FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FImGuiCanvasSizeInfoCustomization::MakeInstance)); + PropertyModule->RegisterCustomPropertyTypeLayout("ImGuiKeyInfo", + FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FImGuiKeyInfoCustomization::MakeInstance)); + } + } +} + +void FImGuiEditor::Unregister() +{ + if (bSettingsRegistered) + { + bSettingsRegistered = false; + + if (ISettingsModule* SettingsModule = GetSettingsModule()) + { + SettingsModule->UnregisterSettings(SETTINGS_CONTAINER); + } + } + + if (bCustomPropertyTypeLayoutsRegistered) + { + bCustomPropertyTypeLayoutsRegistered = false; + + if (FPropertyEditorModule* PropertyModule = GetPropertyEditorModule()) + { + PropertyModule->UnregisterCustomPropertyTypeLayout("ImGuiCanvasSizeInfo"); + PropertyModule->UnregisterCustomPropertyTypeLayout("ImGuiKeyInfo"); + } + } +} + +void FImGuiEditor::CreateRegistrator() +{ + if (!RegistratorHandle.IsValid()) + { + RegistratorHandle = FModuleManager::Get().OnModulesChanged().AddLambda([this](FName Name, EModuleChangeReason Reason) + { + if (Reason == EModuleChangeReason::ModuleLoaded) + { + Register(); + } + + if (IsRegistrationCompleted()) + { + ReleaseRegistrator(); + } + }); + } +} + +void FImGuiEditor::ReleaseRegistrator() +{ + if (RegistratorHandle.IsValid()) + { + FModuleManager::Get().OnModulesChanged().Remove(RegistratorHandle); + RegistratorHandle.Reset(); + } +} + + +#undef SETTINGS_CONTAINER +#undef LOCTEXT_NAMESPACE + +#endif // WITH_EDITOR + +``` + +`Plugins/ImGui/Source/ImGui/Private/Editor/ImGuiEditor.h`: + +```h +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#if WITH_EDITOR + +#include + + +// Registers module's settings in editor (due to a small size of this code we don't use a separate editor module). +class FImGuiEditor +{ +public: + + FImGuiEditor(); + ~FImGuiEditor(); + +private: + + bool IsRegistrationCompleted() const { return bSettingsRegistered && bCustomPropertyTypeLayoutsRegistered; } + + void Register(); + void Unregister(); + + void CreateRegistrator(); + void ReleaseRegistrator(); + + FDelegateHandle RegistratorHandle; + + bool bSettingsRegistered = false; + bool bCustomPropertyTypeLayoutsRegistered = false; +}; + +#endif // WITH_EDITOR + +``` + +`Plugins/ImGui/Source/ImGui/Private/Editor/ImGuiKeyInfoCustomization.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiKeyInfoCustomization.h" + +#if WITH_EDITOR + +#include "ImGuiModuleSettings.h" + +#include +#include +#include +#include + + +#define LOCTEXT_NAMESPACE "ImGuiEditor" + + +//---------------------------------------------------------------------------------------------------- +// Helper widgets +//---------------------------------------------------------------------------------------------------- + +namespace +{ + class SPropertyKeySelector : public SCompoundWidget + { + public: + SLATE_BEGIN_ARGS(SPropertyKeySelector) + {} + SLATE_ARGUMENT(TSharedPtr, KeyHandle) + SLATE_ATTRIBUTE(FSlateFontInfo, Font) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs) + { + KeyHandle = InArgs._KeyHandle; + + ChildSlot + [ + SNew(SKeySelector) + .CurrentKey(this, &SPropertyKeySelector::GetCurrentKey) + .OnKeyChanged(this, &SPropertyKeySelector::OnKeyChanged) + .Font(InArgs._Font) + ]; + } + + TOptional GetCurrentKey() const + { + TArray RawPtrs; + KeyHandle->AccessRawData(RawPtrs); + + if (RawPtrs.Num()) + { + FKey* KeyPtr = static_cast(RawPtrs[0]); + + if (KeyPtr) + { + for (int32 Index = 1; Index < RawPtrs.Num(); Index++) + { + if (*static_cast(RawPtrs[Index]) != *KeyPtr) + { + return TOptional(); + } + } + + return *KeyPtr; + } + } + + return FKey(); + } + + void OnKeyChanged(TSharedPtr SelectedKey) + { + KeyHandle->SetValueFromFormattedString(SelectedKey->ToString()); + } + + private: + + TSharedPtr KeyHandle; + }; + + class SPropertyTriStateCheckBox : public SCompoundWidget + { + public: + + using FCheckBoxStateRaw = std::underlying_type::type; + + SLATE_BEGIN_ARGS(SPropertyTriStateCheckBox) + {} + SLATE_ARGUMENT(TSharedPtr, PropertyHandle) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs) + { + PropertyHandle = InArgs._PropertyHandle; + + // We are abusing SCheckBox a bit but our GetPropertyValue with custom OnCheckBoxStateChanged implementation + // gives a checkbox that allows to cycle between all three states. + ChildSlot + [ + SNew(SCheckBox) + .IsChecked(this, &SPropertyTriStateCheckBox::GetPropertyValue) + .OnCheckStateChanged(this, &SPropertyTriStateCheckBox::OnCheckBoxStateChanged) + ]; + } + + FCheckBoxStateRaw GetPropertyRawValue() const + { + FCheckBoxStateRaw Value; + PropertyHandle.Get()->GetValue(Value); + return Value; + } + + ECheckBoxState GetPropertyValue() const + { + return static_cast(GetPropertyRawValue()); + } + + void OnCheckBoxStateChanged(ECheckBoxState State) + { + const FCheckBoxStateRaw PrevEnumValue = (GetPropertyRawValue() + 2) % 3; + PropertyHandle.Get()->SetValue(PrevEnumValue); + } + + private: + + TSharedPtr PropertyHandle; + }; +} + + +//---------------------------------------------------------------------------------------------------- +// FImGuiKeyInfoCustomization implementation +//---------------------------------------------------------------------------------------------------- + +namespace InputConstants +{ + static const FMargin PropertyPadding(0.0f, 0.0f, 4.0f, 0.0f); +} + +TSharedRef FImGuiKeyInfoCustomization::MakeInstance() +{ + return MakeShareable(new FImGuiKeyInfoCustomization); +} + +void FImGuiKeyInfoCustomization::CustomizeHeader(TSharedRef InStructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + TSharedPtr KeyHandle = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FImGuiKeyInfo, Key)); + TSharedPtr ShiftHandle = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FImGuiKeyInfo, Shift)); + TSharedPtr CtrlHandle = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FImGuiKeyInfo, Ctrl)); + TSharedPtr AltHandle = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FImGuiKeyInfo, Alt)); + TSharedPtr CmdHandle = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FImGuiKeyInfo, Cmd)); + +#define ADD_CHECK_BOX_SLOT(Handle) \ + + SHorizontalBox::Slot()\ + .Padding(InputConstants::PropertyPadding)\ + .HAlign(HAlign_Left)\ + .VAlign(VAlign_Center)\ + .AutoWidth()\ + [\ + Handle->CreatePropertyNameWidget()\ + ]\ + + SHorizontalBox::Slot()\ + .Padding(InputConstants::PropertyPadding)\ + .HAlign(HAlign_Left)\ + .VAlign(VAlign_Center)\ + .AutoWidth()\ + [\ + SNew(SPropertyTriStateCheckBox).PropertyHandle(Handle)\ + ] + + HeaderRow + .NameContent() + [ + InStructPropertyHandle->CreatePropertyNameWidget() + ] + .ValueContent() + .MaxDesiredWidth(400.f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(InputConstants::PropertyPadding) + .AutoWidth() + [ + SNew(SBox) + .WidthOverride(200.f) + [ + SNew(SPropertyKeySelector) + .KeyHandle(KeyHandle) + .Font(StructCustomizationUtils.GetRegularFont()) + ] + ] + ADD_CHECK_BOX_SLOT(ShiftHandle) + ADD_CHECK_BOX_SLOT(CtrlHandle) + ADD_CHECK_BOX_SLOT(AltHandle) + ADD_CHECK_BOX_SLOT(CmdHandle) + ]; + +#undef ADD_CHECK_BOX_SLOT +} + +void FImGuiKeyInfoCustomization::CustomizeChildren(TSharedRef InStructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ +} + + +#undef LOCTEXT_NAMESPACE + +#endif // WITH_EDITOR + +``` + +`Plugins/ImGui/Source/ImGui/Private/Editor/ImGuiKeyInfoCustomization.h`: + +```h +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#if WITH_EDITOR + +#include +#include + + +// Property type customization for FImGuiKeyInfo. +class FImGuiKeyInfoCustomization : public IPropertyTypeCustomization +{ +public: + static TSharedRef MakeInstance(); + + // IPropertyTypeCustomization interface + virtual void CustomizeHeader(TSharedRef InStructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + virtual void CustomizeChildren(TSharedRef InStructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; +}; + +#endif // WITH_EDITOR + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiContextManager.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiContextManager.h" + +#include "ImGuiDelegatesContainer.h" +#include "ImGuiImplementation.h" +#include "ImGuiModuleSettings.h" +#include "ImGuiModule.h" +#include "Utilities/WorldContext.h" +#include "Utilities/WorldContextIndex.h" + +#include + + +// TODO: Refactor ImGui Context Manager, to handle different types of worlds. + +namespace +{ +#if WITH_EDITOR + + // Name for editor ImGui context. + FORCEINLINE FString GetEditorContextName() + { + return TEXT("Editor"); + } + + // Name for world ImGui context. + FORCEINLINE FString GetWorldContextName(const UWorld& World) + { + using namespace Utilities; + + const FWorldContext* WorldContext = GetWorldContext(World); + switch (WorldContext->WorldType) + { + case EWorldType::PIE: + return FString::Printf(TEXT("PIEContext%d"), GetWorldContextIndex(*WorldContext)); + case EWorldType::Game: + return TEXT("Game"); + case EWorldType::Editor: + return TEXT("Editor"); + default: + return TEXT("Other"); + } + } + +#else + + FORCEINLINE FString GetWorldContextName() + { + return TEXT("Game"); + } + + FORCEINLINE FString GetWorldContextName(const UWorld&) + { + return TEXT("Game"); + } + +#endif // WITH_EDITOR +} + +FImGuiContextManager::FImGuiContextManager(FImGuiModuleSettings& InSettings) + : Settings(InSettings) +{ + Settings.OnDPIScaleChangedDelegate.AddRaw(this, &FImGuiContextManager::SetDPIScale); + + SetDPIScale(Settings.GetDPIScaleInfo()); + BuildFontAtlas(); + + FWorldDelegates::OnWorldTickStart.AddRaw(this, &FImGuiContextManager::OnWorldTickStart); +#if ENGINE_COMPATIBILITY_WITH_WORLD_POST_ACTOR_TICK + FWorldDelegates::OnWorldPostActorTick.AddRaw(this, &FImGuiContextManager::OnWorldPostActorTick); +#endif +} + +FImGuiContextManager::~FImGuiContextManager() +{ + Settings.OnDPIScaleChangedDelegate.RemoveAll(this); + + // Order matters because contexts can be created during World Tick Start events. + FWorldDelegates::OnWorldTickStart.RemoveAll(this); +#if ENGINE_COMPATIBILITY_WITH_WORLD_POST_ACTOR_TICK + FWorldDelegates::OnWorldPostActorTick.RemoveAll(this); +#endif +} + +void FImGuiContextManager::Tick(float DeltaSeconds) +{ + // In editor, worlds can get invalid. We could remove corresponding entries, but that would mean resetting ImGui + // context every time when PIE session is restarted. Instead we freeze contexts until their worlds are re-created. + + for (auto& Pair : Contexts) + { + auto& ContextData = Pair.Value; + if (ContextData.CanTick()) + { + ContextData.ContextProxy->Tick(DeltaSeconds); + } + else + { + // Clear to make sure that we don't store objects registered for world that is no longer valid. + FImGuiDelegatesContainer::Get().OnWorldDebug(Pair.Key).Clear(); + } + } + + // Once all context tick they should use new fonts and we can release the old resources. Extra countdown is added + // wait for contexts that ticked outside of this function, before rebuilding fonts. + if (FontResourcesReleaseCountdown > 0 && !--FontResourcesReleaseCountdown) + { + FontResourcesToRelease.Empty(); + } +} + +#if ENGINE_COMPATIBILITY_LEGACY_WORLD_ACTOR_TICK +void FImGuiContextManager::OnWorldTickStart(ELevelTick TickType, float DeltaSeconds) +{ + OnWorldTickStart(GWorld, TickType, DeltaSeconds); +} +#endif + +void FImGuiContextManager::OnWorldTickStart(UWorld* World, ELevelTick TickType, float DeltaSeconds) +{ + if (World && (World->WorldType == EWorldType::Game || World->WorldType == EWorldType::PIE + || World->WorldType == EWorldType::Editor)) + { + FImGuiContextProxy& ContextProxy = GetWorldContextProxy(*World); + + // Set as current, so we have right context ready when updating world objects. + ContextProxy.SetAsCurrent(); + + ContextProxy.DrawEarlyDebug(); +#if !ENGINE_COMPATIBILITY_WITH_WORLD_POST_ACTOR_TICK + ContextProxy.DrawDebug(); +#endif + } +} + +#if ENGINE_COMPATIBILITY_WITH_WORLD_POST_ACTOR_TICK +void FImGuiContextManager::OnWorldPostActorTick(UWorld* World, ELevelTick TickType, float DeltaSeconds) +{ + if (World && (World->WorldType == EWorldType::Game || World->WorldType == EWorldType::PIE + || World->WorldType == EWorldType::Editor)) + { + GetWorldContextProxy(*World).DrawDebug(); + } +} +#endif // ENGINE_COMPATIBILITY_WITH_WORLD_POST_ACTOR_TICK + +#if WITH_EDITOR +FImGuiContextManager::FContextData& FImGuiContextManager::GetEditorContextData() +{ + FContextData* Data = Contexts.Find(Utilities::EDITOR_CONTEXT_INDEX); + + if (UNLIKELY(!Data)) + { + Data = &Contexts.Emplace(Utilities::EDITOR_CONTEXT_INDEX, FContextData{ GetEditorContextName(), Utilities::EDITOR_CONTEXT_INDEX, FontAtlas, DPIScale, -1 }); + OnContextProxyCreated.Broadcast(Utilities::EDITOR_CONTEXT_INDEX, *Data->ContextProxy); + } + + return *Data; +} +#endif // WITH_EDITOR + +#if !WITH_EDITOR +FImGuiContextManager::FContextData& FImGuiContextManager::GetStandaloneWorldContextData() +{ + FContextData* Data = Contexts.Find(Utilities::STANDALONE_GAME_CONTEXT_INDEX); + + if (UNLIKELY(!Data)) + { + Data = &Contexts.Emplace(Utilities::STANDALONE_GAME_CONTEXT_INDEX, FContextData{ GetWorldContextName(), Utilities::STANDALONE_GAME_CONTEXT_INDEX, FontAtlas, DPIScale }); + OnContextProxyCreated.Broadcast(Utilities::STANDALONE_GAME_CONTEXT_INDEX, *Data->ContextProxy); + } + + return *Data; +} +#endif // !WITH_EDITOR + +FImGuiContextManager::FContextData& FImGuiContextManager::GetWorldContextData(const UWorld& World, int32* OutIndex) +{ + using namespace Utilities; + +#if WITH_EDITOR + // Default to editor context for anything other than a game world. + if (World.WorldType != EWorldType::Game && World.WorldType != EWorldType::PIE) + { + if (OutIndex) + { + *OutIndex = Utilities::EDITOR_CONTEXT_INDEX; + } + + return GetEditorContextData(); + } +#endif + + const FWorldContext* WorldContext = GetWorldContext(World); + const int32 Index = GetWorldContextIndex(*WorldContext); + + checkf(Index != Utilities::INVALID_CONTEXT_INDEX, TEXT("Couldn't find context index for world %s: WorldType = %d"), + *World.GetName(), static_cast(World.WorldType)); + +#if WITH_EDITOR + checkf(!GEngine->IsEditor() || Index != Utilities::EDITOR_CONTEXT_INDEX, + TEXT("Context index %d is reserved for editor and cannot be used for world %s: WorldType = %d, NetMode = %d"), + Index, *World.GetName(), static_cast(World.WorldType), static_cast(World.GetNetMode())); +#endif + + FContextData* Data = Contexts.Find(Index); + +#if WITH_EDITOR + if (UNLIKELY(!Data)) + { + Data = &Contexts.Emplace(Index, FContextData{ GetWorldContextName(World), Index, FontAtlas, DPIScale, WorldContext->PIEInstance }); + OnContextProxyCreated.Broadcast(Index, *Data->ContextProxy); + } + else + { + // Because we allow (for the sake of continuity) to map different PIE instances to the same index. + Data->PIEInstance = WorldContext->PIEInstance; + } +#else + if (UNLIKELY(!Data)) + { + Data = &Contexts.Emplace(Index, FContextData{ GetWorldContextName(World), Index, FontAtlas, DPIScale }); + OnContextProxyCreated.Broadcast(Index, *Data->ContextProxy); + } +#endif + + if (OutIndex) + { + *OutIndex = Index; + } + return *Data; +} + +void FImGuiContextManager::SetDPIScale(const FImGuiDPIScaleInfo& ScaleInfo) +{ + const float Scale = ScaleInfo.GetImGuiScale(); + if (DPIScale != Scale) + { + DPIScale = Scale; + + // Only rebuild font atlas if it is already built. Otherwise allow the other logic to pick a moment. + if (FontAtlas.IsBuilt()) + { + RebuildFontAtlas(); + } + + for (auto& Pair : Contexts) + { + if (Pair.Value.ContextProxy) + { + Pair.Value.ContextProxy->SetDPIScale(DPIScale); + } + } + } +} + +void FImGuiContextManager::BuildFontAtlas(const TMap>& CustomFontConfigs) +{ + if (!FontAtlas.IsBuilt()) + { + ImFontConfig FontConfig = {}; + FontConfig.SizePixels = FMath::RoundFromZero(13.f * DPIScale); + FontAtlas.AddFontDefault(&FontConfig); + + // Build custom fonts + for (const TPair>& CustomFontPair : CustomFontConfigs) + { + FName CustomFontName = CustomFontPair.Key; + TSharedPtr CustomFontConfig = CustomFontPair.Value; + + // Set font name for debugging + if (CustomFontConfig.IsValid()) + { + strcpy_s(CustomFontConfig->Name, 40, TCHAR_TO_ANSI(*CustomFontName.ToString())); + } + + FontAtlas.AddFont(CustomFontConfig.Get()); + } + + unsigned char* Pixels; + int Width, Height, Bpp; + FontAtlas.GetTexDataAsRGBA32(&Pixels, &Width, &Height, &Bpp); + + OnFontAtlasBuilt.Broadcast(); + } +} + +void FImGuiContextManager::RebuildFontAtlas() +{ + if (FontAtlas.IsBuilt()) + { + // Keep the old resources alive for a few frames to give all contexts a chance to bind to new ones. + FontResourcesToRelease.Add(TUniquePtr(new ImFontAtlas())); + Swap(*FontResourcesToRelease.Last(), FontAtlas); + + // Typically, one frame should be enough but since we allow for custom ticking, we need at least to frames to + // wait for contexts that already ticked and will not do that before the end of the next tick of this manager. + FontResourcesReleaseCountdown = 3; + } + + BuildFontAtlas(FImGuiModule::Get().GetProperties().GetCustomFonts()); +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiContextManager.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "ImGuiContextProxy.h" +#include "VersionCompatibility.h" + + +class FImGuiModuleSettings; +struct FImGuiDPIScaleInfo; + +// TODO: It might be useful to broadcast FContextProxyCreatedDelegate to users, to support similar cases to our ImGui +// demo, but we would need to remove from that interface internal classes. + +// Delegate called when new context proxy is created. +// @param ContextIndex - Index for that world +// @param ContextProxy - Created context proxy +DECLARE_MULTICAST_DELEGATE_TwoParams(FContextProxyCreatedDelegate, int32, FImGuiContextProxy&); + +// Manages ImGui context proxies. +class FImGuiContextManager +{ +public: + + FImGuiContextManager(FImGuiModuleSettings& InSettings); + + FImGuiContextManager(const FImGuiContextManager&) = delete; + FImGuiContextManager& operator=(const FImGuiContextManager&) = delete; + + FImGuiContextManager(FImGuiContextManager&&) = delete; + FImGuiContextManager& operator=(FImGuiContextManager&&) = delete; + + ~FImGuiContextManager(); + + ImFontAtlas& GetFontAtlas() { return FontAtlas; } + const ImFontAtlas& GetFontAtlas() const { return FontAtlas; } + +#if WITH_EDITOR + // Get or create editor ImGui context proxy. + FORCEINLINE FImGuiContextProxy& GetEditorContextProxy() { return *GetEditorContextData().ContextProxy; } +#endif + +#if !WITH_EDITOR + // Get or create standalone game ImGui context proxy. + FORCEINLINE FImGuiContextProxy& GetWorldContextProxy() { return *GetStandaloneWorldContextData().ContextProxy; } +#endif + + // Get or create ImGui context proxy for given world. + FORCEINLINE FImGuiContextProxy& GetWorldContextProxy(const UWorld& World) { return *GetWorldContextData(World).ContextProxy; } + + // Get or create ImGui context proxy for given world. Additionally get context index for that proxy. + FORCEINLINE FImGuiContextProxy& GetWorldContextProxy(const UWorld& World, int32& OutContextIndex) { return *GetWorldContextData(World, &OutContextIndex).ContextProxy; } + + // Get context proxy by index, or null if context with that index doesn't exist. + FORCEINLINE FImGuiContextProxy* GetContextProxy(int32 ContextIndex) + { + FContextData* Data = Contexts.Find(ContextIndex); + return Data ? Data->ContextProxy.Get() : nullptr; + } + + // Delegate called when a new context proxy is created. + FContextProxyCreatedDelegate OnContextProxyCreated; + + // Delegate called after font atlas is built. + FSimpleMulticastDelegate OnFontAtlasBuilt; + + void Tick(float DeltaSeconds); + + void RebuildFontAtlas(); + +private: + + struct FContextData + { + FContextData(const FString& ContextName, int32 ContextIndex, ImFontAtlas& FontAtlas, float DPIScale, int32 InPIEInstance = -1) + : PIEInstance(InPIEInstance) + , ContextProxy(new FImGuiContextProxy(ContextName, ContextIndex, &FontAtlas, DPIScale)) + { + } + + FORCEINLINE bool CanTick() const { return PIEInstance < 0 || GEngine->GetWorldContextFromPIEInstance(PIEInstance); } + + int32 PIEInstance = -1; + TUniquePtr ContextProxy; + }; + +#if ENGINE_COMPATIBILITY_LEGACY_WORLD_ACTOR_TICK + void OnWorldTickStart(ELevelTick TickType, float DeltaSeconds); +#endif + void OnWorldTickStart(UWorld* World, ELevelTick TickType, float DeltaSeconds); + +#if ENGINE_COMPATIBILITY_WITH_WORLD_POST_ACTOR_TICK + void OnWorldPostActorTick(UWorld* World, ELevelTick TickType, float DeltaSeconds); +#endif + +#if WITH_EDITOR + FContextData& GetEditorContextData(); +#endif + +#if !WITH_EDITOR + FContextData& GetStandaloneWorldContextData(); +#endif + + FContextData& GetWorldContextData(const UWorld& World, int32* OutContextIndex = nullptr); + + void SetDPIScale(const FImGuiDPIScaleInfo& ScaleInfo); + void BuildFontAtlas(const TMap>& CustomFontConfigs = {}); + + TMap Contexts; + + ImFontAtlas FontAtlas; + TArray> FontResourcesToRelease; + + FImGuiModuleSettings& Settings; + + float DPIScale = -1.f; + int32 FontResourcesReleaseCountdown = 0; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiContextProxy.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiContextProxy.h" + +#include "ImGuiDelegatesContainer.h" +#include "ImGuiImplementation.h" +#include "ImGuiInteroperability.h" +#include "Utilities/Arrays.h" +#include "VersionCompatibility.h" + +#include +#include + + +static constexpr float DEFAULT_CANVAS_WIDTH = 3840.f; +static constexpr float DEFAULT_CANVAS_HEIGHT = 2160.f; + + +namespace +{ + FString GetSaveDirectory() + { +#if ENGINE_COMPATIBILITY_LEGACY_SAVED_DIR + const FString SavedDir = FPaths::GameSavedDir(); +#else + const FString SavedDir = FPaths::ProjectSavedDir(); +#endif + + FString Directory = FPaths::Combine(*SavedDir, TEXT("ImGui")); + + // Make sure that directory is created. + IPlatformFile::GetPlatformPhysical().CreateDirectory(*Directory); + + return Directory; + } + + FString GetIniFile(const FString& Name) + { + static FString SaveDirectory = GetSaveDirectory(); + return FPaths::Combine(SaveDirectory, Name + TEXT(".ini")); + } + + struct FGuardCurrentContext + { + FGuardCurrentContext() + : OldContext(ImGui::GetCurrentContext()) + { + } + + ~FGuardCurrentContext() + { + if (bRestore) + { + ImGui::SetCurrentContext(OldContext); + } + } + + FGuardCurrentContext(FGuardCurrentContext&& Other) + : OldContext(MoveTemp(Other.OldContext)) + { + Other.bRestore = false; + } + + FGuardCurrentContext& operator=(FGuardCurrentContext&&) = delete; + + FGuardCurrentContext(const FGuardCurrentContext&) = delete; + FGuardCurrentContext& operator=(const FGuardCurrentContext&) = delete; + + private: + + ImGuiContext* OldContext = nullptr; + bool bRestore = true; + }; +} + +FImGuiContextProxy::FImGuiContextProxy(const FString& InName, int32 InContextIndex, ImFontAtlas* InFontAtlas, float InDPIScale) + : Name(InName) + , ContextIndex(InContextIndex) + , IniFilename(TCHAR_TO_ANSI(*GetIniFile(InName))) +{ + // Create context. + Context = ImGui::CreateContext(InFontAtlas); + + // Set this context in ImGui for initialization (any allocations will be tracked in this context). + SetAsCurrent(); + + // Start initialization. + ImGuiIO& IO = ImGui::GetIO(); + + // Set session data storage. + IO.IniFilename = IniFilename.c_str(); + + // Start with the default canvas size. + ResetDisplaySize(); + IO.DisplaySize = {(float)DisplaySize.X, (float)DisplaySize.Y}; + + // Set the initial DPI scale. + SetDPIScale(InDPIScale); + + // Initialize key mapping, so context can correctly interpret input state. + ImGuiInterops::SetUnrealKeyMap(IO); + + // Begin frame to complete context initialization (this is to avoid problems with other systems calling to ImGui + // during startup). + BeginFrame(); +} + +FImGuiContextProxy::~FImGuiContextProxy() +{ + if (Context) + { + // It seems that to properly shutdown context we need to set it as the current one (at least in this framework + // version), even though we can pass it to the destroy function. + SetAsCurrent(); + + // Save context data and destroy. + ImGui::DestroyContext(Context); + } +} + +void FImGuiContextProxy::ResetDisplaySize() +{ + DisplaySize = { DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT }; +} + +void FImGuiContextProxy::SetDPIScale(float Scale) +{ + if (DPIScale != Scale) + { + DPIScale = Scale; + + ImGuiStyle NewStyle = ImGuiStyle(); + NewStyle.ScaleAllSizes(Scale); + + FGuardCurrentContext GuardContext; + SetAsCurrent(); + ImGui::GetStyle() = MoveTemp(NewStyle); + } +} + +void FImGuiContextProxy::DrawEarlyDebug() +{ + if (bIsFrameStarted && !bIsDrawEarlyDebugCalled) + { + bIsDrawEarlyDebugCalled = true; + + SetAsCurrent(); + + // Delegates called in order specified in FImGuiDelegates. + BroadcastMultiContextEarlyDebug(); + BroadcastWorldEarlyDebug(); + } +} + +void FImGuiContextProxy::DrawDebug() +{ + if (bIsFrameStarted && !bIsDrawDebugCalled) + { + bIsDrawDebugCalled = true; + + // Make sure that early debug is always called first to guarantee order specified in FImGuiDelegates. + DrawEarlyDebug(); + + SetAsCurrent(); + + // Delegates called in order specified in FImGuiDelegates. + BroadcastWorldDebug(); + BroadcastMultiContextDebug(); + } +} + +void FImGuiContextProxy::Tick(float DeltaSeconds) +{ + // Making sure that we tick only once per frame. + if (LastFrameNumber < GFrameNumber) + { + LastFrameNumber = GFrameNumber; + + SetAsCurrent(); + + if (bIsFrameStarted) + { + // Make sure that draw events are called before the end of the frame. + DrawDebug(); + + // Ending frame will produce render output that we capture and store for later use. This also puts context to + // state in which it does not allow to draw controls, so we want to immediately start a new frame. + EndFrame(); + } + + // Update context information (some data need to be collected before starting a new frame while some other data + // may need to be collected after). + bHasActiveItem = ImGui::IsAnyItemActive(); + MouseCursor = ImGuiInterops::ToSlateMouseCursor(ImGui::GetMouseCursor()); + + // Begin a new frame and set the context back to a state in which it allows to draw controls. + BeginFrame(DeltaSeconds); + + // Update remaining context information. + bWantsMouseCapture = ImGui::GetIO().WantCaptureMouse; + } +} + +void FImGuiContextProxy::BeginFrame(float DeltaTime) +{ + if (!bIsFrameStarted) + { + ImGuiIO& IO = ImGui::GetIO(); + IO.DeltaTime = DeltaTime; + + ImGuiInterops::CopyInput(IO, InputState); + InputState.ClearUpdateState(); + + IO.DisplaySize = { (float)DisplaySize.X, (float)DisplaySize.Y }; + + ImGui::NewFrame(); + + bIsFrameStarted = true; + bIsDrawEarlyDebugCalled = false; + bIsDrawDebugCalled = false; + } +} + +void FImGuiContextProxy::EndFrame() +{ + if (bIsFrameStarted) + { + // Prepare draw data (after this call we cannot draw to this context until we start a new frame). + ImGui::Render(); + + // Update our draw data, so we can use them later during Slate rendering while ImGui is in the middle of the + // next frame. + UpdateDrawData(ImGui::GetDrawData()); + + bIsFrameStarted = false; + } +} + +void FImGuiContextProxy::UpdateDrawData(ImDrawData* DrawData) +{ + if (DrawData && DrawData->CmdListsCount > 0) + { + DrawLists.SetNum(DrawData->CmdListsCount, false); + + for (int Index = 0; Index < DrawData->CmdListsCount; Index++) + { + DrawLists[Index].TransferDrawData(*DrawData->CmdLists[Index]); + } + } + else + { + // If we are not rendering then this might be a good moment to empty the array. + DrawLists.Empty(); + } +} + +void FImGuiContextProxy::BroadcastWorldEarlyDebug() +{ + if (ContextIndex != Utilities::INVALID_CONTEXT_INDEX) + { + FSimpleMulticastDelegate& WorldEarlyDebugEvent = FImGuiDelegatesContainer::Get().OnWorldEarlyDebug(ContextIndex); + if (WorldEarlyDebugEvent.IsBound()) + { + WorldEarlyDebugEvent.Broadcast(); + } + } +} + +void FImGuiContextProxy::BroadcastMultiContextEarlyDebug() +{ + FSimpleMulticastDelegate& MultiContextEarlyDebugEvent = FImGuiDelegatesContainer::Get().OnMultiContextEarlyDebug(); + if (MultiContextEarlyDebugEvent.IsBound()) + { + MultiContextEarlyDebugEvent.Broadcast(); + } +} + +void FImGuiContextProxy::BroadcastWorldDebug() +{ + if (DrawEvent.IsBound()) + { + DrawEvent.Broadcast(); + } + + if (ContextIndex != Utilities::INVALID_CONTEXT_INDEX) + { + FSimpleMulticastDelegate& WorldDebugEvent = FImGuiDelegatesContainer::Get().OnWorldDebug(ContextIndex); + if (WorldDebugEvent.IsBound()) + { + WorldDebugEvent.Broadcast(); + } + } +} + +void FImGuiContextProxy::BroadcastMultiContextDebug() +{ + FSimpleMulticastDelegate& MultiContextDebugEvent = FImGuiDelegatesContainer::Get().OnMultiContextDebug(); + if (MultiContextDebugEvent.IsBound()) + { + MultiContextDebugEvent.Broadcast(); + } +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiContextProxy.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "ImGuiDrawData.h" +#include "ImGuiInputState.h" +#include "Utilities/WorldContextIndex.h" + +#include + +#include + +#include + + +// Represents a single ImGui context. All the context updates should be done through this proxy. During update it +// broadcasts draw events to allow listeners draw their controls. After update it stores draw data. +class FImGuiContextProxy +{ +public: + + FImGuiContextProxy(const FString& Name, int32 InContextIndex, ImFontAtlas* InFontAtlas, float InDPIScale); + ~FImGuiContextProxy(); + + FImGuiContextProxy(const FImGuiContextProxy&) = delete; + FImGuiContextProxy& operator=(const FImGuiContextProxy&) = delete; + + FImGuiContextProxy(FImGuiContextProxy&&) = delete; + FImGuiContextProxy& operator=(FImGuiContextProxy&&) = delete; + + // Get the name of this context. + const FString& GetName() const { return Name; } + + // Get draw data from the last frame. + const TArray& GetDrawData() const { return DrawLists; } + + // Get input state used by this context. + FImGuiInputState& GetInputState() { return InputState; } + const FImGuiInputState& GetInputState() const { return InputState; } + + // Is this context the current ImGui context. + bool IsCurrentContext() const { return ImGui::GetCurrentContext() == Context; } + + // Set this context as current ImGui context. + void SetAsCurrent() { ImGui::SetCurrentContext(Context); } + + // Get the desired context display size. + const FVector2D& GetDisplaySize() const { return DisplaySize; } + + // Set the desired context display size. + void SetDisplaySize(const FVector2D& Size) { DisplaySize = Size; } + + // Reset the desired context display size to default size. + void ResetDisplaySize(); + + // Get the DPI scale set for this context. + float GetDPIScale() const { return DPIScale; } + + // Set the DPI scale for this context. + void SetDPIScale(float Scale); + + // Whether this context has an active item (read once per frame during context update). + bool HasActiveItem() const { return bHasActiveItem; } + + // Whether ImGui will use the mouse inputs (read once per frame during context update). + bool WantsMouseCapture() const { return bWantsMouseCapture; } + + // Cursor type desired by this context (updated once per frame during context update). + EMouseCursor::Type GetMouseCursor() const { return MouseCursor; } + + // Internal draw event used to draw module's examples and debug widgets. Unlike the delegates container, it is not + // passed when the module is reloaded, so all objects that are unloaded with the module should register here. + FSimpleMulticastDelegate& OnDraw() { return DrawEvent; } + + // Call early debug events to allow listeners draw their debug widgets. + void DrawEarlyDebug(); + + // Call debug events to allow listeners draw their debug widgets. + void DrawDebug(); + + // Tick to advance context to the next frame. Only one call per frame will be processed. + void Tick(float DeltaSeconds); + +private: + + void BeginFrame(float DeltaTime = 1.f / 60.f); + void EndFrame(); + + void UpdateDrawData(ImDrawData* DrawData); + + void BroadcastWorldEarlyDebug(); + void BroadcastMultiContextEarlyDebug(); + + void BroadcastWorldDebug(); + void BroadcastMultiContextDebug(); + + ImGuiContext* Context; + + FVector2D DisplaySize = FVector2D::ZeroVector; + float DPIScale = 1.f; + + EMouseCursor::Type MouseCursor = EMouseCursor::None; + bool bHasActiveItem = false; + bool bWantsMouseCapture = false; + + bool bIsFrameStarted = false; + bool bIsDrawEarlyDebugCalled = false; + bool bIsDrawDebugCalled = false; + + FImGuiInputState InputState; + + TArray DrawLists; + + FString Name; + int32 ContextIndex = Utilities::INVALID_CONTEXT_INDEX; + + uint32 LastFrameNumber = 0; + + FSimpleMulticastDelegate DrawEvent; + + std::string IniFilename; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiDelegates.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiDelegates.h" +#include "ImGuiDelegatesContainer.h" + +#include + + +FSimpleMulticastDelegate& FImGuiDelegates::OnWorldEarlyDebug() +{ + return OnWorldEarlyDebug(GWorld); +} + +FSimpleMulticastDelegate& FImGuiDelegates::OnWorldEarlyDebug(UWorld* World) +{ + return FImGuiDelegatesContainer::Get().OnWorldEarlyDebug(World); +} + +FSimpleMulticastDelegate& FImGuiDelegates::OnMultiContextEarlyDebug() +{ + return FImGuiDelegatesContainer::Get().OnMultiContextEarlyDebug(); +} + +FSimpleMulticastDelegate& FImGuiDelegates::OnWorldDebug() +{ + return OnWorldDebug(GWorld); +} + +FSimpleMulticastDelegate& FImGuiDelegates::OnWorldDebug(UWorld* World) +{ + return FImGuiDelegatesContainer::Get().OnWorldDebug(World); +} + +FSimpleMulticastDelegate& FImGuiDelegates::OnMultiContextDebug() +{ + return FImGuiDelegatesContainer::Get().OnMultiContextDebug(); +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiDelegatesContainer.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiDelegatesContainer.h" + +#include "ImGuiModule.h" +#include "Utilities/WorldContextIndex.h" + + +#if !WITH_EDITOR +// +// Non-editor version without container redirection +// + +FImGuiDelegatesContainer& FImGuiDelegatesContainer::Get() +{ + static FImGuiDelegatesContainer* Container = new FImGuiDelegatesContainer(); + return *Container; +} + +#endif // !WITH_EDITOR + + +#if WITH_EDITOR +// +// Editor version supporting container redirection needed for hot-reloading +// + +#include "Utilities/RedirectingHandle.h" + +// Redirecting handle which will always bind to a container from the currently loaded module. +struct FImGuiDelegatesContainerHandle : Utilities::TRedirectingHandle +{ + FImGuiDelegatesContainerHandle(FImGuiDelegatesContainer& InDefaultContainer) + : Utilities::TRedirectingHandle(InDefaultContainer) + { + if (FImGuiModule* Module = FModuleManager::GetModulePtr("ImGui")) + { + SetParent(Module->DelegatesContainerHandle); + } + } +}; + +FImGuiDelegatesContainer& FImGuiDelegatesContainer::Get() +{ + return GetHandle().Get(); +} + +FImGuiDelegatesContainerHandle& FImGuiDelegatesContainer::GetHandle() +{ + struct FContainerInstance + { + FContainerInstance() : Container(), Handle(Container) {} + FImGuiDelegatesContainer Container; + FImGuiDelegatesContainerHandle Handle; + }; + static FContainerInstance* Instance = new FContainerInstance(); + return Instance->Handle; +} + +void FImGuiDelegatesContainer::MoveContainer(FImGuiDelegatesContainerHandle& OtherContainerHandle) +{ + // Only move data if pointer points to default instance, otherwise our data has already been moved and we only + // keep pointer to a more recent version. + if (GetHandle().IsDefault()) + { + OtherContainerHandle.Get() = MoveTemp(GetHandle().Get()); + GetHandle().Get().Clear(); + } + + // Update pointer to the most recent version. + GetHandle().SetParent(&OtherContainerHandle); +} + +#endif // WITH_EDITOR + + +int32 FImGuiDelegatesContainer::GetContextIndex(UWorld* World) +{ + return Utilities::GetWorldContextIndex(*World); +} + +void FImGuiDelegatesContainer::Clear() +{ + WorldEarlyDebugDelegates.Empty(); + WorldDebugDelegates.Empty(); + MultiContextEarlyDebugDelegate.Clear(); + MultiContextDebugDelegate.Clear(); +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiDelegatesContainer.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include +#include + + +#if WITH_EDITOR +struct FImGuiDelegatesContainerHandle; +#endif + +struct FImGuiDelegatesContainer +{ +public: + + // Get the current instance (can change during hot-reloading). + static FImGuiDelegatesContainer& Get(); + +#if WITH_EDITOR + // Get the handle to the container instance (can attach to other handles in hot-reloaded modules). + static FImGuiDelegatesContainerHandle& GetHandle(); + + // Redirect to the other container and if this one is still active move its data to the other one. + static void MoveContainer(FImGuiDelegatesContainerHandle& OtherContainerHandle); +#endif + + // Get delegate to ImGui world early debug event from known world instance. + FSimpleMulticastDelegate& OnWorldEarlyDebug(UWorld* World) { return OnWorldEarlyDebug(GetContextIndex(World)); } + + // Get delegate to ImGui world early debug event from known context index. + FSimpleMulticastDelegate& OnWorldEarlyDebug(int32 ContextIndex) { return WorldEarlyDebugDelegates.FindOrAdd(ContextIndex); } + + // Get delegate to ImGui multi-context early debug event. + FSimpleMulticastDelegate& OnMultiContextEarlyDebug() { return MultiContextEarlyDebugDelegate; } + + // Get delegate to ImGui world debug event from known world instance. + FSimpleMulticastDelegate& OnWorldDebug(UWorld* World) { return OnWorldDebug(GetContextIndex(World)); } + + // Get delegate to ImGui world debug event from known context index. + FSimpleMulticastDelegate& OnWorldDebug(int32 ContextIndex) { return WorldDebugDelegates.FindOrAdd(ContextIndex); } + + // Get delegate to ImGui multi-context debug event. + FSimpleMulticastDelegate& OnMultiContextDebug() { return MultiContextDebugDelegate; } + +private: + + int32 GetContextIndex(UWorld* World); + + void Clear(); + + TMap WorldEarlyDebugDelegates; + TMap WorldDebugDelegates; + FSimpleMulticastDelegate MultiContextEarlyDebugDelegate; + FSimpleMulticastDelegate MultiContextDebugDelegate; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiDemo.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiDemo.h" + +#include "ImGuiModuleProperties.h" + +#include + + +// Demo copied (with minor modifications) from ImGui examples. See https://github.com/ocornut/imgui. +void FImGuiDemo::DrawControls(int32 ContextIndex) +{ + if (Properties.ShowDemo()) + { + const int32 ContextBit = ContextIndex < 0 ? 0 : 1 << ContextIndex; + + // 1. Show a simple window + // Tip: if we don't call ImGui::Begin()/ImGui::End() the widgets appears in a window automatically called "Debug" + { + static float f = 0.0f; + ImGui::Text("Hello, world!"); + ImGui::SliderFloat("float", &f, 0.0f, 1.0f); + ImGui::ColorEdit3("clear color", (float*)&ClearColor); + + if (ContextBit) + { + if (ImGui::Button("Demo Window")) ShowDemoWindowMask ^= ContextBit; + if (ImGui::Button("Another Window")) ShowAnotherWindowMask ^= ContextBit; + } + ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); + } + + // 2. Show another simple window, this time using an explicit Begin/End pair + if (ShowAnotherWindowMask & ContextBit) + { + ImGui::SetNextWindowSize(ImVec2(200, 100), ImGuiCond_FirstUseEver); + ImGui::Begin("Another Window"); + ImGui::Text("Hello"); + ImGui::End(); + } + + // 3. Show the ImGui test window. Most of the sample code is in ImGui::ShowTestWindow() + if (ShowDemoWindowMask & ContextBit) + { + // If more than one demo window is opened display warning about running ImGui examples in multiple contexts. + + // For everything, but the first windows in this frame we assume warning. + bool bWarning = true; + if (GFrameNumber > LastDemoWindowFrameNumber) + { + // If this is the first window in this frame, then we need to look at the last frame to see whether + // there were more than one windows. Higher frame distance automatically means that there were not. + bWarning = ((GFrameNumber - LastDemoWindowFrameNumber) == 1) && (DemoWindowCounter > 1); + + LastDemoWindowFrameNumber = GFrameNumber; + DemoWindowCounter = 0; + } + + DemoWindowCounter++; + + if (bWarning) + { + ImGui::Spacing(); + + ImGui::PushStyleColor(ImGuiCol_Text, { 1.f, 1.f, 0.5f, 1.f }); + ImGui::TextWrapped("Demo Window is opened in more than one context, some of the ImGui examples may not work correctly."); + ImGui::PopStyleColor(); + + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip( + "Some of the ImGui examples that use static variables may not work correctly\n" + "when run concurrently in multiple contexts.\n" + "If you have a problem with an example try to run it in one context only."); + } + } + + // Draw demo window. + ImGui::SetNextWindowPos(ImVec2(650, 20), ImGuiCond_FirstUseEver); + ImGui::ShowDemoWindow(); + } + } +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiDemo.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include + +#include + +class FImGuiModuleProperties; + +// Widget drawing ImGui demo. +class FImGuiDemo +{ +public: + + FImGuiDemo(FImGuiModuleProperties& InProperties) + : Properties(InProperties) + { + } + + void DrawControls(int32 ContextIndex); + +private: + + FImGuiModuleProperties& Properties; + + ImVec4 ClearColor = ImColor{ 114, 144, 154 }; + + int32 ShowDemoWindowMask = 0; + int32 ShowAnotherWindowMask = 0; + + int32 DemoWindowCounter = 0; + uint32 LastDemoWindowFrameNumber = 0; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiDrawData.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiDrawData.h" + + +#if ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API +void FImGuiDrawList::CopyVertexData(TArray& OutVertexBuffer, const FTransform2D& Transform, const FSlateRotatedRect& VertexClippingRect) const +#else +void FImGuiDrawList::CopyVertexData(TArray& OutVertexBuffer, const FTransform2D& Transform) const +#endif // ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API +{ + // Reset and reserve space in destination buffer. + OutVertexBuffer.SetNumUninitialized(ImGuiVertexBuffer.Size, false); + + // Transform and copy vertex data. + for (int Idx = 0; Idx < ImGuiVertexBuffer.Size; Idx++) + { + const ImDrawVert& ImGuiVertex = ImGuiVertexBuffer[Idx]; + FSlateVertex& SlateVertex = OutVertexBuffer[Idx]; + + // Final UV is calculated in shader as XY * ZW, so we need set all components. + SlateVertex.TexCoords[0] = ImGuiVertex.uv.x; + SlateVertex.TexCoords[1] = ImGuiVertex.uv.y; + SlateVertex.TexCoords[2] = SlateVertex.TexCoords[3] = 1.f; + +#if ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + const FVector2D VertexPosition = Transform.TransformPoint(ImGuiInterops::ToVector2D(ImGuiVertex.pos)); + SlateVertex.Position[0] = VertexPosition.X; + SlateVertex.Position[1] = VertexPosition.Y; + SlateVertex.ClipRect = VertexClippingRect; +#else +#if ENGINE_COMPATIBILITY_LEGACY_VECTOR2F + SlateVertex.Position = Transform.TransformPoint(ImGuiInterops::ToVector2D(ImGuiVertex.pos)); +#else + SlateVertex.Position = (FVector2f)Transform.TransformPoint(ImGuiInterops::ToVector2D(ImGuiVertex.pos)); +#endif // ENGINE_COMPATIBILITY_LEGACY_VECTOR2F +#endif // ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + + // Unpack ImU32 color. + SlateVertex.Color = ImGuiInterops::UnpackImU32Color(ImGuiVertex.col); + } +} + +void FImGuiDrawList::CopyIndexData(TArray& OutIndexBuffer, const int32 StartIndex, const int32 NumElements) const +{ + // Reset buffer. + OutIndexBuffer.SetNumUninitialized(NumElements, false); + + // Copy elements (slow copy because of different sizes of ImDrawIdx and SlateIndex and because SlateIndex can + // have different size on different platforms). + for (int i = 0; i < NumElements; i++) + { + OutIndexBuffer[i] = ImGuiIndexBuffer[StartIndex + i]; + } +} + +void FImGuiDrawList::TransferDrawData(ImDrawList& Src) +{ + // Move data from source to this list. + Src.CmdBuffer.swap(ImGuiCommandBuffer); + Src.IdxBuffer.swap(ImGuiIndexBuffer); + Src.VtxBuffer.swap(ImGuiVertexBuffer); +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiDrawData.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "ImGuiInteroperability.h" +#include "VersionCompatibility.h" + +#include + +#include + + +// ImGui draw command data transformed for Slate. +struct FImGuiDrawCommand +{ + uint32 NumElements; + FSlateRect ClippingRect; + TextureIndex TextureId; +}; + +// Wraps raw ImGui draw list data in utilities that transform them for Slate. +class FImGuiDrawList +{ +public: + + // Get the number of draw commands in this list. + FORCEINLINE int NumCommands() const { return ImGuiCommandBuffer.Size; } + + // Get the draw command by number. + // @param CommandNb - Number of draw command + // @param Transform - Transform to apply to clipping rectangle + // @returns Draw command data + FImGuiDrawCommand GetCommand(int CommandNb, const FTransform2D& Transform) const + { + const ImDrawCmd& ImGuiCommand = ImGuiCommandBuffer[CommandNb]; + return { ImGuiCommand.ElemCount, TransformRect(Transform, ImGuiInterops::ToSlateRect(ImGuiCommand.ClipRect)), + ImGuiInterops::ToTextureIndex(ImGuiCommand.TextureId) }; + } + +#if ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + // Transform and copy vertex data to target buffer (old data in the target buffer are replaced). + // @param OutVertexBuffer - Destination buffer + // @param Transform - Transform to apply to all vertices + // @param VertexClippingRect - Clipping rectangle for transformed Slate vertices + void CopyVertexData(TArray& OutVertexBuffer, const FTransform2D& Transform, const FSlateRotatedRect& VertexClippingRect) const; +#else + // Transform and copy vertex data to target buffer (old data in the target buffer are replaced). + // @param OutVertexBuffer - Destination buffer + // @param Transform - Transform to apply to all vertices + void CopyVertexData(TArray& OutVertexBuffer, const FTransform2D& Transform) const; +#endif // ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + + // Transform and copy index data to target buffer (old data in the target buffer are replaced). + // Internal index buffer contains enough data to match the sum of NumElements from all draw commands. + // @param OutIndexBuffer - Destination buffer + // @param StartIndex - Start copying source data starting from this index + // @param NumElements - How many elements we want to copy + void CopyIndexData(TArray& OutIndexBuffer, const int32 StartIndex, const int32 NumElements) const; + + // Transfers data from ImGui source list to this object. Leaves source cleared. + void TransferDrawData(ImDrawList& Src); + +private: + + ImVector ImGuiCommandBuffer; + ImVector ImGuiIndexBuffer; + ImVector ImGuiVertexBuffer; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiImplementation.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiImplementation.h" + +#include + +// For convenience and easy access to the ImGui source code, we build it as part of this module. +// We don't need to define IMGUI_API manually because it is already done for this module. + +#if PLATFORM_XBOXONE +// Disable Win32 functions used in ImGui and not supported on XBox. +#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS +#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS +#endif // PLATFORM_XBOXONE + +#if PLATFORM_WINDOWS +#include +#endif // PLATFORM_WINDOWS + +#if WITH_EDITOR + +#include "ImGuiModule.h" +#include "Utilities/RedirectingHandle.h" + +// Redirecting handle which will automatically bind to another one, if a different instance of the module is loaded. +struct FImGuiContextHandle : public Utilities::TRedirectingHandle +{ + FImGuiContextHandle(ImGuiContext*& InDefaultContext) + : Utilities::TRedirectingHandle(InDefaultContext) + { + if (FImGuiModule* Module = FModuleManager::GetModulePtr("ImGui")) + { + SetParent(Module->ImGuiContextHandle); + } + } +}; + +static ImGuiContext* ImGuiContextPtr = nullptr; +static FImGuiContextHandle ImGuiContextPtrHandle(ImGuiContextPtr); + +// Get the global ImGui context pointer (GImGui) indirectly to allow redirections in obsolete modules. +#define GImGui (ImGuiContextPtrHandle.Get()) +#endif // WITH_EDITOR + +#include "imgui.cpp" +#include "imgui_demo.cpp" +#include "imgui_draw.cpp" +#include "imgui_widgets.cpp" + +#include "imgui_tables.cpp" + +#if PLATFORM_WINDOWS +#include +#endif // PLATFORM_WINDOWS + +#include "ImGuiInteroperability.h" + + +namespace ImGuiImplementation +{ +#if WITH_EDITOR + FImGuiContextHandle& GetContextHandle() + { + return ImGuiContextPtrHandle; + } + + void SetParentContextHandle(FImGuiContextHandle& Parent) + { + ImGuiContextPtrHandle.SetParent(&Parent); + } +#endif // WITH_EDITOR +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiImplementation.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +struct FImGuiContextHandle; + +// Gives access to selected ImGui implementation features. +namespace ImGuiImplementation +{ +#if WITH_EDITOR + // Get the handle to the ImGui Context pointer. + FImGuiContextHandle& GetContextHandle(); + + // Set the ImGui Context pointer handle. + void SetParentContextHandle(FImGuiContextHandle& Parent); +#endif // WITH_EDITOR +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiInputHandler.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiInputHandler.h" + +#include "ImGuiContextProxy.h" +#include "ImGuiInputState.h" +#include "ImGuiModuleDebug.h" +#include "ImGuiModuleManager.h" +#include "ImGuiModuleSettings.h" +#include "VersionCompatibility.h" + +#include +#include +#include +#include +#include + +#if WITH_EDITOR +#include +#include +#include +#endif // WITH_EDITOR + + +DEFINE_LOG_CATEGORY(LogImGuiInputHandler); + +namespace +{ + FReply ToReply(bool bConsume) + { + return bConsume ? FReply::Handled() : FReply::Unhandled(); + } +} + +FReply UImGuiInputHandler::OnKeyChar(const struct FCharacterEvent& CharacterEvent) +{ + InputState->AddCharacter(CharacterEvent.GetCharacter()); + return ToReply(!ModuleManager->GetProperties().IsKeyboardInputShared()); +} + +FReply UImGuiInputHandler::OnKeyDown(const FKeyEvent& KeyEvent) +{ + if (KeyEvent.GetKey().IsGamepadKey()) + { + bool bConsume = false; + if (InputState->IsGamepadNavigationEnabled()) + { + InputState->SetGamepadNavigationKey(KeyEvent, true); + bConsume = !ModuleManager->GetProperties().IsGamepadInputShared(); + } + + return ToReply(bConsume); + } + else + { + // Ignore console events, so we don't block it from opening. + if (IsConsoleEvent(KeyEvent)) + { + return ToReply(false); + } + +#if WITH_EDITOR + // If there is no active ImGui control that would get precedence and this key event is bound to a stop play session + // command, then ignore that event and let the command execute. + if (!HasImGuiActiveItem() && IsStopPlaySessionEvent(KeyEvent)) + { + return ToReply(false); + } +#endif // WITH_EDITOR + + const bool bConsume = !ModuleManager->GetProperties().IsKeyboardInputShared(); + + // With shared input we can leave command bindings for DebugExec to handle, otherwise we need to do it here. + if (bConsume && IsToggleInputEvent(KeyEvent)) + { + ModuleManager->GetProperties().ToggleInput(); + } + + InputState->SetKeyDown(KeyEvent, true); + CopyModifierKeys(KeyEvent); + + InputState->KeyDownEvents.Add(KeyEvent.GetKeyCode(), KeyEvent); + + return ToReply(bConsume); + } +} + +FReply UImGuiInputHandler::OnKeyUp(const FKeyEvent& KeyEvent) +{ + InputState->KeyUpEvents.Add(KeyEvent.GetKeyCode(), KeyEvent); + + if (KeyEvent.GetKey().IsGamepadKey()) + { + bool bConsume = false; + if (InputState->IsGamepadNavigationEnabled()) + { + InputState->SetGamepadNavigationKey(KeyEvent, false); + bConsume = !ModuleManager->GetProperties().IsGamepadInputShared(); + } + + return ToReply(bConsume); + } + else + { + InputState->SetKeyDown(KeyEvent, false); + CopyModifierKeys(KeyEvent); + + return ToReply(!ModuleManager->GetProperties().IsKeyboardInputShared()); + } +} + +FReply UImGuiInputHandler::OnAnalogValueChanged(const FAnalogInputEvent& AnalogInputEvent) +{ + bool bConsume = false; + + if (AnalogInputEvent.GetKey().IsGamepadKey() && InputState->IsGamepadNavigationEnabled()) + { + InputState->SetGamepadNavigationAxis(AnalogInputEvent, AnalogInputEvent.GetAnalogValue()); + bConsume = !ModuleManager->GetProperties().IsGamepadInputShared(); + } + + return ToReply(bConsume); +} + +FReply UImGuiInputHandler::OnMouseButtonDown(const FPointerEvent& MouseEvent) +{ + if (MouseEvent.IsTouchEvent()) + { + return ToReply(false); + } + + InputState->SetMouseDown(MouseEvent, true); + if (ModuleManager) + { + FImGuiContextProxy* Proxy = ModuleManager->GetContextManager().GetContextProxy(0); + if (Proxy) + { + GEngine->AddOnScreenDebugMessage(15, 10, Proxy->WantsMouseCapture() ? FColor::Green : FColor::Red, TEXT("Handler Down")); + return ToReply(Proxy->WantsMouseCapture()); + } + } + return ToReply(true); +} + +FReply UImGuiInputHandler::OnMouseButtonDoubleClick(const FPointerEvent& MouseEvent) +{ + InputState->SetMouseDown(MouseEvent, true); + return ToReply(true); +} + +FReply UImGuiInputHandler::OnMouseButtonUp(const FPointerEvent& MouseEvent) +{ + if (MouseEvent.IsTouchEvent()) + { + return ToReply(false); + } + + InputState->SetMouseDown(MouseEvent, false); + return ToReply(true); +} + +FReply UImGuiInputHandler::OnMouseWheel(const FPointerEvent& MouseEvent) +{ + InputState->AddMouseWheelDelta(MouseEvent.GetWheelDelta()); + return ToReply(true); +} + +FReply UImGuiInputHandler::OnMouseMove(const FVector2D& MousePosition, const FPointerEvent& MouseEvent) +{ + if (MouseEvent.IsTouchEvent()) + { + return ToReply(false); + } + + return OnMouseMove(MousePosition); +} + +FReply UImGuiInputHandler::OnMouseMove(const FVector2D& MousePosition) +{ + InputState->SetMousePosition(MousePosition); + return ToReply(true); +} + +FReply UImGuiInputHandler::OnTouchStarted(const FVector2D& CursorPosition, const FPointerEvent& TouchEvent) +{ + InputState->SetTouchDown(true); + InputState->SetTouchPosition(CursorPosition); + return ToReply(true); +} + +FReply UImGuiInputHandler::OnTouchMoved(const FVector2D& CursorPosition, const FPointerEvent& TouchEvent) +{ + InputState->SetTouchPosition(CursorPosition); + return ToReply(true); +} + +FReply UImGuiInputHandler::OnTouchEnded(const FVector2D& CursorPosition, const FPointerEvent& TouchEvent) +{ + InputState->SetTouchDown(false); + return ToReply(true); +} + +void UImGuiInputHandler::OnKeyboardInputEnabled() +{ + bKeyboardInputEnabled = true; +} + +void UImGuiInputHandler::OnKeyboardInputDisabled() +{ + if (bKeyboardInputEnabled) + { + bKeyboardInputEnabled = false; + InputState->ResetKeyboard(); + } +} + +void UImGuiInputHandler::OnGamepadInputEnabled() +{ + bGamepadInputEnabled = true; +} + +void UImGuiInputHandler::OnGamepadInputDisabled() +{ + if (bGamepadInputEnabled) + { + bGamepadInputEnabled = false; + InputState->ResetGamepadNavigation(); + } +} + +void UImGuiInputHandler::OnMouseInputEnabled() +{ + if (!bMouseInputEnabled) + { + bMouseInputEnabled = true; + UpdateInputStatePointer(); + } +} + +void UImGuiInputHandler::OnMouseInputDisabled() +{ + if (bMouseInputEnabled) + { + bMouseInputEnabled = false; + InputState->ResetMouse(); + UpdateInputStatePointer(); + } +} + +void UImGuiInputHandler::CopyModifierKeys(const FInputEvent& InputEvent) +{ + InputState->SetControlDown(InputEvent.IsControlDown()); + InputState->SetShiftDown(InputEvent.IsShiftDown()); + InputState->SetAltDown(InputEvent.IsAltDown()); +} + +bool UImGuiInputHandler::IsConsoleEvent(const FKeyEvent& KeyEvent) const +{ + // Checking modifiers is based on console implementation. + const bool bModifierDown = KeyEvent.IsControlDown() || KeyEvent.IsShiftDown() || KeyEvent.IsAltDown() || KeyEvent.IsCommandDown(); + return !bModifierDown && GetDefault()->ConsoleKeys.Contains(KeyEvent.GetKey()); +} + +#if WITH_EDITOR +bool UImGuiInputHandler::IsStopPlaySessionEvent(const FKeyEvent& KeyEvent) const +{ + if (StopPlaySessionCommandInfo.IsValid()) + { + const FInputChord InputChord(KeyEvent.GetKey(), KeyEvent.IsShiftDown(), KeyEvent.IsControlDown(), KeyEvent.IsAltDown(), KeyEvent.IsCommandDown()); +#if ENGINE_COMPATIBILITY_SINGLE_KEY_BINDING + const bool bHasActiveChord = (InputChord == StopPlaySessionCommandInfo->GetActiveChord().Get()); +#else + const bool bHasActiveChord = StopPlaySessionCommandInfo->HasActiveChord(InputChord); +#endif + return bHasActiveChord && FPlayWorldCommands::GlobalPlayWorldActions->CanExecuteAction(StopPlaySessionCommandInfo.ToSharedRef()); + } + + return false; +} +#endif // WITH_EDITOR + +namespace +{ + bool IsMatching(ECheckBoxState CheckBoxState, bool bValue) + { + return (CheckBoxState == ECheckBoxState::Undetermined) || ((CheckBoxState == ECheckBoxState::Checked) == bValue); + } + + bool IsMatchingEvent(const FKeyEvent& KeyEvent, const FImGuiKeyInfo& KeyInfo) + { + return (KeyInfo.Key == KeyEvent.GetKey()) + && IsMatching(KeyInfo.Shift, KeyEvent.IsShiftDown()) + && IsMatching(KeyInfo.Ctrl, KeyEvent.IsControlDown()) + && IsMatching(KeyInfo.Alt, KeyEvent.IsAltDown()) + && IsMatching(KeyInfo.Cmd, KeyEvent.IsCommandDown()); + } +} + +bool UImGuiInputHandler::IsToggleInputEvent(const FKeyEvent& KeyEvent) const +{ + return IsMatchingEvent(KeyEvent, ModuleManager->GetSettings().GetToggleInputKey()); +} + +bool UImGuiInputHandler::HasImGuiActiveItem() const +{ + FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); + return ContextProxy && ContextProxy->HasActiveItem(); +} + +void UImGuiInputHandler::UpdateInputStatePointer() +{ + InputState->SetMousePointer(bMouseInputEnabled && ModuleManager->GetSettings().UseSoftwareCursor()); +} + +void UImGuiInputHandler::OnSoftwareCursorChanged(bool) +{ + UpdateInputStatePointer(); +} + +void UImGuiInputHandler::OnPostImGuiUpdate() +{ + InputState->ClearUpdateState(); + + // TODO Replace with delegates after adding property change events. + InputState->SetKeyboardNavigationEnabled(ModuleManager->GetProperties().IsKeyboardNavigationEnabled()); + InputState->SetGamepadNavigationEnabled(ModuleManager->GetProperties().IsGamepadNavigationEnabled()); + + const auto& PlatformApplication = FSlateApplication::Get().GetPlatformApplication(); + InputState->SetGamepad(PlatformApplication.IsValid() && PlatformApplication->IsGamepadAttached()); +} + +void UImGuiInputHandler::Initialize(FImGuiModuleManager* InModuleManager, UGameViewportClient* InGameViewport, int32 InContextIndex) +{ + ModuleManager = InModuleManager; + GameViewport = InGameViewport; + ContextIndex = InContextIndex; + + auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); + checkf(ContextProxy, TEXT("Missing context during initialization of input handler: ContextIndex = %d"), ContextIndex); + InputState = &ContextProxy->GetInputState(); + + // Register to get post-update notifications, so we can clean frame updates. + ModuleManager->OnPostImGuiUpdate().AddUObject(this, &UImGuiInputHandler::OnPostImGuiUpdate); + + auto& Settings = ModuleManager->GetSettings(); + if (!Settings.OnUseSoftwareCursorChanged.IsBoundToObject(this)) + { + Settings.OnUseSoftwareCursorChanged.AddUObject(this, &UImGuiInputHandler::OnSoftwareCursorChanged); + } + +#if WITH_EDITOR + StopPlaySessionCommandInfo = FInputBindingManager::Get().FindCommandInContext("PlayWorld", "StopPlaySession"); + if (!StopPlaySessionCommandInfo.IsValid()) + { + UE_LOG(LogImGuiInputHandler, Warning, TEXT("Couldn't find 'StopPlaySession' in context 'PlayWorld'. ") + TEXT("PIE feature allowing execution of stop command in ImGui input mode will be disabled.")); + } +#endif // WITH_EDITOR +} + +void UImGuiInputHandler::BeginDestroy() +{ + Super::BeginDestroy(); + + // To catch leftovers from modules shutdown during PIE session. + extern FImGuiModuleManager* ImGuiModuleManager; + if (ModuleManager && ModuleManager == ImGuiModuleManager) + { + ModuleManager->GetSettings().OnUseSoftwareCursorChanged.RemoveAll(this); + } +} + + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiInputHandlerFactory.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiInputHandlerFactory.h" +#include "ImGuiInputHandler.h" + +#include "ImGuiModuleDebug.h" + +#include +#include + + +UImGuiInputHandler* FImGuiInputHandlerFactory::NewHandler(const FSoftClassPath& HandlerClassReference, FImGuiModuleManager* ModuleManager, UGameViewportClient* GameViewport, int32 ContextIndex) +{ + UClass* HandlerClass = nullptr; + if (HandlerClassReference.IsValid()) + { + HandlerClass = HandlerClassReference.TryLoadClass(); + + if (!HandlerClass) + { + UE_LOG(LogImGuiInputHandler, Error, TEXT("Couldn't load ImGui Input Handler class '%s'."), *HandlerClassReference.ToString()); + } + } + + if (!HandlerClass) + { + HandlerClass = UImGuiInputHandler::StaticClass(); + } + + UImGuiInputHandler* Handler = NewObject(GameViewport, HandlerClass); + if (Handler) + { + Handler->Initialize(ModuleManager, GameViewport, ContextIndex); + Handler->AddToRoot(); + } + else + { + UE_LOG(LogImGuiInputHandler, Error, TEXT("Failed attempt to create Input Handler: HandlerClass = %s."), *GetNameSafe(HandlerClass)); + } + + return Handler; +} + +void FImGuiInputHandlerFactory::ReleaseHandler(UImGuiInputHandler* Handler) +{ + if (Handler) + { + Handler->RemoveFromRoot(); + } +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiInputHandlerFactory.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "ImGuiModuleSettings.h" + + +class FImGuiModuleManager; +class UGameViewportClient; +class UImGuiInputHandler; + +class FImGuiInputHandlerFactory +{ +public: + + static UImGuiInputHandler* NewHandler(const FSoftClassPath& HandlerClassReference, FImGuiModuleManager* ModuleManager, UGameViewportClient* GameViewport, int32 ContextIndex); + + static void ReleaseHandler(UImGuiInputHandler* Handler); +}; + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiInputState.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiInputState.h" + +#include +#include +#include + + +FImGuiInputState::FImGuiInputState() +{ + Reset(); +} + +void FImGuiInputState::AddCharacter(TCHAR Char) +{ + InputCharacters.Add(Char); +} + +void FImGuiInputState::SetKeyDown(uint32 KeyIndex, bool bIsDown) +{ + if (KeyIndex < Utilities::GetArraySize(KeysDown)) + { + if (KeysDown[KeyIndex] != bIsDown) + { + KeysDown[KeyIndex] = bIsDown; + KeysUpdateRange.AddPosition(KeyIndex); + } + } +} + +void FImGuiInputState::SetMouseDown(uint32 MouseIndex, bool bIsDown) +{ + if (MouseIndex < Utilities::GetArraySize(MouseButtonsDown)) + { + if (MouseButtonsDown[MouseIndex] != bIsDown) + { + MouseButtonsDown[MouseIndex] = bIsDown; + MouseButtonsUpdateRange.AddPosition(MouseIndex); + } + } +} + +void FImGuiInputState::ClearUpdateState() +{ + ClearCharacters(); + + KeyDownEvents.Reset(); + KeyUpEvents.Reset(); + + KeysUpdateRange.SetEmpty(); + MouseButtonsUpdateRange.SetEmpty(); + + MouseWheelDelta = 0.f; + + bTouchProcessed = bTouchDown; +} + +void FImGuiInputState::ClearCharacters() +{ + InputCharacters.Empty(); +} + +void FImGuiInputState::ClearKeys() +{ + using std::fill; + fill(KeysDown, &KeysDown[Utilities::GetArraySize(KeysDown)], false); + + // Mark the whole array as dirty because potentially each entry could be affected. + KeysUpdateRange.SetFull(); +} + +void FImGuiInputState::ClearMouseButtons() +{ + using std::fill; + fill(MouseButtonsDown, &MouseButtonsDown[Utilities::GetArraySize(MouseButtonsDown)], false); + + // Mark the whole array as dirty because potentially each entry could be affected. + MouseButtonsUpdateRange.SetFull(); +} + +void FImGuiInputState::ClearMouseAnalogue() +{ + MousePosition = FVector2D::ZeroVector; + MouseWheelDelta = 0.f; +} + +void FImGuiInputState::ClearModifierKeys() +{ + bIsControlDown = false; + bIsShiftDown = false; + bIsAltDown = false; +} + +void FImGuiInputState::ClearNavigationInputs() +{ + using std::fill; + fill(NavigationInputs, &NavigationInputs[Utilities::GetArraySize(NavigationInputs)], 0.f); +} + + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiInputState.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "ImGuiInteroperability.h" +#include "Utilities/Arrays.h" + +#include + + +// Collects and stores input state and updates for ImGui IO. +class FImGuiInputState +{ +public: + + // Characters buffer. + using FCharactersBuffer = TArray>; + + // Array for mouse button states. + using FMouseButtonsArray = ImGuiInterops::ImGuiTypes::FMouseButtonsArray; + + // Array for key states. + using FKeysArray = ImGuiInterops::ImGuiTypes::FKeysArray; + + // Array for navigation input states. + using FNavInputArray = ImGuiInterops::ImGuiTypes::FNavInputArray; + + // Pair of indices defining range in mouse buttons array. + using FMouseButtonsIndexRange = Utilities::TArrayIndexRange; + + // Pair of indices defining range in keys array. + using FKeysIndexRange = Utilities::TArrayIndexRange; + + // Create empty state with whole range instance with the whole update state marked as dirty. + FImGuiInputState(); + + // Get reference to input characters buffer. + const FCharactersBuffer& GetCharacters() const { return InputCharacters; } + + // Add a character to the characters buffer. We can store and send to ImGui up to 16 characters per frame. Any + // character beyond that limit will be discarded. + // @param Char - Character to add + void AddCharacter(TCHAR Char); + + // Get reference to the array with key down states. + const FKeysArray& GetKeys() const { return KeysDown; } + + // Get possibly empty range of indices bounding dirty part of the keys array. + const FKeysIndexRange& GetKeysUpdateRange() const { return KeysUpdateRange; } + + // Change state of the key in the keys array and expand range bounding dirty part of the array. + // @param KeyEvent - Key event representing the key + // @param bIsDown - True, if key is down + void SetKeyDown(const FKeyEvent& KeyEvent, bool bIsDown) { SetKeyDown(ImGuiInterops::GetKeyIndex(KeyEvent), bIsDown); } + + // Change state of the key in the keys array and expand range bounding dirty part of the array. + // @param Key - Keyboard key + // @param bIsDown - True, if key is down + void SetKeyDown(const FKey& Key, bool bIsDown) { SetKeyDown(ImGuiInterops::GetKeyIndex(Key), bIsDown); } + + // Get reference to the array with mouse button down states. + const FMouseButtonsArray& GetMouseButtons() const { return MouseButtonsDown; } + + // Get possibly empty range of indices bounding dirty part of the mouse buttons array. + const FMouseButtonsIndexRange& GetMouseButtonsUpdateRange() const { return MouseButtonsUpdateRange; } + + // Change state of the button in the mouse buttons array and expand range bounding dirty part of the array. + // @param MouseEvent - Mouse event representing mouse button + // @param bIsDown - True, if button is down + void SetMouseDown(const FPointerEvent& MouseEvent, bool bIsDown) { SetMouseDown(ImGuiInterops::GetMouseIndex(MouseEvent), bIsDown); } + + // Change state of the button in the mouse buttons array and expand range bounding dirty part of the array. + // @param MouseButton - Mouse button key + // @param bIsDown - True, if button is down + void SetMouseDown(const FKey& MouseButton, bool bIsDown) { SetMouseDown(ImGuiInterops::GetMouseIndex(MouseButton), bIsDown); } + + // Get mouse wheel delta accumulated during the last frame. + float GetMouseWheelDelta() const { return MouseWheelDelta; } + + // Add mouse wheel delta. + // @param DeltaValue - Mouse wheel delta to add + void AddMouseWheelDelta(float DeltaValue) { MouseWheelDelta += DeltaValue; } + + // Get the mouse position. + const FVector2D& GetMousePosition() const { return MousePosition; } + + // Set the mouse position. + // @param Position - Mouse position + void SetMousePosition(const FVector2D& Position) { MousePosition = Position; } + + // Check whether input has active mouse pointer. + bool HasMousePointer() const { return bHasMousePointer; } + + // Set whether input has active mouse pointer. + // @param bHasPointer - True, if input has active mouse pointer + void SetMousePointer(bool bInHasMousePointer) { bHasMousePointer = bInHasMousePointer; } + + // Check whether touch input is in progress. True, after touch is started until one frame after it has ended. + // One frame delay is used to process mouse release in ImGui since touch-down is simulated with mouse-down. + bool IsTouchActive() const { return bTouchDown || bTouchProcessed; } + + // Check whether touch input is down. + bool IsTouchDown() const { return bTouchDown; } + + // Set whether touch input is down. + // @param bIsDown - True, if touch is down (or started) and false, if touch is up (or ended) + void SetTouchDown(bool bIsDown) { bTouchDown = bIsDown; } + + // Get the touch position. + const FVector2D& GetTouchPosition() const { return TouchPosition; } + + // Set the touch position. + // @param Position - Touch position + void SetTouchPosition(const FVector2D& Position) { TouchPosition = Position; } + + // Get Control down state. + bool IsControlDown() const { return bIsControlDown; } + + // Set Control down state. + // @param bIsDown - True, if Control is down + void SetControlDown(bool bIsDown) { bIsControlDown = bIsDown; } + + // Get Shift down state. + bool IsShiftDown() const { return bIsShiftDown; } + + // Set Shift down state. + // @param bIsDown - True, if Shift is down + void SetShiftDown(bool bIsDown) { bIsShiftDown = bIsDown; } + + // Get Alt down state. + bool IsAltDown() const { return bIsAltDown; } + + // Set Alt down state. + // @param bIsDown - True, if Alt is down + void SetAltDown(bool bIsDown) { bIsAltDown = bIsDown; } + + // Get reference to the array with navigation input states. + const FNavInputArray& GetNavigationInputs() const { return NavigationInputs; } + + // Change state of the navigation input associated with this gamepad key. + // @param KeyEvent - Key event with gamepad key input + // @param bIsDown - True, if key is down + void SetGamepadNavigationKey(const FKeyEvent& KeyEvent, bool bIsDown) { ImGuiInterops::SetGamepadNavigationKey(NavigationInputs, KeyEvent.GetKey(), bIsDown); } + + // Change state of the navigation input associated with this gamepad axis. + // @param AnalogInputEvent - Analogue input event with gamepad axis input + // @param Value - Analogue value that should be set for this axis + void SetGamepadNavigationAxis(const FAnalogInputEvent& AnalogInputEvent, float Value) { ImGuiInterops::SetGamepadNavigationAxis(NavigationInputs, AnalogInputEvent.GetKey(), Value); } + + // Check whether keyboard navigation is enabled. + bool IsKeyboardNavigationEnabled() const { return bKeyboardNavigationEnabled; } + + // Set whether keyboard navigation is enabled. + // @param bEnabled - True, if navigation is enabled + void SetKeyboardNavigationEnabled(bool bEnabled) { bKeyboardNavigationEnabled = bEnabled; } + + // Check whether gamepad navigation is enabled. + bool IsGamepadNavigationEnabled() const { return bGamepadNavigationEnabled; } + + // Set whether gamepad navigation is enabled. + // @param bEnabled - True, if navigation is enabled + void SetGamepadNavigationEnabled(bool bEnabled) { bGamepadNavigationEnabled = bEnabled; } + + // Check whether gamepad is attached. + bool HasGamepad() const { return bHasGamepad; } + + // Set whether gamepad is attached. + // @param bInHasGamepad - True, if gamepad is attached + void SetGamepad(bool bInHasGamepad) { bHasGamepad = bInHasGamepad; } + + // Reset the whole input state and mark it as dirty. + void Reset() + { + ResetKeyboard(); + ResetMouse(); + ResetGamepadNavigation(); + } + + // Reset the keyboard input state and mark it as dirty. + void ResetKeyboard() + { + ClearCharacters(); + ClearKeys(); + ClearModifierKeys(); + } + + // Reset the mouse input state and mark it as dirty. + void ResetMouse() + { + ClearMouseButtons(); + ClearMouseAnalogue(); + } + + // Reset the gamepad navigation state. + void ResetGamepadNavigation() + { + ClearNavigationInputs(); + } + + // Clear part of the state that is meant to be updated in every frame like: accumulators, buffers, navigation data + // and information about dirty parts of keys or mouse buttons arrays. + void ClearUpdateState(); + + TMap KeyDownEvents; + TMap KeyUpEvents; + +private: + + void SetKeyDown(uint32 KeyIndex, bool bIsDown); + void SetMouseDown(uint32 MouseIndex, bool IsDown); + + void ClearCharacters(); + void ClearKeys(); + void ClearMouseButtons(); + void ClearMouseAnalogue(); + void ClearModifierKeys(); + void ClearNavigationInputs(); + + FVector2D MousePosition = FVector2D::ZeroVector; + FVector2D TouchPosition = FVector2D::ZeroVector; + float MouseWheelDelta = 0.f; + + FMouseButtonsArray MouseButtonsDown; + FMouseButtonsIndexRange MouseButtonsUpdateRange; + + FCharactersBuffer InputCharacters; + + FKeysArray KeysDown; + FKeysIndexRange KeysUpdateRange; + + FNavInputArray NavigationInputs; + + bool bHasMousePointer = false; + bool bTouchDown = false; + bool bTouchProcessed = false; + + bool bIsControlDown = false; + bool bIsShiftDown = false; + bool bIsAltDown = false; + + bool bKeyboardNavigationEnabled = false; + bool bGamepadNavigationEnabled = false; + bool bHasGamepad = false; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiInteroperability.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiInteroperability.h" + +#include "ImGuiInputState.h" +#include "Utilities/Arrays.h" + + +// If TCHAR is wider than ImWchar, enable or disable validation of input character before conversions. +#define VALIDATE_INPUT_CHARACTERS 1 + +#if VALIDATE_INPUT_CHARACTERS +DEFINE_LOG_CATEGORY_STATIC(LogImGuiInput, Warning, All); +#endif + +namespace +{ + //==================================================================================================== + // Character conversion + //==================================================================================================== + + template* = nullptr> + ImWchar CastInputChar(T Char) + { + return static_cast(Char); + } + + template* = nullptr> + ImWchar CastInputChar(T Char) + { +#if VALIDATE_INPUT_CHARACTERS + // We only need a runtime validation if TCHAR is wider than ImWchar. + // Signed and unsigned integral types with the same size as ImWchar should be safely converted. As long as the + // char value is in that range we can safely use it, otherwise we should log an error to notify about possible + // truncations. + static constexpr auto MinLimit = (std::numeric_limits>::min)(); + static constexpr auto MaxLimit = (std::numeric_limits>::max)(); + UE_CLOG(!(Char >= MinLimit && Char <= MaxLimit), LogImGuiInput, Error, + TEXT("TCHAR value '%c' (%#x) is out of range %d (%#x) to %u (%#x) that can be safely converted to ImWchar. ") + TEXT("If you wish to disable this validation, please set VALIDATE_INPUT_CHARACTERS in ImGuiInputState.cpp to 0."), + Char, Char, MinLimit, MinLimit, MaxLimit, MaxLimit); +#endif + + return static_cast(Char); + } + + //==================================================================================================== + // Copying Utilities + //==================================================================================================== + + // Copy all elements from source to destination array of the same size. + template + void Copy(const TArray& Src, TArray& Dst) + { + using std::copy; + using std::begin; + using std::end; + copy(begin(Src), end(Src), begin(Dst)); + } + + // Copy subrange of source array to destination array of the same size. + template + void Copy(const TArray& Src, TArray& Dst, const Utilities::TArrayIndexRange& Range) + { + using std::copy; + using std::begin; + copy(begin(Src) + Range.GetBegin(), begin(Src) + Range.GetEnd(), begin(Dst) + Range.GetBegin()); + } + + // Copy number of elements from the beginning of source array to the beginning of destination array of the same size. + template + void Copy(const TArray& Src, TArray& Dst, SizeType Count) + { + checkf(Count < Utilities::ArraySize::value, TEXT("Number of copied elements is larger than array size.")); + + using std::copy; + using std::begin; + copy(begin(Src), begin(Src) + Count, begin(Dst)); + } +} + +namespace ImGuiInterops +{ + //==================================================================================================== + // Input Mapping + //==================================================================================================== + + static TMap UnrealToImGuiKeyMap; + + void SetUnrealKeyMap(ImGuiIO& IO) + { + UnrealToImGuiKeyMap.Add(EKeys::Tab, ImGuiKey_Tab); + + UnrealToImGuiKeyMap.Add(EKeys::Left, ImGuiKey_LeftArrow); + UnrealToImGuiKeyMap.Add(EKeys::Right, ImGuiKey_RightArrow); + UnrealToImGuiKeyMap.Add(EKeys::Up, ImGuiKey_UpArrow); + UnrealToImGuiKeyMap.Add(EKeys::Down, ImGuiKey_DownArrow); + + UnrealToImGuiKeyMap.Add(EKeys::PageUp, ImGuiKey_PageUp); + UnrealToImGuiKeyMap.Add(EKeys::PageDown, ImGuiKey_PageDown); + UnrealToImGuiKeyMap.Add(EKeys::Home, ImGuiKey_Home); + UnrealToImGuiKeyMap.Add(EKeys::End, ImGuiKey_End); + UnrealToImGuiKeyMap.Add(EKeys::Insert, ImGuiKey_Insert); + UnrealToImGuiKeyMap.Add(EKeys::Delete, ImGuiKey_Delete); + + UnrealToImGuiKeyMap.Add(EKeys::NumLock, ImGuiKey_NumLock); + UnrealToImGuiKeyMap.Add(EKeys::ScrollLock, ImGuiKey_ScrollLock); + UnrealToImGuiKeyMap.Add(EKeys::Pause, ImGuiKey_Pause); + + UnrealToImGuiKeyMap.Add(EKeys::BackSpace, ImGuiKey_Backspace); + UnrealToImGuiKeyMap.Add(EKeys::SpaceBar, ImGuiKey_Space); + UnrealToImGuiKeyMap.Add(EKeys::Enter, ImGuiKey_Enter); + UnrealToImGuiKeyMap.Add(EKeys::Escape, ImGuiKey_Escape); + + UnrealToImGuiKeyMap.Add(EKeys::A, ImGuiKey_A); + UnrealToImGuiKeyMap.Add(EKeys::B, ImGuiKey_B); + UnrealToImGuiKeyMap.Add(EKeys::C, ImGuiKey_C); + UnrealToImGuiKeyMap.Add(EKeys::D, ImGuiKey_D); + UnrealToImGuiKeyMap.Add(EKeys::E, ImGuiKey_E); + UnrealToImGuiKeyMap.Add(EKeys::F, ImGuiKey_F); + UnrealToImGuiKeyMap.Add(EKeys::G, ImGuiKey_G); + UnrealToImGuiKeyMap.Add(EKeys::H, ImGuiKey_H); + UnrealToImGuiKeyMap.Add(EKeys::I, ImGuiKey_I); + UnrealToImGuiKeyMap.Add(EKeys::J, ImGuiKey_J); + UnrealToImGuiKeyMap.Add(EKeys::K, ImGuiKey_K); + UnrealToImGuiKeyMap.Add(EKeys::L, ImGuiKey_L); + UnrealToImGuiKeyMap.Add(EKeys::M, ImGuiKey_M); + UnrealToImGuiKeyMap.Add(EKeys::N, ImGuiKey_N); + UnrealToImGuiKeyMap.Add(EKeys::O, ImGuiKey_O); + UnrealToImGuiKeyMap.Add(EKeys::P, ImGuiKey_P); + UnrealToImGuiKeyMap.Add(EKeys::Q, ImGuiKey_Q); + UnrealToImGuiKeyMap.Add(EKeys::R, ImGuiKey_R); + UnrealToImGuiKeyMap.Add(EKeys::S, ImGuiKey_S); + UnrealToImGuiKeyMap.Add(EKeys::T, ImGuiKey_T); + UnrealToImGuiKeyMap.Add(EKeys::U, ImGuiKey_U); + UnrealToImGuiKeyMap.Add(EKeys::V, ImGuiKey_V); + UnrealToImGuiKeyMap.Add(EKeys::W, ImGuiKey_W); + UnrealToImGuiKeyMap.Add(EKeys::X, ImGuiKey_X); + UnrealToImGuiKeyMap.Add(EKeys::Y, ImGuiKey_Y); + UnrealToImGuiKeyMap.Add(EKeys::Z, ImGuiKey_Z); + + UnrealToImGuiKeyMap.Add(EKeys::F1, ImGuiKey_F1); + UnrealToImGuiKeyMap.Add(EKeys::F2, ImGuiKey_F2); + UnrealToImGuiKeyMap.Add(EKeys::F3, ImGuiKey_F3); + UnrealToImGuiKeyMap.Add(EKeys::F4, ImGuiKey_F4); + UnrealToImGuiKeyMap.Add(EKeys::F5, ImGuiKey_F5); + UnrealToImGuiKeyMap.Add(EKeys::F6, ImGuiKey_F6); + UnrealToImGuiKeyMap.Add(EKeys::F7, ImGuiKey_F7); + UnrealToImGuiKeyMap.Add(EKeys::F8, ImGuiKey_F8); + UnrealToImGuiKeyMap.Add(EKeys::F9, ImGuiKey_F9); + UnrealToImGuiKeyMap.Add(EKeys::F10, ImGuiKey_F10); + UnrealToImGuiKeyMap.Add(EKeys::F11, ImGuiKey_F11); + UnrealToImGuiKeyMap.Add(EKeys::F12, ImGuiKey_F12); + + UnrealToImGuiKeyMap.Add(EKeys::One, ImGuiKey_0); + UnrealToImGuiKeyMap.Add(EKeys::Two, ImGuiKey_1); + UnrealToImGuiKeyMap.Add(EKeys::Three, ImGuiKey_2); + UnrealToImGuiKeyMap.Add(EKeys::Four, ImGuiKey_3); + UnrealToImGuiKeyMap.Add(EKeys::Five, ImGuiKey_4); + UnrealToImGuiKeyMap.Add(EKeys::Six, ImGuiKey_5); + UnrealToImGuiKeyMap.Add(EKeys::Seven, ImGuiKey_6); + UnrealToImGuiKeyMap.Add(EKeys::Eight, ImGuiKey_7); + UnrealToImGuiKeyMap.Add(EKeys::Nine, ImGuiKey_8); + + UnrealToImGuiKeyMap.Add(EKeys::Equals, ImGuiKey_Equal); + UnrealToImGuiKeyMap.Add(EKeys::Comma, ImGuiKey_Comma); + UnrealToImGuiKeyMap.Add(EKeys::Period, ImGuiKey_Period); + UnrealToImGuiKeyMap.Add(EKeys::Slash, ImGuiKey_Slash); + UnrealToImGuiKeyMap.Add(EKeys::LeftBracket, ImGuiKey_LeftBracket); + UnrealToImGuiKeyMap.Add(EKeys::RightBracket, ImGuiKey_RightBracket); + UnrealToImGuiKeyMap.Add(EKeys::Apostrophe, ImGuiKey_Apostrophe); + UnrealToImGuiKeyMap.Add(EKeys::Semicolon, ImGuiKey_Semicolon); + + UnrealToImGuiKeyMap.Add(EKeys::NumPadZero, ImGuiKey_Keypad0); + UnrealToImGuiKeyMap.Add(EKeys::NumPadOne, ImGuiKey_Keypad1); + UnrealToImGuiKeyMap.Add(EKeys::NumPadTwo, ImGuiKey_Keypad2); + UnrealToImGuiKeyMap.Add(EKeys::NumPadThree, ImGuiKey_Keypad3); + UnrealToImGuiKeyMap.Add(EKeys::NumPadFour, ImGuiKey_Keypad4); + UnrealToImGuiKeyMap.Add(EKeys::NumPadFive, ImGuiKey_Keypad5); + UnrealToImGuiKeyMap.Add(EKeys::NumPadSix, ImGuiKey_Keypad6); + UnrealToImGuiKeyMap.Add(EKeys::NumPadSeven, ImGuiKey_Keypad7); + UnrealToImGuiKeyMap.Add(EKeys::NumPadEight, ImGuiKey_Keypad8); + UnrealToImGuiKeyMap.Add(EKeys::NumPadNine, ImGuiKey_Keypad9); + + UnrealToImGuiKeyMap.Add(EKeys::Multiply, ImGuiKey_KeypadMultiply); + UnrealToImGuiKeyMap.Add(EKeys::Add, ImGuiKey_KeypadAdd); + UnrealToImGuiKeyMap.Add(EKeys::Subtract, ImGuiKey_KeypadSubtract); + UnrealToImGuiKeyMap.Add(EKeys::Decimal, ImGuiKey_KeypadDecimal); + UnrealToImGuiKeyMap.Add(EKeys::Divide, ImGuiKey_KeypadDivide); + } + + // Simple transform mapping key codes to 0-511 range used in ImGui. + // From what I can tell, on most supported platforms key codes should comfortably fit in that range anyway + // but the SDL key-codes used on Linux can go way out of this range (because of the extra flag). However, + // after this transform they should fit in the range without conflicts. + // NOTE: Should any of the platforms have other conflicts or any trouble with inputs, this is the likely + // candidate for change. + static uint32 MapKeyCode(uint32 KeyCode) + { + return (KeyCode < 512) ? KeyCode : 256 + (KeyCode % 256); + } + + uint32 GetKeyIndex(const FKey& Key) + { + const uint32* pKeyCode = nullptr; + const uint32* pCharCode = nullptr; + + FInputKeyManager::Get().GetCodesFromKey(Key, pKeyCode, pCharCode); + + const uint32 KeyCode = + pKeyCode ? *pKeyCode + : pCharCode ? *pCharCode + : 0; + + return MapKeyCode(KeyCode); + } + + uint32 GetKeyIndex(const FKeyEvent& KeyEvent) + { + return MapKeyCode(KeyEvent.GetKeyCode()); + } + + uint32 GetMouseIndex(const FKey& MouseButton) + { + if (MouseButton == EKeys::LeftMouseButton) + { + return 0; + } + else if (MouseButton == EKeys::MiddleMouseButton) + { + return 2; + } + else if (MouseButton == EKeys::RightMouseButton) + { + return 1; + } + else if (MouseButton == EKeys::ThumbMouseButton) + { + return 3; + } + else if (MouseButton == EKeys::ThumbMouseButton2) + { + return 4; + } + + return -1; + } + + EMouseCursor::Type ToSlateMouseCursor(ImGuiMouseCursor MouseCursor) + { + switch (MouseCursor) + { + case ImGuiMouseCursor_Arrow: + return EMouseCursor::Default; + case ImGuiMouseCursor_TextInput: + return EMouseCursor::TextEditBeam; + case ImGuiMouseCursor_ResizeAll: + return EMouseCursor::CardinalCross; + case ImGuiMouseCursor_ResizeNS: + return EMouseCursor::ResizeUpDown; + case ImGuiMouseCursor_ResizeEW: + return EMouseCursor::ResizeLeftRight; + case ImGuiMouseCursor_ResizeNESW: + return EMouseCursor::ResizeSouthWest; + case ImGuiMouseCursor_ResizeNWSE: + return EMouseCursor::ResizeSouthEast; + case ImGuiMouseCursor_None: + default: + return EMouseCursor::None; + } + } + + namespace + { + inline void UpdateKey(const FKey& Key, const FKey& KeyCondition, float& Value, bool bIsDown) + { + if (Key == KeyCondition) + { + Value = (bIsDown) ? 1.f : 0.f; + } + } + + inline void UpdateAxisValues(float& Axis, float& Opposite, float Value) + { + constexpr float AxisInputThreshold = 0.166f; + + // Filter out small values to avoid false positives (helpful in case of worn controllers). + Axis = FMath::Max(0.f, Value - AxisInputThreshold); + Opposite = 0.f; + } + + inline void UpdateSymmetricAxis(const FKey& Key, const FKey& KeyCondition, float& Negative, float& Positive, float Value) + { + if (Key == KeyCondition) + { + if (Value < 0.f) + { + UpdateAxisValues(Negative, Positive, -Value); + } + else + { + UpdateAxisValues(Positive, Negative, Value); + } + } + } + } + + void SetGamepadNavigationKey(ImGuiTypes::FNavInputArray& NavInputs, const FKey& Key, bool bIsDown) + { +#define MAP_KEY(KeyCondition, NavIndex) UpdateKey(Key, KeyCondition, NavInputs[NavIndex], bIsDown) + + if (Key.IsGamepadKey()) + { + MAP_KEY(EKeys::Gamepad_FaceButton_Bottom, ImGuiNavInput_Activate); + MAP_KEY(EKeys::Gamepad_FaceButton_Right, ImGuiNavInput_Cancel); + MAP_KEY(EKeys::Gamepad_FaceButton_Top, ImGuiNavInput_Input); + MAP_KEY(EKeys::Gamepad_FaceButton_Left, ImGuiNavInput_Menu); + MAP_KEY(EKeys::Gamepad_DPad_Left, ImGuiNavInput_DpadLeft); + MAP_KEY(EKeys::Gamepad_DPad_Right, ImGuiNavInput_DpadRight); + MAP_KEY(EKeys::Gamepad_DPad_Up, ImGuiNavInput_DpadUp); + MAP_KEY(EKeys::Gamepad_DPad_Down, ImGuiNavInput_DpadDown); + MAP_KEY(EKeys::Gamepad_LeftShoulder, ImGuiNavInput_FocusPrev); + MAP_KEY(EKeys::Gamepad_RightShoulder, ImGuiNavInput_FocusNext); + MAP_KEY(EKeys::Gamepad_LeftShoulder, ImGuiNavInput_TweakSlow); + MAP_KEY(EKeys::Gamepad_RightShoulder, ImGuiNavInput_TweakFast); + } + +#undef MAP_KEY + } + + void SetGamepadNavigationAxis(ImGuiTypes::FNavInputArray& NavInputs, const FKey& Key, float Value) + { +#define MAP_SYMMETRIC_AXIS(KeyCondition, NegNavIndex, PosNavIndex) UpdateSymmetricAxis(Key, KeyCondition, NavInputs[NegNavIndex], NavInputs[PosNavIndex], Value) + + if (Key.IsGamepadKey()) + { + MAP_SYMMETRIC_AXIS(EKeys::Gamepad_LeftX, ImGuiNavInput_LStickLeft, ImGuiNavInput_LStickRight); + MAP_SYMMETRIC_AXIS(EKeys::Gamepad_LeftY, ImGuiNavInput_LStickDown, ImGuiNavInput_LStickUp); + } + +#undef MAP_SYMMETRIC_AXIS + } + + //==================================================================================================== + // Input State Copying + //==================================================================================================== + + template + static inline constexpr void SetFlag(TFlags& Flags, TFlag Flag, bool bSet) + { + Flags = bSet ? Flags | Flag : Flags & ~Flag; + } + + void CopyInput(ImGuiIO& IO, const FImGuiInputState& InputState) + { + static const uint32 LeftControl = GetKeyIndex(EKeys::LeftControl); + static const uint32 RightControl = GetKeyIndex(EKeys::RightControl); + static const uint32 LeftShift = GetKeyIndex(EKeys::LeftShift); + static const uint32 RightShift = GetKeyIndex(EKeys::RightShift); + static const uint32 LeftAlt = GetKeyIndex(EKeys::LeftAlt); + static const uint32 RightAlt = GetKeyIndex(EKeys::RightAlt); + + // Copy key modifiers. + IO.KeyCtrl = InputState.IsControlDown(); + IO.KeyShift = InputState.IsShiftDown(); + IO.KeyAlt = InputState.IsAltDown(); + IO.KeySuper = false; + + // Copy buffers. + if (!InputState.GetKeysUpdateRange().IsEmpty()) + { + // Key down events + for(const auto& Pair : InputState.KeyDownEvents) + { + if(UnrealToImGuiKeyMap.Contains(Pair.Value.GetKey())) + { + IO.AddKeyEvent(UnrealToImGuiKeyMap[Pair.Value.GetKey()], true); + } + } + + // Key up events + for(const auto& Pair : InputState.KeyUpEvents) + { + if(UnrealToImGuiKeyMap.Contains(Pair.Value.GetKey())) + { + IO.AddKeyEvent(UnrealToImGuiKeyMap[Pair.Value.GetKey()], false); + } + } + } + + if (!InputState.GetMouseButtonsUpdateRange().IsEmpty()) + { + Copy(InputState.GetMouseButtons(), IO.MouseDown, InputState.GetMouseButtonsUpdateRange()); + } + + for (const TCHAR Char : InputState.GetCharacters()) + { + IO.AddInputCharacter(CastInputChar(Char)); + } + + if (InputState.IsGamepadNavigationEnabled() && InputState.HasGamepad()) + { + Copy(InputState.GetNavigationInputs(), IO.NavInputs); + } + + SetFlag(IO.ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard, InputState.IsKeyboardNavigationEnabled()); + SetFlag(IO.ConfigFlags, ImGuiConfigFlags_NavEnableGamepad, InputState.IsGamepadNavigationEnabled()); + SetFlag(IO.BackendFlags, ImGuiBackendFlags_HasGamepad, InputState.HasGamepad()); + + // Check whether we need to draw cursor. + IO.MouseDrawCursor = InputState.HasMousePointer(); + + // If touch is enabled and active, give it a precedence. + if (InputState.IsTouchActive()) + { + // Copy the touch position to mouse position. + IO.MousePos.x = InputState.GetTouchPosition().X; + IO.MousePos.y = InputState.GetTouchPosition().Y; + + // With touch active one frame longer than it is down, we have one frame to processed touch up. + IO.MouseDown[0] = InputState.IsTouchDown(); + } + else + { + // Copy the mouse position. + IO.MousePos.x = InputState.GetMousePosition().X; + IO.MousePos.y = InputState.GetMousePosition().Y; + + // Copy mouse wheel delta. + IO.MouseWheel += InputState.GetMouseWheelDelta(); + } + } +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiInteroperability.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "TextureManager.h" + +#include +#include + +#include + + +class FImGuiInputState; + +// Utilities to help standardise operations between Unreal and ImGui. +namespace ImGuiInterops +{ + //==================================================================================================== + // ImGui Types + //==================================================================================================== + + namespace ImGuiTypes + { + using FMouseButtonsArray = decltype(ImGuiIO::MouseDown); + using FKeysArray = decltype(ImGuiIO::KeysDown); + using FNavInputArray = decltype(ImGuiIO::NavInputs); + + using FKeyMap = decltype(ImGuiIO::KeyMap); + } + + + //==================================================================================================== + // Input Mapping + //==================================================================================================== + + // Set in ImGui IO mapping to recognize indices generated from Unreal input events. + void SetUnrealKeyMap(ImGuiIO& IO); + + // Map FKey to index in keys buffer. + uint32 GetKeyIndex(const FKey& Key); + + // Map key event to index in keys buffer. + uint32 GetKeyIndex(const FKeyEvent& KeyEvent); + + // Map mouse FKey to index in mouse buttons buffer. + uint32 GetMouseIndex(const FKey& MouseButton); + + // Map pointer event to index in mouse buttons buffer. + FORCEINLINE uint32 GetMouseIndex(const FPointerEvent& MouseEvent) + { + return GetMouseIndex(MouseEvent.GetEffectingButton()); + } + + // Convert from ImGuiMouseCursor type to EMouseCursor. + EMouseCursor::Type ToSlateMouseCursor(ImGuiMouseCursor MouseCursor); + + // Set in the target array navigation input corresponding to gamepad key. + // @param NavInputs - Target array + // @param Key - Gamepad key mapped to navigation input (non-mapped keys will be ignored) + // @param bIsDown - True, if key is down + void SetGamepadNavigationKey(ImGuiTypes::FNavInputArray& NavInputs, const FKey& Key, bool bIsDown); + + // Set in the target array navigation input corresponding to gamepad axis. + // @param NavInputs - Target array + // @param Key - Gamepad axis key mapped to navigation input (non-axis or non-mapped inputs will be ignored) + // @param Value - Axis value (-1..1 values from Unreal are mapped to separate ImGui axes with values in range 0..1) + void SetGamepadNavigationAxis(ImGuiTypes::FNavInputArray& NavInputs, const FKey& Key, float Value); + + + //==================================================================================================== + // Input State Copying + //==================================================================================================== + + // Copy input to ImGui IO. + // @param IO - Target ImGui IO + // @param InputState - Input state to copy + void CopyInput(ImGuiIO& IO, const FImGuiInputState& InputState); + + + //==================================================================================================== + // Conversions + //==================================================================================================== + + // Convert from ImGui packed color to FColor. + FORCEINLINE FColor UnpackImU32Color(ImU32 Color) + { + // We use IM_COL32_R/G/B/A_SHIFT macros to support different ImGui configurations. + return FColor{ (uint8)((Color >> IM_COL32_R_SHIFT) & 0xFF), (uint8)((Color >> IM_COL32_G_SHIFT) & 0xFF), + (uint8)((Color >> IM_COL32_B_SHIFT) & 0xFF), (uint8)((Color >> IM_COL32_A_SHIFT) & 0xFF) }; + } + + // Convert from ImVec4 rectangle to FSlateRect. + FORCEINLINE FSlateRect ToSlateRect(const ImVec4& ImGuiRect) + { + return FSlateRect{ ImGuiRect.x, ImGuiRect.y, ImGuiRect.z, ImGuiRect.w }; + } + + // Convert from ImVec2 rectangle to FVector2D. + FORCEINLINE FVector2D ToVector2D(const ImVec2& ImGuiVector) + { + return FVector2D{ ImGuiVector.x, ImGuiVector.y }; + } + + // Convert from ImGui Texture Id to Texture Index that we use for texture resources. + FORCEINLINE TextureIndex ToTextureIndex(ImTextureID Index) + { + return static_cast(reinterpret_cast(Index)); + } + + // Convert from Texture Index to ImGui Texture Id that we pass to ImGui. + FORCEINLINE ImTextureID ToImTextureID(TextureIndex Index) + { + return reinterpret_cast(static_cast(Index)); + } +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiModule.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiModule.h" + +#include "ImGuiDelegatesContainer.h" +#include "ImGuiModuleManager.h" +#include "TextureManager.h" +#include "Utilities/WorldContext.h" +#include "Utilities/WorldContextIndex.h" + +#if WITH_EDITOR +#include "ImGuiImplementation.h" +#include "Editor/ImGuiEditor.h" +#endif + +#include + + +#define LOCTEXT_NAMESPACE "FImGuiModule" + + +struct EDelegateCategory +{ + enum + { + // Default per-context draw events. + Default, + + // Multi-context draw event defined in context manager. + MultiContext + }; +}; + +FImGuiModuleManager* ImGuiModuleManager = nullptr; + +#if WITH_EDITOR +static FImGuiEditor* ImGuiEditor = nullptr; +#endif + +#if IMGUI_WITH_OBSOLETE_DELEGATES + +#if WITH_EDITOR +FImGuiDelegateHandle FImGuiModule::AddEditorImGuiDelegate(const FImGuiDelegate& Delegate) +{ + return { FImGuiDelegatesContainer::Get().OnWorldDebug(Utilities::EDITOR_CONTEXT_INDEX).Add(Delegate), + EDelegateCategory::Default, Utilities::EDITOR_CONTEXT_INDEX }; +} +#endif + +FImGuiDelegateHandle FImGuiModule::AddWorldImGuiDelegate(const FImGuiDelegate& Delegate) +{ + const int32 ContextIndex = Utilities::GetWorldContextIndex((UWorld*)GWorld); + return { FImGuiDelegatesContainer::Get().OnWorldDebug(ContextIndex).Add(Delegate), EDelegateCategory::Default, ContextIndex }; +} + +FImGuiDelegateHandle FImGuiModule::AddWorldImGuiDelegate(const UWorld* World, const FImGuiDelegate& Delegate) +{ + const int32 ContextIndex = Utilities::GetWorldContextIndex(World); + return { FImGuiDelegatesContainer::Get().OnWorldDebug(ContextIndex).Add(Delegate), EDelegateCategory::Default, ContextIndex }; +} + +FImGuiDelegateHandle FImGuiModule::AddMultiContextImGuiDelegate(const FImGuiDelegate& Delegate) +{ + return { FImGuiDelegatesContainer::Get().OnMultiContextDebug().Add(Delegate), EDelegateCategory::MultiContext }; +} + +void FImGuiModule::RemoveImGuiDelegate(const FImGuiDelegateHandle& Handle) +{ + if (Handle.Category == EDelegateCategory::MultiContext) + { + FImGuiDelegatesContainer::Get().OnMultiContextDebug().Remove(Handle.Handle); + } + else + { + FImGuiDelegatesContainer::Get().OnWorldDebug(Handle.Index).Remove(Handle.Handle); + } +} + +#endif // IMGUI_WITH_OBSOLETE_DELEGATES + +FImGuiTextureHandle FImGuiModule::FindTextureHandle(const FName& Name) +{ + const TextureIndex Index = ImGuiModuleManager->GetTextureManager().FindTextureIndex(Name); + return (Index != INDEX_NONE) ? FImGuiTextureHandle{ Name, ImGuiInterops::ToImTextureID(Index) } : FImGuiTextureHandle{}; +} + +FImGuiTextureHandle FImGuiModule::RegisterTexture(const FName& Name, class UTexture* Texture, bool bMakeUnique) +{ + FTextureManager& TextureManager = ImGuiModuleManager->GetTextureManager(); + + checkf(!bMakeUnique || TextureManager.FindTextureIndex(Name) == INDEX_NONE, + TEXT("Trying to register a texture with a name '%s' that is already used. Chose a different name ") + TEXT("or use bMakeUnique false, to update existing texture resources."), *Name.ToString()); + + const TextureIndex Index = TextureManager.CreateTextureResources(Name, Texture); + return FImGuiTextureHandle{ Name, ImGuiInterops::ToImTextureID(Index) }; +} + +void FImGuiModule::ReleaseTexture(const FImGuiTextureHandle& Handle) +{ + if (Handle.IsValid()) + { + ImGuiModuleManager->GetTextureManager().ReleaseTextureResources(ImGuiInterops::ToTextureIndex(Handle.GetTextureId())); + } +} + +void FImGuiModule::RebuildFontAtlas() +{ + if (ImGuiModuleManager) + { + ImGuiModuleManager->RebuildFontAtlas(); + } +} + +void FImGuiModule::StartupModule() +{ + // Initialize handles to allow cross-module redirections. Other handles will always look for parents in the active + // module, which means that we can only redirect to started modules. We don't have to worry about self-referencing + // as local handles are guaranteed to be constructed before initializing pointers. + // This supports in-editor recompilation and hot-reloading after compiling from the command line. The latter method + // theoretically doesn't support plug-ins and will not load re-compiled module, but its handles will still redirect + // to the active one. + +#if WITH_EDITOR + ImGuiContextHandle = &ImGuiImplementation::GetContextHandle(); + DelegatesContainerHandle = &FImGuiDelegatesContainer::GetHandle(); +#endif + + // Create managers that implements module logic. + + checkf(!ImGuiModuleManager, TEXT("Instance of the ImGui Module Manager already exists. Instance should be created only during module startup.")); + ImGuiModuleManager = new FImGuiModuleManager(); + +#if WITH_EDITOR + checkf(!ImGuiEditor, TEXT("Instance of the ImGui Editor already exists. Instance should be created only during module startup.")); + ImGuiEditor = new FImGuiEditor(); +#endif +} + +void FImGuiModule::ShutdownModule() +{ + // In editor store data that we want to move to hot-reloaded module. + +#if WITH_EDITOR + static bool bMoveProperties = true; + static FImGuiModuleProperties PropertiesToMove = ImGuiModuleManager->GetProperties(); +#endif + + // Before we shutdown we need to delete managers that will do all the necessary cleanup. + +#if WITH_EDITOR + checkf(ImGuiEditor, TEXT("Null ImGui Editor. ImGui editor instance should be deleted during module shutdown.")); + delete ImGuiEditor; + ImGuiEditor = nullptr; +#endif + + checkf(ImGuiModuleManager, TEXT("Null ImGui Module Manager. Module manager instance should be deleted during module shutdown.")); + delete ImGuiModuleManager; + ImGuiModuleManager = nullptr; + +#if WITH_EDITOR + // When shutting down we leave the global ImGui context pointer and handle pointing to resources that are already + // deleted. This can cause troubles after hot-reload when code in other modules calls ImGui interface functions + // which are statically bound to the obsolete module. To keep ImGui code functional we can redirect context handle + // to point to the new module. + + // When shutting down during hot-reloading, we might want to rewire handles used in statically bound functions + // or move data to a new module. + + FModuleManager::Get().OnModulesChanged().AddLambda([this] (FName Name, EModuleChangeReason Reason) + { + if (Reason == EModuleChangeReason::ModuleLoaded && Name == "ImGui") + { + FImGuiModule& LoadedModule = FImGuiModule::Get(); + if (&LoadedModule != this) + { + // Statically bound functions can be bound to the obsolete module, so we need to manually redirect. + + if (LoadedModule.ImGuiContextHandle) + { + ImGuiImplementation::SetParentContextHandle(*LoadedModule.ImGuiContextHandle); + } + + if (LoadedModule.DelegatesContainerHandle) + { + FImGuiDelegatesContainer::MoveContainer(*LoadedModule.DelegatesContainerHandle); + } + + if (bMoveProperties) + { + bMoveProperties = false; + LoadedModule.SetProperties(PropertiesToMove); + } + } + } + }); +#endif // WITH_EDITOR +} + +#if WITH_EDITOR +void FImGuiModule::SetProperties(const FImGuiModuleProperties& Properties) +{ + ImGuiModuleManager->GetProperties() = Properties; +} +#endif + +FImGuiModuleProperties& FImGuiModule::GetProperties() +{ + return ImGuiModuleManager->GetProperties(); +} + +const FImGuiModuleProperties& FImGuiModule::GetProperties() const +{ + return ImGuiModuleManager->GetProperties(); +} + +bool FImGuiModule::IsInputMode() const +{ + return ImGuiModuleManager && ImGuiModuleManager->GetProperties().IsInputEnabled(); +} + +void FImGuiModule::SetInputMode(bool bEnabled) +{ + if (ImGuiModuleManager) + { + ImGuiModuleManager->GetProperties().SetInputEnabled(bEnabled); + } +} + +void FImGuiModule::ToggleInputMode() +{ + if (ImGuiModuleManager) + { + ImGuiModuleManager->GetProperties().ToggleInput(); + } +} + +bool FImGuiModule::IsShowingDemo() const +{ + return ImGuiModuleManager && ImGuiModuleManager->GetProperties().ShowDemo(); +} + +void FImGuiModule::SetShowDemo(bool bShow) +{ + if (ImGuiModuleManager) + { + ImGuiModuleManager->GetProperties().SetShowDemo(bShow); + } +} + +void FImGuiModule::ToggleShowDemo() +{ + if (ImGuiModuleManager) + { + ImGuiModuleManager->GetProperties().ToggleDemo(); + } +} + + +//---------------------------------------------------------------------------------------------------- +// Runtime loader +//---------------------------------------------------------------------------------------------------- + +#if !WITH_EDITOR && RUNTIME_LOADER_ENABLED + +class FImGuiModuleLoader +{ + FImGuiModuleLoader() + { + if (!Load()) + { + FModuleManager::Get().OnModulesChanged().AddRaw(this, &FImGuiModuleLoader::LoadAndRelease); + } + } + + // For different engine versions. + static FORCEINLINE bool IsValid(const TSharedPtr& Ptr) { return Ptr.IsValid(); } + static FORCEINLINE bool IsValid(const IModuleInterface* const Ptr) { return Ptr != nullptr; } + + bool Load() + { + return IsValid(FModuleManager::Get().LoadModule(ModuleName)); + } + + void LoadAndRelease(FName Name, EModuleChangeReason Reason) + { + // Avoid handling own load event. + if (Name != ModuleName) + { + // Try loading until success and then release. + if (Load()) + { + FModuleManager::Get().OnModulesChanged().RemoveAll(this); + } + } + } + + static FName ModuleName; + + static FImGuiModuleLoader Instance; +}; + +FName FImGuiModuleLoader::ModuleName = "ImGui"; + +// In monolithic builds this will start loading process. +FImGuiModuleLoader FImGuiModuleLoader::Instance; + +#endif // !WITH_EDITOR && RUNTIME_LOADER_ENABLED + + +//---------------------------------------------------------------------------------------------------- +// Partial implementations of other classes that needs access to ImGuiModuleManager +//---------------------------------------------------------------------------------------------------- + +bool FImGuiTextureHandle::HasValidEntry() const +{ + const TextureIndex Index = ImGuiInterops::ToTextureIndex(TextureId); + return Index != INDEX_NONE && ImGuiModuleManager && ImGuiModuleManager->GetTextureManager().GetTextureName(Index) == Name; +} + + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FImGuiModule, ImGui) + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiModuleCommands.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiModuleCommands.h" + +#include "ImGuiModuleProperties.h" +#include "Utilities/DebugExecBindings.h" + + +const TCHAR* const FImGuiModuleCommands::ToggleInput = TEXT("ImGui.ToggleInput"); +const TCHAR* const FImGuiModuleCommands::ToggleKeyboardNavigation = TEXT("ImGui.ToggleKeyboardNavigation"); +const TCHAR* const FImGuiModuleCommands::ToggleGamepadNavigation = TEXT("ImGui.ToggleGamepadNavigation"); +const TCHAR* const FImGuiModuleCommands::ToggleKeyboardInputSharing = TEXT("ImGui.ToggleKeyboardInputSharing"); +const TCHAR* const FImGuiModuleCommands::ToggleGamepadInputSharing = TEXT("ImGui.ToggleGamepadInputSharing"); +const TCHAR* const FImGuiModuleCommands::ToggleMouseInputSharing = TEXT("ImGui.ToggleMouseInputSharing"); +const TCHAR* const FImGuiModuleCommands::SetMouseInputSharing = TEXT("ImGui.SetMouseInputSharing"); +const TCHAR* const FImGuiModuleCommands::ToggleDemo = TEXT("ImGui.ToggleDemo"); + +FImGuiModuleCommands::FImGuiModuleCommands(FImGuiModuleProperties& InProperties) + : Properties(InProperties) + , ToggleInputCommand(ToggleInput, + TEXT("Toggle ImGui input mode."), + FConsoleCommandDelegate::CreateRaw(this, &FImGuiModuleCommands::ToggleInputImpl)) + , ToggleKeyboardNavigationCommand(ToggleKeyboardNavigation, + TEXT("Toggle ImGui keyboard navigation."), + FConsoleCommandDelegate::CreateRaw(this, &FImGuiModuleCommands::ToggleKeyboardNavigationImpl)) + , ToggleGamepadNavigationCommand(ToggleGamepadNavigation, + TEXT("Toggle ImGui gamepad navigation."), + FConsoleCommandDelegate::CreateRaw(this, &FImGuiModuleCommands::ToggleGamepadNavigationImpl)) + , ToggleKeyboardInputSharingCommand(ToggleKeyboardInputSharing, + TEXT("Toggle ImGui keyboard input sharing."), + FConsoleCommandDelegate::CreateRaw(this, &FImGuiModuleCommands::ToggleKeyboardInputSharingImpl)) + , ToggleGamepadInputSharingCommand(ToggleGamepadInputSharing, + TEXT("Toggle ImGui gamepad input sharing."), + FConsoleCommandDelegate::CreateRaw(this, &FImGuiModuleCommands::ToggleGamepadInputSharingImpl)) + , ToggleMouseInputSharingCommand(ToggleMouseInputSharing, + TEXT("Toggle ImGui mouse input sharing."), + FConsoleCommandDelegate::CreateRaw(this, &FImGuiModuleCommands::ToggleMouseInputSharingImpl)) + , SetMouseInputSharingCommand(SetMouseInputSharing, + TEXT("Toggle ImGui mouse input sharing."), + FConsoleCommandWithArgsDelegate::CreateRaw(this, &FImGuiModuleCommands::SetMouseInputSharingImpl)) + , ToggleDemoCommand(ToggleDemo, + TEXT("Toggle ImGui demo."), + FConsoleCommandDelegate::CreateRaw(this, &FImGuiModuleCommands::ToggleDemoImpl)) +{ +} + +void FImGuiModuleCommands::SetKeyBinding(const TCHAR* CommandName, const FImGuiKeyInfo& KeyInfo) +{ + DebugExecBindings::UpdatePlayerInputs(KeyInfo, CommandName); +} + +void FImGuiModuleCommands::ToggleInputImpl() +{ + Properties.ToggleInput(); +} + +void FImGuiModuleCommands::ToggleKeyboardNavigationImpl() +{ + Properties.ToggleKeyboardNavigation(); +} + +void FImGuiModuleCommands::ToggleGamepadNavigationImpl() +{ + Properties.ToggleGamepadNavigation(); +} + +void FImGuiModuleCommands::ToggleKeyboardInputSharingImpl() +{ + Properties.ToggleKeyboardInputSharing(); +} + +void FImGuiModuleCommands::ToggleGamepadInputSharingImpl() +{ + Properties.ToggleGamepadInputSharing(); +} + +void FImGuiModuleCommands::ToggleMouseInputSharingImpl() +{ + Properties.ToggleMouseInputSharing(); +} + +void FImGuiModuleCommands::SetMouseInputSharingImpl(const TArray& Args) +{ + bool bIsEnabled = false; + if (Args.Num() > 0) + { + LexFromString(bIsEnabled, *Args[0]); + } + Properties.SetMouseInputShared(bIsEnabled); +} + +void FImGuiModuleCommands::ToggleDemoImpl() +{ + Properties.ToggleDemo(); +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiModuleCommands.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include + + +struct FImGuiKeyInfo; +class FImGuiModuleProperties; + +// Manges ImGui module console commands. +class FImGuiModuleCommands +{ +public: + + static const TCHAR* const ToggleInput; + static const TCHAR* const ToggleKeyboardNavigation; + static const TCHAR* const ToggleGamepadNavigation; + static const TCHAR* const ToggleKeyboardInputSharing; + static const TCHAR* const ToggleGamepadInputSharing; + static const TCHAR* const ToggleMouseInputSharing; + static const TCHAR* const SetMouseInputSharing; + static const TCHAR* const ToggleDemo; + + FImGuiModuleCommands(FImGuiModuleProperties& InProperties); + + void SetKeyBinding(const TCHAR* CommandName, const FImGuiKeyInfo& KeyInfo); + +private: + + void ToggleInputImpl(); + void ToggleKeyboardNavigationImpl(); + void ToggleGamepadNavigationImpl(); + void ToggleKeyboardInputSharingImpl(); + void ToggleGamepadInputSharingImpl(); + void ToggleMouseInputSharingImpl(); + void SetMouseInputSharingImpl(const TArray< FString >& Args); + void ToggleDemoImpl(); + + FImGuiModuleProperties& Properties; + + FAutoConsoleCommand ToggleInputCommand; + FAutoConsoleCommand ToggleKeyboardNavigationCommand; + FAutoConsoleCommand ToggleGamepadNavigationCommand; + FAutoConsoleCommand ToggleKeyboardInputSharingCommand; + FAutoConsoleCommand ToggleGamepadInputSharingCommand; + FAutoConsoleCommand ToggleMouseInputSharingCommand; + FAutoConsoleCommand SetMouseInputSharingCommand; + FAutoConsoleCommand ToggleDemoCommand; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiModuleDebug.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include + + +// Module-wide debug symbols and loggers. + + +// If enabled, it activates debug code and console variables that in normal usage are hidden. +#define IMGUI_MODULE_DEVELOPER 0 + + +// Input Handler logger (used also in non-developer mode to raise problems with handler extensions). +DECLARE_LOG_CATEGORY_EXTERN(LogImGuiInputHandler, Warning, All); + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiModuleManager.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiModuleManager.h" + +#include "ImGuiInteroperability.h" +#include "Utilities/WorldContextIndex.h" + +#include +#include + +#include + + +// High enough z-order guarantees that ImGui output is rendered on top of the game UI. +constexpr int32 IMGUI_WIDGET_Z_ORDER = 10000; + +// Module texture names. +const static FName PlainTextureName = "ImGuiModule_Plain"; +const static FName FontAtlasTextureName = "ImGuiModule_FontAtlas"; + +FImGuiModuleManager::FImGuiModuleManager() + : Commands(Properties) + , Settings(Properties, Commands) + , ImGuiDemo(Properties) + , ContextManager(Settings) +{ + // Register in context manager to get information whenever a new context proxy is created. + ContextManager.OnContextProxyCreated.AddRaw(this, &FImGuiModuleManager::OnContextProxyCreated); + + // Typically we will use viewport created events to add widget to new game viewports. + ViewportCreatedHandle = UGameViewportClient::OnViewportCreated().AddRaw(this, &FImGuiModuleManager::OnViewportCreated); + + // Try to register tick delegate (it may fail if Slate application isn't yet ready). + RegisterTick(); + + // If we failed to register, create an initializer that will do it later. + if (!IsTickRegistered()) + { + CreateTickInitializer(); + } + + // We need to add widgets to active game viewports as they won't generate on-created events. This is especially + // important during hot-reloading. + AddWidgetsToActiveViewports(); +} + +FImGuiModuleManager::~FImGuiModuleManager() +{ + ContextManager.OnFontAtlasBuilt.RemoveAll(this); + + // We are no longer interested with adding widgets to viewports. + if (ViewportCreatedHandle.IsValid()) + { + UGameViewportClient::OnViewportCreated().Remove(ViewportCreatedHandle); + ViewportCreatedHandle.Reset(); + } + + // Remove still active widgets (important during hot-reloading). + for (auto& Widget : Widgets) + { + auto SharedWidget = Widget.Pin(); + if (SharedWidget.IsValid()) + { + auto& WidgetGameViewport = SharedWidget->GetGameViewport(); + if (WidgetGameViewport.IsValid()) + { + WidgetGameViewport->RemoveViewportWidgetContent(SharedWidget.ToSharedRef()); + } + } + } + + // Deactivate this manager. + ReleaseTickInitializer(); + UnregisterTick(); +} + +void FImGuiModuleManager::RebuildFontAtlas() +{ + ContextManager.RebuildFontAtlas(); +} + +void FImGuiModuleManager::LoadTextures() +{ + checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can create textures.")); + + if (!bTexturesLoaded) + { + bTexturesLoaded = true; + + TextureManager.InitializeErrorTexture(FColor::Magenta); + + // Create an empty texture at index 0. We will use it for ImGui outputs with null texture id. + TextureManager.CreatePlainTexture(PlainTextureName, 2, 2, FColor::White); + + // Register for atlas built events, so we can rebuild textures. + ContextManager.OnFontAtlasBuilt.AddRaw(this, &FImGuiModuleManager::BuildFontAtlasTexture); + + BuildFontAtlasTexture(); + } +} + +void FImGuiModuleManager::BuildFontAtlasTexture() +{ + // Create a font atlas texture. + ImFontAtlas& Fonts = ContextManager.GetFontAtlas(); + + unsigned char* Pixels; + int Width, Height, Bpp; + Fonts.GetTexDataAsRGBA32(&Pixels, &Width, &Height, &Bpp); + + const TextureIndex FontsTexureIndex = TextureManager.CreateTexture(FontAtlasTextureName, Width, Height, Bpp, Pixels); + + // Set the font texture index in the ImGui. + Fonts.TexID = ImGuiInterops::ToImTextureID(FontsTexureIndex); +} + +void FImGuiModuleManager::RegisterTick() +{ + // Slate Post-Tick is a good moment to end and advance ImGui frame as it minimises a tearing. + if (!TickDelegateHandle.IsValid() && FSlateApplication::IsInitialized()) + { + TickDelegateHandle = FSlateApplication::Get().OnPostTick().AddRaw(this, &FImGuiModuleManager::Tick); + } +} + +void FImGuiModuleManager::UnregisterTick() +{ + if (TickDelegateHandle.IsValid()) + { + if (FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().OnPostTick().Remove(TickDelegateHandle); + } + TickDelegateHandle.Reset(); + } +} + +void FImGuiModuleManager::CreateTickInitializer() +{ + if (!TickInitializerHandle.IsValid()) + { + // Try to register tick delegate until we finally succeed. + + TickInitializerHandle = FModuleManager::Get().OnModulesChanged().AddLambda([this](FName Name, EModuleChangeReason Reason) + { + if (Reason == EModuleChangeReason::ModuleLoaded) + { + RegisterTick(); + } + + if (IsTickRegistered()) + { + ReleaseTickInitializer(); + } + }); + } +} + +void FImGuiModuleManager::ReleaseTickInitializer() +{ + if (TickInitializerHandle.IsValid()) + { + FModuleManager::Get().OnModulesChanged().Remove(TickInitializerHandle); + TickInitializerHandle.Reset(); + } +} + +void FImGuiModuleManager::Tick(float DeltaSeconds) +{ + if (IsInGameThread()) + { + // Update context manager to advance all ImGui contexts to the next frame. + ContextManager.Tick(DeltaSeconds); + + // Inform that we finished updating ImGui, so other subsystems can react. + PostImGuiUpdateEvent.Broadcast(); + } +} + +void FImGuiModuleManager::OnViewportCreated() +{ + checkf(FSlateApplication::IsInitialized(), TEXT("We expect Slate to be initialized when game viewport is created.")); + + // Create widget to viewport responsible for this event. + AddWidgetToViewport(GEngine->GameViewport); +} + +void FImGuiModuleManager::AddWidgetToViewport(UGameViewportClient* GameViewport) +{ + checkf(GameViewport, TEXT("Null game viewport.")); + checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can add widget to game viewports.")); + + // Make sure that we have a context for this viewport's world and get its index. + int32 ContextIndex; + auto& ContextProxy = ContextManager.GetWorldContextProxy(*GameViewport->GetWorld(), ContextIndex); + + // Make sure that textures are loaded before the first Slate widget is created. + LoadTextures(); + + // Create and initialize the widget. + TSharedPtr SharedWidget; + SAssignNew(SharedWidget, SImGuiLayout).ModuleManager(this).GameViewport(GameViewport).ContextIndex(ContextIndex); + + GameViewport->AddViewportWidgetContent(SharedWidget.ToSharedRef(), IMGUI_WIDGET_Z_ORDER); + + // We transfer widget ownerships to viewports but we keep weak references in case we need to manually detach active + // widgets during module shutdown (important during hot-reloading). + if (TWeakPtr* Slot = Widgets.FindByPredicate([](auto& Widget) { return !Widget.IsValid(); })) + { + *Slot = SharedWidget; + } + else + { + Widgets.Emplace(SharedWidget); + } +} + +void FImGuiModuleManager::AddWidgetsToActiveViewports() +{ + if (FSlateApplication::IsInitialized() && GEngine) + { + // Loop as long as we have a valid viewport or until we detect a cycle. + UGameViewportClient* GameViewport = GEngine->GameViewport; + while (GameViewport) + { + AddWidgetToViewport(GameViewport); + + GameViewport = GEngine->GetNextPIEViewport(GameViewport); + if (GameViewport == GEngine->GameViewport) + { + break; + } + } + } +} + +void FImGuiModuleManager::OnContextProxyCreated(int32 ContextIndex, FImGuiContextProxy& ContextProxy) +{ + ContextProxy.OnDraw().AddLambda([this, ContextIndex]() { ImGuiDemo.DrawControls(ContextIndex); }); +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiModuleManager.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "ImGuiContextManager.h" +#include "ImGuiDemo.h" +#include "ImGuiModuleCommands.h" +#include "ImGuiModuleProperties.h" +#include "ImGuiModuleSettings.h" +#include "TextureManager.h" +#include "Widgets/SImGuiLayout.h" + + +// Central manager that implements module logic. It initializes and controls remaining module components. +class FImGuiModuleManager +{ + // Allow module to control life-cycle of this class. + friend class FImGuiModule; + +public: + + // Get interface to module settings. + FImGuiModuleSettings& GetSettings() { return Settings; } + + // Get interface to module state properties. + FImGuiModuleProperties& GetProperties() { return Properties; } + + // Get ImGui contexts manager. + FImGuiContextManager& GetContextManager() { return ContextManager; } + + // Get texture resources manager. + FTextureManager& GetTextureManager() { return TextureManager; } + + // Event called right after ImGui is updated, to give other subsystems chance to react. + FSimpleMulticastDelegate& OnPostImGuiUpdate() { return PostImGuiUpdateEvent; } + + void RebuildFontAtlas(); + +private: + + FImGuiModuleManager(); + ~FImGuiModuleManager(); + + FImGuiModuleManager(const FImGuiModuleManager&) = delete; + FImGuiModuleManager& operator=(const FImGuiModuleManager&) = delete; + + FImGuiModuleManager(FImGuiModuleManager&&) = delete; + FImGuiModuleManager& operator=(FImGuiModuleManager&&) = delete; + + void LoadTextures(); + void BuildFontAtlasTexture(); + + bool IsTickRegistered() { return TickDelegateHandle.IsValid(); } + void RegisterTick(); + void UnregisterTick(); + + void CreateTickInitializer(); + void ReleaseTickInitializer(); + + void Tick(float DeltaSeconds); + + void OnViewportCreated(); + + void AddWidgetToViewport(UGameViewportClient* GameViewport); + void AddWidgetsToActiveViewports(); + + void OnContextProxyCreated(int32 ContextIndex, FImGuiContextProxy& ContextProxy); + + // Event that we call after ImGui is updated. + FSimpleMulticastDelegate PostImGuiUpdateEvent; + + // Collection of module state properties. + FImGuiModuleProperties Properties; + + // Tying module console commands to life-cycle of this manager and module. + FImGuiModuleCommands Commands; + + // ImGui settings proxy (valid in every loading stage). + FImGuiModuleSettings Settings; + + // Widget that we add to all created contexts to draw ImGui demo. + FImGuiDemo ImGuiDemo; + + // Manager for ImGui contexts. + FImGuiContextManager ContextManager; + + // Manager for textures resources. + FTextureManager TextureManager; + + // Slate widgets that we created. + TArray> Widgets; + + FDelegateHandle TickInitializerHandle; + FDelegateHandle TickDelegateHandle; + FDelegateHandle ViewportCreatedHandle; + + bool bTexturesLoaded = false; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiModuleSettings.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiModuleSettings.h" + +#include "ImGuiModuleCommands.h" +#include "ImGuiModuleProperties.h" + +#include +#include +#include + + +//==================================================================================================== +// FImGuiDPIScaleInfo +//==================================================================================================== + +FImGuiDPIScaleInfo::FImGuiDPIScaleInfo() +{ + if (FRichCurve* Curve = DPICurve.GetRichCurve()) + { + Curve->AddKey( 0.0f, 1.f); + + Curve->AddKey(2159.5f, 1.f); + Curve->AddKey(2160.0f, 2.f); + + Curve->AddKey(4319.5f, 2.f); + Curve->AddKey(4320.0f, 4.f); + } +} + +float FImGuiDPIScaleInfo::CalculateResolutionBasedScale() const +{ + float ResolutionBasedScale = 1.f; + if (bScaleWithCurve && GEngine && GEngine->GameUserSettings) + { + if (const FRichCurve* Curve = DPICurve.GetRichCurveConst()) + { + ResolutionBasedScale *= Curve->Eval((float)GEngine->GameUserSettings->GetDesktopResolution().Y, 1.f); + } + } + return ResolutionBasedScale; +} + +//==================================================================================================== +// UImGuiSettings +//==================================================================================================== + +UImGuiSettings* UImGuiSettings::DefaultInstance = nullptr; + +FSimpleMulticastDelegate UImGuiSettings::OnSettingsLoaded; + +void UImGuiSettings::PostInitProperties() +{ + Super::PostInitProperties(); + + if (IsTemplate()) + { + DefaultInstance = this; + OnSettingsLoaded.Broadcast(); + } +} + +void UImGuiSettings::BeginDestroy() +{ + Super::BeginDestroy(); + + if (DefaultInstance == this) + { + DefaultInstance = nullptr; + } +} + +//==================================================================================================== +// FImGuiModuleSettings +//==================================================================================================== + +FImGuiModuleSettings::FImGuiModuleSettings(FImGuiModuleProperties& InProperties, FImGuiModuleCommands& InCommands) + : Properties(InProperties) + , Commands(InCommands) +{ +#if WITH_EDITOR + FCoreUObjectDelegates::OnObjectPropertyChanged.AddRaw(this, &FImGuiModuleSettings::OnPropertyChanged); +#endif + + // Delegate initializer to support settings loaded after this object creation (in stand-alone builds) and potential + // reloading of settings. + UImGuiSettings::OnSettingsLoaded.AddRaw(this, &FImGuiModuleSettings::InitializeAllSettings); + + // Call initializer to support settings already loaded (editor). + InitializeAllSettings(); +} + +FImGuiModuleSettings::~FImGuiModuleSettings() +{ + + UImGuiSettings::OnSettingsLoaded.RemoveAll(this); + +#if WITH_EDITOR + FCoreUObjectDelegates::OnObjectPropertyChanged.RemoveAll(this); +#endif +} + +void FImGuiModuleSettings::InitializeAllSettings() +{ + UpdateSettings(); + UpdateDPIScaleInfo(); +} + +void FImGuiModuleSettings::UpdateSettings() +{ + if (UImGuiSettings* SettingsObject = UImGuiSettings::Get()) + { + SetImGuiInputHandlerClass(SettingsObject->ImGuiInputHandlerClass); + SetShareKeyboardInput(SettingsObject->bShareKeyboardInput); + SetShareGamepadInput(SettingsObject->bShareGamepadInput); + SetShareMouseInput(SettingsObject->bShareMouseInput); + SetUseSoftwareCursor(SettingsObject->bUseSoftwareCursor); + SetToggleInputKey(SettingsObject->ToggleInput); + SetCanvasSizeInfo(SettingsObject->CanvasSize); + } +} + +void FImGuiModuleSettings::UpdateDPIScaleInfo() +{ + if (UImGuiSettings* SettingsObject = UImGuiSettings::Get()) + { + SetDPIScaleInfo(SettingsObject->DPIScale); + } +} + +void FImGuiModuleSettings::SetImGuiInputHandlerClass(const FSoftClassPath& ClassReference) +{ + if (ImGuiInputHandlerClass != ClassReference) + { + ImGuiInputHandlerClass = ClassReference; + OnImGuiInputHandlerClassChanged.Broadcast(ClassReference); + } +} + +void FImGuiModuleSettings::SetShareKeyboardInput(bool bShare) +{ + if (bShareKeyboardInput != bShare) + { + bShareKeyboardInput = bShare; + Properties.SetKeyboardInputShared(bShare); + } +} + +void FImGuiModuleSettings::SetShareGamepadInput(bool bShare) +{ + if (bShareGamepadInput != bShare) + { + bShareGamepadInput = bShare; + Properties.SetGamepadInputShared(bShare); + } +} + +void FImGuiModuleSettings::SetShareMouseInput(bool bShare) +{ + if (bShareMouseInput != bShare) + { + bShareMouseInput = bShare; + Properties.SetMouseInputShared(bShare); + } +} + +void FImGuiModuleSettings::SetUseSoftwareCursor(bool bUse) +{ + if (bUseSoftwareCursor != bUse) + { + bUseSoftwareCursor = bUse; + OnUseSoftwareCursorChanged.Broadcast(bUse); + } +} + +void FImGuiModuleSettings::SetToggleInputKey(const FImGuiKeyInfo& KeyInfo) +{ + if (ToggleInputKey != KeyInfo) + { + ToggleInputKey = KeyInfo; + Commands.SetKeyBinding(FImGuiModuleCommands::ToggleInput, ToggleInputKey); + } +} + +void FImGuiModuleSettings::SetCanvasSizeInfo(const FImGuiCanvasSizeInfo& CanvasSizeInfo) +{ + if (CanvasSize != CanvasSizeInfo) + { + CanvasSize = CanvasSizeInfo; + OnCanvasSizeChangedDelegate.Broadcast(CanvasSize); + } +} + +void FImGuiModuleSettings::SetDPIScaleInfo(const FImGuiDPIScaleInfo& ScaleInfo) +{ + DPIScale = ScaleInfo; + OnDPIScaleChangedDelegate.Broadcast(DPIScale); +} + +#if WITH_EDITOR + +void FImGuiModuleSettings::OnPropertyChanged(class UObject* ObjectBeingModified, struct FPropertyChangedEvent& PropertyChangedEvent) +{ + if (ObjectBeingModified == UImGuiSettings::Get()) + { + UpdateSettings(); + if (PropertyChangedEvent.MemberProperty + && (PropertyChangedEvent.MemberProperty->GetFName() == GET_MEMBER_NAME_CHECKED(FImGuiModuleSettings, DPIScale))) + { + UpdateDPIScaleInfo(); + } + } +} + +#endif // WITH_EDITOR + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiModuleSettings.h`: + +```h +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "VersionCompatibility.h" + +#include +#include +#include +#include +#include + +// We use FSoftClassPath, which is supported by older and newer engine versions. Starting from 4.18, it is +// a typedef of FSoftClassPath, which is also recognized by UHT. +#if ENGINE_COMPATIBILITY_LEGACY_STRING_CLASS_REF +#include +#else +#include +#endif + +#include "ImGuiModuleSettings.generated.h" + + +/** + * Struct containing key information that can be used for key binding. Using 'Undetermined' value for modifier keys + * means that those keys should be ignored when testing for a match. + */ +USTRUCT() +struct FImGuiKeyInfo +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category = "Input") + FKey Key; + + UPROPERTY(EditAnywhere, Category = "Input") + ECheckBoxState Shift = ECheckBoxState::Undetermined; + + UPROPERTY(EditAnywhere, Category = "Input") + ECheckBoxState Ctrl = ECheckBoxState::Undetermined; + + UPROPERTY(EditAnywhere, Category = "Input") + ECheckBoxState Alt = ECheckBoxState::Undetermined; + + UPROPERTY(EditAnywhere, Category = "Input") + ECheckBoxState Cmd = ECheckBoxState::Undetermined; + + friend bool operator==(const FImGuiKeyInfo& Lhs, const FImGuiKeyInfo& Rhs) + { + return Lhs.Key == Rhs.Key + && Lhs.Shift == Rhs.Shift + && Lhs.Ctrl == Rhs.Ctrl + && Lhs.Alt == Rhs.Alt + && Lhs.Cmd == Rhs.Cmd; + } + + friend bool operator!=(const FImGuiKeyInfo& Lhs, const FImGuiKeyInfo& Rhs) + { + return !(Lhs == Rhs); + } +}; + +UENUM(BlueprintType) +enum class EImGuiCanvasSizeType : uint8 +{ + Custom UMETA(ToolTip = "Canvas will have a custom width and height."), + Desktop UMETA(ToolTip = "Canvas will have the same width and height as the desktop."), + Viewport UMETA(ToolTip = "Canvas will always have the same width and height as the viewport."), +}; + +/** + * Struct with information how to calculate canvas size. + */ +USTRUCT() +struct FImGuiCanvasSizeInfo +{ + GENERATED_BODY() + + // Select how to specify canvas size. + UPROPERTY(EditAnywhere, Category = "Canvas Size") + EImGuiCanvasSizeType SizeType = EImGuiCanvasSizeType::Desktop; + + // Custom canvas width. + UPROPERTY(EditAnywhere, Category = "Canvas Size", meta = (ClampMin = 0, UIMin = 0)) + int32 Width = 3840; + + // Custom canvas height. + UPROPERTY(EditAnywhere, Category = "Canvas Size", meta = (ClampMin = 0, UIMin = 0)) + int32 Height = 2160; + + // If this is true, canvas width or height may be extended, if the viewport size is larger. + UPROPERTY(EditAnywhere, Category = "Canvas Size", meta = (ClampMin = 0, UIMin = 0)) + bool bExtendToViewport = true; + + bool operator==(const FImGuiCanvasSizeInfo& Other) const + { + return (SizeType == Other.SizeType) && (Width == Other.Width) + && (Height == Other.Height) && (bExtendToViewport == Other.bExtendToViewport); + } + + bool operator!=(const FImGuiCanvasSizeInfo& Other) const { return !(*this == Other); } +}; + +UENUM(BlueprintType) +enum class EImGuiDPIScaleMethod : uint8 +{ + ImGui UMETA(DisplayName = "ImGui", ToolTip = "Scale ImGui fonts and styles."), + Slate UMETA(ToolTip = "Scale in Slate. ImGui canvas size will be adjusted to get the screen size that is the same as defined in the Canvas Size property.") +}; + +/** + * Struct with DPI scale data. + */ +USTRUCT() +struct FImGuiDPIScaleInfo +{ + GENERATED_BODY() + +protected: + + // Whether to scale in ImGui or in Slate. Scaling in ImGui gives better looking results but Slate might be a better + // option when layouts do not account for different fonts and styles. When scaling in Slate, ImGui canvas size will + // be adjusted to get the screen size that is the same as defined in the Canvas Size property. + UPROPERTY(EditAnywhere, Category = "DPI Scale") + EImGuiDPIScaleMethod ScalingMethod = EImGuiDPIScaleMethod::ImGui; + + // An optional scale to apply on top or instead of the curve-based scale. + UPROPERTY(EditAnywhere, Category = "DPI Scale", meta = (ClampMin = 0, UIMin = 0)) + float Scale = 1.f; + + // Curve mapping resolution height to scale. + UPROPERTY(config, EditAnywhere, Category = "DPI Scale", meta = (XAxisName = "Resolution Height", YAxisName = "Scale", EditCondition = "bScaleWithCurve")) + FRuntimeFloatCurve DPICurve; + + // Whether to use curve-based scaling. If enabled, Scale will be multiplied by a value read from the DPICurve. + // If disabled, only the Scale property will be used. + UPROPERTY(config, EditAnywhere, Category = "DPI Scale") + bool bScaleWithCurve = true; + +public: + + FImGuiDPIScaleInfo(); + + float GetImGuiScale() const { return ShouldScaleInSlate() ? 1.f : CalculateScale(); } + + float GetSlateScale() const { return ShouldScaleInSlate() ? CalculateScale() : 1.f; } + + bool ShouldScaleInSlate() const { return ScalingMethod == EImGuiDPIScaleMethod::Slate; } + +private: + + float CalculateScale() const { return Scale * CalculateResolutionBasedScale(); } + + float CalculateResolutionBasedScale() const; +}; + +// UObject used for loading and saving ImGui settings. To access actual settings use FImGuiModuleSettings interface. +UCLASS(config=ImGui, defaultconfig) +class UImGuiSettings : public UObject +{ + GENERATED_BODY() + +public: + + // Get default instance or null if it is not loaded. + static UImGuiSettings* Get() { return DefaultInstance; } + + // Delegate raised when default instance is loaded. + static FSimpleMulticastDelegate OnSettingsLoaded; + + virtual void PostInitProperties() override; + virtual void BeginDestroy() override; + +protected: + + // Path to own implementation of ImGui Input Handler allowing to customize handling of keyboard and gamepad input. + // If not set then default handler is used. + UPROPERTY(EditAnywhere, config, Category = "Extensions", meta = (MetaClass = "ImGuiInputHandler")) + FSoftClassPath ImGuiInputHandlerClass; + + // Whether ImGui should share keyboard input with game. + // This defines initial behaviour which can be later changed using 'ImGui.ToggleKeyboardInputSharing' command or + // module properties interface. + UPROPERTY(EditAnywhere, config, Category = "Input") + bool bShareKeyboardInput = false; + + // Whether ImGui should share gamepad input with game. + // This defines initial behaviour which can be later changed using 'ImGui.ToggleGamepadInputSharing' command or + // module properties interface. + UPROPERTY(EditAnywhere, config, Category = "Input") + bool bShareGamepadInput = false; + + // Whether ImGui should share mouse input with game. + // This defines initial behaviour which can be later changed using 'ImGui.ToggleMouseInputSharing' command or + // module properties interface. + UPROPERTY(EditAnywhere, config, Category = "Input") + bool bShareMouseInput = false; + + // If true, then in input mode ImGui will draw its own cursor in place of the hardware one. + // When disabled (default) there is a noticeable difference between cursor position seen by ImGui and position on + // the screen. Enabling this option removes that effect but with lower frame-rates UI becomes quickly unusable. + UPROPERTY(EditAnywhere, config, Category = "Input", AdvancedDisplay) + bool bUseSoftwareCursor = false; + + // Define a shortcut key to 'ImGui.ToggleInput' command. Binding is only set if the key field is valid. + // Note that modifier key properties can be set to one of the three values: undetermined means that state of the given + // modifier is not important, checked means that it needs to be pressed and unchecked means that it cannot be pressed. + // + // This binding is using Player Input's DebugExecBindings which only works in non-shipment builds. + UPROPERTY(EditAnywhere, config, Category = "Keyboard Shortcuts") + FImGuiKeyInfo ToggleInput; + + // Chose how to define the ImGui canvas size. Select between custom, desktop and viewport. + UPROPERTY(EditAnywhere, config, Category = "Canvas Size") + FImGuiCanvasSizeInfo CanvasSize; + + // Setup DPI Scale. + UPROPERTY(EditAnywhere, config, Category = "DPI Scale", Meta = (ShowOnlyInnerProperties)) + FImGuiDPIScaleInfo DPIScale; + + static UImGuiSettings* DefaultInstance; + + friend class FImGuiModuleSettings; +}; + + +class FImGuiModuleCommands; +class FImGuiModuleProperties; + +// Interface for ImGui module settings. It shadows all the settings and keep them in sync after UImGuiSettings class is +// loaded, but it can also work before that time what simplifies workflow in early-loading scenarios. +// It binds to module properties and commands objects that need to be passed during construction. +class FImGuiModuleSettings +{ +public: + + // Generic delegate used to notify changes of boolean properties. + DECLARE_MULTICAST_DELEGATE_OneParam(FBoolChangeDelegate, bool); + DECLARE_MULTICAST_DELEGATE_OneParam(FStringClassReferenceChangeDelegate, const FSoftClassPath&); + DECLARE_MULTICAST_DELEGATE_OneParam(FImGuiCanvasSizeInfoChangeDelegate, const FImGuiCanvasSizeInfo&); + DECLARE_MULTICAST_DELEGATE_OneParam(FImGuiDPIScaleInfoChangeDelegate, const FImGuiDPIScaleInfo&); + + // Constructor for ImGui module settings. It will bind to instances of module properties and commands and will + // update them every time when settings are changed. + // + // @param InProperties - Instance of module properties that will be bound and updated by this object. + // @param InCommands - Instance of module commands that will be bound and updated by this object. + FImGuiModuleSettings(FImGuiModuleProperties& InProperties, FImGuiModuleCommands& InCommands); + ~FImGuiModuleSettings(); + + // It doesn't offer interface for settings that define initial values for properties, as those are passed during + // start-up and should be accessed trough properties interface. Remaining settings can have getter and/or change + // event that are defined depending on needs. + + // Get the path to custom implementation of ImGui Input Handler. + const FSoftClassPath& GetImGuiInputHandlerClass() const { return ImGuiInputHandlerClass; } + + // Get the software cursor configuration. + bool UseSoftwareCursor() const { return bUseSoftwareCursor; } + + // Get the shortcut configuration for 'ImGui.ToggleInput' command. + const FImGuiKeyInfo& GetToggleInputKey() const { return ToggleInputKey; } + + // Get the information how to calculate the canvas size. + const FImGuiCanvasSizeInfo& GetCanvasSizeInfo() const { return CanvasSize; } + + // Get the DPI Scale information. + const FImGuiDPIScaleInfo& GetDPIScaleInfo() const { return DPIScale; } + + // Delegate raised when ImGui Input Handle is changed. + FStringClassReferenceChangeDelegate OnImGuiInputHandlerClassChanged; + + // Delegate raised when software cursor configuration is changed. + FBoolChangeDelegate OnUseSoftwareCursorChanged; + + // Delegate raised when information how to calculate the canvas size is changed. + FImGuiCanvasSizeInfoChangeDelegate OnCanvasSizeChangedDelegate; + + // Delegate raised when the DPI scale is changed. + FImGuiDPIScaleInfoChangeDelegate OnDPIScaleChangedDelegate; + +private: + + void InitializeAllSettings(); + void UpdateSettings(); + void UpdateDPIScaleInfo(); + + void SetImGuiInputHandlerClass(const FSoftClassPath& ClassReference); + void SetShareKeyboardInput(bool bShare); + void SetShareGamepadInput(bool bShare); + void SetShareMouseInput(bool bShare); + void SetUseSoftwareCursor(bool bUse); + void SetToggleInputKey(const FImGuiKeyInfo& KeyInfo); + void SetCanvasSizeInfo(const FImGuiCanvasSizeInfo& CanvasSizeInfo); + void SetDPIScaleInfo(const FImGuiDPIScaleInfo& ScaleInfo); + +#if WITH_EDITOR + void OnPropertyChanged(class UObject* ObjectBeingModified, struct FPropertyChangedEvent& PropertyChangedEvent); +#endif // WITH_EDITOR + + FImGuiModuleProperties& Properties; + FImGuiModuleCommands& Commands; + + FSoftClassPath ImGuiInputHandlerClass; + FImGuiKeyInfo ToggleInputKey; + FImGuiCanvasSizeInfo CanvasSize; + FImGuiDPIScaleInfo DPIScale; + bool bShareKeyboardInput = false; + bool bShareGamepadInput = false; + bool bShareMouseInput = false; + bool bUseSoftwareCursor = false; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Private/ImGuiTextureHandle.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiTextureHandle.h" + +#include "ImGuiInteroperability.h" + + +FImGuiTextureHandle::FImGuiTextureHandle() + : FImGuiTextureHandle(NAME_None, ImGuiInterops::ToImTextureID(INDEX_NONE)) +{ +} + +FImGuiTextureHandle::FImGuiTextureHandle(const FName& InName, ImTextureID InTextureId) + : Name(InName) + , TextureId(InTextureId) +{ + const TextureIndex Index = ImGuiInterops::ToTextureIndex(TextureId); + checkf((Index == INDEX_NONE) == (Name == NAME_None), + TEXT("Mismatch between Name and TextureId parameters, with only one indicating a null handle.") + TEXT(" Name = '%s', TextureIndex(TextureId) = %d"), *Name.ToString(), Index); +} + +// FImGuiTextureHandle::HasValidEntry() is implemented in ImGuiModule.cpp to get access to FImGuiModuleManager instance +// without referencing in this class. + +``` + +`Plugins/ImGui/Source/ImGui/Private/TextureManager.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "TextureManager.h" + +#include +#include + +#include + + +void FTextureManager::InitializeErrorTexture(const FColor& Color) +{ + CreatePlainTextureInternal(NAME_ErrorTexture, 2, 2, Color); +} + +TextureIndex FTextureManager::CreateTexture(const FName& Name, int32 Width, int32 Height, uint32 SrcBpp, uint8* SrcData, TFunction SrcDataCleanup) +{ + checkf(Name != NAME_None, TEXT("Trying to create a texture with a name 'NAME_None' is not allowed.")); + + return CreateTextureInternal(Name, Width, Height, SrcBpp, SrcData, SrcDataCleanup); +} + +TextureIndex FTextureManager::CreatePlainTexture(const FName& Name, int32 Width, int32 Height, FColor Color) +{ + checkf(Name != NAME_None, TEXT("Trying to create a texture with a name 'NAME_None' is not allowed.")); + + return CreatePlainTextureInternal(Name, Width, Height, Color); +} + +TextureIndex FTextureManager::CreateTextureResources(const FName& Name, UTexture* Texture) +{ + checkf(Name != NAME_None, TEXT("Trying to create texture resources with a name 'NAME_None' is not allowed.")); + checkf(Texture, TEXT("Null Texture.")); + + // Create an entry for the texture. + return AddTextureEntry(Name, Texture, false); +} + +void FTextureManager::ReleaseTextureResources(TextureIndex Index) +{ + checkf(IsInRange(Index), TEXT("Invalid texture index %d. Texture resources array has %d entries total."), Index, TextureResources.Num()); + + TextureResources[Index] = {}; +} + +TextureIndex FTextureManager::CreateTextureInternal(const FName& Name, int32 Width, int32 Height, uint32 SrcBpp, uint8* SrcData, TFunction SrcDataCleanup) +{ + // Create a texture. + UTexture2D* Texture = UTexture2D::CreateTransient(Width, Height); + + // Create a new resource for that texture. + Texture->UpdateResource(); + + // Update texture data. + FUpdateTextureRegion2D* TextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, Width, Height); + auto DataCleanup = [SrcDataCleanup](uint8* Data, const FUpdateTextureRegion2D* UpdateRegion) + { + SrcDataCleanup(Data); + delete UpdateRegion; + }; + Texture->UpdateTextureRegions(0, 1u, TextureRegion, SrcBpp * Width, SrcBpp, SrcData, DataCleanup); + + // Create an entry for the texture. + if (Name == NAME_ErrorTexture) + { + ErrorTexture = { Name, Texture, true }; + return INDEX_ErrorTexture; + } + else + { + return AddTextureEntry(Name, Texture, true); + } +} + +TextureIndex FTextureManager::CreatePlainTextureInternal(const FName& Name, int32 Width, int32 Height, const FColor& Color) +{ + // Create buffer with raw data. + const uint32 ColorPacked = Color.ToPackedARGB(); + const uint32 Bpp = sizeof(ColorPacked); + const uint32 SizeInPixels = Width * Height; + const uint32 SizeInBytes = SizeInPixels * Bpp; + uint8* SrcData = new uint8[SizeInBytes]; + std::fill(reinterpret_cast(SrcData), reinterpret_cast(SrcData) + SizeInPixels, ColorPacked); + auto SrcDataCleanup = [](uint8* Data) { delete[] Data; }; + + // Create new texture from raw data. + return CreateTextureInternal(Name, Width, Height, Bpp, SrcData, SrcDataCleanup); +} + +TextureIndex FTextureManager::AddTextureEntry(const FName& Name, UTexture* Texture, bool bAddToRoot) +{ + // Try to find an entry with that name. + TextureIndex Index = FindTextureIndex(Name); + + // If this is a new name, try to find an entry to reuse. + if (Index == INDEX_NONE) + { + Index = FindTextureIndex(NAME_None); + } + + // Either update/reuse an entry or add a new one. + if (Index != INDEX_NONE) + { + TextureResources[Index] = { Name, Texture, bAddToRoot }; + return Index; + } + else + { + return TextureResources.Emplace(Name, Texture, bAddToRoot); + } +} + +FTextureManager::FTextureEntry::FTextureEntry(const FName& InName, UTexture* InTexture, bool bAddToRoot) + : Name(InName) +{ + checkf(InTexture, TEXT("Null texture.")); + + if (bAddToRoot) + { + // Get pointer only for textures that we added to root, so we can later release them. + Texture = InTexture; + // Add texture to the root to prevent garbage collection. + InTexture->AddToRoot(); + } + + // Create brush and resource handle for input texture. + Brush.SetResourceObject(InTexture); + CachedResourceHandle = FSlateApplication::Get().GetRenderer()->GetResourceHandle(Brush); +} + +FTextureManager::FTextureEntry::~FTextureEntry() +{ + Reset(true); +} + +FTextureManager::FTextureEntry& FTextureManager::FTextureEntry::operator=(FTextureEntry&& Other) +{ + // Release old resources if allocated. + Reset(true); + + // Move data and ownership to this instance. + Name = MoveTemp(Other.Name); + Texture = MoveTemp(Other.Texture); + Brush = MoveTemp(Other.Brush); + CachedResourceHandle = MoveTemp(Other.CachedResourceHandle); + + // Reset the other entry (without releasing resources which are already moved to this instance) to remove tracks + // of ownership and mark it as empty/reusable. + Other.Reset(false); + + return *this; +} + +const FSlateResourceHandle& FTextureManager::FTextureEntry::GetResourceHandle() const +{ + if (!CachedResourceHandle.IsValid() && Brush.HasUObject()) + { + CachedResourceHandle = FSlateApplication::Get().GetRenderer()->GetResourceHandle(Brush); + } + return CachedResourceHandle; +} + +void FTextureManager::FTextureEntry::Reset(bool bReleaseResources) +{ + if (bReleaseResources) + { + // Release brush. + if (Brush.HasUObject() && FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().GetRenderer()->ReleaseDynamicResource(Brush); + } + + // Remove texture from root to allow for garbage collection (it might be invalid, if we never set it + // or this is an application shutdown). + if (Texture.IsValid()) + { + Texture->RemoveFromRoot(); + } + } + + // We use empty name to mark unused entries. + Name = NAME_None; + + // Clean fields to make sure that we don't reference released or moved resources. + Texture.Reset(); + Brush = FSlateNoResource(); + CachedResourceHandle = FSlateResourceHandle(); +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/TextureManager.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include +#include +#include + + +class UTexture2D; + +// Index type to be used as a texture handle. +using TextureIndex = int32; + +// Manager for textures resources which can be referenced by a unique name or index. +// Name is primarily for lookup and index provides a direct access to resources. +class FTextureManager +{ +public: + + // Creates an empty manager. + FTextureManager() = default; + + // Copying is disabled to protected resource ownership. + FTextureManager(const FTextureManager&) = delete; + FTextureManager& operator=(const FTextureManager&) = delete; + + // Moving transfers ownership and leaves source empty. + FTextureManager(FTextureManager&&) = delete; + FTextureManager& operator=(FTextureManager&&) = delete; + + // Initialize error texture that will be used for rendering textures without registered resources. Can be called + // multiple time, if color needs to be changed. + // Note: Because of any-time module loading and lazy resources initialization goals we can't simply call it from + // constructor. + // @param Color - The color of the error texture + void InitializeErrorTexture(const FColor& Color); + + // Find texture index by name. + // @param Name - The name of a texture to find + // @returns The index of a texture with given name or INDEX_NONE if there is no such texture + TextureIndex FindTextureIndex(const FName& Name) const + { + return TextureResources.IndexOfByPredicate([&](const auto& Entry) { return Entry.GetName() == Name; }); + } + + // Get the name of a texture at given index. Returns NAME_None, if index is out of range. + // @param Index - Index of a texture + // @returns The name of a texture at given index or NAME_None if index is out of range. + FName GetTextureName(TextureIndex Index) const + { + return IsInRange(Index) ? TextureResources[Index].GetName() : NAME_None; + } + + // Get the Slate Resource Handle to a texture at given index. If index is out of range or resources are not valid + // it returns a handle to the error texture. + // @param Index - Index of a texture + // @returns The Slate Resource Handle for a texture at given index or to error texture, if no valid resources were + // found at given index + const FSlateResourceHandle& GetTextureHandle(TextureIndex Index) const + { + return IsValidTexture(Index) ? TextureResources[Index].GetResourceHandle() : ErrorTexture.GetResourceHandle(); + } + + // Create a texture from raw data. + // @param Name - The texture name + // @param Width - The texture width + // @param Height - The texture height + // @param SrcBpp - The size in bytes of one pixel + // @param SrcData - The source data + // @param SrcDataCleanup - Optional function called to release source data after texture is created (only needed, if data need to be released) + // @returns The index of a texture that was created + TextureIndex CreateTexture(const FName& Name, int32 Width, int32 Height, uint32 SrcBpp, uint8* SrcData, TFunction SrcDataCleanup = [](uint8*) {}); + + // Create a plain texture. + // @param Name - The texture name + // @param Width - The texture width + // @param Height - The texture height + // @param Color - The texture color + // @returns The index of a texture that was created + TextureIndex CreatePlainTexture(const FName& Name, int32 Width, int32 Height, FColor Color); + + // Create Slate resources to an existing texture, managed externally. + // @param Name - The texture name + // @param Texture - The texture + // @returns The index to created/updated texture resources + TextureIndex CreateTextureResources(const FName& Name, UTexture* Texture); + + // Release resources for given texture. Ignores invalid indices. + // @param Index - The index of a texture resources + void ReleaseTextureResources(TextureIndex Index); + +private: + + // See CreateTexture for general description. + // Internal implementations doesn't validate name or resource uniqueness. Instead it uses NAME_ErrorTexture + // (aka NAME_None) and INDEX_ErrorTexture (aka INDEX_NONE) to identify ErrorTexture. + TextureIndex CreateTextureInternal(const FName& Name, int32 Width, int32 Height, uint32 SrcBpp, uint8* SrcData, TFunction SrcDataCleanup = [](uint8*) {}); + + // See CreatePlainTexture for general description. + // Internal implementations doesn't validate name or resource uniqueness. Instead it uses NAME_ErrorTexture + // (aka NAME_None) and INDEX_ErrorTexture (aka INDEX_NONE) to identify ErrorTexture. + TextureIndex CreatePlainTextureInternal(const FName& Name, int32 Width, int32 Height, const FColor& Color); + + // Add or reuse texture entry. + // @param Name - The texture name + // @param Texture - The texture + // @param bAddToRoot - If true, we should add texture to root to prevent garbage collection (use for own textures) + // @returns The index of the entry that we created or reused + TextureIndex AddTextureEntry(const FName& Name, UTexture* Texture, bool bAddToRoot); + + // Check whether index is in range allocated for TextureResources (it doesn't mean that resources are valid). + FORCEINLINE bool IsInRange(TextureIndex Index) const + { + return static_cast(Index) < static_cast(TextureResources.Num()); + } + + // Check whether index is in range and whether texture resources are valid (using NAME_None sentinel). + FORCEINLINE bool IsValidTexture(TextureIndex Index) const + { + return IsInRange(Index) && TextureResources[Index].GetName() != NAME_None; + } + + // Entry for texture resources. Only supports explicit construction. + struct FTextureEntry + { + FTextureEntry() = default; + FTextureEntry(const FName& InName, UTexture* InTexture, bool bAddToRoot); + ~FTextureEntry(); + + // Copying is not supported. + FTextureEntry(const FTextureEntry&) = delete; + FTextureEntry& operator=(const FTextureEntry&) = delete; + + // We rely on TArray and don't implement custom move constructor... + FTextureEntry(FTextureEntry&&) = delete; + // ... but we need move assignment to support reusing entries. + FTextureEntry& operator=(FTextureEntry&& Other); + + const FName& GetName() const { return Name; } + const FSlateResourceHandle& GetResourceHandle() const; + + private: + + void Reset(bool bReleaseResources); + + FName Name = NAME_None; + mutable FSlateResourceHandle CachedResourceHandle; + TWeakObjectPtr Texture; + FSlateBrush Brush; + }; + + TArray TextureResources; + FTextureEntry ErrorTexture; + + static constexpr EName NAME_ErrorTexture = NAME_None; + static constexpr TextureIndex INDEX_ErrorTexture = INDEX_NONE; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Private/Utilities/Arrays.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "Range.h" + +#include +#include +#include + + +// Utilities to work with one-dimensional, statically bound arrays. Code relying on those utilities should work without +// modifications with fixed-sized arrays (currently used in ImGui) and with standard arrays. + +namespace Utilities +{ + //==================================================================================================== + // Helper functions + //==================================================================================================== + + // Function to determine number of elements in fixed size array. + template + constexpr std::size_t GetArraySize(const T(&)[N]) + { + return N; + } + + // Function to determine number of elements in std array. + template + constexpr std::size_t GetArraySize(const std::array&) + { + return N; + } + + + //==================================================================================================== + // Traits + //==================================================================================================== + + template + struct ArraySize; + + // Struct to determine number of elements in fixed size array. + template + struct ArraySize : std::extent + { + }; + + // Struct to determine number of elements in std array. + template + struct ArraySize> : std::tuple_size> + { + }; + + + //==================================================================================================== + // Ranges + //==================================================================================================== + + // Array indices range. Limited by 0 and array size. + template + using TArrayIndexRange = TBoundedRange::value>; +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/Utilities/DebugExecBindings.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "DebugExecBindings.h" + +#include "ImGuiModuleSettings.h" + +#include +#include + + +namespace +{ + FKeyBind CreateKeyBind(const FImGuiKeyInfo& KeyInfo, const FString& Command) + { + FKeyBind KeyBind; + KeyBind.Command = Command; + KeyBind.Key = KeyInfo.Key; + KeyBind.bDisabled = false; + +#define FILL_MODIFIER_DATA(KeyInfoProperty, BindProperty, BindIgnoreProperty)\ + if (KeyInfo.KeyInfoProperty == ECheckBoxState::Undetermined)\ + {\ + KeyBind.BindProperty = KeyBind.BindIgnoreProperty = false;\ + }\ + else\ + {\ + KeyBind.BindProperty = (KeyInfo.KeyInfoProperty == ECheckBoxState::Checked);\ + KeyBind.BindIgnoreProperty = !KeyBind.BindProperty;\ + } + + FILL_MODIFIER_DATA(Shift, Shift, bIgnoreShift); + FILL_MODIFIER_DATA(Ctrl, Control, bIgnoreCtrl); + FILL_MODIFIER_DATA(Alt, Alt, bIgnoreAlt); + FILL_MODIFIER_DATA(Cmd, Cmd, bIgnoreCmd); + +#undef FILL_MODIFIER_DATA + + return KeyBind; + } + + bool IsBindable(const FKey& Key) + { +#if ENGINE_COMPATIBILITY_LEGACY_KEY_AXIS_API + return Key.IsValid() && Key != EKeys::AnyKey && !Key.IsFloatAxis() && !Key.IsVectorAxis() + && !Key.IsGamepadKey() && !Key.IsModifierKey() && !Key.IsMouseButton(); +#else + return Key.IsValid() && Key != EKeys::AnyKey && !Key.IsAxis1D() && !Key.IsAxis2D() + && !Key.IsAxis3D() && !Key.IsGamepadKey() && !Key.IsModifierKey() && !Key.IsMouseButton(); +#endif + } + + void UpdatePlayerInput(UPlayerInput* PlayerInput, const FKeyBind& KeyBind) + { + const int32 Index = PlayerInput->DebugExecBindings.IndexOfByPredicate([&](const FKeyBind& PlayerKeyBind) + { + return PlayerKeyBind.Command.Equals(KeyBind.Command, ESearchCase::IgnoreCase); + }); + + if (IsBindable(KeyBind.Key)) + { + if (Index != INDEX_NONE) + { + PlayerInput->DebugExecBindings[Index] = KeyBind; + } + else + { + PlayerInput->DebugExecBindings.Add(KeyBind); + } + } + else + { + if (Index != INDEX_NONE) + { + PlayerInput->DebugExecBindings.RemoveAt(Index); + } + } + } +} + +namespace DebugExecBindings +{ + void UpdatePlayerInputs(const FImGuiKeyInfo& KeyInfo, const FString& Command) + { + checkf(!Command.IsEmpty(), TEXT("Empty command.")); + + const FKeyBind KeyBind = CreateKeyBind(KeyInfo, Command); + + // Update default player input, so changes will be visible in all PIE sessions created after this point. + if (UPlayerInput* DefaultPlayerInput = GetMutableDefault()) + { + UpdatePlayerInput(DefaultPlayerInput, KeyBind); + } + + // Update all existing player inputs to see changes in running PIE session. + for (TObjectIterator It; It; ++It) + { + UpdatePlayerInput(*It, KeyBind); + } + } +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/Utilities/DebugExecBindings.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +class FString; +struct FImGuiKeyInfo; + +namespace DebugExecBindings +{ + void UpdatePlayerInputs(const FImGuiKeyInfo& KeyInfo, const FString& Command); +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/Utilities/Range.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include + + +namespace Utilities +{ + //==================================================================================================== + // Range + //==================================================================================================== + + template + class TRange + { + public: + + TRange() {} + + TRange(const T& RangeBegin, const T& RangeEnd) { SetRange(RangeBegin, RangeEnd); } + + const T& GetBegin() const { return Begin; } + const T& GetEnd() const { return End; } + + bool IsEmpty() const { return Begin == End; } + + void SetEmpty() { Begin = End = T(); } + + void SetRange(const T& RangeBegin, const T& RangeEnd) + { + checkf(RangeBegin <= RangeEnd, TEXT("Invalid arguments: RangeBegin > RangeEnd.")); + Begin = RangeBegin; + End = RangeEnd; + } + + void AddPosition(const T& Position) + { + AddRangeUnchecked(Position, Position + 1); + } + + void AddRange(const T& RangeBegin, const T& RangeEnd) + { + checkf(RangeBegin <= RangeEnd, TEXT("Invalid arguments: RangeBegin > RangeEnd.")); + AddRangeUnchecked(RangeBegin, RangeEnd); + } + + private: + + void AddRangeUnchecked(const T& RangeBegin, const T& RangeEnd) + { + if (IsEmpty()) + { + Begin = RangeBegin; + End = RangeEnd; + } + else + { + if (Begin > RangeBegin) + { + Begin = RangeBegin; + } + + if (End < RangeEnd) + { + End = RangeEnd; + } + } + } + + T Begin = T(); + T End = T(); + }; + + + // Enable range-based loops + + template + const T& begin(const TRange& Range) + { + return Range.GetBegin(); + } + + template + const T& end(const TRange& Range) + { + return Range.GetEnd(); + } + + + //==================================================================================================== + // Bounded Range + //==================================================================================================== + + template + class TBoundedRange + { + public: + + constexpr const T& GetLowerBound() const { return BeginBound; } + constexpr const T& GetUpperBound() const { return EndBound; } + + const T& GetBegin() const { return Begin; } + const T& GetEnd() const { return End; } + + bool IsEmpty() const { return Begin == End; } + + void SetEmpty() { Begin = End = BeginBound; } + + void SetFull() + { + Begin = BeginBound; + End = EndBound; + } + + void AddPosition(const T& Position) + { + checkf(Position >= BeginBound && Position < EndBound, TEXT("Position out of range.")); + + AddRangeUnchecked(Position, Position + 1); + } + + void AddRange(const T& RangeBegin, const T& RangeEnd) + { + checkf(RangeBegin <= RangeEnd, TEXT("Invalid arguments: RangeBegin > MaxPosition.")); + checkf(RangeBegin >= BeginBound, TEXT("RangeBegin out of range.")); + checkf(RangeBegin <= EndBound, TEXT("RangeEnd out of range.")); + + AddRangeUnchecked(RangeBegin, RangeEnd); + } + + private: + + void AddRangeUnchecked(const T& RangeBegin, const T& RangeEnd) + { + if (IsEmpty()) + { + Begin = RangeBegin; + End = RangeEnd; + } + else + { + if (Begin > RangeBegin) + { + Begin = RangeBegin; + } + + if (End < RangeEnd) + { + End = RangeEnd; + } + } + } + + T Begin = EndBound; + T End = BeginBound; + }; + + + // Enable range-based loops + + template + const T& begin(const TBoundedRange& Range) + { + return Range.GetBegin(); + } + + template + const T& end(const TBoundedRange& Range) + { + return Range.GetEnd(); + } +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/Utilities/RedirectingHandle.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include +#include + + +namespace Utilities +{ + // Handle initialized as a pointer to a default value, but can be redirected to follow other handles. + // When detached, it will revert to the default value. Intended for cross-module redirections. + template + struct TRedirectingHandle + { + TRedirectingHandle(T& InDefaultValue) + : Handle(&InDefaultValue) + , DefaultHandle(&InDefaultValue) + { + } + + ~TRedirectingHandle() + { + // Broadcast null pointer as a request to detach. + OnRedirectionUpdate.Broadcast(nullptr); + } + + // Check whether this handle points to the default value. + bool IsDefault() const + { + return (Handle == DefaultHandle); + } + + // Get the current value. + T& Get() const + { + return *Handle; + } + + // Set the other handle as a parent to this one. Passing null or itself will result with detaching from + // the current parent (if any) and reverting back to the default value. + void SetParent(TRedirectingHandle* InParent) + { + if (InParent != Parent) + { + if (Parent) + { + Parent->OnRedirectionUpdate.RemoveAll(this); + } + + // Protecting from setting itself as a parent. + Parent = (InParent != this) ? InParent : nullptr; + + if (Parent) + { + Parent->OnRedirectionUpdate.AddRaw(this, &TRedirectingHandle::UpdateRedirection); + } + + SetHandle((Parent) ? Parent->Handle : DefaultHandle); + } + } + + protected: + + void UpdateRedirection(T* InHandle) + { + if (InHandle) + { + SetHandle(InHandle); + } + else + { + // Interpreting null as a signal to detach. + SetParent(nullptr); + } + } + + void SetHandle(T* InHandle) + { + if (InHandle != Handle) + { + Handle = InHandle; + OnRedirectionUpdate.Broadcast(Handle); + } + } + + T* Handle; + T* DefaultHandle; + + TRedirectingHandle* Parent = nullptr; + + // Event with a new handle value or null pointer as a signal to detach. + DECLARE_MULTICAST_DELEGATE_OneParam(FRedirectionUpdateDelegate, T*); + FRedirectionUpdateDelegate OnRedirectionUpdate; + }; +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/Utilities/WorldContext.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "WorldContext.h" + + +namespace Utilities +{ + const FWorldContext* GetWorldContextFromNetMode(ENetMode NetMode) + { + checkf(GEngine, TEXT("GEngine required to get list of worlds.")); + + for (const FWorldContext& WorldContext : GEngine->GetWorldContexts()) + { + if (WorldContext.World() && WorldContext.World()->GetNetMode() == NetMode) + { + return &WorldContext; + } + } + + return nullptr; + } +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/Utilities/WorldContext.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include +#include +#include +#include + + +// Utilities helping to get a World Context. + +namespace Utilities +{ + template + FORCEINLINE const FWorldContext* GetWorldContext(const T* Obj); + + FORCEINLINE const FWorldContext* GetWorldContext(const UGameInstance& GameInstance) + { + return GameInstance.GetWorldContext(); + } + + template + FORCEINLINE const FWorldContext* GetWorldContext(const TWeakObjectPtr& Obj) + { + return Obj.IsValid() ? GetWorldContext(*Obj.Get()) : nullptr; + } + + FORCEINLINE const FWorldContext* GetWorldContext(const UGameViewportClient& GameViewportClient) + { + return GetWorldContext(GameViewportClient.GetGameInstance()); + } + + FORCEINLINE const FWorldContext* GetWorldContext(const UWorld& World) + { + return GetWorldContext(World.GetGameInstance()); + } + + template + FORCEINLINE const FWorldContext* GetWorldContext(const T* Obj) + { + return Obj ? GetWorldContext(*Obj) : nullptr; + } + + const FWorldContext* GetWorldContextFromNetMode(ENetMode NetMode); +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/Utilities/WorldContextIndex.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "Utilities/WorldContext.h" + + +// Utilities mapping worlds to indices that we use to identify ImGui contexts. + +namespace Utilities +{ + // Invalid context index for parameters that cannot be resolved to a valid world. + static constexpr int32 INVALID_CONTEXT_INDEX = -10; + + // Standalone context index. + static constexpr int32 STANDALONE_GAME_CONTEXT_INDEX = -2; + +#if WITH_EDITOR + + // Editor context index. We are lacking flexibility here, so we might need to change it somehow. + static constexpr int32 EDITOR_CONTEXT_INDEX = -1; + + FORCEINLINE int32 GetWorldContextIndex(const FWorldContext& WorldContext) + { + switch (WorldContext.WorldType) + { + case EWorldType::PIE: + return WorldContext.PIEInstance; + case EWorldType::Game: + return STANDALONE_GAME_CONTEXT_INDEX; + case EWorldType::Editor: + return EDITOR_CONTEXT_INDEX; + default: + return INVALID_CONTEXT_INDEX; + } + } + + template + FORCEINLINE int32 GetWorldContextIndex(const T& Obj) + { + const FWorldContext* WorldContext = GetWorldContext(Obj); + return WorldContext ? GetWorldContextIndex(*WorldContext) : INVALID_CONTEXT_INDEX; + } + + FORCEINLINE int32 GetWorldContextIndex(const UWorld& World) + { + return (World.WorldType == EWorldType::Editor) ? EDITOR_CONTEXT_INDEX : GetWorldContextIndex(World.GetGameInstance()); + } + + FORCEINLINE int32 GetWorldContextIndex(const UWorld* World) + { + return World ? GetWorldContextIndex(*World) : INVALID_CONTEXT_INDEX; + } + +#else + + template + constexpr int32 GetWorldContextIndex(const T&) + { + // The only option is standalone game with one context. + return STANDALONE_GAME_CONTEXT_INDEX; + } + +#endif // #if WITH_EDITOR +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/VersionCompatibility.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include + +#define BELOW_ENGINE_VERSION(Major, Minor) (ENGINE_MAJOR_VERSION < (Major) || (ENGINE_MAJOR_VERSION == (Major) && ENGINE_MINOR_VERSION < (Minor))) +#define FROM_ENGINE_VERSION(Major, Minor) !BELOW_ENGINE_VERSION(Major, Minor) + + +// One place to define compatibility with older engine versions. + + +// Starting from version 4.17, Slate has an improved clipping API. Old version required to specify per-vertex clipping +// rectangle and unofficial GSlateScissorRect to correctly clip custom vertices made with FSlateDrawElement. +#define ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API BELOW_ENGINE_VERSION(4, 17) + +// Starting from version 4.18, FPaths::GameSavedDir() has been superseded by FPaths::ProjectSavedDir(). +#define ENGINE_COMPATIBILITY_LEGACY_SAVED_DIR BELOW_ENGINE_VERSION(4, 18) + +// Starting from version 4.18, we have support for dual key bindings. +#define ENGINE_COMPATIBILITY_SINGLE_KEY_BINDING BELOW_ENGINE_VERSION(4, 18) + +// Starting from version 4.18, FStringClassReference is replaced by FSoftClassPath. The new header contains a typedef +// that renames FStringClassReference to FSoftClassPath, so it is still possible tu use the old type name in code. +// The old header forwards to the new one but if used it outputs a warning, so we want to avoid it. +#define ENGINE_COMPATIBILITY_LEGACY_STRING_CLASS_REF BELOW_ENGINE_VERSION(4, 18) + +// Starting from version 4.18, engine has a world post actor tick event which if available, provides a good opportunity +// to call debug delegates after world actors are already updated. +#define ENGINE_COMPATIBILITY_WITH_WORLD_POST_ACTOR_TICK FROM_ENGINE_VERSION(4, 18) + +// Starting from version 4.24, world actor tick event has additional world parameter. +#define ENGINE_COMPATIBILITY_LEGACY_WORLD_ACTOR_TICK BELOW_ENGINE_VERSION(4, 24) + +// Starting from version 4.26, FKey::IsFloatAxis and FKey::IsVectorAxis are deprecated and replaced with FKey::IsAxis[1|2|3]D methods. +#define ENGINE_COMPATIBILITY_LEGACY_KEY_AXIS_API BELOW_ENGINE_VERSION(4, 26) + +#define ENGINE_COMPATIBILITY_LEGACY_VECTOR2F BELOW_ENGINE_VERSION(5, 0) + +``` + +`Plugins/ImGui/Source/ImGui/Private/Widgets/SImGuiCanvasControl.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "SImGuiCanvasControl.h" + +#include "VersionCompatibility.h" + +#include +#include + + +namespace +{ + // Mouse wheel to zoom ratio - how fast zoom changes with mouse wheel delta. + const float ZoomScrollSpeed = 0.03125f; + + // Speed of blending out - how much zoom scale and canvas offset fades in every frame. + const float BlendOutSpeed = 0.25f; + + // TODO: Move to settings + namespace Colors + { + const FLinearColor CanvasMargin = FColor(0, 4, 32, 64); + const FLinearColor CanvasBorder = FColor::Black.WithAlpha(127); + const FLinearColor CanvasBorderHighlight = FColor(16, 24, 64, 160); + const FLinearColor FrameBorder = FColor(222, 163, 9, 128); + const FLinearColor FrameBorderHighlight = FColor(255, 180, 10, 160); + } + + // Defines type of drag operation. + enum class EDragType + { + Content, + Canvas + }; + + // Data for drag & drop operations. Calculations are made in widget where we have more straightforward access to data + // like geometry or scale. + class FImGuiDragDropOperation : public FDragDropOperation + { + public: + + DRAG_DROP_OPERATOR_TYPE(FImGuiDragDropOperation, FDragDropOperation) + + FImGuiDragDropOperation(const FVector2D& InPosition, const FVector2D& InOffset, EDragType InDragType) + : StartPosition(InPosition) + , StartOffset(InOffset) + , DragType(InDragType) + { + bCreateNewWindow = false; + MouseCursor = EMouseCursor::GrabHandClosed; + } + + FVector2D StartPosition; + FVector2D StartOffset; + EDragType DragType; + }; +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void SImGuiCanvasControl::Construct(const FArguments& InArgs) +{ + OnTransformChanged = InArgs._OnTransformChanged; + UpdateVisibility(); +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +void SImGuiCanvasControl::SetActive(bool bInActive) +{ + if (bActive != bInActive) + { + bActive = bInActive; + bBlendingOut = !bInActive; + if (bInActive) + { + Opacity = 1.f; + } + UpdateVisibility(); + } +} + +void SImGuiCanvasControl::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) +{ + Super::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); + + if (bBlendingOut) + { + if (FMath::IsNearlyEqual(CanvasScale, 1.f, ZoomScrollSpeed) && CanvasOffset.IsNearlyZero(1.f)) + { + CanvasOffset = FVector2D::ZeroVector; + CanvasScale = 1.f; + Opacity = 0.f; + bBlendingOut = false; + UpdateVisibility(); + } + else + { + CanvasOffset = FMath::Lerp(CanvasOffset, FVector2D::ZeroVector, BlendOutSpeed); + CanvasScale = FMath::Lerp(CanvasScale, 1.f, BlendOutSpeed); + Opacity = FMath::Lerp(Opacity, 0.f, BlendOutSpeed); + } + + UpdateRenderTransform(); + } +} + +FReply SImGuiCanvasControl::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + if (DragRequest == EDragRequest::None) + { + if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton) + { + DragRequest = EDragRequest::Content; + return FReply::Handled().DetectDrag(SharedThis(this), EKeys::RightMouseButton).CaptureMouse(SharedThis(this)); + } + else if (MouseEvent.GetEffectingButton() == EKeys::MiddleMouseButton) + { + DragRequest = EDragRequest::Canvas; + return FReply::Handled().DetectDrag(SharedThis(this), EKeys::MiddleMouseButton).CaptureMouse(SharedThis(this)); + } + } + + return FReply::Unhandled(); +} + +FReply SImGuiCanvasControl::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton) + { + if (DragRequest == EDragRequest::Content) + { + DragRequest = EDragRequest::None; + } + } + else if (MouseEvent.GetEffectingButton() == EKeys::MiddleMouseButton) + { + if (DragRequest == EDragRequest::Canvas) + { + DragRequest = EDragRequest::None; + } + } + + return FReply::Unhandled(); +} + +FReply SImGuiCanvasControl::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + Zoom(MyGeometry, MouseEvent.GetWheelDelta() * ZoomScrollSpeed, MouseEvent.GetScreenSpacePosition()); + return FReply::Unhandled(); +} + +FReply SImGuiCanvasControl::OnDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + if (DragRequest == EDragRequest::Content) + { + return FReply::Handled() + .BeginDragDrop(MakeShareable(new FImGuiDragDropOperation( + MouseEvent.GetScreenSpacePosition(), ContentOffset, EDragType::Content))) + .LockMouseToWidget(SharedThis(this)); + } + else if (DragRequest == EDragRequest::Canvas) + { + return FReply::Handled() + .BeginDragDrop(MakeShareable(new FImGuiDragDropOperation( + MouseEvent.GetScreenSpacePosition(), CanvasOffset, EDragType::Canvas))) + .LockMouseToWidget(SharedThis(this)); + } + else + { + return FReply::Unhandled(); + } +} + +FReply SImGuiCanvasControl::OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) +{ + auto Operation = DragDropEvent.GetOperationAs(); + if (Operation.IsValid()) + { + const FSlateRenderTransform ScreenToWidget = MyGeometry.GetAccumulatedRenderTransform().Inverse(); + const FVector2D DragDelta = ScreenToWidget.TransformVector(DragDropEvent.GetScreenSpacePosition() - Operation->StartPosition); + + if (Operation->DragType == EDragType::Content) + { + // Content offset is in ImGui space, so we need to scale drag calculated in widget space. + ContentOffset = Operation->StartOffset + DragDelta / CanvasScale; + } + else + { + // Canvas offset is in widget space, so we can apply drag calculated in widget space directly. + CanvasOffset = Operation->StartOffset + DragDelta; + } + + UpdateRenderTransform(); + + return FReply::Handled(); + } + else + { + return FReply::Unhandled(); + } +} + +FReply SImGuiCanvasControl::SImGuiCanvasControl::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) +{ + DragRequest = EDragRequest::None; + return FReply::Handled().ReleaseMouseLock(); +} + +FVector2D SImGuiCanvasControl::ComputeDesiredSize(float InScale) const +{ + return FVector2D{ 3840.f, 2160.f } * InScale; +} + +namespace +{ + FORCEINLINE FMargin CalculateInset(const FSlateRect& From, const FSlateRect& To) + { + return { To.Left - From.Left, To.Top - From.Top, From.Right - To.Right, From.Bottom - To.Bottom }; + } + + FORCEINLINE FLinearColor ScaleAlpha(FLinearColor Color, float Scale) + { + Color.A *= Scale; + return Color; + } + + FORCEINLINE FVector2D Round(const FVector2D& Vec) + { + return FVector2D{ FMath::FloorToFloat(Vec.X), FMath::FloorToFloat(Vec.Y) }; + } +} + +int32 SImGuiCanvasControl::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const +{ + const FPaintGeometry PaintGeometry = AllottedGeometry.ToPaintGeometry(); + + const FSlateRenderTransform& WidgetToScreen = AllottedGeometry.GetAccumulatedRenderTransform(); + const FSlateRenderTransform ImGuiToScreen = Transform.Concatenate(WidgetToScreen); + + const FSlateRect CanvasRect = FSlateRect( + ImGuiToScreen.TransformPoint(FVector2D::ZeroVector), + ImGuiToScreen.TransformPoint(ComputeDesiredSize(1.f))); + const FMargin CanvasMargin = CalculateInset(MyCullingRect, CanvasRect); + + if (CanvasMargin.GetDesiredSize().SizeSquared() > 0.f) + { + CanvasBorderBrush.Margin = CanvasMargin; + + const FLinearColor CanvasMarginColor = ScaleAlpha(Colors::CanvasMargin, Opacity); + const FLinearColor CanvasBorderColor = ScaleAlpha(DragRequest == EDragRequest::Content + ? Colors::CanvasBorderHighlight : Colors::CanvasBorder, Opacity); + +#if ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + FSlateDrawElement::MakeBox(OutDrawElements, LayerId, PaintGeometry, &CanvasBorderBrush, MyCullingRect, + ESlateDrawEffect::None, CanvasMarginColor); + + FSlateDrawElement::MakeBox(OutDrawElements, LayerId, PaintGeometry, &CanvasBorderBrush, + CanvasRect.ExtendBy(1).IntersectionWith(MyCullingRect), ESlateDrawEffect::None, CanvasBorderColor); + +#else + + FSlateDrawElement::MakeBox(OutDrawElements, LayerId, PaintGeometry, &CanvasBorderBrush, ESlateDrawEffect::None, + CanvasMarginColor); + + OutDrawElements.PushClip(FSlateClippingZone{ CanvasRect.ExtendBy(1) }); + FSlateDrawElement::MakeBox(OutDrawElements, LayerId, PaintGeometry, &CanvasBorderBrush, ESlateDrawEffect::None, + CanvasBorderColor); + OutDrawElements.PopClip(); +#endif + } + + const FSlateRect FrameRect = FSlateRect::FromPointAndExtent( + WidgetToScreen.TransformPoint(Round(CanvasOffset)), + Round(MyCullingRect.GetSize() * CanvasScale)); + const FMargin FrameMargin = CalculateInset(MyCullingRect, FrameRect); + + if (FrameMargin.GetDesiredSize().SizeSquared() > 0.f) + { + FrameBorderBrush.Margin = FrameMargin; + + const FLinearColor FrameBorderColor = ScaleAlpha(DragRequest == EDragRequest::Canvas + ? Colors::FrameBorderHighlight : Colors::FrameBorder, Opacity); + +#if ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + FSlateDrawElement::MakeBox(OutDrawElements, LayerId, PaintGeometry, &FrameBorderBrush, + FrameRect.ExtendBy(1).IntersectionWith(MyCullingRect), ESlateDrawEffect::None, FrameBorderColor); +#else + OutDrawElements.PushClip(FSlateClippingZone{ FrameRect.ExtendBy(1) }); + FSlateDrawElement::MakeBox(OutDrawElements, LayerId, PaintGeometry, &FrameBorderBrush, ESlateDrawEffect::None, + FrameBorderColor); + OutDrawElements.PopClip(); +#endif + } + + return LayerId; +} + +void SImGuiCanvasControl::UpdateVisibility() +{ + SetVisibility(bActive ? EVisibility::Visible : bBlendingOut ? EVisibility::HitTestInvisible : EVisibility::Hidden); +} + +void SImGuiCanvasControl::Zoom(const FGeometry& MyGeometry, const float Delta, const FVector2D& MousePosition) +{ + // If blending out, then cancel. + bBlendingOut = false; + + float OldCanvasScale = CanvasScale; + + // Normalize scale to make sure that it changes in fixed steps and that we don't accumulate rounding errors. + // Normalizing before applying delta allows for scales that at the edges are not rounded to the closes step. + CanvasScale = FMath::RoundToFloat(CanvasScale / ZoomScrollSpeed) * ZoomScrollSpeed; + + // Update the scale. + CanvasScale = FMath::Clamp(CanvasScale + Delta, GetMinScale(MyGeometry), 2.f); + + // Update canvas offset to keep it fixed around pivot point. + if (CanvasScale != OldCanvasScale && OldCanvasScale != 0.f) + { + // Pivot points (in screen space): + // 1) Around mouse: MousePosition + // 2) Fixed in top-left corner: MyGeometry.GetLayoutBoundingRect().GetTopLeft() + // 3) Fixed in centre: MyGeometry.GetLayoutBoundingRect().GetCenter() + const FVector2D PivotPoint = MyGeometry.GetAccumulatedRenderTransform().Inverse().TransformPoint(MousePosition); + const FVector2D Pivot = PivotPoint - CanvasOffset; + + CanvasOffset += Pivot * (OldCanvasScale - CanvasScale) / OldCanvasScale; + } + + UpdateRenderTransform(); +} + +void SImGuiCanvasControl::UpdateRenderTransform() +{ + const FVector2D RenderOffset = Round(ContentOffset * CanvasScale + CanvasOffset); + Transform = FSlateRenderTransform(CanvasScale, RenderOffset); + OnTransformChanged.ExecuteIfBound(Transform); +} + +float SImGuiCanvasControl::GetMinScale(const FGeometry& MyGeometry) +{ +#if FROM_ENGINE_VERSION(4, 17) +#define GET_BOUNDING_RECT GetLayoutBoundingRect +#else +#define GET_BOUNDING_RECT GetClippingRect +#endif + + const FVector2D DefaultCanvasSize = MyGeometry.GetAccumulatedRenderTransform().TransformVector(ComputeDesiredSize(1.f)); + const FVector2D WidgetSize = MyGeometry.GET_BOUNDING_RECT().GetSize(); + return FMath::Min(WidgetSize.X / DefaultCanvasSize.X, WidgetSize.Y / DefaultCanvasSize.Y); + +#undef GET_BOUNDING_RECT +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/Widgets/SImGuiCanvasControl.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "ImGuiInputState.h" +#include "ImGuiModuleSettings.h" + +#include +#include +#include + + +// Widget that controls transform of ImGui canvas/space. +// When active, additionally it shows boundaries of ImGui canvas and default visible area. +// TODO: Bind to ImGui context or properties to dynamically read canvas size. +// TODO: Bind to properties to allow configure colors. +class SImGuiCanvasControl : public SLeafWidget +{ + typedef SLeafWidget Super; + +public: + + DECLARE_DELEGATE_OneParam(FOnTransformChanged, const FSlateRenderTransform&); + + SLATE_BEGIN_ARGS(SImGuiCanvasControl) + {} + SLATE_EVENT(FOnTransformChanged, OnTransformChanged) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + + bool IsActive() const { return bActive; } + void SetActive(bool bInActive); + + const FSlateRenderTransform& GetTransform() const { return Transform; } + + //---------------------------------------------------------------------------------------------------- + // SWidget overrides + //---------------------------------------------------------------------------------------------------- + + virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override; + + virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + virtual FReply OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + virtual FReply OnDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + virtual FReply OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override; + + virtual FReply OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override; + + virtual FVector2D ComputeDesiredSize(float InScale) const override; + + virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; + +private: + + enum class EDragRequest : uint8 + { + None, + Content, + Canvas + }; + + void UpdateVisibility(); + + void Zoom(const FGeometry& MyGeometry, const float Delta, const FVector2D& MousePosition); + + void UpdateRenderTransform(); + + float GetMinScale(const FGeometry& MyGeometry); + + mutable FSlateBorderBrush CanvasBorderBrush = FSlateBorderBrush("SImGuiCanvasControl-CanvasBorder", FMargin(0.f, 0.f, 1.f, 1.f), FLinearColor::White); + mutable FSlateBorderBrush FrameBorderBrush = FSlateBorderBrush("SImGuiCanvasControl-FrameBorder", FMargin(0.f, 0.f, 1.f, 1.f), FLinearColor::White); + + FOnTransformChanged OnTransformChanged; + + // Transform from ImGui space. + FSlateRenderTransform Transform; + + // Offset of the ImGui content in ImGui space. + FVector2D ContentOffset = FVector2D::ZeroVector; + + // Offset of the ImGui canvas in widget local space. + FVector2D CanvasOffset = FVector2D::ZeroVector; + + // Scale of the ImGui canvas in widget local space. + float CanvasScale = 1.f; + + // Opacity scaling visibility of elements during blending. + float Opacity = 1.f; + + // Whether this widget is active. While active, widget allows to modify transform of ImGui canvas, shows its + // boundaries and default visible area. + bool bActive = false; + + // Whether we are blending out after widget was deactivated. While blending out, widget is visible but it doesn't + // process inputs anymore. + bool bBlendingOut = false; + + // Request is set on mouse button press before drag operation is started. It remains valid until activating button + // is released or until drag operation is finished or until it is replaced by alternative request. + // Highlights are bound to requests, what means that they can also be activated before drag operation is started. + EDragRequest DragRequest = EDragRequest::None; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Private/Widgets/SImGuiLayout.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "SImGuiLayout.h" +#include "SImGuiWidget.h" + +#include "ImGuiModuleManager.h" +#include "ImGuiModuleSettings.h" + +#include +#include +#include +#include + + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void SImGuiLayout::Construct(const FArguments& InArgs) +{ + checkf(InArgs._GameViewport, TEXT("Null Game Viewport argument")); + + ModuleManager = InArgs._ModuleManager; + GameViewport = InArgs._GameViewport; + + if (ModuleManager) + { + auto& Settings = ModuleManager->GetSettings(); + SetDPIScale(Settings.GetDPIScaleInfo()); + if (!Settings.OnDPIScaleChangedDelegate.IsBoundToObject(this)) + { + Settings.OnDPIScaleChangedDelegate.AddRaw(this, &SImGuiLayout::SetDPIScale); + } + } + + ChildSlot + [ + // Remove accumulated scale to manually control how we draw data. + SNew(SScaleBox) + .IgnoreInheritedScale(true) + .HAlign(HAlign_Fill) + .VAlign(VAlign_Fill) + .Visibility(EVisibility::SelfHitTestInvisible) + [ + // Apply custom scale if needed. + SNew(SDPIScaler) + .DPIScale(TAttribute(this, &SImGuiLayout::GetDPIScale)) + .Visibility(EVisibility::SelfHitTestInvisible) + [ + SNew(SConstraintCanvas) + + SConstraintCanvas::Slot() + .Anchors(FAnchors(0.f, 0.f, 1.f, 1.f)) + .AutoSize(true) + .Offset(FMargin(1.f, 1.f, 0.f, 1.f)) + .Alignment(FVector2D::ZeroVector) + [ + SNew(SImGuiWidget) + .ModuleManager(InArgs._ModuleManager) + .GameViewport(InArgs._GameViewport) + .ContextIndex(InArgs._ContextIndex) +#if !ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + // To correctly clip borders. Using SScissorRectBox in older versions seems to be not necessary. + .Clipping(EWidgetClipping::ClipToBounds) +#endif + ] + ] + ] + ]; + + SetVisibility(EVisibility::SelfHitTestInvisible); +} + +SImGuiLayout::~SImGuiLayout() +{ + if (ModuleManager) + { + ModuleManager->GetSettings().OnDPIScaleChangedDelegate.RemoveAll(this); + } +} + +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +void SImGuiLayout::SetDPIScale(const FImGuiDPIScaleInfo& ScaleInfo) +{ + DPIScale = ScaleInfo.GetSlateScale(); +} + +``` + +`Plugins/ImGui/Source/ImGui/Private/Widgets/SImGuiLayout.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include +#include +#include + + +class FImGuiModuleManager; +class UGameViewportClient; +struct FImGuiDPIScaleInfo; + +// Layout preset for ImGui Widget. +class SImGuiLayout : public SCompoundWidget +{ + typedef SCompoundWidget Super; + +public: + + SLATE_BEGIN_ARGS(SImGuiLayout) + {} + SLATE_ARGUMENT(FImGuiModuleManager*, ModuleManager) + SLATE_ARGUMENT(UGameViewportClient*, GameViewport) + SLATE_ARGUMENT(int32, ContextIndex) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + + ~SImGuiLayout(); + + const TWeakObjectPtr& GetGameViewport() const { return GameViewport; } + +private: + + float GetDPIScale() const { return DPIScale; } + void SetDPIScale(const FImGuiDPIScaleInfo& ScaleInfo); + + FImGuiModuleManager* ModuleManager = nullptr; + TWeakObjectPtr GameViewport; + + float DPIScale = 1.f; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Private/Widgets/SImGuiWidget.cpp`: + +```cpp +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "SImGuiWidget.h" +#include "SImGuiCanvasControl.h" + +#include "ImGuiContextManager.h" +#include "ImGuiContextProxy.h" +#include "ImGuiInputHandler.h" +#include "ImGuiInputHandlerFactory.h" +#include "ImGuiInteroperability.h" +#include "ImGuiModuleManager.h" +#include "ImGuiModuleSettings.h" +#include "TextureManager.h" +#include "Utilities/Arrays.h" +#include "VersionCompatibility.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + + +#if IMGUI_WIDGET_DEBUG + +DEFINE_LOG_CATEGORY_STATIC(LogImGuiWidget, Warning, All); + +#define IMGUI_WIDGET_LOG(Verbosity, Format, ...) UE_LOG(LogImGuiWidget, Verbosity, Format, __VA_ARGS__) + +#define TEXT_INPUT_MODE(Val) (\ + (Val) == EInputMode::Full ? TEXT("Full") :\ + (Val) == EInputMode::MousePointerOnly ? TEXT("MousePointerOnly") :\ + TEXT("None")) + +#define TEXT_BOOL(Val) ((Val) ? TEXT("true") : TEXT("false")) + +#else + +#define IMGUI_WIDGET_LOG(...) + +#endif // IMGUI_WIDGET_DEBUG + +#if IMGUI_WIDGET_DEBUG +namespace CVars +{ + TAutoConsoleVariable DebugWidget(TEXT("ImGui.Debug.Widget"), 0, + TEXT("Show debug for SImGuiWidget.\n") + TEXT("0: disabled (default)\n") + TEXT("1: enabled"), + ECVF_Default); + + TAutoConsoleVariable DebugInput(TEXT("ImGui.Debug.Input"), 0, + TEXT("Show debug for input state.\n") + TEXT("0: disabled (default)\n") + TEXT("1: enabled"), + ECVF_Default); +} +#endif // IMGUI_WIDGET_DEBUG + +namespace +{ + FORCEINLINE FVector2D MaxVector(const FVector2D& A, const FVector2D& B) + { + return FVector2D(FMath::Max(A.X, B.X), FMath::Max(A.Y, B.Y)); + } + + FORCEINLINE FVector2D RoundVector(const FVector2D& Vector) + { + return FVector2D(FMath::RoundToFloat(Vector.X), FMath::RoundToFloat(Vector.Y)); + } + + FORCEINLINE FSlateRenderTransform RoundTranslation(const FSlateRenderTransform& Transform) + { + return FSlateRenderTransform(Transform.GetMatrix(), RoundVector(Transform.GetTranslation())); + } +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void SImGuiWidget::Construct(const FArguments& InArgs) +{ + checkf(InArgs._ModuleManager, TEXT("Null Module Manager argument")); + checkf(InArgs._GameViewport, TEXT("Null Game Viewport argument")); + + ModuleManager = InArgs._ModuleManager; + GameViewport = InArgs._GameViewport; + ContextIndex = InArgs._ContextIndex; + + // Register to get post-update notifications. + ModuleManager->OnPostImGuiUpdate().AddRaw(this, &SImGuiWidget::OnPostImGuiUpdate); + + // Register debug delegate. + auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); + checkf(ContextProxy, TEXT("Missing context during widget construction: ContextIndex = %d"), ContextIndex); +#if IMGUI_WIDGET_DEBUG + ContextProxy->OnDraw().AddRaw(this, &SImGuiWidget::OnDebugDraw); +#endif // IMGUI_WIDGET_DEBUG + + // Register for settings change. + RegisterImGuiSettingsDelegates(); + + // Get initial settings. + const auto& Settings = ModuleManager->GetSettings(); + SetHideMouseCursor(Settings.UseSoftwareCursor()); + CreateInputHandler(Settings.GetImGuiInputHandlerClass()); + SetDPIScale(Settings.GetDPIScaleInfo()); + SetCanvasSizeInfo(Settings.GetCanvasSizeInfo()); + + // Initialize state. + UpdateVisibility(); + UpdateMouseCursor(); + + ChildSlot + [ + SAssignNew(CanvasControlWidget, SImGuiCanvasControl).OnTransformChanged(this, &SImGuiWidget::SetImGuiTransform) + ]; + + ImGuiTransform = CanvasControlWidget->GetTransform(); +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +SImGuiWidget::~SImGuiWidget() +{ + // Stop listening for settings change. + UnregisterImGuiSettingsDelegates(); + + // Release ImGui Input Handler. + ReleaseInputHandler(); + + // Remove binding between this widget and its context proxy. + if (auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex)) + { +#if IMGUI_WIDGET_DEBUG + ContextProxy->OnDraw().RemoveAll(this); +#endif // IMGUI_WIDGET_DEBUG + } + + // Unregister from post-update notifications. + ModuleManager->OnPostImGuiUpdate().RemoveAll(this); +} + +void SImGuiWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) +{ + Super::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); + + UpdateInputState(); + UpdateTransparentMouseInput(AllottedGeometry); + HandleWindowFocusLost(); + UpdateCanvasSize(); +} + +FReply SImGuiWidget::OnKeyChar(const FGeometry& MyGeometry, const FCharacterEvent& CharacterEvent) +{ + return InputHandler->OnKeyChar(CharacterEvent); +} + +FReply SImGuiWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent) +{ + UpdateCanvasControlMode(KeyEvent); + return InputHandler->OnKeyDown(KeyEvent); +} + +FReply SImGuiWidget::OnKeyUp(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent) +{ + UpdateCanvasControlMode(KeyEvent); + return InputHandler->OnKeyUp(KeyEvent); +} + +FReply SImGuiWidget::OnAnalogValueChanged(const FGeometry& MyGeometry, const FAnalogInputEvent& AnalogInputEvent) +{ + return InputHandler->OnAnalogValueChanged(AnalogInputEvent); +} + +FReply SImGuiWidget::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + return InputHandler->OnMouseButtonDown(MouseEvent).LockMouseToWidget(SharedThis(this)); +} + +FReply SImGuiWidget::OnMouseButtonDoubleClick(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + return InputHandler->OnMouseButtonDoubleClick(MouseEvent).LockMouseToWidget(SharedThis(this)); +} + +namespace +{ + bool NeedMouseLock(const FPointerEvent& MouseEvent) + { +#if FROM_ENGINE_VERSION(4, 20) + return FSlateApplication::Get().GetPressedMouseButtons().Num() > 0; +#else + return MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton) || MouseEvent.IsMouseButtonDown(EKeys::MiddleMouseButton) + || MouseEvent.IsMouseButtonDown(EKeys::RightMouseButton); +#endif + } +} + +FReply SImGuiWidget::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + FReply Reply = InputHandler->OnMouseButtonUp(MouseEvent); + if (!NeedMouseLock(MouseEvent)) + { + Reply.ReleaseMouseLock(); + } + return Reply; +} + +FReply SImGuiWidget::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + return InputHandler->OnMouseWheel(MouseEvent); +} + +FReply SImGuiWidget::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + return InputHandler->OnMouseMove(TransformScreenPointToImGui(MyGeometry, MouseEvent.GetScreenSpacePosition()), MouseEvent); +} + +FReply SImGuiWidget::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& FocusEvent) +{ + Super::OnFocusReceived(MyGeometry, FocusEvent); + + IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Focus Received."), ContextIndex); + + bForegroundWindow = GameViewport->Viewport->IsForegroundWindow(); + InputHandler->OnKeyboardInputEnabled(); + InputHandler->OnGamepadInputEnabled(); + + FSlateApplication::Get().ResetToDefaultPointerInputSettings(); + return FReply::Handled(); +} + +void SImGuiWidget::OnFocusLost(const FFocusEvent& FocusEvent) +{ + Super::OnFocusLost(FocusEvent); + + IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Focus Lost."), ContextIndex); + + InputHandler->OnKeyboardInputDisabled(); + InputHandler->OnGamepadInputDisabled(); +} + +void SImGuiWidget::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + Super::OnMouseEnter(MyGeometry, MouseEvent); + + IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Mouse Enter."), ContextIndex); + + InputHandler->OnMouseInputEnabled(); +} + +void SImGuiWidget::OnMouseLeave(const FPointerEvent& MouseEvent) +{ + Super::OnMouseLeave(MouseEvent); + + IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Mouse Leave."), ContextIndex); + + InputHandler->OnMouseInputDisabled(); +} + +FReply SImGuiWidget::OnTouchStarted(const FGeometry& MyGeometry, const FPointerEvent& TouchEvent) +{ + return InputHandler->OnTouchStarted(TransformScreenPointToImGui(MyGeometry, TouchEvent.GetScreenSpacePosition()), TouchEvent); +} + +FReply SImGuiWidget::OnTouchMoved(const FGeometry& MyGeometry, const FPointerEvent& TouchEvent) +{ + return InputHandler->OnTouchMoved(TransformScreenPointToImGui(MyGeometry, TouchEvent.GetScreenSpacePosition()), TouchEvent); +} + +FReply SImGuiWidget::OnTouchEnded(const FGeometry& MyGeometry, const FPointerEvent& TouchEvent) +{ + UpdateVisibility(); + return InputHandler->OnTouchEnded(TransformScreenPointToImGui(MyGeometry, TouchEvent.GetScreenSpacePosition()), TouchEvent); +} + +void SImGuiWidget::CreateInputHandler(const FSoftClassPath& HandlerClassReference) +{ + ReleaseInputHandler(); + + if (!InputHandler.IsValid()) + { + InputHandler = FImGuiInputHandlerFactory::NewHandler(HandlerClassReference, ModuleManager, GameViewport.Get(), ContextIndex); + } +} + +void SImGuiWidget::ReleaseInputHandler() +{ + if (InputHandler.IsValid()) + { + FImGuiInputHandlerFactory::ReleaseHandler(InputHandler.Get()); + InputHandler.Reset(); + } +} + +void SImGuiWidget::RegisterImGuiSettingsDelegates() +{ + auto& Settings = ModuleManager->GetSettings(); + + if (!Settings.OnImGuiInputHandlerClassChanged.IsBoundToObject(this)) + { + Settings.OnImGuiInputHandlerClassChanged.AddRaw(this, &SImGuiWidget::CreateInputHandler); + } + if (!Settings.OnUseSoftwareCursorChanged.IsBoundToObject(this)) + { + Settings.OnUseSoftwareCursorChanged.AddRaw(this, &SImGuiWidget::SetHideMouseCursor); + } + if (!Settings.OnDPIScaleChangedDelegate.IsBoundToObject(this)) + { + Settings.OnDPIScaleChangedDelegate.AddRaw(this, &SImGuiWidget::SetDPIScale); + } + if (!Settings.OnCanvasSizeChangedDelegate.IsBoundToObject(this)) + { + Settings.OnCanvasSizeChangedDelegate.AddRaw(this, &SImGuiWidget::SetCanvasSizeInfo); + } +} + +void SImGuiWidget::UnregisterImGuiSettingsDelegates() +{ + auto& Settings = ModuleManager->GetSettings(); + + Settings.OnImGuiInputHandlerClassChanged.RemoveAll(this); + Settings.OnUseSoftwareCursorChanged.RemoveAll(this); + Settings.OnDPIScaleChangedDelegate.RemoveAll(this); + Settings.OnCanvasSizeChangedDelegate.RemoveAll(this); +} + +void SImGuiWidget::SetHideMouseCursor(bool bHide) +{ + if (bHideMouseCursor != bHide) + { + bHideMouseCursor = bHide; + UpdateMouseCursor(); + } +} + +bool SImGuiWidget::IsConsoleOpened() const +{ + return GameViewport->ViewportConsole && GameViewport->ViewportConsole->ConsoleState != NAME_None; +} + +void SImGuiWidget::UpdateVisibility() +{ + // Make sure that we do not occlude other widgets, if input is disabled or if mouse is set to work in a transparent + // mode (hit-test invisible). + SetVisibility(bInputEnabled && !bTransparentMouseInput ? EVisibility::Visible : EVisibility::HitTestInvisible); + + IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Visibility updated to '%s'."), + ContextIndex, *GetVisibility().ToString()); +} + +void SImGuiWidget::UpdateMouseCursor() +{ + if (!bHideMouseCursor) + { + const FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); + SetCursor(ContextProxy ? ContextProxy->GetMouseCursor() : EMouseCursor::Default); + } + else + { + SetCursor(EMouseCursor::None); + } +} + +ULocalPlayer* SImGuiWidget::GetLocalPlayer() const +{ + if (GameViewport.IsValid()) + { + if (UWorld* World = GameViewport->GetWorld()) + { + if (ULocalPlayer* LocalPlayer = World->GetFirstLocalPlayerFromController()) + { + return World->GetFirstLocalPlayerFromController(); + } + } + } + + return nullptr; +} + +void SImGuiWidget::TakeFocus() +{ + auto& SlateApplication = FSlateApplication::Get(); + + PreviousUserFocusedWidget = SlateApplication.GetUserFocusedWidget(SlateApplication.GetUserIndexForKeyboard()); + + if (ULocalPlayer* LocalPlayer = GetLocalPlayer()) + { + TSharedRef FocusWidget = SharedThis(this); + LocalPlayer->GetSlateOperations().CaptureMouse(FocusWidget); + LocalPlayer->GetSlateOperations().SetUserFocus(FocusWidget); + } + else + { + SlateApplication.SetKeyboardFocus(SharedThis(this)); + } +} + +void SImGuiWidget::ReturnFocus() +{ + if (HasKeyboardFocus()) + { + auto FocusWidgetPtr = PreviousUserFocusedWidget.IsValid() + ? PreviousUserFocusedWidget.Pin() + : GameViewport->GetGameViewportWidget(); + + if (ULocalPlayer* LocalPlayer = GetLocalPlayer()) + { + auto FocusWidgetRef = FocusWidgetPtr.ToSharedRef(); + LocalPlayer->GetSlateOperations().CaptureMouse(FocusWidgetRef); + LocalPlayer->GetSlateOperations().SetUserFocus(FocusWidgetRef); + } + else + { + auto& SlateApplication = FSlateApplication::Get(); + SlateApplication.ResetToDefaultPointerInputSettings(); + SlateApplication.SetUserFocus(SlateApplication.GetUserIndexForKeyboard(), FocusWidgetPtr); + } + } + + PreviousUserFocusedWidget.Reset(); +} + +void SImGuiWidget::UpdateInputState() +{ + auto& Properties = ModuleManager->GetProperties(); + auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); + + const bool bEnableTransparentMouseInput = Properties.IsMouseInputShared() +#if PLATFORM_ANDROID || PLATFORM_IOS + && (FSlateApplication::Get().GetCursorPos() != FVector2D::ZeroVector) +#endif + && !(ContextProxy->WantsMouseCapture() || ContextProxy->HasActiveItem()); + if (bTransparentMouseInput != bEnableTransparentMouseInput) + { + bTransparentMouseInput = bEnableTransparentMouseInput; + if (bInputEnabled) + { + UpdateVisibility(); + } + } + + const bool bPropertiesInputEnabled = Properties.IsInputEnabled(); + if (bInputEnabled != bPropertiesInputEnabled) + { + IMGUI_WIDGET_LOG(Log, TEXT("ImGui Widget %d - Input Enabled changed to '%s'."), + ContextIndex, TEXT_BOOL(bPropertiesInputEnabled)); + + bInputEnabled = bPropertiesInputEnabled; + + UpdateVisibility(); + UpdateMouseCursor(); + + if (bInputEnabled) + { + // We won't get mouse enter, if viewport is already hovered. + if (GameViewport->GetGameViewportWidget()->IsHovered()) + { + InputHandler->OnMouseInputEnabled(); + } + + TakeFocus(); + } + else + { + ReturnFocus(); + } + } + else if(bInputEnabled) + { + const auto& ViewportWidget = GameViewport->GetGameViewportWidget(); + + if (bTransparentMouseInput) + { + // If mouse is in transparent input mode and focus is lost to viewport, let viewport keep it and disable + // the whole input to match that state. + if (GameViewport->GetGameViewportWidget()->HasMouseCapture()) + { + // DON'T DISABLE OUR INPUT WHEN WE LOSE FOCUS + //Properties.SetInputEnabled(false); + //UpdateInputState(); + } + } + else + { + // Widget tends to lose keyboard focus after console is opened. With non-transparent mouse we can fix that + // by manually restoring it. + if (!HasKeyboardFocus() && !IsConsoleOpened() && (ViewportWidget->HasKeyboardFocus() || ViewportWidget->HasFocusedDescendants())) + { + TakeFocus(); + } + } + } +} + +void SImGuiWidget::UpdateTransparentMouseInput(const FGeometry& AllottedGeometry) +{ + if (bInputEnabled && bTransparentMouseInput) + { + if (!GameViewport->GetGameViewportWidget()->HasMouseCapture()) + { + InputHandler->OnMouseMove(TransformScreenPointToImGui(AllottedGeometry, FSlateApplication::Get().GetCursorPos())); + } + } +} + +void SImGuiWidget::HandleWindowFocusLost() +{ + // We can use window foreground status to notify about application losing or receiving focus. In some situations + // we get mouse leave or enter events, but they are only sent if mouse pointer is inside of the viewport. + if (bInputEnabled && HasKeyboardFocus()) + { + if (bForegroundWindow != GameViewport->Viewport->IsForegroundWindow()) + { + bForegroundWindow = !bForegroundWindow; + + IMGUI_WIDGET_LOG(VeryVerbose, TEXT("ImGui Widget %d - Updating input after %s foreground window status."), + ContextIndex, bForegroundWindow ? TEXT("getting") : TEXT("losing")); + + if (bForegroundWindow) + { + InputHandler->OnKeyboardInputEnabled(); + InputHandler->OnGamepadInputEnabled(); + } + else + { + InputHandler->OnKeyboardInputDisabled(); + InputHandler->OnGamepadInputDisabled(); + } + } + } +} + +void SImGuiWidget::SetDPIScale(const FImGuiDPIScaleInfo& ScaleInfo) +{ + const float Scale = ScaleInfo.GetSlateScale(); + if (DPIScale != Scale) + { + DPIScale = Scale; + bUpdateCanvasSize = true; + } +} + +void SImGuiWidget::SetCanvasSizeInfo(const FImGuiCanvasSizeInfo& CanvasSizeInfo) +{ + switch (CanvasSizeInfo.SizeType) + { + case EImGuiCanvasSizeType::Custom: + MinCanvasSize = { static_cast(CanvasSizeInfo.Width), static_cast(CanvasSizeInfo.Height) }; + bAdaptiveCanvasSize = CanvasSizeInfo.bExtendToViewport; + bCanvasControlEnabled = true; + break; + + case EImGuiCanvasSizeType::Desktop: + MinCanvasSize = (GEngine && GEngine->GameUserSettings) + ? GEngine->GameUserSettings->GetDesktopResolution() : FVector2D::ZeroVector; + bAdaptiveCanvasSize = CanvasSizeInfo.bExtendToViewport; + bCanvasControlEnabled = true; + break; + + case EImGuiCanvasSizeType::Viewport: + default: + MinCanvasSize = FVector2D::ZeroVector; + bAdaptiveCanvasSize = true; + bCanvasControlEnabled = false; + } + + // We only update canvas control widget when canvas control is enabled. Make sure that we will not leave + // that widget active after disabling canvas control. + if (CanvasControlWidget.IsValid() && !bCanvasControlEnabled) + { + CanvasControlWidget->SetActive(false); + } + + bUpdateCanvasSize = true; +} + +void SImGuiWidget::UpdateCanvasSize() +{ + if (bUpdateCanvasSize) + { + if (auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex)) + { + CanvasSize = MinCanvasSize; + if (bAdaptiveCanvasSize && GameViewport.IsValid()) + { + FVector2D ViewportSize; + GameViewport->GetViewportSize(ViewportSize); + CanvasSize = MaxVector(CanvasSize, ViewportSize); + } + else + { + // No need for more updates, if we successfully processed fixed-canvas size. + bUpdateCanvasSize = false; + } + + // Clamping DPI Scale to keep the canvas size from getting too big. + CanvasSize /= FMath::Max(DPIScale, 0.01f); + CanvasSize = RoundVector(CanvasSize); + + ContextProxy->SetDisplaySize(CanvasSize); + } + } +} + +void SImGuiWidget::UpdateCanvasControlMode(const FInputEvent& InputEvent) +{ + if (bCanvasControlEnabled) + { + CanvasControlWidget->SetActive(InputEvent.IsLeftAltDown() && InputEvent.IsLeftShiftDown()); + } +} + +void SImGuiWidget::OnPostImGuiUpdate() +{ + ImGuiRenderTransform = ImGuiTransform; + UpdateMouseCursor(); +} + +FVector2D SImGuiWidget::TransformScreenPointToImGui(const FGeometry& MyGeometry, const FVector2D& Point) const +{ + const FSlateRenderTransform ImGuiToScreen = ImGuiTransform.Concatenate(MyGeometry.GetAccumulatedRenderTransform()); + return ImGuiToScreen.Inverse().TransformPoint(Point); +} + +int32 SImGuiWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, + FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& WidgetStyle, bool bParentEnabled) const +{ + if (FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex)) + { + // Manually update ImGui context to minimise lag between creating and rendering ImGui output. This will also + // keep frame tearing at minimum because it is executed at the very end of the frame. + ContextProxy->Tick(FSlateApplication::Get().GetDeltaTime()); + + // Calculate transform from ImGui to screen space. Rounding translation is necessary to keep it pixel-perfect + // in older engine versions. + const FSlateRenderTransform& WidgetToScreen = AllottedGeometry.GetAccumulatedRenderTransform(); + const FSlateRenderTransform ImGuiToScreen = RoundTranslation(ImGuiRenderTransform.Concatenate(WidgetToScreen)); + +#if ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + // Convert clipping rectangle to format required by Slate vertex. + const FSlateRotatedRect VertexClippingRect{ MyClippingRect }; +#endif // ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + + for (const auto& DrawList : ContextProxy->GetDrawData()) + { +#if ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + DrawList.CopyVertexData(VertexBuffer, ImGuiToScreen, VertexClippingRect); +#else + DrawList.CopyVertexData(VertexBuffer, ImGuiToScreen); +#endif // ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + + int IndexBufferOffset = 0; + for (int CommandNb = 0; CommandNb < DrawList.NumCommands(); CommandNb++) + { + const auto& DrawCommand = DrawList.GetCommand(CommandNb, ImGuiToScreen); + + DrawList.CopyIndexData(IndexBuffer, IndexBufferOffset, DrawCommand.NumElements); + + // Advance offset by number of copied elements to position it for the next command. + IndexBufferOffset += DrawCommand.NumElements; + + // Get texture resource handle for this draw command (null index will be also mapped to a valid texture). + const FSlateResourceHandle& Handle = ModuleManager->GetTextureManager().GetTextureHandle(DrawCommand.TextureId); + + // Transform clipping rectangle to screen space and apply to elements that we draw. + const FSlateRect ClippingRect = DrawCommand.ClippingRect.IntersectionWith(MyClippingRect); + +#if ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + // Get access to the Slate scissor rectangle defined in Slate Core API, so we can customize elements drawing. + extern SLATECORE_API TOptional GSlateScissorRect; + TGuardValue> GSlateScissorRecGuard(GSlateScissorRect, FShortRect{ ClippingRect }); +#else + OutDrawElements.PushClip(FSlateClippingZone{ ClippingRect }); +#endif // ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + + // Add elements to the list. + FSlateDrawElement::MakeCustomVerts(OutDrawElements, LayerId, Handle, VertexBuffer, IndexBuffer, nullptr, 0, 0); + +#if !ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + OutDrawElements.PopClip(); +#endif // ENGINE_COMPATIBILITY_LEGACY_CLIPPING_API + } + } + } + + return Super::OnPaint(Args, AllottedGeometry, MyClippingRect, OutDrawElements, LayerId, WidgetStyle, bParentEnabled); +} + +FVector2D SImGuiWidget::ComputeDesiredSize(float Scale) const +{ + return CanvasSize * Scale; +} + +#if IMGUI_WIDGET_DEBUG + +static TArray GetImGuiMappedKeys() +{ + TArray Keys; + Keys.Reserve(Utilities::ArraySize::value + 8); + + // ImGui IO key map. + Keys.Emplace(EKeys::Tab); + Keys.Emplace(EKeys::Left); + Keys.Emplace(EKeys::Right); + Keys.Emplace(EKeys::Up); + Keys.Emplace(EKeys::Down); + Keys.Emplace(EKeys::PageUp); + Keys.Emplace(EKeys::PageDown); + Keys.Emplace(EKeys::Home); + Keys.Emplace(EKeys::End); + Keys.Emplace(EKeys::Delete); + Keys.Emplace(EKeys::BackSpace); + Keys.Emplace(EKeys::Enter); + Keys.Emplace(EKeys::Escape); + Keys.Emplace(EKeys::A); + Keys.Emplace(EKeys::C); + Keys.Emplace(EKeys::V); + Keys.Emplace(EKeys::X); + Keys.Emplace(EKeys::Y); + Keys.Emplace(EKeys::Z); + + // Modifier keys. + Keys.Emplace(EKeys::LeftShift); + Keys.Emplace(EKeys::RightShift); + Keys.Emplace(EKeys::LeftControl); + Keys.Emplace(EKeys::RightControl); + Keys.Emplace(EKeys::LeftAlt); + Keys.Emplace(EKeys::RightAlt); + Keys.Emplace(EKeys::LeftCommand); + Keys.Emplace(EKeys::RightCommand); + + return Keys; +} + +namespace +{ + void Text(const char* Str) + { + ImGui::Text("%s:", Str); + } + + void Text(const wchar_t* Str) + { + ImGui::Text("%ls:", Str); + } + + template::value && !std::is_same::value, TCHAR>> + void Text(const CharType* Str) + { + ImGui::Text("%ls", TCHAR_TO_WCHAR(Str)); + } +} + +// Column layout utilities. +namespace Columns +{ + template + static void CollapsingGroup(const char* Name, int Columns, FunctorType&& DrawContent) + { + if (ImGui::CollapsingHeader(Name, ImGuiTreeNodeFlags_DefaultOpen)) + { + const int LastColumns = ImGui::GetColumnsCount(); + ImGui::Columns(Columns, nullptr, false); + DrawContent(); + ImGui::Columns(LastColumns); + } + } +} + +// Controls tweaked for 2-columns layout. +namespace TwoColumns +{ + template + static inline void CollapsingGroup(const char* Name, FunctorType&& DrawContent) + { + Columns::CollapsingGroup(Name, 2, std::forward(DrawContent)); + } + + template + static void Value(LabelType&& Label, int32 Value) + { + Text(Label); ImGui::NextColumn(); + ImGui::Text("%d", Value); ImGui::NextColumn(); + } + + template + static void Value(LabelType&& Label, uint32 Value) + { + Text(Label); ImGui::NextColumn(); + ImGui::Text("%u", Value); ImGui::NextColumn(); + } + + template + static void Value(LabelType&& Label, float Value) + { + Text(Label); ImGui::NextColumn(); + ImGui::Text("%f", Value); ImGui::NextColumn(); + } + + template + static void Value(LabelType&& Label, bool bValue) + { + Text(Label); ImGui::NextColumn(); + Text(TEXT_BOOL(bValue)); ImGui::NextColumn(); + } + + template + static void Value(LabelType&& Label, const TCHAR* Value) + { + Text(Label); ImGui::NextColumn(); + Text(Value); ImGui::NextColumn(); + } + + template + static void ValueWidthHeight(LabelType&& Label, const FVector2D& Value) + { + Text(Label); ImGui::NextColumn(); + ImGui::Text("Width = %.0f, Height = %.0f", Value.X, Value.Y); ImGui::NextColumn(); + } +} + +namespace Styles +{ + template + static void TextHighlight(bool bHighlight, FunctorType&& DrawContent) + { + if (bHighlight) + { + ImGui::PushStyleColor(ImGuiCol_Text, { 1.f, 1.f, 0.5f, 1.f }); + } + DrawContent(); + if (bHighlight) + { + ImGui::PopStyleColor(); + } + } +} + +void SImGuiWidget::OnDebugDraw() +{ + FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); + + if (CVars::DebugWidget.GetValueOnGameThread() > 0) + { + bool bDebug = true; + ImGui::SetNextWindowSize(ImVec2(380, 480), ImGuiCond_Once); + if (ImGui::Begin("ImGui Widget Debug", &bDebug)) + { + ImGui::Spacing(); + + TwoColumns::CollapsingGroup("Context", [&]() + { + TwoColumns::Value("Context Index", ContextIndex); + TwoColumns::Value("Context Name", ContextProxy ? *ContextProxy->GetName() : TEXT("< Null >")); + TwoColumns::Value("Game Viewport", *GameViewport->GetName()); + }); + + TwoColumns::CollapsingGroup("Canvas Size", [&]() + { + TwoColumns::Value("Is Adaptive", bAdaptiveCanvasSize); + TwoColumns::Value("Is Updating", bUpdateCanvasSize); + TwoColumns::ValueWidthHeight("Min Canvas Size", MinCanvasSize); + TwoColumns::ValueWidthHeight("Canvas Size", CanvasSize); + }); + + TwoColumns::CollapsingGroup("DPI Scale", [&]() + { + TwoColumns::Value("Slate Scale", DPIScale); + TwoColumns::Value("ImGui Scale", ContextProxy ? ContextProxy->GetDPIScale() : 1.f); + }); + + TwoColumns::CollapsingGroup("Input Mode", [&]() + { + TwoColumns::Value("Input Enabled", bInputEnabled); + }); + + TwoColumns::CollapsingGroup("Widget", [&]() + { + TwoColumns::Value("Visibility", *GetVisibility().ToString()); + TwoColumns::Value("Is Hovered", IsHovered()); + TwoColumns::Value("Is Directly Hovered", IsDirectlyHovered()); + TwoColumns::Value("Has Keyboard Input", HasKeyboardFocus()); + }); + + TwoColumns::CollapsingGroup("Viewport", [&]() + { + const auto& ViewportWidget = GameViewport->GetGameViewportWidget(); + TwoColumns::Value("Is Foreground Window", GameViewport->Viewport->IsForegroundWindow()); + TwoColumns::Value("Is Hovered", ViewportWidget->IsHovered()); + TwoColumns::Value("Is Directly Hovered", ViewportWidget->IsDirectlyHovered()); + TwoColumns::Value("Has Mouse Capture", ViewportWidget->HasMouseCapture()); + TwoColumns::Value("Has Keyboard Input", ViewportWidget->HasKeyboardFocus()); + TwoColumns::Value("Has Focused Descendants", ViewportWidget->HasFocusedDescendants()); + auto Widget = PreviousUserFocusedWidget.Pin(); + TwoColumns::Value("Previous User Focused", Widget.IsValid() ? *Widget->GetTypeAsString() : TEXT("None")); + }); + } + ImGui::End(); + + if (!bDebug) + { + CVars::DebugWidget->Set(0, ECVF_SetByConsole); + } + } + + if (ContextProxy && CVars::DebugInput.GetValueOnGameThread() > 0) + { + FImGuiInputState& InputState = ContextProxy->GetInputState(); + + bool bDebug = true; + ImGui::SetNextWindowSize(ImVec2(460, 480), ImGuiCond_Once); + if (ImGui::Begin("ImGui Input State", &bDebug)) + { + const ImVec4 HiglightColor{ 1.f, 1.f, 0.5f, 1.f }; + Columns::CollapsingGroup("Mapped Keys", 4, [&]() + { + static const auto& Keys = GetImGuiMappedKeys(); + + const int32 Num = Keys.Num(); + + // Simplified when slicing for two 2. + const int32 RowsNum = (Num + 1) / 2; + + for (int32 Row = 0; Row < RowsNum; Row++) + { + for (int32 Col = 0; Col < 2; Col++) + { + const int32 Idx = Row + Col * RowsNum; + if (Idx < Num) + { + const FKey& Key = Keys[Idx]; + const uint32 KeyIndex = ImGuiInterops::GetKeyIndex(Key); + Styles::TextHighlight(InputState.GetKeys()[KeyIndex], [&]() + { + TwoColumns::Value(*Key.GetDisplayName().ToString(), KeyIndex); + }); + } + else + { + ImGui::NextColumn(); ImGui::NextColumn(); + } + } + } + }); + + Columns::CollapsingGroup("Modifier Keys", 4, [&]() + { + Styles::TextHighlight(InputState.IsShiftDown(), [&]() { ImGui::Text("Shift"); }); ImGui::NextColumn(); + Styles::TextHighlight(InputState.IsControlDown(), [&]() { ImGui::Text("Control"); }); ImGui::NextColumn(); + Styles::TextHighlight(InputState.IsAltDown(), [&]() { ImGui::Text("Alt"); }); ImGui::NextColumn(); + ImGui::NextColumn(); + }); + + Columns::CollapsingGroup("Mouse Buttons", 4, [&]() + { + static const FKey Buttons[] = { EKeys::LeftMouseButton, EKeys::RightMouseButton, + EKeys::MiddleMouseButton, EKeys::ThumbMouseButton, EKeys::ThumbMouseButton2 }; + + const int32 Num = Utilities::GetArraySize(Buttons); + + // Simplified when slicing for two 2. + const int32 RowsNum = (Num + 1) / 2; + + for (int32 Row = 0; Row < RowsNum; Row++) + { + for (int32 Col = 0; Col < 2; Col++) + { + const int32 Idx = Row + Col * RowsNum; + if (Idx < Num) + { + const FKey& Button = Buttons[Idx]; + const uint32 MouseIndex = ImGuiInterops::GetMouseIndex(Button); + Styles::TextHighlight(InputState.GetMouseButtons()[MouseIndex], [&]() + { + TwoColumns::Value(*Button.GetDisplayName().ToString(), MouseIndex); + }); + } + else + { + ImGui::NextColumn(); ImGui::NextColumn(); + } + } + } + }); + + Columns::CollapsingGroup("Mouse Axes", 4, [&]() + { + TwoColumns::Value("Position X", InputState.GetMousePosition().X); + TwoColumns::Value("Position Y", InputState.GetMousePosition().Y); + TwoColumns::Value("Wheel Delta", InputState.GetMouseWheelDelta()); + ImGui::NextColumn(); ImGui::NextColumn(); + }); + + if (!bDebug) + { + CVars::DebugInput->Set(0, ECVF_SetByConsole); + } + } + ImGui::End(); + } +} + +#undef TEXT_INPUT_MODE +#undef TEXT_BOOL + +#endif // IMGUI_WIDGET_DEBUG + +#undef IMGUI_WIDGET_LOG + +``` + +`Plugins/ImGui/Source/ImGui/Private/Widgets/SImGuiWidget.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "ImGuiModuleDebug.h" +#include "ImGuiModuleSettings.h" + +#include +#include +#include +#include + + +// Hide ImGui Widget debug in non-developer mode. +#define IMGUI_WIDGET_DEBUG IMGUI_MODULE_DEVELOPER + +class FImGuiModuleManager; +class SImGuiCanvasControl; +class UImGuiInputHandler; + +class UGameViewportClient; +class ULocalPlayer; + +// Slate widget for rendering ImGui output and storing Slate inputs. +class SImGuiWidget : public SCompoundWidget +{ + typedef SCompoundWidget Super; + +public: + + SLATE_BEGIN_ARGS(SImGuiWidget) + {} + SLATE_ARGUMENT(FImGuiModuleManager*, ModuleManager) + SLATE_ARGUMENT(UGameViewportClient*, GameViewport) + SLATE_ARGUMENT(int32, ContextIndex) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + + ~SImGuiWidget(); + + // Get index of the context that this widget is targeting. + int32 GetContextIndex() const { return ContextIndex; } + + //---------------------------------------------------------------------------------------------------- + // SWidget overrides + //---------------------------------------------------------------------------------------------------- + + virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override; + + virtual bool SupportsKeyboardFocus() const override { return bInputEnabled && !IsConsoleOpened(); } + + virtual FReply OnKeyChar(const FGeometry& MyGeometry, const FCharacterEvent& CharacterEvent) override; + + virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent) override; + + virtual FReply OnKeyUp(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent) override; + + virtual FReply OnAnalogValueChanged(const FGeometry& MyGeometry, const FAnalogInputEvent& AnalogInputEvent) override; + + virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + virtual FReply OnMouseButtonDoubleClick(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + virtual FReply OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + virtual FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + virtual FReply OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& FocusEvent) override; + + virtual void OnFocusLost(const FFocusEvent& FocusEvent) override; + + virtual void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + + virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override; + + virtual FReply OnTouchStarted(const FGeometry& MyGeometry, const FPointerEvent& TouchEvent) override; + + virtual FReply OnTouchMoved(const FGeometry& MyGeometry, const FPointerEvent& TouchEvent) override; + + virtual FReply OnTouchEnded(const FGeometry& MyGeometry, const FPointerEvent& TouchEvent) override; + +private: + + void CreateInputHandler(const FSoftClassPath& HandlerClassReference); + void ReleaseInputHandler(); + + void RegisterImGuiSettingsDelegates(); + void UnregisterImGuiSettingsDelegates(); + + void SetHideMouseCursor(bool bHide); + + bool IsConsoleOpened() const; + + // Update visibility based on input state. + void UpdateVisibility(); + + // Update cursor based on input state. + void UpdateMouseCursor(); + + ULocalPlayer* GetLocalPlayer() const; + void TakeFocus(); + void ReturnFocus(); + + // Update input state. + void UpdateInputState(); + void UpdateTransparentMouseInput(const FGeometry& AllottedGeometry); + void HandleWindowFocusLost(); + + void SetDPIScale(const FImGuiDPIScaleInfo& ScaleInfo); + + void SetCanvasSizeInfo(const FImGuiCanvasSizeInfo& CanvasSizeInfo); + void UpdateCanvasSize(); + + void UpdateCanvasControlMode(const FInputEvent& InputEvent); + + void OnPostImGuiUpdate(); + + FVector2D TransformScreenPointToImGui(const FGeometry& MyGeometry, const FVector2D& Point) const; + + virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& WidgetStyle, bool bParentEnabled) const override; + + virtual FVector2D ComputeDesiredSize(float) const override; + + void SetImGuiTransform(const FSlateRenderTransform& Transform) { ImGuiTransform = Transform; } + +#if IMGUI_WIDGET_DEBUG + void OnDebugDraw(); +#endif // IMGUI_WIDGET_DEBUG + + FImGuiModuleManager* ModuleManager = nullptr; + TWeakObjectPtr GameViewport; + TWeakObjectPtr InputHandler; + + FSlateRenderTransform ImGuiTransform; + FSlateRenderTransform ImGuiRenderTransform; + + mutable TArray VertexBuffer; + mutable TArray IndexBuffer; + + int32 ContextIndex = 0; + + FVector2D MinCanvasSize = FVector2D::ZeroVector; + FVector2D CanvasSize = FVector2D::ZeroVector; + + float DPIScale = 1.f; + + bool bInputEnabled = false; + bool bForegroundWindow = false; + bool bHideMouseCursor = true; + bool bTransparentMouseInput = false; + bool bAdaptiveCanvasSize = false; + bool bUpdateCanvasSize = false; + bool bCanvasControlEnabled = false; + + TSharedPtr CanvasControlWidget; + TWeakPtr PreviousUserFocusedWidget; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Public/ImGuiDelegates.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include + + +class UWorld; + +/** + * Delegates to ImGui debug events. World delegates are called once per frame during world updates and have invocation + * lists cleared after their worlds become invalid. Multi-context delegates are called once for every updated world. + * Early debug delegates are called during world tick start and debug delegates are called during world post actor tick + * or in engine versions below 4.18 during world tick start. + * + * Order of events is defined in a way that multi-context delegates can be used to draw headers and/or footers: + * multi-context early debug, world early debug, world debug, multi-context debug. + */ +class IMGUI_API FImGuiDelegates +{ +public: + + /** + * Get a delegate to ImGui world early debug event for current world (GWorld). + * @returns Simple multicast delegate to debug events called once per frame to debug current world + */ + static FSimpleMulticastDelegate& OnWorldEarlyDebug(); + + /** + * Get a delegate to ImGui world early debug event for given world. + * @param World - World for which we need a delegate + * @returns Simple multicast delegate to debug events called once per frame to debug given world + */ + static FSimpleMulticastDelegate& OnWorldEarlyDebug(UWorld* World); + + /** + * Get a delegate to ImGui multi-context early debug event. + * @returns Simple multicast delegate to debug events called once per frame for every world to debug + */ + static FSimpleMulticastDelegate& OnMultiContextEarlyDebug(); + + /** + * Get a delegate to ImGui world debug event for current world (GWorld). + * @returns Simple multicast delegate to debug events called once per frame to debug current world + */ + static FSimpleMulticastDelegate& OnWorldDebug(); + + /** + * Get a delegate to ImGui world debug event for given world. + * @param World - World for which we need a delegate + * @returns Simple multicast delegate to debug events called once per frame to debug given world + */ + static FSimpleMulticastDelegate& OnWorldDebug(UWorld* World); + + /** + * Get a delegate to ImGui multi-context debug event. + * @returns Simple multicast delegate to debug events called once per frame for every world to debug + */ + static FSimpleMulticastDelegate& OnMultiContextDebug(); +}; + + +/** Enable to support legacy ImGui delegates API. */ +#define IMGUI_WITH_OBSOLETE_DELEGATES 1 + +#if IMGUI_WITH_OBSOLETE_DELEGATES + +/** Delegate that allows to subscribe for ImGui events. */ +typedef FSimpleMulticastDelegate::FDelegate FImGuiDelegate; + +/** + * Handle to ImGui delegate. Contains additional information locating delegates in different contexts. + */ +class FImGuiDelegateHandle +{ +public: + + FImGuiDelegateHandle() = default; + + bool IsValid() const + { + return Handle.IsValid(); + } + + void Reset() + { + Handle.Reset(); + Index = 0; + } + +private: + + FImGuiDelegateHandle(const FDelegateHandle& InHandle, int32 InCategory, int32 InIndex = 0) + : Handle(InHandle) + , Category(InCategory) + , Index(InIndex) + { + } + + friend bool operator==(const FImGuiDelegateHandle& Lhs, const FImGuiDelegateHandle& Rhs) + { + return Lhs.Handle == Rhs.Handle && Lhs.Category == Rhs.Category && Lhs.Index == Rhs.Index; + } + + friend bool operator!=(const FImGuiDelegateHandle& Lhs, const FImGuiDelegateHandle& Rhs) + { + return !(Lhs == Rhs); + } + + FDelegateHandle Handle; + int32 Category = 0; + int32 Index = 0; + + friend class FImGuiModule; +}; + +#endif // IMGUI_WITH_OBSOLETE_DELEGATES + +``` + +`Plugins/ImGui/Source/ImGui/Public/ImGuiInputHandler.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include +#include +#include +#include + +#include "ImGuiInputHandler.generated.h" + + +class FImGuiModuleManager; +class UGameViewportClient; + +struct FAnalogInputEvent; +struct FCharacterEvent; +struct FKeyEvent; + +#if WITH_EDITOR +class FUICommandInfo; +#endif // WITH_EDITOR + + +/** + * Handles input and sends it to the input state, which is copied to the ImGui IO at the beginning of the frame. + * Implementation of the input handler can be changed in the ImGui project settings by changing ImGuiInputHandlerClass. + */ +UCLASS() +class IMGUI_API UImGuiInputHandler : public UObject +{ + GENERATED_BODY() + +public: + + /** + * Called to handle character events. + * @returns Response whether the event was handled + */ + virtual FReply OnKeyChar(const struct FCharacterEvent& CharacterEvent); + + /** + * Called to handle key down events. + * @returns Response whether the event was handled + */ + virtual FReply OnKeyDown(const FKeyEvent& KeyEvent); + + /** + * Called to handle key up events. + * @returns Response whether the event was handled + */ + virtual FReply OnKeyUp(const FKeyEvent& KeyEvent); + + /** + * Called to handle analog value change events. + * @returns Response whether the event was handled + */ + virtual FReply OnAnalogValueChanged(const FAnalogInputEvent& AnalogInputEvent); + + /** + * Called to handle mouse button down events. + * @returns Response whether the event was handled + */ + virtual FReply OnMouseButtonDown(const FPointerEvent& MouseEvent); + + /** + * Called to handle mouse button double-click events. + * @returns Response whether the event was handled + */ + virtual FReply OnMouseButtonDoubleClick(const FPointerEvent& MouseEvent); + + /** + * Called to handle mouse button up events. + * @returns Response whether the event was handled + */ + virtual FReply OnMouseButtonUp(const FPointerEvent& MouseEvent); + + /** + * Called to handle mouse wheel events. + * @returns Response whether the event was handled + */ + virtual FReply OnMouseWheel(const FPointerEvent& MouseEvent); + + /** + * Called to handle mouse move events. + * @param MousePosition Mouse position (in ImGui space) + * @param MouseEvent Optional mouse event passed from Slate + * @returns Response whether the event was handled + */ + virtual FReply OnMouseMove(const FVector2D& MousePosition, const FPointerEvent& MouseEvent); + virtual FReply OnMouseMove(const FVector2D& MousePosition); + + /** + * Called to handle touch started event. + * @param TouchPosition Touch position (in ImGui space) + * @param TouchEvent Touch event passed from Slate + * @returns Response whether the event was handled + */ + virtual FReply OnTouchStarted(const FVector2D& TouchPosition, const FPointerEvent& TouchEvent); + + /** + * Called to handle touch moved event. + * @param TouchPosition Touch position (in ImGui space) + * @param TouchEvent Touch event passed from Slate + * @returns Response whether the event was handled + */ + virtual FReply OnTouchMoved(const FVector2D& TouchPosition, const FPointerEvent& TouchEvent); + + /** + * Called to handle touch ended event. + * @param TouchPosition Touch position (in ImGui space) + * @param TouchEvent Touch event passed from Slate + * @returns Response whether the event was handled + */ + virtual FReply OnTouchEnded(const FVector2D& TouchPosition, const FPointerEvent& TouchEvent); + + /** Called to handle activation of the keyboard input. */ + virtual void OnKeyboardInputEnabled(); + + /** Called to handle deactivation of the keyboard input. */ + virtual void OnKeyboardInputDisabled(); + + /** Called to handle activation of the gamepad input. */ + virtual void OnGamepadInputEnabled(); + + /** Called to handle deactivation of the gamepad input. */ + virtual void OnGamepadInputDisabled(); + + /** Called to handle activation of the mouse input. */ + virtual void OnMouseInputEnabled(); + + /** Called to handle deactivation of the mouse input. */ + virtual void OnMouseInputDisabled(); + +protected: + + /** Copy state of modifier keys to input state. */ + void CopyModifierKeys(const FInputEvent& InputEvent); + + /** + * Checks whether this is a key event that can open console. + * @param KeyEvent - Key event to test. + * @returns True, if this key event can open console. + */ + bool IsConsoleEvent(const FKeyEvent& KeyEvent) const; + +#if WITH_EDITOR + /** + * Checks whether this is a key event that can stop PIE session. + * @param KeyEvent - Key event to test. + * @returns True, if this key event can stop PIE session. + */ + bool IsStopPlaySessionEvent(const FKeyEvent& KeyEvent) const; +#endif + + /** + * Checks whether this key event can toggle ImGui input (as defined in settings). + * @param KeyEvent - Key event to test. + * @returns True, if this key is bound to 'ImGui.ToggleInput' command that switches ImGui input mode. + */ + bool IsToggleInputEvent(const FKeyEvent& KeyEvent) const; + + /** + * Checks whether corresponding ImGui context has an active item (holding cursor focus). + * @returns True, if corresponding context has an active item. + */ + bool HasImGuiActiveItem() const; + +private: + + void UpdateInputStatePointer(); + + void OnSoftwareCursorChanged(bool); + + void OnPostImGuiUpdate(); + + void Initialize(FImGuiModuleManager* InModuleManager, UGameViewportClient* InGameViewport, int32 InContextIndex); + + virtual void BeginDestroy() override; + + class FImGuiInputState* InputState = nullptr; + + bool bMouseInputEnabled = false; + bool bKeyboardInputEnabled = false; + bool bGamepadInputEnabled = false; + + FImGuiModuleManager* ModuleManager = nullptr; + + TWeakObjectPtr GameViewport; + + int32 ContextIndex = -1; + +#if WITH_EDITOR + TSharedPtr StopPlaySessionCommandInfo; +#endif + + friend class FImGuiInputHandlerFactory; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Public/ImGuiModule.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "ImGuiDelegates.h" +#include "ImGuiModuleProperties.h" +#include "ImGuiTextureHandle.h" + +#include + + +class FImGuiModule : public IModuleInterface +{ +public: + + /** + * Singleton-like access to this module's interface. This is just for convenience! + * Beware of calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline FImGuiModule& Get() + { + return FModuleManager::LoadModuleChecked("ImGui"); + } + + /** + * Checks to see if this module is loaded and ready. It is only valid to call Get() if IsAvailable() returns true. + * + * @return True if the module is loaded and ready to use + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("ImGui"); + } + +#if IMGUI_WITH_OBSOLETE_DELEGATES + +#if WITH_EDITOR + /** + * Add a delegate called at the end of editor debug frame to draw debug controls in its ImGui context, creating + * that context on demand. + * + * @param Delegate - Delegate that we want to add (@see FImGuiDelegate::Create...) + * @returns Returns handle that can be used to remove delegate (@see RemoveImGuiDelegate) + */ + virtual FImGuiDelegateHandle AddEditorImGuiDelegate(const FImGuiDelegate& Delegate); +#endif + + /** + * Add a delegate called at the end of current world debug frame to draw debug controls in its ImGui context, + * creating that context on demand. + * This function will throw if called outside of a world context (i.e. current world cannot be found). + * + * @param Delegate - Delegate that we want to add (@see FImGuiDelegate::Create...) + * @returns Returns handle that can be used to remove delegate (@see RemoveImGuiDelegate) + */ + virtual FImGuiDelegateHandle AddWorldImGuiDelegate(const FImGuiDelegate& Delegate); + + /** + * Add a delegate called at the end of a specific world's debug frame to draw debug controls in its ImGui context, + * creating that context on demand. + * + * @param World - A specific world to add the delegate to to + * @param Delegate - Delegate that we want to add (@see FImGuiDelegate::Create...) + * @returns Returns handle that can be used to remove delegate (@see RemoveImGuiDelegate) + */ + virtual FImGuiDelegateHandle AddWorldImGuiDelegate(const UWorld* World, const FImGuiDelegate& Delegate); + + /** + * Add shared delegate called for each ImGui context at the end of debug frame, after calling context specific + * delegate. This delegate will be used for any ImGui context, created before or after it is registered. + * + * @param Delegate - Delegate that we want to add (@see FImGuiDelegate::Create...) + * @returns Returns handle that can be used to remove delegate (@see RemoveImGuiDelegate) + */ + virtual FImGuiDelegateHandle AddMultiContextImGuiDelegate(const FImGuiDelegate& Delegate); + + /** + * Remove delegate added with any version of Add...ImGuiDelegate + * + * @param Handle - Delegate handle that was returned by adding function + */ + virtual void RemoveImGuiDelegate(const FImGuiDelegateHandle& Handle); + +#endif // #if IMGUI_WITH_OBSOLETE_DELEGATES + + /** + * If it exists, get a handle to the texture with given resource name. + * + * @param Name - Resource name of a texture to find + * @returns Handle to a registered texture or invalid handle if resources could not be found or were not valid + */ + virtual FImGuiTextureHandle FindTextureHandle(const FName& Name); + + /** + * Register texture and create its Slate resources. If texture with that name already exists then it may be updated + * or if bMakeUnique is true, exception will be thrown. Throws exception, if name argument is NAME_None or texture + * is null. + * + * Note, that updating texture resources doesn't invalidate already existing handles and returned handle will have + * the same value. + * + * @param Name - Resource name for the texture that needs to be registered or updated + * @param Texture - Texture for which we want to create or update Slate resources + * @param bMakeUnique - If false then existing resources are updated/overwritten (default). If true, then stricter + * policy is applied and if resource with that name exists then exception is thrown. + * @returns Handle to the texture resources, which can be used to release allocated resources and as an argument to + * relevant ImGui functions + */ + virtual FImGuiTextureHandle RegisterTexture(const FName& Name, class UTexture* Texture, bool bMakeUnique = false); + + /** + * Unregister texture and release its Slate resources. If handle is null or not valid, this function fails silently + * (for definition of 'valid' look @ FImGuiTextureHandle). + * + * @returns ImGui Texture Handle to texture that needs to be unregistered + */ + virtual void ReleaseTexture(const FImGuiTextureHandle& Handle); + + virtual void RebuildFontAtlas(); + + /** + * Get ImGui module properties. + * + * @returns Reference to an instance of ImGui module properties that allows to read and/or modify module state. + */ + virtual FImGuiModuleProperties& GetProperties(); + virtual const FImGuiModuleProperties& GetProperties() const; + + /** + * DEPRECIATED: Please use GetProperties() as this function is scheduled for removal. + * Check whether Input Mode is enabled (tests ImGui.InputEnabled console variable). + * + * @returns True, if Input Mode is enabled (ImGui.InputEnabled != 0) and false otherwise. + */ + virtual bool IsInputMode() const; + + /** + * DEPRECIATED: Please use GetProperties() as this function is scheduled for removal. + * Set Input Mode state (sets ImGui.InputEnabled console variable, so it can be used together with a console). + * + * @param bEnabled - Whether Input Mode should be enabled (ImGui.InputEnabled = 1) or not (ImGui.InputEnabled = 0). + */ + virtual void SetInputMode(bool bEnabled); + + /** + * DEPRECIATED: Please use GetProperties() as this function is scheduled for removal. + * Toggle Input Mode state (changes ImGui.InputEnabled console variable). + */ + virtual void ToggleInputMode(); + + /** + * DEPRECIATED: Please use GetProperties() as this function is scheduled for removal. + * Check whether ImGui Demo is shown (tests ImGui.ShowDemo console variable). + * + * @returns True, if demo is shown (ImGui.ShowDemo != 0) and false otherwise. + */ + virtual bool IsShowingDemo() const; + + /** + * DEPRECIATED: Please use GetProperties() as this function is scheduled for removal. + * Set whether to show ImGui Demo (sets ImGui.ShowDemo console variable, so it can be used together with a console). + * + * @param bShow - Whether to show ImGui Demo (ImGui.ShowDemo = 1) or not (ImGui.ShowDemo = 0). + */ + virtual void SetShowDemo(bool bShow); + + /** + * DEPRECIATED: Please use GetProperties() as this function is scheduled for removal. + * Toggle ImGui Demo (changes ImGui.ShowDemo console variable). + */ + virtual void ToggleShowDemo(); + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +private: +#if WITH_EDITOR + virtual void SetProperties(const FImGuiModuleProperties& Properties); + struct FImGuiContextHandle* ImGuiContextHandle = nullptr; + struct FImGuiDelegatesContainerHandle* DelegatesContainerHandle = nullptr; + friend struct FImGuiContextHandle; + friend struct FImGuiDelegatesContainerHandle; +#endif +}; + +``` + +`Plugins/ImGui/Source/ImGui/Public/ImGuiModuleProperties.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +struct ImFontConfig; + +/** Properties that define state of the ImGui module. */ +class IMGUI_API FImGuiModuleProperties +{ +public: + + /** Check whether input is enabled. */ + bool IsInputEnabled() const { return bInputEnabled; } + + /** Enable or disable ImGui input. */ + void SetInputEnabled(bool bEnabled) { bInputEnabled = bEnabled; } + + /** Toggle ImGui input. */ + void ToggleInput() { SetInputEnabled(!IsInputEnabled()); } + + /** Check whether keyboard navigation is enabled. */ + bool IsKeyboardNavigationEnabled() const { return bKeyboardNavigationEnabled; } + + /** Enable or disable keyboard navigation. */ + void SetKeyboardNavigationEnabled(bool bEnabled) { bKeyboardNavigationEnabled = bEnabled; } + + /** Toggle keyboard navigation. */ + void ToggleKeyboardNavigation() { SetKeyboardNavigationEnabled(!IsKeyboardNavigationEnabled()); } + + /** Check whether gamepad navigation is enabled. */ + bool IsGamepadNavigationEnabled() const { return bGamepadNavigationEnabled; } + + /** Enable or disable gamepad navigation. */ + void SetGamepadNavigationEnabled(bool bEnabled) { bGamepadNavigationEnabled = bEnabled; } + + /** Toggle gamepad navigation. */ + void ToggleGamepadNavigation() { SetGamepadNavigationEnabled(!IsGamepadNavigationEnabled()); } + + /** Check whether keyboard input is shared with game. */ + bool IsKeyboardInputShared() const { return bKeyboardInputShared; } + + /** Set whether keyboard input should be shared with game. */ + void SetKeyboardInputShared(bool bShared) { bKeyboardInputShared = bShared; } + + /** Toggle whether keyboard input should be shared with game. */ + void ToggleKeyboardInputSharing() { SetKeyboardInputShared(!IsKeyboardInputShared()); } + + /** Check whether gamepad input is shared with game. */ + bool IsGamepadInputShared() const { return bGamepadInputShared; } + + /** Set whether gamepad input should be shared with game. */ + void SetGamepadInputShared(bool bShared) { bGamepadInputShared = bShared; } + + /** Toggle whether gamepad input should be shared with game. */ + void ToggleGamepadInputSharing() { SetGamepadInputShared(!IsGamepadInputShared()); } + + /** Check whether mouse input is shared with game. */ + bool IsMouseInputShared() const { return bMouseInputShared; } + + /** Set whether mouse input should be shared with game. */ + void SetMouseInputShared(bool bShared) { bMouseInputShared = bShared; } + + /** Toggle whether mouse input should be shared with game. */ + void ToggleMouseInputSharing() { SetMouseInputShared(!IsMouseInputShared()); } + + /** Check whether ImGui demo is visible. */ + bool ShowDemo() const { return bShowDemo; } + + /** Show or hide ImGui demo. */ + void SetShowDemo(bool bShow) { bShowDemo = bShow; } + + /** Toggle ImGui demo. */ + void ToggleDemo() { SetShowDemo(!ShowDemo()); } + + /** Adds a new font to initialize */ + void AddCustomFont(FName FontName, TSharedPtr Font) { CustomFonts.Emplace(FontName, Font); } + + /** Removes a font from the custom font list */ + void RemoveCustomFont(FName FontName) { CustomFonts.Remove(FontName); } + + /** Gets the map of registered custom fonts */ + TMap>& GetCustomFonts() { return CustomFonts; } + +private: + + bool bInputEnabled = false; + + bool bKeyboardNavigationEnabled = false; + bool bGamepadNavigationEnabled = false; + + bool bKeyboardInputShared = false; + bool bGamepadInputShared = false; + bool bMouseInputShared = false; + + bool bShowDemo = false; + + TMap> CustomFonts; +}; + +``` + +`Plugins/ImGui/Source/ImGui/Public/ImGuiTextureHandle.h`: + +```h +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include + +#include + + +/** + * Handle to texture resources registered in module instance. Returned after successful texture registration. + * Can be implicitly converted to ImTextureID making it possible to use it directly with ImGui interface. + * Once texture is not needed handle can be used to release resources. + */ +class IMGUI_API FImGuiTextureHandle +{ +public: + + /** Creates an empty (null and not valid) texture handle. */ + FImGuiTextureHandle(); + + /** + * Checks whether this handle is null. Can be used as a quick test whether it points to any resources but it does + * not check whether those resources are valid (see @ IsValid). + * + * @returns True, if this handle is null (Name is NAME_None and TextureId is invalid) and false otherwise. + */ + bool IsNull() const { return Name == NAME_None; } + + /** + * Checks whether this handle is not null and valid. Valid handle points to valid texture resources. + * It is slower but safer test, more useful when there is no guarantee that resources haven't been released. + * + * @returns True, if this handle is not null and valid, false otherwise. + */ + bool IsValid() const { return !IsNull() && HasValidEntry(); } + + /** Get the name of the texture resources (NAME_None if handle is null). */ + const FName& GetName() const { return Name; } + + /** Get the ImGui texture id for this texture (invalid if handle is null). */ + ImTextureID GetTextureId() const { return TextureId; } + + /** Implicit conversion to ImTextureID. */ + operator ImTextureID() const { return GetTextureId(); } + +private: + + /** + * Creates a texture handle with known name and texture id. + * @param InName - Name of the texture + * @param InTextureId - ImGui id of texture + */ + FImGuiTextureHandle(const FName& InName, ImTextureID InTextureId); + + /** Checks if texture manager has entry that matches this name and texture id index. */ + bool HasValidEntry() const; + + FName Name; + ImTextureID TextureId; + + // Give module class a private access, so it can create valid handles. + friend class FImGuiModule; +}; + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Docs/BACKENDS.md`: + +```md +_(You may browse this at https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md or view this file with any Markdown viewer)_ + +## Dear ImGui: Backends + +**The backends/ folder contains backends for popular platforms/graphics API, which you can use in +your application or engine to easily integrate Dear ImGui.** Each backend is typically self-contained in a pair of files: imgui_impl_XXXX.cpp + imgui_impl_XXXX.h. + +- The 'Platform' backends are in charge of: mouse/keyboard/gamepad inputs, cursor shape, timing, windowing.
+ e.g. Windows ([imgui_impl_win32.cpp](https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_win32.cpp)), GLFW ([imgui_impl_glfw.cpp](https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_glfw.cpp)), SDL2 ([imgui_impl_sdl.cpp](https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_sdl.cpp)), etc. + +- The 'Renderer' backends are in charge of: creating atlas texture, rendering imgui draw data.
+ e.g. DirectX11 ([imgui_impl_dx11.cpp](https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_dx11.cpp)), OpenGL/WebGL ([imgui_impl_opengl3.cpp](https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_opengl3.cpp)), Vulkan ([imgui_impl_vulkan.cpp](https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_vulkan.cpp)), etc. + +- For some high-level frameworks, a single backend usually handle both 'Platform' and 'Renderer' parts.
+ e.g. Allegro 5 ([imgui_impl_allegro5.cpp](https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_allegro5.cpp)). If you end up creating a custom backend for your engine, you may want to do the same. + +An application usually combines 1 Platform backend + 1 Renderer backend + main Dear ImGui sources. +For example, the [example_win32_directx11](https://github.com/ocornut/imgui/tree/master/examples/example_win32_directx11) application combines imgui_impl_win32.cpp + imgui_impl_dx11.cpp. There are 20+ examples in the [examples/](https://github.com/ocornut/imgui/blob/master/examples/) folder. See [EXAMPLES.MD](https://github.com/ocornut/imgui/blob/master/docs/EXAMPLES.md) for details. + +**Once Dear ImGui is setup and running, run and refer to `ImGui::ShowDemoWindow()` in imgui_demo.cpp for usage of the end-user API.** + + +### What are backends + +Dear ImGui is highly portable and only requires a few things to run and render, typically: + + - Required: providing mouse/keyboard inputs (fed into the `ImGuiIO` structure). + - Required: uploading the font atlas texture into graphics memory. + - Required: rendering indexed textured triangles with a clipping rectangle. + + Extra features are opt-in, our backends try to support as many as possible: + + - Optional: custom texture binding support. + - Optional: clipboard support. + - Optional: gamepad support. + - Optional: mouse cursor shape support. + - Optional: IME support. + - Optional: multi-viewports support. + etc. + +This is essentially what each backend is doing + obligatory portability cruft. Using default backends ensure you can get all those features including the ones that would be harder to implement on your side (e.g. multi-viewports support). + +It is important to understand the difference between the core Dear ImGui library (files in the root folder) +and backends which we are describing here (backends/ folder). + +- Some issues may only be backend or platform specific. +- You should be able to write backends for pretty much any platform and any 3D graphics API. + e.g. you can get creative and use software rendering or render remotely on a different machine. + + +### Integrating a backend + +See "Getting Started" section of [EXAMPLES.MD](https://github.com/ocornut/imgui/blob/master/docs/EXAMPLES.md) for more details. + + +### List of backends + +In the [backends/](https://github.com/ocornut/imgui/blob/master/backends) folder: + +List of Platforms Backends: + + imgui_impl_android.cpp ; Android native app API + imgui_impl_glfw.cpp ; GLFW (Windows, macOS, Linux, etc.) http://www.glfw.org/ + imgui_impl_osx.mm ; macOS native API (not as feature complete as glfw/sdl backends) + imgui_impl_sdl.cpp ; SDL2 (Windows, macOS, Linux, iOS, Android) https://www.libsdl.org + imgui_impl_win32.cpp ; Win32 native API (Windows) + imgui_impl_glut.cpp ; GLUT/FreeGLUT (this is prehistoric software and absolutely not recommended today!) + +List of Renderer Backends: + + imgui_impl_dx9.cpp ; DirectX9 + imgui_impl_dx10.cpp ; DirectX10 + imgui_impl_dx11.cpp ; DirectX11 + imgui_impl_dx12.cpp ; DirectX12 + imgui_impl_metal.mm ; Metal (with ObjC) + imgui_impl_opengl2.cpp ; OpenGL 2 (legacy, fixed pipeline <- don't use with modern OpenGL context) + imgui_impl_opengl3.cpp ; OpenGL 3/4, OpenGL ES 2, OpenGL ES 3 (modern programmable pipeline) + imgui_impl_sdlrenderer.cpp; SDL_Renderer (optional component of SDL2 available from SDL 2.0.18+) + imgui_impl_vulkan.cpp ; Vulkan + imgui_impl_wgpu.cpp ; WebGPU + +List of high-level Frameworks Backends (combining Platform + Renderer): + + imgui_impl_allegro5.cpp + +Emscripten is also supported. +The [example_emscripten_opengl3](https://github.com/ocornut/imgui/tree/master/examples/example_emscripten_opengl3) app uses imgui_impl_sdl.cpp + imgui_impl_opengl3.cpp, but other combos are possible. + +### Backends for third-party frameworks, graphics API or other languages + +See https://github.com/ocornut/imgui/wiki/Bindings for the full list (e.g. Adventure Game Studio, Cinder, Cocos2d-x, Game Maker Studio2, Godot, LÖVE+LUA, Magnum, Monogame, Ogre, openFrameworks, OpenSceneGraph, SFML, Sokol, Unity, Unreal Engine and many others). + +### Recommended Backends + +If you are not sure which backend to use, the recommended platform/frameworks for portable applications: + +|Library |Website |Backend |Note | +|--------|--------|--------|-----| +| GLFW | https://github.com/glfw/glfw | imgui_impl_glfw.cpp | | +| SDL2 | https://www.libsdl.org | imgui_impl_sdl.cpp | | +| Sokol | https://github.com/floooh/sokol | [util/sokol_imgui.h](https://github.com/floooh/sokol/blob/master/util/sokol_imgui.h) | Lower-level than GLFW/SDL | + + +### Using a custom engine? + +You will likely be tempted to start by rewrite your own backend using your own custom/high-level facilities...
+Think twice! + +If you are new to Dear ImGui, first try using the existing backends as-is. +You will save lots of time integrating the library. +You can LATER decide to rewrite yourself a custom backend if you really need to. +In most situations, custom backends have less features and more bugs than the standard backends we provide. +If you want portability, you can use multiple backends and choose between them either at compile time +or at runtime. + +**Example A**: your engine is built over Windows + DirectX11 but you have your own high-level rendering +system layered over DirectX11.
+Suggestion: try using imgui_impl_win32.cpp + imgui_impl_dx11.cpp first. +Once it works, if you really need it you can replace the imgui_impl_dx11.cpp code with a +custom renderer using your own rendering functions, and keep using the standard Win32 code etc. + +**Example B**: your engine runs on Windows, Mac, Linux and uses DirectX11, Metal, Vulkan respectively.
+Suggestion: use multiple generic backends! +Once it works, if you really need it you can replace parts of backends with your own abstractions. + +**Example C**: your engine runs on platforms we can't provide public backends for (e.g. PS4/PS5, Switch), +and you have high-level systems everywhere.
+Suggestion: try using a non-portable backend first (e.g. win32 + underlying graphics API) to get +your desktop builds working first. This will get you running faster and get your acquainted with +how Dear ImGui works and is setup. You can then rewrite a custom backend using your own engine API... + +Generally: +It is unlikely you will add value to your project by creating your own backend. + +Also: +The [multi-viewports feature](https://github.com/ocornut/imgui/issues/1542) of the 'docking' branch allows +Dear ImGui windows to be seamlessly detached from the main application window. This is achieved using an +extra layer to the Platform and Renderer backends, which allows Dear ImGui to communicate platform-specific +requests such as: "create an additional OS window", "create a render context", "get the OS position of this +window" etc. See 'ImGuiPlatformIO' for details. +Supporting the multi-viewports feature correctly using 100% of your own abstractions is more difficult +than supporting single-viewport. +If you decide to use unmodified imgui_impl_XXXX.cpp files, you can automatically benefit from +improvements and fixes related to viewports and platform windows without extra work on your side. + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Docs/CHANGELOG.txt`: + +```txt +dear imgui +CHANGELOG + +This document holds the user-facing changelog that we also use in release notes. +We generally fold multiple commits pertaining to the same topic as a single entry. +Changes to backends are also included within the individual .cpp files of each backend. + +RELEASE NOTES: https://github.com/ocornut/imgui/releases +REPORT ISSUES: https://github.com/ocornut/imgui/issues +DISCUSS, ASK QUESTIONS: https://github.com/ocornut/imgui/discussions +FAQ https://www.dearimgui.org/faq/ +WIKI https://github.com/ocornut/imgui/wiki + +WHEN TO UPDATE? + +- Keeping your copy of Dear ImGui updated regularly is recommended. +- It is generally safe to sync to the latest commit in master or docking branches + The library is fairly stable and regressions tends to be fixed fast when reported. + +HOW TO UPDATE? + +- Overwrite every file except imconfig.h (if you have modified it). +- You may also locally branch to modify imconfig.h and merge latest into your branch. +- Read the `Breaking Changes` section (in imgui.cpp or here in the Changelog). +- If you have a problem with a missing function/symbols, search for its name in the code, there will likely be a comment about it. +- If you are dropping this repository in your codebase, please leave the demo and text files in there, they will be useful. +- You may diff your previous Changelog with the one you just copied and read that diff. +- You may enable `IMGUI_DISABLE_OBSOLETE_FUNCTIONS` in imconfig.h to forcefully disable legacy names and symbols. + Doing it every once in a while is a good way to make sure you are not using obsolete symbols. Dear ImGui is in active development, + and API updates have been a little more frequent lately. They are documented below and in imgui.cpp and should not affect all users. +- Please report any issue! + + +----------------------------------------------------------------------- + VERSION 1.87 (Released 2022-02-07) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.87 + +Breaking Changes: + +- Removed support for pre-C++11 compilers. We'll stop supporting VS2010. (#4537) +- Reworked IO mouse input API: (#4921, #4858) [@thedmd, @ocornut] + - Added io.AddMousePosEvent(), io.AddMouseButtonEvent(), io.AddMouseWheelEvent() functions, + obsoleting writing directly to io.MousePos, io.MouseDown[], io.MouseWheel, etc. + - This enable input queue trickling to support low framerates. (#2787, #1992, #3383, #2525, #1320) +- Reworked IO keyboard input API: (#4921, #2625, #3724) [@thedmd, @ocornut] + - Added io.AddKeyEvent() function, obsoleting writing directly to io.KeyMap[], io.KeysDown[] arrays. + - For keyboard modifiers, you can call io.AddKeyEvent() with ImGuiKey_ModXXX values, + obsoleting writing directly to io.KeyCtrl, io.KeyShift etc. + - Added io.SetKeyEventNativeData() function (optional) to pass native and old legacy indices. + - Added full range of key enums in ImGuiKey (e.g. ImGuiKey_F1). + - Added GetKeyName() helper function. + - Obsoleted GetKeyIndex(): it is now unnecessary and will now return the same value. + - All keyboard related functions taking 'int user_key_index' now take 'ImGuiKey key': + - IsKeyDown(), IsKeyPressed(), IsKeyReleased(), GetKeyPressedAmount(). + - Added io.ConfigInputTrickleEventQueue (defaulting to true) to disable input queue trickling. + - Backward compatibility: + - All backends updated to use new functions. + - Old backends populating those arrays should still work! + - Calling e.g. IsKeyPressed(MY_NATIVE_KEY_XXX) will still work! (for a while) + - Those legacy arrays will only be disabled if '#define IMGUI_DISABLE_OBSOLETE_KEYIO' is set in your imconfig. + In a few versions, IMGUI_DISABLE_OBSOLETE_FUNCTIONS will automatically enable IMGUI_DISABLE_OBSOLETE_KEYIO, + so this will be moved into the regular obsolescence path. + - BREAKING: If your custom backend used ImGuiKey as mock native indices (e.g. "io.KeyMap[ImGuiKey_A] = ImGuiKey_A") + this is a use case that will now assert and be breaking for your old backend. + - Transition guide: + - IsKeyPressed(MY_NATIVE_KEY_XXX) -> use IsKeyPressed(ImGuiKey_XXX) + - IsKeyPressed(GetKeyIndex(ImGuiKey_XXX)) -> use IsKeyPressed(ImGuiKey_XXX) + - Backend writing to io.KeyMap[],KeysDown[] -> backend should call io.AddKeyEvent(), if legacy indexing is desired, call io.SetKeyEventNativeData() + - Basically the trick we took advantage of is that we previously only supported native keycode from 0 to 511, + so ImGuiKey values can still express a legacy native keycode, and new named keys are all >= 512. + - This will enable a few things in the future: + - Access to portable keys allows for backend-agnostic keyboard input code. Until now it was difficult + to share code using keyboard across project because of this gap. (#2625, #3724) + - Access to full key ranges will allow us to develop a proper keyboard shortcut system. (#456) + - io.SetKeyEventNativeData() include native keycode/scancode which may later be exposed. (#3141, #2959) +- Reworked IO nav/gamepad input API and unifying inputs sources: (#4921, #4858, #787) + - Added full range of ImGuiKey_GamepadXXXX enums (e.g. ImGuiKey_GamepadDpadUp, ImGuiKey_GamepadR2) to use with + io.AddKeyEvent(), io.AddKeyAnalogEvent(). + - Added io.AddKeyAnalogEvent() function, obsoleting writing directly to io.NavInputs[] arrays. +- Renamed ImGuiKey_KeyPadEnter to ImGuiKey_KeypadEnter to align with new symbols. Kept redirection enum. (#2625) +- Removed support for legacy arithmetic operators (+,+-,*,/) when inputing text into a slider/drag. (#4917, #3184) + This doesn't break any api/code but a feature that was accessible by end-users (which seemingly no one used). + (Instead you may implement custom expression evaluators to provide a better version of this). +- Backends: GLFW: backend now uses glfwSetCursorPosCallback(). + - If calling ImGui_ImplGlfw_InitXXX with install_callbacks=true: nothing to do. is already done for you. + - If calling ImGui_ImplGlfw_InitXXX with install_callbacks=false: you WILL NEED to register the GLFW callback + using glfwSetCursorPosCallback() and forward it to the backend function ImGui_ImplGlfw_CursorPosCallback(). +- Backends: SDL: Added SDL_Renderer* parameter to ImGui_ImplSDL2_InitForSDLRenderer(), so backend can call + SDL_GetRendererOutputSize() to obtain framebuffer size valid for hi-dpi. (#4927) [@Clownacy] +- Commented out redirecting functions/enums names that were marked obsolete in 1.69, 1.70, 1.71, 1.72 (March-July 2019) + - ImGui::SetNextTreeNodeOpen() -> use ImGui::SetNextItemOpen() + - ImGui::GetContentRegionAvailWidth() -> use ImGui::GetContentRegionAvail().x + - ImGui::TreeAdvanceToLabelPos() -> use ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetTreeNodeToLabelSpacing()); + - ImFontAtlas::CustomRect -> use ImFontAtlasCustomRect + - ImGuiColorEditFlags_RGB/HSV/HEX -> use ImGuiColorEditFlags_DisplayRGB/HSV/Hex +- Removed io.ImeSetInputScreenPosFn() in favor of more flexible io.SetPlatformImeDataFn() for IME support. + Because this field was mostly only ever used by Dear ImGui internally, not by backends nor the vast majority + of user code, this should only affect a very small fraction for users who are already very IME-aware. +- Obsoleted 'void* io.ImeWindowHandle' in favor of writing to 'void* ImGuiViewport::PlatformHandleRaw'. + This removes an incompatibility between 'master' and 'multi-viewports' backends and toward enabling + better support for IME. Updated backends accordingly. Because the old field is set by existing backends, + we are keeping it (marked as obsolete). + + +Other Changes: + +- IO: Added event based input queue API, which now trickles events to support low framerates. [@thedmd, @ocornut] + Previously the most common issue case (button presses in low framerates) was handled by backend. This is now + handled by core automatically for all kind of inputs. (#4858, #2787, #1992, #3383, #2525, #1320) + - New IO functions for keyboard/gamepad: AddKeyEvent(), AddKeyAnalogEvent(). + - New IO functions for mouse: AddMousePosEvent(), AddMouseButtonEvent(), AddMouseWheelEvent(). +- IO: Unified key enums allow using key functions on key mods and gamepad values. +- Fixed CTRL+Tab into an empty window causing artifacts on the highlight rectangle due to bad reordering on ImDrawCmd. +- Fixed a situation where CTRL+Tab or Modal can occasionally lead to the creation of ImDrawCmd with zero triangles, + which would makes the draw operation of some backends assert (e.g. Metal with debugging). (#4857) +- Popups: Fixed a regression crash when a new window is created after a modal on the same frame. (#4920) [@rokups] +- Popups: Fixed an issue when reopening a same popup multiple times would offset them by 1 pixel on the right. (#4936) +- Tables, ImDrawListSplitter: Fixed erroneously stripping trailing ImDrawList::AddCallback() when submitted in + last column or last channel and when there are no other drawing operation. (#4843, #4844) [@hoffstadt] +- Tables: Fixed positioning of Sort icon on right-most column with some settings (not resizable + no borders). (#4918). +- Nav: Fixed gamepad navigation in wrapping popups not wrapping all the way. (#4365) +- Sliders, Drags: Fixed text input of values with a leading sign, common when using a format enforcing sign. (#4917) +- Demo: draw a section of keyboard in "Inputs > Keyboard, Gamepad & Navigation state" to visualize keys. [@thedmd] +- Platform IME: changed io.ImeSetInputScreenPosFn() to io.SetPlatformImeDataFn() API, + now taking a ImGuiPlatformImeData structure which we can more easily extend in the future. +- Platform IME: moved io.ImeWindowHandle to GetMainViewport()->PlatformHandleRaw. +- Platform IME: add ImGuiPlatformImeData::WantVisible, hide IME composition window when not used. (#2589) [@actboy168] +- Platform IME: add ImGuiPlatformImeData::InputLineHeight. (#3113) [@liuliu] +- Platform IME: [windows] call ImmSetCandidateWindow() to position candidate window. +- Backends: GLFW: Pass localized keys (matching keyboard layout). Fix e.g. CTRL+A, CTRL+Z, CTRL+Y shortcuts. + We are now converting GLFW untranslated keycodes back to translated keycodes in order to match the behavior of + other backend, and facilitate the use of GLFW with lettered-shortcuts API. (#456, #2625) +- Backends: GLFW: Submit keys and key mods using io.AddKeyEvent(). (#2625, #4921) +- Backends: GLFW: Submit mouse data using io.AddMousePosEvent(), io.AddMouseButtonEvent(), io.AddMouseWheelEvent() functions. (#4921) +- Backends: GLFW: Retrieve mouse position using glfwSetCursorPosCallback() + fallback when focused but not hovered/captured. +- Backends: GLFW: Submit gamepad data using io.AddKeyEvent/AddKeyAnalogEvent() functions, stopped writing to io.NavInputs[]. (#4921) +- Backends: GLFW: Added ImGui_ImplGlfw_InstallCallbacks()/ImGui_ImplGlfw_RestoreCallbacks() helpers to facilitate user installing + callbacks after iniitializing backend. (#4981) +- Backends: Win32: Submit keys and key mods using io.AddKeyEvent(). (#2625, #4921) +- Backends: Win32: Retrieve mouse position using WM_MOUSEMOVE/WM_MOUSELEAVE + fallback when focused but not hovered/captured. +- Backends: Win32: Submit mouse data using io.AddMousePosEvent(), AddMouseButtonEvent(), AddMouseWheelEvent() functions. (#4921) +- Backends: Win32: Maintain a MouseButtonsDown mask instead of using ImGui::IsAnyMouseDown() which will be obsoleted. +- Backends: Win32: Submit gamepad data using io.AddKeyEvent/AddKeyAnalogEvent() functions, stopped writing to io.NavInputs[]. (#4921) +- Backends: SDL: Pass localized keys (matching keyboard layout). Fix e.g. CTRL+A, CTRL+Z, CTRL+Y shortcuts. (#456, #2625) +- Backends: SDL: Submit key data using io.AddKeyEvent(). Submit keymods using io.AddKeyModsEvent() at the same time. (#2625) +- Backends: SDL: Retrieve mouse position using SDL_MOUSEMOTION/SDL_WINDOWEVENT_LEAVE + fallback when focused but not hovered/captured. +- Backends: SDL: Submit mouse data using io.AddMousePosEvent(), AddMouseButtonEvent(), AddMouseWheelEvent() functions. (#4921) +- Backends: SDL: Maintain a MouseButtonsDown mask instead of using ImGui::IsAnyMouseDown() which will be obsoleted. +- Backends: SDL: Submit gamepad data using io.AddKeyEvent/AddKeyAnalogEvent() functions, stopped writing to io.NavInputs[]. (#4921) +- Backends: Allegro5: Submit keys using io.AddKeyEvent(). Submit keymods using io.AddKeyModsEvent() at the same time. (#2625) +- Backends: Allegro5: Submit mouse data using io.AddMousePosEvent(), AddMouseButtonEvent(), AddMouseWheelEvent() functions. (#4921) +- Backends: OSX: Submit keys using io.AddKeyEvent(). Submit keymods using io.AddKeyModsEvent() at the same time. (#2625) +- Backends: OSX: Submit mouse data using io.AddMousePosEvent(), AddMouseButtonEvent(), AddMouseWheelEvent() functions. (#4921) +- Backends: OSX: Submit gamepad data using io.AddKeyEvent/AddKeyAnalogEvent() functions, stopped writing to io.NavInputs[]. (#4921) +- Backends: OSX: Added basic Platform IME support. (#3108, #2598) [@liuliu] +- Backends: OSX: Fix Game Controller nav mapping to use shoulder for both focusing and tweak speed. (#4759) +- Backends: OSX: Fix building with old Xcode versions that are missing gamepad features. [@rokups] +- Backends: OSX: Forward keyDown/keyUp events to OS when unused by Dear ImGui. +- Backends: Android, GLUT: Submit keys using io.AddKeyEvent(). Submit keymods using io.AddKeyModsEvent() at the same time. (#2625) +- Backends: Android, GLUT: Submit mouse data using io.AddMousePosEvent(), AddMouseButtonEvent(), AddMouseWheelEvent() functions. (#4858) +- Backends: OpenGL3: Fixed a buffer overflow in imgui_impl_opengl3_loader.h init (added in 1.86). (#4468, #4830) [@dymk] + It would generally not have noticeable side-effect at runtime but would be detected by runtime checkers. +- Backends: OpenGL3: Fix OpenGL ES2 includes on Apple systems. [@rokups] +- Backends: Metal: Added Apple Metal C++ API support. (#4824, #4746) [@luigifcruz] + Enable with '#define IMGUI_IMPL_METAL_CPP' in your imconfig.h file. +- Backends: Metal: Ignore ImDrawCmd where ElemCount == 0, which are normally not emitted by the library but + can theoretically be created by user code manipulating a ImDrawList. (#4857) +- Backends: Vulkan: Added support for ImTextureID as VkDescriptorSet, add ImGui_ImplVulkan_AddTexture(). (#914) [@martty] +- Backends: SDL_Renderer: Fix texture atlas format on big-endian hardware (#4927) [@Clownacy] +- Backends: WebGPU: Fixed incorrect size parameters in wgpuRenderPassEncoderSetIndexBuffer() and + wgpuRenderPassEncoderSetVertexBuffer() calls. (#4891) [@FeepsDev] + + +----------------------------------------------------------------------- + VERSION 1.86 (Released 2021-12-22) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.86 + +Breaking Changes: + +- Removed CalcListClipping() function. Prefer using ImGuiListClipper which can return non-contiguous ranges. + Please open an issue if you think you really need this function. (#3841) +- Backends: OSX: Added NSView* parameter to ImGui_ImplOSX_Init(). (#4759) [@stuartcarnie] +- Backends: Marmalade: Removed obsolete Marmalade backend (imgui_impl_marmalade.cpp) + example app. (#368, #375) + Find last supported version at https://github.com/ocornut/imgui/wiki/Bindings + +Other Changes: + +- Added an assertion for the common user mistake of using "" as an identifier at the root level of a window + instead of using "##something". Empty identifiers are valid and useful in a very small amount of cases, + but 99.9% of the time if you need an empty label you should use "##something". (#1414, #2562, #2807, #4008, + #4158, #4375, #4548, #4657, #4796). READ THE FAQ ABOUT HOW THE ID STACK WORKS -> https://dearimgui.org/faq +- Added GetMouseClickedCount() function, returning the number of successive clicks. (#3229) [@kudaba] + (so IsMouseDoubleClicked(ImGuiMouseButton_Left) is same as GetMouseClickedCount(ImGuiMouseButton_Left) == 2, + but it allows testing for triple clicks and more). +- Modals: fixed issue hovering popups inside a child windows inside a modal. (#4676, #4527) +- Modals, Popups, Windows: changes how appearing windows are interrupting popups and modals. (#4317) [@rokups] + - appearing windows created from within the begin stack of a popup/modal will no longer close it. + - appearing windows created not within the begin stack of a modal will no longer close the modal, + and automatically appear behind it. +- Fixed IsWindowFocused()/IsWindowHovered() issues with child windows inside popups. (#4676) +- Nav: Ctrl+tabbing to cycle through windows is now enabled regardless of using the _NavEnableKeyboard + configuration flag. This is part of an effort to generalize the use of keyboard inputs. (#4023, #787). + Note that while this is active you can also moving windows (with arrow) and resize (shift+arrows). +- Nav: tabbing now cycles through clipped items and scroll accordingly. (#4449) +- Nav: pressing PageUp/PageDown/Home/End when in Menu layer automatically moves back to Main layer. +- Nav: fixed resizing window from borders setting navigation to Menu layer. +- Nav: prevent child from clipping items when using _NavFlattened and parent has a pending request. +- Nav: pressing Esc to exit a child window reactivates the Nav highlight if it was disabled by mouse. +- Nav: with ImGuiConfigFlags_NavEnableSetMousePos enabled: Fixed absolute mouse position when using + Home/End leads to scrolling. Fixed not setting mouse position when a failed move request (e.g. when + already at edge) reactivates the navigation highlight. +- Menus: fixed closing a menu inside a popup/modal by clicking on the popup/modal. (#3496, #4797) +- Menus: fixed closing a menu by clicking on its menu-bar item when inside a popup. (#3496, #4797) [@xndcn] +- Menus: fixed menu inside a popup/modal not inhibiting hovering of items in the popup/modal. (#3496, #4797) +- Menus: fixed sub-menu items inside a popups from closing the popup. +- Menus: fixed top-level menu from not consistently using style.PopupRounding. (#4788) +- InputText, Nav: fixed repeated calls to SetKeyboardFocusHere() preventing to use InputText(). (#4682) +- Inputtext, Nav: fixed using SetKeyboardFocusHere() on InputTextMultiline(). (#4761) +- InputText: made double-click select word, triple-line select line. Word delimitation logic differs + slightly from the one used by CTRL+arrows. (#2244) +- InputText: fixed ReadOnly flag preventing callbacks from receiving the text buffer. (#4762) [@actondev] +- InputText: fixed Shift+Delete from not cutting into clipboard. (#4818, #1541) [@corporateshark] +- InputTextMultiline: fixed incorrect padding when FrameBorder > 0. (#3781, #4794) +- InputTextMultiline: fixed vertical tracking with large values of FramePadding.y. (#3781, #4794) +- Separator: fixed cover all columns while called inside a table. (#4787) +- Clipper: currently focused item is automatically included in clipper range. + Fixes issue where e.g. drag and dropping an item and scrolling ensure the item source location is + still submitted. (#3841, #1725) [@GamingMinds-DanielC, @ocornut] +- Clipper: added ForceDisplayRangeByIndices() to force a given item (or several) to be stepped out + during a clipping operation. (#3841) [@@GamingMinds-DanielC] +- Clipper: rework so gamepad/keyboard navigation doesn't create spikes in number of items requested + by the clipper to display. (#3841) +- Clipper: fixed content height declaration slightly mismatching the value of when not using a clipper. + (an additional ItemSpacing.y was declared, affecting scrollbar range). +- Clipper: various and incomplete changes to tame down scrolling and precision issues on very large ranges. + Passing an explicit height to the clipper now allows larger ranges. (#3609, #3962). +- Clipper: fixed invalid state when number of frozen table row is smaller than ItemCount. +- Drag and Drop: BeginDragDropSource() with ImGuiDragDropFlags_SourceAllowNullID doesn't lose + tooltip when scrolling. (#143) +- Fonts: fixed infinite loop in ImFontGlyphRangesBuilder::AddRanges() when passing UINT16_MAX or UINT32_MAX + without the IMGUI_USE_WCHAR32 compile-time option. (#4802) [@SlavicPotato] +- Metrics: Added a node showing windows in submission order and showing the Begin() stack. +- Misc: Added missing ImGuiMouseCursor_NotAllowed cursor for software rendering (when the + io.MouseDrawCursor flag is enabled). (#4713) [@nobody-special666] +- Misc: Fixed software mouse cursor being rendered multiple times if Render() is called more than once. +- Misc: Fix MinGW DLL build issue (when IMGUI_API is defined). [@rokups] +- CI: Add MinGW DLL build to test suite. [@rokups] +- Backends: Vulkan: Call vkCmdSetScissor() at the end of render with a full-viewport to reduce + likehood of issues with people using VK_DYNAMIC_STATE_SCISSOR in their app without calling + vkCmdSetScissor() explicitly every frame. (#4644) +- Backends: OpenGL3: Using buffer orphaning + glBufferSubData(), seems to fix leaks with multi-viewports + with some Intel HD drivers, and perhaps improve performances. (#4468, #4504, #2981, #3381) [@parbo] +- Backends: OpenGL2, Allegro5, Marmalade: Fixed mishandling of the ImDrawCmd::IdxOffset field. + This is an old bug, but due to the way we created drawlists, it never had any visible side-effect before. + The new code for handling Modal and CTRL+Tab dimming/whitening recently made the bug surface. (#4790) +- Backends: Win32: Store left/right variants of Ctrl/Shift/Alt mods in KeysDown[] array. (#2625) [@thedmd] +- Backends: DX12: Fixed DRAW_EMPTY_SCISSOR_RECTANGLE warnings. (#4775) +- Backends: SDL_Renderer: Added support for large meshes (64k+ vertices) with 16-bit indices, + enabling 'ImGuiBackendFlags_RendererHasVtxOffset' in the backend. (#3926) [@rokups] +- Backends: SDL_Renderer: Fix for SDL 2.0.19+ RenderGeometryRaw() API signature change. (#4819) [@sridenour] +- Backends: OSX: Generally fix keyboard support. Keyboard arrays indexed using kVK_* codes, e.g. + ImGui::IsKeyPressed(kVK_Space). Don't set mouse cursor shape unconditionally. Handle two fingers scroll + cancel event. (#4759, #4253, #1873) [@stuartcarnie] +- Backends: OSX: Add Game Controller support (need linking GameController framework) (#4759) [@stuartcarnie] +- Backends: WebGPU: Passing explicit buffer sizes to wgpuRenderPassEncoderSetVertexBuffer() and + wgpuRenderPassEncoderSetIndexBuffer() functions as validation layers appears to not do what the + in-flux specs says. (#4766) [@meshula] + + +----------------------------------------------------------------------- + VERSION 1.85 (Released 2021-10-12) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.85 + +This is the last release officially supporting C++03 and Visual Studio 2008/2010. (#4537) +We expect that the next release will require a subset of the C++11 language (VS 2012~, GCC 4.8.1, Clang 3.3). +We may use some C++11 language features but we will not use any C++ library headers. +If you are stuck on ancient compiler you may need to stay at this version onward. + +Breaking Changes: + +- Removed GetWindowContentRegionWidth() function. keep inline redirection helper. + Can use 'GetWindowContentRegionMax().x - GetWindowContentRegionMin().x' instead but it's not + very useful in practice, and the only use of it in the demo was illfit. + Using 'GetContentRegionAvail().x' is generally a better choice. + +Other Changes: + +- Debug: Stack Tool: Added "Stack Tool" available in "Demo->Tools->Stack Tool", "Metrics->Tools", + or by calling the ShowStackToolWindow() function. The tool run queries on hovered id to display + details about individual components that were hashed to create an ID. It helps understanding + the ID stack system and debugging potential ID collisions. (#4631) [@ocornut, @rokups] +- Windows: Fixed background order of overlapping childs submitted sequentially. (#4493) +- IsWindowFocused: Added ImGuiFocusedFlags_NoPopupHierarchy flag allowing to exclude child popups + from the tested windows when combined with _ChildWindows. +- IsWindowHovered: Added ImGuiHoveredFlags_NoPopupHierarchy flag allowing to exclude child popups + from the tested windows when combined with _ChildWindows. +- InputTextMultiline: Fixed label size not being included into window contents rect unless + the whole widget is clipped. +- InputText: Allow activating/cancelling/validating input with gamepad nav events. (#2321, #4552) +- InputText: Fixed selection rectangle appearing one frame late when selecting all. +- TextUnformatted: Accept null ranges including (NULL,NULL) without asserting, in order to conform + to common idioms (e.g. passing .data(), .data() + .size() from a null string). (#3615) +- Disabled: Added assert guard for mismatching BeginDisabled()/EndDisabled() blocks. (#211) +- Nav: Fixed using SetKeyboardFocusHere() on non-visible/clipped items. It now works and will scroll + toward the item. When called during a frame where the parent window is appearing, scrolling will + aim to center the item in the window. When calling during a frame where the parent window is already + visible, scrolling will aim to scroll as little as possible to make the item visible. We will later + expose scroll functions and flags in public API to select those behaviors. (#343, #4079, #2352) +- Nav: Fixed using SetKeyboardFocusHere() from activating a different item on the next frame if + submitted items have changed during that frame. (#432) +- Nav: Fixed toggling menu layer with Alt or exiting menu layer with Esc not moving mouse when + the ImGuiConfigFlags_NavEnableSetMousePos config flag is set. +- Nav: Fixed a few widgets from not setting reference keyboard/gamepad navigation ID when + activated with mouse. More specifically: BeginTabItem(), the scrolling arrows of BeginTabBar(), + the arrow section of TreeNode(), the +/- buttons of InputInt()/InputFloat(), Selectable() with + ImGuiSelectableFlags_SelectOnRelease. More generally: any direct use of ButtonBehavior() with + the PressedOnClick/PressedOnDoubleClick/PressedOnRelease button policy. +- Nav: Fixed an issue with losing focus on docked windows when pressing Alt while keyboard navigation + is disabled. (#4547, #4439) [@PathogenDavid] +- Nav: Fixed vertical scoring offset when wrapping on Y in a decorated window. +- Nav: Improve scrolling behavior when navigating to an item larger than view. +- TreePush(): removed unnecessary/inconsistent legacy behavior where passing a NULL value to + the TreePush(const char*) and TreePush(const void*) functions would use an hard-coded replacement. + The only situation where that change would make a meaningful difference is TreePush((const char*)NULL) + (_explicitly_ casting a null pointer to const char*), which is unlikely and will now crash. + You may replace it with anything else. +- ColorEdit4: Fixed not being able to change hue when saturation is 0. (#4014) [@rokups] +- ColorEdit4: Fixed hue resetting to 0 when it is set to 255. [@rokups] +- ColorEdit4: Fixed hue value jitter when source color is stored as RGB in 32-bit integer and perform + RGB<>HSV round trips every frames. [@rokups] +- ColorPicker4: Fixed picker being unable to select exact 1.0f color when dragging toward the edges + of the SV square (previously picked 0.999989986f). (#3517) [@rokups] +- Menus: Fixed vertical alignments of MenuItem() calls within a menu bar (broken in 1.84). (#4538) +- Menus: Improve closing logic when moving diagonally in empty between between parent and child menus to + accommodate for varying font size and dpi. +- Menus: Fixed crash when navigating left inside a child window inside a sub-menu. (#4510). +- Menus: Fixed an assertion happening in some situations when closing nested menus (broken in 1.83). (#4640) +- Drag and Drop: Fixed using BeginDragDropSource() inside a BeginChild() that returned false. (#4515) +- PlotHistogram: Fixed zero-line position when manually specifying min<0 and max>0. (#4349) [@filippocrocchini] +- Misc: Added asserts for missing PopItemFlag() calls. +- Misc: Fixed printf-style format checks on Clang+MinGW. (#4626, #4183, #3592) [@guusw] +- IO: Added 'io.WantCaptureMouseUnlessPopupClose' alternative to `io.WantCaptureMouse'. (#4480) + This allows apps to receive the click on void when that click is used to close popup (by default, + clicking on a void when a popup is open will close the popup but not release io.WantCaptureMouse). +- Fonts: imgui_freetype: Fixed crash when FT_Render_Glyph() fails to render a glyph and returns NULL + (which apparently happens with Freetype 2.11). (#4394, #4145?). +- Fonts: Fixed ImFontAtlas::ClearInputData() marking atlas as not built. (#4455, #3487) +- Backends: Added more implicit asserts to detect invalid/redundant calls to Shutdown functions. (#4562) +- Backends: OpenGL3: Fixed our custom GL loader conflicting with user using GL3W. (#4445) [@rokups] +- Backends: WebGPU: Fixed for latest specs. (#4472, #4512) [@Kangz, @bfierz] +- Backends: SDL_Renderer: Added SDL_Renderer backend compatible with upcoming SDL 2.0.18. (#3926) [@1bsyl] +- Backends: Metal: Fixed a crash when clipping rect larger than framebuffer is submitted via + a direct unclipped PushClipRect() call. (#4464) +- Backends: OSX: Use mach_absolute_time as CFAbsoluteTimeGetCurrent can jump backwards. (#4557, #4563) [@lfnoise] +- Backends: All renderers: Normalize clipping rect handling across backends. (#4464) +- Examples: Added SDL + SDL_Renderer example in "examples/example_sdl_sdlrenderer/" folder. (#3926) [@1bsyl] + + +----------------------------------------------------------------------- + VERSION 1.84.2 (Released 2021-08-23) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.84.2 + +- Disabled: Fixed nested BeginDisabled()/EndDisabled() calls. (#211, #4452, #4453, #4462) [@Legulysse] +- Backends: OpenGL3: OpenGL: Fixed ES 3.0 shader ("#version 300 es") to use normal precision + floats. Avoid wobbly rendering at HD resolutions. (#4463) [@nicolasnoble] + + +----------------------------------------------------------------------- + VERSION 1.84.1 (Released 2021-08-20) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.84.1 + +- Disabled: Fixed BeginDisabled(false) - BeginDisabled(true) was working. (#211, #4452, #4453) + + +----------------------------------------------------------------------- + VERSION 1.84 (Released 2021-08-20) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.84 + +Breaking Changes: + +- Commented out redirecting functions/enums names that were marked obsolete in 1.67 and 1.69 (March 2019): + - ImGui::GetOverlayDrawList() -> use ImGui::GetForegroundDrawList() + - ImFont::GlyphRangesBuilder -> use ImFontGlyphRangesBuilder +- Backends: OpenGL3: added a third source file "imgui_impl_opengl3_loader.h". [@rokups] +- Backends: GLFW: backend now uses glfwSetCursorEnterCallback(). (#3751, #4377, #2445) +- Backends: GLFW: backend now uses glfwSetWindowFocusCallback(). (#4388) [@thedmd] + - If calling ImGui_ImplGlfw_InitXXX with install_callbacks=true: this is already done for you. + - If calling ImGui_ImplGlfw_InitXXX with install_callbacks=false: you WILL NEED to register the GLFW callbacks + and forward them to the backend: + - Register glfwSetCursorEnterCallback, forward events to ImGui_ImplGlfw_CursorEnterCallback(). + - Register glfwSetWindowFocusCallback, forward events to ImGui_ImplGlfw_WindowFocusCallback(). +- Backends: SDL2: removed unnecessary SDL_Window* parameter from ImGui_ImplSDL2_NewFrame(). (#3244) [@funchal] + Kept inline redirection function (will obsolete). +- Backends: SDL2: backend needs to set 'SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")' in order to + receive mouse clicks events on window focus, otherwise SDL doesn't emit the event. (#3751, #4377, #2445) + This is unfortunately a global SDL setting, so enabling it _might_ have a side-effect on your application. + It is unlikely to make a difference, but if your app absolutely needs to ignore the initial on-focus click: + you can ignore SDL_MOUSEBUTTONDOWN events coming right after a SDL_WINDOWEVENT_FOCUS_GAINED event). +- Internals: (for custom widgets): because disabled items now sets HoveredId, if you want custom widgets to + not react as hovered when disabled, in the majority of use cases it is preferable to check the "hovered" + return value of ButtonBehavior() rather than (HoveredId == id). + +Other Changes: + +- IO: Added io.AddFocusEvent() api for backend to tell when host window has gained/lost focus. (#4388) [@thedmd] + If you use a custom backend, consider adding support for this! +- Disabled: added BeginDisabled()/EndDisabled() api to create a scope where interactions are disabled. (#211) + - Added style.DisabledAlpha (default to 0.60f) and ImGuiStyleVar_DisabledAlpha. (#211) + - Unlike the internal-and-undocumented-but-somehow-known PushItemFlag(ImGuiItemFlags_Disabled), this also alters + visuals. Currently this is done by lowering alpha of all widgets. Future styling system may do that differently. + - Disabled items set HoveredId, allowing e.g. HoveredIdTimer to run. (#211, #3419) [@rokups] + - Disabled items more consistently release ActiveId if the active item got disabled. (#211) + - Nav: Fixed disabled items from being candidate for default focus. (#211, #787) + - Fixed Selectable() selection not showing when disabled. (#211) + - Fixed IsItemHovered() returning true on disabled item when navigated to. (#211) + - Fixed IsItemHovered() when popping disabled state after item, or when using Selectable_Disabled. (#211) +- Windows: ImGuiWindowFlags_UnsavedDocument/ImGuiTabItemFlags_UnsavedDocument displays a dot instead of a '*' so it + is independent from font style. When in a tab, the dot is displayed at the same position as the close button. + Added extra comments to clarify the purpose of this flag in the context of docked windows. +- Tables: Added ImGuiTableColumnFlags_Disabled acting a master disable over (hidden from user/context menu). (#3935) +- Tables: Clarified that TableSetColumnEnabled() requires the table to use the ImGuiTableFlags_Hideable flag, + because it manipulates the user-accessible show/hide state. (#3935) +- Tables: Added ImGuiTableColumnFlags_NoHeaderLabel to request TableHeadersRow() to not submit label for a column. + Convenient for some small columns. Name will still appear in context menu. (#4206). +- Tables: Fixed columns order on TableSetupScrollFreeze() if previous data got frozen columns out of their section. +- Tables: Fixed invalid data in TableGetSortSpecs() when SpecsDirty flag is unset. (#4233) +- TabBar: Fixed using more than 32 KB-worth of tab names. (#4176) +- InputInt/InputFloat: When used with Steps values and _ReadOnly flag, the step button look disabled. (#211) +- InputText: Fixed named filtering flags disabling newline or tabs in multiline inputs (#4409, #4410) [@kfsone] +- Drag and Drop: drop target highlight doesn't try to bypass host clipping rectangle. (#4281, #3272) +- Drag and Drop: Fixed using AcceptDragDropPayload() with ImGuiDragDropFlags_AcceptNoPreviewTooltip. [@JeffM2501] +- Menus: MenuItem() and BeginMenu() are not affected/overlapping when style.SelectableTextAlign is altered. +- Menus: Fixed hovering a disabled menu or menu item not closing other menus. (#211) +- Popups: Fixed BeginPopup/OpenPopup sequence failing when there are no focused windows. (#4308) [@rokups] +- Nav: Alt doesn't toggle menu layer if other modifiers are held. (#4439) +- Fixed printf-style format checks on non-MinGW flavors. (#4183, #3592) +- Fonts: Functions with a 'float size_pixels' parameter can accept zero if it is set in ImFontSize::SizePixels. +- Fonts: Prefer using U+FFFD character for fallback instead of '?', if available. (#4269) +- Fonts: Use U+FF0E dot character to construct an ellipsis if U+002E '.' is not available. (#4269) +- Fonts: Added U+FFFD ("replacement character") to default asian glyphs ranges. (#4269) +- Fonts: Fixed calling ClearTexData() (clearing CPU side font data) triggering an assert in NewFrame(). (#3487) +- DrawList: Fixed AddCircle/AddCircleFilled() with auto-tesselation not using accelerated paths for small circles. + Fixed AddCircle/AddCircleFilled() with 12 segments which had a broken edge. (#4419, #4421) [@thedmd] +- Demo: Fixed requirement in 1.83 to link with imgui_demo.cpp if IMGUI_DISABLE_METRICS_WINDOW is not set. (#4171) + Normally the right way to disable compiling the demo is to set IMGUI_DISABLE_DEMO_WINDOWS, but we want to avoid + implying that the file is required. +- Metrics: Fixed a crash when inspecting the individual draw command of a foreground drawlist. [@rokups] +- Backends: Reorganized most backends (Win32, SDL, GLFW, OpenGL2/3, DX9/10/11/12, Vulkan, Allegro) to pull their + data from a single structure stored inside the main Dear ImGui context. This facilitate/allow usage of standard + backends with multiple-contexts BUT is only partially tested and not well supported. It is generally advised to + instead use the multi-viewports feature of docking branch where a single Dear ImGui context can be used across + multiple windows. (#586, #1851, #2004, #3012, #3934, #4141) +- Backends: Win32: Rework to handle certain Windows 8.1/10 features without a manifest. (#4200, #4191) + - ImGui_ImplWin32_GetDpiScaleForMonitor() will handle per-monitor DPI on Windows 10 without a manifest. + - ImGui_ImplWin32_EnableDpiAwareness() will call SetProcessDpiAwareness() fallback on Windows 8.1 without a manifest. +- Backends: Win32: IME functions are disabled by default for non-Visual Studio compilers (MinGW etc.). Enable with + '#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS' for those compilers. Undo change from 1.82. (#2590, #738, #4185, #4301) +- Backends: Win32: Mouse position is correctly reported when the host window is hovered but not focused. (#2445, #2696, #3751, #4377) +- Backends: Win32, SDL2, GLFW, OSX, Allegro: now calling io.AddFocusEvent() on focus gain/loss. (#4388) [@thedmd] + This allow us to ignore certain inputs on focus loss (previously relied on mouse loss but backends are now + reporting mouse even when host window is unfocused, as per #2445, #2696, #3751, #4377) +- Backends: Fixed keyboard modifiers being reported when host window doesn't have focus. (#2622) +- Backends: GLFW: Mouse position is correctly reported when the host window is hovered but not focused. (#3751, #4377, #2445) + (backend now uses glfwSetCursorEnterCallback(). If you called ImGui_ImplGlfw_InitXXX with install_callbacks=false, you will + need to install this callback and forward the data to the backend via ImGui_ImplGlfw_CursorEnterCallback). +- Backends: SDL2: Mouse position is correctly reported when the host window is hovered but not focused. (#3751, #4377, #2445) + (enabled with SDL 2.0.5+ as SDL_GetMouseFocus() is only usable with SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH). +- Backends: DX9: Explicitly disable texture state stages after >= 1. (#4268) [@NZJenkins] +- Backends: DX12: Fix texture casting crash on 32-bit systems (introduced on 2021/05/19 and v1.83) + added comments + about building on 32-bit systems. (#4225) [@kingofthebongo2008] +- Backends: OpenGL3: Embed our own minimal GL headers/loader (imgui_impl_opengl3_loader.h) based on gl3w. + Reduces the frequent issues and confusion coming from having to support multiple loaders and requiring users to use and + initialize the same loader as the backend. [@rokups] + Removed support for gl3w, glew, glad, glad2, glbinding2, glbinding3 (all now unnecessary). +- Backends: OpenGL3: Handle GL_CLIP_ORIGIN on <4.5 contexts if "GL_ARB_clip_control" extension is detected. (#4170, #3998) +- Backends: OpenGL3: Destroy vertex/fragment shader objects right after they are linked into main shader. (#4244) [@Crowbarous] +- Backends: OpenGL3: Use OES_vertex_array extension on Emscripten + backup/restore current state. (#4266, #4267) [@harry75369] +- Backends: GLFW: Installing and exposed ImGui_ImplGlfw_MonitorCallback() for forward compatibility with docking branch. +- Backends: OSX: Added a fix for shortcuts using CTRL key instead of CMD key. (#4253) [@rokups] +- Examples: DX12: Fixed handling of Alt+Enter in example app (using swapchain's ResizeBuffers). (#4346) [@PathogenDavid] +- Examples: DX12: Removed unnecessary recreation of backend-owned device objects when window is resized. (#4347) [@PathogenDavid] +- Examples: OpenGL3+GLFW,SDL: Remove include cruft to support variety of GL loaders (no longer necessary). [@rokups] +- Examples: OSX+OpenGL2: Fix event forwarding (fix key remaining stuck when using shortcuts with Cmd/Super key). + Other OSX examples were not affected. (#4253, #1873) [@rokups] +- Examples: Updated all .vcxproj to VS2015 (toolset v140) to facilitate usage with vcpkg. +- Examples: SDL2: Accommodate for vcpkg install having headers in SDL2/SDL.h vs SDL.h. + + +----------------------------------------------------------------------- + VERSION 1.83 (Released 2021-05-24) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.83 + +Breaking Changes: + +- Backends: Obsoleted direct access to ImDrawCmd::TextureId in favor of calling ImDrawCmd::GetTexID(). (#3761) [@thedmd] + - If you are using official backends from the source tree: you have nothing to do. + - If you copied old backend code or using your own: change access to draw_cmd->TextureId to draw_cmd->GetTexID(). + Why are we doing this? + - This change will be required in the future when adding support for incremental texture atlas updates. + - Please note this won't break soon, but we are making the change ahead of time. + +Other Changes: + +- Scrolling: Fix scroll tracking with e.g. SetScrollHereX/Y() when WindowPadding < ItemSpacing. +- Scrolling: Fix scroll snapping on edge of scroll region when both scrollbars are enabled. +- Scrolling: Fix mouse wheel axis swap when using SHIFT on macOS (system already does it). (#4010) +- Window: Fix IsWindowAppearing() from returning true twice in most cases. (#3982, #1497, #1061) +- Nav: Fixed toggling menu layer while an InputText() is active not stealing active id. (#787) +- Nav: Fixed pressing Escape to leave menu layer while in a popup or child window. (#787) +- Nav, InputText: Fixed accidental menu toggling while typing non-ascii characters using AltGR. [@rokups] (#370) +- Nav: Fixed using SetItemDefaultFocus() on windows with _NavFlattened flag. (#787) +- Nav: Fixed Tabbing initial activation from skipping the first item if it is tabbable through. (#787) +- Nav: Fixed fast CTRL+Tab (where keys are only held for one single frame) from properly enabling the + menu layer of target window if it doesn't have other active layers. +- Tables: Expose TableSetColumnEnabled() in public api. (#3935) +- Tables: Better preserve widths when columns count changes. (#4046) +- Tables: Sharing more memory buffers between tables, reducing general memory footprints. (#3740) +- TabBar: Fixed mouse reordering with very fast movements (e.g. crossing multiple tabs in a single + frame and then immediately standing still (would only affect automation/bots). [@rokups] +- Menus: made MenuItem() in a menu bar reflect the 'selected' argument with a highlight. (#4128) [@mattelegende] +- Drags, Sliders, Inputs: Specifying a NULL format to Float functions default them to "%.3f" to be + consistent with the compile-time default. (#3922) +- DragScalar: Add default value for v_speed argument to match higher-level functions. (#3922) [@eliasdaler] +- ColorEdit4: Alpha default to 255 (instead of 0) when omitted in hex input. (#3973) [@squadack] +- InputText: Fix handling of paste failure (buffer full) which in some cases could corrupt the undo stack. (#4038) + (fix submitted to https://github.com/nothings/stb/pull/1158) [@Unit2Ed, @ocornut] +- InputText: Do not filter private unicode codepoints (e.g. icons) when pasted from clipboard. (#4005) [@dougbinks] +- InputText: Align caret/cursor to pixel coordinates. (#4080) [@elvissteinjr] +- InputText: Fixed CTRL+Arrow or OSX double-click leaking the presence of spaces when ImGuiInputTextFlags_Password + is used. (#4155, #4156) [@michael-swan] +- LabelText: Fixed clipping of multi-line value text when label is single-line. (#4004) +- LabelText: Fixed vertical alignment of single-line value text when label is multi-line. (#4004) +- Combos: Changed the combo popup to use a different id to also using a context menu with the default item id. + Fixed using BeginPopupContextItem() with no parameter after a combo. (#4167) +- Popups: Added 'OpenPopup(ImGuiID id)' overload to facilitate calling from nested stacks. (#3993, #331) [@zlash] +- Tweak computation of io.Framerate so it is less biased toward high-values in the first 120 frames. (#4138) +- Optimization: Disabling some of MSVC most aggressive Debug runtime checks for some simple/low-level functions + (e.g. ImVec2, ImVector) leading to a 10-20% increase of performances with MSVC "default" Debug settings. +- ImDrawList: Add and use SSE-enabled ImRsqrt() in place of 1.0f / ImSqrt(). (#4091) [@wolfpld] +- ImDrawList: Fixed/improved thickness of thick strokes with sharp angles. (#4053, #3366, #2964, #2868, #2518, #2183) + Effectively introduced a regression in 1.67 (Jan 2019), and a fix in 1.70 (Apr 2019) but the fix wasn't actually on + par with original version. Now incorporating the correct revert. +- ImDrawList: Fixed PathArcTo() regression from 1.82 preventing use of counter-clockwise angles. (#4030, #3491) [@thedmd] +- Demo: Improved popups demo and comments. +- Metrics: Added "Fonts" section with same information as available in "Style Editor">"Fonts". +- Backends: SDL2: Rework global mouse pos availability check listing supported platforms explicitly, + effectively fixing mouse access on Raspberry Pi. (#2837, #3950) [@lethal-guitar, @hinxx] +- Backends: Win32: Clearing keyboard down array when losing focus (WM_KILLFOCUS). (#2062, #3532, #3961) + [@1025798851] +- Backends: OSX: Fix keys remaining stuck when CMD-tabbing to a different application. (#3832) [@rokups] +- Backends: DirectX9: calling IDirect3DStateBlock9::Capture() after CreateStateBlock() which appears to + workaround/fix state restoring issues. Unknown exactly why so, bit of a cargo-cult fix. (#3857) +- Backends: DirectX9: explicitly setting up more graphics states to increase compatibility with unusual + non-default states. (#4063) +- Backends: DirectX10, DirectX11: fixed a crash when backing/restoring state if nothing is bound when + entering the rendering function. (#4045) [@Nemirtingas] +- Backends: GLFW: Adding bound check in KeyCallback because GLFW appears to send -1 on some setups. [#4124] +- Backends: Vulkan: Fix mapped memory Vulkan validation error when buffer sizes are not multiple of + VkPhysicalDeviceLimits::nonCoherentAtomSize. (#3957) [@AgentX1994] +- Backends: WebGPU: Update to latest specs (Chrome Canary 92 and Emscripten 2.0.20). (#4116, #3632) [@bfierz, @Kangz] +- Backends: OpenGL3: Don't try to read GL_CLIP_ORIGIN unless we're OpenGL 4.5. (#3998, #2366, #2186) [@s7jones] +- Examples: OpenGL: Add OpenGL ES 2.0 support to modern GL examples. (#2837, #3951) [@lethal-guitar, @hinxx] +- Examples: Vulkan: Rebuild swapchain on VK_SUBOPTIMAL_KHR. (#3881) +- Examples: Vulkan: Prefer using discrete GPU if there are more than one available. (#4012) [@rokups] +- Examples: SDL2: Link with shell32.lib required by SDL2main.lib since SDL 2.0.12. [#3988] +- Examples: Android: Make Android example build compatible with Gradle 7.0. (#3446) +- Docs: Improvements to description of using colored glyphs/emojis. (#4169, #3369) +- Docs: Improvements to minor mistakes in documentation comments (#3923) [@ANF-Studios] + + +----------------------------------------------------------------------- + VERSION 1.82 (Released 2021-02-15) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.82 + +Breaking Changes: + +- Removed redirecting functions/enums names that were marked obsolete in 1.66 (September 2018): + - ImGui::SetScrollHere() --> use ImGui::SetScrollHereY() +- ImDrawList: upgraded AddPolyline()/PathStroke()'s "bool closed" parameter to use "ImDrawFlags flags". + - bool closed = false --> use ImDrawFlags_None, or 0 + - bool closed = true --> use ImDrawFlags_Closed + The matching ImDrawFlags_Closed value is guaranteed to always stay == 1 in the future. + Difference may not be noticeable for most but zealous type-checking tools may report a need to change. +- ImDrawList: upgraded AddRect(), AddRectFilled(), PathRect() to use ImDrawFlags instead of ImDrawCornersFlags. + - ImDrawCornerFlags_TopLeft --> use ImDrawFlags_RoundCornersTopLeft + - ImDrawCornerFlags_BotRight --> use ImDrawFlags_RoundCornersBottomRight + - ImDrawCornerFlags_None --> use ImDrawFlags_RoundCornersNone etc. + Flags now sanely defaults to 0 instead of 0x0F, consistent with all other flags in the API. + IMPORTANT: The default with rounding > 0.0f is now "round all corners" vs old implicit "round no corners": + - rounding == 0.0f + flags == 0 --> meant no rounding --> unchanged (common use) + - rounding > 0.0f + flags != 0 --> meant rounding --> unchanged (common use) + - rounding == 0.0f + flags != 0 --> meant no rounding --> unchanged (unlikely use) + - rounding > 0.0f + flags == 0 --> meant no rounding --> BREAKING (unlikely use)! + - this ONLY matters for hardcoded use of 0 with rounding > 0.0f. + - fix by using named ImDrawFlags_RoundCornersNone or rounding == 0.0f! + - this is technically the only real breaking change which we can't solve automatically (it's also uncommon). + The old ImDrawCornersFlags used awkward default values of ~0 or 0xF (4 lower bits set) to signify "round all corners" + and we sometimes encouraged using them as shortcuts. As a result the legacy path still support use of hardcoded ~0 + or any value from 0x1 or 0xF. They will behave the same with legacy paths enabled (will assert otherwise). + Courtesy of legacy untangling commity: [@rokups, @ocornut, @thedmd] +- ImDrawList: clarified that PathArcTo()/PathArcToFast() won't render with radius < 0.0f. Previously it sorts + of accidentally worked but would lead to counter-clockwise paths which and have an effect on anti-aliasing. +- InputText: renamed ImGuiInputTextFlags_AlwaysInsertMode to ImGuiInputTextFlags_AlwaysOverwrite, old name was an + incorrect description of behavior. Was ostly used by memory editor. Kept inline redirection function. (#2863) +- Moved 'misc/natvis/imgui.natvis' to 'misc/debuggers/imgui.natvis' as we will provide scripts for other debuggers. +- Style: renamed rarely used style.CircleSegmentMaxError (old default = 1.60f) + to style.CircleTessellationMaxError (new default = 0.30f) as its meaning changed. (#3808) [@thedmd] +- Win32+MinGW: Re-enabled IME functions by default even under MinGW. In July 2016, issue #738 had me incorrectly + disable those default functions for MinGW. MinGW users should: either link with -limm32, either set their + imconfig file with '#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS'. (#2590, #738) [@actboy168] + *EDIT* Undid in 1.84. +- Backends: Win32: Pragma linking with dwmapi.lib (Vista-era, ~9 kb). MinGW users will need to link with -ldwmapi. + +Other Changes: + +- Window, Nav: Fixed crash when calling SetWindowFocus(NULL) at the time a new window appears. (#3865) [@nem0] +- Window: Shrink close button hit-testing region when it covers an abnormally high portion of the window visible + area (e.g. when window is collapsed + moved in a corner) to facilitate moving the window away. (#3825) +- Nav: Various fixes for losing gamepad/keyboard navigation reference point when a window reappears or + when it appears while gamepad/keyboard are not being used. (#787) +- Drags: Fixed crash when using DragScalar() directly (not via common wrapper like DragFloat() etc.) + with ImGuiSliderFlags_AlwaysClamp + only one of either p_min or p_max set. (#3824) [@harry75369] +- Drags, Sliders: Fixed a bug where editing value would use wrong number if there were digits right after + format specifier (e.g. using "%f123" as a format string). [@rokups] +- Drags, Sliders: Fixed a bug where using custom formatting flags (',$,_) supported by stb_sprintf.h + would cause incorrect value to be displayed. (#3604) [@rokups] +- Drags, Sliders: Support ImGuiSliderFlags_Logarithmic flag with integers. Because why not? (#3786) +- Tables: Fixed unaligned accesses when using TableSetBgColor(ImGuiTableBgTarget_CellBg). (#3872) +- IsItemHovered(): fixed return value false positive when used after EndChild(), EndGroup() or widgets using + either of them, when the hovered location is located within a child window, e.g. InputTextMultiline(). + This is intended to have no side effects, but brace yourself for the possible comeback.. (#3851, #1370) +- Drag and Drop: can use BeginDragDropSource() for other than the left mouse button as long as the item + has an ID (for ID-less items will add new functionalities later). (#1637, #3885) +- ImFontAtlas: Added 'bool TexPixelsUseColors' output to help backend decide of underlying texture format. (#3369) + This can currently only ever be set by the Freetype renderer. +- imgui_freetype: Added ImGuiFreeTypeBuilderFlags_Bitmap flag to request Freetype loading bitmap data. + This may have an effect on size and must be called with correct size values. (#3879) [@metarutaiga] +- ImDrawList: PathArcTo() now supports "int num_segments = 0" (new default) and adaptively tessellate. + The adaptive tessellation uses look up tables, tends to be faster than old PathArcTo() while maintaining + quality for large arcs (tessellation quality derived from "style.CircleTessellationMaxError") (#3491) [@thedmd] +- ImDrawList: PathArcToFast() also adaptively tessellate efficiently. This means that large rounded corners + in e.g. hi-dpi settings will generally look better. (#3491) [@thedmd] +- ImDrawList: AddCircle, AddCircleFilled(): Tweaked default segment count calculation to honor MaxError + with more accuracy. Made default segment count always even for better looking result. (#3808) [@thedmd] +- Misc: Added GetAllocatorFunctions() to facilitate sharing allocators across DLL boundaries. (#3836) +- Misc: Added 'debuggers/imgui.gdb' and 'debuggers/imgui.natstepfilter' (along with existing 'imgui.natvis') + scripts to configure popular debuggers into skipping trivial functions when using StepInto. [@rokups] +- Backends: Android: Added native Android backend. (#3446) [@duddel] +- Backends: Win32: Added ImGui_ImplWin32_EnableAlphaCompositing() to facilitate experimenting with + alpha compositing and transparent windows. (#2766, #3447 etc.). +- Backends: OpenGL, Vulkan, DX9, DX10, DX11, DX12, Metal, WebGPU, Allegro: Rework blending equation to + preserve alpha in output buffer (using SrcBlendAlpha = ONE, DstBlendAlpha = ONE_MINUS_SRC_ALPHA consistently + accross all backends), facilitating compositing of the output buffer with another buffer. + (#2693, #2764, #2766, #2873, #3447, #3813, #3816) [@ocornut, @thedmd, @ShawnM427, @Ubpa, @aiekick] +- Backends: DX9: Fix to support IMGUI_USE_BGRA_PACKED_COLOR. (#3844) [@Xiliusha] +- Backends: DX9: Fix to support colored glyphs, using newly introduced 'TexPixelsUseColors' info. (#3844) +- Examples: Android: Added Android + GL ES3 example. (#3446) [@duddel] +- Examples: Reworked setup of clear color to be compatible with transparent values. +- CI: Use a dedicated "scheduled" workflow to trigger scheduled builds. Forks may disable this workflow if + scheduled builds builds are not required. [@rokups] +- Log/Capture: Added LogTextV, a va_list variant of LogText. [@PathogenDavid] + + +----------------------------------------------------------------------- + VERSION 1.81 (Released 2021-02-10) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.81 + +Breaking Changes: + +- ListBox helpers: + - Renamed ListBoxHeader(const char* label, ImVec2 size) to BeginListBox(). + - Renamed ListBoxFooter() to EndListBox(). + - Removed ListBoxHeader(const char* label, int items_count, int height_in_items = -1) in favor of specifying size. + In the redirection function, made vertical padding consistent regardless of (items_count <= height_in_items) or not. + - Kept inline redirection function for all threes (will obsolete). +- imgui_freetype: + - Removed ImGuiFreeType::BuildFontAtlas(). Kept inline redirection function. + Prefer using '#define IMGUI_ENABLE_FREETYPE', but there's a runtime selection path available too. + - The shared extra flags parameters (very rarely used) are now stored in ImFontAtlas::FontBuilderFlags. + - Renamed ImFontConfig::RasterizerFlags (used by FreeType) to ImFontConfig::FontBuilderFlags. + - Renamed ImGuiFreeType::XXX flags to ImGuiFreeTypeBuilderFlags_XXX for consistency with other API. + +Other Changes: + +- Viewports Added ImGui::GetMainViewport() as a way to get the bounds and work area of the host display. (#3789, #1542) + - In 'master' branch or without multi-viewports feature enabled: + - GetMainViewport()->Pos is always == (0,0) + - GetMainViewport()->Size is always == io.DisplaySize + - In 'docking' branch and with the multi-viewports feature enabled: + - GetMainViewport() will return information from your host Platform Window. + - In the future, we will support a "no main viewport" mode and this may return bounds of your main monitor. + - For forward compatibility with multi-viewports/multi-monitors: + - Code using (0,0) as a way to signify "upper-left of the host window" should use GetMainViewport()->Pos. + - Code using io.DisplaySize as a way to signify "size of the host window" should use GetMainViewport()->Size. + - We are also exposing a work area in ImGuiViewport ('WorkPos', 'WorkSize' vs 'Pos', 'Size' for full area): + - For a Platform Window, the work area is generally the full area minus space used by menu-bars. + - For a Platform Monitor, the work area is generally the full area minus space used by task-bars. + - All of this has been the case in 'docking' branch for a long time. What we've done is merely merging + a small chunk of the multi-viewport logic into 'master' to standardize some concepts ahead of time. +- Tables: Fixed PopItemWidth() or multi-components items not restoring per-colum ItemWidth correctly. (#3760) +- Window: Fixed minor title bar text clipping issue when FramePadding is small/zero and there are no + close button in the window. (#3731) +- SliderInt: Fixed click/drag when v_min==v_max from setting the value to zero. (#3774) [@erwincoumans] + Would also repro with DragFloat() when using ImGuiSliderFlags_Logarithmic with v_min==v_max. +- Menus: Fixed an issue with child-menu auto sizing (issue introduced in 1.80 on 2021/01/25) (#3779) +- InputText: Fixed slightly off ScrollX tracking, noticeable with large values of FramePadding.x. (#3781) +- InputText: Multiline: Fixed padding/cliprect not precisely matching single-line version. (#3781) +- InputText: Multiline: Fixed FramePadding.y worth of vertical offset when aiming with mouse. +- ListBox: Tweaked default height calculation. +- Fonts: imgui_freetype: Facilitated using FreeType integration: [@Xipiryon, @ocornut] + - Use '#define IMGUI_ENABLE_FREETYPE' in imconfig.h should make it work with no other modifications + other than compiling misc/freetype/imgui_freetype.cpp and linking with FreeType. + - Use '#define IMGUI_ENABLE_STB_TRUETYPE' if you somehow need the stb_truetype rasterizer to be + compiled in along with the FreeType one, otherwise it is enabled by default. +- Fonts: imgui_freetype: Added support for colored glyphs as supported by Freetype 2.10+ (for .ttf using CPAL/COLR + tables only). Enable the ImGuiFreeTypeBuilderFlags_LoadColor on a given font. Atlas always output directly + as RGBA8 in this situation. Likely to make sense with IMGUI_USE_WCHAR32. (#3369) [@pshurgal] +- Fonts: Fixed CalcTextSize() width rounding so it behaves more like a ceil. This is in order for text wrapping + to have enough space when provided width precisely calculated with CalcTextSize().x. (#3776) + Note that the rounding of either positions and widths are technically undesirable (e.g. #3437, #791) but + variety of code is currently on it so we are first fixing current behavior before we'll eventually change it. +- Log/Capture: Fix various new line/spacing issue when logging widgets. [@Xipiryon, @ocornut] +- Log/Capture: Improved the ASCII look of various widgets, making large dumps more easily human readable. +- ImDrawList: Fixed AddCircle()/AddCircleFilled() with (rad > 0.0f && rad < 1.0f && num_segments == 0). (#3738) + Would lead to a buffer read overflow. +- ImDrawList: Clarified PathArcTo() need for a_min <= a_max with an assert. +- ImDrawList: Fixed PathArcToFast() handling of a_min > a_max. +- Metrics: Back-ported "Viewports" debug visualizer from 'docking' branch. +- Demo: Added 'Examples->Fullscreen Window' demo using GetMainViewport() values. (#3789) +- Demo: 'Simple Overlay' demo now moves under main menu-bar (if any) using GetMainViewport()'s work area. +- Backends: Win32: Dynamically loading XInput DLL instead of linking with it, facilitate compiling with + old WindowSDK versions or running on Windows 7. (#3646, #3645, #3248, #2716) [@Demonese] +- Backends: Vulkan: Add support for custom Vulkan function loader and VK_NO_PROTOTYPES. (#3759, #3227) [@Hossein-Noroozpour] + User needs to call ImGui_ImplVulkan_LoadFunctions() with their custom loader prior to other functions. +- Backends: Metal: Fixed texture storage mode when building on Mac Catalyst. (#3748) [@Belinsky-L-V] +- Backends: OSX: Fixed mouse position not being reported when mouse buttons other than left one are down. (#3762) [@rokups] +- Backends: WebGPU: Added enderer backend for WebGPU support (imgui_impl_wgpu.cpp) (#3632) [@bfierz] + Please note that WebGPU is currently experimental, will not run on non-beta browsers, and may break. +- Examples: WebGPU: Added Emscripten+WebGPU example. (#3632) [@bfierz] +- Backends: GLFW: Added ImGui_ImplGlfw_InitForOther() initialization call to use with non OpenGL API. (#3632) + + +----------------------------------------------------------------------- + VERSION 1.80 (Released 2021-01-21) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.80 + +Breaking Changes: + +- Added imgui_tables.cpp file! Manually constructed project files will need the new file added! (#3740) +- Backends: moved all backends files (imgui_impl_XXXX.cpp, imgui_impl_XXXX.h) from examples/ to backends/. (#3513) +- Renamed ImDrawList::AddBezierCurve() to ImDrawList::AddBezierCubic(). Kept inline redirection function (will obsolete). +- Renamed ImDrawList::PathBezierCurveTo() to ImDrawList::PathBezierCubicCurveTo(). Kept inline redirection function (will obsolete). +- Removed redirecting functions/enums names that were marked obsolete in 1.60 (April 2018): + - io.RenderDrawListsFn pointer -> use ImGui::GetDrawData() value and call the render function of your backend + - ImGui::IsAnyWindowFocused() -> use ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow) + - ImGui::IsAnyWindowHovered() -> use ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) + - ImGuiStyleVar_Count_ -> use ImGuiStyleVar_COUNT + - ImGuiMouseCursor_Count_ -> use ImGuiMouseCursor_COUNT +- Removed redirecting functions/enums names that were marked obsolete in 1.61 (May 2018): + - InputFloat (... int decimal_precision ...) -> use InputFloat (... const char* format ...) with format = "%.Xf" where X was value for decimal_precision. + - same for InputFloat2()/InputFloat3()/InputFloat4() variants taking a `int decimal_precision` parameter. +- Removed redirecting functions/enums names that were marked obsolete in 1.63 (August 2018): + - ImGui::IsItemDeactivatedAfterChange() -> use ImGui::IsItemDeactivatedAfterEdit(). + - ImGuiCol_ModalWindowDarkening -> use ImGuiCol_ModalWindowDimBg + - ImGuiInputTextCallback -> use ImGuiTextEditCallback + - ImGuiInputTextCallbackData -> use ImGuiTextEditCallbackData +- If you were still using the old names, while you are cleaning up, considering enabling + IMGUI_DISABLE_OBSOLETE_FUNCTIONS in imconfig.h even temporarily to have a pass at finding + and removing up old API calls, if any remaining. +- Internals: Columns: renamed undocumented/internals ImGuiColumnsFlags_* to ImGuiOldColumnFlags_* to reduce + confusion with Tables API. Keep redirection enums (will obsolete). (#125, #513, #913, #1204, #1444, #2142, #2707) +- Renamed io.ConfigWindowsMemoryCompactTimer to io.ConfigMemoryCompactTimer as the feature now applies + to other data structures. (#2636) + +Other Changes: + +- Tables: added new Tables Beta API as a replacement for old Columns. (#3740, #2957, #125) + Check out 'Demo->Tables' for many demos. + Read API comments in imgui.h for details. Read extra commentary in imgui_tables.cpp. + - Added 16 functions: + - BeginTable(), EndTable() + - TableNextRow(), TableNextColumn(), TableSetColumnIndex() + - TableSetupColumn(), TableSetupScrollFreeze() + - TableHeadersRow(), TableHeader() + - TableGetRowIndex(), TableGetColumnCount(), TableGetColumnIndex(), TableGetColumnName(), TableGetColumnFlags() + - TableGetSortSpecs(), TableSetBgColor() + - Added 3 flags sets: + - ImGuiTableFlags (29 flags for: features, decorations, sizing policies, padding, clipping, scrolling, sorting etc.) + - ImGuiTableColumnFlags (24 flags for: width policies, default settings, sorting options, indentation options etc.) + - ImGuiTableRowFlags (1 flag for: header row) + - Added 2 structures: ImGuiTableSortSpecs, ImGuiTableColumnSortSpecs + - Added 2 enums: ImGuiSortDirection, ImGuiTableBgTarget + - Added 1 style variable: ImGuiStyleVar_CellPadding + - Added 5 style colors: ImGuiCol_TableHeaderBg, ImGuiCol_TableBorderStrong, ImGuiCol_TableBorderLight, ImGuiCol_TableRowBg, ImGuiCol_TableRowBgAlt. +- Tab Bar: Made it possible to append to an existing tab bar by calling BeginTabBar()/EndTabBar() again. +- Tab Bar: Fixed using more than 128 tabs in a tab bar (scrolling policy recommended). +- Tab Bar: Do not display a tooltip if the name already fits over a given tab. (#3521) +- Tab Bar: Fixed minor/unlikely bug skipping over a button when scrolling left with arrows. +- Tab Bar: Requested ideal content size (for auto-fit) doesn't affect horizontal scrolling. (#3414) +- Drag and Drop: Fix losing drop source ActiveID (and often source tooltip) when opening a TreeNode() + or CollapsingHeader() while dragging. (#1738) +- Drag and Drop: Fix drag and drop to tie same-size drop targets by chosen the later one. Fixes dragging + into a full-window-sized dockspace inside a zero-padded window. (#3519, #2717) [@Black-Cat] +- Checkbox: Added CheckboxFlags() helper with int* type (internals have a template version, not exposed). +- Clipper: Fixed incorrect end-list positioning when using ImGuiListClipper with 1 item (bug in 1.79). (#3663) [@nyorain] +- InputText: Fixed updating cursor/selection position when a callback altered the buffer in a way + where the byte count is unchanged but the decoded character count changes. (#3587) [@gqw] +- InputText: Fixed switching from single to multi-line while preserving same ID. +- Combo: Fixed using IsItemEdited() after Combo() not matching the return value from Combo(). (#2034) +- DragFloat, DragInt: very slightly increased mouse drag threshold + expressing it as a factor of default value. +- DragFloat, DragInt: added experimental io.ConfigDragClickToInputText feature to enable turning DragXXX widgets + into text input with a simple mouse click-release (without moving). (#3737) +- Nav: Fixed IsItemFocused() from returning false when Nav highlight is hidden because mouse has moved. + It's essentially been always the case but it doesn't make much sense. Instead we will aim at exposing + feedback and control of keyboard/gamepad navigation highlight and mouse hover disable flag. (#787, #2048) +- Metrics: Fixed mishandling of ImDrawCmd::VtxOffset in wireframe mesh renderer. +- Metrics: Rebranded as "Dear ImGui Metrics/Debugger" to clarify its purpose. +- ImDrawList: Added ImDrawList::AddQuadBezierCurve(), ImDrawList::PathQuadBezierCurveTo() quadratic bezier + helpers. (#3127, #3664, #3665) [@aiekick] +- Fonts: Updated GetGlyphRangesJapanese() to include a larger 2999 ideograms selection of Joyo/Jinmeiyo + kanjis, from the previous 1946 ideograms selection. This will consume a some more memory but be generally + much more fitting for Japanese display, until we switch to a more dynamic atlas. (#3627) [@vaiorabbit] +- Log/Capture: fix capture to work on clipped child windows. +- Misc: Made the ItemFlags stack shared, so effectively the ButtonRepeat/AllowKeyboardFocus states + (and others exposed in internals such as PushItemFlag) are inherited by stacked Begin/End pairs, + vs previously a non-child stacked Begin() would reset those flags back to zero for the stacked window. +- Misc: Replaced UTF-8 decoder with one based on branchless one by Christopher Wellons. [@rokups] + Super minor fix handling incomplete UTF-8 contents: if input does not contain enough bytes, decoder + returns IM_UNICODE_CODEPOINT_INVALID and consume remaining bytes (vs old decoded consumed only 1 byte). +- Misc: Fix format warnings when using gnu printf extensions in a setup that supports them (gcc/mingw). (#3592) +- Misc: Made EndFrame() assertion for key modifiers being unchanged during the frame (added in 1.76) more + lenient, allowing full mid-frame releases. This is to accommodate the use of mid-frame modal native + windows calls, which leads backends such as GLFW to send key clearing events on focus loss. (#3575) +- Style: Changed default style.WindowRounding value to 0.0f (matches default for multi-viewports). +- Style: Reduced the size of the resizing grip, made alpha less prominent. +- Style: Classic: Increase the default alpha value of WindowBg to be closer to other styles. +- Demo: Clarify usage of right-aligned items in Demo>Layout>Widgets Width. +- Backends: OpenGL3: Use glGetString(GL_VERSION) query instead of glGetIntegerv(GL_MAJOR_VERSION, ...) + when the later returns zero (e.g. Desktop GL 2.x). (#3530) [@xndcn] +- Backends: OpenGL2: Backup and restore GL_SHADE_MODEL and disable GL_NORMAL_ARRAY state to increase + compatibility with legacy code. (#3671) +- Backends: OpenGL3: Backup and restore GL_PRIMITIVE_RESTART state. (#3544) [@Xipiryon] +- Backends: OpenGL2, OpenGL3: Backup and restore GL_STENCIL_TEST enable state. (#3668) +- Backends: Vulkan: Added support for specifying which sub-pass to reference during VkPipeline creation. (@3579) [@bdero] +- Backends: DX12: Improve Windows 7 compatibility (for D3D12On7) by loading d3d12.dll dynamically. (#3696) [@Mattiwatti] +- Backends: Win32: Fix setting of io.DisplaySize to invalid/uninitialized data after hwnd has been closed. +- Backends: OSX: Fix keypad-enter key not working on MacOS. (#3554) [@rokups, @lfnoise] +- Examples: Apple+Metal: Consolidated/simplified to get closer to other examples. (#3543) [@warrenm] +- Examples: Apple+Metal: Forward events down so OS key combination like Cmd+Q can work. (#3554) [@rokups] +- Examples: Emscripten: Renamed example_emscripten/ to example_emscripten_opengl3/. (#3632) +- Examples: Emscripten: Added 'make serve' helper to spawn a web-server on localhost. (#3705) [@Horki] +- Examples: DirectX12: Move ImGui::Render() call above the first barrier to clarify its lack of effect on the graphics pipe. +- CI: Fix testing for Windows DLL builds. (#3603, #3601) [@iboB] +- Docs: Improved the wiki and added a https://github.com/ocornut/imgui/wiki/Useful-Widgets page. [@Xipiryon] + [2021/05/20: moved to https://github.com/ocornut/imgui/wiki/Useful-Extensions] +- Docs: Split examples/README.txt into docs/BACKENDS.md and docs/EXAMPLES.md, and improved them. +- Docs: Consistently renamed all occurrences of "binding" and "back-end" to "backend" in comments and docs. + + +----------------------------------------------------------------------- + VERSION 1.79 (Released 2020-10-08) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.79 + +Breaking Changes: + +- Fonts: Removed ImFont::DisplayOffset in favor of ImFontConfig::GlyphOffset. DisplayOffset was applied + after scaling and not very meaningful/useful outside of being needed by the default ProggyClean font. + It was also getting in the way of better font scaling, so let's get rid of it now! + If you used DisplayOffset it was probably in association to rasterizing a font at a specific size, + in which case the corresponding offset may be reported into GlyphOffset. (#1619) + If you scaled this value after calling AddFontDefault(), this is now done automatically. +- ImGuiListClipper: Renamed constructor parameters which created an ambiguous alternative to using + the ImGuiListClipper::Begin() function, with misleading edge cases. Always use ImGuiListClipper::Begin()! + Kept inline redirection function (will obsolete). + (note: imgui_memory_editor <0.40 from imgui_club/ used this old clipper API. Update your copy if needed). +- Style: Renamed style.TabMinWidthForUnselectedCloseButton to style.TabMinWidthForCloseButton. +- Renamed ImGuiSliderFlags_ClampOnInput to ImGuiSliderFlags_AlwaysClamp. Kept redirection enum (will obsolete). +- Renamed OpenPopupContextItem() back to OpenPopupOnItemClick(), REVERTED CHANGE FROM 1.77. + For variety of reason this is more self-explanatory and less error-prone. Kept inline redirection function. +- Removed return value from OpenPopupOnItemClick() - returned true on mouse release on an item - because it + is inconsistent with other popups API and makes others misleading. It's also and unnecessary: you can + use IsWindowAppearing() after BeginPopup() for a similar result. + +Other Changes: + +- Window: Fixed using non-zero pivot in SetNextWindowPos() when the window is collapsed. (#3433) +- Nav: Fixed navigation resuming on first visible item when using gamepad. [@rokups] +- Nav: Fixed using Alt to toggle the Menu layer when inside a Modal window. (#787) +- Scrolling: Fixed SetScrollHere(0) functions edge snapping when called during a frame where + ContentSize is changing (issue introduced in 1.78). (#3452). +- InputText: Added support for Page Up/Down in InputTextMultiline(). (#3430) [@Xipiryon] +- InputText: Added selection helpers in ImGuiInputTextCallbackData(). +- InputText: Added ImGuiInputTextFlags_CallbackEdit to modify internally owned buffer after an edit. + (note that InputText() already returns true on edit, the callback is useful mainly to manipulate the + underlying buffer while focus is active). +- InputText: Fixed using ImGuiInputTextFlags_Password with InputTextMultiline(). (#3427, #3428) + It is a rather unusual or useless combination of features but no reason it shouldn't work! +- InputText: Fixed minor scrolling glitch when erasing trailing lines in InputTextMultiline(). +- InputText: Fixed cursor being partially covered after using Ctrl+End key. +- InputText: Fixed callback's helper DeleteChars() function when cursor is inside the deleted block. (#3454) +- InputText: Made pressing Down arrow on the last line when it doesn't have a carriage return not move to + the end of the line (so it is consistent with Up arrow, and behave same as Notepad and Visual Studio. + Note that some other text editors instead would move the cursor to the end of the line). [@Xipiryon] +- DragFloat, DragScalar: Fixed ImGuiSliderFlags_ClampOnInput not being honored in the special case + where v_min == v_max. (#3361) +- SliderInt, SliderScalar: Fixed reaching of maximum value with inverted integer min/max ranges, both + with signed and unsigned types. Added reverse Sliders to Demo. (#3432, #3449) [@rokups] +- Text: Bypass unnecessary formatting when using the TextColored()/TextWrapped()/TextDisabled() helpers + with a "%s" format string. (#3466) +- CheckboxFlags: Display mixed-value/tristate marker when passed flags that have multiple bits set and + stored value matches neither zero neither the full set. +- BeginMenuBar: Fixed minor bug where CursorPosMax gets pushed to CursorPos prior to calling BeginMenuBar(), + so e.g. calling the function at the end of a window would often add +ItemSpacing.y to scrolling range. +- TreeNode, CollapsingHeader: Made clicking on arrow toggle toggle the open state on the Mouse Down event + rather than the Mouse Down+Up sequence, even if the _OpenOnArrow flag isn't set. This is standard behavior + and amends the change done in 1.76 which only affected cases were _OpenOnArrow flag was set. + (This is also necessary to support full multi/range-select/drag and drop operations.) +- Tab Bar: Added TabItemButton() to submit tab that behave like a button. (#3291) [@Xipiryon] +- Tab Bar: Added ImGuiTabItemFlags_Leading and ImGuiTabItemFlags_Trailing flags to position tabs or button + at either end of the tab bar. Those tabs won't be part of the scrolling region, and when reordering cannot + be moving outside of their section. Most often used with TabItemButton(). (#3291) [@Xipiryon] +- Tab Bar: Added ImGuiTabItemFlags_NoReorder flag to disable reordering a given tab. +- Tab Bar: Keep tab item close button visible while dragging a tab (independent of hovering state). +- Tab Bar: Fixed a small bug where closing a tab that is not selected would leave a tab hole for a frame. +- Tab Bar: Fixed a small bug where scrolling buttons (with ImGuiTabBarFlags_FittingPolicyScroll) would + generate an unnecessary extra draw call. +- Tab Bar: Fixed a small bug where toggling a tab bar from Reorderable to not Reorderable would leave + tabs reordered in the tab list popup. [@Xipiryon] +- Columns: Fix inverted ClipRect being passed to renderer when using certain primitives inside of + a fully clipped column. (#3475) [@szreder] +- Popups, Tooltips: Fix edge cases issues with positioning popups and tooltips when they are larger than + viewport on either or both axises. [@Rokups] +- Fonts: AddFontDefault() adjust its vertical offset based on floor(size/13) instead of always +1. + Was previously done by altering DisplayOffset.y but wouldn't work for DPI scaled font. +- Metrics: Various tweaks, listing windows front-to-back, greying inactive items when possible. +- Demo: Add simple InputText() callbacks demo (aside from the more elaborate ones in 'Examples->Console'). +- Backends: OpenGL3: Fix to avoid compiling/calling glBindSampler() on ES or pre 3.3 contexts which have + the defines set by a loader. (#3467, #1985) [@jjwebb] +- Backends: Vulkan: Some internal refactor aimed at allowing multi-viewport feature to create their + own render pass. (#3455, #3459) [@FunMiles] +- Backends: DX12: Clarified that imgui_impl_dx12 can be compiled on 32-bit systems by redefining + the ImTextureID to be 64-bit (e.g. '#define ImTextureID ImU64' in imconfig.h). (#301) +- Backends: DX12: Fix debug layer warning when scissor rect is zero-sized. (#3472, #3462) [@StoneWolf] +- Examples: Vulkan: Reworked buffer resize handling, fix for Linux/X11. (#3390, #2626) [@RoryO] +- Examples: Vulkan: Switch validation layer to use "VK_LAYER_KHRONOS_validation" instead of + "VK_LAYER_LUNARG_standard_validation" which is deprecated (#3459) [@FunMiles] +- Examples: DX12: Enable breaking on any warning/error when debug interface is enabled. +- Examples: DX12: Added '#define ImTextureID ImU64' in project and build files to also allow building + on 32-bit systems. Added project to default Visual Studio solution file. (#301) + + +----------------------------------------------------------------------- + VERSION 1.78 (Released 2020-08-18) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.78 + +Breaking Changes: + +- Obsoleted use of the trailing 'float power=1.0f' parameter for those functions: [@Shironekoben, @ocornut] + - DragFloat(), DragFloat2(), DragFloat3(), DragFloat4(), DragFloatRange2(), DragScalar(), DragScalarN() + - SliderFloat(), SliderFloat2(), SliderFloat3(), SliderFloat4(), SliderScalar(), SliderScalarN() + - VSliderFloat(), VSliderScalar() + Replaced the final 'float power=1.0f' argument with ImGuiSliderFlags defaulting to 0 (as with all our flags). + Worked out a backward-compatibility scheme so hopefully most C++ codebase should not be affected. + In short, when calling those functions: + - If you omitted the 'power' parameter (likely!), you are not affected. + - If you set the 'power' parameter to 1.0f (same as previous default value): + - Your compiler may warn on float>int conversion. + - Everything else will work (but will assert if IMGUI_DISABLE_OBSOLETE_FUNCTIONS is defined). + - You can replace the 1.0f value with 0 to fix the warning, and be technically correct. + - If you set the 'power' parameter to >1.0f (to enable non-linear editing): + - Your compiler may warn on float>int conversion. + - Code will assert at runtime for IM_ASSERT(power == 1.0f) with the following assert description: + "Call Drag function with ImGuiSliderFlags_Logarithmic instead of using the old 'float power' function!". + - In case asserts are disabled, the code will not crash and enable the _Logarithmic flag. + - You can replace the >1.0f value with ImGuiSliderFlags_Logarithmic to fix the warning/assert + and get a _similar_ effect as previous uses of power >1.0f. + See https://github.com/ocornut/imgui/issues/3361 for all details. + For shared code, you can version check at compile-time with `#if IMGUI_VERSION_NUM >= 17704`. + Kept inline redirection functions (will obsolete) apart for: DragFloatRange2(), VSliderFloat(), VSliderScalar(). + For those three the 'float power=1.0f' version was removed directly as they were most unlikely ever used. +- DragInt, DragFloat, DragScalar: Obsoleted use of v_min > v_max to lock edits (introduced in 1.73, this was not + demoed nor documented much, will be replaced a more generic ReadOnly feature). + +Other Changes: + +- Nav: Fixed clicking on void (behind any windows) from not clearing the focused window. + This would be problematic e.g. in situation where the application relies on io.WantCaptureKeyboard + flag being cleared accordingly. (bug introduced in 1.77 WIP on 2020/06/16) (#3344, #2880) +- Window: Fixed clicking over an item which hovering has been disabled (e.g inhibited by a popup) + from marking the window as moved. +- Drag, Slider: Added ImGuiSliderFlags parameters. + - For float functions they replace the old trailing 'float power=1.0' parameter. + (See #3361 and the "Breaking Changes" block above for all details). + - Added ImGuiSliderFlags_Logarithmic flag to enable logarithmic editing + (generally more precision around zero), as a replacement to the old 'float power' parameter + which was obsoleted. (#1823, #1316, #642) [@Shironekoben, @AndrewBelt] + - Added ImGuiSliderFlags_ClampOnInput flag to force clamping value when using + CTRL+Click to type in a value manually. (#1829, #3209, #946, #413). + [note: RENAMED to ImGuiSliderFlags_AlwaysClamp in 1.79]. + - Added ImGuiSliderFlags_NoRoundToFormat flag to disable rounding underlying + value to match precision of the display format string. (#642) + - Added ImGuiSliderFlags_NoInput flag to disable turning widget into a text input + with CTRL+Click or Nav Enter. +- Nav, Slider: Fix using keyboard/gamepad controls with certain logarithmic sliders where + pushing a direction near zero values would be cancelled out. [@Shironekoben] +- DragFloatRange2, DragIntRange2: Fixed an issue allowing to drag out of bounds when both + min and max value are on the same value. (#1441) +- InputText, ImDrawList: Fixed assert triggering when drawing single line of text with more + than ~16 KB characters. (Note that current code is going to show corrupted display if after + clipping, more than 16 KB characters are visible in the same low-level ImDrawList::RenderText() + call. ImGui-level functions such as TextUnformatted() are not affected. This is quite rare + but it will be addressed later). (#3349) +- Selectable: Fixed highlight/hit extent when used with horizontal scrolling (in or outside columns). + Also fixed related text clipping when used in a column after the first one. (#3187, #3386) +- Scrolling: Avoid SetScroll, SetScrollFromPos functions from snapping on the edge of scroll + limits when close-enough by (WindowPadding - ItemPadding), which was a tweak with too many + side-effects. The behavior is still present in SetScrollHere functions as they are more explicitly + aiming at making widgets visible. May later be moved to a flag. +- Tab Bar: Allow calling SetTabItemClosed() after a tab has been submitted (will process next frame). +- InvisibleButton: Made public a small selection of ImGuiButtonFlags (previously in imgui_internal.h) + and allowed to pass them to InvisibleButton(): ImGuiButtonFlags_MouseButtonLeft/Right/Middle. + This is a small but rather important change because lots of multi-button behaviors could previously + only be achieved using lower-level/internal API. Now also available via high-level InvisibleButton() + with is a de-facto versatile building block to creating custom widgets with the public API. +- Fonts: Fixed ImFontConfig::GlyphExtraSpacing and ImFontConfig::PixelSnapH settings being pulled + from the merged/target font settings when merging fonts, instead of being pulled from the source + font settings. +- ImDrawList: Thick anti-aliased strokes (> 1.0f) with integer thickness now use a texture-based + path, reducing the amount of vertices/indices and CPU/GPU usage. (#3245) [@Shironekoben] + - This change will facilitate the wider use of thick borders in future style changes. + - Requires an extra bit of texture space (~64x64 by default), relies on GPU bilinear filtering. + - Set `io.AntiAliasedLinesUseTex = false` to disable rendering using this method. + - Clear `ImFontAtlasFlags_NoBakedLines` in ImFontAtlas::Flags to disable baking data in texture. +- ImDrawList: changed AddCircle(), AddCircleFilled() default num_segments from 12 to 0, effectively + enabling auto-tessellation by default. Tweak tessellation in Style Editor->Rendering section, or + by modifying the 'style.CircleSegmentMaxError' value. [@ShironekoBen] +- ImDrawList: Fixed minor bug introduced in 1.75 where AddCircle() with 12 segments would generate + an extra vertex. (This bug was mistakenly marked as fixed in earlier 1.77 release). [@ShironekoBen] +- Demo: Improved "Custom Rendering"->"Canvas" demo with a grid, scrolling and context menu. + Also showcase using InvisibleButton() with multiple mouse buttons flags. +- Demo: Improved "Layout & Scrolling" -> "Clipping" section. +- Demo: Improved "Layout & Scrolling" -> "Child Windows" section. +- Style Editor: Added preview of circle auto-tessellation when editing the corresponding value. +- Backends: OpenGL3: Added support for glad2 loader. (#3330) [@moritz-h] +- Backends: Allegro 5: Fixed horizontal scrolling direction with mouse wheel / touch pads (it seems + like Allegro 5 reports it differently from GLFW and SDL). (#3394, #2424, #1463) [@nobody-special666] +- Examples: Vulkan: Fixed GLFW+Vulkan and SDL+Vulkan clear color not being set. (#3390) [@RoryO] +- CI: Emscripten has stopped their support for their fastcomp backend, switching to latest sdk [@Xipiryon] + + +----------------------------------------------------------------------- + VERSION 1.77 (Released 2020-06-29) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.77 + +Breaking Changes: + +- Removed unnecessary ID (first arg) of ImFontAtlas::AddCustomRectRegular() function. Please + note that this is a Beta api and will likely be reworked in order to support multi-DPI across + multiple monitors. +- Renamed OpenPopupOnItemClick() to OpenPopupContextItem(). Kept inline redirection function (will obsolete). + [NOTE: THIS WAS REVERTED IN 1.79] +- Removed BeginPopupContextWindow(const char*, int mouse_button, bool also_over_items) in favor + of BeginPopupContextWindow(const char*, ImGuiPopupFlags flags) with ImGuiPopupFlags_NoOverItems. + Kept inline redirection function (will obsolete). +- Removed obsoleted CalcItemRectClosestPoint() entry point (has been asserting since December 2017). + +Other Changes: + +- TreeNode: Fixed bug where BeginDragDropSource() failed when the _OpenOnDoubleClick flag is + enabled (bug introduced in 1.76, but pre-1.76 it would also fail unless the _OpenOnArrow + flag was also set, and _OpenOnArrow is frequently set along with _OpenOnDoubleClick). +- TreeNode: Fixed bug where dragging a payload over a TreeNode() with either _OpenOnDoubleClick + or _OpenOnArrow would open the node. (#143) +- Windows: Fix unintended feedback loops when resizing windows close to main viewport edges. [@rokups] +- Tabs: Added style.TabMinWidthForUnselectedCloseButton settings: + - Set to 0.0f (default) to always make a close button appear on hover (same as Chrome, VS). + - Set to FLT_MAX to only display a close button when selected (merely hovering is not enough). + - Set to an intermediary value to toggle behavior based on width (same as Firefox). +- Tabs: Added a ImGuiTabItemFlags_NoTooltip flag to disable the tooltip for individual tab item + (vs ImGuiTabBarFlags_NoTooltip for entire tab bar). [@Xipiryon] +- Popups: All functions capable of opening popups (OpenPopup*, BeginPopupContext*) now take a new + ImGuiPopupFlags sets of flags instead of a mouse button index. The API is automatically backward + compatible as ImGuiPopupFlags is guaranteed to hold mouse button index in the lower bits. +- Popups: Added ImGuiPopupFlags_NoOpenOverExistingPopup for OpenPopup*/BeginPopupContext* functions + to first test for the presence of another popup at the same level. +- Popups: Added ImGuiPopupFlags_NoOpenOverItems for BeginPopupContextWindow() - similar to testing + for !IsAnyItemHovered() prior to doing an OpenPopup(). +- Popups: Added ImGuiPopupFlags_AnyPopupId and ImGuiPopupFlags_AnyPopupLevel flags for IsPopupOpen(), + allowing to check if any popup is open at the current level, if a given popup is open at any popup + level, if any popup is open at all. +- Popups: Fix an edge case where programmatically closing a popup while clicking on its empty space + would attempt to focus it and close other popups. (#2880) +- Popups: Fix BeginPopupContextVoid() when clicking over the area made unavailable by a modal. (#1636) +- Popups: Clarified some of the comments and function prototypes. +- Modals: BeginPopupModal() doesn't set the ImGuiWindowFlags_NoSavedSettings flag anymore, and will + not always be auto-centered. Note that modals are more similar to regular windows than they are to + popups, so api and behavior may evolve further toward embracing this. (#915, #3091) + Enforce centering using e.g. SetNextWindowPos(io.DisplaySize * 0.5f, ImGuiCond_Appearing, ImVec2(0.5f,0.5f)). +- Metrics: Added a "Settings" section with some details about persistent ini settings. +- Nav, Menus: Fix vertical wrap-around in menus or popups created with multiple appending calls to + BeginMenu()/EndMenu() or BeginPopup(0/EndPopup(). (#3223, #1207) [@rokups] +- Drag and Drop: Fixed unintended fallback "..." tooltip display during drag operation when + drag source uses _SourceNoPreviewTooltip flags. (#3160) [@rokups] +- Columns: Lower overhead on column switches and switching to background channel. + Benefits Columns but was primarily made with Tables in mind! +- Fonts: Fix GetGlyphRangesKorean() end-range to end at 0xD7A3 (instead of 0xD79D). (#348, #3217) [@marukrap] +- ImDrawList: Fixed an issue where draw command merging or primitive unreserve while crossing the + VtxOffset boundary would lead to draw commands with wrong VtxOffset. (#3129, #3163, #3232, #2591) + [@thedmd, @Shironekoben, @sergeyn, @ocornut] +- ImDrawList, ImDrawListSplitter, Columns: Fixed an issue where changing channels with different + TextureId, VtxOffset would incorrectly apply new settings to draw channels. (#3129, #3163) + [@ocornut, @thedmd, @Shironekoben] +- ImDrawList, ImDrawListSplitter, Columns: Fixed an issue where starting a split when current + VtxOffset was not zero would lead to draw commands with wrong VtxOffset. (#2591) +- ImDrawList, ImDrawListSplitter, Columns: Fixed an issue where starting a split right after + a callback draw command would incorrectly override the callback draw command. +- Misc, Freetype: Fix for rare case where FT_Get_Char_Index() succeeds but FT_Load_Glyph() fails. +- Docs: Improved and moved font documentation to docs/FONTS.md so it can be readable on the web. + Updated various links/wiki accordingly. Added FAQ entry about DPI. (#2861) [@ButternCream, @ocornut] +- CI: Added CI test to verify we're never accidentally dragging libstdc++ (on some compiler setups, + static constructors for non-pod data seems to drag in libstdc++ due to thread-safety concerns). + Fixed a static constructor which led to this dependency on some compiler setups. [@rokups] +- Backends: Win32: Support for #define NOGDI, won't try to call GetDeviceCaps(). (#3137, #2327) +- Backends: Win32: Fix _WIN32_WINNT < 0x0600 (MinGW defaults to 0x502 == Windows 2003). (#3183) +- Backends: SDL: Report a zero display-size when window is minimized, consistent with other backends, + making more render/clipping code use an early out path. +- Backends: OpenGL: Fixed handling of GL 4.5+ glClipControl(GL_UPPER_LEFT) by inverting the + projection matrix top and bottom values. (#3143, #3146) [@u3shit] +- Backends: OpenGL: On OSX, if unspecified by app, made default GLSL version 150. (#3199) [@albertvaka] +- Backends: OpenGL: Fixed loader auto-detection to not interfere with ES2/ES3 defines. (#3246) [@funchal] +- Backends: Vulkan: Fixed error in if initial frame has no vertices. (#3177) +- Backends: Vulkan: Fixed edge case where render callbacks wouldn't be called if the ImDrawData + structure didn't have any vertices. (#2697) [@kudaba] +- Backends: OSX: Added workaround to avoid fast mouse clicks. (#3261, #1992, #2525) [@nburrus] +- Examples: GLFW+Vulkan, SDL+Vulkan: Fix for handling of minimized windows. (#3259) +- Examples: Apple: Fixed example_apple_metal and example_apple_opengl2 using imgui_impl_osx.mm + not forwarding right and center mouse clicks. (#3260) [@nburrus] + + +----------------------------------------------------------------------- + VERSION 1.76 (Released 2020-04-12) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.76 + +Other Changes: + +- Drag and Drop, Nav: Disabling navigation arrow keys when drag and drop is active. In the docking + branch pressing arrow keys while dragging a window from a tab could trigger an assert. (#3025) +- BeginMenu: Using same ID multiple times appends content to a menu. (#1207) [@rokups] +- BeginMenu: Fixed a bug where SetNextWindowXXX data before a BeginMenu() would not be cleared + when the menu is not open. (#3030) +- InputText: Fixed password fields displaying ASCII spaces as blanks instead of using the '*' + glyph. (#2149, #515) +- Selectable: Fixed honoring style.SelectableTextAlign with unspecified size. (#2347, #2601) +- Selectable: Allow using ImGuiSelectableFlags_SpanAllColumns in other columns than first. (#125) +- TreeNode: Made clicking on arrow with _OpenOnArrow toggle the open state on the Mouse Down + event rather than the Mouse Down+Up sequence (this is rather standard behavior). +- ColorButton: Added ImGuiColorEditFlags_NoBorder flag to remove the border normally enforced + by default for standalone ColorButton. +- Nav: Fixed interactions with ImGuiListClipper, so e.g. Home/End result would not clip the + landing item on the landing frame. (#787) +- Nav: Fixed currently focused item from ever being clipped by ItemAdd(). (#787) +- Scrolling: Fixed scrolling centering API leading to non-integer scrolling values and initial + cursor position. This would often get fixed after the fix item submission, but using the + ImGuiListClipper as the first thing after Begin() could largely break size calculations. (#3073) +- Added optional support for Unicode plane 1-16 (#2538, #2541, #2815) [@cloudwu, @samhocevar] + - Compile-time enable with '#define IMGUI_USE_WCHAR32' in imconfig.h. + - More onsistent handling of unsupported code points (0xFFFD). + - Surrogate pairs are supported when submitting UTF-16 data via io.AddInputCharacterUTF16(), + allowing for more complete CJK input. + - sizeof(ImWchar) goes from 2 to 4. IM_UNICODE_CODEPOINT_MAX goes from 0xFFFF to 0x10FFFF. + - Various structures such as ImFont, ImFontGlyphRangesBuilder will use more memory, this + is currently not particularly efficient. +- Columns: undid the change in 1.75 were Columns()/BeginColumns() were preemptively limited + to 64 columns with an assert. (#3037, #125) +- Window: Fixed a bug with child window inheriting ItemFlags from their parent when the child + window also manipulate the ItemFlags stack. (#3024) [@Stanbroek] +- Font: Fixed non-ASCII space occasionally creating unnecessary empty looking polygons. +- Misc: Added an explicit compile-time test for non-scoped IM_ASSERT() macros to redirect users + to a solution rather than encourage people to add braces in the codebase. +- Misc: Added additional checks in EndFrame() to verify that io.KeyXXX values have not been + tampered with between NewFrame() and EndFrame(). +- Misc: Made default clipboard handlers for Win32 and OSX use a buffer inside the main context + instead of a static buffer, so it can be freed properly on Shutdown. (#3110) +- Misc, Freetype: Fixed support for IMGUI_STB_RECT_PACK_FILENAME compile time directive + in imgui_freetype.cpp (matching support in the regular code path). (#3062) [@DonKult] +- Metrics: Made Tools section more prominent. Showing wire-frame mesh directly hovering the ImDrawCmd + instead of requiring to open it. Added options to disable bounding box and mesh display. + Added notes on inactive/gc-ed windows. +- Demo: Added black and white and color gradients to Demo>Examples>Custom Rendering. +- CI: Added more tests on the continuous-integration server: extra warnings for Clang/GCC, building + SDL+Metal example, building imgui_freetype.cpp, more compile-time imconfig.h settings: disabling + obsolete functions, enabling 32-bit ImDrawIdx, enabling 32-bit ImWchar, disabling demo. [@rokups] +- Backends: OpenGL3: Fixed version check mistakenly testing for GL 4.0+ instead of 3.2+ to enable + ImGuiBackendFlags_RendererHasVtxOffset, leaving 3.2 contexts without it. (#3119, #2866) [@wolfpld] +- Backends: OpenGL3: Added include support for older glbinding 2.x loader. (#3061) [@DonKult] +- Backends: Win32: Added ImGui_ImplWin32_EnableDpiAwareness(), ImGui_ImplWin32_GetDpiScaleForHwnd(), + ImGui_ImplWin32_GetDpiScaleForMonitor() helpers functions (backported from the docking branch). + Those functions makes it easier for example apps to support hi-dpi features without setting up + a manifest. +- Backends: Win32: Calling AddInputCharacterUTF16() from WM_CHAR message handler in order to support + high-plane surrogate pairs. (#2815) [@cloudwu, @samhocevar] +- Backends: SDL: Added ImGui_ImplSDL2_InitForMetal() for API consistency (even though the function + currently does nothing). +- Backends: SDL: Fixed mapping for ImGuiKey_KeyPadEnter. (#3031) [@Davido71] +- Examples: Win32+DX12: Fixed resizing main window, enabled debug layer. (#3087, #3115) [@sergeyn] +- Examples: SDL+DX11: Fixed resizing main window. (#3057) [@joeslay] +- Examples: Added SDL+Metal example application. (#3017) [@coding-jackalope] + + +----------------------------------------------------------------------- + VERSION 1.75 (Released 2020-02-10) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.75 + +Breaking Changes: + +- Removed redirecting functions/enums names that were marked obsolete in 1.53 (December 2017): + - ShowTestWindow() -> use ShowDemoWindow() + - IsRootWindowFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootWindow) + - IsRootWindowOrAnyChildFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) + - SetNextWindowContentWidth(w) -> use SetNextWindowContentSize(ImVec2(w, 0.0f) + - GetItemsLineHeightWithSpacing() -> use GetFrameHeightWithSpacing() + - ImGuiCol_ChildWindowBg -> use ImGuiCol_ChildBg + - ImGuiStyleVar_ChildWindowRounding -> use ImGuiStyleVar_ChildRounding + - ImGuiTreeNodeFlags_AllowOverlapMode -> use ImGuiTreeNodeFlags_AllowItemOverlap + - IMGUI_DISABLE_TEST_WINDOWS -> use IMGUI_DISABLE_DEMO_WINDOWS + If you were still using the old names, while you are cleaning up, considering enabling + IMGUI_DISABLE_OBSOLETE_FUNCTIONS in imconfig.h even temporarily to have a pass at finding + and removing up old API calls, if any remaining. +- Removed implicit default parameter to IsMouseDragging(int button = 0) to be consistent + with other mouse functions (none of the other functions have it). +- Obsoleted calling ImDrawList::PrimReserve() with a negative count (which was vaguely + documented and rarely if ever used). Instead we added an explicit PrimUnreserve() API + which can be implemented faster. Also clarified pre-existing constraints which weren't + documented (can only unreserve from the last reserve call). If you suspect you ever + used that feature before (very unlikely, but grep for call to PrimReserve in your code), + you can #define IMGUI_DEBUG_PARANOID in imconfig.h to catch existing calls. [@ShironekoBen] +- ImDrawList::AddCircle()/AddCircleFilled() functions don't accept negative radius. +- Limiting Columns()/BeginColumns() api to 64 columns with an assert. While the current code + technically supports it, future code may not so we're putting the restriction ahead. + [Undid that change in 1.76] +- imgui_internal.h: changed ImRect() default constructor initializes all fields to 0.0f instead + of (FLT_MAX,FLT_MAX,-FLT_MAX,-FLT_MAX). If you used ImRect::Add() to create bounding boxes by + adding points into it without explicit initialization, you may need to fix your initial value. + +Other Changes: + +- Inputs: Added ImGuiMouseButton enum for convenience (e.g. ImGuiMouseButton_Right=1). + We forever guarantee that the existing value will not changes so existing code is free to use 0/1/2. +- Nav: Fixed a bug where the initial CTRL-Tab press while in a child window sometimes selected + the current root window instead of always selecting the previous root window. (#787) +- ColorEdit: Fix label alignment when using ImGuiColorEditFlags_NoInputs. (#2955) [@rokups] +- ColorEdit: In HSV display of a RGB stored value, attempt to locally preserve Saturation + when Value==0.0 (similar to changes done in 1.73 for Hue). Removed Hue editing lock since + those improvements in 1.73 makes them unnecessary. (#2722, #2770). [@rokups] +- ColorEdit: "Copy As" context-menu tool shows hex values with a '#' prefix instead of '0x'. +- ColorEdit: "Copy As" content-menu tool shows hex values both with/without alpha when available. +- InputText: Fix corruption or crash when executing undo after clearing input with ESC, as a + byproduct we are allowing to later undo the revert with a CTRL+Z. (#3008). +- InputText: Fix using a combination of _CallbackResize (e.g. for std::string binding), along with the + _EnterReturnsTrue flag along with the rarely used property of using an InputText without persisting + user-side storage. Previously if you had e.g. a local unsaved std::string and reading result back + from the widget, the user string object wouldn't be resized when Enter key was pressed. (#3009) +- MenuBar: Fix minor clipping issue where occasionally a menu text can overlap the right-most border. +- Window: Fix SetNextWindowBgAlpha(1.0f) failing to override alpha component. (#3007) [@Albog] +- Window: When testing for the presence of the ImGuiWindowFlags_NoBringToFrontOnFocus flag we + test both the focused/clicked window (which could be a child window) and the root window. +- ImDrawList: AddCircle(), AddCircleFilled() API can now auto-tessellate when provided a segment + count of zero. Alter tessellation quality with 'style.CircleSegmentMaxError'. [@ShironekoBen] +- ImDrawList: Add AddNgon(), AddNgonFilled() API with a guarantee on the explicit segment count. + In the current branch they are essentially the same as AddCircle(), AddCircleFilled() but as + we will rework the circle rendering functions to use textures and automatic segment count + selection, those new api can fill a gap. [@ShironekoBen] +- Columns: ImDrawList::Channels* functions now work inside columns. Added extra comments to + suggest using user-owned ImDrawListSplitter instead of ImDrawList functions. [@rokups] +- Misc: Added ImGuiMouseCursor_NotAllowed enum so it can be used by more shared widgets. [@rokups] +- Misc: Added IMGUI_DISABLE compile-time definition to make all headers and sources empty. +- Misc: Disable format checks when using stb_printf, to allow using extra formats. + Made IMGUI_USE_STB_SPRINTF a properly documented imconfig.h flag. (#2954) [@loicmolinari] +- Misc: Added misc/single_file/imgui_single_file.h, We use this to validate compiling all *.cpp + files in a same compilation unit. Actual users of that technique (also called "Unity builds") + can generally provide this themselves, so we don't really recommend you use this. [@rokups] +- CI: Added PVS-Studio static analysis on the continuous-integration server. [@rokups] +- Backends: GLFW, SDL, Win32, OSX, Allegro: Added support for ImGuiMouseCursor_NotAllowed. [@rokups] +- Backends: GLFW: Added support for the missing mouse cursors newly added in GLFW 3.4+. [@rokups] +- Backends: SDL: Wayland: use SDL_GetMouseState (because there is no global mouse state available + on Wayland). (#2800, #2802) [@NeroBurner] +- Backends: GLFW, SDL: report Windows key (io.KeySuper) as always released. Neither GLFW nor SDL can + correctly report the key release in every cases (e.g. when using Win+V) causing problems with some + widgets. The next release of GLFW (3.4+) will have a fix for it. However since it is both difficult + and discouraged to make use of this key for Windows application anyway, we just hide it. (#2976) +- Backends: Win32: Added support for #define IMGUI_IMPL_WIN32_DISABLE_GAMEPAD to disable all + XInput using code, and IMGUI_IMPL_WIN32_DISABLE_LINKING_XINPUT to disable linking with XInput, + the later may be problematic if compiling with recent Windows SDK and you want your app to run + on Windows 7. You can instead try linking with Xinput9_1_0.lib instead. (#2716) +- Backends: Glut: Improved FreeGLUT support for MinGW. (#3004) [@podsvirov] +- Backends: Emscripten: Avoid forcefully setting IMGUI_DISABLE_FILE_FUNCTIONS. (#3005) [@podsvirov] +- Examples: OpenGL: Explicitly adding -DIMGUI_IMPL_OPENGL_LOADER_GL3W to Makefile to match linking + settings (otherwise if another loader such as Glew is accessible, the OpenGL3 backend might + automatically use it). (#2919, #2798) +- Examples: OpenGL: Added support for glbinding OpenGL loader. (#2870) [@rokups] +- Examples: Emscripten: Demonstrating embedding fonts in Makefile and code. (#2953) [@Oipo] +- Examples: Metal: Wrapped main loop in @autoreleasepool block to ensure allocations get freed + even if underlying system event loop gets paused due to app nap. (#2910, #2917) [@bear24rw] + + +----------------------------------------------------------------------- + VERSION 1.74 (Released 2019-11-25) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.74 + +Breaking Changes: + +- Removed redirecting functions/enums names that were marked obsolete in 1.52 (October 2017): + - Begin() [old 5 args version] -> use Begin() [3 args], use SetNextWindowSize() SetNextWindowBgAlpha() if needed + - IsRootWindowOrAnyChildHovered() -> use IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows) + - AlignFirstTextHeightToWidgets() -> use AlignTextToFramePadding() + - SetNextWindowPosCenter() -> use SetNextWindowPos() with a pivot of (0.5f, 0.5f) + - ImFont::Glyph -> use ImFontGlyph + If you were still using the old names, read "API Breaking Changes" section of imgui.cpp to find out + the new names or equivalent features, or see how they were implemented until 1.73. +- Inputs: Fixed a miscalculation in the keyboard/mouse "typematic" repeat delay/rate calculation, used + by keys and e.g. repeating mouse buttons as well as the GetKeyPressedAmount() function. + If you were using a non-default value for io.KeyRepeatRate (previous default was 0.250), you can + add +io.KeyRepeatDelay to it to compensate for the fix. + The function was triggering on: 0.0 and (delay+rate*N) where (N>=1). Fixed formula responds to (N>=0). + Effectively it made io.KeyRepeatRate behave like it was set to (io.KeyRepeatRate + io.KeyRepeatDelay). + Fixed the code and altered default io.KeyRepeatRate,Delay from 0.250,0.050 to 0.300,0.050 to compensate. + If you never altered io.KeyRepeatRate nor used GetKeyPressedAmount() this won't affect you. +- Misc: Renamed IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS to IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS. (#1038) +- Misc: Renamed IMGUI_DISABLE_MATH_FUNCTIONS to IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS. +- Fonts: ImFontAtlas::AddCustomRectRegular() now requires an ID larger than 0x110000 (instead of 0x10000) to + conform with supporting Unicode planes 1-16 in a future update. ID below 0x110000 will now assert. +- Backends: DX12: Added extra ID3D12DescriptorHeap parameter to ImGui_ImplDX12_Init() function. + The value is unused in master branch but will be used by the multi-viewport feature. (#2851) [@obfuscate] + +Other Changes: + +- InputText, Nav: Fixed Home/End key broken when activating Keyboard Navigation. (#787) +- InputText: Filter out ASCII 127 (DEL) emitted by low-level OSX layer, as we are using the Key value. (#2578) +- Layout: Fixed a couple of subtle bounding box vertical positioning issues relating to the handling of text + baseline alignment. The issue would generally manifest when laying out multiple items on a same line, + with varying heights and text baseline offsets. + Some specific examples, e.g. a button with regular frame padding followed by another item with a + multi-line label and no frame padding, such as: multi-line text, small button, tree node item, etc. + The second item was correctly offset to match text baseline, and would interact/display correctly, + but it wouldn't push the contents area boundary low enough. +- Scrollbar: Fixed an issue where scrollbars wouldn't display on the frame following a frame where + all child window contents would be culled. +- ColorPicker: Fixed SV triangle gradient to block (broken in 1.73). (#2864, #2711). [@lewa-j] +- TreeNode: Fixed combination of ImGuiTreeNodeFlags_SpanFullWidth and ImGuiTreeNodeFlags_OpenOnArrow + incorrectly locating the arrow hit position to the left of the frame. (#2451, #2438, #1897) +- TreeNode: The collapsing arrow accepts click even if modifier keys are being held, facilitating + interactions with custom multi-selections patterns. (#2886, #1896, #1861) +- TreeNode: Added IsItemToggledOpen() to explicitly query if item was just open/closed, facilitating + interactions with custom multi-selections patterns. (#1896, #1861) +- DragScalar, SliderScalar, InputScalar: Added p_ prefix to parameter that are pointers to the data + to clarify how they are used, and more comments redirecting to the demo code. (#2844) +- Error handling: Assert if user mistakenly calls End() instead of EndChild() on a child window. (#1651) +- Misc: Optimized storage of window settings data (reducing allocation count). +- Misc: Windows: Do not use _wfopen() if IMGUI_DISABLE_WIN32_FUNCTIONS is defined. (#2815) +- Misc: Windows: Disabled win32 function by default when building with UWP. (#2892, #2895) +- Misc: Using static_assert() when using C++11, instead of our own construct (avoid zealous Clang warnings). +- Misc: Added IMGUI_DISABLE_FILE_FUNCTIONS/IMGUI_DISABLE_DEFAULT_FILE_FUNCTION to nullify or disable + default implementation of ImFileXXX functions linking with fopen/fclose/fread/fwrite. (#2734) +- Docs: Improved and moved FAQ to docs/FAQ.md so it can be readable on the web. [@ButternCream, @ocornut] +- Docs: Moved misc/fonts/README.txt to docs/FONTS.txt. +- Docs: Added permanent redirect from https://www.dearimgui.org/faq to FAQ page. +- Demo: Added simple item reordering demo in Widgets -> Drag and Drop section. (#2823, #143) [@rokups] +- Metrics: Show wire-frame mesh and approximate surface area when hovering ImDrawCmd. [@ShironekoBen] +- Metrics: Expose basic details of each window key/value state storage. +- Examples: DX12: Using IDXGIDebug1::ReportLiveObjects() when DX12_ENABLE_DEBUG_LAYER is enabled. +- Examples: Emscripten: Removed BINARYEN_TRAP_MODE=clamp from Makefile which was removed in Emscripten 1.39.0 + but required prior to 1.39.0, making life easier for absolutely no-one. (#2877, #2878) [@podsvirov] +- Backends: OpenGL2: Explicitly backup, setup and restore GL_TEXTURE_ENV to increase compatibility with + legacy OpenGL applications. (#3000) +- Backends: OpenGL3: Fix building with pre-3.2 GL loaders which do not expose glDrawElementsBaseVertex(), + using runtime GL version to decide if we set ImGuiBackendFlags_RendererHasVtxOffset. (#2866, #2852) [@dpilawa] +- Backends: OSX: Fix using Backspace key. (#2578, #2817, #2818) [@DiligentGraphics] +- Backends: GLFW: Previously installed user callbacks are now restored on shutdown. (#2836) [@malte-v] +- CI: Set up a bunch of continuous-integration tests using GitHub Actions. We now compile many of the example + applications on Windows, Linux, MacOS, iOS, Emscripten. Removed Travis integration. (#2865) [@rokups] + + +----------------------------------------------------------------------- + VERSION 1.73 (Released 2019-09-24) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.73 + +Other Changes: + +- Nav, Scrolling: Added support for Home/End key. (#787) +- ColorEdit: Disable Hue edit when Saturation==0 instead of letting Hue values jump around. +- ColorEdit, ColorPicker: In HSV display of a RGB stored value, attempt to locally preserve Hue + when Saturation==0, which reduces accidentally lossy interactions. (#2722, #2770) [@rokups] +- ColorPicker: Made rendering aware of global style alpha of the picker can be faded out. (#2711) + Note that some elements won't accurately fade down with the same intensity, and the color wheel + when enabled will have small overlap glitches with (style.Alpha < 1.0). +- TabBar: Fixed single-tab not shrinking their width down. +- TabBar: Fixed clicking on a tab larger than tab-bar width creating a bouncing feedback loop. +- TabBar: Feed desired width (sum of unclipped tabs width) into layout system to allow for auto-resize. (#2768) + (before 1.71 tab bars fed the sum of current width which created feedback loops in certain situations). +- TabBar: Improved shrinking for large number of tabs to avoid leaving extraneous space on the right side. + Individuals tabs are given integer-rounded width and remainder is spread between tabs left-to-right. +- Columns, Separator: Fixed a bug where non-visible separators within columns would alter the next row position + differently than visible ones. +- SliderScalar: Improved assert when using U32 or U64 types with a large v_max value. (#2765) [@loicmouton] +- DragInt, DragFloat, DragScalar: Using (v_min > v_max) allows locking any edits to the value. +- DragScalar: Fixed dragging of unsigned values on ARM cpu (float to uint cast is undefined). (#2780) [@dBagrat] +- TreeNode: Added ImGuiTreeNodeFlags_SpanAvailWidth flag. (#2451, #2438, #1897) [@Melix19, @PathogenDavid] + This extends the hit-box to the right-most edge, even if the node is not framed. + (Note: this is not the default in order to allow adding other items on the same line. In the future we will + aim toward refactoring the hit-system to be front-to-back, allowing more natural overlapping of items, + and then we will be able to make this the default.) +- TreeNode: Added ImGuiTreeNodeFlags_SpanFullWidth flag. This extends the hit-box to both the left-most and + right-most edge of the working area, bypassing indentation. +- CollapsingHeader: Added support for ImGuiTreeNodeFlags_Bullet and ImGuiTreeNodeFlags_Leaf on framed nodes, + mostly for consistency. (#2159, #2160) [@goran-w] +- Selectable: Added ImGuiSelectableFlags_AllowItemOverlap flag in public api (was previously internal only). +- Style: Allow style.WindowMenuButtonPosition to be set to ImGuiDir_None to hide the collapse button. (#2634, #2639) +- Font: Better ellipsis ("...") drawing implementation. Instead of drawing three pixel-ey dots (which was glaringly + unfitting with many types of fonts) we first attempt to find a standard ellipsis glyphs within the loaded set. + Otherwise we render ellipsis using '.' from the font from where we trim excessive spacing to make it as narrow + as possible. (#2775) [@rokups] +- ImDrawList: Clarified the name of many parameters so reading the code is a little easier. (#2740) +- ImDrawListSplitter: Fixed merging channels if the last submitted draw command used a different texture. (#2506) +- Using offsetof() when available in C++11. Avoids Clang sanitizer complaining about old-style macros. (#94) +- ImVector: Added find(), find_erase(), find_erase_unsorted() helpers. +- Added a mechanism to compact/free the larger allocations of unused windows (buffers are compacted when + a window is unused for 60 seconds, as per io.ConfigWindowsMemoryCompactTimer = 60.0f). Note that memory + usage has never been reported as a problem, so this is merely a touch of overzealous luxury. (#2636) +- Documentation: Various tweaks and improvements to the README page. [@ker0chan] +- Backends: OpenGL3: Tweaked initialization code allow application calling ImGui_ImplOpenGL3_CreateFontsTexture() + before ImGui_ImplOpenGL3_NewFrame(), which sometimes can be convenient. +- Backends: OpenGL3: Attempt to automatically detect default GL loader by using __has_include. (#2798) [@o-micron] +- Backends: DX11: Fixed GSGetShader() call not passing an initialized instance count, which would + generally make the DX11 debug layer complain (bug added in 1.72). +- Backends: Vulkan: Added support for specifying multisample count. Set 'ImGui_ImplVulkan_InitInfo::MSAASamples' to + one of the VkSampleCountFlagBits values to use, default is non-multisampled as before. (#2705, #2706) [@vilya] +- Examples: OSX: Fix example_apple_opengl2/main.mm not forwarding mouse clicks and drags correctly. (#1961, #2710) + [@intonarumori, @ElectricMagic] +- Misc: Updated stb_rect_pack.h from 0.99 to 1.00 (fixes by @rygorous: off-by-1 bug in best-fit heuristic, + fix handling of rectangles too large to fit inside texture). (#2762) [@tido64] + + +----------------------------------------------------------------------- + VERSION 1.72b (Released 2019-07-31) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.72b + +Other Changes: + +- Nav, Scrolling: Fixed programmatic scroll leading to a slightly incorrect scroll offset when + the window has decorations or a menu-bar (broken in 1.71). This was mostly noticeable when + a keyboard/gamepad movement led to scrolling the view, or using e.g. SetScrollHereY() function. +- Nav: Made hovering non-MenuItem Selectable not re-assign the source item for keyboard navigation. +- Nav: Fixed an issue with NavFlattened window flag (beta) where widgets not entirely fitting + in child window (often selectables because of their protruding sides) would be not considered + as entry points to to navigate toward the child window. (#787) + + +----------------------------------------------------------------------- + VERSION 1.72 (Released 2019-07-27) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.72 + +Breaking Changes: + +- Removed redirecting functions/enums names that were marked obsolete in 1.51 (June 2017): + - ImGuiCol_Column*, ImGuiSetCond_* enums. + - IsItemHoveredRect(), IsPosHoveringAnyWindow(), IsMouseHoveringAnyWindow(), IsMouseHoveringWindow() functions. + - IMGUI_ONCE_UPON_A_FRAME macro. + If you were still using the old names, read "API Breaking Changes" section of imgui.cpp to find out + the new names or equivalent features. +- Renamed ImFontAtlas::CustomRect to ImFontAtlasCustomRect. Kept redirection typedef (will obsolete). +- Removed TreeAdvanceToLabelPos() which is rarely used and only does SetCursorPosX(GetCursorPosX() + GetTreeNodeToLabelSpacing()). + Kept redirection function (will obsolete). (#581, #324) + +Other Changes: + +- Scrolling: Made mouse-wheel scrolling lock the underlying window until the mouse is moved again or + until a short delay expires (~2 seconds). This allow uninterrupted scroll even if child windows are + passing under the mouse cursor. (#2604) +- Scrolling: Made it possible for mouse wheel and navigation-triggered scrolling to override a call to + SetScrollX()/SetScrollY(), making it possible to use a simpler stateless pattern for auto-scrolling: + // (Submit items..) + if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) // If scrolling at the already at the bottom.. + ImGui::SetScrollHereY(1.0f); // ..make last item fully visible +- Scrolling: Added SetScrollHereX(), SetScrollFromPosX() for completeness. (#1580) [@kevreco] +- Scrolling: Mouse wheel scrolling while hovering a child window is automatically forwarded to parent window + if ScrollMax is zero on the scrolling axis. + Also still the case if ImGuiWindowFlags_NoScrollWithMouse is set (not new), but previously the forwarding + would be disabled if ImGuiWindowFlags_NoScrollbar was set on the child window, which is not the case + any more. Forwarding can still be disabled by setting ImGuiWindowFlags_NoInputs. (amend #1502, #1380). +- Window: Fixed InnerClipRect right-most coordinates using wrong padding setting (introduced in 1.71). +- Window: Fixed old SetWindowFontScale() api value from not being inherited by child window. Added + comments about the right way to scale your UI (load a font at the right side, rebuild atlas, scale style). +- Scrollbar: Avoid overlapping the opposite side when window (often a child window) is forcibly too small. +- Combo: Hide arrow when there's not enough space even for the square button. +- InputText: Testing for newly added ImGuiKey_KeyPadEnter key. (#2677, #2005) [@amc522] +- TabBar: Fixed unfocused tab bar separator color (was using ImGuiCol_Tab, should use ImGuiCol_TabUnfocusedActive). +- Columns: Fixed a regression from 1.71 where the right-side of the contents rectangle within each column + would wrongly use a WindowPadding.x instead of ItemSpacing.x like it always did. (#125, #2666) +- Columns: Made the right-most edge reaches up to the clipping rectangle (removing half of WindowPadding.x + worth of asymmetrical/extraneous padding, note that there's another half that conservatively has to offset + the right-most column, otherwise it's clipping width won't match the other columns). (#125, #2666) +- Columns: Improved honoring alignment with various values of ItemSpacing.x and WindowPadding.x. (#125, #2666) +- Columns: Made GetColumnOffset() and GetColumnWidth() behave when there's no column set, consistently with + other column functions. (#2683) +- InputTextMultiline: Fixed vertical scrolling tracking glitch. +- Word-wrapping: Fixed overzealous word-wrapping when glyph edge lands exactly on the limit. Because + of this, auto-fitting exactly unwrapped text would make it wrap. (fixes initial 1.15 commit, 78645a7d). +- Style: Attenuated default opacity of ImGuiCol_Separator in Classic and Light styles. +- Style: Added style.ColorButtonPosition (left/right, defaults to ImGuiDir_Right) to move the color button + of ColorEdit3/ColorEdit4 functions to either side of the inputs. +- IO: Added ImGuiKey_KeyPadEnter and support in various backends (previously backends would need to + specifically redirect key-pad keys to their regular counterpart). This is a temporary attenuating measure + until we actually refactor and add whole sets of keys into the ImGuiKey enum. (#2677, #2005) [@amc522] +- Misc: Made Button(), ColorButton() not trigger an "edited" event leading to IsItemDeactivatedAfterEdit() + returning true. This also effectively make ColorEdit4() not incorrect trigger IsItemDeactivatedAfterEdit() + when clicking the color button to open the picker popup. (#1875) +- Misc: Added IMGUI_DISABLE_METRICS_WINDOW imconfig.h setting to explicitly compile out ShowMetricsWindow(). +- Debug, Metrics: Added "Tools->Item Picker" tool which allow clicking on a widget to break in the debugger + within the item code. The tool calls IM_DEBUG_BREAK() which can be redefined in imconfig.h if needed. +- ImDrawList: Fixed CloneOutput() helper crashing. (#1860) [@gviot] +- ImDrawList::ChannelsSplit(), ImDrawListSplitter: Fixed an issue with merging draw commands between + channel 0 and 1. (#2624) +- ImDrawListSplitter: Fixed memory leak when using low-level split api (was not affecting ImDrawList api, + also this type was added in 1.71 and not advertised as a public-facing feature). +- Fonts: binary_to_compressed_c.cpp: Display an error message if failing to open/read the input font file. +- Demo: Log, Console: Using a simpler stateless pattern for auto-scrolling. +- Demo: Widgets: Showing how to use the format parameter of Slider/Drag functions to display the name + of an enum value instead of the underlying integer value. +- Demo: Renamed the "Help" menu to "Tools" (more accurate). +- Backends: DX10/DX11: Backup, clear and restore Geometry Shader is any is bound when calling renderer. +- Backends: DX11: Clear Hull Shader, Domain Shader, Compute Shader before rendering. Not backing/restoring them. +- Backends: OSX: Disabled default native Mac clipboard copy/paste implementation in core library (added in 1.71), + because it needs application to be linked with '-framework ApplicationServices'. It can be explicitly + enabled back by using '#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS' in imconfig.h. Re-added + equivalent using NSPasteboard api in the imgui_impl_osx.mm experimental backend. (#2546) +- Backends: SDL2: Added ImGui_ImplSDL2_InitForD3D() function to make D3D support more visible. + (#2482, #2632) [@josiahmanson] +- Examples: Added SDL2+DirectX11 example application. (#2632, #2612, #2482) [@vincenthamm] + + +----------------------------------------------------------------------- + VERSION 1.71 (Released 2019-06-12) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.71 + +Breaking Changes: + +- IO: changed AddInputCharacter(unsigned short c) signature to AddInputCharacter(unsigned int c). +- Renamed SetNextTreeNodeOpen() to SetNextItemOpen(). Kept inline redirection function (will obsolete). +- Window: rendering of child windows outer decorations (e.g. bg color, border, scrollbars) is now + performed as part of their parent window, avoiding the creation of an extraneous draw commands. + If you have overlapping child windows with decorations, and relied on their relative z-order to be + mapped to submission their order, this will affect your rendering. The optimization is disabled + if the parent window has no visual output because it appears to be the most common situation leading + to the creation of overlapping child windows. Please reach out if you are affected by this change! + +Other Changes: + +- Window: clarified behavior of SetNextWindowContentSize(). Content size is defined as the size available + after removal of WindowPadding on each sides. So SetNextWindowContentSize(ImVec2(100,100)) + auto-resize + will always allow submitting a 100x100 item without creating a scrollbar, regarding of WindowPadding. + The exact meaning of ContentSize for decorated windows was previously ill-defined. +- Window: Fixed auto-resize with AlwaysVerticalScrollbar or AlwaysHorizontalScrollbar flags. +- Window: Fixed one case where auto-resize by double-clicking the resize grip would make either scrollbar + appear for a single frame after the resize. +- Separator: Revert 1.70 "Declare its thickness (1.0f) to the layout" change. It's not incorrect + but it breaks existing some layout patterns. Will return back to it when we expose Separator flags. +- Fixed InputScalar, InputScalarN, SliderScalarN, DragScalarN with non-visible label from inserting + style.ItemInnerSpacing.x worth of trailing spacing. +- Fixed InputFloatX, SliderFloatX, DragFloatX functions erroneously reporting IsItemEdited() multiple + times when the text input doesn't match the formatted output value (e.g. input "1" shows "1.000"). + It wasn't much of a problem because we typically use the return value instead of IsItemEdited() here. +- Fixed uses of IsItemDeactivated(), IsItemDeactivatedAfterEdit() on multi-components widgets and + after EndGroup(). (#2550, #1875) +- Fixed crash when appending with BeginMainMenuBar() more than once and no other window are showing. (#2567) +- ColorEdit: Fixed the color picker popup only displaying inputs as HSV instead of showing multiple + options. (#2587, broken in 1.69 by #2384). +- CollapsingHeader: Better clipping when a close button is enabled and it overlaps the label. (#600) +- Scrollbar: Minor bounding box adjustment to cope with various border size. +- Scrollbar, Style: Changed default style.ScrollbarSize from 16 to 14. +- Combo: Fixed rounding not applying with the ImGuiComboFlags_NoArrowButton flag. (#2607) [@DucaRii] +- Nav: Fixed gamepad/keyboard moving of window affecting contents size incorrectly, sometimes leading + to scrollbars appearing during the movement. +- Nav: Fixed rare crash when e.g. releasing Alt-key while focusing a window with a menu at the same + frame as clearing the focus. This was in most noticeable in backends such as Glfw and SDL which + emits key release events when focusing another viewport, leading to Alt+clicking on void on another + viewport triggering the issue. (#2609) +- TreeNode, CollapsingHeader: Fixed highlight frame not covering horizontal area fully when using + horizontal scrolling. (#2211, #2579) +- TabBar: Fixed BeginTabBar() within a window with horizontal scrolling from creating a feedback + loop with the horizontal contents size. +- Columns: Fixed Columns() within a window with horizontal scrolling from not covering the full + horizontal area (previously only worked with an explicit contents size). (#125) +- Columns: Fixed Separator from creating an extraneous draw command. (#125) +- Columns: Fixed Selectable with SpanAllColumns flag from creating an extraneous draw command. (#125) +- Style: Added style.WindowMenuButtonPosition (left/right, defaults to ImGuiDir_Left) to move the + collapsing/docking button to the other side of the title bar. +- Style: Made window close button cross slightly smaller. +- Log/Capture: Fixed BeginTabItem() label not being included in a text log/capture. +- ImDrawList: Added ImDrawCmd::VtxOffset value to support large meshes (64k+ vertices) using 16-bit indices. + The renderer backend needs to set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset' to enable + this, and honor the ImDrawCmd::VtxOffset field. Otherwise the value will always be zero. (#2591) + This has the advantage of preserving smaller index buffers and allowing to execute on hardware that do not + support 32-bit indices. Most examples backends have been modified to support the VtxOffset field. +- ImDrawList: Added ImDrawCmd::IdxOffset value, equivalent to summing element count for each draw command. + This is provided for convenience and consistency with VtxOffset. (#2591) +- ImDrawCallback: Allow to override the signature of ImDrawCallback by #define-ing it. This is meant to + facilitate custom rendering backends passing local render-specific data to the draw callback. +- ImFontAtlas: FreeType: Added RasterizerFlags::Monochrome flag to disable font anti-aliasing. Combine + with RasterizerFlags::MonoHinting for best results. (#2545) [@HolyBlackCat] +- ImFontGlyphRangesBuilder: Fixed unnecessarily over-sized buffer, which incidentally was also not + fully cleared. Fixed edge-case overflow when adding character 0xFFFF. (#2568). [@NIKE3500] +- Demo: Added full "Dear ImGui" prefix to the title of "Dear ImGui Demo" and "Dear ImGui Metrics" windows. +- Backends: Add native Mac clipboard copy/paste default implementation in core library to match what we are + dealing with Win32, and to facilitate integration in custom engines. (#2546) [@andrewwillmott] +- Backends: OSX: imgui_impl_osx: Added mouse cursor support. (#2585, #1873) [@actboy168] +- Examples/Backends: DirectX9/10/11/12, Metal, Vulkan, OpenGL3 (Desktop GL only): Added support for large meshes + (64k+ vertices) with 16-bit indices, enable 'ImGuiBackendFlags_RendererHasVtxOffset' in those backends. (#2591) +- Examples/Backends: Don't filter characters under 0x10000 before calling io.AddInputCharacter(), + the filtering is done in io.AddInputCharacter() itself. This is in prevision for fuller Unicode + support. (#2538, #2541) + + +----------------------------------------------------------------------- + VERSION 1.70 (Released 2019-05-06) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.70 + +Breaking Changes: + +- ImDrawList: Improved algorithm for mitre joints on thick lines, preserving correct thickness + up to 90 degrees angles (e.g. rectangles). If you have custom rendering using thick lines, + they will appear a little thicker now. (#2518) [@rmitton] +- Obsoleted GetContentRegionAvailWidth(), use GetContentRegionAvail().x instead. + Kept inline redirection function. +- Examples: Vulkan: Added MinImageCount/ImageCount fields in ImGui_ImplVulkan_InitInfo, required + during initialization to specify the number of in-flight image requested by swap chains. + (was previously a hard #define IMGUI_VK_QUEUED_FRAMES 2). (#2071, #1677) [@nathanvoglsam] +- Examples: Vulkan: Tidying up the demo/internals helpers (most engine/app should not rely + on them but it is possible you have!). + +Other Changes: + +- ImDrawList: Added ImDrawCallback_ResetRenderState, a special ImDrawList::AddCallback() value + to request the renderer backend to reset its render state. (#2037, #1639, #2452) + Examples: Added support for ImDrawCallback_ResetRenderState in all renderer backends. Each + renderer code setting up initial render state has been moved to a function so it could be + called at the start of rendering and when a ResetRenderState is requested. [@ocornut, @bear24rw] +- InputText: Fixed selection background rendering one frame after the cursor movement when + first transitioning from no-selection to has-selection. (Bug in 1.69) (#2436) [@Nazg-Gul] +- InputText: Work-around for buggy standard libraries where isprint('\t') returns true. (#2467, #1336) +- InputText: Fixed ImGuiInputTextFlags_AllowTabInput leading to two tabs characters being inserted + if the backend provided both Key and Character input. (#2467, #1336) +- Layout: Added SetNextItemWidth() helper to avoid using PushItemWidth/PopItemWidth() for single items. + Note that SetNextItemWidth() currently only affect the same subset of items as PushItemWidth(), + generally referred to as the large framed+labeled items. Because the new SetNextItemWidth() + function is explicit we may later extend its effect to more items. +- Layout: Fixed PushItemWidth(-width) for right-side alignment laying out some items (button, listbox, etc.) + with negative sizes if the 'width' argument was smaller than the available width at the time of item + submission. +- Window: Fixed window with the AlwaysAutoResize flag unnecessarily extending their hovering boundaries + by a few pixels (this is used to facilitate resizing from borders when available for a given window). + One of the noticeable minor side effect was that navigating menus would have had a tendency to disable + highlight from parent menu items earlier than necessary while approaching the child menu. +- Window: Close button is horizontally aligned with style.FramePadding.x. +- Window: Fixed contents region being off by WindowBorderSize amount on the right when scrollbar is active. +- Window: Fixed SetNextWindowSizeConstraints() with non-rounded positions making windows drift. (#2067, #2530) +- Popups: Closing a popup restores the focused/nav window in place at the time of the popup opening, + instead of restoring the window that was in the window stack at the time of the OpenPopup call. (#2517) + Among other things, this allows opening a popup while no window are focused, and pressing Escape to + clear the focus again. +- Popups: Fixed right-click from closing all popups instead of aiming at the hovered popup level + (regression in 1.67). +- Selectable: With ImGuiSelectableFlags_AllowDoubleClick doesn't return true on the mouse button release + following the double-click. Only first mouse release + second mouse down (double-click) returns true. + Likewise for internal ButtonBehavior() with both _PressedOnClickRelease | _PressedOnDoubleClick. (#2503) +- GetMouseDragDelta(): also returns the delta on the mouse button released frame. (#2419) +- GetMouseDragDelta(): verify that mouse positions are valid otherwise returns zero. +- Inputs: Also add support for horizontal scroll with Shift+Mouse Wheel. (#2424, #1463) [@LucaRood] +- PlotLines, PlotHistogram: Ignore NaN values when calculating min/max bounds. (#2485) +- Columns: Fixed boundary of clipping being off by 1 pixel within the left column. (#125) +- Separator: Declare its thickness (1.0f) to the layout, making items around separator more symmetrical. +- Combo, Slider, Scrollbar: Improve rendering in situation when there's only a few pixels available (<3 pixels). +- Nav: Fixed Drag/Slider functions going into text input mode when keyboard CTRL is held while pressing NavActivate. +- Drag and Drop: Fixed drag source with ImGuiDragDropFlags_SourceAllowNullID and null ID from receiving click + regardless of being covered by another window (it didn't honor correct hovering rules). (#2521) +- ImDrawList: Improved algorithm for mitre joints on thick lines, preserving correct thickness up to 90 degrees + angles, also faster to output. (#2518) [@rmitton] +- Misc: Added IM_MALLOC/IM_FREE macros mimicking IM_NEW/IM_DELETE so user doesn't need to revert + to using the ImGui::MemAlloc()/MemFree() calls directly. +- Misc: Made IMGUI_CHECKVERSION() macro also check for matching size of ImDrawIdx. +- Metrics: Added "Show windows rectangles" tool to visualize the different rectangles. +- Demo: Improved trees in columns demo. +- Examples: OpenGL: Added a test GL call + comments in ImGui_ImplOpenGL3_Init() to detect uninitialized + GL function loaders early, and help users understand what they are missing. (#2421) +- Examples: SDL: Added support for SDL_GameController gamepads (enable with ImGuiConfigFlags_NavEnableGamepad). (#2509) [@DJLink] +- Examples: Emscripten: Added Emscripten+SDL+GLES2 example. (#2494, #2492, #2351, #336) [@nicolasnoble, @redblobgames] +- Examples: Metal: Added Glfw+Metal example. (#2527) [@bear24rw] +- Examples: OpenGL3: Minor tweaks + not calling glBindBuffer more than necessary in the render loop. +- Examples: Vulkan: Fixed in-flight buffers issues when using multi-viewports. (#2461, #2348, #2378, #2097) +- Examples: Vulkan: Added missing support for 32-bit indices (#define ImDrawIdx unsigned int). +- Examples: Vulkan: Avoid passing negative coordinates to vkCmdSetScissor, which debug validation layers do not like. +- Examples: Vulkan: Added ImGui_ImplVulkan_SetMinImageCount() to change min image count at runtime. (#2071) [@nathanvoglsam] +- Examples: DirectX9: Fixed erroneous assert in ImGui_ImplDX9_InvalidateDeviceObjects(). (#2454) +- Examples: DirectX10/11/12, Allegro, Marmalade: Render functions early out when display size is zero (minimized). (#2496) +- Examples: GLUT: Fixed existing FreeGLUT example to work with regular GLUT. (#2465) [@andrewwillmott] +- Examples: GLUT: Renamed imgui_impl_freeglut.cpp/.h to imgui_impl_glut.cpp/.h. (#2465) [@andrewwillmott] +- Examples: GLUT: Made io.DeltaTime always > 0. (#2430) +- Examples: Visual Studio: Updated default platform toolset+sdk in vcproj files from v100+sdk7 (vs2010) + to v110+sdk8 (vs2012). This is mostly so we can remove reliance on DXSDK_DIR for the DX10/DX11 example, + which if existing and when switching to recent SDK ends up conflicting and creating warnings. + + +----------------------------------------------------------------------- + VERSION 1.69 (Released 2019-03-13) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.69 + +Breaking Changes: + +- Renamed ColorEdit/ColorPicker's ImGuiColorEditFlags_RGB/_HSV/_HEX flags to respectively + ImGuiColorEditFlags_DisplayRGB/_DisplayHSV/_DisplayHex. This is because the addition of + new flag ImGuiColorEditFlags_InputHSV makes the earlier one ambiguous. + Kept redirection enum values (will obsolete). (#2384) [@haldean] +- Renamed GetOverlayDrawList() to GetForegroundDrawList(). Kept redirection function (will obsolete). (#2391) + +Other Changes: + +- Added GetBackgroundDrawList() helper to quickly get access to a ImDrawList that will be rendered + behind every other windows. (#2391, #545) +- DragScalar, InputScalar, SliderScalar: Added support for u8/s8/u16/s16 data types (ImGuiDataType_S8, etc.) + We are reusing function instances of larger types to reduce code size. (#643, #320, #708, #1011) +- Added InputTextWithHint() to display a description/hint in the text box when no text + has been entered. (#2400) [@Organic-Code, @ocornut] +- Nav: Fixed a tap on AltGR (e.g. German keyboard) from navigating to the menu layer. +- Nav: Fixed Ctrl+Tab keeping active InputText() of a previous window active after the switch. (#2380) +- Fixed IsItemDeactivated()/IsItemDeactivatedAfterEdit() from not correctly returning true + when tabbing out of a focusable widget (Input/Slider/Drag) in most situations. (#2215, #1875) +- InputInt, InputFloat, InputScalar: Fix to keep the label of the +/- buttons centered when + style.FramePadding.x is abnormally larger than style.FramePadding.y. Since the buttons are + meant to be square (to align with e.g. color button) we always use FramePadding.y. (#2367) +- InputInt, InputScalar: +/- buttons now respects the natural type limits instead of + overflowing or underflowing the value. +- InputText: Fixed an edge case crash that would happen if another widget sharing the same ID + is being swapped with an InputText that has yet to be activated. +- InputText: Fixed various display corruption related to swapping the underlying buffer while + a input widget is active (both for writable and read-only paths). Often they would manifest + when manipulating the scrollbar of a multi-line input text. +- ColorEdit, ColorPicker, ColorButton: Added ImGuiColorEditFlags_InputHSV to manipulate color + values encoded as HSV (in order to avoid HSV<>RGB round trips and associated singularities). + (#2383, #2384) [@haldean] +- ColorPicker: Fixed a bug/assertion when displaying a color picker in a collapsed window + while dragging its title bar. (#2389) +- ColorEdit: Fixed tooltip not honoring the ImGuiColorEditFlags_NoAlpha contract of never + reading the 4th float in the array (value was read and discarded). (#2384) [@haldean] +- MenuItem, Selectable: Fixed disabled widget interfering with navigation (fix c2db7f63 in 1.67). +- TabBar: Fixed a crash when using many BeginTabBar() recursively (didn't affect docking). (#2371) +- TabBar: Added extra mis-usage error recovery. Past the assert, common mis-usage don't lead to + hard crashes any more, facilitating integration with scripting languages. (#1651) +- TabBar: Fixed ImGuiTabItemFlags_SetSelected being ignored if the tab is not visible (with + scrolling policy enabled) or if is currently appearing. +- TabBar: Fixed Tab tooltip code making drag and drop tooltip disappear during the frame where + the drag payload activate a tab. +- TabBar: Reworked scrolling policy (when ImGuiTabBarFlags_FittingPolicyScroll is set) to + teleport the view when aiming at a tab far away the visible section, and otherwise accelerate + the scrolling speed to cap the scrolling time to 0.3 seconds. +- Text: Fixed large Text/TextUnformatted calls not feeding their size into layout when starting + below the lower point of the current clipping rectangle. This bug has been there since v1.0! + It was hardly noticeable but would affect the scrolling range, which in turn would affect + some scrolling request functions when called during the appearing frame of a window. +- Plot: Fixed divide-by-zero in PlotLines() when passing a count of 1. (#2387) [@Lectem] +- Log/Capture: Fixed LogXXX functions emitting extraneous leading carriage return. +- Log/Capture: Fixed an issue when empty string on a new line would not emit a carriage return. +- Log/Capture: Fixed LogXXX functions 'auto_open_depth' parameter being treated as an absolute + tree depth instead of a relative one. +- Log/Capture: Fixed CollapsingHeader trailing ascii representation being "#" instead of "##". +- ImFont: Added GetGlyphRangesVietnamese() helper. (#2403) +- Misc: Asserting in NewFrame() if style.WindowMinSize is zero or smaller than (1.0f,1.0f). +- Demo: Using GetBackgroundDrawList() and GetForegroundDrawList() in "Custom Rendering" demo. +- Demo: InputText: Demonstrating use of ImGuiInputTextFlags_CallbackResize. (#2006, #1443, #1008). +- Examples: GLFW, SDL: Preserve DisplayFramebufferScale when main viewport is minimized. + (This is particularly useful for the viewport branch because we are not supporting per-viewport + frame-buffer scale. It fixes windows not refreshing when main viewport is minimized.) (#2416) +- Examples: OpenGL: Fix to be able to run on ES 2.0 / WebGL 1.0. [@rmitton, @gabrielcuvillier] +- Examples: OpenGL: Fix for OSX not supporting OpenGL 4.5, we don't try to read GL_CLIP_ORIGIN + even if the OpenGL headers/loader happens to define the value. (#2366, #2186) +- Examples: Allegro: Added support for touch events (emulating mouse). (#2219) [@dos1] +- Examples: DirectX9: Minor changes to match the other DirectX examples more closely. (#2394) + + +----------------------------------------------------------------------- + VERSION 1.68 (Released 2019-02-19) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.68 + +Breaking Changes: + +- Removed io.DisplayVisibleMin/DisplayVisibleMax (which were marked obsolete and removed from viewport/docking branch already). +- Made it illegal/assert when io.DisplayTime == 0.0f (with an exception for the first frame). + If for some reason your time step calculation gives you a zero value, replace it with a arbitrarily small value! + +Other Changes: + +- Added .editorconfig file for text editors to standardize using spaces. (#2038) [@kudaba] +- ImDrawData: Added FramebufferScale field (currently a copy of the value from io.DisplayFramebufferScale). + This is to allow render functions being written without pulling any data from ImGuiIO, allowing incoming + multi-viewport feature to behave on Retina display and with multiple displays. + If you are not using a custom backend, please update your render function code ahead of time, + and use draw_data->FramebufferScale instead of io.DisplayFramebufferScale. (#2306, #1676) +- Added IsItemActivated() as an extension to the IsItemDeactivated/IsItemDeactivatedAfterEdit functions + which are useful to implement variety of undo patterns. (#820, #956, #1875) +- InputText: Fixed a bug where ESCAPE would not restore the initial value in all situations. (#2321) [@relick] +- InputText: Fixed a bug where ESCAPE would be first captured by the Keyboard Navigation code. (#2321, #787) +- InputText: Fixed redo buffer exhaustion handling (rare) which could corrupt the undo character buffer. (#2333) + The way the redo/undo buffers work would have made it generally unnoticeable to the user. +- Fixed range-version of PushID() and GetID() not honoring the ### operator to restart from the seed value. +- Fixed CloseCurrentPopup() on a child-menu of a modal incorrectly closing the modal. (#2308) +- Tabs: Added ImGuiTabBarFlags_TabListPopupButton flag to show a popup button on manual tab bars. (#261, #351) +- Tabs: Removed ImGuiTabBarFlags_NoTabListPopupButton which was available in 1.67 but actually had zero use. +- Tabs: Fixed a minor clipping glitch when changing style's FramePadding from frame to frame. +- Tabs: Fixed border (when enabled) so it is aligned correctly mid-pixel and appears as bright as other borders. +- Style, Selectable: Added ImGuiStyle::SelectableTextAlign and ImGuiStyleVar_SelectableTextAlign. (#2347) [@haldean] +- Menus: Tweaked horizontal overlap between parent and child menu (to help convey relative depth) + from using style.ItemSpacing.x to style.ItemInnerSpacing.x, the later being expected to be smaller. (#1086) +- RadioButton: Fixed label horizontal alignment to precisely match Checkbox(). +- Window: When resizing from an edge, the border is more visible and better follow the rounded corners. +- Window: Fixed initial width of collapsed windows not taking account of contents width (broken in 1.67). (#2336, #176) +- Scrollbar: Fade out and disable interaction when too small, in order to facilitate using the resize grab on very + small window, as well as reducing visual noise/overlap. +- ListBox: Better optimized when clipped / non-visible. +- InputTextMultiline: Better optimized when clipped / non-visible. +- Font: Fixed high-level ImGui::CalcTextSize() used by most widgets from erroneously subtracting 1.0f*scale to + calculated text width. Among noticeable side-effects, it would make sequences of repeated Text/SameLine calls + not align the same as a single call, and create mismatch between high-level size calculation and those performed + with the lower-level ImDrawList api. (#792) [@SlNPacifist] +- Font: Fixed building atlas when specifying duplicate/overlapping ranges within a same font. (#2353, #2233) +- ImDrawList: Fixed AddCircle(), AddCircleFilled() angle step being off, which was visible when drawing a "circle" + with a small number of segments (e.g. an hexagon). (#2287) [@baktery] +- ImGuiTextBuffer: Added append() function (unformatted). +- ImFontAtlas: Added 0x2000-0x206F general punctuation range to default ChineseFull/ChineseSimplifiedCommon ranges. (#2093) +- ImFontAtlas: FreeType: Added support for imgui allocators + custom FreeType only SetAllocatorFunctions. (#2285) [@Vuhdo] +- ImFontAtlas: FreeType: Fixed using imgui_freetype.cpp in unity builds. (#2302) +- Demo: Fixed "Log" demo not initializing properly, leading to the first line not showing before a Clear. (#2318) [@bluescan] +- Demo: Added "Auto-scroll" option in Log/Console demos. (#2300) [@nicolasnoble, @ocornut] +- Examples: Metal, OpenGL2, OpenGL3, Vulkan: Fixed offsetting of clipping rectangle with ImDrawData::DisplayPos != (0,0) + when the display frame-buffer scale scale is not (1,1). While this doesn't make a difference when using master branch, + this is effectively fixing support for multi-viewport with Mac Retina Displays on those examples. (#2306) [@rasky, @ocornut] + Also using ImDrawData::FramebufferScale instead of io.DisplayFramebufferScale. +- Examples: Clarified the use the ImDrawData::DisplayPos to offset clipping rectangles. +- Examples: Win32: Using GetForegroundWindow()+IsChild() instead of GetActiveWindow() to be compatible with windows created + in a different thread or parent. (#1951, #2087, #2156, #2232) [many people] +- Examples: SDL: Using the SDL_WINDOW_ALLOW_HIGHDPI flag. (#2306, #1676) [@rasky] +- Examples: Win32: Added support for XInput gamepads (if ImGuiConfigFlags_NavEnableGamepad is enabled). +- Examples: Win32: Added support for mouse buttons 4 and 5 via WM_XBUTTON* messages. (#2264) +- Examples: DirectX9: Explicitly disable fog (D3DRS_FOGENABLE) before drawing in case user state has it set. (#2288, #2230) +- Examples: OpenGL2: Added #define GL_SILENCE_DEPRECATION to cope with newer XCode warnings. +- Examples: OpenGL3: Using GLSL 4.10 shaders for any GLSL version over 410 (e.g. 430, 450). (#2329) [@BrutPitt] + + +----------------------------------------------------------------------- + VERSION 1.67 (Released 2019-01-14) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.67 + +Breaking Changes: + +- Made it illegal to call Begin("") with an empty string. This somehow half-worked before but had various undesirable + side-effect because the window would have ID zero. In particular it is causing problems in viewport/docking branches. +- Renamed io.ConfigResizeWindowsFromEdges to io.ConfigWindowsResizeFromEdges and removed its [Beta] mark. + The addition of new configuration options in the Docking branch is pushing for a little reorganization of those names. +- Renamed ImFontAtlas::GlyphRangesBuilder to ImFontGlyphRangesBuilder. Kept redirection typedef (will obsolete). + +Other Changes: +- Added BETA api for Tab Bar/Tabs widgets: (#261, #351) + - Added BeginTabBar(), EndTabBar(), BeginTabItem(), EndTabItem(), SetTabItemClosed() API. + - Added ImGuiTabBarFlags flags for BeginTabBar(). + - Added ImGuiTabItemFlags flags for BeginTabItem(). + - Style: Added ImGuiCol_Tab, ImGuiCol_TabHovered, ImGuiCol_TabActive, ImGuiCol_TabUnfocused, ImGuiCol_TabUnfocusedActive colors. + - Demo: Added Layout->Tabs demo code. + - Demo: Added "Documents" example app showcasing possible use for tabs. + This feature was merged from the Docking branch in order to allow the use of regular tabs in your code. + (It does not provide the docking/splitting/merging of windows available in the Docking branch) +- Added ImGuiWindowFlags_UnsavedDocument window flag to append '*' to title without altering the ID, as a convenience + to avoid using the ### operator. In the Docking branch this also has an effect on tab closing behavior. +- Window, Focus, Popup: Fixed an issue where closing a popup by clicking another window with the _NoMove flag would refocus + the parent window of the popup instead of the newly clicked window. +- Window: Contents size is preserved while a window collapsed. Fix auto-resizing window losing their size for one frame when uncollapsed. +- Window: Contents size is preserved while a window contents is hidden (unless it is hidden for resizing purpose). +- Window: Resizing windows from edge is now enabled by default (io.ConfigWindowsResizeFromEdges=true). Note that + it only works _if_ the backend sets ImGuiBackendFlags_HasMouseCursors, which the standard backends do. +- Window: Added io.ConfigWindowsMoveFromTitleBarOnly option. This is ignored by window with no title bars (often popups). + This affects clamping window within the visible area: with this option enabled title bars need to be visible. (#899) +- Window: Fixed using SetNextWindowPos() on a child window (which wasn't really documented) position the cursor as expected + in the parent window, so there is no mismatch between the layout in parent and the position of the child window. +- InputFloat: When using ImGuiInputTextFlags_ReadOnly the step buttons are disabled. (#2257) +- DragFloat: Fixed broken mouse direction change with power!=1.0. (#2174, #2206) [@Joshhua5] +- Nav: Fixed an keyboard issue where holding Activate/Space for longer than two frames on a button would unnecessary + keep the focus on the parent window, which could steal it from newly appearing windows. (#787) +- Nav: Fixed animated window titles from being updated when displayed in the CTRL+Tab list. (#787) +- Error recovery: Extraneous/undesired calls to End() are now being caught by an assert in the End() function closer + to the user call site (instead of being reported in EndFrame). Past the assert, they don't lead to crashes any more. (#1651) + Missing calls to End(), past the assert, should not lead to crashes or to the fallback Debug window appearing on screen. + Those changes makes it easier to integrate dear imgui with a scripting language allowing, given asserts are redirected + into e.g. an error log and stopping the script execution. +- ImFontAtlas: Stb and FreeType: Atlas width is now properly based on total surface rather than glyph count (unless overridden with TexDesiredWidth). +- ImFontAtlas: Stb and FreeType: Fixed atlas builder so missing glyphs won't influence the atlas texture width. (#2233) +- ImFontAtlas: Stb and FreeType: Fixed atlas builder so duplicate glyphs (when merging fonts) won't be included in the rasterized atlas. +- ImFontAtlas: FreeType: Fixed abnormally high atlas height. +- ImFontAtlas: FreeType: Fixed support for any values of TexGlyphPadding (not just only 1). +- ImDrawList: Optimized some of the functions for performance of debug builds where non-inline function call cost are non-negligible. + (Our test UI scene on VS2015 Debug Win64 with /RTC1 went ~5.9 ms -> ~4.9 ms. In Release same scene stays at ~0.3 ms.) +- IO: Added BackendPlatformUserData, BackendRendererUserData, BackendLanguageUserData void* for storage use by backends. +- IO: Renamed InputCharacters[], marked internal as was always intended. Please don't access directly, and use AddInputCharacter() instead! +- IO: AddInputCharacter() goes into a queue which can receive as many characters as needed during the frame. This is useful + for automation to not have an upper limit on typing speed. Will later transition key/mouse to use the event queue later. +- Style: Tweaked default value of style.DisplayWindowPadding from (20,20) to (19,19) so the default style as a value + which is the same as the title bar height. +- Demo: "Simple Layout" and "Style Editor" are now using tabs. +- Demo: Added a few more things under "Child windows" (changing ImGuiCol_ChildBg, positioning child, using IsItemHovered after a child). +- Examples: DirectX10/11/12: Made imgui_impl_dx10/dx11/dx12.cpp link d3dcompiler.lib from the .cpp file to ease integration. +- Examples: Allegro 5: Properly destroy globals on shutdown to allow for restart. (#2262) [@DomRe] + + +----------------------------------------------------------------------- + VERSION 1.66b (Released 2018-12-01) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.66b + +Other Changes: + +- Fixed a text rendering/clipping bug introduced in 1.66 (on 2018-10-12, commit ede3a3b9) that affect single ImDrawList::AddText() + calls with single strings larger than 10k. Text/TextUnformatted() calls were not affected, but e.g. InputText() was. [@pdoane] +- When the focused window become inactive don't restore focus to a window with the ImGuiWindowFlags_NoInputs flag. (#2213) [@zzzyap] +- Separator: Fixed Separator() outputting an extraneous empty line when captured into clipboard/text/file. +- Demo: Added ShowAboutWindow() call, previously was only accessible from the demo window. +- Demo: ShowAboutWindow() now display various Build/Config Information (compiler, os, etc.) that can easily be copied into bug reports. +- Fixed build issue with osxcross and macOS. (#2218) [@dos1] +- Examples: Setting up 'io.BackendPlatformName'/'io.BackendRendererName' fields to the current backend can be displayed in the About window. +- Examples: SDL: changed the signature of ImGui_ImplSDL2_ProcessEvent() to use a const SDL_Event*. (#2187) + + +----------------------------------------------------------------------- + VERSION 1.66 (Released 2018-11-22) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.66 + +Breaking Changes: + +- Renamed SetScrollHere() to SetScrollHereY(). Kept redirection function (will obsolete). +- Renamed misc/stl/imgui_stl.* to misc/cpp/imgui_stdlib.* in prevision for other C++ helper files. (#2035, #2096) + +Other Changes: + +- Fixed calling SetNextWindowSize()/SetWindowSize() with non-integer values leading to + accidental alteration of window position. We now round the provided size. (#2067) +- Fixed calling DestroyContext() always saving .ini data with the current context instead + of the supplied context pointer. (#2066) +- Nav, Focus: Fixed ImGuiWindowFlags_NoBringToFrontOnFocus windows not being restoring focus + properly after the main menu bar or last focused window is deactivated. +- Nav: Fixed an assert in certain circumstance (mostly when using popups) when mouse positions stop being valid. (#2168) +- Nav: Fixed explicit directional input not re-highlighting current nav item if there is a single item in the window + and highlight has been previously disabled by the mouse. (#787) +- DragFloat: Fixed a situation where dragging with value rounding enabled or with a power curve + erroneously wrapped the value to one of the min/max edge. (#2024, #708, #320, #2075). +- DragFloat: Disabled using power curve when one edge is FLT_MAX (broken in 1.61). (#2024) +- DragFloat: Disabled setting a default drag speed when one edge is FLT_MAX. (#2024) +- SliderAngle: Added optional format argument to alter precision or localize the string. (#2150) [@podsvirov] +- Window: Resizing from edges (with io.ConfigResizeWindowsFromEdges Beta flag) extends the hit region + of root floating windows outside the window, making it easier to resize windows. Resize grips are also + extended accordingly so there are no discontinuity when hovering between borders and corners. (#1495, #822) +- Window: Added ImGuiWindowFlags_NoBackground flag to avoid rendering window background. This is mostly to allow + the creation of new flag combinations, as we could already use SetNextWindowBgAlpha(0.0f). (#1660) [@biojppm, @ocornut] +- Window: Added ImGuiWindowFlags_NoDecoration helper flag which is essentially NoTitleBar+NoResize+NoScrollbar+NoCollapse. +- Window: Added ImGuiWindowFlags_NoMouseInputs which is basically the old ImGuiWindowFlags_NoInputs (essentially + we have renamed ImGuiWindowFlags_NoInputs to ImGuiWindowFlags_NoMouseInputs). Made the new ImGuiWindowFlags_NoInputs + encompass both NoMouseInputs+NoNav, which is consistent with its description. (#1660, #787) +- Window, Inputs: Fixed resizing from edges when io.MousePos is not pixel-rounded by rounding mouse position input. (#2110) +- BeginChild(): Fixed BeginChild(const char*, ...) variation erroneously not applying the ID stack + to the provided string to uniquely identify the child window. This was undoing an intentional change + introduced in 1.50 and broken in 1.60. (#1698, #894, #713). +- TextUnformatted(): Fixed a case where large-text path would read bytes past the text_end marker depending + on the position of new lines in the buffer (it wasn't affecting the output but still not the right thing to do!) +- ListBox(): Fixed frame sizing when items_count==1 unnecessarily showing a scrollbar. (#2173) [@luk1337, @ocornut] +- ListBox(): Tweaked frame sizing so list boxes will look more consistent when FramePadding is far from ItemSpacing. +- RenderText(): Some optimization for very large text buffers, useful for non-optimized builds. +- BeginMenu(): Fixed menu popup horizontal offset being off the item in the menu bar when WindowPadding=0.0f. +- ArrowButton(): Fixed arrow shape being horizontally misaligned by (FramePadding.y-FramePadding.x) if they are different. +- Demo: Split the contents of ShowDemoWindow() into smaller functions as it appears to speed up link time with VS. (#2152) +- Drag and Drop: Added GetDragDropPayload() to peek directly into the payload (if any) from anywhere. (#143) +- ImGuiTextBuffer: Avoid heap allocation when empty. +- ImDrawList: Fixed AddConvexPolyFilled() undefined behavior when passing points_count smaller than 3, + in particular, points_count==0 could lead to a memory stomp if the draw list was previously empty. +- Examples: DirectX10, DirectX11: Removed seemingly unnecessary calls to invalidate and recreate device objects + in the WM_SIZE handler. (#2088) [@ice1000] +- Examples: GLFW: User previously installed GLFW callbacks are now saved and chain-called by the default callbacks. (#1759) +- Examples: OpenGL3: Added support for GL 4.5's glClipControl(GL_UPPER_LEFT). (#2186) +- Examples: OpenGL3+GLFW: Fixed error condition when using the GLAD loader. (#2157) [@blackball] +- Examples: OpenGL3+GLFW/SDL: Made main.cpp compile with IMGUI_IMPL_OPENGL_LOADER_CUSTOM (may be missing init). (#2178) [@doug-moen] +- Examples: SDL2+Vulkan: Fixed application shutdown which could deadlock on Linux + Xorg. (#2181) [@eRabbit0] + + +----------------------------------------------------------------------- + VERSION 1.65 (Released 2018-09-06) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.65 + +Breaking Changes: + +- Renamed stb_truetype.h to imstb_truetype.h, stb_textedit.h to imstb_textedit.h, and + stb_rect_pack.h to imstb_rectpack.h. If you were conveniently using the imgui copy of those + STB headers in your project, you will have to update your include paths. (#1718, #2036) + The reason for this change is to avoid conflicts for projects that may also be importing + their own copy of the STB libraries. Note that imgui's copy of stb_textedit.h is modified. +- Renamed io.ConfigCursorBlink to io.ConfigInputTextCursorBlink. (#1427) + +Other Changes: + +- This is a minor release following the 1.64 refactor, with a little more shuffling of code. +- Clarified and improved the source code sectioning in all files (easier to search or browse sections). +- Nav: Removed the [Beta] tag from various descriptions of the gamepad/keyboard navigation system. + Although it is not perfect and will keep being improved, it is fairly functional and used by many. (#787) +- Fixed a build issue with non-Cygwin GCC under Windows. +- Demo: Added a "Configuration" block to make io.ConfigFlags/io.BackendFlags more prominent. +- Examples: OpenGL3+SDL2: Fixed error condition when using the GLAD loader. (#2059, #2002) [@jiri] + + +----------------------------------------------------------------------- + VERSION 1.64 (Released 2018-08-31) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.64 + +Changes: + +- Moved README, CHANGELOG and TODO files to the docs/ folder. + If you are updating dear imgui by copying files, take the chance to delete the old files. +- Added imgui_widgets.cpp file, extracted and moved widgets code out of imgui.cpp into imgui_widgets.cpp. + Re-ordered some of the code remaining in imgui.cpp. + NONE OF THE FUNCTIONS HAVE CHANGED. THE CODE IS SEMANTICALLY 100% IDENTICAL, BUT _EVERY_ FUNCTIONS HAS BEEN MOVED. + Because of this, any local modifications to imgui.cpp will likely conflict when you update. + If you have any modifications to imgui.cpp, it is suggested that you first update to 1.63, then + isolate your patches. You can peak at imgui_widgets.cpp from 1.64 to get a sense of what is included in it, + then separate your changes into several patches that can more easily be applied to 1.64 on a per-file basis. + What I found worked nicely for me, was to open the diff of the old patches in an interactive merge/diff tool, + search for the corresponding function in the new code and apply the chunks manually. +- As a reminder, if you have any change to imgui.cpp it is a good habit to discuss them on the github, + so a solution applicable on the Master branch can be found. If your company has changes that you cannot + disclose you may also contact me privately. + + +----------------------------------------------------------------------- + VERSION 1.63 (Released 2018-08-29) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.63 + +Breaking Changes: + +- Style: Renamed ImGuiCol_ModalWindowDarkening to ImGuiCol_ModalWindowDimBg for consistency with other features. + Kept redirection enum (will obsolete). +- Changed ImGui::GetTime() return value from float to double to avoid accumulating floating point imprecision over time. +- Removed per-window ImGuiWindowFlags_ResizeFromAnySide Beta flag in favor `io.ConfigResizeWindowsFromEdges=true` to + enable the feature globally. (#1495) + The feature is not currently enabled by default because it is not satisfying enough, but will eventually be. +- InputText: Renamed ImGuiTextEditCallback to ImGuiInputTextCallback, ImGuiTextEditCallbackData to ImGuiInputTextCallbackData + for consistency. Kept redirection types (will obsolete). +- InputText: Removed ImGuiTextEditCallbackData::ReadOnly because it is a duplication of (::Flags & ImGuiInputTextFlags_ReadOnly). +- Renamed IsItemDeactivatedAfterChange() to IsItemDeactivatedAfterEdit() for consistency with new IsItemEdited() API. + Kept redirection function (will obsolete soonish as IsItemDeactivatedAfterChange() is very recent). +- Renamed io.OptCursorBlink to io.ConfigCursorBlink [-> io.ConfigInputTextCursorBlink in 1.65], io.OptMacOSXBehaviors to + io.ConfigMacOSXBehaviors for consistency. (#1427, #473) +- Removed obsolete redirection functions: CollapsingHeader() variation with 2 bools - marked obsolete in v1.49, May 2016. + +Other Changes: + +- ArrowButton: Fixed to honor PushButtonRepeat() setting (and internals' ImGuiItemFlags_ButtonRepeat). +- ArrowButton: Setup current line text baseline so that ArrowButton() + SameLine() + Text() are aligned properly. +- Nav: Added a CTRL+TAB window list and changed the highlight system accordingly. The change is motivated by upcoming + Docking features. (#787) +- Nav: Made CTRL+TAB skip menus + skip the current navigation window if is has the ImGuiWindow_NoNavFocus set. (#787) + While it was previously possible, you won't be able to CTRL-TAB out and immediately back in a window with the + ImGuiWindow_NoNavFocus flag. +- Window: Allow menu and popups windows from ignoring the style.WindowMinSize values so short menus/popups are not padded. (#1909) +- Window: Added global io.ConfigResizeWindowsFromEdges option to enable resizing windows from their edges and from + the lower-left corner. (#1495) +- Window: Collapse button shows hovering highlight + clicking and dragging on it allows to drag the window as well. +- Added IsItemEdited() to query if the last item modified its value (or was pressed). This is equivalent to the bool + returned by most widgets. + It is useful in some situation e.g. using InputText() with ImGuiInputTextFlags_EnterReturnsTrue. (#2034) +- InputText: Added support for buffer size/capacity changes via the ImGuiInputTextFlags_CallbackResize flag. (#2006, #1443, #1008). +- InputText: Fixed not tracking the cursor horizontally when modifying the text buffer through a callback. +- InputText: Fixed minor off-by-one issue when submitting a buffer size smaller than the initial zero-terminated buffer contents. +- InputText: Fixed a few pathological crash cases on single-line InputText widget with multiple millions characters worth of contents. + Because the current text drawing function reserve for a worst-case amount of vertices and how we handle horizontal clipping, + we currently just avoid displaying those single-line widgets when they are over a threshold of 2 millions characters, + until a better solution is found. +- Drag and Drop: Fixed an incorrect assert when dropping a source that is submitted after the target (bug introduced with 1.62 changes + related to the addition of IsItemDeactivated()). (#1875, #143) +- Drag and Drop: Fixed ImGuiDragDropFlags_SourceNoDisableHover to affect hovering state prior to calling IsItemHovered() + fixed description. (#143) +- Drag and Drop: Calling BeginTooltip() between a BeginDragSource()/EndDragSource() or BeginDropTarget()/EndDropTarget() uses adjusted tooltip + settings matching the one created when calling BeginDragSource() without the ImGuiDragDropFlags_SourceNoPreviewTooltip flag. (#143) +- Drag and Drop: Payload stays available and under the mouse if the source stops being submitted, however the tooltip is replaced by "...". (#1725) +- Drag and Drop: Added ImGuiDragDropFlags_SourceAutoExpirePayload flag to force payload to expire if the source stops being submitted. (#1725, #143). +- IsItemHovered(): Added ImGuiHoveredFlags_AllowWhenDisabled flag to query hovered status on disabled items. (#1940, #211) +- Selectable: Added ImGuiSelectableFlags_Disabled flag in the public API. (#211) +- ColorEdit4: Fixed a bug when text input or drag and drop leading to unsaturated HSV values would erroneously alter the resulting color. (#2050) +- Misc: Added optional misc/stl/imgui_stl.h wrapper to use with STL types (e.g. InputText with std::string). (#2006, #1443, #1008) + [*EDIT* renamed to misc/std/imgui_stdlib.h in 1.66] +- Misc: Added IMGUI_VERSION_NUM for easy compile-time testing. (#2025) +- Misc: Added ImGuiMouseCursor_Hand cursor enum + corresponding software cursor. (#1913, 1914) [@aiekick, @ocornut] +- Misc: Tweaked software mouse cursor offset to match the offset of the corresponding Windows 10 cursors. +- Made assertion more clear when trying to call Begin() outside of the NewFrame()..EndFrame() scope. (#1987) +- Fixed assertion when transitioning from an active ID to another within a group, affecting ColorPicker (broken in 1.62). (#2023, #820, #956, #1875). +- Fixed PushID() from keeping alive the new ID Stack top value (if a previously active widget shared the ID it would be erroneously kept alive). +- Fixed horizontal mouse wheel not forwarding the request to the parent window if ImGuiWindowFlags_NoScrollWithMouse is set. (#1463, #1380, #1502) +- Fixed a include build issue for Cygwin in non-POSIX (Win32) mode. (#1917, #1319, #276) +- ImDrawList: Improved handling for worst-case vertices reservation policy when large amount of text (e.g. 1+ million character strings) + are being submitted in a single call. It would typically have crashed InputTextMultiline(). (#200) +- OS/Windows: Fixed missing ImmReleaseContext() call in the default Win32 IME handler. (#1932) [@vby] +- Metrics: Changed io.MetricsActiveWindows to reflect the number of active windows (!= from visible windows), which is useful + for lazy/idle render mechanisms as new windows are typically not visible for one frame. +- Metrics: Added io.MetricsRenderWindow to reflect the number of visible windows. +- Metrics: Added io.MetricsActiveAllocations, moving away from the cross-context global counters than we previously used. (#1565, #1599, #586) +- Demo: Added basic Drag and Drop demo. (#143) +- Demo: Modified the Console example to use InsertChars() in the input text callback instead of poking directly into the buffer. + Although this won't make a difference in the example itself, using InsertChars() will honor the resizing callback properly. (#2006, #1443, #1008). +- Demo: Clarified the use of IsItemHovered()/IsItemActive() right after being in the "Active, Focused, Hovered & Focused Tests" section. +- Examples: Tweaked the main.cpp of each example. +- Examples: Metal: Added Metal rendering backend. (#1929, #1873) [@warrenm] +- Examples: OSX: Added early raw OSX platform backend. (#1873) [@pagghiu, @itamago, @ocornut] +- Examples: Added mac OSX & iOS + Metal example in example_apple_metal/. (#1929, #1873) [@warrenm] +- Examples: Added mac OSX + OpenGL2 example in example_apple_opengl2/. (#1873) +- Examples: OpenGL3: Added shaders more versions of GLSL. (#1938, #1941, #1900, #1513, #1466, etc.) +- Examples: OpenGL3: Tweaked the imgui_impl_opengl3.cpp to work as-is with Emscripten + WebGL 2.0. (#1941). [@o-micron] +- Examples: OpenGL3: Made the example app default to GL 3.0 + GLSL 130 (instead of GL 3.2 + GLSL 150) unless on Mac. +- Examples: OpenGL3: Added error output when shaders fail to compile/link. +- Examples: OpenGL3: Added support for glew and glad OpenGL loaders out of the box. (#2001, #2002) [@jdumas] +- Examples: OpenGL2: Disabling/restoring GL_LIGHTING and GL_COLOR_MATERIAL to increase compatibility with legacy OpenGL applications. (#1996) +- Examples: DirectX10, DirectX11: Fixed unreleased resources in Init and Shutdown functions. (#1944) +- Examples: DirectX11: Querying for IDXGIFactory instead of IDXGIFactory1 to increase compatibility. (#1989) [@matt77hias] +- Examples: Vulkan: Fixed handling of VkSurfaceCapabilitiesKHR::maxImageCount = 0 case. Tweaked present mode selections. +- Examples: Win32, Glfw, SDL: Added support for the ImGuiMouseCursor_Hand cursor. + + +----------------------------------------------------------------------- + VERSION 1.62 (Released 2018-06-22) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.62 + +Breaking Changes: + +- TreeNodeEx(): The helper ImGuiTreeNodeFlags_CollapsingHeader flag now include ImGuiTreeNodeFlags_NoTreePushOnOpen. + The flag was already set by CollapsingHeader(). + The only difference is if you were using TreeNodeEx() manually with ImGuiTreeNodeFlags_CollapsingHeader and without + ImGuiTreeNodeFlags_NoTreePushOnOpen. In this case you can remove the ImGuiTreeNodeFlags_NoTreePushOnOpen flag from + your call (ImGuiTreeNodeFlags_CollapsingHeader & ~ImGuiTreeNodeFlags_NoTreePushOnOpen). (#1864) + This also apply if you were using internal's TreeNodeBehavior() with the ImGuiTreeNodeFlags_CollapsingHeader flag directly. +- ImFontAtlas: Renamed GetGlyphRangesChinese() to GetGlyphRangesChineseFull() to distinguish new smaller variants and + discourage using the full set. (#1859) + +Other Changes: + +- Examples backends have been refactored to separate the platform code (e.g. Win32, Glfw, SDL2) from the renderer code (e.g. DirectX11, OpenGL3, Vulkan). + The "Platform" backends are in charge of: mouse/keyboard/gamepad inputs, cursor shape, timing, etc. + The "Renderer" backends are in charge of: creating the main font texture, rendering imgui draw data. + before: imgui_impl_dx11.cpp --> after: imgui_impl_win32.cpp + imgui_impl_dx11.cpp + before: imgui_impl_dx12.cpp --> after: imgui_impl_win32.cpp + imgui_impl_dx12.cpp + before: imgui_impl_glfw_gl3.cpp --> after: imgui_impl_glfw.cpp + imgui_impl_opengl2.cpp + before: imgui_impl_glfw_vulkan.cpp --> after: imgui_impl_glfw.cpp + imgui_impl_vulkan.cpp + before: imgui_impl_sdl_gl3.cpp --> after: imgui_impl_sdl2.cpp + imgui_impl_opengl2.cpp + before: imgui_impl_sdl_gl3.cpp --> after: imgui_impl_sdl2.cpp + imgui_impl_opengl3.cpp etc. + - The idea is what we can now easily combine and maintain backends and reduce code redundancy. Individual files are + smaller and more reusable. Integration of imgui into a new/custom engine may also be easier as there is less overlap + between "windowing / inputs" and "rendering" code, so you may study or grab one half of the code and not the other. + - This change was motivated by the fact that adding support for the upcoming multi-viewport feature requires more work + from the Platform and Renderer backends, and the amount of redundancy across files was becoming too difficult to + maintain. If you use default backends, you'll benefit from an easy update path to support multi-viewports later + (for future ImGui 1.7x). + - This is not strictly a breaking change if you keep your old backends, but when you'll want to fully update your backends, + expect to have to reshuffle a few things. + - Each example still has its own main.cpp which you may refer you to understand how to initialize and glue everything together. + - Some frameworks (such as the Allegro, Marmalade) handle both the "platform" and "rendering" part, and your custom engine may as well. + - Read examples/README.txt for details. +- Added IsItemDeactivated() to query if the last item was active previously and isn't anymore. Useful for Undo/Redo patterns. (#820, #956, #1875) +- Added IsItemDeactivatedAfterChange() [*EDIT* renamed to IsItemDeactivatedAfterEdit() in 1.63] if the last item was active previously, + is not anymore, and during its active state modified a value. Note that you may still get false positive (e.g. drag value and while + holding return on the same value). (#820, #956, #1875) +- Nav: Added support for PageUp/PageDown (explorer-style: first aim at bottom/top most item, when scroll a page worth of contents). (#787) +- Nav: To keep the navigated item in view we also attempt to scroll the parent window as well as the current window. (#787) +- ColorEdit3, ColorEdit4, ColorButton: Added ImGuiColorEditFlags_NoDragDrop flag to disable ColorEditX as drag target and ColorButton as drag source. (#1826) +- BeginDragDropSource(): Offset tooltip position so it is off the mouse cursor, but also closer to it than regular tooltips, + and not clamped by viewport. (#1739) +- BeginDragDropTarget(): Added ImGuiDragDropFlags_AcceptNoPreviewTooltip flag to request hiding the drag source tooltip + from the target site. (#143) +- BeginCombo(), BeginMainMenuBar(), BeginChildFrame(): Temporary style modification are restored at the end of BeginXXX + instead of EndXXX, to not affect tooltips and child windows. +- Popup: Improved handling of (erroneously) repeating calls to OpenPopup() to not close the popup's child popups. (#1497, #1533, #1865). +- InputTextMultiline(): Fixed double navigation highlight when scrollbar is active. (#787) +- InputText(): Fixed Undo corruption after pasting large amount of text (Redo will still fail when undo buffers are exhausted, + but text won't be corrupted). +- SliderFloat(): When using keyboard/gamepad and a zero precision format string (e.g. "%.0f"), always step in integer units. (#1866) +- ImFontConfig: Added GlyphMinAdvanceX/GlyphMaxAdvanceX settings useful to make a font appears monospaced, particularly useful + for icon fonts. (#1869) +- ImFontAtlas: Added GetGlyphRangesChineseSimplifiedCommon() helper that returns a list of ~2500 most common Simplified Chinese + characters. (#1859) [@JX-Master, @ocornut] +- Examples: OSX: Added imgui_impl_osx.mm backend to be used along with e.g. imgui_impl_opengl2.cpp. (#281, #1870) [@pagghiu, @itamago, @ocornut] +- Examples: GLFW: Made it possible to Shutdown/Init the backend again (by resetting the time storage properly). (#1827) [@ice1000] +- Examples: Win32: Fixed handling of mouse wheel messages to support sub-unit scrolling messages (typically sent by track-pads). (#1874) [@zx64] +- Examples: SDL+Vulkan: Added SDL+Vulkan example. +- Examples: Allegro5: Added support for ImGuiConfigFlags_NoMouseCursorChange flag. Added clipboard support. +- Examples: Allegro5: Unindexing buffers ourselves as Allegro indexed drawing primitives are buggy in the DirectX9 backend + (will be fixed in Allegro 5.2.5+). +- Examples: DirectX12: Moved the ID3D12GraphicsCommandList* parameter from ImGui_ImplDX12_NewFrame() to ImGui_ImplDX12_RenderDrawData() which makes a lots more sense. (#301) +- Examples: Vulkan: Reordered parameters ImGui_ImplVulkan_RenderDrawData() to be consistent with other backends, + a good occasion since we refactored the code. +- Examples: FreeGLUT: Added FreeGLUT backends. Added FreeGLUT+OpenGL2 example. (#801) +- Examples: The functions in imgui_impl_xxx.cpp are prefixed with IMGUI_IMPL_API (which defaults to IMGUI_API) to facilitate + some uses. (#1888) +- Examples: Fixed backends to use ImGuiMouseCursor_COUNT instead of old name ImGuiMouseCursor_Count_ so they can compile + with IMGUI_DISABLE_OBSOLETE_FUNCTIONS. (#1887) +- Misc: Updated stb_textedit from 1.09 + patches to 1.12 + minor patches. +- Internals: PushItemFlag() flags are inherited by BeginChild(). + + +----------------------------------------------------------------------- + VERSION 1.61 (Released 2018-05-14) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.61 + +Breaking Changes: + +- DragInt(): The default compile-time format string has been changed from "%.0f" to "%d", as we are not using integers internally + any more. If you used DragInt() with custom format strings, make sure you change them to use %d or an integer-compatible format. + To honor backward-compatibility, the DragInt() code will currently parse and modify format strings to replace %*f with %d, + giving time to users to upgrade their code. + If you have IMGUI_DISABLE_OBSOLETE_FUNCTIONS enabled, the code will instead assert! You may run a reg-exp search on your + codebase for e.g. "DragInt.*%f" to you find them. +- InputFloat(): Obsoleted InputFloat() functions taking an optional "int decimal_precision" in favor of an equivalent and more + flexible "const char* format", consistent with other functions. Kept redirection functions (will obsolete). +- Misc: IM_DELETE() helper function added in 1.60 doesn't set the input pointer to NULL, more consistent with standard + expectation and allows passing r-values. + +Other Changes: + +- Added DragScalar, DragScalarN: supports signed/unsigned, 32/64 bits, float/double data types. (#643, #320, #708, #1011) +- Added InputScalar, InputScalarN: supports signed/unsigned, 32/64 bits, float/double data types. (#643, #320, #708, #1011) +- Added SliderScalar, SliderScalarN: supports signed/unsigned, 32/64 bits, float/double data types. (#643, #320, #708, #1011) +- Window: Fixed pop-ups/tooltips/menus not honoring style.DisplaySafeAreaPadding as well as it should have (part of menus + displayed outside the safe area, etc.). +- Window: Fixed windows using the ImGuiWindowFlags_NoSavedSettings flag from not using the same default position as other windows. (#1760) +- Window: Relaxed the internal stack size checker to allow Push/Begin/Pop/.../End patterns to be used with PushStyleColor, PushStyleVar, PushFont without causing a false positive assert. (#1767) +- Window: Fixed the default proportional item width lagging by one frame on resize. +- Columns: Fixed a bug introduced in 1.51 where columns would affect the contents size of their container, often creating + feedback loops when ImGuiWindowFlags_AlwaysAutoResize was used. (#1760) +- Settings: Fixed saving an empty .ini file if CreateContext/DestroyContext are called without a single call to NewFrame(). (#1741) +- Settings: Added LoadIniSettingsFromDisk(), LoadIniSettingsFromMemory(), SaveIniSettingsToDisk(), SaveIniSettingsToMemory() + to manually load/save .ini settings. (#923, #993) +- Settings: Added io.WantSaveIniSettings flag, which is set to notify the application that e.g. SaveIniSettingsToMemory() + should be called. (#923, #993) +- Scrolling: Fixed a case where using SetScrollHere(1.0f) at the bottom of a window on the same frame the window height + has been growing would have the scroll clamped using the previous height. (#1804) +- MenuBar: Made BeginMainMenuBar() honor style.DisplaySafeAreaPadding so the text can be made visible on TV settings that + don't display all pixels. (#1439) [@dougbinks] +- InputText: On Mac OS X, filter out characters when the CMD modifier is held. (#1747) [@sivu] +- InputText: On Mac OS X, support CMD+SHIFT+Z for Redo. CMD+Y is also supported as major apps seems to default to support both. (#1765) [@lfnoise] +- InputText: Fixed returning true when edition is cancelled with ESC and the current buffer matches the initial value. +- InputFloat,InputFloat2,InputFloat3,InputFloat4: Added variations taking a more flexible and consistent optional + "const char* format" parameter instead of "int decimal_precision". This allow using custom formats to display values + in scientific notation, and is generally more consistent with other API. + Obsoleted functions using the optional "int decimal_precision" parameter. (#648, #712) +- DragFloat, DragInt: Cancel mouse tweak when current value is initially past the min/max boundaries and mouse is pushing + in the same direction (keyboard/gamepad version already did this). +- DragFloat, DragInt: Honor natural type limits (e.g. INT_MAX, FLT_MAX) instead of wrapping around. (#708, #320) +- DragFloat, SliderFloat: Fixes to allow input of scientific notation numbers when using CTRL+Click to input the value. (~#648, #1011) +- DragFloat, SliderFloat: Rounding-on-write uses the provided format string instead of parsing the precision from the string, + which allows for finer uses of %e %g etc. (#648, #642) +- DragFloat: Improved computation when using the power curve. Improved lost of input precision with very small steps. + Added an assert than power-curve requires a min/max range. (~#642) +- DragFloat: The 'power' parameter is only honored if the min/max parameter are also setup. +- DragInt, SliderInt: Fixed handling of large integers (we previously passed data around internally as float, which reduced + the range of valid integers). +- ColorEdit: Fixed not being able to pass the ImGuiColorEditFlags_NoAlpha or ImGuiColorEditFlags_HDR flags to SetColorEditOptions(). +- Nav: Fixed hovering a Selectable() with the mouse so that it update the navigation cursor (as it happened in the pre-1.60 navigation branch). (#787) +- Style: Changed default style.DisplaySafeAreaPadding values from (4,4) to (3,3) so it is smaller than FramePadding and has no effect on main menu bar on a computer. (#1439) +- Fonts: When building font atlas, glyphs that are missing in the fonts are not using the glyph slot to render the default glyph. Saves space and allow merging fonts with + overlapping font ranges such as FontAwesome5 which split out the Brands separately from the Solid fonts. (#1703, #1671) +- Misc: Added IMGUI_CHECKVERSION() macro to compare version string and data structure sizes in order to catch issues with mismatching compilation unit settings. (#1695, #1769) +- Misc: Added IMGUI_DISABLE_MATH_FUNCTIONS in imconfig.h to make it easier to redefine wrappers for std/crt math functions. +- Misc: Fix to allow compiling in unity builds where stb_rectpack/stb_truetype may be already included in the same compilation unit. +- Demo: Simple Overlay: Added a context menu item to enable freely moving the window. +- Demo: Added demo for DragScalar(), InputScalar(), SliderScalar(). (#643) +- Examples: Calling IMGUI_CHECKVERSION() in the main.cpp of every example application. +- Examples: Allegro 5: Added support for 32-bit indices setup via defining ImDrawIdx, to avoid an unnecessary conversion (Allegro 5 doesn't support 16-bit indices). +- Examples: Allegro 5: Renamed backend from imgui_impl_a5.cpp to imgui_impl_allegro5.cpp. +- Examples: DirectX 9: Saving/restoring Transform because they don't seem to be included in the StateBlock. Setting shading mode to Gouraud. (#1790, #1687) [@sr-tream] +- Examples: SDL: Fixed clipboard paste memory leak in the SDL backend code. (#1803) [@eliasdaler] +- Various minor fixes, tweaks, refactoring, comments. + + +----------------------------------------------------------------------- + VERSION 1.60 (Released 2018-04-07) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.60 + +The gamepad/keyboard navigation branch (which has been in the work since July 2016) has been merged. +Gamepad/keyboard navigation is still marked as Beta and has to be enabled explicitly. +Various internal refactoring have also been done, as part of the navigation work and as part of the upcoming viewport/docking work. + +Breaking Changes: + +- Obsoleted the io.RenderDrawListsFn callback, you can call your graphics engine render function after ImGui::Render(). + e.g. with example backends, call ImDrawData* draw_data = ImGui::GetDrawData(); ImGui_ImplXXXX_RenderDrawData(draw_data). +- Reorganized context handling to be more explicit: (#1599) + - YOU NOW NEED TO CALL ImGui::CreateContext() AT THE BEGINNING OF YOUR APP, AND CALL ImGui::DestroyContext() AT THE END. + - removed Shutdown() function, as DestroyContext() serve this purpose. If you are using an old backend from the examples/ folder, remove the line that calls Shutdown(). + - you may pass a ImFontAtlas* pointer to CreateContext() to share a font atlas between contexts. Otherwise CreateContext() will create its own font atlas instance. + - removed allocator parameters from CreateContext(), they are now setup with SetAllocatorFunctions(), and shared by all contexts. + - removed the default global context and font atlas instance, which were confusing for users of DLL reloading and users of multiple contexts. +- Renamed ImGuiStyleVar_Count_ to ImGuiStyleVar_COUNT and ImGuiMouseCursor_Count_ to ImGuiMouseCursor_COUNT for consistency with other public enums. +- Fonts: Moved sample TTF files from extra_fonts/ to misc/fonts/. If you loaded files directly from the imgui repo you may need to update your paths. +- Fonts: Changed ImFont::DisplayOffset.y to defaults to 0 instead of +1. Fixed vertical rounding of Ascent/Descent to match TrueType renderer. + If you were adding or subtracting (not assigning) to ImFont::DisplayOffset check if your fonts are correctly aligned vertically. (#1619) +- BeginDragDropSource(): temporarily removed the optional mouse_button=0 parameter because it is not really usable in many situations at the moment. +- Obsoleted IsAnyWindowHovered() in favor of IsWindowHovered(ImGuiHoveredFlags_AnyWindow). Kept redirection function (will obsolete). +- Obsoleted IsAnyWindowFocused() in favor of IsWindowFocused(ImGuiFocusedFlags_AnyWindow). Kept redirection function (will obsolete). +- Renamed io.WantMoveMouse to io.WantSetMousePos for consistency and ease of understanding (was added in 1.52, not used by core, and honored by some backend ahead of merging the Nav branch). +- Removed ImGuiCol_CloseButton, ImGuiCol_CloseButtonActive, ImGuiCol_CloseButtonHovered style colors as the closing cross uses regular button colors now. +- Renamed ImGuiSizeConstraintCallback to ImGuiSizeCallback, ImGuiSizeConstraintCallbackData to ImGuiSizeCallbackData. +- Removed CalcItemRectClosestPoint() which was weird and not really used by anyone except demo code. If you need it should be easy to replicate on your side (you can find the code in 1.53). +- [EDITED] Window: BeginChild() with an explicit name doesn't include the hash within the internal window name. (#1698) + This change was erroneously introduced, undoing the change done for #894, #713, and not documented properly in the original + 1.60 release Changelog. It was fixed on 2018-09-28 (1.66) and I wrote this paragraph the same day. + +Other Changes: + +- Doc: Added a Changelog file in the repository to ease comparing versions (it goes back to dear imgui 1.48), until now it was only on GitHub. +- Navigation: merged in the gamepad/keyboard navigation (about a million changes!). (#787, #323) + The initial focus was to support game controllers, but keyboard is becoming increasingly and decently usable. +- To use Gamepad Navigation: + - Set io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad to enable. + - Backend: Set io.BackendFlags |= ImGuiBackendFlags_HasGamepad + fill the io.NavInputs[] fields before calling NewFrame(). Read imgui.cpp for more details. + - See https://github.com/ocornut/imgui/issues/1599 for recommended gamepad mapping or download PNG/PSD at http://goo.gl/9LgVZW + - See 'enum ImGuiNavInput_' in imgui.h for a description of inputs. Read imgui.cpp for more details. +- To use Keyboard Navigation: + - Set io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard to enable. NewFrame() will automatically fill io.NavInputs[] based on your io.KeysDown[] + io.KeyMap[] arrays. + - Basic controls: arrows to navigate, Alt to enter menus, Space to activate item, Enter to edit text, Escape to cancel/close, Ctrl-Tab to focus windows, etc. + - When keyboard navigation is active (io.NavActive + ImGuiConfigFlags_NavEnableKeyboard), the io.WantCaptureKeyboard flag will be set. + For more advanced uses, you may want to read from io.NavActive or io.NavVisible. Read imgui.cpp for more details. +- Navigation: SetItemDefaultFocus() sets the navigation position in addition to scrolling. (#787) +- Navigation: Added IsItemFocused(), added IsAnyItemFocused(). (#787) +- Navigation: Added window flags: ImGuiWindowFlags_NoNav (== ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus). +- Navigation: Style: Added ImGuiCol_NavHighlight, ImGuiCol_NavWindowingHighlight colors. (#787) +- Navigation: TreeNode: Added ImGuiTreeNodeFlags_NavLeftJumpsBackHere flag to allow Nav Left direction to jump back to parent tree node from any of its child. (#1079) +- Navigation: IO: Added io.ConfigFlags (input), io.NavActive (output), io.NavVisible (output). (#787) +- Context: Removed the default global context and font atlas instances, which caused various problems to users of multiple contexts and DLL users. (#1565, #1599) + YOU NOW NEED TO CALL ImGui::CreateContext() AT THE BEGINNING OF YOUR APP, AND CALL ImGui::DestroyContext() AT THE END. Existing apps will assert/crash without it. +- Context: Added SetAllocatorFunctions() to rewire memory allocators (as a replacement to previous parameters to CreateContext()). Allocators are shared by all contexts and imgui helpers. (#1565, #586, #992, #1007, #1558) +- Context: You may pass a ImFontAtlas to CreateContext() to specify a font atlas to share. Shared font atlas are not owned by the context and not destroyed along with it. (#1599) +- Context: Added IMGUI_DISABLE_DEFAULT_ALLOCATORS to disable linking with malloc/free. (#1565, #586, #992, #1007, #1558) +- IO: Added io.ConfigFlags for user application to store settings for imgui and for the backend: + - ImGuiConfigFlags_NavEnableKeyboard: Enable keyboard navigation. + - ImGuiConfigFlags_NavEnableGamepad: Enable gamepad navigation (provided ImGuiBackendFlags_HasGamepad is also set by backend). + - ImGuiConfigFlags_NavEnableSetMousePos: Instruct navigation to move the mouse cursor. May be useful on TV/console systems where moving a virtual mouse is awkward. + - ImGuiConfigFlags_NoMouseCursorChange: Instruct backend to not alter mouse cursor shape and visibility (by default the example backend use mouse cursor API of the platform when available) + - ImGuiConfigFlags_NoMouse: Instruct imgui to clear mouse position/buttons in NewFrame(). This allows ignoring the mouse information passed by the backend. + - ImGuiConfigFlags_IsSRGB, ImGuiConfigFlags_IsTouchScreen: Flags for general application use. +- IO: Added io.BackendFlags for backend to store its capabilities (currently: _HasGamepad, _HasMouseCursors, _HasSetMousePos). This will be used more in the next version. +- IO: Added ImGuiKey_Insert, ImGuiKey_Space keys. Setup in all example backends. (#1541) +- IO: Added Horizontal Mouse Wheel support for horizontal scrolling. (#1463) [@tseeker] +- IO: Added IsAnyMouseDown() helper which is helpful for backends to handle mouse capturing. +- Window: Clicking on a window with the ImGuiWIndowFlags_NoMove flags takes an ActiveId so we can't hover something else when dragging afterwards. (#1381, #1337) +- Window: IsWindowHovered(): Added ImGuiHoveredFlags_AnyWindow, ImGuiFocusedFlags_AnyWindow flags (See Breaking Changes). Added to demo. (#1382) +- Window: Added SetNextWindowBgAlpha() helper. Particularly helpful since the legacy 5-parameters version of Begin() has been marked as obsolete in 1.53. (#1567) +- Window: Fixed SetNextWindowContentSize() with 0.0f on Y axis (or SetNextWindowContentWidth()) overwriting the contents size. Got broken on Dec 10 (1.53). (#1363) +- ArrowButton: Added ArrowButton() given a cardinal direction (e.g. ImGuiDir_Left). +- InputText: Added alternative clipboard shortcuts: Shift+Delete (cut), CTRL+Insert (copy), Shift+Insert (paste). (#1541) +- InputText: Fixed losing Cursor X position when clicking outside on an item that's submitted after the InputText(). It was only noticeable when restoring focus programmatically. (#1418, #1554) +- InputText: Added ImGuiInputTextFlags_CharsScientific flag to also allow 'e'/'E' for input of values using scientific notation. Automatically used by InputFloat. +- Style: Default style is now StyleColorsDark(), instead of the old StyleColorsClassic(). (#707) +- Style: Enable window border by default. (#707) +- Style: Exposed ImGuiStyleVar_WindowTitleAlign, ImGuiStyleVar_ScrollbarSize, ImGuiStyleVar_ScrollbarRounding, ImGuiStyleVar_GrabRounding + added an assert to reduce accidental breakage. (#1181) +- Style: Added style.MouseCursorScale help when using the software mouse cursor facility. (#939). +- Style: Close button nows display a cross before hovering. Fixed cross positioning being a little off. Uses button colors for highlight when hovering. (#707) +- Popup: OpenPopup() Always reopen existing pop-ups. (Removed imgui_internal.h's OpenPopupEx() which was used for this.) (#1497, #1533). +- Popup: BeginPopupContextItem(), BeginPopupContextWindow(), BeginPopupContextVoid(), OpenPopupOnItemClick() all react on mouse release instead of mouse press. (~#439) +- Popup: Better handling of user mistakenly calling OpenPopup() every frame (with reopen_existing option). The error will now be more visible and easier to understand. (#1497) +- Popup: BeginPopup(): Exposed extra_flags parameter that are passed through to Begin(). (#1533) +- Popup: BeginPopupModal: fixed the conditional test for SetNextWindowPos() which was polling the wrong window, which in practice made the test succeed all the time. +- Tooltip: BeginTooltip() sets ImGuiWindowFlags_NoInputs flag. +- Scrollbar: Fixed ScrollbarY enable test after ScrollbarX has been enabled being a little off (small regression from Nov 2017). (#1574) +- Scrollbar: Fixed ScrollbarX enable test subtracting WindowPadding.x (this has been there since the addition of horizontal scroll bar!). +- Columns: Clear offsets data when columns count changed. (#1525) +- Columns: Fixed a memory leak of ImGuiColumnsSet's Columns vector. (#1529) [@unprompted] +- Columns: Fixed resizing a window very small breaking some columns positioning (broken in 1.53). +- Columns: The available column extent takes consideration of the right-most clipped pixel, so the right-most column may look a little wider but will contain the same amount of visible contents. +- MenuBar: Fixed menu bar pushing a clipping rect outside of its allocated bound (usually unnoticeable). +- TreeNode: nodes with the ImGuiTreeNodeFlags_Leaf flag correctly disable highlight when DragDrop is active. (#143, #581) +- Drag and Drop: Increased payload type string to 32 characters instead of 8. (#143) +- Drag and Drop: TreeNode as drop target displays rectangle over full frame. (#1597, #143) +- DragFloat: Fix/workaround for backends which do not preserve a valid mouse position when dragged out of bounds. (#1559) +- InputFloat: Allow inputing value using scientific notation e.g. "1e+10". +- InputDouble: Added InputDouble() function. We use a format string instead of a decimal_precision parameter to also for "%e" and variants. (#1011) +- Slider, Combo: Use ImGuiCol_FrameBgHovered color when hovered. (#1456) [@stfx] +- Combo: BeginCombo(): Added ImGuiComboFlags_NoArrowButton to disable the arrow button and only display the wide value preview box. +- Combo: BeginCombo(): Added ImGuiComboFlags_NoPreview to disable the preview and only display a square arrow button. +- Combo: Arrow button isn't displayed over frame background so its blended color matches other buttons. Left side of the button isn't rounded. +- PlotLines: plot a flat line if scale_min==scale_max. (#1621) +- Fonts: Changed DisplayOffset.y to defaults to 0 instead of +1. Fixed rounding of Ascent/Descent to match TrueType renderer. + If you were adding or subtracting (not assigning) to ImFont::DisplayOffset check if your fonts are correctly aligned vertically. (#1619) +- Fonts: Updated stb_truetype from 1.14 to stb_truetype 1.19. (w/ include fix from some platforms #1622) +- Fonts: Added optional FreeType rasterizer in misc/freetype. Moved from imgui_club repo. (#618) [@Vuhdo, @mikesart, @ocornut] +- Fonts: Moved extra_fonts/ to misc/fonts/. +- ImFontAtlas: Fixed cfg.MergeMode not reusing existing glyphs if available (always overwrote). +- ImFontAtlas: Handle stb_truetype stbtt_InitFont() and stbtt_PackBegin() possible failures more gracefully, GetTexDataAsRGBA32() won't crash during conversion. (#1527) +- ImFontAtlas: Moved mouse cursor data out of ImGuiContext, fix drawing them with multiple contexts. Also remove the last remaining undesirable dependency on ImGui in imgui_draw.cpp. (#939) +- ImFontAtlas: Added ImFontAtlasFlags_NoPowerOfTwoHeight flag to disable padding font height to nearest power of two. (#1613) +- ImFontAtlas: Added ImFontAtlasFlags_NoMouseCursors flag to disable baking software mouse cursors, mostly to save texture memory on very low end hardware. (#1613) +- ImDrawList: Fixed AddRect() with anti-aliasing disabled (lower-right corner pixel was often missing, rounding looks a little better.) (#1646) +- ImDrawList: Added CloneOutput() helper to facilitate the cloning of ImDrawData or ImDrawList for multi-threaded rendering. +- Misc: Functions passed to libc qsort are explicitly marked cdecl to support compiling with vectorcall as the default calling convention. (#1230, #1611) [@RandyGaul] +- Misc: ImVec2: added [] operator. This is becoming desirable for some code working of either axes independently. Better adding it sooner than later. +- Misc: NewFrame(): Added an assert to detect incorrect filling of the io.KeyMap[] array earlier. (#1555) +- Misc: Added IM_OFFSETOF() helper in imgui.h (previously was in imgui_internal.h) +- Misc: Added IM_NEW(), IM_DELETE() helpers in imgui.h (previously were in imgui_internal.h) +- Misc: Added obsolete redirection function GetItemsLineHeightWithSpacing() (which redirects to GetFrameHeightWithSpacing()), as intended and stated in docs of 1.53. +- Misc: Added misc/natvis/imgui.natvis for visual studio debugger users to easily visualize imgui internal types. Added to examples projects. +- Misc: Added IMGUI_USER_CONFIG to define a custom configuration filename. (#255, #1573, #1144, #41) +- Misc: Added IMGUI_STB_TRUETYPE_FILENAME and IMGUI_STB_RECT_PACK_FILENAME compile time directives to use another version of the stb_ files. +- Misc: Updated stb_rect_pack from 0.10 to 0.11 (minor changes). + (Those flags are not used by ImGui itself, they only exists to make it easy for the engine/backend to pass information to the application in a standard manner.) +- Metrics: Added display of Columns state. +- Demo: Improved Selectable() examples. (#1528) +- Demo: Tweaked the Child demos, added a menu bar to the second child to test some navigation functions. +- Demo: Console: Using ImGuiCol_Text to be more friendly to color changes. +- Demo: Using IM_COL32() instead of ImColor() in ImDrawList centric contexts. Trying to phase out use of the ImColor helper whenever possible. +- Examples: Files in examples/ now include their own changelog so it is easier to occasionally update your backends if needed. +- Examples: Using Dark theme by default. (#707). Tweaked demo code. +- Examples: Added support for horizontal mouse wheel for API that allows it. (#1463) [@tseeker] +- Examples: All examples now setup the io.BackendFlags to signify they can honor mouse cursors, gamepad, etc. +- Examples: DirectX10: Fixed erroneous call to io.Fonts->ClearInputData() + ClearTexData() that was left in DX10 example but removed in 1.47 (Nov 2015) in every other backends. (#1733) +- Examples: DirectX12: Added DirectX 12 example. (#301) [@jdm3] +- Examples: OpenGL3+GLFW,SDL: Changed GLSL shader version from 330 to 150. (#1466, #1504) +- Examples: OpenGL3+GLFW,SDL: Added a way to override the GLSL version string in the Init function. (#1466, #1504). +- Examples: OpenGL3+GLFW,SDL: Creating VAO in the render function so it can be more easily used by multiple shared OpenGL contexts. (#1217) +- Examples: OpenGL3+GLFW: Using 3.2 context instead of 3.3. (#1466) +- Examples: OpenGL: Setting up glPixelStorei() explicitly before uploading texture. +- Examples: OpenGL: Calls to glPolygonMode() are casting parameters as GLEnum to not fail with more strict backends. (#1628) [@ilia-glushchenko] +- Examples: Win32 (DirectX9,10,11,12): Added support for mouse cursor shapes. (#1495) +- Examples: Win32 (DirectX9,10,11,12: Support for windows using the CS_DBLCLKS class flag by handling the double-click messages (WM_LBUTTONDBLCLK etc.). (#1538, #754) [@ndandoulakis] +- Examples: Win32 (DirectX9,10,11,12): Made the Win32 proc handlers not assert if there is no active context yet, to be more flexible with creation order. (#1565) +- Examples: GLFW: Added support for mouse cursor shapes (the diagonal resize cursors are unfortunately not supported by GLFW at the moment. (#1495) +- Examples: GLFW: Don't attempt to change the mouse cursor input mode if it is set to GLFW_CURSOR_DISABLED by the application. (#1202) [@PhilCK] +- Examples: SDL: Added support for mouse cursor shapes. (#1626) [@olls] +- Examples: SDL: Using SDL_CaptureMouse() to retrieve coordinates outside of client area when dragging (SDL 2.0.4+ only, otherwise using SDL_WINDOW_INPUT_FOCUS instead of previously SDL_WINDOW_MOUSE_FOCUS). (#1559) +- Examples: SDL: Enabled vsync by default so people don't come at us when the examples are running at 2000 FPS and burning a CPU core. +- Examples: SDL: Using SDL_GetPerformanceCounter() / SDL_GetPerformanceFrequency() to handle frame-rate over 1000 FPS properly. (#996) +- Examples: SDL: Using scan-code exclusively instead of a confusing mixture of scan-codes and key-codes. +- Examples: SDL: Visual Studio: Added .vcxproj file. Using %SDL2_DIR% in the default .vcxproj and build files instead of %SDL_DIR%, the earlier being more standard. +- Examples: Vulkan: Visual Studio: Added .vcxproj file. +- Examples: Apple: Fixed filenames in OSX xcode project. Various other Mac friendly fixes. [@gerryhernandez etc.] +- Examples: Visual Studio: Disabled extraneous function-level check in Release build. +- Various fixes, tweaks, internal refactoring, optimizations, comments. + + +----------------------------------------------------------------------- + VERSION 1.53 (Released 2017-12-25) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.53 + +Breaking Changes: + +- Renamed the emblematic `ShowTestWindow()` function to `ShowDemoWindow()`. Kept redirection function (will obsolete). +- Renamed `GetItemsLineHeightWithSpacing()` to `GetFrameHeightWithSpacing()` for consistency. Kept redirection function (will obsolete). +- Renamed `ImGuiTreeNodeFlags_AllowOverlapMode` flag to `ImGuiTreeNodeFlags_AllowItemOverlap`. Kept redirection enum (will obsolete). +- Obsoleted `IsRootWindowFocused()` in favor of using `IsWindowFocused(ImGuiFocusedFlags_RootWindow)`. Kept redirection function (will obsolete). (#1382) +- Obsoleted `IsRootWindowOrAnyChildFocused()` in favor of using `IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)`. Kept redirection function (will obsolete). (#1382) +- Obsoleted `IsRootWindowOrAnyChildHovered()` in favor of using `IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows)`. Kept redirection function (will obsolete). (#1382) +- Obsoleted `SetNextWindowContentWidth() in favor of using `SetNextWindowContentSize()`. Kept redirection function (will obsolete). +- Renamed `ImGuiTextBuffer::append()` helper to `appendf()`, and `appendv()` to `appendfv()` for consistency. If you copied the 'Log' demo in your code, it uses appendv() so that needs to be renamed. +- ImDrawList: Removed 'bool anti_aliased = true' final parameter of `ImDrawList::AddPolyline()` and `ImDrawList::AddConvexPolyFilled()`. Prefer manipulating ImDrawList::Flags if you need to toggle them during the frame. +- Style, ImDrawList: Renamed `style.AntiAliasedShapes` to `style.AntiAliasedFill` for consistency and as a way to explicitly break code that manipulate those flag at runtime. You can now manipulate ImDrawList::Flags. +- Style, Begin: Removed `ImGuiWindowFlags_ShowBorders` window flag. Borders are now fully set up in the ImGuiStyle structure (see e.g. `style.FrameBorderSize`, `style.WindowBorderSize`, `style.PopupBorderSize`). + Use `ImGui::ShowStyleEditor()` to look them up. + Please note that the style system will keep evolving (hopefully stabilizing in Q1 2018), and so custom styles will probably subtly break over time. + It is recommended that you use the `StyleColorsClassic()`, `StyleColorsDark()`, `StyleColorsLight()` functions. Also see `ShowStyleSelector()`. +- Style: Removed `ImGuiCol_ComboBg` in favor of combo boxes using `ImGuiCol_PopupBg` for consistency. Combo are normal pop-ups. +- Style: Renamed `ImGuiCol_ChildWindowBg` to `ImGuiCol_ChildBg`. +- Style: Renamed `style.ChildWindowRounding` to `style.ChildRounding`, `ImGuiStyleVar_ChildWindowRounding` to `ImGuiStyleVar_ChildRounding`. +- Removed obsolete redirection functions: SetScrollPosHere() - marked obsolete in v1.42, July 2015. +- Removed obsolete redirection functions: GetWindowFont(), GetWindowFontSize() - marked obsolete in v1.48, March 2016. + +Other Changes: + +- Added `io.OptCursorBlink` option to allow disabling cursor blinking. (#1427) [renamed to io.ConfigCursorBlink in 1.63] +- Added `GetOverlayDrawList()` helper to quickly get access to a ImDrawList that will be rendered in front of every windows. +- Added `GetFrameHeight()` helper which returns `(FontSize + style.FramePadding.y * 2)`. +- Drag and Drop: Added Beta API to easily use drag and drop patterns between imgui widgets. + - Setup a source on a widget with `BeginDragDropSource()`, `SetDragDropPayload()`, `EndDragDropSource()` functions. + - Receive data with `BeginDragDropTarget()`, `AcceptDragDropPayload()`, `EndDragDropTarget()`. + - See ImGuiDragDropFlags for various options. + - The ColorEdit4() and ColorButton() widgets now support Drag and Drop. + - The API is tagged as Beta as it still may be subject to small changes. +- Drag and Drop: When drag and drop is active, tree nodes and collapsing header can be opened by hovering on them for 0.7 seconds. +- Renamed io.OSXBehaviors to io.OptMacOSXBehaviors. Should not affect users as the compile-time default is usually enough. (#473, #650) +- Style: Added StyleColorsDark() style. (#707) [@dougbinks] +- Style: Added StyleColorsLight() style. Best used with frame borders + thicker font than the default font. (#707) +- Style: Added style.PopupRounding setting. (#1112) +- Style: Added style.FrameBorderSize, style.WindowBorderSize, style.PopupBorderSize. Removed ImGuiWindowFlags_ShowBorders window flag! + Borders are now fully set up in the ImGuiStyle structure. Use ImGui::ShowStyleEditor() to look them up. (#707, fix #819, #1031) +- Style: Various small changes to the classic style (most noticeably, buttons are now using blue shades). (#707) +- Style: Renamed ImGuiCol_ChildWindowBg to ImGuiCol_ChildBg. +- Style: Renamed style.ChildWindowRounding to style.ChildRounding, ImGuiStyleVar_ChildWindowRounding to ImGuiStyleVar_ChildRounding. +- Style: Removed ImGuiCol_ComboBg in favor of combo boxes using ImGuiCol_PopupBg for consistency. (#707) +- Style: Made the ScaleAllSizes() helper rounds down every values so they are aligned on integers. +- Focus: Added SetItemDefaultFocus(), which in the current (master) branch behave the same as doing `if (IsWindowAppearing()) SetScrollHere()`. + In the navigation branch this will also set the default focus. Prefer using this when creating combo boxes with `BeginCombo()` so your code will be forward-compatible with gamepad/keyboard navigation features. (#787) +- Combo: Pop-up grows horizontally to accommodate for contents that is larger then the parent combo button. +- Combo: Added BeginCombo()/EndCombo() API which allows use to submit content of any form and manage your selection state without relying on indices. +- Combo: Added ImGuiComboFlags_PopupAlignLeft flag to BeginCombo() to prioritize keeping the pop-up on the left side (for small-button-looking combos). +- Combo: Added ImGuiComboFlags_HeightSmall, ImGuiComboFlags_HeightLarge, ImGuiComboFlags_HeightLargest to easily provide desired pop-up height. +- Combo: You can use SetNextWindowSizeConstraints() before BeginCombo() to specify specific pop-up width/height constraints. +- Combo: Offset popup position by border size so that a double border isn't so visible. (#707) +- Combo: Recycling windows by using a stack number instead of a unique id, wasting less memory (like menus do). +- InputText: Added ImGuiInputTextFlags_NoUndoRedo flag. (#1506, #1508) [@ibachar] +- Window: Fixed auto-resize allocating too much space for scrollbar when SizeContents is bigger than maximum window size (fixes c0547d3). (#1417) +- Window: Child windows with MenuBar use regular WindowPadding.y so layout look consistent as child or as a regular window. +- Window: Begin(): Fixed appending into a child window with a second Begin() from a different window stack querying the wrong window for the window->Collapsed test. +- Window: Calling IsItemActive(), IsItemHovered() etc. after a call to Begin() provides item data for the title bar, so you can easily test if the title bar is being hovered, etc. (#823) +- Window: Made it possible to use SetNextWindowPos() on a child window. +- Window: Fixed a one frame glitch. When an appearing window claimed the focus themselves, the title bar wouldn't use the focused color for one frame. +- Window: Added ImGuiWindowFlags_ResizeFromAnySide flag to resize from any borders or from the lower-left corner of a window. This requires your backend to honor GetMouseCursor() requests for full usability. (#822) +- Window: Sizing fixes when using SetNextWindowSize() on individual axises. +- Window: Hide new window for one frame until they calculate their size. Also fixes SetNextWindowPos() given a non-zero pivot. (#1694) +- Window: Made mouse wheel scrolling accommodate better to windows that are smaller than the scroll step. +- Window: SetNextWindowContentSize() adjust for the size of decorations (title bar/menu bar), but _not_ for borders are we consistently make borders not affect layout. + If you need a non-child window of an exact size with border enabled but zero window padding, you'll need to accommodate for the border size yourself. +- Window: Using the ImGuiWindowFlags_NoScrollWithMouse flag on a child window forwards the mouse wheel event to the parent window, unless either ImGuiWindowFlags_NoInputs or ImGuiWindowFlags_NoScrollbar are also set. (#1380, #1502) +- Window: Active Modal window always set the WantCaptureKeyboard flag. (#744) +- Window: Moving window doesn't use accumulating MouseDelta so straying out of imgui boundaries keeps moved imgui window at the same cursor-relative position. +- IsWindowFocused(): Added ImGuiFocusedFlags_ChildWindows flag to include child windows in the focused test. (#1382). +- IsWindowFocused(): Added ImGuiFocusedFlags_RootWindow flag to start focused test from the root (top-most) window. Obsolete IsRootWindowFocused(). (#1382) +- IsWindowHovered(): Added ImGuiHoveredFlags_ChildWindows flag to include child windows in the hovered test. (#1382). +- IsWindowHovered(): Added ImGuiHoveredFlags_RootWindow flag to start hovered test from the root (top-most) window. The combination of both flags obsoletes IsRootWindowOrAnyChildHovered(). (#1382) +- IsWindowHovered(): Fixed return value when an item is active to use the same logic as IsItemHovered(). (#1382, #1404) +- IsWindowHovered(): Always return true when current window is being moved. (#1382) +- Scrollbar: Fixed issues with vertical scrollbar flickering/appearing, typically when manually resizing and using a pattern of filling available height (e.g. full sized BeginChild). +- Scrollbar: Minor graphical fix for when scrollbar don't have enough visible space to display the full grab. +- Scrolling: Fixed padding and scrolling asymmetry where lower/right sides of a window wouldn't use WindowPadding properly + causing minor scrolling glitches. +- Tree: TreePush with zero arguments was ambiguous. Resolved by making it call TreePush(const void*). [@JasonWilkins] +- Tree: Renamed ImGuiTreeNodeFlags_AllowOverlapMode to ImGuiTreeNodeFlags_AllowItemOverlap. (#600, #1330) +- MenuBar: Fixed minor rendering issues on the right size when resizing a window very small and using rounded window corners. +- MenuBar: better software clipping to handle small windows, in particular child window don't have minimum constraints so we need to render clipped menus better. +- BeginMenu(): Tweaked the Arrow/Triangle displayed on child menu items. +- Columns: Clipping columns borders on Y axis on CPU because some Linux GPU drivers appears to be unhappy with triangle spanning large regions. (#125) +- Columns: Added ImGuiColumnsFlags_GrowParentContentsSize to internal API to restore old content sizes behavior (may be obsolete). (#1444, #125) +- Columns: Columns width is no longer lost when dragging a column to the right side of the window, until releasing the mouse button you have a chance to save them. (#1499, #125). [@ggtucker] +- Columns: Fixed dragging when using a same of columns multiple times in the frame. (#125) +- Indent(), Unindent(): Allow passing negative values. +- ColorEdit4(): Made IsItemActive() return true when picker pop-up is active. (#1489) +- ColorEdit4(): Tweaked tooltip so that the color button aligns more correctly with text. +- ColorEdit4(): Support drag and drop. Color buttons can be used as drag sources, and ColorEdit widgets as drag targets. (#143) +- ColorPicker4(): Fixed continuously returning true when holding mouse button on the sat/value/alpha locations. We only return true on value change. (#1489) +- NewFrame(): using literal strings in the most-frequently firing IM_ASSERT expressions to increase the odd of programmers seeing them (especially those who don't use a debugger). +- NewFrame() now asserts if neither Render or EndFrame have been called. Exposed EndFrame(). Made it legal to call EndFrame() more than one. (#1423) +- ImGuiStorage: Added BuildSortByKey() helper to rebuild storage from scratch. +- ImFont: Added GetDebugName() helper. +- ImFontAtlas: Added missing Thai punctuation in the GetGlyphRangesThai() ranges. (#1396) [@nProtect] +- ImDrawList: Removed 'bool anti_aliased = true' final parameter of ImDrawList::AddPolyline() and ImDrawList::AddConvexPolyFilled(). Anti-aliasing is controlled via the regular style.AntiAliased flags. +- ImDrawList: Added ImDrawList::AddImageRounded() helper. (#845) [@thedmd] +- ImDrawList: Refactored to make ImDrawList independent of ImGui. Removed static variable in PathArcToFast() which caused linking issues to some. +- ImDrawList: Exposed ImDrawCornerFlags, replaced occurrences of ~0 with an explicit ImDrawCornerFlags_All. NB: Inversed BotLeft (prev 1<<3, now 1<<2) and BotRight (prev 1<<2, now 1<<3). +- ImVector: Added ImVector::push_front() helper. +- ImVector: Added ImVector::contains() helper. +- ImVector: insert() uses grow_capacity() instead of using grow policy inconsistent with push_back(). +- Internals: Remove requirement to define IMGUI_DEFINE_PLACEMENT_NEW to use the IM_PLACEMENT_NEW macro. (#1103) +- Internals: ButtonBehavior: Fixed ImGuiButtonFlags_NoHoldingActiveID flag from incorrectly setting the ActiveIdClickOffset field. + This had no known effect within imgui code but could have affected custom drag and drop patterns. And it is more correct this way! (#1418) +- Internals: ButtonBehavior: Fixed ImGuiButtonFlags_AllowOverlapMode to avoid temporarily activating widgets on click before they have been correctly double-hovered. (#319, #600) +- Internals: Added SplitterBehavior() helper. (#319) +- Internals: Added IM_NEW(), IM_DELETE() helpers. (#484, #504, #1517) +- Internals: Basic refactor of the settings API which now allows external elements to be loaded/saved. +- Demo: Added ShowFontSelector() showing loaded fonts. +- Demo: Added ShowStyleSelector() to select among default styles. (#707) +- Demo: Renamed the emblematic ShowTestWindow() function to ShowDemoWindow(). +- Demo: Style Editor: Added a "Simplified settings" sections with check-boxes for border size and frame rounding. (#707, #1019) +- Demo: Style Editor: Added combo box to select stock styles and select current font when multiple are loaded. (#707) +- Demo: Style Editor: Using local storage so Save/Revert button makes more sense without code passing its storage. Added horizontal scroll bar. Fixed Save/Revert button to be always accessible. (#1211) +- Demo: Console: Fixed context menu issue. (#1404) +- Demo: Console: Fixed incorrect positioning which was hidden by a minor scroll issue (this would affect people who copied the Console code as is). +- Demo: Constrained Resize: Added more test cases. (#1417) +- Demo: Custom Rendering: Fixed clipping rectangle extruding out of parent window. +- Demo: Layout: Removed unnecessary and misleading BeginChild/EndChild calls. +- Demo: The "Color Picker with Palette" demo supports drag and drop. (#143) +- Demo: Display better mouse cursor info for debugging backends. +- Demo: Stopped using rand() function in demo code. +- Examples: Added a handful of extra comments (about fonts, third-party libraries used in the examples, etc.). +- Examples: DirectX9: Handle loss of D3D9 device (D3DERR_DEVICELOST). (#1464) +- Examples: Added null_example/ which is helpful for quick testing on multiple compilers/settings without relying on graphics library. +- Fix for using alloca() in "Clang with Microsoft Codechain" mode. +- Various fixes, optimizations, comments. + + +----------------------------------------------------------------------- + VERSION 1.52 (2017-10-27) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.52 + +Breaking Changes: + +- IO: `io.MousePos` needs to be set to ImVec2(-FLT_MAX,-FLT_MAX) when mouse is unavailable/missing, instead of ImVec2(-1,-1) as previously) This is needed so we can clear `io.MouseDelta` field when the mouse is made available again. +- Renamed `AlignFirstTextHeightToWidgets()` to `AlignTextToFramePadding()`. Kept inline redirection function (will obsolete). +- Obsoleted the legacy 5 parameters version of Begin(). Please avoid using it. If you need a transparent window background, uses `PushStyleColor()`. The old size parameter there was also misleading and equivalent to calling `SetNextWindowSize(size, ImGuiCond_FirstTimeEver)`. Kept inline redirection function (will obsolete). +- Obsoleted `IsItemHoveredRect()`, `IsMouseHoveringWindow()` in favor of using the newly introduced flags of `IsItemHovered()` and `IsWindowHovered()`. Kept inline redirection function (will obsolete). (#1382) +- Obsoleted 'SetNextWindowPosCenter()' in favor of using 1SetNextWindowPos()` with a pivot value which allows to do the same and more. Keep inline redirection function. +- Removed `IsItemRectHovered()`, `IsWindowRectHovered()` recently introduced in 1.51 which were merely the more consistent/correct names for the above functions which are now obsolete anyway. (#1382) +- Changed `IsWindowHovered()` default parameters behavior to return false if an item is active in another window (e.g. click-dragging item from another window to this window). You can use the newly introduced IsWindowHovered() flags to requests this specific behavior if you need it. (#1382) +- Renamed imconfig.h's `IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCS`/`IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCS` to `IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS`/`IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS` for consistency. +- Renamed ImFont::Glyph to ImFontGlyph. Kept redirection typedef (will obsolete). + +Other Changes: + +- ProgressBar: fixed rendering when straddling rounded area. (#1296) +- SliderFloat, DragFloat: Using scientific notation e.g. "%.1e" in the displayed format string doesn't mistakenly trigger rounding of the value. [@MomentsInGraphics] +- Combo, InputFloat, InputInt: Made the small button on the right side align properly with the equivalent colored button of ColorEdit4(). +- IO: Tweaked logic for `io.WantCaptureMouse` so it now outputs false when e.g. hovering over void while an InputText() is active. (#621) [@pdoane] +- IO: Fixed `io.WantTextInput` from mistakenly outputting true when an activated Drag or Slider was previously turned into an InputText(). (#1317) +- Misc: Added flags to `IsItemHovered()`, `IsWindowHovered()` to access advanced hovering-test behavior. Generally useful for pop-ups and drag and drop behaviors: (relates to ~#439, #1013, #143, #925) + - `ImGuiHoveredFlags_AllowWhenBlockedByPopup` + - `ImGuiHoveredFlags_AllowWhenBlockedByActiveItem` + - `ImGuiHoveredFlags_AllowWhenOverlapped` + - `ImGuiHoveredFlags_RectOnly` +- Input: Added `IsMousePosValid()` helper. +- Input: Added `GetKeyPressedAmount()` to easily measure press count when the repeat rate is faster than the frame rate. +- Input/Focus: Disabled TAB and Shift+TAB when CTRL key is held. +- CheckBox: Now rendering a tick mark instead of a full square. +- ColorEdit4: Added "Copy as..." option in context menu. (#346) +- ColorPicker: Improved ColorPicker hue wheel color interpolation. (#1313) [@thevaber] +- ColorButton: Reduced bordering artifact that would be particularly visible with an opaque Col_FrameBg and FrameRounding enabled. +- ColorButton: Fixed rendering color button with a checkerboard if the transparency comes from the global style.Alpha and not from the actual source color. +- TreeNode: Added `ImGuiTreeNodeFlags_FramePadding` flag to conveniently create a tree node with full padding at the beginning of a line, without having to call `AlignTextToFramePadding()`. +- Trees: Fixed calling `SetNextTreeNodeOpen()` on a collapsed window leaking to the first tree node item of the next frame. +- Layout: Horizontal layout is automatically enforced in a menu bar, so you can use non-MenuItem elements without calling SameLine(). +- Separator: Output a vertical separator when used inside a menu bar (or in general when horizontal layout is active, but that isn't exposed yet!). +- Window: Added `IsWindowAppearing()` helper (helpful e.g. as a condition before initializing some of your own things.). +- Window: Added pivot parameter to `SetNextWindowPos()`, making it possible to center or right align a window. Obsoleted `SetNextWindowPosCenter()`. +- Window: Fixed title bar color of top-most window under a modal window. +- Window: Fixed not being able to move a window by clicking on one of its child window. (#1337, #635) +- Window: Fixed `Begin()` auto-fit calculation code that predict the presence of a scrollbar so it works better when window size constraints are used. +- Window: Fixed calling `Begin()` more than once per frame setting `window_just_activated_by_user` which in turn would set enable the Appearing condition for that frame. +- Window: The implicit "Debug" window now uses a "Debug##Default" identifier instead of "Debug" to allow user creating a window called "Debug" without losing their custom flags. +- Window: Made the `ImGuiWindowFlags_NoMove` flag properly inherited from parent to child. In a setup with ParentWindow (no flag) -> Child (NoMove) -> SubChild (no flag), the user won't be able to move the parent window by clicking on SubChild. (#1381) +- Popups: Pop-ups can be closed with a right-click anywhere, without altering focus under the pop-up. (~#439) +- Popups: `BeginPopupContextItem()`, `BeginPopupContextWindow()` are now setup to allow reopening a context menu by right-clicking again. (~#439) +- Popups: `BeginPopupContextItem()` now supports a NULL string identifier and uses the last item ID if available. +- Popups: Added `OpenPopupOnItemClick()` helper which mimic `BeginPopupContextItem()` but doesn't do the BeginPopup(). +- MenuItem: Only activating on mouse release. [@Urmeli0815] (was already fixed in nav branch). +- MenuItem: Made tick mark thicker (thick mark?). +- MenuItem: Tweaks to be usable inside a menu bar (nb: it looks like a regular menu and thus is misleading, prefer using Button() and regular widgets in menu bar if you need to). (#1387) +- ImDrawList: Fixed a rare draw call merging bug which could lead to undisplayed triangles. (#1172, #1368) +- ImDrawList: Fixed a rare bug in `ChannelsMerge()` when all contents has been clipped, leading to an extraneous draw call being created. (#1172, #1368) +- ImFont: Added `AddGlyph()` building helper for use by custom atlas builders. +- ImFontAtlas: Added support for CustomRect API to submit custom rectangles to be packed into the atlas. You can map them as font glyphs, or use them for custom purposes. + After the atlas is built you can query the position of your rectangles in the texture and then copy your data there. You can use this features to create e.g. full color font-mapped icons. +- ImFontAtlas: Fixed fall-back handling when merging fonts, if a glyph was missing from the second font input it could have used a glyph from the first one. (#1349) [@inolen] +- ImFontAtlas: Fixed memory leak on build failure case when stbtt_InitFont failed (generally due to incorrect or supported font type). (#1391) (@Moka42) +- ImFontConfig: Added `RasterizerMultiply` option to alter the brightness of individual fonts at rasterization time, which may help increasing readability for some. +- ImFontConfig: Added `RasterizerFlags` to pass options to custom rasterizer (e.g. the [imgui_freetype](https://github.com/ocornut/imgui_club/tree/master/imgui_freetype) rasterizer in imgui_club has such options). +- ImVector: added resize() variant with initialization value. +- Misc: Changed the internal name formatting of child windows identifier to use slashes (instead of dots) as separator, more readable. +- Misc: Fixed compilation with `IMGUI_DISABLE_OBSOLETE_FUNCTIONS` defined. +- Misc: Marked all format+va_list functions with format attribute so GCC/Clang can warn about misuses. +- Misc: Fixed compilation on NetBSD due to missing alloca.h (#1319) [@RyuKojiro] +- Misc: Improved warnings compilation for newer versions of Clang. (#1324) (@waywardmonkeys) +- Misc: Added `io.WantMoveMouse flags` (from Nav branch) and honored in Examples applications. Currently unused but trying to spread Examples applications code that supports it. +- Misc: Added `IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS` support in imconfig.h to allow user reimplementing the `ImFormatString()` functions e.g. to use stb_printf(). (#1038) +- Misc: [Windows] Fixed default Win32 `SetClipboardText()` handler leaving the Win32 clipboard handler unclosed on failure. [@pdoane] +- Style: Added `ImGuiStyle::ScaleAllSizes(float)` helper to make it easier to have application transition e.g. from low to high DPI with a matching style. +- Metrics: Draw window bounding boxes when hovering Pos/Size; List all draw layers; Trimming empty commands like Render() does. +- Examples: OpenGL3: Save and restore sampler state. (#1145) [@nlguillemot] +- Examples: OpenGL2, OpenGL3: Save and restore polygon mode. (#1307) [@JJscott] +- Examples: DirectX11: Allow creating device with feature level 10 since we don't really need much for that example. (#1333) +- Examples: DirectX9/10/12: Using the Win32 SetCapture/ReleaseCapture API to read mouse coordinates when they are out of bounds. (#1375) [@Gargaj, @ocornut] +- Tools: Fixed binary_to_compressed_c tool to return 0 when successful. (#1350) [@benvanik] +- Internals: Exposed more helpers and unfinished features in imgui_internal.h. (use at your own risk!). +- Internals: A bunch of internal refactoring, hopefully haven't broken anything! Merged a bunch of internal changes from the upcoming Navigation branch. +- Various tweaks, fixes and documentation changes. + +Beta Navigation Branch: +(Lots of work has been done toward merging the Beta Gamepad/Keyboard Navigation branch (#787) in master.) +(Please note that this branch is always kept up to date with master. If you are using the navigation branch, some of the changes include:) +- Nav: Added `#define IMGUI_HAS_NAV` in imgui.h to ease sharing code between both branches. (#787) +- Nav: MainMenuBar now releases focus when user gets out of the menu layer. (#787) +- Nav: When applying focus to a window with only menus, the menu layer is automatically activated. (#787) +- Nav: Added `ImGuiNavInput_KeyMenu` (~Alt key) aside from ImGuiNavInput_PadMenu input as it is one differentiator of pad vs keyboard that was detrimental to the keyboard experience. Although isn't officially supported, it makes the current experience better. (#787) +- Nav: Move requests now wrap vertically inside Menus and Pop-ups. (#787) +- Nav: Allow to collapse tree nodes with NavLeft and open them with NavRight. (#787, #1079). +- Nav: It's now possible to navigate sibling of a menu-bar while navigating inside one of their child. If a Left<>Right navigation request fails to find a match we forward the request to the root menu. (#787, #126) +- Nav: Fixed `SetItemDefaultFocus` from stealing default focus when we are initializing default focus for a menu bar layer. (#787) +- Nav: Support for fall-back horizontal scrolling with PadLeft/PadRight (nb: fall-back scrolling is only used to navigate windows that have no interactive items). (#787) +- Nav: Fixed tool-tip from being selectable in the window selection list. (#787) +- Nav: `CollapsingHeader(bool*)` variant: fixed for `IsItemHovered()` not working properly in the nav branch. (#600, #787) +- Nav: InputText: Fixed using Up/Down history callback feature when Nav is enabled. (#787) +- Nav: InputTextMultiline: Fixed navigation/selection. Disabled selecting all when activating a multi-line text editor. (#787) +- Nav: More consistently drawing a (thin) navigation rectangle hover filled frames such as tree nodes, collapsing header, menus. (#787) +- Nav: Various internal refactoring. + + +----------------------------------------------------------------------- + VERSION 1.51 (2017-08-24) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.51 + +Breaking Changes: + +Work on dear imgui has been gradually resuming. It means that fixes and new features should be tackled at a faster rate than last year. However, in order to move forward with the library and get rid of some cruft, I have taken the liberty to be a little bit more aggressive than usual with API breaking changes. Read the details below and search for those names in your code! In the grand scheme of things, those changes are small and should not affect everyone, but this is technically our most aggressive release so far in term of API breakage. If you want to be extra forward-facing, you can enable `#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS` in your imconfig.h to disable the obsolete names/redirection. + +- Renamed `IsItemHoveredRect()` to `IsItemRectHovered()`. Kept inline redirection function (will obsolete). +- Renamed `IsMouseHoveringWindow()` to `IsWindowRectHovered()` for consistency. Kept inline redirection function (will obsolete). +- Renamed `IsMouseHoveringAnyWindow()` to `IsAnyWindowHovered()` for consistency. Kept inline redirection function (will obsolete). +- Renamed `ImGuiCol_Columns***` enums to `ImGuiCol_Separator***`. Kept redirection enums (will obsolete). +- Renamed `ImGuiSetCond***` types and enums to `ImGuiCond***`. Kept redirection enums (will obsolete). +- Renamed `GetStyleColName()` to `GetStyleColorName()` for consistency. Unlikely to be used by end-user! +- Added `PushStyleColor(ImGuiCol idx, ImU32 col)` overload, which _might_ cause an "ambiguous call" compilation error if you are using ImColor() with implicit cast. Cast to ImU32 or ImVec4 explicitly to fix. +- Marked the weird `IMGUI_ONCE_UPON_A_FRAME` helper macro as obsolete. Prefer using the more explicit `ImGuiOnceUponAFrame`. +- Changed `ColorEdit4(const char* label, float col[4], bool show_alpha = true)` signature to `ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0)`, where flags 0x01 is a safe no-op (hello dodgy backward compatibility!). The new `ColorEdit4`/`ColorPicker4` functions have lots of available flags! Check and run the demo window, under "Color/Picker Widgets", to understand the various new options. +- Changed signature of `ColorButton(ImVec4 col, bool small_height = false, bool outline_border = true)` to `ColorButton(const char* desc_id, ImVec4 col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0,0))`. This function was rarely used and was very dodgy (no explicit ID!). +- Changed `BeginPopupContextWindow(bool also_over_items=true, const char* str_id=NULL, int mouse_button=1)` signature to `(const char* str_id=NULL, int mouse_button=1, bool also_over_items=true)`. This is perhaps the most aggressive change in this update, but note that the majority of users relied on default parameters completely, so this will affect only a fraction of users of this already rarely used function. +- Removed `IsPosHoveringAnyWindow()`, which was partly broken and misleading. In the vast majority of cases, people using that function wanted to use `io.WantCaptureMouse` flag. Replaced with IM_ASSERT + comment redirecting user to `io.WantCaptureMouse`. (#1237) +- Removed the old `ValueColor()` helpers, they are equivalent to calling `Text(label)` + `SameLine()` + `ColorButton()`. +- Removed `ColorEditMode()` and `ImGuiColorEditMode` type in favor of `ImGuiColorEditFlags` and parameters to the various Color*() functions. The `SetColorEditOptions()` function allows to initialize default but the user can still change them with right-click context menu. Commenting out your old call to `ColorEditMode()` may just be fine! + +Other Changes: + +- Added flags to `ColorEdit3()`, `ColorEdit4()`. The color edit widget now has a context-menu and access to the color picker. (#346) +- Added flags to `ColorButton()`. (#346) +- Added `ColorPicker3()`, `ColorPicker4()`. The API along with those of the updated `ColorEdit4()` was designed so you may use them in various situation and hopefully compose your own picker if required. There are a bunch of available flags, check the Demo window and comment for `ImGuiColorEditFlags_`. Some of the options it supports are: two color picker types (hue bar + sat/val rectangle, hue wheel + rotating sat/val triangle), display as u8 or float, lifting 0.0..1.0 constraints (currently rgba only), context menus, alpha bar, background checkerboard options, preview tooltip, basic revert. For simple use, calling the existing `ColorEdit4()` function as you did before will be enough, as you can now open the color picker from there. (#346) [@r-lyeh, @nem0, @thennequin, @dariomanesku and @ocornut] +- Added `SetColorEditOptions()` to set default color options (e.g. if you want HSV over RGBA, float over u8, select a default picker mode etc. at startup time without a user intervention. Note that the user can still change options with the context menu unless disabled with `ImGuiColorFlags_NoOptions` or explicitly enforcing a display type/picker mode etc.). +- Added user-facing `IsPopupOpen()` function. (#891) [@mkeeter] +- Added `GetColorU32(u32)` variant that perform the style alpha multiply without a floating-point round trip, and helps makes code more consistent when using ImDrawList APIs. +- Added `PushStyleColor(ImGuiCol idx, ImU32 col)` overload. +- Added `GetStyleColorVec4(ImGuiCol idx)` which is equivalent to accessing `ImGui::GetStyle().Colors[idx]` (aka return the raw style color without alpha alteration). +- ImFontAtlas: Added `GlyphRangesBuilder` helper class, which makes it easier to build custom glyph ranges from your app/game localization data, or add into existing glyph ranges. +- ImFontAtlas: Added `TexGlyphPadding` option. (#1282) [@jadwallis] +- ImFontAtlas: Made it possible to override size of AddFontDefault() (even if it isn't really recommended!). +- ImDrawList: Added `GetClipRectMin()`, `GetClipRectMax()` helpers. +- Fixed Ini saving crash if the ImGuiWindowFlags_NoSavedSettings gets removed from a window after its creation (unlikely!). (#1000) +- Fixed `PushID()`/`PopID()` from marking parent window as Accessed (which needlessly woke up the root "Debug" window when used outside of a regular window). (#747) +- Fixed an assert when calling `CloseCurrentPopup()` twice in a row. [@nem0] +- Window size can be loaded from .ini data even if ImGuiWindowFlags_NoResize flag is set. (#1048, #1056) +- Columns: Added `SetColumnWidth()`. (#913) [@ggtucker] +- Columns: Dragging a column preserve its width by default. (#913) [@ggtucker] +- Columns: Fixed first column appearing wider than others. (#1266) +- Columns: Fixed allocating space on the right-most side with the assumption of a vertical scrollbar. The space is only allocated when needed. (#125, #913, #893, #1138) +- Columns: Fixed the right-most column from registering its content width to the parent window, which led to various issues when using auto-resizing window or e.g. horizontal scrolling. (#519, #125, #913) +- Columns: Refactored some of the columns code internally toward a better API (not yet exposed) + minor optimizations. (#913) [@ggtucker, @ocornut] +- Popups: Most pop-ups windows can be moved by the user after appearing (if they don't have explicit positions provided by caller, or e.g. sub-menu pop-up). The previous restriction was totally arbitrary. (#1252) +- Tooltip: `SetTooltip()` is expanded immediately into a window, honoring current font / styling setting. Add internal mechanism to override tooltips. (#862) +- PlotHistogram: bars are drawn based on zero-line, so negative values are going under. (#828) +- Scrolling: Fixed return values of `GetScrollMaxX()`, `GetScrollMaxY()` when both scrollbars were enabled. Tweak demo to display more data. (#1271) [@degracode] +- Scrolling: Fixes for Vertical Scrollbar not automatically getting enabled if enabled Horizontal Scrollbar straddle the vertical limit. (#1271, #246) +- Scrolling: `SetScrollHere()`, `SetScrollFromPosY()`: Fixed Y scroll aiming when Horizontal Scrollbar is enabled. (#665). +- [Windows] Clipboard: Fixed not closing Win32 clipboard on early open failure path. (#1264) +- Removed an unnecessary dependency on int64_t which failed on some older compilers. +- Demo: Rearranged everything under Widgets in a more consistent way. +- Demo: Columns: Added Horizontal Scrolling demo. Tweaked another Columns demo. (#519, #125, #913) +- Examples: OpenGL: Various makefiles for MINGW, Linux. (#1209, #1229, #1209) [@fr500, @acda] +- Examples: Enabled vsync by default in example applications, so it doesn't confuse people that the sample run at 2000+ fps and waste an entire CPU. (#1213, #1151). +- Various other small fixes, tweaks, comments, optimizations. + + +----------------------------------------------------------------------- + VERSION 1.50 (2017-06-02) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.50 + +Breaking Changes: + +- Added a void* user_data parameter to Clipboard function handlers. (#875) +- SameLine(x) with x>0.0f is now relative to left of column/group if any, and not always to left of window. This was sort of always the intent and hopefully breakage should be minimal. +- Renamed ImDrawList::PathFill() - rarely used directly - to ImDrawList::PathFillConvex() for clarity and consistency. +- Removed ImFontConfig::MergeGlyphCenterV in favor of a more multipurpose ImFontConfig::GlyphOffset. +- Style: style.WindowTitleAlign is now a ImVec2 (ImGuiAlign enum was removed). set to (0.5f,0.5f) for horizontal+vertical centering, (0.0f,0.0f) for upper-left, etc. +- BeginChild(const char*) now applies the stack id to the provided label, consistently with other functions as it should always have been. It shouldn't affect you unless (extremely unlikely) you were appending multiple times to a same child from different locations of the stack id. If that's the case, generate an id with GetId() and use it instead of passing string to BeginChild(). + +Other Changes: + +- InputText(): Added support for CTRL+Backspace (delete word). +- InputText(): OSX uses Super+Arrows for home/end. Add Shortcut+Backspace support. (#650) [@michaelbartnett] +- InputText(): Got rid of individual OSX-specific options in ImGuiIO, added a single io.OSXBehaviors flag. (#473, #650) +- InputText(): Fixed pressing home key on last character when it isn't a trailing \n (#588, #815) +- InputText(): Fixed state corruption/crash bug in stb_textedit.h redo logic when exhausting undo/redo char buffer. (#715. #681) +- InputTextMultiline(): Fixed CTRL+DownArrow moving scrolling out of bounds. +- InputTextMultiline(): Scrollbar fix for when input and latched internal buffers differs in a way that affects vertical scrollbar existence. (#725) +- ImFormatString(): Fixed an overflow handling bug with implementation of vsnprintf() that do not return -1. (#793) +- BeginChild(const char*) now applies stack id to provided label, consistent with other widgets. (#894, #713) +- SameLine() with explicit X position is relative to left of group/columns. (ref #746, #125, #630) +- SliderInt(), SliderFloat() supports reverse direction (where v_min > v_max). (#854) +- SliderInt(), SliderFloat() better support for when v_min==v_max. (#919) +- SliderInt(), SliderFloat() enforces writing back value when interacting, to be consistent with other widgets. (#919) +- SliderInt, SliderFloat(): Fixed edge case where style.GrabMinSize being bigger than slider width can lead to a division by zero. (#919) +- Added IsRectVisible() variation with explicit start-end positions. (#768) [@thedmd] +- Fixed TextUnformatted() clipping bug in the large-text path when horizontal scroll has been applied. (#692, #246) +- Fixed minor text clipping issue in window title when using font straying above usual line. (#699) +- Fixed SetCursorScreenPos() fixed not adjusting CursorMaxPos as well. +- Fixed scrolling offset when using SetScrollY(), SetScrollFromPosY(), SetScrollHere() with menu bar. +- Fixed using IsItemActive() after EndGroup() or any widget using groups. (#840, #479) +- Fixed IsItemActive() lagging by one frame on initial widget activation. (#840) +- Fixed Separator() zero-height bounding box resulting in clipping when laying exactly on top line of clipping rectangle (#860) +- Fixed PlotLines() PlotHistogram() calling with values_count == 0. +- Fixed clicking on a window's void while staying still overzealously marking .ini settings as dirty. (#923) +- Fixed assert triggering when a window has zero rendering but has a callback. (#810) +- Scrollbar: Fixed rendering when sizes are negative to reduce glitches (which can happen with certain style settings and zero WindowMinSize). +- EndGroup(): Made IsItemHovered() work when an item was activated within the group. (#849) +- BulletText(): Fixed stopping to display formatted string after the '##' mark. +- Closing the focused window restore focus to the first active root window in descending z-order .(part of #727) +- Word-wrapping: Fixed a bug where we never wrapped after a 1 character word. [@sronsse] +- Word-wrapping: Fixed TextWrapped() overriding wrap position if one is already set. (#690) +- Word-wrapping: Fixed incorrect testing for negative wrap coordinates, they are perfectly legal. (#706) +- ImGuiListClipper: Fixed automatic-height calc path dumbly having user display element 0 twice. (#661, #716) +- ImGuiListClipper: Fix to behave within column. (#661, #662, #716) +- ImDrawList: Renamed ImDrawList::PathFill() to ImDrawList::PathFillConvex() for clarity. (BREAKING API) +- Columns: End() avoid calling Columns(1) if no columns set is open, not sure why it wasn't the case already (pros: faster, cons: exercise less code). +- ColorButton(): Fix ColorButton showing wrong hex value for alpha. (#1068) [@codecat] +- ColorEdit4(): better preserve inputting value out of 0..255 range, display then clamped in Hexadecimal form. +- Shutdown() clear out some remaining pointers for sanity. (#836) +- Added IMGUI_USE_BGRA_PACKED_COLOR option in imconfig.h (#767, #844) [@thedmd] +- Style: Removed the inconsistent shadow under RenderCollapseTriangle() (~#707) +- Style: Added ButtonTextAlign, ImGuiStyleVar_ButtonTextAlign. (#842) +- ImFont: Allowing to use up to 0xFFFE glyphs in same font (increased from previous 0x8000). +- ImFont: Added GetGlyphRangesThai() helper. [@nProtect] +- ImFont: CalcWordWrapPositionA() fixed font scaling with fallback character. +- ImFont: Calculate and store the approximate texture surface to get an idea of how costly each source font is. +- ImFontConfig: Added GlyphOffset to explicitly offset glyphs at font build time, useful for merged fonts. Removed MergeGlyphCenterV. (BREAKING API) +- Clarified asserts in CheckStacksSize() when there is a stack mismatch. +- Context: Support for #define-ing GImGui and IMGUI_SET_CURRENT_CONTEXT_FUNC to enable custom thread-based hackery (#586) +- Updated stb_truetype.h to 1.14 (added OTF support, removed warnings). (#883, #976) +- Updated stb_rect_pack.h to 0.10 (removed warnings). (#883) +- Added ImGuiMouseCursor_None enum value for convenient usage by app/backends. +- Clipboard: Added a void* user_data parameter to Clipboard function handlers. (#875) (BREAKING API) +- Internals: Refactor internal text alignment options to use ImVec2, removed ImGuiAlign. (#842, #222) +- Internals: Renamed ImLoadFileToMemory to ImFileLoadToMemory to be consistent with ImFileOpen + fix mismatching .h name. (#917) +- OS/Windows: Fixed Windows default clipboard handler leaving its buffer unfreed on application's exit. (#714) +- OS/Windows: No default IME handler when compiling for Windows using GCC. (#738) +- OS/Windows: Now using _wfopen() instead of fopen() to allow passing in paths/filenames with UTF-8 characters. (#917) +- Tools: binary_to_compressed_c: Avoid ?? trigraphs sequences in string outputs which break some older compilers. (#839) +- Demo: Added an extra 3-way columns demo. +- Demo: ShowStyleEditor: show font character map / grid in more details. +- Demo: Console: Fixed a completion bug when multiple candidates are equals and match until the end. +- Demo: Fixed 1-byte off overflow in the ShowStyleEditor() combo usage. (#783) [@bear24rw] +- Examples: Accessing ImVector fields directly, feel less stl-ey. (#810) +- Examples: OpenGL*: Saving/restoring existing scissor rectangle for completeness. (#807) +- Examples: OpenGL*: Saving/restoring active texture number (the value modified by glActiveTexture). (#1087, #1088, #1116) +- Examples: OpenGL*: Saving/restoring separate color/alpha blend functions correctly. (#1120) [@greggman] +- Examples: OpenGL2: Uploading font texture as RGBA32 to increase compatibility with users shaders for beginners. (#824) +- Examples: Vulkan: Countless fixes and improvements. (#785, #804, #910, #1017, #1039, #1041, #1042, #1043, #1080) [@martty, @Loftilus, @ParticlePeter, @SaschaWillems] +- Examples: DirectX9/10/10: Only call SetCursor(NULL) is io.MouseDrawCursor is set. (#585, #909) +- Examples: DirectX9: Explicitly setting viewport to match that other examples are doing. (#937) +- Examples: GLFW+OpenGL3: Fixed Shutdown() calling GL functions with NULL parameters if NewFrame was never called. (#800) +- Examples: GLFW+OpenGL2: Renaming opengl_example/ to opengl2_example/ for clarity. +- Examples: SDL+OpenGL: explicitly setting GL_UNPACK_ROW_LENGTH to reduce issues because SDL changes it. (#752) +- Examples: SDL2: Added build .bat files for Win32. +- Added various links to language/engine bindings. +- Various other minor fixes, tweaks, comments, optimizations. + + +----------------------------------------------------------------------- + VERSION 1.49 (2016-05-09) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.49 + +Breaking Changes: + +- Renamed `SetNextTreeNodeOpened()` to `SetNextTreeNodeOpen()` for consistency, no redirection. +- Removed confusing set of `GetInternalState()`, `GetInternalStateSize()`, `SetInternalState()` functions. Now using `CreateContext()`, `DestroyContext()`, `GetCurrentContext()`, `SetCurrentContext()`. If you were using multiple contexts the change should be obvious and trivial. +- Obsoleted old signature of `CollapsingHeader(const char* label, const char* str_id = NULL, bool display_frame = true, bool default_open = false)`, as extra parameters were badly designed and rarely used. Most uses were using 1 parameter and shouldn't affect you. You can replace the "default_open = true" flag in new API with `CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen)`. +- Changed `ImDrawList::PushClipRect(ImVec4 rect)` to `ImDraw::PushClipRect(ImVec2 min,ImVec2 max,bool intersect_with_current_clip_rect=false)`. Note that higher-level `ImGui::PushClipRect()` is preferable because it will clip at logic/widget level, whereas `ImDrawList::PushClipRect()` only affect your renderer. +- Title bar (using ImGuiCol_TitleBg/ImGuiCol_TitleBgActive colors) isn't rendered over a window background (ImGuiCol_WindowBg color) anymore (see #655). If your TitleBg/TitleBgActive alpha was 1.0f or you are using the default theme it will not affect you. However if your TitleBg/TitleBgActive alpha was <1.0f you need to tweak your custom theme to readjust for the fact that we don't draw a WindowBg background behind the title bar. + This helper function will convert an old TitleBg/TitleBgActive color into a new one with the same visual output, given the OLD color and the OLD WindowBg color. (Or If this is confusing, just pick the RGB value from title bar from an old screenshot and apply this as TitleBg/TitleBgActive. Or you may just create TitleBgActive from a tweaked TitleBg color.) + + ImVec4 ConvertTitleBgCol(const ImVec4& win_bg_col, const ImVec4& title_bg_col) + { + float new_a = 1.0f - ((1.0f - win_bg_col.w) * (1.0f - title_bg_col.w)); + float k = title_bg_col.w / new_a; + return ImVec4((win_bg_col.x * win_bg_col.w + title_bg_col.x) * k, (win_bg_col.y * win_bg_col.w + title_bg_col.y) * k, (win_bg_col.z * win_bg_col.w + title_bg_col.z) * k, new_a); + } + +Other changes: + +- New version of ImGuiListClipper helper calculates item height automatically. See comments and demo code. (#662, #661, #660) +- Added SetNextWindowSizeConstraints() to enable basic min/max and programmatic size constraints on window. Added demo. (#668) +- Added PushClipRect()/PopClipRect() (previously part of imgui_internal.h). Changed ImDrawList::PushClipRect() prototype. (#610) +- Added IsRootWindowOrAnyChildHovered() helper. (#615) +- Added TreeNodeEx() functions. (#581, #600, #190) +- Added ImGuiTreeNodeFlags_Selected flag to display TreeNode as "selected". (#581, #190) +- Added ImGuiTreeNodeFlags_AllowOverlapMode flag. (#600) +- Added ImGuiTreeNodeFlags_NoTreePushOnOpen flag (#590). +- Added ImGuiTreeNodeFlags_NoAutoOpenOnLog flag (previously private). +- Added ImGuiTreeNodeFlags_DefaultOpen flag (previously private). +- Added ImGuiTreeNodeFlags_OpenOnDoubleClick flag. +- Added ImGuiTreeNodeFlags_OpenOnArrow flag. +- Added ImGuiTreeNodeFlags_Leaf flag, always opened, no arrow, for convenience. For simple use case prefer using TreeAdvanceToLabelPos()+Text(). +- Added ImGuiTreeNodeFlags_Bullet flag, to add a bullet to Leaf node or replace Arrow with a bullet. +- Added TreeAdvanceToLabelPos(), GetTreeNodeToLabelSpacing() helpers. (#581, #324) +- Added CreateContext()/DestroyContext()/GetCurrentContext()/SetCurrentContext(). Obsoleted nearly identical GetInternalState()/SetInternalState() functions. (#586, #269) +- Added NewLine() to undo a SameLine() and as a shy reminder that horizontal layout support hasn't been implemented yet. +- Added IsItemClicked() helper. (#581) +- Added CollapsingHeader() variant with close button. (#600) +- Fixed MenuBar missing lower border when borders are enabled. +- InputText(): Fixed clipping of cursor rendering in case it gets out of the box (which can be forced w/ ImGuiInputTextFlags_NoHorizontalScroll. (#601) +- Style: Changed default IndentSpacing from 22 to 21. (#581, #324) +- Style: Fixed TitleBg/TitleBgActive color being rendered above WindowBg color, which was inconsistent and causing visual artifact. (#655) + This broke the meaning of TitleBg and TitleBgActive. Only affect values where Alpha<1.0f. Fixed default theme. Read comments in "API BREAKING CHANGES" section to convert. +- Relative rendering of order of Child windows creation is preserved, to allow more control with overlapping children. (#595) +- Fixed GetWindowContentRegionMax() being off by ScrollbarSize amount when explicit SizeContents is set. +- Indent(), Unindent(): optional non-default indenting width. (#324, #581) +- Bullet(), BulletText(): Slightly bigger. Less polygons. +- ButtonBehavior(): fixed subtle old bug when a repeating button would also return true on mouse release (barely noticeable unless RepeatRate is set to be very slow). (#656) +- BeginMenu(): a menu that becomes disabled while open gets closed down, facilitate user's code. (#126) +- BeginGroup(): fixed using within Columns set. (#630) +- Fixed a lag in reading the currently hovered window when dragging a window. (#635) +- Obsoleted 4 parameters version of CollapsingHeader(). Refactored code into TreeNodeBehavior. (#600, #579) +- Scrollbar: minor fix for top-right rounding of scrollbar background when window has menu bar but no title bar. +- MenuItem(): the check mark renders in disabled color when menu item is disabled. +- Fixed clipping rectangle floating point representation to ensure renderer-side float point operations yield correct results in typical DirectX/GL settings. (#582, 597) +- Fixed GetFrontMostModalRootWindow(), fixing missing fade-out when a combo pop was used stacked over a modal window. (#604) +- ImDrawList: Added AddQuad(), AddQuadFilled() helpers. +- ImDrawList: AddText() refactor, moving some code to ImFont, reserving less unused vertices when large vertical clipping occurs. +- ImFont: Added RenderChar() helper. +- ImFont: Added AddRemapChar() helper. (#609) +- ImFontConfig: Clarified persistence requirement of GlyphRanges array. (#651) +- ImGuiStorage: Added bool helper functions for completeness. +- AddFontFromMemoryCompressedTTF(): Fix ImFontConfig propagation. (#587) +- Renamed majority of use of the word "opened" to "open" for clarity. Renamed SetNextTreeNodeOpened() to SetNextTreeNodeOpen(). (#625, #579) +- Examples: OpenGL3: Saving/restoring glActiveTexture() state. (#602) +- Examples: DirectX9: save/restore all device state. +- Examples: DirectX9: Removed dependency on d3dx9.h, d3dx9.lib, dxguid.lib so it can be used in a DirectXMath.h only environment. (#611) +- Examples: DirectX10/X11: Apply depth-stencil state (no use of depth buffer). (#640, #636) +- Examples: DirectX11/X11: Added comments on removing dependency on D3DCompiler. (#638) +- Examples: SDL: Initialize video+timer subsystem only. +- Examples: Apple/iOS: lowered XCode project deployment target from 10.7 to 10.11. (#598, #575) + + +----------------------------------------------------------------------- + VERSION 1.48 (2016-04-09) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.48 + +Breaking Changes: + +- Consistently honoring exact width passed to PushItemWidth() (when positive), previously it would add extra FramePadding.x*2 over that width. Some hand-tuned layout may be affected slightly. (#346) +- Style: removed `style.WindowFillAlphaDefault` which was confusing and redundant, baked alpha into `ImGuiCol_WindowBg` color. If you had a custom WindowBg color but didn't change WindowFillAlphaDefault, multiply WindowBg alpha component by 0.7. Renamed `ImGuiCol_TooltipBg` to `ImGuiCol_PopupBG`, applies to other types of pop-ups. `bg_alpha` parameter of 5-parameters version of Begin() is an override. (#337) +- InputText(): Added BufTextLen field in ImGuiTextEditCallbackData. Requesting user to update it if the buffer is modified in the callback. Added a temporary length-check assert to minimize panic for the 3 people using the callback. (#541) +- Renamed GetWindowFont() to GetFont(), GetWindowFontSize() to GetFontSize(). Kept inline redirection function (will obsolete). (#340) + +Other Changes: + +- Consistently honoring exact width passed to PushItemWidth(), previously it would add extra FramePadding.x*2 over that width. Some hand-tuned layout may be affected slightly. (#346) +- Fixed clipping of child windows within parent not taking account of child outer clipping boundaries (including scrollbar, etc.). (#506) +- TextUnformatted(): Fixed rare crash bug with large blurb of text (2k+) not finished with a '\n' and fully above the clipping Y line. (#535) +- IO: Added 'KeySuper' field to hold CMD keyboard modifiers for OS X. Updated all examples accordingly. (#473) +- Added ImGuiWindowFlags_ForceVerticalScrollbar, ImGuiWindowFlags_ForceHorizontalScrollbar flags. (#476) +- Added IM_COL32 macros to generate a U32 packed color, convenient for direct use of ImDrawList api. (#346) +- Added GetFontTexUvWhitePixel() helper, convenient for direct use of ImDrawList api. +- Selectable(): Added ImGuiSelectableFlags_AllowDoubleClick flag to allow user reacting on double-click. (@zapolnov) (#516) +- Begin(): made the close button explicitly set the boolean to false instead of toggling it. (#499) +- BeginChild()/EndChild(): fixed incorrect layout to allow widgets submitted after an auto-fitted child window. (#540) +- BeginChild(): Added ImGuiWindowFlags_AlwaysUseWindowPadding flag to ensure non-bordered child window uses window padding. (#462) +- Fixed InputTextMultiLine(), ListBox(), BeginChildFrame(), ProgressBar(): outer frame not honoring bordering. (#462, #503) +- Fixed Image(), ImageButtion() rendering a rectangle 1 px too large on each axis. (#457) +- SetItemAllowOverlap(): Promoted from imgui_internal.h to public imgui.h api. (#517) +- Combo(): Right-most button stays highlighted when pop-up is open. +- Combo(): Display pop-up above if there's isn't enough space below / or select largest side. (#505) +- DragFloat(), SliderFloat(), InputFloat(): fixed cases of erroneously returning true repeatedly after a text input modification (e.g. "0.0" --> "0.000" would keep returning true). (#564) +- DragFloat(): Always apply value when mouse is held/widget active, so that an always-resetting variable (e.g. non saved local) can be passed. +- InputText(): OS X friendly behaviors: Word movement uses ALT key; Shortcuts uses CMD key; Double-clicking text select a single word; Jumping to next word sets cursor to end of current word instead of beginning of current word. (@zhiayang), (#473) +- InputText(): Added BufTextLen in ImGuiTextEditCallbackData. Requesting user to maintain it if buffer is modified. Zero-ing structure properly before use. (#541) +- CheckboxFlags(): Added support for testing/setting multiple flags at the same time. (@DMartinek) (#555) +- TreeNode(), CollapsingHeader() fixed not being able to use "##" sequence in a formatted label. +- ColorEdit4(): Empty label doesn't add InnerSpacing.x, matching behavior of other widgets. (#346) +- ColorEdit4(): Removed unnecessary calls to scanf() when idle in hexadecimal edit mode. +- BeginPopupContextItem(), BeginPopupContextWindow(): added early out optimization. +- CaptureKeyboardFromApp() / CaptureMouseFromApp(): added argument to allow clearing the capture flag. (#533) +- ImDrawList: Fixed index-overflow check broken by AddText() casting current index back to ImDrawIdx. (#514) +- ImDrawList: Fixed incorrect removal of trailing draw command if it is a callback command. +- ImDrawList: Allow windows with only a callback only to be functional. (#524) +- ImDrawList: Fixed ImDrawList::AddRect() which used to render a rectangle 1 px too large on each axis. (#457) +- ImDrawList: Fixed ImDrawList::AddCircle() to fit precisely within bounding box like AddCircleFilled() and AddRectFilled(). (#457) +- ImDrawList: AddCircle(), AddRect() takes optional thickness parameter. +- ImDrawList: Added AddTriangle(). +- ImDrawList: Added PrimQuadUV() helper to ease custom rendering of textured quads (require primitive reserve). +- ImDrawList: Allow AddText(ImFont\* font, float font_size, ...) variant to take NULL/0.0f as default. +- ImFontAtlas: heuristic increase default texture width up for large number of glyphs. (#491) +- ImTextBuffer: Fixed empty() helper which was utterly broken. +- Metrics: allow to inspect individual triangles in draw calls. +- Demo: added more draw primitives in the Custom Rendering example. (#457) +- Demo: extra comments and example for PushItemWidth(-1) patterns. +- Demo: InputText password demo filters out blanks. (#515) +- Demo: Fixed malloc/free mismatch and leak when destructing demo console, if it has been used. (@fungos) (#536) +- Demo: plot code doesn't use ImVector to avoid heap allocation and be more friendly to custom allocator users. (#538) +- Fixed compilation on DragonFly BSD (@mneumann) (#563) +- Examples: Vulkan: Added a Vulkan example (@Loftilus) (#549) +- Examples: DX10, DX11: Saving/restoring most device state so dropping render function in your codebase shouldn't have DX device side-effects. (#570) +- Examples: DX10, DX11: Fixed ImGui_ImplDX??_NewFrame() from recreating device objects if render isn't called (g_pVB not set). +- Examples: OpenGL3: Fix BindVertexArray/BindBuffer order. (@nlguillemot) (#527) +- Examples: OpenGL: skip rendering and calling glViewport() if we have zero-fixed buffer. (#486) +- Examples: SDL2+OpenGL3: Fix context creation options. Made ImGui_ImplSdlGL3_NewFrame() signature match GL2 one. (#468, #463) +- Examples: SDL2+OpenGL2/3: Fix for high-dpi displays. (@nickgravelyn) +- Various extra comments and clarification in the code. +- Various other fixes and optimizations. + + +----------------------------------------------------------------------- + VERSION 1.47 (2015-12-25) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.47 + +Changes: + +- Rebranding "ImGui" -> "dear imgui" as an optional first name to reduce ambiguity with IMGUI term. (#21) +- Added ProgressBar(). (#333) +- InputText(): Added ImGuiInputTextFlags_Password mode: hide display, disable logging/copying to clipboard. (#237, #363, #374) +- Added GetColorU32() helper to retrieve color given enum with global alpha and extra applied. +- Added ImGuiIO::ClearInputCharacters() superfluous helper. +- Fixed ImDrawList draw command merging bug where using PopClipRect() along with PushTextureID()/PopTextureID() functions + would occasionally restore an incorrect clipping rectangle. +- Fixed ImDrawList draw command merging so PushTextureID(XXX)/PopTextureID()/PushTextureID(XXX) sequence are now properly merged. +- Fixed large popups positioning issues when their contents on either axis is larger than DisplaySize, + and WindowPadding < DisplaySafeAreaPadding. +- Fixed border rendering in various situations when using non-pixel aligned glyphs. +- Fixed border rendering of windows to always contain the border within the window. +- Fixed Shutdown() leaking font atlas data if NewFrame() was never called. (#396, #303) +- Fixed int>void\* warnings for 64-bit architectures with fancy warnings enabled. +- Renamed the dubious Color() helpers to ValueColor() - dangerously named, rarely used and probably to be made obsolete. +- InputText(): Fixed and better handling of using keyboard while mouse button if being held and dragging. (#429) +- InputText(): Replace OS IME (Input Method Editor) cursor on top-left when we are not text editing. +- TreeNode(), CollapsingHeader(), Bullet(), BulletText(): various sizing and layout fixes to better support laying out + multiple item with different height on same line. (#414, #282) +- Begin(): Initial window creation with ImGuiWindowFlags_NoBringToFrontOnFocus flag pushes it at the front of global window list. +- BeginPopupContextWindow() and BeginPopupContextVoid() reopen window on subsequent click. (#439) +- ColorEdit4(): Fixed broken tooltip on hovering the color button. (actually fixes #373, #380) +- ImageButton(): uses FrameRounding up to a maximum of available framing size. (#394) +- Columns: Fixed bug with indentation within columns, also making code a bit shorter/faster. (#414, #125) +- Columns: Columns set with no implicit id include the columns count within the id to reduce collisions. (#125) +- Columns: Removed one unnecessary allocation when columns are not used by a window. (#125) +- ImFontAtlas: Tweaked GetGlyphRangesJapanese() so it is easier to modify. +- ImFontAtlas: Updated stb_rect_pack.h to 0.08. +- Metrics: Fixed computing ImDrawCmd bounding box when the draw buffer have been unindexed. +- Demo: Added a simple "Property Editor" demo applet. (#125, #414) +- Demo: Fixed assertion in "Custom Rendering" demo when holding both mouse buttons. (#393) +- Demo: Lots of extra comments, fixes. +- Demo: Tweaks to Style Editor. +- Examples: Not clearing input data/tex data in atlas (will be required for dynamic atlas anyway). +- Examples: Added /Zi (output debug information) to Win32 batch files. +- Examples: Various fixes for resizing window and recreating graphic context. +- Examples: OpenGL2/3: Save/restore viewport as part of default render function. (#392, #441). +- Examples; OpenGL3: Fixed gl3w.c for Linux when compiled with a C++ compiler. (#411) +- Examples: DirectX: Removed assumption about Unicode build in example main.cpp. (#399) +- Examples: DirectX10: Added DirectX10 example. (#424) +- Examples: DirectX11: Downgraded requirement from shader model 5.0 to 4.0. (#420) +- Examples: DirectX11: Removed Debug flag from graphics context. (#415) +- Examples: Added SDL+OpenGL3 example. (#356) + + +----------------------------------------------------------------------- + VERSION 1.46 (2015-10-18) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.46 + +Changes: + +- Begin*(): added ImGuiWindowFlags_NoFocusOnAppearing flag. (#314) +- Begin*(): added ImGuiWindowFlags_NoBringToFrontOnFocus flag. +- Added GetDrawData() alternative to setting a Render function pointer in ImGuiIO structure. +- Added SetClipboardText(), GetClipboardText() helper shortcuts that user code can call directly without reading + from the ImGuiIO structure (to match MemAlloc/MemFree) +- Fixed handling of malformed UTF-8 at the end of a non-zero terminated string range. +- Fixed mouse click detection when passing DeltaTime 0.0. (#338) +- Fixed IsKeyReleased() and IsMouseReleased() returning true on the first frame. +- Fixed using SetNextWindow\* functions on Modal windows with a ImGuiSetCond_Appearing condition. (#377) +- IsMouseHoveringRect(): Added 'bool clip' parameter to disable clipping provided rectangle. (#316) +- InputText(): added ImGuiInputTextFlags_ReadOnly flag. (#211) +- InputText(): lose cursor/undo-stack when reactivating focus is buffer has changed size. +- InputText(): fixed ignoring text inputs when ALT or ALTGR are pressed. (#334) +- InputText(): fixed mouse-dragging not tracking the cursor when text doesn't fit. (#339) +- InputText(): fixed cursor pixel-perfect alignment when horizontally scrolling. +- InputText(): fixed crash when passing a buf_size==0 (which can be of use for read-only selectable text boxes). (#360) +- InputFloat() fixed explicit precision modifier, both display and input were broken. +- PlotHistogram(): improved rendering of histogram with a lot of values. +- Dummy(): creates an item so functions such as IsItemHovered() can be used. +- BeginChildFrame() helper: added the extra_flags parameter. +- Scrollbar: fixed rounding of background + child window consistenly have ChildWindowBg color under ScrollbarBg fill. (#355). +- Scrollbar: background color less translucent in default style so it works better when changing background color. +- Scrollbar: fixed minor rendering offset when borders are enabled. (#365) +- ImDrawList: fixed 1 leak per ImDrawList using the ChannelsSplit() API (via Columns). (#318) +- ImDrawList: fixed rectangle rendering glitches with width/height <= 1/2 and rounding enabled. +- ImDrawList: AddImage() uv parameters default to (0,0) and (1,1). +- ImFontAtlas: Added TexDesiredWidth and tweaked default cheapo best-width choice. (#327) +- ImFontAtlas: Added GetGlyphRangesKorean() helper to retrieve unicode ranges for Korean. (#348) +- ImGuiTextFilter::Draw() helper return bool and build when filter is modified. +- ImGuiTextBuffer: added c_str() helper. +- ColorEdit4(): fixed hovering the color button always showing 1.0 alpha. (#373) +- ColorConvertFloat4ToU32() round the floats instead of truncating them. +- Window: Fixed window lower-right clipping limit so it plays more friendly with both OpenGL and DirectX coordinates. +- Internal: Extracted a EndFrame() function out of Render() but kept it internal/private + clarified some asserts. (#335) +- Internal: Added missing IMGUI_API definitions in imgui_internal.h (#326) +- Internal: ImLoadFileToMemory() return void\* instead of taking void*\* + allow optional int\* file_size. +- Demo: Horizontal scrollbar demo allows to enable simultanaeous scrollbars on both axises. +- Tools: binary_to_compressed_c.cpp: added -nocompress option. +- Examples: Added example for the Marmalade platform. +- Examples: Added batch files to build Windows examples with VS. +- Examples: OpenGL3: Saving/restoring more GL state correctly. (#347) +- Examples: OpenGL2/3: Added msys2/mingw64 target to Makefiles. + + +----------------------------------------------------------------------- + VERSION 1.45 (2015-09-01) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.45 + +Breaking Changes: + +- With the addition of better horizontal scrolling primitives I had to make some consistency fixes. + `GetCursorPos()` `SetCursorPos()` `GetContentRegionMax()` `GetWindowContentRegionMin()` `GetWindowContentRegionMax()` + are now incorporating the scrolling amount. They were incorrectly not incorporating this amount previously. + It PROBABLY shouldn't break anything, but that depends on how you used them. Namely: + - If you always used SetCursorPos() with values relative to GetCursorPos() there shouldn't be a problem. + However if you used absolute coordinates, note that SetCursorPosY(100.0f) will put you at +100 from the initial Y position (which may be scrolled out of the view), NOT at +100 from the window top border. Since there wasn't any official scrolling value on X axis (past just manually moving the cursor) this can only affect you if you used to set absolute coordinates on the Y axis which is hopefully rare/unlikely, and trivial to fix. + - The value of GetWindowContentRegionMax() isn't necessarily close to GetWindowWidth() if horizontally scrolling. + Previously they were roughly interchangeable (roughly because the content region exclude window padding). + +Other Changes: + +- Added Horizontal Scrollbar via ImGuiWindowFlags_HorizontalScroll (#246). +- Added GetScrollX(), GetScrollX(), GetScrollMaxX() apis (#246). +- Added SetNextWindowContentSize(), SetNextWindowContentWidth() to explicitly set the content size of a window, which + define the range of scrollbar. When set explicitly it also define the base value from which widget width are derived. +- Added IO.WantTextInput telling when ImGui is expecting text input, so that e.g. OS on-screen keyboard can be enabled. +- Added printf attribute to printf-like text formatting functions (Clang/GCC). +- Added GetMousePosOnOpeningCurrentPopup() helper. +- Added GetContentRegionAvailWidth() helper. +- Malformed UTF-8 data don't terminate string, output 0xFFFD instead (#307). +- ImDrawList: Added AddBezierCurve(), PathBezierCurveTo() API for cubic bezier curves (#311). +- ImDrawList: Allow to override ImDrawIdx type (#292). +- ImDrawList: Added an assert on overflowing index value (#292). +- ImDrawList: Fixed issues with channels split/merge. Now functional without manually adding a draw cmd. Added comments. +- ImDrawData: Added ScaleClipRects() helper useful when rendering scaled. (#287). +- Fixed Bullet() inconsistent layout behaviour when clipped. +- Fixed IsWindowHovered() not taking account of window hoverability (may be disabled because of a popup). +- Fixed InvisibleButton() not honoring negative size consistently with other widgets that do so. +- Fixed OpenPopup() accessing current window, effectively opening "Debug" when called from an empty window stack. +- TreeNode(): Fixed IsItemHovered() result being inconsistent with interaction visuals (#282). +- TreeNode(): Fixed mouse interaction padding past the node label being accounted for in layout (#282). +- BeginChild(): Passing a ImGuiWindowFlags_NoMove inhibits moving parent window from this child. +- BeginChild() fixed missing rounding for child sizes which leaked into layout and have items misaligned. +- Begin(): Removed default name = "Debug" parameter. We already have a "Debug" window pushed to the stack in the first place so it's not really a useful default. +- Begin(): Minor fixes with windows main clipping rectangle (e.g. child window with border). +- Begin(): Window flags are only read on the first call of the frame. Subsequent calls ignore flags, which allows appending to a window without worryin about flags. +- InputText(): ignore character input when ctrl/alt are held. (Normally those text input are ignored by most wrappers.) (#279). +- Demo: Fixed incorrectly formed string passed to Combo (#298). +- Demo: Added simple Log demo. +- Demo: Added horizontal scrolling example + enabled in console, log and child examples (#246). +- Style: made scrollbars rounded by default. Because nice. Minor menu bar background alpha tweak. (#246) +- Metrics: display indices along with triangles count (#299) and some internal state. +- ImGuiTextFilter::PassFilter() supports string range. Added [] helper to ImGuiTextBuffer. +- ImGuiTextFilter::Draw() default parameter width=0.0f for no override, allow override with negative values. +- Examples: OpenGL2/OpenGL3: fix for retina displays. Default font current lack crispness. +- Examples: OpenGL2/OpenGL3: save/restore more GL state correctly. +- Examples: DirectX9/DirectX11: resizing buffers dynamically (#299). +- Examples: DirectX9/DirectX11: added missing middle mouse button to Windows event handler. +- Examples: DirectX11: fix for Visual Studio 2015 presumably shipping with an updated version of DX11. +- Examples: iOS: fixed missing files in project. + + +----------------------------------------------------------------------- + VERSION 1.44 (2015-08-08) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.44 + +Breaking Changes: + +- imgui.cpp has been split intro extra files: imgui_demo.cpp, imgui_draw.cpp, imgui_internal.h. + Add the two extra .cpp to your project or #include them from another .cpp file. (#219) + +Other Changes: + +- Internal data structure and several useful functions are now exposed in imgui_internal.h. This should make it easier + and more natural to extend ImGui. However please note that none of the content in imgui_internal.h is guaranteed + for forward-compatibility and code using those types/functions may occasionally break. (#219) +- All sample code is in imgui_demo.cpp. Please keep this file in your project and consider allowing your code to call + the ShowTestWindow() function as de-facto guide to ImGui features. It will be stripped out by the linker when unused. +- Added GetContentRegionAvail() helper (basically GetContentRegionMax() - GetCursorPos()). +- Added ImGuiWindowFlags_NoInputs for totally input-passthru window. +- Button(): honor negative size consistently with other widgets that do so (width -100 to align the button 100 pixels + before the right-most position of the contents region). +- InputTextMultiline(): honor negative size consistently with other widgets that do so. +- Combo() clamp popup to lower edge of visible area. +- InputInt(): value doesn't pass through an int>float>int casting chain, fix handling lost of precision with "large" integer. +- InputInt() allow hexadecimal input (awkwardly via ImGuiInputTextFlags_CharsHexadecimal but we will allow format + string in InputInt* later). +- Checkbox(), RadioButton(): fixed scaling of checkbox and radio button for the filling of "active" visual. +- Columns: never assume horizontal space for scrollbar if NoScrollbar flag is explicitly set. +- Slider: fixed using FramePadding between frame and grab visual. Scaling that spacing would look odd. +- Fixed lower-right resize grip hit box not scaling along with its rendered size (#287) +- ImDrawList: Fixed angles in ImDrawList::PathArcTo(), PathArcToFast() (v1.43) being off by an extra PI for no reason. +- ImDrawList: Added ImDrawList::AddText() shorthand helper. +- ImDrawList: Add missing support for anti-aliased thick-lines (#133, also ref #288) +- ImFontAtlas: Added AddFontFromMemoryCompressedBase85TTF() to load base85 encoded font string. Default font encoded + as base85 saves ~100 lines / 26 KB of source code. Added base85 output to the binary_to_compressed_c tool. +- Build fix for MinGW (#276). +- Examples: OpenGL3: Fixed running on script core profiles for OSX (#277). +- Examples: OpenGL3: Simplified code using glBufferData for vertices as well (#277, #278) +- Examples: DirectX11: Clear font texture view to ensure Release() doesn't get called twice (#290). +- Updated to stb_truetype 1.07 (back to vanilla version as our minor changes are now in master & fix unlikely assert + with odd fonts (#280) + + +----------------------------------------------------------------------- + VERSION 1.43 (2015-07-17) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.43 + +Breaking Changes: + +- This is a rather important release and we unfortunately had to break the rendering API. + ImGui now requires you to render indexed vertices instead of non-indexed ones. The fix should be very easy. + Sorry for that! This change is saving a fair amount of CPU/GPU and enables us to get anti-aliasing for a marginal cost. + Each ImDrawList now contains both a vertex buffer and an index buffer. For each command, render ElemCount/3 triangles + using indices from the index buffer. +- If you are using a vanilla copy of one of the imgui_impl_XXXX.cpp provided in the example, you just need to update + your copy and you can ignore the rest. +- The signature of the io.RenderDrawListsFn handler has changed + From: ImGui_XXXX_RenderDrawLists(ImDrawList** const cmd_lists, int cmd_lists_count) + To: ImGui_XXXX_RenderDrawLists(ImDrawData* draw_data) + With: argument 'cmd_lists' -> 'draw_data->CmdLists' + argument 'cmd_lists_count' -> 'draw_data->CmdListsCount' + ImDrawList 'commands' -> 'CmdBuffer' + ImDrawList 'vtx_buffer' -> 'VtxBuffer' + ImDrawList n/a -> 'IdxBuffer' (new) + ImDrawCmd 'vtx_count' -> 'ElemCount' + ImDrawCmd 'clip_rect' -> 'ClipRect' + ImDrawCmd 'user_callback' -> 'UserCallback' + ImDrawCmd 'texture_id' -> 'TextureId' +- If you REALLY cannot render indexed primitives, you can call the draw_data->DeIndexAllBuffers() method to de-index + the buffers. This is slow and a waste of CPU/GPU. Prefer using indexed rendering! + Refer to code in the examples/ folder or ask on the GitHub if you are unsure of how to upgrade. Please upgrade! + +Other Changes: + +- Added anti-aliasing on lines and shapes based on primitives by @MikkoMononen (#133). + Between the use of indexed-rendering and the fact that the entire rendering codebase has been optimized and massaged + enough, with anti-aliasing enabled ImGui 1.43 is now running FASTER than 1.41. + Made some extra effort in making the code run faster in your typical Debug build. +- Anti-aliasing can be disabled in the ImGuiStyle structure via the AntiAliasedLines/AntiAliasedShapes fields for further gains. +- ImDrawList: Added AddPolyline(), AddConvexPolyFilled() with optional anti-aliasing. +- ImDrawList: Added stateful path building and stroking API. PathLineTo(), PathArcTo(), PathRect(), PathFill(), PathStroke() + with optional anti-aliasing. +- ImDrawList: Added AddRectFilledMultiColor() helper. +- ImDrawList: Added multi-channel rendering so out of order elements can be rendered in separate channels and then merged + back together (used by columns). +- ImDrawList: Fixed merging draw commands when equal clip rectangles are in the two first commands. +- ImDrawList: Fixed window draw lists not destructed properly on Shutdown(). +- ImDrawData: Added DeIndexAllBuffers() helper. +- Added lots of new font options ImFontAtlas::AddFont() and the new ImFontConfig structure. + - Added support for oversampling (ImFontConfig: OversampleH, OversampleV) and sub-pixel positioning (ImFontConfig: PixelSnapH). + Oversampling allows sub-pixel positioning but can also be used as a way to get some leeway with scaling fonts without re-rasterizing. + - Added GlyphExtraSpacing option to add extra horizontal spacing between characters (#242). + - Added MergeMode option to merge glyphs from different font inputs into a same font (#182, #232). + - Added FontDataOwnedByAtlas option to keep ownership from the TTF data buffer and request the atlas to make a copy (#220). +- Updated to stb_truetype 1.06 (+ minor mods) with better font rasterization. +- InputText: Added ImGuiInputTextFlags_NoHorizontalScroll flag. +- InputText: Added ImGuiInputTextFlags_AlwaysInsertMode flag. +- InputText: Added HasSelection() helper in ImGuiTextEditCallbackData as a clarification. +- InputText: Fix for using END key on a multi-line text editor (#275) +- Columns: Dispatch render of each column in a sub-draw list and merge on closure, saving a lot of draw calls! (#125) +- Popups: Fixed Combo boxes inside menus. (#272) +- Style: Added GrabRounding setting to make the sliders etc. grabs rounded. +- Changed SameLine() parameters from int to float. +- Fixed incorrect assert triggering when code stole ActiveID from user moving a window by calling e.g. SetKeyboardFocusHere(). +- Fixed CollapsingHeader() label rendering outside its frame in columns context where ClipRect max isn't aligned with the + right-side of the header. +- Metrics window: calculate bounding box of actual vertices when hovering a draw list. +- Examples: Showing more information in the Fonts section. +- Examples: Added a gratuitous About window. +- Examples: Updated all examples code (OpenGL/DX9/DX11/SDL/Allegro/iOS) to use indexed rendering. +- Examples: Fixed the SDL2 example to support Unicode text input (#274). + + +----------------------------------------------------------------------- + VERSION 1.42 (2015-07-08) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.42 + +Breaking Changes: + +- Renamed SetScrollPosHere() to SetScrollHere(). Kept inline redirection function (will obsolete). +- Renamed GetScrollPosY() to GetScrollY(). Necessary to reduce confusion and make scrolling API consistent, + because positions (e.g. cursor position) are not equivalent to scrolling amount. +- Removed obsolete GetDefaultFontData() function that would assert anyway. + If you are updating from <1.30 you'll get a compile error instead of an assertion. (obsoleted 2015/01/11) + +Other Changes: + +- Added SDL2 example application (courtesy of @CedricGuillemet) +- Added iOS example application (courtesy of @joeld42) +- Added Allegro 5 example application (courtesy of @bggd) +- Added TitleBgActive color in style so focused window is made visible. (#253) +- Added CaptureKeyboardFromApp() / CaptureMouseFromApp() to manually enforce inputs capturing. +- Added DragFloatRange2() DragIntRange2() helpers. (#76) +- Added a Y centering ratio to SetScrollFromCursorPos() which can be used to aim the top or bottom of the window. (#150) +- Added SetScrollY(), SetScrollFromPos(), GetCursorStartPos() for manual scrolling manipulations. (#150). +- Added GetKeyIndex() helper for converting from ImGuiKey_\* enum to user's keycodes. Basically pulls from io.KeysMap[]. +- Added missing ImGuiKey_PageUp, ImGuiKey_PageDown so more UI code can be written without referring to implementation-side keycodes. +- MenuItem() can be activated on release. (#245) +- Allowing NewFrame() with DeltaTime==0.0f to not assert. +- Fixed IsMouseDragging(). (#260) +- Fixed PlotLines(), PlotHistogram() using incorrect hovering test so they would show their tooltip even when there is + a popup between mouse and the graph. +- Fixed window padding being reported incorrectly for child windows with borders when parent have no borders. +- Fixed a bug with TextUnformatted() clipping of long text blob when clipping y1 line sits on the first line of text. (#257) +- Fixed text baseline alignment of small button (no padding) after regular buttons. +- Fixed ListBoxHeader() not honoring negative sizes the same way as BeginChild() or BeginChildFrame(). (#263) +- Fixed warnings for more pedantic compiler settings (#258). +- ImVector<> cannot be re-defined anymore, cannot be replaced with std::vector<>. Allowed us to clean up and optimize + lots of code. Yeah! (#262) +- ImDrawList: store pointer to their owner name for easier auditing/debugging. +- Examples: added scroll tracking example with SetScrollFromCursorPos(). +- Examples: metrics windows render clip rectangle when hovering over a draw call. +- Lots of small optimization (particularly to run faster on unoptimized builds) and tidying up. +- Added font links in extra_fonts/ + instructions for using compressed fonts in C array. + + +----------------------------------------------------------------------- + VERSION 1.41 (2015-06-26) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.41 + +Breaking Changes: + +- Changed ImageButton() default bg_col parameter from (0,0,0,1) (black) to (0,0,0,0) (transparent). + Only makes a difference when texture have transparency. +- Changed Selectable() API from (label, selected, size) to (label, selected, flags, size). + Size override should be used very rarely so hopefully it doesn't affect many people. Sorry! + +Other Changes: + +- Added InputTextMultiline() multi-line text editor, vertical scrolling, selection, optimized enough to handle rather + big chunks of text in stateless context (thousands of lines are ok), option for allowing Tab to be input, option + for validating with Return or Ctrl+Return (#200). +- Added modal window API, BeginPopupModal(), follows the popup api scheme. Modal windows can be closed by clicking + outside. By default the rest of the screen is dimmed (using ImGuiCol_ModalWindowDarkening). Modal windows can be stacked. +- Added GetGlyphRangesCyrillic() helper (#237). +- Added SetNextWindowPosCenter() to center a window prior to knowing its size. (#249) +- Added IsWindowHovered() helper. +- Added IsMouseReleased(), IsKeyReleased() helpers to allow to user to avoid tracking them. (#248) +- Allow Set*WindowSize() calls to be used with popups. +- Window: AutoFit can be triggered on each axis separately via SetNextWindowSize(), etc. +- Window: fixed scrolling with mouse wheel while window was collapsed. +- Window: fixed mouse wheel scroll issues. +- DragFloat(), SliderFloat(): Fixed rounding of negative numbers which sometime made the negative lower bound unreachable. +- InputText(): lifted character count limit. +- InputText(): fixes in case of using per-window font scaling. +- Selectable(), MenuItem(): do not use frame rounding for hovering/selection. +- Selectable(): Added flag ImGuiSelectableFlags_DontClosePopups. +- Selectable(): Added flag ImGuiSelectableFlags_SpanAllColumns (#125). +- Combo(): Fixed issue with activating a Combo() not taking active id (#241). +- ColorButton(), ColorEdit4(): fix to ensure that the colored square stays square when non-default padding settings are used. +- BeginChildFrame(): returns bool like BeginChild() for clipping. +- SetScrollPosHere(): takes account of item height + more accurate centering + fixed precision issue. +- ImFont: ignoring '\r'. +- ImFont: added GetCharAdvance() helper. Exposed font Ascent and font Descent. +- ImFont: additional rendering optimizations. +- Metrics windows display storage size. + + +----------------------------------------------------------------------- + VERSION 1.40 (2015-05-31) +----------------------------------------------------------------------- + +Decorated log: https://github.com/ocornut/imgui/releases/tag/v1.40 + +Breaking Changes: + +- The BeginPopup() API (introduced in 1.37) had to be changed to allow for stacked popups and menus. + Use OpenPopup() to toggle the opened state and BeginPopup() to append.** +- The third parameter of Button(), 'repeat_if_held' has been removed. While it's been very rarely used, + some code will possibly break if you didn't rely on the default parameter. + Use PushButtonRepeat()/PopButtonRepeat() to configure repeat. +- Renamed IsRectClipped() to !IsRectVisible() for consistency (opposite return value!). Kept inline redirection function (will obsolete) +- Renamed GetWindowCollapsed() to IsWindowCollapsed() for consistency. Kept inline indirection function (will obsolete). + +Other Changes: + +- Menus: Added a menu system! Menus are typically populated with menu items and sub-menus, but you can add any sort of + widgets in them (buttons, text inputs, sliders, etc.). (#126) +- Menus: Added MenuItem() to append a menu item. Optional shortcut display, acts a button & toggle with checked/unchecked state, + disabled mode. Menu items can be used in any window. +- Menus: Added BeginMenu() to append a sub-menu. Note that you generally want to add sub-menu inside a popup or a menu-bar. + They will work inside a normal window but it will be a bit unusual. +- Menus: Added BeginMenuBar() to append to window menu-bar (set ImGuiWindowFlags_MenuBar to enable). +- Menus: Added BeginMainMenuBar() helper to append to a fullscreen main menu-bar. +- Popups: Support for stacked popups. Each popup level inhibit inputs to lower levels. The menus system is based on this. (#126). +- Popups: Added BeginPopupContextItem(), BeginPopupContextWindow(), BeginPopupContextVoid() to create a popup window on mouse-click. +- Popups: Popups have borders by default (#197), attenuated border alpha in default theme. +- Popups & Tooltip: Fit within display. Handling various positioning/sizing/scrolling edge cases. Better hysteresis when moving + in corners. Tooltip always tries to stay away from mouse-cursor. +- Added ImGuiStorage::GetVoidPtrRef() for manipulating stored void*. +- Added IsKeyDown() IsMouseDown() as convenience and for consistency with existing functions (instead of reading them from IO structures). +- Added Dummy() helper to advance layout by a given size. Unlike InvisibleButton() this doesn't catch any click. +- Added configurable io.KeyRepeatDelay, io.KeyRepeatRate keyboard and mouse repeat rate. +- Added PushButtonRepeat() / PopButtonRepeat() to enable hold-button-to-repeat press on any button. +- Removed the third 'repeat' parameter of Button(). +- Added IsAnyItemHovered() helper. +- Added GetItemsLineHeightWithSpacing() helper. +- Added ImGuiListClipper helper for clipping large list of evenly sized items, to avoid using CalcListClipping() directly. +- Separator: within group start on group horizontal offset. (#205) +- InputText: Fixed incorrect edit state after text buffer is appended to by user via the callback. (#206) +- InputText: CTRL+letter-key shortcuts (e.g. CTRL+C/V/X) makes sure only CTRL is pressed. (#214) +- InputText: Fixed cursor generating a zero-width wire-frame rectangle turning into a division by zero (would go unnoticed + unless you trapped exceptions). +- InputFloatN/InputIntN: Flags parameter added to match scalar versions. (#218) +- Selectable: Horizontal filling not declared to ItemSize() so Selectable(),SameLine() works and we can better auto-fit the window. +- Selectable: Handling text baseline alignment for line that aren't of text height. +- Combo: Empty label doesn't add ItemInnerSpacing alignment, matching other widgets. +- EndGroup: Carries the text base offset from the last line of the group (sort of incorrect but better than nothing, + should use the first line of the group, will implement in the future). +- Columns: distinguish columns-set ID from other widgets as a convenience, added asserts and sailors. +- ListBox: ListBox() function only use public API to encourage creating custom versions. ListBoxHeader() can return false. +- ListBox: Uses ImGuiListClipper and assume items of matching height, so large lists can be handled. +- Plot: overlay label clipped within frame when not fitting. +- Window: Added ImGuiSetCond_Appearing to test the hidden->visible transition in SetWindow***/SetNextWindow*** functions. +- Window: Auto-fitting cancel out one worth of vertical spacing for vertical symmetry (like what group and tooltip do). +- Window: Default item width for auto-resizing windows expressed as a factor of font height, scales better with different font. +- Window: Fixed auto-fit calculation mismatch of whether a scrollbar will be added by maximum height clamping. Also honor NoScrollBar in the case of height clamping, not adding extra horizontal space. +- Window: Hovering require to hover same child window. Reverted 860cf57 (December 3). Might break something if you have + child overlapping items in parent window. +- Window: Fixed appending multiple times to an existing child via multiple BeginChild/EndChild calls to same child name. + Allows a simple form of out-of-order appending. +- Window: Fixed auto-filling child window using WindowMinSize at their minimum size, irrelevant. +- Metrics: Added io.MetricsActiveWindows counter. (#213. +- Metrics: Added io.MetricsAllocs counter (number of active memory allocations). +- Metrics: ShowMetricsWindow() shows popups stack, allocations. +- Style: Added style.DisplayWindowPadding to prevent windows from reaching edges of display (similar to style.DisplaySafeAreaPadding which is still in effect and also affect popups/tooltips). +- Style: Removed style.AutoFitPadding, using style.WindowPadding makes more sense (the default values were already the same). +- Style: Added style.ScrollbarRounding. (#212) +- Style: Added ImGuiCol_TextDisabled for disabled text. Added TextDisabled() helper. +- Style: Added style.WindowTitleAlign alignment options, to e.g. center title on windows. (#222) +- ImVector: tweak growth strategy, matches vector from VS2010. +- ImFontAtlas: Added ClearFonts(), making the different clear funcs more explicit. (#224) +- ImFontAtlas: Fixed appending new fonts without clearing existing fonts. Clearing input data left to application. (#224) +- ImDrawList: Merge draw command better, cases of multiple Begin/End gets merged properly. +- Store common stacked settings contiguously in memory to avoid heap allocation for unused features, and reduce cache misses. +- Shutdown() tests for g.IO.Fonts not being NULL to ease use of multiple ImGui contexts. (#207) +- Added IMGUI_DISABLE_OBSOLETE_FUNCTIONS define to disable the functions that are meant to be removed. +- Examples: Added ? marks with tooltips next to various widgets. Added more comments in the demo window. +- Examples: Added Menu-bar example. +- Examples: Added Simple Layout example. +- Examples: AutoResize demo doesn't use TextWrapped(). +- Examples: Console example uses standard malloc/free, makes more sense as a copy & pastable example. +- Examples: DirectX9/11: Fixed key mapping for down arrow. +- Examples: DirectX9/11: hide OS cursor if ImGui is drawing it. (#155) +- Examples: DirectX11: explicitly set rasterizer state. +- Examples: OpenGL3: Add conditional compilation of forward compat as required by glfw on OSX. (#229) +- Fixed build with Visual Studio 2008 (possibly earlier versions as well). +- Other fixes, comments, tweaks. + + +----------------------------------------------------------------------- + +For older version, see https://github.com/ocornut/imgui/releases + + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Docs/EXAMPLES.md`: + +```md +_(You may browse this at https://github.com/ocornut/imgui/blob/master/docs/EXAMPLES.md or view this file with any Markdown viewer)_ + +## Dear ImGui: Examples + +**The [examples/](https://github.com/ocornut/imgui/blob/master/examples) folder example applications (standalone, ready-to-build) for variety of +platforms and graphics APIs.** They all use standard backends from the [backends/](https://github.com/ocornut/imgui/blob/master/backends) folder (see [BACKENDS.md](https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md)). + +The purpose of Examples is to showcase integration with backends, let you try Dear ImGui, and guide you toward +integrating Dear ImGui in your own application/game/engine. +**Once Dear ImGui is setup and running, run and refer to `ImGui::ShowDemoWindow()` in imgui_demo.cpp for usage of the end-user API.** + +You can find Windows binaries for some of those example applications at: + http://www.dearimgui.org/binaries + + +### Getting Started + +Integration in a typical existing application, should take <20 lines when using standard backends. + + At initialization: + call ImGui::CreateContext() + call ImGui_ImplXXXX_Init() for each backend. + + At the beginning of your frame: + call ImGui_ImplXXXX_NewFrame() for each backend. + call ImGui::NewFrame() + + At the end of your frame: + call ImGui::Render() + call ImGui_ImplXXXX_RenderDrawData() for your Renderer backend. + + At shutdown: + call ImGui_ImplXXXX_Shutdown() for each backend. + call ImGui::DestroyContext() + +Example (using [backends/imgui_impl_win32.cpp](https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_win32.cpp) + [backends/imgui_impl_dx11.cpp](https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_dx11.cpp)): + + // Create a Dear ImGui context, setup some options + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable some options + + // Initialize Platform + Renderer backends (here: using imgui_impl_win32.cpp + imgui_impl_dx11.cpp) + ImGui_ImplWin32_Init(my_hwnd); + ImGui_ImplDX11_Init(my_d3d_device, my_d3d_device_context); + + // Application main loop + while (true) + { + // Beginning of frame: update Renderer + Platform backend, start Dear ImGui frame + ImGui_ImplDX11_NewFrame(); + ImGui_ImplWin32_NewFrame(); + ImGui::NewFrame(); + + // Any application code here + ImGui::Text("Hello, world!"); + + // End of frame: render Dear ImGui + ImGui::Render(); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + + // Swap + g_pSwapChain->Present(1, 0); + } + + // Shutdown + ImGui_ImplDX11_Shutdown(); + ImGui_ImplWin32_Shutdown(); + ImGui::DestroyContext(); + +Please read 'PROGRAMMER GUIDE' in imgui.cpp for notes on how to setup Dear ImGui in your codebase. +Please read the comments and instruction at the top of each file. +Please read FAQ at http://www.dearimgui.org/faq + +If you are using any of the backends provided here, you can add the backends/imgui_impl_xxxx(.cpp,.h) +files to your project and use as-in. Each imgui_impl_xxxx.cpp file comes with its own individual +Changelog, so if you want to update them later it will be easier to catch up with what changed. + + +### Examples Applications + +[example_allegro5/](https://github.com/ocornut/imgui/blob/master/examples/example_allegro5/)
+Allegro 5 example.
+= main.cpp + imgui_impl_allegro5.cpp + +[example_android_opengl3/](https://github.com/ocornut/imgui/blob/master/examples/example_android_opengl3/)
+Android + OpenGL3 (ES) example.
+= main.cpp + imgui_impl_android.cpp + imgui_impl_opengl3.cpp + +[example_apple_metal/](https://github.com/ocornut/imgui/blob/master/examples/example_metal/)
+OSX & iOS + Metal example.
+= main.m + imgui_impl_osx.mm + imgui_impl_metal.mm
+It is based on the "cross-platform" game template provided with Xcode as of Xcode 9. +(NB: imgui_impl_osx.mm is currently not as feature complete as other platforms backends. +You may prefer to use the GLFW Or SDL backends, which will also support Windows and Linux.) + +[example_apple_opengl2/](https://github.com/ocornut/imgui/blob/master/examples/example_apple_opengl2/)
+OSX + OpenGL2 example.
+= main.mm + imgui_impl_osx.mm + imgui_impl_opengl2.cpp
+(NB: imgui_impl_osx.mm is currently not as feature complete as other platforms backends. + You may prefer to use the GLFW Or SDL backends, which will also support Windows and Linux.) + +[example_emscripten_opengl3/](https://github.com/ocornut/imgui/blob/master/examples/example_emscripten_opengl3/)
+Emcripten + SDL2 + OpenGL3+/ES2/ES3 example.
+= main.cpp + imgui_impl_sdl.cpp + imgui_impl_opengl3.cpp
+Note that other examples based on SDL or GLFW + OpenGL could easily be modified to work with Emscripten. +We provide this to make the Emscripten differences obvious, and have them not pollute all other examples. + +[example_emscripten_wgpu/](https://github.com/ocornut/imgui/blob/master/examples/example_emscripten_wgpu/)
+Emcripten + GLFW + WebGPU example.
+= main.cpp + imgui_impl_glfw.cpp + imgui_impl_wgpu.cpp + +[example_glfw_metal/](https://github.com/ocornut/imgui/blob/master/examples/example_glfw_metal/)
+GLFW (Mac) + Metal example.
+= main.mm + imgui_impl_glfw.cpp + imgui_impl_metal.mm + +[example_glfw_opengl2/](https://github.com/ocornut/imgui/blob/master/examples/example_glfw_opengl2/)
+GLFW + OpenGL2 example (legacy, fixed pipeline).
+= main.cpp + imgui_impl_glfw.cpp + imgui_impl_opengl2.cpp
+**DO NOT USE THIS IF YOUR CODE/ENGINE IS USING MODERN OPENGL (SHADERS, VBO, VAO, etc.)**
+This code is mostly provided as a reference to learn about Dear ImGui integration, because it is shorter. +If your code is using GL3+ context or any semi modern OpenGL calls, using this renderer is likely to +make things more complicated, will require your code to reset many OpenGL attributes to their initial +state, and might confuse your GPU driver. One star, not recommended. + +[example_glfw_opengl3/](https://github.com/ocornut/imgui/blob/master/examples/example_glfw_opengl3/)
+GLFW (Win32, Mac, Linux) + OpenGL3+/ES2/ES3 example (modern, programmable pipeline).
+= main.cpp + imgui_impl_glfw.cpp + imgui_impl_opengl3.cpp
+This uses more modern OpenGL calls and custom shaders.
+This may actually also work with OpenGL 2.x contexts!
+Prefer using that if you are using modern OpenGL in your application (anything with shaders). + +[example_glfw_vulkan/](https://github.com/ocornut/imgui/blob/master/examples/example_glfw_vulkan/)
+GLFW (Win32, Mac, Linux) + Vulkan example.
+= main.cpp + imgui_impl_glfw.cpp + imgui_impl_vulkan.cpp
+This is quite long and tedious, because: Vulkan. +For this example, the main.cpp file exceptionally use helpers function from imgui_impl_vulkan.h/cpp. + +[example_glut_opengl2/](https://github.com/ocornut/imgui/blob/master/examples/example_glut_opengl2/)
+GLUT (e.g., FreeGLUT on Linux/Windows, GLUT framework on OSX) + OpenGL2 example.
+= main.cpp + imgui_impl_glut.cpp + imgui_impl_opengl2.cpp
+Note that GLUT/FreeGLUT is largely obsolete software, prefer using GLFW or SDL. + +[example_null/](https://github.com/ocornut/imgui/blob/master/examples/example_null/)
+Null example, compile and link imgui, create context, run headless with no inputs and no graphics output.
+= main.cpp
+This is used to quickly test compilation of core imgui files in as many setups as possible. +Because this application doesn't create a window nor a graphic context, there's no graphics output. + +[example_sdl_directx11/](https://github.com/ocornut/imgui/blob/master/examples/example_sdl_directx11/)
+SDL2 + DirectX11 example, Windows only.
+= main.cpp + imgui_impl_sdl.cpp + imgui_impl_dx11.cpp
+This to demonstrate usage of DirectX with SDL. + +[example_sdl_metal/](https://github.com/ocornut/imgui/blob/master/examples/example_sdl_metal/)
+SDL2 (Mac) + Metal example.
+= main.mm + imgui_impl_sdl.cpp + imgui_impl_metal.mm + +[example_sdl_opengl2/](https://github.com/ocornut/imgui/blob/master/examples/example_sdl_opengl2/)
+SDL2 (Win32, Mac, Linux etc.) + OpenGL example (legacy, fixed pipeline).
+= main.cpp + imgui_impl_sdl.cpp + imgui_impl_opengl2.cpp
+**DO NOT USE OPENGL2 CODE IF YOUR CODE/ENGINE IS USING MODERN OPENGL (SHADERS, VBO, VAO, etc.)**
+This code is mostly provided as a reference to learn about Dear ImGui integration, because it is shorter. +If your code is using GL3+ context or any semi modern OpenGL calls, using this renderer is likely to +make things more complicated, will require your code to reset many OpenGL attributes to their initial +state, and might confuse your GPU driver. One star, not recommended. + +[example_sdl_opengl3/](https://github.com/ocornut/imgui/blob/master/examples/example_sdl_opengl3/)
+SDL2 (Win32, Mac, Linux, etc.) + OpenGL3+/ES2/ES3 example.
+= main.cpp + imgui_impl_sdl.cpp + imgui_impl_opengl3.cpp
+This uses more modern OpenGL calls and custom shaders.
+This may actually also work with OpenGL 2.x contexts!
+ +[example_sdl_sdlrenderer/](https://github.com/ocornut/imgui/blob/master/examples/example_sdl_sdlrenderer/)
+SDL2 (Win32, Mac, Linux, etc.) + SDL_Renderer (most graphics backends are supported underneath)
+= main.cpp + imgui_impl_sdl.cpp + imgui_impl_sdlrenderer.cpp
+This requires SDL 2.0.17+ (expected to release November 2021)
+We do not really recommend using SDL_Renderer as it is a rather primitive API. + +[example_sdl_vulkan/](https://github.com/ocornut/imgui/blob/master/examples/example_sdl_vulkan/)
+SDL2 (Win32, Mac, Linux, etc.) + Vulkan example.
+= main.cpp + imgui_impl_sdl.cpp + imgui_impl_vulkan.cpp
+This is quite long and tedious, because: Vulkan.
+For this example, the main.cpp file exceptionally use helpers function from imgui_impl_vulkan.h/cpp. + +[example_win32_directx9/](https://github.com/ocornut/imgui/blob/master/examples/example_win32_directx9/)
+DirectX9 example, Windows only.
+= main.cpp + imgui_impl_win32.cpp + imgui_impl_dx9.cpp + +[example_win32_directx10/](https://github.com/ocornut/imgui/blob/master/examples/example_win32_directx10/)
+DirectX10 example, Windows only.
+= main.cpp + imgui_impl_win32.cpp + imgui_impl_dx10.cpp + +[example_win32_directx11/](https://github.com/ocornut/imgui/blob/master/examples/example_win32_directx11/)
+DirectX11 example, Windows only.
+= main.cpp + imgui_impl_win32.cpp + imgui_impl_dx11.cpp + +[example_win32_directx12/](https://github.com/ocornut/imgui/blob/master/examples/example_win32_directx12/)
+DirectX12 example, Windows only.
+= main.cpp + imgui_impl_win32.cpp + imgui_impl_dx12.cpp
+This is quite long and tedious, because: DirectX12. + + +### Miscallaneous + +**Building** + +Unfortunately nowadays it is still tedious to create and maintain portable build files using external +libraries (the kind we're using here to create a window and render 3D triangles) without relying on +third party software and build systems. For most examples here we choose to provide: + - Makefiles for Linux/OSX + - Batch files for Visual Studio 2008+ + - A .sln project file for Visual Studio 2012+ + - Xcode project files for the Apple examples +Please let us know if they don't work with your setup! +You can probably just import the imgui_impl_xxx.cpp/.h files into your own codebase or compile those +directly with a command-line compiler. + +If you are interested in using Cmake to build and links examples, see: + https://github.com/ocornut/imgui/pull/1713 and https://github.com/ocornut/imgui/pull/3027 + +**About mouse cursor latency** + +Dear ImGui has no particular extra lag for most behaviors, +e.g. the last value passed to 'io.AddMousePosEvent()' before NewFrame() will result in windows being moved +to the right spot at the time of EndFrame()/Render(). At 60 FPS your experience should be pleasant. + +However, consider that OS mouse cursors are typically drawn through a very specific hardware accelerated +path and will feel smoother than the majority of contents rendered via regular graphics API (including, +but not limited to Dear ImGui windows). Because UI rendering and interaction happens on the same plane +as the mouse, that disconnect may be jarring to particularly sensitive users. +You may experiment with enabling the io.MouseDrawCursor flag to request Dear ImGui to draw a mouse cursor +using the regular graphics API, to help you visualize the difference between a "hardware" cursor and a +regularly rendered software cursor. +However, rendering a mouse cursor at 60 FPS will feel sluggish so you likely won't want to enable that at +all times. It might be beneficial for the user experience to switch to a software rendered cursor _only_ +when an interactive drag is in progress. + +Note that some setup or GPU drivers are likely to be causing extra display lag depending on their settings. +If you feel that dragging windows feels laggy and you are not sure what the cause is: try to build a simple +drawing a flat 2D shape directly under the mouse cursor! + + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Docs/FAQ.md`: + +```md +# FAQ (Frequenty Asked Questions) + +You may link to this document using short form: + https://www.dearimgui.org/faq +or its real address: + https://github.com/ocornut/imgui/blob/master/docs/FAQ.md +or view this file with any Markdown viewer. + + +## Index + +| **Q&A: Basics** | +:---------------------------------------------------------- | +| [Where is the documentation?](#q-where-is-the-documentation) | +| [What is this library called?](#q-what-is-this-library-called) | +| [Which version should I get?](#q-which-version-should-i-get) | +| **Q&A: Integration** | +| **[How to get started?](#q-how-to-get-started)** | +| **[How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?](#q-how-can-i-tell-whether-to-dispatch-mousekeyboard-to-dear-imgui-or-my-application)** | +| [How can I enable keyboard or gamepad controls?](#q-how-can-i-enable-keyboard-or-gamepad-controls) | +| [How can I use this on a machine without mouse, keyboard or screen? (input share, remote display)](#q-how-can-i-use-this-on-a-machine-without-mouse-keyboard-or-screen-input-share-remote-display) | +| [I integrated Dear ImGui in my engine and little squares are showing instead of text...](#q-i-integrated-dear-imgui-in-my-engine-and-little-squares-are-showing-instead-of-text) | +| [I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around...](#q-i-integrated-dear-imgui-in-my-engine-and-some-elements-are-clipping-or-disappearing-when-i-move-windows-around) | +| [I integrated Dear ImGui in my engine and some elements are displaying outside their expected windows boundaries...](#q-i-integrated-dear-imgui-in-my-engine-and-some-elements-are-displaying-outside-their-expected-windows-boundaries) | +| **Q&A: Usage** | +| **[About the ID Stack system..
Why is my widget not reacting when I click on it?
How can I have widgets with an empty label?
How can I have multiple widgets with the same label?
How can I have multiple windows with the same label?](#q-about-the-id-stack-system)** | +| [How can I display an image? What is ImTextureID, how does it work?](#q-how-can-i-display-an-image-what-is-imtextureid-how-does-it-work)| +| [How can I use my own math types instead of ImVec2/ImVec4?](#q-how-can-i-use-my-own-math-types-instead-of-imvec2imvec4) | +| [How can I interact with standard C++ types (such as std::string and std::vector)?](#q-how-can-i-interact-with-standard-c-types-such-as-stdstring-and-stdvector) | +| [How can I display custom shapes? (using low-level ImDrawList API)](#q-how-can-i-display-custom-shapes-using-low-level-imdrawlist-api) | +| **Q&A: Fonts, Text** | +| [How should I handle DPI in my application?](#q-how-should-i-handle-dpi-in-my-application) | +| [How can I load a different font than the default?](#q-how-can-i-load-a-different-font-than-the-default) | +| [How can I easily use icons in my application?](#q-how-can-i-easily-use-icons-in-my-application) | +| [How can I load multiple fonts?](#q-how-can-i-load-multiple-fonts) | +| [How can I display and input non-Latin characters such as Chinese, Japanese, Korean, Cyrillic?](#q-how-can-i-display-and-input-non-latin-characters-such-as-chinese-japanese-korean-cyrillic) | +| **Q&A: Concerns** | +| [Who uses Dear ImGui?](#q-who-uses-dear-imgui) | +| [Can you create elaborate/serious tools with Dear ImGui?](#q-can-you-create-elaborateserious-tools-with-dear-imgui) | +| [Can you reskin the look of Dear ImGui?](#q-can-you-reskin-the-look-of-dear-imgui) | +| [Why using C++ (as opposed to C)?](#q-why-using-c-as-opposed-to-c) | +| **Q&A: Community** | +| [How can I help?](#q-how-can-i-help) | + + +# Q&A: Basics + +### Q: Where is the documentation? + +**This library is poorly documented at the moment and expects the user to be acquainted with C/C++.** +- The [Wiki](https://github.com/ocornut/imgui/wiki) is a hub to many resources and links. +- Dozens of standalone example applications using e.g. OpenGL/DirectX are provided in the [examples/](https://github.com/ocornut/imgui/blob/master/examples/) folder to explain how to integrate Dear ImGui with your own engine/application. You can run those applications and explore them. +- See demo code in [imgui_demo.cpp](https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp) and particularly the `ImGui::ShowDemoWindow()` function. The demo covers most features of Dear ImGui, so you can read the code and see its output. +- See documentation: [Backends](https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md), [Examples](https://github.com/ocornut/imgui/blob/master/docs/EXAMPLES.md), [Fonts](https://github.com/ocornut/imgui/blob/master/docs/FONTS.md). +- See documentation and comments at the top of [imgui.cpp](https://github.com/ocornut/imgui/blob/master/imgui.cpp) + general API comments in [imgui.h](https://github.com/ocornut/imgui/blob/master/imgui.h). +- The [Glossary](https://github.com/ocornut/imgui/wiki/Glossary) page may be useful. +- The [Issues](https://github.com/ocornut/imgui/issues) and [Discussions](https://github.com/ocornut/imgui/discussions) sections can be searched for past questions and issues. +- Your programming IDE is your friend, find the type or function declaration to find comments associated with it. +- The `ImGui::ShowMetricsWindow()` function exposes lots of internal information and tools. Although it is primary designed as a debugging tool, having access to that information tends to help understands concepts. + +##### [Return to Index](#index) + +--- + +### Q. What is this library called? + +**This library is called Dear ImGui**. Please refer to it as Dear ImGui (not ImGui, not IMGUI). + +(The library misleadingly started its life in 2014 as "ImGui" due to the fact that I didn't give it a proper name when I released 1.0, and had no particular expectation that it would take off. However, the term IMGUI (immediate-mode graphical user interface) was coined before and is being used in variety of other situations e.g. Unity uses it own implementation of the IMGUI paradigm. To reduce the ambiguity without affecting existing code bases, I have decided in December 2015 a fully qualified name "Dear ImGui" for this library. + +##### [Return to Index](#index) + +--- + +### Q: Which version should I get? +I occasionally tag [Releases](https://github.com/ocornut/imgui/releases) but it is generally safe and recommended to sync to master/latest. The library is fairly stable and regressions tend to be fixed fast when reported. + +You may use the [docking](https://github.com/ocornut/imgui/tree/docking) branch which includes: +- [Docking features](https://github.com/ocornut/imgui/issues/2109) +- [Multi-viewport features](https://github.com/ocornut/imgui/issues/1542) + +Many projects are using this branch and it is kept in sync with master regularly. + +##### [Return to Index](#index) + +---- + +# Q&A: Integration + +### Q: How to get started? + +Read [EXAMPLES.md](https://github.com/ocornut/imgui/blob/master/docs/EXAMPLES.md).
+Read [BACKENDS.md](https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md).
+Read `PROGRAMMER GUIDE` section of [imgui.cpp](https://github.com/ocornut/imgui/blob/master/imgui.cpp).
+The [Wiki](https://github.com/ocornut/imgui/wiki) is a hub to many resources and links. + +For first-time users having issues compiling/linking/running or issues loading fonts, please use [GitHub Discussions](https://github.com/ocornut/imgui/discussions). + +##### [Return to Index](#index) + +--- + +### Q: How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application? + +You can read the `io.WantCaptureMouse`, `io.WantCaptureKeyboard` and `io.WantTextInput` flags from the ImGuiIO structure. +- When `io.WantCaptureMouse` is set, you need to discard/hide the mouse inputs from your underlying application. +- When `io.WantCaptureKeyboard` is set, you need to discard/hide the keyboard inputs from your underlying application. +- When `io.WantTextInput` is set, you can notify your OS/engine to popup an on-screen keyboard, if available (e.g. on a mobile phone, or console OS). + +Important: you should always pass your mouse/keyboard inputs to Dear ImGui, regardless of the value `io.WantCaptureMouse`/`io.WantCaptureKeyboard`. This is because e.g. we need to detect that you clicked in the void to unfocus its own windows, and other reasons. + +```cpp +void MyLowLevelMouseButtonHandler(int button, bool down) +{ + // (1) ALWAYS forward mouse data to ImGui! This is automatic with default backends. With your own backend: + ImGuiIO& io = ImGui::GetIO(); + io.AddMouseButtonEvent(button, down); + + // (2) ONLY forward mouse data to your underlying app/game. + if (!io.WantCaptureMouse) + my_game->HandleMouseData(...); +} +``` + + +**Note:** The `io.WantCaptureMouse` is more correct that any manual attempt to "check if the mouse is hovering a window" (don't do that!). It handle mouse dragging correctly (both dragging that started over your application or over a Dear ImGui window) and handle e.g. popup and modal windows blocking inputs. + +**Note:** Those flags are updated by `ImGui::NewFrame()`. However it is generally more correct and easier that you poll flags from the previous frame, then submit your inputs, then call `NewFrame()`. If you attempt to do the opposite (which is generally harder) you are likely going to submit your inputs after `NewFrame()`, and therefore too late. + +**Note:** If you are using a touch device, you may find use for an early call to `UpdateHoveredWindowAndCaptureFlags()` to correctly dispatch your initial touch. We will work on better out-of-the-box touch support in the future. + +**Note:** Text input widget releases focus on the "KeyDown" event of the Return key, so the subsequent "KeyUp" event that your application receive will typically have `io.WantCaptureKeyboard == false`. Depending on your application logic it may or not be inconvenient to receive that KeyUp event. You might want to track which key-downs were targeted for Dear ImGui, e.g. with an array of bool, and filter out the corresponding key-ups.) + +##### [Return to Index](#index) + +--- + +### Q: How can I enable keyboard or gamepad controls? +- The gamepad/keyboard navigation is fairly functional and keeps being improved. The initial focus was to support game controllers, but keyboard is becoming increasingly and decently usable. Gamepad support is particularly useful to use Dear ImGui on a game console (e.g. PS4, Switch, XB1) without a mouse connected! +- Keyboard: set `io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard` to enable. +- Gamepad: set `io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad` to enable (with a supporting backend). +- See [Control Sheets for Gamepads](http://www.dearimgui.org/controls_sheets) (reference PNG/PSD for PS4, XB1, Switch gamepads). +- See `USING GAMEPAD/KEYBOARD NAVIGATION CONTROLS` section of [imgui.cpp](https://github.com/ocornut/imgui/blob/master/imgui.cpp) for more details. + +##### [Return to Index](#index) + +--- + +### Q: How can I use this on a machine without mouse, keyboard or screen? (input share, remote display) +- You can share your computer mouse seamlessly with your console/tablet/phone using solutions such as [Synergy](https://symless.com/synergy) +This is the preferred solution for developer productivity. +In particular, the [micro-synergy-client repository](https://github.com/symless/micro-synergy-client) has simple +and portable source code (uSynergy.c/.h) for a small embeddable client that you can use on any platform to connect +to your host computer, based on the Synergy 1.x protocol. Make sure you download the Synergy 1 server on your computer. +Console SDK also sometimes provide equivalent tooling or wrapper for Synergy-like protocols. +- Game console users: consider emulating a mouse cursor with DualShock4 touch pad or a spare analog stick as a mouse-emulation fallback. +- You may also use a third party solution such as [netImgui](https://github.com/sammyfreg/netImgui), [Remote ImGui](https://github.com/JordiRos/remoteimgui) or [imgui-ws](https://github.com/ggerganov/imgui-ws) which sends the vertices to render over the local network, allowing you to use Dear ImGui even on a screen-less machine. See [Wiki](https://github.com/ocornut/imgui/wiki) index for most details. +- For touch inputs, you can increase the hit box of widgets (via the `style.TouchPadding` setting) to accommodate for the lack of precision of touch inputs, but it is recommended you use a mouse or gamepad to allow optimizing for screen real-estate and precision. + +##### [Return to Index](#index) + +--- + +### Q: I integrated Dear ImGui in my engine and little squares are showing instead of text... +Your renderer is not using the font texture correctly or it hasn't been uploaded to the GPU. +- If this happens using the standard backends: A) have you modified the font atlas after `ImGui_ImplXXX_NewFrame()`? B) maybe the texture failed to upload, which could happens if for some reason your texture is too big. Also see [docs/FONTS.md](https://github.com/ocornut/imgui/blob/master/docs/FONTS.md). +- If this happens with a custom backend: make sure you have uploaded the font texture to the GPU, that all shaders are rendering states are setup properly (e.g. texture is bound). Compare your code to existing backends and use a graphics debugger such as [RenderDoc](https://renderdoc.org) to debug your rendering states. + +##### [Return to Index](#index) + +--- + +### Q: I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around... +### Q: I integrated Dear ImGui in my engine and some elements are displaying outside their expected windows boundaries... +You are probably mishandling the clipping rectangles in your render function. +Each draw command needs the triangle rendered using the clipping rectangle provided in the ImDrawCmd structure (`ImDrawCmd->CllipRect`). +Rectangles provided by Dear ImGui are defined as +`(x1=left,y1=top,x2=right,y2=bottom)` +and **NOT** as +`(x1,y1,width,height)` +Refer to rendering backends in the [examples/](https://github.com/ocornut/imgui/tree/master/examples) folder for references of how to handle the `ClipRect` field. + +##### [Return to Index](#index) + +--- + +# Q&A: Usage + +### Q: About the ID Stack system... +### Q: Why is my widget not reacting when I click on it? +### Q: How can I have widgets with an empty label? +### Q: How can I have multiple widgets with the same label? +### Q: How can I have multiple windows with the same label? + +A primer on labels and the ID Stack... + +Dear ImGui internally needs to uniquely identify UI elements. +Elements that are typically not clickable (such as calls to the Text functions) don't need an ID. +Interactive widgets (such as calls to Button buttons) need a unique ID. + +**Unique ID are used internally to track active widgets and occasionally associate state to widgets.
+Unique ID are implicitly built from the hash of multiple elements that identify the "path" to the UI element.** + +Since Dear ImGui 1.85 you can use `Demo>Tools>Stack Tool` or call `ImGui::ShowStackToolWindow()`. The tool display intermediate values leading to the creation of a unique ID, making things easier to debug and understand. + +![Stack tool](https://user-images.githubusercontent.com/8225057/136235657-a0ea5665-dcd1-423f-9be6-dc3f8ced8f12.png) + +- Unique ID are often derived from a string label and at minimum scoped within their host window: +```cpp +Begin("MyWindow"); +Button("OK"); // Label = "OK", ID = hash of ("MyWindow", "OK") +Button("Cancel"); // Label = "Cancel", ID = hash of ("MyWindow", "Cancel") +End(); +``` +- Other elements such as tree nodes, etc. also pushes to the ID stack: +```cpp +Begin("MyWindow"); +if (TreeNode("MyTreeNode")) +{ + Button("OK"); // Label = "OK", ID = hash of ("MyWindow", "MyTreeNode", "OK") + TreePop(); +} +End(); +``` +- Two items labeled "OK" in different windows or different tree locations won't collide: +```cpp +Begin("MyFirstWindow"); +Button("OK"); // Label = "OK", ID = hash of ("MyFirstWindow", "OK") +End(); +Begin("MyOtherWindow"); +Button("OK"); // Label = "OK", ID = hash of ("MyOtherWindow", "OK") +End(); +``` + +- If you have a same ID twice in the same location, you'll have a conflict: +```cpp +Begin("MyWindow"); +Button("OK"); +Button("OK"); // ERROR: ID collision with the first button! Interacting with either button will trigger the first one. +Button(""); // ERROR: ID collision with Begin("MyWindow")! +End(); +``` +Fear not! this is easy to solve and there are many ways to solve it! + +- Solving ID conflict in a simple/local context: +When passing a label you can optionally specify extra ID information within string itself. +Use "##" to pass a complement to the ID that won't be visible to the end-user. +This helps solving the simple collision cases when you know e.g. at compilation time which items +are going to be created: +```cpp +Begin("MyWindow"); +Button("Play"); // Label = "Play", ID = hash of ("MyWindow", "Play") +Button("Play##foo1"); // Label = "Play", ID = hash of ("MyWindow", "Play##foo1") // Different from other buttons +Button("Play##foo2"); // Label = "Play", ID = hash of ("MyWindow", "Play##foo2") // Different from other buttons +Button("##foo"); // Label = "", ID = hash of ("MyWindow", "##foo") // Different from window +End(); +``` +- If you want to completely hide the label, but still need an ID: +```cpp +Checkbox("##On", &b); // Label = "", ID = hash of (..., "##On") // No visible label, just a checkbox! +``` +- Occasionally/rarely you might want change a label while preserving a constant ID. This allows +you to animate labels. For example you may want to include varying information in a window title bar, +but windows are uniquely identified by their ID. Use "###" to pass a label that isn't part of ID: +```cpp +Button("Hello###ID"); // Label = "Hello", ID = hash of (..., "###ID") +Button("World###ID"); // Label = "World", ID = hash of (..., "###ID") // Same ID, different label + +sprintf(buf, "My game (%f FPS)###MyGame", fps); +Begin(buf); // Variable title, ID = hash of "MyGame" +``` +- Solving ID conflict in a more general manner: +Use `PushID()` / `PopID()` to create scopes and manipulate the ID stack, as to avoid ID conflicts +within the same window. This is the most convenient way of distinguishing ID when iterating and +creating many UI elements programmatically. +You can push a pointer, a string or an integer value into the ID stack. +Remember that ID are formed from the concatenation of _everything_ pushed into the ID stack. +At each level of the stack we store the seed used for items at this level of the ID stack. +```cpp +Begin("Window"); +for (int i = 0; i < 100; i++) +{ + PushID(i); // Push i to the id tack + Button("Click"); // Label = "Click", ID = hash of ("Window", i, "Click") + PopID(); +} +for (int i = 0; i < 100; i++) +{ + MyObject* obj = Objects[i]; + PushID(obj); + Button("Click"); // Label = "Click", ID = hash of ("Window", obj pointer, "Click") + PopID(); +} +for (int i = 0; i < 100; i++) +{ + MyObject* obj = Objects[i]; + PushID(obj->Name); + Button("Click"); // Label = "Click", ID = hash of ("Window", obj->Name, "Click") + PopID(); +} +End(); +``` +- You can stack multiple prefixes into the ID stack: +```cpp +Button("Click"); // Label = "Click", ID = hash of (..., "Click") +PushID("node"); + Button("Click"); // Label = "Click", ID = hash of (..., "node", "Click") + PushID(my_ptr); + Button("Click"); // Label = "Click", ID = hash of (..., "node", my_ptr, "Click") + PopID(); +PopID(); +``` +- Tree nodes implicitly creates a scope for you by calling `PushID()`: +```cpp +Button("Click"); // Label = "Click", ID = hash of (..., "Click") +if (TreeNode("node")) // <-- this function call will do a PushID() for you (unless instructed not to, with a special flag) +{ + Button("Click"); // Label = "Click", ID = hash of (..., "node", "Click") + TreePop(); +} +``` + +When working with trees, ID are used to preserve the open/close state of each tree node. +Depending on your use cases you may want to use strings, indices or pointers as ID. +- e.g. when following a single pointer that may change over time, using a static string as ID +will preserve your node open/closed state when the targeted object change. +- e.g. when displaying a list of objects, using indices or pointers as ID will preserve the +node open/closed state differently. See what makes more sense in your situation! + +##### [Return to Index](#index) + +--- + +### Q: How can I display an image? What is ImTextureID, how does it work? + +Short explanation: +- Refer to [Image Loading and Displaying Examples](https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples) on the [Wiki](https://github.com/ocornut/imgui/wiki). +- You may use functions such as `ImGui::Image()`, `ImGui::ImageButton()` or lower-level `ImDrawList::AddImage()` to emit draw calls that will use your own textures. +- Actual textures are identified in a way that is up to the user/engine. Those identifiers are stored and passed as ImTextureID (void*) value. +- Loading image files from the disk and turning them into a texture is not within the scope of Dear ImGui (for a good reason). + +**Please read documentations or tutorials on your graphics API to understand how to display textures on the screen before moving onward.** + +Long explanation: +- Dear ImGui's job is to create "meshes", defined in a renderer-agnostic format made of draw commands and vertices. At the end of the frame those meshes (ImDrawList) will be displayed by your rendering function. They are made up of textured polygons and the code to render them is generally fairly short (a few dozen lines). In the examples/ folder we provide functions for popular graphics API (OpenGL, DirectX, etc.). +- Each rendering function decides on a data type to represent "textures". The concept of what is a "texture" is entirely tied to your underlying engine/graphics API. + We carry the information to identify a "texture" in the ImTextureID type. +ImTextureID is nothing more that a void*, aka 4/8 bytes worth of data: just enough to store 1 pointer or 1 integer of your choice. +Dear ImGui doesn't know or understand what you are storing in ImTextureID, it merely pass ImTextureID values until they reach your rendering function. +- In the [examples/](https://github.com/ocornut/imgui/tree/master/examples) backends, for each graphics API we decided on a type that is likely to be a good representation for specifying an image from the end-user perspective. This is what the _examples_ rendering functions are using: +```cpp +OpenGL: +- ImTextureID = GLuint +- See ImGui_ImplOpenGL3_RenderDrawData() function in imgui_impl_opengl3.cpp +``` +```cpp +DirectX9: +- ImTextureID = LPDIRECT3DTEXTURE9 +- See ImGui_ImplDX9_RenderDrawData() function in imgui_impl_dx9.cpp +``` +```cpp +DirectX11: +- ImTextureID = ID3D11ShaderResourceView* +- See ImGui_ImplDX11_RenderDrawData() function in imgui_impl_dx11.cpp +``` +```cpp +DirectX12: +- ImTextureID = D3D12_GPU_DESCRIPTOR_HANDLE +- See ImGui_ImplDX12_RenderDrawData() function in imgui_impl_dx12.cpp +``` +For example, in the OpenGL example backend we store raw OpenGL texture identifier (GLuint) inside ImTextureID. +Whereas in the DirectX11 example backend we store a pointer to ID3D11ShaderResourceView inside ImTextureID, which is a higher-level structure tying together both the texture and information about its format and how to read it. + +- If you have a custom engine built over e.g. OpenGL, instead of passing GLuint around you may decide to use a high-level data type to carry information about the texture as well as how to display it (shaders, etc.). The decision of what to use as ImTextureID can always be made better knowing how your codebase is designed. If your engine has high-level data types for "textures" and "material" then you may want to use them. +If you are starting with OpenGL or DirectX or Vulkan and haven't built much of a rendering engine over them, keeping the default ImTextureID representation suggested by the example backends is probably the best choice. +(Advanced users may also decide to keep a low-level type in ImTextureID, and use ImDrawList callback and pass information to their renderer) + +User code may do: +```cpp +// Cast our texture type to ImTextureID / void* +MyTexture* texture = g_CoffeeTableTexture; +ImGui::Image((void*)texture, ImVec2(texture->Width, texture->Height)); +``` +The renderer function called after ImGui::Render() will receive that same value that the user code passed: +```cpp +// Cast ImTextureID / void* stored in the draw command as our texture type +MyTexture* texture = (MyTexture*)pcmd->GetTexID(); +MyEngineBindTexture2D(texture); +``` +Once you understand this design you will understand that loading image files and turning them into displayable textures is not within the scope of Dear ImGui. +This is by design and is actually a good thing, because it means your code has full control over your data types and how you display them. +If you want to display an image file (e.g. PNG file) into the screen, please refer to documentation and tutorials for the graphics API you are using. + +Refer to [Image Loading and Displaying Examples](https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples) on the [Wiki](https://github.com/ocornut/imgui/wiki) to find simplified examples for loading textures with OpenGL, DirectX9 and DirectX11. + +C/C++ tip: a void* is pointer-sized storage. You may safely store any pointer or integer into it by casting your value to ImTextureID / void*, and vice-versa. +Because both end-points (user code and rendering function) are under your control, you know exactly what is stored inside the ImTextureID / void*. +Examples: +```cpp +GLuint my_tex = XXX; +void* my_void_ptr; +my_void_ptr = (void*)(intptr_t)my_tex; // cast a GLuint into a void* (we don't take its address! we literally store the value inside the pointer) +my_tex = (GLuint)(intptr_t)my_void_ptr; // cast a void* into a GLuint + +ID3D11ShaderResourceView* my_dx11_srv = XXX; +void* my_void_ptr; +my_void_ptr = (void*)my_dx11_srv; // cast a ID3D11ShaderResourceView* into an opaque void* +my_dx11_srv = (ID3D11ShaderResourceView*)my_void_ptr; // cast a void* into a ID3D11ShaderResourceView* +``` +Finally, you may call `ImGui::ShowMetricsWindow()` to explore/visualize/understand how the ImDrawList are generated. + +##### [Return to Index](#index) + +--- + +### Q: How can I use my own math types instead of ImVec2/ImVec4? + +You can edit [imconfig.h](https://github.com/ocornut/imgui/blob/master/imconfig.h) and setup the `IM_VEC2_CLASS_EXTRA`/`IM_VEC4_CLASS_EXTRA` macros to add implicit type conversions. +This way you'll be able to use your own types everywhere, e.g. passing `MyVector2` or `glm::vec2` to ImGui functions instead of `ImVec2`. + +##### [Return to Index](#index) + +--- + +### Q: How can I interact with standard C++ types (such as std::string and std::vector)? +- Being highly portable (backends/bindings for several languages, frameworks, programming style, obscure or older platforms/compilers), and aiming for compatibility & performance suitable for every modern real-time game engines, dear imgui does not use any of std C++ types. We use raw types (e.g. char* instead of std::string) because they adapt to more use cases. +- To use ImGui::InputText() with a std::string or any resizable string class, see [misc/cpp/imgui_stdlib.h](https://github.com/ocornut/imgui/blob/master/misc/cpp/imgui_stdlib.h). +- To use combo boxes and list boxes with `std::vector` or any other data structure: the `BeginCombo()/EndCombo()` API +lets you iterate and submit items yourself, so does the `ListBoxHeader()/ListBoxFooter()` API. +Prefer using them over the old and awkward `Combo()/ListBox()` api. +- Generally for most high-level types you should be able to access the underlying data type. +You may write your own one-liner wrappers to facilitate user code (tip: add new functions in ImGui:: namespace from your code). +- Dear ImGui applications often need to make intensive use of strings. It is expected that many of the strings you will pass +to the API are raw literals (free in C/C++) or allocated in a manner that won't incur a large cost on your application. +Please bear in mind that using `std::string` on applications with large amount of UI may incur unsatisfactory performances. +Modern implementations of `std::string` often include small-string optimization (which is often a local buffer) but those +are not configurable and not the same across implementations. +- If you are finding your UI traversal cost to be too large, make sure your string usage is not leading to excessive amount +of heap allocations. Consider using literals, statically sized buffers and your own helper functions. A common pattern +is that you will need to build lots of strings on the fly, and their maximum length can be easily be scoped ahead. +One possible implementation of a helper to facilitate printf-style building of strings: https://github.com/ocornut/Str +This is a small helper where you can instance strings with configurable local buffers length. Many game engines will +provide similar or better string helpers. + +##### [Return to Index](#index) + +--- + +### Q: How can I display custom shapes? (using low-level ImDrawList API) + +- You can use the low-level `ImDrawList` api to render shapes within a window. +```cpp +ImGui::Begin("My shapes"); + +ImDrawList* draw_list = ImGui::GetWindowDrawList(); + +// Get the current ImGui cursor position +ImVec2 p = ImGui::GetCursorScreenPos(); + +// Draw a red circle +draw_list->AddCircleFilled(ImVec2(p.x + 50, p.y + 50), 30.0f, IM_COL32(255, 0, 0, 255), 16); + +// Draw a 3 pixel thick yellow line +draw_list->AddLine(ImVec2(p.x, p.y), ImVec2(p.x + 100.0f, p.y + 100.0f), IM_COL32(255, 255, 0, 255), 3.0f); + +// Advance the ImGui cursor to claim space in the window (otherwise the window will appear small and needs to be resized) +ImGui::Dummy(ImVec2(200, 200)); + +ImGui::End(); +``` +![ImDrawList usage](https://raw.githubusercontent.com/wiki/ocornut/imgui/tutorials/CustomRendering01.png) + +- Refer to "Demo > Examples > Custom Rendering" in the demo window and read the code of `ShowExampleAppCustomRendering()` in `imgui_demo.cpp` from more examples. +- To generate colors: you can use the macro `IM_COL32(255,255,255,255)` to generate them at compile time, or use `ImGui::GetColorU32(IM_COL32(255,255,255,255))` or `ImGui::GetColorU32(ImVec4(1.0f,1.0f,1.0f,1.0f))` to generate a color that is multiplied by the current value of `style.Alpha`. +- Math operators: if you have setup `IM_VEC2_CLASS_EXTRA` in `imconfig.h` to bind your own math types, you can use your own math types and their natural operators instead of ImVec2. ImVec2 by default doesn't export any math operators in the public API. You may use `#define IMGUI_DEFINE_MATH_OPERATORS` `#include "imgui_internal.h"` to use the internally defined math operators, but instead prefer using your own math library and set it up in `imconfig.h`. +- You can use `ImGui::GetBackgroundDrawList()` or `ImGui::GetForegroundDrawList()` to access draw lists which will be displayed behind and over every other dear imgui windows (one bg/fg drawlist per viewport). This is very convenient if you need to quickly display something on the screen that is not associated to a dear imgui window. +- You can also create your own empty window and draw inside it. Call Begin() with the NoBackground | NoDecoration | NoSavedSettings | NoInputs flags (The `ImGuiWindowFlags_NoDecoration` flag itself is a shortcut for NoTitleBar | NoResize | NoScrollbar | NoCollapse). Then you can retrieve the ImDrawList* via `GetWindowDrawList()` and draw to it in any way you like. +- You can create your own ImDrawList instance. You'll need to initialize them with `ImGui::GetDrawListSharedData()`, or create your own instancing `ImDrawListSharedData`, and then call your renderer function with your own ImDrawList or ImDrawData data. +- Looking for fun? The [ImDrawList coding party 2020](https://github.com/ocornut/imgui/issues/3606) thread is full of "don't do this at home" extreme uses of the ImDrawList API. + +##### [Return to Index](#index) + +--- + +# Q&A: Fonts, Text + +### Q: How should I handle DPI in my application? + +The short answer is: obtain the desired DPI scale, load your fonts resized with that scale (always round down fonts size to nearest integer), and scale your Style structure accordingly using `style.ScaleAllSizes()`. + +Your application may want to detect DPI change and reload the fonts and reset style between frames. + +Your ui code should avoid using hardcoded constants for size and positioning. Prefer to express values as multiple of reference values such as `ImGui::GetFontSize()` or `ImGui::GetFrameHeight()`. So e.g. instead of seeing a hardcoded height of 500 for a given item/window, you may want to use `30*ImGui::GetFontSize()` instead. + +Down the line Dear ImGui will provide a variety of standardized reference values to facilitate using this. + +Applications in the `examples/` folder are not DPI aware partly because they are unable to load a custom font from the file-system (may change that in the future). + +The reason DPI is not auto-magically solved in stock examples is that we don't yet have a satisfying solution for the "multi-dpi" problem (using the `docking` branch: when multiple viewport windows are over multiple monitors using different DPI scale). The current way to handle this on the application side is: +- Create and maintain one font atlas per active DPI scale (e.g. by iterating `platform_io.Monitors[]` before `NewFrame()`). +- Hook `platform_io.OnChangedViewport()` to detect when a `Begin()` call makes a Dear ImGui window change monitor (and therefore DPI). +- In the hook: swap atlas, swap style with correctly sized one, remap the current font from one atlas to the other (may need to maintain a remapping table of your fonts at variying DPI scale). + +This approach is relatively easy and functional but come with two issues: +- It's not possibly to reliably size or position a window ahead of `Begin()` without knowing on which monitor it'll land. +- Style override may be lost during the `Begin()` call crossing monitor boundaries. You may need to do some custom scaling mumbo-jumbo if you want your `OnChangedViewport()` handler to preserve style overrides. + +Please note that if you are not using multi-viewports with multi-monitors using different DPI scale, you can ignore all of this and use the simpler technique recommended at the top. + +### Q: How can I load a different font than the default? +Use the font atlas to load the TTF/OTF file you want: + +```cpp +ImGuiIO& io = ImGui::GetIO(); +io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels); +io.Fonts->GetTexDataAsRGBA32() or GetTexDataAsAlpha8() +``` + +Default is ProggyClean.ttf, monospace, rendered at size 13, embedded in dear imgui's source code. + +(Tip: monospace fonts are convenient because they allow to facilitate horizontal alignment directly at the string level.) + +(Read the [docs/FONTS.md](https://github.com/ocornut/imgui/blob/master/docs/FONTS.md) file for more details about font loading.) + +New programmers: remember that in C/C++ and most programming languages if you want to use a +backslash \ within a string literal, you need to write it double backslash "\\": + +```cpp +io.Fonts->AddFontFromFileTTF("MyFolder\MyFont.ttf", size); // WRONG (you are escaping the M here!) +io.Fonts->AddFontFromFileTTF("MyFolder\\MyFont.ttf", size; // CORRECT (Windows only) +io.Fonts->AddFontFromFileTTF("MyFolder/MyFont.ttf", size); // ALSO CORRECT +``` + +##### [Return to Index](#index) + +--- + +### Q: How can I easily use icons in my application? +The most convenient and practical way is to merge an icon font such as FontAwesome inside you +main font. Then you can refer to icons within your strings. +You may want to see `ImFontConfig::GlyphMinAdvanceX` to make your icon look monospace to facilitate alignment. +(Read the [docs/FONTS.md](https://github.com/ocornut/imgui/blob/master/docs/FONTS.md) file for more details about icons font loading.) +With some extra effort, you may use colorful icon by registering custom rectangle space inside the font atlas, +and copying your own graphics data into it. See docs/FONTS.md about using the AddCustomRectFontGlyph API. + +##### [Return to Index](#index) + +--- + +### Q: How can I load multiple fonts? +Use the font atlas to pack them into a single texture: +(Read the [docs/FONTS.md](https://github.com/ocornut/imgui/blob/master/docs/FONTS.md) file and the code in ImFontAtlas for more details.) + +```cpp +ImGuiIO& io = ImGui::GetIO(); +ImFont* font0 = io.Fonts->AddFontDefault(); +ImFont* font1 = io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels); +ImFont* font2 = io.Fonts->AddFontFromFileTTF("myfontfile2.ttf", size_in_pixels); +io.Fonts->GetTexDataAsRGBA32() or GetTexDataAsAlpha8() +// the first loaded font gets used by default +// use ImGui::PushFont()/ImGui::PopFont() to change the font at runtime + +// Options +ImFontConfig config; +config.OversampleH = 2; +config.OversampleV = 1; +config.GlyphOffset.y -= 1.0f; // Move everything by 1 pixels up +config.GlyphExtraSpacing.x = 1.0f; // Increase spacing between characters +io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_pixels, &config); + +// Combine multiple fonts into one (e.g. for icon fonts) +static ImWchar ranges[] = { 0xf000, 0xf3ff, 0 }; +ImFontConfig config; +config.MergeMode = true; +io.Fonts->AddFontDefault(); +io.Fonts->AddFontFromFileTTF("fontawesome-webfont.ttf", 16.0f, &config, ranges); // Merge icon font +io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_pixels, NULL, &config, io.Fonts->GetGlyphRangesJapanese()); // Merge japanese glyphs +``` + +##### [Return to Index](#index) + +--- + +### Q: How can I display and input non-Latin characters such as Chinese, Japanese, Korean, Cyrillic? +When loading a font, pass custom Unicode ranges to specify the glyphs to load. + +```cpp +// Add default Japanese ranges +io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels, NULL, io.Fonts->GetGlyphRangesJapanese()); + +// Or create your own custom ranges (e.g. for a game you can feed your entire game script and only build the characters the game need) +ImVector ranges; +ImFontGlyphRangesBuilder builder; +builder.AddText("Hello world"); // Add a string (here "Hello world" contains 7 unique characters) +builder.AddChar(0x7262); // Add a specific character +builder.AddRanges(io.Fonts->GetGlyphRangesJapanese()); // Add one of the default ranges +builder.BuildRanges(&ranges); // Build the final result (ordered ranges with all the unique characters submitted) +io.Fonts->AddFontFromFileTTF("myfontfile.ttf", 16.0f, NULL, ranges.Data); +``` + +All your strings needs to use UTF-8 encoding. In C++11 you can encode a string literal in UTF-8 +by using the u8"hello" syntax. Specifying literal in your source code using a local code page +(such as CP-923 for Japanese or CP-1251 for Cyrillic) will NOT work! +Otherwise you can convert yourself to UTF-8 or load text data from file already saved as UTF-8. + +Text input: it is up to your application to pass the right character code by calling `io.AddInputCharacter()`. +The applications in examples/ are doing that. +Windows: you can use the WM_CHAR or WM_UNICHAR or WM_IME_CHAR message (depending if your app is built using Unicode or MultiByte mode). +You may also use MultiByteToWideChar() or ToUnicode() to retrieve Unicode codepoints from MultiByte characters or keyboard state. +Windows: if your language is relying on an Input Method Editor (IME), you can write your HWND to ImGui::GetMainViewport()->PlatformHandleRaw +in order for the default the default implementation of io.SetPlatformImeDataFn() to set your Microsoft IME position correctly. + +##### [Return to Index](#index) + +--- + +# Q&A: Concerns + +### Q: Who uses Dear ImGui? + +You may take a look at: + +- [Quotes](https://github.com/ocornut/imgui/wiki/Quotes) +- [Software using Dear ImGui](https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui) +- [Sponsors](https://github.com/ocornut/imgui/wiki/Sponsors) +- [Gallery](https://github.com/ocornut/imgui/issues/4451) + +##### [Return to Index](#index) + +--- + +### Q: Can you create elaborate/serious tools with Dear ImGui? + +Yes. People have written game editors, data browsers, debuggers, profilers and all sort of non-trivial tools with the library. In my experience the simplicity of the API is very empowering. Your UI runs close to your live data. Make the tools always-on and everybody in the team will be inclined to create new tools (as opposed to more "offline" UI toolkits where only a fraction of your team effectively creates tools). The list of sponsors below is also an indicator that serious game teams have been using the library. + +Dear ImGui is very programmer centric and the immediate-mode GUI paradigm might require you to readjust some habits before you can realize its full potential. Dear ImGui is about making things that are simple, efficient and powerful. + +Dear ImGui is built to be efficient and scalable toward the needs for AAA-quality applications running all day. The IMGUI paradigm offers different opportunities for optimization that the more typical RMGUI paradigm. + +##### [Return to Index](#index) + +--- + +### Q: Can you reskin the look of Dear ImGui? + +Somehow. You can alter the look of the interface to some degree: changing colors, sizes, padding, rounding, fonts. However, as Dear ImGui is designed and optimized to create debug tools, the amount of skinning you can apply is limited. There is only so much you can stray away from the default look and feel of the interface. Dear ImGui is NOT designed to create user interface for games, although with ingenious use of the low-level API you can do it. + +A reasonably skinned application may look like (screenshot from [#2529](https://github.com/ocornut/imgui/issues/2529#issuecomment-524281119)) +![minipars](https://user-images.githubusercontent.com/314805/63589441-d9794f00-c5b1-11e9-8d96-cfc1b93702f7.png) + +##### [Return to Index](#index) + +--- + +### Q: Why using C++ (as opposed to C)? + +Dear ImGui takes advantage of a few C++ languages features for convenience but nothing anywhere Boost insanity/quagmire. Dear ImGui doesn't use any C++ header file. Dear ImGui uses a very small subset of C++11 features. In particular, function overloading and default parameters are used to make the API easier to use and code more terse. Doing so I believe the API is sitting on a sweet spot and giving up on those features would make the API more cumbersome. Other features such as namespace, constructors and templates (in the case of the ImVector<> class) are also relied on as a convenience. + +There is an auto-generated [c-api for Dear ImGui (cimgui)](https://github.com/cimgui/cimgui) by Sonoro1234 and Stephan Dilly. It is designed for creating bindings to other languages. If possible, I would suggest using your target language functionalities to try replicating the function overloading and default parameters used in C++ else the API may be harder to use. Also see [Bindings](https://github.com/ocornut/imgui/wiki/Bindings) for various third-party bindings. + +##### [Return to Index](#index) + +--- + +# Q&A: Community + +### Q: How can I help? +- Businesses: please reach out to `contact AT dearimgui.com` if you work in a place using Dear ImGui! We can discuss ways for your company to fund development via invoiced technical support, maintenance or sponsoring contacts. This is among the most useful thing you can do for Dear ImGui. With increased funding, we can hire more people working on this project. +- Individuals: you can support continued maintenance and development via PayPal donations. See [README](https://github.com/ocornut/imgui/blob/master/docs/README.md). +- If you are experienced with Dear ImGui and C++, look at [GitHub Issues](https://github.com/ocornut/imgui/issues), [GitHub Discussions](https://github.com/ocornut/imgui/discussions), the [Wiki](https://github.com/ocornut/imgui/wiki), read [docs/TODO.txt](https://github.com/ocornut/imgui/blob/master/docs/TODO.txt) and see how you want to help and can help! +- Disclose your usage of Dear ImGui via a dev blog post, a tweet, a screenshot, a mention somewhere etc. +You may post screenshot or links in the [gallery threads](https://github.com/ocornut/imgui/issues/4451). Visuals are ideal as they inspire other programmers. Disclosing your use of Dear ImGui helps the library grow credibility, and help other teams and programmers with taking decisions. +- If you have issues or if you need to hack into the library, even if you don't expect any support it is useful that you share your issues or sometimes incomplete PR. + +##### [Return to Index](#index) + + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Docs/FONTS.md`: + +```md +_(You may browse this at https://github.com/ocornut/imgui/blob/master/docs/FONTS.md or view this file with any Markdown viewer)_ + +## Dear ImGui: Using Fonts + +The code in imgui.cpp embeds a copy of 'ProggyClean.ttf' (by Tristan Grimmer), +a 13 pixels high, pixel-perfect font used by default. We embed it in the source code so you can use Dear ImGui without any file system access. ProggyClean does not scale smoothly, therefore it is recommended that you load your own file when using Dear ImGui in an application aiming to look nice and wanting to support multiple resolutions. + +You may also load external .TTF/.OTF files. +In the [misc/fonts/](https://github.com/ocornut/imgui/tree/master/misc/fonts) folder you can find a few suggested fonts, provided as a convenience. + +**Also read the FAQ:** https://www.dearimgui.org/faq (there is a Fonts section!) + +## Index +- [Readme First](#readme-first) +- [How should I handle DPI in my application?](#how-should-i-handle-dpi-in-my-application) +- [Fonts Loading Instructions](#font-loading-instructions) +- [Using Icon Fonts](#using-icon-fonts) +- [Using FreeType Rasterizer (imgui_freetype)](#using-freetype-rasterizer-imgui_freetype) +- [Using Colorful Glyphs/Emojis](#using-colorful-glyphsemojis) +- [Using Custom Glyph Ranges](#using-custom-glyph-ranges) +- [Using Custom Colorful Icons](#using-custom-colorful-icons) +- [Using Font Data Embedded In Source Code](#using-font-data-embedded-in-source-code) +- [About filenames](#about-filenames) +- [Credits/Licenses For Fonts Included In Repository](#creditslicenses-for-fonts-included-in-repository) +- [Font Links](#font-links) + +--------------------------------------- + ## Readme First + +- You can use the `Metrics/Debugger` window (available in `Demo>Tools`) to browse your fonts and understand what's going on if you have an issue. You can also reach it in `Demo->Tools->Style Editor->Fonts`. The same information are also available in the Style Editor under Fonts. + +![imgui_capture_0008](https://user-images.githubusercontent.com/8225057/135429892-0e41ef8d-33c5-4991-bcf6-f997a0bcfd6b.png) + +- All loaded fonts glyphs are rendered into a single texture atlas ahead of time. Calling either of `io.Fonts->GetTexDataAsAlpha8()`, `io.Fonts->GetTexDataAsRGBA32()` or `io.Fonts->Build()` will build the atlas. + +- Make sure your font ranges data are persistent (available during the calls to `GetTexDataAsAlpha8()`/`GetTexDataAsRGBA32()/`Build()`. + +- Use C++11 u8"my text" syntax to encode literal strings as UTF-8. e.g.: +```cpp +u8"hello" +u8"こんにちは" // this will be encoded as UTF-8 +``` + +##### [Return to Index](#index) + +## How should I handle DPI in my application? + +See [FAQ entry](https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#q-how-should-i-handle-dpi-in-my-application). + +##### [Return to Index](#index) + + +## Font Loading Instructions + +**Load default font:** +```cpp +ImGuiIO& io = ImGui::GetIO(); +io.Fonts->AddFontDefault(); +``` + + +**Load .TTF/.OTF file with:** +```cpp +ImGuiIO& io = ImGui::GetIO(); +io.Fonts->AddFontFromFileTTF("font.ttf", size_pixels); +``` +If you get an assert stating "Could not load font file!", your font filename is likely incorrect. Read "[About filenames](#about-filenames)" carefully. + + +**Load multiple fonts:** +```cpp +// Init +ImGuiIO& io = ImGui::GetIO(); +ImFont* font1 = io.Fonts->AddFontFromFileTTF("font.ttf", size_pixels); +ImFont* font2 = io.Fonts->AddFontFromFileTTF("anotherfont.otf", size_pixels); +``` +```cpp +// In application loop: select font at runtime +ImGui::Text("Hello"); // use the default font (which is the first loaded font) +ImGui::PushFont(font2); +ImGui::Text("Hello with another font"); +ImGui::PopFont(); +``` + + +**For advanced options create a ImFontConfig structure and pass it to the AddFont() function (it will be copied internally):** +```cpp +ImFontConfig config; +config.OversampleH = 2; +config.OversampleV = 1; +config.GlyphExtraSpacing.x = 1.0f; +ImFont* font = io.Fonts->AddFontFromFileTTF("font.ttf", size_pixels, &config); +``` + + +**Combine multiple fonts into one:** +```cpp +// Load a first font +ImFont* font = io.Fonts->AddFontDefault(); + +// Add character ranges and merge into the previous font +// The ranges array is not copied by the AddFont* functions and is used lazily +// so ensure it is available at the time of building or calling GetTexDataAsRGBA32(). +static const ImWchar icons_ranges[] = { 0xf000, 0xf3ff, 0 }; // Will not be copied by AddFont* so keep in scope. +ImFontConfig config; +config.MergeMode = true; +io.Fonts->AddFontFromFileTTF("DroidSans.ttf", 18.0f, &config, io.Fonts->GetGlyphRangesJapanese()); // Merge into first font +io.Fonts->AddFontFromFileTTF("fontawesome-webfont.ttf", 18.0f, &config, icons_ranges); // Merge into first font +io.Fonts->Build(); +``` + +**Add a fourth parameter to bake specific font ranges only:** + +```cpp +// Basic Latin, Extended Latin +io.Fonts->AddFontFromFileTTF("font.ttf", size_pixels, NULL, io.Fonts->GetGlyphRangesDefault()); + +// Default + Selection of 2500 Ideographs used by Simplified Chinese +io.Fonts->AddFontFromFileTTF("font.ttf", size_pixels, NULL, io.Fonts->GetGlyphRangesChineseSimplifiedCommon()); + +// Default + Hiragana, Katakana, Half-Width, Selection of 1946 Ideographs +io.Fonts->AddFontFromFileTTF("font.ttf", size_pixels, NULL, io.Fonts->GetGlyphRangesJapanese()); +``` +See [Using Custom Glyph Ranges](#using-custom-glyph-ranges) section to create your own ranges. + + +**Example loading and using a Japanese font:** + +```cpp +ImGuiIO& io = ImGui::GetIO(); +io.Fonts->AddFontFromFileTTF("NotoSansCJKjp-Medium.otf", 20.0f, NULL, io.Fonts->GetGlyphRangesJapanese()); +``` +```cpp +ImGui::Text(u8"こんにちは!テスト %d", 123); +if (ImGui::Button(u8"ロード")) +{ + // do stuff +} +ImGui::InputText("string", buf, IM_ARRAYSIZE(buf)); +ImGui::SliderFloat("float", &f, 0.0f, 1.0f); +``` + +![sample code output](https://raw.githubusercontent.com/wiki/ocornut/imgui/web/v160/code_sample_02_jp.png) +
_(settings: Dark style (left), Light style (right) / Font: NotoSansCJKjp-Medium, 20px / Rounding: 5)_ + +**Font Atlas too large?** + +- If you have very large number of glyphs or multiple fonts, the texture may become too big for your graphics API. The typical result of failing to upload a texture is if every glyphs appears as white rectangles. +- Mind the fact that some graphics drivers have texture size limitation. If you are building a PC application, mind the fact that your users may use hardware with lower limitations than yours. + +Some solutions: + +1. Reduce glyphs ranges by calculating them from source localization data. + You can use the `ImFontGlyphRangesBuilder` for this purpose and rebuilding your atlas between frames when new characters are needed. This will be the biggest win! +2. You may reduce oversampling, e.g. `font_config.OversampleH = 2`, this will largely reduce your texture size. + Note that while OversampleH = 2 looks visibly very close to 3 in most situations, with OversampleH = 1 the quality drop will be noticeable. +3. Set `io.Fonts.TexDesiredWidth` to specify a texture width to minimize texture height (see comment in `ImFontAtlas::Build()` function). +4. Set `io.Fonts.Flags |= ImFontAtlasFlags_NoPowerOfTwoHeight;` to disable rounding the texture height to the next power of two. +5. Read about oversampling [here](https://github.com/nothings/stb/blob/master/tests/oversample). +6. To support the extended range of unicode beyond 0xFFFF (e.g. emoticons, dingbats, symbols, shapes, ancient languages, etc...) add `#define IMGUI_USE_WCHAR32`in your `imconfig.h`. + +##### [Return to Index](#index) + +## Using Icon Fonts + +Using an icon font (such as [FontAwesome](http://fontawesome.io) or [OpenFontIcons](https://github.com/traverseda/OpenFontIcons)) is an easy and practical way to use icons in your Dear ImGui application. +A common pattern is to merge the icon font within your main font, so you can embed icons directly from your strings without having to change fonts back and forth. + +To refer to the icon UTF-8 codepoints from your C++ code, you may use those headers files created by Juliette Foucaut: https://github.com/juliettef/IconFontCppHeaders. + +So you can use `ICON_FA_SEARCH` as a string that will render as a "Search" icon. + +Example Setup: +```cpp +// Merge icons into default tool font +#include "IconsFontAwesome.h" +ImGuiIO& io = ImGui::GetIO(); +io.Fonts->AddFontDefault(); + +ImFontConfig config; +config.MergeMode = true; +config.GlyphMinAdvanceX = 13.0f; // Use if you want to make the icon monospaced +static const ImWchar icon_ranges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 }; +io.Fonts->AddFontFromFileTTF("fonts/fontawesome-webfont.ttf", 13.0f, &config, icon_ranges); +``` +Example Usage: +```cpp +// Usage, e.g. +ImGui::Text("%s among %d items", ICON_FA_SEARCH, count); +ImGui::Button(ICON_FA_SEARCH " Search"); +// C string _literals_ can be concatenated at compilation time, e.g. "hello" " world" +// ICON_FA_SEARCH is defined as a string literal so this is the same as "A" "B" becoming "AB" +``` +See Links below for other icons fonts and related tools. + +Here's an application using icons ("Avoyd", https://www.avoyd.com): +![avoyd](https://user-images.githubusercontent.com/8225057/81696852-c15d9e80-9464-11ea-9cab-2a4d4fc84396.jpg) + +##### [Return to Index](#index) + +## Using FreeType Rasterizer (imgui_freetype) + +- Dear ImGui uses imstb\_truetype.h to rasterize fonts (with optional oversampling). This technique and its implementation are not ideal for fonts rendered at small sizes, which may appear a little blurry or hard to read. +- There is an implementation of the ImFontAtlas builder using FreeType that you can use in the [misc/freetype/](https://github.com/ocornut/imgui/tree/master/misc/freetype) folder. +- FreeType supports auto-hinting which tends to improve the readability of small fonts. +- Read documentation in the [misc/freetype/](https://github.com/ocornut/imgui/tree/master/misc/freetype) folder. +- Correct sRGB space blending will have an important effect on your font rendering quality. + +##### [Return to Index](#index) + +## Using Colorful Glyphs/Emojis + +- Rendering of colored emojis is only supported by imgui_freetype with FreeType 2.10+. +- You will need to load fonts with the `ImGuiFreeTypeBuilderFlags_LoadColor` flag. +- Emojis are frequently encoded in upper Unicode layers (character codes >0x10000) and will need dear imgui compiled with `IMGUI_USE_WCHAR32`. +- Not all types of color fonts are supported by FreeType at the moment. +- Stateful Unicode features such as skin tone modifiers are not supported by the text renderer. + +![colored glyphs](https://user-images.githubusercontent.com/8225057/106171241-9dc4ba80-6191-11eb-8a69-ca1467b206d1.png) + +```cpp +io.Fonts->AddFontFromFileTTF("../../../imgui_dev/data/fonts/NotoSans-Regular.ttf", 16.0f); +static ImWchar ranges[] = { 0x1, 0x1FFFF, 0 }; +static ImFontConfig cfg; +cfg.OversampleH = cfg.OversampleV = 1; +cfg.MergeMode = true; +cfg.FontBuilderFlags |= ImGuiFreeTypeBuilderFlags_LoadColor; +io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\seguiemj.ttf", 16.0f, &cfg, ranges); +``` + +##### [Return to Index](#index) + +## Using Custom Glyph Ranges + +You can use the `ImFontGlyphRangesBuilder` helper to create glyph ranges based on text input. For example: for a game where your script is known, if you can feed your entire script to it and only build the characters the game needs. +```cpp +ImVector ranges; +ImFontGlyphRangesBuilder builder; +builder.AddText("Hello world"); // Add a string (here "Hello world" contains 7 unique characters) +builder.AddChar(0x7262); // Add a specific character +builder.AddRanges(io.Fonts->GetGlyphRangesJapanese()); // Add one of the default ranges +builder.BuildRanges(&ranges); // Build the final result (ordered ranges with all the unique characters submitted) + +io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels, NULL, ranges.Data); +io.Fonts->Build(); // Build the atlas while 'ranges' is still in scope and not deleted. +``` + +##### [Return to Index](#index) + +## Using Custom Colorful Icons + +As an alternative to rendering colorful glyphs using imgui_freetype with `ImGuiFreeTypeBuilderFlags_LoadColor`, you may allocate your own space in the texture atlas and write yourself into it. **(This is a BETA api, use if you are familiar with dear imgui and with your rendering backend)** + +- You can use the `ImFontAtlas::AddCustomRect()` and `ImFontAtlas::AddCustomRectFontGlyph()` api to register rectangles that will be packed into the font atlas texture. Register them before building the atlas, then call Build()`. +- You can then use `ImFontAtlas::GetCustomRectByIndex(int)` to query the position/size of your rectangle within the texture, and blit/copy any graphics data of your choice into those rectangles. +- This API is beta because it is likely to change in order to support multi-dpi (multiple viewports on multiple monitors with varying DPI scale). + +#### Pseudo-code: +```cpp +// Add font, then register two custom 13x13 rectangles mapped to glyph 'a' and 'b' of this font +ImFont* font = io.Fonts->AddFontDefault(); +int rect_ids[2]; +rect_ids[0] = io.Fonts->AddCustomRectFontGlyph(font, 'a', 13, 13, 13+1); +rect_ids[1] = io.Fonts->AddCustomRectFontGlyph(font, 'b', 13, 13, 13+1); + +// Build atlas +io.Fonts->Build(); + +// Retrieve texture in RGBA format +unsigned char* tex_pixels = NULL; +int tex_width, tex_height; +io.Fonts->GetTexDataAsRGBA32(&tex_pixels, &tex_width, &tex_height); + +for (int rect_n = 0; rect_n < IM_ARRAYSIZE(rect_ids); rect_n++) +{ + int rect_id = rects_ids[rect_n]; + if (const ImFontAtlas::CustomRect* rect = io.Fonts->GetCustomRectByIndex(rect_id)) + { + // Fill the custom rectangle with red pixels (in reality you would draw/copy your bitmap data here!) + for (int y = 0; y < rect->Height; y++) + { + ImU32* p = (ImU32*)tex_pixels + (rect->Y + y) * tex_width + (rect->X); + for (int x = rect->Width; x > 0; x--) + *p++ = IM_COL32(255, 0, 0, 255); + } + } +} +``` + +##### [Return to Index](#index) + +## Using Font Data Embedded In Source Code + +- Compile and use [binary_to_compressed_c.cpp](https://github.com/ocornut/imgui/blob/master/misc/fonts/binary_to_compressed_c.cpp) to create a compressed C style array that you can embed in source code. +- See the documentation in [binary_to_compressed_c.cpp](https://github.com/ocornut/imgui/blob/master/misc/fonts/binary_to_compressed_c.cpp) for instructions on how to use the tool. +- You may find a precompiled version binary_to_compressed_c.exe for Windows inside the demo binaries package (see [README](https://github.com/ocornut/imgui/blob/master/docs/README.md)). +- The tool can optionally output Base85 encoding to reduce the size of _source code_ but the read-only arrays in the actual binary will be about 20% bigger. + +Then load the font with: +```cpp +ImFont* font = io.Fonts->AddFontFromMemoryCompressedTTF(compressed_data, compressed_data_size, size_pixels, ...); +``` +or +```cpp +ImFont* font = io.Fonts->AddFontFromMemoryCompressedBase85TTF(compressed_data_base85, size_pixels, ...); +``` + +##### [Return to Index](#index) + +## About filenames + +**Please note that many new C/C++ users have issues loading their files _because the filename they provide is wrong_.** + +Two things to watch for: +- Make sure your IDE/debugger settings starts your executable from the right working directory. In Visual Studio you can change your working directory in project `Properties > General > Debugging > Working Directory`. People assume that their execution will start from the root folder of the project, where by default it oftens start from the folder where object or executable files are stored. +```cpp +// Relative filename depends on your Working Directory when running your program! +io.Fonts->AddFontFromFileTTF("MyImage01.jpg", ...); + +// Load from the parent folder of your Working Directory +io.Fonts->AddFontFromFileTTF("../MyImage01.jpg", ...); +``` +- In C/C++ and most programming languages if you want to use a backslash `\` within a string literal, you need to write it double backslash `\\`. At it happens, Windows uses backslashes as a path separator, so be mindful. +```cpp +io.Fonts->AddFontFromFileTTF("MyFiles\MyImage01.jpg", ...); // This is INCORRECT!! +io.Fonts->AddFontFromFileTTF("MyFiles\\MyImage01.jpg", ...); // This is CORRECT +``` +In some situations, you may also use `/` path separator under Windows. + +##### [Return to Index](#index) + +## Credits/Licenses For Fonts Included In Repository + +Some fonts files are available in the `misc/fonts/` folder: + +**Roboto-Medium.ttf**, by Christian Robetson +
Apache License 2.0 +
https://fonts.google.com/specimen/Roboto + +**Cousine-Regular.ttf**, by Steve Matteson +
Digitized data copyright (c) 2010 Google Corporation. +
Licensed under the SIL Open Font License, Version 1.1 +
https://fonts.google.com/specimen/Cousine + +**DroidSans.ttf**, by Steve Matteson +
Apache License 2.0 +
https://www.fontsquirrel.com/fonts/droid-sans + +**ProggyClean.ttf**, by Tristan Grimmer +
MIT License +
(recommended loading setting: Size = 13.0, GlyphOffset.y = +1) +
http://www.proggyfonts.net/ + +**ProggyTiny.ttf**, by Tristan Grimmer +
MIT License +
(recommended loading setting: Size = 10.0, GlyphOffset.y = +1) +
http://www.proggyfonts.net/ + +**Karla-Regular.ttf**, by Jonathan Pinhorn +
SIL OPEN FONT LICENSE Version 1.1 + +##### [Return to Index](#index) + +## Font Links + +#### ICON FONTS + +- C/C++ header for icon fonts (#define with code points to use in source code string literals) https://github.com/juliettef/IconFontCppHeaders +- FontAwesome https://fortawesome.github.io/Font-Awesome +- OpenFontIcons https://github.com/traverseda/OpenFontIcons +- Google Icon Fonts https://design.google.com/icons/ +- Kenney Icon Font (Game Controller Icons) https://github.com/nicodinh/kenney-icon-font +- IcoMoon - Custom Icon font builder https://icomoon.io/app + +#### REGULAR FONTS + +- Google Noto Fonts (worldwide languages) https://www.google.com/get/noto/ +- Open Sans Fonts https://fonts.google.com/specimen/Open+Sans +- (Japanese) M+ fonts by Coji Morishita http://mplus-fonts.sourceforge.jp/mplus-outline-fonts/index-en.html + +#### MONOSPACE FONTS + +Pixel Perfect: +- Proggy Fonts, by Tristan Grimmer http://www.proggyfonts.net or http://upperbounds.net +- Sweet16, Sweet16 Mono, by Martin Sedlak (Latin + Supplemental + Extended A) https://github.com/kmar/Sweet16Font (also include an .inl file to use directly in dear imgui.) + +Regular: +- Google Noto Mono Fonts https://www.google.com/get/noto/ +- Typefaces for source code beautification https://github.com/chrissimpkins/codeface +- Programmation fonts http://s9w.github.io/font_compare/ +- Inconsolata http://www.levien.com/type/myfonts/inconsolata.html +- Adobe Source Code Pro: Monospaced font family for ui & coding environments https://github.com/adobe-fonts/source-code-pro +- Monospace/Fixed Width Programmer's Fonts http://www.lowing.org/fonts/ + +Or use Arial Unicode or other Unicode fonts provided with Windows for full characters coverage (not sure of their licensing). + +##### [Return to Index](#index) + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Docs/README.md`: + +```md +Dear ImGui +===== +[![Build Status](https://github.com/ocornut/imgui/workflows/build/badge.svg)](https://github.com/ocornut/imgui/actions?workflow=build) [![Static Analysis Status](https://github.com/ocornut/imgui/workflows/static-analysis/badge.svg)](https://github.com/ocornut/imgui/actions?workflow=static-analysis) + + +(This library is available under a free and permissive license, but needs financial support to sustain its continued improvements. In addition to maintenance and stability there are many desirable features yet to be added. If your company is using Dear ImGui, please consider reaching out.) + +Businesses: support continued development and maintenance via invoiced technical support, maintenance, sponsoring contracts: +
  _E-mail: contact @ dearimgui dot com_ + +Individuals: support continued development and maintenance [here](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WGHNC6MBFLZ2S). + +Also see [Sponsors](https://github.com/ocornut/imgui/wiki/Sponsors) page. + +---- + +Dear ImGui is a **bloat-free graphical user interface library for C++**. It outputs optimized vertex buffers that you can render anytime in your 3D-pipeline enabled application. It is fast, portable, renderer agnostic and self-contained (no external dependencies). + +Dear ImGui is designed to **enable fast iterations** and to **empower programmers** to create **content creation tools and visualization / debug tools** (as opposed to UI for the average end-user). It favors simplicity and productivity toward this goal, and lacks certain features normally found in more high-level libraries. + +Dear ImGui is particularly suited to integration in games engine (for tooling), real-time 3D applications, fullscreen applications, embedded applications, or any applications on consoles platforms where operating system features are non-standard. + +| [Usage](#usage) - [How it works](#how-it-works) - [Releases & Changelogs](#releases--changelogs) - [Demo](#demo) - [Integration](#integration) | +:----------------------------------------------------------: | +| [Upcoming changes](#upcoming-changes) - [Gallery](#gallery) - [Support, FAQ](#support-frequently-asked-questions-faq) - [How to help](#how-to-help) - [Sponsors](#sponsors) - [Credits](#credits) - [License](#license) | +| [Wiki](https://github.com/ocornut/imgui/wiki) - [Languages & frameworks backends/bindings](https://github.com/ocornut/imgui/wiki/Bindings) - [Software using Dear ImGui](https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui) - [User quotes](https://github.com/ocornut/imgui/wiki/Quotes) | + +### Usage + +**The core of Dear ImGui is self-contained within a few platform-agnostic files** which you can easily compile in your application/engine. They are all the files in the root folder of the repository (imgui*.cpp, imgui*.h). + +**No specific build process is required**. You can add the .cpp files to your existing project. + +You will need a backend to integrate Dear ImGui in your app. The backend passes mouse/keyboard/gamepad inputs and variety of settings to Dear ImGui, and is in charge of rendering the resulting vertices. + +**Backends for a variety of graphics api and rendering platforms** are provided in the [backends/](https://github.com/ocornut/imgui/tree/master/backends) folder, along with example applications in the [examples/](https://github.com/ocornut/imgui/tree/master/examples) folder. See the [Integration](#integration) section of this document for details. You may also create your own backend. Anywhere where you can render textured triangles, you can render Dear ImGui. + +After Dear ImGui is setup in your application, you can use it from \_anywhere\_ in your program loop: + +Code: +```cpp +ImGui::Text("Hello, world %d", 123); +if (ImGui::Button("Save")) + MySaveFunction(); +ImGui::InputText("string", buf, IM_ARRAYSIZE(buf)); +ImGui::SliderFloat("float", &f, 0.0f, 1.0f); +``` +Result: +
![sample code output (dark)](https://raw.githubusercontent.com/wiki/ocornut/imgui/web/v175/capture_readme_styles_0001.png) ![sample code output (light)](https://raw.githubusercontent.com/wiki/ocornut/imgui/web/v175/capture_readme_styles_0002.png) +
_(settings: Dark style (left), Light style (right) / Font: Roboto-Medium, 16px)_ + +Code: +```cpp +// Create a window called "My First Tool", with a menu bar. +ImGui::Begin("My First Tool", &my_tool_active, ImGuiWindowFlags_MenuBar); +if (ImGui::BeginMenuBar()) +{ + if (ImGui::BeginMenu("File")) + { + if (ImGui::MenuItem("Open..", "Ctrl+O")) { /* Do stuff */ } + if (ImGui::MenuItem("Save", "Ctrl+S")) { /* Do stuff */ } + if (ImGui::MenuItem("Close", "Ctrl+W")) { my_tool_active = false; } + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); +} + +// Edit a color (stored as ~4 floats) +ImGui::ColorEdit4("Color", my_color); + +// Plot some values +const float my_values[] = { 0.2f, 0.1f, 1.0f, 0.5f, 0.9f, 2.2f }; +ImGui::PlotLines("Frame Times", my_values, IM_ARRAYSIZE(my_values)); + +// Display contents in a scrolling region +ImGui::TextColored(ImVec4(1,1,0,1), "Important Stuff"); +ImGui::BeginChild("Scrolling"); +for (int n = 0; n < 50; n++) + ImGui::Text("%04d: Some text", n); +ImGui::EndChild(); +ImGui::End(); +``` +Result: +
![sample code output](https://raw.githubusercontent.com/wiki/ocornut/imgui/web/v180/code_sample_04_color.gif) + +Dear ImGui allows you to **create elaborate tools** as well as very short-lived ones. On the extreme side of short-livedness: using the Edit&Continue (hot code reload) feature of modern compilers you can add a few widgets to tweaks variables while your application is running, and remove the code a minute later! Dear ImGui is not just for tweaking values. You can use it to trace a running algorithm by just emitting text commands. You can use it along with your own reflection data to browse your dataset live. You can use it to expose the internals of a subsystem in your engine, to create a logger, an inspection tool, a profiler, a debugger, an entire game making editor/framework, etc. + +### How it works + +Check out the Wiki's [About the IMGUI paradigm](https://github.com/ocornut/imgui/wiki#about-the-imgui-paradigm) section if you want to understand the core principles behind the IMGUI paradigm. An IMGUI tries to minimize superfluous state duplication, state synchronization and state retention from the user's point of view. It is less error prone (less code and less bugs) than traditional retained-mode interfaces, and lends itself to create dynamic user interfaces. + +Dear ImGui outputs vertex buffers and command lists that you can easily render in your application. The number of draw calls and state changes required to render them is fairly small. Because Dear ImGui doesn't know or touch graphics state directly, you can call its functions anywhere in your code (e.g. in the middle of a running algorithm, or in the middle of your own rendering process). Refer to the sample applications in the examples/ folder for instructions on how to integrate Dear ImGui with your existing codebase. + +_A common misunderstanding is to mistake immediate mode gui for immediate mode rendering, which usually implies hammering your driver/GPU with a bunch of inefficient draw calls and state changes as the gui functions are called. This is NOT what Dear ImGui does. Dear ImGui outputs vertex buffers and a small list of draw calls batches. It never touches your GPU directly. The draw call batches are decently optimal and you can render them later, in your app or even remotely._ + +### Releases & Changelogs + +See [Releases](https://github.com/ocornut/imgui/releases) page. +Reading the changelogs is a good way to keep up to date with the things Dear ImGui has to offer, and maybe will give you ideas of some features that you've been ignoring until now! + +### Demo + +Calling the `ImGui::ShowDemoWindow()` function will create a demo window showcasing variety of features and examples. The code is always available for reference in `imgui_demo.cpp`. + +![screenshot demo](https://raw.githubusercontent.com/wiki/ocornut/imgui/web/v167/v167-misc.png) + +You should be able to build the examples from sources (tested on Windows/Mac/Linux). If you don't, let us know! If you want to have a quick look at some Dear ImGui features, you can download Windows binaries of the demo app here: +- [imgui-demo-binaries-20210331.zip](https://www.dearimgui.org/binaries/imgui-demo-binaries-20210331.zip) (Windows, 1.83 WIP, built 2021/03/31, master branch) or [older demo binaries](https://www.dearimgui.org/binaries). + +The demo applications are not DPI aware so expect some blurriness on a 4K screen. For DPI awareness in your application, you can load/reload your font at different scale, and scale your style with `style.ScaleAllSizes()` (see [FAQ](https://www.dearimgui.org/faq)). + +### Integration + +On most platforms and when using C++, **you should be able to use a combination of the [imgui_impl_xxxx](https://github.com/ocornut/imgui/tree/master/backends) backends without modification** (e.g. `imgui_impl_win32.cpp` + `imgui_impl_dx11.cpp`). If your engine supports multiple platforms, consider using more of the imgui_impl_xxxx files instead of rewriting them: this will be less work for you and you can get Dear ImGui running immediately. You can _later_ decide to rewrite a custom backend using your custom engine functions if you wish so. + +Integrating Dear ImGui within your custom engine is a matter of 1) wiring mouse/keyboard/gamepad inputs 2) uploading one texture to your GPU/render engine 3) providing a render function that can bind textures and render textured triangles. The [examples/](https://github.com/ocornut/imgui/tree/master/examples) folder is populated with applications doing just that. If you are an experienced programmer at ease with those concepts, it should take you less than two hours to integrate Dear ImGui in your custom engine. **Make sure to spend time reading the [FAQ](https://www.dearimgui.org/faq), comments, and some of the examples/ application!** + +Officially maintained backends/bindings (in repository): +- Renderers: DirectX9, DirectX10, DirectX11, DirectX12, Metal, OpenGL/ES/ES2, SDL_Renderer, Vulkan, WebGPU. +- Platforms: GLFW, SDL2, Win32, Glut, OSX, Android. +- Frameworks: Allegro5, Emscripten. + +[Third-party backends/bindings](https://github.com/ocornut/imgui/wiki/Bindings) wiki page: +- Languages: C, C# and: Beef, ChaiScript, Crystal, D, Go, Haskell, Haxe/hxcpp, Java, JavaScript, Julia, Kotlin, Lobster, Lua, Odin, Pascal, PureBasic, Python, Ruby, Rust, Swift... +- Frameworks: AGS/Adventure Game Studio, Amethyst, Blender, bsf, Cinder, Cocos2d-x, Diligent Engine, Flexium, GML/Game Maker Studio2, GLEQ, Godot, GTK3+OpenGL3, Irrlicht Engine, LÖVE+LUA, Magnum, Monogame, NanoRT, nCine, Nim Game Lib, Nintendo 3DS & Switch (homebrew), Ogre, openFrameworks, OSG/OpenSceneGraph, Orx, Photoshop, px_render, Qt/QtDirect3D, SDL_Renderer, SFML, Sokol, Unity, Unreal Engine 4, vtk, VulkanHpp, VulkanSceneGraph, Win32 GDI, WxWidgets. +- Note that C bindings ([cimgui](https://github.com/cimgui/cimgui)) are auto-generated, you can use its json/lua output to generate bindings for other languages. + +[Useful Extensions/Widgets](https://github.com/ocornut/imgui/wiki/Useful-Extensions) wiki page: +- Text editors, node editors, timeline editors, plotting, software renderers, remote network access, memory editors, gizmos etc. + +Also see [Wiki](https://github.com/ocornut/imgui/wiki) for more links and ideas. + +### Upcoming Changes + +Some of the goals for 2022 are: +- Work on Docking (see [#2109](https://github.com/ocornut/imgui/issues/2109), in public [docking](https://github.com/ocornut/imgui/tree/docking) branch) +- Work on Multi-Viewport / Multiple OS windows. (see [#1542](https://github.com/ocornut/imgui/issues/1542), in public [docking](https://github.com/ocornut/imgui/tree/docking) branch looking for feedback) +- Work on gamepad/keyboard controls. (see [#787](https://github.com/ocornut/imgui/issues/787)) +- Work on automation and testing system, both to test the library and end-user apps. (see [#435](https://github.com/ocornut/imgui/issues/435)) +- Make the examples look better, improve styles, improve font support, make the examples hi-DPI and multi-DPI aware. + +### Gallery + +For more user-submitted screenshots of projects using Dear ImGui, check out the [Gallery Threads](https://github.com/ocornut/imgui/issues/4451)! + +For a list of third-party widgets and extensions, check out the [Useful Extensions/Widgets](https://github.com/ocornut/imgui/wiki/Useful-Extensions) wiki page. + +Custom engine +[![screenshot game](https://raw.githubusercontent.com/wiki/ocornut/imgui/web/v149/gallery_TheDragonsTrap-01-thumb.jpg)](https://cloud.githubusercontent.com/assets/8225057/20628927/33e14cac-b329-11e6-80f6-9524e93b048a.png) + +Custom engine +[![screenshot tool](https://raw.githubusercontent.com/wiki/ocornut/imgui/web/v160/editor_white_preview.jpg)](https://raw.githubusercontent.com/wiki/ocornut/imgui/web/v160/editor_white.png) + +[Tracy Profiler](https://github.com/wolfpld/tracy) +![tracy profiler](https://raw.githubusercontent.com/wiki/ocornut/imgui/web/v176/tracy_profiler.png) + +### Support, Frequently Asked Questions (FAQ) + +See: [Frequently Asked Questions (FAQ)](https://github.com/ocornut/imgui/blob/master/docs/FAQ.md) where common questions are answered. + +See: [Wiki](https://github.com/ocornut/imgui/wiki) for many links, references, articles. + +See: [Articles about the IMGUI paradigm](https://github.com/ocornut/imgui/wiki#about-the-imgui-paradigm) to read/learn about the Immediate Mode GUI paradigm. + +Getting started? For first-time users having issues compiling/linking/running or issues loading fonts, please use [GitHub Discussions](https://github.com/ocornut/imgui/discussions). + +For other questions, bug reports, requests, feedback, you may post on [GitHub Issues](https://github.com/ocornut/imgui/issues). Please read and fill the New Issue template carefully. + +Private support is available for paying business customers (E-mail: _contact @ dearimgui dot com_). + +**Which version should I get?** + +We occasionally tag [Releases](https://github.com/ocornut/imgui/releases) but it is generally safe and recommended to sync to master/latest. The library is fairly stable and regressions tend to be fixed fast when reported. + +Advanced users may want to use the `docking` branch with [Multi-Viewport](https://github.com/ocornut/imgui/issues/1542) and [Docking](https://github.com/ocornut/imgui/issues/2109) features. This branch is kept in sync with master regularly. + +**Who uses Dear ImGui?** + +See the [Quotes](https://github.com/ocornut/imgui/wiki/Quotes), [Sponsors](https://github.com/ocornut/imgui/wiki/Sponsors), [Software using dear imgui](https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui) Wiki pages for an idea of who is using Dear ImGui. Please add your game/software if you can! Also see the [Gallery Threads](https://github.com/ocornut/imgui/issues/4451)! + +How to help +----------- + +**How can I help?** + +- See [GitHub Forum/issues](https://github.com/ocornut/imgui/issues) and [Github Discussions](https://github.com/ocornut/imgui/discussions). +- You may help with development and submit pull requests! Please understand that by submitting a PR you are also submitting a request for the maintainer to review your code and then take over its maintenance forever. PR should be crafted both in the interest in the end-users and also to ease the maintainer into understanding and accepting it. +- See [Help wanted](https://github.com/ocornut/imgui/wiki/Help-Wanted) on the [Wiki](https://github.com/ocornut/imgui/wiki/) for some more ideas. +- Have your company financially support this project (please reach by e-mail) + +**How can I help financing further development of Dear ImGui?** + +See [Sponsors](https://github.com/ocornut/imgui/wiki/Sponsors) page. + +Sponsors +-------- + +Ongoing Dear ImGui development is currently financially supported by users and private sponsors: + +*Platinum-chocolate sponsors* +- [Blizzard](https://careers.blizzard.com/en-us/openings/engineering/all/all/all/1) + +*Double-chocolate sponsors* +- [Ubisoft](https://montreal.ubisoft.com/en/ubisoft-sponsors-user-interface-library-for-c-dear-imgui), [Supercell](https://supercell.com) + +*Chocolate sponsors* +- [Activision](https://careers.activision.com/c/programmingsoftware-engineering-jobs), [Adobe](https://www.adobe.com/products/medium.html), [Aras Pranckevičius](https://aras-p.info), [Arkane Studios](https://www.arkane-studios.com), [Epic](https://www.unrealengine.com/en-US/megagrants), [Google](https://github.com/google/filament), [Nvidia](https://developer.nvidia.com/nvidia-omniverse), [RAD Game Tools](http://www.radgametools.com/) + +*Salty-caramel sponsors* +- [Framefield](http://framefield.com), [Grinding Gear Games](https://www.grindinggear.com), [Kylotonn](https://www.kylotonn.com), [Next Level Games](https://www.nextlevelgames.com), [O-Net Communications (USA)](http://en.o-netcom.com) + +Please see [detailed list of Dear ImGui supporters](https://github.com/ocornut/imgui/wiki/Sponsors) for past sponsors. +From November 2014 to December 2019, ongoing development has also been financially supported by its users on Patreon and through individual donations. + +**THANK YOU to all past and present supporters for helping to keep this project alive and thriving!** + +Dear ImGui is using software and services provided free of charge for open source projects: +- [PVS-Studio](https://www.viva64.com/en/b/0570/) for static analysis. +- [GitHub actions](https://github.com/features/actions) for continuous integration systems. +- [OpenCppCoverage](https://github.com/OpenCppCoverage/OpenCppCoverage) for code coverage analysis. + +Credits +------- + +Developed by [Omar Cornut](https://www.miracleworld.net) and every direct or indirect [contributors](https://github.com/ocornut/imgui/graphs/contributors) to the GitHub. The early version of this library was developed with the support of [Media Molecule](https://www.mediamolecule.com) and first used internally on the game [Tearaway](https://tearaway.mediamolecule.com) (PS Vita). + +Recurring contributors (2020): Omar Cornut [@ocornut](https://github.com/ocornut), Rokas Kupstys [@rokups](https://github.com/rokups), Ben Carter [@ShironekoBen](https://github.com/ShironekoBen). +A large portion of work on automation systems, regression tests and other features are currently unpublished. + +Sponsoring, support contracts and other B2B transactions are hosted and handled by [Lizardcube](https://www.lizardcube.com). + +Omar: "I first discovered the IMGUI paradigm at [Q-Games](https://www.q-games.com) where Atman Binstock had dropped his own simple implementation in the codebase, which I spent quite some time improving and thinking about. It turned out that Atman was exposed to the concept directly by working with Casey. When I moved to Media Molecule I rewrote a new library trying to overcome the flaws and limitations of the first one I've worked with. It became this library and since then I have spent an unreasonable amount of time iterating and improving it." + +Embeds [ProggyClean.ttf](http://upperbounds.net) font by Tristan Grimmer (MIT license). + +Embeds [stb_textedit.h, stb_truetype.h, stb_rect_pack.h](https://github.com/nothings/stb/) by Sean Barrett (public domain). + +Inspiration, feedback, and testing for early versions: Casey Muratori, Atman Binstock, Mikko Mononen, Emmanuel Briney, Stefan Kamoda, Anton Mikhailov, Matt Willis. Also thank you to everyone posting feedback, questions and patches on GitHub. + +License +------- + +Dear ImGui is licensed under the MIT License, see [LICENSE.txt](https://github.com/ocornut/imgui/blob/master/LICENSE.txt) for more information. + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Docs/TODO.txt`: + +```txt +dear imgui +ISSUES & TODO LIST + +Issue numbers (#) refer to github issues listed at https://github.com/ocornut/imgui/issues/XXXX +The list below consist mostly of ideas noted down before they are requested/discussed by users (at which point they usually exist on the github issue tracker). +It's mostly a bunch of personal notes, probably incomplete. Feel free to query if you have any questions. + + - doc/test: add a proper documentation+regression testing system (#435) + - doc/test: checklist app to verify backends/integration of imgui (test inputs, rendering, callback, etc.). + - doc/tips: tips of the day: website? applet in imgui_club? + - doc/wiki: work on the wiki https://github.com/ocornut/imgui/wiki + + - window: preserve/restore relative focus ordering (persistent or not) (#2304) -> also see docking reference to same #. + - window: calling SetNextWindowSize() every frame with <= 0 doesn't do anything, may be useful to allow (particularly when used for a single axis). (#690) + - window: add a way for very transient windows (non-saved, temporary overlay over hundreds of objects) to "clean" up from the global window list. perhaps a lightweight explicit cleanup pass. + - window: auto-fit feedback loop when user relies on any dynamic layout (window width multiplier, column) appears weird to end-user. clarify. + - window: begin with *p_open == false could return false. + - window: get size/pos helpers given names (see discussion in #249) + - window: a collapsed window can be stuck behind the main menu bar? + - window: when window is very small, prioritize resize button over close button. + - window: detect extra End() call that pop the "Debug" window out and assert at End() call site instead of at end of frame. + - window: increase minimum size of a window with menus or fix the menu rendering so that it doesn't look odd. + - window: double-clicking on title bar to minimize isn't consistent, perhaps move to single-click on left-most collapse icon? + - window: expose contents size. (#1045) + - window: using SetWindowPos() inside Begin() and moving the window with the mouse reacts a very ugly glitch. We should just defer the SetWindowPos() call. + - window: GetWindowSize() returns (0,0) when not calculated? (#1045) + - window: investigate better auto-positioning for new windows. + - window: top most window flag? (#2574) + - window/size: manually triggered auto-fit (double-click on grip) shouldn't resize window down to viewport size? + - window/size: how to allow to e.g. auto-size vertically to fit contents, but be horizontally resizable? Assuming SetNextWindowSize() is modified to treat -1.0f on each axis as "keep as-is" (would be good but might break erroneous code): Problem is UpdateWindowManualResize() and lots of code treat (window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) together. + - window/opt: freeze window flag: if not focused/hovered, return false, render with previous ImDrawList. and/or reduce refresh rate. -> this may require enforcing that it is illegal to submit contents if Begin returns false. + - window/child: background options for child windows, border option (disable rounding). + - window/child: allow resizing of child windows (possibly given min/max for each axis?.) + - window/child: the first draw command of a child window could be moved into the current draw command of the parent window (unless child+tooltip?). + - window/child: border could be emitted in parent as well. + - window/child: allow SetNextWindowContentSize() to work on child windows. + - window/clipping: some form of clipping when DisplaySize (or corresponding viewport) is zero. + - window/tabbing: add a way to signify that a window or docked window requires attention (e.g. blinking title bar). + - window/id_stack: add e.g. window->GetIDFromPath() with support for leading / and ../ (#1390, #331) + ! scrolling: exposing horizontal scrolling with Shift+Wheel even when scrollbar is disabled expose lots of issues (#2424, #1463) + - scrolling: while holding down a scrollbar, try to keep the same contents visible (at least while not moving mouse) + - scrolling: allow immediately effective change of scroll after Begin() if we haven't appended items yet. + - scrolling: forward mouse wheel scrolling to parent window when at the edge of scrolling limits? (useful for listbox,tables?) + - scrolling/clipping: separator on the initial position of a window is not visible (cursorpos.y <= clippos.y). (2017-08-20: can't repro) + - scrolling/style: shadows on scrollable areas to denote that there is more contents (see e.g. DaVinci Resolve ui) + + - drawdata: make it easy to clone (or swap?) a full ImDrawData so user can easily save that data if they use threaded rendering. (e.g. #2646) + ! drawlist: add calctextsize func to facilitate consistent code from user pov (currently need to use ImGui or ImFont alternatives!) + - drawlist: end-user probably can't call Clear() directly because we expect a texture to be pushed in the stack. + - drawlist: maintaining bounding box per command would allow to merge draw command when clipping isn't relied on (typical non-scrolling window or non-overflowing column would merge with previous command). + - drawlist: primitives/helpers to manipulate vertices post submission, so e.g. a quad/rect can be resized to fit later submitted content, _without_ using the ChannelSplit api + - drawlist: make it easier to toggle AA per primitive, so we can use e.g. non-AA fill + AA borders more naturally + - drawlist: non-AA strokes have gaps between points (#593, #288), glitch especially on RenderCheckmark() and ColorPicker4(). + - drawlist: would be good to be able to deep copy of ImDrawData (we have a deep copy of ImDrawList now). + - drawlist: rendering: provide a way for imgui to output to a single/global vertex buffer, re-order indices only at the end of the frame (ref: https://gist.github.com/floooh/10388a0afbe08fce9e617d8aefa7d302) + - drawlist: callback: add an extra void* in ImDrawCallback to allow passing render-local data to the callback (would break API). + - drawlist: AddRect vs AddLine position confusing (#2441) + - drawlist: channel splitter should be external helper and not stored in ImDrawList. + - drawlist: Add quadratic bezier curves? (#3127) + - drawlist/opt: store rounded corners in texture to use 1 quad per corner (filled and wireframe) to lower the cost of rounding. (#1962) + - drawlist/opt: AddRect() axis aligned pixel aligned (no-aa) could use 8 triangles instead of 16 and no normal calculation. + - drawlist/opt: thick AA line could be doable in same number of triangles as 1.0 AA line by storing gradient+full color in atlas. + + - main: find a way to preserve relative orders of multiple reappearing windows (so an app toggling between "modes" e.g. fullscreen vs all tools) won't lose relative ordering. + - main: IsItemHovered() make it more consistent for various type of widgets, widgets with multiple components, etc. also effectively IsHovered() region sometimes differs from hot region, e.g tree nodes + - main: IsItemHovered() info stored in a stack? so that 'if TreeNode() { Text; TreePop; } if IsHovered' return the hover state of the TreeNode? + + - widgets: display mode: widget-label, label-widget (aligned on column or using fixed size), label-newline-tab-widget etc. (#395) + - widgets: clean up widgets internal toward exposing everything and stabilizing imgui_internals.h. + - widgets: add visuals for Disabled/ReadOnly mode and expose publicly (#211) + - widgets: add always-allow-overlap mode. This should perhaps be the default? one problem is that highlight after mouse-wheel scrolling gets deferred, makes scrolling more flickery. + - widgets: start exposing PushItemFlag() and ImGuiItemFlags + - widgets: alignment options in style (e.g. center Selectable, Right-Align within Button, etc.) #1260 + - widgets: activate by identifier (trigger button, focus given id) + - widgets: a way to represent "mixed" values, so e.g. all values replaced with *, including check-boxes, colors, etc. with support for multi-components widgets (e.g. SliderFloat3, make only "Y" mixed) (#2644) + - widgets: checkbox: checkbox with custom glyph inside frame. + - widgets: coloredit: keep reporting as active when picker is on? + - widgets: group/scalarn functions: expose more per-component information. e.g. store NextItemData.ComponentIdx set by scalarn function, groups can expose them back somehow. + - selectable: using (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported. + - selectable: generic BeginSelectable()/EndSelectable() mechanism. + - selectable: a way to visualize partial/mixed selection (e.g. parent tree node has children with mixed selection) + + - input text: clean up the mess caused by converting UTF-8 <> wchar. the code is rather inefficient right now and super fragile. + - input text: preserve scrolling when unfocused? + - input text: reorganize event handling, allow CharFilter to modify buffers, allow multiple events? (#541) + - input text: expose CursorPos in char filter event (#816) + - input text: try usage idiom of using InputText with data only exposed through get/set accessors, without extraneous copy/alloc. (#3009) + - input text: access public fields via a non-callback API e.g. InputTextGetState("xxx") that may return NULL if not active. + - input text: flag to disable live update of the user buffer (also applies to float/int text input) (#701) + - input text: hover tooltip could show unclamped text + - input text: support for INSERT key to toggle overwrite mode. currently disabled because stb_textedit behavior is unsatisfactory on multi-line. (#2863) + - input text: option to Tab after an Enter validation. + - input text: add ImGuiInputTextFlags_EnterToApply? (off #218) + - input text: easier ways to update buffer (from source char*) while owned. preserve some sort of cursor position for multi-line text. + - input text: add flag (e.g. ImGuiInputTextFlags_EscapeClearsBuffer) to clear instead of revert. what to do with focus? (also see #2890) + - input text: add discard flag (e.g. ImGuiInputTextFlags_DiscardActiveBuffer) or make it easier to clear active focus for text replacement during edition (#725) + - input text: display bug when clicking a drag/slider after an input text in a different window has all-selected text (order dependent). actually a very old bug but no one appears to have noticed it. + - input text: allow centering/positioning text so that ctrl+clicking Drag or Slider keeps the textual value at the same pixel position. + - input text: decorrelate layout from inputs - e.g. what's the easiest way to implement a nice IP/Mac address input editor? + - input text: global callback system so user can plug in an expression evaluator easily. (#1691) + - input text: force scroll to end or scroll to a given line/contents (so user can implement a log or a search feature) + - input text: a way to preview completion (e.g. disabled text completing from the cursor) + - input text: a side bar that could e.g. preview where errors are. probably left to the user to draw but we'd need to give them the info there. + - input text: a way for the user to provide syntax coloring. + - input text: Shift+TAB with ImGuiInputTextFlags_AllowTabInput could eat preceding blanks, up to tab_count. + - input text: facilitate patterns like if (InputText(..., obj.get_string_ref()) { obj.set_string(...); } relying on internally held buffer. + - input text multi-line: don't directly call AddText() which does an unnecessary vertex reserve for character count prior to clipping. and/or more line-based clipping to AddText(). and/or reorganize TextUnformatted/RenderText for more efficiency for large text (e.g TextUnformatted could clip and log separately, etc). + - input text multi-line: support for cut/paste without selection (cut/paste the current line) + - input text multi-line: line numbers? status bar? (follow up on #200) + - input text multi-line: behave better when user changes input buffer while editing is active (even though it is illegal behavior). namely, the change of buffer can create a scrollbar glitch (#725) + - input text multi-line: better horizontal scrolling support (#383, #1224) + - input text multi-line: single call to AddText() should be coarse clipped on InputTextEx() end. + - input number: optional range min/max for Input*() functions + - input number: holding [-]/[+] buttons could increase the step speed non-linearly (or user-controlled) + - input number: use mouse wheel to step up/down + + - layout: helper or a way to express ImGui::SameLine(ImGui::GetCursorStartPos().x + ImGui::CalcItemWidth() + ImGui::GetStyle().ItemInnerSpacing.x); in a simpler manner. + - layout, font: horizontal tab support, A) text mode: forward only tabs (e.g. every 4 characters/N pixels from pos x1), B) manual mode: explicit tab stops acting as mini columns, no clipping (for menu items, many kind of uses, also vaguely relate to #267, #395) + - layout: horizontal layout helper (#97) + - layout: horizontal flow until no space left (#404) + - layout: more generic alignment state (left/right/centered) for single items? + - layout: clean up the InputFloatN/SliderFloatN/ColorEdit4 layout code. item width should include frame padding. + - layout: vertical alignment of mixed height items (e.g. buttons) within a same line (#1284) + - layout: null layout mode were items are not rendered but user can query GetItemRectMin()/Max/Size. + - layout: (R&D) local multi-pass layout mode. + - layout: (R&D) bind authored layout data (created by an off-line tool), items fetch their pos/size at submission, self-optimize data structures to stable linear access. + + - group: BeginGroup() needs a border option. (~#1496) + - group: IsHovered() after EndGroup() covers whole aabb rather than the intersection of individual items. Is that desirable? + - group: merge deactivation/activation within same group (fwd WasEdited flag). (#2550) + +!- color: the color conversion helpers/types are a mess and needs sorting out. + - color: (api breaking) ImGui::ColorConvertXXX functions should be loose ImColorConvertXX to match imgui_internals.h + + - plot: full featured plot/graph api w/ scrolling, zooming etc. all bell & whistle. why not! + - plot: PlotLines() should use the polygon-stroke facilities, less vertices (currently issues with averaging normals) + - plot: make it easier for user to draw extra stuff into the graph (e.g: draw basis, highlight certain points, 2d plots, multiple plots) + - plot: "smooth" automatic scale over time, user give an input 0.0(full user scale) 1.0(full derived from value) + - plot: option/feature: draw the zero line + - plot: option/feature: draw grid, vertical markers + - plot: option/feature: draw unit + - plot: add a helper e.g. Plot(char* label, float value, float time_span=2.0f) that stores values and Plot them for you - probably another function name. and/or automatically allow to plot ANY displayed value (more reliance on stable ID) + + - clipper: ability to disable the clipping through a simple flag/bool. + - clipper: ability to run without knowing full count in advance. + - clipper: horizontal clipping support. (#2580) + + - separator: expose flags (#759) + - separator: take indent into consideration (optional) + - separator: width, thickness, centering (#1643) + - splitter: formalize the splitter idiom into an official api (we want to handle n-way split) (#319) + + - dock: merge docking branch (#2109) + - dock: dock out from a collapsing header? would work nicely but need emitting window to keep submitting the code. + + - tabs: "there is currently a problem because TabItem() will try to submit their own tooltip after 0.50 second, and this will have the effect of making your tooltip flicker once." -> tooltip priority work + - tabs: close button tends to overlap unsaved-document star + - tabs: consider showing the star at the same spot as the close button, like VS Code does. + - tabs: make EndTabBar fail if users doesn't respect BeginTabBar return value, for consistency/future-proofing. + - tabs: persistent order/focus in BeginTabBar() api (#261, #351) + - tabs: TabItem could honor SetNextItemWidth()? + - tabs: explicit api (even if internal) to cleanly manipulate tab order. + - tabs: Mouse wheel over tab bar could scroll? (#2702) + + - image/image button: misalignment on padded/bordered button? + - image/image button: parameters are confusing, image() has tint_col,border_col whereas imagebutton() has bg_col/tint_col. Even thou they are different parameters ordering could be more consistent. can we fix that? + - image button: not taking an explicit id can be problematic. (#2464, #1390) + - button: provide a button that looks framed. (?) + - slider/drag: ctrl+click when format doesn't include a % character.. disable? display underlying value in default format? (see TempInputTextScalar) + - slider: allow using the [-]/[+] buttons used by InputFloat()/InputInt() + - slider: initial absolute click is imprecise. change to relative movement slider (same as scrollbar). (#1946) + - slider: add dragging-based widgets to edit values with mouse (on 2 axises), saving screen real-estate. + - slider: tint background based on value (e.g. v_min -> v_max, or use 0.0f either side of the sign) + - slider: relative dragging? + precision dragging + - slider: step option (#1183) + - slider: style: fill % of the bar instead of positioning a drag. + - knob: rotating knob widget (#942) + - drag float: support for reversed drags (min > max) (removed is_locked, also see fdc526e) + - drag float: up/down axis + - drag float: power != 0.0f with current value being outside the range keeps the value stuck. + - drag float: added leeway on edge (e.g. a few invisible steps past the clamp limits) + + - combo: use clipper: make it easier to disable clipper with a single flag. + - combo: flag for BeginCombo to not return true when unchanged (#1182) + - combo: a way/helper to customize the combo preview (#1658) -> exeperimental BeginComboPreview() + - combo/listbox: keyboard control. need InputText-like non-active focus + key handling. considering keyboard for custom listbox (pr #203) + - listbox: multiple selection. + - listbox: unselect option (#1208) + - listbox: make it easier/more natural to implement range-select (need some sort of info/ref about the last clicked/focused item that user can translate to an index?) (wip stash) + - listbox: user may want to initial scroll to focus on the one selected value? + - listbox: expose hovered item for a simplified ListBox api + - listbox: keyboard navigation. + - listbox: disable capturing mouse wheel if the listbox has no scrolling. (#1681) + - listbox: scrolling should track modified selection. + - listbox: future api should allow to enable horizontal scrolling (#2510) + +!- popups/menus: clarify usage of popups id, how MenuItem/Selectable closing parent popups affects the ID, etc. this is quite fishy needs improvement! (#331, #402) + - modals: make modal title bar blink when trying to click outside the modal + - modals: technically speaking, we could make Begin() with ImGuiWindowFlags_Modal work without involving popup. May help untangle a few things, as modals are more like regular windows than popups. + - popups: if the popup functions took explicit ImGuiID it would allow the user to manage the scope of those ID. (#331) + - popups: clicking outside (to close popup) and holding shouldn't drag window below. + - popups: add variant using global identifier similar to Begin/End (#402) + - popups: border options. richer api like BeginChild() perhaps? (#197) + - popups: flags could be reworked to allow both mouse buttons as index (0..5 and as flags using higher-bit) allowing to or them. + - popups/modals: although it is sometimes convenient that popups/modals lifetime is owned by imgui, we could also a bool-owned-by-user api as long as Begin() return value testing is enforced. + + - tooltip: drag and drop with tooltip near monitor edges lose/changes its last direction instead of locking one. The drag and drop tooltip should always follow without changing direction. + - tooltip: allow to set the width of a tooltip to allow TextWrapped() etc. while keeping the height automatic. + - tooltip: tooltips with delay timers? or general timer policy? (instantaneous vs timed): IsItemHovered() with timer + implicit aabb-id for items with no ID. (#1485) + - tooltip: drag tooltip hovering over source widget with IsItemHovered/SetTooltip flickers. + + - menus: menu bars inside modal windows are acting weird. + - status-bar: add a per-window status bar helper similar to what menu-bar does. + - shortcuts: local-style shortcut api, e.g. parse "&Save" + - shortcuts,menus: global-style shortcut api e.g. "Save (CTRL+S)" -> explicit flag for recursing into closed menu + - shortcuts: programmatically access shortcuts "Focus("&Save")) + - menus: hovering a disabled BeginMenu or MenuItem won't close another menu + - menus: menu-bar: main menu-bar could affect clamping of windows position (~ akin to modifying DisplayMin) + - menus: hovering from menu to menu on a menu-bar has 1 frame without any menu, which is a little annoying. ideally either 0 either longer. + - menus: could merge draw call in most cases (how about storing an optional aabb in ImDrawCmd to move the burden of merging in a single spot). + - menus: would be nice if the Selectable() supported horizontal alignment (must be given the equivalent of WorkRect.Max.x matching the position of the shortcut column) + + - tree node: add treenode/treepush int variants? not there because (void*) cast from int warns on some platforms/settings? + - tree node: try to apply scrolling at time of TreePop() if node was just opened and end of node is past scrolling limits? + - tree node / selectable render mismatch which is visible if you use them both next to each other (e.g. cf. property viewer) + - tree node: tweak color scheme to distinguish headers from selected tree node (#581) + - tree node: leaf/non-leaf highlight mismatch. + - tree node: flag to disable formatting and/or detect "%s" + - tree node/opt: could avoid formatting when clipped (flag assuming we don't care about width/height, assume single line height? format only %s/%c to be able to count height?) + + - settings: write more decent code to allow saving/loading new fields: columns, selected tree nodes? + - settings: api for per-tool simple persistent data (bool,int,float,columns sizes,etc.) in .ini file (#437) + - settings/persistence: helpers to make TreeNodeBehavior persist (even during dev!) - may need to store some semantic and/or data type in ImGuiStoragePair + + - style: better default styles. (#707) + - style: PushStyleVar: allow direct access to individual float X/Y elements. + - style: add a highlighted text color (for headers, etc.) + - style: border types: out-screen, in-screen, etc. (#447) + - style: add window shadow (fading away from the window. Paint-style calculation of vertices alpha after drawlist would be easier) + - style: a concept of "compact style" that the end-user can easily rely on (e.g. PushStyleCompact()?) that maps to other settings? avoid implementing duplicate helpers such as SmallCheckbox(), etc. + - style: try to make PushStyleVar() more robust to incorrect parameters (to be more friendly to edit & continues situation). + - style: global scale setting. + - style: FramePadding could be different for up vs down (#584) + - style: WindowPadding needs to be EVEN as the 0.5 multiplier used on this value probably have a subtle effect on clip rectangle + - style: have a more global HSV setter (e.g. alter hue on all elements). consider replacing active/hovered by offset in HSV space? (#438, #707, #1223) + - style: gradients fill (#1223) ~ 2 bg colors for each fill? tricky with rounded shapes and using textures for corners. + - style editor: color child window height expressed in multiple of line height. + + - log: improve logging of ArrowButton, ListBox, TabItem + - log: carry on indent / tree depth when opening a child window + - log: enabling log ends up pushing and growing vertices buffers because we don't distinguish layout vs render clipping + - log: have more control over the log scope (e.g. stop logging when leaving current tree node scope) + - log: be able to log anything (e.g. right-click on a window/tree-node, shows context menu? log into tty/file/clipboard) + - log: let user copy any window content to clipboard easily (CTRL+C on windows? while moving it? context menu?). code is commented because it fails with multiple Begin/End pairs. + - log: obsolete LogButtons() all together. + - log: LogButtons() options for specifying depth and/or hiding depth slider + + - filters: set a current filter that tree node can automatically query to hide themselves + - filters: handle wild-cards (with implicit leading/trailing *), reg-exprs + - filters: fuzzy matches (may use code at blog.forrestthewoods.com/4cffeed33fdb) + + - drag and drop: fix/support/options for overlapping drag sources. + - drag and drop: focus drag target window on hold (even without open) + - drag and drop: releasing a drop shows the "..." tooltip for one frame - since e13e598 (#1725) + - drag and drop: drag source on a group object (would need e.g. an invisible button covering group in EndGroup) https://twitter.com/paniq/status/1121446364909535233 + - drag and drop: have some way to know when a drag begin from BeginDragDropSource() pov. (see 2018/01/11 post in #143) + - drag and drop: allow preview tooltip to be submitted from a different place than the drag source. (#1725) + - drag and drop: allow using with other mouse buttons (where activeid won't be set). (#1637) + - drag and drop: make it easier and provide a demo to have tooltip both are source and target site, with a more detailed one on target site (tooltip ordering problem) + - drag and drop: demo with reordering nodes (in a list, or a tree node). (#143) + - drag and drop: test integrating with os drag and drop (make it easy to do a naive WM_DROPFILE integration) + - drag and drop: allow for multiple payload types. (#143) + - drag and drop: make payload optional? payload promise? (see 2018/01/11 post in #143) + - drag and drop: (#143) "both an in-process pointer and a promise to generate a serialized version, for whether the drag ends inside or outside the same process" + - drag and drop: feedback when hovering a region blocked by modal (mouse cursor "NO"?) + + - node/graph editors (#306) (also see https://github.com/ocornut/imgui/wiki#node-editors) + - pie menus patterns (#434) + - markup: simple markup language for color change? (#902) + + - text: selectable text (for copy) as a generic feature (ItemFlags?) + - text: proper alignment options in imgui_internal.h + - text: it's currently impossible to have a window title with "##". perhaps an official workaround would be nice. \ style inhibitor? non-visible ascii code to insert between #? + - text: provided a framed text helper, e.g. https://pastebin.com/1Laxy8bT + - text: refactor TextUnformatted (or underlying function) to more explicitly request if we need width measurement or not + - text/layout/tabs: \t pulling position from base pos + step, or offset array (e.g. could be used in text edit, menus for simple icon+text alignment, etc.) + - text link/url button: underlined. should api expose an ID or use text contents as ID? which colors enum to use? + - text/wrapped: should be a more first-class citizen, e.g. wrapped text within a Selectable with known width. + - text/wrapped: custom separator for text wrapping. (#3002) + - text/wrapped: figure out better way to use TextWrapped() in an always auto-resize context (tooltip, etc.) (#249) + + - font: arbitrary line spacing. (#2945) + - font: MergeMode: flags to select overwriting or not (this is now very easy with refactored ImFontAtlasBuildWithStbTruetype) + - font: free the Alpha buffer if user only requested RGBA. +!- font: better CalcTextSizeA() API, at least for simple use cases. current one is horrible (perhaps have simple vs extended versions). + - font: for the purpose of RenderTextEllipsis(), it might be useful that CalcTextSizeA() can ignore the trailing padding? + - font: a CalcTextHeight() helper could run faster than CalcTextSize().y + - font: enforce monospace through ImFontConfig (for icons?) + create dual ImFont output from same input, reusing rasterized data but with different glyphs/AdvanceX + - font: finish CustomRectRegister() to allow mapping Unicode codepoint to custom texture data + - font: remove ID from CustomRect registration, it seems unnecessary! + - font: make it easier to submit own bitmap font (same texture, another texture?). (#2127, #2575) + - font: PushFontSize API (#1018) + - font: MemoryTTF taking ownership confusing/not obvious, maybe default should be opposite? + - font: storing MinAdvanceX per font would allow us to skip calculating line width (under a threshold of character count) in loops looking for block width + - font/demo: add tools to show glyphs used by a text blob, display U16 value, list missing glyphs. + - font/demo: demonstrate use of ImFontGlyphRangesBuilder. + - font/atlas: add a missing Glyphs.reserve() + - font/atlas: incremental updates + - font/atlas: dynamic font atlas to avoid baking huge ranges into bitmap and make scaling easier. + - font/draw: vertical and/or rotated text renderer (#705) - vertical is easier clipping wise + - font/draw: need to be able to specify wrap start position. + - font/draw: better reserve policy for large horizontal block of text (shouldn't reserve for all clipped lines). also see #3349. + - font/draw: fix for drawing 16k+ visible characters in same call. + - font/draw: underline, squiggle line rendering helpers. + - font: optimization: for monospace font (like the default one) we can trim IndexXAdvance as long as trailing value is == FallbackXAdvance (need to make sure TAB is still correct), would save on cache line. + - font: add support for kerning, probably optional. A) perhaps default to (32..128)^2 matrix ~ 9K entries = 36KB, then hash for non-ascii?. B) or sparse lookup into per-char list? + - font: add a simpler CalcTextSizeA() api? current one ok but not welcome if user needs to call it directly (without going through ImGui::CalcTextSize) + - font: fix AddRemapChar() to work before atlas has been built. + - font: support for unicode codepoints higher than 0xFFFF? (pr #2815) + - font: (api breaking) remove "TTF" from symbol names. also because it now supports OTF. + - font/opt: Considering storing standalone AdvanceX table as 16-bit fixed point integer? + - font/opt: Glyph currently 40 bytes (2+9*4). Consider storing UV as 16 bits integer? (->32 bytes). X0/Y0/X1/Y1 as 16 fixed-point integers? Or X0/Y0 as float and X1/Y1 as fixed8_8? + + - nav: some features such as PageUp/Down/Home/End should probably work without ImGuiConfigFlags_NavEnableKeyboard? (where do we draw the line?) + - nav: configuration flag to disable global shortcuts (currently only CTRL-Tab) ? + ! nav: never clear NavId on some setup (e.g. gamepad centric) + - nav: scroll up/down if possible when move request fails + - nav: there's currently no way to completely clear focus with the keyboard. depending on patterns used by the application to dispatch inputs, it may be desirable. + - nav: code to focus child-window on restoring NavId appears to have issue: e.g. when focus change is implicit because of window closure. + - nav: Home/End behavior when navigable item is not fully visible at the edge of scrolling? should be backtrack to keep item into view? + - nav: NavScrollToBringItemIntoView() with item bigger than view should focus top-right? Repro: using Nav in "About Window" + - nav: wrap around logic to allow e.g. grid based layout (pressing NavRight on the right-most element would go to the next row, etc.). see internal's NavMoveRequestTryWrapping(). + - nav: patterns to make it possible for arrows key to update selection (see JustMovedTo in range_select branch) + - nav: restore/find nearest NavId when current one disappear (e.g. pressed a button that disappear, or perhaps auto restoring when current button change name) + - nav: SetItemDefaultFocus() level of priority, so widget like Selectable when inside a popup could claim a low-priority default focus on the first selected iem + - nav: NavFlattened: init requests don't work properly on flattened siblings. + - nav: NavFlattened: pageup/pagedown/home/end don't work properly on flattened siblings. + - nav: NavFlattened: ESC on a flattened child should select something. + - nav: NavFlattened: broken: in typical usage scenario, the items of a fully clipped child are currently not considered to enter into a NavFlattened child. + - nav: NavFlattened: cannot access menu-bar of a flattened child window with Alt/menu key (not a very common use case..). + - nav: simulate right-click or context activation? (SHIFT+F10) + - nav/popup: esc/enter default behavior for popups, e.g. be able to mark an "ok" or "cancel" button that would get triggered by those keys, default validation button, etc. + - nav/treenode: left within a tree node block as a fallback (ImGuiTreeNodeFlags_NavLeftJumpsBackHere by default?) + - nav/menus: pressing left-right on a vertically clipped menu bar tends to jump to the collapse/close buttons. + - nav/menus: allow pressing Menu to leave a sub-menu. + - nav/menus: a way to access the main menu bar with Alt? (currently needs CTRL+TAB) or last focused window menu bar? + - nav/menus: when using the main menu bar, even though we restore focus after, the underlying window loses its title bar highlight during menu manipulation. could we prevent it? + - nav/menus: main menu bar currently cannot restore a NULL focus. Could save NavWindow at the time of being focused, similarly to what popup do? + - nav/menus: Alt,Up could open the first menu (e.g. "File") currently it tends to nav into the window/collapse menu. Do do that we would need custom transition? + - nav/windowing: configure fade-in/fade-out delay on Ctrl+Tab? + - nav/windowing: when CTRL-Tab/windowing is active, the HoveredWindow detection doesn't take account of the window display re-ordering. + - nav/windowing: Resizing window will currently fail with certain types of resizing constraints/callback applied + - focus: preserve ActiveId/focus stack state, e.g. when opening a menu and close it, previously selected InputText() focus gets restored (#622) + + - inputs: we need an explicit flag about whether the imgui window is focused, to be able to distinguish focused key releases vs alt-tabbing all release behaviors. + - inputs: rework IO system to be able to pass actual ordered/timestamped events. use an event queue? (~#335, #71) + - inputs: support track pad style scrolling & slider edit. + - inputs/io: backspace and arrows in the context of a text input could use system repeat rate. + - inputs/io: clarify/standardize/expose repeat rate and repeat delays (#1808) + - inputs/scrolling: support for smooth scrolling (#2462, #2569) + + - misc: idle: expose "woken up" boolean (set by inputs) and/or animation time (for cursor blink) for backend to be able stop refreshing easily. + - misc: idle: if cursor blink if the _only_ visible animation, core imgui could rewrite vertex alpha to avoid CPU pass on ImGui:: calls. + - misc: idle: if cursor blink if the _only_ visible animation, could even expose a dirty rectangle that optionally can be leverage by some app to render in a smaller viewport, getting rid of much pixel shading cost. + - misc: no way to run a root-most GetID() with ImGui:: api since there's always a Debug window in the stack. (mentioned in #2960) + - misc: make the ImGuiCond values linear (non-power-of-two). internal storage for ImGuiWindow can use integers to combine into flags (Why?) + - misc: PushItemFlag(): add a flag to disable keyboard capture when used with mouse? (#1682) + - misc: use more size_t in public api? + - misc: possible compile-time support for string view/range instead of char* would e.g. facilitate usage with Rust (#683) + - misc: possible compile-time support for wchar_t instead of char*? + + - remote: make a system like RemoteImGui first-class citizen/project (#75) + + - demo: find a way to demonstrate textures in the examples application, as it such a common issue for new users. + - demo: demonstrate using PushStyleVar() in more details. + - demo: add vertical separator demo + - demo: add virtual scrolling example? + - demo: demonstrate Plot offset + - demo: window size constraint: square demo is broken when resizing from edges (#1975), would need to rework the callback system to solve this + + - examples: window minimize, maximize (#583) + - examples: provide a zero frame-rate/idle example. + - examples: dx11/dx12: try to use new swapchain blit models (#2970) + - backends: move to backends/ folder? + - backends: report it better when not able to create texture? + - backends: apple: example_apple should be using modern GL3. + - backends: glfw: could go idle when minimized? if (glfwGetWindowAttrib(window, GLFW_ICONIFIED)) { glfwWaitEvents(); continue; } // issue: DeltaTime will be super high on resume, perhaps provide a way to let impl know (#440) + - backends: opengl: rename imgui_impl_opengl2 to impl_opengl_legacy and imgui_impl_opengl3 to imgui_impl_opengl? (#1900) + - backends: opengl: could use a single vertex buffer and glBufferSubData for uploads? + - backends: opengl: explicitly disable GL_STENCIL_TEST in bindings. + - backends: vulkan: viewport: support for synchronized swapping of multiple swap chains. + - backends: bgfx: https://gist.github.com/RichardGale/6e2b74bc42b3005e08397236e4be0fd0 + - backends: mscriptem: with refactored examples, we could provide a direct imgui_impl_emscripten platform layer (see eg. https://github.com/floooh/sokol-samples/blob/master/html5/imgui-emsc.cc#L42) + + - bindings: ways to use clang ast dump to generate bindings or helpers for bindings? (e.g. clang++ -Xclang -ast-dump=json imgui.h) + + - optimization: replace vsnprintf with stb_printf? using IMGUI_USE_STB_SPRINTF.(#1038) + - optimization: add clipping for multi-component widgets (SliderFloatX, ColorEditX, etc.). one problem is that nav branch can't easily clip parent group when there is a move request. + - optimization: add a flag to disable most of rendering, for the case where the user expect to skip it (#335) + - optimization: fully covered window (covered by another with non-translucent bg + WindowRounding worth of padding) may want to clip rendering. + - optimization: use another hash function than crc32, e.g. FNV1a + - optimization/render: merge command-lists with same clip-rect into one even if they aren't sequential? (as long as in-between clip rectangle don't overlap)? + - optimization: turn some the various stack vectors into statically-sized arrays + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/ImGuiLibrary.Build.cs`: + +```cs +using UnrealBuildTool; + +public class ImGuiLibrary : ModuleRules +{ +#if WITH_FORWARDED_MODULE_RULES_CTOR + public ImGuiLibrary(ReadOnlyTargetRules Target) : base(Target) +#else + public ImGuiLibrary(TargetInfo Target) +#endif + { + Type = ModuleType.External; + } +} + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/ImGuiLibrary.tps`: + +```tps + + + +Dear ImGui source files re-packaged to be included in Unreal Engine. Original Omar Cornut's repository can be found at https://github.com/ocornut/imgui. + + + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Include/imconfig.h`: + +```h +//----------------------------------------------------------------------------- +// COMPILE-TIME OPTIONS FOR DEAR IMGUI +// Runtime options (clipboard callbacks, enabling various features, etc.) can generally be set via the ImGuiIO structure. +// You can use ImGui::SetAllocatorFunctions() before calling ImGui::CreateContext() to rewire memory allocation functions. +//----------------------------------------------------------------------------- +// A) You may edit imconfig.h (and not overwrite it when updating Dear ImGui, or maintain a patch/rebased branch with your modifications to it) +// B) or '#define IMGUI_USER_CONFIG "my_imgui_config.h"' in your project and then add directives in your own file without touching this template. +//----------------------------------------------------------------------------- +// You need to make sure that configuration settings are defined consistently _everywhere_ Dear ImGui is used, which include the imgui*.cpp +// files but also _any_ of your code that uses Dear ImGui. This is because some compile-time options have an affect on data structures. +// Defining those options in imconfig.h will ensure every compilation unit gets to see the same data structure layouts. +// Call IMGUI_CHECKVERSION() from your .cpp files to verify that the data structures your files are using are matching the ones imgui.cpp is using. +//----------------------------------------------------------------------------- + +#pragma once + +//---- Define assertion handler. Defaults to calling assert(). +// If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement. +//#define IM_ASSERT(_EXPR) MyAssert(_EXPR) +//#define IM_ASSERT(_EXPR) ((void)(_EXPR)) // Disable asserts + +//---- Define attributes of all API symbols declarations, e.g. for DLL under Windows +// Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility. +// DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() +// for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details. +//#define IMGUI_API __declspec( dllexport ) +//#define IMGUI_API __declspec( dllimport ) + +//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to avoid using soon-to-be obsolete function/names. +//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS +//#define IMGUI_DISABLE_OBSOLETE_KEYIO // 1.87: disable legacy io.KeyMap[]+io.KeysDown[] in favor io.AddKeyEvent(). This will be folded into IMGUI_DISABLE_OBSOLETE_FUNCTIONS in a few versions. + +//---- Disable all of Dear ImGui or don't implement standard windows. +// It is very strongly recommended to NOT disable the demo windows during development. Please read comments in imgui_demo.cpp. +//#define IMGUI_DISABLE // Disable everything: all headers and source files will be empty. +//#define IMGUI_DISABLE_DEMO_WINDOWS // Disable demo windows: ShowDemoWindow()/ShowStyleEditor() will be empty. Not recommended. +//#define IMGUI_DISABLE_METRICS_WINDOW // Disable metrics/debugger and other debug tools: ShowMetricsWindow() and ShowStackToolWindow() will be empty. + +//---- Don't implement some functions to reduce linkage requirements. +//#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS // [Win32] Don't implement default clipboard handler. Won't use and link with OpenClipboard/GetClipboardData/CloseClipboard etc. (user32.lib/.a, kernel32.lib/.a) +//#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with Visual Studio] Implement default IME handler (require imm32.lib/.a, auto-link for Visual Studio, -limm32 on command-line for MinGW) +//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a) +//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, ime). +//#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default). +//#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf) +//#define IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 so you can implement them yourself. +//#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies) +//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function. +//#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions(). +//#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available + +//---- Include imgui_user.h at the end of imgui.h as a convenience +//#define IMGUI_INCLUDE_IMGUI_USER_H + +//---- Pack colors to BGRA8 instead of RGBA8 (to avoid converting from one to another) +//#define IMGUI_USE_BGRA_PACKED_COLOR + +//---- Use 32-bit for ImWchar (default is 16-bit) to support unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...) +//#define IMGUI_USE_WCHAR32 + +//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version +// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files. +//#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h" +//#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h" +//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION + +//---- Use stb_printf's faster implementation of vsnprintf instead of the one from libc (unless IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS is defined) +// Requires 'stb_sprintf.h' to be available in the include path. Compatibility checks of arguments and formats done by clang and GCC will be disabled in order to support the extra formats provided by STB sprintf. +// #define IMGUI_USE_STB_SPRINTF + +//---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui) +// Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided). +// On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'. +//#define IMGUI_ENABLE_FREETYPE + +//---- Use stb_truetype to build and rasterize the font atlas (default) +// The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend. +//#define IMGUI_ENABLE_STB_TRUETYPE + +//---- Define constructor and implicit cast operators to convert back<>forth between your math types and ImVec2/ImVec4. +// This will be inlined as part of ImVec2 and ImVec4 class declarations. +/* +#define IM_VEC2_CLASS_EXTRA \ + ImVec2(const MyVec2& f) { x = f.x; y = f.y; } \ + operator MyVec2() const { return MyVec2(x,y); } + +#define IM_VEC4_CLASS_EXTRA \ + ImVec4(const MyVec4& f) { x = f.x; y = f.y; z = f.z; w = f.w; } \ + operator MyVec4() const { return MyVec4(x,y,z,w); } +*/ + +//---- Use 32-bit vertex indices (default is 16-bit) is one way to allow large meshes with more than 64K vertices. +// Your renderer backend will need to support it (most example renderer backends support both 16/32-bit indices). +// Another way to allow large meshes while keeping 16-bit indices is to handle ImDrawCmd::VtxOffset in your renderer. +// Read about ImGuiBackendFlags_RendererHasVtxOffset for details. +//#define ImDrawIdx unsigned int + +//---- Override ImDrawCallback signature (will need to modify renderer backends accordingly) +//struct ImDrawList; +//struct ImDrawCmd; +//typedef void (*MyImDrawCallback)(const ImDrawList* draw_list, const ImDrawCmd* cmd, void* my_renderer_user_data); +//#define ImDrawCallback MyImDrawCallback + +//---- Debug Tools: Macro to break in Debugger +// (use 'Metrics->Tools->Item Picker' to pick widgets with the mouse and break into them for easy debugging.) +//#define IM_DEBUG_BREAK IM_ASSERT(0) +//#define IM_DEBUG_BREAK __debugbreak() + +//---- Debug Tools: Have the Item Picker break in the ItemAdd() function instead of ItemHoverable(), +// (which comes earlier in the code, will catch a few extra items, allow picking items other than Hovered one.) +// This adds a small runtime cost which is why it is not enabled by default. +//#define IMGUI_DEBUG_TOOL_ITEM_PICKER_EX + +//---- Debug Tools: Enable slower asserts +//#define IMGUI_DEBUG_PARANOID + +//---- Tip: You can add extra functions within the ImGui:: namespace, here or in your own headers files. +/* +namespace ImGui +{ + void MyFunction(const char* name, const MyMatrix44& v); +} +*/ + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Include/imgui.h`: + +```h +// dear imgui, v1.87 +// (headers) + +// Help: +// - Read FAQ at http://dearimgui.org/faq +// - Newcomers, read 'Programmer guide' in imgui.cpp for notes on how to setup Dear ImGui in your codebase. +// - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. +// Read imgui.cpp for details, links and comments. + +// Resources: +// - FAQ http://dearimgui.org/faq +// - Homepage & latest https://github.com/ocornut/imgui +// - Releases & changelog https://github.com/ocornut/imgui/releases +// - Gallery https://github.com/ocornut/imgui/issues/4451 (please post your screenshots/video there!) +// - Wiki https://github.com/ocornut/imgui/wiki (lots of good stuff there) +// - Glossary https://github.com/ocornut/imgui/wiki/Glossary +// - Issues & support https://github.com/ocornut/imgui/issues + +// Getting Started? +// - For first-time users having issues compiling/linking/running or issues loading fonts: +// please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. + +/* + +Index of this file: +// [SECTION] Header mess +// [SECTION] Forward declarations and basic types +// [SECTION] Dear ImGui end-user API functions +// [SECTION] Flags & Enumerations +// [SECTION] Helpers: Memory allocations macros, ImVector<> +// [SECTION] ImGuiStyle +// [SECTION] ImGuiIO +// [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload, ImGuiTableSortSpecs, ImGuiTableColumnSortSpecs) +// [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, ImColor) +// [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData) +// [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) +// [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport) +// [SECTION] Platform Dependent Interfaces (ImGuiPlatformImeData) +// [SECTION] Obsolete functions and types + +*/ + +#pragma once + +// Configuration file with compile-time options (edit imconfig.h or '#define IMGUI_USER_CONFIG "myfilename.h" from your build system') +#ifdef IMGUI_USER_CONFIG +#include IMGUI_USER_CONFIG +#endif +#if !defined(IMGUI_DISABLE_INCLUDE_IMCONFIG_H) || defined(IMGUI_INCLUDE_IMCONFIG_H) +#include "imconfig.h" +#endif + +#ifndef IMGUI_DISABLE + +//----------------------------------------------------------------------------- +// [SECTION] Header mess +//----------------------------------------------------------------------------- + +// Includes +#include // FLT_MIN, FLT_MAX +#include // va_list, va_start, va_end +#include // ptrdiff_t, NULL +#include // memset, memmove, memcpy, strlen, strchr, strcpy, strcmp + +// Version +// (Integer encoded as XYYZZ for use in #if preprocessor conditionals. Work in progress versions typically starts at XYY99 then bounce up to XYY00, XYY01 etc. when release tagging happens) +#define IMGUI_VERSION "1.87" +#define IMGUI_VERSION_NUM 18700 +#define IMGUI_CHECKVERSION() ImGui::DebugCheckVersionAndDataLayout(IMGUI_VERSION, sizeof(ImGuiIO), sizeof(ImGuiStyle), sizeof(ImVec2), sizeof(ImVec4), sizeof(ImDrawVert), sizeof(ImDrawIdx)) +#define IMGUI_HAS_TABLE + +// Define attributes of all API symbols declarations (e.g. for DLL under Windows) +// IMGUI_API is used for core imgui functions, IMGUI_IMPL_API is used for the default backends files (imgui_impl_xxx.h) +// Using dear imgui via a shared library is not recommended, because we don't guarantee backward nor forward ABI compatibility (also function call overhead, as dear imgui is a call-heavy API) +#ifndef IMGUI_API +#define IMGUI_API +#endif +#ifndef IMGUI_IMPL_API +#define IMGUI_IMPL_API IMGUI_API +#endif + +// Helper Macros +#ifndef IM_ASSERT +#include +#define IM_ASSERT(_EXPR) assert(_EXPR) // You can override the default assert handler by editing imconfig.h +#endif +#define IM_ARRAYSIZE(_ARR) ((int)(sizeof(_ARR) / sizeof(*(_ARR)))) // Size of a static C-style array. Don't use on pointers! +#define IM_UNUSED(_VAR) ((void)(_VAR)) // Used to silence "unused variable warnings". Often useful as asserts may be stripped out from final builds. +#define IM_OFFSETOF(_TYPE,_MEMBER) offsetof(_TYPE, _MEMBER) // Offset of _MEMBER within _TYPE. Standardized as offsetof() in C++11 + +// Helper Macros - IM_FMTARGS, IM_FMTLIST: Apply printf-style warnings to our formatting functions. +#if !defined(IMGUI_USE_STB_SPRINTF) && defined(__MINGW32__) && !defined(__clang__) +#define IM_FMTARGS(FMT) __attribute__((format(gnu_printf, FMT, FMT+1))) +#define IM_FMTLIST(FMT) __attribute__((format(gnu_printf, FMT, 0))) +#elif !defined(IMGUI_USE_STB_SPRINTF) && (defined(__clang__) || defined(__GNUC__)) +#define IM_FMTARGS(FMT) __attribute__((format(printf, FMT, FMT+1))) +#define IM_FMTLIST(FMT) __attribute__((format(printf, FMT, 0))) +#else +#define IM_FMTARGS(FMT) +#define IM_FMTLIST(FMT) +#endif + +// Disable some of MSVC most aggressive Debug runtime checks in function header/footer (used in some simple/low-level functions) +#if defined(_MSC_VER) && !defined(__clang__) && !defined(__INTEL_COMPILER) && !defined(IMGUI_DEBUG_PARANOID) +#define IM_MSVC_RUNTIME_CHECKS_OFF __pragma(runtime_checks("",off)) __pragma(check_stack(off)) __pragma(strict_gs_check(push,off)) +#define IM_MSVC_RUNTIME_CHECKS_RESTORE __pragma(runtime_checks("",restore)) __pragma(check_stack()) __pragma(strict_gs_check(pop)) +#else +#define IM_MSVC_RUNTIME_CHECKS_OFF +#define IM_MSVC_RUNTIME_CHECKS_RESTORE +#endif + +// Warnings +#ifdef _MSC_VER +#pragma warning (push) +#pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#endif +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wold-style-cast" +#if __has_warning("-Wzero-as-null-pointer-constant") +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" +#endif +#elif defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Forward declarations and basic types +//----------------------------------------------------------------------------- + +// Forward declarations +struct ImDrawChannel; // Temporary storage to output draw commands out of order, used by ImDrawListSplitter and ImDrawList::ChannelsSplit() +struct ImDrawCmd; // A single draw command within a parent ImDrawList (generally maps to 1 GPU draw call, unless it is a callback) +struct ImDrawData; // All draw command lists required to render the frame + pos/size coordinates to use for the projection matrix. +struct ImDrawList; // A single draw command list (generally one per window, conceptually you may see this as a dynamic "mesh" builder) +struct ImDrawListSharedData; // Data shared among multiple draw lists (typically owned by parent ImGui context, but you may create one yourself) +struct ImDrawListSplitter; // Helper to split a draw list into different layers which can be drawn into out of order, then flattened back. +struct ImDrawVert; // A single vertex (pos + uv + col = 20 bytes by default. Override layout with IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT) +struct ImFont; // Runtime data for a single font within a parent ImFontAtlas +struct ImFontAtlas; // Runtime data for multiple fonts, bake multiple fonts into a single texture, TTF/OTF font loader +struct ImFontBuilderIO; // Opaque interface to a font builder (stb_truetype or FreeType). +struct ImFontConfig; // Configuration data when adding a font or merging fonts +struct ImFontGlyph; // A single font glyph (code point + coordinates within in ImFontAtlas + offset) +struct ImFontGlyphRangesBuilder; // Helper to build glyph ranges from text/string data +struct ImColor; // Helper functions to create a color that can be converted to either u32 or float4 (*OBSOLETE* please avoid using) +struct ImGuiContext; // Dear ImGui context (opaque structure, unless including imgui_internal.h) +struct ImGuiIO; // Main configuration and I/O between your application and ImGui +struct ImGuiInputTextCallbackData; // Shared state of InputText() when using custom ImGuiInputTextCallback (rare/advanced use) +struct ImGuiKeyData; // Storage for ImGuiIO and IsKeyDown(), IsKeyPressed() etc functions. +struct ImGuiListClipper; // Helper to manually clip large list of items +struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame +struct ImGuiPayload; // User data payload for drag and drop operations +struct ImGuiPlatformImeData; // Platform IME data for io.SetPlatformImeDataFn() function. +struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) +struct ImGuiStorage; // Helper for key->value storage +struct ImGuiStyle; // Runtime data for styling/colors +struct ImGuiTableSortSpecs; // Sorting specifications for a table (often handling sort specs for a single column, occasionally more) +struct ImGuiTableColumnSortSpecs; // Sorting specification for one column of a table +struct ImGuiTextBuffer; // Helper to hold and append into a text buffer (~string builder) +struct ImGuiTextFilter; // Helper to parse and apply text filters (e.g. "aaaaa[,bbbbb][,ccccc]") +struct ImGuiViewport; // A Platform Window (always only one in 'master' branch), in the future may represent Platform Monitor + +// Enums/Flags (declared as int for compatibility with old C++, to allow using as flags without overhead, and to not pollute the top of this file) +// - Tip: Use your programming IDE navigation facilities on the names in the _central column_ below to find the actual flags/enum lists! +// In Visual Studio IDE: CTRL+comma ("Edit.NavigateTo") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. +typedef int ImGuiCol; // -> enum ImGuiCol_ // Enum: A color identifier for styling +typedef int ImGuiCond; // -> enum ImGuiCond_ // Enum: A condition for many Set*() functions +typedef int ImGuiDataType; // -> enum ImGuiDataType_ // Enum: A primary data type +typedef int ImGuiDir; // -> enum ImGuiDir_ // Enum: A cardinal direction +typedef int ImGuiKey; // -> enum ImGuiKey_ // Enum: A key identifier +typedef int ImGuiNavInput; // -> enum ImGuiNavInput_ // Enum: An input identifier for navigation +typedef int ImGuiMouseButton; // -> enum ImGuiMouseButton_ // Enum: A mouse button identifier (0=left, 1=right, 2=middle) +typedef int ImGuiMouseCursor; // -> enum ImGuiMouseCursor_ // Enum: A mouse cursor identifier +typedef int ImGuiSortDirection; // -> enum ImGuiSortDirection_ // Enum: A sorting direction (ascending or descending) +typedef int ImGuiStyleVar; // -> enum ImGuiStyleVar_ // Enum: A variable identifier for styling +typedef int ImGuiTableBgTarget; // -> enum ImGuiTableBgTarget_ // Enum: A color target for TableSetBgColor() +typedef int ImDrawFlags; // -> enum ImDrawFlags_ // Flags: for ImDrawList functions +typedef int ImDrawListFlags; // -> enum ImDrawListFlags_ // Flags: for ImDrawList instance +typedef int ImFontAtlasFlags; // -> enum ImFontAtlasFlags_ // Flags: for ImFontAtlas build +typedef int ImGuiBackendFlags; // -> enum ImGuiBackendFlags_ // Flags: for io.BackendFlags +typedef int ImGuiButtonFlags; // -> enum ImGuiButtonFlags_ // Flags: for InvisibleButton() +typedef int ImGuiColorEditFlags; // -> enum ImGuiColorEditFlags_ // Flags: for ColorEdit4(), ColorPicker4() etc. +typedef int ImGuiConfigFlags; // -> enum ImGuiConfigFlags_ // Flags: for io.ConfigFlags +typedef int ImGuiComboFlags; // -> enum ImGuiComboFlags_ // Flags: for BeginCombo() +typedef int ImGuiDragDropFlags; // -> enum ImGuiDragDropFlags_ // Flags: for BeginDragDropSource(), AcceptDragDropPayload() +typedef int ImGuiFocusedFlags; // -> enum ImGuiFocusedFlags_ // Flags: for IsWindowFocused() +typedef int ImGuiHoveredFlags; // -> enum ImGuiHoveredFlags_ // Flags: for IsItemHovered(), IsWindowHovered() etc. +typedef int ImGuiInputTextFlags; // -> enum ImGuiInputTextFlags_ // Flags: for InputText(), InputTextMultiline() +typedef int ImGuiKeyModFlags; // -> enum ImGuiKeyModFlags_ // Flags: for io.KeyMods (Ctrl/Shift/Alt/Super) +typedef int ImGuiPopupFlags; // -> enum ImGuiPopupFlags_ // Flags: for OpenPopup*(), BeginPopupContext*(), IsPopupOpen() +typedef int ImGuiSelectableFlags; // -> enum ImGuiSelectableFlags_ // Flags: for Selectable() +typedef int ImGuiSliderFlags; // -> enum ImGuiSliderFlags_ // Flags: for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc. +typedef int ImGuiTabBarFlags; // -> enum ImGuiTabBarFlags_ // Flags: for BeginTabBar() +typedef int ImGuiTabItemFlags; // -> enum ImGuiTabItemFlags_ // Flags: for BeginTabItem() +typedef int ImGuiTableFlags; // -> enum ImGuiTableFlags_ // Flags: For BeginTable() +typedef int ImGuiTableColumnFlags; // -> enum ImGuiTableColumnFlags_// Flags: For TableSetupColumn() +typedef int ImGuiTableRowFlags; // -> enum ImGuiTableRowFlags_ // Flags: For TableNextRow() +typedef int ImGuiTreeNodeFlags; // -> enum ImGuiTreeNodeFlags_ // Flags: for TreeNode(), TreeNodeEx(), CollapsingHeader() +typedef int ImGuiViewportFlags; // -> enum ImGuiViewportFlags_ // Flags: for ImGuiViewport +typedef int ImGuiWindowFlags; // -> enum ImGuiWindowFlags_ // Flags: for Begin(), BeginChild() + +// ImTexture: user data for renderer backend to identify a texture [Compile-time configurable type] +// - To use something else than an opaque void* pointer: override with e.g. '#define ImTextureID MyTextureType*' in your imconfig.h file. +// - This can be whatever to you want it to be! read the FAQ about ImTextureID for details. +#ifndef ImTextureID +typedef void* ImTextureID; // Default: store a pointer or an integer fitting in a pointer (most renderer backends are ok with that) +#endif + +// ImDrawIdx: vertex index. [Compile-time configurable type] +// - To use 16-bit indices + allow large meshes: backend need to set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset' and handle ImDrawCmd::VtxOffset (recommended). +// - To use 32-bit indices: override with '#define ImDrawIdx unsigned int' in your imconfig.h file. +#ifndef ImDrawIdx +typedef unsigned short ImDrawIdx; // Default: 16-bit (for maximum compatibility with renderer backends) +#endif + +// Scalar data types +typedef unsigned int ImGuiID;// A unique ID used by widgets (typically the result of hashing a stack of string) +typedef signed char ImS8; // 8-bit signed integer +typedef unsigned char ImU8; // 8-bit unsigned integer +typedef signed short ImS16; // 16-bit signed integer +typedef unsigned short ImU16; // 16-bit unsigned integer +typedef signed int ImS32; // 32-bit signed integer == int +typedef unsigned int ImU32; // 32-bit unsigned integer (often used to store packed colors) +typedef signed long long ImS64; // 64-bit signed integer +typedef unsigned long long ImU64; // 64-bit unsigned integer + +// Character types +// (we generally use UTF-8 encoded string in the API. This is storage specifically for a decoded character used for keyboard input and display) +typedef unsigned short ImWchar16; // A single decoded U16 character/code point. We encode them as multi bytes UTF-8 when used in strings. +typedef unsigned int ImWchar32; // A single decoded U32 character/code point. We encode them as multi bytes UTF-8 when used in strings. +#ifdef IMGUI_USE_WCHAR32 // ImWchar [configurable type: override in imconfig.h with '#define IMGUI_USE_WCHAR32' to support Unicode planes 1-16] +typedef ImWchar32 ImWchar; +#else +typedef ImWchar16 ImWchar; +#endif + +// Callback and functions types +typedef int (*ImGuiInputTextCallback)(ImGuiInputTextCallbackData* data); // Callback function for ImGui::InputText() +typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData* data); // Callback function for ImGui::SetNextWindowSizeConstraints() +typedef void* (*ImGuiMemAllocFunc)(size_t sz, void* user_data); // Function signature for ImGui::SetAllocatorFunctions() +typedef void (*ImGuiMemFreeFunc)(void* ptr, void* user_data); // Function signature for ImGui::SetAllocatorFunctions() + +// ImVec2: 2D vector used to store positions, sizes etc. [Compile-time configurable type] +// This is a frequently used type in the API. Consider using IM_VEC2_CLASS_EXTRA to create implicit cast from/to our preferred type. +IM_MSVC_RUNTIME_CHECKS_OFF +struct ImVec2 +{ + float x, y; + ImVec2() { x = y = 0.0f; } + ImVec2(float _x, float _y) { x = _x; y = _y; } + float operator[] (size_t idx) const { IM_ASSERT(idx <= 1); return (&x)[idx]; } // We very rarely use this [] operator, the assert overhead is fine. + float& operator[] (size_t idx) { IM_ASSERT(idx <= 1); return (&x)[idx]; } // We very rarely use this [] operator, the assert overhead is fine. +#ifdef IM_VEC2_CLASS_EXTRA + IM_VEC2_CLASS_EXTRA // Define additional constructors and implicit cast operators in imconfig.h to convert back and forth between your math types and ImVec2. +#endif +}; + +// ImVec4: 4D vector used to store clipping rectangles, colors etc. [Compile-time configurable type] +struct ImVec4 +{ + float x, y, z, w; + ImVec4() { x = y = z = w = 0.0f; } + ImVec4(float _x, float _y, float _z, float _w) { x = _x; y = _y; z = _z; w = _w; } +#ifdef IM_VEC4_CLASS_EXTRA + IM_VEC4_CLASS_EXTRA // Define additional constructors and implicit cast operators in imconfig.h to convert back and forth between your math types and ImVec4. +#endif +}; +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] Dear ImGui end-user API functions +// (Note that ImGui:: being a namespace, you can add extra ImGui:: functions in your own separate file. Please don't modify imgui source files!) +//----------------------------------------------------------------------------- + +namespace ImGui +{ + // Context creation and access + // - Each context create its own ImFontAtlas by default. You may instance one yourself and pass it to CreateContext() to share a font atlas between contexts. + // - DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() + // for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for details. + IMGUI_API ImGuiContext* CreateContext(ImFontAtlas* shared_font_atlas = NULL); + IMGUI_API void DestroyContext(ImGuiContext* ctx = NULL); // NULL = destroy current context + IMGUI_API ImGuiContext* GetCurrentContext(); + IMGUI_API void SetCurrentContext(ImGuiContext* ctx); + + // Main + IMGUI_API ImGuiIO& GetIO(); // access the IO structure (mouse/keyboard/gamepad inputs, time, various configuration options/flags) + IMGUI_API ImGuiStyle& GetStyle(); // access the Style structure (colors, sizes). Always use PushStyleCol(), PushStyleVar() to modify style mid-frame! + IMGUI_API void NewFrame(); // start a new Dear ImGui frame, you can submit any command from this point until Render()/EndFrame(). + IMGUI_API void EndFrame(); // ends the Dear ImGui frame. automatically called by Render(). If you don't need to render data (skipping rendering) you may call EndFrame() without Render()... but you'll have wasted CPU already! If you don't need to render, better to not create any windows and not call NewFrame() at all! + IMGUI_API void Render(); // ends the Dear ImGui frame, finalize the draw data. You can then get call GetDrawData(). + IMGUI_API ImDrawData* GetDrawData(); // valid after Render() and until the next call to NewFrame(). this is what you have to render. + + // Demo, Debug, Information + IMGUI_API void ShowDemoWindow(bool* p_open = NULL); // create Demo window. demonstrate most ImGui features. call this to learn about the library! try to make it always available in your application! + IMGUI_API void ShowMetricsWindow(bool* p_open = NULL); // create Metrics/Debugger window. display Dear ImGui internals: windows, draw commands, various internal state, etc. + IMGUI_API void ShowStackToolWindow(bool* p_open = NULL); // create Stack Tool window. hover items with mouse to query information about the source of their unique ID. + IMGUI_API void ShowAboutWindow(bool* p_open = NULL); // create About window. display Dear ImGui version, credits and build/system information. + IMGUI_API void ShowStyleEditor(ImGuiStyle* ref = NULL); // add style editor block (not a window). you can pass in a reference ImGuiStyle structure to compare to, revert to and save to (else it uses the default style) + IMGUI_API bool ShowStyleSelector(const char* label); // add style selector block (not a window), essentially a combo listing the default styles. + IMGUI_API void ShowFontSelector(const char* label); // add font selector block (not a window), essentially a combo listing the loaded fonts. + IMGUI_API void ShowUserGuide(); // add basic help/info block (not a window): how to manipulate ImGui as a end-user (mouse/keyboard controls). + IMGUI_API const char* GetVersion(); // get the compiled version string e.g. "1.80 WIP" (essentially the value for IMGUI_VERSION from the compiled version of imgui.cpp) + + // Styles + IMGUI_API void StyleColorsDark(ImGuiStyle* dst = NULL); // new, recommended style (default) + IMGUI_API void StyleColorsLight(ImGuiStyle* dst = NULL); // best used with borders and a custom, thicker font + IMGUI_API void StyleColorsClassic(ImGuiStyle* dst = NULL); // classic imgui style + + // Windows + // - Begin() = push window to the stack and start appending to it. End() = pop window from the stack. + // - Passing 'bool* p_open != NULL' shows a window-closing widget in the upper-right corner of the window, + // which clicking will set the boolean to false when clicked. + // - You may append multiple times to the same window during the same frame by calling Begin()/End() pairs multiple times. + // Some information such as 'flags' or 'p_open' will only be considered by the first call to Begin(). + // - Begin() return false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting + // anything to the window. Always call a matching End() for each Begin() call, regardless of its return value! + // [Important: due to legacy reason, this is inconsistent with most other functions such as BeginMenu/EndMenu, + // BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding BeginXXX function + // returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] + // - Note that the bottom of window stack always contains a window called "Debug". + IMGUI_API bool Begin(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); + IMGUI_API void End(); + + // Child Windows + // - Use child windows to begin into a self-contained independent scrolling/clipping regions within a host window. Child windows can embed their own child. + // - For each independent axis of 'size': ==0.0f: use remaining host window size / >0.0f: fixed size / <0.0f: use remaining window size minus abs(size) / Each axis can use a different mode, e.g. ImVec2(0,400). + // - BeginChild() returns false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting anything to the window. + // Always call a matching EndChild() for each BeginChild() call, regardless of its return value. + // [Important: due to legacy reason, this is inconsistent with most other functions such as BeginMenu/EndMenu, + // BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding BeginXXX function + // returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] + IMGUI_API bool BeginChild(const char* str_id, const ImVec2& size = ImVec2(0, 0), bool border = false, ImGuiWindowFlags flags = 0); + IMGUI_API bool BeginChild(ImGuiID id, const ImVec2& size = ImVec2(0, 0), bool border = false, ImGuiWindowFlags flags = 0); + IMGUI_API void EndChild(); + + // Windows Utilities + // - 'current window' = the window we are appending into while inside a Begin()/End() block. 'next window' = next window we will Begin() into. + IMGUI_API bool IsWindowAppearing(); + IMGUI_API bool IsWindowCollapsed(); + IMGUI_API bool IsWindowFocused(ImGuiFocusedFlags flags=0); // is current window focused? or its root/child, depending on flags. see flags for options. + IMGUI_API bool IsWindowHovered(ImGuiHoveredFlags flags=0); // is current window hovered (and typically: not blocked by a popup/modal)? see flags for options. NB: If you are trying to check whether your mouse should be dispatched to imgui or to your app, you should use the 'io.WantCaptureMouse' boolean for that! Please read the FAQ! + IMGUI_API ImDrawList* GetWindowDrawList(); // get draw list associated to the current window, to append your own drawing primitives + IMGUI_API ImVec2 GetWindowPos(); // get current window position in screen space (useful if you want to do your own drawing via the DrawList API) + IMGUI_API ImVec2 GetWindowSize(); // get current window size + IMGUI_API float GetWindowWidth(); // get current window width (shortcut for GetWindowSize().x) + IMGUI_API float GetWindowHeight(); // get current window height (shortcut for GetWindowSize().y) + + // Window manipulation + // - Prefer using SetNextXXX functions (before Begin) rather that SetXXX functions (after Begin). + IMGUI_API void SetNextWindowPos(const ImVec2& pos, ImGuiCond cond = 0, const ImVec2& pivot = ImVec2(0, 0)); // set next window position. call before Begin(). use pivot=(0.5f,0.5f) to center on given point, etc. + IMGUI_API void SetNextWindowSize(const ImVec2& size, ImGuiCond cond = 0); // set next window size. set axis to 0.0f to force an auto-fit on this axis. call before Begin() + IMGUI_API void SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback = NULL, void* custom_callback_data = NULL); // set next window size limits. use -1,-1 on either X/Y axis to preserve the current size. Sizes will be rounded down. Use callback to apply non-trivial programmatic constraints. + IMGUI_API void SetNextWindowContentSize(const ImVec2& size); // set next window content size (~ scrollable client area, which enforce the range of scrollbars). Not including window decorations (title bar, menu bar, etc.) nor WindowPadding. set an axis to 0.0f to leave it automatic. call before Begin() + IMGUI_API void SetNextWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // set next window collapsed state. call before Begin() + IMGUI_API void SetNextWindowFocus(); // set next window to be focused / top-most. call before Begin() + IMGUI_API void SetNextWindowBgAlpha(float alpha); // set next window background color alpha. helper to easily override the Alpha component of ImGuiCol_WindowBg/ChildBg/PopupBg. you may also use ImGuiWindowFlags_NoBackground. + IMGUI_API void SetWindowPos(const ImVec2& pos, ImGuiCond cond = 0); // (not recommended) set current window position - call within Begin()/End(). prefer using SetNextWindowPos(), as this may incur tearing and side-effects. + IMGUI_API void SetWindowSize(const ImVec2& size, ImGuiCond cond = 0); // (not recommended) set current window size - call within Begin()/End(). set to ImVec2(0, 0) to force an auto-fit. prefer using SetNextWindowSize(), as this may incur tearing and minor side-effects. + IMGUI_API void SetWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // (not recommended) set current window collapsed state. prefer using SetNextWindowCollapsed(). + IMGUI_API void SetWindowFocus(); // (not recommended) set current window to be focused / top-most. prefer using SetNextWindowFocus(). + IMGUI_API void SetWindowFontScale(float scale); // [OBSOLETE] set font scale. Adjust IO.FontGlobalScale if you want to scale all windows. This is an old API! For correct scaling, prefer to reload font + rebuild ImFontAtlas + call style.ScaleAllSizes(). + IMGUI_API void SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond = 0); // set named window position. + IMGUI_API void SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond = 0); // set named window size. set axis to 0.0f to force an auto-fit on this axis. + IMGUI_API void SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond = 0); // set named window collapsed state + IMGUI_API void SetWindowFocus(const char* name); // set named window to be focused / top-most. use NULL to remove focus. + + // Content region + // - Retrieve available space from a given point. GetContentRegionAvail() is frequently useful. + // - Those functions are bound to be redesigned (they are confusing, incomplete and the Min/Max return values are in local window coordinates which increases confusion) + IMGUI_API ImVec2 GetContentRegionAvail(); // == GetContentRegionMax() - GetCursorPos() + IMGUI_API ImVec2 GetContentRegionMax(); // current content boundaries (typically window boundaries including scrolling, or current column boundaries), in windows coordinates + IMGUI_API ImVec2 GetWindowContentRegionMin(); // content boundaries min for the full window (roughly (0,0)-Scroll), in window coordinates + IMGUI_API ImVec2 GetWindowContentRegionMax(); // content boundaries max for the full window (roughly (0,0)+Size-Scroll) where Size can be override with SetNextWindowContentSize(), in window coordinates + + // Windows Scrolling + IMGUI_API float GetScrollX(); // get scrolling amount [0 .. GetScrollMaxX()] + IMGUI_API float GetScrollY(); // get scrolling amount [0 .. GetScrollMaxY()] + IMGUI_API void SetScrollX(float scroll_x); // set scrolling amount [0 .. GetScrollMaxX()] + IMGUI_API void SetScrollY(float scroll_y); // set scrolling amount [0 .. GetScrollMaxY()] + IMGUI_API float GetScrollMaxX(); // get maximum scrolling amount ~~ ContentSize.x - WindowSize.x - DecorationsSize.x + IMGUI_API float GetScrollMaxY(); // get maximum scrolling amount ~~ ContentSize.y - WindowSize.y - DecorationsSize.y + IMGUI_API void SetScrollHereX(float center_x_ratio = 0.5f); // adjust scrolling amount to make current cursor position visible. center_x_ratio=0.0: left, 0.5: center, 1.0: right. When using to make a "default/current item" visible, consider using SetItemDefaultFocus() instead. + IMGUI_API void SetScrollHereY(float center_y_ratio = 0.5f); // adjust scrolling amount to make current cursor position visible. center_y_ratio=0.0: top, 0.5: center, 1.0: bottom. When using to make a "default/current item" visible, consider using SetItemDefaultFocus() instead. + IMGUI_API void SetScrollFromPosX(float local_x, float center_x_ratio = 0.5f); // adjust scrolling amount to make given position visible. Generally GetCursorStartPos() + offset to compute a valid position. + IMGUI_API void SetScrollFromPosY(float local_y, float center_y_ratio = 0.5f); // adjust scrolling amount to make given position visible. Generally GetCursorStartPos() + offset to compute a valid position. + + // Parameters stacks (shared) + IMGUI_API void PushFont(ImFont* font); // use NULL as a shortcut to push default font + IMGUI_API void PopFont(); + IMGUI_API void PushStyleColor(ImGuiCol idx, ImU32 col); // modify a style color. always use this if you modify the style after NewFrame(). + IMGUI_API void PushStyleColor(ImGuiCol idx, const ImVec4& col); + IMGUI_API void PopStyleColor(int count = 1); + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, float val); // modify a style float variable. always use this if you modify the style after NewFrame(). + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, const ImVec2& val); // modify a style ImVec2 variable. always use this if you modify the style after NewFrame(). + IMGUI_API void PopStyleVar(int count = 1); + IMGUI_API void PushAllowKeyboardFocus(bool allow_keyboard_focus); // == tab stop enable. Allow focusing using TAB/Shift-TAB, enabled by default but you can disable it for certain widgets + IMGUI_API void PopAllowKeyboardFocus(); + IMGUI_API void PushButtonRepeat(bool repeat); // in 'repeat' mode, Button*() functions return repeated true in a typematic manner (using io.KeyRepeatDelay/io.KeyRepeatRate setting). Note that you can call IsItemActive() after any Button() to tell if the button is held in the current frame. + IMGUI_API void PopButtonRepeat(); + + // Parameters stacks (current window) + IMGUI_API void PushItemWidth(float item_width); // push width of items for common large "item+label" widgets. >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -FLT_MIN always align width to the right side). + IMGUI_API void PopItemWidth(); + IMGUI_API void SetNextItemWidth(float item_width); // set width of the _next_ common large "item+label" widget. >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -FLT_MIN always align width to the right side) + IMGUI_API float CalcItemWidth(); // width of item given pushed settings and current cursor position. NOT necessarily the width of last item unlike most 'Item' functions. + IMGUI_API void PushTextWrapPos(float wrap_local_pos_x = 0.0f); // push word-wrapping position for Text*() commands. < 0.0f: no wrapping; 0.0f: wrap to end of window (or column); > 0.0f: wrap at 'wrap_pos_x' position in window local space + IMGUI_API void PopTextWrapPos(); + + // Style read access + // - Use the style editor (ShowStyleEditor() function) to interactively see what the colors are) + IMGUI_API ImFont* GetFont(); // get current font + IMGUI_API float GetFontSize(); // get current font size (= height in pixels) of current font with current scale applied + IMGUI_API ImVec2 GetFontTexUvWhitePixel(); // get UV coordinate for a while pixel, useful to draw custom shapes via the ImDrawList API + IMGUI_API ImU32 GetColorU32(ImGuiCol idx, float alpha_mul = 1.0f); // retrieve given style color with style alpha applied and optional extra alpha multiplier, packed as a 32-bit value suitable for ImDrawList + IMGUI_API ImU32 GetColorU32(const ImVec4& col); // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList + IMGUI_API ImU32 GetColorU32(ImU32 col); // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList + IMGUI_API const ImVec4& GetStyleColorVec4(ImGuiCol idx); // retrieve style color as stored in ImGuiStyle structure. use to feed back into PushStyleColor(), otherwise use GetColorU32() to get style color with style alpha baked in. + + // Cursor / Layout + // - By "cursor" we mean the current output position. + // - The typical widget behavior is to output themselves at the current cursor position, then move the cursor one line down. + // - You can call SameLine() between widgets to undo the last carriage return and output at the right of the preceding widget. + // - Attention! We currently have inconsistencies between window-local and absolute positions we will aim to fix with future API: + // Window-local coordinates: SameLine(), GetCursorPos(), SetCursorPos(), GetCursorStartPos(), GetContentRegionMax(), GetWindowContentRegion*(), PushTextWrapPos() + // Absolute coordinate: GetCursorScreenPos(), SetCursorScreenPos(), all ImDrawList:: functions. + IMGUI_API void Separator(); // separator, generally horizontal. inside a menu bar or in horizontal layout mode, this becomes a vertical separator. + IMGUI_API void SameLine(float offset_from_start_x=0.0f, float spacing=-1.0f); // call between widgets or groups to layout them horizontally. X position given in window coordinates. + IMGUI_API void NewLine(); // undo a SameLine() or force a new line when in an horizontal-layout context. + IMGUI_API void Spacing(); // add vertical spacing. + IMGUI_API void Dummy(const ImVec2& size); // add a dummy item of given size. unlike InvisibleButton(), Dummy() won't take the mouse click or be navigable into. + IMGUI_API void Indent(float indent_w = 0.0f); // move content position toward the right, by indent_w, or style.IndentSpacing if indent_w <= 0 + IMGUI_API void Unindent(float indent_w = 0.0f); // move content position back to the left, by indent_w, or style.IndentSpacing if indent_w <= 0 + IMGUI_API void BeginGroup(); // lock horizontal starting position + IMGUI_API void EndGroup(); // unlock horizontal starting position + capture the whole group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) + IMGUI_API ImVec2 GetCursorPos(); // cursor position in window coordinates (relative to window position) + IMGUI_API float GetCursorPosX(); // (some functions are using window-relative coordinates, such as: GetCursorPos, GetCursorStartPos, GetContentRegionMax, GetWindowContentRegion* etc. + IMGUI_API float GetCursorPosY(); // other functions such as GetCursorScreenPos or everything in ImDrawList:: + IMGUI_API void SetCursorPos(const ImVec2& local_pos); // are using the main, absolute coordinate system. + IMGUI_API void SetCursorPosX(float local_x); // GetWindowPos() + GetCursorPos() == GetCursorScreenPos() etc.) + IMGUI_API void SetCursorPosY(float local_y); // + IMGUI_API ImVec2 GetCursorStartPos(); // initial cursor position in window coordinates + IMGUI_API ImVec2 GetCursorScreenPos(); // cursor position in absolute coordinates (useful to work with ImDrawList API). generally top-left == GetMainViewport()->Pos == (0,0) in single viewport mode, and bottom-right == GetMainViewport()->Pos+Size == io.DisplaySize in single-viewport mode. + IMGUI_API void SetCursorScreenPos(const ImVec2& pos); // cursor position in absolute coordinates + IMGUI_API void AlignTextToFramePadding(); // vertically align upcoming text baseline to FramePadding.y so that it will align properly to regularly framed items (call if you have text on a line before a framed item) + IMGUI_API float GetTextLineHeight(); // ~ FontSize + IMGUI_API float GetTextLineHeightWithSpacing(); // ~ FontSize + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of text) + IMGUI_API float GetFrameHeight(); // ~ FontSize + style.FramePadding.y * 2 + IMGUI_API float GetFrameHeightWithSpacing(); // ~ FontSize + style.FramePadding.y * 2 + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of framed widgets) + + // ID stack/scopes + // Read the FAQ (docs/FAQ.md or http://dearimgui.org/faq) for more details about how ID are handled in dear imgui. + // - Those questions are answered and impacted by understanding of the ID stack system: + // - "Q: Why is my widget not reacting when I click on it?" + // - "Q: How can I have widgets with an empty label?" + // - "Q: How can I have multiple widgets with the same label?" + // - Short version: ID are hashes of the entire ID stack. If you are creating widgets in a loop you most likely + // want to push a unique identifier (e.g. object pointer, loop index) to uniquely differentiate them. + // - You can also use the "Label##foobar" syntax within widget label to distinguish them from each others. + // - In this header file we use the "label"/"name" terminology to denote a string that will be displayed + used as an ID, + // whereas "str_id" denote a string that is only used as an ID and not normally displayed. + IMGUI_API void PushID(const char* str_id); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const char* str_id_begin, const char* str_id_end); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const void* ptr_id); // push pointer into the ID stack (will hash pointer). + IMGUI_API void PushID(int int_id); // push integer into the ID stack (will hash integer). + IMGUI_API void PopID(); // pop from the ID stack. + IMGUI_API ImGuiID GetID(const char* str_id); // calculate unique ID (hash of whole ID stack + given parameter). e.g. if you want to query into ImGuiStorage yourself + IMGUI_API ImGuiID GetID(const char* str_id_begin, const char* str_id_end); + IMGUI_API ImGuiID GetID(const void* ptr_id); + + // Widgets: Text + IMGUI_API void TextUnformatted(const char* text, const char* text_end = NULL); // raw text without formatting. Roughly equivalent to Text("%s", text) but: A) doesn't require null terminated string if 'text_end' is specified, B) it's faster, no memory copy is done, no buffer size limits, recommended for long chunks of text. + IMGUI_API void Text(const char* fmt, ...) IM_FMTARGS(1); // formatted text + IMGUI_API void TextV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void TextColored(const ImVec4& col, const char* fmt, ...) IM_FMTARGS(2); // shortcut for PushStyleColor(ImGuiCol_Text, col); Text(fmt, ...); PopStyleColor(); + IMGUI_API void TextColoredV(const ImVec4& col, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API void TextDisabled(const char* fmt, ...) IM_FMTARGS(1); // shortcut for PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]); Text(fmt, ...); PopStyleColor(); + IMGUI_API void TextDisabledV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void TextWrapped(const char* fmt, ...) IM_FMTARGS(1); // shortcut for PushTextWrapPos(0.0f); Text(fmt, ...); PopTextWrapPos();. Note that this won't work on an auto-resizing window if there's no other widgets to extend the window width, yoy may need to set a size using SetNextWindowSize(). + IMGUI_API void TextWrappedV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void LabelText(const char* label, const char* fmt, ...) IM_FMTARGS(2); // display text+label aligned the same way as value+label widgets + IMGUI_API void LabelTextV(const char* label, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API void BulletText(const char* fmt, ...) IM_FMTARGS(1); // shortcut for Bullet()+Text() + IMGUI_API void BulletTextV(const char* fmt, va_list args) IM_FMTLIST(1); + + // Widgets: Main + // - Most widgets return true when the value has been changed or when pressed/selected + // - You may also use one of the many IsItemXXX functions (e.g. IsItemActive, IsItemHovered, etc.) to query widget state. + IMGUI_API bool Button(const char* label, const ImVec2& size = ImVec2(0, 0)); // button + IMGUI_API bool SmallButton(const char* label); // button with FramePadding=(0,0) to easily embed within text + IMGUI_API bool InvisibleButton(const char* str_id, const ImVec2& size, ImGuiButtonFlags flags = 0); // flexible button behavior without the visuals, frequently useful to build custom behaviors using the public api (along with IsItemActive, IsItemHovered, etc.) + IMGUI_API bool ArrowButton(const char* str_id, ImGuiDir dir); // square button with an arrow shape + IMGUI_API void Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1,1), const ImVec4& tint_col = ImVec4(1,1,1,1), const ImVec4& border_col = ImVec4(0,0,0,0)); + IMGUI_API bool ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1,1), int frame_padding = -1, const ImVec4& bg_col = ImVec4(0,0,0,0), const ImVec4& tint_col = ImVec4(1,1,1,1)); // <0 frame_padding uses default frame padding settings. 0 for no padding + IMGUI_API bool Checkbox(const char* label, bool* v); + IMGUI_API bool CheckboxFlags(const char* label, int* flags, int flags_value); + IMGUI_API bool CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value); + IMGUI_API bool RadioButton(const char* label, bool active); // use with e.g. if (RadioButton("one", my_value==1)) { my_value = 1; } + IMGUI_API bool RadioButton(const char* label, int* v, int v_button); // shortcut to handle the above pattern when value is an integer + IMGUI_API void ProgressBar(float fraction, const ImVec2& size_arg = ImVec2(-FLT_MIN, 0), const char* overlay = NULL); + IMGUI_API void Bullet(); // draw a small circle + keep the cursor on the same line. advance cursor x position by GetTreeNodeToLabelSpacing(), same distance that TreeNode() uses + + // Widgets: Combo Box + // - The BeginCombo()/EndCombo() api allows you to manage your contents and selection state however you want it, by creating e.g. Selectable() items. + // - The old Combo() api are helpers over BeginCombo()/EndCombo() which are kept available for convenience purpose. This is analogous to how ListBox are created. + IMGUI_API bool BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags = 0); + IMGUI_API void EndCombo(); // only call EndCombo() if BeginCombo() returns true! + IMGUI_API bool Combo(const char* label, int* current_item, const char* const items[], int items_count, int popup_max_height_in_items = -1); + IMGUI_API bool Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int popup_max_height_in_items = -1); // Separate items with \0 within a string, end item-list with \0\0. e.g. "One\0Two\0Three\0" + IMGUI_API bool Combo(const char* label, int* current_item, bool(*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int popup_max_height_in_items = -1); + + // Widgets: Drag Sliders + // - CTRL+Click on any drag box to turn them into an input box. Manually input values aren't clamped by default and can go off-bounds. Use ImGuiSliderFlags_AlwaysClamp to always clamp. + // - For all the Float2/Float3/Float4/Int2/Int3/Int4 versions of every functions, note that a 'float v[X]' function argument is the same as 'float* v', + // the array syntax is just a way to document the number of elements that are expected to be accessible. You can pass address of your first element out of a contiguous set, e.g. &myvector.x + // - Adjust format string to decorate the value with a prefix, a suffix, or adapt the editing and display precision e.g. "%.3f" -> 1.234; "%5.2f secs" -> 01.23 secs; "Biscuit: %.0f" -> Biscuit: 1; etc. + // - Format string may also be set to NULL or use the default format ("%f" or "%d"). + // - Speed are per-pixel of mouse movement (v_speed=0.2f: mouse needs to move by 5 pixels to increase value by 1). For gamepad/keyboard navigation, minimum speed is Max(v_speed, minimum_step_at_given_precision). + // - Use v_min < v_max to clamp edits to given limits. Note that CTRL+Click manual input can override those limits if ImGuiSliderFlags_AlwaysClamp is not used. + // - Use v_max = FLT_MAX / INT_MAX etc to avoid clamping to a maximum, same with v_min = -FLT_MAX / INT_MIN to avoid clamping to a minimum. + // - We use the same sets of flags for DragXXX() and SliderXXX() functions as the features are the same and it makes it easier to swap them. + // - Legacy: Pre-1.78 there are DragXXX() function signatures that takes a final `float power=1.0f' argument instead of the `ImGuiSliderFlags flags=0' argument. + // If you get a warning converting a float to ImGuiSliderFlags, read https://github.com/ocornut/imgui/issues/3361 + IMGUI_API bool DragFloat(const char* label, float* v, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); // If v_min >= v_max we have no bound + IMGUI_API bool DragFloat2(const char* label, float v[2], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloat3(const char* label, float v[3], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloat4(const char* label, float v[4], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", const char* format_max = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt(const char* label, int* v, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); // If v_min >= v_max we have no bound + IMGUI_API bool DragInt2(const char* label, int v[2], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt3(const char* label, int v[3], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt4(const char* label, int v[4], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", const char* format_max = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed = 1.0f, const void* p_min = NULL, const void* p_max = NULL, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed = 1.0f, const void* p_min = NULL, const void* p_max = NULL, const char* format = NULL, ImGuiSliderFlags flags = 0); + + // Widgets: Regular Sliders + // - CTRL+Click on any slider to turn them into an input box. Manually input values aren't clamped by default and can go off-bounds. Use ImGuiSliderFlags_AlwaysClamp to always clamp. + // - Adjust format string to decorate the value with a prefix, a suffix, or adapt the editing and display precision e.g. "%.3f" -> 1.234; "%5.2f secs" -> 01.23 secs; "Biscuit: %.0f" -> Biscuit: 1; etc. + // - Format string may also be set to NULL or use the default format ("%f" or "%d"). + // - Legacy: Pre-1.78 there are SliderXXX() function signatures that takes a final `float power=1.0f' argument instead of the `ImGuiSliderFlags flags=0' argument. + // If you get a warning converting a float to ImGuiSliderFlags, read https://github.com/ocornut/imgui/issues/3361 + IMGUI_API bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); // adjust format to decorate the value with a prefix or a suffix for in-slider labels or unit display. + IMGUI_API bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderAngle(const char* label, float* v_rad, float v_degrees_min = -360.0f, float v_degrees_max = +360.0f, const char* format = "%.0f deg", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt(const char* label, int* v, int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + + // Widgets: Input with Keyboard + // - If you want to use InputText() with std::string or any custom dynamic string type, see misc/cpp/imgui_stdlib.h and comments in imgui_demo.cpp. + // - Most of the ImGuiInputTextFlags flags are only useful for InputText() and not for InputFloatX, InputIntX, InputDouble etc. + IMGUI_API bool InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputFloat(const char* label, float* v, float step = 0.0f, float step_fast = 0.0f, const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputFloat2(const char* label, float v[2], const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputFloat3(const char* label, float v[3], const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputFloat4(const char* label, float v[4], const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt(const char* label, int* v, int step = 1, int step_fast = 100, ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputDouble(const char* label, double* v, double step = 0.0, double step_fast = 0.0, const char* format = "%.6f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step = NULL, const void* p_step_fast = NULL, const char* format = NULL, ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_step = NULL, const void* p_step_fast = NULL, const char* format = NULL, ImGuiInputTextFlags flags = 0); + + // Widgets: Color Editor/Picker (tip: the ColorEdit* functions have a little color square that can be left-clicked to open a picker, and right-clicked to open an option menu.) + // - Note that in C++ a 'float v[X]' function argument is the _same_ as 'float* v', the array syntax is just a way to document the number of elements that are expected to be accessible. + // - You can pass the address of a first float element out of a contiguous structure, e.g. &myvector.x + IMGUI_API bool ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags = 0, const float* ref_col = NULL); + IMGUI_API bool ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0, 0)); // display a color square/button, hover for details, return true when pressed. + IMGUI_API void SetColorEditOptions(ImGuiColorEditFlags flags); // initialize current options (generally on application startup) if you want to select a default format, picker type, etc. User will be able to change many settings, unless you pass the _NoOptions flag to your calls. + + // Widgets: Trees + // - TreeNode functions return true when the node is open, in which case you need to also call TreePop() when you are finished displaying the tree node contents. + IMGUI_API bool TreeNode(const char* label); + IMGUI_API bool TreeNode(const char* str_id, const char* fmt, ...) IM_FMTARGS(2); // helper variation to easily decorelate the id from the displayed string. Read the FAQ about why and how to use ID. to align arbitrary text at the same level as a TreeNode() you can use Bullet(). + IMGUI_API bool TreeNode(const void* ptr_id, const char* fmt, ...) IM_FMTARGS(2); // " + IMGUI_API bool TreeNodeV(const char* str_id, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API bool TreeNodeV(const void* ptr_id, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API bool TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags = 0); + IMGUI_API bool TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); + IMGUI_API bool TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); + IMGUI_API bool TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); + IMGUI_API bool TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); + IMGUI_API void TreePush(const char* str_id); // ~ Indent()+PushId(). Already called by TreeNode() when returning true, but you can call TreePush/TreePop yourself if desired. + IMGUI_API void TreePush(const void* ptr_id = NULL); // " + IMGUI_API void TreePop(); // ~ Unindent()+PopId() + IMGUI_API float GetTreeNodeToLabelSpacing(); // horizontal distance preceding label when using TreeNode*() or Bullet() == (g.FontSize + style.FramePadding.x*2) for a regular unframed TreeNode + IMGUI_API bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0); // if returning 'true' the header is open. doesn't indent nor push on ID stack. user doesn't have to call TreePop(). + IMGUI_API bool CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags = 0); // when 'p_visible != NULL': if '*p_visible==true' display an additional small close button on upper right of the header which will set the bool to false when clicked, if '*p_visible==false' don't display the header. + IMGUI_API void SetNextItemOpen(bool is_open, ImGuiCond cond = 0); // set next TreeNode/CollapsingHeader open state. + + // Widgets: Selectables + // - A selectable highlights when hovered, and can display another color when selected. + // - Neighbors selectable extend their highlight bounds in order to leave no gap between them. This is so a series of selected Selectable appear contiguous. + IMGUI_API bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height + IMGUI_API bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper. + + // Widgets: List Boxes + // - This is essentially a thin wrapper to using BeginChild/EndChild with some stylistic changes. + // - The BeginListBox()/EndListBox() api allows you to manage your contents and selection state however you want it, by creating e.g. Selectable() or any items. + // - The simplified/old ListBox() api are helpers over BeginListBox()/EndListBox() which are kept available for convenience purpose. This is analoguous to how Combos are created. + // - Choose frame width: size.x > 0.0f: custom / size.x < 0.0f or -FLT_MIN: right-align / size.x = 0.0f (default): use current ItemWidth + // - Choose frame height: size.y > 0.0f: custom / size.y < 0.0f or -FLT_MIN: bottom-align / size.y = 0.0f (default): arbitrary default height which can fit ~7 items + IMGUI_API bool BeginListBox(const char* label, const ImVec2& size = ImVec2(0, 0)); // open a framed scrolling region + IMGUI_API void EndListBox(); // only call EndListBox() if BeginListBox() returned true! + IMGUI_API bool ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items = -1); + IMGUI_API bool ListBox(const char* label, int* current_item, bool (*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int height_in_items = -1); + + // Widgets: Data Plotting + // - Consider using ImPlot (https://github.com/epezent/implot) which is much better! + IMGUI_API void PlotLines(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); + IMGUI_API void PlotLines(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); + IMGUI_API void PlotHistogram(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); + IMGUI_API void PlotHistogram(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); + + // Widgets: Value() Helpers. + // - Those are merely shortcut to calling Text() with a format string. Output single value in "name: value" format (tip: freely declare more in your code to handle your types. you can add functions to the ImGui namespace) + IMGUI_API void Value(const char* prefix, bool b); + IMGUI_API void Value(const char* prefix, int v); + IMGUI_API void Value(const char* prefix, unsigned int v); + IMGUI_API void Value(const char* prefix, float v, const char* float_format = NULL); + + // Widgets: Menus + // - Use BeginMenuBar() on a window ImGuiWindowFlags_MenuBar to append to its menu bar. + // - Use BeginMainMenuBar() to create a menu bar at the top of the screen and append to it. + // - Use BeginMenu() to create a menu. You can call BeginMenu() multiple time with the same identifier to append more items to it. + // - Not that MenuItem() keyboardshortcuts are displayed as a convenience but _not processed_ by Dear ImGui at the moment. + IMGUI_API bool BeginMenuBar(); // append to menu-bar of current window (requires ImGuiWindowFlags_MenuBar flag set on parent window). + IMGUI_API void EndMenuBar(); // only call EndMenuBar() if BeginMenuBar() returns true! + IMGUI_API bool BeginMainMenuBar(); // create and append to a full screen menu-bar. + IMGUI_API void EndMainMenuBar(); // only call EndMainMenuBar() if BeginMainMenuBar() returns true! + IMGUI_API bool BeginMenu(const char* label, bool enabled = true); // create a sub-menu entry. only call EndMenu() if this returns true! + IMGUI_API void EndMenu(); // only call EndMenu() if BeginMenu() returns true! + IMGUI_API bool MenuItem(const char* label, const char* shortcut = NULL, bool selected = false, bool enabled = true); // return true when activated. + IMGUI_API bool MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled = true); // return true when activated + toggle (*p_selected) if p_selected != NULL + + // Tooltips + // - Tooltip are windows following the mouse. They do not take focus away. + IMGUI_API void BeginTooltip(); // begin/append a tooltip window. to create full-featured tooltip (with any kind of items). + IMGUI_API void EndTooltip(); + IMGUI_API void SetTooltip(const char* fmt, ...) IM_FMTARGS(1); // set a text-only tooltip, typically use with ImGui::IsItemHovered(). override any previous call to SetTooltip(). + IMGUI_API void SetTooltipV(const char* fmt, va_list args) IM_FMTLIST(1); + + // Popups, Modals + // - They block normal mouse hovering detection (and therefore most mouse interactions) behind them. + // - If not modal: they can be closed by clicking anywhere outside them, or by pressing ESCAPE. + // - Their visibility state (~bool) is held internally instead of being held by the programmer as we are used to with regular Begin*() calls. + // - The 3 properties above are related: we need to retain popup visibility state in the library because popups may be closed as any time. + // - You can bypass the hovering restriction by using ImGuiHoveredFlags_AllowWhenBlockedByPopup when calling IsItemHovered() or IsWindowHovered(). + // - IMPORTANT: Popup identifiers are relative to the current ID stack, so OpenPopup and BeginPopup generally needs to be at the same level of the stack. + // This is sometimes leading to confusing mistakes. May rework this in the future. + + // Popups: begin/end functions + // - BeginPopup(): query popup state, if open start appending into the window. Call EndPopup() afterwards. ImGuiWindowFlags are forwarded to the window. + // - BeginPopupModal(): block every interactions behind the window, cannot be closed by user, add a dimming background, has a title bar. + IMGUI_API bool BeginPopup(const char* str_id, ImGuiWindowFlags flags = 0); // return true if the popup is open, and you can start outputting to it. + IMGUI_API bool BeginPopupModal(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); // return true if the modal is open, and you can start outputting to it. + IMGUI_API void EndPopup(); // only call EndPopup() if BeginPopupXXX() returns true! + + // Popups: open/close functions + // - OpenPopup(): set popup state to open. ImGuiPopupFlags are available for opening options. + // - If not modal: they can be closed by clicking anywhere outside them, or by pressing ESCAPE. + // - CloseCurrentPopup(): use inside the BeginPopup()/EndPopup() scope to close manually. + // - CloseCurrentPopup() is called by default by Selectable()/MenuItem() when activated (FIXME: need some options). + // - Use ImGuiPopupFlags_NoOpenOverExistingPopup to avoid opening a popup if there's already one at the same level. This is equivalent to e.g. testing for !IsAnyPopupOpen() prior to OpenPopup(). + // - Use IsWindowAppearing() after BeginPopup() to tell if a window just opened. + // - IMPORTANT: Notice that for OpenPopupOnItemClick() we exceptionally default flags to 1 (== ImGuiPopupFlags_MouseButtonRight) for backward compatibility with older API taking 'int mouse_button = 1' parameter + IMGUI_API void OpenPopup(const char* str_id, ImGuiPopupFlags popup_flags = 0); // call to mark popup as open (don't call every frame!). + IMGUI_API void OpenPopup(ImGuiID id, ImGuiPopupFlags popup_flags = 0); // id overload to facilitate calling from nested stacks + IMGUI_API void OpenPopupOnItemClick(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // helper to open popup when clicked on last item. Default to ImGuiPopupFlags_MouseButtonRight == 1. (note: actually triggers on the mouse _released_ event to be consistent with popup behaviors) + IMGUI_API void CloseCurrentPopup(); // manually close the popup we have begin-ed into. + + // Popups: open+begin combined functions helpers + // - Helpers to do OpenPopup+BeginPopup where the Open action is triggered by e.g. hovering an item and right-clicking. + // - They are convenient to easily create context menus, hence the name. + // - IMPORTANT: Notice that BeginPopupContextXXX takes ImGuiPopupFlags just like OpenPopup() and unlike BeginPopup(). For full consistency, we may add ImGuiWindowFlags to the BeginPopupContextXXX functions in the future. + // - IMPORTANT: Notice that we exceptionally default their flags to 1 (== ImGuiPopupFlags_MouseButtonRight) for backward compatibility with older API taking 'int mouse_button = 1' parameter, so if you add other flags remember to re-add the ImGuiPopupFlags_MouseButtonRight. + IMGUI_API bool BeginPopupContextItem(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // open+begin popup when clicked on last item. Use str_id==NULL to associate the popup to previous item. If you want to use that on a non-interactive item such as Text() you need to pass in an explicit ID here. read comments in .cpp! + IMGUI_API bool BeginPopupContextWindow(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1);// open+begin popup when clicked on current window. + IMGUI_API bool BeginPopupContextVoid(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // open+begin popup when clicked in void (where there are no windows). + + // Popups: query functions + // - IsPopupOpen(): return true if the popup is open at the current BeginPopup() level of the popup stack. + // - IsPopupOpen() with ImGuiPopupFlags_AnyPopupId: return true if any popup is open at the current BeginPopup() level of the popup stack. + // - IsPopupOpen() with ImGuiPopupFlags_AnyPopupId + ImGuiPopupFlags_AnyPopupLevel: return true if any popup is open. + IMGUI_API bool IsPopupOpen(const char* str_id, ImGuiPopupFlags flags = 0); // return true if the popup is open. + + // Tables + // - Full-featured replacement for old Columns API. + // - See Demo->Tables for demo code. + // - See top of imgui_tables.cpp for general commentary. + // - See ImGuiTableFlags_ and ImGuiTableColumnFlags_ enums for a description of available flags. + // The typical call flow is: + // - 1. Call BeginTable(). + // - 2. Optionally call TableSetupColumn() to submit column name/flags/defaults. + // - 3. Optionally call TableSetupScrollFreeze() to request scroll freezing of columns/rows. + // - 4. Optionally call TableHeadersRow() to submit a header row. Names are pulled from TableSetupColumn() data. + // - 5. Populate contents: + // - In most situations you can use TableNextRow() + TableSetColumnIndex(N) to start appending into a column. + // - If you are using tables as a sort of grid, where every columns is holding the same type of contents, + // you may prefer using TableNextColumn() instead of TableNextRow() + TableSetColumnIndex(). + // TableNextColumn() will automatically wrap-around into the next row if needed. + // - IMPORTANT: Comparatively to the old Columns() API, we need to call TableNextColumn() for the first column! + // - Summary of possible call flow: + // -------------------------------------------------------------------------------------------------------- + // TableNextRow() -> TableSetColumnIndex(0) -> Text("Hello 0") -> TableSetColumnIndex(1) -> Text("Hello 1") // OK + // TableNextRow() -> TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK + // TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK: TableNextColumn() automatically gets to next row! + // TableNextRow() -> Text("Hello 0") // Not OK! Missing TableSetColumnIndex() or TableNextColumn()! Text will not appear! + // -------------------------------------------------------------------------------------------------------- + // - 5. Call EndTable() + IMGUI_API bool BeginTable(const char* str_id, int column, ImGuiTableFlags flags = 0, const ImVec2& outer_size = ImVec2(0.0f, 0.0f), float inner_width = 0.0f); + IMGUI_API void EndTable(); // only call EndTable() if BeginTable() returns true! + IMGUI_API void TableNextRow(ImGuiTableRowFlags row_flags = 0, float min_row_height = 0.0f); // append into the first cell of a new row. + IMGUI_API bool TableNextColumn(); // append into the next column (or first column of next row if currently in last column). Return true when column is visible. + IMGUI_API bool TableSetColumnIndex(int column_n); // append into the specified column. Return true when column is visible. + + // Tables: Headers & Columns declaration + // - Use TableSetupColumn() to specify label, resizing policy, default width/weight, id, various other flags etc. + // - Use TableHeadersRow() to create a header row and automatically submit a TableHeader() for each column. + // Headers are required to perform: reordering, sorting, and opening the context menu. + // The context menu can also be made available in columns body using ImGuiTableFlags_ContextMenuInBody. + // - You may manually submit headers using TableNextRow() + TableHeader() calls, but this is only useful in + // some advanced use cases (e.g. adding custom widgets in header row). + // - Use TableSetupScrollFreeze() to lock columns/rows so they stay visible when scrolled. + IMGUI_API void TableSetupColumn(const char* label, ImGuiTableColumnFlags flags = 0, float init_width_or_weight = 0.0f, ImGuiID user_id = 0); + IMGUI_API void TableSetupScrollFreeze(int cols, int rows); // lock columns/rows so they stay visible when scrolled. + IMGUI_API void TableHeadersRow(); // submit all headers cells based on data provided to TableSetupColumn() + submit context menu + IMGUI_API void TableHeader(const char* label); // submit one header cell manually (rarely used) + + // Tables: Sorting + // - Call TableGetSortSpecs() to retrieve latest sort specs for the table. NULL when not sorting. + // - When 'SpecsDirty == true' you should sort your data. It will be true when sorting specs have changed + // since last call, or the first time. Make sure to set 'SpecsDirty = false' after sorting, else you may + // wastefully sort your data every frame! + // - Lifetime: don't hold on this pointer over multiple frames or past any subsequent call to BeginTable(). + IMGUI_API ImGuiTableSortSpecs* TableGetSortSpecs(); // get latest sort specs for the table (NULL if not sorting). + + // Tables: Miscellaneous functions + // - Functions args 'int column_n' treat the default value of -1 as the same as passing the current column index. + IMGUI_API int TableGetColumnCount(); // return number of columns (value passed to BeginTable) + IMGUI_API int TableGetColumnIndex(); // return current column index. + IMGUI_API int TableGetRowIndex(); // return current row index. + IMGUI_API const char* TableGetColumnName(int column_n = -1); // return "" if column didn't have a name declared by TableSetupColumn(). Pass -1 to use current column. + IMGUI_API ImGuiTableColumnFlags TableGetColumnFlags(int column_n = -1); // return column flags so you can query their Enabled/Visible/Sorted/Hovered status flags. Pass -1 to use current column. + IMGUI_API void TableSetColumnEnabled(int column_n, bool v);// change user accessible enabled/disabled state of a column. Set to false to hide the column. User can use the context menu to change this themselves (right-click in headers, or right-click in columns body with ImGuiTableFlags_ContextMenuInBody) + IMGUI_API void TableSetBgColor(ImGuiTableBgTarget target, ImU32 color, int column_n = -1); // change the color of a cell, row, or column. See ImGuiTableBgTarget_ flags for details. + + // Legacy Columns API (prefer using Tables!) + // - You can also use SameLine(pos_x) to mimic simplified columns. + IMGUI_API void Columns(int count = 1, const char* id = NULL, bool border = true); + IMGUI_API void NextColumn(); // next column, defaults to current row or next row if the current row is finished + IMGUI_API int GetColumnIndex(); // get current column index + IMGUI_API float GetColumnWidth(int column_index = -1); // get column width (in pixels). pass -1 to use current column + IMGUI_API void SetColumnWidth(int column_index, float width); // set column width (in pixels). pass -1 to use current column + IMGUI_API float GetColumnOffset(int column_index = -1); // get position of column line (in pixels, from the left side of the contents region). pass -1 to use current column, otherwise 0..GetColumnsCount() inclusive. column 0 is typically 0.0f + IMGUI_API void SetColumnOffset(int column_index, float offset_x); // set position of column line (in pixels, from the left side of the contents region). pass -1 to use current column + IMGUI_API int GetColumnsCount(); + + // Tab Bars, Tabs + IMGUI_API bool BeginTabBar(const char* str_id, ImGuiTabBarFlags flags = 0); // create and append into a TabBar + IMGUI_API void EndTabBar(); // only call EndTabBar() if BeginTabBar() returns true! + IMGUI_API bool BeginTabItem(const char* label, bool* p_open = NULL, ImGuiTabItemFlags flags = 0); // create a Tab. Returns true if the Tab is selected. + IMGUI_API void EndTabItem(); // only call EndTabItem() if BeginTabItem() returns true! + IMGUI_API bool TabItemButton(const char* label, ImGuiTabItemFlags flags = 0); // create a Tab behaving like a button. return true when clicked. cannot be selected in the tab bar. + IMGUI_API void SetTabItemClosed(const char* tab_or_docked_window_label); // notify TabBar or Docking system of a closed tab/window ahead (useful to reduce visual flicker on reorderable tab bars). For tab-bar: call after BeginTabBar() and before Tab submissions. Otherwise call with a window name. + + // Logging/Capture + // - All text output from the interface can be captured into tty/file/clipboard. By default, tree nodes are automatically opened during logging. + IMGUI_API void LogToTTY(int auto_open_depth = -1); // start logging to tty (stdout) + IMGUI_API void LogToFile(int auto_open_depth = -1, const char* filename = NULL); // start logging to file + IMGUI_API void LogToClipboard(int auto_open_depth = -1); // start logging to OS clipboard + IMGUI_API void LogFinish(); // stop logging (close file, etc.) + IMGUI_API void LogButtons(); // helper to display buttons for logging to tty/file/clipboard + IMGUI_API void LogText(const char* fmt, ...) IM_FMTARGS(1); // pass text data straight to log (without being displayed) + IMGUI_API void LogTextV(const char* fmt, va_list args) IM_FMTLIST(1); + + // Drag and Drop + // - On source items, call BeginDragDropSource(), if it returns true also call SetDragDropPayload() + EndDragDropSource(). + // - On target candidates, call BeginDragDropTarget(), if it returns true also call AcceptDragDropPayload() + EndDragDropTarget(). + // - If you stop calling BeginDragDropSource() the payload is preserved however it won't have a preview tooltip (we currently display a fallback "..." tooltip, see #1725) + // - An item can be both drag source and drop target. + IMGUI_API bool BeginDragDropSource(ImGuiDragDropFlags flags = 0); // call after submitting an item which may be dragged. when this return true, you can call SetDragDropPayload() + EndDragDropSource() + IMGUI_API bool SetDragDropPayload(const char* type, const void* data, size_t sz, ImGuiCond cond = 0); // type is a user defined string of maximum 32 characters. Strings starting with '_' are reserved for dear imgui internal types. Data is copied and held by imgui. Return true when payload has been accepted. + IMGUI_API void EndDragDropSource(); // only call EndDragDropSource() if BeginDragDropSource() returns true! + IMGUI_API bool BeginDragDropTarget(); // call after submitting an item that may receive a payload. If this returns true, you can call AcceptDragDropPayload() + EndDragDropTarget() + IMGUI_API const ImGuiPayload* AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags = 0); // accept contents of a given type. If ImGuiDragDropFlags_AcceptBeforeDelivery is set you can peek into the payload before the mouse button is released. + IMGUI_API void EndDragDropTarget(); // only call EndDragDropTarget() if BeginDragDropTarget() returns true! + IMGUI_API const ImGuiPayload* GetDragDropPayload(); // peek directly into the current payload from anywhere. may return NULL. use ImGuiPayload::IsDataType() to test for the payload type. + + // Disabling [BETA API] + // - Disable all user interactions and dim items visuals (applying style.DisabledAlpha over current colors) + // - Those can be nested but it cannot be used to enable an already disabled section (a single BeginDisabled(true) in the stack is enough to keep everything disabled) + // - BeginDisabled(false) essentially does nothing useful but is provided to facilitate use of boolean expressions. If you can avoid calling BeginDisabled(False)/EndDisabled() best to avoid it. + IMGUI_API void BeginDisabled(bool disabled = true); + IMGUI_API void EndDisabled(); + + // Clipping + // - Mouse hovering is affected by ImGui::PushClipRect() calls, unlike direct calls to ImDrawList::PushClipRect() which are render only. + IMGUI_API void PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect); + IMGUI_API void PopClipRect(); + + // Focus, Activation + // - Prefer using "SetItemDefaultFocus()" over "if (IsWindowAppearing()) SetScrollHereY()" when applicable to signify "this is the default item" + IMGUI_API void SetItemDefaultFocus(); // make last item the default focused item of a window. + IMGUI_API void SetKeyboardFocusHere(int offset = 0); // focus keyboard on the next widget. Use positive 'offset' to access sub components of a multiple component widget. Use -1 to access previous widget. + + // Item/Widgets Utilities and Query Functions + // - Most of the functions are referring to the previous Item that has been submitted. + // - See Demo Window under "Widgets->Querying Status" for an interactive visualization of most of those functions. + IMGUI_API bool IsItemHovered(ImGuiHoveredFlags flags = 0); // is the last item hovered? (and usable, aka not blocked by a popup, etc.). See ImGuiHoveredFlags for more options. + IMGUI_API bool IsItemActive(); // is the last item active? (e.g. button being held, text field being edited. This will continuously return true while holding mouse button on an item. Items that don't interact will always return false) + IMGUI_API bool IsItemFocused(); // is the last item focused for keyboard/gamepad navigation? + IMGUI_API bool IsItemClicked(ImGuiMouseButton mouse_button = 0); // is the last item hovered and mouse clicked on? (**) == IsMouseClicked(mouse_button) && IsItemHovered()Important. (**) this it NOT equivalent to the behavior of e.g. Button(). Read comments in function definition. + IMGUI_API bool IsItemVisible(); // is the last item visible? (items may be out of sight because of clipping/scrolling) + IMGUI_API bool IsItemEdited(); // did the last item modify its underlying value this frame? or was pressed? This is generally the same as the "bool" return value of many widgets. + IMGUI_API bool IsItemActivated(); // was the last item just made active (item was previously inactive). + IMGUI_API bool IsItemDeactivated(); // was the last item just made inactive (item was previously active). Useful for Undo/Redo patterns with widgets that requires continuous editing. + IMGUI_API bool IsItemDeactivatedAfterEdit(); // was the last item just made inactive and made a value change when it was active? (e.g. Slider/Drag moved). Useful for Undo/Redo patterns with widgets that requires continuous editing. Note that you may get false positives (some widgets such as Combo()/ListBox()/Selectable() will return true even when clicking an already selected item). + IMGUI_API bool IsItemToggledOpen(); // was the last item open state toggled? set by TreeNode(). + IMGUI_API bool IsAnyItemHovered(); // is any item hovered? + IMGUI_API bool IsAnyItemActive(); // is any item active? + IMGUI_API bool IsAnyItemFocused(); // is any item focused? + IMGUI_API ImVec2 GetItemRectMin(); // get upper-left bounding rectangle of the last item (screen space) + IMGUI_API ImVec2 GetItemRectMax(); // get lower-right bounding rectangle of the last item (screen space) + IMGUI_API ImVec2 GetItemRectSize(); // get size of last item + IMGUI_API void SetItemAllowOverlap(); // allow last item to be overlapped by a subsequent item. sometimes useful with invisible buttons, selectables, etc. to catch unused area. + + // Viewports + // - Currently represents the Platform Window created by the application which is hosting our Dear ImGui windows. + // - In 'docking' branch with multi-viewport enabled, we extend this concept to have multiple active viewports. + // - In the future we will extend this concept further to also represent Platform Monitor and support a "no main platform window" operation mode. + IMGUI_API ImGuiViewport* GetMainViewport(); // return primary/default viewport. This can never be NULL. + + // Miscellaneous Utilities + IMGUI_API bool IsRectVisible(const ImVec2& size); // test if rectangle (of given size, starting from cursor position) is visible / not clipped. + IMGUI_API bool IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max); // test if rectangle (in screen space) is visible / not clipped. to perform coarse clipping on user's side. + IMGUI_API double GetTime(); // get global imgui time. incremented by io.DeltaTime every frame. + IMGUI_API int GetFrameCount(); // get global imgui frame count. incremented by 1 every frame. + IMGUI_API ImDrawList* GetBackgroundDrawList(); // this draw list will be the first rendering one. Useful to quickly draw shapes/text behind dear imgui contents. + IMGUI_API ImDrawList* GetForegroundDrawList(); // this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents. + IMGUI_API ImDrawListSharedData* GetDrawListSharedData(); // you may use this when creating your own ImDrawList instances. + IMGUI_API const char* GetStyleColorName(ImGuiCol idx); // get a string corresponding to the enum value (for display, saving, etc.). + IMGUI_API void SetStateStorage(ImGuiStorage* storage); // replace current window storage with our own (if you want to manipulate it yourself, typically clear subsection of it) + IMGUI_API ImGuiStorage* GetStateStorage(); + IMGUI_API bool BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags flags = 0); // helper to create a child window / scrolling region that looks like a normal widget frame + IMGUI_API void EndChildFrame(); // always call EndChildFrame() regardless of BeginChildFrame() return values (which indicates a collapsed/clipped window) + + // Text Utilities + IMGUI_API ImVec2 CalcTextSize(const char* text, const char* text_end = NULL, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); + + // Color Utilities + IMGUI_API ImVec4 ColorConvertU32ToFloat4(ImU32 in); + IMGUI_API ImU32 ColorConvertFloat4ToU32(const ImVec4& in); + IMGUI_API void ColorConvertRGBtoHSV(float r, float g, float b, float& out_h, float& out_s, float& out_v); + IMGUI_API void ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& out_g, float& out_b); + + // Inputs Utilities: Keyboard + // Without IMGUI_DISABLE_OBSOLETE_KEYIO: (legacy support) + // - For 'ImGuiKey key' you can still use your legacy native/user indices according to how your backend/engine stored them in io.KeysDown[]. + // With IMGUI_DISABLE_OBSOLETE_KEYIO: (this is the way forward) + // - Any use of 'ImGuiKey' will assert when key < 512 will be passed, previously reserved as native/user keys indices + // - GetKeyIndex() is pass-through and therefore deprecated (gone if IMGUI_DISABLE_OBSOLETE_KEYIO is defined) + IMGUI_API bool IsKeyDown(ImGuiKey key); // is key being held. + IMGUI_API bool IsKeyPressed(ImGuiKey key, bool repeat = true); // was key pressed (went from !Down to Down)? if repeat=true, uses io.KeyRepeatDelay / KeyRepeatRate + IMGUI_API bool IsKeyReleased(ImGuiKey key); // was key released (went from Down to !Down)? + IMGUI_API int GetKeyPressedAmount(ImGuiKey key, float repeat_delay, float rate); // uses provided repeat rate/delay. return a count, most often 0 or 1 but might be >1 if RepeatRate is small enough that DeltaTime > RepeatRate + IMGUI_API const char* GetKeyName(ImGuiKey key); // [DEBUG] returns English name of the key. Those names a provided for debugging purpose and are not meant to be saved persistently not compared. + IMGUI_API void CaptureKeyboardFromApp(bool want_capture_keyboard_value = true); // attention: misleading name! manually override io.WantCaptureKeyboard flag next frame (said flag is entirely left for your application to handle). e.g. force capture keyboard when your widget is being hovered. This is equivalent to setting "io.WantCaptureKeyboard = want_capture_keyboard_value"; after the next NewFrame() call. + + // Inputs Utilities: Mouse + // - To refer to a mouse button, you may use named enums in your code e.g. ImGuiMouseButton_Left, ImGuiMouseButton_Right. + // - You can also use regular integer: it is forever guaranteed that 0=Left, 1=Right, 2=Middle. + // - Dragging operations are only reported after mouse has moved a certain distance away from the initial clicking position (see 'lock_threshold' and 'io.MouseDraggingThreshold') + IMGUI_API bool IsMouseDown(ImGuiMouseButton button); // is mouse button held? + IMGUI_API bool IsMouseClicked(ImGuiMouseButton button, bool repeat = false); // did mouse button clicked? (went from !Down to Down). Same as GetMouseClickedCount() == 1. + IMGUI_API bool IsMouseReleased(ImGuiMouseButton button); // did mouse button released? (went from Down to !Down) + IMGUI_API bool IsMouseDoubleClicked(ImGuiMouseButton button); // did mouse button double-clicked? Same as GetMouseClickedCount() == 2. (note that a double-click will also report IsMouseClicked() == true) + IMGUI_API int GetMouseClickedCount(ImGuiMouseButton button); // return the number of successive mouse-clicks at the time where a click happen (otherwise 0). + IMGUI_API bool IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip = true);// is mouse hovering given bounding rect (in screen space). clipped by current clipping settings, but disregarding of other consideration of focus/window ordering/popup-block. + IMGUI_API bool IsMousePosValid(const ImVec2* mouse_pos = NULL); // by convention we use (-FLT_MAX,-FLT_MAX) to denote that there is no mouse available + IMGUI_API bool IsAnyMouseDown(); // [WILL OBSOLETE] is any mouse button held? This was designed for backends, but prefer having backend maintain a mask of held mouse buttons, because upcoming input queue system will make this invalid. + IMGUI_API ImVec2 GetMousePos(); // shortcut to ImGui::GetIO().MousePos provided by user, to be consistent with other calls + IMGUI_API ImVec2 GetMousePosOnOpeningCurrentPopup(); // retrieve mouse position at the time of opening popup we have BeginPopup() into (helper to avoid user backing that value themselves) + IMGUI_API bool IsMouseDragging(ImGuiMouseButton button, float lock_threshold = -1.0f); // is mouse dragging? (if lock_threshold < -1.0f, uses io.MouseDraggingThreshold) + IMGUI_API ImVec2 GetMouseDragDelta(ImGuiMouseButton button = 0, float lock_threshold = -1.0f); // return the delta from the initial clicking position while the mouse button is pressed or was just released. This is locked and return 0.0f until the mouse moves past a distance threshold at least once (if lock_threshold < -1.0f, uses io.MouseDraggingThreshold) + IMGUI_API void ResetMouseDragDelta(ImGuiMouseButton button = 0); // + IMGUI_API ImGuiMouseCursor GetMouseCursor(); // get desired cursor type, reset in ImGui::NewFrame(), this is updated during the frame. valid before Render(). If you use software rendering by setting io.MouseDrawCursor ImGui will render those for you + IMGUI_API void SetMouseCursor(ImGuiMouseCursor cursor_type); // set desired cursor type + IMGUI_API void CaptureMouseFromApp(bool want_capture_mouse_value = true); // attention: misleading name! manually override io.WantCaptureMouse flag next frame (said flag is entirely left for your application to handle). This is equivalent to setting "io.WantCaptureMouse = want_capture_mouse_value;" after the next NewFrame() call. + + // Clipboard Utilities + // - Also see the LogToClipboard() function to capture GUI into clipboard, or easily output text data to the clipboard. + IMGUI_API const char* GetClipboardText(); + IMGUI_API void SetClipboardText(const char* text); + + // Settings/.Ini Utilities + // - The disk functions are automatically called if io.IniFilename != NULL (default is "imgui.ini"). + // - Set io.IniFilename to NULL to load/save manually. Read io.WantSaveIniSettings description about handling .ini saving manually. + // - Important: default value "imgui.ini" is relative to current working dir! Most apps will want to lock this to an absolute path (e.g. same path as executables). + IMGUI_API void LoadIniSettingsFromDisk(const char* ini_filename); // call after CreateContext() and before the first call to NewFrame(). NewFrame() automatically calls LoadIniSettingsFromDisk(io.IniFilename). + IMGUI_API void LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size=0); // call after CreateContext() and before the first call to NewFrame() to provide .ini data from your own data source. + IMGUI_API void SaveIniSettingsToDisk(const char* ini_filename); // this is automatically called (if io.IniFilename is not empty) a few seconds after any modification that should be reflected in the .ini file (and also by DestroyContext). + IMGUI_API const char* SaveIniSettingsToMemory(size_t* out_ini_size = NULL); // return a zero-terminated string with the .ini data which you can save by your own mean. call when io.WantSaveIniSettings is set, then save data by your own mean and clear io.WantSaveIniSettings. + + // Debug Utilities + // - This is used by the IMGUI_CHECKVERSION() macro. + IMGUI_API bool DebugCheckVersionAndDataLayout(const char* version_str, size_t sz_io, size_t sz_style, size_t sz_vec2, size_t sz_vec4, size_t sz_drawvert, size_t sz_drawidx); // This is called by IMGUI_CHECKVERSION() macro. + + // Memory Allocators + // - Those functions are not reliant on the current context. + // - DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() + // for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details. + IMGUI_API void SetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, ImGuiMemFreeFunc free_func, void* user_data = NULL); + IMGUI_API void GetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, ImGuiMemFreeFunc* p_free_func, void** p_user_data); + IMGUI_API void* MemAlloc(size_t size); + IMGUI_API void MemFree(void* ptr); + +} // namespace ImGui + +//----------------------------------------------------------------------------- +// [SECTION] Flags & Enumerations +//----------------------------------------------------------------------------- + +// Flags for ImGui::Begin() +enum ImGuiWindowFlags_ +{ + ImGuiWindowFlags_None = 0, + ImGuiWindowFlags_NoTitleBar = 1 << 0, // Disable title-bar + ImGuiWindowFlags_NoResize = 1 << 1, // Disable user resizing with the lower-right grip + ImGuiWindowFlags_NoMove = 1 << 2, // Disable user moving the window + ImGuiWindowFlags_NoScrollbar = 1 << 3, // Disable scrollbars (window can still scroll with mouse or programmatically) + ImGuiWindowFlags_NoScrollWithMouse = 1 << 4, // Disable user vertically scrolling with mouse wheel. On child window, mouse wheel will be forwarded to the parent unless NoScrollbar is also set. + ImGuiWindowFlags_NoCollapse = 1 << 5, // Disable user collapsing window by double-clicking on it. Also referred to as Window Menu Button (e.g. within a docking node). + ImGuiWindowFlags_AlwaysAutoResize = 1 << 6, // Resize every window to its content every frame + ImGuiWindowFlags_NoBackground = 1 << 7, // Disable drawing background color (WindowBg, etc.) and outside border. Similar as using SetNextWindowBgAlpha(0.0f). + ImGuiWindowFlags_NoSavedSettings = 1 << 8, // Never load/save settings in .ini file + ImGuiWindowFlags_NoMouseInputs = 1 << 9, // Disable catching mouse, hovering test with pass through. + ImGuiWindowFlags_MenuBar = 1 << 10, // Has a menu-bar + ImGuiWindowFlags_HorizontalScrollbar = 1 << 11, // Allow horizontal scrollbar to appear (off by default). You may use SetNextWindowContentSize(ImVec2(width,0.0f)); prior to calling Begin() to specify width. Read code in imgui_demo in the "Horizontal Scrolling" section. + ImGuiWindowFlags_NoFocusOnAppearing = 1 << 12, // Disable taking focus when transitioning from hidden to visible state + ImGuiWindowFlags_NoBringToFrontOnFocus = 1 << 13, // Disable bringing window to front when taking focus (e.g. clicking on it or programmatically giving it focus) + ImGuiWindowFlags_AlwaysVerticalScrollbar= 1 << 14, // Always show vertical scrollbar (even if ContentSize.y < Size.y) + ImGuiWindowFlags_AlwaysHorizontalScrollbar=1<< 15, // Always show horizontal scrollbar (even if ContentSize.x < Size.x) + ImGuiWindowFlags_AlwaysUseWindowPadding = 1 << 16, // Ensure child windows without border uses style.WindowPadding (ignored by default for non-bordered child windows, because more convenient) + ImGuiWindowFlags_NoNavInputs = 1 << 18, // No gamepad/keyboard navigation within the window + ImGuiWindowFlags_NoNavFocus = 1 << 19, // No focusing toward this window with gamepad/keyboard navigation (e.g. skipped by CTRL+TAB) + ImGuiWindowFlags_UnsavedDocument = 1 << 20, // Display a dot next to the title. When used in a tab/docking context, tab is selected when clicking the X + closure is not assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. + ImGuiWindowFlags_NoNav = ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, + ImGuiWindowFlags_NoDecoration = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse, + ImGuiWindowFlags_NoInputs = ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, + + // [Internal] + ImGuiWindowFlags_NavFlattened = 1 << 23, // [BETA] On child window: allow gamepad/keyboard navigation to cross over parent border to this child or between sibling child windows. + ImGuiWindowFlags_ChildWindow = 1 << 24, // Don't use! For internal use by BeginChild() + ImGuiWindowFlags_Tooltip = 1 << 25, // Don't use! For internal use by BeginTooltip() + ImGuiWindowFlags_Popup = 1 << 26, // Don't use! For internal use by BeginPopup() + ImGuiWindowFlags_Modal = 1 << 27, // Don't use! For internal use by BeginPopupModal() + ImGuiWindowFlags_ChildMenu = 1 << 28 // Don't use! For internal use by BeginMenu() + //ImGuiWindowFlags_ResizeFromAnySide = 1 << 17, // [Obsolete] --> Set io.ConfigWindowsResizeFromEdges=true and make sure mouse cursors are supported by backend (io.BackendFlags & ImGuiBackendFlags_HasMouseCursors) +}; + +// Flags for ImGui::InputText() +enum ImGuiInputTextFlags_ +{ + ImGuiInputTextFlags_None = 0, + ImGuiInputTextFlags_CharsDecimal = 1 << 0, // Allow 0123456789.+-*/ + ImGuiInputTextFlags_CharsHexadecimal = 1 << 1, // Allow 0123456789ABCDEFabcdef + ImGuiInputTextFlags_CharsUppercase = 1 << 2, // Turn a..z into A..Z + ImGuiInputTextFlags_CharsNoBlank = 1 << 3, // Filter out spaces, tabs + ImGuiInputTextFlags_AutoSelectAll = 1 << 4, // Select entire text when first taking mouse focus + ImGuiInputTextFlags_EnterReturnsTrue = 1 << 5, // Return 'true' when Enter is pressed (as opposed to every time the value was modified). Consider looking at the IsItemDeactivatedAfterEdit() function. + ImGuiInputTextFlags_CallbackCompletion = 1 << 6, // Callback on pressing TAB (for completion handling) + ImGuiInputTextFlags_CallbackHistory = 1 << 7, // Callback on pressing Up/Down arrows (for history handling) + ImGuiInputTextFlags_CallbackAlways = 1 << 8, // Callback on each iteration. User code may query cursor position, modify text buffer. + ImGuiInputTextFlags_CallbackCharFilter = 1 << 9, // Callback on character inputs to replace or discard them. Modify 'EventChar' to replace or discard, or return 1 in callback to discard. + ImGuiInputTextFlags_AllowTabInput = 1 << 10, // Pressing TAB input a '\t' character into the text field + ImGuiInputTextFlags_CtrlEnterForNewLine = 1 << 11, // In multi-line mode, unfocus with Enter, add new line with Ctrl+Enter (default is opposite: unfocus with Ctrl+Enter, add line with Enter). + ImGuiInputTextFlags_NoHorizontalScroll = 1 << 12, // Disable following the cursor horizontally + ImGuiInputTextFlags_AlwaysOverwrite = 1 << 13, // Overwrite mode + ImGuiInputTextFlags_ReadOnly = 1 << 14, // Read-only mode + ImGuiInputTextFlags_Password = 1 << 15, // Password mode, display all characters as '*' + ImGuiInputTextFlags_NoUndoRedo = 1 << 16, // Disable undo/redo. Note that input text owns the text data while active, if you want to provide your own undo/redo stack you need e.g. to call ClearActiveID(). + ImGuiInputTextFlags_CharsScientific = 1 << 17, // Allow 0123456789.+-*/eE (Scientific notation input) + ImGuiInputTextFlags_CallbackResize = 1 << 18, // Callback on buffer capacity changes request (beyond 'buf_size' parameter value), allowing the string to grow. Notify when the string wants to be resized (for string types which hold a cache of their Size). You will be provided a new BufSize in the callback and NEED to honor it. (see misc/cpp/imgui_stdlib.h for an example of using this) + ImGuiInputTextFlags_CallbackEdit = 1 << 19 // Callback on any edit (note that InputText() already returns true on edit, the callback is useful mainly to manipulate the underlying buffer while focus is active) + + // Obsolete names (will be removed soon) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + , ImGuiInputTextFlags_AlwaysInsertMode = ImGuiInputTextFlags_AlwaysOverwrite // [renamed in 1.82] name was not matching behavior +#endif +}; + +// Flags for ImGui::TreeNodeEx(), ImGui::CollapsingHeader*() +enum ImGuiTreeNodeFlags_ +{ + ImGuiTreeNodeFlags_None = 0, + ImGuiTreeNodeFlags_Selected = 1 << 0, // Draw as selected + ImGuiTreeNodeFlags_Framed = 1 << 1, // Draw frame with background (e.g. for CollapsingHeader) + ImGuiTreeNodeFlags_AllowItemOverlap = 1 << 2, // Hit testing to allow subsequent widgets to overlap this one + ImGuiTreeNodeFlags_NoTreePushOnOpen = 1 << 3, // Don't do a TreePush() when open (e.g. for CollapsingHeader) = no extra indent nor pushing on ID stack + ImGuiTreeNodeFlags_NoAutoOpenOnLog = 1 << 4, // Don't automatically and temporarily open node when Logging is active (by default logging will automatically open tree nodes) + ImGuiTreeNodeFlags_DefaultOpen = 1 << 5, // Default node to be open + ImGuiTreeNodeFlags_OpenOnDoubleClick = 1 << 6, // Need double-click to open node + ImGuiTreeNodeFlags_OpenOnArrow = 1 << 7, // Only open when clicking on the arrow part. If ImGuiTreeNodeFlags_OpenOnDoubleClick is also set, single-click arrow or double-click all box to open. + ImGuiTreeNodeFlags_Leaf = 1 << 8, // No collapsing, no arrow (use as a convenience for leaf nodes). + ImGuiTreeNodeFlags_Bullet = 1 << 9, // Display a bullet instead of arrow + ImGuiTreeNodeFlags_FramePadding = 1 << 10, // Use FramePadding (even for an unframed text node) to vertically align text baseline to regular widget height. Equivalent to calling AlignTextToFramePadding(). + ImGuiTreeNodeFlags_SpanAvailWidth = 1 << 11, // Extend hit box to the right-most edge, even if not framed. This is not the default in order to allow adding other items on the same line. In the future we may refactor the hit system to be front-to-back, allowing natural overlaps and then this can become the default. + ImGuiTreeNodeFlags_SpanFullWidth = 1 << 12, // Extend hit box to the left-most and right-most edges (bypass the indented area). + ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 13, // (WIP) Nav: left direction may move to this TreeNode() from any of its child (items submitted between TreeNode and TreePop) + //ImGuiTreeNodeFlags_NoScrollOnOpen = 1 << 14, // FIXME: TODO: Disable automatic scroll on TreePop() if node got just open and contents is not visible + ImGuiTreeNodeFlags_CollapsingHeader = ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_NoAutoOpenOnLog +}; + +// Flags for OpenPopup*(), BeginPopupContext*(), IsPopupOpen() functions. +// - To be backward compatible with older API which took an 'int mouse_button = 1' argument, we need to treat +// small flags values as a mouse button index, so we encode the mouse button in the first few bits of the flags. +// It is therefore guaranteed to be legal to pass a mouse button index in ImGuiPopupFlags. +// - For the same reason, we exceptionally default the ImGuiPopupFlags argument of BeginPopupContextXXX functions to 1 instead of 0. +// IMPORTANT: because the default parameter is 1 (==ImGuiPopupFlags_MouseButtonRight), if you rely on the default parameter +// and want to another another flag, you need to pass in the ImGuiPopupFlags_MouseButtonRight flag. +// - Multiple buttons currently cannot be combined/or-ed in those functions (we could allow it later). +enum ImGuiPopupFlags_ +{ + ImGuiPopupFlags_None = 0, + ImGuiPopupFlags_MouseButtonLeft = 0, // For BeginPopupContext*(): open on Left Mouse release. Guaranteed to always be == 0 (same as ImGuiMouseButton_Left) + ImGuiPopupFlags_MouseButtonRight = 1, // For BeginPopupContext*(): open on Right Mouse release. Guaranteed to always be == 1 (same as ImGuiMouseButton_Right) + ImGuiPopupFlags_MouseButtonMiddle = 2, // For BeginPopupContext*(): open on Middle Mouse release. Guaranteed to always be == 2 (same as ImGuiMouseButton_Middle) + ImGuiPopupFlags_MouseButtonMask_ = 0x1F, + ImGuiPopupFlags_MouseButtonDefault_ = 1, + ImGuiPopupFlags_NoOpenOverExistingPopup = 1 << 5, // For OpenPopup*(), BeginPopupContext*(): don't open if there's already a popup at the same level of the popup stack + ImGuiPopupFlags_NoOpenOverItems = 1 << 6, // For BeginPopupContextWindow(): don't return true when hovering items, only when hovering empty space + ImGuiPopupFlags_AnyPopupId = 1 << 7, // For IsPopupOpen(): ignore the ImGuiID parameter and test for any popup. + ImGuiPopupFlags_AnyPopupLevel = 1 << 8, // For IsPopupOpen(): search/test at any level of the popup stack (default test in the current level) + ImGuiPopupFlags_AnyPopup = ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel +}; + +// Flags for ImGui::Selectable() +enum ImGuiSelectableFlags_ +{ + ImGuiSelectableFlags_None = 0, + ImGuiSelectableFlags_DontClosePopups = 1 << 0, // Clicking this don't close parent popup window + ImGuiSelectableFlags_SpanAllColumns = 1 << 1, // Selectable frame can span all columns (text will still fit in current column) + ImGuiSelectableFlags_AllowDoubleClick = 1 << 2, // Generate press events on double clicks too + ImGuiSelectableFlags_Disabled = 1 << 3, // Cannot be selected, display grayed out text + ImGuiSelectableFlags_AllowItemOverlap = 1 << 4 // (WIP) Hit testing to allow subsequent widgets to overlap this one +}; + +// Flags for ImGui::BeginCombo() +enum ImGuiComboFlags_ +{ + ImGuiComboFlags_None = 0, + ImGuiComboFlags_PopupAlignLeft = 1 << 0, // Align the popup toward the left by default + ImGuiComboFlags_HeightSmall = 1 << 1, // Max ~4 items visible. Tip: If you want your combo popup to be a specific size you can use SetNextWindowSizeConstraints() prior to calling BeginCombo() + ImGuiComboFlags_HeightRegular = 1 << 2, // Max ~8 items visible (default) + ImGuiComboFlags_HeightLarge = 1 << 3, // Max ~20 items visible + ImGuiComboFlags_HeightLargest = 1 << 4, // As many fitting items as possible + ImGuiComboFlags_NoArrowButton = 1 << 5, // Display on the preview box without the square arrow button + ImGuiComboFlags_NoPreview = 1 << 6, // Display only a square arrow button + ImGuiComboFlags_HeightMask_ = ImGuiComboFlags_HeightSmall | ImGuiComboFlags_HeightRegular | ImGuiComboFlags_HeightLarge | ImGuiComboFlags_HeightLargest +}; + +// Flags for ImGui::BeginTabBar() +enum ImGuiTabBarFlags_ +{ + ImGuiTabBarFlags_None = 0, + ImGuiTabBarFlags_Reorderable = 1 << 0, // Allow manually dragging tabs to re-order them + New tabs are appended at the end of list + ImGuiTabBarFlags_AutoSelectNewTabs = 1 << 1, // Automatically select new tabs when they appear + ImGuiTabBarFlags_TabListPopupButton = 1 << 2, // Disable buttons to open the tab list popup + ImGuiTabBarFlags_NoCloseWithMiddleMouseButton = 1 << 3, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You can still repro this behavior on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. + ImGuiTabBarFlags_NoTabListScrollingButtons = 1 << 4, // Disable scrolling buttons (apply when fitting policy is ImGuiTabBarFlags_FittingPolicyScroll) + ImGuiTabBarFlags_NoTooltip = 1 << 5, // Disable tooltips when hovering a tab + ImGuiTabBarFlags_FittingPolicyResizeDown = 1 << 6, // Resize tabs when they don't fit + ImGuiTabBarFlags_FittingPolicyScroll = 1 << 7, // Add scroll buttons when tabs don't fit + ImGuiTabBarFlags_FittingPolicyMask_ = ImGuiTabBarFlags_FittingPolicyResizeDown | ImGuiTabBarFlags_FittingPolicyScroll, + ImGuiTabBarFlags_FittingPolicyDefault_ = ImGuiTabBarFlags_FittingPolicyResizeDown +}; + +// Flags for ImGui::BeginTabItem() +enum ImGuiTabItemFlags_ +{ + ImGuiTabItemFlags_None = 0, + ImGuiTabItemFlags_UnsavedDocument = 1 << 0, // Display a dot next to the title + tab is selected when clicking the X + closure is not assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. + ImGuiTabItemFlags_SetSelected = 1 << 1, // Trigger flag to programmatically make the tab selected when calling BeginTabItem() + ImGuiTabItemFlags_NoCloseWithMiddleMouseButton = 1 << 2, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You can still repro this behavior on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. + ImGuiTabItemFlags_NoPushId = 1 << 3, // Don't call PushID(tab->ID)/PopID() on BeginTabItem()/EndTabItem() + ImGuiTabItemFlags_NoTooltip = 1 << 4, // Disable tooltip for the given tab + ImGuiTabItemFlags_NoReorder = 1 << 5, // Disable reordering this tab or having another tab cross over this tab + ImGuiTabItemFlags_Leading = 1 << 6, // Enforce the tab position to the left of the tab bar (after the tab list popup button) + ImGuiTabItemFlags_Trailing = 1 << 7 // Enforce the tab position to the right of the tab bar (before the scrolling buttons) +}; + +// Flags for ImGui::BeginTable() +// - Important! Sizing policies have complex and subtle side effects, much more so than you would expect. +// Read comments/demos carefully + experiment with live demos to get acquainted with them. +// - The DEFAULT sizing policies are: +// - Default to ImGuiTableFlags_SizingFixedFit if ScrollX is on, or if host window has ImGuiWindowFlags_AlwaysAutoResize. +// - Default to ImGuiTableFlags_SizingStretchSame if ScrollX is off. +// - When ScrollX is off: +// - Table defaults to ImGuiTableFlags_SizingStretchSame -> all Columns defaults to ImGuiTableColumnFlags_WidthStretch with same weight. +// - Columns sizing policy allowed: Stretch (default), Fixed/Auto. +// - Fixed Columns (if any) will generally obtain their requested width (unless the table cannot fit them all). +// - Stretch Columns will share the remaining width according to their respective weight. +// - Mixed Fixed/Stretch columns is possible but has various side-effects on resizing behaviors. +// The typical use of mixing sizing policies is: any number of LEADING Fixed columns, followed by one or two TRAILING Stretch columns. +// (this is because the visible order of columns have subtle but necessary effects on how they react to manual resizing). +// - When ScrollX is on: +// - Table defaults to ImGuiTableFlags_SizingFixedFit -> all Columns defaults to ImGuiTableColumnFlags_WidthFixed +// - Columns sizing policy allowed: Fixed/Auto mostly. +// - Fixed Columns can be enlarged as needed. Table will show an horizontal scrollbar if needed. +// - When using auto-resizing (non-resizable) fixed columns, querying the content width to use item right-alignment e.g. SetNextItemWidth(-FLT_MIN) doesn't make sense, would create a feedback loop. +// - Using Stretch columns OFTEN DOES NOT MAKE SENSE if ScrollX is on, UNLESS you have specified a value for 'inner_width' in BeginTable(). +// If you specify a value for 'inner_width' then effectively the scrolling space is known and Stretch or mixed Fixed/Stretch columns become meaningful again. +// - Read on documentation at the top of imgui_tables.cpp for details. +enum ImGuiTableFlags_ +{ + // Features + ImGuiTableFlags_None = 0, + ImGuiTableFlags_Resizable = 1 << 0, // Enable resizing columns. + ImGuiTableFlags_Reorderable = 1 << 1, // Enable reordering columns in header row (need calling TableSetupColumn() + TableHeadersRow() to display headers) + ImGuiTableFlags_Hideable = 1 << 2, // Enable hiding/disabling columns in context menu. + ImGuiTableFlags_Sortable = 1 << 3, // Enable sorting. Call TableGetSortSpecs() to obtain sort specs. Also see ImGuiTableFlags_SortMulti and ImGuiTableFlags_SortTristate. + ImGuiTableFlags_NoSavedSettings = 1 << 4, // Disable persisting columns order, width and sort settings in the .ini file. + ImGuiTableFlags_ContextMenuInBody = 1 << 5, // Right-click on columns body/contents will display table context menu. By default it is available in TableHeadersRow(). + // Decorations + ImGuiTableFlags_RowBg = 1 << 6, // Set each RowBg color with ImGuiCol_TableRowBg or ImGuiCol_TableRowBgAlt (equivalent of calling TableSetBgColor with ImGuiTableBgFlags_RowBg0 on each row manually) + ImGuiTableFlags_BordersInnerH = 1 << 7, // Draw horizontal borders between rows. + ImGuiTableFlags_BordersOuterH = 1 << 8, // Draw horizontal borders at the top and bottom. + ImGuiTableFlags_BordersInnerV = 1 << 9, // Draw vertical borders between columns. + ImGuiTableFlags_BordersOuterV = 1 << 10, // Draw vertical borders on the left and right sides. + ImGuiTableFlags_BordersH = ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuterH, // Draw horizontal borders. + ImGuiTableFlags_BordersV = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersOuterV, // Draw vertical borders. + ImGuiTableFlags_BordersInner = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersInnerH, // Draw inner borders. + ImGuiTableFlags_BordersOuter = ImGuiTableFlags_BordersOuterV | ImGuiTableFlags_BordersOuterH, // Draw outer borders. + ImGuiTableFlags_Borders = ImGuiTableFlags_BordersInner | ImGuiTableFlags_BordersOuter, // Draw all borders. + ImGuiTableFlags_NoBordersInBody = 1 << 11, // [ALPHA] Disable vertical borders in columns Body (borders will always appears in Headers). -> May move to style + ImGuiTableFlags_NoBordersInBodyUntilResize = 1 << 12, // [ALPHA] Disable vertical borders in columns Body until hovered for resize (borders will always appears in Headers). -> May move to style + // Sizing Policy (read above for defaults) + ImGuiTableFlags_SizingFixedFit = 1 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching contents width. + ImGuiTableFlags_SizingFixedSame = 2 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching the maximum contents width of all columns. Implicitly enable ImGuiTableFlags_NoKeepColumnsVisible. + ImGuiTableFlags_SizingStretchProp = 3 << 13, // Columns default to _WidthStretch with default weights proportional to each columns contents widths. + ImGuiTableFlags_SizingStretchSame = 4 << 13, // Columns default to _WidthStretch with default weights all equal, unless overridden by TableSetupColumn(). + // Sizing Extra Options + ImGuiTableFlags_NoHostExtendX = 1 << 16, // Make outer width auto-fit to columns, overriding outer_size.x value. Only available when ScrollX/ScrollY are disabled and Stretch columns are not used. + ImGuiTableFlags_NoHostExtendY = 1 << 17, // Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit). Only available when ScrollX/ScrollY are disabled. Data below the limit will be clipped and not visible. + ImGuiTableFlags_NoKeepColumnsVisible = 1 << 18, // Disable keeping column always minimally visible when ScrollX is off and table gets too small. Not recommended if columns are resizable. + ImGuiTableFlags_PreciseWidths = 1 << 19, // Disable distributing remainder width to stretched columns (width allocation on a 100-wide table with 3 columns: Without this flag: 33,33,34. With this flag: 33,33,33). With larger number of columns, resizing will appear to be less smooth. + // Clipping + ImGuiTableFlags_NoClip = 1 << 20, // Disable clipping rectangle for every individual columns (reduce draw command count, items will be able to overflow into other columns). Generally incompatible with TableSetupScrollFreeze(). + // Padding + ImGuiTableFlags_PadOuterX = 1 << 21, // Default if BordersOuterV is on. Enable outer-most padding. Generally desirable if you have headers. + ImGuiTableFlags_NoPadOuterX = 1 << 22, // Default if BordersOuterV is off. Disable outer-most padding. + ImGuiTableFlags_NoPadInnerX = 1 << 23, // Disable inner padding between columns (double inner padding if BordersOuterV is on, single inner padding if BordersOuterV is off). + // Scrolling + ImGuiTableFlags_ScrollX = 1 << 24, // Enable horizontal scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. Changes default sizing policy. Because this create a child window, ScrollY is currently generally recommended when using ScrollX. + ImGuiTableFlags_ScrollY = 1 << 25, // Enable vertical scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. + // Sorting + ImGuiTableFlags_SortMulti = 1 << 26, // Hold shift when clicking headers to sort on multiple column. TableGetSortSpecs() may return specs where (SpecsCount > 1). + ImGuiTableFlags_SortTristate = 1 << 27, // Allow no sorting, disable default sorting. TableGetSortSpecs() may return specs where (SpecsCount == 0). + + // [Internal] Combinations and masks + ImGuiTableFlags_SizingMask_ = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_SizingFixedSame | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_SizingStretchSame + + // Obsolete names (will be removed soon) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + //, ImGuiTableFlags_ColumnsWidthFixed = ImGuiTableFlags_SizingFixedFit, ImGuiTableFlags_ColumnsWidthStretch = ImGuiTableFlags_SizingStretchSame // WIP Tables 2020/12 + //, ImGuiTableFlags_SizingPolicyFixed = ImGuiTableFlags_SizingFixedFit, ImGuiTableFlags_SizingPolicyStretch = ImGuiTableFlags_SizingStretchSame // WIP Tables 2021/01 +#endif +}; + +// Flags for ImGui::TableSetupColumn() +enum ImGuiTableColumnFlags_ +{ + // Input configuration flags + ImGuiTableColumnFlags_None = 0, + ImGuiTableColumnFlags_Disabled = 1 << 0, // Overriding/master disable flag: hide column, won't show in context menu (unlike calling TableSetColumnEnabled() which manipulates the user accessible state) + ImGuiTableColumnFlags_DefaultHide = 1 << 1, // Default as a hidden/disabled column. + ImGuiTableColumnFlags_DefaultSort = 1 << 2, // Default as a sorting column. + ImGuiTableColumnFlags_WidthStretch = 1 << 3, // Column will stretch. Preferable with horizontal scrolling disabled (default if table sizing policy is _SizingStretchSame or _SizingStretchProp). + ImGuiTableColumnFlags_WidthFixed = 1 << 4, // Column will not stretch. Preferable with horizontal scrolling enabled (default if table sizing policy is _SizingFixedFit and table is resizable). + ImGuiTableColumnFlags_NoResize = 1 << 5, // Disable manual resizing. + ImGuiTableColumnFlags_NoReorder = 1 << 6, // Disable manual reordering this column, this will also prevent other columns from crossing over this column. + ImGuiTableColumnFlags_NoHide = 1 << 7, // Disable ability to hide/disable this column. + ImGuiTableColumnFlags_NoClip = 1 << 8, // Disable clipping for this column (all NoClip columns will render in a same draw command). + ImGuiTableColumnFlags_NoSort = 1 << 9, // Disable ability to sort on this field (even if ImGuiTableFlags_Sortable is set on the table). + ImGuiTableColumnFlags_NoSortAscending = 1 << 10, // Disable ability to sort in the ascending direction. + ImGuiTableColumnFlags_NoSortDescending = 1 << 11, // Disable ability to sort in the descending direction. + ImGuiTableColumnFlags_NoHeaderLabel = 1 << 12, // TableHeadersRow() will not submit label for this column. Convenient for some small columns. Name will still appear in context menu. + ImGuiTableColumnFlags_NoHeaderWidth = 1 << 13, // Disable header text width contribution to automatic column width. + ImGuiTableColumnFlags_PreferSortAscending = 1 << 14, // Make the initial sort direction Ascending when first sorting on this column (default). + ImGuiTableColumnFlags_PreferSortDescending = 1 << 15, // Make the initial sort direction Descending when first sorting on this column. + ImGuiTableColumnFlags_IndentEnable = 1 << 16, // Use current Indent value when entering cell (default for column 0). + ImGuiTableColumnFlags_IndentDisable = 1 << 17, // Ignore current Indent value when entering cell (default for columns > 0). Indentation changes _within_ the cell will still be honored. + + // Output status flags, read-only via TableGetColumnFlags() + ImGuiTableColumnFlags_IsEnabled = 1 << 24, // Status: is enabled == not hidden by user/api (referred to as "Hide" in _DefaultHide and _NoHide) flags. + ImGuiTableColumnFlags_IsVisible = 1 << 25, // Status: is visible == is enabled AND not clipped by scrolling. + ImGuiTableColumnFlags_IsSorted = 1 << 26, // Status: is currently part of the sort specs + ImGuiTableColumnFlags_IsHovered = 1 << 27, // Status: is hovered by mouse + + // [Internal] Combinations and masks + ImGuiTableColumnFlags_WidthMask_ = ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_WidthFixed, + ImGuiTableColumnFlags_IndentMask_ = ImGuiTableColumnFlags_IndentEnable | ImGuiTableColumnFlags_IndentDisable, + ImGuiTableColumnFlags_StatusMask_ = ImGuiTableColumnFlags_IsEnabled | ImGuiTableColumnFlags_IsVisible | ImGuiTableColumnFlags_IsSorted | ImGuiTableColumnFlags_IsHovered, + ImGuiTableColumnFlags_NoDirectResize_ = 1 << 30 // [Internal] Disable user resizing this column directly (it may however we resized indirectly from its left edge) + + // Obsolete names (will be removed soon) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + //ImGuiTableColumnFlags_WidthAuto = ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize, // Column will not stretch and keep resizing based on submitted contents. +#endif +}; + +// Flags for ImGui::TableNextRow() +enum ImGuiTableRowFlags_ +{ + ImGuiTableRowFlags_None = 0, + ImGuiTableRowFlags_Headers = 1 << 0 // Identify header row (set default background color + width of its contents accounted differently for auto column width) +}; + +// Enum for ImGui::TableSetBgColor() +// Background colors are rendering in 3 layers: +// - Layer 0: draw with RowBg0 color if set, otherwise draw with ColumnBg0 if set. +// - Layer 1: draw with RowBg1 color if set, otherwise draw with ColumnBg1 if set. +// - Layer 2: draw with CellBg color if set. +// The purpose of the two row/columns layers is to let you decide if a background color changes should override or blend with the existing color. +// When using ImGuiTableFlags_RowBg on the table, each row has the RowBg0 color automatically set for odd/even rows. +// If you set the color of RowBg0 target, your color will override the existing RowBg0 color. +// If you set the color of RowBg1 or ColumnBg1 target, your color will blend over the RowBg0 color. +enum ImGuiTableBgTarget_ +{ + ImGuiTableBgTarget_None = 0, + ImGuiTableBgTarget_RowBg0 = 1, // Set row background color 0 (generally used for background, automatically set when ImGuiTableFlags_RowBg is used) + ImGuiTableBgTarget_RowBg1 = 2, // Set row background color 1 (generally used for selection marking) + ImGuiTableBgTarget_CellBg = 3 // Set cell background color (top-most color) +}; + +// Flags for ImGui::IsWindowFocused() +enum ImGuiFocusedFlags_ +{ + ImGuiFocusedFlags_None = 0, + ImGuiFocusedFlags_ChildWindows = 1 << 0, // Return true if any children of the window is focused + ImGuiFocusedFlags_RootWindow = 1 << 1, // Test from root window (top most parent of the current hierarchy) + ImGuiFocusedFlags_AnyWindow = 1 << 2, // Return true if any window is focused. Important: If you are trying to tell how to dispatch your low-level inputs, do NOT use this. Use 'io.WantCaptureMouse' instead! Please read the FAQ! + ImGuiFocusedFlags_NoPopupHierarchy = 1 << 3, // Do not consider popup hierarchy (do not treat popup emitter as parent of popup) (when used with _ChildWindows or _RootWindow) + //ImGuiFocusedFlags_DockHierarchy = 1 << 4, // Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) + ImGuiFocusedFlags_RootAndChildWindows = ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows +}; + +// Flags for ImGui::IsItemHovered(), ImGui::IsWindowHovered() +// Note: if you are trying to check whether your mouse should be dispatched to Dear ImGui or to your app, you should use 'io.WantCaptureMouse' instead! Please read the FAQ! +// Note: windows with the ImGuiWindowFlags_NoInputs flag are ignored by IsWindowHovered() calls. +enum ImGuiHoveredFlags_ +{ + ImGuiHoveredFlags_None = 0, // Return true if directly over the item/window, not obstructed by another window, not obstructed by an active popup or modal blocking inputs under them. + ImGuiHoveredFlags_ChildWindows = 1 << 0, // IsWindowHovered() only: Return true if any children of the window is hovered + ImGuiHoveredFlags_RootWindow = 1 << 1, // IsWindowHovered() only: Test from root window (top most parent of the current hierarchy) + ImGuiHoveredFlags_AnyWindow = 1 << 2, // IsWindowHovered() only: Return true if any window is hovered + ImGuiHoveredFlags_NoPopupHierarchy = 1 << 3, // IsWindowHovered() only: Do not consider popup hierarchy (do not treat popup emitter as parent of popup) (when used with _ChildWindows or _RootWindow) + //ImGuiHoveredFlags_DockHierarchy = 1 << 4, // IsWindowHovered() only: Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) + ImGuiHoveredFlags_AllowWhenBlockedByPopup = 1 << 5, // Return true even if a popup window is normally blocking access to this item/window + //ImGuiHoveredFlags_AllowWhenBlockedByModal = 1 << 6, // Return true even if a modal popup window is normally blocking access to this item/window. FIXME-TODO: Unavailable yet. + ImGuiHoveredFlags_AllowWhenBlockedByActiveItem = 1 << 7, // Return true even if an active item is blocking access to this item/window. Useful for Drag and Drop patterns. + ImGuiHoveredFlags_AllowWhenOverlapped = 1 << 8, // IsItemHovered() only: Return true even if the position is obstructed or overlapped by another window + ImGuiHoveredFlags_AllowWhenDisabled = 1 << 9, // IsItemHovered() only: Return true even if the item is disabled + ImGuiHoveredFlags_RectOnly = ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenOverlapped, + ImGuiHoveredFlags_RootAndChildWindows = ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows +}; + +// Flags for ImGui::BeginDragDropSource(), ImGui::AcceptDragDropPayload() +enum ImGuiDragDropFlags_ +{ + ImGuiDragDropFlags_None = 0, + // BeginDragDropSource() flags + ImGuiDragDropFlags_SourceNoPreviewTooltip = 1 << 0, // By default, a successful call to BeginDragDropSource opens a tooltip so you can display a preview or description of the source contents. This flag disable this behavior. + ImGuiDragDropFlags_SourceNoDisableHover = 1 << 1, // By default, when dragging we clear data so that IsItemHovered() will return false, to avoid subsequent user code submitting tooltips. This flag disable this behavior so you can still call IsItemHovered() on the source item. + ImGuiDragDropFlags_SourceNoHoldToOpenOthers = 1 << 2, // Disable the behavior that allows to open tree nodes and collapsing header by holding over them while dragging a source item. + ImGuiDragDropFlags_SourceAllowNullID = 1 << 3, // Allow items such as Text(), Image() that have no unique identifier to be used as drag source, by manufacturing a temporary identifier based on their window-relative position. This is extremely unusual within the dear imgui ecosystem and so we made it explicit. + ImGuiDragDropFlags_SourceExtern = 1 << 4, // External source (from outside of dear imgui), won't attempt to read current item/window info. Will always return true. Only one Extern source can be active simultaneously. + ImGuiDragDropFlags_SourceAutoExpirePayload = 1 << 5, // Automatically expire the payload if the source cease to be submitted (otherwise payloads are persisting while being dragged) + // AcceptDragDropPayload() flags + ImGuiDragDropFlags_AcceptBeforeDelivery = 1 << 10, // AcceptDragDropPayload() will returns true even before the mouse button is released. You can then call IsDelivery() to test if the payload needs to be delivered. + ImGuiDragDropFlags_AcceptNoDrawDefaultRect = 1 << 11, // Do not draw the default highlight rectangle when hovering over target. + ImGuiDragDropFlags_AcceptNoPreviewTooltip = 1 << 12, // Request hiding the BeginDragDropSource tooltip from the BeginDragDropTarget site. + ImGuiDragDropFlags_AcceptPeekOnly = ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect // For peeking ahead and inspecting the payload before delivery. +}; + +// Standard Drag and Drop payload types. You can define you own payload types using short strings. Types starting with '_' are defined by Dear ImGui. +#define IMGUI_PAYLOAD_TYPE_COLOR_3F "_COL3F" // float[3]: Standard type for colors, without alpha. User code may use this type. +#define IMGUI_PAYLOAD_TYPE_COLOR_4F "_COL4F" // float[4]: Standard type for colors. User code may use this type. + +// A primary data type +enum ImGuiDataType_ +{ + ImGuiDataType_S8, // signed char / char (with sensible compilers) + ImGuiDataType_U8, // unsigned char + ImGuiDataType_S16, // short + ImGuiDataType_U16, // unsigned short + ImGuiDataType_S32, // int + ImGuiDataType_U32, // unsigned int + ImGuiDataType_S64, // long long / __int64 + ImGuiDataType_U64, // unsigned long long / unsigned __int64 + ImGuiDataType_Float, // float + ImGuiDataType_Double, // double + ImGuiDataType_COUNT +}; + +// A cardinal direction +enum ImGuiDir_ +{ + ImGuiDir_None = -1, + ImGuiDir_Left = 0, + ImGuiDir_Right = 1, + ImGuiDir_Up = 2, + ImGuiDir_Down = 3, + ImGuiDir_COUNT +}; + +// A sorting direction +enum ImGuiSortDirection_ +{ + ImGuiSortDirection_None = 0, + ImGuiSortDirection_Ascending = 1, // Ascending = 0->9, A->Z etc. + ImGuiSortDirection_Descending = 2 // Descending = 9->0, Z->A etc. +}; + +enum ImGuiKey_ +{ + // Keyboard + ImGuiKey_None = 0, + ImGuiKey_Tab = 512, // == ImGuiKey_NamedKey_BEGIN + ImGuiKey_LeftArrow, + ImGuiKey_RightArrow, + ImGuiKey_UpArrow, + ImGuiKey_DownArrow, + ImGuiKey_PageUp, + ImGuiKey_PageDown, + ImGuiKey_Home, + ImGuiKey_End, + ImGuiKey_Insert, + ImGuiKey_Delete, + ImGuiKey_Backspace, + ImGuiKey_Space, + ImGuiKey_Enter, + ImGuiKey_Escape, + ImGuiKey_LeftCtrl, ImGuiKey_LeftShift, ImGuiKey_LeftAlt, ImGuiKey_LeftSuper, + ImGuiKey_RightCtrl, ImGuiKey_RightShift, ImGuiKey_RightAlt, ImGuiKey_RightSuper, + ImGuiKey_Menu, + ImGuiKey_0, ImGuiKey_1, ImGuiKey_2, ImGuiKey_3, ImGuiKey_4, ImGuiKey_5, ImGuiKey_6, ImGuiKey_7, ImGuiKey_8, ImGuiKey_9, + ImGuiKey_A, ImGuiKey_B, ImGuiKey_C, ImGuiKey_D, ImGuiKey_E, ImGuiKey_F, ImGuiKey_G, ImGuiKey_H, ImGuiKey_I, ImGuiKey_J, + ImGuiKey_K, ImGuiKey_L, ImGuiKey_M, ImGuiKey_N, ImGuiKey_O, ImGuiKey_P, ImGuiKey_Q, ImGuiKey_R, ImGuiKey_S, ImGuiKey_T, + ImGuiKey_U, ImGuiKey_V, ImGuiKey_W, ImGuiKey_X, ImGuiKey_Y, ImGuiKey_Z, + ImGuiKey_F1, ImGuiKey_F2, ImGuiKey_F3, ImGuiKey_F4, ImGuiKey_F5, ImGuiKey_F6, + ImGuiKey_F7, ImGuiKey_F8, ImGuiKey_F9, ImGuiKey_F10, ImGuiKey_F11, ImGuiKey_F12, + ImGuiKey_Apostrophe, // ' + ImGuiKey_Comma, // , + ImGuiKey_Minus, // - + ImGuiKey_Period, // . + ImGuiKey_Slash, // / + ImGuiKey_Semicolon, // ; + ImGuiKey_Equal, // = + ImGuiKey_LeftBracket, // [ + ImGuiKey_Backslash, // \ (this text inhibit multiline comment caused by backslash) + ImGuiKey_RightBracket, // ] + ImGuiKey_GraveAccent, // ` + ImGuiKey_CapsLock, + ImGuiKey_ScrollLock, + ImGuiKey_NumLock, + ImGuiKey_PrintScreen, + ImGuiKey_Pause, + ImGuiKey_Keypad0, ImGuiKey_Keypad1, ImGuiKey_Keypad2, ImGuiKey_Keypad3, ImGuiKey_Keypad4, + ImGuiKey_Keypad5, ImGuiKey_Keypad6, ImGuiKey_Keypad7, ImGuiKey_Keypad8, ImGuiKey_Keypad9, + ImGuiKey_KeypadDecimal, + ImGuiKey_KeypadDivide, + ImGuiKey_KeypadMultiply, + ImGuiKey_KeypadSubtract, + ImGuiKey_KeypadAdd, + ImGuiKey_KeypadEnter, + ImGuiKey_KeypadEqual, + + // Gamepad (some of those are analog values, 0.0f to 1.0f) // NAVIGATION action + ImGuiKey_GamepadStart, // Menu (Xbox) + (Switch) Start/Options (PS) // -- + ImGuiKey_GamepadBack, // View (Xbox) - (Switch) Share (PS) // -- + ImGuiKey_GamepadFaceUp, // Y (Xbox) X (Switch) Triangle (PS) // -> ImGuiNavInput_Input + ImGuiKey_GamepadFaceDown, // A (Xbox) B (Switch) Cross (PS) // -> ImGuiNavInput_Activate + ImGuiKey_GamepadFaceLeft, // X (Xbox) Y (Switch) Square (PS) // -> ImGuiNavInput_Menu + ImGuiKey_GamepadFaceRight, // B (Xbox) A (Switch) Circle (PS) // -> ImGuiNavInput_Cancel + ImGuiKey_GamepadDpadUp, // D-pad Up // -> ImGuiNavInput_DpadUp + ImGuiKey_GamepadDpadDown, // D-pad Down // -> ImGuiNavInput_DpadDown + ImGuiKey_GamepadDpadLeft, // D-pad Left // -> ImGuiNavInput_DpadLeft + ImGuiKey_GamepadDpadRight, // D-pad Right // -> ImGuiNavInput_DpadRight + ImGuiKey_GamepadL1, // L Bumper (Xbox) L (Switch) L1 (PS) // -> ImGuiNavInput_FocusPrev + ImGuiNavInput_TweakSlow + ImGuiKey_GamepadR1, // R Bumper (Xbox) R (Switch) R1 (PS) // -> ImGuiNavInput_FocusNext + ImGuiNavInput_TweakFast + ImGuiKey_GamepadL2, // L Trigger (Xbox) ZL (Switch) L2 (PS) [Analog] + ImGuiKey_GamepadR2, // R Trigger (Xbox) ZR (Switch) R2 (PS) [Analog] + ImGuiKey_GamepadL3, // L Thumbstick (Xbox) L3 (Switch) L3 (PS) + ImGuiKey_GamepadR3, // R Thumbstick (Xbox) R3 (Switch) R3 (PS) + ImGuiKey_GamepadLStickUp, // [Analog] // -> ImGuiNavInput_LStickUp + ImGuiKey_GamepadLStickDown, // [Analog] // -> ImGuiNavInput_LStickDown + ImGuiKey_GamepadLStickLeft, // [Analog] // -> ImGuiNavInput_LStickLeft + ImGuiKey_GamepadLStickRight, // [Analog] // -> ImGuiNavInput_LStickRight + ImGuiKey_GamepadRStickUp, // [Analog] + ImGuiKey_GamepadRStickDown, // [Analog] + ImGuiKey_GamepadRStickLeft, // [Analog] + ImGuiKey_GamepadRStickRight, // [Analog] + + // Keyboard Modifiers + // - This is mirroring the data also written to io.KeyCtrl, io.KeyShift, io.KeyAlt, io.KeySuper, in a format allowing + // them to be accessed via standard key API, allowing calls such as IsKeyPressed(), IsKeyReleased(), querying duration etc. + // - Code polling every keys (e.g. an interface to detect a key press for input mapping) might want to ignore those + // and prefer using the real keys (e.g. ImGuiKey_LeftCtrl, ImGuiKey_RightCtrl instead of ImGuiKey_ModCtrl). + // - In theory the value of keyboard modifiers should be roughly equivalent to a logical or of the equivalent left/right keys. + // In practice: it's complicated; mods are often provided from different sources. Keyboard layout, IME, sticky keys and + // backends tend to interfere and break that equivalence. The safer decision is to relay that ambiguity down to the end-user... + ImGuiKey_ModCtrl, + ImGuiKey_ModShift, + ImGuiKey_ModAlt, + ImGuiKey_ModSuper, + + ImGuiKey_COUNT, // No valid ImGuiKey is ever greater than this value + + // [Internal] Prior to 1.87 we required user to fill io.KeysDown[512] using their own native index + a io.KeyMap[] array. + // We are ditching this method but keeping a legacy path for user code doing e.g. IsKeyPressed(MY_NATIVE_KEY_CODE) + ImGuiKey_NamedKey_BEGIN = 512, + ImGuiKey_NamedKey_END = ImGuiKey_COUNT, + ImGuiKey_NamedKey_COUNT = ImGuiKey_NamedKey_END - ImGuiKey_NamedKey_BEGIN, +#ifdef IMGUI_DISABLE_OBSOLETE_KEYIO + ImGuiKey_KeysData_SIZE = ImGuiKey_NamedKey_COUNT, // Size of KeysData[]: only hold named keys + ImGuiKey_KeysData_OFFSET = ImGuiKey_NamedKey_BEGIN // First key stored in KeysData[0] +#else + ImGuiKey_KeysData_SIZE = ImGuiKey_COUNT, // Size of KeysData[]: hold legacy 0..512 keycodes + named keys + ImGuiKey_KeysData_OFFSET = 0 // First key stored in KeysData[0] +#endif + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + , ImGuiKey_KeyPadEnter = ImGuiKey_KeypadEnter // Renamed in 1.87 +#endif +}; + +// Helper "flags" version of key-mods to store and compare multiple key-mods easily. Sometimes used for storage (e.g. io.KeyMods) but otherwise not much used in public API. +enum ImGuiKeyModFlags_ +{ + ImGuiKeyModFlags_None = 0, + ImGuiKeyModFlags_Ctrl = 1 << 0, + ImGuiKeyModFlags_Shift = 1 << 1, + ImGuiKeyModFlags_Alt = 1 << 2, + ImGuiKeyModFlags_Super = 1 << 3 // Cmd/Super/Windows key +}; + +// Gamepad/Keyboard navigation +// Keyboard: Set io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard to enable. NewFrame() will automatically fill io.NavInputs[] based on your io.AddKeyEvent() calls. +// Gamepad: Set io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad to enable. Backend: set ImGuiBackendFlags_HasGamepad and fill the io.NavInputs[] fields before calling NewFrame(). Note that io.NavInputs[] is cleared by EndFrame(). +// Read instructions in imgui.cpp for more details. Download PNG/PSD at http://dearimgui.org/controls_sheets. +enum ImGuiNavInput_ +{ + // Gamepad Mapping + ImGuiNavInput_Activate, // Activate / Open / Toggle / Tweak value // e.g. Cross (PS4), A (Xbox), A (Switch), Space (Keyboard) + ImGuiNavInput_Cancel, // Cancel / Close / Exit // e.g. Circle (PS4), B (Xbox), B (Switch), Escape (Keyboard) + ImGuiNavInput_Input, // Text input / On-Screen keyboard // e.g. Triang.(PS4), Y (Xbox), X (Switch), Return (Keyboard) + ImGuiNavInput_Menu, // Tap: Toggle menu / Hold: Focus, Move, Resize // e.g. Square (PS4), X (Xbox), Y (Switch), Alt (Keyboard) + ImGuiNavInput_DpadLeft, // Move / Tweak / Resize window (w/ PadMenu) // e.g. D-pad Left/Right/Up/Down (Gamepads), Arrow keys (Keyboard) + ImGuiNavInput_DpadRight, // + ImGuiNavInput_DpadUp, // + ImGuiNavInput_DpadDown, // + ImGuiNavInput_LStickLeft, // Scroll / Move window (w/ PadMenu) // e.g. Left Analog Stick Left/Right/Up/Down + ImGuiNavInput_LStickRight, // + ImGuiNavInput_LStickUp, // + ImGuiNavInput_LStickDown, // + ImGuiNavInput_FocusPrev, // Focus Next window (w/ PadMenu) // e.g. L1 or L2 (PS4), LB or LT (Xbox), L or ZL (Switch) + ImGuiNavInput_FocusNext, // Focus Prev window (w/ PadMenu) // e.g. R1 or R2 (PS4), RB or RT (Xbox), R or ZL (Switch) + ImGuiNavInput_TweakSlow, // Slower tweaks // e.g. L1 or L2 (PS4), LB or LT (Xbox), L or ZL (Switch) + ImGuiNavInput_TweakFast, // Faster tweaks // e.g. R1 or R2 (PS4), RB or RT (Xbox), R or ZL (Switch) + + // [Internal] Don't use directly! This is used internally to differentiate keyboard from gamepad inputs for behaviors that require to differentiate them. + // Keyboard behavior that have no corresponding gamepad mapping (e.g. CTRL+TAB) will be directly reading from keyboard keys instead of io.NavInputs[]. + ImGuiNavInput_KeyLeft_, // Move left // = Arrow keys + ImGuiNavInput_KeyRight_, // Move right + ImGuiNavInput_KeyUp_, // Move up + ImGuiNavInput_KeyDown_, // Move down + ImGuiNavInput_COUNT +}; + +// Configuration flags stored in io.ConfigFlags. Set by user/application. +enum ImGuiConfigFlags_ +{ + ImGuiConfigFlags_None = 0, + ImGuiConfigFlags_NavEnableKeyboard = 1 << 0, // Master keyboard navigation enable flag. NewFrame() will automatically fill io.NavInputs[] based on io.AddKeyEvent() calls + ImGuiConfigFlags_NavEnableGamepad = 1 << 1, // Master gamepad navigation enable flag. This is mostly to instruct your imgui backend to fill io.NavInputs[]. Backend also needs to set ImGuiBackendFlags_HasGamepad. + ImGuiConfigFlags_NavEnableSetMousePos = 1 << 2, // Instruct navigation to move the mouse cursor. May be useful on TV/console systems where moving a virtual mouse is awkward. Will update io.MousePos and set io.WantSetMousePos=true. If enabled you MUST honor io.WantSetMousePos requests in your backend, otherwise ImGui will react as if the mouse is jumping around back and forth. + ImGuiConfigFlags_NavNoCaptureKeyboard = 1 << 3, // Instruct navigation to not set the io.WantCaptureKeyboard flag when io.NavActive is set. + ImGuiConfigFlags_NoMouse = 1 << 4, // Instruct imgui to clear mouse position/buttons in NewFrame(). This allows ignoring the mouse information set by the backend. + ImGuiConfigFlags_NoMouseCursorChange = 1 << 5, // Instruct backend to not alter mouse cursor shape and visibility. Use if the backend cursor changes are interfering with yours and you don't want to use SetMouseCursor() to change mouse cursor. You may want to honor requests from imgui by reading GetMouseCursor() yourself instead. + + // User storage (to allow your backend/engine to communicate to code that may be shared between multiple projects. Those flags are not used by core Dear ImGui) + ImGuiConfigFlags_IsSRGB = 1 << 20, // Application is SRGB-aware. + ImGuiConfigFlags_IsTouchScreen = 1 << 21 // Application is using a touch screen instead of a mouse. +}; + +// Backend capabilities flags stored in io.BackendFlags. Set by imgui_impl_xxx or custom backend. +enum ImGuiBackendFlags_ +{ + ImGuiBackendFlags_None = 0, + ImGuiBackendFlags_HasGamepad = 1 << 0, // Backend Platform supports gamepad and currently has one connected. + ImGuiBackendFlags_HasMouseCursors = 1 << 1, // Backend Platform supports honoring GetMouseCursor() value to change the OS cursor shape. + ImGuiBackendFlags_HasSetMousePos = 1 << 2, // Backend Platform supports io.WantSetMousePos requests to reposition the OS mouse position (only used if ImGuiConfigFlags_NavEnableSetMousePos is set). + ImGuiBackendFlags_RendererHasVtxOffset = 1 << 3 // Backend Renderer supports ImDrawCmd::VtxOffset. This enables output of large meshes (64K+ vertices) while still using 16-bit indices. +}; + +// Enumeration for PushStyleColor() / PopStyleColor() +enum ImGuiCol_ +{ + ImGuiCol_Text, + ImGuiCol_TextDisabled, + ImGuiCol_WindowBg, // Background of normal windows + ImGuiCol_ChildBg, // Background of child windows + ImGuiCol_PopupBg, // Background of popups, menus, tooltips windows + ImGuiCol_Border, + ImGuiCol_BorderShadow, + ImGuiCol_FrameBg, // Background of checkbox, radio button, plot, slider, text input + ImGuiCol_FrameBgHovered, + ImGuiCol_FrameBgActive, + ImGuiCol_TitleBg, + ImGuiCol_TitleBgActive, + ImGuiCol_TitleBgCollapsed, + ImGuiCol_MenuBarBg, + ImGuiCol_ScrollbarBg, + ImGuiCol_ScrollbarGrab, + ImGuiCol_ScrollbarGrabHovered, + ImGuiCol_ScrollbarGrabActive, + ImGuiCol_CheckMark, + ImGuiCol_SliderGrab, + ImGuiCol_SliderGrabActive, + ImGuiCol_Button, + ImGuiCol_ButtonHovered, + ImGuiCol_ButtonActive, + ImGuiCol_Header, // Header* colors are used for CollapsingHeader, TreeNode, Selectable, MenuItem + ImGuiCol_HeaderHovered, + ImGuiCol_HeaderActive, + ImGuiCol_Separator, + ImGuiCol_SeparatorHovered, + ImGuiCol_SeparatorActive, + ImGuiCol_ResizeGrip, + ImGuiCol_ResizeGripHovered, + ImGuiCol_ResizeGripActive, + ImGuiCol_Tab, + ImGuiCol_TabHovered, + ImGuiCol_TabActive, + ImGuiCol_TabUnfocused, + ImGuiCol_TabUnfocusedActive, + ImGuiCol_PlotLines, + ImGuiCol_PlotLinesHovered, + ImGuiCol_PlotHistogram, + ImGuiCol_PlotHistogramHovered, + ImGuiCol_TableHeaderBg, // Table header background + ImGuiCol_TableBorderStrong, // Table outer and header borders (prefer using Alpha=1.0 here) + ImGuiCol_TableBorderLight, // Table inner borders (prefer using Alpha=1.0 here) + ImGuiCol_TableRowBg, // Table row background (even rows) + ImGuiCol_TableRowBgAlt, // Table row background (odd rows) + ImGuiCol_TextSelectedBg, + ImGuiCol_DragDropTarget, + ImGuiCol_NavHighlight, // Gamepad/keyboard: current highlighted item + ImGuiCol_NavWindowingHighlight, // Highlight window when using CTRL+TAB + ImGuiCol_NavWindowingDimBg, // Darken/colorize entire screen behind the CTRL+TAB window list, when active + ImGuiCol_ModalWindowDimBg, // Darken/colorize entire screen behind a modal window, when one is active + ImGuiCol_COUNT +}; + +// Enumeration for PushStyleVar() / PopStyleVar() to temporarily modify the ImGuiStyle structure. +// - The enum only refers to fields of ImGuiStyle which makes sense to be pushed/popped inside UI code. +// During initialization or between frames, feel free to just poke into ImGuiStyle directly. +// - Tip: Use your programming IDE navigation facilities on the names in the _second column_ below to find the actual members and their description. +// In Visual Studio IDE: CTRL+comma ("Edit.NavigateTo") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. +// - When changing this enum, you need to update the associated internal table GStyleVarInfo[] accordingly. This is where we link enum values to members offset/type. +enum ImGuiStyleVar_ +{ + // Enum name --------------------- // Member in ImGuiStyle structure (see ImGuiStyle for descriptions) + ImGuiStyleVar_Alpha, // float Alpha + ImGuiStyleVar_DisabledAlpha, // float DisabledAlpha + ImGuiStyleVar_WindowPadding, // ImVec2 WindowPadding + ImGuiStyleVar_WindowRounding, // float WindowRounding + ImGuiStyleVar_WindowBorderSize, // float WindowBorderSize + ImGuiStyleVar_WindowMinSize, // ImVec2 WindowMinSize + ImGuiStyleVar_WindowTitleAlign, // ImVec2 WindowTitleAlign + ImGuiStyleVar_ChildRounding, // float ChildRounding + ImGuiStyleVar_ChildBorderSize, // float ChildBorderSize + ImGuiStyleVar_PopupRounding, // float PopupRounding + ImGuiStyleVar_PopupBorderSize, // float PopupBorderSize + ImGuiStyleVar_FramePadding, // ImVec2 FramePadding + ImGuiStyleVar_FrameRounding, // float FrameRounding + ImGuiStyleVar_FrameBorderSize, // float FrameBorderSize + ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing + ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing + ImGuiStyleVar_IndentSpacing, // float IndentSpacing + ImGuiStyleVar_CellPadding, // ImVec2 CellPadding + ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize + ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding + ImGuiStyleVar_GrabMinSize, // float GrabMinSize + ImGuiStyleVar_GrabRounding, // float GrabRounding + ImGuiStyleVar_TabRounding, // float TabRounding + ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign + ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign + ImGuiStyleVar_COUNT +}; + +// Flags for InvisibleButton() [extended in imgui_internal.h] +enum ImGuiButtonFlags_ +{ + ImGuiButtonFlags_None = 0, + ImGuiButtonFlags_MouseButtonLeft = 1 << 0, // React on left mouse button (default) + ImGuiButtonFlags_MouseButtonRight = 1 << 1, // React on right mouse button + ImGuiButtonFlags_MouseButtonMiddle = 1 << 2, // React on center mouse button + + // [Internal] + ImGuiButtonFlags_MouseButtonMask_ = ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle, + ImGuiButtonFlags_MouseButtonDefault_ = ImGuiButtonFlags_MouseButtonLeft +}; + +// Flags for ColorEdit3() / ColorEdit4() / ColorPicker3() / ColorPicker4() / ColorButton() +enum ImGuiColorEditFlags_ +{ + ImGuiColorEditFlags_None = 0, + ImGuiColorEditFlags_NoAlpha = 1 << 1, // // ColorEdit, ColorPicker, ColorButton: ignore Alpha component (will only read 3 components from the input pointer). + ImGuiColorEditFlags_NoPicker = 1 << 2, // // ColorEdit: disable picker when clicking on color square. + ImGuiColorEditFlags_NoOptions = 1 << 3, // // ColorEdit: disable toggling options menu when right-clicking on inputs/small preview. + ImGuiColorEditFlags_NoSmallPreview = 1 << 4, // // ColorEdit, ColorPicker: disable color square preview next to the inputs. (e.g. to show only the inputs) + ImGuiColorEditFlags_NoInputs = 1 << 5, // // ColorEdit, ColorPicker: disable inputs sliders/text widgets (e.g. to show only the small preview color square). + ImGuiColorEditFlags_NoTooltip = 1 << 6, // // ColorEdit, ColorPicker, ColorButton: disable tooltip when hovering the preview. + ImGuiColorEditFlags_NoLabel = 1 << 7, // // ColorEdit, ColorPicker: disable display of inline text label (the label is still forwarded to the tooltip and picker). + ImGuiColorEditFlags_NoSidePreview = 1 << 8, // // ColorPicker: disable bigger color preview on right side of the picker, use small color square preview instead. + ImGuiColorEditFlags_NoDragDrop = 1 << 9, // // ColorEdit: disable drag and drop target. ColorButton: disable drag and drop source. + ImGuiColorEditFlags_NoBorder = 1 << 10, // // ColorButton: disable border (which is enforced by default) + + // User Options (right-click on widget to change some of them). + ImGuiColorEditFlags_AlphaBar = 1 << 16, // // ColorEdit, ColorPicker: show vertical alpha bar/gradient in picker. + ImGuiColorEditFlags_AlphaPreview = 1 << 17, // // ColorEdit, ColorPicker, ColorButton: display preview as a transparent color over a checkerboard, instead of opaque. + ImGuiColorEditFlags_AlphaPreviewHalf= 1 << 18, // // ColorEdit, ColorPicker, ColorButton: display half opaque / half checkerboard, instead of opaque. + ImGuiColorEditFlags_HDR = 1 << 19, // // (WIP) ColorEdit: Currently only disable 0.0f..1.0f limits in RGBA edition (note: you probably want to use ImGuiColorEditFlags_Float flag as well). + ImGuiColorEditFlags_DisplayRGB = 1 << 20, // [Display] // ColorEdit: override _display_ type among RGB/HSV/Hex. ColorPicker: select any combination using one or more of RGB/HSV/Hex. + ImGuiColorEditFlags_DisplayHSV = 1 << 21, // [Display] // " + ImGuiColorEditFlags_DisplayHex = 1 << 22, // [Display] // " + ImGuiColorEditFlags_Uint8 = 1 << 23, // [DataType] // ColorEdit, ColorPicker, ColorButton: _display_ values formatted as 0..255. + ImGuiColorEditFlags_Float = 1 << 24, // [DataType] // ColorEdit, ColorPicker, ColorButton: _display_ values formatted as 0.0f..1.0f floats instead of 0..255 integers. No round-trip of value via integers. + ImGuiColorEditFlags_PickerHueBar = 1 << 25, // [Picker] // ColorPicker: bar for Hue, rectangle for Sat/Value. + ImGuiColorEditFlags_PickerHueWheel = 1 << 26, // [Picker] // ColorPicker: wheel for Hue, triangle for Sat/Value. + ImGuiColorEditFlags_InputRGB = 1 << 27, // [Input] // ColorEdit, ColorPicker: input and output data in RGB format. + ImGuiColorEditFlags_InputHSV = 1 << 28, // [Input] // ColorEdit, ColorPicker: input and output data in HSV format. + + // Defaults Options. You can set application defaults using SetColorEditOptions(). The intent is that you probably don't want to + // override them in most of your calls. Let the user choose via the option menu and/or call SetColorEditOptions() once during startup. + ImGuiColorEditFlags_DefaultOptions_ = ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_PickerHueBar, + + // [Internal] Masks + ImGuiColorEditFlags_DisplayMask_ = ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_DisplayHex, + ImGuiColorEditFlags_DataTypeMask_ = ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_Float, + ImGuiColorEditFlags_PickerMask_ = ImGuiColorEditFlags_PickerHueWheel | ImGuiColorEditFlags_PickerHueBar, + ImGuiColorEditFlags_InputMask_ = ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_InputHSV + + // Obsolete names (will be removed) + // ImGuiColorEditFlags_RGB = ImGuiColorEditFlags_DisplayRGB, ImGuiColorEditFlags_HSV = ImGuiColorEditFlags_DisplayHSV, ImGuiColorEditFlags_HEX = ImGuiColorEditFlags_DisplayHex // [renamed in 1.69] +}; + +// Flags for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc. +// We use the same sets of flags for DragXXX() and SliderXXX() functions as the features are the same and it makes it easier to swap them. +enum ImGuiSliderFlags_ +{ + ImGuiSliderFlags_None = 0, + ImGuiSliderFlags_AlwaysClamp = 1 << 4, // Clamp value to min/max bounds when input manually with CTRL+Click. By default CTRL+Click allows going out of bounds. + ImGuiSliderFlags_Logarithmic = 1 << 5, // Make the widget logarithmic (linear otherwise). Consider using ImGuiSliderFlags_NoRoundToFormat with this if using a format-string with small amount of digits. + ImGuiSliderFlags_NoRoundToFormat = 1 << 6, // Disable rounding underlying value to match precision of the display format string (e.g. %.3f values are rounded to those 3 digits) + ImGuiSliderFlags_NoInput = 1 << 7, // Disable CTRL+Click or Enter key allowing to input text directly into the widget + ImGuiSliderFlags_InvalidMask_ = 0x7000000F // [Internal] We treat using those bits as being potentially a 'float power' argument from the previous API that has got miscast to this enum, and will trigger an assert if needed. + + // Obsolete names (will be removed) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + , ImGuiSliderFlags_ClampOnInput = ImGuiSliderFlags_AlwaysClamp // [renamed in 1.79] +#endif +}; + +// Identify a mouse button. +// Those values are guaranteed to be stable and we frequently use 0/1 directly. Named enums provided for convenience. +enum ImGuiMouseButton_ +{ + ImGuiMouseButton_Left = 0, + ImGuiMouseButton_Right = 1, + ImGuiMouseButton_Middle = 2, + ImGuiMouseButton_COUNT = 5 +}; + +// Enumeration for GetMouseCursor() +// User code may request backend to display given cursor by calling SetMouseCursor(), which is why we have some cursors that are marked unused here +enum ImGuiMouseCursor_ +{ + ImGuiMouseCursor_None = -1, + ImGuiMouseCursor_Arrow = 0, + ImGuiMouseCursor_TextInput, // When hovering over InputText, etc. + ImGuiMouseCursor_ResizeAll, // (Unused by Dear ImGui functions) + ImGuiMouseCursor_ResizeNS, // When hovering over an horizontal border + ImGuiMouseCursor_ResizeEW, // When hovering over a vertical border or a column + ImGuiMouseCursor_ResizeNESW, // When hovering over the bottom-left corner of a window + ImGuiMouseCursor_ResizeNWSE, // When hovering over the bottom-right corner of a window + ImGuiMouseCursor_Hand, // (Unused by Dear ImGui functions. Use for e.g. hyperlinks) + ImGuiMouseCursor_NotAllowed, // When hovering something with disallowed interaction. Usually a crossed circle. + ImGuiMouseCursor_COUNT +}; + +// Enumeration for ImGui::SetWindow***(), SetNextWindow***(), SetNextItem***() functions +// Represent a condition. +// Important: Treat as a regular enum! Do NOT combine multiple values using binary operators! All the functions above treat 0 as a shortcut to ImGuiCond_Always. +enum ImGuiCond_ +{ + ImGuiCond_None = 0, // No condition (always set the variable), same as _Always + ImGuiCond_Always = 1 << 0, // No condition (always set the variable) + ImGuiCond_Once = 1 << 1, // Set the variable once per runtime session (only the first call will succeed) + ImGuiCond_FirstUseEver = 1 << 2, // Set the variable if the object/window has no persistently saved data (no entry in .ini file) + ImGuiCond_Appearing = 1 << 3 // Set the variable if the object/window is appearing after being hidden/inactive (or the first time) +}; + +//----------------------------------------------------------------------------- +// [SECTION] Helpers: Memory allocations macros, ImVector<> +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// IM_MALLOC(), IM_FREE(), IM_NEW(), IM_PLACEMENT_NEW(), IM_DELETE() +// We call C++ constructor on own allocated memory via the placement "new(ptr) Type()" syntax. +// Defining a custom placement new() with a custom parameter allows us to bypass including which on some platforms complains when user has disabled exceptions. +//----------------------------------------------------------------------------- + +struct ImNewWrapper {}; +inline void* operator new(size_t, ImNewWrapper, void* ptr) { return ptr; } +inline void operator delete(void*, ImNewWrapper, void*) {} // This is only required so we can use the symmetrical new() +#define IM_ALLOC(_SIZE) ImGui::MemAlloc(_SIZE) +#define IM_FREE(_PTR) ImGui::MemFree(_PTR) +#define IM_PLACEMENT_NEW(_PTR) new(ImNewWrapper(), _PTR) +#define IM_NEW(_TYPE) new(ImNewWrapper(), ImGui::MemAlloc(sizeof(_TYPE))) _TYPE +template void IM_DELETE(T* p) { if (p) { p->~T(); ImGui::MemFree(p); } } + +//----------------------------------------------------------------------------- +// ImVector<> +// Lightweight std::vector<>-like class to avoid dragging dependencies (also, some implementations of STL with debug enabled are absurdly slow, we bypass it so our code runs fast in debug). +//----------------------------------------------------------------------------- +// - You generally do NOT need to care or use this ever. But we need to make it available in imgui.h because some of our public structures are relying on it. +// - We use std-like naming convention here, which is a little unusual for this codebase. +// - Important: clear() frees memory, resize(0) keep the allocated buffer. We use resize(0) a lot to intentionally recycle allocated buffers across frames and amortize our costs. +// - Important: our implementation does NOT call C++ constructors/destructors, we treat everything as raw data! This is intentional but be extra mindful of that, +// Do NOT use this class as a std::vector replacement in your own code! Many of the structures used by dear imgui can be safely initialized by a zero-memset. +//----------------------------------------------------------------------------- + +IM_MSVC_RUNTIME_CHECKS_OFF +template +struct ImVector +{ + int Size; + int Capacity; + T* Data; + + // Provide standard typedefs but we don't use them ourselves. + typedef T value_type; + typedef value_type* iterator; + typedef const value_type* const_iterator; + + // Constructors, destructor + inline ImVector() { Size = Capacity = 0; Data = NULL; } + inline ImVector(const ImVector& src) { Size = Capacity = 0; Data = NULL; operator=(src); } + inline ImVector& operator=(const ImVector& src) { clear(); resize(src.Size); memcpy(Data, src.Data, (size_t)Size * sizeof(T)); return *this; } + inline ~ImVector() { if (Data) IM_FREE(Data); } // Important: does not destruct anything + + inline void clear() { if (Data) { Size = Capacity = 0; IM_FREE(Data); Data = NULL; } } // Important: does not destruct anything + inline void clear_delete() { for (int n = 0; n < Size; n++) IM_DELETE(Data[n]); clear(); } // Important: never called automatically! always explicit. + inline void clear_destruct() { for (int n = 0; n < Size; n++) Data[n].~T(); clear(); } // Important: never called automatically! always explicit. + + inline bool empty() const { return Size == 0; } + inline int size() const { return Size; } + inline int size_in_bytes() const { return Size * (int)sizeof(T); } + inline int max_size() const { return 0x7FFFFFFF / (int)sizeof(T); } + inline int capacity() const { return Capacity; } + inline T& operator[](int i) { IM_ASSERT(i >= 0 && i < Size); return Data[i]; } + inline const T& operator[](int i) const { IM_ASSERT(i >= 0 && i < Size); return Data[i]; } + + inline T* begin() { return Data; } + inline const T* begin() const { return Data; } + inline T* end() { return Data + Size; } + inline const T* end() const { return Data + Size; } + inline T& front() { IM_ASSERT(Size > 0); return Data[0]; } + inline const T& front() const { IM_ASSERT(Size > 0); return Data[0]; } + inline T& back() { IM_ASSERT(Size > 0); return Data[Size - 1]; } + inline const T& back() const { IM_ASSERT(Size > 0); return Data[Size - 1]; } + inline void swap(ImVector& rhs) { int rhs_size = rhs.Size; rhs.Size = Size; Size = rhs_size; int rhs_cap = rhs.Capacity; rhs.Capacity = Capacity; Capacity = rhs_cap; T* rhs_data = rhs.Data; rhs.Data = Data; Data = rhs_data; } + + inline int _grow_capacity(int sz) const { int new_capacity = Capacity ? (Capacity + Capacity / 2) : 8; return new_capacity > sz ? new_capacity : sz; } + inline void resize(int new_size) { if (new_size > Capacity) reserve(_grow_capacity(new_size)); Size = new_size; } + inline void resize(int new_size, const T& v) { if (new_size > Capacity) reserve(_grow_capacity(new_size)); if (new_size > Size) for (int n = Size; n < new_size; n++) memcpy(&Data[n], &v, sizeof(v)); Size = new_size; } + inline void shrink(int new_size) { IM_ASSERT(new_size <= Size); Size = new_size; } // Resize a vector to a smaller size, guaranteed not to cause a reallocation + inline void reserve(int new_capacity) { if (new_capacity <= Capacity) return; T* new_data = (T*)IM_ALLOC((size_t)new_capacity * sizeof(T)); if (Data) { memcpy(new_data, Data, (size_t)Size * sizeof(T)); IM_FREE(Data); } Data = new_data; Capacity = new_capacity; } + + // NB: It is illegal to call push_back/push_front/insert with a reference pointing inside the ImVector data itself! e.g. v.push_back(v[10]) is forbidden. + inline void push_back(const T& v) { if (Size == Capacity) reserve(_grow_capacity(Size + 1)); memcpy(&Data[Size], &v, sizeof(v)); Size++; } + inline void pop_back() { IM_ASSERT(Size > 0); Size--; } + inline void push_front(const T& v) { if (Size == 0) push_back(v); else insert(Data, v); } + inline T* erase(const T* it) { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + 1, ((size_t)Size - (size_t)off - 1) * sizeof(T)); Size--; return Data + off; } + inline T* erase(const T* it, const T* it_last){ IM_ASSERT(it >= Data && it < Data + Size && it_last > it && it_last <= Data + Size); const ptrdiff_t count = it_last - it; const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + count, ((size_t)Size - (size_t)off - (size_t)count) * sizeof(T)); Size -= (int)count; return Data + off; } + inline T* erase_unsorted(const T* it) { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; if (it < Data + Size - 1) memcpy(Data + off, Data + Size - 1, sizeof(T)); Size--; return Data + off; } + inline T* insert(const T* it, const T& v) { IM_ASSERT(it >= Data && it <= Data + Size); const ptrdiff_t off = it - Data; if (Size == Capacity) reserve(_grow_capacity(Size + 1)); if (off < (int)Size) memmove(Data + off + 1, Data + off, ((size_t)Size - (size_t)off) * sizeof(T)); memcpy(&Data[off], &v, sizeof(v)); Size++; return Data + off; } + inline bool contains(const T& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data++ == v) return true; return false; } + inline T* find(const T& v) { T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data == v) break; else ++data; return data; } + inline const T* find(const T& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data == v) break; else ++data; return data; } + inline bool find_erase(const T& v) { const T* it = find(v); if (it < Data + Size) { erase(it); return true; } return false; } + inline bool find_erase_unsorted(const T& v) { const T* it = find(v); if (it < Data + Size) { erase_unsorted(it); return true; } return false; } + inline int index_from_ptr(const T* it) const { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; return (int)off; } +}; +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiStyle +//----------------------------------------------------------------------------- +// You may modify the ImGui::GetStyle() main instance during initialization and before NewFrame(). +// During the frame, use ImGui::PushStyleVar(ImGuiStyleVar_XXXX)/PopStyleVar() to alter the main style values, +// and ImGui::PushStyleColor(ImGuiCol_XXX)/PopStyleColor() for colors. +//----------------------------------------------------------------------------- + +struct ImGuiStyle +{ + float Alpha; // Global alpha applies to everything in Dear ImGui. + float DisabledAlpha; // Additional alpha multiplier applied by BeginDisabled(). Multiply over current value of Alpha. + ImVec2 WindowPadding; // Padding within a window. + float WindowRounding; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. Large values tend to lead to variety of artifacts and are not recommended. + float WindowBorderSize; // Thickness of border around windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 WindowMinSize; // Minimum window size. This is a global setting. If you want to constraint individual windows, use SetNextWindowSizeConstraints(). + ImVec2 WindowTitleAlign; // Alignment for title bar text. Defaults to (0.0f,0.5f) for left-aligned,vertically centered. + ImGuiDir WindowMenuButtonPosition; // Side of the collapsing/docking button in the title bar (None/Left/Right). Defaults to ImGuiDir_Left. + float ChildRounding; // Radius of child window corners rounding. Set to 0.0f to have rectangular windows. + float ChildBorderSize; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + float PopupRounding; // Radius of popup window corners rounding. (Note that tooltip windows use WindowRounding) + float PopupBorderSize; // Thickness of border around popup/tooltip windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 FramePadding; // Padding within a framed rectangle (used by most widgets). + float FrameRounding; // Radius of frame corners rounding. Set to 0.0f to have rectangular frame (used by most widgets). + float FrameBorderSize; // Thickness of border around frames. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 ItemSpacing; // Horizontal and vertical spacing between widgets/lines. + ImVec2 ItemInnerSpacing; // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label). + ImVec2 CellPadding; // Padding within a table cell + ImVec2 TouchExtraPadding; // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! + float IndentSpacing; // Horizontal indentation when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). + float ColumnsMinSpacing; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). + float ScrollbarSize; // Width of the vertical scrollbar, Height of the horizontal scrollbar. + float ScrollbarRounding; // Radius of grab corners for scrollbar. + float GrabMinSize; // Minimum width/height of a grab box for slider/scrollbar. + float GrabRounding; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. + float LogSliderDeadzone; // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero. + float TabRounding; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. + float TabBorderSize; // Thickness of border around tabs. + float TabMinWidthForCloseButton; // Minimum width for close button to appears on an unselected tab when hovered. Set to 0.0f to always show when hovering, set to FLT_MAX to never show close button unless selected. + ImGuiDir ColorButtonPosition; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. + ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered). + ImVec2 SelectableTextAlign; // Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. + ImVec2 DisplayWindowPadding; // Window position are clamped to be visible within the display area or monitors by at least this amount. Only applies to regular windows. + ImVec2 DisplaySafeAreaPadding; // If you cannot see the edges of your screen (e.g. on a TV) increase the safe area padding. Apply to popups/tooltips as well regular windows. NB: Prefer configuring your TV sets correctly! + float MouseCursorScale; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. + bool AntiAliasedLines; // Enable anti-aliased lines/borders. Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList). + bool AntiAliasedLinesUseTex; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering. Latched at the beginning of the frame (copied to ImDrawList). + bool AntiAliasedFill; // Enable anti-aliased edges around filled shapes (rounded rectangles, circles, etc.). Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList). + float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + float CircleTessellationMaxError; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + ImVec4 Colors[ImGuiCol_COUNT]; + + IMGUI_API ImGuiStyle(); + IMGUI_API void ScaleAllSizes(float scale_factor); +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiIO +//----------------------------------------------------------------------------- +// Communicate most settings and inputs/outputs to Dear ImGui using this structure. +// Access via ImGui::GetIO(). Read 'Programmer guide' section in .cpp file for general usage. +//----------------------------------------------------------------------------- + +// [Internal] Storage used by IsKeyDown(), IsKeyPressed() etc functions. +// If prior to 1.87 you used io.KeysDownDuration[] (which was marked as internal), you should use GetKeyData(key)->DownDuration and not io.KeysData[key]->DownDuration. +struct ImGuiKeyData +{ + bool Down; // True for if key is down + float DownDuration; // Duration the key has been down (<0.0f: not pressed, 0.0f: just pressed, >0.0f: time held) + float DownDurationPrev; // Last frame duration the key has been down + float AnalogValue; // 0.0f..1.0f for gamepad values +}; + +struct ImGuiIO +{ + //------------------------------------------------------------------ + // Configuration (fill once) // Default value + //------------------------------------------------------------------ + + ImGuiConfigFlags ConfigFlags; // = 0 // See ImGuiConfigFlags_ enum. Set by user/application. Gamepad/keyboard navigation options, etc. + ImGuiBackendFlags BackendFlags; // = 0 // See ImGuiBackendFlags_ enum. Set by backend (imgui_impl_xxx files or custom backend) to communicate features supported by the backend. + ImVec2 DisplaySize; // // Main display size, in pixels (generally == GetMainViewport()->Size) + float DeltaTime; // = 1.0f/60.0f // Time elapsed since last frame, in seconds. + float IniSavingRate; // = 5.0f // Minimum time between saving positions/sizes to .ini file, in seconds. + const char* IniFilename; // = "imgui.ini" // Path to .ini file (important: default "imgui.ini" is relative to current working dir!). Set NULL to disable automatic .ini loading/saving or if you want to manually call LoadIniSettingsXXX() / SaveIniSettingsXXX() functions. + const char* LogFilename; // = "imgui_log.txt"// Path to .log file (default parameter to ImGui::LogToFile when no file is specified). + float MouseDoubleClickTime; // = 0.30f // Time for a double-click, in seconds. + float MouseDoubleClickMaxDist; // = 6.0f // Distance threshold to stay in to validate a double-click, in pixels. + float MouseDragThreshold; // = 6.0f // Distance threshold before considering we are dragging. + float KeyRepeatDelay; // = 0.250f // When holding a key/button, time before it starts repeating, in seconds (for buttons in Repeat mode, etc.). + float KeyRepeatRate; // = 0.050f // When holding a key/button, rate at which it repeats, in seconds. + void* UserData; // = NULL // Store your own data for retrieval by callbacks. + + ImFontAtlas*Fonts; // // Font atlas: load, rasterize and pack one or more fonts into a single texture. + float FontGlobalScale; // = 1.0f // Global scale all fonts + bool FontAllowUserScaling; // = false // Allow user scaling text of individual window with CTRL+Wheel. + ImFont* FontDefault; // = NULL // Font to use on NewFrame(). Use NULL to uses Fonts->Fonts[0]. + ImVec2 DisplayFramebufferScale; // = (1, 1) // For retina display or other situations where window coordinates are different from framebuffer coordinates. This generally ends up in ImDrawData::FramebufferScale. + + // Miscellaneous options + bool MouseDrawCursor; // = false // Request ImGui to draw a mouse cursor for you (if you are on a platform without a mouse cursor). Cannot be easily renamed to 'io.ConfigXXX' because this is frequently used by backend implementations. + bool ConfigMacOSXBehaviors; // = defined(__APPLE__) // OS X style: Text editing cursor movement using Alt instead of Ctrl, Shortcuts using Cmd/Super instead of Ctrl, Line/Text Start and End using Cmd+Arrows instead of Home/End, Double click selects by word instead of selecting whole text, Multi-selection in lists uses Cmd/Super instead of Ctrl. + bool ConfigInputTrickleEventQueue; // = true // Enable input queue trickling: some types of events submitted during the same frame (e.g. button down + up) will be spread over multiple frames, improving interactions with low framerates. + bool ConfigInputTextCursorBlink; // = true // Enable blinking cursor (optional as some users consider it to be distracting). + bool ConfigDragClickToInputText; // = false // [BETA] Enable turning DragXXX widgets into text input with a simple mouse click-release (without moving). Not desirable on devices without a keyboard. + bool ConfigWindowsResizeFromEdges; // = true // Enable resizing of windows from their edges and from the lower-left corner. This requires (io.BackendFlags & ImGuiBackendFlags_HasMouseCursors) because it needs mouse cursor feedback. (This used to be a per-window ImGuiWindowFlags_ResizeFromAnySide flag) + bool ConfigWindowsMoveFromTitleBarOnly; // = false // Enable allowing to move windows only when clicking on their title bar. Does not apply to windows without a title bar. + float ConfigMemoryCompactTimer; // = 60.0f // Timer (in seconds) to free transient windows/tables memory buffers when unused. Set to -1.0f to disable. + + //------------------------------------------------------------------ + // Platform Functions + // (the imgui_impl_xxxx backend files are setting those up for you) + //------------------------------------------------------------------ + + // Optional: Platform/Renderer backend name (informational only! will be displayed in About Window) + User data for backend/wrappers to store their own stuff. + const char* BackendPlatformName; // = NULL + const char* BackendRendererName; // = NULL + void* BackendPlatformUserData; // = NULL // User data for platform backend + void* BackendRendererUserData; // = NULL // User data for renderer backend + void* BackendLanguageUserData; // = NULL // User data for non C++ programming language backend + + // Optional: Access OS clipboard + // (default to use native Win32 clipboard on Windows, otherwise uses a private clipboard. Override to access OS clipboard on other architectures) + const char* (*GetClipboardTextFn)(void* user_data); + void (*SetClipboardTextFn)(void* user_data, const char* text); + void* ClipboardUserData; + + // Optional: Notify OS Input Method Editor of the screen position of your cursor for text input position (e.g. when using Japanese/Chinese IME on Windows) + // (default to use native imm32 api on Windows) + void (*SetPlatformImeDataFn)(ImGuiViewport* viewport, ImGuiPlatformImeData* data); +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + void* ImeWindowHandle; // = NULL // [Obsolete] Set ImGuiViewport::PlatformHandleRaw instead. Set this to your HWND to get automatic IME cursor positioning. +#else + void* _UnusedPadding; // Unused field to keep data structure the same size. +#endif + + //------------------------------------------------------------------ + // Input - Call before calling NewFrame() + //------------------------------------------------------------------ + + // Input Functions + IMGUI_API void AddKeyEvent(ImGuiKey key, bool down); // Queue a new key down/up event. Key should be "translated" (as in, generally ImGuiKey_A matches the key end-user would use to emit an 'A' character) + IMGUI_API void AddKeyAnalogEvent(ImGuiKey key, bool down, float v); // Queue a new key down/up event for analog values (e.g. ImGuiKey_Gamepad_ values). Dead-zones should be handled by the backend. + IMGUI_API void AddMousePosEvent(float x, float y); // Queue a mouse position update. Use -FLT_MAX,-FLT_MAX to signify no mouse (e.g. app not focused and not hovered) + IMGUI_API void AddMouseButtonEvent(int button, bool down); // Queue a mouse button change + IMGUI_API void AddMouseWheelEvent(float wh_x, float wh_y); // Queue a mouse wheel update + IMGUI_API void AddFocusEvent(bool focused); // Queue a gain/loss of focus for the application (generally based on OS/platform focus of your window) + IMGUI_API void AddInputCharacter(unsigned int c); // Queue a new character input + IMGUI_API void AddInputCharacterUTF16(ImWchar16 c); // Queue a new character input from an UTF-16 character, it can be a surrogate + IMGUI_API void AddInputCharactersUTF8(const char* str); // Queue a new characters input from an UTF-8 string + + IMGUI_API void ClearInputCharacters(); // [Internal] Clear the text input buffer manually + IMGUI_API void ClearInputKeys(); // [Internal] Release all keys + IMGUI_API void SetKeyEventNativeData(ImGuiKey key, int native_keycode, int native_scancode, int native_legacy_index = -1); // [Optional] Specify index for legacy <1.87 IsKeyXXX() functions with native indices + specify native keycode, scancode. + + //------------------------------------------------------------------ + // Output - Updated by NewFrame() or EndFrame()/Render() + // (when reading from the io.WantCaptureMouse, io.WantCaptureKeyboard flags to dispatch your inputs, it is + // generally easier and more correct to use their state BEFORE calling NewFrame(). See FAQ for details!) + //------------------------------------------------------------------ + + bool WantCaptureMouse; // Set when Dear ImGui will use mouse inputs, in this case do not dispatch them to your main game/application (either way, always pass on mouse inputs to imgui). (e.g. unclicked mouse is hovering over an imgui window, widget is active, mouse was clicked over an imgui window, etc.). + bool WantCaptureKeyboard; // Set when Dear ImGui will use keyboard inputs, in this case do not dispatch them to your main game/application (either way, always pass keyboard inputs to imgui). (e.g. InputText active, or an imgui window is focused and navigation is enabled, etc.). + bool WantTextInput; // Mobile/console: when set, you may display an on-screen keyboard. This is set by Dear ImGui when it wants textual keyboard input to happen (e.g. when a InputText widget is active). + bool WantSetMousePos; // MousePos has been altered, backend should reposition mouse on next frame. Rarely used! Set only when ImGuiConfigFlags_NavEnableSetMousePos flag is enabled. + bool WantSaveIniSettings; // When manual .ini load/save is active (io.IniFilename == NULL), this will be set to notify your application that you can call SaveIniSettingsToMemory() and save yourself. Important: clear io.WantSaveIniSettings yourself after saving! + bool NavActive; // Keyboard/Gamepad navigation is currently allowed (will handle ImGuiKey_NavXXX events) = a window is focused and it doesn't use the ImGuiWindowFlags_NoNavInputs flag. + bool NavVisible; // Keyboard/Gamepad navigation is visible and allowed (will handle ImGuiKey_NavXXX events). + float Framerate; // Rough estimate of application framerate, in frame per second. Solely for convenience. Rolling average estimation based on io.DeltaTime over 120 frames. + int MetricsRenderVertices; // Vertices output during last call to Render() + int MetricsRenderIndices; // Indices output during last call to Render() = number of triangles * 3 + int MetricsRenderWindows; // Number of visible windows + int MetricsActiveWindows; // Number of active windows + int MetricsActiveAllocations; // Number of active allocations, updated by MemAlloc/MemFree based on current context. May be off if you have multiple imgui contexts. + ImVec2 MouseDelta; // Mouse delta. Note that this is zero if either current or previous position are invalid (-FLT_MAX,-FLT_MAX), so a disappearing/reappearing mouse won't have a huge delta. + + // Legacy: before 1.87, we required backend to fill io.KeyMap[] (imgui->native map) during initialization and io.KeysDown[] (native indices) every frame. + // This is still temporarily supported as a legacy feature. However the new preferred scheme is for backend to call io.AddKeyEvent(). +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + int KeyMap[ImGuiKey_COUNT]; // [LEGACY] Input: map of indices into the KeysDown[512] entries array which represent your "native" keyboard state. The first 512 are now unused and should be kept zero. Legacy backend will write into KeyMap[] using ImGuiKey_ indices which are always >512. + bool KeysDown[512]; // [LEGACY] Input: Keyboard keys that are pressed (ideally left in the "native" order your engine has access to keyboard keys, so you can use your own defines/enums for keys). +#endif + + //------------------------------------------------------------------ + // [Internal] Dear ImGui will maintain those fields. Forward compatibility not guaranteed! + //------------------------------------------------------------------ + + // Main Input State + // (this block used to be written by backend, since 1.87 it is best to NOT write to those directly, call the AddXXX functions above instead) + // (reading from those variables is fair game, as they are extremely unlikely to be moving anywhere) + ImVec2 MousePos; // Mouse position, in pixels. Set to ImVec2(-FLT_MAX, -FLT_MAX) if mouse is unavailable (on another screen, etc.) + bool MouseDown[5]; // Mouse buttons: 0=left, 1=right, 2=middle + extras (ImGuiMouseButton_COUNT == 5). Dear ImGui mostly uses left and right buttons. Others buttons allows us to track if the mouse is being used by your application + available to user as a convenience via IsMouse** API. + float MouseWheel; // Mouse wheel Vertical: 1 unit scrolls about 5 lines text. + float MouseWheelH; // Mouse wheel Horizontal. Most users don't have a mouse with an horizontal wheel, may not be filled by all backends. + bool KeyCtrl; // Keyboard modifier down: Control + bool KeyShift; // Keyboard modifier down: Shift + bool KeyAlt; // Keyboard modifier down: Alt + bool KeySuper; // Keyboard modifier down: Cmd/Super/Windows + float NavInputs[ImGuiNavInput_COUNT]; // Gamepad inputs. Cleared back to zero by EndFrame(). Keyboard keys will be auto-mapped and be written here by NewFrame(). + + // Other state maintained from data above + IO function calls + ImGuiKeyModFlags KeyMods; // Key mods flags (same as io.KeyCtrl/KeyShift/KeyAlt/KeySuper but merged into flags), updated by NewFrame() + ImGuiKeyModFlags KeyModsPrev; // Key mods flags (from previous frame) + ImGuiKeyData KeysData[ImGuiKey_KeysData_SIZE]; // Key state for all known keys. Use IsKeyXXX() functions to access this. + bool WantCaptureMouseUnlessPopupClose; // Alternative to WantCaptureMouse: (WantCaptureMouse == true && WantCaptureMouseUnlessPopupClose == false) when a click over void is expected to close a popup. + ImVec2 MousePosPrev; // Previous mouse position (note that MouseDelta is not necessary == MousePos-MousePosPrev, in case either position is invalid) + ImVec2 MouseClickedPos[5]; // Position at time of clicking + double MouseClickedTime[5]; // Time of last click (used to figure out double-click) + bool MouseClicked[5]; // Mouse button went from !Down to Down (same as MouseClickedCount[x] != 0) + bool MouseDoubleClicked[5]; // Has mouse button been double-clicked? (same as MouseClickedCount[x] == 2) + ImU16 MouseClickedCount[5]; // == 0 (not clicked), == 1 (same as MouseClicked[]), == 2 (double-clicked), == 3 (triple-clicked) etc. when going from !Down to Down + ImU16 MouseClickedLastCount[5]; // Count successive number of clicks. Stays valid after mouse release. Reset after another click is done. + bool MouseReleased[5]; // Mouse button went from Down to !Down + bool MouseDownOwned[5]; // Track if button was clicked inside a dear imgui window or over void blocked by a popup. We don't request mouse capture from the application if click started outside ImGui bounds. + bool MouseDownOwnedUnlessPopupClose[5]; //Track if button was clicked inside a dear imgui window. + float MouseDownDuration[5]; // Duration the mouse button has been down (0.0f == just clicked) + float MouseDownDurationPrev[5]; // Previous time the mouse button has been down + float MouseDragMaxDistanceSqr[5]; // Squared maximum distance of how much mouse has traveled from the clicking point (used for moving thresholds) + float NavInputsDownDuration[ImGuiNavInput_COUNT]; + float NavInputsDownDurationPrev[ImGuiNavInput_COUNT]; + float PenPressure; // Touch/Pen pressure (0.0f to 1.0f, should be >0.0f only when MouseDown[0] == true). Helper storage currently unused by Dear ImGui. + bool AppFocusLost; + ImS8 BackendUsingLegacyKeyArrays; // -1: unknown, 0: using AddKeyEvent(), 1: using legacy io.KeysDown[] + bool BackendUsingLegacyNavInputArray; // 0: using AddKeyAnalogEvent(), 1: writing to legacy io.NavInputs[] directly + ImWchar16 InputQueueSurrogate; // For AddInputCharacterUTF16() + ImVector InputQueueCharacters; // Queue of _characters_ input (obtained by platform backend). Fill using AddInputCharacter() helper. + + IMGUI_API ImGuiIO(); +}; + +//----------------------------------------------------------------------------- +// [SECTION] Misc data structures +//----------------------------------------------------------------------------- + +// Shared state of InputText(), passed as an argument to your callback when a ImGuiInputTextFlags_Callback* flag is used. +// The callback function should return 0 by default. +// Callbacks (follow a flag name and see comments in ImGuiInputTextFlags_ declarations for more details) +// - ImGuiInputTextFlags_CallbackEdit: Callback on buffer edit (note that InputText() already returns true on edit, the callback is useful mainly to manipulate the underlying buffer while focus is active) +// - ImGuiInputTextFlags_CallbackAlways: Callback on each iteration +// - ImGuiInputTextFlags_CallbackCompletion: Callback on pressing TAB +// - ImGuiInputTextFlags_CallbackHistory: Callback on pressing Up/Down arrows +// - ImGuiInputTextFlags_CallbackCharFilter: Callback on character inputs to replace or discard them. Modify 'EventChar' to replace or discard, or return 1 in callback to discard. +// - ImGuiInputTextFlags_CallbackResize: Callback on buffer capacity changes request (beyond 'buf_size' parameter value), allowing the string to grow. +struct ImGuiInputTextCallbackData +{ + ImGuiInputTextFlags EventFlag; // One ImGuiInputTextFlags_Callback* // Read-only + ImGuiInputTextFlags Flags; // What user passed to InputText() // Read-only + void* UserData; // What user passed to InputText() // Read-only + + // Arguments for the different callback events + // - To modify the text buffer in a callback, prefer using the InsertChars() / DeleteChars() function. InsertChars() will take care of calling the resize callback if necessary. + // - If you know your edits are not going to resize the underlying buffer allocation, you may modify the contents of 'Buf[]' directly. You need to update 'BufTextLen' accordingly (0 <= BufTextLen < BufSize) and set 'BufDirty'' to true so InputText can update its internal state. + ImWchar EventChar; // Character input // Read-write // [CharFilter] Replace character with another one, or set to zero to drop. return 1 is equivalent to setting EventChar=0; + ImGuiKey EventKey; // Key pressed (Up/Down/TAB) // Read-only // [Completion,History] + char* Buf; // Text buffer // Read-write // [Resize] Can replace pointer / [Completion,History,Always] Only write to pointed data, don't replace the actual pointer! + int BufTextLen; // Text length (in bytes) // Read-write // [Resize,Completion,History,Always] Exclude zero-terminator storage. In C land: == strlen(some_text), in C++ land: string.length() + int BufSize; // Buffer size (in bytes) = capacity+1 // Read-only // [Resize,Completion,History,Always] Include zero-terminator storage. In C land == ARRAYSIZE(my_char_array), in C++ land: string.capacity()+1 + bool BufDirty; // Set if you modify Buf/BufTextLen! // Write // [Completion,History,Always] + int CursorPos; // // Read-write // [Completion,History,Always] + int SelectionStart; // // Read-write // [Completion,History,Always] == to SelectionEnd when no selection) + int SelectionEnd; // // Read-write // [Completion,History,Always] + + // Helper functions for text manipulation. + // Use those function to benefit from the CallbackResize behaviors. Calling those function reset the selection. + IMGUI_API ImGuiInputTextCallbackData(); + IMGUI_API void DeleteChars(int pos, int bytes_count); + IMGUI_API void InsertChars(int pos, const char* text, const char* text_end = NULL); + void SelectAll() { SelectionStart = 0; SelectionEnd = BufTextLen; } + void ClearSelection() { SelectionStart = SelectionEnd = BufTextLen; } + bool HasSelection() const { return SelectionStart != SelectionEnd; } +}; + +// Resizing callback data to apply custom constraint. As enabled by SetNextWindowSizeConstraints(). Callback is called during the next Begin(). +// NB: For basic min/max size constraint on each axis you don't need to use the callback! The SetNextWindowSizeConstraints() parameters are enough. +struct ImGuiSizeCallbackData +{ + void* UserData; // Read-only. What user passed to SetNextWindowSizeConstraints() + ImVec2 Pos; // Read-only. Window position, for reference. + ImVec2 CurrentSize; // Read-only. Current window size. + ImVec2 DesiredSize; // Read-write. Desired size, based on user's mouse position. Write to this field to restrain resizing. +}; + +// Data payload for Drag and Drop operations: AcceptDragDropPayload(), GetDragDropPayload() +struct ImGuiPayload +{ + // Members + void* Data; // Data (copied and owned by dear imgui) + int DataSize; // Data size + + // [Internal] + ImGuiID SourceId; // Source item id + ImGuiID SourceParentId; // Source parent id (if available) + int DataFrameCount; // Data timestamp + char DataType[32 + 1]; // Data type tag (short user-supplied string, 32 characters max) + bool Preview; // Set when AcceptDragDropPayload() was called and mouse has been hovering the target item (nb: handle overlapping drag targets) + bool Delivery; // Set when AcceptDragDropPayload() was called and mouse button is released over the target item. + + ImGuiPayload() { Clear(); } + void Clear() { SourceId = SourceParentId = 0; Data = NULL; DataSize = 0; memset(DataType, 0, sizeof(DataType)); DataFrameCount = -1; Preview = Delivery = false; } + bool IsDataType(const char* type) const { return DataFrameCount != -1 && strcmp(type, DataType) == 0; } + bool IsPreview() const { return Preview; } + bool IsDelivery() const { return Delivery; } +}; + +// Sorting specification for one column of a table (sizeof == 12 bytes) +struct ImGuiTableColumnSortSpecs +{ + ImGuiID ColumnUserID; // User id of the column (if specified by a TableSetupColumn() call) + ImS16 ColumnIndex; // Index of the column + ImS16 SortOrder; // Index within parent ImGuiTableSortSpecs (always stored in order starting from 0, tables sorted on a single criteria will always have a 0 here) + ImGuiSortDirection SortDirection : 8; // ImGuiSortDirection_Ascending or ImGuiSortDirection_Descending (you can use this or SortSign, whichever is more convenient for your sort function) + + ImGuiTableColumnSortSpecs() { memset(this, 0, sizeof(*this)); } +}; + +// Sorting specifications for a table (often handling sort specs for a single column, occasionally more) +// Obtained by calling TableGetSortSpecs(). +// When 'SpecsDirty == true' you can sort your data. It will be true with sorting specs have changed since last call, or the first time. +// Make sure to set 'SpecsDirty = false' after sorting, else you may wastefully sort your data every frame! +struct ImGuiTableSortSpecs +{ + const ImGuiTableColumnSortSpecs* Specs; // Pointer to sort spec array. + int SpecsCount; // Sort spec count. Most often 1. May be > 1 when ImGuiTableFlags_SortMulti is enabled. May be == 0 when ImGuiTableFlags_SortTristate is enabled. + bool SpecsDirty; // Set to true when specs have changed since last time! Use this to sort again, then clear the flag. + + ImGuiTableSortSpecs() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, ImColor) +//----------------------------------------------------------------------------- + +// Helper: Unicode defines +#define IM_UNICODE_CODEPOINT_INVALID 0xFFFD // Invalid Unicode code point (standard value). +#ifdef IMGUI_USE_WCHAR32 +#define IM_UNICODE_CODEPOINT_MAX 0x10FFFF // Maximum Unicode code point supported by this build. +#else +#define IM_UNICODE_CODEPOINT_MAX 0xFFFF // Maximum Unicode code point supported by this build. +#endif + +// Helper: Execute a block of code at maximum once a frame. Convenient if you want to quickly create an UI within deep-nested code that runs multiple times every frame. +// Usage: static ImGuiOnceUponAFrame oaf; if (oaf) ImGui::Text("This will be called only once per frame"); +struct ImGuiOnceUponAFrame +{ + ImGuiOnceUponAFrame() { RefFrame = -1; } + mutable int RefFrame; + operator bool() const { int current_frame = ImGui::GetFrameCount(); if (RefFrame == current_frame) return false; RefFrame = current_frame; return true; } +}; + +// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]" +struct ImGuiTextFilter +{ + IMGUI_API ImGuiTextFilter(const char* default_filter = ""); + IMGUI_API bool Draw(const char* label = "Filter (inc,-exc)", float width = 0.0f); // Helper calling InputText+Build + IMGUI_API bool PassFilter(const char* text, const char* text_end = NULL) const; + IMGUI_API void Build(); + void Clear() { InputBuf[0] = 0; Build(); } + bool IsActive() const { return !Filters.empty(); } + + // [Internal] + struct ImGuiTextRange + { + const char* b; + const char* e; + + ImGuiTextRange() { b = e = NULL; } + ImGuiTextRange(const char* _b, const char* _e) { b = _b; e = _e; } + bool empty() const { return b == e; } + IMGUI_API void split(char separator, ImVector* out) const; + }; + char InputBuf[256]; + ImVectorFilters; + int CountGrep; +}; + +// Helper: Growable text buffer for logging/accumulating text +// (this could be called 'ImGuiTextBuilder' / 'ImGuiStringBuilder') +struct ImGuiTextBuffer +{ + ImVector Buf; + IMGUI_API static char EmptyString[1]; + + ImGuiTextBuffer() { } + inline char operator[](int i) const { IM_ASSERT(Buf.Data != NULL); return Buf.Data[i]; } + const char* begin() const { return Buf.Data ? &Buf.front() : EmptyString; } + const char* end() const { return Buf.Data ? &Buf.back() : EmptyString; } // Buf is zero-terminated, so end() will point on the zero-terminator + int size() const { return Buf.Size ? Buf.Size - 1 : 0; } + bool empty() const { return Buf.Size <= 1; } + void clear() { Buf.clear(); } + void reserve(int capacity) { Buf.reserve(capacity); } + const char* c_str() const { return Buf.Data ? Buf.Data : EmptyString; } + IMGUI_API void append(const char* str, const char* str_end = NULL); + IMGUI_API void appendf(const char* fmt, ...) IM_FMTARGS(2); + IMGUI_API void appendfv(const char* fmt, va_list args) IM_FMTLIST(2); +}; + +// Helper: Key->Value storage +// Typically you don't have to worry about this since a storage is held within each Window. +// We use it to e.g. store collapse state for a tree (Int 0/1) +// This is optimized for efficient lookup (dichotomy into a contiguous buffer) and rare insertion (typically tied to user interactions aka max once a frame) +// You can use it as custom user storage for temporary values. Declare your own storage if, for example: +// - You want to manipulate the open/close state of a particular sub-tree in your interface (tree node uses Int 0/1 to store their state). +// - You want to store custom debug data easily without adding or editing structures in your code (probably not efficient, but convenient) +// Types are NOT stored, so it is up to you to make sure your Key don't collide with different types. +struct ImGuiStorage +{ + // [Internal] + struct ImGuiStoragePair + { + ImGuiID key; + union { int val_i; float val_f; void* val_p; }; + ImGuiStoragePair(ImGuiID _key, int _val_i) { key = _key; val_i = _val_i; } + ImGuiStoragePair(ImGuiID _key, float _val_f) { key = _key; val_f = _val_f; } + ImGuiStoragePair(ImGuiID _key, void* _val_p) { key = _key; val_p = _val_p; } + }; + + ImVector Data; + + // - Get***() functions find pair, never add/allocate. Pairs are sorted so a query is O(log N) + // - Set***() functions find pair, insertion on demand if missing. + // - Sorted insertion is costly, paid once. A typical frame shouldn't need to insert any new pair. + void Clear() { Data.clear(); } + IMGUI_API int GetInt(ImGuiID key, int default_val = 0) const; + IMGUI_API void SetInt(ImGuiID key, int val); + IMGUI_API bool GetBool(ImGuiID key, bool default_val = false) const; + IMGUI_API void SetBool(ImGuiID key, bool val); + IMGUI_API float GetFloat(ImGuiID key, float default_val = 0.0f) const; + IMGUI_API void SetFloat(ImGuiID key, float val); + IMGUI_API void* GetVoidPtr(ImGuiID key) const; // default_val is NULL + IMGUI_API void SetVoidPtr(ImGuiID key, void* val); + + // - Get***Ref() functions finds pair, insert on demand if missing, return pointer. Useful if you intend to do Get+Set. + // - References are only valid until a new value is added to the storage. Calling a Set***() function or a Get***Ref() function invalidates the pointer. + // - A typical use case where this is convenient for quick hacking (e.g. add storage during a live Edit&Continue session if you can't modify existing struct) + // float* pvar = ImGui::GetFloatRef(key); ImGui::SliderFloat("var", pvar, 0, 100.0f); some_var += *pvar; + IMGUI_API int* GetIntRef(ImGuiID key, int default_val = 0); + IMGUI_API bool* GetBoolRef(ImGuiID key, bool default_val = false); + IMGUI_API float* GetFloatRef(ImGuiID key, float default_val = 0.0f); + IMGUI_API void** GetVoidPtrRef(ImGuiID key, void* default_val = NULL); + + // Use on your own storage if you know only integer are being stored (open/close all tree nodes) + IMGUI_API void SetAllInt(int val); + + // For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. + IMGUI_API void BuildSortByKey(); +}; + +// Helper: Manually clip large list of items. +// If you have lots evenly spaced items and you have a random access to the list, you can perform coarse +// clipping based on visibility to only submit items that are in view. +// The clipper calculates the range of visible items and advance the cursor to compensate for the non-visible items we have skipped. +// (Dear ImGui already clip items based on their bounds but: it needs to first layout the item to do so, and generally +// fetching/submitting your own data incurs additional cost. Coarse clipping using ImGuiListClipper allows you to easily +// scale using lists with tens of thousands of items without a problem) +// Usage: +// ImGuiListClipper clipper; +// clipper.Begin(1000); // We have 1000 elements, evenly spaced. +// while (clipper.Step()) +// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) +// ImGui::Text("line number %d", i); +// Generally what happens is: +// - Clipper lets you process the first element (DisplayStart = 0, DisplayEnd = 1) regardless of it being visible or not. +// - User code submit that one element. +// - Clipper can measure the height of the first element +// - Clipper calculate the actual range of elements to display based on the current clipping rectangle, position the cursor before the first visible element. +// - User code submit visible elements. +// - The clipper also handles various subtleties related to keyboard/gamepad navigation, wrapping etc. +struct ImGuiListClipper +{ + int DisplayStart; // First item to display, updated by each call to Step() + int DisplayEnd; // End of items to display (exclusive) + int ItemsCount; // [Internal] Number of items + float ItemsHeight; // [Internal] Height of item after a first step and item submission can calculate it + float StartPosY; // [Internal] Cursor position at the time of Begin() or after table frozen rows are all processed + void* TempData; // [Internal] Internal data + + // items_count: Use INT_MAX if you don't know how many items you have (in which case the cursor won't be advanced in the final step) + // items_height: Use -1.0f to be calculated automatically on first step. Otherwise pass in the distance between your items, typically GetTextLineHeightWithSpacing() or GetFrameHeightWithSpacing(). + IMGUI_API ImGuiListClipper(); + IMGUI_API ~ImGuiListClipper(); + IMGUI_API void Begin(int items_count, float items_height = -1.0f); + IMGUI_API void End(); // Automatically called on the last call of Step() that returns false. + IMGUI_API bool Step(); // Call until it returns false. The DisplayStart/DisplayEnd fields will be set and you can process/draw those items. + + // Call ForceDisplayRangeByIndices() before first call to Step() if you need a range of items to be displayed regardless of visibility. + IMGUI_API void ForceDisplayRangeByIndices(int item_min, int item_max); // item_max is exclusive e.g. use (42, 42+1) to make item 42 always visible BUT due to alignment/padding of certain items it is likely that an extra item may be included on either end of the display range. + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + inline ImGuiListClipper(int items_count, float items_height = -1.0f) { memset(this, 0, sizeof(*this)); ItemsCount = -1; Begin(items_count, items_height); } // [removed in 1.79] +#endif +}; + +// Helpers macros to generate 32-bit encoded colors +#ifdef IMGUI_USE_BGRA_PACKED_COLOR +#define IM_COL32_R_SHIFT 16 +#define IM_COL32_G_SHIFT 8 +#define IM_COL32_B_SHIFT 0 +#define IM_COL32_A_SHIFT 24 +#define IM_COL32_A_MASK 0xFF000000 +#else +#define IM_COL32_R_SHIFT 0 +#define IM_COL32_G_SHIFT 8 +#define IM_COL32_B_SHIFT 16 +#define IM_COL32_A_SHIFT 24 +#define IM_COL32_A_MASK 0xFF000000 +#endif +#define IM_COL32(R,G,B,A) (((ImU32)(A)<> IM_COL32_R_SHIFT) & 0xFF) * sc; Value.y = (float)((rgba >> IM_COL32_G_SHIFT) & 0xFF) * sc; Value.z = (float)((rgba >> IM_COL32_B_SHIFT) & 0xFF) * sc; Value.w = (float)((rgba >> IM_COL32_A_SHIFT) & 0xFF) * sc; } + ImColor(float r, float g, float b, float a = 1.0f) { Value.x = r; Value.y = g; Value.z = b; Value.w = a; } + ImColor(const ImVec4& col) { Value = col; } + inline operator ImU32() const { return ImGui::ColorConvertFloat4ToU32(Value); } + inline operator ImVec4() const { return Value; } + + // FIXME-OBSOLETE: May need to obsolete/cleanup those helpers. + inline void SetHSV(float h, float s, float v, float a = 1.0f){ ImGui::ColorConvertHSVtoRGB(h, s, v, Value.x, Value.y, Value.z); Value.w = a; } + static ImColor HSV(float h, float s, float v, float a = 1.0f) { float r, g, b; ImGui::ColorConvertHSVtoRGB(h, s, v, r, g, b); return ImColor(r, g, b, a); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Drawing API (ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawListFlags, ImDrawList, ImDrawData) +// Hold a series of drawing commands. The user provides a renderer for ImDrawData which essentially contains an array of ImDrawList. +//----------------------------------------------------------------------------- + +// The maximum line width to bake anti-aliased textures for. Build atlas with ImFontAtlasFlags_NoBakedLines to disable baking. +#ifndef IM_DRAWLIST_TEX_LINES_WIDTH_MAX +#define IM_DRAWLIST_TEX_LINES_WIDTH_MAX (63) +#endif + +// ImDrawCallback: Draw callbacks for advanced uses [configurable type: override in imconfig.h] +// NB: You most likely do NOT need to use draw callbacks just to create your own widget or customized UI rendering, +// you can poke into the draw list for that! Draw callback may be useful for example to: +// A) Change your GPU render state, +// B) render a complex 3D scene inside a UI element without an intermediate texture/render target, etc. +// The expected behavior from your rendering function is 'if (cmd.UserCallback != NULL) { cmd.UserCallback(parent_list, cmd); } else { RenderTriangles() }' +// If you want to override the signature of ImDrawCallback, you can simply use e.g. '#define ImDrawCallback MyDrawCallback' (in imconfig.h) + update rendering backend accordingly. +#ifndef ImDrawCallback +typedef void (*ImDrawCallback)(const ImDrawList* parent_list, const ImDrawCmd* cmd); +#endif + +// Special Draw callback value to request renderer backend to reset the graphics/render state. +// The renderer backend needs to handle this special value, otherwise it will crash trying to call a function at this address. +// This is useful for example if you submitted callbacks which you know have altered the render state and you want it to be restored. +// It is not done by default because they are many perfectly useful way of altering render state for imgui contents (e.g. changing shader/blending settings before an Image call). +#define ImDrawCallback_ResetRenderState (ImDrawCallback)(-1) + +// Typically, 1 command = 1 GPU draw call (unless command is a callback) +// - VtxOffset: When 'io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset' is enabled, +// this fields allow us to render meshes larger than 64K vertices while keeping 16-bit indices. +// Backends made for <1.71. will typically ignore the VtxOffset fields. +// - The ClipRect/TextureId/VtxOffset fields must be contiguous as we memcmp() them together (this is asserted for). +struct ImDrawCmd +{ + ImVec4 ClipRect; // 4*4 // Clipping rectangle (x1, y1, x2, y2). Subtract ImDrawData->DisplayPos to get clipping rectangle in "viewport" coordinates + ImTextureID TextureId; // 4-8 // User-provided texture ID. Set by user in ImfontAtlas::SetTexID() for fonts or passed to Image*() functions. Ignore if never using images or multiple fonts atlas. + unsigned int VtxOffset; // 4 // Start offset in vertex buffer. ImGuiBackendFlags_RendererHasVtxOffset: always 0, otherwise may be >0 to support meshes larger than 64K vertices with 16-bit indices. + unsigned int IdxOffset; // 4 // Start offset in index buffer. + unsigned int ElemCount; // 4 // Number of indices (multiple of 3) to be rendered as triangles. Vertices are stored in the callee ImDrawList's vtx_buffer[] array, indices in idx_buffer[]. + ImDrawCallback UserCallback; // 4-8 // If != NULL, call the function instead of rendering the vertices. clip_rect and texture_id will be set normally. + void* UserCallbackData; // 4-8 // The draw callback code can access this. + + ImDrawCmd() { memset(this, 0, sizeof(*this)); } // Also ensure our padding fields are zeroed + + // Since 1.83: returns ImTextureID associated with this draw call. Warning: DO NOT assume this is always same as 'TextureId' (we will change this function for an upcoming feature) + inline ImTextureID GetTexID() const { return TextureId; } +}; + +// Vertex layout +#ifndef IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT +struct ImDrawVert +{ + ImVec2 pos; + ImVec2 uv; + ImU32 col; +}; +#else +// You can override the vertex format layout by defining IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT in imconfig.h +// The code expect ImVec2 pos (8 bytes), ImVec2 uv (8 bytes), ImU32 col (4 bytes), but you can re-order them or add other fields as needed to simplify integration in your engine. +// The type has to be described within the macro (you can either declare the struct or use a typedef). This is because ImVec2/ImU32 are likely not declared a the time you'd want to set your type up. +// NOTE: IMGUI DOESN'T CLEAR THE STRUCTURE AND DOESN'T CALL A CONSTRUCTOR SO ANY CUSTOM FIELD WILL BE UNINITIALIZED. IF YOU ADD EXTRA FIELDS (SUCH AS A 'Z' COORDINATES) YOU WILL NEED TO CLEAR THEM DURING RENDER OR TO IGNORE THEM. +IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT; +#endif + +// [Internal] For use by ImDrawList +struct ImDrawCmdHeader +{ + ImVec4 ClipRect; + ImTextureID TextureId; + unsigned int VtxOffset; +}; + +// [Internal] For use by ImDrawListSplitter +struct ImDrawChannel +{ + ImVector _CmdBuffer; + ImVector _IdxBuffer; +}; + + +// Split/Merge functions are used to split the draw list into different layers which can be drawn into out of order. +// This is used by the Columns/Tables API, so items of each column can be batched together in a same draw call. +struct ImDrawListSplitter +{ + int _Current; // Current channel number (0) + int _Count; // Number of active channels (1+) + ImVector _Channels; // Draw channels (not resized down so _Count might be < Channels.Size) + + inline ImDrawListSplitter() { memset(this, 0, sizeof(*this)); } + inline ~ImDrawListSplitter() { ClearFreeMemory(); } + inline void Clear() { _Current = 0; _Count = 1; } // Do not clear Channels[] so our allocations are reused next frame + IMGUI_API void ClearFreeMemory(); + IMGUI_API void Split(ImDrawList* draw_list, int count); + IMGUI_API void Merge(ImDrawList* draw_list); + IMGUI_API void SetCurrentChannel(ImDrawList* draw_list, int channel_idx); +}; + +// Flags for ImDrawList functions +// (Legacy: bit 0 must always correspond to ImDrawFlags_Closed to be backward compatible with old API using a bool. Bits 1..3 must be unused) +enum ImDrawFlags_ +{ + ImDrawFlags_None = 0, + ImDrawFlags_Closed = 1 << 0, // PathStroke(), AddPolyline(): specify that shape should be closed (Important: this is always == 1 for legacy reason) + ImDrawFlags_RoundCornersTopLeft = 1 << 4, // AddRect(), AddRectFilled(), PathRect(): enable rounding top-left corner only (when rounding > 0.0f, we default to all corners). Was 0x01. + ImDrawFlags_RoundCornersTopRight = 1 << 5, // AddRect(), AddRectFilled(), PathRect(): enable rounding top-right corner only (when rounding > 0.0f, we default to all corners). Was 0x02. + ImDrawFlags_RoundCornersBottomLeft = 1 << 6, // AddRect(), AddRectFilled(), PathRect(): enable rounding bottom-left corner only (when rounding > 0.0f, we default to all corners). Was 0x04. + ImDrawFlags_RoundCornersBottomRight = 1 << 7, // AddRect(), AddRectFilled(), PathRect(): enable rounding bottom-right corner only (when rounding > 0.0f, we default to all corners). Wax 0x08. + ImDrawFlags_RoundCornersNone = 1 << 8, // AddRect(), AddRectFilled(), PathRect(): disable rounding on all corners (when rounding > 0.0f). This is NOT zero, NOT an implicit flag! + ImDrawFlags_RoundCornersTop = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight, + ImDrawFlags_RoundCornersBottom = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, + ImDrawFlags_RoundCornersLeft = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersTopLeft, + ImDrawFlags_RoundCornersRight = ImDrawFlags_RoundCornersBottomRight | ImDrawFlags_RoundCornersTopRight, + ImDrawFlags_RoundCornersAll = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight | ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, + ImDrawFlags_RoundCornersDefault_ = ImDrawFlags_RoundCornersAll, // Default to ALL corners if none of the _RoundCornersXX flags are specified. + ImDrawFlags_RoundCornersMask_ = ImDrawFlags_RoundCornersAll | ImDrawFlags_RoundCornersNone +}; + +// Flags for ImDrawList instance. Those are set automatically by ImGui:: functions from ImGuiIO settings, and generally not manipulated directly. +// It is however possible to temporarily alter flags between calls to ImDrawList:: functions. +enum ImDrawListFlags_ +{ + ImDrawListFlags_None = 0, + ImDrawListFlags_AntiAliasedLines = 1 << 0, // Enable anti-aliased lines/borders (*2 the number of triangles for 1.0f wide line or lines thin enough to be drawn using textures, otherwise *3 the number of triangles) + ImDrawListFlags_AntiAliasedLinesUseTex = 1 << 1, // Enable anti-aliased lines/borders using textures when possible. Require backend to render with bilinear filtering. + ImDrawListFlags_AntiAliasedFill = 1 << 2, // Enable anti-aliased edge around filled shapes (rounded rectangles, circles). + ImDrawListFlags_AllowVtxOffset = 1 << 3 // Can emit 'VtxOffset > 0' to allow large meshes. Set when 'ImGuiBackendFlags_RendererHasVtxOffset' is enabled. +}; + +// Draw command list +// This is the low-level list of polygons that ImGui:: functions are filling. At the end of the frame, +// all command lists are passed to your ImGuiIO::RenderDrawListFn function for rendering. +// Each dear imgui window contains its own ImDrawList. You can use ImGui::GetWindowDrawList() to +// access the current window draw list and draw custom primitives. +// You can interleave normal ImGui:: calls and adding primitives to the current draw list. +// In single viewport mode, top-left is == GetMainViewport()->Pos (generally 0,0), bottom-right is == GetMainViewport()->Pos+Size (generally io.DisplaySize). +// You are totally free to apply whatever transformation matrix to want to the data (depending on the use of the transformation you may want to apply it to ClipRect as well!) +// Important: Primitives are always added to the list and not culled (culling is done at higher-level by ImGui:: functions), if you use this API a lot consider coarse culling your drawn objects. +struct ImDrawList +{ + // This is what you have to render + ImVector CmdBuffer; // Draw commands. Typically 1 command = 1 GPU draw call, unless the command is a callback. + ImVector IdxBuffer; // Index buffer. Each command consume ImDrawCmd::ElemCount of those + ImVector VtxBuffer; // Vertex buffer. + ImDrawListFlags Flags; // Flags, you may poke into these to adjust anti-aliasing settings per-primitive. + + // [Internal, used while building lists] + unsigned int _VtxCurrentIdx; // [Internal] generally == VtxBuffer.Size unless we are past 64K vertices, in which case this gets reset to 0. + const ImDrawListSharedData* _Data; // Pointer to shared draw data (you can use ImGui::GetDrawListSharedData() to get the one from current ImGui context) + const char* _OwnerName; // Pointer to owner window's name for debugging + ImDrawVert* _VtxWritePtr; // [Internal] point within VtxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) + ImDrawIdx* _IdxWritePtr; // [Internal] point within IdxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) + ImVector _ClipRectStack; // [Internal] + ImVector _TextureIdStack; // [Internal] + ImVector _Path; // [Internal] current path building + ImDrawCmdHeader _CmdHeader; // [Internal] template of active commands. Fields should match those of CmdBuffer.back(). + ImDrawListSplitter _Splitter; // [Internal] for channels api (note: prefer using your own persistent instance of ImDrawListSplitter!) + float _FringeScale; // [Internal] anti-alias fringe is scaled by this value, this helps to keep things sharp while zooming at vertex buffer content + + // If you want to create ImDrawList instances, pass them ImGui::GetDrawListSharedData() or create and use your own ImDrawListSharedData (so you can use ImDrawList without ImGui) + ImDrawList(const ImDrawListSharedData* shared_data) { memset(this, 0, sizeof(*this)); _Data = shared_data; } + + ~ImDrawList() { _ClearFreeMemory(); } + IMGUI_API void PushClipRect(ImVec2 clip_rect_min, ImVec2 clip_rect_max, bool intersect_with_current_clip_rect = false); // Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) + IMGUI_API void PushClipRectFullScreen(); + IMGUI_API void PopClipRect(); + IMGUI_API void PushTextureID(ImTextureID texture_id); + IMGUI_API void PopTextureID(); + inline ImVec2 GetClipRectMin() const { const ImVec4& cr = _ClipRectStack.back(); return ImVec2(cr.x, cr.y); } + inline ImVec2 GetClipRectMax() const { const ImVec4& cr = _ClipRectStack.back(); return ImVec2(cr.z, cr.w); } + + // Primitives + // - For rectangular primitives, "p_min" and "p_max" represent the upper-left and lower-right corners. + // - For circle primitives, use "num_segments == 0" to automatically calculate tessellation (preferred). + // In older versions (until Dear ImGui 1.77) the AddCircle functions defaulted to num_segments == 12. + // In future versions we will use textures to provide cheaper and higher-quality circles. + // Use AddNgon() and AddNgonFilled() functions if you need to guaranteed a specific number of sides. + IMGUI_API void AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0, float thickness = 1.0f); // a: upper-left, b: lower-right (== upper-left + size) + IMGUI_API void AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0); // a: upper-left, b: lower-right (== upper-left + size) + IMGUI_API void AddRectFilledMultiColor(const ImVec2& p_min, const ImVec2& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left); + IMGUI_API void AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col); + IMGUI_API void AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col); + IMGUI_API void AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments = 0, float thickness = 1.0f); + IMGUI_API void AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments = 0); + IMGUI_API void AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness = 1.0f); + IMGUI_API void AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments); + IMGUI_API void AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL); + IMGUI_API void AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL, float wrap_width = 0.0f, const ImVec4* cpu_fine_clip_rect = NULL); + IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness); + IMGUI_API void AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col); // Note: Anti-aliased filling requires points to be in clockwise order. + IMGUI_API void AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments = 0); // Cubic Bezier (4 control points) + IMGUI_API void AddBezierQuadratic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness, int num_segments = 0); // Quadratic Bezier (3 control points) + + // Image primitives + // - Read FAQ to understand what ImTextureID is. + // - "p_min" and "p_max" represent the upper-left and lower-right corners of the rectangle. + // - "uv_min" and "uv_max" represent the normalized texture coordinates to use for those corners. Using (0,0)->(1,1) texture coordinates will generally display the entire texture. + IMGUI_API void AddImage(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min = ImVec2(0, 0), const ImVec2& uv_max = ImVec2(1, 1), ImU32 col = IM_COL32_WHITE); + IMGUI_API void AddImageQuad(ImTextureID user_texture_id, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1 = ImVec2(0, 0), const ImVec2& uv2 = ImVec2(1, 0), const ImVec2& uv3 = ImVec2(1, 1), const ImVec2& uv4 = ImVec2(0, 1), ImU32 col = IM_COL32_WHITE); + IMGUI_API void AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags = 0); + + // Stateful path API, add points then finish with PathFillConvex() or PathStroke() + inline void PathClear() { _Path.Size = 0; } + inline void PathLineTo(const ImVec2& pos) { _Path.push_back(pos); } + inline void PathLineToMergeDuplicate(const ImVec2& pos) { if (_Path.Size == 0 || memcmp(&_Path.Data[_Path.Size - 1], &pos, 8) != 0) _Path.push_back(pos); } + inline void PathFillConvex(ImU32 col) { AddConvexPolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } // Note: Anti-aliased filling requires points to be in clockwise order. + inline void PathStroke(ImU32 col, ImDrawFlags flags = 0, float thickness = 1.0f) { AddPolyline(_Path.Data, _Path.Size, col, flags, thickness); _Path.Size = 0; } + IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 0); + IMGUI_API void PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle + IMGUI_API void PathBezierCubicCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0); // Cubic Bezier (4 control points) + IMGUI_API void PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3, int num_segments = 0); // Quadratic Bezier (3 control points) + IMGUI_API void PathRect(const ImVec2& rect_min, const ImVec2& rect_max, float rounding = 0.0f, ImDrawFlags flags = 0); + + // Advanced + IMGUI_API void AddCallback(ImDrawCallback callback, void* callback_data); // Your rendering function must check for 'UserCallback' in ImDrawCmd and call the function instead of rendering triangles. + IMGUI_API void AddDrawCmd(); // This is useful if you need to forcefully create a new draw call (to allow for dependent rendering / blending). Otherwise primitives are merged into the same draw-call as much as possible + IMGUI_API ImDrawList* CloneOutput() const; // Create a clone of the CmdBuffer/IdxBuffer/VtxBuffer. + + // Advanced: Channels + // - Use to split render into layers. By switching channels to can render out-of-order (e.g. submit FG primitives before BG primitives) + // - Use to minimize draw calls (e.g. if going back-and-forth between multiple clipping rectangles, prefer to append into separate channels then merge at the end) + // - FIXME-OBSOLETE: This API shouldn't have been in ImDrawList in the first place! + // Prefer using your own persistent instance of ImDrawListSplitter as you can stack them. + // Using the ImDrawList::ChannelsXXXX you cannot stack a split over another. + inline void ChannelsSplit(int count) { _Splitter.Split(this, count); } + inline void ChannelsMerge() { _Splitter.Merge(this); } + inline void ChannelsSetCurrent(int n) { _Splitter.SetCurrentChannel(this, n); } + + // Advanced: Primitives allocations + // - We render triangles (three vertices) + // - All primitives needs to be reserved via PrimReserve() beforehand. + IMGUI_API void PrimReserve(int idx_count, int vtx_count); + IMGUI_API void PrimUnreserve(int idx_count, int vtx_count); + IMGUI_API void PrimRect(const ImVec2& a, const ImVec2& b, ImU32 col); // Axis aligned rectangle (composed of two triangles) + IMGUI_API void PrimRectUV(const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, ImU32 col); + IMGUI_API void PrimQuadUV(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a, const ImVec2& uv_b, const ImVec2& uv_c, const ImVec2& uv_d, ImU32 col); + inline void PrimWriteVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col) { _VtxWritePtr->pos = pos; _VtxWritePtr->uv = uv; _VtxWritePtr->col = col; _VtxWritePtr++; _VtxCurrentIdx++; } + inline void PrimWriteIdx(ImDrawIdx idx) { *_IdxWritePtr = idx; _IdxWritePtr++; } + inline void PrimVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col) { PrimWriteIdx((ImDrawIdx)_VtxCurrentIdx); PrimWriteVtx(pos, uv, col); } // Write vertex with unique index + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + inline void AddBezierCurve(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments = 0) { AddBezierCubic(p1, p2, p3, p4, col, thickness, num_segments); } // OBSOLETED in 1.80 (Jan 2021) + inline void PathBezierCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0) { PathBezierCubicCurveTo(p2, p3, p4, num_segments); } // OBSOLETED in 1.80 (Jan 2021) +#endif + + // [Internal helpers] + IMGUI_API void _ResetForNewFrame(); + IMGUI_API void _ClearFreeMemory(); + IMGUI_API void _PopUnusedDrawCmd(); + IMGUI_API void _TryMergeDrawCmds(); + IMGUI_API void _OnChangedClipRect(); + IMGUI_API void _OnChangedTextureID(); + IMGUI_API void _OnChangedVtxOffset(); + IMGUI_API int _CalcCircleAutoSegmentCount(float radius) const; + IMGUI_API void _PathArcToFastEx(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step); + IMGUI_API void _PathArcToN(const ImVec2& center, float radius, float a_min, float a_max, int num_segments); +}; + +// All draw data to render a Dear ImGui frame +// (NB: the style and the naming convention here is a little inconsistent, we currently preserve them for backward compatibility purpose, +// as this is one of the oldest structure exposed by the library! Basically, ImDrawList == CmdList) +struct ImDrawData +{ + bool Valid; // Only valid after Render() is called and before the next NewFrame() is called. + int CmdListsCount; // Number of ImDrawList* to render + int TotalIdxCount; // For convenience, sum of all ImDrawList's IdxBuffer.Size + int TotalVtxCount; // For convenience, sum of all ImDrawList's VtxBuffer.Size + ImDrawList** CmdLists; // Array of ImDrawList* to render. The ImDrawList are owned by ImGuiContext and only pointed to from here. + ImVec2 DisplayPos; // Top-left position of the viewport to render (== top-left of the orthogonal projection matrix to use) (== GetMainViewport()->Pos for the main viewport, == (0.0) in most single-viewport applications) + ImVec2 DisplaySize; // Size of the viewport to render (== GetMainViewport()->Size for the main viewport, == io.DisplaySize in most single-viewport applications) + ImVec2 FramebufferScale; // Amount of pixels for each unit of DisplaySize. Based on io.DisplayFramebufferScale. Generally (1,1) on normal display, (2,2) on OSX with Retina display. + + // Functions + ImDrawData() { Clear(); } + void Clear() { memset(this, 0, sizeof(*this)); } // The ImDrawList are owned by ImGuiContext! + IMGUI_API void DeIndexAllBuffers(); // Helper to convert all buffers from indexed to non-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! + IMGUI_API void ScaleClipRects(const ImVec2& fb_scale); // Helper to scale the ClipRect field of each ImDrawCmd. Use if your final output buffer is at a different scale than Dear ImGui expects, or if there is a difference between your window resolution and framebuffer resolution. +}; + +//----------------------------------------------------------------------------- +// [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontAtlasFlags, ImFontAtlas, ImFontGlyphRangesBuilder, ImFont) +//----------------------------------------------------------------------------- + +struct ImFontConfig +{ + void* FontData; // // TTF/OTF data + int FontDataSize; // // TTF/OTF data size + bool FontDataOwnedByAtlas; // true // TTF/OTF data ownership taken by the container ImFontAtlas (will delete memory itself). + int FontNo; // 0 // Index of font within TTF/OTF file + float SizePixels; // // Size in pixels for rasterizer (more or less maps to the resulting font height). + int OversampleH; // 3 // Rasterize at higher quality for sub-pixel positioning. Note the difference between 2 and 3 is minimal so you can reduce this to 2 to save memory. Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. + int OversampleV; // 1 // Rasterize at higher quality for sub-pixel positioning. This is not really useful as we don't use sub-pixel positions on the Y axis. + bool PixelSnapH; // false // Align every glyph to pixel boundary. Useful e.g. if you are merging a non-pixel aligned font with the default font. If enabled, you can set OversampleH/V to 1. + ImVec2 GlyphExtraSpacing; // 0, 0 // Extra spacing (in pixels) between glyphs. Only X axis is supported for now. + ImVec2 GlyphOffset; // 0, 0 // Offset all glyphs from this font input. + const ImWchar* GlyphRanges; // NULL // Pointer to a user-provided list of Unicode range (2 value per range, values are inclusive, zero-terminated list). THE ARRAY DATA NEEDS TO PERSIST AS LONG AS THE FONT IS ALIVE. + float GlyphMinAdvanceX; // 0 // Minimum AdvanceX for glyphs, set Min to align font icons, set both Min/Max to enforce mono-space font + float GlyphMaxAdvanceX; // FLT_MAX // Maximum AdvanceX for glyphs + bool MergeMode; // false // Merge into previous ImFont, so you can combine multiple inputs font into one ImFont (e.g. ASCII font + icons + Japanese glyphs). You may want to use GlyphOffset.y when merge font of different heights. + unsigned int FontBuilderFlags; // 0 // Settings for custom font builder. THIS IS BUILDER IMPLEMENTATION DEPENDENT. Leave as zero if unsure. + float RasterizerMultiply; // 1.0f // Brighten (>1.0f) or darken (<1.0f) font output. Brightening small fonts may be a good workaround to make them more readable. + ImWchar EllipsisChar; // -1 // Explicitly specify unicode codepoint of ellipsis character. When fonts are being merged first specified ellipsis will be used. + + // [Internal] + char Name[40]; // Name (strictly to ease debugging) + ImFont* DstFont; + + IMGUI_API ImFontConfig(); +}; + +// Hold rendering data for one glyph. +// (Note: some language parsers may fail to convert the 31+1 bitfield members, in this case maybe drop store a single u32 or we can rework this) +struct ImFontGlyph +{ + unsigned int Colored : 1; // Flag to indicate glyph is colored and should generally ignore tinting (make it usable with no shift on little-endian as this is used in loops) + unsigned int Visible : 1; // Flag to indicate glyph has no visible pixels (e.g. space). Allow early out when rendering. + unsigned int Codepoint : 30; // 0x0000..0x10FFFF + float AdvanceX; // Distance to next character (= data from font + ImFontConfig::GlyphExtraSpacing.x baked in) + float X0, Y0, X1, Y1; // Glyph corners + float U0, V0, U1, V1; // Texture coordinates +}; + +// Helper to build glyph ranges from text/string data. Feed your application strings/characters to it then call BuildRanges(). +// This is essentially a tightly packed of vector of 64k booleans = 8KB storage. +struct ImFontGlyphRangesBuilder +{ + ImVector UsedChars; // Store 1-bit per Unicode code point (0=unused, 1=used) + + ImFontGlyphRangesBuilder() { Clear(); } + inline void Clear() { int size_in_bytes = (IM_UNICODE_CODEPOINT_MAX + 1) / 8; UsedChars.resize(size_in_bytes / (int)sizeof(ImU32)); memset(UsedChars.Data, 0, (size_t)size_in_bytes); } + inline bool GetBit(size_t n) const { int off = (int)(n >> 5); ImU32 mask = 1u << (n & 31); return (UsedChars[off] & mask) != 0; } // Get bit n in the array + inline void SetBit(size_t n) { int off = (int)(n >> 5); ImU32 mask = 1u << (n & 31); UsedChars[off] |= mask; } // Set bit n in the array + inline void AddChar(ImWchar c) { SetBit(c); } // Add character + IMGUI_API void AddText(const char* text, const char* text_end = NULL); // Add string (each character of the UTF-8 string are added) + IMGUI_API void AddRanges(const ImWchar* ranges); // Add ranges, e.g. builder.AddRanges(ImFontAtlas::GetGlyphRangesDefault()) to force add all of ASCII/Latin+Ext + IMGUI_API void BuildRanges(ImVector* out_ranges); // Output new ranges +}; + +// See ImFontAtlas::AddCustomRectXXX functions. +struct ImFontAtlasCustomRect +{ + unsigned short Width, Height; // Input // Desired rectangle dimension + unsigned short X, Y; // Output // Packed position in Atlas + unsigned int GlyphID; // Input // For custom font glyphs only (ID < 0x110000) + float GlyphAdvanceX; // Input // For custom font glyphs only: glyph xadvance + ImVec2 GlyphOffset; // Input // For custom font glyphs only: glyph display offset + ImFont* Font; // Input // For custom font glyphs only: target font + ImFontAtlasCustomRect() { Width = Height = 0; X = Y = 0xFFFF; GlyphID = 0; GlyphAdvanceX = 0.0f; GlyphOffset = ImVec2(0, 0); Font = NULL; } + bool IsPacked() const { return X != 0xFFFF; } +}; + +// Flags for ImFontAtlas build +enum ImFontAtlasFlags_ +{ + ImFontAtlasFlags_None = 0, + ImFontAtlasFlags_NoPowerOfTwoHeight = 1 << 0, // Don't round the height to next power of two + ImFontAtlasFlags_NoMouseCursors = 1 << 1, // Don't build software mouse cursors into the atlas (save a little texture memory) + ImFontAtlasFlags_NoBakedLines = 1 << 2 // Don't build thick line textures into the atlas (save a little texture memory). The AntiAliasedLinesUseTex features uses them, otherwise they will be rendered using polygons (more expensive for CPU/GPU). +}; + +// Load and rasterize multiple TTF/OTF fonts into a same texture. The font atlas will build a single texture holding: +// - One or more fonts. +// - Custom graphics data needed to render the shapes needed by Dear ImGui. +// - Mouse cursor shapes for software cursor rendering (unless setting 'Flags |= ImFontAtlasFlags_NoMouseCursors' in the font atlas). +// It is the user-code responsibility to setup/build the atlas, then upload the pixel data into a texture accessible by your graphics api. +// - Optionally, call any of the AddFont*** functions. If you don't call any, the default font embedded in the code will be loaded for you. +// - Call GetTexDataAsAlpha8() or GetTexDataAsRGBA32() to build and retrieve pixels data. +// - Upload the pixels data into a texture within your graphics system (see imgui_impl_xxxx.cpp examples) +// - Call SetTexID(my_tex_id); and pass the pointer/identifier to your texture in a format natural to your graphics API. +// This value will be passed back to you during rendering to identify the texture. Read FAQ entry about ImTextureID for more details. +// Common pitfalls: +// - If you pass a 'glyph_ranges' array to AddFont*** functions, you need to make sure that your array persist up until the +// atlas is build (when calling GetTexData*** or Build()). We only copy the pointer, not the data. +// - Important: By default, AddFontFromMemoryTTF() takes ownership of the data. Even though we are not writing to it, we will free the pointer on destruction. +// You can set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed, +// - Even though many functions are suffixed with "TTF", OTF data is supported just as well. +// - This is an old API and it is currently awkward for those and and various other reasons! We will address them in the future! +struct ImFontAtlas +{ + IMGUI_API ImFontAtlas(); + IMGUI_API ~ImFontAtlas(); + IMGUI_API ImFont* AddFont(const ImFontConfig* font_cfg); + IMGUI_API ImFont* AddFontDefault(const ImFontConfig* font_cfg = NULL); + IMGUI_API ImFont* AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); + IMGUI_API ImFont* AddFontFromMemoryTTF(void* font_data, int font_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after destruction of the atlas. Set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed. + IMGUI_API ImFont* AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp. + IMGUI_API ImFont* AddFontFromMemoryCompressedBase85TTF(const char* compressed_font_data_base85, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data_base85' still owned by caller. Compress with binary_to_compressed_c.cpp with -base85 parameter. + IMGUI_API void ClearInputData(); // Clear input data (all ImFontConfig structures including sizes, TTF data, glyph ranges, etc.) = all the data used to build the texture and fonts. + IMGUI_API void ClearTexData(); // Clear output texture data (CPU side). Saves RAM once the texture has been copied to graphics memory. + IMGUI_API void ClearFonts(); // Clear output font data (glyphs storage, UV coordinates). + IMGUI_API void Clear(); // Clear all input and output. + + // Build atlas, retrieve pixel data. + // User is in charge of copying the pixels into graphics memory (e.g. create a texture with your engine). Then store your texture handle with SetTexID(). + // The pitch is always = Width * BytesPerPixels (1 or 4) + // Building in RGBA32 format is provided for convenience and compatibility, but note that unless you manually manipulate or copy color data into + // the texture (e.g. when using the AddCustomRect*** api), then the RGB pixels emitted will always be white (~75% of memory/bandwidth waste. + IMGUI_API bool Build(); // Build pixels data. This is called automatically for you by the GetTexData*** functions. + IMGUI_API void GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 1 byte per-pixel + IMGUI_API void GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 4 bytes-per-pixel + bool IsBuilt() const { return Fonts.Size > 0 && TexReady; } // Bit ambiguous: used to detect when user didn't built texture but effectively we should check TexID != 0 except that would be backend dependent... + void SetTexID(ImTextureID id) { TexID = id; } + + //------------------------------------------- + // Glyph Ranges + //------------------------------------------- + + // Helpers to retrieve list of common Unicode ranges (2 value per range, values are inclusive, zero-terminated list) + // NB: Make sure that your string are UTF-8 and NOT in your local code page. In C++11, you can create UTF-8 string literal using the u8"Hello world" syntax. See FAQ for details. + // NB: Consider using ImFontGlyphRangesBuilder to build glyph ranges from textual data. + IMGUI_API const ImWchar* GetGlyphRangesDefault(); // Basic Latin, Extended Latin + IMGUI_API const ImWchar* GetGlyphRangesKorean(); // Default + Korean characters + IMGUI_API const ImWchar* GetGlyphRangesJapanese(); // Default + Hiragana, Katakana, Half-Width, Selection of 2999 Ideographs + IMGUI_API const ImWchar* GetGlyphRangesChineseFull(); // Default + Half-Width + Japanese Hiragana/Katakana + full set of about 21000 CJK Unified Ideographs + IMGUI_API const ImWchar* GetGlyphRangesChineseSimplifiedCommon();// Default + Half-Width + Japanese Hiragana/Katakana + set of 2500 CJK Unified Ideographs for common simplified Chinese + IMGUI_API const ImWchar* GetGlyphRangesCyrillic(); // Default + about 400 Cyrillic characters + IMGUI_API const ImWchar* GetGlyphRangesThai(); // Default + Thai characters + IMGUI_API const ImWchar* GetGlyphRangesVietnamese(); // Default + Vietnamese characters + + //------------------------------------------- + // [BETA] Custom Rectangles/Glyphs API + //------------------------------------------- + + // You can request arbitrary rectangles to be packed into the atlas, for your own purposes. + // - After calling Build(), you can query the rectangle position and render your pixels. + // - If you render colored output, set 'atlas->TexPixelsUseColors = true' as this may help some backends decide of prefered texture format. + // - You can also request your rectangles to be mapped as font glyph (given a font + Unicode point), + // so you can render e.g. custom colorful icons and use them as regular glyphs. + // - Read docs/FONTS.md for more details about using colorful icons. + // - Note: this API may be redesigned later in order to support multi-monitor varying DPI settings. + IMGUI_API int AddCustomRectRegular(int width, int height); + IMGUI_API int AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset = ImVec2(0, 0)); + ImFontAtlasCustomRect* GetCustomRectByIndex(int index) { IM_ASSERT(index >= 0); return &CustomRects[index]; } + + // [Internal] + IMGUI_API void CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) const; + IMGUI_API bool GetMouseCursorTexData(ImGuiMouseCursor cursor, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]); + + //------------------------------------------- + // Members + //------------------------------------------- + + ImFontAtlasFlags Flags; // Build flags (see ImFontAtlasFlags_) + ImTextureID TexID; // User data to refer to the texture once it has been uploaded to user's graphic systems. It is passed back to you during rendering via the ImDrawCmd structure. + int TexDesiredWidth; // Texture width desired by user before Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height. + int TexGlyphPadding; // Padding between glyphs within texture in pixels. Defaults to 1. If your rendering method doesn't rely on bilinear filtering you may set this to 0. + bool Locked; // Marked as Locked by ImGui::NewFrame() so attempt to modify the atlas will assert. + + // [Internal] + // NB: Access texture data via GetTexData*() calls! Which will setup a default font for you. + bool TexReady; // Set when texture was built matching current font input + bool TexPixelsUseColors; // Tell whether our texture data is known to use colors (rather than just alpha channel), in order to help backend select a format. + unsigned char* TexPixelsAlpha8; // 1 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight + unsigned int* TexPixelsRGBA32; // 4 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight * 4 + int TexWidth; // Texture width calculated during Build(). + int TexHeight; // Texture height calculated during Build(). + ImVec2 TexUvScale; // = (1.0f/TexWidth, 1.0f/TexHeight) + ImVec2 TexUvWhitePixel; // Texture coordinates to a white pixel + ImVector Fonts; // Hold all the fonts returned by AddFont*. Fonts[0] is the default font upon calling ImGui::NewFrame(), use ImGui::PushFont()/PopFont() to change the current font. + ImVector CustomRects; // Rectangles for packing custom texture data into the atlas. + ImVector ConfigData; // Configuration data + ImVec4 TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1]; // UVs for baked anti-aliased lines + + // [Internal] Font builder + const ImFontBuilderIO* FontBuilderIO; // Opaque interface to a font builder (default to stb_truetype, can be changed to use FreeType by defining IMGUI_ENABLE_FREETYPE). + unsigned int FontBuilderFlags; // Shared flags (for all fonts) for custom font builder. THIS IS BUILD IMPLEMENTATION DEPENDENT. Per-font override is also available in ImFontConfig. + + // [Internal] Packing data + int PackIdMouseCursors; // Custom texture rectangle ID for white pixel and mouse cursors + int PackIdLines; // Custom texture rectangle ID for baked anti-aliased lines + + // [Obsolete] + //typedef ImFontAtlasCustomRect CustomRect; // OBSOLETED in 1.72+ + //typedef ImFontGlyphRangesBuilder GlyphRangesBuilder; // OBSOLETED in 1.67+ +}; + +// Font runtime data and rendering +// ImFontAtlas automatically loads a default embedded font for you when you call GetTexDataAsAlpha8() or GetTexDataAsRGBA32(). +struct ImFont +{ + // Members: Hot ~20/24 bytes (for CalcTextSize) + ImVector IndexAdvanceX; // 12-16 // out // // Sparse. Glyphs->AdvanceX in a directly indexable way (cache-friendly for CalcTextSize functions which only this this info, and are often bottleneck in large UI). + float FallbackAdvanceX; // 4 // out // = FallbackGlyph->AdvanceX + float FontSize; // 4 // in // // Height of characters/line, set during loading (don't change after loading) + + // Members: Hot ~28/40 bytes (for CalcTextSize + render loop) + ImVector IndexLookup; // 12-16 // out // // Sparse. Index glyphs by Unicode code-point. + ImVector Glyphs; // 12-16 // out // // All glyphs. + const ImFontGlyph* FallbackGlyph; // 4-8 // out // = FindGlyph(FontFallbackChar) + + // Members: Cold ~32/40 bytes + ImFontAtlas* ContainerAtlas; // 4-8 // out // // What we has been loaded into + const ImFontConfig* ConfigData; // 4-8 // in // // Pointer within ContainerAtlas->ConfigData + short ConfigDataCount; // 2 // in // ~ 1 // Number of ImFontConfig involved in creating this font. Bigger than 1 when merging multiple font sources into one ImFont. + ImWchar FallbackChar; // 2 // out // = FFFD/'?' // Character used if a glyph isn't found. + ImWchar EllipsisChar; // 2 // out // = '...' // Character used for ellipsis rendering. + ImWchar DotChar; // 2 // out // = '.' // Character used for ellipsis rendering (if a single '...' character isn't found) + bool DirtyLookupTables; // 1 // out // + float Scale; // 4 // in // = 1.f // Base font scale, multiplied by the per-window font scale which you can adjust with SetWindowFontScale() + float Ascent, Descent; // 4+4 // out // // Ascent: distance from top to bottom of e.g. 'A' [0..FontSize] + int MetricsTotalSurface;// 4 // out // // Total surface in pixels to get an idea of the font rasterization/texture cost (not exact, we approximate the cost of padding between glyphs) + ImU8 Used4kPagesMap[(IM_UNICODE_CODEPOINT_MAX+1)/4096/8]; // 2 bytes if ImWchar=ImWchar16, 34 bytes if ImWchar==ImWchar32. Store 1-bit for each block of 4K codepoints that has one active glyph. This is mainly used to facilitate iterations across all used codepoints. + + // Methods + IMGUI_API ImFont(); + IMGUI_API ~ImFont(); + IMGUI_API const ImFontGlyph*FindGlyph(ImWchar c) const; + IMGUI_API const ImFontGlyph*FindGlyphNoFallback(ImWchar c) const; + float GetCharAdvance(ImWchar c) const { return ((int)c < IndexAdvanceX.Size) ? IndexAdvanceX[(int)c] : FallbackAdvanceX; } + bool IsLoaded() const { return ContainerAtlas != NULL; } + const char* GetDebugName() const { return ConfigData ? ConfigData->Name : ""; } + + // 'max_width' stops rendering after a certain width (could be turned into a 2d size). FLT_MAX to disable. + // 'wrap_width' enable automatic word-wrapping across multiple lines to fit into given width. 0.0f to disable. + IMGUI_API ImVec2 CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end = NULL, const char** remaining = NULL) const; // utf8 + IMGUI_API const char* CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) const; + IMGUI_API void RenderChar(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, ImWchar c) const; + IMGUI_API void RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, bool cpu_fine_clip = false) const; + + // [Internal] Don't use! + IMGUI_API void BuildLookupTable(); + IMGUI_API void ClearOutputData(); + IMGUI_API void GrowIndex(int new_size); + IMGUI_API void AddGlyph(const ImFontConfig* src_cfg, ImWchar c, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x); + IMGUI_API void AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst = true); // Makes 'dst' character/glyph points to 'src' character/glyph. Currently needs to be called AFTER fonts have been built. + IMGUI_API void SetGlyphVisible(ImWchar c, bool visible); + IMGUI_API bool IsGlyphRangeUnused(unsigned int c_begin, unsigned int c_last); +}; + +//----------------------------------------------------------------------------- +// [SECTION] Viewports +//----------------------------------------------------------------------------- + +// Flags stored in ImGuiViewport::Flags, giving indications to the platform backends. +enum ImGuiViewportFlags_ +{ + ImGuiViewportFlags_None = 0, + ImGuiViewportFlags_IsPlatformWindow = 1 << 0, // Represent a Platform Window + ImGuiViewportFlags_IsPlatformMonitor = 1 << 1, // Represent a Platform Monitor (unused yet) + ImGuiViewportFlags_OwnedByApp = 1 << 2 // Platform Window: is created/managed by the application (rather than a dear imgui backend) +}; + +// - Currently represents the Platform Window created by the application which is hosting our Dear ImGui windows. +// - In 'docking' branch with multi-viewport enabled, we extend this concept to have multiple active viewports. +// - In the future we will extend this concept further to also represent Platform Monitor and support a "no main platform window" operation mode. +// - About Main Area vs Work Area: +// - Main Area = entire viewport. +// - Work Area = entire viewport minus sections used by main menu bars (for platform windows), or by task bar (for platform monitor). +// - Windows are generally trying to stay within the Work Area of their host viewport. +struct ImGuiViewport +{ + ImGuiViewportFlags Flags; // See ImGuiViewportFlags_ + ImVec2 Pos; // Main Area: Position of the viewport (Dear ImGui coordinates are the same as OS desktop/native coordinates) + ImVec2 Size; // Main Area: Size of the viewport. + ImVec2 WorkPos; // Work Area: Position of the viewport minus task bars, menus bars, status bars (>= Pos) + ImVec2 WorkSize; // Work Area: Size of the viewport minus task bars, menu bars, status bars (<= Size) + + // Platform/Backend Dependent Data + void* PlatformHandleRaw; // void* to hold lower-level, platform-native window handle (under Win32 this is expected to be a HWND, unused for other platforms) + + ImGuiViewport() { memset(this, 0, sizeof(*this)); } + + // Helpers + ImVec2 GetCenter() const { return ImVec2(Pos.x + Size.x * 0.5f, Pos.y + Size.y * 0.5f); } + ImVec2 GetWorkCenter() const { return ImVec2(WorkPos.x + WorkSize.x * 0.5f, WorkPos.y + WorkSize.y * 0.5f); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Platform Dependent Interfaces +//----------------------------------------------------------------------------- + +// (Optional) Support for IME (Input Method Editor) via the io.SetPlatformImeDataFn() function. +struct ImGuiPlatformImeData +{ + bool WantVisible; // A widget wants the IME to be visible + ImVec2 InputPos; // Position of the input cursor + float InputLineHeight; // Line height + + ImGuiPlatformImeData() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Obsolete functions and types +// (Will be removed! Read 'API BREAKING CHANGES' section in imgui.cpp for details) +// Please keep your copy of dear imgui up to date! Occasionally set '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' in imconfig.h to stay ahead. +//----------------------------------------------------------------------------- + +namespace ImGui +{ +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + IMGUI_API int GetKeyIndex(ImGuiKey key); // map ImGuiKey_* values into legacy native key index. == io.KeyMap[key] +#else + static inline int GetKeyIndex(ImGuiKey key) { IM_ASSERT(key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END && "ImGuiKey and native_index was merged together and native_index is disabled by IMGUI_DISABLE_OBSOLETE_KEYIO. Please switch to ImGuiKey."); return key; } +#endif +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +namespace ImGui +{ + // OBSOLETED in 1.86 (from November 2021) + IMGUI_API void CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end); // Calculate coarse clipping for large list of evenly sized items. Prefer using ImGuiListClipper. + // OBSOLETED in 1.85 (from August 2021) + static inline float GetWindowContentRegionWidth() { return GetWindowContentRegionMax().x - GetWindowContentRegionMin().x; } + // OBSOLETED in 1.81 (from February 2021) + IMGUI_API bool ListBoxHeader(const char* label, int items_count, int height_in_items = -1); // Helper to calculate size from items_count and height_in_items + static inline bool ListBoxHeader(const char* label, const ImVec2& size = ImVec2(0, 0)) { return BeginListBox(label, size); } + static inline void ListBoxFooter() { EndListBox(); } + // OBSOLETED in 1.79 (from August 2020) + static inline void OpenPopupContextItem(const char* str_id = NULL, ImGuiMouseButton mb = 1) { OpenPopupOnItemClick(str_id, mb); } // Bool return value removed. Use IsWindowAppearing() in BeginPopup() instead. Renamed in 1.77, renamed back in 1.79. Sorry! + // OBSOLETED in 1.78 (from June 2020) + // Old drag/sliders functions that took a 'float power = 1.0' argument instead of flags. + // For shared code, you can version check at compile-time with `#if IMGUI_VERSION_NUM >= 17704`. + IMGUI_API bool DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, float power); + IMGUI_API bool DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, float power); + static inline bool DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, float power) { return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, power); } + static inline bool DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, float power) { return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, power); } + static inline bool DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, float power) { return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, power); } + static inline bool DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, float power) { return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, power); } + IMGUI_API bool SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, float power); + IMGUI_API bool SliderScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_min, const void* p_max, const char* format, float power); + static inline bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, float power) { return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, power); } + static inline bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, float power) { return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, power); } + static inline bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, float power) { return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, power); } + static inline bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, float power) { return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, power); } + // OBSOLETED in 1.77 (from June 2020) + static inline bool BeginPopupContextWindow(const char* str_id, ImGuiMouseButton mb, bool over_items) { return BeginPopupContextWindow(str_id, mb | (over_items ? 0 : ImGuiPopupFlags_NoOpenOverItems)); } + + // Some of the older obsolete names along with their replacement (commented out so they are not reported in IDE) + //static inline void TreeAdvanceToLabelPos() { SetCursorPosX(GetCursorPosX() + GetTreeNodeToLabelSpacing()); } // OBSOLETED in 1.72 (from July 2019) + //static inline void SetNextTreeNodeOpen(bool open, ImGuiCond cond = 0) { SetNextItemOpen(open, cond); } // OBSOLETED in 1.71 (from June 2019) + //static inline float GetContentRegionAvailWidth() { return GetContentRegionAvail().x; } // OBSOLETED in 1.70 (from May 2019) + //static inline ImDrawList* GetOverlayDrawList() { return GetForegroundDrawList(); } // OBSOLETED in 1.69 (from Mar 2019) + //static inline void SetScrollHere(float ratio = 0.5f) { SetScrollHereY(ratio); } // OBSOLETED in 1.66 (from Nov 2018) + //static inline bool IsItemDeactivatedAfterChange() { return IsItemDeactivatedAfterEdit(); } // OBSOLETED in 1.63 (from Aug 2018) + //static inline bool IsAnyWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_AnyWindow); } // OBSOLETED in 1.60 (from Apr 2018) + //static inline bool IsAnyWindowHovered() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } // OBSOLETED in 1.60 (between Dec 2017 and Apr 2018) + //static inline void ShowTestWindow() { return ShowDemoWindow(); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline bool IsRootWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootWindow); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline bool IsRootWindowOrAnyChildFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline void SetNextWindowContentWidth(float w) { SetNextWindowContentSize(ImVec2(w, 0.0f)); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline float GetItemsLineHeightWithSpacing() { return GetFrameHeightWithSpacing(); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) +} + +// OBSOLETED in 1.82 (from Mars 2021): flags for AddRect(), AddRectFilled(), AddImageRounded(), PathRect() +typedef ImDrawFlags ImDrawCornerFlags; +enum ImDrawCornerFlags_ +{ + ImDrawCornerFlags_None = ImDrawFlags_RoundCornersNone, // Was == 0 prior to 1.82, this is now == ImDrawFlags_RoundCornersNone which is != 0 and not implicit + ImDrawCornerFlags_TopLeft = ImDrawFlags_RoundCornersTopLeft, // Was == 0x01 (1 << 0) prior to 1.82. Order matches ImDrawFlags_NoRoundCorner* flag (we exploit this internally). + ImDrawCornerFlags_TopRight = ImDrawFlags_RoundCornersTopRight, // Was == 0x02 (1 << 1) prior to 1.82. + ImDrawCornerFlags_BotLeft = ImDrawFlags_RoundCornersBottomLeft, // Was == 0x04 (1 << 2) prior to 1.82. + ImDrawCornerFlags_BotRight = ImDrawFlags_RoundCornersBottomRight, // Was == 0x08 (1 << 3) prior to 1.82. + ImDrawCornerFlags_All = ImDrawFlags_RoundCornersAll, // Was == 0x0F prior to 1.82 + ImDrawCornerFlags_Top = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_TopRight, + ImDrawCornerFlags_Bot = ImDrawCornerFlags_BotLeft | ImDrawCornerFlags_BotRight, + ImDrawCornerFlags_Left = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotLeft, + ImDrawCornerFlags_Right = ImDrawCornerFlags_TopRight | ImDrawCornerFlags_BotRight +}; + +#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +//----------------------------------------------------------------------------- + +#if defined(__clang__) +#pragma clang diagnostic pop +#elif defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +#ifdef _MSC_VER +#pragma warning (pop) +#endif + +// Include imgui_user.h at the end of imgui.h (convenient for user to only explicitly include vanilla imgui.h) +#ifdef IMGUI_INCLUDE_IMGUI_USER_H +#include "imgui_user.h" +#endif + +#endif // #ifndef IMGUI_DISABLE + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Include/imgui_internal.h`: + +```h +// dear imgui, v1.87 +// (internal structures/api) + +// You may use this file to debug, understand or extend ImGui features but we don't provide any guarantee of forward compatibility! +// Set: +// #define IMGUI_DEFINE_MATH_OPERATORS +// To implement maths operators for ImVec2 (disabled by default to not collide with using IM_VEC2_CLASS_EXTRA along with your own math types+operators) + +/* + +Index of this file: + +// [SECTION] Header mess +// [SECTION] Forward declarations +// [SECTION] Context pointer +// [SECTION] STB libraries includes +// [SECTION] Macros +// [SECTION] Generic helpers +// [SECTION] ImDrawList support +// [SECTION] Widgets support: flags, enums, data structures +// [SECTION] Inputs support +// [SECTION] Clipper support +// [SECTION] Navigation support +// [SECTION] Columns support +// [SECTION] Multi-select support +// [SECTION] Docking support +// [SECTION] Viewport support +// [SECTION] Settings support +// [SECTION] Metrics, Debug tools +// [SECTION] Generic context hooks +// [SECTION] ImGuiContext (main imgui context) +// [SECTION] ImGuiWindowTempData, ImGuiWindow +// [SECTION] Tab bar, Tab item support +// [SECTION] Table support +// [SECTION] ImGui internal API +// [SECTION] ImFontAtlas internal API +// [SECTION] Test Engine specific hooks (imgui_test_engine) + +*/ + +#pragma once +#ifndef IMGUI_DISABLE + +//----------------------------------------------------------------------------- +// [SECTION] Header mess +//----------------------------------------------------------------------------- + +#ifndef IMGUI_VERSION +#include "imgui.h" +#endif + +#include // FILE*, sscanf +#include // NULL, malloc, free, qsort, atoi, atof +#include // sqrtf, fabsf, fmodf, powf, floorf, ceilf, cosf, sinf +#include // INT_MIN, INT_MAX + +// Enable SSE intrinsics if available +#if (defined __SSE__ || defined __x86_64__ || defined _M_X64) && !defined(IMGUI_DISABLE_SSE) +#define IMGUI_ENABLE_SSE +#include +#endif + +// Visual Studio warnings +#ifdef _MSC_VER +#pragma warning (push) +#pragma warning (disable: 4251) // class 'xxx' needs to have dll-interface to be used by clients of struct 'xxx' // when IMGUI_API is set to__declspec(dllexport) +#pragma warning (disable: 26812) // The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). [MSVC Static Analyzer) +#pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types +#endif +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#pragma clang diagnostic push +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants ok, for ImFloorSigned() +#pragma clang diagnostic ignored "-Wunused-function" // for stb_textedit.h +#pragma clang diagnostic ignored "-Wmissing-prototypes" // for stb_textedit.h +#pragma clang diagnostic ignored "-Wold-style-cast" +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" +#pragma clang diagnostic ignored "-Wdouble-promotion" +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wmissing-noreturn" // warning: function 'xxx' could be declared with attribute 'noreturn' +#elif defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#endif + +// Legacy defines +#ifdef IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS // Renamed in 1.74 +#error Use IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS +#endif +#ifdef IMGUI_DISABLE_MATH_FUNCTIONS // Renamed in 1.74 +#error Use IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS +#endif + +// Enable stb_truetype by default unless FreeType is enabled. +// You can compile with both by defining both IMGUI_ENABLE_FREETYPE and IMGUI_ENABLE_STB_TRUETYPE together. +#ifndef IMGUI_ENABLE_FREETYPE +#define IMGUI_ENABLE_STB_TRUETYPE +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Forward declarations +//----------------------------------------------------------------------------- + +struct ImBitVector; // Store 1-bit per value +struct ImRect; // An axis-aligned rectangle (2 points) +struct ImDrawDataBuilder; // Helper to build a ImDrawData instance +struct ImDrawListSharedData; // Data shared between all ImDrawList instances +struct ImGuiColorMod; // Stacked color modifier, backup of modified data so we can restore it +struct ImGuiContext; // Main Dear ImGui context +struct ImGuiContextHook; // Hook for extensions like ImGuiTestEngine +struct ImGuiDataTypeInfo; // Type information associated to a ImGuiDataType enum +struct ImGuiGroupData; // Stacked storage data for BeginGroup()/EndGroup() +struct ImGuiInputTextState; // Internal state of the currently focused/edited text input box +struct ImGuiLastItemData; // Status storage for last submitted items +struct ImGuiMenuColumns; // Simple column measurement, currently used for MenuItem() only +struct ImGuiNavItemData; // Result of a gamepad/keyboard directional navigation move query result +struct ImGuiMetricsConfig; // Storage for ShowMetricsWindow() and DebugNodeXXX() functions +struct ImGuiNextWindowData; // Storage for SetNextWindow** functions +struct ImGuiNextItemData; // Storage for SetNextItem** functions +struct ImGuiOldColumnData; // Storage data for a single column for legacy Columns() api +struct ImGuiOldColumns; // Storage data for a columns set for legacy Columns() api +struct ImGuiPopupData; // Storage for current popup stack +struct ImGuiSettingsHandler; // Storage for one type registered in the .ini file +struct ImGuiStackSizes; // Storage of stack sizes for debugging/asserting +struct ImGuiStyleMod; // Stacked style modifier, backup of modified data so we can restore it +struct ImGuiTabBar; // Storage for a tab bar +struct ImGuiTabItem; // Storage for a tab item (within a tab bar) +struct ImGuiTable; // Storage for a table +struct ImGuiTableColumn; // Storage for one column of a table +struct ImGuiTableTempData; // Temporary storage for one table (one per table in the stack), shared between tables. +struct ImGuiTableSettings; // Storage for a table .ini settings +struct ImGuiTableColumnsSettings; // Storage for a column .ini settings +struct ImGuiWindow; // Storage for one window +struct ImGuiWindowTempData; // Temporary storage for one window (that's the data which in theory we could ditch at the end of the frame, in practice we currently keep it for each window) +struct ImGuiWindowSettings; // Storage for a window .ini settings (we keep one of those even if the actual window wasn't instanced during this session) + +// Use your programming IDE "Go to definition" facility on the names of the center columns to find the actual flags/enum lists. +typedef int ImGuiLayoutType; // -> enum ImGuiLayoutType_ // Enum: Horizontal or vertical +typedef int ImGuiActivateFlags; // -> enum ImGuiActivateFlags_ // Flags: for navigation/focus function (will be for ActivateItem() later) +typedef int ImGuiItemFlags; // -> enum ImGuiItemFlags_ // Flags: for PushItemFlag() +typedef int ImGuiItemStatusFlags; // -> enum ImGuiItemStatusFlags_ // Flags: for DC.LastItemStatusFlags +typedef int ImGuiOldColumnFlags; // -> enum ImGuiOldColumnFlags_ // Flags: for BeginColumns() +typedef int ImGuiNavHighlightFlags; // -> enum ImGuiNavHighlightFlags_ // Flags: for RenderNavHighlight() +typedef int ImGuiNavDirSourceFlags; // -> enum ImGuiNavDirSourceFlags_ // Flags: for GetNavInputAmount2d() +typedef int ImGuiNavMoveFlags; // -> enum ImGuiNavMoveFlags_ // Flags: for navigation requests +typedef int ImGuiNextItemDataFlags; // -> enum ImGuiNextItemDataFlags_ // Flags: for SetNextItemXXX() functions +typedef int ImGuiNextWindowDataFlags; // -> enum ImGuiNextWindowDataFlags_// Flags: for SetNextWindowXXX() functions +typedef int ImGuiScrollFlags; // -> enum ImGuiScrollFlags_ // Flags: for ScrollToItem() and navigation requests +typedef int ImGuiSeparatorFlags; // -> enum ImGuiSeparatorFlags_ // Flags: for SeparatorEx() +typedef int ImGuiTextFlags; // -> enum ImGuiTextFlags_ // Flags: for TextEx() +typedef int ImGuiTooltipFlags; // -> enum ImGuiTooltipFlags_ // Flags: for BeginTooltipEx() + +typedef void (*ImGuiErrorLogCallback)(void* user_data, const char* fmt, ...); + +//----------------------------------------------------------------------------- +// [SECTION] Context pointer +// See implementation of this variable in imgui.cpp for comments and details. +//----------------------------------------------------------------------------- + +#ifndef GImGui +extern IMGUI_API ImGuiContext* GImGui; // Current implicit context pointer +#endif + +//------------------------------------------------------------------------- +// [SECTION] STB libraries includes +//------------------------------------------------------------------------- + +namespace ImStb +{ + +#undef STB_TEXTEDIT_STRING +#undef STB_TEXTEDIT_CHARTYPE +#define STB_TEXTEDIT_STRING ImGuiInputTextState +#define STB_TEXTEDIT_CHARTYPE ImWchar +#define STB_TEXTEDIT_GETWIDTH_NEWLINE (-1.0f) +#define STB_TEXTEDIT_UNDOSTATECOUNT 99 +#define STB_TEXTEDIT_UNDOCHARCOUNT 999 +#include "imstb_textedit.h" + +} // namespace ImStb + +//----------------------------------------------------------------------------- +// [SECTION] Macros +//----------------------------------------------------------------------------- + +// Debug Logging +#ifndef IMGUI_DEBUG_LOG +#define IMGUI_DEBUG_LOG(_FMT,...) printf("[%05d] " _FMT, GImGui->FrameCount, __VA_ARGS__) +#endif + +// Debug Logging for selected systems. Remove the '((void)0) //' to enable. +//#define IMGUI_DEBUG_LOG_POPUP IMGUI_DEBUG_LOG // Enable log +//#define IMGUI_DEBUG_LOG_NAV IMGUI_DEBUG_LOG // Enable log +//#define IMGUI_DEBUG_LOG_IO IMGUI_DEBUG_LOG // Enable log +#define IMGUI_DEBUG_LOG_POPUP(...) ((void)0) // Disable log +#define IMGUI_DEBUG_LOG_NAV(...) ((void)0) // Disable log +#define IMGUI_DEBUG_LOG_IO(...) ((void)0) // Disable log + +// Static Asserts +#define IM_STATIC_ASSERT(_COND) static_assert(_COND, "") + +// "Paranoid" Debug Asserts are meant to only be enabled during specific debugging/work, otherwise would slow down the code too much. +// We currently don't have many of those so the effect is currently negligible, but onward intent to add more aggressive ones in the code. +//#define IMGUI_DEBUG_PARANOID +#ifdef IMGUI_DEBUG_PARANOID +#define IM_ASSERT_PARANOID(_EXPR) IM_ASSERT(_EXPR) +#else +#define IM_ASSERT_PARANOID(_EXPR) +#endif + +// Error handling +// Down the line in some frameworks/languages we would like to have a way to redirect those to the programmer and recover from more faults. +#ifndef IM_ASSERT_USER_ERROR +#define IM_ASSERT_USER_ERROR(_EXP,_MSG) IM_ASSERT((_EXP) && _MSG) // Recoverable User Error +#endif + +// Misc Macros +#define IM_PI 3.14159265358979323846f +#ifdef _WIN32 +#define IM_NEWLINE "\r\n" // Play it nice with Windows users (Update: since 2018-05, Notepad finally appears to support Unix-style carriage returns!) +#else +#define IM_NEWLINE "\n" +#endif +#define IM_TABSIZE (4) +#define IM_MEMALIGN(_OFF,_ALIGN) (((_OFF) + (_ALIGN - 1)) & ~(_ALIGN - 1)) // Memory align e.g. IM_ALIGN(0,4)=0, IM_ALIGN(1,4)=4, IM_ALIGN(4,4)=4, IM_ALIGN(5,4)=8 +#define IM_F32_TO_INT8_UNBOUND(_VAL) ((int)((_VAL) * 255.0f + ((_VAL)>=0 ? 0.5f : -0.5f))) // Unsaturated, for display purpose +#define IM_F32_TO_INT8_SAT(_VAL) ((int)(ImSaturate(_VAL) * 255.0f + 0.5f)) // Saturated, always output 0..255 +#define IM_FLOOR(_VAL) ((float)(int)(_VAL)) // ImFloor() is not inlined in MSVC debug builds +#define IM_ROUND(_VAL) ((float)(int)((_VAL) + 0.5f)) // + +// Enforce cdecl calling convention for functions called by the standard library, in case compilation settings changed the default to e.g. __vectorcall +#ifdef _MSC_VER +#define IMGUI_CDECL __cdecl +#else +#define IMGUI_CDECL +#endif + +// Warnings +#if defined(_MSC_VER) && !defined(__clang__) +#define IM_MSVC_WARNING_SUPPRESS(XXXX) __pragma(warning(suppress: XXXX)) +#else +#define IM_MSVC_WARNING_SUPPRESS(XXXX) +#endif + +// Debug Tools +// Use 'Metrics/Debugger->Tools->Item Picker' to break into the call-stack of a specific item. +// This will call IM_DEBUG_BREAK() which you may redefine yourself. See https://github.com/scottt/debugbreak for more reference. +#ifndef IM_DEBUG_BREAK +#if defined (_MSC_VER) +#define IM_DEBUG_BREAK() __debugbreak() +#elif defined(__clang__) +#define IM_DEBUG_BREAK() __builtin_debugtrap() +#elif defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__)) +#define IM_DEBUG_BREAK() __asm__ volatile("int $0x03") +#elif defined(__GNUC__) && defined(__thumb__) +#define IM_DEBUG_BREAK() __asm__ volatile(".inst 0xde01") +#elif defined(__GNUC__) && defined(__arm__) && !defined(__thumb__) +#define IM_DEBUG_BREAK() __asm__ volatile(".inst 0xe7f001f0"); +#else +#define IM_DEBUG_BREAK() IM_ASSERT(0) // It is expected that you define IM_DEBUG_BREAK() into something that will break nicely in a debugger! +#endif +#endif // #ifndef IM_DEBUG_BREAK + +//----------------------------------------------------------------------------- +// [SECTION] Generic helpers +// Note that the ImXXX helpers functions are lower-level than ImGui functions. +// ImGui functions or the ImGui context are never called/used from other ImXXX functions. +//----------------------------------------------------------------------------- +// - Helpers: Hashing +// - Helpers: Sorting +// - Helpers: Bit manipulation +// - Helpers: String, Formatting +// - Helpers: UTF-8 <> wchar conversions +// - Helpers: ImVec2/ImVec4 operators +// - Helpers: Maths +// - Helpers: Geometry +// - Helper: ImVec1 +// - Helper: ImVec2ih +// - Helper: ImRect +// - Helper: ImBitArray +// - Helper: ImBitVector +// - Helper: ImSpan<>, ImSpanAllocator<> +// - Helper: ImPool<> +// - Helper: ImChunkStream<> +//----------------------------------------------------------------------------- + +// Helpers: Hashing +IMGUI_API ImGuiID ImHashData(const void* data, size_t data_size, ImU32 seed = 0); +IMGUI_API ImGuiID ImHashStr(const char* data, size_t data_size = 0, ImU32 seed = 0); +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +static inline ImGuiID ImHash(const void* data, int size, ImU32 seed = 0) { return size ? ImHashData(data, (size_t)size, seed) : ImHashStr((const char*)data, 0, seed); } // [moved to ImHashStr/ImHashData in 1.68] +#endif + +// Helpers: Sorting +#ifndef ImQsort +static inline void ImQsort(void* base, size_t count, size_t size_of_element, int(IMGUI_CDECL *compare_func)(void const*, void const*)) { if (count > 1) qsort(base, count, size_of_element, compare_func); } +#endif + +// Helpers: Color Blending +IMGUI_API ImU32 ImAlphaBlendColors(ImU32 col_a, ImU32 col_b); + +// Helpers: Bit manipulation +static inline bool ImIsPowerOfTwo(int v) { return v != 0 && (v & (v - 1)) == 0; } +static inline bool ImIsPowerOfTwo(ImU64 v) { return v != 0 && (v & (v - 1)) == 0; } +static inline int ImUpperPowerOfTwo(int v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } + +// Helpers: String, Formatting +IMGUI_API int ImStricmp(const char* str1, const char* str2); +IMGUI_API int ImStrnicmp(const char* str1, const char* str2, size_t count); +IMGUI_API void ImStrncpy(char* dst, const char* src, size_t count); +IMGUI_API char* ImStrdup(const char* str); +IMGUI_API char* ImStrdupcpy(char* dst, size_t* p_dst_size, const char* str); +IMGUI_API const char* ImStrchrRange(const char* str_begin, const char* str_end, char c); +IMGUI_API int ImStrlenW(const ImWchar* str); +IMGUI_API const char* ImStreolRange(const char* str, const char* str_end); // End end-of-line +IMGUI_API const ImWchar*ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin); // Find beginning-of-line +IMGUI_API const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end); +IMGUI_API void ImStrTrimBlanks(char* str); +IMGUI_API const char* ImStrSkipBlank(const char* str); +IMGUI_API int ImFormatString(char* buf, size_t buf_size, const char* fmt, ...) IM_FMTARGS(3); +IMGUI_API int ImFormatStringV(char* buf, size_t buf_size, const char* fmt, va_list args) IM_FMTLIST(3); +IMGUI_API const char* ImParseFormatFindStart(const char* format); +IMGUI_API const char* ImParseFormatFindEnd(const char* format); +IMGUI_API const char* ImParseFormatTrimDecorations(const char* format, char* buf, size_t buf_size); +IMGUI_API int ImParseFormatPrecision(const char* format, int default_value); +static inline bool ImCharIsBlankA(char c) { return c == ' ' || c == '\t'; } +static inline bool ImCharIsBlankW(unsigned int c) { return c == ' ' || c == '\t' || c == 0x3000; } + +// Helpers: UTF-8 <> wchar conversions +IMGUI_API const char* ImTextCharToUtf8(char out_buf[5], unsigned int c); // return out_buf +IMGUI_API int ImTextStrToUtf8(char* out_buf, int out_buf_size, const ImWchar* in_text, const ImWchar* in_text_end); // return output UTF-8 bytes count +IMGUI_API int ImTextCharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end); // read one character. return input UTF-8 bytes count +IMGUI_API int ImTextStrFromUtf8(ImWchar* out_buf, int out_buf_size, const char* in_text, const char* in_text_end, const char** in_remaining = NULL); // return input UTF-8 bytes count +IMGUI_API int ImTextCountCharsFromUtf8(const char* in_text, const char* in_text_end); // return number of UTF-8 code-points (NOT bytes count) +IMGUI_API int ImTextCountUtf8BytesFromChar(const char* in_text, const char* in_text_end); // return number of bytes to express one char in UTF-8 +IMGUI_API int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end); // return number of bytes to express string in UTF-8 + +// Helpers: ImVec2/ImVec4 operators +// We are keeping those disabled by default so they don't leak in user space, to allow user enabling implicit cast operators between ImVec2 and their own types (using IM_VEC2_CLASS_EXTRA etc.) +// We unfortunately don't have a unary- operator for ImVec2 because this would needs to be defined inside the class itself. +#ifdef IMGUI_DEFINE_MATH_OPERATORS +IM_MSVC_RUNTIME_CHECKS_OFF +static inline ImVec2 operator*(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x * rhs, lhs.y * rhs); } +static inline ImVec2 operator/(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x / rhs, lhs.y / rhs); } +static inline ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x + rhs.x, lhs.y + rhs.y); } +static inline ImVec2 operator-(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x - rhs.x, lhs.y - rhs.y); } +static inline ImVec2 operator*(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } +static inline ImVec2 operator/(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x / rhs.x, lhs.y / rhs.y); } +static inline ImVec2& operator*=(ImVec2& lhs, const float rhs) { lhs.x *= rhs; lhs.y *= rhs; return lhs; } +static inline ImVec2& operator/=(ImVec2& lhs, const float rhs) { lhs.x /= rhs; lhs.y /= rhs; return lhs; } +static inline ImVec2& operator+=(ImVec2& lhs, const ImVec2& rhs) { lhs.x += rhs.x; lhs.y += rhs.y; return lhs; } +static inline ImVec2& operator-=(ImVec2& lhs, const ImVec2& rhs) { lhs.x -= rhs.x; lhs.y -= rhs.y; return lhs; } +static inline ImVec2& operator*=(ImVec2& lhs, const ImVec2& rhs) { lhs.x *= rhs.x; lhs.y *= rhs.y; return lhs; } +static inline ImVec2& operator/=(ImVec2& lhs, const ImVec2& rhs) { lhs.x /= rhs.x; lhs.y /= rhs.y; return lhs; } +static inline ImVec4 operator+(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z, lhs.w + rhs.w); } +static inline ImVec4 operator-(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z, lhs.w - rhs.w); } +static inline ImVec4 operator*(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x * rhs.x, lhs.y * rhs.y, lhs.z * rhs.z, lhs.w * rhs.w); } +IM_MSVC_RUNTIME_CHECKS_RESTORE +#endif + +// Helpers: File System +#ifdef IMGUI_DISABLE_FILE_FUNCTIONS +#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS +typedef void* ImFileHandle; +static inline ImFileHandle ImFileOpen(const char*, const char*) { return NULL; } +static inline bool ImFileClose(ImFileHandle) { return false; } +static inline ImU64 ImFileGetSize(ImFileHandle) { return (ImU64)-1; } +static inline ImU64 ImFileRead(void*, ImU64, ImU64, ImFileHandle) { return 0; } +static inline ImU64 ImFileWrite(const void*, ImU64, ImU64, ImFileHandle) { return 0; } +#endif +#ifndef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS +typedef FILE* ImFileHandle; +IMGUI_API ImFileHandle ImFileOpen(const char* filename, const char* mode); +IMGUI_API bool ImFileClose(ImFileHandle file); +IMGUI_API ImU64 ImFileGetSize(ImFileHandle file); +IMGUI_API ImU64 ImFileRead(void* data, ImU64 size, ImU64 count, ImFileHandle file); +IMGUI_API ImU64 ImFileWrite(const void* data, ImU64 size, ImU64 count, ImFileHandle file); +#else +#define IMGUI_DISABLE_TTY_FUNCTIONS // Can't use stdout, fflush if we are not using default file functions +#endif +IMGUI_API void* ImFileLoadToMemory(const char* filename, const char* mode, size_t* out_file_size = NULL, int padding_bytes = 0); + +// Helpers: Maths +IM_MSVC_RUNTIME_CHECKS_OFF +// - Wrapper for standard libs functions. (Note that imgui_demo.cpp does _not_ use them to keep the code easy to copy) +#ifndef IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS +#define ImFabs(X) fabsf(X) +#define ImSqrt(X) sqrtf(X) +#define ImFmod(X, Y) fmodf((X), (Y)) +#define ImCos(X) cosf(X) +#define ImSin(X) sinf(X) +#define ImAcos(X) acosf(X) +#define ImAtan2(Y, X) atan2f((Y), (X)) +#define ImAtof(STR) atof(STR) +//#define ImFloorStd(X) floorf(X) // We use our own, see ImFloor() and ImFloorSigned() +#define ImCeil(X) ceilf(X) +static inline float ImPow(float x, float y) { return powf(x, y); } // DragBehaviorT/SliderBehaviorT uses ImPow with either float/double and need the precision +static inline double ImPow(double x, double y) { return pow(x, y); } +static inline float ImLog(float x) { return logf(x); } // DragBehaviorT/SliderBehaviorT uses ImLog with either float/double and need the precision +static inline double ImLog(double x) { return log(x); } +static inline int ImAbs(int x) { return x < 0 ? -x : x; } +static inline float ImAbs(float x) { return fabsf(x); } +static inline double ImAbs(double x) { return fabs(x); } +static inline float ImSign(float x) { return (x < 0.0f) ? -1.0f : (x > 0.0f) ? 1.0f : 0.0f; } // Sign operator - returns -1, 0 or 1 based on sign of argument +static inline double ImSign(double x) { return (x < 0.0) ? -1.0 : (x > 0.0) ? 1.0 : 0.0; } +#ifdef IMGUI_ENABLE_SSE +static inline float ImRsqrt(float x) { return _mm_cvtss_f32(_mm_rsqrt_ss(_mm_set_ss(x))); } +#else +static inline float ImRsqrt(float x) { return 1.0f / sqrtf(x); } +#endif +static inline double ImRsqrt(double x) { return 1.0 / sqrt(x); } +#endif +// - ImMin/ImMax/ImClamp/ImLerp/ImSwap are used by widgets which support variety of types: signed/unsigned int/long long float/double +// (Exceptionally using templates here but we could also redefine them for those types) +template static inline T ImMin(T lhs, T rhs) { return lhs < rhs ? lhs : rhs; } +template static inline T ImMax(T lhs, T rhs) { return lhs >= rhs ? lhs : rhs; } +template static inline T ImClamp(T v, T mn, T mx) { return (v < mn) ? mn : (v > mx) ? mx : v; } +template static inline T ImLerp(T a, T b, float t) { return (T)(a + (b - a) * t); } +template static inline void ImSwap(T& a, T& b) { T tmp = a; a = b; b = tmp; } +template static inline T ImAddClampOverflow(T a, T b, T mn, T mx) { if (b < 0 && (a < mn - b)) return mn; if (b > 0 && (a > mx - b)) return mx; return a + b; } +template static inline T ImSubClampOverflow(T a, T b, T mn, T mx) { if (b > 0 && (a < mn + b)) return mn; if (b < 0 && (a > mx + b)) return mx; return a - b; } +// - Misc maths helpers +static inline ImVec2 ImMin(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x < rhs.x ? lhs.x : rhs.x, lhs.y < rhs.y ? lhs.y : rhs.y); } +static inline ImVec2 ImMax(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x >= rhs.x ? lhs.x : rhs.x, lhs.y >= rhs.y ? lhs.y : rhs.y); } +static inline ImVec2 ImClamp(const ImVec2& v, const ImVec2& mn, ImVec2 mx) { return ImVec2((v.x < mn.x) ? mn.x : (v.x > mx.x) ? mx.x : v.x, (v.y < mn.y) ? mn.y : (v.y > mx.y) ? mx.y : v.y); } +static inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, float t) { return ImVec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); } +static inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, const ImVec2& t) { return ImVec2(a.x + (b.x - a.x) * t.x, a.y + (b.y - a.y) * t.y); } +static inline ImVec4 ImLerp(const ImVec4& a, const ImVec4& b, float t) { return ImVec4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t); } +static inline float ImSaturate(float f) { return (f < 0.0f) ? 0.0f : (f > 1.0f) ? 1.0f : f; } +static inline float ImLengthSqr(const ImVec2& lhs) { return (lhs.x * lhs.x) + (lhs.y * lhs.y); } +static inline float ImLengthSqr(const ImVec4& lhs) { return (lhs.x * lhs.x) + (lhs.y * lhs.y) + (lhs.z * lhs.z) + (lhs.w * lhs.w); } +static inline float ImInvLength(const ImVec2& lhs, float fail_value) { float d = (lhs.x * lhs.x) + (lhs.y * lhs.y); if (d > 0.0f) return ImRsqrt(d); return fail_value; } +static inline float ImFloor(float f) { return (float)(int)(f); } +static inline float ImFloorSigned(float f) { return (float)((f >= 0 || (float)(int)f == f) ? (int)f : (int)f - 1); } // Decent replacement for floorf() +static inline ImVec2 ImFloor(const ImVec2& v) { return ImVec2((float)(int)(v.x), (float)(int)(v.y)); } +static inline ImVec2 ImFloorSigned(const ImVec2& v) { return ImVec2(ImFloorSigned(v.x), ImFloorSigned(v.y)); } +static inline int ImModPositive(int a, int b) { return (a + b) % b; } +static inline float ImDot(const ImVec2& a, const ImVec2& b) { return a.x * b.x + a.y * b.y; } +static inline ImVec2 ImRotate(const ImVec2& v, float cos_a, float sin_a) { return ImVec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a); } +static inline float ImLinearSweep(float current, float target, float speed) { if (current < target) return ImMin(current + speed, target); if (current > target) return ImMax(current - speed, target); return current; } +static inline ImVec2 ImMul(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } +static inline bool ImIsFloatAboveGuaranteedIntegerPrecision(float f) { return f <= -16777216 || f >= 16777216; } +IM_MSVC_RUNTIME_CHECKS_RESTORE + +// Helpers: Geometry +IMGUI_API ImVec2 ImBezierCubicCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, float t); +IMGUI_API ImVec2 ImBezierCubicClosestPoint(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, int num_segments); // For curves with explicit number of segments +IMGUI_API ImVec2 ImBezierCubicClosestPointCasteljau(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, float tess_tol);// For auto-tessellated curves you can use tess_tol = style.CurveTessellationTol +IMGUI_API ImVec2 ImBezierQuadraticCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float t); +IMGUI_API ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p); +IMGUI_API bool ImTriangleContainsPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); +IMGUI_API ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); +IMGUI_API void ImTriangleBarycentricCoords(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p, float& out_u, float& out_v, float& out_w); +inline float ImTriangleArea(const ImVec2& a, const ImVec2& b, const ImVec2& c) { return ImFabs((a.x * (b.y - c.y)) + (b.x * (c.y - a.y)) + (c.x * (a.y - b.y))) * 0.5f; } +IMGUI_API ImGuiDir ImGetDirQuadrantFromDelta(float dx, float dy); + +// Helper: ImVec1 (1D vector) +// (this odd construct is used to facilitate the transition between 1D and 2D, and the maintenance of some branches/patches) +IM_MSVC_RUNTIME_CHECKS_OFF +struct ImVec1 +{ + float x; + ImVec1() { x = 0.0f; } + ImVec1(float _x) { x = _x; } +}; + +// Helper: ImVec2ih (2D vector, half-size integer, for long-term packed storage) +struct ImVec2ih +{ + short x, y; + ImVec2ih() { x = y = 0; } + ImVec2ih(short _x, short _y) { x = _x; y = _y; } + explicit ImVec2ih(const ImVec2& rhs) { x = (short)rhs.x; y = (short)rhs.y; } +}; + +// Helper: ImRect (2D axis aligned bounding-box) +// NB: we can't rely on ImVec2 math operators being available here! +struct IMGUI_API ImRect +{ + ImVec2 Min; // Upper-left + ImVec2 Max; // Lower-right + + ImRect() : Min(0.0f, 0.0f), Max(0.0f, 0.0f) {} + ImRect(const ImVec2& min, const ImVec2& max) : Min(min), Max(max) {} + ImRect(const ImVec4& v) : Min(v.x, v.y), Max(v.z, v.w) {} + ImRect(float x1, float y1, float x2, float y2) : Min(x1, y1), Max(x2, y2) {} + + ImVec2 GetCenter() const { return ImVec2((Min.x + Max.x) * 0.5f, (Min.y + Max.y) * 0.5f); } + ImVec2 GetSize() const { return ImVec2(Max.x - Min.x, Max.y - Min.y); } + float GetWidth() const { return Max.x - Min.x; } + float GetHeight() const { return Max.y - Min.y; } + float GetArea() const { return (Max.x - Min.x) * (Max.y - Min.y); } + ImVec2 GetTL() const { return Min; } // Top-left + ImVec2 GetTR() const { return ImVec2(Max.x, Min.y); } // Top-right + ImVec2 GetBL() const { return ImVec2(Min.x, Max.y); } // Bottom-left + ImVec2 GetBR() const { return Max; } // Bottom-right + bool Contains(const ImVec2& p) const { return p.x >= Min.x && p.y >= Min.y && p.x < Max.x && p.y < Max.y; } + bool Contains(const ImRect& r) const { return r.Min.x >= Min.x && r.Min.y >= Min.y && r.Max.x <= Max.x && r.Max.y <= Max.y; } + bool Overlaps(const ImRect& r) const { return r.Min.y < Max.y && r.Max.y > Min.y && r.Min.x < Max.x && r.Max.x > Min.x; } + void Add(const ImVec2& p) { if (Min.x > p.x) Min.x = p.x; if (Min.y > p.y) Min.y = p.y; if (Max.x < p.x) Max.x = p.x; if (Max.y < p.y) Max.y = p.y; } + void Add(const ImRect& r) { if (Min.x > r.Min.x) Min.x = r.Min.x; if (Min.y > r.Min.y) Min.y = r.Min.y; if (Max.x < r.Max.x) Max.x = r.Max.x; if (Max.y < r.Max.y) Max.y = r.Max.y; } + void Expand(const float amount) { Min.x -= amount; Min.y -= amount; Max.x += amount; Max.y += amount; } + void Expand(const ImVec2& amount) { Min.x -= amount.x; Min.y -= amount.y; Max.x += amount.x; Max.y += amount.y; } + void Translate(const ImVec2& d) { Min.x += d.x; Min.y += d.y; Max.x += d.x; Max.y += d.y; } + void TranslateX(float dx) { Min.x += dx; Max.x += dx; } + void TranslateY(float dy) { Min.y += dy; Max.y += dy; } + void ClipWith(const ImRect& r) { Min = ImMax(Min, r.Min); Max = ImMin(Max, r.Max); } // Simple version, may lead to an inverted rectangle, which is fine for Contains/Overlaps test but not for display. + void ClipWithFull(const ImRect& r) { Min = ImClamp(Min, r.Min, r.Max); Max = ImClamp(Max, r.Min, r.Max); } // Full version, ensure both points are fully clipped. + void Floor() { Min.x = IM_FLOOR(Min.x); Min.y = IM_FLOOR(Min.y); Max.x = IM_FLOOR(Max.x); Max.y = IM_FLOOR(Max.y); } + bool IsInverted() const { return Min.x > Max.x || Min.y > Max.y; } + ImVec4 ToVec4() const { return ImVec4(Min.x, Min.y, Max.x, Max.y); } +}; +IM_MSVC_RUNTIME_CHECKS_RESTORE + +// Helper: ImBitArray +inline bool ImBitArrayTestBit(const ImU32* arr, int n) { ImU32 mask = (ImU32)1 << (n & 31); return (arr[n >> 5] & mask) != 0; } +inline void ImBitArrayClearBit(ImU32* arr, int n) { ImU32 mask = (ImU32)1 << (n & 31); arr[n >> 5] &= ~mask; } +inline void ImBitArraySetBit(ImU32* arr, int n) { ImU32 mask = (ImU32)1 << (n & 31); arr[n >> 5] |= mask; } +inline void ImBitArraySetBitRange(ImU32* arr, int n, int n2) // Works on range [n..n2) +{ + n2--; + while (n <= n2) + { + int a_mod = (n & 31); + int b_mod = (n2 > (n | 31) ? 31 : (n2 & 31)) + 1; + ImU32 mask = (ImU32)(((ImU64)1 << b_mod) - 1) & ~(ImU32)(((ImU64)1 << a_mod) - 1); + arr[n >> 5] |= mask; + n = (n + 32) & ~31; + } +} + +// Helper: ImBitArray class (wrapper over ImBitArray functions) +// Store 1-bit per value. +template +struct ImBitArray +{ + ImU32 Storage[(BITCOUNT + 31) >> 5]; + ImBitArray() { ClearAllBits(); } + void ClearAllBits() { memset(Storage, 0, sizeof(Storage)); } + void SetAllBits() { memset(Storage, 255, sizeof(Storage)); } + bool TestBit(int n) const { IM_ASSERT(n + OFFSET < BITCOUNT); return ImBitArrayTestBit(Storage, n + OFFSET); } + void SetBit(int n) { IM_ASSERT(n + OFFSET < BITCOUNT); ImBitArraySetBit(Storage, n + OFFSET); } + void ClearBit(int n) { IM_ASSERT(n + OFFSET < BITCOUNT); ImBitArrayClearBit(Storage, n + OFFSET); } + void SetBitRange(int n, int n2) { ImBitArraySetBitRange(Storage, n + OFFSET, n2 + OFFSET); } // Works on range [n..n2) + bool operator[](int n) const { IM_ASSERT(n + OFFSET < BITCOUNT); return ImBitArrayTestBit(Storage, n + OFFSET); } +}; + +// Helper: ImBitVector +// Store 1-bit per value. +struct IMGUI_API ImBitVector +{ + ImVector Storage; + void Create(int sz) { Storage.resize((sz + 31) >> 5); memset(Storage.Data, 0, (size_t)Storage.Size * sizeof(Storage.Data[0])); } + void Clear() { Storage.clear(); } + bool TestBit(int n) const { IM_ASSERT(n < (Storage.Size << 5)); return ImBitArrayTestBit(Storage.Data, n); } + void SetBit(int n) { IM_ASSERT(n < (Storage.Size << 5)); ImBitArraySetBit(Storage.Data, n); } + void ClearBit(int n) { IM_ASSERT(n < (Storage.Size << 5)); ImBitArrayClearBit(Storage.Data, n); } +}; + +// Helper: ImSpan<> +// Pointing to a span of data we don't own. +template +struct ImSpan +{ + T* Data; + T* DataEnd; + + // Constructors, destructor + inline ImSpan() { Data = DataEnd = NULL; } + inline ImSpan(T* data, int size) { Data = data; DataEnd = data + size; } + inline ImSpan(T* data, T* data_end) { Data = data; DataEnd = data_end; } + + inline void set(T* data, int size) { Data = data; DataEnd = data + size; } + inline void set(T* data, T* data_end) { Data = data; DataEnd = data_end; } + inline int size() const { return (int)(ptrdiff_t)(DataEnd - Data); } + inline int size_in_bytes() const { return (int)(ptrdiff_t)(DataEnd - Data) * (int)sizeof(T); } + inline T& operator[](int i) { T* p = Data + i; IM_ASSERT(p >= Data && p < DataEnd); return *p; } + inline const T& operator[](int i) const { const T* p = Data + i; IM_ASSERT(p >= Data && p < DataEnd); return *p; } + + inline T* begin() { return Data; } + inline const T* begin() const { return Data; } + inline T* end() { return DataEnd; } + inline const T* end() const { return DataEnd; } + + // Utilities + inline int index_from_ptr(const T* it) const { IM_ASSERT(it >= Data && it < DataEnd); const ptrdiff_t off = it - Data; return (int)off; } +}; + +// Helper: ImSpanAllocator<> +// Facilitate storing multiple chunks into a single large block (the "arena") +// - Usage: call Reserve() N times, allocate GetArenaSizeInBytes() worth, pass it to SetArenaBasePtr(), call GetSpan() N times to retrieve the aligned ranges. +template +struct ImSpanAllocator +{ + char* BasePtr; + int CurrOff; + int CurrIdx; + int Offsets[CHUNKS]; + int Sizes[CHUNKS]; + + ImSpanAllocator() { memset(this, 0, sizeof(*this)); } + inline void Reserve(int n, size_t sz, int a=4) { IM_ASSERT(n == CurrIdx && n < CHUNKS); CurrOff = IM_MEMALIGN(CurrOff, a); Offsets[n] = CurrOff; Sizes[n] = (int)sz; CurrIdx++; CurrOff += (int)sz; } + inline int GetArenaSizeInBytes() { return CurrOff; } + inline void SetArenaBasePtr(void* base_ptr) { BasePtr = (char*)base_ptr; } + inline void* GetSpanPtrBegin(int n) { IM_ASSERT(n >= 0 && n < CHUNKS && CurrIdx == CHUNKS); return (void*)(BasePtr + Offsets[n]); } + inline void* GetSpanPtrEnd(int n) { IM_ASSERT(n >= 0 && n < CHUNKS && CurrIdx == CHUNKS); return (void*)(BasePtr + Offsets[n] + Sizes[n]); } + template + inline void GetSpan(int n, ImSpan* span) { span->set((T*)GetSpanPtrBegin(n), (T*)GetSpanPtrEnd(n)); } +}; + +// Helper: ImPool<> +// Basic keyed storage for contiguous instances, slow/amortized insertion, O(1) indexable, O(Log N) queries by ID over a dense/hot buffer, +// Honor constructor/destructor. Add/remove invalidate all pointers. Indexes have the same lifetime as the associated object. +typedef int ImPoolIdx; +template +struct ImPool +{ + ImVector Buf; // Contiguous data + ImGuiStorage Map; // ID->Index + ImPoolIdx FreeIdx; // Next free idx to use + ImPoolIdx AliveCount; // Number of active/alive items (for display purpose) + + ImPool() { FreeIdx = AliveCount = 0; } + ~ImPool() { Clear(); } + T* GetByKey(ImGuiID key) { int idx = Map.GetInt(key, -1); return (idx != -1) ? &Buf[idx] : NULL; } + T* GetByIndex(ImPoolIdx n) { return &Buf[n]; } + ImPoolIdx GetIndex(const T* p) const { IM_ASSERT(p >= Buf.Data && p < Buf.Data + Buf.Size); return (ImPoolIdx)(p - Buf.Data); } + T* GetOrAddByKey(ImGuiID key) { int* p_idx = Map.GetIntRef(key, -1); if (*p_idx != -1) return &Buf[*p_idx]; *p_idx = FreeIdx; return Add(); } + bool Contains(const T* p) const { return (p >= Buf.Data && p < Buf.Data + Buf.Size); } + void Clear() { for (int n = 0; n < Map.Data.Size; n++) { int idx = Map.Data[n].val_i; if (idx != -1) Buf[idx].~T(); } Map.Clear(); Buf.clear(); FreeIdx = AliveCount = 0; } + T* Add() { int idx = FreeIdx; if (idx == Buf.Size) { Buf.resize(Buf.Size + 1); FreeIdx++; } else { FreeIdx = *(int*)&Buf[idx]; } IM_PLACEMENT_NEW(&Buf[idx]) T(); AliveCount++; return &Buf[idx]; } + void Remove(ImGuiID key, const T* p) { Remove(key, GetIndex(p)); } + void Remove(ImGuiID key, ImPoolIdx idx) { Buf[idx].~T(); *(int*)&Buf[idx] = FreeIdx; FreeIdx = idx; Map.SetInt(key, -1); AliveCount--; } + void Reserve(int capacity) { Buf.reserve(capacity); Map.Data.reserve(capacity); } + + // To iterate a ImPool: for (int n = 0; n < pool.GetMapSize(); n++) if (T* t = pool.TryGetMapData(n)) { ... } + // Can be avoided if you know .Remove() has never been called on the pool, or AliveCount == GetMapSize() + int GetAliveCount() const { return AliveCount; } // Number of active/alive items in the pool (for display purpose) + int GetBufSize() const { return Buf.Size; } + int GetMapSize() const { return Map.Data.Size; } // It is the map we need iterate to find valid items, since we don't have "alive" storage anywhere + T* TryGetMapData(ImPoolIdx n) { int idx = Map.Data[n].val_i; if (idx == -1) return NULL; return GetByIndex(idx); } +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + int GetSize() { return GetMapSize(); } // For ImPlot: should use GetMapSize() from (IMGUI_VERSION_NUM >= 18304) +#endif +}; + +// Helper: ImChunkStream<> +// Build and iterate a contiguous stream of variable-sized structures. +// This is used by Settings to store persistent data while reducing allocation count. +// We store the chunk size first, and align the final size on 4 bytes boundaries. +// The tedious/zealous amount of casting is to avoid -Wcast-align warnings. +template +struct ImChunkStream +{ + ImVector Buf; + + void clear() { Buf.clear(); } + bool empty() const { return Buf.Size == 0; } + int size() const { return Buf.Size; } + T* alloc_chunk(size_t sz) { size_t HDR_SZ = 4; sz = IM_MEMALIGN(HDR_SZ + sz, 4u); int off = Buf.Size; Buf.resize(off + (int)sz); ((int*)(void*)(Buf.Data + off))[0] = (int)sz; return (T*)(void*)(Buf.Data + off + (int)HDR_SZ); } + T* begin() { size_t HDR_SZ = 4; if (!Buf.Data) return NULL; return (T*)(void*)(Buf.Data + HDR_SZ); } + T* next_chunk(T* p) { size_t HDR_SZ = 4; IM_ASSERT(p >= begin() && p < end()); p = (T*)(void*)((char*)(void*)p + chunk_size(p)); if (p == (T*)(void*)((char*)end() + HDR_SZ)) return (T*)0; IM_ASSERT(p < end()); return p; } + int chunk_size(const T* p) { return ((const int*)p)[-1]; } + T* end() { return (T*)(void*)(Buf.Data + Buf.Size); } + int offset_from_ptr(const T* p) { IM_ASSERT(p >= begin() && p < end()); const ptrdiff_t off = (const char*)p - Buf.Data; return (int)off; } + T* ptr_from_offset(int off) { IM_ASSERT(off >= 4 && off < Buf.Size); return (T*)(void*)(Buf.Data + off); } + void swap(ImChunkStream& rhs) { rhs.Buf.swap(Buf); } + +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImDrawList support +//----------------------------------------------------------------------------- + +// ImDrawList: Helper function to calculate a circle's segment count given its radius and a "maximum error" value. +// Estimation of number of circle segment based on error is derived using method described in https://stackoverflow.com/a/2244088/15194693 +// Number of segments (N) is calculated using equation: +// N = ceil ( pi / acos(1 - error / r) ) where r > 0, error <= r +// Our equation is significantly simpler that one in the post thanks for choosing segment that is +// perpendicular to X axis. Follow steps in the article from this starting condition and you will +// will get this result. +// +// Rendering circles with an odd number of segments, while mathematically correct will produce +// asymmetrical results on the raster grid. Therefore we're rounding N to next even number (7->8, 8->8, 9->10 etc.) +// +#define IM_ROUNDUP_TO_EVEN(_V) ((((_V) + 1) / 2) * 2) +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN 4 +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX 512 +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(_RAD,_MAXERROR) ImClamp(IM_ROUNDUP_TO_EVEN((int)ImCeil(IM_PI / ImAcos(1 - ImMin((_MAXERROR), (_RAD)) / (_RAD)))), IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX) + +// Raw equation from IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC rewritten for 'r' and 'error'. +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(_N,_MAXERROR) ((_MAXERROR) / (1 - ImCos(IM_PI / ImMax((float)(_N), IM_PI)))) +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_ERROR(_N,_RAD) ((1 - ImCos(IM_PI / ImMax((float)(_N), IM_PI))) / (_RAD)) + +// ImDrawList: Lookup table size for adaptive arc drawing, cover full circle. +#ifndef IM_DRAWLIST_ARCFAST_TABLE_SIZE +#define IM_DRAWLIST_ARCFAST_TABLE_SIZE 48 // Number of samples in lookup table. +#endif +#define IM_DRAWLIST_ARCFAST_SAMPLE_MAX IM_DRAWLIST_ARCFAST_TABLE_SIZE // Sample index _PathArcToFastEx() for 360 angle. + +// Data shared between all ImDrawList instances +// You may want to create your own instance of this if you want to use ImDrawList completely without ImGui. In that case, watch out for future changes to this structure. +struct IMGUI_API ImDrawListSharedData +{ + ImVec2 TexUvWhitePixel; // UV of white pixel in the atlas + ImFont* Font; // Current/default font (optional, for simplified AddText overload) + float FontSize; // Current/default font size (optional, for simplified AddText overload) + float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() + float CircleSegmentMaxError; // Number of circle segments to use per pixel of radius for AddCircle() etc + ImVec4 ClipRectFullscreen; // Value for PushClipRectFullscreen() + ImDrawListFlags InitialFlags; // Initial flags at the beginning of the frame (it is possible to alter flags on a per-drawlist basis afterwards) + + // [Internal] Lookup tables + ImVec2 ArcFastVtx[IM_DRAWLIST_ARCFAST_TABLE_SIZE]; // Sample points on the quarter of the circle. + float ArcFastRadiusCutoff; // Cutoff radius after which arc drawing will fallback to slower PathArcTo() + ImU8 CircleSegmentCounts[64]; // Precomputed segment count for given radius before we calculate it dynamically (to avoid calculation overhead) + const ImVec4* TexUvLines; // UV of anti-aliased lines in the atlas + + ImDrawListSharedData(); + void SetCircleTessellationMaxError(float max_error); +}; + +struct ImDrawDataBuilder +{ + ImVector Layers[2]; // Global layers for: regular, tooltip + + void Clear() { for (int n = 0; n < IM_ARRAYSIZE(Layers); n++) Layers[n].resize(0); } + void ClearFreeMemory() { for (int n = 0; n < IM_ARRAYSIZE(Layers); n++) Layers[n].clear(); } + int GetDrawListCount() const { int count = 0; for (int n = 0; n < IM_ARRAYSIZE(Layers); n++) count += Layers[n].Size; return count; } + IMGUI_API void FlattenIntoSingleLayer(); +}; + +//----------------------------------------------------------------------------- +// [SECTION] Widgets support: flags, enums, data structures +//----------------------------------------------------------------------------- + +// Transient per-window flags, reset at the beginning of the frame. For child window, inherited from parent on first Begin(). +// This is going to be exposed in imgui.h when stabilized enough. +enum ImGuiItemFlags_ +{ + ImGuiItemFlags_None = 0, + ImGuiItemFlags_NoTabStop = 1 << 0, // false // Disable keyboard tabbing (FIXME: should merge with _NoNav) + ImGuiItemFlags_ButtonRepeat = 1 << 1, // false // Button() will return true multiple times based on io.KeyRepeatDelay and io.KeyRepeatRate settings. + ImGuiItemFlags_Disabled = 1 << 2, // false // Disable interactions but doesn't affect visuals. See BeginDisabled()/EndDisabled(). See github.com/ocornut/imgui/issues/211 + ImGuiItemFlags_NoNav = 1 << 3, // false // Disable keyboard/gamepad directional navigation (FIXME: should merge with _NoTabStop) + ImGuiItemFlags_NoNavDefaultFocus = 1 << 4, // false // Disable item being a candidate for default focus (e.g. used by title bar items) + ImGuiItemFlags_SelectableDontClosePopup = 1 << 5, // false // Disable MenuItem/Selectable() automatically closing their popup window + ImGuiItemFlags_MixedValue = 1 << 6, // false // [BETA] Represent a mixed/indeterminate value, generally multi-selection where values differ. Currently only supported by Checkbox() (later should support all sorts of widgets) + ImGuiItemFlags_ReadOnly = 1 << 7, // false // [ALPHA] Allow hovering interactions but underlying value is not changed. + ImGuiItemFlags_Inputable = 1 << 8 // false // [WIP] Auto-activate input mode when tab focused. Currently only used and supported by a few items before it becomes a generic feature. +}; + +// Storage for LastItem data +enum ImGuiItemStatusFlags_ +{ + ImGuiItemStatusFlags_None = 0, + ImGuiItemStatusFlags_HoveredRect = 1 << 0, // Mouse position is within item rectangle (does NOT mean that the window is in correct z-order and can be hovered!, this is only one part of the most-common IsItemHovered test) + ImGuiItemStatusFlags_HasDisplayRect = 1 << 1, // g.LastItemData.DisplayRect is valid + ImGuiItemStatusFlags_Edited = 1 << 2, // Value exposed by item was edited in the current frame (should match the bool return value of most widgets) + ImGuiItemStatusFlags_ToggledSelection = 1 << 3, // Set when Selectable(), TreeNode() reports toggling a selection. We can't report "Selected", only state changes, in order to easily handle clipping with less issues. + ImGuiItemStatusFlags_ToggledOpen = 1 << 4, // Set when TreeNode() reports toggling their open state. + ImGuiItemStatusFlags_HasDeactivated = 1 << 5, // Set if the widget/group is able to provide data for the ImGuiItemStatusFlags_Deactivated flag. + ImGuiItemStatusFlags_Deactivated = 1 << 6, // Only valid if ImGuiItemStatusFlags_HasDeactivated is set. + ImGuiItemStatusFlags_HoveredWindow = 1 << 7, // Override the HoveredWindow test to allow cross-window hover testing. + ImGuiItemStatusFlags_FocusedByTabbing = 1 << 8 // Set when the Focusable item just got focused by Tabbing (FIXME: to be removed soon) + +#ifdef IMGUI_ENABLE_TEST_ENGINE + , // [imgui_tests only] + ImGuiItemStatusFlags_Openable = 1 << 20, // + ImGuiItemStatusFlags_Opened = 1 << 21, // + ImGuiItemStatusFlags_Checkable = 1 << 22, // + ImGuiItemStatusFlags_Checked = 1 << 23 // +#endif +}; + +// Extend ImGuiInputTextFlags_ +enum ImGuiInputTextFlagsPrivate_ +{ + // [Internal] + ImGuiInputTextFlags_Multiline = 1 << 26, // For internal use by InputTextMultiline() + ImGuiInputTextFlags_NoMarkEdited = 1 << 27, // For internal use by functions using InputText() before reformatting data + ImGuiInputTextFlags_MergedItem = 1 << 28 // For internal use by TempInputText(), will skip calling ItemAdd(). Require bounding-box to strictly match. +}; + +// Extend ImGuiButtonFlags_ +enum ImGuiButtonFlagsPrivate_ +{ + ImGuiButtonFlags_PressedOnClick = 1 << 4, // return true on click (mouse down event) + ImGuiButtonFlags_PressedOnClickRelease = 1 << 5, // [Default] return true on click + release on same item <-- this is what the majority of Button are using + ImGuiButtonFlags_PressedOnClickReleaseAnywhere = 1 << 6, // return true on click + release even if the release event is not done while hovering the item + ImGuiButtonFlags_PressedOnRelease = 1 << 7, // return true on release (default requires click+release) + ImGuiButtonFlags_PressedOnDoubleClick = 1 << 8, // return true on double-click (default requires click+release) + ImGuiButtonFlags_PressedOnDragDropHold = 1 << 9, // return true when held into while we are drag and dropping another item (used by e.g. tree nodes, collapsing headers) + ImGuiButtonFlags_Repeat = 1 << 10, // hold to repeat + ImGuiButtonFlags_FlattenChildren = 1 << 11, // allow interactions even if a child window is overlapping + ImGuiButtonFlags_AllowItemOverlap = 1 << 12, // require previous frame HoveredId to either match id or be null before being usable, use along with SetItemAllowOverlap() + ImGuiButtonFlags_DontClosePopups = 1 << 13, // disable automatically closing parent popup on press // [UNUSED] + //ImGuiButtonFlags_Disabled = 1 << 14, // disable interactions -> use BeginDisabled() or ImGuiItemFlags_Disabled + ImGuiButtonFlags_AlignTextBaseLine = 1 << 15, // vertically align button to match text baseline - ButtonEx() only // FIXME: Should be removed and handled by SmallButton(), not possible currently because of DC.CursorPosPrevLine + ImGuiButtonFlags_NoKeyModifiers = 1 << 16, // disable mouse interaction if a key modifier is held + ImGuiButtonFlags_NoHoldingActiveId = 1 << 17, // don't set ActiveId while holding the mouse (ImGuiButtonFlags_PressedOnClick only) + ImGuiButtonFlags_NoNavFocus = 1 << 18, // don't override navigation focus when activated + ImGuiButtonFlags_NoHoveredOnFocus = 1 << 19, // don't report as hovered when nav focus is on this item + ImGuiButtonFlags_PressedOnMask_ = ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere | ImGuiButtonFlags_PressedOnRelease | ImGuiButtonFlags_PressedOnDoubleClick | ImGuiButtonFlags_PressedOnDragDropHold, + ImGuiButtonFlags_PressedOnDefault_ = ImGuiButtonFlags_PressedOnClickRelease +}; + +// Extend ImGuiComboFlags_ +enum ImGuiComboFlagsPrivate_ +{ + ImGuiComboFlags_CustomPreview = 1 << 20 // enable BeginComboPreview() +}; + +// Extend ImGuiSliderFlags_ +enum ImGuiSliderFlagsPrivate_ +{ + ImGuiSliderFlags_Vertical = 1 << 20, // Should this slider be orientated vertically? + ImGuiSliderFlags_ReadOnly = 1 << 21 +}; + +// Extend ImGuiSelectableFlags_ +enum ImGuiSelectableFlagsPrivate_ +{ + // NB: need to be in sync with last value of ImGuiSelectableFlags_ + ImGuiSelectableFlags_NoHoldingActiveID = 1 << 20, + ImGuiSelectableFlags_SelectOnNav = 1 << 21, // (WIP) Auto-select when moved into. This is not exposed in public API as to handle multi-select and modifiers we will need user to explicitly control focus scope. May be replaced with a BeginSelection() API. + ImGuiSelectableFlags_SelectOnClick = 1 << 22, // Override button behavior to react on Click (default is Click+Release) + ImGuiSelectableFlags_SelectOnRelease = 1 << 23, // Override button behavior to react on Release (default is Click+Release) + ImGuiSelectableFlags_SpanAvailWidth = 1 << 24, // Span all avail width even if we declared less for layout purpose. FIXME: We may be able to remove this (added in 6251d379, 2bcafc86 for menus) + ImGuiSelectableFlags_DrawHoveredWhenHeld = 1 << 25, // Always show active when held, even is not hovered. This concept could probably be renamed/formalized somehow. + ImGuiSelectableFlags_SetNavIdOnHover = 1 << 26, // Set Nav/Focus ID on mouse hover (used by MenuItem) + ImGuiSelectableFlags_NoPadWithHalfSpacing = 1 << 27 // Disable padding each side with ItemSpacing * 0.5f +}; + +// Extend ImGuiTreeNodeFlags_ +enum ImGuiTreeNodeFlagsPrivate_ +{ + ImGuiTreeNodeFlags_ClipLabelForTrailingButton = 1 << 20 +}; + +enum ImGuiSeparatorFlags_ +{ + ImGuiSeparatorFlags_None = 0, + ImGuiSeparatorFlags_Horizontal = 1 << 0, // Axis default to current layout type, so generally Horizontal unless e.g. in a menu bar + ImGuiSeparatorFlags_Vertical = 1 << 1, + ImGuiSeparatorFlags_SpanAllColumns = 1 << 2 +}; + +enum ImGuiTextFlags_ +{ + ImGuiTextFlags_None = 0, + ImGuiTextFlags_NoWidthForLargeClippedText = 1 << 0 +}; + +enum ImGuiTooltipFlags_ +{ + ImGuiTooltipFlags_None = 0, + ImGuiTooltipFlags_OverridePreviousTooltip = 1 << 0 // Override will clear/ignore previously submitted tooltip (defaults to append) +}; + +// FIXME: this is in development, not exposed/functional as a generic feature yet. +// Horizontal/Vertical enums are fixed to 0/1 so they may be used to index ImVec2 +enum ImGuiLayoutType_ +{ + ImGuiLayoutType_Horizontal = 0, + ImGuiLayoutType_Vertical = 1 +}; + +enum ImGuiLogType +{ + ImGuiLogType_None = 0, + ImGuiLogType_TTY, + ImGuiLogType_File, + ImGuiLogType_Buffer, + ImGuiLogType_Clipboard +}; + +// X/Y enums are fixed to 0/1 so they may be used to index ImVec2 +enum ImGuiAxis +{ + ImGuiAxis_None = -1, + ImGuiAxis_X = 0, + ImGuiAxis_Y = 1 +}; + +enum ImGuiPlotType +{ + ImGuiPlotType_Lines, + ImGuiPlotType_Histogram +}; + +enum ImGuiPopupPositionPolicy +{ + ImGuiPopupPositionPolicy_Default, + ImGuiPopupPositionPolicy_ComboBox, + ImGuiPopupPositionPolicy_Tooltip +}; + +struct ImGuiDataTypeTempStorage +{ + ImU8 Data[8]; // Can fit any data up to ImGuiDataType_COUNT +}; + +// Type information associated to one ImGuiDataType. Retrieve with DataTypeGetInfo(). +struct ImGuiDataTypeInfo +{ + size_t Size; // Size in bytes + const char* Name; // Short descriptive name for the type, for debugging + const char* PrintFmt; // Default printf format for the type + const char* ScanFmt; // Default scanf format for the type +}; + +// Extend ImGuiDataType_ +enum ImGuiDataTypePrivate_ +{ + ImGuiDataType_String = ImGuiDataType_COUNT + 1, + ImGuiDataType_Pointer, + ImGuiDataType_ID +}; + +// Stacked color modifier, backup of modified data so we can restore it +struct ImGuiColorMod +{ + ImGuiCol Col; + ImVec4 BackupValue; +}; + +// Stacked style modifier, backup of modified data so we can restore it. Data type inferred from the variable. +struct ImGuiStyleMod +{ + ImGuiStyleVar VarIdx; + union { int BackupInt[2]; float BackupFloat[2]; }; + ImGuiStyleMod(ImGuiStyleVar idx, int v) { VarIdx = idx; BackupInt[0] = v; } + ImGuiStyleMod(ImGuiStyleVar idx, float v) { VarIdx = idx; BackupFloat[0] = v; } + ImGuiStyleMod(ImGuiStyleVar idx, ImVec2 v) { VarIdx = idx; BackupFloat[0] = v.x; BackupFloat[1] = v.y; } +}; + +// Storage data for BeginComboPreview()/EndComboPreview() +struct IMGUI_API ImGuiComboPreviewData +{ + ImRect PreviewRect; + ImVec2 BackupCursorPos; + ImVec2 BackupCursorMaxPos; + ImVec2 BackupCursorPosPrevLine; + float BackupPrevLineTextBaseOffset; + ImGuiLayoutType BackupLayout; + + ImGuiComboPreviewData() { memset(this, 0, sizeof(*this)); } +}; + +// Stacked storage data for BeginGroup()/EndGroup() +struct IMGUI_API ImGuiGroupData +{ + ImGuiID WindowID; + ImVec2 BackupCursorPos; + ImVec2 BackupCursorMaxPos; + ImVec1 BackupIndent; + ImVec1 BackupGroupOffset; + ImVec2 BackupCurrLineSize; + float BackupCurrLineTextBaseOffset; + ImGuiID BackupActiveIdIsAlive; + bool BackupActiveIdPreviousFrameIsAlive; + bool BackupHoveredIdIsAlive; + bool EmitItem; +}; + +// Simple column measurement, currently used for MenuItem() only.. This is very short-sighted/throw-away code and NOT a generic helper. +struct IMGUI_API ImGuiMenuColumns +{ + ImU32 TotalWidth; + ImU32 NextTotalWidth; + ImU16 Spacing; + ImU16 OffsetIcon; // Always zero for now + ImU16 OffsetLabel; // Offsets are locked in Update() + ImU16 OffsetShortcut; + ImU16 OffsetMark; + ImU16 Widths[4]; // Width of: Icon, Label, Shortcut, Mark (accumulators for current frame) + + ImGuiMenuColumns() { memset(this, 0, sizeof(*this)); } + void Update(float spacing, bool window_reappearing); + float DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark); + void CalcNextTotalWidth(bool update_offsets); +}; + +// Internal state of the currently focused/edited text input box +// For a given item ID, access with ImGui::GetInputTextState() +struct IMGUI_API ImGuiInputTextState +{ + ImGuiID ID; // widget id owning the text state + int CurLenW, CurLenA; // we need to maintain our buffer length in both UTF-8 and wchar format. UTF-8 length is valid even if TextA is not. + ImVector TextW; // edit buffer, we need to persist but can't guarantee the persistence of the user-provided buffer. so we copy into own buffer. + ImVector TextA; // temporary UTF8 buffer for callbacks and other operations. this is not updated in every code-path! size=capacity. + ImVector InitialTextA; // backup of end-user buffer at the time of focus (in UTF-8, unaltered) + bool TextAIsValid; // temporary UTF8 buffer is not initially valid before we make the widget active (until then we pull the data from user argument) + int BufCapacityA; // end-user buffer capacity + float ScrollX; // horizontal scrolling/offset + ImStb::STB_TexteditState Stb; // state for stb_textedit.h + float CursorAnim; // timer for cursor blink, reset on every user action so the cursor reappears immediately + bool CursorFollow; // set when we want scrolling to follow the current cursor position (not always!) + bool SelectedAllMouseLock; // after a double-click to select all, we ignore further mouse drags to update selection + bool Edited; // edited this frame + ImGuiInputTextFlags Flags; // copy of InputText() flags + + ImGuiInputTextState() { memset(this, 0, sizeof(*this)); } + void ClearText() { CurLenW = CurLenA = 0; TextW[0] = 0; TextA[0] = 0; CursorClamp(); } + void ClearFreeMemory() { TextW.clear(); TextA.clear(); InitialTextA.clear(); } + int GetUndoAvailCount() const { return Stb.undostate.undo_point; } + int GetRedoAvailCount() const { return STB_TEXTEDIT_UNDOSTATECOUNT - Stb.undostate.redo_point; } + void OnKeyPressed(int key); // Cannot be inline because we call in code in stb_textedit.h implementation + + // Cursor & Selection + void CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking + void CursorClamp() { Stb.cursor = ImMin(Stb.cursor, CurLenW); Stb.select_start = ImMin(Stb.select_start, CurLenW); Stb.select_end = ImMin(Stb.select_end, CurLenW); } + bool HasSelection() const { return Stb.select_start != Stb.select_end; } + void ClearSelection() { Stb.select_start = Stb.select_end = Stb.cursor; } + int GetCursorPos() const { return Stb.cursor; } + int GetSelectionStart() const { return Stb.select_start; } + int GetSelectionEnd() const { return Stb.select_end; } + void SelectAll() { Stb.select_start = 0; Stb.cursor = Stb.select_end = CurLenW; Stb.has_preferred_x = 0; } +}; + +// Storage for current popup stack +struct ImGuiPopupData +{ + ImGuiID PopupId; // Set on OpenPopup() + ImGuiWindow* Window; // Resolved on BeginPopup() - may stay unresolved if user never calls OpenPopup() + ImGuiWindow* SourceWindow; // Set on OpenPopup() copy of NavWindow at the time of opening the popup + int OpenFrameCount; // Set on OpenPopup() + ImGuiID OpenParentId; // Set on OpenPopup(), we need this to differentiate multiple menu sets from each others (e.g. inside menu bar vs loose menu items) + ImVec2 OpenPopupPos; // Set on OpenPopup(), preferred popup position (typically == OpenMousePos when using mouse) + ImVec2 OpenMousePos; // Set on OpenPopup(), copy of mouse position at the time of opening popup + + ImGuiPopupData() { memset(this, 0, sizeof(*this)); OpenFrameCount = -1; } +}; + +enum ImGuiNextWindowDataFlags_ +{ + ImGuiNextWindowDataFlags_None = 0, + ImGuiNextWindowDataFlags_HasPos = 1 << 0, + ImGuiNextWindowDataFlags_HasSize = 1 << 1, + ImGuiNextWindowDataFlags_HasContentSize = 1 << 2, + ImGuiNextWindowDataFlags_HasCollapsed = 1 << 3, + ImGuiNextWindowDataFlags_HasSizeConstraint = 1 << 4, + ImGuiNextWindowDataFlags_HasFocus = 1 << 5, + ImGuiNextWindowDataFlags_HasBgAlpha = 1 << 6, + ImGuiNextWindowDataFlags_HasScroll = 1 << 7 +}; + +// Storage for SetNexWindow** functions +struct ImGuiNextWindowData +{ + ImGuiNextWindowDataFlags Flags; + ImGuiCond PosCond; + ImGuiCond SizeCond; + ImGuiCond CollapsedCond; + ImVec2 PosVal; + ImVec2 PosPivotVal; + ImVec2 SizeVal; + ImVec2 ContentSizeVal; + ImVec2 ScrollVal; + bool CollapsedVal; + ImRect SizeConstraintRect; + ImGuiSizeCallback SizeCallback; + void* SizeCallbackUserData; + float BgAlphaVal; // Override background alpha + ImVec2 MenuBarOffsetMinVal; // (Always on) This is not exposed publicly, so we don't clear it and it doesn't have a corresponding flag (could we? for consistency?) + + ImGuiNextWindowData() { memset(this, 0, sizeof(*this)); } + inline void ClearFlags() { Flags = ImGuiNextWindowDataFlags_None; } +}; + +enum ImGuiNextItemDataFlags_ +{ + ImGuiNextItemDataFlags_None = 0, + ImGuiNextItemDataFlags_HasWidth = 1 << 0, + ImGuiNextItemDataFlags_HasOpen = 1 << 1 +}; + +struct ImGuiNextItemData +{ + ImGuiNextItemDataFlags Flags; + float Width; // Set by SetNextItemWidth() + ImGuiID FocusScopeId; // Set by SetNextItemMultiSelectData() (!= 0 signify value has been set, so it's an alternate version of HasSelectionData, we don't use Flags for this because they are cleared too early. This is mostly used for debugging) + ImGuiCond OpenCond; + bool OpenVal; // Set by SetNextItemOpen() + + ImGuiNextItemData() { memset(this, 0, sizeof(*this)); } + inline void ClearFlags() { Flags = ImGuiNextItemDataFlags_None; } // Also cleared manually by ItemAdd()! +}; + +// Status storage for the last submitted item +struct ImGuiLastItemData +{ + ImGuiID ID; + ImGuiItemFlags InFlags; // See ImGuiItemFlags_ + ImGuiItemStatusFlags StatusFlags; // See ImGuiItemStatusFlags_ + ImRect Rect; // Full rectangle + ImRect NavRect; // Navigation scoring rectangle (not displayed) + ImRect DisplayRect; // Display rectangle (only if ImGuiItemStatusFlags_HasDisplayRect is set) + + ImGuiLastItemData() { memset(this, 0, sizeof(*this)); } +}; + +struct IMGUI_API ImGuiStackSizes +{ + short SizeOfIDStack; + short SizeOfColorStack; + short SizeOfStyleVarStack; + short SizeOfFontStack; + short SizeOfFocusScopeStack; + short SizeOfGroupStack; + short SizeOfItemFlagsStack; + short SizeOfBeginPopupStack; + short SizeOfDisabledStack; + + ImGuiStackSizes() { memset(this, 0, sizeof(*this)); } + void SetToCurrentState(); + void CompareWithCurrentState(); +}; + +// Data saved for each window pushed into the stack +struct ImGuiWindowStackData +{ + ImGuiWindow* Window; + ImGuiLastItemData ParentLastItemDataBackup; + ImGuiStackSizes StackSizesOnBegin; // Store size of various stacks for asserting +}; + +struct ImGuiShrinkWidthItem +{ + int Index; + float Width; +}; + +struct ImGuiPtrOrIndex +{ + void* Ptr; // Either field can be set, not both. e.g. Dock node tab bars are loose while BeginTabBar() ones are in a pool. + int Index; // Usually index in a main pool. + + ImGuiPtrOrIndex(void* ptr) { Ptr = ptr; Index = -1; } + ImGuiPtrOrIndex(int index) { Ptr = NULL; Index = index; } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Inputs support +//----------------------------------------------------------------------------- + +typedef ImBitArray ImBitArrayForNamedKeys; + +enum ImGuiKeyPrivate_ +{ + ImGuiKey_LegacyNativeKey_BEGIN = 0, + ImGuiKey_LegacyNativeKey_END = 512, + ImGuiKey_Gamepad_BEGIN = ImGuiKey_GamepadStart, + ImGuiKey_Gamepad_END = ImGuiKey_GamepadRStickRight + 1 +}; + +enum ImGuiInputEventType +{ + ImGuiInputEventType_None = 0, + ImGuiInputEventType_MousePos, + ImGuiInputEventType_MouseWheel, + ImGuiInputEventType_MouseButton, + ImGuiInputEventType_Key, + ImGuiInputEventType_Char, + ImGuiInputEventType_Focus, + ImGuiInputEventType_COUNT +}; + +enum ImGuiInputSource +{ + ImGuiInputSource_None = 0, + ImGuiInputSource_Mouse, + ImGuiInputSource_Keyboard, + ImGuiInputSource_Gamepad, + ImGuiInputSource_Clipboard, // Currently only used by InputText() + ImGuiInputSource_Nav, // Stored in g.ActiveIdSource only + ImGuiInputSource_COUNT +}; + +// FIXME: Structures in the union below need to be declared as anonymous unions appears to be an extension? +// Using ImVec2() would fail on Clang 'union member 'MousePos' has a non-trivial default constructor' +struct ImGuiInputEventMousePos { float PosX, PosY; }; +struct ImGuiInputEventMouseWheel { float WheelX, WheelY; }; +struct ImGuiInputEventMouseButton { int Button; bool Down; }; +struct ImGuiInputEventKey { ImGuiKey Key; bool Down; float AnalogValue; }; +struct ImGuiInputEventText { unsigned int Char; }; +struct ImGuiInputEventAppFocused { bool Focused; }; + +struct ImGuiInputEvent +{ + ImGuiInputEventType Type; + ImGuiInputSource Source; + union + { + ImGuiInputEventMousePos MousePos; // if Type == ImGuiInputEventType_MousePos + ImGuiInputEventMouseWheel MouseWheel; // if Type == ImGuiInputEventType_MouseWheel + ImGuiInputEventMouseButton MouseButton; // if Type == ImGuiInputEventType_MouseButton + ImGuiInputEventKey Key; // if Type == ImGuiInputEventType_Key + ImGuiInputEventText Text; // if Type == ImGuiInputEventType_Text + ImGuiInputEventAppFocused AppFocused; // if Type == ImGuiInputEventType_Focus + }; + bool AddedByTestEngine; + + ImGuiInputEvent() { memset(this, 0, sizeof(*this)); } +}; + +// FIXME-NAV: Clarify/expose various repeat delay/rate +enum ImGuiInputReadMode +{ + ImGuiInputReadMode_Down, + ImGuiInputReadMode_Pressed, + ImGuiInputReadMode_Released, + ImGuiInputReadMode_Repeat, + ImGuiInputReadMode_RepeatSlow, + ImGuiInputReadMode_RepeatFast +}; + +//----------------------------------------------------------------------------- +// [SECTION] Clipper support +//----------------------------------------------------------------------------- + +struct ImGuiListClipperRange +{ + int Min; + int Max; + bool PosToIndexConvert; // Begin/End are absolute position (will be converted to indices later) + ImS8 PosToIndexOffsetMin; // Add to Min after converting to indices + ImS8 PosToIndexOffsetMax; // Add to Min after converting to indices + + static ImGuiListClipperRange FromIndices(int min, int max) { ImGuiListClipperRange r = { min, max, false, 0, 0 }; return r; } + static ImGuiListClipperRange FromPositions(float y1, float y2, int off_min, int off_max) { ImGuiListClipperRange r = { (int)y1, (int)y2, true, (ImS8)off_min, (ImS8)off_max }; return r; } +}; + +// Temporary clipper data, buffers shared/reused between instances +struct ImGuiListClipperData +{ + ImGuiListClipper* ListClipper; + float LossynessOffset; + int StepNo; + int ItemsFrozen; + ImVector Ranges; + + ImGuiListClipperData() { memset(this, 0, sizeof(*this)); } + void Reset(ImGuiListClipper* clipper) { ListClipper = clipper; StepNo = ItemsFrozen = 0; Ranges.resize(0); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Navigation support +//----------------------------------------------------------------------------- + +enum ImGuiActivateFlags_ +{ + ImGuiActivateFlags_None = 0, + ImGuiActivateFlags_PreferInput = 1 << 0, // Favor activation that requires keyboard text input (e.g. for Slider/Drag). Default if keyboard is available. + ImGuiActivateFlags_PreferTweak = 1 << 1, // Favor activation for tweaking with arrows or gamepad (e.g. for Slider/Drag). Default if keyboard is not available. + ImGuiActivateFlags_TryToPreserveState = 1 << 2 // Request widget to preserve state if it can (e.g. InputText will try to preserve cursor/selection) +}; + +// Early work-in-progress API for ScrollToItem() +enum ImGuiScrollFlags_ +{ + ImGuiScrollFlags_None = 0, + ImGuiScrollFlags_KeepVisibleEdgeX = 1 << 0, // If item is not visible: scroll as little as possible on X axis to bring item back into view [default for X axis] + ImGuiScrollFlags_KeepVisibleEdgeY = 1 << 1, // If item is not visible: scroll as little as possible on Y axis to bring item back into view [default for Y axis for windows that are already visible] + ImGuiScrollFlags_KeepVisibleCenterX = 1 << 2, // If item is not visible: scroll to make the item centered on X axis [rarely used] + ImGuiScrollFlags_KeepVisibleCenterY = 1 << 3, // If item is not visible: scroll to make the item centered on Y axis + ImGuiScrollFlags_AlwaysCenterX = 1 << 4, // Always center the result item on X axis [rarely used] + ImGuiScrollFlags_AlwaysCenterY = 1 << 5, // Always center the result item on Y axis [default for Y axis for appearing window) + ImGuiScrollFlags_NoScrollParent = 1 << 6, // Disable forwarding scrolling to parent window if required to keep item/rect visible (only scroll window the function was applied to). + ImGuiScrollFlags_MaskX_ = ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleCenterX | ImGuiScrollFlags_AlwaysCenterX, + ImGuiScrollFlags_MaskY_ = ImGuiScrollFlags_KeepVisibleEdgeY | ImGuiScrollFlags_KeepVisibleCenterY | ImGuiScrollFlags_AlwaysCenterY +}; + +enum ImGuiNavHighlightFlags_ +{ + ImGuiNavHighlightFlags_None = 0, + ImGuiNavHighlightFlags_TypeDefault = 1 << 0, + ImGuiNavHighlightFlags_TypeThin = 1 << 1, + ImGuiNavHighlightFlags_AlwaysDraw = 1 << 2, // Draw rectangular highlight if (g.NavId == id) _even_ when using the mouse. + ImGuiNavHighlightFlags_NoRounding = 1 << 3 +}; + +enum ImGuiNavDirSourceFlags_ +{ + ImGuiNavDirSourceFlags_None = 0, + ImGuiNavDirSourceFlags_RawKeyboard = 1 << 0, // Raw keyboard (not pulled from nav), faciliate use of some functions before we can unify nav and keys + ImGuiNavDirSourceFlags_Keyboard = 1 << 1, + ImGuiNavDirSourceFlags_PadDPad = 1 << 2, + ImGuiNavDirSourceFlags_PadLStick = 1 << 3 +}; + +enum ImGuiNavMoveFlags_ +{ + ImGuiNavMoveFlags_None = 0, + ImGuiNavMoveFlags_LoopX = 1 << 0, // On failed request, restart from opposite side + ImGuiNavMoveFlags_LoopY = 1 << 1, + ImGuiNavMoveFlags_WrapX = 1 << 2, // On failed request, request from opposite side one line down (when NavDir==right) or one line up (when NavDir==left) + ImGuiNavMoveFlags_WrapY = 1 << 3, // This is not super useful but provided for completeness + ImGuiNavMoveFlags_AllowCurrentNavId = 1 << 4, // Allow scoring and considering the current NavId as a move target candidate. This is used when the move source is offset (e.g. pressing PageDown actually needs to send a Up move request, if we are pressing PageDown from the bottom-most item we need to stay in place) + ImGuiNavMoveFlags_AlsoScoreVisibleSet = 1 << 5, // Store alternate result in NavMoveResultLocalVisible that only comprise elements that are already fully visible (used by PageUp/PageDown) + ImGuiNavMoveFlags_ScrollToEdgeY = 1 << 6, // Force scrolling to min/max (used by Home/End) // FIXME-NAV: Aim to remove or reword, probably unnecessary + ImGuiNavMoveFlags_Forwarded = 1 << 7, + ImGuiNavMoveFlags_DebugNoResult = 1 << 8, // Dummy scoring for debug purpose, don't apply result + ImGuiNavMoveFlags_FocusApi = 1 << 9, + ImGuiNavMoveFlags_Tabbing = 1 << 10, // == Focus + Activate if item is Inputable + DontChangeNavHighlight + ImGuiNavMoveFlags_Activate = 1 << 11, + ImGuiNavMoveFlags_DontSetNavHighlight = 1 << 12 // Do not alter the visible state of keyboard vs mouse nav highlight +}; + +enum ImGuiNavLayer +{ + ImGuiNavLayer_Main = 0, // Main scrolling layer + ImGuiNavLayer_Menu = 1, // Menu layer (access with Alt/ImGuiNavInput_Menu) + ImGuiNavLayer_COUNT +}; + +struct ImGuiNavItemData +{ + ImGuiWindow* Window; // Init,Move // Best candidate window (result->ItemWindow->RootWindowForNav == request->Window) + ImGuiID ID; // Init,Move // Best candidate item ID + ImGuiID FocusScopeId; // Init,Move // Best candidate focus scope ID + ImRect RectRel; // Init,Move // Best candidate bounding box in window relative space + ImGuiItemFlags InFlags; // ????,Move // Best candidate item flags + float DistBox; // Move // Best candidate box distance to current NavId + float DistCenter; // Move // Best candidate center distance to current NavId + float DistAxial; // Move // Best candidate axial distance to current NavId + + ImGuiNavItemData() { Clear(); } + void Clear() { Window = NULL; ID = FocusScopeId = 0; InFlags = 0; DistBox = DistCenter = DistAxial = FLT_MAX; } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Columns support +//----------------------------------------------------------------------------- + +// Flags for internal's BeginColumns(). Prefix using BeginTable() nowadays! +enum ImGuiOldColumnFlags_ +{ + ImGuiOldColumnFlags_None = 0, + ImGuiOldColumnFlags_NoBorder = 1 << 0, // Disable column dividers + ImGuiOldColumnFlags_NoResize = 1 << 1, // Disable resizing columns when clicking on the dividers + ImGuiOldColumnFlags_NoPreserveWidths = 1 << 2, // Disable column width preservation when adjusting columns + ImGuiOldColumnFlags_NoForceWithinWindow = 1 << 3, // Disable forcing columns to fit within window + ImGuiOldColumnFlags_GrowParentContentsSize = 1 << 4 // (WIP) Restore pre-1.51 behavior of extending the parent window contents size but _without affecting the columns width at all_. Will eventually remove. + + // Obsolete names (will be removed) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + , ImGuiColumnsFlags_None = ImGuiOldColumnFlags_None, + ImGuiColumnsFlags_NoBorder = ImGuiOldColumnFlags_NoBorder, + ImGuiColumnsFlags_NoResize = ImGuiOldColumnFlags_NoResize, + ImGuiColumnsFlags_NoPreserveWidths = ImGuiOldColumnFlags_NoPreserveWidths, + ImGuiColumnsFlags_NoForceWithinWindow = ImGuiOldColumnFlags_NoForceWithinWindow, + ImGuiColumnsFlags_GrowParentContentsSize = ImGuiOldColumnFlags_GrowParentContentsSize +#endif +}; + +struct ImGuiOldColumnData +{ + float OffsetNorm; // Column start offset, normalized 0.0 (far left) -> 1.0 (far right) + float OffsetNormBeforeResize; + ImGuiOldColumnFlags Flags; // Not exposed + ImRect ClipRect; + + ImGuiOldColumnData() { memset(this, 0, sizeof(*this)); } +}; + +struct ImGuiOldColumns +{ + ImGuiID ID; + ImGuiOldColumnFlags Flags; + bool IsFirstFrame; + bool IsBeingResized; + int Current; + int Count; + float OffMinX, OffMaxX; // Offsets from HostWorkRect.Min.x + float LineMinY, LineMaxY; + float HostCursorPosY; // Backup of CursorPos at the time of BeginColumns() + float HostCursorMaxPosX; // Backup of CursorMaxPos at the time of BeginColumns() + ImRect HostInitialClipRect; // Backup of ClipRect at the time of BeginColumns() + ImRect HostBackupClipRect; // Backup of ClipRect during PushColumnsBackground()/PopColumnsBackground() + ImRect HostBackupParentWorkRect;//Backup of WorkRect at the time of BeginColumns() + ImVector Columns; + ImDrawListSplitter Splitter; + + ImGuiOldColumns() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Multi-select support +//----------------------------------------------------------------------------- + +#ifdef IMGUI_HAS_MULTI_SELECT +// +#endif // #ifdef IMGUI_HAS_MULTI_SELECT + +//----------------------------------------------------------------------------- +// [SECTION] Docking support +//----------------------------------------------------------------------------- + +#ifdef IMGUI_HAS_DOCK +// +#endif // #ifdef IMGUI_HAS_DOCK + +//----------------------------------------------------------------------------- +// [SECTION] Viewport support +//----------------------------------------------------------------------------- + +// ImGuiViewport Private/Internals fields (cardinal sin: we are using inheritance!) +// Every instance of ImGuiViewport is in fact a ImGuiViewportP. +struct ImGuiViewportP : public ImGuiViewport +{ + int DrawListsLastFrame[2]; // Last frame number the background (0) and foreground (1) draw lists were used + ImDrawList* DrawLists[2]; // Convenience background (0) and foreground (1) draw lists. We use them to draw software mouser cursor when io.MouseDrawCursor is set and to draw most debug overlays. + ImDrawData DrawDataP; + ImDrawDataBuilder DrawDataBuilder; + + ImVec2 WorkOffsetMin; // Work Area: Offset from Pos to top-left corner of Work Area. Generally (0,0) or (0,+main_menu_bar_height). Work Area is Full Area but without menu-bars/status-bars (so WorkArea always fit inside Pos/Size!) + ImVec2 WorkOffsetMax; // Work Area: Offset from Pos+Size to bottom-right corner of Work Area. Generally (0,0) or (0,-status_bar_height). + ImVec2 BuildWorkOffsetMin; // Work Area: Offset being built during current frame. Generally >= 0.0f. + ImVec2 BuildWorkOffsetMax; // Work Area: Offset being built during current frame. Generally <= 0.0f. + + ImGuiViewportP() { DrawListsLastFrame[0] = DrawListsLastFrame[1] = -1; DrawLists[0] = DrawLists[1] = NULL; } + ~ImGuiViewportP() { if (DrawLists[0]) IM_DELETE(DrawLists[0]); if (DrawLists[1]) IM_DELETE(DrawLists[1]); } + + // Calculate work rect pos/size given a set of offset (we have 1 pair of offset for rect locked from last frame data, and 1 pair for currently building rect) + ImVec2 CalcWorkRectPos(const ImVec2& off_min) const { return ImVec2(Pos.x + off_min.x, Pos.y + off_min.y); } + ImVec2 CalcWorkRectSize(const ImVec2& off_min, const ImVec2& off_max) const { return ImVec2(ImMax(0.0f, Size.x - off_min.x + off_max.x), ImMax(0.0f, Size.y - off_min.y + off_max.y)); } + void UpdateWorkRect() { WorkPos = CalcWorkRectPos(WorkOffsetMin); WorkSize = CalcWorkRectSize(WorkOffsetMin, WorkOffsetMax); } // Update public fields + + // Helpers to retrieve ImRect (we don't need to store BuildWorkRect as every access tend to change it, hence the code asymmetry) + ImRect GetMainRect() const { return ImRect(Pos.x, Pos.y, Pos.x + Size.x, Pos.y + Size.y); } + ImRect GetWorkRect() const { return ImRect(WorkPos.x, WorkPos.y, WorkPos.x + WorkSize.x, WorkPos.y + WorkSize.y); } + ImRect GetBuildWorkRect() const { ImVec2 pos = CalcWorkRectPos(BuildWorkOffsetMin); ImVec2 size = CalcWorkRectSize(BuildWorkOffsetMin, BuildWorkOffsetMax); return ImRect(pos.x, pos.y, pos.x + size.x, pos.y + size.y); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Settings support +//----------------------------------------------------------------------------- + +// Windows data saved in imgui.ini file +// Because we never destroy or rename ImGuiWindowSettings, we can store the names in a separate buffer easily. +// (this is designed to be stored in a ImChunkStream buffer, with the variable-length Name following our structure) +struct ImGuiWindowSettings +{ + ImGuiID ID; + ImVec2ih Pos; + ImVec2ih Size; + bool Collapsed; + bool WantApply; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) + + ImGuiWindowSettings() { memset(this, 0, sizeof(*this)); } + char* GetName() { return (char*)(this + 1); } +}; + +struct ImGuiSettingsHandler +{ + const char* TypeName; // Short description stored in .ini file. Disallowed characters: '[' ']' + ImGuiID TypeHash; // == ImHashStr(TypeName) + void (*ClearAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); // Clear all settings data + void (*ReadInitFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); // Read: Called before reading (in registration order) + void* (*ReadOpenFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, const char* name); // Read: Called when entering into a new ini entry e.g. "[Window][Name]" + void (*ReadLineFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, void* entry, const char* line); // Read: Called for every line of text within an ini entry + void (*ApplyAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); // Read: Called after reading (in registration order) + void (*WriteAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* out_buf); // Write: Output every entries into 'out_buf' + void* UserData; + + ImGuiSettingsHandler() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Metrics, Debug Tools +//----------------------------------------------------------------------------- + +struct ImGuiMetricsConfig +{ + bool ShowStackTool; + bool ShowWindowsRects; + bool ShowWindowsBeginOrder; + bool ShowTablesRects; + bool ShowDrawCmdMesh; + bool ShowDrawCmdBoundingBoxes; + int ShowWindowsRectsType; + int ShowTablesRectsType; + + ImGuiMetricsConfig() + { + ShowStackTool = false; + ShowWindowsRects = false; + ShowWindowsBeginOrder = false; + ShowTablesRects = false; + ShowDrawCmdMesh = true; + ShowDrawCmdBoundingBoxes = true; + ShowWindowsRectsType = -1; + ShowTablesRectsType = -1; + } +}; + +struct ImGuiStackLevelInfo +{ + ImGuiID ID; + ImS8 QueryFrameCount; // >= 1: Query in progress + bool QuerySuccess; // Obtained result from DebugHookIdInfo() + char Desc[58]; // Arbitrarily sized buffer to hold a result (FIXME: could replace Results[] with a chunk stream?) + + ImGuiStackLevelInfo() { memset(this, 0, sizeof(*this)); } +}; + +// State for Stack tool queries +struct ImGuiStackTool +{ + int LastActiveFrame; + int StackLevel; // -1: query stack and resize Results, >= 0: individual stack level + ImGuiID QueryId; // ID to query details for + ImVector Results; + + ImGuiStackTool() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Generic context hooks +//----------------------------------------------------------------------------- + +typedef void (*ImGuiContextHookCallback)(ImGuiContext* ctx, ImGuiContextHook* hook); +enum ImGuiContextHookType { ImGuiContextHookType_NewFramePre, ImGuiContextHookType_NewFramePost, ImGuiContextHookType_EndFramePre, ImGuiContextHookType_EndFramePost, ImGuiContextHookType_RenderPre, ImGuiContextHookType_RenderPost, ImGuiContextHookType_Shutdown, ImGuiContextHookType_PendingRemoval_ }; + +struct ImGuiContextHook +{ + ImGuiID HookId; // A unique ID assigned by AddContextHook() + ImGuiContextHookType Type; + ImGuiID Owner; + ImGuiContextHookCallback Callback; + void* UserData; + + ImGuiContextHook() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiContext (main imgui context) +//----------------------------------------------------------------------------- + +struct ImGuiContext +{ + bool Initialized; + bool FontAtlasOwnedByContext; // IO.Fonts-> is owned by the ImGuiContext and will be destructed along with it. + ImGuiIO IO; + ImVector InputEventsQueue; // Input events which will be tricked/written into IO structure. + ImVector InputEventsTrail; // Past input events processed in NewFrame(). This is to allow domain-specific application to access e.g mouse/pen trail. + ImGuiStyle Style; + ImFont* Font; // (Shortcut) == FontStack.empty() ? IO.Font : FontStack.back() + float FontSize; // (Shortcut) == FontBaseSize * g.CurrentWindow->FontWindowScale == window->FontSize(). Text height for current window. + float FontBaseSize; // (Shortcut) == IO.FontGlobalScale * Font->Scale * Font->FontSize. Base text height. + ImDrawListSharedData DrawListSharedData; + double Time; + int FrameCount; + int FrameCountEnded; + int FrameCountRendered; + bool WithinFrameScope; // Set by NewFrame(), cleared by EndFrame() + bool WithinFrameScopeWithImplicitWindow; // Set by NewFrame(), cleared by EndFrame() when the implicit debug window has been pushed + bool WithinEndChild; // Set within EndChild() + bool GcCompactAll; // Request full GC + bool TestEngineHookItems; // Will call test engine hooks: ImGuiTestEngineHook_ItemAdd(), ImGuiTestEngineHook_ItemInfo(), ImGuiTestEngineHook_Log() + void* TestEngine; // Test engine user data + + // Windows state + ImVector Windows; // Windows, sorted in display order, back to front + ImVector WindowsFocusOrder; // Root windows, sorted in focus order, back to front. + ImVector WindowsTempSortBuffer; // Temporary buffer used in EndFrame() to reorder windows so parents are kept before their child + ImVector CurrentWindowStack; + ImGuiStorage WindowsById; // Map window's ImGuiID to ImGuiWindow* + int WindowsActiveCount; // Number of unique windows submitted by frame + ImVec2 WindowsHoverPadding; // Padding around resizable windows for which hovering on counts as hovering the window == ImMax(style.TouchExtraPadding, WINDOWS_HOVER_PADDING) + ImGuiWindow* CurrentWindow; // Window being drawn into + ImGuiWindow* HoveredWindow; // Window the mouse is hovering. Will typically catch mouse inputs. + ImGuiWindow* HoveredWindowUnderMovingWindow; // Hovered window ignoring MovingWindow. Only set if MovingWindow is set. + ImGuiWindow* MovingWindow; // Track the window we clicked on (in order to preserve focus). The actual window that is moved is generally MovingWindow->RootWindow. + ImGuiWindow* WheelingWindow; // Track the window we started mouse-wheeling on. Until a timer elapse or mouse has moved, generally keep scrolling the same window even if during the course of scrolling the mouse ends up hovering a child window. + ImVec2 WheelingWindowRefMousePos; + float WheelingWindowTimer; + + // Item/widgets state and tracking information + ImGuiID DebugHookIdInfo; // Will call core hooks: DebugHookIdInfo() from GetID functions, used by Stack Tool [next HoveredId/ActiveId to not pull in an extra cache-line] + ImGuiID HoveredId; // Hovered widget, filled during the frame + ImGuiID HoveredIdPreviousFrame; + bool HoveredIdAllowOverlap; + bool HoveredIdUsingMouseWheel; // Hovered widget will use mouse wheel. Blocks scrolling the underlying window. + bool HoveredIdPreviousFrameUsingMouseWheel; + bool HoveredIdDisabled; // At least one widget passed the rect test, but has been discarded by disabled flag or popup inhibit. May be true even if HoveredId == 0. + float HoveredIdTimer; // Measure contiguous hovering time + float HoveredIdNotActiveTimer; // Measure contiguous hovering time where the item has not been active + ImGuiID ActiveId; // Active widget + ImGuiID ActiveIdIsAlive; // Active widget has been seen this frame (we can't use a bool as the ActiveId may change within the frame) + float ActiveIdTimer; + bool ActiveIdIsJustActivated; // Set at the time of activation for one frame + bool ActiveIdAllowOverlap; // Active widget allows another widget to steal active id (generally for overlapping widgets, but not always) + bool ActiveIdNoClearOnFocusLoss; // Disable losing active id if the active id window gets unfocused. + bool ActiveIdHasBeenPressedBefore; // Track whether the active id led to a press (this is to allow changing between PressOnClick and PressOnRelease without pressing twice). Used by range_select branch. + bool ActiveIdHasBeenEditedBefore; // Was the value associated to the widget Edited over the course of the Active state. + bool ActiveIdHasBeenEditedThisFrame; + bool ActiveIdUsingMouseWheel; // Active widget will want to read mouse wheel. Blocks scrolling the underlying window. + ImU32 ActiveIdUsingNavDirMask; // Active widget will want to read those nav move requests (e.g. can activate a button and move away from it) + ImU32 ActiveIdUsingNavInputMask; // Active widget will want to read those nav inputs. + ImBitArrayForNamedKeys ActiveIdUsingKeyInputMask; // Active widget will want to read those key inputs. When we grow the ImGuiKey enum we'll need to either to order the enum to make useful keys come first, either redesign this into e.g. a small array. + ImVec2 ActiveIdClickOffset; // Clicked offset from upper-left corner, if applicable (currently only set by ButtonBehavior) + ImGuiWindow* ActiveIdWindow; + ImGuiInputSource ActiveIdSource; // Activating with mouse or nav (gamepad/keyboard) + int ActiveIdMouseButton; + ImGuiID ActiveIdPreviousFrame; + bool ActiveIdPreviousFrameIsAlive; + bool ActiveIdPreviousFrameHasBeenEditedBefore; + ImGuiWindow* ActiveIdPreviousFrameWindow; + ImGuiID LastActiveId; // Store the last non-zero ActiveId, useful for animation. + float LastActiveIdTimer; // Store the last non-zero ActiveId timer since the beginning of activation, useful for animation. + + // Next window/item data + ImGuiItemFlags CurrentItemFlags; // == g.ItemFlagsStack.back() + ImGuiNextItemData NextItemData; // Storage for SetNextItem** functions + ImGuiLastItemData LastItemData; // Storage for last submitted item (setup by ItemAdd) + ImGuiNextWindowData NextWindowData; // Storage for SetNextWindow** functions + + // Shared stacks + ImVector ColorStack; // Stack for PushStyleColor()/PopStyleColor() - inherited by Begin() + ImVector StyleVarStack; // Stack for PushStyleVar()/PopStyleVar() - inherited by Begin() + ImVector FontStack; // Stack for PushFont()/PopFont() - inherited by Begin() + ImVector FocusScopeStack; // Stack for PushFocusScope()/PopFocusScope() - not inherited by Begin(), unless child window + ImVectorItemFlagsStack; // Stack for PushItemFlag()/PopItemFlag() - inherited by Begin() + ImVectorGroupStack; // Stack for BeginGroup()/EndGroup() - not inherited by Begin() + ImVectorOpenPopupStack; // Which popups are open (persistent) + ImVectorBeginPopupStack; // Which level of BeginPopup() we are in (reset every frame) + int BeginMenuCount; + + // Viewports + ImVector Viewports; // Active viewports (Size==1 in 'master' branch). Each viewports hold their copy of ImDrawData. + + // Gamepad/keyboard Navigation + ImGuiWindow* NavWindow; // Focused window for navigation. Could be called 'FocusWindow' + ImGuiID NavId; // Focused item for navigation + ImGuiID NavFocusScopeId; // Identify a selection scope (selection code often wants to "clear other items" when landing on an item of the selection set) + ImGuiID NavActivateId; // ~~ (g.ActiveId == 0) && IsNavInputPressed(ImGuiNavInput_Activate) ? NavId : 0, also set when calling ActivateItem() + ImGuiID NavActivateDownId; // ~~ IsNavInputDown(ImGuiNavInput_Activate) ? NavId : 0 + ImGuiID NavActivatePressedId; // ~~ IsNavInputPressed(ImGuiNavInput_Activate) ? NavId : 0 + ImGuiID NavActivateInputId; // ~~ IsNavInputPressed(ImGuiNavInput_Input) ? NavId : 0; ImGuiActivateFlags_PreferInput will be set and NavActivateId will be 0. + ImGuiActivateFlags NavActivateFlags; + ImGuiID NavJustMovedToId; // Just navigated to this id (result of a successfully MoveRequest). + ImGuiID NavJustMovedToFocusScopeId; // Just navigated to this focus scope id (result of a successfully MoveRequest). + ImGuiKeyModFlags NavJustMovedToKeyMods; + ImGuiID NavNextActivateId; // Set by ActivateItem(), queued until next frame. + ImGuiActivateFlags NavNextActivateFlags; + ImGuiInputSource NavInputSource; // Keyboard or Gamepad mode? THIS WILL ONLY BE None or NavGamepad or NavKeyboard. + ImGuiNavLayer NavLayer; // Layer we are navigating on. For now the system is hard-coded for 0=main contents and 1=menu/title bar, may expose layers later. + bool NavIdIsAlive; // Nav widget has been seen this frame ~~ NavRectRel is valid + bool NavMousePosDirty; // When set we will update mouse position if (io.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos) if set (NB: this not enabled by default) + bool NavDisableHighlight; // When user starts using mouse, we hide gamepad/keyboard highlight (NB: but they are still available, which is why NavDisableHighlight isn't always != NavDisableMouseHover) + bool NavDisableMouseHover; // When user starts using gamepad/keyboard, we hide mouse hovering highlight until mouse is touched again. + + // Navigation: Init & Move Requests + bool NavAnyRequest; // ~~ NavMoveRequest || NavInitRequest this is to perform early out in ItemAdd() + bool NavInitRequest; // Init request for appearing window to select first item + bool NavInitRequestFromMove; + ImGuiID NavInitResultId; // Init request result (first item of the window, or one for which SetItemDefaultFocus() was called) + ImRect NavInitResultRectRel; // Init request result rectangle (relative to parent window) + bool NavMoveSubmitted; // Move request submitted, will process result on next NewFrame() + bool NavMoveScoringItems; // Move request submitted, still scoring incoming items + bool NavMoveForwardToNextFrame; + ImGuiNavMoveFlags NavMoveFlags; + ImGuiScrollFlags NavMoveScrollFlags; + ImGuiKeyModFlags NavMoveKeyMods; + ImGuiDir NavMoveDir; // Direction of the move request (left/right/up/down) + ImGuiDir NavMoveDirForDebug; + ImGuiDir NavMoveClipDir; // FIXME-NAV: Describe the purpose of this better. Might want to rename? + ImRect NavScoringRect; // Rectangle used for scoring, in screen space. Based of window->NavRectRel[], modified for directional navigation scoring. + ImRect NavScoringNoClipRect; // Some nav operations (such as PageUp/PageDown) enforce a region which clipper will attempt to always keep submitted + int NavScoringDebugCount; // Metrics for debugging + int NavTabbingDir; // Generally -1 or +1, 0 when tabbing without a nav id + int NavTabbingCounter; // >0 when counting items for tabbing + ImGuiNavItemData NavMoveResultLocal; // Best move request candidate within NavWindow + ImGuiNavItemData NavMoveResultLocalVisible; // Best move request candidate within NavWindow that are mostly visible (when using ImGuiNavMoveFlags_AlsoScoreVisibleSet flag) + ImGuiNavItemData NavMoveResultOther; // Best move request candidate within NavWindow's flattened hierarchy (when using ImGuiWindowFlags_NavFlattened flag) + ImGuiNavItemData NavTabbingResultFirst; // First tabbing request candidate within NavWindow and flattened hierarchy + + // Navigation: Windowing (CTRL+TAB for list, or Menu button + keys or directional pads to move/resize) + ImGuiWindow* NavWindowingTarget; // Target window when doing CTRL+Tab (or Pad Menu + FocusPrev/Next), this window is temporarily displayed top-most! + ImGuiWindow* NavWindowingTargetAnim; // Record of last valid NavWindowingTarget until DimBgRatio and NavWindowingHighlightAlpha becomes 0.0f, so the fade-out can stay on it. + ImGuiWindow* NavWindowingListWindow; // Internal window actually listing the CTRL+Tab contents + float NavWindowingTimer; + float NavWindowingHighlightAlpha; + bool NavWindowingToggleLayer; + + // Render + float DimBgRatio; // 0.0..1.0 animation when fading in a dimming background (for modal window and CTRL+TAB list) + ImGuiMouseCursor MouseCursor; + + // Drag and Drop + bool DragDropActive; + bool DragDropWithinSource; // Set when within a BeginDragDropXXX/EndDragDropXXX block for a drag source. + bool DragDropWithinTarget; // Set when within a BeginDragDropXXX/EndDragDropXXX block for a drag target. + ImGuiDragDropFlags DragDropSourceFlags; + int DragDropSourceFrameCount; + int DragDropMouseButton; + ImGuiPayload DragDropPayload; + ImRect DragDropTargetRect; // Store rectangle of current target candidate (we favor small targets when overlapping) + ImGuiID DragDropTargetId; + ImGuiDragDropFlags DragDropAcceptFlags; + float DragDropAcceptIdCurrRectSurface; // Target item surface (we resolve overlapping targets by prioritizing the smaller surface) + ImGuiID DragDropAcceptIdCurr; // Target item id (set at the time of accepting the payload) + ImGuiID DragDropAcceptIdPrev; // Target item id from previous frame (we need to store this to allow for overlapping drag and drop targets) + int DragDropAcceptFrameCount; // Last time a target expressed a desire to accept the source + ImGuiID DragDropHoldJustPressedId; // Set when holding a payload just made ButtonBehavior() return a press. + ImVector DragDropPayloadBufHeap; // We don't expose the ImVector<> directly, ImGuiPayload only holds pointer+size + unsigned char DragDropPayloadBufLocal[16]; // Local buffer for small payloads + + // Clipper + int ClipperTempDataStacked; + ImVector ClipperTempData; + + // Table + ImGuiTable* CurrentTable; + int TablesTempDataStacked; // Temporary table data size (because we leave previous instances undestructed, we generally don't use TablesTempData.Size) + ImVector TablesTempData; // Temporary table data (buffers reused/shared across instances, support nesting) + ImPool Tables; // Persistent table data + ImVector TablesLastTimeActive; // Last used timestamp of each tables (SOA, for efficient GC) + ImVector DrawChannelsTempMergeBuffer; + + // Tab bars + ImGuiTabBar* CurrentTabBar; + ImPool TabBars; + ImVector CurrentTabBarStack; + ImVector ShrinkWidthBuffer; + + // Widget state + ImVec2 MouseLastValidPos; + ImGuiInputTextState InputTextState; + ImFont InputTextPasswordFont; + ImGuiID TempInputId; // Temporary text input when CTRL+clicking on a slider, etc. + ImGuiColorEditFlags ColorEditOptions; // Store user options for color edit widgets + float ColorEditLastHue; // Backup of last Hue associated to LastColor, so we can restore Hue in lossy RGB<>HSV round trips + float ColorEditLastSat; // Backup of last Saturation associated to LastColor, so we can restore Saturation in lossy RGB<>HSV round trips + ImU32 ColorEditLastColor; // RGB value with alpha set to 0. + ImVec4 ColorPickerRef; // Initial/reference color at the time of opening the color picker. + ImGuiComboPreviewData ComboPreviewData; + float SliderCurrentAccum; // Accumulated slider delta when using navigation controls. + bool SliderCurrentAccumDirty; // Has the accumulated slider delta changed since last time we tried to apply it? + bool DragCurrentAccumDirty; + float DragCurrentAccum; // Accumulator for dragging modification. Always high-precision, not rounded by end-user precision settings + float DragSpeedDefaultRatio; // If speed == 0.0f, uses (max-min) * DragSpeedDefaultRatio + float ScrollbarClickDeltaToGrabCenter; // Distance between mouse and center of grab box, normalized in parent space. Use storage? + float DisabledAlphaBackup; // Backup for style.Alpha for BeginDisabled() + short DisabledStackSize; + short TooltipOverrideCount; + float TooltipSlowDelay; // Time before slow tooltips appears (FIXME: This is temporary until we merge in tooltip timer+priority work) + ImVector ClipboardHandlerData; // If no custom clipboard handler is defined + ImVector MenusIdSubmittedThisFrame; // A list of menu IDs that were rendered at least once + + // Platform support + ImGuiPlatformImeData PlatformImeData; // Data updated by current frame + ImGuiPlatformImeData PlatformImeDataPrev; // Previous frame data (when changing we will call io.SetPlatformImeDataFn + char PlatformLocaleDecimalPoint; // '.' or *localeconv()->decimal_point + + // Settings + bool SettingsLoaded; + float SettingsDirtyTimer; // Save .ini Settings to memory when time reaches zero + ImGuiTextBuffer SettingsIniData; // In memory .ini settings + ImVector SettingsHandlers; // List of .ini settings handlers + ImChunkStream SettingsWindows; // ImGuiWindow .ini settings entries + ImChunkStream SettingsTables; // ImGuiTable .ini settings entries + ImVector Hooks; // Hooks for extensions (e.g. test engine) + ImGuiID HookIdNext; // Next available HookId + + // Capture/Logging + bool LogEnabled; // Currently capturing + ImGuiLogType LogType; // Capture target + ImFileHandle LogFile; // If != NULL log to stdout/ file + ImGuiTextBuffer LogBuffer; // Accumulation buffer when log to clipboard. This is pointer so our GImGui static constructor doesn't call heap allocators. + const char* LogNextPrefix; + const char* LogNextSuffix; + float LogLinePosY; + bool LogLineFirstItem; + int LogDepthRef; + int LogDepthToExpand; + int LogDepthToExpandDefault; // Default/stored value for LogDepthMaxExpand if not specified in the LogXXX function call. + + // Debug Tools + bool DebugItemPickerActive; // Item picker is active (started with DebugStartItemPicker()) + ImGuiID DebugItemPickerBreakId; // Will call IM_DEBUG_BREAK() when encountering this ID + ImGuiMetricsConfig DebugMetricsConfig; + ImGuiStackTool DebugStackTool; + + // Misc + float FramerateSecPerFrame[120]; // Calculate estimate of framerate for user over the last 2 seconds. + int FramerateSecPerFrameIdx; + int FramerateSecPerFrameCount; + float FramerateSecPerFrameAccum; + int WantCaptureMouseNextFrame; // Explicit capture via CaptureKeyboardFromApp()/CaptureMouseFromApp() sets those flags + int WantCaptureKeyboardNextFrame; + int WantTextInputNextFrame; + char TempBuffer[1024 * 3 + 1]; // Temporary text buffer + + ImGuiContext(ImFontAtlas* shared_font_atlas) + { + Initialized = false; + FontAtlasOwnedByContext = shared_font_atlas ? false : true; + Font = NULL; + FontSize = FontBaseSize = 0.0f; + IO.Fonts = shared_font_atlas ? shared_font_atlas : IM_NEW(ImFontAtlas)(); + Time = 0.0f; + FrameCount = 0; + FrameCountEnded = FrameCountRendered = -1; + WithinFrameScope = WithinFrameScopeWithImplicitWindow = WithinEndChild = false; + GcCompactAll = false; + TestEngineHookItems = false; + TestEngine = NULL; + + WindowsActiveCount = 0; + CurrentWindow = NULL; + HoveredWindow = NULL; + HoveredWindowUnderMovingWindow = NULL; + MovingWindow = NULL; + WheelingWindow = NULL; + WheelingWindowTimer = 0.0f; + + DebugHookIdInfo = 0; + HoveredId = HoveredIdPreviousFrame = 0; + HoveredIdAllowOverlap = false; + HoveredIdUsingMouseWheel = HoveredIdPreviousFrameUsingMouseWheel = false; + HoveredIdDisabled = false; + HoveredIdTimer = HoveredIdNotActiveTimer = 0.0f; + ActiveId = 0; + ActiveIdIsAlive = 0; + ActiveIdTimer = 0.0f; + ActiveIdIsJustActivated = false; + ActiveIdAllowOverlap = false; + ActiveIdNoClearOnFocusLoss = false; + ActiveIdHasBeenPressedBefore = false; + ActiveIdHasBeenEditedBefore = false; + ActiveIdHasBeenEditedThisFrame = false; + ActiveIdUsingMouseWheel = false; + ActiveIdUsingNavDirMask = 0x00; + ActiveIdUsingNavInputMask = 0x00; + ActiveIdUsingKeyInputMask.ClearAllBits(); + ActiveIdClickOffset = ImVec2(-1, -1); + ActiveIdWindow = NULL; + ActiveIdSource = ImGuiInputSource_None; + ActiveIdMouseButton = -1; + ActiveIdPreviousFrame = 0; + ActiveIdPreviousFrameIsAlive = false; + ActiveIdPreviousFrameHasBeenEditedBefore = false; + ActiveIdPreviousFrameWindow = NULL; + LastActiveId = 0; + LastActiveIdTimer = 0.0f; + + CurrentItemFlags = ImGuiItemFlags_None; + BeginMenuCount = 0; + + NavWindow = NULL; + NavId = NavFocusScopeId = NavActivateId = NavActivateDownId = NavActivatePressedId = NavActivateInputId = 0; + NavJustMovedToId = NavJustMovedToFocusScopeId = NavNextActivateId = 0; + NavActivateFlags = NavNextActivateFlags = ImGuiActivateFlags_None; + NavJustMovedToKeyMods = ImGuiKeyModFlags_None; + NavInputSource = ImGuiInputSource_None; + NavLayer = ImGuiNavLayer_Main; + NavIdIsAlive = false; + NavMousePosDirty = false; + NavDisableHighlight = true; + NavDisableMouseHover = false; + NavAnyRequest = false; + NavInitRequest = false; + NavInitRequestFromMove = false; + NavInitResultId = 0; + NavMoveSubmitted = false; + NavMoveScoringItems = false; + NavMoveForwardToNextFrame = false; + NavMoveFlags = ImGuiNavMoveFlags_None; + NavMoveScrollFlags = ImGuiScrollFlags_None; + NavMoveKeyMods = ImGuiKeyModFlags_None; + NavMoveDir = NavMoveDirForDebug = NavMoveClipDir = ImGuiDir_None; + NavScoringDebugCount = 0; + NavTabbingDir = 0; + NavTabbingCounter = 0; + + NavWindowingTarget = NavWindowingTargetAnim = NavWindowingListWindow = NULL; + NavWindowingTimer = NavWindowingHighlightAlpha = 0.0f; + NavWindowingToggleLayer = false; + + DimBgRatio = 0.0f; + MouseCursor = ImGuiMouseCursor_Arrow; + + DragDropActive = DragDropWithinSource = DragDropWithinTarget = false; + DragDropSourceFlags = ImGuiDragDropFlags_None; + DragDropSourceFrameCount = -1; + DragDropMouseButton = -1; + DragDropTargetId = 0; + DragDropAcceptFlags = ImGuiDragDropFlags_None; + DragDropAcceptIdCurrRectSurface = 0.0f; + DragDropAcceptIdPrev = DragDropAcceptIdCurr = 0; + DragDropAcceptFrameCount = -1; + DragDropHoldJustPressedId = 0; + memset(DragDropPayloadBufLocal, 0, sizeof(DragDropPayloadBufLocal)); + + ClipperTempDataStacked = 0; + + CurrentTable = NULL; + TablesTempDataStacked = 0; + CurrentTabBar = NULL; + + TempInputId = 0; + ColorEditOptions = ImGuiColorEditFlags_DefaultOptions_; + ColorEditLastHue = ColorEditLastSat = 0.0f; + ColorEditLastColor = 0; + SliderCurrentAccum = 0.0f; + SliderCurrentAccumDirty = false; + DragCurrentAccumDirty = false; + DragCurrentAccum = 0.0f; + DragSpeedDefaultRatio = 1.0f / 100.0f; + DisabledAlphaBackup = 0.0f; + DisabledStackSize = 0; + ScrollbarClickDeltaToGrabCenter = 0.0f; + TooltipOverrideCount = 0; + TooltipSlowDelay = 0.50f; + + PlatformImeData.InputPos = ImVec2(0.0f, 0.0f); + PlatformImeDataPrev.InputPos = ImVec2(-1.0f, -1.0f); // Different to ensure initial submission + PlatformLocaleDecimalPoint = '.'; + + SettingsLoaded = false; + SettingsDirtyTimer = 0.0f; + HookIdNext = 0; + + LogEnabled = false; + LogType = ImGuiLogType_None; + LogNextPrefix = LogNextSuffix = NULL; + LogFile = NULL; + LogLinePosY = FLT_MAX; + LogLineFirstItem = false; + LogDepthRef = 0; + LogDepthToExpand = LogDepthToExpandDefault = 2; + + DebugItemPickerActive = false; + DebugItemPickerBreakId = 0; + + memset(FramerateSecPerFrame, 0, sizeof(FramerateSecPerFrame)); + FramerateSecPerFrameIdx = FramerateSecPerFrameCount = 0; + FramerateSecPerFrameAccum = 0.0f; + WantCaptureMouseNextFrame = WantCaptureKeyboardNextFrame = WantTextInputNextFrame = -1; + memset(TempBuffer, 0, sizeof(TempBuffer)); + } +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiWindowTempData, ImGuiWindow +//----------------------------------------------------------------------------- + +// Transient per-window data, reset at the beginning of the frame. This used to be called ImGuiDrawContext, hence the DC variable name in ImGuiWindow. +// (That's theory, in practice the delimitation between ImGuiWindow and ImGuiWindowTempData is quite tenuous and could be reconsidered..) +// (This doesn't need a constructor because we zero-clear it as part of ImGuiWindow and all frame-temporary data are setup on Begin) +struct IMGUI_API ImGuiWindowTempData +{ + // Layout + ImVec2 CursorPos; // Current emitting position, in absolute coordinates. + ImVec2 CursorPosPrevLine; + ImVec2 CursorStartPos; // Initial position after Begin(), generally ~ window position + WindowPadding. + ImVec2 CursorMaxPos; // Used to implicitly calculate ContentSize at the beginning of next frame, for scrolling range and auto-resize. Always growing during the frame. + ImVec2 IdealMaxPos; // Used to implicitly calculate ContentSizeIdeal at the beginning of next frame, for auto-resize only. Always growing during the frame. + ImVec2 CurrLineSize; + ImVec2 PrevLineSize; + float CurrLineTextBaseOffset; // Baseline offset (0.0f by default on a new line, generally == style.FramePadding.y when a framed item has been added). + float PrevLineTextBaseOffset; + ImVec1 Indent; // Indentation / start position from left of window (increased by TreePush/TreePop, etc.) + ImVec1 ColumnsOffset; // Offset to the current column (if ColumnsCurrent > 0). FIXME: This and the above should be a stack to allow use cases like Tree->Column->Tree. Need revamp columns API. + ImVec1 GroupOffset; + ImVec2 CursorStartPosLossyness;// Record the loss of precision of CursorStartPos due to really large scrolling amount. This is used by clipper to compensentate and fix the most common use case of large scroll area. + + // Keyboard/Gamepad navigation + ImGuiNavLayer NavLayerCurrent; // Current layer, 0..31 (we currently only use 0..1) + short NavLayersActiveMask; // Which layers have been written to (result from previous frame) + short NavLayersActiveMaskNext;// Which layers have been written to (accumulator for current frame) + ImGuiID NavFocusScopeIdCurrent; // Current focus scope ID while appending + bool NavHideHighlightOneFrame; + bool NavHasScroll; // Set when scrolling can be used (ScrollMax > 0.0f) + + // Miscellaneous + bool MenuBarAppending; // FIXME: Remove this + ImVec2 MenuBarOffset; // MenuBarOffset.x is sort of equivalent of a per-layer CursorPos.x, saved/restored as we switch to the menu bar. The only situation when MenuBarOffset.y is > 0 if when (SafeAreaPadding.y > FramePadding.y), often used on TVs. + ImGuiMenuColumns MenuColumns; // Simplified columns storage for menu items measurement + int TreeDepth; // Current tree depth. + ImU32 TreeJumpToParentOnPopMask; // Store a copy of !g.NavIdIsAlive for TreeDepth 0..31.. Could be turned into a ImU64 if necessary. + ImVector ChildWindows; + ImGuiStorage* StateStorage; // Current persistent per-window storage (store e.g. tree node open/close state) + ImGuiOldColumns* CurrentColumns; // Current columns set + int CurrentTableIdx; // Current table index (into g.Tables) + ImGuiLayoutType LayoutType; + ImGuiLayoutType ParentLayoutType; // Layout type of parent window at the time of Begin() + + // Local parameters stacks + // We store the current settings outside of the vectors to increase memory locality (reduce cache misses). The vectors are rarely modified. Also it allows us to not heap allocate for short-lived windows which are not using those settings. + float ItemWidth; // Current item width (>0.0: width in pixels, <0.0: align xx pixels to the right of window). + float TextWrapPos; // Current text wrap pos. + ImVector ItemWidthStack; // Store item widths to restore (attention: .back() is not == ItemWidth) + ImVector TextWrapPosStack; // Store text wrap pos to restore (attention: .back() is not == TextWrapPos) +}; + +// Storage for one window +struct IMGUI_API ImGuiWindow +{ + char* Name; // Window name, owned by the window. + ImGuiID ID; // == ImHashStr(Name) + ImGuiWindowFlags Flags; // See enum ImGuiWindowFlags_ + ImVec2 Pos; // Position (always rounded-up to nearest pixel) + ImVec2 Size; // Current size (==SizeFull or collapsed title bar size) + ImVec2 SizeFull; // Size when non collapsed + ImVec2 ContentSize; // Size of contents/scrollable client area (calculated from the extents reach of the cursor) from previous frame. Does not include window decoration or window padding. + ImVec2 ContentSizeIdeal; + ImVec2 ContentSizeExplicit; // Size of contents/scrollable client area explicitly request by the user via SetNextWindowContentSize(). + ImVec2 WindowPadding; // Window padding at the time of Begin(). + float WindowRounding; // Window rounding at the time of Begin(). May be clamped lower to avoid rendering artifacts with title bar, menu bar etc. + float WindowBorderSize; // Window border size at the time of Begin(). + int NameBufLen; // Size of buffer storing Name. May be larger than strlen(Name)! + ImGuiID MoveId; // == window->GetID("#MOVE") + ImGuiID ChildId; // ID of corresponding item in parent window (for navigation to return from child window to parent window) + ImVec2 Scroll; + ImVec2 ScrollMax; + ImVec2 ScrollTarget; // target scroll position. stored as cursor position with scrolling canceled out, so the highest point is always 0.0f. (FLT_MAX for no change) + ImVec2 ScrollTargetCenterRatio; // 0.0f = scroll so that target position is at top, 0.5f = scroll so that target position is centered + ImVec2 ScrollTargetEdgeSnapDist; // 0.0f = no snapping, >0.0f snapping threshold + ImVec2 ScrollbarSizes; // Size taken by each scrollbars on their smaller axis. Pay attention! ScrollbarSizes.x == width of the vertical scrollbar, ScrollbarSizes.y = height of the horizontal scrollbar. + bool ScrollbarX, ScrollbarY; // Are scrollbars visible? + bool Active; // Set to true on Begin(), unless Collapsed + bool WasActive; + bool WriteAccessed; // Set to true when any widget access the current window + bool Collapsed; // Set when collapsing window to become only title-bar + bool WantCollapseToggle; + bool SkipItems; // Set when items can safely be all clipped (e.g. window not visible or collapsed) + bool Appearing; // Set during the frame where the window is appearing (or re-appearing) + bool Hidden; // Do not display (== HiddenFrames*** > 0) + bool IsFallbackWindow; // Set on the "Debug##Default" window. + bool IsExplicitChild; // Set when passed _ChildWindow, left to false by BeginDocked() + bool HasCloseButton; // Set when the window has a close button (p_open != NULL) + signed char ResizeBorderHeld; // Current border being held for resize (-1: none, otherwise 0-3) + short BeginCount; // Number of Begin() during the current frame (generally 0 or 1, 1+ if appending via multiple Begin/End pairs) + short BeginOrderWithinParent; // Begin() order within immediate parent window, if we are a child window. Otherwise 0. + short BeginOrderWithinContext; // Begin() order within entire imgui context. This is mostly used for debugging submission order related issues. + short FocusOrder; // Order within WindowsFocusOrder[], altered when windows are focused. + ImGuiID PopupId; // ID in the popup stack when this window is used as a popup/menu (because we use generic Name/ID for recycling) + ImS8 AutoFitFramesX, AutoFitFramesY; + ImS8 AutoFitChildAxises; + bool AutoFitOnlyGrows; + ImGuiDir AutoPosLastDirection; + ImS8 HiddenFramesCanSkipItems; // Hide the window for N frames + ImS8 HiddenFramesCannotSkipItems; // Hide the window for N frames while allowing items to be submitted so we can measure their size + ImS8 HiddenFramesForRenderOnly; // Hide the window until frame N at Render() time only + ImS8 DisableInputsFrames; // Disable window interactions for N frames + ImGuiCond SetWindowPosAllowFlags : 8; // store acceptable condition flags for SetNextWindowPos() use. + ImGuiCond SetWindowSizeAllowFlags : 8; // store acceptable condition flags for SetNextWindowSize() use. + ImGuiCond SetWindowCollapsedAllowFlags : 8; // store acceptable condition flags for SetNextWindowCollapsed() use. + ImVec2 SetWindowPosVal; // store window position when using a non-zero Pivot (position set needs to be processed when we know the window size) + ImVec2 SetWindowPosPivot; // store window pivot for positioning. ImVec2(0, 0) when positioning from top-left corner; ImVec2(0.5f, 0.5f) for centering; ImVec2(1, 1) for bottom right. + + ImVector IDStack; // ID stack. ID are hashes seeded with the value at the top of the stack. (In theory this should be in the TempData structure) + ImGuiWindowTempData DC; // Temporary per-window data, reset at the beginning of the frame. This used to be called ImGuiDrawContext, hence the "DC" variable name. + + // The best way to understand what those rectangles are is to use the 'Metrics->Tools->Show Windows Rectangles' viewer. + // The main 'OuterRect', omitted as a field, is window->Rect(). + ImRect OuterRectClipped; // == Window->Rect() just after setup in Begin(). == window->Rect() for root window. + ImRect InnerRect; // Inner rectangle (omit title bar, menu bar, scroll bar) + ImRect InnerClipRect; // == InnerRect shrunk by WindowPadding*0.5f on each side, clipped within viewport or parent clip rect. + ImRect WorkRect; // Initially covers the whole scrolling region. Reduced by containers e.g columns/tables when active. Shrunk by WindowPadding*1.0f on each side. This is meant to replace ContentRegionRect over time (from 1.71+ onward). + ImRect ParentWorkRect; // Backup of WorkRect before entering a container such as columns/tables. Used by e.g. SpanAllColumns functions to easily access. Stacked containers are responsible for maintaining this. // FIXME-WORKRECT: Could be a stack? + ImRect ClipRect; // Current clipping/scissoring rectangle, evolve as we are using PushClipRect(), etc. == DrawList->clip_rect_stack.back(). + ImRect ContentRegionRect; // FIXME: This is currently confusing/misleading. It is essentially WorkRect but not handling of scrolling. We currently rely on it as right/bottom aligned sizing operation need some size to rely on. + ImVec2ih HitTestHoleSize; // Define an optional rectangular hole where mouse will pass-through the window. + ImVec2ih HitTestHoleOffset; + + int LastFrameActive; // Last frame number the window was Active. + float LastTimeActive; // Last timestamp the window was Active (using float as we don't need high precision there) + float ItemWidthDefault; + ImGuiStorage StateStorage; + ImVector ColumnsStorage; + float FontWindowScale; // User scale multiplier per-window, via SetWindowFontScale() + int SettingsOffset; // Offset into SettingsWindows[] (offsets are always valid as we only grow the array from the back) + + ImDrawList* DrawList; // == &DrawListInst (for backward compatibility reason with code using imgui_internal.h we keep this a pointer) + ImDrawList DrawListInst; + ImGuiWindow* ParentWindow; // If we are a child _or_ popup _or_ docked window, this is pointing to our parent. Otherwise NULL. + ImGuiWindow* ParentWindowInBeginStack; + ImGuiWindow* RootWindow; // Point to ourself or first ancestor that is not a child window. Doesn't cross through popups/dock nodes. + ImGuiWindow* RootWindowPopupTree; // Point to ourself or first ancestor that is not a child window. Cross through popups parent<>child. + ImGuiWindow* RootWindowForTitleBarHighlight; // Point to ourself or first ancestor which will display TitleBgActive color when this window is active. + ImGuiWindow* RootWindowForNav; // Point to ourself or first ancestor which doesn't have the NavFlattened flag. + + ImGuiWindow* NavLastChildNavWindow; // When going to the menu bar, we remember the child window we came from. (This could probably be made implicit if we kept g.Windows sorted by last focused including child window.) + ImGuiID NavLastIds[ImGuiNavLayer_COUNT]; // Last known NavId for this window, per layer (0/1) + ImRect NavRectRel[ImGuiNavLayer_COUNT]; // Reference rectangle, in window relative space + + int MemoryDrawListIdxCapacity; // Backup of last idx/vtx count, so when waking up the window we can preallocate and avoid iterative alloc/copy + int MemoryDrawListVtxCapacity; + bool MemoryCompacted; // Set when window extraneous data have been garbage collected + +public: + ImGuiWindow(ImGuiContext* context, const char* name); + ~ImGuiWindow(); + + ImGuiID GetID(const char* str, const char* str_end = NULL); + ImGuiID GetID(const void* ptr); + ImGuiID GetID(int n); + ImGuiID GetIDNoKeepAlive(const char* str, const char* str_end = NULL); + ImGuiID GetIDNoKeepAlive(const void* ptr); + ImGuiID GetIDNoKeepAlive(int n); + ImGuiID GetIDFromRectangle(const ImRect& r_abs); + + // We don't use g.FontSize because the window may be != g.CurrentWidow. + ImRect Rect() const { return ImRect(Pos.x, Pos.y, Pos.x + Size.x, Pos.y + Size.y); } + float CalcFontSize() const { ImGuiContext& g = *GImGui; float scale = g.FontBaseSize * FontWindowScale; if (ParentWindow) scale *= ParentWindow->FontWindowScale; return scale; } + float TitleBarHeight() const { ImGuiContext& g = *GImGui; return (Flags & ImGuiWindowFlags_NoTitleBar) ? 0.0f : CalcFontSize() + g.Style.FramePadding.y * 2.0f; } + ImRect TitleBarRect() const { return ImRect(Pos, ImVec2(Pos.x + SizeFull.x, Pos.y + TitleBarHeight())); } + float MenuBarHeight() const { ImGuiContext& g = *GImGui; return (Flags & ImGuiWindowFlags_MenuBar) ? DC.MenuBarOffset.y + CalcFontSize() + g.Style.FramePadding.y * 2.0f : 0.0f; } + ImRect MenuBarRect() const { float y1 = Pos.y + TitleBarHeight(); return ImRect(Pos.x, y1, Pos.x + SizeFull.x, y1 + MenuBarHeight()); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Tab bar, Tab item support +//----------------------------------------------------------------------------- + +// Extend ImGuiTabBarFlags_ +enum ImGuiTabBarFlagsPrivate_ +{ + ImGuiTabBarFlags_DockNode = 1 << 20, // Part of a dock node [we don't use this in the master branch but it facilitate branch syncing to keep this around] + ImGuiTabBarFlags_IsFocused = 1 << 21, + ImGuiTabBarFlags_SaveSettings = 1 << 22 // FIXME: Settings are handled by the docking system, this only request the tab bar to mark settings dirty when reordering tabs +}; + +// Extend ImGuiTabItemFlags_ +enum ImGuiTabItemFlagsPrivate_ +{ + ImGuiTabItemFlags_SectionMask_ = ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing, + ImGuiTabItemFlags_NoCloseButton = 1 << 20, // Track whether p_open was set or not (we'll need this info on the next frame to recompute ContentWidth during layout) + ImGuiTabItemFlags_Button = 1 << 21 // Used by TabItemButton, change the tab item behavior to mimic a button +}; + +// Storage for one active tab item (sizeof() 40 bytes) +struct ImGuiTabItem +{ + ImGuiID ID; + ImGuiTabItemFlags Flags; + int LastFrameVisible; + int LastFrameSelected; // This allows us to infer an ordered list of the last activated tabs with little maintenance + float Offset; // Position relative to beginning of tab + float Width; // Width currently displayed + float ContentWidth; // Width of label, stored during BeginTabItem() call + ImS32 NameOffset; // When Window==NULL, offset to name within parent ImGuiTabBar::TabsNames + ImS16 BeginOrder; // BeginTabItem() order, used to re-order tabs after toggling ImGuiTabBarFlags_Reorderable + ImS16 IndexDuringLayout; // Index only used during TabBarLayout() + bool WantClose; // Marked as closed by SetTabItemClosed() + + ImGuiTabItem() { memset(this, 0, sizeof(*this)); LastFrameVisible = LastFrameSelected = -1; NameOffset = -1; BeginOrder = IndexDuringLayout = -1; } +}; + +// Storage for a tab bar (sizeof() 152 bytes) +struct IMGUI_API ImGuiTabBar +{ + ImVector Tabs; + ImGuiTabBarFlags Flags; + ImGuiID ID; // Zero for tab-bars used by docking + ImGuiID SelectedTabId; // Selected tab/window + ImGuiID NextSelectedTabId; // Next selected tab/window. Will also trigger a scrolling animation + ImGuiID VisibleTabId; // Can occasionally be != SelectedTabId (e.g. when previewing contents for CTRL+TAB preview) + int CurrFrameVisible; + int PrevFrameVisible; + ImRect BarRect; + float CurrTabsContentsHeight; + float PrevTabsContentsHeight; // Record the height of contents submitted below the tab bar + float WidthAllTabs; // Actual width of all tabs (locked during layout) + float WidthAllTabsIdeal; // Ideal width if all tabs were visible and not clipped + float ScrollingAnim; + float ScrollingTarget; + float ScrollingTargetDistToVisibility; + float ScrollingSpeed; + float ScrollingRectMinX; + float ScrollingRectMaxX; + ImGuiID ReorderRequestTabId; + ImS16 ReorderRequestOffset; + ImS8 BeginCount; + bool WantLayout; + bool VisibleTabWasSubmitted; + bool TabsAddedNew; // Set to true when a new tab item or button has been added to the tab bar during last frame + ImS16 TabsActiveCount; // Number of tabs submitted this frame. + ImS16 LastTabItemIdx; // Index of last BeginTabItem() tab for use by EndTabItem() + float ItemSpacingY; + ImVec2 FramePadding; // style.FramePadding locked at the time of BeginTabBar() + ImVec2 BackupCursorPos; + ImGuiTextBuffer TabsNames; // For non-docking tab bar we re-append names in a contiguous buffer. + + ImGuiTabBar(); + int GetTabOrder(const ImGuiTabItem* tab) const { return Tabs.index_from_ptr(tab); } + const char* GetTabName(const ImGuiTabItem* tab) const + { + IM_ASSERT(tab->NameOffset != -1 && tab->NameOffset < TabsNames.Buf.Size); + return TabsNames.Buf.Data + tab->NameOffset; + } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Table support +//----------------------------------------------------------------------------- + +#define IM_COL32_DISABLE IM_COL32(0,0,0,1) // Special sentinel code which cannot be used as a regular color. +#define IMGUI_TABLE_MAX_COLUMNS 64 // sizeof(ImU64) * 8. This is solely because we frequently encode columns set in a ImU64. +#define IMGUI_TABLE_MAX_DRAW_CHANNELS (4 + 64 * 2) // See TableSetupDrawChannels() + +// Our current column maximum is 64 but we may raise that in the future. +typedef ImS8 ImGuiTableColumnIdx; +typedef ImU8 ImGuiTableDrawChannelIdx; + +// [Internal] sizeof() ~ 104 +// We use the terminology "Enabled" to refer to a column that is not Hidden by user/api. +// We use the terminology "Clipped" to refer to a column that is out of sight because of scrolling/clipping. +// This is in contrast with some user-facing api such as IsItemVisible() / IsRectVisible() which use "Visible" to mean "not clipped". +struct ImGuiTableColumn +{ + ImGuiTableColumnFlags Flags; // Flags after some patching (not directly same as provided by user). See ImGuiTableColumnFlags_ + float WidthGiven; // Final/actual width visible == (MaxX - MinX), locked in TableUpdateLayout(). May be > WidthRequest to honor minimum width, may be < WidthRequest to honor shrinking columns down in tight space. + float MinX; // Absolute positions + float MaxX; + float WidthRequest; // Master width absolute value when !(Flags & _WidthStretch). When Stretch this is derived every frame from StretchWeight in TableUpdateLayout() + float WidthAuto; // Automatic width + float StretchWeight; // Master width weight when (Flags & _WidthStretch). Often around ~1.0f initially. + float InitStretchWeightOrWidth; // Value passed to TableSetupColumn(). For Width it is a content width (_without padding_). + ImRect ClipRect; // Clipping rectangle for the column + ImGuiID UserID; // Optional, value passed to TableSetupColumn() + float WorkMinX; // Contents region min ~(MinX + CellPaddingX + CellSpacingX1) == cursor start position when entering column + float WorkMaxX; // Contents region max ~(MaxX - CellPaddingX - CellSpacingX2) + float ItemWidth; // Current item width for the column, preserved across rows + float ContentMaxXFrozen; // Contents maximum position for frozen rows (apart from headers), from which we can infer content width. + float ContentMaxXUnfrozen; + float ContentMaxXHeadersUsed; // Contents maximum position for headers rows (regardless of freezing). TableHeader() automatically softclip itself + report ideal desired size, to avoid creating extraneous draw calls + float ContentMaxXHeadersIdeal; + ImS16 NameOffset; // Offset into parent ColumnsNames[] + ImGuiTableColumnIdx DisplayOrder; // Index within Table's IndexToDisplayOrder[] (column may be reordered by users) + ImGuiTableColumnIdx IndexWithinEnabledSet; // Index within enabled/visible set (<= IndexToDisplayOrder) + ImGuiTableColumnIdx PrevEnabledColumn; // Index of prev enabled/visible column within Columns[], -1 if first enabled/visible column + ImGuiTableColumnIdx NextEnabledColumn; // Index of next enabled/visible column within Columns[], -1 if last enabled/visible column + ImGuiTableColumnIdx SortOrder; // Index of this column within sort specs, -1 if not sorting on this column, 0 for single-sort, may be >0 on multi-sort + ImGuiTableDrawChannelIdx DrawChannelCurrent; // Index within DrawSplitter.Channels[] + ImGuiTableDrawChannelIdx DrawChannelFrozen; // Draw channels for frozen rows (often headers) + ImGuiTableDrawChannelIdx DrawChannelUnfrozen; // Draw channels for unfrozen rows + bool IsEnabled; // IsUserEnabled && (Flags & ImGuiTableColumnFlags_Disabled) == 0 + bool IsUserEnabled; // Is the column not marked Hidden by the user? (unrelated to being off view, e.g. clipped by scrolling). + bool IsUserEnabledNextFrame; + bool IsVisibleX; // Is actually in view (e.g. overlapping the host window clipping rectangle, not scrolled). + bool IsVisibleY; + bool IsRequestOutput; // Return value for TableSetColumnIndex() / TableNextColumn(): whether we request user to output contents or not. + bool IsSkipItems; // Do we want item submissions to this column to be completely ignored (no layout will happen). + bool IsPreserveWidthAuto; + ImS8 NavLayerCurrent; // ImGuiNavLayer in 1 byte + ImU8 AutoFitQueue; // Queue of 8 values for the next 8 frames to request auto-fit + ImU8 CannotSkipItemsQueue; // Queue of 8 values for the next 8 frames to disable Clipped/SkipItem + ImU8 SortDirection : 2; // ImGuiSortDirection_Ascending or ImGuiSortDirection_Descending + ImU8 SortDirectionsAvailCount : 2; // Number of available sort directions (0 to 3) + ImU8 SortDirectionsAvailMask : 4; // Mask of available sort directions (1-bit each) + ImU8 SortDirectionsAvailList; // Ordered of available sort directions (2-bits each) + + ImGuiTableColumn() + { + memset(this, 0, sizeof(*this)); + StretchWeight = WidthRequest = -1.0f; + NameOffset = -1; + DisplayOrder = IndexWithinEnabledSet = -1; + PrevEnabledColumn = NextEnabledColumn = -1; + SortOrder = -1; + SortDirection = ImGuiSortDirection_None; + DrawChannelCurrent = DrawChannelFrozen = DrawChannelUnfrozen = (ImU8)-1; + } +}; + +// Transient cell data stored per row. +// sizeof() ~ 6 +struct ImGuiTableCellData +{ + ImU32 BgColor; // Actual color + ImGuiTableColumnIdx Column; // Column number +}; + +// FIXME-TABLE: more transient data could be stored in a per-stacked table structure: DrawSplitter, SortSpecs, incoming RowData +struct IMGUI_API ImGuiTable +{ + ImGuiID ID; + ImGuiTableFlags Flags; + void* RawData; // Single allocation to hold Columns[], DisplayOrderToIndex[] and RowCellData[] + ImGuiTableTempData* TempData; // Transient data while table is active. Point within g.CurrentTableStack[] + ImSpan Columns; // Point within RawData[] + ImSpan DisplayOrderToIndex; // Point within RawData[]. Store display order of columns (when not reordered, the values are 0...Count-1) + ImSpan RowCellData; // Point within RawData[]. Store cells background requests for current row. + ImU64 EnabledMaskByDisplayOrder; // Column DisplayOrder -> IsEnabled map + ImU64 EnabledMaskByIndex; // Column Index -> IsEnabled map (== not hidden by user/api) in a format adequate for iterating column without touching cold data + ImU64 VisibleMaskByIndex; // Column Index -> IsVisibleX|IsVisibleY map (== not hidden by user/api && not hidden by scrolling/cliprect) + ImU64 RequestOutputMaskByIndex; // Column Index -> IsVisible || AutoFit (== expect user to submit items) + ImGuiTableFlags SettingsLoadedFlags; // Which data were loaded from the .ini file (e.g. when order is not altered we won't save order) + int SettingsOffset; // Offset in g.SettingsTables + int LastFrameActive; + int ColumnsCount; // Number of columns declared in BeginTable() + int CurrentRow; + int CurrentColumn; + ImS16 InstanceCurrent; // Count of BeginTable() calls with same ID in the same frame (generally 0). This is a little bit similar to BeginCount for a window, but multiple table with same ID look are multiple tables, they are just synched. + ImS16 InstanceInteracted; // Mark which instance (generally 0) of the same ID is being interacted with + float RowPosY1; + float RowPosY2; + float RowMinHeight; // Height submitted to TableNextRow() + float RowTextBaseline; + float RowIndentOffsetX; + ImGuiTableRowFlags RowFlags : 16; // Current row flags, see ImGuiTableRowFlags_ + ImGuiTableRowFlags LastRowFlags : 16; + int RowBgColorCounter; // Counter for alternating background colors (can be fast-forwarded by e.g clipper), not same as CurrentRow because header rows typically don't increase this. + ImU32 RowBgColor[2]; // Background color override for current row. + ImU32 BorderColorStrong; + ImU32 BorderColorLight; + float BorderX1; + float BorderX2; + float HostIndentX; + float MinColumnWidth; + float OuterPaddingX; + float CellPaddingX; // Padding from each borders + float CellPaddingY; + float CellSpacingX1; // Spacing between non-bordered cells + float CellSpacingX2; + float LastOuterHeight; // Outer height from last frame + float LastFirstRowHeight; // Height of first row from last frame + float InnerWidth; // User value passed to BeginTable(), see comments at the top of BeginTable() for details. + float ColumnsGivenWidth; // Sum of current column width + float ColumnsAutoFitWidth; // Sum of ideal column width in order nothing to be clipped, used for auto-fitting and content width submission in outer window + float ResizedColumnNextWidth; + float ResizeLockMinContentsX2; // Lock minimum contents width while resizing down in order to not create feedback loops. But we allow growing the table. + float RefScale; // Reference scale to be able to rescale columns on font/dpi changes. + ImRect OuterRect; // Note: for non-scrolling table, OuterRect.Max.y is often FLT_MAX until EndTable(), unless a height has been specified in BeginTable(). + ImRect InnerRect; // InnerRect but without decoration. As with OuterRect, for non-scrolling tables, InnerRect.Max.y is + ImRect WorkRect; + ImRect InnerClipRect; + ImRect BgClipRect; // We use this to cpu-clip cell background color fill, evolve during the frame as we cross frozen rows boundaries + ImRect Bg0ClipRectForDrawCmd; // Actual ImDrawCmd clip rect for BG0/1 channel. This tends to be == OuterWindow->ClipRect at BeginTable() because output in BG0/BG1 is cpu-clipped + ImRect Bg2ClipRectForDrawCmd; // Actual ImDrawCmd clip rect for BG2 channel. This tends to be a correct, tight-fit, because output to BG2 are done by widgets relying on regular ClipRect. + ImRect HostClipRect; // This is used to check if we can eventually merge our columns draw calls into the current draw call of the current window. + ImRect HostBackupInnerClipRect; // Backup of InnerWindow->ClipRect during PushTableBackground()/PopTableBackground() + ImGuiWindow* OuterWindow; // Parent window for the table + ImGuiWindow* InnerWindow; // Window holding the table data (== OuterWindow or a child window) + ImGuiTextBuffer ColumnsNames; // Contiguous buffer holding columns names + ImDrawListSplitter* DrawSplitter; // Shortcut to TempData->DrawSplitter while in table. Isolate draw commands per columns to avoid switching clip rect constantly + ImGuiTableColumnSortSpecs SortSpecsSingle; + ImVector SortSpecsMulti; // FIXME-OPT: Using a small-vector pattern would be good. + ImGuiTableSortSpecs SortSpecs; // Public facing sorts specs, this is what we return in TableGetSortSpecs() + ImGuiTableColumnIdx SortSpecsCount; + ImGuiTableColumnIdx ColumnsEnabledCount; // Number of enabled columns (<= ColumnsCount) + ImGuiTableColumnIdx ColumnsEnabledFixedCount; // Number of enabled columns (<= ColumnsCount) + ImGuiTableColumnIdx DeclColumnsCount; // Count calls to TableSetupColumn() + ImGuiTableColumnIdx HoveredColumnBody; // Index of column whose visible region is being hovered. Important: == ColumnsCount when hovering empty region after the right-most column! + ImGuiTableColumnIdx HoveredColumnBorder; // Index of column whose right-border is being hovered (for resizing). + ImGuiTableColumnIdx AutoFitSingleColumn; // Index of single column requesting auto-fit. + ImGuiTableColumnIdx ResizedColumn; // Index of column being resized. Reset when InstanceCurrent==0. + ImGuiTableColumnIdx LastResizedColumn; // Index of column being resized from previous frame. + ImGuiTableColumnIdx HeldHeaderColumn; // Index of column header being held. + ImGuiTableColumnIdx ReorderColumn; // Index of column being reordered. (not cleared) + ImGuiTableColumnIdx ReorderColumnDir; // -1 or +1 + ImGuiTableColumnIdx LeftMostEnabledColumn; // Index of left-most non-hidden column. + ImGuiTableColumnIdx RightMostEnabledColumn; // Index of right-most non-hidden column. + ImGuiTableColumnIdx LeftMostStretchedColumn; // Index of left-most stretched column. + ImGuiTableColumnIdx RightMostStretchedColumn; // Index of right-most stretched column. + ImGuiTableColumnIdx ContextPopupColumn; // Column right-clicked on, of -1 if opening context menu from a neutral/empty spot + ImGuiTableColumnIdx FreezeRowsRequest; // Requested frozen rows count + ImGuiTableColumnIdx FreezeRowsCount; // Actual frozen row count (== FreezeRowsRequest, or == 0 when no scrolling offset) + ImGuiTableColumnIdx FreezeColumnsRequest; // Requested frozen columns count + ImGuiTableColumnIdx FreezeColumnsCount; // Actual frozen columns count (== FreezeColumnsRequest, or == 0 when no scrolling offset) + ImGuiTableColumnIdx RowCellDataCurrent; // Index of current RowCellData[] entry in current row + ImGuiTableDrawChannelIdx DummyDrawChannel; // Redirect non-visible columns here. + ImGuiTableDrawChannelIdx Bg2DrawChannelCurrent; // For Selectable() and other widgets drawing across columns after the freezing line. Index within DrawSplitter.Channels[] + ImGuiTableDrawChannelIdx Bg2DrawChannelUnfrozen; + bool IsLayoutLocked; // Set by TableUpdateLayout() which is called when beginning the first row. + bool IsInsideRow; // Set when inside TableBeginRow()/TableEndRow(). + bool IsInitializing; + bool IsSortSpecsDirty; + bool IsUsingHeaders; // Set when the first row had the ImGuiTableRowFlags_Headers flag. + bool IsContextPopupOpen; // Set when default context menu is open (also see: ContextPopupColumn, InstanceInteracted). + bool IsSettingsRequestLoad; + bool IsSettingsDirty; // Set when table settings have changed and needs to be reported into ImGuiTableSetttings data. + bool IsDefaultDisplayOrder; // Set when display order is unchanged from default (DisplayOrder contains 0...Count-1) + bool IsResetAllRequest; + bool IsResetDisplayOrderRequest; + bool IsUnfrozenRows; // Set when we got past the frozen row. + bool IsDefaultSizingPolicy; // Set if user didn't explicitly set a sizing policy in BeginTable() + bool MemoryCompacted; + bool HostSkipItems; // Backup of InnerWindow->SkipItem at the end of BeginTable(), because we will overwrite InnerWindow->SkipItem on a per-column basis + + ImGuiTable() { memset(this, 0, sizeof(*this)); LastFrameActive = -1; } + ~ImGuiTable() { IM_FREE(RawData); } +}; + +// Transient data that are only needed between BeginTable() and EndTable(), those buffers are shared (1 per level of stacked table). +// - Accessing those requires chasing an extra pointer so for very frequently used data we leave them in the main table structure. +// - We also leave out of this structure data that tend to be particularly useful for debugging/metrics. +struct IMGUI_API ImGuiTableTempData +{ + int TableIndex; // Index in g.Tables.Buf[] pool + float LastTimeActive; // Last timestamp this structure was used + + ImVec2 UserOuterSize; // outer_size.x passed to BeginTable() + ImDrawListSplitter DrawSplitter; + + ImRect HostBackupWorkRect; // Backup of InnerWindow->WorkRect at the end of BeginTable() + ImRect HostBackupParentWorkRect; // Backup of InnerWindow->ParentWorkRect at the end of BeginTable() + ImVec2 HostBackupPrevLineSize; // Backup of InnerWindow->DC.PrevLineSize at the end of BeginTable() + ImVec2 HostBackupCurrLineSize; // Backup of InnerWindow->DC.CurrLineSize at the end of BeginTable() + ImVec2 HostBackupCursorMaxPos; // Backup of InnerWindow->DC.CursorMaxPos at the end of BeginTable() + ImVec1 HostBackupColumnsOffset; // Backup of OuterWindow->DC.ColumnsOffset at the end of BeginTable() + float HostBackupItemWidth; // Backup of OuterWindow->DC.ItemWidth at the end of BeginTable() + int HostBackupItemWidthStackSize;//Backup of OuterWindow->DC.ItemWidthStack.Size at the end of BeginTable() + + ImGuiTableTempData() { memset(this, 0, sizeof(*this)); LastTimeActive = -1.0f; } +}; + +// sizeof() ~ 12 +struct ImGuiTableColumnSettings +{ + float WidthOrWeight; + ImGuiID UserID; + ImGuiTableColumnIdx Index; + ImGuiTableColumnIdx DisplayOrder; + ImGuiTableColumnIdx SortOrder; + ImU8 SortDirection : 2; + ImU8 IsEnabled : 1; // "Visible" in ini file + ImU8 IsStretch : 1; + + ImGuiTableColumnSettings() + { + WidthOrWeight = 0.0f; + UserID = 0; + Index = -1; + DisplayOrder = SortOrder = -1; + SortDirection = ImGuiSortDirection_None; + IsEnabled = 1; + IsStretch = 0; + } +}; + +// This is designed to be stored in a single ImChunkStream (1 header followed by N ImGuiTableColumnSettings, etc.) +struct ImGuiTableSettings +{ + ImGuiID ID; // Set to 0 to invalidate/delete the setting + ImGuiTableFlags SaveFlags; // Indicate data we want to save using the Resizable/Reorderable/Sortable/Hideable flags (could be using its own flags..) + float RefScale; // Reference scale to be able to rescale columns on font/dpi changes. + ImGuiTableColumnIdx ColumnsCount; + ImGuiTableColumnIdx ColumnsCountMax; // Maximum number of columns this settings instance can store, we can recycle a settings instance with lower number of columns but not higher + bool WantApply; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) + + ImGuiTableSettings() { memset(this, 0, sizeof(*this)); } + ImGuiTableColumnSettings* GetColumnSettings() { return (ImGuiTableColumnSettings*)(this + 1); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGui internal API +// No guarantee of forward compatibility here! +//----------------------------------------------------------------------------- + +namespace ImGui +{ + // Windows + // We should always have a CurrentWindow in the stack (there is an implicit "Debug" window) + // If this ever crash because g.CurrentWindow is NULL it means that either + // - ImGui::NewFrame() has never been called, which is illegal. + // - You are calling ImGui functions after ImGui::EndFrame()/ImGui::Render() and before the next ImGui::NewFrame(), which is also illegal. + inline ImGuiWindow* GetCurrentWindowRead() { ImGuiContext& g = *GImGui; return g.CurrentWindow; } + inline ImGuiWindow* GetCurrentWindow() { ImGuiContext& g = *GImGui; g.CurrentWindow->WriteAccessed = true; return g.CurrentWindow; } + IMGUI_API ImGuiWindow* FindWindowByID(ImGuiID id); + IMGUI_API ImGuiWindow* FindWindowByName(const char* name); + IMGUI_API void UpdateWindowParentAndRootLinks(ImGuiWindow* window, ImGuiWindowFlags flags, ImGuiWindow* parent_window); + IMGUI_API ImVec2 CalcWindowNextAutoFitSize(ImGuiWindow* window); + IMGUI_API bool IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent, bool popup_hierarchy); + IMGUI_API bool IsWindowWithinBeginStackOf(ImGuiWindow* window, ImGuiWindow* potential_parent); + IMGUI_API bool IsWindowAbove(ImGuiWindow* potential_above, ImGuiWindow* potential_below); + IMGUI_API bool IsWindowNavFocusable(ImGuiWindow* window); + IMGUI_API void SetWindowPos(ImGuiWindow* window, const ImVec2& pos, ImGuiCond cond = 0); + IMGUI_API void SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond cond = 0); + IMGUI_API void SetWindowCollapsed(ImGuiWindow* window, bool collapsed, ImGuiCond cond = 0); + IMGUI_API void SetWindowHitTestHole(ImGuiWindow* window, const ImVec2& pos, const ImVec2& size); + inline ImRect WindowRectAbsToRel(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x - off.x, r.Min.y - off.y, r.Max.x - off.x, r.Max.y - off.y); } + inline ImRect WindowRectRelToAbs(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x + off.x, r.Min.y + off.y, r.Max.x + off.x, r.Max.y + off.y); } + + // Windows: Display Order and Focus Order + IMGUI_API void FocusWindow(ImGuiWindow* window); + IMGUI_API void FocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, ImGuiWindow* ignore_window); + IMGUI_API void BringWindowToFocusFront(ImGuiWindow* window); + IMGUI_API void BringWindowToDisplayFront(ImGuiWindow* window); + IMGUI_API void BringWindowToDisplayBack(ImGuiWindow* window); + IMGUI_API void BringWindowToDisplayBehind(ImGuiWindow* window, ImGuiWindow* above_window); + IMGUI_API int FindWindowDisplayIndex(ImGuiWindow* window); + IMGUI_API ImGuiWindow* FindBottomMostVisibleWindowWithinBeginStack(ImGuiWindow* window); + + // Fonts, drawing + IMGUI_API void SetCurrentFont(ImFont* font); + inline ImFont* GetDefaultFont() { ImGuiContext& g = *GImGui; return g.IO.FontDefault ? g.IO.FontDefault : g.IO.Fonts->Fonts[0]; } + inline ImDrawList* GetForegroundDrawList(ImGuiWindow* window) { IM_UNUSED(window); return GetForegroundDrawList(); } // This seemingly unnecessary wrapper simplifies compatibility between the 'master' and 'docking' branches. + IMGUI_API ImDrawList* GetBackgroundDrawList(ImGuiViewport* viewport); // get background draw list for the given viewport. this draw list will be the first rendering one. Useful to quickly draw shapes/text behind dear imgui contents. + IMGUI_API ImDrawList* GetForegroundDrawList(ImGuiViewport* viewport); // get foreground draw list for the given viewport. this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents. + + // Init + IMGUI_API void Initialize(ImGuiContext* context); + IMGUI_API void Shutdown(ImGuiContext* context); // Since 1.60 this is a _private_ function. You can call DestroyContext() to destroy the context created by CreateContext(). + + // NewFrame + IMGUI_API void UpdateInputEvents(bool trickle_fast_inputs); + IMGUI_API void UpdateHoveredWindowAndCaptureFlags(); + IMGUI_API void StartMouseMovingWindow(ImGuiWindow* window); + IMGUI_API void UpdateMouseMovingWindowNewFrame(); + IMGUI_API void UpdateMouseMovingWindowEndFrame(); + + // Generic context hooks + IMGUI_API ImGuiID AddContextHook(ImGuiContext* context, const ImGuiContextHook* hook); + IMGUI_API void RemoveContextHook(ImGuiContext* context, ImGuiID hook_to_remove); + IMGUI_API void CallContextHooks(ImGuiContext* context, ImGuiContextHookType type); + + // Settings + IMGUI_API void MarkIniSettingsDirty(); + IMGUI_API void MarkIniSettingsDirty(ImGuiWindow* window); + IMGUI_API void ClearIniSettings(); + IMGUI_API ImGuiWindowSettings* CreateNewWindowSettings(const char* name); + IMGUI_API ImGuiWindowSettings* FindWindowSettings(ImGuiID id); + IMGUI_API ImGuiWindowSettings* FindOrCreateWindowSettings(const char* name); + IMGUI_API ImGuiSettingsHandler* FindSettingsHandler(const char* type_name); + + // Scrolling + IMGUI_API void SetNextWindowScroll(const ImVec2& scroll); // Use -1.0f on one axis to leave as-is + IMGUI_API void SetScrollX(ImGuiWindow* window, float scroll_x); + IMGUI_API void SetScrollY(ImGuiWindow* window, float scroll_y); + IMGUI_API void SetScrollFromPosX(ImGuiWindow* window, float local_x, float center_x_ratio); + IMGUI_API void SetScrollFromPosY(ImGuiWindow* window, float local_y, float center_y_ratio); + + // Early work-in-progress API (ScrollToItem() will become public) + IMGUI_API void ScrollToItem(ImGuiScrollFlags flags = 0); + IMGUI_API void ScrollToRect(ImGuiWindow* window, const ImRect& rect, ImGuiScrollFlags flags = 0); + IMGUI_API ImVec2 ScrollToRectEx(ImGuiWindow* window, const ImRect& rect, ImGuiScrollFlags flags = 0); +//#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + inline void ScrollToBringRectIntoView(ImGuiWindow* window, const ImRect& rect) { ScrollToRect(window, rect, ImGuiScrollFlags_KeepVisibleEdgeY); } +//#endif + + // Basic Accessors + inline ImGuiID GetItemID() { ImGuiContext& g = *GImGui; return g.LastItemData.ID; } // Get ID of last item (~~ often same ImGui::GetID(label) beforehand) + inline ImGuiItemStatusFlags GetItemStatusFlags(){ ImGuiContext& g = *GImGui; return g.LastItemData.StatusFlags; } + inline ImGuiItemFlags GetItemFlags() { ImGuiContext& g = *GImGui; return g.LastItemData.InFlags; } + inline ImGuiID GetActiveID() { ImGuiContext& g = *GImGui; return g.ActiveId; } + inline ImGuiID GetFocusID() { ImGuiContext& g = *GImGui; return g.NavId; } + IMGUI_API void SetActiveID(ImGuiID id, ImGuiWindow* window); + IMGUI_API void SetFocusID(ImGuiID id, ImGuiWindow* window); + IMGUI_API void ClearActiveID(); + IMGUI_API ImGuiID GetHoveredID(); + IMGUI_API void SetHoveredID(ImGuiID id); + IMGUI_API void KeepAliveID(ImGuiID id); + IMGUI_API void MarkItemEdited(ImGuiID id); // Mark data associated to given item as "edited", used by IsItemDeactivatedAfterEdit() function. + IMGUI_API void PushOverrideID(ImGuiID id); // Push given value as-is at the top of the ID stack (whereas PushID combines old and new hashes) + IMGUI_API ImGuiID GetIDWithSeed(const char* str_id_begin, const char* str_id_end, ImGuiID seed); + + // Basic Helpers for widget code + IMGUI_API void ItemSize(const ImVec2& size, float text_baseline_y = -1.0f); + IMGUI_API void ItemSize(const ImRect& bb, float text_baseline_y = -1.0f); + IMGUI_API bool ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb = NULL, ImGuiItemFlags extra_flags = 0); + IMGUI_API bool ItemHoverable(const ImRect& bb, ImGuiID id); + IMGUI_API bool IsClippedEx(const ImRect& bb, ImGuiID id); + IMGUI_API void SetLastItemData(ImGuiID item_id, ImGuiItemFlags in_flags, ImGuiItemStatusFlags status_flags, const ImRect& item_rect); + IMGUI_API ImVec2 CalcItemSize(ImVec2 size, float default_w, float default_h); + IMGUI_API float CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x); + IMGUI_API void PushMultiItemsWidths(int components, float width_full); + IMGUI_API bool IsItemToggledSelection(); // Was the last item selection toggled? (after Selectable(), TreeNode() etc. We only returns toggle _event_ in order to handle clipping correctly) + IMGUI_API ImVec2 GetContentRegionMaxAbs(); + IMGUI_API void ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess); + + // Parameter stacks + IMGUI_API void PushItemFlag(ImGuiItemFlags option, bool enabled); + IMGUI_API void PopItemFlag(); + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + // Currently refactoring focus/nav/tabbing system + // If you have old/custom copy-and-pasted widgets that used FocusableItemRegister(): + // (Old) IMGUI_VERSION_NUM < 18209: using 'ItemAdd(....)' and 'bool tab_focused = FocusableItemRegister(...)' + // (Old) IMGUI_VERSION_NUM >= 18209: using 'ItemAdd(..., ImGuiItemAddFlags_Focusable)' and 'bool tab_focused = (GetItemStatusFlags() & ImGuiItemStatusFlags_Focused) != 0' + // (New) IMGUI_VERSION_NUM >= 18413: using 'ItemAdd(..., ImGuiItemFlags_Inputable)' and 'bool tab_focused = (GetItemStatusFlags() & ImGuiItemStatusFlags_FocusedTabbing) != 0 || g.NavActivateInputId == id' (WIP) + // Widget code are simplified as there's no need to call FocusableItemUnregister() while managing the transition from regular widget to TempInputText() + inline bool FocusableItemRegister(ImGuiWindow* window, ImGuiID id) { IM_ASSERT(0); IM_UNUSED(window); IM_UNUSED(id); return false; } // -> pass ImGuiItemAddFlags_Inputable flag to ItemAdd() + inline void FocusableItemUnregister(ImGuiWindow* window) { IM_ASSERT(0); IM_UNUSED(window); } // -> unnecessary: TempInputText() uses ImGuiInputTextFlags_MergedItem +#endif + + // Logging/Capture + IMGUI_API void LogBegin(ImGuiLogType type, int auto_open_depth); // -> BeginCapture() when we design v2 api, for now stay under the radar by using the old name. + IMGUI_API void LogToBuffer(int auto_open_depth = -1); // Start logging/capturing to internal buffer + IMGUI_API void LogRenderedText(const ImVec2* ref_pos, const char* text, const char* text_end = NULL); + IMGUI_API void LogSetNextTextDecoration(const char* prefix, const char* suffix); + + // Popups, Modals, Tooltips + IMGUI_API bool BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, bool border, ImGuiWindowFlags flags); + IMGUI_API void OpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags = ImGuiPopupFlags_None); + IMGUI_API void ClosePopupToLevel(int remaining, bool restore_focus_to_window_under_popup); + IMGUI_API void ClosePopupsOverWindow(ImGuiWindow* ref_window, bool restore_focus_to_window_under_popup); + IMGUI_API void ClosePopupsExceptModals(); + IMGUI_API bool IsPopupOpen(ImGuiID id, ImGuiPopupFlags popup_flags); + IMGUI_API bool BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_flags); + IMGUI_API void BeginTooltipEx(ImGuiTooltipFlags tooltip_flags, ImGuiWindowFlags extra_window_flags); + IMGUI_API ImRect GetPopupAllowedExtentRect(ImGuiWindow* window); + IMGUI_API ImGuiWindow* GetTopMostPopupModal(); + IMGUI_API ImGuiWindow* GetTopMostAndVisiblePopupModal(); + IMGUI_API ImVec2 FindBestWindowPosForPopup(ImGuiWindow* window); + IMGUI_API ImVec2 FindBestWindowPosForPopupEx(const ImVec2& ref_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_outer, const ImRect& r_avoid, ImGuiPopupPositionPolicy policy); + + // Menus + IMGUI_API bool BeginViewportSideBar(const char* name, ImGuiViewport* viewport, ImGuiDir dir, float size, ImGuiWindowFlags window_flags); + IMGUI_API bool BeginMenuEx(const char* label, const char* icon, bool enabled = true); + IMGUI_API bool MenuItemEx(const char* label, const char* icon, const char* shortcut = NULL, bool selected = false, bool enabled = true); + + // Combos + IMGUI_API bool BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags); + IMGUI_API bool BeginComboPreview(); + IMGUI_API void EndComboPreview(); + + // Gamepad/Keyboard Navigation + IMGUI_API void NavInitWindow(ImGuiWindow* window, bool force_reinit); + IMGUI_API void NavInitRequestApplyResult(); + IMGUI_API bool NavMoveRequestButNoResultYet(); + IMGUI_API void NavMoveRequestSubmit(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags); + IMGUI_API void NavMoveRequestForward(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags); + IMGUI_API void NavMoveRequestResolveWithLastItem(ImGuiNavItemData* result); + IMGUI_API void NavMoveRequestCancel(); + IMGUI_API void NavMoveRequestApplyResult(); + IMGUI_API void NavMoveRequestTryWrapping(ImGuiWindow* window, ImGuiNavMoveFlags move_flags); + IMGUI_API const char* GetNavInputName(ImGuiNavInput n); + IMGUI_API float GetNavInputAmount(ImGuiNavInput n, ImGuiInputReadMode mode); + IMGUI_API ImVec2 GetNavInputAmount2d(ImGuiNavDirSourceFlags dir_sources, ImGuiInputReadMode mode, float slow_factor = 0.0f, float fast_factor = 0.0f); + IMGUI_API int CalcTypematicRepeatAmount(float t0, float t1, float repeat_delay, float repeat_rate); + IMGUI_API void ActivateItem(ImGuiID id); // Remotely activate a button, checkbox, tree node etc. given its unique ID. activation is queued and processed on the next frame when the item is encountered again. + IMGUI_API void SetNavID(ImGuiID id, ImGuiNavLayer nav_layer, ImGuiID focus_scope_id, const ImRect& rect_rel); + + // Focus Scope (WIP) + // This is generally used to identify a selection set (multiple of which may be in the same window), as selection + // patterns generally need to react (e.g. clear selection) when landing on an item of the set. + IMGUI_API void PushFocusScope(ImGuiID id); + IMGUI_API void PopFocusScope(); + inline ImGuiID GetFocusedFocusScope() { ImGuiContext& g = *GImGui; return g.NavFocusScopeId; } // Focus scope which is actually active + inline ImGuiID GetFocusScope() { ImGuiContext& g = *GImGui; return g.CurrentWindow->DC.NavFocusScopeIdCurrent; } // Focus scope we are outputting into, set by PushFocusScope() + + // Inputs + // FIXME: Eventually we should aim to move e.g. IsActiveIdUsingKey() into IsKeyXXX functions. + inline bool IsNamedKey(ImGuiKey key) { return key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END; } + inline bool IsLegacyKey(ImGuiKey key) { return key >= ImGuiKey_LegacyNativeKey_BEGIN && key < ImGuiKey_LegacyNativeKey_END; } + inline bool IsGamepadKey(ImGuiKey key) { return key >= ImGuiKey_Gamepad_BEGIN && key < ImGuiKey_Gamepad_END; } + IMGUI_API ImGuiKeyData* GetKeyData(ImGuiKey key); + IMGUI_API void SetItemUsingMouseWheel(); + IMGUI_API void SetActiveIdUsingNavAndKeys(); + inline bool IsActiveIdUsingNavDir(ImGuiDir dir) { ImGuiContext& g = *GImGui; return (g.ActiveIdUsingNavDirMask & (1 << dir)) != 0; } + inline bool IsActiveIdUsingNavInput(ImGuiNavInput input) { ImGuiContext& g = *GImGui; return (g.ActiveIdUsingNavInputMask & (1 << input)) != 0; } + inline bool IsActiveIdUsingKey(ImGuiKey key) { ImGuiContext& g = *GImGui; return g.ActiveIdUsingKeyInputMask[key]; } + inline void SetActiveIdUsingKey(ImGuiKey key) { ImGuiContext& g = *GImGui; g.ActiveIdUsingKeyInputMask.SetBit(key); } + IMGUI_API bool IsMouseDragPastThreshold(ImGuiMouseButton button, float lock_threshold = -1.0f); + inline bool IsNavInputDown(ImGuiNavInput n) { ImGuiContext& g = *GImGui; return g.IO.NavInputs[n] > 0.0f; } + inline bool IsNavInputTest(ImGuiNavInput n, ImGuiInputReadMode rm) { return (GetNavInputAmount(n, rm) > 0.0f); } + IMGUI_API ImGuiKeyModFlags GetMergedKeyModFlags(); +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + inline bool IsKeyPressedMap(ImGuiKey key, bool repeat = true) { IM_ASSERT(IsNamedKey(key)); return IsKeyPressed(key, repeat); } +#endif + + // Drag and Drop + IMGUI_API bool BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id); + IMGUI_API void ClearDragDrop(); + IMGUI_API bool IsDragDropPayloadBeingAccepted(); + + // Internal Columns API (this is not exposed because we will encourage transitioning to the Tables API) + IMGUI_API void SetWindowClipRectBeforeSetChannel(ImGuiWindow* window, const ImRect& clip_rect); + IMGUI_API void BeginColumns(const char* str_id, int count, ImGuiOldColumnFlags flags = 0); // setup number of columns. use an identifier to distinguish multiple column sets. close with EndColumns(). + IMGUI_API void EndColumns(); // close columns + IMGUI_API void PushColumnClipRect(int column_index); + IMGUI_API void PushColumnsBackground(); + IMGUI_API void PopColumnsBackground(); + IMGUI_API ImGuiID GetColumnsID(const char* str_id, int count); + IMGUI_API ImGuiOldColumns* FindOrCreateColumns(ImGuiWindow* window, ImGuiID id); + IMGUI_API float GetColumnOffsetFromNorm(const ImGuiOldColumns* columns, float offset_norm); + IMGUI_API float GetColumnNormFromOffset(const ImGuiOldColumns* columns, float offset); + + // Tables: Candidates for public API + IMGUI_API void TableOpenContextMenu(int column_n = -1); + IMGUI_API void TableSetColumnWidth(int column_n, float width); + IMGUI_API void TableSetColumnSortDirection(int column_n, ImGuiSortDirection sort_direction, bool append_to_sort_specs); + IMGUI_API int TableGetHoveredColumn(); // May use (TableGetColumnFlags() & ImGuiTableColumnFlags_IsHovered) instead. Return hovered column. return -1 when table is not hovered. return columns_count if the unused space at the right of visible columns is hovered. + IMGUI_API float TableGetHeaderRowHeight(); + IMGUI_API void TablePushBackgroundChannel(); + IMGUI_API void TablePopBackgroundChannel(); + + // Tables: Internals + inline ImGuiTable* GetCurrentTable() { ImGuiContext& g = *GImGui; return g.CurrentTable; } + IMGUI_API ImGuiTable* TableFindByID(ImGuiID id); + IMGUI_API bool BeginTableEx(const char* name, ImGuiID id, int columns_count, ImGuiTableFlags flags = 0, const ImVec2& outer_size = ImVec2(0, 0), float inner_width = 0.0f); + IMGUI_API void TableBeginInitMemory(ImGuiTable* table, int columns_count); + IMGUI_API void TableBeginApplyRequests(ImGuiTable* table); + IMGUI_API void TableSetupDrawChannels(ImGuiTable* table); + IMGUI_API void TableUpdateLayout(ImGuiTable* table); + IMGUI_API void TableUpdateBorders(ImGuiTable* table); + IMGUI_API void TableUpdateColumnsWeightFromWidth(ImGuiTable* table); + IMGUI_API void TableDrawBorders(ImGuiTable* table); + IMGUI_API void TableDrawContextMenu(ImGuiTable* table); + IMGUI_API void TableMergeDrawChannels(ImGuiTable* table); + IMGUI_API void TableSortSpecsSanitize(ImGuiTable* table); + IMGUI_API void TableSortSpecsBuild(ImGuiTable* table); + IMGUI_API ImGuiSortDirection TableGetColumnNextSortDirection(ImGuiTableColumn* column); + IMGUI_API void TableFixColumnSortDirection(ImGuiTable* table, ImGuiTableColumn* column); + IMGUI_API float TableGetColumnWidthAuto(ImGuiTable* table, ImGuiTableColumn* column); + IMGUI_API void TableBeginRow(ImGuiTable* table); + IMGUI_API void TableEndRow(ImGuiTable* table); + IMGUI_API void TableBeginCell(ImGuiTable* table, int column_n); + IMGUI_API void TableEndCell(ImGuiTable* table); + IMGUI_API ImRect TableGetCellBgRect(const ImGuiTable* table, int column_n); + IMGUI_API const char* TableGetColumnName(const ImGuiTable* table, int column_n); + IMGUI_API ImGuiID TableGetColumnResizeID(const ImGuiTable* table, int column_n, int instance_no = 0); + IMGUI_API float TableGetMaxColumnWidth(const ImGuiTable* table, int column_n); + IMGUI_API void TableSetColumnWidthAutoSingle(ImGuiTable* table, int column_n); + IMGUI_API void TableSetColumnWidthAutoAll(ImGuiTable* table); + IMGUI_API void TableRemove(ImGuiTable* table); + IMGUI_API void TableGcCompactTransientBuffers(ImGuiTable* table); + IMGUI_API void TableGcCompactTransientBuffers(ImGuiTableTempData* table); + IMGUI_API void TableGcCompactSettings(); + + // Tables: Settings + IMGUI_API void TableLoadSettings(ImGuiTable* table); + IMGUI_API void TableSaveSettings(ImGuiTable* table); + IMGUI_API void TableResetSettings(ImGuiTable* table); + IMGUI_API ImGuiTableSettings* TableGetBoundSettings(ImGuiTable* table); + IMGUI_API void TableSettingsInstallHandler(ImGuiContext* context); + IMGUI_API ImGuiTableSettings* TableSettingsCreate(ImGuiID id, int columns_count); + IMGUI_API ImGuiTableSettings* TableSettingsFindByID(ImGuiID id); + + // Tab Bars + IMGUI_API bool BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& bb, ImGuiTabBarFlags flags); + IMGUI_API ImGuiTabItem* TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id); + IMGUI_API void TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id); + IMGUI_API void TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); + IMGUI_API void TabBarQueueReorder(ImGuiTabBar* tab_bar, const ImGuiTabItem* tab, int offset); + IMGUI_API void TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, const ImGuiTabItem* tab, ImVec2 mouse_pos); + IMGUI_API bool TabBarProcessReorder(ImGuiTabBar* tab_bar); + IMGUI_API bool TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags); + IMGUI_API ImVec2 TabItemCalcSize(const char* label, bool has_close_button); + IMGUI_API void TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col); + IMGUI_API void TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id, bool is_contents_visible, bool* out_just_closed, bool* out_text_clipped); + + // Render helpers + // AVOID USING OUTSIDE OF IMGUI.CPP! NOT FOR PUBLIC CONSUMPTION. THOSE FUNCTIONS ARE A MESS. THEIR SIGNATURE AND BEHAVIOR WILL CHANGE, THEY NEED TO BE REFACTORED INTO SOMETHING DECENT. + // NB: All position are in absolute pixels coordinates (we are never using window coordinates internally) + IMGUI_API void RenderText(ImVec2 pos, const char* text, const char* text_end = NULL, bool hide_text_after_hash = true); + IMGUI_API void RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end, float wrap_width); + IMGUI_API void RenderTextClipped(const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0, 0), const ImRect* clip_rect = NULL); + IMGUI_API void RenderTextClippedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0, 0), const ImRect* clip_rect = NULL); + IMGUI_API void RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, float clip_max_x, float ellipsis_max_x, const char* text, const char* text_end, const ImVec2* text_size_if_known); + IMGUI_API void RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool border = true, float rounding = 0.0f); + IMGUI_API void RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding = 0.0f); + IMGUI_API void RenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, float grid_step, ImVec2 grid_off, float rounding = 0.0f, ImDrawFlags flags = 0); + IMGUI_API void RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavHighlightFlags flags = ImGuiNavHighlightFlags_TypeDefault); // Navigation highlight + IMGUI_API const char* FindRenderedTextEnd(const char* text, const char* text_end = NULL); // Find the optional ## from which we stop displaying text. + + // Render helpers (those functions don't access any ImGui state!) + IMGUI_API void RenderArrow(ImDrawList* draw_list, ImVec2 pos, ImU32 col, ImGuiDir dir, float scale = 1.0f); + IMGUI_API void RenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col); + IMGUI_API void RenderCheckMark(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz); + IMGUI_API void RenderMouseCursor(ImDrawList* draw_list, ImVec2 pos, float scale, ImGuiMouseCursor mouse_cursor, ImU32 col_fill, ImU32 col_border, ImU32 col_shadow); + IMGUI_API void RenderArrowPointingAt(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, ImGuiDir direction, ImU32 col); + IMGUI_API void RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding); + IMGUI_API void RenderRectFilledWithHole(ImDrawList* draw_list, ImRect outer, ImRect inner, ImU32 col, float rounding); + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + // [1.71: 2019/06/07: Updating prototypes of some of the internal functions. Leaving those for reference for a short while] + inline void RenderArrow(ImVec2 pos, ImGuiDir dir, float scale=1.0f) { ImGuiWindow* window = GetCurrentWindow(); RenderArrow(window->DrawList, pos, GetColorU32(ImGuiCol_Text), dir, scale); } + inline void RenderBullet(ImVec2 pos) { ImGuiWindow* window = GetCurrentWindow(); RenderBullet(window->DrawList, pos, GetColorU32(ImGuiCol_Text)); } +#endif + + // Widgets + IMGUI_API void TextEx(const char* text, const char* text_end = NULL, ImGuiTextFlags flags = 0); + IMGUI_API bool ButtonEx(const char* label, const ImVec2& size_arg = ImVec2(0, 0), ImGuiButtonFlags flags = 0); + IMGUI_API bool CloseButton(ImGuiID id, const ImVec2& pos); + IMGUI_API bool CollapseButton(ImGuiID id, const ImVec2& pos); + IMGUI_API bool ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size_arg, ImGuiButtonFlags flags = 0); + IMGUI_API void Scrollbar(ImGuiAxis axis); + IMGUI_API bool ScrollbarEx(const ImRect& bb, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 avail_v, ImS64 contents_v, ImDrawFlags flags); + IMGUI_API bool ImageButtonEx(ImGuiID id, ImTextureID texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec2& padding, const ImVec4& bg_col, const ImVec4& tint_col); + IMGUI_API ImRect GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis); + IMGUI_API ImGuiID GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis); + IMGUI_API ImGuiID GetWindowResizeCornerID(ImGuiWindow* window, int n); // 0..3: corners + IMGUI_API ImGuiID GetWindowResizeBorderID(ImGuiWindow* window, ImGuiDir dir); + IMGUI_API void SeparatorEx(ImGuiSeparatorFlags flags); + IMGUI_API bool CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value); + IMGUI_API bool CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value); + + // Widgets low-level behaviors + IMGUI_API bool ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags = 0); + IMGUI_API bool DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags); + IMGUI_API bool SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb); + IMGUI_API bool SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend = 0.0f, float hover_visibility_delay = 0.0f); + IMGUI_API bool TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end = NULL); + IMGUI_API bool TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags = 0); // Consume previous SetNextItemOpen() data, if any. May return true when logging + IMGUI_API void TreePushOverrideID(ImGuiID id); + + // Template functions are instantiated in imgui_widgets.cpp for a finite number of types. + // To use them externally (for custom widget) you may need an "extern template" statement in your code in order to link to existing instances and silence Clang warnings (see #2036). + // e.g. " extern template IMGUI_API float RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, float v); " + template IMGUI_API float ScaleRatioFromValueT(ImGuiDataType data_type, T v, T v_min, T v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_size); + template IMGUI_API T ScaleValueFromRatioT(ImGuiDataType data_type, float t, T v_min, T v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_size); + template IMGUI_API bool DragBehaviorT(ImGuiDataType data_type, T* v, float v_speed, T v_min, T v_max, const char* format, ImGuiSliderFlags flags); + template IMGUI_API bool SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, T* v, T v_min, T v_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb); + template IMGUI_API T RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, T v); + template IMGUI_API bool CheckboxFlagsT(const char* label, T* flags, T flags_value); + + // Data type helpers + IMGUI_API const ImGuiDataTypeInfo* DataTypeGetInfo(ImGuiDataType data_type); + IMGUI_API int DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format); + IMGUI_API void DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg_1, const void* arg_2); + IMGUI_API bool DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format); + IMGUI_API int DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2); + IMGUI_API bool DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max); + + // InputText + IMGUI_API bool InputTextEx(const char* label, const char* hint, char* buf, int buf_size, const ImVec2& size_arg, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags); + IMGUI_API bool TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data, const char* format, const void* p_clamp_min = NULL, const void* p_clamp_max = NULL); + inline bool TempInputIsActive(ImGuiID id) { ImGuiContext& g = *GImGui; return (g.ActiveId == id && g.TempInputId == id); } + inline ImGuiInputTextState* GetInputTextState(ImGuiID id) { ImGuiContext& g = *GImGui; return (g.InputTextState.ID == id) ? &g.InputTextState : NULL; } // Get input text state if active + + // Color + IMGUI_API void ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags); + IMGUI_API void ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags); + IMGUI_API void ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags); + + // Plot + IMGUI_API int PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 frame_size); + + // Shade functions (write over already created vertices) + IMGUI_API void ShadeVertsLinearColorGradientKeepAlpha(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, ImVec2 gradient_p0, ImVec2 gradient_p1, ImU32 col0, ImU32 col1); + IMGUI_API void ShadeVertsLinearUV(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, bool clamp); + + // Garbage collection + IMGUI_API void GcCompactTransientMiscBuffers(); + IMGUI_API void GcCompactTransientWindowBuffers(ImGuiWindow* window); + IMGUI_API void GcAwakeTransientWindowBuffers(ImGuiWindow* window); + + // Debug Tools + IMGUI_API void ErrorCheckEndFrameRecover(ImGuiErrorLogCallback log_callback, void* user_data = NULL); + IMGUI_API void ErrorCheckEndWindowRecover(ImGuiErrorLogCallback log_callback, void* user_data = NULL); + inline void DebugDrawItemRect(ImU32 col = IM_COL32(255,0,0,255)) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; GetForegroundDrawList(window)->AddRect(g.LastItemData.Rect.Min, g.LastItemData.Rect.Max, col); } + inline void DebugStartItemPicker() { ImGuiContext& g = *GImGui; g.DebugItemPickerActive = true; } + + IMGUI_API void ShowFontAtlas(ImFontAtlas* atlas); + IMGUI_API void DebugHookIdInfo(ImGuiID id, ImGuiDataType data_type, const void* data_id, const void* data_id_end); + IMGUI_API void DebugNodeColumns(ImGuiOldColumns* columns); + IMGUI_API void DebugNodeDrawList(ImGuiWindow* window, const ImDrawList* draw_list, const char* label); + IMGUI_API void DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList* out_draw_list, const ImDrawList* draw_list, const ImDrawCmd* draw_cmd, bool show_mesh, bool show_aabb); + IMGUI_API void DebugNodeFont(ImFont* font); + IMGUI_API void DebugNodeStorage(ImGuiStorage* storage, const char* label); + IMGUI_API void DebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label); + IMGUI_API void DebugNodeTable(ImGuiTable* table); + IMGUI_API void DebugNodeTableSettings(ImGuiTableSettings* settings); + IMGUI_API void DebugNodeWindow(ImGuiWindow* window, const char* label); + IMGUI_API void DebugNodeWindowSettings(ImGuiWindowSettings* settings); + IMGUI_API void DebugNodeWindowsList(ImVector* windows, const char* label); + IMGUI_API void DebugNodeWindowsListByBeginStackParent(ImGuiWindow** windows, int windows_size, ImGuiWindow* parent_in_begin_stack); + IMGUI_API void DebugNodeViewport(ImGuiViewportP* viewport); + IMGUI_API void DebugRenderViewportThumbnail(ImDrawList* draw_list, ImGuiViewportP* viewport, const ImRect& bb); + +} // namespace ImGui + + +//----------------------------------------------------------------------------- +// [SECTION] ImFontAtlas internal API +//----------------------------------------------------------------------------- + +// This structure is likely to evolve as we add support for incremental atlas updates +struct ImFontBuilderIO +{ + bool (*FontBuilder_Build)(ImFontAtlas* atlas); +}; + +// Helper for font builder +#ifdef IMGUI_ENABLE_STB_TRUETYPE +IMGUI_API const ImFontBuilderIO* ImFontAtlasGetBuilderForStbTruetype(); +#endif +IMGUI_API void ImFontAtlasBuildInit(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent); +IMGUI_API void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque); +IMGUI_API void ImFontAtlasBuildFinish(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildRender8bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned char in_marker_pixel_value); +IMGUI_API void ImFontAtlasBuildRender32bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned int in_marker_pixel_value); +IMGUI_API void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_multiply_factor); +IMGUI_API void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride); + +//----------------------------------------------------------------------------- +// [SECTION] Test Engine specific hooks (imgui_test_engine) +//----------------------------------------------------------------------------- + +#ifdef IMGUI_ENABLE_TEST_ENGINE +extern void ImGuiTestEngineHook_ItemAdd(ImGuiContext* ctx, const ImRect& bb, ImGuiID id); +extern void ImGuiTestEngineHook_ItemInfo(ImGuiContext* ctx, ImGuiID id, const char* label, ImGuiItemStatusFlags flags); +extern void ImGuiTestEngineHook_Log(ImGuiContext* ctx, const char* fmt, ...); +extern const char* ImGuiTestEngine_FindItemDebugLabel(ImGuiContext* ctx, ImGuiID id); + +#define IMGUI_TEST_ENGINE_ITEM_ADD(_BB,_ID) if (g.TestEngineHookItems) ImGuiTestEngineHook_ItemAdd(&g, _BB, _ID) // Register item bounding box +#define IMGUI_TEST_ENGINE_ITEM_INFO(_ID,_LABEL,_FLAGS) if (g.TestEngineHookItems) ImGuiTestEngineHook_ItemInfo(&g, _ID, _LABEL, _FLAGS) // Register item label and status flags (optional) +#define IMGUI_TEST_ENGINE_LOG(_FMT,...) if (g.TestEngineHookItems) ImGuiTestEngineHook_Log(&g, _FMT, __VA_ARGS__) // Custom log entry from user land into test log +#else +#define IMGUI_TEST_ENGINE_ITEM_ADD(_BB,_ID) ((void)0) +#define IMGUI_TEST_ENGINE_ITEM_INFO(_ID,_LABEL,_FLAGS) ((void)g) +#endif + +//----------------------------------------------------------------------------- + +#if defined(__clang__) +#pragma clang diagnostic pop +#elif defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +#ifdef _MSC_VER +#pragma warning (pop) +#endif + +#endif // #ifndef IMGUI_DISABLE + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Include/imstb_textedit.h`: + +```h +// [DEAR IMGUI] +// This is a slightly modified version of stb_textedit.h 1.13. +// Those changes would need to be pushed into nothings/stb: +// - Fix in stb_textedit_discard_redo (see https://github.com/nothings/stb/issues/321) +// Grep for [DEAR IMGUI] to find the changes. + +// stb_textedit.h - v1.13 - public domain - Sean Barrett +// Development of this library was sponsored by RAD Game Tools +// +// This C header file implements the guts of a multi-line text-editing +// widget; you implement display, word-wrapping, and low-level string +// insertion/deletion, and stb_textedit will map user inputs into +// insertions & deletions, plus updates to the cursor position, +// selection state, and undo state. +// +// It is intended for use in games and other systems that need to build +// their own custom widgets and which do not have heavy text-editing +// requirements (this library is not recommended for use for editing large +// texts, as its performance does not scale and it has limited undo). +// +// Non-trivial behaviors are modelled after Windows text controls. +// +// +// LICENSE +// +// See end of file for license information. +// +// +// DEPENDENCIES +// +// Uses the C runtime function 'memmove', which you can override +// by defining STB_TEXTEDIT_memmove before the implementation. +// Uses no other functions. Performs no runtime allocations. +// +// +// VERSION HISTORY +// +// 1.13 (2019-02-07) fix bug in undo size management +// 1.12 (2018-01-29) user can change STB_TEXTEDIT_KEYTYPE, fix redo to avoid crash +// 1.11 (2017-03-03) fix HOME on last line, dragging off single-line textfield +// 1.10 (2016-10-25) supress warnings about casting away const with -Wcast-qual +// 1.9 (2016-08-27) customizable move-by-word +// 1.8 (2016-04-02) better keyboard handling when mouse button is down +// 1.7 (2015-09-13) change y range handling in case baseline is non-0 +// 1.6 (2015-04-15) allow STB_TEXTEDIT_memmove +// 1.5 (2014-09-10) add support for secondary keys for OS X +// 1.4 (2014-08-17) fix signed/unsigned warnings +// 1.3 (2014-06-19) fix mouse clicking to round to nearest char boundary +// 1.2 (2014-05-27) fix some RAD types that had crept into the new code +// 1.1 (2013-12-15) move-by-word (requires STB_TEXTEDIT_IS_SPACE ) +// 1.0 (2012-07-26) improve documentation, initial public release +// 0.3 (2012-02-24) bugfixes, single-line mode; insert mode +// 0.2 (2011-11-28) fixes to undo/redo +// 0.1 (2010-07-08) initial version +// +// ADDITIONAL CONTRIBUTORS +// +// Ulf Winklemann: move-by-word in 1.1 +// Fabian Giesen: secondary key inputs in 1.5 +// Martins Mozeiko: STB_TEXTEDIT_memmove in 1.6 +// +// Bugfixes: +// Scott Graham +// Daniel Keller +// Omar Cornut +// Dan Thompson +// +// USAGE +// +// This file behaves differently depending on what symbols you define +// before including it. +// +// +// Header-file mode: +// +// If you do not define STB_TEXTEDIT_IMPLEMENTATION before including this, +// it will operate in "header file" mode. In this mode, it declares a +// single public symbol, STB_TexteditState, which encapsulates the current +// state of a text widget (except for the string, which you will store +// separately). +// +// To compile in this mode, you must define STB_TEXTEDIT_CHARTYPE to a +// primitive type that defines a single character (e.g. char, wchar_t, etc). +// +// To save space or increase undo-ability, you can optionally define the +// following things that are used by the undo system: +// +// STB_TEXTEDIT_POSITIONTYPE small int type encoding a valid cursor position +// STB_TEXTEDIT_UNDOSTATECOUNT the number of undo states to allow +// STB_TEXTEDIT_UNDOCHARCOUNT the number of characters to store in the undo buffer +// +// If you don't define these, they are set to permissive types and +// moderate sizes. The undo system does no memory allocations, so +// it grows STB_TexteditState by the worst-case storage which is (in bytes): +// +// [4 + 3 * sizeof(STB_TEXTEDIT_POSITIONTYPE)] * STB_TEXTEDIT_UNDOSTATE_COUNT +// + sizeof(STB_TEXTEDIT_CHARTYPE) * STB_TEXTEDIT_UNDOCHAR_COUNT +// +// +// Implementation mode: +// +// If you define STB_TEXTEDIT_IMPLEMENTATION before including this, it +// will compile the implementation of the text edit widget, depending +// on a large number of symbols which must be defined before the include. +// +// The implementation is defined only as static functions. You will then +// need to provide your own APIs in the same file which will access the +// static functions. +// +// The basic concept is that you provide a "string" object which +// behaves like an array of characters. stb_textedit uses indices to +// refer to positions in the string, implicitly representing positions +// in the displayed textedit. This is true for both plain text and +// rich text; even with rich text stb_truetype interacts with your +// code as if there was an array of all the displayed characters. +// +// Symbols that must be the same in header-file and implementation mode: +// +// STB_TEXTEDIT_CHARTYPE the character type +// STB_TEXTEDIT_POSITIONTYPE small type that is a valid cursor position +// STB_TEXTEDIT_UNDOSTATECOUNT the number of undo states to allow +// STB_TEXTEDIT_UNDOCHARCOUNT the number of characters to store in the undo buffer +// +// Symbols you must define for implementation mode: +// +// STB_TEXTEDIT_STRING the type of object representing a string being edited, +// typically this is a wrapper object with other data you need +// +// STB_TEXTEDIT_STRINGLEN(obj) the length of the string (ideally O(1)) +// STB_TEXTEDIT_LAYOUTROW(&r,obj,n) returns the results of laying out a line of characters +// starting from character #n (see discussion below) +// STB_TEXTEDIT_GETWIDTH(obj,n,i) returns the pixel delta from the xpos of the i'th character +// to the xpos of the i+1'th char for a line of characters +// starting at character #n (i.e. accounts for kerning +// with previous char) +// STB_TEXTEDIT_KEYTOTEXT(k) maps a keyboard input to an insertable character +// (return type is int, -1 means not valid to insert) +// STB_TEXTEDIT_GETCHAR(obj,i) returns the i'th character of obj, 0-based +// STB_TEXTEDIT_NEWLINE the character returned by _GETCHAR() we recognize +// as manually wordwrapping for end-of-line positioning +// +// STB_TEXTEDIT_DELETECHARS(obj,i,n) delete n characters starting at i +// STB_TEXTEDIT_INSERTCHARS(obj,i,c*,n) insert n characters at i (pointed to by STB_TEXTEDIT_CHARTYPE*) +// +// STB_TEXTEDIT_K_SHIFT a power of two that is or'd in to a keyboard input to represent the shift key +// +// STB_TEXTEDIT_K_LEFT keyboard input to move cursor left +// STB_TEXTEDIT_K_RIGHT keyboard input to move cursor right +// STB_TEXTEDIT_K_UP keyboard input to move cursor up +// STB_TEXTEDIT_K_DOWN keyboard input to move cursor down +// STB_TEXTEDIT_K_PGUP keyboard input to move cursor up a page +// STB_TEXTEDIT_K_PGDOWN keyboard input to move cursor down a page +// STB_TEXTEDIT_K_LINESTART keyboard input to move cursor to start of line // e.g. HOME +// STB_TEXTEDIT_K_LINEEND keyboard input to move cursor to end of line // e.g. END +// STB_TEXTEDIT_K_TEXTSTART keyboard input to move cursor to start of text // e.g. ctrl-HOME +// STB_TEXTEDIT_K_TEXTEND keyboard input to move cursor to end of text // e.g. ctrl-END +// STB_TEXTEDIT_K_DELETE keyboard input to delete selection or character under cursor +// STB_TEXTEDIT_K_BACKSPACE keyboard input to delete selection or character left of cursor +// STB_TEXTEDIT_K_UNDO keyboard input to perform undo +// STB_TEXTEDIT_K_REDO keyboard input to perform redo +// +// Optional: +// STB_TEXTEDIT_K_INSERT keyboard input to toggle insert mode +// STB_TEXTEDIT_IS_SPACE(ch) true if character is whitespace (e.g. 'isspace'), +// required for default WORDLEFT/WORDRIGHT handlers +// STB_TEXTEDIT_MOVEWORDLEFT(obj,i) custom handler for WORDLEFT, returns index to move cursor to +// STB_TEXTEDIT_MOVEWORDRIGHT(obj,i) custom handler for WORDRIGHT, returns index to move cursor to +// STB_TEXTEDIT_K_WORDLEFT keyboard input to move cursor left one word // e.g. ctrl-LEFT +// STB_TEXTEDIT_K_WORDRIGHT keyboard input to move cursor right one word // e.g. ctrl-RIGHT +// STB_TEXTEDIT_K_LINESTART2 secondary keyboard input to move cursor to start of line +// STB_TEXTEDIT_K_LINEEND2 secondary keyboard input to move cursor to end of line +// STB_TEXTEDIT_K_TEXTSTART2 secondary keyboard input to move cursor to start of text +// STB_TEXTEDIT_K_TEXTEND2 secondary keyboard input to move cursor to end of text +// +// Keyboard input must be encoded as a single integer value; e.g. a character code +// and some bitflags that represent shift states. to simplify the interface, SHIFT must +// be a bitflag, so we can test the shifted state of cursor movements to allow selection, +// i.e. (STB_TEXTEDIT_K_RIGHT|STB_TEXTEDIT_K_SHIFT) should be shifted right-arrow. +// +// You can encode other things, such as CONTROL or ALT, in additional bits, and +// then test for their presence in e.g. STB_TEXTEDIT_K_WORDLEFT. For example, +// my Windows implementations add an additional CONTROL bit, and an additional KEYDOWN +// bit. Then all of the STB_TEXTEDIT_K_ values bitwise-or in the KEYDOWN bit, +// and I pass both WM_KEYDOWN and WM_CHAR events to the "key" function in the +// API below. The control keys will only match WM_KEYDOWN events because of the +// keydown bit I add, and STB_TEXTEDIT_KEYTOTEXT only tests for the KEYDOWN +// bit so it only decodes WM_CHAR events. +// +// STB_TEXTEDIT_LAYOUTROW returns information about the shape of one displayed +// row of characters assuming they start on the i'th character--the width and +// the height and the number of characters consumed. This allows this library +// to traverse the entire layout incrementally. You need to compute word-wrapping +// here. +// +// Each textfield keeps its own insert mode state, which is not how normal +// applications work. To keep an app-wide insert mode, update/copy the +// "insert_mode" field of STB_TexteditState before/after calling API functions. +// +// API +// +// void stb_textedit_initialize_state(STB_TexteditState *state, int is_single_line) +// +// void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +// void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +// int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +// int stb_textedit_paste(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE *text, int len) +// void stb_textedit_key(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXEDIT_KEYTYPE key) +// +// Each of these functions potentially updates the string and updates the +// state. +// +// initialize_state: +// set the textedit state to a known good default state when initially +// constructing the textedit. +// +// click: +// call this with the mouse x,y on a mouse down; it will update the cursor +// and reset the selection start/end to the cursor point. the x,y must +// be relative to the text widget, with (0,0) being the top left. +// +// drag: +// call this with the mouse x,y on a mouse drag/up; it will update the +// cursor and the selection end point +// +// cut: +// call this to delete the current selection; returns true if there was +// one. you should FIRST copy the current selection to the system paste buffer. +// (To copy, just copy the current selection out of the string yourself.) +// +// paste: +// call this to paste text at the current cursor point or over the current +// selection if there is one. +// +// key: +// call this for keyboard inputs sent to the textfield. you can use it +// for "key down" events or for "translated" key events. if you need to +// do both (as in Win32), or distinguish Unicode characters from control +// inputs, set a high bit to distinguish the two; then you can define the +// various definitions like STB_TEXTEDIT_K_LEFT have the is-key-event bit +// set, and make STB_TEXTEDIT_KEYTOCHAR check that the is-key-event bit is +// clear. STB_TEXTEDIT_KEYTYPE defaults to int, but you can #define it to +// anything other type you wante before including. +// +// +// When rendering, you can read the cursor position and selection state from +// the STB_TexteditState. +// +// +// Notes: +// +// This is designed to be usable in IMGUI, so it allows for the possibility of +// running in an IMGUI that has NOT cached the multi-line layout. For this +// reason, it provides an interface that is compatible with computing the +// layout incrementally--we try to make sure we make as few passes through +// as possible. (For example, to locate the mouse pointer in the text, we +// could define functions that return the X and Y positions of characters +// and binary search Y and then X, but if we're doing dynamic layout this +// will run the layout algorithm many times, so instead we manually search +// forward in one pass. Similar logic applies to e.g. up-arrow and +// down-arrow movement.) +// +// If it's run in a widget that *has* cached the layout, then this is less +// efficient, but it's not horrible on modern computers. But you wouldn't +// want to edit million-line files with it. + + +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +//// +//// Header-file mode +//// +//// + +#ifndef INCLUDE_STB_TEXTEDIT_H +#define INCLUDE_STB_TEXTEDIT_H + +//////////////////////////////////////////////////////////////////////// +// +// STB_TexteditState +// +// Definition of STB_TexteditState which you should store +// per-textfield; it includes cursor position, selection state, +// and undo state. +// + +#ifndef STB_TEXTEDIT_UNDOSTATECOUNT +#define STB_TEXTEDIT_UNDOSTATECOUNT 99 +#endif +#ifndef STB_TEXTEDIT_UNDOCHARCOUNT +#define STB_TEXTEDIT_UNDOCHARCOUNT 999 +#endif +#ifndef STB_TEXTEDIT_CHARTYPE +#define STB_TEXTEDIT_CHARTYPE int +#endif +#ifndef STB_TEXTEDIT_POSITIONTYPE +#define STB_TEXTEDIT_POSITIONTYPE int +#endif + +typedef struct +{ + // private data + STB_TEXTEDIT_POSITIONTYPE where; + STB_TEXTEDIT_POSITIONTYPE insert_length; + STB_TEXTEDIT_POSITIONTYPE delete_length; + int char_storage; +} StbUndoRecord; + +typedef struct +{ + // private data + StbUndoRecord undo_rec [STB_TEXTEDIT_UNDOSTATECOUNT]; + STB_TEXTEDIT_CHARTYPE undo_char[STB_TEXTEDIT_UNDOCHARCOUNT]; + short undo_point, redo_point; + int undo_char_point, redo_char_point; +} StbUndoState; + +typedef struct +{ + ///////////////////// + // + // public data + // + + int cursor; + // position of the text cursor within the string + + int select_start; // selection start point + int select_end; + // selection start and end point in characters; if equal, no selection. + // note that start may be less than or greater than end (e.g. when + // dragging the mouse, start is where the initial click was, and you + // can drag in either direction) + + unsigned char insert_mode; + // each textfield keeps its own insert mode state. to keep an app-wide + // insert mode, copy this value in/out of the app state + + int row_count_per_page; + // page size in number of row. + // this value MUST be set to >0 for pageup or pagedown in multilines documents. + + ///////////////////// + // + // private data + // + unsigned char cursor_at_end_of_line; // not implemented yet + unsigned char initialized; + unsigned char has_preferred_x; + unsigned char single_line; + unsigned char padding1, padding2, padding3; + float preferred_x; // this determines where the cursor up/down tries to seek to along x + StbUndoState undostate; +} STB_TexteditState; + + +//////////////////////////////////////////////////////////////////////// +// +// StbTexteditRow +// +// Result of layout query, used by stb_textedit to determine where +// the text in each row is. + +// result of layout query +typedef struct +{ + float x0,x1; // starting x location, end x location (allows for align=right, etc) + float baseline_y_delta; // position of baseline relative to previous row's baseline + float ymin,ymax; // height of row above and below baseline + int num_chars; +} StbTexteditRow; +#endif //INCLUDE_STB_TEXTEDIT_H + + +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +//// +//// Implementation mode +//// +//// + + +// implementation isn't include-guarded, since it might have indirectly +// included just the "header" portion +#ifdef STB_TEXTEDIT_IMPLEMENTATION + +#ifndef STB_TEXTEDIT_memmove +#include +#define STB_TEXTEDIT_memmove memmove +#endif + + +///////////////////////////////////////////////////////////////////////////// +// +// Mouse input handling +// + +// traverse the layout to locate the nearest character to a display position +static int stb_text_locate_coord(STB_TEXTEDIT_STRING *str, float x, float y) +{ + StbTexteditRow r; + int n = STB_TEXTEDIT_STRINGLEN(str); + float base_y = 0, prev_x; + int i=0, k; + + r.x0 = r.x1 = 0; + r.ymin = r.ymax = 0; + r.num_chars = 0; + + // search rows to find one that straddles 'y' + while (i < n) { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + if (r.num_chars <= 0) + return n; + + if (i==0 && y < base_y + r.ymin) + return 0; + + if (y < base_y + r.ymax) + break; + + i += r.num_chars; + base_y += r.baseline_y_delta; + } + + // below all text, return 'after' last character + if (i >= n) + return n; + + // check if it's before the beginning of the line + if (x < r.x0) + return i; + + // check if it's before the end of the line + if (x < r.x1) { + // search characters in row for one that straddles 'x' + prev_x = r.x0; + for (k=0; k < r.num_chars; ++k) { + float w = STB_TEXTEDIT_GETWIDTH(str, i, k); + if (x < prev_x+w) { + if (x < prev_x+w/2) + return k+i; + else + return k+i+1; + } + prev_x += w; + } + // shouldn't happen, but if it does, fall through to end-of-line case + } + + // if the last character is a newline, return that. otherwise return 'after' the last character + if (STB_TEXTEDIT_GETCHAR(str, i+r.num_chars-1) == STB_TEXTEDIT_NEWLINE) + return i+r.num_chars-1; + else + return i+r.num_chars; +} + +// API click: on mouse down, move the cursor to the clicked location, and reset the selection +static void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +{ + // In single-line mode, just always make y = 0. This lets the drag keep working if the mouse + // goes off the top or bottom of the text + if( state->single_line ) + { + StbTexteditRow r; + STB_TEXTEDIT_LAYOUTROW(&r, str, 0); + y = r.ymin; + } + + state->cursor = stb_text_locate_coord(str, x, y); + state->select_start = state->cursor; + state->select_end = state->cursor; + state->has_preferred_x = 0; +} + +// API drag: on mouse drag, move the cursor and selection endpoint to the clicked location +static void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +{ + int p = 0; + + // In single-line mode, just always make y = 0. This lets the drag keep working if the mouse + // goes off the top or bottom of the text + if( state->single_line ) + { + StbTexteditRow r; + STB_TEXTEDIT_LAYOUTROW(&r, str, 0); + y = r.ymin; + } + + if (state->select_start == state->select_end) + state->select_start = state->cursor; + + p = stb_text_locate_coord(str, x, y); + state->cursor = state->select_end = p; +} + +///////////////////////////////////////////////////////////////////////////// +// +// Keyboard input handling +// + +// forward declarations +static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state); +static void stb_text_redo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state); +static void stb_text_makeundo_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length); +static void stb_text_makeundo_insert(STB_TexteditState *state, int where, int length); +static void stb_text_makeundo_replace(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length); + +typedef struct +{ + float x,y; // position of n'th character + float height; // height of line + int first_char, length; // first char of row, and length + int prev_first; // first char of previous row +} StbFindState; + +// find the x/y location of a character, and remember info about the previous row in +// case we get a move-up event (for page up, we'll have to rescan) +static void stb_textedit_find_charpos(StbFindState *find, STB_TEXTEDIT_STRING *str, int n, int single_line) +{ + StbTexteditRow r; + int prev_start = 0; + int z = STB_TEXTEDIT_STRINGLEN(str); + int i=0, first; + + if (n == z) { + // if it's at the end, then find the last line -- simpler than trying to + // explicitly handle this case in the regular code + if (single_line) { + STB_TEXTEDIT_LAYOUTROW(&r, str, 0); + find->y = 0; + find->first_char = 0; + find->length = z; + find->height = r.ymax - r.ymin; + find->x = r.x1; + } else { + find->y = 0; + find->x = 0; + find->height = 1; + while (i < z) { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + prev_start = i; + i += r.num_chars; + } + find->first_char = i; + find->length = 0; + find->prev_first = prev_start; + } + return; + } + + // search rows to find the one that straddles character n + find->y = 0; + + for(;;) { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + if (n < i + r.num_chars) + break; + prev_start = i; + i += r.num_chars; + find->y += r.baseline_y_delta; + } + + find->first_char = first = i; + find->length = r.num_chars; + find->height = r.ymax - r.ymin; + find->prev_first = prev_start; + + // now scan to find xpos + find->x = r.x0; + for (i=0; first+i < n; ++i) + find->x += STB_TEXTEDIT_GETWIDTH(str, first, i); +} + +#define STB_TEXT_HAS_SELECTION(s) ((s)->select_start != (s)->select_end) + +// make the selection/cursor state valid if client altered the string +static void stb_textedit_clamp(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + int n = STB_TEXTEDIT_STRINGLEN(str); + if (STB_TEXT_HAS_SELECTION(state)) { + if (state->select_start > n) state->select_start = n; + if (state->select_end > n) state->select_end = n; + // if clamping forced them to be equal, move the cursor to match + if (state->select_start == state->select_end) + state->cursor = state->select_start; + } + if (state->cursor > n) state->cursor = n; +} + +// delete characters while updating undo +static void stb_textedit_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int len) +{ + stb_text_makeundo_delete(str, state, where, len); + STB_TEXTEDIT_DELETECHARS(str, where, len); + state->has_preferred_x = 0; +} + +// delete the section +static void stb_textedit_delete_selection(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + stb_textedit_clamp(str, state); + if (STB_TEXT_HAS_SELECTION(state)) { + if (state->select_start < state->select_end) { + stb_textedit_delete(str, state, state->select_start, state->select_end - state->select_start); + state->select_end = state->cursor = state->select_start; + } else { + stb_textedit_delete(str, state, state->select_end, state->select_start - state->select_end); + state->select_start = state->cursor = state->select_end; + } + state->has_preferred_x = 0; + } +} + +// canoncialize the selection so start <= end +static void stb_textedit_sortselection(STB_TexteditState *state) +{ + if (state->select_end < state->select_start) { + int temp = state->select_end; + state->select_end = state->select_start; + state->select_start = temp; + } +} + +// move cursor to first character of selection +static void stb_textedit_move_to_first(STB_TexteditState *state) +{ + if (STB_TEXT_HAS_SELECTION(state)) { + stb_textedit_sortselection(state); + state->cursor = state->select_start; + state->select_end = state->select_start; + state->has_preferred_x = 0; + } +} + +// move cursor to last character of selection +static void stb_textedit_move_to_last(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + if (STB_TEXT_HAS_SELECTION(state)) { + stb_textedit_sortselection(state); + stb_textedit_clamp(str, state); + state->cursor = state->select_end; + state->select_start = state->select_end; + state->has_preferred_x = 0; + } +} + +#ifdef STB_TEXTEDIT_IS_SPACE +static int is_word_boundary( STB_TEXTEDIT_STRING *str, int idx ) +{ + return idx > 0 ? (STB_TEXTEDIT_IS_SPACE( STB_TEXTEDIT_GETCHAR(str,idx-1) ) && !STB_TEXTEDIT_IS_SPACE( STB_TEXTEDIT_GETCHAR(str, idx) ) ) : 1; +} + +#ifndef STB_TEXTEDIT_MOVEWORDLEFT +static int stb_textedit_move_to_word_previous( STB_TEXTEDIT_STRING *str, int c ) +{ + --c; // always move at least one character + while( c >= 0 && !is_word_boundary( str, c ) ) + --c; + + if( c < 0 ) + c = 0; + + return c; +} +#define STB_TEXTEDIT_MOVEWORDLEFT stb_textedit_move_to_word_previous +#endif + +#ifndef STB_TEXTEDIT_MOVEWORDRIGHT +static int stb_textedit_move_to_word_next( STB_TEXTEDIT_STRING *str, int c ) +{ + const int len = STB_TEXTEDIT_STRINGLEN(str); + ++c; // always move at least one character + while( c < len && !is_word_boundary( str, c ) ) + ++c; + + if( c > len ) + c = len; + + return c; +} +#define STB_TEXTEDIT_MOVEWORDRIGHT stb_textedit_move_to_word_next +#endif + +#endif + +// update selection and cursor to match each other +static void stb_textedit_prep_selection_at_cursor(STB_TexteditState *state) +{ + if (!STB_TEXT_HAS_SELECTION(state)) + state->select_start = state->select_end = state->cursor; + else + state->cursor = state->select_end; +} + +// API cut: delete selection +static int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + if (STB_TEXT_HAS_SELECTION(state)) { + stb_textedit_delete_selection(str,state); // implicitly clamps + state->has_preferred_x = 0; + return 1; + } + return 0; +} + +// API paste: replace existing selection with passed-in text +static int stb_textedit_paste_internal(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE *text, int len) +{ + // if there's a selection, the paste should delete it + stb_textedit_clamp(str, state); + stb_textedit_delete_selection(str,state); + // try to insert the characters + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, len)) { + stb_text_makeundo_insert(state, state->cursor, len); + state->cursor += len; + state->has_preferred_x = 0; + return 1; + } + // [DEAR IMGUI] + //// remove the undo since we didn't actually insert the characters + //if (state->undostate.undo_point) + // --state->undostate.undo_point; + // note: paste failure will leave deleted selection, may be restored with an undo (see https://github.com/nothings/stb/issues/734 for details) + return 0; +} + +#ifndef STB_TEXTEDIT_KEYTYPE +#define STB_TEXTEDIT_KEYTYPE int +#endif + +// API key: process a keyboard input +static void stb_textedit_key(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_KEYTYPE key) +{ +retry: + switch (key) { + default: { + int c = STB_TEXTEDIT_KEYTOTEXT(key); + if (c > 0) { + STB_TEXTEDIT_CHARTYPE ch = (STB_TEXTEDIT_CHARTYPE) c; + + // can't add newline in single-line mode + if (c == '\n' && state->single_line) + break; + + if (state->insert_mode && !STB_TEXT_HAS_SELECTION(state) && state->cursor < STB_TEXTEDIT_STRINGLEN(str)) { + stb_text_makeundo_replace(str, state, state->cursor, 1, 1); + STB_TEXTEDIT_DELETECHARS(str, state->cursor, 1); + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) { + ++state->cursor; + state->has_preferred_x = 0; + } + } else { + stb_textedit_delete_selection(str,state); // implicitly clamps + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) { + stb_text_makeundo_insert(state, state->cursor, 1); + ++state->cursor; + state->has_preferred_x = 0; + } + } + } + break; + } + +#ifdef STB_TEXTEDIT_K_INSERT + case STB_TEXTEDIT_K_INSERT: + state->insert_mode = !state->insert_mode; + break; +#endif + + case STB_TEXTEDIT_K_UNDO: + stb_text_undo(str, state); + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_REDO: + stb_text_redo(str, state); + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_LEFT: + // if currently there's a selection, move cursor to start of selection + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_first(state); + else + if (state->cursor > 0) + --state->cursor; + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_RIGHT: + // if currently there's a selection, move cursor to end of selection + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_last(str, state); + else + ++state->cursor; + stb_textedit_clamp(str, state); + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_LEFT | STB_TEXTEDIT_K_SHIFT: + stb_textedit_clamp(str, state); + stb_textedit_prep_selection_at_cursor(state); + // move selection left + if (state->select_end > 0) + --state->select_end; + state->cursor = state->select_end; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_MOVEWORDLEFT + case STB_TEXTEDIT_K_WORDLEFT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_first(state); + else { + state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor); + stb_textedit_clamp( str, state ); + } + break; + + case STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT: + if( !STB_TEXT_HAS_SELECTION( state ) ) + stb_textedit_prep_selection_at_cursor(state); + + state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor); + state->select_end = state->cursor; + + stb_textedit_clamp( str, state ); + break; +#endif + +#ifdef STB_TEXTEDIT_MOVEWORDRIGHT + case STB_TEXTEDIT_K_WORDRIGHT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_last(str, state); + else { + state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor); + stb_textedit_clamp( str, state ); + } + break; + + case STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT: + if( !STB_TEXT_HAS_SELECTION( state ) ) + stb_textedit_prep_selection_at_cursor(state); + + state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor); + state->select_end = state->cursor; + + stb_textedit_clamp( str, state ); + break; +#endif + + case STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT: + stb_textedit_prep_selection_at_cursor(state); + // move selection right + ++state->select_end; + stb_textedit_clamp(str, state); + state->cursor = state->select_end; + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_DOWN: + case STB_TEXTEDIT_K_DOWN | STB_TEXTEDIT_K_SHIFT: + case STB_TEXTEDIT_K_PGDOWN: + case STB_TEXTEDIT_K_PGDOWN | STB_TEXTEDIT_K_SHIFT: { + StbFindState find; + StbTexteditRow row; + int i, j, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; + int is_page = (key & ~STB_TEXTEDIT_K_SHIFT) == STB_TEXTEDIT_K_PGDOWN; + int row_count = is_page ? state->row_count_per_page : 1; + + if (!is_page && state->single_line) { + // on windows, up&down in single-line behave like left&right + key = STB_TEXTEDIT_K_RIGHT | (key & STB_TEXTEDIT_K_SHIFT); + goto retry; + } + + if (sel) + stb_textedit_prep_selection_at_cursor(state); + else if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_last(str, state); + + // compute current position of cursor point + stb_textedit_clamp(str, state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + + for (j = 0; j < row_count; ++j) { + float x, goal_x = state->has_preferred_x ? state->preferred_x : find.x; + int start = find.first_char + find.length; + + if (find.length == 0) + break; + + // [DEAR IMGUI] + // going down while being on the last line shouldn't bring us to that line end + if (STB_TEXTEDIT_GETCHAR(str, find.first_char + find.length - 1) != STB_TEXTEDIT_NEWLINE) + break; + + // now find character position down a row + state->cursor = start; + STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor); + x = row.x0; + for (i=0; i < row.num_chars; ++i) { + float dx = STB_TEXTEDIT_GETWIDTH(str, start, i); + #ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE + if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE) + break; + #endif + x += dx; + if (x > goal_x) + break; + ++state->cursor; + } + stb_textedit_clamp(str, state); + + state->has_preferred_x = 1; + state->preferred_x = goal_x; + + if (sel) + state->select_end = state->cursor; + + // go to next line + find.first_char = find.first_char + find.length; + find.length = row.num_chars; + } + break; + } + + case STB_TEXTEDIT_K_UP: + case STB_TEXTEDIT_K_UP | STB_TEXTEDIT_K_SHIFT: + case STB_TEXTEDIT_K_PGUP: + case STB_TEXTEDIT_K_PGUP | STB_TEXTEDIT_K_SHIFT: { + StbFindState find; + StbTexteditRow row; + int i, j, prev_scan, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; + int is_page = (key & ~STB_TEXTEDIT_K_SHIFT) == STB_TEXTEDIT_K_PGUP; + int row_count = is_page ? state->row_count_per_page : 1; + + if (!is_page && state->single_line) { + // on windows, up&down become left&right + key = STB_TEXTEDIT_K_LEFT | (key & STB_TEXTEDIT_K_SHIFT); + goto retry; + } + + if (sel) + stb_textedit_prep_selection_at_cursor(state); + else if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_first(state); + + // compute current position of cursor point + stb_textedit_clamp(str, state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + + for (j = 0; j < row_count; ++j) { + float x, goal_x = state->has_preferred_x ? state->preferred_x : find.x; + + // can only go up if there's a previous row + if (find.prev_first == find.first_char) + break; + + // now find character position up a row + state->cursor = find.prev_first; + STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor); + x = row.x0; + for (i=0; i < row.num_chars; ++i) { + float dx = STB_TEXTEDIT_GETWIDTH(str, find.prev_first, i); + #ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE + if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE) + break; + #endif + x += dx; + if (x > goal_x) + break; + ++state->cursor; + } + stb_textedit_clamp(str, state); + + state->has_preferred_x = 1; + state->preferred_x = goal_x; + + if (sel) + state->select_end = state->cursor; + + // go to previous line + // (we need to scan previous line the hard way. maybe we could expose this as a new API function?) + prev_scan = find.prev_first > 0 ? find.prev_first - 1 : 0; + while (prev_scan > 0 && STB_TEXTEDIT_GETCHAR(str, prev_scan - 1) != STB_TEXTEDIT_NEWLINE) + --prev_scan; + find.first_char = find.prev_first; + find.prev_first = prev_scan; + } + break; + } + + case STB_TEXTEDIT_K_DELETE: + case STB_TEXTEDIT_K_DELETE | STB_TEXTEDIT_K_SHIFT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_delete_selection(str, state); + else { + int n = STB_TEXTEDIT_STRINGLEN(str); + if (state->cursor < n) + stb_textedit_delete(str, state, state->cursor, 1); + } + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_BACKSPACE: + case STB_TEXTEDIT_K_BACKSPACE | STB_TEXTEDIT_K_SHIFT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_delete_selection(str, state); + else { + stb_textedit_clamp(str, state); + if (state->cursor > 0) { + stb_textedit_delete(str, state, state->cursor-1, 1); + --state->cursor; + } + } + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_TEXTSTART2 + case STB_TEXTEDIT_K_TEXTSTART2: +#endif + case STB_TEXTEDIT_K_TEXTSTART: + state->cursor = state->select_start = state->select_end = 0; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_TEXTEND2 + case STB_TEXTEDIT_K_TEXTEND2: +#endif + case STB_TEXTEDIT_K_TEXTEND: + state->cursor = STB_TEXTEDIT_STRINGLEN(str); + state->select_start = state->select_end = 0; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_TEXTSTART2 + case STB_TEXTEDIT_K_TEXTSTART2 | STB_TEXTEDIT_K_SHIFT: +#endif + case STB_TEXTEDIT_K_TEXTSTART | STB_TEXTEDIT_K_SHIFT: + stb_textedit_prep_selection_at_cursor(state); + state->cursor = state->select_end = 0; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_TEXTEND2 + case STB_TEXTEDIT_K_TEXTEND2 | STB_TEXTEDIT_K_SHIFT: +#endif + case STB_TEXTEDIT_K_TEXTEND | STB_TEXTEDIT_K_SHIFT: + stb_textedit_prep_selection_at_cursor(state); + state->cursor = state->select_end = STB_TEXTEDIT_STRINGLEN(str); + state->has_preferred_x = 0; + break; + + +#ifdef STB_TEXTEDIT_K_LINESTART2 + case STB_TEXTEDIT_K_LINESTART2: +#endif + case STB_TEXTEDIT_K_LINESTART: + stb_textedit_clamp(str, state); + stb_textedit_move_to_first(state); + if (state->single_line) + state->cursor = 0; + else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE) + --state->cursor; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_LINEEND2 + case STB_TEXTEDIT_K_LINEEND2: +#endif + case STB_TEXTEDIT_K_LINEEND: { + int n = STB_TEXTEDIT_STRINGLEN(str); + stb_textedit_clamp(str, state); + stb_textedit_move_to_first(state); + if (state->single_line) + state->cursor = n; + else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE) + ++state->cursor; + state->has_preferred_x = 0; + break; + } + +#ifdef STB_TEXTEDIT_K_LINESTART2 + case STB_TEXTEDIT_K_LINESTART2 | STB_TEXTEDIT_K_SHIFT: +#endif + case STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT: + stb_textedit_clamp(str, state); + stb_textedit_prep_selection_at_cursor(state); + if (state->single_line) + state->cursor = 0; + else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE) + --state->cursor; + state->select_end = state->cursor; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_LINEEND2 + case STB_TEXTEDIT_K_LINEEND2 | STB_TEXTEDIT_K_SHIFT: +#endif + case STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT: { + int n = STB_TEXTEDIT_STRINGLEN(str); + stb_textedit_clamp(str, state); + stb_textedit_prep_selection_at_cursor(state); + if (state->single_line) + state->cursor = n; + else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE) + ++state->cursor; + state->select_end = state->cursor; + state->has_preferred_x = 0; + break; + } + } +} + +///////////////////////////////////////////////////////////////////////////// +// +// Undo processing +// +// @OPTIMIZE: the undo/redo buffer should be circular + +static void stb_textedit_flush_redo(StbUndoState *state) +{ + state->redo_point = STB_TEXTEDIT_UNDOSTATECOUNT; + state->redo_char_point = STB_TEXTEDIT_UNDOCHARCOUNT; +} + +// discard the oldest entry in the undo list +static void stb_textedit_discard_undo(StbUndoState *state) +{ + if (state->undo_point > 0) { + // if the 0th undo state has characters, clean those up + if (state->undo_rec[0].char_storage >= 0) { + int n = state->undo_rec[0].insert_length, i; + // delete n characters from all other records + state->undo_char_point -= n; + STB_TEXTEDIT_memmove(state->undo_char, state->undo_char + n, (size_t) (state->undo_char_point*sizeof(STB_TEXTEDIT_CHARTYPE))); + for (i=0; i < state->undo_point; ++i) + if (state->undo_rec[i].char_storage >= 0) + state->undo_rec[i].char_storage -= n; // @OPTIMIZE: get rid of char_storage and infer it + } + --state->undo_point; + STB_TEXTEDIT_memmove(state->undo_rec, state->undo_rec+1, (size_t) (state->undo_point*sizeof(state->undo_rec[0]))); + } +} + +// discard the oldest entry in the redo list--it's bad if this +// ever happens, but because undo & redo have to store the actual +// characters in different cases, the redo character buffer can +// fill up even though the undo buffer didn't +static void stb_textedit_discard_redo(StbUndoState *state) +{ + int k = STB_TEXTEDIT_UNDOSTATECOUNT-1; + + if (state->redo_point <= k) { + // if the k'th undo state has characters, clean those up + if (state->undo_rec[k].char_storage >= 0) { + int n = state->undo_rec[k].insert_length, i; + // move the remaining redo character data to the end of the buffer + state->redo_char_point += n; + STB_TEXTEDIT_memmove(state->undo_char + state->redo_char_point, state->undo_char + state->redo_char_point-n, (size_t) ((STB_TEXTEDIT_UNDOCHARCOUNT - state->redo_char_point)*sizeof(STB_TEXTEDIT_CHARTYPE))); + // adjust the position of all the other records to account for above memmove + for (i=state->redo_point; i < k; ++i) + if (state->undo_rec[i].char_storage >= 0) + state->undo_rec[i].char_storage += n; + } + // now move all the redo records towards the end of the buffer; the first one is at 'redo_point' + // [DEAR IMGUI] + size_t move_size = (size_t)((STB_TEXTEDIT_UNDOSTATECOUNT - state->redo_point - 1) * sizeof(state->undo_rec[0])); + const char* buf_begin = (char*)state->undo_rec; (void)buf_begin; + const char* buf_end = (char*)state->undo_rec + sizeof(state->undo_rec); (void)buf_end; + IM_ASSERT(((char*)(state->undo_rec + state->redo_point)) >= buf_begin); + IM_ASSERT(((char*)(state->undo_rec + state->redo_point + 1) + move_size) <= buf_end); + STB_TEXTEDIT_memmove(state->undo_rec + state->redo_point+1, state->undo_rec + state->redo_point, move_size); + + // now move redo_point to point to the new one + ++state->redo_point; + } +} + +static StbUndoRecord *stb_text_create_undo_record(StbUndoState *state, int numchars) +{ + // any time we create a new undo record, we discard redo + stb_textedit_flush_redo(state); + + // if we have no free records, we have to make room, by sliding the + // existing records down + if (state->undo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + stb_textedit_discard_undo(state); + + // if the characters to store won't possibly fit in the buffer, we can't undo + if (numchars > STB_TEXTEDIT_UNDOCHARCOUNT) { + state->undo_point = 0; + state->undo_char_point = 0; + return NULL; + } + + // if we don't have enough free characters in the buffer, we have to make room + while (state->undo_char_point + numchars > STB_TEXTEDIT_UNDOCHARCOUNT) + stb_textedit_discard_undo(state); + + return &state->undo_rec[state->undo_point++]; +} + +static STB_TEXTEDIT_CHARTYPE *stb_text_createundo(StbUndoState *state, int pos, int insert_len, int delete_len) +{ + StbUndoRecord *r = stb_text_create_undo_record(state, insert_len); + if (r == NULL) + return NULL; + + r->where = pos; + r->insert_length = (STB_TEXTEDIT_POSITIONTYPE) insert_len; + r->delete_length = (STB_TEXTEDIT_POSITIONTYPE) delete_len; + + if (insert_len == 0) { + r->char_storage = -1; + return NULL; + } else { + r->char_storage = state->undo_char_point; + state->undo_char_point += insert_len; + return &state->undo_char[r->char_storage]; + } +} + +static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + StbUndoState *s = &state->undostate; + StbUndoRecord u, *r; + if (s->undo_point == 0) + return; + + // we need to do two things: apply the undo record, and create a redo record + u = s->undo_rec[s->undo_point-1]; + r = &s->undo_rec[s->redo_point-1]; + r->char_storage = -1; + + r->insert_length = u.delete_length; + r->delete_length = u.insert_length; + r->where = u.where; + + if (u.delete_length) { + // if the undo record says to delete characters, then the redo record will + // need to re-insert the characters that get deleted, so we need to store + // them. + + // there are three cases: + // there's enough room to store the characters + // characters stored for *redoing* don't leave room for redo + // characters stored for *undoing* don't leave room for redo + // if the last is true, we have to bail + + if (s->undo_char_point + u.delete_length >= STB_TEXTEDIT_UNDOCHARCOUNT) { + // the undo records take up too much character space; there's no space to store the redo characters + r->insert_length = 0; + } else { + int i; + + // there's definitely room to store the characters eventually + while (s->undo_char_point + u.delete_length > s->redo_char_point) { + // should never happen: + if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + return; + // there's currently not enough room, so discard a redo record + stb_textedit_discard_redo(s); + } + r = &s->undo_rec[s->redo_point-1]; + + r->char_storage = s->redo_char_point - u.delete_length; + s->redo_char_point = s->redo_char_point - u.delete_length; + + // now save the characters + for (i=0; i < u.delete_length; ++i) + s->undo_char[r->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u.where + i); + } + + // now we can carry out the deletion + STB_TEXTEDIT_DELETECHARS(str, u.where, u.delete_length); + } + + // check type of recorded action: + if (u.insert_length) { + // easy case: was a deletion, so we need to insert n characters + STB_TEXTEDIT_INSERTCHARS(str, u.where, &s->undo_char[u.char_storage], u.insert_length); + s->undo_char_point -= u.insert_length; + } + + state->cursor = u.where + u.insert_length; + + s->undo_point--; + s->redo_point--; +} + +static void stb_text_redo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + StbUndoState *s = &state->undostate; + StbUndoRecord *u, r; + if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + return; + + // we need to do two things: apply the redo record, and create an undo record + u = &s->undo_rec[s->undo_point]; + r = s->undo_rec[s->redo_point]; + + // we KNOW there must be room for the undo record, because the redo record + // was derived from an undo record + + u->delete_length = r.insert_length; + u->insert_length = r.delete_length; + u->where = r.where; + u->char_storage = -1; + + if (r.delete_length) { + // the redo record requires us to delete characters, so the undo record + // needs to store the characters + + if (s->undo_char_point + u->insert_length > s->redo_char_point) { + u->insert_length = 0; + u->delete_length = 0; + } else { + int i; + u->char_storage = s->undo_char_point; + s->undo_char_point = s->undo_char_point + u->insert_length; + + // now save the characters + for (i=0; i < u->insert_length; ++i) + s->undo_char[u->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u->where + i); + } + + STB_TEXTEDIT_DELETECHARS(str, r.where, r.delete_length); + } + + if (r.insert_length) { + // easy case: need to insert n characters + STB_TEXTEDIT_INSERTCHARS(str, r.where, &s->undo_char[r.char_storage], r.insert_length); + s->redo_char_point += r.insert_length; + } + + state->cursor = r.where + r.insert_length; + + s->undo_point++; + s->redo_point++; +} + +static void stb_text_makeundo_insert(STB_TexteditState *state, int where, int length) +{ + stb_text_createundo(&state->undostate, where, 0, length); +} + +static void stb_text_makeundo_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length) +{ + int i; + STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, length, 0); + if (p) { + for (i=0; i < length; ++i) + p[i] = STB_TEXTEDIT_GETCHAR(str, where+i); + } +} + +static void stb_text_makeundo_replace(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length) +{ + int i; + STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, old_length, new_length); + if (p) { + for (i=0; i < old_length; ++i) + p[i] = STB_TEXTEDIT_GETCHAR(str, where+i); + } +} + +// reset the state to default +static void stb_textedit_clear_state(STB_TexteditState *state, int is_single_line) +{ + state->undostate.undo_point = 0; + state->undostate.undo_char_point = 0; + state->undostate.redo_point = STB_TEXTEDIT_UNDOSTATECOUNT; + state->undostate.redo_char_point = STB_TEXTEDIT_UNDOCHARCOUNT; + state->select_end = state->select_start = 0; + state->cursor = 0; + state->has_preferred_x = 0; + state->preferred_x = 0; + state->cursor_at_end_of_line = 0; + state->initialized = 1; + state->single_line = (unsigned char) is_single_line; + state->insert_mode = 0; + state->row_count_per_page = 0; +} + +// API initialize +static void stb_textedit_initialize_state(STB_TexteditState *state, int is_single_line) +{ + stb_textedit_clear_state(state, is_single_line); +} + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + +static int stb_textedit_paste(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE const *ctext, int len) +{ + return stb_textedit_paste_internal(str, state, (STB_TEXTEDIT_CHARTYPE *) ctext, len); +} + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + +#endif//STB_TEXTEDIT_IMPLEMENTATION + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/LICENSE.txt`: + +```txt +The MIT License (MIT) + +Copyright (c) 2014-2022 Omar Cornut + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Private/imgui.cpp`: + +```cpp +// dear imgui, v1.87 +// (main code and documentation) + +// Help: +// - Read FAQ at http://dearimgui.org/faq +// - Newcomers, read 'Programmer guide' below for notes on how to setup Dear ImGui in your codebase. +// - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. +// Read imgui.cpp for details, links and comments. + +// Resources: +// - FAQ http://dearimgui.org/faq +// - Homepage & latest https://github.com/ocornut/imgui +// - Releases & changelog https://github.com/ocornut/imgui/releases +// - Gallery https://github.com/ocornut/imgui/issues/4451 (please post your screenshots/video there!) +// - Wiki https://github.com/ocornut/imgui/wiki (lots of good stuff there) +// - Glossary https://github.com/ocornut/imgui/wiki/Glossary +// - Issues & support https://github.com/ocornut/imgui/issues + +// Getting Started? +// - For first-time users having issues compiling/linking/running or issues loading fonts: +// please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. + +// Developed by Omar Cornut and every direct or indirect contributors to the GitHub. +// See LICENSE.txt for copyright and licensing details (standard MIT License). +// This library is free but needs your support to sustain development and maintenance. +// Businesses: you can support continued development via invoiced technical support, maintenance and sponsoring contracts. Please reach out to "contact AT dearimgui.com". +// Individuals: you can support continued development via donations. See docs/README or web page. + +// It is recommended that you don't modify imgui.cpp! It will become difficult for you to update the library. +// Note that 'ImGui::' being a namespace, you can add functions into the namespace from your own source files, without +// modifying imgui.h or imgui.cpp. You may include imgui_internal.h to access internal data structures, but it doesn't +// come with any guarantee of forward compatibility. Discussing your changes on the GitHub Issue Tracker may lead you +// to a better solution or official support for them. + +/* + +Index of this file: + +DOCUMENTATION + +- MISSION STATEMENT +- END-USER GUIDE +- PROGRAMMER GUIDE + - READ FIRST + - HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI + - GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE + - HOW A SIMPLE APPLICATION MAY LOOK LIKE + - HOW A SIMPLE RENDERING FUNCTION MAY LOOK LIKE + - USING GAMEPAD/KEYBOARD NAVIGATION CONTROLS +- API BREAKING CHANGES (read me when you update!) +- FREQUENTLY ASKED QUESTIONS (FAQ) + - Read all answers online: https://www.dearimgui.org/faq, or in docs/FAQ.md (with a Markdown viewer) + +CODE +(search for "[SECTION]" in the code to find them) + +// [SECTION] INCLUDES +// [SECTION] FORWARD DECLARATIONS +// [SECTION] CONTEXT AND MEMORY ALLOCATORS +// [SECTION] USER FACING STRUCTURES (ImGuiStyle, ImGuiIO) +// [SECTION] MISC HELPERS/UTILITIES (Geometry functions) +// [SECTION] MISC HELPERS/UTILITIES (String, Format, Hash functions) +// [SECTION] MISC HELPERS/UTILITIES (File functions) +// [SECTION] MISC HELPERS/UTILITIES (ImText* functions) +// [SECTION] MISC HELPERS/UTILITIES (Color functions) +// [SECTION] ImGuiStorage +// [SECTION] ImGuiTextFilter +// [SECTION] ImGuiTextBuffer +// [SECTION] ImGuiListClipper +// [SECTION] STYLING +// [SECTION] RENDER HELPERS +// [SECTION] MAIN CODE (most of the code! lots of stuff, needs tidying up!) +// [SECTION] INPUTS +// [SECTION] ERROR CHECKING +// [SECTION] LAYOUT +// [SECTION] SCROLLING +// [SECTION] TOOLTIPS +// [SECTION] POPUPS +// [SECTION] KEYBOARD/GAMEPAD NAVIGATION +// [SECTION] DRAG AND DROP +// [SECTION] LOGGING/CAPTURING +// [SECTION] SETTINGS +// [SECTION] VIEWPORTS +// [SECTION] PLATFORM DEPENDENT HELPERS +// [SECTION] METRICS/DEBUGGER WINDOW +// [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, STACK TOOL) + +*/ + +//----------------------------------------------------------------------------- +// DOCUMENTATION +//----------------------------------------------------------------------------- + +/* + + MISSION STATEMENT + ================= + + - Easy to use to create code-driven and data-driven tools. + - Easy to use to create ad hoc short-lived tools and long-lived, more elaborate tools. + - Easy to hack and improve. + - Minimize setup and maintenance. + - Minimize state storage on user side. + - Portable, minimize dependencies, run on target (consoles, phones, etc.). + - Efficient runtime and memory consumption. + + Designed for developers and content-creators, not the typical end-user! Some of the current weaknesses includes: + + - Doesn't look fancy, doesn't animate. + - Limited layout features, intricate layouts are typically crafted in code. + + + END-USER GUIDE + ============== + + - Double-click on title bar to collapse window. + - Click upper right corner to close a window, available when 'bool* p_open' is passed to ImGui::Begin(). + - Click and drag on lower right corner to resize window (double-click to auto fit window to its contents). + - Click and drag on any empty space to move window. + - TAB/SHIFT+TAB to cycle through keyboard editable fields. + - CTRL+Click on a slider or drag box to input value as text. + - Use mouse wheel to scroll. + - Text editor: + - Hold SHIFT or use mouse to select text. + - CTRL+Left/Right to word jump. + - CTRL+Shift+Left/Right to select words. + - CTRL+A our Double-Click to select all. + - CTRL+X,CTRL+C,CTRL+V to use OS clipboard/ + - CTRL+Z,CTRL+Y to undo/redo. + - ESCAPE to revert text to its original value. + - Controls are automatically adjusted for OSX to match standard OSX text editing operations. + - General Keyboard controls: enable with ImGuiConfigFlags_NavEnableKeyboard. + - General Gamepad controls: enable with ImGuiConfigFlags_NavEnableGamepad. See suggested mappings in imgui.h ImGuiNavInput_ + download PNG/PSD at http://dearimgui.org/controls_sheets + + + PROGRAMMER GUIDE + ================ + + READ FIRST + ---------- + - Remember to check the wonderful Wiki (https://github.com/ocornut/imgui/wiki) + - Your code creates the UI, if your code doesn't run the UI is gone! The UI can be highly dynamic, there are no construction or + destruction steps, less superfluous data retention on your side, less state duplication, less state synchronization, fewer bugs. + - Call and read ImGui::ShowDemoWindow() for demo code demonstrating most features. + - The library is designed to be built from sources. Avoid pre-compiled binaries and packaged versions. See imconfig.h to configure your build. + - Dear ImGui is an implementation of the IMGUI paradigm (immediate-mode graphical user interface, a term coined by Casey Muratori). + You can learn about IMGUI principles at http://www.johno.se/book/imgui.html, http://mollyrocket.com/861 & more links in Wiki. + - Dear ImGui is a "single pass" rasterizing implementation of the IMGUI paradigm, aimed at ease of use and high-performances. + For every application frame, your UI code will be called only once. This is in contrast to e.g. Unity's implementation of an IMGUI, + where the UI code is called multiple times ("multiple passes") from a single entry point. There are pros and cons to both approaches. + - Our origin is on the top-left. In axis aligned bounding boxes, Min = top-left, Max = bottom-right. + - This codebase is also optimized to yield decent performances with typical "Debug" builds settings. + - Please make sure you have asserts enabled (IM_ASSERT redirects to assert() by default, but can be redirected). + If you get an assert, read the messages and comments around the assert. + - C++: this is a very C-ish codebase: we don't rely on C++11, we don't include any C++ headers, and ImGui:: is a namespace. + - C++: ImVec2/ImVec4 do not expose math operators by default, because it is expected that you use your own math types. + See FAQ "How can I use my own math types instead of ImVec2/ImVec4?" for details about setting up imconfig.h for that. + However, imgui_internal.h can optionally export math operators for ImVec2/ImVec4, which we use in this codebase. + - C++: pay attention that ImVector<> manipulates plain-old-data and does not honor construction/destruction (avoid using it in your code!). + + + HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI + ---------------------------------------------- + - Overwrite all the sources files except for imconfig.h (if you have modified your copy of imconfig.h) + - Or maintain your own branch where you have imconfig.h modified as a top-most commit which you can regularly rebase over "master". + - You can also use '#define IMGUI_USER_CONFIG "my_config_file.h" to redirect configuration to your own file. + - Read the "API BREAKING CHANGES" section (below). This is where we list occasional API breaking changes. + If a function/type has been renamed / or marked obsolete, try to fix the name in your code before it is permanently removed + from the public API. If you have a problem with a missing function/symbols, search for its name in the code, there will + likely be a comment about it. Please report any issue to the GitHub page! + - To find out usage of old API, you can add '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' in your configuration file. + - Try to keep your copy of Dear ImGui reasonably up to date. + + + GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE + --------------------------------------------------------------- + - Run and study the examples and demo in imgui_demo.cpp to get acquainted with the library. + - In the majority of cases you should be able to use unmodified backends files available in the backends/ folder. + - Add the Dear ImGui source files + selected backend source files to your projects or using your preferred build system. + It is recommended you build and statically link the .cpp files as part of your project and NOT as a shared library (DLL). + - You can later customize the imconfig.h file to tweak some compile-time behavior, such as integrating Dear ImGui types with your own maths types. + - When using Dear ImGui, your programming IDE is your friend: follow the declaration of variables, functions and types to find comments about them. + - Dear ImGui never touches or knows about your GPU state. The only function that knows about GPU is the draw function that you provide. + Effectively it means you can create widgets at any time in your code, regardless of considerations of being in "update" vs "render" + phases of your own application. All rendering information is stored into command-lists that you will retrieve after calling ImGui::Render(). + - Refer to the backends and demo applications in the examples/ folder for instruction on how to setup your code. + - If you are running over a standard OS with a common graphics API, you should be able to use unmodified imgui_impl_*** files from the examples/ folder. + + + HOW A SIMPLE APPLICATION MAY LOOK LIKE + -------------------------------------- + EXHIBIT 1: USING THE EXAMPLE BACKENDS (= imgui_impl_XXX.cpp files from the backends/ folder). + The sub-folders in examples/ contain examples applications following this structure. + + // Application init: create a dear imgui context, setup some options, load fonts + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + // TODO: Set optional io.ConfigFlags values, e.g. 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard' to enable keyboard controls. + // TODO: Fill optional fields of the io structure later. + // TODO: Load TTF/OTF fonts if you don't want to use the default font. + + // Initialize helper Platform and Renderer backends (here we are using imgui_impl_win32.cpp and imgui_impl_dx11.cpp) + ImGui_ImplWin32_Init(hwnd); + ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext); + + // Application main loop + while (true) + { + // Feed inputs to dear imgui, start new frame + ImGui_ImplDX11_NewFrame(); + ImGui_ImplWin32_NewFrame(); + ImGui::NewFrame(); + + // Any application code here + ImGui::Text("Hello, world!"); + + // Render dear imgui into screen + ImGui::Render(); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + g_pSwapChain->Present(1, 0); + } + + // Shutdown + ImGui_ImplDX11_Shutdown(); + ImGui_ImplWin32_Shutdown(); + ImGui::DestroyContext(); + + EXHIBIT 2: IMPLEMENTING CUSTOM BACKEND / CUSTOM ENGINE + + // Application init: create a dear imgui context, setup some options, load fonts + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + // TODO: Set optional io.ConfigFlags values, e.g. 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard' to enable keyboard controls. + // TODO: Fill optional fields of the io structure later. + // TODO: Load TTF/OTF fonts if you don't want to use the default font. + + // Build and load the texture atlas into a texture + // (In the examples/ app this is usually done within the ImGui_ImplXXX_Init() function from one of the demo Renderer) + int width, height; + unsigned char* pixels = NULL; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); + + // At this point you've got the texture data and you need to upload that to your graphic system: + // After we have created the texture, store its pointer/identifier (_in whichever format your engine uses_) in 'io.Fonts->TexID'. + // This will be passed back to your via the renderer. Basically ImTextureID == void*. Read FAQ for details about ImTextureID. + MyTexture* texture = MyEngine::CreateTextureFromMemoryPixels(pixels, width, height, TEXTURE_TYPE_RGBA32) + io.Fonts->SetTexID((void*)texture); + + // Application main loop + while (true) + { + // Setup low-level inputs, e.g. on Win32: calling GetKeyboardState(), or write to those fields from your Windows message handlers, etc. + // (In the examples/ app this is usually done within the ImGui_ImplXXX_NewFrame() function from one of the demo Platform Backends) + io.DeltaTime = 1.0f/60.0f; // set the time elapsed since the previous frame (in seconds) + io.DisplaySize.x = 1920.0f; // set the current display width + io.DisplaySize.y = 1280.0f; // set the current display height here + io.AddMousePosEvent(mouse_x, mouse_y); // update mouse position + io.AddMouseButtonEvent(0, mouse_b[0]); // update mouse button states + io.AddMouseButtonEvent(1, mouse_b[1]); // update mouse button states + + // Call NewFrame(), after this point you can use ImGui::* functions anytime + // (So you want to try calling NewFrame() as early as you can in your main loop to be able to use Dear ImGui everywhere) + ImGui::NewFrame(); + + // Most of your application code here + ImGui::Text("Hello, world!"); + MyGameUpdate(); // may use any Dear ImGui functions, e.g. ImGui::Begin("My window"); ImGui::Text("Hello, world!"); ImGui::End(); + MyGameRender(); // may use any Dear ImGui functions as well! + + // Render dear imgui, swap buffers + // (You want to try calling EndFrame/Render as late as you can, to be able to use Dear ImGui in your own game rendering code) + ImGui::EndFrame(); + ImGui::Render(); + ImDrawData* draw_data = ImGui::GetDrawData(); + MyImGuiRenderFunction(draw_data); + SwapBuffers(); + } + + // Shutdown + ImGui::DestroyContext(); + + To decide whether to dispatch mouse/keyboard inputs to Dear ImGui to the rest of your application, + you should read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags! + Please read the FAQ and example applications for details about this! + + + HOW A SIMPLE RENDERING FUNCTION MAY LOOK LIKE + --------------------------------------------- + The backends in impl_impl_XXX.cpp files contain many working implementations of a rendering function. + + void void MyImGuiRenderFunction(ImDrawData* draw_data) + { + // TODO: Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled + // TODO: Setup viewport covering draw_data->DisplayPos to draw_data->DisplayPos + draw_data->DisplaySize + // TODO: Setup orthographic projection matrix cover draw_data->DisplayPos to draw_data->DisplayPos + draw_data->DisplaySize + // TODO: Setup shader: vertex { float2 pos, float2 uv, u32 color }, fragment shader sample color from 1 texture, multiply by vertex color. + ImVec2 clip_off = draw_data->DisplayPos; + for (int n = 0; n < draw_data->CmdListsCount; n++) + { + const ImDrawList* cmd_list = draw_data->CmdLists[n]; + const ImDrawVert* vtx_buffer = cmd_list->VtxBuffer.Data; // vertex buffer generated by Dear ImGui + const ImDrawIdx* idx_buffer = cmd_list->IdxBuffer.Data; // index buffer generated by Dear ImGui + for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) + { + const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; + if (pcmd->UserCallback) + { + pcmd->UserCallback(cmd_list, pcmd); + } + else + { + // Project scissor/clipping rectangles into framebuffer space + ImVec2 clip_min(pcmd->ClipRect.x - clip_off.x, pcmd->ClipRect.y - clip_off.y); + ImVec2 clip_max(pcmd->ClipRect.z - clip_off.x, pcmd->ClipRect.w - clip_off.y); + if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) + continue; + + // We are using scissoring to clip some objects. All low-level graphics API should support it. + // - If your engine doesn't support scissoring yet, you may ignore this at first. You will get some small glitches + // (some elements visible outside their bounds) but you can fix that once everything else works! + // - Clipping coordinates are provided in imgui coordinates space: + // - For a given viewport, draw_data->DisplayPos == viewport->Pos and draw_data->DisplaySize == viewport->Size + // - In a single viewport application, draw_data->DisplayPos == (0,0) and draw_data->DisplaySize == io.DisplaySize, but always use GetMainViewport()->Pos/Size instead of hardcoding those values. + // - In the interest of supporting multi-viewport applications (see 'docking' branch on github), + // always subtract draw_data->DisplayPos from clipping bounds to convert them to your viewport space. + // - Note that pcmd->ClipRect contains Min+Max bounds. Some graphics API may use Min+Max, other may use Min+Size (size being Max-Min) + MyEngineSetScissor(clip_min.x, clip_min.y, clip_max.x, clip_max.y); + + // The texture for the draw call is specified by pcmd->GetTexID(). + // The vast majority of draw calls will use the Dear ImGui texture atlas, which value you have set yourself during initialization. + MyEngineBindTexture((MyTexture*)pcmd->GetTexID()); + + // Render 'pcmd->ElemCount/3' indexed triangles. + // By default the indices ImDrawIdx are 16-bit, you can change them to 32-bit in imconfig.h if your engine doesn't support 16-bit indices. + MyEngineDrawIndexedTriangles(pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer + pcmd->IdxOffset, vtx_buffer, pcmd->VtxOffset); + } + } + } + } + + + USING GAMEPAD/KEYBOARD NAVIGATION CONTROLS + ------------------------------------------ + - The gamepad/keyboard navigation is fairly functional and keeps being improved. + - Gamepad support is particularly useful to use Dear ImGui on a console system (e.g. PS4, Switch, XB1) without a mouse! + - You can ask questions and report issues at https://github.com/ocornut/imgui/issues/787 + - The initial focus was to support game controllers, but keyboard is becoming increasingly and decently usable. + - Keyboard: + - Application: Set io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard to enable. + - Internally: NewFrame() will automatically fill io.NavInputs[] based on backend's io.AddKeyEvent() calls. + - When keyboard navigation is active (io.NavActive + ImGuiConfigFlags_NavEnableKeyboard), + the io.WantCaptureKeyboard flag will be set. For more advanced uses, you may want to read from: + - io.NavActive: true when a window is focused and it doesn't have the ImGuiWindowFlags_NoNavInputs flag set. + - io.NavVisible: true when the navigation cursor is visible (and usually goes false when mouse is used). + - or query focus information with e.g. IsWindowFocused(ImGuiFocusedFlags_AnyWindow), IsItemFocused() etc. functions. + Please reach out if you think the game vs navigation input sharing could be improved. + - Gamepad: + - Application: Set io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad to enable. + - Backend: Set io.BackendFlags |= ImGuiBackendFlags_HasGamepad + call io.AddKeyEvent/AddKeyAnalogEvent() with ImGuiKey_Gamepad_XXX keys. + For analog values (0.0f to 1.0f), backend is responsible to handling a dead-zone and rescaling inputs accordingly. + Backend code will probably need to transform your raw inputs (such as e.g. remapping your 0.2..0.9 raw input range to 0.0..1.0 imgui range, etc.). + - Internally: NewFrame() will automatically fill io.NavInputs[] based on backend's io.AddKeyEvent() + io.AddKeyAnalogEvent() calls. + - BEFORE 1.87, BACKENDS USED TO WRITE DIRECTLY TO io.NavInputs[]. This is going to be obsoleted in the future. Please call io functions instead! + - You can download PNG/PSD files depicting the gamepad controls for common controllers at: http://dearimgui.org/controls_sheets + - If you need to share inputs between your game and the Dear ImGui interface, the easiest approach is to go all-or-nothing, + with a buttons combo to toggle the target. Please reach out if you think the game vs navigation input sharing could be improved. + - Mouse: + - PS4/PS5 users: Consider emulating a mouse cursor with DualShock4 touch pad or a spare analog stick as a mouse-emulation fallback. + - Consoles/Tablet/Phone users: Consider using a Synergy 1.x server (on your PC) + uSynergy.c (on your console/tablet/phone app) to share your PC mouse/keyboard. + - On a TV/console system where readability may be lower or mouse inputs may be awkward, you may want to set the ImGuiConfigFlags_NavEnableSetMousePos flag. + Enabling ImGuiConfigFlags_NavEnableSetMousePos + ImGuiBackendFlags_HasSetMousePos instructs dear imgui to move your mouse cursor along with navigation movements. + When enabled, the NewFrame() function may alter 'io.MousePos' and set 'io.WantSetMousePos' to notify you that it wants the mouse cursor to be moved. + When that happens your backend NEEDS to move the OS or underlying mouse cursor on the next frame. Some of the backends in examples/ do that. + (If you set the NavEnableSetMousePos flag but don't honor 'io.WantSetMousePos' properly, imgui will misbehave as it will see your mouse moving back and forth!) + (In a setup when you may not have easy control over the mouse cursor, e.g. uSynergy.c doesn't expose moving remote mouse cursor, you may want + to set a boolean to ignore your other external mouse positions until the external source is moved again.) + + + API BREAKING CHANGES + ==================== + + Occasionally introducing changes that are breaking the API. We try to make the breakage minor and easy to fix. + Below is a change-log of API breaking changes only. If you are using one of the functions listed, expect to have to fix some code. + When you are not sure about an old symbol or function name, try using the Search/Find function of your IDE to look for comments or references in all imgui files. + You can read releases logs https://github.com/ocornut/imgui/releases for more details. + + - 2022/01/20 (1.87) - inputs: reworded gamepad IO. + - Backend writing to io.NavInputs[] -> backend should call io.AddKeyEvent()/io.AddKeyAnalogEvent() with ImGuiKey_GamepadXXX values. + - 2022/01/19 (1.87) - sliders, drags: removed support for legacy arithmetic operators (+,+-,*,/) when inputing text. This doesn't break any api/code but a feature that used to be accessible by end-users (which seemingly no one used). + - 2022/01/17 (1.87) - inputs: reworked mouse IO. + - Backend writing to io.MousePos -> backend should call io.AddMousePosEvent() + - Backend writing to io.MouseDown[] -> backend should call io.AddMouseButtonEvent() + - Backend writing to io.MouseWheel -> backend should call io.AddMouseWheelEvent() + - Backend writing to io.MouseHoveredViewport -> backend should call io.AddMouseViewportEvent() [Docking branch w/ multi-viewports only] + - 2022/01/10 (1.87) - inputs: reworked keyboard IO. Removed io.KeyMap[], io.KeysDown[] in favor of calling io.AddKeyEvent(). Removed GetKeyIndex(), now unecessary. All IsKeyXXX() functions now take ImGuiKey values. All features are still functional until IMGUI_DISABLE_OBSOLETE_KEYIO is defined. Read Changelog and Release Notes for details. + - IsKeyPressed(MY_NATIVE_KEY_XXX) -> use IsKeyPressed(ImGuiKey_XXX) + - IsKeyPressed(GetKeyIndex(ImGuiKey_XXX)) -> use IsKeyPressed(ImGuiKey_XXX) + - Backend writing to io.KeyMap[],io.KeysDown[] -> backend should call io.AddKeyEvent() + - Backend writing to io.KeyCtrl, io.KeyShift.. -> backend should call io.AddKeyEvent() with ImGuiKey_ModXXX values. *IF YOU PULLED CODE BETWEEN 2021/01/10 and 2021/01/27: We used to have a io.AddKeyModsEvent() function which was now replaced by io.AddKeyEvent() with ImGuiKey_ModXXX values.* + - one case won't work with backward compatibility: if your custom backend used ImGuiKey as mock native indices (e.g. "io.KeyMap[ImGuiKey_A] = ImGuiKey_A") because those values are now larger than the legacy KeyDown[] array. Will assert. + - inputs: added ImGuiKey_ModCtrl/ImGuiKey_ModShift/ImGuiKey_ModAlt/ImGuiKey_ModSuper values to submit keyboard modifiers using io.AddKeyEvent(), instead of writing directly to io.KeyCtrl, io.KeyShift, io.KeyAlt, io.KeySuper. + - 2022/01/05 (1.87) - inputs: renamed ImGuiKey_KeyPadEnter to ImGuiKey_KeypadEnter to align with new symbols. Kept redirection enum. + - 2022/01/05 (1.87) - removed io.ImeSetInputScreenPosFn() in favor of more flexible io.SetPlatformImeDataFn(). Removed 'void* io.ImeWindowHandle' in favor of writing to 'void* ImGuiViewport::PlatformHandleRaw'. + - 2022/01/01 (1.87) - commented out redirecting functions/enums names that were marked obsolete in 1.69, 1.70, 1.71, 1.72 (March-July 2019) + - ImGui::SetNextTreeNodeOpen() -> use ImGui::SetNextItemOpen() + - ImGui::GetContentRegionAvailWidth() -> use ImGui::GetContentRegionAvail().x + - ImGui::TreeAdvanceToLabelPos() -> use ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetTreeNodeToLabelSpacing()); + - ImFontAtlas::CustomRect -> use ImFontAtlasCustomRect + - ImGuiColorEditFlags_RGB/HSV/HEX -> use ImGuiColorEditFlags_DisplayRGB/HSV/Hex + - 2021/12/20 (1.86) - backends: removed obsolete Marmalade backend (imgui_impl_marmalade.cpp) + example. Find last supported version at https://github.com/ocornut/imgui/wiki/Bindings + - 2021/11/04 (1.86) - removed CalcListClipping() function. Prefer using ImGuiListClipper which can return non-contiguous ranges. Please open an issue if you think you really need this function. + - 2021/08/23 (1.85) - removed GetWindowContentRegionWidth() function. keep inline redirection helper. can use 'GetWindowContentRegionMax().x - GetWindowContentRegionMin().x' instead for generally 'GetContentRegionAvail().x' is more useful. + - 2021/07/26 (1.84) - commented out redirecting functions/enums names that were marked obsolete in 1.67 and 1.69 (March 2019): + - ImGui::GetOverlayDrawList() -> use ImGui::GetForegroundDrawList() + - ImFont::GlyphRangesBuilder -> use ImFontGlyphRangesBuilder + - 2021/05/19 (1.83) - backends: obsoleted direct access to ImDrawCmd::TextureId in favor of calling ImDrawCmd::GetTexID(). + - if you are using official backends from the source tree: you have nothing to do. + - if you have copied old backend code or using your own: change access to draw_cmd->TextureId to draw_cmd->GetTexID(). + - 2021/03/12 (1.82) - upgraded ImDrawList::AddRect(), AddRectFilled(), PathRect() to use ImDrawFlags instead of ImDrawCornersFlags. + - ImDrawCornerFlags_TopLeft -> use ImDrawFlags_RoundCornersTopLeft + - ImDrawCornerFlags_BotRight -> use ImDrawFlags_RoundCornersBottomRight + - ImDrawCornerFlags_None -> use ImDrawFlags_RoundCornersNone etc. + flags now sanely defaults to 0 instead of 0x0F, consistent with all other flags in the API. + breaking: the default with rounding > 0.0f is now "round all corners" vs old implicit "round no corners": + - rounding == 0.0f + flags == 0 --> meant no rounding --> unchanged (common use) + - rounding > 0.0f + flags != 0 --> meant rounding --> unchanged (common use) + - rounding == 0.0f + flags != 0 --> meant no rounding --> unchanged (unlikely use) + - rounding > 0.0f + flags == 0 --> meant no rounding --> BREAKING (unlikely use): will now round all corners --> use ImDrawFlags_RoundCornersNone or rounding == 0.0f. + this ONLY matters for hard coded use of 0 + rounding > 0.0f. Use of named ImDrawFlags_RoundCornersNone (new) or ImDrawCornerFlags_None (old) are ok. + the old ImDrawCornersFlags used awkward default values of ~0 or 0xF (4 lower bits set) to signify "round all corners" and we sometimes encouraged using them as shortcuts. + legacy path still support use of hard coded ~0 or any value from 0x1 or 0xF. They will behave the same with legacy paths enabled (will assert otherwise). + - 2021/03/11 (1.82) - removed redirecting functions/enums names that were marked obsolete in 1.66 (September 2018): + - ImGui::SetScrollHere() -> use ImGui::SetScrollHereY() + - 2021/03/11 (1.82) - clarified that ImDrawList::PathArcTo(), ImDrawList::PathArcToFast() won't render with radius < 0.0f. Previously it sorts of accidentally worked but would generally lead to counter-clockwise paths and have an effect on anti-aliasing. + - 2021/03/10 (1.82) - upgraded ImDrawList::AddPolyline() and PathStroke() "bool closed" parameter to "ImDrawFlags flags". The matching ImDrawFlags_Closed value is guaranteed to always stay == 1 in the future. + - 2021/02/22 (1.82) - (*undone in 1.84*) win32+mingw: Re-enabled IME functions by default even under MinGW. In July 2016, issue #738 had me incorrectly disable those default functions for MinGW. MinGW users should: either link with -limm32, either set their imconfig file with '#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS'. + - 2021/02/17 (1.82) - renamed rarely used style.CircleSegmentMaxError (old default = 1.60f) to style.CircleTessellationMaxError (new default = 0.30f) as the meaning of the value changed. + - 2021/02/03 (1.81) - renamed ListBoxHeader(const char* label, ImVec2 size) to BeginListBox(). Kept inline redirection function (will obsolete). + - removed ListBoxHeader(const char* label, int items_count, int height_in_items = -1) in favor of specifying size. Kept inline redirection function (will obsolete). + - renamed ListBoxFooter() to EndListBox(). Kept inline redirection function (will obsolete). + - 2021/01/26 (1.81) - removed ImGuiFreeType::BuildFontAtlas(). Kept inline redirection function. Prefer using '#define IMGUI_ENABLE_FREETYPE', but there's a runtime selection path available too. The shared extra flags parameters (very rarely used) are now stored in ImFontAtlas::FontBuilderFlags. + - renamed ImFontConfig::RasterizerFlags (used by FreeType) to ImFontConfig::FontBuilderFlags. + - renamed ImGuiFreeType::XXX flags to ImGuiFreeTypeBuilderFlags_XXX for consistency with other API. + - 2020/10/12 (1.80) - removed redirecting functions/enums that were marked obsolete in 1.63 (August 2018): + - ImGui::IsItemDeactivatedAfterChange() -> use ImGui::IsItemDeactivatedAfterEdit(). + - ImGuiCol_ModalWindowDarkening -> use ImGuiCol_ModalWindowDimBg + - ImGuiInputTextCallback -> use ImGuiTextEditCallback + - ImGuiInputTextCallbackData -> use ImGuiTextEditCallbackData + - 2020/12/21 (1.80) - renamed ImDrawList::AddBezierCurve() to AddBezierCubic(), and PathBezierCurveTo() to PathBezierCubicCurveTo(). Kept inline redirection function (will obsolete). + - 2020/12/04 (1.80) - added imgui_tables.cpp file! Manually constructed project files will need the new file added! + - 2020/11/18 (1.80) - renamed undocumented/internals ImGuiColumnsFlags_* to ImGuiOldColumnFlags_* in prevision of incoming Tables API. + - 2020/11/03 (1.80) - renamed io.ConfigWindowsMemoryCompactTimer to io.ConfigMemoryCompactTimer as the feature will apply to other data structures + - 2020/10/14 (1.80) - backends: moved all backends files (imgui_impl_XXXX.cpp, imgui_impl_XXXX.h) from examples/ to backends/. + - 2020/10/12 (1.80) - removed redirecting functions/enums that were marked obsolete in 1.60 (April 2018): + - io.RenderDrawListsFn pointer -> use ImGui::GetDrawData() value and call the render function of your backend + - ImGui::IsAnyWindowFocused() -> use ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow) + - ImGui::IsAnyWindowHovered() -> use ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) + - ImGuiStyleVar_Count_ -> use ImGuiStyleVar_COUNT + - ImGuiMouseCursor_Count_ -> use ImGuiMouseCursor_COUNT + - removed redirecting functions names that were marked obsolete in 1.61 (May 2018): + - InputFloat (... int decimal_precision ...) -> use InputFloat (... const char* format ...) with format = "%.Xf" where X is your value for decimal_precision. + - same for InputFloat2()/InputFloat3()/InputFloat4() variants taking a `int decimal_precision` parameter. + - 2020/10/05 (1.79) - removed ImGuiListClipper: Renamed constructor parameters which created an ambiguous alternative to using the ImGuiListClipper::Begin() function, with misleading edge cases (note: imgui_memory_editor <0.40 from imgui_club/ used this old clipper API. Update your copy if needed). + - 2020/09/25 (1.79) - renamed ImGuiSliderFlags_ClampOnInput to ImGuiSliderFlags_AlwaysClamp. Kept redirection enum (will obsolete sooner because previous name was added recently). + - 2020/09/25 (1.79) - renamed style.TabMinWidthForUnselectedCloseButton to style.TabMinWidthForCloseButton. + - 2020/09/21 (1.79) - renamed OpenPopupContextItem() back to OpenPopupOnItemClick(), reverting the change from 1.77. For varieties of reason this is more self-explanatory. + - 2020/09/21 (1.79) - removed return value from OpenPopupOnItemClick() - returned true on mouse release on an item - because it is inconsistent with other popup APIs and makes others misleading. It's also and unnecessary: you can use IsWindowAppearing() after BeginPopup() for a similar result. + - 2020/09/17 (1.79) - removed ImFont::DisplayOffset in favor of ImFontConfig::GlyphOffset. DisplayOffset was applied after scaling and not very meaningful/useful outside of being needed by the default ProggyClean font. If you scaled this value after calling AddFontDefault(), this is now done automatically. It was also getting in the way of better font scaling, so let's get rid of it now! + - 2020/08/17 (1.78) - obsoleted use of the trailing 'float power=1.0f' parameter for DragFloat(), DragFloat2(), DragFloat3(), DragFloat4(), DragFloatRange2(), DragScalar(), DragScalarN(), SliderFloat(), SliderFloat2(), SliderFloat3(), SliderFloat4(), SliderScalar(), SliderScalarN(), VSliderFloat() and VSliderScalar(). + replaced the 'float power=1.0f' argument with integer-based flags defaulting to 0 (as with all our flags). + worked out a backward-compatibility scheme so hopefully most C++ codebase should not be affected. in short, when calling those functions: + - if you omitted the 'power' parameter (likely!), you are not affected. + - if you set the 'power' parameter to 1.0f (same as previous default value): 1/ your compiler may warn on float>int conversion, 2/ everything else will work. 3/ you can replace the 1.0f value with 0 to fix the warning, and be technically correct. + - if you set the 'power' parameter to >1.0f (to enable non-linear editing): 1/ your compiler may warn on float>int conversion, 2/ code will assert at runtime, 3/ in case asserts are disabled, the code will not crash and enable the _Logarithmic flag. 4/ you can replace the >1.0f value with ImGuiSliderFlags_Logarithmic to fix the warning/assert and get a _similar_ effect as previous uses of power >1.0f. + see https://github.com/ocornut/imgui/issues/3361 for all details. + kept inline redirection functions (will obsolete) apart for: DragFloatRange2(), VSliderFloat(), VSliderScalar(). For those three the 'float power=1.0f' version was removed directly as they were most unlikely ever used. + for shared code, you can version check at compile-time with `#if IMGUI_VERSION_NUM >= 17704`. + - obsoleted use of v_min > v_max in DragInt, DragFloat, DragScalar to lock edits (introduced in 1.73, was not demoed nor documented very), will be replaced by a more generic ReadOnly feature. You may use the ImGuiSliderFlags_ReadOnly internal flag in the meantime. + - 2020/06/23 (1.77) - removed BeginPopupContextWindow(const char*, int mouse_button, bool also_over_items) in favor of BeginPopupContextWindow(const char*, ImGuiPopupFlags flags) with ImGuiPopupFlags_NoOverItems. + - 2020/06/15 (1.77) - renamed OpenPopupOnItemClick() to OpenPopupContextItem(). Kept inline redirection function (will obsolete). [NOTE: THIS WAS REVERTED IN 1.79] + - 2020/06/15 (1.77) - removed CalcItemRectClosestPoint() entry point which was made obsolete and asserting in December 2017. + - 2020/04/23 (1.77) - removed unnecessary ID (first arg) of ImFontAtlas::AddCustomRectRegular(). + - 2020/01/22 (1.75) - ImDrawList::AddCircle()/AddCircleFilled() functions don't accept negative radius any more. + - 2019/12/17 (1.75) - [undid this change in 1.76] made Columns() limited to 64 columns by asserting above that limit. While the current code technically supports it, future code may not so we're putting the restriction ahead. + - 2019/12/13 (1.75) - [imgui_internal.h] changed ImRect() default constructor initializes all fields to 0.0f instead of (FLT_MAX,FLT_MAX,-FLT_MAX,-FLT_MAX). If you used ImRect::Add() to create bounding boxes by adding multiple points into it, you may need to fix your initial value. + - 2019/12/08 (1.75) - removed redirecting functions/enums that were marked obsolete in 1.53 (December 2017): + - ShowTestWindow() -> use ShowDemoWindow() + - IsRootWindowFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootWindow) + - IsRootWindowOrAnyChildFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) + - SetNextWindowContentWidth(w) -> use SetNextWindowContentSize(ImVec2(w, 0.0f) + - GetItemsLineHeightWithSpacing() -> use GetFrameHeightWithSpacing() + - ImGuiCol_ChildWindowBg -> use ImGuiCol_ChildBg + - ImGuiStyleVar_ChildWindowRounding -> use ImGuiStyleVar_ChildRounding + - ImGuiTreeNodeFlags_AllowOverlapMode -> use ImGuiTreeNodeFlags_AllowItemOverlap + - IMGUI_DISABLE_TEST_WINDOWS -> use IMGUI_DISABLE_DEMO_WINDOWS + - 2019/12/08 (1.75) - obsoleted calling ImDrawList::PrimReserve() with a negative count (which was vaguely documented and rarely if ever used). Instead, we added an explicit PrimUnreserve() API. + - 2019/12/06 (1.75) - removed implicit default parameter to IsMouseDragging(int button = 0) to be consistent with other mouse functions (none of the other functions have it). + - 2019/11/21 (1.74) - ImFontAtlas::AddCustomRectRegular() now requires an ID larger than 0x110000 (instead of 0x10000) to conform with supporting Unicode planes 1-16 in a future update. ID below 0x110000 will now assert. + - 2019/11/19 (1.74) - renamed IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS to IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS for consistency. + - 2019/11/19 (1.74) - renamed IMGUI_DISABLE_MATH_FUNCTIONS to IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS for consistency. + - 2019/10/22 (1.74) - removed redirecting functions/enums that were marked obsolete in 1.52 (October 2017): + - Begin() [old 5 args version] -> use Begin() [3 args], use SetNextWindowSize() SetNextWindowBgAlpha() if needed + - IsRootWindowOrAnyChildHovered() -> use IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows) + - AlignFirstTextHeightToWidgets() -> use AlignTextToFramePadding() + - SetNextWindowPosCenter() -> use SetNextWindowPos() with a pivot of (0.5f, 0.5f) + - ImFont::Glyph -> use ImFontGlyph + - 2019/10/14 (1.74) - inputs: Fixed a miscalculation in the keyboard/mouse "typematic" repeat delay/rate calculation, used by keys and e.g. repeating mouse buttons as well as the GetKeyPressedAmount() function. + if you were using a non-default value for io.KeyRepeatRate (previous default was 0.250), you can add +io.KeyRepeatDelay to it to compensate for the fix. + The function was triggering on: 0.0 and (delay+rate*N) where (N>=1). Fixed formula responds to (N>=0). Effectively it made io.KeyRepeatRate behave like it was set to (io.KeyRepeatRate + io.KeyRepeatDelay). + If you never altered io.KeyRepeatRate nor used GetKeyPressedAmount() this won't affect you. + - 2019/07/15 (1.72) - removed TreeAdvanceToLabelPos() which is rarely used and only does SetCursorPosX(GetCursorPosX() + GetTreeNodeToLabelSpacing()). Kept redirection function (will obsolete). + - 2019/07/12 (1.72) - renamed ImFontAtlas::CustomRect to ImFontAtlasCustomRect. Kept redirection typedef (will obsolete). + - 2019/06/14 (1.72) - removed redirecting functions/enums names that were marked obsolete in 1.51 (June 2017): ImGuiCol_Column*, ImGuiSetCond_*, IsItemHoveredRect(), IsPosHoveringAnyWindow(), IsMouseHoveringAnyWindow(), IsMouseHoveringWindow(), IMGUI_ONCE_UPON_A_FRAME. Grep this log for details and new names, or see how they were implemented until 1.71. + - 2019/06/07 (1.71) - rendering of child window outer decorations (bg color, border, scrollbars) is now performed as part of the parent window. If you have + overlapping child windows in a same parent, and relied on their relative z-order to be mapped to their submission order, this will affect your rendering. + This optimization is disabled if the parent window has no visual output, because it appears to be the most common situation leading to the creation of overlapping child windows. + Please reach out if you are affected. + - 2019/05/13 (1.71) - renamed SetNextTreeNodeOpen() to SetNextItemOpen(). Kept inline redirection function (will obsolete). + - 2019/05/11 (1.71) - changed io.AddInputCharacter(unsigned short c) signature to io.AddInputCharacter(unsigned int c). + - 2019/04/29 (1.70) - improved ImDrawList thick strokes (>1.0f) preserving correct thickness up to 90 degrees angles (e.g. rectangles). If you have custom rendering using thick lines, they will appear thicker now. + - 2019/04/29 (1.70) - removed GetContentRegionAvailWidth(), use GetContentRegionAvail().x instead. Kept inline redirection function (will obsolete). + - 2019/03/04 (1.69) - renamed GetOverlayDrawList() to GetForegroundDrawList(). Kept redirection function (will obsolete). + - 2019/02/26 (1.69) - renamed ImGuiColorEditFlags_RGB/ImGuiColorEditFlags_HSV/ImGuiColorEditFlags_HEX to ImGuiColorEditFlags_DisplayRGB/ImGuiColorEditFlags_DisplayHSV/ImGuiColorEditFlags_DisplayHex. Kept redirection enums (will obsolete). + - 2019/02/14 (1.68) - made it illegal/assert when io.DisplayTime == 0.0f (with an exception for the first frame). If for some reason your time step calculation gives you a zero value, replace it with an arbitrarily small value! + - 2019/02/01 (1.68) - removed io.DisplayVisibleMin/DisplayVisibleMax (which were marked obsolete and removed from viewport/docking branch already). + - 2019/01/06 (1.67) - renamed io.InputCharacters[], marked internal as was always intended. Please don't access directly, and use AddInputCharacter() instead! + - 2019/01/06 (1.67) - renamed ImFontAtlas::GlyphRangesBuilder to ImFontGlyphRangesBuilder. Kept redirection typedef (will obsolete). + - 2018/12/20 (1.67) - made it illegal to call Begin("") with an empty string. This somehow half-worked before but had various undesirable side-effects. + - 2018/12/10 (1.67) - renamed io.ConfigResizeWindowsFromEdges to io.ConfigWindowsResizeFromEdges as we are doing a large pass on configuration flags. + - 2018/10/12 (1.66) - renamed misc/stl/imgui_stl.* to misc/cpp/imgui_stdlib.* in prevision for other C++ helper files. + - 2018/09/28 (1.66) - renamed SetScrollHere() to SetScrollHereY(). Kept redirection function (will obsolete). + - 2018/09/06 (1.65) - renamed stb_truetype.h to imstb_truetype.h, stb_textedit.h to imstb_textedit.h, and stb_rect_pack.h to imstb_rectpack.h. + If you were conveniently using the imgui copy of those STB headers in your project you will have to update your include paths. + - 2018/09/05 (1.65) - renamed io.OptCursorBlink/io.ConfigCursorBlink to io.ConfigInputTextCursorBlink. (#1427) + - 2018/08/31 (1.64) - added imgui_widgets.cpp file, extracted and moved widgets code out of imgui.cpp into imgui_widgets.cpp. Re-ordered some of the code remaining in imgui.cpp. + NONE OF THE FUNCTIONS HAVE CHANGED. THE CODE IS SEMANTICALLY 100% IDENTICAL, BUT _EVERY_ FUNCTION HAS BEEN MOVED. + Because of this, any local modifications to imgui.cpp will likely conflict when you update. Read docs/CHANGELOG.txt for suggestions. + - 2018/08/22 (1.63) - renamed IsItemDeactivatedAfterChange() to IsItemDeactivatedAfterEdit() for consistency with new IsItemEdited() API. Kept redirection function (will obsolete soonish as IsItemDeactivatedAfterChange() is very recent). + - 2018/08/21 (1.63) - renamed ImGuiTextEditCallback to ImGuiInputTextCallback, ImGuiTextEditCallbackData to ImGuiInputTextCallbackData for consistency. Kept redirection types (will obsolete). + - 2018/08/21 (1.63) - removed ImGuiInputTextCallbackData::ReadOnly since it is a duplication of (ImGuiInputTextCallbackData::Flags & ImGuiInputTextFlags_ReadOnly). + - 2018/08/01 (1.63) - removed per-window ImGuiWindowFlags_ResizeFromAnySide beta flag in favor of a global io.ConfigResizeWindowsFromEdges [update 1.67 renamed to ConfigWindowsResizeFromEdges] to enable the feature. + - 2018/08/01 (1.63) - renamed io.OptCursorBlink to io.ConfigCursorBlink [-> io.ConfigInputTextCursorBlink in 1.65], io.OptMacOSXBehaviors to ConfigMacOSXBehaviors for consistency. + - 2018/07/22 (1.63) - changed ImGui::GetTime() return value from float to double to avoid accumulating floating point imprecisions over time. + - 2018/07/08 (1.63) - style: renamed ImGuiCol_ModalWindowDarkening to ImGuiCol_ModalWindowDimBg for consistency with other features. Kept redirection enum (will obsolete). + - 2018/06/08 (1.62) - examples: the imgui_impl_XXX files have been split to separate platform (Win32, GLFW, SDL2, etc.) from renderer (DX11, OpenGL, Vulkan, etc.). + old backends will still work as is, however prefer using the separated backends as they will be updated to support multi-viewports. + when adopting new backends follow the main.cpp code of your preferred examples/ folder to know which functions to call. + in particular, note that old backends called ImGui::NewFrame() at the end of their ImGui_ImplXXXX_NewFrame() function. + - 2018/06/06 (1.62) - renamed GetGlyphRangesChinese() to GetGlyphRangesChineseFull() to distinguish other variants and discourage using the full set. + - 2018/06/06 (1.62) - TreeNodeEx()/TreeNodeBehavior(): the ImGuiTreeNodeFlags_CollapsingHeader helper now include the ImGuiTreeNodeFlags_NoTreePushOnOpen flag. See Changelog for details. + - 2018/05/03 (1.61) - DragInt(): the default compile-time format string has been changed from "%.0f" to "%d", as we are not using integers internally any more. + If you used DragInt() with custom format strings, make sure you change them to use %d or an integer-compatible format. + To honor backward-compatibility, the DragInt() code will currently parse and modify format strings to replace %*f with %d, giving time to users to upgrade their code. + If you have IMGUI_DISABLE_OBSOLETE_FUNCTIONS enabled, the code will instead assert! You may run a reg-exp search on your codebase for e.g. "DragInt.*%f" to help you find them. + - 2018/04/28 (1.61) - obsoleted InputFloat() functions taking an optional "int decimal_precision" in favor of an equivalent and more flexible "const char* format", + consistent with other functions. Kept redirection functions (will obsolete). + - 2018/04/09 (1.61) - IM_DELETE() helper function added in 1.60 doesn't clear the input _pointer_ reference, more consistent with expectation and allows passing r-value. + - 2018/03/20 (1.60) - renamed io.WantMoveMouse to io.WantSetMousePos for consistency and ease of understanding (was added in 1.52, _not_ used by core and only honored by some backend ahead of merging the Nav branch). + - 2018/03/12 (1.60) - removed ImGuiCol_CloseButton, ImGuiCol_CloseButtonActive, ImGuiCol_CloseButtonHovered as the closing cross uses regular button colors now. + - 2018/03/08 (1.60) - changed ImFont::DisplayOffset.y to default to 0 instead of +1. Fixed rounding of Ascent/Descent to match TrueType renderer. If you were adding or subtracting to ImFont::DisplayOffset check if your fonts are correctly aligned vertically. + - 2018/03/03 (1.60) - renamed ImGuiStyleVar_Count_ to ImGuiStyleVar_COUNT and ImGuiMouseCursor_Count_ to ImGuiMouseCursor_COUNT for consistency with other public enums. + - 2018/02/18 (1.60) - BeginDragDropSource(): temporarily removed the optional mouse_button=0 parameter because it is not really usable in many situations at the moment. + - 2018/02/16 (1.60) - obsoleted the io.RenderDrawListsFn callback, you can call your graphics engine render function after ImGui::Render(). Use ImGui::GetDrawData() to retrieve the ImDrawData* to display. + - 2018/02/07 (1.60) - reorganized context handling to be more explicit, + - YOU NOW NEED TO CALL ImGui::CreateContext() AT THE BEGINNING OF YOUR APP, AND CALL ImGui::DestroyContext() AT THE END. + - removed Shutdown() function, as DestroyContext() serve this purpose. + - you may pass a ImFontAtlas* pointer to CreateContext() to share a font atlas between contexts. Otherwise CreateContext() will create its own font atlas instance. + - removed allocator parameters from CreateContext(), they are now setup with SetAllocatorFunctions(), and shared by all contexts. + - removed the default global context and font atlas instance, which were confusing for users of DLL reloading and users of multiple contexts. + - 2018/01/31 (1.60) - moved sample TTF files from extra_fonts/ to misc/fonts/. If you loaded files directly from the imgui repo you may need to update your paths. + - 2018/01/11 (1.60) - obsoleted IsAnyWindowHovered() in favor of IsWindowHovered(ImGuiHoveredFlags_AnyWindow). Kept redirection function (will obsolete). + - 2018/01/11 (1.60) - obsoleted IsAnyWindowFocused() in favor of IsWindowFocused(ImGuiFocusedFlags_AnyWindow). Kept redirection function (will obsolete). + - 2018/01/03 (1.60) - renamed ImGuiSizeConstraintCallback to ImGuiSizeCallback, ImGuiSizeConstraintCallbackData to ImGuiSizeCallbackData. + - 2017/12/29 (1.60) - removed CalcItemRectClosestPoint() which was weird and not really used by anyone except demo code. If you need it it's easy to replicate on your side. + - 2017/12/24 (1.53) - renamed the emblematic ShowTestWindow() function to ShowDemoWindow(). Kept redirection function (will obsolete). + - 2017/12/21 (1.53) - ImDrawList: renamed style.AntiAliasedShapes to style.AntiAliasedFill for consistency and as a way to explicitly break code that manipulate those flag at runtime. You can now manipulate ImDrawList::Flags + - 2017/12/21 (1.53) - ImDrawList: removed 'bool anti_aliased = true' final parameter of ImDrawList::AddPolyline() and ImDrawList::AddConvexPolyFilled(). Prefer manipulating ImDrawList::Flags if you need to toggle them during the frame. + - 2017/12/14 (1.53) - using the ImGuiWindowFlags_NoScrollWithMouse flag on a child window forwards the mouse wheel event to the parent window, unless either ImGuiWindowFlags_NoInputs or ImGuiWindowFlags_NoScrollbar are also set. + - 2017/12/13 (1.53) - renamed GetItemsLineHeightWithSpacing() to GetFrameHeightWithSpacing(). Kept redirection function (will obsolete). + - 2017/12/13 (1.53) - obsoleted IsRootWindowFocused() in favor of using IsWindowFocused(ImGuiFocusedFlags_RootWindow). Kept redirection function (will obsolete). + - obsoleted IsRootWindowOrAnyChildFocused() in favor of using IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows). Kept redirection function (will obsolete). + - 2017/12/12 (1.53) - renamed ImGuiTreeNodeFlags_AllowOverlapMode to ImGuiTreeNodeFlags_AllowItemOverlap. Kept redirection enum (will obsolete). + - 2017/12/10 (1.53) - removed SetNextWindowContentWidth(), prefer using SetNextWindowContentSize(). Kept redirection function (will obsolete). + - 2017/11/27 (1.53) - renamed ImGuiTextBuffer::append() helper to appendf(), appendv() to appendfv(). If you copied the 'Log' demo in your code, it uses appendv() so that needs to be renamed. + - 2017/11/18 (1.53) - Style, Begin: removed ImGuiWindowFlags_ShowBorders window flag. Borders are now fully set up in the ImGuiStyle structure (see e.g. style.FrameBorderSize, style.WindowBorderSize). Use ImGui::ShowStyleEditor() to look them up. + Please note that the style system will keep evolving (hopefully stabilizing in Q1 2018), and so custom styles will probably subtly break over time. It is recommended you use the StyleColorsClassic(), StyleColorsDark(), StyleColorsLight() functions. + - 2017/11/18 (1.53) - Style: removed ImGuiCol_ComboBg in favor of combo boxes using ImGuiCol_PopupBg for consistency. + - 2017/11/18 (1.53) - Style: renamed ImGuiCol_ChildWindowBg to ImGuiCol_ChildBg. + - 2017/11/18 (1.53) - Style: renamed style.ChildWindowRounding to style.ChildRounding, ImGuiStyleVar_ChildWindowRounding to ImGuiStyleVar_ChildRounding. + - 2017/11/02 (1.53) - obsoleted IsRootWindowOrAnyChildHovered() in favor of using IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); + - 2017/10/24 (1.52) - renamed IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCS/IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCS to IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS/IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS for consistency. + - 2017/10/20 (1.52) - changed IsWindowHovered() default parameters behavior to return false if an item is active in another window (e.g. click-dragging item from another window to this window). You can use the newly introduced IsWindowHovered() flags to requests this specific behavior if you need it. + - 2017/10/20 (1.52) - marked IsItemHoveredRect()/IsMouseHoveringWindow() as obsolete, in favor of using the newly introduced flags for IsItemHovered() and IsWindowHovered(). See https://github.com/ocornut/imgui/issues/1382 for details. + removed the IsItemRectHovered()/IsWindowRectHovered() names introduced in 1.51 since they were merely more consistent names for the two functions we are now obsoleting. + IsItemHoveredRect() --> IsItemHovered(ImGuiHoveredFlags_RectOnly) + IsMouseHoveringAnyWindow() --> IsWindowHovered(ImGuiHoveredFlags_AnyWindow) + IsMouseHoveringWindow() --> IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) [weird, old behavior] + - 2017/10/17 (1.52) - marked the old 5-parameters version of Begin() as obsolete (still available). Use SetNextWindowSize()+Begin() instead! + - 2017/10/11 (1.52) - renamed AlignFirstTextHeightToWidgets() to AlignTextToFramePadding(). Kept inline redirection function (will obsolete). + - 2017/09/26 (1.52) - renamed ImFont::Glyph to ImFontGlyph. Kept redirection typedef (will obsolete). + - 2017/09/25 (1.52) - removed SetNextWindowPosCenter() because SetNextWindowPos() now has the optional pivot information to do the same and more. Kept redirection function (will obsolete). + - 2017/08/25 (1.52) - io.MousePos needs to be set to ImVec2(-FLT_MAX,-FLT_MAX) when mouse is unavailable/missing. Previously ImVec2(-1,-1) was enough but we now accept negative mouse coordinates. In your backend if you need to support unavailable mouse, make sure to replace "io.MousePos = ImVec2(-1,-1)" with "io.MousePos = ImVec2(-FLT_MAX,-FLT_MAX)". + - 2017/08/22 (1.51) - renamed IsItemHoveredRect() to IsItemRectHovered(). Kept inline redirection function (will obsolete). -> (1.52) use IsItemHovered(ImGuiHoveredFlags_RectOnly)! + - renamed IsMouseHoveringAnyWindow() to IsAnyWindowHovered() for consistency. Kept inline redirection function (will obsolete). + - renamed IsMouseHoveringWindow() to IsWindowRectHovered() for consistency. Kept inline redirection function (will obsolete). + - 2017/08/20 (1.51) - renamed GetStyleColName() to GetStyleColorName() for consistency. + - 2017/08/20 (1.51) - added PushStyleColor(ImGuiCol idx, ImU32 col) overload, which _might_ cause an "ambiguous call" compilation error if you are using ImColor() with implicit cast. Cast to ImU32 or ImVec4 explicily to fix. + - 2017/08/15 (1.51) - marked the weird IMGUI_ONCE_UPON_A_FRAME helper macro as obsolete. prefer using the more explicit ImGuiOnceUponAFrame type. + - 2017/08/15 (1.51) - changed parameter order for BeginPopupContextWindow() from (const char*,int buttons,bool also_over_items) to (const char*,int buttons,bool also_over_items). Note that most calls relied on default parameters completely. + - 2017/08/13 (1.51) - renamed ImGuiCol_Column to ImGuiCol_Separator, ImGuiCol_ColumnHovered to ImGuiCol_SeparatorHovered, ImGuiCol_ColumnActive to ImGuiCol_SeparatorActive. Kept redirection enums (will obsolete). + - 2017/08/11 (1.51) - renamed ImGuiSetCond_Always to ImGuiCond_Always, ImGuiSetCond_Once to ImGuiCond_Once, ImGuiSetCond_FirstUseEver to ImGuiCond_FirstUseEver, ImGuiSetCond_Appearing to ImGuiCond_Appearing. Kept redirection enums (will obsolete). + - 2017/08/09 (1.51) - removed ValueColor() helpers, they are equivalent to calling Text(label) + SameLine() + ColorButton(). + - 2017/08/08 (1.51) - removed ColorEditMode() and ImGuiColorEditMode in favor of ImGuiColorEditFlags and parameters to the various Color*() functions. The SetColorEditOptions() allows to initialize default but the user can still change them with right-click context menu. + - changed prototype of 'ColorEdit4(const char* label, float col[4], bool show_alpha = true)' to 'ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0)', where passing flags = 0x01 is a safe no-op (hello dodgy backward compatibility!). - check and run the demo window, under "Color/Picker Widgets", to understand the various new options. + - changed prototype of rarely used 'ColorButton(ImVec4 col, bool small_height = false, bool outline_border = true)' to 'ColorButton(const char* desc_id, ImVec4 col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0, 0))' + - 2017/07/20 (1.51) - removed IsPosHoveringAnyWindow(ImVec2), which was partly broken and misleading. ASSERT + redirect user to io.WantCaptureMouse + - 2017/05/26 (1.50) - removed ImFontConfig::MergeGlyphCenterV in favor of a more multipurpose ImFontConfig::GlyphOffset. + - 2017/05/01 (1.50) - renamed ImDrawList::PathFill() (rarely used directly) to ImDrawList::PathFillConvex() for clarity. + - 2016/11/06 (1.50) - BeginChild(const char*) now applies the stack id to the provided label, consistently with other functions as it should always have been. It shouldn't affect you unless (extremely unlikely) you were appending multiple times to a same child from different locations of the stack id. If that's the case, generate an id with GetID() and use it instead of passing string to BeginChild(). + - 2016/10/15 (1.50) - avoid 'void* user_data' parameter to io.SetClipboardTextFn/io.GetClipboardTextFn pointers. We pass io.ClipboardUserData to it. + - 2016/09/25 (1.50) - style.WindowTitleAlign is now a ImVec2 (ImGuiAlign enum was removed). set to (0.5f,0.5f) for horizontal+vertical centering, (0.0f,0.0f) for upper-left, etc. + - 2016/07/30 (1.50) - SameLine(x) with x>0.0f is now relative to left of column/group if any, and not always to left of window. This was sort of always the intent and hopefully, breakage should be minimal. + - 2016/05/12 (1.49) - title bar (using ImGuiCol_TitleBg/ImGuiCol_TitleBgActive colors) isn't rendered over a window background (ImGuiCol_WindowBg color) anymore. + If your TitleBg/TitleBgActive alpha was 1.0f or you are using the default theme it will not affect you, otherwise if <1.0f you need to tweak your custom theme to readjust for the fact that we don't draw a WindowBg background behind the title bar. + This helper function will convert an old TitleBg/TitleBgActive color into a new one with the same visual output, given the OLD color and the OLD WindowBg color: + ImVec4 ConvertTitleBgCol(const ImVec4& win_bg_col, const ImVec4& title_bg_col) { float new_a = 1.0f - ((1.0f - win_bg_col.w) * (1.0f - title_bg_col.w)), k = title_bg_col.w / new_a; return ImVec4((win_bg_col.x * win_bg_col.w + title_bg_col.x) * k, (win_bg_col.y * win_bg_col.w + title_bg_col.y) * k, (win_bg_col.z * win_bg_col.w + title_bg_col.z) * k, new_a); } + If this is confusing, pick the RGB value from title bar from an old screenshot and apply this as TitleBg/TitleBgActive. Or you may just create TitleBgActive from a tweaked TitleBg color. + - 2016/05/07 (1.49) - removed confusing set of GetInternalState(), GetInternalStateSize(), SetInternalState() functions. Now using CreateContext(), DestroyContext(), GetCurrentContext(), SetCurrentContext(). + - 2016/05/02 (1.49) - renamed SetNextTreeNodeOpened() to SetNextTreeNodeOpen(), no redirection. + - 2016/05/01 (1.49) - obsoleted old signature of CollapsingHeader(const char* label, const char* str_id = NULL, bool display_frame = true, bool default_open = false) as extra parameters were badly designed and rarely used. You can replace the "default_open = true" flag in new API with CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen). + - 2016/04/26 (1.49) - changed ImDrawList::PushClipRect(ImVec4 rect) to ImDrawList::PushClipRect(Imvec2 min,ImVec2 max,bool intersect_with_current_clip_rect=false). Note that higher-level ImGui::PushClipRect() is preferable because it will clip at logic/widget level, whereas ImDrawList::PushClipRect() only affect your renderer. + - 2016/04/03 (1.48) - removed style.WindowFillAlphaDefault setting which was redundant. Bake default BG alpha inside style.Colors[ImGuiCol_WindowBg] and all other Bg color values. (ref GitHub issue #337). + - 2016/04/03 (1.48) - renamed ImGuiCol_TooltipBg to ImGuiCol_PopupBg, used by popups/menus and tooltips. popups/menus were previously using ImGuiCol_WindowBg. (ref github issue #337) + - 2016/03/21 (1.48) - renamed GetWindowFont() to GetFont(), GetWindowFontSize() to GetFontSize(). Kept inline redirection function (will obsolete). + - 2016/03/02 (1.48) - InputText() completion/history/always callbacks: if you modify the text buffer manually (without using DeleteChars()/InsertChars() helper) you need to maintain the BufTextLen field. added an assert. + - 2016/01/23 (1.48) - fixed not honoring exact width passed to PushItemWidth(), previously it would add extra FramePadding.x*2 over that width. if you had manual pixel-perfect alignment in place it might affect you. + - 2015/12/27 (1.48) - fixed ImDrawList::AddRect() which used to render a rectangle 1 px too large on each axis. + - 2015/12/04 (1.47) - renamed Color() helpers to ValueColor() - dangerously named, rarely used and probably to be made obsolete. + - 2015/08/29 (1.45) - with the addition of horizontal scrollbar we made various fixes to inconsistencies with dealing with cursor position. + GetCursorPos()/SetCursorPos() functions now include the scrolled amount. It shouldn't affect the majority of users, but take note that SetCursorPosX(100.0f) puts you at +100 from the starting x position which may include scrolling, not at +100 from the window left side. + GetContentRegionMax()/GetWindowContentRegionMin()/GetWindowContentRegionMax() functions allow include the scrolled amount. Typically those were used in cases where no scrolling would happen so it may not be a problem, but watch out! + - 2015/08/29 (1.45) - renamed style.ScrollbarWidth to style.ScrollbarSize + - 2015/08/05 (1.44) - split imgui.cpp into extra files: imgui_demo.cpp imgui_draw.cpp imgui_internal.h that you need to add to your project. + - 2015/07/18 (1.44) - fixed angles in ImDrawList::PathArcTo(), PathArcToFast() (introduced in 1.43) being off by an extra PI for no justifiable reason + - 2015/07/14 (1.43) - add new ImFontAtlas::AddFont() API. For the old AddFont***, moved the 'font_no' parameter of ImFontAtlas::AddFont** functions to the ImFontConfig structure. + you need to render your textured triangles with bilinear filtering to benefit from sub-pixel positioning of text. + - 2015/07/08 (1.43) - switched rendering data to use indexed rendering. this is saving a fair amount of CPU/GPU and enables us to get anti-aliasing for a marginal cost. + this necessary change will break your rendering function! the fix should be very easy. sorry for that :( + - if you are using a vanilla copy of one of the imgui_impl_XXX.cpp provided in the example, you just need to update your copy and you can ignore the rest. + - the signature of the io.RenderDrawListsFn handler has changed! + old: ImGui_XXXX_RenderDrawLists(ImDrawList** const cmd_lists, int cmd_lists_count) + new: ImGui_XXXX_RenderDrawLists(ImDrawData* draw_data). + parameters: 'cmd_lists' becomes 'draw_data->CmdLists', 'cmd_lists_count' becomes 'draw_data->CmdListsCount' + ImDrawList: 'commands' becomes 'CmdBuffer', 'vtx_buffer' becomes 'VtxBuffer', 'IdxBuffer' is new. + ImDrawCmd: 'vtx_count' becomes 'ElemCount', 'clip_rect' becomes 'ClipRect', 'user_callback' becomes 'UserCallback', 'texture_id' becomes 'TextureId'. + - each ImDrawList now contains both a vertex buffer and an index buffer. For each command, render ElemCount/3 triangles using indices from the index buffer. + - if you REALLY cannot render indexed primitives, you can call the draw_data->DeIndexAllBuffers() method to de-index the buffers. This is slow and a waste of CPU/GPU. Prefer using indexed rendering! + - refer to code in the examples/ folder or ask on the GitHub if you are unsure of how to upgrade. please upgrade! + - 2015/07/10 (1.43) - changed SameLine() parameters from int to float. + - 2015/07/02 (1.42) - renamed SetScrollPosHere() to SetScrollFromCursorPos(). Kept inline redirection function (will obsolete). + - 2015/07/02 (1.42) - renamed GetScrollPosY() to GetScrollY(). Necessary to reduce confusion along with other scrolling functions, because positions (e.g. cursor position) are not equivalent to scrolling amount. + - 2015/06/14 (1.41) - changed ImageButton() default bg_col parameter from (0,0,0,1) (black) to (0,0,0,0) (transparent) - makes a difference when texture have transparence + - 2015/06/14 (1.41) - changed Selectable() API from (label, selected, size) to (label, selected, flags, size). Size override should have been rarely used. Sorry! + - 2015/05/31 (1.40) - renamed GetWindowCollapsed() to IsWindowCollapsed() for consistency. Kept inline redirection function (will obsolete). + - 2015/05/31 (1.40) - renamed IsRectClipped() to IsRectVisible() for consistency. Note that return value is opposite! Kept inline redirection function (will obsolete). + - 2015/05/27 (1.40) - removed the third 'repeat_if_held' parameter from Button() - sorry! it was rarely used and inconsistent. Use PushButtonRepeat(true) / PopButtonRepeat() to enable repeat on desired buttons. + - 2015/05/11 (1.40) - changed BeginPopup() API, takes a string identifier instead of a bool. ImGui needs to manage the open/closed state of popups. Call OpenPopup() to actually set the "open" state of a popup. BeginPopup() returns true if the popup is opened. + - 2015/05/03 (1.40) - removed style.AutoFitPadding, using style.WindowPadding makes more sense (the default values were already the same). + - 2015/04/13 (1.38) - renamed IsClipped() to IsRectClipped(). Kept inline redirection function until 1.50. + - 2015/04/09 (1.38) - renamed ImDrawList::AddArc() to ImDrawList::AddArcFast() for compatibility with future API + - 2015/04/03 (1.38) - removed ImGuiCol_CheckHovered, ImGuiCol_CheckActive, replaced with the more general ImGuiCol_FrameBgHovered, ImGuiCol_FrameBgActive. + - 2014/04/03 (1.38) - removed support for passing -FLT_MAX..+FLT_MAX as the range for a SliderFloat(). Use DragFloat() or Inputfloat() instead. + - 2015/03/17 (1.36) - renamed GetItemBoxMin()/GetItemBoxMax()/IsMouseHoveringBox() to GetItemRectMin()/GetItemRectMax()/IsMouseHoveringRect(). Kept inline redirection function until 1.50. + - 2015/03/15 (1.36) - renamed style.TreeNodeSpacing to style.IndentSpacing, ImGuiStyleVar_TreeNodeSpacing to ImGuiStyleVar_IndentSpacing + - 2015/03/13 (1.36) - renamed GetWindowIsFocused() to IsWindowFocused(). Kept inline redirection function until 1.50. + - 2015/03/08 (1.35) - renamed style.ScrollBarWidth to style.ScrollbarWidth (casing) + - 2015/02/27 (1.34) - renamed OpenNextNode(bool) to SetNextTreeNodeOpened(bool, ImGuiSetCond). Kept inline redirection function until 1.50. + - 2015/02/27 (1.34) - renamed ImGuiSetCondition_*** to ImGuiSetCond_***, and _FirstUseThisSession becomes _Once. + - 2015/02/11 (1.32) - changed text input callback ImGuiTextEditCallback return type from void-->int. reserved for future use, return 0 for now. + - 2015/02/10 (1.32) - renamed GetItemWidth() to CalcItemWidth() to clarify its evolving behavior + - 2015/02/08 (1.31) - renamed GetTextLineSpacing() to GetTextLineHeightWithSpacing() + - 2015/02/01 (1.31) - removed IO.MemReallocFn (unused) + - 2015/01/19 (1.30) - renamed ImGuiStorage::GetIntPtr()/GetFloatPtr() to GetIntRef()/GetIntRef() because Ptr was conflicting with actual pointer storage functions. + - 2015/01/11 (1.30) - big font/image API change! now loads TTF file. allow for multiple fonts. no need for a PNG loader. + - 2015/01/11 (1.30) - removed GetDefaultFontData(). uses io.Fonts->GetTextureData*() API to retrieve uncompressed pixels. + - old: const void* png_data; unsigned int png_size; ImGui::GetDefaultFontData(NULL, NULL, &png_data, &png_size); [..Upload texture to GPU..]; + - new: unsigned char* pixels; int width, height; io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); [..Upload texture to GPU..]; io.Fonts->SetTexID(YourTexIdentifier); + you now have more flexibility to load multiple TTF fonts and manage the texture buffer for internal needs. It is now recommended that you sample the font texture with bilinear interpolation. + - 2015/01/11 (1.30) - added texture identifier in ImDrawCmd passed to your render function (we can now render images). make sure to call io.Fonts->SetTexID() + - 2015/01/11 (1.30) - removed IO.PixelCenterOffset (unnecessary, can be handled in user projection matrix) + - 2015/01/11 (1.30) - removed ImGui::IsItemFocused() in favor of ImGui::IsItemActive() which handles all widgets + - 2014/12/10 (1.18) - removed SetNewWindowDefaultPos() in favor of new generic API SetNextWindowPos(pos, ImGuiSetCondition_FirstUseEver) + - 2014/11/28 (1.17) - moved IO.Font*** options to inside the IO.Font-> structure (FontYOffset, FontTexUvForWhite, FontBaseScale, FontFallbackGlyph) + - 2014/11/26 (1.17) - reworked syntax of IMGUI_ONCE_UPON_A_FRAME helper macro to increase compiler compatibility + - 2014/11/07 (1.15) - renamed IsHovered() to IsItemHovered() + - 2014/10/02 (1.14) - renamed IMGUI_INCLUDE_IMGUI_USER_CPP to IMGUI_INCLUDE_IMGUI_USER_INL and imgui_user.cpp to imgui_user.inl (more IDE friendly) + - 2014/09/25 (1.13) - removed 'text_end' parameter from IO.SetClipboardTextFn (the string is now always zero-terminated for simplicity) + - 2014/09/24 (1.12) - renamed SetFontScale() to SetWindowFontScale() + - 2014/09/24 (1.12) - moved IM_MALLOC/IM_REALLOC/IM_FREE preprocessor defines to IO.MemAllocFn/IO.MemReallocFn/IO.MemFreeFn + - 2014/08/30 (1.09) - removed IO.FontHeight (now computed automatically) + - 2014/08/30 (1.09) - moved IMGUI_FONT_TEX_UV_FOR_WHITE preprocessor define to IO.FontTexUvForWhite + - 2014/08/28 (1.09) - changed the behavior of IO.PixelCenterOffset following various rendering fixes + + + FREQUENTLY ASKED QUESTIONS (FAQ) + ================================ + + Read all answers online: + https://www.dearimgui.org/faq or https://github.com/ocornut/imgui/blob/master/docs/FAQ.md (same url) + Read all answers locally (with a text editor or ideally a Markdown viewer): + docs/FAQ.md + Some answers are copied down here to facilitate searching in code. + + Q&A: Basics + =========== + + Q: Where is the documentation? + A: This library is poorly documented at the moment and expects the user to be acquainted with C/C++. + - Run the examples/ and explore them. + - See demo code in imgui_demo.cpp and particularly the ImGui::ShowDemoWindow() function. + - The demo covers most features of Dear ImGui, so you can read the code and see its output. + - See documentation and comments at the top of imgui.cpp + effectively imgui.h. + - Dozens of standalone example applications using e.g. OpenGL/DirectX are provided in the + examples/ folder to explain how to integrate Dear ImGui with your own engine/application. + - The Wiki (https://github.com/ocornut/imgui/wiki) has many resources and links. + - The Glossary (https://github.com/ocornut/imgui/wiki/Glossary) page also may be useful. + - Your programming IDE is your friend, find the type or function declaration to find comments + associated with it. + + Q: What is this library called? + Q: Which version should I get? + >> This library is called "Dear ImGui", please don't call it "ImGui" :) + >> See https://www.dearimgui.org/faq for details. + + Q&A: Integration + ================ + + Q: How to get started? + A: Read 'PROGRAMMER GUIDE' above. Read examples/README.txt. + + Q: How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application? + A: You should read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags! + >> See https://www.dearimgui.org/faq for a fully detailed answer. You really want to read this. + + Q. How can I enable keyboard controls? + Q: How can I use this without a mouse, without a keyboard or without a screen? (gamepad, input share, remote display) + Q: I integrated Dear ImGui in my engine and little squares are showing instead of text... + Q: I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around... + Q: I integrated Dear ImGui in my engine and some elements are displaying outside their expected windows boundaries... + >> See https://www.dearimgui.org/faq + + Q&A: Usage + ---------- + + Q: About the ID Stack system.. + - Why is my widget not reacting when I click on it? + - How can I have widgets with an empty label? + - How can I have multiple widgets with the same label? + - How can I have multiple windows with the same label? + Q: How can I display an image? What is ImTextureID, how does it works? + Q: How can I use my own math types instead of ImVec2/ImVec4? + Q: How can I interact with standard C++ types (such as std::string and std::vector)? + Q: How can I display custom shapes? (using low-level ImDrawList API) + >> See https://www.dearimgui.org/faq + + Q&A: Fonts, Text + ================ + + Q: How should I handle DPI in my application? + Q: How can I load a different font than the default? + Q: How can I easily use icons in my application? + Q: How can I load multiple fonts? + Q: How can I display and input non-Latin characters such as Chinese, Japanese, Korean, Cyrillic? + >> See https://www.dearimgui.org/faq and https://github.com/ocornut/imgui/edit/master/docs/FONTS.md + + Q&A: Concerns + ============= + + Q: Who uses Dear ImGui? + Q: Can you create elaborate/serious tools with Dear ImGui? + Q: Can you reskin the look of Dear ImGui? + Q: Why using C++ (as opposed to C)? + >> See https://www.dearimgui.org/faq + + Q&A: Community + ============== + + Q: How can I help? + A: - Businesses: please reach out to "contact AT dearimgui.com" if you work in a place using Dear ImGui! + We can discuss ways for your company to fund development via invoiced technical support, maintenance or sponsoring contacts. + This is among the most useful thing you can do for Dear ImGui. With increased funding, we can hire more people working on this project. + - Individuals: you can support continued development via PayPal donations. See README. + - If you are experienced with Dear ImGui and C++, look at the GitHub issues, look at the Wiki, read docs/TODO.txt + and see how you want to help and can help! + - Disclose your usage of Dear ImGui via a dev blog post, a tweet, a screenshot, a mention somewhere etc. + You may post screenshot or links in the gallery threads. Visuals are ideal as they inspire other programmers. + But even without visuals, disclosing your use of dear imgui helps the library grow credibility, and help other teams and programmers with taking decisions. + - If you have issues or if you need to hack into the library, even if you don't expect any support it is useful that you share your issues (on GitHub or privately). + +*/ + +//------------------------------------------------------------------------- +// [SECTION] INCLUDES +//------------------------------------------------------------------------- + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include "imgui_internal.h" + +// System includes +#include // toupper +#include // vsnprintf, sscanf, printf +#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier +#include // intptr_t +#else +#include // intptr_t +#endif + +// [Windows] On non-Visual Studio compilers, we default to IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS unless explicitly enabled +#if defined(_WIN32) && !defined(_MSC_VER) && !defined(IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) +#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS +#endif + +// [Windows] OS specific includes (optional) +#if defined(_WIN32) && defined(IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS) && defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) && defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) +#define IMGUI_DISABLE_WIN32_FUNCTIONS +#endif +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef __MINGW32__ +#include // _wfopen, OpenClipboard +#else +#include +#endif +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) // UWP doesn't have all Win32 functions +#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS +#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS +#endif +#endif + +// [Apple] OS specific includes +#if defined(__APPLE__) +#include +#endif + +// Visual Studio warnings +#ifdef _MSC_VER +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types +#endif +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. +#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. +#pragma clang diagnostic ignored "-Wexit-time-destructors" // warning: declaration requires an exit-time destructor // exit-time destruction order is undefined. if MemFree() leads to users code that has been disabled before exit it might cause problems. ImGui coding style welcomes static/globals. +#pragma clang diagnostic ignored "-Wglobal-constructors" // warning: declaration requires a global destructor // similar to above, not sure what the exact difference is. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wformat-pedantic" // warning: format specifies type 'void *' but the argument has type 'xxxx *' // unreasonable, would lead to casting every %p arg to void*. probably enabled by -pedantic. +#pragma clang diagnostic ignored "-Wint-to-void-pointer-cast" // warning: cast to 'void *' from smaller integer type 'int' +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#elif defined(__GNUC__) +// We disable -Wpragmas because GCC doesn't provide an has_warning equivalent and some forks/patches may not following the warning/version association. +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used +#pragma GCC diagnostic ignored "-Wint-to-pointer-cast" // warning: cast to pointer from integer of different size +#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'void*', but argument 6 has type 'ImGuiWindow*' +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value +#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked +#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when assuming that (X - c) > X is always false +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#endif + +// Debug options +#define IMGUI_DEBUG_NAV_SCORING 0 // Display navigation scoring preview when hovering items. Display last moving direction matches when holding CTRL +#define IMGUI_DEBUG_NAV_RECTS 0 // Display the reference navigation rectangle for each window +#define IMGUI_DEBUG_INI_SETTINGS 0 // Save additional comments in .ini file (particularly helps for Docking, but makes saving slower) + +// When using CTRL+TAB (or Gamepad Square+L/R) we delay the visual a little in order to reduce visual noise doing a fast switch. +static const float NAV_WINDOWING_HIGHLIGHT_DELAY = 0.20f; // Time before the highlight and screen dimming starts fading in +static const float NAV_WINDOWING_LIST_APPEAR_DELAY = 0.15f; // Time before the window list starts to appear + +// Window resizing from edges (when io.ConfigWindowsResizeFromEdges = true and ImGuiBackendFlags_HasMouseCursors is set in io.BackendFlags by backend) +static const float WINDOWS_HOVER_PADDING = 4.0f; // Extend outside window for hovering/resizing (maxxed with TouchPadding) and inside windows for borders. Affect FindHoveredWindow(). +static const float WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER = 0.04f; // Reduce visual noise by only highlighting the border after a certain time. +static const float WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER = 2.00f; // Lock scrolled window (so it doesn't pick child windows that are scrolling through) for a certain time, unless mouse moved. + +//------------------------------------------------------------------------- +// [SECTION] FORWARD DECLARATIONS +//------------------------------------------------------------------------- + +static void SetCurrentWindow(ImGuiWindow* window); +static void FindHoveredWindow(); +static ImGuiWindow* CreateNewWindow(const char* name, ImGuiWindowFlags flags); +static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window); + +static void AddDrawListToDrawData(ImVector* out_list, ImDrawList* draw_list); +static void AddWindowToSortBuffer(ImVector* out_sorted_windows, ImGuiWindow* window); + +// Settings +static void WindowSettingsHandler_ClearAll(ImGuiContext*, ImGuiSettingsHandler*); +static void* WindowSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name); +static void WindowSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line); +static void WindowSettingsHandler_ApplyAll(ImGuiContext*, ImGuiSettingsHandler*); +static void WindowSettingsHandler_WriteAll(ImGuiContext*, ImGuiSettingsHandler*, ImGuiTextBuffer* buf); + +// Platform Dependents default implementation for IO functions +static const char* GetClipboardTextFn_DefaultImpl(void* user_data); +static void SetClipboardTextFn_DefaultImpl(void* user_data, const char* text); +static void SetPlatformImeDataFn_DefaultImpl(ImGuiViewport* viewport, ImGuiPlatformImeData* data); + +namespace ImGui +{ +// Navigation +static void NavUpdate(); +static void NavUpdateWindowing(); +static void NavUpdateWindowingOverlay(); +static void NavUpdateCancelRequest(); +static void NavUpdateCreateMoveRequest(); +static void NavUpdateCreateTabbingRequest(); +static float NavUpdatePageUpPageDown(); +static inline void NavUpdateAnyRequestFlag(); +static void NavUpdateCreateWrappingRequest(); +static void NavEndFrame(); +static bool NavScoreItem(ImGuiNavItemData* result); +static void NavApplyItemToResult(ImGuiNavItemData* result); +static void NavProcessItem(); +static void NavProcessItemForTabbingRequest(ImGuiID id); +static ImVec2 NavCalcPreferredRefPos(); +static void NavSaveLastChildNavWindowIntoParent(ImGuiWindow* nav_window); +static ImGuiWindow* NavRestoreLastChildNavWindow(ImGuiWindow* window); +static void NavRestoreLayer(ImGuiNavLayer layer); +static void NavRestoreHighlightAfterMove(); +static int FindWindowFocusIndex(ImGuiWindow* window); + +// Error Checking and Debug Tools +static void ErrorCheckNewFrameSanityChecks(); +static void ErrorCheckEndFrameSanityChecks(); +static void UpdateDebugToolItemPicker(); +static void UpdateDebugToolStackQueries(); + +// Misc +static void UpdateSettings(); +static void UpdateKeyboardInputs(); +static void UpdateMouseInputs(); +static void UpdateMouseWheel(); +static bool UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect); +static void RenderWindowOuterBorders(ImGuiWindow* window); +static void RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar_rect, bool title_bar_is_highlight, int resize_grip_count, const ImU32 resize_grip_col[4], float resize_grip_draw_size); +static void RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& title_bar_rect, const char* name, bool* p_open); +static void RenderDimmedBackgroundBehindWindow(ImGuiWindow* window, ImU32 col); +static void RenderDimmedBackgrounds(); +static ImGuiWindow* FindBlockingModal(ImGuiWindow* window); + +// Viewports +static void UpdateViewportsNewFrame(); + +} + +//----------------------------------------------------------------------------- +// [SECTION] CONTEXT AND MEMORY ALLOCATORS +//----------------------------------------------------------------------------- + +// DLL users: +// - Heaps and globals are not shared across DLL boundaries! +// - You will need to call SetCurrentContext() + SetAllocatorFunctions() for each static/DLL boundary you are calling from. +// - Same applies for hot-reloading mechanisms that are reliant on reloading DLL (note that many hot-reloading mechanisms work without DLL). +// - Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility. +// - Confused? In a debugger: add GImGui to your watch window and notice how its value changes depending on your current location (which DLL boundary you are in). + +// Current context pointer. Implicitly used by all Dear ImGui functions. Always assumed to be != NULL. +// - ImGui::CreateContext() will automatically set this pointer if it is NULL. +// Change to a different context by calling ImGui::SetCurrentContext(). +// - Important: Dear ImGui functions are not thread-safe because of this pointer. +// If you want thread-safety to allow N threads to access N different contexts: +// - Change this variable to use thread local storage so each thread can refer to a different context, in your imconfig.h: +// struct ImGuiContext; +// extern thread_local ImGuiContext* MyImGuiTLS; +// #define GImGui MyImGuiTLS +// And then define MyImGuiTLS in one of your cpp files. Note that thread_local is a C++11 keyword, earlier C++ uses compiler-specific keyword. +// - Future development aims to make this context pointer explicit to all calls. Also read https://github.com/ocornut/imgui/issues/586 +// - If you need a finite number of contexts, you may compile and use multiple instances of the ImGui code from a different namespace. +// - DLL users: read comments above. +#ifndef GImGui +ImGuiContext* GImGui = NULL; +#endif + +// Memory Allocator functions. Use SetAllocatorFunctions() to change them. +// - You probably don't want to modify that mid-program, and if you use global/static e.g. ImVector<> instances you may need to keep them accessible during program destruction. +// - DLL users: read comments above. +#ifndef IMGUI_DISABLE_DEFAULT_ALLOCATORS +static void* MallocWrapper(size_t size, void* user_data) { IM_UNUSED(user_data); return malloc(size); } +static void FreeWrapper(void* ptr, void* user_data) { IM_UNUSED(user_data); free(ptr); } +#else +static void* MallocWrapper(size_t size, void* user_data) { IM_UNUSED(user_data); IM_UNUSED(size); IM_ASSERT(0); return NULL; } +static void FreeWrapper(void* ptr, void* user_data) { IM_UNUSED(user_data); IM_UNUSED(ptr); IM_ASSERT(0); } +#endif +static ImGuiMemAllocFunc GImAllocatorAllocFunc = MallocWrapper; +static ImGuiMemFreeFunc GImAllocatorFreeFunc = FreeWrapper; +static void* GImAllocatorUserData = NULL; + +//----------------------------------------------------------------------------- +// [SECTION] USER FACING STRUCTURES (ImGuiStyle, ImGuiIO) +//----------------------------------------------------------------------------- + +ImGuiStyle::ImGuiStyle() +{ + Alpha = 1.0f; // Global alpha applies to everything in Dear ImGui. + DisabledAlpha = 0.60f; // Additional alpha multiplier applied by BeginDisabled(). Multiply over current value of Alpha. + WindowPadding = ImVec2(8,8); // Padding within a window + WindowRounding = 0.0f; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. Large values tend to lead to variety of artifacts and are not recommended. + WindowBorderSize = 1.0f; // Thickness of border around windows. Generally set to 0.0f or 1.0f. Other values not well tested. + WindowMinSize = ImVec2(32,32); // Minimum window size + WindowTitleAlign = ImVec2(0.0f,0.5f);// Alignment for title bar text + WindowMenuButtonPosition= ImGuiDir_Left; // Position of the collapsing/docking button in the title bar (left/right). Defaults to ImGuiDir_Left. + ChildRounding = 0.0f; // Radius of child window corners rounding. Set to 0.0f to have rectangular child windows + ChildBorderSize = 1.0f; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. Other values not well tested. + PopupRounding = 0.0f; // Radius of popup window corners rounding. Set to 0.0f to have rectangular child windows + PopupBorderSize = 1.0f; // Thickness of border around popup or tooltip windows. Generally set to 0.0f or 1.0f. Other values not well tested. + FramePadding = ImVec2(4,3); // Padding within a framed rectangle (used by most widgets) + FrameRounding = 0.0f; // Radius of frame corners rounding. Set to 0.0f to have rectangular frames (used by most widgets). + FrameBorderSize = 0.0f; // Thickness of border around frames. Generally set to 0.0f or 1.0f. Other values not well tested. + ItemSpacing = ImVec2(8,4); // Horizontal and vertical spacing between widgets/lines + ItemInnerSpacing = ImVec2(4,4); // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label) + CellPadding = ImVec2(4,2); // Padding within a table cell + TouchExtraPadding = ImVec2(0,0); // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! + IndentSpacing = 21.0f; // Horizontal spacing when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). + ColumnsMinSpacing = 6.0f; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). + ScrollbarSize = 14.0f; // Width of the vertical scrollbar, Height of the horizontal scrollbar + ScrollbarRounding = 9.0f; // Radius of grab corners rounding for scrollbar + GrabMinSize = 10.0f; // Minimum width/height of a grab box for slider/scrollbar + GrabRounding = 0.0f; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. + LogSliderDeadzone = 4.0f; // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero. + TabRounding = 4.0f; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. + TabBorderSize = 0.0f; // Thickness of border around tabs. + TabMinWidthForCloseButton = 0.0f; // Minimum width for close button to appears on an unselected tab when hovered. Set to 0.0f to always show when hovering, set to FLT_MAX to never show close button unless selected. + ColorButtonPosition = ImGuiDir_Right; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. + ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text. + SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. + DisplayWindowPadding = ImVec2(19,19); // Window position are clamped to be visible within the display area or monitors by at least this amount. Only applies to regular windows. + DisplaySafeAreaPadding = ImVec2(3,3); // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. + MouseCursorScale = 1.0f; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. + AntiAliasedLines = true; // Enable anti-aliased lines/borders. Disable if you are really tight on CPU/GPU. + AntiAliasedLinesUseTex = true; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering. + AntiAliasedFill = true; // Enable anti-aliased filled shapes (rounded rectangles, circles, etc.). + CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + CircleTessellationMaxError = 0.30f; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + + // Default theme + ImGui::StyleColorsDark(this); +} + +// To scale your entire UI (e.g. if you want your app to use High DPI or generally be DPI aware) you may use this helper function. Scaling the fonts is done separately and is up to you. +// Important: This operation is lossy because we round all sizes to integer. If you need to change your scale multiples, call this over a freshly initialized ImGuiStyle structure rather than scaling multiple times. +void ImGuiStyle::ScaleAllSizes(float scale_factor) +{ + WindowPadding = ImFloor(WindowPadding * scale_factor); + WindowRounding = ImFloor(WindowRounding * scale_factor); + WindowMinSize = ImFloor(WindowMinSize * scale_factor); + ChildRounding = ImFloor(ChildRounding * scale_factor); + PopupRounding = ImFloor(PopupRounding * scale_factor); + FramePadding = ImFloor(FramePadding * scale_factor); + FrameRounding = ImFloor(FrameRounding * scale_factor); + ItemSpacing = ImFloor(ItemSpacing * scale_factor); + ItemInnerSpacing = ImFloor(ItemInnerSpacing * scale_factor); + CellPadding = ImFloor(CellPadding * scale_factor); + TouchExtraPadding = ImFloor(TouchExtraPadding * scale_factor); + IndentSpacing = ImFloor(IndentSpacing * scale_factor); + ColumnsMinSpacing = ImFloor(ColumnsMinSpacing * scale_factor); + ScrollbarSize = ImFloor(ScrollbarSize * scale_factor); + ScrollbarRounding = ImFloor(ScrollbarRounding * scale_factor); + GrabMinSize = ImFloor(GrabMinSize * scale_factor); + GrabRounding = ImFloor(GrabRounding * scale_factor); + LogSliderDeadzone = ImFloor(LogSliderDeadzone * scale_factor); + TabRounding = ImFloor(TabRounding * scale_factor); + TabMinWidthForCloseButton = (TabMinWidthForCloseButton != FLT_MAX) ? ImFloor(TabMinWidthForCloseButton * scale_factor) : FLT_MAX; + DisplayWindowPadding = ImFloor(DisplayWindowPadding * scale_factor); + DisplaySafeAreaPadding = ImFloor(DisplaySafeAreaPadding * scale_factor); + MouseCursorScale = ImFloor(MouseCursorScale * scale_factor); +} + +ImGuiIO::ImGuiIO() +{ + // Most fields are initialized with zero + memset(this, 0, sizeof(*this)); + IM_STATIC_ASSERT(IM_ARRAYSIZE(ImGuiIO::MouseDown) == ImGuiMouseButton_COUNT && IM_ARRAYSIZE(ImGuiIO::MouseClicked) == ImGuiMouseButton_COUNT); + + // Settings + ConfigFlags = ImGuiConfigFlags_None; + BackendFlags = ImGuiBackendFlags_None; + DisplaySize = ImVec2(-1.0f, -1.0f); + DeltaTime = 1.0f / 60.0f; + IniSavingRate = 5.0f; + IniFilename = "imgui.ini"; // Important: "imgui.ini" is relative to current working dir, most apps will want to lock this to an absolute path (e.g. same path as executables). + LogFilename = "imgui_log.txt"; + MouseDoubleClickTime = 0.30f; + MouseDoubleClickMaxDist = 6.0f; +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + for (int i = 0; i < ImGuiKey_COUNT; i++) + KeyMap[i] = -1; +#endif + KeyRepeatDelay = 0.275f; + KeyRepeatRate = 0.050f; + UserData = NULL; + + Fonts = NULL; + FontGlobalScale = 1.0f; + FontDefault = NULL; + FontAllowUserScaling = false; + DisplayFramebufferScale = ImVec2(1.0f, 1.0f); + + // Miscellaneous options + MouseDrawCursor = false; +#ifdef __APPLE__ + ConfigMacOSXBehaviors = true; // Set Mac OS X style defaults based on __APPLE__ compile time flag +#else + ConfigMacOSXBehaviors = false; +#endif + ConfigInputTrickleEventQueue = true; + ConfigInputTextCursorBlink = true; + ConfigWindowsResizeFromEdges = true; + ConfigWindowsMoveFromTitleBarOnly = false; + ConfigMemoryCompactTimer = 60.0f; + + // Platform Functions + BackendPlatformName = BackendRendererName = NULL; + BackendPlatformUserData = BackendRendererUserData = BackendLanguageUserData = NULL; + GetClipboardTextFn = GetClipboardTextFn_DefaultImpl; // Platform dependent default implementations + SetClipboardTextFn = SetClipboardTextFn_DefaultImpl; + ClipboardUserData = NULL; + SetPlatformImeDataFn = SetPlatformImeDataFn_DefaultImpl; + + // Input (NB: we already have memset zero the entire structure!) + MousePos = ImVec2(-FLT_MAX, -FLT_MAX); + MousePosPrev = ImVec2(-FLT_MAX, -FLT_MAX); + MouseDragThreshold = 6.0f; + for (int i = 0; i < IM_ARRAYSIZE(MouseDownDuration); i++) MouseDownDuration[i] = MouseDownDurationPrev[i] = -1.0f; + for (int i = 0; i < IM_ARRAYSIZE(KeysData); i++) { KeysData[i].DownDuration = KeysData[i].DownDurationPrev = -1.0f; } + for (int i = 0; i < IM_ARRAYSIZE(NavInputsDownDuration); i++) NavInputsDownDuration[i] = -1.0f; + BackendUsingLegacyKeyArrays = (ImS8)-1; + BackendUsingLegacyNavInputArray = true; // assume using legacy array until proven wrong +} + +// Pass in translated ASCII characters for text input. +// - with glfw you can get those from the callback set in glfwSetCharCallback() +// - on Windows you can get those using ToAscii+keyboard state, or via the WM_CHAR message +// FIXME: Should in theory be called "AddCharacterEvent()" to be consistent with new API +void ImGuiIO::AddInputCharacter(unsigned int c) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(&g.IO == this && "Can only add events to current context."); + if (c == 0) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_Char; + e.Source = ImGuiInputSource_Keyboard; + e.Text.Char = c; + g.InputEventsQueue.push_back(e); +} + +// UTF16 strings use surrogate pairs to encode codepoints >= 0x10000, so +// we should save the high surrogate. +void ImGuiIO::AddInputCharacterUTF16(ImWchar16 c) +{ + if (c == 0 && InputQueueSurrogate == 0) + return; + + if ((c & 0xFC00) == 0xD800) // High surrogate, must save + { + if (InputQueueSurrogate != 0) + AddInputCharacter(IM_UNICODE_CODEPOINT_INVALID); + InputQueueSurrogate = c; + return; + } + + ImWchar cp = c; + if (InputQueueSurrogate != 0) + { + if ((c & 0xFC00) != 0xDC00) // Invalid low surrogate + { + AddInputCharacter(IM_UNICODE_CODEPOINT_INVALID); + } + else + { +#if IM_UNICODE_CODEPOINT_MAX == 0xFFFF + cp = IM_UNICODE_CODEPOINT_INVALID; // Codepoint will not fit in ImWchar +#else + cp = (ImWchar)(((InputQueueSurrogate - 0xD800) << 10) + (c - 0xDC00) + 0x10000); +#endif + } + + InputQueueSurrogate = 0; + } + AddInputCharacter((unsigned)cp); +} + +void ImGuiIO::AddInputCharactersUTF8(const char* utf8_chars) +{ + while (*utf8_chars != 0) + { + unsigned int c = 0; + utf8_chars += ImTextCharFromUtf8(&c, utf8_chars, NULL); + if (c != 0) + AddInputCharacter(c); + } +} + +void ImGuiIO::ClearInputCharacters() +{ + InputQueueCharacters.resize(0); +} + +void ImGuiIO::ClearInputKeys() +{ +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + memset(KeysDown, 0, sizeof(KeysDown)); +#endif + for (int n = 0; n < IM_ARRAYSIZE(KeysData); n++) + { + KeysData[n].Down = false; + KeysData[n].DownDuration = -1.0f; + KeysData[n].DownDurationPrev = -1.0f; + } + KeyCtrl = KeyShift = KeyAlt = KeySuper = false; + KeyMods = KeyModsPrev = ImGuiKeyModFlags_None; + for (int n = 0; n < IM_ARRAYSIZE(NavInputsDownDuration); n++) + NavInputsDownDuration[n] = NavInputsDownDurationPrev[n] = -1.0f; +} + +// Queue a new key down/up event. +// - ImGuiKey key: Translated key (as in, generally ImGuiKey_A matches the key end-user would use to emit an 'A' character) +// - bool down: Is the key down? use false to signify a key release. +// - float analog_value: 0.0f..1.0f +void ImGuiIO::AddKeyAnalogEvent(ImGuiKey key, bool down, float analog_value) +{ + //if (e->Down) { IMGUI_DEBUG_LOG("AddKeyEvent() Key='%s' %d, NativeKeycode = %d, NativeScancode = %d\n", ImGui::GetKeyName(e->Key), e->Down, e->NativeKeycode, e->NativeScancode); } + if (key == ImGuiKey_None) + return; + ImGuiContext& g = *GImGui; + IM_ASSERT(&g.IO == this && "Can only add events to current context."); + IM_ASSERT(ImGui::IsNamedKey(key)); // Backend needs to pass a valid ImGuiKey_ constant. 0..511 values are legacy native key codes which are not accepted by this API. + + // Verify that backend isn't mixing up using new io.AddKeyEvent() api and old io.KeysDown[] + io.KeyMap[] data. +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + IM_ASSERT((BackendUsingLegacyKeyArrays == -1 || BackendUsingLegacyKeyArrays == 0) && "Backend needs to either only use io.AddKeyEvent(), either only fill legacy io.KeysDown[] + io.KeyMap[]. Not both!"); + if (BackendUsingLegacyKeyArrays == -1) + for (int n = ImGuiKey_NamedKey_BEGIN; n < ImGuiKey_NamedKey_END; n++) + IM_ASSERT(KeyMap[n] == -1 && "Backend needs to either only use io.AddKeyEvent(), either only fill legacy io.KeysDown[] + io.KeyMap[]. Not both!"); + BackendUsingLegacyKeyArrays = 0; +#endif + if (ImGui::IsGamepadKey(key)) + BackendUsingLegacyNavInputArray = false; + + // Partial filter of duplicates (not strictly needed, but makes data neater in particular for key mods and gamepad values which are most commonly spmamed) + ImGuiKeyData* key_data = ImGui::GetKeyData(key); + if (key_data->Down == down && key_data->AnalogValue == analog_value) + { + bool found = false; + for (int n = g.InputEventsQueue.Size - 1; n >= 0 && !found; n--) + if (g.InputEventsQueue[n].Type == ImGuiInputEventType_Key && g.InputEventsQueue[n].Key.Key == key) + found = true; + if (!found) + return; + } + + // Add event + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_Key; + e.Source = ImGui::IsGamepadKey(key) ? ImGuiInputSource_Gamepad : ImGuiInputSource_Keyboard; + e.Key.Key = key; + e.Key.Down = down; + e.Key.AnalogValue = analog_value; + g.InputEventsQueue.push_back(e); +} + +void ImGuiIO::AddKeyEvent(ImGuiKey key, bool down) +{ + AddKeyAnalogEvent(key, down, down ? 1.0f : 0.0f); +} + +// [Optional] Call after AddKeyEvent(). +// Specify native keycode, scancode + Specify index for legacy <1.87 IsKeyXXX() functions with native indices. +// If you are writing a backend in 2022 or don't use IsKeyXXX() with native values that are not ImGuiKey values, you can avoid calling this. +void ImGuiIO::SetKeyEventNativeData(ImGuiKey key, int native_keycode, int native_scancode, int native_legacy_index) +{ + if (key == ImGuiKey_None) + return; + IM_ASSERT(ImGui::IsNamedKey(key)); // >= 512 + IM_ASSERT(native_legacy_index == -1 || ImGui::IsLegacyKey(native_legacy_index)); // >= 0 && <= 511 + IM_UNUSED(native_keycode); // Yet unused + IM_UNUSED(native_scancode); // Yet unused + + // Build native->imgui map so old user code can still call key functions with native 0..511 values. +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + const int legacy_key = (native_legacy_index != -1) ? native_legacy_index : native_keycode; + if (ImGui::IsLegacyKey(legacy_key)) + KeyMap[legacy_key] = key; +#else + IM_UNUSED(key); + IM_UNUSED(native_legacy_index); +#endif +} + +// Queue a mouse move event +void ImGuiIO::AddMousePosEvent(float x, float y) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(&g.IO == this && "Can only add events to current context."); + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MousePos; + e.Source = ImGuiInputSource_Mouse; + e.MousePos.PosX = x; + e.MousePos.PosY = y; + g.InputEventsQueue.push_back(e); +} + +void ImGuiIO::AddMouseButtonEvent(int mouse_button, bool down) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(&g.IO == this && "Can only add events to current context."); + IM_ASSERT(mouse_button >= 0 && mouse_button < ImGuiMouseButton_COUNT); + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MouseButton; + e.Source = ImGuiInputSource_Mouse; + e.MouseButton.Button = mouse_button; + e.MouseButton.Down = down; + g.InputEventsQueue.push_back(e); +} + +// Queue a mouse wheel event (most mouse/API will only have a Y component) +void ImGuiIO::AddMouseWheelEvent(float wheel_x, float wheel_y) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(&g.IO == this && "Can only add events to current context."); + if (wheel_x == 0.0f && wheel_y == 0.0f) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MouseWheel; + e.Source = ImGuiInputSource_Mouse; + e.MouseWheel.WheelX = wheel_x; + e.MouseWheel.WheelY = wheel_y; + g.InputEventsQueue.push_back(e); +} + +void ImGuiIO::AddFocusEvent(bool focused) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(&g.IO == this && "Can only add events to current context."); + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_Focus; + e.AppFocused.Focused = focused; + g.InputEventsQueue.push_back(e); +} + +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (Geometry functions) +//----------------------------------------------------------------------------- + +ImVec2 ImBezierCubicClosestPoint(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, int num_segments) +{ + IM_ASSERT(num_segments > 0); // Use ImBezierCubicClosestPointCasteljau() + ImVec2 p_last = p1; + ImVec2 p_closest; + float p_closest_dist2 = FLT_MAX; + float t_step = 1.0f / (float)num_segments; + for (int i_step = 1; i_step <= num_segments; i_step++) + { + ImVec2 p_current = ImBezierCubicCalc(p1, p2, p3, p4, t_step * i_step); + ImVec2 p_line = ImLineClosestPoint(p_last, p_current, p); + float dist2 = ImLengthSqr(p - p_line); + if (dist2 < p_closest_dist2) + { + p_closest = p_line; + p_closest_dist2 = dist2; + } + p_last = p_current; + } + return p_closest; +} + +// Closely mimics PathBezierToCasteljau() in imgui_draw.cpp +static void ImBezierCubicClosestPointCasteljauStep(const ImVec2& p, ImVec2& p_closest, ImVec2& p_last, float& p_closest_dist2, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tess_tol, int level) +{ + float dx = x4 - x1; + float dy = y4 - y1; + float d2 = ((x2 - x4) * dy - (y2 - y4) * dx); + float d3 = ((x3 - x4) * dy - (y3 - y4) * dx); + d2 = (d2 >= 0) ? d2 : -d2; + d3 = (d3 >= 0) ? d3 : -d3; + if ((d2 + d3) * (d2 + d3) < tess_tol * (dx * dx + dy * dy)) + { + ImVec2 p_current(x4, y4); + ImVec2 p_line = ImLineClosestPoint(p_last, p_current, p); + float dist2 = ImLengthSqr(p - p_line); + if (dist2 < p_closest_dist2) + { + p_closest = p_line; + p_closest_dist2 = dist2; + } + p_last = p_current; + } + else if (level < 10) + { + float x12 = (x1 + x2)*0.5f, y12 = (y1 + y2)*0.5f; + float x23 = (x2 + x3)*0.5f, y23 = (y2 + y3)*0.5f; + float x34 = (x3 + x4)*0.5f, y34 = (y3 + y4)*0.5f; + float x123 = (x12 + x23)*0.5f, y123 = (y12 + y23)*0.5f; + float x234 = (x23 + x34)*0.5f, y234 = (y23 + y34)*0.5f; + float x1234 = (x123 + x234)*0.5f, y1234 = (y123 + y234)*0.5f; + ImBezierCubicClosestPointCasteljauStep(p, p_closest, p_last, p_closest_dist2, x1, y1, x12, y12, x123, y123, x1234, y1234, tess_tol, level + 1); + ImBezierCubicClosestPointCasteljauStep(p, p_closest, p_last, p_closest_dist2, x1234, y1234, x234, y234, x34, y34, x4, y4, tess_tol, level + 1); + } +} + +// tess_tol is generally the same value you would find in ImGui::GetStyle().CurveTessellationTol +// Because those ImXXX functions are lower-level than ImGui:: we cannot access this value automatically. +ImVec2 ImBezierCubicClosestPointCasteljau(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, float tess_tol) +{ + IM_ASSERT(tess_tol > 0.0f); + ImVec2 p_last = p1; + ImVec2 p_closest; + float p_closest_dist2 = FLT_MAX; + ImBezierCubicClosestPointCasteljauStep(p, p_closest, p_last, p_closest_dist2, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, tess_tol, 0); + return p_closest; +} + +ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p) +{ + ImVec2 ap = p - a; + ImVec2 ab_dir = b - a; + float dot = ap.x * ab_dir.x + ap.y * ab_dir.y; + if (dot < 0.0f) + return a; + float ab_len_sqr = ab_dir.x * ab_dir.x + ab_dir.y * ab_dir.y; + if (dot > ab_len_sqr) + return b; + return a + ab_dir * dot / ab_len_sqr; +} + +bool ImTriangleContainsPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p) +{ + bool b1 = ((p.x - b.x) * (a.y - b.y) - (p.y - b.y) * (a.x - b.x)) < 0.0f; + bool b2 = ((p.x - c.x) * (b.y - c.y) - (p.y - c.y) * (b.x - c.x)) < 0.0f; + bool b3 = ((p.x - a.x) * (c.y - a.y) - (p.y - a.y) * (c.x - a.x)) < 0.0f; + return ((b1 == b2) && (b2 == b3)); +} + +void ImTriangleBarycentricCoords(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p, float& out_u, float& out_v, float& out_w) +{ + ImVec2 v0 = b - a; + ImVec2 v1 = c - a; + ImVec2 v2 = p - a; + const float denom = v0.x * v1.y - v1.x * v0.y; + out_v = (v2.x * v1.y - v1.x * v2.y) / denom; + out_w = (v0.x * v2.y - v2.x * v0.y) / denom; + out_u = 1.0f - out_v - out_w; +} + +ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p) +{ + ImVec2 proj_ab = ImLineClosestPoint(a, b, p); + ImVec2 proj_bc = ImLineClosestPoint(b, c, p); + ImVec2 proj_ca = ImLineClosestPoint(c, a, p); + float dist2_ab = ImLengthSqr(p - proj_ab); + float dist2_bc = ImLengthSqr(p - proj_bc); + float dist2_ca = ImLengthSqr(p - proj_ca); + float m = ImMin(dist2_ab, ImMin(dist2_bc, dist2_ca)); + if (m == dist2_ab) + return proj_ab; + if (m == dist2_bc) + return proj_bc; + return proj_ca; +} + +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (String, Format, Hash functions) +//----------------------------------------------------------------------------- + +// Consider using _stricmp/_strnicmp under Windows or strcasecmp/strncasecmp. We don't actually use either ImStricmp/ImStrnicmp in the codebase any more. +int ImStricmp(const char* str1, const char* str2) +{ + int d; + while ((d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; } + return d; +} + +int ImStrnicmp(const char* str1, const char* str2, size_t count) +{ + int d = 0; + while (count > 0 && (d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; count--; } + return d; +} + +void ImStrncpy(char* dst, const char* src, size_t count) +{ + if (count < 1) + return; + if (count > 1) + strncpy(dst, src, count - 1); + dst[count - 1] = 0; +} + +char* ImStrdup(const char* str) +{ + size_t len = strlen(str); + void* buf = IM_ALLOC(len + 1); + return (char*)memcpy(buf, (const void*)str, len + 1); +} + +char* ImStrdupcpy(char* dst, size_t* p_dst_size, const char* src) +{ + size_t dst_buf_size = p_dst_size ? *p_dst_size : strlen(dst) + 1; + size_t src_size = strlen(src) + 1; + if (dst_buf_size < src_size) + { + IM_FREE(dst); + dst = (char*)IM_ALLOC(src_size); + if (p_dst_size) + *p_dst_size = src_size; + } + return (char*)memcpy(dst, (const void*)src, src_size); +} + +const char* ImStrchrRange(const char* str, const char* str_end, char c) +{ + const char* p = (const char*)memchr(str, (int)c, str_end - str); + return p; +} + +int ImStrlenW(const ImWchar* str) +{ + //return (int)wcslen((const wchar_t*)str); // FIXME-OPT: Could use this when wchar_t are 16-bit + int n = 0; + while (*str++) n++; + return n; +} + +// Find end-of-line. Return pointer will point to either first \n, either str_end. +const char* ImStreolRange(const char* str, const char* str_end) +{ + const char* p = (const char*)memchr(str, '\n', str_end - str); + return p ? p : str_end; +} + +const ImWchar* ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin) // find beginning-of-line +{ + while (buf_mid_line > buf_begin && buf_mid_line[-1] != '\n') + buf_mid_line--; + return buf_mid_line; +} + +const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end) +{ + if (!needle_end) + needle_end = needle + strlen(needle); + + const char un0 = (char)toupper(*needle); + while ((!haystack_end && *haystack) || (haystack_end && haystack < haystack_end)) + { + if (toupper(*haystack) == un0) + { + const char* b = needle + 1; + for (const char* a = haystack + 1; b < needle_end; a++, b++) + if (toupper(*a) != toupper(*b)) + break; + if (b == needle_end) + return haystack; + } + haystack++; + } + return NULL; +} + +// Trim str by offsetting contents when there's leading data + writing a \0 at the trailing position. We use this in situation where the cost is negligible. +void ImStrTrimBlanks(char* buf) +{ + char* p = buf; + while (p[0] == ' ' || p[0] == '\t') // Leading blanks + p++; + char* p_start = p; + while (*p != 0) // Find end of string + p++; + while (p > p_start && (p[-1] == ' ' || p[-1] == '\t')) // Trailing blanks + p--; + if (p_start != buf) // Copy memory if we had leading blanks + memmove(buf, p_start, p - p_start); + buf[p - p_start] = 0; // Zero terminate +} + +const char* ImStrSkipBlank(const char* str) +{ + while (str[0] == ' ' || str[0] == '\t') + str++; + return str; +} + +// A) MSVC version appears to return -1 on overflow, whereas glibc appears to return total count (which may be >= buf_size). +// Ideally we would test for only one of those limits at runtime depending on the behavior the vsnprintf(), but trying to deduct it at compile time sounds like a pandora can of worm. +// B) When buf==NULL vsnprintf() will return the output size. +#ifndef IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS + +// We support stb_sprintf which is much faster (see: https://github.com/nothings/stb/blob/master/stb_sprintf.h) +// You may set IMGUI_USE_STB_SPRINTF to use our default wrapper, or set IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS +// and setup the wrapper yourself. (FIXME-OPT: Some of our high-level operations such as ImGuiTextBuffer::appendfv() are +// designed using two-passes worst case, which probably could be improved using the stbsp_vsprintfcb() function.) +#ifdef IMGUI_USE_STB_SPRINTF +#define STB_SPRINTF_IMPLEMENTATION +#include "stb_sprintf.h" +#endif + +#if defined(_MSC_VER) && !defined(vsnprintf) +#define vsnprintf _vsnprintf +#endif + +int ImFormatString(char* buf, size_t buf_size, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); +#ifdef IMGUI_USE_STB_SPRINTF + int w = stbsp_vsnprintf(buf, (int)buf_size, fmt, args); +#else + int w = vsnprintf(buf, buf_size, fmt, args); +#endif + va_end(args); + if (buf == NULL) + return w; + if (w == -1 || w >= (int)buf_size) + w = (int)buf_size - 1; + buf[w] = 0; + return w; +} + +int ImFormatStringV(char* buf, size_t buf_size, const char* fmt, va_list args) +{ +#ifdef IMGUI_USE_STB_SPRINTF + int w = stbsp_vsnprintf(buf, (int)buf_size, fmt, args); +#else + int w = vsnprintf(buf, buf_size, fmt, args); +#endif + if (buf == NULL) + return w; + if (w == -1 || w >= (int)buf_size) + w = (int)buf_size - 1; + buf[w] = 0; + return w; +} +#endif // #ifdef IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS + +// CRC32 needs a 1KB lookup table (not cache friendly) +// Although the code to generate the table is simple and shorter than the table itself, using a const table allows us to easily: +// - avoid an unnecessary branch/memory tap, - keep the ImHashXXX functions usable by static constructors, - make it thread-safe. +static const ImU32 GCrc32LookupTable[256] = +{ + 0x00000000,0x77073096,0xEE0E612C,0x990951BA,0x076DC419,0x706AF48F,0xE963A535,0x9E6495A3,0x0EDB8832,0x79DCB8A4,0xE0D5E91E,0x97D2D988,0x09B64C2B,0x7EB17CBD,0xE7B82D07,0x90BF1D91, + 0x1DB71064,0x6AB020F2,0xF3B97148,0x84BE41DE,0x1ADAD47D,0x6DDDE4EB,0xF4D4B551,0x83D385C7,0x136C9856,0x646BA8C0,0xFD62F97A,0x8A65C9EC,0x14015C4F,0x63066CD9,0xFA0F3D63,0x8D080DF5, + 0x3B6E20C8,0x4C69105E,0xD56041E4,0xA2677172,0x3C03E4D1,0x4B04D447,0xD20D85FD,0xA50AB56B,0x35B5A8FA,0x42B2986C,0xDBBBC9D6,0xACBCF940,0x32D86CE3,0x45DF5C75,0xDCD60DCF,0xABD13D59, + 0x26D930AC,0x51DE003A,0xC8D75180,0xBFD06116,0x21B4F4B5,0x56B3C423,0xCFBA9599,0xB8BDA50F,0x2802B89E,0x5F058808,0xC60CD9B2,0xB10BE924,0x2F6F7C87,0x58684C11,0xC1611DAB,0xB6662D3D, + 0x76DC4190,0x01DB7106,0x98D220BC,0xEFD5102A,0x71B18589,0x06B6B51F,0x9FBFE4A5,0xE8B8D433,0x7807C9A2,0x0F00F934,0x9609A88E,0xE10E9818,0x7F6A0DBB,0x086D3D2D,0x91646C97,0xE6635C01, + 0x6B6B51F4,0x1C6C6162,0x856530D8,0xF262004E,0x6C0695ED,0x1B01A57B,0x8208F4C1,0xF50FC457,0x65B0D9C6,0x12B7E950,0x8BBEB8EA,0xFCB9887C,0x62DD1DDF,0x15DA2D49,0x8CD37CF3,0xFBD44C65, + 0x4DB26158,0x3AB551CE,0xA3BC0074,0xD4BB30E2,0x4ADFA541,0x3DD895D7,0xA4D1C46D,0xD3D6F4FB,0x4369E96A,0x346ED9FC,0xAD678846,0xDA60B8D0,0x44042D73,0x33031DE5,0xAA0A4C5F,0xDD0D7CC9, + 0x5005713C,0x270241AA,0xBE0B1010,0xC90C2086,0x5768B525,0x206F85B3,0xB966D409,0xCE61E49F,0x5EDEF90E,0x29D9C998,0xB0D09822,0xC7D7A8B4,0x59B33D17,0x2EB40D81,0xB7BD5C3B,0xC0BA6CAD, + 0xEDB88320,0x9ABFB3B6,0x03B6E20C,0x74B1D29A,0xEAD54739,0x9DD277AF,0x04DB2615,0x73DC1683,0xE3630B12,0x94643B84,0x0D6D6A3E,0x7A6A5AA8,0xE40ECF0B,0x9309FF9D,0x0A00AE27,0x7D079EB1, + 0xF00F9344,0x8708A3D2,0x1E01F268,0x6906C2FE,0xF762575D,0x806567CB,0x196C3671,0x6E6B06E7,0xFED41B76,0x89D32BE0,0x10DA7A5A,0x67DD4ACC,0xF9B9DF6F,0x8EBEEFF9,0x17B7BE43,0x60B08ED5, + 0xD6D6A3E8,0xA1D1937E,0x38D8C2C4,0x4FDFF252,0xD1BB67F1,0xA6BC5767,0x3FB506DD,0x48B2364B,0xD80D2BDA,0xAF0A1B4C,0x36034AF6,0x41047A60,0xDF60EFC3,0xA867DF55,0x316E8EEF,0x4669BE79, + 0xCB61B38C,0xBC66831A,0x256FD2A0,0x5268E236,0xCC0C7795,0xBB0B4703,0x220216B9,0x5505262F,0xC5BA3BBE,0xB2BD0B28,0x2BB45A92,0x5CB36A04,0xC2D7FFA7,0xB5D0CF31,0x2CD99E8B,0x5BDEAE1D, + 0x9B64C2B0,0xEC63F226,0x756AA39C,0x026D930A,0x9C0906A9,0xEB0E363F,0x72076785,0x05005713,0x95BF4A82,0xE2B87A14,0x7BB12BAE,0x0CB61B38,0x92D28E9B,0xE5D5BE0D,0x7CDCEFB7,0x0BDBDF21, + 0x86D3D2D4,0xF1D4E242,0x68DDB3F8,0x1FDA836E,0x81BE16CD,0xF6B9265B,0x6FB077E1,0x18B74777,0x88085AE6,0xFF0F6A70,0x66063BCA,0x11010B5C,0x8F659EFF,0xF862AE69,0x616BFFD3,0x166CCF45, + 0xA00AE278,0xD70DD2EE,0x4E048354,0x3903B3C2,0xA7672661,0xD06016F7,0x4969474D,0x3E6E77DB,0xAED16A4A,0xD9D65ADC,0x40DF0B66,0x37D83BF0,0xA9BCAE53,0xDEBB9EC5,0x47B2CF7F,0x30B5FFE9, + 0xBDBDF21C,0xCABAC28A,0x53B39330,0x24B4A3A6,0xBAD03605,0xCDD70693,0x54DE5729,0x23D967BF,0xB3667A2E,0xC4614AB8,0x5D681B02,0x2A6F2B94,0xB40BBE37,0xC30C8EA1,0x5A05DF1B,0x2D02EF8D, +}; + +// Known size hash +// It is ok to call ImHashData on a string with known length but the ### operator won't be supported. +// FIXME-OPT: Replace with e.g. FNV1a hash? CRC32 pretty much randomly access 1KB. Need to do proper measurements. +ImGuiID ImHashData(const void* data_p, size_t data_size, ImU32 seed) +{ + ImU32 crc = ~seed; + const unsigned char* data = (const unsigned char*)data_p; + const ImU32* crc32_lut = GCrc32LookupTable; + while (data_size-- != 0) + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ *data++]; + return ~crc; +} + +// Zero-terminated string hash, with support for ### to reset back to seed value +// We support a syntax of "label###id" where only "###id" is included in the hash, and only "label" gets displayed. +// Because this syntax is rarely used we are optimizing for the common case. +// - If we reach ### in the string we discard the hash so far and reset to the seed. +// - We don't do 'current += 2; continue;' after handling ### to keep the code smaller/faster (measured ~10% diff in Debug build) +// FIXME-OPT: Replace with e.g. FNV1a hash? CRC32 pretty much randomly access 1KB. Need to do proper measurements. +ImGuiID ImHashStr(const char* data_p, size_t data_size, ImU32 seed) +{ + seed = ~seed; + ImU32 crc = seed; + const unsigned char* data = (const unsigned char*)data_p; + const ImU32* crc32_lut = GCrc32LookupTable; + if (data_size != 0) + { + while (data_size-- != 0) + { + unsigned char c = *data++; + if (c == '#' && data_size >= 2 && data[0] == '#' && data[1] == '#') + crc = seed; + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ c]; + } + } + else + { + while (unsigned char c = *data++) + { + if (c == '#' && data[0] == '#' && data[1] == '#') + crc = seed; + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ c]; + } + } + return ~crc; +} + +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (File functions) +//----------------------------------------------------------------------------- + +// Default file functions +#ifndef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS + +ImFileHandle ImFileOpen(const char* filename, const char* mode) +{ +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && !defined(__CYGWIN__) && !defined(__GNUC__) + // We need a fopen() wrapper because MSVC/Windows fopen doesn't handle UTF-8 filenames. + // Previously we used ImTextCountCharsFromUtf8/ImTextStrFromUtf8 here but we now need to support ImWchar16 and ImWchar32! + const int filename_wsize = ::MultiByteToWideChar(CP_UTF8, 0, filename, -1, NULL, 0); + const int mode_wsize = ::MultiByteToWideChar(CP_UTF8, 0, mode, -1, NULL, 0); + ImVector buf; + buf.resize(filename_wsize + mode_wsize); + ::MultiByteToWideChar(CP_UTF8, 0, filename, -1, (wchar_t*)&buf[0], filename_wsize); + ::MultiByteToWideChar(CP_UTF8, 0, mode, -1, (wchar_t*)&buf[filename_wsize], mode_wsize); + return ::_wfopen((const wchar_t*)&buf[0], (const wchar_t*)&buf[filename_wsize]); +#else + return fopen(filename, mode); +#endif +} + +// We should in theory be using fseeko()/ftello() with off_t and _fseeki64()/_ftelli64() with __int64, waiting for the PR that does that in a very portable pre-C++11 zero-warnings way. +bool ImFileClose(ImFileHandle f) { return fclose(f) == 0; } +ImU64 ImFileGetSize(ImFileHandle f) { long off = 0, sz = 0; return ((off = ftell(f)) != -1 && !fseek(f, 0, SEEK_END) && (sz = ftell(f)) != -1 && !fseek(f, off, SEEK_SET)) ? (ImU64)sz : (ImU64)-1; } +ImU64 ImFileRead(void* data, ImU64 sz, ImU64 count, ImFileHandle f) { return fread(data, (size_t)sz, (size_t)count, f); } +ImU64 ImFileWrite(const void* data, ImU64 sz, ImU64 count, ImFileHandle f) { return fwrite(data, (size_t)sz, (size_t)count, f); } +#endif // #ifndef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS + +// Helper: Load file content into memory +// Memory allocated with IM_ALLOC(), must be freed by user using IM_FREE() == ImGui::MemFree() +// This can't really be used with "rt" because fseek size won't match read size. +void* ImFileLoadToMemory(const char* filename, const char* mode, size_t* out_file_size, int padding_bytes) +{ + IM_ASSERT(filename && mode); + if (out_file_size) + *out_file_size = 0; + + ImFileHandle f; + if ((f = ImFileOpen(filename, mode)) == NULL) + return NULL; + + size_t file_size = (size_t)ImFileGetSize(f); + if (file_size == (size_t)-1) + { + ImFileClose(f); + return NULL; + } + + void* file_data = IM_ALLOC(file_size + padding_bytes); + if (file_data == NULL) + { + ImFileClose(f); + return NULL; + } + if (ImFileRead(file_data, 1, file_size, f) != file_size) + { + ImFileClose(f); + IM_FREE(file_data); + return NULL; + } + if (padding_bytes > 0) + memset((void*)(((char*)file_data) + file_size), 0, (size_t)padding_bytes); + + ImFileClose(f); + if (out_file_size) + *out_file_size = file_size; + + return file_data; +} + +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (ImText* functions) +//----------------------------------------------------------------------------- + +// Convert UTF-8 to 32-bit character, process single character input. +// A nearly-branchless UTF-8 decoder, based on work of Christopher Wellons (https://github.com/skeeto/branchless-utf8). +// We handle UTF-8 decoding error by skipping forward. +int ImTextCharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end) +{ + static const char lengths[32] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 3, 3, 4, 0 }; + static const int masks[] = { 0x00, 0x7f, 0x1f, 0x0f, 0x07 }; + static const uint32_t mins[] = { 0x400000, 0, 0x80, 0x800, 0x10000 }; + static const int shiftc[] = { 0, 18, 12, 6, 0 }; + static const int shifte[] = { 0, 6, 4, 2, 0 }; + int len = lengths[*(const unsigned char*)in_text >> 3]; + int wanted = len + !len; + + if (in_text_end == NULL) + in_text_end = in_text + wanted; // Max length, nulls will be taken into account. + + // Copy at most 'len' bytes, stop copying at 0 or past in_text_end. Branch predictor does a good job here, + // so it is fast even with excessive branching. + unsigned char s[4]; + s[0] = in_text + 0 < in_text_end ? in_text[0] : 0; + s[1] = in_text + 1 < in_text_end ? in_text[1] : 0; + s[2] = in_text + 2 < in_text_end ? in_text[2] : 0; + s[3] = in_text + 3 < in_text_end ? in_text[3] : 0; + + // Assume a four-byte character and load four bytes. Unused bits are shifted out. + *out_char = (uint32_t)(s[0] & masks[len]) << 18; + *out_char |= (uint32_t)(s[1] & 0x3f) << 12; + *out_char |= (uint32_t)(s[2] & 0x3f) << 6; + *out_char |= (uint32_t)(s[3] & 0x3f) << 0; + *out_char >>= shiftc[len]; + + // Accumulate the various error conditions. + int e = 0; + e = (*out_char < mins[len]) << 6; // non-canonical encoding + e |= ((*out_char >> 11) == 0x1b) << 7; // surrogate half? + e |= (*out_char > IM_UNICODE_CODEPOINT_MAX) << 8; // out of range? + e |= (s[1] & 0xc0) >> 2; + e |= (s[2] & 0xc0) >> 4; + e |= (s[3] ) >> 6; + e ^= 0x2a; // top two bits of each tail byte correct? + e >>= shifte[len]; + + if (e) + { + // No bytes are consumed when *in_text == 0 || in_text == in_text_end. + // One byte is consumed in case of invalid first byte of in_text. + // All available bytes (at most `len` bytes) are consumed on incomplete/invalid second to last bytes. + // Invalid or incomplete input may consume less bytes than wanted, therefore every byte has to be inspected in s. + wanted = ImMin(wanted, !!s[0] + !!s[1] + !!s[2] + !!s[3]); + *out_char = IM_UNICODE_CODEPOINT_INVALID; + } + + return wanted; +} + +int ImTextStrFromUtf8(ImWchar* buf, int buf_size, const char* in_text, const char* in_text_end, const char** in_text_remaining) +{ + ImWchar* buf_out = buf; + ImWchar* buf_end = buf + buf_size; + while (buf_out < buf_end - 1 && (!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c; + in_text += ImTextCharFromUtf8(&c, in_text, in_text_end); + if (c == 0) + break; + *buf_out++ = (ImWchar)c; + } + *buf_out = 0; + if (in_text_remaining) + *in_text_remaining = in_text; + return (int)(buf_out - buf); +} + +int ImTextCountCharsFromUtf8(const char* in_text, const char* in_text_end) +{ + int char_count = 0; + while ((!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c; + in_text += ImTextCharFromUtf8(&c, in_text, in_text_end); + if (c == 0) + break; + char_count++; + } + return char_count; +} + +// Based on stb_to_utf8() from github.com/nothings/stb/ +static inline int ImTextCharToUtf8_inline(char* buf, int buf_size, unsigned int c) +{ + if (c < 0x80) + { + buf[0] = (char)c; + return 1; + } + if (c < 0x800) + { + if (buf_size < 2) return 0; + buf[0] = (char)(0xc0 + (c >> 6)); + buf[1] = (char)(0x80 + (c & 0x3f)); + return 2; + } + if (c < 0x10000) + { + if (buf_size < 3) return 0; + buf[0] = (char)(0xe0 + (c >> 12)); + buf[1] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[2] = (char)(0x80 + ((c ) & 0x3f)); + return 3; + } + if (c <= 0x10FFFF) + { + if (buf_size < 4) return 0; + buf[0] = (char)(0xf0 + (c >> 18)); + buf[1] = (char)(0x80 + ((c >> 12) & 0x3f)); + buf[2] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[3] = (char)(0x80 + ((c ) & 0x3f)); + return 4; + } + // Invalid code point, the max unicode is 0x10FFFF + return 0; +} + +const char* ImTextCharToUtf8(char out_buf[5], unsigned int c) +{ + int count = ImTextCharToUtf8_inline(out_buf, 5, c); + out_buf[count] = 0; + return out_buf; +} + +// Not optimal but we very rarely use this function. +int ImTextCountUtf8BytesFromChar(const char* in_text, const char* in_text_end) +{ + unsigned int unused = 0; + return ImTextCharFromUtf8(&unused, in_text, in_text_end); +} + +static inline int ImTextCountUtf8BytesFromChar(unsigned int c) +{ + if (c < 0x80) return 1; + if (c < 0x800) return 2; + if (c < 0x10000) return 3; + if (c <= 0x10FFFF) return 4; + return 3; +} + +int ImTextStrToUtf8(char* out_buf, int out_buf_size, const ImWchar* in_text, const ImWchar* in_text_end) +{ + char* buf_p = out_buf; + const char* buf_end = out_buf + out_buf_size; + while (buf_p < buf_end - 1 && (!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c = (unsigned int)(*in_text++); + if (c < 0x80) + *buf_p++ = (char)c; + else + buf_p += ImTextCharToUtf8_inline(buf_p, (int)(buf_end - buf_p - 1), c); + } + *buf_p = 0; + return (int)(buf_p - out_buf); +} + +int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end) +{ + int bytes_count = 0; + while ((!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c = (unsigned int)(*in_text++); + if (c < 0x80) + bytes_count++; + else + bytes_count += ImTextCountUtf8BytesFromChar(c); + } + return bytes_count; +} + +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (Color functions) +// Note: The Convert functions are early design which are not consistent with other API. +//----------------------------------------------------------------------------- + +IMGUI_API ImU32 ImAlphaBlendColors(ImU32 col_a, ImU32 col_b) +{ + float t = ((col_b >> IM_COL32_A_SHIFT) & 0xFF) / 255.f; + int r = ImLerp((int)(col_a >> IM_COL32_R_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_R_SHIFT) & 0xFF, t); + int g = ImLerp((int)(col_a >> IM_COL32_G_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_G_SHIFT) & 0xFF, t); + int b = ImLerp((int)(col_a >> IM_COL32_B_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_B_SHIFT) & 0xFF, t); + return IM_COL32(r, g, b, 0xFF); +} + +ImVec4 ImGui::ColorConvertU32ToFloat4(ImU32 in) +{ + float s = 1.0f / 255.0f; + return ImVec4( + ((in >> IM_COL32_R_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_G_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_B_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_A_SHIFT) & 0xFF) * s); +} + +ImU32 ImGui::ColorConvertFloat4ToU32(const ImVec4& in) +{ + ImU32 out; + out = ((ImU32)IM_F32_TO_INT8_SAT(in.x)) << IM_COL32_R_SHIFT; + out |= ((ImU32)IM_F32_TO_INT8_SAT(in.y)) << IM_COL32_G_SHIFT; + out |= ((ImU32)IM_F32_TO_INT8_SAT(in.z)) << IM_COL32_B_SHIFT; + out |= ((ImU32)IM_F32_TO_INT8_SAT(in.w)) << IM_COL32_A_SHIFT; + return out; +} + +// Convert rgb floats ([0-1],[0-1],[0-1]) to hsv floats ([0-1],[0-1],[0-1]), from Foley & van Dam p592 +// Optimized http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv +void ImGui::ColorConvertRGBtoHSV(float r, float g, float b, float& out_h, float& out_s, float& out_v) +{ + float K = 0.f; + if (g < b) + { + ImSwap(g, b); + K = -1.f; + } + if (r < g) + { + ImSwap(r, g); + K = -2.f / 6.f - K; + } + + const float chroma = r - (g < b ? g : b); + out_h = ImFabs(K + (g - b) / (6.f * chroma + 1e-20f)); + out_s = chroma / (r + 1e-20f); + out_v = r; +} + +// Convert hsv floats ([0-1],[0-1],[0-1]) to rgb floats ([0-1],[0-1],[0-1]), from Foley & van Dam p593 +// also http://en.wikipedia.org/wiki/HSL_and_HSV +void ImGui::ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& out_g, float& out_b) +{ + if (s == 0.0f) + { + // gray + out_r = out_g = out_b = v; + return; + } + + h = ImFmod(h, 1.0f) / (60.0f / 360.0f); + int i = (int)h; + float f = h - (float)i; + float p = v * (1.0f - s); + float q = v * (1.0f - s * f); + float t = v * (1.0f - s * (1.0f - f)); + + switch (i) + { + case 0: out_r = v; out_g = t; out_b = p; break; + case 1: out_r = q; out_g = v; out_b = p; break; + case 2: out_r = p; out_g = v; out_b = t; break; + case 3: out_r = p; out_g = q; out_b = v; break; + case 4: out_r = t; out_g = p; out_b = v; break; + case 5: default: out_r = v; out_g = p; out_b = q; break; + } +} + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiStorage +// Helper: Key->value storage +//----------------------------------------------------------------------------- + +// std::lower_bound but without the bullshit +static ImGuiStorage::ImGuiStoragePair* LowerBound(ImVector& data, ImGuiID key) +{ + ImGuiStorage::ImGuiStoragePair* first = data.Data; + ImGuiStorage::ImGuiStoragePair* last = data.Data + data.Size; + size_t count = (size_t)(last - first); + while (count > 0) + { + size_t count2 = count >> 1; + ImGuiStorage::ImGuiStoragePair* mid = first + count2; + if (mid->key < key) + { + first = ++mid; + count -= count2 + 1; + } + else + { + count = count2; + } + } + return first; +} + +// For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. +void ImGuiStorage::BuildSortByKey() +{ + struct StaticFunc + { + static int IMGUI_CDECL PairComparerByID(const void* lhs, const void* rhs) + { + // We can't just do a subtraction because qsort uses signed integers and subtracting our ID doesn't play well with that. + if (((const ImGuiStoragePair*)lhs)->key > ((const ImGuiStoragePair*)rhs)->key) return +1; + if (((const ImGuiStoragePair*)lhs)->key < ((const ImGuiStoragePair*)rhs)->key) return -1; + return 0; + } + }; + ImQsort(Data.Data, (size_t)Data.Size, sizeof(ImGuiStoragePair), StaticFunc::PairComparerByID); +} + +int ImGuiStorage::GetInt(ImGuiID key, int default_val) const +{ + ImGuiStoragePair* it = LowerBound(const_cast&>(Data), key); + if (it == Data.end() || it->key != key) + return default_val; + return it->val_i; +} + +bool ImGuiStorage::GetBool(ImGuiID key, bool default_val) const +{ + return GetInt(key, default_val ? 1 : 0) != 0; +} + +float ImGuiStorage::GetFloat(ImGuiID key, float default_val) const +{ + ImGuiStoragePair* it = LowerBound(const_cast&>(Data), key); + if (it == Data.end() || it->key != key) + return default_val; + return it->val_f; +} + +void* ImGuiStorage::GetVoidPtr(ImGuiID key) const +{ + ImGuiStoragePair* it = LowerBound(const_cast&>(Data), key); + if (it == Data.end() || it->key != key) + return NULL; + return it->val_p; +} + +// References are only valid until a new value is added to the storage. Calling a Set***() function or a Get***Ref() function invalidates the pointer. +int* ImGuiStorage::GetIntRef(ImGuiID key, int default_val) +{ + ImGuiStoragePair* it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + it = Data.insert(it, ImGuiStoragePair(key, default_val)); + return &it->val_i; +} + +bool* ImGuiStorage::GetBoolRef(ImGuiID key, bool default_val) +{ + return (bool*)GetIntRef(key, default_val ? 1 : 0); +} + +float* ImGuiStorage::GetFloatRef(ImGuiID key, float default_val) +{ + ImGuiStoragePair* it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + it = Data.insert(it, ImGuiStoragePair(key, default_val)); + return &it->val_f; +} + +void** ImGuiStorage::GetVoidPtrRef(ImGuiID key, void* default_val) +{ + ImGuiStoragePair* it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + it = Data.insert(it, ImGuiStoragePair(key, default_val)); + return &it->val_p; +} + +// FIXME-OPT: Need a way to reuse the result of lower_bound when doing GetInt()/SetInt() - not too bad because it only happens on explicit interaction (maximum one a frame) +void ImGuiStorage::SetInt(ImGuiID key, int val) +{ + ImGuiStoragePair* it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + { + Data.insert(it, ImGuiStoragePair(key, val)); + return; + } + it->val_i = val; +} + +void ImGuiStorage::SetBool(ImGuiID key, bool val) +{ + SetInt(key, val ? 1 : 0); +} + +void ImGuiStorage::SetFloat(ImGuiID key, float val) +{ + ImGuiStoragePair* it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + { + Data.insert(it, ImGuiStoragePair(key, val)); + return; + } + it->val_f = val; +} + +void ImGuiStorage::SetVoidPtr(ImGuiID key, void* val) +{ + ImGuiStoragePair* it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + { + Data.insert(it, ImGuiStoragePair(key, val)); + return; + } + it->val_p = val; +} + +void ImGuiStorage::SetAllInt(int v) +{ + for (int i = 0; i < Data.Size; i++) + Data[i].val_i = v; +} + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiTextFilter +//----------------------------------------------------------------------------- + +// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]" +ImGuiTextFilter::ImGuiTextFilter(const char* default_filter) +{ + if (default_filter) + { + ImStrncpy(InputBuf, default_filter, IM_ARRAYSIZE(InputBuf)); + Build(); + } + else + { + InputBuf[0] = 0; + CountGrep = 0; + } +} + +bool ImGuiTextFilter::Draw(const char* label, float width) +{ + if (width != 0.0f) + ImGui::SetNextItemWidth(width); + bool value_changed = ImGui::InputText(label, InputBuf, IM_ARRAYSIZE(InputBuf)); + if (value_changed) + Build(); + return value_changed; +} + +void ImGuiTextFilter::ImGuiTextRange::split(char separator, ImVector* out) const +{ + out->resize(0); + const char* wb = b; + const char* we = wb; + while (we < e) + { + if (*we == separator) + { + out->push_back(ImGuiTextRange(wb, we)); + wb = we + 1; + } + we++; + } + if (wb != we) + out->push_back(ImGuiTextRange(wb, we)); +} + +void ImGuiTextFilter::Build() +{ + Filters.resize(0); + ImGuiTextRange input_range(InputBuf, InputBuf + strlen(InputBuf)); + input_range.split(',', &Filters); + + CountGrep = 0; + for (int i = 0; i != Filters.Size; i++) + { + ImGuiTextRange& f = Filters[i]; + while (f.b < f.e && ImCharIsBlankA(f.b[0])) + f.b++; + while (f.e > f.b && ImCharIsBlankA(f.e[-1])) + f.e--; + if (f.empty()) + continue; + if (Filters[i].b[0] != '-') + CountGrep += 1; + } +} + +bool ImGuiTextFilter::PassFilter(const char* text, const char* text_end) const +{ + if (Filters.empty()) + return true; + + if (text == NULL) + text = ""; + + for (int i = 0; i != Filters.Size; i++) + { + const ImGuiTextRange& f = Filters[i]; + if (f.empty()) + continue; + if (f.b[0] == '-') + { + // Subtract + if (ImStristr(text, text_end, f.b + 1, f.e) != NULL) + return false; + } + else + { + // Grep + if (ImStristr(text, text_end, f.b, f.e) != NULL) + return true; + } + } + + // Implicit * grep + if (CountGrep == 0) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiTextBuffer +//----------------------------------------------------------------------------- + +// On some platform vsnprintf() takes va_list by reference and modifies it. +// va_copy is the 'correct' way to copy a va_list but Visual Studio prior to 2013 doesn't have it. +#ifndef va_copy +#if defined(__GNUC__) || defined(__clang__) +#define va_copy(dest, src) __builtin_va_copy(dest, src) +#else +#define va_copy(dest, src) (dest = src) +#endif +#endif + +char ImGuiTextBuffer::EmptyString[1] = { 0 }; + +void ImGuiTextBuffer::append(const char* str, const char* str_end) +{ + int len = str_end ? (int)(str_end - str) : (int)strlen(str); + + // Add zero-terminator the first time + const int write_off = (Buf.Size != 0) ? Buf.Size : 1; + const int needed_sz = write_off + len; + if (write_off + len >= Buf.Capacity) + { + int new_capacity = Buf.Capacity * 2; + Buf.reserve(needed_sz > new_capacity ? needed_sz : new_capacity); + } + + Buf.resize(needed_sz); + memcpy(&Buf[write_off - 1], str, (size_t)len); + Buf[write_off - 1 + len] = 0; +} + +void ImGuiTextBuffer::appendf(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + appendfv(fmt, args); + va_end(args); +} + +// Helper: Text buffer for logging/accumulating text +void ImGuiTextBuffer::appendfv(const char* fmt, va_list args) +{ + va_list args_copy; + va_copy(args_copy, args); + + int len = ImFormatStringV(NULL, 0, fmt, args); // FIXME-OPT: could do a first pass write attempt, likely successful on first pass. + if (len <= 0) + { + va_end(args_copy); + return; + } + + // Add zero-terminator the first time + const int write_off = (Buf.Size != 0) ? Buf.Size : 1; + const int needed_sz = write_off + len; + if (write_off + len >= Buf.Capacity) + { + int new_capacity = Buf.Capacity * 2; + Buf.reserve(needed_sz > new_capacity ? needed_sz : new_capacity); + } + + Buf.resize(needed_sz); + ImFormatStringV(&Buf[write_off - 1], (size_t)len + 1, fmt, args_copy); + va_end(args_copy); +} + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiListClipper +// This is currently not as flexible/powerful as it should be and really confusing/spaghetti, mostly because we changed +// the API mid-way through development and support two ways to using the clipper, needs some rework (see TODO) +//----------------------------------------------------------------------------- + +// FIXME-TABLE: This prevents us from using ImGuiListClipper _inside_ a table cell. +// The problem we have is that without a Begin/End scheme for rows using the clipper is ambiguous. +static bool GetSkipItemForListClipping() +{ + ImGuiContext& g = *GImGui; + return (g.CurrentTable ? g.CurrentTable->HostSkipItems : g.CurrentWindow->SkipItems); +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +// Legacy helper to calculate coarse clipping of large list of evenly sized items. +// This legacy API is not ideal because it assume we will return a single contiguous rectangle. +// Prefer using ImGuiListClipper which can returns non-contiguous ranges. +void ImGui::CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.LogEnabled) + { + // If logging is active, do not perform any clipping + *out_items_display_start = 0; + *out_items_display_end = items_count; + return; + } + if (GetSkipItemForListClipping()) + { + *out_items_display_start = *out_items_display_end = 0; + return; + } + + // We create the union of the ClipRect and the scoring rect which at worst should be 1 page away from ClipRect + // We don't include g.NavId's rectangle in there (unless g.NavJustMovedToId is set) because the rectangle enlargement can get costly. + ImRect rect = window->ClipRect; + if (g.NavMoveScoringItems) + rect.Add(g.NavScoringNoClipRect); + if (g.NavJustMovedToId && window->NavLastIds[0] == g.NavJustMovedToId) + rect.Add(WindowRectRelToAbs(window, window->NavRectRel[0])); // Could store and use NavJustMovedToRectRel + + const ImVec2 pos = window->DC.CursorPos; + int start = (int)((rect.Min.y - pos.y) / items_height); + int end = (int)((rect.Max.y - pos.y) / items_height); + + // When performing a navigation request, ensure we have one item extra in the direction we are moving to + // FIXME: Verify this works with tabbing + const bool is_nav_request = (g.NavMoveScoringItems && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav); + if (is_nav_request && g.NavMoveClipDir == ImGuiDir_Up) + start--; + if (is_nav_request && g.NavMoveClipDir == ImGuiDir_Down) + end++; + + start = ImClamp(start, 0, items_count); + end = ImClamp(end + 1, start, items_count); + *out_items_display_start = start; + *out_items_display_end = end; +} +#endif + +static void ImGuiListClipper_SortAndFuseRanges(ImVector& ranges, int offset = 0) +{ + if (ranges.Size - offset <= 1) + return; + + // Helper to order ranges and fuse them together if possible (bubble sort is fine as we are only sorting 2-3 entries) + for (int sort_end = ranges.Size - offset - 1; sort_end > 0; --sort_end) + for (int i = offset; i < sort_end + offset; ++i) + if (ranges[i].Min > ranges[i + 1].Min) + ImSwap(ranges[i], ranges[i + 1]); + + // Now fuse ranges together as much as possible. + for (int i = 1 + offset; i < ranges.Size; i++) + { + IM_ASSERT(!ranges[i].PosToIndexConvert && !ranges[i - 1].PosToIndexConvert); + if (ranges[i - 1].Max < ranges[i].Min) + continue; + ranges[i - 1].Min = ImMin(ranges[i - 1].Min, ranges[i].Min); + ranges[i - 1].Max = ImMax(ranges[i - 1].Max, ranges[i].Max); + ranges.erase(ranges.Data + i); + i--; + } +} + +static void ImGuiListClipper_SeekCursorAndSetupPrevLine(float pos_y, float line_height) +{ + // Set cursor position and a few other things so that SetScrollHereY() and Columns() can work when seeking cursor. + // FIXME: It is problematic that we have to do that here, because custom/equivalent end-user code would stumble on the same issue. + // The clipper should probably have a final step to display the last item in a regular manner, maybe with an opt-out flag for data sets which may have costly seek? + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float off_y = pos_y - window->DC.CursorPos.y; + window->DC.CursorPos.y = pos_y; + window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, pos_y - g.Style.ItemSpacing.y); + window->DC.CursorPosPrevLine.y = window->DC.CursorPos.y - line_height; // Setting those fields so that SetScrollHereY() can properly function after the end of our clipper usage. + window->DC.PrevLineSize.y = (line_height - g.Style.ItemSpacing.y); // If we end up needing more accurate data (to e.g. use SameLine) we may as well make the clipper have a fourth step to let user process and display the last item in their list. + if (ImGuiOldColumns* columns = window->DC.CurrentColumns) + columns->LineMinY = window->DC.CursorPos.y; // Setting this so that cell Y position are set properly + if (ImGuiTable* table = g.CurrentTable) + { + if (table->IsInsideRow) + ImGui::TableEndRow(table); + table->RowPosY2 = window->DC.CursorPos.y; + const int row_increase = (int)((off_y / line_height) + 0.5f); + //table->CurrentRow += row_increase; // Can't do without fixing TableEndRow() + table->RowBgColorCounter += row_increase; + } +} + +static void ImGuiListClipper_SeekCursorForItem(ImGuiListClipper* clipper, int item_n) +{ + // StartPosY starts from ItemsFrozen hence the subtraction + // Perform the add and multiply with double to allow seeking through larger ranges + ImGuiListClipperData* data = (ImGuiListClipperData*)clipper->TempData; + float pos_y = (float)((double)clipper->StartPosY + data->LossynessOffset + (double)(item_n - data->ItemsFrozen) * clipper->ItemsHeight); + ImGuiListClipper_SeekCursorAndSetupPrevLine(pos_y, clipper->ItemsHeight); +} + +ImGuiListClipper::ImGuiListClipper() +{ + memset(this, 0, sizeof(*this)); + ItemsCount = -1; +} + +ImGuiListClipper::~ImGuiListClipper() +{ + End(); +} + +// Use case A: Begin() called from constructor with items_height<0, then called again from Step() in StepNo 1 +// Use case B: Begin() called from constructor with items_height>0 +// FIXME-LEGACY: Ideally we should remove the Begin/End functions but they are part of the legacy API we still support. This is why some of the code in Step() calling Begin() and reassign some fields, spaghetti style. +void ImGuiListClipper::Begin(int items_count, float items_height) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + if (ImGuiTable* table = g.CurrentTable) + if (table->IsInsideRow) + ImGui::TableEndRow(table); + + StartPosY = window->DC.CursorPos.y; + ItemsHeight = items_height; + ItemsCount = items_count; + DisplayStart = -1; + DisplayEnd = 0; + + // Acquire temporary buffer + if (++g.ClipperTempDataStacked > g.ClipperTempData.Size) + g.ClipperTempData.resize(g.ClipperTempDataStacked, ImGuiListClipperData()); + ImGuiListClipperData* data = &g.ClipperTempData[g.ClipperTempDataStacked - 1]; + data->Reset(this); + data->LossynessOffset = window->DC.CursorStartPosLossyness.y; + TempData = data; +} + +void ImGuiListClipper::End() +{ + // In theory here we should assert that we are already at the right position, but it seems saner to just seek at the end and not assert/crash the user. + ImGuiContext& g = *GImGui; + if (ItemsCount >= 0 && ItemsCount < INT_MAX && DisplayStart >= 0) + ImGuiListClipper_SeekCursorForItem(this, ItemsCount); + ItemsCount = -1; + + // Restore temporary buffer and fix back pointers which may be invalidated when nesting + if (ImGuiListClipperData* data = (ImGuiListClipperData*)TempData) + { + IM_ASSERT(data->ListClipper == this); + data->StepNo = data->Ranges.Size; + if (--g.ClipperTempDataStacked > 0) + { + data = &g.ClipperTempData[g.ClipperTempDataStacked - 1]; + data->ListClipper->TempData = data; + } + TempData = NULL; + } +} + +void ImGuiListClipper::ForceDisplayRangeByIndices(int item_min, int item_max) +{ + ImGuiListClipperData* data = (ImGuiListClipperData*)TempData; + IM_ASSERT(DisplayStart < 0); // Only allowed after Begin() and if there has not been a specified range yet. + IM_ASSERT(item_min <= item_max); + if (item_min < item_max) + data->Ranges.push_back(ImGuiListClipperRange::FromIndices(item_min, item_max)); +} + +bool ImGuiListClipper::Step() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiListClipperData* data = (ImGuiListClipperData*)TempData; + + ImGuiTable* table = g.CurrentTable; + if (table && table->IsInsideRow) + ImGui::TableEndRow(table); + + // No items + if (ItemsCount == 0 || GetSkipItemForListClipping()) + return (void)End(), false; + + // While we are in frozen row state, keep displaying items one by one, unclipped + // FIXME: Could be stored as a table-agnostic state. + if (data->StepNo == 0 && table != NULL && !table->IsUnfrozenRows) + { + DisplayStart = data->ItemsFrozen; + DisplayEnd = data->ItemsFrozen + 1; + if (DisplayStart >= ItemsCount) + return (void)End(), false; + data->ItemsFrozen++; + return true; + } + + // Step 0: Let you process the first element (regardless of it being visible or not, so we can measure the element height) + bool calc_clipping = false; + if (data->StepNo == 0) + { + StartPosY = window->DC.CursorPos.y; + if (ItemsHeight <= 0.0f) + { + // Submit the first item (or range) so we can measure its height (generally the first range is 0..1) + data->Ranges.push_front(ImGuiListClipperRange::FromIndices(data->ItemsFrozen, data->ItemsFrozen + 1)); + DisplayStart = ImMax(data->Ranges[0].Min, data->ItemsFrozen); + DisplayEnd = ImMin(data->Ranges[0].Max, ItemsCount); + if (DisplayStart == DisplayEnd) + return (void)End(), false; + data->StepNo = 1; + return true; + } + calc_clipping = true; // If on the first step with known item height, calculate clipping. + } + + // Step 1: Let the clipper infer height from first range + if (ItemsHeight <= 0.0f) + { + IM_ASSERT(data->StepNo == 1); + if (table) + IM_ASSERT(table->RowPosY1 == StartPosY && table->RowPosY2 == window->DC.CursorPos.y); + + ItemsHeight = (window->DC.CursorPos.y - StartPosY) / (float)(DisplayEnd - DisplayStart); + bool affected_by_floating_point_precision = ImIsFloatAboveGuaranteedIntegerPrecision(StartPosY) || ImIsFloatAboveGuaranteedIntegerPrecision(window->DC.CursorPos.y); + if (affected_by_floating_point_precision) + ItemsHeight = window->DC.PrevLineSize.y + g.Style.ItemSpacing.y; // FIXME: Technically wouldn't allow multi-line entries. + + IM_ASSERT(ItemsHeight > 0.0f && "Unable to calculate item height! First item hasn't moved the cursor vertically!"); + calc_clipping = true; // If item height had to be calculated, calculate clipping afterwards. + } + + // Step 0 or 1: Calculate the actual ranges of visible elements. + const int already_submitted = DisplayEnd; + if (calc_clipping) + { + if (g.LogEnabled) + { + // If logging is active, do not perform any clipping + data->Ranges.push_back(ImGuiListClipperRange::FromIndices(0, ItemsCount)); + } + else + { + // Add range selected to be included for navigation + const bool is_nav_request = (g.NavMoveScoringItems && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav); + if (is_nav_request) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(g.NavScoringNoClipRect.Min.y, g.NavScoringNoClipRect.Max.y, 0, 0)); + if (is_nav_request && (g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) && g.NavTabbingDir == -1) + data->Ranges.push_back(ImGuiListClipperRange::FromIndices(ItemsCount - 1, ItemsCount)); + + // Add focused/active item + ImRect nav_rect_abs = ImGui::WindowRectRelToAbs(window, window->NavRectRel[0]); + if (g.NavId != 0 && window->NavLastIds[0] == g.NavId) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(nav_rect_abs.Min.y, nav_rect_abs.Max.y, 0, 0)); + + // Add visible range + const int off_min = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Up) ? -1 : 0; + const int off_max = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Down) ? 1 : 0; + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(window->ClipRect.Min.y, window->ClipRect.Max.y, off_min, off_max)); + } + + // Convert position ranges to item index ranges + // - Very important: when a starting position is after our maximum item, we set Min to (ItemsCount - 1). This allows us to handle most forms of wrapping. + // - Due to how Selectable extra padding they tend to be "unaligned" with exact unit in the item list, + // which with the flooring/ceiling tend to lead to 2 items instead of one being submitted. + for (int i = 0; i < data->Ranges.Size; i++) + if (data->Ranges[i].PosToIndexConvert) + { + int m1 = (int)(((double)data->Ranges[i].Min - window->DC.CursorPos.y - data->LossynessOffset) / ItemsHeight); + int m2 = (int)((((double)data->Ranges[i].Max - window->DC.CursorPos.y - data->LossynessOffset) / ItemsHeight) + 0.999999f); + data->Ranges[i].Min = ImClamp(already_submitted + m1 + data->Ranges[i].PosToIndexOffsetMin, already_submitted, ItemsCount - 1); + data->Ranges[i].Max = ImClamp(already_submitted + m2 + data->Ranges[i].PosToIndexOffsetMax, data->Ranges[i].Min + 1, ItemsCount); + data->Ranges[i].PosToIndexConvert = false; + } + ImGuiListClipper_SortAndFuseRanges(data->Ranges, data->StepNo); + } + + // Step 0+ (if item height is given in advance) or 1+: Display the next range in line. + if (data->StepNo < data->Ranges.Size) + { + DisplayStart = ImMax(data->Ranges[data->StepNo].Min, already_submitted); + DisplayEnd = ImMin(data->Ranges[data->StepNo].Max, ItemsCount); + if (DisplayStart > already_submitted) //-V1051 + ImGuiListClipper_SeekCursorForItem(this, DisplayStart); + data->StepNo++; + return true; + } + + // After the last step: Let the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), + // Advance the cursor to the end of the list and then returns 'false' to end the loop. + if (ItemsCount < INT_MAX) + ImGuiListClipper_SeekCursorForItem(this, ItemsCount); + ItemsCount = -1; + + return false; +} + +//----------------------------------------------------------------------------- +// [SECTION] STYLING +//----------------------------------------------------------------------------- + +ImGuiStyle& ImGui::GetStyle() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); + return GImGui->Style; +} + +ImU32 ImGui::GetColorU32(ImGuiCol idx, float alpha_mul) +{ + ImGuiStyle& style = GImGui->Style; + ImVec4 c = style.Colors[idx]; + c.w *= style.Alpha * alpha_mul; + return ColorConvertFloat4ToU32(c); +} + +ImU32 ImGui::GetColorU32(const ImVec4& col) +{ + ImGuiStyle& style = GImGui->Style; + ImVec4 c = col; + c.w *= style.Alpha; + return ColorConvertFloat4ToU32(c); +} + +const ImVec4& ImGui::GetStyleColorVec4(ImGuiCol idx) +{ + ImGuiStyle& style = GImGui->Style; + return style.Colors[idx]; +} + +ImU32 ImGui::GetColorU32(ImU32 col) +{ + ImGuiStyle& style = GImGui->Style; + if (style.Alpha >= 1.0f) + return col; + ImU32 a = (col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT; + a = (ImU32)(a * style.Alpha); // We don't need to clamp 0..255 because Style.Alpha is in 0..1 range. + return (col & ~IM_COL32_A_MASK) | (a << IM_COL32_A_SHIFT); +} + +// FIXME: This may incur a round-trip (if the end user got their data from a float4) but eventually we aim to store the in-flight colors as ImU32 +void ImGui::PushStyleColor(ImGuiCol idx, ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiColorMod backup; + backup.Col = idx; + backup.BackupValue = g.Style.Colors[idx]; + g.ColorStack.push_back(backup); + g.Style.Colors[idx] = ColorConvertU32ToFloat4(col); +} + +void ImGui::PushStyleColor(ImGuiCol idx, const ImVec4& col) +{ + ImGuiContext& g = *GImGui; + ImGuiColorMod backup; + backup.Col = idx; + backup.BackupValue = g.Style.Colors[idx]; + g.ColorStack.push_back(backup); + g.Style.Colors[idx] = col; +} + +void ImGui::PopStyleColor(int count) +{ + ImGuiContext& g = *GImGui; + while (count > 0) + { + ImGuiColorMod& backup = g.ColorStack.back(); + g.Style.Colors[backup.Col] = backup.BackupValue; + g.ColorStack.pop_back(); + count--; + } +} + +struct ImGuiStyleVarInfo +{ + ImGuiDataType Type; + ImU32 Count; + ImU32 Offset; + void* GetVarPtr(ImGuiStyle* style) const { return (void*)((unsigned char*)style + Offset); } +}; + +static const ImGuiStyleVarInfo GStyleVarInfo[] = +{ + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, Alpha) }, // ImGuiStyleVar_Alpha + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, DisabledAlpha) }, // ImGuiStyleVar_DisabledAlpha + { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowPadding) }, // ImGuiStyleVar_WindowPadding + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowRounding) }, // ImGuiStyleVar_WindowRounding + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowBorderSize) }, // ImGuiStyleVar_WindowBorderSize + { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowMinSize) }, // ImGuiStyleVar_WindowMinSize + { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowTitleAlign) }, // ImGuiStyleVar_WindowTitleAlign + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ChildRounding) }, // ImGuiStyleVar_ChildRounding + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ChildBorderSize) }, // ImGuiStyleVar_ChildBorderSize + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, PopupRounding) }, // ImGuiStyleVar_PopupRounding + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, PopupBorderSize) }, // ImGuiStyleVar_PopupBorderSize + { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, FramePadding) }, // ImGuiStyleVar_FramePadding + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, FrameRounding) }, // ImGuiStyleVar_FrameRounding + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, FrameBorderSize) }, // ImGuiStyleVar_FrameBorderSize + { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemSpacing) }, // ImGuiStyleVar_ItemSpacing + { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemInnerSpacing) }, // ImGuiStyleVar_ItemInnerSpacing + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, IndentSpacing) }, // ImGuiStyleVar_IndentSpacing + { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, CellPadding) }, // ImGuiStyleVar_CellPadding + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarSize) }, // ImGuiStyleVar_ScrollbarSize + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarRounding) }, // ImGuiStyleVar_ScrollbarRounding + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, GrabMinSize) }, // ImGuiStyleVar_GrabMinSize + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, GrabRounding) }, // ImGuiStyleVar_GrabRounding + { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, TabRounding) }, // ImGuiStyleVar_TabRounding + { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign + { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign +}; + +static const ImGuiStyleVarInfo* GetStyleVarInfo(ImGuiStyleVar idx) +{ + IM_ASSERT(idx >= 0 && idx < ImGuiStyleVar_COUNT); + IM_ASSERT(IM_ARRAYSIZE(GStyleVarInfo) == ImGuiStyleVar_COUNT); + return &GStyleVarInfo[idx]; +} + +void ImGui::PushStyleVar(ImGuiStyleVar idx, float val) +{ + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->Type == ImGuiDataType_Float && var_info->Count == 1) + { + ImGuiContext& g = *GImGui; + float* pvar = (float*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + *pvar = val; + return; + } + IM_ASSERT(0 && "Called PushStyleVar() float variant but variable is not a float!"); +} + +void ImGui::PushStyleVar(ImGuiStyleVar idx, const ImVec2& val) +{ + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->Type == ImGuiDataType_Float && var_info->Count == 2) + { + ImGuiContext& g = *GImGui; + ImVec2* pvar = (ImVec2*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + *pvar = val; + return; + } + IM_ASSERT(0 && "Called PushStyleVar() ImVec2 variant but variable is not a ImVec2!"); +} + +void ImGui::PopStyleVar(int count) +{ + ImGuiContext& g = *GImGui; + while (count > 0) + { + // We avoid a generic memcpy(data, &backup.Backup.., GDataTypeSize[info->Type] * info->Count), the overhead in Debug is not worth it. + ImGuiStyleMod& backup = g.StyleVarStack.back(); + const ImGuiStyleVarInfo* info = GetStyleVarInfo(backup.VarIdx); + void* data = info->GetVarPtr(&g.Style); + if (info->Type == ImGuiDataType_Float && info->Count == 1) { ((float*)data)[0] = backup.BackupFloat[0]; } + else if (info->Type == ImGuiDataType_Float && info->Count == 2) { ((float*)data)[0] = backup.BackupFloat[0]; ((float*)data)[1] = backup.BackupFloat[1]; } + g.StyleVarStack.pop_back(); + count--; + } +} + +const char* ImGui::GetStyleColorName(ImGuiCol idx) +{ + // Create switch-case from enum with regexp: ImGuiCol_{.*}, --> case ImGuiCol_\1: return "\1"; + switch (idx) + { + case ImGuiCol_Text: return "Text"; + case ImGuiCol_TextDisabled: return "TextDisabled"; + case ImGuiCol_WindowBg: return "WindowBg"; + case ImGuiCol_ChildBg: return "ChildBg"; + case ImGuiCol_PopupBg: return "PopupBg"; + case ImGuiCol_Border: return "Border"; + case ImGuiCol_BorderShadow: return "BorderShadow"; + case ImGuiCol_FrameBg: return "FrameBg"; + case ImGuiCol_FrameBgHovered: return "FrameBgHovered"; + case ImGuiCol_FrameBgActive: return "FrameBgActive"; + case ImGuiCol_TitleBg: return "TitleBg"; + case ImGuiCol_TitleBgActive: return "TitleBgActive"; + case ImGuiCol_TitleBgCollapsed: return "TitleBgCollapsed"; + case ImGuiCol_MenuBarBg: return "MenuBarBg"; + case ImGuiCol_ScrollbarBg: return "ScrollbarBg"; + case ImGuiCol_ScrollbarGrab: return "ScrollbarGrab"; + case ImGuiCol_ScrollbarGrabHovered: return "ScrollbarGrabHovered"; + case ImGuiCol_ScrollbarGrabActive: return "ScrollbarGrabActive"; + case ImGuiCol_CheckMark: return "CheckMark"; + case ImGuiCol_SliderGrab: return "SliderGrab"; + case ImGuiCol_SliderGrabActive: return "SliderGrabActive"; + case ImGuiCol_Button: return "Button"; + case ImGuiCol_ButtonHovered: return "ButtonHovered"; + case ImGuiCol_ButtonActive: return "ButtonActive"; + case ImGuiCol_Header: return "Header"; + case ImGuiCol_HeaderHovered: return "HeaderHovered"; + case ImGuiCol_HeaderActive: return "HeaderActive"; + case ImGuiCol_Separator: return "Separator"; + case ImGuiCol_SeparatorHovered: return "SeparatorHovered"; + case ImGuiCol_SeparatorActive: return "SeparatorActive"; + case ImGuiCol_ResizeGrip: return "ResizeGrip"; + case ImGuiCol_ResizeGripHovered: return "ResizeGripHovered"; + case ImGuiCol_ResizeGripActive: return "ResizeGripActive"; + case ImGuiCol_Tab: return "Tab"; + case ImGuiCol_TabHovered: return "TabHovered"; + case ImGuiCol_TabActive: return "TabActive"; + case ImGuiCol_TabUnfocused: return "TabUnfocused"; + case ImGuiCol_TabUnfocusedActive: return "TabUnfocusedActive"; + case ImGuiCol_PlotLines: return "PlotLines"; + case ImGuiCol_PlotLinesHovered: return "PlotLinesHovered"; + case ImGuiCol_PlotHistogram: return "PlotHistogram"; + case ImGuiCol_PlotHistogramHovered: return "PlotHistogramHovered"; + case ImGuiCol_TableHeaderBg: return "TableHeaderBg"; + case ImGuiCol_TableBorderStrong: return "TableBorderStrong"; + case ImGuiCol_TableBorderLight: return "TableBorderLight"; + case ImGuiCol_TableRowBg: return "TableRowBg"; + case ImGuiCol_TableRowBgAlt: return "TableRowBgAlt"; + case ImGuiCol_TextSelectedBg: return "TextSelectedBg"; + case ImGuiCol_DragDropTarget: return "DragDropTarget"; + case ImGuiCol_NavHighlight: return "NavHighlight"; + case ImGuiCol_NavWindowingHighlight: return "NavWindowingHighlight"; + case ImGuiCol_NavWindowingDimBg: return "NavWindowingDimBg"; + case ImGuiCol_ModalWindowDimBg: return "ModalWindowDimBg"; + } + IM_ASSERT(0); + return "Unknown"; +} + + +//----------------------------------------------------------------------------- +// [SECTION] RENDER HELPERS +// Some of those (internal) functions are currently quite a legacy mess - their signature and behavior will change, +// we need a nicer separation between low-level functions and high-level functions relying on the ImGui context. +// Also see imgui_draw.cpp for some more which have been reworked to not rely on ImGui:: context. +//----------------------------------------------------------------------------- + +const char* ImGui::FindRenderedTextEnd(const char* text, const char* text_end) +{ + const char* text_display_end = text; + if (!text_end) + text_end = (const char*)-1; + + while (text_display_end < text_end && *text_display_end != '\0' && (text_display_end[0] != '#' || text_display_end[1] != '#')) + text_display_end++; + return text_display_end; +} + +// Internal ImGui functions to render text +// RenderText***() functions calls ImDrawList::AddText() calls ImBitmapFont::RenderText() +void ImGui::RenderText(ImVec2 pos, const char* text, const char* text_end, bool hide_text_after_hash) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Hide anything after a '##' string + const char* text_display_end; + if (hide_text_after_hash) + { + text_display_end = FindRenderedTextEnd(text, text_end); + } + else + { + if (!text_end) + text_end = text + strlen(text); // FIXME-OPT + text_display_end = text_end; + } + + if (text != text_display_end) + { + window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_display_end); + if (g.LogEnabled) + LogRenderedText(&pos, text, text_display_end); + } +} + +void ImGui::RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end, float wrap_width) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + if (!text_end) + text_end = text + strlen(text); // FIXME-OPT + + if (text != text_end) + { + window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_end, wrap_width); + if (g.LogEnabled) + LogRenderedText(&pos, text, text_end); + } +} + +// Default clip_rect uses (pos_min,pos_max) +// Handle clipping on CPU immediately (vs typically let the GPU clip the triangles that are overlapping the clipping rectangle edges) +void ImGui::RenderTextClippedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_display_end, const ImVec2* text_size_if_known, const ImVec2& align, const ImRect* clip_rect) +{ + // Perform CPU side clipping for single clipped element to avoid using scissor state + ImVec2 pos = pos_min; + const ImVec2 text_size = text_size_if_known ? *text_size_if_known : CalcTextSize(text, text_display_end, false, 0.0f); + + const ImVec2* clip_min = clip_rect ? &clip_rect->Min : &pos_min; + const ImVec2* clip_max = clip_rect ? &clip_rect->Max : &pos_max; + bool need_clipping = (pos.x + text_size.x >= clip_max->x) || (pos.y + text_size.y >= clip_max->y); + if (clip_rect) // If we had no explicit clipping rectangle then pos==clip_min + need_clipping |= (pos.x < clip_min->x) || (pos.y < clip_min->y); + + // Align whole block. We should defer that to the better rendering function when we'll have support for individual line alignment. + if (align.x > 0.0f) pos.x = ImMax(pos.x, pos.x + (pos_max.x - pos.x - text_size.x) * align.x); + if (align.y > 0.0f) pos.y = ImMax(pos.y, pos.y + (pos_max.y - pos.y - text_size.y) * align.y); + + // Render + if (need_clipping) + { + ImVec4 fine_clip_rect(clip_min->x, clip_min->y, clip_max->x, clip_max->y); + draw_list->AddText(NULL, 0.0f, pos, GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, &fine_clip_rect); + } + else + { + draw_list->AddText(NULL, 0.0f, pos, GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, NULL); + } +} + +void ImGui::RenderTextClipped(const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align, const ImRect* clip_rect) +{ + // Hide anything after a '##' string + const char* text_display_end = FindRenderedTextEnd(text, text_end); + const int text_len = (int)(text_display_end - text); + if (text_len == 0) + return; + + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + RenderTextClippedEx(window->DrawList, pos_min, pos_max, text, text_display_end, text_size_if_known, align, clip_rect); + if (g.LogEnabled) + LogRenderedText(&pos_min, text, text_display_end); +} + + +// Another overly complex function until we reorganize everything into a nice all-in-one helper. +// This is made more complex because we have dissociated the layout rectangle (pos_min..pos_max) which define _where_ the ellipsis is, from actual clipping of text and limit of the ellipsis display. +// This is because in the context of tabs we selectively hide part of the text when the Close Button appears, but we don't want the ellipsis to move. +void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, float clip_max_x, float ellipsis_max_x, const char* text, const char* text_end_full, const ImVec2* text_size_if_known) +{ + ImGuiContext& g = *GImGui; + if (text_end_full == NULL) + text_end_full = FindRenderedTextEnd(text); + const ImVec2 text_size = text_size_if_known ? *text_size_if_known : CalcTextSize(text, text_end_full, false, 0.0f); + + //draw_list->AddLine(ImVec2(pos_max.x, pos_min.y - 4), ImVec2(pos_max.x, pos_max.y + 4), IM_COL32(0, 0, 255, 255)); + //draw_list->AddLine(ImVec2(ellipsis_max_x, pos_min.y-2), ImVec2(ellipsis_max_x, pos_max.y+2), IM_COL32(0, 255, 0, 255)); + //draw_list->AddLine(ImVec2(clip_max_x, pos_min.y), ImVec2(clip_max_x, pos_max.y), IM_COL32(255, 0, 0, 255)); + // FIXME: We could technically remove (last_glyph->AdvanceX - last_glyph->X1) from text_size.x here and save a few pixels. + if (text_size.x > pos_max.x - pos_min.x) + { + // Hello wo... + // | | | + // min max ellipsis_max + // <-> this is generally some padding value + + const ImFont* font = draw_list->_Data->Font; + const float font_size = draw_list->_Data->FontSize; + const char* text_end_ellipsis = NULL; + + ImWchar ellipsis_char = font->EllipsisChar; + int ellipsis_char_count = 1; + if (ellipsis_char == (ImWchar)-1) + { + ellipsis_char = font->DotChar; + ellipsis_char_count = 3; + } + const ImFontGlyph* glyph = font->FindGlyph(ellipsis_char); + + float ellipsis_glyph_width = glyph->X1; // Width of the glyph with no padding on either side + float ellipsis_total_width = ellipsis_glyph_width; // Full width of entire ellipsis + + if (ellipsis_char_count > 1) + { + // Full ellipsis size without free spacing after it. + const float spacing_between_dots = 1.0f * (draw_list->_Data->FontSize / font->FontSize); + ellipsis_glyph_width = glyph->X1 - glyph->X0 + spacing_between_dots; + ellipsis_total_width = ellipsis_glyph_width * (float)ellipsis_char_count - spacing_between_dots; + } + + // We can now claim the space between pos_max.x and ellipsis_max.x + const float text_avail_width = ImMax((ImMax(pos_max.x, ellipsis_max_x) - ellipsis_total_width) - pos_min.x, 1.0f); + float text_size_clipped_x = font->CalcTextSizeA(font_size, text_avail_width, 0.0f, text, text_end_full, &text_end_ellipsis).x; + if (text == text_end_ellipsis && text_end_ellipsis < text_end_full) + { + // Always display at least 1 character if there's no room for character + ellipsis + text_end_ellipsis = text + ImTextCountUtf8BytesFromChar(text, text_end_full); + text_size_clipped_x = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, text, text_end_ellipsis).x; + } + while (text_end_ellipsis > text && ImCharIsBlankA(text_end_ellipsis[-1])) + { + // Trim trailing space before ellipsis (FIXME: Supporting non-ascii blanks would be nice, for this we need a function to backtrack in UTF-8 text) + text_end_ellipsis--; + text_size_clipped_x -= font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, text_end_ellipsis, text_end_ellipsis + 1).x; // Ascii blanks are always 1 byte + } + + // Render text, render ellipsis + RenderTextClippedEx(draw_list, pos_min, ImVec2(clip_max_x, pos_max.y), text, text_end_ellipsis, &text_size, ImVec2(0.0f, 0.0f)); + float ellipsis_x = pos_min.x + text_size_clipped_x; + if (ellipsis_x + ellipsis_total_width <= ellipsis_max_x) + for (int i = 0; i < ellipsis_char_count; i++) + { + font->RenderChar(draw_list, font_size, ImVec2(ellipsis_x, pos_min.y), GetColorU32(ImGuiCol_Text), ellipsis_char); + ellipsis_x += ellipsis_glyph_width; + } + } + else + { + RenderTextClippedEx(draw_list, pos_min, ImVec2(clip_max_x, pos_max.y), text, text_end_full, &text_size, ImVec2(0.0f, 0.0f)); + } + + if (g.LogEnabled) + LogRenderedText(&pos_min, text, text_end_full); +} + +// Render a rectangle shaped with optional rounding and borders +void ImGui::RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool border, float rounding) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DrawList->AddRectFilled(p_min, p_max, fill_col, rounding); + const float border_size = g.Style.FrameBorderSize; + if (border && border_size > 0.0f) + { + window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, 0, border_size); + window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, 0, border_size); + } +} + +void ImGui::RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const float border_size = g.Style.FrameBorderSize; + if (border_size > 0.0f) + { + window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, 0, border_size); + window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, 0, border_size); + } +} + +void ImGui::RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavHighlightFlags flags) +{ + ImGuiContext& g = *GImGui; + if (id != g.NavId) + return; + if (g.NavDisableHighlight && !(flags & ImGuiNavHighlightFlags_AlwaysDraw)) + return; + ImGuiWindow* window = g.CurrentWindow; + if (window->DC.NavHideHighlightOneFrame) + return; + + float rounding = (flags & ImGuiNavHighlightFlags_NoRounding) ? 0.0f : g.Style.FrameRounding; + ImRect display_rect = bb; + display_rect.ClipWith(window->ClipRect); + if (flags & ImGuiNavHighlightFlags_TypeDefault) + { + const float THICKNESS = 2.0f; + const float DISTANCE = 3.0f + THICKNESS * 0.5f; + display_rect.Expand(ImVec2(DISTANCE, DISTANCE)); + bool fully_visible = window->ClipRect.Contains(display_rect); + if (!fully_visible) + window->DrawList->PushClipRect(display_rect.Min, display_rect.Max); + window->DrawList->AddRect(display_rect.Min + ImVec2(THICKNESS * 0.5f, THICKNESS * 0.5f), display_rect.Max - ImVec2(THICKNESS * 0.5f, THICKNESS * 0.5f), GetColorU32(ImGuiCol_NavHighlight), rounding, 0, THICKNESS); + if (!fully_visible) + window->DrawList->PopClipRect(); + } + if (flags & ImGuiNavHighlightFlags_TypeThin) + { + window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavHighlight), rounding, 0, 1.0f); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] MAIN CODE (most of the code! lots of stuff, needs tidying up!) +//----------------------------------------------------------------------------- + +// ImGuiWindow is mostly a dumb struct. It merely has a constructor and a few helper methods +ImGuiWindow::ImGuiWindow(ImGuiContext* context, const char* name) : DrawListInst(NULL) +{ + memset(this, 0, sizeof(*this)); + Name = ImStrdup(name); + NameBufLen = (int)strlen(name) + 1; + ID = ImHashStr(name); + IDStack.push_back(ID); + MoveId = GetID("#MOVE"); + ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); + ScrollTargetCenterRatio = ImVec2(0.5f, 0.5f); + AutoFitFramesX = AutoFitFramesY = -1; + AutoPosLastDirection = ImGuiDir_None; + SetWindowPosAllowFlags = SetWindowSizeAllowFlags = SetWindowCollapsedAllowFlags = ImGuiCond_Always | ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing; + SetWindowPosVal = SetWindowPosPivot = ImVec2(FLT_MAX, FLT_MAX); + LastFrameActive = -1; + LastTimeActive = -1.0f; + FontWindowScale = 1.0f; + SettingsOffset = -1; + DrawList = &DrawListInst; + DrawList->_Data = &context->DrawListSharedData; + DrawList->_OwnerName = Name; +} + +ImGuiWindow::~ImGuiWindow() +{ + IM_ASSERT(DrawList == &DrawListInst); + IM_DELETE(Name); + ColumnsStorage.clear_destruct(); +} + +ImGuiID ImGuiWindow::GetID(const char* str, const char* str_end) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); + ImGui::KeepAliveID(id); + ImGuiContext& g = *GImGui; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); + return id; +} + +ImGuiID ImGuiWindow::GetID(const void* ptr) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); + ImGui::KeepAliveID(id); + ImGuiContext& g = *GImGui; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_Pointer, ptr, NULL); + return id; +} + +ImGuiID ImGuiWindow::GetID(int n) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashData(&n, sizeof(n), seed); + ImGui::KeepAliveID(id); + ImGuiContext& g = *GImGui; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL); + return id; +} + +ImGuiID ImGuiWindow::GetIDNoKeepAlive(const char* str, const char* str_end) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); + ImGuiContext& g = *GImGui; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); + return id; +} + +ImGuiID ImGuiWindow::GetIDNoKeepAlive(const void* ptr) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); + ImGuiContext& g = *GImGui; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_Pointer, ptr, NULL); + return id; +} + +ImGuiID ImGuiWindow::GetIDNoKeepAlive(int n) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashData(&n, sizeof(n), seed); + ImGuiContext& g = *GImGui; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL); + return id; +} + +// This is only used in rare/specific situations to manufacture an ID out of nowhere. +ImGuiID ImGuiWindow::GetIDFromRectangle(const ImRect& r_abs) +{ + ImGuiID seed = IDStack.back(); + ImRect r_rel = ImGui::WindowRectAbsToRel(this, r_abs); + ImGuiID id = ImHashData(&r_rel, sizeof(r_rel), seed); + ImGui::KeepAliveID(id); + return id; +} + +static void SetCurrentWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + g.CurrentWindow = window; + g.CurrentTable = window && window->DC.CurrentTableIdx != -1 ? g.Tables.GetByIndex(window->DC.CurrentTableIdx) : NULL; + if (window) + g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); +} + +void ImGui::GcCompactTransientMiscBuffers() +{ + ImGuiContext& g = *GImGui; + g.ItemFlagsStack.clear(); + g.GroupStack.clear(); + TableGcCompactSettings(); +} + +// Free up/compact internal window buffers, we can use this when a window becomes unused. +// Not freed: +// - ImGuiWindow, ImGuiWindowSettings, Name, StateStorage, ColumnsStorage (may hold useful data) +// This should have no noticeable visual effect. When the window reappear however, expect new allocation/buffer growth/copy cost. +void ImGui::GcCompactTransientWindowBuffers(ImGuiWindow* window) +{ + window->MemoryCompacted = true; + window->MemoryDrawListIdxCapacity = window->DrawList->IdxBuffer.Capacity; + window->MemoryDrawListVtxCapacity = window->DrawList->VtxBuffer.Capacity; + window->IDStack.clear(); + window->DrawList->_ClearFreeMemory(); + window->DC.ChildWindows.clear(); + window->DC.ItemWidthStack.clear(); + window->DC.TextWrapPosStack.clear(); +} + +void ImGui::GcAwakeTransientWindowBuffers(ImGuiWindow* window) +{ + // We stored capacity of the ImDrawList buffer to reduce growth-caused allocation/copy when awakening. + // The other buffers tends to amortize much faster. + window->MemoryCompacted = false; + window->DrawList->IdxBuffer.reserve(window->MemoryDrawListIdxCapacity); + window->DrawList->VtxBuffer.reserve(window->MemoryDrawListVtxCapacity); + window->MemoryDrawListIdxCapacity = window->MemoryDrawListVtxCapacity = 0; +} + +void ImGui::SetActiveID(ImGuiID id, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + g.ActiveIdIsJustActivated = (g.ActiveId != id); + if (g.ActiveIdIsJustActivated) + { + g.ActiveIdTimer = 0.0f; + g.ActiveIdHasBeenPressedBefore = false; + g.ActiveIdHasBeenEditedBefore = false; + g.ActiveIdMouseButton = -1; + if (id != 0) + { + g.LastActiveId = id; + g.LastActiveIdTimer = 0.0f; + } + } + g.ActiveId = id; + g.ActiveIdAllowOverlap = false; + g.ActiveIdNoClearOnFocusLoss = false; + g.ActiveIdWindow = window; + g.ActiveIdHasBeenEditedThisFrame = false; + if (id) + { + g.ActiveIdIsAlive = id; + g.ActiveIdSource = (g.NavActivateId == id || g.NavActivateInputId == id || g.NavJustMovedToId == id) ? (ImGuiInputSource)ImGuiInputSource_Nav : ImGuiInputSource_Mouse; + } + + // Clear declaration of inputs claimed by the widget + // (Please note that this is WIP and not all keys/inputs are thoroughly declared by all widgets yet) + g.ActiveIdUsingMouseWheel = false; + g.ActiveIdUsingNavDirMask = 0x00; + g.ActiveIdUsingNavInputMask = 0x00; + g.ActiveIdUsingKeyInputMask.ClearAllBits(); +} + +void ImGui::ClearActiveID() +{ + SetActiveID(0, NULL); // g.ActiveId = 0; +} + +void ImGui::SetHoveredID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.HoveredId = id; + g.HoveredIdAllowOverlap = false; + g.HoveredIdUsingMouseWheel = false; + if (id != 0 && g.HoveredIdPreviousFrame != id) + g.HoveredIdTimer = g.HoveredIdNotActiveTimer = 0.0f; +} + +ImGuiID ImGui::GetHoveredID() +{ + ImGuiContext& g = *GImGui; + return g.HoveredId ? g.HoveredId : g.HoveredIdPreviousFrame; +} + +void ImGui::KeepAliveID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + if (g.ActiveId == id) + g.ActiveIdIsAlive = id; + if (g.ActiveIdPreviousFrame == id) + g.ActiveIdPreviousFrameIsAlive = true; +} + +void ImGui::MarkItemEdited(ImGuiID id) +{ + // This marking is solely to be able to provide info for IsItemDeactivatedAfterEdit(). + // ActiveId might have been released by the time we call this (as in the typical press/release button behavior) but still need need to fill the data. + ImGuiContext& g = *GImGui; + IM_ASSERT(g.ActiveId == id || g.ActiveId == 0 || g.DragDropActive); + IM_UNUSED(id); // Avoid unused variable warnings when asserts are compiled out. + //IM_ASSERT(g.CurrentWindow->DC.LastItemId == id); + g.ActiveIdHasBeenEditedThisFrame = true; + g.ActiveIdHasBeenEditedBefore = true; + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited; +} + +static inline bool IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags) +{ + // An active popup disable hovering on other windows (apart from its own children) + // FIXME-OPT: This could be cached/stored within the window. + ImGuiContext& g = *GImGui; + if (g.NavWindow) + if (ImGuiWindow* focused_root_window = g.NavWindow->RootWindow) + if (focused_root_window->WasActive && focused_root_window != window->RootWindow) + { + // For the purpose of those flags we differentiate "standard popup" from "modal popup" + // NB: The order of those two tests is important because Modal windows are also Popups. + if (focused_root_window->Flags & ImGuiWindowFlags_Modal) + return false; + if ((focused_root_window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + return false; + } + return true; +} + +// This is roughly matching the behavior of internal-facing ItemHoverable() +// - we allow hovering to be true when ActiveId==window->MoveID, so that clicking on non-interactive items such as a Text() item still returns true with IsItemHovered() +// - this should work even for non-interactive items that have no ID, so we cannot use LastItemId +bool ImGui::IsItemHovered(ImGuiHoveredFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.NavDisableMouseHover && !g.NavDisableHighlight) + { + if ((g.LastItemData.InFlags & ImGuiItemFlags_Disabled) && !(flags & ImGuiHoveredFlags_AllowWhenDisabled)) + return false; + if (!IsItemFocused()) + return false; + } + else + { + // Test for bounding box overlap, as updated as ItemAdd() + ImGuiItemStatusFlags status_flags = g.LastItemData.StatusFlags; + if (!(status_flags & ImGuiItemStatusFlags_HoveredRect)) + return false; + IM_ASSERT((flags & (ImGuiHoveredFlags_AnyWindow | ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_NoPopupHierarchy)) == 0); // Flags not supported by this function + + // Test if we are hovering the right window (our window could be behind another window) + // [2021/03/02] Reworked / reverted the revert, finally. Note we want e.g. BeginGroup/ItemAdd/EndGroup to work as well. (#3851) + // [2017/10/16] Reverted commit 344d48be3 and testing RootWindow instead. I believe it is correct to NOT test for RootWindow but this leaves us unable + // to use IsItemHovered() after EndChild() itself. Until a solution is found I believe reverting to the test from 2017/09/27 is safe since this was + // the test that has been running for a long while. + if (g.HoveredWindow != window && (status_flags & ImGuiItemStatusFlags_HoveredWindow) == 0) + if ((flags & ImGuiHoveredFlags_AllowWhenOverlapped) == 0) + return false; + + // Test if another item is active (e.g. being dragged) + if ((flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) == 0) + if (g.ActiveId != 0 && g.ActiveId != g.LastItemData.ID && !g.ActiveIdAllowOverlap && g.ActiveId != window->MoveId) + return false; + + // Test if interactions on this window are blocked by an active popup or modal. + // The ImGuiHoveredFlags_AllowWhenBlockedByPopup flag will be tested here. + if (!IsWindowContentHoverable(window, flags)) + return false; + + // Test if the item is disabled + if ((g.LastItemData.InFlags & ImGuiItemFlags_Disabled) && !(flags & ImGuiHoveredFlags_AllowWhenDisabled)) + return false; + + // Special handling for calling after Begin() which represent the title bar or tab. + // When the window is collapsed (SkipItems==true) that last item will never be overwritten so we need to detect the case. + if (g.LastItemData.ID == window->MoveId && window->WriteAccessed) + return false; + } + + return true; +} + +// Internal facing ItemHoverable() used when submitting widgets. Differs slightly from IsItemHovered(). +bool ImGui::ItemHoverable(const ImRect& bb, ImGuiID id) +{ + ImGuiContext& g = *GImGui; + if (g.HoveredId != 0 && g.HoveredId != id && !g.HoveredIdAllowOverlap) + return false; + + ImGuiWindow* window = g.CurrentWindow; + if (g.HoveredWindow != window) + return false; + if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap) + return false; + if (!IsMouseHoveringRect(bb.Min, bb.Max)) + return false; + if (g.NavDisableMouseHover) + return false; + if (!IsWindowContentHoverable(window, ImGuiHoveredFlags_None)) + { + g.HoveredIdDisabled = true; + return false; + } + + // We exceptionally allow this function to be called with id==0 to allow using it for easy high-level + // hover test in widgets code. We could also decide to split this function is two. + if (id != 0) + SetHoveredID(id); + + // When disabled we'll return false but still set HoveredId + ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.InFlags : g.CurrentItemFlags); + if (item_flags & ImGuiItemFlags_Disabled) + { + // Release active id if turning disabled + if (g.ActiveId == id) + ClearActiveID(); + g.HoveredIdDisabled = true; + return false; + } + + if (id != 0) + { + // [DEBUG] Item Picker tool! + // We perform the check here because SetHoveredID() is not frequently called (1~ time a frame), making + // the cost of this tool near-zero. We can get slightly better call-stack and support picking non-hovered + // items if we perform the test in ItemAdd(), but that would incur a small runtime cost. + // #define IMGUI_DEBUG_TOOL_ITEM_PICKER_EX in imconfig.h if you want this check to also be performed in ItemAdd(). + if (g.DebugItemPickerActive && g.HoveredIdPreviousFrame == id) + GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(255, 255, 0, 255)); + if (g.DebugItemPickerBreakId == id) + IM_DEBUG_BREAK(); + } + + return true; +} + +bool ImGui::IsClippedEx(const ImRect& bb, ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!bb.Overlaps(window->ClipRect)) + if (id == 0 || (id != g.ActiveId && id != g.NavId)) + if (!g.LogEnabled) + return true; + return false; +} + +// This is also inlined in ItemAdd() +// Note: if ImGuiItemStatusFlags_HasDisplayRect is set, user needs to set window->DC.LastItemDisplayRect! +void ImGui::SetLastItemData(ImGuiID item_id, ImGuiItemFlags in_flags, ImGuiItemStatusFlags item_flags, const ImRect& item_rect) +{ + ImGuiContext& g = *GImGui; + g.LastItemData.ID = item_id; + g.LastItemData.InFlags = in_flags; + g.LastItemData.StatusFlags = item_flags; + g.LastItemData.Rect = item_rect; +} + +float ImGui::CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x) +{ + if (wrap_pos_x < 0.0f) + return 0.0f; + + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (wrap_pos_x == 0.0f) + { + // We could decide to setup a default wrapping max point for auto-resizing windows, + // or have auto-wrap (with unspecified wrapping pos) behave as a ContentSize extending function? + //if (window->Hidden && (window->Flags & ImGuiWindowFlags_AlwaysAutoResize)) + // wrap_pos_x = ImMax(window->WorkRect.Min.x + g.FontSize * 10.0f, window->WorkRect.Max.x); + //else + wrap_pos_x = window->WorkRect.Max.x; + } + else if (wrap_pos_x > 0.0f) + { + wrap_pos_x += window->Pos.x - window->Scroll.x; // wrap_pos_x is provided is window local space + } + + return ImMax(wrap_pos_x - pos.x, 1.0f); +} + +// IM_ALLOC() == ImGui::MemAlloc() +void* ImGui::MemAlloc(size_t size) +{ + if (ImGuiContext* ctx = GImGui) + ctx->IO.MetricsActiveAllocations++; + return (*GImAllocatorAllocFunc)(size, GImAllocatorUserData); +} + +// IM_FREE() == ImGui::MemFree() +void ImGui::MemFree(void* ptr) +{ + if (ptr) + if (ImGuiContext* ctx = GImGui) + ctx->IO.MetricsActiveAllocations--; + return (*GImAllocatorFreeFunc)(ptr, GImAllocatorUserData); +} + +const char* ImGui::GetClipboardText() +{ + ImGuiContext& g = *GImGui; + return g.IO.GetClipboardTextFn ? g.IO.GetClipboardTextFn(g.IO.ClipboardUserData) : ""; +} + +void ImGui::SetClipboardText(const char* text) +{ + ImGuiContext& g = *GImGui; + if (g.IO.SetClipboardTextFn) + g.IO.SetClipboardTextFn(g.IO.ClipboardUserData, text); +} + +const char* ImGui::GetVersion() +{ + return IMGUI_VERSION; +} + +// Internal state access - if you want to share Dear ImGui state between modules (e.g. DLL) or allocate it yourself +// Note that we still point to some static data and members (such as GFontAtlas), so the state instance you end up using will point to the static data within its module +ImGuiContext* ImGui::GetCurrentContext() +{ + return GImGui; +} + +void ImGui::SetCurrentContext(ImGuiContext* ctx) +{ +#ifdef IMGUI_SET_CURRENT_CONTEXT_FUNC + IMGUI_SET_CURRENT_CONTEXT_FUNC(ctx); // For custom thread-based hackery you may want to have control over this. +#else + GImGui = ctx; +#endif +} + +void ImGui::SetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, ImGuiMemFreeFunc free_func, void* user_data) +{ + GImAllocatorAllocFunc = alloc_func; + GImAllocatorFreeFunc = free_func; + GImAllocatorUserData = user_data; +} + +// This is provided to facilitate copying allocators from one static/DLL boundary to another (e.g. retrieve default allocator of your executable address space) +void ImGui::GetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, ImGuiMemFreeFunc* p_free_func, void** p_user_data) +{ + *p_alloc_func = GImAllocatorAllocFunc; + *p_free_func = GImAllocatorFreeFunc; + *p_user_data = GImAllocatorUserData; +} + +ImGuiContext* ImGui::CreateContext(ImFontAtlas* shared_font_atlas) +{ + ImGuiContext* ctx = IM_NEW(ImGuiContext)(shared_font_atlas); + if (GImGui == NULL) + SetCurrentContext(ctx); + Initialize(ctx); + return ctx; +} + +void ImGui::DestroyContext(ImGuiContext* ctx) +{ + if (ctx == NULL) + ctx = GImGui; + Shutdown(ctx); + if (GImGui == ctx) + SetCurrentContext(NULL); + IM_DELETE(ctx); +} + +// No specific ordering/dependency support, will see as needed +ImGuiID ImGui::AddContextHook(ImGuiContext* ctx, const ImGuiContextHook* hook) +{ + ImGuiContext& g = *ctx; + IM_ASSERT(hook->Callback != NULL && hook->HookId == 0 && hook->Type != ImGuiContextHookType_PendingRemoval_); + g.Hooks.push_back(*hook); + g.Hooks.back().HookId = ++g.HookIdNext; + return g.HookIdNext; +} + +// Deferred removal, avoiding issue with changing vector while iterating it +void ImGui::RemoveContextHook(ImGuiContext* ctx, ImGuiID hook_id) +{ + ImGuiContext& g = *ctx; + IM_ASSERT(hook_id != 0); + for (int n = 0; n < g.Hooks.Size; n++) + if (g.Hooks[n].HookId == hook_id) + g.Hooks[n].Type = ImGuiContextHookType_PendingRemoval_; +} + +// Call context hooks (used by e.g. test engine) +// We assume a small number of hooks so all stored in same array +void ImGui::CallContextHooks(ImGuiContext* ctx, ImGuiContextHookType hook_type) +{ + ImGuiContext& g = *ctx; + for (int n = 0; n < g.Hooks.Size; n++) + if (g.Hooks[n].Type == hook_type) + g.Hooks[n].Callback(&g, &g.Hooks[n]); +} + +ImGuiIO& ImGui::GetIO() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); + return GImGui->IO; +} + +// Pass this to your backend rendering function! Valid after Render() and until the next call to NewFrame() +ImDrawData* ImGui::GetDrawData() +{ + ImGuiContext& g = *GImGui; + ImGuiViewportP* viewport = g.Viewports[0]; + return viewport->DrawDataP.Valid ? &viewport->DrawDataP : NULL; +} + +double ImGui::GetTime() +{ + return GImGui->Time; +} + +int ImGui::GetFrameCount() +{ + return GImGui->FrameCount; +} + +static ImDrawList* GetViewportDrawList(ImGuiViewportP* viewport, size_t drawlist_no, const char* drawlist_name) +{ + // Create the draw list on demand, because they are not frequently used for all viewports + ImGuiContext& g = *GImGui; + IM_ASSERT(drawlist_no < IM_ARRAYSIZE(viewport->DrawLists)); + ImDrawList* draw_list = viewport->DrawLists[drawlist_no]; + if (draw_list == NULL) + { + draw_list = IM_NEW(ImDrawList)(&g.DrawListSharedData); + draw_list->_OwnerName = drawlist_name; + viewport->DrawLists[drawlist_no] = draw_list; + } + + // Our ImDrawList system requires that there is always a command + if (viewport->DrawListsLastFrame[drawlist_no] != g.FrameCount) + { + draw_list->_ResetForNewFrame(); + draw_list->PushTextureID(g.IO.Fonts->TexID); + draw_list->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size, false); + viewport->DrawListsLastFrame[drawlist_no] = g.FrameCount; + } + return draw_list; +} + +ImDrawList* ImGui::GetBackgroundDrawList(ImGuiViewport* viewport) +{ + return GetViewportDrawList((ImGuiViewportP*)viewport, 0, "##Background"); +} + +ImDrawList* ImGui::GetBackgroundDrawList() +{ + ImGuiContext& g = *GImGui; + return GetBackgroundDrawList(g.Viewports[0]); +} + +ImDrawList* ImGui::GetForegroundDrawList(ImGuiViewport* viewport) +{ + return GetViewportDrawList((ImGuiViewportP*)viewport, 1, "##Foreground"); +} + +ImDrawList* ImGui::GetForegroundDrawList() +{ + ImGuiContext& g = *GImGui; + return GetForegroundDrawList(g.Viewports[0]); +} + +ImDrawListSharedData* ImGui::GetDrawListSharedData() +{ + return &GImGui->DrawListSharedData; +} + +void ImGui::StartMouseMovingWindow(ImGuiWindow* window) +{ + // Set ActiveId even if the _NoMove flag is set. Without it, dragging away from a window with _NoMove would activate hover on other windows. + // We _also_ call this when clicking in a window empty space when io.ConfigWindowsMoveFromTitleBarOnly is set, but clear g.MovingWindow afterward. + // This is because we want ActiveId to be set even when the window is not permitted to move. + ImGuiContext& g = *GImGui; + FocusWindow(window); + SetActiveID(window->MoveId, window); + g.NavDisableHighlight = true; + g.ActiveIdClickOffset = g.IO.MouseClickedPos[0] - window->RootWindow->Pos; + g.ActiveIdNoClearOnFocusLoss = true; + SetActiveIdUsingNavAndKeys(); + + bool can_move_window = true; + if ((window->Flags & ImGuiWindowFlags_NoMove) || (window->RootWindow->Flags & ImGuiWindowFlags_NoMove)) + can_move_window = false; + if (can_move_window) + g.MovingWindow = window; +} + +// Handle mouse moving window +// Note: moving window with the navigation keys (Square + d-pad / CTRL+TAB + Arrows) are processed in NavUpdateWindowing() +// FIXME: We don't have strong guarantee that g.MovingWindow stay synched with g.ActiveId == g.MovingWindow->MoveId. +// This is currently enforced by the fact that BeginDragDropSource() is setting all g.ActiveIdUsingXXXX flags to inhibit navigation inputs, +// but if we should more thoroughly test cases where g.ActiveId or g.MovingWindow gets changed and not the other. +void ImGui::UpdateMouseMovingWindowNewFrame() +{ + ImGuiContext& g = *GImGui; + if (g.MovingWindow != NULL) + { + // We actually want to move the root window. g.MovingWindow == window we clicked on (could be a child window). + // We track it to preserve Focus and so that generally ActiveIdWindow == MovingWindow and ActiveId == MovingWindow->MoveId for consistency. + KeepAliveID(g.ActiveId); + IM_ASSERT(g.MovingWindow && g.MovingWindow->RootWindow); + ImGuiWindow* moving_window = g.MovingWindow->RootWindow; + if (g.IO.MouseDown[0] && IsMousePosValid(&g.IO.MousePos)) + { + ImVec2 pos = g.IO.MousePos - g.ActiveIdClickOffset; + if (moving_window->Pos.x != pos.x || moving_window->Pos.y != pos.y) + { + MarkIniSettingsDirty(moving_window); + SetWindowPos(moving_window, pos, ImGuiCond_Always); + } + FocusWindow(g.MovingWindow); + } + else + { + g.MovingWindow = NULL; + ClearActiveID(); + } + } + else + { + // When clicking/dragging from a window that has the _NoMove flag, we still set the ActiveId in order to prevent hovering others. + if (g.ActiveIdWindow && g.ActiveIdWindow->MoveId == g.ActiveId) + { + KeepAliveID(g.ActiveId); + if (!g.IO.MouseDown[0]) + ClearActiveID(); + } + } +} + +// Initiate moving window when clicking on empty space or title bar. +// Handle left-click and right-click focus. +void ImGui::UpdateMouseMovingWindowEndFrame() +{ + ImGuiContext& g = *GImGui; + if (g.ActiveId != 0 || g.HoveredId != 0) + return; + + // Unless we just made a window/popup appear + if (g.NavWindow && g.NavWindow->Appearing) + return; + + // Click on empty space to focus window and start moving + // (after we're done with all our widgets) + if (g.IO.MouseClicked[0]) + { + // Handle the edge case of a popup being closed while clicking in its empty space. + // If we try to focus it, FocusWindow() > ClosePopupsOverWindow() will accidentally close any parent popups because they are not linked together any more. + ImGuiWindow* root_window = g.HoveredWindow ? g.HoveredWindow->RootWindow : NULL; + const bool is_closed_popup = root_window && (root_window->Flags & ImGuiWindowFlags_Popup) && !IsPopupOpen(root_window->PopupId, ImGuiPopupFlags_AnyPopupLevel); + + if (root_window != NULL && !is_closed_popup) + { + StartMouseMovingWindow(g.HoveredWindow); //-V595 + + // Cancel moving if clicked outside of title bar + if (g.IO.ConfigWindowsMoveFromTitleBarOnly && !(root_window->Flags & ImGuiWindowFlags_NoTitleBar)) + if (!root_window->TitleBarRect().Contains(g.IO.MouseClickedPos[0])) + g.MovingWindow = NULL; + + // Cancel moving if clicked over an item which was disabled or inhibited by popups (note that we know HoveredId == 0 already) + if (g.HoveredIdDisabled) + g.MovingWindow = NULL; + } + else if (root_window == NULL && g.NavWindow != NULL && GetTopMostPopupModal() == NULL) + { + // Clicking on void disable focus + FocusWindow(NULL); + } + } + + // With right mouse button we close popups without changing focus based on where the mouse is aimed + // Instead, focus will be restored to the window under the bottom-most closed popup. + // (The left mouse button path calls FocusWindow on the hovered window, which will lead NewFrame->ClosePopupsOverWindow to trigger) + if (g.IO.MouseClicked[1]) + { + // Find the top-most window between HoveredWindow and the top-most Modal Window. + // This is where we can trim the popup stack. + ImGuiWindow* modal = GetTopMostPopupModal(); + bool hovered_window_above_modal = g.HoveredWindow && (modal == NULL || IsWindowAbove(g.HoveredWindow, modal)); + ClosePopupsOverWindow(hovered_window_above_modal ? g.HoveredWindow : modal, true); + } +} + +static bool IsWindowActiveAndVisible(ImGuiWindow* window) +{ + return (window->Active) && (!window->Hidden); +} + +static void ImGui::UpdateKeyboardInputs() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + // Synchronize io.KeyMods with individual modifiers io.KeyXXX bools + io.KeyMods = GetMergedKeyModFlags(); + + // Import legacy keys or verify they are not used +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + if (io.BackendUsingLegacyKeyArrays == 0) + { + // Backend used new io.AddKeyEvent() API: Good! Verify that old arrays are never written too. + for (int n = 0; n < IM_ARRAYSIZE(io.KeysDown); n++) + IM_ASSERT(io.KeysDown[n] == false && "Backend needs to either only use io.AddKeyEvent(), either only fill legacy io.KeysDown[] + io.KeyMap[]. Not both!"); + } + else + { + if (g.FrameCount == 0) + for (int n = ImGuiKey_LegacyNativeKey_BEGIN; n < ImGuiKey_LegacyNativeKey_END; n++) + IM_ASSERT(g.IO.KeyMap[n] == -1 && "Backend is not allowed to write to io.KeyMap[0..511]!"); + + // Build reverse KeyMap (Named -> Legacy) + for (int n = ImGuiKey_NamedKey_BEGIN; n < ImGuiKey_NamedKey_END; n++) + if (io.KeyMap[n] != -1) + { + IM_ASSERT(IsLegacyKey((ImGuiKey)io.KeyMap[n])); + io.KeyMap[io.KeyMap[n]] = n; + } + + // Import legacy keys into new ones + for (int n = ImGuiKey_LegacyNativeKey_BEGIN; n < ImGuiKey_LegacyNativeKey_END; n++) + if (io.KeysDown[n] || io.BackendUsingLegacyKeyArrays == 1) + { + const ImGuiKey key = (ImGuiKey)(io.KeyMap[n] != -1 ? io.KeyMap[n] : n); + IM_ASSERT(io.KeyMap[n] == -1 || IsNamedKey(key)); + io.KeysData[key].Down = io.KeysDown[n]; + io.BackendUsingLegacyKeyArrays = 1; + } + if (io.BackendUsingLegacyKeyArrays == 1) + { + io.KeysData[ImGuiKey_ModCtrl].Down = io.KeyCtrl; + io.KeysData[ImGuiKey_ModShift].Down = io.KeyShift; + io.KeysData[ImGuiKey_ModAlt].Down = io.KeyAlt; + io.KeysData[ImGuiKey_ModSuper].Down = io.KeySuper; + } + } +#endif + + // Clear gamepad data if disabled + if ((io.BackendFlags & ImGuiBackendFlags_HasGamepad) == 0) + for (int i = ImGuiKey_Gamepad_BEGIN; i < ImGuiKey_Gamepad_END; i++) + { + io.KeysData[i - ImGuiKey_KeysData_OFFSET].Down = false; + io.KeysData[i - ImGuiKey_KeysData_OFFSET].AnalogValue = 0.0f; + } + + // Update keys + for (int i = 0; i < IM_ARRAYSIZE(io.KeysData); i++) + { + ImGuiKeyData& key_data = io.KeysData[i]; + key_data.DownDurationPrev = key_data.DownDuration; + key_data.DownDuration = key_data.Down ? (key_data.DownDuration < 0.0f ? 0.0f : key_data.DownDuration + io.DeltaTime) : -1.0f; + } +} + +static void ImGui::UpdateMouseInputs() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + // Round mouse position to avoid spreading non-rounded position (e.g. UpdateManualResize doesn't support them well) + if (IsMousePosValid(&io.MousePos)) + io.MousePos = g.MouseLastValidPos = ImFloorSigned(io.MousePos); + + // If mouse just appeared or disappeared (usually denoted by -FLT_MAX components) we cancel out movement in MouseDelta + if (IsMousePosValid(&io.MousePos) && IsMousePosValid(&io.MousePosPrev)) + io.MouseDelta = io.MousePos - io.MousePosPrev; + else + io.MouseDelta = ImVec2(0.0f, 0.0f); + + // If mouse moved we re-enable mouse hovering in case it was disabled by gamepad/keyboard. In theory should use a >0.0f threshold but would need to reset in everywhere we set this to true. + if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) + g.NavDisableMouseHover = false; + + io.MousePosPrev = io.MousePos; + for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) + { + io.MouseClicked[i] = io.MouseDown[i] && io.MouseDownDuration[i] < 0.0f; + io.MouseClickedCount[i] = 0; // Will be filled below + io.MouseReleased[i] = !io.MouseDown[i] && io.MouseDownDuration[i] >= 0.0f; + io.MouseDownDurationPrev[i] = io.MouseDownDuration[i]; + io.MouseDownDuration[i] = io.MouseDown[i] ? (io.MouseDownDuration[i] < 0.0f ? 0.0f : io.MouseDownDuration[i] + io.DeltaTime) : -1.0f; + if (io.MouseClicked[i]) + { + bool is_repeated_click = false; + if ((float)(g.Time - io.MouseClickedTime[i]) < io.MouseDoubleClickTime) + { + ImVec2 delta_from_click_pos = IsMousePosValid(&io.MousePos) ? (io.MousePos - io.MouseClickedPos[i]) : ImVec2(0.0f, 0.0f); + if (ImLengthSqr(delta_from_click_pos) < io.MouseDoubleClickMaxDist * io.MouseDoubleClickMaxDist) + is_repeated_click = true; + } + if (is_repeated_click) + io.MouseClickedLastCount[i]++; + else + io.MouseClickedLastCount[i] = 1; + io.MouseClickedTime[i] = g.Time; + io.MouseClickedPos[i] = io.MousePos; + io.MouseClickedCount[i] = io.MouseClickedLastCount[i]; + io.MouseDragMaxDistanceSqr[i] = 0.0f; + } + else if (io.MouseDown[i]) + { + // Maintain the maximum distance we reaching from the initial click position, which is used with dragging threshold + float delta_sqr_click_pos = IsMousePosValid(&io.MousePos) ? ImLengthSqr(io.MousePos - io.MouseClickedPos[i]) : 0.0f; + io.MouseDragMaxDistanceSqr[i] = ImMax(io.MouseDragMaxDistanceSqr[i], delta_sqr_click_pos); + } + + // We provide io.MouseDoubleClicked[] as a legacy service + io.MouseDoubleClicked[i] = (io.MouseClickedCount[i] == 2); + + // Clicking any mouse button reactivate mouse hovering which may have been deactivated by gamepad/keyboard navigation + if (io.MouseClicked[i]) + g.NavDisableMouseHover = false; + } +} + +static void StartLockWheelingWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.WheelingWindow == window) + return; + g.WheelingWindow = window; + g.WheelingWindowRefMousePos = g.IO.MousePos; + g.WheelingWindowTimer = WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER; +} + +void ImGui::UpdateMouseWheel() +{ + ImGuiContext& g = *GImGui; + + // Reset the locked window if we move the mouse or after the timer elapses + if (g.WheelingWindow != NULL) + { + g.WheelingWindowTimer -= g.IO.DeltaTime; + if (IsMousePosValid() && ImLengthSqr(g.IO.MousePos - g.WheelingWindowRefMousePos) > g.IO.MouseDragThreshold * g.IO.MouseDragThreshold) + g.WheelingWindowTimer = 0.0f; + if (g.WheelingWindowTimer <= 0.0f) + { + g.WheelingWindow = NULL; + g.WheelingWindowTimer = 0.0f; + } + } + + if (g.IO.MouseWheel == 0.0f && g.IO.MouseWheelH == 0.0f) + return; + + if ((g.ActiveId != 0 && g.ActiveIdUsingMouseWheel) || (g.HoveredIdPreviousFrame != 0 && g.HoveredIdPreviousFrameUsingMouseWheel)) + return; + + ImGuiWindow* window = g.WheelingWindow ? g.WheelingWindow : g.HoveredWindow; + if (!window || window->Collapsed) + return; + + // Zoom / Scale window + // FIXME-OBSOLETE: This is an old feature, it still works but pretty much nobody is using it and may be best redesigned. + if (g.IO.MouseWheel != 0.0f && g.IO.KeyCtrl && g.IO.FontAllowUserScaling) + { + StartLockWheelingWindow(window); + const float new_font_scale = ImClamp(window->FontWindowScale + g.IO.MouseWheel * 0.10f, 0.50f, 2.50f); + const float scale = new_font_scale / window->FontWindowScale; + window->FontWindowScale = new_font_scale; + if (window == window->RootWindow) + { + const ImVec2 offset = window->Size * (1.0f - scale) * (g.IO.MousePos - window->Pos) / window->Size; + SetWindowPos(window, window->Pos + offset, 0); + window->Size = ImFloor(window->Size * scale); + window->SizeFull = ImFloor(window->SizeFull * scale); + } + return; + } + + // Mouse wheel scrolling + // If a child window has the ImGuiWindowFlags_NoScrollWithMouse flag, we give a chance to scroll its parent + if (g.IO.KeyCtrl) + return; + + // As a standard behavior holding SHIFT while using Vertical Mouse Wheel triggers Horizontal scroll instead + // (we avoid doing it on OSX as it the OS input layer handles this already) + const bool swap_axis = g.IO.KeyShift && !g.IO.ConfigMacOSXBehaviors; + const float wheel_y = swap_axis ? 0.0f : g.IO.MouseWheel; + const float wheel_x = swap_axis ? g.IO.MouseWheel : g.IO.MouseWheelH; + + // Vertical Mouse Wheel scrolling + if (wheel_y != 0.0f) + { + StartLockWheelingWindow(window); + while ((window->Flags & ImGuiWindowFlags_ChildWindow) && ((window->ScrollMax.y == 0.0f) || ((window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs)))) + window = window->ParentWindow; + if (!(window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs)) + { + float max_step = window->InnerRect.GetHeight() * 0.67f; + float scroll_step = ImFloor(ImMin(5 * window->CalcFontSize(), max_step)); + SetScrollY(window, window->Scroll.y - wheel_y * scroll_step); + } + } + + // Horizontal Mouse Wheel scrolling, or Vertical Mouse Wheel w/ Shift held + if (wheel_x != 0.0f) + { + StartLockWheelingWindow(window); + while ((window->Flags & ImGuiWindowFlags_ChildWindow) && ((window->ScrollMax.x == 0.0f) || ((window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs)))) + window = window->ParentWindow; + if (!(window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs)) + { + float max_step = window->InnerRect.GetWidth() * 0.67f; + float scroll_step = ImFloor(ImMin(2 * window->CalcFontSize(), max_step)); + SetScrollX(window, window->Scroll.x - wheel_x * scroll_step); + } + } +} + +// The reason this is exposed in imgui_internal.h is: on touch-based system that don't have hovering, we want to dispatch inputs to the right target (imgui vs imgui+app) +void ImGui::UpdateHoveredWindowAndCaptureFlags() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + g.WindowsHoverPadding = ImMax(g.Style.TouchExtraPadding, ImVec2(WINDOWS_HOVER_PADDING, WINDOWS_HOVER_PADDING)); + + // Find the window hovered by mouse: + // - Child windows can extend beyond the limit of their parent so we need to derive HoveredRootWindow from HoveredWindow. + // - When moving a window we can skip the search, which also conveniently bypasses the fact that window->WindowRectClipped is lagging as this point of the frame. + // - We also support the moved window toggling the NoInputs flag after moving has started in order to be able to detect windows below it, which is useful for e.g. docking mechanisms. + bool clear_hovered_windows = false; + FindHoveredWindow(); + + // Modal windows prevents mouse from hovering behind them. + ImGuiWindow* modal_window = GetTopMostPopupModal(); + if (modal_window && g.HoveredWindow && !IsWindowWithinBeginStackOf(g.HoveredWindow->RootWindow, modal_window)) + clear_hovered_windows = true; + + // Disabled mouse? + if (io.ConfigFlags & ImGuiConfigFlags_NoMouse) + clear_hovered_windows = true; + + // We track click ownership. When clicked outside of a window the click is owned by the application and + // won't report hovering nor request capture even while dragging over our windows afterward. + const bool has_open_popup = (g.OpenPopupStack.Size > 0); + const bool has_open_modal = (modal_window != NULL); + int mouse_earliest_down = -1; + bool mouse_any_down = false; + for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) + { + if (io.MouseClicked[i]) + { + io.MouseDownOwned[i] = (g.HoveredWindow != NULL) || has_open_popup; + io.MouseDownOwnedUnlessPopupClose[i] = (g.HoveredWindow != NULL) || has_open_modal; + } + mouse_any_down |= io.MouseDown[i]; + if (io.MouseDown[i]) + if (mouse_earliest_down == -1 || io.MouseClickedTime[i] < io.MouseClickedTime[mouse_earliest_down]) + mouse_earliest_down = i; + } + const bool mouse_avail = (mouse_earliest_down == -1) || io.MouseDownOwned[mouse_earliest_down]; + const bool mouse_avail_unless_popup_close = (mouse_earliest_down == -1) || io.MouseDownOwnedUnlessPopupClose[mouse_earliest_down]; + + // If mouse was first clicked outside of ImGui bounds we also cancel out hovering. + // FIXME: For patterns of drag and drop across OS windows, we may need to rework/remove this test (first committed 311c0ca9 on 2015/02) + const bool mouse_dragging_extern_payload = g.DragDropActive && (g.DragDropSourceFlags & ImGuiDragDropFlags_SourceExtern) != 0; + if (!mouse_avail && !mouse_dragging_extern_payload) + clear_hovered_windows = true; + + if (clear_hovered_windows) + g.HoveredWindow = g.HoveredWindowUnderMovingWindow = NULL; + + // Update io.WantCaptureMouse for the user application (true = dispatch mouse info to Dear ImGui only, false = dispatch mouse to Dear ImGui + underlying app) + // Update io.WantCaptureMouseAllowPopupClose (experimental) to give a chance for app to react to popup closure with a drag + if (g.WantCaptureMouseNextFrame != -1) + { + io.WantCaptureMouse = io.WantCaptureMouseUnlessPopupClose = (g.WantCaptureMouseNextFrame != 0); + } + else + { + io.WantCaptureMouse = (mouse_avail && (g.HoveredWindow != NULL || mouse_any_down)) || has_open_popup; + io.WantCaptureMouseUnlessPopupClose = (mouse_avail_unless_popup_close && (g.HoveredWindow != NULL || mouse_any_down)) || has_open_modal; + } + + // Update io.WantCaptureKeyboard for the user application (true = dispatch keyboard info to Dear ImGui only, false = dispatch keyboard info to Dear ImGui + underlying app) + if (g.WantCaptureKeyboardNextFrame != -1) + io.WantCaptureKeyboard = (g.WantCaptureKeyboardNextFrame != 0); + else + io.WantCaptureKeyboard = (g.ActiveId != 0) || (modal_window != NULL); + if (io.NavActive && (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) && !(io.ConfigFlags & ImGuiConfigFlags_NavNoCaptureKeyboard)) + io.WantCaptureKeyboard = true; + + // Update io.WantTextInput flag, this is to allow systems without a keyboard (e.g. mobile, hand-held) to show a software keyboard if possible + io.WantTextInput = (g.WantTextInputNextFrame != -1) ? (g.WantTextInputNextFrame != 0) : false; +} + +ImGuiKeyModFlags ImGui::GetMergedKeyModFlags() +{ + ImGuiContext& g = *GImGui; + ImGuiKeyModFlags key_mod_flags = ImGuiKeyModFlags_None; + if (g.IO.KeyCtrl) { key_mod_flags |= ImGuiKeyModFlags_Ctrl; } + if (g.IO.KeyShift) { key_mod_flags |= ImGuiKeyModFlags_Shift; } + if (g.IO.KeyAlt) { key_mod_flags |= ImGuiKeyModFlags_Alt; } + if (g.IO.KeySuper) { key_mod_flags |= ImGuiKeyModFlags_Super; } + return key_mod_flags; +} + +void ImGui::NewFrame() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); + ImGuiContext& g = *GImGui; + + // Remove pending delete hooks before frame start. + // This deferred removal avoid issues of removal while iterating the hook vector + for (int n = g.Hooks.Size - 1; n >= 0; n--) + if (g.Hooks[n].Type == ImGuiContextHookType_PendingRemoval_) + g.Hooks.erase(&g.Hooks[n]); + + CallContextHooks(&g, ImGuiContextHookType_NewFramePre); + + // Check and assert for various common IO and Configuration mistakes + ErrorCheckNewFrameSanityChecks(); + + // Load settings on first frame, save settings when modified (after a delay) + UpdateSettings(); + + g.Time += g.IO.DeltaTime; + g.WithinFrameScope = true; + g.FrameCount += 1; + g.TooltipOverrideCount = 0; + g.WindowsActiveCount = 0; + g.MenusIdSubmittedThisFrame.resize(0); + + // Calculate frame-rate for the user, as a purely luxurious feature + g.FramerateSecPerFrameAccum += g.IO.DeltaTime - g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx]; + g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx] = g.IO.DeltaTime; + g.FramerateSecPerFrameIdx = (g.FramerateSecPerFrameIdx + 1) % IM_ARRAYSIZE(g.FramerateSecPerFrame); + g.FramerateSecPerFrameCount = ImMin(g.FramerateSecPerFrameCount + 1, IM_ARRAYSIZE(g.FramerateSecPerFrame)); + g.IO.Framerate = (g.FramerateSecPerFrameAccum > 0.0f) ? (1.0f / (g.FramerateSecPerFrameAccum / (float)g.FramerateSecPerFrameCount)) : FLT_MAX; + + UpdateViewportsNewFrame(); + + // Setup current font and draw list shared data + g.IO.Fonts->Locked = true; + SetCurrentFont(GetDefaultFont()); + IM_ASSERT(g.Font->IsLoaded()); + ImRect virtual_space(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + for (int n = 0; n < g.Viewports.Size; n++) + virtual_space.Add(g.Viewports[n]->GetMainRect()); + g.DrawListSharedData.ClipRectFullscreen = virtual_space.ToVec4(); + g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol; + g.DrawListSharedData.SetCircleTessellationMaxError(g.Style.CircleTessellationMaxError); + g.DrawListSharedData.InitialFlags = ImDrawListFlags_None; + if (g.Style.AntiAliasedLines) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLines; + if (g.Style.AntiAliasedLinesUseTex && !(g.Font->ContainerAtlas->Flags & ImFontAtlasFlags_NoBakedLines)) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLinesUseTex; + if (g.Style.AntiAliasedFill) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedFill; + if (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AllowVtxOffset; + + // Mark rendering data as invalid to prevent user who may have a handle on it to use it. + for (int n = 0; n < g.Viewports.Size; n++) + { + ImGuiViewportP* viewport = g.Viewports[n]; + viewport->DrawDataP.Clear(); + } + + // Drag and drop keep the source ID alive so even if the source disappear our state is consistent + if (g.DragDropActive && g.DragDropPayload.SourceId == g.ActiveId) + KeepAliveID(g.DragDropPayload.SourceId); + + // Update HoveredId data + if (!g.HoveredIdPreviousFrame) + g.HoveredIdTimer = 0.0f; + if (!g.HoveredIdPreviousFrame || (g.HoveredId && g.ActiveId == g.HoveredId)) + g.HoveredIdNotActiveTimer = 0.0f; + if (g.HoveredId) + g.HoveredIdTimer += g.IO.DeltaTime; + if (g.HoveredId && g.ActiveId != g.HoveredId) + g.HoveredIdNotActiveTimer += g.IO.DeltaTime; + g.HoveredIdPreviousFrame = g.HoveredId; + g.HoveredIdPreviousFrameUsingMouseWheel = g.HoveredIdUsingMouseWheel; + g.HoveredId = 0; + g.HoveredIdAllowOverlap = false; + g.HoveredIdUsingMouseWheel = false; + g.HoveredIdDisabled = false; + + // Update ActiveId data (clear reference to active widget if the widget isn't alive anymore) + if (g.ActiveIdIsAlive != g.ActiveId && g.ActiveIdPreviousFrame == g.ActiveId && g.ActiveId != 0) + ClearActiveID(); + if (g.ActiveId) + g.ActiveIdTimer += g.IO.DeltaTime; + g.LastActiveIdTimer += g.IO.DeltaTime; + g.ActiveIdPreviousFrame = g.ActiveId; + g.ActiveIdPreviousFrameWindow = g.ActiveIdWindow; + g.ActiveIdPreviousFrameHasBeenEditedBefore = g.ActiveIdHasBeenEditedBefore; + g.ActiveIdIsAlive = 0; + g.ActiveIdHasBeenEditedThisFrame = false; + g.ActiveIdPreviousFrameIsAlive = false; + g.ActiveIdIsJustActivated = false; + if (g.TempInputId != 0 && g.ActiveId != g.TempInputId) + g.TempInputId = 0; + if (g.ActiveId == 0) + { + g.ActiveIdUsingNavDirMask = 0x00; + g.ActiveIdUsingNavInputMask = 0x00; + g.ActiveIdUsingKeyInputMask.ClearAllBits(); + } + + // Drag and drop + g.DragDropAcceptIdPrev = g.DragDropAcceptIdCurr; + g.DragDropAcceptIdCurr = 0; + g.DragDropAcceptIdCurrRectSurface = FLT_MAX; + g.DragDropWithinSource = false; + g.DragDropWithinTarget = false; + g.DragDropHoldJustPressedId = 0; + + // Close popups on focus lost (currently wip/opt-in) + //if (g.IO.AppFocusLost) + // ClosePopupsExceptModals(); + + // Process input queue (trickle as many events as possible) + g.InputEventsTrail.resize(0); + UpdateInputEvents(g.IO.ConfigInputTrickleEventQueue); + + // Update keyboard input state + UpdateKeyboardInputs(); + + //IM_ASSERT(g.IO.KeyCtrl == IsKeyDown(ImGuiKey_LeftCtrl) || IsKeyDown(ImGuiKey_RightCtrl)); + //IM_ASSERT(g.IO.KeyShift == IsKeyDown(ImGuiKey_LeftShift) || IsKeyDown(ImGuiKey_RightShift)); + //IM_ASSERT(g.IO.KeyAlt == IsKeyDown(ImGuiKey_LeftAlt) || IsKeyDown(ImGuiKey_RightAlt)); + //IM_ASSERT(g.IO.KeySuper == IsKeyDown(ImGuiKey_LeftSuper) || IsKeyDown(ImGuiKey_RightSuper)); + + // Update gamepad/keyboard navigation + NavUpdate(); + + // Update mouse input state + UpdateMouseInputs(); + + // Find hovered window + // (needs to be before UpdateMouseMovingWindowNewFrame so we fill g.HoveredWindowUnderMovingWindow on the mouse release frame) + UpdateHoveredWindowAndCaptureFlags(); + + // Handle user moving window with mouse (at the beginning of the frame to avoid input lag or sheering) + UpdateMouseMovingWindowNewFrame(); + + // Background darkening/whitening + if (GetTopMostPopupModal() != NULL || (g.NavWindowingTarget != NULL && g.NavWindowingHighlightAlpha > 0.0f)) + g.DimBgRatio = ImMin(g.DimBgRatio + g.IO.DeltaTime * 6.0f, 1.0f); + else + g.DimBgRatio = ImMax(g.DimBgRatio - g.IO.DeltaTime * 10.0f, 0.0f); + + g.MouseCursor = ImGuiMouseCursor_Arrow; + g.WantCaptureMouseNextFrame = g.WantCaptureKeyboardNextFrame = g.WantTextInputNextFrame = -1; + + // Platform IME data: reset for the frame + g.PlatformImeDataPrev = g.PlatformImeData; + g.PlatformImeData.WantVisible = false; + + // Mouse wheel scrolling, scale + UpdateMouseWheel(); + + // Mark all windows as not visible and compact unused memory. + IM_ASSERT(g.WindowsFocusOrder.Size <= g.Windows.Size); + const float memory_compact_start_time = (g.GcCompactAll || g.IO.ConfigMemoryCompactTimer < 0.0f) ? FLT_MAX : (float)g.Time - g.IO.ConfigMemoryCompactTimer; + for (int i = 0; i != g.Windows.Size; i++) + { + ImGuiWindow* window = g.Windows[i]; + window->WasActive = window->Active; + window->BeginCount = 0; + window->Active = false; + window->WriteAccessed = false; + + // Garbage collect transient buffers of recently unused windows + if (!window->WasActive && !window->MemoryCompacted && window->LastTimeActive < memory_compact_start_time) + GcCompactTransientWindowBuffers(window); + } + + // Garbage collect transient buffers of recently unused tables + for (int i = 0; i < g.TablesLastTimeActive.Size; i++) + if (g.TablesLastTimeActive[i] >= 0.0f && g.TablesLastTimeActive[i] < memory_compact_start_time) + TableGcCompactTransientBuffers(g.Tables.GetByIndex(i)); + for (int i = 0; i < g.TablesTempData.Size; i++) + if (g.TablesTempData[i].LastTimeActive >= 0.0f && g.TablesTempData[i].LastTimeActive < memory_compact_start_time) + TableGcCompactTransientBuffers(&g.TablesTempData[i]); + if (g.GcCompactAll) + GcCompactTransientMiscBuffers(); + g.GcCompactAll = false; + + // Closing the focused window restore focus to the first active root window in descending z-order + if (g.NavWindow && !g.NavWindow->WasActive) + FocusTopMostWindowUnderOne(NULL, NULL); + + // No window should be open at the beginning of the frame. + // But in order to allow the user to call NewFrame() multiple times without calling Render(), we are doing an explicit clear. + g.CurrentWindowStack.resize(0); + g.BeginPopupStack.resize(0); + g.ItemFlagsStack.resize(0); + g.ItemFlagsStack.push_back(ImGuiItemFlags_None); + g.GroupStack.resize(0); + + // [DEBUG] Update debug features + UpdateDebugToolItemPicker(); + UpdateDebugToolStackQueries(); + + // Create implicit/fallback window - which we will only render it if the user has added something to it. + // We don't use "Debug" to avoid colliding with user trying to create a "Debug" window with custom flags. + // This fallback is particularly important as it avoid ImGui:: calls from crashing. + g.WithinFrameScopeWithImplicitWindow = true; + SetNextWindowSize(ImVec2(400, 400), ImGuiCond_FirstUseEver); + Begin("Debug##Default"); + IM_ASSERT(g.CurrentWindow->IsFallbackWindow == true); + + CallContextHooks(&g, ImGuiContextHookType_NewFramePost); +} + +void ImGui::Initialize(ImGuiContext* context) +{ + ImGuiContext& g = *context; + IM_ASSERT(!g.Initialized && !g.SettingsLoaded); + + // Add .ini handle for ImGuiWindow type + { + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "Window"; + ini_handler.TypeHash = ImHashStr("Window"); + ini_handler.ClearAllFn = WindowSettingsHandler_ClearAll; + ini_handler.ReadOpenFn = WindowSettingsHandler_ReadOpen; + ini_handler.ReadLineFn = WindowSettingsHandler_ReadLine; + ini_handler.ApplyAllFn = WindowSettingsHandler_ApplyAll; + ini_handler.WriteAllFn = WindowSettingsHandler_WriteAll; + g.SettingsHandlers.push_back(ini_handler); + } + + // Add .ini handle for ImGuiTable type + TableSettingsInstallHandler(context); + + // Create default viewport + ImGuiViewportP* viewport = IM_NEW(ImGuiViewportP)(); + g.Viewports.push_back(viewport); + +#ifdef IMGUI_HAS_DOCK +#endif + + g.Initialized = true; +} + +// This function is merely here to free heap allocations. +void ImGui::Shutdown(ImGuiContext* context) +{ + // The fonts atlas can be used prior to calling NewFrame(), so we clear it even if g.Initialized is FALSE (which would happen if we never called NewFrame) + ImGuiContext& g = *context; + if (g.IO.Fonts && g.FontAtlasOwnedByContext) + { + g.IO.Fonts->Locked = false; + IM_DELETE(g.IO.Fonts); + } + g.IO.Fonts = NULL; + + // Cleanup of other data are conditional on actually having initialized Dear ImGui. + if (!g.Initialized) + return; + + // Save settings (unless we haven't attempted to load them: CreateContext/DestroyContext without a call to NewFrame shouldn't save an empty file) + if (g.SettingsLoaded && g.IO.IniFilename != NULL) + { + ImGuiContext* backup_context = GImGui; + SetCurrentContext(&g); + SaveIniSettingsToDisk(g.IO.IniFilename); + SetCurrentContext(backup_context); + } + + CallContextHooks(&g, ImGuiContextHookType_Shutdown); + + // Clear everything else + g.Windows.clear_delete(); + g.WindowsFocusOrder.clear(); + g.WindowsTempSortBuffer.clear(); + g.CurrentWindow = NULL; + g.CurrentWindowStack.clear(); + g.WindowsById.Clear(); + g.NavWindow = NULL; + g.HoveredWindow = g.HoveredWindowUnderMovingWindow = NULL; + g.ActiveIdWindow = g.ActiveIdPreviousFrameWindow = NULL; + g.MovingWindow = NULL; + g.ColorStack.clear(); + g.StyleVarStack.clear(); + g.FontStack.clear(); + g.OpenPopupStack.clear(); + g.BeginPopupStack.clear(); + + g.Viewports.clear_delete(); + + g.TabBars.Clear(); + g.CurrentTabBarStack.clear(); + g.ShrinkWidthBuffer.clear(); + + g.ClipperTempData.clear_destruct(); + + g.Tables.Clear(); + g.TablesTempData.clear_destruct(); + g.DrawChannelsTempMergeBuffer.clear(); + + g.ClipboardHandlerData.clear(); + g.MenusIdSubmittedThisFrame.clear(); + g.InputTextState.ClearFreeMemory(); + + g.SettingsWindows.clear(); + g.SettingsHandlers.clear(); + + if (g.LogFile) + { +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + if (g.LogFile != stdout) +#endif + ImFileClose(g.LogFile); + g.LogFile = NULL; + } + g.LogBuffer.clear(); + + g.Initialized = false; +} + +// FIXME: Add a more explicit sort order in the window structure. +static int IMGUI_CDECL ChildWindowComparer(const void* lhs, const void* rhs) +{ + const ImGuiWindow* const a = *(const ImGuiWindow* const *)lhs; + const ImGuiWindow* const b = *(const ImGuiWindow* const *)rhs; + if (int d = (a->Flags & ImGuiWindowFlags_Popup) - (b->Flags & ImGuiWindowFlags_Popup)) + return d; + if (int d = (a->Flags & ImGuiWindowFlags_Tooltip) - (b->Flags & ImGuiWindowFlags_Tooltip)) + return d; + return (a->BeginOrderWithinParent - b->BeginOrderWithinParent); +} + +static void AddWindowToSortBuffer(ImVector* out_sorted_windows, ImGuiWindow* window) +{ + out_sorted_windows->push_back(window); + if (window->Active) + { + int count = window->DC.ChildWindows.Size; + ImQsort(window->DC.ChildWindows.Data, (size_t)count, sizeof(ImGuiWindow*), ChildWindowComparer); + for (int i = 0; i < count; i++) + { + ImGuiWindow* child = window->DC.ChildWindows[i]; + if (child->Active) + AddWindowToSortBuffer(out_sorted_windows, child); + } + } +} + +static void AddDrawListToDrawData(ImVector* out_list, ImDrawList* draw_list) +{ + if (draw_list->CmdBuffer.Size == 0) + return; + if (draw_list->CmdBuffer.Size == 1 && draw_list->CmdBuffer[0].ElemCount == 0 && draw_list->CmdBuffer[0].UserCallback == NULL) + return; + + // Draw list sanity check. Detect mismatch between PrimReserve() calls and incrementing _VtxCurrentIdx, _VtxWritePtr etc. + // May trigger for you if you are using PrimXXX functions incorrectly. + IM_ASSERT(draw_list->VtxBuffer.Size == 0 || draw_list->_VtxWritePtr == draw_list->VtxBuffer.Data + draw_list->VtxBuffer.Size); + IM_ASSERT(draw_list->IdxBuffer.Size == 0 || draw_list->_IdxWritePtr == draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size); + if (!(draw_list->Flags & ImDrawListFlags_AllowVtxOffset)) + IM_ASSERT((int)draw_list->_VtxCurrentIdx == draw_list->VtxBuffer.Size); + + // Check that draw_list doesn't use more vertices than indexable (default ImDrawIdx = unsigned short = 2 bytes = 64K vertices per ImDrawList = per window) + // If this assert triggers because you are drawing lots of stuff manually: + // - First, make sure you are coarse clipping yourself and not trying to draw many things outside visible bounds. + // Be mindful that the ImDrawList API doesn't filter vertices. Use the Metrics/Debugger window to inspect draw list contents. + // - If you want large meshes with more than 64K vertices, you can either: + // (A) Handle the ImDrawCmd::VtxOffset value in your renderer backend, and set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset'. + // Most example backends already support this from 1.71. Pre-1.71 backends won't. + // Some graphics API such as GL ES 1/2 don't have a way to offset the starting vertex so it is not supported for them. + // (B) Or handle 32-bit indices in your renderer backend, and uncomment '#define ImDrawIdx unsigned int' line in imconfig.h. + // Most example backends already support this. For example, the OpenGL example code detect index size at compile-time: + // glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer_offset); + // Your own engine or render API may use different parameters or function calls to specify index sizes. + // 2 and 4 bytes indices are generally supported by most graphics API. + // - If for some reason neither of those solutions works for you, a workaround is to call BeginChild()/EndChild() before reaching + // the 64K limit to split your draw commands in multiple draw lists. + if (sizeof(ImDrawIdx) == 2) + IM_ASSERT(draw_list->_VtxCurrentIdx < (1 << 16) && "Too many vertices in ImDrawList using 16-bit indices. Read comment above"); + + out_list->push_back(draw_list); +} + +static void AddWindowToDrawData(ImGuiWindow* window, int layer) +{ + ImGuiContext& g = *GImGui; + ImGuiViewportP* viewport = g.Viewports[0]; + g.IO.MetricsRenderWindows++; + AddDrawListToDrawData(&viewport->DrawDataBuilder.Layers[layer], window->DrawList); + for (int i = 0; i < window->DC.ChildWindows.Size; i++) + { + ImGuiWindow* child = window->DC.ChildWindows[i]; + if (IsWindowActiveAndVisible(child)) // Clipped children may have been marked not active + AddWindowToDrawData(child, layer); + } +} + +static inline int GetWindowDisplayLayer(ImGuiWindow* window) +{ + return (window->Flags & ImGuiWindowFlags_Tooltip) ? 1 : 0; +} + +// Layer is locked for the root window, however child windows may use a different viewport (e.g. extruding menu) +static inline void AddRootWindowToDrawData(ImGuiWindow* window) +{ + AddWindowToDrawData(window, GetWindowDisplayLayer(window)); +} + +void ImDrawDataBuilder::FlattenIntoSingleLayer() +{ + int n = Layers[0].Size; + int size = n; + for (int i = 1; i < IM_ARRAYSIZE(Layers); i++) + size += Layers[i].Size; + Layers[0].resize(size); + for (int layer_n = 1; layer_n < IM_ARRAYSIZE(Layers); layer_n++) + { + ImVector& layer = Layers[layer_n]; + if (layer.empty()) + continue; + memcpy(&Layers[0][n], &layer[0], layer.Size * sizeof(ImDrawList*)); + n += layer.Size; + layer.resize(0); + } +} + +static void SetupViewportDrawData(ImGuiViewportP* viewport, ImVector* draw_lists) +{ + ImGuiIO& io = ImGui::GetIO(); + ImDrawData* draw_data = &viewport->DrawDataP; + draw_data->Valid = true; + draw_data->CmdLists = (draw_lists->Size > 0) ? draw_lists->Data : NULL; + draw_data->CmdListsCount = draw_lists->Size; + draw_data->TotalVtxCount = draw_data->TotalIdxCount = 0; + draw_data->DisplayPos = viewport->Pos; + draw_data->DisplaySize = viewport->Size; + draw_data->FramebufferScale = io.DisplayFramebufferScale; + for (int n = 0; n < draw_lists->Size; n++) + { + ImDrawList* draw_list = draw_lists->Data[n]; + draw_list->_PopUnusedDrawCmd(); + draw_data->TotalVtxCount += draw_list->VtxBuffer.Size; + draw_data->TotalIdxCount += draw_list->IdxBuffer.Size; + } +} + +// Push a clipping rectangle for both ImGui logic (hit-testing etc.) and low-level ImDrawList rendering. +// - When using this function it is sane to ensure that float are perfectly rounded to integer values, +// so that e.g. (int)(max.x-min.x) in user's render produce correct result. +// - If the code here changes, may need to update code of functions like NextColumn() and PushColumnClipRect(): +// some frequently called functions which to modify both channels and clipping simultaneously tend to use the +// more specialized SetWindowClipRectBeforeSetChannel() to avoid extraneous updates of underlying ImDrawCmds. +void ImGui::PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DrawList->PushClipRect(clip_rect_min, clip_rect_max, intersect_with_current_clip_rect); + window->ClipRect = window->DrawList->_ClipRectStack.back(); +} + +void ImGui::PopClipRect() +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DrawList->PopClipRect(); + window->ClipRect = window->DrawList->_ClipRectStack.back(); +} + +static void ImGui::RenderDimmedBackgroundBehindWindow(ImGuiWindow* window, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + ImGuiViewportP* viewport = (ImGuiViewportP*)GetMainViewport(); + ImRect viewport_rect = viewport->GetMainRect(); + + // Draw behind window by moving the draw command at the FRONT of the draw list + { + // We've already called AddWindowToDrawData() which called DrawList->ChannelsMerge() on DockNodeHost windows, + // and draw list have been trimmed already, hence the explicit recreation of a draw command if missing. + // FIXME: This is creating complication, might be simpler if we could inject a drawlist in drawdata at a given position and not attempt to manipulate ImDrawCmd order. + ImDrawList* draw_list = window->RootWindow->DrawList; + if (draw_list->CmdBuffer.Size == 0) + draw_list->AddDrawCmd(); + draw_list->PushClipRect(viewport_rect.Min - ImVec2(1, 1), viewport_rect.Max + ImVec2(1, 1), false); // Ensure ImDrawCmd are not merged + draw_list->AddRectFilled(viewport_rect.Min, viewport_rect.Max, col); + ImDrawCmd cmd = draw_list->CmdBuffer.back(); + IM_ASSERT(cmd.ElemCount == 6); + draw_list->CmdBuffer.pop_back(); + draw_list->CmdBuffer.push_front(cmd); + draw_list->PopClipRect(); + draw_list->AddDrawCmd(); // We need to create a command as CmdBuffer.back().IdxOffset won't be correct if we append to same command. + } +} + +ImGuiWindow* ImGui::FindBottomMostVisibleWindowWithinBeginStack(ImGuiWindow* parent_window) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* bottom_most_visible_window = parent_window; + for (int i = FindWindowDisplayIndex(parent_window); i >= 0; i--) + { + ImGuiWindow* window = g.Windows[i]; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + continue; + if (!IsWindowWithinBeginStackOf(window, parent_window)) + break; + if (IsWindowActiveAndVisible(window) && GetWindowDisplayLayer(window) <= GetWindowDisplayLayer(parent_window)) + bottom_most_visible_window = window; + } + return bottom_most_visible_window; +} + +static void ImGui::RenderDimmedBackgrounds() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* modal_window = GetTopMostAndVisiblePopupModal(); + if (g.DimBgRatio <= 0.0f && g.NavWindowingHighlightAlpha <= 0.0f) + return; + const bool dim_bg_for_modal = (modal_window != NULL); + const bool dim_bg_for_window_list = (g.NavWindowingTargetAnim != NULL && g.NavWindowingTargetAnim->Active); + if (!dim_bg_for_modal && !dim_bg_for_window_list) + return; + + if (dim_bg_for_modal) + { + // Draw dimming behind modal or a begin stack child, whichever comes first in draw order. + ImGuiWindow* dim_behind_window = FindBottomMostVisibleWindowWithinBeginStack(modal_window); + RenderDimmedBackgroundBehindWindow(dim_behind_window, GetColorU32(ImGuiCol_ModalWindowDimBg, g.DimBgRatio)); + } + else if (dim_bg_for_window_list) + { + // Draw dimming behind CTRL+Tab target window + RenderDimmedBackgroundBehindWindow(g.NavWindowingTargetAnim, GetColorU32(ImGuiCol_NavWindowingDimBg, g.DimBgRatio)); + + // Draw border around CTRL+Tab target window + ImGuiWindow* window = g.NavWindowingTargetAnim; + ImGuiViewport* viewport = GetMainViewport(); + float distance = g.FontSize; + ImRect bb = window->Rect(); + bb.Expand(distance); + if (bb.GetWidth() >= viewport->Size.x && bb.GetHeight() >= viewport->Size.y) + bb.Expand(-distance - 1.0f); // If a window fits the entire viewport, adjust its highlight inward + if (window->DrawList->CmdBuffer.Size == 0) + window->DrawList->AddDrawCmd(); + window->DrawList->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size); + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha), window->WindowRounding, 0, 3.0f); + window->DrawList->PopClipRect(); + } +} + +// This is normally called by Render(). You may want to call it directly if you want to avoid calling Render() but the gain will be very minimal. +void ImGui::EndFrame() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Initialized); + + // Don't process EndFrame() multiple times. + if (g.FrameCountEnded == g.FrameCount) + return; + IM_ASSERT(g.WithinFrameScope && "Forgot to call ImGui::NewFrame()?"); + + CallContextHooks(&g, ImGuiContextHookType_EndFramePre); + + ErrorCheckEndFrameSanityChecks(); + + // Notify Platform/OS when our Input Method Editor cursor has moved (e.g. CJK inputs using Microsoft IME) + if (g.IO.SetPlatformImeDataFn && memcmp(&g.PlatformImeData, &g.PlatformImeDataPrev, sizeof(ImGuiPlatformImeData)) != 0) + g.IO.SetPlatformImeDataFn(GetMainViewport(), &g.PlatformImeData); + + // Hide implicit/fallback "Debug" window if it hasn't been used + g.WithinFrameScopeWithImplicitWindow = false; + if (g.CurrentWindow && !g.CurrentWindow->WriteAccessed) + g.CurrentWindow->Active = false; + End(); + + // Update navigation: CTRL+Tab, wrap-around requests + NavEndFrame(); + + // Drag and Drop: Elapse payload (if delivered, or if source stops being submitted) + if (g.DragDropActive) + { + bool is_delivered = g.DragDropPayload.Delivery; + bool is_elapsed = (g.DragDropPayload.DataFrameCount + 1 < g.FrameCount) && ((g.DragDropSourceFlags & ImGuiDragDropFlags_SourceAutoExpirePayload) || !IsMouseDown(g.DragDropMouseButton)); + if (is_delivered || is_elapsed) + ClearDragDrop(); + } + + // Drag and Drop: Fallback for source tooltip. This is not ideal but better than nothing. + if (g.DragDropActive && g.DragDropSourceFrameCount < g.FrameCount && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + g.DragDropWithinSource = true; + SetTooltip("..."); + g.DragDropWithinSource = false; + } + + // End frame + g.WithinFrameScope = false; + g.FrameCountEnded = g.FrameCount; + + // Initiate moving window + handle left-click and right-click focus + UpdateMouseMovingWindowEndFrame(); + + // Sort the window list so that all child windows are after their parent + // We cannot do that on FocusWindow() because children may not exist yet + g.WindowsTempSortBuffer.resize(0); + g.WindowsTempSortBuffer.reserve(g.Windows.Size); + for (int i = 0; i != g.Windows.Size; i++) + { + ImGuiWindow* window = g.Windows[i]; + if (window->Active && (window->Flags & ImGuiWindowFlags_ChildWindow)) // if a child is active its parent will add it + continue; + AddWindowToSortBuffer(&g.WindowsTempSortBuffer, window); + } + + // This usually assert if there is a mismatch between the ImGuiWindowFlags_ChildWindow / ParentWindow values and DC.ChildWindows[] in parents, aka we've done something wrong. + IM_ASSERT(g.Windows.Size == g.WindowsTempSortBuffer.Size); + g.Windows.swap(g.WindowsTempSortBuffer); + g.IO.MetricsActiveWindows = g.WindowsActiveCount; + + // Unlock font atlas + g.IO.Fonts->Locked = false; + + // Clear Input data for next frame + g.IO.MouseWheel = g.IO.MouseWheelH = 0.0f; + g.IO.InputQueueCharacters.resize(0); + g.IO.KeyModsPrev = g.IO.KeyMods; // doing it here is better than in NewFrame() as we'll tolerate backend writing to KeyMods. If we want to firmly disallow it we should detect it. + memset(g.IO.NavInputs, 0, sizeof(g.IO.NavInputs)); + + CallContextHooks(&g, ImGuiContextHookType_EndFramePost); +} + +// Prepare the data for rendering so you can call GetDrawData() +// (As with anything within the ImGui:: namspace this doesn't touch your GPU or graphics API at all: +// it is the role of the ImGui_ImplXXXX_RenderDrawData() function provided by the renderer backend) +void ImGui::Render() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Initialized); + + if (g.FrameCountEnded != g.FrameCount) + EndFrame(); + const bool first_render_of_frame = (g.FrameCountRendered != g.FrameCount); + g.FrameCountRendered = g.FrameCount; + g.IO.MetricsRenderWindows = 0; + + CallContextHooks(&g, ImGuiContextHookType_RenderPre); + + // Add background ImDrawList (for each active viewport) + for (int n = 0; n != g.Viewports.Size; n++) + { + ImGuiViewportP* viewport = g.Viewports[n]; + viewport->DrawDataBuilder.Clear(); + if (viewport->DrawLists[0] != NULL) + AddDrawListToDrawData(&viewport->DrawDataBuilder.Layers[0], GetBackgroundDrawList(viewport)); + } + + // Draw modal/window whitening backgrounds + if (first_render_of_frame) + RenderDimmedBackgrounds(); + + // Add ImDrawList to render + ImGuiWindow* windows_to_render_top_most[2]; + windows_to_render_top_most[0] = (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) ? g.NavWindowingTarget->RootWindow : NULL; + windows_to_render_top_most[1] = (g.NavWindowingTarget ? g.NavWindowingListWindow : NULL); + for (int n = 0; n != g.Windows.Size; n++) + { + ImGuiWindow* window = g.Windows[n]; + IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'" + if (IsWindowActiveAndVisible(window) && (window->Flags & ImGuiWindowFlags_ChildWindow) == 0 && window != windows_to_render_top_most[0] && window != windows_to_render_top_most[1]) + AddRootWindowToDrawData(window); + } + for (int n = 0; n < IM_ARRAYSIZE(windows_to_render_top_most); n++) + if (windows_to_render_top_most[n] && IsWindowActiveAndVisible(windows_to_render_top_most[n])) // NavWindowingTarget is always temporarily displayed as the top-most window + AddRootWindowToDrawData(windows_to_render_top_most[n]); + + // Setup ImDrawData structures for end-user + g.IO.MetricsRenderVertices = g.IO.MetricsRenderIndices = 0; + for (int n = 0; n < g.Viewports.Size; n++) + { + ImGuiViewportP* viewport = g.Viewports[n]; + viewport->DrawDataBuilder.FlattenIntoSingleLayer(); + + // Draw software mouse cursor if requested by io.MouseDrawCursor flag + if (g.IO.MouseDrawCursor && first_render_of_frame) + RenderMouseCursor(GetForegroundDrawList(viewport), g.IO.MousePos, g.Style.MouseCursorScale, g.MouseCursor, IM_COL32_WHITE, IM_COL32_BLACK, IM_COL32(0, 0, 0, 48)); + + // Add foreground ImDrawList (for each active viewport) + if (viewport->DrawLists[1] != NULL) + AddDrawListToDrawData(&viewport->DrawDataBuilder.Layers[0], GetForegroundDrawList(viewport)); + + SetupViewportDrawData(viewport, &viewport->DrawDataBuilder.Layers[0]); + ImDrawData* draw_data = &viewport->DrawDataP; + g.IO.MetricsRenderVertices += draw_data->TotalVtxCount; + g.IO.MetricsRenderIndices += draw_data->TotalIdxCount; + } + + CallContextHooks(&g, ImGuiContextHookType_RenderPost); +} + +// Calculate text size. Text can be multi-line. Optionally ignore text after a ## marker. +// CalcTextSize("") should return ImVec2(0.0f, g.FontSize) +ImVec2 ImGui::CalcTextSize(const char* text, const char* text_end, bool hide_text_after_double_hash, float wrap_width) +{ + ImGuiContext& g = *GImGui; + + const char* text_display_end; + if (hide_text_after_double_hash) + text_display_end = FindRenderedTextEnd(text, text_end); // Hide anything after a '##' string + else + text_display_end = text_end; + + ImFont* font = g.Font; + const float font_size = g.FontSize; + if (text == text_display_end) + return ImVec2(0.0f, font_size); + ImVec2 text_size = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, text, text_display_end, NULL); + + // Round + // FIXME: This has been here since Dec 2015 (7b0bf230) but down the line we want this out. + // FIXME: Investigate using ceilf or e.g. + // - https://git.musl-libc.org/cgit/musl/tree/src/math/ceilf.c + // - https://embarkstudios.github.io/rust-gpu/api/src/libm/math/ceilf.rs.html + text_size.x = IM_FLOOR(text_size.x + 0.99999f); + + return text_size; +} + +// Find window given position, search front-to-back +// FIXME: Note that we have an inconsequential lag here: OuterRectClipped is updated in Begin(), so windows moved programmatically +// with SetWindowPos() and not SetNextWindowPos() will have that rectangle lagging by a frame at the time FindHoveredWindow() is +// called, aka before the next Begin(). Moving window isn't affected. +static void FindHoveredWindow() +{ + ImGuiContext& g = *GImGui; + + ImGuiWindow* hovered_window = NULL; + ImGuiWindow* hovered_window_ignoring_moving_window = NULL; + if (g.MovingWindow && !(g.MovingWindow->Flags & ImGuiWindowFlags_NoMouseInputs)) + hovered_window = g.MovingWindow; + + ImVec2 padding_regular = g.Style.TouchExtraPadding; + ImVec2 padding_for_resize = g.IO.ConfigWindowsResizeFromEdges ? g.WindowsHoverPadding : padding_regular; + for (int i = g.Windows.Size - 1; i >= 0; i--) + { + ImGuiWindow* window = g.Windows[i]; + IM_MSVC_WARNING_SUPPRESS(28182); // [Static Analyzer] Dereferencing NULL pointer. + if (!window->Active || window->Hidden) + continue; + if (window->Flags & ImGuiWindowFlags_NoMouseInputs) + continue; + + // Using the clipped AABB, a child window will typically be clipped by its parent (not always) + ImRect bb(window->OuterRectClipped); + if (window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) + bb.Expand(padding_regular); + else + bb.Expand(padding_for_resize); + if (!bb.Contains(g.IO.MousePos)) + continue; + + // Support for one rectangular hole in any given window + // FIXME: Consider generalizing hit-testing override (with more generic data, callback, etc.) (#1512) + if (window->HitTestHoleSize.x != 0) + { + ImVec2 hole_pos(window->Pos.x + (float)window->HitTestHoleOffset.x, window->Pos.y + (float)window->HitTestHoleOffset.y); + ImVec2 hole_size((float)window->HitTestHoleSize.x, (float)window->HitTestHoleSize.y); + if (ImRect(hole_pos, hole_pos + hole_size).Contains(g.IO.MousePos)) + continue; + } + + if (hovered_window == NULL) + hovered_window = window; + IM_MSVC_WARNING_SUPPRESS(28182); // [Static Analyzer] Dereferencing NULL pointer. + if (hovered_window_ignoring_moving_window == NULL && (!g.MovingWindow || window->RootWindow != g.MovingWindow->RootWindow)) + hovered_window_ignoring_moving_window = window; + if (hovered_window && hovered_window_ignoring_moving_window) + break; + } + + g.HoveredWindow = hovered_window; + g.HoveredWindowUnderMovingWindow = hovered_window_ignoring_moving_window; +} + +bool ImGui::IsItemActive() +{ + ImGuiContext& g = *GImGui; + if (g.ActiveId) + return g.ActiveId == g.LastItemData.ID; + return false; +} + +bool ImGui::IsItemActivated() +{ + ImGuiContext& g = *GImGui; + if (g.ActiveId) + if (g.ActiveId == g.LastItemData.ID && g.ActiveIdPreviousFrame != g.LastItemData.ID) + return true; + return false; +} + +bool ImGui::IsItemDeactivated() +{ + ImGuiContext& g = *GImGui; + if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasDeactivated) + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Deactivated) != 0; + return (g.ActiveIdPreviousFrame == g.LastItemData.ID && g.ActiveIdPreviousFrame != 0 && g.ActiveId != g.LastItemData.ID); +} + +bool ImGui::IsItemDeactivatedAfterEdit() +{ + ImGuiContext& g = *GImGui; + return IsItemDeactivated() && (g.ActiveIdPreviousFrameHasBeenEditedBefore || (g.ActiveId == 0 && g.ActiveIdHasBeenEditedBefore)); +} + +// == GetItemID() == GetFocusID() +bool ImGui::IsItemFocused() +{ + ImGuiContext& g = *GImGui; + if (g.NavId != g.LastItemData.ID || g.NavId == 0) + return false; + return true; +} + +// Important: this can be useful but it is NOT equivalent to the behavior of e.g.Button()! +// Most widgets have specific reactions based on mouse-up/down state, mouse position etc. +bool ImGui::IsItemClicked(ImGuiMouseButton mouse_button) +{ + return IsMouseClicked(mouse_button) && IsItemHovered(ImGuiHoveredFlags_None); +} + +bool ImGui::IsItemToggledOpen() +{ + ImGuiContext& g = *GImGui; + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledOpen) ? true : false; +} + +bool ImGui::IsItemToggledSelection() +{ + ImGuiContext& g = *GImGui; + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledSelection) ? true : false; +} + +bool ImGui::IsAnyItemHovered() +{ + ImGuiContext& g = *GImGui; + return g.HoveredId != 0 || g.HoveredIdPreviousFrame != 0; +} + +bool ImGui::IsAnyItemActive() +{ + ImGuiContext& g = *GImGui; + return g.ActiveId != 0; +} + +bool ImGui::IsAnyItemFocused() +{ + ImGuiContext& g = *GImGui; + return g.NavId != 0 && !g.NavDisableHighlight; +} + +bool ImGui::IsItemVisible() +{ + ImGuiContext& g = *GImGui; + return g.CurrentWindow->ClipRect.Overlaps(g.LastItemData.Rect); +} + +bool ImGui::IsItemEdited() +{ + ImGuiContext& g = *GImGui; + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Edited) != 0; +} + +// Allow last item to be overlapped by a subsequent item. Both may be activated during the same frame before the later one takes priority. +// FIXME: Although this is exposed, its interaction and ideal idiom with using ImGuiButtonFlags_AllowItemOverlap flag are extremely confusing, need rework. +void ImGui::SetItemAllowOverlap() +{ + ImGuiContext& g = *GImGui; + ImGuiID id = g.LastItemData.ID; + if (g.HoveredId == id) + g.HoveredIdAllowOverlap = true; + if (g.ActiveId == id) + g.ActiveIdAllowOverlap = true; +} + +void ImGui::SetItemUsingMouseWheel() +{ + ImGuiContext& g = *GImGui; + ImGuiID id = g.LastItemData.ID; + if (g.HoveredId == id) + g.HoveredIdUsingMouseWheel = true; + if (g.ActiveId == id) + g.ActiveIdUsingMouseWheel = true; +} + +void ImGui::SetActiveIdUsingNavAndKeys() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.ActiveId != 0); + g.ActiveIdUsingNavDirMask = ~(ImU32)0; + g.ActiveIdUsingNavInputMask = ~(ImU32)0; + g.ActiveIdUsingKeyInputMask.SetAllBits(); + NavMoveRequestCancel(); +} + +ImVec2 ImGui::GetItemRectMin() +{ + ImGuiContext& g = *GImGui; + return g.LastItemData.Rect.Min; +} + +ImVec2 ImGui::GetItemRectMax() +{ + ImGuiContext& g = *GImGui; + return g.LastItemData.Rect.Max; +} + +ImVec2 ImGui::GetItemRectSize() +{ + ImGuiContext& g = *GImGui; + return g.LastItemData.Rect.GetSize(); +} + +bool ImGui::BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, bool border, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* parent_window = g.CurrentWindow; + + flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_ChildWindow; + flags |= (parent_window->Flags & ImGuiWindowFlags_NoMove); // Inherit the NoMove flag + + // Size + const ImVec2 content_avail = GetContentRegionAvail(); + ImVec2 size = ImFloor(size_arg); + const int auto_fit_axises = ((size.x == 0.0f) ? (1 << ImGuiAxis_X) : 0x00) | ((size.y == 0.0f) ? (1 << ImGuiAxis_Y) : 0x00); + if (size.x <= 0.0f) + size.x = ImMax(content_avail.x + size.x, 4.0f); // Arbitrary minimum child size (0.0f causing too much issues) + if (size.y <= 0.0f) + size.y = ImMax(content_avail.y + size.y, 4.0f); + SetNextWindowSize(size); + + // Build up name. If you need to append to a same child from multiple location in the ID stack, use BeginChild(ImGuiID id) with a stable value. + if (name) + ImFormatString(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), "%s/%s_%08X", parent_window->Name, name, id); + else + ImFormatString(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), "%s/%08X", parent_window->Name, id); + + const float backup_border_size = g.Style.ChildBorderSize; + if (!border) + g.Style.ChildBorderSize = 0.0f; + bool ret = Begin(g.TempBuffer, NULL, flags); + g.Style.ChildBorderSize = backup_border_size; + + ImGuiWindow* child_window = g.CurrentWindow; + child_window->ChildId = id; + child_window->AutoFitChildAxises = (ImS8)auto_fit_axises; + + // Set the cursor to handle case where the user called SetNextWindowPos()+BeginChild() manually. + // While this is not really documented/defined, it seems that the expected thing to do. + if (child_window->BeginCount == 1) + parent_window->DC.CursorPos = child_window->Pos; + + // Process navigation-in immediately so NavInit can run on first frame + if (g.NavActivateId == id && !(flags & ImGuiWindowFlags_NavFlattened) && (child_window->DC.NavLayersActiveMask != 0 || child_window->DC.NavHasScroll)) + { + FocusWindow(child_window); + NavInitWindow(child_window, false); + SetActiveID(id + 1, child_window); // Steal ActiveId with another arbitrary id so that key-press won't activate child item + g.ActiveIdSource = ImGuiInputSource_Nav; + } + return ret; +} + +bool ImGui::BeginChild(const char* str_id, const ImVec2& size_arg, bool border, ImGuiWindowFlags extra_flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + return BeginChildEx(str_id, window->GetID(str_id), size_arg, border, extra_flags); +} + +bool ImGui::BeginChild(ImGuiID id, const ImVec2& size_arg, bool border, ImGuiWindowFlags extra_flags) +{ + IM_ASSERT(id != 0); + return BeginChildEx(NULL, id, size_arg, border, extra_flags); +} + +void ImGui::EndChild() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + IM_ASSERT(g.WithinEndChild == false); + IM_ASSERT(window->Flags & ImGuiWindowFlags_ChildWindow); // Mismatched BeginChild()/EndChild() calls + + g.WithinEndChild = true; + if (window->BeginCount > 1) + { + End(); + } + else + { + ImVec2 sz = window->Size; + if (window->AutoFitChildAxises & (1 << ImGuiAxis_X)) // Arbitrary minimum zero-ish child size of 4.0f causes less trouble than a 0.0f + sz.x = ImMax(4.0f, sz.x); + if (window->AutoFitChildAxises & (1 << ImGuiAxis_Y)) + sz.y = ImMax(4.0f, sz.y); + End(); + + ImGuiWindow* parent_window = g.CurrentWindow; + ImRect bb(parent_window->DC.CursorPos, parent_window->DC.CursorPos + sz); + ItemSize(sz); + if ((window->DC.NavLayersActiveMask != 0 || window->DC.NavHasScroll) && !(window->Flags & ImGuiWindowFlags_NavFlattened)) + { + ItemAdd(bb, window->ChildId); + RenderNavHighlight(bb, window->ChildId); + + // When browsing a window that has no activable items (scroll only) we keep a highlight on the child (pass g.NavId to trick into always displaying) + if (window->DC.NavLayersActiveMask == 0 && window == g.NavWindow) + RenderNavHighlight(ImRect(bb.Min - ImVec2(2, 2), bb.Max + ImVec2(2, 2)), g.NavId, ImGuiNavHighlightFlags_TypeThin); + } + else + { + // Not navigable into + ItemAdd(bb, 0); + } + if (g.HoveredWindow == window) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + } + g.WithinEndChild = false; + g.LogLinePosY = -FLT_MAX; // To enforce a carriage return +} + +// Helper to create a child window / scrolling region that looks like a normal widget frame. +bool ImGui::BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags extra_flags) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]); + PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding); + PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize); + PushStyleVar(ImGuiStyleVar_WindowPadding, style.FramePadding); + bool ret = BeginChild(id, size, true, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysUseWindowPadding | extra_flags); + PopStyleVar(3); + PopStyleColor(); + return ret; +} + +void ImGui::EndChildFrame() +{ + EndChild(); +} + +static void SetWindowConditionAllowFlags(ImGuiWindow* window, ImGuiCond flags, bool enabled) +{ + window->SetWindowPosAllowFlags = enabled ? (window->SetWindowPosAllowFlags | flags) : (window->SetWindowPosAllowFlags & ~flags); + window->SetWindowSizeAllowFlags = enabled ? (window->SetWindowSizeAllowFlags | flags) : (window->SetWindowSizeAllowFlags & ~flags); + window->SetWindowCollapsedAllowFlags = enabled ? (window->SetWindowCollapsedAllowFlags | flags) : (window->SetWindowCollapsedAllowFlags & ~flags); +} + +ImGuiWindow* ImGui::FindWindowByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + return (ImGuiWindow*)g.WindowsById.GetVoidPtr(id); +} + +ImGuiWindow* ImGui::FindWindowByName(const char* name) +{ + ImGuiID id = ImHashStr(name); + return FindWindowByID(id); +} + +static void ApplyWindowSettings(ImGuiWindow* window, ImGuiWindowSettings* settings) +{ + window->Pos = ImFloor(ImVec2(settings->Pos.x, settings->Pos.y)); + if (settings->Size.x > 0 && settings->Size.y > 0) + window->Size = window->SizeFull = ImFloor(ImVec2(settings->Size.x, settings->Size.y)); + window->Collapsed = settings->Collapsed; +} + +static void UpdateWindowInFocusOrderList(ImGuiWindow* window, bool just_created, ImGuiWindowFlags new_flags) +{ + ImGuiContext& g = *GImGui; + + const bool new_is_explicit_child = (new_flags & ImGuiWindowFlags_ChildWindow) != 0; + const bool child_flag_changed = new_is_explicit_child != window->IsExplicitChild; + if ((just_created || child_flag_changed) && !new_is_explicit_child) + { + IM_ASSERT(!g.WindowsFocusOrder.contains(window)); + g.WindowsFocusOrder.push_back(window); + window->FocusOrder = (short)(g.WindowsFocusOrder.Size - 1); + } + else if (!just_created && child_flag_changed && new_is_explicit_child) + { + IM_ASSERT(g.WindowsFocusOrder[window->FocusOrder] == window); + for (int n = window->FocusOrder + 1; n < g.WindowsFocusOrder.Size; n++) + g.WindowsFocusOrder[n]->FocusOrder--; + g.WindowsFocusOrder.erase(g.WindowsFocusOrder.Data + window->FocusOrder); + window->FocusOrder = -1; + } + window->IsExplicitChild = new_is_explicit_child; +} + +static ImGuiWindow* CreateNewWindow(const char* name, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + //IMGUI_DEBUG_LOG("CreateNewWindow '%s', flags = 0x%08X\n", name, flags); + + // Create window the first time + ImGuiWindow* window = IM_NEW(ImGuiWindow)(&g, name); + window->Flags = flags; + g.WindowsById.SetVoidPtr(window->ID, window); + + // Default/arbitrary window position. Use SetNextWindowPos() with the appropriate condition flag to change the initial position of a window. + const ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + window->Pos = main_viewport->Pos + ImVec2(60, 60); + + // User can disable loading and saving of settings. Tooltip and child windows also don't store settings. + if (!(flags & ImGuiWindowFlags_NoSavedSettings)) + if (ImGuiWindowSettings* settings = ImGui::FindWindowSettings(window->ID)) + { + // Retrieve settings from .ini file + window->SettingsOffset = g.SettingsWindows.offset_from_ptr(settings); + SetWindowConditionAllowFlags(window, ImGuiCond_FirstUseEver, false); + ApplyWindowSettings(window, settings); + } + window->DC.CursorStartPos = window->DC.CursorMaxPos = window->Pos; // So first call to CalcContentSize() doesn't return crazy values + + if ((flags & ImGuiWindowFlags_AlwaysAutoResize) != 0) + { + window->AutoFitFramesX = window->AutoFitFramesY = 2; + window->AutoFitOnlyGrows = false; + } + else + { + if (window->Size.x <= 0.0f) + window->AutoFitFramesX = 2; + if (window->Size.y <= 0.0f) + window->AutoFitFramesY = 2; + window->AutoFitOnlyGrows = (window->AutoFitFramesX > 0) || (window->AutoFitFramesY > 0); + } + + if (flags & ImGuiWindowFlags_NoBringToFrontOnFocus) + g.Windows.push_front(window); // Quite slow but rare and only once + else + g.Windows.push_back(window); + UpdateWindowInFocusOrderList(window, true, window->Flags); + + return window; +} + +static ImVec2 CalcWindowSizeAfterConstraint(ImGuiWindow* window, const ImVec2& size_desired) +{ + ImGuiContext& g = *GImGui; + ImVec2 new_size = size_desired; + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint) + { + // Using -1,-1 on either X/Y axis to preserve the current size. + ImRect cr = g.NextWindowData.SizeConstraintRect; + new_size.x = (cr.Min.x >= 0 && cr.Max.x >= 0) ? ImClamp(new_size.x, cr.Min.x, cr.Max.x) : window->SizeFull.x; + new_size.y = (cr.Min.y >= 0 && cr.Max.y >= 0) ? ImClamp(new_size.y, cr.Min.y, cr.Max.y) : window->SizeFull.y; + if (g.NextWindowData.SizeCallback) + { + ImGuiSizeCallbackData data; + data.UserData = g.NextWindowData.SizeCallbackUserData; + data.Pos = window->Pos; + data.CurrentSize = window->SizeFull; + data.DesiredSize = new_size; + g.NextWindowData.SizeCallback(&data); + new_size = data.DesiredSize; + } + new_size.x = IM_FLOOR(new_size.x); + new_size.y = IM_FLOOR(new_size.y); + } + + // Minimum size + if (!(window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_AlwaysAutoResize))) + { + ImGuiWindow* window_for_height = window; + const float decoration_up_height = window_for_height->TitleBarHeight() + window_for_height->MenuBarHeight(); + new_size = ImMax(new_size, g.Style.WindowMinSize); + new_size.y = ImMax(new_size.y, decoration_up_height + ImMax(0.0f, g.Style.WindowRounding - 1.0f)); // Reduce artifacts with very small windows + } + return new_size; +} + +static void CalcWindowContentSizes(ImGuiWindow* window, ImVec2* content_size_current, ImVec2* content_size_ideal) +{ + bool preserve_old_content_sizes = false; + if (window->Collapsed && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0) + preserve_old_content_sizes = true; + else if (window->Hidden && window->HiddenFramesCannotSkipItems == 0 && window->HiddenFramesCanSkipItems > 0) + preserve_old_content_sizes = true; + if (preserve_old_content_sizes) + { + *content_size_current = window->ContentSize; + *content_size_ideal = window->ContentSizeIdeal; + return; + } + + content_size_current->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_FLOOR(window->DC.CursorMaxPos.x - window->DC.CursorStartPos.x); + content_size_current->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_FLOOR(window->DC.CursorMaxPos.y - window->DC.CursorStartPos.y); + content_size_ideal->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_FLOOR(ImMax(window->DC.CursorMaxPos.x, window->DC.IdealMaxPos.x) - window->DC.CursorStartPos.x); + content_size_ideal->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_FLOOR(ImMax(window->DC.CursorMaxPos.y, window->DC.IdealMaxPos.y) - window->DC.CursorStartPos.y); +} + +static ImVec2 CalcWindowAutoFitSize(ImGuiWindow* window, const ImVec2& size_contents) +{ + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + const float decoration_up_height = window->TitleBarHeight() + window->MenuBarHeight(); + ImVec2 size_pad = window->WindowPadding * 2.0f; + ImVec2 size_desired = size_contents + size_pad + ImVec2(0.0f, decoration_up_height); + if (window->Flags & ImGuiWindowFlags_Tooltip) + { + // Tooltip always resize + return size_desired; + } + else + { + // Maximum window size is determined by the viewport size or monitor size + const bool is_popup = (window->Flags & ImGuiWindowFlags_Popup) != 0; + const bool is_menu = (window->Flags & ImGuiWindowFlags_ChildMenu) != 0; + ImVec2 size_min = style.WindowMinSize; + if (is_popup || is_menu) // Popups and menus bypass style.WindowMinSize by default, but we give then a non-zero minimum size to facilitate understanding problematic cases (e.g. empty popups) + size_min = ImMin(size_min, ImVec2(4.0f, 4.0f)); + + // FIXME-VIEWPORT-WORKAREA: May want to use GetWorkSize() instead of Size depending on the type of windows? + ImVec2 avail_size = ImGui::GetMainViewport()->Size; + ImVec2 size_auto_fit = ImClamp(size_desired, size_min, ImMax(size_min, avail_size - style.DisplaySafeAreaPadding * 2.0f)); + + // When the window cannot fit all contents (either because of constraints, either because screen is too small), + // we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than ViewportSize-WindowPadding. + ImVec2 size_auto_fit_after_constraint = CalcWindowSizeAfterConstraint(window, size_auto_fit); + bool will_have_scrollbar_x = (size_auto_fit_after_constraint.x - size_pad.x - 0.0f < size_contents.x && !(window->Flags & ImGuiWindowFlags_NoScrollbar) && (window->Flags & ImGuiWindowFlags_HorizontalScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar); + bool will_have_scrollbar_y = (size_auto_fit_after_constraint.y - size_pad.y - decoration_up_height < size_contents.y && !(window->Flags & ImGuiWindowFlags_NoScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysVerticalScrollbar); + if (will_have_scrollbar_x) + size_auto_fit.y += style.ScrollbarSize; + if (will_have_scrollbar_y) + size_auto_fit.x += style.ScrollbarSize; + return size_auto_fit; + } +} + +ImVec2 ImGui::CalcWindowNextAutoFitSize(ImGuiWindow* window) +{ + ImVec2 size_contents_current; + ImVec2 size_contents_ideal; + CalcWindowContentSizes(window, &size_contents_current, &size_contents_ideal); + ImVec2 size_auto_fit = CalcWindowAutoFitSize(window, size_contents_ideal); + ImVec2 size_final = CalcWindowSizeAfterConstraint(window, size_auto_fit); + return size_final; +} + +static ImGuiCol GetWindowBgColorIdx(ImGuiWindow* window) +{ + if (window->Flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_Popup)) + return ImGuiCol_PopupBg; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + return ImGuiCol_ChildBg; + return ImGuiCol_WindowBg; +} + +static void CalcResizePosSizeFromAnyCorner(ImGuiWindow* window, const ImVec2& corner_target, const ImVec2& corner_norm, ImVec2* out_pos, ImVec2* out_size) +{ + ImVec2 pos_min = ImLerp(corner_target, window->Pos, corner_norm); // Expected window upper-left + ImVec2 pos_max = ImLerp(window->Pos + window->Size, corner_target, corner_norm); // Expected window lower-right + ImVec2 size_expected = pos_max - pos_min; + ImVec2 size_constrained = CalcWindowSizeAfterConstraint(window, size_expected); + *out_pos = pos_min; + if (corner_norm.x == 0.0f) + out_pos->x -= (size_constrained.x - size_expected.x); + if (corner_norm.y == 0.0f) + out_pos->y -= (size_constrained.y - size_expected.y); + *out_size = size_constrained; +} + +// Data for resizing from corner +struct ImGuiResizeGripDef +{ + ImVec2 CornerPosN; + ImVec2 InnerDir; + int AngleMin12, AngleMax12; +}; +static const ImGuiResizeGripDef resize_grip_def[4] = +{ + { ImVec2(1, 1), ImVec2(-1, -1), 0, 3 }, // Lower-right + { ImVec2(0, 1), ImVec2(+1, -1), 3, 6 }, // Lower-left + { ImVec2(0, 0), ImVec2(+1, +1), 6, 9 }, // Upper-left (Unused) + { ImVec2(1, 0), ImVec2(-1, +1), 9, 12 } // Upper-right (Unused) +}; + +// Data for resizing from borders +struct ImGuiResizeBorderDef +{ + ImVec2 InnerDir; + ImVec2 SegmentN1, SegmentN2; + float OuterAngle; +}; +static const ImGuiResizeBorderDef resize_border_def[4] = +{ + { ImVec2(+1, 0), ImVec2(0, 1), ImVec2(0, 0), IM_PI * 1.00f }, // Left + { ImVec2(-1, 0), ImVec2(1, 0), ImVec2(1, 1), IM_PI * 0.00f }, // Right + { ImVec2(0, +1), ImVec2(0, 0), ImVec2(1, 0), IM_PI * 1.50f }, // Up + { ImVec2(0, -1), ImVec2(1, 1), ImVec2(0, 1), IM_PI * 0.50f } // Down +}; + +static ImRect GetResizeBorderRect(ImGuiWindow* window, int border_n, float perp_padding, float thickness) +{ + ImRect rect = window->Rect(); + if (thickness == 0.0f) + rect.Max -= ImVec2(1, 1); + if (border_n == ImGuiDir_Left) { return ImRect(rect.Min.x - thickness, rect.Min.y + perp_padding, rect.Min.x + thickness, rect.Max.y - perp_padding); } + if (border_n == ImGuiDir_Right) { return ImRect(rect.Max.x - thickness, rect.Min.y + perp_padding, rect.Max.x + thickness, rect.Max.y - perp_padding); } + if (border_n == ImGuiDir_Up) { return ImRect(rect.Min.x + perp_padding, rect.Min.y - thickness, rect.Max.x - perp_padding, rect.Min.y + thickness); } + if (border_n == ImGuiDir_Down) { return ImRect(rect.Min.x + perp_padding, rect.Max.y - thickness, rect.Max.x - perp_padding, rect.Max.y + thickness); } + IM_ASSERT(0); + return ImRect(); +} + +// 0..3: corners (Lower-right, Lower-left, Unused, Unused) +ImGuiID ImGui::GetWindowResizeCornerID(ImGuiWindow* window, int n) +{ + IM_ASSERT(n >= 0 && n < 4); + ImGuiID id = window->ID; + id = ImHashStr("#RESIZE", 0, id); + id = ImHashData(&n, sizeof(int), id); + return id; +} + +// Borders (Left, Right, Up, Down) +ImGuiID ImGui::GetWindowResizeBorderID(ImGuiWindow* window, ImGuiDir dir) +{ + IM_ASSERT(dir >= 0 && dir < 4); + int n = (int)dir + 4; + ImGuiID id = window->ID; + id = ImHashStr("#RESIZE", 0, id); + id = ImHashData(&n, sizeof(int), id); + return id; +} + +// Handle resize for: Resize Grips, Borders, Gamepad +// Return true when using auto-fit (double click on resize grip) +static bool ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect) +{ + ImGuiContext& g = *GImGui; + ImGuiWindowFlags flags = window->Flags; + + if ((flags & ImGuiWindowFlags_NoResize) || (flags & ImGuiWindowFlags_AlwaysAutoResize) || window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + return false; + if (window->WasActive == false) // Early out to avoid running this code for e.g. an hidden implicit/fallback Debug window. + return false; + + bool ret_auto_fit = false; + const int resize_border_count = g.IO.ConfigWindowsResizeFromEdges ? 4 : 0; + const float grip_draw_size = IM_FLOOR(ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); + const float grip_hover_inner_size = IM_FLOOR(grip_draw_size * 0.75f); + const float grip_hover_outer_size = g.IO.ConfigWindowsResizeFromEdges ? WINDOWS_HOVER_PADDING : 0.0f; + + ImVec2 pos_target(FLT_MAX, FLT_MAX); + ImVec2 size_target(FLT_MAX, FLT_MAX); + + // Resize grips and borders are on layer 1 + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + + // Manual resize grips + PushID("#RESIZE"); + for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) + { + const ImGuiResizeGripDef& def = resize_grip_def[resize_grip_n]; + const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, def.CornerPosN); + + // Using the FlattenChilds button flag we make the resize button accessible even if we are hovering over a child window + bool hovered, held; + ImRect resize_rect(corner - def.InnerDir * grip_hover_outer_size, corner + def.InnerDir * grip_hover_inner_size); + if (resize_rect.Min.x > resize_rect.Max.x) ImSwap(resize_rect.Min.x, resize_rect.Max.x); + if (resize_rect.Min.y > resize_rect.Max.y) ImSwap(resize_rect.Min.y, resize_rect.Max.y); + ImGuiID resize_grip_id = window->GetID(resize_grip_n); // == GetWindowResizeCornerID() + ButtonBehavior(resize_rect, resize_grip_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_NoNavFocus); + //GetForegroundDrawList(window)->AddRect(resize_rect.Min, resize_rect.Max, IM_COL32(255, 255, 0, 255)); + if (hovered || held) + g.MouseCursor = (resize_grip_n & 1) ? ImGuiMouseCursor_ResizeNESW : ImGuiMouseCursor_ResizeNWSE; + + if (held && g.IO.MouseClickedCount[0] == 2 && resize_grip_n == 0) + { + // Manual auto-fit when double-clicking + size_target = CalcWindowSizeAfterConstraint(window, size_auto_fit); + ret_auto_fit = true; + ClearActiveID(); + } + else if (held) + { + // Resize from any of the four corners + // We don't use an incremental MouseDelta but rather compute an absolute target size based on mouse position + ImVec2 clamp_min = ImVec2(def.CornerPosN.x == 1.0f ? visibility_rect.Min.x : -FLT_MAX, def.CornerPosN.y == 1.0f ? visibility_rect.Min.y : -FLT_MAX); + ImVec2 clamp_max = ImVec2(def.CornerPosN.x == 0.0f ? visibility_rect.Max.x : +FLT_MAX, def.CornerPosN.y == 0.0f ? visibility_rect.Max.y : +FLT_MAX); + ImVec2 corner_target = g.IO.MousePos - g.ActiveIdClickOffset + ImLerp(def.InnerDir * grip_hover_outer_size, def.InnerDir * -grip_hover_inner_size, def.CornerPosN); // Corner of the window corresponding to our corner grip + corner_target = ImClamp(corner_target, clamp_min, clamp_max); + CalcResizePosSizeFromAnyCorner(window, corner_target, def.CornerPosN, &pos_target, &size_target); + } + + // Only lower-left grip is visible before hovering/activating + if (resize_grip_n == 0 || held || hovered) + resize_grip_col[resize_grip_n] = GetColorU32(held ? ImGuiCol_ResizeGripActive : hovered ? ImGuiCol_ResizeGripHovered : ImGuiCol_ResizeGrip); + } + for (int border_n = 0; border_n < resize_border_count; border_n++) + { + const ImGuiResizeBorderDef& def = resize_border_def[border_n]; + const ImGuiAxis axis = (border_n == ImGuiDir_Left || border_n == ImGuiDir_Right) ? ImGuiAxis_X : ImGuiAxis_Y; + + bool hovered, held; + ImRect border_rect = GetResizeBorderRect(window, border_n, grip_hover_inner_size, WINDOWS_HOVER_PADDING); + ImGuiID border_id = window->GetID(border_n + 4); // == GetWindowResizeBorderID() + ButtonBehavior(border_rect, border_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_NoNavFocus); + //GetForegroundDrawLists(window)->AddRect(border_rect.Min, border_rect.Max, IM_COL32(255, 255, 0, 255)); + if ((hovered && g.HoveredIdTimer > WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER) || held) + { + g.MouseCursor = (axis == ImGuiAxis_X) ? ImGuiMouseCursor_ResizeEW : ImGuiMouseCursor_ResizeNS; + if (held) + *border_held = border_n; + } + if (held) + { + ImVec2 clamp_min(border_n == ImGuiDir_Right ? visibility_rect.Min.x : -FLT_MAX, border_n == ImGuiDir_Down ? visibility_rect.Min.y : -FLT_MAX); + ImVec2 clamp_max(border_n == ImGuiDir_Left ? visibility_rect.Max.x : +FLT_MAX, border_n == ImGuiDir_Up ? visibility_rect.Max.y : +FLT_MAX); + ImVec2 border_target = window->Pos; + border_target[axis] = g.IO.MousePos[axis] - g.ActiveIdClickOffset[axis] + WINDOWS_HOVER_PADDING; + border_target = ImClamp(border_target, clamp_min, clamp_max); + CalcResizePosSizeFromAnyCorner(window, border_target, ImMin(def.SegmentN1, def.SegmentN2), &pos_target, &size_target); + } + } + PopID(); + + // Restore nav layer + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + + // Navigation resize (keyboard/gamepad) + if (g.NavWindowingTarget && g.NavWindowingTarget->RootWindow == window) + { + ImVec2 nav_resize_delta; + if (g.NavInputSource == ImGuiInputSource_Keyboard && g.IO.KeyShift) + nav_resize_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_RawKeyboard, ImGuiInputReadMode_Down); + if (g.NavInputSource == ImGuiInputSource_Gamepad) + nav_resize_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_Down); + if (nav_resize_delta.x != 0.0f || nav_resize_delta.y != 0.0f) + { + const float NAV_RESIZE_SPEED = 600.0f; + nav_resize_delta *= ImFloor(NAV_RESIZE_SPEED * g.IO.DeltaTime * ImMin(g.IO.DisplayFramebufferScale.x, g.IO.DisplayFramebufferScale.y)); + nav_resize_delta = ImMax(nav_resize_delta, visibility_rect.Min - window->Pos - window->Size); + g.NavWindowingToggleLayer = false; + g.NavDisableMouseHover = true; + resize_grip_col[0] = GetColorU32(ImGuiCol_ResizeGripActive); + // FIXME-NAV: Should store and accumulate into a separate size buffer to handle sizing constraints properly, right now a constraint will make us stuck. + size_target = CalcWindowSizeAfterConstraint(window, window->SizeFull + nav_resize_delta); + } + } + + // Apply back modified position/size to window + if (size_target.x != FLT_MAX) + { + window->SizeFull = size_target; + MarkIniSettingsDirty(window); + } + if (pos_target.x != FLT_MAX) + { + window->Pos = ImFloor(pos_target); + MarkIniSettingsDirty(window); + } + + window->Size = window->SizeFull; + return ret_auto_fit; +} + +static inline void ClampWindowRect(ImGuiWindow* window, const ImRect& visibility_rect) +{ + ImGuiContext& g = *GImGui; + ImVec2 size_for_clamping = window->Size; + if (g.IO.ConfigWindowsMoveFromTitleBarOnly && !(window->Flags & ImGuiWindowFlags_NoTitleBar)) + size_for_clamping.y = window->TitleBarHeight(); + window->Pos = ImClamp(window->Pos, visibility_rect.Min - size_for_clamping, visibility_rect.Max); +} + +static void ImGui::RenderWindowOuterBorders(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + float rounding = window->WindowRounding; + float border_size = window->WindowBorderSize; + if (border_size > 0.0f && !(window->Flags & ImGuiWindowFlags_NoBackground)) + window->DrawList->AddRect(window->Pos, window->Pos + window->Size, GetColorU32(ImGuiCol_Border), rounding, 0, border_size); + + int border_held = window->ResizeBorderHeld; + if (border_held != -1) + { + const ImGuiResizeBorderDef& def = resize_border_def[border_held]; + ImRect border_r = GetResizeBorderRect(window, border_held, rounding, 0.0f); + window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN1) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle - IM_PI * 0.25f, def.OuterAngle); + window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN2) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle, def.OuterAngle + IM_PI * 0.25f); + window->DrawList->PathStroke(GetColorU32(ImGuiCol_SeparatorActive), 0, ImMax(2.0f, border_size)); // Thicker than usual + } + if (g.Style.FrameBorderSize > 0 && !(window->Flags & ImGuiWindowFlags_NoTitleBar)) + { + float y = window->Pos.y + window->TitleBarHeight() - 1; + window->DrawList->AddLine(ImVec2(window->Pos.x + border_size, y), ImVec2(window->Pos.x + window->Size.x - border_size, y), GetColorU32(ImGuiCol_Border), g.Style.FrameBorderSize); + } +} + +// Draw background and borders +// Draw and handle scrollbars +void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar_rect, bool title_bar_is_highlight, int resize_grip_count, const ImU32 resize_grip_col[4], float resize_grip_draw_size) +{ + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + ImGuiWindowFlags flags = window->Flags; + + // Ensure that ScrollBar doesn't read last frame's SkipItems + IM_ASSERT(window->BeginCount == 0); + window->SkipItems = false; + + // Draw window + handle manual resize + // As we highlight the title bar when want_focus is set, multiple reappearing windows will have have their title bar highlighted on their reappearing frame. + const float window_rounding = window->WindowRounding; + const float window_border_size = window->WindowBorderSize; + if (window->Collapsed) + { + // Title bar only + float backup_border_size = style.FrameBorderSize; + g.Style.FrameBorderSize = window->WindowBorderSize; + ImU32 title_bar_col = GetColorU32((title_bar_is_highlight && !g.NavDisableHighlight) ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBgCollapsed); + RenderFrame(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, true, window_rounding); + g.Style.FrameBorderSize = backup_border_size; + } + else + { + // Window background + if (!(flags & ImGuiWindowFlags_NoBackground)) + { + ImU32 bg_col = GetColorU32(GetWindowBgColorIdx(window)); + bool override_alpha = false; + float alpha = 1.0f; + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasBgAlpha) + { + alpha = g.NextWindowData.BgAlphaVal; + override_alpha = true; + } + if (override_alpha) + bg_col = (bg_col & ~IM_COL32_A_MASK) | (IM_F32_TO_INT8_SAT(alpha) << IM_COL32_A_SHIFT); + window->DrawList->AddRectFilled(window->Pos + ImVec2(0, window->TitleBarHeight()), window->Pos + window->Size, bg_col, window_rounding, (flags & ImGuiWindowFlags_NoTitleBar) ? 0 : ImDrawFlags_RoundCornersBottom); + } + + // Title bar + if (!(flags & ImGuiWindowFlags_NoTitleBar)) + { + ImU32 title_bar_col = GetColorU32(title_bar_is_highlight ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBg); + window->DrawList->AddRectFilled(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, window_rounding, ImDrawFlags_RoundCornersTop); + } + + // Menu bar + if (flags & ImGuiWindowFlags_MenuBar) + { + ImRect menu_bar_rect = window->MenuBarRect(); + menu_bar_rect.ClipWith(window->Rect()); // Soft clipping, in particular child window don't have minimum size covering the menu bar so this is useful for them. + window->DrawList->AddRectFilled(menu_bar_rect.Min + ImVec2(window_border_size, 0), menu_bar_rect.Max - ImVec2(window_border_size, 0), GetColorU32(ImGuiCol_MenuBarBg), (flags & ImGuiWindowFlags_NoTitleBar) ? window_rounding : 0.0f, ImDrawFlags_RoundCornersTop); + if (style.FrameBorderSize > 0.0f && menu_bar_rect.Max.y < window->Pos.y + window->Size.y) + window->DrawList->AddLine(menu_bar_rect.GetBL(), menu_bar_rect.GetBR(), GetColorU32(ImGuiCol_Border), style.FrameBorderSize); + } + + // Scrollbars + if (window->ScrollbarX) + Scrollbar(ImGuiAxis_X); + if (window->ScrollbarY) + Scrollbar(ImGuiAxis_Y); + + // Render resize grips (after their input handling so we don't have a frame of latency) + if (!(flags & ImGuiWindowFlags_NoResize)) + { + for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) + { + const ImGuiResizeGripDef& grip = resize_grip_def[resize_grip_n]; + const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, grip.CornerPosN); + window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(window_border_size, resize_grip_draw_size) : ImVec2(resize_grip_draw_size, window_border_size))); + window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(resize_grip_draw_size, window_border_size) : ImVec2(window_border_size, resize_grip_draw_size))); + window->DrawList->PathArcToFast(ImVec2(corner.x + grip.InnerDir.x * (window_rounding + window_border_size), corner.y + grip.InnerDir.y * (window_rounding + window_border_size)), window_rounding, grip.AngleMin12, grip.AngleMax12); + window->DrawList->PathFillConvex(resize_grip_col[resize_grip_n]); + } + } + + // Borders + RenderWindowOuterBorders(window); + } +} + +// Render title text, collapse button, close button +void ImGui::RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& title_bar_rect, const char* name, bool* p_open) +{ + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + ImGuiWindowFlags flags = window->Flags; + + const bool has_close_button = (p_open != NULL); + const bool has_collapse_button = !(flags & ImGuiWindowFlags_NoCollapse) && (style.WindowMenuButtonPosition != ImGuiDir_None); + + // Close & Collapse button are on the Menu NavLayer and don't default focus (unless there's nothing else on that layer) + // FIXME-NAV: Might want (or not?) to set the equivalent of ImGuiButtonFlags_NoNavFocus so that mouse clicks on standard title bar items don't necessarily set nav/keyboard ref? + const ImGuiItemFlags item_flags_backup = g.CurrentItemFlags; + g.CurrentItemFlags |= ImGuiItemFlags_NoNavDefaultFocus; + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + + // Layout buttons + // FIXME: Would be nice to generalize the subtleties expressed here into reusable code. + float pad_l = style.FramePadding.x; + float pad_r = style.FramePadding.x; + float button_sz = g.FontSize; + ImVec2 close_button_pos; + ImVec2 collapse_button_pos; + if (has_close_button) + { + pad_r += button_sz; + close_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - style.FramePadding.x, title_bar_rect.Min.y); + } + if (has_collapse_button && style.WindowMenuButtonPosition == ImGuiDir_Right) + { + pad_r += button_sz; + collapse_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - style.FramePadding.x, title_bar_rect.Min.y); + } + if (has_collapse_button && style.WindowMenuButtonPosition == ImGuiDir_Left) + { + collapse_button_pos = ImVec2(title_bar_rect.Min.x + pad_l - style.FramePadding.x, title_bar_rect.Min.y); + pad_l += button_sz; + } + + // Collapse button (submitting first so it gets priority when choosing a navigation init fallback) + if (has_collapse_button) + if (CollapseButton(window->GetID("#COLLAPSE"), collapse_button_pos)) + window->WantCollapseToggle = true; // Defer actual collapsing to next frame as we are too far in the Begin() function + + // Close button + if (has_close_button) + if (CloseButton(window->GetID("#CLOSE"), close_button_pos)) + *p_open = false; + + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + g.CurrentItemFlags = item_flags_backup; + + // Title bar text (with: horizontal alignment, avoiding collapse/close button, optional "unsaved document" marker) + // FIXME: Refactor text alignment facilities along with RenderText helpers, this is WAY too much messy code.. + const float marker_size_x = (flags & ImGuiWindowFlags_UnsavedDocument) ? button_sz * 0.80f : 0.0f; + const ImVec2 text_size = CalcTextSize(name, NULL, true) + ImVec2(marker_size_x, 0.0f); + + // As a nice touch we try to ensure that centered title text doesn't get affected by visibility of Close/Collapse button, + // while uncentered title text will still reach edges correctly. + if (pad_l > style.FramePadding.x) + pad_l += g.Style.ItemInnerSpacing.x; + if (pad_r > style.FramePadding.x) + pad_r += g.Style.ItemInnerSpacing.x; + if (style.WindowTitleAlign.x > 0.0f && style.WindowTitleAlign.x < 1.0f) + { + float centerness = ImSaturate(1.0f - ImFabs(style.WindowTitleAlign.x - 0.5f) * 2.0f); // 0.0f on either edges, 1.0f on center + float pad_extend = ImMin(ImMax(pad_l, pad_r), title_bar_rect.GetWidth() - pad_l - pad_r - text_size.x); + pad_l = ImMax(pad_l, pad_extend * centerness); + pad_r = ImMax(pad_r, pad_extend * centerness); + } + + ImRect layout_r(title_bar_rect.Min.x + pad_l, title_bar_rect.Min.y, title_bar_rect.Max.x - pad_r, title_bar_rect.Max.y); + ImRect clip_r(layout_r.Min.x, layout_r.Min.y, ImMin(layout_r.Max.x + g.Style.ItemInnerSpacing.x, title_bar_rect.Max.x), layout_r.Max.y); + if (flags & ImGuiWindowFlags_UnsavedDocument) + { + ImVec2 marker_pos; + marker_pos.x = ImClamp(layout_r.Min.x + (layout_r.GetWidth() - text_size.x) * style.WindowTitleAlign.x + text_size.x, layout_r.Min.x, layout_r.Max.x); + marker_pos.y = (layout_r.Min.y + layout_r.Max.y) * 0.5f; + if (marker_pos.x > layout_r.Min.x) + { + RenderBullet(window->DrawList, marker_pos, GetColorU32(ImGuiCol_Text)); + clip_r.Max.x = ImMin(clip_r.Max.x, marker_pos.x - (int)(marker_size_x * 0.5f)); + } + } + //if (g.IO.KeyShift) window->DrawList->AddRect(layout_r.Min, layout_r.Max, IM_COL32(255, 128, 0, 255)); // [DEBUG] + //if (g.IO.KeyCtrl) window->DrawList->AddRect(clip_r.Min, clip_r.Max, IM_COL32(255, 128, 0, 255)); // [DEBUG] + RenderTextClipped(layout_r.Min, layout_r.Max, name, NULL, &text_size, style.WindowTitleAlign, &clip_r); +} + +void ImGui::UpdateWindowParentAndRootLinks(ImGuiWindow* window, ImGuiWindowFlags flags, ImGuiWindow* parent_window) +{ + window->ParentWindow = parent_window; + window->RootWindow = window->RootWindowPopupTree = window->RootWindowForTitleBarHighlight = window->RootWindowForNav = window; + if (parent_window && (flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Tooltip)) + window->RootWindow = parent_window->RootWindow; + if (parent_window && (flags & ImGuiWindowFlags_Popup)) + window->RootWindowPopupTree = parent_window->RootWindowPopupTree; + if (parent_window && !(flags & ImGuiWindowFlags_Modal) && (flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup))) + window->RootWindowForTitleBarHighlight = parent_window->RootWindowForTitleBarHighlight; + while (window->RootWindowForNav->Flags & ImGuiWindowFlags_NavFlattened) + { + IM_ASSERT(window->RootWindowForNav->ParentWindow != NULL); + window->RootWindowForNav = window->RootWindowForNav->ParentWindow; + } +} + +// When a modal popup is open, newly created windows that want focus (i.e. are not popups and do not specify ImGuiWindowFlags_NoFocusOnAppearing) +// should be positioned behind that modal window, unless the window was created inside the modal begin-stack. +// In case of multiple stacked modals newly created window honors begin stack order and does not go below its own modal parent. +// - Window // FindBlockingModal() returns Modal1 +// - Window // .. returns Modal1 +// - Modal1 // .. returns Modal2 +// - Window // .. returns Modal2 +// - Window // .. returns Modal2 +// - Modal2 // .. returns Modal2 +static ImGuiWindow* ImGui::FindBlockingModal(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.OpenPopupStack.Size <= 0) + return NULL; + + // Find a modal that has common parent with specified window. Specified window should be positioned behind that modal. + for (int i = g.OpenPopupStack.Size - 1; i >= 0; i--) + { + ImGuiWindow* popup_window = g.OpenPopupStack.Data[i].Window; + if (popup_window == NULL || !(popup_window->Flags & ImGuiWindowFlags_Modal)) + continue; + if (!popup_window->Active && !popup_window->WasActive) // Check WasActive, because this code may run before popup renders on current frame, also check Active to handle newly created windows. + continue; + if (IsWindowWithinBeginStackOf(window, popup_window)) // Window is rendered over last modal, no render order change needed. + break; + for (ImGuiWindow* parent = popup_window->ParentWindowInBeginStack->RootWindow; parent != NULL; parent = parent->ParentWindowInBeginStack->RootWindow) + if (IsWindowWithinBeginStackOf(window, parent)) + return popup_window; // Place window above its begin stack parent. + } + return NULL; +} + +// Push a new Dear ImGui window to add widgets to. +// - A default window called "Debug" is automatically stacked at the beginning of every frame so you can use widgets without explicitly calling a Begin/End pair. +// - Begin/End can be called multiple times during the frame with the same window name to append content. +// - The window name is used as a unique identifier to preserve window information across frames (and save rudimentary information to the .ini file). +// You can use the "##" or "###" markers to use the same label with different id, or same id with different label. See documentation at the top of this file. +// - Return false when window is collapsed, so you can early out in your code. You always need to call ImGui::End() even if false is returned. +// - Passing 'bool* p_open' displays a Close button on the upper-right corner of the window, the pointed value will be set to false when the button is pressed. +bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + IM_ASSERT(name != NULL && name[0] != '\0'); // Window name required + IM_ASSERT(g.WithinFrameScope); // Forgot to call ImGui::NewFrame() + IM_ASSERT(g.FrameCountEnded != g.FrameCount); // Called ImGui::Render() or ImGui::EndFrame() and haven't called ImGui::NewFrame() again yet + + // Find or create + ImGuiWindow* window = FindWindowByName(name); + const bool window_just_created = (window == NULL); + if (window_just_created) + window = CreateNewWindow(name, flags); + else + UpdateWindowInFocusOrderList(window, window_just_created, flags); + + // Automatically disable manual moving/resizing when NoInputs is set + if ((flags & ImGuiWindowFlags_NoInputs) == ImGuiWindowFlags_NoInputs) + flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; + + if (flags & ImGuiWindowFlags_NavFlattened) + IM_ASSERT(flags & ImGuiWindowFlags_ChildWindow); + + const int current_frame = g.FrameCount; + const bool first_begin_of_the_frame = (window->LastFrameActive != current_frame); + window->IsFallbackWindow = (g.CurrentWindowStack.Size == 0 && g.WithinFrameScopeWithImplicitWindow); + + // Update the Appearing flag + bool window_just_activated_by_user = (window->LastFrameActive < current_frame - 1); // Not using !WasActive because the implicit "Debug" window would always toggle off->on + if (flags & ImGuiWindowFlags_Popup) + { + ImGuiPopupData& popup_ref = g.OpenPopupStack[g.BeginPopupStack.Size]; + window_just_activated_by_user |= (window->PopupId != popup_ref.PopupId); // We recycle popups so treat window as activated if popup id changed + window_just_activated_by_user |= (window != popup_ref.Window); + } + window->Appearing = window_just_activated_by_user; + if (window->Appearing) + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, true); + + // Update Flags, LastFrameActive, BeginOrderXXX fields + if (first_begin_of_the_frame) + { + window->Flags = (ImGuiWindowFlags)flags; + window->LastFrameActive = current_frame; + window->LastTimeActive = (float)g.Time; + window->BeginOrderWithinParent = 0; + window->BeginOrderWithinContext = (short)(g.WindowsActiveCount++); + } + else + { + flags = window->Flags; + } + + // Parent window is latched only on the first call to Begin() of the frame, so further append-calls can be done from a different window stack + ImGuiWindow* parent_window_in_stack = g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back().Window; + ImGuiWindow* parent_window = first_begin_of_the_frame ? ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup)) ? parent_window_in_stack : NULL) : window->ParentWindow; + IM_ASSERT(parent_window != NULL || !(flags & ImGuiWindowFlags_ChildWindow)); + + // We allow window memory to be compacted so recreate the base stack when needed. + if (window->IDStack.Size == 0) + window->IDStack.push_back(window->ID); + + // Add to stack + // We intentionally set g.CurrentWindow to NULL to prevent usage until when the viewport is set, then will call SetCurrentWindow() + g.CurrentWindow = window; + ImGuiWindowStackData window_stack_data; + window_stack_data.Window = window; + window_stack_data.ParentLastItemDataBackup = g.LastItemData; + window_stack_data.StackSizesOnBegin.SetToCurrentState(); + g.CurrentWindowStack.push_back(window_stack_data); + g.CurrentWindow = NULL; + if (flags & ImGuiWindowFlags_ChildMenu) + g.BeginMenuCount++; + + if (flags & ImGuiWindowFlags_Popup) + { + ImGuiPopupData& popup_ref = g.OpenPopupStack[g.BeginPopupStack.Size]; + popup_ref.Window = window; + g.BeginPopupStack.push_back(popup_ref); + window->PopupId = popup_ref.PopupId; + } + + // Update ->RootWindow and others pointers (before any possible call to FocusWindow) + if (first_begin_of_the_frame) + { + UpdateWindowParentAndRootLinks(window, flags, parent_window); + window->ParentWindowInBeginStack = parent_window_in_stack; + } + + // Process SetNextWindow***() calls + // (FIXME: Consider splitting the HasXXX flags into X/Y components + bool window_pos_set_by_api = false; + bool window_size_x_set_by_api = false, window_size_y_set_by_api = false; + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasPos) + { + window_pos_set_by_api = (window->SetWindowPosAllowFlags & g.NextWindowData.PosCond) != 0; + if (window_pos_set_by_api && ImLengthSqr(g.NextWindowData.PosPivotVal) > 0.00001f) + { + // May be processed on the next frame if this is our first frame and we are measuring size + // FIXME: Look into removing the branch so everything can go through this same code path for consistency. + window->SetWindowPosVal = g.NextWindowData.PosVal; + window->SetWindowPosPivot = g.NextWindowData.PosPivotVal; + window->SetWindowPosAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + } + else + { + SetWindowPos(window, g.NextWindowData.PosVal, g.NextWindowData.PosCond); + } + } + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) + { + window_size_x_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.x > 0.0f); + window_size_y_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.y > 0.0f); + SetWindowSize(window, g.NextWindowData.SizeVal, g.NextWindowData.SizeCond); + } + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasScroll) + { + if (g.NextWindowData.ScrollVal.x >= 0.0f) + { + window->ScrollTarget.x = g.NextWindowData.ScrollVal.x; + window->ScrollTargetCenterRatio.x = 0.0f; + } + if (g.NextWindowData.ScrollVal.y >= 0.0f) + { + window->ScrollTarget.y = g.NextWindowData.ScrollVal.y; + window->ScrollTargetCenterRatio.y = 0.0f; + } + } + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasContentSize) + window->ContentSizeExplicit = g.NextWindowData.ContentSizeVal; + else if (first_begin_of_the_frame) + window->ContentSizeExplicit = ImVec2(0.0f, 0.0f); + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasCollapsed) + SetWindowCollapsed(window, g.NextWindowData.CollapsedVal, g.NextWindowData.CollapsedCond); + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasFocus) + FocusWindow(window); + if (window->Appearing) + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, false); + + // When reusing window again multiple times a frame, just append content (don't need to setup again) + if (first_begin_of_the_frame) + { + // Initialize + const bool window_is_child_tooltip = (flags & ImGuiWindowFlags_ChildWindow) && (flags & ImGuiWindowFlags_Tooltip); // FIXME-WIP: Undocumented behavior of Child+Tooltip for pinned tooltip (#1345) + const bool window_just_appearing_after_hidden_for_resize = (window->HiddenFramesCannotSkipItems > 0); + window->Active = true; + window->HasCloseButton = (p_open != NULL); + window->ClipRect = ImVec4(-FLT_MAX, -FLT_MAX, +FLT_MAX, +FLT_MAX); + window->IDStack.resize(1); + window->DrawList->_ResetForNewFrame(); + window->DC.CurrentTableIdx = -1; + + // Restore buffer capacity when woken from a compacted state, to avoid + if (window->MemoryCompacted) + GcAwakeTransientWindowBuffers(window); + + // Update stored window name when it changes (which can _only_ happen with the "###" operator, so the ID would stay unchanged). + // The title bar always display the 'name' parameter, so we only update the string storage if it needs to be visible to the end-user elsewhere. + bool window_title_visible_elsewhere = false; + if (g.NavWindowingListWindow != NULL && (window->Flags & ImGuiWindowFlags_NoNavFocus) == 0) // Window titles visible when using CTRL+TAB + window_title_visible_elsewhere = true; + if (window_title_visible_elsewhere && !window_just_created && strcmp(name, window->Name) != 0) + { + size_t buf_len = (size_t)window->NameBufLen; + window->Name = ImStrdupcpy(window->Name, &buf_len, name); + window->NameBufLen = (int)buf_len; + } + + // UPDATE CONTENTS SIZE, UPDATE HIDDEN STATUS + + // Update contents size from last frame for auto-fitting (or use explicit size) + CalcWindowContentSizes(window, &window->ContentSize, &window->ContentSizeIdeal); + if (window->HiddenFramesCanSkipItems > 0) + window->HiddenFramesCanSkipItems--; + if (window->HiddenFramesCannotSkipItems > 0) + window->HiddenFramesCannotSkipItems--; + if (window->HiddenFramesForRenderOnly > 0) + window->HiddenFramesForRenderOnly--; + + // Hide new windows for one frame until they calculate their size + if (window_just_created && (!window_size_x_set_by_api || !window_size_y_set_by_api)) + window->HiddenFramesCannotSkipItems = 1; + + // Hide popup/tooltip window when re-opening while we measure size (because we recycle the windows) + // We reset Size/ContentSize for reappearing popups/tooltips early in this function, so further code won't be tempted to use the old size. + if (window_just_activated_by_user && (flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) != 0) + { + window->HiddenFramesCannotSkipItems = 1; + if (flags & ImGuiWindowFlags_AlwaysAutoResize) + { + if (!window_size_x_set_by_api) + window->Size.x = window->SizeFull.x = 0.f; + if (!window_size_y_set_by_api) + window->Size.y = window->SizeFull.y = 0.f; + window->ContentSize = window->ContentSizeIdeal = ImVec2(0.f, 0.f); + } + } + + // SELECT VIEWPORT + // FIXME-VIEWPORT: In the docking/viewport branch, this is the point where we select the current viewport (which may affect the style) + SetCurrentWindow(window); + + // LOCK BORDER SIZE AND PADDING FOR THE FRAME (so that altering them doesn't cause inconsistencies) + + if (flags & ImGuiWindowFlags_ChildWindow) + window->WindowBorderSize = style.ChildBorderSize; + else + window->WindowBorderSize = ((flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupBorderSize : style.WindowBorderSize; + window->WindowPadding = style.WindowPadding; + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & (ImGuiWindowFlags_AlwaysUseWindowPadding | ImGuiWindowFlags_Popup)) && window->WindowBorderSize == 0.0f) + window->WindowPadding = ImVec2(0.0f, (flags & ImGuiWindowFlags_MenuBar) ? style.WindowPadding.y : 0.0f); + + // Lock menu offset so size calculation can use it as menu-bar windows need a minimum size. + window->DC.MenuBarOffset.x = ImMax(ImMax(window->WindowPadding.x, style.ItemSpacing.x), g.NextWindowData.MenuBarOffsetMinVal.x); + window->DC.MenuBarOffset.y = g.NextWindowData.MenuBarOffsetMinVal.y; + + // Collapse window by double-clicking on title bar + // At this point we don't have a clipping rectangle setup yet, so we can use the title bar area for hit detection and drawing + if (!(flags & ImGuiWindowFlags_NoTitleBar) && !(flags & ImGuiWindowFlags_NoCollapse)) + { + // We don't use a regular button+id to test for double-click on title bar (mostly due to legacy reason, could be fixed), so verify that we don't have items over the title bar. + ImRect title_bar_rect = window->TitleBarRect(); + if (g.HoveredWindow == window && g.HoveredId == 0 && g.HoveredIdPreviousFrame == 0 && IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max) && g.IO.MouseClickedCount[0] == 2) + window->WantCollapseToggle = true; + if (window->WantCollapseToggle) + { + window->Collapsed = !window->Collapsed; + MarkIniSettingsDirty(window); + } + } + else + { + window->Collapsed = false; + } + window->WantCollapseToggle = false; + + // SIZE + + // Calculate auto-fit size, handle automatic resize + const ImVec2 size_auto_fit = CalcWindowAutoFitSize(window, window->ContentSizeIdeal); + bool use_current_size_for_scrollbar_x = window_just_created; + bool use_current_size_for_scrollbar_y = window_just_created; + if ((flags & ImGuiWindowFlags_AlwaysAutoResize) && !window->Collapsed) + { + // Using SetNextWindowSize() overrides ImGuiWindowFlags_AlwaysAutoResize, so it can be used on tooltips/popups, etc. + if (!window_size_x_set_by_api) + { + window->SizeFull.x = size_auto_fit.x; + use_current_size_for_scrollbar_x = true; + } + if (!window_size_y_set_by_api) + { + window->SizeFull.y = size_auto_fit.y; + use_current_size_for_scrollbar_y = true; + } + } + else if (window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + { + // Auto-fit may only grow window during the first few frames + // We still process initial auto-fit on collapsed windows to get a window width, but otherwise don't honor ImGuiWindowFlags_AlwaysAutoResize when collapsed. + if (!window_size_x_set_by_api && window->AutoFitFramesX > 0) + { + window->SizeFull.x = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.x, size_auto_fit.x) : size_auto_fit.x; + use_current_size_for_scrollbar_x = true; + } + if (!window_size_y_set_by_api && window->AutoFitFramesY > 0) + { + window->SizeFull.y = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.y, size_auto_fit.y) : size_auto_fit.y; + use_current_size_for_scrollbar_y = true; + } + if (!window->Collapsed) + MarkIniSettingsDirty(window); + } + + // Apply minimum/maximum window size constraints and final size + window->SizeFull = CalcWindowSizeAfterConstraint(window, window->SizeFull); + window->Size = window->Collapsed && !(flags & ImGuiWindowFlags_ChildWindow) ? window->TitleBarRect().GetSize() : window->SizeFull; + + // Decoration size + const float decoration_up_height = window->TitleBarHeight() + window->MenuBarHeight(); + + // POSITION + + // Popup latch its initial position, will position itself when it appears next frame + if (window_just_activated_by_user) + { + window->AutoPosLastDirection = ImGuiDir_None; + if ((flags & ImGuiWindowFlags_Popup) != 0 && !(flags & ImGuiWindowFlags_Modal) && !window_pos_set_by_api) // FIXME: BeginPopup() could use SetNextWindowPos() + window->Pos = g.BeginPopupStack.back().OpenPopupPos; + } + + // Position child window + if (flags & ImGuiWindowFlags_ChildWindow) + { + IM_ASSERT(parent_window && parent_window->Active); + window->BeginOrderWithinParent = (short)parent_window->DC.ChildWindows.Size; + parent_window->DC.ChildWindows.push_back(window); + if (!(flags & ImGuiWindowFlags_Popup) && !window_pos_set_by_api && !window_is_child_tooltip) + window->Pos = parent_window->DC.CursorPos; + } + + const bool window_pos_with_pivot = (window->SetWindowPosVal.x != FLT_MAX && window->HiddenFramesCannotSkipItems == 0); + if (window_pos_with_pivot) + SetWindowPos(window, window->SetWindowPosVal - window->Size * window->SetWindowPosPivot, 0); // Position given a pivot (e.g. for centering) + else if ((flags & ImGuiWindowFlags_ChildMenu) != 0) + window->Pos = FindBestWindowPosForPopup(window); + else if ((flags & ImGuiWindowFlags_Popup) != 0 && !window_pos_set_by_api && window_just_appearing_after_hidden_for_resize) + window->Pos = FindBestWindowPosForPopup(window); + else if ((flags & ImGuiWindowFlags_Tooltip) != 0 && !window_pos_set_by_api && !window_is_child_tooltip) + window->Pos = FindBestWindowPosForPopup(window); + + // Calculate the range of allowed position for that window (to be movable and visible past safe area padding) + // When clamping to stay visible, we will enforce that window->Pos stays inside of visibility_rect. + ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport(); + ImRect viewport_rect(viewport->GetMainRect()); + ImRect viewport_work_rect(viewport->GetWorkRect()); + ImVec2 visibility_padding = ImMax(style.DisplayWindowPadding, style.DisplaySafeAreaPadding); + ImRect visibility_rect(viewport_work_rect.Min + visibility_padding, viewport_work_rect.Max - visibility_padding); + + // Clamp position/size so window stays visible within its viewport or monitor + // Ignore zero-sized display explicitly to avoid losing positions if a window manager reports zero-sized window when initializing or minimizing. + if (!window_pos_set_by_api && !(flags & ImGuiWindowFlags_ChildWindow) && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0) + if (viewport_rect.GetWidth() > 0.0f && viewport_rect.GetHeight() > 0.0f) + ClampWindowRect(window, visibility_rect); + window->Pos = ImFloor(window->Pos); + + // Lock window rounding for the frame (so that altering them doesn't cause inconsistencies) + // Large values tend to lead to variety of artifacts and are not recommended. + window->WindowRounding = (flags & ImGuiWindowFlags_ChildWindow) ? style.ChildRounding : ((flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupRounding : style.WindowRounding; + + // For windows with title bar or menu bar, we clamp to FrameHeight(FontSize + FramePadding.y * 2.0f) to completely hide artifacts. + //if ((window->Flags & ImGuiWindowFlags_MenuBar) || !(window->Flags & ImGuiWindowFlags_NoTitleBar)) + // window->WindowRounding = ImMin(window->WindowRounding, g.FontSize + style.FramePadding.y * 2.0f); + + // Apply window focus (new and reactivated windows are moved to front) + bool want_focus = false; + if (window_just_activated_by_user && !(flags & ImGuiWindowFlags_NoFocusOnAppearing)) + { + if (flags & ImGuiWindowFlags_Popup) + want_focus = true; + else if ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Tooltip)) == 0) + want_focus = true; + + ImGuiWindow* modal = GetTopMostPopupModal(); + if (modal != NULL && !IsWindowWithinBeginStackOf(window, modal)) + { + // Avoid focusing a window that is created outside of active modal. This will prevent active modal from being closed. + // Since window is not focused it would reappear at the same display position like the last time it was visible. + // In case of completely new windows it would go to the top (over current modal), but input to such window would still be blocked by modal. + // Position window behind a modal that is not a begin-parent of this window. + want_focus = false; + if (window == window->RootWindow) + { + ImGuiWindow* blocking_modal = FindBlockingModal(window); + IM_ASSERT(blocking_modal != NULL); + BringWindowToDisplayBehind(window, blocking_modal); + } + } + } + + // [Test Engine] Register whole window in the item system +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (g.TestEngineHookItems) + { + IM_ASSERT(window->IDStack.Size == 1); + window->IDStack.Size = 0; + IMGUI_TEST_ENGINE_ITEM_ADD(window->Rect(), window->ID); + IMGUI_TEST_ENGINE_ITEM_INFO(window->ID, window->Name, (g.HoveredWindow == window) ? ImGuiItemStatusFlags_HoveredRect : 0); + window->IDStack.Size = 1; + } +#endif + + // Handle manual resize: Resize Grips, Borders, Gamepad + int border_held = -1; + ImU32 resize_grip_col[4] = {}; + const int resize_grip_count = g.IO.ConfigWindowsResizeFromEdges ? 2 : 1; // Allow resize from lower-left if we have the mouse cursor feedback for it. + const float resize_grip_draw_size = IM_FLOOR(ImMax(g.FontSize * 1.10f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); + if (!window->Collapsed) + if (UpdateWindowManualResize(window, size_auto_fit, &border_held, resize_grip_count, &resize_grip_col[0], visibility_rect)) + use_current_size_for_scrollbar_x = use_current_size_for_scrollbar_y = true; + window->ResizeBorderHeld = (signed char)border_held; + + // SCROLLBAR VISIBILITY + + // Update scrollbar visibility (based on the Size that was effective during last frame or the auto-resized Size). + if (!window->Collapsed) + { + // When reading the current size we need to read it after size constraints have been applied. + // When we use InnerRect here we are intentionally reading last frame size, same for ScrollbarSizes values before we set them again. + ImVec2 avail_size_from_current_frame = ImVec2(window->SizeFull.x, window->SizeFull.y - decoration_up_height); + ImVec2 avail_size_from_last_frame = window->InnerRect.GetSize() + window->ScrollbarSizes; + ImVec2 needed_size_from_last_frame = window_just_created ? ImVec2(0, 0) : window->ContentSize + window->WindowPadding * 2.0f; + float size_x_for_scrollbars = use_current_size_for_scrollbar_x ? avail_size_from_current_frame.x : avail_size_from_last_frame.x; + float size_y_for_scrollbars = use_current_size_for_scrollbar_y ? avail_size_from_current_frame.y : avail_size_from_last_frame.y; + //bool scrollbar_y_from_last_frame = window->ScrollbarY; // FIXME: May want to use that in the ScrollbarX expression? How many pros vs cons? + window->ScrollbarY = (flags & ImGuiWindowFlags_AlwaysVerticalScrollbar) || ((needed_size_from_last_frame.y > size_y_for_scrollbars) && !(flags & ImGuiWindowFlags_NoScrollbar)); + window->ScrollbarX = (flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar) || ((needed_size_from_last_frame.x > size_x_for_scrollbars - (window->ScrollbarY ? style.ScrollbarSize : 0.0f)) && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)); + if (window->ScrollbarX && !window->ScrollbarY) + window->ScrollbarY = (needed_size_from_last_frame.y > size_y_for_scrollbars) && !(flags & ImGuiWindowFlags_NoScrollbar); + window->ScrollbarSizes = ImVec2(window->ScrollbarY ? style.ScrollbarSize : 0.0f, window->ScrollbarX ? style.ScrollbarSize : 0.0f); + } + + // UPDATE RECTANGLES (1- THOSE NOT AFFECTED BY SCROLLING) + // Update various regions. Variables they depends on should be set above in this function. + // We set this up after processing the resize grip so that our rectangles doesn't lag by a frame. + + // Outer rectangle + // Not affected by window border size. Used by: + // - FindHoveredWindow() (w/ extra padding when border resize is enabled) + // - Begin() initial clipping rect for drawing window background and borders. + // - Begin() clipping whole child + const ImRect host_rect = ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !window_is_child_tooltip) ? parent_window->ClipRect : viewport_rect; + const ImRect outer_rect = window->Rect(); + const ImRect title_bar_rect = window->TitleBarRect(); + window->OuterRectClipped = outer_rect; + window->OuterRectClipped.ClipWith(host_rect); + + // Inner rectangle + // Not affected by window border size. Used by: + // - InnerClipRect + // - ScrollToRectEx() + // - NavUpdatePageUpPageDown() + // - Scrollbar() + window->InnerRect.Min.x = window->Pos.x; + window->InnerRect.Min.y = window->Pos.y + decoration_up_height; + window->InnerRect.Max.x = window->Pos.x + window->Size.x - window->ScrollbarSizes.x; + window->InnerRect.Max.y = window->Pos.y + window->Size.y - window->ScrollbarSizes.y; + + // Inner clipping rectangle. + // Will extend a little bit outside the normal work region. + // This is to allow e.g. Selectable or CollapsingHeader or some separators to cover that space. + // Force round operator last to ensure that e.g. (int)(max.x-min.x) in user's render code produce correct result. + // Note that if our window is collapsed we will end up with an inverted (~null) clipping rectangle which is the correct behavior. + // Affected by window/frame border size. Used by: + // - Begin() initial clip rect + float top_border_size = (((flags & ImGuiWindowFlags_MenuBar) || !(flags & ImGuiWindowFlags_NoTitleBar)) ? style.FrameBorderSize : window->WindowBorderSize); + window->InnerClipRect.Min.x = ImFloor(0.5f + window->InnerRect.Min.x + ImMax(ImFloor(window->WindowPadding.x * 0.5f), window->WindowBorderSize)); + window->InnerClipRect.Min.y = ImFloor(0.5f + window->InnerRect.Min.y + top_border_size); + window->InnerClipRect.Max.x = ImFloor(0.5f + window->InnerRect.Max.x - ImMax(ImFloor(window->WindowPadding.x * 0.5f), window->WindowBorderSize)); + window->InnerClipRect.Max.y = ImFloor(0.5f + window->InnerRect.Max.y - window->WindowBorderSize); + window->InnerClipRect.ClipWithFull(host_rect); + + // Default item width. Make it proportional to window size if window manually resizes + if (window->Size.x > 0.0f && !(flags & ImGuiWindowFlags_Tooltip) && !(flags & ImGuiWindowFlags_AlwaysAutoResize)) + window->ItemWidthDefault = ImFloor(window->Size.x * 0.65f); + else + window->ItemWidthDefault = ImFloor(g.FontSize * 16.0f); + + // SCROLLING + + // Lock down maximum scrolling + // The value of ScrollMax are ahead from ScrollbarX/ScrollbarY which is intentionally using InnerRect from previous rect in order to accommodate + // for right/bottom aligned items without creating a scrollbar. + window->ScrollMax.x = ImMax(0.0f, window->ContentSize.x + window->WindowPadding.x * 2.0f - window->InnerRect.GetWidth()); + window->ScrollMax.y = ImMax(0.0f, window->ContentSize.y + window->WindowPadding.y * 2.0f - window->InnerRect.GetHeight()); + + // Apply scrolling + window->Scroll = CalcNextScrollFromScrollTargetAndClamp(window); + window->ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); + + // DRAWING + + // Setup draw list and outer clipping rectangle + IM_ASSERT(window->DrawList->CmdBuffer.Size == 1 && window->DrawList->CmdBuffer[0].ElemCount == 0); + window->DrawList->PushTextureID(g.Font->ContainerAtlas->TexID); + PushClipRect(host_rect.Min, host_rect.Max, false); + + // Child windows can render their decoration (bg color, border, scrollbars, etc.) within their parent to save a draw call (since 1.71) + // When using overlapping child windows, this will break the assumption that child z-order is mapped to submission order. + // FIXME: User code may rely on explicit sorting of overlapping child window and would need to disable this somehow. Please get in contact if you are affected (github #4493) + { + bool render_decorations_in_parent = false; + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !window_is_child_tooltip) + { + // - We test overlap with the previous child window only (testing all would end up being O(log N) not a good investment here) + // - We disable this when the parent window has zero vertices, which is a common pattern leading to laying out multiple overlapping childs + ImGuiWindow* previous_child = parent_window->DC.ChildWindows.Size >= 2 ? parent_window->DC.ChildWindows[parent_window->DC.ChildWindows.Size - 2] : NULL; + bool previous_child_overlapping = previous_child ? previous_child->Rect().Overlaps(window->Rect()) : false; + bool parent_is_empty = parent_window->DrawList->VtxBuffer.Size > 0; + if (window->DrawList->CmdBuffer.back().ElemCount == 0 && parent_is_empty && !previous_child_overlapping) + render_decorations_in_parent = true; + } + if (render_decorations_in_parent) + window->DrawList = parent_window->DrawList; + + // Handle title bar, scrollbar, resize grips and resize borders + const ImGuiWindow* window_to_highlight = g.NavWindowingTarget ? g.NavWindowingTarget : g.NavWindow; + const bool title_bar_is_highlight = want_focus || (window_to_highlight && window->RootWindowForTitleBarHighlight == window_to_highlight->RootWindowForTitleBarHighlight); + RenderWindowDecorations(window, title_bar_rect, title_bar_is_highlight, resize_grip_count, resize_grip_col, resize_grip_draw_size); + + if (render_decorations_in_parent) + window->DrawList = &window->DrawListInst; + } + + // UPDATE RECTANGLES (2- THOSE AFFECTED BY SCROLLING) + + // Work rectangle. + // Affected by window padding and border size. Used by: + // - Columns() for right-most edge + // - TreeNode(), CollapsingHeader() for right-most edge + // - BeginTabBar() for right-most edge + const bool allow_scrollbar_x = !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar); + const bool allow_scrollbar_y = !(flags & ImGuiWindowFlags_NoScrollbar); + const float work_rect_size_x = (window->ContentSizeExplicit.x != 0.0f ? window->ContentSizeExplicit.x : ImMax(allow_scrollbar_x ? window->ContentSize.x : 0.0f, window->Size.x - window->WindowPadding.x * 2.0f - window->ScrollbarSizes.x)); + const float work_rect_size_y = (window->ContentSizeExplicit.y != 0.0f ? window->ContentSizeExplicit.y : ImMax(allow_scrollbar_y ? window->ContentSize.y : 0.0f, window->Size.y - window->WindowPadding.y * 2.0f - decoration_up_height - window->ScrollbarSizes.y)); + window->WorkRect.Min.x = ImFloor(window->InnerRect.Min.x - window->Scroll.x + ImMax(window->WindowPadding.x, window->WindowBorderSize)); + window->WorkRect.Min.y = ImFloor(window->InnerRect.Min.y - window->Scroll.y + ImMax(window->WindowPadding.y, window->WindowBorderSize)); + window->WorkRect.Max.x = window->WorkRect.Min.x + work_rect_size_x; + window->WorkRect.Max.y = window->WorkRect.Min.y + work_rect_size_y; + window->ParentWorkRect = window->WorkRect; + + // [LEGACY] Content Region + // FIXME-OBSOLETE: window->ContentRegionRect.Max is currently very misleading / partly faulty, but some BeginChild() patterns relies on it. + // Used by: + // - Mouse wheel scrolling + many other things + window->ContentRegionRect.Min.x = window->Pos.x - window->Scroll.x + window->WindowPadding.x; + window->ContentRegionRect.Min.y = window->Pos.y - window->Scroll.y + window->WindowPadding.y + decoration_up_height; + window->ContentRegionRect.Max.x = window->ContentRegionRect.Min.x + (window->ContentSizeExplicit.x != 0.0f ? window->ContentSizeExplicit.x : (window->Size.x - window->WindowPadding.x * 2.0f - window->ScrollbarSizes.x)); + window->ContentRegionRect.Max.y = window->ContentRegionRect.Min.y + (window->ContentSizeExplicit.y != 0.0f ? window->ContentSizeExplicit.y : (window->Size.y - window->WindowPadding.y * 2.0f - decoration_up_height - window->ScrollbarSizes.y)); + + // Setup drawing context + // (NB: That term "drawing context / DC" lost its meaning a long time ago. Initially was meant to hold transient data only. Nowadays difference between window-> and window->DC-> is dubious.) + window->DC.Indent.x = 0.0f + window->WindowPadding.x - window->Scroll.x; + window->DC.GroupOffset.x = 0.0f; + window->DC.ColumnsOffset.x = 0.0f; + + // Record the loss of precision of CursorStartPos which can happen due to really large scrolling amount. + // This is used by clipper to compensate and fix the most common use case of large scroll area. Easy and cheap, next best thing compared to switching everything to double or ImU64. + double start_pos_highp_x = (double)window->Pos.x + window->WindowPadding.x - (double)window->Scroll.x + window->DC.ColumnsOffset.x; + double start_pos_highp_y = (double)window->Pos.y + window->WindowPadding.y - (double)window->Scroll.y + decoration_up_height; + window->DC.CursorStartPos = ImVec2((float)start_pos_highp_x, (float)start_pos_highp_y); + window->DC.CursorStartPosLossyness = ImVec2((float)(start_pos_highp_x - window->DC.CursorStartPos.x), (float)(start_pos_highp_y - window->DC.CursorStartPos.y)); + window->DC.CursorPos = window->DC.CursorStartPos; + window->DC.CursorPosPrevLine = window->DC.CursorPos; + window->DC.CursorMaxPos = window->DC.CursorStartPos; + window->DC.IdealMaxPos = window->DC.CursorStartPos; + window->DC.CurrLineSize = window->DC.PrevLineSize = ImVec2(0.0f, 0.0f); + window->DC.CurrLineTextBaseOffset = window->DC.PrevLineTextBaseOffset = 0.0f; + + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + window->DC.NavLayersActiveMask = window->DC.NavLayersActiveMaskNext; + window->DC.NavLayersActiveMaskNext = 0x00; + window->DC.NavHideHighlightOneFrame = false; + window->DC.NavHasScroll = (window->ScrollMax.y > 0.0f); + + window->DC.MenuBarAppending = false; + window->DC.MenuColumns.Update(style.ItemSpacing.x, window_just_activated_by_user); + window->DC.TreeDepth = 0; + window->DC.TreeJumpToParentOnPopMask = 0x00; + window->DC.ChildWindows.resize(0); + window->DC.StateStorage = &window->StateStorage; + window->DC.CurrentColumns = NULL; + window->DC.LayoutType = ImGuiLayoutType_Vertical; + window->DC.ParentLayoutType = parent_window ? parent_window->DC.LayoutType : ImGuiLayoutType_Vertical; + + window->DC.ItemWidth = window->ItemWidthDefault; + window->DC.TextWrapPos = -1.0f; // disabled + window->DC.ItemWidthStack.resize(0); + window->DC.TextWrapPosStack.resize(0); + + if (window->AutoFitFramesX > 0) + window->AutoFitFramesX--; + if (window->AutoFitFramesY > 0) + window->AutoFitFramesY--; + + // Apply focus (we need to call FocusWindow() AFTER setting DC.CursorStartPos so our initial navigation reference rectangle can start around there) + if (want_focus) + { + FocusWindow(window); + NavInitWindow(window, false); // <-- this is in the way for us to be able to defer and sort reappearing FocusWindow() calls + } + + // Title bar + if (!(flags & ImGuiWindowFlags_NoTitleBar)) + RenderWindowTitleBarContents(window, ImRect(title_bar_rect.Min.x + window->WindowBorderSize, title_bar_rect.Min.y, title_bar_rect.Max.x - window->WindowBorderSize, title_bar_rect.Max.y), name, p_open); + + // Clear hit test shape every frame + window->HitTestHoleSize.x = window->HitTestHoleSize.y = 0; + + // Pressing CTRL+C while holding on a window copy its content to the clipboard + // This works but 1. doesn't handle multiple Begin/End pairs, 2. recursing into another Begin/End pair - so we need to work that out and add better logging scope. + // Maybe we can support CTRL+C on every element? + /* + //if (g.NavWindow == window && g.ActiveId == 0) + if (g.ActiveId == window->MoveId) + if (g.IO.KeyCtrl && IsKeyPressedMap(ImGuiKey_C)) + LogToClipboard(); + */ + + // We fill last item data based on Title Bar/Tab, in order for IsItemHovered() and IsItemActive() to be usable after Begin(). + // This is useful to allow creating context menus on title bar only, etc. + SetLastItemData(window->MoveId, g.CurrentItemFlags, IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max, false) ? ImGuiItemStatusFlags_HoveredRect : 0, title_bar_rect); + + // [Test Engine] Register title bar / tab + if (!(window->Flags & ImGuiWindowFlags_NoTitleBar)) + IMGUI_TEST_ENGINE_ITEM_ADD(g.LastItemData.Rect, g.LastItemData.ID); + } + else + { + // Append + SetCurrentWindow(window); + } + + // Pull/inherit current state + window->DC.NavFocusScopeIdCurrent = (flags & ImGuiWindowFlags_ChildWindow) ? parent_window->DC.NavFocusScopeIdCurrent : window->GetID("#FOCUSSCOPE"); // Inherit from parent only // -V595 + + PushClipRect(window->InnerClipRect.Min, window->InnerClipRect.Max, true); + + // Clear 'accessed' flag last thing (After PushClipRect which will set the flag. We want the flag to stay false when the default "Debug" window is unused) + window->WriteAccessed = false; + window->BeginCount++; + g.NextWindowData.ClearFlags(); + + // Update visibility + if (first_begin_of_the_frame) + { + if (flags & ImGuiWindowFlags_ChildWindow) + { + // Child window can be out of sight and have "negative" clip windows. + // Mark them as collapsed so commands are skipped earlier (we can't manually collapse them because they have no title bar). + IM_ASSERT((flags & ImGuiWindowFlags_NoTitleBar) != 0); + if (!(flags & ImGuiWindowFlags_AlwaysAutoResize) && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0) // FIXME: Doesn't make sense for ChildWindow?? + { + const bool nav_request = (flags & ImGuiWindowFlags_NavFlattened) && (g.NavAnyRequest && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav); + if (!g.LogEnabled && !nav_request) + if (window->OuterRectClipped.Min.x >= window->OuterRectClipped.Max.x || window->OuterRectClipped.Min.y >= window->OuterRectClipped.Max.y) + window->HiddenFramesCanSkipItems = 1; + } + + // Hide along with parent or if parent is collapsed + if (parent_window && (parent_window->Collapsed || parent_window->HiddenFramesCanSkipItems > 0)) + window->HiddenFramesCanSkipItems = 1; + if (parent_window && (parent_window->Collapsed || parent_window->HiddenFramesCannotSkipItems > 0)) + window->HiddenFramesCannotSkipItems = 1; + } + + // Don't render if style alpha is 0.0 at the time of Begin(). This is arbitrary and inconsistent but has been there for a long while (may remove at some point) + if (style.Alpha <= 0.0f) + window->HiddenFramesCanSkipItems = 1; + + // Update the Hidden flag + bool hidden_regular = (window->HiddenFramesCanSkipItems > 0) || (window->HiddenFramesCannotSkipItems > 0); + window->Hidden = hidden_regular || (window->HiddenFramesForRenderOnly > 0); + + // Disable inputs for requested number of frames + if (window->DisableInputsFrames > 0) + { + window->DisableInputsFrames--; + window->Flags |= ImGuiWindowFlags_NoInputs; + } + + // Update the SkipItems flag, used to early out of all items functions (no layout required) + bool skip_items = false; + if (window->Collapsed || !window->Active || hidden_regular) + if (window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0 && window->HiddenFramesCannotSkipItems <= 0) + skip_items = true; + window->SkipItems = skip_items; + } + + return !window->SkipItems; +} + +void ImGui::End() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Error checking: verify that user hasn't called End() too many times! + if (g.CurrentWindowStack.Size <= 1 && g.WithinFrameScopeWithImplicitWindow) + { + IM_ASSERT_USER_ERROR(g.CurrentWindowStack.Size > 1, "Calling End() too many times!"); + return; + } + IM_ASSERT(g.CurrentWindowStack.Size > 0); + + // Error checking: verify that user doesn't directly call End() on a child window. + if (window->Flags & ImGuiWindowFlags_ChildWindow) + IM_ASSERT_USER_ERROR(g.WithinEndChild, "Must call EndChild() and not End()!"); + + // Close anything that is open + if (window->DC.CurrentColumns) + EndColumns(); + PopClipRect(); // Inner window clip rectangle + + // Stop logging + if (!(window->Flags & ImGuiWindowFlags_ChildWindow)) // FIXME: add more options for scope of logging + LogFinish(); + + // Pop from window stack + g.LastItemData = g.CurrentWindowStack.back().ParentLastItemDataBackup; + if (window->Flags & ImGuiWindowFlags_ChildMenu) + g.BeginMenuCount--; + if (window->Flags & ImGuiWindowFlags_Popup) + g.BeginPopupStack.pop_back(); + g.CurrentWindowStack.back().StackSizesOnBegin.CompareWithCurrentState(); + g.CurrentWindowStack.pop_back(); + SetCurrentWindow(g.CurrentWindowStack.Size == 0 ? NULL : g.CurrentWindowStack.back().Window); +} + +void ImGui::BringWindowToFocusFront(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(window == window->RootWindow); + + const int cur_order = window->FocusOrder; + IM_ASSERT(g.WindowsFocusOrder[cur_order] == window); + if (g.WindowsFocusOrder.back() == window) + return; + + const int new_order = g.WindowsFocusOrder.Size - 1; + for (int n = cur_order; n < new_order; n++) + { + g.WindowsFocusOrder[n] = g.WindowsFocusOrder[n + 1]; + g.WindowsFocusOrder[n]->FocusOrder--; + IM_ASSERT(g.WindowsFocusOrder[n]->FocusOrder == n); + } + g.WindowsFocusOrder[new_order] = window; + window->FocusOrder = (short)new_order; +} + +void ImGui::BringWindowToDisplayFront(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* current_front_window = g.Windows.back(); + if (current_front_window == window || current_front_window->RootWindow == window) // Cheap early out (could be better) + return; + for (int i = g.Windows.Size - 2; i >= 0; i--) // We can ignore the top-most window + if (g.Windows[i] == window) + { + memmove(&g.Windows[i], &g.Windows[i + 1], (size_t)(g.Windows.Size - i - 1) * sizeof(ImGuiWindow*)); + g.Windows[g.Windows.Size - 1] = window; + break; + } +} + +void ImGui::BringWindowToDisplayBack(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.Windows[0] == window) + return; + for (int i = 0; i < g.Windows.Size; i++) + if (g.Windows[i] == window) + { + memmove(&g.Windows[1], &g.Windows[0], (size_t)i * sizeof(ImGuiWindow*)); + g.Windows[0] = window; + break; + } +} + +void ImGui::BringWindowToDisplayBehind(ImGuiWindow* window, ImGuiWindow* behind_window) +{ + IM_ASSERT(window != NULL && behind_window != NULL); + ImGuiContext& g = *GImGui; + window = window->RootWindow; + behind_window = behind_window->RootWindow; + int pos_wnd = FindWindowDisplayIndex(window); + int pos_beh = FindWindowDisplayIndex(behind_window); + if (pos_wnd < pos_beh) + { + size_t copy_bytes = (pos_beh - pos_wnd - 1) * sizeof(ImGuiWindow*); + memmove(&g.Windows.Data[pos_wnd], &g.Windows.Data[pos_wnd + 1], copy_bytes); + g.Windows[pos_beh - 1] = window; + } + else + { + size_t copy_bytes = (pos_wnd - pos_beh) * sizeof(ImGuiWindow*); + memmove(&g.Windows.Data[pos_beh + 1], &g.Windows.Data[pos_beh], copy_bytes); + g.Windows[pos_beh] = window; + } +} + +int ImGui::FindWindowDisplayIndex(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + return g.Windows.index_from_ptr(g.Windows.find(window)); +} + +// Moving window to front of display and set focus (which happens to be back of our sorted list) +void ImGui::FocusWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + + if (g.NavWindow != window) + { + g.NavWindow = window; + if (window && g.NavDisableMouseHover) + g.NavMousePosDirty = true; + g.NavId = window ? window->NavLastIds[0] : 0; // Restore NavId + g.NavFocusScopeId = 0; + g.NavIdIsAlive = false; + g.NavLayer = ImGuiNavLayer_Main; + g.NavInitRequest = g.NavMoveSubmitted = g.NavMoveScoringItems = false; + NavUpdateAnyRequestFlag(); + //IMGUI_DEBUG_LOG("FocusWindow(\"%s\")\n", window ? window->Name : NULL); + } + + // Close popups if any + ClosePopupsOverWindow(window, false); + + // Move the root window to the top of the pile + IM_ASSERT(window == NULL || window->RootWindow != NULL); + ImGuiWindow* focus_front_window = window ? window->RootWindow : NULL; // NB: In docking branch this is window->RootWindowDockStop + ImGuiWindow* display_front_window = window ? window->RootWindow : NULL; + + // Steal active widgets. Some of the cases it triggers includes: + // - Focus a window while an InputText in another window is active, if focus happens before the old InputText can run. + // - When using Nav to activate menu items (due to timing of activating on press->new window appears->losing ActiveId) + if (g.ActiveId != 0 && g.ActiveIdWindow && g.ActiveIdWindow->RootWindow != focus_front_window) + if (!g.ActiveIdNoClearOnFocusLoss) + ClearActiveID(); + + // Passing NULL allow to disable keyboard focus + if (!window) + return; + + // Bring to front + BringWindowToFocusFront(focus_front_window); + if (((window->Flags | display_front_window->Flags) & ImGuiWindowFlags_NoBringToFrontOnFocus) == 0) + BringWindowToDisplayFront(display_front_window); +} + +void ImGui::FocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, ImGuiWindow* ignore_window) +{ + ImGuiContext& g = *GImGui; + int start_idx = g.WindowsFocusOrder.Size - 1; + if (under_this_window != NULL) + { + // Aim at root window behind us, if we are in a child window that's our own root (see #4640) + int offset = -1; + while (under_this_window->Flags & ImGuiWindowFlags_ChildWindow) + { + under_this_window = under_this_window->ParentWindow; + offset = 0; + } + start_idx = FindWindowFocusIndex(under_this_window) + offset; + } + for (int i = start_idx; i >= 0; i--) + { + // We may later decide to test for different NoXXXInputs based on the active navigation input (mouse vs nav) but that may feel more confusing to the user. + ImGuiWindow* window = g.WindowsFocusOrder[i]; + IM_ASSERT(window == window->RootWindow); + if (window != ignore_window && window->WasActive) + if ((window->Flags & (ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs)) != (ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs)) + { + ImGuiWindow* focus_window = NavRestoreLastChildNavWindow(window); + FocusWindow(focus_window); + return; + } + } + FocusWindow(NULL); +} + +// Important: this alone doesn't alter current ImDrawList state. This is called by PushFont/PopFont only. +void ImGui::SetCurrentFont(ImFont* font) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(font && font->IsLoaded()); // Font Atlas not created. Did you call io.Fonts->GetTexDataAsRGBA32 / GetTexDataAsAlpha8 ? + IM_ASSERT(font->Scale > 0.0f); + g.Font = font; + g.FontBaseSize = ImMax(1.0f, g.IO.FontGlobalScale * g.Font->FontSize * g.Font->Scale); + g.FontSize = g.CurrentWindow ? g.CurrentWindow->CalcFontSize() : 0.0f; + + ImFontAtlas* atlas = g.Font->ContainerAtlas; + g.DrawListSharedData.TexUvWhitePixel = atlas->TexUvWhitePixel; + g.DrawListSharedData.TexUvLines = atlas->TexUvLines; + g.DrawListSharedData.Font = g.Font; + g.DrawListSharedData.FontSize = g.FontSize; +} + +void ImGui::PushFont(ImFont* font) +{ + ImGuiContext& g = *GImGui; + if (!font) + font = GetDefaultFont(); + SetCurrentFont(font); + g.FontStack.push_back(font); + g.CurrentWindow->DrawList->PushTextureID(font->ContainerAtlas->TexID); +} + +void ImGui::PopFont() +{ + ImGuiContext& g = *GImGui; + g.CurrentWindow->DrawList->PopTextureID(); + g.FontStack.pop_back(); + SetCurrentFont(g.FontStack.empty() ? GetDefaultFont() : g.FontStack.back()); +} + +void ImGui::PushItemFlag(ImGuiItemFlags option, bool enabled) +{ + ImGuiContext& g = *GImGui; + ImGuiItemFlags item_flags = g.CurrentItemFlags; + IM_ASSERT(item_flags == g.ItemFlagsStack.back()); + if (enabled) + item_flags |= option; + else + item_flags &= ~option; + g.CurrentItemFlags = item_flags; + g.ItemFlagsStack.push_back(item_flags); +} + +void ImGui::PopItemFlag() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.ItemFlagsStack.Size > 1); // Too many calls to PopItemFlag() - we always leave a 0 at the bottom of the stack. + g.ItemFlagsStack.pop_back(); + g.CurrentItemFlags = g.ItemFlagsStack.back(); +} + +// BeginDisabled()/EndDisabled() +// - Those can be nested but it cannot be used to enable an already disabled section (a single BeginDisabled(true) in the stack is enough to keep everything disabled) +// - Visually this is currently altering alpha, but it is expected that in a future styling system this would work differently. +// - Feedback welcome at https://github.com/ocornut/imgui/issues/211 +// - BeginDisabled(false) essentially does nothing useful but is provided to facilitate use of boolean expressions. If you can avoid calling BeginDisabled(False)/EndDisabled() best to avoid it. +// - Optimized shortcuts instead of PushStyleVar() + PushItemFlag() +void ImGui::BeginDisabled(bool disabled) +{ + ImGuiContext& g = *GImGui; + bool was_disabled = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; + if (!was_disabled && disabled) + { + g.DisabledAlphaBackup = g.Style.Alpha; + g.Style.Alpha *= g.Style.DisabledAlpha; // PushStyleVar(ImGuiStyleVar_Alpha, g.Style.Alpha * g.Style.DisabledAlpha); + } + if (was_disabled || disabled) + g.CurrentItemFlags |= ImGuiItemFlags_Disabled; + g.ItemFlagsStack.push_back(g.CurrentItemFlags); + g.DisabledStackSize++; +} + +void ImGui::EndDisabled() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.DisabledStackSize > 0); + g.DisabledStackSize--; + bool was_disabled = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; + //PopItemFlag(); + g.ItemFlagsStack.pop_back(); + g.CurrentItemFlags = g.ItemFlagsStack.back(); + if (was_disabled && (g.CurrentItemFlags & ImGuiItemFlags_Disabled) == 0) + g.Style.Alpha = g.DisabledAlphaBackup; //PopStyleVar(); +} + +// FIXME: Look into renaming this once we have settled the new Focus/Activation/TabStop system. +void ImGui::PushAllowKeyboardFocus(bool allow_keyboard_focus) +{ + PushItemFlag(ImGuiItemFlags_NoTabStop, !allow_keyboard_focus); +} + +void ImGui::PopAllowKeyboardFocus() +{ + PopItemFlag(); +} + +void ImGui::PushButtonRepeat(bool repeat) +{ + PushItemFlag(ImGuiItemFlags_ButtonRepeat, repeat); +} + +void ImGui::PopButtonRepeat() +{ + PopItemFlag(); +} + +void ImGui::PushTextWrapPos(float wrap_pos_x) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.TextWrapPosStack.push_back(window->DC.TextWrapPos); + window->DC.TextWrapPos = wrap_pos_x; +} + +void ImGui::PopTextWrapPos() +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.TextWrapPos = window->DC.TextWrapPosStack.back(); + window->DC.TextWrapPosStack.pop_back(); +} + +static ImGuiWindow* GetCombinedRootWindow(ImGuiWindow* window, bool popup_hierarchy) +{ + ImGuiWindow* last_window = NULL; + while (last_window != window) + { + last_window = window; + window = window->RootWindow; + if (popup_hierarchy) + window = window->RootWindowPopupTree; + } + return window; +} + +bool ImGui::IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent, bool popup_hierarchy) +{ + ImGuiWindow* window_root = GetCombinedRootWindow(window, popup_hierarchy); + if (window_root == potential_parent) + return true; + while (window != NULL) + { + if (window == potential_parent) + return true; + if (window == window_root) // end of chain + return false; + window = window->ParentWindow; + } + return false; +} + +bool ImGui::IsWindowWithinBeginStackOf(ImGuiWindow* window, ImGuiWindow* potential_parent) +{ + if (window->RootWindow == potential_parent) + return true; + while (window != NULL) + { + if (window == potential_parent) + return true; + window = window->ParentWindowInBeginStack; + } + return false; +} + +bool ImGui::IsWindowAbove(ImGuiWindow* potential_above, ImGuiWindow* potential_below) +{ + ImGuiContext& g = *GImGui; + + // It would be saner to ensure that display layer is always reflected in the g.Windows[] order, which would likely requires altering all manipulations of that array + const int display_layer_delta = GetWindowDisplayLayer(potential_above) - GetWindowDisplayLayer(potential_below); + if (display_layer_delta != 0) + return display_layer_delta > 0; + + for (int i = g.Windows.Size - 1; i >= 0; i--) + { + ImGuiWindow* candidate_window = g.Windows[i]; + if (candidate_window == potential_above) + return true; + if (candidate_window == potential_below) + return false; + } + return false; +} + +bool ImGui::IsWindowHovered(ImGuiHoveredFlags flags) +{ + IM_ASSERT((flags & (ImGuiHoveredFlags_AllowWhenOverlapped | ImGuiHoveredFlags_AllowWhenDisabled)) == 0); // Flags not supported by this function + ImGuiContext& g = *GImGui; + ImGuiWindow* ref_window = g.HoveredWindow; + ImGuiWindow* cur_window = g.CurrentWindow; + if (ref_window == NULL) + return false; + + if ((flags & ImGuiHoveredFlags_AnyWindow) == 0) + { + IM_ASSERT(cur_window); // Not inside a Begin()/End() + const bool popup_hierarchy = (flags & ImGuiHoveredFlags_NoPopupHierarchy) == 0; + if (flags & ImGuiHoveredFlags_RootWindow) + cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy); + + bool result; + if (flags & ImGuiHoveredFlags_ChildWindows) + result = IsWindowChildOf(ref_window, cur_window, popup_hierarchy); + else + result = (ref_window == cur_window); + if (!result) + return false; + } + + if (!IsWindowContentHoverable(ref_window, flags)) + return false; + if (!(flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + if (g.ActiveId != 0 && !g.ActiveIdAllowOverlap && g.ActiveId != ref_window->MoveId) + return false; + return true; +} + +bool ImGui::IsWindowFocused(ImGuiFocusedFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* ref_window = g.NavWindow; + ImGuiWindow* cur_window = g.CurrentWindow; + + if (ref_window == NULL) + return false; + if (flags & ImGuiFocusedFlags_AnyWindow) + return true; + + IM_ASSERT(cur_window); // Not inside a Begin()/End() + const bool popup_hierarchy = (flags & ImGuiFocusedFlags_NoPopupHierarchy) == 0; + if (flags & ImGuiHoveredFlags_RootWindow) + cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy); + + if (flags & ImGuiHoveredFlags_ChildWindows) + return IsWindowChildOf(ref_window, cur_window, popup_hierarchy); + else + return (ref_window == cur_window); +} + +// Can we focus this window with CTRL+TAB (or PadMenu + PadFocusPrev/PadFocusNext) +// Note that NoNavFocus makes the window not reachable with CTRL+TAB but it can still be focused with mouse or programmatically. +// If you want a window to never be focused, you may use the e.g. NoInputs flag. +bool ImGui::IsWindowNavFocusable(ImGuiWindow* window) +{ + return window->WasActive && window == window->RootWindow && !(window->Flags & ImGuiWindowFlags_NoNavFocus); +} + +float ImGui::GetWindowWidth() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Size.x; +} + +float ImGui::GetWindowHeight() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Size.y; +} + +ImVec2 ImGui::GetWindowPos() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + return window->Pos; +} + +void ImGui::SetWindowPos(ImGuiWindow* window, const ImVec2& pos, ImGuiCond cond) +{ + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowPosAllowFlags & cond) == 0) + return; + + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + window->SetWindowPosAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + window->SetWindowPosVal = ImVec2(FLT_MAX, FLT_MAX); + + // Set + const ImVec2 old_pos = window->Pos; + window->Pos = ImFloor(pos); + ImVec2 offset = window->Pos - old_pos; + window->DC.CursorPos += offset; // As we happen to move the window while it is being appended to (which is a bad idea - will smear) let's at least offset the cursor + window->DC.CursorMaxPos += offset; // And more importantly we need to offset CursorMaxPos/CursorStartPos this so ContentSize calculation doesn't get affected. + window->DC.IdealMaxPos += offset; + window->DC.CursorStartPos += offset; +} + +void ImGui::SetWindowPos(const ImVec2& pos, ImGuiCond cond) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + SetWindowPos(window, pos, cond); +} + +void ImGui::SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond) +{ + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowPos(window, pos, cond); +} + +ImVec2 ImGui::GetWindowSize() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Size; +} + +void ImGui::SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond cond) +{ + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowSizeAllowFlags & cond) == 0) + return; + + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + window->SetWindowSizeAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + + // Set + if (size.x > 0.0f) + { + window->AutoFitFramesX = 0; + window->SizeFull.x = IM_FLOOR(size.x); + } + else + { + window->AutoFitFramesX = 2; + window->AutoFitOnlyGrows = false; + } + if (size.y > 0.0f) + { + window->AutoFitFramesY = 0; + window->SizeFull.y = IM_FLOOR(size.y); + } + else + { + window->AutoFitFramesY = 2; + window->AutoFitOnlyGrows = false; + } +} + +void ImGui::SetWindowSize(const ImVec2& size, ImGuiCond cond) +{ + SetWindowSize(GImGui->CurrentWindow, size, cond); +} + +void ImGui::SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond) +{ + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowSize(window, size, cond); +} + +void ImGui::SetWindowCollapsed(ImGuiWindow* window, bool collapsed, ImGuiCond cond) +{ + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowCollapsedAllowFlags & cond) == 0) + return; + window->SetWindowCollapsedAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + + // Set + window->Collapsed = collapsed; +} + +void ImGui::SetWindowHitTestHole(ImGuiWindow* window, const ImVec2& pos, const ImVec2& size) +{ + IM_ASSERT(window->HitTestHoleSize.x == 0); // We don't support multiple holes/hit test filters + window->HitTestHoleSize = ImVec2ih(size); + window->HitTestHoleOffset = ImVec2ih(pos - window->Pos); +} + +void ImGui::SetWindowCollapsed(bool collapsed, ImGuiCond cond) +{ + SetWindowCollapsed(GImGui->CurrentWindow, collapsed, cond); +} + +bool ImGui::IsWindowCollapsed() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Collapsed; +} + +bool ImGui::IsWindowAppearing() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Appearing; +} + +void ImGui::SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond) +{ + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowCollapsed(window, collapsed, cond); +} + +void ImGui::SetWindowFocus() +{ + FocusWindow(GImGui->CurrentWindow); +} + +void ImGui::SetWindowFocus(const char* name) +{ + if (name) + { + if (ImGuiWindow* window = FindWindowByName(name)) + FocusWindow(window); + } + else + { + FocusWindow(NULL); + } +} + +void ImGui::SetNextWindowPos(const ImVec2& pos, ImGuiCond cond, const ImVec2& pivot) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasPos; + g.NextWindowData.PosVal = pos; + g.NextWindowData.PosPivotVal = pivot; + g.NextWindowData.PosCond = cond ? cond : ImGuiCond_Always; +} + +void ImGui::SetNextWindowSize(const ImVec2& size, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasSize; + g.NextWindowData.SizeVal = size; + g.NextWindowData.SizeCond = cond ? cond : ImGuiCond_Always; +} + +void ImGui::SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback, void* custom_callback_user_data) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasSizeConstraint; + g.NextWindowData.SizeConstraintRect = ImRect(size_min, size_max); + g.NextWindowData.SizeCallback = custom_callback; + g.NextWindowData.SizeCallbackUserData = custom_callback_user_data; +} + +// Content size = inner scrollable rectangle, padded with WindowPadding. +// SetNextWindowContentSize(ImVec2(100,100) + ImGuiWindowFlags_AlwaysAutoResize will always allow submitting a 100x100 item. +void ImGui::SetNextWindowContentSize(const ImVec2& size) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasContentSize; + g.NextWindowData.ContentSizeVal = ImFloor(size); +} + +void ImGui::SetNextWindowScroll(const ImVec2& scroll) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasScroll; + g.NextWindowData.ScrollVal = scroll; +} + +void ImGui::SetNextWindowCollapsed(bool collapsed, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasCollapsed; + g.NextWindowData.CollapsedVal = collapsed; + g.NextWindowData.CollapsedCond = cond ? cond : ImGuiCond_Always; +} + +void ImGui::SetNextWindowFocus() +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasFocus; +} + +void ImGui::SetNextWindowBgAlpha(float alpha) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasBgAlpha; + g.NextWindowData.BgAlphaVal = alpha; +} + +ImDrawList* ImGui::GetWindowDrawList() +{ + ImGuiWindow* window = GetCurrentWindow(); + return window->DrawList; +} + +ImFont* ImGui::GetFont() +{ + return GImGui->Font; +} + +float ImGui::GetFontSize() +{ + return GImGui->FontSize; +} + +ImVec2 ImGui::GetFontTexUvWhitePixel() +{ + return GImGui->DrawListSharedData.TexUvWhitePixel; +} + +void ImGui::SetWindowFontScale(float scale) +{ + IM_ASSERT(scale > 0.0f); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + window->FontWindowScale = scale; + g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); +} + +void ImGui::ActivateItem(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.NavNextActivateId = id; + g.NavNextActivateFlags = ImGuiActivateFlags_None; +} + +void ImGui::PushFocusScope(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + g.FocusScopeStack.push_back(window->DC.NavFocusScopeIdCurrent); + window->DC.NavFocusScopeIdCurrent = id; +} + +void ImGui::PopFocusScope() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(g.FocusScopeStack.Size > 0); // Too many PopFocusScope() ? + window->DC.NavFocusScopeIdCurrent = g.FocusScopeStack.back(); + g.FocusScopeStack.pop_back(); +} + +void ImGui::SetKeyboardFocusHere(int offset) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(offset >= -1); // -1 is allowed but not below + g.NavWindow = window; + ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; + NavMoveRequestSubmit(ImGuiDir_None, offset < 0 ? ImGuiDir_Up : ImGuiDir_Down, ImGuiNavMoveFlags_Tabbing | ImGuiNavMoveFlags_FocusApi, scroll_flags); // FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable. + if (offset == -1) + { + NavMoveRequestResolveWithLastItem(&g.NavMoveResultLocal); + } + else + { + g.NavTabbingDir = 1; + g.NavTabbingCounter = offset + 1; + } +} + +void ImGui::SetItemDefaultFocus() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!window->Appearing) + return; + if (g.NavWindow != window->RootWindowForNav || (!g.NavInitRequest && g.NavInitResultId == 0) || g.NavLayer != window->DC.NavLayerCurrent) + return; + + g.NavInitRequest = false; + g.NavInitResultId = g.LastItemData.ID; + g.NavInitResultRectRel = WindowRectAbsToRel(window, g.LastItemData.Rect); + NavUpdateAnyRequestFlag(); + + // Scroll could be done in NavInitRequestApplyResult() via a opt-in flag (we however don't want regular init requests to scroll) + if (!IsItemVisible()) + ScrollToRectEx(window, g.LastItemData.Rect, ImGuiScrollFlags_None); +} + +void ImGui::SetStateStorage(ImGuiStorage* tree) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + window->DC.StateStorage = tree ? tree : &window->StateStorage; +} + +ImGuiStorage* ImGui::GetStateStorage() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->DC.StateStorage; +} + +void ImGui::PushID(const char* str_id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetIDNoKeepAlive(str_id); + window->IDStack.push_back(id); +} + +void ImGui::PushID(const char* str_id_begin, const char* str_id_end) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetIDNoKeepAlive(str_id_begin, str_id_end); + window->IDStack.push_back(id); +} + +void ImGui::PushID(const void* ptr_id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetIDNoKeepAlive(ptr_id); + window->IDStack.push_back(id); +} + +void ImGui::PushID(int int_id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetIDNoKeepAlive(int_id); + window->IDStack.push_back(id); +} + +// Push a given id value ignoring the ID stack as a seed. +void ImGui::PushOverrideID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.DebugHookIdInfo == id) + DebugHookIdInfo(id, ImGuiDataType_ID, NULL, NULL); + window->IDStack.push_back(id); +} + +// Helper to avoid a common series of PushOverrideID -> GetID() -> PopID() call +// (note that when using this pattern, TestEngine's "Stack Tool" will tend to not display the intermediate stack level. +// for that to work we would need to do PushOverrideID() -> ItemAdd() -> PopID() which would alter widget code a little more) +ImGuiID ImGui::GetIDWithSeed(const char* str, const char* str_end, ImGuiID seed) +{ + ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); + KeepAliveID(id); + ImGuiContext& g = *GImGui; + if (g.DebugHookIdInfo == id) + DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); + return id; +} + +void ImGui::PopID() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + IM_ASSERT(window->IDStack.Size > 1); // Too many PopID(), or could be popping in a wrong/different window? + window->IDStack.pop_back(); +} + +ImGuiID ImGui::GetID(const char* str_id) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(str_id); +} + +ImGuiID ImGui::GetID(const char* str_id_begin, const char* str_id_end) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(str_id_begin, str_id_end); +} + +ImGuiID ImGui::GetID(const void* ptr_id) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(ptr_id); +} + +bool ImGui::IsRectVisible(const ImVec2& size) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ClipRect.Overlaps(ImRect(window->DC.CursorPos, window->DC.CursorPos + size)); +} + +bool ImGui::IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ClipRect.Overlaps(ImRect(rect_min, rect_max)); +} + + +//----------------------------------------------------------------------------- +// [SECTION] INPUTS +//----------------------------------------------------------------------------- + +// Test if mouse cursor is hovering given rectangle +// NB- Rectangle is clipped by our current clip setting +// NB- Expand the rectangle to be generous on imprecise inputs systems (g.Style.TouchExtraPadding) +bool ImGui::IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip) +{ + ImGuiContext& g = *GImGui; + + // Clip + ImRect rect_clipped(r_min, r_max); + if (clip) + rect_clipped.ClipWith(g.CurrentWindow->ClipRect); + + // Expand for touch input + const ImRect rect_for_touch(rect_clipped.Min - g.Style.TouchExtraPadding, rect_clipped.Max + g.Style.TouchExtraPadding); + if (!rect_for_touch.Contains(g.IO.MousePos)) + return false; + return true; +} + +ImGuiKeyData* ImGui::GetKeyData(ImGuiKey key) +{ + ImGuiContext& g = *GImGui; + int index; +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + IM_ASSERT(key >= ImGuiKey_LegacyNativeKey_BEGIN && key < ImGuiKey_NamedKey_END); + if (IsLegacyKey(key)) + index = (g.IO.KeyMap[key] != -1) ? g.IO.KeyMap[key] : key; // Remap native->imgui or imgui->native + else + index = key; +#else + IM_ASSERT(IsNamedKey(key) && "Support for user key indices was dropped in favor of ImGuiKey. Please update backend & user code."); + index = key - ImGuiKey_NamedKey_BEGIN; +#endif + return &g.IO.KeysData[index]; +} + +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO +int ImGui::GetKeyIndex(ImGuiKey key) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(IsNamedKey(key)); + const ImGuiKeyData* key_data = GetKeyData(key); + return (int)(key_data - g.IO.KeysData); +} +#endif + +// Those names a provided for debugging purpose and are not meant to be saved persistently not compared. +static const char* const GKeyNames[] = +{ + "Tab", "LeftArrow", "RightArrow", "UpArrow", "DownArrow", "PageUp", "PageDown", + "Home", "End", "Insert", "Delete", "Backspace", "Space", "Enter", "Escape", + "LeftCtrl", "LeftShift", "LeftAlt", "LeftSuper", "RightCtrl", "RightShift", "RightAlt", "RightSuper", "Menu", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", + "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", + "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", + "Apostrophe", "Comma", "Minus", "Period", "Slash", "Semicolon", "Equal", "LeftBracket", + "Backslash", "RightBracket", "GraveAccent", "CapsLock", "ScrollLock", "NumLock", "PrintScreen", + "Pause", "Keypad0", "Keypad1", "Keypad2", "Keypad3", "Keypad4", "Keypad5", "Keypad6", + "Keypad7", "Keypad8", "Keypad9", "KeypadDecimal", "KeypadDivide", "KeypadMultiply", + "KeypadSubtract", "KeypadAdd", "KeypadEnter", "KeypadEqual", + "GamepadStart", "GamepadBack", "GamepadFaceUp", "GamepadFaceDown", "GamepadFaceLeft", "GamepadFaceRight", + "GamepadDpadUp", "GamepadDpadDown", "GamepadDpadLeft", "GamepadDpadRight", + "GamepadL1", "GamepadR1", "GamepadL2", "GamepadR2", "GamepadL3", "GamepadR3", + "GamepadLStickUp", "GamepadLStickDown", "GamepadLStickLeft", "GamepadLStickRight", + "GamepadRStickUp", "GamepadRStickDown", "GamepadRStickLeft", "GamepadRStickRight", + "ModCtrl", "ModShift", "ModAlt", "ModSuper" +}; +IM_STATIC_ASSERT(ImGuiKey_NamedKey_COUNT == IM_ARRAYSIZE(GKeyNames)); + +const char* ImGui::GetKeyName(ImGuiKey key) +{ +#ifdef IMGUI_DISABLE_OBSOLETE_KEYIO + IM_ASSERT((IsNamedKey(key) || key == ImGuiKey_None) && "Support for user key indices was dropped in favor of ImGuiKey. Please update backend and user code."); +#else + if (IsLegacyKey(key)) + { + ImGuiIO& io = GetIO(); + if (io.KeyMap[key] == -1) + return "N/A"; + IM_ASSERT(IsNamedKey((ImGuiKey)io.KeyMap[key])); + key = (ImGuiKey)io.KeyMap[key]; + } +#endif + if (key == ImGuiKey_None) + return "None"; + if (!IsNamedKey(key)) + return "Unknown"; + + return GKeyNames[key - ImGuiKey_NamedKey_BEGIN]; +} + +// Note that Dear ImGui doesn't know the meaning/semantic of ImGuiKey from 0..511: they are legacy native keycodes. +// Consider transitioning from 'IsKeyDown(MY_ENGINE_KEY_A)' (<1.87) to IsKeyDown(ImGuiKey_A) (>= 1.87) +bool ImGui::IsKeyDown(ImGuiKey key) +{ + const ImGuiKeyData* key_data = GetKeyData(key); + return key_data->Down; +} + +// t0 = previous time (e.g.: g.Time - g.IO.DeltaTime) +// t1 = current time (e.g.: g.Time) +// An event is triggered at: +// t = 0.0f t = repeat_delay, t = repeat_delay + repeat_rate*N +int ImGui::CalcTypematicRepeatAmount(float t0, float t1, float repeat_delay, float repeat_rate) +{ + if (t1 == 0.0f) + return 1; + if (t0 >= t1) + return 0; + if (repeat_rate <= 0.0f) + return (t0 < repeat_delay) && (t1 >= repeat_delay); + const int count_t0 = (t0 < repeat_delay) ? -1 : (int)((t0 - repeat_delay) / repeat_rate); + const int count_t1 = (t1 < repeat_delay) ? -1 : (int)((t1 - repeat_delay) / repeat_rate); + const int count = count_t1 - count_t0; + return count; +} + +int ImGui::GetKeyPressedAmount(ImGuiKey key, float repeat_delay, float repeat_rate) +{ + ImGuiContext& g = *GImGui; + const ImGuiKeyData* key_data = GetKeyData(key); + const float t = key_data->DownDuration; + return CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, repeat_delay, repeat_rate); +} + +bool ImGui::IsKeyPressed(ImGuiKey key, bool repeat) +{ + ImGuiContext& g = *GImGui; + const ImGuiKeyData* key_data = GetKeyData(key); + const float t = key_data->DownDuration; + if (t == 0.0f) + return true; + if (repeat && t > g.IO.KeyRepeatDelay) + return GetKeyPressedAmount(key, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0; + return false; +} + +bool ImGui::IsKeyReleased(ImGuiKey key) +{ + const ImGuiKeyData* key_data = GetKeyData(key); + return key_data->DownDurationPrev >= 0.0f && !key_data->Down; +} + +bool ImGui::IsMouseDown(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseDown[button]; +} + +bool ImGui::IsMouseClicked(ImGuiMouseButton button, bool repeat) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + const float t = g.IO.MouseDownDuration[button]; + if (t == 0.0f) + return true; + + if (repeat && t > g.IO.KeyRepeatDelay) + { + // FIXME: 2019/05/03: Our old repeat code was wrong here and led to doubling the repeat rate, which made it an ok rate for repeat on mouse hold. + int amount = CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate * 0.50f); + if (amount > 0) + return true; + } + return false; +} + +bool ImGui::IsMouseReleased(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseReleased[button]; +} + +bool ImGui::IsMouseDoubleClicked(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseClickedCount[button] == 2; +} + +int ImGui::GetMouseClickedCount(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseClickedCount[button]; +} + +// Return if a mouse click/drag went past the given threshold. Valid to call during the MouseReleased frame. +// [Internal] This doesn't test if the button is pressed +bool ImGui::IsMouseDragPastThreshold(ImGuiMouseButton button, float lock_threshold) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (lock_threshold < 0.0f) + lock_threshold = g.IO.MouseDragThreshold; + return g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold; +} + +bool ImGui::IsMouseDragging(ImGuiMouseButton button, float lock_threshold) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (!g.IO.MouseDown[button]) + return false; + return IsMouseDragPastThreshold(button, lock_threshold); +} + +ImVec2 ImGui::GetMousePos() +{ + ImGuiContext& g = *GImGui; + return g.IO.MousePos; +} + +// NB: prefer to call right after BeginPopup(). At the time Selectable/MenuItem is activated, the popup is already closed! +ImVec2 ImGui::GetMousePosOnOpeningCurrentPopup() +{ + ImGuiContext& g = *GImGui; + if (g.BeginPopupStack.Size > 0) + return g.OpenPopupStack[g.BeginPopupStack.Size - 1].OpenMousePos; + return g.IO.MousePos; +} + +// We typically use ImVec2(-FLT_MAX,-FLT_MAX) to denote an invalid mouse position. +bool ImGui::IsMousePosValid(const ImVec2* mouse_pos) +{ + // The assert is only to silence a false-positive in XCode Static Analysis. + // Because GImGui is not dereferenced in every code path, the static analyzer assume that it may be NULL (which it doesn't for other functions). + IM_ASSERT(GImGui != NULL); + const float MOUSE_INVALID = -256000.0f; + ImVec2 p = mouse_pos ? *mouse_pos : GImGui->IO.MousePos; + return p.x >= MOUSE_INVALID && p.y >= MOUSE_INVALID; +} + +// [WILL OBSOLETE] This was designed for backends, but prefer having backend maintain a mask of held mouse buttons, because upcoming input queue system will make this invalid. +bool ImGui::IsAnyMouseDown() +{ + ImGuiContext& g = *GImGui; + for (int n = 0; n < IM_ARRAYSIZE(g.IO.MouseDown); n++) + if (g.IO.MouseDown[n]) + return true; + return false; +} + +// Return the delta from the initial clicking position while the mouse button is clicked or was just released. +// This is locked and return 0.0f until the mouse moves past a distance threshold at least once. +// NB: This is only valid if IsMousePosValid(). backends in theory should always keep mouse position valid when dragging even outside the client window. +ImVec2 ImGui::GetMouseDragDelta(ImGuiMouseButton button, float lock_threshold) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (lock_threshold < 0.0f) + lock_threshold = g.IO.MouseDragThreshold; + if (g.IO.MouseDown[button] || g.IO.MouseReleased[button]) + if (g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold) + if (IsMousePosValid(&g.IO.MousePos) && IsMousePosValid(&g.IO.MouseClickedPos[button])) + return g.IO.MousePos - g.IO.MouseClickedPos[button]; + return ImVec2(0.0f, 0.0f); +} + +void ImGui::ResetMouseDragDelta(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + // NB: We don't need to reset g.IO.MouseDragMaxDistanceSqr + g.IO.MouseClickedPos[button] = g.IO.MousePos; +} + +ImGuiMouseCursor ImGui::GetMouseCursor() +{ + ImGuiContext& g = *GImGui; + return g.MouseCursor; +} + +void ImGui::SetMouseCursor(ImGuiMouseCursor cursor_type) +{ + ImGuiContext& g = *GImGui; + g.MouseCursor = cursor_type; +} + +void ImGui::CaptureKeyboardFromApp(bool capture) +{ + ImGuiContext& g = *GImGui; + g.WantCaptureKeyboardNextFrame = capture ? 1 : 0; +} + +void ImGui::CaptureMouseFromApp(bool capture) +{ + ImGuiContext& g = *GImGui; + g.WantCaptureMouseNextFrame = capture ? 1 : 0; +} + +static const char* GetInputSourceName(ImGuiInputSource source) +{ + const char* input_source_names[] = { "None", "Mouse", "Keyboard", "Gamepad", "Nav", "Clipboard" }; + IM_ASSERT(IM_ARRAYSIZE(input_source_names) == ImGuiInputSource_COUNT && source >= 0 && source < ImGuiInputSource_COUNT); + return input_source_names[source]; +} + + +// Process input queue +// - trickle_fast_inputs = false : process all events, turn into flattened input state (e.g. successive down/up/down/up will be lost) +// - trickle_fast_inputs = true : process as many events as possible (successive down/up/down/up will be trickled over several frames so nothing is lost) (new feature in 1.87) +void ImGui::UpdateInputEvents(bool trickle_fast_inputs) +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + bool mouse_moved = false, mouse_wheeled = false, key_changed = false, text_inputed = false; + int mouse_button_changed = 0x00; + ImBitArray key_changed_mask; + + int event_n = 0; + for (; event_n < g.InputEventsQueue.Size; event_n++) + { + const ImGuiInputEvent* e = &g.InputEventsQueue[event_n]; + if (e->Type == ImGuiInputEventType_MousePos) + { + ImVec2 event_pos(e->MousePos.PosX, e->MousePos.PosY); + if (IsMousePosValid(&event_pos)) + event_pos = ImVec2(ImFloorSigned(event_pos.x), ImFloorSigned(event_pos.y)); // Apply same flooring as UpdateMouseInputs() + if (io.MousePos.x != event_pos.x || io.MousePos.y != event_pos.y) + { + // Trickling Rule: Stop processing queued events if we already handled a mouse button change + if (trickle_fast_inputs && (mouse_button_changed != 0 || mouse_wheeled || key_changed || text_inputed)) + break; + io.MousePos = event_pos; + mouse_moved = true; + } + } + else if (e->Type == ImGuiInputEventType_MouseButton) + { + const ImGuiMouseButton button = e->MouseButton.Button; + IM_ASSERT(button >= 0 && button < ImGuiMouseButton_COUNT); + if (io.MouseDown[button] != e->MouseButton.Down) + { + // Trickling Rule: Stop processing queued events if we got multiple action on the same button + if (trickle_fast_inputs && ((mouse_button_changed & (1 << button)) || mouse_wheeled)) + break; + io.MouseDown[button] = e->MouseButton.Down; + mouse_button_changed |= (1 << button); + } + } + else if (e->Type == ImGuiInputEventType_MouseWheel) + { + if (e->MouseWheel.WheelX != 0.0f || e->MouseWheel.WheelY != 0.0f) + { + // Trickling Rule: Stop processing queued events if we got multiple action on the event + if (trickle_fast_inputs && (mouse_wheeled || mouse_button_changed != 0)) + break; + io.MouseWheelH += e->MouseWheel.WheelX; + io.MouseWheel += e->MouseWheel.WheelY; + mouse_wheeled = true; + } + } + else if (e->Type == ImGuiInputEventType_Key) + { + ImGuiKey key = e->Key.Key; + IM_ASSERT(key != ImGuiKey_None); + const int keydata_index = (key - ImGuiKey_KeysData_OFFSET); + ImGuiKeyData* keydata = &io.KeysData[keydata_index]; + if (keydata->Down != e->Key.Down || keydata->AnalogValue != e->Key.AnalogValue) + { + // Trickling Rule: Stop processing queued events if we got multiple action on the same button + if (trickle_fast_inputs && keydata->Down != e->Key.Down && (key_changed_mask.TestBit(keydata_index) || text_inputed || mouse_button_changed != 0)) + break; + keydata->Down = e->Key.Down; + keydata->AnalogValue = e->Key.AnalogValue; + key_changed = true; + key_changed_mask.SetBit(keydata_index); + + if (key == ImGuiKey_ModCtrl || key == ImGuiKey_ModShift || key == ImGuiKey_ModAlt || key == ImGuiKey_ModSuper) + { + if (key == ImGuiKey_ModCtrl) { io.KeyCtrl = keydata->Down; } + if (key == ImGuiKey_ModShift) { io.KeyShift = keydata->Down; } + if (key == ImGuiKey_ModAlt) { io.KeyAlt = keydata->Down; } + if (key == ImGuiKey_ModSuper) { io.KeySuper = keydata->Down; } + io.KeyMods = GetMergedKeyModFlags(); + } + } + } + else if (e->Type == ImGuiInputEventType_Char) + { + // Trickling Rule: Stop processing queued events if keys/mouse have been interacted with + if (trickle_fast_inputs && (key_changed || mouse_button_changed != 0 || mouse_moved || mouse_wheeled)) + break; + unsigned int c = e->Text.Char; + io.InputQueueCharacters.push_back(c <= IM_UNICODE_CODEPOINT_MAX ? (ImWchar)c : IM_UNICODE_CODEPOINT_INVALID); + text_inputed = true; + } + else if (e->Type == ImGuiInputEventType_Focus) + { + // We intentionally overwrite this and process lower, in order to give a chance + // to multi-viewports backends to queue AddFocusEvent(false) + AddFocusEvent(true) in same frame. + io.AppFocusLost = !e->AppFocused.Focused; + } + else + { + IM_ASSERT(0 && "Unknown event!"); + } + } + + // Record trail (for domain-specific applications wanting to access a precise trail) + //if (event_n != 0) IMGUI_DEBUG_LOG("Processed: %d / Remaining: %d\n", event_n, g.InputEventsQueue.Size - event_n); + for (int n = 0; n < event_n; n++) + g.InputEventsTrail.push_back(g.InputEventsQueue[n]); + + // Remaining events will be processed on the next frame + if (event_n == g.InputEventsQueue.Size) + g.InputEventsQueue.resize(0); + else + g.InputEventsQueue.erase(g.InputEventsQueue.Data, g.InputEventsQueue.Data + event_n); + + // Clear buttons state when focus is lost + // (this is useful so e.g. releasing Alt after focus loss on Alt-Tab doesn't trigger the Alt menu toggle) + if (g.IO.AppFocusLost) + { + g.IO.ClearInputKeys(); + g.IO.AppFocusLost = false; + } +} + + +//----------------------------------------------------------------------------- +// [SECTION] ERROR CHECKING +//----------------------------------------------------------------------------- + +// Helper function to verify ABI compatibility between caller code and compiled version of Dear ImGui. +// Verify that the type sizes are matching between the calling file's compilation unit and imgui.cpp's compilation unit +// If the user has inconsistent compilation settings, imgui configuration #define, packing pragma, etc. your user code +// may see different structures than what imgui.cpp sees, which is problematic. +// We usually require settings to be in imconfig.h to make sure that they are accessible to all compilation units involved with Dear ImGui. +bool ImGui::DebugCheckVersionAndDataLayout(const char* version, size_t sz_io, size_t sz_style, size_t sz_vec2, size_t sz_vec4, size_t sz_vert, size_t sz_idx) +{ + bool error = false; + if (strcmp(version, IMGUI_VERSION) != 0) { error = true; IM_ASSERT(strcmp(version, IMGUI_VERSION) == 0 && "Mismatched version string!"); } + if (sz_io != sizeof(ImGuiIO)) { error = true; IM_ASSERT(sz_io == sizeof(ImGuiIO) && "Mismatched struct layout!"); } + if (sz_style != sizeof(ImGuiStyle)) { error = true; IM_ASSERT(sz_style == sizeof(ImGuiStyle) && "Mismatched struct layout!"); } + if (sz_vec2 != sizeof(ImVec2)) { error = true; IM_ASSERT(sz_vec2 == sizeof(ImVec2) && "Mismatched struct layout!"); } + if (sz_vec4 != sizeof(ImVec4)) { error = true; IM_ASSERT(sz_vec4 == sizeof(ImVec4) && "Mismatched struct layout!"); } + if (sz_vert != sizeof(ImDrawVert)) { error = true; IM_ASSERT(sz_vert == sizeof(ImDrawVert) && "Mismatched struct layout!"); } + if (sz_idx != sizeof(ImDrawIdx)) { error = true; IM_ASSERT(sz_idx == sizeof(ImDrawIdx) && "Mismatched struct layout!"); } + return !error; +} + +static void ImGui::ErrorCheckNewFrameSanityChecks() +{ + ImGuiContext& g = *GImGui; + + // Check user IM_ASSERT macro + // (IF YOU GET A WARNING OR COMPILE ERROR HERE: it means your assert macro is incorrectly defined! + // If your macro uses multiple statements, it NEEDS to be surrounded by a 'do { ... } while (0)' block. + // This is a common C/C++ idiom to allow multiple statements macros to be used in control flow blocks.) + // #define IM_ASSERT(EXPR) if (SomeCode(EXPR)) SomeMoreCode(); // Wrong! + // #define IM_ASSERT(EXPR) do { if (SomeCode(EXPR)) SomeMoreCode(); } while (0) // Correct! + if (true) IM_ASSERT(1); else IM_ASSERT(0); + + // Check user data + // (We pass an error message in the assert expression to make it visible to programmers who are not using a debugger, as most assert handlers display their argument) + IM_ASSERT(g.Initialized); + IM_ASSERT((g.IO.DeltaTime > 0.0f || g.FrameCount == 0) && "Need a positive DeltaTime!"); + IM_ASSERT((g.FrameCount == 0 || g.FrameCountEnded == g.FrameCount) && "Forgot to call Render() or EndFrame() at the end of the previous frame?"); + IM_ASSERT(g.IO.DisplaySize.x >= 0.0f && g.IO.DisplaySize.y >= 0.0f && "Invalid DisplaySize value!"); + IM_ASSERT(g.IO.Fonts->IsBuilt() && "Font Atlas not built! Make sure you called ImGui_ImplXXXX_NewFrame() function for renderer backend, which should call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8()"); + IM_ASSERT(g.Style.CurveTessellationTol > 0.0f && "Invalid style setting!"); + IM_ASSERT(g.Style.CircleTessellationMaxError > 0.0f && "Invalid style setting!"); + IM_ASSERT(g.Style.Alpha >= 0.0f && g.Style.Alpha <= 1.0f && "Invalid style setting!"); // Allows us to avoid a few clamps in color computations + IM_ASSERT(g.Style.WindowMinSize.x >= 1.0f && g.Style.WindowMinSize.y >= 1.0f && "Invalid style setting."); + IM_ASSERT(g.Style.WindowMenuButtonPosition == ImGuiDir_None || g.Style.WindowMenuButtonPosition == ImGuiDir_Left || g.Style.WindowMenuButtonPosition == ImGuiDir_Right); +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + for (int n = ImGuiKey_NamedKey_BEGIN; n < ImGuiKey_COUNT; n++) + IM_ASSERT(g.IO.KeyMap[n] >= -1 && g.IO.KeyMap[n] < IM_ARRAYSIZE(g.IO.KeysDown) && "io.KeyMap[] contains an out of bound value (need to be 0..511, or -1 for unmapped key)"); + + // Check: required key mapping (we intentionally do NOT check all keys to not pressure user into setting up everything, but Space is required and was only added in 1.60 WIP) + if ((g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) && g.IO.BackendUsingLegacyKeyArrays == 1) + IM_ASSERT(g.IO.KeyMap[ImGuiKey_Space] != -1 && "ImGuiKey_Space is not mapped, required for keyboard navigation."); +#endif + + // Check: the io.ConfigWindowsResizeFromEdges option requires backend to honor mouse cursor changes and set the ImGuiBackendFlags_HasMouseCursors flag accordingly. + if (g.IO.ConfigWindowsResizeFromEdges && !(g.IO.BackendFlags & ImGuiBackendFlags_HasMouseCursors)) + g.IO.ConfigWindowsResizeFromEdges = false; +} + +static void ImGui::ErrorCheckEndFrameSanityChecks() +{ + ImGuiContext& g = *GImGui; + + // Verify that io.KeyXXX fields haven't been tampered with. Key mods should not be modified between NewFrame() and EndFrame() + // One possible reason leading to this assert is that your backends update inputs _AFTER_ NewFrame(). + // It is known that when some modal native windows called mid-frame takes focus away, some backends such as GLFW will + // send key release events mid-frame. This would normally trigger this assertion and lead to sheared inputs. + // We silently accommodate for this case by ignoring/ the case where all io.KeyXXX modifiers were released (aka key_mod_flags == 0), + // while still correctly asserting on mid-frame key press events. + const ImGuiKeyModFlags key_mod_flags = GetMergedKeyModFlags(); + IM_ASSERT((key_mod_flags == 0 || g.IO.KeyMods == key_mod_flags) && "Mismatching io.KeyCtrl/io.KeyShift/io.KeyAlt/io.KeySuper vs io.KeyMods"); + IM_UNUSED(key_mod_flags); + + // Recover from errors + //ErrorCheckEndFrameRecover(); + + // Report when there is a mismatch of Begin/BeginChild vs End/EndChild calls. Important: Remember that the Begin/BeginChild API requires you + // to always call End/EndChild even if Begin/BeginChild returns false! (this is unfortunately inconsistent with most other Begin* API). + if (g.CurrentWindowStack.Size != 1) + { + if (g.CurrentWindowStack.Size > 1) + { + IM_ASSERT_USER_ERROR(g.CurrentWindowStack.Size == 1, "Mismatched Begin/BeginChild vs End/EndChild calls: did you forget to call End/EndChild?"); + while (g.CurrentWindowStack.Size > 1) + End(); + } + else + { + IM_ASSERT_USER_ERROR(g.CurrentWindowStack.Size == 1, "Mismatched Begin/BeginChild vs End/EndChild calls: did you call End/EndChild too much?"); + } + } + + IM_ASSERT_USER_ERROR(g.GroupStack.Size == 0, "Missing EndGroup call!"); +} + +// Experimental recovery from incorrect usage of BeginXXX/EndXXX/PushXXX/PopXXX calls. +// Must be called during or before EndFrame(). +// This is generally flawed as we are not necessarily End/Popping things in the right order. +// FIXME: Can't recover from inside BeginTabItem/EndTabItem yet. +// FIXME: Can't recover from interleaved BeginTabBar/Begin +void ImGui::ErrorCheckEndFrameRecover(ImGuiErrorLogCallback log_callback, void* user_data) +{ + // PVS-Studio V1044 is "Loop break conditions do not depend on the number of iterations" + ImGuiContext& g = *GImGui; + while (g.CurrentWindowStack.Size > 0) //-V1044 + { + ErrorCheckEndWindowRecover(log_callback, user_data); + ImGuiWindow* window = g.CurrentWindow; + if (g.CurrentWindowStack.Size == 1) + { + IM_ASSERT(window->IsFallbackWindow); + break; + } + IM_ASSERT(window == g.CurrentWindow); + if (window->Flags & ImGuiWindowFlags_ChildWindow) + { + if (log_callback) log_callback(user_data, "Recovered from missing EndChild() for '%s'", window->Name); + EndChild(); + } + else + { + if (log_callback) log_callback(user_data, "Recovered from missing End() for '%s'", window->Name); + End(); + } + } +} + +// Must be called before End()/EndChild() +void ImGui::ErrorCheckEndWindowRecover(ImGuiErrorLogCallback log_callback, void* user_data) +{ + ImGuiContext& g = *GImGui; + while (g.CurrentTable && (g.CurrentTable->OuterWindow == g.CurrentWindow || g.CurrentTable->InnerWindow == g.CurrentWindow)) + { + if (log_callback) log_callback(user_data, "Recovered from missing EndTable() in '%s'", g.CurrentTable->OuterWindow->Name); + EndTable(); + } + + ImGuiWindow* window = g.CurrentWindow; + ImGuiStackSizes* stack_sizes = &g.CurrentWindowStack.back().StackSizesOnBegin; + IM_ASSERT(window != NULL); + while (g.CurrentTabBar != NULL) //-V1044 + { + if (log_callback) log_callback(user_data, "Recovered from missing EndTabBar() in '%s'", window->Name); + EndTabBar(); + } + while (window->DC.TreeDepth > 0) + { + if (log_callback) log_callback(user_data, "Recovered from missing TreePop() in '%s'", window->Name); + TreePop(); + } + while (g.GroupStack.Size > stack_sizes->SizeOfGroupStack) //-V1044 + { + if (log_callback) log_callback(user_data, "Recovered from missing EndGroup() in '%s'", window->Name); + EndGroup(); + } + while (window->IDStack.Size > 1) + { + if (log_callback) log_callback(user_data, "Recovered from missing PopID() in '%s'", window->Name); + PopID(); + } + while (g.DisabledStackSize > stack_sizes->SizeOfDisabledStack) //-V1044 + { + if (log_callback) log_callback(user_data, "Recovered from missing EndDisabled() in '%s'", window->Name); + EndDisabled(); + } + while (g.ColorStack.Size > stack_sizes->SizeOfColorStack) + { + if (log_callback) log_callback(user_data, "Recovered from missing PopStyleColor() in '%s' for ImGuiCol_%s", window->Name, GetStyleColorName(g.ColorStack.back().Col)); + PopStyleColor(); + } + while (g.ItemFlagsStack.Size > stack_sizes->SizeOfItemFlagsStack) //-V1044 + { + if (log_callback) log_callback(user_data, "Recovered from missing PopItemFlag() in '%s'", window->Name); + PopItemFlag(); + } + while (g.StyleVarStack.Size > stack_sizes->SizeOfStyleVarStack) //-V1044 + { + if (log_callback) log_callback(user_data, "Recovered from missing PopStyleVar() in '%s'", window->Name); + PopStyleVar(); + } + while (g.FocusScopeStack.Size > stack_sizes->SizeOfFocusScopeStack) //-V1044 + { + if (log_callback) log_callback(user_data, "Recovered from missing PopFocusScope() in '%s'", window->Name); + PopFocusScope(); + } +} + +// Save current stack sizes for later compare +void ImGuiStackSizes::SetToCurrentState() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + SizeOfIDStack = (short)window->IDStack.Size; + SizeOfColorStack = (short)g.ColorStack.Size; + SizeOfStyleVarStack = (short)g.StyleVarStack.Size; + SizeOfFontStack = (short)g.FontStack.Size; + SizeOfFocusScopeStack = (short)g.FocusScopeStack.Size; + SizeOfGroupStack = (short)g.GroupStack.Size; + SizeOfItemFlagsStack = (short)g.ItemFlagsStack.Size; + SizeOfBeginPopupStack = (short)g.BeginPopupStack.Size; + SizeOfDisabledStack = (short)g.DisabledStackSize; +} + +// Compare to detect usage errors +void ImGuiStackSizes::CompareWithCurrentState() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_UNUSED(window); + + // Window stacks + // NOT checking: DC.ItemWidth, DC.TextWrapPos (per window) to allow user to conveniently push once and not pop (they are cleared on Begin) + IM_ASSERT(SizeOfIDStack == window->IDStack.Size && "PushID/PopID or TreeNode/TreePop Mismatch!"); + + // Global stacks + // For color, style and font stacks there is an incentive to use Push/Begin/Pop/.../End patterns, so we relax our checks a little to allow them. + IM_ASSERT(SizeOfGroupStack == g.GroupStack.Size && "BeginGroup/EndGroup Mismatch!"); + IM_ASSERT(SizeOfBeginPopupStack == g.BeginPopupStack.Size && "BeginPopup/EndPopup or BeginMenu/EndMenu Mismatch!"); + IM_ASSERT(SizeOfDisabledStack == g.DisabledStackSize && "BeginDisabled/EndDisabled Mismatch!"); + IM_ASSERT(SizeOfItemFlagsStack >= g.ItemFlagsStack.Size && "PushItemFlag/PopItemFlag Mismatch!"); + IM_ASSERT(SizeOfColorStack >= g.ColorStack.Size && "PushStyleColor/PopStyleColor Mismatch!"); + IM_ASSERT(SizeOfStyleVarStack >= g.StyleVarStack.Size && "PushStyleVar/PopStyleVar Mismatch!"); + IM_ASSERT(SizeOfFontStack >= g.FontStack.Size && "PushFont/PopFont Mismatch!"); + IM_ASSERT(SizeOfFocusScopeStack == g.FocusScopeStack.Size && "PushFocusScope/PopFocusScope Mismatch!"); +} + + +//----------------------------------------------------------------------------- +// [SECTION] LAYOUT +//----------------------------------------------------------------------------- +// - ItemSize() +// - ItemAdd() +// - SameLine() +// - GetCursorScreenPos() +// - SetCursorScreenPos() +// - GetCursorPos(), GetCursorPosX(), GetCursorPosY() +// - SetCursorPos(), SetCursorPosX(), SetCursorPosY() +// - GetCursorStartPos() +// - Indent() +// - Unindent() +// - SetNextItemWidth() +// - PushItemWidth() +// - PushMultiItemsWidths() +// - PopItemWidth() +// - CalcItemWidth() +// - CalcItemSize() +// - GetTextLineHeight() +// - GetTextLineHeightWithSpacing() +// - GetFrameHeight() +// - GetFrameHeightWithSpacing() +// - GetContentRegionMax() +// - GetContentRegionMaxAbs() [Internal] +// - GetContentRegionAvail(), +// - GetWindowContentRegionMin(), GetWindowContentRegionMax() +// - BeginGroup() +// - EndGroup() +// Also see in imgui_widgets: tab bars, columns. +//----------------------------------------------------------------------------- + +// Advance cursor given item size for layout. +// Register minimum needed size so it can extend the bounding box used for auto-fit calculation. +// See comments in ItemAdd() about how/why the size provided to ItemSize() vs ItemAdd() may often different. +void ImGui::ItemSize(const ImVec2& size, float text_baseline_y) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + // We increase the height in this function to accommodate for baseline offset. + // In theory we should be offsetting the starting position (window->DC.CursorPos), that will be the topic of a larger refactor, + // but since ItemSize() is not yet an API that moves the cursor (to handle e.g. wrapping) enlarging the height has the same effect. + const float offset_to_match_baseline_y = (text_baseline_y >= 0) ? ImMax(0.0f, window->DC.CurrLineTextBaseOffset - text_baseline_y) : 0.0f; + const float line_height = ImMax(window->DC.CurrLineSize.y, size.y + offset_to_match_baseline_y); + + // Always align ourselves on pixel boundaries + //if (g.IO.KeyAlt) window->DrawList->AddRect(window->DC.CursorPos, window->DC.CursorPos + ImVec2(size.x, line_height), IM_COL32(255,0,0,200)); // [DEBUG] + window->DC.CursorPosPrevLine.x = window->DC.CursorPos.x + size.x; + window->DC.CursorPosPrevLine.y = window->DC.CursorPos.y; + window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); // Next line + window->DC.CursorPos.y = IM_FLOOR(window->DC.CursorPos.y + line_height + g.Style.ItemSpacing.y); // Next line + window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPosPrevLine.x); + window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y - g.Style.ItemSpacing.y); + //if (g.IO.KeyAlt) window->DrawList->AddCircle(window->DC.CursorMaxPos, 3.0f, IM_COL32(255,0,0,255), 4); // [DEBUG] + + window->DC.PrevLineSize.y = line_height; + window->DC.CurrLineSize.y = 0.0f; + window->DC.PrevLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, text_baseline_y); + window->DC.CurrLineTextBaseOffset = 0.0f; + + // Horizontal layout mode + if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) + SameLine(); +} + +void ImGui::ItemSize(const ImRect& bb, float text_baseline_y) +{ + ItemSize(bb.GetSize(), text_baseline_y); +} + +// Declare item bounding box for clipping and interaction. +// Note that the size can be different than the one provided to ItemSize(). Typically, widgets that spread over available surface +// declare their minimum size requirement to ItemSize() and provide a larger region to ItemAdd() which is used drawing/interaction. +bool ImGui::ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb_arg, ImGuiItemFlags extra_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Set item data + // (DisplayRect is left untouched, made valid when ImGuiItemStatusFlags_HasDisplayRect is set) + g.LastItemData.ID = id; + g.LastItemData.Rect = bb; + g.LastItemData.NavRect = nav_bb_arg ? *nav_bb_arg : bb; + g.LastItemData.InFlags = g.CurrentItemFlags | extra_flags; + g.LastItemData.StatusFlags = ImGuiItemStatusFlags_None; + + // Directional navigation processing + if (id != 0) + { + // Runs prior to clipping early-out + // (a) So that NavInitRequest can be honored, for newly opened windows to select a default widget + // (b) So that we can scroll up/down past clipped items. This adds a small O(N) cost to regular navigation requests + // unfortunately, but it is still limited to one window. It may not scale very well for windows with ten of + // thousands of item, but at least NavMoveRequest is only set on user interaction, aka maximum once a frame. + // We could early out with "if (is_clipped && !g.NavInitRequest) return false;" but when we wouldn't be able + // to reach unclipped widgets. This would work if user had explicit scrolling control (e.g. mapped on a stick). + // We intentionally don't check if g.NavWindow != NULL because g.NavAnyRequest should only be set when it is non null. + // If we crash on a NULL g.NavWindow we need to fix the bug elsewhere. + window->DC.NavLayersActiveMaskNext |= (1 << window->DC.NavLayerCurrent); + if (g.NavId == id || g.NavAnyRequest) + if (g.NavWindow->RootWindowForNav == window->RootWindowForNav) + if (window == g.NavWindow || ((window->Flags | g.NavWindow->Flags) & ImGuiWindowFlags_NavFlattened)) + NavProcessItem(); + + // [DEBUG] People keep stumbling on this problem and using "" as identifier in the root of a window instead of "##something". + // Empty identifier are valid and useful in a small amount of cases, but 99.9% of the time you want to use "##something". + // READ THE FAQ: https://dearimgui.org/faq + IM_ASSERT(id != window->ID && "Cannot have an empty ID at the root of a window. If you need an empty label, use ## and read the FAQ about how the ID Stack works!"); + + // [DEBUG] Item Picker tool, when enabling the "extended" version we perform the check in ItemAdd() +#ifdef IMGUI_DEBUG_TOOL_ITEM_PICKER_EX + if (id == g.DebugItemPickerBreakId) + { + IM_DEBUG_BREAK(); + g.DebugItemPickerBreakId = 0; + } +#endif + } + g.NextItemData.Flags = ImGuiNextItemDataFlags_None; + +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (id != 0) + IMGUI_TEST_ENGINE_ITEM_ADD(nav_bb_arg ? *nav_bb_arg : bb, id); +#endif + + // Clipping test + const bool is_clipped = IsClippedEx(bb, id); + if (is_clipped) + return false; + //if (g.IO.KeyAlt) window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255,255,0,120)); // [DEBUG] + + // We need to calculate this now to take account of the current clipping rectangle (as items like Selectable may change them) + if (IsMouseHoveringRect(bb.Min, bb.Max)) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; + return true; +} + +// Gets back to previous line and continue with horizontal layout +// offset_from_start_x == 0 : follow right after previous item +// offset_from_start_x != 0 : align to specified x position (relative to window/group left) +// spacing_w < 0 : use default spacing if pos_x == 0, no spacing if pos_x != 0 +// spacing_w >= 0 : enforce spacing amount +void ImGui::SameLine(float offset_from_start_x, float spacing_w) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + if (offset_from_start_x != 0.0f) + { + if (spacing_w < 0.0f) spacing_w = 0.0f; + window->DC.CursorPos.x = window->Pos.x - window->Scroll.x + offset_from_start_x + spacing_w + window->DC.GroupOffset.x + window->DC.ColumnsOffset.x; + window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; + } + else + { + if (spacing_w < 0.0f) spacing_w = g.Style.ItemSpacing.x; + window->DC.CursorPos.x = window->DC.CursorPosPrevLine.x + spacing_w; + window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; + } + window->DC.CurrLineSize = window->DC.PrevLineSize; + window->DC.CurrLineTextBaseOffset = window->DC.PrevLineTextBaseOffset; +} + +ImVec2 ImGui::GetCursorScreenPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos; +} + +void ImGui::SetCursorScreenPos(const ImVec2& pos) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos = pos; + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); +} + +// User generally sees positions in window coordinates. Internally we store CursorPos in absolute screen coordinates because it is more convenient. +// Conversion happens as we pass the value to user, but it makes our naming convention confusing because GetCursorPos() == (DC.CursorPos - window.Pos). May want to rename 'DC.CursorPos'. +ImVec2 ImGui::GetCursorPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos - window->Pos + window->Scroll; +} + +float ImGui::GetCursorPosX() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos.x - window->Pos.x + window->Scroll.x; +} + +float ImGui::GetCursorPosY() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos.y - window->Pos.y + window->Scroll.y; +} + +void ImGui::SetCursorPos(const ImVec2& local_pos) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos = window->Pos - window->Scroll + local_pos; + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); +} + +void ImGui::SetCursorPosX(float x) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos.x = window->Pos.x - window->Scroll.x + x; + window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPos.x); +} + +void ImGui::SetCursorPosY(float y) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos.y = window->Pos.y - window->Scroll.y + y; + window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y); +} + +ImVec2 ImGui::GetCursorStartPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorStartPos - window->Pos; +} + +void ImGui::Indent(float indent_w) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + window->DC.Indent.x += (indent_w != 0.0f) ? indent_w : g.Style.IndentSpacing; + window->DC.CursorPos.x = window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x; +} + +void ImGui::Unindent(float indent_w) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + window->DC.Indent.x -= (indent_w != 0.0f) ? indent_w : g.Style.IndentSpacing; + window->DC.CursorPos.x = window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x; +} + +// Affect large frame+labels widgets only. +void ImGui::SetNextItemWidth(float item_width) +{ + ImGuiContext& g = *GImGui; + g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasWidth; + g.NextItemData.Width = item_width; +} + +// FIXME: Remove the == 0.0f behavior? +void ImGui::PushItemWidth(float item_width) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DC.ItemWidthStack.push_back(window->DC.ItemWidth); // Backup current width + window->DC.ItemWidth = (item_width == 0.0f ? window->ItemWidthDefault : item_width); + g.NextItemData.Flags &= ~ImGuiNextItemDataFlags_HasWidth; +} + +void ImGui::PushMultiItemsWidths(int components, float w_full) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const ImGuiStyle& style = g.Style; + const float w_item_one = ImMax(1.0f, IM_FLOOR((w_full - (style.ItemInnerSpacing.x) * (components - 1)) / (float)components)); + const float w_item_last = ImMax(1.0f, IM_FLOOR(w_full - (w_item_one + style.ItemInnerSpacing.x) * (components - 1))); + window->DC.ItemWidthStack.push_back(window->DC.ItemWidth); // Backup current width + window->DC.ItemWidthStack.push_back(w_item_last); + for (int i = 0; i < components - 2; i++) + window->DC.ItemWidthStack.push_back(w_item_one); + window->DC.ItemWidth = (components == 1) ? w_item_last : w_item_one; + g.NextItemData.Flags &= ~ImGuiNextItemDataFlags_HasWidth; +} + +void ImGui::PopItemWidth() +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.ItemWidth = window->DC.ItemWidthStack.back(); + window->DC.ItemWidthStack.pop_back(); +} + +// Calculate default item width given value passed to PushItemWidth() or SetNextItemWidth(). +// The SetNextItemWidth() data is generally cleared/consumed by ItemAdd() or NextItemData.ClearFlags() +float ImGui::CalcItemWidth() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float w; + if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasWidth) + w = g.NextItemData.Width; + else + w = window->DC.ItemWidth; + if (w < 0.0f) + { + float region_max_x = GetContentRegionMaxAbs().x; + w = ImMax(1.0f, region_max_x - window->DC.CursorPos.x + w); + } + w = IM_FLOOR(w); + return w; +} + +// [Internal] Calculate full item size given user provided 'size' parameter and default width/height. Default width is often == CalcItemWidth(). +// Those two functions CalcItemWidth vs CalcItemSize are awkwardly named because they are not fully symmetrical. +// Note that only CalcItemWidth() is publicly exposed. +// The 4.0f here may be changed to match CalcItemWidth() and/or BeginChild() (right now we have a mismatch which is harmless but undesirable) +ImVec2 ImGui::CalcItemSize(ImVec2 size, float default_w, float default_h) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + + ImVec2 region_max; + if (size.x < 0.0f || size.y < 0.0f) + region_max = GetContentRegionMaxAbs(); + + if (size.x == 0.0f) + size.x = default_w; + else if (size.x < 0.0f) + size.x = ImMax(4.0f, region_max.x - window->DC.CursorPos.x + size.x); + + if (size.y == 0.0f) + size.y = default_h; + else if (size.y < 0.0f) + size.y = ImMax(4.0f, region_max.y - window->DC.CursorPos.y + size.y); + + return size; +} + +float ImGui::GetTextLineHeight() +{ + ImGuiContext& g = *GImGui; + return g.FontSize; +} + +float ImGui::GetTextLineHeightWithSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.ItemSpacing.y; +} + +float ImGui::GetFrameHeight() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.FramePadding.y * 2.0f; +} + +float ImGui::GetFrameHeightWithSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.FramePadding.y * 2.0f + g.Style.ItemSpacing.y; +} + +// FIXME: All the Contents Region function are messy or misleading. WE WILL AIM TO OBSOLETE ALL OF THEM WITH A NEW "WORK RECT" API. Thanks for your patience! + +// FIXME: This is in window space (not screen space!). +ImVec2 ImGui::GetContentRegionMax() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImVec2 mx = window->ContentRegionRect.Max - window->Pos; + if (window->DC.CurrentColumns || g.CurrentTable) + mx.x = window->WorkRect.Max.x - window->Pos.x; + return mx; +} + +// [Internal] Absolute coordinate. Saner. This is not exposed until we finishing refactoring work rect features. +ImVec2 ImGui::GetContentRegionMaxAbs() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImVec2 mx = window->ContentRegionRect.Max; + if (window->DC.CurrentColumns || g.CurrentTable) + mx.x = window->WorkRect.Max.x; + return mx; +} + +ImVec2 ImGui::GetContentRegionAvail() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return GetContentRegionMaxAbs() - window->DC.CursorPos; +} + +// In window space (not screen space!) +ImVec2 ImGui::GetWindowContentRegionMin() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ContentRegionRect.Min - window->Pos; +} + +ImVec2 ImGui::GetWindowContentRegionMax() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ContentRegionRect.Max - window->Pos; +} + +// Lock horizontal starting position + capture group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) +// Groups are currently a mishmash of functionalities which should perhaps be clarified and separated. +// FIXME-OPT: Could we safely early out on ->SkipItems? +void ImGui::BeginGroup() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + g.GroupStack.resize(g.GroupStack.Size + 1); + ImGuiGroupData& group_data = g.GroupStack.back(); + group_data.WindowID = window->ID; + group_data.BackupCursorPos = window->DC.CursorPos; + group_data.BackupCursorMaxPos = window->DC.CursorMaxPos; + group_data.BackupIndent = window->DC.Indent; + group_data.BackupGroupOffset = window->DC.GroupOffset; + group_data.BackupCurrLineSize = window->DC.CurrLineSize; + group_data.BackupCurrLineTextBaseOffset = window->DC.CurrLineTextBaseOffset; + group_data.BackupActiveIdIsAlive = g.ActiveIdIsAlive; + group_data.BackupHoveredIdIsAlive = g.HoveredId != 0; + group_data.BackupActiveIdPreviousFrameIsAlive = g.ActiveIdPreviousFrameIsAlive; + group_data.EmitItem = true; + + window->DC.GroupOffset.x = window->DC.CursorPos.x - window->Pos.x - window->DC.ColumnsOffset.x; + window->DC.Indent = window->DC.GroupOffset; + window->DC.CursorMaxPos = window->DC.CursorPos; + window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); + if (g.LogEnabled) + g.LogLinePosY = -FLT_MAX; // To enforce a carriage return +} + +void ImGui::EndGroup() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(g.GroupStack.Size > 0); // Mismatched BeginGroup()/EndGroup() calls + + ImGuiGroupData& group_data = g.GroupStack.back(); + IM_ASSERT(group_data.WindowID == window->ID); // EndGroup() in wrong window? + + ImRect group_bb(group_data.BackupCursorPos, ImMax(window->DC.CursorMaxPos, group_data.BackupCursorPos)); + + window->DC.CursorPos = group_data.BackupCursorPos; + window->DC.CursorMaxPos = ImMax(group_data.BackupCursorMaxPos, window->DC.CursorMaxPos); + window->DC.Indent = group_data.BackupIndent; + window->DC.GroupOffset = group_data.BackupGroupOffset; + window->DC.CurrLineSize = group_data.BackupCurrLineSize; + window->DC.CurrLineTextBaseOffset = group_data.BackupCurrLineTextBaseOffset; + if (g.LogEnabled) + g.LogLinePosY = -FLT_MAX; // To enforce a carriage return + + if (!group_data.EmitItem) + { + g.GroupStack.pop_back(); + return; + } + + window->DC.CurrLineTextBaseOffset = ImMax(window->DC.PrevLineTextBaseOffset, group_data.BackupCurrLineTextBaseOffset); // FIXME: Incorrect, we should grab the base offset from the *first line* of the group but it is hard to obtain now. + ItemSize(group_bb.GetSize()); + ItemAdd(group_bb, 0, NULL, ImGuiItemFlags_NoTabStop); + + // If the current ActiveId was declared within the boundary of our group, we copy it to LastItemId so IsItemActive(), IsItemDeactivated() etc. will be functional on the entire group. + // It would be be neater if we replaced window.DC.LastItemId by e.g. 'bool LastItemIsActive', but would put a little more burden on individual widgets. + // Also if you grep for LastItemId you'll notice it is only used in that context. + // (The two tests not the same because ActiveIdIsAlive is an ID itself, in order to be able to handle ActiveId being overwritten during the frame.) + const bool group_contains_curr_active_id = (group_data.BackupActiveIdIsAlive != g.ActiveId) && (g.ActiveIdIsAlive == g.ActiveId) && g.ActiveId; + const bool group_contains_prev_active_id = (group_data.BackupActiveIdPreviousFrameIsAlive == false) && (g.ActiveIdPreviousFrameIsAlive == true); + if (group_contains_curr_active_id) + g.LastItemData.ID = g.ActiveId; + else if (group_contains_prev_active_id) + g.LastItemData.ID = g.ActiveIdPreviousFrame; + g.LastItemData.Rect = group_bb; + + // Forward Hovered flag + const bool group_contains_curr_hovered_id = (group_data.BackupHoveredIdIsAlive == false) && g.HoveredId != 0; + if (group_contains_curr_hovered_id) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + + // Forward Edited flag + if (group_contains_curr_active_id && g.ActiveIdHasBeenEditedThisFrame) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited; + + // Forward Deactivated flag + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDeactivated; + if (group_contains_prev_active_id && g.ActiveId != g.ActiveIdPreviousFrame) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Deactivated; + + g.GroupStack.pop_back(); + //window->DrawList->AddRect(group_bb.Min, group_bb.Max, IM_COL32(255,0,255,255)); // [Debug] +} + + +//----------------------------------------------------------------------------- +// [SECTION] SCROLLING +//----------------------------------------------------------------------------- + +// Helper to snap on edges when aiming at an item very close to the edge, +// So the difference between WindowPadding and ItemSpacing will be in the visible area after scrolling. +// When we refactor the scrolling API this may be configurable with a flag? +// Note that the effect for this won't be visible on X axis with default Style settings as WindowPadding.x == ItemSpacing.x by default. +static float CalcScrollEdgeSnap(float target, float snap_min, float snap_max, float snap_threshold, float center_ratio) +{ + if (target <= snap_min + snap_threshold) + return ImLerp(snap_min, target, center_ratio); + if (target >= snap_max - snap_threshold) + return ImLerp(target, snap_max, center_ratio); + return target; +} + +static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window) +{ + ImVec2 scroll = window->Scroll; + if (window->ScrollTarget.x < FLT_MAX) + { + float decoration_total_width = window->ScrollbarSizes.x; + float center_x_ratio = window->ScrollTargetCenterRatio.x; + float scroll_target_x = window->ScrollTarget.x; + if (window->ScrollTargetEdgeSnapDist.x > 0.0f) + { + float snap_x_min = 0.0f; + float snap_x_max = window->ScrollMax.x + window->SizeFull.x - decoration_total_width; + scroll_target_x = CalcScrollEdgeSnap(scroll_target_x, snap_x_min, snap_x_max, window->ScrollTargetEdgeSnapDist.x, center_x_ratio); + } + scroll.x = scroll_target_x - center_x_ratio * (window->SizeFull.x - decoration_total_width); + } + if (window->ScrollTarget.y < FLT_MAX) + { + float decoration_total_height = window->TitleBarHeight() + window->MenuBarHeight() + window->ScrollbarSizes.y; + float center_y_ratio = window->ScrollTargetCenterRatio.y; + float scroll_target_y = window->ScrollTarget.y; + if (window->ScrollTargetEdgeSnapDist.y > 0.0f) + { + float snap_y_min = 0.0f; + float snap_y_max = window->ScrollMax.y + window->SizeFull.y - decoration_total_height; + scroll_target_y = CalcScrollEdgeSnap(scroll_target_y, snap_y_min, snap_y_max, window->ScrollTargetEdgeSnapDist.y, center_y_ratio); + } + scroll.y = scroll_target_y - center_y_ratio * (window->SizeFull.y - decoration_total_height); + } + scroll.x = IM_FLOOR(ImMax(scroll.x, 0.0f)); + scroll.y = IM_FLOOR(ImMax(scroll.y, 0.0f)); + if (!window->Collapsed && !window->SkipItems) + { + scroll.x = ImMin(scroll.x, window->ScrollMax.x); + scroll.y = ImMin(scroll.y, window->ScrollMax.y); + } + return scroll; +} + +void ImGui::ScrollToItem(ImGuiScrollFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ScrollToRectEx(window, g.LastItemData.NavRect, flags); +} + +void ImGui::ScrollToRect(ImGuiWindow* window, const ImRect& item_rect, ImGuiScrollFlags flags) +{ + ScrollToRectEx(window, item_rect, flags); +} + +// Scroll to keep newly navigated item fully into view +ImVec2 ImGui::ScrollToRectEx(ImGuiWindow* window, const ImRect& item_rect, ImGuiScrollFlags flags) +{ + ImGuiContext& g = *GImGui; + ImRect window_rect(window->InnerRect.Min - ImVec2(1, 1), window->InnerRect.Max + ImVec2(1, 1)); + //GetForegroundDrawList(window)->AddRect(window_rect.Min, window_rect.Max, IM_COL32_WHITE); // [DEBUG] + + // Check that only one behavior is selected per axis + IM_ASSERT((flags & ImGuiScrollFlags_MaskX_) == 0 || ImIsPowerOfTwo(flags & ImGuiScrollFlags_MaskX_)); + IM_ASSERT((flags & ImGuiScrollFlags_MaskY_) == 0 || ImIsPowerOfTwo(flags & ImGuiScrollFlags_MaskY_)); + + // Defaults + ImGuiScrollFlags in_flags = flags; + if ((flags & ImGuiScrollFlags_MaskX_) == 0 && window->ScrollbarX) + flags |= ImGuiScrollFlags_KeepVisibleEdgeX; + if ((flags & ImGuiScrollFlags_MaskY_) == 0) + flags |= window->Appearing ? ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeY; + + const bool fully_visible_x = item_rect.Min.x >= window_rect.Min.x && item_rect.Max.x <= window_rect.Max.x; + const bool fully_visible_y = item_rect.Min.y >= window_rect.Min.y && item_rect.Max.y <= window_rect.Max.y; + const bool can_be_fully_visible_x = (item_rect.GetWidth() + g.Style.ItemSpacing.x * 2.0f) <= window_rect.GetWidth(); + const bool can_be_fully_visible_y = (item_rect.GetHeight() + g.Style.ItemSpacing.y * 2.0f) <= window_rect.GetHeight(); + + if ((flags & ImGuiScrollFlags_KeepVisibleEdgeX) && !fully_visible_x) + { + if (item_rect.Min.x < window_rect.Min.x || !can_be_fully_visible_x) + SetScrollFromPosX(window, item_rect.Min.x - g.Style.ItemSpacing.x - window->Pos.x, 0.0f); + else if (item_rect.Max.x >= window_rect.Max.x) + SetScrollFromPosX(window, item_rect.Max.x + g.Style.ItemSpacing.x - window->Pos.x, 1.0f); + } + else if (((flags & ImGuiScrollFlags_KeepVisibleCenterX) && !fully_visible_x) || (flags & ImGuiScrollFlags_AlwaysCenterX)) + { + float target_x = can_be_fully_visible_x ? ImFloor((item_rect.Min.x + item_rect.Max.x - window->InnerRect.GetWidth()) * 0.5f) : item_rect.Min.x; + SetScrollFromPosX(window, target_x - window->Pos.x, 0.0f); + } + + if ((flags & ImGuiScrollFlags_KeepVisibleEdgeY) && !fully_visible_y) + { + if (item_rect.Min.y < window_rect.Min.y || !can_be_fully_visible_y) + SetScrollFromPosY(window, item_rect.Min.y - g.Style.ItemSpacing.y - window->Pos.y, 0.0f); + else if (item_rect.Max.y >= window_rect.Max.y) + SetScrollFromPosY(window, item_rect.Max.y + g.Style.ItemSpacing.y - window->Pos.y, 1.0f); + } + else if (((flags & ImGuiScrollFlags_KeepVisibleCenterY) && !fully_visible_y) || (flags & ImGuiScrollFlags_AlwaysCenterY)) + { + float target_y = can_be_fully_visible_y ? ImFloor((item_rect.Min.y + item_rect.Max.y - window->InnerRect.GetHeight()) * 0.5f) : item_rect.Min.y; + SetScrollFromPosY(window, target_y - window->Pos.y, 0.0f); + } + + ImVec2 next_scroll = CalcNextScrollFromScrollTargetAndClamp(window); + ImVec2 delta_scroll = next_scroll - window->Scroll; + + // Also scroll parent window to keep us into view if necessary + if (!(flags & ImGuiScrollFlags_NoScrollParent) && (window->Flags & ImGuiWindowFlags_ChildWindow)) + { + // FIXME-SCROLL: May be an option? + if ((in_flags & (ImGuiScrollFlags_AlwaysCenterX | ImGuiScrollFlags_KeepVisibleCenterX)) != 0) + in_flags = (in_flags & ~ImGuiScrollFlags_MaskX_) | ImGuiScrollFlags_KeepVisibleEdgeX; + if ((in_flags & (ImGuiScrollFlags_AlwaysCenterY | ImGuiScrollFlags_KeepVisibleCenterY)) != 0) + in_flags = (in_flags & ~ImGuiScrollFlags_MaskY_) | ImGuiScrollFlags_KeepVisibleEdgeY; + delta_scroll += ScrollToRectEx(window->ParentWindow, ImRect(item_rect.Min - delta_scroll, item_rect.Max - delta_scroll), in_flags); + } + + return delta_scroll; +} + +float ImGui::GetScrollX() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Scroll.x; +} + +float ImGui::GetScrollY() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Scroll.y; +} + +float ImGui::GetScrollMaxX() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ScrollMax.x; +} + +float ImGui::GetScrollMaxY() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ScrollMax.y; +} + +void ImGui::SetScrollX(ImGuiWindow* window, float scroll_x) +{ + window->ScrollTarget.x = scroll_x; + window->ScrollTargetCenterRatio.x = 0.0f; + window->ScrollTargetEdgeSnapDist.x = 0.0f; +} + +void ImGui::SetScrollY(ImGuiWindow* window, float scroll_y) +{ + window->ScrollTarget.y = scroll_y; + window->ScrollTargetCenterRatio.y = 0.0f; + window->ScrollTargetEdgeSnapDist.y = 0.0f; +} + +void ImGui::SetScrollX(float scroll_x) +{ + ImGuiContext& g = *GImGui; + SetScrollX(g.CurrentWindow, scroll_x); +} + +void ImGui::SetScrollY(float scroll_y) +{ + ImGuiContext& g = *GImGui; + SetScrollY(g.CurrentWindow, scroll_y); +} + +// Note that a local position will vary depending on initial scroll value, +// This is a little bit confusing so bear with us: +// - local_pos = (absolution_pos - window->Pos) +// - So local_x/local_y are 0.0f for a position at the upper-left corner of a window, +// and generally local_x/local_y are >(padding+decoration) && <(size-padding-decoration) when in the visible area. +// - They mostly exists because of legacy API. +// Following the rules above, when trying to work with scrolling code, consider that: +// - SetScrollFromPosY(0.0f) == SetScrollY(0.0f + scroll.y) == has no effect! +// - SetScrollFromPosY(-scroll.y) == SetScrollY(-scroll.y + scroll.y) == SetScrollY(0.0f) == reset scroll. Of course writing SetScrollY(0.0f) directly then makes more sense +// We store a target position so centering and clamping can occur on the next frame when we are guaranteed to have a known window size +void ImGui::SetScrollFromPosX(ImGuiWindow* window, float local_x, float center_x_ratio) +{ + IM_ASSERT(center_x_ratio >= 0.0f && center_x_ratio <= 1.0f); + window->ScrollTarget.x = IM_FLOOR(local_x + window->Scroll.x); // Convert local position to scroll offset + window->ScrollTargetCenterRatio.x = center_x_ratio; + window->ScrollTargetEdgeSnapDist.x = 0.0f; +} + +void ImGui::SetScrollFromPosY(ImGuiWindow* window, float local_y, float center_y_ratio) +{ + IM_ASSERT(center_y_ratio >= 0.0f && center_y_ratio <= 1.0f); + const float decoration_up_height = window->TitleBarHeight() + window->MenuBarHeight(); // FIXME: Would be nice to have a more standardized access to our scrollable/client rect; + local_y -= decoration_up_height; + window->ScrollTarget.y = IM_FLOOR(local_y + window->Scroll.y); // Convert local position to scroll offset + window->ScrollTargetCenterRatio.y = center_y_ratio; + window->ScrollTargetEdgeSnapDist.y = 0.0f; +} + +void ImGui::SetScrollFromPosX(float local_x, float center_x_ratio) +{ + ImGuiContext& g = *GImGui; + SetScrollFromPosX(g.CurrentWindow, local_x, center_x_ratio); +} + +void ImGui::SetScrollFromPosY(float local_y, float center_y_ratio) +{ + ImGuiContext& g = *GImGui; + SetScrollFromPosY(g.CurrentWindow, local_y, center_y_ratio); +} + +// center_x_ratio: 0.0f left of last item, 0.5f horizontal center of last item, 1.0f right of last item. +void ImGui::SetScrollHereX(float center_x_ratio) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float spacing_x = ImMax(window->WindowPadding.x, g.Style.ItemSpacing.x); + float target_pos_x = ImLerp(g.LastItemData.Rect.Min.x - spacing_x, g.LastItemData.Rect.Max.x + spacing_x, center_x_ratio); + SetScrollFromPosX(window, target_pos_x - window->Pos.x, center_x_ratio); // Convert from absolute to local pos + + // Tweak: snap on edges when aiming at an item very close to the edge + window->ScrollTargetEdgeSnapDist.x = ImMax(0.0f, window->WindowPadding.x - spacing_x); +} + +// center_y_ratio: 0.0f top of last item, 0.5f vertical center of last item, 1.0f bottom of last item. +void ImGui::SetScrollHereY(float center_y_ratio) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float spacing_y = ImMax(window->WindowPadding.y, g.Style.ItemSpacing.y); + float target_pos_y = ImLerp(window->DC.CursorPosPrevLine.y - spacing_y, window->DC.CursorPosPrevLine.y + window->DC.PrevLineSize.y + spacing_y, center_y_ratio); + SetScrollFromPosY(window, target_pos_y - window->Pos.y, center_y_ratio); // Convert from absolute to local pos + + // Tweak: snap on edges when aiming at an item very close to the edge + window->ScrollTargetEdgeSnapDist.y = ImMax(0.0f, window->WindowPadding.y - spacing_y); +} + +//----------------------------------------------------------------------------- +// [SECTION] TOOLTIPS +//----------------------------------------------------------------------------- + +void ImGui::BeginTooltip() +{ + BeginTooltipEx(ImGuiTooltipFlags_None, ImGuiWindowFlags_None); +} + +void ImGui::BeginTooltipEx(ImGuiTooltipFlags tooltip_flags, ImGuiWindowFlags extra_window_flags) +{ + ImGuiContext& g = *GImGui; + + if (g.DragDropWithinSource || g.DragDropWithinTarget) + { + // The default tooltip position is a little offset to give space to see the context menu (it's also clamped within the current viewport/monitor) + // In the context of a dragging tooltip we try to reduce that offset and we enforce following the cursor. + // Whatever we do we want to call SetNextWindowPos() to enforce a tooltip position and disable clipping the tooltip without our display area, like regular tooltip do. + //ImVec2 tooltip_pos = g.IO.MousePos - g.ActiveIdClickOffset - g.Style.WindowPadding; + ImVec2 tooltip_pos = g.IO.MousePos + ImVec2(16 * g.Style.MouseCursorScale, 8 * g.Style.MouseCursorScale); + SetNextWindowPos(tooltip_pos); + SetNextWindowBgAlpha(g.Style.Colors[ImGuiCol_PopupBg].w * 0.60f); + //PushStyleVar(ImGuiStyleVar_Alpha, g.Style.Alpha * 0.60f); // This would be nice but e.g ColorButton with checkboard has issue with transparent colors :( + tooltip_flags |= ImGuiTooltipFlags_OverridePreviousTooltip; + } + + char window_name[16]; + ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", g.TooltipOverrideCount); + if (tooltip_flags & ImGuiTooltipFlags_OverridePreviousTooltip) + if (ImGuiWindow* window = FindWindowByName(window_name)) + if (window->Active) + { + // Hide previous tooltip from being displayed. We can't easily "reset" the content of a window so we create a new one. + window->Hidden = true; + window->HiddenFramesCanSkipItems = 1; // FIXME: This may not be necessary? + ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", ++g.TooltipOverrideCount); + } + ImGuiWindowFlags flags = ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize; + Begin(window_name, NULL, flags | extra_window_flags); +} + +void ImGui::EndTooltip() +{ + IM_ASSERT(GetCurrentWindowRead()->Flags & ImGuiWindowFlags_Tooltip); // Mismatched BeginTooltip()/EndTooltip() calls + End(); +} + +void ImGui::SetTooltipV(const char* fmt, va_list args) +{ + BeginTooltipEx(ImGuiTooltipFlags_OverridePreviousTooltip, ImGuiWindowFlags_None); + TextV(fmt, args); + EndTooltip(); +} + +void ImGui::SetTooltip(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + SetTooltipV(fmt, args); + va_end(args); +} + +//----------------------------------------------------------------------------- +// [SECTION] POPUPS +//----------------------------------------------------------------------------- + +// Supported flags: ImGuiPopupFlags_AnyPopupId, ImGuiPopupFlags_AnyPopupLevel +bool ImGui::IsPopupOpen(ImGuiID id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + if (popup_flags & ImGuiPopupFlags_AnyPopupId) + { + // Return true if any popup is open at the current BeginPopup() level of the popup stack + // This may be used to e.g. test for another popups already opened to handle popups priorities at the same level. + IM_ASSERT(id == 0); + if (popup_flags & ImGuiPopupFlags_AnyPopupLevel) + return g.OpenPopupStack.Size > 0; + else + return g.OpenPopupStack.Size > g.BeginPopupStack.Size; + } + else + { + if (popup_flags & ImGuiPopupFlags_AnyPopupLevel) + { + // Return true if the popup is open anywhere in the popup stack + for (int n = 0; n < g.OpenPopupStack.Size; n++) + if (g.OpenPopupStack[n].PopupId == id) + return true; + return false; + } + else + { + // Return true if the popup is open at the current BeginPopup() level of the popup stack (this is the most-common query) + return g.OpenPopupStack.Size > g.BeginPopupStack.Size && g.OpenPopupStack[g.BeginPopupStack.Size].PopupId == id; + } + } +} + +bool ImGui::IsPopupOpen(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiID id = (popup_flags & ImGuiPopupFlags_AnyPopupId) ? 0 : g.CurrentWindow->GetID(str_id); + if ((popup_flags & ImGuiPopupFlags_AnyPopupLevel) && id != 0) + IM_ASSERT(0 && "Cannot use IsPopupOpen() with a string id and ImGuiPopupFlags_AnyPopupLevel."); // But non-string version is legal and used internally + return IsPopupOpen(id, popup_flags); +} + +ImGuiWindow* ImGui::GetTopMostPopupModal() +{ + ImGuiContext& g = *GImGui; + for (int n = g.OpenPopupStack.Size - 1; n >= 0; n--) + if (ImGuiWindow* popup = g.OpenPopupStack.Data[n].Window) + if (popup->Flags & ImGuiWindowFlags_Modal) + return popup; + return NULL; +} + +ImGuiWindow* ImGui::GetTopMostAndVisiblePopupModal() +{ + ImGuiContext& g = *GImGui; + for (int n = g.OpenPopupStack.Size - 1; n >= 0; n--) + if (ImGuiWindow* popup = g.OpenPopupStack.Data[n].Window) + if ((popup->Flags & ImGuiWindowFlags_Modal) && IsWindowActiveAndVisible(popup)) + return popup; + return NULL; +} + +void ImGui::OpenPopup(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + OpenPopupEx(g.CurrentWindow->GetID(str_id), popup_flags); +} + +void ImGui::OpenPopup(ImGuiID id, ImGuiPopupFlags popup_flags) +{ + OpenPopupEx(id, popup_flags); +} + +// Mark popup as open (toggle toward open state). +// Popups are closed when user click outside, or activate a pressable item, or CloseCurrentPopup() is called within a BeginPopup()/EndPopup() block. +// Popup identifiers are relative to the current ID-stack (so OpenPopup and BeginPopup needs to be at the same level). +// One open popup per level of the popup hierarchy (NB: when assigning we reset the Window member of ImGuiPopupRef to NULL) +void ImGui::OpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* parent_window = g.CurrentWindow; + const int current_stack_size = g.BeginPopupStack.Size; + + if (popup_flags & ImGuiPopupFlags_NoOpenOverExistingPopup) + if (IsPopupOpen(0u, ImGuiPopupFlags_AnyPopupId)) + return; + + ImGuiPopupData popup_ref; // Tagged as new ref as Window will be set back to NULL if we write this into OpenPopupStack. + popup_ref.PopupId = id; + popup_ref.Window = NULL; + popup_ref.SourceWindow = g.NavWindow; + popup_ref.OpenFrameCount = g.FrameCount; + popup_ref.OpenParentId = parent_window->IDStack.back(); + popup_ref.OpenPopupPos = NavCalcPreferredRefPos(); + popup_ref.OpenMousePos = IsMousePosValid(&g.IO.MousePos) ? g.IO.MousePos : popup_ref.OpenPopupPos; + + IMGUI_DEBUG_LOG_POPUP("OpenPopupEx(0x%08X)\n", id); + if (g.OpenPopupStack.Size < current_stack_size + 1) + { + g.OpenPopupStack.push_back(popup_ref); + } + else + { + // Gently handle the user mistakenly calling OpenPopup() every frame. It is a programming mistake! However, if we were to run the regular code path, the ui + // would become completely unusable because the popup will always be in hidden-while-calculating-size state _while_ claiming focus. Which would be a very confusing + // situation for the programmer. Instead, we silently allow the popup to proceed, it will keep reappearing and the programming error will be more obvious to understand. + if (g.OpenPopupStack[current_stack_size].PopupId == id && g.OpenPopupStack[current_stack_size].OpenFrameCount == g.FrameCount - 1) + { + g.OpenPopupStack[current_stack_size].OpenFrameCount = popup_ref.OpenFrameCount; + } + else + { + // Close child popups if any, then flag popup for open/reopen + ClosePopupToLevel(current_stack_size, false); + g.OpenPopupStack.push_back(popup_ref); + } + + // When reopening a popup we first refocus its parent, otherwise if its parent is itself a popup it would get closed by ClosePopupsOverWindow(). + // This is equivalent to what ClosePopupToLevel() does. + //if (g.OpenPopupStack[current_stack_size].PopupId == id) + // FocusWindow(parent_window); + } +} + +// When popups are stacked, clicking on a lower level popups puts focus back to it and close popups above it. +// This function closes any popups that are over 'ref_window'. +void ImGui::ClosePopupsOverWindow(ImGuiWindow* ref_window, bool restore_focus_to_window_under_popup) +{ + ImGuiContext& g = *GImGui; + if (g.OpenPopupStack.Size == 0) + return; + + // Don't close our own child popup windows. + int popup_count_to_keep = 0; + if (ref_window) + { + // Find the highest popup which is a descendant of the reference window (generally reference window = NavWindow) + for (; popup_count_to_keep < g.OpenPopupStack.Size; popup_count_to_keep++) + { + ImGuiPopupData& popup = g.OpenPopupStack[popup_count_to_keep]; + if (!popup.Window) + continue; + IM_ASSERT((popup.Window->Flags & ImGuiWindowFlags_Popup) != 0); + if (popup.Window->Flags & ImGuiWindowFlags_ChildWindow) + continue; + + // Trim the stack unless the popup is a direct parent of the reference window (the reference window is often the NavWindow) + // - With this stack of window, clicking/focusing Popup1 will close Popup2 and Popup3: + // Window -> Popup1 -> Popup2 -> Popup3 + // - Each popups may contain child windows, which is why we compare ->RootWindow! + // Window -> Popup1 -> Popup1_Child -> Popup2 -> Popup2_Child + bool ref_window_is_descendent_of_popup = false; + for (int n = popup_count_to_keep; n < g.OpenPopupStack.Size; n++) + if (ImGuiWindow* popup_window = g.OpenPopupStack[n].Window) + if (IsWindowWithinBeginStackOf(ref_window, popup_window)) + { + ref_window_is_descendent_of_popup = true; + break; + } + if (!ref_window_is_descendent_of_popup) + break; + } + } + if (popup_count_to_keep < g.OpenPopupStack.Size) // This test is not required but it allows to set a convenient breakpoint on the statement below + { + IMGUI_DEBUG_LOG_POPUP("ClosePopupsOverWindow(\"%s\") -> ClosePopupToLevel(%d)\n", ref_window->Name, popup_count_to_keep); + ClosePopupToLevel(popup_count_to_keep, restore_focus_to_window_under_popup); + } +} + +void ImGui::ClosePopupsExceptModals() +{ + ImGuiContext& g = *GImGui; + + int popup_count_to_keep; + for (popup_count_to_keep = g.OpenPopupStack.Size; popup_count_to_keep > 0; popup_count_to_keep--) + { + ImGuiWindow* window = g.OpenPopupStack[popup_count_to_keep - 1].Window; + if (!window || window->Flags & ImGuiWindowFlags_Modal) + break; + } + if (popup_count_to_keep < g.OpenPopupStack.Size) // This test is not required but it allows to set a convenient breakpoint on the statement below + ClosePopupToLevel(popup_count_to_keep, true); +} + +void ImGui::ClosePopupToLevel(int remaining, bool restore_focus_to_window_under_popup) +{ + ImGuiContext& g = *GImGui; + IMGUI_DEBUG_LOG_POPUP("ClosePopupToLevel(%d), restore_focus_to_window_under_popup=%d\n", remaining, restore_focus_to_window_under_popup); + IM_ASSERT(remaining >= 0 && remaining < g.OpenPopupStack.Size); + + // Trim open popup stack + ImGuiWindow* focus_window = g.OpenPopupStack[remaining].SourceWindow; + ImGuiWindow* popup_window = g.OpenPopupStack[remaining].Window; + g.OpenPopupStack.resize(remaining); + + if (restore_focus_to_window_under_popup) + { + if (focus_window && !focus_window->WasActive && popup_window) + { + // Fallback + FocusTopMostWindowUnderOne(popup_window, NULL); + } + else + { + if (g.NavLayer == ImGuiNavLayer_Main && focus_window) + focus_window = NavRestoreLastChildNavWindow(focus_window); + FocusWindow(focus_window); + } + } +} + +// Close the popup we have begin-ed into. +void ImGui::CloseCurrentPopup() +{ + ImGuiContext& g = *GImGui; + int popup_idx = g.BeginPopupStack.Size - 1; + if (popup_idx < 0 || popup_idx >= g.OpenPopupStack.Size || g.BeginPopupStack[popup_idx].PopupId != g.OpenPopupStack[popup_idx].PopupId) + return; + + // Closing a menu closes its top-most parent popup (unless a modal) + while (popup_idx > 0) + { + ImGuiWindow* popup_window = g.OpenPopupStack[popup_idx].Window; + ImGuiWindow* parent_popup_window = g.OpenPopupStack[popup_idx - 1].Window; + bool close_parent = false; + if (popup_window && (popup_window->Flags & ImGuiWindowFlags_ChildMenu)) + if (parent_popup_window && !(parent_popup_window->Flags & ImGuiWindowFlags_MenuBar)) + close_parent = true; + if (!close_parent) + break; + popup_idx--; + } + IMGUI_DEBUG_LOG_POPUP("CloseCurrentPopup %d -> %d\n", g.BeginPopupStack.Size - 1, popup_idx); + ClosePopupToLevel(popup_idx, true); + + // A common pattern is to close a popup when selecting a menu item/selectable that will open another window. + // To improve this usage pattern, we avoid nav highlight for a single frame in the parent window. + // Similarly, we could avoid mouse hover highlight in this window but it is less visually problematic. + if (ImGuiWindow* window = g.NavWindow) + window->DC.NavHideHighlightOneFrame = true; +} + +// Attention! BeginPopup() adds default flags which BeginPopupEx()! +bool ImGui::BeginPopupEx(ImGuiID id, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + if (!IsPopupOpen(id, ImGuiPopupFlags_None)) + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + return false; + } + + char name[20]; + if (flags & ImGuiWindowFlags_ChildMenu) + ImFormatString(name, IM_ARRAYSIZE(name), "##Menu_%02d", g.BeginMenuCount); // Recycle windows based on depth + else + ImFormatString(name, IM_ARRAYSIZE(name), "##Popup_%08x", id); // Not recycling, so we can close/open during the same frame + + flags |= ImGuiWindowFlags_Popup; + bool is_open = Begin(name, NULL, flags); + if (!is_open) // NB: Begin can return false when the popup is completely clipped (e.g. zero size display) + EndPopup(); + + return is_open; +} + +bool ImGui::BeginPopup(const char* str_id, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + if (g.OpenPopupStack.Size <= g.BeginPopupStack.Size) // Early out for performance + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + return false; + } + flags |= ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings; + ImGuiID id = g.CurrentWindow->GetID(str_id); + return BeginPopupEx(id, flags); +} + +// If 'p_open' is specified for a modal popup window, the popup will have a regular close button which will close the popup. +// Note that popup visibility status is owned by Dear ImGui (and manipulated with e.g. OpenPopup) so the actual value of *p_open is meaningless here. +bool ImGui::BeginPopupModal(const char* name, bool* p_open, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const ImGuiID id = window->GetID(name); + if (!IsPopupOpen(id, ImGuiPopupFlags_None)) + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + return false; + } + + // Center modal windows by default for increased visibility + // (this won't really last as settings will kick in, and is mostly for backward compatibility. user may do the same themselves) + // FIXME: Should test for (PosCond & window->SetWindowPosAllowFlags) with the upcoming window. + if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasPos) == 0) + { + const ImGuiViewport* viewport = GetMainViewport(); + SetNextWindowPos(viewport->GetCenter(), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); + } + + flags |= ImGuiWindowFlags_Popup | ImGuiWindowFlags_Modal | ImGuiWindowFlags_NoCollapse; + const bool is_open = Begin(name, p_open, flags); + if (!is_open || (p_open && !*p_open)) // NB: is_open can be 'false' when the popup is completely clipped (e.g. zero size display) + { + EndPopup(); + if (is_open) + ClosePopupToLevel(g.BeginPopupStack.Size, true); + return false; + } + return is_open; +} + +void ImGui::EndPopup() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginPopup()/EndPopup() calls + IM_ASSERT(g.BeginPopupStack.Size > 0); + + // Make all menus and popups wrap around for now, may need to expose that policy (e.g. focus scope could include wrap/loop policy flags used by new move requests) + if (g.NavWindow == window) + NavMoveRequestTryWrapping(window, ImGuiNavMoveFlags_LoopY); + + // Child-popups don't need to be laid out + IM_ASSERT(g.WithinEndChild == false); + if (window->Flags & ImGuiWindowFlags_ChildWindow) + g.WithinEndChild = true; + End(); + g.WithinEndChild = false; +} + +// Helper to open a popup if mouse button is released over the item +// - This is essentially the same as BeginPopupContextItem() but without the trailing BeginPopup() +void ImGui::OpenPopupOnItemClick(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); + if (IsMouseReleased(mouse_button) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + { + ImGuiID id = str_id ? window->GetID(str_id) : g.LastItemData.ID; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! + IM_ASSERT(id != 0); // You cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) + OpenPopupEx(id, popup_flags); + } +} + +// This is a helper to handle the simplest case of associating one named popup to one given widget. +// - To create a popup associated to the last item, you generally want to pass a NULL value to str_id. +// - To create a popup with a specific identifier, pass it in str_id. +// - This is useful when using using BeginPopupContextItem() on an item which doesn't have an identifier, e.g. a Text() call. +// - This is useful when multiple code locations may want to manipulate/open the same popup, given an explicit id. +// - You may want to handle the whole on user side if you have specific needs (e.g. tweaking IsItemHovered() parameters). +// This is essentially the same as: +// id = str_id ? GetID(str_id) : GetItemID(); +// OpenPopupOnItemClick(str_id, ImGuiPopupFlags_MouseButtonRight); +// return BeginPopup(id); +// Which is essentially the same as: +// id = str_id ? GetID(str_id) : GetItemID(); +// if (IsItemHovered() && IsMouseReleased(ImGuiMouseButton_Right)) +// OpenPopup(id); +// return BeginPopup(id); +// The main difference being that this is tweaked to avoid computing the ID twice. +bool ImGui::BeginPopupContextItem(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + ImGuiID id = str_id ? window->GetID(str_id) : g.LastItemData.ID; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! + IM_ASSERT(id != 0); // You cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); + if (IsMouseReleased(mouse_button) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + OpenPopupEx(id, popup_flags); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); +} + +bool ImGui::BeginPopupContextWindow(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!str_id) + str_id = "window_context"; + ImGuiID id = window->GetID(str_id); + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); + if (IsMouseReleased(mouse_button) && IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + if (!(popup_flags & ImGuiPopupFlags_NoOpenOverItems) || !IsAnyItemHovered()) + OpenPopupEx(id, popup_flags); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); +} + +bool ImGui::BeginPopupContextVoid(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!str_id) + str_id = "void_context"; + ImGuiID id = window->GetID(str_id); + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); + if (IsMouseReleased(mouse_button) && !IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) + if (GetTopMostPopupModal() == NULL) + OpenPopupEx(id, popup_flags); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); +} + +// r_avoid = the rectangle to avoid (e.g. for tooltip it is a rectangle around the mouse cursor which we want to avoid. for popups it's a small point around the cursor.) +// r_outer = the visible area rectangle, minus safe area padding. If our popup size won't fit because of safe area padding we ignore it. +// (r_outer is usually equivalent to the viewport rectangle minus padding, but when multi-viewports are enabled and monitor +// information are available, it may represent the entire platform monitor from the frame of reference of the current viewport. +// this allows us to have tooltips/popups displayed out of the parent viewport.) +ImVec2 ImGui::FindBestWindowPosForPopupEx(const ImVec2& ref_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_outer, const ImRect& r_avoid, ImGuiPopupPositionPolicy policy) +{ + ImVec2 base_pos_clamped = ImClamp(ref_pos, r_outer.Min, r_outer.Max - size); + //GetForegroundDrawList()->AddRect(r_avoid.Min, r_avoid.Max, IM_COL32(255,0,0,255)); + //GetForegroundDrawList()->AddRect(r_outer.Min, r_outer.Max, IM_COL32(0,255,0,255)); + + // Combo Box policy (we want a connecting edge) + if (policy == ImGuiPopupPositionPolicy_ComboBox) + { + const ImGuiDir dir_prefered_order[ImGuiDir_COUNT] = { ImGuiDir_Down, ImGuiDir_Right, ImGuiDir_Left, ImGuiDir_Up }; + for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_COUNT; n++) + { + const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; + ImVec2 pos; + if (dir == ImGuiDir_Down) pos = ImVec2(r_avoid.Min.x, r_avoid.Max.y); // Below, Toward Right (default) + if (dir == ImGuiDir_Right) pos = ImVec2(r_avoid.Min.x, r_avoid.Min.y - size.y); // Above, Toward Right + if (dir == ImGuiDir_Left) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Max.y); // Below, Toward Left + if (dir == ImGuiDir_Up) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Min.y - size.y); // Above, Toward Left + if (!r_outer.Contains(ImRect(pos, pos + size))) + continue; + *last_dir = dir; + return pos; + } + } + + // Tooltip and Default popup policy + // (Always first try the direction we used on the last frame, if any) + if (policy == ImGuiPopupPositionPolicy_Tooltip || policy == ImGuiPopupPositionPolicy_Default) + { + const ImGuiDir dir_prefered_order[ImGuiDir_COUNT] = { ImGuiDir_Right, ImGuiDir_Down, ImGuiDir_Up, ImGuiDir_Left }; + for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_COUNT; n++) + { + const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; + + const float avail_w = (dir == ImGuiDir_Left ? r_avoid.Min.x : r_outer.Max.x) - (dir == ImGuiDir_Right ? r_avoid.Max.x : r_outer.Min.x); + const float avail_h = (dir == ImGuiDir_Up ? r_avoid.Min.y : r_outer.Max.y) - (dir == ImGuiDir_Down ? r_avoid.Max.y : r_outer.Min.y); + + // If there not enough room on one axis, there's no point in positioning on a side on this axis (e.g. when not enough width, use a top/bottom position to maximize available width) + if (avail_w < size.x && (dir == ImGuiDir_Left || dir == ImGuiDir_Right)) + continue; + if (avail_h < size.y && (dir == ImGuiDir_Up || dir == ImGuiDir_Down)) + continue; + + ImVec2 pos; + pos.x = (dir == ImGuiDir_Left) ? r_avoid.Min.x - size.x : (dir == ImGuiDir_Right) ? r_avoid.Max.x : base_pos_clamped.x; + pos.y = (dir == ImGuiDir_Up) ? r_avoid.Min.y - size.y : (dir == ImGuiDir_Down) ? r_avoid.Max.y : base_pos_clamped.y; + + // Clamp top-left corner of popup + pos.x = ImMax(pos.x, r_outer.Min.x); + pos.y = ImMax(pos.y, r_outer.Min.y); + + *last_dir = dir; + return pos; + } + } + + // Fallback when not enough room: + *last_dir = ImGuiDir_None; + + // For tooltip we prefer avoiding the cursor at all cost even if it means that part of the tooltip won't be visible. + if (policy == ImGuiPopupPositionPolicy_Tooltip) + return ref_pos + ImVec2(2, 2); + + // Otherwise try to keep within display + ImVec2 pos = ref_pos; + pos.x = ImMax(ImMin(pos.x + size.x, r_outer.Max.x) - size.x, r_outer.Min.x); + pos.y = ImMax(ImMin(pos.y + size.y, r_outer.Max.y) - size.y, r_outer.Min.y); + return pos; +} + +// Note that this is used for popups, which can overlap the non work-area of individual viewports. +ImRect ImGui::GetPopupAllowedExtentRect(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_UNUSED(window); + ImRect r_screen = ((ImGuiViewportP*)(void*)GetMainViewport())->GetMainRect(); + ImVec2 padding = g.Style.DisplaySafeAreaPadding; + r_screen.Expand(ImVec2((r_screen.GetWidth() > padding.x * 2) ? -padding.x : 0.0f, (r_screen.GetHeight() > padding.y * 2) ? -padding.y : 0.0f)); + return r_screen; +} + +ImVec2 ImGui::FindBestWindowPosForPopup(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + + ImRect r_outer = GetPopupAllowedExtentRect(window); + if (window->Flags & ImGuiWindowFlags_ChildMenu) + { + // Child menus typically request _any_ position within the parent menu item, and then we move the new menu outside the parent bounds. + // This is how we end up with child menus appearing (most-commonly) on the right of the parent menu. + IM_ASSERT(g.CurrentWindow == window); + ImGuiWindow* parent_window = g.CurrentWindowStack[g.CurrentWindowStack.Size - 2].Window; + float horizontal_overlap = g.Style.ItemInnerSpacing.x; // We want some overlap to convey the relative depth of each menu (currently the amount of overlap is hard-coded to style.ItemSpacing.x). + ImRect r_avoid; + if (parent_window->DC.MenuBarAppending) + r_avoid = ImRect(-FLT_MAX, parent_window->ClipRect.Min.y, FLT_MAX, parent_window->ClipRect.Max.y); // Avoid parent menu-bar. If we wanted multi-line menu-bar, we may instead want to have the calling window setup e.g. a NextWindowData.PosConstraintAvoidRect field + else + r_avoid = ImRect(parent_window->Pos.x + horizontal_overlap, -FLT_MAX, parent_window->Pos.x + parent_window->Size.x - horizontal_overlap - parent_window->ScrollbarSizes.x, FLT_MAX); + return FindBestWindowPosForPopupEx(window->Pos, window->Size, &window->AutoPosLastDirection, r_outer, r_avoid, ImGuiPopupPositionPolicy_Default); + } + if (window->Flags & ImGuiWindowFlags_Popup) + { + return FindBestWindowPosForPopupEx(window->Pos, window->Size, &window->AutoPosLastDirection, r_outer, ImRect(window->Pos, window->Pos), ImGuiPopupPositionPolicy_Default); // Ideally we'd disable r_avoid here + } + if (window->Flags & ImGuiWindowFlags_Tooltip) + { + // Position tooltip (always follows mouse) + float sc = g.Style.MouseCursorScale; + ImVec2 ref_pos = NavCalcPreferredRefPos(); + ImRect r_avoid; + if (!g.NavDisableHighlight && g.NavDisableMouseHover && !(g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos)) + r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 16, ref_pos.y + 8); + else + r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 24 * sc, ref_pos.y + 24 * sc); // FIXME: Hard-coded based on mouse cursor shape expectation. Exact dimension not very important. + return FindBestWindowPosForPopupEx(ref_pos, window->Size, &window->AutoPosLastDirection, r_outer, r_avoid, ImGuiPopupPositionPolicy_Tooltip); + } + IM_ASSERT(0); + return window->Pos; +} + +//----------------------------------------------------------------------------- +// [SECTION] KEYBOARD/GAMEPAD NAVIGATION +//----------------------------------------------------------------------------- + +// FIXME-NAV: The existence of SetNavID vs SetFocusID properly needs to be clarified/reworked. +// In our terminology those should be interchangeable. Those two functions are merely a legacy artifact, so at minimum naming should be clarified. +void ImGui::SetNavID(ImGuiID id, ImGuiNavLayer nav_layer, ImGuiID focus_scope_id, const ImRect& rect_rel) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindow != NULL); + IM_ASSERT(nav_layer == ImGuiNavLayer_Main || nav_layer == ImGuiNavLayer_Menu); + g.NavId = id; + g.NavLayer = nav_layer; + g.NavFocusScopeId = focus_scope_id; + g.NavWindow->NavLastIds[nav_layer] = id; + g.NavWindow->NavRectRel[nav_layer] = rect_rel; +} + +void ImGui::SetFocusID(ImGuiID id, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(id != 0); + + // Assume that SetFocusID() is called in the context where its window->DC.NavLayerCurrent and window->DC.NavFocusScopeIdCurrent are valid. + // Note that window may be != g.CurrentWindow (e.g. SetFocusID call in InputTextEx for multi-line text) + const ImGuiNavLayer nav_layer = window->DC.NavLayerCurrent; + if (g.NavWindow != window) + g.NavInitRequest = false; + g.NavWindow = window; + g.NavId = id; + g.NavLayer = nav_layer; + g.NavFocusScopeId = window->DC.NavFocusScopeIdCurrent; + window->NavLastIds[nav_layer] = id; + if (g.LastItemData.ID == id) + window->NavRectRel[nav_layer] = WindowRectAbsToRel(window, g.LastItemData.NavRect); + + if (g.ActiveIdSource == ImGuiInputSource_Nav) + g.NavDisableMouseHover = true; + else + g.NavDisableHighlight = true; +} + +ImGuiDir ImGetDirQuadrantFromDelta(float dx, float dy) +{ + if (ImFabs(dx) > ImFabs(dy)) + return (dx > 0.0f) ? ImGuiDir_Right : ImGuiDir_Left; + return (dy > 0.0f) ? ImGuiDir_Down : ImGuiDir_Up; +} + +static float inline NavScoreItemDistInterval(float a0, float a1, float b0, float b1) +{ + if (a1 < b0) + return a1 - b0; + if (b1 < a0) + return a0 - b1; + return 0.0f; +} + +static void inline NavClampRectToVisibleAreaForMoveDir(ImGuiDir move_dir, ImRect& r, const ImRect& clip_rect) +{ + if (move_dir == ImGuiDir_Left || move_dir == ImGuiDir_Right) + { + r.Min.y = ImClamp(r.Min.y, clip_rect.Min.y, clip_rect.Max.y); + r.Max.y = ImClamp(r.Max.y, clip_rect.Min.y, clip_rect.Max.y); + } + else // FIXME: PageUp/PageDown are leaving move_dir == None + { + r.Min.x = ImClamp(r.Min.x, clip_rect.Min.x, clip_rect.Max.x); + r.Max.x = ImClamp(r.Max.x, clip_rect.Min.x, clip_rect.Max.x); + } +} + +// Scoring function for gamepad/keyboard directional navigation. Based on https://gist.github.com/rygorous/6981057 +static bool ImGui::NavScoreItem(ImGuiNavItemData* result) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.NavLayer != window->DC.NavLayerCurrent) + return false; + + // FIXME: Those are not good variables names + ImRect cand = g.LastItemData.NavRect; // Current item nav rectangle + const ImRect curr = g.NavScoringRect; // Current modified source rect (NB: we've applied Max.x = Min.x in NavUpdate() to inhibit the effect of having varied item width) + g.NavScoringDebugCount++; + + // When entering through a NavFlattened border, we consider child window items as fully clipped for scoring + if (window->ParentWindow == g.NavWindow) + { + IM_ASSERT((window->Flags | g.NavWindow->Flags) & ImGuiWindowFlags_NavFlattened); + if (!window->ClipRect.Overlaps(cand)) + return false; + cand.ClipWithFull(window->ClipRect); // This allows the scored item to not overlap other candidates in the parent window + } + + // We perform scoring on items bounding box clipped by the current clipping rectangle on the other axis (clipping on our movement axis would give us equal scores for all clipped items) + // For example, this ensure that items in one column are not reached when moving vertically from items in another column. + NavClampRectToVisibleAreaForMoveDir(g.NavMoveClipDir, cand, window->ClipRect); + + // Compute distance between boxes + // FIXME-NAV: Introducing biases for vertical navigation, needs to be removed. + float dbx = NavScoreItemDistInterval(cand.Min.x, cand.Max.x, curr.Min.x, curr.Max.x); + float dby = NavScoreItemDistInterval(ImLerp(cand.Min.y, cand.Max.y, 0.2f), ImLerp(cand.Min.y, cand.Max.y, 0.8f), ImLerp(curr.Min.y, curr.Max.y, 0.2f), ImLerp(curr.Min.y, curr.Max.y, 0.8f)); // Scale down on Y to keep using box-distance for vertically touching items + if (dby != 0.0f && dbx != 0.0f) + dbx = (dbx / 1000.0f) + ((dbx > 0.0f) ? +1.0f : -1.0f); + float dist_box = ImFabs(dbx) + ImFabs(dby); + + // Compute distance between centers (this is off by a factor of 2, but we only compare center distances with each other so it doesn't matter) + float dcx = (cand.Min.x + cand.Max.x) - (curr.Min.x + curr.Max.x); + float dcy = (cand.Min.y + cand.Max.y) - (curr.Min.y + curr.Max.y); + float dist_center = ImFabs(dcx) + ImFabs(dcy); // L1 metric (need this for our connectedness guarantee) + + // Determine which quadrant of 'curr' our candidate item 'cand' lies in based on distance + ImGuiDir quadrant; + float dax = 0.0f, day = 0.0f, dist_axial = 0.0f; + if (dbx != 0.0f || dby != 0.0f) + { + // For non-overlapping boxes, use distance between boxes + dax = dbx; + day = dby; + dist_axial = dist_box; + quadrant = ImGetDirQuadrantFromDelta(dbx, dby); + } + else if (dcx != 0.0f || dcy != 0.0f) + { + // For overlapping boxes with different centers, use distance between centers + dax = dcx; + day = dcy; + dist_axial = dist_center; + quadrant = ImGetDirQuadrantFromDelta(dcx, dcy); + } + else + { + // Degenerate case: two overlapping buttons with same center, break ties arbitrarily (note that LastItemId here is really the _previous_ item order, but it doesn't matter) + quadrant = (g.LastItemData.ID < g.NavId) ? ImGuiDir_Left : ImGuiDir_Right; + } + +#if IMGUI_DEBUG_NAV_SCORING + char buf[128]; + if (IsMouseHoveringRect(cand.Min, cand.Max)) + { + ImFormatString(buf, IM_ARRAYSIZE(buf), "dbox (%.2f,%.2f->%.4f)\ndcen (%.2f,%.2f->%.4f)\nd (%.2f,%.2f->%.4f)\nnav %c, quadrant %c", dbx, dby, dist_box, dcx, dcy, dist_center, dax, day, dist_axial, "WENS"[g.NavMoveDir], "WENS"[quadrant]); + ImDrawList* draw_list = GetForegroundDrawList(window); + draw_list->AddRect(curr.Min, curr.Max, IM_COL32(255,200,0,100)); + draw_list->AddRect(cand.Min, cand.Max, IM_COL32(255,255,0,200)); + draw_list->AddRectFilled(cand.Max - ImVec2(4, 4), cand.Max + CalcTextSize(buf) + ImVec2(4, 4), IM_COL32(40,0,0,150)); + draw_list->AddText(cand.Max, ~0U, buf); + } + else if (g.IO.KeyCtrl) // Hold to preview score in matching quadrant. Press C to rotate. + { + if (quadrant == g.NavMoveDir) + { + ImFormatString(buf, IM_ARRAYSIZE(buf), "%.0f/%.0f", dist_box, dist_center); + ImDrawList* draw_list = GetForegroundDrawList(window); + draw_list->AddRectFilled(cand.Min, cand.Max, IM_COL32(255, 0, 0, 200)); + draw_list->AddText(cand.Min, IM_COL32(255, 255, 255, 255), buf); + } + } +#endif + + // Is it in the quadrant we're interesting in moving to? + bool new_best = false; + const ImGuiDir move_dir = g.NavMoveDir; + if (quadrant == move_dir) + { + // Does it beat the current best candidate? + if (dist_box < result->DistBox) + { + result->DistBox = dist_box; + result->DistCenter = dist_center; + return true; + } + if (dist_box == result->DistBox) + { + // Try using distance between center points to break ties + if (dist_center < result->DistCenter) + { + result->DistCenter = dist_center; + new_best = true; + } + else if (dist_center == result->DistCenter) + { + // Still tied! we need to be extra-careful to make sure everything gets linked properly. We consistently break ties by symbolically moving "later" items + // (with higher index) to the right/downwards by an infinitesimal amount since we the current "best" button already (so it must have a lower index), + // this is fairly easy. This rule ensures that all buttons with dx==dy==0 will end up being linked in order of appearance along the x axis. + if (((move_dir == ImGuiDir_Up || move_dir == ImGuiDir_Down) ? dby : dbx) < 0.0f) // moving bj to the right/down decreases distance + new_best = true; + } + } + } + + // Axial check: if 'curr' has no link at all in some direction and 'cand' lies roughly in that direction, add a tentative link. This will only be kept if no "real" matches + // are found, so it only augments the graph produced by the above method using extra links. (important, since it doesn't guarantee strong connectedness) + // This is just to avoid buttons having no links in a particular direction when there's a suitable neighbor. you get good graphs without this too. + // 2017/09/29: FIXME: This now currently only enabled inside menu bars, ideally we'd disable it everywhere. Menus in particular need to catch failure. For general navigation it feels awkward. + // Disabling it may lead to disconnected graphs when nodes are very spaced out on different axis. Perhaps consider offering this as an option? + if (result->DistBox == FLT_MAX && dist_axial < result->DistAxial) // Check axial match + if (g.NavLayer == ImGuiNavLayer_Menu && !(g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) + if ((move_dir == ImGuiDir_Left && dax < 0.0f) || (move_dir == ImGuiDir_Right && dax > 0.0f) || (move_dir == ImGuiDir_Up && day < 0.0f) || (move_dir == ImGuiDir_Down && day > 0.0f)) + { + result->DistAxial = dist_axial; + new_best = true; + } + + return new_best; +} + +static void ImGui::NavApplyItemToResult(ImGuiNavItemData* result) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + result->Window = window; + result->ID = g.LastItemData.ID; + result->FocusScopeId = window->DC.NavFocusScopeIdCurrent; + result->InFlags = g.LastItemData.InFlags; + result->RectRel = WindowRectAbsToRel(window, g.LastItemData.NavRect); +} + +// We get there when either NavId == id, or when g.NavAnyRequest is set (which is updated by NavUpdateAnyRequestFlag above) +// This is called after LastItemData is set. +static void ImGui::NavProcessItem() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const ImGuiID id = g.LastItemData.ID; + const ImRect nav_bb = g.LastItemData.NavRect; + const ImGuiItemFlags item_flags = g.LastItemData.InFlags; + + // Process Init Request + if (g.NavInitRequest && g.NavLayer == window->DC.NavLayerCurrent) + { + // Even if 'ImGuiItemFlags_NoNavDefaultFocus' is on (typically collapse/close button) we record the first ResultId so they can be used as a fallback + const bool candidate_for_nav_default_focus = (item_flags & (ImGuiItemFlags_NoNavDefaultFocus | ImGuiItemFlags_Disabled)) == 0; + if (candidate_for_nav_default_focus || g.NavInitResultId == 0) + { + g.NavInitResultId = id; + g.NavInitResultRectRel = WindowRectAbsToRel(window, nav_bb); + } + if (candidate_for_nav_default_focus) + { + g.NavInitRequest = false; // Found a match, clear request + NavUpdateAnyRequestFlag(); + } + } + + // Process Move Request (scoring for navigation) + // FIXME-NAV: Consider policy for double scoring (scoring from NavScoringRect + scoring from a rect wrapped according to current wrapping policy) + if (g.NavMoveScoringItems) + { + const bool is_tab_stop = (item_flags & ImGuiItemFlags_Inputable) && (item_flags & (ImGuiItemFlags_NoTabStop | ImGuiItemFlags_Disabled)) == 0; + const bool is_tabbing = (g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) != 0; + if (is_tabbing) + { + if (is_tab_stop || (g.NavMoveFlags & ImGuiNavMoveFlags_FocusApi)) + NavProcessItemForTabbingRequest(id); + } + else if ((g.NavId != id || (g.NavMoveFlags & ImGuiNavMoveFlags_AllowCurrentNavId)) && !(item_flags & (ImGuiItemFlags_Disabled | ImGuiItemFlags_NoNav))) + { + ImGuiNavItemData* result = (window == g.NavWindow) ? &g.NavMoveResultLocal : &g.NavMoveResultOther; + if (!is_tabbing) + { + if (NavScoreItem(result)) + NavApplyItemToResult(result); + + // Features like PageUp/PageDown need to maintain a separate score for the visible set of items. + const float VISIBLE_RATIO = 0.70f; + if ((g.NavMoveFlags & ImGuiNavMoveFlags_AlsoScoreVisibleSet) && window->ClipRect.Overlaps(nav_bb)) + if (ImClamp(nav_bb.Max.y, window->ClipRect.Min.y, window->ClipRect.Max.y) - ImClamp(nav_bb.Min.y, window->ClipRect.Min.y, window->ClipRect.Max.y) >= (nav_bb.Max.y - nav_bb.Min.y) * VISIBLE_RATIO) + if (NavScoreItem(&g.NavMoveResultLocalVisible)) + NavApplyItemToResult(&g.NavMoveResultLocalVisible); + } + } + } + + // Update window-relative bounding box of navigated item + if (g.NavId == id) + { + g.NavWindow = window; // Always refresh g.NavWindow, because some operations such as FocusItem() don't have a window. + g.NavLayer = window->DC.NavLayerCurrent; + g.NavFocusScopeId = window->DC.NavFocusScopeIdCurrent; + g.NavIdIsAlive = true; + window->NavRectRel[window->DC.NavLayerCurrent] = WindowRectAbsToRel(window, nav_bb); // Store item bounding box (relative to window position) + } +} + +// Handle "scoring" of an item for a tabbing/focusing request initiated by NavUpdateCreateTabbingRequest(). +// Note that SetKeyboardFocusHere() API calls are considered tabbing requests! +// - Case 1: no nav/active id: set result to first eligible item, stop storing. +// - Case 2: tab forward: on ref id set counter, on counter elapse store result +// - Case 3: tab forward wrap: set result to first eligible item (preemptively), on ref id set counter, on next frame if counter hasn't elapsed store result. // FIXME-TABBING: Could be done as a next-frame forwarded request +// - Case 4: tab backward: store all results, on ref id pick prev, stop storing +// - Case 5: tab backward wrap: store all results, on ref id if no result keep storing until last // FIXME-TABBING: Could be done as next-frame forwarded requested +void ImGui::NavProcessItemForTabbingRequest(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + + // Always store in NavMoveResultLocal (unlike directional request which uses NavMoveResultOther on sibling/flattened windows) + ImGuiNavItemData* result = &g.NavMoveResultLocal; + if (g.NavTabbingDir == +1) + { + // Tab Forward or SetKeyboardFocusHere() with >= 0 + if (g.NavTabbingResultFirst.ID == 0) + NavApplyItemToResult(&g.NavTabbingResultFirst); + if (--g.NavTabbingCounter == 0) + NavMoveRequestResolveWithLastItem(result); + else if (g.NavId == id) + g.NavTabbingCounter = 1; + } + else if (g.NavTabbingDir == -1) + { + // Tab Backward + if (g.NavId == id) + { + if (result->ID) + { + g.NavMoveScoringItems = false; + NavUpdateAnyRequestFlag(); + } + } + else + { + NavApplyItemToResult(result); + } + } + else if (g.NavTabbingDir == 0) + { + // Tab Init + if (g.NavTabbingResultFirst.ID == 0) + NavMoveRequestResolveWithLastItem(&g.NavTabbingResultFirst); + } +} + +bool ImGui::NavMoveRequestButNoResultYet() +{ + ImGuiContext& g = *GImGui; + return g.NavMoveScoringItems && g.NavMoveResultLocal.ID == 0 && g.NavMoveResultOther.ID == 0; +} + +// FIXME: ScoringRect is not set +void ImGui::NavMoveRequestSubmit(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindow != NULL); + + if (move_flags & ImGuiNavMoveFlags_Tabbing) + move_flags |= ImGuiNavMoveFlags_AllowCurrentNavId; + + g.NavMoveSubmitted = g.NavMoveScoringItems = true; + g.NavMoveDir = move_dir; + g.NavMoveDirForDebug = move_dir; + g.NavMoveClipDir = clip_dir; + g.NavMoveFlags = move_flags; + g.NavMoveScrollFlags = scroll_flags; + g.NavMoveForwardToNextFrame = false; + g.NavMoveKeyMods = g.IO.KeyMods; + g.NavTabbingCounter = 0; + g.NavMoveResultLocal.Clear(); + g.NavMoveResultLocalVisible.Clear(); + g.NavMoveResultOther.Clear(); + NavUpdateAnyRequestFlag(); +} + +void ImGui::NavMoveRequestResolveWithLastItem(ImGuiNavItemData* result) +{ + ImGuiContext& g = *GImGui; + g.NavMoveScoringItems = false; // Ensure request doesn't need more processing + NavApplyItemToResult(result); + NavUpdateAnyRequestFlag(); +} + +void ImGui::NavMoveRequestCancel() +{ + ImGuiContext& g = *GImGui; + g.NavMoveSubmitted = g.NavMoveScoringItems = false; + NavUpdateAnyRequestFlag(); +} + +// Forward will reuse the move request again on the next frame (generally with modifications done to it) +void ImGui::NavMoveRequestForward(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavMoveForwardToNextFrame == false); + NavMoveRequestCancel(); + g.NavMoveForwardToNextFrame = true; + g.NavMoveDir = move_dir; + g.NavMoveClipDir = clip_dir; + g.NavMoveFlags = move_flags | ImGuiNavMoveFlags_Forwarded; + g.NavMoveScrollFlags = scroll_flags; +} + +// Navigation wrap-around logic is delayed to the end of the frame because this operation is only valid after entire +// popup is assembled and in case of appended popups it is not clear which EndPopup() call is final. +void ImGui::NavMoveRequestTryWrapping(ImGuiWindow* window, ImGuiNavMoveFlags wrap_flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(wrap_flags != 0); // Call with _WrapX, _WrapY, _LoopX, _LoopY + // In theory we should test for NavMoveRequestButNoResultYet() but there's no point doing it, NavEndFrame() will do the same test + if (g.NavWindow == window && g.NavMoveScoringItems && g.NavLayer == ImGuiNavLayer_Main) + g.NavMoveFlags |= wrap_flags; +} + +// FIXME: This could be replaced by updating a frame number in each window when (window == NavWindow) and (NavLayer == 0). +// This way we could find the last focused window among our children. It would be much less confusing this way? +static void ImGui::NavSaveLastChildNavWindowIntoParent(ImGuiWindow* nav_window) +{ + ImGuiWindow* parent = nav_window; + while (parent && parent->RootWindow != parent && (parent->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) + parent = parent->ParentWindow; + if (parent && parent != nav_window) + parent->NavLastChildNavWindow = nav_window; +} + +// Restore the last focused child. +// Call when we are expected to land on the Main Layer (0) after FocusWindow() +static ImGuiWindow* ImGui::NavRestoreLastChildNavWindow(ImGuiWindow* window) +{ + if (window->NavLastChildNavWindow && window->NavLastChildNavWindow->WasActive) + return window->NavLastChildNavWindow; + return window; +} + +void ImGui::NavRestoreLayer(ImGuiNavLayer layer) +{ + ImGuiContext& g = *GImGui; + if (layer == ImGuiNavLayer_Main) + g.NavWindow = NavRestoreLastChildNavWindow(g.NavWindow); + ImGuiWindow* window = g.NavWindow; + if (window->NavLastIds[layer] != 0) + { + SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]); + } + else + { + g.NavLayer = layer; + NavInitWindow(window, true); + } +} + +void ImGui::NavRestoreHighlightAfterMove() +{ + ImGuiContext& g = *GImGui; + g.NavDisableHighlight = false; + g.NavDisableMouseHover = g.NavMousePosDirty = true; +} + +static inline void ImGui::NavUpdateAnyRequestFlag() +{ + ImGuiContext& g = *GImGui; + g.NavAnyRequest = g.NavMoveScoringItems || g.NavInitRequest || (IMGUI_DEBUG_NAV_SCORING && g.NavWindow != NULL); + if (g.NavAnyRequest) + IM_ASSERT(g.NavWindow != NULL); +} + +// This needs to be called before we submit any widget (aka in or before Begin) +void ImGui::NavInitWindow(ImGuiWindow* window, bool force_reinit) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(window == g.NavWindow); + + if (window->Flags & ImGuiWindowFlags_NoNavInputs) + { + g.NavId = g.NavFocusScopeId = 0; + return; + } + + bool init_for_nav = false; + if (window == window->RootWindow || (window->Flags & ImGuiWindowFlags_Popup) || (window->NavLastIds[0] == 0) || force_reinit) + init_for_nav = true; + IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: from NavInitWindow(), init_for_nav=%d, window=\"%s\", layer=%d\n", init_for_nav, window->Name, g.NavLayer); + if (init_for_nav) + { + SetNavID(0, g.NavLayer, 0, ImRect()); + g.NavInitRequest = true; + g.NavInitRequestFromMove = false; + g.NavInitResultId = 0; + g.NavInitResultRectRel = ImRect(); + NavUpdateAnyRequestFlag(); + } + else + { + g.NavId = window->NavLastIds[0]; + g.NavFocusScopeId = 0; + } +} + +static ImVec2 ImGui::NavCalcPreferredRefPos() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + if (g.NavDisableHighlight || !g.NavDisableMouseHover || !window) + { + // Mouse (we need a fallback in case the mouse becomes invalid after being used) + // The +1.0f offset when stored by OpenPopupEx() allows reopening this or another popup (same or another mouse button) while not moving the mouse, it is pretty standard. + // In theory we could move that +1.0f offset in OpenPopupEx() + ImVec2 p = IsMousePosValid(&g.IO.MousePos) ? g.IO.MousePos : g.MouseLastValidPos; + return ImVec2(p.x + 1.0f, p.y); + } + else + { + // When navigation is active and mouse is disabled, pick a position around the bottom left of the currently navigated item + // Take account of upcoming scrolling (maybe set mouse pos should be done in EndFrame?) + ImRect rect_rel = WindowRectRelToAbs(window, window->NavRectRel[g.NavLayer]); + if (window->LastFrameActive != g.FrameCount && (window->ScrollTarget.x != FLT_MAX || window->ScrollTarget.y != FLT_MAX)) + { + ImVec2 next_scroll = CalcNextScrollFromScrollTargetAndClamp(window); + rect_rel.Translate(window->Scroll - next_scroll); + } + ImVec2 pos = ImVec2(rect_rel.Min.x + ImMin(g.Style.FramePadding.x * 4, rect_rel.GetWidth()), rect_rel.Max.y - ImMin(g.Style.FramePadding.y, rect_rel.GetHeight())); + ImGuiViewport* viewport = GetMainViewport(); + return ImFloor(ImClamp(pos, viewport->Pos, viewport->Pos + viewport->Size)); // ImFloor() is important because non-integer mouse position application in backend might be lossy and result in undesirable non-zero delta. + } +} + +const char* ImGui::GetNavInputName(ImGuiNavInput n) +{ + static const char* names[] = + { + "Activate", "Cancel", "Input", "Menu", "DpadLeft", "DpadRight", "DpadUp", "DpadDown", "LStickLeft", "LStickRight", "LStickUp", "LStickDown", + "FocusPrev", "FocusNext", "TweakSlow", "TweakFast", "KeyLeft", "KeyRight", "KeyUp", "KeyDown" + }; + IM_ASSERT(IM_ARRAYSIZE(names) == ImGuiNavInput_COUNT); + IM_ASSERT(n >= 0 && n < ImGuiNavInput_COUNT); + return names[n]; +} + +float ImGui::GetNavInputAmount(ImGuiNavInput n, ImGuiInputReadMode mode) +{ + ImGuiContext& g = *GImGui; + if (mode == ImGuiInputReadMode_Down) + return g.IO.NavInputs[n]; // Instant, read analog input (0.0f..1.0f, as provided by user) + + const float t = g.IO.NavInputsDownDuration[n]; + if (t < 0.0f && mode == ImGuiInputReadMode_Released) // Return 1.0f when just released, no repeat, ignore analog input. + return (g.IO.NavInputsDownDurationPrev[n] >= 0.0f ? 1.0f : 0.0f); + if (t < 0.0f) + return 0.0f; + if (mode == ImGuiInputReadMode_Pressed) // Return 1.0f when just pressed, no repeat, ignore analog input. + return (t == 0.0f) ? 1.0f : 0.0f; + if (mode == ImGuiInputReadMode_Repeat) + return (float)CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, g.IO.KeyRepeatDelay * 0.72f, g.IO.KeyRepeatRate * 0.80f); + if (mode == ImGuiInputReadMode_RepeatSlow) + return (float)CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, g.IO.KeyRepeatDelay * 1.25f, g.IO.KeyRepeatRate * 2.00f); + if (mode == ImGuiInputReadMode_RepeatFast) + return (float)CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, g.IO.KeyRepeatDelay * 0.72f, g.IO.KeyRepeatRate * 0.30f); + return 0.0f; +} + +ImVec2 ImGui::GetNavInputAmount2d(ImGuiNavDirSourceFlags dir_sources, ImGuiInputReadMode mode, float slow_factor, float fast_factor) +{ + ImVec2 delta(0.0f, 0.0f); + if (dir_sources & ImGuiNavDirSourceFlags_RawKeyboard) + delta += ImVec2((float)IsKeyDown(ImGuiKey_RightArrow) - (float)IsKeyDown(ImGuiKey_LeftArrow), (float)IsKeyDown(ImGuiKey_DownArrow) - (float)IsKeyDown(ImGuiKey_UpArrow)); + if (dir_sources & ImGuiNavDirSourceFlags_Keyboard) + delta += ImVec2(GetNavInputAmount(ImGuiNavInput_KeyRight_, mode) - GetNavInputAmount(ImGuiNavInput_KeyLeft_, mode), GetNavInputAmount(ImGuiNavInput_KeyDown_, mode) - GetNavInputAmount(ImGuiNavInput_KeyUp_, mode)); + if (dir_sources & ImGuiNavDirSourceFlags_PadDPad) + delta += ImVec2(GetNavInputAmount(ImGuiNavInput_DpadRight, mode) - GetNavInputAmount(ImGuiNavInput_DpadLeft, mode), GetNavInputAmount(ImGuiNavInput_DpadDown, mode) - GetNavInputAmount(ImGuiNavInput_DpadUp, mode)); + if (dir_sources & ImGuiNavDirSourceFlags_PadLStick) + delta += ImVec2(GetNavInputAmount(ImGuiNavInput_LStickRight, mode) - GetNavInputAmount(ImGuiNavInput_LStickLeft, mode), GetNavInputAmount(ImGuiNavInput_LStickDown, mode) - GetNavInputAmount(ImGuiNavInput_LStickUp, mode)); + if (slow_factor != 0.0f && IsNavInputDown(ImGuiNavInput_TweakSlow)) + delta *= slow_factor; + if (fast_factor != 0.0f && IsNavInputDown(ImGuiNavInput_TweakFast)) + delta *= fast_factor; + return delta; +} + +static void ImGui::NavUpdate() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + io.WantSetMousePos = false; + //if (g.NavScoringDebugCount > 0) IMGUI_DEBUG_LOG("NavScoringDebugCount %d for '%s' layer %d (Init:%d, Move:%d)\n", g.NavScoringDebugCount, g.NavWindow ? g.NavWindow->Name : "NULL", g.NavLayer, g.NavInitRequest || g.NavInitResultId != 0, g.NavMoveRequest); + + // Update Gamepad->Nav inputs mapping + // Set input source as Gamepad when buttons are pressed (as some features differs when used with Gamepad vs Keyboard) + const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + if (nav_gamepad_active && g.IO.BackendUsingLegacyNavInputArray == false) + { + for (int n = 0; n < ImGuiNavInput_COUNT; n++) + IM_ASSERT(io.NavInputs[n] == 0.0f && "Backend needs to either only use io.AddKeyEvent()/io.AddKeyAnalogEvent(), either only fill legacy io.NavInputs[]. Not both!"); + #define NAV_MAP_KEY(_KEY, _NAV_INPUT, _ACTIVATE_NAV) do { io.NavInputs[_NAV_INPUT] = io.KeysData[_KEY - ImGuiKey_KeysData_OFFSET].AnalogValue; if (_ACTIVATE_NAV && io.NavInputs[_NAV_INPUT] > 0.0f) { g.NavInputSource = ImGuiInputSource_Gamepad; } } while (0) + NAV_MAP_KEY(ImGuiKey_GamepadFaceDown, ImGuiNavInput_Activate, true); + NAV_MAP_KEY(ImGuiKey_GamepadFaceRight, ImGuiNavInput_Cancel, true); + NAV_MAP_KEY(ImGuiKey_GamepadFaceLeft, ImGuiNavInput_Menu, true); + NAV_MAP_KEY(ImGuiKey_GamepadFaceUp, ImGuiNavInput_Input, true); + NAV_MAP_KEY(ImGuiKey_GamepadDpadLeft, ImGuiNavInput_DpadLeft, true); + NAV_MAP_KEY(ImGuiKey_GamepadDpadRight, ImGuiNavInput_DpadRight, true); + NAV_MAP_KEY(ImGuiKey_GamepadDpadUp, ImGuiNavInput_DpadUp, true); + NAV_MAP_KEY(ImGuiKey_GamepadDpadDown, ImGuiNavInput_DpadDown, true); + NAV_MAP_KEY(ImGuiKey_GamepadL1, ImGuiNavInput_FocusPrev, false); + NAV_MAP_KEY(ImGuiKey_GamepadR1, ImGuiNavInput_FocusNext, false); + NAV_MAP_KEY(ImGuiKey_GamepadL1, ImGuiNavInput_TweakSlow, false); + NAV_MAP_KEY(ImGuiKey_GamepadR1, ImGuiNavInput_TweakFast, false); + NAV_MAP_KEY(ImGuiKey_GamepadLStickLeft, ImGuiNavInput_LStickLeft, false); + NAV_MAP_KEY(ImGuiKey_GamepadLStickRight, ImGuiNavInput_LStickRight, false); + NAV_MAP_KEY(ImGuiKey_GamepadLStickUp, ImGuiNavInput_LStickUp, false); + NAV_MAP_KEY(ImGuiKey_GamepadLStickDown, ImGuiNavInput_LStickDown, false); + #undef NAV_MAP_KEY + } + + // Update Keyboard->Nav inputs mapping + const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + if (nav_keyboard_active) + { + #define NAV_MAP_KEY(_KEY, _NAV_INPUT) do { if (IsKeyDown(_KEY)) { io.NavInputs[_NAV_INPUT] = 1.0f; g.NavInputSource = ImGuiInputSource_Keyboard; } } while (0) + NAV_MAP_KEY(ImGuiKey_Space, ImGuiNavInput_Activate ); + NAV_MAP_KEY(ImGuiKey_Enter, ImGuiNavInput_Input ); + NAV_MAP_KEY(ImGuiKey_Escape, ImGuiNavInput_Cancel ); + NAV_MAP_KEY(ImGuiKey_LeftArrow, ImGuiNavInput_KeyLeft_ ); + NAV_MAP_KEY(ImGuiKey_RightArrow,ImGuiNavInput_KeyRight_); + NAV_MAP_KEY(ImGuiKey_UpArrow, ImGuiNavInput_KeyUp_ ); + NAV_MAP_KEY(ImGuiKey_DownArrow, ImGuiNavInput_KeyDown_ ); + if (io.KeyCtrl) + io.NavInputs[ImGuiNavInput_TweakSlow] = 1.0f; + if (io.KeyShift) + io.NavInputs[ImGuiNavInput_TweakFast] = 1.0f; + #undef NAV_MAP_KEY + } + memcpy(io.NavInputsDownDurationPrev, io.NavInputsDownDuration, sizeof(io.NavInputsDownDuration)); + for (int i = 0; i < IM_ARRAYSIZE(io.NavInputs); i++) + io.NavInputsDownDuration[i] = (io.NavInputs[i] > 0.0f) ? (io.NavInputsDownDuration[i] < 0.0f ? 0.0f : io.NavInputsDownDuration[i] + io.DeltaTime) : -1.0f; + + // Process navigation init request (select first/default focus) + if (g.NavInitResultId != 0) + NavInitRequestApplyResult(); + g.NavInitRequest = false; + g.NavInitRequestFromMove = false; + g.NavInitResultId = 0; + g.NavJustMovedToId = 0; + + // Process navigation move request + if (g.NavMoveSubmitted) + NavMoveRequestApplyResult(); + g.NavTabbingCounter = 0; + g.NavMoveSubmitted = g.NavMoveScoringItems = false; + + // Schedule mouse position update (will be done at the bottom of this function, after 1) processing all move requests and 2) updating scrolling) + bool set_mouse_pos = false; + if (g.NavMousePosDirty && g.NavIdIsAlive) + if (!g.NavDisableHighlight && g.NavDisableMouseHover && g.NavWindow) + set_mouse_pos = true; + g.NavMousePosDirty = false; + IM_ASSERT(g.NavLayer == ImGuiNavLayer_Main || g.NavLayer == ImGuiNavLayer_Menu); + + // Store our return window (for returning from Menu Layer to Main Layer) and clear it as soon as we step back in our own Layer 0 + if (g.NavWindow) + NavSaveLastChildNavWindowIntoParent(g.NavWindow); + if (g.NavWindow && g.NavWindow->NavLastChildNavWindow != NULL && g.NavLayer == ImGuiNavLayer_Main) + g.NavWindow->NavLastChildNavWindow = NULL; + + // Update CTRL+TAB and Windowing features (hold Square to move/resize/etc.) + NavUpdateWindowing(); + + // Set output flags for user application + io.NavActive = (nav_keyboard_active || nav_gamepad_active) && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs); + io.NavVisible = (io.NavActive && g.NavId != 0 && !g.NavDisableHighlight) || (g.NavWindowingTarget != NULL); + + // Process NavCancel input (to close a popup, get back to parent, clear focus) + NavUpdateCancelRequest(); + + // Process manual activation request + g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = g.NavActivateInputId = 0; + g.NavActivateFlags = ImGuiActivateFlags_None; + if (g.NavId != 0 && !g.NavDisableHighlight && !g.NavWindowingTarget && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) + { + bool activate_down = IsNavInputDown(ImGuiNavInput_Activate); + bool input_down = IsNavInputDown(ImGuiNavInput_Input); + bool activate_pressed = activate_down && IsNavInputTest(ImGuiNavInput_Activate, ImGuiInputReadMode_Pressed); + bool input_pressed = input_down && IsNavInputTest(ImGuiNavInput_Input, ImGuiInputReadMode_Pressed); + if (g.ActiveId == 0 && activate_pressed) + { + g.NavActivateId = g.NavId; + g.NavActivateFlags = ImGuiActivateFlags_PreferTweak; + } + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && input_pressed) + { + g.NavActivateInputId = g.NavId; + g.NavActivateFlags = ImGuiActivateFlags_PreferInput; + } + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && activate_down) + g.NavActivateDownId = g.NavId; + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && activate_pressed) + g.NavActivatePressedId = g.NavId; + } + if (g.NavWindow && (g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) + g.NavDisableHighlight = true; + if (g.NavActivateId != 0) + IM_ASSERT(g.NavActivateDownId == g.NavActivateId); + + // Process programmatic activation request + // FIXME-NAV: Those should eventually be queued (unlike focus they don't cancel each others) + if (g.NavNextActivateId != 0) + { + if (g.NavNextActivateFlags & ImGuiActivateFlags_PreferInput) + g.NavActivateInputId = g.NavNextActivateId; + else + g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = g.NavNextActivateId; + g.NavActivateFlags = g.NavNextActivateFlags; + } + g.NavNextActivateId = 0; + + // Process move requests + NavUpdateCreateMoveRequest(); + if (g.NavMoveDir == ImGuiDir_None) + NavUpdateCreateTabbingRequest(); + NavUpdateAnyRequestFlag(); + g.NavIdIsAlive = false; + + // Scrolling + if (g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs) && !g.NavWindowingTarget) + { + // *Fallback* manual-scroll with Nav directional keys when window has no navigable item + ImGuiWindow* window = g.NavWindow; + const float scroll_speed = IM_ROUND(window->CalcFontSize() * 100 * io.DeltaTime); // We need round the scrolling speed because sub-pixel scroll isn't reliably supported. + const ImGuiDir move_dir = g.NavMoveDir; + if (window->DC.NavLayersActiveMask == 0x00 && window->DC.NavHasScroll && move_dir != ImGuiDir_None) + { + if (move_dir == ImGuiDir_Left || move_dir == ImGuiDir_Right) + SetScrollX(window, ImFloor(window->Scroll.x + ((move_dir == ImGuiDir_Left) ? -1.0f : +1.0f) * scroll_speed)); + if (move_dir == ImGuiDir_Up || move_dir == ImGuiDir_Down) + SetScrollY(window, ImFloor(window->Scroll.y + ((move_dir == ImGuiDir_Up) ? -1.0f : +1.0f) * scroll_speed)); + } + + // *Normal* Manual scroll with NavScrollXXX keys + // Next movement request will clamp the NavId reference rectangle to the visible area, so navigation will resume within those bounds. + ImVec2 scroll_dir = GetNavInputAmount2d(ImGuiNavDirSourceFlags_PadLStick, ImGuiInputReadMode_Down, 1.0f / 10.0f, 10.0f); + if (scroll_dir.x != 0.0f && window->ScrollbarX) + SetScrollX(window, ImFloor(window->Scroll.x + scroll_dir.x * scroll_speed)); + if (scroll_dir.y != 0.0f) + SetScrollY(window, ImFloor(window->Scroll.y + scroll_dir.y * scroll_speed)); + } + + // Always prioritize mouse highlight if navigation is disabled + if (!nav_keyboard_active && !nav_gamepad_active) + { + g.NavDisableHighlight = true; + g.NavDisableMouseHover = set_mouse_pos = false; + } + + // Update mouse position if requested + // (This will take into account the possibility that a Scroll was queued in the window to offset our absolute mouse position before scroll has been applied) + if (set_mouse_pos && (io.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos) && (io.BackendFlags & ImGuiBackendFlags_HasSetMousePos)) + { + io.MousePos = io.MousePosPrev = NavCalcPreferredRefPos(); + io.WantSetMousePos = true; + //IMGUI_DEBUG_LOG("SetMousePos: (%.1f,%.1f)\n", io.MousePos.x, io.MousePos.y); + } + + // [DEBUG] + g.NavScoringDebugCount = 0; +#if IMGUI_DEBUG_NAV_RECTS + if (g.NavWindow) + { + ImDrawList* draw_list = GetForegroundDrawList(g.NavWindow); + if (1) { for (int layer = 0; layer < 2; layer++) { ImRect r = WindowRectRelToAbs(g.NavWindow, g.NavWindow->NavRectRel[layer]); draw_list->AddRect(r.Min, r.Max, IM_COL32(255,200,0,255)); } } // [DEBUG] + if (1) { ImU32 col = (!g.NavWindow->Hidden) ? IM_COL32(255,0,255,255) : IM_COL32(255,0,0,255); ImVec2 p = NavCalcPreferredRefPos(); char buf[32]; ImFormatString(buf, 32, "%d", g.NavLayer); draw_list->AddCircleFilled(p, 3.0f, col); draw_list->AddText(NULL, 13.0f, p + ImVec2(8,-4), col, buf); } + } +#endif +} + +void ImGui::NavInitRequestApplyResult() +{ + // In very rare cases g.NavWindow may be null (e.g. clearing focus after requesting an init request, which does happen when releasing Alt while clicking on void) + ImGuiContext& g = *GImGui; + if (!g.NavWindow) + return; + + // Apply result from previous navigation init request (will typically select the first item, unless SetItemDefaultFocus() has been called) + // FIXME-NAV: On _NavFlattened windows, g.NavWindow will only be updated during subsequent frame. Not a problem currently. + IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: result NavID 0x%08X in Layer %d Window \"%s\"\n", g.NavInitResultId, g.NavLayer, g.NavWindow->Name); + SetNavID(g.NavInitResultId, g.NavLayer, 0, g.NavInitResultRectRel); + g.NavIdIsAlive = true; // Mark as alive from previous frame as we got a result + if (g.NavInitRequestFromMove) + NavRestoreHighlightAfterMove(); +} + +void ImGui::NavUpdateCreateMoveRequest() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + ImGuiWindow* window = g.NavWindow; + + if (g.NavMoveForwardToNextFrame && window != NULL) + { + // Forwarding previous request (which has been modified, e.g. wrap around menus rewrite the requests with a starting rectangle at the other side of the window) + // (preserve most state, which were already set by the NavMoveRequestForward() function) + IM_ASSERT(g.NavMoveDir != ImGuiDir_None && g.NavMoveClipDir != ImGuiDir_None); + IM_ASSERT(g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded); + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequestForward %d\n", g.NavMoveDir); + } + else + { + // Initiate directional inputs request + g.NavMoveDir = ImGuiDir_None; + g.NavMoveFlags = ImGuiNavMoveFlags_None; + g.NavMoveScrollFlags = ImGuiScrollFlags_None; + if (window && !g.NavWindowingTarget && !(window->Flags & ImGuiWindowFlags_NoNavInputs)) + { + const ImGuiInputReadMode read_mode = ImGuiInputReadMode_Repeat; + if (!IsActiveIdUsingNavDir(ImGuiDir_Left) && (IsNavInputTest(ImGuiNavInput_DpadLeft, read_mode) || IsNavInputTest(ImGuiNavInput_KeyLeft_, read_mode))) { g.NavMoveDir = ImGuiDir_Left; } + if (!IsActiveIdUsingNavDir(ImGuiDir_Right) && (IsNavInputTest(ImGuiNavInput_DpadRight, read_mode) || IsNavInputTest(ImGuiNavInput_KeyRight_, read_mode))) { g.NavMoveDir = ImGuiDir_Right; } + if (!IsActiveIdUsingNavDir(ImGuiDir_Up) && (IsNavInputTest(ImGuiNavInput_DpadUp, read_mode) || IsNavInputTest(ImGuiNavInput_KeyUp_, read_mode))) { g.NavMoveDir = ImGuiDir_Up; } + if (!IsActiveIdUsingNavDir(ImGuiDir_Down) && (IsNavInputTest(ImGuiNavInput_DpadDown, read_mode) || IsNavInputTest(ImGuiNavInput_KeyDown_, read_mode))) { g.NavMoveDir = ImGuiDir_Down; } + } + g.NavMoveClipDir = g.NavMoveDir; + g.NavScoringNoClipRect = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); + } + + // Update PageUp/PageDown/Home/End scroll + // FIXME-NAV: Consider enabling those keys even without the master ImGuiConfigFlags_NavEnableKeyboard flag? + const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + float scoring_rect_offset_y = 0.0f; + if (window && g.NavMoveDir == ImGuiDir_None && nav_keyboard_active) + scoring_rect_offset_y = NavUpdatePageUpPageDown(); + if (scoring_rect_offset_y != 0.0f) + { + g.NavScoringNoClipRect = window->InnerRect; + g.NavScoringNoClipRect.TranslateY(scoring_rect_offset_y); + } + + // [DEBUG] Always send a request +#if IMGUI_DEBUG_NAV_SCORING + if (io.KeyCtrl && IsKeyPressed(ImGuiKey_C)) + g.NavMoveDirForDebug = (ImGuiDir)((g.NavMoveDirForDebug + 1) & 3); + if (io.KeyCtrl && g.NavMoveDir == ImGuiDir_None) + { + g.NavMoveDir = g.NavMoveDirForDebug; + g.NavMoveFlags |= ImGuiNavMoveFlags_DebugNoResult; + } +#endif + + // Submit + g.NavMoveForwardToNextFrame = false; + if (g.NavMoveDir != ImGuiDir_None) + NavMoveRequestSubmit(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); + + // Moving with no reference triggers a init request (will be used as a fallback if the direction fails to find a match) + if (g.NavMoveSubmitted && g.NavId == 0) + { + IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: from move, window \"%s\", layer=%d\n", g.NavWindow->Name, g.NavLayer); + g.NavInitRequest = g.NavInitRequestFromMove = true; + g.NavInitResultId = 0; + g.NavDisableHighlight = false; + } + + // When using gamepad, we project the reference nav bounding box into window visible area. + // This is to allow resuming navigation inside the visible area after doing a large amount of scrolling, since with gamepad every movements are relative + // (can't focus a visible object like we can with the mouse). + if (g.NavMoveSubmitted && g.NavInputSource == ImGuiInputSource_Gamepad && g.NavLayer == ImGuiNavLayer_Main && window != NULL)// && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded)) + { + bool clamp_x = (g.NavMoveFlags & (ImGuiNavMoveFlags_LoopX | ImGuiNavMoveFlags_WrapX)) == 0; + bool clamp_y = (g.NavMoveFlags & (ImGuiNavMoveFlags_LoopY | ImGuiNavMoveFlags_WrapY)) == 0; + ImRect inner_rect_rel = WindowRectAbsToRel(window, ImRect(window->InnerRect.Min - ImVec2(1, 1), window->InnerRect.Max + ImVec2(1, 1))); + if ((clamp_x || clamp_y) && !inner_rect_rel.Contains(window->NavRectRel[g.NavLayer])) + { + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequest: clamp NavRectRel for gamepad move\n"); + float pad_x = ImMin(inner_rect_rel.GetWidth(), window->CalcFontSize() * 0.5f); + float pad_y = ImMin(inner_rect_rel.GetHeight(), window->CalcFontSize() * 0.5f); // Terrible approximation for the intent of starting navigation from first fully visible item + inner_rect_rel.Min.x = clamp_x ? (inner_rect_rel.Min.x + pad_x) : -FLT_MAX; + inner_rect_rel.Max.x = clamp_x ? (inner_rect_rel.Max.x - pad_x) : +FLT_MAX; + inner_rect_rel.Min.y = clamp_y ? (inner_rect_rel.Min.y + pad_y) : -FLT_MAX; + inner_rect_rel.Max.y = clamp_y ? (inner_rect_rel.Max.y - pad_y) : +FLT_MAX; + window->NavRectRel[g.NavLayer].ClipWithFull(inner_rect_rel); + g.NavId = g.NavFocusScopeId = 0; + } + } + + // For scoring we use a single segment on the left side our current item bounding box (not touching the edge to avoid box overlap with zero-spaced items) + ImRect scoring_rect; + if (window != NULL) + { + ImRect nav_rect_rel = !window->NavRectRel[g.NavLayer].IsInverted() ? window->NavRectRel[g.NavLayer] : ImRect(0, 0, 0, 0); + scoring_rect = WindowRectRelToAbs(window, nav_rect_rel); + scoring_rect.TranslateY(scoring_rect_offset_y); + scoring_rect.Min.x = ImMin(scoring_rect.Min.x + 1.0f, scoring_rect.Max.x); + scoring_rect.Max.x = scoring_rect.Min.x; + IM_ASSERT(!scoring_rect.IsInverted()); // Ensure if we have a finite, non-inverted bounding box here will allows us to remove extraneous ImFabs() calls in NavScoreItem(). + //GetForegroundDrawList()->AddRect(scoring_rect.Min, scoring_rect.Max, IM_COL32(255,200,0,255)); // [DEBUG] + //if (!g.NavScoringNoClipRect.IsInverted()) { GetForegroundDrawList()->AddRect(g.NavScoringNoClipRect.Min, g.NavScoringNoClipRect.Max, IM_COL32(255, 200, 0, 255)); } // [DEBUG] + } + g.NavScoringRect = scoring_rect; + g.NavScoringNoClipRect.Add(scoring_rect); +} + +void ImGui::NavUpdateCreateTabbingRequest() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + IM_ASSERT(g.NavMoveDir == ImGuiDir_None); + if (window == NULL || g.NavWindowingTarget != NULL || (window->Flags & ImGuiWindowFlags_NoNavInputs)) + return; + + const bool tab_pressed = IsKeyPressed(ImGuiKey_Tab, true) && !IsActiveIdUsingKey(ImGuiKey_Tab) && !g.IO.KeyCtrl && !g.IO.KeyAlt; + if (!tab_pressed) + return; + + // Initiate tabbing request + // (this is ALWAYS ENABLED, regardless of ImGuiConfigFlags_NavEnableKeyboard flag!) + // Initially this was designed to use counters and modulo arithmetic, but that could not work with unsubmitted items (list clipper). Instead we use a strategy close to other move requests. + // See NavProcessItemForTabbingRequest() for a description of the various forward/backward tabbing cases with and without wrapping. + //// FIXME: We use (g.ActiveId == 0) but (g.NavDisableHighlight == false) might be righter once we can tab through anything + g.NavTabbingDir = g.IO.KeyShift ? -1 : (g.ActiveId == 0) ? 0 : +1; + ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; + ImGuiDir clip_dir = (g.NavTabbingDir < 0) ? ImGuiDir_Up : ImGuiDir_Down; + NavMoveRequestSubmit(ImGuiDir_None, clip_dir, ImGuiNavMoveFlags_Tabbing, scroll_flags); // FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable. + g.NavTabbingResultFirst.Clear(); + g.NavTabbingCounter = -1; +} + +// Apply result from previous frame navigation directional move request. Always called from NavUpdate() +void ImGui::NavMoveRequestApplyResult() +{ + ImGuiContext& g = *GImGui; +#if IMGUI_DEBUG_NAV_SCORING + if (g.NavMoveFlags & ImGuiNavMoveFlags_DebugNoResult) // [DEBUG] Scoring all items in NavWindow at all times + return; +#endif + + // Select which result to use + ImGuiNavItemData* result = (g.NavMoveResultLocal.ID != 0) ? &g.NavMoveResultLocal : (g.NavMoveResultOther.ID != 0) ? &g.NavMoveResultOther : NULL; + + // Tabbing forward wrap + if (g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) + if ((g.NavTabbingCounter == 1 || g.NavTabbingDir == 0) && g.NavTabbingResultFirst.ID) + result = &g.NavTabbingResultFirst; + + // In a situation when there is no results but NavId != 0, re-enable the Navigation highlight (because g.NavId is not considered as a possible result) + if (result == NULL) + { + if (g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) + g.NavMoveFlags |= ImGuiNavMoveFlags_DontSetNavHighlight; + if (g.NavId != 0 && (g.NavMoveFlags & ImGuiNavMoveFlags_DontSetNavHighlight) == 0) + NavRestoreHighlightAfterMove(); + return; + } + + // PageUp/PageDown behavior first jumps to the bottom/top mostly visible item, _otherwise_ use the result from the previous/next page. + if (g.NavMoveFlags & ImGuiNavMoveFlags_AlsoScoreVisibleSet) + if (g.NavMoveResultLocalVisible.ID != 0 && g.NavMoveResultLocalVisible.ID != g.NavId) + result = &g.NavMoveResultLocalVisible; + + // Maybe entering a flattened child from the outside? In this case solve the tie using the regular scoring rules. + if (result != &g.NavMoveResultOther && g.NavMoveResultOther.ID != 0 && g.NavMoveResultOther.Window->ParentWindow == g.NavWindow) + if ((g.NavMoveResultOther.DistBox < result->DistBox) || (g.NavMoveResultOther.DistBox == result->DistBox && g.NavMoveResultOther.DistCenter < result->DistCenter)) + result = &g.NavMoveResultOther; + IM_ASSERT(g.NavWindow && result->Window); + + // Scroll to keep newly navigated item fully into view. + if (g.NavLayer == ImGuiNavLayer_Main) + { + if (g.NavMoveFlags & ImGuiNavMoveFlags_ScrollToEdgeY) + { + // FIXME: Should remove this + float scroll_target = (g.NavMoveDir == ImGuiDir_Up) ? result->Window->ScrollMax.y : 0.0f; + SetScrollY(result->Window, scroll_target); + } + else + { + ImRect rect_abs = WindowRectRelToAbs(result->Window, result->RectRel); + ScrollToRectEx(result->Window, rect_abs, g.NavMoveScrollFlags); + } + } + + g.NavWindow = result->Window; + if (g.ActiveId != result->ID) + ClearActiveID(); + if (g.NavId != result->ID) + { + // Don't set NavJustMovedToId if just landed on the same spot (which may happen with ImGuiNavMoveFlags_AllowCurrentNavId) + g.NavJustMovedToId = result->ID; + g.NavJustMovedToFocusScopeId = result->FocusScopeId; + g.NavJustMovedToKeyMods = g.NavMoveKeyMods; + } + + // Focus + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequest: result NavID 0x%08X in Layer %d Window \"%s\"\n", result->ID, g.NavLayer, g.NavWindow->Name); + SetNavID(result->ID, g.NavLayer, result->FocusScopeId, result->RectRel); + + // Tabbing: Activates Inputable or Focus non-Inputable + if ((g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) && (result->InFlags & ImGuiItemFlags_Inputable)) + { + g.NavNextActivateId = result->ID; + g.NavNextActivateFlags = ImGuiActivateFlags_PreferInput | ImGuiActivateFlags_TryToPreserveState; + g.NavMoveFlags |= ImGuiNavMoveFlags_DontSetNavHighlight; + } + + // Activate + if (g.NavMoveFlags & ImGuiNavMoveFlags_Activate) + { + g.NavNextActivateId = result->ID; + g.NavNextActivateFlags = ImGuiActivateFlags_None; + } + + // Enable nav highlight + if ((g.NavMoveFlags & ImGuiNavMoveFlags_DontSetNavHighlight) == 0) + NavRestoreHighlightAfterMove(); +} + +// Process NavCancel input (to close a popup, get back to parent, clear focus) +// FIXME: In order to support e.g. Escape to clear a selection we'll need: +// - either to store the equivalent of ActiveIdUsingKeyInputMask for a FocusScope and test for it. +// - either to move most/all of those tests to the epilogue/end functions of the scope they are dealing with (e.g. exit child window in EndChild()) or in EndFrame(), to allow an earlier intercept +static void ImGui::NavUpdateCancelRequest() +{ + ImGuiContext& g = *GImGui; + if (!IsNavInputTest(ImGuiNavInput_Cancel, ImGuiInputReadMode_Pressed)) + return; + + IMGUI_DEBUG_LOG_NAV("[nav] ImGuiNavInput_Cancel\n"); + if (g.ActiveId != 0) + { + if (!IsActiveIdUsingNavInput(ImGuiNavInput_Cancel)) + ClearActiveID(); + } + else if (g.NavLayer != ImGuiNavLayer_Main) + { + // Leave the "menu" layer + NavRestoreLayer(ImGuiNavLayer_Main); + NavRestoreHighlightAfterMove(); + } + else if (g.NavWindow && g.NavWindow != g.NavWindow->RootWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_Popup) && g.NavWindow->ParentWindow) + { + // Exit child window + ImGuiWindow* child_window = g.NavWindow; + ImGuiWindow* parent_window = g.NavWindow->ParentWindow; + IM_ASSERT(child_window->ChildId != 0); + ImRect child_rect = child_window->Rect(); + FocusWindow(parent_window); + SetNavID(child_window->ChildId, ImGuiNavLayer_Main, 0, WindowRectAbsToRel(parent_window, child_rect)); + NavRestoreHighlightAfterMove(); + } + else if (g.OpenPopupStack.Size > 0) + { + // Close open popup/menu + if (!(g.OpenPopupStack.back().Window->Flags & ImGuiWindowFlags_Modal)) + ClosePopupToLevel(g.OpenPopupStack.Size - 1, true); + } + else + { + // Clear NavLastId for popups but keep it for regular child window so we can leave one and come back where we were + if (g.NavWindow && ((g.NavWindow->Flags & ImGuiWindowFlags_Popup) || !(g.NavWindow->Flags & ImGuiWindowFlags_ChildWindow))) + g.NavWindow->NavLastIds[0] = 0; + g.NavId = g.NavFocusScopeId = 0; + } +} + +// Handle PageUp/PageDown/Home/End keys +// Called from NavUpdateCreateMoveRequest() which will use our output to create a move request +// FIXME-NAV: This doesn't work properly with NavFlattened siblings as we use NavWindow rectangle for reference +// FIXME-NAV: how to get Home/End to aim at the beginning/end of a 2D grid? +static float ImGui::NavUpdatePageUpPageDown() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + if ((window->Flags & ImGuiWindowFlags_NoNavInputs) || g.NavWindowingTarget != NULL) + return 0.0f; + + const bool page_up_held = IsKeyDown(ImGuiKey_PageUp) && !IsActiveIdUsingKey(ImGuiKey_PageUp); + const bool page_down_held = IsKeyDown(ImGuiKey_PageDown) && !IsActiveIdUsingKey(ImGuiKey_PageDown); + const bool home_pressed = IsKeyPressed(ImGuiKey_Home) && !IsActiveIdUsingKey(ImGuiKey_Home); + const bool end_pressed = IsKeyPressed(ImGuiKey_End) && !IsActiveIdUsingKey(ImGuiKey_End); + if (page_up_held == page_down_held && home_pressed == end_pressed) // Proceed if either (not both) are pressed, otherwise early out + return 0.0f; + + if (g.NavLayer != ImGuiNavLayer_Main) + NavRestoreLayer(ImGuiNavLayer_Main); + + if (window->DC.NavLayersActiveMask == 0x00 && window->DC.NavHasScroll) + { + // Fallback manual-scroll when window has no navigable item + if (IsKeyPressed(ImGuiKey_PageUp, true)) + SetScrollY(window, window->Scroll.y - window->InnerRect.GetHeight()); + else if (IsKeyPressed(ImGuiKey_PageDown, true)) + SetScrollY(window, window->Scroll.y + window->InnerRect.GetHeight()); + else if (home_pressed) + SetScrollY(window, 0.0f); + else if (end_pressed) + SetScrollY(window, window->ScrollMax.y); + } + else + { + ImRect& nav_rect_rel = window->NavRectRel[g.NavLayer]; + const float page_offset_y = ImMax(0.0f, window->InnerRect.GetHeight() - window->CalcFontSize() * 1.0f + nav_rect_rel.GetHeight()); + float nav_scoring_rect_offset_y = 0.0f; + if (IsKeyPressed(ImGuiKey_PageUp, true)) + { + nav_scoring_rect_offset_y = -page_offset_y; + g.NavMoveDir = ImGuiDir_Down; // Because our scoring rect is offset up, we request the down direction (so we can always land on the last item) + g.NavMoveClipDir = ImGuiDir_Up; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet; + } + else if (IsKeyPressed(ImGuiKey_PageDown, true)) + { + nav_scoring_rect_offset_y = +page_offset_y; + g.NavMoveDir = ImGuiDir_Up; // Because our scoring rect is offset down, we request the up direction (so we can always land on the last item) + g.NavMoveClipDir = ImGuiDir_Down; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet; + } + else if (home_pressed) + { + // FIXME-NAV: handling of Home/End is assuming that the top/bottom most item will be visible with Scroll.y == 0/ScrollMax.y + // Scrolling will be handled via the ImGuiNavMoveFlags_ScrollToEdgeY flag, we don't scroll immediately to avoid scrolling happening before nav result. + // Preserve current horizontal position if we have any. + nav_rect_rel.Min.y = nav_rect_rel.Max.y = 0.0f; + if (nav_rect_rel.IsInverted()) + nav_rect_rel.Min.x = nav_rect_rel.Max.x = 0.0f; + g.NavMoveDir = ImGuiDir_Down; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_ScrollToEdgeY; + // FIXME-NAV: MoveClipDir left to _None, intentional? + } + else if (end_pressed) + { + nav_rect_rel.Min.y = nav_rect_rel.Max.y = window->ContentSize.y; + if (nav_rect_rel.IsInverted()) + nav_rect_rel.Min.x = nav_rect_rel.Max.x = 0.0f; + g.NavMoveDir = ImGuiDir_Up; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_ScrollToEdgeY; + // FIXME-NAV: MoveClipDir left to _None, intentional? + } + return nav_scoring_rect_offset_y; + } + return 0.0f; +} + +static void ImGui::NavEndFrame() +{ + ImGuiContext& g = *GImGui; + + // Show CTRL+TAB list window + if (g.NavWindowingTarget != NULL) + NavUpdateWindowingOverlay(); + + // Perform wrap-around in menus + // FIXME-NAV: Wrap may need to apply a weight bias on the other axis. e.g. 4x4 grid with 2 last items missing on last item won't handle LoopY/WrapY correctly. + // FIXME-NAV: Wrap (not Loop) support could be handled by the scoring function and then WrapX would function without an extra frame. + const ImGuiNavMoveFlags wanted_flags = ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_LoopX | ImGuiNavMoveFlags_WrapY | ImGuiNavMoveFlags_LoopY; + if (g.NavWindow && NavMoveRequestButNoResultYet() && (g.NavMoveFlags & wanted_flags) && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0) + NavUpdateCreateWrappingRequest(); +} + +static void ImGui::NavUpdateCreateWrappingRequest() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + + bool do_forward = false; + ImRect bb_rel = window->NavRectRel[g.NavLayer]; + ImGuiDir clip_dir = g.NavMoveDir; + const ImGuiNavMoveFlags move_flags = g.NavMoveFlags; + if (g.NavMoveDir == ImGuiDir_Left && (move_flags & (ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_LoopX))) + { + bb_rel.Min.x = bb_rel.Max.x = window->ContentSize.x + window->WindowPadding.x; + if (move_flags & ImGuiNavMoveFlags_WrapX) + { + bb_rel.TranslateY(-bb_rel.GetHeight()); // Previous row + clip_dir = ImGuiDir_Up; + } + do_forward = true; + } + if (g.NavMoveDir == ImGuiDir_Right && (move_flags & (ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_LoopX))) + { + bb_rel.Min.x = bb_rel.Max.x = -window->WindowPadding.x; + if (move_flags & ImGuiNavMoveFlags_WrapX) + { + bb_rel.TranslateY(+bb_rel.GetHeight()); // Next row + clip_dir = ImGuiDir_Down; + } + do_forward = true; + } + if (g.NavMoveDir == ImGuiDir_Up && (move_flags & (ImGuiNavMoveFlags_WrapY | ImGuiNavMoveFlags_LoopY))) + { + bb_rel.Min.y = bb_rel.Max.y = window->ContentSize.y + window->WindowPadding.y; + if (move_flags & ImGuiNavMoveFlags_WrapY) + { + bb_rel.TranslateX(-bb_rel.GetWidth()); // Previous column + clip_dir = ImGuiDir_Left; + } + do_forward = true; + } + if (g.NavMoveDir == ImGuiDir_Down && (move_flags & (ImGuiNavMoveFlags_WrapY | ImGuiNavMoveFlags_LoopY))) + { + bb_rel.Min.y = bb_rel.Max.y = -window->WindowPadding.y; + if (move_flags & ImGuiNavMoveFlags_WrapY) + { + bb_rel.TranslateX(+bb_rel.GetWidth()); // Next column + clip_dir = ImGuiDir_Right; + } + do_forward = true; + } + if (!do_forward) + return; + window->NavRectRel[g.NavLayer] = bb_rel; + NavMoveRequestForward(g.NavMoveDir, clip_dir, move_flags, g.NavMoveScrollFlags); +} + +static int ImGui::FindWindowFocusIndex(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_UNUSED(g); + int order = window->FocusOrder; + IM_ASSERT(window->RootWindow == window); // No child window (not testing _ChildWindow because of docking) + IM_ASSERT(g.WindowsFocusOrder[order] == window); + return order; +} + +static ImGuiWindow* FindWindowNavFocusable(int i_start, int i_stop, int dir) // FIXME-OPT O(N) +{ + ImGuiContext& g = *GImGui; + for (int i = i_start; i >= 0 && i < g.WindowsFocusOrder.Size && i != i_stop; i += dir) + if (ImGui::IsWindowNavFocusable(g.WindowsFocusOrder[i])) + return g.WindowsFocusOrder[i]; + return NULL; +} + +static void NavUpdateWindowingHighlightWindow(int focus_change_dir) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindowingTarget); + if (g.NavWindowingTarget->Flags & ImGuiWindowFlags_Modal) + return; + + const int i_current = ImGui::FindWindowFocusIndex(g.NavWindowingTarget); + ImGuiWindow* window_target = FindWindowNavFocusable(i_current + focus_change_dir, -INT_MAX, focus_change_dir); + if (!window_target) + window_target = FindWindowNavFocusable((focus_change_dir < 0) ? (g.WindowsFocusOrder.Size - 1) : 0, i_current, focus_change_dir); + if (window_target) // Don't reset windowing target if there's a single window in the list + g.NavWindowingTarget = g.NavWindowingTargetAnim = window_target; + g.NavWindowingToggleLayer = false; +} + +// Windowing management mode +// Keyboard: CTRL+Tab (change focus/move/resize), Alt (toggle menu layer) +// Gamepad: Hold Menu/Square (change focus/move/resize), Tap Menu/Square (toggle menu layer) +static void ImGui::NavUpdateWindowing() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + ImGuiWindow* apply_focus_window = NULL; + bool apply_toggle_layer = false; + + ImGuiWindow* modal_window = GetTopMostPopupModal(); + bool allow_windowing = (modal_window == NULL); + if (!allow_windowing) + g.NavWindowingTarget = NULL; + + // Fade out + if (g.NavWindowingTargetAnim && g.NavWindowingTarget == NULL) + { + g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha - io.DeltaTime * 10.0f, 0.0f); + if (g.DimBgRatio <= 0.0f && g.NavWindowingHighlightAlpha <= 0.0f) + g.NavWindowingTargetAnim = NULL; + } + + // Start CTRL+Tab or Square+L/R window selection + const bool start_windowing_with_gamepad = allow_windowing && !g.NavWindowingTarget && IsNavInputTest(ImGuiNavInput_Menu, ImGuiInputReadMode_Pressed); + const bool start_windowing_with_keyboard = allow_windowing && !g.NavWindowingTarget && io.KeyCtrl && IsKeyPressed(ImGuiKey_Tab); + if (start_windowing_with_gamepad || start_windowing_with_keyboard) + if (ImGuiWindow* window = g.NavWindow ? g.NavWindow : FindWindowNavFocusable(g.WindowsFocusOrder.Size - 1, -INT_MAX, -1)) + { + g.NavWindowingTarget = g.NavWindowingTargetAnim = window->RootWindow; + g.NavWindowingTimer = g.NavWindowingHighlightAlpha = 0.0f; + g.NavWindowingToggleLayer = start_windowing_with_gamepad ? true : false; // Gamepad starts toggling layer + g.NavInputSource = start_windowing_with_keyboard ? ImGuiInputSource_Keyboard : ImGuiInputSource_Gamepad; + } + + // Gamepad update + g.NavWindowingTimer += io.DeltaTime; + if (g.NavWindowingTarget && g.NavInputSource == ImGuiInputSource_Gamepad) + { + // Highlight only appears after a brief time holding the button, so that a fast tap on PadMenu (to toggle NavLayer) doesn't add visual noise + g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingTimer - NAV_WINDOWING_HIGHLIGHT_DELAY) / 0.05f)); + + // Select window to focus + const int focus_change_dir = (int)IsNavInputTest(ImGuiNavInput_FocusPrev, ImGuiInputReadMode_RepeatSlow) - (int)IsNavInputTest(ImGuiNavInput_FocusNext, ImGuiInputReadMode_RepeatSlow); + if (focus_change_dir != 0) + { + NavUpdateWindowingHighlightWindow(focus_change_dir); + g.NavWindowingHighlightAlpha = 1.0f; + } + + // Single press toggles NavLayer, long press with L/R apply actual focus on release (until then the window was merely rendered top-most) + if (!IsNavInputDown(ImGuiNavInput_Menu)) + { + g.NavWindowingToggleLayer &= (g.NavWindowingHighlightAlpha < 1.0f); // Once button was held long enough we don't consider it a tap-to-toggle-layer press anymore. + if (g.NavWindowingToggleLayer && g.NavWindow) + apply_toggle_layer = true; + else if (!g.NavWindowingToggleLayer) + apply_focus_window = g.NavWindowingTarget; + g.NavWindowingTarget = NULL; + } + } + + // Keyboard: Focus + if (g.NavWindowingTarget && g.NavInputSource == ImGuiInputSource_Keyboard) + { + // Visuals only appears after a brief time after pressing TAB the first time, so that a fast CTRL+TAB doesn't add visual noise + g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingTimer - NAV_WINDOWING_HIGHLIGHT_DELAY) / 0.05f)); // 1.0f + if (IsKeyPressed(ImGuiKey_Tab, true)) + NavUpdateWindowingHighlightWindow(io.KeyShift ? +1 : -1); + if (!io.KeyCtrl) + apply_focus_window = g.NavWindowingTarget; + } + + // Keyboard: Press and Release ALT to toggle menu layer + // - Testing that only Alt is tested prevents Alt+Shift or AltGR from toggling menu layer. + // - AltGR is normally Alt+Ctrl but we can't reliably detect it (not all backends/systems/layout emit it as Alt+Ctrl). But even on keyboards without AltGR we don't want Alt+Ctrl to open menu anyway. + const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + if (nav_keyboard_active && io.KeyMods == ImGuiKeyModFlags_Alt && (io.KeyModsPrev & ImGuiKeyModFlags_Alt) == 0) + { + g.NavWindowingToggleLayer = true; + g.NavInputSource = ImGuiInputSource_Keyboard; + } + if (g.NavWindowingToggleLayer && g.NavInputSource == ImGuiInputSource_Keyboard) + { + // We cancel toggling nav layer when any text has been typed (generally while holding Alt). (See #370) + // We cancel toggling nav layer when other modifiers are pressed. (See #4439) + if (io.InputQueueCharacters.Size > 0 || io.KeyCtrl || io.KeyShift || io.KeySuper) + g.NavWindowingToggleLayer = false; + + // Apply layer toggle on release + // Important: we don't assume that Alt was previously held in order to handle loss of focus when backend calls io.AddFocusEvent(false) + // Important: as before version <18314 we lacked an explicit IO event for focus gain/loss, we also compare mouse validity to detect old backends clearing mouse pos on focus loss. + if (!(io.KeyMods & ImGuiKeyModFlags_Alt) && (io.KeyModsPrev & ImGuiKeyModFlags_Alt) && g.NavWindowingToggleLayer) + if (g.ActiveId == 0 || g.ActiveIdAllowOverlap) + if (IsMousePosValid(&io.MousePos) == IsMousePosValid(&io.MousePosPrev)) + apply_toggle_layer = true; + if (!io.KeyAlt) + g.NavWindowingToggleLayer = false; + } + + // Move window + if (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoMove)) + { + ImVec2 move_delta; + if (g.NavInputSource == ImGuiInputSource_Keyboard && !io.KeyShift) + move_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_RawKeyboard, ImGuiInputReadMode_Down); + if (g.NavInputSource == ImGuiInputSource_Gamepad) + move_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_PadLStick, ImGuiInputReadMode_Down); + if (move_delta.x != 0.0f || move_delta.y != 0.0f) + { + const float NAV_MOVE_SPEED = 800.0f; + const float move_speed = ImFloor(NAV_MOVE_SPEED * io.DeltaTime * ImMin(io.DisplayFramebufferScale.x, io.DisplayFramebufferScale.y)); // FIXME: Doesn't handle variable framerate very well + ImGuiWindow* moving_window = g.NavWindowingTarget->RootWindow; + SetWindowPos(moving_window, moving_window->Pos + move_delta * move_speed, ImGuiCond_Always); + MarkIniSettingsDirty(moving_window); + g.NavDisableMouseHover = true; + } + } + + // Apply final focus + if (apply_focus_window && (g.NavWindow == NULL || apply_focus_window != g.NavWindow->RootWindow)) + { + ClearActiveID(); + NavRestoreHighlightAfterMove(); + apply_focus_window = NavRestoreLastChildNavWindow(apply_focus_window); + ClosePopupsOverWindow(apply_focus_window, false); + FocusWindow(apply_focus_window); + if (apply_focus_window->NavLastIds[0] == 0) + NavInitWindow(apply_focus_window, false); + + // If the window has ONLY a menu layer (no main layer), select it directly + // Use NavLayersActiveMaskNext since windows didn't have a chance to be Begin()-ed on this frame, + // so CTRL+Tab where the keys are only held for 1 frame will be able to use correct layers mask since + // the target window as already been previewed once. + // FIXME-NAV: This should be done in NavInit.. or in FocusWindow... However in both of those cases, + // we won't have a guarantee that windows has been visible before and therefore NavLayersActiveMask* + // won't be valid. + if (apply_focus_window->DC.NavLayersActiveMaskNext == (1 << ImGuiNavLayer_Menu)) + g.NavLayer = ImGuiNavLayer_Menu; + } + if (apply_focus_window) + g.NavWindowingTarget = NULL; + + // Apply menu/layer toggle + if (apply_toggle_layer && g.NavWindow) + { + ClearActiveID(); + + // Move to parent menu if necessary + ImGuiWindow* new_nav_window = g.NavWindow; + while (new_nav_window->ParentWindow + && (new_nav_window->DC.NavLayersActiveMask & (1 << ImGuiNavLayer_Menu)) == 0 + && (new_nav_window->Flags & ImGuiWindowFlags_ChildWindow) != 0 + && (new_nav_window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) + new_nav_window = new_nav_window->ParentWindow; + if (new_nav_window != g.NavWindow) + { + ImGuiWindow* old_nav_window = g.NavWindow; + FocusWindow(new_nav_window); + new_nav_window->NavLastChildNavWindow = old_nav_window; + } + + // Toggle layer + const ImGuiNavLayer new_nav_layer = (g.NavWindow->DC.NavLayersActiveMask & (1 << ImGuiNavLayer_Menu)) ? (ImGuiNavLayer)((int)g.NavLayer ^ 1) : ImGuiNavLayer_Main; + if (new_nav_layer != g.NavLayer) + { + // Reinitialize navigation when entering menu bar with the Alt key (FIXME: could be a properly of the layer?) + if (new_nav_layer == ImGuiNavLayer_Menu) + g.NavWindow->NavLastIds[new_nav_layer] = 0; + NavRestoreLayer(new_nav_layer); + NavRestoreHighlightAfterMove(); + } + } +} + +// Window has already passed the IsWindowNavFocusable() +static const char* GetFallbackWindowNameForWindowingList(ImGuiWindow* window) +{ + if (window->Flags & ImGuiWindowFlags_Popup) + return "(Popup)"; + if ((window->Flags & ImGuiWindowFlags_MenuBar) && strcmp(window->Name, "##MainMenuBar") == 0) + return "(Main menu bar)"; + return "(Untitled)"; +} + +// Overlay displayed when using CTRL+TAB. Called by EndFrame(). +void ImGui::NavUpdateWindowingOverlay() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindowingTarget != NULL); + + if (g.NavWindowingTimer < NAV_WINDOWING_LIST_APPEAR_DELAY) + return; + + if (g.NavWindowingListWindow == NULL) + g.NavWindowingListWindow = FindWindowByName("###NavWindowingList"); + const ImGuiViewport* viewport = GetMainViewport(); + SetNextWindowSizeConstraints(ImVec2(viewport->Size.x * 0.20f, viewport->Size.y * 0.20f), ImVec2(FLT_MAX, FLT_MAX)); + SetNextWindowPos(viewport->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + PushStyleVar(ImGuiStyleVar_WindowPadding, g.Style.WindowPadding * 2.0f); + Begin("###NavWindowingList", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings); + for (int n = g.WindowsFocusOrder.Size - 1; n >= 0; n--) + { + ImGuiWindow* window = g.WindowsFocusOrder[n]; + IM_ASSERT(window != NULL); // Fix static analyzers + if (!IsWindowNavFocusable(window)) + continue; + const char* label = window->Name; + if (label == FindRenderedTextEnd(label)) + label = GetFallbackWindowNameForWindowingList(window); + Selectable(label, g.NavWindowingTarget == window); + } + End(); + PopStyleVar(); +} + + +//----------------------------------------------------------------------------- +// [SECTION] DRAG AND DROP +//----------------------------------------------------------------------------- + +void ImGui::ClearDragDrop() +{ + ImGuiContext& g = *GImGui; + g.DragDropActive = false; + g.DragDropPayload.Clear(); + g.DragDropAcceptFlags = ImGuiDragDropFlags_None; + g.DragDropAcceptIdCurr = g.DragDropAcceptIdPrev = 0; + g.DragDropAcceptIdCurrRectSurface = FLT_MAX; + g.DragDropAcceptFrameCount = -1; + + g.DragDropPayloadBufHeap.clear(); + memset(&g.DragDropPayloadBufLocal, 0, sizeof(g.DragDropPayloadBufLocal)); +} + +// When this returns true you need to: a) call SetDragDropPayload() exactly once, b) you may render the payload visual/description, c) call EndDragDropSource() +// If the item has an identifier: +// - This assume/require the item to be activated (typically via ButtonBehavior). +// - Therefore if you want to use this with a mouse button other than left mouse button, it is up to the item itself to activate with another button. +// - We then pull and use the mouse button that was used to activate the item and use it to carry on the drag. +// If the item has no identifier: +// - Currently always assume left mouse button. +bool ImGui::BeginDragDropSource(ImGuiDragDropFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // FIXME-DRAGDROP: While in the common-most "drag from non-zero active id" case we can tell the mouse button, + // in both SourceExtern and id==0 cases we may requires something else (explicit flags or some heuristic). + ImGuiMouseButton mouse_button = ImGuiMouseButton_Left; + + bool source_drag_active = false; + ImGuiID source_id = 0; + ImGuiID source_parent_id = 0; + if (!(flags & ImGuiDragDropFlags_SourceExtern)) + { + source_id = g.LastItemData.ID; + if (source_id != 0) + { + // Common path: items with ID + if (g.ActiveId != source_id) + return false; + if (g.ActiveIdMouseButton != -1) + mouse_button = g.ActiveIdMouseButton; + if (g.IO.MouseDown[mouse_button] == false || window->SkipItems) + return false; + g.ActiveIdAllowOverlap = false; + } + else + { + // Uncommon path: items without ID + if (g.IO.MouseDown[mouse_button] == false || window->SkipItems) + return false; + if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) == 0 && (g.ActiveId == 0 || g.ActiveIdWindow != window)) + return false; + + // If you want to use BeginDragDropSource() on an item with no unique identifier for interaction, such as Text() or Image(), you need to: + // A) Read the explanation below, B) Use the ImGuiDragDropFlags_SourceAllowNullID flag. + if (!(flags & ImGuiDragDropFlags_SourceAllowNullID)) + { + IM_ASSERT(0); + return false; + } + + // Magic fallback to handle items with no assigned ID, e.g. Text(), Image() + // We build a throwaway ID based on current ID stack + relative AABB of items in window. + // THE IDENTIFIER WON'T SURVIVE ANY REPOSITIONING/RESIZINGG OF THE WIDGET, so if your widget moves your dragging operation will be canceled. + // We don't need to maintain/call ClearActiveID() as releasing the button will early out this function and trigger !ActiveIdIsAlive. + // Rely on keeping other window->LastItemXXX fields intact. + source_id = g.LastItemData.ID = window->GetIDFromRectangle(g.LastItemData.Rect); + bool is_hovered = ItemHoverable(g.LastItemData.Rect, source_id); + if (is_hovered && g.IO.MouseClicked[mouse_button]) + { + SetActiveID(source_id, window); + FocusWindow(window); + } + if (g.ActiveId == source_id) // Allow the underlying widget to display/return hovered during the mouse release frame, else we would get a flicker. + g.ActiveIdAllowOverlap = is_hovered; + } + if (g.ActiveId != source_id) + return false; + source_parent_id = window->IDStack.back(); + source_drag_active = IsMouseDragging(mouse_button); + + // Disable navigation and key inputs while dragging + cancel existing request if any + SetActiveIdUsingNavAndKeys(); + } + else + { + window = NULL; + source_id = ImHashStr("#SourceExtern"); + source_drag_active = true; + } + + if (source_drag_active) + { + if (!g.DragDropActive) + { + IM_ASSERT(source_id != 0); + ClearDragDrop(); + ImGuiPayload& payload = g.DragDropPayload; + payload.SourceId = source_id; + payload.SourceParentId = source_parent_id; + g.DragDropActive = true; + g.DragDropSourceFlags = flags; + g.DragDropMouseButton = mouse_button; + if (payload.SourceId == g.ActiveId) + g.ActiveIdNoClearOnFocusLoss = true; + } + g.DragDropSourceFrameCount = g.FrameCount; + g.DragDropWithinSource = true; + + if (!(flags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + // Target can request the Source to not display its tooltip (we use a dedicated flag to make this request explicit) + // We unfortunately can't just modify the source flags and skip the call to BeginTooltip, as caller may be emitting contents. + BeginTooltip(); + if (g.DragDropAcceptIdPrev && (g.DragDropAcceptFlags & ImGuiDragDropFlags_AcceptNoPreviewTooltip)) + { + ImGuiWindow* tooltip_window = g.CurrentWindow; + tooltip_window->Hidden = tooltip_window->SkipItems = true; + tooltip_window->HiddenFramesCanSkipItems = 1; + } + } + + if (!(flags & ImGuiDragDropFlags_SourceNoDisableHover) && !(flags & ImGuiDragDropFlags_SourceExtern)) + g.LastItemData.StatusFlags &= ~ImGuiItemStatusFlags_HoveredRect; + + return true; + } + return false; +} + +void ImGui::EndDragDropSource() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.DragDropActive); + IM_ASSERT(g.DragDropWithinSource && "Not after a BeginDragDropSource()?"); + + if (!(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + EndTooltip(); + + // Discard the drag if have not called SetDragDropPayload() + if (g.DragDropPayload.DataFrameCount == -1) + ClearDragDrop(); + g.DragDropWithinSource = false; +} + +// Use 'cond' to choose to submit payload on drag start or every frame +bool ImGui::SetDragDropPayload(const char* type, const void* data, size_t data_size, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + ImGuiPayload& payload = g.DragDropPayload; + if (cond == 0) + cond = ImGuiCond_Always; + + IM_ASSERT(type != NULL); + IM_ASSERT(strlen(type) < IM_ARRAYSIZE(payload.DataType) && "Payload type can be at most 32 characters long"); + IM_ASSERT((data != NULL && data_size > 0) || (data == NULL && data_size == 0)); + IM_ASSERT(cond == ImGuiCond_Always || cond == ImGuiCond_Once); + IM_ASSERT(payload.SourceId != 0); // Not called between BeginDragDropSource() and EndDragDropSource() + + if (cond == ImGuiCond_Always || payload.DataFrameCount == -1) + { + // Copy payload + ImStrncpy(payload.DataType, type, IM_ARRAYSIZE(payload.DataType)); + g.DragDropPayloadBufHeap.resize(0); + if (data_size > sizeof(g.DragDropPayloadBufLocal)) + { + // Store in heap + g.DragDropPayloadBufHeap.resize((int)data_size); + payload.Data = g.DragDropPayloadBufHeap.Data; + memcpy(payload.Data, data, data_size); + } + else if (data_size > 0) + { + // Store locally + memset(&g.DragDropPayloadBufLocal, 0, sizeof(g.DragDropPayloadBufLocal)); + payload.Data = g.DragDropPayloadBufLocal; + memcpy(payload.Data, data, data_size); + } + else + { + payload.Data = NULL; + } + payload.DataSize = (int)data_size; + } + payload.DataFrameCount = g.FrameCount; + + // Return whether the payload has been accepted + return (g.DragDropAcceptFrameCount == g.FrameCount) || (g.DragDropAcceptFrameCount == g.FrameCount - 1); +} + +bool ImGui::BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id) +{ + ImGuiContext& g = *GImGui; + if (!g.DragDropActive) + return false; + + ImGuiWindow* window = g.CurrentWindow; + ImGuiWindow* hovered_window = g.HoveredWindowUnderMovingWindow; + if (hovered_window == NULL || window->RootWindow != hovered_window->RootWindow) + return false; + IM_ASSERT(id != 0); + if (!IsMouseHoveringRect(bb.Min, bb.Max) || (id == g.DragDropPayload.SourceId)) + return false; + if (window->SkipItems) + return false; + + IM_ASSERT(g.DragDropWithinTarget == false); + g.DragDropTargetRect = bb; + g.DragDropTargetId = id; + g.DragDropWithinTarget = true; + return true; +} + +// We don't use BeginDragDropTargetCustom() and duplicate its code because: +// 1) we use LastItemRectHoveredRect which handles items that pushes a temporarily clip rectangle in their code. Calling BeginDragDropTargetCustom(LastItemRect) would not handle them. +// 2) and it's faster. as this code may be very frequently called, we want to early out as fast as we can. +// Also note how the HoveredWindow test is positioned differently in both functions (in both functions we optimize for the cheapest early out case) +bool ImGui::BeginDragDropTarget() +{ + ImGuiContext& g = *GImGui; + if (!g.DragDropActive) + return false; + + ImGuiWindow* window = g.CurrentWindow; + if (!(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)) + return false; + ImGuiWindow* hovered_window = g.HoveredWindowUnderMovingWindow; + if (hovered_window == NULL || window->RootWindow != hovered_window->RootWindow || window->SkipItems) + return false; + + const ImRect& display_rect = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasDisplayRect) ? g.LastItemData.DisplayRect : g.LastItemData.Rect; + ImGuiID id = g.LastItemData.ID; + if (id == 0) + id = window->GetIDFromRectangle(display_rect); + if (g.DragDropPayload.SourceId == id) + return false; + + IM_ASSERT(g.DragDropWithinTarget == false); + g.DragDropTargetRect = display_rect; + g.DragDropTargetId = id; + g.DragDropWithinTarget = true; + return true; +} + +bool ImGui::IsDragDropPayloadBeingAccepted() +{ + ImGuiContext& g = *GImGui; + return g.DragDropActive && g.DragDropAcceptIdPrev != 0; +} + +const ImGuiPayload* ImGui::AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiPayload& payload = g.DragDropPayload; + IM_ASSERT(g.DragDropActive); // Not called between BeginDragDropTarget() and EndDragDropTarget() ? + IM_ASSERT(payload.DataFrameCount != -1); // Forgot to call EndDragDropTarget() ? + if (type != NULL && !payload.IsDataType(type)) + return NULL; + + // Accept smallest drag target bounding box, this allows us to nest drag targets conveniently without ordering constraints. + // NB: We currently accept NULL id as target. However, overlapping targets requires a unique ID to function! + const bool was_accepted_previously = (g.DragDropAcceptIdPrev == g.DragDropTargetId); + ImRect r = g.DragDropTargetRect; + float r_surface = r.GetWidth() * r.GetHeight(); + if (r_surface <= g.DragDropAcceptIdCurrRectSurface) + { + g.DragDropAcceptFlags = flags; + g.DragDropAcceptIdCurr = g.DragDropTargetId; + g.DragDropAcceptIdCurrRectSurface = r_surface; + } + + // Render default drop visuals + // FIXME-DRAGDROP: Settle on a proper default visuals for drop target. + payload.Preview = was_accepted_previously; + flags |= (g.DragDropSourceFlags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect); // Source can also inhibit the preview (useful for external sources that lives for 1 frame) + if (!(flags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect) && payload.Preview) + window->DrawList->AddRect(r.Min - ImVec2(3.5f,3.5f), r.Max + ImVec2(3.5f, 3.5f), GetColorU32(ImGuiCol_DragDropTarget), 0.0f, 0, 2.0f); + + g.DragDropAcceptFrameCount = g.FrameCount; + payload.Delivery = was_accepted_previously && !IsMouseDown(g.DragDropMouseButton); // For extern drag sources affecting os window focus, it's easier to just test !IsMouseDown() instead of IsMouseReleased() + if (!payload.Delivery && !(flags & ImGuiDragDropFlags_AcceptBeforeDelivery)) + return NULL; + + return &payload; +} + +const ImGuiPayload* ImGui::GetDragDropPayload() +{ + ImGuiContext& g = *GImGui; + return g.DragDropActive ? &g.DragDropPayload : NULL; +} + +// We don't really use/need this now, but added it for the sake of consistency and because we might need it later. +void ImGui::EndDragDropTarget() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.DragDropActive); + IM_ASSERT(g.DragDropWithinTarget); + g.DragDropWithinTarget = false; +} + +//----------------------------------------------------------------------------- +// [SECTION] LOGGING/CAPTURING +//----------------------------------------------------------------------------- +// All text output from the interface can be captured into tty/file/clipboard. +// By default, tree nodes are automatically opened during logging. +//----------------------------------------------------------------------------- + +// Pass text data straight to log (without being displayed) +static inline void LogTextV(ImGuiContext& g, const char* fmt, va_list args) +{ + if (g.LogFile) + { + g.LogBuffer.Buf.resize(0); + g.LogBuffer.appendfv(fmt, args); + ImFileWrite(g.LogBuffer.c_str(), sizeof(char), (ImU64)g.LogBuffer.size(), g.LogFile); + } + else + { + g.LogBuffer.appendfv(fmt, args); + } +} + +void ImGui::LogText(const char* fmt, ...) +{ + ImGuiContext& g = *GImGui; + if (!g.LogEnabled) + return; + + va_list args; + va_start(args, fmt); + LogTextV(g, fmt, args); + va_end(args); +} + +void ImGui::LogTextV(const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + if (!g.LogEnabled) + return; + + LogTextV(g, fmt, args); +} + +// Internal version that takes a position to decide on newline placement and pad items according to their depth. +// We split text into individual lines to add current tree level padding +// FIXME: This code is a little complicated perhaps, considering simplifying the whole system. +void ImGui::LogRenderedText(const ImVec2* ref_pos, const char* text, const char* text_end) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + const char* prefix = g.LogNextPrefix; + const char* suffix = g.LogNextSuffix; + g.LogNextPrefix = g.LogNextSuffix = NULL; + + if (!text_end) + text_end = FindRenderedTextEnd(text, text_end); + + const bool log_new_line = ref_pos && (ref_pos->y > g.LogLinePosY + g.Style.FramePadding.y + 1); + if (ref_pos) + g.LogLinePosY = ref_pos->y; + if (log_new_line) + { + LogText(IM_NEWLINE); + g.LogLineFirstItem = true; + } + + if (prefix) + LogRenderedText(ref_pos, prefix, prefix + strlen(prefix)); // Calculate end ourself to ensure "##" are included here. + + // Re-adjust padding if we have popped out of our starting depth + if (g.LogDepthRef > window->DC.TreeDepth) + g.LogDepthRef = window->DC.TreeDepth; + const int tree_depth = (window->DC.TreeDepth - g.LogDepthRef); + + const char* text_remaining = text; + for (;;) + { + // Split the string. Each new line (after a '\n') is followed by indentation corresponding to the current depth of our log entry. + // We don't add a trailing \n yet to allow a subsequent item on the same line to be captured. + const char* line_start = text_remaining; + const char* line_end = ImStreolRange(line_start, text_end); + const bool is_last_line = (line_end == text_end); + if (line_start != line_end || !is_last_line) + { + const int line_length = (int)(line_end - line_start); + const int indentation = g.LogLineFirstItem ? tree_depth * 4 : 1; + LogText("%*s%.*s", indentation, "", line_length, line_start); + g.LogLineFirstItem = false; + if (*line_end == '\n') + { + LogText(IM_NEWLINE); + g.LogLineFirstItem = true; + } + } + if (is_last_line) + break; + text_remaining = line_end + 1; + } + + if (suffix) + LogRenderedText(ref_pos, suffix, suffix + strlen(suffix)); +} + +// Start logging/capturing text output +void ImGui::LogBegin(ImGuiLogType type, int auto_open_depth) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(g.LogEnabled == false); + IM_ASSERT(g.LogFile == NULL); + IM_ASSERT(g.LogBuffer.empty()); + g.LogEnabled = true; + g.LogType = type; + g.LogNextPrefix = g.LogNextSuffix = NULL; + g.LogDepthRef = window->DC.TreeDepth; + g.LogDepthToExpand = ((auto_open_depth >= 0) ? auto_open_depth : g.LogDepthToExpandDefault); + g.LogLinePosY = FLT_MAX; + g.LogLineFirstItem = true; +} + +// Important: doesn't copy underlying data, use carefully (prefix/suffix must be in scope at the time of the next LogRenderedText) +void ImGui::LogSetNextTextDecoration(const char* prefix, const char* suffix) +{ + ImGuiContext& g = *GImGui; + g.LogNextPrefix = prefix; + g.LogNextSuffix = suffix; +} + +void ImGui::LogToTTY(int auto_open_depth) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + IM_UNUSED(auto_open_depth); +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + LogBegin(ImGuiLogType_TTY, auto_open_depth); + g.LogFile = stdout; +#endif +} + +// Start logging/capturing text output to given file +void ImGui::LogToFile(int auto_open_depth, const char* filename) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + + // FIXME: We could probably open the file in text mode "at", however note that clipboard/buffer logging will still + // be subject to outputting OS-incompatible carriage return if within strings the user doesn't use IM_NEWLINE. + // By opening the file in binary mode "ab" we have consistent output everywhere. + if (!filename) + filename = g.IO.LogFilename; + if (!filename || !filename[0]) + return; + ImFileHandle f = ImFileOpen(filename, "ab"); + if (!f) + { + IM_ASSERT(0); + return; + } + + LogBegin(ImGuiLogType_File, auto_open_depth); + g.LogFile = f; +} + +// Start logging/capturing text output to clipboard +void ImGui::LogToClipboard(int auto_open_depth) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + LogBegin(ImGuiLogType_Clipboard, auto_open_depth); +} + +void ImGui::LogToBuffer(int auto_open_depth) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + LogBegin(ImGuiLogType_Buffer, auto_open_depth); +} + +void ImGui::LogFinish() +{ + ImGuiContext& g = *GImGui; + if (!g.LogEnabled) + return; + + LogText(IM_NEWLINE); + switch (g.LogType) + { + case ImGuiLogType_TTY: +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + fflush(g.LogFile); +#endif + break; + case ImGuiLogType_File: + ImFileClose(g.LogFile); + break; + case ImGuiLogType_Buffer: + break; + case ImGuiLogType_Clipboard: + if (!g.LogBuffer.empty()) + SetClipboardText(g.LogBuffer.begin()); + break; + case ImGuiLogType_None: + IM_ASSERT(0); + break; + } + + g.LogEnabled = false; + g.LogType = ImGuiLogType_None; + g.LogFile = NULL; + g.LogBuffer.clear(); +} + +// Helper to display logging buttons +// FIXME-OBSOLETE: We should probably obsolete this and let the user have their own helper (this is one of the oldest function alive!) +void ImGui::LogButtons() +{ + ImGuiContext& g = *GImGui; + + PushID("LogButtons"); +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + const bool log_to_tty = Button("Log To TTY"); SameLine(); +#else + const bool log_to_tty = false; +#endif + const bool log_to_file = Button("Log To File"); SameLine(); + const bool log_to_clipboard = Button("Log To Clipboard"); SameLine(); + PushAllowKeyboardFocus(false); + SetNextItemWidth(80.0f); + SliderInt("Default Depth", &g.LogDepthToExpandDefault, 0, 9, NULL); + PopAllowKeyboardFocus(); + PopID(); + + // Start logging at the end of the function so that the buttons don't appear in the log + if (log_to_tty) + LogToTTY(); + if (log_to_file) + LogToFile(); + if (log_to_clipboard) + LogToClipboard(); +} + + +//----------------------------------------------------------------------------- +// [SECTION] SETTINGS +//----------------------------------------------------------------------------- +// - UpdateSettings() [Internal] +// - MarkIniSettingsDirty() [Internal] +// - CreateNewWindowSettings() [Internal] +// - FindWindowSettings() [Internal] +// - FindOrCreateWindowSettings() [Internal] +// - FindSettingsHandler() [Internal] +// - ClearIniSettings() [Internal] +// - LoadIniSettingsFromDisk() +// - LoadIniSettingsFromMemory() +// - SaveIniSettingsToDisk() +// - SaveIniSettingsToMemory() +// - WindowSettingsHandler_***() [Internal] +//----------------------------------------------------------------------------- + +// Called by NewFrame() +void ImGui::UpdateSettings() +{ + // Load settings on first frame (if not explicitly loaded manually before) + ImGuiContext& g = *GImGui; + if (!g.SettingsLoaded) + { + IM_ASSERT(g.SettingsWindows.empty()); + if (g.IO.IniFilename) + LoadIniSettingsFromDisk(g.IO.IniFilename); + g.SettingsLoaded = true; + } + + // Save settings (with a delay after the last modification, so we don't spam disk too much) + if (g.SettingsDirtyTimer > 0.0f) + { + g.SettingsDirtyTimer -= g.IO.DeltaTime; + if (g.SettingsDirtyTimer <= 0.0f) + { + if (g.IO.IniFilename != NULL) + SaveIniSettingsToDisk(g.IO.IniFilename); + else + g.IO.WantSaveIniSettings = true; // Let user know they can call SaveIniSettingsToMemory(). user will need to clear io.WantSaveIniSettings themselves. + g.SettingsDirtyTimer = 0.0f; + } + } +} + +void ImGui::MarkIniSettingsDirty() +{ + ImGuiContext& g = *GImGui; + if (g.SettingsDirtyTimer <= 0.0f) + g.SettingsDirtyTimer = g.IO.IniSavingRate; +} + +void ImGui::MarkIniSettingsDirty(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (!(window->Flags & ImGuiWindowFlags_NoSavedSettings)) + if (g.SettingsDirtyTimer <= 0.0f) + g.SettingsDirtyTimer = g.IO.IniSavingRate; +} + +ImGuiWindowSettings* ImGui::CreateNewWindowSettings(const char* name) +{ + ImGuiContext& g = *GImGui; + +#if !IMGUI_DEBUG_INI_SETTINGS + // Skip to the "###" marker if any. We don't skip past to match the behavior of GetID() + // Preserve the full string when IMGUI_DEBUG_INI_SETTINGS is set to make .ini inspection easier. + if (const char* p = strstr(name, "###")) + name = p; +#endif + const size_t name_len = strlen(name); + + // Allocate chunk + const size_t chunk_size = sizeof(ImGuiWindowSettings) + name_len + 1; + ImGuiWindowSettings* settings = g.SettingsWindows.alloc_chunk(chunk_size); + IM_PLACEMENT_NEW(settings) ImGuiWindowSettings(); + settings->ID = ImHashStr(name, name_len); + memcpy(settings->GetName(), name, name_len + 1); // Store with zero terminator + + return settings; +} + +ImGuiWindowSettings* ImGui::FindWindowSettings(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->ID == id) + return settings; + return NULL; +} + +ImGuiWindowSettings* ImGui::FindOrCreateWindowSettings(const char* name) +{ + if (ImGuiWindowSettings* settings = FindWindowSettings(ImHashStr(name))) + return settings; + return CreateNewWindowSettings(name); +} + +ImGuiSettingsHandler* ImGui::FindSettingsHandler(const char* type_name) +{ + ImGuiContext& g = *GImGui; + const ImGuiID type_hash = ImHashStr(type_name); + for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) + if (g.SettingsHandlers[handler_n].TypeHash == type_hash) + return &g.SettingsHandlers[handler_n]; + return NULL; +} + +void ImGui::ClearIniSettings() +{ + ImGuiContext& g = *GImGui; + g.SettingsIniData.clear(); + for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) + if (g.SettingsHandlers[handler_n].ClearAllFn) + g.SettingsHandlers[handler_n].ClearAllFn(&g, &g.SettingsHandlers[handler_n]); +} + +void ImGui::LoadIniSettingsFromDisk(const char* ini_filename) +{ + size_t file_data_size = 0; + char* file_data = (char*)ImFileLoadToMemory(ini_filename, "rb", &file_data_size); + if (!file_data) + return; + LoadIniSettingsFromMemory(file_data, (size_t)file_data_size); + IM_FREE(file_data); +} + +// Zero-tolerance, no error reporting, cheap .ini parsing +void ImGui::LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Initialized); + //IM_ASSERT(!g.WithinFrameScope && "Cannot be called between NewFrame() and EndFrame()"); + //IM_ASSERT(g.SettingsLoaded == false && g.FrameCount == 0); + + // For user convenience, we allow passing a non zero-terminated string (hence the ini_size parameter). + // For our convenience and to make the code simpler, we'll also write zero-terminators within the buffer. So let's create a writable copy.. + if (ini_size == 0) + ini_size = strlen(ini_data); + g.SettingsIniData.Buf.resize((int)ini_size + 1); + char* const buf = g.SettingsIniData.Buf.Data; + char* const buf_end = buf + ini_size; + memcpy(buf, ini_data, ini_size); + buf_end[0] = 0; + + // Call pre-read handlers + // Some types will clear their data (e.g. dock information) some types will allow merge/override (window) + for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) + if (g.SettingsHandlers[handler_n].ReadInitFn) + g.SettingsHandlers[handler_n].ReadInitFn(&g, &g.SettingsHandlers[handler_n]); + + void* entry_data = NULL; + ImGuiSettingsHandler* entry_handler = NULL; + + char* line_end = NULL; + for (char* line = buf; line < buf_end; line = line_end + 1) + { + // Skip new lines markers, then find end of the line + while (*line == '\n' || *line == '\r') + line++; + line_end = line; + while (line_end < buf_end && *line_end != '\n' && *line_end != '\r') + line_end++; + line_end[0] = 0; + if (line[0] == ';') + continue; + if (line[0] == '[' && line_end > line && line_end[-1] == ']') + { + // Parse "[Type][Name]". Note that 'Name' can itself contains [] characters, which is acceptable with the current format and parsing code. + line_end[-1] = 0; + const char* name_end = line_end - 1; + const char* type_start = line + 1; + char* type_end = (char*)(void*)ImStrchrRange(type_start, name_end, ']'); + const char* name_start = type_end ? ImStrchrRange(type_end + 1, name_end, '[') : NULL; + if (!type_end || !name_start) + continue; + *type_end = 0; // Overwrite first ']' + name_start++; // Skip second '[' + entry_handler = FindSettingsHandler(type_start); + entry_data = entry_handler ? entry_handler->ReadOpenFn(&g, entry_handler, name_start) : NULL; + } + else if (entry_handler != NULL && entry_data != NULL) + { + // Let type handler parse the line + entry_handler->ReadLineFn(&g, entry_handler, entry_data, line); + } + } + g.SettingsLoaded = true; + + // [DEBUG] Restore untouched copy so it can be browsed in Metrics (not strictly necessary) + memcpy(buf, ini_data, ini_size); + + // Call post-read handlers + for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) + if (g.SettingsHandlers[handler_n].ApplyAllFn) + g.SettingsHandlers[handler_n].ApplyAllFn(&g, &g.SettingsHandlers[handler_n]); +} + +void ImGui::SaveIniSettingsToDisk(const char* ini_filename) +{ + ImGuiContext& g = *GImGui; + g.SettingsDirtyTimer = 0.0f; + if (!ini_filename) + return; + + size_t ini_data_size = 0; + const char* ini_data = SaveIniSettingsToMemory(&ini_data_size); + ImFileHandle f = ImFileOpen(ini_filename, "wt"); + if (!f) + return; + ImFileWrite(ini_data, sizeof(char), ini_data_size, f); + ImFileClose(f); +} + +// Call registered handlers (e.g. SettingsHandlerWindow_WriteAll() + custom handlers) to write their stuff into a text buffer +const char* ImGui::SaveIniSettingsToMemory(size_t* out_size) +{ + ImGuiContext& g = *GImGui; + g.SettingsDirtyTimer = 0.0f; + g.SettingsIniData.Buf.resize(0); + g.SettingsIniData.Buf.push_back(0); + for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) + { + ImGuiSettingsHandler* handler = &g.SettingsHandlers[handler_n]; + handler->WriteAllFn(&g, handler, &g.SettingsIniData); + } + if (out_size) + *out_size = (size_t)g.SettingsIniData.size(); + return g.SettingsIniData.c_str(); +} + +static void WindowSettingsHandler_ClearAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (int i = 0; i != g.Windows.Size; i++) + g.Windows[i]->SettingsOffset = -1; + g.SettingsWindows.clear(); +} + +static void* WindowSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) +{ + ImGuiWindowSettings* settings = ImGui::FindOrCreateWindowSettings(name); + ImGuiID id = settings->ID; + *settings = ImGuiWindowSettings(); // Clear existing if recycling previous entry + settings->ID = id; + settings->WantApply = true; + return (void*)settings; +} + +static void WindowSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line) +{ + ImGuiWindowSettings* settings = (ImGuiWindowSettings*)entry; + int x, y; + int i; + if (sscanf(line, "Pos=%i,%i", &x, &y) == 2) { settings->Pos = ImVec2ih((short)x, (short)y); } + else if (sscanf(line, "Size=%i,%i", &x, &y) == 2) { settings->Size = ImVec2ih((short)x, (short)y); } + else if (sscanf(line, "Collapsed=%d", &i) == 1) { settings->Collapsed = (i != 0); } +} + +// Apply to existing windows (if any) +static void WindowSettingsHandler_ApplyAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->WantApply) + { + if (ImGuiWindow* window = ImGui::FindWindowByID(settings->ID)) + ApplyWindowSettings(window, settings); + settings->WantApply = false; + } +} + +static void WindowSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) +{ + // Gather data from windows that were active during this session + // (if a window wasn't opened in this session we preserve its settings) + ImGuiContext& g = *ctx; + for (int i = 0; i != g.Windows.Size; i++) + { + ImGuiWindow* window = g.Windows[i]; + if (window->Flags & ImGuiWindowFlags_NoSavedSettings) + continue; + + ImGuiWindowSettings* settings = (window->SettingsOffset != -1) ? g.SettingsWindows.ptr_from_offset(window->SettingsOffset) : ImGui::FindWindowSettings(window->ID); + if (!settings) + { + settings = ImGui::CreateNewWindowSettings(window->Name); + window->SettingsOffset = g.SettingsWindows.offset_from_ptr(settings); + } + IM_ASSERT(settings->ID == window->ID); + settings->Pos = ImVec2ih(window->Pos); + settings->Size = ImVec2ih(window->SizeFull); + + settings->Collapsed = window->Collapsed; + } + + // Write to text buffer + buf->reserve(buf->size() + g.SettingsWindows.size() * 6); // ballpark reserve + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + { + const char* settings_name = settings->GetName(); + buf->appendf("[%s][%s]\n", handler->TypeName, settings_name); + buf->appendf("Pos=%d,%d\n", settings->Pos.x, settings->Pos.y); + buf->appendf("Size=%d,%d\n", settings->Size.x, settings->Size.y); + buf->appendf("Collapsed=%d\n", settings->Collapsed); + buf->append("\n"); + } +} + + +//----------------------------------------------------------------------------- +// [SECTION] VIEWPORTS, PLATFORM WINDOWS +//----------------------------------------------------------------------------- +// - GetMainViewport() +// - UpdateViewportsNewFrame() [Internal] +// (this section is more complete in the 'docking' branch) +//----------------------------------------------------------------------------- + +ImGuiViewport* ImGui::GetMainViewport() +{ + ImGuiContext& g = *GImGui; + return g.Viewports[0]; +} + +// Update viewports and monitor infos +static void ImGui::UpdateViewportsNewFrame() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Viewports.Size == 1); + + // Update main viewport with current platform position. + // FIXME-VIEWPORT: Size is driven by backend/user code for backward-compatibility but we should aim to make this more consistent. + ImGuiViewportP* main_viewport = g.Viewports[0]; + main_viewport->Flags = ImGuiViewportFlags_IsPlatformWindow | ImGuiViewportFlags_OwnedByApp; + main_viewport->Pos = ImVec2(0.0f, 0.0f); + main_viewport->Size = g.IO.DisplaySize; + + for (int n = 0; n < g.Viewports.Size; n++) + { + ImGuiViewportP* viewport = g.Viewports[n]; + + // Lock down space taken by menu bars and status bars, reset the offset for fucntions like BeginMainMenuBar() to alter them again. + viewport->WorkOffsetMin = viewport->BuildWorkOffsetMin; + viewport->WorkOffsetMax = viewport->BuildWorkOffsetMax; + viewport->BuildWorkOffsetMin = viewport->BuildWorkOffsetMax = ImVec2(0.0f, 0.0f); + viewport->UpdateWorkRect(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DOCKING +//----------------------------------------------------------------------------- + +// (this section is filled in the 'docking' branch) + + +//----------------------------------------------------------------------------- +// [SECTION] PLATFORM DEPENDENT HELPERS +//----------------------------------------------------------------------------- + +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) + +#ifdef _MSC_VER +#pragma comment(lib, "user32") +#pragma comment(lib, "kernel32") +#endif + +// Win32 clipboard implementation +// We use g.ClipboardHandlerData for temporary storage to ensure it is freed on Shutdown() +static const char* GetClipboardTextFn_DefaultImpl(void*) +{ + ImGuiContext& g = *GImGui; + g.ClipboardHandlerData.clear(); + if (!::OpenClipboard(NULL)) + return NULL; + HANDLE wbuf_handle = ::GetClipboardData(CF_UNICODETEXT); + if (wbuf_handle == NULL) + { + ::CloseClipboard(); + return NULL; + } + if (const WCHAR* wbuf_global = (const WCHAR*)::GlobalLock(wbuf_handle)) + { + int buf_len = ::WideCharToMultiByte(CP_UTF8, 0, wbuf_global, -1, NULL, 0, NULL, NULL); + g.ClipboardHandlerData.resize(buf_len); + ::WideCharToMultiByte(CP_UTF8, 0, wbuf_global, -1, g.ClipboardHandlerData.Data, buf_len, NULL, NULL); + } + ::GlobalUnlock(wbuf_handle); + ::CloseClipboard(); + return g.ClipboardHandlerData.Data; +} + +static void SetClipboardTextFn_DefaultImpl(void*, const char* text) +{ + if (!::OpenClipboard(NULL)) + return; + const int wbuf_length = ::MultiByteToWideChar(CP_UTF8, 0, text, -1, NULL, 0); + HGLOBAL wbuf_handle = ::GlobalAlloc(GMEM_MOVEABLE, (SIZE_T)wbuf_length * sizeof(WCHAR)); + if (wbuf_handle == NULL) + { + ::CloseClipboard(); + return; + } + WCHAR* wbuf_global = (WCHAR*)::GlobalLock(wbuf_handle); + ::MultiByteToWideChar(CP_UTF8, 0, text, -1, wbuf_global, wbuf_length); + ::GlobalUnlock(wbuf_handle); + ::EmptyClipboard(); + if (::SetClipboardData(CF_UNICODETEXT, wbuf_handle) == NULL) + ::GlobalFree(wbuf_handle); + ::CloseClipboard(); +} + +#elif defined(__APPLE__) && TARGET_OS_OSX && defined(IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS) + +#include // Use old API to avoid need for separate .mm file +static PasteboardRef main_clipboard = 0; + +// OSX clipboard implementation +// If you enable this you will need to add '-framework ApplicationServices' to your linker command-line! +static void SetClipboardTextFn_DefaultImpl(void*, const char* text) +{ + if (!main_clipboard) + PasteboardCreate(kPasteboardClipboard, &main_clipboard); + PasteboardClear(main_clipboard); + CFDataRef cf_data = CFDataCreate(kCFAllocatorDefault, (const UInt8*)text, strlen(text)); + if (cf_data) + { + PasteboardPutItemFlavor(main_clipboard, (PasteboardItemID)1, CFSTR("public.utf8-plain-text"), cf_data, 0); + CFRelease(cf_data); + } +} + +static const char* GetClipboardTextFn_DefaultImpl(void*) +{ + if (!main_clipboard) + PasteboardCreate(kPasteboardClipboard, &main_clipboard); + PasteboardSynchronize(main_clipboard); + + ItemCount item_count = 0; + PasteboardGetItemCount(main_clipboard, &item_count); + for (ItemCount i = 0; i < item_count; i++) + { + PasteboardItemID item_id = 0; + PasteboardGetItemIdentifier(main_clipboard, i + 1, &item_id); + CFArrayRef flavor_type_array = 0; + PasteboardCopyItemFlavors(main_clipboard, item_id, &flavor_type_array); + for (CFIndex j = 0, nj = CFArrayGetCount(flavor_type_array); j < nj; j++) + { + CFDataRef cf_data; + if (PasteboardCopyItemFlavorData(main_clipboard, item_id, CFSTR("public.utf8-plain-text"), &cf_data) == noErr) + { + ImGuiContext& g = *GImGui; + g.ClipboardHandlerData.clear(); + int length = (int)CFDataGetLength(cf_data); + g.ClipboardHandlerData.resize(length + 1); + CFDataGetBytes(cf_data, CFRangeMake(0, length), (UInt8*)g.ClipboardHandlerData.Data); + g.ClipboardHandlerData[length] = 0; + CFRelease(cf_data); + return g.ClipboardHandlerData.Data; + } + } + } + return NULL; +} + +#else + +// Local Dear ImGui-only clipboard implementation, if user hasn't defined better clipboard handlers. +static const char* GetClipboardTextFn_DefaultImpl(void*) +{ + ImGuiContext& g = *GImGui; + return g.ClipboardHandlerData.empty() ? NULL : g.ClipboardHandlerData.begin(); +} + +static void SetClipboardTextFn_DefaultImpl(void*, const char* text) +{ + ImGuiContext& g = *GImGui; + g.ClipboardHandlerData.clear(); + const char* text_end = text + strlen(text); + g.ClipboardHandlerData.resize((int)(text_end - text) + 1); + memcpy(&g.ClipboardHandlerData[0], text, (size_t)(text_end - text)); + g.ClipboardHandlerData[(int)(text_end - text)] = 0; +} + +#endif + +// Win32 API IME support (for Asian languages, etc.) +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) + +#include +#ifdef _MSC_VER +#pragma comment(lib, "imm32") +#endif + +static void SetPlatformImeDataFn_DefaultImpl(ImGuiViewport* viewport, ImGuiPlatformImeData* data) +{ + // Notify OS Input Method Editor of text input position + HWND hwnd = (HWND)viewport->PlatformHandleRaw; +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + if (hwnd == 0) + hwnd = (HWND)ImGui::GetIO().ImeWindowHandle; +#endif + if (hwnd == 0) + return; + + ::ImmAssociateContextEx(hwnd, NULL, data->WantVisible ? IACE_DEFAULT : 0); + + if (HIMC himc = ::ImmGetContext(hwnd)) + { + COMPOSITIONFORM composition_form = {}; + composition_form.ptCurrentPos.x = (LONG)data->InputPos.x; + composition_form.ptCurrentPos.y = (LONG)data->InputPos.y; + composition_form.dwStyle = CFS_FORCE_POSITION; + ::ImmSetCompositionWindow(himc, &composition_form); + CANDIDATEFORM candidate_form = {}; + candidate_form.dwStyle = CFS_CANDIDATEPOS; + candidate_form.ptCurrentPos.x = (LONG)data->InputPos.x; + candidate_form.ptCurrentPos.y = (LONG)data->InputPos.y; + ::ImmSetCandidateWindow(himc, &candidate_form); + ::ImmReleaseContext(hwnd, himc); + } +} + +#else + +static void SetPlatformImeDataFn_DefaultImpl(ImGuiViewport*, ImGuiPlatformImeData*) {} + +#endif + +//----------------------------------------------------------------------------- +// [SECTION] METRICS/DEBUGGER WINDOW +//----------------------------------------------------------------------------- +// - RenderViewportThumbnail() [Internal] +// - RenderViewportsThumbnails() [Internal] +// - MetricsHelpMarker() [Internal] +// - ShowMetricsWindow() +// - DebugNodeColumns() [Internal] +// - DebugNodeDrawList() [Internal] +// - DebugNodeDrawCmdShowMeshAndBoundingBox() [Internal] +// - DebugNodeStorage() [Internal] +// - DebugNodeTabBar() [Internal] +// - DebugNodeViewport() [Internal] +// - DebugNodeWindow() [Internal] +// - DebugNodeWindowSettings() [Internal] +// - DebugNodeWindowsList() [Internal] +// - DebugNodeWindowsListByBeginStackParent() [Internal] +//----------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_METRICS_WINDOW + +void ImGui::DebugRenderViewportThumbnail(ImDrawList* draw_list, ImGuiViewportP* viewport, const ImRect& bb) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + ImVec2 scale = bb.GetSize() / viewport->Size; + ImVec2 off = bb.Min - viewport->Pos * scale; + float alpha_mul = 1.0f; + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border, alpha_mul * 0.40f)); + for (int i = 0; i != g.Windows.Size; i++) + { + ImGuiWindow* thumb_window = g.Windows[i]; + if (!thumb_window->WasActive || (thumb_window->Flags & ImGuiWindowFlags_ChildWindow)) + continue; + + ImRect thumb_r = thumb_window->Rect(); + ImRect title_r = thumb_window->TitleBarRect(); + thumb_r = ImRect(ImFloor(off + thumb_r.Min * scale), ImFloor(off + thumb_r.Max * scale)); + title_r = ImRect(ImFloor(off + title_r.Min * scale), ImFloor(off + ImVec2(title_r.Max.x, title_r.Min.y) * scale) + ImVec2(0,5)); // Exaggerate title bar height + thumb_r.ClipWithFull(bb); + title_r.ClipWithFull(bb); + const bool window_is_focused = (g.NavWindow && thumb_window->RootWindowForTitleBarHighlight == g.NavWindow->RootWindowForTitleBarHighlight); + window->DrawList->AddRectFilled(thumb_r.Min, thumb_r.Max, GetColorU32(ImGuiCol_WindowBg, alpha_mul)); + window->DrawList->AddRectFilled(title_r.Min, title_r.Max, GetColorU32(window_is_focused ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBg, alpha_mul)); + window->DrawList->AddRect(thumb_r.Min, thumb_r.Max, GetColorU32(ImGuiCol_Border, alpha_mul)); + window->DrawList->AddText(g.Font, g.FontSize * 1.0f, title_r.Min, GetColorU32(ImGuiCol_Text, alpha_mul), thumb_window->Name, FindRenderedTextEnd(thumb_window->Name)); + } + draw_list->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border, alpha_mul)); +} + +static void RenderViewportsThumbnails() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // We don't display full monitor bounds (we could, but it often looks awkward), instead we display just enough to cover all of our viewports. + float SCALE = 1.0f / 8.0f; + ImRect bb_full(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + for (int n = 0; n < g.Viewports.Size; n++) + bb_full.Add(g.Viewports[n]->GetMainRect()); + ImVec2 p = window->DC.CursorPos; + ImVec2 off = p - bb_full.Min * SCALE; + for (int n = 0; n < g.Viewports.Size; n++) + { + ImGuiViewportP* viewport = g.Viewports[n]; + ImRect viewport_draw_bb(off + (viewport->Pos) * SCALE, off + (viewport->Pos + viewport->Size) * SCALE); + ImGui::DebugRenderViewportThumbnail(window->DrawList, viewport, viewport_draw_bb); + } + ImGui::Dummy(bb_full.GetSize() * SCALE); +} + +// Avoid naming collision with imgui_demo.cpp's HelpMarker() for unity builds. +static void MetricsHelpMarker(const char* desc) +{ + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(desc); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +#ifndef IMGUI_DISABLE_DEMO_WINDOWS +namespace ImGui { void ShowFontAtlas(ImFontAtlas* atlas); } +#endif + +void ImGui::ShowMetricsWindow(bool* p_open) +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig; + if (cfg->ShowStackTool) + ShowStackToolWindow(&cfg->ShowStackTool); + + if (!Begin("Dear ImGui Metrics/Debugger", p_open) || GetCurrentWindow()->BeginCount > 1) + { + End(); + return; + } + + // Basic info + Text("Dear ImGui %s", GetVersion()); + Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); + Text("%d vertices, %d indices (%d triangles)", io.MetricsRenderVertices, io.MetricsRenderIndices, io.MetricsRenderIndices / 3); + Text("%d visible windows, %d active allocations", io.MetricsRenderWindows, io.MetricsActiveAllocations); + //SameLine(); if (SmallButton("GC")) { g.GcCompactAll = true; } + + Separator(); + + // Debugging enums + enum { WRT_OuterRect, WRT_OuterRectClipped, WRT_InnerRect, WRT_InnerClipRect, WRT_WorkRect, WRT_Content, WRT_ContentIdeal, WRT_ContentRegionRect, WRT_Count }; // Windows Rect Type + const char* wrt_rects_names[WRT_Count] = { "OuterRect", "OuterRectClipped", "InnerRect", "InnerClipRect", "WorkRect", "Content", "ContentIdeal", "ContentRegionRect" }; + enum { TRT_OuterRect, TRT_InnerRect, TRT_WorkRect, TRT_HostClipRect, TRT_InnerClipRect, TRT_BackgroundClipRect, TRT_ColumnsRect, TRT_ColumnsWorkRect, TRT_ColumnsClipRect, TRT_ColumnsContentHeadersUsed, TRT_ColumnsContentHeadersIdeal, TRT_ColumnsContentFrozen, TRT_ColumnsContentUnfrozen, TRT_Count }; // Tables Rect Type + const char* trt_rects_names[TRT_Count] = { "OuterRect", "InnerRect", "WorkRect", "HostClipRect", "InnerClipRect", "BackgroundClipRect", "ColumnsRect", "ColumnsWorkRect", "ColumnsClipRect", "ColumnsContentHeadersUsed", "ColumnsContentHeadersIdeal", "ColumnsContentFrozen", "ColumnsContentUnfrozen" }; + if (cfg->ShowWindowsRectsType < 0) + cfg->ShowWindowsRectsType = WRT_WorkRect; + if (cfg->ShowTablesRectsType < 0) + cfg->ShowTablesRectsType = TRT_WorkRect; + + struct Funcs + { + static ImRect GetTableRect(ImGuiTable* table, int rect_type, int n) + { + if (rect_type == TRT_OuterRect) { return table->OuterRect; } + else if (rect_type == TRT_InnerRect) { return table->InnerRect; } + else if (rect_type == TRT_WorkRect) { return table->WorkRect; } + else if (rect_type == TRT_HostClipRect) { return table->HostClipRect; } + else if (rect_type == TRT_InnerClipRect) { return table->InnerClipRect; } + else if (rect_type == TRT_BackgroundClipRect) { return table->BgClipRect; } + else if (rect_type == TRT_ColumnsRect) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->MinX, table->InnerClipRect.Min.y, c->MaxX, table->InnerClipRect.Min.y + table->LastOuterHeight); } + else if (rect_type == TRT_ColumnsWorkRect) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->WorkRect.Min.y, c->WorkMaxX, table->WorkRect.Max.y); } + else if (rect_type == TRT_ColumnsClipRect) { ImGuiTableColumn* c = &table->Columns[n]; return c->ClipRect; } + else if (rect_type == TRT_ColumnsContentHeadersUsed){ ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXHeadersUsed, table->InnerClipRect.Min.y + table->LastFirstRowHeight); } // Note: y1/y2 not always accurate + else if (rect_type == TRT_ColumnsContentHeadersIdeal){ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXHeadersIdeal, table->InnerClipRect.Min.y + table->LastFirstRowHeight); } + else if (rect_type == TRT_ColumnsContentFrozen) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXFrozen, table->InnerClipRect.Min.y + table->LastFirstRowHeight); } + else if (rect_type == TRT_ColumnsContentUnfrozen) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y + table->LastFirstRowHeight, c->ContentMaxXUnfrozen, table->InnerClipRect.Max.y); } + IM_ASSERT(0); + return ImRect(); + } + + static ImRect GetWindowRect(ImGuiWindow* window, int rect_type) + { + if (rect_type == WRT_OuterRect) { return window->Rect(); } + else if (rect_type == WRT_OuterRectClipped) { return window->OuterRectClipped; } + else if (rect_type == WRT_InnerRect) { return window->InnerRect; } + else if (rect_type == WRT_InnerClipRect) { return window->InnerClipRect; } + else if (rect_type == WRT_WorkRect) { return window->WorkRect; } + else if (rect_type == WRT_Content) { ImVec2 min = window->InnerRect.Min - window->Scroll + window->WindowPadding; return ImRect(min, min + window->ContentSize); } + else if (rect_type == WRT_ContentIdeal) { ImVec2 min = window->InnerRect.Min - window->Scroll + window->WindowPadding; return ImRect(min, min + window->ContentSizeIdeal); } + else if (rect_type == WRT_ContentRegionRect) { return window->ContentRegionRect; } + IM_ASSERT(0); + return ImRect(); + } + }; + + // Tools + if (TreeNode("Tools")) + { + // Stack Tool is your best friend! + Checkbox("Show stack tool", &cfg->ShowStackTool); + SameLine(); + MetricsHelpMarker("You can also call ImGui::ShowStackToolWindow() from your code."); + + Checkbox("Show windows begin order", &cfg->ShowWindowsBeginOrder); + Checkbox("Show windows rectangles", &cfg->ShowWindowsRects); + SameLine(); + SetNextItemWidth(GetFontSize() * 12); + cfg->ShowWindowsRects |= Combo("##show_windows_rect_type", &cfg->ShowWindowsRectsType, wrt_rects_names, WRT_Count, WRT_Count); + if (cfg->ShowWindowsRects && g.NavWindow != NULL) + { + BulletText("'%s':", g.NavWindow->Name); + Indent(); + for (int rect_n = 0; rect_n < WRT_Count; rect_n++) + { + ImRect r = Funcs::GetWindowRect(g.NavWindow, rect_n); + Text("(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), wrt_rects_names[rect_n]); + } + Unindent(); + } + + Checkbox("Show tables rectangles", &cfg->ShowTablesRects); + SameLine(); + SetNextItemWidth(GetFontSize() * 12); + cfg->ShowTablesRects |= Combo("##show_table_rects_type", &cfg->ShowTablesRectsType, trt_rects_names, TRT_Count, TRT_Count); + if (cfg->ShowTablesRects && g.NavWindow != NULL) + { + for (int table_n = 0; table_n < g.Tables.GetMapSize(); table_n++) + { + ImGuiTable* table = g.Tables.TryGetMapData(table_n); + if (table == NULL || table->LastFrameActive < g.FrameCount - 1 || (table->OuterWindow != g.NavWindow && table->InnerWindow != g.NavWindow)) + continue; + + BulletText("Table 0x%08X (%d columns, in '%s')", table->ID, table->ColumnsCount, table->OuterWindow->Name); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(table->OuterRect.Min - ImVec2(1, 1), table->OuterRect.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + Indent(); + char buf[128]; + for (int rect_n = 0; rect_n < TRT_Count; rect_n++) + { + if (rect_n >= TRT_ColumnsRect) + { + if (rect_n != TRT_ColumnsRect && rect_n != TRT_ColumnsClipRect) + continue; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImRect r = Funcs::GetTableRect(table, rect_n, column_n); + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) Col %d %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), column_n, trt_rects_names[rect_n]); + Selectable(buf); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + } + } + else + { + ImRect r = Funcs::GetTableRect(table, rect_n, -1); + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), trt_rects_names[rect_n]); + Selectable(buf); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + } + } + Unindent(); + } + } + + // The Item Picker tool is super useful to visually select an item and break into the call-stack of where it was submitted. + if (Button("Item Picker..")) + DebugStartItemPicker(); + SameLine(); + MetricsHelpMarker("Will call the IM_DEBUG_BREAK() macro to break in debugger.\nWarning: If you don't have a debugger attached, this will probably crash."); + + TreePop(); + } + + // Windows + if (TreeNode("Windows", "Windows (%d)", g.Windows.Size)) + { + //SetNextItemOpen(true, ImGuiCond_Once); + DebugNodeWindowsList(&g.Windows, "By display order"); + DebugNodeWindowsList(&g.WindowsFocusOrder, "By focus order (root windows)"); + if (TreeNode("By submission order (begin stack)")) + { + // Here we display windows in their submitted order/hierarchy, however note that the Begin stack doesn't constitute a Parent<>Child relationship! + ImVector& temp_buffer = g.WindowsTempSortBuffer; + temp_buffer.resize(0); + for (int i = 0; i < g.Windows.Size; i++) + if (g.Windows[i]->LastFrameActive + 1 >= g.FrameCount) + temp_buffer.push_back(g.Windows[i]); + struct Func { static int IMGUI_CDECL WindowComparerByBeginOrder(const void* lhs, const void* rhs) { return ((int)(*(const ImGuiWindow* const *)lhs)->BeginOrderWithinContext - (*(const ImGuiWindow* const*)rhs)->BeginOrderWithinContext); } }; + ImQsort(temp_buffer.Data, (size_t)temp_buffer.Size, sizeof(ImGuiWindow*), Func::WindowComparerByBeginOrder); + DebugNodeWindowsListByBeginStackParent(temp_buffer.Data, temp_buffer.Size, NULL); + TreePop(); + } + + TreePop(); + } + + // DrawLists + int drawlist_count = 0; + for (int viewport_i = 0; viewport_i < g.Viewports.Size; viewport_i++) + drawlist_count += g.Viewports[viewport_i]->DrawDataBuilder.GetDrawListCount(); + if (TreeNode("DrawLists", "DrawLists (%d)", drawlist_count)) + { + Checkbox("Show ImDrawCmd mesh when hovering", &cfg->ShowDrawCmdMesh); + Checkbox("Show ImDrawCmd bounding boxes when hovering", &cfg->ShowDrawCmdBoundingBoxes); + for (int viewport_i = 0; viewport_i < g.Viewports.Size; viewport_i++) + { + ImGuiViewportP* viewport = g.Viewports[viewport_i]; + for (int layer_i = 0; layer_i < IM_ARRAYSIZE(viewport->DrawDataBuilder.Layers); layer_i++) + for (int draw_list_i = 0; draw_list_i < viewport->DrawDataBuilder.Layers[layer_i].Size; draw_list_i++) + DebugNodeDrawList(NULL, viewport->DrawDataBuilder.Layers[layer_i][draw_list_i], "DrawList"); + } + TreePop(); + } + + // Viewports + if (TreeNode("Viewports", "Viewports (%d)", g.Viewports.Size)) + { + Indent(GetTreeNodeToLabelSpacing()); + RenderViewportsThumbnails(); + Unindent(GetTreeNodeToLabelSpacing()); + for (int i = 0; i < g.Viewports.Size; i++) + DebugNodeViewport(g.Viewports[i]); + TreePop(); + } + + // Details for Popups + if (TreeNode("Popups", "Popups (%d)", g.OpenPopupStack.Size)) + { + for (int i = 0; i < g.OpenPopupStack.Size; i++) + { + ImGuiWindow* window = g.OpenPopupStack[i].Window; + BulletText("PopupID: %08x, Window: '%s'%s%s", g.OpenPopupStack[i].PopupId, window ? window->Name : "NULL", window && (window->Flags & ImGuiWindowFlags_ChildWindow) ? " ChildWindow" : "", window && (window->Flags & ImGuiWindowFlags_ChildMenu) ? " ChildMenu" : ""); + } + TreePop(); + } + + // Details for TabBars + if (TreeNode("TabBars", "Tab Bars (%d)", g.TabBars.GetAliveCount())) + { + for (int n = 0; n < g.TabBars.GetMapSize(); n++) + if (ImGuiTabBar* tab_bar = g.TabBars.TryGetMapData(n)) + { + PushID(tab_bar); + DebugNodeTabBar(tab_bar, "TabBar"); + PopID(); + } + TreePop(); + } + + // Details for Tables + if (TreeNode("Tables", "Tables (%d)", g.Tables.GetAliveCount())) + { + for (int n = 0; n < g.Tables.GetMapSize(); n++) + if (ImGuiTable* table = g.Tables.TryGetMapData(n)) + DebugNodeTable(table); + TreePop(); + } + + // Details for Fonts +#ifndef IMGUI_DISABLE_DEMO_WINDOWS + ImFontAtlas* atlas = g.IO.Fonts; + if (TreeNode("Fonts", "Fonts (%d)", atlas->Fonts.Size)) + { + ShowFontAtlas(atlas); + TreePop(); + } +#endif + + // Details for Docking +#ifdef IMGUI_HAS_DOCK + if (TreeNode("Docking")) + { + TreePop(); + } +#endif // #ifdef IMGUI_HAS_DOCK + + // Settings + if (TreeNode("Settings")) + { + if (SmallButton("Clear")) + ClearIniSettings(); + SameLine(); + if (SmallButton("Save to memory")) + SaveIniSettingsToMemory(); + SameLine(); + if (SmallButton("Save to disk")) + SaveIniSettingsToDisk(g.IO.IniFilename); + SameLine(); + if (g.IO.IniFilename) + Text("\"%s\"", g.IO.IniFilename); + else + TextUnformatted(""); + Text("SettingsDirtyTimer %.2f", g.SettingsDirtyTimer); + if (TreeNode("SettingsHandlers", "Settings handlers: (%d)", g.SettingsHandlers.Size)) + { + for (int n = 0; n < g.SettingsHandlers.Size; n++) + BulletText("%s", g.SettingsHandlers[n].TypeName); + TreePop(); + } + if (TreeNode("SettingsWindows", "Settings packed data: Windows: %d bytes", g.SettingsWindows.size())) + { + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + DebugNodeWindowSettings(settings); + TreePop(); + } + + if (TreeNode("SettingsTables", "Settings packed data: Tables: %d bytes", g.SettingsTables.size())) + { + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + DebugNodeTableSettings(settings); + TreePop(); + } + +#ifdef IMGUI_HAS_DOCK +#endif // #ifdef IMGUI_HAS_DOCK + + if (TreeNode("SettingsIniData", "Settings unpacked data (.ini): %d bytes", g.SettingsIniData.size())) + { + InputTextMultiline("##Ini", (char*)(void*)g.SettingsIniData.c_str(), g.SettingsIniData.Buf.Size, ImVec2(-FLT_MIN, GetTextLineHeight() * 20), ImGuiInputTextFlags_ReadOnly); + TreePop(); + } + TreePop(); + } + + // Misc Details + if (TreeNode("Internal state")) + { + Text("WINDOWING"); + Indent(); + Text("HoveredWindow: '%s'", g.HoveredWindow ? g.HoveredWindow->Name : "NULL"); + Text("HoveredWindow->Root: '%s'", g.HoveredWindow ? g.HoveredWindow->RootWindow->Name : "NULL"); + Text("HoveredWindowUnderMovingWindow: '%s'", g.HoveredWindowUnderMovingWindow ? g.HoveredWindowUnderMovingWindow->Name : "NULL"); + Text("MovingWindow: '%s'", g.MovingWindow ? g.MovingWindow->Name : "NULL"); + Unindent(); + + Text("ITEMS"); + Indent(); + Text("ActiveId: 0x%08X/0x%08X (%.2f sec), AllowOverlap: %d, Source: %s", g.ActiveId, g.ActiveIdPreviousFrame, g.ActiveIdTimer, g.ActiveIdAllowOverlap, GetInputSourceName(g.ActiveIdSource)); + Text("ActiveIdWindow: '%s'", g.ActiveIdWindow ? g.ActiveIdWindow->Name : "NULL"); + + int active_id_using_key_input_count = 0; + for (int n = 0; n < ImGuiKey_NamedKey_COUNT; n++) + active_id_using_key_input_count += g.ActiveIdUsingKeyInputMask[n] ? 1 : 0; + Text("ActiveIdUsing: Wheel: %d, NavDirMask: %X, NavInputMask: %X, KeyInputMask: %d key(s)", g.ActiveIdUsingMouseWheel, g.ActiveIdUsingNavDirMask, g.ActiveIdUsingNavInputMask, active_id_using_key_input_count); + Text("HoveredId: 0x%08X (%.2f sec), AllowOverlap: %d", g.HoveredIdPreviousFrame, g.HoveredIdTimer, g.HoveredIdAllowOverlap); // Not displaying g.HoveredId as it is update mid-frame + Text("DragDrop: %d, SourceId = 0x%08X, Payload \"%s\" (%d bytes)", g.DragDropActive, g.DragDropPayload.SourceId, g.DragDropPayload.DataType, g.DragDropPayload.DataSize); + Unindent(); + + Text("NAV,FOCUS"); + Indent(); + Text("NavWindow: '%s'", g.NavWindow ? g.NavWindow->Name : "NULL"); + Text("NavId: 0x%08X, NavLayer: %d", g.NavId, g.NavLayer); + Text("NavInputSource: %s", GetInputSourceName(g.NavInputSource)); + Text("NavActive: %d, NavVisible: %d", g.IO.NavActive, g.IO.NavVisible); + Text("NavActivateId/DownId/PressedId/InputId: %08X/%08X/%08X/%08X", g.NavActivateId, g.NavActivateDownId, g.NavActivatePressedId, g.NavActivateInputId); + Text("NavActivateFlags: %04X", g.NavActivateFlags); + Text("NavDisableHighlight: %d, NavDisableMouseHover: %d", g.NavDisableHighlight, g.NavDisableMouseHover); + Text("NavFocusScopeId = 0x%08X", g.NavFocusScopeId); + Text("NavWindowingTarget: '%s'", g.NavWindowingTarget ? g.NavWindowingTarget->Name : "NULL"); + Unindent(); + + TreePop(); + } + + // Overlay: Display windows Rectangles and Begin Order + if (cfg->ShowWindowsRects || cfg->ShowWindowsBeginOrder) + { + for (int n = 0; n < g.Windows.Size; n++) + { + ImGuiWindow* window = g.Windows[n]; + if (!window->WasActive) + continue; + ImDrawList* draw_list = GetForegroundDrawList(window); + if (cfg->ShowWindowsRects) + { + ImRect r = Funcs::GetWindowRect(window, cfg->ShowWindowsRectsType); + draw_list->AddRect(r.Min, r.Max, IM_COL32(255, 0, 128, 255)); + } + if (cfg->ShowWindowsBeginOrder && !(window->Flags & ImGuiWindowFlags_ChildWindow)) + { + char buf[32]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "%d", window->BeginOrderWithinContext); + float font_size = GetFontSize(); + draw_list->AddRectFilled(window->Pos, window->Pos + ImVec2(font_size, font_size), IM_COL32(200, 100, 100, 255)); + draw_list->AddText(window->Pos, IM_COL32(255, 255, 255, 255), buf); + } + } + } + + // Overlay: Display Tables Rectangles + if (cfg->ShowTablesRects) + { + for (int table_n = 0; table_n < g.Tables.GetMapSize(); table_n++) + { + ImGuiTable* table = g.Tables.TryGetMapData(table_n); + if (table == NULL || table->LastFrameActive < g.FrameCount - 1) + continue; + ImDrawList* draw_list = GetForegroundDrawList(table->OuterWindow); + if (cfg->ShowTablesRectsType >= TRT_ColumnsRect) + { + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImRect r = Funcs::GetTableRect(table, cfg->ShowTablesRectsType, column_n); + ImU32 col = (table->HoveredColumnBody == column_n) ? IM_COL32(255, 255, 128, 255) : IM_COL32(255, 0, 128, 255); + float thickness = (table->HoveredColumnBody == column_n) ? 3.0f : 1.0f; + draw_list->AddRect(r.Min, r.Max, col, 0.0f, 0, thickness); + } + } + else + { + ImRect r = Funcs::GetTableRect(table, cfg->ShowTablesRectsType, -1); + draw_list->AddRect(r.Min, r.Max, IM_COL32(255, 0, 128, 255)); + } + } + } + +#ifdef IMGUI_HAS_DOCK + // Overlay: Display Docking info + if (show_docking_nodes && g.IO.KeyCtrl) + { + } +#endif // #ifdef IMGUI_HAS_DOCK + + End(); +} + +// [DEBUG] List fonts in a font atlas and display its texture +void ImGui::ShowFontAtlas(ImFontAtlas* atlas) +{ + for (int i = 0; i < atlas->Fonts.Size; i++) + { + ImFont* font = atlas->Fonts[i]; + PushID(font); + DebugNodeFont(font); + PopID(); + } + if (TreeNode("Atlas texture", "Atlas texture (%dx%d pixels)", atlas->TexWidth, atlas->TexHeight)) + { + ImVec4 tint_col = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + ImVec4 border_col = ImVec4(1.0f, 1.0f, 1.0f, 0.5f); + Image(atlas->TexID, ImVec2((float)atlas->TexWidth, (float)atlas->TexHeight), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), tint_col, border_col); + TreePop(); + } +} + +// [DEBUG] Display contents of Columns +void ImGui::DebugNodeColumns(ImGuiOldColumns* columns) +{ + if (!TreeNode((void*)(uintptr_t)columns->ID, "Columns Id: 0x%08X, Count: %d, Flags: 0x%04X", columns->ID, columns->Count, columns->Flags)) + return; + BulletText("Width: %.1f (MinX: %.1f, MaxX: %.1f)", columns->OffMaxX - columns->OffMinX, columns->OffMinX, columns->OffMaxX); + for (int column_n = 0; column_n < columns->Columns.Size; column_n++) + BulletText("Column %02d: OffsetNorm %.3f (= %.1f px)", column_n, columns->Columns[column_n].OffsetNorm, GetColumnOffsetFromNorm(columns, columns->Columns[column_n].OffsetNorm)); + TreePop(); +} + +// [DEBUG] Display contents of ImDrawList +void ImGui::DebugNodeDrawList(ImGuiWindow* window, const ImDrawList* draw_list, const char* label) +{ + ImGuiContext& g = *GImGui; + ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig; + int cmd_count = draw_list->CmdBuffer.Size; + if (cmd_count > 0 && draw_list->CmdBuffer.back().ElemCount == 0 && draw_list->CmdBuffer.back().UserCallback == NULL) + cmd_count--; + bool node_open = TreeNode(draw_list, "%s: '%s' %d vtx, %d indices, %d cmds", label, draw_list->_OwnerName ? draw_list->_OwnerName : "", draw_list->VtxBuffer.Size, draw_list->IdxBuffer.Size, cmd_count); + if (draw_list == GetWindowDrawList()) + { + SameLine(); + TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "CURRENTLY APPENDING"); // Can't display stats for active draw list! (we don't have the data double-buffered) + if (node_open) + TreePop(); + return; + } + + ImDrawList* fg_draw_list = GetForegroundDrawList(window); // Render additional visuals into the top-most draw list + if (window && IsItemHovered() && fg_draw_list) + fg_draw_list->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); + if (!node_open) + return; + + if (window && !window->WasActive) + TextDisabled("Warning: owning Window is inactive. This DrawList is not being rendered!"); + + for (const ImDrawCmd* pcmd = draw_list->CmdBuffer.Data; pcmd < draw_list->CmdBuffer.Data + cmd_count; pcmd++) + { + if (pcmd->UserCallback) + { + BulletText("Callback %p, user_data %p", pcmd->UserCallback, pcmd->UserCallbackData); + continue; + } + + char buf[300]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "DrawCmd:%5d tris, Tex 0x%p, ClipRect (%4.0f,%4.0f)-(%4.0f,%4.0f)", + pcmd->ElemCount / 3, (void*)(intptr_t)pcmd->TextureId, + pcmd->ClipRect.x, pcmd->ClipRect.y, pcmd->ClipRect.z, pcmd->ClipRect.w); + bool pcmd_node_open = TreeNode((void*)(pcmd - draw_list->CmdBuffer.begin()), "%s", buf); + if (IsItemHovered() && (cfg->ShowDrawCmdMesh || cfg->ShowDrawCmdBoundingBoxes) && fg_draw_list) + DebugNodeDrawCmdShowMeshAndBoundingBox(fg_draw_list, draw_list, pcmd, cfg->ShowDrawCmdMesh, cfg->ShowDrawCmdBoundingBoxes); + if (!pcmd_node_open) + continue; + + // Calculate approximate coverage area (touched pixel count) + // This will be in pixels squared as long there's no post-scaling happening to the renderer output. + const ImDrawIdx* idx_buffer = (draw_list->IdxBuffer.Size > 0) ? draw_list->IdxBuffer.Data : NULL; + const ImDrawVert* vtx_buffer = draw_list->VtxBuffer.Data + pcmd->VtxOffset; + float total_area = 0.0f; + for (unsigned int idx_n = pcmd->IdxOffset; idx_n < pcmd->IdxOffset + pcmd->ElemCount; ) + { + ImVec2 triangle[3]; + for (int n = 0; n < 3; n++, idx_n++) + triangle[n] = vtx_buffer[idx_buffer ? idx_buffer[idx_n] : idx_n].pos; + total_area += ImTriangleArea(triangle[0], triangle[1], triangle[2]); + } + + // Display vertex information summary. Hover to get all triangles drawn in wire-frame + ImFormatString(buf, IM_ARRAYSIZE(buf), "Mesh: ElemCount: %d, VtxOffset: +%d, IdxOffset: +%d, Area: ~%0.f px", pcmd->ElemCount, pcmd->VtxOffset, pcmd->IdxOffset, total_area); + Selectable(buf); + if (IsItemHovered() && fg_draw_list) + DebugNodeDrawCmdShowMeshAndBoundingBox(fg_draw_list, draw_list, pcmd, true, false); + + // Display individual triangles/vertices. Hover on to get the corresponding triangle highlighted. + ImGuiListClipper clipper; + clipper.Begin(pcmd->ElemCount / 3); // Manually coarse clip our print out of individual vertices to save CPU, only items that may be visible. + while (clipper.Step()) + for (int prim = clipper.DisplayStart, idx_i = pcmd->IdxOffset + clipper.DisplayStart * 3; prim < clipper.DisplayEnd; prim++) + { + char* buf_p = buf, * buf_end = buf + IM_ARRAYSIZE(buf); + ImVec2 triangle[3]; + for (int n = 0; n < 3; n++, idx_i++) + { + const ImDrawVert& v = vtx_buffer[idx_buffer ? idx_buffer[idx_i] : idx_i]; + triangle[n] = v.pos; + buf_p += ImFormatString(buf_p, buf_end - buf_p, "%s %04d: pos (%8.2f,%8.2f), uv (%.6f,%.6f), col %08X\n", + (n == 0) ? "Vert:" : " ", idx_i, v.pos.x, v.pos.y, v.uv.x, v.uv.y, v.col); + } + + Selectable(buf, false); + if (fg_draw_list && IsItemHovered()) + { + ImDrawListFlags backup_flags = fg_draw_list->Flags; + fg_draw_list->Flags &= ~ImDrawListFlags_AntiAliasedLines; // Disable AA on triangle outlines is more readable for very large and thin triangles. + fg_draw_list->AddPolyline(triangle, 3, IM_COL32(255, 255, 0, 255), ImDrawFlags_Closed, 1.0f); + fg_draw_list->Flags = backup_flags; + } + } + TreePop(); + } + TreePop(); +} + +// [DEBUG] Display mesh/aabb of a ImDrawCmd +void ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList* out_draw_list, const ImDrawList* draw_list, const ImDrawCmd* draw_cmd, bool show_mesh, bool show_aabb) +{ + IM_ASSERT(show_mesh || show_aabb); + + // Draw wire-frame version of all triangles + ImRect clip_rect = draw_cmd->ClipRect; + ImRect vtxs_rect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + ImDrawListFlags backup_flags = out_draw_list->Flags; + out_draw_list->Flags &= ~ImDrawListFlags_AntiAliasedLines; // Disable AA on triangle outlines is more readable for very large and thin triangles. + for (unsigned int idx_n = draw_cmd->IdxOffset, idx_end = draw_cmd->IdxOffset + draw_cmd->ElemCount; idx_n < idx_end; ) + { + ImDrawIdx* idx_buffer = (draw_list->IdxBuffer.Size > 0) ? draw_list->IdxBuffer.Data : NULL; // We don't hold on those pointers past iterations as ->AddPolyline() may invalidate them if out_draw_list==draw_list + ImDrawVert* vtx_buffer = draw_list->VtxBuffer.Data + draw_cmd->VtxOffset; + + ImVec2 triangle[3]; + for (int n = 0; n < 3; n++, idx_n++) + vtxs_rect.Add((triangle[n] = vtx_buffer[idx_buffer ? idx_buffer[idx_n] : idx_n].pos)); + if (show_mesh) + out_draw_list->AddPolyline(triangle, 3, IM_COL32(255, 255, 0, 255), ImDrawFlags_Closed, 1.0f); // In yellow: mesh triangles + } + // Draw bounding boxes + if (show_aabb) + { + out_draw_list->AddRect(ImFloor(clip_rect.Min), ImFloor(clip_rect.Max), IM_COL32(255, 0, 255, 255)); // In pink: clipping rectangle submitted to GPU + out_draw_list->AddRect(ImFloor(vtxs_rect.Min), ImFloor(vtxs_rect.Max), IM_COL32(0, 255, 255, 255)); // In cyan: bounding box of triangles + } + out_draw_list->Flags = backup_flags; +} + +// [DEBUG] Display details for a single font, called by ShowStyleEditor(). +void ImGui::DebugNodeFont(ImFont* font) +{ + bool opened = TreeNode(font, "Font: \"%s\"\n%.2f px, %d glyphs, %d file(s)", + font->ConfigData ? font->ConfigData[0].Name : "", font->FontSize, font->Glyphs.Size, font->ConfigDataCount); + SameLine(); + if (SmallButton("Set as default")) + GetIO().FontDefault = font; + if (!opened) + return; + + // Display preview text + PushFont(font); + Text("The quick brown fox jumps over the lazy dog"); + PopFont(); + + // Display details + SetNextItemWidth(GetFontSize() * 8); + DragFloat("Font scale", &font->Scale, 0.005f, 0.3f, 2.0f, "%.1f"); + SameLine(); MetricsHelpMarker( + "Note than the default embedded font is NOT meant to be scaled.\n\n" + "Font are currently rendered into bitmaps at a given size at the time of building the atlas. " + "You may oversample them to get some flexibility with scaling. " + "You can also render at multiple sizes and select which one to use at runtime.\n\n" + "(Glimmer of hope: the atlas system will be rewritten in the future to make scaling more flexible.)"); + Text("Ascent: %f, Descent: %f, Height: %f", font->Ascent, font->Descent, font->Ascent - font->Descent); + char c_str[5]; + Text("Fallback character: '%s' (U+%04X)", ImTextCharToUtf8(c_str, font->FallbackChar), font->FallbackChar); + Text("Ellipsis character: '%s' (U+%04X)", ImTextCharToUtf8(c_str, font->EllipsisChar), font->EllipsisChar); + const int surface_sqrt = (int)ImSqrt((float)font->MetricsTotalSurface); + Text("Texture Area: about %d px ~%dx%d px", font->MetricsTotalSurface, surface_sqrt, surface_sqrt); + for (int config_i = 0; config_i < font->ConfigDataCount; config_i++) + if (font->ConfigData) + if (const ImFontConfig* cfg = &font->ConfigData[config_i]) + BulletText("Input %d: \'%s\', Oversample: (%d,%d), PixelSnapH: %d, Offset: (%.1f,%.1f)", + config_i, cfg->Name, cfg->OversampleH, cfg->OversampleV, cfg->PixelSnapH, cfg->GlyphOffset.x, cfg->GlyphOffset.y); + + // Display all glyphs of the fonts in separate pages of 256 characters + if (TreeNode("Glyphs", "Glyphs (%d)", font->Glyphs.Size)) + { + ImDrawList* draw_list = GetWindowDrawList(); + const ImU32 glyph_col = GetColorU32(ImGuiCol_Text); + const float cell_size = font->FontSize * 1; + const float cell_spacing = GetStyle().ItemSpacing.y; + for (unsigned int base = 0; base <= IM_UNICODE_CODEPOINT_MAX; base += 256) + { + // Skip ahead if a large bunch of glyphs are not present in the font (test in chunks of 4k) + // This is only a small optimization to reduce the number of iterations when IM_UNICODE_MAX_CODEPOINT + // is large // (if ImWchar==ImWchar32 we will do at least about 272 queries here) + if (!(base & 4095) && font->IsGlyphRangeUnused(base, base + 4095)) + { + base += 4096 - 256; + continue; + } + + int count = 0; + for (unsigned int n = 0; n < 256; n++) + if (font->FindGlyphNoFallback((ImWchar)(base + n))) + count++; + if (count <= 0) + continue; + if (!TreeNode((void*)(intptr_t)base, "U+%04X..U+%04X (%d %s)", base, base + 255, count, count > 1 ? "glyphs" : "glyph")) + continue; + + // Draw a 16x16 grid of glyphs + ImVec2 base_pos = GetCursorScreenPos(); + for (unsigned int n = 0; n < 256; n++) + { + // We use ImFont::RenderChar as a shortcut because we don't have UTF-8 conversion functions + // available here and thus cannot easily generate a zero-terminated UTF-8 encoded string. + ImVec2 cell_p1(base_pos.x + (n % 16) * (cell_size + cell_spacing), base_pos.y + (n / 16) * (cell_size + cell_spacing)); + ImVec2 cell_p2(cell_p1.x + cell_size, cell_p1.y + cell_size); + const ImFontGlyph* glyph = font->FindGlyphNoFallback((ImWchar)(base + n)); + draw_list->AddRect(cell_p1, cell_p2, glyph ? IM_COL32(255, 255, 255, 100) : IM_COL32(255, 255, 255, 50)); + if (glyph) + font->RenderChar(draw_list, cell_size, cell_p1, glyph_col, (ImWchar)(base + n)); + if (glyph && IsMouseHoveringRect(cell_p1, cell_p2)) + { + BeginTooltip(); + Text("Codepoint: U+%04X", base + n); + Separator(); + Text("Visible: %d", glyph->Visible); + Text("AdvanceX: %.1f", glyph->AdvanceX); + Text("Pos: (%.2f,%.2f)->(%.2f,%.2f)", glyph->X0, glyph->Y0, glyph->X1, glyph->Y1); + Text("UV: (%.3f,%.3f)->(%.3f,%.3f)", glyph->U0, glyph->V0, glyph->U1, glyph->V1); + EndTooltip(); + } + } + Dummy(ImVec2((cell_size + cell_spacing) * 16, (cell_size + cell_spacing) * 16)); + TreePop(); + } + TreePop(); + } + TreePop(); +} + +// [DEBUG] Display contents of ImGuiStorage +void ImGui::DebugNodeStorage(ImGuiStorage* storage, const char* label) +{ + if (!TreeNode(label, "%s: %d entries, %d bytes", label, storage->Data.Size, storage->Data.size_in_bytes())) + return; + for (int n = 0; n < storage->Data.Size; n++) + { + const ImGuiStorage::ImGuiStoragePair& p = storage->Data[n]; + BulletText("Key 0x%08X Value { i: %d }", p.key, p.val_i); // Important: we currently don't store a type, real value may not be integer. + } + TreePop(); +} + +// [DEBUG] Display contents of ImGuiTabBar +void ImGui::DebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label) +{ + // Standalone tab bars (not associated to docking/windows functionality) currently hold no discernible strings. + char buf[256]; + char* p = buf; + const char* buf_end = buf + IM_ARRAYSIZE(buf); + const bool is_active = (tab_bar->PrevFrameVisible >= GetFrameCount() - 2); + p += ImFormatString(p, buf_end - p, "%s 0x%08X (%d tabs)%s", label, tab_bar->ID, tab_bar->Tabs.Size, is_active ? "" : " *Inactive*"); + p += ImFormatString(p, buf_end - p, " { "); + for (int tab_n = 0; tab_n < ImMin(tab_bar->Tabs.Size, 3); tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + p += ImFormatString(p, buf_end - p, "%s'%s'", + tab_n > 0 ? ", " : "", (tab->NameOffset != -1) ? tab_bar->GetTabName(tab) : "???"); + } + p += ImFormatString(p, buf_end - p, (tab_bar->Tabs.Size > 3) ? " ... }" : " } "); + if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + bool open = TreeNode(label, "%s", buf); + if (!is_active) { PopStyleColor(); } + if (is_active && IsItemHovered()) + { + ImDrawList* draw_list = GetForegroundDrawList(); + draw_list->AddRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, IM_COL32(255, 255, 0, 255)); + draw_list->AddLine(ImVec2(tab_bar->ScrollingRectMinX, tab_bar->BarRect.Min.y), ImVec2(tab_bar->ScrollingRectMinX, tab_bar->BarRect.Max.y), IM_COL32(0, 255, 0, 255)); + draw_list->AddLine(ImVec2(tab_bar->ScrollingRectMaxX, tab_bar->BarRect.Min.y), ImVec2(tab_bar->ScrollingRectMaxX, tab_bar->BarRect.Max.y), IM_COL32(0, 255, 0, 255)); + } + if (open) + { + for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + { + const ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + PushID(tab); + if (SmallButton("<")) { TabBarQueueReorder(tab_bar, tab, -1); } SameLine(0, 2); + if (SmallButton(">")) { TabBarQueueReorder(tab_bar, tab, +1); } SameLine(); + Text("%02d%c Tab 0x%08X '%s' Offset: %.1f, Width: %.1f/%.1f", + tab_n, (tab->ID == tab_bar->SelectedTabId) ? '*' : ' ', tab->ID, (tab->NameOffset != -1) ? tab_bar->GetTabName(tab) : "???", tab->Offset, tab->Width, tab->ContentWidth); + PopID(); + } + TreePop(); + } +} + +void ImGui::DebugNodeViewport(ImGuiViewportP* viewport) +{ + SetNextItemOpen(true, ImGuiCond_Once); + if (TreeNode("viewport0", "Viewport #%d", 0)) + { + ImGuiWindowFlags flags = viewport->Flags; + BulletText("Main Pos: (%.0f,%.0f), Size: (%.0f,%.0f)\nWorkArea Offset Left: %.0f Top: %.0f, Right: %.0f, Bottom: %.0f", + viewport->Pos.x, viewport->Pos.y, viewport->Size.x, viewport->Size.y, + viewport->WorkOffsetMin.x, viewport->WorkOffsetMin.y, viewport->WorkOffsetMax.x, viewport->WorkOffsetMax.y); + BulletText("Flags: 0x%04X =%s%s%s", viewport->Flags, + (flags & ImGuiViewportFlags_IsPlatformWindow) ? " IsPlatformWindow" : "", + (flags & ImGuiViewportFlags_IsPlatformMonitor) ? " IsPlatformMonitor" : "", + (flags & ImGuiViewportFlags_OwnedByApp) ? " OwnedByApp" : ""); + for (int layer_i = 0; layer_i < IM_ARRAYSIZE(viewport->DrawDataBuilder.Layers); layer_i++) + for (int draw_list_i = 0; draw_list_i < viewport->DrawDataBuilder.Layers[layer_i].Size; draw_list_i++) + DebugNodeDrawList(NULL, viewport->DrawDataBuilder.Layers[layer_i][draw_list_i], "DrawList"); + TreePop(); + } +} + +void ImGui::DebugNodeWindow(ImGuiWindow* window, const char* label) +{ + if (window == NULL) + { + BulletText("%s: NULL", label); + return; + } + + ImGuiContext& g = *GImGui; + const bool is_active = window->WasActive; + ImGuiTreeNodeFlags tree_node_flags = (window == g.NavWindow) ? ImGuiTreeNodeFlags_Selected : ImGuiTreeNodeFlags_None; + if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + const bool open = TreeNodeEx(label, tree_node_flags, "%s '%s'%s", label, window->Name, is_active ? "" : " *Inactive*"); + if (!is_active) { PopStyleColor(); } + if (IsItemHovered() && is_active) + GetForegroundDrawList(window)->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); + if (!open) + return; + + if (window->MemoryCompacted) + TextDisabled("Note: some memory buffers have been compacted/freed."); + + ImGuiWindowFlags flags = window->Flags; + DebugNodeDrawList(window, window->DrawList, "DrawList"); + BulletText("Pos: (%.1f,%.1f), Size: (%.1f,%.1f), ContentSize (%.1f,%.1f) Ideal (%.1f,%.1f)", window->Pos.x, window->Pos.y, window->Size.x, window->Size.y, window->ContentSize.x, window->ContentSize.y, window->ContentSizeIdeal.x, window->ContentSizeIdeal.y); + BulletText("Flags: 0x%08X (%s%s%s%s%s%s%s%s%s..)", flags, + (flags & ImGuiWindowFlags_ChildWindow) ? "Child " : "", (flags & ImGuiWindowFlags_Tooltip) ? "Tooltip " : "", (flags & ImGuiWindowFlags_Popup) ? "Popup " : "", + (flags & ImGuiWindowFlags_Modal) ? "Modal " : "", (flags & ImGuiWindowFlags_ChildMenu) ? "ChildMenu " : "", (flags & ImGuiWindowFlags_NoSavedSettings) ? "NoSavedSettings " : "", + (flags & ImGuiWindowFlags_NoMouseInputs)? "NoMouseInputs":"", (flags & ImGuiWindowFlags_NoNavInputs) ? "NoNavInputs" : "", (flags & ImGuiWindowFlags_AlwaysAutoResize) ? "AlwaysAutoResize" : ""); + BulletText("Scroll: (%.2f/%.2f,%.2f/%.2f) Scrollbar:%s%s", window->Scroll.x, window->ScrollMax.x, window->Scroll.y, window->ScrollMax.y, window->ScrollbarX ? "X" : "", window->ScrollbarY ? "Y" : ""); + BulletText("Active: %d/%d, WriteAccessed: %d, BeginOrderWithinContext: %d", window->Active, window->WasActive, window->WriteAccessed, (window->Active || window->WasActive) ? window->BeginOrderWithinContext : -1); + BulletText("Appearing: %d, Hidden: %d (CanSkip %d Cannot %d), SkipItems: %d", window->Appearing, window->Hidden, window->HiddenFramesCanSkipItems, window->HiddenFramesCannotSkipItems, window->SkipItems); + for (int layer = 0; layer < ImGuiNavLayer_COUNT; layer++) + { + ImRect r = window->NavRectRel[layer]; + if (r.Min.x >= r.Max.y && r.Min.y >= r.Max.y) + { + BulletText("NavLastIds[%d]: 0x%08X", layer, window->NavLastIds[layer]); + continue; + } + BulletText("NavLastIds[%d]: 0x%08X at +(%.1f,%.1f)(%.1f,%.1f)", layer, window->NavLastIds[layer], r.Min.x, r.Min.y, r.Max.x, r.Max.y); + if (IsItemHovered()) + GetForegroundDrawList(window)->AddRect(r.Min + window->Pos, r.Max + window->Pos, IM_COL32(255, 255, 0, 255)); + } + BulletText("NavLayersActiveMask: %X, NavLastChildNavWindow: %s", window->DC.NavLayersActiveMask, window->NavLastChildNavWindow ? window->NavLastChildNavWindow->Name : "NULL"); + if (window->RootWindow != window) { DebugNodeWindow(window->RootWindow, "RootWindow"); } + if (window->ParentWindow != NULL) { DebugNodeWindow(window->ParentWindow, "ParentWindow"); } + if (window->DC.ChildWindows.Size > 0) { DebugNodeWindowsList(&window->DC.ChildWindows, "ChildWindows"); } + if (window->ColumnsStorage.Size > 0 && TreeNode("Columns", "Columns sets (%d)", window->ColumnsStorage.Size)) + { + for (int n = 0; n < window->ColumnsStorage.Size; n++) + DebugNodeColumns(&window->ColumnsStorage[n]); + TreePop(); + } + DebugNodeStorage(&window->StateStorage, "Storage"); + TreePop(); +} + +void ImGui::DebugNodeWindowSettings(ImGuiWindowSettings* settings) +{ + Text("0x%08X \"%s\" Pos (%d,%d) Size (%d,%d) Collapsed=%d", + settings->ID, settings->GetName(), settings->Pos.x, settings->Pos.y, settings->Size.x, settings->Size.y, settings->Collapsed); +} + +void ImGui::DebugNodeWindowsList(ImVector* windows, const char* label) +{ + if (!TreeNode(label, "%s (%d)", label, windows->Size)) + return; + for (int i = windows->Size - 1; i >= 0; i--) // Iterate front to back + { + PushID((*windows)[i]); + DebugNodeWindow((*windows)[i], "Window"); + PopID(); + } + TreePop(); +} + +// FIXME-OPT: This is technically suboptimal, but it is simpler this way. +void ImGui::DebugNodeWindowsListByBeginStackParent(ImGuiWindow** windows, int windows_size, ImGuiWindow* parent_in_begin_stack) +{ + for (int i = 0; i < windows_size; i++) + { + ImGuiWindow* window = windows[i]; + if (window->ParentWindowInBeginStack != parent_in_begin_stack) + continue; + char buf[20]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "[%04d] Window", window->BeginOrderWithinContext); + //BulletText("[%04d] Window '%s'", window->BeginOrderWithinContext, window->Name); + DebugNodeWindow(window, buf); + Indent(); + DebugNodeWindowsListByBeginStackParent(windows + i + 1, windows_size - i - 1, window); + Unindent(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, STACK TOOL) +//----------------------------------------------------------------------------- + +// [DEBUG] Item picker tool - start with DebugStartItemPicker() - useful to visually select an item and break into its call-stack. +void ImGui::UpdateDebugToolItemPicker() +{ + ImGuiContext& g = *GImGui; + g.DebugItemPickerBreakId = 0; + if (!g.DebugItemPickerActive) + return; + + const ImGuiID hovered_id = g.HoveredIdPreviousFrame; + SetMouseCursor(ImGuiMouseCursor_Hand); + if (IsKeyPressed(ImGuiKey_Escape)) + g.DebugItemPickerActive = false; + if (IsMouseClicked(0) && hovered_id) + { + g.DebugItemPickerBreakId = hovered_id; + g.DebugItemPickerActive = false; + } + SetNextWindowBgAlpha(0.60f); + BeginTooltip(); + Text("HoveredId: 0x%08X", hovered_id); + Text("Press ESC to abort picking."); + TextColored(GetStyleColorVec4(hovered_id ? ImGuiCol_Text : ImGuiCol_TextDisabled), "Click to break in debugger!"); + EndTooltip(); +} + +// [DEBUG] Stack Tool: update queries. Called by NewFrame() +void ImGui::UpdateDebugToolStackQueries() +{ + ImGuiContext& g = *GImGui; + ImGuiStackTool* tool = &g.DebugStackTool; + + // Clear hook when stack tool is not visible + g.DebugHookIdInfo = 0; + if (g.FrameCount != tool->LastActiveFrame + 1) + return; + + // Update queries. The steps are: -1: query Stack, >= 0: query each stack item + // We can only perform 1 ID Info query every frame. This is designed so the GetID() tests are cheap and constant-time + const ImGuiID query_id = g.HoveredIdPreviousFrame ? g.HoveredIdPreviousFrame : g.ActiveId; + if (tool->QueryId != query_id) + { + tool->QueryId = query_id; + tool->StackLevel = -1; + tool->Results.resize(0); + } + if (query_id == 0) + return; + + // Advance to next stack level when we got our result, or after 2 frames (in case we never get a result) + int stack_level = tool->StackLevel; + if (stack_level >= 0 && stack_level < tool->Results.Size) + if (tool->Results[stack_level].QuerySuccess || tool->Results[stack_level].QueryFrameCount > 2) + tool->StackLevel++; + + // Update hook + stack_level = tool->StackLevel; + if (stack_level == -1) + g.DebugHookIdInfo = query_id; + if (stack_level >= 0 && stack_level < tool->Results.Size) + { + g.DebugHookIdInfo = tool->Results[stack_level].ID; + tool->Results[stack_level].QueryFrameCount++; + } +} + +// [DEBUG] Stack tool: hooks called by GetID() family functions +void ImGui::DebugHookIdInfo(ImGuiID id, ImGuiDataType data_type, const void* data_id, const void* data_id_end) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStackTool* tool = &g.DebugStackTool; + + // Step 0: stack query + // This assume that the ID was computed with the current ID stack, which tends to be the case for our widget. + if (tool->StackLevel == -1) + { + tool->StackLevel++; + tool->Results.resize(window->IDStack.Size + 1, ImGuiStackLevelInfo()); + for (int n = 0; n < window->IDStack.Size + 1; n++) + tool->Results[n].ID = (n < window->IDStack.Size) ? window->IDStack[n] : id; + return; + } + + // Step 1+: query for individual level + IM_ASSERT(tool->StackLevel >= 0); + if (tool->StackLevel != window->IDStack.Size) + return; + ImGuiStackLevelInfo* info = &tool->Results[tool->StackLevel]; + IM_ASSERT(info->ID == id && info->QueryFrameCount > 0); + + int data_len; + switch (data_type) + { + case ImGuiDataType_S32: + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "%d", (int)(intptr_t)data_id); + break; + case ImGuiDataType_String: + data_len = data_id_end ? (int)((const char*)data_id_end - (const char*)data_id) : (int)strlen((const char*)data_id); + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "\"%.*s\"", data_len, (const char*)data_id); + break; + case ImGuiDataType_Pointer: + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "(void*)0x%p", data_id); + break; + case ImGuiDataType_ID: + if (info->Desc[0] == 0) // PushOverrideID() is often used to avoid hashing twice, which would lead to 2 calls to DebugHookIdInfo(). We prioritize the first one. + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "0x%08X [override]", id); + break; + default: + IM_ASSERT(0); + } + info->QuerySuccess = true; +} + +// Stack Tool: Display UI +void ImGui::ShowStackToolWindow(bool* p_open) +{ + ImGuiContext& g = *GImGui; + if (!(g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize)) + SetNextWindowSize(ImVec2(0.0f, GetFontSize() * 8.0f), ImGuiCond_FirstUseEver); + if (!Begin("Dear ImGui Stack Tool", p_open) || GetCurrentWindow()->BeginCount > 1) + { + End(); + return; + } + + // Display hovered/active status + const ImGuiID hovered_id = g.HoveredIdPreviousFrame; + const ImGuiID active_id = g.ActiveId; +#ifdef IMGUI_ENABLE_TEST_ENGINE + Text("HoveredId: 0x%08X (\"%s\"), ActiveId: 0x%08X (\"%s\")", hovered_id, hovered_id ? ImGuiTestEngine_FindItemDebugLabel(&g, hovered_id) : "", active_id, active_id ? ImGuiTestEngine_FindItemDebugLabel(&g, active_id) : ""); +#else + Text("HoveredId: 0x%08X, ActiveId: 0x%08X", hovered_id, active_id); +#endif + SameLine(); + MetricsHelpMarker("Hover an item with the mouse to display elements of the ID Stack leading to the item's final ID.\nEach level of the stack correspond to a PushID() call.\nAll levels of the stack are hashed together to make the final ID of a widget (ID displayed at the bottom level of the stack).\nRead FAQ entry about the ID stack for details."); + + // Display decorated stack + ImGuiStackTool* tool = &g.DebugStackTool; + tool->LastActiveFrame = g.FrameCount; + if (tool->Results.Size > 0 && BeginTable("##table", 3, ImGuiTableFlags_Borders)) + { + const float id_width = CalcTextSize("0xDDDDDDDD").x; + TableSetupColumn("Seed", ImGuiTableColumnFlags_WidthFixed, id_width); + TableSetupColumn("PushID", ImGuiTableColumnFlags_WidthStretch); + TableSetupColumn("Result", ImGuiTableColumnFlags_WidthFixed, id_width); + TableHeadersRow(); + for (int n = 0; n < tool->Results.Size; n++) + { + ImGuiStackLevelInfo* info = &tool->Results[n]; + TableNextColumn(); + Text("0x%08X", (n > 0) ? tool->Results[n - 1].ID : 0); + + TableNextColumn(); + ImGuiWindow* window = (info->Desc[0] == 0 && n == 0) ? FindWindowByID(info->ID) : NULL; + if (window) // Source: window name (because the root ID don't call GetID() and so doesn't get hooked) + Text("\"%s\" [window]", window->Name); + else if (info->QuerySuccess) // Source: GetID() hooks (prioritize over ItemInfo() because we frequently use patterns like: PushID(str), Button("") where they both have same id) + TextUnformatted(info->Desc); + else if (tool->StackLevel >= tool->Results.Size) // Only start using fallback below when all queries are done, so during queries we don't flickering ??? markers. + { +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (const char* label = ImGuiTestEngine_FindItemDebugLabel(&g, info->ID)) // Source: ImGuiTestEngine's ItemInfo() + Text("??? \"%s\"", label); + else +#endif + TextUnformatted("???"); + } + + TableNextColumn(); + Text("0x%08X", info->ID); + if (n == tool->Results.Size - 1) + TableSetBgColor(ImGuiTableBgTarget_CellBg, GetColorU32(ImGuiCol_Header)); + } + EndTable(); + } + End(); +} + +#else + +void ImGui::ShowMetricsWindow(bool*) {} +void ImGui::ShowFontAtlas(ImFontAtlas*) {} +void ImGui::DebugNodeColumns(ImGuiOldColumns*) {} +void ImGui::DebugNodeDrawList(ImGuiWindow*, const ImDrawList*, const char*) {} +void ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList*, const ImDrawList*, const ImDrawCmd*, bool, bool) {} +void ImGui::DebugNodeFont(ImFont*) {} +void ImGui::DebugNodeStorage(ImGuiStorage*, const char*) {} +void ImGui::DebugNodeTabBar(ImGuiTabBar*, const char*) {} +void ImGui::DebugNodeWindow(ImGuiWindow*, const char*) {} +void ImGui::DebugNodeWindowSettings(ImGuiWindowSettings*) {} +void ImGui::DebugNodeWindowsList(ImVector*, const char*) {} +void ImGui::DebugNodeViewport(ImGuiViewportP*) {} + +void ImGui::ShowStackToolWindow(bool*) {} +void ImGui::DebugHookIdInfo(ImGuiID, ImGuiDataType, const void*, const void*) {} +void ImGui::UpdateDebugToolItemPicker() {} +void ImGui::UpdateDebugToolStackQueries() {} + +#endif // #ifndef IMGUI_DISABLE_METRICS_WINDOW + +//----------------------------------------------------------------------------- + +// Include imgui_user.inl at the end of imgui.cpp to access private data/functions that aren't exposed. +// Prefer just including imgui_internal.h from your code rather than using this define. If a declaration is missing from imgui_internal.h add it or request it on the github. +#ifdef IMGUI_INCLUDE_IMGUI_USER_INL +#include "imgui_user.inl" +#endif + +//----------------------------------------------------------------------------- + +#endif // #ifndef IMGUI_DISABLE + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Private/imgui_demo.cpp`: + +```cpp +// dear imgui, v1.87 +// (demo code) + +// Help: +// - Read FAQ at http://dearimgui.org/faq +// - Newcomers, read 'Programmer guide' in imgui.cpp for notes on how to setup Dear ImGui in your codebase. +// - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. +// Read imgui.cpp for more details, documentation and comments. +// Get the latest version at https://github.com/ocornut/imgui + +// Message to the person tempted to delete this file when integrating Dear ImGui into their codebase: +// Do NOT remove this file from your project! Think again! It is the most useful reference code that you and other +// coders will want to refer to and call. Have the ImGui::ShowDemoWindow() function wired in an always-available +// debug menu of your game/app! Removing this file from your project is hindering access to documentation for everyone +// in your team, likely leading you to poorer usage of the library. +// Everything in this file will be stripped out by the linker if you don't call ImGui::ShowDemoWindow(). +// If you want to link core Dear ImGui in your shipped builds but want a thorough guarantee that the demo will not be +// linked, you can setup your imconfig.h with #define IMGUI_DISABLE_DEMO_WINDOWS and those functions will be empty. +// In another situation, whenever you have Dear ImGui available you probably want this to be available for reference. +// Thank you, +// -Your beloved friend, imgui_demo.cpp (which you won't delete) + +// Message to beginner C/C++ programmers about the meaning of the 'static' keyword: +// In this demo code, we frequently use 'static' variables inside functions. A static variable persists across calls, +// so it is essentially like a global variable but declared inside the scope of the function. We do this as a way to +// gather code and data in the same place, to make the demo source code faster to read, faster to write, and smaller +// in size. It also happens to be a convenient way of storing simple UI related information as long as your function +// doesn't need to be reentrant or used in multiple threads. This might be a pattern you will want to use in your code, +// but most of the real data you would be editing is likely going to be stored outside your functions. + +// The Demo code in this file is designed to be easy to copy-and-paste into your application! +// Because of this: +// - We never omit the ImGui:: prefix when calling functions, even though most code here is in the same namespace. +// - We try to declare static variables in the local scope, as close as possible to the code using them. +// - We never use any of the helpers/facilities used internally by Dear ImGui, unless available in the public API. +// - We never use maths operators on ImVec2/ImVec4. For our other sources files we use them, and they are provided +// by imgui_internal.h using the IMGUI_DEFINE_MATH_OPERATORS define. For your own sources file they are optional +// and require you either enable those, either provide your own via IM_VEC2_CLASS_EXTRA in imconfig.h. +// Because we can't assume anything about your support of maths operators, we cannot use them in imgui_demo.cpp. + +// Navigating this file: +// - In Visual Studio IDE: CTRL+comma ("Edit.NavigateTo") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. + +/* + +Index of this file: + +// [SECTION] Forward Declarations, Helpers +// [SECTION] Demo Window / ShowDemoWindow() +// - sub section: ShowDemoWindowWidgets() +// - sub section: ShowDemoWindowLayout() +// - sub section: ShowDemoWindowPopups() +// - sub section: ShowDemoWindowTables() +// - sub section: ShowDemoWindowMisc() +// [SECTION] About Window / ShowAboutWindow() +// [SECTION] Style Editor / ShowStyleEditor() +// [SECTION] Example App: Main Menu Bar / ShowExampleAppMainMenuBar() +// [SECTION] Example App: Debug Console / ShowExampleAppConsole() +// [SECTION] Example App: Debug Log / ShowExampleAppLog() +// [SECTION] Example App: Simple Layout / ShowExampleAppLayout() +// [SECTION] Example App: Property Editor / ShowExampleAppPropertyEditor() +// [SECTION] Example App: Long Text / ShowExampleAppLongText() +// [SECTION] Example App: Auto Resize / ShowExampleAppAutoResize() +// [SECTION] Example App: Constrained Resize / ShowExampleAppConstrainedResize() +// [SECTION] Example App: Simple overlay / ShowExampleAppSimpleOverlay() +// [SECTION] Example App: Fullscreen window / ShowExampleAppFullscreen() +// [SECTION] Example App: Manipulating window titles / ShowExampleAppWindowTitles() +// [SECTION] Example App: Custom Rendering using ImDrawList API / ShowExampleAppCustomRendering() +// [SECTION] Example App: Documents Handling / ShowExampleAppDocuments() + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE + +// System includes +#include // toupper +#include // INT_MIN, INT_MAX +#include // sqrtf, powf, cosf, sinf, floorf, ceilf +#include // vsnprintf, sscanf, printf +#include // NULL, malloc, free, atoi +#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier +#include // intptr_t +#else +#include // intptr_t +#endif + +// Visual Studio warnings +#ifdef _MSC_VER +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wdeprecated-declarations" // warning: 'xx' is deprecated: The POSIX name for this.. // for strdup used in demo code (so user can copy & paste the code) +#pragma clang diagnostic ignored "-Wint-to-void-pointer-cast" // warning: cast to 'void *' from smaller integer type +#pragma clang diagnostic ignored "-Wformat-security" // warning: format string is not a string literal +#pragma clang diagnostic ignored "-Wexit-time-destructors" // warning: declaration requires an exit-time destructor // exit-time destruction order is undefined. if MemFree() leads to users code that has been disabled before exit it might cause problems. ImGui coding style welcomes static/globals. +#pragma clang diagnostic ignored "-Wunused-macros" // warning: macro is not used // we define snprintf/vsnprintf on Windows so they are available, but not always used. +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wreserved-id-macro" // warning: macro name is a reserved identifier +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#elif defined(__GNUC__) +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wint-to-pointer-cast" // warning: cast to pointer from integer of different size +#pragma GCC diagnostic ignored "-Wformat-security" // warning: format string is not a string literal (potentially insecure) +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value +#pragma GCC diagnostic ignored "-Wmisleading-indentation" // [__GNUC__ >= 6] warning: this 'if' clause does not guard this statement // GCC 6.0+ only. See #883 on GitHub. +#endif + +// Play it nice with Windows users (Update: May 2018, Notepad now supports Unix-style carriage returns!) +#ifdef _WIN32 +#define IM_NEWLINE "\r\n" +#else +#define IM_NEWLINE "\n" +#endif + +// Helpers +#if defined(_MSC_VER) && !defined(snprintf) +#define snprintf _snprintf +#endif +#if defined(_MSC_VER) && !defined(vsnprintf) +#define vsnprintf _vsnprintf +#endif + +// Format specifiers, printing 64-bit hasn't been decently standardized... +// In a real application you should be using PRId64 and PRIu64 from (non-windows) and on Windows define them yourself. +#ifdef _MSC_VER +#define IM_PRId64 "I64d" +#define IM_PRIu64 "I64u" +#else +#define IM_PRId64 "lld" +#define IM_PRIu64 "llu" +#endif + +// Helpers macros +// We normally try to not use many helpers in imgui_demo.cpp in order to make code easier to copy and paste, +// but making an exception here as those are largely simplifying code... +// In other imgui sources we can use nicer internal functions from imgui_internal.h (ImMin/ImMax) but not in the demo. +#define IM_MIN(A, B) (((A) < (B)) ? (A) : (B)) +#define IM_MAX(A, B) (((A) >= (B)) ? (A) : (B)) +#define IM_CLAMP(V, MN, MX) ((V) < (MN) ? (MN) : (V) > (MX) ? (MX) : (V)) + +// Enforce cdecl calling convention for functions called by the standard library, in case compilation settings changed the default to e.g. __vectorcall +#ifndef IMGUI_CDECL +#ifdef _MSC_VER +#define IMGUI_CDECL __cdecl +#else +#define IMGUI_CDECL +#endif +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Forward Declarations, Helpers +//----------------------------------------------------------------------------- + +#if !defined(IMGUI_DISABLE_DEMO_WINDOWS) + +// Forward Declarations +static void ShowExampleAppDocuments(bool* p_open); +static void ShowExampleAppMainMenuBar(); +static void ShowExampleAppConsole(bool* p_open); +static void ShowExampleAppLog(bool* p_open); +static void ShowExampleAppLayout(bool* p_open); +static void ShowExampleAppPropertyEditor(bool* p_open); +static void ShowExampleAppLongText(bool* p_open); +static void ShowExampleAppAutoResize(bool* p_open); +static void ShowExampleAppConstrainedResize(bool* p_open); +static void ShowExampleAppSimpleOverlay(bool* p_open); +static void ShowExampleAppFullscreen(bool* p_open); +static void ShowExampleAppWindowTitles(bool* p_open); +static void ShowExampleAppCustomRendering(bool* p_open); +static void ShowExampleMenuFile(); + +// Helper to display a little (?) mark which shows a tooltip when hovered. +// In your own code you may want to display an actual icon if you are using a merged icon fonts (see docs/FONTS.md) +static void HelpMarker(const char* desc) +{ + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(desc); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +// Helper to wire demo markers located in code to a interactive browser +typedef void (*ImGuiDemoMarkerCallback)(const char* file, int line, const char* section, void* user_data); +extern ImGuiDemoMarkerCallback GImGuiDemoMarkerCallback; +extern void* GImGuiDemoMarkerCallbackUserData; +ImGuiDemoMarkerCallback GImGuiDemoMarkerCallback = NULL; +void* GImGuiDemoMarkerCallbackUserData = NULL; +#define IMGUI_DEMO_MARKER(section) do { if (GImGuiDemoMarkerCallback != NULL) GImGuiDemoMarkerCallback(__FILE__, __LINE__, section, GImGuiDemoMarkerCallbackUserData); } while (0) + +// Helper to display basic user controls. +void ImGui::ShowUserGuide() +{ + ImGuiIO& io = ImGui::GetIO(); + ImGui::BulletText("Double-click on title bar to collapse window."); + ImGui::BulletText( + "Click and drag on lower corner to resize window\n" + "(double-click to auto fit window to its contents)."); + ImGui::BulletText("CTRL+Click on a slider or drag box to input value as text."); + ImGui::BulletText("TAB/SHIFT+TAB to cycle through keyboard editable fields."); + ImGui::BulletText("CTRL+Tab to select a window."); + if (io.FontAllowUserScaling) + ImGui::BulletText("CTRL+Mouse Wheel to zoom window contents."); + ImGui::BulletText("While inputing text:\n"); + ImGui::Indent(); + ImGui::BulletText("CTRL+Left/Right to word jump."); + ImGui::BulletText("CTRL+A or double-click to select all."); + ImGui::BulletText("CTRL+X/C/V to use clipboard cut/copy/paste."); + ImGui::BulletText("CTRL+Z,CTRL+Y to undo/redo."); + ImGui::BulletText("ESCAPE to revert."); + ImGui::Unindent(); + ImGui::BulletText("With keyboard navigation enabled:"); + ImGui::Indent(); + ImGui::BulletText("Arrow keys to navigate."); + ImGui::BulletText("Space to activate a widget."); + ImGui::BulletText("Return to input text into a widget."); + ImGui::BulletText("Escape to deactivate a widget, close popup, exit child window."); + ImGui::BulletText("Alt to jump to the menu layer of a window."); + ImGui::Unindent(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Demo Window / ShowDemoWindow() +//----------------------------------------------------------------------------- +// - ShowDemoWindowWidgets() +// - ShowDemoWindowLayout() +// - ShowDemoWindowPopups() +// - ShowDemoWindowTables() +// - ShowDemoWindowColumns() +// - ShowDemoWindowMisc() +//----------------------------------------------------------------------------- + +// We split the contents of the big ShowDemoWindow() function into smaller functions +// (because the link time of very large functions grow non-linearly) +static void ShowDemoWindowWidgets(); +static void ShowDemoWindowLayout(); +static void ShowDemoWindowPopups(); +static void ShowDemoWindowTables(); +static void ShowDemoWindowColumns(); +static void ShowDemoWindowMisc(); + +// Demonstrate most Dear ImGui features (this is big function!) +// You may execute this function to experiment with the UI and understand what it does. +// You may then search for keywords in the code when you are interested by a specific feature. +void ImGui::ShowDemoWindow(bool* p_open) +{ + // Exceptionally add an extra assert here for people confused about initial Dear ImGui setup + // Most ImGui functions would normally just crash if the context is missing. + IM_ASSERT(ImGui::GetCurrentContext() != NULL && "Missing dear imgui context. Refer to examples app!"); + + // Examples Apps (accessible from the "Examples" menu) + static bool show_app_main_menu_bar = false; + static bool show_app_documents = false; + + static bool show_app_console = false; + static bool show_app_log = false; + static bool show_app_layout = false; + static bool show_app_property_editor = false; + static bool show_app_long_text = false; + static bool show_app_auto_resize = false; + static bool show_app_constrained_resize = false; + static bool show_app_simple_overlay = false; + static bool show_app_fullscreen = false; + static bool show_app_window_titles = false; + static bool show_app_custom_rendering = false; + + if (show_app_main_menu_bar) ShowExampleAppMainMenuBar(); + if (show_app_documents) ShowExampleAppDocuments(&show_app_documents); + + if (show_app_console) ShowExampleAppConsole(&show_app_console); + if (show_app_log) ShowExampleAppLog(&show_app_log); + if (show_app_layout) ShowExampleAppLayout(&show_app_layout); + if (show_app_property_editor) ShowExampleAppPropertyEditor(&show_app_property_editor); + if (show_app_long_text) ShowExampleAppLongText(&show_app_long_text); + if (show_app_auto_resize) ShowExampleAppAutoResize(&show_app_auto_resize); + if (show_app_constrained_resize) ShowExampleAppConstrainedResize(&show_app_constrained_resize); + if (show_app_simple_overlay) ShowExampleAppSimpleOverlay(&show_app_simple_overlay); + if (show_app_fullscreen) ShowExampleAppFullscreen(&show_app_fullscreen); + if (show_app_window_titles) ShowExampleAppWindowTitles(&show_app_window_titles); + if (show_app_custom_rendering) ShowExampleAppCustomRendering(&show_app_custom_rendering); + + // Dear ImGui Apps (accessible from the "Tools" menu) + static bool show_app_metrics = false; + static bool show_app_stack_tool = false; + static bool show_app_style_editor = false; + static bool show_app_about = false; + + if (show_app_metrics) { ImGui::ShowMetricsWindow(&show_app_metrics); } + if (show_app_stack_tool) { ImGui::ShowStackToolWindow(&show_app_stack_tool); } + if (show_app_about) { ImGui::ShowAboutWindow(&show_app_about); } + if (show_app_style_editor) + { + ImGui::Begin("Dear ImGui Style Editor", &show_app_style_editor); + ImGui::ShowStyleEditor(); + ImGui::End(); + } + + // Demonstrate the various window flags. Typically you would just use the default! + static bool no_titlebar = false; + static bool no_scrollbar = false; + static bool no_menu = false; + static bool no_move = false; + static bool no_resize = false; + static bool no_collapse = false; + static bool no_close = false; + static bool no_nav = false; + static bool no_background = false; + static bool no_bring_to_front = false; + static bool unsaved_document = false; + + ImGuiWindowFlags window_flags = 0; + if (no_titlebar) window_flags |= ImGuiWindowFlags_NoTitleBar; + if (no_scrollbar) window_flags |= ImGuiWindowFlags_NoScrollbar; + if (!no_menu) window_flags |= ImGuiWindowFlags_MenuBar; + if (no_move) window_flags |= ImGuiWindowFlags_NoMove; + if (no_resize) window_flags |= ImGuiWindowFlags_NoResize; + if (no_collapse) window_flags |= ImGuiWindowFlags_NoCollapse; + if (no_nav) window_flags |= ImGuiWindowFlags_NoNav; + if (no_background) window_flags |= ImGuiWindowFlags_NoBackground; + if (no_bring_to_front) window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus; + if (unsaved_document) window_flags |= ImGuiWindowFlags_UnsavedDocument; + if (no_close) p_open = NULL; // Don't pass our bool* to Begin + + // We specify a default position/size in case there's no data in the .ini file. + // We only do it to make the demo applications a little more welcoming, but typically this isn't required. + const ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(ImVec2(main_viewport->WorkPos.x + 650, main_viewport->WorkPos.y + 20), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(550, 680), ImGuiCond_FirstUseEver); + + // Main body of the Demo window starts here. + if (!ImGui::Begin("Dear ImGui Demo", p_open, window_flags)) + { + // Early out if the window is collapsed, as an optimization. + ImGui::End(); + return; + } + + // Most "big" widgets share a common width settings by default. See 'Demo->Layout->Widgets Width' for details. + + // e.g. Use 2/3 of the space for widgets and 1/3 for labels (right align) + //ImGui::PushItemWidth(-ImGui::GetWindowWidth() * 0.35f); + + // e.g. Leave a fixed amount of width for labels (by passing a negative value), the rest goes to widgets. + ImGui::PushItemWidth(ImGui::GetFontSize() * -12); + + // Menu Bar + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Menu")) + { + IMGUI_DEMO_MARKER("Menu/File"); + ShowExampleMenuFile(); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Examples")) + { + IMGUI_DEMO_MARKER("Menu/Examples"); + ImGui::MenuItem("Main menu bar", NULL, &show_app_main_menu_bar); + ImGui::MenuItem("Console", NULL, &show_app_console); + ImGui::MenuItem("Log", NULL, &show_app_log); + ImGui::MenuItem("Simple layout", NULL, &show_app_layout); + ImGui::MenuItem("Property editor", NULL, &show_app_property_editor); + ImGui::MenuItem("Long text display", NULL, &show_app_long_text); + ImGui::MenuItem("Auto-resizing window", NULL, &show_app_auto_resize); + ImGui::MenuItem("Constrained-resizing window", NULL, &show_app_constrained_resize); + ImGui::MenuItem("Simple overlay", NULL, &show_app_simple_overlay); + ImGui::MenuItem("Fullscreen window", NULL, &show_app_fullscreen); + ImGui::MenuItem("Manipulating window titles", NULL, &show_app_window_titles); + ImGui::MenuItem("Custom rendering", NULL, &show_app_custom_rendering); + ImGui::MenuItem("Documents", NULL, &show_app_documents); + ImGui::EndMenu(); + } + //if (ImGui::MenuItem("MenuItem")) {} // You can also use MenuItem() inside a menu bar! + if (ImGui::BeginMenu("Tools")) + { + IMGUI_DEMO_MARKER("Menu/Tools"); +#ifndef IMGUI_DISABLE_METRICS_WINDOW + ImGui::MenuItem("Metrics/Debugger", NULL, &show_app_metrics); + ImGui::MenuItem("Stack Tool", NULL, &show_app_stack_tool); +#endif + ImGui::MenuItem("Style Editor", NULL, &show_app_style_editor); + ImGui::MenuItem("About Dear ImGui", NULL, &show_app_about); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + ImGui::Text("dear imgui says hello. (%s)", IMGUI_VERSION); + ImGui::Spacing(); + + IMGUI_DEMO_MARKER("Help"); + if (ImGui::CollapsingHeader("Help")) + { + ImGui::Text("ABOUT THIS DEMO:"); + ImGui::BulletText("Sections below are demonstrating many aspects of the library."); + ImGui::BulletText("The \"Examples\" menu above leads to more demo contents."); + ImGui::BulletText("The \"Tools\" menu above gives access to: About Box, Style Editor,\n" + "and Metrics/Debugger (general purpose Dear ImGui debugging tool)."); + ImGui::Separator(); + + ImGui::Text("PROGRAMMER GUIDE:"); + ImGui::BulletText("See the ShowDemoWindow() code in imgui_demo.cpp. <- you are here!"); + ImGui::BulletText("See comments in imgui.cpp."); + ImGui::BulletText("See example applications in the examples/ folder."); + ImGui::BulletText("Read the FAQ at http://www.dearimgui.org/faq/"); + ImGui::BulletText("Set 'io.ConfigFlags |= NavEnableKeyboard' for keyboard controls."); + ImGui::BulletText("Set 'io.ConfigFlags |= NavEnableGamepad' for gamepad controls."); + ImGui::Separator(); + + ImGui::Text("USER GUIDE:"); + ImGui::ShowUserGuide(); + } + + IMGUI_DEMO_MARKER("Configuration"); + if (ImGui::CollapsingHeader("Configuration")) + { + ImGuiIO& io = ImGui::GetIO(); + + if (ImGui::TreeNode("Configuration##2")) + { + ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", &io.ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard); + ImGui::SameLine(); HelpMarker("Enable keyboard controls."); + ImGui::CheckboxFlags("io.ConfigFlags: NavEnableGamepad", &io.ConfigFlags, ImGuiConfigFlags_NavEnableGamepad); + ImGui::SameLine(); HelpMarker("Enable gamepad controls. Require backend to set io.BackendFlags |= ImGuiBackendFlags_HasGamepad.\n\nRead instructions in imgui.cpp for details."); + ImGui::CheckboxFlags("io.ConfigFlags: NavEnableSetMousePos", &io.ConfigFlags, ImGuiConfigFlags_NavEnableSetMousePos); + ImGui::SameLine(); HelpMarker("Instruct navigation to move the mouse cursor. See comment for ImGuiConfigFlags_NavEnableSetMousePos."); + ImGui::CheckboxFlags("io.ConfigFlags: NoMouse", &io.ConfigFlags, ImGuiConfigFlags_NoMouse); + if (io.ConfigFlags & ImGuiConfigFlags_NoMouse) + { + // The "NoMouse" option can get us stuck with a disabled mouse! Let's provide an alternative way to fix it: + if (fmodf((float)ImGui::GetTime(), 0.40f) < 0.20f) + { + ImGui::SameLine(); + ImGui::Text("<>"); + } + if (ImGui::IsKeyPressed(ImGuiKey_Space)) + io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; + } + ImGui::CheckboxFlags("io.ConfigFlags: NoMouseCursorChange", &io.ConfigFlags, ImGuiConfigFlags_NoMouseCursorChange); + ImGui::SameLine(); HelpMarker("Instruct backend to not alter mouse cursor shape and visibility."); + ImGui::Checkbox("io.ConfigInputTrickleEventQueue", &io.ConfigInputTrickleEventQueue); + ImGui::SameLine(); HelpMarker("Enable input queue trickling: some types of events submitted during the same frame (e.g. button down + up) will be spread over multiple frames, improving interactions with low framerates."); + ImGui::Checkbox("io.ConfigInputTextCursorBlink", &io.ConfigInputTextCursorBlink); + ImGui::SameLine(); HelpMarker("Enable blinking cursor (optional as some users consider it to be distracting)."); + ImGui::Checkbox("io.ConfigDragClickToInputText", &io.ConfigDragClickToInputText); + ImGui::SameLine(); HelpMarker("Enable turning DragXXX widgets into text input with a simple mouse click-release (without moving)."); + ImGui::Checkbox("io.ConfigWindowsResizeFromEdges", &io.ConfigWindowsResizeFromEdges); + ImGui::SameLine(); HelpMarker("Enable resizing of windows from their edges and from the lower-left corner.\nThis requires (io.BackendFlags & ImGuiBackendFlags_HasMouseCursors) because it needs mouse cursor feedback."); + ImGui::Checkbox("io.ConfigWindowsMoveFromTitleBarOnly", &io.ConfigWindowsMoveFromTitleBarOnly); + ImGui::Checkbox("io.MouseDrawCursor", &io.MouseDrawCursor); + ImGui::SameLine(); HelpMarker("Instruct Dear ImGui to render a mouse cursor itself. Note that a mouse cursor rendered via your application GPU rendering path will feel more laggy than hardware cursor, but will be more in sync with your other visuals.\n\nSome desktop applications may use both kinds of cursors (e.g. enable software cursor only when resizing/dragging something)."); + ImGui::Text("Also see Style->Rendering for rendering options."); + ImGui::TreePop(); + ImGui::Separator(); + } + + IMGUI_DEMO_MARKER("Configuration/Backend Flags"); + if (ImGui::TreeNode("Backend Flags")) + { + HelpMarker( + "Those flags are set by the backends (imgui_impl_xxx files) to specify their capabilities.\n" + "Here we expose them as read-only fields to avoid breaking interactions with your backend."); + + // Make a local copy to avoid modifying actual backend flags. + // FIXME: We don't use BeginDisabled() to keep label bright, maybe we need a BeginReadonly() equivalent.. + ImGuiBackendFlags backend_flags = io.BackendFlags; + ImGui::CheckboxFlags("io.BackendFlags: HasGamepad", &backend_flags, ImGuiBackendFlags_HasGamepad); + ImGui::CheckboxFlags("io.BackendFlags: HasMouseCursors", &backend_flags, ImGuiBackendFlags_HasMouseCursors); + ImGui::CheckboxFlags("io.BackendFlags: HasSetMousePos", &backend_flags, ImGuiBackendFlags_HasSetMousePos); + ImGui::CheckboxFlags("io.BackendFlags: RendererHasVtxOffset", &backend_flags, ImGuiBackendFlags_RendererHasVtxOffset); + ImGui::TreePop(); + ImGui::Separator(); + } + + IMGUI_DEMO_MARKER("Configuration/Style"); + if (ImGui::TreeNode("Style")) + { + HelpMarker("The same contents can be accessed in 'Tools->Style Editor' or by calling the ShowStyleEditor() function."); + ImGui::ShowStyleEditor(); + ImGui::TreePop(); + ImGui::Separator(); + } + + IMGUI_DEMO_MARKER("Configuration/Capture, Logging"); + if (ImGui::TreeNode("Capture/Logging")) + { + HelpMarker( + "The logging API redirects all text output so you can easily capture the content of " + "a window or a block. Tree nodes can be automatically expanded.\n" + "Try opening any of the contents below in this window and then click one of the \"Log To\" button."); + ImGui::LogButtons(); + + HelpMarker("You can also call ImGui::LogText() to output directly to the log without a visual output."); + if (ImGui::Button("Copy \"Hello, world!\" to clipboard")) + { + ImGui::LogToClipboard(); + ImGui::LogText("Hello, world!"); + ImGui::LogFinish(); + } + ImGui::TreePop(); + } + } + + IMGUI_DEMO_MARKER("Window options"); + if (ImGui::CollapsingHeader("Window options")) + { + if (ImGui::BeginTable("split", 3)) + { + ImGui::TableNextColumn(); ImGui::Checkbox("No titlebar", &no_titlebar); + ImGui::TableNextColumn(); ImGui::Checkbox("No scrollbar", &no_scrollbar); + ImGui::TableNextColumn(); ImGui::Checkbox("No menu", &no_menu); + ImGui::TableNextColumn(); ImGui::Checkbox("No move", &no_move); + ImGui::TableNextColumn(); ImGui::Checkbox("No resize", &no_resize); + ImGui::TableNextColumn(); ImGui::Checkbox("No collapse", &no_collapse); + ImGui::TableNextColumn(); ImGui::Checkbox("No close", &no_close); + ImGui::TableNextColumn(); ImGui::Checkbox("No nav", &no_nav); + ImGui::TableNextColumn(); ImGui::Checkbox("No background", &no_background); + ImGui::TableNextColumn(); ImGui::Checkbox("No bring to front", &no_bring_to_front); + ImGui::TableNextColumn(); ImGui::Checkbox("Unsaved document", &unsaved_document); + ImGui::EndTable(); + } + } + + // All demo contents + ShowDemoWindowWidgets(); + ShowDemoWindowLayout(); + ShowDemoWindowPopups(); + ShowDemoWindowTables(); + ShowDemoWindowMisc(); + + // End of ShowDemoWindow() + ImGui::PopItemWidth(); + ImGui::End(); +} + +static void ShowDemoWindowWidgets() +{ + IMGUI_DEMO_MARKER("Widgets"); + if (!ImGui::CollapsingHeader("Widgets")) + return; + + static bool disable_all = false; // The Checkbox for that is inside the "Disabled" section at the bottom + if (disable_all) + ImGui::BeginDisabled(); + + IMGUI_DEMO_MARKER("Widgets/Basic"); + if (ImGui::TreeNode("Basic")) + { + IMGUI_DEMO_MARKER("Widgets/Basic/Button"); + static int clicked = 0; + if (ImGui::Button("Button")) + clicked++; + if (clicked & 1) + { + ImGui::SameLine(); + ImGui::Text("Thanks for clicking me!"); + } + + IMGUI_DEMO_MARKER("Widgets/Basic/Checkbox"); + static bool check = true; + ImGui::Checkbox("checkbox", &check); + + IMGUI_DEMO_MARKER("Widgets/Basic/RadioButton"); + static int e = 0; + ImGui::RadioButton("radio a", &e, 0); ImGui::SameLine(); + ImGui::RadioButton("radio b", &e, 1); ImGui::SameLine(); + ImGui::RadioButton("radio c", &e, 2); + + // Color buttons, demonstrate using PushID() to add unique identifier in the ID stack, and changing style. + IMGUI_DEMO_MARKER("Widgets/Basic/Buttons (Colored)"); + for (int i = 0; i < 7; i++) + { + if (i > 0) + ImGui::SameLine(); + ImGui::PushID(i); + ImGui::PushStyleColor(ImGuiCol_Button, (ImVec4)ImColor::HSV(i / 7.0f, 0.6f, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, (ImVec4)ImColor::HSV(i / 7.0f, 0.7f, 0.7f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, (ImVec4)ImColor::HSV(i / 7.0f, 0.8f, 0.8f)); + ImGui::Button("Click"); + ImGui::PopStyleColor(3); + ImGui::PopID(); + } + + // Use AlignTextToFramePadding() to align text baseline to the baseline of framed widgets elements + // (otherwise a Text+SameLine+Button sequence will have the text a little too high by default!) + // See 'Demo->Layout->Text Baseline Alignment' for details. + ImGui::AlignTextToFramePadding(); + ImGui::Text("Hold to repeat:"); + ImGui::SameLine(); + + // Arrow buttons with Repeater + IMGUI_DEMO_MARKER("Widgets/Basic/Buttons (Repeating)"); + static int counter = 0; + float spacing = ImGui::GetStyle().ItemInnerSpacing.x; + ImGui::PushButtonRepeat(true); + if (ImGui::ArrowButton("##left", ImGuiDir_Left)) { counter--; } + ImGui::SameLine(0.0f, spacing); + if (ImGui::ArrowButton("##right", ImGuiDir_Right)) { counter++; } + ImGui::PopButtonRepeat(); + ImGui::SameLine(); + ImGui::Text("%d", counter); + + IMGUI_DEMO_MARKER("Widgets/Basic/Tooltips"); + ImGui::Text("Hover over me"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("I am a tooltip"); + + ImGui::SameLine(); + ImGui::Text("- or me"); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::Text("I am a fancy tooltip"); + static float arr[] = { 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f }; + ImGui::PlotLines("Curve", arr, IM_ARRAYSIZE(arr)); + ImGui::EndTooltip(); + } + + ImGui::Separator(); + ImGui::LabelText("label", "Value"); + + { + // Using the _simplified_ one-liner Combo() api here + // See "Combo" section for examples of how to use the more flexible BeginCombo()/EndCombo() api. + IMGUI_DEMO_MARKER("Widgets/Basic/Combo"); + const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIIIIII", "JJJJ", "KKKKKKK" }; + static int item_current = 0; + ImGui::Combo("combo", &item_current, items, IM_ARRAYSIZE(items)); + ImGui::SameLine(); HelpMarker( + "Using the simplified one-liner Combo API here.\nRefer to the \"Combo\" section below for an explanation of how to use the more flexible and general BeginCombo/EndCombo API."); + } + + { + // To wire InputText() with std::string or any other custom string type, + // see the "Text Input > Resize Callback" section of this demo, and the misc/cpp/imgui_stdlib.h file. + IMGUI_DEMO_MARKER("Widgets/Basic/InputText"); + static char str0[128] = "Hello, world!"; + ImGui::InputText("input text", str0, IM_ARRAYSIZE(str0)); + ImGui::SameLine(); HelpMarker( + "USER:\n" + "Hold SHIFT or use mouse to select text.\n" + "CTRL+Left/Right to word jump.\n" + "CTRL+A or double-click to select all.\n" + "CTRL+X,CTRL+C,CTRL+V clipboard.\n" + "CTRL+Z,CTRL+Y undo/redo.\n" + "ESCAPE to revert.\n\n" + "PROGRAMMER:\n" + "You can use the ImGuiInputTextFlags_CallbackResize facility if you need to wire InputText() " + "to a dynamic string type. See misc/cpp/imgui_stdlib.h for an example (this is not demonstrated " + "in imgui_demo.cpp)."); + + static char str1[128] = ""; + ImGui::InputTextWithHint("input text (w/ hint)", "enter text here", str1, IM_ARRAYSIZE(str1)); + + IMGUI_DEMO_MARKER("Widgets/Basic/InputInt, InputFloat"); + static int i0 = 123; + ImGui::InputInt("input int", &i0); + + static float f0 = 0.001f; + ImGui::InputFloat("input float", &f0, 0.01f, 1.0f, "%.3f"); + + static double d0 = 999999.00000001; + ImGui::InputDouble("input double", &d0, 0.01f, 1.0f, "%.8f"); + + static float f1 = 1.e10f; + ImGui::InputFloat("input scientific", &f1, 0.0f, 0.0f, "%e"); + ImGui::SameLine(); HelpMarker( + "You can input value using the scientific notation,\n" + " e.g. \"1e+8\" becomes \"100000000\"."); + + static float vec4a[4] = { 0.10f, 0.20f, 0.30f, 0.44f }; + ImGui::InputFloat3("input float3", vec4a); + } + + { + IMGUI_DEMO_MARKER("Widgets/Basic/DragInt, DragFloat"); + static int i1 = 50, i2 = 42; + ImGui::DragInt("drag int", &i1, 1); + ImGui::SameLine(); HelpMarker( + "Click and drag to edit value.\n" + "Hold SHIFT/ALT for faster/slower edit.\n" + "Double-click or CTRL+click to input value."); + + ImGui::DragInt("drag int 0..100", &i2, 1, 0, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp); + + static float f1 = 1.00f, f2 = 0.0067f; + ImGui::DragFloat("drag float", &f1, 0.005f); + ImGui::DragFloat("drag small float", &f2, 0.0001f, 0.0f, 0.0f, "%.06f ns"); + } + + { + IMGUI_DEMO_MARKER("Widgets/Basic/SliderInt, SliderFloat"); + static int i1 = 0; + ImGui::SliderInt("slider int", &i1, -1, 3); + ImGui::SameLine(); HelpMarker("CTRL+click to input value."); + + static float f1 = 0.123f, f2 = 0.0f; + ImGui::SliderFloat("slider float", &f1, 0.0f, 1.0f, "ratio = %.3f"); + ImGui::SliderFloat("slider float (log)", &f2, -10.0f, 10.0f, "%.4f", ImGuiSliderFlags_Logarithmic); + + IMGUI_DEMO_MARKER("Widgets/Basic/SliderAngle"); + static float angle = 0.0f; + ImGui::SliderAngle("slider angle", &angle); + + // Using the format string to display a name instead of an integer. + // Here we completely omit '%d' from the format string, so it'll only display a name. + // This technique can also be used with DragInt(). + IMGUI_DEMO_MARKER("Widgets/Basic/Slider (enum)"); + enum Element { Element_Fire, Element_Earth, Element_Air, Element_Water, Element_COUNT }; + static int elem = Element_Fire; + const char* elems_names[Element_COUNT] = { "Fire", "Earth", "Air", "Water" }; + const char* elem_name = (elem >= 0 && elem < Element_COUNT) ? elems_names[elem] : "Unknown"; + ImGui::SliderInt("slider enum", &elem, 0, Element_COUNT - 1, elem_name); + ImGui::SameLine(); HelpMarker("Using the format string parameter to display a name instead of the underlying integer."); + } + + { + IMGUI_DEMO_MARKER("Widgets/Basic/ColorEdit3, ColorEdit4"); + static float col1[3] = { 1.0f, 0.0f, 0.2f }; + static float col2[4] = { 0.4f, 0.7f, 0.0f, 0.5f }; + ImGui::ColorEdit3("color 1", col1); + ImGui::SameLine(); HelpMarker( + "Click on the color square to open a color picker.\n" + "Click and hold to use drag and drop.\n" + "Right-click on the color square to show options.\n" + "CTRL+click on individual component to input value.\n"); + + ImGui::ColorEdit4("color 2", col2); + } + + { + // Using the _simplified_ one-liner ListBox() api here + // See "List boxes" section for examples of how to use the more flexible BeginListBox()/EndListBox() api. + IMGUI_DEMO_MARKER("Widgets/Basic/ListBox"); + const char* items[] = { "Apple", "Banana", "Cherry", "Kiwi", "Mango", "Orange", "Pineapple", "Strawberry", "Watermelon" }; + static int item_current = 1; + ImGui::ListBox("listbox", &item_current, items, IM_ARRAYSIZE(items), 4); + ImGui::SameLine(); HelpMarker( + "Using the simplified one-liner ListBox API here.\nRefer to the \"List boxes\" section below for an explanation of how to use the more flexible and general BeginListBox/EndListBox API."); + } + + ImGui::TreePop(); + } + + // Testing ImGuiOnceUponAFrame helper. + //static ImGuiOnceUponAFrame once; + //for (int i = 0; i < 5; i++) + // if (once) + // ImGui::Text("This will be displayed only once."); + + IMGUI_DEMO_MARKER("Widgets/Trees"); + if (ImGui::TreeNode("Trees")) + { + IMGUI_DEMO_MARKER("Widgets/Trees/Basic trees"); + if (ImGui::TreeNode("Basic trees")) + { + for (int i = 0; i < 5; i++) + { + // Use SetNextItemOpen() so set the default state of a node to be open. We could + // also use TreeNodeEx() with the ImGuiTreeNodeFlags_DefaultOpen flag to achieve the same thing! + if (i == 0) + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + + if (ImGui::TreeNode((void*)(intptr_t)i, "Child %d", i)) + { + ImGui::Text("blah blah"); + ImGui::SameLine(); + if (ImGui::SmallButton("button")) {} + ImGui::TreePop(); + } + } + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Trees/Advanced, with Selectable nodes"); + if (ImGui::TreeNode("Advanced, with Selectable nodes")) + { + HelpMarker( + "This is a more typical looking tree with selectable nodes.\n" + "Click to select, CTRL+Click to toggle, click on arrows or double-click to open."); + static ImGuiTreeNodeFlags base_flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_SpanAvailWidth; + static bool align_label_with_current_x_position = false; + static bool test_drag_and_drop = false; + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_OpenOnArrow", &base_flags, ImGuiTreeNodeFlags_OpenOnArrow); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_OpenOnDoubleClick", &base_flags, ImGuiTreeNodeFlags_OpenOnDoubleClick); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAvailWidth", &base_flags, ImGuiTreeNodeFlags_SpanAvailWidth); ImGui::SameLine(); HelpMarker("Extend hit area to all available width instead of allowing more items to be laid out after the node."); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanFullWidth", &base_flags, ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::Checkbox("Align label with current X position", &align_label_with_current_x_position); + ImGui::Checkbox("Test tree node as drag source", &test_drag_and_drop); + ImGui::Text("Hello!"); + if (align_label_with_current_x_position) + ImGui::Unindent(ImGui::GetTreeNodeToLabelSpacing()); + + // 'selection_mask' is dumb representation of what may be user-side selection state. + // You may retain selection state inside or outside your objects in whatever format you see fit. + // 'node_clicked' is temporary storage of what node we have clicked to process selection at the end + /// of the loop. May be a pointer to your own node type, etc. + static int selection_mask = (1 << 2); + int node_clicked = -1; + for (int i = 0; i < 6; i++) + { + // Disable the default "open on single-click behavior" + set Selected flag according to our selection. + // To alter selection we use IsItemClicked() && !IsItemToggledOpen(), so clicking on an arrow doesn't alter selection. + ImGuiTreeNodeFlags node_flags = base_flags; + const bool is_selected = (selection_mask & (1 << i)) != 0; + if (is_selected) + node_flags |= ImGuiTreeNodeFlags_Selected; + if (i < 3) + { + // Items 0..2 are Tree Node + bool node_open = ImGui::TreeNodeEx((void*)(intptr_t)i, node_flags, "Selectable Node %d", i); + if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) + node_clicked = i; + if (test_drag_and_drop && ImGui::BeginDragDropSource()) + { + ImGui::SetDragDropPayload("_TREENODE", NULL, 0); + ImGui::Text("This is a drag and drop source"); + ImGui::EndDragDropSource(); + } + if (node_open) + { + ImGui::BulletText("Blah blah\nBlah Blah"); + ImGui::TreePop(); + } + } + else + { + // Items 3..5 are Tree Leaves + // The only reason we use TreeNode at all is to allow selection of the leaf. Otherwise we can + // use BulletText() or advance the cursor by GetTreeNodeToLabelSpacing() and call Text(). + node_flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; // ImGuiTreeNodeFlags_Bullet + ImGui::TreeNodeEx((void*)(intptr_t)i, node_flags, "Selectable Leaf %d", i); + if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) + node_clicked = i; + if (test_drag_and_drop && ImGui::BeginDragDropSource()) + { + ImGui::SetDragDropPayload("_TREENODE", NULL, 0); + ImGui::Text("This is a drag and drop source"); + ImGui::EndDragDropSource(); + } + } + } + if (node_clicked != -1) + { + // Update selection state + // (process outside of tree loop to avoid visual inconsistencies during the clicking frame) + if (ImGui::GetIO().KeyCtrl) + selection_mask ^= (1 << node_clicked); // CTRL+click to toggle + else //if (!(selection_mask & (1 << node_clicked))) // Depending on selection behavior you want, may want to preserve selection when clicking on item that is part of the selection + selection_mask = (1 << node_clicked); // Click to single-select + } + if (align_label_with_current_x_position) + ImGui::Indent(ImGui::GetTreeNodeToLabelSpacing()); + ImGui::TreePop(); + } + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Collapsing Headers"); + if (ImGui::TreeNode("Collapsing Headers")) + { + static bool closable_group = true; + ImGui::Checkbox("Show 2nd header", &closable_group); + if (ImGui::CollapsingHeader("Header", ImGuiTreeNodeFlags_None)) + { + ImGui::Text("IsItemHovered: %d", ImGui::IsItemHovered()); + for (int i = 0; i < 5; i++) + ImGui::Text("Some content %d", i); + } + if (ImGui::CollapsingHeader("Header with a close button", &closable_group)) + { + ImGui::Text("IsItemHovered: %d", ImGui::IsItemHovered()); + for (int i = 0; i < 5; i++) + ImGui::Text("More content %d", i); + } + /* + if (ImGui::CollapsingHeader("Header with a bullet", ImGuiTreeNodeFlags_Bullet)) + ImGui::Text("IsItemHovered: %d", ImGui::IsItemHovered()); + */ + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Bullets"); + if (ImGui::TreeNode("Bullets")) + { + ImGui::BulletText("Bullet point 1"); + ImGui::BulletText("Bullet point 2\nOn multiple lines"); + if (ImGui::TreeNode("Tree node")) + { + ImGui::BulletText("Another bullet point"); + ImGui::TreePop(); + } + ImGui::Bullet(); ImGui::Text("Bullet point 3 (two calls)"); + ImGui::Bullet(); ImGui::SmallButton("Button"); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text"); + if (ImGui::TreeNode("Text")) + { + IMGUI_DEMO_MARKER("Widgets/Text/Colored Text"); + if (ImGui::TreeNode("Colorful Text")) + { + // Using shortcut. You can use PushStyleColor()/PopStyleColor() for more flexibility. + ImGui::TextColored(ImVec4(1.0f, 0.0f, 1.0f, 1.0f), "Pink"); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Yellow"); + ImGui::TextDisabled("Disabled"); + ImGui::SameLine(); HelpMarker("The TextDisabled color is stored in ImGuiStyle."); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text/Word Wrapping"); + if (ImGui::TreeNode("Word Wrapping")) + { + // Using shortcut. You can use PushTextWrapPos()/PopTextWrapPos() for more flexibility. + ImGui::TextWrapped( + "This text should automatically wrap on the edge of the window. The current implementation " + "for text wrapping follows simple rules suitable for English and possibly other languages."); + ImGui::Spacing(); + + static float wrap_width = 200.0f; + ImGui::SliderFloat("Wrap width", &wrap_width, -20, 600, "%.0f"); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + for (int n = 0; n < 2; n++) + { + ImGui::Text("Test paragraph %d:", n); + ImVec2 pos = ImGui::GetCursorScreenPos(); + ImVec2 marker_min = ImVec2(pos.x + wrap_width, pos.y); + ImVec2 marker_max = ImVec2(pos.x + wrap_width + 10, pos.y + ImGui::GetTextLineHeight()); + ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + wrap_width); + if (n == 0) + ImGui::Text("The lazy dog is a good dog. This paragraph should fit within %.0f pixels. Testing a 1 character word. The quick brown fox jumps over the lazy dog.", wrap_width); + else + ImGui::Text("aaaaaaaa bbbbbbbb, c cccccccc,dddddddd. d eeeeeeee ffffffff. gggggggg!hhhhhhhh"); + + // Draw actual text bounding box, following by marker of our expected limit (should not overlap!) + draw_list->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(255, 255, 0, 255)); + draw_list->AddRectFilled(marker_min, marker_max, IM_COL32(255, 0, 255, 255)); + ImGui::PopTextWrapPos(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text/UTF-8 Text"); + if (ImGui::TreeNode("UTF-8 Text")) + { + // UTF-8 test with Japanese characters + // (Needs a suitable font? Try "Google Noto" or "Arial Unicode". See docs/FONTS.md for details.) + // - From C++11 you can use the u8"my text" syntax to encode literal strings as UTF-8 + // - For earlier compiler, you may be able to encode your sources as UTF-8 (e.g. in Visual Studio, you + // can save your source files as 'UTF-8 without signature'). + // - FOR THIS DEMO FILE ONLY, BECAUSE WE WANT TO SUPPORT OLD COMPILERS, WE ARE *NOT* INCLUDING RAW UTF-8 + // CHARACTERS IN THIS SOURCE FILE. Instead we are encoding a few strings with hexadecimal constants. + // Don't do this in your application! Please use u8"text in any language" in your application! + // Note that characters values are preserved even by InputText() if the font cannot be displayed, + // so you can safely copy & paste garbled characters into another application. + ImGui::TextWrapped( + "CJK text will only appears if the font was loaded with the appropriate CJK character ranges. " + "Call io.Fonts->AddFontFromFileTTF() manually to load extra character ranges. " + "Read docs/FONTS.md for details."); + ImGui::Text("Hiragana: \xe3\x81\x8b\xe3\x81\x8d\xe3\x81\x8f\xe3\x81\x91\xe3\x81\x93 (kakikukeko)"); // Normally we would use u8"blah blah" with the proper characters directly in the string. + ImGui::Text("Kanjis: \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e (nihongo)"); + static char buf[32] = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"; + //static char buf[32] = u8"NIHONGO"; // <- this is how you would write it with C++11, using real kanjis + ImGui::InputText("UTF-8 input", buf, IM_ARRAYSIZE(buf)); + ImGui::TreePop(); + } + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Images"); + if (ImGui::TreeNode("Images")) + { + ImGuiIO& io = ImGui::GetIO(); + ImGui::TextWrapped( + "Below we are displaying the font texture (which is the only texture we have access to in this demo). " + "Use the 'ImTextureID' type as storage to pass pointers or identifier to your own texture data. " + "Hover the texture for a zoomed view!"); + + // Below we are displaying the font texture because it is the only texture we have access to inside the demo! + // Remember that ImTextureID is just storage for whatever you want it to be. It is essentially a value that + // will be passed to the rendering backend via the ImDrawCmd structure. + // If you use one of the default imgui_impl_XXXX.cpp rendering backend, they all have comments at the top + // of their respective source file to specify what they expect to be stored in ImTextureID, for example: + // - The imgui_impl_dx11.cpp renderer expect a 'ID3D11ShaderResourceView*' pointer + // - The imgui_impl_opengl3.cpp renderer expect a GLuint OpenGL texture identifier, etc. + // More: + // - If you decided that ImTextureID = MyEngineTexture*, then you can pass your MyEngineTexture* pointers + // to ImGui::Image(), and gather width/height through your own functions, etc. + // - You can use ShowMetricsWindow() to inspect the draw data that are being passed to your renderer, + // it will help you debug issues if you are confused about it. + // - Consider using the lower-level ImDrawList::AddImage() API, via ImGui::GetWindowDrawList()->AddImage(). + // - Read https://github.com/ocornut/imgui/blob/master/docs/FAQ.md + // - Read https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples + ImTextureID my_tex_id = io.Fonts->TexID; + float my_tex_w = (float)io.Fonts->TexWidth; + float my_tex_h = (float)io.Fonts->TexHeight; + { + ImGui::Text("%.0fx%.0f", my_tex_w, my_tex_h); + ImVec2 pos = ImGui::GetCursorScreenPos(); + ImVec2 uv_min = ImVec2(0.0f, 0.0f); // Top-left + ImVec2 uv_max = ImVec2(1.0f, 1.0f); // Lower-right + ImVec4 tint_col = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // No tint + ImVec4 border_col = ImVec4(1.0f, 1.0f, 1.0f, 0.5f); // 50% opaque white + ImGui::Image(my_tex_id, ImVec2(my_tex_w, my_tex_h), uv_min, uv_max, tint_col, border_col); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + float region_sz = 32.0f; + float region_x = io.MousePos.x - pos.x - region_sz * 0.5f; + float region_y = io.MousePos.y - pos.y - region_sz * 0.5f; + float zoom = 4.0f; + if (region_x < 0.0f) { region_x = 0.0f; } + else if (region_x > my_tex_w - region_sz) { region_x = my_tex_w - region_sz; } + if (region_y < 0.0f) { region_y = 0.0f; } + else if (region_y > my_tex_h - region_sz) { region_y = my_tex_h - region_sz; } + ImGui::Text("Min: (%.2f, %.2f)", region_x, region_y); + ImGui::Text("Max: (%.2f, %.2f)", region_x + region_sz, region_y + region_sz); + ImVec2 uv0 = ImVec2((region_x) / my_tex_w, (region_y) / my_tex_h); + ImVec2 uv1 = ImVec2((region_x + region_sz) / my_tex_w, (region_y + region_sz) / my_tex_h); + ImGui::Image(my_tex_id, ImVec2(region_sz * zoom, region_sz * zoom), uv0, uv1, tint_col, border_col); + ImGui::EndTooltip(); + } + } + + IMGUI_DEMO_MARKER("Widgets/Images/Textured buttons"); + ImGui::TextWrapped("And now some textured buttons.."); + static int pressed_count = 0; + for (int i = 0; i < 8; i++) + { + ImGui::PushID(i); + int frame_padding = -1 + i; // -1 == uses default padding (style.FramePadding) + ImVec2 size = ImVec2(32.0f, 32.0f); // Size of the image we want to make visible + ImVec2 uv0 = ImVec2(0.0f, 0.0f); // UV coordinates for lower-left + ImVec2 uv1 = ImVec2(32.0f / my_tex_w, 32.0f / my_tex_h);// UV coordinates for (32,32) in our texture + ImVec4 bg_col = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); // Black background + ImVec4 tint_col = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // No tint + if (ImGui::ImageButton(my_tex_id, size, uv0, uv1, frame_padding, bg_col, tint_col)) + pressed_count += 1; + ImGui::PopID(); + ImGui::SameLine(); + } + ImGui::NewLine(); + ImGui::Text("Pressed %d times.", pressed_count); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Combo"); + if (ImGui::TreeNode("Combo")) + { + // Expose flags as checkbox for the demo + static ImGuiComboFlags flags = 0; + ImGui::CheckboxFlags("ImGuiComboFlags_PopupAlignLeft", &flags, ImGuiComboFlags_PopupAlignLeft); + ImGui::SameLine(); HelpMarker("Only makes a difference if the popup is larger than the combo"); + if (ImGui::CheckboxFlags("ImGuiComboFlags_NoArrowButton", &flags, ImGuiComboFlags_NoArrowButton)) + flags &= ~ImGuiComboFlags_NoPreview; // Clear the other flag, as we cannot combine both + if (ImGui::CheckboxFlags("ImGuiComboFlags_NoPreview", &flags, ImGuiComboFlags_NoPreview)) + flags &= ~ImGuiComboFlags_NoArrowButton; // Clear the other flag, as we cannot combine both + + // Using the generic BeginCombo() API, you have full control over how to display the combo contents. + // (your selection data could be an index, a pointer to the object, an id for the object, a flag intrusively + // stored in the object itself, etc.) + const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLLLLL", "MMMM", "OOOOOOO" }; + static int item_current_idx = 0; // Here we store our selection data as an index. + const char* combo_preview_value = items[item_current_idx]; // Pass in the preview value visible before opening the combo (it could be anything) + if (ImGui::BeginCombo("combo 1", combo_preview_value, flags)) + { + for (int n = 0; n < IM_ARRAYSIZE(items); n++) + { + const bool is_selected = (item_current_idx == n); + if (ImGui::Selectable(items[n], is_selected)) + item_current_idx = n; + + // Set the initial focus when opening the combo (scrolling + keyboard navigation focus) + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + // Simplified one-liner Combo() API, using values packed in a single constant string + // This is a convenience for when the selection set is small and known at compile-time. + static int item_current_2 = 0; + ImGui::Combo("combo 2 (one-liner)", &item_current_2, "aaaa\0bbbb\0cccc\0dddd\0eeee\0\0"); + + // Simplified one-liner Combo() using an array of const char* + // This is not very useful (may obsolete): prefer using BeginCombo()/EndCombo() for full control. + static int item_current_3 = -1; // If the selection isn't within 0..count, Combo won't display a preview + ImGui::Combo("combo 3 (array)", &item_current_3, items, IM_ARRAYSIZE(items)); + + // Simplified one-liner Combo() using an accessor function + struct Funcs { static bool ItemGetter(void* data, int n, const char** out_str) { *out_str = ((const char**)data)[n]; return true; } }; + static int item_current_4 = 0; + ImGui::Combo("combo 4 (function)", &item_current_4, &Funcs::ItemGetter, items, IM_ARRAYSIZE(items)); + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/List Boxes"); + if (ImGui::TreeNode("List boxes")) + { + // Using the generic BeginListBox() API, you have full control over how to display the combo contents. + // (your selection data could be an index, a pointer to the object, an id for the object, a flag intrusively + // stored in the object itself, etc.) + const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLLLLL", "MMMM", "OOOOOOO" }; + static int item_current_idx = 0; // Here we store our selection data as an index. + if (ImGui::BeginListBox("listbox 1")) + { + for (int n = 0; n < IM_ARRAYSIZE(items); n++) + { + const bool is_selected = (item_current_idx == n); + if (ImGui::Selectable(items[n], is_selected)) + item_current_idx = n; + + // Set the initial focus when opening the combo (scrolling + keyboard navigation focus) + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndListBox(); + } + + // Custom size: use all width, 5 items tall + ImGui::Text("Full-width:"); + if (ImGui::BeginListBox("##listbox 2", ImVec2(-FLT_MIN, 5 * ImGui::GetTextLineHeightWithSpacing()))) + { + for (int n = 0; n < IM_ARRAYSIZE(items); n++) + { + const bool is_selected = (item_current_idx == n); + if (ImGui::Selectable(items[n], is_selected)) + item_current_idx = n; + + // Set the initial focus when opening the combo (scrolling + keyboard navigation focus) + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndListBox(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Selectables"); + if (ImGui::TreeNode("Selectables")) + { + // Selectable() has 2 overloads: + // - The one taking "bool selected" as a read-only selection information. + // When Selectable() has been clicked it returns true and you can alter selection state accordingly. + // - The one taking "bool* p_selected" as a read-write selection information (convenient in some cases) + // The earlier is more flexible, as in real application your selection may be stored in many different ways + // and not necessarily inside a bool value (e.g. in flags within objects, as an external list, etc). + IMGUI_DEMO_MARKER("Widgets/Selectables/Basic"); + if (ImGui::TreeNode("Basic")) + { + static bool selection[5] = { false, true, false, false, false }; + ImGui::Selectable("1. I am selectable", &selection[0]); + ImGui::Selectable("2. I am selectable", &selection[1]); + ImGui::Text("(I am not selectable)"); + ImGui::Selectable("4. I am selectable", &selection[3]); + if (ImGui::Selectable("5. I am double clickable", selection[4], ImGuiSelectableFlags_AllowDoubleClick)) + if (ImGui::IsMouseDoubleClicked(0)) + selection[4] = !selection[4]; + ImGui::TreePop(); + } + IMGUI_DEMO_MARKER("Widgets/Selectables/Single Selection"); + if (ImGui::TreeNode("Selection State: Single Selection")) + { + static int selected = -1; + for (int n = 0; n < 5; n++) + { + char buf[32]; + sprintf(buf, "Object %d", n); + if (ImGui::Selectable(buf, selected == n)) + selected = n; + } + ImGui::TreePop(); + } + IMGUI_DEMO_MARKER("Widgets/Selectables/Multiple Selection"); + if (ImGui::TreeNode("Selection State: Multiple Selection")) + { + HelpMarker("Hold CTRL and click to select multiple items."); + static bool selection[5] = { false, false, false, false, false }; + for (int n = 0; n < 5; n++) + { + char buf[32]; + sprintf(buf, "Object %d", n); + if (ImGui::Selectable(buf, selection[n])) + { + if (!ImGui::GetIO().KeyCtrl) // Clear selection when CTRL is not held + memset(selection, 0, sizeof(selection)); + selection[n] ^= 1; + } + } + ImGui::TreePop(); + } + IMGUI_DEMO_MARKER("Widgets/Selectables/Rendering more text into the same line"); + if (ImGui::TreeNode("Rendering more text into the same line")) + { + // Using the Selectable() override that takes "bool* p_selected" parameter, + // this function toggle your bool value automatically. + static bool selected[3] = { false, false, false }; + ImGui::Selectable("main.c", &selected[0]); ImGui::SameLine(300); ImGui::Text(" 2,345 bytes"); + ImGui::Selectable("Hello.cpp", &selected[1]); ImGui::SameLine(300); ImGui::Text("12,345 bytes"); + ImGui::Selectable("Hello.h", &selected[2]); ImGui::SameLine(300); ImGui::Text(" 2,345 bytes"); + ImGui::TreePop(); + } + IMGUI_DEMO_MARKER("Widgets/Selectables/In columns"); + if (ImGui::TreeNode("In columns")) + { + static bool selected[10] = {}; + + if (ImGui::BeginTable("split1", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_Borders)) + { + for (int i = 0; i < 10; i++) + { + char label[32]; + sprintf(label, "Item %d", i); + ImGui::TableNextColumn(); + ImGui::Selectable(label, &selected[i]); // FIXME-TABLE: Selection overlap + } + ImGui::EndTable(); + } + ImGui::Spacing(); + if (ImGui::BeginTable("split2", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_Borders)) + { + for (int i = 0; i < 10; i++) + { + char label[32]; + sprintf(label, "Item %d", i); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Selectable(label, &selected[i], ImGuiSelectableFlags_SpanAllColumns); + ImGui::TableNextColumn(); + ImGui::Text("Some other contents"); + ImGui::TableNextColumn(); + ImGui::Text("123456"); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + IMGUI_DEMO_MARKER("Widgets/Selectables/Grid"); + if (ImGui::TreeNode("Grid")) + { + static char selected[4][4] = { { 1, 0, 0, 0 }, { 0, 1, 0, 0 }, { 0, 0, 1, 0 }, { 0, 0, 0, 1 } }; + + // Add in a bit of silly fun... + const float time = (float)ImGui::GetTime(); + const bool winning_state = memchr(selected, 0, sizeof(selected)) == NULL; // If all cells are selected... + if (winning_state) + ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.5f + 0.5f * cosf(time * 2.0f), 0.5f + 0.5f * sinf(time * 3.0f))); + + for (int y = 0; y < 4; y++) + for (int x = 0; x < 4; x++) + { + if (x > 0) + ImGui::SameLine(); + ImGui::PushID(y * 4 + x); + if (ImGui::Selectable("Sailor", selected[y][x] != 0, 0, ImVec2(50, 50))) + { + // Toggle clicked cell + toggle neighbors + selected[y][x] ^= 1; + if (x > 0) { selected[y][x - 1] ^= 1; } + if (x < 3) { selected[y][x + 1] ^= 1; } + if (y > 0) { selected[y - 1][x] ^= 1; } + if (y < 3) { selected[y + 1][x] ^= 1; } + } + ImGui::PopID(); + } + + if (winning_state) + ImGui::PopStyleVar(); + ImGui::TreePop(); + } + IMGUI_DEMO_MARKER("Widgets/Selectables/Alignment"); + if (ImGui::TreeNode("Alignment")) + { + HelpMarker( + "By default, Selectables uses style.SelectableTextAlign but it can be overridden on a per-item " + "basis using PushStyleVar(). You'll probably want to always keep your default situation to " + "left-align otherwise it becomes difficult to layout multiple items on a same line"); + static bool selected[3 * 3] = { true, false, true, false, true, false, true, false, true }; + for (int y = 0; y < 3; y++) + { + for (int x = 0; x < 3; x++) + { + ImVec2 alignment = ImVec2((float)x / 2.0f, (float)y / 2.0f); + char name[32]; + sprintf(name, "(%.1f,%.1f)", alignment.x, alignment.y); + if (x > 0) ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, alignment); + ImGui::Selectable(name, &selected[3 * y + x], ImGuiSelectableFlags_None, ImVec2(80, 80)); + ImGui::PopStyleVar(); + } + } + ImGui::TreePop(); + } + ImGui::TreePop(); + } + + // To wire InputText() with std::string or any other custom string type, + // see the "Text Input > Resize Callback" section of this demo, and the misc/cpp/imgui_stdlib.h file. + IMGUI_DEMO_MARKER("Widgets/Text Input"); + if (ImGui::TreeNode("Text Input")) + { + IMGUI_DEMO_MARKER("Widgets/Text Input/Multi-line Text Input"); + if (ImGui::TreeNode("Multi-line Text Input")) + { + // Note: we are using a fixed-sized buffer for simplicity here. See ImGuiInputTextFlags_CallbackResize + // and the code in misc/cpp/imgui_stdlib.h for how to setup InputText() for dynamically resizing strings. + static char text[1024 * 16] = + "/*\n" + " The Pentium F00F bug, shorthand for F0 0F C7 C8,\n" + " the hexadecimal encoding of one offending instruction,\n" + " more formally, the invalid operand with locked CMPXCHG8B\n" + " instruction bug, is a design flaw in the majority of\n" + " Intel Pentium, Pentium MMX, and Pentium OverDrive\n" + " processors (all in the P5 microarchitecture).\n" + "*/\n\n" + "label:\n" + "\tlock cmpxchg8b eax\n"; + + static ImGuiInputTextFlags flags = ImGuiInputTextFlags_AllowTabInput; + HelpMarker("You can use the ImGuiInputTextFlags_CallbackResize facility if you need to wire InputTextMultiline() to a dynamic string type. See misc/cpp/imgui_stdlib.h for an example. (This is not demonstrated in imgui_demo.cpp because we don't want to include in here)"); + ImGui::CheckboxFlags("ImGuiInputTextFlags_ReadOnly", &flags, ImGuiInputTextFlags_ReadOnly); + ImGui::CheckboxFlags("ImGuiInputTextFlags_AllowTabInput", &flags, ImGuiInputTextFlags_AllowTabInput); + ImGui::CheckboxFlags("ImGuiInputTextFlags_CtrlEnterForNewLine", &flags, ImGuiInputTextFlags_CtrlEnterForNewLine); + ImGui::InputTextMultiline("##source", text, IM_ARRAYSIZE(text), ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 16), flags); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text Input/Filtered Text Input"); + if (ImGui::TreeNode("Filtered Text Input")) + { + struct TextFilters + { + // Return 0 (pass) if the character is 'i' or 'm' or 'g' or 'u' or 'i' + static int FilterImGuiLetters(ImGuiInputTextCallbackData* data) + { + if (data->EventChar < 256 && strchr("imgui", (char)data->EventChar)) + return 0; + return 1; + } + }; + + static char buf1[64] = ""; ImGui::InputText("default", buf1, 64); + static char buf2[64] = ""; ImGui::InputText("decimal", buf2, 64, ImGuiInputTextFlags_CharsDecimal); + static char buf3[64] = ""; ImGui::InputText("hexadecimal", buf3, 64, ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase); + static char buf4[64] = ""; ImGui::InputText("uppercase", buf4, 64, ImGuiInputTextFlags_CharsUppercase); + static char buf5[64] = ""; ImGui::InputText("no blank", buf5, 64, ImGuiInputTextFlags_CharsNoBlank); + static char buf6[64] = ""; ImGui::InputText("\"imgui\" letters", buf6, 64, ImGuiInputTextFlags_CallbackCharFilter, TextFilters::FilterImGuiLetters); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text Input/Password input"); + if (ImGui::TreeNode("Password Input")) + { + static char password[64] = "password123"; + ImGui::InputText("password", password, IM_ARRAYSIZE(password), ImGuiInputTextFlags_Password); + ImGui::SameLine(); HelpMarker("Display all characters as '*'.\nDisable clipboard cut and copy.\nDisable logging.\n"); + ImGui::InputTextWithHint("password (w/ hint)", "", password, IM_ARRAYSIZE(password), ImGuiInputTextFlags_Password); + ImGui::InputText("password (clear)", password, IM_ARRAYSIZE(password)); + ImGui::TreePop(); + } + + if (ImGui::TreeNode("Completion, History, Edit Callbacks")) + { + struct Funcs + { + static int MyCallback(ImGuiInputTextCallbackData* data) + { + if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion) + { + data->InsertChars(data->CursorPos, ".."); + } + else if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) + { + if (data->EventKey == ImGuiKey_UpArrow) + { + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, "Pressed Up!"); + data->SelectAll(); + } + else if (data->EventKey == ImGuiKey_DownArrow) + { + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, "Pressed Down!"); + data->SelectAll(); + } + } + else if (data->EventFlag == ImGuiInputTextFlags_CallbackEdit) + { + // Toggle casing of first character + char c = data->Buf[0]; + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) data->Buf[0] ^= 32; + data->BufDirty = true; + + // Increment a counter + int* p_int = (int*)data->UserData; + *p_int = *p_int + 1; + } + return 0; + } + }; + static char buf1[64]; + ImGui::InputText("Completion", buf1, 64, ImGuiInputTextFlags_CallbackCompletion, Funcs::MyCallback); + ImGui::SameLine(); HelpMarker("Here we append \"..\" each time Tab is pressed. See 'Examples>Console' for a more meaningful demonstration of using this callback."); + + static char buf2[64]; + ImGui::InputText("History", buf2, 64, ImGuiInputTextFlags_CallbackHistory, Funcs::MyCallback); + ImGui::SameLine(); HelpMarker("Here we replace and select text each time Up/Down are pressed. See 'Examples>Console' for a more meaningful demonstration of using this callback."); + + static char buf3[64]; + static int edit_count = 0; + ImGui::InputText("Edit", buf3, 64, ImGuiInputTextFlags_CallbackEdit, Funcs::MyCallback, (void*)&edit_count); + ImGui::SameLine(); HelpMarker("Here we toggle the casing of the first character on every edits + count edits."); + ImGui::SameLine(); ImGui::Text("(%d)", edit_count); + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text Input/Resize Callback"); + if (ImGui::TreeNode("Resize Callback")) + { + // To wire InputText() with std::string or any other custom string type, + // you can use the ImGuiInputTextFlags_CallbackResize flag + create a custom ImGui::InputText() wrapper + // using your preferred type. See misc/cpp/imgui_stdlib.h for an implementation of this using std::string. + HelpMarker( + "Using ImGuiInputTextFlags_CallbackResize to wire your custom string type to InputText().\n\n" + "See misc/cpp/imgui_stdlib.h for an implementation of this for std::string."); + struct Funcs + { + static int MyResizeCallback(ImGuiInputTextCallbackData* data) + { + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) + { + ImVector* my_str = (ImVector*)data->UserData; + IM_ASSERT(my_str->begin() == data->Buf); + my_str->resize(data->BufSize); // NB: On resizing calls, generally data->BufSize == data->BufTextLen + 1 + data->Buf = my_str->begin(); + } + return 0; + } + + // Note: Because ImGui:: is a namespace you would typically add your own function into the namespace. + // For example, you code may declare a function 'ImGui::InputText(const char* label, MyString* my_str)' + static bool MyInputTextMultiline(const char* label, ImVector* my_str, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0) + { + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + return ImGui::InputTextMultiline(label, my_str->begin(), (size_t)my_str->size(), size, flags | ImGuiInputTextFlags_CallbackResize, Funcs::MyResizeCallback, (void*)my_str); + } + }; + + // For this demo we are using ImVector as a string container. + // Note that because we need to store a terminating zero character, our size/capacity are 1 more + // than usually reported by a typical string class. + static ImVector my_str; + if (my_str.empty()) + my_str.push_back(0); + Funcs::MyInputTextMultiline("##MyStr", &my_str, ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 16)); + ImGui::Text("Data: %p\nSize: %d\nCapacity: %d", (void*)my_str.begin(), my_str.size(), my_str.capacity()); + ImGui::TreePop(); + } + + ImGui::TreePop(); + } + + // Tabs + IMGUI_DEMO_MARKER("Widgets/Tabs"); + if (ImGui::TreeNode("Tabs")) + { + IMGUI_DEMO_MARKER("Widgets/Tabs/Basic"); + if (ImGui::TreeNode("Basic")) + { + ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_None; + if (ImGui::BeginTabBar("MyTabBar", tab_bar_flags)) + { + if (ImGui::BeginTabItem("Avocado")) + { + ImGui::Text("This is the Avocado tab!\nblah blah blah blah blah"); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Broccoli")) + { + ImGui::Text("This is the Broccoli tab!\nblah blah blah blah blah"); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Cucumber")) + { + ImGui::Text("This is the Cucumber tab!\nblah blah blah blah blah"); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + ImGui::Separator(); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Tabs/Advanced & Close Button"); + if (ImGui::TreeNode("Advanced & Close Button")) + { + // Expose a couple of the available flags. In most cases you may just call BeginTabBar() with no flags (0). + static ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_Reorderable; + ImGui::CheckboxFlags("ImGuiTabBarFlags_Reorderable", &tab_bar_flags, ImGuiTabBarFlags_Reorderable); + ImGui::CheckboxFlags("ImGuiTabBarFlags_AutoSelectNewTabs", &tab_bar_flags, ImGuiTabBarFlags_AutoSelectNewTabs); + ImGui::CheckboxFlags("ImGuiTabBarFlags_TabListPopupButton", &tab_bar_flags, ImGuiTabBarFlags_TabListPopupButton); + ImGui::CheckboxFlags("ImGuiTabBarFlags_NoCloseWithMiddleMouseButton", &tab_bar_flags, ImGuiTabBarFlags_NoCloseWithMiddleMouseButton); + if ((tab_bar_flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0) + tab_bar_flags |= ImGuiTabBarFlags_FittingPolicyDefault_; + if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyResizeDown", &tab_bar_flags, ImGuiTabBarFlags_FittingPolicyResizeDown)) + tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyResizeDown); + if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyScroll", &tab_bar_flags, ImGuiTabBarFlags_FittingPolicyScroll)) + tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyScroll); + + // Tab Bar + const char* names[4] = { "Artichoke", "Beetroot", "Celery", "Daikon" }; + static bool opened[4] = { true, true, true, true }; // Persistent user state + for (int n = 0; n < IM_ARRAYSIZE(opened); n++) + { + if (n > 0) { ImGui::SameLine(); } + ImGui::Checkbox(names[n], &opened[n]); + } + + // Passing a bool* to BeginTabItem() is similar to passing one to Begin(): + // the underlying bool will be set to false when the tab is closed. + if (ImGui::BeginTabBar("MyTabBar", tab_bar_flags)) + { + for (int n = 0; n < IM_ARRAYSIZE(opened); n++) + if (opened[n] && ImGui::BeginTabItem(names[n], &opened[n], ImGuiTabItemFlags_None)) + { + ImGui::Text("This is the %s tab!", names[n]); + if (n & 1) + ImGui::Text("I am an odd tab."); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + ImGui::Separator(); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Tabs/TabItemButton & Leading-Trailing flags"); + if (ImGui::TreeNode("TabItemButton & Leading/Trailing flags")) + { + static ImVector active_tabs; + static int next_tab_id = 0; + if (next_tab_id == 0) // Initialize with some default tabs + for (int i = 0; i < 3; i++) + active_tabs.push_back(next_tab_id++); + + // TabItemButton() and Leading/Trailing flags are distinct features which we will demo together. + // (It is possible to submit regular tabs with Leading/Trailing flags, or TabItemButton tabs without Leading/Trailing flags... + // but they tend to make more sense together) + static bool show_leading_button = true; + static bool show_trailing_button = true; + ImGui::Checkbox("Show Leading TabItemButton()", &show_leading_button); + ImGui::Checkbox("Show Trailing TabItemButton()", &show_trailing_button); + + // Expose some other flags which are useful to showcase how they interact with Leading/Trailing tabs + static ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_AutoSelectNewTabs | ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown; + ImGui::CheckboxFlags("ImGuiTabBarFlags_TabListPopupButton", &tab_bar_flags, ImGuiTabBarFlags_TabListPopupButton); + if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyResizeDown", &tab_bar_flags, ImGuiTabBarFlags_FittingPolicyResizeDown)) + tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyResizeDown); + if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyScroll", &tab_bar_flags, ImGuiTabBarFlags_FittingPolicyScroll)) + tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyScroll); + + if (ImGui::BeginTabBar("MyTabBar", tab_bar_flags)) + { + // Demo a Leading TabItemButton(): click the "?" button to open a menu + if (show_leading_button) + if (ImGui::TabItemButton("?", ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_NoTooltip)) + ImGui::OpenPopup("MyHelpMenu"); + if (ImGui::BeginPopup("MyHelpMenu")) + { + ImGui::Selectable("Hello!"); + ImGui::EndPopup(); + } + + // Demo Trailing Tabs: click the "+" button to add a new tab (in your app you may want to use a font icon instead of the "+") + // Note that we submit it before the regular tabs, but because of the ImGuiTabItemFlags_Trailing flag it will always appear at the end. + if (show_trailing_button) + if (ImGui::TabItemButton("+", ImGuiTabItemFlags_Trailing | ImGuiTabItemFlags_NoTooltip)) + active_tabs.push_back(next_tab_id++); // Add new tab + + // Submit our regular tabs + for (int n = 0; n < active_tabs.Size; ) + { + bool open = true; + char name[16]; + snprintf(name, IM_ARRAYSIZE(name), "%04d", active_tabs[n]); + if (ImGui::BeginTabItem(name, &open, ImGuiTabItemFlags_None)) + { + ImGui::Text("This is the %s tab!", name); + ImGui::EndTabItem(); + } + + if (!open) + active_tabs.erase(active_tabs.Data + n); + else + n++; + } + + ImGui::EndTabBar(); + } + ImGui::Separator(); + ImGui::TreePop(); + } + ImGui::TreePop(); + } + + // Plot/Graph widgets are not very good. + // Consider using a third-party library such as ImPlot: https://github.com/epezent/implot + // (see others https://github.com/ocornut/imgui/wiki/Useful-Extensions) + IMGUI_DEMO_MARKER("Widgets/Plotting"); + if (ImGui::TreeNode("Plotting")) + { + static bool animate = true; + ImGui::Checkbox("Animate", &animate); + + // Plot as lines and plot as histogram + IMGUI_DEMO_MARKER("Widgets/Plotting/PlotLines, PlotHistogram"); + static float arr[] = { 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f }; + ImGui::PlotLines("Frame Times", arr, IM_ARRAYSIZE(arr)); + ImGui::PlotHistogram("Histogram", arr, IM_ARRAYSIZE(arr), 0, NULL, 0.0f, 1.0f, ImVec2(0, 80.0f)); + + // Fill an array of contiguous float values to plot + // Tip: If your float aren't contiguous but part of a structure, you can pass a pointer to your first float + // and the sizeof() of your structure in the "stride" parameter. + static float values[90] = {}; + static int values_offset = 0; + static double refresh_time = 0.0; + if (!animate || refresh_time == 0.0) + refresh_time = ImGui::GetTime(); + while (refresh_time < ImGui::GetTime()) // Create data at fixed 60 Hz rate for the demo + { + static float phase = 0.0f; + values[values_offset] = cosf(phase); + values_offset = (values_offset + 1) % IM_ARRAYSIZE(values); + phase += 0.10f * values_offset; + refresh_time += 1.0f / 60.0f; + } + + // Plots can display overlay texts + // (in this example, we will display an average value) + { + float average = 0.0f; + for (int n = 0; n < IM_ARRAYSIZE(values); n++) + average += values[n]; + average /= (float)IM_ARRAYSIZE(values); + char overlay[32]; + sprintf(overlay, "avg %f", average); + ImGui::PlotLines("Lines", values, IM_ARRAYSIZE(values), values_offset, overlay, -1.0f, 1.0f, ImVec2(0, 80.0f)); + } + + // Use functions to generate output + // FIXME: This is rather awkward because current plot API only pass in indices. + // We probably want an API passing floats and user provide sample rate/count. + struct Funcs + { + static float Sin(void*, int i) { return sinf(i * 0.1f); } + static float Saw(void*, int i) { return (i & 1) ? 1.0f : -1.0f; } + }; + static int func_type = 0, display_count = 70; + ImGui::Separator(); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::Combo("func", &func_type, "Sin\0Saw\0"); + ImGui::SameLine(); + ImGui::SliderInt("Sample count", &display_count, 1, 400); + float (*func)(void*, int) = (func_type == 0) ? Funcs::Sin : Funcs::Saw; + ImGui::PlotLines("Lines", func, NULL, display_count, 0, NULL, -1.0f, 1.0f, ImVec2(0, 80)); + ImGui::PlotHistogram("Histogram", func, NULL, display_count, 0, NULL, -1.0f, 1.0f, ImVec2(0, 80)); + ImGui::Separator(); + + // Animate a simple progress bar + IMGUI_DEMO_MARKER("Widgets/Plotting/ProgressBar"); + static float progress = 0.0f, progress_dir = 1.0f; + if (animate) + { + progress += progress_dir * 0.4f * ImGui::GetIO().DeltaTime; + if (progress >= +1.1f) { progress = +1.1f; progress_dir *= -1.0f; } + if (progress <= -0.1f) { progress = -0.1f; progress_dir *= -1.0f; } + } + + // Typically we would use ImVec2(-1.0f,0.0f) or ImVec2(-FLT_MIN,0.0f) to use all available width, + // or ImVec2(width,0.0f) for a specified width. ImVec2(0.0f,0.0f) uses ItemWidth. + ImGui::ProgressBar(progress, ImVec2(0.0f, 0.0f)); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Text("Progress Bar"); + + float progress_saturated = IM_CLAMP(progress, 0.0f, 1.0f); + char buf[32]; + sprintf(buf, "%d/%d", (int)(progress_saturated * 1753), 1753); + ImGui::ProgressBar(progress, ImVec2(0.f, 0.f), buf); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Color"); + if (ImGui::TreeNode("Color/Picker Widgets")) + { + static ImVec4 color = ImVec4(114.0f / 255.0f, 144.0f / 255.0f, 154.0f / 255.0f, 200.0f / 255.0f); + + static bool alpha_preview = true; + static bool alpha_half_preview = false; + static bool drag_and_drop = true; + static bool options_menu = true; + static bool hdr = false; + ImGui::Checkbox("With Alpha Preview", &alpha_preview); + ImGui::Checkbox("With Half Alpha Preview", &alpha_half_preview); + ImGui::Checkbox("With Drag and Drop", &drag_and_drop); + ImGui::Checkbox("With Options Menu", &options_menu); ImGui::SameLine(); HelpMarker("Right-click on the individual color widget to show options."); + ImGui::Checkbox("With HDR", &hdr); ImGui::SameLine(); HelpMarker("Currently all this does is to lift the 0..1 limits on dragging widgets."); + ImGuiColorEditFlags misc_flags = (hdr ? ImGuiColorEditFlags_HDR : 0) | (drag_and_drop ? 0 : ImGuiColorEditFlags_NoDragDrop) | (alpha_half_preview ? ImGuiColorEditFlags_AlphaPreviewHalf : (alpha_preview ? ImGuiColorEditFlags_AlphaPreview : 0)) | (options_menu ? 0 : ImGuiColorEditFlags_NoOptions); + + IMGUI_DEMO_MARKER("Widgets/Color/ColorEdit"); + ImGui::Text("Color widget:"); + ImGui::SameLine(); HelpMarker( + "Click on the color square to open a color picker.\n" + "CTRL+click on individual component to input value.\n"); + ImGui::ColorEdit3("MyColor##1", (float*)&color, misc_flags); + + IMGUI_DEMO_MARKER("Widgets/Color/ColorEdit (HSV, with Alpha)"); + ImGui::Text("Color widget HSV with Alpha:"); + ImGui::ColorEdit4("MyColor##2", (float*)&color, ImGuiColorEditFlags_DisplayHSV | misc_flags); + + IMGUI_DEMO_MARKER("Widgets/Color/ColorEdit (float display)"); + ImGui::Text("Color widget with Float Display:"); + ImGui::ColorEdit4("MyColor##2f", (float*)&color, ImGuiColorEditFlags_Float | misc_flags); + + IMGUI_DEMO_MARKER("Widgets/Color/ColorButton (with Picker)"); + ImGui::Text("Color button with Picker:"); + ImGui::SameLine(); HelpMarker( + "With the ImGuiColorEditFlags_NoInputs flag you can hide all the slider/text inputs.\n" + "With the ImGuiColorEditFlags_NoLabel flag you can pass a non-empty label which will only " + "be used for the tooltip and picker popup."); + ImGui::ColorEdit4("MyColor##3", (float*)&color, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | misc_flags); + + IMGUI_DEMO_MARKER("Widgets/Color/ColorButton (with custom Picker popup)"); + ImGui::Text("Color button with Custom Picker Popup:"); + + // Generate a default palette. The palette will persist and can be edited. + static bool saved_palette_init = true; + static ImVec4 saved_palette[32] = {}; + if (saved_palette_init) + { + for (int n = 0; n < IM_ARRAYSIZE(saved_palette); n++) + { + ImGui::ColorConvertHSVtoRGB(n / 31.0f, 0.8f, 0.8f, + saved_palette[n].x, saved_palette[n].y, saved_palette[n].z); + saved_palette[n].w = 1.0f; // Alpha + } + saved_palette_init = false; + } + + static ImVec4 backup_color; + bool open_popup = ImGui::ColorButton("MyColor##3b", color, misc_flags); + ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); + open_popup |= ImGui::Button("Palette"); + if (open_popup) + { + ImGui::OpenPopup("mypicker"); + backup_color = color; + } + if (ImGui::BeginPopup("mypicker")) + { + ImGui::Text("MY CUSTOM COLOR PICKER WITH AN AMAZING PALETTE!"); + ImGui::Separator(); + ImGui::ColorPicker4("##picker", (float*)&color, misc_flags | ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_NoSmallPreview); + ImGui::SameLine(); + + ImGui::BeginGroup(); // Lock X position + ImGui::Text("Current"); + ImGui::ColorButton("##current", color, ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_AlphaPreviewHalf, ImVec2(60, 40)); + ImGui::Text("Previous"); + if (ImGui::ColorButton("##previous", backup_color, ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_AlphaPreviewHalf, ImVec2(60, 40))) + color = backup_color; + ImGui::Separator(); + ImGui::Text("Palette"); + for (int n = 0; n < IM_ARRAYSIZE(saved_palette); n++) + { + ImGui::PushID(n); + if ((n % 8) != 0) + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y); + + ImGuiColorEditFlags palette_button_flags = ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_NoTooltip; + if (ImGui::ColorButton("##palette", saved_palette[n], palette_button_flags, ImVec2(20, 20))) + color = ImVec4(saved_palette[n].x, saved_palette[n].y, saved_palette[n].z, color.w); // Preserve alpha! + + // Allow user to drop colors into each palette entry. Note that ColorButton() is already a + // drag source by default, unless specifying the ImGuiColorEditFlags_NoDragDrop flag. + if (ImGui::BeginDragDropTarget()) + { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) + memcpy((float*)&saved_palette[n], payload->Data, sizeof(float) * 3); + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) + memcpy((float*)&saved_palette[n], payload->Data, sizeof(float) * 4); + ImGui::EndDragDropTarget(); + } + + ImGui::PopID(); + } + ImGui::EndGroup(); + ImGui::EndPopup(); + } + + IMGUI_DEMO_MARKER("Widgets/Color/ColorButton (simple)"); + ImGui::Text("Color button only:"); + static bool no_border = false; + ImGui::Checkbox("ImGuiColorEditFlags_NoBorder", &no_border); + ImGui::ColorButton("MyColor##3c", *(ImVec4*)&color, misc_flags | (no_border ? ImGuiColorEditFlags_NoBorder : 0), ImVec2(80, 80)); + + IMGUI_DEMO_MARKER("Widgets/Color/ColorPicker"); + ImGui::Text("Color picker:"); + static bool alpha = true; + static bool alpha_bar = true; + static bool side_preview = true; + static bool ref_color = false; + static ImVec4 ref_color_v(1.0f, 0.0f, 1.0f, 0.5f); + static int display_mode = 0; + static int picker_mode = 0; + ImGui::Checkbox("With Alpha", &alpha); + ImGui::Checkbox("With Alpha Bar", &alpha_bar); + ImGui::Checkbox("With Side Preview", &side_preview); + if (side_preview) + { + ImGui::SameLine(); + ImGui::Checkbox("With Ref Color", &ref_color); + if (ref_color) + { + ImGui::SameLine(); + ImGui::ColorEdit4("##RefColor", &ref_color_v.x, ImGuiColorEditFlags_NoInputs | misc_flags); + } + } + ImGui::Combo("Display Mode", &display_mode, "Auto/Current\0None\0RGB Only\0HSV Only\0Hex Only\0"); + ImGui::SameLine(); HelpMarker( + "ColorEdit defaults to displaying RGB inputs if you don't specify a display mode, " + "but the user can change it with a right-click.\n\nColorPicker defaults to displaying RGB+HSV+Hex " + "if you don't specify a display mode.\n\nYou can change the defaults using SetColorEditOptions()."); + ImGui::Combo("Picker Mode", &picker_mode, "Auto/Current\0Hue bar + SV rect\0Hue wheel + SV triangle\0"); + ImGui::SameLine(); HelpMarker("User can right-click the picker to change mode."); + ImGuiColorEditFlags flags = misc_flags; + if (!alpha) flags |= ImGuiColorEditFlags_NoAlpha; // This is by default if you call ColorPicker3() instead of ColorPicker4() + if (alpha_bar) flags |= ImGuiColorEditFlags_AlphaBar; + if (!side_preview) flags |= ImGuiColorEditFlags_NoSidePreview; + if (picker_mode == 1) flags |= ImGuiColorEditFlags_PickerHueBar; + if (picker_mode == 2) flags |= ImGuiColorEditFlags_PickerHueWheel; + if (display_mode == 1) flags |= ImGuiColorEditFlags_NoInputs; // Disable all RGB/HSV/Hex displays + if (display_mode == 2) flags |= ImGuiColorEditFlags_DisplayRGB; // Override display mode + if (display_mode == 3) flags |= ImGuiColorEditFlags_DisplayHSV; + if (display_mode == 4) flags |= ImGuiColorEditFlags_DisplayHex; + ImGui::ColorPicker4("MyColor##4", (float*)&color, flags, ref_color ? &ref_color_v.x : NULL); + + ImGui::Text("Set defaults in code:"); + ImGui::SameLine(); HelpMarker( + "SetColorEditOptions() is designed to allow you to set boot-time default.\n" + "We don't have Push/Pop functions because you can force options on a per-widget basis if needed," + "and the user can change non-forced ones with the options menu.\nWe don't have a getter to avoid" + "encouraging you to persistently save values that aren't forward-compatible."); + if (ImGui::Button("Default: Uint8 + HSV + Hue Bar")) + ImGui::SetColorEditOptions(ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_PickerHueBar); + if (ImGui::Button("Default: Float + HDR + Hue Wheel")) + ImGui::SetColorEditOptions(ImGuiColorEditFlags_Float | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_PickerHueWheel); + + // HSV encoded support (to avoid RGB<>HSV round trips and singularities when S==0 or V==0) + static ImVec4 color_hsv(0.23f, 1.0f, 1.0f, 1.0f); // Stored as HSV! + ImGui::Spacing(); + ImGui::Text("HSV encoded colors"); + ImGui::SameLine(); HelpMarker( + "By default, colors are given to ColorEdit and ColorPicker in RGB, but ImGuiColorEditFlags_InputHSV" + "allows you to store colors as HSV and pass them to ColorEdit and ColorPicker as HSV. This comes with the" + "added benefit that you can manipulate hue values with the picker even when saturation or value are zero."); + ImGui::Text("Color widget with InputHSV:"); + ImGui::ColorEdit4("HSV shown as RGB##1", (float*)&color_hsv, ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_InputHSV | ImGuiColorEditFlags_Float); + ImGui::ColorEdit4("HSV shown as HSV##1", (float*)&color_hsv, ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_InputHSV | ImGuiColorEditFlags_Float); + ImGui::DragFloat4("Raw HSV values", (float*)&color_hsv, 0.01f, 0.0f, 1.0f); + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Drag and Slider Flags"); + if (ImGui::TreeNode("Drag/Slider Flags")) + { + // Demonstrate using advanced flags for DragXXX and SliderXXX functions. Note that the flags are the same! + static ImGuiSliderFlags flags = ImGuiSliderFlags_None; + ImGui::CheckboxFlags("ImGuiSliderFlags_AlwaysClamp", &flags, ImGuiSliderFlags_AlwaysClamp); + ImGui::SameLine(); HelpMarker("Always clamp value to min/max bounds (if any) when input manually with CTRL+Click."); + ImGui::CheckboxFlags("ImGuiSliderFlags_Logarithmic", &flags, ImGuiSliderFlags_Logarithmic); + ImGui::SameLine(); HelpMarker("Enable logarithmic editing (more precision for small values)."); + ImGui::CheckboxFlags("ImGuiSliderFlags_NoRoundToFormat", &flags, ImGuiSliderFlags_NoRoundToFormat); + ImGui::SameLine(); HelpMarker("Disable rounding underlying value to match precision of the format string (e.g. %.3f values are rounded to those 3 digits)."); + ImGui::CheckboxFlags("ImGuiSliderFlags_NoInput", &flags, ImGuiSliderFlags_NoInput); + ImGui::SameLine(); HelpMarker("Disable CTRL+Click or Enter key allowing to input text directly into the widget."); + + // Drags + static float drag_f = 0.5f; + static int drag_i = 50; + ImGui::Text("Underlying float value: %f", drag_f); + ImGui::DragFloat("DragFloat (0 -> 1)", &drag_f, 0.005f, 0.0f, 1.0f, "%.3f", flags); + ImGui::DragFloat("DragFloat (0 -> +inf)", &drag_f, 0.005f, 0.0f, FLT_MAX, "%.3f", flags); + ImGui::DragFloat("DragFloat (-inf -> 1)", &drag_f, 0.005f, -FLT_MAX, 1.0f, "%.3f", flags); + ImGui::DragFloat("DragFloat (-inf -> +inf)", &drag_f, 0.005f, -FLT_MAX, +FLT_MAX, "%.3f", flags); + ImGui::DragInt("DragInt (0 -> 100)", &drag_i, 0.5f, 0, 100, "%d", flags); + + // Sliders + static float slider_f = 0.5f; + static int slider_i = 50; + ImGui::Text("Underlying float value: %f", slider_f); + ImGui::SliderFloat("SliderFloat (0 -> 1)", &slider_f, 0.0f, 1.0f, "%.3f", flags); + ImGui::SliderInt("SliderInt (0 -> 100)", &slider_i, 0, 100, "%d", flags); + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Range Widgets"); + if (ImGui::TreeNode("Range Widgets")) + { + static float begin = 10, end = 90; + static int begin_i = 100, end_i = 1000; + ImGui::DragFloatRange2("range float", &begin, &end, 0.25f, 0.0f, 100.0f, "Min: %.1f %%", "Max: %.1f %%", ImGuiSliderFlags_AlwaysClamp); + ImGui::DragIntRange2("range int", &begin_i, &end_i, 5, 0, 1000, "Min: %d units", "Max: %d units"); + ImGui::DragIntRange2("range int (no bounds)", &begin_i, &end_i, 5, 0, 0, "Min: %d units", "Max: %d units"); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Data Types"); + if (ImGui::TreeNode("Data Types")) + { + // DragScalar/InputScalar/SliderScalar functions allow various data types + // - signed/unsigned + // - 8/16/32/64-bits + // - integer/float/double + // To avoid polluting the public API with all possible combinations, we use the ImGuiDataType enum + // to pass the type, and passing all arguments by pointer. + // This is the reason the test code below creates local variables to hold "zero" "one" etc. for each types. + // In practice, if you frequently use a given type that is not covered by the normal API entry points, + // you can wrap it yourself inside a 1 line function which can take typed argument as value instead of void*, + // and then pass their address to the generic function. For example: + // bool MySliderU64(const char *label, u64* value, u64 min = 0, u64 max = 0, const char* format = "%lld") + // { + // return SliderScalar(label, ImGuiDataType_U64, value, &min, &max, format); + // } + + // Setup limits (as helper variables so we can take their address, as explained above) + // Note: SliderScalar() functions have a maximum usable range of half the natural type maximum, hence the /2. + #ifndef LLONG_MIN + ImS64 LLONG_MIN = -9223372036854775807LL - 1; + ImS64 LLONG_MAX = 9223372036854775807LL; + ImU64 ULLONG_MAX = (2ULL * 9223372036854775807LL + 1); + #endif + const char s8_zero = 0, s8_one = 1, s8_fifty = 50, s8_min = -128, s8_max = 127; + const ImU8 u8_zero = 0, u8_one = 1, u8_fifty = 50, u8_min = 0, u8_max = 255; + const short s16_zero = 0, s16_one = 1, s16_fifty = 50, s16_min = -32768, s16_max = 32767; + const ImU16 u16_zero = 0, u16_one = 1, u16_fifty = 50, u16_min = 0, u16_max = 65535; + const ImS32 s32_zero = 0, s32_one = 1, s32_fifty = 50, s32_min = INT_MIN/2, s32_max = INT_MAX/2, s32_hi_a = INT_MAX/2 - 100, s32_hi_b = INT_MAX/2; + const ImU32 u32_zero = 0, u32_one = 1, u32_fifty = 50, u32_min = 0, u32_max = UINT_MAX/2, u32_hi_a = UINT_MAX/2 - 100, u32_hi_b = UINT_MAX/2; + const ImS64 s64_zero = 0, s64_one = 1, s64_fifty = 50, s64_min = LLONG_MIN/2, s64_max = LLONG_MAX/2, s64_hi_a = LLONG_MAX/2 - 100, s64_hi_b = LLONG_MAX/2; + const ImU64 u64_zero = 0, u64_one = 1, u64_fifty = 50, u64_min = 0, u64_max = ULLONG_MAX/2, u64_hi_a = ULLONG_MAX/2 - 100, u64_hi_b = ULLONG_MAX/2; + const float f32_zero = 0.f, f32_one = 1.f, f32_lo_a = -10000000000.0f, f32_hi_a = +10000000000.0f; + const double f64_zero = 0., f64_one = 1., f64_lo_a = -1000000000000000.0, f64_hi_a = +1000000000000000.0; + + // State + static char s8_v = 127; + static ImU8 u8_v = 255; + static short s16_v = 32767; + static ImU16 u16_v = 65535; + static ImS32 s32_v = -1; + static ImU32 u32_v = (ImU32)-1; + static ImS64 s64_v = -1; + static ImU64 u64_v = (ImU64)-1; + static float f32_v = 0.123f; + static double f64_v = 90000.01234567890123456789; + + const float drag_speed = 0.2f; + static bool drag_clamp = false; + IMGUI_DEMO_MARKER("Widgets/Data Types/Drags"); + ImGui::Text("Drags:"); + ImGui::Checkbox("Clamp integers to 0..50", &drag_clamp); + ImGui::SameLine(); HelpMarker( + "As with every widgets in dear imgui, we never modify values unless there is a user interaction.\n" + "You can override the clamping limits by using CTRL+Click to input a value."); + ImGui::DragScalar("drag s8", ImGuiDataType_S8, &s8_v, drag_speed, drag_clamp ? &s8_zero : NULL, drag_clamp ? &s8_fifty : NULL); + ImGui::DragScalar("drag u8", ImGuiDataType_U8, &u8_v, drag_speed, drag_clamp ? &u8_zero : NULL, drag_clamp ? &u8_fifty : NULL, "%u ms"); + ImGui::DragScalar("drag s16", ImGuiDataType_S16, &s16_v, drag_speed, drag_clamp ? &s16_zero : NULL, drag_clamp ? &s16_fifty : NULL); + ImGui::DragScalar("drag u16", ImGuiDataType_U16, &u16_v, drag_speed, drag_clamp ? &u16_zero : NULL, drag_clamp ? &u16_fifty : NULL, "%u ms"); + ImGui::DragScalar("drag s32", ImGuiDataType_S32, &s32_v, drag_speed, drag_clamp ? &s32_zero : NULL, drag_clamp ? &s32_fifty : NULL); + ImGui::DragScalar("drag u32", ImGuiDataType_U32, &u32_v, drag_speed, drag_clamp ? &u32_zero : NULL, drag_clamp ? &u32_fifty : NULL, "%u ms"); + ImGui::DragScalar("drag s64", ImGuiDataType_S64, &s64_v, drag_speed, drag_clamp ? &s64_zero : NULL, drag_clamp ? &s64_fifty : NULL); + ImGui::DragScalar("drag u64", ImGuiDataType_U64, &u64_v, drag_speed, drag_clamp ? &u64_zero : NULL, drag_clamp ? &u64_fifty : NULL); + ImGui::DragScalar("drag float", ImGuiDataType_Float, &f32_v, 0.005f, &f32_zero, &f32_one, "%f"); + ImGui::DragScalar("drag float log", ImGuiDataType_Float, &f32_v, 0.005f, &f32_zero, &f32_one, "%f", ImGuiSliderFlags_Logarithmic); + ImGui::DragScalar("drag double", ImGuiDataType_Double, &f64_v, 0.0005f, &f64_zero, NULL, "%.10f grams"); + ImGui::DragScalar("drag double log",ImGuiDataType_Double, &f64_v, 0.0005f, &f64_zero, &f64_one, "0 < %.10f < 1", ImGuiSliderFlags_Logarithmic); + + IMGUI_DEMO_MARKER("Widgets/Data Types/Sliders"); + ImGui::Text("Sliders"); + ImGui::SliderScalar("slider s8 full", ImGuiDataType_S8, &s8_v, &s8_min, &s8_max, "%d"); + ImGui::SliderScalar("slider u8 full", ImGuiDataType_U8, &u8_v, &u8_min, &u8_max, "%u"); + ImGui::SliderScalar("slider s16 full", ImGuiDataType_S16, &s16_v, &s16_min, &s16_max, "%d"); + ImGui::SliderScalar("slider u16 full", ImGuiDataType_U16, &u16_v, &u16_min, &u16_max, "%u"); + ImGui::SliderScalar("slider s32 low", ImGuiDataType_S32, &s32_v, &s32_zero, &s32_fifty,"%d"); + ImGui::SliderScalar("slider s32 high", ImGuiDataType_S32, &s32_v, &s32_hi_a, &s32_hi_b, "%d"); + ImGui::SliderScalar("slider s32 full", ImGuiDataType_S32, &s32_v, &s32_min, &s32_max, "%d"); + ImGui::SliderScalar("slider u32 low", ImGuiDataType_U32, &u32_v, &u32_zero, &u32_fifty,"%u"); + ImGui::SliderScalar("slider u32 high", ImGuiDataType_U32, &u32_v, &u32_hi_a, &u32_hi_b, "%u"); + ImGui::SliderScalar("slider u32 full", ImGuiDataType_U32, &u32_v, &u32_min, &u32_max, "%u"); + ImGui::SliderScalar("slider s64 low", ImGuiDataType_S64, &s64_v, &s64_zero, &s64_fifty,"%" IM_PRId64); + ImGui::SliderScalar("slider s64 high", ImGuiDataType_S64, &s64_v, &s64_hi_a, &s64_hi_b, "%" IM_PRId64); + ImGui::SliderScalar("slider s64 full", ImGuiDataType_S64, &s64_v, &s64_min, &s64_max, "%" IM_PRId64); + ImGui::SliderScalar("slider u64 low", ImGuiDataType_U64, &u64_v, &u64_zero, &u64_fifty,"%" IM_PRIu64 " ms"); + ImGui::SliderScalar("slider u64 high", ImGuiDataType_U64, &u64_v, &u64_hi_a, &u64_hi_b, "%" IM_PRIu64 " ms"); + ImGui::SliderScalar("slider u64 full", ImGuiDataType_U64, &u64_v, &u64_min, &u64_max, "%" IM_PRIu64 " ms"); + ImGui::SliderScalar("slider float low", ImGuiDataType_Float, &f32_v, &f32_zero, &f32_one); + ImGui::SliderScalar("slider float low log", ImGuiDataType_Float, &f32_v, &f32_zero, &f32_one, "%.10f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderScalar("slider float high", ImGuiDataType_Float, &f32_v, &f32_lo_a, &f32_hi_a, "%e"); + ImGui::SliderScalar("slider double low", ImGuiDataType_Double, &f64_v, &f64_zero, &f64_one, "%.10f grams"); + ImGui::SliderScalar("slider double low log",ImGuiDataType_Double, &f64_v, &f64_zero, &f64_one, "%.10f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderScalar("slider double high", ImGuiDataType_Double, &f64_v, &f64_lo_a, &f64_hi_a, "%e grams"); + + ImGui::Text("Sliders (reverse)"); + ImGui::SliderScalar("slider s8 reverse", ImGuiDataType_S8, &s8_v, &s8_max, &s8_min, "%d"); + ImGui::SliderScalar("slider u8 reverse", ImGuiDataType_U8, &u8_v, &u8_max, &u8_min, "%u"); + ImGui::SliderScalar("slider s32 reverse", ImGuiDataType_S32, &s32_v, &s32_fifty, &s32_zero, "%d"); + ImGui::SliderScalar("slider u32 reverse", ImGuiDataType_U32, &u32_v, &u32_fifty, &u32_zero, "%u"); + ImGui::SliderScalar("slider s64 reverse", ImGuiDataType_S64, &s64_v, &s64_fifty, &s64_zero, "%" IM_PRId64); + ImGui::SliderScalar("slider u64 reverse", ImGuiDataType_U64, &u64_v, &u64_fifty, &u64_zero, "%" IM_PRIu64 " ms"); + + IMGUI_DEMO_MARKER("Widgets/Data Types/Inputs"); + static bool inputs_step = true; + ImGui::Text("Inputs"); + ImGui::Checkbox("Show step buttons", &inputs_step); + ImGui::InputScalar("input s8", ImGuiDataType_S8, &s8_v, inputs_step ? &s8_one : NULL, NULL, "%d"); + ImGui::InputScalar("input u8", ImGuiDataType_U8, &u8_v, inputs_step ? &u8_one : NULL, NULL, "%u"); + ImGui::InputScalar("input s16", ImGuiDataType_S16, &s16_v, inputs_step ? &s16_one : NULL, NULL, "%d"); + ImGui::InputScalar("input u16", ImGuiDataType_U16, &u16_v, inputs_step ? &u16_one : NULL, NULL, "%u"); + ImGui::InputScalar("input s32", ImGuiDataType_S32, &s32_v, inputs_step ? &s32_one : NULL, NULL, "%d"); + ImGui::InputScalar("input s32 hex", ImGuiDataType_S32, &s32_v, inputs_step ? &s32_one : NULL, NULL, "%08X", ImGuiInputTextFlags_CharsHexadecimal); + ImGui::InputScalar("input u32", ImGuiDataType_U32, &u32_v, inputs_step ? &u32_one : NULL, NULL, "%u"); + ImGui::InputScalar("input u32 hex", ImGuiDataType_U32, &u32_v, inputs_step ? &u32_one : NULL, NULL, "%08X", ImGuiInputTextFlags_CharsHexadecimal); + ImGui::InputScalar("input s64", ImGuiDataType_S64, &s64_v, inputs_step ? &s64_one : NULL); + ImGui::InputScalar("input u64", ImGuiDataType_U64, &u64_v, inputs_step ? &u64_one : NULL); + ImGui::InputScalar("input float", ImGuiDataType_Float, &f32_v, inputs_step ? &f32_one : NULL); + ImGui::InputScalar("input double", ImGuiDataType_Double, &f64_v, inputs_step ? &f64_one : NULL); + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Multi-component Widgets"); + if (ImGui::TreeNode("Multi-component Widgets")) + { + static float vec4f[4] = { 0.10f, 0.20f, 0.30f, 0.44f }; + static int vec4i[4] = { 1, 5, 100, 255 }; + + ImGui::InputFloat2("input float2", vec4f); + ImGui::DragFloat2("drag float2", vec4f, 0.01f, 0.0f, 1.0f); + ImGui::SliderFloat2("slider float2", vec4f, 0.0f, 1.0f); + ImGui::InputInt2("input int2", vec4i); + ImGui::DragInt2("drag int2", vec4i, 1, 0, 255); + ImGui::SliderInt2("slider int2", vec4i, 0, 255); + ImGui::Spacing(); + + ImGui::InputFloat3("input float3", vec4f); + ImGui::DragFloat3("drag float3", vec4f, 0.01f, 0.0f, 1.0f); + ImGui::SliderFloat3("slider float3", vec4f, 0.0f, 1.0f); + ImGui::InputInt3("input int3", vec4i); + ImGui::DragInt3("drag int3", vec4i, 1, 0, 255); + ImGui::SliderInt3("slider int3", vec4i, 0, 255); + ImGui::Spacing(); + + ImGui::InputFloat4("input float4", vec4f); + ImGui::DragFloat4("drag float4", vec4f, 0.01f, 0.0f, 1.0f); + ImGui::SliderFloat4("slider float4", vec4f, 0.0f, 1.0f); + ImGui::InputInt4("input int4", vec4i); + ImGui::DragInt4("drag int4", vec4i, 1, 0, 255); + ImGui::SliderInt4("slider int4", vec4i, 0, 255); + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Vertical Sliders"); + if (ImGui::TreeNode("Vertical Sliders")) + { + const float spacing = 4; + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing)); + + static int int_value = 0; + ImGui::VSliderInt("##int", ImVec2(18, 160), &int_value, 0, 5); + ImGui::SameLine(); + + static float values[7] = { 0.0f, 0.60f, 0.35f, 0.9f, 0.70f, 0.20f, 0.0f }; + ImGui::PushID("set1"); + for (int i = 0; i < 7; i++) + { + if (i > 0) ImGui::SameLine(); + ImGui::PushID(i); + ImGui::PushStyleColor(ImGuiCol_FrameBg, (ImVec4)ImColor::HSV(i / 7.0f, 0.5f, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, (ImVec4)ImColor::HSV(i / 7.0f, 0.6f, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, (ImVec4)ImColor::HSV(i / 7.0f, 0.7f, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_SliderGrab, (ImVec4)ImColor::HSV(i / 7.0f, 0.9f, 0.9f)); + ImGui::VSliderFloat("##v", ImVec2(18, 160), &values[i], 0.0f, 1.0f, ""); + if (ImGui::IsItemActive() || ImGui::IsItemHovered()) + ImGui::SetTooltip("%.3f", values[i]); + ImGui::PopStyleColor(4); + ImGui::PopID(); + } + ImGui::PopID(); + + ImGui::SameLine(); + ImGui::PushID("set2"); + static float values2[4] = { 0.20f, 0.80f, 0.40f, 0.25f }; + const int rows = 3; + const ImVec2 small_slider_size(18, (float)(int)((160.0f - (rows - 1) * spacing) / rows)); + for (int nx = 0; nx < 4; nx++) + { + if (nx > 0) ImGui::SameLine(); + ImGui::BeginGroup(); + for (int ny = 0; ny < rows; ny++) + { + ImGui::PushID(nx * rows + ny); + ImGui::VSliderFloat("##v", small_slider_size, &values2[nx], 0.0f, 1.0f, ""); + if (ImGui::IsItemActive() || ImGui::IsItemHovered()) + ImGui::SetTooltip("%.3f", values2[nx]); + ImGui::PopID(); + } + ImGui::EndGroup(); + } + ImGui::PopID(); + + ImGui::SameLine(); + ImGui::PushID("set3"); + for (int i = 0; i < 4; i++) + { + if (i > 0) ImGui::SameLine(); + ImGui::PushID(i); + ImGui::PushStyleVar(ImGuiStyleVar_GrabMinSize, 40); + ImGui::VSliderFloat("##v", ImVec2(40, 160), &values[i], 0.0f, 1.0f, "%.2f\nsec"); + ImGui::PopStyleVar(); + ImGui::PopID(); + } + ImGui::PopID(); + ImGui::PopStyleVar(); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Drag and drop"); + if (ImGui::TreeNode("Drag and Drop")) + { + IMGUI_DEMO_MARKER("Widgets/Drag and drop/Standard widgets"); + if (ImGui::TreeNode("Drag and drop in standard widgets")) + { + // ColorEdit widgets automatically act as drag source and drag target. + // They are using standardized payload strings IMGUI_PAYLOAD_TYPE_COLOR_3F and IMGUI_PAYLOAD_TYPE_COLOR_4F + // to allow your own widgets to use colors in their drag and drop interaction. + // Also see 'Demo->Widgets->Color/Picker Widgets->Palette' demo. + HelpMarker("You can drag from the color squares."); + static float col1[3] = { 1.0f, 0.0f, 0.2f }; + static float col2[4] = { 0.4f, 0.7f, 0.0f, 0.5f }; + ImGui::ColorEdit3("color 1", col1); + ImGui::ColorEdit4("color 2", col2); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Drag and drop/Copy-swap items"); + if (ImGui::TreeNode("Drag and drop to copy/swap items")) + { + enum Mode + { + Mode_Copy, + Mode_Move, + Mode_Swap + }; + static int mode = 0; + if (ImGui::RadioButton("Copy", mode == Mode_Copy)) { mode = Mode_Copy; } ImGui::SameLine(); + if (ImGui::RadioButton("Move", mode == Mode_Move)) { mode = Mode_Move; } ImGui::SameLine(); + if (ImGui::RadioButton("Swap", mode == Mode_Swap)) { mode = Mode_Swap; } + static const char* names[9] = + { + "Bobby", "Beatrice", "Betty", + "Brianna", "Barry", "Bernard", + "Bibi", "Blaine", "Bryn" + }; + for (int n = 0; n < IM_ARRAYSIZE(names); n++) + { + ImGui::PushID(n); + if ((n % 3) != 0) + ImGui::SameLine(); + ImGui::Button(names[n], ImVec2(60, 60)); + + // Our buttons are both drag sources and drag targets here! + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) + { + // Set payload to carry the index of our item (could be anything) + ImGui::SetDragDropPayload("DND_DEMO_CELL", &n, sizeof(int)); + + // Display preview (could be anything, e.g. when dragging an image we could decide to display + // the filename and a small preview of the image, etc.) + if (mode == Mode_Copy) { ImGui::Text("Copy %s", names[n]); } + if (mode == Mode_Move) { ImGui::Text("Move %s", names[n]); } + if (mode == Mode_Swap) { ImGui::Text("Swap %s", names[n]); } + ImGui::EndDragDropSource(); + } + if (ImGui::BeginDragDropTarget()) + { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("DND_DEMO_CELL")) + { + IM_ASSERT(payload->DataSize == sizeof(int)); + int payload_n = *(const int*)payload->Data; + if (mode == Mode_Copy) + { + names[n] = names[payload_n]; + } + if (mode == Mode_Move) + { + names[n] = names[payload_n]; + names[payload_n] = ""; + } + if (mode == Mode_Swap) + { + const char* tmp = names[n]; + names[n] = names[payload_n]; + names[payload_n] = tmp; + } + } + ImGui::EndDragDropTarget(); + } + ImGui::PopID(); + } + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Drag and Drop/Drag to reorder items (simple)"); + if (ImGui::TreeNode("Drag to reorder items (simple)")) + { + // Simple reordering + HelpMarker( + "We don't use the drag and drop api at all here! " + "Instead we query when the item is held but not hovered, and order items accordingly."); + static const char* item_names[] = { "Item One", "Item Two", "Item Three", "Item Four", "Item Five" }; + for (int n = 0; n < IM_ARRAYSIZE(item_names); n++) + { + const char* item = item_names[n]; + ImGui::Selectable(item); + + if (ImGui::IsItemActive() && !ImGui::IsItemHovered()) + { + int n_next = n + (ImGui::GetMouseDragDelta(0).y < 0.f ? -1 : 1); + if (n_next >= 0 && n_next < IM_ARRAYSIZE(item_names)) + { + item_names[n] = item_names[n_next]; + item_names[n_next] = item; + ImGui::ResetMouseDragDelta(); + } + } + } + ImGui::TreePop(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Querying Item Status (Edited,Active,Hovered etc.)"); + if (ImGui::TreeNode("Querying Item Status (Edited/Active/Hovered etc.)")) + { + // Select an item type + const char* item_names[] = + { + "Text", "Button", "Button (w/ repeat)", "Checkbox", "SliderFloat", "InputText", "InputTextMultiline", "InputFloat", + "InputFloat3", "ColorEdit4", "Selectable", "MenuItem", "TreeNode", "TreeNode (w/ double-click)", "Combo", "ListBox" + }; + static int item_type = 4; + static bool item_disabled = false; + ImGui::Combo("Item Type", &item_type, item_names, IM_ARRAYSIZE(item_names), IM_ARRAYSIZE(item_names)); + ImGui::SameLine(); + HelpMarker("Testing how various types of items are interacting with the IsItemXXX functions. Note that the bool return value of most ImGui function is generally equivalent to calling ImGui::IsItemHovered()."); + ImGui::Checkbox("Item Disabled", &item_disabled); + + // Submit selected item item so we can query their status in the code following it. + bool ret = false; + static bool b = false; + static float col4f[4] = { 1.0f, 0.5, 0.0f, 1.0f }; + static char str[16] = {}; + if (item_disabled) + ImGui::BeginDisabled(true); + if (item_type == 0) { ImGui::Text("ITEM: Text"); } // Testing text items with no identifier/interaction + if (item_type == 1) { ret = ImGui::Button("ITEM: Button"); } // Testing button + if (item_type == 2) { ImGui::PushButtonRepeat(true); ret = ImGui::Button("ITEM: Button"); ImGui::PopButtonRepeat(); } // Testing button (with repeater) + if (item_type == 3) { ret = ImGui::Checkbox("ITEM: Checkbox", &b); } // Testing checkbox + if (item_type == 4) { ret = ImGui::SliderFloat("ITEM: SliderFloat", &col4f[0], 0.0f, 1.0f); } // Testing basic item + if (item_type == 5) { ret = ImGui::InputText("ITEM: InputText", &str[0], IM_ARRAYSIZE(str)); } // Testing input text (which handles tabbing) + if (item_type == 6) { ret = ImGui::InputTextMultiline("ITEM: InputTextMultiline", &str[0], IM_ARRAYSIZE(str)); } // Testing input text (which uses a child window) + if (item_type == 7) { ret = ImGui::InputFloat("ITEM: InputFloat", col4f, 1.0f); } // Testing +/- buttons on scalar input + if (item_type == 8) { ret = ImGui::InputFloat3("ITEM: InputFloat3", col4f); } // Testing multi-component items (IsItemXXX flags are reported merged) + if (item_type == 9) { ret = ImGui::ColorEdit4("ITEM: ColorEdit4", col4f); } // Testing multi-component items (IsItemXXX flags are reported merged) + if (item_type == 10){ ret = ImGui::Selectable("ITEM: Selectable"); } // Testing selectable item + if (item_type == 11){ ret = ImGui::MenuItem("ITEM: MenuItem"); } // Testing menu item (they use ImGuiButtonFlags_PressedOnRelease button policy) + if (item_type == 12){ ret = ImGui::TreeNode("ITEM: TreeNode"); if (ret) ImGui::TreePop(); } // Testing tree node + if (item_type == 13){ ret = ImGui::TreeNodeEx("ITEM: TreeNode w/ ImGuiTreeNodeFlags_OpenOnDoubleClick", ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_NoTreePushOnOpen); } // Testing tree node with ImGuiButtonFlags_PressedOnDoubleClick button policy. + if (item_type == 14){ const char* items[] = { "Apple", "Banana", "Cherry", "Kiwi" }; static int current = 1; ret = ImGui::Combo("ITEM: Combo", ¤t, items, IM_ARRAYSIZE(items)); } + if (item_type == 15){ const char* items[] = { "Apple", "Banana", "Cherry", "Kiwi" }; static int current = 1; ret = ImGui::ListBox("ITEM: ListBox", ¤t, items, IM_ARRAYSIZE(items), IM_ARRAYSIZE(items)); } + + // Display the values of IsItemHovered() and other common item state functions. + // Note that the ImGuiHoveredFlags_XXX flags can be combined. + // Because BulletText is an item itself and that would affect the output of IsItemXXX functions, + // we query every state in a single call to avoid storing them and to simplify the code. + ImGui::BulletText( + "Return value = %d\n" + "IsItemFocused() = %d\n" + "IsItemHovered() = %d\n" + "IsItemHovered(_AllowWhenBlockedByPopup) = %d\n" + "IsItemHovered(_AllowWhenBlockedByActiveItem) = %d\n" + "IsItemHovered(_AllowWhenOverlapped) = %d\n" + "IsItemHovered(_AllowWhenDisabled) = %d\n" + "IsItemHovered(_RectOnly) = %d\n" + "IsItemActive() = %d\n" + "IsItemEdited() = %d\n" + "IsItemActivated() = %d\n" + "IsItemDeactivated() = %d\n" + "IsItemDeactivatedAfterEdit() = %d\n" + "IsItemVisible() = %d\n" + "IsItemClicked() = %d\n" + "IsItemToggledOpen() = %d\n" + "GetItemRectMin() = (%.1f, %.1f)\n" + "GetItemRectMax() = (%.1f, %.1f)\n" + "GetItemRectSize() = (%.1f, %.1f)", + ret, + ImGui::IsItemFocused(), + ImGui::IsItemHovered(), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlapped), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled), + ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly), + ImGui::IsItemActive(), + ImGui::IsItemEdited(), + ImGui::IsItemActivated(), + ImGui::IsItemDeactivated(), + ImGui::IsItemDeactivatedAfterEdit(), + ImGui::IsItemVisible(), + ImGui::IsItemClicked(), + ImGui::IsItemToggledOpen(), + ImGui::GetItemRectMin().x, ImGui::GetItemRectMin().y, + ImGui::GetItemRectMax().x, ImGui::GetItemRectMax().y, + ImGui::GetItemRectSize().x, ImGui::GetItemRectSize().y + ); + + if (item_disabled) + ImGui::EndDisabled(); + + char buf[1] = ""; + ImGui::InputText("unused", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_ReadOnly); + ImGui::SameLine(); + HelpMarker("This widget is only here to be able to tab-out of the widgets above and see e.g. Deactivated() status."); + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Querying Window Status (Focused,Hovered etc.)"); + if (ImGui::TreeNode("Querying Window Status (Focused/Hovered etc.)")) + { + static bool embed_all_inside_a_child_window = false; + ImGui::Checkbox("Embed everything inside a child window for testing _RootWindow flag.", &embed_all_inside_a_child_window); + if (embed_all_inside_a_child_window) + ImGui::BeginChild("outer_child", ImVec2(0, ImGui::GetFontSize() * 20.0f), true); + + // Testing IsWindowFocused() function with its various flags. + ImGui::BulletText( + "IsWindowFocused() = %d\n" + "IsWindowFocused(_ChildWindows) = %d\n" + "IsWindowFocused(_ChildWindows|_NoPopupHierarchy) = %d\n" + "IsWindowFocused(_ChildWindows|_RootWindow) = %d\n" + "IsWindowFocused(_ChildWindows|_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowFocused(_RootWindow) = %d\n" + "IsWindowFocused(_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowFocused(_AnyWindow) = %d\n", + ImGui::IsWindowFocused(), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_NoPopupHierarchy), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_RootWindow), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_NoPopupHierarchy), + ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow), + ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_NoPopupHierarchy), + ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)); + + // Testing IsWindowHovered() function with its various flags. + ImGui::BulletText( + "IsWindowHovered() = %d\n" + "IsWindowHovered(_AllowWhenBlockedByPopup) = %d\n" + "IsWindowHovered(_AllowWhenBlockedByActiveItem) = %d\n" + "IsWindowHovered(_ChildWindows) = %d\n" + "IsWindowHovered(_ChildWindows|_NoPopupHierarchy) = %d\n" + "IsWindowHovered(_ChildWindows|_RootWindow) = %d\n" + "IsWindowHovered(_ChildWindows|_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowHovered(_RootWindow) = %d\n" + "IsWindowHovered(_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowHovered(_ChildWindows|_AllowWhenBlockedByPopup) = %d\n" + "IsWindowHovered(_AnyWindow) = %d\n", + ImGui::IsWindowHovered(), + ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup), + ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_NoPopupHierarchy), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_NoPopupHierarchy), + ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow), + ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_NoPopupHierarchy), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByPopup), + ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)); + + ImGui::BeginChild("child", ImVec2(0, 50), true); + ImGui::Text("This is another child window for testing the _ChildWindows flag."); + ImGui::EndChild(); + if (embed_all_inside_a_child_window) + ImGui::EndChild(); + + // Calling IsItemHovered() after begin returns the hovered status of the title bar. + // This is useful in particular if you want to create a context menu associated to the title bar of a window. + static bool test_window = false; + ImGui::Checkbox("Hovered/Active tests after Begin() for title bar testing", &test_window); + if (test_window) + { + ImGui::Begin("Title bar Hovered/Active tests", &test_window); + if (ImGui::BeginPopupContextItem()) // <-- This is using IsItemHovered() + { + if (ImGui::MenuItem("Close")) { test_window = false; } + ImGui::EndPopup(); + } + ImGui::Text( + "IsItemHovered() after begin = %d (== is title bar hovered)\n" + "IsItemActive() after begin = %d (== is window being clicked/moved)\n", + ImGui::IsItemHovered(), ImGui::IsItemActive()); + ImGui::End(); + } + + ImGui::TreePop(); + } + + // Demonstrate BeginDisabled/EndDisabled using a checkbox located at the bottom of the section (which is a bit odd: + // logically we'd have this checkbox at the top of the section, but we don't want this feature to steal that space) + if (disable_all) + ImGui::EndDisabled(); + + IMGUI_DEMO_MARKER("Widgets/Disable Block"); + if (ImGui::TreeNode("Disable block")) + { + ImGui::Checkbox("Disable entire section above", &disable_all); + ImGui::SameLine(); HelpMarker("Demonstrate using BeginDisabled()/EndDisabled() across this section."); + ImGui::TreePop(); + } +} + +static void ShowDemoWindowLayout() +{ + IMGUI_DEMO_MARKER("Layout"); + if (!ImGui::CollapsingHeader("Layout & Scrolling")) + return; + + IMGUI_DEMO_MARKER("Layout/Child windows"); + if (ImGui::TreeNode("Child windows")) + { + HelpMarker("Use child windows to begin into a self-contained independent scrolling/clipping regions within a host window."); + static bool disable_mouse_wheel = false; + static bool disable_menu = false; + ImGui::Checkbox("Disable Mouse Wheel", &disable_mouse_wheel); + ImGui::Checkbox("Disable Menu", &disable_menu); + + // Child 1: no border, enable horizontal scrollbar + { + ImGuiWindowFlags window_flags = ImGuiWindowFlags_HorizontalScrollbar; + if (disable_mouse_wheel) + window_flags |= ImGuiWindowFlags_NoScrollWithMouse; + ImGui::BeginChild("ChildL", ImVec2(ImGui::GetContentRegionAvail().x * 0.5f, 260), false, window_flags); + for (int i = 0; i < 100; i++) + ImGui::Text("%04d: scrollable region", i); + ImGui::EndChild(); + } + + ImGui::SameLine(); + + // Child 2: rounded border + { + ImGuiWindowFlags window_flags = ImGuiWindowFlags_None; + if (disable_mouse_wheel) + window_flags |= ImGuiWindowFlags_NoScrollWithMouse; + if (!disable_menu) + window_flags |= ImGuiWindowFlags_MenuBar; + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f); + ImGui::BeginChild("ChildR", ImVec2(0, 260), true, window_flags); + if (!disable_menu && ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Menu")) + { + ShowExampleMenuFile(); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + if (ImGui::BeginTable("split", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings)) + { + for (int i = 0; i < 100; i++) + { + char buf[32]; + sprintf(buf, "%03d", i); + ImGui::TableNextColumn(); + ImGui::Button(buf, ImVec2(-FLT_MIN, 0.0f)); + } + ImGui::EndTable(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + } + + ImGui::Separator(); + + // Demonstrate a few extra things + // - Changing ImGuiCol_ChildBg (which is transparent black in default styles) + // - Using SetCursorPos() to position child window (the child window is an item from the POV of parent window) + // You can also call SetNextWindowPos() to position the child window. The parent window will effectively + // layout from this position. + // - Using ImGui::GetItemRectMin/Max() to query the "item" state (because the child window is an item from + // the POV of the parent window). See 'Demo->Querying Status (Edited/Active/Hovered etc.)' for details. + { + static int offset_x = 0; + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::DragInt("Offset X", &offset_x, 1.0f, -1000, 1000); + + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (float)offset_x); + ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(255, 0, 0, 100)); + ImGui::BeginChild("Red", ImVec2(200, 100), true, ImGuiWindowFlags_None); + for (int n = 0; n < 50; n++) + ImGui::Text("Some test %d", n); + ImGui::EndChild(); + bool child_is_hovered = ImGui::IsItemHovered(); + ImVec2 child_rect_min = ImGui::GetItemRectMin(); + ImVec2 child_rect_max = ImGui::GetItemRectMax(); + ImGui::PopStyleColor(); + ImGui::Text("Hovered: %d", child_is_hovered); + ImGui::Text("Rect of child window is: (%.0f,%.0f) (%.0f,%.0f)", child_rect_min.x, child_rect_min.y, child_rect_max.x, child_rect_max.y); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Layout/Widgets Width"); + if (ImGui::TreeNode("Widgets Width")) + { + static float f = 0.0f; + static bool show_indented_items = true; + ImGui::Checkbox("Show indented items", &show_indented_items); + + // Use SetNextItemWidth() to set the width of a single upcoming item. + // Use PushItemWidth()/PopItemWidth() to set the width of a group of items. + // In real code use you'll probably want to choose width values that are proportional to your font size + // e.g. Using '20.0f * GetFontSize()' as width instead of '200.0f', etc. + + ImGui::Text("SetNextItemWidth/PushItemWidth(100)"); + ImGui::SameLine(); HelpMarker("Fixed width."); + ImGui::PushItemWidth(100); + ImGui::DragFloat("float##1b", &f); + if (show_indented_items) + { + ImGui::Indent(); + ImGui::DragFloat("float (indented)##1b", &f); + ImGui::Unindent(); + } + ImGui::PopItemWidth(); + + ImGui::Text("SetNextItemWidth/PushItemWidth(-100)"); + ImGui::SameLine(); HelpMarker("Align to right edge minus 100"); + ImGui::PushItemWidth(-100); + ImGui::DragFloat("float##2a", &f); + if (show_indented_items) + { + ImGui::Indent(); + ImGui::DragFloat("float (indented)##2b", &f); + ImGui::Unindent(); + } + ImGui::PopItemWidth(); + + ImGui::Text("SetNextItemWidth/PushItemWidth(GetContentRegionAvail().x * 0.5f)"); + ImGui::SameLine(); HelpMarker("Half of available width.\n(~ right-cursor_pos)\n(works within a column set)"); + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x * 0.5f); + ImGui::DragFloat("float##3a", &f); + if (show_indented_items) + { + ImGui::Indent(); + ImGui::DragFloat("float (indented)##3b", &f); + ImGui::Unindent(); + } + ImGui::PopItemWidth(); + + ImGui::Text("SetNextItemWidth/PushItemWidth(-GetContentRegionAvail().x * 0.5f)"); + ImGui::SameLine(); HelpMarker("Align to right edge minus half"); + ImGui::PushItemWidth(-ImGui::GetContentRegionAvail().x * 0.5f); + ImGui::DragFloat("float##4a", &f); + if (show_indented_items) + { + ImGui::Indent(); + ImGui::DragFloat("float (indented)##4b", &f); + ImGui::Unindent(); + } + ImGui::PopItemWidth(); + + // Demonstrate using PushItemWidth to surround three items. + // Calling SetNextItemWidth() before each of them would have the same effect. + ImGui::Text("SetNextItemWidth/PushItemWidth(-FLT_MIN)"); + ImGui::SameLine(); HelpMarker("Align to right edge"); + ImGui::PushItemWidth(-FLT_MIN); + ImGui::DragFloat("##float5a", &f); + if (show_indented_items) + { + ImGui::Indent(); + ImGui::DragFloat("float (indented)##5b", &f); + ImGui::Unindent(); + } + ImGui::PopItemWidth(); + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Layout/Basic Horizontal Layout"); + if (ImGui::TreeNode("Basic Horizontal Layout")) + { + ImGui::TextWrapped("(Use ImGui::SameLine() to keep adding items to the right of the preceding item)"); + + // Text + IMGUI_DEMO_MARKER("Layout/Basic Horizontal Layout/SameLine"); + ImGui::Text("Two items: Hello"); ImGui::SameLine(); + ImGui::TextColored(ImVec4(1,1,0,1), "Sailor"); + + // Adjust spacing + ImGui::Text("More spacing: Hello"); ImGui::SameLine(0, 20); + ImGui::TextColored(ImVec4(1,1,0,1), "Sailor"); + + // Button + ImGui::AlignTextToFramePadding(); + ImGui::Text("Normal buttons"); ImGui::SameLine(); + ImGui::Button("Banana"); ImGui::SameLine(); + ImGui::Button("Apple"); ImGui::SameLine(); + ImGui::Button("Corniflower"); + + // Button + ImGui::Text("Small buttons"); ImGui::SameLine(); + ImGui::SmallButton("Like this one"); ImGui::SameLine(); + ImGui::Text("can fit within a text block."); + + // Aligned to arbitrary position. Easy/cheap column. + IMGUI_DEMO_MARKER("Layout/Basic Horizontal Layout/SameLine (with offset)"); + ImGui::Text("Aligned"); + ImGui::SameLine(150); ImGui::Text("x=150"); + ImGui::SameLine(300); ImGui::Text("x=300"); + ImGui::Text("Aligned"); + ImGui::SameLine(150); ImGui::SmallButton("x=150"); + ImGui::SameLine(300); ImGui::SmallButton("x=300"); + + // Checkbox + IMGUI_DEMO_MARKER("Layout/Basic Horizontal Layout/SameLine (more)"); + static bool c1 = false, c2 = false, c3 = false, c4 = false; + ImGui::Checkbox("My", &c1); ImGui::SameLine(); + ImGui::Checkbox("Tailor", &c2); ImGui::SameLine(); + ImGui::Checkbox("Is", &c3); ImGui::SameLine(); + ImGui::Checkbox("Rich", &c4); + + // Various + static float f0 = 1.0f, f1 = 2.0f, f2 = 3.0f; + ImGui::PushItemWidth(80); + const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD" }; + static int item = -1; + ImGui::Combo("Combo", &item, items, IM_ARRAYSIZE(items)); ImGui::SameLine(); + ImGui::SliderFloat("X", &f0, 0.0f, 5.0f); ImGui::SameLine(); + ImGui::SliderFloat("Y", &f1, 0.0f, 5.0f); ImGui::SameLine(); + ImGui::SliderFloat("Z", &f2, 0.0f, 5.0f); + ImGui::PopItemWidth(); + + ImGui::PushItemWidth(80); + ImGui::Text("Lists:"); + static int selection[4] = { 0, 1, 2, 3 }; + for (int i = 0; i < 4; i++) + { + if (i > 0) ImGui::SameLine(); + ImGui::PushID(i); + ImGui::ListBox("", &selection[i], items, IM_ARRAYSIZE(items)); + ImGui::PopID(); + //if (ImGui::IsItemHovered()) ImGui::SetTooltip("ListBox %d hovered", i); + } + ImGui::PopItemWidth(); + + // Dummy + IMGUI_DEMO_MARKER("Layout/Basic Horizontal Layout/Dummy"); + ImVec2 button_sz(40, 40); + ImGui::Button("A", button_sz); ImGui::SameLine(); + ImGui::Dummy(button_sz); ImGui::SameLine(); + ImGui::Button("B", button_sz); + + // Manually wrapping + // (we should eventually provide this as an automatic layout feature, but for now you can do it manually) + IMGUI_DEMO_MARKER("Layout/Basic Horizontal Layout/Manual wrapping"); + ImGui::Text("Manual wrapping:"); + ImGuiStyle& style = ImGui::GetStyle(); + int buttons_count = 20; + float window_visible_x2 = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x; + for (int n = 0; n < buttons_count; n++) + { + ImGui::PushID(n); + ImGui::Button("Box", button_sz); + float last_button_x2 = ImGui::GetItemRectMax().x; + float next_button_x2 = last_button_x2 + style.ItemSpacing.x + button_sz.x; // Expected position if next button was on same line + if (n + 1 < buttons_count && next_button_x2 < window_visible_x2) + ImGui::SameLine(); + ImGui::PopID(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Layout/Groups"); + if (ImGui::TreeNode("Groups")) + { + HelpMarker( + "BeginGroup() basically locks the horizontal position for new line. " + "EndGroup() bundles the whole group so that you can use \"item\" functions such as " + "IsItemHovered()/IsItemActive() or SameLine() etc. on the whole group."); + ImGui::BeginGroup(); + { + ImGui::BeginGroup(); + ImGui::Button("AAA"); + ImGui::SameLine(); + ImGui::Button("BBB"); + ImGui::SameLine(); + ImGui::BeginGroup(); + ImGui::Button("CCC"); + ImGui::Button("DDD"); + ImGui::EndGroup(); + ImGui::SameLine(); + ImGui::Button("EEE"); + ImGui::EndGroup(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("First group hovered"); + } + // Capture the group size and create widgets using the same size + ImVec2 size = ImGui::GetItemRectSize(); + const float values[5] = { 0.5f, 0.20f, 0.80f, 0.60f, 0.25f }; + ImGui::PlotHistogram("##values", values, IM_ARRAYSIZE(values), 0, NULL, 0.0f, 1.0f, size); + + ImGui::Button("ACTION", ImVec2((size.x - ImGui::GetStyle().ItemSpacing.x) * 0.5f, size.y)); + ImGui::SameLine(); + ImGui::Button("REACTION", ImVec2((size.x - ImGui::GetStyle().ItemSpacing.x) * 0.5f, size.y)); + ImGui::EndGroup(); + ImGui::SameLine(); + + ImGui::Button("LEVERAGE\nBUZZWORD", size); + ImGui::SameLine(); + + if (ImGui::BeginListBox("List", size)) + { + ImGui::Selectable("Selected", true); + ImGui::Selectable("Not Selected", false); + ImGui::EndListBox(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Layout/Text Baseline Alignment"); + if (ImGui::TreeNode("Text Baseline Alignment")) + { + { + ImGui::BulletText("Text baseline:"); + ImGui::SameLine(); HelpMarker( + "This is testing the vertical alignment that gets applied on text to keep it aligned with widgets. " + "Lines only composed of text or \"small\" widgets use less vertical space than lines with framed widgets."); + ImGui::Indent(); + + ImGui::Text("KO Blahblah"); ImGui::SameLine(); + ImGui::Button("Some framed item"); ImGui::SameLine(); + HelpMarker("Baseline of button will look misaligned with text.."); + + // If your line starts with text, call AlignTextToFramePadding() to align text to upcoming widgets. + // (because we don't know what's coming after the Text() statement, we need to move the text baseline + // down by FramePadding.y ahead of time) + ImGui::AlignTextToFramePadding(); + ImGui::Text("OK Blahblah"); ImGui::SameLine(); + ImGui::Button("Some framed item"); ImGui::SameLine(); + HelpMarker("We call AlignTextToFramePadding() to vertically align the text baseline by +FramePadding.y"); + + // SmallButton() uses the same vertical padding as Text + ImGui::Button("TEST##1"); ImGui::SameLine(); + ImGui::Text("TEST"); ImGui::SameLine(); + ImGui::SmallButton("TEST##2"); + + // If your line starts with text, call AlignTextToFramePadding() to align text to upcoming widgets. + ImGui::AlignTextToFramePadding(); + ImGui::Text("Text aligned to framed item"); ImGui::SameLine(); + ImGui::Button("Item##1"); ImGui::SameLine(); + ImGui::Text("Item"); ImGui::SameLine(); + ImGui::SmallButton("Item##2"); ImGui::SameLine(); + ImGui::Button("Item##3"); + + ImGui::Unindent(); + } + + ImGui::Spacing(); + + { + ImGui::BulletText("Multi-line text:"); + ImGui::Indent(); + ImGui::Text("One\nTwo\nThree"); ImGui::SameLine(); + ImGui::Text("Hello\nWorld"); ImGui::SameLine(); + ImGui::Text("Banana"); + + ImGui::Text("Banana"); ImGui::SameLine(); + ImGui::Text("Hello\nWorld"); ImGui::SameLine(); + ImGui::Text("One\nTwo\nThree"); + + ImGui::Button("HOP##1"); ImGui::SameLine(); + ImGui::Text("Banana"); ImGui::SameLine(); + ImGui::Text("Hello\nWorld"); ImGui::SameLine(); + ImGui::Text("Banana"); + + ImGui::Button("HOP##2"); ImGui::SameLine(); + ImGui::Text("Hello\nWorld"); ImGui::SameLine(); + ImGui::Text("Banana"); + ImGui::Unindent(); + } + + ImGui::Spacing(); + + { + ImGui::BulletText("Misc items:"); + ImGui::Indent(); + + // SmallButton() sets FramePadding to zero. Text baseline is aligned to match baseline of previous Button. + ImGui::Button("80x80", ImVec2(80, 80)); + ImGui::SameLine(); + ImGui::Button("50x50", ImVec2(50, 50)); + ImGui::SameLine(); + ImGui::Button("Button()"); + ImGui::SameLine(); + ImGui::SmallButton("SmallButton()"); + + // Tree + const float spacing = ImGui::GetStyle().ItemInnerSpacing.x; + ImGui::Button("Button##1"); + ImGui::SameLine(0.0f, spacing); + if (ImGui::TreeNode("Node##1")) + { + // Placeholder tree data + for (int i = 0; i < 6; i++) + ImGui::BulletText("Item %d..", i); + ImGui::TreePop(); + } + + // Vertically align text node a bit lower so it'll be vertically centered with upcoming widget. + // Otherwise you can use SmallButton() (smaller fit). + ImGui::AlignTextToFramePadding(); + + // Common mistake to avoid: if we want to SameLine after TreeNode we need to do it before we add + // other contents below the node. + bool node_open = ImGui::TreeNode("Node##2"); + ImGui::SameLine(0.0f, spacing); ImGui::Button("Button##2"); + if (node_open) + { + // Placeholder tree data + for (int i = 0; i < 6; i++) + ImGui::BulletText("Item %d..", i); + ImGui::TreePop(); + } + + // Bullet + ImGui::Button("Button##3"); + ImGui::SameLine(0.0f, spacing); + ImGui::BulletText("Bullet text"); + + ImGui::AlignTextToFramePadding(); + ImGui::BulletText("Node"); + ImGui::SameLine(0.0f, spacing); ImGui::Button("Button##4"); + ImGui::Unindent(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Layout/Scrolling"); + if (ImGui::TreeNode("Scrolling")) + { + // Vertical scroll functions + IMGUI_DEMO_MARKER("Layout/Scrolling/Vertical"); + HelpMarker("Use SetScrollHereY() or SetScrollFromPosY() to scroll to a given vertical position."); + + static int track_item = 50; + static bool enable_track = true; + static bool enable_extra_decorations = false; + static float scroll_to_off_px = 0.0f; + static float scroll_to_pos_px = 200.0f; + + ImGui::Checkbox("Decoration", &enable_extra_decorations); + + ImGui::Checkbox("Track", &enable_track); + ImGui::PushItemWidth(100); + ImGui::SameLine(140); enable_track |= ImGui::DragInt("##item", &track_item, 0.25f, 0, 99, "Item = %d"); + + bool scroll_to_off = ImGui::Button("Scroll Offset"); + ImGui::SameLine(140); scroll_to_off |= ImGui::DragFloat("##off", &scroll_to_off_px, 1.00f, 0, FLT_MAX, "+%.0f px"); + + bool scroll_to_pos = ImGui::Button("Scroll To Pos"); + ImGui::SameLine(140); scroll_to_pos |= ImGui::DragFloat("##pos", &scroll_to_pos_px, 1.00f, -10, FLT_MAX, "X/Y = %.0f px"); + ImGui::PopItemWidth(); + + if (scroll_to_off || scroll_to_pos) + enable_track = false; + + ImGuiStyle& style = ImGui::GetStyle(); + float child_w = (ImGui::GetContentRegionAvail().x - 4 * style.ItemSpacing.x) / 5; + if (child_w < 1.0f) + child_w = 1.0f; + ImGui::PushID("##VerticalScrolling"); + for (int i = 0; i < 5; i++) + { + if (i > 0) ImGui::SameLine(); + ImGui::BeginGroup(); + const char* names[] = { "Top", "25%", "Center", "75%", "Bottom" }; + ImGui::TextUnformatted(names[i]); + + const ImGuiWindowFlags child_flags = enable_extra_decorations ? ImGuiWindowFlags_MenuBar : 0; + const ImGuiID child_id = ImGui::GetID((void*)(intptr_t)i); + const bool child_is_visible = ImGui::BeginChild(child_id, ImVec2(child_w, 200.0f), true, child_flags); + if (ImGui::BeginMenuBar()) + { + ImGui::TextUnformatted("abc"); + ImGui::EndMenuBar(); + } + if (scroll_to_off) + ImGui::SetScrollY(scroll_to_off_px); + if (scroll_to_pos) + ImGui::SetScrollFromPosY(ImGui::GetCursorStartPos().y + scroll_to_pos_px, i * 0.25f); + if (child_is_visible) // Avoid calling SetScrollHereY when running with culled items + { + for (int item = 0; item < 100; item++) + { + if (enable_track && item == track_item) + { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Item %d", item); + ImGui::SetScrollHereY(i * 0.25f); // 0.0f:top, 0.5f:center, 1.0f:bottom + } + else + { + ImGui::Text("Item %d", item); + } + } + } + float scroll_y = ImGui::GetScrollY(); + float scroll_max_y = ImGui::GetScrollMaxY(); + ImGui::EndChild(); + ImGui::Text("%.0f/%.0f", scroll_y, scroll_max_y); + ImGui::EndGroup(); + } + ImGui::PopID(); + + // Horizontal scroll functions + IMGUI_DEMO_MARKER("Layout/Scrolling/Horizontal"); + ImGui::Spacing(); + HelpMarker( + "Use SetScrollHereX() or SetScrollFromPosX() to scroll to a given horizontal position.\n\n" + "Because the clipping rectangle of most window hides half worth of WindowPadding on the " + "left/right, using SetScrollFromPosX(+1) will usually result in clipped text whereas the " + "equivalent SetScrollFromPosY(+1) wouldn't."); + ImGui::PushID("##HorizontalScrolling"); + for (int i = 0; i < 5; i++) + { + float child_height = ImGui::GetTextLineHeight() + style.ScrollbarSize + style.WindowPadding.y * 2.0f; + ImGuiWindowFlags child_flags = ImGuiWindowFlags_HorizontalScrollbar | (enable_extra_decorations ? ImGuiWindowFlags_AlwaysVerticalScrollbar : 0); + ImGuiID child_id = ImGui::GetID((void*)(intptr_t)i); + bool child_is_visible = ImGui::BeginChild(child_id, ImVec2(-100, child_height), true, child_flags); + if (scroll_to_off) + ImGui::SetScrollX(scroll_to_off_px); + if (scroll_to_pos) + ImGui::SetScrollFromPosX(ImGui::GetCursorStartPos().x + scroll_to_pos_px, i * 0.25f); + if (child_is_visible) // Avoid calling SetScrollHereY when running with culled items + { + for (int item = 0; item < 100; item++) + { + if (item > 0) + ImGui::SameLine(); + if (enable_track && item == track_item) + { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Item %d", item); + ImGui::SetScrollHereX(i * 0.25f); // 0.0f:left, 0.5f:center, 1.0f:right + } + else + { + ImGui::Text("Item %d", item); + } + } + } + float scroll_x = ImGui::GetScrollX(); + float scroll_max_x = ImGui::GetScrollMaxX(); + ImGui::EndChild(); + ImGui::SameLine(); + const char* names[] = { "Left", "25%", "Center", "75%", "Right" }; + ImGui::Text("%s\n%.0f/%.0f", names[i], scroll_x, scroll_max_x); + ImGui::Spacing(); + } + ImGui::PopID(); + + // Miscellaneous Horizontal Scrolling Demo + IMGUI_DEMO_MARKER("Layout/Scrolling/Horizontal (more)"); + HelpMarker( + "Horizontal scrolling for a window is enabled via the ImGuiWindowFlags_HorizontalScrollbar flag.\n\n" + "You may want to also explicitly specify content width by using SetNextWindowContentWidth() before Begin()."); + static int lines = 7; + ImGui::SliderInt("Lines", &lines, 1, 15); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); + ImVec2 scrolling_child_size = ImVec2(0, ImGui::GetFrameHeightWithSpacing() * 7 + 30); + ImGui::BeginChild("scrolling", scrolling_child_size, true, ImGuiWindowFlags_HorizontalScrollbar); + for (int line = 0; line < lines; line++) + { + // Display random stuff. For the sake of this trivial demo we are using basic Button() + SameLine() + // If you want to create your own time line for a real application you may be better off manipulating + // the cursor position yourself, aka using SetCursorPos/SetCursorScreenPos to position the widgets + // yourself. You may also want to use the lower-level ImDrawList API. + int num_buttons = 10 + ((line & 1) ? line * 9 : line * 3); + for (int n = 0; n < num_buttons; n++) + { + if (n > 0) ImGui::SameLine(); + ImGui::PushID(n + line * 1000); + char num_buf[16]; + sprintf(num_buf, "%d", n); + const char* label = (!(n % 15)) ? "FizzBuzz" : (!(n % 3)) ? "Fizz" : (!(n % 5)) ? "Buzz" : num_buf; + float hue = n * 0.05f; + ImGui::PushStyleColor(ImGuiCol_Button, (ImVec4)ImColor::HSV(hue, 0.6f, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, (ImVec4)ImColor::HSV(hue, 0.7f, 0.7f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, (ImVec4)ImColor::HSV(hue, 0.8f, 0.8f)); + ImGui::Button(label, ImVec2(40.0f + sinf((float)(line + n)) * 20.0f, 0.0f)); + ImGui::PopStyleColor(3); + ImGui::PopID(); + } + } + float scroll_x = ImGui::GetScrollX(); + float scroll_max_x = ImGui::GetScrollMaxX(); + ImGui::EndChild(); + ImGui::PopStyleVar(2); + float scroll_x_delta = 0.0f; + ImGui::SmallButton("<<"); + if (ImGui::IsItemActive()) + scroll_x_delta = -ImGui::GetIO().DeltaTime * 1000.0f; + ImGui::SameLine(); + ImGui::Text("Scroll from code"); ImGui::SameLine(); + ImGui::SmallButton(">>"); + if (ImGui::IsItemActive()) + scroll_x_delta = +ImGui::GetIO().DeltaTime * 1000.0f; + ImGui::SameLine(); + ImGui::Text("%.0f/%.0f", scroll_x, scroll_max_x); + if (scroll_x_delta != 0.0f) + { + // Demonstrate a trick: you can use Begin to set yourself in the context of another window + // (here we are already out of your child window) + ImGui::BeginChild("scrolling"); + ImGui::SetScrollX(ImGui::GetScrollX() + scroll_x_delta); + ImGui::EndChild(); + } + ImGui::Spacing(); + + static bool show_horizontal_contents_size_demo_window = false; + ImGui::Checkbox("Show Horizontal contents size demo window", &show_horizontal_contents_size_demo_window); + + if (show_horizontal_contents_size_demo_window) + { + static bool show_h_scrollbar = true; + static bool show_button = true; + static bool show_tree_nodes = true; + static bool show_text_wrapped = false; + static bool show_columns = true; + static bool show_tab_bar = true; + static bool show_child = false; + static bool explicit_content_size = false; + static float contents_size_x = 300.0f; + if (explicit_content_size) + ImGui::SetNextWindowContentSize(ImVec2(contents_size_x, 0.0f)); + ImGui::Begin("Horizontal contents size demo window", &show_horizontal_contents_size_demo_window, show_h_scrollbar ? ImGuiWindowFlags_HorizontalScrollbar : 0); + IMGUI_DEMO_MARKER("Layout/Scrolling/Horizontal contents size demo window"); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 0)); + HelpMarker("Test of different widgets react and impact the work rectangle growing when horizontal scrolling is enabled.\n\nUse 'Metrics->Tools->Show windows rectangles' to visualize rectangles."); + ImGui::Checkbox("H-scrollbar", &show_h_scrollbar); + ImGui::Checkbox("Button", &show_button); // Will grow contents size (unless explicitly overwritten) + ImGui::Checkbox("Tree nodes", &show_tree_nodes); // Will grow contents size and display highlight over full width + ImGui::Checkbox("Text wrapped", &show_text_wrapped);// Will grow and use contents size + ImGui::Checkbox("Columns", &show_columns); // Will use contents size + ImGui::Checkbox("Tab bar", &show_tab_bar); // Will use contents size + ImGui::Checkbox("Child", &show_child); // Will grow and use contents size + ImGui::Checkbox("Explicit content size", &explicit_content_size); + ImGui::Text("Scroll %.1f/%.1f %.1f/%.1f", ImGui::GetScrollX(), ImGui::GetScrollMaxX(), ImGui::GetScrollY(), ImGui::GetScrollMaxY()); + if (explicit_content_size) + { + ImGui::SameLine(); + ImGui::SetNextItemWidth(100); + ImGui::DragFloat("##csx", &contents_size_x); + ImVec2 p = ImGui::GetCursorScreenPos(); + ImGui::GetWindowDrawList()->AddRectFilled(p, ImVec2(p.x + 10, p.y + 10), IM_COL32_WHITE); + ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(p.x + contents_size_x - 10, p.y), ImVec2(p.x + contents_size_x, p.y + 10), IM_COL32_WHITE); + ImGui::Dummy(ImVec2(0, 10)); + } + ImGui::PopStyleVar(2); + ImGui::Separator(); + if (show_button) + { + ImGui::Button("this is a 300-wide button", ImVec2(300, 0)); + } + if (show_tree_nodes) + { + bool open = true; + if (ImGui::TreeNode("this is a tree node")) + { + if (ImGui::TreeNode("another one of those tree node...")) + { + ImGui::Text("Some tree contents"); + ImGui::TreePop(); + } + ImGui::TreePop(); + } + ImGui::CollapsingHeader("CollapsingHeader", &open); + } + if (show_text_wrapped) + { + ImGui::TextWrapped("This text should automatically wrap on the edge of the work rectangle."); + } + if (show_columns) + { + ImGui::Text("Tables:"); + if (ImGui::BeginTable("table", 4, ImGuiTableFlags_Borders)) + { + for (int n = 0; n < 4; n++) + { + ImGui::TableNextColumn(); + ImGui::Text("Width %.2f", ImGui::GetContentRegionAvail().x); + } + ImGui::EndTable(); + } + ImGui::Text("Columns:"); + ImGui::Columns(4); + for (int n = 0; n < 4; n++) + { + ImGui::Text("Width %.2f", ImGui::GetColumnWidth()); + ImGui::NextColumn(); + } + ImGui::Columns(1); + } + if (show_tab_bar && ImGui::BeginTabBar("Hello")) + { + if (ImGui::BeginTabItem("OneOneOne")) { ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("TwoTwoTwo")) { ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("ThreeThreeThree")) { ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("FourFourFour")) { ImGui::EndTabItem(); } + ImGui::EndTabBar(); + } + if (show_child) + { + ImGui::BeginChild("child", ImVec2(0, 0), true); + ImGui::EndChild(); + } + ImGui::End(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Layout/Clipping"); + if (ImGui::TreeNode("Clipping")) + { + static ImVec2 size(100.0f, 100.0f); + static ImVec2 offset(30.0f, 30.0f); + ImGui::DragFloat2("size", (float*)&size, 0.5f, 1.0f, 200.0f, "%.0f"); + ImGui::TextWrapped("(Click and drag to scroll)"); + + for (int n = 0; n < 3; n++) + { + if (n > 0) + ImGui::SameLine(); + ImGui::PushID(n); + ImGui::BeginGroup(); // Lock X position + + ImGui::InvisibleButton("##empty", size); + if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) + { + offset.x += ImGui::GetIO().MouseDelta.x; + offset.y += ImGui::GetIO().MouseDelta.y; + } + const ImVec2 p0 = ImGui::GetItemRectMin(); + const ImVec2 p1 = ImGui::GetItemRectMax(); + const char* text_str = "Line 1 hello\nLine 2 clip me!"; + const ImVec2 text_pos = ImVec2(p0.x + offset.x, p0.y + offset.y); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + switch (n) + { + case 0: + HelpMarker( + "Using ImGui::PushClipRect():\n" + "Will alter ImGui hit-testing logic + ImDrawList rendering.\n" + "(use this if you want your clipping rectangle to affect interactions)"); + ImGui::PushClipRect(p0, p1, true); + draw_list->AddRectFilled(p0, p1, IM_COL32(90, 90, 120, 255)); + draw_list->AddText(text_pos, IM_COL32_WHITE, text_str); + ImGui::PopClipRect(); + break; + case 1: + HelpMarker( + "Using ImDrawList::PushClipRect():\n" + "Will alter ImDrawList rendering only.\n" + "(use this as a shortcut if you are only using ImDrawList calls)"); + draw_list->PushClipRect(p0, p1, true); + draw_list->AddRectFilled(p0, p1, IM_COL32(90, 90, 120, 255)); + draw_list->AddText(text_pos, IM_COL32_WHITE, text_str); + draw_list->PopClipRect(); + break; + case 2: + HelpMarker( + "Using ImDrawList::AddText() with a fine ClipRect:\n" + "Will alter only this specific ImDrawList::AddText() rendering.\n" + "(this is often used internally to avoid altering the clipping rectangle and minimize draw calls)"); + ImVec4 clip_rect(p0.x, p0.y, p1.x, p1.y); // AddText() takes a ImVec4* here so let's convert. + draw_list->AddRectFilled(p0, p1, IM_COL32(90, 90, 120, 255)); + draw_list->AddText(ImGui::GetFont(), ImGui::GetFontSize(), text_pos, IM_COL32_WHITE, text_str, NULL, 0.0f, &clip_rect); + break; + } + ImGui::EndGroup(); + ImGui::PopID(); + } + + ImGui::TreePop(); + } +} + +static void ShowDemoWindowPopups() +{ + IMGUI_DEMO_MARKER("Popups"); + if (!ImGui::CollapsingHeader("Popups & Modal windows")) + return; + + // The properties of popups windows are: + // - They block normal mouse hovering detection outside them. (*) + // - Unless modal, they can be closed by clicking anywhere outside them, or by pressing ESCAPE. + // - Their visibility state (~bool) is held internally by Dear ImGui instead of being held by the programmer as + // we are used to with regular Begin() calls. User can manipulate the visibility state by calling OpenPopup(). + // (*) One can use IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) to bypass it and detect hovering even + // when normally blocked by a popup. + // Those three properties are connected. The library needs to hold their visibility state BECAUSE it can close + // popups at any time. + + // Typical use for regular windows: + // bool my_tool_is_active = false; if (ImGui::Button("Open")) my_tool_is_active = true; [...] if (my_tool_is_active) Begin("My Tool", &my_tool_is_active) { [...] } End(); + // Typical use for popups: + // if (ImGui::Button("Open")) ImGui::OpenPopup("MyPopup"); if (ImGui::BeginPopup("MyPopup") { [...] EndPopup(); } + + // With popups we have to go through a library call (here OpenPopup) to manipulate the visibility state. + // This may be a bit confusing at first but it should quickly make sense. Follow on the examples below. + + IMGUI_DEMO_MARKER("Popups/Popups"); + if (ImGui::TreeNode("Popups")) + { + ImGui::TextWrapped( + "When a popup is active, it inhibits interacting with windows that are behind the popup. " + "Clicking outside the popup closes it."); + + static int selected_fish = -1; + const char* names[] = { "Bream", "Haddock", "Mackerel", "Pollock", "Tilefish" }; + static bool toggles[] = { true, false, false, false, false }; + + // Simple selection popup (if you want to show the current selection inside the Button itself, + // you may want to build a string using the "###" operator to preserve a constant ID with a variable label) + if (ImGui::Button("Select..")) + ImGui::OpenPopup("my_select_popup"); + ImGui::SameLine(); + ImGui::TextUnformatted(selected_fish == -1 ? "" : names[selected_fish]); + if (ImGui::BeginPopup("my_select_popup")) + { + ImGui::Text("Aquarium"); + ImGui::Separator(); + for (int i = 0; i < IM_ARRAYSIZE(names); i++) + if (ImGui::Selectable(names[i])) + selected_fish = i; + ImGui::EndPopup(); + } + + // Showing a menu with toggles + if (ImGui::Button("Toggle..")) + ImGui::OpenPopup("my_toggle_popup"); + if (ImGui::BeginPopup("my_toggle_popup")) + { + for (int i = 0; i < IM_ARRAYSIZE(names); i++) + ImGui::MenuItem(names[i], "", &toggles[i]); + if (ImGui::BeginMenu("Sub-menu")) + { + ImGui::MenuItem("Click me"); + ImGui::EndMenu(); + } + + ImGui::Separator(); + ImGui::Text("Tooltip here"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("I am a tooltip over a popup"); + + if (ImGui::Button("Stacked Popup")) + ImGui::OpenPopup("another popup"); + if (ImGui::BeginPopup("another popup")) + { + for (int i = 0; i < IM_ARRAYSIZE(names); i++) + ImGui::MenuItem(names[i], "", &toggles[i]); + if (ImGui::BeginMenu("Sub-menu")) + { + ImGui::MenuItem("Click me"); + if (ImGui::Button("Stacked Popup")) + ImGui::OpenPopup("another popup"); + if (ImGui::BeginPopup("another popup")) + { + ImGui::Text("I am the last one here."); + ImGui::EndPopup(); + } + ImGui::EndMenu(); + } + ImGui::EndPopup(); + } + ImGui::EndPopup(); + } + + // Call the more complete ShowExampleMenuFile which we use in various places of this demo + if (ImGui::Button("With a menu..")) + ImGui::OpenPopup("my_file_popup"); + if (ImGui::BeginPopup("my_file_popup", ImGuiWindowFlags_MenuBar)) + { + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("File")) + { + ShowExampleMenuFile(); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Edit")) + { + ImGui::MenuItem("Dummy"); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + ImGui::Text("Hello from popup!"); + ImGui::Button("This is a dummy button.."); + ImGui::EndPopup(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Popups/Context menus"); + if (ImGui::TreeNode("Context menus")) + { + HelpMarker("\"Context\" functions are simple helpers to associate a Popup to a given Item or Window identifier."); + + // BeginPopupContextItem() is a helper to provide common/simple popup behavior of essentially doing: + // if (id == 0) + // id = GetItemID(); // Use last item id + // if (IsItemHovered() && IsMouseReleased(ImGuiMouseButton_Right)) + // OpenPopup(id); + // return BeginPopup(id); + // For advanced advanced uses you may want to replicate and customize this code. + // See more details in BeginPopupContextItem(). + + // Example 1 + // When used after an item that has an ID (e.g. Button), we can skip providing an ID to BeginPopupContextItem(), + // and BeginPopupContextItem() will use the last item ID as the popup ID. + { + const char* names[5] = { "Label1", "Label2", "Label3", "Label4", "Label5" }; + for (int n = 0; n < 5; n++) + { + ImGui::Selectable(names[n]); + if (ImGui::BeginPopupContextItem()) // <-- use last item id as popup id + { + ImGui::Text("This a popup for \"%s\"!", names[n]); + if (ImGui::Button("Close")) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Right-click to open popup"); + } + } + + // Example 2 + // Popup on a Text() element which doesn't have an identifier: we need to provide an identifier to BeginPopupContextItem(). + // Using an explicit identifier is also convenient if you want to activate the popups from different locations. + { + HelpMarker("Text() elements don't have stable identifiers so we need to provide one."); + static float value = 0.5f; + ImGui::Text("Value = %.3f <-- (1) right-click this value", value); + if (ImGui::BeginPopupContextItem("my popup")) + { + if (ImGui::Selectable("Set to zero")) value = 0.0f; + if (ImGui::Selectable("Set to PI")) value = 3.1415f; + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::DragFloat("##Value", &value, 0.1f, 0.0f, 0.0f); + ImGui::EndPopup(); + } + + // We can also use OpenPopupOnItemClick() to toggle the visibility of a given popup. + // Here we make it that right-clicking this other text element opens the same popup as above. + // The popup itself will be submitted by the code above. + ImGui::Text("(2) Or right-click this text"); + ImGui::OpenPopupOnItemClick("my popup", ImGuiPopupFlags_MouseButtonRight); + + // Back to square one: manually open the same popup. + if (ImGui::Button("(3) Or click this button")) + ImGui::OpenPopup("my popup"); + } + + // Example 3 + // When using BeginPopupContextItem() with an implicit identifier (NULL == use last item ID), + // we need to make sure your item identifier is stable. + // In this example we showcase altering the item label while preserving its identifier, using the ### operator (see FAQ). + { + HelpMarker("Showcase using a popup ID linked to item ID, with the item having a changing label + stable ID using the ### operator."); + static char name[32] = "Label1"; + char buf[64]; + sprintf(buf, "Button: %s###Button", name); // ### operator override ID ignoring the preceding label + ImGui::Button(buf); + if (ImGui::BeginPopupContextItem()) + { + ImGui::Text("Edit name:"); + ImGui::InputText("##edit", name, IM_ARRAYSIZE(name)); + if (ImGui::Button("Close")) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + ImGui::SameLine(); ImGui::Text("(<-- right-click here)"); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Popups/Modals"); + if (ImGui::TreeNode("Modals")) + { + ImGui::TextWrapped("Modal windows are like popups but the user cannot close them by clicking outside."); + + if (ImGui::Button("Delete..")) + ImGui::OpenPopup("Delete?"); + + // Always center this window when appearing + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + + if (ImGui::BeginPopupModal("Delete?", NULL, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::Text("All those beautiful files will be deleted.\nThis operation cannot be undone!\n\n"); + ImGui::Separator(); + + //static int unused_i = 0; + //ImGui::Combo("Combo", &unused_i, "Delete\0Delete harder\0"); + + static bool dont_ask_me_next_time = false; + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + ImGui::Checkbox("Don't ask me next time", &dont_ask_me_next_time); + ImGui::PopStyleVar(); + + if (ImGui::Button("OK", ImVec2(120, 0))) { ImGui::CloseCurrentPopup(); } + ImGui::SetItemDefaultFocus(); + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) { ImGui::CloseCurrentPopup(); } + ImGui::EndPopup(); + } + + if (ImGui::Button("Stacked modals..")) + ImGui::OpenPopup("Stacked 1"); + if (ImGui::BeginPopupModal("Stacked 1", NULL, ImGuiWindowFlags_MenuBar)) + { + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("File")) + { + if (ImGui::MenuItem("Some menu item")) {} + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + ImGui::Text("Hello from Stacked The First\nUsing style.Colors[ImGuiCol_ModalWindowDimBg] behind it."); + + // Testing behavior of widgets stacking their own regular popups over the modal. + static int item = 1; + static float color[4] = { 0.4f, 0.7f, 0.0f, 0.5f }; + ImGui::Combo("Combo", &item, "aaaa\0bbbb\0cccc\0dddd\0eeee\0\0"); + ImGui::ColorEdit4("color", color); + + if (ImGui::Button("Add another modal..")) + ImGui::OpenPopup("Stacked 2"); + + // Also demonstrate passing a bool* to BeginPopupModal(), this will create a regular close button which + // will close the popup. Note that the visibility state of popups is owned by imgui, so the input value + // of the bool actually doesn't matter here. + bool unused_open = true; + if (ImGui::BeginPopupModal("Stacked 2", &unused_open)) + { + ImGui::Text("Hello from Stacked The Second!"); + if (ImGui::Button("Close")) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + + if (ImGui::Button("Close")) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Popups/Menus inside a regular window"); + if (ImGui::TreeNode("Menus inside a regular window")) + { + ImGui::TextWrapped("Below we are testing adding menu items to a regular window. It's rather unusual but should work!"); + ImGui::Separator(); + + // Note: As a quirk in this very specific example, we want to differentiate the parent of this menu from the + // parent of the various popup menus above. To do so we are encloding the items in a PushID()/PopID() block + // to make them two different menusets. If we don't, opening any popup above and hovering our menu here would + // open it. This is because once a menu is active, we allow to switch to a sibling menu by just hovering on it, + // which is the desired behavior for regular menus. + ImGui::PushID("foo"); + ImGui::MenuItem("Menu item", "CTRL+M"); + if (ImGui::BeginMenu("Menu inside a regular window")) + { + ShowExampleMenuFile(); + ImGui::EndMenu(); + } + ImGui::PopID(); + ImGui::Separator(); + ImGui::TreePop(); + } +} + +// Dummy data structure that we use for the Table demo. +// (pre-C++11 doesn't allow us to instantiate ImVector template if this structure if defined inside the demo function) +namespace +{ +// We are passing our own identifier to TableSetupColumn() to facilitate identifying columns in the sorting code. +// This identifier will be passed down into ImGuiTableSortSpec::ColumnUserID. +// But it is possible to omit the user id parameter of TableSetupColumn() and just use the column index instead! (ImGuiTableSortSpec::ColumnIndex) +// If you don't use sorting, you will generally never care about giving column an ID! +enum MyItemColumnID +{ + MyItemColumnID_ID, + MyItemColumnID_Name, + MyItemColumnID_Action, + MyItemColumnID_Quantity, + MyItemColumnID_Description +}; + +struct MyItem +{ + int ID; + const char* Name; + int Quantity; + + // We have a problem which is affecting _only this demo_ and should not affect your code: + // As we don't rely on std:: or other third-party library to compile dear imgui, we only have reliable access to qsort(), + // however qsort doesn't allow passing user data to comparing function. + // As a workaround, we are storing the sort specs in a static/global for the comparing function to access. + // In your own use case you would probably pass the sort specs to your sorting/comparing functions directly and not use a global. + // We could technically call ImGui::TableGetSortSpecs() in CompareWithSortSpecs(), but considering that this function is called + // very often by the sorting algorithm it would be a little wasteful. + static const ImGuiTableSortSpecs* s_current_sort_specs; + + // Compare function to be used by qsort() + static int IMGUI_CDECL CompareWithSortSpecs(const void* lhs, const void* rhs) + { + const MyItem* a = (const MyItem*)lhs; + const MyItem* b = (const MyItem*)rhs; + for (int n = 0; n < s_current_sort_specs->SpecsCount; n++) + { + // Here we identify columns using the ColumnUserID value that we ourselves passed to TableSetupColumn() + // We could also choose to identify columns based on their index (sort_spec->ColumnIndex), which is simpler! + const ImGuiTableColumnSortSpecs* sort_spec = &s_current_sort_specs->Specs[n]; + int delta = 0; + switch (sort_spec->ColumnUserID) + { + case MyItemColumnID_ID: delta = (a->ID - b->ID); break; + case MyItemColumnID_Name: delta = (strcmp(a->Name, b->Name)); break; + case MyItemColumnID_Quantity: delta = (a->Quantity - b->Quantity); break; + case MyItemColumnID_Description: delta = (strcmp(a->Name, b->Name)); break; + default: IM_ASSERT(0); break; + } + if (delta > 0) + return (sort_spec->SortDirection == ImGuiSortDirection_Ascending) ? +1 : -1; + if (delta < 0) + return (sort_spec->SortDirection == ImGuiSortDirection_Ascending) ? -1 : +1; + } + + // qsort() is instable so always return a way to differenciate items. + // Your own compare function may want to avoid fallback on implicit sort specs e.g. a Name compare if it wasn't already part of the sort specs. + return (a->ID - b->ID); + } +}; +const ImGuiTableSortSpecs* MyItem::s_current_sort_specs = NULL; +} + +// Make the UI compact because there are so many fields +static void PushStyleCompact() +{ + ImGuiStyle& style = ImGui::GetStyle(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(style.FramePadding.x, (float)(int)(style.FramePadding.y * 0.60f))); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x, (float)(int)(style.ItemSpacing.y * 0.60f))); +} + +static void PopStyleCompact() +{ + ImGui::PopStyleVar(2); +} + +// Show a combo box with a choice of sizing policies +static void EditTableSizingFlags(ImGuiTableFlags* p_flags) +{ + struct EnumDesc { ImGuiTableFlags Value; const char* Name; const char* Tooltip; }; + static const EnumDesc policies[] = + { + { ImGuiTableFlags_None, "Default", "Use default sizing policy:\n- ImGuiTableFlags_SizingFixedFit if ScrollX is on or if host window has ImGuiWindowFlags_AlwaysAutoResize.\n- ImGuiTableFlags_SizingStretchSame otherwise." }, + { ImGuiTableFlags_SizingFixedFit, "ImGuiTableFlags_SizingFixedFit", "Columns default to _WidthFixed (if resizable) or _WidthAuto (if not resizable), matching contents width." }, + { ImGuiTableFlags_SizingFixedSame, "ImGuiTableFlags_SizingFixedSame", "Columns are all the same width, matching the maximum contents width.\nImplicitly disable ImGuiTableFlags_Resizable and enable ImGuiTableFlags_NoKeepColumnsVisible." }, + { ImGuiTableFlags_SizingStretchProp, "ImGuiTableFlags_SizingStretchProp", "Columns default to _WidthStretch with weights proportional to their widths." }, + { ImGuiTableFlags_SizingStretchSame, "ImGuiTableFlags_SizingStretchSame", "Columns default to _WidthStretch with same weights." } + }; + int idx; + for (idx = 0; idx < IM_ARRAYSIZE(policies); idx++) + if (policies[idx].Value == (*p_flags & ImGuiTableFlags_SizingMask_)) + break; + const char* preview_text = (idx < IM_ARRAYSIZE(policies)) ? policies[idx].Name + (idx > 0 ? strlen("ImGuiTableFlags") : 0) : ""; + if (ImGui::BeginCombo("Sizing Policy", preview_text)) + { + for (int n = 0; n < IM_ARRAYSIZE(policies); n++) + if (ImGui::Selectable(policies[n].Name, idx == n)) + *p_flags = (*p_flags & ~ImGuiTableFlags_SizingMask_) | policies[n].Value; + ImGui::EndCombo(); + } + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 50.0f); + for (int m = 0; m < IM_ARRAYSIZE(policies); m++) + { + ImGui::Separator(); + ImGui::Text("%s:", policies[m].Name); + ImGui::Separator(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetStyle().IndentSpacing * 0.5f); + ImGui::TextUnformatted(policies[m].Tooltip); + } + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +static void EditTableColumnsFlags(ImGuiTableColumnFlags* p_flags) +{ + ImGui::CheckboxFlags("_Disabled", p_flags, ImGuiTableColumnFlags_Disabled); ImGui::SameLine(); HelpMarker("Master disable flag (also hide from context menu)"); + ImGui::CheckboxFlags("_DefaultHide", p_flags, ImGuiTableColumnFlags_DefaultHide); + ImGui::CheckboxFlags("_DefaultSort", p_flags, ImGuiTableColumnFlags_DefaultSort); + if (ImGui::CheckboxFlags("_WidthStretch", p_flags, ImGuiTableColumnFlags_WidthStretch)) + *p_flags &= ~(ImGuiTableColumnFlags_WidthMask_ ^ ImGuiTableColumnFlags_WidthStretch); + if (ImGui::CheckboxFlags("_WidthFixed", p_flags, ImGuiTableColumnFlags_WidthFixed)) + *p_flags &= ~(ImGuiTableColumnFlags_WidthMask_ ^ ImGuiTableColumnFlags_WidthFixed); + ImGui::CheckboxFlags("_NoResize", p_flags, ImGuiTableColumnFlags_NoResize); + ImGui::CheckboxFlags("_NoReorder", p_flags, ImGuiTableColumnFlags_NoReorder); + ImGui::CheckboxFlags("_NoHide", p_flags, ImGuiTableColumnFlags_NoHide); + ImGui::CheckboxFlags("_NoClip", p_flags, ImGuiTableColumnFlags_NoClip); + ImGui::CheckboxFlags("_NoSort", p_flags, ImGuiTableColumnFlags_NoSort); + ImGui::CheckboxFlags("_NoSortAscending", p_flags, ImGuiTableColumnFlags_NoSortAscending); + ImGui::CheckboxFlags("_NoSortDescending", p_flags, ImGuiTableColumnFlags_NoSortDescending); + ImGui::CheckboxFlags("_NoHeaderLabel", p_flags, ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::CheckboxFlags("_NoHeaderWidth", p_flags, ImGuiTableColumnFlags_NoHeaderWidth); + ImGui::CheckboxFlags("_PreferSortAscending", p_flags, ImGuiTableColumnFlags_PreferSortAscending); + ImGui::CheckboxFlags("_PreferSortDescending", p_flags, ImGuiTableColumnFlags_PreferSortDescending); + ImGui::CheckboxFlags("_IndentEnable", p_flags, ImGuiTableColumnFlags_IndentEnable); ImGui::SameLine(); HelpMarker("Default for column 0"); + ImGui::CheckboxFlags("_IndentDisable", p_flags, ImGuiTableColumnFlags_IndentDisable); ImGui::SameLine(); HelpMarker("Default for column >0"); +} + +static void ShowTableColumnsStatusFlags(ImGuiTableColumnFlags flags) +{ + ImGui::CheckboxFlags("_IsEnabled", &flags, ImGuiTableColumnFlags_IsEnabled); + ImGui::CheckboxFlags("_IsVisible", &flags, ImGuiTableColumnFlags_IsVisible); + ImGui::CheckboxFlags("_IsSorted", &flags, ImGuiTableColumnFlags_IsSorted); + ImGui::CheckboxFlags("_IsHovered", &flags, ImGuiTableColumnFlags_IsHovered); +} + +static void ShowDemoWindowTables() +{ + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); + IMGUI_DEMO_MARKER("Tables"); + if (!ImGui::CollapsingHeader("Tables & Columns")) + return; + + // Using those as a base value to create width/height that are factor of the size of our font + const float TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x; + const float TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing(); + + ImGui::PushID("Tables"); + + int open_action = -1; + if (ImGui::Button("Open all")) + open_action = 1; + ImGui::SameLine(); + if (ImGui::Button("Close all")) + open_action = 0; + ImGui::SameLine(); + + // Options + static bool disable_indent = false; + ImGui::Checkbox("Disable tree indentation", &disable_indent); + ImGui::SameLine(); + HelpMarker("Disable the indenting of tree nodes so demo tables can use the full window width."); + ImGui::Separator(); + if (disable_indent) + ImGui::PushStyleVar(ImGuiStyleVar_IndentSpacing, 0.0f); + + // About Styling of tables + // Most settings are configured on a per-table basis via the flags passed to BeginTable() and TableSetupColumns APIs. + // There are however a few settings that a shared and part of the ImGuiStyle structure: + // style.CellPadding // Padding within each cell + // style.Colors[ImGuiCol_TableHeaderBg] // Table header background + // style.Colors[ImGuiCol_TableBorderStrong] // Table outer and header borders + // style.Colors[ImGuiCol_TableBorderLight] // Table inner borders + // style.Colors[ImGuiCol_TableRowBg] // Table row background when ImGuiTableFlags_RowBg is enabled (even rows) + // style.Colors[ImGuiCol_TableRowBgAlt] // Table row background when ImGuiTableFlags_RowBg is enabled (odds rows) + + // Demos + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Basic"); + if (ImGui::TreeNode("Basic")) + { + // Here we will showcase three different ways to output a table. + // They are very simple variations of a same thing! + + // [Method 1] Using TableNextRow() to create a new row, and TableSetColumnIndex() to select the column. + // In many situations, this is the most flexible and easy to use pattern. + HelpMarker("Using TableNextRow() + calling TableSetColumnIndex() _before_ each cell, in a loop."); + if (ImGui::BeginTable("table1", 3)) + { + for (int row = 0; row < 4; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Row %d Column %d", row, column); + } + } + ImGui::EndTable(); + } + + // [Method 2] Using TableNextColumn() called multiple times, instead of using a for loop + TableSetColumnIndex(). + // This is generally more convenient when you have code manually submitting the contents of each columns. + HelpMarker("Using TableNextRow() + calling TableNextColumn() _before_ each cell, manually."); + if (ImGui::BeginTable("table2", 3)) + { + for (int row = 0; row < 4; row++) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Row %d", row); + ImGui::TableNextColumn(); + ImGui::Text("Some contents"); + ImGui::TableNextColumn(); + ImGui::Text("123.456"); + } + ImGui::EndTable(); + } + + // [Method 3] We call TableNextColumn() _before_ each cell. We never call TableNextRow(), + // as TableNextColumn() will automatically wrap around and create new roes as needed. + // This is generally more convenient when your cells all contains the same type of data. + HelpMarker( + "Only using TableNextColumn(), which tends to be convenient for tables where every cells contains the same type of contents.\n" + "This is also more similar to the old NextColumn() function of the Columns API, and provided to facilitate the Columns->Tables API transition."); + if (ImGui::BeginTable("table3", 3)) + { + for (int item = 0; item < 14; item++) + { + ImGui::TableNextColumn(); + ImGui::Text("Item %d", item); + } + ImGui::EndTable(); + } + + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Borders, background"); + if (ImGui::TreeNode("Borders, background")) + { + // Expose a few Borders related flags interactively + enum ContentsType { CT_Text, CT_FillButton }; + static ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg; + static bool display_headers = false; + static int contents_type = CT_Text; + + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_RowBg", &flags, ImGuiTableFlags_RowBg); + ImGui::CheckboxFlags("ImGuiTableFlags_Borders", &flags, ImGuiTableFlags_Borders); + ImGui::SameLine(); HelpMarker("ImGuiTableFlags_Borders\n = ImGuiTableFlags_BordersInnerV\n | ImGuiTableFlags_BordersOuterV\n | ImGuiTableFlags_BordersInnerV\n | ImGuiTableFlags_BordersOuterH"); + ImGui::Indent(); + + ImGui::CheckboxFlags("ImGuiTableFlags_BordersH", &flags, ImGuiTableFlags_BordersH); + ImGui::Indent(); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterH", &flags, ImGuiTableFlags_BordersOuterH); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerH", &flags, ImGuiTableFlags_BordersInnerH); + ImGui::Unindent(); + + ImGui::CheckboxFlags("ImGuiTableFlags_BordersV", &flags, ImGuiTableFlags_BordersV); + ImGui::Indent(); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterV", &flags, ImGuiTableFlags_BordersOuterV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerV", &flags, ImGuiTableFlags_BordersInnerV); + ImGui::Unindent(); + + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuter", &flags, ImGuiTableFlags_BordersOuter); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInner", &flags, ImGuiTableFlags_BordersInner); + ImGui::Unindent(); + + ImGui::AlignTextToFramePadding(); ImGui::Text("Cell contents:"); + ImGui::SameLine(); ImGui::RadioButton("Text", &contents_type, CT_Text); + ImGui::SameLine(); ImGui::RadioButton("FillButton", &contents_type, CT_FillButton); + ImGui::Checkbox("Display headers", &display_headers); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body (borders will always appears in Headers"); + PopStyleCompact(); + + if (ImGui::BeginTable("table1", 3, flags)) + { + // Display headers so we can inspect their interaction with borders. + // (Headers are not the main purpose of this section of the demo, so we are not elaborating on them too much. See other sections for details) + if (display_headers) + { + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + ImGui::TableHeadersRow(); + } + + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + char buf[32]; + sprintf(buf, "Hello %d,%d", column, row); + if (contents_type == CT_Text) + ImGui::TextUnformatted(buf); + else if (contents_type) + ImGui::Button(buf, ImVec2(-FLT_MIN, 0.0f)); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Resizable, stretch"); + if (ImGui::TreeNode("Resizable, stretch")) + { + // By default, if we don't enable ScrollX the sizing policy for each columns is "Stretch" + // Each columns maintain a sizing weight, and they will occupy all available width. + static ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_ContextMenuInBody; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersV", &flags, ImGuiTableFlags_BordersV); + ImGui::SameLine(); HelpMarker("Using the _Resizable flag automatically enables the _BordersInnerV flag as well, this is why the resize borders are still showing when unchecking this."); + PopStyleCompact(); + + if (ImGui::BeginTable("table1", 3, flags)) + { + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Hello %d,%d", column, row); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Resizable, fixed"); + if (ImGui::TreeNode("Resizable, fixed")) + { + // Here we use ImGuiTableFlags_SizingFixedFit (even though _ScrollX is not set) + // So columns will adopt the "Fixed" policy and will maintain a fixed width regardless of the whole available width (unless table is small) + // If there is not enough available width to fit all columns, they will however be resized down. + // FIXME-TABLE: Providing a stretch-on-init would make sense especially for tables which don't have saved settings + HelpMarker( + "Using _Resizable + _SizingFixedFit flags.\n" + "Fixed-width columns generally makes more sense if you want to use horizontal scrolling.\n\n" + "Double-click a column border to auto-fit the column to its contents."); + PushStyleCompact(); + static ImGuiTableFlags flags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_ContextMenuInBody; + ImGui::CheckboxFlags("ImGuiTableFlags_NoHostExtendX", &flags, ImGuiTableFlags_NoHostExtendX); + PopStyleCompact(); + + if (ImGui::BeginTable("table1", 3, flags)) + { + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Hello %d,%d", column, row); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Resizable, mixed"); + if (ImGui::TreeNode("Resizable, mixed")) + { + HelpMarker( + "Using TableSetupColumn() to alter resizing policy on a per-column basis.\n\n" + "When combining Fixed and Stretch columns, generally you only want one, maybe two trailing columns to use _WidthStretch."); + static ImGuiTableFlags flags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable; + + if (ImGui::BeginTable("table1", 3, flags)) + { + ImGui::TableSetupColumn("AAA", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("BBB", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("CCC", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("%s %d,%d", (column == 2) ? "Stretch" : "Fixed", column, row); + } + } + ImGui::EndTable(); + } + if (ImGui::BeginTable("table2", 6, flags)) + { + ImGui::TableSetupColumn("AAA", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("BBB", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("CCC", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_DefaultHide); + ImGui::TableSetupColumn("DDD", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("EEE", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("FFF", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_DefaultHide); + ImGui::TableHeadersRow(); + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 6; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("%s %d,%d", (column >= 3) ? "Stretch" : "Fixed", column, row); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Reorderable, hideable, with headers"); + if (ImGui::TreeNode("Reorderable, hideable, with headers")) + { + HelpMarker( + "Click and drag column headers to reorder columns.\n\n" + "Right-click on a header to open a context menu."); + static ImGuiTableFlags flags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_Reorderable", &flags, ImGuiTableFlags_Reorderable); + ImGui::CheckboxFlags("ImGuiTableFlags_Hideable", &flags, ImGuiTableFlags_Hideable); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBodyUntilResize", &flags, ImGuiTableFlags_NoBordersInBodyUntilResize); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body until hovered for resize (borders will always appears in Headers)"); + PopStyleCompact(); + + if (ImGui::BeginTable("table1", 3, flags)) + { + // Submit columns name with TableSetupColumn() and call TableHeadersRow() to create a row with a header in each column. + // (Later we will show how TableSetupColumn() has other uses, optional flags, sizing weight etc.) + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + ImGui::TableHeadersRow(); + for (int row = 0; row < 6; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Hello %d,%d", column, row); + } + } + ImGui::EndTable(); + } + + // Use outer_size.x == 0.0f instead of default to make the table as tight as possible (only valid when no scrolling and no stretch column) + if (ImGui::BeginTable("table2", 3, flags | ImGuiTableFlags_SizingFixedFit, ImVec2(0.0f, 0.0f))) + { + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + ImGui::TableHeadersRow(); + for (int row = 0; row < 6; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Fixed %d,%d", column, row); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Padding"); + if (ImGui::TreeNode("Padding")) + { + // First example: showcase use of padding flags and effect of BorderOuterV/BorderInnerV on X padding. + // We don't expose BorderOuterH/BorderInnerH here because they have no effect on X padding. + HelpMarker( + "We often want outer padding activated when any using features which makes the edges of a column visible:\n" + "e.g.:\n" + "- BorderOuterV\n" + "- any form of row selection\n" + "Because of this, activating BorderOuterV sets the default to PadOuterX. Using PadOuterX or NoPadOuterX you can override the default.\n\n" + "Actual padding values are using style.CellPadding.\n\n" + "In this demo we don't show horizontal borders to emphasis how they don't affect default horizontal padding."); + + static ImGuiTableFlags flags1 = ImGuiTableFlags_BordersV; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_PadOuterX", &flags1, ImGuiTableFlags_PadOuterX); + ImGui::SameLine(); HelpMarker("Enable outer-most padding (default if ImGuiTableFlags_BordersOuterV is set)"); + ImGui::CheckboxFlags("ImGuiTableFlags_NoPadOuterX", &flags1, ImGuiTableFlags_NoPadOuterX); + ImGui::SameLine(); HelpMarker("Disable outer-most padding (default if ImGuiTableFlags_BordersOuterV is not set)"); + ImGui::CheckboxFlags("ImGuiTableFlags_NoPadInnerX", &flags1, ImGuiTableFlags_NoPadInnerX); + ImGui::SameLine(); HelpMarker("Disable inner padding between columns (double inner padding if BordersOuterV is on, single inner padding if BordersOuterV is off)"); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterV", &flags1, ImGuiTableFlags_BordersOuterV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerV", &flags1, ImGuiTableFlags_BordersInnerV); + static bool show_headers = false; + ImGui::Checkbox("show_headers", &show_headers); + PopStyleCompact(); + + if (ImGui::BeginTable("table_padding", 3, flags1)) + { + if (show_headers) + { + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + ImGui::TableHeadersRow(); + } + + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + if (row == 0) + { + ImGui::Text("Avail %.2f", ImGui::GetContentRegionAvail().x); + } + else + { + char buf[32]; + sprintf(buf, "Hello %d,%d", column, row); + ImGui::Button(buf, ImVec2(-FLT_MIN, 0.0f)); + } + //if (ImGui::TableGetColumnFlags() & ImGuiTableColumnFlags_IsHovered) + // ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, IM_COL32(0, 100, 0, 255)); + } + } + ImGui::EndTable(); + } + + // Second example: set style.CellPadding to (0.0) or a custom value. + // FIXME-TABLE: Vertical border effectively not displayed the same way as horizontal one... + HelpMarker("Setting style.CellPadding to (0,0) or a custom value."); + static ImGuiTableFlags flags2 = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg; + static ImVec2 cell_padding(0.0f, 0.0f); + static bool show_widget_frame_bg = true; + + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Borders", &flags2, ImGuiTableFlags_Borders); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersH", &flags2, ImGuiTableFlags_BordersH); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersV", &flags2, ImGuiTableFlags_BordersV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInner", &flags2, ImGuiTableFlags_BordersInner); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuter", &flags2, ImGuiTableFlags_BordersOuter); + ImGui::CheckboxFlags("ImGuiTableFlags_RowBg", &flags2, ImGuiTableFlags_RowBg); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags2, ImGuiTableFlags_Resizable); + ImGui::Checkbox("show_widget_frame_bg", &show_widget_frame_bg); + ImGui::SliderFloat2("CellPadding", &cell_padding.x, 0.0f, 10.0f, "%.0f"); + PopStyleCompact(); + + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, cell_padding); + if (ImGui::BeginTable("table_padding_2", 3, flags2)) + { + static char text_bufs[3 * 5][16]; // Mini text storage for 3x5 cells + static bool init = true; + if (!show_widget_frame_bg) + ImGui::PushStyleColor(ImGuiCol_FrameBg, 0); + for (int cell = 0; cell < 3 * 5; cell++) + { + ImGui::TableNextColumn(); + if (init) + strcpy(text_bufs[cell], "edit me"); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::PushID(cell); + ImGui::InputText("##cell", text_bufs[cell], IM_ARRAYSIZE(text_bufs[cell])); + ImGui::PopID(); + } + if (!show_widget_frame_bg) + ImGui::PopStyleColor(); + init = false; + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Explicit widths"); + if (ImGui::TreeNode("Sizing policies")) + { + static ImGuiTableFlags flags1 = ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH | ImGuiTableFlags_RowBg | ImGuiTableFlags_ContextMenuInBody; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags1, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_NoHostExtendX", &flags1, ImGuiTableFlags_NoHostExtendX); + PopStyleCompact(); + + static ImGuiTableFlags sizing_policy_flags[4] = { ImGuiTableFlags_SizingFixedFit, ImGuiTableFlags_SizingFixedSame, ImGuiTableFlags_SizingStretchProp, ImGuiTableFlags_SizingStretchSame }; + for (int table_n = 0; table_n < 4; table_n++) + { + ImGui::PushID(table_n); + ImGui::SetNextItemWidth(TEXT_BASE_WIDTH * 30); + EditTableSizingFlags(&sizing_policy_flags[table_n]); + + // To make it easier to understand the different sizing policy, + // For each policy: we display one table where the columns have equal contents width, and one where the columns have different contents width. + if (ImGui::BeginTable("table1", 3, sizing_policy_flags[table_n] | flags1)) + { + for (int row = 0; row < 3; row++) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); ImGui::Text("Oh dear"); + ImGui::TableNextColumn(); ImGui::Text("Oh dear"); + ImGui::TableNextColumn(); ImGui::Text("Oh dear"); + } + ImGui::EndTable(); + } + if (ImGui::BeginTable("table2", 3, sizing_policy_flags[table_n] | flags1)) + { + for (int row = 0; row < 3; row++) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); ImGui::Text("AAAA"); + ImGui::TableNextColumn(); ImGui::Text("BBBBBBBB"); + ImGui::TableNextColumn(); ImGui::Text("CCCCCCCCCCCC"); + } + ImGui::EndTable(); + } + ImGui::PopID(); + } + + ImGui::Spacing(); + ImGui::TextUnformatted("Advanced"); + ImGui::SameLine(); + HelpMarker("This section allows you to interact and see the effect of various sizing policies depending on whether Scroll is enabled and the contents of your columns."); + + enum ContentsType { CT_ShowWidth, CT_ShortText, CT_LongText, CT_Button, CT_FillButton, CT_InputText }; + static ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable; + static int contents_type = CT_ShowWidth; + static int column_count = 3; + + PushStyleCompact(); + ImGui::PushID("Advanced"); + ImGui::PushItemWidth(TEXT_BASE_WIDTH * 30); + EditTableSizingFlags(&flags); + ImGui::Combo("Contents", &contents_type, "Show width\0Short Text\0Long Text\0Button\0Fill Button\0InputText\0"); + if (contents_type == CT_FillButton) + { + ImGui::SameLine(); + HelpMarker("Be mindful that using right-alignment (e.g. size.x = -FLT_MIN) creates a feedback loop where contents width can feed into auto-column width can feed into contents width."); + } + ImGui::DragInt("Columns", &column_count, 0.1f, 1, 64, "%d", ImGuiSliderFlags_AlwaysClamp); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_PreciseWidths", &flags, ImGuiTableFlags_PreciseWidths); + ImGui::SameLine(); HelpMarker("Disable distributing remainder width to stretched columns (width allocation on a 100-wide table with 3 columns: Without this flag: 33,33,34. With this flag: 33,33,33). With larger number of columns, resizing will appear to be less smooth."); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollX", &flags, ImGuiTableFlags_ScrollX); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollY", &flags, ImGuiTableFlags_ScrollY); + ImGui::CheckboxFlags("ImGuiTableFlags_NoClip", &flags, ImGuiTableFlags_NoClip); + ImGui::PopItemWidth(); + ImGui::PopID(); + PopStyleCompact(); + + if (ImGui::BeginTable("table2", column_count, flags, ImVec2(0.0f, TEXT_BASE_HEIGHT * 7))) + { + for (int cell = 0; cell < 10 * column_count; cell++) + { + ImGui::TableNextColumn(); + int column = ImGui::TableGetColumnIndex(); + int row = ImGui::TableGetRowIndex(); + + ImGui::PushID(cell); + char label[32]; + static char text_buf[32] = ""; + sprintf(label, "Hello %d,%d", column, row); + switch (contents_type) + { + case CT_ShortText: ImGui::TextUnformatted(label); break; + case CT_LongText: ImGui::Text("Some %s text %d,%d\nOver two lines..", column == 0 ? "long" : "longeeer", column, row); break; + case CT_ShowWidth: ImGui::Text("W: %.1f", ImGui::GetContentRegionAvail().x); break; + case CT_Button: ImGui::Button(label); break; + case CT_FillButton: ImGui::Button(label, ImVec2(-FLT_MIN, 0.0f)); break; + case CT_InputText: ImGui::SetNextItemWidth(-FLT_MIN); ImGui::InputText("##", text_buf, IM_ARRAYSIZE(text_buf)); break; + } + ImGui::PopID(); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Vertical scrolling, with clipping"); + if (ImGui::TreeNode("Vertical scrolling, with clipping")) + { + HelpMarker("Here we activate ScrollY, which will create a child window container to allow hosting scrollable contents.\n\nWe also demonstrate using ImGuiListClipper to virtualize the submission of many items."); + static ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable; + + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollY", &flags, ImGuiTableFlags_ScrollY); + PopStyleCompact(); + + // When using ScrollX or ScrollY we need to specify a size for our table container! + // Otherwise by default the table will fit all available space, like a BeginChild() call. + ImVec2 outer_size = ImVec2(0.0f, TEXT_BASE_HEIGHT * 8); + if (ImGui::BeginTable("table_scrolly", 3, flags, outer_size)) + { + ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible + ImGui::TableSetupColumn("One", ImGuiTableColumnFlags_None); + ImGui::TableSetupColumn("Two", ImGuiTableColumnFlags_None); + ImGui::TableSetupColumn("Three", ImGuiTableColumnFlags_None); + ImGui::TableHeadersRow(); + + // Demonstrate using clipper for large vertical lists + ImGuiListClipper clipper; + clipper.Begin(1000); + while (clipper.Step()) + { + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Hello %d,%d", column, row); + } + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Horizontal scrolling"); + if (ImGui::TreeNode("Horizontal scrolling")) + { + HelpMarker( + "When ScrollX is enabled, the default sizing policy becomes ImGuiTableFlags_SizingFixedFit, " + "as automatically stretching columns doesn't make much sense with horizontal scrolling.\n\n" + "Also note that as of the current version, you will almost always want to enable ScrollY along with ScrollX," + "because the container window won't automatically extend vertically to fix contents (this may be improved in future versions)."); + static ImGuiTableFlags flags = ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable; + static int freeze_cols = 1; + static int freeze_rows = 1; + + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollX", &flags, ImGuiTableFlags_ScrollX); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollY", &flags, ImGuiTableFlags_ScrollY); + ImGui::SetNextItemWidth(ImGui::GetFrameHeight()); + ImGui::DragInt("freeze_cols", &freeze_cols, 0.2f, 0, 9, NULL, ImGuiSliderFlags_NoInput); + ImGui::SetNextItemWidth(ImGui::GetFrameHeight()); + ImGui::DragInt("freeze_rows", &freeze_rows, 0.2f, 0, 9, NULL, ImGuiSliderFlags_NoInput); + PopStyleCompact(); + + // When using ScrollX or ScrollY we need to specify a size for our table container! + // Otherwise by default the table will fit all available space, like a BeginChild() call. + ImVec2 outer_size = ImVec2(0.0f, TEXT_BASE_HEIGHT * 8); + if (ImGui::BeginTable("table_scrollx", 7, flags, outer_size)) + { + ImGui::TableSetupScrollFreeze(freeze_cols, freeze_rows); + ImGui::TableSetupColumn("Line #", ImGuiTableColumnFlags_NoHide); // Make the first column not hideable to match our use of TableSetupScrollFreeze() + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + ImGui::TableSetupColumn("Four"); + ImGui::TableSetupColumn("Five"); + ImGui::TableSetupColumn("Six"); + ImGui::TableHeadersRow(); + for (int row = 0; row < 20; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 7; column++) + { + // Both TableNextColumn() and TableSetColumnIndex() return true when a column is visible or performing width measurement. + // Because here we know that: + // - A) all our columns are contributing the same to row height + // - B) column 0 is always visible, + // We only always submit this one column and can skip others. + // More advanced per-column clipping behaviors may benefit from polling the status flags via TableGetColumnFlags(). + if (!ImGui::TableSetColumnIndex(column) && column > 0) + continue; + if (column == 0) + ImGui::Text("Line %d", row); + else + ImGui::Text("Hello world %d,%d", column, row); + } + } + ImGui::EndTable(); + } + + ImGui::Spacing(); + ImGui::TextUnformatted("Stretch + ScrollX"); + ImGui::SameLine(); + HelpMarker( + "Showcase using Stretch columns + ScrollX together: " + "this is rather unusual and only makes sense when specifying an 'inner_width' for the table!\n" + "Without an explicit value, inner_width is == outer_size.x and therefore using Stretch columns + ScrollX together doesn't make sense."); + static ImGuiTableFlags flags2 = ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_RowBg | ImGuiTableFlags_ContextMenuInBody; + static float inner_width = 1000.0f; + PushStyleCompact(); + ImGui::PushID("flags3"); + ImGui::PushItemWidth(TEXT_BASE_WIDTH * 30); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollX", &flags2, ImGuiTableFlags_ScrollX); + ImGui::DragFloat("inner_width", &inner_width, 1.0f, 0.0f, FLT_MAX, "%.1f"); + ImGui::PopItemWidth(); + ImGui::PopID(); + PopStyleCompact(); + if (ImGui::BeginTable("table2", 7, flags2, outer_size, inner_width)) + { + for (int cell = 0; cell < 20 * 7; cell++) + { + ImGui::TableNextColumn(); + ImGui::Text("Hello world %d,%d", ImGui::TableGetColumnIndex(), ImGui::TableGetRowIndex()); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Columns flags"); + if (ImGui::TreeNode("Columns flags")) + { + // Create a first table just to show all the options/flags we want to make visible in our example! + const int column_count = 3; + const char* column_names[column_count] = { "One", "Two", "Three" }; + static ImGuiTableColumnFlags column_flags[column_count] = { ImGuiTableColumnFlags_DefaultSort, ImGuiTableColumnFlags_None, ImGuiTableColumnFlags_DefaultHide }; + static ImGuiTableColumnFlags column_flags_out[column_count] = { 0, 0, 0 }; // Output from TableGetColumnFlags() + + if (ImGui::BeginTable("table_columns_flags_checkboxes", column_count, ImGuiTableFlags_None)) + { + PushStyleCompact(); + for (int column = 0; column < column_count; column++) + { + ImGui::TableNextColumn(); + ImGui::PushID(column); + ImGui::AlignTextToFramePadding(); // FIXME-TABLE: Workaround for wrong text baseline propagation + ImGui::Text("'%s'", column_names[column]); + ImGui::Spacing(); + ImGui::Text("Input flags:"); + EditTableColumnsFlags(&column_flags[column]); + ImGui::Spacing(); + ImGui::Text("Output flags:"); + ShowTableColumnsStatusFlags(column_flags_out[column]); + ImGui::PopID(); + } + PopStyleCompact(); + ImGui::EndTable(); + } + + // Create the real table we care about for the example! + // We use a scrolling table to be able to showcase the difference between the _IsEnabled and _IsVisible flags above, otherwise in + // a non-scrolling table columns are always visible (unless using ImGuiTableFlags_NoKeepColumnsVisible + resizing the parent window down) + const ImGuiTableFlags flags + = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY + | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV + | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Sortable; + ImVec2 outer_size = ImVec2(0.0f, TEXT_BASE_HEIGHT * 9); + if (ImGui::BeginTable("table_columns_flags", column_count, flags, outer_size)) + { + for (int column = 0; column < column_count; column++) + ImGui::TableSetupColumn(column_names[column], column_flags[column]); + ImGui::TableHeadersRow(); + for (int column = 0; column < column_count; column++) + column_flags_out[column] = ImGui::TableGetColumnFlags(column); + float indent_step = (float)((int)TEXT_BASE_WIDTH / 2); + for (int row = 0; row < 8; row++) + { + ImGui::Indent(indent_step); // Add some indentation to demonstrate usage of per-column IndentEnable/IndentDisable flags. + ImGui::TableNextRow(); + for (int column = 0; column < column_count; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("%s %s", (column == 0) ? "Indented" : "Hello", ImGui::TableGetColumnName(column)); + } + } + ImGui::Unindent(indent_step * 8.0f); + + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Columns widths"); + if (ImGui::TreeNode("Columns widths")) + { + HelpMarker("Using TableSetupColumn() to setup default width."); + + static ImGuiTableFlags flags1 = ImGuiTableFlags_Borders | ImGuiTableFlags_NoBordersInBodyUntilResize; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags1, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBodyUntilResize", &flags1, ImGuiTableFlags_NoBordersInBodyUntilResize); + PopStyleCompact(); + if (ImGui::BeginTable("table1", 3, flags1)) + { + // We could also set ImGuiTableFlags_SizingFixedFit on the table and all columns will default to ImGuiTableColumnFlags_WidthFixed. + ImGui::TableSetupColumn("one", ImGuiTableColumnFlags_WidthFixed, 100.0f); // Default to 100.0f + ImGui::TableSetupColumn("two", ImGuiTableColumnFlags_WidthFixed, 200.0f); // Default to 200.0f + ImGui::TableSetupColumn("three", ImGuiTableColumnFlags_WidthFixed); // Default to auto + ImGui::TableHeadersRow(); + for (int row = 0; row < 4; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + if (row == 0) + ImGui::Text("(w: %5.1f)", ImGui::GetContentRegionAvail().x); + else + ImGui::Text("Hello %d,%d", column, row); + } + } + ImGui::EndTable(); + } + + HelpMarker("Using TableSetupColumn() to setup explicit width.\n\nUnless _NoKeepColumnsVisible is set, fixed columns with set width may still be shrunk down if there's not enough space in the host."); + + static ImGuiTableFlags flags2 = ImGuiTableFlags_None; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_NoKeepColumnsVisible", &flags2, ImGuiTableFlags_NoKeepColumnsVisible); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerV", &flags2, ImGuiTableFlags_BordersInnerV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterV", &flags2, ImGuiTableFlags_BordersOuterV); + PopStyleCompact(); + if (ImGui::BeginTable("table2", 4, flags2)) + { + // We could also set ImGuiTableFlags_SizingFixedFit on the table and all columns will default to ImGuiTableColumnFlags_WidthFixed. + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 100.0f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 15.0f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 30.0f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 15.0f); + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 4; column++) + { + ImGui::TableSetColumnIndex(column); + if (row == 0) + ImGui::Text("(w: %5.1f)", ImGui::GetContentRegionAvail().x); + else + ImGui::Text("Hello %d,%d", column, row); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Nested tables"); + if (ImGui::TreeNode("Nested tables")) + { + HelpMarker("This demonstrate embedding a table into another table cell."); + + if (ImGui::BeginTable("table_nested1", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable)) + { + ImGui::TableSetupColumn("A0"); + ImGui::TableSetupColumn("A1"); + ImGui::TableHeadersRow(); + + ImGui::TableNextColumn(); + ImGui::Text("A0 Row 0"); + { + float rows_height = TEXT_BASE_HEIGHT * 2; + if (ImGui::BeginTable("table_nested2", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable)) + { + ImGui::TableSetupColumn("B0"); + ImGui::TableSetupColumn("B1"); + ImGui::TableHeadersRow(); + + ImGui::TableNextRow(ImGuiTableRowFlags_None, rows_height); + ImGui::TableNextColumn(); + ImGui::Text("B0 Row 0"); + ImGui::TableNextColumn(); + ImGui::Text("B1 Row 0"); + ImGui::TableNextRow(ImGuiTableRowFlags_None, rows_height); + ImGui::TableNextColumn(); + ImGui::Text("B0 Row 1"); + ImGui::TableNextColumn(); + ImGui::Text("B1 Row 1"); + + ImGui::EndTable(); + } + } + ImGui::TableNextColumn(); ImGui::Text("A1 Row 0"); + ImGui::TableNextColumn(); ImGui::Text("A0 Row 1"); + ImGui::TableNextColumn(); ImGui::Text("A1 Row 1"); + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Row height"); + if (ImGui::TreeNode("Row height")) + { + HelpMarker("You can pass a 'min_row_height' to TableNextRow().\n\nRows are padded with 'style.CellPadding.y' on top and bottom, so effectively the minimum row height will always be >= 'style.CellPadding.y * 2.0f'.\n\nWe cannot honor a _maximum_ row height as that would requires a unique clipping rectangle per row."); + if (ImGui::BeginTable("table_row_height", 1, ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersInnerV)) + { + for (int row = 0; row < 10; row++) + { + float min_row_height = (float)(int)(TEXT_BASE_HEIGHT * 0.30f * row); + ImGui::TableNextRow(ImGuiTableRowFlags_None, min_row_height); + ImGui::TableNextColumn(); + ImGui::Text("min_row_height = %.2f", min_row_height); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Outer size"); + if (ImGui::TreeNode("Outer size")) + { + // Showcasing use of ImGuiTableFlags_NoHostExtendX and ImGuiTableFlags_NoHostExtendY + // Important to that note how the two flags have slightly different behaviors! + ImGui::Text("Using NoHostExtendX and NoHostExtendY:"); + PushStyleCompact(); + static ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_ContextMenuInBody | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX; + ImGui::CheckboxFlags("ImGuiTableFlags_NoHostExtendX", &flags, ImGuiTableFlags_NoHostExtendX); + ImGui::SameLine(); HelpMarker("Make outer width auto-fit to columns, overriding outer_size.x value.\n\nOnly available when ScrollX/ScrollY are disabled and Stretch columns are not used."); + ImGui::CheckboxFlags("ImGuiTableFlags_NoHostExtendY", &flags, ImGuiTableFlags_NoHostExtendY); + ImGui::SameLine(); HelpMarker("Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit).\n\nOnly available when ScrollX/ScrollY are disabled. Data below the limit will be clipped and not visible."); + PopStyleCompact(); + + ImVec2 outer_size = ImVec2(0.0f, TEXT_BASE_HEIGHT * 5.5f); + if (ImGui::BeginTable("table1", 3, flags, outer_size)) + { + for (int row = 0; row < 10; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableNextColumn(); + ImGui::Text("Cell %d,%d", column, row); + } + } + ImGui::EndTable(); + } + ImGui::SameLine(); + ImGui::Text("Hello!"); + + ImGui::Spacing(); + + ImGui::Text("Using explicit size:"); + if (ImGui::BeginTable("table2", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg, ImVec2(TEXT_BASE_WIDTH * 30, 0.0f))) + { + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableNextColumn(); + ImGui::Text("Cell %d,%d", column, row); + } + } + ImGui::EndTable(); + } + ImGui::SameLine(); + if (ImGui::BeginTable("table3", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg, ImVec2(TEXT_BASE_WIDTH * 30, 0.0f))) + { + for (int row = 0; row < 3; row++) + { + ImGui::TableNextRow(0, TEXT_BASE_HEIGHT * 1.5f); + for (int column = 0; column < 3; column++) + { + ImGui::TableNextColumn(); + ImGui::Text("Cell %d,%d", column, row); + } + } + ImGui::EndTable(); + } + + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Background color"); + if (ImGui::TreeNode("Background color")) + { + static ImGuiTableFlags flags = ImGuiTableFlags_RowBg; + static int row_bg_type = 1; + static int row_bg_target = 1; + static int cell_bg_type = 1; + + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Borders", &flags, ImGuiTableFlags_Borders); + ImGui::CheckboxFlags("ImGuiTableFlags_RowBg", &flags, ImGuiTableFlags_RowBg); + ImGui::SameLine(); HelpMarker("ImGuiTableFlags_RowBg automatically sets RowBg0 to alternative colors pulled from the Style."); + ImGui::Combo("row bg type", (int*)&row_bg_type, "None\0Red\0Gradient\0"); + ImGui::Combo("row bg target", (int*)&row_bg_target, "RowBg0\0RowBg1\0"); ImGui::SameLine(); HelpMarker("Target RowBg0 to override the alternating odd/even colors,\nTarget RowBg1 to blend with them."); + ImGui::Combo("cell bg type", (int*)&cell_bg_type, "None\0Blue\0"); ImGui::SameLine(); HelpMarker("We are colorizing cells to B1->C2 here."); + IM_ASSERT(row_bg_type >= 0 && row_bg_type <= 2); + IM_ASSERT(row_bg_target >= 0 && row_bg_target <= 1); + IM_ASSERT(cell_bg_type >= 0 && cell_bg_type <= 1); + PopStyleCompact(); + + if (ImGui::BeginTable("table1", 5, flags)) + { + for (int row = 0; row < 6; row++) + { + ImGui::TableNextRow(); + + // Demonstrate setting a row background color with 'ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBgX, ...)' + // We use a transparent color so we can see the one behind in case our target is RowBg1 and RowBg0 was already targeted by the ImGuiTableFlags_RowBg flag. + if (row_bg_type != 0) + { + ImU32 row_bg_color = ImGui::GetColorU32(row_bg_type == 1 ? ImVec4(0.7f, 0.3f, 0.3f, 0.65f) : ImVec4(0.2f + row * 0.1f, 0.2f, 0.2f, 0.65f)); // Flat or Gradient? + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0 + row_bg_target, row_bg_color); + } + + // Fill cells + for (int column = 0; column < 5; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("%c%c", 'A' + row, '0' + column); + + // Change background of Cells B1->C2 + // Demonstrate setting a cell background color with 'ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ...)' + // (the CellBg color will be blended over the RowBg and ColumnBg colors) + // We can also pass a column number as a third parameter to TableSetBgColor() and do this outside the column loop. + if (row >= 1 && row <= 2 && column >= 1 && column <= 2 && cell_bg_type == 1) + { + ImU32 cell_bg_color = ImGui::GetColorU32(ImVec4(0.3f, 0.3f, 0.7f, 0.65f)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, cell_bg_color); + } + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Tree view"); + if (ImGui::TreeNode("Tree view")) + { + static ImGuiTableFlags flags = ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody; + + if (ImGui::BeginTable("3ways", 3, flags)) + { + // The first column will use the default _WidthStretch when ScrollX is Off and _WidthFixed when ScrollX is On + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_NoHide); + ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 12.0f); + ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 18.0f); + ImGui::TableHeadersRow(); + + // Simple storage to output a dummy file-system. + struct MyTreeNode + { + const char* Name; + const char* Type; + int Size; + int ChildIdx; + int ChildCount; + static void DisplayNode(const MyTreeNode* node, const MyTreeNode* all_nodes) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + const bool is_folder = (node->ChildCount > 0); + if (is_folder) + { + bool open = ImGui::TreeNodeEx(node->Name, ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::TableNextColumn(); + ImGui::TextDisabled("--"); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(node->Type); + if (open) + { + for (int child_n = 0; child_n < node->ChildCount; child_n++) + DisplayNode(&all_nodes[node->ChildIdx + child_n], all_nodes); + ImGui::TreePop(); + } + } + else + { + ImGui::TreeNodeEx(node->Name, ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_Bullet | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::TableNextColumn(); + ImGui::Text("%d", node->Size); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(node->Type); + } + } + }; + static const MyTreeNode nodes[] = + { + { "Root", "Folder", -1, 1, 3 }, // 0 + { "Music", "Folder", -1, 4, 2 }, // 1 + { "Textures", "Folder", -1, 6, 3 }, // 2 + { "desktop.ini", "System file", 1024, -1,-1 }, // 3 + { "File1_a.wav", "Audio file", 123000, -1,-1 }, // 4 + { "File1_b.wav", "Audio file", 456000, -1,-1 }, // 5 + { "Image001.png", "Image file", 203128, -1,-1 }, // 6 + { "Copy of Image001.png", "Image file", 203256, -1,-1 }, // 7 + { "Copy of Image001 (Final2).png","Image file", 203512, -1,-1 }, // 8 + }; + + MyTreeNode::DisplayNode(&nodes[0], nodes); + + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Item width"); + if (ImGui::TreeNode("Item width")) + { + HelpMarker( + "Showcase using PushItemWidth() and how it is preserved on a per-column basis.\n\n" + "Note that on auto-resizing non-resizable fixed columns, querying the content width for e.g. right-alignment doesn't make sense."); + if (ImGui::BeginTable("table_item_width", 3, ImGuiTableFlags_Borders)) + { + ImGui::TableSetupColumn("small"); + ImGui::TableSetupColumn("half"); + ImGui::TableSetupColumn("right-align"); + ImGui::TableHeadersRow(); + + for (int row = 0; row < 3; row++) + { + ImGui::TableNextRow(); + if (row == 0) + { + // Setup ItemWidth once (instead of setting up every time, which is also possible but less efficient) + ImGui::TableSetColumnIndex(0); + ImGui::PushItemWidth(TEXT_BASE_WIDTH * 3.0f); // Small + ImGui::TableSetColumnIndex(1); + ImGui::PushItemWidth(-ImGui::GetContentRegionAvail().x * 0.5f); + ImGui::TableSetColumnIndex(2); + ImGui::PushItemWidth(-FLT_MIN); // Right-aligned + } + + // Draw our contents + static float dummy_f = 0.0f; + ImGui::PushID(row); + ImGui::TableSetColumnIndex(0); + ImGui::SliderFloat("float0", &dummy_f, 0.0f, 1.0f); + ImGui::TableSetColumnIndex(1); + ImGui::SliderFloat("float1", &dummy_f, 0.0f, 1.0f); + ImGui::TableSetColumnIndex(2); + ImGui::SliderFloat("float2", &dummy_f, 0.0f, 1.0f); + ImGui::PopID(); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + // Demonstrate using TableHeader() calls instead of TableHeadersRow() + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Custom headers"); + if (ImGui::TreeNode("Custom headers")) + { + const int COLUMNS_COUNT = 3; + if (ImGui::BeginTable("table_custom_headers", COLUMNS_COUNT, ImGuiTableFlags_Borders | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable)) + { + ImGui::TableSetupColumn("Apricot"); + ImGui::TableSetupColumn("Banana"); + ImGui::TableSetupColumn("Cherry"); + + // Dummy entire-column selection storage + // FIXME: It would be nice to actually demonstrate full-featured selection using those checkbox. + static bool column_selected[3] = {}; + + // Instead of calling TableHeadersRow() we'll submit custom headers ourselves + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + for (int column = 0; column < COLUMNS_COUNT; column++) + { + ImGui::TableSetColumnIndex(column); + const char* column_name = ImGui::TableGetColumnName(column); // Retrieve name passed to TableSetupColumn() + ImGui::PushID(column); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + ImGui::Checkbox("##checkall", &column_selected[column]); + ImGui::PopStyleVar(); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::TableHeader(column_name); + ImGui::PopID(); + } + + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + char buf[32]; + sprintf(buf, "Cell %d,%d", column, row); + ImGui::TableSetColumnIndex(column); + ImGui::Selectable(buf, column_selected[column]); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + // Demonstrate creating custom context menus inside columns, while playing it nice with context menus provided by TableHeadersRow()/TableHeader() + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Context menus"); + if (ImGui::TreeNode("Context menus")) + { + HelpMarker("By default, right-clicking over a TableHeadersRow()/TableHeader() line will open the default context-menu.\nUsing ImGuiTableFlags_ContextMenuInBody we also allow right-clicking over columns body."); + static ImGuiTableFlags flags1 = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders | ImGuiTableFlags_ContextMenuInBody; + + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_ContextMenuInBody", &flags1, ImGuiTableFlags_ContextMenuInBody); + PopStyleCompact(); + + // Context Menus: first example + // [1.1] Right-click on the TableHeadersRow() line to open the default table context menu. + // [1.2] Right-click in columns also open the default table context menu (if ImGuiTableFlags_ContextMenuInBody is set) + const int COLUMNS_COUNT = 3; + if (ImGui::BeginTable("table_context_menu", COLUMNS_COUNT, flags1)) + { + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + + // [1.1]] Right-click on the TableHeadersRow() line to open the default table context menu. + ImGui::TableHeadersRow(); + + // Submit dummy contents + for (int row = 0; row < 4; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < COLUMNS_COUNT; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Cell %d,%d", column, row); + } + } + ImGui::EndTable(); + } + + // Context Menus: second example + // [2.1] Right-click on the TableHeadersRow() line to open the default table context menu. + // [2.2] Right-click on the ".." to open a custom popup + // [2.3] Right-click in columns to open another custom popup + HelpMarker("Demonstrate mixing table context menu (over header), item context button (over button) and custom per-colum context menu (over column body)."); + ImGuiTableFlags flags2 = ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders; + if (ImGui::BeginTable("table_context_menu_2", COLUMNS_COUNT, flags2)) + { + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + + // [2.1] Right-click on the TableHeadersRow() line to open the default table context menu. + ImGui::TableHeadersRow(); + for (int row = 0; row < 4; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < COLUMNS_COUNT; column++) + { + // Submit dummy contents + ImGui::TableSetColumnIndex(column); + ImGui::Text("Cell %d,%d", column, row); + ImGui::SameLine(); + + // [2.2] Right-click on the ".." to open a custom popup + ImGui::PushID(row * COLUMNS_COUNT + column); + ImGui::SmallButton(".."); + if (ImGui::BeginPopupContextItem()) + { + ImGui::Text("This is the popup for Button(\"..\") in Cell %d,%d", column, row); + if (ImGui::Button("Close")) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + ImGui::PopID(); + } + } + + // [2.3] Right-click anywhere in columns to open another custom popup + // (instead of testing for !IsAnyItemHovered() we could also call OpenPopup() with ImGuiPopupFlags_NoOpenOverExistingPopup + // to manage popup priority as the popups triggers, here "are we hovering a column" are overlapping) + int hovered_column = -1; + for (int column = 0; column < COLUMNS_COUNT + 1; column++) + { + ImGui::PushID(column); + if (ImGui::TableGetColumnFlags(column) & ImGuiTableColumnFlags_IsHovered) + hovered_column = column; + if (hovered_column == column && !ImGui::IsAnyItemHovered() && ImGui::IsMouseReleased(1)) + ImGui::OpenPopup("MyPopup"); + if (ImGui::BeginPopup("MyPopup")) + { + if (column == COLUMNS_COUNT) + ImGui::Text("This is a custom popup for unused space after the last column."); + else + ImGui::Text("This is a custom popup for Column %d", column); + if (ImGui::Button("Close")) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + ImGui::PopID(); + } + + ImGui::EndTable(); + ImGui::Text("Hovered column: %d", hovered_column); + } + ImGui::TreePop(); + } + + // Demonstrate creating multiple tables with the same ID + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Synced instances"); + if (ImGui::TreeNode("Synced instances")) + { + HelpMarker("Multiple tables with the same identifier will share their settings, width, visibility, order etc."); + for (int n = 0; n < 3; n++) + { + char buf[32]; + sprintf(buf, "Synced Table %d", n); + bool open = ImGui::CollapsingHeader(buf, ImGuiTreeNodeFlags_DefaultOpen); + if (open && ImGui::BeginTable("Table", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings)) + { + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + ImGui::TableHeadersRow(); + for (int cell = 0; cell < 9; cell++) + { + ImGui::TableNextColumn(); + ImGui::Text("this cell %d", cell); + } + ImGui::EndTable(); + } + } + ImGui::TreePop(); + } + + // Demonstrate using Sorting facilities + // This is a simplified version of the "Advanced" example, where we mostly focus on the code necessary to handle sorting. + // Note that the "Advanced" example also showcase manually triggering a sort (e.g. if item quantities have been modified) + static const char* template_items_names[] = + { + "Banana", "Apple", "Cherry", "Watermelon", "Grapefruit", "Strawberry", "Mango", + "Kiwi", "Orange", "Pineapple", "Blueberry", "Plum", "Coconut", "Pear", "Apricot" + }; + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Sorting"); + if (ImGui::TreeNode("Sorting")) + { + // Create item list + static ImVector items; + if (items.Size == 0) + { + items.resize(50, MyItem()); + for (int n = 0; n < items.Size; n++) + { + const int template_n = n % IM_ARRAYSIZE(template_items_names); + MyItem& item = items[n]; + item.ID = n; + item.Name = template_items_names[template_n]; + item.Quantity = (n * n - n) % 20; // Assign default quantities + } + } + + // Options + static ImGuiTableFlags flags = + ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti + | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_NoBordersInBody + | ImGuiTableFlags_ScrollY; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_SortMulti", &flags, ImGuiTableFlags_SortMulti); + ImGui::SameLine(); HelpMarker("When sorting is enabled: hold shift when clicking headers to sort on multiple column. TableGetSortSpecs() may return specs where (SpecsCount > 1)."); + ImGui::CheckboxFlags("ImGuiTableFlags_SortTristate", &flags, ImGuiTableFlags_SortTristate); + ImGui::SameLine(); HelpMarker("When sorting is enabled: allow no sorting, disable default sorting. TableGetSortSpecs() may return specs where (SpecsCount == 0)."); + PopStyleCompact(); + + if (ImGui::BeginTable("table_sorting", 4, flags, ImVec2(0.0f, TEXT_BASE_HEIGHT * 15), 0.0f)) + { + // Declare columns + // We use the "user_id" parameter of TableSetupColumn() to specify a user id that will be stored in the sort specifications. + // This is so our sort function can identify a column given our own identifier. We could also identify them based on their index! + // Demonstrate using a mixture of flags among available sort-related flags: + // - ImGuiTableColumnFlags_DefaultSort + // - ImGuiTableColumnFlags_NoSort / ImGuiTableColumnFlags_NoSortAscending / ImGuiTableColumnFlags_NoSortDescending + // - ImGuiTableColumnFlags_PreferSortAscending / ImGuiTableColumnFlags_PreferSortDescending + ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_ID); + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_Name); + ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_Action); + ImGui::TableSetupColumn("Quantity", ImGuiTableColumnFlags_PreferSortDescending | ImGuiTableColumnFlags_WidthStretch, 0.0f, MyItemColumnID_Quantity); + ImGui::TableSetupScrollFreeze(0, 1); // Make row always visible + ImGui::TableHeadersRow(); + + // Sort our data if sort specs have been changed! + if (ImGuiTableSortSpecs* sorts_specs = ImGui::TableGetSortSpecs()) + if (sorts_specs->SpecsDirty) + { + MyItem::s_current_sort_specs = sorts_specs; // Store in variable accessible by the sort function. + if (items.Size > 1) + qsort(&items[0], (size_t)items.Size, sizeof(items[0]), MyItem::CompareWithSortSpecs); + MyItem::s_current_sort_specs = NULL; + sorts_specs->SpecsDirty = false; + } + + // Demonstrate using clipper for large vertical lists + ImGuiListClipper clipper; + clipper.Begin(items.Size); + while (clipper.Step()) + for (int row_n = clipper.DisplayStart; row_n < clipper.DisplayEnd; row_n++) + { + // Display a data item + MyItem* item = &items[row_n]; + ImGui::PushID(item->ID); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%04d", item->ID); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(item->Name); + ImGui::TableNextColumn(); + ImGui::SmallButton("None"); + ImGui::TableNextColumn(); + ImGui::Text("%d", item->Quantity); + ImGui::PopID(); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + // In this example we'll expose most table flags and settings. + // For specific flags and settings refer to the corresponding section for more detailed explanation. + // This section is mostly useful to experiment with combining certain flags or settings with each others. + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); // [DEBUG] + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Advanced"); + if (ImGui::TreeNode("Advanced")) + { + static ImGuiTableFlags flags = + ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable + | ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti + | ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_NoBordersInBody + | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY + | ImGuiTableFlags_SizingFixedFit; + + enum ContentsType { CT_Text, CT_Button, CT_SmallButton, CT_FillButton, CT_Selectable, CT_SelectableSpanRow }; + static int contents_type = CT_SelectableSpanRow; + const char* contents_type_names[] = { "Text", "Button", "SmallButton", "FillButton", "Selectable", "Selectable (span row)" }; + static int freeze_cols = 1; + static int freeze_rows = 1; + static int items_count = IM_ARRAYSIZE(template_items_names) * 2; + static ImVec2 outer_size_value = ImVec2(0.0f, TEXT_BASE_HEIGHT * 12); + static float row_min_height = 0.0f; // Auto + static float inner_width_with_scroll = 0.0f; // Auto-extend + static bool outer_size_enabled = true; + static bool show_headers = true; + static bool show_wrapped_text = false; + //static ImGuiTextFilter filter; + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); // FIXME-TABLE: Enabling this results in initial clipped first pass on table which tend to affects column sizing + if (ImGui::TreeNode("Options")) + { + // Make the UI compact because there are so many fields + PushStyleCompact(); + ImGui::PushItemWidth(TEXT_BASE_WIDTH * 28.0f); + + if (ImGui::TreeNodeEx("Features:", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_Reorderable", &flags, ImGuiTableFlags_Reorderable); + ImGui::CheckboxFlags("ImGuiTableFlags_Hideable", &flags, ImGuiTableFlags_Hideable); + ImGui::CheckboxFlags("ImGuiTableFlags_Sortable", &flags, ImGuiTableFlags_Sortable); + ImGui::CheckboxFlags("ImGuiTableFlags_NoSavedSettings", &flags, ImGuiTableFlags_NoSavedSettings); + ImGui::CheckboxFlags("ImGuiTableFlags_ContextMenuInBody", &flags, ImGuiTableFlags_ContextMenuInBody); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Decorations:", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::CheckboxFlags("ImGuiTableFlags_RowBg", &flags, ImGuiTableFlags_RowBg); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersV", &flags, ImGuiTableFlags_BordersV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterV", &flags, ImGuiTableFlags_BordersOuterV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerV", &flags, ImGuiTableFlags_BordersInnerV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersH", &flags, ImGuiTableFlags_BordersH); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterH", &flags, ImGuiTableFlags_BordersOuterH); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerH", &flags, ImGuiTableFlags_BordersInnerH); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body (borders will always appears in Headers"); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBodyUntilResize", &flags, ImGuiTableFlags_NoBordersInBodyUntilResize); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body until hovered for resize (borders will always appears in Headers)"); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Sizing:", ImGuiTreeNodeFlags_DefaultOpen)) + { + EditTableSizingFlags(&flags); + ImGui::SameLine(); HelpMarker("In the Advanced demo we override the policy of each column so those table-wide settings have less effect that typical."); + ImGui::CheckboxFlags("ImGuiTableFlags_NoHostExtendX", &flags, ImGuiTableFlags_NoHostExtendX); + ImGui::SameLine(); HelpMarker("Make outer width auto-fit to columns, overriding outer_size.x value.\n\nOnly available when ScrollX/ScrollY are disabled and Stretch columns are not used."); + ImGui::CheckboxFlags("ImGuiTableFlags_NoHostExtendY", &flags, ImGuiTableFlags_NoHostExtendY); + ImGui::SameLine(); HelpMarker("Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit).\n\nOnly available when ScrollX/ScrollY are disabled. Data below the limit will be clipped and not visible."); + ImGui::CheckboxFlags("ImGuiTableFlags_NoKeepColumnsVisible", &flags, ImGuiTableFlags_NoKeepColumnsVisible); + ImGui::SameLine(); HelpMarker("Only available if ScrollX is disabled."); + ImGui::CheckboxFlags("ImGuiTableFlags_PreciseWidths", &flags, ImGuiTableFlags_PreciseWidths); + ImGui::SameLine(); HelpMarker("Disable distributing remainder width to stretched columns (width allocation on a 100-wide table with 3 columns: Without this flag: 33,33,34. With this flag: 33,33,33). With larger number of columns, resizing will appear to be less smooth."); + ImGui::CheckboxFlags("ImGuiTableFlags_NoClip", &flags, ImGuiTableFlags_NoClip); + ImGui::SameLine(); HelpMarker("Disable clipping rectangle for every individual columns (reduce draw command count, items will be able to overflow into other columns). Generally incompatible with ScrollFreeze options."); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Padding:", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::CheckboxFlags("ImGuiTableFlags_PadOuterX", &flags, ImGuiTableFlags_PadOuterX); + ImGui::CheckboxFlags("ImGuiTableFlags_NoPadOuterX", &flags, ImGuiTableFlags_NoPadOuterX); + ImGui::CheckboxFlags("ImGuiTableFlags_NoPadInnerX", &flags, ImGuiTableFlags_NoPadInnerX); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Scrolling:", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollX", &flags, ImGuiTableFlags_ScrollX); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetFrameHeight()); + ImGui::DragInt("freeze_cols", &freeze_cols, 0.2f, 0, 9, NULL, ImGuiSliderFlags_NoInput); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollY", &flags, ImGuiTableFlags_ScrollY); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetFrameHeight()); + ImGui::DragInt("freeze_rows", &freeze_rows, 0.2f, 0, 9, NULL, ImGuiSliderFlags_NoInput); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Sorting:", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::CheckboxFlags("ImGuiTableFlags_SortMulti", &flags, ImGuiTableFlags_SortMulti); + ImGui::SameLine(); HelpMarker("When sorting is enabled: hold shift when clicking headers to sort on multiple column. TableGetSortSpecs() may return specs where (SpecsCount > 1)."); + ImGui::CheckboxFlags("ImGuiTableFlags_SortTristate", &flags, ImGuiTableFlags_SortTristate); + ImGui::SameLine(); HelpMarker("When sorting is enabled: allow no sorting, disable default sorting. TableGetSortSpecs() may return specs where (SpecsCount == 0)."); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Other:", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Checkbox("show_headers", &show_headers); + ImGui::Checkbox("show_wrapped_text", &show_wrapped_text); + + ImGui::DragFloat2("##OuterSize", &outer_size_value.x); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Checkbox("outer_size", &outer_size_enabled); + ImGui::SameLine(); + HelpMarker("If scrolling is disabled (ScrollX and ScrollY not set):\n" + "- The table is output directly in the parent window.\n" + "- OuterSize.x < 0.0f will right-align the table.\n" + "- OuterSize.x = 0.0f will narrow fit the table unless there are any Stretch column.\n" + "- OuterSize.y then becomes the minimum size for the table, which will extend vertically if there are more rows (unless NoHostExtendY is set)."); + + // From a user point of view we will tend to use 'inner_width' differently depending on whether our table is embedding scrolling. + // To facilitate toying with this demo we will actually pass 0.0f to the BeginTable() when ScrollX is disabled. + ImGui::DragFloat("inner_width (when ScrollX active)", &inner_width_with_scroll, 1.0f, 0.0f, FLT_MAX); + + ImGui::DragFloat("row_min_height", &row_min_height, 1.0f, 0.0f, FLT_MAX); + ImGui::SameLine(); HelpMarker("Specify height of the Selectable item."); + + ImGui::DragInt("items_count", &items_count, 0.1f, 0, 9999); + ImGui::Combo("items_type (first column)", &contents_type, contents_type_names, IM_ARRAYSIZE(contents_type_names)); + //filter.Draw("filter"); + ImGui::TreePop(); + } + + ImGui::PopItemWidth(); + PopStyleCompact(); + ImGui::Spacing(); + ImGui::TreePop(); + } + + // Update item list if we changed the number of items + static ImVector items; + static ImVector selection; + static bool items_need_sort = false; + if (items.Size != items_count) + { + items.resize(items_count, MyItem()); + for (int n = 0; n < items_count; n++) + { + const int template_n = n % IM_ARRAYSIZE(template_items_names); + MyItem& item = items[n]; + item.ID = n; + item.Name = template_items_names[template_n]; + item.Quantity = (template_n == 3) ? 10 : (template_n == 4) ? 20 : 0; // Assign default quantities + } + } + + const ImDrawList* parent_draw_list = ImGui::GetWindowDrawList(); + const int parent_draw_list_draw_cmd_count = parent_draw_list->CmdBuffer.Size; + ImVec2 table_scroll_cur, table_scroll_max; // For debug display + const ImDrawList* table_draw_list = NULL; // " + + // Submit table + const float inner_width_to_use = (flags & ImGuiTableFlags_ScrollX) ? inner_width_with_scroll : 0.0f; + if (ImGui::BeginTable("table_advanced", 6, flags, outer_size_enabled ? outer_size_value : ImVec2(0, 0), inner_width_to_use)) + { + // Declare columns + // We use the "user_id" parameter of TableSetupColumn() to specify a user id that will be stored in the sort specifications. + // This is so our sort function can identify a column given our own identifier. We could also identify them based on their index! + ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHide, 0.0f, MyItemColumnID_ID); + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_Name); + ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_Action); + ImGui::TableSetupColumn("Quantity", ImGuiTableColumnFlags_PreferSortDescending, 0.0f, MyItemColumnID_Quantity); + ImGui::TableSetupColumn("Description", (flags & ImGuiTableFlags_NoHostExtendX) ? 0 : ImGuiTableColumnFlags_WidthStretch, 0.0f, MyItemColumnID_Description); + ImGui::TableSetupColumn("Hidden", ImGuiTableColumnFlags_DefaultHide | ImGuiTableColumnFlags_NoSort); + ImGui::TableSetupScrollFreeze(freeze_cols, freeze_rows); + + // Sort our data if sort specs have been changed! + ImGuiTableSortSpecs* sorts_specs = ImGui::TableGetSortSpecs(); + if (sorts_specs && sorts_specs->SpecsDirty) + items_need_sort = true; + if (sorts_specs && items_need_sort && items.Size > 1) + { + MyItem::s_current_sort_specs = sorts_specs; // Store in variable accessible by the sort function. + qsort(&items[0], (size_t)items.Size, sizeof(items[0]), MyItem::CompareWithSortSpecs); + MyItem::s_current_sort_specs = NULL; + sorts_specs->SpecsDirty = false; + } + items_need_sort = false; + + // Take note of whether we are currently sorting based on the Quantity field, + // we will use this to trigger sorting when we know the data of this column has been modified. + const bool sorts_specs_using_quantity = (ImGui::TableGetColumnFlags(3) & ImGuiTableColumnFlags_IsSorted) != 0; + + // Show headers + if (show_headers) + ImGui::TableHeadersRow(); + + // Show data + // FIXME-TABLE FIXME-NAV: How we can get decent up/down even though we have the buttons here? + ImGui::PushButtonRepeat(true); +#if 1 + // Demonstrate using clipper for large vertical lists + ImGuiListClipper clipper; + clipper.Begin(items.Size); + while (clipper.Step()) + { + for (int row_n = clipper.DisplayStart; row_n < clipper.DisplayEnd; row_n++) +#else + // Without clipper + { + for (int row_n = 0; row_n < items.Size; row_n++) +#endif + { + MyItem* item = &items[row_n]; + //if (!filter.PassFilter(item->Name)) + // continue; + + const bool item_is_selected = selection.contains(item->ID); + ImGui::PushID(item->ID); + ImGui::TableNextRow(ImGuiTableRowFlags_None, row_min_height); + + // For the demo purpose we can select among different type of items submitted in the first column + ImGui::TableSetColumnIndex(0); + char label[32]; + sprintf(label, "%04d", item->ID); + if (contents_type == CT_Text) + ImGui::TextUnformatted(label); + else if (contents_type == CT_Button) + ImGui::Button(label); + else if (contents_type == CT_SmallButton) + ImGui::SmallButton(label); + else if (contents_type == CT_FillButton) + ImGui::Button(label, ImVec2(-FLT_MIN, 0.0f)); + else if (contents_type == CT_Selectable || contents_type == CT_SelectableSpanRow) + { + ImGuiSelectableFlags selectable_flags = (contents_type == CT_SelectableSpanRow) ? ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap : ImGuiSelectableFlags_None; + if (ImGui::Selectable(label, item_is_selected, selectable_flags, ImVec2(0, row_min_height))) + { + if (ImGui::GetIO().KeyCtrl) + { + if (item_is_selected) + selection.find_erase_unsorted(item->ID); + else + selection.push_back(item->ID); + } + else + { + selection.clear(); + selection.push_back(item->ID); + } + } + } + + if (ImGui::TableSetColumnIndex(1)) + ImGui::TextUnformatted(item->Name); + + // Here we demonstrate marking our data set as needing to be sorted again if we modified a quantity, + // and we are currently sorting on the column showing the Quantity. + // To avoid triggering a sort while holding the button, we only trigger it when the button has been released. + // You will probably need a more advanced system in your code if you want to automatically sort when a specific entry changes. + if (ImGui::TableSetColumnIndex(2)) + { + if (ImGui::SmallButton("Chop")) { item->Quantity += 1; } + if (sorts_specs_using_quantity && ImGui::IsItemDeactivated()) { items_need_sort = true; } + ImGui::SameLine(); + if (ImGui::SmallButton("Eat")) { item->Quantity -= 1; } + if (sorts_specs_using_quantity && ImGui::IsItemDeactivated()) { items_need_sort = true; } + } + + if (ImGui::TableSetColumnIndex(3)) + ImGui::Text("%d", item->Quantity); + + ImGui::TableSetColumnIndex(4); + if (show_wrapped_text) + ImGui::TextWrapped("Lorem ipsum dolor sit amet"); + else + ImGui::Text("Lorem ipsum dolor sit amet"); + + if (ImGui::TableSetColumnIndex(5)) + ImGui::Text("1234"); + + ImGui::PopID(); + } + } + ImGui::PopButtonRepeat(); + + // Store some info to display debug details below + table_scroll_cur = ImVec2(ImGui::GetScrollX(), ImGui::GetScrollY()); + table_scroll_max = ImVec2(ImGui::GetScrollMaxX(), ImGui::GetScrollMaxY()); + table_draw_list = ImGui::GetWindowDrawList(); + ImGui::EndTable(); + } + static bool show_debug_details = false; + ImGui::Checkbox("Debug details", &show_debug_details); + if (show_debug_details && table_draw_list) + { + ImGui::SameLine(0.0f, 0.0f); + const int table_draw_list_draw_cmd_count = table_draw_list->CmdBuffer.Size; + if (table_draw_list == parent_draw_list) + ImGui::Text(": DrawCmd: +%d (in same window)", + table_draw_list_draw_cmd_count - parent_draw_list_draw_cmd_count); + else + ImGui::Text(": DrawCmd: +%d (in child window), Scroll: (%.f/%.f) (%.f/%.f)", + table_draw_list_draw_cmd_count - 1, table_scroll_cur.x, table_scroll_max.x, table_scroll_cur.y, table_scroll_max.y); + } + ImGui::TreePop(); + } + + ImGui::PopID(); + + ShowDemoWindowColumns(); + + if (disable_indent) + ImGui::PopStyleVar(); +} + +// Demonstrate old/legacy Columns API! +// [2020: Columns are under-featured and not maintained. Prefer using the more flexible and powerful BeginTable() API!] +static void ShowDemoWindowColumns() +{ + IMGUI_DEMO_MARKER("Columns (legacy API)"); + bool open = ImGui::TreeNode("Legacy Columns API"); + ImGui::SameLine(); + HelpMarker("Columns() is an old API! Prefer using the more flexible and powerful BeginTable() API!"); + if (!open) + return; + + // Basic columns + IMGUI_DEMO_MARKER("Columns (legacy API)/Basic"); + if (ImGui::TreeNode("Basic")) + { + ImGui::Text("Without border:"); + ImGui::Columns(3, "mycolumns3", false); // 3-ways, no border + ImGui::Separator(); + for (int n = 0; n < 14; n++) + { + char label[32]; + sprintf(label, "Item %d", n); + if (ImGui::Selectable(label)) {} + //if (ImGui::Button(label, ImVec2(-FLT_MIN,0.0f))) {} + ImGui::NextColumn(); + } + ImGui::Columns(1); + ImGui::Separator(); + + ImGui::Text("With border:"); + ImGui::Columns(4, "mycolumns"); // 4-ways, with border + ImGui::Separator(); + ImGui::Text("ID"); ImGui::NextColumn(); + ImGui::Text("Name"); ImGui::NextColumn(); + ImGui::Text("Path"); ImGui::NextColumn(); + ImGui::Text("Hovered"); ImGui::NextColumn(); + ImGui::Separator(); + const char* names[3] = { "One", "Two", "Three" }; + const char* paths[3] = { "/path/one", "/path/two", "/path/three" }; + static int selected = -1; + for (int i = 0; i < 3; i++) + { + char label[32]; + sprintf(label, "%04d", i); + if (ImGui::Selectable(label, selected == i, ImGuiSelectableFlags_SpanAllColumns)) + selected = i; + bool hovered = ImGui::IsItemHovered(); + ImGui::NextColumn(); + ImGui::Text(names[i]); ImGui::NextColumn(); + ImGui::Text(paths[i]); ImGui::NextColumn(); + ImGui::Text("%d", hovered); ImGui::NextColumn(); + } + ImGui::Columns(1); + ImGui::Separator(); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Columns (legacy API)/Borders"); + if (ImGui::TreeNode("Borders")) + { + // NB: Future columns API should allow automatic horizontal borders. + static bool h_borders = true; + static bool v_borders = true; + static int columns_count = 4; + const int lines_count = 3; + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::DragInt("##columns_count", &columns_count, 0.1f, 2, 10, "%d columns"); + if (columns_count < 2) + columns_count = 2; + ImGui::SameLine(); + ImGui::Checkbox("horizontal", &h_borders); + ImGui::SameLine(); + ImGui::Checkbox("vertical", &v_borders); + ImGui::Columns(columns_count, NULL, v_borders); + for (int i = 0; i < columns_count * lines_count; i++) + { + if (h_borders && ImGui::GetColumnIndex() == 0) + ImGui::Separator(); + ImGui::Text("%c%c%c", 'a' + i, 'a' + i, 'a' + i); + ImGui::Text("Width %.2f", ImGui::GetColumnWidth()); + ImGui::Text("Avail %.2f", ImGui::GetContentRegionAvail().x); + ImGui::Text("Offset %.2f", ImGui::GetColumnOffset()); + ImGui::Text("Long text that is likely to clip"); + ImGui::Button("Button", ImVec2(-FLT_MIN, 0.0f)); + ImGui::NextColumn(); + } + ImGui::Columns(1); + if (h_borders) + ImGui::Separator(); + ImGui::TreePop(); + } + + // Create multiple items in a same cell before switching to next column + IMGUI_DEMO_MARKER("Columns (legacy API)/Mixed items"); + if (ImGui::TreeNode("Mixed items")) + { + ImGui::Columns(3, "mixed"); + ImGui::Separator(); + + ImGui::Text("Hello"); + ImGui::Button("Banana"); + ImGui::NextColumn(); + + ImGui::Text("ImGui"); + ImGui::Button("Apple"); + static float foo = 1.0f; + ImGui::InputFloat("red", &foo, 0.05f, 0, "%.3f"); + ImGui::Text("An extra line here."); + ImGui::NextColumn(); + + ImGui::Text("Sailor"); + ImGui::Button("Corniflower"); + static float bar = 1.0f; + ImGui::InputFloat("blue", &bar, 0.05f, 0, "%.3f"); + ImGui::NextColumn(); + + if (ImGui::CollapsingHeader("Category A")) { ImGui::Text("Blah blah blah"); } ImGui::NextColumn(); + if (ImGui::CollapsingHeader("Category B")) { ImGui::Text("Blah blah blah"); } ImGui::NextColumn(); + if (ImGui::CollapsingHeader("Category C")) { ImGui::Text("Blah blah blah"); } ImGui::NextColumn(); + ImGui::Columns(1); + ImGui::Separator(); + ImGui::TreePop(); + } + + // Word wrapping + IMGUI_DEMO_MARKER("Columns (legacy API)/Word-wrapping"); + if (ImGui::TreeNode("Word-wrapping")) + { + ImGui::Columns(2, "word-wrapping"); + ImGui::Separator(); + ImGui::TextWrapped("The quick brown fox jumps over the lazy dog."); + ImGui::TextWrapped("Hello Left"); + ImGui::NextColumn(); + ImGui::TextWrapped("The quick brown fox jumps over the lazy dog."); + ImGui::TextWrapped("Hello Right"); + ImGui::Columns(1); + ImGui::Separator(); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Columns (legacy API)/Horizontal Scrolling"); + if (ImGui::TreeNode("Horizontal Scrolling")) + { + ImGui::SetNextWindowContentSize(ImVec2(1500.0f, 0.0f)); + ImVec2 child_size = ImVec2(0, ImGui::GetFontSize() * 20.0f); + ImGui::BeginChild("##ScrollingRegion", child_size, false, ImGuiWindowFlags_HorizontalScrollbar); + ImGui::Columns(10); + + // Also demonstrate using clipper for large vertical lists + int ITEMS_COUNT = 2000; + ImGuiListClipper clipper; + clipper.Begin(ITEMS_COUNT); + while (clipper.Step()) + { + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + for (int j = 0; j < 10; j++) + { + ImGui::Text("Line %d Column %d...", i, j); + ImGui::NextColumn(); + } + } + ImGui::Columns(1); + ImGui::EndChild(); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Columns (legacy API)/Tree"); + if (ImGui::TreeNode("Tree")) + { + ImGui::Columns(2, "tree", true); + for (int x = 0; x < 3; x++) + { + bool open1 = ImGui::TreeNode((void*)(intptr_t)x, "Node%d", x); + ImGui::NextColumn(); + ImGui::Text("Node contents"); + ImGui::NextColumn(); + if (open1) + { + for (int y = 0; y < 3; y++) + { + bool open2 = ImGui::TreeNode((void*)(intptr_t)y, "Node%d.%d", x, y); + ImGui::NextColumn(); + ImGui::Text("Node contents"); + if (open2) + { + ImGui::Text("Even more contents"); + if (ImGui::TreeNode("Tree in column")) + { + ImGui::Text("The quick brown fox jumps over the lazy dog"); + ImGui::TreePop(); + } + } + ImGui::NextColumn(); + if (open2) + ImGui::TreePop(); + } + ImGui::TreePop(); + } + } + ImGui::Columns(1); + ImGui::TreePop(); + } + + ImGui::TreePop(); +} + +namespace ImGui { extern ImGuiKeyData* GetKeyData(ImGuiKey key); } + +static void ShowDemoWindowMisc() +{ + IMGUI_DEMO_MARKER("Filtering"); + if (ImGui::CollapsingHeader("Filtering")) + { + // Helper class to easy setup a text filter. + // You may want to implement a more feature-full filtering scheme in your own application. + static ImGuiTextFilter filter; + ImGui::Text("Filter usage:\n" + " \"\" display all lines\n" + " \"xxx\" display lines containing \"xxx\"\n" + " \"xxx,yyy\" display lines containing \"xxx\" or \"yyy\"\n" + " \"-xxx\" hide lines containing \"xxx\""); + filter.Draw(); + const char* lines[] = { "aaa1.c", "bbb1.c", "ccc1.c", "aaa2.cpp", "bbb2.cpp", "ccc2.cpp", "abc.h", "hello, world" }; + for (int i = 0; i < IM_ARRAYSIZE(lines); i++) + if (filter.PassFilter(lines[i])) + ImGui::BulletText("%s", lines[i]); + } + + IMGUI_DEMO_MARKER("Inputs, Navigation & Focus"); + if (ImGui::CollapsingHeader("Inputs, Navigation & Focus")) + { + ImGuiIO& io = ImGui::GetIO(); + + // Display ImGuiIO output flags + ImGui::Text("WantCaptureMouse: %d", io.WantCaptureMouse); + ImGui::Text("WantCaptureMouseUnlessPopupClose: %d", io.WantCaptureMouseUnlessPopupClose); + ImGui::Text("WantCaptureKeyboard: %d", io.WantCaptureKeyboard); + ImGui::Text("WantTextInput: %d", io.WantTextInput); + ImGui::Text("WantSetMousePos: %d", io.WantSetMousePos); + ImGui::Text("NavActive: %d, NavVisible: %d", io.NavActive, io.NavVisible); + + // Display Mouse state + IMGUI_DEMO_MARKER("Inputs, Navigation & Focus/Mouse State"); + if (ImGui::TreeNode("Mouse State")) + { + if (ImGui::IsMousePosValid()) + ImGui::Text("Mouse pos: (%g, %g)", io.MousePos.x, io.MousePos.y); + else + ImGui::Text("Mouse pos: "); + ImGui::Text("Mouse delta: (%g, %g)", io.MouseDelta.x, io.MouseDelta.y); + + int count = IM_ARRAYSIZE(io.MouseDown); + ImGui::Text("Mouse down:"); for (int i = 0; i < count; i++) if (ImGui::IsMouseDown(i)) { ImGui::SameLine(); ImGui::Text("b%d (%.02f secs)", i, io.MouseDownDuration[i]); } + ImGui::Text("Mouse clicked:"); for (int i = 0; i < count; i++) if (ImGui::IsMouseClicked(i)) { ImGui::SameLine(); ImGui::Text("b%d (%d)", i, ImGui::GetMouseClickedCount(i)); } + ImGui::Text("Mouse released:"); for (int i = 0; i < count; i++) if (ImGui::IsMouseReleased(i)) { ImGui::SameLine(); ImGui::Text("b%d", i); } + ImGui::Text("Mouse wheel: %.1f", io.MouseWheel); + ImGui::Text("Pen Pressure: %.1f", io.PenPressure); // Note: currently unused + ImGui::TreePop(); + } + + // Display Keyboard/Mouse state + IMGUI_DEMO_MARKER("Inputs, Navigation & Focus/Keyboard, Gamepad & Navigation State"); + if (ImGui::TreeNode("Keyboard, Gamepad & Navigation State")) + { + // We iterate both legacy native range and named ImGuiKey ranges, which is a little odd but this allow displaying the data for old/new backends. + // User code should never have to go through such hoops: old code may use native keycodes, new code may use ImGuiKey codes. +#ifdef IMGUI_DISABLE_OBSOLETE_KEYIO + struct funcs { static bool IsLegacyNativeDupe(ImGuiKey) { return false; } }; + const ImGuiKey key_first = ImGuiKey_NamedKey_BEGIN; +#else + struct funcs { static bool IsLegacyNativeDupe(ImGuiKey key) { return key < 512 && ImGui::GetIO().KeyMap[key] != -1; } }; // Hide Native<>ImGuiKey duplicates when both exists in the array + const ImGuiKey key_first = 0; +#endif + ImGui::Text("Keys down:"); for (ImGuiKey key = key_first; key < ImGuiKey_COUNT; key++) { if (funcs::IsLegacyNativeDupe(key)) continue; if (ImGui::IsKeyDown(key)) { ImGui::SameLine(); ImGui::Text("\"%s\" %d (%.02f secs)", ImGui::GetKeyName(key), key, ImGui::GetKeyData(key)->DownDuration); } } + ImGui::Text("Keys pressed:"); for (ImGuiKey key = key_first; key < ImGuiKey_COUNT; key++) { if (funcs::IsLegacyNativeDupe(key)) continue; if (ImGui::IsKeyPressed(key)) { ImGui::SameLine(); ImGui::Text("\"%s\" %d", ImGui::GetKeyName(key), key); } } + ImGui::Text("Keys released:"); for (ImGuiKey key = key_first; key < ImGuiKey_COUNT; key++) { if (funcs::IsLegacyNativeDupe(key)) continue; if (ImGui::IsKeyReleased(key)) { ImGui::SameLine(); ImGui::Text("\"%s\" %d", ImGui::GetKeyName(key), key); } } + ImGui::Text("Keys mods: %s%s%s%s", io.KeyCtrl ? "CTRL " : "", io.KeyShift ? "SHIFT " : "", io.KeyAlt ? "ALT " : "", io.KeySuper ? "SUPER " : ""); + ImGui::Text("Chars queue:"); for (int i = 0; i < io.InputQueueCharacters.Size; i++) { ImWchar c = io.InputQueueCharacters[i]; ImGui::SameLine(); ImGui::Text("\'%c\' (0x%04X)", (c > ' ' && c <= 255) ? (char)c : '?', c); } // FIXME: We should convert 'c' to UTF-8 here but the functions are not public. + ImGui::Text("NavInputs down:"); for (int i = 0; i < IM_ARRAYSIZE(io.NavInputs); i++) if (io.NavInputs[i] > 0.0f) { ImGui::SameLine(); ImGui::Text("[%d] %.2f (%.02f secs)", i, io.NavInputs[i], io.NavInputsDownDuration[i]); } + ImGui::Text("NavInputs pressed:"); for (int i = 0; i < IM_ARRAYSIZE(io.NavInputs); i++) if (io.NavInputsDownDuration[i] == 0.0f) { ImGui::SameLine(); ImGui::Text("[%d]", i); } + + // Draw an arbitrary US keyboard layout to visualize translated keys + { + const ImVec2 key_size = ImVec2(35.0f, 35.0f); + const float key_rounding = 3.0f; + const ImVec2 key_face_size = ImVec2(25.0f, 25.0f); + const ImVec2 key_face_pos = ImVec2(5.0f, 3.0f); + const float key_face_rounding = 2.0f; + const ImVec2 key_label_pos = ImVec2(7.0f, 4.0f); + const ImVec2 key_step = ImVec2(key_size.x - 1.0f, key_size.y - 1.0f); + const float key_row_offset = 9.0f; + + ImVec2 board_min = ImGui::GetCursorScreenPos(); + ImVec2 board_max = ImVec2(board_min.x + 3 * key_step.x + 2 * key_row_offset + 10.0f, board_min.y + 3 * key_step.y + 10.0f); + ImVec2 start_pos = ImVec2(board_min.x + 5.0f - key_step.x, board_min.y); + + struct KeyLayoutData { int Row, Col; const char* Label; ImGuiKey Key; }; + const KeyLayoutData keys_to_display[] = + { + { 0, 0, "", ImGuiKey_Tab }, { 0, 1, "Q", ImGuiKey_Q }, { 0, 2, "W", ImGuiKey_W }, { 0, 3, "E", ImGuiKey_E }, { 0, 4, "R", ImGuiKey_R }, + { 1, 0, "", ImGuiKey_CapsLock }, { 1, 1, "A", ImGuiKey_A }, { 1, 2, "S", ImGuiKey_S }, { 1, 3, "D", ImGuiKey_D }, { 1, 4, "F", ImGuiKey_F }, + { 2, 0, "", ImGuiKey_LeftShift },{ 2, 1, "Z", ImGuiKey_Z }, { 2, 2, "X", ImGuiKey_X }, { 2, 3, "C", ImGuiKey_C }, { 2, 4, "V", ImGuiKey_V } + }; + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + draw_list->PushClipRect(board_min, board_max, true); + for (int n = 0; n < IM_ARRAYSIZE(keys_to_display); n++) + { + const KeyLayoutData* key_data = &keys_to_display[n]; + ImVec2 key_min = ImVec2(start_pos.x + key_data->Col * key_step.x + key_data->Row * key_row_offset, start_pos.y + key_data->Row * key_step.y); + ImVec2 key_max = ImVec2(key_min.x + key_size.x, key_min.y + key_size.y); + draw_list->AddRectFilled(key_min, key_max, IM_COL32(204, 204, 204, 255), key_rounding); + draw_list->AddRect(key_min, key_max, IM_COL32(24, 24, 24, 255), key_rounding); + ImVec2 face_min = ImVec2(key_min.x + key_face_pos.x, key_min.y + key_face_pos.y); + ImVec2 face_max = ImVec2(face_min.x + key_face_size.x, face_min.y + key_face_size.y); + draw_list->AddRect(face_min, face_max, IM_COL32(193, 193, 193, 255), key_face_rounding, ImDrawFlags_None, 2.0f); + draw_list->AddRectFilled(face_min, face_max, IM_COL32(252, 252, 252, 255), key_face_rounding); + ImVec2 label_min = ImVec2(key_min.x + key_label_pos.x, key_min.y + key_label_pos.y); + draw_list->AddText(label_min, IM_COL32(64, 64, 64, 255), key_data->Label); + if (ImGui::IsKeyDown(key_data->Key)) + draw_list->AddRectFilled(key_min, key_max, IM_COL32(255, 0, 0, 128), key_rounding); + } + draw_list->PopClipRect(); + ImGui::Dummy(ImVec2(board_max.x - board_min.x, board_max.y - board_min.y)); + } + ImGui::TreePop(); + } + + if (ImGui::TreeNode("Capture override")) + { + ImGui::Button("Hovering me sets the\nkeyboard capture flag"); + if (ImGui::IsItemHovered()) + ImGui::CaptureKeyboardFromApp(true); + ImGui::SameLine(); + ImGui::Button("Holding me clears the\nthe keyboard capture flag"); + if (ImGui::IsItemActive()) + ImGui::CaptureKeyboardFromApp(false); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Inputs, Navigation & Focus/Tabbing"); + if (ImGui::TreeNode("Tabbing")) + { + ImGui::Text("Use TAB/SHIFT+TAB to cycle through keyboard editable fields."); + static char buf[32] = "hello"; + ImGui::InputText("1", buf, IM_ARRAYSIZE(buf)); + ImGui::InputText("2", buf, IM_ARRAYSIZE(buf)); + ImGui::InputText("3", buf, IM_ARRAYSIZE(buf)); + ImGui::PushAllowKeyboardFocus(false); + ImGui::InputText("4 (tab skip)", buf, IM_ARRAYSIZE(buf)); + ImGui::SameLine(); HelpMarker("Item won't be cycled through when using TAB or Shift+Tab."); + ImGui::PopAllowKeyboardFocus(); + ImGui::InputText("5", buf, IM_ARRAYSIZE(buf)); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Inputs, Navigation & Focus/Focus from code"); + if (ImGui::TreeNode("Focus from code")) + { + bool focus_1 = ImGui::Button("Focus on 1"); ImGui::SameLine(); + bool focus_2 = ImGui::Button("Focus on 2"); ImGui::SameLine(); + bool focus_3 = ImGui::Button("Focus on 3"); + int has_focus = 0; + static char buf[128] = "click on a button to set focus"; + + if (focus_1) ImGui::SetKeyboardFocusHere(); + ImGui::InputText("1", buf, IM_ARRAYSIZE(buf)); + if (ImGui::IsItemActive()) has_focus = 1; + + if (focus_2) ImGui::SetKeyboardFocusHere(); + ImGui::InputText("2", buf, IM_ARRAYSIZE(buf)); + if (ImGui::IsItemActive()) has_focus = 2; + + ImGui::PushAllowKeyboardFocus(false); + if (focus_3) ImGui::SetKeyboardFocusHere(); + ImGui::InputText("3 (tab skip)", buf, IM_ARRAYSIZE(buf)); + if (ImGui::IsItemActive()) has_focus = 3; + ImGui::SameLine(); HelpMarker("Item won't be cycled through when using TAB or Shift+Tab."); + ImGui::PopAllowKeyboardFocus(); + + if (has_focus) + ImGui::Text("Item with focus: %d", has_focus); + else + ImGui::Text("Item with focus: "); + + // Use >= 0 parameter to SetKeyboardFocusHere() to focus an upcoming item + static float f3[3] = { 0.0f, 0.0f, 0.0f }; + int focus_ahead = -1; + if (ImGui::Button("Focus on X")) { focus_ahead = 0; } ImGui::SameLine(); + if (ImGui::Button("Focus on Y")) { focus_ahead = 1; } ImGui::SameLine(); + if (ImGui::Button("Focus on Z")) { focus_ahead = 2; } + if (focus_ahead != -1) ImGui::SetKeyboardFocusHere(focus_ahead); + ImGui::SliderFloat3("Float3", &f3[0], 0.0f, 1.0f); + + ImGui::TextWrapped("NB: Cursor & selection are preserved when refocusing last used item in code."); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Inputs, Navigation & Focus/Dragging"); + if (ImGui::TreeNode("Dragging")) + { + ImGui::TextWrapped("You can use ImGui::GetMouseDragDelta(0) to query for the dragged amount on any widget."); + for (int button = 0; button < 3; button++) + { + ImGui::Text("IsMouseDragging(%d):", button); + ImGui::Text(" w/ default threshold: %d,", ImGui::IsMouseDragging(button)); + ImGui::Text(" w/ zero threshold: %d,", ImGui::IsMouseDragging(button, 0.0f)); + ImGui::Text(" w/ large threshold: %d,", ImGui::IsMouseDragging(button, 20.0f)); + } + + ImGui::Button("Drag Me"); + if (ImGui::IsItemActive()) + ImGui::GetForegroundDrawList()->AddLine(io.MouseClickedPos[0], io.MousePos, ImGui::GetColorU32(ImGuiCol_Button), 4.0f); // Draw a line between the button and the mouse cursor + + // Drag operations gets "unlocked" when the mouse has moved past a certain threshold + // (the default threshold is stored in io.MouseDragThreshold). You can request a lower or higher + // threshold using the second parameter of IsMouseDragging() and GetMouseDragDelta(). + ImVec2 value_raw = ImGui::GetMouseDragDelta(0, 0.0f); + ImVec2 value_with_lock_threshold = ImGui::GetMouseDragDelta(0); + ImVec2 mouse_delta = io.MouseDelta; + ImGui::Text("GetMouseDragDelta(0):"); + ImGui::Text(" w/ default threshold: (%.1f, %.1f)", value_with_lock_threshold.x, value_with_lock_threshold.y); + ImGui::Text(" w/ zero threshold: (%.1f, %.1f)", value_raw.x, value_raw.y); + ImGui::Text("io.MouseDelta: (%.1f, %.1f)", mouse_delta.x, mouse_delta.y); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Inputs, Navigation & Focus/Mouse cursors"); + if (ImGui::TreeNode("Mouse cursors")) + { + const char* mouse_cursors_names[] = { "Arrow", "TextInput", "ResizeAll", "ResizeNS", "ResizeEW", "ResizeNESW", "ResizeNWSE", "Hand", "NotAllowed" }; + IM_ASSERT(IM_ARRAYSIZE(mouse_cursors_names) == ImGuiMouseCursor_COUNT); + + ImGuiMouseCursor current = ImGui::GetMouseCursor(); + ImGui::Text("Current mouse cursor = %d: %s", current, mouse_cursors_names[current]); + ImGui::Text("Hover to see mouse cursors:"); + ImGui::SameLine(); HelpMarker( + "Your application can render a different mouse cursor based on what ImGui::GetMouseCursor() returns. " + "If software cursor rendering (io.MouseDrawCursor) is set ImGui will draw the right cursor for you, " + "otherwise your backend needs to handle it."); + for (int i = 0; i < ImGuiMouseCursor_COUNT; i++) + { + char label[32]; + sprintf(label, "Mouse cursor %d: %s", i, mouse_cursors_names[i]); + ImGui::Bullet(); ImGui::Selectable(label, false); + if (ImGui::IsItemHovered()) + ImGui::SetMouseCursor(i); + } + ImGui::TreePop(); + } + } +} + +//----------------------------------------------------------------------------- +// [SECTION] About Window / ShowAboutWindow() +// Access from Dear ImGui Demo -> Tools -> About +//----------------------------------------------------------------------------- + +void ImGui::ShowAboutWindow(bool* p_open) +{ + if (!ImGui::Begin("About Dear ImGui", p_open, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::End(); + return; + } + IMGUI_DEMO_MARKER("Tools/About Dear ImGui"); + ImGui::Text("Dear ImGui %s", ImGui::GetVersion()); + ImGui::Separator(); + ImGui::Text("By Omar Cornut and all Dear ImGui contributors."); + ImGui::Text("Dear ImGui is licensed under the MIT License, see LICENSE for more information."); + + static bool show_config_info = false; + ImGui::Checkbox("Config/Build Information", &show_config_info); + if (show_config_info) + { + ImGuiIO& io = ImGui::GetIO(); + ImGuiStyle& style = ImGui::GetStyle(); + + bool copy_to_clipboard = ImGui::Button("Copy to clipboard"); + ImVec2 child_size = ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 18); + ImGui::BeginChildFrame(ImGui::GetID("cfg_infos"), child_size, ImGuiWindowFlags_NoMove); + if (copy_to_clipboard) + { + ImGui::LogToClipboard(); + ImGui::LogText("```\n"); // Back quotes will make text appears without formatting when pasting on GitHub + } + + ImGui::Text("Dear ImGui %s (%d)", IMGUI_VERSION, IMGUI_VERSION_NUM); + ImGui::Separator(); + ImGui::Text("sizeof(size_t): %d, sizeof(ImDrawIdx): %d, sizeof(ImDrawVert): %d", (int)sizeof(size_t), (int)sizeof(ImDrawIdx), (int)sizeof(ImDrawVert)); + ImGui::Text("define: __cplusplus=%d", (int)__cplusplus); +#ifdef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_OBSOLETE_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_OBSOLETE_KEYIO + ImGui::Text("define: IMGUI_DISABLE_OBSOLETE_KEYIO"); +#endif +#ifdef IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_WIN32_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_WIN32_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_FILE_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_FILE_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_DEFAULT_ALLOCATORS + ImGui::Text("define: IMGUI_DISABLE_DEFAULT_ALLOCATORS"); +#endif +#ifdef IMGUI_USE_BGRA_PACKED_COLOR + ImGui::Text("define: IMGUI_USE_BGRA_PACKED_COLOR"); +#endif +#ifdef _WIN32 + ImGui::Text("define: _WIN32"); +#endif +#ifdef _WIN64 + ImGui::Text("define: _WIN64"); +#endif +#ifdef __linux__ + ImGui::Text("define: __linux__"); +#endif +#ifdef __APPLE__ + ImGui::Text("define: __APPLE__"); +#endif +#ifdef _MSC_VER + ImGui::Text("define: _MSC_VER=%d", _MSC_VER); +#endif +#ifdef _MSVC_LANG + ImGui::Text("define: _MSVC_LANG=%d", (int)_MSVC_LANG); +#endif +#ifdef __MINGW32__ + ImGui::Text("define: __MINGW32__"); +#endif +#ifdef __MINGW64__ + ImGui::Text("define: __MINGW64__"); +#endif +#ifdef __GNUC__ + ImGui::Text("define: __GNUC__=%d", (int)__GNUC__); +#endif +#ifdef __clang_version__ + ImGui::Text("define: __clang_version__=%s", __clang_version__); +#endif + ImGui::Separator(); + ImGui::Text("io.BackendPlatformName: %s", io.BackendPlatformName ? io.BackendPlatformName : "NULL"); + ImGui::Text("io.BackendRendererName: %s", io.BackendRendererName ? io.BackendRendererName : "NULL"); + ImGui::Text("io.ConfigFlags: 0x%08X", io.ConfigFlags); + if (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) ImGui::Text(" NavEnableKeyboard"); + if (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) ImGui::Text(" NavEnableGamepad"); + if (io.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos) ImGui::Text(" NavEnableSetMousePos"); + if (io.ConfigFlags & ImGuiConfigFlags_NavNoCaptureKeyboard) ImGui::Text(" NavNoCaptureKeyboard"); + if (io.ConfigFlags & ImGuiConfigFlags_NoMouse) ImGui::Text(" NoMouse"); + if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange) ImGui::Text(" NoMouseCursorChange"); + if (io.MouseDrawCursor) ImGui::Text("io.MouseDrawCursor"); + if (io.ConfigMacOSXBehaviors) ImGui::Text("io.ConfigMacOSXBehaviors"); + if (io.ConfigInputTextCursorBlink) ImGui::Text("io.ConfigInputTextCursorBlink"); + if (io.ConfigWindowsResizeFromEdges) ImGui::Text("io.ConfigWindowsResizeFromEdges"); + if (io.ConfigWindowsMoveFromTitleBarOnly) ImGui::Text("io.ConfigWindowsMoveFromTitleBarOnly"); + if (io.ConfigMemoryCompactTimer >= 0.0f) ImGui::Text("io.ConfigMemoryCompactTimer = %.1f", io.ConfigMemoryCompactTimer); + ImGui::Text("io.BackendFlags: 0x%08X", io.BackendFlags); + if (io.BackendFlags & ImGuiBackendFlags_HasGamepad) ImGui::Text(" HasGamepad"); + if (io.BackendFlags & ImGuiBackendFlags_HasMouseCursors) ImGui::Text(" HasMouseCursors"); + if (io.BackendFlags & ImGuiBackendFlags_HasSetMousePos) ImGui::Text(" HasSetMousePos"); + if (io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset) ImGui::Text(" RendererHasVtxOffset"); + ImGui::Separator(); + ImGui::Text("io.Fonts: %d fonts, Flags: 0x%08X, TexSize: %d,%d", io.Fonts->Fonts.Size, io.Fonts->Flags, io.Fonts->TexWidth, io.Fonts->TexHeight); + ImGui::Text("io.DisplaySize: %.2f,%.2f", io.DisplaySize.x, io.DisplaySize.y); + ImGui::Text("io.DisplayFramebufferScale: %.2f,%.2f", io.DisplayFramebufferScale.x, io.DisplayFramebufferScale.y); + ImGui::Separator(); + ImGui::Text("style.WindowPadding: %.2f,%.2f", style.WindowPadding.x, style.WindowPadding.y); + ImGui::Text("style.WindowBorderSize: %.2f", style.WindowBorderSize); + ImGui::Text("style.FramePadding: %.2f,%.2f", style.FramePadding.x, style.FramePadding.y); + ImGui::Text("style.FrameRounding: %.2f", style.FrameRounding); + ImGui::Text("style.FrameBorderSize: %.2f", style.FrameBorderSize); + ImGui::Text("style.ItemSpacing: %.2f,%.2f", style.ItemSpacing.x, style.ItemSpacing.y); + ImGui::Text("style.ItemInnerSpacing: %.2f,%.2f", style.ItemInnerSpacing.x, style.ItemInnerSpacing.y); + + if (copy_to_clipboard) + { + ImGui::LogText("\n```\n"); + ImGui::LogFinish(); + } + ImGui::EndChildFrame(); + } + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Style Editor / ShowStyleEditor() +//----------------------------------------------------------------------------- +// - ShowFontSelector() +// - ShowStyleSelector() +// - ShowStyleEditor() +//----------------------------------------------------------------------------- + +// Forward declare ShowFontAtlas() which isn't worth putting in public API yet +namespace ImGui { IMGUI_API void ShowFontAtlas(ImFontAtlas* atlas); } + +// Demo helper function to select among loaded fonts. +// Here we use the regular BeginCombo()/EndCombo() api which is the more flexible one. +void ImGui::ShowFontSelector(const char* label) +{ + ImGuiIO& io = ImGui::GetIO(); + ImFont* font_current = ImGui::GetFont(); + if (ImGui::BeginCombo(label, font_current->GetDebugName())) + { + for (int n = 0; n < io.Fonts->Fonts.Size; n++) + { + ImFont* font = io.Fonts->Fonts[n]; + ImGui::PushID((void*)font); + if (ImGui::Selectable(font->GetDebugName(), font == font_current)) + io.FontDefault = font; + ImGui::PopID(); + } + ImGui::EndCombo(); + } + ImGui::SameLine(); + HelpMarker( + "- Load additional fonts with io.Fonts->AddFontFromFileTTF().\n" + "- The font atlas is built when calling io.Fonts->GetTexDataAsXXXX() or io.Fonts->Build().\n" + "- Read FAQ and docs/FONTS.md for more details.\n" + "- If you need to add/remove fonts at runtime (e.g. for DPI change), do it before calling NewFrame()."); +} + +// Demo helper function to select among default colors. See ShowStyleEditor() for more advanced options. +// Here we use the simplified Combo() api that packs items into a single literal string. +// Useful for quick combo boxes where the choices are known locally. +bool ImGui::ShowStyleSelector(const char* label) +{ + static int style_idx = -1; + if (ImGui::Combo(label, &style_idx, "Dark\0Light\0Classic\0")) + { + switch (style_idx) + { + case 0: ImGui::StyleColorsDark(); break; + case 1: ImGui::StyleColorsLight(); break; + case 2: ImGui::StyleColorsClassic(); break; + } + return true; + } + return false; +} + +void ImGui::ShowStyleEditor(ImGuiStyle* ref) +{ + IMGUI_DEMO_MARKER("Tools/Style Editor"); + // You can pass in a reference ImGuiStyle structure to compare to, revert to and save to + // (without a reference style pointer, we will use one compared locally as a reference) + ImGuiStyle& style = ImGui::GetStyle(); + static ImGuiStyle ref_saved_style; + + // Default to using internal storage as reference + static bool init = true; + if (init && ref == NULL) + ref_saved_style = style; + init = false; + if (ref == NULL) + ref = &ref_saved_style; + + ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.50f); + + if (ImGui::ShowStyleSelector("Colors##Selector")) + ref_saved_style = style; + ImGui::ShowFontSelector("Fonts##Selector"); + + // Simplified Settings (expose floating-pointer border sizes as boolean representing 0.0f or 1.0f) + if (ImGui::SliderFloat("FrameRounding", &style.FrameRounding, 0.0f, 12.0f, "%.0f")) + style.GrabRounding = style.FrameRounding; // Make GrabRounding always the same value as FrameRounding + { bool border = (style.WindowBorderSize > 0.0f); if (ImGui::Checkbox("WindowBorder", &border)) { style.WindowBorderSize = border ? 1.0f : 0.0f; } } + ImGui::SameLine(); + { bool border = (style.FrameBorderSize > 0.0f); if (ImGui::Checkbox("FrameBorder", &border)) { style.FrameBorderSize = border ? 1.0f : 0.0f; } } + ImGui::SameLine(); + { bool border = (style.PopupBorderSize > 0.0f); if (ImGui::Checkbox("PopupBorder", &border)) { style.PopupBorderSize = border ? 1.0f : 0.0f; } } + + // Save/Revert button + if (ImGui::Button("Save Ref")) + *ref = ref_saved_style = style; + ImGui::SameLine(); + if (ImGui::Button("Revert Ref")) + style = *ref; + ImGui::SameLine(); + HelpMarker( + "Save/Revert in local non-persistent storage. Default Colors definition are not affected. " + "Use \"Export\" below to save them somewhere."); + + ImGui::Separator(); + + if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_None)) + { + if (ImGui::BeginTabItem("Sizes")) + { + ImGui::Text("Main"); + ImGui::SliderFloat2("WindowPadding", (float*)&style.WindowPadding, 0.0f, 20.0f, "%.0f"); + ImGui::SliderFloat2("FramePadding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f"); + ImGui::SliderFloat2("CellPadding", (float*)&style.CellPadding, 0.0f, 20.0f, "%.0f"); + ImGui::SliderFloat2("ItemSpacing", (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f"); + ImGui::SliderFloat2("ItemInnerSpacing", (float*)&style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f"); + ImGui::SliderFloat2("TouchExtraPadding", (float*)&style.TouchExtraPadding, 0.0f, 10.0f, "%.0f"); + ImGui::SliderFloat("IndentSpacing", &style.IndentSpacing, 0.0f, 30.0f, "%.0f"); + ImGui::SliderFloat("ScrollbarSize", &style.ScrollbarSize, 1.0f, 20.0f, "%.0f"); + ImGui::SliderFloat("GrabMinSize", &style.GrabMinSize, 1.0f, 20.0f, "%.0f"); + ImGui::Text("Borders"); + ImGui::SliderFloat("WindowBorderSize", &style.WindowBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui::SliderFloat("ChildBorderSize", &style.ChildBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui::SliderFloat("PopupBorderSize", &style.PopupBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui::SliderFloat("FrameBorderSize", &style.FrameBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui::SliderFloat("TabBorderSize", &style.TabBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui::Text("Rounding"); + ImGui::SliderFloat("WindowRounding", &style.WindowRounding, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat("ChildRounding", &style.ChildRounding, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat("FrameRounding", &style.FrameRounding, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat("PopupRounding", &style.PopupRounding, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat("ScrollbarRounding", &style.ScrollbarRounding, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat("GrabRounding", &style.GrabRounding, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat("LogSliderDeadzone", &style.LogSliderDeadzone, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat("TabRounding", &style.TabRounding, 0.0f, 12.0f, "%.0f"); + ImGui::Text("Alignment"); + ImGui::SliderFloat2("WindowTitleAlign", (float*)&style.WindowTitleAlign, 0.0f, 1.0f, "%.2f"); + int window_menu_button_position = style.WindowMenuButtonPosition + 1; + if (ImGui::Combo("WindowMenuButtonPosition", (int*)&window_menu_button_position, "None\0Left\0Right\0")) + style.WindowMenuButtonPosition = window_menu_button_position - 1; + ImGui::Combo("ColorButtonPosition", (int*)&style.ColorButtonPosition, "Left\0Right\0"); + ImGui::SliderFloat2("ButtonTextAlign", (float*)&style.ButtonTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui::SameLine(); HelpMarker("Alignment applies when a button is larger than its text content."); + ImGui::SliderFloat2("SelectableTextAlign", (float*)&style.SelectableTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui::SameLine(); HelpMarker("Alignment applies when a selectable is larger than its text content."); + ImGui::Text("Safe Area Padding"); + ImGui::SameLine(); HelpMarker("Adjust if you cannot see the edges of your screen (e.g. on a TV where scaling has not been configured)."); + ImGui::SliderFloat2("DisplaySafeAreaPadding", (float*)&style.DisplaySafeAreaPadding, 0.0f, 30.0f, "%.0f"); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Colors")) + { + static int output_dest = 0; + static bool output_only_modified = true; + if (ImGui::Button("Export")) + { + if (output_dest == 0) + ImGui::LogToClipboard(); + else + ImGui::LogToTTY(); + ImGui::LogText("ImVec4* colors = ImGui::GetStyle().Colors;" IM_NEWLINE); + for (int i = 0; i < ImGuiCol_COUNT; i++) + { + const ImVec4& col = style.Colors[i]; + const char* name = ImGui::GetStyleColorName(i); + if (!output_only_modified || memcmp(&col, &ref->Colors[i], sizeof(ImVec4)) != 0) + ImGui::LogText("colors[ImGuiCol_%s]%*s= ImVec4(%.2ff, %.2ff, %.2ff, %.2ff);" IM_NEWLINE, + name, 23 - (int)strlen(name), "", col.x, col.y, col.z, col.w); + } + ImGui::LogFinish(); + } + ImGui::SameLine(); ImGui::SetNextItemWidth(120); ImGui::Combo("##output_type", &output_dest, "To Clipboard\0To TTY\0"); + ImGui::SameLine(); ImGui::Checkbox("Only Modified Colors", &output_only_modified); + + static ImGuiTextFilter filter; + filter.Draw("Filter colors", ImGui::GetFontSize() * 16); + + static ImGuiColorEditFlags alpha_flags = 0; + if (ImGui::RadioButton("Opaque", alpha_flags == ImGuiColorEditFlags_None)) { alpha_flags = ImGuiColorEditFlags_None; } ImGui::SameLine(); + if (ImGui::RadioButton("Alpha", alpha_flags == ImGuiColorEditFlags_AlphaPreview)) { alpha_flags = ImGuiColorEditFlags_AlphaPreview; } ImGui::SameLine(); + if (ImGui::RadioButton("Both", alpha_flags == ImGuiColorEditFlags_AlphaPreviewHalf)) { alpha_flags = ImGuiColorEditFlags_AlphaPreviewHalf; } ImGui::SameLine(); + HelpMarker( + "In the color list:\n" + "Left-click on color square to open color picker,\n" + "Right-click to open edit options menu."); + + ImGui::BeginChild("##colors", ImVec2(0, 0), true, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NavFlattened); + ImGui::PushItemWidth(-160); + for (int i = 0; i < ImGuiCol_COUNT; i++) + { + const char* name = ImGui::GetStyleColorName(i); + if (!filter.PassFilter(name)) + continue; + ImGui::PushID(i); + ImGui::ColorEdit4("##color", (float*)&style.Colors[i], ImGuiColorEditFlags_AlphaBar | alpha_flags); + if (memcmp(&style.Colors[i], &ref->Colors[i], sizeof(ImVec4)) != 0) + { + // Tips: in a real user application, you may want to merge and use an icon font into the main font, + // so instead of "Save"/"Revert" you'd use icons! + // Read the FAQ and docs/FONTS.md about using icon fonts. It's really easy and super convenient! + ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); if (ImGui::Button("Save")) { ref->Colors[i] = style.Colors[i]; } + ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); if (ImGui::Button("Revert")) { style.Colors[i] = ref->Colors[i]; } + } + ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); + ImGui::TextUnformatted(name); + ImGui::PopID(); + } + ImGui::PopItemWidth(); + ImGui::EndChild(); + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Fonts")) + { + ImGuiIO& io = ImGui::GetIO(); + ImFontAtlas* atlas = io.Fonts; + HelpMarker("Read FAQ and docs/FONTS.md for details on font loading."); + ImGui::ShowFontAtlas(atlas); + + // Post-baking font scaling. Note that this is NOT the nice way of scaling fonts, read below. + // (we enforce hard clamping manually as by default DragFloat/SliderFloat allows CTRL+Click text to get out of bounds). + const float MIN_SCALE = 0.3f; + const float MAX_SCALE = 2.0f; + HelpMarker( + "Those are old settings provided for convenience.\n" + "However, the _correct_ way of scaling your UI is currently to reload your font at the designed size, " + "rebuild the font atlas, and call style.ScaleAllSizes() on a reference ImGuiStyle structure.\n" + "Using those settings here will give you poor quality results."); + static float window_scale = 1.0f; + ImGui::PushItemWidth(ImGui::GetFontSize() * 8); + if (ImGui::DragFloat("window scale", &window_scale, 0.005f, MIN_SCALE, MAX_SCALE, "%.2f", ImGuiSliderFlags_AlwaysClamp)) // Scale only this window + ImGui::SetWindowFontScale(window_scale); + ImGui::DragFloat("global scale", &io.FontGlobalScale, 0.005f, MIN_SCALE, MAX_SCALE, "%.2f", ImGuiSliderFlags_AlwaysClamp); // Scale everything + ImGui::PopItemWidth(); + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Rendering")) + { + ImGui::Checkbox("Anti-aliased lines", &style.AntiAliasedLines); + ImGui::SameLine(); + HelpMarker("When disabling anti-aliasing lines, you'll probably want to disable borders in your style as well."); + + ImGui::Checkbox("Anti-aliased lines use texture", &style.AntiAliasedLinesUseTex); + ImGui::SameLine(); + HelpMarker("Faster lines using texture data. Require backend to render with bilinear filtering (not point/nearest filtering)."); + + ImGui::Checkbox("Anti-aliased fill", &style.AntiAliasedFill); + ImGui::PushItemWidth(ImGui::GetFontSize() * 8); + ImGui::DragFloat("Curve Tessellation Tolerance", &style.CurveTessellationTol, 0.02f, 0.10f, 10.0f, "%.2f"); + if (style.CurveTessellationTol < 0.10f) style.CurveTessellationTol = 0.10f; + + // When editing the "Circle Segment Max Error" value, draw a preview of its effect on auto-tessellated circles. + ImGui::DragFloat("Circle Tessellation Max Error", &style.CircleTessellationMaxError , 0.005f, 0.10f, 5.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + if (ImGui::IsItemActive()) + { + ImGui::SetNextWindowPos(ImGui::GetCursorScreenPos()); + ImGui::BeginTooltip(); + ImGui::TextUnformatted("(R = radius, N = number of segments)"); + ImGui::Spacing(); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + const float min_widget_width = ImGui::CalcTextSize("N: MMM\nR: MMM").x; + for (int n = 0; n < 8; n++) + { + const float RAD_MIN = 5.0f; + const float RAD_MAX = 70.0f; + const float rad = RAD_MIN + (RAD_MAX - RAD_MIN) * (float)n / (8.0f - 1.0f); + + ImGui::BeginGroup(); + + ImGui::Text("R: %.f\nN: %d", rad, draw_list->_CalcCircleAutoSegmentCount(rad)); + + const float canvas_width = IM_MAX(min_widget_width, rad * 2.0f); + const float offset_x = floorf(canvas_width * 0.5f); + const float offset_y = floorf(RAD_MAX); + + const ImVec2 p1 = ImGui::GetCursorScreenPos(); + draw_list->AddCircle(ImVec2(p1.x + offset_x, p1.y + offset_y), rad, ImGui::GetColorU32(ImGuiCol_Text)); + ImGui::Dummy(ImVec2(canvas_width, RAD_MAX * 2)); + + /* + const ImVec2 p2 = ImGui::GetCursorScreenPos(); + draw_list->AddCircleFilled(ImVec2(p2.x + offset_x, p2.y + offset_y), rad, ImGui::GetColorU32(ImGuiCol_Text)); + ImGui::Dummy(ImVec2(canvas_width, RAD_MAX * 2)); + */ + + ImGui::EndGroup(); + ImGui::SameLine(); + } + ImGui::EndTooltip(); + } + ImGui::SameLine(); + HelpMarker("When drawing circle primitives with \"num_segments == 0\" tesselation will be calculated automatically."); + + ImGui::DragFloat("Global Alpha", &style.Alpha, 0.005f, 0.20f, 1.0f, "%.2f"); // Not exposing zero here so user doesn't "lose" the UI (zero alpha clips all widgets). But application code could have a toggle to switch between zero and non-zero. + ImGui::DragFloat("Disabled Alpha", &style.DisabledAlpha, 0.005f, 0.0f, 1.0f, "%.2f"); ImGui::SameLine(); HelpMarker("Additional alpha multiplier for disabled items (multiply over current value of Alpha)."); + ImGui::PopItemWidth(); + + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + ImGui::PopItemWidth(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Main Menu Bar / ShowExampleAppMainMenuBar() +//----------------------------------------------------------------------------- +// - ShowExampleAppMainMenuBar() +// - ShowExampleMenuFile() +//----------------------------------------------------------------------------- + +// Demonstrate creating a "main" fullscreen menu bar and populating it. +// Note the difference between BeginMainMenuBar() and BeginMenuBar(): +// - BeginMenuBar() = menu-bar inside current window (which needs the ImGuiWindowFlags_MenuBar flag!) +// - BeginMainMenuBar() = helper to create menu-bar-sized window at the top of the main viewport + call BeginMenuBar() into it. +static void ShowExampleAppMainMenuBar() +{ + if (ImGui::BeginMainMenuBar()) + { + if (ImGui::BeginMenu("File")) + { + ShowExampleMenuFile(); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Edit")) + { + if (ImGui::MenuItem("Undo", "CTRL+Z")) {} + if (ImGui::MenuItem("Redo", "CTRL+Y", false, false)) {} // Disabled item + ImGui::Separator(); + if (ImGui::MenuItem("Cut", "CTRL+X")) {} + if (ImGui::MenuItem("Copy", "CTRL+C")) {} + if (ImGui::MenuItem("Paste", "CTRL+V")) {} + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + } +} + +// Note that shortcuts are currently provided for display only +// (future version will add explicit flags to BeginMenu() to request processing shortcuts) +static void ShowExampleMenuFile() +{ + IMGUI_DEMO_MARKER("Examples/Menu"); + ImGui::MenuItem("(demo menu)", NULL, false, false); + if (ImGui::MenuItem("New")) {} + if (ImGui::MenuItem("Open", "Ctrl+O")) {} + if (ImGui::BeginMenu("Open Recent")) + { + ImGui::MenuItem("fish_hat.c"); + ImGui::MenuItem("fish_hat.inl"); + ImGui::MenuItem("fish_hat.h"); + if (ImGui::BeginMenu("More..")) + { + ImGui::MenuItem("Hello"); + ImGui::MenuItem("Sailor"); + if (ImGui::BeginMenu("Recurse..")) + { + ShowExampleMenuFile(); + ImGui::EndMenu(); + } + ImGui::EndMenu(); + } + ImGui::EndMenu(); + } + if (ImGui::MenuItem("Save", "Ctrl+S")) {} + if (ImGui::MenuItem("Save As..")) {} + + ImGui::Separator(); + IMGUI_DEMO_MARKER("Examples/Menu/Options"); + if (ImGui::BeginMenu("Options")) + { + static bool enabled = true; + ImGui::MenuItem("Enabled", "", &enabled); + ImGui::BeginChild("child", ImVec2(0, 60), true); + for (int i = 0; i < 10; i++) + ImGui::Text("Scrolling Text %d", i); + ImGui::EndChild(); + static float f = 0.5f; + static int n = 0; + ImGui::SliderFloat("Value", &f, 0.0f, 1.0f); + ImGui::InputFloat("Input", &f, 0.1f); + ImGui::Combo("Combo", &n, "Yes\0No\0Maybe\0\0"); + ImGui::EndMenu(); + } + + IMGUI_DEMO_MARKER("Examples/Menu/Colors"); + if (ImGui::BeginMenu("Colors")) + { + float sz = ImGui::GetTextLineHeight(); + for (int i = 0; i < ImGuiCol_COUNT; i++) + { + const char* name = ImGui::GetStyleColorName((ImGuiCol)i); + ImVec2 p = ImGui::GetCursorScreenPos(); + ImGui::GetWindowDrawList()->AddRectFilled(p, ImVec2(p.x + sz, p.y + sz), ImGui::GetColorU32((ImGuiCol)i)); + ImGui::Dummy(ImVec2(sz, sz)); + ImGui::SameLine(); + ImGui::MenuItem(name); + } + ImGui::EndMenu(); + } + + // Here we demonstrate appending again to the "Options" menu (which we already created above) + // Of course in this demo it is a little bit silly that this function calls BeginMenu("Options") twice. + // In a real code-base using it would make senses to use this feature from very different code locations. + if (ImGui::BeginMenu("Options")) // <-- Append! + { + IMGUI_DEMO_MARKER("Examples/Menu/Append to an existing menu"); + static bool b = true; + ImGui::Checkbox("SomeOption", &b); + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Disabled", false)) // Disabled + { + IM_ASSERT(0); + } + if (ImGui::MenuItem("Checked", NULL, true)) {} + if (ImGui::MenuItem("Quit", "Alt+F4")) {} +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Debug Console / ShowExampleAppConsole() +//----------------------------------------------------------------------------- + +// Demonstrate creating a simple console window, with scrolling, filtering, completion and history. +// For the console example, we are using a more C++ like approach of declaring a class to hold both data and functions. +struct ExampleAppConsole +{ + char InputBuf[256]; + ImVector Items; + ImVector Commands; + ImVector History; + int HistoryPos; // -1: new line, 0..History.Size-1 browsing history. + ImGuiTextFilter Filter; + bool AutoScroll; + bool ScrollToBottom; + + ExampleAppConsole() + { + IMGUI_DEMO_MARKER("Examples/Console"); + ClearLog(); + memset(InputBuf, 0, sizeof(InputBuf)); + HistoryPos = -1; + + // "CLASSIFY" is here to provide the test case where "C"+[tab] completes to "CL" and display multiple matches. + Commands.push_back("HELP"); + Commands.push_back("HISTORY"); + Commands.push_back("CLEAR"); + Commands.push_back("CLASSIFY"); + AutoScroll = true; + ScrollToBottom = false; + AddLog("Welcome to Dear ImGui!"); + } + ~ExampleAppConsole() + { + ClearLog(); + for (int i = 0; i < History.Size; i++) + free(History[i]); + } + + // Portable helpers + static int Stricmp(const char* s1, const char* s2) { int d; while ((d = toupper(*s2) - toupper(*s1)) == 0 && *s1) { s1++; s2++; } return d; } + static int Strnicmp(const char* s1, const char* s2, int n) { int d = 0; while (n > 0 && (d = toupper(*s2) - toupper(*s1)) == 0 && *s1) { s1++; s2++; n--; } return d; } + static char* Strdup(const char* s) { IM_ASSERT(s); size_t len = strlen(s) + 1; void* buf = malloc(len); IM_ASSERT(buf); return (char*)memcpy(buf, (const void*)s, len); } + static void Strtrim(char* s) { char* str_end = s + strlen(s); while (str_end > s && str_end[-1] == ' ') str_end--; *str_end = 0; } + + void ClearLog() + { + for (int i = 0; i < Items.Size; i++) + free(Items[i]); + Items.clear(); + } + + void AddLog(const char* fmt, ...) IM_FMTARGS(2) + { + // FIXME-OPT + char buf[1024]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, IM_ARRAYSIZE(buf), fmt, args); + buf[IM_ARRAYSIZE(buf)-1] = 0; + va_end(args); + Items.push_back(Strdup(buf)); + } + + void Draw(const char* title, bool* p_open) + { + ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); + if (!ImGui::Begin(title, p_open)) + { + ImGui::End(); + return; + } + + // As a specific feature guaranteed by the library, after calling Begin() the last Item represent the title bar. + // So e.g. IsItemHovered() will return true when hovering the title bar. + // Here we create a context menu only available from the title bar. + if (ImGui::BeginPopupContextItem()) + { + if (ImGui::MenuItem("Close Console")) + *p_open = false; + ImGui::EndPopup(); + } + + ImGui::TextWrapped( + "This example implements a console with basic coloring, completion (TAB key) and history (Up/Down keys). A more elaborate " + "implementation may want to store entries along with extra data such as timestamp, emitter, etc."); + ImGui::TextWrapped("Enter 'HELP' for help."); + + // TODO: display items starting from the bottom + + if (ImGui::SmallButton("Add Debug Text")) { AddLog("%d some text", Items.Size); AddLog("some more text"); AddLog("display very important message here!"); } + ImGui::SameLine(); + if (ImGui::SmallButton("Add Debug Error")) { AddLog("[error] something went wrong"); } + ImGui::SameLine(); + if (ImGui::SmallButton("Clear")) { ClearLog(); } + ImGui::SameLine(); + bool copy_to_clipboard = ImGui::SmallButton("Copy"); + //static float t = 0.0f; if (ImGui::GetTime() - t > 0.02f) { t = ImGui::GetTime(); AddLog("Spam %f", t); } + + ImGui::Separator(); + + // Options menu + if (ImGui::BeginPopup("Options")) + { + ImGui::Checkbox("Auto-scroll", &AutoScroll); + ImGui::EndPopup(); + } + + // Options, Filter + if (ImGui::Button("Options")) + ImGui::OpenPopup("Options"); + ImGui::SameLine(); + Filter.Draw("Filter (\"incl,-excl\") (\"error\")", 180); + ImGui::Separator(); + + // Reserve enough left-over height for 1 separator + 1 input text + const float footer_height_to_reserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); + ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footer_height_to_reserve), false, ImGuiWindowFlags_HorizontalScrollbar); + if (ImGui::BeginPopupContextWindow()) + { + if (ImGui::Selectable("Clear")) ClearLog(); + ImGui::EndPopup(); + } + + // Display every line as a separate entry so we can change their color or add custom widgets. + // If you only want raw text you can use ImGui::TextUnformatted(log.begin(), log.end()); + // NB- if you have thousands of entries this approach may be too inefficient and may require user-side clipping + // to only process visible items. The clipper will automatically measure the height of your first item and then + // "seek" to display only items in the visible area. + // To use the clipper we can replace your standard loop: + // for (int i = 0; i < Items.Size; i++) + // With: + // ImGuiListClipper clipper; + // clipper.Begin(Items.Size); + // while (clipper.Step()) + // for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + // - That your items are evenly spaced (same height) + // - That you have cheap random access to your elements (you can access them given their index, + // without processing all the ones before) + // You cannot this code as-is if a filter is active because it breaks the 'cheap random-access' property. + // We would need random-access on the post-filtered list. + // A typical application wanting coarse clipping and filtering may want to pre-compute an array of indices + // or offsets of items that passed the filtering test, recomputing this array when user changes the filter, + // and appending newly elements as they are inserted. This is left as a task to the user until we can manage + // to improve this example code! + // If your items are of variable height: + // - Split them into same height items would be simpler and facilitate random-seeking into your list. + // - Consider using manual call to IsRectVisible() and skipping extraneous decoration from your items. + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1)); // Tighten spacing + if (copy_to_clipboard) + ImGui::LogToClipboard(); + for (int i = 0; i < Items.Size; i++) + { + const char* item = Items[i]; + if (!Filter.PassFilter(item)) + continue; + + // Normally you would store more information in your item than just a string. + // (e.g. make Items[] an array of structure, store color/type etc.) + ImVec4 color; + bool has_color = false; + if (strstr(item, "[error]")) { color = ImVec4(1.0f, 0.4f, 0.4f, 1.0f); has_color = true; } + else if (strncmp(item, "# ", 2) == 0) { color = ImVec4(1.0f, 0.8f, 0.6f, 1.0f); has_color = true; } + if (has_color) + ImGui::PushStyleColor(ImGuiCol_Text, color); + ImGui::TextUnformatted(item); + if (has_color) + ImGui::PopStyleColor(); + } + if (copy_to_clipboard) + ImGui::LogFinish(); + + if (ScrollToBottom || (AutoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) + ImGui::SetScrollHereY(1.0f); + ScrollToBottom = false; + + ImGui::PopStyleVar(); + ImGui::EndChild(); + ImGui::Separator(); + + // Command-line + bool reclaim_focus = false; + ImGuiInputTextFlags input_text_flags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory; + if (ImGui::InputText("Input", InputBuf, IM_ARRAYSIZE(InputBuf), input_text_flags, &TextEditCallbackStub, (void*)this)) + { + char* s = InputBuf; + Strtrim(s); + if (s[0]) + ExecCommand(s); + strcpy(s, ""); + reclaim_focus = true; + } + + // Auto-focus on window apparition + ImGui::SetItemDefaultFocus(); + if (reclaim_focus) + ImGui::SetKeyboardFocusHere(-1); // Auto focus previous widget + + ImGui::End(); + } + + void ExecCommand(const char* command_line) + { + AddLog("# %s\n", command_line); + + // Insert into history. First find match and delete it so it can be pushed to the back. + // This isn't trying to be smart or optimal. + HistoryPos = -1; + for (int i = History.Size - 1; i >= 0; i--) + if (Stricmp(History[i], command_line) == 0) + { + free(History[i]); + History.erase(History.begin() + i); + break; + } + History.push_back(Strdup(command_line)); + + // Process command + if (Stricmp(command_line, "CLEAR") == 0) + { + ClearLog(); + } + else if (Stricmp(command_line, "HELP") == 0) + { + AddLog("Commands:"); + for (int i = 0; i < Commands.Size; i++) + AddLog("- %s", Commands[i]); + } + else if (Stricmp(command_line, "HISTORY") == 0) + { + int first = History.Size - 10; + for (int i = first > 0 ? first : 0; i < History.Size; i++) + AddLog("%3d: %s\n", i, History[i]); + } + else + { + AddLog("Unknown command: '%s'\n", command_line); + } + + // On command input, we scroll to bottom even if AutoScroll==false + ScrollToBottom = true; + } + + // In C++11 you'd be better off using lambdas for this sort of forwarding callbacks + static int TextEditCallbackStub(ImGuiInputTextCallbackData* data) + { + ExampleAppConsole* console = (ExampleAppConsole*)data->UserData; + return console->TextEditCallback(data); + } + + int TextEditCallback(ImGuiInputTextCallbackData* data) + { + //AddLog("cursor: %d, selection: %d-%d", data->CursorPos, data->SelectionStart, data->SelectionEnd); + switch (data->EventFlag) + { + case ImGuiInputTextFlags_CallbackCompletion: + { + // Example of TEXT COMPLETION + + // Locate beginning of current word + const char* word_end = data->Buf + data->CursorPos; + const char* word_start = word_end; + while (word_start > data->Buf) + { + const char c = word_start[-1]; + if (c == ' ' || c == '\t' || c == ',' || c == ';') + break; + word_start--; + } + + // Build a list of candidates + ImVector candidates; + for (int i = 0; i < Commands.Size; i++) + if (Strnicmp(Commands[i], word_start, (int)(word_end - word_start)) == 0) + candidates.push_back(Commands[i]); + + if (candidates.Size == 0) + { + // No match + AddLog("No match for \"%.*s\"!\n", (int)(word_end - word_start), word_start); + } + else if (candidates.Size == 1) + { + // Single match. Delete the beginning of the word and replace it entirely so we've got nice casing. + data->DeleteChars((int)(word_start - data->Buf), (int)(word_end - word_start)); + data->InsertChars(data->CursorPos, candidates[0]); + data->InsertChars(data->CursorPos, " "); + } + else + { + // Multiple matches. Complete as much as we can.. + // So inputing "C"+Tab will complete to "CL" then display "CLEAR" and "CLASSIFY" as matches. + int match_len = (int)(word_end - word_start); + for (;;) + { + int c = 0; + bool all_candidates_matches = true; + for (int i = 0; i < candidates.Size && all_candidates_matches; i++) + if (i == 0) + c = toupper(candidates[i][match_len]); + else if (c == 0 || c != toupper(candidates[i][match_len])) + all_candidates_matches = false; + if (!all_candidates_matches) + break; + match_len++; + } + + if (match_len > 0) + { + data->DeleteChars((int)(word_start - data->Buf), (int)(word_end - word_start)); + data->InsertChars(data->CursorPos, candidates[0], candidates[0] + match_len); + } + + // List matches + AddLog("Possible matches:\n"); + for (int i = 0; i < candidates.Size; i++) + AddLog("- %s\n", candidates[i]); + } + + break; + } + case ImGuiInputTextFlags_CallbackHistory: + { + // Example of HISTORY + const int prev_history_pos = HistoryPos; + if (data->EventKey == ImGuiKey_UpArrow) + { + if (HistoryPos == -1) + HistoryPos = History.Size - 1; + else if (HistoryPos > 0) + HistoryPos--; + } + else if (data->EventKey == ImGuiKey_DownArrow) + { + if (HistoryPos != -1) + if (++HistoryPos >= History.Size) + HistoryPos = -1; + } + + // A better implementation would preserve the data on the current input line along with cursor position. + if (prev_history_pos != HistoryPos) + { + const char* history_str = (HistoryPos >= 0) ? History[HistoryPos] : ""; + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, history_str); + } + } + } + return 0; + } +}; + +static void ShowExampleAppConsole(bool* p_open) +{ + static ExampleAppConsole console; + console.Draw("Example: Console", p_open); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Debug Log / ShowExampleAppLog() +//----------------------------------------------------------------------------- + +// Usage: +// static ExampleAppLog my_log; +// my_log.AddLog("Hello %d world\n", 123); +// my_log.Draw("title"); +struct ExampleAppLog +{ + ImGuiTextBuffer Buf; + ImGuiTextFilter Filter; + ImVector LineOffsets; // Index to lines offset. We maintain this with AddLog() calls. + bool AutoScroll; // Keep scrolling if already at the bottom. + + ExampleAppLog() + { + AutoScroll = true; + Clear(); + } + + void Clear() + { + Buf.clear(); + LineOffsets.clear(); + LineOffsets.push_back(0); + } + + void AddLog(const char* fmt, ...) IM_FMTARGS(2) + { + int old_size = Buf.size(); + va_list args; + va_start(args, fmt); + Buf.appendfv(fmt, args); + va_end(args); + for (int new_size = Buf.size(); old_size < new_size; old_size++) + if (Buf[old_size] == '\n') + LineOffsets.push_back(old_size + 1); + } + + void Draw(const char* title, bool* p_open = NULL) + { + if (!ImGui::Begin(title, p_open)) + { + ImGui::End(); + return; + } + + // Options menu + if (ImGui::BeginPopup("Options")) + { + ImGui::Checkbox("Auto-scroll", &AutoScroll); + ImGui::EndPopup(); + } + + // Main window + if (ImGui::Button("Options")) + ImGui::OpenPopup("Options"); + ImGui::SameLine(); + bool clear = ImGui::Button("Clear"); + ImGui::SameLine(); + bool copy = ImGui::Button("Copy"); + ImGui::SameLine(); + Filter.Draw("Filter", -100.0f); + + ImGui::Separator(); + ImGui::BeginChild("scrolling", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar); + + if (clear) + Clear(); + if (copy) + ImGui::LogToClipboard(); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + const char* buf = Buf.begin(); + const char* buf_end = Buf.end(); + if (Filter.IsActive()) + { + // In this example we don't use the clipper when Filter is enabled. + // This is because we don't have a random access on the result on our filter. + // A real application processing logs with ten of thousands of entries may want to store the result of + // search/filter.. especially if the filtering function is not trivial (e.g. reg-exp). + for (int line_no = 0; line_no < LineOffsets.Size; line_no++) + { + const char* line_start = buf + LineOffsets[line_no]; + const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end; + if (Filter.PassFilter(line_start, line_end)) + ImGui::TextUnformatted(line_start, line_end); + } + } + else + { + // The simplest and easy way to display the entire buffer: + // ImGui::TextUnformatted(buf_begin, buf_end); + // And it'll just work. TextUnformatted() has specialization for large blob of text and will fast-forward + // to skip non-visible lines. Here we instead demonstrate using the clipper to only process lines that are + // within the visible area. + // If you have tens of thousands of items and their processing cost is non-negligible, coarse clipping them + // on your side is recommended. Using ImGuiListClipper requires + // - A) random access into your data + // - B) items all being the same height, + // both of which we can handle since we an array pointing to the beginning of each line of text. + // When using the filter (in the block of code above) we don't have random access into the data to display + // anymore, which is why we don't use the clipper. Storing or skimming through the search result would make + // it possible (and would be recommended if you want to search through tens of thousands of entries). + ImGuiListClipper clipper; + clipper.Begin(LineOffsets.Size); + while (clipper.Step()) + { + for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++) + { + const char* line_start = buf + LineOffsets[line_no]; + const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end; + ImGui::TextUnformatted(line_start, line_end); + } + } + clipper.End(); + } + ImGui::PopStyleVar(); + + if (AutoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) + ImGui::SetScrollHereY(1.0f); + + ImGui::EndChild(); + ImGui::End(); + } +}; + +// Demonstrate creating a simple log window with basic filtering. +static void ShowExampleAppLog(bool* p_open) +{ + static ExampleAppLog log; + + // For the demo: add a debug button _BEFORE_ the normal log window contents + // We take advantage of a rarely used feature: multiple calls to Begin()/End() are appending to the _same_ window. + // Most of the contents of the window will be added by the log.Draw() call. + ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver); + ImGui::Begin("Example: Log", p_open); + IMGUI_DEMO_MARKER("Examples/Log"); + if (ImGui::SmallButton("[Debug] Add 5 entries")) + { + static int counter = 0; + const char* categories[3] = { "info", "warn", "error" }; + const char* words[] = { "Bumfuzzled", "Cattywampus", "Snickersnee", "Abibliophobia", "Absquatulate", "Nincompoop", "Pauciloquent" }; + for (int n = 0; n < 5; n++) + { + const char* category = categories[counter % IM_ARRAYSIZE(categories)]; + const char* word = words[counter % IM_ARRAYSIZE(words)]; + log.AddLog("[%05d] [%s] Hello, current time is %.1f, here's a word: '%s'\n", + ImGui::GetFrameCount(), category, ImGui::GetTime(), word); + counter++; + } + } + ImGui::End(); + + // Actually call in the regular Log helper (which will Begin() into the same window as we just did) + log.Draw("Example: Log", p_open); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Simple Layout / ShowExampleAppLayout() +//----------------------------------------------------------------------------- + +// Demonstrate create a window with multiple child windows. +static void ShowExampleAppLayout(bool* p_open) +{ + ImGui::SetNextWindowSize(ImVec2(500, 440), ImGuiCond_FirstUseEver); + if (ImGui::Begin("Example: Simple layout", p_open, ImGuiWindowFlags_MenuBar)) + { + IMGUI_DEMO_MARKER("Examples/Simple layout"); + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("File")) + { + if (ImGui::MenuItem("Close")) *p_open = false; + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + // Left + static int selected = 0; + { + ImGui::BeginChild("left pane", ImVec2(150, 0), true); + for (int i = 0; i < 100; i++) + { + // FIXME: Good candidate to use ImGuiSelectableFlags_SelectOnNav + char label[128]; + sprintf(label, "MyObject %d", i); + if (ImGui::Selectable(label, selected == i)) + selected = i; + } + ImGui::EndChild(); + } + ImGui::SameLine(); + + // Right + { + ImGui::BeginGroup(); + ImGui::BeginChild("item view", ImVec2(0, -ImGui::GetFrameHeightWithSpacing())); // Leave room for 1 line below us + ImGui::Text("MyObject: %d", selected); + ImGui::Separator(); + if (ImGui::BeginTabBar("##Tabs", ImGuiTabBarFlags_None)) + { + if (ImGui::BeginTabItem("Description")) + { + ImGui::TextWrapped("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Details")) + { + ImGui::Text("ID: 0123456789"); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + ImGui::EndChild(); + if (ImGui::Button("Revert")) {} + ImGui::SameLine(); + if (ImGui::Button("Save")) {} + ImGui::EndGroup(); + } + } + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Property Editor / ShowExampleAppPropertyEditor() +//----------------------------------------------------------------------------- + +static void ShowPlaceholderObject(const char* prefix, int uid) +{ + // Use object uid as identifier. Most commonly you could also use the object pointer as a base ID. + ImGui::PushID(uid); + + // Text and Tree nodes are less high than framed widgets, using AlignTextToFramePadding() we add vertical spacing to make the tree lines equal high. + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + bool node_open = ImGui::TreeNode("Object", "%s_%u", prefix, uid); + ImGui::TableSetColumnIndex(1); + ImGui::Text("my sailor is rich"); + + if (node_open) + { + static float placeholder_members[8] = { 0.0f, 0.0f, 1.0f, 3.1416f, 100.0f, 999.0f }; + for (int i = 0; i < 8; i++) + { + ImGui::PushID(i); // Use field index as identifier. + if (i < 2) + { + ShowPlaceholderObject("Child", 424242); + } + else + { + // Here we use a TreeNode to highlight on hover (we could use e.g. Selectable as well) + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_Bullet; + ImGui::TreeNodeEx("Field", flags, "Field_%d", i); + + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-FLT_MIN); + if (i >= 5) + ImGui::InputFloat("##value", &placeholder_members[i], 1.0f); + else + ImGui::DragFloat("##value", &placeholder_members[i], 0.01f); + ImGui::NextColumn(); + } + ImGui::PopID(); + } + ImGui::TreePop(); + } + ImGui::PopID(); +} + +// Demonstrate create a simple property editor. +static void ShowExampleAppPropertyEditor(bool* p_open) +{ + ImGui::SetNextWindowSize(ImVec2(430, 450), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Example: Property editor", p_open)) + { + ImGui::End(); + return; + } + IMGUI_DEMO_MARKER("Examples/Property Editor"); + + HelpMarker( + "This example shows how you may implement a property editor using two columns.\n" + "All objects/fields data are dummies here.\n" + "Remember that in many simple cases, you can use ImGui::SameLine(xxx) to position\n" + "your cursor horizontally instead of using the Columns() API."); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); + if (ImGui::BeginTable("split", 2, ImGuiTableFlags_BordersOuter | ImGuiTableFlags_Resizable)) + { + // Iterate placeholder objects (all the same data) + for (int obj_i = 0; obj_i < 4; obj_i++) + { + ShowPlaceholderObject("Object", obj_i); + //ImGui::Separator(); + } + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Long Text / ShowExampleAppLongText() +//----------------------------------------------------------------------------- + +// Demonstrate/test rendering huge amount of text, and the incidence of clipping. +static void ShowExampleAppLongText(bool* p_open) +{ + ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Example: Long text display", p_open)) + { + ImGui::End(); + return; + } + IMGUI_DEMO_MARKER("Examples/Long text display"); + + static int test_type = 0; + static ImGuiTextBuffer log; + static int lines = 0; + ImGui::Text("Printing unusually long amount of text."); + ImGui::Combo("Test type", &test_type, + "Single call to TextUnformatted()\0" + "Multiple calls to Text(), clipped\0" + "Multiple calls to Text(), not clipped (slow)\0"); + ImGui::Text("Buffer contents: %d lines, %d bytes", lines, log.size()); + if (ImGui::Button("Clear")) { log.clear(); lines = 0; } + ImGui::SameLine(); + if (ImGui::Button("Add 1000 lines")) + { + for (int i = 0; i < 1000; i++) + log.appendf("%i The quick brown fox jumps over the lazy dog\n", lines + i); + lines += 1000; + } + ImGui::BeginChild("Log"); + switch (test_type) + { + case 0: + // Single call to TextUnformatted() with a big buffer + ImGui::TextUnformatted(log.begin(), log.end()); + break; + case 1: + { + // Multiple calls to Text(), manually coarsely clipped - demonstrate how to use the ImGuiListClipper helper. + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + ImGuiListClipper clipper; + clipper.Begin(lines); + while (clipper.Step()) + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + ImGui::Text("%i The quick brown fox jumps over the lazy dog", i); + ImGui::PopStyleVar(); + break; + } + case 2: + // Multiple calls to Text(), not clipped (slow) + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + for (int i = 0; i < lines; i++) + ImGui::Text("%i The quick brown fox jumps over the lazy dog", i); + ImGui::PopStyleVar(); + break; + } + ImGui::EndChild(); + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Auto Resize / ShowExampleAppAutoResize() +//----------------------------------------------------------------------------- + +// Demonstrate creating a window which gets auto-resized according to its content. +static void ShowExampleAppAutoResize(bool* p_open) +{ + if (!ImGui::Begin("Example: Auto-resizing window", p_open, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::End(); + return; + } + IMGUI_DEMO_MARKER("Examples/Auto-resizing window"); + + static int lines = 10; + ImGui::TextUnformatted( + "Window will resize every-frame to the size of its content.\n" + "Note that you probably don't want to query the window size to\n" + "output your content because that would create a feedback loop."); + ImGui::SliderInt("Number of lines", &lines, 1, 20); + for (int i = 0; i < lines; i++) + ImGui::Text("%*sThis is line %d", i * 4, "", i); // Pad with space to extend size horizontally + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Constrained Resize / ShowExampleAppConstrainedResize() +//----------------------------------------------------------------------------- + +// Demonstrate creating a window with custom resize constraints. +static void ShowExampleAppConstrainedResize(bool* p_open) +{ + struct CustomConstraints + { + // Helper functions to demonstrate programmatic constraints + static void Square(ImGuiSizeCallbackData* data) { data->DesiredSize.x = data->DesiredSize.y = IM_MAX(data->DesiredSize.x, data->DesiredSize.y); } + static void Step(ImGuiSizeCallbackData* data) { float step = (float)(int)(intptr_t)data->UserData; data->DesiredSize = ImVec2((int)(data->DesiredSize.x / step + 0.5f) * step, (int)(data->DesiredSize.y / step + 0.5f) * step); } + }; + + const char* test_desc[] = + { + "Resize vertical only", + "Resize horizontal only", + "Width > 100, Height > 100", + "Width 400-500", + "Height 400-500", + "Custom: Always Square", + "Custom: Fixed Steps (100)", + }; + + static bool auto_resize = false; + static int type = 0; + static int display_lines = 10; + if (type == 0) ImGui::SetNextWindowSizeConstraints(ImVec2(-1, 0), ImVec2(-1, FLT_MAX)); // Vertical only + if (type == 1) ImGui::SetNextWindowSizeConstraints(ImVec2(0, -1), ImVec2(FLT_MAX, -1)); // Horizontal only + if (type == 2) ImGui::SetNextWindowSizeConstraints(ImVec2(100, 100), ImVec2(FLT_MAX, FLT_MAX)); // Width > 100, Height > 100 + if (type == 3) ImGui::SetNextWindowSizeConstraints(ImVec2(400, -1), ImVec2(500, -1)); // Width 400-500 + if (type == 4) ImGui::SetNextWindowSizeConstraints(ImVec2(-1, 400), ImVec2(-1, 500)); // Height 400-500 + if (type == 5) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::Square); // Always Square + if (type == 6) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::Step, (void*)(intptr_t)100); // Fixed Step + + ImGuiWindowFlags flags = auto_resize ? ImGuiWindowFlags_AlwaysAutoResize : 0; + if (ImGui::Begin("Example: Constrained Resize", p_open, flags)) + { + IMGUI_DEMO_MARKER("Examples/Constrained Resizing window"); + if (ImGui::Button("200x200")) { ImGui::SetWindowSize(ImVec2(200, 200)); } ImGui::SameLine(); + if (ImGui::Button("500x500")) { ImGui::SetWindowSize(ImVec2(500, 500)); } ImGui::SameLine(); + if (ImGui::Button("800x200")) { ImGui::SetWindowSize(ImVec2(800, 200)); } + ImGui::SetNextItemWidth(200); + ImGui::Combo("Constraint", &type, test_desc, IM_ARRAYSIZE(test_desc)); + ImGui::SetNextItemWidth(200); + ImGui::DragInt("Lines", &display_lines, 0.2f, 1, 100); + ImGui::Checkbox("Auto-resize", &auto_resize); + for (int i = 0; i < display_lines; i++) + ImGui::Text("%*sHello, sailor! Making this line long enough for the example.", i * 4, ""); + } + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Simple overlay / ShowExampleAppSimpleOverlay() +//----------------------------------------------------------------------------- + +// Demonstrate creating a simple static window with no decoration +// + a context-menu to choose which corner of the screen to use. +static void ShowExampleAppSimpleOverlay(bool* p_open) +{ + static int corner = 0; + ImGuiIO& io = ImGui::GetIO(); + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; + if (corner != -1) + { + const float PAD = 10.0f; + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImVec2 work_pos = viewport->WorkPos; // Use work area to avoid menu-bar/task-bar, if any! + ImVec2 work_size = viewport->WorkSize; + ImVec2 window_pos, window_pos_pivot; + window_pos.x = (corner & 1) ? (work_pos.x + work_size.x - PAD) : (work_pos.x + PAD); + window_pos.y = (corner & 2) ? (work_pos.y + work_size.y - PAD) : (work_pos.y + PAD); + window_pos_pivot.x = (corner & 1) ? 1.0f : 0.0f; + window_pos_pivot.y = (corner & 2) ? 1.0f : 0.0f; + ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, window_pos_pivot); + window_flags |= ImGuiWindowFlags_NoMove; + } + ImGui::SetNextWindowBgAlpha(0.35f); // Transparent background + if (ImGui::Begin("Example: Simple overlay", p_open, window_flags)) + { + IMGUI_DEMO_MARKER("Examples/Simple Overlay"); + ImGui::Text("Simple overlay\n" "in the corner of the screen.\n" "(right-click to change position)"); + ImGui::Separator(); + if (ImGui::IsMousePosValid()) + ImGui::Text("Mouse Position: (%.1f,%.1f)", io.MousePos.x, io.MousePos.y); + else + ImGui::Text("Mouse Position: "); + if (ImGui::BeginPopupContextWindow()) + { + if (ImGui::MenuItem("Custom", NULL, corner == -1)) corner = -1; + if (ImGui::MenuItem("Top-left", NULL, corner == 0)) corner = 0; + if (ImGui::MenuItem("Top-right", NULL, corner == 1)) corner = 1; + if (ImGui::MenuItem("Bottom-left", NULL, corner == 2)) corner = 2; + if (ImGui::MenuItem("Bottom-right", NULL, corner == 3)) corner = 3; + if (p_open && ImGui::MenuItem("Close")) *p_open = false; + ImGui::EndPopup(); + } + } + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Fullscreen window / ShowExampleAppFullscreen() +//----------------------------------------------------------------------------- + +// Demonstrate creating a window covering the entire screen/viewport +static void ShowExampleAppFullscreen(bool* p_open) +{ + static bool use_work_area = true; + static ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings; + + // We demonstrate using the full viewport area or the work area (without menu-bars, task-bars etc.) + // Based on your use case you may want one of the other. + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(use_work_area ? viewport->WorkPos : viewport->Pos); + ImGui::SetNextWindowSize(use_work_area ? viewport->WorkSize : viewport->Size); + + if (ImGui::Begin("Example: Fullscreen window", p_open, flags)) + { + ImGui::Checkbox("Use work area instead of main area", &use_work_area); + ImGui::SameLine(); + HelpMarker("Main Area = entire viewport,\nWork Area = entire viewport minus sections used by the main menu bars, task bars etc.\n\nEnable the main-menu bar in Examples menu to see the difference."); + + ImGui::CheckboxFlags("ImGuiWindowFlags_NoBackground", &flags, ImGuiWindowFlags_NoBackground); + ImGui::CheckboxFlags("ImGuiWindowFlags_NoDecoration", &flags, ImGuiWindowFlags_NoDecoration); + ImGui::Indent(); + ImGui::CheckboxFlags("ImGuiWindowFlags_NoTitleBar", &flags, ImGuiWindowFlags_NoTitleBar); + ImGui::CheckboxFlags("ImGuiWindowFlags_NoCollapse", &flags, ImGuiWindowFlags_NoCollapse); + ImGui::CheckboxFlags("ImGuiWindowFlags_NoScrollbar", &flags, ImGuiWindowFlags_NoScrollbar); + ImGui::Unindent(); + + if (p_open && ImGui::Button("Close this window")) + *p_open = false; + } + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Manipulating Window Titles / ShowExampleAppWindowTitles() +//----------------------------------------------------------------------------- + +// Demonstrate using "##" and "###" in identifiers to manipulate ID generation. +// This apply to all regular items as well. +// Read FAQ section "How can I have multiple widgets with the same label?" for details. +static void ShowExampleAppWindowTitles(bool*) +{ + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + const ImVec2 base_pos = viewport->Pos; + + // By default, Windows are uniquely identified by their title. + // You can use the "##" and "###" markers to manipulate the display/ID. + + // Using "##" to display same title but have unique identifier. + ImGui::SetNextWindowPos(ImVec2(base_pos.x + 100, base_pos.y + 100), ImGuiCond_FirstUseEver); + ImGui::Begin("Same title as another window##1"); + IMGUI_DEMO_MARKER("Examples/Manipulating window titles"); + ImGui::Text("This is window 1.\nMy title is the same as window 2, but my identifier is unique."); + ImGui::End(); + + ImGui::SetNextWindowPos(ImVec2(base_pos.x + 100, base_pos.y + 200), ImGuiCond_FirstUseEver); + ImGui::Begin("Same title as another window##2"); + ImGui::Text("This is window 2.\nMy title is the same as window 1, but my identifier is unique."); + ImGui::End(); + + // Using "###" to display a changing title but keep a static identifier "AnimatedTitle" + char buf[128]; + sprintf(buf, "Animated title %c %d###AnimatedTitle", "|/-\\"[(int)(ImGui::GetTime() / 0.25f) & 3], ImGui::GetFrameCount()); + ImGui::SetNextWindowPos(ImVec2(base_pos.x + 100, base_pos.y + 300), ImGuiCond_FirstUseEver); + ImGui::Begin(buf); + ImGui::Text("This window has a changing title."); + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Custom Rendering using ImDrawList API / ShowExampleAppCustomRendering() +//----------------------------------------------------------------------------- + +// Demonstrate using the low-level ImDrawList to draw custom shapes. +static void ShowExampleAppCustomRendering(bool* p_open) +{ + if (!ImGui::Begin("Example: Custom rendering", p_open)) + { + ImGui::End(); + return; + } + IMGUI_DEMO_MARKER("Examples/Custom Rendering"); + + // Tip: If you do a lot of custom rendering, you probably want to use your own geometrical types and benefit of + // overloaded operators, etc. Define IM_VEC2_CLASS_EXTRA in imconfig.h to create implicit conversions between your + // types and ImVec2/ImVec4. Dear ImGui defines overloaded operators but they are internal to imgui.cpp and not + // exposed outside (to avoid messing with your types) In this example we are not using the maths operators! + + if (ImGui::BeginTabBar("##TabBar")) + { + if (ImGui::BeginTabItem("Primitives")) + { + ImGui::PushItemWidth(-ImGui::GetFontSize() * 15); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + // Draw gradients + // (note that those are currently exacerbating our sRGB/Linear issues) + // Calling ImGui::GetColorU32() multiplies the given colors by the current Style Alpha, but you may pass the IM_COL32() directly as well.. + ImGui::Text("Gradients"); + ImVec2 gradient_size = ImVec2(ImGui::CalcItemWidth(), ImGui::GetFrameHeight()); + { + ImVec2 p0 = ImGui::GetCursorScreenPos(); + ImVec2 p1 = ImVec2(p0.x + gradient_size.x, p0.y + gradient_size.y); + ImU32 col_a = ImGui::GetColorU32(IM_COL32(0, 0, 0, 255)); + ImU32 col_b = ImGui::GetColorU32(IM_COL32(255, 255, 255, 255)); + draw_list->AddRectFilledMultiColor(p0, p1, col_a, col_b, col_b, col_a); + ImGui::InvisibleButton("##gradient1", gradient_size); + } + { + ImVec2 p0 = ImGui::GetCursorScreenPos(); + ImVec2 p1 = ImVec2(p0.x + gradient_size.x, p0.y + gradient_size.y); + ImU32 col_a = ImGui::GetColorU32(IM_COL32(0, 255, 0, 255)); + ImU32 col_b = ImGui::GetColorU32(IM_COL32(255, 0, 0, 255)); + draw_list->AddRectFilledMultiColor(p0, p1, col_a, col_b, col_b, col_a); + ImGui::InvisibleButton("##gradient2", gradient_size); + } + + // Draw a bunch of primitives + ImGui::Text("All primitives"); + static float sz = 36.0f; + static float thickness = 3.0f; + static int ngon_sides = 6; + static bool circle_segments_override = false; + static int circle_segments_override_v = 12; + static bool curve_segments_override = false; + static int curve_segments_override_v = 8; + static ImVec4 colf = ImVec4(1.0f, 1.0f, 0.4f, 1.0f); + ImGui::DragFloat("Size", &sz, 0.2f, 2.0f, 100.0f, "%.0f"); + ImGui::DragFloat("Thickness", &thickness, 0.05f, 1.0f, 8.0f, "%.02f"); + ImGui::SliderInt("N-gon sides", &ngon_sides, 3, 12); + ImGui::Checkbox("##circlesegmentoverride", &circle_segments_override); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + circle_segments_override |= ImGui::SliderInt("Circle segments override", &circle_segments_override_v, 3, 40); + ImGui::Checkbox("##curvessegmentoverride", &curve_segments_override); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + curve_segments_override |= ImGui::SliderInt("Curves segments override", &curve_segments_override_v, 3, 40); + ImGui::ColorEdit4("Color", &colf.x); + + const ImVec2 p = ImGui::GetCursorScreenPos(); + const ImU32 col = ImColor(colf); + const float spacing = 10.0f; + const ImDrawFlags corners_tl_br = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersBottomRight; + const float rounding = sz / 5.0f; + const int circle_segments = circle_segments_override ? circle_segments_override_v : 0; + const int curve_segments = curve_segments_override ? curve_segments_override_v : 0; + float x = p.x + 4.0f; + float y = p.y + 4.0f; + for (int n = 0; n < 2; n++) + { + // First line uses a thickness of 1.0f, second line uses the configurable thickness + float th = (n == 0) ? 1.0f : thickness; + draw_list->AddNgon(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, ngon_sides, th); x += sz + spacing; // N-gon + draw_list->AddCircle(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, circle_segments, th); x += sz + spacing; // Circle + draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 0.0f, ImDrawFlags_None, th); x += sz + spacing; // Square + draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, rounding, ImDrawFlags_None, th); x += sz + spacing; // Square with all rounded corners + draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, rounding, corners_tl_br, th); x += sz + spacing; // Square with two rounded corners + draw_list->AddTriangle(ImVec2(x+sz*0.5f,y), ImVec2(x+sz, y+sz-0.5f), ImVec2(x, y+sz-0.5f), col, th);x += sz + spacing; // Triangle + //draw_list->AddTriangle(ImVec2(x+sz*0.2f,y), ImVec2(x, y+sz-0.5f), ImVec2(x+sz*0.4f, y+sz-0.5f), col, th);x+= sz*0.4f + spacing; // Thin triangle + draw_list->AddLine(ImVec2(x, y), ImVec2(x + sz, y), col, th); x += sz + spacing; // Horizontal line (note: drawing a filled rectangle will be faster!) + draw_list->AddLine(ImVec2(x, y), ImVec2(x, y + sz), col, th); x += spacing; // Vertical line (note: drawing a filled rectangle will be faster!) + draw_list->AddLine(ImVec2(x, y), ImVec2(x + sz, y + sz), col, th); x += sz + spacing; // Diagonal line + + // Quadratic Bezier Curve (3 control points) + ImVec2 cp3[3] = { ImVec2(x, y + sz * 0.6f), ImVec2(x + sz * 0.5f, y - sz * 0.4f), ImVec2(x + sz, y + sz) }; + draw_list->AddBezierQuadratic(cp3[0], cp3[1], cp3[2], col, th, curve_segments); x += sz + spacing; + + // Cubic Bezier Curve (4 control points) + ImVec2 cp4[4] = { ImVec2(x, y), ImVec2(x + sz * 1.3f, y + sz * 0.3f), ImVec2(x + sz - sz * 1.3f, y + sz - sz * 0.3f), ImVec2(x + sz, y + sz) }; + draw_list->AddBezierCubic(cp4[0], cp4[1], cp4[2], cp4[3], col, th, curve_segments); + + x = p.x + 4; + y += sz + spacing; + } + draw_list->AddNgonFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz*0.5f, col, ngon_sides); x += sz + spacing; // N-gon + draw_list->AddCircleFilled(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, circle_segments); x += sz + spacing; // Circle + draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col); x += sz + spacing; // Square + draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f); x += sz + spacing; // Square with all rounded corners + draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f, corners_tl_br); x += sz + spacing; // Square with two rounded corners + draw_list->AddTriangleFilled(ImVec2(x+sz*0.5f,y), ImVec2(x+sz, y+sz-0.5f), ImVec2(x, y+sz-0.5f), col); x += sz + spacing; // Triangle + //draw_list->AddTriangleFilled(ImVec2(x+sz*0.2f,y), ImVec2(x, y+sz-0.5f), ImVec2(x+sz*0.4f, y+sz-0.5f), col); x += sz*0.4f + spacing; // Thin triangle + draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + thickness), col); x += sz + spacing; // Horizontal line (faster than AddLine, but only handle integer thickness) + draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + thickness, y + sz), col); x += spacing * 2.0f;// Vertical line (faster than AddLine, but only handle integer thickness) + draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + 1, y + 1), col); x += sz; // Pixel (faster than AddLine) + draw_list->AddRectFilledMultiColor(ImVec2(x, y), ImVec2(x + sz, y + sz), IM_COL32(0, 0, 0, 255), IM_COL32(255, 0, 0, 255), IM_COL32(255, 255, 0, 255), IM_COL32(0, 255, 0, 255)); + + ImGui::Dummy(ImVec2((sz + spacing) * 10.2f, (sz + spacing) * 3.0f)); + ImGui::PopItemWidth(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Canvas")) + { + static ImVector points; + static ImVec2 scrolling(0.0f, 0.0f); + static bool opt_enable_grid = true; + static bool opt_enable_context_menu = true; + static bool adding_line = false; + + ImGui::Checkbox("Enable grid", &opt_enable_grid); + ImGui::Checkbox("Enable context menu", &opt_enable_context_menu); + ImGui::Text("Mouse Left: drag to add lines,\nMouse Right: drag to scroll, click for context menu."); + + // Typically you would use a BeginChild()/EndChild() pair to benefit from a clipping region + own scrolling. + // Here we demonstrate that this can be replaced by simple offsetting + custom drawing + PushClipRect/PopClipRect() calls. + // To use a child window instead we could use, e.g: + // ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Disable padding + // ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(50, 50, 50, 255)); // Set a background color + // ImGui::BeginChild("canvas", ImVec2(0.0f, 0.0f), true, ImGuiWindowFlags_NoMove); + // ImGui::PopStyleColor(); + // ImGui::PopStyleVar(); + // [...] + // ImGui::EndChild(); + + // Using InvisibleButton() as a convenience 1) it will advance the layout cursor and 2) allows us to use IsItemHovered()/IsItemActive() + ImVec2 canvas_p0 = ImGui::GetCursorScreenPos(); // ImDrawList API uses screen coordinates! + ImVec2 canvas_sz = ImGui::GetContentRegionAvail(); // Resize canvas to what's available + if (canvas_sz.x < 50.0f) canvas_sz.x = 50.0f; + if (canvas_sz.y < 50.0f) canvas_sz.y = 50.0f; + ImVec2 canvas_p1 = ImVec2(canvas_p0.x + canvas_sz.x, canvas_p0.y + canvas_sz.y); + + // Draw border and background color + ImGuiIO& io = ImGui::GetIO(); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + draw_list->AddRectFilled(canvas_p0, canvas_p1, IM_COL32(50, 50, 50, 255)); + draw_list->AddRect(canvas_p0, canvas_p1, IM_COL32(255, 255, 255, 255)); + + // This will catch our interactions + ImGui::InvisibleButton("canvas", canvas_sz, ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight); + const bool is_hovered = ImGui::IsItemHovered(); // Hovered + const bool is_active = ImGui::IsItemActive(); // Held + const ImVec2 origin(canvas_p0.x + scrolling.x, canvas_p0.y + scrolling.y); // Lock scrolled origin + const ImVec2 mouse_pos_in_canvas(io.MousePos.x - origin.x, io.MousePos.y - origin.y); + + // Add first and second point + if (is_hovered && !adding_line && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + points.push_back(mouse_pos_in_canvas); + points.push_back(mouse_pos_in_canvas); + adding_line = true; + } + if (adding_line) + { + points.back() = mouse_pos_in_canvas; + if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) + adding_line = false; + } + + // Pan (we use a zero mouse threshold when there's no context menu) + // You may decide to make that threshold dynamic based on whether the mouse is hovering something etc. + const float mouse_threshold_for_pan = opt_enable_context_menu ? -1.0f : 0.0f; + if (is_active && ImGui::IsMouseDragging(ImGuiMouseButton_Right, mouse_threshold_for_pan)) + { + scrolling.x += io.MouseDelta.x; + scrolling.y += io.MouseDelta.y; + } + + // Context menu (under default mouse threshold) + ImVec2 drag_delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Right); + if (opt_enable_context_menu && drag_delta.x == 0.0f && drag_delta.y == 0.0f) + ImGui::OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + if (ImGui::BeginPopup("context")) + { + if (adding_line) + points.resize(points.size() - 2); + adding_line = false; + if (ImGui::MenuItem("Remove one", NULL, false, points.Size > 0)) { points.resize(points.size() - 2); } + if (ImGui::MenuItem("Remove all", NULL, false, points.Size > 0)) { points.clear(); } + ImGui::EndPopup(); + } + + // Draw grid + all lines in the canvas + draw_list->PushClipRect(canvas_p0, canvas_p1, true); + if (opt_enable_grid) + { + const float GRID_STEP = 64.0f; + for (float x = fmodf(scrolling.x, GRID_STEP); x < canvas_sz.x; x += GRID_STEP) + draw_list->AddLine(ImVec2(canvas_p0.x + x, canvas_p0.y), ImVec2(canvas_p0.x + x, canvas_p1.y), IM_COL32(200, 200, 200, 40)); + for (float y = fmodf(scrolling.y, GRID_STEP); y < canvas_sz.y; y += GRID_STEP) + draw_list->AddLine(ImVec2(canvas_p0.x, canvas_p0.y + y), ImVec2(canvas_p1.x, canvas_p0.y + y), IM_COL32(200, 200, 200, 40)); + } + for (int n = 0; n < points.Size; n += 2) + draw_list->AddLine(ImVec2(origin.x + points[n].x, origin.y + points[n].y), ImVec2(origin.x + points[n + 1].x, origin.y + points[n + 1].y), IM_COL32(255, 255, 0, 255), 2.0f); + draw_list->PopClipRect(); + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("BG/FG draw lists")) + { + static bool draw_bg = true; + static bool draw_fg = true; + ImGui::Checkbox("Draw in Background draw list", &draw_bg); + ImGui::SameLine(); HelpMarker("The Background draw list will be rendered below every Dear ImGui windows."); + ImGui::Checkbox("Draw in Foreground draw list", &draw_fg); + ImGui::SameLine(); HelpMarker("The Foreground draw list will be rendered over every Dear ImGui windows."); + ImVec2 window_pos = ImGui::GetWindowPos(); + ImVec2 window_size = ImGui::GetWindowSize(); + ImVec2 window_center = ImVec2(window_pos.x + window_size.x * 0.5f, window_pos.y + window_size.y * 0.5f); + if (draw_bg) + ImGui::GetBackgroundDrawList()->AddCircle(window_center, window_size.x * 0.6f, IM_COL32(255, 0, 0, 200), 0, 10 + 4); + if (draw_fg) + ImGui::GetForegroundDrawList()->AddCircle(window_center, window_size.y * 0.6f, IM_COL32(0, 255, 0, 200), 0, 10); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + ImGui::End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] Example App: Documents Handling / ShowExampleAppDocuments() +//----------------------------------------------------------------------------- + +// Simplified structure to mimic a Document model +struct MyDocument +{ + const char* Name; // Document title + bool Open; // Set when open (we keep an array of all available documents to simplify demo code!) + bool OpenPrev; // Copy of Open from last update. + bool Dirty; // Set when the document has been modified + bool WantClose; // Set when the document + ImVec4 Color; // An arbitrary variable associated to the document + + MyDocument(const char* name, bool open = true, const ImVec4& color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f)) + { + Name = name; + Open = OpenPrev = open; + Dirty = false; + WantClose = false; + Color = color; + } + void DoOpen() { Open = true; } + void DoQueueClose() { WantClose = true; } + void DoForceClose() { Open = false; Dirty = false; } + void DoSave() { Dirty = false; } + + // Display placeholder contents for the Document + static void DisplayContents(MyDocument* doc) + { + ImGui::PushID(doc); + ImGui::Text("Document \"%s\"", doc->Name); + ImGui::PushStyleColor(ImGuiCol_Text, doc->Color); + ImGui::TextWrapped("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."); + ImGui::PopStyleColor(); + if (ImGui::Button("Modify", ImVec2(100, 0))) + doc->Dirty = true; + ImGui::SameLine(); + if (ImGui::Button("Save", ImVec2(100, 0))) + doc->DoSave(); + ImGui::ColorEdit3("color", &doc->Color.x); // Useful to test drag and drop and hold-dragged-to-open-tab behavior. + ImGui::PopID(); + } + + // Display context menu for the Document + static void DisplayContextMenu(MyDocument* doc) + { + if (!ImGui::BeginPopupContextItem()) + return; + + char buf[256]; + sprintf(buf, "Save %s", doc->Name); + if (ImGui::MenuItem(buf, "CTRL+S", false, doc->Open)) + doc->DoSave(); + if (ImGui::MenuItem("Close", "CTRL+W", false, doc->Open)) + doc->DoQueueClose(); + ImGui::EndPopup(); + } +}; + +struct ExampleAppDocuments +{ + ImVector Documents; + + ExampleAppDocuments() + { + Documents.push_back(MyDocument("Lettuce", true, ImVec4(0.4f, 0.8f, 0.4f, 1.0f))); + Documents.push_back(MyDocument("Eggplant", true, ImVec4(0.8f, 0.5f, 1.0f, 1.0f))); + Documents.push_back(MyDocument("Carrot", true, ImVec4(1.0f, 0.8f, 0.5f, 1.0f))); + Documents.push_back(MyDocument("Tomato", false, ImVec4(1.0f, 0.3f, 0.4f, 1.0f))); + Documents.push_back(MyDocument("A Rather Long Title", false)); + Documents.push_back(MyDocument("Some Document", false)); + } +}; + +// [Optional] Notify the system of Tabs/Windows closure that happened outside the regular tab interface. +// If a tab has been closed programmatically (aka closed from another source such as the Checkbox() in the demo, +// as opposed to clicking on the regular tab closing button) and stops being submitted, it will take a frame for +// the tab bar to notice its absence. During this frame there will be a gap in the tab bar, and if the tab that has +// disappeared was the selected one, the tab bar will report no selected tab during the frame. This will effectively +// give the impression of a flicker for one frame. +// We call SetTabItemClosed() to manually notify the Tab Bar or Docking system of removed tabs to avoid this glitch. +// Note that this completely optional, and only affect tab bars with the ImGuiTabBarFlags_Reorderable flag. +static void NotifyOfDocumentsClosedElsewhere(ExampleAppDocuments& app) +{ + for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) + { + MyDocument* doc = &app.Documents[doc_n]; + if (!doc->Open && doc->OpenPrev) + ImGui::SetTabItemClosed(doc->Name); + doc->OpenPrev = doc->Open; + } +} + +void ShowExampleAppDocuments(bool* p_open) +{ + static ExampleAppDocuments app; + + // Options + static bool opt_reorderable = true; + static ImGuiTabBarFlags opt_fitting_flags = ImGuiTabBarFlags_FittingPolicyDefault_; + + bool window_contents_visible = ImGui::Begin("Example: Documents", p_open, ImGuiWindowFlags_MenuBar); + if (!window_contents_visible) + { + ImGui::End(); + return; + } + + // Menu + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("File")) + { + int open_count = 0; + for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) + open_count += app.Documents[doc_n].Open ? 1 : 0; + + if (ImGui::BeginMenu("Open", open_count < app.Documents.Size)) + { + for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) + { + MyDocument* doc = &app.Documents[doc_n]; + if (!doc->Open) + if (ImGui::MenuItem(doc->Name)) + doc->DoOpen(); + } + ImGui::EndMenu(); + } + if (ImGui::MenuItem("Close All Documents", NULL, false, open_count > 0)) + for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) + app.Documents[doc_n].DoQueueClose(); + if (ImGui::MenuItem("Exit", "Alt+F4")) {} + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + // [Debug] List documents with one checkbox for each + for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) + { + MyDocument* doc = &app.Documents[doc_n]; + if (doc_n > 0) + ImGui::SameLine(); + ImGui::PushID(doc); + if (ImGui::Checkbox(doc->Name, &doc->Open)) + if (!doc->Open) + doc->DoForceClose(); + ImGui::PopID(); + } + + ImGui::Separator(); + + // About the ImGuiWindowFlags_UnsavedDocument / ImGuiTabItemFlags_UnsavedDocument flags. + // They have multiple effects: + // - Display a dot next to the title. + // - Tab is selected when clicking the X close button. + // - Closure is not assumed (will wait for user to stop submitting the tab). + // Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. + // We need to assume closure by default otherwise waiting for "lack of submission" on the next frame would leave an empty + // hole for one-frame, both in the tab-bar and in tab-contents when closing a tab/window. + // The rarely used SetTabItemClosed() function is a way to notify of programmatic closure to avoid the one-frame hole. + + // Submit Tab Bar and Tabs + { + ImGuiTabBarFlags tab_bar_flags = (opt_fitting_flags) | (opt_reorderable ? ImGuiTabBarFlags_Reorderable : 0); + if (ImGui::BeginTabBar("##tabs", tab_bar_flags)) + { + if (opt_reorderable) + NotifyOfDocumentsClosedElsewhere(app); + + // [DEBUG] Stress tests + //if ((ImGui::GetFrameCount() % 30) == 0) docs[1].Open ^= 1; // [DEBUG] Automatically show/hide a tab. Test various interactions e.g. dragging with this on. + //if (ImGui::GetIO().KeyCtrl) ImGui::SetTabItemSelected(docs[1].Name); // [DEBUG] Test SetTabItemSelected(), probably not very useful as-is anyway.. + + // Submit Tabs + for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) + { + MyDocument* doc = &app.Documents[doc_n]; + if (!doc->Open) + continue; + + ImGuiTabItemFlags tab_flags = (doc->Dirty ? ImGuiTabItemFlags_UnsavedDocument : 0); + bool visible = ImGui::BeginTabItem(doc->Name, &doc->Open, tab_flags); + + // Cancel attempt to close when unsaved add to save queue so we can display a popup. + if (!doc->Open && doc->Dirty) + { + doc->Open = true; + doc->DoQueueClose(); + } + + MyDocument::DisplayContextMenu(doc); + if (visible) + { + MyDocument::DisplayContents(doc); + ImGui::EndTabItem(); + } + } + + ImGui::EndTabBar(); + } + } + + // Update closing queue + static ImVector close_queue; + if (close_queue.empty()) + { + // Close queue is locked once we started a popup + for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) + { + MyDocument* doc = &app.Documents[doc_n]; + if (doc->WantClose) + { + doc->WantClose = false; + close_queue.push_back(doc); + } + } + } + + // Display closing confirmation UI + if (!close_queue.empty()) + { + int close_queue_unsaved_documents = 0; + for (int n = 0; n < close_queue.Size; n++) + if (close_queue[n]->Dirty) + close_queue_unsaved_documents++; + + if (close_queue_unsaved_documents == 0) + { + // Close documents when all are unsaved + for (int n = 0; n < close_queue.Size; n++) + close_queue[n]->DoForceClose(); + close_queue.clear(); + } + else + { + if (!ImGui::IsPopupOpen("Save?")) + ImGui::OpenPopup("Save?"); + if (ImGui::BeginPopupModal("Save?", NULL, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::Text("Save change to the following items?"); + float item_height = ImGui::GetTextLineHeightWithSpacing(); + if (ImGui::BeginChildFrame(ImGui::GetID("frame"), ImVec2(-FLT_MIN, 6.25f * item_height))) + { + for (int n = 0; n < close_queue.Size; n++) + if (close_queue[n]->Dirty) + ImGui::Text("%s", close_queue[n]->Name); + ImGui::EndChildFrame(); + } + + ImVec2 button_size(ImGui::GetFontSize() * 7.0f, 0.0f); + if (ImGui::Button("Yes", button_size)) + { + for (int n = 0; n < close_queue.Size; n++) + { + if (close_queue[n]->Dirty) + close_queue[n]->DoSave(); + close_queue[n]->DoForceClose(); + } + close_queue.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("No", button_size)) + { + for (int n = 0; n < close_queue.Size; n++) + close_queue[n]->DoForceClose(); + close_queue.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", button_size)) + { + close_queue.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } + } + + ImGui::End(); +} + +// End of Demo code +#else + +void ImGui::ShowAboutWindow(bool*) {} +void ImGui::ShowDemoWindow(bool*) {} +void ImGui::ShowUserGuide() {} +void ImGui::ShowStyleEditor(ImGuiStyle*) {} + +#endif + +#endif // #ifndef IMGUI_DISABLE + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Private/imgui_draw.cpp`: + +```cpp +// dear imgui, v1.87 +// (drawing and font code) + +/* + +Index of this file: + +// [SECTION] STB libraries implementation +// [SECTION] Style functions +// [SECTION] ImDrawList +// [SECTION] ImDrawListSplitter +// [SECTION] ImDrawData +// [SECTION] Helpers ShadeVertsXXX functions +// [SECTION] ImFontConfig +// [SECTION] ImFontAtlas +// [SECTION] ImFontAtlas glyph ranges helpers +// [SECTION] ImFontGlyphRangesBuilder +// [SECTION] ImFont +// [SECTION] ImGui Internal Render Helpers +// [SECTION] Decompression code +// [SECTION] Default font data (ProggyClean.ttf) + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif + +#include "imgui_internal.h" +#ifdef IMGUI_ENABLE_FREETYPE +#include "misc/freetype/imgui_freetype.h" +#endif + +#include // vsnprintf, sscanf, printf +#if !defined(alloca) +#if defined(__GLIBC__) || defined(__sun) || defined(__APPLE__) || defined(__NEWLIB__) +#include // alloca (glibc uses . Note that Cygwin may have _WIN32 defined, so the order matters here) +#elif defined(_WIN32) +#include // alloca +#if !defined(alloca) +#define alloca _alloca // for clang with MS Codegen +#endif +#else +#include // alloca +#endif +#endif + +// Visual Studio warnings +#ifdef _MSC_VER +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff) +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#pragma warning (disable: 6255) // [Static Analyzer] _alloca indicates failure by raising a stack overflow exception. Consider using _malloca instead. +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). [MSVC Static Analyzer) +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! +#endif +#if __has_warning("-Walloca") +#pragma clang diagnostic ignored "-Walloca" // warning: use of function '__builtin_alloca' is discouraged +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants ok. +#pragma clang diagnostic ignored "-Wglobal-constructors" // warning: declaration requires a global destructor // similar to above, not sure what the exact difference is. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wcomma" // warning: possible misuse of comma operator here +#pragma clang diagnostic ignored "-Wreserved-id-macro" // warning: macro name is a reserved identifier +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#elif defined(__GNUC__) +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value +#pragma GCC diagnostic ignored "-Wstack-protector" // warning: stack protector not protecting local variables: variable length buffer +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#endif + +//------------------------------------------------------------------------- +// [SECTION] STB libraries implementation +//------------------------------------------------------------------------- + +// Compile time options: +//#define IMGUI_STB_NAMESPACE ImStb +//#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h" +//#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h" +//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION + +#ifdef IMGUI_STB_NAMESPACE +namespace IMGUI_STB_NAMESPACE +{ +#endif + +#ifdef _MSC_VER +#pragma warning (push) +#pragma warning (disable: 4456) // declaration of 'xx' hides previous local declaration +#pragma warning (disable: 6011) // (stb_rectpack) Dereferencing NULL pointer 'cur->next'. +#pragma warning (disable: 6385) // (stb_truetype) Reading invalid data from 'buffer': the readable size is '_Old_3`kernel_width' bytes, but '3' bytes may be read. +#pragma warning (disable: 28182) // (stb_rectpack) Dereferencing NULL pointer. 'cur' contains the same NULL value as 'cur->next' did. +#endif + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#pragma clang diagnostic ignored "-Wimplicit-fallthrough" +#pragma clang diagnostic ignored "-Wcast-qual" // warning: cast from 'const xxxx *' to 'xxx *' drops const qualifier +#endif + +#if defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtype-limits" // warning: comparison is always true due to limited range of data type [-Wtype-limits] +#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers +#endif + +#ifndef STB_RECT_PACK_IMPLEMENTATION // in case the user already have an implementation in the _same_ compilation unit (e.g. unity builds) +#ifndef IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION // in case the user already have an implementation in another compilation unit +#define STBRP_STATIC +#define STBRP_ASSERT(x) do { IM_ASSERT(x); } while (0) +#define STBRP_SORT ImQsort +#define STB_RECT_PACK_IMPLEMENTATION +#endif +#ifdef IMGUI_STB_RECT_PACK_FILENAME +#include IMGUI_STB_RECT_PACK_FILENAME +#else +#include "imstb_rectpack.h" +#endif +#endif + +#ifdef IMGUI_ENABLE_STB_TRUETYPE +#ifndef STB_TRUETYPE_IMPLEMENTATION // in case the user already have an implementation in the _same_ compilation unit (e.g. unity builds) +#ifndef IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION // in case the user already have an implementation in another compilation unit +#define STBTT_malloc(x,u) ((void)(u), IM_ALLOC(x)) +#define STBTT_free(x,u) ((void)(u), IM_FREE(x)) +#define STBTT_assert(x) do { IM_ASSERT(x); } while(0) +#define STBTT_fmod(x,y) ImFmod(x,y) +#define STBTT_sqrt(x) ImSqrt(x) +#define STBTT_pow(x,y) ImPow(x,y) +#define STBTT_fabs(x) ImFabs(x) +#define STBTT_ifloor(x) ((int)ImFloorSigned(x)) +#define STBTT_iceil(x) ((int)ImCeil(x)) +#define STBTT_STATIC +#define STB_TRUETYPE_IMPLEMENTATION +#else +#define STBTT_DEF extern +#endif +#ifdef IMGUI_STB_TRUETYPE_FILENAME +#include IMGUI_STB_TRUETYPE_FILENAME +#else +#include "imstb_truetype.h" +#endif +#endif +#endif // IMGUI_ENABLE_STB_TRUETYPE + +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#if defined(_MSC_VER) +#pragma warning (pop) +#endif + +#ifdef IMGUI_STB_NAMESPACE +} // namespace ImStb +using namespace IMGUI_STB_NAMESPACE; +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Style functions +//----------------------------------------------------------------------------- + +void ImGui::StyleColorsDark(ImGuiStyle* dst) +{ + ImGuiStyle* style = dst ? dst : &ImGui::GetStyle(); + ImVec4* colors = style->Colors; + + colors[ImGuiCol_Text] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.94f); + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f); + colors[ImGuiCol_Border] = ImVec4(0.43f, 0.43f, 0.50f, 0.50f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.29f, 0.48f, 0.54f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_TitleBg] = ImVec4(0.04f, 0.04f, 0.04f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.16f, 0.29f, 0.48f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.53f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.31f, 0.31f, 0.31f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.41f, 0.41f, 0.41f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.51f, 0.51f, 0.51f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.24f, 0.52f, 0.88f, 1.00f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.26f, 0.59f, 0.98f, 0.31f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_Separator] = colors[ImGuiCol_Border]; + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.10f, 0.40f, 0.75f, 0.78f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.10f, 0.40f, 0.75f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.26f, 0.59f, 0.98f, 0.20f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); + colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.80f); + colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_TabActive] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); + colors[ImGuiCol_TabUnfocused] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); + colors[ImGuiCol_TabUnfocusedActive] = ImLerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.19f, 0.19f, 0.20f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.31f, 0.31f, 0.35f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableBorderLight] = ImVec4(0.23f, 0.23f, 0.25f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.06f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); + colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); +} + +void ImGui::StyleColorsClassic(ImGuiStyle* dst) +{ + ImGuiStyle* style = dst ? dst : &ImGui::GetStyle(); + ImVec4* colors = style->Colors; + + colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.85f); + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.11f, 0.11f, 0.14f, 0.92f); + colors[ImGuiCol_Border] = ImVec4(0.50f, 0.50f, 0.50f, 0.50f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.43f, 0.43f, 0.43f, 0.39f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.47f, 0.47f, 0.69f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.42f, 0.41f, 0.64f, 0.69f); + colors[ImGuiCol_TitleBg] = ImVec4(0.27f, 0.27f, 0.54f, 0.83f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.32f, 0.32f, 0.63f, 0.87f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.40f, 0.40f, 0.80f, 0.20f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.40f, 0.40f, 0.55f, 0.80f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.20f, 0.25f, 0.30f, 0.60f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.40f, 0.40f, 0.80f, 0.30f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.40f, 0.40f, 0.80f, 0.40f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f); + colors[ImGuiCol_CheckMark] = ImVec4(0.90f, 0.90f, 0.90f, 0.50f); + colors[ImGuiCol_SliderGrab] = ImVec4(1.00f, 1.00f, 1.00f, 0.30f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f); + colors[ImGuiCol_Button] = ImVec4(0.35f, 0.40f, 0.61f, 0.62f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.40f, 0.48f, 0.71f, 0.79f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.46f, 0.54f, 0.80f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.40f, 0.40f, 0.90f, 0.45f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.45f, 0.45f, 0.90f, 0.80f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.53f, 0.53f, 0.87f, 0.80f); + colors[ImGuiCol_Separator] = ImVec4(0.50f, 0.50f, 0.50f, 0.60f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.60f, 0.60f, 0.70f, 1.00f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.70f, 0.70f, 0.90f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(1.00f, 1.00f, 1.00f, 0.10f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.78f, 0.82f, 1.00f, 0.60f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.78f, 0.82f, 1.00f, 0.90f); + colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.80f); + colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_TabActive] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); + colors[ImGuiCol_TabUnfocused] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); + colors[ImGuiCol_TabUnfocusedActive] = ImLerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_PlotLines] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.27f, 0.27f, 0.38f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.31f, 0.31f, 0.45f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableBorderLight] = ImVec4(0.26f, 0.26f, 0.28f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.07f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.00f, 0.00f, 1.00f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); + colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); +} + +// Those light colors are better suited with a thicker font than the default one + FrameBorder +void ImGui::StyleColorsLight(ImGuiStyle* dst) +{ + ImGuiStyle* style = dst ? dst : &ImGui::GetStyle(); + ImVec4* colors = style->Colors; + + colors[ImGuiCol_Text] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.94f, 0.94f, 0.94f, 1.00f); + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.98f); + colors[ImGuiCol_Border] = ImVec4(0.00f, 0.00f, 0.00f, 0.30f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_TitleBg] = ImVec4(0.96f, 0.96f, 0.96f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.82f, 0.82f, 0.82f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(1.00f, 1.00f, 1.00f, 0.51f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.86f, 0.86f, 0.86f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.98f, 0.98f, 0.98f, 0.53f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.69f, 0.69f, 0.69f, 0.80f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.49f, 0.49f, 0.49f, 0.80f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.49f, 0.49f, 0.49f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.26f, 0.59f, 0.98f, 0.78f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.46f, 0.54f, 0.80f, 0.60f); + colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.26f, 0.59f, 0.98f, 0.31f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_Separator] = ImVec4(0.39f, 0.39f, 0.39f, 0.62f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.14f, 0.44f, 0.80f, 0.78f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.14f, 0.44f, 0.80f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.35f, 0.35f, 0.35f, 0.17f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); + colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.90f); + colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_TabActive] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); + colors[ImGuiCol_TabUnfocused] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); + colors[ImGuiCol_TabUnfocusedActive] = ImLerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_PlotLines] = ImVec4(0.39f, 0.39f, 0.39f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.45f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.78f, 0.87f, 0.98f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.57f, 0.57f, 0.64f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableBorderLight] = ImVec4(0.68f, 0.68f, 0.74f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(0.30f, 0.30f, 0.30f, 0.09f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); + colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(0.70f, 0.70f, 0.70f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); +} + +//----------------------------------------------------------------------------- +// [SECTION] ImDrawList +//----------------------------------------------------------------------------- + +ImDrawListSharedData::ImDrawListSharedData() +{ + memset(this, 0, sizeof(*this)); + for (int i = 0; i < IM_ARRAYSIZE(ArcFastVtx); i++) + { + const float a = ((float)i * 2 * IM_PI) / (float)IM_ARRAYSIZE(ArcFastVtx); + ArcFastVtx[i] = ImVec2(ImCos(a), ImSin(a)); + } + ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, CircleSegmentMaxError); +} + +void ImDrawListSharedData::SetCircleTessellationMaxError(float max_error) +{ + if (CircleSegmentMaxError == max_error) + return; + + IM_ASSERT(max_error > 0.0f); + CircleSegmentMaxError = max_error; + for (int i = 0; i < IM_ARRAYSIZE(CircleSegmentCounts); i++) + { + const float radius = (float)i; + CircleSegmentCounts[i] = (ImU8)((i > 0) ? IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, CircleSegmentMaxError) : 0); + } + ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, CircleSegmentMaxError); +} + +// Initialize before use in a new frame. We always have a command ready in the buffer. +void ImDrawList::_ResetForNewFrame() +{ + // Verify that the ImDrawCmd fields we want to memcmp() are contiguous in memory. + IM_STATIC_ASSERT(IM_OFFSETOF(ImDrawCmd, ClipRect) == 0); + IM_STATIC_ASSERT(IM_OFFSETOF(ImDrawCmd, TextureId) == sizeof(ImVec4)); + IM_STATIC_ASSERT(IM_OFFSETOF(ImDrawCmd, VtxOffset) == sizeof(ImVec4) + sizeof(ImTextureID)); + + CmdBuffer.resize(0); + IdxBuffer.resize(0); + VtxBuffer.resize(0); + Flags = _Data->InitialFlags; + memset(&_CmdHeader, 0, sizeof(_CmdHeader)); + _VtxCurrentIdx = 0; + _VtxWritePtr = NULL; + _IdxWritePtr = NULL; + _ClipRectStack.resize(0); + _TextureIdStack.resize(0); + _Path.resize(0); + _Splitter.Clear(); + CmdBuffer.push_back(ImDrawCmd()); + _FringeScale = 1.0f; +} + +void ImDrawList::_ClearFreeMemory() +{ + CmdBuffer.clear(); + IdxBuffer.clear(); + VtxBuffer.clear(); + Flags = ImDrawListFlags_None; + _VtxCurrentIdx = 0; + _VtxWritePtr = NULL; + _IdxWritePtr = NULL; + _ClipRectStack.clear(); + _TextureIdStack.clear(); + _Path.clear(); + _Splitter.ClearFreeMemory(); +} + +ImDrawList* ImDrawList::CloneOutput() const +{ + ImDrawList* dst = IM_NEW(ImDrawList(_Data)); + dst->CmdBuffer = CmdBuffer; + dst->IdxBuffer = IdxBuffer; + dst->VtxBuffer = VtxBuffer; + dst->Flags = Flags; + return dst; +} + +void ImDrawList::AddDrawCmd() +{ + ImDrawCmd draw_cmd; + draw_cmd.ClipRect = _CmdHeader.ClipRect; // Same as calling ImDrawCmd_HeaderCopy() + draw_cmd.TextureId = _CmdHeader.TextureId; + draw_cmd.VtxOffset = _CmdHeader.VtxOffset; + draw_cmd.IdxOffset = IdxBuffer.Size; + + IM_ASSERT(draw_cmd.ClipRect.x <= draw_cmd.ClipRect.z && draw_cmd.ClipRect.y <= draw_cmd.ClipRect.w); + CmdBuffer.push_back(draw_cmd); +} + +// Pop trailing draw command (used before merging or presenting to user) +// Note that this leaves the ImDrawList in a state unfit for further commands, as most code assume that CmdBuffer.Size > 0 && CmdBuffer.back().UserCallback == NULL +void ImDrawList::_PopUnusedDrawCmd() +{ + if (CmdBuffer.Size == 0) + return; + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount == 0 && curr_cmd->UserCallback == NULL) + CmdBuffer.pop_back(); +} + +void ImDrawList::AddCallback(ImDrawCallback callback, void* callback_data) +{ + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + IM_ASSERT(curr_cmd->UserCallback == NULL); + if (curr_cmd->ElemCount != 0) + { + AddDrawCmd(); + curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + } + curr_cmd->UserCallback = callback; + curr_cmd->UserCallbackData = callback_data; + + AddDrawCmd(); // Force a new command after us (see comment below) +} + +// Compare ClipRect, TextureId and VtxOffset with a single memcmp() +#define ImDrawCmd_HeaderSize (IM_OFFSETOF(ImDrawCmd, VtxOffset) + sizeof(unsigned int)) +#define ImDrawCmd_HeaderCompare(CMD_LHS, CMD_RHS) (memcmp(CMD_LHS, CMD_RHS, ImDrawCmd_HeaderSize)) // Compare ClipRect, TextureId, VtxOffset +#define ImDrawCmd_HeaderCopy(CMD_DST, CMD_SRC) (memcpy(CMD_DST, CMD_SRC, ImDrawCmd_HeaderSize)) // Copy ClipRect, TextureId, VtxOffset +#define ImDrawCmd_AreSequentialIdxOffset(CMD_0, CMD_1) (CMD_0->IdxOffset + CMD_0->ElemCount == CMD_1->IdxOffset) + +// Try to merge two last draw commands +void ImDrawList::_TryMergeDrawCmds() +{ + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + ImDrawCmd* prev_cmd = curr_cmd - 1; + if (ImDrawCmd_HeaderCompare(curr_cmd, prev_cmd) == 0 && ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) && curr_cmd->UserCallback == NULL && prev_cmd->UserCallback == NULL) + { + prev_cmd->ElemCount += curr_cmd->ElemCount; + CmdBuffer.pop_back(); + } +} + +// Our scheme may appears a bit unusual, basically we want the most-common calls AddLine AddRect etc. to not have to perform any check so we always have a command ready in the stack. +// The cost of figuring out if a new command has to be added or if we can merge is paid in those Update** functions only. +void ImDrawList::_OnChangedClipRect() +{ + // If current command is used with different settings we need to add a new command + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount != 0 && memcmp(&curr_cmd->ClipRect, &_CmdHeader.ClipRect, sizeof(ImVec4)) != 0) + { + AddDrawCmd(); + return; + } + IM_ASSERT(curr_cmd->UserCallback == NULL); + + // Try to merge with previous command if it matches, else use current command + ImDrawCmd* prev_cmd = curr_cmd - 1; + if (curr_cmd->ElemCount == 0 && CmdBuffer.Size > 1 && ImDrawCmd_HeaderCompare(&_CmdHeader, prev_cmd) == 0 && ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) && prev_cmd->UserCallback == NULL) + { + CmdBuffer.pop_back(); + return; + } + + curr_cmd->ClipRect = _CmdHeader.ClipRect; +} + +void ImDrawList::_OnChangedTextureID() +{ + // If current command is used with different settings we need to add a new command + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount != 0 && curr_cmd->TextureId != _CmdHeader.TextureId) + { + AddDrawCmd(); + return; + } + IM_ASSERT(curr_cmd->UserCallback == NULL); + + // Try to merge with previous command if it matches, else use current command + ImDrawCmd* prev_cmd = curr_cmd - 1; + if (curr_cmd->ElemCount == 0 && CmdBuffer.Size > 1 && ImDrawCmd_HeaderCompare(&_CmdHeader, prev_cmd) == 0 && ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) && prev_cmd->UserCallback == NULL) + { + CmdBuffer.pop_back(); + return; + } + + curr_cmd->TextureId = _CmdHeader.TextureId; +} + +void ImDrawList::_OnChangedVtxOffset() +{ + // We don't need to compare curr_cmd->VtxOffset != _CmdHeader.VtxOffset because we know it'll be different at the time we call this. + _VtxCurrentIdx = 0; + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + //IM_ASSERT(curr_cmd->VtxOffset != _CmdHeader.VtxOffset); // See #3349 + if (curr_cmd->ElemCount != 0) + { + AddDrawCmd(); + return; + } + IM_ASSERT(curr_cmd->UserCallback == NULL); + curr_cmd->VtxOffset = _CmdHeader.VtxOffset; +} + +int ImDrawList::_CalcCircleAutoSegmentCount(float radius) const +{ + // Automatic segment count + const int radius_idx = (int)(radius + 0.999999f); // ceil to never reduce accuracy + if (radius_idx < IM_ARRAYSIZE(_Data->CircleSegmentCounts)) + return _Data->CircleSegmentCounts[radius_idx]; // Use cached value + else + return IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, _Data->CircleSegmentMaxError); +} + +// Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) +void ImDrawList::PushClipRect(ImVec2 cr_min, ImVec2 cr_max, bool intersect_with_current_clip_rect) +{ + ImVec4 cr(cr_min.x, cr_min.y, cr_max.x, cr_max.y); + if (intersect_with_current_clip_rect) + { + ImVec4 current = _CmdHeader.ClipRect; + if (cr.x < current.x) cr.x = current.x; + if (cr.y < current.y) cr.y = current.y; + if (cr.z > current.z) cr.z = current.z; + if (cr.w > current.w) cr.w = current.w; + } + cr.z = ImMax(cr.x, cr.z); + cr.w = ImMax(cr.y, cr.w); + + _ClipRectStack.push_back(cr); + _CmdHeader.ClipRect = cr; + _OnChangedClipRect(); +} + +void ImDrawList::PushClipRectFullScreen() +{ + PushClipRect(ImVec2(_Data->ClipRectFullscreen.x, _Data->ClipRectFullscreen.y), ImVec2(_Data->ClipRectFullscreen.z, _Data->ClipRectFullscreen.w)); +} + +void ImDrawList::PopClipRect() +{ + _ClipRectStack.pop_back(); + _CmdHeader.ClipRect = (_ClipRectStack.Size == 0) ? _Data->ClipRectFullscreen : _ClipRectStack.Data[_ClipRectStack.Size - 1]; + _OnChangedClipRect(); +} + +void ImDrawList::PushTextureID(ImTextureID texture_id) +{ + _TextureIdStack.push_back(texture_id); + _CmdHeader.TextureId = texture_id; + _OnChangedTextureID(); +} + +void ImDrawList::PopTextureID() +{ + _TextureIdStack.pop_back(); + _CmdHeader.TextureId = (_TextureIdStack.Size == 0) ? (ImTextureID)NULL : _TextureIdStack.Data[_TextureIdStack.Size - 1]; + _OnChangedTextureID(); +} + +// Reserve space for a number of vertices and indices. +// You must finish filling your reserved data before calling PrimReserve() again, as it may reallocate or +// submit the intermediate results. PrimUnreserve() can be used to release unused allocations. +void ImDrawList::PrimReserve(int idx_count, int vtx_count) +{ + // Large mesh support (when enabled) + IM_ASSERT_PARANOID(idx_count >= 0 && vtx_count >= 0); + if (sizeof(ImDrawIdx) == 2 && (_VtxCurrentIdx + vtx_count >= (1 << 16)) && (Flags & ImDrawListFlags_AllowVtxOffset)) + { + // FIXME: In theory we should be testing that vtx_count <64k here. + // In practice, RenderText() relies on reserving ahead for a worst case scenario so it is currently useful for us + // to not make that check until we rework the text functions to handle clipping and large horizontal lines better. + _CmdHeader.VtxOffset = VtxBuffer.Size; + _OnChangedVtxOffset(); + } + + ImDrawCmd* draw_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + draw_cmd->ElemCount += idx_count; + + int vtx_buffer_old_size = VtxBuffer.Size; + VtxBuffer.resize(vtx_buffer_old_size + vtx_count); + _VtxWritePtr = VtxBuffer.Data + vtx_buffer_old_size; + + int idx_buffer_old_size = IdxBuffer.Size; + IdxBuffer.resize(idx_buffer_old_size + idx_count); + _IdxWritePtr = IdxBuffer.Data + idx_buffer_old_size; +} + +// Release the a number of reserved vertices/indices from the end of the last reservation made with PrimReserve(). +void ImDrawList::PrimUnreserve(int idx_count, int vtx_count) +{ + IM_ASSERT_PARANOID(idx_count >= 0 && vtx_count >= 0); + + ImDrawCmd* draw_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + draw_cmd->ElemCount -= idx_count; + VtxBuffer.shrink(VtxBuffer.Size - vtx_count); + IdxBuffer.shrink(IdxBuffer.Size - idx_count); +} + +// Fully unrolled with inline call to keep our debug builds decently fast. +void ImDrawList::PrimRect(const ImVec2& a, const ImVec2& c, ImU32 col) +{ + ImVec2 b(c.x, a.y), d(a.x, c.y), uv(_Data->TexUvWhitePixel); + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + _IdxWritePtr[0] = idx; _IdxWritePtr[1] = (ImDrawIdx)(idx+1); _IdxWritePtr[2] = (ImDrawIdx)(idx+2); + _IdxWritePtr[3] = idx; _IdxWritePtr[4] = (ImDrawIdx)(idx+2); _IdxWritePtr[5] = (ImDrawIdx)(idx+3); + _VtxWritePtr[0].pos = a; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = b; _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = c; _VtxWritePtr[2].uv = uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = d; _VtxWritePtr[3].uv = uv; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + _VtxCurrentIdx += 4; + _IdxWritePtr += 6; +} + +void ImDrawList::PrimRectUV(const ImVec2& a, const ImVec2& c, const ImVec2& uv_a, const ImVec2& uv_c, ImU32 col) +{ + ImVec2 b(c.x, a.y), d(a.x, c.y), uv_b(uv_c.x, uv_a.y), uv_d(uv_a.x, uv_c.y); + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + _IdxWritePtr[0] = idx; _IdxWritePtr[1] = (ImDrawIdx)(idx+1); _IdxWritePtr[2] = (ImDrawIdx)(idx+2); + _IdxWritePtr[3] = idx; _IdxWritePtr[4] = (ImDrawIdx)(idx+2); _IdxWritePtr[5] = (ImDrawIdx)(idx+3); + _VtxWritePtr[0].pos = a; _VtxWritePtr[0].uv = uv_a; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = b; _VtxWritePtr[1].uv = uv_b; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = c; _VtxWritePtr[2].uv = uv_c; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = d; _VtxWritePtr[3].uv = uv_d; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + _VtxCurrentIdx += 4; + _IdxWritePtr += 6; +} + +void ImDrawList::PrimQuadUV(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a, const ImVec2& uv_b, const ImVec2& uv_c, const ImVec2& uv_d, ImU32 col) +{ + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + _IdxWritePtr[0] = idx; _IdxWritePtr[1] = (ImDrawIdx)(idx+1); _IdxWritePtr[2] = (ImDrawIdx)(idx+2); + _IdxWritePtr[3] = idx; _IdxWritePtr[4] = (ImDrawIdx)(idx+2); _IdxWritePtr[5] = (ImDrawIdx)(idx+3); + _VtxWritePtr[0].pos = a; _VtxWritePtr[0].uv = uv_a; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = b; _VtxWritePtr[1].uv = uv_b; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = c; _VtxWritePtr[2].uv = uv_c; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = d; _VtxWritePtr[3].uv = uv_d; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + _VtxCurrentIdx += 4; + _IdxWritePtr += 6; +} + +// On AddPolyline() and AddConvexPolyFilled() we intentionally avoid using ImVec2 and superfluous function calls to optimize debug/non-inlined builds. +// - Those macros expects l-values and need to be used as their own statement. +// - Those macros are intentionally not surrounded by the 'do {} while (0)' idiom because even that translates to runtime with debug compilers. +#define IM_NORMALIZE2F_OVER_ZERO(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 > 0.0f) { float inv_len = ImRsqrt(d2); VX *= inv_len; VY *= inv_len; } } (void)0 +#define IM_FIXNORMAL2F_MAX_INVLEN2 100.0f // 500.0f (see #4053, #3366) +#define IM_FIXNORMAL2F(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 > 0.000001f) { float inv_len2 = 1.0f / d2; if (inv_len2 > IM_FIXNORMAL2F_MAX_INVLEN2) inv_len2 = IM_FIXNORMAL2F_MAX_INVLEN2; VX *= inv_len2; VY *= inv_len2; } } (void)0 + +// TODO: Thickness anti-aliased lines cap are missing their AA fringe. +// We avoid using the ImVec2 math operators here to reduce cost to a minimum for debug/non-inlined builds. +void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 col, ImDrawFlags flags, float thickness) +{ + if (points_count < 2) + return; + + const bool closed = (flags & ImDrawFlags_Closed) != 0; + const ImVec2 opaque_uv = _Data->TexUvWhitePixel; + const int count = closed ? points_count : points_count - 1; // The number of line segments we need to draw + const bool thick_line = (thickness > _FringeScale); + + if (Flags & ImDrawListFlags_AntiAliasedLines) + { + // Anti-aliased stroke + const float AA_SIZE = _FringeScale; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + + // Thicknesses <1.0 should behave like thickness 1.0 + thickness = ImMax(thickness, 1.0f); + const int integer_thickness = (int)thickness; + const float fractional_thickness = thickness - integer_thickness; + + // Do we want to draw this line using a texture? + // - For now, only draw integer-width lines using textures to avoid issues with the way scaling occurs, could be improved. + // - If AA_SIZE is not 1.0f we cannot use the texture path. + const bool use_texture = (Flags & ImDrawListFlags_AntiAliasedLinesUseTex) && (integer_thickness < IM_DRAWLIST_TEX_LINES_WIDTH_MAX) && (fractional_thickness <= 0.00001f) && (AA_SIZE == 1.0f); + + // We should never hit this, because NewFrame() doesn't set ImDrawListFlags_AntiAliasedLinesUseTex unless ImFontAtlasFlags_NoBakedLines is off + IM_ASSERT_PARANOID(!use_texture || !(_Data->Font->ContainerAtlas->Flags & ImFontAtlasFlags_NoBakedLines)); + + const int idx_count = use_texture ? (count * 6) : (thick_line ? count * 18 : count * 12); + const int vtx_count = use_texture ? (points_count * 2) : (thick_line ? points_count * 4 : points_count * 3); + PrimReserve(idx_count, vtx_count); + + // Temporary buffer + // The first items are normals at each line point, then after that there are either 2 or 4 temp points for each line point + ImVec2* temp_normals = (ImVec2*)alloca(points_count * ((use_texture || !thick_line) ? 3 : 5) * sizeof(ImVec2)); //-V630 + ImVec2* temp_points = temp_normals + points_count; + + // Calculate normals (tangents) for each line segment + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; + float dx = points[i2].x - points[i1].x; + float dy = points[i2].y - points[i1].y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + temp_normals[i1].x = dy; + temp_normals[i1].y = -dx; + } + if (!closed) + temp_normals[points_count - 1] = temp_normals[points_count - 2]; + + // If we are drawing a one-pixel-wide line without a texture, or a textured line of any width, we only need 2 or 3 vertices per point + if (use_texture || !thick_line) + { + // [PATH 1] Texture-based lines (thick or non-thick) + // [PATH 2] Non texture-based lines (non-thick) + + // The width of the geometry we need to draw - this is essentially pixels for the line itself, plus "one pixel" for AA. + // - In the texture-based path, we don't use AA_SIZE here because the +1 is tied to the generated texture + // (see ImFontAtlasBuildRenderLinesTexData() function), and so alternate values won't work without changes to that code. + // - In the non texture-based paths, we would allow AA_SIZE to potentially be != 1.0f with a patch (e.g. fringe_scale patch to + // allow scaling geometry while preserving one-screen-pixel AA fringe). + const float half_draw_size = use_texture ? ((thickness * 0.5f) + 1) : AA_SIZE; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend + if (!closed) + { + temp_points[0] = points[0] + temp_normals[0] * half_draw_size; + temp_points[1] = points[0] - temp_normals[0] * half_draw_size; + temp_points[(points_count-1)*2+0] = points[points_count-1] + temp_normals[points_count-1] * half_draw_size; + temp_points[(points_count-1)*2+1] = points[points_count-1] - temp_normals[points_count-1] * half_draw_size; + } + + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) + // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. + unsigned int idx1 = _VtxCurrentIdx; // Vertex index for start of line segment + for (int i1 = 0; i1 < count; i1++) // i1 is the first point of the line segment + { + const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; // i2 is the second point of the line segment + const unsigned int idx2 = ((i1 + 1) == points_count) ? _VtxCurrentIdx : (idx1 + (use_texture ? 2 : 3)); // Vertex index for end of segment + + // Average normals + float dm_x = (temp_normals[i1].x + temp_normals[i2].x) * 0.5f; + float dm_y = (temp_normals[i1].y + temp_normals[i2].y) * 0.5f; + IM_FIXNORMAL2F(dm_x, dm_y); + dm_x *= half_draw_size; // dm_x, dm_y are offset to the outer edge of the AA area + dm_y *= half_draw_size; + + // Add temporary vertexes for the outer edges + ImVec2* out_vtx = &temp_points[i2 * 2]; + out_vtx[0].x = points[i2].x + dm_x; + out_vtx[0].y = points[i2].y + dm_y; + out_vtx[1].x = points[i2].x - dm_x; + out_vtx[1].y = points[i2].y - dm_y; + + if (use_texture) + { + // Add indices for two triangles + _IdxWritePtr[0] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[1] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[2] = (ImDrawIdx)(idx1 + 1); // Right tri + _IdxWritePtr[3] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[4] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[5] = (ImDrawIdx)(idx2 + 0); // Left tri + _IdxWritePtr += 6; + } + else + { + // Add indexes for four triangles + _IdxWritePtr[0] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[1] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[2] = (ImDrawIdx)(idx1 + 2); // Right tri 1 + _IdxWritePtr[3] = (ImDrawIdx)(idx1 + 2); _IdxWritePtr[4] = (ImDrawIdx)(idx2 + 2); _IdxWritePtr[5] = (ImDrawIdx)(idx2 + 0); // Right tri 2 + _IdxWritePtr[6] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[7] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[8] = (ImDrawIdx)(idx1 + 0); // Left tri 1 + _IdxWritePtr[9] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[10] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[11] = (ImDrawIdx)(idx2 + 1); // Left tri 2 + _IdxWritePtr += 12; + } + + idx1 = idx2; + } + + // Add vertexes for each point on the line + if (use_texture) + { + // If we're using textures we only need to emit the left/right edge vertices + ImVec4 tex_uvs = _Data->TexUvLines[integer_thickness]; + /*if (fractional_thickness != 0.0f) // Currently always zero when use_texture==false! + { + const ImVec4 tex_uvs_1 = _Data->TexUvLines[integer_thickness + 1]; + tex_uvs.x = tex_uvs.x + (tex_uvs_1.x - tex_uvs.x) * fractional_thickness; // inlined ImLerp() + tex_uvs.y = tex_uvs.y + (tex_uvs_1.y - tex_uvs.y) * fractional_thickness; + tex_uvs.z = tex_uvs.z + (tex_uvs_1.z - tex_uvs.z) * fractional_thickness; + tex_uvs.w = tex_uvs.w + (tex_uvs_1.w - tex_uvs.w) * fractional_thickness; + }*/ + ImVec2 tex_uv0(tex_uvs.x, tex_uvs.y); + ImVec2 tex_uv1(tex_uvs.z, tex_uvs.w); + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = temp_points[i * 2 + 0]; _VtxWritePtr[0].uv = tex_uv0; _VtxWritePtr[0].col = col; // Left-side outer edge + _VtxWritePtr[1].pos = temp_points[i * 2 + 1]; _VtxWritePtr[1].uv = tex_uv1; _VtxWritePtr[1].col = col; // Right-side outer edge + _VtxWritePtr += 2; + } + } + else + { + // If we're not using a texture, we need the center vertex as well + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = col; // Center of line + _VtxWritePtr[1].pos = temp_points[i * 2 + 0]; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = col_trans; // Left-side outer edge + _VtxWritePtr[2].pos = temp_points[i * 2 + 1]; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = col_trans; // Right-side outer edge + _VtxWritePtr += 3; + } + } + } + else + { + // [PATH 2] Non texture-based lines (thick): we need to draw the solid line core and thus require four vertices per point + const float half_inner_thickness = (thickness - AA_SIZE) * 0.5f; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend + if (!closed) + { + const int points_last = points_count - 1; + temp_points[0] = points[0] + temp_normals[0] * (half_inner_thickness + AA_SIZE); + temp_points[1] = points[0] + temp_normals[0] * (half_inner_thickness); + temp_points[2] = points[0] - temp_normals[0] * (half_inner_thickness); + temp_points[3] = points[0] - temp_normals[0] * (half_inner_thickness + AA_SIZE); + temp_points[points_last * 4 + 0] = points[points_last] + temp_normals[points_last] * (half_inner_thickness + AA_SIZE); + temp_points[points_last * 4 + 1] = points[points_last] + temp_normals[points_last] * (half_inner_thickness); + temp_points[points_last * 4 + 2] = points[points_last] - temp_normals[points_last] * (half_inner_thickness); + temp_points[points_last * 4 + 3] = points[points_last] - temp_normals[points_last] * (half_inner_thickness + AA_SIZE); + } + + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) + // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. + unsigned int idx1 = _VtxCurrentIdx; // Vertex index for start of line segment + for (int i1 = 0; i1 < count; i1++) // i1 is the first point of the line segment + { + const int i2 = (i1 + 1) == points_count ? 0 : (i1 + 1); // i2 is the second point of the line segment + const unsigned int idx2 = (i1 + 1) == points_count ? _VtxCurrentIdx : (idx1 + 4); // Vertex index for end of segment + + // Average normals + float dm_x = (temp_normals[i1].x + temp_normals[i2].x) * 0.5f; + float dm_y = (temp_normals[i1].y + temp_normals[i2].y) * 0.5f; + IM_FIXNORMAL2F(dm_x, dm_y); + float dm_out_x = dm_x * (half_inner_thickness + AA_SIZE); + float dm_out_y = dm_y * (half_inner_thickness + AA_SIZE); + float dm_in_x = dm_x * half_inner_thickness; + float dm_in_y = dm_y * half_inner_thickness; + + // Add temporary vertices + ImVec2* out_vtx = &temp_points[i2 * 4]; + out_vtx[0].x = points[i2].x + dm_out_x; + out_vtx[0].y = points[i2].y + dm_out_y; + out_vtx[1].x = points[i2].x + dm_in_x; + out_vtx[1].y = points[i2].y + dm_in_y; + out_vtx[2].x = points[i2].x - dm_in_x; + out_vtx[2].y = points[i2].y - dm_in_y; + out_vtx[3].x = points[i2].x - dm_out_x; + out_vtx[3].y = points[i2].y - dm_out_y; + + // Add indexes + _IdxWritePtr[0] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[1] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[2] = (ImDrawIdx)(idx1 + 2); + _IdxWritePtr[3] = (ImDrawIdx)(idx1 + 2); _IdxWritePtr[4] = (ImDrawIdx)(idx2 + 2); _IdxWritePtr[5] = (ImDrawIdx)(idx2 + 1); + _IdxWritePtr[6] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[7] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[8] = (ImDrawIdx)(idx1 + 0); + _IdxWritePtr[9] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[10] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[11] = (ImDrawIdx)(idx2 + 1); + _IdxWritePtr[12] = (ImDrawIdx)(idx2 + 2); _IdxWritePtr[13] = (ImDrawIdx)(idx1 + 2); _IdxWritePtr[14] = (ImDrawIdx)(idx1 + 3); + _IdxWritePtr[15] = (ImDrawIdx)(idx1 + 3); _IdxWritePtr[16] = (ImDrawIdx)(idx2 + 3); _IdxWritePtr[17] = (ImDrawIdx)(idx2 + 2); + _IdxWritePtr += 18; + + idx1 = idx2; + } + + // Add vertices + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = temp_points[i * 4 + 0]; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = col_trans; + _VtxWritePtr[1].pos = temp_points[i * 4 + 1]; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = temp_points[i * 4 + 2]; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = temp_points[i * 4 + 3]; _VtxWritePtr[3].uv = opaque_uv; _VtxWritePtr[3].col = col_trans; + _VtxWritePtr += 4; + } + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // [PATH 4] Non texture-based, Non anti-aliased lines + const int idx_count = count * 6; + const int vtx_count = count * 4; // FIXME-OPT: Not sharing edges + PrimReserve(idx_count, vtx_count); + + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; + const ImVec2& p1 = points[i1]; + const ImVec2& p2 = points[i2]; + + float dx = p2.x - p1.x; + float dy = p2.y - p1.y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + dx *= (thickness * 0.5f); + dy *= (thickness * 0.5f); + + _VtxWritePtr[0].pos.x = p1.x + dy; _VtxWritePtr[0].pos.y = p1.y - dx; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos.x = p2.x + dy; _VtxWritePtr[1].pos.y = p2.y - dx; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos.x = p2.x - dy; _VtxWritePtr[2].pos.y = p2.y + dx; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos.x = p1.x - dy; _VtxWritePtr[3].pos.y = p1.y + dx; _VtxWritePtr[3].uv = opaque_uv; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx + 1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx + 2); + _IdxWritePtr[3] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[4] = (ImDrawIdx)(_VtxCurrentIdx + 2); _IdxWritePtr[5] = (ImDrawIdx)(_VtxCurrentIdx + 3); + _IdxWritePtr += 6; + _VtxCurrentIdx += 4; + } + } +} + +// We intentionally avoid using ImVec2 and its math operators here to reduce cost to a minimum for debug/non-inlined builds. +void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_count, ImU32 col) +{ + if (points_count < 3) + return; + + const ImVec2 uv = _Data->TexUvWhitePixel; + + if (Flags & ImDrawListFlags_AntiAliasedFill) + { + // Anti-aliased Fill + const float AA_SIZE = _FringeScale; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + const int idx_count = (points_count - 2)*3 + points_count * 6; + const int vtx_count = (points_count * 2); + PrimReserve(idx_count, vtx_count); + + // Add indexes for fill + unsigned int vtx_inner_idx = _VtxCurrentIdx; + unsigned int vtx_outer_idx = _VtxCurrentIdx + 1; + for (int i = 2; i < points_count; i++) + { + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + ((i - 1) << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_inner_idx + (i << 1)); + _IdxWritePtr += 3; + } + + // Compute normals + ImVec2* temp_normals = (ImVec2*)alloca(points_count * sizeof(ImVec2)); //-V630 + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + const ImVec2& p0 = points[i0]; + const ImVec2& p1 = points[i1]; + float dx = p1.x - p0.x; + float dy = p1.y - p0.y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + temp_normals[i0].x = dy; + temp_normals[i0].y = -dx; + } + + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + // Average normals + const ImVec2& n0 = temp_normals[i0]; + const ImVec2& n1 = temp_normals[i1]; + float dm_x = (n0.x + n1.x) * 0.5f; + float dm_y = (n0.y + n1.y) * 0.5f; + IM_FIXNORMAL2F(dm_x, dm_y); + dm_x *= AA_SIZE * 0.5f; + dm_y *= AA_SIZE * 0.5f; + + // Add vertices + _VtxWritePtr[0].pos.x = (points[i1].x - dm_x); _VtxWritePtr[0].pos.y = (points[i1].y - dm_y); _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; // Inner + _VtxWritePtr[1].pos.x = (points[i1].x + dm_x); _VtxWritePtr[1].pos.y = (points[i1].y + dm_y); _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col_trans; // Outer + _VtxWritePtr += 2; + + // Add indexes for fringes + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + (i0 << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); + _IdxWritePtr[3] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); _IdxWritePtr[4] = (ImDrawIdx)(vtx_outer_idx + (i1 << 1)); _IdxWritePtr[5] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); + _IdxWritePtr += 6; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // Non Anti-aliased Fill + const int idx_count = (points_count - 2)*3; + const int vtx_count = points_count; + PrimReserve(idx_count, vtx_count); + for (int i = 0; i < vtx_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr++; + } + for (int i = 2; i < points_count; i++) + { + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx + i - 1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx + i); + _IdxWritePtr += 3; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } +} + +void ImDrawList::_PathArcToFastEx(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step) +{ + if (radius <= 0.0f) + { + _Path.push_back(center); + return; + } + + // Calculate arc auto segment step size + if (a_step <= 0) + a_step = IM_DRAWLIST_ARCFAST_SAMPLE_MAX / _CalcCircleAutoSegmentCount(radius); + + // Make sure we never do steps larger than one quarter of the circle + a_step = ImClamp(a_step, 1, IM_DRAWLIST_ARCFAST_TABLE_SIZE / 4); + + const int sample_range = ImAbs(a_max_sample - a_min_sample); + const int a_next_step = a_step; + + int samples = sample_range + 1; + bool extra_max_sample = false; + if (a_step > 1) + { + samples = sample_range / a_step + 1; + const int overstep = sample_range % a_step; + + if (overstep > 0) + { + extra_max_sample = true; + samples++; + + // When we have overstep to avoid awkwardly looking one long line and one tiny one at the end, + // distribute first step range evenly between them by reducing first step size. + if (sample_range > 0) + a_step -= (a_step - overstep) / 2; + } + } + + _Path.resize(_Path.Size + samples); + ImVec2* out_ptr = _Path.Data + (_Path.Size - samples); + + int sample_index = a_min_sample; + if (sample_index < 0 || sample_index >= IM_DRAWLIST_ARCFAST_SAMPLE_MAX) + { + sample_index = sample_index % IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + if (sample_index < 0) + sample_index += IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + } + + if (a_max_sample >= a_min_sample) + { + for (int a = a_min_sample; a <= a_max_sample; a += a_step, sample_index += a_step, a_step = a_next_step) + { + // a_step is clamped to IM_DRAWLIST_ARCFAST_SAMPLE_MAX, so we have guaranteed that it will not wrap over range twice or more + if (sample_index >= IM_DRAWLIST_ARCFAST_SAMPLE_MAX) + sample_index -= IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + + const ImVec2 s = _Data->ArcFastVtx[sample_index]; + out_ptr->x = center.x + s.x * radius; + out_ptr->y = center.y + s.y * radius; + out_ptr++; + } + } + else + { + for (int a = a_min_sample; a >= a_max_sample; a -= a_step, sample_index -= a_step, a_step = a_next_step) + { + // a_step is clamped to IM_DRAWLIST_ARCFAST_SAMPLE_MAX, so we have guaranteed that it will not wrap over range twice or more + if (sample_index < 0) + sample_index += IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + + const ImVec2 s = _Data->ArcFastVtx[sample_index]; + out_ptr->x = center.x + s.x * radius; + out_ptr->y = center.y + s.y * radius; + out_ptr++; + } + } + + if (extra_max_sample) + { + int normalized_max_sample = a_max_sample % IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + if (normalized_max_sample < 0) + normalized_max_sample += IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + + const ImVec2 s = _Data->ArcFastVtx[normalized_max_sample]; + out_ptr->x = center.x + s.x * radius; + out_ptr->y = center.y + s.y * radius; + out_ptr++; + } + + IM_ASSERT_PARANOID(_Path.Data + _Path.Size == out_ptr); +} + +void ImDrawList::_PathArcToN(const ImVec2& center, float radius, float a_min, float a_max, int num_segments) +{ + if (radius <= 0.0f) + { + _Path.push_back(center); + return; + } + + // Note that we are adding a point at both a_min and a_max. + // If you are trying to draw a full closed circle you don't want the overlapping points! + _Path.reserve(_Path.Size + (num_segments + 1)); + for (int i = 0; i <= num_segments; i++) + { + const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min); + _Path.push_back(ImVec2(center.x + ImCos(a) * radius, center.y + ImSin(a) * radius)); + } +} + +// 0: East, 3: South, 6: West, 9: North, 12: East +void ImDrawList::PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12) +{ + if (radius <= 0.0f) + { + _Path.push_back(center); + return; + } + _PathArcToFastEx(center, radius, a_min_of_12 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, a_max_of_12 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 0); +} + +void ImDrawList::PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments) +{ + if (radius <= 0.0f) + { + _Path.push_back(center); + return; + } + + if (num_segments > 0) + { + _PathArcToN(center, radius, a_min, a_max, num_segments); + return; + } + + // Automatic segment count + if (radius <= _Data->ArcFastRadiusCutoff) + { + const bool a_is_reverse = a_max < a_min; + + // We are going to use precomputed values for mid samples. + // Determine first and last sample in lookup table that belong to the arc. + const float a_min_sample_f = IM_DRAWLIST_ARCFAST_SAMPLE_MAX * a_min / (IM_PI * 2.0f); + const float a_max_sample_f = IM_DRAWLIST_ARCFAST_SAMPLE_MAX * a_max / (IM_PI * 2.0f); + + const int a_min_sample = a_is_reverse ? (int)ImFloorSigned(a_min_sample_f) : (int)ImCeil(a_min_sample_f); + const int a_max_sample = a_is_reverse ? (int)ImCeil(a_max_sample_f) : (int)ImFloorSigned(a_max_sample_f); + const int a_mid_samples = a_is_reverse ? ImMax(a_min_sample - a_max_sample, 0) : ImMax(a_max_sample - a_min_sample, 0); + + const float a_min_segment_angle = a_min_sample * IM_PI * 2.0f / IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + const float a_max_segment_angle = a_max_sample * IM_PI * 2.0f / IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + const bool a_emit_start = (a_min_segment_angle - a_min) != 0.0f; + const bool a_emit_end = (a_max - a_max_segment_angle) != 0.0f; + + _Path.reserve(_Path.Size + (a_mid_samples + 1 + (a_emit_start ? 1 : 0) + (a_emit_end ? 1 : 0))); + if (a_emit_start) + _Path.push_back(ImVec2(center.x + ImCos(a_min) * radius, center.y + ImSin(a_min) * radius)); + if (a_mid_samples > 0) + _PathArcToFastEx(center, radius, a_min_sample, a_max_sample, 0); + if (a_emit_end) + _Path.push_back(ImVec2(center.x + ImCos(a_max) * radius, center.y + ImSin(a_max) * radius)); + } + else + { + const float arc_length = ImAbs(a_max - a_min); + const int circle_segment_count = _CalcCircleAutoSegmentCount(radius); + const int arc_segment_count = ImMax((int)ImCeil(circle_segment_count * arc_length / (IM_PI * 2.0f)), (int)(2.0f * IM_PI / arc_length)); + _PathArcToN(center, radius, a_min, a_max, arc_segment_count); + } +} + +ImVec2 ImBezierCubicCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, float t) +{ + float u = 1.0f - t; + float w1 = u * u * u; + float w2 = 3 * u * u * t; + float w3 = 3 * u * t * t; + float w4 = t * t * t; + return ImVec2(w1 * p1.x + w2 * p2.x + w3 * p3.x + w4 * p4.x, w1 * p1.y + w2 * p2.y + w3 * p3.y + w4 * p4.y); +} + +ImVec2 ImBezierQuadraticCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float t) +{ + float u = 1.0f - t; + float w1 = u * u; + float w2 = 2 * u * t; + float w3 = t * t; + return ImVec2(w1 * p1.x + w2 * p2.x + w3 * p3.x, w1 * p1.y + w2 * p2.y + w3 * p3.y); +} + +// Closely mimics ImBezierCubicClosestPointCasteljau() in imgui.cpp +static void PathBezierCubicCurveToCasteljau(ImVector* path, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tess_tol, int level) +{ + float dx = x4 - x1; + float dy = y4 - y1; + float d2 = (x2 - x4) * dy - (y2 - y4) * dx; + float d3 = (x3 - x4) * dy - (y3 - y4) * dx; + d2 = (d2 >= 0) ? d2 : -d2; + d3 = (d3 >= 0) ? d3 : -d3; + if ((d2 + d3) * (d2 + d3) < tess_tol * (dx * dx + dy * dy)) + { + path->push_back(ImVec2(x4, y4)); + } + else if (level < 10) + { + float x12 = (x1 + x2) * 0.5f, y12 = (y1 + y2) * 0.5f; + float x23 = (x2 + x3) * 0.5f, y23 = (y2 + y3) * 0.5f; + float x34 = (x3 + x4) * 0.5f, y34 = (y3 + y4) * 0.5f; + float x123 = (x12 + x23) * 0.5f, y123 = (y12 + y23) * 0.5f; + float x234 = (x23 + x34) * 0.5f, y234 = (y23 + y34) * 0.5f; + float x1234 = (x123 + x234) * 0.5f, y1234 = (y123 + y234) * 0.5f; + PathBezierCubicCurveToCasteljau(path, x1, y1, x12, y12, x123, y123, x1234, y1234, tess_tol, level + 1); + PathBezierCubicCurveToCasteljau(path, x1234, y1234, x234, y234, x34, y34, x4, y4, tess_tol, level + 1); + } +} + +static void PathBezierQuadraticCurveToCasteljau(ImVector* path, float x1, float y1, float x2, float y2, float x3, float y3, float tess_tol, int level) +{ + float dx = x3 - x1, dy = y3 - y1; + float det = (x2 - x3) * dy - (y2 - y3) * dx; + if (det * det * 4.0f < tess_tol * (dx * dx + dy * dy)) + { + path->push_back(ImVec2(x3, y3)); + } + else if (level < 10) + { + float x12 = (x1 + x2) * 0.5f, y12 = (y1 + y2) * 0.5f; + float x23 = (x2 + x3) * 0.5f, y23 = (y2 + y3) * 0.5f; + float x123 = (x12 + x23) * 0.5f, y123 = (y12 + y23) * 0.5f; + PathBezierQuadraticCurveToCasteljau(path, x1, y1, x12, y12, x123, y123, tess_tol, level + 1); + PathBezierQuadraticCurveToCasteljau(path, x123, y123, x23, y23, x3, y3, tess_tol, level + 1); + } +} + +void ImDrawList::PathBezierCubicCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments) +{ + ImVec2 p1 = _Path.back(); + if (num_segments == 0) + { + PathBezierCubicCurveToCasteljau(&_Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, _Data->CurveTessellationTol, 0); // Auto-tessellated + } + else + { + float t_step = 1.0f / (float)num_segments; + for (int i_step = 1; i_step <= num_segments; i_step++) + _Path.push_back(ImBezierCubicCalc(p1, p2, p3, p4, t_step * i_step)); + } +} + +void ImDrawList::PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3, int num_segments) +{ + ImVec2 p1 = _Path.back(); + if (num_segments == 0) + { + PathBezierQuadraticCurveToCasteljau(&_Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, _Data->CurveTessellationTol, 0);// Auto-tessellated + } + else + { + float t_step = 1.0f / (float)num_segments; + for (int i_step = 1; i_step <= num_segments; i_step++) + _Path.push_back(ImBezierQuadraticCalc(p1, p2, p3, t_step * i_step)); + } +} + +IM_STATIC_ASSERT(ImDrawFlags_RoundCornersTopLeft == (1 << 4)); +static inline ImDrawFlags FixRectCornerFlags(ImDrawFlags flags) +{ +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + // Legacy Support for hard coded ~0 (used to be a suggested equivalent to ImDrawCornerFlags_All) + // ~0 --> ImDrawFlags_RoundCornersAll or 0 + if (flags == ~0) + return ImDrawFlags_RoundCornersAll; + + // Legacy Support for hard coded 0x01 to 0x0F (matching 15 out of 16 old flags combinations) + // 0x01 --> ImDrawFlags_RoundCornersTopLeft (VALUE 0x01 OVERLAPS ImDrawFlags_Closed but ImDrawFlags_Closed is never valid in this path!) + // 0x02 --> ImDrawFlags_RoundCornersTopRight + // 0x03 --> ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight + // 0x04 --> ImDrawFlags_RoundCornersBotLeft + // 0x05 --> ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersBotLeft + // ... + // 0x0F --> ImDrawFlags_RoundCornersAll or 0 + // (See all values in ImDrawCornerFlags_) + if (flags >= 0x01 && flags <= 0x0F) + return (flags << 4); + + // We cannot support hard coded 0x00 with 'float rounding > 0.0f' --> replace with ImDrawFlags_RoundCornersNone or use 'float rounding = 0.0f' +#endif + + // If this triggers, please update your code replacing hardcoded values with new ImDrawFlags_RoundCorners* values. + // Note that ImDrawFlags_Closed (== 0x01) is an invalid flag for AddRect(), AddRectFilled(), PathRect() etc... + IM_ASSERT((flags & 0x0F) == 0 && "Misuse of legacy hardcoded ImDrawCornerFlags values!"); + + if ((flags & ImDrawFlags_RoundCornersMask_) == 0) + flags |= ImDrawFlags_RoundCornersAll; + + return flags; +} + +void ImDrawList::PathRect(const ImVec2& a, const ImVec2& b, float rounding, ImDrawFlags flags) +{ + flags = FixRectCornerFlags(flags); + rounding = ImMin(rounding, ImFabs(b.x - a.x) * ( ((flags & ImDrawFlags_RoundCornersTop) == ImDrawFlags_RoundCornersTop) || ((flags & ImDrawFlags_RoundCornersBottom) == ImDrawFlags_RoundCornersBottom) ? 0.5f : 1.0f ) - 1.0f); + rounding = ImMin(rounding, ImFabs(b.y - a.y) * ( ((flags & ImDrawFlags_RoundCornersLeft) == ImDrawFlags_RoundCornersLeft) || ((flags & ImDrawFlags_RoundCornersRight) == ImDrawFlags_RoundCornersRight) ? 0.5f : 1.0f ) - 1.0f); + + if (rounding <= 0.0f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) + { + PathLineTo(a); + PathLineTo(ImVec2(b.x, a.y)); + PathLineTo(b); + PathLineTo(ImVec2(a.x, b.y)); + } + else + { + const float rounding_tl = (flags & ImDrawFlags_RoundCornersTopLeft) ? rounding : 0.0f; + const float rounding_tr = (flags & ImDrawFlags_RoundCornersTopRight) ? rounding : 0.0f; + const float rounding_br = (flags & ImDrawFlags_RoundCornersBottomRight) ? rounding : 0.0f; + const float rounding_bl = (flags & ImDrawFlags_RoundCornersBottomLeft) ? rounding : 0.0f; + PathArcToFast(ImVec2(a.x + rounding_tl, a.y + rounding_tl), rounding_tl, 6, 9); + PathArcToFast(ImVec2(b.x - rounding_tr, a.y + rounding_tr), rounding_tr, 9, 12); + PathArcToFast(ImVec2(b.x - rounding_br, b.y - rounding_br), rounding_br, 0, 3); + PathArcToFast(ImVec2(a.x + rounding_bl, b.y - rounding_bl), rounding_bl, 3, 6); + } +} + +void ImDrawList::AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + PathLineTo(p1 + ImVec2(0.5f, 0.5f)); + PathLineTo(p2 + ImVec2(0.5f, 0.5f)); + PathStroke(col, 0, thickness); +} + +// p_min = upper-left, p_max = lower-right +// Note we don't render 1 pixels sized rectangles properly. +void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + if (Flags & ImDrawListFlags_AntiAliasedLines) + PathRect(p_min + ImVec2(0.50f, 0.50f), p_max - ImVec2(0.50f, 0.50f), rounding, flags); + else + PathRect(p_min + ImVec2(0.50f, 0.50f), p_max - ImVec2(0.49f, 0.49f), rounding, flags); // Better looking lower-right corner and rounded non-AA shapes. + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +void ImDrawList::AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + if (rounding <= 0.0f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) + { + PrimReserve(6, 4); + PrimRect(p_min, p_max, col); + } + else + { + PathRect(p_min, p_max, rounding, flags); + PathFillConvex(col); + } +} + +// p_min = upper-left, p_max = lower-right +void ImDrawList::AddRectFilledMultiColor(const ImVec2& p_min, const ImVec2& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left) +{ + if (((col_upr_left | col_upr_right | col_bot_right | col_bot_left) & IM_COL32_A_MASK) == 0) + return; + + const ImVec2 uv = _Data->TexUvWhitePixel; + PrimReserve(6, 4); + PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 1)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 2)); + PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 2)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 3)); + PrimWriteVtx(p_min, uv, col_upr_left); + PrimWriteVtx(ImVec2(p_max.x, p_min.y), uv, col_upr_right); + PrimWriteVtx(p_max, uv, col_bot_right); + PrimWriteVtx(ImVec2(p_min.x, p_max.y), uv, col_bot_left); +} + +void ImDrawList::AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathLineTo(p2); + PathLineTo(p3); + PathLineTo(p4); + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +void ImDrawList::AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathLineTo(p2); + PathLineTo(p3); + PathLineTo(p4); + PathFillConvex(col); +} + +void ImDrawList::AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathLineTo(p2); + PathLineTo(p3); + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +void ImDrawList::AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathLineTo(p2); + PathLineTo(p3); + PathFillConvex(col); +} + +void ImDrawList::AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0 || radius <= 0.0f) + return; + + if (num_segments <= 0) + { + // Use arc with automatic segment count + _PathArcToFastEx(center, radius - 0.5f, 0, IM_DRAWLIST_ARCFAST_SAMPLE_MAX, 0); + _Path.Size--; + } + else + { + // Explicit segment count (still clamp to avoid drawing insanely tessellated shapes) + num_segments = ImClamp(num_segments, 3, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX); + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1); + } + + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0 || radius <= 0.0f) + return; + + if (num_segments <= 0) + { + // Use arc with automatic segment count + _PathArcToFastEx(center, radius, 0, IM_DRAWLIST_ARCFAST_SAMPLE_MAX, 0); + _Path.Size--; + } + else + { + // Explicit segment count (still clamp to avoid drawing insanely tessellated shapes) + num_segments = ImClamp(num_segments, 3, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX); + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius, 0.0f, a_max, num_segments - 1); + } + + PathFillConvex(col); +} + +// Guaranteed to honor 'num_segments' +void ImDrawList::AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0 || num_segments <= 2) + return; + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1); + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +// Guaranteed to honor 'num_segments' +void ImDrawList::AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0 || num_segments <= 2) + return; + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius, 0.0f, a_max, num_segments - 1); + PathFillConvex(col); +} + +// Cubic Bezier takes 4 controls points +void ImDrawList::AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathBezierCubicCurveTo(p2, p3, p4, num_segments); + PathStroke(col, 0, thickness); +} + +// Quadratic Bezier takes 3 controls points +void ImDrawList::AddBezierQuadratic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathBezierQuadraticCurveTo(p2, p3, num_segments); + PathStroke(col, 0, thickness); +} + +void ImDrawList::AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width, const ImVec4* cpu_fine_clip_rect) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + if (text_end == NULL) + text_end = text_begin + strlen(text_begin); + if (text_begin == text_end) + return; + + // Pull default font/size from the shared ImDrawListSharedData instance + if (font == NULL) + font = _Data->Font; + if (font_size == 0.0f) + font_size = _Data->FontSize; + + IM_ASSERT(font->ContainerAtlas->TexID == _CmdHeader.TextureId); // Use high-level ImGui::PushFont() or low-level ImDrawList::PushTextureId() to change font. + + ImVec4 clip_rect = _CmdHeader.ClipRect; + if (cpu_fine_clip_rect) + { + clip_rect.x = ImMax(clip_rect.x, cpu_fine_clip_rect->x); + clip_rect.y = ImMax(clip_rect.y, cpu_fine_clip_rect->y); + clip_rect.z = ImMin(clip_rect.z, cpu_fine_clip_rect->z); + clip_rect.w = ImMin(clip_rect.w, cpu_fine_clip_rect->w); + } + font->RenderText(this, font_size, pos, col, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip_rect != NULL); +} + +void ImDrawList::AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end) +{ + AddText(NULL, 0.0f, pos, col, text_begin, text_end); +} + +void ImDrawList::AddImage(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + const bool push_texture_id = user_texture_id != _CmdHeader.TextureId; + if (push_texture_id) + PushTextureID(user_texture_id); + + PrimReserve(6, 4); + PrimRectUV(p_min, p_max, uv_min, uv_max, col); + + if (push_texture_id) + PopTextureID(); +} + +void ImDrawList::AddImageQuad(ImTextureID user_texture_id, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1, const ImVec2& uv2, const ImVec2& uv3, const ImVec2& uv4, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + const bool push_texture_id = user_texture_id != _CmdHeader.TextureId; + if (push_texture_id) + PushTextureID(user_texture_id); + + PrimReserve(6, 4); + PrimQuadUV(p1, p2, p3, p4, uv1, uv2, uv3, uv4, col); + + if (push_texture_id) + PopTextureID(); +} + +void ImDrawList::AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + flags = FixRectCornerFlags(flags); + if (rounding <= 0.0f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) + { + AddImage(user_texture_id, p_min, p_max, uv_min, uv_max, col); + return; + } + + const bool push_texture_id = user_texture_id != _CmdHeader.TextureId; + if (push_texture_id) + PushTextureID(user_texture_id); + + int vert_start_idx = VtxBuffer.Size; + PathRect(p_min, p_max, rounding, flags); + PathFillConvex(col); + int vert_end_idx = VtxBuffer.Size; + ImGui::ShadeVertsLinearUV(this, vert_start_idx, vert_end_idx, p_min, p_max, uv_min, uv_max, true); + + if (push_texture_id) + PopTextureID(); +} + + +//----------------------------------------------------------------------------- +// [SECTION] ImDrawListSplitter +//----------------------------------------------------------------------------- +// FIXME: This may be a little confusing, trying to be a little too low-level/optimal instead of just doing vector swap.. +//----------------------------------------------------------------------------- + +void ImDrawListSplitter::ClearFreeMemory() +{ + for (int i = 0; i < _Channels.Size; i++) + { + if (i == _Current) + memset(&_Channels[i], 0, sizeof(_Channels[i])); // Current channel is a copy of CmdBuffer/IdxBuffer, don't destruct again + _Channels[i]._CmdBuffer.clear(); + _Channels[i]._IdxBuffer.clear(); + } + _Current = 0; + _Count = 1; + _Channels.clear(); +} + +void ImDrawListSplitter::Split(ImDrawList* draw_list, int channels_count) +{ + IM_UNUSED(draw_list); + IM_ASSERT(_Current == 0 && _Count <= 1 && "Nested channel splitting is not supported. Please use separate instances of ImDrawListSplitter."); + int old_channels_count = _Channels.Size; + if (old_channels_count < channels_count) + { + _Channels.reserve(channels_count); // Avoid over reserving since this is likely to stay stable + _Channels.resize(channels_count); + } + _Count = channels_count; + + // Channels[] (24/32 bytes each) hold storage that we'll swap with draw_list->_CmdBuffer/_IdxBuffer + // The content of Channels[0] at this point doesn't matter. We clear it to make state tidy in a debugger but we don't strictly need to. + // When we switch to the next channel, we'll copy draw_list->_CmdBuffer/_IdxBuffer into Channels[0] and then Channels[1] into draw_list->CmdBuffer/_IdxBuffer + memset(&_Channels[0], 0, sizeof(ImDrawChannel)); + for (int i = 1; i < channels_count; i++) + { + if (i >= old_channels_count) + { + IM_PLACEMENT_NEW(&_Channels[i]) ImDrawChannel(); + } + else + { + _Channels[i]._CmdBuffer.resize(0); + _Channels[i]._IdxBuffer.resize(0); + } + } +} + +void ImDrawListSplitter::Merge(ImDrawList* draw_list) +{ + // Note that we never use or rely on _Channels.Size because it is merely a buffer that we never shrink back to 0 to keep all sub-buffers ready for use. + if (_Count <= 1) + return; + + SetCurrentChannel(draw_list, 0); + draw_list->_PopUnusedDrawCmd(); + + // Calculate our final buffer sizes. Also fix the incorrect IdxOffset values in each command. + int new_cmd_buffer_count = 0; + int new_idx_buffer_count = 0; + ImDrawCmd* last_cmd = (_Count > 0 && draw_list->CmdBuffer.Size > 0) ? &draw_list->CmdBuffer.back() : NULL; + int idx_offset = last_cmd ? last_cmd->IdxOffset + last_cmd->ElemCount : 0; + for (int i = 1; i < _Count; i++) + { + ImDrawChannel& ch = _Channels[i]; + if (ch._CmdBuffer.Size > 0 && ch._CmdBuffer.back().ElemCount == 0 && ch._CmdBuffer.back().UserCallback == NULL) // Equivalent of PopUnusedDrawCmd() + ch._CmdBuffer.pop_back(); + + if (ch._CmdBuffer.Size > 0 && last_cmd != NULL) + { + // Do not include ImDrawCmd_AreSequentialIdxOffset() in the compare as we rebuild IdxOffset values ourselves. + // Manipulating IdxOffset (e.g. by reordering draw commands like done by RenderDimmedBackgroundBehindWindow()) is not supported within a splitter. + ImDrawCmd* next_cmd = &ch._CmdBuffer[0]; + if (ImDrawCmd_HeaderCompare(last_cmd, next_cmd) == 0 && last_cmd->UserCallback == NULL && next_cmd->UserCallback == NULL) + { + // Merge previous channel last draw command with current channel first draw command if matching. + last_cmd->ElemCount += next_cmd->ElemCount; + idx_offset += next_cmd->ElemCount; + ch._CmdBuffer.erase(ch._CmdBuffer.Data); // FIXME-OPT: Improve for multiple merges. + } + } + if (ch._CmdBuffer.Size > 0) + last_cmd = &ch._CmdBuffer.back(); + new_cmd_buffer_count += ch._CmdBuffer.Size; + new_idx_buffer_count += ch._IdxBuffer.Size; + for (int cmd_n = 0; cmd_n < ch._CmdBuffer.Size; cmd_n++) + { + ch._CmdBuffer.Data[cmd_n].IdxOffset = idx_offset; + idx_offset += ch._CmdBuffer.Data[cmd_n].ElemCount; + } + } + draw_list->CmdBuffer.resize(draw_list->CmdBuffer.Size + new_cmd_buffer_count); + draw_list->IdxBuffer.resize(draw_list->IdxBuffer.Size + new_idx_buffer_count); + + // Write commands and indices in order (they are fairly small structures, we don't copy vertices only indices) + ImDrawCmd* cmd_write = draw_list->CmdBuffer.Data + draw_list->CmdBuffer.Size - new_cmd_buffer_count; + ImDrawIdx* idx_write = draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size - new_idx_buffer_count; + for (int i = 1; i < _Count; i++) + { + ImDrawChannel& ch = _Channels[i]; + if (int sz = ch._CmdBuffer.Size) { memcpy(cmd_write, ch._CmdBuffer.Data, sz * sizeof(ImDrawCmd)); cmd_write += sz; } + if (int sz = ch._IdxBuffer.Size) { memcpy(idx_write, ch._IdxBuffer.Data, sz * sizeof(ImDrawIdx)); idx_write += sz; } + } + draw_list->_IdxWritePtr = idx_write; + + // Ensure there's always a non-callback draw command trailing the command-buffer + if (draw_list->CmdBuffer.Size == 0 || draw_list->CmdBuffer.back().UserCallback != NULL) + draw_list->AddDrawCmd(); + + // If current command is used with different settings we need to add a new command + ImDrawCmd* curr_cmd = &draw_list->CmdBuffer.Data[draw_list->CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount == 0) + ImDrawCmd_HeaderCopy(curr_cmd, &draw_list->_CmdHeader); // Copy ClipRect, TextureId, VtxOffset + else if (ImDrawCmd_HeaderCompare(curr_cmd, &draw_list->_CmdHeader) != 0) + draw_list->AddDrawCmd(); + + _Count = 1; +} + +void ImDrawListSplitter::SetCurrentChannel(ImDrawList* draw_list, int idx) +{ + IM_ASSERT(idx >= 0 && idx < _Count); + if (_Current == idx) + return; + + // Overwrite ImVector (12/16 bytes), four times. This is merely a silly optimization instead of doing .swap() + memcpy(&_Channels.Data[_Current]._CmdBuffer, &draw_list->CmdBuffer, sizeof(draw_list->CmdBuffer)); + memcpy(&_Channels.Data[_Current]._IdxBuffer, &draw_list->IdxBuffer, sizeof(draw_list->IdxBuffer)); + _Current = idx; + memcpy(&draw_list->CmdBuffer, &_Channels.Data[idx]._CmdBuffer, sizeof(draw_list->CmdBuffer)); + memcpy(&draw_list->IdxBuffer, &_Channels.Data[idx]._IdxBuffer, sizeof(draw_list->IdxBuffer)); + draw_list->_IdxWritePtr = draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size; + + // If current command is used with different settings we need to add a new command + ImDrawCmd* curr_cmd = (draw_list->CmdBuffer.Size == 0) ? NULL : &draw_list->CmdBuffer.Data[draw_list->CmdBuffer.Size - 1]; + if (curr_cmd == NULL) + draw_list->AddDrawCmd(); + else if (curr_cmd->ElemCount == 0) + ImDrawCmd_HeaderCopy(curr_cmd, &draw_list->_CmdHeader); // Copy ClipRect, TextureId, VtxOffset + else if (ImDrawCmd_HeaderCompare(curr_cmd, &draw_list->_CmdHeader) != 0) + draw_list->AddDrawCmd(); +} + +//----------------------------------------------------------------------------- +// [SECTION] ImDrawData +//----------------------------------------------------------------------------- + +// For backward compatibility: convert all buffers from indexed to de-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! +void ImDrawData::DeIndexAllBuffers() +{ + ImVector new_vtx_buffer; + TotalVtxCount = TotalIdxCount = 0; + for (int i = 0; i < CmdListsCount; i++) + { + ImDrawList* cmd_list = CmdLists[i]; + if (cmd_list->IdxBuffer.empty()) + continue; + new_vtx_buffer.resize(cmd_list->IdxBuffer.Size); + for (int j = 0; j < cmd_list->IdxBuffer.Size; j++) + new_vtx_buffer[j] = cmd_list->VtxBuffer[cmd_list->IdxBuffer[j]]; + cmd_list->VtxBuffer.swap(new_vtx_buffer); + cmd_list->IdxBuffer.resize(0); + TotalVtxCount += cmd_list->VtxBuffer.Size; + } +} + +// Helper to scale the ClipRect field of each ImDrawCmd. +// Use if your final output buffer is at a different scale than draw_data->DisplaySize, +// or if there is a difference between your window resolution and framebuffer resolution. +void ImDrawData::ScaleClipRects(const ImVec2& fb_scale) +{ + for (int i = 0; i < CmdListsCount; i++) + { + ImDrawList* cmd_list = CmdLists[i]; + for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) + { + ImDrawCmd* cmd = &cmd_list->CmdBuffer[cmd_i]; + cmd->ClipRect = ImVec4(cmd->ClipRect.x * fb_scale.x, cmd->ClipRect.y * fb_scale.y, cmd->ClipRect.z * fb_scale.x, cmd->ClipRect.w * fb_scale.y); + } + } +} + +//----------------------------------------------------------------------------- +// [SECTION] Helpers ShadeVertsXXX functions +//----------------------------------------------------------------------------- + +// Generic linear color gradient, write to RGB fields, leave A untouched. +void ImGui::ShadeVertsLinearColorGradientKeepAlpha(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, ImVec2 gradient_p0, ImVec2 gradient_p1, ImU32 col0, ImU32 col1) +{ + ImVec2 gradient_extent = gradient_p1 - gradient_p0; + float gradient_inv_length2 = 1.0f / ImLengthSqr(gradient_extent); + ImDrawVert* vert_start = draw_list->VtxBuffer.Data + vert_start_idx; + ImDrawVert* vert_end = draw_list->VtxBuffer.Data + vert_end_idx; + const int col0_r = (int)(col0 >> IM_COL32_R_SHIFT) & 0xFF; + const int col0_g = (int)(col0 >> IM_COL32_G_SHIFT) & 0xFF; + const int col0_b = (int)(col0 >> IM_COL32_B_SHIFT) & 0xFF; + const int col_delta_r = ((int)(col1 >> IM_COL32_R_SHIFT) & 0xFF) - col0_r; + const int col_delta_g = ((int)(col1 >> IM_COL32_G_SHIFT) & 0xFF) - col0_g; + const int col_delta_b = ((int)(col1 >> IM_COL32_B_SHIFT) & 0xFF) - col0_b; + for (ImDrawVert* vert = vert_start; vert < vert_end; vert++) + { + float d = ImDot(vert->pos - gradient_p0, gradient_extent); + float t = ImClamp(d * gradient_inv_length2, 0.0f, 1.0f); + int r = (int)(col0_r + col_delta_r * t); + int g = (int)(col0_g + col_delta_g * t); + int b = (int)(col0_b + col_delta_b * t); + vert->col = (r << IM_COL32_R_SHIFT) | (g << IM_COL32_G_SHIFT) | (b << IM_COL32_B_SHIFT) | (vert->col & IM_COL32_A_MASK); + } +} + +// Distribute UV over (a, b) rectangle +void ImGui::ShadeVertsLinearUV(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, bool clamp) +{ + const ImVec2 size = b - a; + const ImVec2 uv_size = uv_b - uv_a; + const ImVec2 scale = ImVec2( + size.x != 0.0f ? (uv_size.x / size.x) : 0.0f, + size.y != 0.0f ? (uv_size.y / size.y) : 0.0f); + + ImDrawVert* vert_start = draw_list->VtxBuffer.Data + vert_start_idx; + ImDrawVert* vert_end = draw_list->VtxBuffer.Data + vert_end_idx; + if (clamp) + { + const ImVec2 min = ImMin(uv_a, uv_b); + const ImVec2 max = ImMax(uv_a, uv_b); + for (ImDrawVert* vertex = vert_start; vertex < vert_end; ++vertex) + vertex->uv = ImClamp(uv_a + ImMul(ImVec2(vertex->pos.x, vertex->pos.y) - a, scale), min, max); + } + else + { + for (ImDrawVert* vertex = vert_start; vertex < vert_end; ++vertex) + vertex->uv = uv_a + ImMul(ImVec2(vertex->pos.x, vertex->pos.y) - a, scale); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] ImFontConfig +//----------------------------------------------------------------------------- + +ImFontConfig::ImFontConfig() +{ + memset(this, 0, sizeof(*this)); + FontDataOwnedByAtlas = true; + OversampleH = 3; // FIXME: 2 may be a better default? + OversampleV = 1; + GlyphMaxAdvanceX = FLT_MAX; + RasterizerMultiply = 1.0f; + EllipsisChar = (ImWchar)-1; +} + +//----------------------------------------------------------------------------- +// [SECTION] ImFontAtlas +//----------------------------------------------------------------------------- + +// A work of art lies ahead! (. = white layer, X = black layer, others are blank) +// The 2x2 white texels on the top left are the ones we'll use everywhere in Dear ImGui to render filled shapes. +// (This is used when io.MouseDrawCursor = true) +const int FONT_ATLAS_DEFAULT_TEX_DATA_W = 122; // Actual texture will be 2 times that + 1 spacing. +const int FONT_ATLAS_DEFAULT_TEX_DATA_H = 27; +static const char FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[FONT_ATLAS_DEFAULT_TEX_DATA_W * FONT_ATLAS_DEFAULT_TEX_DATA_H + 1] = +{ + "..- -XXXXXXX- X - X -XXXXXXX - XXXXXXX- XX - XX XX " + "..- -X.....X- X.X - X.X -X.....X - X.....X- X..X -X..X X..X" + "--- -XXX.XXX- X...X - X...X -X....X - X....X- X..X -X...X X...X" + "X - X.X - X.....X - X.....X -X...X - X...X- X..X - X...X X...X " + "XX - X.X -X.......X- X.......X -X..X.X - X.X..X- X..X - X...X...X " + "X.X - X.X -XXXX.XXXX- XXXX.XXXX -X.X X.X - X.X X.X- X..XXX - X.....X " + "X..X - X.X - X.X - X.X -XX X.X - X.X XX- X..X..XXX - X...X " + "X...X - X.X - X.X - XX X.X XX - X.X - X.X - X..X..X..XX - X.X " + "X....X - X.X - X.X - X.X X.X X.X - X.X - X.X - X..X..X..X.X - X...X " + "X.....X - X.X - X.X - X..X X.X X..X - X.X - X.X -XXX X..X..X..X..X- X.....X " + "X......X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X XX-XX X.X -X..XX........X..X- X...X...X " + "X.......X - X.X - X.X -X.....................X- X.X X.X-X.X X.X -X...X...........X- X...X X...X " + "X........X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X..X-X..X.X - X..............X-X...X X...X" + "X.........X -XXX.XXX- X.X - X..X X.X X..X - X...X-X...X - X.............X-X..X X..X" + "X..........X-X.....X- X.X - X.X X.X X.X - X....X-X....X - X.............X- XX XX " + "X......XXXXX-XXXXXXX- X.X - XX X.X XX - X.....X-X.....X - X............X--------------" + "X...X..X --------- X.X - X.X - XXXXXXX-XXXXXXX - X...........X - " + "X..X X..X - -XXXX.XXXX- XXXX.XXXX ------------------------------------- X..........X - " + "X.X X..X - -X.......X- X.......X - XX XX - - X..........X - " + "XX X..X - - X.....X - X.....X - X.X X.X - - X........X - " + " X..X - - X...X - X...X - X..X X..X - - X........X - " + " XX - - X.X - X.X - X...XXXXXXXXXXXXX...X - - XXXXXXXXXX - " + "------------- - X - X -X.....................X- ------------------- " + " ----------------------------------- X...XXXXXXXXXXXXX...X - " + " - X..X X..X - " + " - X.X X.X - " + " - XX XX - " +}; + +static const ImVec2 FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[ImGuiMouseCursor_COUNT][3] = +{ + // Pos ........ Size ......... Offset ...... + { ImVec2( 0,3), ImVec2(12,19), ImVec2( 0, 0) }, // ImGuiMouseCursor_Arrow + { ImVec2(13,0), ImVec2( 7,16), ImVec2( 1, 8) }, // ImGuiMouseCursor_TextInput + { ImVec2(31,0), ImVec2(23,23), ImVec2(11,11) }, // ImGuiMouseCursor_ResizeAll + { ImVec2(21,0), ImVec2( 9,23), ImVec2( 4,11) }, // ImGuiMouseCursor_ResizeNS + { ImVec2(55,18),ImVec2(23, 9), ImVec2(11, 4) }, // ImGuiMouseCursor_ResizeEW + { ImVec2(73,0), ImVec2(17,17), ImVec2( 8, 8) }, // ImGuiMouseCursor_ResizeNESW + { ImVec2(55,0), ImVec2(17,17), ImVec2( 8, 8) }, // ImGuiMouseCursor_ResizeNWSE + { ImVec2(91,0), ImVec2(17,22), ImVec2( 5, 0) }, // ImGuiMouseCursor_Hand + { ImVec2(109,0),ImVec2(13,15), ImVec2( 6, 7) }, // ImGuiMouseCursor_NotAllowed +}; + +ImFontAtlas::ImFontAtlas() +{ + memset(this, 0, sizeof(*this)); + TexGlyphPadding = 1; + PackIdMouseCursors = PackIdLines = -1; +} + +ImFontAtlas::~ImFontAtlas() +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + Clear(); +} + +void ImFontAtlas::ClearInputData() +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + for (int i = 0; i < ConfigData.Size; i++) + if (ConfigData[i].FontData && ConfigData[i].FontDataOwnedByAtlas) + { + IM_FREE(ConfigData[i].FontData); + ConfigData[i].FontData = NULL; + } + + // When clearing this we lose access to the font name and other information used to build the font. + for (int i = 0; i < Fonts.Size; i++) + if (Fonts[i]->ConfigData >= ConfigData.Data && Fonts[i]->ConfigData < ConfigData.Data + ConfigData.Size) + { + Fonts[i]->ConfigData = NULL; + Fonts[i]->ConfigDataCount = 0; + } + ConfigData.clear(); + CustomRects.clear(); + PackIdMouseCursors = PackIdLines = -1; + // Important: we leave TexReady untouched +} + +void ImFontAtlas::ClearTexData() +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + if (TexPixelsAlpha8) + IM_FREE(TexPixelsAlpha8); + if (TexPixelsRGBA32) + IM_FREE(TexPixelsRGBA32); + TexPixelsAlpha8 = NULL; + TexPixelsRGBA32 = NULL; + TexPixelsUseColors = false; + // Important: we leave TexReady untouched +} + +void ImFontAtlas::ClearFonts() +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + Fonts.clear_delete(); + TexReady = false; +} + +void ImFontAtlas::Clear() +{ + ClearInputData(); + ClearTexData(); + ClearFonts(); +} + +void ImFontAtlas::GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel) +{ + // Build atlas on demand + if (TexPixelsAlpha8 == NULL) + Build(); + + *out_pixels = TexPixelsAlpha8; + if (out_width) *out_width = TexWidth; + if (out_height) *out_height = TexHeight; + if (out_bytes_per_pixel) *out_bytes_per_pixel = 1; +} + +void ImFontAtlas::GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel) +{ + // Convert to RGBA32 format on demand + // Although it is likely to be the most commonly used format, our font rendering is 1 channel / 8 bpp + if (!TexPixelsRGBA32) + { + unsigned char* pixels = NULL; + GetTexDataAsAlpha8(&pixels, NULL, NULL); + if (pixels) + { + TexPixelsRGBA32 = (unsigned int*)IM_ALLOC((size_t)TexWidth * (size_t)TexHeight * 4); + const unsigned char* src = pixels; + unsigned int* dst = TexPixelsRGBA32; + for (int n = TexWidth * TexHeight; n > 0; n--) + *dst++ = IM_COL32(255, 255, 255, (unsigned int)(*src++)); + } + } + + *out_pixels = (unsigned char*)TexPixelsRGBA32; + if (out_width) *out_width = TexWidth; + if (out_height) *out_height = TexHeight; + if (out_bytes_per_pixel) *out_bytes_per_pixel = 4; +} + +ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg) +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + IM_ASSERT(font_cfg->FontData != NULL && font_cfg->FontDataSize > 0); + IM_ASSERT(font_cfg->SizePixels > 0.0f); + + // Create new font + if (!font_cfg->MergeMode) + Fonts.push_back(IM_NEW(ImFont)); + else + IM_ASSERT(!Fonts.empty() && "Cannot use MergeMode for the first font"); // When using MergeMode make sure that a font has already been added before. You can use ImGui::GetIO().Fonts->AddFontDefault() to add the default imgui font. + + ConfigData.push_back(*font_cfg); + ImFontConfig& new_font_cfg = ConfigData.back(); + if (new_font_cfg.DstFont == NULL) + new_font_cfg.DstFont = Fonts.back(); + if (!new_font_cfg.FontDataOwnedByAtlas) + { + new_font_cfg.FontData = IM_ALLOC(new_font_cfg.FontDataSize); + new_font_cfg.FontDataOwnedByAtlas = true; + memcpy(new_font_cfg.FontData, font_cfg->FontData, (size_t)new_font_cfg.FontDataSize); + } + + if (new_font_cfg.DstFont->EllipsisChar == (ImWchar)-1) + new_font_cfg.DstFont->EllipsisChar = font_cfg->EllipsisChar; + + // Invalidate texture + TexReady = false; + ClearTexData(); + return new_font_cfg.DstFont; +} + +// Default font TTF is compressed with stb_compress then base85 encoded (see misc/fonts/binary_to_compressed_c.cpp for encoder) +static unsigned int stb_decompress_length(const unsigned char* input); +static unsigned int stb_decompress(unsigned char* output, const unsigned char* input, unsigned int length); +static const char* GetDefaultCompressedFontDataTTFBase85(); +static unsigned int Decode85Byte(char c) { return c >= '\\' ? c-36 : c-35; } +static void Decode85(const unsigned char* src, unsigned char* dst) +{ + while (*src) + { + unsigned int tmp = Decode85Byte(src[0]) + 85 * (Decode85Byte(src[1]) + 85 * (Decode85Byte(src[2]) + 85 * (Decode85Byte(src[3]) + 85 * Decode85Byte(src[4])))); + dst[0] = ((tmp >> 0) & 0xFF); dst[1] = ((tmp >> 8) & 0xFF); dst[2] = ((tmp >> 16) & 0xFF); dst[3] = ((tmp >> 24) & 0xFF); // We can't assume little-endianness. + src += 5; + dst += 4; + } +} + +// Load embedded ProggyClean.ttf at size 13, disable oversampling +ImFont* ImFontAtlas::AddFontDefault(const ImFontConfig* font_cfg_template) +{ + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + if (!font_cfg_template) + { + font_cfg.OversampleH = font_cfg.OversampleV = 1; + font_cfg.PixelSnapH = true; + } + if (font_cfg.SizePixels <= 0.0f) + font_cfg.SizePixels = 13.0f * 1.0f; + if (font_cfg.Name[0] == '\0') + ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "ProggyClean.ttf, %dpx", (int)font_cfg.SizePixels); + font_cfg.EllipsisChar = (ImWchar)0x0085; + font_cfg.GlyphOffset.y = 1.0f * IM_FLOOR(font_cfg.SizePixels / 13.0f); // Add +1 offset per 13 units + + const char* ttf_compressed_base85 = GetDefaultCompressedFontDataTTFBase85(); + const ImWchar* glyph_ranges = font_cfg.GlyphRanges != NULL ? font_cfg.GlyphRanges : GetGlyphRangesDefault(); + ImFont* font = AddFontFromMemoryCompressedBase85TTF(ttf_compressed_base85, font_cfg.SizePixels, &font_cfg, glyph_ranges); + return font; +} + +ImFont* ImFontAtlas::AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + size_t data_size = 0; + void* data = ImFileLoadToMemory(filename, "rb", &data_size, 0); + if (!data) + { + IM_ASSERT_USER_ERROR(0, "Could not load font file!"); + return NULL; + } + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + if (font_cfg.Name[0] == '\0') + { + // Store a short copy of filename into into the font name for convenience + const char* p; + for (p = filename + strlen(filename); p > filename && p[-1] != '/' && p[-1] != '\\'; p--) {} + ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "%s, %.0fpx", p, size_pixels); + } + return AddFontFromMemoryTTF(data, (int)data_size, size_pixels, &font_cfg, glyph_ranges); +} + +// NB: Transfer ownership of 'ttf_data' to ImFontAtlas, unless font_cfg_template->FontDataOwnedByAtlas == false. Owned TTF buffer will be deleted after Build(). +ImFont* ImFontAtlas::AddFontFromMemoryTTF(void* ttf_data, int ttf_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + IM_ASSERT(font_cfg.FontData == NULL); + font_cfg.FontData = ttf_data; + font_cfg.FontDataSize = ttf_size; + font_cfg.SizePixels = size_pixels > 0.0f ? size_pixels : font_cfg.SizePixels; + if (glyph_ranges) + font_cfg.GlyphRanges = glyph_ranges; + return AddFont(&font_cfg); +} + +ImFont* ImFontAtlas::AddFontFromMemoryCompressedTTF(const void* compressed_ttf_data, int compressed_ttf_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) +{ + const unsigned int buf_decompressed_size = stb_decompress_length((const unsigned char*)compressed_ttf_data); + unsigned char* buf_decompressed_data = (unsigned char*)IM_ALLOC(buf_decompressed_size); + stb_decompress(buf_decompressed_data, (const unsigned char*)compressed_ttf_data, (unsigned int)compressed_ttf_size); + + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + IM_ASSERT(font_cfg.FontData == NULL); + font_cfg.FontDataOwnedByAtlas = true; + return AddFontFromMemoryTTF(buf_decompressed_data, (int)buf_decompressed_size, size_pixels, &font_cfg, glyph_ranges); +} + +ImFont* ImFontAtlas::AddFontFromMemoryCompressedBase85TTF(const char* compressed_ttf_data_base85, float size_pixels, const ImFontConfig* font_cfg, const ImWchar* glyph_ranges) +{ + int compressed_ttf_size = (((int)strlen(compressed_ttf_data_base85) + 4) / 5) * 4; + void* compressed_ttf = IM_ALLOC((size_t)compressed_ttf_size); + Decode85((const unsigned char*)compressed_ttf_data_base85, (unsigned char*)compressed_ttf); + ImFont* font = AddFontFromMemoryCompressedTTF(compressed_ttf, compressed_ttf_size, size_pixels, font_cfg, glyph_ranges); + IM_FREE(compressed_ttf); + return font; +} + +int ImFontAtlas::AddCustomRectRegular(int width, int height) +{ + IM_ASSERT(width > 0 && width <= 0xFFFF); + IM_ASSERT(height > 0 && height <= 0xFFFF); + ImFontAtlasCustomRect r; + r.Width = (unsigned short)width; + r.Height = (unsigned short)height; + CustomRects.push_back(r); + return CustomRects.Size - 1; // Return index +} + +int ImFontAtlas::AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset) +{ +#ifdef IMGUI_USE_WCHAR32 + IM_ASSERT(id <= IM_UNICODE_CODEPOINT_MAX); +#endif + IM_ASSERT(font != NULL); + IM_ASSERT(width > 0 && width <= 0xFFFF); + IM_ASSERT(height > 0 && height <= 0xFFFF); + ImFontAtlasCustomRect r; + r.Width = (unsigned short)width; + r.Height = (unsigned short)height; + r.GlyphID = id; + r.GlyphAdvanceX = advance_x; + r.GlyphOffset = offset; + r.Font = font; + CustomRects.push_back(r); + return CustomRects.Size - 1; // Return index +} + +void ImFontAtlas::CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) const +{ + IM_ASSERT(TexWidth > 0 && TexHeight > 0); // Font atlas needs to be built before we can calculate UV coordinates + IM_ASSERT(rect->IsPacked()); // Make sure the rectangle has been packed + *out_uv_min = ImVec2((float)rect->X * TexUvScale.x, (float)rect->Y * TexUvScale.y); + *out_uv_max = ImVec2((float)(rect->X + rect->Width) * TexUvScale.x, (float)(rect->Y + rect->Height) * TexUvScale.y); +} + +bool ImFontAtlas::GetMouseCursorTexData(ImGuiMouseCursor cursor_type, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]) +{ + if (cursor_type <= ImGuiMouseCursor_None || cursor_type >= ImGuiMouseCursor_COUNT) + return false; + if (Flags & ImFontAtlasFlags_NoMouseCursors) + return false; + + IM_ASSERT(PackIdMouseCursors != -1); + ImFontAtlasCustomRect* r = GetCustomRectByIndex(PackIdMouseCursors); + ImVec2 pos = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][0] + ImVec2((float)r->X, (float)r->Y); + ImVec2 size = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][1]; + *out_size = size; + *out_offset = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][2]; + out_uv_border[0] = (pos) * TexUvScale; + out_uv_border[1] = (pos + size) * TexUvScale; + pos.x += FONT_ATLAS_DEFAULT_TEX_DATA_W + 1; + out_uv_fill[0] = (pos) * TexUvScale; + out_uv_fill[1] = (pos + size) * TexUvScale; + return true; +} + +bool ImFontAtlas::Build() +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + + // Default font is none are specified + if (ConfigData.Size == 0) + AddFontDefault(); + + // Select builder + // - Note that we do not reassign to atlas->FontBuilderIO, since it is likely to point to static data which + // may mess with some hot-reloading schemes. If you need to assign to this (for dynamic selection) AND are + // using a hot-reloading scheme that messes up static data, store your own instance of ImFontBuilderIO somewhere + // and point to it instead of pointing directly to return value of the GetBuilderXXX functions. + const ImFontBuilderIO* builder_io = FontBuilderIO; + if (builder_io == NULL) + { +#ifdef IMGUI_ENABLE_FREETYPE + builder_io = ImGuiFreeType::GetBuilderForFreeType(); +#elif defined(IMGUI_ENABLE_STB_TRUETYPE) + builder_io = ImFontAtlasGetBuilderForStbTruetype(); +#else + IM_ASSERT(0); // Invalid Build function +#endif + } + + // Build + return builder_io->FontBuilder_Build(this); +} + +void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_brighten_factor) +{ + for (unsigned int i = 0; i < 256; i++) + { + unsigned int value = (unsigned int)(i * in_brighten_factor); + out_table[i] = value > 255 ? 255 : (value & 0xFF); + } +} + +void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride) +{ + unsigned char* data = pixels + x + y * stride; + for (int j = h; j > 0; j--, data += stride) + for (int i = 0; i < w; i++) + data[i] = table[data[i]]; +} + +#ifdef IMGUI_ENABLE_STB_TRUETYPE +// Temporary data for one source font (multiple source fonts can be merged into one destination ImFont) +// (C++03 doesn't allow instancing ImVector<> with function-local types so we declare the type here.) +struct ImFontBuildSrcData +{ + stbtt_fontinfo FontInfo; + stbtt_pack_range PackRange; // Hold the list of codepoints to pack (essentially points to Codepoints.Data) + stbrp_rect* Rects; // Rectangle to pack. We first fill in their size and the packer will give us their position. + stbtt_packedchar* PackedChars; // Output glyphs + const ImWchar* SrcRanges; // Ranges as requested by user (user is allowed to request too much, e.g. 0x0020..0xFFFF) + int DstIndex; // Index into atlas->Fonts[] and dst_tmp_array[] + int GlyphsHighest; // Highest requested codepoint + int GlyphsCount; // Glyph count (excluding missing glyphs and glyphs already set by an earlier source font) + ImBitVector GlyphsSet; // Glyph bit map (random access, 1-bit per codepoint. This will be a maximum of 8KB) + ImVector GlyphsList; // Glyph codepoints list (flattened version of GlyphsMap) +}; + +// Temporary data for one destination ImFont* (multiple source fonts can be merged into one destination ImFont) +struct ImFontBuildDstData +{ + int SrcCount; // Number of source fonts targeting this destination font. + int GlyphsHighest; + int GlyphsCount; + ImBitVector GlyphsSet; // This is used to resolve collision when multiple sources are merged into a same destination font. +}; + +static void UnpackBitVectorToFlatIndexList(const ImBitVector* in, ImVector* out) +{ + IM_ASSERT(sizeof(in->Storage.Data[0]) == sizeof(int)); + const ImU32* it_begin = in->Storage.begin(); + const ImU32* it_end = in->Storage.end(); + for (const ImU32* it = it_begin; it < it_end; it++) + if (ImU32 entries_32 = *it) + for (ImU32 bit_n = 0; bit_n < 32; bit_n++) + if (entries_32 & ((ImU32)1 << bit_n)) + out->push_back((int)(((it - it_begin) << 5) + bit_n)); +} + +static bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) +{ + IM_ASSERT(atlas->ConfigData.Size > 0); + + ImFontAtlasBuildInit(atlas); + + // Clear atlas + atlas->TexID = (ImTextureID)NULL; + atlas->TexWidth = atlas->TexHeight = 0; + atlas->TexUvScale = ImVec2(0.0f, 0.0f); + atlas->TexUvWhitePixel = ImVec2(0.0f, 0.0f); + atlas->ClearTexData(); + + // Temporary storage for building + ImVector src_tmp_array; + ImVector dst_tmp_array; + src_tmp_array.resize(atlas->ConfigData.Size); + dst_tmp_array.resize(atlas->Fonts.Size); + memset(src_tmp_array.Data, 0, (size_t)src_tmp_array.size_in_bytes()); + memset(dst_tmp_array.Data, 0, (size_t)dst_tmp_array.size_in_bytes()); + + // 1. Initialize font loading structure, check font data validity + for (int src_i = 0; src_i < atlas->ConfigData.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + ImFontConfig& cfg = atlas->ConfigData[src_i]; + IM_ASSERT(cfg.DstFont && (!cfg.DstFont->IsLoaded() || cfg.DstFont->ContainerAtlas == atlas)); + + // Find index from cfg.DstFont (we allow the user to set cfg.DstFont. Also it makes casual debugging nicer than when storing indices) + src_tmp.DstIndex = -1; + for (int output_i = 0; output_i < atlas->Fonts.Size && src_tmp.DstIndex == -1; output_i++) + if (cfg.DstFont == atlas->Fonts[output_i]) + src_tmp.DstIndex = output_i; + if (src_tmp.DstIndex == -1) + { + IM_ASSERT(src_tmp.DstIndex != -1); // cfg.DstFont not pointing within atlas->Fonts[] array? + return false; + } + // Initialize helper structure for font loading and verify that the TTF/OTF data is correct + const int font_offset = stbtt_GetFontOffsetForIndex((unsigned char*)cfg.FontData, cfg.FontNo); + IM_ASSERT(font_offset >= 0 && "FontData is incorrect, or FontNo cannot be found."); + if (!stbtt_InitFont(&src_tmp.FontInfo, (unsigned char*)cfg.FontData, font_offset)) + return false; + + // Measure highest codepoints + ImFontBuildDstData& dst_tmp = dst_tmp_array[src_tmp.DstIndex]; + src_tmp.SrcRanges = cfg.GlyphRanges ? cfg.GlyphRanges : atlas->GetGlyphRangesDefault(); + for (const ImWchar* src_range = src_tmp.SrcRanges; src_range[0] && src_range[1]; src_range += 2) + src_tmp.GlyphsHighest = ImMax(src_tmp.GlyphsHighest, (int)src_range[1]); + dst_tmp.SrcCount++; + dst_tmp.GlyphsHighest = ImMax(dst_tmp.GlyphsHighest, src_tmp.GlyphsHighest); + } + + // 2. For every requested codepoint, check for their presence in the font data, and handle redundancy or overlaps between source fonts to avoid unused glyphs. + int total_glyphs_count = 0; + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + ImFontBuildDstData& dst_tmp = dst_tmp_array[src_tmp.DstIndex]; + src_tmp.GlyphsSet.Create(src_tmp.GlyphsHighest + 1); + if (dst_tmp.GlyphsSet.Storage.empty()) + dst_tmp.GlyphsSet.Create(dst_tmp.GlyphsHighest + 1); + + for (const ImWchar* src_range = src_tmp.SrcRanges; src_range[0] && src_range[1]; src_range += 2) + for (unsigned int codepoint = src_range[0]; codepoint <= src_range[1]; codepoint++) + { + if (dst_tmp.GlyphsSet.TestBit(codepoint)) // Don't overwrite existing glyphs. We could make this an option for MergeMode (e.g. MergeOverwrite==true) + continue; + if (!stbtt_FindGlyphIndex(&src_tmp.FontInfo, codepoint)) // It is actually in the font? + continue; + + // Add to avail set/counters + src_tmp.GlyphsCount++; + dst_tmp.GlyphsCount++; + src_tmp.GlyphsSet.SetBit(codepoint); + dst_tmp.GlyphsSet.SetBit(codepoint); + total_glyphs_count++; + } + } + + // 3. Unpack our bit map into a flat list (we now have all the Unicode points that we know are requested _and_ available _and_ not overlapping another) + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + src_tmp.GlyphsList.reserve(src_tmp.GlyphsCount); + UnpackBitVectorToFlatIndexList(&src_tmp.GlyphsSet, &src_tmp.GlyphsList); + src_tmp.GlyphsSet.Clear(); + IM_ASSERT(src_tmp.GlyphsList.Size == src_tmp.GlyphsCount); + } + for (int dst_i = 0; dst_i < dst_tmp_array.Size; dst_i++) + dst_tmp_array[dst_i].GlyphsSet.Clear(); + dst_tmp_array.clear(); + + // Allocate packing character data and flag packed characters buffer as non-packed (x0=y0=x1=y1=0) + // (We technically don't need to zero-clear buf_rects, but let's do it for the sake of sanity) + ImVector buf_rects; + ImVector buf_packedchars; + buf_rects.resize(total_glyphs_count); + buf_packedchars.resize(total_glyphs_count); + memset(buf_rects.Data, 0, (size_t)buf_rects.size_in_bytes()); + memset(buf_packedchars.Data, 0, (size_t)buf_packedchars.size_in_bytes()); + + // 4. Gather glyphs sizes so we can pack them in our virtual canvas. + int total_surface = 0; + int buf_rects_out_n = 0; + int buf_packedchars_out_n = 0; + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + if (src_tmp.GlyphsCount == 0) + continue; + + src_tmp.Rects = &buf_rects[buf_rects_out_n]; + src_tmp.PackedChars = &buf_packedchars[buf_packedchars_out_n]; + buf_rects_out_n += src_tmp.GlyphsCount; + buf_packedchars_out_n += src_tmp.GlyphsCount; + + // Convert our ranges in the format stb_truetype wants + ImFontConfig& cfg = atlas->ConfigData[src_i]; + src_tmp.PackRange.font_size = cfg.SizePixels; + src_tmp.PackRange.first_unicode_codepoint_in_range = 0; + src_tmp.PackRange.array_of_unicode_codepoints = src_tmp.GlyphsList.Data; + src_tmp.PackRange.num_chars = src_tmp.GlyphsList.Size; + src_tmp.PackRange.chardata_for_range = src_tmp.PackedChars; + src_tmp.PackRange.h_oversample = (unsigned char)cfg.OversampleH; + src_tmp.PackRange.v_oversample = (unsigned char)cfg.OversampleV; + + // Gather the sizes of all rectangles we will need to pack (this loop is based on stbtt_PackFontRangesGatherRects) + const float scale = (cfg.SizePixels > 0) ? stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, cfg.SizePixels) : stbtt_ScaleForMappingEmToPixels(&src_tmp.FontInfo, -cfg.SizePixels); + const int padding = atlas->TexGlyphPadding; + for (int glyph_i = 0; glyph_i < src_tmp.GlyphsList.Size; glyph_i++) + { + int x0, y0, x1, y1; + const int glyph_index_in_font = stbtt_FindGlyphIndex(&src_tmp.FontInfo, src_tmp.GlyphsList[glyph_i]); + IM_ASSERT(glyph_index_in_font != 0); + stbtt_GetGlyphBitmapBoxSubpixel(&src_tmp.FontInfo, glyph_index_in_font, scale * cfg.OversampleH, scale * cfg.OversampleV, 0, 0, &x0, &y0, &x1, &y1); + src_tmp.Rects[glyph_i].w = (stbrp_coord)(x1 - x0 + padding + cfg.OversampleH - 1); + src_tmp.Rects[glyph_i].h = (stbrp_coord)(y1 - y0 + padding + cfg.OversampleV - 1); + total_surface += src_tmp.Rects[glyph_i].w * src_tmp.Rects[glyph_i].h; + } + } + + // We need a width for the skyline algorithm, any width! + // The exact width doesn't really matter much, but some API/GPU have texture size limitations and increasing width can decrease height. + // User can override TexDesiredWidth and TexGlyphPadding if they wish, otherwise we use a simple heuristic to select the width based on expected surface. + const int surface_sqrt = (int)ImSqrt((float)total_surface) + 1; + atlas->TexHeight = 0; + if (atlas->TexDesiredWidth > 0) + atlas->TexWidth = atlas->TexDesiredWidth; + else + atlas->TexWidth = (surface_sqrt >= 4096 * 0.7f) ? 4096 : (surface_sqrt >= 2048 * 0.7f) ? 2048 : (surface_sqrt >= 1024 * 0.7f) ? 1024 : 512; + + // 5. Start packing + // Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values). + const int TEX_HEIGHT_MAX = 1024 * 32; + stbtt_pack_context spc = {}; + stbtt_PackBegin(&spc, NULL, atlas->TexWidth, TEX_HEIGHT_MAX, 0, atlas->TexGlyphPadding, NULL); + ImFontAtlasBuildPackCustomRects(atlas, spc.pack_info); + + // 6. Pack each source font. No rendering yet, we are working with rectangles in an infinitely tall texture at this point. + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + if (src_tmp.GlyphsCount == 0) + continue; + + stbrp_pack_rects((stbrp_context*)spc.pack_info, src_tmp.Rects, src_tmp.GlyphsCount); + + // Extend texture height and mark missing glyphs as non-packed so we won't render them. + // FIXME: We are not handling packing failure here (would happen if we got off TEX_HEIGHT_MAX or if a single if larger than TexWidth?) + for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++) + if (src_tmp.Rects[glyph_i].was_packed) + atlas->TexHeight = ImMax(atlas->TexHeight, src_tmp.Rects[glyph_i].y + src_tmp.Rects[glyph_i].h); + } + + // 7. Allocate texture + atlas->TexHeight = (atlas->Flags & ImFontAtlasFlags_NoPowerOfTwoHeight) ? (atlas->TexHeight + 1) : ImUpperPowerOfTwo(atlas->TexHeight); + atlas->TexUvScale = ImVec2(1.0f / atlas->TexWidth, 1.0f / atlas->TexHeight); + atlas->TexPixelsAlpha8 = (unsigned char*)IM_ALLOC(atlas->TexWidth * atlas->TexHeight); + memset(atlas->TexPixelsAlpha8, 0, atlas->TexWidth * atlas->TexHeight); + spc.pixels = atlas->TexPixelsAlpha8; + spc.height = atlas->TexHeight; + + // 8. Render/rasterize font characters into the texture + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontConfig& cfg = atlas->ConfigData[src_i]; + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + if (src_tmp.GlyphsCount == 0) + continue; + + stbtt_PackFontRangesRenderIntoRects(&spc, &src_tmp.FontInfo, &src_tmp.PackRange, 1, src_tmp.Rects); + + // Apply multiply operator + if (cfg.RasterizerMultiply != 1.0f) + { + unsigned char multiply_table[256]; + ImFontAtlasBuildMultiplyCalcLookupTable(multiply_table, cfg.RasterizerMultiply); + stbrp_rect* r = &src_tmp.Rects[0]; + for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++, r++) + if (r->was_packed) + ImFontAtlasBuildMultiplyRectAlpha8(multiply_table, atlas->TexPixelsAlpha8, r->x, r->y, r->w, r->h, atlas->TexWidth * 1); + } + src_tmp.Rects = NULL; + } + + // End packing + stbtt_PackEnd(&spc); + buf_rects.clear(); + + // 9. Setup ImFont and glyphs for runtime + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + if (src_tmp.GlyphsCount == 0) + continue; + + // When merging fonts with MergeMode=true: + // - We can have multiple input fonts writing into a same destination font. + // - dst_font->ConfigData is != from cfg which is our source configuration. + ImFontConfig& cfg = atlas->ConfigData[src_i]; + ImFont* dst_font = cfg.DstFont; + + const float font_scale = stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, cfg.SizePixels); + int unscaled_ascent, unscaled_descent, unscaled_line_gap; + stbtt_GetFontVMetrics(&src_tmp.FontInfo, &unscaled_ascent, &unscaled_descent, &unscaled_line_gap); + + const float ascent = ImFloor(unscaled_ascent * font_scale + ((unscaled_ascent > 0.0f) ? +1 : -1)); + const float descent = ImFloor(unscaled_descent * font_scale + ((unscaled_descent > 0.0f) ? +1 : -1)); + ImFontAtlasBuildSetupFont(atlas, dst_font, &cfg, ascent, descent); + const float font_off_x = cfg.GlyphOffset.x; + const float font_off_y = cfg.GlyphOffset.y + IM_ROUND(dst_font->Ascent); + + for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++) + { + // Register glyph + const int codepoint = src_tmp.GlyphsList[glyph_i]; + const stbtt_packedchar& pc = src_tmp.PackedChars[glyph_i]; + stbtt_aligned_quad q; + float unused_x = 0.0f, unused_y = 0.0f; + stbtt_GetPackedQuad(src_tmp.PackedChars, atlas->TexWidth, atlas->TexHeight, glyph_i, &unused_x, &unused_y, &q, 0); + dst_font->AddGlyph(&cfg, (ImWchar)codepoint, q.x0 + font_off_x, q.y0 + font_off_y, q.x1 + font_off_x, q.y1 + font_off_y, q.s0, q.t0, q.s1, q.t1, pc.xadvance); + } + } + + // Cleanup + src_tmp_array.clear_destruct(); + + ImFontAtlasBuildFinish(atlas); + return true; +} + +const ImFontBuilderIO* ImFontAtlasGetBuilderForStbTruetype() +{ + static ImFontBuilderIO io; + io.FontBuilder_Build = ImFontAtlasBuildWithStbTruetype; + return &io; +} + +#endif // IMGUI_ENABLE_STB_TRUETYPE + +void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent) +{ + if (!font_config->MergeMode) + { + font->ClearOutputData(); + font->FontSize = font_config->SizePixels; + font->ConfigData = font_config; + font->ConfigDataCount = 0; + font->ContainerAtlas = atlas; + font->Ascent = ascent; + font->Descent = descent; + } + font->ConfigDataCount++; +} + +void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque) +{ + stbrp_context* pack_context = (stbrp_context*)stbrp_context_opaque; + IM_ASSERT(pack_context != NULL); + + ImVector& user_rects = atlas->CustomRects; + IM_ASSERT(user_rects.Size >= 1); // We expect at least the default custom rects to be registered, else something went wrong. + + ImVector pack_rects; + pack_rects.resize(user_rects.Size); + memset(pack_rects.Data, 0, (size_t)pack_rects.size_in_bytes()); + for (int i = 0; i < user_rects.Size; i++) + { + pack_rects[i].w = user_rects[i].Width; + pack_rects[i].h = user_rects[i].Height; + } + stbrp_pack_rects(pack_context, &pack_rects[0], pack_rects.Size); + for (int i = 0; i < pack_rects.Size; i++) + if (pack_rects[i].was_packed) + { + user_rects[i].X = pack_rects[i].x; + user_rects[i].Y = pack_rects[i].y; + IM_ASSERT(pack_rects[i].w == user_rects[i].Width && pack_rects[i].h == user_rects[i].Height); + atlas->TexHeight = ImMax(atlas->TexHeight, pack_rects[i].y + pack_rects[i].h); + } +} + +void ImFontAtlasBuildRender8bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned char in_marker_pixel_value) +{ + IM_ASSERT(x >= 0 && x + w <= atlas->TexWidth); + IM_ASSERT(y >= 0 && y + h <= atlas->TexHeight); + unsigned char* out_pixel = atlas->TexPixelsAlpha8 + x + (y * atlas->TexWidth); + for (int off_y = 0; off_y < h; off_y++, out_pixel += atlas->TexWidth, in_str += w) + for (int off_x = 0; off_x < w; off_x++) + out_pixel[off_x] = (in_str[off_x] == in_marker_char) ? in_marker_pixel_value : 0x00; +} + +void ImFontAtlasBuildRender32bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned int in_marker_pixel_value) +{ + IM_ASSERT(x >= 0 && x + w <= atlas->TexWidth); + IM_ASSERT(y >= 0 && y + h <= atlas->TexHeight); + unsigned int* out_pixel = atlas->TexPixelsRGBA32 + x + (y * atlas->TexWidth); + for (int off_y = 0; off_y < h; off_y++, out_pixel += atlas->TexWidth, in_str += w) + for (int off_x = 0; off_x < w; off_x++) + out_pixel[off_x] = (in_str[off_x] == in_marker_char) ? in_marker_pixel_value : IM_COL32_BLACK_TRANS; +} + +static void ImFontAtlasBuildRenderDefaultTexData(ImFontAtlas* atlas) +{ + ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(atlas->PackIdMouseCursors); + IM_ASSERT(r->IsPacked()); + + const int w = atlas->TexWidth; + if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) + { + // Render/copy pixels + IM_ASSERT(r->Width == FONT_ATLAS_DEFAULT_TEX_DATA_W * 2 + 1 && r->Height == FONT_ATLAS_DEFAULT_TEX_DATA_H); + const int x_for_white = r->X; + const int x_for_black = r->X + FONT_ATLAS_DEFAULT_TEX_DATA_W + 1; + if (atlas->TexPixelsAlpha8 != NULL) + { + ImFontAtlasBuildRender8bppRectFromString(atlas, x_for_white, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, '.', 0xFF); + ImFontAtlasBuildRender8bppRectFromString(atlas, x_for_black, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, 'X', 0xFF); + } + else + { + ImFontAtlasBuildRender32bppRectFromString(atlas, x_for_white, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, '.', IM_COL32_WHITE); + ImFontAtlasBuildRender32bppRectFromString(atlas, x_for_black, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, 'X', IM_COL32_WHITE); + } + } + else + { + // Render 4 white pixels + IM_ASSERT(r->Width == 2 && r->Height == 2); + const int offset = (int)r->X + (int)r->Y * w; + if (atlas->TexPixelsAlpha8 != NULL) + { + atlas->TexPixelsAlpha8[offset] = atlas->TexPixelsAlpha8[offset + 1] = atlas->TexPixelsAlpha8[offset + w] = atlas->TexPixelsAlpha8[offset + w + 1] = 0xFF; + } + else + { + atlas->TexPixelsRGBA32[offset] = atlas->TexPixelsRGBA32[offset + 1] = atlas->TexPixelsRGBA32[offset + w] = atlas->TexPixelsRGBA32[offset + w + 1] = IM_COL32_WHITE; + } + } + atlas->TexUvWhitePixel = ImVec2((r->X + 0.5f) * atlas->TexUvScale.x, (r->Y + 0.5f) * atlas->TexUvScale.y); +} + +static void ImFontAtlasBuildRenderLinesTexData(ImFontAtlas* atlas) +{ + if (atlas->Flags & ImFontAtlasFlags_NoBakedLines) + return; + + // This generates a triangular shape in the texture, with the various line widths stacked on top of each other to allow interpolation between them + ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(atlas->PackIdLines); + IM_ASSERT(r->IsPacked()); + for (unsigned int n = 0; n < IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1; n++) // +1 because of the zero-width row + { + // Each line consists of at least two empty pixels at the ends, with a line of solid pixels in the middle + unsigned int y = n; + unsigned int line_width = n; + unsigned int pad_left = (r->Width - line_width) / 2; + unsigned int pad_right = r->Width - (pad_left + line_width); + + // Write each slice + IM_ASSERT(pad_left + line_width + pad_right == r->Width && y < r->Height); // Make sure we're inside the texture bounds before we start writing pixels + if (atlas->TexPixelsAlpha8 != NULL) + { + unsigned char* write_ptr = &atlas->TexPixelsAlpha8[r->X + ((r->Y + y) * atlas->TexWidth)]; + for (unsigned int i = 0; i < pad_left; i++) + *(write_ptr + i) = 0x00; + + for (unsigned int i = 0; i < line_width; i++) + *(write_ptr + pad_left + i) = 0xFF; + + for (unsigned int i = 0; i < pad_right; i++) + *(write_ptr + pad_left + line_width + i) = 0x00; + } + else + { + unsigned int* write_ptr = &atlas->TexPixelsRGBA32[r->X + ((r->Y + y) * atlas->TexWidth)]; + for (unsigned int i = 0; i < pad_left; i++) + *(write_ptr + i) = IM_COL32_BLACK_TRANS; + + for (unsigned int i = 0; i < line_width; i++) + *(write_ptr + pad_left + i) = IM_COL32_WHITE; + + for (unsigned int i = 0; i < pad_right; i++) + *(write_ptr + pad_left + line_width + i) = IM_COL32_BLACK_TRANS; + } + + // Calculate UVs for this line + ImVec2 uv0 = ImVec2((float)(r->X + pad_left - 1), (float)(r->Y + y)) * atlas->TexUvScale; + ImVec2 uv1 = ImVec2((float)(r->X + pad_left + line_width + 1), (float)(r->Y + y + 1)) * atlas->TexUvScale; + float half_v = (uv0.y + uv1.y) * 0.5f; // Calculate a constant V in the middle of the row to avoid sampling artifacts + atlas->TexUvLines[n] = ImVec4(uv0.x, half_v, uv1.x, half_v); + } +} + +// Note: this is called / shared by both the stb_truetype and the FreeType builder +void ImFontAtlasBuildInit(ImFontAtlas* atlas) +{ + // Register texture region for mouse cursors or standard white pixels + if (atlas->PackIdMouseCursors < 0) + { + if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) + atlas->PackIdMouseCursors = atlas->AddCustomRectRegular(FONT_ATLAS_DEFAULT_TEX_DATA_W * 2 + 1, FONT_ATLAS_DEFAULT_TEX_DATA_H); + else + atlas->PackIdMouseCursors = atlas->AddCustomRectRegular(2, 2); + } + + // Register texture region for thick lines + // The +2 here is to give space for the end caps, whilst height +1 is to accommodate the fact we have a zero-width row + if (atlas->PackIdLines < 0) + { + if (!(atlas->Flags & ImFontAtlasFlags_NoBakedLines)) + atlas->PackIdLines = atlas->AddCustomRectRegular(IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 2, IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1); + } +} + +// This is called/shared by both the stb_truetype and the FreeType builder. +void ImFontAtlasBuildFinish(ImFontAtlas* atlas) +{ + // Render into our custom data blocks + IM_ASSERT(atlas->TexPixelsAlpha8 != NULL || atlas->TexPixelsRGBA32 != NULL); + ImFontAtlasBuildRenderDefaultTexData(atlas); + ImFontAtlasBuildRenderLinesTexData(atlas); + + // Register custom rectangle glyphs + for (int i = 0; i < atlas->CustomRects.Size; i++) + { + const ImFontAtlasCustomRect* r = &atlas->CustomRects[i]; + if (r->Font == NULL || r->GlyphID == 0) + continue; + + // Will ignore ImFontConfig settings: GlyphMinAdvanceX, GlyphMinAdvanceY, GlyphExtraSpacing, PixelSnapH + IM_ASSERT(r->Font->ContainerAtlas == atlas); + ImVec2 uv0, uv1; + atlas->CalcCustomRectUV(r, &uv0, &uv1); + r->Font->AddGlyph(NULL, (ImWchar)r->GlyphID, r->GlyphOffset.x, r->GlyphOffset.y, r->GlyphOffset.x + r->Width, r->GlyphOffset.y + r->Height, uv0.x, uv0.y, uv1.x, uv1.y, r->GlyphAdvanceX); + } + + // Build all fonts lookup tables + for (int i = 0; i < atlas->Fonts.Size; i++) + if (atlas->Fonts[i]->DirtyLookupTables) + atlas->Fonts[i]->BuildLookupTable(); + + atlas->TexReady = true; +} + +// Retrieve list of range (2 int per range, values are inclusive) +const ImWchar* ImFontAtlas::GetGlyphRangesDefault() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesKorean() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x3131, 0x3163, // Korean alphabets + 0xAC00, 0xD7A3, // Korean characters + 0xFFFD, 0xFFFD, // Invalid + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesChineseFull() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x2000, 0x206F, // General Punctuation + 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana + 0x31F0, 0x31FF, // Katakana Phonetic Extensions + 0xFF00, 0xFFEF, // Half-width characters + 0xFFFD, 0xFFFD, // Invalid + 0x4e00, 0x9FAF, // CJK Ideograms + 0, + }; + return &ranges[0]; +} + +static void UnpackAccumulativeOffsetsIntoRanges(int base_codepoint, const short* accumulative_offsets, int accumulative_offsets_count, ImWchar* out_ranges) +{ + for (int n = 0; n < accumulative_offsets_count; n++, out_ranges += 2) + { + out_ranges[0] = out_ranges[1] = (ImWchar)(base_codepoint + accumulative_offsets[n]); + base_codepoint += accumulative_offsets[n]; + } + out_ranges[0] = 0; +} + +//------------------------------------------------------------------------- +// [SECTION] ImFontAtlas glyph ranges helpers +//------------------------------------------------------------------------- + +const ImWchar* ImFontAtlas::GetGlyphRangesChineseSimplifiedCommon() +{ + // Store 2500 regularly used characters for Simplified Chinese. + // Sourced from https://zh.wiktionary.org/wiki/%E9%99%84%E5%BD%95:%E7%8E%B0%E4%BB%A3%E6%B1%89%E8%AF%AD%E5%B8%B8%E7%94%A8%E5%AD%97%E8%A1%A8 + // This table covers 97.97% of all characters used during the month in July, 1987. + // You can use ImFontGlyphRangesBuilder to create your own ranges derived from this, by merging existing ranges or adding new characters. + // (Stored as accumulative offsets from the initial unicode codepoint 0x4E00. This encoding is designed to helps us compact the source code size.) + static const short accumulative_offsets_from_0x4E00[] = + { + 0,1,2,4,1,1,1,1,2,1,3,2,1,2,2,1,1,1,1,1,5,2,1,2,3,3,3,2,2,4,1,1,1,2,1,5,2,3,1,2,1,2,1,1,2,1,1,2,2,1,4,1,1,1,1,5,10,1,2,19,2,1,2,1,2,1,2,1,2, + 1,5,1,6,3,2,1,2,2,1,1,1,4,8,5,1,1,4,1,1,3,1,2,1,5,1,2,1,1,1,10,1,1,5,2,4,6,1,4,2,2,2,12,2,1,1,6,1,1,1,4,1,1,4,6,5,1,4,2,2,4,10,7,1,1,4,2,4, + 2,1,4,3,6,10,12,5,7,2,14,2,9,1,1,6,7,10,4,7,13,1,5,4,8,4,1,1,2,28,5,6,1,1,5,2,5,20,2,2,9,8,11,2,9,17,1,8,6,8,27,4,6,9,20,11,27,6,68,2,2,1,1, + 1,2,1,2,2,7,6,11,3,3,1,1,3,1,2,1,1,1,1,1,3,1,1,8,3,4,1,5,7,2,1,4,4,8,4,2,1,2,1,1,4,5,6,3,6,2,12,3,1,3,9,2,4,3,4,1,5,3,3,1,3,7,1,5,1,1,1,1,2, + 3,4,5,2,3,2,6,1,1,2,1,7,1,7,3,4,5,15,2,2,1,5,3,22,19,2,1,1,1,1,2,5,1,1,1,6,1,1,12,8,2,9,18,22,4,1,1,5,1,16,1,2,7,10,15,1,1,6,2,4,1,2,4,1,6, + 1,1,3,2,4,1,6,4,5,1,2,1,1,2,1,10,3,1,3,2,1,9,3,2,5,7,2,19,4,3,6,1,1,1,1,1,4,3,2,1,1,1,2,5,3,1,1,1,2,2,1,1,2,1,1,2,1,3,1,1,1,3,7,1,4,1,1,2,1, + 1,2,1,2,4,4,3,8,1,1,1,2,1,3,5,1,3,1,3,4,6,2,2,14,4,6,6,11,9,1,15,3,1,28,5,2,5,5,3,1,3,4,5,4,6,14,3,2,3,5,21,2,7,20,10,1,2,19,2,4,28,28,2,3, + 2,1,14,4,1,26,28,42,12,40,3,52,79,5,14,17,3,2,2,11,3,4,6,3,1,8,2,23,4,5,8,10,4,2,7,3,5,1,1,6,3,1,2,2,2,5,28,1,1,7,7,20,5,3,29,3,17,26,1,8,4, + 27,3,6,11,23,5,3,4,6,13,24,16,6,5,10,25,35,7,3,2,3,3,14,3,6,2,6,1,4,2,3,8,2,1,1,3,3,3,4,1,1,13,2,2,4,5,2,1,14,14,1,2,2,1,4,5,2,3,1,14,3,12, + 3,17,2,16,5,1,2,1,8,9,3,19,4,2,2,4,17,25,21,20,28,75,1,10,29,103,4,1,2,1,1,4,2,4,1,2,3,24,2,2,2,1,1,2,1,3,8,1,1,1,2,1,1,3,1,1,1,6,1,5,3,1,1, + 1,3,4,1,1,5,2,1,5,6,13,9,16,1,1,1,1,3,2,3,2,4,5,2,5,2,2,3,7,13,7,2,2,1,1,1,1,2,3,3,2,1,6,4,9,2,1,14,2,14,2,1,18,3,4,14,4,11,41,15,23,15,23, + 176,1,3,4,1,1,1,1,5,3,1,2,3,7,3,1,1,2,1,2,4,4,6,2,4,1,9,7,1,10,5,8,16,29,1,1,2,2,3,1,3,5,2,4,5,4,1,1,2,2,3,3,7,1,6,10,1,17,1,44,4,6,2,1,1,6, + 5,4,2,10,1,6,9,2,8,1,24,1,2,13,7,8,8,2,1,4,1,3,1,3,3,5,2,5,10,9,4,9,12,2,1,6,1,10,1,1,7,7,4,10,8,3,1,13,4,3,1,6,1,3,5,2,1,2,17,16,5,2,16,6, + 1,4,2,1,3,3,6,8,5,11,11,1,3,3,2,4,6,10,9,5,7,4,7,4,7,1,1,4,2,1,3,6,8,7,1,6,11,5,5,3,24,9,4,2,7,13,5,1,8,82,16,61,1,1,1,4,2,2,16,10,3,8,1,1, + 6,4,2,1,3,1,1,1,4,3,8,4,2,2,1,1,1,1,1,6,3,5,1,1,4,6,9,2,1,1,1,2,1,7,2,1,6,1,5,4,4,3,1,8,1,3,3,1,3,2,2,2,2,3,1,6,1,2,1,2,1,3,7,1,8,2,1,2,1,5, + 2,5,3,5,10,1,2,1,1,3,2,5,11,3,9,3,5,1,1,5,9,1,2,1,5,7,9,9,8,1,3,3,3,6,8,2,3,2,1,1,32,6,1,2,15,9,3,7,13,1,3,10,13,2,14,1,13,10,2,1,3,10,4,15, + 2,15,15,10,1,3,9,6,9,32,25,26,47,7,3,2,3,1,6,3,4,3,2,8,5,4,1,9,4,2,2,19,10,6,2,3,8,1,2,2,4,2,1,9,4,4,4,6,4,8,9,2,3,1,1,1,1,3,5,5,1,3,8,4,6, + 2,1,4,12,1,5,3,7,13,2,5,8,1,6,1,2,5,14,6,1,5,2,4,8,15,5,1,23,6,62,2,10,1,1,8,1,2,2,10,4,2,2,9,2,1,1,3,2,3,1,5,3,3,2,1,3,8,1,1,1,11,3,1,1,4, + 3,7,1,14,1,2,3,12,5,2,5,1,6,7,5,7,14,11,1,3,1,8,9,12,2,1,11,8,4,4,2,6,10,9,13,1,1,3,1,5,1,3,2,4,4,1,18,2,3,14,11,4,29,4,2,7,1,3,13,9,2,2,5, + 3,5,20,7,16,8,5,72,34,6,4,22,12,12,28,45,36,9,7,39,9,191,1,1,1,4,11,8,4,9,2,3,22,1,1,1,1,4,17,1,7,7,1,11,31,10,2,4,8,2,3,2,1,4,2,16,4,32,2, + 3,19,13,4,9,1,5,2,14,8,1,1,3,6,19,6,5,1,16,6,2,10,8,5,1,2,3,1,5,5,1,11,6,6,1,3,3,2,6,3,8,1,1,4,10,7,5,7,7,5,8,9,2,1,3,4,1,1,3,1,3,3,2,6,16, + 1,4,6,3,1,10,6,1,3,15,2,9,2,10,25,13,9,16,6,2,2,10,11,4,3,9,1,2,6,6,5,4,30,40,1,10,7,12,14,33,6,3,6,7,3,1,3,1,11,14,4,9,5,12,11,49,18,51,31, + 140,31,2,2,1,5,1,8,1,10,1,4,4,3,24,1,10,1,3,6,6,16,3,4,5,2,1,4,2,57,10,6,22,2,22,3,7,22,6,10,11,36,18,16,33,36,2,5,5,1,1,1,4,10,1,4,13,2,7, + 5,2,9,3,4,1,7,43,3,7,3,9,14,7,9,1,11,1,1,3,7,4,18,13,1,14,1,3,6,10,73,2,2,30,6,1,11,18,19,13,22,3,46,42,37,89,7,3,16,34,2,2,3,9,1,7,1,1,1,2, + 2,4,10,7,3,10,3,9,5,28,9,2,6,13,7,3,1,3,10,2,7,2,11,3,6,21,54,85,2,1,4,2,2,1,39,3,21,2,2,5,1,1,1,4,1,1,3,4,15,1,3,2,4,4,2,3,8,2,20,1,8,7,13, + 4,1,26,6,2,9,34,4,21,52,10,4,4,1,5,12,2,11,1,7,2,30,12,44,2,30,1,1,3,6,16,9,17,39,82,2,2,24,7,1,7,3,16,9,14,44,2,1,2,1,2,3,5,2,4,1,6,7,5,3, + 2,6,1,11,5,11,2,1,18,19,8,1,3,24,29,2,1,3,5,2,2,1,13,6,5,1,46,11,3,5,1,1,5,8,2,10,6,12,6,3,7,11,2,4,16,13,2,5,1,1,2,2,5,2,28,5,2,23,10,8,4, + 4,22,39,95,38,8,14,9,5,1,13,5,4,3,13,12,11,1,9,1,27,37,2,5,4,4,63,211,95,2,2,2,1,3,5,2,1,1,2,2,1,1,1,3,2,4,1,2,1,1,5,2,2,1,1,2,3,1,3,1,1,1, + 3,1,4,2,1,3,6,1,1,3,7,15,5,3,2,5,3,9,11,4,2,22,1,6,3,8,7,1,4,28,4,16,3,3,25,4,4,27,27,1,4,1,2,2,7,1,3,5,2,28,8,2,14,1,8,6,16,25,3,3,3,14,3, + 3,1,1,2,1,4,6,3,8,4,1,1,1,2,3,6,10,6,2,3,18,3,2,5,5,4,3,1,5,2,5,4,23,7,6,12,6,4,17,11,9,5,1,1,10,5,12,1,1,11,26,33,7,3,6,1,17,7,1,5,12,1,11, + 2,4,1,8,14,17,23,1,2,1,7,8,16,11,9,6,5,2,6,4,16,2,8,14,1,11,8,9,1,1,1,9,25,4,11,19,7,2,15,2,12,8,52,7,5,19,2,16,4,36,8,1,16,8,24,26,4,6,2,9, + 5,4,36,3,28,12,25,15,37,27,17,12,59,38,5,32,127,1,2,9,17,14,4,1,2,1,1,8,11,50,4,14,2,19,16,4,17,5,4,5,26,12,45,2,23,45,104,30,12,8,3,10,2,2, + 3,3,1,4,20,7,2,9,6,15,2,20,1,3,16,4,11,15,6,134,2,5,59,1,2,2,2,1,9,17,3,26,137,10,211,59,1,2,4,1,4,1,1,1,2,6,2,3,1,1,2,3,2,3,1,3,4,4,2,3,3, + 1,4,3,1,7,2,2,3,1,2,1,3,3,3,2,2,3,2,1,3,14,6,1,3,2,9,6,15,27,9,34,145,1,1,2,1,1,1,1,2,1,1,1,1,2,2,2,3,1,2,1,1,1,2,3,5,8,3,5,2,4,1,3,2,2,2,12, + 4,1,1,1,10,4,5,1,20,4,16,1,15,9,5,12,2,9,2,5,4,2,26,19,7,1,26,4,30,12,15,42,1,6,8,172,1,1,4,2,1,1,11,2,2,4,2,1,2,1,10,8,1,2,1,4,5,1,2,5,1,8, + 4,1,3,4,2,1,6,2,1,3,4,1,2,1,1,1,1,12,5,7,2,4,3,1,1,1,3,3,6,1,2,2,3,3,3,2,1,2,12,14,11,6,6,4,12,2,8,1,7,10,1,35,7,4,13,15,4,3,23,21,28,52,5, + 26,5,6,1,7,10,2,7,53,3,2,1,1,1,2,163,532,1,10,11,1,3,3,4,8,2,8,6,2,2,23,22,4,2,2,4,2,1,3,1,3,3,5,9,8,2,1,2,8,1,10,2,12,21,20,15,105,2,3,1,1, + 3,2,3,1,1,2,5,1,4,15,11,19,1,1,1,1,5,4,5,1,1,2,5,3,5,12,1,2,5,1,11,1,1,15,9,1,4,5,3,26,8,2,1,3,1,1,15,19,2,12,1,2,5,2,7,2,19,2,20,6,26,7,5, + 2,2,7,34,21,13,70,2,128,1,1,2,1,1,2,1,1,3,2,2,2,15,1,4,1,3,4,42,10,6,1,49,85,8,1,2,1,1,4,4,2,3,6,1,5,7,4,3,211,4,1,2,1,2,5,1,2,4,2,2,6,5,6, + 10,3,4,48,100,6,2,16,296,5,27,387,2,2,3,7,16,8,5,38,15,39,21,9,10,3,7,59,13,27,21,47,5,21,6 + }; + static ImWchar base_ranges[] = // not zero-terminated + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x2000, 0x206F, // General Punctuation + 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana + 0x31F0, 0x31FF, // Katakana Phonetic Extensions + 0xFF00, 0xFFEF, // Half-width characters + 0xFFFD, 0xFFFD // Invalid + }; + static ImWchar full_ranges[IM_ARRAYSIZE(base_ranges) + IM_ARRAYSIZE(accumulative_offsets_from_0x4E00) * 2 + 1] = { 0 }; + if (!full_ranges[0]) + { + memcpy(full_ranges, base_ranges, sizeof(base_ranges)); + UnpackAccumulativeOffsetsIntoRanges(0x4E00, accumulative_offsets_from_0x4E00, IM_ARRAYSIZE(accumulative_offsets_from_0x4E00), full_ranges + IM_ARRAYSIZE(base_ranges)); + } + return &full_ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesJapanese() +{ + // 2999 ideograms code points for Japanese + // - 2136 Joyo (meaning "for regular use" or "for common use") Kanji code points + // - 863 Jinmeiyo (meaning "for personal name") Kanji code points + // - Sourced from the character information database of the Information-technology Promotion Agency, Japan + // - https://mojikiban.ipa.go.jp/mji/ + // - Available under the terms of the Creative Commons Attribution-ShareAlike 2.1 Japan (CC BY-SA 2.1 JP). + // - https://creativecommons.org/licenses/by-sa/2.1/jp/deed.en + // - https://creativecommons.org/licenses/by-sa/2.1/jp/legalcode + // - You can generate this code by the script at: + // - https://github.com/vaiorabbit/everyday_use_kanji + // - References: + // - List of Joyo Kanji + // - (Official list by the Agency for Cultural Affairs) https://www.bunka.go.jp/kokugo_nihongo/sisaku/joho/joho/kakuki/14/tosin02/index.html + // - (Wikipedia) https://en.wikipedia.org/wiki/List_of_j%C5%8Dy%C5%8D_kanji + // - List of Jinmeiyo Kanji + // - (Official list by the Ministry of Justice) http://www.moj.go.jp/MINJI/minji86.html + // - (Wikipedia) https://en.wikipedia.org/wiki/Jinmeiy%C5%8D_kanji + // - Missing 1 Joyo Kanji: U+20B9F (Kun'yomi: Shikaru, On'yomi: Shitsu,shichi), see https://github.com/ocornut/imgui/pull/3627 for details. + // You can use ImFontGlyphRangesBuilder to create your own ranges derived from this, by merging existing ranges or adding new characters. + // (Stored as accumulative offsets from the initial unicode codepoint 0x4E00. This encoding is designed to helps us compact the source code size.) + static const short accumulative_offsets_from_0x4E00[] = + { + 0,1,2,4,1,1,1,1,2,1,3,3,2,2,1,5,3,5,7,5,6,1,2,1,7,2,6,3,1,8,1,1,4,1,1,18,2,11,2,6,2,1,2,1,5,1,2,1,3,1,2,1,2,3,3,1,1,2,3,1,1,1,12,7,9,1,4,5,1, + 1,2,1,10,1,1,9,2,2,4,5,6,9,3,1,1,1,1,9,3,18,5,2,2,2,2,1,6,3,7,1,1,1,1,2,2,4,2,1,23,2,10,4,3,5,2,4,10,2,4,13,1,6,1,9,3,1,1,6,6,7,6,3,1,2,11,3, + 2,2,3,2,15,2,2,5,4,3,6,4,1,2,5,2,12,16,6,13,9,13,2,1,1,7,16,4,7,1,19,1,5,1,2,2,7,7,8,2,6,5,4,9,18,7,4,5,9,13,11,8,15,2,1,1,1,2,1,2,2,1,2,2,8, + 2,9,3,3,1,1,4,4,1,1,1,4,9,1,4,3,5,5,2,7,5,3,4,8,2,1,13,2,3,3,1,14,1,1,4,5,1,3,6,1,5,2,1,1,3,3,3,3,1,1,2,7,6,6,7,1,4,7,6,1,1,1,1,1,12,3,3,9,5, + 2,6,1,5,6,1,2,3,18,2,4,14,4,1,3,6,1,1,6,3,5,5,3,2,2,2,2,12,3,1,4,2,3,2,3,11,1,7,4,1,2,1,3,17,1,9,1,24,1,1,4,2,2,4,1,2,7,1,1,1,3,1,2,2,4,15,1, + 1,2,1,1,2,1,5,2,5,20,2,5,9,1,10,8,7,6,1,1,1,1,1,1,6,2,1,2,8,1,1,1,1,5,1,1,3,1,1,1,1,3,1,1,12,4,1,3,1,1,1,1,1,10,3,1,7,5,13,1,2,3,4,6,1,1,30, + 2,9,9,1,15,38,11,3,1,8,24,7,1,9,8,10,2,1,9,31,2,13,6,2,9,4,49,5,2,15,2,1,10,2,1,1,1,2,2,6,15,30,35,3,14,18,8,1,16,10,28,12,19,45,38,1,3,2,3, + 13,2,1,7,3,6,5,3,4,3,1,5,7,8,1,5,3,18,5,3,6,1,21,4,24,9,24,40,3,14,3,21,3,2,1,2,4,2,3,1,15,15,6,5,1,1,3,1,5,6,1,9,7,3,3,2,1,4,3,8,21,5,16,4, + 5,2,10,11,11,3,6,3,2,9,3,6,13,1,2,1,1,1,1,11,12,6,6,1,4,2,6,5,2,1,1,3,3,6,13,3,1,1,5,1,2,3,3,14,2,1,2,2,2,5,1,9,5,1,1,6,12,3,12,3,4,13,2,14, + 2,8,1,17,5,1,16,4,2,2,21,8,9,6,23,20,12,25,19,9,38,8,3,21,40,25,33,13,4,3,1,4,1,2,4,1,2,5,26,2,1,1,2,1,3,6,2,1,1,1,1,1,1,2,3,1,1,1,9,2,3,1,1, + 1,3,6,3,2,1,1,6,6,1,8,2,2,2,1,4,1,2,3,2,7,3,2,4,1,2,1,2,2,1,1,1,1,1,3,1,2,5,4,10,9,4,9,1,1,1,1,1,1,5,3,2,1,6,4,9,6,1,10,2,31,17,8,3,7,5,40,1, + 7,7,1,6,5,2,10,7,8,4,15,39,25,6,28,47,18,10,7,1,3,1,1,2,1,1,1,3,3,3,1,1,1,3,4,2,1,4,1,3,6,10,7,8,6,2,2,1,3,3,2,5,8,7,9,12,2,15,1,1,4,1,2,1,1, + 1,3,2,1,3,3,5,6,2,3,2,10,1,4,2,8,1,1,1,11,6,1,21,4,16,3,1,3,1,4,2,3,6,5,1,3,1,1,3,3,4,6,1,1,10,4,2,7,10,4,7,4,2,9,4,3,1,1,1,4,1,8,3,4,1,3,1, + 6,1,4,2,1,4,7,2,1,8,1,4,5,1,1,2,2,4,6,2,7,1,10,1,1,3,4,11,10,8,21,4,6,1,3,5,2,1,2,28,5,5,2,3,13,1,2,3,1,4,2,1,5,20,3,8,11,1,3,3,3,1,8,10,9,2, + 10,9,2,3,1,1,2,4,1,8,3,6,1,7,8,6,11,1,4,29,8,4,3,1,2,7,13,1,4,1,6,2,6,12,12,2,20,3,2,3,6,4,8,9,2,7,34,5,1,18,6,1,1,4,4,5,7,9,1,2,2,4,3,4,1,7, + 2,2,2,6,2,3,25,5,3,6,1,4,6,7,4,2,1,4,2,13,6,4,4,3,1,5,3,4,4,3,2,1,1,4,1,2,1,1,3,1,11,1,6,3,1,7,3,6,2,8,8,6,9,3,4,11,3,2,10,12,2,5,11,1,6,4,5, + 3,1,8,5,4,6,6,3,5,1,1,3,2,1,2,2,6,17,12,1,10,1,6,12,1,6,6,19,9,6,16,1,13,4,4,15,7,17,6,11,9,15,12,6,7,2,1,2,2,15,9,3,21,4,6,49,18,7,3,2,3,1, + 6,8,2,2,6,2,9,1,3,6,4,4,1,2,16,2,5,2,1,6,2,3,5,3,1,2,5,1,2,1,9,3,1,8,6,4,8,11,3,1,1,1,1,3,1,13,8,4,1,3,2,2,1,4,1,11,1,5,2,1,5,2,5,8,6,1,1,7, + 4,3,8,3,2,7,2,1,5,1,5,2,4,7,6,2,8,5,1,11,4,5,3,6,18,1,2,13,3,3,1,21,1,1,4,1,4,1,1,1,8,1,2,2,7,1,2,4,2,2,9,2,1,1,1,4,3,6,3,12,5,1,1,1,5,6,3,2, + 4,8,2,2,4,2,7,1,8,9,5,2,3,2,1,3,2,13,7,14,6,5,1,1,2,1,4,2,23,2,1,1,6,3,1,4,1,15,3,1,7,3,9,14,1,3,1,4,1,1,5,8,1,3,8,3,8,15,11,4,14,4,4,2,5,5, + 1,7,1,6,14,7,7,8,5,15,4,8,6,5,6,2,1,13,1,20,15,11,9,2,5,6,2,11,2,6,2,5,1,5,8,4,13,19,25,4,1,1,11,1,34,2,5,9,14,6,2,2,6,1,1,14,1,3,14,13,1,6, + 12,21,14,14,6,32,17,8,32,9,28,1,2,4,11,8,3,1,14,2,5,15,1,1,1,1,3,6,4,1,3,4,11,3,1,1,11,30,1,5,1,4,1,5,8,1,1,3,2,4,3,17,35,2,6,12,17,3,1,6,2, + 1,1,12,2,7,3,3,2,1,16,2,8,3,6,5,4,7,3,3,8,1,9,8,5,1,2,1,3,2,8,1,2,9,12,1,1,2,3,8,3,24,12,4,3,7,5,8,3,3,3,3,3,3,1,23,10,3,1,2,2,6,3,1,16,1,16, + 22,3,10,4,11,6,9,7,7,3,6,2,2,2,4,10,2,1,1,2,8,7,1,6,4,1,3,3,3,5,10,12,12,2,3,12,8,15,1,1,16,6,6,1,5,9,11,4,11,4,2,6,12,1,17,5,13,1,4,9,5,1,11, + 2,1,8,1,5,7,28,8,3,5,10,2,17,3,38,22,1,2,18,12,10,4,38,18,1,4,44,19,4,1,8,4,1,12,1,4,31,12,1,14,7,75,7,5,10,6,6,13,3,2,11,11,3,2,5,28,15,6,18, + 18,5,6,4,3,16,1,7,18,7,36,3,5,3,1,7,1,9,1,10,7,2,4,2,6,2,9,7,4,3,32,12,3,7,10,2,23,16,3,1,12,3,31,4,11,1,3,8,9,5,1,30,15,6,12,3,2,2,11,19,9, + 14,2,6,2,3,19,13,17,5,3,3,25,3,14,1,1,1,36,1,3,2,19,3,13,36,9,13,31,6,4,16,34,2,5,4,2,3,3,5,1,1,1,4,3,1,17,3,2,3,5,3,1,3,2,3,5,6,3,12,11,1,3, + 1,2,26,7,12,7,2,14,3,3,7,7,11,25,25,28,16,4,36,1,2,1,6,2,1,9,3,27,17,4,3,4,13,4,1,3,2,2,1,10,4,2,4,6,3,8,2,1,18,1,1,24,2,2,4,33,2,3,63,7,1,6, + 40,7,3,4,4,2,4,15,18,1,16,1,1,11,2,41,14,1,3,18,13,3,2,4,16,2,17,7,15,24,7,18,13,44,2,2,3,6,1,1,7,5,1,7,1,4,3,3,5,10,8,2,3,1,8,1,1,27,4,2,1, + 12,1,2,1,10,6,1,6,7,5,2,3,7,11,5,11,3,6,6,2,3,15,4,9,1,1,2,1,2,11,2,8,12,8,5,4,2,3,1,5,2,2,1,14,1,12,11,4,1,11,17,17,4,3,2,5,5,7,3,1,5,9,9,8, + 2,5,6,6,13,13,2,1,2,6,1,2,2,49,4,9,1,2,10,16,7,8,4,3,2,23,4,58,3,29,1,14,19,19,11,11,2,7,5,1,3,4,6,2,18,5,12,12,17,17,3,3,2,4,1,6,2,3,4,3,1, + 1,1,1,5,1,1,9,1,3,1,3,6,1,8,1,1,2,6,4,14,3,1,4,11,4,1,3,32,1,2,4,13,4,1,2,4,2,1,3,1,11,1,4,2,1,4,4,6,3,5,1,6,5,7,6,3,23,3,5,3,5,3,3,13,3,9,10, + 1,12,10,2,3,18,13,7,160,52,4,2,2,3,2,14,5,4,12,4,6,4,1,20,4,11,6,2,12,27,1,4,1,2,2,7,4,5,2,28,3,7,25,8,3,19,3,6,10,2,2,1,10,2,5,4,1,3,4,1,5, + 3,2,6,9,3,6,2,16,3,3,16,4,5,5,3,2,1,2,16,15,8,2,6,21,2,4,1,22,5,8,1,1,21,11,2,1,11,11,19,13,12,4,2,3,2,3,6,1,8,11,1,4,2,9,5,2,1,11,2,9,1,1,2, + 14,31,9,3,4,21,14,4,8,1,7,2,2,2,5,1,4,20,3,3,4,10,1,11,9,8,2,1,4,5,14,12,14,2,17,9,6,31,4,14,1,20,13,26,5,2,7,3,6,13,2,4,2,19,6,2,2,18,9,3,5, + 12,12,14,4,6,2,3,6,9,5,22,4,5,25,6,4,8,5,2,6,27,2,35,2,16,3,7,8,8,6,6,5,9,17,2,20,6,19,2,13,3,1,1,1,4,17,12,2,14,7,1,4,18,12,38,33,2,10,1,1, + 2,13,14,17,11,50,6,33,20,26,74,16,23,45,50,13,38,33,6,6,7,4,4,2,1,3,2,5,8,7,8,9,3,11,21,9,13,1,3,10,6,7,1,2,2,18,5,5,1,9,9,2,68,9,19,13,2,5, + 1,4,4,7,4,13,3,9,10,21,17,3,26,2,1,5,2,4,5,4,1,7,4,7,3,4,2,1,6,1,1,20,4,1,9,2,2,1,3,3,2,3,2,1,1,1,20,2,3,1,6,2,3,6,2,4,8,1,3,2,10,3,5,3,4,4, + 3,4,16,1,6,1,10,2,4,2,1,1,2,10,11,2,2,3,1,24,31,4,10,10,2,5,12,16,164,15,4,16,7,9,15,19,17,1,2,1,1,5,1,1,1,1,1,3,1,4,3,1,3,1,3,1,2,1,1,3,3,7, + 2,8,1,2,2,2,1,3,4,3,7,8,12,92,2,10,3,1,3,14,5,25,16,42,4,7,7,4,2,21,5,27,26,27,21,25,30,31,2,1,5,13,3,22,5,6,6,11,9,12,1,5,9,7,5,5,22,60,3,5, + 13,1,1,8,1,1,3,3,2,1,9,3,3,18,4,1,2,3,7,6,3,1,2,3,9,1,3,1,3,2,1,3,1,1,1,2,1,11,3,1,6,9,1,3,2,3,1,2,1,5,1,1,4,3,4,1,2,2,4,4,1,7,2,1,2,2,3,5,13, + 18,3,4,14,9,9,4,16,3,7,5,8,2,6,48,28,3,1,1,4,2,14,8,2,9,2,1,15,2,4,3,2,10,16,12,8,7,1,1,3,1,1,1,2,7,4,1,6,4,38,39,16,23,7,15,15,3,2,12,7,21, + 37,27,6,5,4,8,2,10,8,8,6,5,1,2,1,3,24,1,16,17,9,23,10,17,6,1,51,55,44,13,294,9,3,6,2,4,2,2,15,1,1,1,13,21,17,68,14,8,9,4,1,4,9,3,11,7,1,1,1, + 5,6,3,2,1,1,1,2,3,8,1,2,2,4,1,5,5,2,1,4,3,7,13,4,1,4,1,3,1,1,1,5,5,10,1,6,1,5,2,1,5,2,4,1,4,5,7,3,18,2,9,11,32,4,3,3,2,4,7,11,16,9,11,8,13,38, + 32,8,4,2,1,1,2,1,2,4,4,1,1,1,4,1,21,3,11,1,16,1,1,6,1,3,2,4,9,8,57,7,44,1,3,3,13,3,10,1,1,7,5,2,7,21,47,63,3,15,4,7,1,16,1,1,2,8,2,3,42,15,4, + 1,29,7,22,10,3,78,16,12,20,18,4,67,11,5,1,3,15,6,21,31,32,27,18,13,71,35,5,142,4,10,1,2,50,19,33,16,35,37,16,19,27,7,1,133,19,1,4,8,7,20,1,4, + 4,1,10,3,1,6,1,2,51,5,40,15,24,43,22928,11,1,13,154,70,3,1,1,7,4,10,1,2,1,1,2,1,2,1,2,2,1,1,2,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1, + 3,2,1,1,1,1,2,1,1, + }; + static ImWchar base_ranges[] = // not zero-terminated + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana + 0x31F0, 0x31FF, // Katakana Phonetic Extensions + 0xFF00, 0xFFEF, // Half-width characters + 0xFFFD, 0xFFFD // Invalid + }; + static ImWchar full_ranges[IM_ARRAYSIZE(base_ranges) + IM_ARRAYSIZE(accumulative_offsets_from_0x4E00)*2 + 1] = { 0 }; + if (!full_ranges[0]) + { + memcpy(full_ranges, base_ranges, sizeof(base_ranges)); + UnpackAccumulativeOffsetsIntoRanges(0x4E00, accumulative_offsets_from_0x4E00, IM_ARRAYSIZE(accumulative_offsets_from_0x4E00), full_ranges + IM_ARRAYSIZE(base_ranges)); + } + return &full_ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesCyrillic() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x0400, 0x052F, // Cyrillic + Cyrillic Supplement + 0x2DE0, 0x2DFF, // Cyrillic Extended-A + 0xA640, 0xA69F, // Cyrillic Extended-B + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesThai() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + 0x2010, 0x205E, // Punctuations + 0x0E00, 0x0E7F, // Thai + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesVietnamese() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + 0x0102, 0x0103, + 0x0110, 0x0111, + 0x0128, 0x0129, + 0x0168, 0x0169, + 0x01A0, 0x01A1, + 0x01AF, 0x01B0, + 0x1EA0, 0x1EF9, + 0, + }; + return &ranges[0]; +} + +//----------------------------------------------------------------------------- +// [SECTION] ImFontGlyphRangesBuilder +//----------------------------------------------------------------------------- + +void ImFontGlyphRangesBuilder::AddText(const char* text, const char* text_end) +{ + while (text_end ? (text < text_end) : *text) + { + unsigned int c = 0; + int c_len = ImTextCharFromUtf8(&c, text, text_end); + text += c_len; + if (c_len == 0) + break; + AddChar((ImWchar)c); + } +} + +void ImFontGlyphRangesBuilder::AddRanges(const ImWchar* ranges) +{ + for (; ranges[0]; ranges += 2) + for (unsigned int c = ranges[0]; c <= ranges[1] && c <= IM_UNICODE_CODEPOINT_MAX; c++) //-V560 + AddChar((ImWchar)c); +} + +void ImFontGlyphRangesBuilder::BuildRanges(ImVector* out_ranges) +{ + const int max_codepoint = IM_UNICODE_CODEPOINT_MAX; + for (int n = 0; n <= max_codepoint; n++) + if (GetBit(n)) + { + out_ranges->push_back((ImWchar)n); + while (n < max_codepoint && GetBit(n + 1)) + n++; + out_ranges->push_back((ImWchar)n); + } + out_ranges->push_back(0); +} + +//----------------------------------------------------------------------------- +// [SECTION] ImFont +//----------------------------------------------------------------------------- + +ImFont::ImFont() +{ + FontSize = 0.0f; + FallbackAdvanceX = 0.0f; + FallbackChar = (ImWchar)-1; + EllipsisChar = (ImWchar)-1; + DotChar = (ImWchar)-1; + FallbackGlyph = NULL; + ContainerAtlas = NULL; + ConfigData = NULL; + ConfigDataCount = 0; + DirtyLookupTables = false; + Scale = 1.0f; + Ascent = Descent = 0.0f; + MetricsTotalSurface = 0; + memset(Used4kPagesMap, 0, sizeof(Used4kPagesMap)); +} + +ImFont::~ImFont() +{ + ClearOutputData(); +} + +void ImFont::ClearOutputData() +{ + FontSize = 0.0f; + FallbackAdvanceX = 0.0f; + Glyphs.clear(); + IndexAdvanceX.clear(); + IndexLookup.clear(); + FallbackGlyph = NULL; + ContainerAtlas = NULL; + DirtyLookupTables = true; + Ascent = Descent = 0.0f; + MetricsTotalSurface = 0; +} + +static ImWchar FindFirstExistingGlyph(ImFont* font, const ImWchar* candidate_chars, int candidate_chars_count) +{ + for (int n = 0; n < candidate_chars_count; n++) + if (font->FindGlyphNoFallback(candidate_chars[n]) != NULL) + return candidate_chars[n]; + return (ImWchar)-1; +} + +void ImFont::BuildLookupTable() +{ + int max_codepoint = 0; + for (int i = 0; i != Glyphs.Size; i++) + max_codepoint = ImMax(max_codepoint, (int)Glyphs[i].Codepoint); + + // Build lookup table + IM_ASSERT(Glyphs.Size < 0xFFFF); // -1 is reserved + IndexAdvanceX.clear(); + IndexLookup.clear(); + DirtyLookupTables = false; + memset(Used4kPagesMap, 0, sizeof(Used4kPagesMap)); + GrowIndex(max_codepoint + 1); + for (int i = 0; i < Glyphs.Size; i++) + { + int codepoint = (int)Glyphs[i].Codepoint; + IndexAdvanceX[codepoint] = Glyphs[i].AdvanceX; + IndexLookup[codepoint] = (ImWchar)i; + + // Mark 4K page as used + const int page_n = codepoint / 4096; + Used4kPagesMap[page_n >> 3] |= 1 << (page_n & 7); + } + + // Create a glyph to handle TAB + // FIXME: Needs proper TAB handling but it needs to be contextualized (or we could arbitrary say that each string starts at "column 0" ?) + if (FindGlyph((ImWchar)' ')) + { + if (Glyphs.back().Codepoint != '\t') // So we can call this function multiple times (FIXME: Flaky) + Glyphs.resize(Glyphs.Size + 1); + ImFontGlyph& tab_glyph = Glyphs.back(); + tab_glyph = *FindGlyph((ImWchar)' '); + tab_glyph.Codepoint = '\t'; + tab_glyph.AdvanceX *= IM_TABSIZE; + IndexAdvanceX[(int)tab_glyph.Codepoint] = (float)tab_glyph.AdvanceX; + IndexLookup[(int)tab_glyph.Codepoint] = (ImWchar)(Glyphs.Size - 1); + } + + // Mark special glyphs as not visible (note that AddGlyph already mark as non-visible glyphs with zero-size polygons) + SetGlyphVisible((ImWchar)' ', false); + SetGlyphVisible((ImWchar)'\t', false); + + // Ellipsis character is required for rendering elided text. We prefer using U+2026 (horizontal ellipsis). + // However some old fonts may contain ellipsis at U+0085. Here we auto-detect most suitable ellipsis character. + // FIXME: Note that 0x2026 is rarely included in our font ranges. Because of this we are more likely to use three individual dots. + const ImWchar ellipsis_chars[] = { (ImWchar)0x2026, (ImWchar)0x0085 }; + const ImWchar dots_chars[] = { (ImWchar)'.', (ImWchar)0xFF0E }; + if (EllipsisChar == (ImWchar)-1) + EllipsisChar = FindFirstExistingGlyph(this, ellipsis_chars, IM_ARRAYSIZE(ellipsis_chars)); + if (DotChar == (ImWchar)-1) + DotChar = FindFirstExistingGlyph(this, dots_chars, IM_ARRAYSIZE(dots_chars)); + + // Setup fallback character + const ImWchar fallback_chars[] = { (ImWchar)IM_UNICODE_CODEPOINT_INVALID, (ImWchar)'?', (ImWchar)' ' }; + FallbackGlyph = FindGlyphNoFallback(FallbackChar); + if (FallbackGlyph == NULL) + { + FallbackChar = FindFirstExistingGlyph(this, fallback_chars, IM_ARRAYSIZE(fallback_chars)); + FallbackGlyph = FindGlyphNoFallback(FallbackChar); + if (FallbackGlyph == NULL) + { + FallbackGlyph = &Glyphs.back(); + FallbackChar = (ImWchar)FallbackGlyph->Codepoint; + } + } + + FallbackAdvanceX = FallbackGlyph->AdvanceX; + for (int i = 0; i < max_codepoint + 1; i++) + if (IndexAdvanceX[i] < 0.0f) + IndexAdvanceX[i] = FallbackAdvanceX; +} + +// API is designed this way to avoid exposing the 4K page size +// e.g. use with IsGlyphRangeUnused(0, 255) +bool ImFont::IsGlyphRangeUnused(unsigned int c_begin, unsigned int c_last) +{ + unsigned int page_begin = (c_begin / 4096); + unsigned int page_last = (c_last / 4096); + for (unsigned int page_n = page_begin; page_n <= page_last; page_n++) + if ((page_n >> 3) < sizeof(Used4kPagesMap)) + if (Used4kPagesMap[page_n >> 3] & (1 << (page_n & 7))) + return false; + return true; +} + +void ImFont::SetGlyphVisible(ImWchar c, bool visible) +{ + if (ImFontGlyph* glyph = (ImFontGlyph*)(void*)FindGlyph((ImWchar)c)) + glyph->Visible = visible ? 1 : 0; +} + +void ImFont::GrowIndex(int new_size) +{ + IM_ASSERT(IndexAdvanceX.Size == IndexLookup.Size); + if (new_size <= IndexLookup.Size) + return; + IndexAdvanceX.resize(new_size, -1.0f); + IndexLookup.resize(new_size, (ImWchar)-1); +} + +// x0/y0/x1/y1 are offset from the character upper-left layout position, in pixels. Therefore x0/y0 are often fairly close to zero. +// Not to be mistaken with texture coordinates, which are held by u0/v0/u1/v1 in normalized format (0.0..1.0 on each texture axis). +// 'cfg' is not necessarily == 'this->ConfigData' because multiple source fonts+configs can be used to build one target font. +void ImFont::AddGlyph(const ImFontConfig* cfg, ImWchar codepoint, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x) +{ + if (cfg != NULL) + { + // Clamp & recenter if needed + const float advance_x_original = advance_x; + advance_x = ImClamp(advance_x, cfg->GlyphMinAdvanceX, cfg->GlyphMaxAdvanceX); + if (advance_x != advance_x_original) + { + float char_off_x = cfg->PixelSnapH ? ImFloor((advance_x - advance_x_original) * 0.5f) : (advance_x - advance_x_original) * 0.5f; + x0 += char_off_x; + x1 += char_off_x; + } + + // Snap to pixel + if (cfg->PixelSnapH) + advance_x = IM_ROUND(advance_x); + + // Bake spacing + advance_x += cfg->GlyphExtraSpacing.x; + } + + Glyphs.resize(Glyphs.Size + 1); + ImFontGlyph& glyph = Glyphs.back(); + glyph.Codepoint = (unsigned int)codepoint; + glyph.Visible = (x0 != x1) && (y0 != y1); + glyph.Colored = false; + glyph.X0 = x0; + glyph.Y0 = y0; + glyph.X1 = x1; + glyph.Y1 = y1; + glyph.U0 = u0; + glyph.V0 = v0; + glyph.U1 = u1; + glyph.V1 = v1; + glyph.AdvanceX = advance_x; + + // Compute rough surface usage metrics (+1 to account for average padding, +0.99 to round) + // We use (U1-U0)*TexWidth instead of X1-X0 to account for oversampling. + float pad = ContainerAtlas->TexGlyphPadding + 0.99f; + DirtyLookupTables = true; + MetricsTotalSurface += (int)((glyph.U1 - glyph.U0) * ContainerAtlas->TexWidth + pad) * (int)((glyph.V1 - glyph.V0) * ContainerAtlas->TexHeight + pad); +} + +void ImFont::AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst) +{ + IM_ASSERT(IndexLookup.Size > 0); // Currently this can only be called AFTER the font has been built, aka after calling ImFontAtlas::GetTexDataAs*() function. + unsigned int index_size = (unsigned int)IndexLookup.Size; + + if (dst < index_size && IndexLookup.Data[dst] == (ImWchar)-1 && !overwrite_dst) // 'dst' already exists + return; + if (src >= index_size && dst >= index_size) // both 'dst' and 'src' don't exist -> no-op + return; + + GrowIndex(dst + 1); + IndexLookup[dst] = (src < index_size) ? IndexLookup.Data[src] : (ImWchar)-1; + IndexAdvanceX[dst] = (src < index_size) ? IndexAdvanceX.Data[src] : 1.0f; +} + +const ImFontGlyph* ImFont::FindGlyph(ImWchar c) const +{ + if (c >= (size_t)IndexLookup.Size) + return FallbackGlyph; + const ImWchar i = IndexLookup.Data[c]; + if (i == (ImWchar)-1) + return FallbackGlyph; + return &Glyphs.Data[i]; +} + +const ImFontGlyph* ImFont::FindGlyphNoFallback(ImWchar c) const +{ + if (c >= (size_t)IndexLookup.Size) + return NULL; + const ImWchar i = IndexLookup.Data[c]; + if (i == (ImWchar)-1) + return NULL; + return &Glyphs.Data[i]; +} + +const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) const +{ + // Simple word-wrapping for English, not full-featured. Please submit failing cases! + // FIXME: Much possible improvements (don't cut things like "word !", "word!!!" but cut within "word,,,,", more sensible support for punctuations, support for Unicode punctuations, etc.) + + // For references, possible wrap point marked with ^ + // "aaa bbb, ccc,ddd. eee fff. ggg!" + // ^ ^ ^ ^ ^__ ^ ^ + + // List of hardcoded separators: .,;!?'" + + // Skip extra blanks after a line returns (that includes not counting them in width computation) + // e.g. "Hello world" --> "Hello" "World" + + // Cut words that cannot possibly fit within one line. + // e.g.: "The tropical fish" with ~5 characters worth of width --> "The tr" "opical" "fish" + + float line_width = 0.0f; + float word_width = 0.0f; + float blank_width = 0.0f; + wrap_width /= scale; // We work with unscaled widths to avoid scaling every characters + + const char* word_end = text; + const char* prev_word_end = NULL; + bool inside_word = true; + + const char* s = text; + while (s < text_end) + { + unsigned int c = (unsigned int)*s; + const char* next_s; + if (c < 0x80) + next_s = s + 1; + else + next_s = s + ImTextCharFromUtf8(&c, s, text_end); + if (c == 0) + break; + + if (c < 32) + { + if (c == '\n') + { + line_width = word_width = blank_width = 0.0f; + inside_word = true; + s = next_s; + continue; + } + if (c == '\r') + { + s = next_s; + continue; + } + } + + const float char_width = ((int)c < IndexAdvanceX.Size ? IndexAdvanceX.Data[c] : FallbackAdvanceX); + if (ImCharIsBlankW(c)) + { + if (inside_word) + { + line_width += blank_width; + blank_width = 0.0f; + word_end = s; + } + blank_width += char_width; + inside_word = false; + } + else + { + word_width += char_width; + if (inside_word) + { + word_end = next_s; + } + else + { + prev_word_end = word_end; + line_width += word_width + blank_width; + word_width = blank_width = 0.0f; + } + + // Allow wrapping after punctuation. + inside_word = (c != '.' && c != ',' && c != ';' && c != '!' && c != '?' && c != '\"'); + } + + // We ignore blank width at the end of the line (they can be skipped) + if (line_width + word_width > wrap_width) + { + // Words that cannot possibly fit within an entire line will be cut anywhere. + if (word_width < wrap_width) + s = prev_word_end ? prev_word_end : word_end; + break; + } + + s = next_s; + } + + return s; +} + +ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining) const +{ + if (!text_end) + text_end = text_begin + strlen(text_begin); // FIXME-OPT: Need to avoid this. + + const float line_height = size; + const float scale = size / FontSize; + + ImVec2 text_size = ImVec2(0, 0); + float line_width = 0.0f; + + const bool word_wrap_enabled = (wrap_width > 0.0f); + const char* word_wrap_eol = NULL; + + const char* s = text_begin; + while (s < text_end) + { + if (word_wrap_enabled) + { + // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. + if (!word_wrap_eol) + { + word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - line_width); + if (word_wrap_eol == s) // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. + word_wrap_eol++; // +1 may not be a character start point in UTF-8 but it's ok because we use s >= word_wrap_eol below + } + + if (s >= word_wrap_eol) + { + if (text_size.x < line_width) + text_size.x = line_width; + text_size.y += line_height; + line_width = 0.0f; + word_wrap_eol = NULL; + + // Wrapping skips upcoming blanks + while (s < text_end) + { + const char c = *s; + if (ImCharIsBlankA(c)) { s++; } else if (c == '\n') { s++; break; } else { break; } + } + continue; + } + } + + // Decode and advance source + const char* prev_s = s; + unsigned int c = (unsigned int)*s; + if (c < 0x80) + { + s += 1; + } + else + { + s += ImTextCharFromUtf8(&c, s, text_end); + if (c == 0) // Malformed UTF-8? + break; + } + + if (c < 32) + { + if (c == '\n') + { + text_size.x = ImMax(text_size.x, line_width); + text_size.y += line_height; + line_width = 0.0f; + continue; + } + if (c == '\r') + continue; + } + + const float char_width = ((int)c < IndexAdvanceX.Size ? IndexAdvanceX.Data[c] : FallbackAdvanceX) * scale; + if (line_width + char_width >= max_width) + { + s = prev_s; + break; + } + + line_width += char_width; + } + + if (text_size.x < line_width) + text_size.x = line_width; + + if (line_width > 0 || text_size.y == 0.0f) + text_size.y += line_height; + + if (remaining) + *remaining = s; + + return text_size; +} + +// Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. +void ImFont::RenderChar(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, ImWchar c) const +{ + const ImFontGlyph* glyph = FindGlyph(c); + if (!glyph || !glyph->Visible) + return; + if (glyph->Colored) + col |= ~IM_COL32_A_MASK; + float scale = (size >= 0.0f) ? (size / FontSize) : 1.0f; + pos.x = IM_FLOOR(pos.x); + pos.y = IM_FLOOR(pos.y); + draw_list->PrimReserve(6, 4); + draw_list->PrimRectUV(ImVec2(pos.x + glyph->X0 * scale, pos.y + glyph->Y0 * scale), ImVec2(pos.x + glyph->X1 * scale, pos.y + glyph->Y1 * scale), ImVec2(glyph->U0, glyph->V0), ImVec2(glyph->U1, glyph->V1), col); +} + +// Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. +void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, bool cpu_fine_clip) const +{ + if (!text_end) + text_end = text_begin + strlen(text_begin); // ImGui:: functions generally already provides a valid text_end, so this is merely to handle direct calls. + + // Align to be pixel perfect + pos.x = IM_FLOOR(pos.x); + pos.y = IM_FLOOR(pos.y); + float x = pos.x; + float y = pos.y; + if (y > clip_rect.w) + return; + + const float scale = size / FontSize; + const float line_height = FontSize * scale; + const bool word_wrap_enabled = (wrap_width > 0.0f); + const char* word_wrap_eol = NULL; + + // Fast-forward to first visible line + const char* s = text_begin; + if (y + line_height < clip_rect.y && !word_wrap_enabled) + while (y + line_height < clip_rect.y && s < text_end) + { + s = (const char*)memchr(s, '\n', text_end - s); + s = s ? s + 1 : text_end; + y += line_height; + } + + // For large text, scan for the last visible line in order to avoid over-reserving in the call to PrimReserve() + // Note that very large horizontal line will still be affected by the issue (e.g. a one megabyte string buffer without a newline will likely crash atm) + if (text_end - s > 10000 && !word_wrap_enabled) + { + const char* s_end = s; + float y_end = y; + while (y_end < clip_rect.w && s_end < text_end) + { + s_end = (const char*)memchr(s_end, '\n', text_end - s_end); + s_end = s_end ? s_end + 1 : text_end; + y_end += line_height; + } + text_end = s_end; + } + if (s == text_end) + return; + + // Reserve vertices for remaining worse case (over-reserving is useful and easily amortized) + const int vtx_count_max = (int)(text_end - s) * 4; + const int idx_count_max = (int)(text_end - s) * 6; + const int idx_expected_size = draw_list->IdxBuffer.Size + idx_count_max; + draw_list->PrimReserve(idx_count_max, vtx_count_max); + + ImDrawVert* vtx_write = draw_list->_VtxWritePtr; + ImDrawIdx* idx_write = draw_list->_IdxWritePtr; + unsigned int vtx_current_idx = draw_list->_VtxCurrentIdx; + + const ImU32 col_untinted = col | ~IM_COL32_A_MASK; + + while (s < text_end) + { + if (word_wrap_enabled) + { + // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. + if (!word_wrap_eol) + { + word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - (x - pos.x)); + if (word_wrap_eol == s) // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. + word_wrap_eol++; // +1 may not be a character start point in UTF-8 but it's ok because we use s >= word_wrap_eol below + } + + if (s >= word_wrap_eol) + { + x = pos.x; + y += line_height; + word_wrap_eol = NULL; + + // Wrapping skips upcoming blanks + while (s < text_end) + { + const char c = *s; + if (ImCharIsBlankA(c)) { s++; } else if (c == '\n') { s++; break; } else { break; } + } + continue; + } + } + + // Decode and advance source + unsigned int c = (unsigned int)*s; + if (c < 0x80) + { + s += 1; + } + else + { + s += ImTextCharFromUtf8(&c, s, text_end); + if (c == 0) // Malformed UTF-8? + break; + } + + if (c < 32) + { + if (c == '\n') + { + x = pos.x; + y += line_height; + if (y > clip_rect.w) + break; // break out of main loop + continue; + } + if (c == '\r') + continue; + } + + const ImFontGlyph* glyph = FindGlyph((ImWchar)c); + if (glyph == NULL) + continue; + + float char_width = glyph->AdvanceX * scale; + if (glyph->Visible) + { + // We don't do a second finer clipping test on the Y axis as we've already skipped anything before clip_rect.y and exit once we pass clip_rect.w + float x1 = x + glyph->X0 * scale; + float x2 = x + glyph->X1 * scale; + float y1 = y + glyph->Y0 * scale; + float y2 = y + glyph->Y1 * scale; + if (x1 <= clip_rect.z && x2 >= clip_rect.x) + { + // Render a character + float u1 = glyph->U0; + float v1 = glyph->V0; + float u2 = glyph->U1; + float v2 = glyph->V1; + + // CPU side clipping used to fit text in their frame when the frame is too small. Only does clipping for axis aligned quads. + if (cpu_fine_clip) + { + if (x1 < clip_rect.x) + { + u1 = u1 + (1.0f - (x2 - clip_rect.x) / (x2 - x1)) * (u2 - u1); + x1 = clip_rect.x; + } + if (y1 < clip_rect.y) + { + v1 = v1 + (1.0f - (y2 - clip_rect.y) / (y2 - y1)) * (v2 - v1); + y1 = clip_rect.y; + } + if (x2 > clip_rect.z) + { + u2 = u1 + ((clip_rect.z - x1) / (x2 - x1)) * (u2 - u1); + x2 = clip_rect.z; + } + if (y2 > clip_rect.w) + { + v2 = v1 + ((clip_rect.w - y1) / (y2 - y1)) * (v2 - v1); + y2 = clip_rect.w; + } + if (y1 >= y2) + { + x += char_width; + continue; + } + } + + // Support for untinted glyphs + ImU32 glyph_col = glyph->Colored ? col_untinted : col; + + // We are NOT calling PrimRectUV() here because non-inlined causes too much overhead in a debug builds. Inlined here: + { + idx_write[0] = (ImDrawIdx)(vtx_current_idx); idx_write[1] = (ImDrawIdx)(vtx_current_idx+1); idx_write[2] = (ImDrawIdx)(vtx_current_idx+2); + idx_write[3] = (ImDrawIdx)(vtx_current_idx); idx_write[4] = (ImDrawIdx)(vtx_current_idx+2); idx_write[5] = (ImDrawIdx)(vtx_current_idx+3); + vtx_write[0].pos.x = x1; vtx_write[0].pos.y = y1; vtx_write[0].col = glyph_col; vtx_write[0].uv.x = u1; vtx_write[0].uv.y = v1; + vtx_write[1].pos.x = x2; vtx_write[1].pos.y = y1; vtx_write[1].col = glyph_col; vtx_write[1].uv.x = u2; vtx_write[1].uv.y = v1; + vtx_write[2].pos.x = x2; vtx_write[2].pos.y = y2; vtx_write[2].col = glyph_col; vtx_write[2].uv.x = u2; vtx_write[2].uv.y = v2; + vtx_write[3].pos.x = x1; vtx_write[3].pos.y = y2; vtx_write[3].col = glyph_col; vtx_write[3].uv.x = u1; vtx_write[3].uv.y = v2; + vtx_write += 4; + vtx_current_idx += 4; + idx_write += 6; + } + } + } + x += char_width; + } + + // Give back unused vertices (clipped ones, blanks) ~ this is essentially a PrimUnreserve() action. + draw_list->VtxBuffer.Size = (int)(vtx_write - draw_list->VtxBuffer.Data); // Same as calling shrink() + draw_list->IdxBuffer.Size = (int)(idx_write - draw_list->IdxBuffer.Data); + draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ElemCount -= (idx_expected_size - draw_list->IdxBuffer.Size); + draw_list->_VtxWritePtr = vtx_write; + draw_list->_IdxWritePtr = idx_write; + draw_list->_VtxCurrentIdx = vtx_current_idx; +} + +//----------------------------------------------------------------------------- +// [SECTION] ImGui Internal Render Helpers +//----------------------------------------------------------------------------- +// Vaguely redesigned to stop accessing ImGui global state: +// - RenderArrow() +// - RenderBullet() +// - RenderCheckMark() +// - RenderMouseCursor() +// - RenderArrowPointingAt() +// - RenderRectFilledRangeH() +// - RenderRectFilledWithHole() +//----------------------------------------------------------------------------- +// Function in need of a redesign (legacy mess) +// - RenderColorRectWithAlphaCheckerboard() +//----------------------------------------------------------------------------- + +// Render an arrow aimed to be aligned with text (p_min is a position in the same space text would be positioned). To e.g. denote expanded/collapsed state +void ImGui::RenderArrow(ImDrawList* draw_list, ImVec2 pos, ImU32 col, ImGuiDir dir, float scale) +{ + const float h = draw_list->_Data->FontSize * 1.00f; + float r = h * 0.40f * scale; + ImVec2 center = pos + ImVec2(h * 0.50f, h * 0.50f * scale); + + ImVec2 a, b, c; + switch (dir) + { + case ImGuiDir_Up: + case ImGuiDir_Down: + if (dir == ImGuiDir_Up) r = -r; + a = ImVec2(+0.000f, +0.750f) * r; + b = ImVec2(-0.866f, -0.750f) * r; + c = ImVec2(+0.866f, -0.750f) * r; + break; + case ImGuiDir_Left: + case ImGuiDir_Right: + if (dir == ImGuiDir_Left) r = -r; + a = ImVec2(+0.750f, +0.000f) * r; + b = ImVec2(-0.750f, +0.866f) * r; + c = ImVec2(-0.750f, -0.866f) * r; + break; + case ImGuiDir_None: + case ImGuiDir_COUNT: + IM_ASSERT(0); + break; + } + draw_list->AddTriangleFilled(center + a, center + b, center + c, col); +} + +void ImGui::RenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col) +{ + draw_list->AddCircleFilled(pos, draw_list->_Data->FontSize * 0.20f, col, 8); +} + +void ImGui::RenderCheckMark(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz) +{ + float thickness = ImMax(sz / 5.0f, 1.0f); + sz -= thickness * 0.5f; + pos += ImVec2(thickness * 0.25f, thickness * 0.25f); + + float third = sz / 3.0f; + float bx = pos.x + third; + float by = pos.y + sz - third * 0.5f; + draw_list->PathLineTo(ImVec2(bx - third, by - third)); + draw_list->PathLineTo(ImVec2(bx, by)); + draw_list->PathLineTo(ImVec2(bx + third * 2.0f, by - third * 2.0f)); + draw_list->PathStroke(col, 0, thickness); +} + +void ImGui::RenderMouseCursor(ImDrawList* draw_list, ImVec2 pos, float scale, ImGuiMouseCursor mouse_cursor, ImU32 col_fill, ImU32 col_border, ImU32 col_shadow) +{ + if (mouse_cursor == ImGuiMouseCursor_None) + return; + IM_ASSERT(mouse_cursor > ImGuiMouseCursor_None && mouse_cursor < ImGuiMouseCursor_COUNT); + + ImFontAtlas* font_atlas = draw_list->_Data->Font->ContainerAtlas; + ImVec2 offset, size, uv[4]; + if (font_atlas->GetMouseCursorTexData(mouse_cursor, &offset, &size, &uv[0], &uv[2])) + { + pos -= offset; + ImTextureID tex_id = font_atlas->TexID; + draw_list->PushTextureID(tex_id); + draw_list->AddImage(tex_id, pos + ImVec2(1, 0) * scale, pos + (ImVec2(1, 0) + size) * scale, uv[2], uv[3], col_shadow); + draw_list->AddImage(tex_id, pos + ImVec2(2, 0) * scale, pos + (ImVec2(2, 0) + size) * scale, uv[2], uv[3], col_shadow); + draw_list->AddImage(tex_id, pos, pos + size * scale, uv[2], uv[3], col_border); + draw_list->AddImage(tex_id, pos, pos + size * scale, uv[0], uv[1], col_fill); + draw_list->PopTextureID(); + } +} + +// Render an arrow. 'pos' is position of the arrow tip. half_sz.x is length from base to tip. half_sz.y is length on each side. +void ImGui::RenderArrowPointingAt(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, ImGuiDir direction, ImU32 col) +{ + switch (direction) + { + case ImGuiDir_Left: draw_list->AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), pos, col); return; + case ImGuiDir_Right: draw_list->AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), pos, col); return; + case ImGuiDir_Up: draw_list->AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), pos, col); return; + case ImGuiDir_Down: draw_list->AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), pos, col); return; + case ImGuiDir_None: case ImGuiDir_COUNT: break; // Fix warnings + } +} + +static inline float ImAcos01(float x) +{ + if (x <= 0.0f) return IM_PI * 0.5f; + if (x >= 1.0f) return 0.0f; + return ImAcos(x); + //return (-0.69813170079773212f * x * x - 0.87266462599716477f) * x + 1.5707963267948966f; // Cheap approximation, may be enough for what we do. +} + +// FIXME: Cleanup and move code to ImDrawList. +void ImGui::RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding) +{ + if (x_end_norm == x_start_norm) + return; + if (x_start_norm > x_end_norm) + ImSwap(x_start_norm, x_end_norm); + + ImVec2 p0 = ImVec2(ImLerp(rect.Min.x, rect.Max.x, x_start_norm), rect.Min.y); + ImVec2 p1 = ImVec2(ImLerp(rect.Min.x, rect.Max.x, x_end_norm), rect.Max.y); + if (rounding == 0.0f) + { + draw_list->AddRectFilled(p0, p1, col, 0.0f); + return; + } + + rounding = ImClamp(ImMin((rect.Max.x - rect.Min.x) * 0.5f, (rect.Max.y - rect.Min.y) * 0.5f) - 1.0f, 0.0f, rounding); + const float inv_rounding = 1.0f / rounding; + const float arc0_b = ImAcos01(1.0f - (p0.x - rect.Min.x) * inv_rounding); + const float arc0_e = ImAcos01(1.0f - (p1.x - rect.Min.x) * inv_rounding); + const float half_pi = IM_PI * 0.5f; // We will == compare to this because we know this is the exact value ImAcos01 can return. + const float x0 = ImMax(p0.x, rect.Min.x + rounding); + if (arc0_b == arc0_e) + { + draw_list->PathLineTo(ImVec2(x0, p1.y)); + draw_list->PathLineTo(ImVec2(x0, p0.y)); + } + else if (arc0_b == 0.0f && arc0_e == half_pi) + { + draw_list->PathArcToFast(ImVec2(x0, p1.y - rounding), rounding, 3, 6); // BL + draw_list->PathArcToFast(ImVec2(x0, p0.y + rounding), rounding, 6, 9); // TR + } + else + { + draw_list->PathArcTo(ImVec2(x0, p1.y - rounding), rounding, IM_PI - arc0_e, IM_PI - arc0_b, 3); // BL + draw_list->PathArcTo(ImVec2(x0, p0.y + rounding), rounding, IM_PI + arc0_b, IM_PI + arc0_e, 3); // TR + } + if (p1.x > rect.Min.x + rounding) + { + const float arc1_b = ImAcos01(1.0f - (rect.Max.x - p1.x) * inv_rounding); + const float arc1_e = ImAcos01(1.0f - (rect.Max.x - p0.x) * inv_rounding); + const float x1 = ImMin(p1.x, rect.Max.x - rounding); + if (arc1_b == arc1_e) + { + draw_list->PathLineTo(ImVec2(x1, p0.y)); + draw_list->PathLineTo(ImVec2(x1, p1.y)); + } + else if (arc1_b == 0.0f && arc1_e == half_pi) + { + draw_list->PathArcToFast(ImVec2(x1, p0.y + rounding), rounding, 9, 12); // TR + draw_list->PathArcToFast(ImVec2(x1, p1.y - rounding), rounding, 0, 3); // BR + } + else + { + draw_list->PathArcTo(ImVec2(x1, p0.y + rounding), rounding, -arc1_e, -arc1_b, 3); // TR + draw_list->PathArcTo(ImVec2(x1, p1.y - rounding), rounding, +arc1_b, +arc1_e, 3); // BR + } + } + draw_list->PathFillConvex(col); +} + +void ImGui::RenderRectFilledWithHole(ImDrawList* draw_list, ImRect outer, ImRect inner, ImU32 col, float rounding) +{ + const bool fill_L = (inner.Min.x > outer.Min.x); + const bool fill_R = (inner.Max.x < outer.Max.x); + const bool fill_U = (inner.Min.y > outer.Min.y); + const bool fill_D = (inner.Max.y < outer.Max.y); + if (fill_L) draw_list->AddRectFilled(ImVec2(outer.Min.x, inner.Min.y), ImVec2(inner.Min.x, inner.Max.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_U ? 0 : ImDrawFlags_RoundCornersTopLeft) | (fill_D ? 0 : ImDrawFlags_RoundCornersBottomLeft)); + if (fill_R) draw_list->AddRectFilled(ImVec2(inner.Max.x, inner.Min.y), ImVec2(outer.Max.x, inner.Max.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_U ? 0 : ImDrawFlags_RoundCornersTopRight) | (fill_D ? 0 : ImDrawFlags_RoundCornersBottomRight)); + if (fill_U) draw_list->AddRectFilled(ImVec2(inner.Min.x, outer.Min.y), ImVec2(inner.Max.x, inner.Min.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_L ? 0 : ImDrawFlags_RoundCornersTopLeft) | (fill_R ? 0 : ImDrawFlags_RoundCornersTopRight)); + if (fill_D) draw_list->AddRectFilled(ImVec2(inner.Min.x, inner.Max.y), ImVec2(inner.Max.x, outer.Max.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_L ? 0 : ImDrawFlags_RoundCornersBottomLeft) | (fill_R ? 0 : ImDrawFlags_RoundCornersBottomRight)); + if (fill_L && fill_U) draw_list->AddRectFilled(ImVec2(outer.Min.x, outer.Min.y), ImVec2(inner.Min.x, inner.Min.y), col, rounding, ImDrawFlags_RoundCornersTopLeft); + if (fill_R && fill_U) draw_list->AddRectFilled(ImVec2(inner.Max.x, outer.Min.y), ImVec2(outer.Max.x, inner.Min.y), col, rounding, ImDrawFlags_RoundCornersTopRight); + if (fill_L && fill_D) draw_list->AddRectFilled(ImVec2(outer.Min.x, inner.Max.y), ImVec2(inner.Min.x, outer.Max.y), col, rounding, ImDrawFlags_RoundCornersBottomLeft); + if (fill_R && fill_D) draw_list->AddRectFilled(ImVec2(inner.Max.x, inner.Max.y), ImVec2(outer.Max.x, outer.Max.y), col, rounding, ImDrawFlags_RoundCornersBottomRight); +} + +// Helper for ColorPicker4() +// NB: This is rather brittle and will show artifact when rounding this enabled if rounded corners overlap multiple cells. Caller currently responsible for avoiding that. +// Spent a non reasonable amount of time trying to getting this right for ColorButton with rounding+anti-aliasing+ImGuiColorEditFlags_HalfAlphaPreview flag + various grid sizes and offsets, and eventually gave up... probably more reasonable to disable rounding altogether. +// FIXME: uses ImGui::GetColorU32 +void ImGui::RenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, ImVec2 p_min, ImVec2 p_max, ImU32 col, float grid_step, ImVec2 grid_off, float rounding, ImDrawFlags flags) +{ + if ((flags & ImDrawFlags_RoundCornersMask_) == 0) + flags = ImDrawFlags_RoundCornersDefault_; + if (((col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT) < 0xFF) + { + ImU32 col_bg1 = GetColorU32(ImAlphaBlendColors(IM_COL32(204, 204, 204, 255), col)); + ImU32 col_bg2 = GetColorU32(ImAlphaBlendColors(IM_COL32(128, 128, 128, 255), col)); + draw_list->AddRectFilled(p_min, p_max, col_bg1, rounding, flags); + + int yi = 0; + for (float y = p_min.y + grid_off.y; y < p_max.y; y += grid_step, yi++) + { + float y1 = ImClamp(y, p_min.y, p_max.y), y2 = ImMin(y + grid_step, p_max.y); + if (y2 <= y1) + continue; + for (float x = p_min.x + grid_off.x + (yi & 1) * grid_step; x < p_max.x; x += grid_step * 2.0f) + { + float x1 = ImClamp(x, p_min.x, p_max.x), x2 = ImMin(x + grid_step, p_max.x); + if (x2 <= x1) + continue; + ImDrawFlags cell_flags = ImDrawFlags_RoundCornersNone; + if (y1 <= p_min.y) { if (x1 <= p_min.x) cell_flags |= ImDrawFlags_RoundCornersTopLeft; if (x2 >= p_max.x) cell_flags |= ImDrawFlags_RoundCornersTopRight; } + if (y2 >= p_max.y) { if (x1 <= p_min.x) cell_flags |= ImDrawFlags_RoundCornersBottomLeft; if (x2 >= p_max.x) cell_flags |= ImDrawFlags_RoundCornersBottomRight; } + + // Combine flags + cell_flags = (flags == ImDrawFlags_RoundCornersNone || cell_flags == ImDrawFlags_RoundCornersNone) ? ImDrawFlags_RoundCornersNone : (cell_flags & flags); + draw_list->AddRectFilled(ImVec2(x1, y1), ImVec2(x2, y2), col_bg2, rounding, cell_flags); + } + } + } + else + { + draw_list->AddRectFilled(p_min, p_max, col, rounding, flags); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] Decompression code +//----------------------------------------------------------------------------- +// Compressed with stb_compress() then converted to a C array and encoded as base85. +// Use the program in misc/fonts/binary_to_compressed_c.cpp to create the array from a TTF file. +// The purpose of encoding as base85 instead of "0x00,0x01,..." style is only save on _source code_ size. +// Decompression from stb.h (public domain) by Sean Barrett https://github.com/nothings/stb/blob/master/stb.h +//----------------------------------------------------------------------------- + +static unsigned int stb_decompress_length(const unsigned char *input) +{ + return (input[8] << 24) + (input[9] << 16) + (input[10] << 8) + input[11]; +} + +static unsigned char *stb__barrier_out_e, *stb__barrier_out_b; +static const unsigned char *stb__barrier_in_b; +static unsigned char *stb__dout; +static void stb__match(const unsigned char *data, unsigned int length) +{ + // INVERSE of memmove... write each byte before copying the next... + IM_ASSERT(stb__dout + length <= stb__barrier_out_e); + if (stb__dout + length > stb__barrier_out_e) { stb__dout += length; return; } + if (data < stb__barrier_out_b) { stb__dout = stb__barrier_out_e+1; return; } + while (length--) *stb__dout++ = *data++; +} + +static void stb__lit(const unsigned char *data, unsigned int length) +{ + IM_ASSERT(stb__dout + length <= stb__barrier_out_e); + if (stb__dout + length > stb__barrier_out_e) { stb__dout += length; return; } + if (data < stb__barrier_in_b) { stb__dout = stb__barrier_out_e+1; return; } + memcpy(stb__dout, data, length); + stb__dout += length; +} + +#define stb__in2(x) ((i[x] << 8) + i[(x)+1]) +#define stb__in3(x) ((i[x] << 16) + stb__in2((x)+1)) +#define stb__in4(x) ((i[x] << 24) + stb__in3((x)+1)) + +static const unsigned char *stb_decompress_token(const unsigned char *i) +{ + if (*i >= 0x20) { // use fewer if's for cases that expand small + if (*i >= 0x80) stb__match(stb__dout-i[1]-1, i[0] - 0x80 + 1), i += 2; + else if (*i >= 0x40) stb__match(stb__dout-(stb__in2(0) - 0x4000 + 1), i[2]+1), i += 3; + else /* *i >= 0x20 */ stb__lit(i+1, i[0] - 0x20 + 1), i += 1 + (i[0] - 0x20 + 1); + } else { // more ifs for cases that expand large, since overhead is amortized + if (*i >= 0x18) stb__match(stb__dout-(stb__in3(0) - 0x180000 + 1), i[3]+1), i += 4; + else if (*i >= 0x10) stb__match(stb__dout-(stb__in3(0) - 0x100000 + 1), stb__in2(3)+1), i += 5; + else if (*i >= 0x08) stb__lit(i+2, stb__in2(0) - 0x0800 + 1), i += 2 + (stb__in2(0) - 0x0800 + 1); + else if (*i == 0x07) stb__lit(i+3, stb__in2(1) + 1), i += 3 + (stb__in2(1) + 1); + else if (*i == 0x06) stb__match(stb__dout-(stb__in3(1)+1), i[4]+1), i += 5; + else if (*i == 0x04) stb__match(stb__dout-(stb__in3(1)+1), stb__in2(4)+1), i += 6; + } + return i; +} + +static unsigned int stb_adler32(unsigned int adler32, unsigned char *buffer, unsigned int buflen) +{ + const unsigned long ADLER_MOD = 65521; + unsigned long s1 = adler32 & 0xffff, s2 = adler32 >> 16; + unsigned long blocklen = buflen % 5552; + + unsigned long i; + while (buflen) { + for (i=0; i + 7 < blocklen; i += 8) { + s1 += buffer[0], s2 += s1; + s1 += buffer[1], s2 += s1; + s1 += buffer[2], s2 += s1; + s1 += buffer[3], s2 += s1; + s1 += buffer[4], s2 += s1; + s1 += buffer[5], s2 += s1; + s1 += buffer[6], s2 += s1; + s1 += buffer[7], s2 += s1; + + buffer += 8; + } + + for (; i < blocklen; ++i) + s1 += *buffer++, s2 += s1; + + s1 %= ADLER_MOD, s2 %= ADLER_MOD; + buflen -= blocklen; + blocklen = 5552; + } + return (unsigned int)(s2 << 16) + (unsigned int)s1; +} + +static unsigned int stb_decompress(unsigned char *output, const unsigned char *i, unsigned int /*length*/) +{ + if (stb__in4(0) != 0x57bC0000) return 0; + if (stb__in4(4) != 0) return 0; // error! stream is > 4GB + const unsigned int olen = stb_decompress_length(i); + stb__barrier_in_b = i; + stb__barrier_out_e = output + olen; + stb__barrier_out_b = output; + i += 16; + + stb__dout = output; + for (;;) { + const unsigned char *old_i = i; + i = stb_decompress_token(i); + if (i == old_i) { + if (*i == 0x05 && i[1] == 0xfa) { + IM_ASSERT(stb__dout == output + olen); + if (stb__dout != output + olen) return 0; + if (stb_adler32(1, output, olen) != (unsigned int) stb__in4(2)) + return 0; + return olen; + } else { + IM_ASSERT(0); /* NOTREACHED */ + return 0; + } + } + IM_ASSERT(stb__dout <= output + olen); + if (stb__dout > output + olen) + return 0; + } +} + +//----------------------------------------------------------------------------- +// [SECTION] Default font data (ProggyClean.ttf) +//----------------------------------------------------------------------------- +// ProggyClean.ttf +// Copyright (c) 2004, 2005 Tristan Grimmer +// MIT license (see License.txt in http://www.upperbounds.net/download/ProggyClean.ttf.zip) +// Download and more information at http://upperbounds.net +//----------------------------------------------------------------------------- +// File: 'ProggyClean.ttf' (41208 bytes) +// Exported using misc/fonts/binary_to_compressed_c.cpp (with compression + base85 string encoding). +// The purpose of encoding as base85 instead of "0x00,0x01,..." style is only save on _source code_ size. +//----------------------------------------------------------------------------- +static const char proggy_clean_ttf_compressed_data_base85[11980 + 1] = + "7])#######hV0qs'/###[),##/l:$#Q6>##5[n42>c-TH`->>#/e>11NNV=Bv(*:.F?uu#(gRU.o0XGH`$vhLG1hxt9?W`#,5LsCp#-i>.r$<$6pD>Lb';9Crc6tgXmKVeU2cD4Eo3R/" + "2*>]b(MC;$jPfY.;h^`IWM9Qo#t'X#(v#Y9w0#1D$CIf;W'#pWUPXOuxXuU(H9M(1=Ke$$'5F%)]0^#0X@U.a$FBjVQTSDgEKnIS7EM9>ZY9w0#L;>>#Mx&4Mvt//L[MkA#W@lK.N'[0#7RL_&#w+F%HtG9M#XL`N&.,GM4Pg;--VsM.M0rJfLH2eTM`*oJMHRC`N" + "kfimM2J,W-jXS:)r0wK#@Fge$U>`w'N7G#$#fB#$E^$#:9:hk+eOe--6x)F7*E%?76%^GMHePW-Z5l'&GiF#$956:rS?dA#fiK:)Yr+`�j@'DbG&#^$PG.Ll+DNa&VZ>1i%h1S9u5o@YaaW$e+bROPOpxTO7Stwi1::iB1q)C_=dV26J;2,]7op$]uQr@_V7$q^%lQwtuHY]=DX,n3L#0PHDO4f9>dC@O>HBuKPpP*E,N+b3L#lpR/MrTEH.IAQk.a>D[.e;mc." + "x]Ip.PH^'/aqUO/$1WxLoW0[iLAw=4h(9.`G" + "CRUxHPeR`5Mjol(dUWxZa(>STrPkrJiWx`5U7F#.g*jrohGg`cg:lSTvEY/EV_7H4Q9[Z%cnv;JQYZ5q.l7Zeas:HOIZOB?Ggv:[7MI2k).'2($5FNP&EQ(,)" + "U]W]+fh18.vsai00);D3@4ku5P?DP8aJt+;qUM]=+b'8@;mViBKx0DE[-auGl8:PJ&Dj+M6OC]O^((##]`0i)drT;-7X`=-H3[igUnPG-NZlo.#k@h#=Ork$m>a>$-?Tm$UV(?#P6YY#" + "'/###xe7q.73rI3*pP/$1>s9)W,JrM7SN]'/4C#v$U`0#V.[0>xQsH$fEmPMgY2u7Kh(G%siIfLSoS+MK2eTM$=5,M8p`A.;_R%#u[K#$x4AG8.kK/HSB==-'Ie/QTtG?-.*^N-4B/ZM" + "_3YlQC7(p7q)&](`6_c)$/*JL(L-^(]$wIM`dPtOdGA,U3:w2M-0+WomX2u7lqM2iEumMTcsF?-aT=Z-97UEnXglEn1K-bnEO`gu" + "Ft(c%=;Am_Qs@jLooI&NX;]0#j4#F14;gl8-GQpgwhrq8'=l_f-b49'UOqkLu7-##oDY2L(te+Mch&gLYtJ,MEtJfLh'x'M=$CS-ZZ%P]8bZ>#S?YY#%Q&q'3^Fw&?D)UDNrocM3A76/" + "/oL?#h7gl85[qW/NDOk%16ij;+:1a'iNIdb-ou8.P*w,v5#EI$TWS>Pot-R*H'-SEpA:g)f+O$%%`kA#G=8RMmG1&O`>to8bC]T&$,n.LoO>29sp3dt-52U%VM#q7'DHpg+#Z9%H[Ket`e;)f#Km8&+DC$I46>#Kr]]u-[=99tts1.qb#q72g1WJO81q+eN'03'eM>&1XxY-caEnO" + "j%2n8)),?ILR5^.Ibn<-X-Mq7[a82Lq:F&#ce+S9wsCK*x`569E8ew'He]h:sI[2LM$[guka3ZRd6:t%IG:;$%YiJ:Nq=?eAw;/:nnDq0(CYcMpG)qLN4$##&J-XTt,%OVU4)S1+R-#dg0/Nn?Ku1^0f$B*P:Rowwm-`0PKjYDDM'3]d39VZHEl4,.j']Pk-M.h^&:0FACm$maq-&sgw0t7/6(^xtk%" + "LuH88Fj-ekm>GA#_>568x6(OFRl-IZp`&b,_P'$MhLbxfc$mj`,O;&%W2m`Zh:/)Uetw:aJ%]K9h:TcF]u_-Sj9,VK3M.*'&0D[Ca]J9gp8,kAW]" + "%(?A%R$f<->Zts'^kn=-^@c4%-pY6qI%J%1IGxfLU9CP8cbPlXv);C=b),<2mOvP8up,UVf3839acAWAW-W?#ao/^#%KYo8fRULNd2.>%m]UK:n%r$'sw]J;5pAoO_#2mO3n,'=H5(et" + "Hg*`+RLgv>=4U8guD$I%D:W>-r5V*%j*W:Kvej.Lp$'?;++O'>()jLR-^u68PHm8ZFWe+ej8h:9r6L*0//c&iH&R8pRbA#Kjm%upV1g:" + "a_#Ur7FuA#(tRh#.Y5K+@?3<-8m0$PEn;J:rh6?I6uG<-`wMU'ircp0LaE_OtlMb&1#6T.#FDKu#1Lw%u%+GM+X'e?YLfjM[VO0MbuFp7;>Q&#WIo)0@F%q7c#4XAXN-U&VBpqB>0ie&jhZ[?iLR@@_AvA-iQC(=ksRZRVp7`.=+NpBC%rh&3]R:8XDmE5^V8O(x<-+k?'(^](H.aREZSi,#1:[IXaZFOm<-ui#qUq2$##Ri;u75OK#(RtaW-K-F`S+cF]uN`-KMQ%rP/Xri.LRcB##=YL3BgM/3M" + "D?@f&1'BW-)Ju#bmmWCMkk&#TR`C,5d>g)F;t,4:@_l8G/5h4vUd%&%950:VXD'QdWoY-F$BtUwmfe$YqL'8(PWX(" + "P?^@Po3$##`MSs?DWBZ/S>+4%>fX,VWv/w'KD`LP5IbH;rTV>n3cEK8U#bX]l-/V+^lj3;vlMb&[5YQ8#pekX9JP3XUC72L,,?+Ni&co7ApnO*5NK,((W-i:$,kp'UDAO(G0Sq7MVjJs" + "bIu)'Z,*[>br5fX^:FPAWr-m2KgLQ_nN6'8uTGT5g)uLv:873UpTLgH+#FgpH'_o1780Ph8KmxQJ8#H72L4@768@Tm&Q" + "h4CB/5OvmA&,Q&QbUoi$a_%3M01H)4x7I^&KQVgtFnV+;[Pc>[m4k//,]1?#`VY[Jr*3&&slRfLiVZJ:]?=K3Sw=[$=uRB?3xk48@aege0jT6'N#(q%.O=?2S]u*(m<-" + "V8J'(1)G][68hW$5'q[GC&5j`TE?m'esFGNRM)j,ffZ?-qx8;->g4t*:CIP/[Qap7/9'#(1sao7w-.qNUdkJ)tCF&#B^;xGvn2r9FEPFFFcL@.iFNkTve$m%#QvQS8U@)2Z+3K:AKM5i" + "sZ88+dKQ)W6>J%CL`.d*(B`-n8D9oK-XV1q['-5k'cAZ69e;D_?$ZPP&s^+7])$*$#@QYi9,5P r+$%CE=68>K8r0=dSC%%(@p7" + ".m7jilQ02'0-VWAgTlGW'b)Tq7VT9q^*^$$.:&N@@" + "$&)WHtPm*5_rO0&e%K&#-30j(E4#'Zb.o/(Tpm$>K'f@[PvFl,hfINTNU6u'0pao7%XUp9]5.>%h`8_=VYbxuel.NTSsJfLacFu3B'lQSu/m6-Oqem8T+oE--$0a/k]uj9EwsG>%veR*" + "hv^BFpQj:K'#SJ,sB-'#](j.Lg92rTw-*n%@/;39rrJF,l#qV%OrtBeC6/,;qB3ebNW[?,Hqj2L.1NP&GjUR=1D8QaS3Up&@*9wP?+lo7b?@%'k4`p0Z$22%K3+iCZj?XJN4Nm&+YF]u" + "@-W$U%VEQ/,,>>#)D#%8cY#YZ?=,`Wdxu/ae&#" + "w6)R89tI#6@s'(6Bf7a&?S=^ZI_kS&ai`&=tE72L_D,;^R)7[$so8lKN%5/$(vdfq7+ebA#" + "u1p]ovUKW&Y%q]'>$1@-[xfn$7ZTp7mM,G,Ko7a&Gu%G[RMxJs[0MM%wci.LFDK)(%:_i2B5CsR8&9Z&#=mPEnm0f`<&c)QL5uJ#%u%lJj+D-r;BoFDoS97h5g)E#o:&S4weDF,9^Hoe`h*L+_a*NrLW-1pG_&2UdB8" + "6e%B/:=>)N4xeW.*wft-;$'58-ESqr#U`'6AQ]m&6/`Z>#S?YY#Vc;r7U2&326d=w&H####?TZ`*4?&.MK?LP8Vxg>$[QXc%QJv92.(Db*B)gb*BM9dM*hJMAo*c&#" + "b0v=Pjer]$gG&JXDf->'StvU7505l9$AFvgYRI^&<^b68?j#q9QX4SM'RO#&sL1IM.rJfLUAj221]d##DW=m83u5;'bYx,*Sl0hL(W;;$doB&O/TQ:(Z^xBdLjLV#*8U_72Lh+2Q8Cj0i:6hp&$C/:p(HK>T8Y[gHQ4`4)'$Ab(Nof%V'8hL&#SfD07&6D@M.*J:;$-rv29'M]8qMv-tLp,'886iaC=Hb*YJoKJ,(j%K=H`K.v9HggqBIiZu'QvBT.#=)0ukruV&.)3=(^1`o*Pj4<-#MJ+gLq9-##@HuZPN0]u:h7.T..G:;$/Usj(T7`Q8tT72LnYl<-qx8;-HV7Q-&Xdx%1a,hC=0u+HlsV>nuIQL-5" + "_>@kXQtMacfD.m-VAb8;IReM3$wf0''hra*so568'Ip&vRs849'MRYSp%:t:h5qSgwpEr$B>Q,;s(C#$)`svQuF$##-D,##,g68@2[T;.XSdN9Qe)rpt._K-#5wF)sP'##p#C0c%-Gb%" + "hd+<-j'Ai*x&&HMkT]C'OSl##5RG[JXaHN;d'uA#x._U;.`PU@(Z3dt4r152@:v,'R.Sj'w#0<-;kPI)FfJ&#AYJ&#//)>-k=m=*XnK$>=)72L]0I%>.G690a:$##<,);?;72#?x9+d;" + "^V'9;jY@;)br#q^YQpx:X#Te$Z^'=-=bGhLf:D6&bNwZ9-ZD#n^9HhLMr5G;']d&6'wYmTFmLq9wI>P(9mI[>kC-ekLC/R&CH+s'B;K-M6$EB%is00:" + "+A4[7xks.LrNk0&E)wILYF@2L'0Nb$+pv<(2.768/FrY&h$^3i&@+G%JT'<-,v`3;_)I9M^AE]CN?Cl2AZg+%4iTpT3$U4O]GKx'm9)b@p7YsvK3w^YR-" + "CdQ*:Ir<($u&)#(&?L9Rg3H)4fiEp^iI9O8KnTj,]H?D*r7'M;PwZ9K0E^k&-cpI;.p/6_vwoFMV<->#%Xi.LxVnrU(4&8/P+:hLSKj$#U%]49t'I:rgMi'FL@a:0Y-uA[39',(vbma*" + "hU%<-SRF`Tt:542R_VV$p@[p8DV[A,?1839FWdFTi1O*H&#(AL8[_P%.M>v^-))qOT*F5Cq0`Ye%+$B6i:7@0IXSsDiWP,##P`%/L-" + "S(qw%sf/@%#B6;/U7K]uZbi^Oc^2n%t<)'mEVE''n`WnJra$^TKvX5B>;_aSEK',(hwa0:i4G?.Bci.(X[?b*($,=-n<.Q%`(X=?+@Am*Js0&=3bh8K]mL69=Lb,OcZV/);TTm8VI;?%OtJ<(b4mq7M6:u?KRdFl*:xP?Yb.5)%w_I?7uk5JC+FS(m#i'k.'a0i)9<7b'fs'59hq$*5Uhv##pi^8+hIEBF`nvo`;'l0.^S1<-wUK2/Coh58KKhLj" + "M=SO*rfO`+qC`W-On.=AJ56>>i2@2LH6A:&5q`?9I3@@'04&p2/LVa*T-4<-i3;M9UvZd+N7>b*eIwg:CC)c<>nO&#$(>.Z-I&J(Q0Hd5Q%7Co-b`-cP)hI;*_F]u`Rb[.j8_Q/<&>uu+VsH$sM9TA%?)(vmJ80),P7E>)tjD%2L=-t#fK[%`v=Q8WlA2);Sa" + ">gXm8YB`1d@K#n]76-a$U,mF%Ul:#/'xoFM9QX-$.QN'>" + "[%$Z$uF6pA6Ki2O5:8w*vP1<-1`[G,)-m#>0`P&#eb#.3i)rtB61(o'$?X3B2Qft^ae_5tKL9MUe9b*sLEQ95C&`=G?@Mj=wh*'3E>=-<)Gt*Iw)'QG:`@I" + "wOf7&]1i'S01B+Ev/Nac#9S;=;YQpg_6U`*kVY39xK,[/6Aj7:'1Bm-_1EYfa1+o&o4hp7KN_Q(OlIo@S%;jVdn0'1h19w,WQhLI)3S#f$2(eb,jr*b;3Vw]*7NH%$c4Vs,eD9>XW8?N]o+(*pgC%/72LV-uW%iewS8W6m2rtCpo'RS1R84=@paTKt)>=%&1[)*vp'u+x,VrwN;&]kuO9JDbg=pO$J*.jVe;u'm0dr9l,<*wMK*Oe=g8lV_KEBFkO'oU]^=[-792#ok,)" + "i]lR8qQ2oA8wcRCZ^7w/Njh;?.stX?Q1>S1q4Bn$)K1<-rGdO'$Wr.Lc.CG)$/*JL4tNR/,SVO3,aUw'DJN:)Ss;wGn9A32ijw%FL+Z0Fn.U9;reSq)bmI32U==5ALuG&#Vf1398/pVo" + "1*c-(aY168o<`JsSbk-,1N;$>0:OUas(3:8Z972LSfF8eb=c-;>SPw7.6hn3m`9^Xkn(r.qS[0;T%&Qc=+STRxX'q1BNk3&*eu2;&8q$&x>Q#Q7^Tf+6<(d%ZVmj2bDi%.3L2n+4W'$P" + "iDDG)g,r%+?,$@?uou5tSe2aN_AQU*'IAO" + "URQ##V^Fv-XFbGM7Fl(N<3DhLGF%q.1rC$#:T__&Pi68%0xi_&[qFJ(77j_&JWoF.V735&T,[R*:xFR*K5>>#`bW-?4Ne_&6Ne_&6Ne_&n`kr-#GJcM6X;uM6X;uM(.a..^2TkL%oR(#" + ";u.T%fAr%4tJ8&><1=GHZ_+m9/#H1F^R#SC#*N=BA9(D?v[UiFY>>^8p,KKF.W]L29uLkLlu/+4T" + "w$)F./^n3+rlo+DB;5sIYGNk+i1t-69Jg--0pao7Sm#K)pdHW&;LuDNH@H>#/X-TI(;P>#,Gc>#0Su>#4`1?#8lC?#xL$#B.`$#F:r$#JF.%#NR@%#R_R%#Vke%#Zww%#_-4^Rh%Sflr-k'MS.o?.5/sWel/wpEM0%3'/1)K^f1-d>G21&v(35>V`39V7A4=onx4" + "A1OY5EI0;6Ibgr6M$HS7Q<)58C5w,;WoA*#[%T*#`1g*#d=#+#hI5+#lUG+#pbY+#tnl+#x$),#&1;,#*=M,#.I`,#2Ur,#6b.-#;w[H#iQtA#m^0B#qjBB#uvTB##-hB#'9$C#+E6C#" + "/QHC#3^ZC#7jmC#;v)D#?,)4kMYD4lVu`4m`:&5niUA5@(A5BA1]PBB:xlBCC=2CDLXMCEUtiCf&0g2'tN?PGT4CPGT4CPGT4CPGT4CPGT4CPGT4CPGT4CP" + "GT4CPGT4CPGT4CPGT4CPGT4CPGT4CP-qekC`.9kEg^+F$kwViFJTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5o,^<-28ZI'O?;xp" + "O?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xp;7q-#lLYI:xvD=#"; + +static const char* GetDefaultCompressedFontDataTTFBase85() +{ + return proggy_clean_ttf_compressed_data_base85; +} + +#endif // #ifndef IMGUI_DISABLE + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Private/imgui_tables.cpp`: + +```cpp +// dear imgui, v1.87 +// (tables and columns code) + +/* + +Index of this file: + +// [SECTION] Commentary +// [SECTION] Header mess +// [SECTION] Tables: Main code +// [SECTION] Tables: Simple accessors +// [SECTION] Tables: Row changes +// [SECTION] Tables: Columns changes +// [SECTION] Tables: Columns width management +// [SECTION] Tables: Drawing +// [SECTION] Tables: Sorting +// [SECTION] Tables: Headers +// [SECTION] Tables: Context Menu +// [SECTION] Tables: Settings (.ini data) +// [SECTION] Tables: Garbage Collection +// [SECTION] Tables: Debugging +// [SECTION] Columns, BeginColumns, EndColumns, etc. + +*/ + +// Navigating this file: +// - In Visual Studio IDE: CTRL+comma ("Edit.NavigateTo") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. + +//----------------------------------------------------------------------------- +// [SECTION] Commentary +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Typical tables call flow: (root level is generally public API): +//----------------------------------------------------------------------------- +// - BeginTable() user begin into a table +// | BeginChild() - (if ScrollX/ScrollY is set) +// | TableBeginInitMemory() - first time table is used +// | TableResetSettings() - on settings reset +// | TableLoadSettings() - on settings load +// | TableBeginApplyRequests() - apply queued resizing/reordering/hiding requests +// | - TableSetColumnWidth() - apply resizing width (for mouse resize, often requested by previous frame) +// | - TableUpdateColumnsWeightFromWidth()- recompute columns weights (of stretch columns) from their respective width +// - TableSetupColumn() user submit columns details (optional) +// - TableSetupScrollFreeze() user submit scroll freeze information (optional) +//----------------------------------------------------------------------------- +// - TableUpdateLayout() [Internal] followup to BeginTable(): setup everything: widths, columns positions, clipping rectangles. Automatically called by the FIRST call to TableNextRow() or TableHeadersRow(). +// | TableSetupDrawChannels() - setup ImDrawList channels +// | TableUpdateBorders() - detect hovering columns for resize, ahead of contents submission +// | TableDrawContextMenu() - draw right-click context menu +//----------------------------------------------------------------------------- +// - TableHeadersRow() or TableHeader() user submit a headers row (optional) +// | TableSortSpecsClickColumn() - when left-clicked: alter sort order and sort direction +// | TableOpenContextMenu() - when right-clicked: trigger opening of the default context menu +// - TableGetSortSpecs() user queries updated sort specs (optional, generally after submitting headers) +// - TableNextRow() user begin into a new row (also automatically called by TableHeadersRow()) +// | TableEndRow() - finish existing row +// | TableBeginRow() - add a new row +// - TableSetColumnIndex() / TableNextColumn() user begin into a cell +// | TableEndCell() - close existing column/cell +// | TableBeginCell() - enter into current column/cell +// - [...] user emit contents +//----------------------------------------------------------------------------- +// - EndTable() user ends the table +// | TableDrawBorders() - draw outer borders, inner vertical borders +// | TableMergeDrawChannels() - merge draw channels if clipping isn't required +// | EndChild() - (if ScrollX/ScrollY is set) +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// TABLE SIZING +//----------------------------------------------------------------------------- +// (Read carefully because this is subtle but it does make sense!) +//----------------------------------------------------------------------------- +// About 'outer_size': +// Its meaning needs to differ slightly depending on if we are using ScrollX/ScrollY flags. +// Default value is ImVec2(0.0f, 0.0f). +// X +// - outer_size.x <= 0.0f -> Right-align from window/work-rect right-most edge. With -FLT_MIN or 0.0f will align exactly on right-most edge. +// - outer_size.x > 0.0f -> Set Fixed width. +// Y with ScrollX/ScrollY disabled: we output table directly in current window +// - outer_size.y < 0.0f -> Bottom-align (but will auto extend, unless _NoHostExtendY is set). Not meaningful is parent window can vertically scroll. +// - outer_size.y = 0.0f -> No minimum height (but will auto extend, unless _NoHostExtendY is set) +// - outer_size.y > 0.0f -> Set Minimum height (but will auto extend, unless _NoHostExtenY is set) +// Y with ScrollX/ScrollY enabled: using a child window for scrolling +// - outer_size.y < 0.0f -> Bottom-align. Not meaningful is parent window can vertically scroll. +// - outer_size.y = 0.0f -> Bottom-align, consistent with BeginChild(). Not recommended unless table is last item in parent window. +// - outer_size.y > 0.0f -> Set Exact height. Recommended when using Scrolling on any axis. +//----------------------------------------------------------------------------- +// Outer size is also affected by the NoHostExtendX/NoHostExtendY flags. +// Important to that note how the two flags have slightly different behaviors! +// - ImGuiTableFlags_NoHostExtendX -> Make outer width auto-fit to columns (overriding outer_size.x value). Only available when ScrollX/ScrollY are disabled and Stretch columns are not used. +// - ImGuiTableFlags_NoHostExtendY -> Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit). Only available when ScrollX/ScrollY is disabled. Data below the limit will be clipped and not visible. +// In theory ImGuiTableFlags_NoHostExtendY could be the default and any non-scrolling tables with outer_size.y != 0.0f would use exact height. +// This would be consistent but perhaps less useful and more confusing (as vertically clipped items are not easily noticeable) +//----------------------------------------------------------------------------- +// About 'inner_width': +// With ScrollX disabled: +// - inner_width -> *ignored* +// With ScrollX enabled: +// - inner_width < 0.0f -> *illegal* fit in known width (right align from outer_size.x) <-- weird +// - inner_width = 0.0f -> fit in outer_width: Fixed size columns will take space they need (if avail, otherwise shrink down), Stretch columns becomes Fixed columns. +// - inner_width > 0.0f -> override scrolling width, generally to be larger than outer_size.x. Fixed column take space they need (if avail, otherwise shrink down), Stretch columns share remaining space! +//----------------------------------------------------------------------------- +// Details: +// - If you want to use Stretch columns with ScrollX, you generally need to specify 'inner_width' otherwise the concept +// of "available space" doesn't make sense. +// - Even if not really useful, we allow 'inner_width < outer_size.x' for consistency and to facilitate understanding +// of what the value does. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// COLUMNS SIZING POLICIES +//----------------------------------------------------------------------------- +// About overriding column sizing policy and width/weight with TableSetupColumn(): +// We use a default parameter of 'init_width_or_weight == -1'. +// - with ImGuiTableColumnFlags_WidthFixed, init_width <= 0 (default) --> width is automatic +// - with ImGuiTableColumnFlags_WidthFixed, init_width > 0 (explicit) --> width is custom +// - with ImGuiTableColumnFlags_WidthStretch, init_weight <= 0 (default) --> weight is 1.0f +// - with ImGuiTableColumnFlags_WidthStretch, init_weight > 0 (explicit) --> weight is custom +// Widths are specified _without_ CellPadding. If you specify a width of 100.0f, the column will be cover (100.0f + Padding * 2.0f) +// and you can fit a 100.0f wide item in it without clipping and with full padding. +//----------------------------------------------------------------------------- +// About default sizing policy (if you don't specify a ImGuiTableColumnFlags_WidthXXXX flag) +// - with Table policy ImGuiTableFlags_SizingFixedFit --> default Column policy is ImGuiTableColumnFlags_WidthFixed, default Width is equal to contents width +// - with Table policy ImGuiTableFlags_SizingFixedSame --> default Column policy is ImGuiTableColumnFlags_WidthFixed, default Width is max of all contents width +// - with Table policy ImGuiTableFlags_SizingStretchSame --> default Column policy is ImGuiTableColumnFlags_WidthStretch, default Weight is 1.0f +// - with Table policy ImGuiTableFlags_SizingStretchWeight --> default Column policy is ImGuiTableColumnFlags_WidthStretch, default Weight is proportional to contents +// Default Width and default Weight can be overridden when calling TableSetupColumn(). +//----------------------------------------------------------------------------- +// About mixing Fixed/Auto and Stretch columns together: +// - the typical use of mixing sizing policies is: any number of LEADING Fixed columns, followed by one or two TRAILING Stretch columns. +// - using mixed policies with ScrollX does not make much sense, as using Stretch columns with ScrollX does not make much sense in the first place! +// that is, unless 'inner_width' is passed to BeginTable() to explicitly provide a total width to layout columns in. +// - when using ImGuiTableFlags_SizingFixedSame with mixed columns, only the Fixed/Auto columns will match their widths to the width of the maximum contents. +// - when using ImGuiTableFlags_SizingStretchSame with mixed columns, only the Stretch columns will match their weight/widths. +//----------------------------------------------------------------------------- +// About using column width: +// If a column is manual resizable or has a width specified with TableSetupColumn(): +// - you may use GetContentRegionAvail().x to query the width available in a given column. +// - right-side alignment features such as SetNextItemWidth(-x) or PushItemWidth(-x) will rely on this width. +// If the column is not resizable and has no width specified with TableSetupColumn(): +// - its width will be automatic and be set to the max of items submitted. +// - therefore you generally cannot have ALL items of the columns use e.g. SetNextItemWidth(-FLT_MIN). +// - but if the column has one or more items of known/fixed size, this will become the reference width used by SetNextItemWidth(-FLT_MIN). +//----------------------------------------------------------------------------- + + +//----------------------------------------------------------------------------- +// TABLES CLIPPING/CULLING +//----------------------------------------------------------------------------- +// About clipping/culling of Rows in Tables: +// - For large numbers of rows, it is recommended you use ImGuiListClipper to only submit visible rows. +// ImGuiListClipper is reliant on the fact that rows are of equal height. +// See 'Demo->Tables->Vertical Scrolling' or 'Demo->Tables->Advanced' for a demo of using the clipper. +// - Note that auto-resizing columns don't play well with using the clipper. +// By default a table with _ScrollX but without _Resizable will have column auto-resize. +// So, if you want to use the clipper, make sure to either enable _Resizable, either setup columns width explicitly with _WidthFixed. +//----------------------------------------------------------------------------- +// About clipping/culling of Columns in Tables: +// - Both TableSetColumnIndex() and TableNextColumn() return true when the column is visible or performing +// width measurements. Otherwise, you may skip submitting the contents of a cell/column, BUT ONLY if you know +// it is not going to contribute to row height. +// In many situations, you may skip submitting contents for every column but one (e.g. the first one). +// - Case A: column is not hidden by user, and at least partially in sight (most common case). +// - Case B: column is clipped / out of sight (because of scrolling or parent ClipRect): TableNextColumn() return false as a hint but we still allow layout output. +// - Case C: column is hidden explicitly by the user (e.g. via the context menu, or _DefaultHide column flag, etc.). +// +// [A] [B] [C] +// TableNextColumn(): true false false -> [userland] when TableNextColumn() / TableSetColumnIndex() return false, user can skip submitting items but only if the column doesn't contribute to row height. +// SkipItems: false false true -> [internal] when SkipItems is true, most widgets will early out if submitted, resulting is no layout output. +// ClipRect: normal zero-width zero-width -> [internal] when ClipRect is zero, ItemAdd() will return false and most widgets will early out mid-way. +// ImDrawList output: normal dummy dummy -> [internal] when using the dummy channel, ImDrawList submissions (if any) will be wasted (because cliprect is zero-width anyway). +// +// - We need to distinguish those cases because non-hidden columns that are clipped outside of scrolling bounds should still contribute their height to the row. +// However, in the majority of cases, the contribution to row height is the same for all columns, or the tallest cells are known by the programmer. +//----------------------------------------------------------------------------- +// About clipping/culling of whole Tables: +// - Scrolling tables with a known outer size can be clipped earlier as BeginTable() will return false. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// [SECTION] Header mess +//----------------------------------------------------------------------------- + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include "imgui_internal.h" + +// System includes +#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier +#include // intptr_t +#else +#include // intptr_t +#endif + +// Visual Studio warnings +#ifdef _MSC_VER +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types +#endif +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. +#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') +#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#elif defined(__GNUC__) +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Tables: Main code +//----------------------------------------------------------------------------- +// - TableFixFlags() [Internal] +// - TableFindByID() [Internal] +// - BeginTable() +// - BeginTableEx() [Internal] +// - TableBeginInitMemory() [Internal] +// - TableBeginApplyRequests() [Internal] +// - TableSetupColumnFlags() [Internal] +// - TableUpdateLayout() [Internal] +// - TableUpdateBorders() [Internal] +// - EndTable() +// - TableSetupColumn() +// - TableSetupScrollFreeze() +//----------------------------------------------------------------------------- + +// Configuration +static const int TABLE_DRAW_CHANNEL_BG0 = 0; +static const int TABLE_DRAW_CHANNEL_BG2_FROZEN = 1; +static const int TABLE_DRAW_CHANNEL_NOCLIP = 2; // When using ImGuiTableFlags_NoClip (this becomes the last visible channel) +static const float TABLE_BORDER_SIZE = 1.0f; // FIXME-TABLE: Currently hard-coded because of clipping assumptions with outer borders rendering. +static const float TABLE_RESIZE_SEPARATOR_HALF_THICKNESS = 4.0f; // Extend outside inner borders. +static const float TABLE_RESIZE_SEPARATOR_FEEDBACK_TIMER = 0.06f; // Delay/timer before making the hover feedback (color+cursor) visible because tables/columns tends to be more cramped. + +// Helper +inline ImGuiTableFlags TableFixFlags(ImGuiTableFlags flags, ImGuiWindow* outer_window) +{ + // Adjust flags: set default sizing policy + if ((flags & ImGuiTableFlags_SizingMask_) == 0) + flags |= ((flags & ImGuiTableFlags_ScrollX) || (outer_window->Flags & ImGuiWindowFlags_AlwaysAutoResize)) ? ImGuiTableFlags_SizingFixedFit : ImGuiTableFlags_SizingStretchSame; + + // Adjust flags: enable NoKeepColumnsVisible when using ImGuiTableFlags_SizingFixedSame + if ((flags & ImGuiTableFlags_SizingMask_) == ImGuiTableFlags_SizingFixedSame) + flags |= ImGuiTableFlags_NoKeepColumnsVisible; + + // Adjust flags: enforce borders when resizable + if (flags & ImGuiTableFlags_Resizable) + flags |= ImGuiTableFlags_BordersInnerV; + + // Adjust flags: disable NoHostExtendX/NoHostExtendY if we have any scrolling going on + if (flags & (ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) + flags &= ~(ImGuiTableFlags_NoHostExtendX | ImGuiTableFlags_NoHostExtendY); + + // Adjust flags: NoBordersInBodyUntilResize takes priority over NoBordersInBody + if (flags & ImGuiTableFlags_NoBordersInBodyUntilResize) + flags &= ~ImGuiTableFlags_NoBordersInBody; + + // Adjust flags: disable saved settings if there's nothing to save + if ((flags & (ImGuiTableFlags_Resizable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Sortable)) == 0) + flags |= ImGuiTableFlags_NoSavedSettings; + + // Inherit _NoSavedSettings from top-level window (child windows always have _NoSavedSettings set) + if (outer_window->RootWindow->Flags & ImGuiWindowFlags_NoSavedSettings) + flags |= ImGuiTableFlags_NoSavedSettings; + + return flags; +} + +ImGuiTable* ImGui::TableFindByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + return g.Tables.GetByKey(id); +} + +// Read about "TABLE SIZING" at the top of this file. +bool ImGui::BeginTable(const char* str_id, int columns_count, ImGuiTableFlags flags, const ImVec2& outer_size, float inner_width) +{ + ImGuiID id = GetID(str_id); + return BeginTableEx(str_id, id, columns_count, flags, outer_size, inner_width); +} + +bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImGuiTableFlags flags, const ImVec2& outer_size, float inner_width) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* outer_window = GetCurrentWindow(); + if (outer_window->SkipItems) // Consistent with other tables + beneficial side effect that assert on miscalling EndTable() will be more visible. + return false; + + // Sanity checks + IM_ASSERT(columns_count > 0 && columns_count <= IMGUI_TABLE_MAX_COLUMNS && "Only 1..64 columns allowed!"); + if (flags & ImGuiTableFlags_ScrollX) + IM_ASSERT(inner_width >= 0.0f); + + // If an outer size is specified ahead we will be able to early out when not visible. Exact clipping rules may evolve. + const bool use_child_window = (flags & (ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) != 0; + const ImVec2 avail_size = GetContentRegionAvail(); + ImVec2 actual_outer_size = CalcItemSize(outer_size, ImMax(avail_size.x, 1.0f), use_child_window ? ImMax(avail_size.y, 1.0f) : 0.0f); + ImRect outer_rect(outer_window->DC.CursorPos, outer_window->DC.CursorPos + actual_outer_size); + if (use_child_window && IsClippedEx(outer_rect, 0)) + { + ItemSize(outer_rect); + return false; + } + + // Acquire storage for the table + ImGuiTable* table = g.Tables.GetOrAddByKey(id); + const int instance_no = (table->LastFrameActive != g.FrameCount) ? 0 : table->InstanceCurrent + 1; + const ImGuiID instance_id = id + instance_no; + const ImGuiTableFlags table_last_flags = table->Flags; + if (instance_no > 0) + IM_ASSERT(table->ColumnsCount == columns_count && "BeginTable(): Cannot change columns count mid-frame while preserving same ID"); + + // Acquire temporary buffers + const int table_idx = g.Tables.GetIndex(table); + if (++g.TablesTempDataStacked > g.TablesTempData.Size) + g.TablesTempData.resize(g.TablesTempDataStacked, ImGuiTableTempData()); + ImGuiTableTempData* temp_data = table->TempData = &g.TablesTempData[g.TablesTempDataStacked - 1]; + temp_data->TableIndex = table_idx; + table->DrawSplitter = &table->TempData->DrawSplitter; + table->DrawSplitter->Clear(); + + // Fix flags + table->IsDefaultSizingPolicy = (flags & ImGuiTableFlags_SizingMask_) == 0; + flags = TableFixFlags(flags, outer_window); + + // Initialize + table->ID = id; + table->Flags = flags; + table->InstanceCurrent = (ImS16)instance_no; + table->LastFrameActive = g.FrameCount; + table->OuterWindow = table->InnerWindow = outer_window; + table->ColumnsCount = columns_count; + table->IsLayoutLocked = false; + table->InnerWidth = inner_width; + temp_data->UserOuterSize = outer_size; + + // When not using a child window, WorkRect.Max will grow as we append contents. + if (use_child_window) + { + // Ensure no vertical scrollbar appears if we only want horizontal one, to make flag consistent + // (we have no other way to disable vertical scrollbar of a window while keeping the horizontal one showing) + ImVec2 override_content_size(FLT_MAX, FLT_MAX); + if ((flags & ImGuiTableFlags_ScrollX) && !(flags & ImGuiTableFlags_ScrollY)) + override_content_size.y = FLT_MIN; + + // Ensure specified width (when not specified, Stretched columns will act as if the width == OuterWidth and + // never lead to any scrolling). We don't handle inner_width < 0.0f, we could potentially use it to right-align + // based on the right side of the child window work rect, which would require knowing ahead if we are going to + // have decoration taking horizontal spaces (typically a vertical scrollbar). + if ((flags & ImGuiTableFlags_ScrollX) && inner_width > 0.0f) + override_content_size.x = inner_width; + + if (override_content_size.x != FLT_MAX || override_content_size.y != FLT_MAX) + SetNextWindowContentSize(ImVec2(override_content_size.x != FLT_MAX ? override_content_size.x : 0.0f, override_content_size.y != FLT_MAX ? override_content_size.y : 0.0f)); + + // Reset scroll if we are reactivating it + if ((table_last_flags & (ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) == 0) + SetNextWindowScroll(ImVec2(0.0f, 0.0f)); + + // Create scrolling region (without border and zero window padding) + ImGuiWindowFlags child_flags = (flags & ImGuiTableFlags_ScrollX) ? ImGuiWindowFlags_HorizontalScrollbar : ImGuiWindowFlags_None; + BeginChildEx(name, instance_id, outer_rect.GetSize(), false, child_flags); + table->InnerWindow = g.CurrentWindow; + table->WorkRect = table->InnerWindow->WorkRect; + table->OuterRect = table->InnerWindow->Rect(); + table->InnerRect = table->InnerWindow->InnerRect; + IM_ASSERT(table->InnerWindow->WindowPadding.x == 0.0f && table->InnerWindow->WindowPadding.y == 0.0f && table->InnerWindow->WindowBorderSize == 0.0f); + } + else + { + // For non-scrolling tables, WorkRect == OuterRect == InnerRect. + // But at this point we do NOT have a correct value for .Max.y (unless a height has been explicitly passed in). It will only be updated in EndTable(). + table->WorkRect = table->OuterRect = table->InnerRect = outer_rect; + } + + // Push a standardized ID for both child-using and not-child-using tables + PushOverrideID(instance_id); + + // Backup a copy of host window members we will modify + ImGuiWindow* inner_window = table->InnerWindow; + table->HostIndentX = inner_window->DC.Indent.x; + table->HostClipRect = inner_window->ClipRect; + table->HostSkipItems = inner_window->SkipItems; + temp_data->HostBackupWorkRect = inner_window->WorkRect; + temp_data->HostBackupParentWorkRect = inner_window->ParentWorkRect; + temp_data->HostBackupColumnsOffset = outer_window->DC.ColumnsOffset; + temp_data->HostBackupPrevLineSize = inner_window->DC.PrevLineSize; + temp_data->HostBackupCurrLineSize = inner_window->DC.CurrLineSize; + temp_data->HostBackupCursorMaxPos = inner_window->DC.CursorMaxPos; + temp_data->HostBackupItemWidth = outer_window->DC.ItemWidth; + temp_data->HostBackupItemWidthStackSize = outer_window->DC.ItemWidthStack.Size; + inner_window->DC.PrevLineSize = inner_window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); + + // Padding and Spacing + // - None ........Content..... Pad .....Content........ + // - PadOuter | Pad ..Content..... Pad .....Content.. Pad | + // - PadInner ........Content.. Pad | Pad ..Content........ + // - PadOuter+PadInner | Pad ..Content.. Pad | Pad ..Content.. Pad | + const bool pad_outer_x = (flags & ImGuiTableFlags_NoPadOuterX) ? false : (flags & ImGuiTableFlags_PadOuterX) ? true : (flags & ImGuiTableFlags_BordersOuterV) != 0; + const bool pad_inner_x = (flags & ImGuiTableFlags_NoPadInnerX) ? false : true; + const float inner_spacing_for_border = (flags & ImGuiTableFlags_BordersInnerV) ? TABLE_BORDER_SIZE : 0.0f; + const float inner_spacing_explicit = (pad_inner_x && (flags & ImGuiTableFlags_BordersInnerV) == 0) ? g.Style.CellPadding.x : 0.0f; + const float inner_padding_explicit = (pad_inner_x && (flags & ImGuiTableFlags_BordersInnerV) != 0) ? g.Style.CellPadding.x : 0.0f; + table->CellSpacingX1 = inner_spacing_explicit + inner_spacing_for_border; + table->CellSpacingX2 = inner_spacing_explicit; + table->CellPaddingX = inner_padding_explicit; + table->CellPaddingY = g.Style.CellPadding.y; + + const float outer_padding_for_border = (flags & ImGuiTableFlags_BordersOuterV) ? TABLE_BORDER_SIZE : 0.0f; + const float outer_padding_explicit = pad_outer_x ? g.Style.CellPadding.x : 0.0f; + table->OuterPaddingX = (outer_padding_for_border + outer_padding_explicit) - table->CellPaddingX; + + table->CurrentColumn = -1; + table->CurrentRow = -1; + table->RowBgColorCounter = 0; + table->LastRowFlags = ImGuiTableRowFlags_None; + table->InnerClipRect = (inner_window == outer_window) ? table->WorkRect : inner_window->ClipRect; + table->InnerClipRect.ClipWith(table->WorkRect); // We need this to honor inner_width + table->InnerClipRect.ClipWithFull(table->HostClipRect); + table->InnerClipRect.Max.y = (flags & ImGuiTableFlags_NoHostExtendY) ? ImMin(table->InnerClipRect.Max.y, inner_window->WorkRect.Max.y) : inner_window->ClipRect.Max.y; + + table->RowPosY1 = table->RowPosY2 = table->WorkRect.Min.y; // This is needed somehow + table->RowTextBaseline = 0.0f; // This will be cleared again by TableBeginRow() + table->FreezeRowsRequest = table->FreezeRowsCount = 0; // This will be setup by TableSetupScrollFreeze(), if any + table->FreezeColumnsRequest = table->FreezeColumnsCount = 0; + table->IsUnfrozenRows = true; + table->DeclColumnsCount = 0; + + // Using opaque colors facilitate overlapping elements of the grid + table->BorderColorStrong = GetColorU32(ImGuiCol_TableBorderStrong); + table->BorderColorLight = GetColorU32(ImGuiCol_TableBorderLight); + + // Make table current + g.CurrentTable = table; + outer_window->DC.CurrentTableIdx = table_idx; + if (inner_window != outer_window) // So EndChild() within the inner window can restore the table properly. + inner_window->DC.CurrentTableIdx = table_idx; + + if ((table_last_flags & ImGuiTableFlags_Reorderable) && (flags & ImGuiTableFlags_Reorderable) == 0) + table->IsResetDisplayOrderRequest = true; + + // Mark as used + if (table_idx >= g.TablesLastTimeActive.Size) + g.TablesLastTimeActive.resize(table_idx + 1, -1.0f); + g.TablesLastTimeActive[table_idx] = (float)g.Time; + temp_data->LastTimeActive = (float)g.Time; + table->MemoryCompacted = false; + + // Setup memory buffer (clear data if columns count changed) + ImGuiTableColumn* old_columns_to_preserve = NULL; + void* old_columns_raw_data = NULL; + const int old_columns_count = table->Columns.size(); + if (old_columns_count != 0 && old_columns_count != columns_count) + { + // Attempt to preserve width on column count change (#4046) + old_columns_to_preserve = table->Columns.Data; + old_columns_raw_data = table->RawData; + table->RawData = NULL; + } + if (table->RawData == NULL) + { + TableBeginInitMemory(table, columns_count); + table->IsInitializing = table->IsSettingsRequestLoad = true; + } + if (table->IsResetAllRequest) + TableResetSettings(table); + if (table->IsInitializing) + { + // Initialize + table->SettingsOffset = -1; + table->IsSortSpecsDirty = true; + table->InstanceInteracted = -1; + table->ContextPopupColumn = -1; + table->ReorderColumn = table->ResizedColumn = table->LastResizedColumn = -1; + table->AutoFitSingleColumn = -1; + table->HoveredColumnBody = table->HoveredColumnBorder = -1; + for (int n = 0; n < columns_count; n++) + { + ImGuiTableColumn* column = &table->Columns[n]; + if (old_columns_to_preserve && n < old_columns_count) + { + // FIXME: We don't attempt to preserve column order in this path. + *column = old_columns_to_preserve[n]; + } + else + { + float width_auto = column->WidthAuto; + *column = ImGuiTableColumn(); + column->WidthAuto = width_auto; + column->IsPreserveWidthAuto = true; // Preserve WidthAuto when reinitializing a live table: not technically necessary but remove a visible flicker + column->IsEnabled = column->IsUserEnabled = column->IsUserEnabledNextFrame = true; + } + column->DisplayOrder = table->DisplayOrderToIndex[n] = (ImGuiTableColumnIdx)n; + } + } + if (old_columns_raw_data) + IM_FREE(old_columns_raw_data); + + // Load settings + if (table->IsSettingsRequestLoad) + TableLoadSettings(table); + + // Handle DPI/font resize + // This is designed to facilitate DPI changes with the assumption that e.g. style.CellPadding has been scaled as well. + // It will also react to changing fonts with mixed results. It doesn't need to be perfect but merely provide a decent transition. + // FIXME-DPI: Provide consistent standards for reference size. Perhaps using g.CurrentDpiScale would be more self explanatory. + // This is will lead us to non-rounded WidthRequest in columns, which should work but is a poorly tested path. + const float new_ref_scale_unit = g.FontSize; // g.Font->GetCharAdvance('A') ? + if (table->RefScale != 0.0f && table->RefScale != new_ref_scale_unit) + { + const float scale_factor = new_ref_scale_unit / table->RefScale; + //IMGUI_DEBUG_LOG("[table] %08X RefScaleUnit %.3f -> %.3f, scaling width by %.3f\n", table->ID, table->RefScaleUnit, new_ref_scale_unit, scale_factor); + for (int n = 0; n < columns_count; n++) + table->Columns[n].WidthRequest = table->Columns[n].WidthRequest * scale_factor; + } + table->RefScale = new_ref_scale_unit; + + // Disable output until user calls TableNextRow() or TableNextColumn() leading to the TableUpdateLayout() call.. + // This is not strictly necessary but will reduce cases were "out of table" output will be misleading to the user. + // Because we cannot safely assert in EndTable() when no rows have been created, this seems like our best option. + inner_window->SkipItems = true; + + // Clear names + // At this point the ->NameOffset field of each column will be invalid until TableUpdateLayout() or the first call to TableSetupColumn() + if (table->ColumnsNames.Buf.Size > 0) + table->ColumnsNames.Buf.resize(0); + + // Apply queued resizing/reordering/hiding requests + TableBeginApplyRequests(table); + + return true; +} + +// For reference, the average total _allocation count_ for a table is: +// + 0 (for ImGuiTable instance, we are pooling allocations in g.Tables) +// + 1 (for table->RawData allocated below) +// + 1 (for table->ColumnsNames, if names are used) +// Shared allocations per number of nested tables +// + 1 (for table->Splitter._Channels) +// + 2 * active_channels_count (for ImDrawCmd and ImDrawIdx buffers inside channels) +// Where active_channels_count is variable but often == columns_count or columns_count + 1, see TableSetupDrawChannels() for details. +// Unused channels don't perform their +2 allocations. +void ImGui::TableBeginInitMemory(ImGuiTable* table, int columns_count) +{ + // Allocate single buffer for our arrays + ImSpanAllocator<3> span_allocator; + span_allocator.Reserve(0, columns_count * sizeof(ImGuiTableColumn)); + span_allocator.Reserve(1, columns_count * sizeof(ImGuiTableColumnIdx)); + span_allocator.Reserve(2, columns_count * sizeof(ImGuiTableCellData), 4); + table->RawData = IM_ALLOC(span_allocator.GetArenaSizeInBytes()); + memset(table->RawData, 0, span_allocator.GetArenaSizeInBytes()); + span_allocator.SetArenaBasePtr(table->RawData); + span_allocator.GetSpan(0, &table->Columns); + span_allocator.GetSpan(1, &table->DisplayOrderToIndex); + span_allocator.GetSpan(2, &table->RowCellData); +} + +// Apply queued resizing/reordering/hiding requests +void ImGui::TableBeginApplyRequests(ImGuiTable* table) +{ + // Handle resizing request + // (We process this at the first TableBegin of the frame) + // FIXME-TABLE: Contains columns if our work area doesn't allow for scrolling? + if (table->InstanceCurrent == 0) + { + if (table->ResizedColumn != -1 && table->ResizedColumnNextWidth != FLT_MAX) + TableSetColumnWidth(table->ResizedColumn, table->ResizedColumnNextWidth); + table->LastResizedColumn = table->ResizedColumn; + table->ResizedColumnNextWidth = FLT_MAX; + table->ResizedColumn = -1; + + // Process auto-fit for single column, which is a special case for stretch columns and fixed columns with FixedSame policy. + // FIXME-TABLE: Would be nice to redistribute available stretch space accordingly to other weights, instead of giving it all to siblings. + if (table->AutoFitSingleColumn != -1) + { + TableSetColumnWidth(table->AutoFitSingleColumn, table->Columns[table->AutoFitSingleColumn].WidthAuto); + table->AutoFitSingleColumn = -1; + } + } + + // Handle reordering request + // Note: we don't clear ReorderColumn after handling the request. + if (table->InstanceCurrent == 0) + { + if (table->HeldHeaderColumn == -1 && table->ReorderColumn != -1) + table->ReorderColumn = -1; + table->HeldHeaderColumn = -1; + if (table->ReorderColumn != -1 && table->ReorderColumnDir != 0) + { + // We need to handle reordering across hidden columns. + // In the configuration below, moving C to the right of E will lead to: + // ... C [D] E ---> ... [D] E C (Column name/index) + // ... 2 3 4 ... 2 3 4 (Display order) + const int reorder_dir = table->ReorderColumnDir; + IM_ASSERT(reorder_dir == -1 || reorder_dir == +1); + IM_ASSERT(table->Flags & ImGuiTableFlags_Reorderable); + ImGuiTableColumn* src_column = &table->Columns[table->ReorderColumn]; + ImGuiTableColumn* dst_column = &table->Columns[(reorder_dir == -1) ? src_column->PrevEnabledColumn : src_column->NextEnabledColumn]; + IM_UNUSED(dst_column); + const int src_order = src_column->DisplayOrder; + const int dst_order = dst_column->DisplayOrder; + src_column->DisplayOrder = (ImGuiTableColumnIdx)dst_order; + for (int order_n = src_order + reorder_dir; order_n != dst_order + reorder_dir; order_n += reorder_dir) + table->Columns[table->DisplayOrderToIndex[order_n]].DisplayOrder -= (ImGuiTableColumnIdx)reorder_dir; + IM_ASSERT(dst_column->DisplayOrder == dst_order - reorder_dir); + + // Display order is stored in both columns->IndexDisplayOrder and table->DisplayOrder[], + // rebuild the later from the former. + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + table->DisplayOrderToIndex[table->Columns[column_n].DisplayOrder] = (ImGuiTableColumnIdx)column_n; + table->ReorderColumnDir = 0; + table->IsSettingsDirty = true; + } + } + + // Handle display order reset request + if (table->IsResetDisplayOrderRequest) + { + for (int n = 0; n < table->ColumnsCount; n++) + table->DisplayOrderToIndex[n] = table->Columns[n].DisplayOrder = (ImGuiTableColumnIdx)n; + table->IsResetDisplayOrderRequest = false; + table->IsSettingsDirty = true; + } +} + +// Adjust flags: default width mode + stretch columns are not allowed when auto extending +static void TableSetupColumnFlags(ImGuiTable* table, ImGuiTableColumn* column, ImGuiTableColumnFlags flags_in) +{ + ImGuiTableColumnFlags flags = flags_in; + + // Sizing Policy + if ((flags & ImGuiTableColumnFlags_WidthMask_) == 0) + { + const ImGuiTableFlags table_sizing_policy = (table->Flags & ImGuiTableFlags_SizingMask_); + if (table_sizing_policy == ImGuiTableFlags_SizingFixedFit || table_sizing_policy == ImGuiTableFlags_SizingFixedSame) + flags |= ImGuiTableColumnFlags_WidthFixed; + else + flags |= ImGuiTableColumnFlags_WidthStretch; + } + else + { + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiTableColumnFlags_WidthMask_)); // Check that only 1 of each set is used. + } + + // Resize + if ((table->Flags & ImGuiTableFlags_Resizable) == 0) + flags |= ImGuiTableColumnFlags_NoResize; + + // Sorting + if ((flags & ImGuiTableColumnFlags_NoSortAscending) && (flags & ImGuiTableColumnFlags_NoSortDescending)) + flags |= ImGuiTableColumnFlags_NoSort; + + // Indentation + if ((flags & ImGuiTableColumnFlags_IndentMask_) == 0) + flags |= (table->Columns.index_from_ptr(column) == 0) ? ImGuiTableColumnFlags_IndentEnable : ImGuiTableColumnFlags_IndentDisable; + + // Alignment + //if ((flags & ImGuiTableColumnFlags_AlignMask_) == 0) + // flags |= ImGuiTableColumnFlags_AlignCenter; + //IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiTableColumnFlags_AlignMask_)); // Check that only 1 of each set is used. + + // Preserve status flags + column->Flags = flags | (column->Flags & ImGuiTableColumnFlags_StatusMask_); + + // Build an ordered list of available sort directions + column->SortDirectionsAvailCount = column->SortDirectionsAvailMask = column->SortDirectionsAvailList = 0; + if (table->Flags & ImGuiTableFlags_Sortable) + { + int count = 0, mask = 0, list = 0; + if ((flags & ImGuiTableColumnFlags_PreferSortAscending) != 0 && (flags & ImGuiTableColumnFlags_NoSortAscending) == 0) { mask |= 1 << ImGuiSortDirection_Ascending; list |= ImGuiSortDirection_Ascending << (count << 1); count++; } + if ((flags & ImGuiTableColumnFlags_PreferSortDescending) != 0 && (flags & ImGuiTableColumnFlags_NoSortDescending) == 0) { mask |= 1 << ImGuiSortDirection_Descending; list |= ImGuiSortDirection_Descending << (count << 1); count++; } + if ((flags & ImGuiTableColumnFlags_PreferSortAscending) == 0 && (flags & ImGuiTableColumnFlags_NoSortAscending) == 0) { mask |= 1 << ImGuiSortDirection_Ascending; list |= ImGuiSortDirection_Ascending << (count << 1); count++; } + if ((flags & ImGuiTableColumnFlags_PreferSortDescending) == 0 && (flags & ImGuiTableColumnFlags_NoSortDescending) == 0) { mask |= 1 << ImGuiSortDirection_Descending; list |= ImGuiSortDirection_Descending << (count << 1); count++; } + if ((table->Flags & ImGuiTableFlags_SortTristate) || count == 0) { mask |= 1 << ImGuiSortDirection_None; count++; } + column->SortDirectionsAvailList = (ImU8)list; + column->SortDirectionsAvailMask = (ImU8)mask; + column->SortDirectionsAvailCount = (ImU8)count; + ImGui::TableFixColumnSortDirection(table, column); + } +} + +// Layout columns for the frame. This is in essence the followup to BeginTable(). +// Runs on the first call to TableNextRow(), to give a chance for TableSetupColumn() to be called first. +// FIXME-TABLE: Our width (and therefore our WorkRect) will be minimal in the first frame for _WidthAuto columns. +// Increase feedback side-effect with widgets relying on WorkRect.Max.x... Maybe provide a default distribution for _WidthAuto columns? +void ImGui::TableUpdateLayout(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(table->IsLayoutLocked == false); + + const ImGuiTableFlags table_sizing_policy = (table->Flags & ImGuiTableFlags_SizingMask_); + table->IsDefaultDisplayOrder = true; + table->ColumnsEnabledCount = 0; + table->EnabledMaskByIndex = 0x00; + table->EnabledMaskByDisplayOrder = 0x00; + table->LeftMostEnabledColumn = -1; + table->MinColumnWidth = ImMax(1.0f, g.Style.FramePadding.x * 1.0f); // g.Style.ColumnsMinSpacing; // FIXME-TABLE + + // [Part 1] Apply/lock Enabled and Order states. Calculate auto/ideal width for columns. Count fixed/stretch columns. + // Process columns in their visible orders as we are building the Prev/Next indices. + int count_fixed = 0; // Number of columns that have fixed sizing policies + int count_stretch = 0; // Number of columns that have stretch sizing policies + int prev_visible_column_idx = -1; + bool has_auto_fit_request = false; + bool has_resizable = false; + float stretch_sum_width_auto = 0.0f; + float fixed_max_width_auto = 0.0f; + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + const int column_n = table->DisplayOrderToIndex[order_n]; + if (column_n != order_n) + table->IsDefaultDisplayOrder = false; + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Clear column setup if not submitted by user. Currently we make it mandatory to call TableSetupColumn() every frame. + // It would easily work without but we're not ready to guarantee it since e.g. names need resubmission anyway. + // We take a slight shortcut but in theory we could be calling TableSetupColumn() here with dummy values, it should yield the same effect. + if (table->DeclColumnsCount <= column_n) + { + TableSetupColumnFlags(table, column, ImGuiTableColumnFlags_None); + column->NameOffset = -1; + column->UserID = 0; + column->InitStretchWeightOrWidth = -1.0f; + } + + // Update Enabled state, mark settings and sort specs dirty + if (!(table->Flags & ImGuiTableFlags_Hideable) || (column->Flags & ImGuiTableColumnFlags_NoHide)) + column->IsUserEnabledNextFrame = true; + if (column->IsUserEnabled != column->IsUserEnabledNextFrame) + { + column->IsUserEnabled = column->IsUserEnabledNextFrame; + table->IsSettingsDirty = true; + } + column->IsEnabled = column->IsUserEnabled && (column->Flags & ImGuiTableColumnFlags_Disabled) == 0; + + if (column->SortOrder != -1 && !column->IsEnabled) + table->IsSortSpecsDirty = true; + if (column->SortOrder > 0 && !(table->Flags & ImGuiTableFlags_SortMulti)) + table->IsSortSpecsDirty = true; + + // Auto-fit unsized columns + const bool start_auto_fit = (column->Flags & ImGuiTableColumnFlags_WidthFixed) ? (column->WidthRequest < 0.0f) : (column->StretchWeight < 0.0f); + if (start_auto_fit) + column->AutoFitQueue = column->CannotSkipItemsQueue = (1 << 3) - 1; // Fit for three frames + + if (!column->IsEnabled) + { + column->IndexWithinEnabledSet = -1; + continue; + } + + // Mark as enabled and link to previous/next enabled column + column->PrevEnabledColumn = (ImGuiTableColumnIdx)prev_visible_column_idx; + column->NextEnabledColumn = -1; + if (prev_visible_column_idx != -1) + table->Columns[prev_visible_column_idx].NextEnabledColumn = (ImGuiTableColumnIdx)column_n; + else + table->LeftMostEnabledColumn = (ImGuiTableColumnIdx)column_n; + column->IndexWithinEnabledSet = table->ColumnsEnabledCount++; + table->EnabledMaskByIndex |= (ImU64)1 << column_n; + table->EnabledMaskByDisplayOrder |= (ImU64)1 << column->DisplayOrder; + prev_visible_column_idx = column_n; + IM_ASSERT(column->IndexWithinEnabledSet <= column->DisplayOrder); + + // Calculate ideal/auto column width (that's the width required for all contents to be visible without clipping) + // Combine width from regular rows + width from headers unless requested not to. + if (!column->IsPreserveWidthAuto) + column->WidthAuto = TableGetColumnWidthAuto(table, column); + + // Non-resizable columns keep their requested width (apply user value regardless of IsPreserveWidthAuto) + const bool column_is_resizable = (column->Flags & ImGuiTableColumnFlags_NoResize) == 0; + if (column_is_resizable) + has_resizable = true; + if ((column->Flags & ImGuiTableColumnFlags_WidthFixed) && column->InitStretchWeightOrWidth > 0.0f && !column_is_resizable) + column->WidthAuto = column->InitStretchWeightOrWidth; + + if (column->AutoFitQueue != 0x00) + has_auto_fit_request = true; + if (column->Flags & ImGuiTableColumnFlags_WidthStretch) + { + stretch_sum_width_auto += column->WidthAuto; + count_stretch++; + } + else + { + fixed_max_width_auto = ImMax(fixed_max_width_auto, column->WidthAuto); + count_fixed++; + } + } + if ((table->Flags & ImGuiTableFlags_Sortable) && table->SortSpecsCount == 0 && !(table->Flags & ImGuiTableFlags_SortTristate)) + table->IsSortSpecsDirty = true; + table->RightMostEnabledColumn = (ImGuiTableColumnIdx)prev_visible_column_idx; + IM_ASSERT(table->LeftMostEnabledColumn >= 0 && table->RightMostEnabledColumn >= 0); + + // [Part 2] Disable child window clipping while fitting columns. This is not strictly necessary but makes it possible + // to avoid the column fitting having to wait until the first visible frame of the child container (may or not be a good thing). + // FIXME-TABLE: for always auto-resizing columns may not want to do that all the time. + if (has_auto_fit_request && table->OuterWindow != table->InnerWindow) + table->InnerWindow->SkipItems = false; + if (has_auto_fit_request) + table->IsSettingsDirty = true; + + // [Part 3] Fix column flags and record a few extra information. + float sum_width_requests = 0.0f; // Sum of all width for fixed and auto-resize columns, excluding width contributed by Stretch columns but including spacing/padding. + float stretch_sum_weights = 0.0f; // Sum of all weights for stretch columns. + table->LeftMostStretchedColumn = table->RightMostStretchedColumn = -1; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + if (!(table->EnabledMaskByIndex & ((ImU64)1 << column_n))) + continue; + ImGuiTableColumn* column = &table->Columns[column_n]; + + const bool column_is_resizable = (column->Flags & ImGuiTableColumnFlags_NoResize) == 0; + if (column->Flags & ImGuiTableColumnFlags_WidthFixed) + { + // Apply same widths policy + float width_auto = column->WidthAuto; + if (table_sizing_policy == ImGuiTableFlags_SizingFixedSame && (column->AutoFitQueue != 0x00 || !column_is_resizable)) + width_auto = fixed_max_width_auto; + + // Apply automatic width + // Latch initial size for fixed columns and update it constantly for auto-resizing column (unless clipped!) + if (column->AutoFitQueue != 0x00) + column->WidthRequest = width_auto; + else if ((column->Flags & ImGuiTableColumnFlags_WidthFixed) && !column_is_resizable && (table->RequestOutputMaskByIndex & ((ImU64)1 << column_n))) + column->WidthRequest = width_auto; + + // FIXME-TABLE: Increase minimum size during init frame to avoid biasing auto-fitting widgets + // (e.g. TextWrapped) too much. Otherwise what tends to happen is that TextWrapped would output a very + // large height (= first frame scrollbar display very off + clipper would skip lots of items). + // This is merely making the side-effect less extreme, but doesn't properly fixes it. + // FIXME: Move this to ->WidthGiven to avoid temporary lossyless? + // FIXME: This break IsPreserveWidthAuto from not flickering if the stored WidthAuto was smaller. + if (column->AutoFitQueue > 0x01 && table->IsInitializing && !column->IsPreserveWidthAuto) + column->WidthRequest = ImMax(column->WidthRequest, table->MinColumnWidth * 4.0f); // FIXME-TABLE: Another constant/scale? + sum_width_requests += column->WidthRequest; + } + else + { + // Initialize stretch weight + if (column->AutoFitQueue != 0x00 || column->StretchWeight < 0.0f || !column_is_resizable) + { + if (column->InitStretchWeightOrWidth > 0.0f) + column->StretchWeight = column->InitStretchWeightOrWidth; + else if (table_sizing_policy == ImGuiTableFlags_SizingStretchProp) + column->StretchWeight = (column->WidthAuto / stretch_sum_width_auto) * count_stretch; + else + column->StretchWeight = 1.0f; + } + + stretch_sum_weights += column->StretchWeight; + if (table->LeftMostStretchedColumn == -1 || table->Columns[table->LeftMostStretchedColumn].DisplayOrder > column->DisplayOrder) + table->LeftMostStretchedColumn = (ImGuiTableColumnIdx)column_n; + if (table->RightMostStretchedColumn == -1 || table->Columns[table->RightMostStretchedColumn].DisplayOrder < column->DisplayOrder) + table->RightMostStretchedColumn = (ImGuiTableColumnIdx)column_n; + } + column->IsPreserveWidthAuto = false; + sum_width_requests += table->CellPaddingX * 2.0f; + } + table->ColumnsEnabledFixedCount = (ImGuiTableColumnIdx)count_fixed; + + // [Part 4] Apply final widths based on requested widths + const ImRect work_rect = table->WorkRect; + const float width_spacings = (table->OuterPaddingX * 2.0f) + (table->CellSpacingX1 + table->CellSpacingX2) * (table->ColumnsEnabledCount - 1); + const float width_avail = ((table->Flags & ImGuiTableFlags_ScrollX) && table->InnerWidth == 0.0f) ? table->InnerClipRect.GetWidth() : work_rect.GetWidth(); + const float width_avail_for_stretched_columns = width_avail - width_spacings - sum_width_requests; + float width_remaining_for_stretched_columns = width_avail_for_stretched_columns; + table->ColumnsGivenWidth = width_spacings + (table->CellPaddingX * 2.0f) * table->ColumnsEnabledCount; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + if (!(table->EnabledMaskByIndex & ((ImU64)1 << column_n))) + continue; + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Allocate width for stretched/weighted columns (StretchWeight gets converted into WidthRequest) + if (column->Flags & ImGuiTableColumnFlags_WidthStretch) + { + float weight_ratio = column->StretchWeight / stretch_sum_weights; + column->WidthRequest = IM_FLOOR(ImMax(width_avail_for_stretched_columns * weight_ratio, table->MinColumnWidth) + 0.01f); + width_remaining_for_stretched_columns -= column->WidthRequest; + } + + // [Resize Rule 1] The right-most Visible column is not resizable if there is at least one Stretch column + // See additional comments in TableSetColumnWidth(). + if (column->NextEnabledColumn == -1 && table->LeftMostStretchedColumn != -1) + column->Flags |= ImGuiTableColumnFlags_NoDirectResize_; + + // Assign final width, record width in case we will need to shrink + column->WidthGiven = ImFloor(ImMax(column->WidthRequest, table->MinColumnWidth)); + table->ColumnsGivenWidth += column->WidthGiven; + } + + // [Part 5] Redistribute stretch remainder width due to rounding (remainder width is < 1.0f * number of Stretch column). + // Using right-to-left distribution (more likely to match resizing cursor). + if (width_remaining_for_stretched_columns >= 1.0f && !(table->Flags & ImGuiTableFlags_PreciseWidths)) + for (int order_n = table->ColumnsCount - 1; stretch_sum_weights > 0.0f && width_remaining_for_stretched_columns >= 1.0f && order_n >= 0; order_n--) + { + if (!(table->EnabledMaskByDisplayOrder & ((ImU64)1 << order_n))) + continue; + ImGuiTableColumn* column = &table->Columns[table->DisplayOrderToIndex[order_n]]; + if (!(column->Flags & ImGuiTableColumnFlags_WidthStretch)) + continue; + column->WidthRequest += 1.0f; + column->WidthGiven += 1.0f; + width_remaining_for_stretched_columns -= 1.0f; + } + + table->HoveredColumnBody = -1; + table->HoveredColumnBorder = -1; + const ImRect mouse_hit_rect(table->OuterRect.Min.x, table->OuterRect.Min.y, table->OuterRect.Max.x, ImMax(table->OuterRect.Max.y, table->OuterRect.Min.y + table->LastOuterHeight)); + const bool is_hovering_table = ItemHoverable(mouse_hit_rect, 0); + + // [Part 6] Setup final position, offset, skip/clip states and clipping rectangles, detect hovered column + // Process columns in their visible orders as we are comparing the visible order and adjusting host_clip_rect while looping. + int visible_n = 0; + bool offset_x_frozen = (table->FreezeColumnsCount > 0); + float offset_x = ((table->FreezeColumnsCount > 0) ? table->OuterRect.Min.x : work_rect.Min.x) + table->OuterPaddingX - table->CellSpacingX1; + ImRect host_clip_rect = table->InnerClipRect; + //host_clip_rect.Max.x += table->CellPaddingX + table->CellSpacingX2; + table->VisibleMaskByIndex = 0x00; + table->RequestOutputMaskByIndex = 0x00; + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + const int column_n = table->DisplayOrderToIndex[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + + column->NavLayerCurrent = (ImS8)((table->FreezeRowsCount > 0 || column_n < table->FreezeColumnsCount) ? ImGuiNavLayer_Menu : ImGuiNavLayer_Main); + + if (offset_x_frozen && table->FreezeColumnsCount == visible_n) + { + offset_x += work_rect.Min.x - table->OuterRect.Min.x; + offset_x_frozen = false; + } + + // Clear status flags + column->Flags &= ~ImGuiTableColumnFlags_StatusMask_; + + if ((table->EnabledMaskByDisplayOrder & ((ImU64)1 << order_n)) == 0) + { + // Hidden column: clear a few fields and we are done with it for the remainder of the function. + // We set a zero-width clip rect but set Min.y/Max.y properly to not interfere with the clipper. + column->MinX = column->MaxX = column->WorkMinX = column->ClipRect.Min.x = column->ClipRect.Max.x = offset_x; + column->WidthGiven = 0.0f; + column->ClipRect.Min.y = work_rect.Min.y; + column->ClipRect.Max.y = FLT_MAX; + column->ClipRect.ClipWithFull(host_clip_rect); + column->IsVisibleX = column->IsVisibleY = column->IsRequestOutput = false; + column->IsSkipItems = true; + column->ItemWidth = 1.0f; + continue; + } + + // Detect hovered column + if (is_hovering_table && g.IO.MousePos.x >= column->ClipRect.Min.x && g.IO.MousePos.x < column->ClipRect.Max.x) + table->HoveredColumnBody = (ImGuiTableColumnIdx)column_n; + + // Lock start position + column->MinX = offset_x; + + // Lock width based on start position and minimum/maximum width for this position + float max_width = TableGetMaxColumnWidth(table, column_n); + column->WidthGiven = ImMin(column->WidthGiven, max_width); + column->WidthGiven = ImMax(column->WidthGiven, ImMin(column->WidthRequest, table->MinColumnWidth)); + column->MaxX = offset_x + column->WidthGiven + table->CellSpacingX1 + table->CellSpacingX2 + table->CellPaddingX * 2.0f; + + // Lock other positions + // - ClipRect.Min.x: Because merging draw commands doesn't compare min boundaries, we make ClipRect.Min.x match left bounds to be consistent regardless of merging. + // - ClipRect.Max.x: using WorkMaxX instead of MaxX (aka including padding) makes things more consistent when resizing down, tho slightly detrimental to visibility in very-small column. + // - ClipRect.Max.x: using MaxX makes it easier for header to receive hover highlight with no discontinuity and display sorting arrow. + // - FIXME-TABLE: We want equal width columns to have equal (ClipRect.Max.x - WorkMinX) width, which means ClipRect.max.x cannot stray off host_clip_rect.Max.x else right-most column may appear shorter. + column->WorkMinX = column->MinX + table->CellPaddingX + table->CellSpacingX1; + column->WorkMaxX = column->MaxX - table->CellPaddingX - table->CellSpacingX2; // Expected max + column->ItemWidth = ImFloor(column->WidthGiven * 0.65f); + column->ClipRect.Min.x = column->MinX; + column->ClipRect.Min.y = work_rect.Min.y; + column->ClipRect.Max.x = column->MaxX; //column->WorkMaxX; + column->ClipRect.Max.y = FLT_MAX; + column->ClipRect.ClipWithFull(host_clip_rect); + + // Mark column as Clipped (not in sight) + // Note that scrolling tables (where inner_window != outer_window) handle Y clipped earlier in BeginTable() so IsVisibleY really only applies to non-scrolling tables. + // FIXME-TABLE: Because InnerClipRect.Max.y is conservatively ==outer_window->ClipRect.Max.y, we never can mark columns _Above_ the scroll line as not IsVisibleY. + // Taking advantage of LastOuterHeight would yield good results there... + // FIXME-TABLE: Y clipping is disabled because it effectively means not submitting will reduce contents width which is fed to outer_window->DC.CursorMaxPos.x, + // and this may be used (e.g. typically by outer_window using AlwaysAutoResize or outer_window's horizontal scrollbar, but could be something else). + // Possible solution to preserve last known content width for clipped column. Test 'table_reported_size' fails when enabling Y clipping and window is resized small. + column->IsVisibleX = (column->ClipRect.Max.x > column->ClipRect.Min.x); + column->IsVisibleY = true; // (column->ClipRect.Max.y > column->ClipRect.Min.y); + const bool is_visible = column->IsVisibleX; //&& column->IsVisibleY; + if (is_visible) + table->VisibleMaskByIndex |= ((ImU64)1 << column_n); + + // Mark column as requesting output from user. Note that fixed + non-resizable sets are auto-fitting at all times and therefore always request output. + column->IsRequestOutput = is_visible || column->AutoFitQueue != 0 || column->CannotSkipItemsQueue != 0; + if (column->IsRequestOutput) + table->RequestOutputMaskByIndex |= ((ImU64)1 << column_n); + + // Mark column as SkipItems (ignoring all items/layout) + column->IsSkipItems = !column->IsEnabled || table->HostSkipItems; + if (column->IsSkipItems) + IM_ASSERT(!is_visible); + + // Update status flags + column->Flags |= ImGuiTableColumnFlags_IsEnabled; + if (is_visible) + column->Flags |= ImGuiTableColumnFlags_IsVisible; + if (column->SortOrder != -1) + column->Flags |= ImGuiTableColumnFlags_IsSorted; + if (table->HoveredColumnBody == column_n) + column->Flags |= ImGuiTableColumnFlags_IsHovered; + + // Alignment + // FIXME-TABLE: This align based on the whole column width, not per-cell, and therefore isn't useful in + // many cases (to be able to honor this we might be able to store a log of cells width, per row, for + // visible rows, but nav/programmatic scroll would have visible artifacts.) + //if (column->Flags & ImGuiTableColumnFlags_AlignRight) + // column->WorkMinX = ImMax(column->WorkMinX, column->MaxX - column->ContentWidthRowsUnfrozen); + //else if (column->Flags & ImGuiTableColumnFlags_AlignCenter) + // column->WorkMinX = ImLerp(column->WorkMinX, ImMax(column->StartX, column->MaxX - column->ContentWidthRowsUnfrozen), 0.5f); + + // Reset content width variables + column->ContentMaxXFrozen = column->ContentMaxXUnfrozen = column->WorkMinX; + column->ContentMaxXHeadersUsed = column->ContentMaxXHeadersIdeal = column->WorkMinX; + + // Don't decrement auto-fit counters until container window got a chance to submit its items + if (table->HostSkipItems == false) + { + column->AutoFitQueue >>= 1; + column->CannotSkipItemsQueue >>= 1; + } + + if (visible_n < table->FreezeColumnsCount) + host_clip_rect.Min.x = ImClamp(column->MaxX + TABLE_BORDER_SIZE, host_clip_rect.Min.x, host_clip_rect.Max.x); + + offset_x += column->WidthGiven + table->CellSpacingX1 + table->CellSpacingX2 + table->CellPaddingX * 2.0f; + visible_n++; + } + + // [Part 7] Detect/store when we are hovering the unused space after the right-most column (so e.g. context menus can react on it) + // Clear Resizable flag if none of our column are actually resizable (either via an explicit _NoResize flag, either + // because of using _WidthAuto/_WidthStretch). This will hide the resizing option from the context menu. + const float unused_x1 = ImMax(table->WorkRect.Min.x, table->Columns[table->RightMostEnabledColumn].ClipRect.Max.x); + if (is_hovering_table && table->HoveredColumnBody == -1) + { + if (g.IO.MousePos.x >= unused_x1) + table->HoveredColumnBody = (ImGuiTableColumnIdx)table->ColumnsCount; + } + if (has_resizable == false && (table->Flags & ImGuiTableFlags_Resizable)) + table->Flags &= ~ImGuiTableFlags_Resizable; + + // [Part 8] Lock actual OuterRect/WorkRect right-most position. + // This is done late to handle the case of fixed-columns tables not claiming more widths that they need. + // Because of this we are careful with uses of WorkRect and InnerClipRect before this point. + if (table->RightMostStretchedColumn != -1) + table->Flags &= ~ImGuiTableFlags_NoHostExtendX; + if (table->Flags & ImGuiTableFlags_NoHostExtendX) + { + table->OuterRect.Max.x = table->WorkRect.Max.x = unused_x1; + table->InnerClipRect.Max.x = ImMin(table->InnerClipRect.Max.x, unused_x1); + } + table->InnerWindow->ParentWorkRect = table->WorkRect; + table->BorderX1 = table->InnerClipRect.Min.x;// +((table->Flags & ImGuiTableFlags_BordersOuter) ? 0.0f : -1.0f); + table->BorderX2 = table->InnerClipRect.Max.x;// +((table->Flags & ImGuiTableFlags_BordersOuter) ? 0.0f : +1.0f); + + // [Part 9] Allocate draw channels and setup background cliprect + TableSetupDrawChannels(table); + + // [Part 10] Hit testing on borders + if (table->Flags & ImGuiTableFlags_Resizable) + TableUpdateBorders(table); + table->LastFirstRowHeight = 0.0f; + table->IsLayoutLocked = true; + table->IsUsingHeaders = false; + + // [Part 11] Context menu + if (table->IsContextPopupOpen && table->InstanceCurrent == table->InstanceInteracted) + { + const ImGuiID context_menu_id = ImHashStr("##ContextMenu", 0, table->ID); + if (BeginPopupEx(context_menu_id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings)) + { + TableDrawContextMenu(table); + EndPopup(); + } + else + { + table->IsContextPopupOpen = false; + } + } + + // [Part 13] Sanitize and build sort specs before we have a change to use them for display. + // This path will only be exercised when sort specs are modified before header rows (e.g. init or visibility change) + if (table->IsSortSpecsDirty && (table->Flags & ImGuiTableFlags_Sortable)) + TableSortSpecsBuild(table); + + // Initial state + ImGuiWindow* inner_window = table->InnerWindow; + if (table->Flags & ImGuiTableFlags_NoClip) + table->DrawSplitter->SetCurrentChannel(inner_window->DrawList, TABLE_DRAW_CHANNEL_NOCLIP); + else + inner_window->DrawList->PushClipRect(inner_window->ClipRect.Min, inner_window->ClipRect.Max, false); +} + +// Process hit-testing on resizing borders. Actual size change will be applied in EndTable() +// - Set table->HoveredColumnBorder with a short delay/timer to reduce feedback noise +// - Submit ahead of table contents and header, use ImGuiButtonFlags_AllowItemOverlap to prioritize widgets +// overlapping the same area. +void ImGui::TableUpdateBorders(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(table->Flags & ImGuiTableFlags_Resizable); + + // At this point OuterRect height may be zero or under actual final height, so we rely on temporal coherency and + // use the final height from last frame. Because this is only affecting _interaction_ with columns, it is not + // really problematic (whereas the actual visual will be displayed in EndTable() and using the current frame height). + // Actual columns highlight/render will be performed in EndTable() and not be affected. + const float hit_half_width = TABLE_RESIZE_SEPARATOR_HALF_THICKNESS; + const float hit_y1 = table->OuterRect.Min.y; + const float hit_y2_body = ImMax(table->OuterRect.Max.y, hit_y1 + table->LastOuterHeight); + const float hit_y2_head = hit_y1 + table->LastFirstRowHeight; + + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + if (!(table->EnabledMaskByDisplayOrder & ((ImU64)1 << order_n))) + continue; + + const int column_n = table->DisplayOrderToIndex[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->Flags & (ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_NoDirectResize_)) + continue; + + // ImGuiTableFlags_NoBordersInBodyUntilResize will be honored in TableDrawBorders() + const float border_y2_hit = (table->Flags & ImGuiTableFlags_NoBordersInBody) ? hit_y2_head : hit_y2_body; + if ((table->Flags & ImGuiTableFlags_NoBordersInBody) && table->IsUsingHeaders == false) + continue; + + if (!column->IsVisibleX && table->LastResizedColumn != column_n) + continue; + + ImGuiID column_id = TableGetColumnResizeID(table, column_n, table->InstanceCurrent); + ImRect hit_rect(column->MaxX - hit_half_width, hit_y1, column->MaxX + hit_half_width, border_y2_hit); + //GetForegroundDrawList()->AddRect(hit_rect.Min, hit_rect.Max, IM_COL32(255, 0, 0, 100)); + KeepAliveID(column_id); + + bool hovered = false, held = false; + bool pressed = ButtonBehavior(hit_rect, column_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnDoubleClick | ImGuiButtonFlags_NoNavFocus); + if (pressed && IsMouseDoubleClicked(0)) + { + TableSetColumnWidthAutoSingle(table, column_n); + ClearActiveID(); + held = hovered = false; + } + if (held) + { + if (table->LastResizedColumn == -1) + table->ResizeLockMinContentsX2 = table->RightMostEnabledColumn != -1 ? table->Columns[table->RightMostEnabledColumn].MaxX : -FLT_MAX; + table->ResizedColumn = (ImGuiTableColumnIdx)column_n; + table->InstanceInteracted = table->InstanceCurrent; + } + if ((hovered && g.HoveredIdTimer > TABLE_RESIZE_SEPARATOR_FEEDBACK_TIMER) || held) + { + table->HoveredColumnBorder = (ImGuiTableColumnIdx)column_n; + SetMouseCursor(ImGuiMouseCursor_ResizeEW); + } + } +} + +void ImGui::EndTable() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL && "Only call EndTable() if BeginTable() returns true!"); + + // This assert would be very useful to catch a common error... unfortunately it would probably trigger in some + // cases, and for consistency user may sometimes output empty tables (and still benefit from e.g. outer border) + //IM_ASSERT(table->IsLayoutLocked && "Table unused: never called TableNextRow(), is that the intent?"); + + // If the user never got to call TableNextRow() or TableNextColumn(), we call layout ourselves to ensure all our + // code paths are consistent (instead of just hoping that TableBegin/TableEnd will work), get borders drawn, etc. + if (!table->IsLayoutLocked) + TableUpdateLayout(table); + + const ImGuiTableFlags flags = table->Flags; + ImGuiWindow* inner_window = table->InnerWindow; + ImGuiWindow* outer_window = table->OuterWindow; + ImGuiTableTempData* temp_data = table->TempData; + IM_ASSERT(inner_window == g.CurrentWindow); + IM_ASSERT(outer_window == inner_window || outer_window == inner_window->ParentWindow); + + if (table->IsInsideRow) + TableEndRow(table); + + // Context menu in columns body + if (flags & ImGuiTableFlags_ContextMenuInBody) + if (table->HoveredColumnBody != -1 && !IsAnyItemHovered() && IsMouseReleased(ImGuiMouseButton_Right)) + TableOpenContextMenu((int)table->HoveredColumnBody); + + // Finalize table height + inner_window->DC.PrevLineSize = temp_data->HostBackupPrevLineSize; + inner_window->DC.CurrLineSize = temp_data->HostBackupCurrLineSize; + inner_window->DC.CursorMaxPos = temp_data->HostBackupCursorMaxPos; + const float inner_content_max_y = table->RowPosY2; + IM_ASSERT(table->RowPosY2 == inner_window->DC.CursorPos.y); + if (inner_window != outer_window) + inner_window->DC.CursorMaxPos.y = inner_content_max_y; + else if (!(flags & ImGuiTableFlags_NoHostExtendY)) + table->OuterRect.Max.y = table->InnerRect.Max.y = ImMax(table->OuterRect.Max.y, inner_content_max_y); // Patch OuterRect/InnerRect height + table->WorkRect.Max.y = ImMax(table->WorkRect.Max.y, table->OuterRect.Max.y); + table->LastOuterHeight = table->OuterRect.GetHeight(); + + // Setup inner scrolling range + // FIXME: This ideally should be done earlier, in BeginTable() SetNextWindowContentSize call, just like writing to inner_window->DC.CursorMaxPos.y, + // but since the later is likely to be impossible to do we'd rather update both axises together. + if (table->Flags & ImGuiTableFlags_ScrollX) + { + const float outer_padding_for_border = (table->Flags & ImGuiTableFlags_BordersOuterV) ? TABLE_BORDER_SIZE : 0.0f; + float max_pos_x = table->InnerWindow->DC.CursorMaxPos.x; + if (table->RightMostEnabledColumn != -1) + max_pos_x = ImMax(max_pos_x, table->Columns[table->RightMostEnabledColumn].WorkMaxX + table->CellPaddingX + table->OuterPaddingX - outer_padding_for_border); + if (table->ResizedColumn != -1) + max_pos_x = ImMax(max_pos_x, table->ResizeLockMinContentsX2); + table->InnerWindow->DC.CursorMaxPos.x = max_pos_x; + } + + // Pop clipping rect + if (!(flags & ImGuiTableFlags_NoClip)) + inner_window->DrawList->PopClipRect(); + inner_window->ClipRect = inner_window->DrawList->_ClipRectStack.back(); + + // Draw borders + if ((flags & ImGuiTableFlags_Borders) != 0) + TableDrawBorders(table); + +#if 0 + // Strip out dummy channel draw calls + // We have no way to prevent user submitting direct ImDrawList calls into a hidden column (but ImGui:: calls will be clipped out) + // Pros: remove draw calls which will have no effect. since they'll have zero-size cliprect they may be early out anyway. + // Cons: making it harder for users watching metrics/debugger to spot the wasted vertices. + if (table->DummyDrawChannel != (ImGuiTableColumnIdx)-1) + { + ImDrawChannel* dummy_channel = &table->DrawSplitter._Channels[table->DummyDrawChannel]; + dummy_channel->_CmdBuffer.resize(0); + dummy_channel->_IdxBuffer.resize(0); + } +#endif + + // Flatten channels and merge draw calls + ImDrawListSplitter* splitter = table->DrawSplitter; + splitter->SetCurrentChannel(inner_window->DrawList, 0); + if ((table->Flags & ImGuiTableFlags_NoClip) == 0) + TableMergeDrawChannels(table); + splitter->Merge(inner_window->DrawList); + + // Update ColumnsAutoFitWidth to get us ahead for host using our size to auto-resize without waiting for next BeginTable() + const float width_spacings = (table->OuterPaddingX * 2.0f) + (table->CellSpacingX1 + table->CellSpacingX2) * (table->ColumnsEnabledCount - 1); + table->ColumnsAutoFitWidth = width_spacings + (table->CellPaddingX * 2.0f) * table->ColumnsEnabledCount; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if (table->EnabledMaskByIndex & ((ImU64)1 << column_n)) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if ((column->Flags & ImGuiTableColumnFlags_WidthFixed) && !(column->Flags & ImGuiTableColumnFlags_NoResize)) + table->ColumnsAutoFitWidth += column->WidthRequest; + else + table->ColumnsAutoFitWidth += TableGetColumnWidthAuto(table, column); + } + + // Update scroll + if ((table->Flags & ImGuiTableFlags_ScrollX) == 0 && inner_window != outer_window) + { + inner_window->Scroll.x = 0.0f; + } + else if (table->LastResizedColumn != -1 && table->ResizedColumn == -1 && inner_window->ScrollbarX && table->InstanceInteracted == table->InstanceCurrent) + { + // When releasing a column being resized, scroll to keep the resulting column in sight + const float neighbor_width_to_keep_visible = table->MinColumnWidth + table->CellPaddingX * 2.0f; + ImGuiTableColumn* column = &table->Columns[table->LastResizedColumn]; + if (column->MaxX < table->InnerClipRect.Min.x) + SetScrollFromPosX(inner_window, column->MaxX - inner_window->Pos.x - neighbor_width_to_keep_visible, 1.0f); + else if (column->MaxX > table->InnerClipRect.Max.x) + SetScrollFromPosX(inner_window, column->MaxX - inner_window->Pos.x + neighbor_width_to_keep_visible, 1.0f); + } + + // Apply resizing/dragging at the end of the frame + if (table->ResizedColumn != -1 && table->InstanceCurrent == table->InstanceInteracted) + { + ImGuiTableColumn* column = &table->Columns[table->ResizedColumn]; + const float new_x2 = (g.IO.MousePos.x - g.ActiveIdClickOffset.x + TABLE_RESIZE_SEPARATOR_HALF_THICKNESS); + const float new_width = ImFloor(new_x2 - column->MinX - table->CellSpacingX1 - table->CellPaddingX * 2.0f); + table->ResizedColumnNextWidth = new_width; + } + + // Pop from id stack + IM_ASSERT_USER_ERROR(inner_window->IDStack.back() == table->ID + table->InstanceCurrent, "Mismatching PushID/PopID!"); + IM_ASSERT_USER_ERROR(outer_window->DC.ItemWidthStack.Size >= temp_data->HostBackupItemWidthStackSize, "Too many PopItemWidth!"); + PopID(); + + // Restore window data that we modified + const ImVec2 backup_outer_max_pos = outer_window->DC.CursorMaxPos; + inner_window->WorkRect = temp_data->HostBackupWorkRect; + inner_window->ParentWorkRect = temp_data->HostBackupParentWorkRect; + inner_window->SkipItems = table->HostSkipItems; + outer_window->DC.CursorPos = table->OuterRect.Min; + outer_window->DC.ItemWidth = temp_data->HostBackupItemWidth; + outer_window->DC.ItemWidthStack.Size = temp_data->HostBackupItemWidthStackSize; + outer_window->DC.ColumnsOffset = temp_data->HostBackupColumnsOffset; + + // Layout in outer window + // (FIXME: To allow auto-fit and allow desirable effect of SameLine() we dissociate 'used' vs 'ideal' size by overriding + // CursorPosPrevLine and CursorMaxPos manually. That should be a more general layout feature, see same problem e.g. #3414) + if (inner_window != outer_window) + { + EndChild(); + } + else + { + ItemSize(table->OuterRect.GetSize()); + ItemAdd(table->OuterRect, 0); + } + + // Override declared contents width/height to enable auto-resize while not needlessly adding a scrollbar + if (table->Flags & ImGuiTableFlags_NoHostExtendX) + { + // FIXME-TABLE: Could we remove this section? + // ColumnsAutoFitWidth may be one frame ahead here since for Fixed+NoResize is calculated from latest contents + IM_ASSERT((table->Flags & ImGuiTableFlags_ScrollX) == 0); + outer_window->DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, table->OuterRect.Min.x + table->ColumnsAutoFitWidth); + } + else if (temp_data->UserOuterSize.x <= 0.0f) + { + const float decoration_size = (table->Flags & ImGuiTableFlags_ScrollX) ? inner_window->ScrollbarSizes.x : 0.0f; + outer_window->DC.IdealMaxPos.x = ImMax(outer_window->DC.IdealMaxPos.x, table->OuterRect.Min.x + table->ColumnsAutoFitWidth + decoration_size - temp_data->UserOuterSize.x); + outer_window->DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, ImMin(table->OuterRect.Max.x, table->OuterRect.Min.x + table->ColumnsAutoFitWidth)); + } + else + { + outer_window->DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, table->OuterRect.Max.x); + } + if (temp_data->UserOuterSize.y <= 0.0f) + { + const float decoration_size = (table->Flags & ImGuiTableFlags_ScrollY) ? inner_window->ScrollbarSizes.y : 0.0f; + outer_window->DC.IdealMaxPos.y = ImMax(outer_window->DC.IdealMaxPos.y, inner_content_max_y + decoration_size - temp_data->UserOuterSize.y); + outer_window->DC.CursorMaxPos.y = ImMax(backup_outer_max_pos.y, ImMin(table->OuterRect.Max.y, inner_content_max_y)); + } + else + { + // OuterRect.Max.y may already have been pushed downward from the initial value (unless ImGuiTableFlags_NoHostExtendY is set) + outer_window->DC.CursorMaxPos.y = ImMax(backup_outer_max_pos.y, table->OuterRect.Max.y); + } + + // Save settings + if (table->IsSettingsDirty) + TableSaveSettings(table); + table->IsInitializing = false; + + // Clear or restore current table, if any + IM_ASSERT(g.CurrentWindow == outer_window && g.CurrentTable == table); + IM_ASSERT(g.TablesTempDataStacked > 0); + temp_data = (--g.TablesTempDataStacked > 0) ? &g.TablesTempData[g.TablesTempDataStacked - 1] : NULL; + g.CurrentTable = temp_data ? g.Tables.GetByIndex(temp_data->TableIndex) : NULL; + if (g.CurrentTable) + { + g.CurrentTable->TempData = temp_data; + g.CurrentTable->DrawSplitter = &temp_data->DrawSplitter; + } + outer_window->DC.CurrentTableIdx = g.CurrentTable ? g.Tables.GetIndex(g.CurrentTable) : -1; +} + +// See "COLUMN SIZING POLICIES" comments at the top of this file +// If (init_width_or_weight <= 0.0f) it is ignored +void ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, float init_width_or_weight, ImGuiID user_id) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL && "Need to call TableSetupColumn() after BeginTable()!"); + IM_ASSERT(table->IsLayoutLocked == false && "Need to call call TableSetupColumn() before first row!"); + IM_ASSERT((flags & ImGuiTableColumnFlags_StatusMask_) == 0 && "Illegal to pass StatusMask values to TableSetupColumn()"); + if (table->DeclColumnsCount >= table->ColumnsCount) + { + IM_ASSERT_USER_ERROR(table->DeclColumnsCount < table->ColumnsCount, "Called TableSetupColumn() too many times!"); + return; + } + + ImGuiTableColumn* column = &table->Columns[table->DeclColumnsCount]; + table->DeclColumnsCount++; + + // Assert when passing a width or weight if policy is entirely left to default, to avoid storing width into weight and vice-versa. + // Give a grace to users of ImGuiTableFlags_ScrollX. + if (table->IsDefaultSizingPolicy && (flags & ImGuiTableColumnFlags_WidthMask_) == 0 && (flags & ImGuiTableFlags_ScrollX) == 0) + IM_ASSERT(init_width_or_weight <= 0.0f && "Can only specify width/weight if sizing policy is set explicitly in either Table or Column."); + + // When passing a width automatically enforce WidthFixed policy + // (whereas TableSetupColumnFlags would default to WidthAuto if table is not Resizable) + if ((flags & ImGuiTableColumnFlags_WidthMask_) == 0 && init_width_or_weight > 0.0f) + if ((table->Flags & ImGuiTableFlags_SizingMask_) == ImGuiTableFlags_SizingFixedFit || (table->Flags & ImGuiTableFlags_SizingMask_) == ImGuiTableFlags_SizingFixedSame) + flags |= ImGuiTableColumnFlags_WidthFixed; + + TableSetupColumnFlags(table, column, flags); + column->UserID = user_id; + flags = column->Flags; + + // Initialize defaults + column->InitStretchWeightOrWidth = init_width_or_weight; + if (table->IsInitializing) + { + // Init width or weight + if (column->WidthRequest < 0.0f && column->StretchWeight < 0.0f) + { + if ((flags & ImGuiTableColumnFlags_WidthFixed) && init_width_or_weight > 0.0f) + column->WidthRequest = init_width_or_weight; + if (flags & ImGuiTableColumnFlags_WidthStretch) + column->StretchWeight = (init_width_or_weight > 0.0f) ? init_width_or_weight : -1.0f; + + // Disable auto-fit if an explicit width/weight has been specified + if (init_width_or_weight > 0.0f) + column->AutoFitQueue = 0x00; + } + + // Init default visibility/sort state + if ((flags & ImGuiTableColumnFlags_DefaultHide) && (table->SettingsLoadedFlags & ImGuiTableFlags_Hideable) == 0) + column->IsUserEnabled = column->IsUserEnabledNextFrame = false; + if (flags & ImGuiTableColumnFlags_DefaultSort && (table->SettingsLoadedFlags & ImGuiTableFlags_Sortable) == 0) + { + column->SortOrder = 0; // Multiple columns using _DefaultSort will be reassigned unique SortOrder values when building the sort specs. + column->SortDirection = (column->Flags & ImGuiTableColumnFlags_PreferSortDescending) ? (ImS8)ImGuiSortDirection_Descending : (ImU8)(ImGuiSortDirection_Ascending); + } + } + + // Store name (append with zero-terminator in contiguous buffer) + column->NameOffset = -1; + if (label != NULL && label[0] != 0) + { + column->NameOffset = (ImS16)table->ColumnsNames.size(); + table->ColumnsNames.append(label, label + strlen(label) + 1); + } +} + +// [Public] +void ImGui::TableSetupScrollFreeze(int columns, int rows) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL && "Need to call TableSetupColumn() after BeginTable()!"); + IM_ASSERT(table->IsLayoutLocked == false && "Need to call TableSetupColumn() before first row!"); + IM_ASSERT(columns >= 0 && columns < IMGUI_TABLE_MAX_COLUMNS); + IM_ASSERT(rows >= 0 && rows < 128); // Arbitrary limit + + table->FreezeColumnsRequest = (table->Flags & ImGuiTableFlags_ScrollX) ? (ImGuiTableColumnIdx)ImMin(columns, table->ColumnsCount) : 0; + table->FreezeColumnsCount = (table->InnerWindow->Scroll.x != 0.0f) ? table->FreezeColumnsRequest : 0; + table->FreezeRowsRequest = (table->Flags & ImGuiTableFlags_ScrollY) ? (ImGuiTableColumnIdx)rows : 0; + table->FreezeRowsCount = (table->InnerWindow->Scroll.y != 0.0f) ? table->FreezeRowsRequest : 0; + table->IsUnfrozenRows = (table->FreezeRowsCount == 0); // Make sure this is set before TableUpdateLayout() so ImGuiListClipper can benefit from it.b + + // Ensure frozen columns are ordered in their section. We still allow multiple frozen columns to be reordered. + // FIXME-TABLE: This work for preserving 2143 into 21|43. How about 4321 turning into 21|43? (preserve relative order in each section) + for (int column_n = 0; column_n < table->FreezeColumnsRequest; column_n++) + { + int order_n = table->DisplayOrderToIndex[column_n]; + if (order_n != column_n && order_n >= table->FreezeColumnsRequest) + { + ImSwap(table->Columns[table->DisplayOrderToIndex[order_n]].DisplayOrder, table->Columns[table->DisplayOrderToIndex[column_n]].DisplayOrder); + ImSwap(table->DisplayOrderToIndex[order_n], table->DisplayOrderToIndex[column_n]); + } + } +} + +//----------------------------------------------------------------------------- +// [SECTION] Tables: Simple accessors +//----------------------------------------------------------------------------- +// - TableGetColumnCount() +// - TableGetColumnName() +// - TableGetColumnName() [Internal] +// - TableSetColumnEnabled() +// - TableGetColumnFlags() +// - TableGetCellBgRect() [Internal] +// - TableGetColumnResizeID() [Internal] +// - TableGetHoveredColumn() [Internal] +// - TableSetBgColor() +//----------------------------------------------------------------------------- + +int ImGui::TableGetColumnCount() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + return table ? table->ColumnsCount : 0; +} + +const char* ImGui::TableGetColumnName(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return NULL; + if (column_n < 0) + column_n = table->CurrentColumn; + return TableGetColumnName(table, column_n); +} + +const char* ImGui::TableGetColumnName(const ImGuiTable* table, int column_n) +{ + if (table->IsLayoutLocked == false && column_n >= table->DeclColumnsCount) + return ""; // NameOffset is invalid at this point + const ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->NameOffset == -1) + return ""; + return &table->ColumnsNames.Buf[column->NameOffset]; +} + +// Change user accessible enabled/disabled state of a column (often perceived as "showing/hiding" from users point of view) +// Note that end-user can use the context menu to change this themselves (right-click in headers, or right-click in columns body with ImGuiTableFlags_ContextMenuInBody) +// - Require table to have the ImGuiTableFlags_Hideable flag because we are manipulating user accessible state. +// - Request will be applied during next layout, which happens on the first call to TableNextRow() after BeginTable(). +// - For the getter you can test (TableGetColumnFlags() & ImGuiTableColumnFlags_IsEnabled) != 0. +// - Alternative: the ImGuiTableColumnFlags_Disabled is an overriding/master disable flag which will also hide the column from context menu. +void ImGui::TableSetColumnEnabled(int column_n, bool enabled) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL); + if (!table) + return; + IM_ASSERT(table->Flags & ImGuiTableFlags_Hideable); // See comments above + if (column_n < 0) + column_n = table->CurrentColumn; + IM_ASSERT(column_n >= 0 && column_n < table->ColumnsCount); + ImGuiTableColumn* column = &table->Columns[column_n]; + column->IsUserEnabledNextFrame = enabled; +} + +// We allow querying for an extra column in order to poll the IsHovered state of the right-most section +ImGuiTableColumnFlags ImGui::TableGetColumnFlags(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return ImGuiTableColumnFlags_None; + if (column_n < 0) + column_n = table->CurrentColumn; + if (column_n == table->ColumnsCount) + return (table->HoveredColumnBody == column_n) ? ImGuiTableColumnFlags_IsHovered : ImGuiTableColumnFlags_None; + return table->Columns[column_n].Flags; +} + +// Return the cell rectangle based on currently known height. +// - Important: we generally don't know our row height until the end of the row, so Max.y will be incorrect in many situations. +// The only case where this is correct is if we provided a min_row_height to TableNextRow() and don't go below it, or in TableEndRow() when we locked that height. +// - Important: if ImGuiTableFlags_PadOuterX is set but ImGuiTableFlags_PadInnerX is not set, the outer-most left and right +// columns report a small offset so their CellBgRect can extend up to the outer border. +// FIXME: But the rendering code in TableEndRow() nullifies that with clamping required for scrolling. +ImRect ImGui::TableGetCellBgRect(const ImGuiTable* table, int column_n) +{ + const ImGuiTableColumn* column = &table->Columns[column_n]; + float x1 = column->MinX; + float x2 = column->MaxX; + //if (column->PrevEnabledColumn == -1) + // x1 -= table->OuterPaddingX; + //if (column->NextEnabledColumn == -1) + // x2 += table->OuterPaddingX; + x1 = ImMax(x1, table->WorkRect.Min.x); + x2 = ImMin(x2, table->WorkRect.Max.x); + return ImRect(x1, table->RowPosY1, x2, table->RowPosY2); +} + +// Return the resizing ID for the right-side of the given column. +ImGuiID ImGui::TableGetColumnResizeID(const ImGuiTable* table, int column_n, int instance_no) +{ + IM_ASSERT(column_n >= 0 && column_n < table->ColumnsCount); + ImGuiID id = table->ID + 1 + (instance_no * table->ColumnsCount) + column_n; + return id; +} + +// Return -1 when table is not hovered. return columns_count if the unused space at the right of visible columns is hovered. +int ImGui::TableGetHoveredColumn() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return -1; + return (int)table->HoveredColumnBody; +} + +void ImGui::TableSetBgColor(ImGuiTableBgTarget target, ImU32 color, int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(target != ImGuiTableBgTarget_None); + + if (color == IM_COL32_DISABLE) + color = 0; + + // We cannot draw neither the cell or row background immediately as we don't know the row height at this point in time. + switch (target) + { + case ImGuiTableBgTarget_CellBg: + { + if (table->RowPosY1 > table->InnerClipRect.Max.y) // Discard + return; + if (column_n == -1) + column_n = table->CurrentColumn; + if ((table->VisibleMaskByIndex & ((ImU64)1 << column_n)) == 0) + return; + if (table->RowCellDataCurrent < 0 || table->RowCellData[table->RowCellDataCurrent].Column != column_n) + table->RowCellDataCurrent++; + ImGuiTableCellData* cell_data = &table->RowCellData[table->RowCellDataCurrent]; + cell_data->BgColor = color; + cell_data->Column = (ImGuiTableColumnIdx)column_n; + break; + } + case ImGuiTableBgTarget_RowBg0: + case ImGuiTableBgTarget_RowBg1: + { + if (table->RowPosY1 > table->InnerClipRect.Max.y) // Discard + return; + IM_ASSERT(column_n == -1); + int bg_idx = (target == ImGuiTableBgTarget_RowBg1) ? 1 : 0; + table->RowBgColor[bg_idx] = color; + break; + } + default: + IM_ASSERT(0); + } +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Row changes +//------------------------------------------------------------------------- +// - TableGetRowIndex() +// - TableNextRow() +// - TableBeginRow() [Internal] +// - TableEndRow() [Internal] +//------------------------------------------------------------------------- + +// [Public] Note: for row coloring we use ->RowBgColorCounter which is the same value without counting header rows +int ImGui::TableGetRowIndex() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return 0; + return table->CurrentRow; +} + +// [Public] Starts into the first cell of a new row +void ImGui::TableNextRow(ImGuiTableRowFlags row_flags, float row_min_height) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + + if (!table->IsLayoutLocked) + TableUpdateLayout(table); + if (table->IsInsideRow) + TableEndRow(table); + + table->LastRowFlags = table->RowFlags; + table->RowFlags = row_flags; + table->RowMinHeight = row_min_height; + TableBeginRow(table); + + // We honor min_row_height requested by user, but cannot guarantee per-row maximum height, + // because that would essentially require a unique clipping rectangle per-cell. + table->RowPosY2 += table->CellPaddingY * 2.0f; + table->RowPosY2 = ImMax(table->RowPosY2, table->RowPosY1 + row_min_height); + + // Disable output until user calls TableNextColumn() + table->InnerWindow->SkipItems = true; +} + +// [Internal] Called by TableNextRow() +void ImGui::TableBeginRow(ImGuiTable* table) +{ + ImGuiWindow* window = table->InnerWindow; + IM_ASSERT(!table->IsInsideRow); + + // New row + table->CurrentRow++; + table->CurrentColumn = -1; + table->RowBgColor[0] = table->RowBgColor[1] = IM_COL32_DISABLE; + table->RowCellDataCurrent = -1; + table->IsInsideRow = true; + + // Begin frozen rows + float next_y1 = table->RowPosY2; + if (table->CurrentRow == 0 && table->FreezeRowsCount > 0) + next_y1 = window->DC.CursorPos.y = table->OuterRect.Min.y; + + table->RowPosY1 = table->RowPosY2 = next_y1; + table->RowTextBaseline = 0.0f; + table->RowIndentOffsetX = window->DC.Indent.x - table->HostIndentX; // Lock indent + window->DC.PrevLineTextBaseOffset = 0.0f; + window->DC.CursorMaxPos.y = next_y1; + + // Making the header BG color non-transparent will allow us to overlay it multiple times when handling smooth dragging. + if (table->RowFlags & ImGuiTableRowFlags_Headers) + { + TableSetBgColor(ImGuiTableBgTarget_RowBg0, GetColorU32(ImGuiCol_TableHeaderBg)); + if (table->CurrentRow == 0) + table->IsUsingHeaders = true; + } +} + +// [Internal] Called by TableNextRow() +void ImGui::TableEndRow(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(window == table->InnerWindow); + IM_ASSERT(table->IsInsideRow); + + if (table->CurrentColumn != -1) + TableEndCell(table); + + // Logging + if (g.LogEnabled) + LogRenderedText(NULL, "|"); + + // Position cursor at the bottom of our row so it can be used for e.g. clipping calculation. However it is + // likely that the next call to TableBeginCell() will reposition the cursor to take account of vertical padding. + window->DC.CursorPos.y = table->RowPosY2; + + // Row background fill + const float bg_y1 = table->RowPosY1; + const float bg_y2 = table->RowPosY2; + const bool unfreeze_rows_actual = (table->CurrentRow + 1 == table->FreezeRowsCount); + const bool unfreeze_rows_request = (table->CurrentRow + 1 == table->FreezeRowsRequest); + if (table->CurrentRow == 0) + table->LastFirstRowHeight = bg_y2 - bg_y1; + + const bool is_visible = (bg_y2 >= table->InnerClipRect.Min.y && bg_y1 <= table->InnerClipRect.Max.y); + if (is_visible) + { + // Decide of background color for the row + ImU32 bg_col0 = 0; + ImU32 bg_col1 = 0; + if (table->RowBgColor[0] != IM_COL32_DISABLE) + bg_col0 = table->RowBgColor[0]; + else if (table->Flags & ImGuiTableFlags_RowBg) + bg_col0 = GetColorU32((table->RowBgColorCounter & 1) ? ImGuiCol_TableRowBgAlt : ImGuiCol_TableRowBg); + if (table->RowBgColor[1] != IM_COL32_DISABLE) + bg_col1 = table->RowBgColor[1]; + + // Decide of top border color + ImU32 border_col = 0; + const float border_size = TABLE_BORDER_SIZE; + if (table->CurrentRow > 0 || table->InnerWindow == table->OuterWindow) + if (table->Flags & ImGuiTableFlags_BordersInnerH) + border_col = (table->LastRowFlags & ImGuiTableRowFlags_Headers) ? table->BorderColorStrong : table->BorderColorLight; + + const bool draw_cell_bg_color = table->RowCellDataCurrent >= 0; + const bool draw_strong_bottom_border = unfreeze_rows_actual; + if ((bg_col0 | bg_col1 | border_col) != 0 || draw_strong_bottom_border || draw_cell_bg_color) + { + // In theory we could call SetWindowClipRectBeforeSetChannel() but since we know TableEndRow() is + // always followed by a change of clipping rectangle we perform the smallest overwrite possible here. + if ((table->Flags & ImGuiTableFlags_NoClip) == 0) + window->DrawList->_CmdHeader.ClipRect = table->Bg0ClipRectForDrawCmd.ToVec4(); + table->DrawSplitter->SetCurrentChannel(window->DrawList, TABLE_DRAW_CHANNEL_BG0); + } + + // Draw row background + // We soft/cpu clip this so all backgrounds and borders can share the same clipping rectangle + if (bg_col0 || bg_col1) + { + ImRect row_rect(table->WorkRect.Min.x, bg_y1, table->WorkRect.Max.x, bg_y2); + row_rect.ClipWith(table->BgClipRect); + if (bg_col0 != 0 && row_rect.Min.y < row_rect.Max.y) + window->DrawList->AddRectFilled(row_rect.Min, row_rect.Max, bg_col0); + if (bg_col1 != 0 && row_rect.Min.y < row_rect.Max.y) + window->DrawList->AddRectFilled(row_rect.Min, row_rect.Max, bg_col1); + } + + // Draw cell background color + if (draw_cell_bg_color) + { + ImGuiTableCellData* cell_data_end = &table->RowCellData[table->RowCellDataCurrent]; + for (ImGuiTableCellData* cell_data = &table->RowCellData[0]; cell_data <= cell_data_end; cell_data++) + { + // As we render the BG here we need to clip things (for layout we would not) + // FIXME: This cancels the OuterPadding addition done by TableGetCellBgRect(), need to keep it while rendering correctly while scrolling. + const ImGuiTableColumn* column = &table->Columns[cell_data->Column]; + ImRect cell_bg_rect = TableGetCellBgRect(table, cell_data->Column); + cell_bg_rect.ClipWith(table->BgClipRect); + cell_bg_rect.Min.x = ImMax(cell_bg_rect.Min.x, column->ClipRect.Min.x); // So that first column after frozen one gets clipped when scrolling + cell_bg_rect.Max.x = ImMin(cell_bg_rect.Max.x, column->MaxX); + window->DrawList->AddRectFilled(cell_bg_rect.Min, cell_bg_rect.Max, cell_data->BgColor); + } + } + + // Draw top border + if (border_col && bg_y1 >= table->BgClipRect.Min.y && bg_y1 < table->BgClipRect.Max.y) + window->DrawList->AddLine(ImVec2(table->BorderX1, bg_y1), ImVec2(table->BorderX2, bg_y1), border_col, border_size); + + // Draw bottom border at the row unfreezing mark (always strong) + if (draw_strong_bottom_border && bg_y2 >= table->BgClipRect.Min.y && bg_y2 < table->BgClipRect.Max.y) + window->DrawList->AddLine(ImVec2(table->BorderX1, bg_y2), ImVec2(table->BorderX2, bg_y2), table->BorderColorStrong, border_size); + } + + // End frozen rows (when we are past the last frozen row line, teleport cursor and alter clipping rectangle) + // We need to do that in TableEndRow() instead of TableBeginRow() so the list clipper can mark end of row and + // get the new cursor position. + if (unfreeze_rows_request) + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + column->NavLayerCurrent = (ImS8)((column_n < table->FreezeColumnsCount) ? ImGuiNavLayer_Menu : ImGuiNavLayer_Main); + } + if (unfreeze_rows_actual) + { + IM_ASSERT(table->IsUnfrozenRows == false); + table->IsUnfrozenRows = true; + + // BgClipRect starts as table->InnerClipRect, reduce it now and make BgClipRectForDrawCmd == BgClipRect + float y0 = ImMax(table->RowPosY2 + 1, window->InnerClipRect.Min.y); + table->BgClipRect.Min.y = table->Bg2ClipRectForDrawCmd.Min.y = ImMin(y0, window->InnerClipRect.Max.y); + table->BgClipRect.Max.y = table->Bg2ClipRectForDrawCmd.Max.y = window->InnerClipRect.Max.y; + table->Bg2DrawChannelCurrent = table->Bg2DrawChannelUnfrozen; + IM_ASSERT(table->Bg2ClipRectForDrawCmd.Min.y <= table->Bg2ClipRectForDrawCmd.Max.y); + + float row_height = table->RowPosY2 - table->RowPosY1; + table->RowPosY2 = window->DC.CursorPos.y = table->WorkRect.Min.y + table->RowPosY2 - table->OuterRect.Min.y; + table->RowPosY1 = table->RowPosY2 - row_height; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + column->DrawChannelCurrent = column->DrawChannelUnfrozen; + column->ClipRect.Min.y = table->Bg2ClipRectForDrawCmd.Min.y; + } + + // Update cliprect ahead of TableBeginCell() so clipper can access to new ClipRect->Min.y + SetWindowClipRectBeforeSetChannel(window, table->Columns[0].ClipRect); + table->DrawSplitter->SetCurrentChannel(window->DrawList, table->Columns[0].DrawChannelCurrent); + } + + if (!(table->RowFlags & ImGuiTableRowFlags_Headers)) + table->RowBgColorCounter++; + table->IsInsideRow = false; +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Columns changes +//------------------------------------------------------------------------- +// - TableGetColumnIndex() +// - TableSetColumnIndex() +// - TableNextColumn() +// - TableBeginCell() [Internal] +// - TableEndCell() [Internal] +//------------------------------------------------------------------------- + +int ImGui::TableGetColumnIndex() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return 0; + return table->CurrentColumn; +} + +// [Public] Append into a specific column +bool ImGui::TableSetColumnIndex(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return false; + + if (table->CurrentColumn != column_n) + { + if (table->CurrentColumn != -1) + TableEndCell(table); + IM_ASSERT(column_n >= 0 && table->ColumnsCount); + TableBeginCell(table, column_n); + } + + // Return whether the column is visible. User may choose to skip submitting items based on this return value, + // however they shouldn't skip submitting for columns that may have the tallest contribution to row height. + return (table->RequestOutputMaskByIndex & ((ImU64)1 << column_n)) != 0; +} + +// [Public] Append into the next column, wrap and create a new row when already on last column +bool ImGui::TableNextColumn() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return false; + + if (table->IsInsideRow && table->CurrentColumn + 1 < table->ColumnsCount) + { + if (table->CurrentColumn != -1) + TableEndCell(table); + TableBeginCell(table, table->CurrentColumn + 1); + } + else + { + TableNextRow(); + TableBeginCell(table, 0); + } + + // Return whether the column is visible. User may choose to skip submitting items based on this return value, + // however they shouldn't skip submitting for columns that may have the tallest contribution to row height. + int column_n = table->CurrentColumn; + return (table->RequestOutputMaskByIndex & ((ImU64)1 << column_n)) != 0; +} + + +// [Internal] Called by TableSetColumnIndex()/TableNextColumn() +// This is called very frequently, so we need to be mindful of unnecessary overhead. +// FIXME-TABLE FIXME-OPT: Could probably shortcut some things for non-active or clipped columns. +void ImGui::TableBeginCell(ImGuiTable* table, int column_n) +{ + ImGuiTableColumn* column = &table->Columns[column_n]; + ImGuiWindow* window = table->InnerWindow; + table->CurrentColumn = column_n; + + // Start position is roughly ~~ CellRect.Min + CellPadding + Indent + float start_x = column->WorkMinX; + if (column->Flags & ImGuiTableColumnFlags_IndentEnable) + start_x += table->RowIndentOffsetX; // ~~ += window.DC.Indent.x - table->HostIndentX, except we locked it for the row. + + window->DC.CursorPos.x = start_x; + window->DC.CursorPos.y = table->RowPosY1 + table->CellPaddingY; + window->DC.CursorMaxPos.x = window->DC.CursorPos.x; + window->DC.ColumnsOffset.x = start_x - window->Pos.x - window->DC.Indent.x; // FIXME-WORKRECT + window->DC.CurrLineTextBaseOffset = table->RowTextBaseline; + window->DC.NavLayerCurrent = (ImGuiNavLayer)column->NavLayerCurrent; + + window->WorkRect.Min.y = window->DC.CursorPos.y; + window->WorkRect.Min.x = column->WorkMinX; + window->WorkRect.Max.x = column->WorkMaxX; + window->DC.ItemWidth = column->ItemWidth; + + // To allow ImGuiListClipper to function we propagate our row height + if (!column->IsEnabled) + window->DC.CursorPos.y = ImMax(window->DC.CursorPos.y, table->RowPosY2); + + window->SkipItems = column->IsSkipItems; + if (column->IsSkipItems) + { + ImGuiContext& g = *GImGui; + g.LastItemData.ID = 0; + g.LastItemData.StatusFlags = 0; + } + + if (table->Flags & ImGuiTableFlags_NoClip) + { + // FIXME: if we end up drawing all borders/bg in EndTable, could remove this and just assert that channel hasn't changed. + table->DrawSplitter->SetCurrentChannel(window->DrawList, TABLE_DRAW_CHANNEL_NOCLIP); + //IM_ASSERT(table->DrawSplitter._Current == TABLE_DRAW_CHANNEL_NOCLIP); + } + else + { + // FIXME-TABLE: Could avoid this if draw channel is dummy channel? + SetWindowClipRectBeforeSetChannel(window, column->ClipRect); + table->DrawSplitter->SetCurrentChannel(window->DrawList, column->DrawChannelCurrent); + } + + // Logging + ImGuiContext& g = *GImGui; + if (g.LogEnabled && !column->IsSkipItems) + { + LogRenderedText(&window->DC.CursorPos, "|"); + g.LogLinePosY = FLT_MAX; + } +} + +// [Internal] Called by TableNextRow()/TableSetColumnIndex()/TableNextColumn() +void ImGui::TableEndCell(ImGuiTable* table) +{ + ImGuiTableColumn* column = &table->Columns[table->CurrentColumn]; + ImGuiWindow* window = table->InnerWindow; + + // Report maximum position so we can infer content size per column. + float* p_max_pos_x; + if (table->RowFlags & ImGuiTableRowFlags_Headers) + p_max_pos_x = &column->ContentMaxXHeadersUsed; // Useful in case user submit contents in header row that is not a TableHeader() call + else + p_max_pos_x = table->IsUnfrozenRows ? &column->ContentMaxXUnfrozen : &column->ContentMaxXFrozen; + *p_max_pos_x = ImMax(*p_max_pos_x, window->DC.CursorMaxPos.x); + table->RowPosY2 = ImMax(table->RowPosY2, window->DC.CursorMaxPos.y + table->CellPaddingY); + column->ItemWidth = window->DC.ItemWidth; + + // Propagate text baseline for the entire row + // FIXME-TABLE: Here we propagate text baseline from the last line of the cell.. instead of the first one. + table->RowTextBaseline = ImMax(table->RowTextBaseline, window->DC.PrevLineTextBaseOffset); +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Columns width management +//------------------------------------------------------------------------- +// - TableGetMaxColumnWidth() [Internal] +// - TableGetColumnWidthAuto() [Internal] +// - TableSetColumnWidth() +// - TableSetColumnWidthAutoSingle() [Internal] +// - TableSetColumnWidthAutoAll() [Internal] +// - TableUpdateColumnsWeightFromWidth() [Internal] +//------------------------------------------------------------------------- + +// Maximum column content width given current layout. Use column->MinX so this value on a per-column basis. +float ImGui::TableGetMaxColumnWidth(const ImGuiTable* table, int column_n) +{ + const ImGuiTableColumn* column = &table->Columns[column_n]; + float max_width = FLT_MAX; + const float min_column_distance = table->MinColumnWidth + table->CellPaddingX * 2.0f + table->CellSpacingX1 + table->CellSpacingX2; + if (table->Flags & ImGuiTableFlags_ScrollX) + { + // Frozen columns can't reach beyond visible width else scrolling will naturally break. + // (we use DisplayOrder as within a set of multiple frozen column reordering is possible) + if (column->DisplayOrder < table->FreezeColumnsRequest) + { + max_width = (table->InnerClipRect.Max.x - (table->FreezeColumnsRequest - column->DisplayOrder) * min_column_distance) - column->MinX; + max_width = max_width - table->OuterPaddingX - table->CellPaddingX - table->CellSpacingX2; + } + } + else if ((table->Flags & ImGuiTableFlags_NoKeepColumnsVisible) == 0) + { + // If horizontal scrolling if disabled, we apply a final lossless shrinking of columns in order to make + // sure they are all visible. Because of this we also know that all of the columns will always fit in + // table->WorkRect and therefore in table->InnerRect (because ScrollX is off) + // FIXME-TABLE: This is solved incorrectly but also quite a difficult problem to fix as we also want ClipRect width to match. + // See "table_width_distrib" and "table_width_keep_visible" tests + max_width = table->WorkRect.Max.x - (table->ColumnsEnabledCount - column->IndexWithinEnabledSet - 1) * min_column_distance - column->MinX; + //max_width -= table->CellSpacingX1; + max_width -= table->CellSpacingX2; + max_width -= table->CellPaddingX * 2.0f; + max_width -= table->OuterPaddingX; + } + return max_width; +} + +// Note this is meant to be stored in column->WidthAuto, please generally use the WidthAuto field +float ImGui::TableGetColumnWidthAuto(ImGuiTable* table, ImGuiTableColumn* column) +{ + const float content_width_body = ImMax(column->ContentMaxXFrozen, column->ContentMaxXUnfrozen) - column->WorkMinX; + const float content_width_headers = column->ContentMaxXHeadersIdeal - column->WorkMinX; + float width_auto = content_width_body; + if (!(column->Flags & ImGuiTableColumnFlags_NoHeaderWidth)) + width_auto = ImMax(width_auto, content_width_headers); + + // Non-resizable fixed columns preserve their requested width + if ((column->Flags & ImGuiTableColumnFlags_WidthFixed) && column->InitStretchWeightOrWidth > 0.0f) + if (!(table->Flags & ImGuiTableFlags_Resizable) || (column->Flags & ImGuiTableColumnFlags_NoResize)) + width_auto = column->InitStretchWeightOrWidth; + + return ImMax(width_auto, table->MinColumnWidth); +} + +// 'width' = inner column width, without padding +void ImGui::TableSetColumnWidth(int column_n, float width) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL && table->IsLayoutLocked == false); + IM_ASSERT(column_n >= 0 && column_n < table->ColumnsCount); + ImGuiTableColumn* column_0 = &table->Columns[column_n]; + float column_0_width = width; + + // Apply constraints early + // Compare both requested and actual given width to avoid overwriting requested width when column is stuck (minimum size, bounded) + IM_ASSERT(table->MinColumnWidth > 0.0f); + const float min_width = table->MinColumnWidth; + const float max_width = ImMax(min_width, TableGetMaxColumnWidth(table, column_n)); + column_0_width = ImClamp(column_0_width, min_width, max_width); + if (column_0->WidthGiven == column_0_width || column_0->WidthRequest == column_0_width) + return; + + //IMGUI_DEBUG_LOG("TableSetColumnWidth(%d, %.1f->%.1f)\n", column_0_idx, column_0->WidthGiven, column_0_width); + ImGuiTableColumn* column_1 = (column_0->NextEnabledColumn != -1) ? &table->Columns[column_0->NextEnabledColumn] : NULL; + + // In this surprisingly not simple because of how we support mixing Fixed and multiple Stretch columns. + // - All fixed: easy. + // - All stretch: easy. + // - One or more fixed + one stretch: easy. + // - One or more fixed + more than one stretch: tricky. + // Qt when manual resize is enabled only support a single _trailing_ stretch column. + + // When forwarding resize from Wn| to Fn+1| we need to be considerate of the _NoResize flag on Fn+1. + // FIXME-TABLE: Find a way to rewrite all of this so interactions feel more consistent for the user. + // Scenarios: + // - F1 F2 F3 resize from F1| or F2| --> ok: alter ->WidthRequested of Fixed column. Subsequent columns will be offset. + // - F1 F2 F3 resize from F3| --> ok: alter ->WidthRequested of Fixed column. If active, ScrollX extent can be altered. + // - F1 F2 W3 resize from F1| or F2| --> ok: alter ->WidthRequested of Fixed column. If active, ScrollX extent can be altered, but it doesn't make much sense as the Stretch column will always be minimal size. + // - F1 F2 W3 resize from W3| --> ok: no-op (disabled by Resize Rule 1) + // - W1 W2 W3 resize from W1| or W2| --> ok + // - W1 W2 W3 resize from W3| --> ok: no-op (disabled by Resize Rule 1) + // - W1 F2 F3 resize from F3| --> ok: no-op (disabled by Resize Rule 1) + // - W1 F2 resize from F2| --> ok: no-op (disabled by Resize Rule 1) + // - W1 W2 F3 resize from W1| or W2| --> ok + // - W1 F2 W3 resize from W1| or F2| --> ok + // - F1 W2 F3 resize from W2| --> ok + // - F1 W3 F2 resize from W3| --> ok + // - W1 F2 F3 resize from W1| --> ok: equivalent to resizing |F2. F3 will not move. + // - W1 F2 F3 resize from F2| --> ok + // All resizes from a Wx columns are locking other columns. + + // Possible improvements: + // - W1 W2 W3 resize W1| --> to not be stuck, both W2 and W3 would stretch down. Seems possible to fix. Would be most beneficial to simplify resize of all-weighted columns. + // - W3 F1 F2 resize W3| --> to not be stuck past F1|, both F1 and F2 would need to stretch down, which would be lossy or ambiguous. Seems hard to fix. + + // [Resize Rule 1] Can't resize from right of right-most visible column if there is any Stretch column. Implemented in TableUpdateLayout(). + + // If we have all Fixed columns OR resizing a Fixed column that doesn't come after a Stretch one, we can do an offsetting resize. + // This is the preferred resize path + if (column_0->Flags & ImGuiTableColumnFlags_WidthFixed) + if (!column_1 || table->LeftMostStretchedColumn == -1 || table->Columns[table->LeftMostStretchedColumn].DisplayOrder >= column_0->DisplayOrder) + { + column_0->WidthRequest = column_0_width; + table->IsSettingsDirty = true; + return; + } + + // We can also use previous column if there's no next one (this is used when doing an auto-fit on the right-most stretch column) + if (column_1 == NULL) + column_1 = (column_0->PrevEnabledColumn != -1) ? &table->Columns[column_0->PrevEnabledColumn] : NULL; + if (column_1 == NULL) + return; + + // Resizing from right-side of a Stretch column before a Fixed column forward sizing to left-side of fixed column. + // (old_a + old_b == new_a + new_b) --> (new_a == old_a + old_b - new_b) + float column_1_width = ImMax(column_1->WidthRequest - (column_0_width - column_0->WidthRequest), min_width); + column_0_width = column_0->WidthRequest + column_1->WidthRequest - column_1_width; + IM_ASSERT(column_0_width > 0.0f && column_1_width > 0.0f); + column_0->WidthRequest = column_0_width; + column_1->WidthRequest = column_1_width; + if ((column_0->Flags | column_1->Flags) & ImGuiTableColumnFlags_WidthStretch) + TableUpdateColumnsWeightFromWidth(table); + table->IsSettingsDirty = true; +} + +// Disable clipping then auto-fit, will take 2 frames +// (we don't take a shortcut for unclipped columns to reduce inconsistencies when e.g. resizing multiple columns) +void ImGui::TableSetColumnWidthAutoSingle(ImGuiTable* table, int column_n) +{ + // Single auto width uses auto-fit + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsEnabled) + return; + column->CannotSkipItemsQueue = (1 << 0); + table->AutoFitSingleColumn = (ImGuiTableColumnIdx)column_n; +} + +void ImGui::TableSetColumnWidthAutoAll(ImGuiTable* table) +{ + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsEnabled && !(column->Flags & ImGuiTableColumnFlags_WidthStretch)) // Cannot reset weight of hidden stretch column + continue; + column->CannotSkipItemsQueue = (1 << 0); + column->AutoFitQueue = (1 << 1); + } +} + +void ImGui::TableUpdateColumnsWeightFromWidth(ImGuiTable* table) +{ + IM_ASSERT(table->LeftMostStretchedColumn != -1 && table->RightMostStretchedColumn != -1); + + // Measure existing quantity + float visible_weight = 0.0f; + float visible_width = 0.0f; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsEnabled || !(column->Flags & ImGuiTableColumnFlags_WidthStretch)) + continue; + IM_ASSERT(column->StretchWeight > 0.0f); + visible_weight += column->StretchWeight; + visible_width += column->WidthRequest; + } + IM_ASSERT(visible_weight > 0.0f && visible_width > 0.0f); + + // Apply new weights + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsEnabled || !(column->Flags & ImGuiTableColumnFlags_WidthStretch)) + continue; + column->StretchWeight = (column->WidthRequest / visible_width) * visible_weight; + IM_ASSERT(column->StretchWeight > 0.0f); + } +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Drawing +//------------------------------------------------------------------------- +// - TablePushBackgroundChannel() [Internal] +// - TablePopBackgroundChannel() [Internal] +// - TableSetupDrawChannels() [Internal] +// - TableMergeDrawChannels() [Internal] +// - TableDrawBorders() [Internal] +//------------------------------------------------------------------------- + +// Bg2 is used by Selectable (and possibly other widgets) to render to the background. +// Unlike our Bg0/1 channel which we uses for RowBg/CellBg/Borders and where we guarantee all shapes to be CPU-clipped, the Bg2 channel being widgets-facing will rely on regular ClipRect. +void ImGui::TablePushBackgroundChannel() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiTable* table = g.CurrentTable; + + // Optimization: avoid SetCurrentChannel() + PushClipRect() + table->HostBackupInnerClipRect = window->ClipRect; + SetWindowClipRectBeforeSetChannel(window, table->Bg2ClipRectForDrawCmd); + table->DrawSplitter->SetCurrentChannel(window->DrawList, table->Bg2DrawChannelCurrent); +} + +void ImGui::TablePopBackgroundChannel() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiTable* table = g.CurrentTable; + ImGuiTableColumn* column = &table->Columns[table->CurrentColumn]; + + // Optimization: avoid PopClipRect() + SetCurrentChannel() + SetWindowClipRectBeforeSetChannel(window, table->HostBackupInnerClipRect); + table->DrawSplitter->SetCurrentChannel(window->DrawList, column->DrawChannelCurrent); +} + +// Allocate draw channels. Called by TableUpdateLayout() +// - We allocate them following storage order instead of display order so reordering columns won't needlessly +// increase overall dormant memory cost. +// - We isolate headers draw commands in their own channels instead of just altering clip rects. +// This is in order to facilitate merging of draw commands. +// - After crossing FreezeRowsCount, all columns see their current draw channel changed to a second set of channels. +// - We only use the dummy draw channel so we can push a null clipping rectangle into it without affecting other +// channels, while simplifying per-row/per-cell overhead. It will be empty and discarded when merged. +// - We allocate 1 or 2 background draw channels. This is because we know TablePushBackgroundChannel() is only used for +// horizontal spanning. If we allowed vertical spanning we'd need one background draw channel per merge group (1-4). +// Draw channel allocation (before merging): +// - NoClip --> 2+D+1 channels: bg0/1 + bg2 + foreground (same clip rect == always 1 draw call) +// - Clip --> 2+D+N channels +// - FreezeRows --> 2+D+N*2 (unless scrolling value is zero) +// - FreezeRows || FreezeColunns --> 3+D+N*2 (unless scrolling value is zero) +// Where D is 1 if any column is clipped or hidden (dummy channel) otherwise 0. +void ImGui::TableSetupDrawChannels(ImGuiTable* table) +{ + const int freeze_row_multiplier = (table->FreezeRowsCount > 0) ? 2 : 1; + const int channels_for_row = (table->Flags & ImGuiTableFlags_NoClip) ? 1 : table->ColumnsEnabledCount; + const int channels_for_bg = 1 + 1 * freeze_row_multiplier; + const int channels_for_dummy = (table->ColumnsEnabledCount < table->ColumnsCount || table->VisibleMaskByIndex != table->EnabledMaskByIndex) ? +1 : 0; + const int channels_total = channels_for_bg + (channels_for_row * freeze_row_multiplier) + channels_for_dummy; + table->DrawSplitter->Split(table->InnerWindow->DrawList, channels_total); + table->DummyDrawChannel = (ImGuiTableDrawChannelIdx)((channels_for_dummy > 0) ? channels_total - 1 : -1); + table->Bg2DrawChannelCurrent = TABLE_DRAW_CHANNEL_BG2_FROZEN; + table->Bg2DrawChannelUnfrozen = (ImGuiTableDrawChannelIdx)((table->FreezeRowsCount > 0) ? 2 + channels_for_row : TABLE_DRAW_CHANNEL_BG2_FROZEN); + + int draw_channel_current = 2; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->IsVisibleX && column->IsVisibleY) + { + column->DrawChannelFrozen = (ImGuiTableDrawChannelIdx)(draw_channel_current); + column->DrawChannelUnfrozen = (ImGuiTableDrawChannelIdx)(draw_channel_current + (table->FreezeRowsCount > 0 ? channels_for_row + 1 : 0)); + if (!(table->Flags & ImGuiTableFlags_NoClip)) + draw_channel_current++; + } + else + { + column->DrawChannelFrozen = column->DrawChannelUnfrozen = table->DummyDrawChannel; + } + column->DrawChannelCurrent = column->DrawChannelFrozen; + } + + // Initial draw cmd starts with a BgClipRect that matches the one of its host, to facilitate merge draw commands by default. + // All our cell highlight are manually clipped with BgClipRect. When unfreezing it will be made smaller to fit scrolling rect. + // (This technically isn't part of setting up draw channels, but is reasonably related to be done here) + table->BgClipRect = table->InnerClipRect; + table->Bg0ClipRectForDrawCmd = table->OuterWindow->ClipRect; + table->Bg2ClipRectForDrawCmd = table->HostClipRect; + IM_ASSERT(table->BgClipRect.Min.y <= table->BgClipRect.Max.y); +} + +// This function reorder draw channels based on matching clip rectangle, to facilitate merging them. Called by EndTable(). +// For simplicity we call it TableMergeDrawChannels() but in fact it only reorder channels + overwrite ClipRect, +// actual merging is done by table->DrawSplitter.Merge() which is called right after TableMergeDrawChannels(). +// +// Columns where the contents didn't stray off their local clip rectangle can be merged. To achieve +// this we merge their clip rect and make them contiguous in the channel list, so they can be merged +// by the call to DrawSplitter.Merge() following to the call to this function. +// We reorder draw commands by arranging them into a maximum of 4 distinct groups: +// +// 1 group: 2 groups: 2 groups: 4 groups: +// [ 0. ] no freeze [ 0. ] row freeze [ 01 ] col freeze [ 01 ] row+col freeze +// [ .. ] or no scroll [ 2. ] and v-scroll [ .. ] and h-scroll [ 23 ] and v+h-scroll +// +// Each column itself can use 1 channel (row freeze disabled) or 2 channels (row freeze enabled). +// When the contents of a column didn't stray off its limit, we move its channels into the corresponding group +// based on its position (within frozen rows/columns groups or not). +// At the end of the operation our 1-4 groups will each have a ImDrawCmd using the same ClipRect. +// This function assume that each column are pointing to a distinct draw channel, +// otherwise merge_group->ChannelsCount will not match set bit count of merge_group->ChannelsMask. +// +// Column channels will not be merged into one of the 1-4 groups in the following cases: +// - The contents stray off its clipping rectangle (we only compare the MaxX value, not the MinX value). +// Direct ImDrawList calls won't be taken into account by default, if you use them make sure the ImGui:: bounds +// matches, by e.g. calling SetCursorScreenPos(). +// - The channel uses more than one draw command itself. We drop all our attempt at merging stuff here.. +// we could do better but it's going to be rare and probably not worth the hassle. +// Columns for which the draw channel(s) haven't been merged with other will use their own ImDrawCmd. +// +// This function is particularly tricky to understand.. take a breath. +void ImGui::TableMergeDrawChannels(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + ImDrawListSplitter* splitter = table->DrawSplitter; + const bool has_freeze_v = (table->FreezeRowsCount > 0); + const bool has_freeze_h = (table->FreezeColumnsCount > 0); + IM_ASSERT(splitter->_Current == 0); + + // Track which groups we are going to attempt to merge, and which channels goes into each group. + struct MergeGroup + { + ImRect ClipRect; + int ChannelsCount; + ImBitArray ChannelsMask; + + MergeGroup() { ChannelsCount = 0; } + }; + int merge_group_mask = 0x00; + MergeGroup merge_groups[4]; + + // 1. Scan channels and take note of those which can be merged + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + if ((table->VisibleMaskByIndex & ((ImU64)1 << column_n)) == 0) + continue; + ImGuiTableColumn* column = &table->Columns[column_n]; + + const int merge_group_sub_count = has_freeze_v ? 2 : 1; + for (int merge_group_sub_n = 0; merge_group_sub_n < merge_group_sub_count; merge_group_sub_n++) + { + const int channel_no = (merge_group_sub_n == 0) ? column->DrawChannelFrozen : column->DrawChannelUnfrozen; + + // Don't attempt to merge if there are multiple draw calls within the column + ImDrawChannel* src_channel = &splitter->_Channels[channel_no]; + if (src_channel->_CmdBuffer.Size > 0 && src_channel->_CmdBuffer.back().ElemCount == 0 && src_channel->_CmdBuffer.back().UserCallback != NULL) // Equivalent of PopUnusedDrawCmd() + src_channel->_CmdBuffer.pop_back(); + if (src_channel->_CmdBuffer.Size != 1) + continue; + + // Find out the width of this merge group and check if it will fit in our column + // (note that we assume that rendering didn't stray on the left direction. we should need a CursorMinPos to detect it) + if (!(column->Flags & ImGuiTableColumnFlags_NoClip)) + { + float content_max_x; + if (!has_freeze_v) + content_max_x = ImMax(column->ContentMaxXUnfrozen, column->ContentMaxXHeadersUsed); // No row freeze + else if (merge_group_sub_n == 0) + content_max_x = ImMax(column->ContentMaxXFrozen, column->ContentMaxXHeadersUsed); // Row freeze: use width before freeze + else + content_max_x = column->ContentMaxXUnfrozen; // Row freeze: use width after freeze + if (content_max_x > column->ClipRect.Max.x) + continue; + } + + const int merge_group_n = (has_freeze_h && column_n < table->FreezeColumnsCount ? 0 : 1) + (has_freeze_v && merge_group_sub_n == 0 ? 0 : 2); + IM_ASSERT(channel_no < IMGUI_TABLE_MAX_DRAW_CHANNELS); + MergeGroup* merge_group = &merge_groups[merge_group_n]; + if (merge_group->ChannelsCount == 0) + merge_group->ClipRect = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); + merge_group->ChannelsMask.SetBit(channel_no); + merge_group->ChannelsCount++; + merge_group->ClipRect.Add(src_channel->_CmdBuffer[0].ClipRect); + merge_group_mask |= (1 << merge_group_n); + } + + // Invalidate current draw channel + // (we don't clear DrawChannelFrozen/DrawChannelUnfrozen solely to facilitate debugging/later inspection of data) + column->DrawChannelCurrent = (ImGuiTableDrawChannelIdx)-1; + } + + // [DEBUG] Display merge groups +#if 0 + if (g.IO.KeyShift) + for (int merge_group_n = 0; merge_group_n < IM_ARRAYSIZE(merge_groups); merge_group_n++) + { + MergeGroup* merge_group = &merge_groups[merge_group_n]; + if (merge_group->ChannelsCount == 0) + continue; + char buf[32]; + ImFormatString(buf, 32, "MG%d:%d", merge_group_n, merge_group->ChannelsCount); + ImVec2 text_pos = merge_group->ClipRect.Min + ImVec2(4, 4); + ImVec2 text_size = CalcTextSize(buf, NULL); + GetForegroundDrawList()->AddRectFilled(text_pos, text_pos + text_size, IM_COL32(0, 0, 0, 255)); + GetForegroundDrawList()->AddText(text_pos, IM_COL32(255, 255, 0, 255), buf, NULL); + GetForegroundDrawList()->AddRect(merge_group->ClipRect.Min, merge_group->ClipRect.Max, IM_COL32(255, 255, 0, 255)); + } +#endif + + // 2. Rewrite channel list in our preferred order + if (merge_group_mask != 0) + { + // We skip channel 0 (Bg0/Bg1) and 1 (Bg2 frozen) from the shuffling since they won't move - see channels allocation in TableSetupDrawChannels(). + const int LEADING_DRAW_CHANNELS = 2; + g.DrawChannelsTempMergeBuffer.resize(splitter->_Count - LEADING_DRAW_CHANNELS); // Use shared temporary storage so the allocation gets amortized + ImDrawChannel* dst_tmp = g.DrawChannelsTempMergeBuffer.Data; + ImBitArray remaining_mask; // We need 132-bit of storage + remaining_mask.SetBitRange(LEADING_DRAW_CHANNELS, splitter->_Count); + remaining_mask.ClearBit(table->Bg2DrawChannelUnfrozen); + IM_ASSERT(has_freeze_v == false || table->Bg2DrawChannelUnfrozen != TABLE_DRAW_CHANNEL_BG2_FROZEN); + int remaining_count = splitter->_Count - (has_freeze_v ? LEADING_DRAW_CHANNELS + 1 : LEADING_DRAW_CHANNELS); + //ImRect host_rect = (table->InnerWindow == table->OuterWindow) ? table->InnerClipRect : table->HostClipRect; + ImRect host_rect = table->HostClipRect; + for (int merge_group_n = 0; merge_group_n < IM_ARRAYSIZE(merge_groups); merge_group_n++) + { + if (int merge_channels_count = merge_groups[merge_group_n].ChannelsCount) + { + MergeGroup* merge_group = &merge_groups[merge_group_n]; + ImRect merge_clip_rect = merge_group->ClipRect; + + // Extend outer-most clip limits to match those of host, so draw calls can be merged even if + // outer-most columns have some outer padding offsetting them from their parent ClipRect. + // The principal cases this is dealing with are: + // - On a same-window table (not scrolling = single group), all fitting columns ClipRect -> will extend and match host ClipRect -> will merge + // - Columns can use padding and have left-most ClipRect.Min.x and right-most ClipRect.Max.x != from host ClipRect -> will extend and match host ClipRect -> will merge + // FIXME-TABLE FIXME-WORKRECT: We are wasting a merge opportunity on tables without scrolling if column doesn't fit + // within host clip rect, solely because of the half-padding difference between window->WorkRect and window->InnerClipRect. + if ((merge_group_n & 1) == 0 || !has_freeze_h) + merge_clip_rect.Min.x = ImMin(merge_clip_rect.Min.x, host_rect.Min.x); + if ((merge_group_n & 2) == 0 || !has_freeze_v) + merge_clip_rect.Min.y = ImMin(merge_clip_rect.Min.y, host_rect.Min.y); + if ((merge_group_n & 1) != 0) + merge_clip_rect.Max.x = ImMax(merge_clip_rect.Max.x, host_rect.Max.x); + if ((merge_group_n & 2) != 0 && (table->Flags & ImGuiTableFlags_NoHostExtendY) == 0) + merge_clip_rect.Max.y = ImMax(merge_clip_rect.Max.y, host_rect.Max.y); +#if 0 + GetOverlayDrawList()->AddRect(merge_group->ClipRect.Min, merge_group->ClipRect.Max, IM_COL32(255, 0, 0, 200), 0.0f, 0, 1.0f); + GetOverlayDrawList()->AddLine(merge_group->ClipRect.Min, merge_clip_rect.Min, IM_COL32(255, 100, 0, 200)); + GetOverlayDrawList()->AddLine(merge_group->ClipRect.Max, merge_clip_rect.Max, IM_COL32(255, 100, 0, 200)); +#endif + remaining_count -= merge_group->ChannelsCount; + for (int n = 0; n < IM_ARRAYSIZE(remaining_mask.Storage); n++) + remaining_mask.Storage[n] &= ~merge_group->ChannelsMask.Storage[n]; + for (int n = 0; n < splitter->_Count && merge_channels_count != 0; n++) + { + // Copy + overwrite new clip rect + if (!merge_group->ChannelsMask.TestBit(n)) + continue; + merge_group->ChannelsMask.ClearBit(n); + merge_channels_count--; + + ImDrawChannel* channel = &splitter->_Channels[n]; + IM_ASSERT(channel->_CmdBuffer.Size == 1 && merge_clip_rect.Contains(ImRect(channel->_CmdBuffer[0].ClipRect))); + channel->_CmdBuffer[0].ClipRect = merge_clip_rect.ToVec4(); + memcpy(dst_tmp++, channel, sizeof(ImDrawChannel)); + } + } + + // Make sure Bg2DrawChannelUnfrozen appears in the middle of our groups (whereas Bg0/Bg1 and Bg2 frozen are fixed to 0 and 1) + if (merge_group_n == 1 && has_freeze_v) + memcpy(dst_tmp++, &splitter->_Channels[table->Bg2DrawChannelUnfrozen], sizeof(ImDrawChannel)); + } + + // Append unmergeable channels that we didn't reorder at the end of the list + for (int n = 0; n < splitter->_Count && remaining_count != 0; n++) + { + if (!remaining_mask.TestBit(n)) + continue; + ImDrawChannel* channel = &splitter->_Channels[n]; + memcpy(dst_tmp++, channel, sizeof(ImDrawChannel)); + remaining_count--; + } + IM_ASSERT(dst_tmp == g.DrawChannelsTempMergeBuffer.Data + g.DrawChannelsTempMergeBuffer.Size); + memcpy(splitter->_Channels.Data + LEADING_DRAW_CHANNELS, g.DrawChannelsTempMergeBuffer.Data, (splitter->_Count - LEADING_DRAW_CHANNELS) * sizeof(ImDrawChannel)); + } +} + +// FIXME-TABLE: This is a mess, need to redesign how we render borders (as some are also done in TableEndRow) +void ImGui::TableDrawBorders(ImGuiTable* table) +{ + ImGuiWindow* inner_window = table->InnerWindow; + if (!table->OuterWindow->ClipRect.Overlaps(table->OuterRect)) + return; + + ImDrawList* inner_drawlist = inner_window->DrawList; + table->DrawSplitter->SetCurrentChannel(inner_drawlist, TABLE_DRAW_CHANNEL_BG0); + inner_drawlist->PushClipRect(table->Bg0ClipRectForDrawCmd.Min, table->Bg0ClipRectForDrawCmd.Max, false); + + // Draw inner border and resizing feedback + const float border_size = TABLE_BORDER_SIZE; + const float draw_y1 = table->InnerRect.Min.y; + const float draw_y2_body = table->InnerRect.Max.y; + const float draw_y2_head = table->IsUsingHeaders ? ImMin(table->InnerRect.Max.y, (table->FreezeRowsCount >= 1 ? table->InnerRect.Min.y : table->WorkRect.Min.y) + table->LastFirstRowHeight) : draw_y1; + if (table->Flags & ImGuiTableFlags_BordersInnerV) + { + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + if (!(table->EnabledMaskByDisplayOrder & ((ImU64)1 << order_n))) + continue; + + const int column_n = table->DisplayOrderToIndex[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + const bool is_hovered = (table->HoveredColumnBorder == column_n); + const bool is_resized = (table->ResizedColumn == column_n) && (table->InstanceInteracted == table->InstanceCurrent); + const bool is_resizable = (column->Flags & (ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_NoDirectResize_)) == 0; + const bool is_frozen_separator = (table->FreezeColumnsCount == order_n + 1); + if (column->MaxX > table->InnerClipRect.Max.x && !is_resized) + continue; + + // Decide whether right-most column is visible + if (column->NextEnabledColumn == -1 && !is_resizable) + if ((table->Flags & ImGuiTableFlags_SizingMask_) != ImGuiTableFlags_SizingFixedSame || (table->Flags & ImGuiTableFlags_NoHostExtendX)) + continue; + if (column->MaxX <= column->ClipRect.Min.x) // FIXME-TABLE FIXME-STYLE: Assume BorderSize==1, this is problematic if we want to increase the border size.. + continue; + + // Draw in outer window so right-most column won't be clipped + // Always draw full height border when being resized/hovered, or on the delimitation of frozen column scrolling. + ImU32 col; + float draw_y2; + if (is_hovered || is_resized || is_frozen_separator) + { + draw_y2 = draw_y2_body; + col = is_resized ? GetColorU32(ImGuiCol_SeparatorActive) : is_hovered ? GetColorU32(ImGuiCol_SeparatorHovered) : table->BorderColorStrong; + } + else + { + draw_y2 = (table->Flags & (ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_NoBordersInBodyUntilResize)) ? draw_y2_head : draw_y2_body; + col = (table->Flags & (ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_NoBordersInBodyUntilResize)) ? table->BorderColorStrong : table->BorderColorLight; + } + + if (draw_y2 > draw_y1) + inner_drawlist->AddLine(ImVec2(column->MaxX, draw_y1), ImVec2(column->MaxX, draw_y2), col, border_size); + } + } + + // Draw outer border + // FIXME: could use AddRect or explicit VLine/HLine helper? + if (table->Flags & ImGuiTableFlags_BordersOuter) + { + // Display outer border offset by 1 which is a simple way to display it without adding an extra draw call + // (Without the offset, in outer_window it would be rendered behind cells, because child windows are above their + // parent. In inner_window, it won't reach out over scrollbars. Another weird solution would be to display part + // of it in inner window, and the part that's over scrollbars in the outer window..) + // Either solution currently won't allow us to use a larger border size: the border would clipped. + const ImRect outer_border = table->OuterRect; + const ImU32 outer_col = table->BorderColorStrong; + if ((table->Flags & ImGuiTableFlags_BordersOuter) == ImGuiTableFlags_BordersOuter) + { + inner_drawlist->AddRect(outer_border.Min, outer_border.Max, outer_col, 0.0f, 0, border_size); + } + else if (table->Flags & ImGuiTableFlags_BordersOuterV) + { + inner_drawlist->AddLine(outer_border.Min, ImVec2(outer_border.Min.x, outer_border.Max.y), outer_col, border_size); + inner_drawlist->AddLine(ImVec2(outer_border.Max.x, outer_border.Min.y), outer_border.Max, outer_col, border_size); + } + else if (table->Flags & ImGuiTableFlags_BordersOuterH) + { + inner_drawlist->AddLine(outer_border.Min, ImVec2(outer_border.Max.x, outer_border.Min.y), outer_col, border_size); + inner_drawlist->AddLine(ImVec2(outer_border.Min.x, outer_border.Max.y), outer_border.Max, outer_col, border_size); + } + } + if ((table->Flags & ImGuiTableFlags_BordersInnerH) && table->RowPosY2 < table->OuterRect.Max.y) + { + // Draw bottom-most row border + const float border_y = table->RowPosY2; + if (border_y >= table->BgClipRect.Min.y && border_y < table->BgClipRect.Max.y) + inner_drawlist->AddLine(ImVec2(table->BorderX1, border_y), ImVec2(table->BorderX2, border_y), table->BorderColorLight, border_size); + } + + inner_drawlist->PopClipRect(); +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Sorting +//------------------------------------------------------------------------- +// - TableGetSortSpecs() +// - TableFixColumnSortDirection() [Internal] +// - TableGetColumnNextSortDirection() [Internal] +// - TableSetColumnSortDirection() [Internal] +// - TableSortSpecsSanitize() [Internal] +// - TableSortSpecsBuild() [Internal] +//------------------------------------------------------------------------- + +// Return NULL if no sort specs (most often when ImGuiTableFlags_Sortable is not set) +// You can sort your data again when 'SpecsChanged == true'. It will be true with sorting specs have changed since +// last call, or the first time. +// Lifetime: don't hold on this pointer over multiple frames or past any subsequent call to BeginTable()! +ImGuiTableSortSpecs* ImGui::TableGetSortSpecs() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL); + + if (!(table->Flags & ImGuiTableFlags_Sortable)) + return NULL; + + // Require layout (in case TableHeadersRow() hasn't been called) as it may alter IsSortSpecsDirty in some paths. + if (!table->IsLayoutLocked) + TableUpdateLayout(table); + + TableSortSpecsBuild(table); + + return &table->SortSpecs; +} + +static inline ImGuiSortDirection TableGetColumnAvailSortDirection(ImGuiTableColumn* column, int n) +{ + IM_ASSERT(n < column->SortDirectionsAvailCount); + return (column->SortDirectionsAvailList >> (n << 1)) & 0x03; +} + +// Fix sort direction if currently set on a value which is unavailable (e.g. activating NoSortAscending/NoSortDescending) +void ImGui::TableFixColumnSortDirection(ImGuiTable* table, ImGuiTableColumn* column) +{ + if (column->SortOrder == -1 || (column->SortDirectionsAvailMask & (1 << column->SortDirection)) != 0) + return; + column->SortDirection = (ImU8)TableGetColumnAvailSortDirection(column, 0); + table->IsSortSpecsDirty = true; +} + +// Calculate next sort direction that would be set after clicking the column +// - If the PreferSortDescending flag is set, we will default to a Descending direction on the first click. +// - Note that the PreferSortAscending flag is never checked, it is essentially the default and therefore a no-op. +IM_STATIC_ASSERT(ImGuiSortDirection_None == 0 && ImGuiSortDirection_Ascending == 1 && ImGuiSortDirection_Descending == 2); +ImGuiSortDirection ImGui::TableGetColumnNextSortDirection(ImGuiTableColumn* column) +{ + IM_ASSERT(column->SortDirectionsAvailCount > 0); + if (column->SortOrder == -1) + return TableGetColumnAvailSortDirection(column, 0); + for (int n = 0; n < 3; n++) + if (column->SortDirection == TableGetColumnAvailSortDirection(column, n)) + return TableGetColumnAvailSortDirection(column, (n + 1) % column->SortDirectionsAvailCount); + IM_ASSERT(0); + return ImGuiSortDirection_None; +} + +// Note that the NoSortAscending/NoSortDescending flags are processed in TableSortSpecsSanitize(), and they may change/revert +// the value of SortDirection. We could technically also do it here but it would be unnecessary and duplicate code. +void ImGui::TableSetColumnSortDirection(int column_n, ImGuiSortDirection sort_direction, bool append_to_sort_specs) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + + if (!(table->Flags & ImGuiTableFlags_SortMulti)) + append_to_sort_specs = false; + if (!(table->Flags & ImGuiTableFlags_SortTristate)) + IM_ASSERT(sort_direction != ImGuiSortDirection_None); + + ImGuiTableColumnIdx sort_order_max = 0; + if (append_to_sort_specs) + for (int other_column_n = 0; other_column_n < table->ColumnsCount; other_column_n++) + sort_order_max = ImMax(sort_order_max, table->Columns[other_column_n].SortOrder); + + ImGuiTableColumn* column = &table->Columns[column_n]; + column->SortDirection = (ImU8)sort_direction; + if (column->SortDirection == ImGuiSortDirection_None) + column->SortOrder = -1; + else if (column->SortOrder == -1 || !append_to_sort_specs) + column->SortOrder = append_to_sort_specs ? sort_order_max + 1 : 0; + + for (int other_column_n = 0; other_column_n < table->ColumnsCount; other_column_n++) + { + ImGuiTableColumn* other_column = &table->Columns[other_column_n]; + if (other_column != column && !append_to_sort_specs) + other_column->SortOrder = -1; + TableFixColumnSortDirection(table, other_column); + } + table->IsSettingsDirty = true; + table->IsSortSpecsDirty = true; +} + +void ImGui::TableSortSpecsSanitize(ImGuiTable* table) +{ + IM_ASSERT(table->Flags & ImGuiTableFlags_Sortable); + + // Clear SortOrder from hidden column and verify that there's no gap or duplicate. + int sort_order_count = 0; + ImU64 sort_order_mask = 0x00; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->SortOrder != -1 && !column->IsEnabled) + column->SortOrder = -1; + if (column->SortOrder == -1) + continue; + sort_order_count++; + sort_order_mask |= ((ImU64)1 << column->SortOrder); + IM_ASSERT(sort_order_count < (int)sizeof(sort_order_mask) * 8); + } + + const bool need_fix_linearize = ((ImU64)1 << sort_order_count) != (sort_order_mask + 1); + const bool need_fix_single_sort_order = (sort_order_count > 1) && !(table->Flags & ImGuiTableFlags_SortMulti); + if (need_fix_linearize || need_fix_single_sort_order) + { + ImU64 fixed_mask = 0x00; + for (int sort_n = 0; sort_n < sort_order_count; sort_n++) + { + // Fix: Rewrite sort order fields if needed so they have no gap or duplicate. + // (e.g. SortOrder 0 disappeared, SortOrder 1..2 exists --> rewrite then as SortOrder 0..1) + int column_with_smallest_sort_order = -1; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if ((fixed_mask & ((ImU64)1 << (ImU64)column_n)) == 0 && table->Columns[column_n].SortOrder != -1) + if (column_with_smallest_sort_order == -1 || table->Columns[column_n].SortOrder < table->Columns[column_with_smallest_sort_order].SortOrder) + column_with_smallest_sort_order = column_n; + IM_ASSERT(column_with_smallest_sort_order != -1); + fixed_mask |= ((ImU64)1 << column_with_smallest_sort_order); + table->Columns[column_with_smallest_sort_order].SortOrder = (ImGuiTableColumnIdx)sort_n; + + // Fix: Make sure only one column has a SortOrder if ImGuiTableFlags_MultiSortable is not set. + if (need_fix_single_sort_order) + { + sort_order_count = 1; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if (column_n != column_with_smallest_sort_order) + table->Columns[column_n].SortOrder = -1; + break; + } + } + } + + // Fallback default sort order (if no column had the ImGuiTableColumnFlags_DefaultSort flag) + if (sort_order_count == 0 && !(table->Flags & ImGuiTableFlags_SortTristate)) + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->IsEnabled && !(column->Flags & ImGuiTableColumnFlags_NoSort)) + { + sort_order_count = 1; + column->SortOrder = 0; + column->SortDirection = (ImU8)TableGetColumnAvailSortDirection(column, 0); + break; + } + } + + table->SortSpecsCount = (ImGuiTableColumnIdx)sort_order_count; +} + +void ImGui::TableSortSpecsBuild(ImGuiTable* table) +{ + bool dirty = table->IsSortSpecsDirty; + if (dirty) + { + TableSortSpecsSanitize(table); + table->SortSpecsMulti.resize(table->SortSpecsCount <= 1 ? 0 : table->SortSpecsCount); + table->SortSpecs.SpecsDirty = true; // Mark as dirty for user + table->IsSortSpecsDirty = false; // Mark as not dirty for us + } + + // Write output + ImGuiTableColumnSortSpecs* sort_specs = (table->SortSpecsCount == 0) ? NULL : (table->SortSpecsCount == 1) ? &table->SortSpecsSingle : table->SortSpecsMulti.Data; + if (dirty && sort_specs != NULL) + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->SortOrder == -1) + continue; + IM_ASSERT(column->SortOrder < table->SortSpecsCount); + ImGuiTableColumnSortSpecs* sort_spec = &sort_specs[column->SortOrder]; + sort_spec->ColumnUserID = column->UserID; + sort_spec->ColumnIndex = (ImGuiTableColumnIdx)column_n; + sort_spec->SortOrder = (ImGuiTableColumnIdx)column->SortOrder; + sort_spec->SortDirection = column->SortDirection; + } + + table->SortSpecs.Specs = sort_specs; + table->SortSpecs.SpecsCount = table->SortSpecsCount; +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Headers +//------------------------------------------------------------------------- +// - TableGetHeaderRowHeight() [Internal] +// - TableHeadersRow() +// - TableHeader() +//------------------------------------------------------------------------- + +float ImGui::TableGetHeaderRowHeight() +{ + // Caring for a minor edge case: + // Calculate row height, for the unlikely case that some labels may be taller than others. + // If we didn't do that, uneven header height would highlight but smaller one before the tallest wouldn't catch input for all height. + // In your custom header row you may omit this all together and just call TableNextRow() without a height... + float row_height = GetTextLineHeight(); + int columns_count = TableGetColumnCount(); + for (int column_n = 0; column_n < columns_count; column_n++) + { + ImGuiTableColumnFlags flags = TableGetColumnFlags(column_n); + if ((flags & ImGuiTableColumnFlags_IsEnabled) && !(flags & ImGuiTableColumnFlags_NoHeaderLabel)) + row_height = ImMax(row_height, CalcTextSize(TableGetColumnName(column_n)).y); + } + row_height += GetStyle().CellPadding.y * 2.0f; + return row_height; +} + +// [Public] This is a helper to output TableHeader() calls based on the column names declared in TableSetupColumn(). +// The intent is that advanced users willing to create customized headers would not need to use this helper +// and can create their own! For example: TableHeader() may be preceeded by Checkbox() or other custom widgets. +// See 'Demo->Tables->Custom headers' for a demonstration of implementing a custom version of this. +// This code is constructed to not make much use of internal functions, as it is intended to be a template to copy. +// FIXME-TABLE: TableOpenContextMenu() and TableGetHeaderRowHeight() are not public. +void ImGui::TableHeadersRow() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL && "Need to call TableHeadersRow() after BeginTable()!"); + + // Layout if not already done (this is automatically done by TableNextRow, we do it here solely to facilitate stepping in debugger as it is frequent to step in TableUpdateLayout) + if (!table->IsLayoutLocked) + TableUpdateLayout(table); + + // Open row + const float row_y1 = GetCursorScreenPos().y; + const float row_height = TableGetHeaderRowHeight(); + TableNextRow(ImGuiTableRowFlags_Headers, row_height); + if (table->HostSkipItems) // Merely an optimization, you may skip in your own code. + return; + + const int columns_count = TableGetColumnCount(); + for (int column_n = 0; column_n < columns_count; column_n++) + { + if (!TableSetColumnIndex(column_n)) + continue; + + // Push an id to allow unnamed labels (generally accidental, but let's behave nicely with them) + // - in your own code you may omit the PushID/PopID all-together, provided you know they won't collide + // - table->InstanceCurrent is only >0 when we use multiple BeginTable/EndTable calls with same identifier. + const char* name = (TableGetColumnFlags(column_n) & ImGuiTableColumnFlags_NoHeaderLabel) ? "" : TableGetColumnName(column_n); + PushID(table->InstanceCurrent * table->ColumnsCount + column_n); + TableHeader(name); + PopID(); + } + + // Allow opening popup from the right-most section after the last column. + ImVec2 mouse_pos = ImGui::GetMousePos(); + if (IsMouseReleased(1) && TableGetHoveredColumn() == columns_count) + if (mouse_pos.y >= row_y1 && mouse_pos.y < row_y1 + row_height) + TableOpenContextMenu(-1); // Will open a non-column-specific popup. +} + +// Emit a column header (text + optional sort order) +// We cpu-clip text here so that all columns headers can be merged into a same draw call. +// Note that because of how we cpu-clip and display sorting indicators, you _cannot_ use SameLine() after a TableHeader() +void ImGui::TableHeader(const char* label) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL && "Need to call TableHeader() after BeginTable()!"); + IM_ASSERT(table->CurrentColumn != -1); + const int column_n = table->CurrentColumn; + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Label + if (label == NULL) + label = ""; + const char* label_end = FindRenderedTextEnd(label); + ImVec2 label_size = CalcTextSize(label, label_end, true); + ImVec2 label_pos = window->DC.CursorPos; + + // If we already got a row height, there's use that. + // FIXME-TABLE: Padding problem if the correct outer-padding CellBgRect strays off our ClipRect? + ImRect cell_r = TableGetCellBgRect(table, column_n); + float label_height = ImMax(label_size.y, table->RowMinHeight - table->CellPaddingY * 2.0f); + + // Calculate ideal size for sort order arrow + float w_arrow = 0.0f; + float w_sort_text = 0.0f; + char sort_order_suf[4] = ""; + const float ARROW_SCALE = 0.65f; + if ((table->Flags & ImGuiTableFlags_Sortable) && !(column->Flags & ImGuiTableColumnFlags_NoSort)) + { + w_arrow = ImFloor(g.FontSize * ARROW_SCALE + g.Style.FramePadding.x); + if (column->SortOrder > 0) + { + ImFormatString(sort_order_suf, IM_ARRAYSIZE(sort_order_suf), "%d", column->SortOrder + 1); + w_sort_text = g.Style.ItemInnerSpacing.x + CalcTextSize(sort_order_suf).x; + } + } + + // We feed our unclipped width to the column without writing on CursorMaxPos, so that column is still considering for merging. + float max_pos_x = label_pos.x + label_size.x + w_sort_text + w_arrow; + column->ContentMaxXHeadersUsed = ImMax(column->ContentMaxXHeadersUsed, column->WorkMaxX); + column->ContentMaxXHeadersIdeal = ImMax(column->ContentMaxXHeadersIdeal, max_pos_x); + + // Keep header highlighted when context menu is open. + const bool selected = (table->IsContextPopupOpen && table->ContextPopupColumn == column_n && table->InstanceInteracted == table->InstanceCurrent); + ImGuiID id = window->GetID(label); + ImRect bb(cell_r.Min.x, cell_r.Min.y, cell_r.Max.x, ImMax(cell_r.Max.y, cell_r.Min.y + label_height + g.Style.CellPadding.y * 2.0f)); + ItemSize(ImVec2(0.0f, label_height)); // Don't declare unclipped width, it'll be fed ContentMaxPosHeadersIdeal + if (!ItemAdd(bb, id)) + return; + + //GetForegroundDrawList()->AddRect(cell_r.Min, cell_r.Max, IM_COL32(255, 0, 0, 255)); // [DEBUG] + //GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(255, 0, 0, 255)); // [DEBUG] + + // Using AllowItemOverlap mode because we cover the whole cell, and we want user to be able to submit subsequent items. + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_AllowItemOverlap); + if (g.ActiveId != id) + SetItemAllowOverlap(); + if (held || hovered || selected) + { + const ImU32 col = GetColorU32(held ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + //RenderFrame(bb.Min, bb.Max, col, false, 0.0f); + TableSetBgColor(ImGuiTableBgTarget_CellBg, col, table->CurrentColumn); + } + else + { + // Submit single cell bg color in the case we didn't submit a full header row + if ((table->RowFlags & ImGuiTableRowFlags_Headers) == 0) + TableSetBgColor(ImGuiTableBgTarget_CellBg, GetColorU32(ImGuiCol_TableHeaderBg), table->CurrentColumn); + } + RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding); + if (held) + table->HeldHeaderColumn = (ImGuiTableColumnIdx)column_n; + window->DC.CursorPos.y -= g.Style.ItemSpacing.y * 0.5f; + + // Drag and drop to re-order columns. + // FIXME-TABLE: Scroll request while reordering a column and it lands out of the scrolling zone. + if (held && (table->Flags & ImGuiTableFlags_Reorderable) && IsMouseDragging(0) && !g.DragDropActive) + { + // While moving a column it will jump on the other side of the mouse, so we also test for MouseDelta.x + table->ReorderColumn = (ImGuiTableColumnIdx)column_n; + table->InstanceInteracted = table->InstanceCurrent; + + // We don't reorder: through the frozen<>unfrozen line, or through a column that is marked with ImGuiTableColumnFlags_NoReorder. + if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < cell_r.Min.x) + if (ImGuiTableColumn* prev_column = (column->PrevEnabledColumn != -1) ? &table->Columns[column->PrevEnabledColumn] : NULL) + if (!((column->Flags | prev_column->Flags) & ImGuiTableColumnFlags_NoReorder)) + if ((column->IndexWithinEnabledSet < table->FreezeColumnsRequest) == (prev_column->IndexWithinEnabledSet < table->FreezeColumnsRequest)) + table->ReorderColumnDir = -1; + if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > cell_r.Max.x) + if (ImGuiTableColumn* next_column = (column->NextEnabledColumn != -1) ? &table->Columns[column->NextEnabledColumn] : NULL) + if (!((column->Flags | next_column->Flags) & ImGuiTableColumnFlags_NoReorder)) + if ((column->IndexWithinEnabledSet < table->FreezeColumnsRequest) == (next_column->IndexWithinEnabledSet < table->FreezeColumnsRequest)) + table->ReorderColumnDir = +1; + } + + // Sort order arrow + const float ellipsis_max = cell_r.Max.x - w_arrow - w_sort_text; + if ((table->Flags & ImGuiTableFlags_Sortable) && !(column->Flags & ImGuiTableColumnFlags_NoSort)) + { + if (column->SortOrder != -1) + { + float x = ImMax(cell_r.Min.x, cell_r.Max.x - w_arrow - w_sort_text); + float y = label_pos.y; + if (column->SortOrder > 0) + { + PushStyleColor(ImGuiCol_Text, GetColorU32(ImGuiCol_Text, 0.70f)); + RenderText(ImVec2(x + g.Style.ItemInnerSpacing.x, y), sort_order_suf); + PopStyleColor(); + x += w_sort_text; + } + RenderArrow(window->DrawList, ImVec2(x, y), GetColorU32(ImGuiCol_Text), column->SortDirection == ImGuiSortDirection_Ascending ? ImGuiDir_Up : ImGuiDir_Down, ARROW_SCALE); + } + + // Handle clicking on column header to adjust Sort Order + if (pressed && table->ReorderColumn != column_n) + { + ImGuiSortDirection sort_direction = TableGetColumnNextSortDirection(column); + TableSetColumnSortDirection(column_n, sort_direction, g.IO.KeyShift); + } + } + + // Render clipped label. Clipping here ensure that in the majority of situations, all our header cells will + // be merged into a single draw call. + //window->DrawList->AddCircleFilled(ImVec2(ellipsis_max, label_pos.y), 40, IM_COL32_WHITE); + RenderTextEllipsis(window->DrawList, label_pos, ImVec2(ellipsis_max, label_pos.y + label_height + g.Style.FramePadding.y), ellipsis_max, ellipsis_max, label, label_end, &label_size); + + const bool text_clipped = label_size.x > (ellipsis_max - label_pos.x); + if (text_clipped && hovered && g.HoveredIdNotActiveTimer > g.TooltipSlowDelay) + SetTooltip("%.*s", (int)(label_end - label), label); + + // We don't use BeginPopupContextItem() because we want the popup to stay up even after the column is hidden + if (IsMouseReleased(1) && IsItemHovered()) + TableOpenContextMenu(column_n); +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Context Menu +//------------------------------------------------------------------------- +// - TableOpenContextMenu() [Internal] +// - TableDrawContextMenu() [Internal] +//------------------------------------------------------------------------- + +// Use -1 to open menu not specific to a given column. +void ImGui::TableOpenContextMenu(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (column_n == -1 && table->CurrentColumn != -1) // When called within a column automatically use this one (for consistency) + column_n = table->CurrentColumn; + if (column_n == table->ColumnsCount) // To facilitate using with TableGetHoveredColumn() + column_n = -1; + IM_ASSERT(column_n >= -1 && column_n < table->ColumnsCount); + if (table->Flags & (ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable)) + { + table->IsContextPopupOpen = true; + table->ContextPopupColumn = (ImGuiTableColumnIdx)column_n; + table->InstanceInteracted = table->InstanceCurrent; + const ImGuiID context_menu_id = ImHashStr("##ContextMenu", 0, table->ID); + OpenPopupEx(context_menu_id, ImGuiPopupFlags_None); + } +} + +// Output context menu into current window (generally a popup) +// FIXME-TABLE: Ideally this should be writable by the user. Full programmatic access to that data? +void ImGui::TableDrawContextMenu(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + bool want_separator = false; + const int column_n = (table->ContextPopupColumn >= 0 && table->ContextPopupColumn < table->ColumnsCount) ? table->ContextPopupColumn : -1; + ImGuiTableColumn* column = (column_n != -1) ? &table->Columns[column_n] : NULL; + + // Sizing + if (table->Flags & ImGuiTableFlags_Resizable) + { + if (column != NULL) + { + const bool can_resize = !(column->Flags & ImGuiTableColumnFlags_NoResize) && column->IsEnabled; + if (MenuItem("Size column to fit###SizeOne", NULL, false, can_resize)) + TableSetColumnWidthAutoSingle(table, column_n); + } + + const char* size_all_desc; + if (table->ColumnsEnabledFixedCount == table->ColumnsEnabledCount && (table->Flags & ImGuiTableFlags_SizingMask_) != ImGuiTableFlags_SizingFixedSame) + size_all_desc = "Size all columns to fit###SizeAll"; // All fixed + else + size_all_desc = "Size all columns to default###SizeAll"; // All stretch or mixed + if (MenuItem(size_all_desc, NULL)) + TableSetColumnWidthAutoAll(table); + want_separator = true; + } + + // Ordering + if (table->Flags & ImGuiTableFlags_Reorderable) + { + if (MenuItem("Reset order", NULL, false, !table->IsDefaultDisplayOrder)) + table->IsResetDisplayOrderRequest = true; + want_separator = true; + } + + // Reset all (should work but seems unnecessary/noisy to expose?) + //if (MenuItem("Reset all")) + // table->IsResetAllRequest = true; + + // Sorting + // (modify TableOpenContextMenu() to add _Sortable flag if enabling this) +#if 0 + if ((table->Flags & ImGuiTableFlags_Sortable) && column != NULL && (column->Flags & ImGuiTableColumnFlags_NoSort) == 0) + { + if (want_separator) + Separator(); + want_separator = true; + + bool append_to_sort_specs = g.IO.KeyShift; + if (MenuItem("Sort in Ascending Order", NULL, column->SortOrder != -1 && column->SortDirection == ImGuiSortDirection_Ascending, (column->Flags & ImGuiTableColumnFlags_NoSortAscending) == 0)) + TableSetColumnSortDirection(table, column_n, ImGuiSortDirection_Ascending, append_to_sort_specs); + if (MenuItem("Sort in Descending Order", NULL, column->SortOrder != -1 && column->SortDirection == ImGuiSortDirection_Descending, (column->Flags & ImGuiTableColumnFlags_NoSortDescending) == 0)) + TableSetColumnSortDirection(table, column_n, ImGuiSortDirection_Descending, append_to_sort_specs); + } +#endif + + // Hiding / Visibility + if (table->Flags & ImGuiTableFlags_Hideable) + { + if (want_separator) + Separator(); + want_separator = true; + + PushItemFlag(ImGuiItemFlags_SelectableDontClosePopup, true); + for (int other_column_n = 0; other_column_n < table->ColumnsCount; other_column_n++) + { + ImGuiTableColumn* other_column = &table->Columns[other_column_n]; + if (other_column->Flags & ImGuiTableColumnFlags_Disabled) + continue; + + const char* name = TableGetColumnName(table, other_column_n); + if (name == NULL || name[0] == 0) + name = ""; + + // Make sure we can't hide the last active column + bool menu_item_active = (other_column->Flags & ImGuiTableColumnFlags_NoHide) ? false : true; + if (other_column->IsUserEnabled && table->ColumnsEnabledCount <= 1) + menu_item_active = false; + if (MenuItem(name, NULL, other_column->IsUserEnabled, menu_item_active)) + other_column->IsUserEnabledNextFrame = !other_column->IsUserEnabled; + } + PopItemFlag(); + } +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Settings (.ini data) +//------------------------------------------------------------------------- +// FIXME: The binding/finding/creating flow are too confusing. +//------------------------------------------------------------------------- +// - TableSettingsInit() [Internal] +// - TableSettingsCalcChunkSize() [Internal] +// - TableSettingsCreate() [Internal] +// - TableSettingsFindByID() [Internal] +// - TableGetBoundSettings() [Internal] +// - TableResetSettings() +// - TableSaveSettings() [Internal] +// - TableLoadSettings() [Internal] +// - TableSettingsHandler_ClearAll() [Internal] +// - TableSettingsHandler_ApplyAll() [Internal] +// - TableSettingsHandler_ReadOpen() [Internal] +// - TableSettingsHandler_ReadLine() [Internal] +// - TableSettingsHandler_WriteAll() [Internal] +// - TableSettingsInstallHandler() [Internal] +//------------------------------------------------------------------------- +// [Init] 1: TableSettingsHandler_ReadXXXX() Load and parse .ini file into TableSettings. +// [Main] 2: TableLoadSettings() When table is created, bind Table to TableSettings, serialize TableSettings data into Table. +// [Main] 3: TableSaveSettings() When table properties are modified, serialize Table data into bound or new TableSettings, mark .ini as dirty. +// [Main] 4: TableSettingsHandler_WriteAll() When .ini file is dirty (which can come from other source), save TableSettings into .ini file. +//------------------------------------------------------------------------- + +// Clear and initialize empty settings instance +static void TableSettingsInit(ImGuiTableSettings* settings, ImGuiID id, int columns_count, int columns_count_max) +{ + IM_PLACEMENT_NEW(settings) ImGuiTableSettings(); + ImGuiTableColumnSettings* settings_column = settings->GetColumnSettings(); + for (int n = 0; n < columns_count_max; n++, settings_column++) + IM_PLACEMENT_NEW(settings_column) ImGuiTableColumnSettings(); + settings->ID = id; + settings->ColumnsCount = (ImGuiTableColumnIdx)columns_count; + settings->ColumnsCountMax = (ImGuiTableColumnIdx)columns_count_max; + settings->WantApply = true; +} + +static size_t TableSettingsCalcChunkSize(int columns_count) +{ + return sizeof(ImGuiTableSettings) + (size_t)columns_count * sizeof(ImGuiTableColumnSettings); +} + +ImGuiTableSettings* ImGui::TableSettingsCreate(ImGuiID id, int columns_count) +{ + ImGuiContext& g = *GImGui; + ImGuiTableSettings* settings = g.SettingsTables.alloc_chunk(TableSettingsCalcChunkSize(columns_count)); + TableSettingsInit(settings, id, columns_count, columns_count); + return settings; +} + +// Find existing settings +ImGuiTableSettings* ImGui::TableSettingsFindByID(ImGuiID id) +{ + // FIXME-OPT: Might want to store a lookup map for this? + ImGuiContext& g = *GImGui; + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + if (settings->ID == id) + return settings; + return NULL; +} + +// Get settings for a given table, NULL if none +ImGuiTableSettings* ImGui::TableGetBoundSettings(ImGuiTable* table) +{ + if (table->SettingsOffset != -1) + { + ImGuiContext& g = *GImGui; + ImGuiTableSettings* settings = g.SettingsTables.ptr_from_offset(table->SettingsOffset); + IM_ASSERT(settings->ID == table->ID); + if (settings->ColumnsCountMax >= table->ColumnsCount) + return settings; // OK + settings->ID = 0; // Invalidate storage, we won't fit because of a count change + } + return NULL; +} + +// Restore initial state of table (with or without saved settings) +void ImGui::TableResetSettings(ImGuiTable* table) +{ + table->IsInitializing = table->IsSettingsDirty = true; + table->IsResetAllRequest = false; + table->IsSettingsRequestLoad = false; // Don't reload from ini + table->SettingsLoadedFlags = ImGuiTableFlags_None; // Mark as nothing loaded so our initialized data becomes authoritative +} + +void ImGui::TableSaveSettings(ImGuiTable* table) +{ + table->IsSettingsDirty = false; + if (table->Flags & ImGuiTableFlags_NoSavedSettings) + return; + + // Bind or create settings data + ImGuiContext& g = *GImGui; + ImGuiTableSettings* settings = TableGetBoundSettings(table); + if (settings == NULL) + { + settings = TableSettingsCreate(table->ID, table->ColumnsCount); + table->SettingsOffset = g.SettingsTables.offset_from_ptr(settings); + } + settings->ColumnsCount = (ImGuiTableColumnIdx)table->ColumnsCount; + + // Serialize ImGuiTable/ImGuiTableColumn into ImGuiTableSettings/ImGuiTableColumnSettings + IM_ASSERT(settings->ID == table->ID); + IM_ASSERT(settings->ColumnsCount == table->ColumnsCount && settings->ColumnsCountMax >= settings->ColumnsCount); + ImGuiTableColumn* column = table->Columns.Data; + ImGuiTableColumnSettings* column_settings = settings->GetColumnSettings(); + + bool save_ref_scale = false; + settings->SaveFlags = ImGuiTableFlags_None; + for (int n = 0; n < table->ColumnsCount; n++, column++, column_settings++) + { + const float width_or_weight = (column->Flags & ImGuiTableColumnFlags_WidthStretch) ? column->StretchWeight : column->WidthRequest; + column_settings->WidthOrWeight = width_or_weight; + column_settings->Index = (ImGuiTableColumnIdx)n; + column_settings->DisplayOrder = column->DisplayOrder; + column_settings->SortOrder = column->SortOrder; + column_settings->SortDirection = column->SortDirection; + column_settings->IsEnabled = column->IsUserEnabled; + column_settings->IsStretch = (column->Flags & ImGuiTableColumnFlags_WidthStretch) ? 1 : 0; + if ((column->Flags & ImGuiTableColumnFlags_WidthStretch) == 0) + save_ref_scale = true; + + // We skip saving some data in the .ini file when they are unnecessary to restore our state. + // Note that fixed width where initial width was derived from auto-fit will always be saved as InitStretchWeightOrWidth will be 0.0f. + // FIXME-TABLE: We don't have logic to easily compare SortOrder to DefaultSortOrder yet so it's always saved when present. + if (width_or_weight != column->InitStretchWeightOrWidth) + settings->SaveFlags |= ImGuiTableFlags_Resizable; + if (column->DisplayOrder != n) + settings->SaveFlags |= ImGuiTableFlags_Reorderable; + if (column->SortOrder != -1) + settings->SaveFlags |= ImGuiTableFlags_Sortable; + if (column->IsUserEnabled != ((column->Flags & ImGuiTableColumnFlags_DefaultHide) == 0)) + settings->SaveFlags |= ImGuiTableFlags_Hideable; + } + settings->SaveFlags &= table->Flags; + settings->RefScale = save_ref_scale ? table->RefScale : 0.0f; + + MarkIniSettingsDirty(); +} + +void ImGui::TableLoadSettings(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + table->IsSettingsRequestLoad = false; + if (table->Flags & ImGuiTableFlags_NoSavedSettings) + return; + + // Bind settings + ImGuiTableSettings* settings; + if (table->SettingsOffset == -1) + { + settings = TableSettingsFindByID(table->ID); + if (settings == NULL) + return; + if (settings->ColumnsCount != table->ColumnsCount) // Allow settings if columns count changed. We could otherwise decide to return... + table->IsSettingsDirty = true; + table->SettingsOffset = g.SettingsTables.offset_from_ptr(settings); + } + else + { + settings = TableGetBoundSettings(table); + } + + table->SettingsLoadedFlags = settings->SaveFlags; + table->RefScale = settings->RefScale; + + // Serialize ImGuiTableSettings/ImGuiTableColumnSettings into ImGuiTable/ImGuiTableColumn + ImGuiTableColumnSettings* column_settings = settings->GetColumnSettings(); + ImU64 display_order_mask = 0; + for (int data_n = 0; data_n < settings->ColumnsCount; data_n++, column_settings++) + { + int column_n = column_settings->Index; + if (column_n < 0 || column_n >= table->ColumnsCount) + continue; + + ImGuiTableColumn* column = &table->Columns[column_n]; + if (settings->SaveFlags & ImGuiTableFlags_Resizable) + { + if (column_settings->IsStretch) + column->StretchWeight = column_settings->WidthOrWeight; + else + column->WidthRequest = column_settings->WidthOrWeight; + column->AutoFitQueue = 0x00; + } + if (settings->SaveFlags & ImGuiTableFlags_Reorderable) + column->DisplayOrder = column_settings->DisplayOrder; + else + column->DisplayOrder = (ImGuiTableColumnIdx)column_n; + display_order_mask |= (ImU64)1 << column->DisplayOrder; + column->IsUserEnabled = column->IsUserEnabledNextFrame = column_settings->IsEnabled; + column->SortOrder = column_settings->SortOrder; + column->SortDirection = column_settings->SortDirection; + } + + // Validate and fix invalid display order data + const ImU64 expected_display_order_mask = (settings->ColumnsCount == 64) ? ~0 : ((ImU64)1 << settings->ColumnsCount) - 1; + if (display_order_mask != expected_display_order_mask) + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + table->Columns[column_n].DisplayOrder = (ImGuiTableColumnIdx)column_n; + + // Rebuild index + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + table->DisplayOrderToIndex[table->Columns[column_n].DisplayOrder] = (ImGuiTableColumnIdx)column_n; +} + +static void TableSettingsHandler_ClearAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (int i = 0; i != g.Tables.GetMapSize(); i++) + if (ImGuiTable* table = g.Tables.TryGetMapData(i)) + table->SettingsOffset = -1; + g.SettingsTables.clear(); +} + +// Apply to existing windows (if any) +static void TableSettingsHandler_ApplyAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (int i = 0; i != g.Tables.GetMapSize(); i++) + if (ImGuiTable* table = g.Tables.TryGetMapData(i)) + { + table->IsSettingsRequestLoad = true; + table->SettingsOffset = -1; + } +} + +static void* TableSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) +{ + ImGuiID id = 0; + int columns_count = 0; + if (sscanf(name, "0x%08X,%d", &id, &columns_count) < 2) + return NULL; + + if (ImGuiTableSettings* settings = ImGui::TableSettingsFindByID(id)) + { + if (settings->ColumnsCountMax >= columns_count) + { + TableSettingsInit(settings, id, columns_count, settings->ColumnsCountMax); // Recycle + return settings; + } + settings->ID = 0; // Invalidate storage, we won't fit because of a count change + } + return ImGui::TableSettingsCreate(id, columns_count); +} + +static void TableSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line) +{ + // "Column 0 UserID=0x42AD2D21 Width=100 Visible=1 Order=0 Sort=0v" + ImGuiTableSettings* settings = (ImGuiTableSettings*)entry; + float f = 0.0f; + int column_n = 0, r = 0, n = 0; + + if (sscanf(line, "RefScale=%f", &f) == 1) { settings->RefScale = f; return; } + + if (sscanf(line, "Column %d%n", &column_n, &r) == 1) + { + if (column_n < 0 || column_n >= settings->ColumnsCount) + return; + line = ImStrSkipBlank(line + r); + char c = 0; + ImGuiTableColumnSettings* column = settings->GetColumnSettings() + column_n; + column->Index = (ImGuiTableColumnIdx)column_n; + if (sscanf(line, "UserID=0x%08X%n", (ImU32*)&n, &r)==1) { line = ImStrSkipBlank(line + r); column->UserID = (ImGuiID)n; } + if (sscanf(line, "Width=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->WidthOrWeight = (float)n; column->IsStretch = 0; settings->SaveFlags |= ImGuiTableFlags_Resizable; } + if (sscanf(line, "Weight=%f%n", &f, &r) == 1) { line = ImStrSkipBlank(line + r); column->WidthOrWeight = f; column->IsStretch = 1; settings->SaveFlags |= ImGuiTableFlags_Resizable; } + if (sscanf(line, "Visible=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->IsEnabled = (ImU8)n; settings->SaveFlags |= ImGuiTableFlags_Hideable; } + if (sscanf(line, "Order=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->DisplayOrder = (ImGuiTableColumnIdx)n; settings->SaveFlags |= ImGuiTableFlags_Reorderable; } + if (sscanf(line, "Sort=%d%c%n", &n, &c, &r) == 2) { line = ImStrSkipBlank(line + r); column->SortOrder = (ImGuiTableColumnIdx)n; column->SortDirection = (c == '^') ? ImGuiSortDirection_Descending : ImGuiSortDirection_Ascending; settings->SaveFlags |= ImGuiTableFlags_Sortable; } + } +} + +static void TableSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) +{ + ImGuiContext& g = *ctx; + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + { + if (settings->ID == 0) // Skip ditched settings + continue; + + // TableSaveSettings() may clear some of those flags when we establish that the data can be stripped + // (e.g. Order was unchanged) + const bool save_size = (settings->SaveFlags & ImGuiTableFlags_Resizable) != 0; + const bool save_visible = (settings->SaveFlags & ImGuiTableFlags_Hideable) != 0; + const bool save_order = (settings->SaveFlags & ImGuiTableFlags_Reorderable) != 0; + const bool save_sort = (settings->SaveFlags & ImGuiTableFlags_Sortable) != 0; + if (!save_size && !save_visible && !save_order && !save_sort) + continue; + + buf->reserve(buf->size() + 30 + settings->ColumnsCount * 50); // ballpark reserve + buf->appendf("[%s][0x%08X,%d]\n", handler->TypeName, settings->ID, settings->ColumnsCount); + if (settings->RefScale != 0.0f) + buf->appendf("RefScale=%g\n", settings->RefScale); + ImGuiTableColumnSettings* column = settings->GetColumnSettings(); + for (int column_n = 0; column_n < settings->ColumnsCount; column_n++, column++) + { + // "Column 0 UserID=0x42AD2D21 Width=100 Visible=1 Order=0 Sort=0v" + bool save_column = column->UserID != 0 || save_size || save_visible || save_order || (save_sort && column->SortOrder != -1); + if (!save_column) + continue; + buf->appendf("Column %-2d", column_n); + if (column->UserID != 0) buf->appendf(" UserID=%08X", column->UserID); + if (save_size && column->IsStretch) buf->appendf(" Weight=%.4f", column->WidthOrWeight); + if (save_size && !column->IsStretch) buf->appendf(" Width=%d", (int)column->WidthOrWeight); + if (save_visible) buf->appendf(" Visible=%d", column->IsEnabled); + if (save_order) buf->appendf(" Order=%d", column->DisplayOrder); + if (save_sort && column->SortOrder != -1) buf->appendf(" Sort=%d%c", column->SortOrder, (column->SortDirection == ImGuiSortDirection_Ascending) ? 'v' : '^'); + buf->append("\n"); + } + buf->append("\n"); + } +} + +void ImGui::TableSettingsInstallHandler(ImGuiContext* context) +{ + ImGuiContext& g = *context; + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "Table"; + ini_handler.TypeHash = ImHashStr("Table"); + ini_handler.ClearAllFn = TableSettingsHandler_ClearAll; + ini_handler.ReadOpenFn = TableSettingsHandler_ReadOpen; + ini_handler.ReadLineFn = TableSettingsHandler_ReadLine; + ini_handler.ApplyAllFn = TableSettingsHandler_ApplyAll; + ini_handler.WriteAllFn = TableSettingsHandler_WriteAll; + g.SettingsHandlers.push_back(ini_handler); +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Garbage Collection +//------------------------------------------------------------------------- +// - TableRemove() [Internal] +// - TableGcCompactTransientBuffers() [Internal] +// - TableGcCompactSettings() [Internal] +//------------------------------------------------------------------------- + +// Remove Table (currently only used by TestEngine) +void ImGui::TableRemove(ImGuiTable* table) +{ + //IMGUI_DEBUG_LOG("TableRemove() id=0x%08X\n", table->ID); + ImGuiContext& g = *GImGui; + int table_idx = g.Tables.GetIndex(table); + //memset(table->RawData.Data, 0, table->RawData.size_in_bytes()); + //memset(table, 0, sizeof(ImGuiTable)); + g.Tables.Remove(table->ID, table); + g.TablesLastTimeActive[table_idx] = -1.0f; +} + +// Free up/compact internal Table buffers for when it gets unused +void ImGui::TableGcCompactTransientBuffers(ImGuiTable* table) +{ + //IMGUI_DEBUG_LOG("TableGcCompactTransientBuffers() id=0x%08X\n", table->ID); + ImGuiContext& g = *GImGui; + IM_ASSERT(table->MemoryCompacted == false); + table->SortSpecs.Specs = NULL; + table->SortSpecsMulti.clear(); + table->IsSortSpecsDirty = true; // FIXME: shouldn't have to leak into user performing a sort + table->ColumnsNames.clear(); + table->MemoryCompacted = true; + for (int n = 0; n < table->ColumnsCount; n++) + table->Columns[n].NameOffset = -1; + g.TablesLastTimeActive[g.Tables.GetIndex(table)] = -1.0f; +} + +void ImGui::TableGcCompactTransientBuffers(ImGuiTableTempData* temp_data) +{ + temp_data->DrawSplitter.ClearFreeMemory(); + temp_data->LastTimeActive = -1.0f; +} + +// Compact and remove unused settings data (currently only used by TestEngine) +void ImGui::TableGcCompactSettings() +{ + ImGuiContext& g = *GImGui; + int required_memory = 0; + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + if (settings->ID != 0) + required_memory += (int)TableSettingsCalcChunkSize(settings->ColumnsCount); + if (required_memory == g.SettingsTables.Buf.Size) + return; + ImChunkStream new_chunk_stream; + new_chunk_stream.Buf.reserve(required_memory); + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + if (settings->ID != 0) + memcpy(new_chunk_stream.alloc_chunk(TableSettingsCalcChunkSize(settings->ColumnsCount)), settings, TableSettingsCalcChunkSize(settings->ColumnsCount)); + g.SettingsTables.swap(new_chunk_stream); +} + + +//------------------------------------------------------------------------- +// [SECTION] Tables: Debugging +//------------------------------------------------------------------------- +// - DebugNodeTable() [Internal] +//------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_METRICS_WINDOW + +static const char* DebugNodeTableGetSizingPolicyDesc(ImGuiTableFlags sizing_policy) +{ + sizing_policy &= ImGuiTableFlags_SizingMask_; + if (sizing_policy == ImGuiTableFlags_SizingFixedFit) { return "FixedFit"; } + if (sizing_policy == ImGuiTableFlags_SizingFixedSame) { return "FixedSame"; } + if (sizing_policy == ImGuiTableFlags_SizingStretchProp) { return "StretchProp"; } + if (sizing_policy == ImGuiTableFlags_SizingStretchSame) { return "StretchSame"; } + return "N/A"; +} + +void ImGui::DebugNodeTable(ImGuiTable* table) +{ + char buf[512]; + char* p = buf; + const char* buf_end = buf + IM_ARRAYSIZE(buf); + const bool is_active = (table->LastFrameActive >= ImGui::GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here. + ImFormatString(p, buf_end - p, "Table 0x%08X (%d columns, in '%s')%s", table->ID, table->ColumnsCount, table->OuterWindow->Name, is_active ? "" : " *Inactive*"); + if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + bool open = TreeNode(table, "%s", buf); + if (!is_active) { PopStyleColor(); } + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(table->OuterRect.Min, table->OuterRect.Max, IM_COL32(255, 255, 0, 255)); + if (IsItemVisible() && table->HoveredColumnBody != -1) + GetForegroundDrawList()->AddRect(GetItemRectMin(), GetItemRectMax(), IM_COL32(255, 255, 0, 255)); + if (!open) + return; + bool clear_settings = SmallButton("Clear settings"); + BulletText("OuterRect: Pos: (%.1f,%.1f) Size: (%.1f,%.1f) Sizing: '%s'", table->OuterRect.Min.x, table->OuterRect.Min.y, table->OuterRect.GetWidth(), table->OuterRect.GetHeight(), DebugNodeTableGetSizingPolicyDesc(table->Flags)); + BulletText("ColumnsGivenWidth: %.1f, ColumnsAutoFitWidth: %.1f, InnerWidth: %.1f%s", table->ColumnsGivenWidth, table->ColumnsAutoFitWidth, table->InnerWidth, table->InnerWidth == 0.0f ? " (auto)" : ""); + BulletText("CellPaddingX: %.1f, CellSpacingX: %.1f/%.1f, OuterPaddingX: %.1f", table->CellPaddingX, table->CellSpacingX1, table->CellSpacingX2, table->OuterPaddingX); + BulletText("HoveredColumnBody: %d, HoveredColumnBorder: %d", table->HoveredColumnBody, table->HoveredColumnBorder); + BulletText("ResizedColumn: %d, ReorderColumn: %d, HeldHeaderColumn: %d", table->ResizedColumn, table->ReorderColumn, table->HeldHeaderColumn); + //BulletText("BgDrawChannels: %d/%d", 0, table->BgDrawChannelUnfrozen); + float sum_weights = 0.0f; + for (int n = 0; n < table->ColumnsCount; n++) + if (table->Columns[n].Flags & ImGuiTableColumnFlags_WidthStretch) + sum_weights += table->Columns[n].StretchWeight; + for (int n = 0; n < table->ColumnsCount; n++) + { + ImGuiTableColumn* column = &table->Columns[n]; + const char* name = TableGetColumnName(table, n); + ImFormatString(buf, IM_ARRAYSIZE(buf), + "Column %d order %d '%s': offset %+.2f to %+.2f%s\n" + "Enabled: %d, VisibleX/Y: %d/%d, RequestOutput: %d, SkipItems: %d, DrawChannels: %d,%d\n" + "WidthGiven: %.1f, Request/Auto: %.1f/%.1f, StretchWeight: %.3f (%.1f%%)\n" + "MinX: %.1f, MaxX: %.1f (%+.1f), ClipRect: %.1f to %.1f (+%.1f)\n" + "ContentWidth: %.1f,%.1f, HeadersUsed/Ideal %.1f/%.1f\n" + "Sort: %d%s, UserID: 0x%08X, Flags: 0x%04X: %s%s%s..", + n, column->DisplayOrder, name, column->MinX - table->WorkRect.Min.x, column->MaxX - table->WorkRect.Min.x, (n < table->FreezeColumnsRequest) ? " (Frozen)" : "", + column->IsEnabled, column->IsVisibleX, column->IsVisibleY, column->IsRequestOutput, column->IsSkipItems, column->DrawChannelFrozen, column->DrawChannelUnfrozen, + column->WidthGiven, column->WidthRequest, column->WidthAuto, column->StretchWeight, column->StretchWeight > 0.0f ? (column->StretchWeight / sum_weights) * 100.0f : 0.0f, + column->MinX, column->MaxX, column->MaxX - column->MinX, column->ClipRect.Min.x, column->ClipRect.Max.x, column->ClipRect.Max.x - column->ClipRect.Min.x, + column->ContentMaxXFrozen - column->WorkMinX, column->ContentMaxXUnfrozen - column->WorkMinX, column->ContentMaxXHeadersUsed - column->WorkMinX, column->ContentMaxXHeadersIdeal - column->WorkMinX, + column->SortOrder, (column->SortDirection == ImGuiSortDirection_Ascending) ? " (Asc)" : (column->SortDirection == ImGuiSortDirection_Descending) ? " (Des)" : "", column->UserID, column->Flags, + (column->Flags & ImGuiTableColumnFlags_WidthStretch) ? "WidthStretch " : "", + (column->Flags & ImGuiTableColumnFlags_WidthFixed) ? "WidthFixed " : "", + (column->Flags & ImGuiTableColumnFlags_NoResize) ? "NoResize " : ""); + Bullet(); + Selectable(buf); + if (IsItemHovered()) + { + ImRect r(column->MinX, table->OuterRect.Min.y, column->MaxX, table->OuterRect.Max.y); + GetForegroundDrawList()->AddRect(r.Min, r.Max, IM_COL32(255, 255, 0, 255)); + } + } + if (ImGuiTableSettings* settings = TableGetBoundSettings(table)) + DebugNodeTableSettings(settings); + if (clear_settings) + table->IsResetAllRequest = true; + TreePop(); +} + +void ImGui::DebugNodeTableSettings(ImGuiTableSettings* settings) +{ + if (!TreeNode((void*)(intptr_t)settings->ID, "Settings 0x%08X (%d columns)", settings->ID, settings->ColumnsCount)) + return; + BulletText("SaveFlags: 0x%08X", settings->SaveFlags); + BulletText("ColumnsCount: %d (max %d)", settings->ColumnsCount, settings->ColumnsCountMax); + for (int n = 0; n < settings->ColumnsCount; n++) + { + ImGuiTableColumnSettings* column_settings = &settings->GetColumnSettings()[n]; + ImGuiSortDirection sort_dir = (column_settings->SortOrder != -1) ? (ImGuiSortDirection)column_settings->SortDirection : ImGuiSortDirection_None; + BulletText("Column %d Order %d SortOrder %d %s Vis %d %s %7.3f UserID 0x%08X", + n, column_settings->DisplayOrder, column_settings->SortOrder, + (sort_dir == ImGuiSortDirection_Ascending) ? "Asc" : (sort_dir == ImGuiSortDirection_Descending) ? "Des" : "---", + column_settings->IsEnabled, column_settings->IsStretch ? "Weight" : "Width ", column_settings->WidthOrWeight, column_settings->UserID); + } + TreePop(); +} + +#else // #ifndef IMGUI_DISABLE_METRICS_WINDOW + +void ImGui::DebugNodeTable(ImGuiTable*) {} +void ImGui::DebugNodeTableSettings(ImGuiTableSettings*) {} + +#endif + + +//------------------------------------------------------------------------- +// [SECTION] Columns, BeginColumns, EndColumns, etc. +// (This is a legacy API, prefer using BeginTable/EndTable!) +//------------------------------------------------------------------------- +// FIXME: sizing is lossy when columns width is very small (default width may turn negative etc.) +//------------------------------------------------------------------------- +// - SetWindowClipRectBeforeSetChannel() [Internal] +// - GetColumnIndex() +// - GetColumnsCount() +// - GetColumnOffset() +// - GetColumnWidth() +// - SetColumnOffset() +// - SetColumnWidth() +// - PushColumnClipRect() [Internal] +// - PushColumnsBackground() [Internal] +// - PopColumnsBackground() [Internal] +// - FindOrCreateColumns() [Internal] +// - GetColumnsID() [Internal] +// - BeginColumns() +// - NextColumn() +// - EndColumns() +// - Columns() +//------------------------------------------------------------------------- + +// [Internal] Small optimization to avoid calls to PopClipRect/SetCurrentChannel/PushClipRect in sequences, +// they would meddle many times with the underlying ImDrawCmd. +// Instead, we do a preemptive overwrite of clipping rectangle _without_ altering the command-buffer and let +// the subsequent single call to SetCurrentChannel() does it things once. +void ImGui::SetWindowClipRectBeforeSetChannel(ImGuiWindow* window, const ImRect& clip_rect) +{ + ImVec4 clip_rect_vec4 = clip_rect.ToVec4(); + window->ClipRect = clip_rect; + window->DrawList->_CmdHeader.ClipRect = clip_rect_vec4; + window->DrawList->_ClipRectStack.Data[window->DrawList->_ClipRectStack.Size - 1] = clip_rect_vec4; +} + +int ImGui::GetColumnIndex() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CurrentColumns ? window->DC.CurrentColumns->Current : 0; +} + +int ImGui::GetColumnsCount() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CurrentColumns ? window->DC.CurrentColumns->Count : 1; +} + +float ImGui::GetColumnOffsetFromNorm(const ImGuiOldColumns* columns, float offset_norm) +{ + return offset_norm * (columns->OffMaxX - columns->OffMinX); +} + +float ImGui::GetColumnNormFromOffset(const ImGuiOldColumns* columns, float offset) +{ + return offset / (columns->OffMaxX - columns->OffMinX); +} + +static const float COLUMNS_HIT_RECT_HALF_WIDTH = 4.0f; + +static float GetDraggedColumnOffset(ImGuiOldColumns* columns, int column_index) +{ + // Active (dragged) column always follow mouse. The reason we need this is that dragging a column to the right edge of an auto-resizing + // window creates a feedback loop because we store normalized positions. So while dragging we enforce absolute positioning. + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(column_index > 0); // We are not supposed to drag column 0. + IM_ASSERT(g.ActiveId == columns->ID + ImGuiID(column_index)); + + float x = g.IO.MousePos.x - g.ActiveIdClickOffset.x + COLUMNS_HIT_RECT_HALF_WIDTH - window->Pos.x; + x = ImMax(x, ImGui::GetColumnOffset(column_index - 1) + g.Style.ColumnsMinSpacing); + if ((columns->Flags & ImGuiOldColumnFlags_NoPreserveWidths)) + x = ImMin(x, ImGui::GetColumnOffset(column_index + 1) - g.Style.ColumnsMinSpacing); + + return x; +} + +float ImGui::GetColumnOffset(int column_index) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns == NULL) + return 0.0f; + + if (column_index < 0) + column_index = columns->Current; + IM_ASSERT(column_index < columns->Columns.Size); + + const float t = columns->Columns[column_index].OffsetNorm; + const float x_offset = ImLerp(columns->OffMinX, columns->OffMaxX, t); + return x_offset; +} + +static float GetColumnWidthEx(ImGuiOldColumns* columns, int column_index, bool before_resize = false) +{ + if (column_index < 0) + column_index = columns->Current; + + float offset_norm; + if (before_resize) + offset_norm = columns->Columns[column_index + 1].OffsetNormBeforeResize - columns->Columns[column_index].OffsetNormBeforeResize; + else + offset_norm = columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm; + return ImGui::GetColumnOffsetFromNorm(columns, offset_norm); +} + +float ImGui::GetColumnWidth(int column_index) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns == NULL) + return GetContentRegionAvail().x; + + if (column_index < 0) + column_index = columns->Current; + return GetColumnOffsetFromNorm(columns, columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm); +} + +void ImGui::SetColumnOffset(int column_index, float offset) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiOldColumns* columns = window->DC.CurrentColumns; + IM_ASSERT(columns != NULL); + + if (column_index < 0) + column_index = columns->Current; + IM_ASSERT(column_index < columns->Columns.Size); + + const bool preserve_width = !(columns->Flags & ImGuiOldColumnFlags_NoPreserveWidths) && (column_index < columns->Count - 1); + const float width = preserve_width ? GetColumnWidthEx(columns, column_index, columns->IsBeingResized) : 0.0f; + + if (!(columns->Flags & ImGuiOldColumnFlags_NoForceWithinWindow)) + offset = ImMin(offset, columns->OffMaxX - g.Style.ColumnsMinSpacing * (columns->Count - column_index)); + columns->Columns[column_index].OffsetNorm = GetColumnNormFromOffset(columns, offset - columns->OffMinX); + + if (preserve_width) + SetColumnOffset(column_index + 1, offset + ImMax(g.Style.ColumnsMinSpacing, width)); +} + +void ImGui::SetColumnWidth(int column_index, float width) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + IM_ASSERT(columns != NULL); + + if (column_index < 0) + column_index = columns->Current; + SetColumnOffset(column_index + 1, GetColumnOffset(column_index) + width); +} + +void ImGui::PushColumnClipRect(int column_index) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (column_index < 0) + column_index = columns->Current; + + ImGuiOldColumnData* column = &columns->Columns[column_index]; + PushClipRect(column->ClipRect.Min, column->ClipRect.Max, false); +} + +// Get into the columns background draw command (which is generally the same draw command as before we called BeginColumns) +void ImGui::PushColumnsBackground() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns->Count == 1) + return; + + // Optimization: avoid SetCurrentChannel() + PushClipRect() + columns->HostBackupClipRect = window->ClipRect; + SetWindowClipRectBeforeSetChannel(window, columns->HostInitialClipRect); + columns->Splitter.SetCurrentChannel(window->DrawList, 0); +} + +void ImGui::PopColumnsBackground() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns->Count == 1) + return; + + // Optimization: avoid PopClipRect() + SetCurrentChannel() + SetWindowClipRectBeforeSetChannel(window, columns->HostBackupClipRect); + columns->Splitter.SetCurrentChannel(window->DrawList, columns->Current + 1); +} + +ImGuiOldColumns* ImGui::FindOrCreateColumns(ImGuiWindow* window, ImGuiID id) +{ + // We have few columns per window so for now we don't need bother much with turning this into a faster lookup. + for (int n = 0; n < window->ColumnsStorage.Size; n++) + if (window->ColumnsStorage[n].ID == id) + return &window->ColumnsStorage[n]; + + window->ColumnsStorage.push_back(ImGuiOldColumns()); + ImGuiOldColumns* columns = &window->ColumnsStorage.back(); + columns->ID = id; + return columns; +} + +ImGuiID ImGui::GetColumnsID(const char* str_id, int columns_count) +{ + ImGuiWindow* window = GetCurrentWindow(); + + // Differentiate column ID with an arbitrary prefix for cases where users name their columns set the same as another widget. + // In addition, when an identifier isn't explicitly provided we include the number of columns in the hash to make it uniquer. + PushID(0x11223347 + (str_id ? 0 : columns_count)); + ImGuiID id = window->GetID(str_id ? str_id : "columns"); + PopID(); + + return id; +} + +void ImGui::BeginColumns(const char* str_id, int columns_count, ImGuiOldColumnFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + IM_ASSERT(columns_count >= 1); + IM_ASSERT(window->DC.CurrentColumns == NULL); // Nested columns are currently not supported + + // Acquire storage for the columns set + ImGuiID id = GetColumnsID(str_id, columns_count); + ImGuiOldColumns* columns = FindOrCreateColumns(window, id); + IM_ASSERT(columns->ID == id); + columns->Current = 0; + columns->Count = columns_count; + columns->Flags = flags; + window->DC.CurrentColumns = columns; + + columns->HostCursorPosY = window->DC.CursorPos.y; + columns->HostCursorMaxPosX = window->DC.CursorMaxPos.x; + columns->HostInitialClipRect = window->ClipRect; + columns->HostBackupParentWorkRect = window->ParentWorkRect; + window->ParentWorkRect = window->WorkRect; + + // Set state for first column + // We aim so that the right-most column will have the same clipping width as other after being clipped by parent ClipRect + const float column_padding = g.Style.ItemSpacing.x; + const float half_clip_extend_x = ImFloor(ImMax(window->WindowPadding.x * 0.5f, window->WindowBorderSize)); + const float max_1 = window->WorkRect.Max.x + column_padding - ImMax(column_padding - window->WindowPadding.x, 0.0f); + const float max_2 = window->WorkRect.Max.x + half_clip_extend_x; + columns->OffMinX = window->DC.Indent.x - column_padding + ImMax(column_padding - window->WindowPadding.x, 0.0f); + columns->OffMaxX = ImMax(ImMin(max_1, max_2) - window->Pos.x, columns->OffMinX + 1.0f); + columns->LineMinY = columns->LineMaxY = window->DC.CursorPos.y; + + // Clear data if columns count changed + if (columns->Columns.Size != 0 && columns->Columns.Size != columns_count + 1) + columns->Columns.resize(0); + + // Initialize default widths + columns->IsFirstFrame = (columns->Columns.Size == 0); + if (columns->Columns.Size == 0) + { + columns->Columns.reserve(columns_count + 1); + for (int n = 0; n < columns_count + 1; n++) + { + ImGuiOldColumnData column; + column.OffsetNorm = n / (float)columns_count; + columns->Columns.push_back(column); + } + } + + for (int n = 0; n < columns_count; n++) + { + // Compute clipping rectangle + ImGuiOldColumnData* column = &columns->Columns[n]; + float clip_x1 = IM_ROUND(window->Pos.x + GetColumnOffset(n)); + float clip_x2 = IM_ROUND(window->Pos.x + GetColumnOffset(n + 1) - 1.0f); + column->ClipRect = ImRect(clip_x1, -FLT_MAX, clip_x2, +FLT_MAX); + column->ClipRect.ClipWithFull(window->ClipRect); + } + + if (columns->Count > 1) + { + columns->Splitter.Split(window->DrawList, 1 + columns->Count); + columns->Splitter.SetCurrentChannel(window->DrawList, 1); + PushColumnClipRect(0); + } + + // We don't generally store Indent.x inside ColumnsOffset because it may be manipulated by the user. + float offset_0 = GetColumnOffset(columns->Current); + float offset_1 = GetColumnOffset(columns->Current + 1); + float width = offset_1 - offset_0; + PushItemWidth(width * 0.65f); + window->DC.ColumnsOffset.x = ImMax(column_padding - window->WindowPadding.x, 0.0f); + window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + window->WorkRect.Max.x = window->Pos.x + offset_1 - column_padding; +} + +void ImGui::NextColumn() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems || window->DC.CurrentColumns == NULL) + return; + + ImGuiContext& g = *GImGui; + ImGuiOldColumns* columns = window->DC.CurrentColumns; + + if (columns->Count == 1) + { + window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + IM_ASSERT(columns->Current == 0); + return; + } + + // Next column + if (++columns->Current == columns->Count) + columns->Current = 0; + + PopItemWidth(); + + // Optimization: avoid PopClipRect() + SetCurrentChannel() + PushClipRect() + // (which would needlessly attempt to update commands in the wrong channel, then pop or overwrite them), + ImGuiOldColumnData* column = &columns->Columns[columns->Current]; + SetWindowClipRectBeforeSetChannel(window, column->ClipRect); + columns->Splitter.SetCurrentChannel(window->DrawList, columns->Current + 1); + + const float column_padding = g.Style.ItemSpacing.x; + columns->LineMaxY = ImMax(columns->LineMaxY, window->DC.CursorPos.y); + if (columns->Current > 0) + { + // Columns 1+ ignore IndentX (by canceling it out) + // FIXME-COLUMNS: Unnecessary, could be locked? + window->DC.ColumnsOffset.x = GetColumnOffset(columns->Current) - window->DC.Indent.x + column_padding; + } + else + { + // New row/line: column 0 honor IndentX. + window->DC.ColumnsOffset.x = ImMax(column_padding - window->WindowPadding.x, 0.0f); + columns->LineMinY = columns->LineMaxY; + } + window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + window->DC.CursorPos.y = columns->LineMinY; + window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); + window->DC.CurrLineTextBaseOffset = 0.0f; + + // FIXME-COLUMNS: Share code with BeginColumns() - move code on columns setup. + float offset_0 = GetColumnOffset(columns->Current); + float offset_1 = GetColumnOffset(columns->Current + 1); + float width = offset_1 - offset_0; + PushItemWidth(width * 0.65f); + window->WorkRect.Max.x = window->Pos.x + offset_1 - column_padding; +} + +void ImGui::EndColumns() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + IM_ASSERT(columns != NULL); + + PopItemWidth(); + if (columns->Count > 1) + { + PopClipRect(); + columns->Splitter.Merge(window->DrawList); + } + + const ImGuiOldColumnFlags flags = columns->Flags; + columns->LineMaxY = ImMax(columns->LineMaxY, window->DC.CursorPos.y); + window->DC.CursorPos.y = columns->LineMaxY; + if (!(flags & ImGuiOldColumnFlags_GrowParentContentsSize)) + window->DC.CursorMaxPos.x = columns->HostCursorMaxPosX; // Restore cursor max pos, as columns don't grow parent + + // Draw columns borders and handle resize + // The IsBeingResized flag ensure we preserve pre-resize columns width so back-and-forth are not lossy + bool is_being_resized = false; + if (!(flags & ImGuiOldColumnFlags_NoBorder) && !window->SkipItems) + { + // We clip Y boundaries CPU side because very long triangles are mishandled by some GPU drivers. + const float y1 = ImMax(columns->HostCursorPosY, window->ClipRect.Min.y); + const float y2 = ImMin(window->DC.CursorPos.y, window->ClipRect.Max.y); + int dragging_column = -1; + for (int n = 1; n < columns->Count; n++) + { + ImGuiOldColumnData* column = &columns->Columns[n]; + float x = window->Pos.x + GetColumnOffset(n); + const ImGuiID column_id = columns->ID + ImGuiID(n); + const float column_hit_hw = COLUMNS_HIT_RECT_HALF_WIDTH; + const ImRect column_hit_rect(ImVec2(x - column_hit_hw, y1), ImVec2(x + column_hit_hw, y2)); + KeepAliveID(column_id); + if (IsClippedEx(column_hit_rect, column_id)) // FIXME: Can be removed or replaced with a lower-level test + continue; + + bool hovered = false, held = false; + if (!(flags & ImGuiOldColumnFlags_NoResize)) + { + ButtonBehavior(column_hit_rect, column_id, &hovered, &held); + if (hovered || held) + g.MouseCursor = ImGuiMouseCursor_ResizeEW; + if (held && !(column->Flags & ImGuiOldColumnFlags_NoResize)) + dragging_column = n; + } + + // Draw column + const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator); + const float xi = IM_FLOOR(x); + window->DrawList->AddLine(ImVec2(xi, y1 + 1.0f), ImVec2(xi, y2), col); + } + + // Apply dragging after drawing the column lines, so our rendered lines are in sync with how items were displayed during the frame. + if (dragging_column != -1) + { + if (!columns->IsBeingResized) + for (int n = 0; n < columns->Count + 1; n++) + columns->Columns[n].OffsetNormBeforeResize = columns->Columns[n].OffsetNorm; + columns->IsBeingResized = is_being_resized = true; + float x = GetDraggedColumnOffset(columns, dragging_column); + SetColumnOffset(dragging_column, x); + } + } + columns->IsBeingResized = is_being_resized; + + window->WorkRect = window->ParentWorkRect; + window->ParentWorkRect = columns->HostBackupParentWorkRect; + window->DC.CurrentColumns = NULL; + window->DC.ColumnsOffset.x = 0.0f; + window->DC.CursorPos.x = IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); +} + +void ImGui::Columns(int columns_count, const char* id, bool border) +{ + ImGuiWindow* window = GetCurrentWindow(); + IM_ASSERT(columns_count >= 1); + + ImGuiOldColumnFlags flags = (border ? 0 : ImGuiOldColumnFlags_NoBorder); + //flags |= ImGuiOldColumnFlags_NoPreserveWidths; // NB: Legacy behavior + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns != NULL && columns->Count == columns_count && columns->Flags == flags) + return; + + if (columns != NULL) + EndColumns(); + + if (columns_count != 1) + BeginColumns(id, columns_count, flags); +} + +//------------------------------------------------------------------------- + +#endif // #ifndef IMGUI_DISABLE + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Private/imgui_widgets.cpp`: + +```cpp +// dear imgui, v1.87 +// (widgets code) + +/* + +Index of this file: + +// [SECTION] Forward Declarations +// [SECTION] Widgets: Text, etc. +// [SECTION] Widgets: Main (Button, Image, Checkbox, RadioButton, ProgressBar, Bullet, etc.) +// [SECTION] Widgets: Low-level Layout helpers (Spacing, Dummy, NewLine, Separator, etc.) +// [SECTION] Widgets: ComboBox +// [SECTION] Data Type and Data Formatting Helpers +// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc. +// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc. +// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc. +// [SECTION] Widgets: InputText, InputTextMultiline +// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc. +// [SECTION] Widgets: TreeNode, CollapsingHeader, etc. +// [SECTION] Widgets: Selectable +// [SECTION] Widgets: ListBox +// [SECTION] Widgets: PlotLines, PlotHistogram +// [SECTION] Widgets: Value helpers +// [SECTION] Widgets: MenuItem, BeginMenu, EndMenu, etc. +// [SECTION] Widgets: BeginTabBar, EndTabBar, etc. +// [SECTION] Widgets: BeginTabItem, EndTabItem, etc. +// [SECTION] Widgets: Columns, BeginColumns, EndColumns, etc. + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include "imgui_internal.h" + +// System includes +#include // toupper +#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier +#include // intptr_t +#else +#include // intptr_t +#endif + +//------------------------------------------------------------------------- +// Warnings +//------------------------------------------------------------------------- + +// Visual Studio warnings +#ifdef _MSC_VER +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types +#endif +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. +#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') +#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#elif defined(__GNUC__) +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#endif + +//------------------------------------------------------------------------- +// Data +//------------------------------------------------------------------------- + +// Widgets +static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior. +static const float DRAG_MOUSE_THRESHOLD_FACTOR = 0.50f; // Multiplier for the default value of io.MouseDragThreshold to make DragFloat/DragInt react faster to mouse drags. + +// Those MIN/MAX values are not define because we need to point to them +static const signed char IM_S8_MIN = -128; +static const signed char IM_S8_MAX = 127; +static const unsigned char IM_U8_MIN = 0; +static const unsigned char IM_U8_MAX = 0xFF; +static const signed short IM_S16_MIN = -32768; +static const signed short IM_S16_MAX = 32767; +static const unsigned short IM_U16_MIN = 0; +static const unsigned short IM_U16_MAX = 0xFFFF; +static const ImS32 IM_S32_MIN = INT_MIN; // (-2147483647 - 1), (0x80000000); +static const ImS32 IM_S32_MAX = INT_MAX; // (2147483647), (0x7FFFFFFF) +static const ImU32 IM_U32_MIN = 0; +static const ImU32 IM_U32_MAX = UINT_MAX; // (0xFFFFFFFF) +#ifdef LLONG_MIN +static const ImS64 IM_S64_MIN = LLONG_MIN; // (-9223372036854775807ll - 1ll); +static const ImS64 IM_S64_MAX = LLONG_MAX; // (9223372036854775807ll); +#else +static const ImS64 IM_S64_MIN = -9223372036854775807LL - 1; +static const ImS64 IM_S64_MAX = 9223372036854775807LL; +#endif +static const ImU64 IM_U64_MIN = 0; +#ifdef ULLONG_MAX +static const ImU64 IM_U64_MAX = ULLONG_MAX; // (0xFFFFFFFFFFFFFFFFull); +#else +static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1); +#endif + +//------------------------------------------------------------------------- +// [SECTION] Forward Declarations +//------------------------------------------------------------------------- + +// For InputTextEx() +static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, ImGuiInputSource input_source); +static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end); +static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false); + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Text, etc. +//------------------------------------------------------------------------- +// - TextEx() [Internal] +// - TextUnformatted() +// - Text() +// - TextV() +// - TextColored() +// - TextColoredV() +// - TextDisabled() +// - TextDisabledV() +// - TextWrapped() +// - TextWrappedV() +// - LabelText() +// - LabelTextV() +// - BulletText() +// - BulletTextV() +//------------------------------------------------------------------------- + +void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + ImGuiContext& g = *GImGui; + + // Accept null ranges + if (text == text_end) + text = text_end = ""; + + // Calculate length + const char* text_begin = text; + if (text_end == NULL) + text_end = text + strlen(text); // FIXME-OPT + + const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + const float wrap_pos_x = window->DC.TextWrapPos; + const bool wrap_enabled = (wrap_pos_x >= 0.0f); + if (text_end - text > 2000 && !wrap_enabled) + { + // Long text! + // Perform manual coarse clipping to optimize for long multi-line text + // - From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled. + // - We also don't vertically center the text within the line full height, which is unlikely to matter because we are likely the biggest and only item on the line. + // - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop. + const char* line = text; + const float line_height = GetTextLineHeight(); + ImVec2 text_size(0, 0); + + // Lines to skip (can't skip when logging text) + ImVec2 pos = text_pos; + if (!g.LogEnabled) + { + int lines_skippable = (int)((window->ClipRect.Min.y - text_pos.y) / line_height); + if (lines_skippable > 0) + { + int lines_skipped = 0; + while (line < text_end && lines_skipped < lines_skippable) + { + const char* line_end = (const char*)memchr(line, '\n', text_end - line); + if (!line_end) + line_end = text_end; + if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0) + text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x); + line = line_end + 1; + lines_skipped++; + } + pos.y += lines_skipped * line_height; + } + } + + // Lines to render + if (line < text_end) + { + ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height)); + while (line < text_end) + { + if (IsClippedEx(line_rect, 0)) + break; + + const char* line_end = (const char*)memchr(line, '\n', text_end - line); + if (!line_end) + line_end = text_end; + text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x); + RenderText(pos, line, line_end, false); + line = line_end + 1; + line_rect.Min.y += line_height; + line_rect.Max.y += line_height; + pos.y += line_height; + } + + // Count remaining lines + int lines_skipped = 0; + while (line < text_end) + { + const char* line_end = (const char*)memchr(line, '\n', text_end - line); + if (!line_end) + line_end = text_end; + if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0) + text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x); + line = line_end + 1; + lines_skipped++; + } + pos.y += lines_skipped * line_height; + } + text_size.y = (pos - text_pos).y; + + ImRect bb(text_pos, text_pos + text_size); + ItemSize(text_size, 0.0f); + ItemAdd(bb, 0); + } + else + { + const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f; + const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width); + + ImRect bb(text_pos, text_pos + text_size); + ItemSize(text_size, 0.0f); + if (!ItemAdd(bb, 0)) + return; + + // Render (we don't hide text after ## in this end-user function) + RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width); + } +} + +void ImGui::TextUnformatted(const char* text, const char* text_end) +{ + TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText); +} + +void ImGui::Text(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextV(fmt, args); + va_end(args); +} + +void ImGui::TextV(const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + // FIXME-OPT: Handle the %s shortcut? + ImGuiContext& g = *GImGui; + const char* text_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); + TextEx(g.TempBuffer, text_end, ImGuiTextFlags_NoWidthForLargeClippedText); +} + +void ImGui::TextColored(const ImVec4& col, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextColoredV(col, fmt, args); + va_end(args); +} + +void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args) +{ + PushStyleColor(ImGuiCol_Text, col); + if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0) + TextEx(va_arg(args, const char*), NULL, ImGuiTextFlags_NoWidthForLargeClippedText); // Skip formatting + else + TextV(fmt, args); + PopStyleColor(); +} + +void ImGui::TextDisabled(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextDisabledV(fmt, args); + va_end(args); +} + +void ImGui::TextDisabledV(const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); + if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0) + TextEx(va_arg(args, const char*), NULL, ImGuiTextFlags_NoWidthForLargeClippedText); // Skip formatting + else + TextV(fmt, args); + PopStyleColor(); +} + +void ImGui::TextWrapped(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextWrappedV(fmt, args); + va_end(args); +} + +void ImGui::TextWrappedV(const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set + if (need_backup) + PushTextWrapPos(0.0f); + if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0) + TextEx(va_arg(args, const char*), NULL, ImGuiTextFlags_NoWidthForLargeClippedText); // Skip formatting + else + TextV(fmt, args); + if (need_backup) + PopTextWrapPos(); +} + +void ImGui::LabelText(const char* label, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + LabelTextV(label, fmt, args); + va_end(args); +} + +// Add a label+text combo aligned to other label+value widgets +void ImGui::LabelTextV(const char* label, const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float w = CalcItemWidth(); + + const char* value_text_begin = &g.TempBuffer[0]; + const char* value_text_end = value_text_begin + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); + const ImVec2 value_size = CalcTextSize(value_text_begin, value_text_end, false); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + const ImVec2 pos = window->DC.CursorPos; + const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2)); + const ImRect total_bb(pos, pos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), ImMax(value_size.y, label_size.y) + style.FramePadding.y * 2)); + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, 0)) + return; + + // Render + RenderTextClipped(value_bb.Min + style.FramePadding, value_bb.Max, value_text_begin, value_text_end, &value_size, ImVec2(0.0f, 0.0f)); + if (label_size.x > 0.0f) + RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label); +} + +void ImGui::BulletText(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + BulletTextV(fmt, args); + va_end(args); +} + +// Text with a little bullet aligned to the typical tree node. +void ImGui::BulletTextV(const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + const char* text_begin = g.TempBuffer; + const char* text_end = text_begin + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); + const ImVec2 label_size = CalcTextSize(text_begin, text_end, false); + const ImVec2 total_size = ImVec2(g.FontSize + (label_size.x > 0.0f ? (label_size.x + style.FramePadding.x * 2) : 0.0f), label_size.y); // Empty text doesn't add padding + ImVec2 pos = window->DC.CursorPos; + pos.y += window->DC.CurrLineTextBaseOffset; + ItemSize(total_size, 0.0f); + const ImRect bb(pos, pos + total_size); + if (!ItemAdd(bb, 0)) + return; + + // Render + ImU32 text_col = GetColorU32(ImGuiCol_Text); + RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), text_col); + RenderText(bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text_begin, text_end, false); +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Main +//------------------------------------------------------------------------- +// - ButtonBehavior() [Internal] +// - Button() +// - SmallButton() +// - InvisibleButton() +// - ArrowButton() +// - CloseButton() [Internal] +// - CollapseButton() [Internal] +// - GetWindowScrollbarID() [Internal] +// - GetWindowScrollbarRect() [Internal] +// - Scrollbar() [Internal] +// - ScrollbarEx() [Internal] +// - Image() +// - ImageButton() +// - Checkbox() +// - CheckboxFlagsT() [Internal] +// - CheckboxFlags() +// - RadioButton() +// - ProgressBar() +// - Bullet() +//------------------------------------------------------------------------- + +// The ButtonBehavior() function is key to many interactions and used by many/most widgets. +// Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_), +// this code is a little complex. +// By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior. +// See the series of events below and the corresponding state reported by dear imgui: +//------------------------------------------------------------------------------------------------------------------------------------------------ +// with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked() +// Frame N+0 (mouse is outside bb) - - - - - - +// Frame N+1 (mouse moves inside bb) - true - - - - +// Frame N+2 (mouse button is down) - true true true - true +// Frame N+3 (mouse button is down) - true true - - - +// Frame N+4 (mouse moves outside bb) - - true - - - +// Frame N+5 (mouse moves inside bb) - true true - - - +// Frame N+6 (mouse button is released) true true - - true - +// Frame N+7 (mouse button is released) - true - - - - +// Frame N+8 (mouse moves outside bb) - - - - - - +//------------------------------------------------------------------------------------------------------------------------------------------------ +// with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked() +// Frame N+2 (mouse button is down) true true true true - true +// Frame N+3 (mouse button is down) - true true - - - +// Frame N+6 (mouse button is released) - true - - true - +// Frame N+7 (mouse button is released) - true - - - - +//------------------------------------------------------------------------------------------------------------------------------------------------ +// with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked() +// Frame N+2 (mouse button is down) - true - - - true +// Frame N+3 (mouse button is down) - true - - - - +// Frame N+6 (mouse button is released) true true - - - - +// Frame N+7 (mouse button is released) - true - - - - +//------------------------------------------------------------------------------------------------------------------------------------------------ +// with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked() +// Frame N+0 (mouse button is down) - true - - - true +// Frame N+1 (mouse button is down) - true - - - - +// Frame N+2 (mouse button is released) - true - - - - +// Frame N+3 (mouse button is released) - true - - - - +// Frame N+4 (mouse button is down) true true true true - true +// Frame N+5 (mouse button is down) - true true - - - +// Frame N+6 (mouse button is released) - true - - true - +// Frame N+7 (mouse button is released) - true - - - - +//------------------------------------------------------------------------------------------------------------------------------------------------ +// Note that some combinations are supported, +// - PressedOnDragDropHold can generally be associated with any flag. +// - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported. +//------------------------------------------------------------------------------------------------------------------------------------------------ +// The behavior of the return-value changes when ImGuiButtonFlags_Repeat is set: +// Repeat+ Repeat+ Repeat+ Repeat+ +// PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick +//------------------------------------------------------------------------------------------------------------------------------------------------- +// Frame N+0 (mouse button is down) - true - true +// ... - - - - +// Frame N + RepeatDelay true true - true +// ... - - - - +// Frame N + RepeatDelay + RepeatRate*N true true - true +//------------------------------------------------------------------------------------------------------------------------------------------------- + +bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + // Default only reacts to left mouse button + if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0) + flags |= ImGuiButtonFlags_MouseButtonDefault_; + + // Default behavior requires click + release inside bounding box + if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0) + flags |= ImGuiButtonFlags_PressedOnDefault_; + + ImGuiWindow* backup_hovered_window = g.HoveredWindow; + const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindow == window; + if (flatten_hovered_children) + g.HoveredWindow = window; + +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (id != 0 && g.LastItemData.ID != id) + IMGUI_TEST_ENGINE_ITEM_ADD(bb, id); +#endif + + bool pressed = false; + bool hovered = ItemHoverable(bb, id); + + // Drag source doesn't report as hovered + if (hovered && g.DragDropActive && g.DragDropPayload.SourceId == id && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoDisableHover)) + hovered = false; + + // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button + if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers)) + if (IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + { + hovered = true; + SetHoveredID(id); + if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER) + { + pressed = true; + g.DragDropHoldJustPressedId = id; + FocusWindow(window); + } + } + + if (flatten_hovered_children) + g.HoveredWindow = backup_hovered_window; + + // AllowOverlap mode (rarely used) requires previous frame HoveredId to be null or to match. This allows using patterns where a later submitted widget overlaps a previous one. + if (hovered && (flags & ImGuiButtonFlags_AllowItemOverlap) && (g.HoveredIdPreviousFrame != id && g.HoveredIdPreviousFrame != 0)) + hovered = false; + + // Mouse handling + if (hovered) + { + if (!(flags & ImGuiButtonFlags_NoKeyModifiers) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt)) + { + // Poll buttons + int mouse_button_clicked = -1; + int mouse_button_released = -1; + if ((flags & ImGuiButtonFlags_MouseButtonLeft) && g.IO.MouseClicked[0]) { mouse_button_clicked = 0; } + else if ((flags & ImGuiButtonFlags_MouseButtonRight) && g.IO.MouseClicked[1]) { mouse_button_clicked = 1; } + else if ((flags & ImGuiButtonFlags_MouseButtonMiddle) && g.IO.MouseClicked[2]) { mouse_button_clicked = 2; } + if ((flags & ImGuiButtonFlags_MouseButtonLeft) && g.IO.MouseReleased[0]) { mouse_button_released = 0; } + else if ((flags & ImGuiButtonFlags_MouseButtonRight) && g.IO.MouseReleased[1]) { mouse_button_released = 1; } + else if ((flags & ImGuiButtonFlags_MouseButtonMiddle) && g.IO.MouseReleased[2]) { mouse_button_released = 2; } + + if (mouse_button_clicked != -1 && g.ActiveId != id) + { + if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere)) + { + SetActiveID(id, window); + g.ActiveIdMouseButton = mouse_button_clicked; + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + SetFocusID(id, window); + FocusWindow(window); + } + if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2)) + { + pressed = true; + if (flags & ImGuiButtonFlags_NoHoldingActiveId) + ClearActiveID(); + else + SetActiveID(id, window); // Hold on ID + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + SetFocusID(id, window); + g.ActiveIdMouseButton = mouse_button_clicked; + FocusWindow(window); + } + } + if ((flags & ImGuiButtonFlags_PressedOnRelease) && mouse_button_released != -1) + { + // Repeat mode trumps on release behavior + const bool has_repeated_at_least_once = (flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[mouse_button_released] >= g.IO.KeyRepeatDelay; + if (!has_repeated_at_least_once) + pressed = true; + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + SetFocusID(id, window); + ClearActiveID(); + } + + // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above). + // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings. + if (g.ActiveId == id && (flags & ImGuiButtonFlags_Repeat)) + if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(g.ActiveIdMouseButton, true)) + pressed = true; + } + + if (pressed) + g.NavDisableHighlight = true; + } + + // Gamepad/Keyboard navigation + // We report navigated item as hovered but we don't set g.HoveredId to not interfere with mouse. + if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover && (g.ActiveId == 0 || g.ActiveId == id || g.ActiveId == window->MoveId)) + if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus)) + hovered = true; + if (g.NavActivateDownId == id) + { + bool nav_activated_by_code = (g.NavActivateId == id); + bool nav_activated_by_inputs = IsNavInputTest(ImGuiNavInput_Activate, (flags & ImGuiButtonFlags_Repeat) ? ImGuiInputReadMode_Repeat : ImGuiInputReadMode_Pressed); + if (nav_activated_by_code || nav_activated_by_inputs) + { + // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button. + pressed = true; + SetActiveID(id, window); + g.ActiveIdSource = ImGuiInputSource_Nav; + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + SetFocusID(id, window); + } + } + + // Process while held + bool held = false; + if (g.ActiveId == id) + { + if (g.ActiveIdSource == ImGuiInputSource_Mouse) + { + if (g.ActiveIdIsJustActivated) + g.ActiveIdClickOffset = g.IO.MousePos - bb.Min; + + const int mouse_button = g.ActiveIdMouseButton; + IM_ASSERT(mouse_button >= 0 && mouse_button < ImGuiMouseButton_COUNT); + if (g.IO.MouseDown[mouse_button]) + { + held = true; + } + else + { + bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0; + bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0; + if ((release_in || release_anywhere) && !g.DragDropActive) + { + // Report as pressed when releasing the mouse (this is the most common path) + bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2; + bool is_repeating_already = (flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps + if (!is_double_click_release && !is_repeating_already) + pressed = true; + } + ClearActiveID(); + } + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + g.NavDisableHighlight = true; + } + else if (g.ActiveIdSource == ImGuiInputSource_Nav) + { + // When activated using Nav, we hold on the ActiveID until activation button is released + if (g.NavActivateDownId != id) + ClearActiveID(); + } + if (pressed) + g.ActiveIdHasBeenPressedBefore = true; + } + + if (out_hovered) *out_hovered = hovered; + if (out_held) *out_held = held; + + return pressed; +} + +bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + ImVec2 pos = window->DC.CursorPos; + if ((flags & ImGuiButtonFlags_AlignTextBaseLine) && style.FramePadding.y < window->DC.CurrLineTextBaseOffset) // Try to vertically align buttons that are smaller/have no padding so that text baseline matches (bit hacky, since it shouldn't be a flag) + pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y; + ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f); + + const ImRect bb(pos, pos + size); + ItemSize(size, style.FramePadding.y); + if (!ItemAdd(bb, id)) + return false; + + if (g.LastItemData.InFlags & ImGuiItemFlags_ButtonRepeat) + flags |= ImGuiButtonFlags_Repeat; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); + + // Render + const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + RenderNavHighlight(bb, id); + RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding); + + if (g.LogEnabled) + LogSetNextTextDecoration("[", "]"); + RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb); + + // Automatically close popups + //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) + // CloseCurrentPopup(); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; +} + +bool ImGui::Button(const char* label, const ImVec2& size_arg) +{ + return ButtonEx(label, size_arg, ImGuiButtonFlags_None); +} + +// Small buttons fits within text without additional vertical spacing. +bool ImGui::SmallButton(const char* label) +{ + ImGuiContext& g = *GImGui; + float backup_padding_y = g.Style.FramePadding.y; + g.Style.FramePadding.y = 0.0f; + bool pressed = ButtonEx(label, ImVec2(0, 0), ImGuiButtonFlags_AlignTextBaseLine); + g.Style.FramePadding.y = backup_padding_y; + return pressed; +} + +// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack. +// Then you can keep 'str_id' empty or the same for all your buttons (instead of creating a string based on a non-string id) +bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + // Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size. + IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f); + + const ImGuiID id = window->GetID(str_id); + ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + ItemSize(size); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags); + return pressed; +} + +bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImGuiID id = window->GetID(str_id); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + const float default_size = GetFrameHeight(); + ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f); + if (!ItemAdd(bb, id)) + return false; + + if (g.LastItemData.InFlags & ImGuiItemFlags_ButtonRepeat) + flags |= ImGuiButtonFlags_Repeat; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); + + // Render + const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + const ImU32 text_col = GetColorU32(ImGuiCol_Text); + RenderNavHighlight(bb, id); + RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding); + RenderArrow(window->DrawList, bb.Min + ImVec2(ImMax(0.0f, (size.x - g.FontSize) * 0.5f), ImMax(0.0f, (size.y - g.FontSize) * 0.5f)), text_col, dir); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags); + return pressed; +} + +bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir) +{ + float sz = GetFrameHeight(); + return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), ImGuiButtonFlags_None); +} + +// Button to close a window +bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Tweak 1: Shrink hit-testing area if button covers an abnormally large proportion of the visible region. That's in order to facilitate moving the window away. (#3825) + // This may better be applied as a general hit-rect reduction mechanism for all widgets to ensure the area to move window is always accessible? + const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f); + ImRect bb_interact = bb; + const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea(); + if (area_to_visible_ratio < 1.5f) + bb_interact.Expand(ImFloor(bb_interact.GetSize() * -0.25f)); + + // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window. + // (this isn't the regular behavior of buttons, but it doesn't affect the user much because navigation tends to keep items visible). + bool is_clipped = !ItemAdd(bb_interact, id); + + bool hovered, held; + bool pressed = ButtonBehavior(bb_interact, id, &hovered, &held); + if (is_clipped) + return pressed; + + // Render + // FIXME: Clarify this mess + ImU32 col = GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered); + ImVec2 center = bb.GetCenter(); + if (hovered) + window->DrawList->AddCircleFilled(center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f), col, 12); + + float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f; + ImU32 cross_col = GetColorU32(ImGuiCol_Text); + center -= ImVec2(0.5f, 0.5f); + window->DrawList->AddLine(center + ImVec2(+cross_extent, +cross_extent), center + ImVec2(-cross_extent, -cross_extent), cross_col, 1.0f); + window->DrawList->AddLine(center + ImVec2(+cross_extent, -cross_extent), center + ImVec2(-cross_extent, +cross_extent), cross_col, 1.0f); + + return pressed; +} + +bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f); + ItemAdd(bb, id); + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None); + + // Render + ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + ImU32 text_col = GetColorU32(ImGuiCol_Text); + ImVec2 center = bb.GetCenter(); + if (hovered || held) + window->DrawList->AddCircleFilled(center/*+ ImVec2(0.0f, -0.5f)*/, g.FontSize * 0.5f + 1.0f, bg_col, 12); + RenderArrow(window->DrawList, bb.Min + g.Style.FramePadding, text_col, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f); + + // Switch to moving the window after mouse is moved beyond the initial drag threshold + if (IsItemActive() && IsMouseDragging(0)) + StartMouseMovingWindow(window); + + return pressed; +} + +ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis) +{ + return window->GetIDNoKeepAlive(axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY"); +} + +// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set. +ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis) +{ + const ImRect outer_rect = window->Rect(); + const ImRect inner_rect = window->InnerRect; + const float border_size = window->WindowBorderSize; + const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar) + IM_ASSERT(scrollbar_size > 0.0f); + if (axis == ImGuiAxis_X) + return ImRect(inner_rect.Min.x, ImMax(outer_rect.Min.y, outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x, outer_rect.Max.y); + else + return ImRect(ImMax(outer_rect.Min.x, outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y, outer_rect.Max.x, inner_rect.Max.y); +} + +void ImGui::Scrollbar(ImGuiAxis axis) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + const ImGuiID id = GetWindowScrollbarID(window, axis); + KeepAliveID(id); + + // Calculate scrollbar bounding box + ImRect bb = GetWindowScrollbarRect(window, axis); + ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone; + if (axis == ImGuiAxis_X) + { + rounding_corners |= ImDrawFlags_RoundCornersBottomLeft; + if (!window->ScrollbarY) + rounding_corners |= ImDrawFlags_RoundCornersBottomRight; + } + else + { + if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar)) + rounding_corners |= ImDrawFlags_RoundCornersTopRight; + if (!window->ScrollbarX) + rounding_corners |= ImDrawFlags_RoundCornersBottomRight; + } + float size_avail = window->InnerRect.Max[axis] - window->InnerRect.Min[axis]; + float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f; + ImS64 scroll = (ImS64)window->Scroll[axis]; + ScrollbarEx(bb, id, axis, &scroll, (ImS64)size_avail, (ImS64)size_contents, rounding_corners); + window->Scroll[axis] = (float)scroll; +} + +// Vertical/Horizontal scrollbar +// The entire piece of code below is rather confusing because: +// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab) +// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar +// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal. +// Still, the code should probably be made simpler.. +bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 size_avail_v, ImS64 size_contents_v, ImDrawFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + const float bb_frame_width = bb_frame.GetWidth(); + const float bb_frame_height = bb_frame.GetHeight(); + if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f) + return false; + + // When we are too small, start hiding and disabling the grab (this reduce visual noise on very small window and facilitate using the window resize grab) + float alpha = 1.0f; + if ((axis == ImGuiAxis_Y) && bb_frame_height < g.FontSize + g.Style.FramePadding.y * 2.0f) + alpha = ImSaturate((bb_frame_height - g.FontSize) / (g.Style.FramePadding.y * 2.0f)); + if (alpha <= 0.0f) + return false; + + const ImGuiStyle& style = g.Style; + const bool allow_interaction = (alpha >= 1.0f); + + ImRect bb = bb_frame; + bb.Expand(ImVec2(-ImClamp(IM_FLOOR((bb_frame_width - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp(IM_FLOOR((bb_frame_height - 2.0f) * 0.5f), 0.0f, 3.0f))); + + // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar) + const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight(); + + // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount) + // But we maintain a minimum size in pixel to allow for the user to still aim inside. + IM_ASSERT(ImMax(size_contents_v, size_avail_v) > 0.0f); // Adding this assert to check if the ImMax(XXX,1.0f) is still needed. PLEASE CONTACT ME if this triggers. + const ImS64 win_size_v = ImMax(ImMax(size_contents_v, size_avail_v), (ImS64)1); + const float grab_h_pixels = ImClamp(scrollbar_size_v * ((float)size_avail_v / (float)win_size_v), style.GrabMinSize, scrollbar_size_v); + const float grab_h_norm = grab_h_pixels / scrollbar_size_v; + + // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar(). + bool held = false; + bool hovered = false; + ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus); + + const ImS64 scroll_max = ImMax((ImS64)1, size_contents_v - size_avail_v); + float scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max); + float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space + if (held && allow_interaction && grab_h_norm < 1.0f) + { + const float scrollbar_pos_v = bb.Min[axis]; + const float mouse_pos_v = g.IO.MousePos[axis]; + + // Click position in scrollbar normalized space (0.0f->1.0f) + const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v); + SetHoveredID(id); + + bool seek_absolute = false; + if (g.ActiveIdIsJustActivated) + { + // On initial click calculate the distance between mouse and the center of the grab + seek_absolute = (clicked_v_norm < grab_v_norm || clicked_v_norm > grab_v_norm + grab_h_norm); + if (seek_absolute) + g.ScrollbarClickDeltaToGrabCenter = 0.0f; + else + g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f; + } + + // Apply scroll (p_scroll_v will generally point on one member of window->Scroll) + // It is ok to modify Scroll here because we are being called in Begin() after the calculation of ContentSize and before setting up our starting position + const float scroll_v_norm = ImSaturate((clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm)); + *p_scroll_v = (ImS64)(scroll_v_norm * scroll_max); + + // Update values for rendering + scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max); + grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; + + // Update distance to grab now that we have seeked and saturated + if (seek_absolute) + g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f; + } + + // Render + const ImU32 bg_col = GetColorU32(ImGuiCol_ScrollbarBg); + const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha); + window->DrawList->AddRectFilled(bb_frame.Min, bb_frame.Max, bg_col, window->WindowRounding, flags); + ImRect grab_rect; + if (axis == ImGuiAxis_X) + grab_rect = ImRect(ImLerp(bb.Min.x, bb.Max.x, grab_v_norm), bb.Min.y, ImLerp(bb.Min.x, bb.Max.x, grab_v_norm) + grab_h_pixels, bb.Max.y); + else + grab_rect = ImRect(bb.Min.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm), bb.Max.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm) + grab_h_pixels); + window->DrawList->AddRectFilled(grab_rect.Min, grab_rect.Max, grab_col, style.ScrollbarRounding); + + return held; +} + +void ImGui::Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + if (border_col.w > 0.0f) + bb.Max += ImVec2(2, 2); + ItemSize(bb); + if (!ItemAdd(bb, 0)) + return; + + if (border_col.w > 0.0f) + { + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(border_col), 0.0f); + window->DrawList->AddImage(user_texture_id, bb.Min + ImVec2(1, 1), bb.Max - ImVec2(1, 1), uv0, uv1, GetColorU32(tint_col)); + } + else + { + window->DrawList->AddImage(user_texture_id, bb.Min, bb.Max, uv0, uv1, GetColorU32(tint_col)); + } +} + +// ImageButton() is flawed as 'id' is always derived from 'texture_id' (see #2464 #1390) +// We provide this internal helper to write your own variant while we figure out how to redesign the public ImageButton() API. +bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec2& padding, const ImVec4& bg_col, const ImVec4& tint_col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size + padding * 2); + ItemSize(bb); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + + // Render + const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + RenderNavHighlight(bb, id); + RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, g.Style.FrameRounding)); + if (bg_col.w > 0.0f) + window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col)); + window->DrawList->AddImage(texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col)); + + return pressed; +} + +// frame_padding < 0: uses FramePadding from style (default) +// frame_padding = 0: no framing +// frame_padding > 0: set framing size +bool ImGui::ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, int frame_padding, const ImVec4& bg_col, const ImVec4& tint_col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + // Default to using texture ID as ID. User can still push string/integer prefixes. + PushID((void*)(intptr_t)user_texture_id); + const ImGuiID id = window->GetID("#image"); + PopID(); + + const ImVec2 padding = (frame_padding >= 0) ? ImVec2((float)frame_padding, (float)frame_padding) : g.Style.FramePadding; + return ImageButtonEx(id, user_texture_id, size, uv0, uv1, padding, bg_col, tint_col); +} + +bool ImGui::Checkbox(const char* label, bool* v) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + const float square_sz = GetFrameHeight(); + const ImVec2 pos = window->DC.CursorPos; + const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 2.0f)); + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id)) + { + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); + return false; + } + + bool hovered, held; + bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); + if (pressed) + { + *v = !(*v); + MarkItemEdited(id); + } + + const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz)); + RenderNavHighlight(total_bb, id); + RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding); + ImU32 check_col = GetColorU32(ImGuiCol_CheckMark); + bool mixed_value = (g.LastItemData.InFlags & ImGuiItemFlags_MixedValue) != 0; + if (mixed_value) + { + // Undocumented tristate/mixed/indeterminate checkbox (#2644) + // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox) + ImVec2 pad(ImMax(1.0f, IM_FLOOR(square_sz / 3.6f)), ImMax(1.0f, IM_FLOOR(square_sz / 3.6f))); + window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding); + } + else if (*v) + { + const float pad = ImMax(1.0f, IM_FLOOR(square_sz / 6.0f)); + RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f); + } + + ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y); + if (g.LogEnabled) + LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]"); + if (label_size.x > 0.0f) + RenderText(label_pos, label); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); + return pressed; +} + +template +bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value) +{ + bool all_on = (*flags & flags_value) == flags_value; + bool any_on = (*flags & flags_value) != 0; + bool pressed; + if (!all_on && any_on) + { + ImGuiContext& g = *GImGui; + ImGuiItemFlags backup_item_flags = g.CurrentItemFlags; + g.CurrentItemFlags |= ImGuiItemFlags_MixedValue; + pressed = Checkbox(label, &all_on); + g.CurrentItemFlags = backup_item_flags; + } + else + { + pressed = Checkbox(label, &all_on); + + } + if (pressed) + { + if (all_on) + *flags |= flags_value; + else + *flags &= ~flags_value; + } + return pressed; +} + +bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value) +{ + return CheckboxFlagsT(label, flags, flags_value); +} + +bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value) +{ + return CheckboxFlagsT(label, flags, flags_value); +} + +bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value) +{ + return CheckboxFlagsT(label, flags, flags_value); +} + +bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value) +{ + return CheckboxFlagsT(label, flags, flags_value); +} + +bool ImGui::RadioButton(const char* label, bool active) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + const float square_sz = GetFrameHeight(); + const ImVec2 pos = window->DC.CursorPos; + const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz)); + const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 2.0f)); + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id)) + return false; + + ImVec2 center = check_bb.GetCenter(); + center.x = IM_ROUND(center.x); + center.y = IM_ROUND(center.y); + const float radius = (square_sz - 1.0f) * 0.5f; + + bool hovered, held; + bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); + if (pressed) + MarkItemEdited(id); + + RenderNavHighlight(total_bb, id); + window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), 16); + if (active) + { + const float pad = ImMax(1.0f, IM_FLOOR(square_sz / 6.0f)); + window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark), 16); + } + + if (style.FrameBorderSize > 0.0f) + { + window->DrawList->AddCircle(center + ImVec2(1, 1), radius, GetColorU32(ImGuiCol_BorderShadow), 16, style.FrameBorderSize); + window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), 16, style.FrameBorderSize); + } + + ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y); + if (g.LogEnabled) + LogRenderedText(&label_pos, active ? "(x)" : "( )"); + if (label_size.x > 0.0f) + RenderText(label_pos, label); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; +} + +// FIXME: This would work nicely if it was a public template, e.g. 'template RadioButton(const char* label, T* v, T v_button)', but I'm not sure how we would expose it.. +bool ImGui::RadioButton(const char* label, int* v, int v_button) +{ + const bool pressed = RadioButton(label, *v == v_button); + if (pressed) + *v = v_button; + return pressed; +} + +// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size +void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + ImVec2 pos = window->DC.CursorPos; + ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f); + ImRect bb(pos, pos + size); + ItemSize(size, style.FramePadding.y); + if (!ItemAdd(bb, 0)) + return; + + // Render + fraction = ImSaturate(fraction); + RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize)); + const ImVec2 fill_br = ImVec2(ImLerp(bb.Min.x, bb.Max.x, fraction), bb.Max.y); + RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), 0.0f, fraction, style.FrameRounding); + + // Default displaying the fraction as percentage string, but user can override it + char overlay_buf[32]; + if (!overlay) + { + ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction * 100 + 0.01f); + overlay = overlay_buf; + } + + ImVec2 overlay_size = CalcTextSize(overlay, NULL); + if (overlay_size.x > 0.0f) + RenderTextClipped(ImVec2(ImClamp(fill_br.x + style.ItemSpacing.x, bb.Min.x, bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), bb.Max, overlay, NULL, &overlay_size, ImVec2(0.0f, 0.5f), &bb); +} + +void ImGui::Bullet() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + g.Style.FramePadding.y * 2), g.FontSize); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height)); + ItemSize(bb); + if (!ItemAdd(bb, 0)) + { + SameLine(0, style.FramePadding.x * 2); + return; + } + + // Render and stay on same line + ImU32 text_col = GetColorU32(ImGuiCol_Text); + RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), text_col); + SameLine(0, style.FramePadding.x * 2.0f); +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Low-level Layout helpers +//------------------------------------------------------------------------- +// - Spacing() +// - Dummy() +// - NewLine() +// - AlignTextToFramePadding() +// - SeparatorEx() [Internal] +// - Separator() +// - SplitterBehavior() [Internal] +// - ShrinkWidths() [Internal] +//------------------------------------------------------------------------- + +void ImGui::Spacing() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + ItemSize(ImVec2(0, 0)); +} + +void ImGui::Dummy(const ImVec2& size) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + ItemSize(size); + ItemAdd(bb, 0); +} + +void ImGui::NewLine() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiLayoutType backup_layout_type = window->DC.LayoutType; + window->DC.LayoutType = ImGuiLayoutType_Vertical; + if (window->DC.CurrLineSize.y > 0.0f) // In the event that we are on a line with items that is smaller that FontSize high, we will preserve its height. + ItemSize(ImVec2(0, 0)); + else + ItemSize(ImVec2(0.0f, g.FontSize)); + window->DC.LayoutType = backup_layout_type; +} + +void ImGui::AlignTextToFramePadding() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + window->DC.CurrLineSize.y = ImMax(window->DC.CurrLineSize.y, g.FontSize + g.Style.FramePadding.y * 2); + window->DC.CurrLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, g.Style.FramePadding.y); +} + +// Horizontal/vertical separating line +void ImGui::SeparatorEx(ImGuiSeparatorFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected + + float thickness_draw = 1.0f; + float thickness_layout = 0.0f; + if (flags & ImGuiSeparatorFlags_Vertical) + { + // Vertical separator, for menu bars (use current line height). Not exposed because it is misleading and it doesn't have an effect on regular layout. + float y1 = window->DC.CursorPos.y; + float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y; + const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness_draw, y2)); + ItemSize(ImVec2(thickness_layout, 0.0f)); + if (!ItemAdd(bb, 0)) + return; + + // Draw + window->DrawList->AddLine(ImVec2(bb.Min.x, bb.Min.y), ImVec2(bb.Min.x, bb.Max.y), GetColorU32(ImGuiCol_Separator)); + if (g.LogEnabled) + LogText(" |"); + } + else if (flags & ImGuiSeparatorFlags_Horizontal) + { + // Horizontal Separator + float x1 = window->Pos.x; + float x2 = window->Pos.x + window->Size.x; + + // FIXME-WORKRECT: old hack (#205) until we decide of consistent behavior with WorkRect/Indent and Separator + if (g.GroupStack.Size > 0 && g.GroupStack.back().WindowID == window->ID) + x1 += window->DC.Indent.x; + + // FIXME-WORKRECT: In theory we should simply be using WorkRect.Min.x/Max.x everywhere but it isn't aesthetically what we want, + // need to introduce a variant of WorkRect for that purpose. (#4787) + if (ImGuiTable* table = g.CurrentTable) + { + x1 = table->Columns[table->CurrentColumn].MinX; + x2 = table->Columns[table->CurrentColumn].MaxX; + } + + ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL; + if (columns) + PushColumnsBackground(); + + // We don't provide our width to the layout so that it doesn't get feed back into AutoFit + // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell) + const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness_draw)); + ItemSize(ImVec2(0.0f, thickness_layout)); + const bool item_visible = ItemAdd(bb, 0); + if (item_visible) + { + // Draw + window->DrawList->AddLine(bb.Min, ImVec2(bb.Max.x, bb.Min.y), GetColorU32(ImGuiCol_Separator)); + if (g.LogEnabled) + LogRenderedText(&bb.Min, "--------------------------------\n"); + + } + if (columns) + { + PopColumnsBackground(); + columns->LineMinY = window->DC.CursorPos.y; + } + } +} + +void ImGui::Separator() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + // Those flags should eventually be overridable by the user + ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal; + flags |= ImGuiSeparatorFlags_SpanAllColumns; // NB: this only applies to legacy Columns() api as they relied on Separator() a lot. + SeparatorEx(flags); +} + +// Using 'hover_visibility_delay' allows us to hide the highlight and mouse cursor for a short time, which can be convenient to reduce visual noise. +bool ImGui::SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend, float hover_visibility_delay) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + const ImGuiItemFlags item_flags_backup = g.CurrentItemFlags; + g.CurrentItemFlags |= ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus; + bool item_add = ItemAdd(bb, id); + g.CurrentItemFlags = item_flags_backup; + if (!item_add) + return false; + + bool hovered, held; + ImRect bb_interact = bb; + bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f)); + ButtonBehavior(bb_interact, id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap); + if (hovered) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb + if (g.ActiveId != id) + SetItemAllowOverlap(); + + if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay)) + SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW); + + ImRect bb_render = bb; + if (held) + { + ImVec2 mouse_delta_2d = g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min; + float mouse_delta = (axis == ImGuiAxis_Y) ? mouse_delta_2d.y : mouse_delta_2d.x; + + // Minimum pane size + float size_1_maximum_delta = ImMax(0.0f, *size1 - min_size1); + float size_2_maximum_delta = ImMax(0.0f, *size2 - min_size2); + if (mouse_delta < -size_1_maximum_delta) + mouse_delta = -size_1_maximum_delta; + if (mouse_delta > size_2_maximum_delta) + mouse_delta = size_2_maximum_delta; + + // Apply resize + if (mouse_delta != 0.0f) + { + if (mouse_delta < 0.0f) + IM_ASSERT(*size1 + mouse_delta >= min_size1); + if (mouse_delta > 0.0f) + IM_ASSERT(*size2 - mouse_delta >= min_size2); + *size1 += mouse_delta; + *size2 -= mouse_delta; + bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta)); + MarkItemEdited(id); + } + } + + // Render + const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator); + window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, col, 0.0f); + + return held; +} + +static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs) +{ + const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs; + const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs; + if (int d = (int)(b->Width - a->Width)) + return d; + return (b->Index - a->Index); +} + +// Shrink excess width from a set of item, by removing width from the larger items first. +// Set items Width to -1.0f to disable shrinking this item. +void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess) +{ + if (count == 1) + { + if (items[0].Width >= 0.0f) + items[0].Width = ImMax(items[0].Width - width_excess, 1.0f); + return; + } + ImQsort(items, (size_t)count, sizeof(ImGuiShrinkWidthItem), ShrinkWidthItemComparer); + int count_same_width = 1; + while (width_excess > 0.0f && count_same_width < count) + { + while (count_same_width < count && items[0].Width <= items[count_same_width].Width) + count_same_width++; + float max_width_to_remove_per_item = (count_same_width < count && items[count_same_width].Width >= 0.0f) ? (items[0].Width - items[count_same_width].Width) : (items[0].Width - 1.0f); + if (max_width_to_remove_per_item <= 0.0f) + break; + float width_to_remove_per_item = ImMin(width_excess / count_same_width, max_width_to_remove_per_item); + for (int item_n = 0; item_n < count_same_width; item_n++) + items[item_n].Width -= width_to_remove_per_item; + width_excess -= width_to_remove_per_item * count_same_width; + } + + // Round width and redistribute remainder left-to-right (could make it an option of the function?) + // Ensure that e.g. the right-most tab of a shrunk tab-bar always reaches exactly at the same distance from the right-most edge of the tab bar separator. + width_excess = 0.0f; + for (int n = 0; n < count; n++) + { + float width_rounded = ImFloor(items[n].Width); + width_excess += items[n].Width - width_rounded; + items[n].Width = width_rounded; + } + if (width_excess > 0.0f) + for (int n = 0; n < count; n++) + if (items[n].Index < (int)(width_excess + 0.01f)) + items[n].Width += 1.0f; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: ComboBox +//------------------------------------------------------------------------- +// - CalcMaxPopupHeightFromItemCount() [Internal] +// - BeginCombo() +// - BeginComboPopup() [Internal] +// - EndCombo() +// - BeginComboPreview() [Internal] +// - EndComboPreview() [Internal] +// - Combo() +//------------------------------------------------------------------------- + +static float CalcMaxPopupHeightFromItemCount(int items_count) +{ + ImGuiContext& g = *GImGui; + if (items_count <= 0) + return FLT_MAX; + return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2); +} + +bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.Flags; + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + if (window->SkipItems) + return false; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together + + const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight(); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : CalcItemWidth(); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); + const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id, &bb)) + return false; + + // Open on click + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, id); + bool popup_open = IsPopupOpen(popup_id, ImGuiPopupFlags_None); + if (pressed && !popup_open) + { + OpenPopupEx(popup_id, ImGuiPopupFlags_None); + popup_open = true; + } + + // Render shape + const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + const float value_x2 = ImMax(bb.Min.x, bb.Max.x - arrow_size); + RenderNavHighlight(bb, id); + if (!(flags & ImGuiComboFlags_NoPreview)) + window->DrawList->AddRectFilled(bb.Min, ImVec2(value_x2, bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft); + if (!(flags & ImGuiComboFlags_NoArrowButton)) + { + ImU32 bg_col = GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + ImU32 text_col = GetColorU32(ImGuiCol_Text); + window->DrawList->AddRectFilled(ImVec2(value_x2, bb.Min.y), bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight); + if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x) + RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f); + } + RenderFrameBorder(bb.Min, bb.Max, style.FrameRounding); + + // Custom preview + if (flags & ImGuiComboFlags_CustomPreview) + { + g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y); + IM_ASSERT(preview_value == NULL || preview_value[0] == 0); + preview_value = NULL; + } + + // Render preview and label + if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview)) + { + if (g.LogEnabled) + LogSetNextTextDecoration("{", "}"); + RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL); + } + if (label_size.x > 0) + RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label); + + if (!popup_open) + return false; + + g.NextWindowData.Flags = backup_next_window_data_flags; + return BeginComboPopup(popup_id, bb, flags); +} + +bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags) +{ + ImGuiContext& g = *GImGui; + if (!IsPopupOpen(popup_id, ImGuiPopupFlags_None)) + { + g.NextWindowData.ClearFlags(); + return false; + } + + // Set popup size + float w = bb.GetWidth(); + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint) + { + g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w); + } + else + { + if ((flags & ImGuiComboFlags_HeightMask_) == 0) + flags |= ImGuiComboFlags_HeightRegular; + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one + int popup_max_height_in_items = -1; + if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8; + else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4; + else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20; + SetNextWindowSizeConstraints(ImVec2(w, 0.0f), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items))); + } + + // This is essentially a specialized version of BeginPopupEx() + char name[16]; + ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginPopupStack.Size); // Recycle windows based on depth + + // Set position given a custom constraint (peak into expected window size so we can position it) + // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function? + // FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()? + if (ImGuiWindow* popup_window = FindWindowByName(name)) + if (popup_window->WasActive) + { + // Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us. + ImVec2 size_expected = CalcWindowNextAutoFitSize(popup_window); + popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)" + ImRect r_outer = GetPopupAllowedExtentRect(popup_window); + ImVec2 pos = FindBestWindowPosForPopupEx(bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, bb, ImGuiPopupPositionPolicy_ComboBox); + SetNextWindowPos(pos); + } + + // We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx() + ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove; + PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(g.Style.FramePadding.x, g.Style.WindowPadding.y)); // Horizontally align ourselves with the framed text + bool ret = Begin(name, NULL, window_flags); + PopStyleVar(); + if (!ret) + { + EndPopup(); + IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above + return false; + } + return true; +} + +void ImGui::EndCombo() +{ + EndPopup(); +} + +// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements +// (Experimental, see GitHub issues: #1658, #4168) +bool ImGui::BeginComboPreview() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiComboPreviewData* preview_data = &g.ComboPreviewData; + + if (window->SkipItems || !window->ClipRect.Overlaps(g.LastItemData.Rect)) // FIXME: Because we don't have a ImGuiItemStatusFlags_Visible flag to test last ItemAdd() result + return false; + IM_ASSERT(g.LastItemData.Rect.Min.x == preview_data->PreviewRect.Min.x && g.LastItemData.Rect.Min.y == preview_data->PreviewRect.Min.y); // Didn't call after BeginCombo/EndCombo block or forgot to pass ImGuiComboFlags_CustomPreview flag? + if (!window->ClipRect.Contains(preview_data->PreviewRect)) // Narrower test (optional) + return false; + + // FIXME: This could be contained in a PushWorkRect() api + preview_data->BackupCursorPos = window->DC.CursorPos; + preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos; + preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine; + preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset; + preview_data->BackupLayout = window->DC.LayoutType; + window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding; + window->DC.CursorMaxPos = window->DC.CursorPos; + window->DC.LayoutType = ImGuiLayoutType_Horizontal; + PushClipRect(preview_data->PreviewRect.Min, preview_data->PreviewRect.Max, true); + + return true; +} + +void ImGui::EndComboPreview() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiComboPreviewData* preview_data = &g.ComboPreviewData; + + // FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future + ImDrawList* draw_list = window->DrawList; + if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y) + if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command + { + draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect; + draw_list->_TryMergeDrawCmds(); + } + PopClipRect(); + window->DC.CursorPos = preview_data->BackupCursorPos; + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, preview_data->BackupCursorMaxPos); + window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine; + window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset; + window->DC.LayoutType = preview_data->BackupLayout; + preview_data->PreviewRect = ImRect(); +} + +// Getter for the old Combo() API: const char*[] +static bool Items_ArrayGetter(void* data, int idx, const char** out_text) +{ + const char* const* items = (const char* const*)data; + if (out_text) + *out_text = items[idx]; + return true; +} + +// Getter for the old Combo() API: "item1\0item2\0item3\0" +static bool Items_SingleStringGetter(void* data, int idx, const char** out_text) +{ + // FIXME-OPT: we could pre-compute the indices to fasten this. But only 1 active combo means the waste is limited. + const char* items_separated_by_zeros = (const char*)data; + int items_count = 0; + const char* p = items_separated_by_zeros; + while (*p) + { + if (idx == items_count) + break; + p += strlen(p) + 1; + items_count++; + } + if (!*p) + return false; + if (out_text) + *out_text = p; + return true; +} + +// Old API, prefer using BeginCombo() nowadays if you can. +bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int popup_max_height_in_items) +{ + ImGuiContext& g = *GImGui; + + // Call the getter to obtain the preview string which is a parameter to BeginCombo() + const char* preview_value = NULL; + if (*current_item >= 0 && *current_item < items_count) + items_getter(data, *current_item, &preview_value); + + // The old Combo() API exposed "popup_max_height_in_items". The new more general BeginCombo() API doesn't have/need it, but we emulate it here. + if (popup_max_height_in_items != -1 && !(g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint)) + SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items))); + + if (!BeginCombo(label, preview_value, ImGuiComboFlags_None)) + return false; + + // Display items + // FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed) + bool value_changed = false; + for (int i = 0; i < items_count; i++) + { + PushID(i); + const bool item_selected = (i == *current_item); + const char* item_text; + if (!items_getter(data, i, &item_text)) + item_text = "*Unknown item*"; + if (Selectable(item_text, item_selected)) + { + value_changed = true; + *current_item = i; + } + if (item_selected) + SetItemDefaultFocus(); + PopID(); + } + + EndCombo(); + + if (value_changed) + MarkItemEdited(g.LastItemData.ID); + + return value_changed; +} + +// Combo box helper allowing to pass an array of strings. +bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items) +{ + const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items); + return value_changed; +} + +// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0" +bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items) +{ + int items_count = 0; + const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open + while (*p) + { + p += strlen(p) + 1; + items_count++; + } + bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items); + return value_changed; +} + +//------------------------------------------------------------------------- +// [SECTION] Data Type and Data Formatting Helpers [Internal] +//------------------------------------------------------------------------- +// - PatchFormatStringFloatToInt() +// - DataTypeGetInfo() +// - DataTypeFormatString() +// - DataTypeApplyOp() +// - DataTypeApplyOpFromText() +// - DataTypeClamp() +// - GetMinimumStepAtDecimalPrecision +// - RoundScalarWithFormat<>() +//------------------------------------------------------------------------- + +static const ImGuiDataTypeInfo GDataTypeInfo[] = +{ + { sizeof(char), "S8", "%d", "%d" }, // ImGuiDataType_S8 + { sizeof(unsigned char), "U8", "%u", "%u" }, + { sizeof(short), "S16", "%d", "%d" }, // ImGuiDataType_S16 + { sizeof(unsigned short), "U16", "%u", "%u" }, + { sizeof(int), "S32", "%d", "%d" }, // ImGuiDataType_S32 + { sizeof(unsigned int), "U32", "%u", "%u" }, +#ifdef _MSC_VER + { sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64 + { sizeof(ImU64), "U64", "%I64u","%I64u" }, +#else + { sizeof(ImS64), "S64", "%lld", "%lld" }, // ImGuiDataType_S64 + { sizeof(ImU64), "U64", "%llu", "%llu" }, +#endif + { sizeof(float), "float", "%.3f","%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg) + { sizeof(double), "double","%f", "%lf" }, // ImGuiDataType_Double +}; +IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo) == ImGuiDataType_COUNT); + +// FIXME-LEGACY: Prior to 1.61 our DragInt() function internally used floats and because of this the compile-time default value for format was "%.0f". +// Even though we changed the compile-time default, we expect users to have carried %f around, which would break the display of DragInt() calls. +// To honor backward compatibility we are rewriting the format string, unless IMGUI_DISABLE_OBSOLETE_FUNCTIONS is enabled. What could possibly go wrong?! +static const char* PatchFormatStringFloatToInt(const char* fmt) +{ + if (fmt[0] == '%' && fmt[1] == '.' && fmt[2] == '0' && fmt[3] == 'f' && fmt[4] == 0) // Fast legacy path for "%.0f" which is expected to be the most common case. + return "%d"; + const char* fmt_start = ImParseFormatFindStart(fmt); // Find % (if any, and ignore %%) + const char* fmt_end = ImParseFormatFindEnd(fmt_start); // Find end of format specifier, which itself is an exercise of confidence/recklessness (because snprintf is dependent on libc or user). + if (fmt_end > fmt_start && fmt_end[-1] == 'f') + { +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + if (fmt_start == fmt && fmt_end[0] == 0) + return "%d"; + ImGuiContext& g = *GImGui; + ImFormatString(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), "%.*s%%d%s", (int)(fmt_start - fmt), fmt, fmt_end); // Honor leading and trailing decorations, but lose alignment/precision. + return g.TempBuffer; +#else + IM_ASSERT(0 && "DragInt(): Invalid format string!"); // Old versions used a default parameter of "%.0f", please replace with e.g. "%d" +#endif + } + return fmt; +} + +const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type) +{ + IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT); + return &GDataTypeInfo[data_type]; +} + +int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format) +{ + // Signedness doesn't matter when pushing integer arguments + if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32) + return ImFormatString(buf, buf_size, format, *(const ImU32*)p_data); + if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64) + return ImFormatString(buf, buf_size, format, *(const ImU64*)p_data); + if (data_type == ImGuiDataType_Float) + return ImFormatString(buf, buf_size, format, *(const float*)p_data); + if (data_type == ImGuiDataType_Double) + return ImFormatString(buf, buf_size, format, *(const double*)p_data); + if (data_type == ImGuiDataType_S8) + return ImFormatString(buf, buf_size, format, *(const ImS8*)p_data); + if (data_type == ImGuiDataType_U8) + return ImFormatString(buf, buf_size, format, *(const ImU8*)p_data); + if (data_type == ImGuiDataType_S16) + return ImFormatString(buf, buf_size, format, *(const ImS16*)p_data); + if (data_type == ImGuiDataType_U16) + return ImFormatString(buf, buf_size, format, *(const ImU16*)p_data); + IM_ASSERT(0); + return 0; +} + +void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2) +{ + IM_ASSERT(op == '+' || op == '-'); + switch (data_type) + { + case ImGuiDataType_S8: + if (op == '+') { *(ImS8*)output = ImAddClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); } + if (op == '-') { *(ImS8*)output = ImSubClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); } + return; + case ImGuiDataType_U8: + if (op == '+') { *(ImU8*)output = ImAddClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); } + if (op == '-') { *(ImU8*)output = ImSubClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); } + return; + case ImGuiDataType_S16: + if (op == '+') { *(ImS16*)output = ImAddClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); } + if (op == '-') { *(ImS16*)output = ImSubClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); } + return; + case ImGuiDataType_U16: + if (op == '+') { *(ImU16*)output = ImAddClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); } + if (op == '-') { *(ImU16*)output = ImSubClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); } + return; + case ImGuiDataType_S32: + if (op == '+') { *(ImS32*)output = ImAddClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); } + if (op == '-') { *(ImS32*)output = ImSubClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); } + return; + case ImGuiDataType_U32: + if (op == '+') { *(ImU32*)output = ImAddClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); } + if (op == '-') { *(ImU32*)output = ImSubClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); } + return; + case ImGuiDataType_S64: + if (op == '+') { *(ImS64*)output = ImAddClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); } + if (op == '-') { *(ImS64*)output = ImSubClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); } + return; + case ImGuiDataType_U64: + if (op == '+') { *(ImU64*)output = ImAddClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); } + if (op == '-') { *(ImU64*)output = ImSubClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); } + return; + case ImGuiDataType_Float: + if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; } + if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; } + return; + case ImGuiDataType_Double: + if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; } + if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; } + return; + case ImGuiDataType_COUNT: break; + } + IM_ASSERT(0); +} + +// User can input math operators (e.g. +100) to edit a numerical values. +// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess.. +bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format) +{ + while (ImCharIsBlankA(*buf)) + buf++; + if (!buf[0]) + return false; + + // Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all. + const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type); + ImGuiDataTypeTempStorage data_backup; + memcpy(&data_backup, p_data, type_info->Size); + + if (format == NULL) + format = type_info->ScanFmt; + + if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32 || data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64 || data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) + { + // For float/double we have to ignore format with precision (e.g. "%.2f") because sscanf doesn't take them in, so force them into %f and %lf + if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) + format = type_info->ScanFmt; + if (sscanf(buf, format, p_data) < 1) + return false; + } + else + { + // Small types need a 32-bit buffer to receive the result from scanf() + int v32; + if (sscanf(buf, format, &v32) < 1) + return false; + if (data_type == ImGuiDataType_S8) + *(ImS8*)p_data = (ImS8)ImClamp(v32, (int)IM_S8_MIN, (int)IM_S8_MAX); + else if (data_type == ImGuiDataType_U8) + *(ImU8*)p_data = (ImU8)ImClamp(v32, (int)IM_U8_MIN, (int)IM_U8_MAX); + else if (data_type == ImGuiDataType_S16) + *(ImS16*)p_data = (ImS16)ImClamp(v32, (int)IM_S16_MIN, (int)IM_S16_MAX); + else if (data_type == ImGuiDataType_U16) + *(ImU16*)p_data = (ImU16)ImClamp(v32, (int)IM_U16_MIN, (int)IM_U16_MAX); + else + IM_ASSERT(0); + } + + return memcmp(&data_backup, p_data, type_info->Size) != 0; +} + +template +static int DataTypeCompareT(const T* lhs, const T* rhs) +{ + if (*lhs < *rhs) return -1; + if (*lhs > *rhs) return +1; + return 0; +} + +int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2) +{ + switch (data_type) + { + case ImGuiDataType_S8: return DataTypeCompareT((const ImS8* )arg_1, (const ImS8* )arg_2); + case ImGuiDataType_U8: return DataTypeCompareT((const ImU8* )arg_1, (const ImU8* )arg_2); + case ImGuiDataType_S16: return DataTypeCompareT((const ImS16* )arg_1, (const ImS16* )arg_2); + case ImGuiDataType_U16: return DataTypeCompareT((const ImU16* )arg_1, (const ImU16* )arg_2); + case ImGuiDataType_S32: return DataTypeCompareT((const ImS32* )arg_1, (const ImS32* )arg_2); + case ImGuiDataType_U32: return DataTypeCompareT((const ImU32* )arg_1, (const ImU32* )arg_2); + case ImGuiDataType_S64: return DataTypeCompareT((const ImS64* )arg_1, (const ImS64* )arg_2); + case ImGuiDataType_U64: return DataTypeCompareT((const ImU64* )arg_1, (const ImU64* )arg_2); + case ImGuiDataType_Float: return DataTypeCompareT((const float* )arg_1, (const float* )arg_2); + case ImGuiDataType_Double: return DataTypeCompareT((const double*)arg_1, (const double*)arg_2); + case ImGuiDataType_COUNT: break; + } + IM_ASSERT(0); + return 0; +} + +template +static bool DataTypeClampT(T* v, const T* v_min, const T* v_max) +{ + // Clamp, both sides are optional, return true if modified + if (v_min && *v < *v_min) { *v = *v_min; return true; } + if (v_max && *v > *v_max) { *v = *v_max; return true; } + return false; +} + +bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max) +{ + switch (data_type) + { + case ImGuiDataType_S8: return DataTypeClampT((ImS8* )p_data, (const ImS8* )p_min, (const ImS8* )p_max); + case ImGuiDataType_U8: return DataTypeClampT((ImU8* )p_data, (const ImU8* )p_min, (const ImU8* )p_max); + case ImGuiDataType_S16: return DataTypeClampT((ImS16* )p_data, (const ImS16* )p_min, (const ImS16* )p_max); + case ImGuiDataType_U16: return DataTypeClampT((ImU16* )p_data, (const ImU16* )p_min, (const ImU16* )p_max); + case ImGuiDataType_S32: return DataTypeClampT((ImS32* )p_data, (const ImS32* )p_min, (const ImS32* )p_max); + case ImGuiDataType_U32: return DataTypeClampT((ImU32* )p_data, (const ImU32* )p_min, (const ImU32* )p_max); + case ImGuiDataType_S64: return DataTypeClampT((ImS64* )p_data, (const ImS64* )p_min, (const ImS64* )p_max); + case ImGuiDataType_U64: return DataTypeClampT((ImU64* )p_data, (const ImU64* )p_min, (const ImU64* )p_max); + case ImGuiDataType_Float: return DataTypeClampT((float* )p_data, (const float* )p_min, (const float* )p_max); + case ImGuiDataType_Double: return DataTypeClampT((double*)p_data, (const double*)p_min, (const double*)p_max); + case ImGuiDataType_COUNT: break; + } + IM_ASSERT(0); + return false; +} + +static float GetMinimumStepAtDecimalPrecision(int decimal_precision) +{ + static const float min_steps[10] = { 1.0f, 0.1f, 0.01f, 0.001f, 0.0001f, 0.00001f, 0.000001f, 0.0000001f, 0.00000001f, 0.000000001f }; + if (decimal_precision < 0) + return FLT_MIN; + return (decimal_precision < IM_ARRAYSIZE(min_steps)) ? min_steps[decimal_precision] : ImPow(10.0f, (float)-decimal_precision); +} + +template +static const char* ImAtoi(const char* src, TYPE* output) +{ + int negative = 0; + if (*src == '-') { negative = 1; src++; } + if (*src == '+') { src++; } + TYPE v = 0; + while (*src >= '0' && *src <= '9') + v = (v * 10) + (*src++ - '0'); + *output = negative ? -v : v; + return src; +} + +// Sanitize format +// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi +// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi. +static void SanitizeFormatString(const char* fmt, char* fmt_out, size_t fmt_out_size) +{ + IM_UNUSED(fmt_out_size); + const char* fmt_end = ImParseFormatFindEnd(fmt); + IM_ASSERT((size_t)(fmt_end - fmt + 1) < fmt_out_size); // Format is too long, let us know if this happens to you! + while (fmt < fmt_end) + { + char c = *(fmt++); + if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '. + *(fmt_out++) = c; + } + *fmt_out = 0; // Zero-terminate +} + +template +TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v) +{ + const char* fmt_start = ImParseFormatFindStart(format); + if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string + return v; + + // Sanitize format + char fmt_sanitized[32]; + SanitizeFormatString(fmt_start, fmt_sanitized, IM_ARRAYSIZE(fmt_sanitized)); + fmt_start = fmt_sanitized; + + // Format value with our rounding, and read back + char v_str[64]; + ImFormatString(v_str, IM_ARRAYSIZE(v_str), fmt_start, v); + const char* p = v_str; + while (*p == ' ') + p++; + if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) + v = (TYPE)ImAtof(p); + else + ImAtoi(p, (SIGNEDTYPE*)&v); + return v; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc. +//------------------------------------------------------------------------- +// - DragBehaviorT<>() [Internal] +// - DragBehavior() [Internal] +// - DragScalar() +// - DragScalarN() +// - DragFloat() +// - DragFloat2() +// - DragFloat3() +// - DragFloat4() +// - DragFloatRange2() +// - DragInt() +// - DragInt2() +// - DragInt3() +// - DragInt4() +// - DragIntRange2() +//------------------------------------------------------------------------- + +// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls) +template +bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags) +{ + ImGuiContext& g = *GImGui; + const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X; + const bool is_clamped = (v_min < v_max); + const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0; + const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); + + // Default tweak speed + if (v_speed == 0.0f && is_clamped && (v_max - v_min < FLT_MAX)) + v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio); + + // Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings + float adjust_delta = 0.0f; + if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR)) + { + adjust_delta = g.IO.MouseDelta[axis]; + if (g.IO.KeyAlt) + adjust_delta *= 1.0f / 100.0f; + if (g.IO.KeyShift) + adjust_delta *= 10.0f; + } + else if (g.ActiveIdSource == ImGuiInputSource_Nav) + { + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0; + adjust_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard | ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_RepeatFast, 1.0f / 10.0f, 10.0f)[axis]; + v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision)); + } + adjust_delta *= v_speed; + + // For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter. + if (axis == ImGuiAxis_Y) + adjust_delta = -adjust_delta; + + // For logarithmic use our range is effectively 0..1 so scale the delta into that range + if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0 + adjust_delta /= (float)(v_max - v_min); + + // Clear current value on activation + // Avoid altering values and clamping when we are _already_ past the limits and heading in the same direction, so e.g. if range is 0..255, current value is 300 and we are pushing to the right side, keep the 300. + bool is_just_activated = g.ActiveIdIsJustActivated; + bool is_already_past_limits_and_pushing_outward = is_clamped && ((*v >= v_max && adjust_delta > 0.0f) || (*v <= v_min && adjust_delta < 0.0f)); + if (is_just_activated || is_already_past_limits_and_pushing_outward) + { + g.DragCurrentAccum = 0.0f; + g.DragCurrentAccumDirty = false; + } + else if (adjust_delta != 0.0f) + { + g.DragCurrentAccum += adjust_delta; + g.DragCurrentAccumDirty = true; + } + + if (!g.DragCurrentAccumDirty) + return false; + + TYPE v_cur = *v; + FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f; + + float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true + const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense) + if (is_logarithmic) + { + // When using logarithmic sliders, we need to clamp to avoid hitting zero, but our choice of clamp value greatly affects slider precision. We attempt to use the specified precision to estimate a good lower bound. + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1; + logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision); + + // Convert to parametric space, apply delta, convert back + float v_old_parametric = ScaleRatioFromValueT(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + float v_new_parametric = v_old_parametric + g.DragCurrentAccum; + v_cur = ScaleValueFromRatioT(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + v_old_ref_for_accum_remainder = v_old_parametric; + } + else + { + v_cur += (SIGNEDTYPE)g.DragCurrentAccum; + } + + // Round to user desired precision based on format string + if (!(flags & ImGuiSliderFlags_NoRoundToFormat)) + v_cur = RoundScalarWithFormatT(format, data_type, v_cur); + + // Preserve remainder after rounding has been applied. This also allow slow tweaking of values. + g.DragCurrentAccumDirty = false; + if (is_logarithmic) + { + // Convert to parametric space, apply delta, convert back + float v_new_parametric = ScaleRatioFromValueT(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder); + } + else + { + g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v); + } + + // Lose zero sign for float/double + if (v_cur == (TYPE)-0) + v_cur = (TYPE)0; + + // Clamp values (+ handle overflow/wrap-around for integer types) + if (*v != v_cur && is_clamped) + { + if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point)) + v_cur = v_min; + if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point)) + v_cur = v_max; + } + + // Apply result + if (*v == v_cur) + return false; + *v = v_cur; + return true; +} + +bool ImGui::DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) +{ + // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert. + IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flags! Has the 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead."); + + ImGuiContext& g = *GImGui; + if (g.ActiveId == id) + { + if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0]) + ClearActiveID(); + else if (g.ActiveIdSource == ImGuiInputSource_Nav && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) + ClearActiveID(); + } + if (g.ActiveId != id) + return false; + if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly)) + return false; + + switch (data_type) + { + case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = DragBehaviorT(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS8*) p_min : IM_S8_MIN, p_max ? *(const ImS8*)p_max : IM_S8_MAX, format, flags); if (r) *(ImS8*)p_v = (ImS8)v32; return r; } + case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = DragBehaviorT(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU8*) p_min : IM_U8_MIN, p_max ? *(const ImU8*)p_max : IM_U8_MAX, format, flags); if (r) *(ImU8*)p_v = (ImU8)v32; return r; } + case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = DragBehaviorT(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS16*)p_min : IM_S16_MIN, p_max ? *(const ImS16*)p_max : IM_S16_MAX, format, flags); if (r) *(ImS16*)p_v = (ImS16)v32; return r; } + case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = DragBehaviorT(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU16*)p_min : IM_U16_MIN, p_max ? *(const ImU16*)p_max : IM_U16_MAX, format, flags); if (r) *(ImU16*)p_v = (ImU16)v32; return r; } + case ImGuiDataType_S32: return DragBehaviorT(data_type, (ImS32*)p_v, v_speed, p_min ? *(const ImS32* )p_min : IM_S32_MIN, p_max ? *(const ImS32* )p_max : IM_S32_MAX, format, flags); + case ImGuiDataType_U32: return DragBehaviorT(data_type, (ImU32*)p_v, v_speed, p_min ? *(const ImU32* )p_min : IM_U32_MIN, p_max ? *(const ImU32* )p_max : IM_U32_MAX, format, flags); + case ImGuiDataType_S64: return DragBehaviorT(data_type, (ImS64*)p_v, v_speed, p_min ? *(const ImS64* )p_min : IM_S64_MIN, p_max ? *(const ImS64* )p_max : IM_S64_MAX, format, flags); + case ImGuiDataType_U64: return DragBehaviorT(data_type, (ImU64*)p_v, v_speed, p_min ? *(const ImU64* )p_min : IM_U64_MIN, p_max ? *(const ImU64* )p_max : IM_U64_MAX, format, flags); + case ImGuiDataType_Float: return DragBehaviorT(data_type, (float*)p_v, v_speed, p_min ? *(const float* )p_min : -FLT_MAX, p_max ? *(const float* )p_max : FLT_MAX, format, flags); + case ImGuiDataType_Double: return DragBehaviorT(data_type, (double*)p_v, v_speed, p_min ? *(const double*)p_min : -DBL_MAX, p_max ? *(const double*)p_max : DBL_MAX, format, flags); + case ImGuiDataType_COUNT: break; + } + IM_ASSERT(0); + return false; +} + +// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a Drag widget, p_min and p_max are optional. +// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly. +bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const float w = CalcItemWidth(); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); + const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + + const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0; + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0)) + return false; + + // Default format string when passing NULL + if (format == NULL) + format = DataTypeGetInfo(data_type)->PrintFmt; + else if (data_type == ImGuiDataType_S32 && strcmp(format, "%d") != 0) // (FIXME-LEGACY: Patch old "%.0f" format string to use "%d", read function more details.) + format = PatchFormatStringFloatToInt(format); + + // Tabbing or CTRL-clicking on Drag turns it into an InputText + const bool hovered = ItemHoverable(frame_bb, id); + bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id); + if (!temp_input_is_active) + { + const bool input_requested_by_tabbing = temp_input_allowed && (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_FocusedByTabbing) != 0; + const bool clicked = (hovered && g.IO.MouseClicked[0]); + const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2); + if (input_requested_by_tabbing || clicked || double_clicked || g.NavActivateId == id || g.NavActivateInputId == id) + { + SetActiveID(id, window); + SetFocusID(id, window); + FocusWindow(window); + g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); + if (temp_input_allowed) + if (input_requested_by_tabbing || (clicked && g.IO.KeyCtrl) || double_clicked || g.NavActivateInputId == id) + temp_input_is_active = true; + } + + // Experimental: simple click (without moving) turns Drag into an InputText + if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active) + if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR)) + { + g.NavActivateId = g.NavActivateInputId = id; + g.NavActivateFlags = ImGuiActivateFlags_PreferInput; + temp_input_is_active = true; + } + } + + if (temp_input_is_active) + { + // Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set + const bool is_clamp_input = (flags & ImGuiSliderFlags_AlwaysClamp) != 0 && (p_min == NULL || p_max == NULL || DataTypeCompare(data_type, p_min, p_max) < 0); + return TempInputScalar(frame_bb, id, label, data_type, p_data, format, is_clamp_input ? p_min : NULL, is_clamp_input ? p_max : NULL); + } + + // Draw frame + const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + RenderNavHighlight(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding); + + // Drag behavior + const bool value_changed = DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, flags); + if (value_changed) + MarkItemEdited(id); + + // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. + char value_buf[64]; + const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format); + if (g.LogEnabled) + LogSetNextTextDecoration("{", "}"); + RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); + + if (label_size.x > 0.0f) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return value_changed; +} + +bool ImGui::DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + bool value_changed = false; + BeginGroup(); + PushID(label); + PushMultiItemsWidths(components, CalcItemWidth()); + size_t type_size = GDataTypeInfo[data_type].Size; + for (int i = 0; i < components; i++) + { + PushID(i); + if (i > 0) + SameLine(0, g.Style.ItemInnerSpacing.x); + value_changed |= DragScalar("", data_type, p_data, v_speed, p_min, p_max, format, flags); + PopID(); + PopItemWidth(); + p_data = (void*)((char*)p_data + type_size); + } + PopID(); + + const char* label_end = FindRenderedTextEnd(label); + if (label != label_end) + { + SameLine(0, g.Style.ItemInnerSpacing.x); + TextEx(label, label_end); + } + + EndGroup(); + return value_changed; +} + +bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, flags); +} + +bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, flags); +} + +bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, flags); +} + +bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, flags); +} + +// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this. +bool ImGui::DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed, float v_min, float v_max, const char* format, const char* format_max, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + PushID(label); + BeginGroup(); + PushMultiItemsWidths(2, CalcItemWidth()); + + float min_min = (v_min >= v_max) ? -FLT_MAX : v_min; + float min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max); + ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0); + bool value_changed = DragScalar("##min", ImGuiDataType_Float, v_current_min, v_speed, &min_min, &min_max, format, min_flags); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + + float max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min); + float max_max = (v_min >= v_max) ? FLT_MAX : v_max; + ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0); + value_changed |= DragScalar("##max", ImGuiDataType_Float, v_current_max, v_speed, &max_min, &max_max, format_max ? format_max : format, max_flags); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + + TextEx(label, FindRenderedTextEnd(label)); + EndGroup(); + PopID(); + + return value_changed; +} + +// NB: v_speed is float to allow adjusting the drag speed with more precision +bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format, flags); +} + +bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalarN(label, ImGuiDataType_S32, v, 2, v_speed, &v_min, &v_max, format, flags); +} + +bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalarN(label, ImGuiDataType_S32, v, 3, v_speed, &v_min, &v_max, format, flags); +} + +bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalarN(label, ImGuiDataType_S32, v, 4, v_speed, &v_min, &v_max, format, flags); +} + +// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this. +bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed, int v_min, int v_max, const char* format, const char* format_max, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + PushID(label); + BeginGroup(); + PushMultiItemsWidths(2, CalcItemWidth()); + + int min_min = (v_min >= v_max) ? INT_MIN : v_min; + int min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max); + ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0); + bool value_changed = DragInt("##min", v_current_min, v_speed, min_min, min_max, format, min_flags); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + + int max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min); + int max_max = (v_min >= v_max) ? INT_MAX : v_max; + ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0); + value_changed |= DragInt("##max", v_current_max, v_speed, max_min, max_max, format_max ? format_max : format, max_flags); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + + TextEx(label, FindRenderedTextEnd(label)); + EndGroup(); + PopID(); + + return value_changed; +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +// Obsolete versions with power parameter. See https://github.com/ocornut/imgui/issues/3361 for details. +bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, float power) +{ + ImGuiSliderFlags drag_flags = ImGuiSliderFlags_None; + if (power != 1.0f) + { + IM_ASSERT(power == 1.0f && "Call function with ImGuiSliderFlags_Logarithmic flags instead of using the old 'float power' function!"); + IM_ASSERT(p_min != NULL && p_max != NULL); // When using a power curve the drag needs to have known bounds + drag_flags |= ImGuiSliderFlags_Logarithmic; // Fallback for non-asserting paths + } + return DragScalar(label, data_type, p_data, v_speed, p_min, p_max, format, drag_flags); +} + +bool ImGui::DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, float power) +{ + ImGuiSliderFlags drag_flags = ImGuiSliderFlags_None; + if (power != 1.0f) + { + IM_ASSERT(power == 1.0f && "Call function with ImGuiSliderFlags_Logarithmic flags instead of using the old 'float power' function!"); + IM_ASSERT(p_min != NULL && p_max != NULL); // When using a power curve the drag needs to have known bounds + drag_flags |= ImGuiSliderFlags_Logarithmic; // Fallback for non-asserting paths + } + return DragScalarN(label, data_type, p_data, components, v_speed, p_min, p_max, format, drag_flags); +} + +#endif // IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +//------------------------------------------------------------------------- +// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc. +//------------------------------------------------------------------------- +// - ScaleRatioFromValueT<> [Internal] +// - ScaleValueFromRatioT<> [Internal] +// - SliderBehaviorT<>() [Internal] +// - SliderBehavior() [Internal] +// - SliderScalar() +// - SliderScalarN() +// - SliderFloat() +// - SliderFloat2() +// - SliderFloat3() +// - SliderFloat4() +// - SliderAngle() +// - SliderInt() +// - SliderInt2() +// - SliderInt3() +// - SliderInt4() +// - VSliderScalar() +// - VSliderFloat() +// - VSliderInt() +//------------------------------------------------------------------------- + +// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT) +template +float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize) +{ + if (v_min == v_max) + return 0.0f; + IM_UNUSED(data_type); + + const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min); + if (is_logarithmic) + { + bool flipped = v_max < v_min; + + if (flipped) // Handle the case where the range is backwards + ImSwap(v_min, v_max); + + // Fudge min/max to avoid getting close to log(0) + FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min; + FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max; + + // Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon) + if ((v_min == 0.0f) && (v_max < 0.0f)) + v_min_fudged = -logarithmic_zero_epsilon; + else if ((v_max == 0.0f) && (v_min < 0.0f)) + v_max_fudged = -logarithmic_zero_epsilon; + + float result; + + if (v_clamped <= v_min_fudged) + result = 0.0f; // Workaround for values that are in-range but below our fudge + else if (v_clamped >= v_max_fudged) + result = 1.0f; // Workaround for values that are in-range but above our fudge + else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions + { + float zero_point_center = (-(float)v_min) / ((float)v_max - (float)v_min); // The zero point in parametric space. There's an argument we should take the logarithmic nature into account when calculating this, but for now this should do (and the most common case of a symmetrical range works fine) + float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize; + float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize; + if (v == 0.0f) + result = zero_point_center; // Special case for exactly zero + else if (v < 0.0f) + result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L; + else + result = zero_point_snap_R + ((float)(ImLog((FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(v_max_fudged / logarithmic_zero_epsilon)) * (1.0f - zero_point_snap_R)); + } + else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider + result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged)); + else + result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged)); + + return flipped ? (1.0f - result) : result; + } + + // Linear slider + return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min)); +} + +// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT) +template +TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize) +{ + if (v_min == v_max) + return v_min; + const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); + + TYPE result; + if (is_logarithmic) + { + // We special-case the extents because otherwise our fudging can lead to "mathematically correct" but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value + if (t <= 0.0f) + result = v_min; + else if (t >= 1.0f) + result = v_max; + else + { + bool flipped = v_max < v_min; // Check if range is "backwards" + + // Fudge min/max to avoid getting silly results close to zero + FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min; + FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max; + + if (flipped) + ImSwap(v_min_fudged, v_max_fudged); + + // Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon) + if ((v_max == 0.0f) && (v_min < 0.0f)) + v_max_fudged = -logarithmic_zero_epsilon; + + float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range + + if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts + { + float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs((float)v_max - (float)v_min); // The zero point in parametric space + float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize; + float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize; + if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R) + result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise) + else if (t_with_flip < zero_point_center) + result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L)))); + else + result = (TYPE)(logarithmic_zero_epsilon * ImPow(v_max_fudged / logarithmic_zero_epsilon, (FLOATTYPE)((t_with_flip - zero_point_snap_R) / (1.0f - zero_point_snap_R)))); + } + else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider + result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip))); + else + result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip)); + } + } + else + { + // Linear slider + if (is_floating_point) + { + result = ImLerp(v_min, v_max, t); + } + else + { + // - For integer values we want the clicking position to match the grab box so we round above + // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property.. + // - Not doing a *1.0 multiply at the end of a range as it tends to be lossy. While absolute aiming at a large s64/u64 + // range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits. + if (t < 1.0) + { + FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t; + result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5))); + } + else + { + result = v_max; + } + } + } + + return result; +} + +// FIXME: Move more of the code into SliderBehavior() +template +bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, TYPE* v, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X; + const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0; + const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); + + const float grab_padding = 2.0f; + const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f; + float grab_sz = style.GrabMinSize; + SIGNEDTYPE v_range = (v_min < v_max ? v_max - v_min : v_min - v_max); + if (!is_floating_point && v_range >= 0) // v_range < 0 may happen on integer overflows + grab_sz = ImMax((float)(slider_sz / (v_range + 1)), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit + grab_sz = ImMin(grab_sz, slider_sz); + const float slider_usable_sz = slider_sz - grab_sz; + const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f; + const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f; + + float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true + float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true + if (is_logarithmic) + { + // When using logarithmic sliders, we need to clamp to avoid hitting zero, but our choice of clamp value greatly affects slider precision. We attempt to use the specified precision to estimate a good lower bound. + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1; + logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision); + zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(slider_usable_sz, 1.0f); + } + + // Process interacting with the slider + bool value_changed = false; + if (g.ActiveId == id) + { + bool set_new_value = false; + float clicked_t = 0.0f; + if (g.ActiveIdSource == ImGuiInputSource_Mouse) + { + if (!g.IO.MouseDown[0]) + { + ClearActiveID(); + } + else + { + const float mouse_abs_pos = g.IO.MousePos[axis]; + clicked_t = (slider_usable_sz > 0.0f) ? ImClamp((mouse_abs_pos - slider_usable_pos_min) / slider_usable_sz, 0.0f, 1.0f) : 0.0f; + if (axis == ImGuiAxis_Y) + clicked_t = 1.0f - clicked_t; + set_new_value = true; + } + } + else if (g.ActiveIdSource == ImGuiInputSource_Nav) + { + if (g.ActiveIdIsJustActivated) + { + g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation + g.SliderCurrentAccumDirty = false; + } + + const ImVec2 input_delta2 = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard | ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_RepeatFast, 0.0f, 0.0f); + float input_delta = (axis == ImGuiAxis_X) ? input_delta2.x : -input_delta2.y; + if (input_delta != 0.0f) + { + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0; + if (decimal_precision > 0) + { + input_delta /= 100.0f; // Gamepad/keyboard tweak speeds in % of slider bounds + if (IsNavInputDown(ImGuiNavInput_TweakSlow)) + input_delta /= 10.0f; + } + else + { + if ((v_range >= -100.0f && v_range <= 100.0f) || IsNavInputDown(ImGuiNavInput_TweakSlow)) + input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / (float)v_range; // Gamepad/keyboard tweak speeds in integer steps + else + input_delta /= 100.0f; + } + if (IsNavInputDown(ImGuiNavInput_TweakFast)) + input_delta *= 10.0f; + + g.SliderCurrentAccum += input_delta; + g.SliderCurrentAccumDirty = true; + } + + float delta = g.SliderCurrentAccum; + if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) + { + ClearActiveID(); + } + else if (g.SliderCurrentAccumDirty) + { + clicked_t = ScaleRatioFromValueT(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + + if ((clicked_t >= 1.0f && delta > 0.0f) || (clicked_t <= 0.0f && delta < 0.0f)) // This is to avoid applying the saturation when already past the limits + { + set_new_value = false; + g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate + } + else + { + set_new_value = true; + float old_clicked_t = clicked_t; + clicked_t = ImSaturate(clicked_t + delta); + + // Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator + TYPE v_new = ScaleValueFromRatioT(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + if (!(flags & ImGuiSliderFlags_NoRoundToFormat)) + v_new = RoundScalarWithFormatT(format, data_type, v_new); + float new_clicked_t = ScaleRatioFromValueT(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + + if (delta > 0) + g.SliderCurrentAccum -= ImMin(new_clicked_t - old_clicked_t, delta); + else + g.SliderCurrentAccum -= ImMax(new_clicked_t - old_clicked_t, delta); + } + + g.SliderCurrentAccumDirty = false; + } + } + + if (set_new_value) + { + TYPE v_new = ScaleValueFromRatioT(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + + // Round to user desired precision based on format string + if (!(flags & ImGuiSliderFlags_NoRoundToFormat)) + v_new = RoundScalarWithFormatT(format, data_type, v_new); + + // Apply result + if (*v != v_new) + { + *v = v_new; + value_changed = true; + } + } + } + + if (slider_sz < 1.0f) + { + *out_grab_bb = ImRect(bb.Min, bb.Min); + } + else + { + // Output grab position so it can be displayed by the caller + float grab_t = ScaleRatioFromValueT(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + if (axis == ImGuiAxis_Y) + grab_t = 1.0f - grab_t; + const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t); + if (axis == ImGuiAxis_X) + *out_grab_bb = ImRect(grab_pos - grab_sz * 0.5f, bb.Min.y + grab_padding, grab_pos + grab_sz * 0.5f, bb.Max.y - grab_padding); + else + *out_grab_bb = ImRect(bb.Min.x + grab_padding, grab_pos - grab_sz * 0.5f, bb.Max.x - grab_padding, grab_pos + grab_sz * 0.5f); + } + + return value_changed; +} + +// For 32-bit and larger types, slider bounds are limited to half the natural type range. +// So e.g. an integer Slider between INT_MAX-10 and INT_MAX will fail, but an integer Slider between INT_MAX/2-10 and INT_MAX/2 will be ok. +// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders. +bool ImGui::SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb) +{ + // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert. + IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flag! Has the 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead."); + + ImGuiContext& g = *GImGui; + if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly)) + return false; + + switch (data_type) + { + case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_S32, &v32, *(const ImS8*)p_min, *(const ImS8*)p_max, format, flags, out_grab_bb); if (r) *(ImS8*)p_v = (ImS8)v32; return r; } + case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_U32, &v32, *(const ImU8*)p_min, *(const ImU8*)p_max, format, flags, out_grab_bb); if (r) *(ImU8*)p_v = (ImU8)v32; return r; } + case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_S32, &v32, *(const ImS16*)p_min, *(const ImS16*)p_max, format, flags, out_grab_bb); if (r) *(ImS16*)p_v = (ImS16)v32; return r; } + case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_U32, &v32, *(const ImU16*)p_min, *(const ImU16*)p_max, format, flags, out_grab_bb); if (r) *(ImU16*)p_v = (ImU16)v32; return r; } + case ImGuiDataType_S32: + IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2); + return SliderBehaviorT(bb, id, data_type, (ImS32*)p_v, *(const ImS32*)p_min, *(const ImS32*)p_max, format, flags, out_grab_bb); + case ImGuiDataType_U32: + IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2); + return SliderBehaviorT(bb, id, data_type, (ImU32*)p_v, *(const ImU32*)p_min, *(const ImU32*)p_max, format, flags, out_grab_bb); + case ImGuiDataType_S64: + IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2); + return SliderBehaviorT(bb, id, data_type, (ImS64*)p_v, *(const ImS64*)p_min, *(const ImS64*)p_max, format, flags, out_grab_bb); + case ImGuiDataType_U64: + IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2); + return SliderBehaviorT(bb, id, data_type, (ImU64*)p_v, *(const ImU64*)p_min, *(const ImU64*)p_max, format, flags, out_grab_bb); + case ImGuiDataType_Float: + IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f); + return SliderBehaviorT(bb, id, data_type, (float*)p_v, *(const float*)p_min, *(const float*)p_max, format, flags, out_grab_bb); + case ImGuiDataType_Double: + IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f); + return SliderBehaviorT(bb, id, data_type, (double*)p_v, *(const double*)p_min, *(const double*)p_max, format, flags, out_grab_bb); + case ImGuiDataType_COUNT: break; + } + IM_ASSERT(0); + return false; +} + +// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required. +// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly. +bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const float w = CalcItemWidth(); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); + const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + + const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0; + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0)) + return false; + + // Default format string when passing NULL + if (format == NULL) + format = DataTypeGetInfo(data_type)->PrintFmt; + else if (data_type == ImGuiDataType_S32 && strcmp(format, "%d") != 0) // (FIXME-LEGACY: Patch old "%.0f" format string to use "%d", read function more details.) + format = PatchFormatStringFloatToInt(format); + + // Tabbing or CTRL-clicking on Slider turns it into an input box + const bool hovered = ItemHoverable(frame_bb, id); + bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id); + if (!temp_input_is_active) + { + const bool input_requested_by_tabbing = temp_input_allowed && (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_FocusedByTabbing) != 0; + const bool clicked = (hovered && g.IO.MouseClicked[0]); + if (input_requested_by_tabbing || clicked || g.NavActivateId == id || g.NavActivateInputId == id) + { + SetActiveID(id, window); + SetFocusID(id, window); + FocusWindow(window); + g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); + if (temp_input_allowed && (input_requested_by_tabbing || (clicked && g.IO.KeyCtrl) || g.NavActivateInputId == id)) + temp_input_is_active = true; + } + } + + if (temp_input_is_active) + { + // Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set + const bool is_clamp_input = (flags & ImGuiSliderFlags_AlwaysClamp) != 0; + return TempInputScalar(frame_bb, id, label, data_type, p_data, format, is_clamp_input ? p_min : NULL, is_clamp_input ? p_max : NULL); + } + + // Draw frame + const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + RenderNavHighlight(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding); + + // Slider behavior + ImRect grab_bb; + const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags, &grab_bb); + if (value_changed) + MarkItemEdited(id); + + // Render grab + if (grab_bb.Max.x > grab_bb.Min.x) + window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding); + + // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. + char value_buf[64]; + const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format); + if (g.LogEnabled) + LogSetNextTextDecoration("{", "}"); + RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); + + if (label_size.x > 0.0f) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return value_changed; +} + +// Add multiple sliders on 1 line for compact edition of multiple components +bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, int components, const void* v_min, const void* v_max, const char* format, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + bool value_changed = false; + BeginGroup(); + PushID(label); + PushMultiItemsWidths(components, CalcItemWidth()); + size_t type_size = GDataTypeInfo[data_type].Size; + for (int i = 0; i < components; i++) + { + PushID(i); + if (i > 0) + SameLine(0, g.Style.ItemInnerSpacing.x); + value_changed |= SliderScalar("", data_type, v, v_min, v_max, format, flags); + PopID(); + PopItemWidth(); + v = (void*)((char*)v + type_size); + } + PopID(); + + const char* label_end = FindRenderedTextEnd(label); + if (label != label_end) + { + SameLine(0, g.Style.ItemInnerSpacing.x); + TextEx(label, label_end); + } + + EndGroup(); + return value_changed; +} + +bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags) +{ + if (format == NULL) + format = "%.0f deg"; + float v_deg = (*v_rad) * 360.0f / (2 * IM_PI); + bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, format, flags); + *v_rad = v_deg * (2 * IM_PI) / 360.0f; + return value_changed; +} + +bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalar(label, ImGuiDataType_S32, v, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalarN(label, ImGuiDataType_S32, v, 2, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalarN(label, ImGuiDataType_S32, v, 3, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format, flags); +} + +bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size); + const ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + + ItemSize(bb, style.FramePadding.y); + if (!ItemAdd(frame_bb, id)) + return false; + + // Default format string when passing NULL + if (format == NULL) + format = DataTypeGetInfo(data_type)->PrintFmt; + else if (data_type == ImGuiDataType_S32 && strcmp(format, "%d") != 0) // (FIXME-LEGACY: Patch old "%.0f" format string to use "%d", read function more details.) + format = PatchFormatStringFloatToInt(format); + + const bool hovered = ItemHoverable(frame_bb, id); + if ((hovered && g.IO.MouseClicked[0]) || g.NavActivateId == id || g.NavActivateInputId == id) + { + SetActiveID(id, window); + SetFocusID(id, window); + FocusWindow(window); + g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + } + + // Draw frame + const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + RenderNavHighlight(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding); + + // Slider behavior + ImRect grab_bb; + const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags | ImGuiSliderFlags_Vertical, &grab_bb); + if (value_changed) + MarkItemEdited(id); + + // Render grab + if (grab_bb.Max.y > grab_bb.Min.y) + window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding); + + // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. + // For the vertical slider we allow centered text to overlap the frame padding + char value_buf[64]; + const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format); + RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.0f)); + if (label_size.x > 0.0f) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + return value_changed; +} + +bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return VSliderScalar(label, size, ImGuiDataType_Float, v, &v_min, &v_max, format, flags); +} + +bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return VSliderScalar(label, size, ImGuiDataType_S32, v, &v_min, &v_max, format, flags); +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +// Obsolete versions with power parameter. See https://github.com/ocornut/imgui/issues/3361 for details. +bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, float power) +{ + ImGuiSliderFlags slider_flags = ImGuiSliderFlags_None; + if (power != 1.0f) + { + IM_ASSERT(power == 1.0f && "Call function with ImGuiSliderFlags_Logarithmic flags instead of using the old 'float power' function!"); + slider_flags |= ImGuiSliderFlags_Logarithmic; // Fallback for non-asserting paths + } + return SliderScalar(label, data_type, p_data, p_min, p_max, format, slider_flags); +} + +bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, int components, const void* v_min, const void* v_max, const char* format, float power) +{ + ImGuiSliderFlags slider_flags = ImGuiSliderFlags_None; + if (power != 1.0f) + { + IM_ASSERT(power == 1.0f && "Call function with ImGuiSliderFlags_Logarithmic flags instead of using the old 'float power' function!"); + slider_flags |= ImGuiSliderFlags_Logarithmic; // Fallback for non-asserting paths + } + return SliderScalarN(label, data_type, v, components, v_min, v_max, format, slider_flags); +} + +#endif // IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +//------------------------------------------------------------------------- +// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc. +//------------------------------------------------------------------------- +// - ImParseFormatFindStart() [Internal] +// - ImParseFormatFindEnd() [Internal] +// - ImParseFormatTrimDecorations() [Internal] +// - ImParseFormatPrecision() [Internal] +// - TempInputTextScalar() [Internal] +// - InputScalar() +// - InputScalarN() +// - InputFloat() +// - InputFloat2() +// - InputFloat3() +// - InputFloat4() +// - InputInt() +// - InputInt2() +// - InputInt3() +// - InputInt4() +// - InputDouble() +//------------------------------------------------------------------------- + +// We don't use strchr() because our strings are usually very short and often start with '%' +const char* ImParseFormatFindStart(const char* fmt) +{ + while (char c = fmt[0]) + { + if (c == '%' && fmt[1] != '%') + return fmt; + else if (c == '%') + fmt++; + fmt++; + } + return fmt; +} + +const char* ImParseFormatFindEnd(const char* fmt) +{ + // Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format. + if (fmt[0] != '%') + return fmt; + const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A')); + const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a')); + for (char c; (c = *fmt) != 0; fmt++) + { + if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0) + return fmt + 1; + if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0) + return fmt + 1; + } + return fmt; +} + +// Extract the format out of a format string with leading or trailing decorations +// fmt = "blah blah" -> return fmt +// fmt = "%.3f" -> return fmt +// fmt = "hello %.3f" -> return fmt + 6 +// fmt = "%.3f hello" -> return buf written with "%.3f" +const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size) +{ + const char* fmt_start = ImParseFormatFindStart(fmt); + if (fmt_start[0] != '%') + return fmt; + const char* fmt_end = ImParseFormatFindEnd(fmt_start); + if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data. + return fmt_start; + ImStrncpy(buf, fmt_start, ImMin((size_t)(fmt_end - fmt_start) + 1, buf_size)); + return buf; +} + +// Parse display precision back from the display format string +// FIXME: This is still used by some navigation code path to infer a minimum tweak step, but we should aim to rework widgets so it isn't needed. +int ImParseFormatPrecision(const char* fmt, int default_precision) +{ + fmt = ImParseFormatFindStart(fmt); + if (fmt[0] != '%') + return default_precision; + fmt++; + while (*fmt >= '0' && *fmt <= '9') + fmt++; + int precision = INT_MAX; + if (*fmt == '.') + { + fmt = ImAtoi(fmt + 1, &precision); + if (precision < 0 || precision > 99) + precision = default_precision; + } + if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation + precision = -1; + if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX) + precision = -1; + return (precision == INT_MAX) ? default_precision : precision; +} + +// Create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets) +// FIXME: Facilitate using this in variety of other situations. +bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags) +{ + // On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id. + // We clear ActiveID on the first frame to allow the InputText() taking it back. + ImGuiContext& g = *GImGui; + const bool init = (g.TempInputId != id); + if (init) + ClearActiveID(); + + g.CurrentWindow->DC.CursorPos = bb.Min; + bool value_changed = InputTextEx(label, NULL, buf, buf_size, bb.GetSize(), flags | ImGuiInputTextFlags_MergedItem); + if (init) + { + // First frame we started displaying the InputText widget, we expect it to take the active id. + IM_ASSERT(g.ActiveId == id); + g.TempInputId = g.ActiveId; + } + return value_changed; +} + +// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set! +// This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility. +// However this may not be ideal for all uses, as some user code may break on out of bound values. +bool ImGui::TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data, const char* format, const void* p_clamp_min, const void* p_clamp_max) +{ + char fmt_buf[32]; + char data_buf[32]; + format = ImParseFormatTrimDecorations(format, fmt_buf, IM_ARRAYSIZE(fmt_buf)); + DataTypeFormatString(data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format); + ImStrTrimBlanks(data_buf); + + ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_NoMarkEdited; + flags |= ((data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) ? ImGuiInputTextFlags_CharsScientific : ImGuiInputTextFlags_CharsDecimal); + bool value_changed = false; + if (TempInputText(bb, id, label, data_buf, IM_ARRAYSIZE(data_buf), flags)) + { + // Backup old value + size_t data_type_size = DataTypeGetInfo(data_type)->Size; + ImGuiDataTypeTempStorage data_backup; + memcpy(&data_backup, p_data, data_type_size); + + // Apply new value (or operations) then clamp + DataTypeApplyFromText(data_buf, data_type, p_data, NULL); + if (p_clamp_min || p_clamp_max) + { + if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, p_clamp_min, p_clamp_max) > 0) + ImSwap(p_clamp_min, p_clamp_max); + DataTypeClamp(data_type, p_data, p_clamp_min, p_clamp_max); + } + + // Only mark as edited if new value is different + value_changed = memcmp(&data_backup, p_data, data_type_size) != 0; + if (value_changed) + MarkItemEdited(id); + } + return value_changed; +} + +// Note: p_data, p_step, p_step_fast are _pointers_ to a memory address holding the data. For an Input widget, p_step and p_step_fast are optional. +// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly. +bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + + if (format == NULL) + format = DataTypeGetInfo(data_type)->PrintFmt; + + char buf[64]; + DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format); + + bool value_changed = false; + if ((flags & (ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsScientific)) == 0) + flags |= ImGuiInputTextFlags_CharsDecimal; + flags |= ImGuiInputTextFlags_AutoSelectAll; + flags |= ImGuiInputTextFlags_NoMarkEdited; // We call MarkItemEdited() ourselves by comparing the actual data rather than the string. + + if (p_step != NULL) + { + const float button_size = GetFrameHeight(); + + BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive() + PushID(label); + SetNextItemWidth(ImMax(1.0f, CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2)); + if (InputText("", buf, IM_ARRAYSIZE(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view + value_changed = DataTypeApplyFromText(buf, data_type, p_data, format); + + // Step buttons + const ImVec2 backup_frame_padding = style.FramePadding; + style.FramePadding.x = style.FramePadding.y; + ImGuiButtonFlags button_flags = ImGuiButtonFlags_Repeat | ImGuiButtonFlags_DontClosePopups; + if (flags & ImGuiInputTextFlags_ReadOnly) + BeginDisabled(); + SameLine(0, style.ItemInnerSpacing.x); + if (ButtonEx("-", ImVec2(button_size, button_size), button_flags)) + { + DataTypeApplyOp(data_type, '-', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step); + value_changed = true; + } + SameLine(0, style.ItemInnerSpacing.x); + if (ButtonEx("+", ImVec2(button_size, button_size), button_flags)) + { + DataTypeApplyOp(data_type, '+', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step); + value_changed = true; + } + if (flags & ImGuiInputTextFlags_ReadOnly) + EndDisabled(); + + const char* label_end = FindRenderedTextEnd(label); + if (label != label_end) + { + SameLine(0, style.ItemInnerSpacing.x); + TextEx(label, label_end); + } + style.FramePadding = backup_frame_padding; + + PopID(); + EndGroup(); + } + else + { + if (InputText(label, buf, IM_ARRAYSIZE(buf), flags)) + value_changed = DataTypeApplyFromText(buf, data_type, p_data, format); + } + if (value_changed) + MarkItemEdited(g.LastItemData.ID); + + return value_changed; +} + +bool ImGui::InputScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + bool value_changed = false; + BeginGroup(); + PushID(label); + PushMultiItemsWidths(components, CalcItemWidth()); + size_t type_size = GDataTypeInfo[data_type].Size; + for (int i = 0; i < components; i++) + { + PushID(i); + if (i > 0) + SameLine(0, g.Style.ItemInnerSpacing.x); + value_changed |= InputScalar("", data_type, p_data, p_step, p_step_fast, format, flags); + PopID(); + PopItemWidth(); + p_data = (void*)((char*)p_data + type_size); + } + PopID(); + + const char* label_end = FindRenderedTextEnd(label); + if (label != label_end) + { + SameLine(0.0f, g.Style.ItemInnerSpacing.x); + TextEx(label, label_end); + } + + EndGroup(); + return value_changed; +} + +bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags) +{ + flags |= ImGuiInputTextFlags_CharsScientific; + return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step > 0.0f ? &step : NULL), (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags); +} + +bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags) +{ + return InputScalarN(label, ImGuiDataType_Float, v, 2, NULL, NULL, format, flags); +} + +bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags) +{ + return InputScalarN(label, ImGuiDataType_Float, v, 3, NULL, NULL, format, flags); +} + +bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags) +{ + return InputScalarN(label, ImGuiDataType_Float, v, 4, NULL, NULL, format, flags); +} + +bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags) +{ + // Hexadecimal input provided as a convenience but the flag name is awkward. Typically you'd use InputText() to parse your own data, if you want to handle prefixes. + const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d"; + return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step > 0 ? &step : NULL), (void*)(step_fast > 0 ? &step_fast : NULL), format, flags); +} + +bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags) +{ + return InputScalarN(label, ImGuiDataType_S32, v, 2, NULL, NULL, "%d", flags); +} + +bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags) +{ + return InputScalarN(label, ImGuiDataType_S32, v, 3, NULL, NULL, "%d", flags); +} + +bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags) +{ + return InputScalarN(label, ImGuiDataType_S32, v, 4, NULL, NULL, "%d", flags); +} + +bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags) +{ + flags |= ImGuiInputTextFlags_CharsScientific; + return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step > 0.0 ? &step : NULL), (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags); +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint +//------------------------------------------------------------------------- +// - InputText() +// - InputTextWithHint() +// - InputTextMultiline() +// - InputTextEx() [Internal] +//------------------------------------------------------------------------- + +bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() + return InputTextEx(label, NULL, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data); +} + +bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + return InputTextEx(label, NULL, buf, (int)buf_size, size, flags | ImGuiInputTextFlags_Multiline, callback, user_data); +} + +bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() + return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data); +} + +static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end) +{ + int line_count = 0; + const char* s = text_begin; + while (char c = *s++) // We are only matching for \n so we can ignore UTF-8 decoding + if (c == '\n') + line_count++; + s--; + if (s[0] != '\n' && s[0] != '\r') + line_count++; + *out_text_end = s; + return line_count; +} + +static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining, ImVec2* out_offset, bool stop_on_new_line) +{ + ImGuiContext& g = *GImGui; + ImFont* font = g.Font; + const float line_height = g.FontSize; + const float scale = line_height / font->FontSize; + + ImVec2 text_size = ImVec2(0, 0); + float line_width = 0.0f; + + const ImWchar* s = text_begin; + while (s < text_end) + { + unsigned int c = (unsigned int)(*s++); + if (c == '\n') + { + text_size.x = ImMax(text_size.x, line_width); + text_size.y += line_height; + line_width = 0.0f; + if (stop_on_new_line) + break; + continue; + } + if (c == '\r') + continue; + + const float char_width = font->GetCharAdvance((ImWchar)c) * scale; + line_width += char_width; + } + + if (text_size.x < line_width) + text_size.x = line_width; + + if (out_offset) + *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n + + if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n + text_size.y += line_height; + + if (remaining) + *remaining = s; + + return text_size; +} + +// Wrapper for stb_textedit.h to edit text (our wrapper is for: statically sized buffer, single-line, wchar characters. InputText converts between UTF-8 and wchar) +namespace ImStb +{ + +static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->CurLenW; } +static ImWchar STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { return obj->TextW[idx]; } +static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { ImWchar c = obj->TextW[line_start_idx + char_idx]; if (c == '\n') return STB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *GImGui; return g.Font->GetCharAdvance(c) * (g.FontSize / g.Font->FontSize); } +static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x200000 ? 0 : key; } +static ImWchar STB_TEXTEDIT_NEWLINE = '\n'; +static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx) +{ + const ImWchar* text = obj->TextW.Data; + const ImWchar* text_remaining = NULL; + const ImVec2 size = InputTextCalcTextSizeW(text + line_start_idx, text + obj->CurLenW, &text_remaining, NULL, true); + r->x0 = 0.0f; + r->x1 = size.x; + r->baseline_y_delta = size.y; + r->ymin = 0.0f; + r->ymax = size.y; + r->num_chars = (int)(text_remaining - (text + line_start_idx)); +} + +// When ImGuiInputTextFlags_Password is set, we don't want actions such as CTRL+Arrow to leak the fact that underlying data are blanks or separators. +static bool is_separator(unsigned int c) { return ImCharIsBlankW(c) || c==',' || c==';' || c=='(' || c==')' || c=='{' || c=='}' || c=='[' || c==']' || c=='|' || c=='\n' || c=='\r'; } +static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx) { if (obj->Flags & ImGuiInputTextFlags_Password) return 0; return idx > 0 ? (is_separator(obj->TextW[idx - 1]) && !is_separator(obj->TextW[idx]) ) : 1; } +static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx) { if (obj->Flags & ImGuiInputTextFlags_Password) return 0; return idx > 0 ? (!is_separator(obj->TextW[idx - 1]) && is_separator(obj->TextW[idx])) : 1; } +static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx) { idx--; while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) idx--; return idx < 0 ? 0 : idx; } +static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_left(obj, idx)) idx++; return idx > len ? len : idx; } +#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h +#ifdef __APPLE__ // FIXME: Move setting to IO structure +#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_MAC +#else +static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_right(obj, idx)) idx++; return idx > len ? len : idx; } +#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_WIN +#endif + +static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n) +{ + ImWchar* dst = obj->TextW.Data + pos; + + // We maintain our buffer length in both UTF-8 and wchar formats + obj->Edited = true; + obj->CurLenA -= ImTextCountUtf8BytesFromStr(dst, dst + n); + obj->CurLenW -= n; + + // Offset remaining text (FIXME-OPT: Use memmove) + const ImWchar* src = obj->TextW.Data + pos + n; + while (ImWchar c = *src++) + *dst++ = c; + *dst = '\0'; +} + +static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const ImWchar* new_text, int new_text_len) +{ + const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0; + const int text_len = obj->CurLenW; + IM_ASSERT(pos <= text_len); + + const int new_text_len_utf8 = ImTextCountUtf8BytesFromStr(new_text, new_text + new_text_len); + if (!is_resizable && (new_text_len_utf8 + obj->CurLenA + 1 > obj->BufCapacityA)) + return false; + + // Grow internal buffer if needed + if (new_text_len + text_len + 1 > obj->TextW.Size) + { + if (!is_resizable) + return false; + IM_ASSERT(text_len < obj->TextW.Size); + obj->TextW.resize(text_len + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1); + } + + ImWchar* text = obj->TextW.Data; + if (pos != text_len) + memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos) * sizeof(ImWchar)); + memcpy(text + pos, new_text, (size_t)new_text_len * sizeof(ImWchar)); + + obj->Edited = true; + obj->CurLenW += new_text_len; + obj->CurLenA += new_text_len_utf8; + obj->TextW[obj->CurLenW] = '\0'; + + return true; +} + +// We don't use an enum so we can build even with conflicting symbols (if another user of stb_textedit.h leak their STB_TEXTEDIT_K_* symbols) +#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left +#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right +#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up +#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down +#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line +#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line +#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text +#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text +#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor +#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor +#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo +#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo +#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word +#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word +#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page +#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page +#define STB_TEXTEDIT_K_SHIFT 0x400000 + +#define STB_TEXTEDIT_IMPLEMENTATION +#include "imstb_textedit.h" + +// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling +// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?) +static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const STB_TEXTEDIT_CHARTYPE* text, int text_len) +{ + stb_text_makeundo_replace(str, state, 0, str->CurLenW, text_len); + ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->CurLenW); + if (text_len <= 0) + return; + if (ImStb::STB_TEXTEDIT_INSERTCHARS(str, 0, text, text_len)) + { + state->cursor = text_len; + state->has_preferred_x = 0; + return; + } + IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace() +} + +} // namespace ImStb + +void ImGuiInputTextState::OnKeyPressed(int key) +{ + stb_textedit_key(this, &Stb, key); + CursorFollow = true; + CursorAnimReset(); +} + +ImGuiInputTextCallbackData::ImGuiInputTextCallbackData() +{ + memset(this, 0, sizeof(*this)); +} + +// Public API to manipulate UTF-8 text +// We expose UTF-8 to the user (unlike the STB_TEXTEDIT_* functions which are manipulating wchar) +// FIXME: The existence of this rarely exercised code path is a bit of a nuisance. +void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count) +{ + IM_ASSERT(pos + bytes_count <= BufTextLen); + char* dst = Buf + pos; + const char* src = Buf + pos + bytes_count; + while (char c = *src++) + *dst++ = c; + *dst = '\0'; + + if (CursorPos >= pos + bytes_count) + CursorPos -= bytes_count; + else if (CursorPos >= pos) + CursorPos = pos; + SelectionStart = SelectionEnd = CursorPos; + BufDirty = true; + BufTextLen -= bytes_count; +} + +void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end) +{ + const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0; + const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)strlen(new_text); + if (new_text_len + BufTextLen >= BufSize) + { + if (!is_resizable) + return; + + // Contrary to STB_TEXTEDIT_INSERTCHARS() this is working in the UTF8 buffer, hence the mildly similar code (until we remove the U16 buffer altogether!) + ImGuiContext& g = *GImGui; + ImGuiInputTextState* edit_state = &g.InputTextState; + IM_ASSERT(edit_state->ID != 0 && g.ActiveId == edit_state->ID); + IM_ASSERT(Buf == edit_state->TextA.Data); + int new_buf_size = BufTextLen + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1; + edit_state->TextA.reserve(new_buf_size + 1); + Buf = edit_state->TextA.Data; + BufSize = edit_state->BufCapacityA = new_buf_size; + } + + if (BufTextLen != pos) + memmove(Buf + pos + new_text_len, Buf + pos, (size_t)(BufTextLen - pos)); + memcpy(Buf + pos, new_text, (size_t)new_text_len * sizeof(char)); + Buf[BufTextLen + new_text_len] = '\0'; + + if (CursorPos >= pos) + CursorPos += new_text_len; + SelectionStart = SelectionEnd = CursorPos; + BufDirty = true; + BufTextLen += new_text_len; +} + +// Return false to discard a character. +static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, ImGuiInputSource input_source) +{ + IM_ASSERT(input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Clipboard); + unsigned int c = *p_char; + + // Filter non-printable (NB: isprint is unreliable! see #2467) + bool apply_named_filters = true; + if (c < 0x20) + { + bool pass = false; + pass |= (c == '\n' && (flags & ImGuiInputTextFlags_Multiline)); + pass |= (c == '\t' && (flags & ImGuiInputTextFlags_AllowTabInput)); + if (!pass) + return false; + apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted. + } + + if (input_source != ImGuiInputSource_Clipboard) + { + // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817) + if (c == 127) + return false; + + // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME) + if (c >= 0xE000 && c <= 0xF8FF) + return false; + } + + // Filter Unicode ranges we are not handling in this build + if (c > IM_UNICODE_CODEPOINT_MAX) + return false; + + // Generic named filters + if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific))) + { + // The libc allows overriding locale, with e.g. 'setlocale(LC_NUMERIC, "de_DE.UTF-8");' which affect the output/input of printf/scanf to use e.g. ',' instead of '.'. + // The standard mandate that programs starts in the "C" locale where the decimal point is '.'. + // We don't really intend to provide widespread support for it, but out of empathy for people stuck with using odd API, we support the bare minimum aka overriding the decimal point. + // Change the default decimal_point with: + // ImGui::GetCurrentContext()->PlatformLocaleDecimalPoint = *localeconv()->decimal_point; + // Users of non-default decimal point (in particular ',') may be affected by word-selection logic (is_word_boundary_from_right/is_word_boundary_from_left) functions. + ImGuiContext& g = *GImGui; + const unsigned c_decimal_point = (unsigned int)g.PlatformLocaleDecimalPoint; + + // Allow 0-9 . - + * / + if (flags & ImGuiInputTextFlags_CharsDecimal) + if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/')) + return false; + + // Allow 0-9 . - + * / e E + if (flags & ImGuiInputTextFlags_CharsScientific) + if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E')) + return false; + + // Allow 0-9 a-F A-F + if (flags & ImGuiInputTextFlags_CharsHexadecimal) + if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F')) + return false; + + // Turn a-z into A-Z + if (flags & ImGuiInputTextFlags_CharsUppercase) + if (c >= 'a' && c <= 'z') + *p_char = (c += (unsigned int)('A' - 'a')); + + if (flags & ImGuiInputTextFlags_CharsNoBlank) + if (ImCharIsBlankW(c)) + return false; + } + + // Custom callback filter + if (flags & ImGuiInputTextFlags_CallbackCharFilter) + { + ImGuiInputTextCallbackData callback_data; + memset(&callback_data, 0, sizeof(ImGuiInputTextCallbackData)); + callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter; + callback_data.EventChar = (ImWchar)c; + callback_data.Flags = flags; + callback_data.UserData = user_data; + if (callback(&callback_data) != 0) + return false; + *p_char = callback_data.EventChar; + if (!callback_data.EventChar) + return false; + } + + return true; +} + +// Edit a string of text +// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!". +// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match +// Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator. +// - When active, hold on a privately held copy of the text (and apply back to 'buf'). So changing 'buf' while the InputText is active has no effect. +// - If you want to use ImGui::InputText() with std::string, see misc/cpp/imgui_stdlib.h +// (FIXME: Rather confusing and messy function, among the worse part of our codebase, expecting to rewrite a V2 at some point.. Partly because we are +// doing UTF8 > U16 > UTF8 conversions on the go to easily interface with stb_textedit. Ideally should stay in UTF-8 all the time. See https://github.com/nothings/stb/issues/188) +bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_size, const ImVec2& size_arg, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* callback_user_data) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + IM_ASSERT(buf != NULL && buf_size >= 0); + IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys) + IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key) + + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + const ImGuiStyle& style = g.Style; + + const bool RENDER_SELECTION_WHEN_INACTIVE = false; + const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0; + const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0; + const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0; + const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0; + const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0; + if (is_resizable) + IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag! + + if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar) + BeginGroup(); + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line + const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y); + + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); + const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size); + + ImGuiWindow* draw_window = window; + ImVec2 inner_size = frame_size; + ImGuiItemStatusFlags item_status_flags = 0; + ImGuiLastItemData item_data_backup; + if (is_multiline) + { + ImVec2 backup_pos = window->DC.CursorPos; + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable)) + { + EndGroup(); + return false; + } + item_status_flags = g.LastItemData.StatusFlags; + item_data_backup = g.LastItemData; + window->DC.CursorPos = backup_pos; + + // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug. + // FIXME-NAV: Pressing NavActivate will trigger general child activation right before triggering our own below. Harmless but bizarre. + PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]); + PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding); + PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize); + PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges + bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), true, ImGuiWindowFlags_NoMove); + PopStyleVar(3); + PopStyleColor(); + if (!child_visible) + { + EndChild(); + EndGroup(); + return false; + } + draw_window = g.CurrentWindow; // Child window + draw_window->DC.NavLayersActiveMaskNext |= (1 << draw_window->DC.NavLayerCurrent); // This is to ensure that EndChild() will display a navigation highlight so we can "enter" into it. + draw_window->DC.CursorPos += style.FramePadding; + inner_size.x -= draw_window->ScrollbarSizes.x; + } + else + { + // Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd) + ItemSize(total_bb, style.FramePadding.y); + if (!(flags & ImGuiInputTextFlags_MergedItem)) + if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable)) + return false; + item_status_flags = g.LastItemData.StatusFlags; + } + const bool hovered = ItemHoverable(frame_bb, id); + if (hovered) + g.MouseCursor = ImGuiMouseCursor_TextInput; + + // We are only allowed to access the state if we are already the active widget. + ImGuiInputTextState* state = GetInputTextState(id); + + const bool input_requested_by_tabbing = (item_status_flags & ImGuiItemStatusFlags_FocusedByTabbing) != 0; + const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateInputId == id) || (g.NavActivateId == id && g.NavInputSource == ImGuiInputSource_Keyboard)); + + const bool user_clicked = hovered && io.MouseClicked[0]; + const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(draw_window, ImGuiAxis_Y); + const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(draw_window, ImGuiAxis_Y); + bool clear_active_id = false; + bool select_all = false; + + float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX; + + const bool init_changed_specs = (state != NULL && state->Stb.single_line != !is_multiline); + const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav || input_requested_by_tabbing); + const bool init_state = (init_make_active || user_scroll_active); + if ((init_state && g.ActiveId != id) || init_changed_specs) + { + // Access state even if we don't own it yet. + state = &g.InputTextState; + state->CursorAnimReset(); + + // Take a copy of the initial buffer value (both in original UTF-8 format and converted to wchar) + // From the moment we focused we are ignoring the content of 'buf' (unless we are in read-only mode) + const int buf_len = (int)strlen(buf); + state->InitialTextA.resize(buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string. + memcpy(state->InitialTextA.Data, buf, buf_len + 1); + + // Start edition + const char* buf_end = NULL; + state->TextW.resize(buf_size + 1); // wchar count <= UTF-8 count. we use +1 to make sure that .Data is always pointing to at least an empty string. + state->TextA.resize(0); + state->TextAIsValid = false; // TextA is not valid yet (we will display buf until then) + state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, buf_size, buf, NULL, &buf_end); + state->CurLenA = (int)(buf_end - buf); // We can't get the result from ImStrncpy() above because it is not UTF-8 aware. Here we'll cut off malformed UTF-8. + + // Preserve cursor position and undo/redo stack if we come back to same widget + // FIXME: For non-readonly widgets we might be able to require that TextAIsValid && TextA == buf ? (untested) and discard undo stack if user buffer has changed. + const bool recycle_state = (state->ID == id && !init_changed_specs); + if (recycle_state) + { + // Recycle existing cursor/selection/undo stack but clamp position + // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler. + state->CursorClamp(); + } + else + { + state->ID = id; + state->ScrollX = 0.0f; + stb_textedit_initialize_state(&state->Stb, !is_multiline); + } + + if (!is_multiline) + { + if (flags & ImGuiInputTextFlags_AutoSelectAll) + select_all = true; + if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState))) + select_all = true; + if (input_requested_by_tabbing || (user_clicked && io.KeyCtrl)) + select_all = true; + } + + if (flags & ImGuiInputTextFlags_AlwaysOverwrite) + state->Stb.insert_mode = 1; // stb field name is indeed incorrect (see #2863) + } + + if (g.ActiveId != id && init_make_active) + { + IM_ASSERT(state && state->ID == id); + SetActiveID(id, window); + SetFocusID(id, window); + FocusWindow(window); + + // Declare our inputs + IM_ASSERT(ImGuiNavInput_COUNT < 32); + g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); + if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory)) + g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + g.ActiveIdUsingNavInputMask |= (1 << ImGuiNavInput_Cancel); + SetActiveIdUsingKey(ImGuiKey_Home); + SetActiveIdUsingKey(ImGuiKey_End); + if (is_multiline) + { + SetActiveIdUsingKey(ImGuiKey_PageUp); + SetActiveIdUsingKey(ImGuiKey_PageDown); + } + if (flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_AllowTabInput)) // Disable keyboard tabbing out as we will use the \t character. + { + SetActiveIdUsingKey(ImGuiKey_Tab); + } + } + + // We have an edge case if ActiveId was set through another widget (e.g. widget being swapped), clear id immediately (don't wait until the end of the function) + if (g.ActiveId == id && state == NULL) + ClearActiveID(); + + // Release focus when we click outside + if (g.ActiveId == id && io.MouseClicked[0] && !init_state && !init_make_active) //-V560 + clear_active_id = true; + + // Lock the decision of whether we are going to take the path displaying the cursor or selection + const bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active); + bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor); + bool value_changed = false; + bool enter_pressed = false; + + // When read-only we always use the live data passed to the function + // FIXME-OPT: Because our selection/cursor code currently needs the wide text we need to convert it when active, which is not ideal :( + if (is_readonly && state != NULL && (render_cursor || render_selection)) + { + const char* buf_end = NULL; + state->TextW.resize(buf_size + 1); + state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, state->TextW.Size, buf, NULL, &buf_end); + state->CurLenA = (int)(buf_end - buf); + state->CursorClamp(); + render_selection &= state->HasSelection(); + } + + // Select the buffer to render. + const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state && state->TextAIsValid; + const bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0); + + // Password pushes a temporary font with only a fallback glyph + if (is_password && !is_displaying_hint) + { + const ImFontGlyph* glyph = g.Font->FindGlyph('*'); + ImFont* password_font = &g.InputTextPasswordFont; + password_font->FontSize = g.Font->FontSize; + password_font->Scale = g.Font->Scale; + password_font->Ascent = g.Font->Ascent; + password_font->Descent = g.Font->Descent; + password_font->ContainerAtlas = g.Font->ContainerAtlas; + password_font->FallbackGlyph = glyph; + password_font->FallbackAdvanceX = glyph->AdvanceX; + IM_ASSERT(password_font->Glyphs.empty() && password_font->IndexAdvanceX.empty() && password_font->IndexLookup.empty()); + PushFont(password_font); + } + + // Process mouse inputs and character inputs + int backup_current_text_length = 0; + if (g.ActiveId == id) + { + IM_ASSERT(state != NULL); + backup_current_text_length = state->CurLenA; + state->Edited = false; + state->BufCapacityA = buf_size; + state->Flags = flags; + + // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget. + // Down the line we should have a cleaner library-wide concept of Selected vs Active. + g.ActiveIdAllowOverlap = !io.MouseDown[0]; + g.WantTextInputNextFrame = 1; + + // Edit in progress + const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->ScrollX; + const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f)); + + const bool is_osx = io.ConfigMacOSXBehaviors; + if (select_all) + { + state->SelectAll(); + state->SelectedAllMouseLock = true; + } + else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift) + { + stb_textedit_click(state, &state->Stb, mouse_x, mouse_y); + const int multiclick_count = (io.MouseClickedCount[0] - 2); + if ((multiclick_count % 2) == 0) + { + // Double-click: Select word + // We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant: + // FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS) + const bool is_bol = (state->Stb.cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb.cursor - 1) == '\n'; + if (STB_TEXT_HAS_SELECTION(&state->Stb) || !is_bol) + state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT); + //state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT); + if (!STB_TEXT_HAS_SELECTION(&state->Stb)) + ImStb::stb_textedit_prep_selection_at_cursor(&state->Stb); + state->Stb.cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(state, state->Stb.cursor); + state->Stb.select_end = state->Stb.cursor; + ImStb::stb_textedit_clamp(state, &state->Stb); + } + else + { + // Triple-click: Select line + const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb.cursor) == '\n'; + state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART); + state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT); + state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT); + if (!is_eol && is_multiline) + { + ImSwap(state->Stb.select_start, state->Stb.select_end); + state->Stb.cursor = state->Stb.select_end; + } + state->CursorFollow = false; + } + state->CursorAnimReset(); + } + else if (io.MouseClicked[0] && !state->SelectedAllMouseLock) + { + // FIXME: unselect on late click could be done release? + if (hovered) + { + stb_textedit_click(state, &state->Stb, mouse_x, mouse_y); + state->CursorAnimReset(); + } + } + else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f)) + { + stb_textedit_drag(state, &state->Stb, mouse_x, mouse_y); + state->CursorAnimReset(); + state->CursorFollow = true; + } + if (state->SelectedAllMouseLock && !io.MouseDown[0]) + state->SelectedAllMouseLock = false; + + // It is ill-defined whether the backend needs to send a \t character when pressing the TAB keys. + // Win32 and GLFW naturally do it but not SDL. + const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeySuper); + if ((flags & ImGuiInputTextFlags_AllowTabInput) && IsKeyPressed(ImGuiKey_Tab) && !ignore_char_inputs && !io.KeyShift && !is_readonly) + if (!io.InputQueueCharacters.contains('\t')) + { + unsigned int c = '\t'; // Insert TAB + if (InputTextFilterCharacter(&c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard)) + state->OnKeyPressed((int)c); + } + + // Process regular text input (before we check for Return because using some IME will effectively send a Return?) + // We ignore CTRL inputs, but need to allow ALT+CTRL as some keyboards (e.g. German) use AltGR (which _is_ Alt+Ctrl) to input certain characters. + if (io.InputQueueCharacters.Size > 0) + { + if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav) + for (int n = 0; n < io.InputQueueCharacters.Size; n++) + { + // Insert character if they pass filtering + unsigned int c = (unsigned int)io.InputQueueCharacters[n]; + if (c == '\t' && io.KeyShift) + continue; + if (InputTextFilterCharacter(&c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard)) + state->OnKeyPressed((int)c); + } + + // Consume characters + io.InputQueueCharacters.resize(0); + } + } + + // Process other shortcuts/key-presses + bool cancel_edit = false; + if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id) + { + IM_ASSERT(state != NULL); + IM_ASSERT(io.KeyMods == GetMergedKeyModFlags() && "Mismatching io.KeyCtrl/io.KeyShift/io.KeyAlt/io.KeySuper vs io.KeyMods"); // We rarely do this check, but if anything let's do it here. + + const int row_count_per_page = ImMax((int)((inner_size.y - style.FramePadding.y) / g.FontSize), 1); + state->Stb.row_count_per_page = row_count_per_page; + + const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0); + const bool is_osx = io.ConfigMacOSXBehaviors; + const bool is_osx_shift_shortcut = is_osx && (io.KeyMods == (ImGuiKeyModFlags_Super | ImGuiKeyModFlags_Shift)); + const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl + const bool is_startend_key_down = is_osx && io.KeySuper && !io.KeyCtrl && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End + const bool is_ctrl_key_only = (io.KeyMods == ImGuiKeyModFlags_Ctrl); + const bool is_shift_key_only = (io.KeyMods == ImGuiKeyModFlags_Shift); + const bool is_shortcut_key = g.IO.ConfigMacOSXBehaviors ? (io.KeyMods == ImGuiKeyModFlags_Super) : (io.KeyMods == ImGuiKeyModFlags_Ctrl); + + const bool is_cut = ((is_shortcut_key && IsKeyPressed(ImGuiKey_X)) || (is_shift_key_only && IsKeyPressed(ImGuiKey_Delete))) && !is_readonly && !is_password && (!is_multiline || state->HasSelection()); + const bool is_copy = ((is_shortcut_key && IsKeyPressed(ImGuiKey_C)) || (is_ctrl_key_only && IsKeyPressed(ImGuiKey_Insert))) && !is_password && (!is_multiline || state->HasSelection()); + const bool is_paste = ((is_shortcut_key && IsKeyPressed(ImGuiKey_V)) || (is_shift_key_only && IsKeyPressed(ImGuiKey_Insert))) && !is_readonly; + const bool is_undo = ((is_shortcut_key && IsKeyPressed(ImGuiKey_Z)) && !is_readonly && is_undoable); + const bool is_redo = ((is_shortcut_key && IsKeyPressed(ImGuiKey_Y)) || (is_osx_shift_shortcut && IsKeyPressed(ImGuiKey_Z))) && !is_readonly && is_undoable; + + // We allow validate/cancel with Nav source (gamepad) to makes it easier to undo an accidental NavInput press with no keyboard wired, but otherwise it isn't very useful. + const bool is_validate_enter = IsKeyPressed(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_KeypadEnter); + const bool is_validate_nav = (IsNavInputTest(ImGuiNavInput_Activate, ImGuiInputReadMode_Pressed) && !IsKeyPressed(ImGuiKey_Space)) || IsNavInputTest(ImGuiNavInput_Input, ImGuiInputReadMode_Pressed); + const bool is_cancel = IsKeyPressed(ImGuiKey_Escape) || IsNavInputTest(ImGuiNavInput_Cancel, ImGuiInputReadMode_Pressed); + + if (IsKeyPressed(ImGuiKey_LeftArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); } + else if (IsKeyPressed(ImGuiKey_RightArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); } + else if (IsKeyPressed(ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMax(draw_window->Scroll.y - g.FontSize, 0.0f)); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); } + else if (IsKeyPressed(ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMin(draw_window->Scroll.y + g.FontSize, GetScrollMaxY())); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); } + else if (IsKeyPressed(ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; } + else if (IsKeyPressed(ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; } + else if (IsKeyPressed(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); } + else if (IsKeyPressed(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); } + else if (IsKeyPressed(ImGuiKey_Delete) && !is_readonly && !is_cut) { state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask); } + else if (IsKeyPressed(ImGuiKey_Backspace) && !is_readonly) + { + if (!state->HasSelection()) + { + if (is_wordmove_key_down) + state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT); + else if (is_osx && io.KeySuper && !io.KeyAlt && !io.KeyCtrl) + state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT); + } + state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask); + } + else if (is_validate_enter) + { + bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0; + if (!is_multiline || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl)) + { + enter_pressed = clear_active_id = true; + } + else if (!is_readonly) + { + unsigned int c = '\n'; // Insert new line + if (InputTextFilterCharacter(&c, flags, callback, callback_user_data, ImGuiInputSource_Keyboard)) + state->OnKeyPressed((int)c); + } + } + else if (is_validate_nav) + { + IM_ASSERT(!is_validate_enter); + enter_pressed = clear_active_id = true; + } + else if (is_cancel) + { + clear_active_id = cancel_edit = true; + } + else if (is_undo || is_redo) + { + state->OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO); + state->ClearSelection(); + } + else if (is_shortcut_key && IsKeyPressed(ImGuiKey_A)) + { + state->SelectAll(); + state->CursorFollow = true; + } + else if (is_cut || is_copy) + { + // Cut, Copy + if (io.SetClipboardTextFn) + { + const int ib = state->HasSelection() ? ImMin(state->Stb.select_start, state->Stb.select_end) : 0; + const int ie = state->HasSelection() ? ImMax(state->Stb.select_start, state->Stb.select_end) : state->CurLenW; + const int clipboard_data_len = ImTextCountUtf8BytesFromStr(state->TextW.Data + ib, state->TextW.Data + ie) + 1; + char* clipboard_data = (char*)IM_ALLOC(clipboard_data_len * sizeof(char)); + ImTextStrToUtf8(clipboard_data, clipboard_data_len, state->TextW.Data + ib, state->TextW.Data + ie); + SetClipboardText(clipboard_data); + MemFree(clipboard_data); + } + if (is_cut) + { + if (!state->HasSelection()) + state->SelectAll(); + state->CursorFollow = true; + stb_textedit_cut(state, &state->Stb); + } + } + else if (is_paste) + { + if (const char* clipboard = GetClipboardText()) + { + // Filter pasted buffer + const int clipboard_len = (int)strlen(clipboard); + ImWchar* clipboard_filtered = (ImWchar*)IM_ALLOC((clipboard_len + 1) * sizeof(ImWchar)); + int clipboard_filtered_len = 0; + for (const char* s = clipboard; *s; ) + { + unsigned int c; + s += ImTextCharFromUtf8(&c, s, NULL); + if (c == 0) + break; + if (!InputTextFilterCharacter(&c, flags, callback, callback_user_data, ImGuiInputSource_Clipboard)) + continue; + clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c; + } + clipboard_filtered[clipboard_filtered_len] = 0; + if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation + { + stb_textedit_paste(state, &state->Stb, clipboard_filtered, clipboard_filtered_len); + state->CursorFollow = true; + } + MemFree(clipboard_filtered); + } + } + + // Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame. + render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor); + } + + // Process callbacks and apply result back to user's buffer. + const char* apply_new_text = NULL; + int apply_new_text_length = 0; + if (g.ActiveId == id) + { + IM_ASSERT(state != NULL); + if (cancel_edit) + { + // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents. + if (!is_readonly && strcmp(buf, state->InitialTextA.Data) != 0) + { + // Push records into the undo stack so we can CTRL+Z the revert operation itself + apply_new_text = state->InitialTextA.Data; + apply_new_text_length = state->InitialTextA.Size - 1; + ImVector w_text; + if (apply_new_text_length > 0) + { + w_text.resize(ImTextCountCharsFromUtf8(apply_new_text, apply_new_text + apply_new_text_length) + 1); + ImTextStrFromUtf8(w_text.Data, w_text.Size, apply_new_text, apply_new_text + apply_new_text_length); + } + stb_textedit_replace(state, &state->Stb, w_text.Data, (apply_new_text_length > 0) ? (w_text.Size - 1) : 0); + } + } + + // When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer before clearing ActiveId, even though strictly speaking it wasn't modified on this frame. + // If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail. + // This also allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize). + bool apply_edit_back_to_user_buffer = !cancel_edit || (enter_pressed && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0); + if (apply_edit_back_to_user_buffer) + { + // Apply new value immediately - copy modified buffer back + // Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer + // FIXME: We actually always render 'buf' when calling DrawList->AddText, making the comment above incorrect. + // FIXME-OPT: CPU waste to do this every time the widget is active, should mark dirty state from the stb_textedit callbacks. + if (!is_readonly) + { + state->TextAIsValid = true; + state->TextA.resize(state->TextW.Size * 4 + 1); + ImTextStrToUtf8(state->TextA.Data, state->TextA.Size, state->TextW.Data, NULL); + } + + // User callback + if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0) + { + IM_ASSERT(callback != NULL); + + // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment. + ImGuiInputTextFlags event_flag = 0; + ImGuiKey event_key = ImGuiKey_None; + if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && IsKeyPressed(ImGuiKey_Tab)) + { + event_flag = ImGuiInputTextFlags_CallbackCompletion; + event_key = ImGuiKey_Tab; + } + else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_UpArrow)) + { + event_flag = ImGuiInputTextFlags_CallbackHistory; + event_key = ImGuiKey_UpArrow; + } + else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_DownArrow)) + { + event_flag = ImGuiInputTextFlags_CallbackHistory; + event_key = ImGuiKey_DownArrow; + } + else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited) + { + event_flag = ImGuiInputTextFlags_CallbackEdit; + } + else if (flags & ImGuiInputTextFlags_CallbackAlways) + { + event_flag = ImGuiInputTextFlags_CallbackAlways; + } + + if (event_flag) + { + ImGuiInputTextCallbackData callback_data; + memset(&callback_data, 0, sizeof(ImGuiInputTextCallbackData)); + callback_data.EventFlag = event_flag; + callback_data.Flags = flags; + callback_data.UserData = callback_user_data; + + char* callback_buf = is_readonly ? buf : state->TextA.Data; + callback_data.EventKey = event_key; + callback_data.Buf = callback_buf; + callback_data.BufTextLen = state->CurLenA; + callback_data.BufSize = state->BufCapacityA; + callback_data.BufDirty = false; + + // We have to convert from wchar-positions to UTF-8-positions, which can be pretty slow (an incentive to ditch the ImWchar buffer, see https://github.com/nothings/stb/issues/188) + ImWchar* text = state->TextW.Data; + const int utf8_cursor_pos = callback_data.CursorPos = ImTextCountUtf8BytesFromStr(text, text + state->Stb.cursor); + const int utf8_selection_start = callback_data.SelectionStart = ImTextCountUtf8BytesFromStr(text, text + state->Stb.select_start); + const int utf8_selection_end = callback_data.SelectionEnd = ImTextCountUtf8BytesFromStr(text, text + state->Stb.select_end); + + // Call user code + callback(&callback_data); + + // Read back what user may have modified + callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback + IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields + IM_ASSERT(callback_data.BufSize == state->BufCapacityA); + IM_ASSERT(callback_data.Flags == flags); + const bool buf_dirty = callback_data.BufDirty; + if (callback_data.CursorPos != utf8_cursor_pos || buf_dirty) { state->Stb.cursor = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.CursorPos); state->CursorFollow = true; } + if (callback_data.SelectionStart != utf8_selection_start || buf_dirty) { state->Stb.select_start = (callback_data.SelectionStart == callback_data.CursorPos) ? state->Stb.cursor : ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionStart); } + if (callback_data.SelectionEnd != utf8_selection_end || buf_dirty) { state->Stb.select_end = (callback_data.SelectionEnd == callback_data.SelectionStart) ? state->Stb.select_start : ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionEnd); } + if (buf_dirty) + { + IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text! + if (callback_data.BufTextLen > backup_current_text_length && is_resizable) + state->TextW.resize(state->TextW.Size + (callback_data.BufTextLen - backup_current_text_length)); + state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, state->TextW.Size, callback_data.Buf, NULL); + state->CurLenA = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen() + state->CursorAnimReset(); + } + } + } + + // Will copy result string if modified + if (!is_readonly && strcmp(state->TextA.Data, buf) != 0) + { + apply_new_text = state->TextA.Data; + apply_new_text_length = state->CurLenA; + } + } + + // Clear temporary user storage + state->Flags = ImGuiInputTextFlags_None; + } + + // Copy result to user buffer. This can currently only happen when (g.ActiveId == id) + if (apply_new_text != NULL) + { + // We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size + // of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used + // without any storage on user's side. + IM_ASSERT(apply_new_text_length >= 0); + if (is_resizable) + { + ImGuiInputTextCallbackData callback_data; + callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize; + callback_data.Flags = flags; + callback_data.Buf = buf; + callback_data.BufTextLen = apply_new_text_length; + callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1); + callback_data.UserData = callback_user_data; + callback(&callback_data); + buf = callback_data.Buf; + buf_size = callback_data.BufSize; + apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1); + IM_ASSERT(apply_new_text_length <= buf_size); + } + //IMGUI_DEBUG_LOG("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length); + + // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size. + ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size)); + value_changed = true; + } + + // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value) + if (clear_active_id && g.ActiveId == id) + ClearActiveID(); + + // Render frame + if (!is_multiline) + { + RenderNavHighlight(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + } + + const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size + ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding; + ImVec2 text_size(0.0f, 0.0f); + + // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line + // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether. + // Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash. + const int buf_display_max_length = 2 * 1024 * 1024; + const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595 + const char* buf_display_end = NULL; // We have specialized paths below for setting the length + if (is_displaying_hint) + { + buf_display = hint; + buf_display_end = hint + strlen(hint); + } + + // Render text. We currently only render selection when the widget is active or while scrolling. + // FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive. + if (render_cursor || render_selection) + { + IM_ASSERT(state != NULL); + if (!is_displaying_hint) + buf_display_end = buf_display + state->CurLenA; + + // Render text (with cursor and selection) + // This is going to be messy. We need to: + // - Display the text (this alone can be more easily clipped) + // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation) + // - Measure text height (for scrollbar) + // We are attempting to do most of that in **one main pass** to minimize the computation cost (non-negligible for large amount of text) + 2nd pass for selection rendering (we could merge them by an extra refactoring effort) + // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8. + const ImWchar* text_begin = state->TextW.Data; + ImVec2 cursor_offset, select_start_offset; + + { + // Find lines numbers straddling 'cursor' (slot 0) and 'select_start' (slot 1) positions. + const ImWchar* searches_input_ptr[2] = { NULL, NULL }; + int searches_result_line_no[2] = { -1000, -1000 }; + int searches_remaining = 0; + if (render_cursor) + { + searches_input_ptr[0] = text_begin + state->Stb.cursor; + searches_result_line_no[0] = -1; + searches_remaining++; + } + if (render_selection) + { + searches_input_ptr[1] = text_begin + ImMin(state->Stb.select_start, state->Stb.select_end); + searches_result_line_no[1] = -1; + searches_remaining++; + } + + // Iterate all lines to find our line numbers + // In multi-line mode, we never exit the loop until all lines are counted, so add one extra to the searches_remaining counter. + searches_remaining += is_multiline ? 1 : 0; + int line_count = 0; + //for (const ImWchar* s = text_begin; (s = (const ImWchar*)wcschr((const wchar_t*)s, (wchar_t)'\n')) != NULL; s++) // FIXME-OPT: Could use this when wchar_t are 16-bit + for (const ImWchar* s = text_begin; *s != 0; s++) + if (*s == '\n') + { + line_count++; + if (searches_result_line_no[0] == -1 && s >= searches_input_ptr[0]) { searches_result_line_no[0] = line_count; if (--searches_remaining <= 0) break; } + if (searches_result_line_no[1] == -1 && s >= searches_input_ptr[1]) { searches_result_line_no[1] = line_count; if (--searches_remaining <= 0) break; } + } + line_count++; + if (searches_result_line_no[0] == -1) + searches_result_line_no[0] = line_count; + if (searches_result_line_no[1] == -1) + searches_result_line_no[1] = line_count; + + // Calculate 2d position by finding the beginning of the line and measuring distance + cursor_offset.x = InputTextCalcTextSizeW(ImStrbolW(searches_input_ptr[0], text_begin), searches_input_ptr[0]).x; + cursor_offset.y = searches_result_line_no[0] * g.FontSize; + if (searches_result_line_no[1] >= 0) + { + select_start_offset.x = InputTextCalcTextSizeW(ImStrbolW(searches_input_ptr[1], text_begin), searches_input_ptr[1]).x; + select_start_offset.y = searches_result_line_no[1] * g.FontSize; + } + + // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224) + if (is_multiline) + text_size = ImVec2(inner_size.x, line_count * g.FontSize); + } + + // Scroll + if (render_cursor && state->CursorFollow) + { + // Horizontal scroll in chunks of quarter width + if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll)) + { + const float scroll_increment_x = inner_size.x * 0.25f; + const float visible_width = inner_size.x - style.FramePadding.x; + if (cursor_offset.x < state->ScrollX) + state->ScrollX = IM_FLOOR(ImMax(0.0f, cursor_offset.x - scroll_increment_x)); + else if (cursor_offset.x - visible_width >= state->ScrollX) + state->ScrollX = IM_FLOOR(cursor_offset.x - visible_width + scroll_increment_x); + } + else + { + state->ScrollX = 0.0f; + } + + // Vertical scroll + if (is_multiline) + { + // Test if cursor is vertically visible + if (cursor_offset.y - g.FontSize < scroll_y) + scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize); + else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y) + scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f; + const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f); + scroll_y = ImClamp(scroll_y, 0.0f, scroll_max_y); + draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag + draw_window->Scroll.y = scroll_y; + } + + state->CursorFollow = false; + } + + // Draw selection + const ImVec2 draw_scroll = ImVec2(state->ScrollX, 0.0f); + if (render_selection) + { + const ImWchar* text_selected_begin = text_begin + ImMin(state->Stb.select_start, state->Stb.select_end); + const ImWchar* text_selected_end = text_begin + ImMax(state->Stb.select_start, state->Stb.select_end); + + ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg, render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests. + float bg_offy_up = is_multiline ? 0.0f : -1.0f; // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection. + float bg_offy_dn = is_multiline ? 0.0f : 2.0f; + ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll; + for (const ImWchar* p = text_selected_begin; p < text_selected_end; ) + { + if (rect_pos.y > clip_rect.w + g.FontSize) + break; + if (rect_pos.y < clip_rect.y) + { + //p = (const ImWchar*)wmemchr((const wchar_t*)p, '\n', text_selected_end - p); // FIXME-OPT: Could use this when wchar_t are 16-bit + //p = p ? p + 1 : text_selected_end; + while (p < text_selected_end) + if (*p++ == '\n') + break; + } + else + { + ImVec2 rect_size = InputTextCalcTextSizeW(p, text_selected_end, &p, NULL, true); + if (rect_size.x <= 0.0f) rect_size.x = IM_FLOOR(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines + ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn)); + rect.ClipWith(clip_rect); + if (rect.Overlaps(clip_rect)) + draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color); + } + rect_pos.x = draw_pos.x - draw_scroll.x; + rect_pos.y += g.FontSize; + } + } + + // We test for 'buf_display_max_length' as a way to avoid some pathological cases (e.g. single-line 1 MB string) which would make ImDrawList crash. + if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length) + { + ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); + draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect); + } + + // Draw blinking cursor + if (render_cursor) + { + state->CursorAnim += io.DeltaTime; + bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f; + ImVec2 cursor_screen_pos = ImFloor(draw_pos + cursor_offset - draw_scroll); + ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f); + if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect)) + draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_Text)); + + // Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.) + if (!is_readonly) + { + g.PlatformImeData.WantVisible = true; + g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize); + g.PlatformImeData.InputLineHeight = g.FontSize; + } + } + } + else + { + // Render text only (no selection, no cursor) + if (is_multiline) + text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_display_end) * g.FontSize); // We don't need width + else if (!is_displaying_hint && g.ActiveId == id) + buf_display_end = buf_display + state->CurLenA; + else if (!is_displaying_hint) + buf_display_end = buf_display + strlen(buf_display); + + if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length) + { + ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); + draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect); + } + } + + if (is_password && !is_displaying_hint) + PopFont(); + + if (is_multiline) + { + // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (ref issue #4761)... + Dummy(ImVec2(text_size.x, text_size.y + style.FramePadding.y)); + ImGuiItemFlags backup_item_flags = g.CurrentItemFlags; + g.CurrentItemFlags |= ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop; + EndChild(); + item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow); + g.CurrentItemFlags = backup_item_flags; + + // ...and then we need to undo the group overriding last item data, which gets a bit messy as EndGroup() tries to forward scrollbar being active... + // FIXME: This quite messy/tricky, should attempt to get rid of the child window. + EndGroup(); + if (g.LastItemData.ID == 0) + { + g.LastItemData.ID = id; + g.LastItemData.InFlags = item_data_backup.InFlags; + g.LastItemData.StatusFlags = item_data_backup.StatusFlags; + } + } + + // Log as text + if (g.LogEnabled && (!is_password || is_displaying_hint)) + { + LogSetNextTextDecoration("{", "}"); + LogRenderedText(&draw_pos, buf_display, buf_display_end); + } + + if (label_size.x > 0) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + if (value_changed && !(flags & ImGuiInputTextFlags_NoMarkEdited)) + MarkItemEdited(id); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0) + return enter_pressed; + else + return value_changed; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc. +//------------------------------------------------------------------------- +// - ColorEdit3() +// - ColorEdit4() +// - ColorPicker3() +// - RenderColorRectWithAlphaCheckerboard() [Internal] +// - ColorPicker4() +// - ColorButton() +// - SetColorEditOptions() +// - ColorTooltip() [Internal] +// - ColorEditOptionsPopup() [Internal] +// - ColorPickerOptionsPopup() [Internal] +//------------------------------------------------------------------------- + +bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags) +{ + return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha); +} + +// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation. +// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting. +static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V) +{ + // This check is optional. Suppose we have two color widgets side by side, both widgets display different colors, but both colors have hue and/or saturation undefined. + // With color check: hue/saturation is preserved in one widget. Editing color in one widget would reset hue/saturation in another one. + // Without color check: common hue/saturation would be displayed in all widgets that have hue/saturation undefined. + // g.ColorEditLastColor is stored as ImU32 RGB value: this essentially gives us color equality check with reduced precision. + // Tiny external color changes would not be detected and this check would still pass. This is OK, since we only restore hue/saturation _only_ if they are undefined, + // therefore this change flipping hue/saturation from undefined to a very tiny value would still be represented in color picker. + ImGuiContext& g = *GImGui; + if (g.ColorEditLastColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0))) + return; + + // When S == 0, H is undefined. + // When H == 1 it wraps around to 0. + if (*S == 0.0f || (*H == 0.0f && g.ColorEditLastHue == 1)) + *H = g.ColorEditLastHue; + + // When V == 0, S is undefined. + if (*V == 0.0f) + *S = g.ColorEditLastSat; +} + +// Edit colors components (each component in 0.0f..1.0f range). +// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. +// With typical options: Left-click on color square to open color picker. Right-click to open option menu. CTRL-Click over input fields to edit them and TAB to go to next item. +bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float square_sz = GetFrameHeight(); + const float w_full = CalcItemWidth(); + const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x); + const float w_inputs = w_full - w_button; + const char* label_display_end = FindRenderedTextEnd(label); + g.NextItemData.ClearFlags(); + + BeginGroup(); + PushID(label); + + // If we're not showing any slider there's no point in doing any HSV conversions + const ImGuiColorEditFlags flags_untouched = flags; + if (flags & ImGuiColorEditFlags_NoInputs) + flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions; + + // Context menu: display and modify options (before defaults are applied) + if (!(flags & ImGuiColorEditFlags_NoOptions)) + ColorEditOptionsPopup(col, flags); + + // Read stored options + if (!(flags & ImGuiColorEditFlags_DisplayMask_)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_); + if (!(flags & ImGuiColorEditFlags_DataTypeMask_)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_); + if (!(flags & ImGuiColorEditFlags_PickerMask_)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_); + if (!(flags & ImGuiColorEditFlags_InputMask_)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_); + flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_)); + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected + + const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0; + const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0; + const int components = alpha ? 4 : 3; + + // Convert to the formats we need + float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f }; + if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB)) + ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]); + else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV)) + { + // Hue is lost when converting from greyscale rgb (saturation=0). Restore it. + ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]); + ColorEditRestoreHS(col, &f[0], &f[1], &f[2]); + } + int i[4] = { IM_F32_TO_INT8_UNBOUND(f[0]), IM_F32_TO_INT8_UNBOUND(f[1]), IM_F32_TO_INT8_UNBOUND(f[2]), IM_F32_TO_INT8_UNBOUND(f[3]) }; + + bool value_changed = false; + bool value_changed_as_float = false; + + const ImVec2 pos = window->DC.CursorPos; + const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f; + window->DC.CursorPos.x = pos.x + inputs_offset_x; + + if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) + { + // RGB/HSV 0..255 Sliders + const float w_item_one = ImMax(1.0f, IM_FLOOR((w_inputs - (style.ItemInnerSpacing.x) * (components - 1)) / (float)components)); + const float w_item_last = ImMax(1.0f, IM_FLOOR(w_inputs - (w_item_one + style.ItemInnerSpacing.x) * (components - 1))); + + const bool hide_prefix = (w_item_one <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x); + static const char* ids[4] = { "##X", "##Y", "##Z", "##W" }; + static const char* fmt_table_int[3][4] = + { + { "%3d", "%3d", "%3d", "%3d" }, // Short display + { "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA + { "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA + }; + static const char* fmt_table_float[3][4] = + { + { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display + { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA + { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA + }; + const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1; + + for (int n = 0; n < components; n++) + { + if (n > 0) + SameLine(0, style.ItemInnerSpacing.x); + SetNextItemWidth((n + 1 < components) ? w_item_one : w_item_last); + + // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0. + if (flags & ImGuiColorEditFlags_Float) + { + value_changed |= DragFloat(ids[n], &f[n], 1.0f / 255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]); + value_changed_as_float |= value_changed; + } + else + { + value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n]); + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + } + } + else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) + { + // RGB Hexadecimal Input + char buf[64]; + if (alpha) + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255), ImClamp(i[3], 0, 255)); + else + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255)); + SetNextItemWidth(w_inputs); + if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase)) + { + value_changed = true; + char* p = buf; + while (*p == '#' || ImCharIsBlankA(*p)) + p++; + i[0] = i[1] = i[2] = 0; + i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha) + int r; + if (alpha) + r = sscanf(p, "%02X%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2], (unsigned int*)&i[3]); // Treat at unsigned (%X is unsigned) + else + r = sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]); + IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'. + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + } + + ImGuiWindow* picker_active_window = NULL; + if (!(flags & ImGuiColorEditFlags_NoSmallPreview)) + { + const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x; + window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y); + + const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f); + if (ColorButton("##ColorButton", col_v4, flags)) + { + if (!(flags & ImGuiColorEditFlags_NoPicker)) + { + // Store current color and open a picker + g.ColorPickerRef = col_v4; + OpenPopup("picker"); + SetNextWindowPos(g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y)); + } + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + + if (BeginPopup("picker")) + { + picker_active_window = g.CurrentWindow; + if (label != label_display_end) + { + TextEx(label, label_display_end); + Spacing(); + } + ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar; + ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf; + SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes? + value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x); + EndPopup(); + } + } + + if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel)) + { + const float text_offset_x = (flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x; + window->DC.CursorPos = ImVec2(pos.x + text_offset_x, pos.y + style.FramePadding.y); + TextEx(label, label_display_end); + } + + // Convert back + if (value_changed && picker_active_window == NULL) + { + if (!value_changed_as_float) + for (int n = 0; n < 4; n++) + f[n] = i[n] / 255.0f; + if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB)) + { + g.ColorEditLastHue = f[0]; + g.ColorEditLastSat = f[1]; + ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]); + g.ColorEditLastColor = ColorConvertFloat4ToU32(ImVec4(f[0], f[1], f[2], 0)); + } + if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV)) + ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]); + + col[0] = f[0]; + col[1] = f[1]; + col[2] = f[2]; + if (alpha) + col[3] = f[3]; + } + + PopID(); + EndGroup(); + + // Drag and Drop Target + // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test. + if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget()) + { + bool accepted_drag_drop = false; + if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) + { + memcpy((float*)col, payload->Data, sizeof(float) * 3); // Preserve alpha if any //-V512 + value_changed = accepted_drag_drop = true; + } + if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) + { + memcpy((float*)col, payload->Data, sizeof(float) * components); + value_changed = accepted_drag_drop = true; + } + + // Drag-drop payloads are always RGB + if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV)) + ColorConvertRGBtoHSV(col[0], col[1], col[2], col[0], col[1], col[2]); + EndDragDropTarget(); + } + + // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4(). + if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window) + g.LastItemData.ID = g.ActiveId; + + if (value_changed) + MarkItemEdited(g.LastItemData.ID); + + return value_changed; +} + +bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags) +{ + float col4[4] = { col[0], col[1], col[2], 1.0f }; + if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha)) + return false; + col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2]; + return true; +} + +// Helper for ColorPicker4() +static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha) +{ + ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha); + ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x + 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Right, IM_COL32(0,0,0,alpha8)); + ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32(255,255,255,alpha8)); + ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x - 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Left, IM_COL32(0,0,0,alpha8)); + ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32(255,255,255,alpha8)); +} + +// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. +// (In C++ the 'float col[4]' notation for a function argument is equivalent to 'float* col', we only specify a size to facilitate understanding of the code.) +// FIXME: we adjust the big color square height based on item width, which may cause a flickering feedback loop (if automatic height makes a vertical scrollbar appears, affecting automatic width..) +// FIXME: this is trying to be aware of style.Alpha but not fully correct. Also, the color wheel will have overlapping glitches with (style.Alpha < 1.0) +bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImDrawList* draw_list = window->DrawList; + ImGuiStyle& style = g.Style; + ImGuiIO& io = g.IO; + + const float width = CalcItemWidth(); + g.NextItemData.ClearFlags(); + + PushID(label); + BeginGroup(); + + if (!(flags & ImGuiColorEditFlags_NoSidePreview)) + flags |= ImGuiColorEditFlags_NoSmallPreview; + + // Context menu: display and store options. + if (!(flags & ImGuiColorEditFlags_NoOptions)) + ColorPickerOptionsPopup(col, flags); + + // Read stored options + if (!(flags & ImGuiColorEditFlags_PickerMask_)) + flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_; + if (!(flags & ImGuiColorEditFlags_InputMask_)) + flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_; + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected + if (!(flags & ImGuiColorEditFlags_NoOptions)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar); + + // Setup + int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4; + bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha); + ImVec2 picker_pos = window->DC.CursorPos; + float square_sz = GetFrameHeight(); + float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars + float sv_picker_size = ImMax(bars_width * 1, width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box + float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x; + float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x; + float bars_triangles_half_sz = IM_FLOOR(bars_width * 0.20f); + + float backup_initial_col[4]; + memcpy(backup_initial_col, col, components * sizeof(float)); + + float wheel_thickness = sv_picker_size * 0.08f; + float wheel_r_outer = sv_picker_size * 0.50f; + float wheel_r_inner = wheel_r_outer - wheel_thickness; + ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f); + + // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic. + float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f); + ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point. + ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point. + ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point. + + float H = col[0], S = col[1], V = col[2]; + float R = col[0], G = col[1], B = col[2]; + if (flags & ImGuiColorEditFlags_InputRGB) + { + // Hue is lost when converting from greyscale rgb (saturation=0). Restore it. + ColorConvertRGBtoHSV(R, G, B, H, S, V); + ColorEditRestoreHS(col, &H, &S, &V); + } + else if (flags & ImGuiColorEditFlags_InputHSV) + { + ColorConvertHSVtoRGB(H, S, V, R, G, B); + } + + bool value_changed = false, value_changed_h = false, value_changed_sv = false; + + PushItemFlag(ImGuiItemFlags_NoNav, true); + if (flags & ImGuiColorEditFlags_PickerHueWheel) + { + // Hue wheel + SV triangle logic + InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size)); + if (IsItemActive()) + { + ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center; + ImVec2 current_off = g.IO.MousePos - wheel_center; + float initial_dist2 = ImLengthSqr(initial_off); + if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1)) + { + // Interactive with Hue wheel + H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f; + if (H < 0.0f) + H += 1.0f; + value_changed = value_changed_h = true; + } + float cos_hue_angle = ImCos(-H * 2.0f * IM_PI); + float sin_hue_angle = ImSin(-H * 2.0f * IM_PI); + if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle))) + { + // Interacting with SV triangle + ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle); + if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated)) + current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated); + float uu, vv, ww; + ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww); + V = ImClamp(1.0f - vv, 0.0001f, 1.0f); + S = ImClamp(uu / V, 0.0001f, 1.0f); + value_changed = value_changed_sv = true; + } + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + } + else if (flags & ImGuiColorEditFlags_PickerHueBar) + { + // SV rectangle logic + InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size)); + if (IsItemActive()) + { + S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size - 1)); + V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); + + // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square. + if (g.ColorEditLastColor == ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0))) + H = g.ColorEditLastHue; + value_changed = value_changed_sv = true; + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + + // Hue bar logic + SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y)); + InvisibleButton("hue", ImVec2(bars_width, sv_picker_size)); + if (IsItemActive()) + { + H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); + value_changed = value_changed_h = true; + } + } + + // Alpha bar logic + if (alpha_bar) + { + SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y)); + InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size)); + if (IsItemActive()) + { + col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); + value_changed = true; + } + } + PopItemFlag(); // ImGuiItemFlags_NoNav + + if (!(flags & ImGuiColorEditFlags_NoSidePreview)) + { + SameLine(0, style.ItemInnerSpacing.x); + BeginGroup(); + } + + if (!(flags & ImGuiColorEditFlags_NoLabel)) + { + const char* label_display_end = FindRenderedTextEnd(label); + if (label != label_display_end) + { + if ((flags & ImGuiColorEditFlags_NoSidePreview)) + SameLine(0, style.ItemInnerSpacing.x); + TextEx(label, label_display_end); + } + } + + if (!(flags & ImGuiColorEditFlags_NoSidePreview)) + { + PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true); + ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); + if ((flags & ImGuiColorEditFlags_NoLabel)) + Text("Current"); + + ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf | ImGuiColorEditFlags_NoTooltip; + ColorButton("##current", col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2)); + if (ref_col != NULL) + { + Text("Original"); + ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]); + if (ColorButton("##original", ref_col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2))) + { + memcpy(col, ref_col, components * sizeof(float)); + value_changed = true; + } + } + PopItemFlag(); + EndGroup(); + } + + // Convert back color to RGB + if (value_changed_h || value_changed_sv) + { + if (flags & ImGuiColorEditFlags_InputRGB) + { + ColorConvertHSVtoRGB(H, S, V, col[0], col[1], col[2]); + g.ColorEditLastHue = H; + g.ColorEditLastSat = S; + g.ColorEditLastColor = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)); + } + else if (flags & ImGuiColorEditFlags_InputHSV) + { + col[0] = H; + col[1] = S; + col[2] = V; + } + } + + // R,G,B and H,S,V slider color editor + bool value_changed_fix_hue_wrap = false; + if ((flags & ImGuiColorEditFlags_NoInputs) == 0) + { + PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x); + ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf; + ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker; + if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0) + if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_DisplayRGB)) + { + // FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget. + // For the later we don't want to run the hue-wrap canceling code. If you are well versed in HSV picker please provide your input! (See #2050) + value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap); + value_changed = true; + } + if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0) + value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_DisplayHSV); + if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0) + value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_DisplayHex); + PopItemWidth(); + } + + // Try to cancel hue wrap (after ColorEdit4 call), if any + if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB)) + { + float new_H, new_S, new_V; + ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V); + if (new_H <= 0 && H > 0) + { + if (new_V <= 0 && V != new_V) + ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]); + else if (new_S <= 0) + ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]); + } + } + + if (value_changed) + { + if (flags & ImGuiColorEditFlags_InputRGB) + { + R = col[0]; + G = col[1]; + B = col[2]; + ColorConvertRGBtoHSV(R, G, B, H, S, V); + ColorEditRestoreHS(col, &H, &S, &V); // Fix local Hue as display below will use it immediately. + } + else if (flags & ImGuiColorEditFlags_InputHSV) + { + H = col[0]; + S = col[1]; + V = col[2]; + ColorConvertHSVtoRGB(H, S, V, R, G, B); + } + } + + const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha); + const ImU32 col_black = IM_COL32(0,0,0,style_alpha8); + const ImU32 col_white = IM_COL32(255,255,255,style_alpha8); + const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8); + const ImU32 col_hues[6 + 1] = { IM_COL32(255,0,0,style_alpha8), IM_COL32(255,255,0,style_alpha8), IM_COL32(0,255,0,style_alpha8), IM_COL32(0,255,255,style_alpha8), IM_COL32(0,0,255,style_alpha8), IM_COL32(255,0,255,style_alpha8), IM_COL32(255,0,0,style_alpha8) }; + + ImVec4 hue_color_f(1, 1, 1, style.Alpha); ColorConvertHSVtoRGB(H, 1, 1, hue_color_f.x, hue_color_f.y, hue_color_f.z); + ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f); + ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!! + + ImVec2 sv_cursor_pos; + + if (flags & ImGuiColorEditFlags_PickerHueWheel) + { + // Render Hue Wheel + const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out). + const int segment_per_arc = ImMax(4, (int)wheel_r_outer / 12); + for (int n = 0; n < 6; n++) + { + const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps; + const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps; + const int vert_start_idx = draw_list->VtxBuffer.Size; + draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc); + draw_list->PathStroke(col_white, 0, wheel_thickness); + const int vert_end_idx = draw_list->VtxBuffer.Size; + + // Paint colors over existing vertices + ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner); + ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner); + ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col_hues[n], col_hues[n + 1]); + } + + // Render Cursor + preview on Hue Wheel + float cos_hue_angle = ImCos(H * 2.0f * IM_PI); + float sin_hue_angle = ImSin(H * 2.0f * IM_PI); + ImVec2 hue_cursor_pos(wheel_center.x + cos_hue_angle * (wheel_r_inner + wheel_r_outer) * 0.5f, wheel_center.y + sin_hue_angle * (wheel_r_inner + wheel_r_outer) * 0.5f); + float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f; + int hue_cursor_segments = ImClamp((int)(hue_cursor_rad / 1.4f), 9, 32); + draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments); + draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad + 1, col_midgrey, hue_cursor_segments); + draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, col_white, hue_cursor_segments); + + // Render SV triangle (rotated according to hue) + ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle); + ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle); + ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle); + ImVec2 uv_white = GetFontTexUvWhitePixel(); + draw_list->PrimReserve(6, 6); + draw_list->PrimVtx(tra, uv_white, hue_color32); + draw_list->PrimVtx(trb, uv_white, hue_color32); + draw_list->PrimVtx(trc, uv_white, col_white); + draw_list->PrimVtx(tra, uv_white, 0); + draw_list->PrimVtx(trb, uv_white, col_black); + draw_list->PrimVtx(trc, uv_white, 0); + draw_list->AddTriangle(tra, trb, trc, col_midgrey, 1.5f); + sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V)); + } + else if (flags & ImGuiColorEditFlags_PickerHueBar) + { + // Render SV Square + draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_white, hue_color32, hue_color32, col_white); + draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0, 0, col_black, col_black); + RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0.0f); + sv_cursor_pos.x = ImClamp(IM_ROUND(picker_pos.x + ImSaturate(S) * sv_picker_size), picker_pos.x + 2, picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much + sv_cursor_pos.y = ImClamp(IM_ROUND(picker_pos.y + ImSaturate(1 - V) * sv_picker_size), picker_pos.y + 2, picker_pos.y + sv_picker_size - 2); + + // Render Hue Bar + for (int i = 0; i < 6; ++i) + draw_list->AddRectFilledMultiColor(ImVec2(bar0_pos_x, picker_pos.y + i * (sv_picker_size / 6)), ImVec2(bar0_pos_x + bars_width, picker_pos.y + (i + 1) * (sv_picker_size / 6)), col_hues[i], col_hues[i], col_hues[i + 1], col_hues[i + 1]); + float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size); + RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f); + RenderArrowsForVerticalBar(draw_list, ImVec2(bar0_pos_x - 1, bar0_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f, style.Alpha); + } + + // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range) + float sv_cursor_rad = value_changed_sv ? 10.0f : 6.0f; + draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, user_col32_striped_of_alpha, 12); + draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad + 1, col_midgrey, 12); + draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, col_white, 12); + + // Render alpha bar + if (alpha_bar) + { + float alpha = ImSaturate(col[3]); + ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size); + RenderColorRectWithAlphaCheckerboard(draw_list, bar1_bb.Min, bar1_bb.Max, 0, bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f)); + draw_list->AddRectFilledMultiColor(bar1_bb.Min, bar1_bb.Max, user_col32_striped_of_alpha, user_col32_striped_of_alpha, user_col32_striped_of_alpha & ~IM_COL32_A_MASK, user_col32_striped_of_alpha & ~IM_COL32_A_MASK); + float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size); + RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f); + RenderArrowsForVerticalBar(draw_list, ImVec2(bar1_pos_x - 1, bar1_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f, style.Alpha); + } + + EndGroup(); + + if (value_changed && memcmp(backup_initial_col, col, components * sizeof(float)) == 0) + value_changed = false; + if (value_changed) + MarkItemEdited(g.LastItemData.ID); + + PopID(); + + return value_changed; +} + +// A little color square. Return true when clicked. +// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip. +// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip. +// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set. +bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, ImVec2 size) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiID id = window->GetID(desc_id); + float default_size = GetFrameHeight(); + if (size.x == 0.0f) + size.x = default_size; + if (size.y == 0.0f) + size.y = default_size; + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + + if (flags & ImGuiColorEditFlags_NoAlpha) + flags &= ~(ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf); + + ImVec4 col_rgb = col; + if (flags & ImGuiColorEditFlags_InputHSV) + ColorConvertHSVtoRGB(col_rgb.x, col_rgb.y, col_rgb.z, col_rgb.x, col_rgb.y, col_rgb.z); + + ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f); + float grid_step = ImMin(size.x, size.y) / 2.99f; + float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f); + ImRect bb_inner = bb; + float off = 0.0f; + if ((flags & ImGuiColorEditFlags_NoBorder) == 0) + { + off = -0.75f; // The border (using Col_FrameBg) tends to look off when color is near-opaque and rounding is enabled. This offset seemed like a good middle ground to reduce those artifacts. + bb_inner.Expand(off); + } + if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f) + { + float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f); + RenderColorRectWithAlphaCheckerboard(window->DrawList, ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), grid_step, ImVec2(-grid_step + off, off), rounding, ImDrawFlags_RoundCornersRight); + window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_rgb_without_alpha), rounding, ImDrawFlags_RoundCornersLeft); + } + else + { + // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha + ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaPreview) ? col_rgb : col_rgb_without_alpha; + if (col_source.w < 1.0f) + RenderColorRectWithAlphaCheckerboard(window->DrawList, bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding); + else + window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding); + } + RenderNavHighlight(bb, id); + if ((flags & ImGuiColorEditFlags_NoBorder) == 0) + { + if (g.Style.FrameBorderSize > 0.0f) + RenderFrameBorder(bb.Min, bb.Max, rounding); + else + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color button are often in need of some sort of border + } + + // Drag and Drop Source + // NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test. + if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource()) + { + if (flags & ImGuiColorEditFlags_NoAlpha) + SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col_rgb, sizeof(float) * 3, ImGuiCond_Once); + else + SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col_rgb, sizeof(float) * 4, ImGuiCond_Once); + ColorButton(desc_id, col, flags); + SameLine(); + TextEx("Color"); + EndDragDropSource(); + } + + // Tooltip + if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered) + ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)); + + return pressed; +} + +// Initialize/override default color options +void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags) +{ + ImGuiContext& g = *GImGui; + if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0) + flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_; + if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0) + flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_; + if ((flags & ImGuiColorEditFlags_PickerMask_) == 0) + flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_; + if ((flags & ImGuiColorEditFlags_InputMask_) == 0) + flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_; + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected + g.ColorEditOptions = flags; +} + +// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. +void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags) +{ + ImGuiContext& g = *GImGui; + + BeginTooltipEx(ImGuiTooltipFlags_OverridePreviousTooltip, ImGuiWindowFlags_None); + const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text; + if (text_end > text) + { + TextEx(text, text_end); + Separator(); + } + + ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2); + ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); + int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]); + ColorButton("##preview", cf, (flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, sz); + SameLine(); + if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_)) + { + if (flags & ImGuiColorEditFlags_NoAlpha) + Text("#%02X%02X%02X\nR: %d, G: %d, B: %d\n(%.3f, %.3f, %.3f)", cr, cg, cb, cr, cg, cb, col[0], col[1], col[2]); + else + Text("#%02X%02X%02X%02X\nR:%d, G:%d, B:%d, A:%d\n(%.3f, %.3f, %.3f, %.3f)", cr, cg, cb, ca, cr, cg, cb, ca, col[0], col[1], col[2], col[3]); + } + else if (flags & ImGuiColorEditFlags_InputHSV) + { + if (flags & ImGuiColorEditFlags_NoAlpha) + Text("H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]); + else + Text("H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]); + } + EndTooltip(); +} + +void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags) +{ + bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_); + bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_); + if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context")) + return; + ImGuiContext& g = *GImGui; + ImGuiColorEditFlags opts = g.ColorEditOptions; + if (allow_opt_inputs) + { + if (RadioButton("RGB", (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB; + if (RadioButton("HSV", (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV; + if (RadioButton("Hex", (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex; + } + if (allow_opt_datatype) + { + if (allow_opt_inputs) Separator(); + if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8; + if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float; + } + + if (allow_opt_inputs || allow_opt_datatype) + Separator(); + if (Button("Copy as..", ImVec2(-1, 0))) + OpenPopup("Copy"); + if (BeginPopup("Copy")) + { + int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]); + char buf[64]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); + if (Selectable(buf)) + SetClipboardText(buf); + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca); + if (Selectable(buf)) + SetClipboardText(buf); + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", cr, cg, cb); + if (Selectable(buf)) + SetClipboardText(buf); + if (!(flags & ImGuiColorEditFlags_NoAlpha)) + { + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", cr, cg, cb, ca); + if (Selectable(buf)) + SetClipboardText(buf); + } + EndPopup(); + } + + g.ColorEditOptions = opts; + EndPopup(); +} + +void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags) +{ + bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_); + bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar); + if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup("context")) + return; + ImGuiContext& g = *GImGui; + if (allow_opt_picker) + { + ImVec2 picker_size(g.FontSize * 8, ImMax(g.FontSize * 8 - (GetFrameHeight() + g.Style.ItemInnerSpacing.x), 1.0f)); // FIXME: Picker size copied from main picker function + PushItemWidth(picker_size.x); + for (int picker_type = 0; picker_type < 2; picker_type++) + { + // Draw small/thumbnail version of each picker type (over an invisible button for selection) + if (picker_type > 0) Separator(); + PushID(picker_type); + ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha); + if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar; + if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel; + ImVec2 backup_pos = GetCursorScreenPos(); + if (Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup + g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_); + SetCursorScreenPos(backup_pos); + ImVec4 previewing_ref_col; + memcpy(&previewing_ref_col, ref_col, sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4)); + ColorPicker4("##previewing_picker", &previewing_ref_col.x, picker_flags); + PopID(); + } + PopItemWidth(); + } + if (allow_opt_alpha_bar) + { + if (allow_opt_picker) Separator(); + CheckboxFlags("Alpha Bar", &g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar); + } + EndPopup(); +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: TreeNode, CollapsingHeader, etc. +//------------------------------------------------------------------------- +// - TreeNode() +// - TreeNodeV() +// - TreeNodeEx() +// - TreeNodeExV() +// - TreeNodeBehavior() [Internal] +// - TreePush() +// - TreePop() +// - GetTreeNodeToLabelSpacing() +// - SetNextItemOpen() +// - CollapsingHeader() +//------------------------------------------------------------------------- + +bool ImGui::TreeNode(const char* str_id, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(str_id, 0, fmt, args); + va_end(args); + return is_open; +} + +bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(ptr_id, 0, fmt, args); + va_end(args); + return is_open; +} + +bool ImGui::TreeNode(const char* label) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + return TreeNodeBehavior(window->GetID(label), 0, label, NULL); +} + +bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args) +{ + return TreeNodeExV(str_id, 0, fmt, args); +} + +bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args) +{ + return TreeNodeExV(ptr_id, 0, fmt, args); +} + +bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + return TreeNodeBehavior(window->GetID(label), flags, label, NULL); +} + +bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(str_id, flags, fmt, args); + va_end(args); + return is_open; +} + +bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(ptr_id, flags, fmt, args); + va_end(args); + return is_open; +} + +bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const char* label_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); + return TreeNodeBehavior(window->GetID(str_id), flags, g.TempBuffer, label_end); +} + +bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const char* label_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); + return TreeNodeBehavior(window->GetID(ptr_id), flags, g.TempBuffer, label_end); +} + +bool ImGui::TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags) +{ + if (flags & ImGuiTreeNodeFlags_Leaf) + return true; + + // We only write to the tree storage if the user clicks (or explicitly use the SetNextItemOpen function) + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStorage* storage = window->DC.StateStorage; + + bool is_open; + if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasOpen) + { + if (g.NextItemData.OpenCond & ImGuiCond_Always) + { + is_open = g.NextItemData.OpenVal; + storage->SetInt(id, is_open); + } + else + { + // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently. + const int stored_value = storage->GetInt(id, -1); + if (stored_value == -1) + { + is_open = g.NextItemData.OpenVal; + storage->SetInt(id, is_open); + } + else + { + is_open = stored_value != 0; + } + } + } + else + { + is_open = storage->GetInt(id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0; + } + + // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior). + // NB- If we are above max depth we still allow manually opened nodes to be logged. + if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand) + is_open = true; + + return is_open; +} + +bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0; + const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(window->DC.CurrLineTextBaseOffset, style.FramePadding.y)); + + if (!label_end) + label_end = FindRenderedTextEnd(label); + const ImVec2 label_size = CalcTextSize(label, label_end, false); + + // We vertically grow up to current line height up the typical widget height. + const float frame_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), label_size.y + padding.y * 2); + ImRect frame_bb; + frame_bb.Min.x = (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x; + frame_bb.Min.y = window->DC.CursorPos.y; + frame_bb.Max.x = window->WorkRect.Max.x; + frame_bb.Max.y = window->DC.CursorPos.y + frame_height; + if (display_frame) + { + // Framed header expand a little outside the default padding, to the edge of InnerClipRect + // (FIXME: May remove this at some point and make InnerClipRect align with WindowPadding.x instead of WindowPadding.x*0.5f) + frame_bb.Min.x -= IM_FLOOR(window->WindowPadding.x * 0.5f - 1.0f); + frame_bb.Max.x += IM_FLOOR(window->WindowPadding.x * 0.5f); + } + + const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapser arrow width + Spacing + const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it + const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x * 2 : 0.0f); // Include collapser + ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y); + ItemSize(ImVec2(text_width, frame_height), padding.y); + + // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing + ImRect interact_bb = frame_bb; + if (!display_frame && (flags & (ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth)) == 0) + interact_bb.Max.x = frame_bb.Min.x + text_width + style.ItemSpacing.x * 2.0f; + + // Store a flag for the current depth to tell if we will allow closing this node when navigating one of its child. + // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop(). + // This is currently only support 32 level deep and we are fine with (1 << Depth) overflowing into a zero. + const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0; + bool is_open = TreeNodeBehaviorIsOpen(id, flags); + if (is_open && !g.NavIdIsAlive && (flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + window->DC.TreeJumpToParentOnPopMask |= (1 << window->DC.TreeDepth); + + bool item_add = ItemAdd(interact_bb, id); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; + g.LastItemData.DisplayRect = frame_bb; + + if (!item_add) + { + if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + TreePushOverrideID(id); + IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0)); + return is_open; + } + + ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None; + if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) + button_flags |= ImGuiButtonFlags_AllowItemOverlap; + if (!is_leaf) + button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; + + // We allow clicking on the arrow section with keyboard modifiers held, in order to easily + // allow browsing a tree while preserving selection with code implementing multi-selection patterns. + // When clicking on the rest of the tree node we always disallow keyboard modifiers. + const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x; + const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x; + const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2); + if (window != g.HoveredWindow || !is_mouse_x_over_arrow) + button_flags |= ImGuiButtonFlags_NoKeyModifiers; + + // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags. + // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support. + // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0) + // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0) + // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1) + // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1) + // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0) + // It is rather standard that arrow click react on Down rather than Up. + // We set ImGuiButtonFlags_PressedOnClickRelease on OpenOnDoubleClick because we want the item to be active on the initial MouseDown in order for drag and drop to work. + if (is_mouse_x_over_arrow) + button_flags |= ImGuiButtonFlags_PressedOnClick; + else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) + button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + + bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0; + const bool was_selected = selected; + + bool hovered, held; + bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags); + bool toggled = false; + if (!is_leaf) + { + if (pressed && g.DragDropHoldJustPressedId != id) + { + if ((flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) == 0 || (g.NavActivateId == id)) + toggled = true; + if (flags & ImGuiTreeNodeFlags_OpenOnArrow) + toggled |= is_mouse_x_over_arrow && !g.NavDisableMouseHover; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job + if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2) + toggled = true; + } + else if (pressed && g.DragDropHoldJustPressedId == id) + { + IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold); + if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again. + toggled = true; + } + + if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open) + { + toggled = true; + NavMoveRequestCancel(); + } + if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right && !is_open) // If there's something upcoming on the line we may want to give it the priority? + { + toggled = true; + NavMoveRequestCancel(); + } + + if (toggled) + { + is_open = !is_open; + window->DC.StateStorage->SetInt(id, is_open); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen; + } + } + if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) + SetItemAllowOverlap(); + + // In this branch, TreeNodeBehavior() cannot toggle the selection so this will never trigger. + if (selected != was_selected) //-V547 + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; + + // Render + const ImU32 text_col = GetColorU32(ImGuiCol_Text); + ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_TypeThin; + if (display_frame) + { + // Framed type + const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, true, style.FrameRounding); + RenderNavHighlight(frame_bb, id, nav_highlight_flags); + if (flags & ImGuiTreeNodeFlags_Bullet) + RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), text_col); + else if (!is_leaf) + RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y), text_col, is_open ? ImGuiDir_Down : ImGuiDir_Right, 1.0f); + else // Leaf without bullet, left-adjusted text + text_pos.x -= text_offset_x; + if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton) + frame_bb.Max.x -= g.FontSize + style.FramePadding.x; + + if (g.LogEnabled) + LogSetNextTextDecoration("###", "###"); + RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); + } + else + { + // Unframed typed for tree nodes + if (hovered || selected) + { + const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false); + } + RenderNavHighlight(frame_bb, id, nav_highlight_flags); + if (flags & ImGuiTreeNodeFlags_Bullet) + RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col); + else if (!is_leaf) + RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.15f), text_col, is_open ? ImGuiDir_Down : ImGuiDir_Right, 0.70f); + if (g.LogEnabled) + LogSetNextTextDecoration(">", NULL); + RenderText(text_pos, label, label_end, false); + } + + if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + TreePushOverrideID(id); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0)); + return is_open; +} + +void ImGui::TreePush(const char* str_id) +{ + ImGuiWindow* window = GetCurrentWindow(); + Indent(); + window->DC.TreeDepth++; + PushID(str_id); +} + +void ImGui::TreePush(const void* ptr_id) +{ + ImGuiWindow* window = GetCurrentWindow(); + Indent(); + window->DC.TreeDepth++; + PushID(ptr_id); +} + +void ImGui::TreePushOverrideID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + Indent(); + window->DC.TreeDepth++; + PushOverrideID(id); +} + +void ImGui::TreePop() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + Unindent(); + + window->DC.TreeDepth--; + ImU32 tree_depth_mask = (1 << window->DC.TreeDepth); + + // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled) + if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) + if (g.NavIdIsAlive && (window->DC.TreeJumpToParentOnPopMask & tree_depth_mask)) + { + SetNavID(window->IDStack.back(), g.NavLayer, 0, ImRect()); + NavMoveRequestCancel(); + } + window->DC.TreeJumpToParentOnPopMask &= tree_depth_mask - 1; + + IM_ASSERT(window->IDStack.Size > 1); // There should always be 1 element in the IDStack (pushed during window creation). If this triggers you called TreePop/PopID too much. + PopID(); +} + +// Horizontal distance preceding label when using TreeNode() or Bullet() +float ImGui::GetTreeNodeToLabelSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + (g.Style.FramePadding.x * 2.0f); +} + +// Set next TreeNode/CollapsingHeader open state. +void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + if (g.CurrentWindow->SkipItems) + return; + g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasOpen; + g.NextItemData.OpenVal = is_open; + g.NextItemData.OpenCond = cond ? cond : ImGuiCond_Always; +} + +// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag). +// This is basically the same as calling TreeNodeEx(label, ImGuiTreeNodeFlags_CollapsingHeader). You can remove the _NoTreePushOnOpen flag if you want behavior closer to normal TreeNode(). +bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + return TreeNodeBehavior(window->GetID(label), flags | ImGuiTreeNodeFlags_CollapsingHeader, label); +} + +// p_visible == NULL : regular collapsing header +// p_visible != NULL && *p_visible == true : show a small close button on the corner of the header, clicking the button will set *p_visible = false +// p_visible != NULL && *p_visible == false : do not show the header at all +// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen. +bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + if (p_visible && !*p_visible) + return false; + + ImGuiID id = window->GetID(label); + flags |= ImGuiTreeNodeFlags_CollapsingHeader; + if (p_visible) + flags |= ImGuiTreeNodeFlags_AllowItemOverlap | ImGuiTreeNodeFlags_ClipLabelForTrailingButton; + bool is_open = TreeNodeBehavior(id, flags, label); + if (p_visible != NULL) + { + // Create a small overlapping close button + // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc. + // FIXME: CloseButton can overlap into text, need find a way to clip the text somehow. + ImGuiContext& g = *GImGui; + ImGuiLastItemData last_item_backup = g.LastItemData; + float button_size = g.FontSize; + float button_x = ImMax(g.LastItemData.Rect.Min.x, g.LastItemData.Rect.Max.x - g.Style.FramePadding.x * 2.0f - button_size); + float button_y = g.LastItemData.Rect.Min.y; + ImGuiID close_button_id = GetIDWithSeed("#CLOSE", NULL, id); + if (CloseButton(close_button_id, ImVec2(button_x, button_y))) + *p_visible = false; + g.LastItemData = last_item_backup; + } + + return is_open; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Selectable +//------------------------------------------------------------------------- +// - Selectable() +//------------------------------------------------------------------------- + +// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image. +// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id. +// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowItemOverlap are also frequently used flags. +// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported. +bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle. + ImGuiID id = window->GetID(label); + ImVec2 label_size = CalcTextSize(label, NULL, true); + ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y); + ImVec2 pos = window->DC.CursorPos; + pos.y += window->DC.CurrLineTextBaseOffset; + ItemSize(size, 0.0f); + + // Fill horizontal space + // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets. + const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0; + const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x; + const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x; + if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth)) + size.x = ImMax(label_size.x, max_x - min_x); + + // Text stays at the submission position, but bounding box may be extended on both sides + const ImVec2 text_min = pos; + const ImVec2 text_max(min_x + size.x, pos.y + size.y); + + // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable. + ImRect bb(min_x, pos.y, text_max.x, text_max.y); + if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0) + { + const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x; + const float spacing_y = style.ItemSpacing.y; + const float spacing_L = IM_FLOOR(spacing_x * 0.50f); + const float spacing_U = IM_FLOOR(spacing_y * 0.50f); + bb.Min.x -= spacing_L; + bb.Min.y -= spacing_U; + bb.Max.x += (spacing_x - spacing_L); + bb.Max.y += (spacing_y - spacing_U); + } + //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); } + + // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackground for every Selectable.. + const float backup_clip_rect_min_x = window->ClipRect.Min.x; + const float backup_clip_rect_max_x = window->ClipRect.Max.x; + if (span_all_columns) + { + window->ClipRect.Min.x = window->ParentWorkRect.Min.x; + window->ClipRect.Max.x = window->ParentWorkRect.Max.x; + } + + const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0; + const bool item_add = ItemAdd(bb, id, NULL, disabled_item ? ImGuiItemFlags_Disabled : ImGuiItemFlags_None); + if (span_all_columns) + { + window->ClipRect.Min.x = backup_clip_rect_min_x; + window->ClipRect.Max.x = backup_clip_rect_max_x; + } + + if (!item_add) + return false; + + const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; + if (disabled_item && !disabled_global) // Only testing this as an optimization + BeginDisabled(); + + // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only, + // which would be advantageous since most selectable are not selected. + if (span_all_columns && window->DC.CurrentColumns) + PushColumnsBackground(); + else if (span_all_columns && g.CurrentTable) + TablePushBackgroundChannel(); + + // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries + ImGuiButtonFlags button_flags = 0; + if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; } + if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; } + if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; } + if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; } + if (flags & ImGuiSelectableFlags_AllowItemOverlap) { button_flags |= ImGuiButtonFlags_AllowItemOverlap; } + + const bool was_selected = selected; + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); + + // Auto-select when moved into + // - This will be more fully fleshed in the range-select branch + // - This is not exposed as it won't nicely work with some user side handling of shift/control + // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons + // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope()) + // - (2) usage will fail with clipped items + // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API. + if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == window->DC.NavFocusScopeIdCurrent) + if (g.NavJustMovedToId == id) + selected = pressed = true; + + // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with gamepad/keyboard + if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover))) + { + if (!g.NavDisableMouseHover && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent) + { + SetNavID(id, window->DC.NavLayerCurrent, window->DC.NavFocusScopeIdCurrent, WindowRectAbsToRel(window, bb)); // (bb == NavRect) + g.NavDisableHighlight = true; + } + } + if (pressed) + MarkItemEdited(id); + + if (flags & ImGuiSelectableFlags_AllowItemOverlap) + SetItemAllowOverlap(); + + // In this branch, Selectable() cannot toggle the selection so this will never trigger. + if (selected != was_selected) //-V547 + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; + + // Render + if (held && (flags & ImGuiSelectableFlags_DrawHoveredWhenHeld)) + hovered = true; + if (hovered || selected) + { + const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(bb.Min, bb.Max, col, false, 0.0f); + } + RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding); + + if (span_all_columns && window->DC.CurrentColumns) + PopColumnsBackground(); + else if (span_all_columns && g.CurrentTable) + TablePopBackgroundChannel(); + + RenderTextClipped(text_min, text_max, label, NULL, &label_size, style.SelectableTextAlign, &bb); + + // Automatically close popups + if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_DontClosePopups) && !(g.LastItemData.InFlags & ImGuiItemFlags_SelectableDontClosePopup)) + CloseCurrentPopup(); + + if (disabled_item && !disabled_global) + EndDisabled(); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; //-V1020 +} + +bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) +{ + if (Selectable(label, *p_selected, flags, size_arg)) + { + *p_selected = !*p_selected; + return true; + } + return false; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: ListBox +//------------------------------------------------------------------------- +// - BeginListBox() +// - EndListBox() +// - ListBox() +//------------------------------------------------------------------------- + +// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty" +// Tip: If your vertical size is calculated from an item count (e.g. 10 * item_height) consider adding a fractional part to facilitate seeing scrolling boundaries (e.g. 10.25 * item_height). +bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + // Size default to hold ~7.25 items. + // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar. + ImVec2 size = ImFloor(CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f)); + ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y)); + ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); + ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + g.NextItemData.ClearFlags(); + + if (!IsRectVisible(bb.Min, bb.Max)) + { + ItemSize(bb.GetSize(), style.FramePadding.y); + ItemAdd(bb, 0, &frame_bb); + return false; + } + + // FIXME-OPT: We could omit the BeginGroup() if label_size.x but would need to omit the EndGroup() as well. + BeginGroup(); + if (label_size.x > 0.0f) + { + ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y); + RenderText(label_pos, label); + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size); + } + + BeginChildFrame(id, frame_bb.GetSize()); + return true; +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +// OBSOLETED in 1.81 (from February 2021) +bool ImGui::ListBoxHeader(const char* label, int items_count, int height_in_items) +{ + // If height_in_items == -1, default height is maximum 7. + ImGuiContext& g = *GImGui; + float height_in_items_f = (height_in_items < 0 ? ImMin(items_count, 7) : height_in_items) + 0.25f; + ImVec2 size; + size.x = 0.0f; + size.y = GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f; + return BeginListBox(label, size); +} +#endif + +void ImGui::EndListBox() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?"); + IM_UNUSED(window); + + EndChildFrame(); + EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label +} + +bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items) +{ + const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items); + return value_changed; +} + +// This is merely a helper around BeginListBox(), EndListBox(). +// Considering using those directly to submit custom data or store selection differently. +bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int height_in_items) +{ + ImGuiContext& g = *GImGui; + + // Calculate size from "height_in_items" + if (height_in_items < 0) + height_in_items = ImMin(items_count, 7); + float height_in_items_f = height_in_items + 0.25f; + ImVec2 size(0.0f, ImFloor(GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f)); + + if (!BeginListBox(label, size)) + return false; + + // Assume all items have even height (= 1 line of text). If you need items of different height, + // you can create a custom version of ListBox() in your code without using the clipper. + bool value_changed = false; + ImGuiListClipper clipper; + clipper.Begin(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to. + while (clipper.Step()) + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + const char* item_text; + if (!items_getter(data, i, &item_text)) + item_text = "*Unknown item*"; + + PushID(i); + const bool item_selected = (i == *current_item); + if (Selectable(item_text, item_selected)) + { + *current_item = i; + value_changed = true; + } + if (item_selected) + SetItemDefaultFocus(); + PopID(); + } + EndListBox(); + + if (value_changed) + MarkItemEdited(g.LastItemData.ID); + + return value_changed; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: PlotLines, PlotHistogram +//------------------------------------------------------------------------- +// - PlotEx() [Internal] +// - PlotLines() +// - PlotHistogram() +//------------------------------------------------------------------------- +// Plot/Graph widgets are not very good. +// Consider writing your own, or using a third-party one, see: +// - ImPlot https://github.com/epezent/implot +// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions +//------------------------------------------------------------------------- + +int ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 frame_size) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return -1; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + if (frame_size.x == 0.0f) + frame_size.x = CalcItemWidth(); + if (frame_size.y == 0.0f) + frame_size.y = label_size.y + (style.FramePadding.y * 2); + + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); + const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding); + const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0)); + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, 0, &frame_bb)) + return -1; + const bool hovered = ItemHoverable(frame_bb, id); + + // Determine scale from values if not specified + if (scale_min == FLT_MAX || scale_max == FLT_MAX) + { + float v_min = FLT_MAX; + float v_max = -FLT_MAX; + for (int i = 0; i < values_count; i++) + { + const float v = values_getter(data, i); + if (v != v) // Ignore NaN values + continue; + v_min = ImMin(v_min, v); + v_max = ImMax(v_max, v); + } + if (scale_min == FLT_MAX) + scale_min = v_min; + if (scale_max == FLT_MAX) + scale_max = v_max; + } + + RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + + const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1; + int idx_hovered = -1; + if (values_count >= values_count_min) + { + int res_w = ImMin((int)frame_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); + int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); + + // Tooltip on hover + if (hovered && inner_bb.Contains(g.IO.MousePos)) + { + const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f); + const int v_idx = (int)(t * item_count); + IM_ASSERT(v_idx >= 0 && v_idx < values_count); + + const float v0 = values_getter(data, (v_idx + values_offset) % values_count); + const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count); + if (plot_type == ImGuiPlotType_Lines) + SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1); + else if (plot_type == ImGuiPlotType_Histogram) + SetTooltip("%d: %8.4g", v_idx, v0); + idx_hovered = v_idx; + } + + const float t_step = 1.0f / (float)res_w; + const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min)); + + float v0 = values_getter(data, (0 + values_offset) % values_count); + float t0 = 0.0f; + ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle + float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (1 + scale_min * inv_scale) : (scale_min < 0.0f ? 0.0f : 1.0f); // Where does the zero line stands + + const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram); + const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered); + + for (int n = 0; n < res_w; n++) + { + const float t1 = t0 + t_step; + const int v1_idx = (int)(t0 * item_count + 0.5f); + IM_ASSERT(v1_idx >= 0 && v1_idx < values_count); + const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count); + const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale) ); + + // NB: Draw calls are merged together by the DrawList system. Still, we should render our batch are lower level to save a bit of CPU. + ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0); + ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t)); + if (plot_type == ImGuiPlotType_Lines) + { + window->DrawList->AddLine(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base); + } + else if (plot_type == ImGuiPlotType_Histogram) + { + if (pos1.x >= pos0.x + 2.0f) + pos1.x -= 1.0f; + window->DrawList->AddRectFilled(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base); + } + + t0 = t1; + tp0 = tp1; + } + } + + // Text overlay + if (overlay_text) + RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, overlay_text, NULL, NULL, ImVec2(0.5f, 0.0f)); + + if (label_size.x > 0.0f) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label); + + // Return hovered index or -1 if none are hovered. + // This is currently not exposed in the public API because we need a larger redesign of the whole thing, but in the short-term we are making it available in PlotEx(). + return idx_hovered; +} + +struct ImGuiPlotArrayGetterData +{ + const float* Values; + int Stride; + + ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; } +}; + +static float Plot_ArrayGetter(void* data, int idx) +{ + ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data; + const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride); + return v; +} + +void ImGui::PlotLines(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) +{ + ImGuiPlotArrayGetterData data(values, stride); + PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +void ImGui::PlotLines(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size) +{ + PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +void ImGui::PlotHistogram(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) +{ + ImGuiPlotArrayGetterData data(values, stride); + PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +void ImGui::PlotHistogram(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size) +{ + PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Value helpers +// Those is not very useful, legacy API. +//------------------------------------------------------------------------- +// - Value() +//------------------------------------------------------------------------- + +void ImGui::Value(const char* prefix, bool b) +{ + Text("%s: %s", prefix, (b ? "true" : "false")); +} + +void ImGui::Value(const char* prefix, int v) +{ + Text("%s: %d", prefix, v); +} + +void ImGui::Value(const char* prefix, unsigned int v) +{ + Text("%s: %d", prefix, v); +} + +void ImGui::Value(const char* prefix, float v, const char* float_format) +{ + if (float_format) + { + char fmt[64]; + ImFormatString(fmt, IM_ARRAYSIZE(fmt), "%%s: %s", float_format); + Text(fmt, prefix, v); + } + else + { + Text("%s: %.3f", prefix, v); + } +} + +//------------------------------------------------------------------------- +// [SECTION] MenuItem, BeginMenu, EndMenu, etc. +//------------------------------------------------------------------------- +// - ImGuiMenuColumns [Internal] +// - BeginMenuBar() +// - EndMenuBar() +// - BeginMainMenuBar() +// - EndMainMenuBar() +// - BeginMenu() +// - EndMenu() +// - MenuItemEx() [Internal] +// - MenuItem() +//------------------------------------------------------------------------- + +// Helpers for internal use +void ImGuiMenuColumns::Update(float spacing, bool window_reappearing) +{ + if (window_reappearing) + memset(Widths, 0, sizeof(Widths)); + Spacing = (ImU16)spacing; + CalcNextTotalWidth(true); + memset(Widths, 0, sizeof(Widths)); + TotalWidth = NextTotalWidth; + NextTotalWidth = 0; +} + +void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets) +{ + ImU16 offset = 0; + bool want_spacing = false; + for (int i = 0; i < IM_ARRAYSIZE(Widths); i++) + { + ImU16 width = Widths[i]; + if (want_spacing && width > 0) + offset += Spacing; + want_spacing |= (width > 0); + if (update_offsets) + { + if (i == 1) { OffsetLabel = offset; } + if (i == 2) { OffsetShortcut = offset; } + if (i == 3) { OffsetMark = offset; } + } + offset += width; + } + NextTotalWidth = offset; +} + +float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark) +{ + Widths[0] = ImMax(Widths[0], (ImU16)w_icon); + Widths[1] = ImMax(Widths[1], (ImU16)w_label); + Widths[2] = ImMax(Widths[2], (ImU16)w_shortcut); + Widths[3] = ImMax(Widths[3], (ImU16)w_mark); + CalcNextTotalWidth(false); + return (float)ImMax(TotalWidth, NextTotalWidth); +} + +// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere.. +// Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer. +// Ideally we also want this to be responsible for claiming space out of the main window scrolling rectangle, in which case ImGuiWindowFlags_MenuBar will become unnecessary. +// Then later the same system could be used for multiple menu-bars, scrollbars, side-bars. +bool ImGui::BeginMenuBar() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + if (!(window->Flags & ImGuiWindowFlags_MenuBar)) + return false; + + IM_ASSERT(!window->DC.MenuBarAppending); + BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore + PushID("##menubar"); + + // We don't clip with current window clipping rectangle as it is already set to the area below. However we clip with window full rect. + // We remove 1 worth of rounding to Max.x to that text in long menus and small windows don't tend to display over the lower-right rounded area, which looks particularly glitchy. + ImRect bar_rect = window->MenuBarRect(); + ImRect clip_rect(IM_ROUND(bar_rect.Min.x + window->WindowBorderSize), IM_ROUND(bar_rect.Min.y + window->WindowBorderSize), IM_ROUND(ImMax(bar_rect.Min.x, bar_rect.Max.x - ImMax(window->WindowRounding, window->WindowBorderSize))), IM_ROUND(bar_rect.Max.y)); + clip_rect.ClipWith(window->OuterRectClipped); + PushClipRect(clip_rect.Min, clip_rect.Max, false); + + // We overwrite CursorMaxPos because BeginGroup sets it to CursorPos (essentially the .EmitItem hack in EndMenuBar() would need something analogous here, maybe a BeginGroupEx() with flags). + window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y); + window->DC.LayoutType = ImGuiLayoutType_Horizontal; + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + window->DC.MenuBarAppending = true; + AlignTextToFramePadding(); + return true; +} + +void ImGui::EndMenuBar() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + ImGuiContext& g = *GImGui; + + // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings. + if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) + { + // Try to find out if the request is for one of our child menu + ImGuiWindow* nav_earliest_child = g.NavWindow; + while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu)) + nav_earliest_child = nav_earliest_child->ParentWindow; + if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0) + { + // To do so we claim focus back, restore NavId and then process the movement request for yet another frame. + // This involve a one-frame delay which isn't very problematic in this situation. We could remove it by scoring in advance for multiple window (probably not worth bothering) + const ImGuiNavLayer layer = ImGuiNavLayer_Menu; + IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check + FocusWindow(window); + SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]); + g.NavDisableHighlight = true; // Hide highlight for the current frame so we don't see the intermediary selection. + g.NavDisableMouseHover = g.NavMousePosDirty = true; + NavMoveRequestForward(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); // Repeat + } + } + + IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'" + IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar); + IM_ASSERT(window->DC.MenuBarAppending); + PopClipRect(); + PopID(); + window->DC.MenuBarOffset.x = window->DC.CursorPos.x - window->Pos.x; // Save horizontal position so next append can reuse it. This is kinda equivalent to a per-layer CursorPos. + g.GroupStack.back().EmitItem = false; + EndGroup(); // Restore position on layer 0 + window->DC.LayoutType = ImGuiLayoutType_Vertical; + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + window->DC.MenuBarAppending = false; +} + +// Important: calling order matters! +// FIXME: Somehow overlapping with docking tech. +// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts) +bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags) +{ + IM_ASSERT(dir != ImGuiDir_None); + + ImGuiWindow* bar_window = FindWindowByName(name); + if (bar_window == NULL || bar_window->BeginCount == 0) + { + // Calculate and set window size/position + ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport()); + ImRect avail_rect = viewport->GetBuildWorkRect(); + ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X; + ImVec2 pos = avail_rect.Min; + if (dir == ImGuiDir_Right || dir == ImGuiDir_Down) + pos[axis] = avail_rect.Max[axis] - axis_size; + ImVec2 size = avail_rect.GetSize(); + size[axis] = axis_size; + SetNextWindowPos(pos); + SetNextWindowSize(size); + + // Report our size into work area (for next frame) using actual window size + if (dir == ImGuiDir_Up || dir == ImGuiDir_Left) + viewport->BuildWorkOffsetMin[axis] += axis_size; + else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right) + viewport->BuildWorkOffsetMax[axis] -= axis_size; + } + + window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; + PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); // Lift normal size constraint + bool is_open = Begin(name, NULL, window_flags); + PopStyleVar(2); + + return is_open; +} + +bool ImGui::BeginMainMenuBar() +{ + ImGuiContext& g = *GImGui; + ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport(); + + // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set. + // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea? + // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings. + g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, 0.0f)); + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar; + float height = GetFrameHeight(); + bool is_open = BeginViewportSideBar("##MainMenuBar", viewport, ImGuiDir_Up, height, window_flags); + g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f); + + if (is_open) + BeginMenuBar(); + else + End(); + return is_open; +} + +void ImGui::EndMainMenuBar() +{ + EndMenuBar(); + + // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window + // FIXME: With this strategy we won't be able to restore a NULL focus. + ImGuiContext& g = *GImGui; + if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest) + FocusTopMostWindowUnderOne(g.NavWindow, NULL); + + End(); +} + +static bool IsRootOfOpenMenuSet() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu)) + return false; + + // Initially we used 'OpenParentId' to differentiate multiple menu sets from each others (e.g. inside menu bar vs loose menu items) based on parent ID. + // This would however prevent the use of e.g. PuhsID() user code submitting menus. + // Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag, + // making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects. + // Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup + // doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first chilld menu. + const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size]; + return (/*upper_popup->OpenParentId == window->IDStack.back() &&*/ upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu)); +} + +bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None); + + // Sub-menus are ChildWindow so that mouse can be hovering across them (otherwise top-most popup menu would steal focus and not allow hovering on parent menu) + // The first menu in a hierarchy isn't so hovering doesn't get accross (otherwise e.g. resizing borders with ImGuiButtonFlags_FlattenChildren would react), but top-most BeginMenu() will bypass that limitation. + ImGuiWindowFlags flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus; + if (window->Flags & ImGuiWindowFlags_ChildMenu) + flags |= ImGuiWindowFlags_ChildWindow; + + // If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin(). + // We are relying on a O(N) search - so O(N log N) over the frame - which seems like the most efficient for the expected small amount of BeginMenu() calls per frame. + // If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used. + if (g.MenusIdSubmittedThisFrame.contains(id)) + { + if (menu_is_open) + menu_is_open = BeginPopupEx(id, flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display) + else + g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values + return menu_is_open; + } + + // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu + g.MenusIdSubmittedThisFrame.push_back(id); + + ImVec2 label_size = CalcTextSize(label, NULL, true); + + // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent without always being a Child window) + const bool menuset_is_open = IsRootOfOpenMenuSet(); + ImGuiWindow* backed_nav_window = g.NavWindow; + if (menuset_is_open) + g.NavWindow = window; + + // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu, + // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup(). + // e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering. + ImVec2 popup_pos, pos = window->DC.CursorPos; + PushID(label); + if (!enabled) + BeginDisabled(); + const ImGuiMenuColumns* offsets = &window->DC.MenuColumns; + bool pressed; + const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_DontClosePopups; + if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) + { + // Menu inside an horizontal menu bar + // Selectable extend their highlight by half ItemSpacing in each direction. + // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin() + popup_pos = ImVec2(pos.x - 1.0f - IM_FLOOR(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight()); + window->DC.CursorPos.x += IM_FLOOR(style.ItemSpacing.x * 0.5f); + PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y)); + float w = label_size.x; + ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, 0.0f)); + RenderText(text_pos, label); + PopStyleVar(); + window->DC.CursorPos.x += IM_FLOOR(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). + } + else + { + // Menu inside a regular/vertical menu + // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f. + // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system. + popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y); + float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f; + float checkmark_w = IM_FLOOR(g.FontSize * 1.20f); + float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, 0.0f, checkmark_w); // Feedback to next frame + float extra_w = ImMax(0.0f, GetContentRegionAvail().x - min_w); + ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, 0.0f)); + RenderText(text_pos, label); + if (icon_w > 0.0f) + RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon); + RenderArrow(window->DrawList, pos + ImVec2(offsets->OffsetMark + extra_w + g.FontSize * 0.30f, 0.0f), GetColorU32(ImGuiCol_Text), ImGuiDir_Right); + } + if (!enabled) + EndDisabled(); + + const bool hovered = (g.HoveredId == id) && enabled; + if (menuset_is_open) + g.NavWindow = backed_nav_window; + + bool want_open = false; + bool want_close = false; + if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu)) + { + // Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu + // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive. + bool moving_toward_other_child_menu = false; + ImGuiWindow* child_menu_window = (g.BeginPopupStack.Size < g.OpenPopupStack.Size && g.OpenPopupStack[g.BeginPopupStack.Size].SourceWindow == window) ? g.OpenPopupStack[g.BeginPopupStack.Size].Window : NULL; + if (g.HoveredWindow == window && child_menu_window != NULL && !(window->Flags & ImGuiWindowFlags_MenuBar)) + { + float ref_unit = g.FontSize; // FIXME-DPI + ImRect next_window_rect = child_menu_window->Rect(); + ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta); + ImVec2 tb = (window->Pos.x < child_menu_window->Pos.x) ? next_window_rect.GetTL() : next_window_rect.GetTR(); + ImVec2 tc = (window->Pos.x < child_menu_window->Pos.x) ? next_window_rect.GetBL() : next_window_rect.GetBR(); + float extra = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, ref_unit * 0.5f, ref_unit * 2.5f); // add a bit of extra slack. + ta.x += (window->Pos.x < child_menu_window->Pos.x) ? -0.5f : +0.5f; // to avoid numerical issues (FIXME: ??) + tb.y = ta.y + ImMax((tb.y - extra) - ta.y, -ref_unit * 8.0f); // triangle is maximum 200 high to limit the slope and the bias toward large sub-menus // FIXME: Multiply by fb_scale? + tc.y = ta.y + ImMin((tc.y + extra) - ta.y, +ref_unit * 8.0f); + moving_toward_other_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos); + //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_other_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG] + } + if (menu_is_open && !hovered && g.HoveredWindow == window && g.HoveredIdPreviousFrame != 0 && g.HoveredIdPreviousFrame != id && !moving_toward_other_child_menu) + want_close = true; + + // Open + if (!menu_is_open && pressed) // Click/activate to open + want_open = true; + else if (!menu_is_open && hovered && !moving_toward_other_child_menu) // Hover to open + want_open = true; + if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open + { + want_open = true; + NavMoveRequestCancel(); + } + } + else + { + // Menu bar + if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it + { + want_close = true; + want_open = menu_is_open = false; + } + else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others + { + want_open = true; + } + else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open + { + want_open = true; + NavMoveRequestCancel(); + } + } + + if (!enabled) // explicitly close if an open menu becomes disabled, facilitate users code a lot in pattern such as 'if (BeginMenu("options", has_object)) { ..use object.. }' + want_close = true; + if (want_close && IsPopupOpen(id, ImGuiPopupFlags_None)) + ClosePopupToLevel(g.BeginPopupStack.Size, true); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0)); + PopID(); + + if (!menu_is_open && want_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size) + { + // Don't recycle same menu level in the same frame, first close the other menu and yield for a frame. + OpenPopup(label); + return false; + } + + menu_is_open |= want_open; + if (want_open) + OpenPopup(label); + + if (menu_is_open) + { + SetNextWindowPos(popup_pos, ImGuiCond_Always); // Note: this is super misleading! The value will serve as reference for FindBestWindowPosForPopup(), not actual pos. + PushStyleVar(ImGuiStyleVar_ChildRounding, style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding + menu_is_open = BeginPopupEx(id, flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display) + PopStyleVar(); + } + else + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + } + + return menu_is_open; +} + +bool ImGui::BeginMenu(const char* label, bool enabled) +{ + return BeginMenuEx(label, NULL, enabled); +} + +void ImGui::EndMenu() +{ + // Nav: When a left move request _within our child menu_ failed, close ourselves (the _parent_ menu). + // A menu doesn't close itself because EndMenuBar() wants the catch the last Left<>Right inputs. + // However, it means that with the current code, a BeginMenu() from outside another menu or a menu-bar won't be closable with the Left direction. + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet() && window->DC.LayoutType == ImGuiLayoutType_Vertical) + if (g.NavWindow && (g.NavWindow->RootWindowForNav->Flags & ImGuiWindowFlags_Popup) && g.NavWindow->RootWindowForNav->ParentWindow == window) + { + ClosePopupToLevel(g.BeginPopupStack.Size, true); + NavMoveRequestCancel(); + } + + EndPopup(); +} + +bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + ImVec2 pos = window->DC.CursorPos; + ImVec2 label_size = CalcTextSize(label, NULL, true); + + const bool menuset_is_open = IsRootOfOpenMenuSet(); + ImGuiWindow* backed_nav_window = g.NavWindow; + if (menuset_is_open) + g.NavWindow = window; + + // We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73), + // but I am unsure whether this should be kept at all. For now moved it to be an opt-in feature used by menus only. + bool pressed; + PushID(label); + if (!enabled) + BeginDisabled(); + + const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_SetNavIdOnHover; + const ImGuiMenuColumns* offsets = &window->DC.MenuColumns; + if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) + { + // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful + // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark. + float w = label_size.x; + window->DC.CursorPos.x += IM_FLOOR(style.ItemSpacing.x * 0.5f); + ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y)); + pressed = Selectable("", selected, selectable_flags, ImVec2(w, 0.0f)); + PopStyleVar(); + RenderText(text_pos, label); + window->DC.CursorPos.x += IM_FLOOR(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). + } + else + { + // Menu item inside a vertical menu + // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f. + // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system. + float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f; + float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f; + float checkmark_w = IM_FLOOR(g.FontSize * 1.20f); + float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, shortcut_w, checkmark_w); // Feedback for next frame + float stretch_w = ImMax(0.0f, GetContentRegionAvail().x - min_w); + pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, 0.0f)); + RenderText(pos + ImVec2(offsets->OffsetLabel, 0.0f), label); + if (icon_w > 0.0f) + RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon); + if (shortcut_w > 0.0f) + { + PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]); + RenderText(pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), shortcut, NULL, false); + PopStyleColor(); + } + if (selected) + RenderCheckMark(window->DrawList, pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f); + } + IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0)); + if (!enabled) + EndDisabled(); + PopID(); + if (menuset_is_open) + g.NavWindow = backed_nav_window; + + return pressed; +} + +bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled) +{ + return MenuItemEx(label, NULL, shortcut, selected, enabled); +} + +bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled) +{ + if (MenuItemEx(label, NULL, shortcut, p_selected ? *p_selected : false, enabled)) + { + if (p_selected) + *p_selected = !*p_selected; + return true; + } + return false; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: BeginTabBar, EndTabBar, etc. +//------------------------------------------------------------------------- +// - BeginTabBar() +// - BeginTabBarEx() [Internal] +// - EndTabBar() +// - TabBarLayout() [Internal] +// - TabBarCalcTabID() [Internal] +// - TabBarCalcMaxTabWidth() [Internal] +// - TabBarFindTabById() [Internal] +// - TabBarRemoveTab() [Internal] +// - TabBarCloseTab() [Internal] +// - TabBarScrollClamp() [Internal] +// - TabBarScrollToTab() [Internal] +// - TabBarQueueChangeTabOrder() [Internal] +// - TabBarScrollingButtons() [Internal] +// - TabBarTabListPopupButton() [Internal] +//------------------------------------------------------------------------- + +struct ImGuiTabBarSection +{ + int TabCount; // Number of tabs in this section. + float Width; // Sum of width of tabs in this section (after shrinking down) + float Spacing; // Horizontal spacing at the end of the section. + + ImGuiTabBarSection() { memset(this, 0, sizeof(*this)); } +}; + +namespace ImGui +{ + static void TabBarLayout(ImGuiTabBar* tab_bar); + static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label); + static float TabBarCalcMaxTabWidth(); + static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling); + static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections); + static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar); + static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar); +} + +ImGuiTabBar::ImGuiTabBar() +{ + memset(this, 0, sizeof(*this)); + CurrFrameVisible = PrevFrameVisible = -1; + LastTabItemIdx = -1; +} + +static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab) +{ + return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1; +} + +static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs) +{ + const ImGuiTabItem* a = (const ImGuiTabItem*)lhs; + const ImGuiTabItem* b = (const ImGuiTabItem*)rhs; + const int a_section = TabItemGetSectionIdx(a); + const int b_section = TabItemGetSectionIdx(b); + if (a_section != b_section) + return a_section - b_section; + return (int)(a->IndexDuringLayout - b->IndexDuringLayout); +} + +static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs) +{ + const ImGuiTabItem* a = (const ImGuiTabItem*)lhs; + const ImGuiTabItem* b = (const ImGuiTabItem*)rhs; + return (int)(a->BeginOrder - b->BeginOrder); +} + +static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref) +{ + ImGuiContext& g = *GImGui; + return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(ref.Index); +} + +static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar) +{ + ImGuiContext& g = *GImGui; + if (g.TabBars.Contains(tab_bar)) + return ImGuiPtrOrIndex(g.TabBars.GetIndex(tab_bar)); + return ImGuiPtrOrIndex(tab_bar); +} + +bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + ImGuiID id = window->GetID(str_id); + ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(id); + ImRect tab_bar_bb = ImRect(window->DC.CursorPos.x, window->DC.CursorPos.y, window->WorkRect.Max.x, window->DC.CursorPos.y + g.FontSize + g.Style.FramePadding.y * 2); + tab_bar->ID = id; + return BeginTabBarEx(tab_bar, tab_bar_bb, flags | ImGuiTabBarFlags_IsFocused); +} + +bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + if ((flags & ImGuiTabBarFlags_DockNode) == 0) + PushOverrideID(tab_bar->ID); + + // Add to stack + g.CurrentTabBarStack.push_back(GetTabBarRefFromTabBar(tab_bar)); + g.CurrentTabBar = tab_bar; + + // Append with multiple BeginTabBar()/EndTabBar() pairs. + tab_bar->BackupCursorPos = window->DC.CursorPos; + if (tab_bar->CurrFrameVisible == g.FrameCount) + { + window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY); + tab_bar->BeginCount++; + return true; + } + + // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable + if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable))) + ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByBeginOrder); + tab_bar->TabsAddedNew = false; + + // Flags + if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0) + flags |= ImGuiTabBarFlags_FittingPolicyDefault_; + + tab_bar->Flags = flags; + tab_bar->BarRect = tab_bar_bb; + tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab() + tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible; + tab_bar->CurrFrameVisible = g.FrameCount; + tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight; + tab_bar->CurrTabsContentsHeight = 0.0f; + tab_bar->ItemSpacingY = g.Style.ItemSpacing.y; + tab_bar->FramePadding = g.Style.FramePadding; + tab_bar->TabsActiveCount = 0; + tab_bar->BeginCount = 1; + + // Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap + window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY); + + // Draw separator + const ImU32 col = GetColorU32((flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabActive : ImGuiCol_TabUnfocusedActive); + const float y = tab_bar->BarRect.Max.y - 1.0f; + { + const float separator_min_x = tab_bar->BarRect.Min.x - IM_FLOOR(window->WindowPadding.x * 0.5f); + const float separator_max_x = tab_bar->BarRect.Max.x + IM_FLOOR(window->WindowPadding.x * 0.5f); + window->DrawList->AddLine(ImVec2(separator_min_x, y), ImVec2(separator_max_x, y), col, 1.0f); + } + return true; +} + +void ImGui::EndTabBar() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + ImGuiTabBar* tab_bar = g.CurrentTabBar; + if (tab_bar == NULL) + { + IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!"); + return; + } + + // Fallback in case no TabItem have been submitted + if (tab_bar->WantLayout) + TabBarLayout(tab_bar); + + // Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed(). + const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount); + if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing) + { + tab_bar->CurrTabsContentsHeight = ImMax(window->DC.CursorPos.y - tab_bar->BarRect.Max.y, tab_bar->CurrTabsContentsHeight); + window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight; + } + else + { + window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight; + } + if (tab_bar->BeginCount > 1) + window->DC.CursorPos = tab_bar->BackupCursorPos; + + if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0) + PopID(); + + g.CurrentTabBarStack.pop_back(); + g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(g.CurrentTabBarStack.back()); +} + +// This is called only once a frame before by the first call to ItemTab() +// The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions. +static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) +{ + ImGuiContext& g = *GImGui; + tab_bar->WantLayout = false; + + // Garbage collect by compacting list + // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section) + int tab_dst_n = 0; + bool need_sort_by_section = false; + ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing + for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n]; + if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose) + { + // Remove tab + if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; } + if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; } + if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; } + continue; + } + if (tab_dst_n != tab_src_n) + tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n]; + + tab = &tab_bar->Tabs[tab_dst_n]; + tab->IndexDuringLayout = (ImS16)tab_dst_n; + + // We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another) + int curr_tab_section_n = TabItemGetSectionIdx(tab); + if (tab_dst_n > 0) + { + ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1]; + int prev_tab_section_n = TabItemGetSectionIdx(prev_tab); + if (curr_tab_section_n == 0 && prev_tab_section_n != 0) + need_sort_by_section = true; + if (prev_tab_section_n == 2 && curr_tab_section_n != 2) + need_sort_by_section = true; + } + + sections[curr_tab_section_n].TabCount++; + tab_dst_n++; + } + if (tab_bar->Tabs.Size != tab_dst_n) + tab_bar->Tabs.resize(tab_dst_n); + + if (need_sort_by_section) + ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerBySection); + + // Calculate spacing between sections + sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f; + sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f; + + // Setup next selected tab + ImGuiID scroll_to_tab_id = 0; + if (tab_bar->NextSelectedTabId) + { + tab_bar->SelectedTabId = tab_bar->NextSelectedTabId; + tab_bar->NextSelectedTabId = 0; + scroll_to_tab_id = tab_bar->SelectedTabId; + } + + // Process order change request (we could probably process it when requested but it's just saner to do it in a single spot). + if (tab_bar->ReorderRequestTabId != 0) + { + if (TabBarProcessReorder(tab_bar)) + if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId) + scroll_to_tab_id = tab_bar->ReorderRequestTabId; + tab_bar->ReorderRequestTabId = 0; + } + + // Tab List Popup (will alter tab_bar->BarRect and therefore the available width!) + const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0; + if (tab_list_popup_button) + if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x! + scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID; + + // Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central + // (whereas our tabs are stored as: leading, central, trailing) + int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount }; + g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size); + + // Compute ideal tabs widths + store them into shrink buffer + ImGuiTabItem* most_recently_selected_tab = NULL; + int curr_section_n = -1; + bool found_selected_tab_id = false; + for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible); + + if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button)) + most_recently_selected_tab = tab; + if (tab->ID == tab_bar->SelectedTabId) + found_selected_tab_id = true; + if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID) + scroll_to_tab_id = tab->ID; + + // Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar. + // Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet, + // and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window. + const char* tab_name = tab_bar->GetTabName(tab); + const bool has_close_button = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) ? false : true; + tab->ContentWidth = TabItemCalcSize(tab_name, has_close_button).x; + + int section_n = TabItemGetSectionIdx(tab); + ImGuiTabBarSection* section = §ions[section_n]; + section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f); + curr_section_n = section_n; + + // Store data so we can build an array sorted by width if we need to shrink tabs down + IM_MSVC_WARNING_SUPPRESS(6385); + int shrink_buffer_index = shrink_buffer_indexes[section_n]++; + g.ShrinkWidthBuffer[shrink_buffer_index].Index = tab_n; + g.ShrinkWidthBuffer[shrink_buffer_index].Width = tab->ContentWidth; + + IM_ASSERT(tab->ContentWidth > 0.0f); + tab->Width = tab->ContentWidth; + } + + // Compute total ideal width (used for e.g. auto-resizing a window) + tab_bar->WidthAllTabsIdeal = 0.0f; + for (int section_n = 0; section_n < 3; section_n++) + tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing; + + // Horizontal scrolling buttons + // (note that TabBarScrollButtons() will alter BarRect.Max.x) + if ((tab_bar->WidthAllTabsIdeal > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll)) + if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar)) + { + scroll_to_tab_id = scroll_and_select_tab->ID; + if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0) + tab_bar->SelectedTabId = scroll_to_tab_id; + } + + // Shrink widths if full tabs don't fit in their allocated space + float section_0_w = sections[0].Width + sections[0].Spacing; + float section_1_w = sections[1].Width + sections[1].Spacing; + float section_2_w = sections[2].Width + sections[2].Spacing; + bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth(); + float width_excess; + if (central_section_is_visible) + width_excess = ImMax(section_1_w - (tab_bar->BarRect.GetWidth() - section_0_w - section_2_w), 0.0f); // Excess used to shrink central section + else + width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section + + // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore + if (width_excess > 0.0f && ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown) || !central_section_is_visible)) + { + int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount); + int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0); + ShrinkWidths(g.ShrinkWidthBuffer.Data + shrink_data_offset, shrink_data_count, width_excess); + + // Apply shrunk values into tabs and sections + for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index]; + float shrinked_width = IM_FLOOR(g.ShrinkWidthBuffer[tab_n].Width); + if (shrinked_width < 0.0f) + continue; + + int section_n = TabItemGetSectionIdx(tab); + sections[section_n].Width -= (tab->Width - shrinked_width); + tab->Width = shrinked_width; + } + } + + // Layout all active tabs + int section_tab_index = 0; + float tab_offset = 0.0f; + tab_bar->WidthAllTabs = 0.0f; + for (int section_n = 0; section_n < 3; section_n++) + { + ImGuiTabBarSection* section = §ions[section_n]; + if (section_n == 2) + tab_offset = ImMin(ImMax(0.0f, tab_bar->BarRect.GetWidth() - section->Width), tab_offset); + + for (int tab_n = 0; tab_n < section->TabCount; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n]; + tab->Offset = tab_offset; + tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f); + } + tab_bar->WidthAllTabs += ImMax(section->Width + section->Spacing, 0.0f); + tab_offset += section->Spacing; + section_tab_index += section->TabCount; + } + + // If we have lost the selected tab, select the next most recently active one + if (found_selected_tab_id == false) + tab_bar->SelectedTabId = 0; + if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL) + scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID; + + // Lock in visible tab + tab_bar->VisibleTabId = tab_bar->SelectedTabId; + tab_bar->VisibleTabWasSubmitted = false; + + // Update scrolling + if (scroll_to_tab_id != 0) + TabBarScrollToTab(tab_bar, scroll_to_tab_id, sections); + tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, tab_bar->ScrollingAnim); + tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget); + if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget) + { + // Scrolling speed adjust itself so we can always reach our target in 1/3 seconds. + // Teleport if we are aiming far off the visible line + tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, 70.0f * g.FontSize); + tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f); + const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize); + tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(tab_bar->ScrollingAnim, tab_bar->ScrollingTarget, g.IO.DeltaTime * tab_bar->ScrollingSpeed); + } + else + { + tab_bar->ScrollingSpeed = 0.0f; + } + tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing; + tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing; + + // Clear name buffers + if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0) + tab_bar->TabsNames.Buf.resize(0); + + // Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame) + ImGuiWindow* window = g.CurrentWindow; + window->DC.CursorPos = tab_bar->BarRect.Min; + ItemSize(ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y); + window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal); +} + +// Dockables uses Name/ID in the global namespace. Non-dockable items use the ID stack. +static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label) +{ + if (tab_bar->Flags & ImGuiTabBarFlags_DockNode) + { + ImGuiID id = ImHashStr(label); + KeepAliveID(id); + return id; + } + else + { + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(label); + } +} + +static float ImGui::TabBarCalcMaxTabWidth() +{ + ImGuiContext& g = *GImGui; + return g.FontSize * 20.0f; +} + +ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id) +{ + if (tab_id != 0) + for (int n = 0; n < tab_bar->Tabs.Size; n++) + if (tab_bar->Tabs[n].ID == tab_id) + return &tab_bar->Tabs[n]; + return NULL; +} + +// The *TabId fields be already set by the docking system _before_ the actual TabItem was created, so we clear them regardless. +void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id) +{ + if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id)) + tab_bar->Tabs.erase(tab); + if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; } + if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; } + if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; } +} + +// Called on manual closure attempt +void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) +{ + IM_ASSERT(!(tab->Flags & ImGuiTabItemFlags_Button)); + if (!(tab->Flags & ImGuiTabItemFlags_UnsavedDocument)) + { + // This will remove a frame of lag for selecting another tab on closure. + // However we don't run it in the case where the 'Unsaved' flag is set, so user gets a chance to fully undo the closure + tab->WantClose = true; + if (tab_bar->VisibleTabId == tab->ID) + { + tab->LastFrameVisible = -1; + tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0; + } + } + else + { + // Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup) + if (tab_bar->VisibleTabId != tab->ID) + tab_bar->NextSelectedTabId = tab->ID; + } +} + +static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling) +{ + scrolling = ImMin(scrolling, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth()); + return ImMax(scrolling, 0.0f); +} + +// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys +static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections) +{ + ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id); + if (tab == NULL) + return; + if (tab->Flags & ImGuiTabItemFlags_SectionMask_) + return; + + ImGuiContext& g = *GImGui; + float margin = g.FontSize * 1.0f; // When to scroll to make Tab N+1 visible always make a bit of N visible to suggest more scrolling area (since we don't have a scrollbar) + int order = tab_bar->GetTabOrder(tab); + + // Scrolling happens only in the central section (leading/trailing sections are not scrolling) + // FIXME: This is all confusing. + float scrollable_width = tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing; + + // We make all tabs positions all relative Sections[0].Width to make code simpler + float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f); + float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f); + tab_bar->ScrollingTargetDistToVisibility = 0.0f; + if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width)) + { + // Scroll to the left + tab_bar->ScrollingTargetDistToVisibility = ImMax(tab_bar->ScrollingAnim - tab_x2, 0.0f); + tab_bar->ScrollingTarget = tab_x1; + } + else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width) + { + // Scroll to the right + tab_bar->ScrollingTargetDistToVisibility = ImMax((tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, 0.0f); + tab_bar->ScrollingTarget = tab_x2 - scrollable_width; + } +} + +void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, const ImGuiTabItem* tab, int offset) +{ + IM_ASSERT(offset != 0); + IM_ASSERT(tab_bar->ReorderRequestTabId == 0); + tab_bar->ReorderRequestTabId = tab->ID; + tab_bar->ReorderRequestOffset = (ImS16)offset; +} + +void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, const ImGuiTabItem* src_tab, ImVec2 mouse_pos) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(tab_bar->ReorderRequestTabId == 0); + if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0) + return; + + const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0; + const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0); + + // Count number of contiguous tabs we are crossing over + const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1; + const int src_idx = tab_bar->Tabs.index_from_ptr(src_tab); + int dst_idx = src_idx; + for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir) + { + // Reordered tabs must share the same section + const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i]; + if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder) + break; + if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_)) + break; + dst_idx = i; + + // Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered. + const float x1 = bar_offset + dst_tab->Offset - g.Style.ItemInnerSpacing.x; + const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + g.Style.ItemInnerSpacing.x; + //GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255)); + if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2)) + break; + } + + if (dst_idx != src_idx) + TabBarQueueReorder(tab_bar, src_tab, dst_idx - src_idx); +} + +bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar) +{ + ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId); + if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder)) + return false; + + //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools + int tab2_order = tab_bar->GetTabOrder(tab1) + tab_bar->ReorderRequestOffset; + if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size) + return false; + + // Reordered tabs must share the same section + // (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too) + ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order]; + if (tab2->Flags & ImGuiTabItemFlags_NoReorder) + return false; + if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_)) + return false; + + ImGuiTabItem item_tmp = *tab1; + ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2; + ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1; + const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset; + memmove(dst_tab, src_tab, move_count * sizeof(ImGuiTabItem)); + *tab2 = item_tmp; + + if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings) + MarkIniSettingsDirty(); + return true; +} + +static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f); + const float scrolling_buttons_width = arrow_button_size.x * 2.0f; + + const ImVec2 backup_cursor_pos = window->DC.CursorPos; + //window->DrawList->AddRect(ImVec2(tab_bar->BarRect.Max.x - scrolling_buttons_width, tab_bar->BarRect.Min.y), ImVec2(tab_bar->BarRect.Max.x, tab_bar->BarRect.Max.y), IM_COL32(255,0,0,255)); + + int select_dir = 0; + ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text]; + arrow_col.w *= 0.5f; + + PushStyleColor(ImGuiCol_Text, arrow_col); + PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + const float backup_repeat_delay = g.IO.KeyRepeatDelay; + const float backup_repeat_rate = g.IO.KeyRepeatRate; + g.IO.KeyRepeatDelay = 0.250f; + g.IO.KeyRepeatRate = 0.200f; + float x = ImMax(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.x - scrolling_buttons_width); + window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y); + if (ArrowButtonEx("##<", ImGuiDir_Left, arrow_button_size, ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat)) + select_dir = -1; + window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y); + if (ArrowButtonEx("##>", ImGuiDir_Right, arrow_button_size, ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat)) + select_dir = +1; + PopStyleColor(2); + g.IO.KeyRepeatRate = backup_repeat_rate; + g.IO.KeyRepeatDelay = backup_repeat_delay; + + ImGuiTabItem* tab_to_scroll_to = NULL; + if (select_dir != 0) + if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId)) + { + int selected_order = tab_bar->GetTabOrder(tab_item); + int target_order = selected_order + select_dir; + + // Skip tab item buttons until another tab item is found or end is reached + while (tab_to_scroll_to == NULL) + { + // If we are at the end of the list, still scroll to make our tab visible + tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order]; + + // Cross through buttons + // (even if first/last item is a button, return it so we can update the scroll) + if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button) + { + target_order += select_dir; + selected_order += select_dir; + tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL; + } + } + } + window->DC.CursorPos = backup_cursor_pos; + tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f; + + return tab_to_scroll_to; +} + +static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // We use g.Style.FramePadding.y to match the square ArrowButton size + const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y; + const ImVec2 backup_cursor_pos = window->DC.CursorPos; + window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y); + tab_bar->BarRect.Min.x += tab_list_popup_button_width; + + ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text]; + arrow_col.w *= 0.5f; + PushStyleColor(ImGuiCol_Text, arrow_col); + PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + bool open = BeginCombo("##v", NULL, ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest); + PopStyleColor(2); + + ImGuiTabItem* tab_to_select = NULL; + if (open) + { + for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + if (tab->Flags & ImGuiTabItemFlags_Button) + continue; + + const char* tab_name = tab_bar->GetTabName(tab); + if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID)) + tab_to_select = tab; + } + EndCombo(); + } + + window->DC.CursorPos = backup_cursor_pos; + return tab_to_select; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: BeginTabItem, EndTabItem, etc. +//------------------------------------------------------------------------- +// - BeginTabItem() +// - EndTabItem() +// - TabItemButton() +// - TabItemEx() [Internal] +// - SetTabItemClosed() +// - TabItemCalcSize() [Internal] +// - TabItemBackground() [Internal] +// - TabItemLabelAndCloseButton() [Internal] +//------------------------------------------------------------------------- + +bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + ImGuiTabBar* tab_bar = g.CurrentTabBar; + if (tab_bar == NULL) + { + IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!"); + return false; + } + IM_ASSERT(!(flags & ImGuiTabItemFlags_Button)); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead! + + bool ret = TabItemEx(tab_bar, label, p_open, flags); + if (ret && !(flags & ImGuiTabItemFlags_NoPushId)) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx]; + PushOverrideID(tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label) + } + return ret; +} + +void ImGui::EndTabItem() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + ImGuiTabBar* tab_bar = g.CurrentTabBar; + if (tab_bar == NULL) + { + IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!"); + return; + } + IM_ASSERT(tab_bar->LastTabItemIdx >= 0); + ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx]; + if (!(tab->Flags & ImGuiTabItemFlags_NoPushId)) + PopID(); +} + +bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + ImGuiTabBar* tab_bar = g.CurrentTabBar; + if (tab_bar == NULL) + { + IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!"); + return false; + } + return TabItemEx(tab_bar, label, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder); +} + +bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags) +{ + // Layout whole tab bar if not already done + if (tab_bar->WantLayout) + TabBarLayout(tab_bar); + + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = TabBarCalcTabID(tab_bar, label); + + // If the user called us with *p_open == false, we early out and don't render. + // We make a call to ItemAdd() so that attempts to use a contextual popup menu with an implicit ID won't use an older ID. + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + if (p_open && !*p_open) + { + ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus); + return false; + } + + IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button)); + IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing + + // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented) + if (flags & ImGuiTabItemFlags_NoCloseButton) + p_open = NULL; + else if (p_open == NULL) + flags |= ImGuiTabItemFlags_NoCloseButton; + + // Calculate tab contents size + ImVec2 size = TabItemCalcSize(label, p_open != NULL); + + // Acquire tab data + ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, id); + bool tab_is_new = false; + if (tab == NULL) + { + tab_bar->Tabs.push_back(ImGuiTabItem()); + tab = &tab_bar->Tabs.back(); + tab->ID = id; + tab->Width = size.x; + tab_bar->TabsAddedNew = true; + tab_is_new = true; + } + tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(tab); + tab->ContentWidth = size.x; + tab->BeginOrder = tab_bar->TabsActiveCount++; + + const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount); + const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0; + const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount); + const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0; + tab->LastFrameVisible = g.FrameCount; + tab->Flags = flags; + + // Append name with zero-terminator + tab->NameOffset = (ImS32)tab_bar->TabsNames.size(); + tab_bar->TabsNames.append(label, label + strlen(label) + 1); + + // Update selected tab + if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0) + if (!tab_bar_appearing || tab_bar->SelectedTabId == 0) + if (!is_tab_button) + tab_bar->NextSelectedTabId = id; // New tabs gets activated + if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // SetSelected can only be passed on explicit tab bar + if (!is_tab_button) + tab_bar->NextSelectedTabId = id; + + // Lock visibility + // (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!) + bool tab_contents_visible = (tab_bar->VisibleTabId == id); + if (tab_contents_visible) + tab_bar->VisibleTabWasSubmitted = true; + + // On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches + if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing) + if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs)) + tab_contents_visible = true; + + // Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted + // and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'. + if (tab_appearing && (!tab_bar_appearing || tab_is_new)) + { + ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus); + if (is_tab_button) + return false; + return tab_contents_visible; + } + + if (tab_bar->SelectedTabId == id) + tab->LastFrameSelected = g.FrameCount; + + // Backup current layout position + const ImVec2 backup_main_cursor_pos = window->DC.CursorPos; + + // Layout + const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0; + size.x = tab->Width; + if (is_central_section) + window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_FLOOR(tab->Offset - tab_bar->ScrollingAnim), 0.0f); + else + window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f); + ImVec2 pos = window->DC.CursorPos; + ImRect bb(pos, pos + size); + + // We don't have CPU clipping primitives to clip the CloseButton (until it becomes a texture), so need to add an extra draw call (temporary in the case of vertical animation) + const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX); + if (want_clip_rect) + PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->ScrollingRectMinX), bb.Min.y - 1), ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), true); + + ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos; + ItemSize(bb.GetSize(), style.FramePadding.y); + window->DC.CursorMaxPos = backup_cursor_max_pos; + + if (!ItemAdd(bb, id)) + { + if (want_clip_rect) + PopClipRect(); + window->DC.CursorPos = backup_main_cursor_pos; + return tab_contents_visible; + } + + // Click to Select a tab + ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowItemOverlap); + if (g.DragDropActive) + button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); + if (pressed && !is_tab_button) + tab_bar->NextSelectedTabId = id; + + // Allow the close button to overlap unless we are dragging (in which case we don't want any overlapping tabs to be hovered) + if (g.ActiveId != id) + SetItemAllowOverlap(); + + // Drag and drop: re-order tabs + if (held && !tab_appearing && IsMouseDragging(0)) + { + if (!g.DragDropActive && (tab_bar->Flags & ImGuiTabBarFlags_Reorderable)) + { + // While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x + if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x) + { + TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos); + } + else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x) + { + TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos); + } + } + } + +#if 0 + if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth) + { + // Enlarge tab display when hovering + bb.Max.x = bb.Min.x + IM_FLOOR(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f))); + display_draw_list = GetForegroundDrawList(window); + TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive)); + } +#endif + + // Render tab shape + ImDrawList* display_draw_list = window->DrawList; + const ImU32 tab_col = GetColorU32((held || hovered) ? ImGuiCol_TabHovered : tab_contents_visible ? (tab_bar_focused ? ImGuiCol_TabActive : ImGuiCol_TabUnfocusedActive) : (tab_bar_focused ? ImGuiCol_Tab : ImGuiCol_TabUnfocused)); + TabItemBackground(display_draw_list, bb, flags, tab_col); + RenderNavHighlight(bb, id); + + // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget. + const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); + if (hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1))) + if (!is_tab_button) + tab_bar->NextSelectedTabId = id; + + if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton) + flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton; + + // Render tab label, process close button + const ImGuiID close_button_id = p_open ? GetIDWithSeed("#CLOSE", NULL, id) : 0; + bool just_closed; + bool text_clipped; + TabItemLabelAndCloseButton(display_draw_list, bb, flags, tab_bar->FramePadding, label, id, close_button_id, tab_contents_visible, &just_closed, &text_clipped); + if (just_closed && p_open != NULL) + { + *p_open = false; + TabBarCloseTab(tab_bar, tab); + } + + // Restore main window position so user can draw there + if (want_clip_rect) + PopClipRect(); + window->DC.CursorPos = backup_main_cursor_pos; + + // Tooltip + // (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok) + // (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores) + // FIXME: This is a mess. + // FIXME: We may want disabled tab to still display the tooltip? + if (text_clipped && g.HoveredId == id && !held && g.HoveredIdNotActiveTimer > g.TooltipSlowDelay && IsItemHovered()) + if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip)) + SetTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label); + + IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected + if (is_tab_button) + return pressed; + return tab_contents_visible; +} + +// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed. +// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar(). +// Tabs closed by the close button will automatically be flagged to avoid this issue. +void ImGui::SetTabItemClosed(const char* label) +{ + ImGuiContext& g = *GImGui; + bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode); + if (is_within_manual_tab_bar) + { + ImGuiTabBar* tab_bar = g.CurrentTabBar; + ImGuiID tab_id = TabBarCalcTabID(tab_bar, label); + if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id)) + tab->WantClose = true; // Will be processed by next call to TabBarLayout() + } +} + +ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button) +{ + ImGuiContext& g = *GImGui; + ImVec2 label_size = CalcTextSize(label, NULL, true); + ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f); + if (has_close_button) + size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle. + else + size.x += g.Style.FramePadding.x + 1.0f; + return ImVec2(ImMin(size.x, TabBarCalcMaxTabWidth()), size.y); +} + +void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col) +{ + // While rendering tabs, we trim 1 pixel off the top of our bounding box so they can fit within a regular frame height while looking "detached" from it. + ImGuiContext& g = *GImGui; + const float width = bb.GetWidth(); + IM_UNUSED(flags); + IM_ASSERT(width > 0.0f); + const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f)); + const float y1 = bb.Min.y + 1.0f; + const float y2 = bb.Max.y - 1.0f; + draw_list->PathLineTo(ImVec2(bb.Min.x, y2)); + draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding, y1 + rounding), rounding, 6, 9); + draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding, y1 + rounding), rounding, 9, 12); + draw_list->PathLineTo(ImVec2(bb.Max.x, y2)); + draw_list->PathFillConvex(col); + if (g.Style.TabBorderSize > 0.0f) + { + draw_list->PathLineTo(ImVec2(bb.Min.x + 0.5f, y2)); + draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9); + draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12); + draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2)); + draw_list->PathStroke(GetColorU32(ImGuiCol_Border), 0, g.Style.TabBorderSize); + } +} + +// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic +// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter. +void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id, bool is_contents_visible, bool* out_just_closed, bool* out_text_clipped) +{ + ImGuiContext& g = *GImGui; + ImVec2 label_size = CalcTextSize(label, NULL, true); + + if (out_just_closed) + *out_just_closed = false; + if (out_text_clipped) + *out_text_clipped = false; + + if (bb.GetWidth() <= 1.0f) + return; + + // In Style V2 we'll have full override of all colors per state (e.g. focused, selected) + // But right now if you want to alter text color of tabs this is what you need to do. +#if 0 + const float backup_alpha = g.Style.Alpha; + if (!is_contents_visible) + g.Style.Alpha *= 0.7f; +#endif + + // Render text label (with clipping + alpha gradient) + unsaved marker + ImRect text_pixel_clip_bb(bb.Min.x + frame_padding.x, bb.Min.y + frame_padding.y, bb.Max.x - frame_padding.x, bb.Max.y); + ImRect text_ellipsis_clip_bb = text_pixel_clip_bb; + + // Return clipped state ignoring the close button + if (out_text_clipped) + { + *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_pixel_clip_bb.Max.x; + //draw_list->AddCircle(text_ellipsis_clip_bb.Min, 3.0f, *out_text_clipped ? IM_COL32(255, 0, 0, 255) : IM_COL32(0, 255, 0, 255)); + } + + const float button_sz = g.FontSize; + const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x * 2.0f - button_sz), bb.Min.y); + + // Close Button & Unsaved Marker + // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap() + // 'hovered' will be true when hovering the Tab but NOT when hovering the close button + // 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button + // 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false + bool close_button_pressed = false; + bool close_button_visible = false; + if (close_button_id != 0) + if (is_contents_visible || bb.GetWidth() >= ImMax(button_sz, g.Style.TabMinWidthForCloseButton)) + if (g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == tab_id || g.ActiveId == close_button_id) + close_button_visible = true; + bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x); + + if (close_button_visible) + { + ImGuiLastItemData last_item_backup = g.LastItemData; + PushStyleVar(ImGuiStyleVar_FramePadding, frame_padding); + if (CloseButton(close_button_id, button_pos)) + close_button_pressed = true; + PopStyleVar(); + g.LastItemData = last_item_backup; + + // Close with middle mouse button + if (!(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(2)) + close_button_pressed = true; + } + else if (unsaved_marker_visible) + { + const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz) + g.Style.FramePadding * 2.0f); + RenderBullet(draw_list, bullet_bb.GetCenter(), GetColorU32(ImGuiCol_Text)); + } + + // This is all rather complicated + // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position) + // FIXME: if FramePadding is noticeably large, ellipsis_max_x will be wrong here (e.g. #3497), maybe for consistency that parameter of RenderTextEllipsis() shouldn't exist.. + float ellipsis_max_x = close_button_visible ? text_pixel_clip_bb.Max.x : bb.Max.x - 1.0f; + if (close_button_visible || unsaved_marker_visible) + { + text_pixel_clip_bb.Max.x -= close_button_visible ? (button_sz) : (button_sz * 0.80f); + text_ellipsis_clip_bb.Max.x -= unsaved_marker_visible ? (button_sz * 0.80f) : 0.0f; + ellipsis_max_x = text_pixel_clip_bb.Max.x; + } + RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, text_pixel_clip_bb.Max.x, ellipsis_max_x, label, NULL, &label_size); + +#if 0 + if (!is_contents_visible) + g.Style.Alpha = backup_alpha; +#endif + + if (out_just_closed) + *out_just_closed = close_button_pressed; +} + + +#endif // #ifndef IMGUI_DISABLE + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Private/imstb_rectpack.h`: + +```h +// [DEAR IMGUI] +// This is a slightly modified version of stb_rect_pack.h 1.00. +// Those changes would need to be pushed into nothings/stb: +// - Added STBRP__CDECL +// Grep for [DEAR IMGUI] to find the changes. + +// stb_rect_pack.h - v1.00 - public domain - rectangle packing +// Sean Barrett 2014 +// +// Useful for e.g. packing rectangular textures into an atlas. +// Does not do rotation. +// +// Not necessarily the awesomest packing method, but better than +// the totally naive one in stb_truetype (which is primarily what +// this is meant to replace). +// +// Has only had a few tests run, may have issues. +// +// More docs to come. +// +// No memory allocations; uses qsort() and assert() from stdlib. +// Can override those by defining STBRP_SORT and STBRP_ASSERT. +// +// This library currently uses the Skyline Bottom-Left algorithm. +// +// Please note: better rectangle packers are welcome! Please +// implement them to the same API, but with a different init +// function. +// +// Credits +// +// Library +// Sean Barrett +// Minor features +// Martins Mozeiko +// github:IntellectualKitty +// +// Bugfixes / warning fixes +// Jeremy Jaussaud +// Fabian Giesen +// +// Version history: +// +// 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles +// 0.99 (2019-02-07) warning fixes +// 0.11 (2017-03-03) return packing success/fail result +// 0.10 (2016-10-25) remove cast-away-const to avoid warnings +// 0.09 (2016-08-27) fix compiler warnings +// 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0) +// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0) +// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort +// 0.05: added STBRP_ASSERT to allow replacing assert +// 0.04: fixed minor bug in STBRP_LARGE_RECTS support +// 0.01: initial release +// +// LICENSE +// +// See end of file for license information. + +////////////////////////////////////////////////////////////////////////////// +// +// INCLUDE SECTION +// + +#ifndef STB_INCLUDE_STB_RECT_PACK_H +#define STB_INCLUDE_STB_RECT_PACK_H + +#define STB_RECT_PACK_VERSION 1 + +#ifdef STBRP_STATIC +#define STBRP_DEF static +#else +#define STBRP_DEF extern +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct stbrp_context stbrp_context; +typedef struct stbrp_node stbrp_node; +typedef struct stbrp_rect stbrp_rect; + +#ifdef STBRP_LARGE_RECTS +typedef int stbrp_coord; +#else +typedef unsigned short stbrp_coord; +#endif + +STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects); +// Assign packed locations to rectangles. The rectangles are of type +// 'stbrp_rect' defined below, stored in the array 'rects', and there +// are 'num_rects' many of them. +// +// Rectangles which are successfully packed have the 'was_packed' flag +// set to a non-zero value and 'x' and 'y' store the minimum location +// on each axis (i.e. bottom-left in cartesian coordinates, top-left +// if you imagine y increasing downwards). Rectangles which do not fit +// have the 'was_packed' flag set to 0. +// +// You should not try to access the 'rects' array from another thread +// while this function is running, as the function temporarily reorders +// the array while it executes. +// +// To pack into another rectangle, you need to call stbrp_init_target +// again. To continue packing into the same rectangle, you can call +// this function again. Calling this multiple times with multiple rect +// arrays will probably produce worse packing results than calling it +// a single time with the full rectangle array, but the option is +// available. +// +// The function returns 1 if all of the rectangles were successfully +// packed and 0 otherwise. + +struct stbrp_rect +{ + // reserved for your use: + int id; + + // input: + stbrp_coord w, h; + + // output: + stbrp_coord x, y; + int was_packed; // non-zero if valid packing + +}; // 16 bytes, nominally + + +STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes); +// Initialize a rectangle packer to: +// pack a rectangle that is 'width' by 'height' in dimensions +// using temporary storage provided by the array 'nodes', which is 'num_nodes' long +// +// You must call this function every time you start packing into a new target. +// +// There is no "shutdown" function. The 'nodes' memory must stay valid for +// the following stbrp_pack_rects() call (or calls), but can be freed after +// the call (or calls) finish. +// +// Note: to guarantee best results, either: +// 1. make sure 'num_nodes' >= 'width' +// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1' +// +// If you don't do either of the above things, widths will be quantized to multiples +// of small integers to guarantee the algorithm doesn't run out of temporary storage. +// +// If you do #2, then the non-quantized algorithm will be used, but the algorithm +// may run out of temporary storage and be unable to pack some rectangles. + +STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem); +// Optionally call this function after init but before doing any packing to +// change the handling of the out-of-temp-memory scenario, described above. +// If you call init again, this will be reset to the default (false). + + +STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic); +// Optionally select which packing heuristic the library should use. Different +// heuristics will produce better/worse results for different data sets. +// If you call init again, this will be reset to the default. + +enum +{ + STBRP_HEURISTIC_Skyline_default=0, + STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default, + STBRP_HEURISTIC_Skyline_BF_sortHeight +}; + + +////////////////////////////////////////////////////////////////////////////// +// +// the details of the following structures don't matter to you, but they must +// be visible so you can handle the memory allocations for them + +struct stbrp_node +{ + stbrp_coord x,y; + stbrp_node *next; +}; + +struct stbrp_context +{ + int width; + int height; + int align; + int init_mode; + int heuristic; + int num_nodes; + stbrp_node *active_head; + stbrp_node *free_head; + stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2' +}; + +#ifdef __cplusplus +} +#endif + +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// IMPLEMENTATION SECTION +// + +#ifdef STB_RECT_PACK_IMPLEMENTATION +#ifndef STBRP_SORT +#include +#define STBRP_SORT qsort +#endif + +#ifndef STBRP_ASSERT +#include +#define STBRP_ASSERT assert +#endif + +// [DEAR IMGUI] Added STBRP__CDECL +#ifdef _MSC_VER +#define STBRP__NOTUSED(v) (void)(v) +#define STBRP__CDECL __cdecl +#else +#define STBRP__NOTUSED(v) (void)sizeof(v) +#define STBRP__CDECL +#endif + +enum +{ + STBRP__INIT_skyline = 1 +}; + +STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic) +{ + switch (context->init_mode) { + case STBRP__INIT_skyline: + STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight); + context->heuristic = heuristic; + break; + default: + STBRP_ASSERT(0); + } +} + +STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem) +{ + if (allow_out_of_mem) + // if it's ok to run out of memory, then don't bother aligning them; + // this gives better packing, but may fail due to OOM (even though + // the rectangles easily fit). @TODO a smarter approach would be to only + // quantize once we've hit OOM, then we could get rid of this parameter. + context->align = 1; + else { + // if it's not ok to run out of memory, then quantize the widths + // so that num_nodes is always enough nodes. + // + // I.e. num_nodes * align >= width + // align >= width / num_nodes + // align = ceil(width/num_nodes) + + context->align = (context->width + context->num_nodes-1) / context->num_nodes; + } +} + +STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes) +{ + int i; +#ifndef STBRP_LARGE_RECTS + STBRP_ASSERT(width <= 0xffff && height <= 0xffff); +#endif + + for (i=0; i < num_nodes-1; ++i) + nodes[i].next = &nodes[i+1]; + nodes[i].next = NULL; + context->init_mode = STBRP__INIT_skyline; + context->heuristic = STBRP_HEURISTIC_Skyline_default; + context->free_head = &nodes[0]; + context->active_head = &context->extra[0]; + context->width = width; + context->height = height; + context->num_nodes = num_nodes; + stbrp_setup_allow_out_of_mem(context, 0); + + // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly) + context->extra[0].x = 0; + context->extra[0].y = 0; + context->extra[0].next = &context->extra[1]; + context->extra[1].x = (stbrp_coord) width; +#ifdef STBRP_LARGE_RECTS + context->extra[1].y = (1<<30); +#else + context->extra[1].y = 65535; +#endif + context->extra[1].next = NULL; +} + +// find minimum y position if it starts at x1 +static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste) +{ + stbrp_node *node = first; + int x1 = x0 + width; + int min_y, visited_width, waste_area; + + STBRP__NOTUSED(c); + + STBRP_ASSERT(first->x <= x0); + + #if 0 + // skip in case we're past the node + while (node->next->x <= x0) + ++node; + #else + STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency + #endif + + STBRP_ASSERT(node->x <= x0); + + min_y = 0; + waste_area = 0; + visited_width = 0; + while (node->x < x1) { + if (node->y > min_y) { + // raise min_y higher. + // we've accounted for all waste up to min_y, + // but we'll now add more waste for everything we've visted + waste_area += visited_width * (node->y - min_y); + min_y = node->y; + // the first time through, visited_width might be reduced + if (node->x < x0) + visited_width += node->next->x - x0; + else + visited_width += node->next->x - node->x; + } else { + // add waste area + int under_width = node->next->x - node->x; + if (under_width + visited_width > width) + under_width = width - visited_width; + waste_area += under_width * (min_y - node->y); + visited_width += under_width; + } + node = node->next; + } + + *pwaste = waste_area; + return min_y; +} + +typedef struct +{ + int x,y; + stbrp_node **prev_link; +} stbrp__findresult; + +static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height) +{ + int best_waste = (1<<30), best_x, best_y = (1 << 30); + stbrp__findresult fr; + stbrp_node **prev, *node, *tail, **best = NULL; + + // align to multiple of c->align + width = (width + c->align - 1); + width -= width % c->align; + STBRP_ASSERT(width % c->align == 0); + + // if it can't possibly fit, bail immediately + if (width > c->width || height > c->height) { + fr.prev_link = NULL; + fr.x = fr.y = 0; + return fr; + } + + node = c->active_head; + prev = &c->active_head; + while (node->x + width <= c->width) { + int y,waste; + y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste); + if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL + // bottom left + if (y < best_y) { + best_y = y; + best = prev; + } + } else { + // best-fit + if (y + height <= c->height) { + // can only use it if it first vertically + if (y < best_y || (y == best_y && waste < best_waste)) { + best_y = y; + best_waste = waste; + best = prev; + } + } + } + prev = &node->next; + node = node->next; + } + + best_x = (best == NULL) ? 0 : (*best)->x; + + // if doing best-fit (BF), we also have to try aligning right edge to each node position + // + // e.g, if fitting + // + // ____________________ + // |____________________| + // + // into + // + // | | + // | ____________| + // |____________| + // + // then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned + // + // This makes BF take about 2x the time + + if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) { + tail = c->active_head; + node = c->active_head; + prev = &c->active_head; + // find first node that's admissible + while (tail->x < width) + tail = tail->next; + while (tail) { + int xpos = tail->x - width; + int y,waste; + STBRP_ASSERT(xpos >= 0); + // find the left position that matches this + while (node->next->x <= xpos) { + prev = &node->next; + node = node->next; + } + STBRP_ASSERT(node->next->x > xpos && node->x <= xpos); + y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste); + if (y + height <= c->height) { + if (y <= best_y) { + if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) { + best_x = xpos; + STBRP_ASSERT(y <= best_y); + best_y = y; + best_waste = waste; + best = prev; + } + } + } + tail = tail->next; + } + } + + fr.prev_link = best; + fr.x = best_x; + fr.y = best_y; + return fr; +} + +static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height) +{ + // find best position according to heuristic + stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height); + stbrp_node *node, *cur; + + // bail if: + // 1. it failed + // 2. the best node doesn't fit (we don't always check this) + // 3. we're out of memory + if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) { + res.prev_link = NULL; + return res; + } + + // on success, create new node + node = context->free_head; + node->x = (stbrp_coord) res.x; + node->y = (stbrp_coord) (res.y + height); + + context->free_head = node->next; + + // insert the new node into the right starting point, and + // let 'cur' point to the remaining nodes needing to be + // stiched back in + + cur = *res.prev_link; + if (cur->x < res.x) { + // preserve the existing one, so start testing with the next one + stbrp_node *next = cur->next; + cur->next = node; + cur = next; + } else { + *res.prev_link = node; + } + + // from here, traverse cur and free the nodes, until we get to one + // that shouldn't be freed + while (cur->next && cur->next->x <= res.x + width) { + stbrp_node *next = cur->next; + // move the current node to the free list + cur->next = context->free_head; + context->free_head = cur; + cur = next; + } + + // stitch the list back in + node->next = cur; + + if (cur->x < res.x + width) + cur->x = (stbrp_coord) (res.x + width); + +#ifdef _DEBUG + cur = context->active_head; + while (cur->x < context->width) { + STBRP_ASSERT(cur->x < cur->next->x); + cur = cur->next; + } + STBRP_ASSERT(cur->next == NULL); + + { + int count=0; + cur = context->active_head; + while (cur) { + cur = cur->next; + ++count; + } + cur = context->free_head; + while (cur) { + cur = cur->next; + ++count; + } + STBRP_ASSERT(count == context->num_nodes+2); + } +#endif + + return res; +} + +// [DEAR IMGUI] Added STBRP__CDECL +static int STBRP__CDECL rect_height_compare(const void *a, const void *b) +{ + const stbrp_rect *p = (const stbrp_rect *) a; + const stbrp_rect *q = (const stbrp_rect *) b; + if (p->h > q->h) + return -1; + if (p->h < q->h) + return 1; + return (p->w > q->w) ? -1 : (p->w < q->w); +} + +// [DEAR IMGUI] Added STBRP__CDECL +static int STBRP__CDECL rect_original_order(const void *a, const void *b) +{ + const stbrp_rect *p = (const stbrp_rect *) a; + const stbrp_rect *q = (const stbrp_rect *) b; + return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed); +} + +#ifdef STBRP_LARGE_RECTS +#define STBRP__MAXVAL 0xffffffff +#else +#define STBRP__MAXVAL 0xffff +#endif + +STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects) +{ + int i, all_rects_packed = 1; + + // we use the 'was_packed' field internally to allow sorting/unsorting + for (i=0; i < num_rects; ++i) { + rects[i].was_packed = i; + } + + // sort according to heuristic + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare); + + for (i=0; i < num_rects; ++i) { + if (rects[i].w == 0 || rects[i].h == 0) { + rects[i].x = rects[i].y = 0; // empty rect needs no space + } else { + stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h); + if (fr.prev_link) { + rects[i].x = (stbrp_coord) fr.x; + rects[i].y = (stbrp_coord) fr.y; + } else { + rects[i].x = rects[i].y = STBRP__MAXVAL; + } + } + } + + // unsort + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order); + + // set was_packed flags and all_rects_packed status + for (i=0; i < num_rects; ++i) { + rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL); + if (!rects[i].was_packed) + all_rects_packed = 0; + } + + // return the all_rects_packed status + return all_rects_packed; +} +#endif + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ + +``` + +`Plugins/ImGui/Source/ThirdParty/ImGuiLibrary/Private/imstb_truetype.h`: + +```h +// [DEAR IMGUI] +// This is a slightly modified version of stb_truetype.h 1.20. +// Mostly fixing for compiler and static analyzer warnings. +// Grep for [DEAR IMGUI] to find the changes. + +// stb_truetype.h - v1.20 - public domain +// authored from 2009-2016 by Sean Barrett / RAD Game Tools +// +// This library processes TrueType files: +// parse files +// extract glyph metrics +// extract glyph shapes +// render glyphs to one-channel bitmaps with antialiasing (box filter) +// render glyphs to one-channel SDF bitmaps (signed-distance field/function) +// +// Todo: +// non-MS cmaps +// crashproof on bad data +// hinting? (no longer patented) +// cleartype-style AA? +// optimize: use simple memory allocator for intermediates +// optimize: build edge-list directly from curves +// optimize: rasterize directly from curves? +// +// ADDITIONAL CONTRIBUTORS +// +// Mikko Mononen: compound shape support, more cmap formats +// Tor Andersson: kerning, subpixel rendering +// Dougall Johnson: OpenType / Type 2 font handling +// Daniel Ribeiro Maciel: basic GPOS-based kerning +// +// Misc other: +// Ryan Gordon +// Simon Glass +// github:IntellectualKitty +// Imanol Celaya +// Daniel Ribeiro Maciel +// +// Bug/warning reports/fixes: +// "Zer" on mollyrocket Fabian "ryg" Giesen +// Cass Everitt Martins Mozeiko +// stoiko (Haemimont Games) Cap Petschulat +// Brian Hook Omar Cornut +// Walter van Niftrik github:aloucks +// David Gow Peter LaValle +// David Given Sergey Popov +// Ivan-Assen Ivanov Giumo X. Clanjor +// Anthony Pesch Higor Euripedes +// Johan Duparc Thomas Fields +// Hou Qiming Derek Vinyard +// Rob Loach Cort Stratton +// Kenney Phillis Jr. github:oyvindjam +// Brian Costabile github:vassvik +// +// VERSION HISTORY +// +// 1.20 (2019-02-07) PackFontRange skips missing codepoints; GetScaleFontVMetrics() +// 1.19 (2018-02-11) GPOS kerning, STBTT_fmod +// 1.18 (2018-01-29) add missing function +// 1.17 (2017-07-23) make more arguments const; doc fix +// 1.16 (2017-07-12) SDF support +// 1.15 (2017-03-03) make more arguments const +// 1.14 (2017-01-16) num-fonts-in-TTC function +// 1.13 (2017-01-02) support OpenType fonts, certain Apple fonts +// 1.12 (2016-10-25) suppress warnings about casting away const with -Wcast-qual +// 1.11 (2016-04-02) fix unused-variable warning +// 1.10 (2016-04-02) user-defined fabs(); rare memory leak; remove duplicate typedef +// 1.09 (2016-01-16) warning fix; avoid crash on outofmem; use allocation userdata properly +// 1.08 (2015-09-13) document stbtt_Rasterize(); fixes for vertical & horizontal edges +// 1.07 (2015-08-01) allow PackFontRanges to accept arrays of sparse codepoints; +// variant PackFontRanges to pack and render in separate phases; +// fix stbtt_GetFontOFfsetForIndex (never worked for non-0 input?); +// fixed an assert() bug in the new rasterizer +// replace assert() with STBTT_assert() in new rasterizer +// +// Full history can be found at the end of this file. +// +// LICENSE +// +// See end of file for license information. +// +// USAGE +// +// Include this file in whatever places need to refer to it. In ONE C/C++ +// file, write: +// #define STB_TRUETYPE_IMPLEMENTATION +// before the #include of this file. This expands out the actual +// implementation into that C/C++ file. +// +// To make the implementation private to the file that generates the implementation, +// #define STBTT_STATIC +// +// Simple 3D API (don't ship this, but it's fine for tools and quick start) +// stbtt_BakeFontBitmap() -- bake a font to a bitmap for use as texture +// stbtt_GetBakedQuad() -- compute quad to draw for a given char +// +// Improved 3D API (more shippable): +// #include "stb_rect_pack.h" -- optional, but you really want it +// stbtt_PackBegin() +// stbtt_PackSetOversampling() -- for improved quality on small fonts +// stbtt_PackFontRanges() -- pack and renders +// stbtt_PackEnd() +// stbtt_GetPackedQuad() +// +// "Load" a font file from a memory buffer (you have to keep the buffer loaded) +// stbtt_InitFont() +// stbtt_GetFontOffsetForIndex() -- indexing for TTC font collections +// stbtt_GetNumberOfFonts() -- number of fonts for TTC font collections +// +// Render a unicode codepoint to a bitmap +// stbtt_GetCodepointBitmap() -- allocates and returns a bitmap +// stbtt_MakeCodepointBitmap() -- renders into bitmap you provide +// stbtt_GetCodepointBitmapBox() -- how big the bitmap must be +// +// Character advance/positioning +// stbtt_GetCodepointHMetrics() +// stbtt_GetFontVMetrics() +// stbtt_GetFontVMetricsOS2() +// stbtt_GetCodepointKernAdvance() +// +// Starting with version 1.06, the rasterizer was replaced with a new, +// faster and generally-more-precise rasterizer. The new rasterizer more +// accurately measures pixel coverage for anti-aliasing, except in the case +// where multiple shapes overlap, in which case it overestimates the AA pixel +// coverage. Thus, anti-aliasing of intersecting shapes may look wrong. If +// this turns out to be a problem, you can re-enable the old rasterizer with +// #define STBTT_RASTERIZER_VERSION 1 +// which will incur about a 15% speed hit. +// +// ADDITIONAL DOCUMENTATION +// +// Immediately after this block comment are a series of sample programs. +// +// After the sample programs is the "header file" section. This section +// includes documentation for each API function. +// +// Some important concepts to understand to use this library: +// +// Codepoint +// Characters are defined by unicode codepoints, e.g. 65 is +// uppercase A, 231 is lowercase c with a cedilla, 0x7e30 is +// the hiragana for "ma". +// +// Glyph +// A visual character shape (every codepoint is rendered as +// some glyph) +// +// Glyph index +// A font-specific integer ID representing a glyph +// +// Baseline +// Glyph shapes are defined relative to a baseline, which is the +// bottom of uppercase characters. Characters extend both above +// and below the baseline. +// +// Current Point +// As you draw text to the screen, you keep track of a "current point" +// which is the origin of each character. The current point's vertical +// position is the baseline. Even "baked fonts" use this model. +// +// Vertical Font Metrics +// The vertical qualities of the font, used to vertically position +// and space the characters. See docs for stbtt_GetFontVMetrics. +// +// Font Size in Pixels or Points +// The preferred interface for specifying font sizes in stb_truetype +// is to specify how tall the font's vertical extent should be in pixels. +// If that sounds good enough, skip the next paragraph. +// +// Most font APIs instead use "points", which are a common typographic +// measurement for describing font size, defined as 72 points per inch. +// stb_truetype provides a point API for compatibility. However, true +// "per inch" conventions don't make much sense on computer displays +// since different monitors have different number of pixels per +// inch. For example, Windows traditionally uses a convention that +// there are 96 pixels per inch, thus making 'inch' measurements have +// nothing to do with inches, and thus effectively defining a point to +// be 1.333 pixels. Additionally, the TrueType font data provides +// an explicit scale factor to scale a given font's glyphs to points, +// but the author has observed that this scale factor is often wrong +// for non-commercial fonts, thus making fonts scaled in points +// according to the TrueType spec incoherently sized in practice. +// +// DETAILED USAGE: +// +// Scale: +// Select how high you want the font to be, in points or pixels. +// Call ScaleForPixelHeight or ScaleForMappingEmToPixels to compute +// a scale factor SF that will be used by all other functions. +// +// Baseline: +// You need to select a y-coordinate that is the baseline of where +// your text will appear. Call GetFontBoundingBox to get the baseline-relative +// bounding box for all characters. SF*-y0 will be the distance in pixels +// that the worst-case character could extend above the baseline, so if +// you want the top edge of characters to appear at the top of the +// screen where y=0, then you would set the baseline to SF*-y0. +// +// Current point: +// Set the current point where the first character will appear. The +// first character could extend left of the current point; this is font +// dependent. You can either choose a current point that is the leftmost +// point and hope, or add some padding, or check the bounding box or +// left-side-bearing of the first character to be displayed and set +// the current point based on that. +// +// Displaying a character: +// Compute the bounding box of the character. It will contain signed values +// relative to . I.e. if it returns x0,y0,x1,y1, +// then the character should be displayed in the rectangle from +// to = 32 && *text < 128) { + stbtt_aligned_quad q; + stbtt_GetBakedQuad(cdata, 512,512, *text-32, &x,&y,&q,1);//1=opengl & d3d10+,0=d3d9 + glTexCoord2f(q.s0,q.t1); glVertex2f(q.x0,q.y0); + glTexCoord2f(q.s1,q.t1); glVertex2f(q.x1,q.y0); + glTexCoord2f(q.s1,q.t0); glVertex2f(q.x1,q.y1); + glTexCoord2f(q.s0,q.t0); glVertex2f(q.x0,q.y1); + } + ++text; + } + glEnd(); +} +#endif +// +// +////////////////////////////////////////////////////////////////////////////// +// +// Complete program (this compiles): get a single bitmap, print as ASCII art +// +#if 0 +#include +#define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation +#include "stb_truetype.h" + +char ttf_buffer[1<<25]; + +int main(int argc, char **argv) +{ + stbtt_fontinfo font; + unsigned char *bitmap; + int w,h,i,j,c = (argc > 1 ? atoi(argv[1]) : 'a'), s = (argc > 2 ? atoi(argv[2]) : 20); + + fread(ttf_buffer, 1, 1<<25, fopen(argc > 3 ? argv[3] : "c:/windows/fonts/arialbd.ttf", "rb")); + + stbtt_InitFont(&font, ttf_buffer, stbtt_GetFontOffsetForIndex(ttf_buffer,0)); + bitmap = stbtt_GetCodepointBitmap(&font, 0,stbtt_ScaleForPixelHeight(&font, s), c, &w, &h, 0,0); + + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) + putchar(" .:ioVM@"[bitmap[j*w+i]>>5]); + putchar('\n'); + } + return 0; +} +#endif +// +// Output: +// +// .ii. +// @@@@@@. +// V@Mio@@o +// :i. V@V +// :oM@@M +// :@@@MM@M +// @@o o@M +// :@@. M@M +// @@@o@@@@ +// :M@@V:@@. +// +////////////////////////////////////////////////////////////////////////////// +// +// Complete program: print "Hello World!" banner, with bugs +// +#if 0 +char buffer[24<<20]; +unsigned char screen[20][79]; + +int main(int arg, char **argv) +{ + stbtt_fontinfo font; + int i,j,ascent,baseline,ch=0; + float scale, xpos=2; // leave a little padding in case the character extends left + char *text = "Heljo World!"; // intentionally misspelled to show 'lj' brokenness + + fread(buffer, 1, 1000000, fopen("c:/windows/fonts/arialbd.ttf", "rb")); + stbtt_InitFont(&font, buffer, 0); + + scale = stbtt_ScaleForPixelHeight(&font, 15); + stbtt_GetFontVMetrics(&font, &ascent,0,0); + baseline = (int) (ascent*scale); + + while (text[ch]) { + int advance,lsb,x0,y0,x1,y1; + float x_shift = xpos - (float) floor(xpos); + stbtt_GetCodepointHMetrics(&font, text[ch], &advance, &lsb); + stbtt_GetCodepointBitmapBoxSubpixel(&font, text[ch], scale,scale,x_shift,0, &x0,&y0,&x1,&y1); + stbtt_MakeCodepointBitmapSubpixel(&font, &screen[baseline + y0][(int) xpos + x0], x1-x0,y1-y0, 79, scale,scale,x_shift,0, text[ch]); + // note that this stomps the old data, so where character boxes overlap (e.g. 'lj') it's wrong + // because this API is really for baking character bitmaps into textures. if you want to render + // a sequence of characters, you really need to render each bitmap to a temp buffer, then + // "alpha blend" that into the working buffer + xpos += (advance * scale); + if (text[ch+1]) + xpos += scale*stbtt_GetCodepointKernAdvance(&font, text[ch],text[ch+1]); + ++ch; + } + + for (j=0; j < 20; ++j) { + for (i=0; i < 78; ++i) + putchar(" .:ioVM@"[screen[j][i]>>5]); + putchar('\n'); + } + + return 0; +} +#endif + + +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// +//// +//// INTEGRATION WITH YOUR CODEBASE +//// +//// The following sections allow you to supply alternate definitions +//// of C library functions used by stb_truetype, e.g. if you don't +//// link with the C runtime library. + +#ifdef STB_TRUETYPE_IMPLEMENTATION + // #define your own (u)stbtt_int8/16/32 before including to override this + #ifndef stbtt_uint8 + typedef unsigned char stbtt_uint8; + typedef signed char stbtt_int8; + typedef unsigned short stbtt_uint16; + typedef signed short stbtt_int16; + typedef unsigned int stbtt_uint32; + typedef signed int stbtt_int32; + #endif + + typedef char stbtt__check_size32[sizeof(stbtt_int32)==4 ? 1 : -1]; + typedef char stbtt__check_size16[sizeof(stbtt_int16)==2 ? 1 : -1]; + + // e.g. #define your own STBTT_ifloor/STBTT_iceil() to avoid math.h + #ifndef STBTT_ifloor + #include + #define STBTT_ifloor(x) ((int) floor(x)) + #define STBTT_iceil(x) ((int) ceil(x)) + #endif + + #ifndef STBTT_sqrt + #include + #define STBTT_sqrt(x) sqrt(x) + #define STBTT_pow(x,y) pow(x,y) + #endif + + #ifndef STBTT_fmod + #include + #define STBTT_fmod(x,y) fmod(x,y) + #endif + + #ifndef STBTT_cos + #include + #define STBTT_cos(x) cos(x) + #define STBTT_acos(x) acos(x) + #endif + + #ifndef STBTT_fabs + #include + #define STBTT_fabs(x) fabs(x) + #endif + + // #define your own functions "STBTT_malloc" / "STBTT_free" to avoid malloc.h + #ifndef STBTT_malloc + #include + #define STBTT_malloc(x,u) ((void)(u),malloc(x)) + #define STBTT_free(x,u) ((void)(u),free(x)) + #endif + + #ifndef STBTT_assert + #include + #define STBTT_assert(x) assert(x) + #endif + + #ifndef STBTT_strlen + #include + #define STBTT_strlen(x) strlen(x) + #endif + + #ifndef STBTT_memcpy + #include + #define STBTT_memcpy memcpy + #define STBTT_memset memset + #endif +#endif + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// INTERFACE +//// +//// + +#ifndef __STB_INCLUDE_STB_TRUETYPE_H__ +#define __STB_INCLUDE_STB_TRUETYPE_H__ + +#ifdef STBTT_STATIC +#define STBTT_DEF static +#else +#define STBTT_DEF extern +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// private structure +typedef struct +{ + unsigned char *data; + int cursor; + int size; +} stbtt__buf; + +////////////////////////////////////////////////////////////////////////////// +// +// TEXTURE BAKING API +// +// If you use this API, you only have to call two functions ever. +// + +typedef struct +{ + unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap + float xoff,yoff,xadvance; +} stbtt_bakedchar; + +STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata); // you allocate this, it's num_chars long +// if return is positive, the first unused row of the bitmap +// if return is negative, returns the negative of the number of characters that fit +// if return is 0, no characters fit and no rows were used +// This uses a very crappy packing. + +typedef struct +{ + float x0,y0,s0,t0; // top-left + float x1,y1,s1,t1; // bottom-right +} stbtt_aligned_quad; + +STBTT_DEF void stbtt_GetBakedQuad(const stbtt_bakedchar *chardata, int pw, int ph, // same data as above + int char_index, // character to display + float *xpos, float *ypos, // pointers to current position in screen pixel space + stbtt_aligned_quad *q, // output: quad to draw + int opengl_fillrule); // true if opengl fill rule; false if DX9 or earlier +// Call GetBakedQuad with char_index = 'character - first_char', and it +// creates the quad you need to draw and advances the current position. +// +// The coordinate system used assumes y increases downwards. +// +// Characters will extend both above and below the current position; +// see discussion of "BASELINE" above. +// +// It's inefficient; you might want to c&p it and optimize it. + +STBTT_DEF void stbtt_GetScaledFontVMetrics(const unsigned char *fontdata, int index, float size, float *ascent, float *descent, float *lineGap); +// Query the font vertical metrics without having to create a font first. + + +////////////////////////////////////////////////////////////////////////////// +// +// NEW TEXTURE BAKING API +// +// This provides options for packing multiple fonts into one atlas, not +// perfectly but better than nothing. + +typedef struct +{ + unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap + float xoff,yoff,xadvance; + float xoff2,yoff2; +} stbtt_packedchar; + +typedef struct stbtt_pack_context stbtt_pack_context; +typedef struct stbtt_fontinfo stbtt_fontinfo; +#ifndef STB_RECT_PACK_VERSION +typedef struct stbrp_rect stbrp_rect; +#endif + +STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, int width, int height, int stride_in_bytes, int padding, void *alloc_context); +// Initializes a packing context stored in the passed-in stbtt_pack_context. +// Future calls using this context will pack characters into the bitmap passed +// in here: a 1-channel bitmap that is width * height. stride_in_bytes is +// the distance from one row to the next (or 0 to mean they are packed tightly +// together). "padding" is the amount of padding to leave between each +// character (normally you want '1' for bitmaps you'll use as textures with +// bilinear filtering). +// +// Returns 0 on failure, 1 on success. + +STBTT_DEF void stbtt_PackEnd (stbtt_pack_context *spc); +// Cleans up the packing context and frees all memory. + +#define STBTT_POINT_SIZE(x) (-(x)) + +STBTT_DEF int stbtt_PackFontRange(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, float font_size, + int first_unicode_char_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range); +// Creates character bitmaps from the font_index'th font found in fontdata (use +// font_index=0 if you don't know what that is). It creates num_chars_in_range +// bitmaps for characters with unicode values starting at first_unicode_char_in_range +// and increasing. Data for how to render them is stored in chardata_for_range; +// pass these to stbtt_GetPackedQuad to get back renderable quads. +// +// font_size is the full height of the character from ascender to descender, +// as computed by stbtt_ScaleForPixelHeight. To use a point size as computed +// by stbtt_ScaleForMappingEmToPixels, wrap the point size in STBTT_POINT_SIZE() +// and pass that result as 'font_size': +// ..., 20 , ... // font max minus min y is 20 pixels tall +// ..., STBTT_POINT_SIZE(20), ... // 'M' is 20 pixels tall + +typedef struct +{ + float font_size; + int first_unicode_codepoint_in_range; // if non-zero, then the chars are continuous, and this is the first codepoint + int *array_of_unicode_codepoints; // if non-zero, then this is an array of unicode codepoints + int num_chars; + stbtt_packedchar *chardata_for_range; // output + unsigned char h_oversample, v_oversample; // don't set these, they're used internally +} stbtt_pack_range; + +STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges); +// Creates character bitmaps from multiple ranges of characters stored in +// ranges. This will usually create a better-packed bitmap than multiple +// calls to stbtt_PackFontRange. Note that you can call this multiple +// times within a single PackBegin/PackEnd. + +STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h_oversample, unsigned int v_oversample); +// Oversampling a font increases the quality by allowing higher-quality subpixel +// positioning, and is especially valuable at smaller text sizes. +// +// This function sets the amount of oversampling for all following calls to +// stbtt_PackFontRange(s) or stbtt_PackFontRangesGatherRects for a given +// pack context. The default (no oversampling) is achieved by h_oversample=1 +// and v_oversample=1. The total number of pixels required is +// h_oversample*v_oversample larger than the default; for example, 2x2 +// oversampling requires 4x the storage of 1x1. For best results, render +// oversampled textures with bilinear filtering. Look at the readme in +// stb/tests/oversample for information about oversampled fonts +// +// To use with PackFontRangesGather etc., you must set it before calls +// call to PackFontRangesGatherRects. + +STBTT_DEF void stbtt_PackSetSkipMissingCodepoints(stbtt_pack_context *spc, int skip); +// If skip != 0, this tells stb_truetype to skip any codepoints for which +// there is no corresponding glyph. If skip=0, which is the default, then +// codepoints without a glyph recived the font's "missing character" glyph, +// typically an empty box by convention. + +STBTT_DEF void stbtt_GetPackedQuad(const stbtt_packedchar *chardata, int pw, int ph, // same data as above + int char_index, // character to display + float *xpos, float *ypos, // pointers to current position in screen pixel space + stbtt_aligned_quad *q, // output: quad to draw + int align_to_integer); + +STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); +STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect *rects, int num_rects); +STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); +// Calling these functions in sequence is roughly equivalent to calling +// stbtt_PackFontRanges(). If you more control over the packing of multiple +// fonts, or if you want to pack custom data into a font texture, take a look +// at the source to of stbtt_PackFontRanges() and create a custom version +// using these functions, e.g. call GatherRects multiple times, +// building up a single array of rects, then call PackRects once, +// then call RenderIntoRects repeatedly. This may result in a +// better packing than calling PackFontRanges multiple times +// (or it may not). + +// this is an opaque structure that you shouldn't mess with which holds +// all the context needed from PackBegin to PackEnd. +struct stbtt_pack_context { + void *user_allocator_context; + void *pack_info; + int width; + int height; + int stride_in_bytes; + int padding; + int skip_missing; + unsigned int h_oversample, v_oversample; + unsigned char *pixels; + void *nodes; +}; + +////////////////////////////////////////////////////////////////////////////// +// +// FONT LOADING +// +// + +STBTT_DEF int stbtt_GetNumberOfFonts(const unsigned char *data); +// This function will determine the number of fonts in a font file. TrueType +// collection (.ttc) files may contain multiple fonts, while TrueType font +// (.ttf) files only contain one font. The number of fonts can be used for +// indexing with the previous function where the index is between zero and one +// less than the total fonts. If an error occurs, -1 is returned. + +STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index); +// Each .ttf/.ttc file may have more than one font. Each font has a sequential +// index number starting from 0. Call this function to get the font offset for +// a given index; it returns -1 if the index is out of range. A regular .ttf +// file will only define one font and it always be at offset 0, so it will +// return '0' for index 0, and -1 for all other indices. + +// The following structure is defined publicly so you can declare one on +// the stack or as a global or etc, but you should treat it as opaque. +struct stbtt_fontinfo +{ + void * userdata; + unsigned char * data; // pointer to .ttf file + int fontstart; // offset of start of font + + int numGlyphs; // number of glyphs, needed for range checking + + int loca,head,glyf,hhea,hmtx,kern,gpos; // table locations as offset from start of .ttf + int index_map; // a cmap mapping for our chosen character encoding + int indexToLocFormat; // format needed to map from glyph index to glyph + + stbtt__buf cff; // cff font data + stbtt__buf charstrings; // the charstring index + stbtt__buf gsubrs; // global charstring subroutines index + stbtt__buf subrs; // private charstring subroutines index + stbtt__buf fontdicts; // array of font dicts + stbtt__buf fdselect; // map from glyph to fontdict +}; + +STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data, int offset); +// Given an offset into the file that defines a font, this function builds +// the necessary cached info for the rest of the system. You must allocate +// the stbtt_fontinfo yourself, and stbtt_InitFont will fill it out. You don't +// need to do anything special to free it, because the contents are pure +// value data with no additional data structures. Returns 0 on failure. + + +////////////////////////////////////////////////////////////////////////////// +// +// CHARACTER TO GLYPH-INDEX CONVERSIOn + +STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint); +// If you're going to perform multiple operations on the same character +// and you want a speed-up, call this function with the character you're +// going to process, then use glyph-based functions instead of the +// codepoint-based functions. +// Returns 0 if the character codepoint is not defined in the font. + + +////////////////////////////////////////////////////////////////////////////// +// +// CHARACTER PROPERTIES +// + +STBTT_DEF float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float pixels); +// computes a scale factor to produce a font whose "height" is 'pixels' tall. +// Height is measured as the distance from the highest ascender to the lowest +// descender; in other words, it's equivalent to calling stbtt_GetFontVMetrics +// and computing: +// scale = pixels / (ascent - descent) +// so if you prefer to measure height by the ascent only, use a similar calculation. + +STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels); +// computes a scale factor to produce a font whose EM size is mapped to +// 'pixels' tall. This is probably what traditional APIs compute, but +// I'm not positive. + +STBTT_DEF void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap); +// ascent is the coordinate above the baseline the font extends; descent +// is the coordinate below the baseline the font extends (i.e. it is typically negative) +// lineGap is the spacing between one row's descent and the next row's ascent... +// so you should advance the vertical position by "*ascent - *descent + *lineGap" +// these are expressed in unscaled coordinates, so you must multiply by +// the scale factor for a given size + +STBTT_DEF int stbtt_GetFontVMetricsOS2(const stbtt_fontinfo *info, int *typoAscent, int *typoDescent, int *typoLineGap); +// analogous to GetFontVMetrics, but returns the "typographic" values from the OS/2 +// table (specific to MS/Windows TTF files). +// +// Returns 1 on success (table present), 0 on failure. + +STBTT_DEF void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1); +// the bounding box around all possible characters + +STBTT_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing); +// leftSideBearing is the offset from the current horizontal position to the left edge of the character +// advanceWidth is the offset from the current horizontal position to the next horizontal position +// these are expressed in unscaled coordinates + +STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2); +// an additional amount to add to the 'advance' value between ch1 and ch2 + +STBTT_DEF int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1); +// Gets the bounding box of the visible part of the glyph, in unscaled coordinates + +STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing); +STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2); +STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1); +// as above, but takes one or more glyph indices for greater efficiency + + +////////////////////////////////////////////////////////////////////////////// +// +// GLYPH SHAPES (you probably don't need these, but they have to go before +// the bitmaps for C declaration-order reasons) +// + +#ifndef STBTT_vmove // you can predefine these to use different values (but why?) + enum { + STBTT_vmove=1, + STBTT_vline, + STBTT_vcurve, + STBTT_vcubic + }; +#endif + +#ifndef stbtt_vertex // you can predefine this to use different values + // (we share this with other code at RAD) + #define stbtt_vertex_type short // can't use stbtt_int16 because that's not visible in the header file + typedef struct + { + stbtt_vertex_type x,y,cx,cy,cx1,cy1; + unsigned char type,padding; + } stbtt_vertex; +#endif + +STBTT_DEF int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index); +// returns non-zero if nothing is drawn for this glyph + +STBTT_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices); +STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **vertices); +// returns # of vertices and fills *vertices with the pointer to them +// these are expressed in "unscaled" coordinates +// +// The shape is a series of contours. Each one starts with +// a STBTT_moveto, then consists of a series of mixed +// STBTT_lineto and STBTT_curveto segments. A lineto +// draws a line from previous endpoint to its x,y; a curveto +// draws a quadratic bezier from previous endpoint to +// its x,y, using cx,cy as the bezier control point. + +STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *vertices); +// frees the data allocated above + +////////////////////////////////////////////////////////////////////////////// +// +// BITMAP RENDERING +// + +STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata); +// frees the bitmap allocated below + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff); +// allocates a large-enough single-channel 8bpp bitmap and renders the +// specified character/glyph at the specified scale into it, with +// antialiasing. 0 is no coverage (transparent), 255 is fully covered (opaque). +// *width & *height are filled out with the width & height of the bitmap, +// which is stored left-to-right, top-to-bottom. +// +// xoff/yoff are the offset it pixel space from the glyph origin to the top-left of the bitmap + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff); +// the same as stbtt_GetCodepoitnBitmap, but you can specify a subpixel +// shift for the character + +STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint); +// the same as stbtt_GetCodepointBitmap, but you pass in storage for the bitmap +// in the form of 'output', with row spacing of 'out_stride' bytes. the bitmap +// is clipped to out_w/out_h bytes. Call stbtt_GetCodepointBitmapBox to get the +// width and height and positioning info for it first. + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint); +// same as stbtt_MakeCodepointBitmap, but you can specify a subpixel +// shift for the character + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int codepoint); +// same as stbtt_MakeCodepointBitmapSubpixel, but prefiltering +// is performed (see stbtt_PackSetOversampling) + +STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); +// get the bbox of the bitmap centered around the glyph origin; so the +// bitmap width is ix1-ix0, height is iy1-iy0, and location to place +// the bitmap top left is (leftSideBearing*scale,iy0). +// (Note that the bitmap uses y-increases-down, but the shape uses +// y-increases-up, so CodepointBitmapBox and CodepointBox are inverted.) + +STBTT_DEF void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); +// same as stbtt_GetCodepointBitmapBox, but you can specify a subpixel +// shift for the character + +// the following functions are equivalent to the above functions, but operate +// on glyph indices instead of Unicode codepoints (for efficiency) +STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph); +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph); +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int glyph); +STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); +STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); + + +// @TODO: don't expose this structure +typedef struct +{ + int w,h,stride; + unsigned char *pixels; +} stbtt__bitmap; + +// rasterize a shape with quadratic beziers into a bitmap +STBTT_DEF void stbtt_Rasterize(stbtt__bitmap *result, // 1-channel bitmap to draw into + float flatness_in_pixels, // allowable error of curve in pixels + stbtt_vertex *vertices, // array of vertices defining shape + int num_verts, // number of vertices in above array + float scale_x, float scale_y, // scale applied to input vertices + float shift_x, float shift_y, // translation applied to input vertices + int x_off, int y_off, // another translation applied to input + int invert, // if non-zero, vertically flip shape + void *userdata); // context for to STBTT_MALLOC + +////////////////////////////////////////////////////////////////////////////// +// +// Signed Distance Function (or Field) rendering + +STBTT_DEF void stbtt_FreeSDF(unsigned char *bitmap, void *userdata); +// frees the SDF bitmap allocated below + +STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float scale, int glyph, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, float scale, int codepoint, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff); +// These functions compute a discretized SDF field for a single character, suitable for storing +// in a single-channel texture, sampling with bilinear filtering, and testing against +// larger than some threshold to produce scalable fonts. +// info -- the font +// scale -- controls the size of the resulting SDF bitmap, same as it would be creating a regular bitmap +// glyph/codepoint -- the character to generate the SDF for +// padding -- extra "pixels" around the character which are filled with the distance to the character (not 0), +// which allows effects like bit outlines +// onedge_value -- value 0-255 to test the SDF against to reconstruct the character (i.e. the isocontour of the character) +// pixel_dist_scale -- what value the SDF should increase by when moving one SDF "pixel" away from the edge (on the 0..255 scale) +// if positive, > onedge_value is inside; if negative, < onedge_value is inside +// width,height -- output height & width of the SDF bitmap (including padding) +// xoff,yoff -- output origin of the character +// return value -- a 2D array of bytes 0..255, width*height in size +// +// pixel_dist_scale & onedge_value are a scale & bias that allows you to make +// optimal use of the limited 0..255 for your application, trading off precision +// and special effects. SDF values outside the range 0..255 are clamped to 0..255. +// +// Example: +// scale = stbtt_ScaleForPixelHeight(22) +// padding = 5 +// onedge_value = 180 +// pixel_dist_scale = 180/5.0 = 36.0 +// +// This will create an SDF bitmap in which the character is about 22 pixels +// high but the whole bitmap is about 22+5+5=32 pixels high. To produce a filled +// shape, sample the SDF at each pixel and fill the pixel if the SDF value +// is greater than or equal to 180/255. (You'll actually want to antialias, +// which is beyond the scope of this example.) Additionally, you can compute +// offset outlines (e.g. to stroke the character border inside & outside, +// or only outside). For example, to fill outside the character up to 3 SDF +// pixels, you would compare against (180-36.0*3)/255 = 72/255. The above +// choice of variables maps a range from 5 pixels outside the shape to +// 2 pixels inside the shape to 0..255; this is intended primarily for apply +// outside effects only (the interior range is needed to allow proper +// antialiasing of the font at *smaller* sizes) +// +// The function computes the SDF analytically at each SDF pixel, not by e.g. +// building a higher-res bitmap and approximating it. In theory the quality +// should be as high as possible for an SDF of this size & representation, but +// unclear if this is true in practice (perhaps building a higher-res bitmap +// and computing from that can allow drop-out prevention). +// +// The algorithm has not been optimized at all, so expect it to be slow +// if computing lots of characters or very large sizes. + + + +////////////////////////////////////////////////////////////////////////////// +// +// Finding the right font... +// +// You should really just solve this offline, keep your own tables +// of what font is what, and don't try to get it out of the .ttf file. +// That's because getting it out of the .ttf file is really hard, because +// the names in the file can appear in many possible encodings, in many +// possible languages, and e.g. if you need a case-insensitive comparison, +// the details of that depend on the encoding & language in a complex way +// (actually underspecified in truetype, but also gigantic). +// +// But you can use the provided functions in two possible ways: +// stbtt_FindMatchingFont() will use *case-sensitive* comparisons on +// unicode-encoded names to try to find the font you want; +// you can run this before calling stbtt_InitFont() +// +// stbtt_GetFontNameString() lets you get any of the various strings +// from the file yourself and do your own comparisons on them. +// You have to have called stbtt_InitFont() first. + + +STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *fontdata, const char *name, int flags); +// returns the offset (not index) of the font that matches, or -1 if none +// if you use STBTT_MACSTYLE_DONTCARE, use a font name like "Arial Bold". +// if you use any other flag, use a font name like "Arial"; this checks +// the 'macStyle' header field; i don't know if fonts set this consistently +#define STBTT_MACSTYLE_DONTCARE 0 +#define STBTT_MACSTYLE_BOLD 1 +#define STBTT_MACSTYLE_ITALIC 2 +#define STBTT_MACSTYLE_UNDERSCORE 4 +#define STBTT_MACSTYLE_NONE 8 // <= not same as 0, this makes us check the bitfield is 0 + +STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2); +// returns 1/0 whether the first string interpreted as utf8 is identical to +// the second string interpreted as big-endian utf16... useful for strings from next func + +STBTT_DEF const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID); +// returns the string (which may be big-endian double byte, e.g. for unicode) +// and puts the length in bytes in *length. +// +// some of the values for the IDs are below; for more see the truetype spec: +// http://developer.apple.com/textfonts/TTRefMan/RM06/Chap6name.html +// http://www.microsoft.com/typography/otspec/name.htm + +enum { // platformID + STBTT_PLATFORM_ID_UNICODE =0, + STBTT_PLATFORM_ID_MAC =1, + STBTT_PLATFORM_ID_ISO =2, + STBTT_PLATFORM_ID_MICROSOFT =3 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_UNICODE + STBTT_UNICODE_EID_UNICODE_1_0 =0, + STBTT_UNICODE_EID_UNICODE_1_1 =1, + STBTT_UNICODE_EID_ISO_10646 =2, + STBTT_UNICODE_EID_UNICODE_2_0_BMP=3, + STBTT_UNICODE_EID_UNICODE_2_0_FULL=4 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_MICROSOFT + STBTT_MS_EID_SYMBOL =0, + STBTT_MS_EID_UNICODE_BMP =1, + STBTT_MS_EID_SHIFTJIS =2, + STBTT_MS_EID_UNICODE_FULL =10 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_MAC; same as Script Manager codes + STBTT_MAC_EID_ROMAN =0, STBTT_MAC_EID_ARABIC =4, + STBTT_MAC_EID_JAPANESE =1, STBTT_MAC_EID_HEBREW =5, + STBTT_MAC_EID_CHINESE_TRAD =2, STBTT_MAC_EID_GREEK =6, + STBTT_MAC_EID_KOREAN =3, STBTT_MAC_EID_RUSSIAN =7 +}; + +enum { // languageID for STBTT_PLATFORM_ID_MICROSOFT; same as LCID... + // problematic because there are e.g. 16 english LCIDs and 16 arabic LCIDs + STBTT_MS_LANG_ENGLISH =0x0409, STBTT_MS_LANG_ITALIAN =0x0410, + STBTT_MS_LANG_CHINESE =0x0804, STBTT_MS_LANG_JAPANESE =0x0411, + STBTT_MS_LANG_DUTCH =0x0413, STBTT_MS_LANG_KOREAN =0x0412, + STBTT_MS_LANG_FRENCH =0x040c, STBTT_MS_LANG_RUSSIAN =0x0419, + STBTT_MS_LANG_GERMAN =0x0407, STBTT_MS_LANG_SPANISH =0x0409, + STBTT_MS_LANG_HEBREW =0x040d, STBTT_MS_LANG_SWEDISH =0x041D +}; + +enum { // languageID for STBTT_PLATFORM_ID_MAC + STBTT_MAC_LANG_ENGLISH =0 , STBTT_MAC_LANG_JAPANESE =11, + STBTT_MAC_LANG_ARABIC =12, STBTT_MAC_LANG_KOREAN =23, + STBTT_MAC_LANG_DUTCH =4 , STBTT_MAC_LANG_RUSSIAN =32, + STBTT_MAC_LANG_FRENCH =1 , STBTT_MAC_LANG_SPANISH =6 , + STBTT_MAC_LANG_GERMAN =2 , STBTT_MAC_LANG_SWEDISH =5 , + STBTT_MAC_LANG_HEBREW =10, STBTT_MAC_LANG_CHINESE_SIMPLIFIED =33, + STBTT_MAC_LANG_ITALIAN =3 , STBTT_MAC_LANG_CHINESE_TRAD =19 +}; + +#ifdef __cplusplus +} +#endif + +#endif // __STB_INCLUDE_STB_TRUETYPE_H__ + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// IMPLEMENTATION +//// +//// + +#ifdef STB_TRUETYPE_IMPLEMENTATION + +#ifndef STBTT_MAX_OVERSAMPLE +#define STBTT_MAX_OVERSAMPLE 8 +#endif + +#if STBTT_MAX_OVERSAMPLE > 255 +#error "STBTT_MAX_OVERSAMPLE cannot be > 255" +#endif + +typedef int stbtt__test_oversample_pow2[(STBTT_MAX_OVERSAMPLE & (STBTT_MAX_OVERSAMPLE-1)) == 0 ? 1 : -1]; + +#ifndef STBTT_RASTERIZER_VERSION +#define STBTT_RASTERIZER_VERSION 2 +#endif + +#ifdef _MSC_VER +#define STBTT__NOTUSED(v) (void)(v) +#else +#define STBTT__NOTUSED(v) (void)sizeof(v) +#endif + +////////////////////////////////////////////////////////////////////////// +// +// stbtt__buf helpers to parse data from file +// + +static stbtt_uint8 stbtt__buf_get8(stbtt__buf *b) +{ + if (b->cursor >= b->size) + return 0; + return b->data[b->cursor++]; +} + +static stbtt_uint8 stbtt__buf_peek8(stbtt__buf *b) +{ + if (b->cursor >= b->size) + return 0; + return b->data[b->cursor]; +} + +static void stbtt__buf_seek(stbtt__buf *b, int o) +{ + STBTT_assert(!(o > b->size || o < 0)); + b->cursor = (o > b->size || o < 0) ? b->size : o; +} + +static void stbtt__buf_skip(stbtt__buf *b, int o) +{ + stbtt__buf_seek(b, b->cursor + o); +} + +static stbtt_uint32 stbtt__buf_get(stbtt__buf *b, int n) +{ + stbtt_uint32 v = 0; + int i; + STBTT_assert(n >= 1 && n <= 4); + for (i = 0; i < n; i++) + v = (v << 8) | stbtt__buf_get8(b); + return v; +} + +static stbtt__buf stbtt__new_buf(const void *p, size_t size) +{ + stbtt__buf r; + STBTT_assert(size < 0x40000000); + r.data = (stbtt_uint8*) p; + r.size = (int) size; + r.cursor = 0; + return r; +} + +#define stbtt__buf_get16(b) stbtt__buf_get((b), 2) +#define stbtt__buf_get32(b) stbtt__buf_get((b), 4) + +static stbtt__buf stbtt__buf_range(const stbtt__buf *b, int o, int s) +{ + stbtt__buf r = stbtt__new_buf(NULL, 0); + if (o < 0 || s < 0 || o > b->size || s > b->size - o) return r; + r.data = b->data + o; + r.size = s; + return r; +} + +static stbtt__buf stbtt__cff_get_index(stbtt__buf *b) +{ + int count, start, offsize; + start = b->cursor; + count = stbtt__buf_get16(b); + if (count) { + offsize = stbtt__buf_get8(b); + STBTT_assert(offsize >= 1 && offsize <= 4); + stbtt__buf_skip(b, offsize * count); + stbtt__buf_skip(b, stbtt__buf_get(b, offsize) - 1); + } + return stbtt__buf_range(b, start, b->cursor - start); +} + +static stbtt_uint32 stbtt__cff_int(stbtt__buf *b) +{ + int b0 = stbtt__buf_get8(b); + if (b0 >= 32 && b0 <= 246) return b0 - 139; + else if (b0 >= 247 && b0 <= 250) return (b0 - 247)*256 + stbtt__buf_get8(b) + 108; + else if (b0 >= 251 && b0 <= 254) return -(b0 - 251)*256 - stbtt__buf_get8(b) - 108; + else if (b0 == 28) return stbtt__buf_get16(b); + else if (b0 == 29) return stbtt__buf_get32(b); + STBTT_assert(0); + return 0; +} + +static void stbtt__cff_skip_operand(stbtt__buf *b) { + int v, b0 = stbtt__buf_peek8(b); + STBTT_assert(b0 >= 28); + if (b0 == 30) { + stbtt__buf_skip(b, 1); + while (b->cursor < b->size) { + v = stbtt__buf_get8(b); + if ((v & 0xF) == 0xF || (v >> 4) == 0xF) + break; + } + } else { + stbtt__cff_int(b); + } +} + +static stbtt__buf stbtt__dict_get(stbtt__buf *b, int key) +{ + stbtt__buf_seek(b, 0); + while (b->cursor < b->size) { + int start = b->cursor, end, op; + while (stbtt__buf_peek8(b) >= 28) + stbtt__cff_skip_operand(b); + end = b->cursor; + op = stbtt__buf_get8(b); + if (op == 12) op = stbtt__buf_get8(b) | 0x100; + if (op == key) return stbtt__buf_range(b, start, end-start); + } + return stbtt__buf_range(b, 0, 0); +} + +static void stbtt__dict_get_ints(stbtt__buf *b, int key, int outcount, stbtt_uint32 *out) +{ + int i; + stbtt__buf operands = stbtt__dict_get(b, key); + for (i = 0; i < outcount && operands.cursor < operands.size; i++) + out[i] = stbtt__cff_int(&operands); +} + +static int stbtt__cff_index_count(stbtt__buf *b) +{ + stbtt__buf_seek(b, 0); + return stbtt__buf_get16(b); +} + +static stbtt__buf stbtt__cff_index_get(stbtt__buf b, int i) +{ + int count, offsize, start, end; + stbtt__buf_seek(&b, 0); + count = stbtt__buf_get16(&b); + offsize = stbtt__buf_get8(&b); + STBTT_assert(i >= 0 && i < count); + STBTT_assert(offsize >= 1 && offsize <= 4); + stbtt__buf_skip(&b, i*offsize); + start = stbtt__buf_get(&b, offsize); + end = stbtt__buf_get(&b, offsize); + return stbtt__buf_range(&b, 2+(count+1)*offsize+start, end - start); +} + +////////////////////////////////////////////////////////////////////////// +// +// accessors to parse data from file +// + +// on platforms that don't allow misaligned reads, if we want to allow +// truetype fonts that aren't padded to alignment, define ALLOW_UNALIGNED_TRUETYPE + +#define ttBYTE(p) (* (stbtt_uint8 *) (p)) +#define ttCHAR(p) (* (stbtt_int8 *) (p)) +#define ttFixed(p) ttLONG(p) + +static stbtt_uint16 ttUSHORT(stbtt_uint8 *p) { return p[0]*256 + p[1]; } +static stbtt_int16 ttSHORT(stbtt_uint8 *p) { return p[0]*256 + p[1]; } +static stbtt_uint32 ttULONG(stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } +static stbtt_int32 ttLONG(stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } + +#define stbtt_tag4(p,c0,c1,c2,c3) ((p)[0] == (c0) && (p)[1] == (c1) && (p)[2] == (c2) && (p)[3] == (c3)) +#define stbtt_tag(p,str) stbtt_tag4(p,str[0],str[1],str[2],str[3]) + +static int stbtt__isfont(stbtt_uint8 *font) +{ + // check the version number + if (stbtt_tag4(font, '1',0,0,0)) return 1; // TrueType 1 + if (stbtt_tag(font, "typ1")) return 1; // TrueType with type 1 font -- we don't support this! + if (stbtt_tag(font, "OTTO")) return 1; // OpenType with CFF + if (stbtt_tag4(font, 0,1,0,0)) return 1; // OpenType 1.0 + if (stbtt_tag(font, "true")) return 1; // Apple specification for TrueType fonts + return 0; +} + +// @OPTIMIZE: binary search +static stbtt_uint32 stbtt__find_table(stbtt_uint8 *data, stbtt_uint32 fontstart, const char *tag) +{ + stbtt_int32 num_tables = ttUSHORT(data+fontstart+4); + stbtt_uint32 tabledir = fontstart + 12; + stbtt_int32 i; + for (i=0; i < num_tables; ++i) { + stbtt_uint32 loc = tabledir + 16*i; + if (stbtt_tag(data+loc+0, tag)) + return ttULONG(data+loc+8); + } + return 0; +} + +static int stbtt_GetFontOffsetForIndex_internal(unsigned char *font_collection, int index) +{ + // if it's just a font, there's only one valid index + if (stbtt__isfont(font_collection)) + return index == 0 ? 0 : -1; + + // check if it's a TTC + if (stbtt_tag(font_collection, "ttcf")) { + // version 1? + if (ttULONG(font_collection+4) == 0x00010000 || ttULONG(font_collection+4) == 0x00020000) { + stbtt_int32 n = ttLONG(font_collection+8); + if (index >= n) + return -1; + return ttULONG(font_collection+12+index*4); + } + } + return -1; +} + +static int stbtt_GetNumberOfFonts_internal(unsigned char *font_collection) +{ + // if it's just a font, there's only one valid font + if (stbtt__isfont(font_collection)) + return 1; + + // check if it's a TTC + if (stbtt_tag(font_collection, "ttcf")) { + // version 1? + if (ttULONG(font_collection+4) == 0x00010000 || ttULONG(font_collection+4) == 0x00020000) { + return ttLONG(font_collection+8); + } + } + return 0; +} + +static stbtt__buf stbtt__get_subrs(stbtt__buf cff, stbtt__buf fontdict) +{ + stbtt_uint32 subrsoff = 0, private_loc[2] = { 0, 0 }; + stbtt__buf pdict; + stbtt__dict_get_ints(&fontdict, 18, 2, private_loc); + if (!private_loc[1] || !private_loc[0]) return stbtt__new_buf(NULL, 0); + pdict = stbtt__buf_range(&cff, private_loc[1], private_loc[0]); + stbtt__dict_get_ints(&pdict, 19, 1, &subrsoff); + if (!subrsoff) return stbtt__new_buf(NULL, 0); + stbtt__buf_seek(&cff, private_loc[1]+subrsoff); + return stbtt__cff_get_index(&cff); +} + +static int stbtt_InitFont_internal(stbtt_fontinfo *info, unsigned char *data, int fontstart) +{ + stbtt_uint32 cmap, t; + stbtt_int32 i,numTables; + + info->data = data; + info->fontstart = fontstart; + info->cff = stbtt__new_buf(NULL, 0); + + cmap = stbtt__find_table(data, fontstart, "cmap"); // required + info->loca = stbtt__find_table(data, fontstart, "loca"); // required + info->head = stbtt__find_table(data, fontstart, "head"); // required + info->glyf = stbtt__find_table(data, fontstart, "glyf"); // required + info->hhea = stbtt__find_table(data, fontstart, "hhea"); // required + info->hmtx = stbtt__find_table(data, fontstart, "hmtx"); // required + info->kern = stbtt__find_table(data, fontstart, "kern"); // not required + info->gpos = stbtt__find_table(data, fontstart, "GPOS"); // not required + + if (!cmap || !info->head || !info->hhea || !info->hmtx) + return 0; + if (info->glyf) { + // required for truetype + if (!info->loca) return 0; + } else { + // initialization for CFF / Type2 fonts (OTF) + stbtt__buf b, topdict, topdictidx; + stbtt_uint32 cstype = 2, charstrings = 0, fdarrayoff = 0, fdselectoff = 0; + stbtt_uint32 cff; + + cff = stbtt__find_table(data, fontstart, "CFF "); + if (!cff) return 0; + + info->fontdicts = stbtt__new_buf(NULL, 0); + info->fdselect = stbtt__new_buf(NULL, 0); + + // @TODO this should use size from table (not 512MB) + info->cff = stbtt__new_buf(data+cff, 512*1024*1024); + b = info->cff; + + // read the header + stbtt__buf_skip(&b, 2); + stbtt__buf_seek(&b, stbtt__buf_get8(&b)); // hdrsize + + // @TODO the name INDEX could list multiple fonts, + // but we just use the first one. + stbtt__cff_get_index(&b); // name INDEX + topdictidx = stbtt__cff_get_index(&b); + topdict = stbtt__cff_index_get(topdictidx, 0); + stbtt__cff_get_index(&b); // string INDEX + info->gsubrs = stbtt__cff_get_index(&b); + + stbtt__dict_get_ints(&topdict, 17, 1, &charstrings); + stbtt__dict_get_ints(&topdict, 0x100 | 6, 1, &cstype); + stbtt__dict_get_ints(&topdict, 0x100 | 36, 1, &fdarrayoff); + stbtt__dict_get_ints(&topdict, 0x100 | 37, 1, &fdselectoff); + info->subrs = stbtt__get_subrs(b, topdict); + + // we only support Type 2 charstrings + if (cstype != 2) return 0; + if (charstrings == 0) return 0; + + if (fdarrayoff) { + // looks like a CID font + if (!fdselectoff) return 0; + stbtt__buf_seek(&b, fdarrayoff); + info->fontdicts = stbtt__cff_get_index(&b); + info->fdselect = stbtt__buf_range(&b, fdselectoff, b.size-fdselectoff); + } + + stbtt__buf_seek(&b, charstrings); + info->charstrings = stbtt__cff_get_index(&b); + } + + t = stbtt__find_table(data, fontstart, "maxp"); + if (t) + info->numGlyphs = ttUSHORT(data+t+4); + else + info->numGlyphs = 0xffff; + + // find a cmap encoding table we understand *now* to avoid searching + // later. (todo: could make this installable) + // the same regardless of glyph. + numTables = ttUSHORT(data + cmap + 2); + info->index_map = 0; + for (i=0; i < numTables; ++i) { + stbtt_uint32 encoding_record = cmap + 4 + 8 * i; + // find an encoding we understand: + switch(ttUSHORT(data+encoding_record)) { + case STBTT_PLATFORM_ID_MICROSOFT: + switch (ttUSHORT(data+encoding_record+2)) { + case STBTT_MS_EID_UNICODE_BMP: + case STBTT_MS_EID_UNICODE_FULL: + // MS/Unicode + info->index_map = cmap + ttULONG(data+encoding_record+4); + break; + } + break; + case STBTT_PLATFORM_ID_UNICODE: + // Mac/iOS has these + // all the encodingIDs are unicode, so we don't bother to check it + info->index_map = cmap + ttULONG(data+encoding_record+4); + break; + } + } + if (info->index_map == 0) + return 0; + + info->indexToLocFormat = ttUSHORT(data+info->head + 50); + return 1; +} + +STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint) +{ + stbtt_uint8 *data = info->data; + stbtt_uint32 index_map = info->index_map; + + stbtt_uint16 format = ttUSHORT(data + index_map + 0); + if (format == 0) { // apple byte encoding + stbtt_int32 bytes = ttUSHORT(data + index_map + 2); + if (unicode_codepoint < bytes-6) + return ttBYTE(data + index_map + 6 + unicode_codepoint); + return 0; + } else if (format == 6) { + stbtt_uint32 first = ttUSHORT(data + index_map + 6); + stbtt_uint32 count = ttUSHORT(data + index_map + 8); + if ((stbtt_uint32) unicode_codepoint >= first && (stbtt_uint32) unicode_codepoint < first+count) + return ttUSHORT(data + index_map + 10 + (unicode_codepoint - first)*2); + return 0; + } else if (format == 2) { + STBTT_assert(0); // @TODO: high-byte mapping for japanese/chinese/korean + return 0; + } else if (format == 4) { // standard mapping for windows fonts: binary search collection of ranges + stbtt_uint16 segcount = ttUSHORT(data+index_map+6) >> 1; + stbtt_uint16 searchRange = ttUSHORT(data+index_map+8) >> 1; + stbtt_uint16 entrySelector = ttUSHORT(data+index_map+10); + stbtt_uint16 rangeShift = ttUSHORT(data+index_map+12) >> 1; + + // do a binary search of the segments + stbtt_uint32 endCount = index_map + 14; + stbtt_uint32 search = endCount; + + if (unicode_codepoint > 0xffff) + return 0; + + // they lie from endCount .. endCount + segCount + // but searchRange is the nearest power of two, so... + if (unicode_codepoint >= ttUSHORT(data + search + rangeShift*2)) + search += rangeShift*2; + + // now decrement to bias correctly to find smallest + search -= 2; + while (entrySelector) { + stbtt_uint16 end; + searchRange >>= 1; + end = ttUSHORT(data + search + searchRange*2); + if (unicode_codepoint > end) + search += searchRange*2; + --entrySelector; + } + search += 2; + + { + stbtt_uint16 offset, start; + stbtt_uint16 item = (stbtt_uint16) ((search - endCount) >> 1); + + STBTT_assert(unicode_codepoint <= ttUSHORT(data + endCount + 2*item)); + start = ttUSHORT(data + index_map + 14 + segcount*2 + 2 + 2*item); + if (unicode_codepoint < start) + return 0; + + offset = ttUSHORT(data + index_map + 14 + segcount*6 + 2 + 2*item); + if (offset == 0) + return (stbtt_uint16) (unicode_codepoint + ttSHORT(data + index_map + 14 + segcount*4 + 2 + 2*item)); + + return ttUSHORT(data + offset + (unicode_codepoint-start)*2 + index_map + 14 + segcount*6 + 2 + 2*item); + } + } else if (format == 12 || format == 13) { + stbtt_uint32 ngroups = ttULONG(data+index_map+12); + stbtt_int32 low,high; + low = 0; high = (stbtt_int32)ngroups; + // Binary search the right group. + while (low < high) { + stbtt_int32 mid = low + ((high-low) >> 1); // rounds down, so low <= mid < high + stbtt_uint32 start_char = ttULONG(data+index_map+16+mid*12); + stbtt_uint32 end_char = ttULONG(data+index_map+16+mid*12+4); + if ((stbtt_uint32) unicode_codepoint < start_char) + high = mid; + else if ((stbtt_uint32) unicode_codepoint > end_char) + low = mid+1; + else { + stbtt_uint32 start_glyph = ttULONG(data+index_map+16+mid*12+8); + if (format == 12) + return start_glyph + unicode_codepoint-start_char; + else // format == 13 + return start_glyph; + } + } + return 0; // not found + } + // @TODO + STBTT_assert(0); + return 0; +} + +STBTT_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices) +{ + return stbtt_GetGlyphShape(info, stbtt_FindGlyphIndex(info, unicode_codepoint), vertices); +} + +static void stbtt_setvertex(stbtt_vertex *v, stbtt_uint8 type, stbtt_int32 x, stbtt_int32 y, stbtt_int32 cx, stbtt_int32 cy) +{ + v->type = type; + v->x = (stbtt_int16) x; + v->y = (stbtt_int16) y; + v->cx = (stbtt_int16) cx; + v->cy = (stbtt_int16) cy; +} + +static int stbtt__GetGlyfOffset(const stbtt_fontinfo *info, int glyph_index) +{ + int g1,g2; + + STBTT_assert(!info->cff.size); + + if (glyph_index >= info->numGlyphs) return -1; // glyph index out of range + if (info->indexToLocFormat >= 2) return -1; // unknown index->glyph map format + + if (info->indexToLocFormat == 0) { + g1 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2) * 2; + g2 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2 + 2) * 2; + } else { + g1 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4); + g2 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4 + 4); + } + + return g1==g2 ? -1 : g1; // if length is 0, return -1 +} + +static int stbtt__GetGlyphInfoT2(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1); + +STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1) +{ + if (info->cff.size) { + stbtt__GetGlyphInfoT2(info, glyph_index, x0, y0, x1, y1); + } else { + int g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) return 0; + + if (x0) *x0 = ttSHORT(info->data + g + 2); + if (y0) *y0 = ttSHORT(info->data + g + 4); + if (x1) *x1 = ttSHORT(info->data + g + 6); + if (y1) *y1 = ttSHORT(info->data + g + 8); + } + return 1; +} + +STBTT_DEF int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1) +{ + return stbtt_GetGlyphBox(info, stbtt_FindGlyphIndex(info,codepoint), x0,y0,x1,y1); +} + +STBTT_DEF int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index) +{ + stbtt_int16 numberOfContours; + int g; + if (info->cff.size) + return stbtt__GetGlyphInfoT2(info, glyph_index, NULL, NULL, NULL, NULL) == 0; + g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) return 1; + numberOfContours = ttSHORT(info->data + g); + return numberOfContours == 0; +} + +static int stbtt__close_shape(stbtt_vertex *vertices, int num_vertices, int was_off, int start_off, + stbtt_int32 sx, stbtt_int32 sy, stbtt_int32 scx, stbtt_int32 scy, stbtt_int32 cx, stbtt_int32 cy) +{ + if (start_off) { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+scx)>>1, (cy+scy)>>1, cx,cy); + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, sx,sy,scx,scy); + } else { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve,sx,sy,cx,cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline,sx,sy,0,0); + } + return num_vertices; +} + +static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + stbtt_int16 numberOfContours; + stbtt_uint8 *endPtsOfContours; + stbtt_uint8 *data = info->data; + stbtt_vertex *vertices=0; + int num_vertices=0; + int g = stbtt__GetGlyfOffset(info, glyph_index); + + *pvertices = NULL; + + if (g < 0) return 0; + + numberOfContours = ttSHORT(data + g); + + if (numberOfContours > 0) { + stbtt_uint8 flags=0,flagcount; + stbtt_int32 ins, i,j=0,m,n, next_move, was_off=0, off, start_off=0; + stbtt_int32 x,y,cx,cy,sx,sy, scx,scy; + stbtt_uint8 *points; + endPtsOfContours = (data + g + 10); + ins = ttUSHORT(data + g + 10 + numberOfContours * 2); + points = data + g + 10 + numberOfContours * 2 + 2 + ins; + + n = 1+ttUSHORT(endPtsOfContours + numberOfContours*2-2); + + m = n + 2*numberOfContours; // a loose bound on how many vertices we might need + vertices = (stbtt_vertex *) STBTT_malloc(m * sizeof(vertices[0]), info->userdata); + if (vertices == 0) + return 0; + + next_move = 0; + flagcount=0; + + // in first pass, we load uninterpreted data into the allocated array + // above, shifted to the end of the array so we won't overwrite it when + // we create our final data starting from the front + + off = m - n; // starting offset for uninterpreted data, regardless of how m ends up being calculated + + // first load flags + + for (i=0; i < n; ++i) { + if (flagcount == 0) { + flags = *points++; + if (flags & 8) + flagcount = *points++; + } else + --flagcount; + vertices[off+i].type = flags; + } + + // now load x coordinates + x=0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + if (flags & 2) { + stbtt_int16 dx = *points++; + x += (flags & 16) ? dx : -dx; // ??? + } else { + if (!(flags & 16)) { + x = x + (stbtt_int16) (points[0]*256 + points[1]); + points += 2; + } + } + vertices[off+i].x = (stbtt_int16) x; + } + + // now load y coordinates + y=0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + if (flags & 4) { + stbtt_int16 dy = *points++; + y += (flags & 32) ? dy : -dy; // ??? + } else { + if (!(flags & 32)) { + y = y + (stbtt_int16) (points[0]*256 + points[1]); + points += 2; + } + } + vertices[off+i].y = (stbtt_int16) y; + } + + // now convert them to our format + num_vertices=0; + sx = sy = cx = cy = scx = scy = 0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + x = (stbtt_int16) vertices[off+i].x; + y = (stbtt_int16) vertices[off+i].y; + + if (next_move == i) { + if (i != 0) + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); + + // now start the new one + start_off = !(flags & 1); + if (start_off) { + // if we start off with an off-curve point, then when we need to find a point on the curve + // where we can start, and we need to save some state for when we wraparound. + scx = x; + scy = y; + if (!(vertices[off+i+1].type & 1)) { + // next point is also a curve point, so interpolate an on-point curve + sx = (x + (stbtt_int32) vertices[off+i+1].x) >> 1; + sy = (y + (stbtt_int32) vertices[off+i+1].y) >> 1; + } else { + // otherwise just use the next point as our start point + sx = (stbtt_int32) vertices[off+i+1].x; + sy = (stbtt_int32) vertices[off+i+1].y; + ++i; // we're using point i+1 as the starting point, so skip it + } + } else { + sx = x; + sy = y; + } + stbtt_setvertex(&vertices[num_vertices++], STBTT_vmove,sx,sy,0,0); + was_off = 0; + next_move = 1 + ttUSHORT(endPtsOfContours+j*2); + ++j; + } else { + if (!(flags & 1)) { // if it's a curve + if (was_off) // two off-curve control points in a row means interpolate an on-curve midpoint + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+x)>>1, (cy+y)>>1, cx, cy); + cx = x; + cy = y; + was_off = 1; + } else { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, x,y, cx, cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline, x,y,0,0); + was_off = 0; + } + } + } + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); + } else if (numberOfContours == -1) { + // Compound shapes. + int more = 1; + stbtt_uint8 *comp = data + g + 10; + num_vertices = 0; + vertices = 0; + while (more) { + stbtt_uint16 flags, gidx; + int comp_num_verts = 0, i; + stbtt_vertex *comp_verts = 0, *tmp = 0; + float mtx[6] = {1,0,0,1,0,0}, m, n; + + flags = ttSHORT(comp); comp+=2; + gidx = ttSHORT(comp); comp+=2; + + if (flags & 2) { // XY values + if (flags & 1) { // shorts + mtx[4] = ttSHORT(comp); comp+=2; + mtx[5] = ttSHORT(comp); comp+=2; + } else { + mtx[4] = ttCHAR(comp); comp+=1; + mtx[5] = ttCHAR(comp); comp+=1; + } + } + else { + // @TODO handle matching point + STBTT_assert(0); + } + if (flags & (1<<3)) { // WE_HAVE_A_SCALE + mtx[0] = mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = mtx[2] = 0; + } else if (flags & (1<<6)) { // WE_HAVE_AN_X_AND_YSCALE + mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = mtx[2] = 0; + mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + } else if (flags & (1<<7)) { // WE_HAVE_A_TWO_BY_TWO + mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[2] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + } + + // Find transformation scales. + m = (float) STBTT_sqrt(mtx[0]*mtx[0] + mtx[1]*mtx[1]); + n = (float) STBTT_sqrt(mtx[2]*mtx[2] + mtx[3]*mtx[3]); + + // Get indexed glyph. + comp_num_verts = stbtt_GetGlyphShape(info, gidx, &comp_verts); + if (comp_num_verts > 0) { + // Transform vertices. + for (i = 0; i < comp_num_verts; ++i) { + stbtt_vertex* v = &comp_verts[i]; + stbtt_vertex_type x,y; + x=v->x; y=v->y; + v->x = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); + v->y = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); + x=v->cx; y=v->cy; + v->cx = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); + v->cy = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); + } + // Append vertices. + tmp = (stbtt_vertex*)STBTT_malloc((num_vertices+comp_num_verts)*sizeof(stbtt_vertex), info->userdata); + if (!tmp) { + if (vertices) STBTT_free(vertices, info->userdata); + if (comp_verts) STBTT_free(comp_verts, info->userdata); + return 0; + } + if (num_vertices > 0) STBTT_memcpy(tmp, vertices, num_vertices*sizeof(stbtt_vertex)); //-V595 + STBTT_memcpy(tmp+num_vertices, comp_verts, comp_num_verts*sizeof(stbtt_vertex)); + if (vertices) STBTT_free(vertices, info->userdata); + vertices = tmp; + STBTT_free(comp_verts, info->userdata); + num_vertices += comp_num_verts; + } + // More components ? + more = flags & (1<<5); + } + } else if (numberOfContours < 0) { + // @TODO other compound variations? + STBTT_assert(0); + } else { + // numberOfCounters == 0, do nothing + } + + *pvertices = vertices; + return num_vertices; +} + +typedef struct +{ + int bounds; + int started; + float first_x, first_y; + float x, y; + stbtt_int32 min_x, max_x, min_y, max_y; + + stbtt_vertex *pvertices; + int num_vertices; +} stbtt__csctx; + +#define STBTT__CSCTX_INIT(bounds) {bounds,0, 0,0, 0,0, 0,0,0,0, NULL, 0} + +static void stbtt__track_vertex(stbtt__csctx *c, stbtt_int32 x, stbtt_int32 y) +{ + if (x > c->max_x || !c->started) c->max_x = x; + if (y > c->max_y || !c->started) c->max_y = y; + if (x < c->min_x || !c->started) c->min_x = x; + if (y < c->min_y || !c->started) c->min_y = y; + c->started = 1; +} + +static void stbtt__csctx_v(stbtt__csctx *c, stbtt_uint8 type, stbtt_int32 x, stbtt_int32 y, stbtt_int32 cx, stbtt_int32 cy, stbtt_int32 cx1, stbtt_int32 cy1) +{ + if (c->bounds) { + stbtt__track_vertex(c, x, y); + if (type == STBTT_vcubic) { + stbtt__track_vertex(c, cx, cy); + stbtt__track_vertex(c, cx1, cy1); + } + } else { + stbtt_setvertex(&c->pvertices[c->num_vertices], type, x, y, cx, cy); + c->pvertices[c->num_vertices].cx1 = (stbtt_int16) cx1; + c->pvertices[c->num_vertices].cy1 = (stbtt_int16) cy1; + } + c->num_vertices++; +} + +static void stbtt__csctx_close_shape(stbtt__csctx *ctx) +{ + if (ctx->first_x != ctx->x || ctx->first_y != ctx->y) + stbtt__csctx_v(ctx, STBTT_vline, (int)ctx->first_x, (int)ctx->first_y, 0, 0, 0, 0); +} + +static void stbtt__csctx_rmove_to(stbtt__csctx *ctx, float dx, float dy) +{ + stbtt__csctx_close_shape(ctx); + ctx->first_x = ctx->x = ctx->x + dx; + ctx->first_y = ctx->y = ctx->y + dy; + stbtt__csctx_v(ctx, STBTT_vmove, (int)ctx->x, (int)ctx->y, 0, 0, 0, 0); +} + +static void stbtt__csctx_rline_to(stbtt__csctx *ctx, float dx, float dy) +{ + ctx->x += dx; + ctx->y += dy; + stbtt__csctx_v(ctx, STBTT_vline, (int)ctx->x, (int)ctx->y, 0, 0, 0, 0); +} + +static void stbtt__csctx_rccurve_to(stbtt__csctx *ctx, float dx1, float dy1, float dx2, float dy2, float dx3, float dy3) +{ + float cx1 = ctx->x + dx1; + float cy1 = ctx->y + dy1; + float cx2 = cx1 + dx2; + float cy2 = cy1 + dy2; + ctx->x = cx2 + dx3; + ctx->y = cy2 + dy3; + stbtt__csctx_v(ctx, STBTT_vcubic, (int)ctx->x, (int)ctx->y, (int)cx1, (int)cy1, (int)cx2, (int)cy2); +} + +static stbtt__buf stbtt__get_subr(stbtt__buf idx, int n) +{ + int count = stbtt__cff_index_count(&idx); + int bias = 107; + if (count >= 33900) + bias = 32768; + else if (count >= 1240) + bias = 1131; + n += bias; + if (n < 0 || n >= count) + return stbtt__new_buf(NULL, 0); + return stbtt__cff_index_get(idx, n); +} + +static stbtt__buf stbtt__cid_get_glyph_subrs(const stbtt_fontinfo *info, int glyph_index) +{ + stbtt__buf fdselect = info->fdselect; + int nranges, start, end, v, fmt, fdselector = -1, i; + + stbtt__buf_seek(&fdselect, 0); + fmt = stbtt__buf_get8(&fdselect); + if (fmt == 0) { + // untested + stbtt__buf_skip(&fdselect, glyph_index); + fdselector = stbtt__buf_get8(&fdselect); + } else if (fmt == 3) { + nranges = stbtt__buf_get16(&fdselect); + start = stbtt__buf_get16(&fdselect); + for (i = 0; i < nranges; i++) { + v = stbtt__buf_get8(&fdselect); + end = stbtt__buf_get16(&fdselect); + if (glyph_index >= start && glyph_index < end) { + fdselector = v; + break; + } + start = end; + } + } + if (fdselector == -1) stbtt__new_buf(NULL, 0); + return stbtt__get_subrs(info->cff, stbtt__cff_index_get(info->fontdicts, fdselector)); +} + +static int stbtt__run_charstring(const stbtt_fontinfo *info, int glyph_index, stbtt__csctx *c) +{ + int in_header = 1, maskbits = 0, subr_stack_height = 0, sp = 0, v, i, b0; + int has_subrs = 0, clear_stack; + float s[48]; + stbtt__buf subr_stack[10], subrs = info->subrs, b; + float f; + +#define STBTT__CSERR(s) (0) + + // this currently ignores the initial width value, which isn't needed if we have hmtx + b = stbtt__cff_index_get(info->charstrings, glyph_index); + while (b.cursor < b.size) { + i = 0; + clear_stack = 1; + b0 = stbtt__buf_get8(&b); + switch (b0) { + // @TODO implement hinting + case 0x13: // hintmask + case 0x14: // cntrmask + if (in_header) + maskbits += (sp / 2); // implicit "vstem" + in_header = 0; + stbtt__buf_skip(&b, (maskbits + 7) / 8); + break; + + case 0x01: // hstem + case 0x03: // vstem + case 0x12: // hstemhm + case 0x17: // vstemhm + maskbits += (sp / 2); + break; + + case 0x15: // rmoveto + in_header = 0; + if (sp < 2) return STBTT__CSERR("rmoveto stack"); + stbtt__csctx_rmove_to(c, s[sp-2], s[sp-1]); + break; + case 0x04: // vmoveto + in_header = 0; + if (sp < 1) return STBTT__CSERR("vmoveto stack"); + stbtt__csctx_rmove_to(c, 0, s[sp-1]); + break; + case 0x16: // hmoveto + in_header = 0; + if (sp < 1) return STBTT__CSERR("hmoveto stack"); + stbtt__csctx_rmove_to(c, s[sp-1], 0); + break; + + case 0x05: // rlineto + if (sp < 2) return STBTT__CSERR("rlineto stack"); + for (; i + 1 < sp; i += 2) + stbtt__csctx_rline_to(c, s[i], s[i+1]); + break; + + // hlineto/vlineto and vhcurveto/hvcurveto alternate horizontal and vertical + // starting from a different place. + + case 0x07: // vlineto + if (sp < 1) return STBTT__CSERR("vlineto stack"); + goto vlineto; + case 0x06: // hlineto + if (sp < 1) return STBTT__CSERR("hlineto stack"); + for (;;) { + if (i >= sp) break; + stbtt__csctx_rline_to(c, s[i], 0); + i++; + vlineto: + if (i >= sp) break; + stbtt__csctx_rline_to(c, 0, s[i]); + i++; + } + break; + + case 0x1F: // hvcurveto + if (sp < 4) return STBTT__CSERR("hvcurveto stack"); + goto hvcurveto; + case 0x1E: // vhcurveto + if (sp < 4) return STBTT__CSERR("vhcurveto stack"); + for (;;) { + if (i + 3 >= sp) break; + stbtt__csctx_rccurve_to(c, 0, s[i], s[i+1], s[i+2], s[i+3], (sp - i == 5) ? s[i + 4] : 0.0f); + i += 4; + hvcurveto: + if (i + 3 >= sp) break; + stbtt__csctx_rccurve_to(c, s[i], 0, s[i+1], s[i+2], (sp - i == 5) ? s[i+4] : 0.0f, s[i+3]); + i += 4; + } + break; + + case 0x08: // rrcurveto + if (sp < 6) return STBTT__CSERR("rcurveline stack"); + for (; i + 5 < sp; i += 6) + stbtt__csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5]); + break; + + case 0x18: // rcurveline + if (sp < 8) return STBTT__CSERR("rcurveline stack"); + for (; i + 5 < sp - 2; i += 6) + stbtt__csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5]); + if (i + 1 >= sp) return STBTT__CSERR("rcurveline stack"); + stbtt__csctx_rline_to(c, s[i], s[i+1]); + break; + + case 0x19: // rlinecurve + if (sp < 8) return STBTT__CSERR("rlinecurve stack"); + for (; i + 1 < sp - 6; i += 2) + stbtt__csctx_rline_to(c, s[i], s[i+1]); + if (i + 5 >= sp) return STBTT__CSERR("rlinecurve stack"); + stbtt__csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5]); + break; + + case 0x1A: // vvcurveto + case 0x1B: // hhcurveto + if (sp < 4) return STBTT__CSERR("(vv|hh)curveto stack"); + f = 0.0; + if (sp & 1) { f = s[i]; i++; } + for (; i + 3 < sp; i += 4) { + if (b0 == 0x1B) + stbtt__csctx_rccurve_to(c, s[i], f, s[i+1], s[i+2], s[i+3], 0.0); + else + stbtt__csctx_rccurve_to(c, f, s[i], s[i+1], s[i+2], 0.0, s[i+3]); + f = 0.0; + } + break; + + case 0x0A: // callsubr + if (!has_subrs) { + if (info->fdselect.size) + subrs = stbtt__cid_get_glyph_subrs(info, glyph_index); + has_subrs = 1; + } + // fallthrough + case 0x1D: // callgsubr + if (sp < 1) return STBTT__CSERR("call(g|)subr stack"); + v = (int) s[--sp]; + if (subr_stack_height >= 10) return STBTT__CSERR("recursion limit"); + subr_stack[subr_stack_height++] = b; + b = stbtt__get_subr(b0 == 0x0A ? subrs : info->gsubrs, v); + if (b.size == 0) return STBTT__CSERR("subr not found"); + b.cursor = 0; + clear_stack = 0; + break; + + case 0x0B: // return + if (subr_stack_height <= 0) return STBTT__CSERR("return outside subr"); + b = subr_stack[--subr_stack_height]; + clear_stack = 0; + break; + + case 0x0E: // endchar + stbtt__csctx_close_shape(c); + return 1; + + case 0x0C: { // two-byte escape + float dx1, dx2, dx3, dx4, dx5, dx6, dy1, dy2, dy3, dy4, dy5, dy6; + float dx, dy; + int b1 = stbtt__buf_get8(&b); + switch (b1) { + // @TODO These "flex" implementations ignore the flex-depth and resolution, + // and always draw beziers. + case 0x22: // hflex + if (sp < 7) return STBTT__CSERR("hflex stack"); + dx1 = s[0]; + dx2 = s[1]; + dy2 = s[2]; + dx3 = s[3]; + dx4 = s[4]; + dx5 = s[5]; + dx6 = s[6]; + stbtt__csctx_rccurve_to(c, dx1, 0, dx2, dy2, dx3, 0); + stbtt__csctx_rccurve_to(c, dx4, 0, dx5, -dy2, dx6, 0); + break; + + case 0x23: // flex + if (sp < 13) return STBTT__CSERR("flex stack"); + dx1 = s[0]; + dy1 = s[1]; + dx2 = s[2]; + dy2 = s[3]; + dx3 = s[4]; + dy3 = s[5]; + dx4 = s[6]; + dy4 = s[7]; + dx5 = s[8]; + dy5 = s[9]; + dx6 = s[10]; + dy6 = s[11]; + //fd is s[12] + stbtt__csctx_rccurve_to(c, dx1, dy1, dx2, dy2, dx3, dy3); + stbtt__csctx_rccurve_to(c, dx4, dy4, dx5, dy5, dx6, dy6); + break; + + case 0x24: // hflex1 + if (sp < 9) return STBTT__CSERR("hflex1 stack"); + dx1 = s[0]; + dy1 = s[1]; + dx2 = s[2]; + dy2 = s[3]; + dx3 = s[4]; + dx4 = s[5]; + dx5 = s[6]; + dy5 = s[7]; + dx6 = s[8]; + stbtt__csctx_rccurve_to(c, dx1, dy1, dx2, dy2, dx3, 0); + stbtt__csctx_rccurve_to(c, dx4, 0, dx5, dy5, dx6, -(dy1+dy2+dy5)); + break; + + case 0x25: // flex1 + if (sp < 11) return STBTT__CSERR("flex1 stack"); + dx1 = s[0]; + dy1 = s[1]; + dx2 = s[2]; + dy2 = s[3]; + dx3 = s[4]; + dy3 = s[5]; + dx4 = s[6]; + dy4 = s[7]; + dx5 = s[8]; + dy5 = s[9]; + dx6 = dy6 = s[10]; + dx = dx1+dx2+dx3+dx4+dx5; + dy = dy1+dy2+dy3+dy4+dy5; + if (STBTT_fabs(dx) > STBTT_fabs(dy)) + dy6 = -dy; + else + dx6 = -dx; + stbtt__csctx_rccurve_to(c, dx1, dy1, dx2, dy2, dx3, dy3); + stbtt__csctx_rccurve_to(c, dx4, dy4, dx5, dy5, dx6, dy6); + break; + + default: + return STBTT__CSERR("unimplemented"); + } + } break; + + default: + if (b0 != 255 && b0 != 28 && (b0 < 32 || b0 > 254)) //-V560 + return STBTT__CSERR("reserved operator"); + + // push immediate + if (b0 == 255) { + f = (float)(stbtt_int32)stbtt__buf_get32(&b) / 0x10000; + } else { + stbtt__buf_skip(&b, -1); + f = (float)(stbtt_int16)stbtt__cff_int(&b); + } + if (sp >= 48) return STBTT__CSERR("push stack overflow"); + s[sp++] = f; + clear_stack = 0; + break; + } + if (clear_stack) sp = 0; + } + return STBTT__CSERR("no endchar"); + +#undef STBTT__CSERR +} + +static int stbtt__GetGlyphShapeT2(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + // runs the charstring twice, once to count and once to output (to avoid realloc) + stbtt__csctx count_ctx = STBTT__CSCTX_INIT(1); + stbtt__csctx output_ctx = STBTT__CSCTX_INIT(0); + if (stbtt__run_charstring(info, glyph_index, &count_ctx)) { + *pvertices = (stbtt_vertex*)STBTT_malloc(count_ctx.num_vertices*sizeof(stbtt_vertex), info->userdata); + output_ctx.pvertices = *pvertices; + if (stbtt__run_charstring(info, glyph_index, &output_ctx)) { + STBTT_assert(output_ctx.num_vertices == count_ctx.num_vertices); + return output_ctx.num_vertices; + } + } + *pvertices = NULL; + return 0; +} + +static int stbtt__GetGlyphInfoT2(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1) +{ + stbtt__csctx c = STBTT__CSCTX_INIT(1); + int r = stbtt__run_charstring(info, glyph_index, &c); + if (x0) *x0 = r ? c.min_x : 0; + if (y0) *y0 = r ? c.min_y : 0; + if (x1) *x1 = r ? c.max_x : 0; + if (y1) *y1 = r ? c.max_y : 0; + return r ? c.num_vertices : 0; +} + +STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + if (!info->cff.size) + return stbtt__GetGlyphShapeTT(info, glyph_index, pvertices); + else + return stbtt__GetGlyphShapeT2(info, glyph_index, pvertices); +} + +STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing) +{ + stbtt_uint16 numOfLongHorMetrics = ttUSHORT(info->data+info->hhea + 34); + if (glyph_index < numOfLongHorMetrics) { + if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*glyph_index); + if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*glyph_index + 2); + } else { + if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*(numOfLongHorMetrics-1)); + if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*numOfLongHorMetrics + 2*(glyph_index - numOfLongHorMetrics)); + } +} + +static int stbtt__GetGlyphKernInfoAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) +{ + stbtt_uint8 *data = info->data + info->kern; + stbtt_uint32 needle, straw; + int l, r, m; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + l = 0; + r = ttUSHORT(data+10) - 1; + needle = glyph1 << 16 | glyph2; + while (l <= r) { + m = (l + r) >> 1; + straw = ttULONG(data+18+(m*6)); // note: unaligned read + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else + return ttSHORT(data+22+(m*6)); + } + return 0; +} + +static stbtt_int32 stbtt__GetCoverageIndex(stbtt_uint8 *coverageTable, int glyph) +{ + stbtt_uint16 coverageFormat = ttUSHORT(coverageTable); + switch(coverageFormat) { + case 1: { + stbtt_uint16 glyphCount = ttUSHORT(coverageTable + 2); + + // Binary search. + stbtt_int32 l=0, r=glyphCount-1, m; + int straw, needle=glyph; + while (l <= r) { + stbtt_uint8 *glyphArray = coverageTable + 4; + stbtt_uint16 glyphID; + m = (l + r) >> 1; + glyphID = ttUSHORT(glyphArray + 2 * m); + straw = glyphID; + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else { + return m; + } + } + } break; + + case 2: { + stbtt_uint16 rangeCount = ttUSHORT(coverageTable + 2); + stbtt_uint8 *rangeArray = coverageTable + 4; + + // Binary search. + stbtt_int32 l=0, r=rangeCount-1, m; + int strawStart, strawEnd, needle=glyph; + while (l <= r) { + stbtt_uint8 *rangeRecord; + m = (l + r) >> 1; + rangeRecord = rangeArray + 6 * m; + strawStart = ttUSHORT(rangeRecord); + strawEnd = ttUSHORT(rangeRecord + 2); + if (needle < strawStart) + r = m - 1; + else if (needle > strawEnd) + l = m + 1; + else { + stbtt_uint16 startCoverageIndex = ttUSHORT(rangeRecord + 4); + return startCoverageIndex + glyph - strawStart; + } + } + } break; + + default: { + // There are no other cases. + STBTT_assert(0); + } break; + } + + return -1; +} + +static stbtt_int32 stbtt__GetGlyphClass(stbtt_uint8 *classDefTable, int glyph) +{ + stbtt_uint16 classDefFormat = ttUSHORT(classDefTable); + switch(classDefFormat) + { + case 1: { + stbtt_uint16 startGlyphID = ttUSHORT(classDefTable + 2); + stbtt_uint16 glyphCount = ttUSHORT(classDefTable + 4); + stbtt_uint8 *classDef1ValueArray = classDefTable + 6; + + if (glyph >= startGlyphID && glyph < startGlyphID + glyphCount) + return (stbtt_int32)ttUSHORT(classDef1ValueArray + 2 * (glyph - startGlyphID)); + + // [DEAR IMGUI] Commented to fix static analyzer warning + //classDefTable = classDef1ValueArray + 2 * glyphCount; + } break; + + case 2: { + stbtt_uint16 classRangeCount = ttUSHORT(classDefTable + 2); + stbtt_uint8 *classRangeRecords = classDefTable + 4; + + // Binary search. + stbtt_int32 l=0, r=classRangeCount-1, m; + int strawStart, strawEnd, needle=glyph; + while (l <= r) { + stbtt_uint8 *classRangeRecord; + m = (l + r) >> 1; + classRangeRecord = classRangeRecords + 6 * m; + strawStart = ttUSHORT(classRangeRecord); + strawEnd = ttUSHORT(classRangeRecord + 2); + if (needle < strawStart) + r = m - 1; + else if (needle > strawEnd) + l = m + 1; + else + return (stbtt_int32)ttUSHORT(classRangeRecord + 4); + } + + // [DEAR IMGUI] Commented to fix static analyzer warning + //classDefTable = classRangeRecords + 6 * classRangeCount; + } break; + + default: { + // There are no other cases. + STBTT_assert(0); + } break; + } + + return -1; +} + +// Define to STBTT_assert(x) if you want to break on unimplemented formats. +#define STBTT_GPOS_TODO_assert(x) + +static stbtt_int32 stbtt__GetGlyphGPOSInfoAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) +{ + stbtt_uint16 lookupListOffset; + stbtt_uint8 *lookupList; + stbtt_uint16 lookupCount; + stbtt_uint8 *data; + stbtt_int32 i; + + if (!info->gpos) return 0; + + data = info->data + info->gpos; + + if (ttUSHORT(data+0) != 1) return 0; // Major version 1 + if (ttUSHORT(data+2) != 0) return 0; // Minor version 0 + + lookupListOffset = ttUSHORT(data+8); + lookupList = data + lookupListOffset; + lookupCount = ttUSHORT(lookupList); + + for (i=0; i> 1; + pairValue = pairValueArray + (2 + valueRecordPairSizeInBytes) * m; + secondGlyph = ttUSHORT(pairValue); + straw = secondGlyph; + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else { + stbtt_int16 xAdvance = ttSHORT(pairValue + 2); + return xAdvance; + } + } + } break; + + case 2: { + stbtt_uint16 valueFormat1 = ttUSHORT(table + 4); + stbtt_uint16 valueFormat2 = ttUSHORT(table + 6); + + stbtt_uint16 classDef1Offset = ttUSHORT(table + 8); + stbtt_uint16 classDef2Offset = ttUSHORT(table + 10); + int glyph1class = stbtt__GetGlyphClass(table + classDef1Offset, glyph1); + int glyph2class = stbtt__GetGlyphClass(table + classDef2Offset, glyph2); + + stbtt_uint16 class1Count = ttUSHORT(table + 12); + stbtt_uint16 class2Count = ttUSHORT(table + 14); + STBTT_assert(glyph1class < class1Count); + STBTT_assert(glyph2class < class2Count); + + // TODO: Support more formats. + STBTT_GPOS_TODO_assert(valueFormat1 == 4); + if (valueFormat1 != 4) return 0; + STBTT_GPOS_TODO_assert(valueFormat2 == 0); + if (valueFormat2 != 0) return 0; + + if (glyph1class >= 0 && glyph1class < class1Count && glyph2class >= 0 && glyph2class < class2Count) { + stbtt_uint8 *class1Records = table + 16; + stbtt_uint8 *class2Records = class1Records + 2 * (glyph1class * class2Count); + stbtt_int16 xAdvance = ttSHORT(class2Records + 2 * glyph2class); + return xAdvance; + } + } break; + + default: { + // There are no other cases. + STBTT_assert(0); + break; + } // [DEAR IMGUI] removed ; + } + } + break; + } // [DEAR IMGUI] removed ; + + default: + // TODO: Implement other stuff. + break; + } + } + + return 0; +} + +STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int g1, int g2) +{ + int xAdvance = 0; + + if (info->gpos) + xAdvance += stbtt__GetGlyphGPOSInfoAdvance(info, g1, g2); + + if (info->kern) + xAdvance += stbtt__GetGlyphKernInfoAdvance(info, g1, g2); + + return xAdvance; +} + +STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2) +{ + if (!info->kern && !info->gpos) // if no kerning table, don't waste time looking up both codepoint->glyphs + return 0; + return stbtt_GetGlyphKernAdvance(info, stbtt_FindGlyphIndex(info,ch1), stbtt_FindGlyphIndex(info,ch2)); +} + +STBTT_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing) +{ + stbtt_GetGlyphHMetrics(info, stbtt_FindGlyphIndex(info,codepoint), advanceWidth, leftSideBearing); +} + +STBTT_DEF void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap) +{ + if (ascent ) *ascent = ttSHORT(info->data+info->hhea + 4); + if (descent) *descent = ttSHORT(info->data+info->hhea + 6); + if (lineGap) *lineGap = ttSHORT(info->data+info->hhea + 8); +} + +STBTT_DEF int stbtt_GetFontVMetricsOS2(const stbtt_fontinfo *info, int *typoAscent, int *typoDescent, int *typoLineGap) +{ + int tab = stbtt__find_table(info->data, info->fontstart, "OS/2"); + if (!tab) + return 0; + if (typoAscent ) *typoAscent = ttSHORT(info->data+tab + 68); + if (typoDescent) *typoDescent = ttSHORT(info->data+tab + 70); + if (typoLineGap) *typoLineGap = ttSHORT(info->data+tab + 72); + return 1; +} + +STBTT_DEF void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1) +{ + *x0 = ttSHORT(info->data + info->head + 36); + *y0 = ttSHORT(info->data + info->head + 38); + *x1 = ttSHORT(info->data + info->head + 40); + *y1 = ttSHORT(info->data + info->head + 42); +} + +STBTT_DEF float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float height) +{ + int fheight = ttSHORT(info->data + info->hhea + 4) - ttSHORT(info->data + info->hhea + 6); + return (float) height / fheight; +} + +STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels) +{ + int unitsPerEm = ttUSHORT(info->data + info->head + 18); + return pixels / unitsPerEm; +} + +STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *v) +{ + STBTT_free(v, info->userdata); +} + +////////////////////////////////////////////////////////////////////////////// +// +// antialiasing software rasterizer +// + +STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + int x0=0,y0=0,x1,y1; // =0 suppresses compiler warning + if (!stbtt_GetGlyphBox(font, glyph, &x0,&y0,&x1,&y1)) { + // e.g. space character + if (ix0) *ix0 = 0; + if (iy0) *iy0 = 0; + if (ix1) *ix1 = 0; + if (iy1) *iy1 = 0; + } else { + // move to integral bboxes (treating pixels as little squares, what pixels get touched)? + if (ix0) *ix0 = STBTT_ifloor( x0 * scale_x + shift_x); + if (iy0) *iy0 = STBTT_ifloor(-y1 * scale_y + shift_y); + if (ix1) *ix1 = STBTT_iceil ( x1 * scale_x + shift_x); + if (iy1) *iy1 = STBTT_iceil (-y0 * scale_y + shift_y); + } +} + +STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetGlyphBitmapBoxSubpixel(font, glyph, scale_x, scale_y,0.0f,0.0f, ix0, iy0, ix1, iy1); +} + +STBTT_DEF void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetGlyphBitmapBoxSubpixel(font, stbtt_FindGlyphIndex(font,codepoint), scale_x, scale_y,shift_x,shift_y, ix0,iy0,ix1,iy1); +} + +STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetCodepointBitmapBoxSubpixel(font, codepoint, scale_x, scale_y,0.0f,0.0f, ix0,iy0,ix1,iy1); +} + +////////////////////////////////////////////////////////////////////////////// +// +// Rasterizer + +typedef struct stbtt__hheap_chunk +{ + struct stbtt__hheap_chunk *next; +} stbtt__hheap_chunk; + +typedef struct stbtt__hheap +{ + struct stbtt__hheap_chunk *head; + void *first_free; + int num_remaining_in_head_chunk; +} stbtt__hheap; + +static void *stbtt__hheap_alloc(stbtt__hheap *hh, size_t size, void *userdata) +{ + if (hh->first_free) { + void *p = hh->first_free; + hh->first_free = * (void **) p; + return p; + } else { + if (hh->num_remaining_in_head_chunk == 0) { + int count = (size < 32 ? 2000 : size < 128 ? 800 : 100); + stbtt__hheap_chunk *c = (stbtt__hheap_chunk *) STBTT_malloc(sizeof(stbtt__hheap_chunk) + size * count, userdata); + if (c == NULL) + return NULL; + c->next = hh->head; + hh->head = c; + hh->num_remaining_in_head_chunk = count; + } + --hh->num_remaining_in_head_chunk; + return (char *) (hh->head) + sizeof(stbtt__hheap_chunk) + size * hh->num_remaining_in_head_chunk; + } +} + +static void stbtt__hheap_free(stbtt__hheap *hh, void *p) +{ + *(void **) p = hh->first_free; + hh->first_free = p; +} + +static void stbtt__hheap_cleanup(stbtt__hheap *hh, void *userdata) +{ + stbtt__hheap_chunk *c = hh->head; + while (c) { + stbtt__hheap_chunk *n = c->next; + STBTT_free(c, userdata); + c = n; + } +} + +typedef struct stbtt__edge { + float x0,y0, x1,y1; + int invert; +} stbtt__edge; + + +typedef struct stbtt__active_edge +{ + struct stbtt__active_edge *next; + #if STBTT_RASTERIZER_VERSION==1 + int x,dx; + float ey; + int direction; + #elif STBTT_RASTERIZER_VERSION==2 + float fx,fdx,fdy; + float direction; + float sy; + float ey; + #else + #error "Unrecognized value of STBTT_RASTERIZER_VERSION" + #endif +} stbtt__active_edge; + +#if STBTT_RASTERIZER_VERSION == 1 +#define STBTT_FIXSHIFT 10 +#define STBTT_FIX (1 << STBTT_FIXSHIFT) +#define STBTT_FIXMASK (STBTT_FIX-1) + +static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, int off_x, float start_point, void *userdata) +{ + stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); + STBTT_assert(z != NULL); + if (!z) return z; + + // round dx down to avoid overshooting + if (dxdy < 0) + z->dx = -STBTT_ifloor(STBTT_FIX * -dxdy); + else + z->dx = STBTT_ifloor(STBTT_FIX * dxdy); + + z->x = STBTT_ifloor(STBTT_FIX * e->x0 + z->dx * (start_point - e->y0)); // use z->dx so when we offset later it's by the same amount + z->x -= off_x * STBTT_FIX; + + z->ey = e->y1; + z->next = 0; + z->direction = e->invert ? 1 : -1; + return z; +} +#elif STBTT_RASTERIZER_VERSION == 2 +static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, int off_x, float start_point, void *userdata) +{ + stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); + STBTT_assert(z != NULL); + //STBTT_assert(e->y0 <= start_point); + if (!z) return z; + z->fdx = dxdy; + z->fdy = dxdy != 0.0f ? (1.0f/dxdy) : 0.0f; + z->fx = e->x0 + dxdy * (start_point - e->y0); + z->fx -= off_x; + z->direction = e->invert ? 1.0f : -1.0f; + z->sy = e->y0; + z->ey = e->y1; + z->next = 0; + return z; +} +#else +#error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + +#if STBTT_RASTERIZER_VERSION == 1 +// note: this routine clips fills that extend off the edges... ideally this +// wouldn't happen, but it could happen if the truetype glyph bounding boxes +// are wrong, or if the user supplies a too-small bitmap +static void stbtt__fill_active_edges(unsigned char *scanline, int len, stbtt__active_edge *e, int max_weight) +{ + // non-zero winding fill + int x0=0, w=0; + + while (e) { + if (w == 0) { + // if we're currently at zero, we need to record the edge start point + x0 = e->x; w += e->direction; + } else { + int x1 = e->x; w += e->direction; + // if we went to zero, we need to draw + if (w == 0) { + int i = x0 >> STBTT_FIXSHIFT; + int j = x1 >> STBTT_FIXSHIFT; + + if (i < len && j >= 0) { + if (i == j) { + // x0,x1 are the same pixel, so compute combined coverage + scanline[i] = scanline[i] + (stbtt_uint8) ((x1 - x0) * max_weight >> STBTT_FIXSHIFT); + } else { + if (i >= 0) // add antialiasing for x0 + scanline[i] = scanline[i] + (stbtt_uint8) (((STBTT_FIX - (x0 & STBTT_FIXMASK)) * max_weight) >> STBTT_FIXSHIFT); + else + i = -1; // clip + + if (j < len) // add antialiasing for x1 + scanline[j] = scanline[j] + (stbtt_uint8) (((x1 & STBTT_FIXMASK) * max_weight) >> STBTT_FIXSHIFT); + else + j = len; // clip + + for (++i; i < j; ++i) // fill pixels between x0 and x1 + scanline[i] = scanline[i] + (stbtt_uint8) max_weight; + } + } + } + } + + e = e->next; + } +} + +static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, int n, int vsubsample, int off_x, int off_y, void *userdata) +{ + stbtt__hheap hh = { 0, 0, 0 }; + stbtt__active_edge *active = NULL; + int y,j=0; + int max_weight = (255 / vsubsample); // weight per vertical scanline + int s; // vertical subsample index + unsigned char scanline_data[512], *scanline; + + if (result->w > 512) + scanline = (unsigned char *) STBTT_malloc(result->w, userdata); + else + scanline = scanline_data; + + y = off_y * vsubsample; + e[n].y0 = (off_y + result->h) * (float) vsubsample + 1; + + while (j < result->h) { + STBTT_memset(scanline, 0, result->w); + for (s=0; s < vsubsample; ++s) { + // find center of pixel for this scanline + float scan_y = y + 0.5f; + stbtt__active_edge **step = &active; + + // update all active edges; + // remove all active edges that terminate before the center of this scanline + while (*step) { + stbtt__active_edge * z = *step; + if (z->ey <= scan_y) { + *step = z->next; // delete from list + STBTT_assert(z->direction); + z->direction = 0; + stbtt__hheap_free(&hh, z); + } else { + z->x += z->dx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + } + + // resort the list if needed + for(;;) { + int changed=0; + step = &active; + while (*step && (*step)->next) { + if ((*step)->x > (*step)->next->x) { + stbtt__active_edge *t = *step; + stbtt__active_edge *q = t->next; + + t->next = q->next; + q->next = t; + *step = q; + changed = 1; + } + step = &(*step)->next; + } + if (!changed) break; + } + + // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline + while (e->y0 <= scan_y) { + if (e->y1 > scan_y) { + stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y, userdata); + if (z != NULL) { + // find insertion point + if (active == NULL) + active = z; + else if (z->x < active->x) { + // insert at front + z->next = active; + active = z; + } else { + // find thing to insert AFTER + stbtt__active_edge *p = active; + while (p->next && p->next->x < z->x) + p = p->next; + // at this point, p->next->x is NOT < z->x + z->next = p->next; + p->next = z; + } + } + } + ++e; + } + + // now process all active edges in XOR fashion + if (active) + stbtt__fill_active_edges(scanline, result->w, active, max_weight); + + ++y; + } + STBTT_memcpy(result->pixels + j * result->stride, scanline, result->w); + ++j; + } + + stbtt__hheap_cleanup(&hh, userdata); + + if (scanline != scanline_data) + STBTT_free(scanline, userdata); +} + +#elif STBTT_RASTERIZER_VERSION == 2 + +// the edge passed in here does not cross the vertical line at x or the vertical line at x+1 +// (i.e. it has already been clipped to those) +static void stbtt__handle_clipped_edge(float *scanline, int x, stbtt__active_edge *e, float x0, float y0, float x1, float y1) +{ + if (y0 == y1) return; + STBTT_assert(y0 < y1); + STBTT_assert(e->sy <= e->ey); + if (y0 > e->ey) return; + if (y1 < e->sy) return; + if (y0 < e->sy) { + x0 += (x1-x0) * (e->sy - y0) / (y1-y0); + y0 = e->sy; + } + if (y1 > e->ey) { + x1 += (x1-x0) * (e->ey - y1) / (y1-y0); + y1 = e->ey; + } + + if (x0 == x) + STBTT_assert(x1 <= x+1); + else if (x0 == x+1) + STBTT_assert(x1 >= x); + else if (x0 <= x) + STBTT_assert(x1 <= x); + else if (x0 >= x+1) + STBTT_assert(x1 >= x+1); + else + STBTT_assert(x1 >= x && x1 <= x+1); + + if (x0 <= x && x1 <= x) + scanline[x] += e->direction * (y1-y0); + else if (x0 >= x+1 && x1 >= x+1) + ; + else { + STBTT_assert(x0 >= x && x0 <= x+1 && x1 >= x && x1 <= x+1); + scanline[x] += e->direction * (y1-y0) * (1-((x0-x)+(x1-x))/2); // coverage = 1 - average x position + } +} + +static void stbtt__fill_active_edges_new(float *scanline, float *scanline_fill, int len, stbtt__active_edge *e, float y_top) +{ + float y_bottom = y_top+1; + + while (e) { + // brute force every pixel + + // compute intersection points with top & bottom + STBTT_assert(e->ey >= y_top); + + if (e->fdx == 0) { + float x0 = e->fx; + if (x0 < len) { + if (x0 >= 0) { + stbtt__handle_clipped_edge(scanline,(int) x0,e, x0,y_top, x0,y_bottom); + stbtt__handle_clipped_edge(scanline_fill-1,(int) x0+1,e, x0,y_top, x0,y_bottom); + } else { + stbtt__handle_clipped_edge(scanline_fill-1,0,e, x0,y_top, x0,y_bottom); + } + } + } else { + float x0 = e->fx; + float dx = e->fdx; + float xb = x0 + dx; + float x_top, x_bottom; + float sy0,sy1; + float dy = e->fdy; + STBTT_assert(e->sy <= y_bottom && e->ey >= y_top); + + // compute endpoints of line segment clipped to this scanline (if the + // line segment starts on this scanline. x0 is the intersection of the + // line with y_top, but that may be off the line segment. + if (e->sy > y_top) { + x_top = x0 + dx * (e->sy - y_top); + sy0 = e->sy; + } else { + x_top = x0; + sy0 = y_top; + } + if (e->ey < y_bottom) { + x_bottom = x0 + dx * (e->ey - y_top); + sy1 = e->ey; + } else { + x_bottom = xb; + sy1 = y_bottom; + } + + if (x_top >= 0 && x_bottom >= 0 && x_top < len && x_bottom < len) { + // from here on, we don't have to range check x values + + if ((int) x_top == (int) x_bottom) { + float height; + // simple case, only spans one pixel + int x = (int) x_top; + height = sy1 - sy0; + STBTT_assert(x >= 0 && x < len); + scanline[x] += e->direction * (1-((x_top - x) + (x_bottom-x))/2) * height; + scanline_fill[x] += e->direction * height; // everything right of this pixel is filled + } else { + int x,x1,x2; + float y_crossing, step, sign, area; + // covers 2+ pixels + if (x_top > x_bottom) { + // flip scanline vertically; signed area is the same + float t; + sy0 = y_bottom - (sy0 - y_top); + sy1 = y_bottom - (sy1 - y_top); + t = sy0, sy0 = sy1, sy1 = t; + t = x_bottom, x_bottom = x_top, x_top = t; + dx = -dx; + dy = -dy; + t = x0, x0 = xb, xb = t; + // [DEAR IMGUI] Fix static analyzer warning + (void)dx; // [ImGui: fix static analyzer warning] + } + + x1 = (int) x_top; + x2 = (int) x_bottom; + // compute intersection with y axis at x1+1 + y_crossing = (x1+1 - x0) * dy + y_top; + + sign = e->direction; + // area of the rectangle covered from y0..y_crossing + area = sign * (y_crossing-sy0); + // area of the triangle (x_top,y0), (x+1,y0), (x+1,y_crossing) + scanline[x1] += area * (1-((x_top - x1)+(x1+1-x1))/2); + + step = sign * dy; + for (x = x1+1; x < x2; ++x) { + scanline[x] += area + step/2; + area += step; + } + y_crossing += dy * (x2 - (x1+1)); + + STBTT_assert(STBTT_fabs(area) <= 1.01f); + + scanline[x2] += area + sign * (1-((x2-x2)+(x_bottom-x2))/2) * (sy1-y_crossing); + + scanline_fill[x2] += sign * (sy1-sy0); + } + } else { + // if edge goes outside of box we're drawing, we require + // clipping logic. since this does not match the intended use + // of this library, we use a different, very slow brute + // force implementation + int x; + for (x=0; x < len; ++x) { + // cases: + // + // there can be up to two intersections with the pixel. any intersection + // with left or right edges can be handled by splitting into two (or three) + // regions. intersections with top & bottom do not necessitate case-wise logic. + // + // the old way of doing this found the intersections with the left & right edges, + // then used some simple logic to produce up to three segments in sorted order + // from top-to-bottom. however, this had a problem: if an x edge was epsilon + // across the x border, then the corresponding y position might not be distinct + // from the other y segment, and it might ignored as an empty segment. to avoid + // that, we need to explicitly produce segments based on x positions. + + // rename variables to clearly-defined pairs + float y0 = y_top; + float x1 = (float) (x); + float x2 = (float) (x+1); + float x3 = xb; + float y3 = y_bottom; + + // x = e->x + e->dx * (y-y_top) + // (y-y_top) = (x - e->x) / e->dx + // y = (x - e->x) / e->dx + y_top + float y1 = (x - x0) / dx + y_top; + float y2 = (x+1 - x0) / dx + y_top; + + if (x0 < x1 && x3 > x2) { // three segments descending down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else if (x3 < x1 && x0 > x2) { // three segments descending down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x0 < x1 && x3 > x1) { // two segments across x, down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x3 < x1 && x0 > x1) { // two segments across x, down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x0 < x2 && x3 > x2) { // two segments across x+1, down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else if (x3 < x2 && x0 > x2) { // two segments across x+1, down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else { // one segment + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x3,y3); + } + } + } + } + e = e->next; + } +} + +// directly AA rasterize edges w/o supersampling +static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, int n, int vsubsample, int off_x, int off_y, void *userdata) +{ + stbtt__hheap hh = { 0, 0, 0 }; + stbtt__active_edge *active = NULL; + int y,j=0, i; + float scanline_data[129], *scanline, *scanline2; + + STBTT__NOTUSED(vsubsample); + + if (result->w > 64) + scanline = (float *) STBTT_malloc((result->w*2+1) * sizeof(float), userdata); + else + scanline = scanline_data; + + scanline2 = scanline + result->w; + + y = off_y; + e[n].y0 = (float) (off_y + result->h) + 1; + + while (j < result->h) { + // find center of pixel for this scanline + float scan_y_top = y + 0.0f; + float scan_y_bottom = y + 1.0f; + stbtt__active_edge **step = &active; + + STBTT_memset(scanline , 0, result->w*sizeof(scanline[0])); + STBTT_memset(scanline2, 0, (result->w+1)*sizeof(scanline[0])); + + // update all active edges; + // remove all active edges that terminate before the top of this scanline + while (*step) { + stbtt__active_edge * z = *step; + if (z->ey <= scan_y_top) { + *step = z->next; // delete from list + STBTT_assert(z->direction); + z->direction = 0; + stbtt__hheap_free(&hh, z); + } else { + step = &((*step)->next); // advance through list + } + } + + // insert all edges that start before the bottom of this scanline + while (e->y0 <= scan_y_bottom) { + if (e->y0 != e->y1) { + stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y_top, userdata); + if (z != NULL) { + if (j == 0 && off_y != 0) { + if (z->ey < scan_y_top) { + // this can happen due to subpixel positioning and some kind of fp rounding error i think + z->ey = scan_y_top; + } + } + STBTT_assert(z->ey >= scan_y_top); // if we get really unlucky a tiny bit of an edge can be out of bounds + // insert at front + z->next = active; + active = z; + } + } + ++e; + } + + // now process all active edges + if (active) + stbtt__fill_active_edges_new(scanline, scanline2+1, result->w, active, scan_y_top); + + { + float sum = 0; + for (i=0; i < result->w; ++i) { + float k; + int m; + sum += scanline2[i]; + k = scanline[i] + sum; + k = (float) STBTT_fabs(k)*255 + 0.5f; + m = (int) k; + if (m > 255) m = 255; + result->pixels[j*result->stride + i] = (unsigned char) m; + } + } + // advance all the edges + step = &active; + while (*step) { + stbtt__active_edge *z = *step; + z->fx += z->fdx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + + ++y; + ++j; + } + + stbtt__hheap_cleanup(&hh, userdata); + + if (scanline != scanline_data) + STBTT_free(scanline, userdata); +} +#else +#error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + +#define STBTT__COMPARE(a,b) ((a)->y0 < (b)->y0) + +static void stbtt__sort_edges_ins_sort(stbtt__edge *p, int n) +{ + int i,j; + for (i=1; i < n; ++i) { + stbtt__edge t = p[i], *a = &t; + j = i; + while (j > 0) { + stbtt__edge *b = &p[j-1]; + int c = STBTT__COMPARE(a,b); + if (!c) break; + p[j] = p[j-1]; + --j; + } + if (i != j) + p[j] = t; + } +} + +static void stbtt__sort_edges_quicksort(stbtt__edge *p, int n) +{ + /* threshold for transitioning to insertion sort */ + while (n > 12) { + stbtt__edge t; + int c01,c12,c,m,i,j; + + /* compute median of three */ + m = n >> 1; + c01 = STBTT__COMPARE(&p[0],&p[m]); + c12 = STBTT__COMPARE(&p[m],&p[n-1]); + /* if 0 >= mid >= end, or 0 < mid < end, then use mid */ + if (c01 != c12) { + /* otherwise, we'll need to swap something else to middle */ + int z; + c = STBTT__COMPARE(&p[0],&p[n-1]); + /* 0>mid && midn => n; 0 0 */ + /* 0n: 0>n => 0; 0 n */ + z = (c == c12) ? 0 : n-1; + t = p[z]; + p[z] = p[m]; + p[m] = t; + } + /* now p[m] is the median-of-three */ + /* swap it to the beginning so it won't move around */ + t = p[0]; + p[0] = p[m]; + p[m] = t; + + /* partition loop */ + i=1; + j=n-1; + for(;;) { + /* handling of equality is crucial here */ + /* for sentinels & efficiency with duplicates */ + for (;;++i) { + if (!STBTT__COMPARE(&p[i], &p[0])) break; + } + for (;;--j) { + if (!STBTT__COMPARE(&p[0], &p[j])) break; + } + /* make sure we haven't crossed */ + if (i >= j) break; + t = p[i]; + p[i] = p[j]; + p[j] = t; + + ++i; + --j; + } + /* recurse on smaller side, iterate on larger */ + if (j < (n-i)) { + stbtt__sort_edges_quicksort(p,j); + p = p+i; + n = n-i; + } else { + stbtt__sort_edges_quicksort(p+i, n-i); + n = j; + } + } +} + +static void stbtt__sort_edges(stbtt__edge *p, int n) +{ + stbtt__sort_edges_quicksort(p, n); + stbtt__sort_edges_ins_sort(p, n); +} + +typedef struct +{ + float x,y; +} stbtt__point; + +static void stbtt__rasterize(stbtt__bitmap *result, stbtt__point *pts, int *wcount, int windings, float scale_x, float scale_y, float shift_x, float shift_y, int off_x, int off_y, int invert, void *userdata) +{ + float y_scale_inv = invert ? -scale_y : scale_y; + stbtt__edge *e; + int n,i,j,k,m; +#if STBTT_RASTERIZER_VERSION == 1 + int vsubsample = result->h < 8 ? 15 : 5; +#elif STBTT_RASTERIZER_VERSION == 2 + int vsubsample = 1; +#else + #error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + // vsubsample should divide 255 evenly; otherwise we won't reach full opacity + + // now we have to blow out the windings into explicit edge lists + n = 0; + for (i=0; i < windings; ++i) + n += wcount[i]; + + e = (stbtt__edge *) STBTT_malloc(sizeof(*e) * (n+1), userdata); // add an extra one as a sentinel + if (e == 0) return; + n = 0; + + m=0; + for (i=0; i < windings; ++i) { + stbtt__point *p = pts + m; + m += wcount[i]; + j = wcount[i]-1; + for (k=0; k < wcount[i]; j=k++) { + int a=k,b=j; + // skip the edge if horizontal + if (p[j].y == p[k].y) + continue; + // add edge from j to k to the list + e[n].invert = 0; + if (invert ? p[j].y > p[k].y : p[j].y < p[k].y) { + e[n].invert = 1; + a=j,b=k; + } + e[n].x0 = p[a].x * scale_x + shift_x; + e[n].y0 = (p[a].y * y_scale_inv + shift_y) * vsubsample; + e[n].x1 = p[b].x * scale_x + shift_x; + e[n].y1 = (p[b].y * y_scale_inv + shift_y) * vsubsample; + ++n; + } + } + + // now sort the edges by their highest point (should snap to integer, and then by x) + //STBTT_sort(e, n, sizeof(e[0]), stbtt__edge_compare); + stbtt__sort_edges(e, n); + + // now, traverse the scanlines and find the intersections on each scanline, use xor winding rule + stbtt__rasterize_sorted_edges(result, e, n, vsubsample, off_x, off_y, userdata); + + STBTT_free(e, userdata); +} + +static void stbtt__add_point(stbtt__point *points, int n, float x, float y) +{ + if (!points) return; // during first pass, it's unallocated + points[n].x = x; + points[n].y = y; +} + +// tessellate until threshold p is happy... @TODO warped to compensate for non-linear stretching +static int stbtt__tesselate_curve(stbtt__point *points, int *num_points, float x0, float y0, float x1, float y1, float x2, float y2, float objspace_flatness_squared, int n) +{ + // midpoint + float mx = (x0 + 2*x1 + x2)/4; + float my = (y0 + 2*y1 + y2)/4; + // versus directly drawn line + float dx = (x0+x2)/2 - mx; + float dy = (y0+y2)/2 - my; + if (n > 16) // 65536 segments on one curve better be enough! + return 1; + if (dx*dx+dy*dy > objspace_flatness_squared) { // half-pixel error allowed... need to be smaller if AA + stbtt__tesselate_curve(points, num_points, x0,y0, (x0+x1)/2.0f,(y0+y1)/2.0f, mx,my, objspace_flatness_squared,n+1); + stbtt__tesselate_curve(points, num_points, mx,my, (x1+x2)/2.0f,(y1+y2)/2.0f, x2,y2, objspace_flatness_squared,n+1); + } else { + stbtt__add_point(points, *num_points,x2,y2); + *num_points = *num_points+1; + } + return 1; +} + +static void stbtt__tesselate_cubic(stbtt__point *points, int *num_points, float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3, float objspace_flatness_squared, int n) +{ + // @TODO this "flatness" calculation is just made-up nonsense that seems to work well enough + float dx0 = x1-x0; + float dy0 = y1-y0; + float dx1 = x2-x1; + float dy1 = y2-y1; + float dx2 = x3-x2; + float dy2 = y3-y2; + float dx = x3-x0; + float dy = y3-y0; + float longlen = (float) (STBTT_sqrt(dx0*dx0+dy0*dy0)+STBTT_sqrt(dx1*dx1+dy1*dy1)+STBTT_sqrt(dx2*dx2+dy2*dy2)); + float shortlen = (float) STBTT_sqrt(dx*dx+dy*dy); + float flatness_squared = longlen*longlen-shortlen*shortlen; + + if (n > 16) // 65536 segments on one curve better be enough! + return; + + if (flatness_squared > objspace_flatness_squared) { + float x01 = (x0+x1)/2; + float y01 = (y0+y1)/2; + float x12 = (x1+x2)/2; + float y12 = (y1+y2)/2; + float x23 = (x2+x3)/2; + float y23 = (y2+y3)/2; + + float xa = (x01+x12)/2; + float ya = (y01+y12)/2; + float xb = (x12+x23)/2; + float yb = (y12+y23)/2; + + float mx = (xa+xb)/2; + float my = (ya+yb)/2; + + stbtt__tesselate_cubic(points, num_points, x0,y0, x01,y01, xa,ya, mx,my, objspace_flatness_squared,n+1); + stbtt__tesselate_cubic(points, num_points, mx,my, xb,yb, x23,y23, x3,y3, objspace_flatness_squared,n+1); + } else { + stbtt__add_point(points, *num_points,x3,y3); + *num_points = *num_points+1; + } +} + +// returns number of contours +static stbtt__point *stbtt_FlattenCurves(stbtt_vertex *vertices, int num_verts, float objspace_flatness, int **contour_lengths, int *num_contours, void *userdata) +{ + stbtt__point *points=0; + int num_points=0; + + float objspace_flatness_squared = objspace_flatness * objspace_flatness; + int i,n=0,start=0, pass; + + // count how many "moves" there are to get the contour count + for (i=0; i < num_verts; ++i) + if (vertices[i].type == STBTT_vmove) + ++n; + + *num_contours = n; + if (n == 0) return 0; + + *contour_lengths = (int *) STBTT_malloc(sizeof(**contour_lengths) * n, userdata); + + if (*contour_lengths == 0) { + *num_contours = 0; + return 0; + } + + // make two passes through the points so we don't need to realloc + for (pass=0; pass < 2; ++pass) { + float x=0,y=0; + if (pass == 1) { + points = (stbtt__point *) STBTT_malloc(num_points * sizeof(points[0]), userdata); + if (points == NULL) goto error; + } + num_points = 0; + n= -1; + for (i=0; i < num_verts; ++i) { + switch (vertices[i].type) { + case STBTT_vmove: + // start the next contour + if (n >= 0) + (*contour_lengths)[n] = num_points - start; + ++n; + start = num_points; + + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x,y); + break; + case STBTT_vline: + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x, y); + break; + case STBTT_vcurve: + stbtt__tesselate_curve(points, &num_points, x,y, + vertices[i].cx, vertices[i].cy, + vertices[i].x, vertices[i].y, + objspace_flatness_squared, 0); + x = vertices[i].x, y = vertices[i].y; + break; + case STBTT_vcubic: + stbtt__tesselate_cubic(points, &num_points, x,y, + vertices[i].cx, vertices[i].cy, + vertices[i].cx1, vertices[i].cy1, + vertices[i].x, vertices[i].y, + objspace_flatness_squared, 0); + x = vertices[i].x, y = vertices[i].y; + break; + } + } + (*contour_lengths)[n] = num_points - start; + } + + return points; +error: + STBTT_free(points, userdata); + STBTT_free(*contour_lengths, userdata); + *contour_lengths = 0; + *num_contours = 0; + return NULL; +} + +STBTT_DEF void stbtt_Rasterize(stbtt__bitmap *result, float flatness_in_pixels, stbtt_vertex *vertices, int num_verts, float scale_x, float scale_y, float shift_x, float shift_y, int x_off, int y_off, int invert, void *userdata) +{ + float scale = scale_x > scale_y ? scale_y : scale_x; + int winding_count = 0; + int *winding_lengths = NULL; + stbtt__point *windings = stbtt_FlattenCurves(vertices, num_verts, flatness_in_pixels / scale, &winding_lengths, &winding_count, userdata); + if (windings) { + stbtt__rasterize(result, windings, winding_lengths, winding_count, scale_x, scale_y, shift_x, shift_y, x_off, y_off, invert, userdata); + STBTT_free(winding_lengths, userdata); + STBTT_free(windings, userdata); + } +} + +STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata) +{ + STBTT_free(bitmap, userdata); +} + +STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff) +{ + int ix0,iy0,ix1,iy1; + stbtt__bitmap gbm; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + + if (scale_x == 0) scale_x = scale_y; + if (scale_y == 0) { + if (scale_x == 0) { + STBTT_free(vertices, info->userdata); + return NULL; + } + scale_y = scale_x; + } + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,&ix1,&iy1); + + // now we get the size + gbm.w = (ix1 - ix0); + gbm.h = (iy1 - iy0); + gbm.pixels = NULL; // in case we error + + if (width ) *width = gbm.w; + if (height) *height = gbm.h; + if (xoff ) *xoff = ix0; + if (yoff ) *yoff = iy0; + + if (gbm.w && gbm.h) { + gbm.pixels = (unsigned char *) STBTT_malloc(gbm.w * gbm.h, info->userdata); + if (gbm.pixels) { + gbm.stride = gbm.w; + + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0, iy0, 1, info->userdata); + } + } + STBTT_free(vertices, info->userdata); + return gbm.pixels; +} + +STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y, 0.0f, 0.0f, glyph, width, height, xoff, yoff); +} + +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph) +{ + int ix0,iy0; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + stbtt__bitmap gbm; + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,0,0); + gbm.pixels = output; + gbm.w = out_w; + gbm.h = out_h; + gbm.stride = out_stride; + + if (gbm.w && gbm.h) + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0,iy0, 1, info->userdata); + + STBTT_free(vertices, info->userdata); +} + +STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph) +{ + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, glyph); +} + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y,shift_x,shift_y, stbtt_FindGlyphIndex(info,codepoint), width,height,xoff,yoff); +} + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int codepoint) +{ + stbtt_MakeGlyphBitmapSubpixelPrefilter(info, output, out_w, out_h, out_stride, scale_x, scale_y, shift_x, shift_y, oversample_x, oversample_y, sub_x, sub_y, stbtt_FindGlyphIndex(info,codepoint)); +} + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint) +{ + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, shift_x, shift_y, stbtt_FindGlyphIndex(info,codepoint)); +} + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetCodepointBitmapSubpixel(info, scale_x, scale_y, 0.0f,0.0f, codepoint, width,height,xoff,yoff); +} + +STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint) +{ + stbtt_MakeCodepointBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, codepoint); +} + +////////////////////////////////////////////////////////////////////////////// +// +// bitmap baking +// +// This is SUPER-CRAPPY packing to keep source code small + +static int stbtt_BakeFontBitmap_internal(unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata) +{ + float scale; + int x,y,bottom_y, i; + stbtt_fontinfo f; + f.userdata = NULL; + if (!stbtt_InitFont(&f, data, offset)) + return -1; + STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels + x=y=1; + bottom_y = 1; + + scale = stbtt_ScaleForPixelHeight(&f, pixel_height); + + for (i=0; i < num_chars; ++i) { + int advance, lsb, x0,y0,x1,y1,gw,gh; + int g = stbtt_FindGlyphIndex(&f, first_char + i); + stbtt_GetGlyphHMetrics(&f, g, &advance, &lsb); + stbtt_GetGlyphBitmapBox(&f, g, scale,scale, &x0,&y0,&x1,&y1); + gw = x1-x0; + gh = y1-y0; + if (x + gw + 1 >= pw) + y = bottom_y, x = 1; // advance to next row + if (y + gh + 1 >= ph) // check if it fits vertically AFTER potentially moving to next row + return -i; + STBTT_assert(x+gw < pw); + STBTT_assert(y+gh < ph); + stbtt_MakeGlyphBitmap(&f, pixels+x+y*pw, gw,gh,pw, scale,scale, g); + chardata[i].x0 = (stbtt_int16) x; + chardata[i].y0 = (stbtt_int16) y; + chardata[i].x1 = (stbtt_int16) (x + gw); + chardata[i].y1 = (stbtt_int16) (y + gh); + chardata[i].xadvance = scale * advance; + chardata[i].xoff = (float) x0; + chardata[i].yoff = (float) y0; + x = x + gw + 1; + if (y+gh+1 > bottom_y) + bottom_y = y+gh+1; + } + return bottom_y; +} + +STBTT_DEF void stbtt_GetBakedQuad(const stbtt_bakedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int opengl_fillrule) +{ + float d3d_bias = opengl_fillrule ? 0 : -0.5f; + float ipw = 1.0f / pw, iph = 1.0f / ph; + const stbtt_bakedchar *b = chardata + char_index; + int round_x = STBTT_ifloor((*xpos + b->xoff) + 0.5f); + int round_y = STBTT_ifloor((*ypos + b->yoff) + 0.5f); + + q->x0 = round_x + d3d_bias; + q->y0 = round_y + d3d_bias; + q->x1 = round_x + b->x1 - b->x0 + d3d_bias; + q->y1 = round_y + b->y1 - b->y0 + d3d_bias; + + q->s0 = b->x0 * ipw; + q->t0 = b->y0 * iph; + q->s1 = b->x1 * ipw; + q->t1 = b->y1 * iph; + + *xpos += b->xadvance; +} + +////////////////////////////////////////////////////////////////////////////// +// +// rectangle packing replacement routines if you don't have stb_rect_pack.h +// + +#ifndef STB_RECT_PACK_VERSION + +typedef int stbrp_coord; + +//////////////////////////////////////////////////////////////////////////////////// +// // +// // +// COMPILER WARNING ?!?!? // +// // +// // +// if you get a compile warning due to these symbols being defined more than // +// once, move #include "stb_rect_pack.h" before #include "stb_truetype.h" // +// // +//////////////////////////////////////////////////////////////////////////////////// + +typedef struct +{ + int width,height; + int x,y,bottom_y; +} stbrp_context; + +typedef struct +{ + unsigned char x; +} stbrp_node; + +struct stbrp_rect +{ + stbrp_coord x,y; + int id,w,h,was_packed; +}; + +static void stbrp_init_target(stbrp_context *con, int pw, int ph, stbrp_node *nodes, int num_nodes) +{ + con->width = pw; + con->height = ph; + con->x = 0; + con->y = 0; + con->bottom_y = 0; + STBTT__NOTUSED(nodes); + STBTT__NOTUSED(num_nodes); +} + +static void stbrp_pack_rects(stbrp_context *con, stbrp_rect *rects, int num_rects) +{ + int i; + for (i=0; i < num_rects; ++i) { + if (con->x + rects[i].w > con->width) { + con->x = 0; + con->y = con->bottom_y; + } + if (con->y + rects[i].h > con->height) + break; + rects[i].x = con->x; + rects[i].y = con->y; + rects[i].was_packed = 1; + con->x += rects[i].w; + if (con->y + rects[i].h > con->bottom_y) + con->bottom_y = con->y + rects[i].h; + } + for ( ; i < num_rects; ++i) + rects[i].was_packed = 0; +} +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// bitmap baking +// +// This is SUPER-AWESOME (tm Ryan Gordon) packing using stb_rect_pack.h. If +// stb_rect_pack.h isn't available, it uses the BakeFontBitmap strategy. + +STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, int pw, int ph, int stride_in_bytes, int padding, void *alloc_context) +{ + stbrp_context *context = (stbrp_context *) STBTT_malloc(sizeof(*context) ,alloc_context); + int num_nodes = pw - padding; + stbrp_node *nodes = (stbrp_node *) STBTT_malloc(sizeof(*nodes ) * num_nodes,alloc_context); + + if (context == NULL || nodes == NULL) { + if (context != NULL) STBTT_free(context, alloc_context); + if (nodes != NULL) STBTT_free(nodes , alloc_context); + return 0; + } + + spc->user_allocator_context = alloc_context; + spc->width = pw; + spc->height = ph; + spc->pixels = pixels; + spc->pack_info = context; + spc->nodes = nodes; + spc->padding = padding; + spc->stride_in_bytes = stride_in_bytes != 0 ? stride_in_bytes : pw; + spc->h_oversample = 1; + spc->v_oversample = 1; + spc->skip_missing = 0; + + stbrp_init_target(context, pw-padding, ph-padding, nodes, num_nodes); + + if (pixels) + STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels + + return 1; +} + +STBTT_DEF void stbtt_PackEnd (stbtt_pack_context *spc) +{ + STBTT_free(spc->nodes , spc->user_allocator_context); + STBTT_free(spc->pack_info, spc->user_allocator_context); +} + +STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h_oversample, unsigned int v_oversample) +{ + STBTT_assert(h_oversample <= STBTT_MAX_OVERSAMPLE); + STBTT_assert(v_oversample <= STBTT_MAX_OVERSAMPLE); + if (h_oversample <= STBTT_MAX_OVERSAMPLE) + spc->h_oversample = h_oversample; + if (v_oversample <= STBTT_MAX_OVERSAMPLE) + spc->v_oversample = v_oversample; +} + +STBTT_DEF void stbtt_PackSetSkipMissingCodepoints(stbtt_pack_context *spc, int skip) +{ + spc->skip_missing = skip; +} + +#define STBTT__OVER_MASK (STBTT_MAX_OVERSAMPLE-1) + +static void stbtt__h_prefilter(unsigned char *pixels, int w, int h, int stride_in_bytes, unsigned int kernel_width) +{ + unsigned char buffer[STBTT_MAX_OVERSAMPLE]; + int safe_w = w - kernel_width; + int j; + STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze + for (j=0; j < h; ++j) { + int i; + unsigned int total; + STBTT_memset(buffer, 0, kernel_width); + + total = 0; + + // make kernel_width a constant in common cases so compiler can optimize out the divide + switch (kernel_width) { + case 2: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 2); + } + break; + case 3: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 3); + } + break; + case 4: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 4); + } + break; + case 5: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 5); + } + break; + default: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / kernel_width); + } + break; + } + + for (; i < w; ++i) { + STBTT_assert(pixels[i] == 0); + total -= buffer[i & STBTT__OVER_MASK]; + pixels[i] = (unsigned char) (total / kernel_width); + } + + pixels += stride_in_bytes; + } +} + +static void stbtt__v_prefilter(unsigned char *pixels, int w, int h, int stride_in_bytes, unsigned int kernel_width) +{ + unsigned char buffer[STBTT_MAX_OVERSAMPLE]; + int safe_h = h - kernel_width; + int j; + STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze + for (j=0; j < w; ++j) { + int i; + unsigned int total; + STBTT_memset(buffer, 0, kernel_width); + + total = 0; + + // make kernel_width a constant in common cases so compiler can optimize out the divide + switch (kernel_width) { + case 2: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 2); + } + break; + case 3: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 3); + } + break; + case 4: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 4); + } + break; + case 5: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 5); + } + break; + default: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / kernel_width); + } + break; + } + + for (; i < h; ++i) { + STBTT_assert(pixels[i*stride_in_bytes] == 0); + total -= buffer[i & STBTT__OVER_MASK]; + pixels[i*stride_in_bytes] = (unsigned char) (total / kernel_width); + } + + pixels += 1; + } +} + +static float stbtt__oversample_shift(int oversample) +{ + if (!oversample) + return 0.0f; + + // The prefilter is a box filter of width "oversample", + // which shifts phase by (oversample - 1)/2 pixels in + // oversampled space. We want to shift in the opposite + // direction to counter this. + return (float)-(oversample - 1) / (2.0f * (float)oversample); +} + +// rects array must be big enough to accommodate all characters in the given ranges +STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) +{ + int i,j,k; + + k=0; + for (i=0; i < num_ranges; ++i) { + float fh = ranges[i].font_size; + float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); + ranges[i].h_oversample = (unsigned char) spc->h_oversample; + ranges[i].v_oversample = (unsigned char) spc->v_oversample; + for (j=0; j < ranges[i].num_chars; ++j) { + int x0,y0,x1,y1; + int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; + int glyph = stbtt_FindGlyphIndex(info, codepoint); + if (glyph == 0 && spc->skip_missing) { + rects[k].w = rects[k].h = 0; + } else { + stbtt_GetGlyphBitmapBoxSubpixel(info,glyph, + scale * spc->h_oversample, + scale * spc->v_oversample, + 0,0, + &x0,&y0,&x1,&y1); + rects[k].w = (stbrp_coord) (x1-x0 + spc->padding + spc->h_oversample-1); + rects[k].h = (stbrp_coord) (y1-y0 + spc->padding + spc->v_oversample-1); + } + ++k; + } + } + + return k; +} + +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int prefilter_x, int prefilter_y, float *sub_x, float *sub_y, int glyph) +{ + stbtt_MakeGlyphBitmapSubpixel(info, + output, + out_w - (prefilter_x - 1), + out_h - (prefilter_y - 1), + out_stride, + scale_x, + scale_y, + shift_x, + shift_y, + glyph); + + if (prefilter_x > 1) + stbtt__h_prefilter(output, out_w, out_h, out_stride, prefilter_x); + + if (prefilter_y > 1) + stbtt__v_prefilter(output, out_w, out_h, out_stride, prefilter_y); + + *sub_x = stbtt__oversample_shift(prefilter_x); + *sub_y = stbtt__oversample_shift(prefilter_y); +} + +// rects array must be big enough to accommodate all characters in the given ranges +STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) +{ + int i,j,k, return_value = 1; + + // save current values + int old_h_over = spc->h_oversample; + int old_v_over = spc->v_oversample; + + k = 0; + for (i=0; i < num_ranges; ++i) { + float fh = ranges[i].font_size; + float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); + float recip_h,recip_v,sub_x,sub_y; + spc->h_oversample = ranges[i].h_oversample; + spc->v_oversample = ranges[i].v_oversample; + recip_h = 1.0f / spc->h_oversample; + recip_v = 1.0f / spc->v_oversample; + sub_x = stbtt__oversample_shift(spc->h_oversample); + sub_y = stbtt__oversample_shift(spc->v_oversample); + for (j=0; j < ranges[i].num_chars; ++j) { + stbrp_rect *r = &rects[k]; + if (r->was_packed && r->w != 0 && r->h != 0) { + stbtt_packedchar *bc = &ranges[i].chardata_for_range[j]; + int advance, lsb, x0,y0,x1,y1; + int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; + int glyph = stbtt_FindGlyphIndex(info, codepoint); + stbrp_coord pad = (stbrp_coord) spc->padding; + + // pad on left and top + r->x += pad; + r->y += pad; + r->w -= pad; + r->h -= pad; + stbtt_GetGlyphHMetrics(info, glyph, &advance, &lsb); + stbtt_GetGlyphBitmapBox(info, glyph, + scale * spc->h_oversample, + scale * spc->v_oversample, + &x0,&y0,&x1,&y1); + stbtt_MakeGlyphBitmapSubpixel(info, + spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w - spc->h_oversample+1, + r->h - spc->v_oversample+1, + spc->stride_in_bytes, + scale * spc->h_oversample, + scale * spc->v_oversample, + 0,0, + glyph); + + if (spc->h_oversample > 1) + stbtt__h_prefilter(spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w, r->h, spc->stride_in_bytes, + spc->h_oversample); + + if (spc->v_oversample > 1) + stbtt__v_prefilter(spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w, r->h, spc->stride_in_bytes, + spc->v_oversample); + + bc->x0 = (stbtt_int16) r->x; + bc->y0 = (stbtt_int16) r->y; + bc->x1 = (stbtt_int16) (r->x + r->w); + bc->y1 = (stbtt_int16) (r->y + r->h); + bc->xadvance = scale * advance; + bc->xoff = (float) x0 * recip_h + sub_x; + bc->yoff = (float) y0 * recip_v + sub_y; + bc->xoff2 = (x0 + r->w) * recip_h + sub_x; + bc->yoff2 = (y0 + r->h) * recip_v + sub_y; + } else { + return_value = 0; // if any fail, report failure + } + + ++k; + } + } + + // restore original values + spc->h_oversample = old_h_over; + spc->v_oversample = old_v_over; + + return return_value; +} + +STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect *rects, int num_rects) +{ + stbrp_pack_rects((stbrp_context *) spc->pack_info, rects, num_rects); +} + +STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges) +{ + stbtt_fontinfo info; + int i,j,n, return_value; // [DEAR IMGUI] removed = 1 + //stbrp_context *context = (stbrp_context *) spc->pack_info; + stbrp_rect *rects; + + // flag all characters as NOT packed + for (i=0; i < num_ranges; ++i) + for (j=0; j < ranges[i].num_chars; ++j) + ranges[i].chardata_for_range[j].x0 = + ranges[i].chardata_for_range[j].y0 = + ranges[i].chardata_for_range[j].x1 = + ranges[i].chardata_for_range[j].y1 = 0; + + n = 0; + for (i=0; i < num_ranges; ++i) + n += ranges[i].num_chars; + + rects = (stbrp_rect *) STBTT_malloc(sizeof(*rects) * n, spc->user_allocator_context); + if (rects == NULL) + return 0; + + info.userdata = spc->user_allocator_context; + stbtt_InitFont(&info, fontdata, stbtt_GetFontOffsetForIndex(fontdata,font_index)); + + n = stbtt_PackFontRangesGatherRects(spc, &info, ranges, num_ranges, rects); + + stbtt_PackFontRangesPackRects(spc, rects, n); + + return_value = stbtt_PackFontRangesRenderIntoRects(spc, &info, ranges, num_ranges, rects); + + STBTT_free(rects, spc->user_allocator_context); + return return_value; +} + +STBTT_DEF int stbtt_PackFontRange(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, float font_size, + int first_unicode_codepoint_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range) +{ + stbtt_pack_range range; + range.first_unicode_codepoint_in_range = first_unicode_codepoint_in_range; + range.array_of_unicode_codepoints = NULL; + range.num_chars = num_chars_in_range; + range.chardata_for_range = chardata_for_range; + range.font_size = font_size; + return stbtt_PackFontRanges(spc, fontdata, font_index, &range, 1); +} + +STBTT_DEF void stbtt_GetScaledFontVMetrics(const unsigned char *fontdata, int index, float size, float *ascent, float *descent, float *lineGap) +{ + int i_ascent, i_descent, i_lineGap; + float scale; + stbtt_fontinfo info; + stbtt_InitFont(&info, fontdata, stbtt_GetFontOffsetForIndex(fontdata, index)); + scale = size > 0 ? stbtt_ScaleForPixelHeight(&info, size) : stbtt_ScaleForMappingEmToPixels(&info, -size); + stbtt_GetFontVMetrics(&info, &i_ascent, &i_descent, &i_lineGap); + *ascent = (float) i_ascent * scale; + *descent = (float) i_descent * scale; + *lineGap = (float) i_lineGap * scale; +} + +STBTT_DEF void stbtt_GetPackedQuad(const stbtt_packedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int align_to_integer) +{ + float ipw = 1.0f / pw, iph = 1.0f / ph; + const stbtt_packedchar *b = chardata + char_index; + + if (align_to_integer) { + float x = (float) STBTT_ifloor((*xpos + b->xoff) + 0.5f); + float y = (float) STBTT_ifloor((*ypos + b->yoff) + 0.5f); + q->x0 = x; + q->y0 = y; + q->x1 = x + b->xoff2 - b->xoff; + q->y1 = y + b->yoff2 - b->yoff; + } else { + q->x0 = *xpos + b->xoff; + q->y0 = *ypos + b->yoff; + q->x1 = *xpos + b->xoff2; + q->y1 = *ypos + b->yoff2; + } + + q->s0 = b->x0 * ipw; + q->t0 = b->y0 * iph; + q->s1 = b->x1 * ipw; + q->t1 = b->y1 * iph; + + *xpos += b->xadvance; +} + +////////////////////////////////////////////////////////////////////////////// +// +// sdf computation +// + +#define STBTT_min(a,b) ((a) < (b) ? (a) : (b)) +#define STBTT_max(a,b) ((a) < (b) ? (b) : (a)) + +static int stbtt__ray_intersect_bezier(float orig[2], float ray[2], float q0[2], float q1[2], float q2[2], float hits[2][2]) +{ + float q0perp = q0[1]*ray[0] - q0[0]*ray[1]; + float q1perp = q1[1]*ray[0] - q1[0]*ray[1]; + float q2perp = q2[1]*ray[0] - q2[0]*ray[1]; + float roperp = orig[1]*ray[0] - orig[0]*ray[1]; + + float a = q0perp - 2*q1perp + q2perp; + float b = q1perp - q0perp; + float c = q0perp - roperp; + + float s0 = 0., s1 = 0.; + int num_s = 0; + + if (a != 0.0) { + float discr = b*b - a*c; + if (discr > 0.0) { + float rcpna = -1 / a; + float d = (float) STBTT_sqrt(discr); + s0 = (b+d) * rcpna; + s1 = (b-d) * rcpna; + if (s0 >= 0.0 && s0 <= 1.0) + num_s = 1; + if (d > 0.0 && s1 >= 0.0 && s1 <= 1.0) { + if (num_s == 0) s0 = s1; + ++num_s; + } + } + } else { + // 2*b*s + c = 0 + // s = -c / (2*b) + s0 = c / (-2 * b); + if (s0 >= 0.0 && s0 <= 1.0) + num_s = 1; + } + + if (num_s == 0) + return 0; + else { + float rcp_len2 = 1 / (ray[0]*ray[0] + ray[1]*ray[1]); + float rayn_x = ray[0] * rcp_len2, rayn_y = ray[1] * rcp_len2; + + float q0d = q0[0]*rayn_x + q0[1]*rayn_y; + float q1d = q1[0]*rayn_x + q1[1]*rayn_y; + float q2d = q2[0]*rayn_x + q2[1]*rayn_y; + float rod = orig[0]*rayn_x + orig[1]*rayn_y; + + float q10d = q1d - q0d; + float q20d = q2d - q0d; + float q0rd = q0d - rod; + + hits[0][0] = q0rd + s0*(2.0f - 2.0f*s0)*q10d + s0*s0*q20d; + hits[0][1] = a*s0+b; + + if (num_s > 1) { + hits[1][0] = q0rd + s1*(2.0f - 2.0f*s1)*q10d + s1*s1*q20d; + hits[1][1] = a*s1+b; + return 2; + } else { + return 1; + } + } +} + +static int equal(float *a, float *b) +{ + return (a[0] == b[0] && a[1] == b[1]); +} + +static int stbtt__compute_crossings_x(float x, float y, int nverts, stbtt_vertex *verts) +{ + int i; + float orig[2], ray[2] = { 1, 0 }; + float y_frac; + int winding = 0; + + orig[0] = x; + //orig[1] = y; // [DEAR IMGUI] commented double assignment + + // make sure y never passes through a vertex of the shape + y_frac = (float) STBTT_fmod(y, 1.0f); + if (y_frac < 0.01f) + y += 0.01f; + else if (y_frac > 0.99f) + y -= 0.01f; + orig[1] = y; + + // test a ray from (-infinity,y) to (x,y) + for (i=0; i < nverts; ++i) { + if (verts[i].type == STBTT_vline) { + int x0 = (int) verts[i-1].x, y0 = (int) verts[i-1].y; + int x1 = (int) verts[i ].x, y1 = (int) verts[i ].y; + if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) { + float x_inter = (y - y0) / (y1 - y0) * (x1-x0) + x0; + if (x_inter < x) + winding += (y0 < y1) ? 1 : -1; + } + } + if (verts[i].type == STBTT_vcurve) { + int x0 = (int) verts[i-1].x , y0 = (int) verts[i-1].y ; + int x1 = (int) verts[i ].cx, y1 = (int) verts[i ].cy; + int x2 = (int) verts[i ].x , y2 = (int) verts[i ].y ; + int ax = STBTT_min(x0,STBTT_min(x1,x2)), ay = STBTT_min(y0,STBTT_min(y1,y2)); + int by = STBTT_max(y0,STBTT_max(y1,y2)); + if (y > ay && y < by && x > ax) { + float q0[2],q1[2],q2[2]; + float hits[2][2]; + q0[0] = (float)x0; + q0[1] = (float)y0; + q1[0] = (float)x1; + q1[1] = (float)y1; + q2[0] = (float)x2; + q2[1] = (float)y2; + if (equal(q0,q1) || equal(q1,q2)) { + x0 = (int)verts[i-1].x; + y0 = (int)verts[i-1].y; + x1 = (int)verts[i ].x; + y1 = (int)verts[i ].y; + if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) { + float x_inter = (y - y0) / (y1 - y0) * (x1-x0) + x0; + if (x_inter < x) + winding += (y0 < y1) ? 1 : -1; + } + } else { + int num_hits = stbtt__ray_intersect_bezier(orig, ray, q0, q1, q2, hits); + if (num_hits >= 1) + if (hits[0][0] < 0) + winding += (hits[0][1] < 0 ? -1 : 1); + if (num_hits >= 2) + if (hits[1][0] < 0) + winding += (hits[1][1] < 0 ? -1 : 1); + } + } + } + } + return winding; +} + +static float stbtt__cuberoot( float x ) +{ + if (x<0) + return -(float) STBTT_pow(-x,1.0f/3.0f); + else + return (float) STBTT_pow( x,1.0f/3.0f); +} + +// x^3 + c*x^2 + b*x + a = 0 +static int stbtt__solve_cubic(float a, float b, float c, float* r) +{ + float s = -a / 3; + float p = b - a*a / 3; + float q = a * (2*a*a - 9*b) / 27 + c; + float p3 = p*p*p; + float d = q*q + 4*p3 / 27; + if (d >= 0) { + float z = (float) STBTT_sqrt(d); + float u = (-q + z) / 2; + float v = (-q - z) / 2; + u = stbtt__cuberoot(u); + v = stbtt__cuberoot(v); + r[0] = s + u + v; + return 1; + } else { + float u = (float) STBTT_sqrt(-p/3); + float v = (float) STBTT_acos(-STBTT_sqrt(-27/p3) * q / 2) / 3; // p3 must be negative, since d is negative + float m = (float) STBTT_cos(v); + float n = (float) STBTT_cos(v-3.141592/2)*1.732050808f; + r[0] = s + u * 2 * m; + r[1] = s - u * (m + n); + r[2] = s - u * (m - n); + + //STBTT_assert( STBTT_fabs(((r[0]+a)*r[0]+b)*r[0]+c) < 0.05f); // these asserts may not be safe at all scales, though they're in bezier t parameter units so maybe? + //STBTT_assert( STBTT_fabs(((r[1]+a)*r[1]+b)*r[1]+c) < 0.05f); + //STBTT_assert( STBTT_fabs(((r[2]+a)*r[2]+b)*r[2]+c) < 0.05f); + return 3; + } +} + +STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float scale, int glyph, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff) +{ + float scale_x = scale, scale_y = scale; + int ix0,iy0,ix1,iy1; + int w,h; + unsigned char *data; + + // if one scale is 0, use same scale for both + if (scale_x == 0) scale_x = scale_y; + if (scale_y == 0) { + if (scale_x == 0) return NULL; // if both scales are 0, return NULL + scale_y = scale_x; + } + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale, scale, 0.0f,0.0f, &ix0,&iy0,&ix1,&iy1); + + // if empty, return NULL + if (ix0 == ix1 || iy0 == iy1) + return NULL; + + ix0 -= padding; + iy0 -= padding; + ix1 += padding; + iy1 += padding; + + w = (ix1 - ix0); + h = (iy1 - iy0); + + if (width ) *width = w; + if (height) *height = h; + if (xoff ) *xoff = ix0; + if (yoff ) *yoff = iy0; + + // invert for y-downwards bitmaps + scale_y = -scale_y; + + { + int x,y,i,j; + float *precompute; + stbtt_vertex *verts; + int num_verts = stbtt_GetGlyphShape(info, glyph, &verts); + data = (unsigned char *) STBTT_malloc(w * h, info->userdata); + precompute = (float *) STBTT_malloc(num_verts * sizeof(float), info->userdata); + + for (i=0,j=num_verts-1; i < num_verts; j=i++) { + if (verts[i].type == STBTT_vline) { + float x0 = verts[i].x*scale_x, y0 = verts[i].y*scale_y; + float x1 = verts[j].x*scale_x, y1 = verts[j].y*scale_y; + float dist = (float) STBTT_sqrt((x1-x0)*(x1-x0) + (y1-y0)*(y1-y0)); + precompute[i] = (dist == 0) ? 0.0f : 1.0f / dist; + } else if (verts[i].type == STBTT_vcurve) { + float x2 = verts[j].x *scale_x, y2 = verts[j].y *scale_y; + float x1 = verts[i].cx*scale_x, y1 = verts[i].cy*scale_y; + float x0 = verts[i].x *scale_x, y0 = verts[i].y *scale_y; + float bx = x0 - 2*x1 + x2, by = y0 - 2*y1 + y2; + float len2 = bx*bx + by*by; + if (len2 != 0.0f) + precompute[i] = 1.0f / (bx*bx + by*by); + else + precompute[i] = 0.0f; + } else + precompute[i] = 0.0f; + } + + for (y=iy0; y < iy1; ++y) { + for (x=ix0; x < ix1; ++x) { + float val; + float min_dist = 999999.0f; + float sx = (float) x + 0.5f; + float sy = (float) y + 0.5f; + float x_gspace = (sx / scale_x); + float y_gspace = (sy / scale_y); + + int winding = stbtt__compute_crossings_x(x_gspace, y_gspace, num_verts, verts); // @OPTIMIZE: this could just be a rasterization, but needs to be line vs. non-tesselated curves so a new path + + for (i=0; i < num_verts; ++i) { + float x0 = verts[i].x*scale_x, y0 = verts[i].y*scale_y; + + // check against every point here rather than inside line/curve primitives -- @TODO: wrong if multiple 'moves' in a row produce a garbage point, and given culling, probably more efficient to do within line/curve + float dist2 = (x0-sx)*(x0-sx) + (y0-sy)*(y0-sy); + if (dist2 < min_dist*min_dist) + min_dist = (float) STBTT_sqrt(dist2); + + if (verts[i].type == STBTT_vline) { + float x1 = verts[i-1].x*scale_x, y1 = verts[i-1].y*scale_y; + + // coarse culling against bbox + //if (sx > STBTT_min(x0,x1)-min_dist && sx < STBTT_max(x0,x1)+min_dist && + // sy > STBTT_min(y0,y1)-min_dist && sy < STBTT_max(y0,y1)+min_dist) + float dist = (float) STBTT_fabs((x1-x0)*(y0-sy) - (y1-y0)*(x0-sx)) * precompute[i]; + STBTT_assert(i != 0); + if (dist < min_dist) { + // check position along line + // x' = x0 + t*(x1-x0), y' = y0 + t*(y1-y0) + // minimize (x'-sx)*(x'-sx)+(y'-sy)*(y'-sy) + float dx = x1-x0, dy = y1-y0; + float px = x0-sx, py = y0-sy; + // minimize (px+t*dx)^2 + (py+t*dy)^2 = px*px + 2*px*dx*t + t^2*dx*dx + py*py + 2*py*dy*t + t^2*dy*dy + // derivative: 2*px*dx + 2*py*dy + (2*dx*dx+2*dy*dy)*t, set to 0 and solve + float t = -(px*dx + py*dy) / (dx*dx + dy*dy); + if (t >= 0.0f && t <= 1.0f) + min_dist = dist; + } + } else if (verts[i].type == STBTT_vcurve) { + float x2 = verts[i-1].x *scale_x, y2 = verts[i-1].y *scale_y; + float x1 = verts[i ].cx*scale_x, y1 = verts[i ].cy*scale_y; + float box_x0 = STBTT_min(STBTT_min(x0,x1),x2); + float box_y0 = STBTT_min(STBTT_min(y0,y1),y2); + float box_x1 = STBTT_max(STBTT_max(x0,x1),x2); + float box_y1 = STBTT_max(STBTT_max(y0,y1),y2); + // coarse culling against bbox to avoid computing cubic unnecessarily + if (sx > box_x0-min_dist && sx < box_x1+min_dist && sy > box_y0-min_dist && sy < box_y1+min_dist) { + int num=0; + float ax = x1-x0, ay = y1-y0; + float bx = x0 - 2*x1 + x2, by = y0 - 2*y1 + y2; + float mx = x0 - sx, my = y0 - sy; + float res[3],px,py,t,it; + float a_inv = precompute[i]; + if (a_inv == 0.0) { // if a_inv is 0, it's 2nd degree so use quadratic formula + float a = 3*(ax*bx + ay*by); + float b = 2*(ax*ax + ay*ay) + (mx*bx+my*by); + float c = mx*ax+my*ay; + if (a == 0.0) { // if a is 0, it's linear + if (b != 0.0) { + res[num++] = -c/b; + } + } else { + float discriminant = b*b - 4*a*c; + if (discriminant < 0) + num = 0; + else { + float root = (float) STBTT_sqrt(discriminant); + res[0] = (-b - root)/(2*a); + res[1] = (-b + root)/(2*a); + num = 2; // don't bother distinguishing 1-solution case, as code below will still work + } + } + } else { + float b = 3*(ax*bx + ay*by) * a_inv; // could precompute this as it doesn't depend on sample point + float c = (2*(ax*ax + ay*ay) + (mx*bx+my*by)) * a_inv; + float d = (mx*ax+my*ay) * a_inv; + num = stbtt__solve_cubic(b, c, d, res); + } + if (num >= 1 && res[0] >= 0.0f && res[0] <= 1.0f) { + t = res[0], it = 1.0f - t; + px = it*it*x0 + 2*t*it*x1 + t*t*x2; + py = it*it*y0 + 2*t*it*y1 + t*t*y2; + dist2 = (px-sx)*(px-sx) + (py-sy)*(py-sy); + if (dist2 < min_dist * min_dist) + min_dist = (float) STBTT_sqrt(dist2); + } + if (num >= 2 && res[1] >= 0.0f && res[1] <= 1.0f) { + t = res[1], it = 1.0f - t; + px = it*it*x0 + 2*t*it*x1 + t*t*x2; + py = it*it*y0 + 2*t*it*y1 + t*t*y2; + dist2 = (px-sx)*(px-sx) + (py-sy)*(py-sy); + if (dist2 < min_dist * min_dist) + min_dist = (float) STBTT_sqrt(dist2); + } + if (num >= 3 && res[2] >= 0.0f && res[2] <= 1.0f) { + t = res[2], it = 1.0f - t; + px = it*it*x0 + 2*t*it*x1 + t*t*x2; + py = it*it*y0 + 2*t*it*y1 + t*t*y2; + dist2 = (px-sx)*(px-sx) + (py-sy)*(py-sy); + if (dist2 < min_dist * min_dist) + min_dist = (float) STBTT_sqrt(dist2); + } + } + } + } + if (winding == 0) + min_dist = -min_dist; // if outside the shape, value is negative + val = onedge_value + pixel_dist_scale * min_dist; + if (val < 0) + val = 0; + else if (val > 255) + val = 255; + data[(y-iy0)*w+(x-ix0)] = (unsigned char) val; + } + } + STBTT_free(precompute, info->userdata); + STBTT_free(verts, info->userdata); + } + return data; +} + +STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, float scale, int codepoint, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphSDF(info, scale, stbtt_FindGlyphIndex(info, codepoint), padding, onedge_value, pixel_dist_scale, width, height, xoff, yoff); +} + +STBTT_DEF void stbtt_FreeSDF(unsigned char *bitmap, void *userdata) +{ + STBTT_free(bitmap, userdata); +} + +////////////////////////////////////////////////////////////////////////////// +// +// font name matching -- recommended not to use this +// + +// check if a utf8 string contains a prefix which is the utf16 string; if so return length of matching utf8 string +static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(stbtt_uint8 *s1, stbtt_int32 len1, stbtt_uint8 *s2, stbtt_int32 len2) +{ + stbtt_int32 i=0; + + // convert utf16 to utf8 and compare the results while converting + while (len2) { + stbtt_uint16 ch = s2[0]*256 + s2[1]; + if (ch < 0x80) { + if (i >= len1) return -1; + if (s1[i++] != ch) return -1; + } else if (ch < 0x800) { + if (i+1 >= len1) return -1; + if (s1[i++] != 0xc0 + (ch >> 6)) return -1; + if (s1[i++] != 0x80 + (ch & 0x3f)) return -1; + } else if (ch >= 0xd800 && ch < 0xdc00) { + stbtt_uint32 c; + stbtt_uint16 ch2 = s2[2]*256 + s2[3]; + if (i+3 >= len1) return -1; + c = ((ch - 0xd800) << 10) + (ch2 - 0xdc00) + 0x10000; + if (s1[i++] != 0xf0 + (c >> 18)) return -1; + if (s1[i++] != 0x80 + ((c >> 12) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((c >> 6) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((c ) & 0x3f)) return -1; + s2 += 2; // plus another 2 below + len2 -= 2; + } else if (ch >= 0xdc00 && ch < 0xe000) { + return -1; + } else { + if (i+2 >= len1) return -1; + if (s1[i++] != 0xe0 + (ch >> 12)) return -1; + if (s1[i++] != 0x80 + ((ch >> 6) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((ch ) & 0x3f)) return -1; + } + s2 += 2; + len2 -= 2; + } + return i; +} + +static int stbtt_CompareUTF8toUTF16_bigendian_internal(char *s1, int len1, char *s2, int len2) +{ + return len1 == stbtt__CompareUTF8toUTF16_bigendian_prefix((stbtt_uint8*) s1, len1, (stbtt_uint8*) s2, len2); +} + +// returns results in whatever encoding you request... but note that 2-byte encodings +// will be BIG-ENDIAN... use stbtt_CompareUTF8toUTF16_bigendian() to compare +STBTT_DEF const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID) +{ + stbtt_int32 i,count,stringOffset; + stbtt_uint8 *fc = font->data; + stbtt_uint32 offset = font->fontstart; + stbtt_uint32 nm = stbtt__find_table(fc, offset, "name"); + if (!nm) return NULL; + + count = ttUSHORT(fc+nm+2); + stringOffset = nm + ttUSHORT(fc+nm+4); + for (i=0; i < count; ++i) { + stbtt_uint32 loc = nm + 6 + 12 * i; + if (platformID == ttUSHORT(fc+loc+0) && encodingID == ttUSHORT(fc+loc+2) + && languageID == ttUSHORT(fc+loc+4) && nameID == ttUSHORT(fc+loc+6)) { + *length = ttUSHORT(fc+loc+8); + return (const char *) (fc+stringOffset+ttUSHORT(fc+loc+10)); + } + } + return NULL; +} + +static int stbtt__matchpair(stbtt_uint8 *fc, stbtt_uint32 nm, stbtt_uint8 *name, stbtt_int32 nlen, stbtt_int32 target_id, stbtt_int32 next_id) +{ + stbtt_int32 i; + stbtt_int32 count = ttUSHORT(fc+nm+2); + stbtt_int32 stringOffset = nm + ttUSHORT(fc+nm+4); + + for (i=0; i < count; ++i) { + stbtt_uint32 loc = nm + 6 + 12 * i; + stbtt_int32 id = ttUSHORT(fc+loc+6); + if (id == target_id) { + // find the encoding + stbtt_int32 platform = ttUSHORT(fc+loc+0), encoding = ttUSHORT(fc+loc+2), language = ttUSHORT(fc+loc+4); + + // is this a Unicode encoding? + if (platform == 0 || (platform == 3 && encoding == 1) || (platform == 3 && encoding == 10)) { + stbtt_int32 slen = ttUSHORT(fc+loc+8); + stbtt_int32 off = ttUSHORT(fc+loc+10); + + // check if there's a prefix match + stbtt_int32 matchlen = stbtt__CompareUTF8toUTF16_bigendian_prefix(name, nlen, fc+stringOffset+off,slen); + if (matchlen >= 0) { + // check for target_id+1 immediately following, with same encoding & language + if (i+1 < count && ttUSHORT(fc+loc+12+6) == next_id && ttUSHORT(fc+loc+12) == platform && ttUSHORT(fc+loc+12+2) == encoding && ttUSHORT(fc+loc+12+4) == language) { + slen = ttUSHORT(fc+loc+12+8); + off = ttUSHORT(fc+loc+12+10); + if (slen == 0) { + if (matchlen == nlen) + return 1; + } else if (matchlen < nlen && name[matchlen] == ' ') { + ++matchlen; + if (stbtt_CompareUTF8toUTF16_bigendian_internal((char*) (name+matchlen), nlen-matchlen, (char*)(fc+stringOffset+off),slen)) + return 1; + } + } else { + // if nothing immediately following + if (matchlen == nlen) + return 1; + } + } + } + + // @TODO handle other encodings + } + } + return 0; +} + +static int stbtt__matches(stbtt_uint8 *fc, stbtt_uint32 offset, stbtt_uint8 *name, stbtt_int32 flags) +{ + stbtt_int32 nlen = (stbtt_int32) STBTT_strlen((char *) name); + stbtt_uint32 nm,hd; + if (!stbtt__isfont(fc+offset)) return 0; + + // check italics/bold/underline flags in macStyle... + if (flags) { + hd = stbtt__find_table(fc, offset, "head"); + if ((ttUSHORT(fc+hd+44) & 7) != (flags & 7)) return 0; + } + + nm = stbtt__find_table(fc, offset, "name"); + if (!nm) return 0; + + if (flags) { + // if we checked the macStyle flags, then just check the family and ignore the subfamily + if (stbtt__matchpair(fc, nm, name, nlen, 16, -1)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, -1)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; + } else { + if (stbtt__matchpair(fc, nm, name, nlen, 16, 17)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, 2)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; + } + + return 0; +} + +static int stbtt_FindMatchingFont_internal(unsigned char *font_collection, char *name_utf8, stbtt_int32 flags) +{ + stbtt_int32 i; + for (i=0;;++i) { + stbtt_int32 off = stbtt_GetFontOffsetForIndex(font_collection, i); + if (off < 0) return off; + if (stbtt__matches((stbtt_uint8 *) font_collection, off, (stbtt_uint8*) name_utf8, flags)) + return off; + } +} + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + +STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, + float pixel_height, unsigned char *pixels, int pw, int ph, + int first_char, int num_chars, stbtt_bakedchar *chardata) +{ + return stbtt_BakeFontBitmap_internal((unsigned char *) data, offset, pixel_height, pixels, pw, ph, first_char, num_chars, chardata); +} + +STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index) +{ + return stbtt_GetFontOffsetForIndex_internal((unsigned char *) data, index); +} + +STBTT_DEF int stbtt_GetNumberOfFonts(const unsigned char *data) +{ + return stbtt_GetNumberOfFonts_internal((unsigned char *) data); +} + +STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data, int offset) +{ + return stbtt_InitFont_internal(info, (unsigned char *) data, offset); +} + +STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *fontdata, const char *name, int flags) +{ + return stbtt_FindMatchingFont_internal((unsigned char *) fontdata, (char *) name, flags); +} + +STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2) +{ + return stbtt_CompareUTF8toUTF16_bigendian_internal((char *) s1, len1, (char *) s2, len2); +} + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + +#endif // STB_TRUETYPE_IMPLEMENTATION + + +// FULL VERSION HISTORY +// +// 1.19 (2018-02-11) OpenType GPOS kerning (horizontal only), STBTT_fmod +// 1.18 (2018-01-29) add missing function +// 1.17 (2017-07-23) make more arguments const; doc fix +// 1.16 (2017-07-12) SDF support +// 1.15 (2017-03-03) make more arguments const +// 1.14 (2017-01-16) num-fonts-in-TTC function +// 1.13 (2017-01-02) support OpenType fonts, certain Apple fonts +// 1.12 (2016-10-25) suppress warnings about casting away const with -Wcast-qual +// 1.11 (2016-04-02) fix unused-variable warning +// 1.10 (2016-04-02) allow user-defined fabs() replacement +// fix memory leak if fontsize=0.0 +// fix warning from duplicate typedef +// 1.09 (2016-01-16) warning fix; avoid crash on outofmem; use alloc userdata for PackFontRanges +// 1.08 (2015-09-13) document stbtt_Rasterize(); fixes for vertical & horizontal edges +// 1.07 (2015-08-01) allow PackFontRanges to accept arrays of sparse codepoints; +// allow PackFontRanges to pack and render in separate phases; +// fix stbtt_GetFontOFfsetForIndex (never worked for non-0 input?); +// fixed an assert() bug in the new rasterizer +// replace assert() with STBTT_assert() in new rasterizer +// 1.06 (2015-07-14) performance improvements (~35% faster on x86 and x64 on test machine) +// also more precise AA rasterizer, except if shapes overlap +// remove need for STBTT_sort +// 1.05 (2015-04-15) fix misplaced definitions for STBTT_STATIC +// 1.04 (2015-04-15) typo in example +// 1.03 (2015-04-12) STBTT_STATIC, fix memory leak in new packing, various fixes +// 1.02 (2014-12-10) fix various warnings & compile issues w/ stb_rect_pack, C++ +// 1.01 (2014-12-08) fix subpixel position when oversampling to exactly match +// non-oversampled; STBTT_POINT_SIZE for packed case only +// 1.00 (2014-12-06) add new PackBegin etc. API, w/ support for oversampling +// 0.99 (2014-09-18) fix multiple bugs with subpixel rendering (ryg) +// 0.9 (2014-08-07) support certain mac/iOS fonts without an MS platformID +// 0.8b (2014-07-07) fix a warning +// 0.8 (2014-05-25) fix a few more warnings +// 0.7 (2013-09-25) bugfix: subpixel glyph bug fixed in 0.5 had come back +// 0.6c (2012-07-24) improve documentation +// 0.6b (2012-07-20) fix a few more warnings +// 0.6 (2012-07-17) fix warnings; added stbtt_ScaleForMappingEmToPixels, +// stbtt_GetFontBoundingBox, stbtt_IsGlyphEmpty +// 0.5 (2011-12-09) bugfixes: +// subpixel glyph renderer computed wrong bounding box +// first vertex of shape can be off-curve (FreeSans) +// 0.4b (2011-12-03) fixed an error in the font baking example +// 0.4 (2011-12-01) kerning, subpixel rendering (tor) +// bugfixes for: +// codepoint-to-glyph conversion using table fmt=12 +// codepoint-to-glyph conversion using table fmt=4 +// stbtt_GetBakedQuad with non-square texture (Zer) +// updated Hello World! sample to use kerning and subpixel +// fixed some warnings +// 0.3 (2009-06-24) cmap fmt=12, compound shapes (MM) +// userdata, malloc-from-userdata, non-zero fill (stb) +// 0.2 (2009-03-11) Fix unsigned/signed char warnings +// 0.1 (2009-03-09) First public release +// + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ + +``` + +`README.md`: + +```md +# UE5 With Dear ImGui + +A sample Unreal Engine 5.0.1 C++ project that incorporates the [Dear ImGui graphical user interface library](https://github.com/ocornut/imgui). + +## YouTube Tutorial + +This project was built as part of [this 25 minute YouTube tutorial](https://youtu.be/qyO38jX5RU8). + +## Getting the Project Running + +* Clone the project to your machine. +* Right-click the `GuiTest.uproject` file and "Generate Visual Studio Files". +* Open the project by double-clicking the newly created GuiTest.sln file. (Assumes Visual Studio 2022 is installed.) +* Click the Visual Studio 2022 play button to run the project in Unreal Engine. + +## Dependencies and More Information + +* A copy of this [UnrealImGui Plugin Fork](https://github.com/benui-dev/UnrealImGui) is included with this repo. (Retrieved on May 15, 2022) +* For more info see [this code example](https://github.com/segross/UnrealImGui/issues/12#issuecomment-450865442). + +``` + +`Source/GuiTest.Target.cs`: + +```cs +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class GuiTestTarget : TargetRules +{ + public GuiTestTarget( TargetInfo Target) : base(Target) + { + Type = TargetType.Game; + DefaultBuildSettings = BuildSettingsVersion.V2; + ExtraModuleNames.AddRange( new string[] { "GuiTest" } ); + } +} + +``` + +`Source/GuiTest/GuiTest.Build.cs`: + +```cs +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class GuiTest : ModuleRules +{ + public GuiTest(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "ImGui" }); + + PrivateDependencyModuleNames.AddRange(new string[] { }); + + // Uncomment if you are using Slate UI + // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); + + // Uncomment if you are using online features + // PrivateDependencyModuleNames.Add("OnlineSubsystem"); + + // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true + } +} + +``` + +`Source/GuiTest/GuiTest.cpp`: + +```cpp +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "GuiTest.h" +#include "Modules/ModuleManager.h" + +IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, GuiTest, "GuiTest" ); + +``` + +`Source/GuiTest/GuiTest.h`: + +```h +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" + + +``` + +`Source/GuiTest/GuiTestGameModeBase.cpp`: + +```cpp +// Copyright Epic Games, Inc. All Rights Reserved. + + +#include "GuiTestGameModeBase.h" + + +``` + +`Source/GuiTest/GuiTestGameModeBase.h`: + +```h +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameModeBase.h" +#include "GuiTestGameModeBase.generated.h" + +/** + * + */ +UCLASS() +class GUITEST_API AGuiTestGameModeBase : public AGameModeBase +{ + GENERATED_BODY() + +}; + +``` + +`Source/GuiTest/Private/ImGuiActor.cpp`: + +```cpp +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "ImGuiActor.h" + +// This header is required to make use of Dear ImGui. +#include + +// Sets default values +AImGuiActor::AImGuiActor() { + // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. + PrimaryActorTick.bCanEverTick = true; + +} + +// Called when the game starts or when spawned +void AImGuiActor::BeginPlay() { + Super::BeginPlay(); + +} + +// ImGui windows need to be rendered every frame. +void AImGuiActor::Tick(float DeltaTime) { + Super::Tick(DeltaTime); + + // WARNING: Actor's only have labels in Development builds. + // If needed in a production build, each ImgGuiActor will need + // a different unique identifier for their ImgGui windows. + const FString Label = GetActorLabel(); + + ImGui::Begin(TCHAR_TO_ANSI(*Label), nullptr, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::SliderFloat("Scale", &Scale, 0.1f, 5.0f); + ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate); + ImGui::End(); + + // Update this actor's scale if the Scale variable has changed + // by way of the ImGui slider. + if (Scale != ScaleOld) { + ScaleOld = Scale; + SetActorScale3D(FVector3d{Scale}); + } +} + +``` + +`Source/GuiTest/Public/ImGuiActor.h`: + +```h +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "ImGuiActor.generated.h" + +UCLASS() +class GUITEST_API AImGuiActor : public AActor { + GENERATED_BODY() + +public: + // Sets default values for this actor's properties + AImGuiActor(); + +protected: + // Called when the game starts or when spawned + virtual void BeginPlay() override; + + float Scale{1.0}; + float ScaleOld{1.0}; + +public: + // Called every frame + virtual void Tick(float DeltaTime) override; + +}; + +``` + +`Source/GuiTestEditor.Target.cs`: + +```cs +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class GuiTestEditorTarget : TargetRules +{ + public GuiTestEditorTarget( TargetInfo Target) : base(Target) + { + Type = TargetType.Editor; + DefaultBuildSettings = BuildSettingsVersion.V2; + ExtraModuleNames.AddRange( new string[] { "GuiTest" } ); + } +} + +``` \ No newline at end of file